spectrum.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. import os
  2. from fastapi import FastAPI, Form
  3. from fastapi import HTTPException
  4. from fastapi.responses import FileResponse
  5. from fastapi.responses import JSONResponse
  6. from lib2to3.fixer_util import FromImport
  7. from scipy.fft import fft
  8. from scipy.signal import butter, sosfilt, decimate
  9. from state import create_session, get_session, update_session
  10. EXPORT_DIR = "exported_plots"
  11. os.makedirs(EXPORT_DIR, exist_ok=True)
  12. app = FastAPI()
  13. from fastapi import UploadFile, File
  14. from uuid import uuid4
  15. import json
  16. import base64
  17. import numpy as np
  18. import scipy.fft as fft
  19. import matplotlib.pyplot as plt
  20. class DataDecoder:
  21. def __init__(self, filename):
  22. self.data = ''
  23. with open(filename, 'r') as file:
  24. self.data = file.read()
  25. self.structed_data = json.loads(self.data)
  26. def getRawData(self, averaging_num=0, data_num=0, channel_num=0):
  27. for items in self.structed_data:
  28. if (items['averaging_num'] == averaging_num and items['data_num'] == data_num):
  29. for channel_data in items['channel_data']:
  30. if (channel_data['channel_num'] == channel_num):
  31. return channel_data['channel_data']
  32. return None
  33. def getDataDecoded(self, averaging_num=0, data_num=0, channel_num=0, points=10):
  34. rawData = self.getRawData(averaging_num, data_num, channel_num)
  35. edata = base64.b64decode(rawData.encode('utf-8'))
  36. arr = np.frombuffer(edata, dtype=np.int16, count=points, offset=0)
  37. return arr
  38. def getDataScaled(self, averaging_num=0, data_num=0, channel_num=0, points=10, range=5.0):
  39. decodedData = self.getDataDecoded(averaging_num, data_num, channel_num, points)
  40. return range * decodedData / 32768
  41. def getDataRate(self, averaging_num=0, data_num=0):
  42. for items in self.structed_data:
  43. if (items['averaging_num'] == averaging_num and items['data_num'] == data_num):
  44. return items['measurement_rate']
  45. def getDataSpectrum(self, averaging_num=0, data_num=0, channel_num=0, points=10, range=5.0, zero_fill=0,
  46. first_idx=0, last_idx=10):
  47. scaledData = self.getDataScaled(averaging_num, data_num, channel_num, points, range)
  48. zerofilledData = np.append(scaledData[first_idx:last_idx], np.zeros(zero_fill))
  49. rate = self.getDataRate()
  50. transferedData = fft.rfft(zerofilledData) * 2 / (last_idx - first_idx)
  51. freqs = fft.rfftfreq((last_idx - first_idx) + zero_fill, 1 / rate)
  52. spectrum = np.abs(transferedData)
  53. phases = np.angle(transferedData)
  54. spect_dict = {'freqs': freqs,
  55. 'phases': phases,
  56. 'spectrum': spectrum}
  57. return spect_dict
  58. def getDataPoints(self, averaging_num=0, data_num=0):
  59. for items in self.structed_data:
  60. if (items['averaging_num'] == averaging_num and items['data_num'] == data_num):
  61. return items['measurement_points']
  62. @app.post("/upload/")
  63. async def upload_json(file: UploadFile = File(...)):
  64. try:
  65. contents = await file.read()
  66. session_id = str(uuid4())
  67. filename = f"uploaded_{session_id}.json"
  68. path = os.path.join(EXPORT_DIR, filename)
  69. with open(path, "wb") as f:
  70. f.write(contents)
  71. decoder = DataDecoder(path)
  72. points = decoder.getDataPoints()
  73. if not points:
  74. raise HTTPException(status_code=400, detail="measurement_points not found")
  75. points = points[0]
  76. rate = decoder.getDataRate()
  77. if not rate:
  78. raise HTTPException(status_code=400, detail="measurement_rate not found")
  79. ch0 = decoder.getDataScaled(channel_num=0, points=points)
  80. ch1 = decoder.getDataScaled(channel_num=1, points=points)
  81. time = np.arange(len(ch0)) / rate
  82. session_id=create_session({
  83. "decoder_path": path,
  84. "ch0": ch0,
  85. "ch1": ch1,
  86. "time": time,
  87. "dt": 1 / rate,
  88. "rate": rate
  89. })
  90. return {"session_id": session_id}
  91. except Exception as e:
  92. import traceback
  93. traceback.print_exc()
  94. raise HTTPException(status_code=500, detail=f"Upload failed: {type(e).__name__}: {str(e)}")
  95. @app.post("/filter/")
  96. def apply_filters(session_id: str = Form(...), dt: float = Form(...),
  97. center_freq: float = Form(...),
  98. lower_freq: float = Form(...),
  99. higher_freq: float = Form(...),
  100. low_freq: float = Form(...)):
  101. state = get_session(session_id)
  102. if not state:
  103. raise HTTPException(status_code=404, detail="Session not found")
  104. # df = state["raw"]
  105. def round_sig(x, sig=4):
  106. if x == 0:
  107. return 0
  108. from math import log10, floor
  109. return round(x, sig - int(floor(log10(abs(x)))) - 1)
  110. # dt = round_sig((df['Время'][1] - df['Время'][0]) / (1000), 4)
  111. # # dt=df['Время'][1]-df['Время'][0]
  112. # fs = 1 / dt
  113. # target = df['Канал A'].to_numpy()
  114. # ch1 = df['Канал B'].to_numpy()
  115. time = state["raw"]["time"]
  116. ch0 = np.array(state["raw"]["ch0"])
  117. ch1 = np.array(state["raw"]["ch1"])
  118. dt = round_sig(time[1] - time[0], 4)
  119. target = ch0
  120. fs = 1 / dt
  121. points_num = len(target)
  122. n = int(len(ch1) / points_num)
  123. ch1 = np.vstack(np.split(ch1[:n * points_num], n))
  124. target = np.vstack(np.split(target[:n * points_num], n))
  125. bp = butter(4, [lower_freq, higher_freq], 'bp', fs=fs, output='sos')
  126. lp = butter(4, low_freq, 'lowpass', fs=fs, output='sos')
  127. ch1_f = sosfilt(bp, ch1, axis=1)
  128. target_f = sosfilt(bp, target, axis=1)
  129. update_session(session_id, "dt", dt)
  130. update_session(session_id, "center_freq", center_freq)
  131. update_session(session_id, "real", np.sin(2 * np.pi * center_freq * np.arange(0, points_num * dt, dt)))
  132. update_session(session_id, "imag", np.cos(2 * np.pi * center_freq * np.arange(0, points_num * dt, dt)))
  133. real_part = np.sin(2 * np.pi * center_freq * np.arange(0, ch1.shape[1] * dt, dt))
  134. imag_part = np.cos(2 * np.pi * center_freq * np.arange(0, ch1.shape[1] * dt, dt))
  135. ch1_real = ch1_f * real_part
  136. ch1_imag = ch1_f * imag_part
  137. ch1_real_f = sosfilt(lp, ch1_real, axis=1)
  138. ch1_imag_f = sosfilt(lp, ch1_imag, axis=1)
  139. ch1_complex = ch1_real_f + 1j * ch1_imag_f
  140. update_session(session_id, "ch1_f", ch1_complex)
  141. update_session(session_id, "dt", dt)
  142. return {"status": "filtered"}
  143. @app.post("/fft/")
  144. def compute_fft(session_id: str = Form(...),
  145. coef_dec_1: int = Form(10),
  146. coef_dec_2: int = Form(2),
  147. coef_dec_3: int = Form(1),
  148. coef_dec_4: int = Form(1)):
  149. state = get_session(session_id)
  150. if not state or "ch1_f" not in state:
  151. raise HTTPException(status_code=400, detail="Not filtered yet")
  152. try:
  153. signal = state["ch1_f"]
  154. decimated = decimate(signal, coef_dec_1, axis=1)
  155. decimated = decimate(decimated, coef_dec_2, axis=1)
  156. decimated = decimate(decimated, coef_dec_3, axis=1)
  157. decimated = decimate(decimated, coef_dec_4, axis=1)
  158. print(coef_dec_1)
  159. # decimated = decimate(decimated, 2, axis=1)
  160. slice_ = decimated[0][2500:]
  161. Td_new = state["dt"] * 10
  162. sp_row = np.abs(fft.fftshift(fft.fft(slice_)))
  163. BW = 1 / Td_new
  164. dff = BW / (len(slice_) - 1)
  165. freq = np.arange(-BW / 2, BW / 2 + dff, dff)[:len(sp_row)] / 1000
  166. update_session(session_id, "fft", {
  167. "fid": slice_.real.tolist(),
  168. "spectrum": sp_row.tolist(),
  169. "frequency": freq.tolist()
  170. })
  171. # print(len(decimated[0]))
  172. except Exception as e:
  173. import traceback
  174. traceback.print_exc()
  175. raise HTTPException(status_code=500, detail=f"Bad coeff: {type(e).__name__}: {str(e)}")
  176. return {"status": "fft done"}
  177. @app.get("/result/")
  178. def get_result(session_id: str):
  179. state = get_session(session_id)
  180. if not state or "fft" not in state:
  181. raise HTTPException(status_code=400, detail="FFT not yet computed")
  182. return JSONResponse(state["fft"])
  183. @app.post("/export/")
  184. def export_plots(session_id: str = Form(...)):
  185. state = get_session(session_id)
  186. if not state or "fft" not in state:
  187. raise HTTPException(status_code=400, detail="FFT not computed")
  188. fid = state["fft"]["fid"]
  189. spectrum = state["fft"]["spectrum"]
  190. freq = state["fft"]["frequency"]
  191. fid_path = os.path.join(EXPORT_DIR, f"{session_id}_fid.png")
  192. spectrum_path = os.path.join(EXPORT_DIR, f"{session_id}_spectrum.png")
  193. # print(session_id)
  194. # print("DONE")
  195. # ========== FID plot ==========
  196. plt.figure(figsize=(8, 4))
  197. plt.plot(fid)
  198. plt.title("FID")
  199. plt.xlabel("Samples")
  200. plt.ylabel("Amplitude")
  201. plt.grid()
  202. plt.tight_layout()
  203. plt.savefig(fid_path)
  204. plt.close()
  205. # ========== Spectrum plot =========
  206. plt.figure(figsize=(8, 4))
  207. plt.plot(freq, spectrum)
  208. plt.title("Spectrum")
  209. plt.xlabel("Frequency (kHz)")
  210. plt.ylabel("Magnitude")
  211. plt.grid()
  212. plt.tight_layout()
  213. plt.savefig(spectrum_path)
  214. plt.close()
  215. return {
  216. "fid_plot": f"/download/{session_id}_fid.png",
  217. "spectrum_plot": f"/download/{session_id}_spectrum.png"
  218. }
  219. @app.post("/plot-raw/")
  220. def plot_raw(session_id: str = Form(...)):
  221. state = get_session(session_id)
  222. if not state:
  223. raise HTTPException(status_code=404, detail="Session not found")
  224. ch0 = state["raw"]["ch0"]
  225. ch1 = state["raw"]["ch1"]
  226. fig, axs = plt.subplots(2, 1, figsize=(10, 5), sharex=True)
  227. axs[0].plot(ch0)
  228. axs[0].set_title("Канал A (весь сигнал)")
  229. axs[0].grid()
  230. axs[1].plot(ch1)
  231. axs[1].set_title("Канал B (весь сигнал)")
  232. axs[1].grid()
  233. plt.tight_layout()
  234. plot_path = os.path.join(EXPORT_DIR, f"{session_id}_raw_channels.png")
  235. plt.savefig(plot_path)
  236. plt.close()
  237. return {
  238. "raw_plot": f"/download/{session_id}_raw_channels.png"
  239. }
  240. @app.get("/download/{filename}")
  241. def download_file(filename: str):
  242. file_path = os.path.join(EXPORT_DIR, filename)
  243. if not os.path.exists(file_path):
  244. raise HTTPException(status_code=404, detail="File not found")
  245. return FileResponse(file_path, media_type="image/png", filename=filename)
  246. @app.post("/export-raw-data/")
  247. def export_raw_data(session_id: str = Form(...)):
  248. state = get_session(session_id)
  249. if not state:
  250. raise HTTPException(status_code=404, detail="Session not found")
  251. signal = state["raw"]["ch1"] # сырые данные без демодуляции и децимации
  252. print(signal)
  253. if isinstance(signal, np.ndarray):
  254. signal = signal.tolist()
  255. return JSONResponse(content={
  256. "status": "raw signal",
  257. "data": signal,
  258. "path": session_id
  259. })
  260. @app.post("/export-filter-data/")
  261. def export_filter_data(session_id: str = Form(...),
  262. center_freq: float = Form(...),
  263. lower_freq: float = Form(...),
  264. higher_freq: float = Form(...),
  265. low_freq: float = Form(...)):
  266. state = get_session(session_id)
  267. if not state:
  268. raise HTTPException(status_code=404, detail="Session not found")
  269. def round_sig(x, sig=4):
  270. if x == 0:
  271. return 0
  272. from math import log10, floor
  273. return round(x, sig - int(floor(log10(abs(x)))) - 1)
  274. time = state["raw"]["time"]
  275. ch0 = np.array(state["raw"]["ch0"])
  276. ch1 = np.array(state["raw"]["ch1"])
  277. dt = round_sig(time[1] - time[0], 4)
  278. target = ch0
  279. fs = 1 / dt
  280. points_num = len(target)
  281. n = int(len(ch1) / points_num)
  282. ch1 = np.vstack(np.split(ch1[:n * points_num], n))
  283. target = np.vstack(np.split(target[:n * points_num], n))
  284. bp = butter(4, [lower_freq, higher_freq], 'bp', fs=fs, output='sos')
  285. lp = butter(4, low_freq, 'lowpass', fs=fs, output='sos')
  286. ch1_f = sosfilt(bp, ch1, axis=1)
  287. target_f = sosfilt(bp, target, axis=1)
  288. update_session(session_id, "dt", dt)
  289. update_session(session_id, "center_freq", center_freq)
  290. update_session(session_id, "real", np.sin(2 * np.pi * center_freq * np.arange(0, points_num * dt, dt)))
  291. update_session(session_id, "imag", np.cos(2 * np.pi * center_freq * np.arange(0, points_num * dt, dt)))
  292. real_part = np.sin(2 * np.pi * center_freq * np.arange(0, ch1.shape[1] * dt, dt))
  293. imag_part = np.cos(2 * np.pi * center_freq * np.arange(0, ch1.shape[1] * dt, dt))
  294. ch1_real = ch1_f * real_part
  295. ch1_imag = ch1_f * imag_part
  296. ch1_real_f = sosfilt(lp, ch1_real, axis=1)
  297. ch1_imag_f = sosfilt(lp, ch1_imag, axis=1)
  298. ch1_complex = ch1_real_f + 1j * ch1_imag_f
  299. update_session(session_id, "ch1_f", ch1_complex)
  300. update_session(session_id, "dt", dt)
  301. # return JSONResponse(state["ch1_f"], state["dt"])
  302. return JSONResponse(content={
  303. "status": "filtered signal",
  304. "signal_real": np.array(ch1_complex).real.tolist(),
  305. "signal_imag": np.array(ch1_complex).imag.tolist(),
  306. "time_data_signal":dt.tolist(),
  307. "path": session_id
  308. })
  309. @app.post("/export-decdem-data/")
  310. def export_filter_data(session_id: str = Form(...)):
  311. state = get_session(session_id)
  312. if not state:
  313. raise HTTPException(status_code=400, detail="Session not found")
  314. if not state["ch1_f"]:
  315. raise HTTPException(status_code=404, detail="Do filter before")
  316. signal = state["ch1_f"]
  317. decimated = decimate(signal, 10, axis=1)
  318. decimated = decimate(decimated, 2, axis=1)
  319. update_session(session_id, "decimated", decimated)
  320. # return JSONResponse(decimated)
  321. return JSONResponse(content={
  322. "status": "dec and dem signal",
  323. "signal_real": np.array(decimated).real.tolist(),
  324. "signal_imag": np.array(decimated).imag.tolist(),
  325. "path": session_id
  326. })
  327. @app.post("/export-position-freq/")
  328. def position_frequency_axis(session_id: str = Form(...)):
  329. state = get_session(session_id)
  330. if not state:
  331. raise HTTPException(status_code=400, detail="Session not found")
  332. if not state["decimated"]:
  333. raise HTTPException(status_code=404, detail="Do decimate before")
  334. signal = state["decimated"]
  335. N = len(signal)
  336. fft_signal = np.fft.fft(signal)
  337. amplitude = np.abs(fft_signal)[:N//2] # только положительные частоты
  338. # индекс максимальной амплитуды
  339. peak_index = np.argmax(amplitude)
  340. peak_frequency = peak_index / N
  341. return JSONResponse(content={
  342. "status": "peak position",
  343. "peak max amplitude in freq": peak_frequency,
  344. "path": session_id
  345. })
  346. @app.post("/export-FWHM/")
  347. def find_FWHM(session_id: str = Form(...)):
  348. state = get_session(session_id)
  349. if not state:
  350. raise HTTPException(status_code=400, detail="Session not found")
  351. if not state["decimated"]:
  352. raise HTTPException(status_code=404, detail="Do decimate before")
  353. signal = state["decimated"]
  354. N = len(signal)
  355. fft_signal = np.fft.fft(signal)
  356. amplitude = np.abs(fft_signal)[:N//2] # положительные частоты
  357. max_amplitude = np.max(amplitude)
  358. half_max = max_amplitude / 2
  359. indices_above_half_max = np.where(amplitude >= half_max)[0]
  360. if len(indices_above_half_max) < 2:
  361. raise HTTPException(status_code=200, detail="Bad signal, not found FWHM")
  362. FWHM_index_range = indices_above_half_max[-1] - indices_above_half_max[0]
  363. FWHM_normalized = FWHM_index_range / N
  364. return JSONResponse(content={
  365. "status": "FWHM",
  366. "width at half height": FWHM_normalized,
  367. "path": session_id
  368. })
  369. @app.post("/export-max-amplitude-freq/")
  370. def max_amplitude_time_domain(session_id: str = Form(...)):
  371. state = get_session(session_id)
  372. if not state:
  373. raise HTTPException(status_code=400, detail="Session not found")
  374. if not state["decimated"]:
  375. raise HTTPException(status_code=404, detail="Do decimate before")
  376. signal = state["decimated"]
  377. max_amplitude = np.max(np.abs(signal))
  378. return JSONResponse(content={
  379. "status": "max amplitude freq",
  380. "max amplitude": max_amplitude,
  381. "path": session_id
  382. })