| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727 |
- from __future__ import annotations
- import json
- import math
- import os
- from typing import Any, Callable, Dict, Optional, Tuple
- import requests
- from PyQt6.QtCore import QRectF, Qt
- from PyQt6.QtGui import QPainter, QPixmap
- from PyQt6.QtWidgets import (
- QFileDialog,
- QGridLayout,
- QGroupBox,
- QHBoxLayout,
- QLabel,
- QLineEdit,
- QMessageBox,
- QPushButton,
- QPlainTextEdit,
- QVBoxLayout,
- QWidget,
- )
- from config import HTTP_TIMEOUT_SEC
- from mainui.clients import LoggedAnalysisApi
- from mainui.widgets import InteractiveImageView
- from mainui.worker import run_in_thread
- class AnalysisTabMixin:
- def _analysis_base_url(self) -> str:
- return (self.analysis.base_url or "").rstrip("/")
- def _analysis_api(self) -> LoggedAnalysisApi:
- base = self._analysis_base_url()
- self.analysis.base_url = base
- return LoggedAnalysisApi(base_url=base, timeout=HTTP_TIMEOUT_SEC)
- def _init_analysis_panel(self) -> None:
- root_layout = self.spec.layout()
- if root_layout is None:
- root_layout = QVBoxLayout(self.spec)
- self.spec.setLayout(root_layout)
- gb = QGroupBox("Analysis service (REST)")
- gl = QGridLayout(gb)
- self.leAnalysisFile = QLineEdit("")
- self.leAnalysisFile.setPlaceholderText("Select CSV to upload...")
- btnBrowse = QPushButton("Browse...")
- btnBrowse.clicked.connect(self.analysis_pick_file)
- gl.addWidget(QLabel("File:"), 0, 0)
- gl.addWidget(self.leAnalysisFile, 0, 1, 1, 4)
- gl.addWidget(btnBrowse, 0, 5)
- self.leSessionId = QLineEdit("")
- self.leSessionId.setReadOnly(True)
- self.leSessionId.setPlaceholderText("auto (after upload)")
- btnUpload = QPushButton("Upload")
- btnUpload.clicked.connect(self.analysis_upload)
- gl.addWidget(QLabel("Session:"), 1, 0)
- gl.addWidget(self.leSessionId, 1, 1, 1, 4)
- gl.addWidget(btnUpload, 1, 5)
- # filter params
- # Defaults aligned with test_spectrum/test.py
- self.leDt = QLineEdit("1.125e-7")
- self.leCenter = QLineEdit("2.97e6")
- self.leLo = QLineEdit("2.92e6")
- self.leHi = QLineEdit("3.02e6")
- self.leLow = QLineEdit("600e3")
- gl.addWidget(QLabel("dt (s)"), 2, 0)
- gl.addWidget(self.leDt, 2, 1)
- gl.addWidget(QLabel("center (Hz)"), 2, 2)
- gl.addWidget(self.leCenter, 2, 3)
- gl.addWidget(QLabel("lo/hi/low (Hz)"), 2, 4)
- w_f = QWidget()
- hl = QHBoxLayout()
- hl.setContentsMargins(0, 0, 0, 0)
- for le in (self.leLo, self.leHi, self.leLow):
- le.setFixedWidth(90)
- hl.addWidget(le)
- w_f.setLayout(hl)
- gl.addWidget(w_f, 2, 5)
- btnFilter = QPushButton("Filter")
- btnFilter.clicked.connect(self.analysis_filter)
- gl.addWidget(btnFilter, 3, 5)
- # fft params
- self.leC1 = QLineEdit("")
- self.leC2 = QLineEdit("")
- self.leC3 = QLineEdit("")
- self.leC4 = QLineEdit("")
- self.leC1.setPlaceholderText("10")
- self.leC2.setPlaceholderText("2")
- self.leC3.setPlaceholderText("1")
- self.leC4.setPlaceholderText("1")
- gl.addWidget(QLabel("FFT dec coefs c1..c4"), 3, 0)
- w_fft = QWidget()
- hl2 = QHBoxLayout()
- hl2.setContentsMargins(0, 0, 0, 0)
- for le in (self.leC1, self.leC2, self.leC3, self.leC4):
- le.setFixedWidth(55)
- hl2.addWidget(le)
- w_fft.setLayout(hl2)
- gl.addWidget(w_fft, 3, 1, 1, 4)
- btnFFT = QPushButton("FFT")
- btnFFT.clicked.connect(self.analysis_fft)
- gl.addWidget(btnFFT, 4, 5)
- btnResult = QPushButton("Get result")
- btnPlotRaw = QPushButton("Plot raw")
- btnExport = QPushButton("Export plots")
- btnResult.clicked.connect(self.analysis_result)
- btnPlotRaw.clicked.connect(self.analysis_plot_raw)
- btnExport.clicked.connect(self.analysis_export)
- gl.addWidget(btnResult, 4, 0)
- gl.addWidget(btnPlotRaw, 4, 1)
- gl.addWidget(btnExport, 4, 2)
- self.pteAnalysisLog = QPlainTextEdit()
- self.pteAnalysisLog.setReadOnly(True)
- self.pteAnalysisLog.setMaximumBlockCount(2000)
- gl.addWidget(self.pteAnalysisLog, 5, 0, 1, 6)
- root_layout.insertWidget(0, gb)
- self.lblAnalysisPing = QLabel("* Analysis service: unknown")
- self.lblAnalysisPing.setStyleSheet("color: #808080;")
- root_layout.addWidget(self.lblAnalysisPing)
- self.lblRawImg = self._ensure_image_view(self.spec.timePlotContainer, "RAW")
- self.lblSpecImg = self._ensure_image_view(self.spec.spectrumPlotContainer, "SPEC")
- self.lblExtraImg = self._ensure_image_view(self.spec.demodPlotContainer, "EXTRA")
- if hasattr(self.spec, "gbTimePlot"):
- self.spec.gbTimePlot.setMinimumHeight(460)
- if hasattr(self.spec, "gbSpectrumPlot"):
- self.spec.gbSpectrumPlot.setMinimumHeight(460)
- if hasattr(self.spec, "gbDemodPlot"):
- self.spec.gbDemodPlot.setMinimumHeight(380)
- if hasattr(self.spec, "gbTimeDomain"):
- self.spec.gbTimeDomain.setVisible(False)
- if hasattr(self.spec, "gbDemod"):
- self.spec.gbDemod.setVisible(False)
- if hasattr(self.spec, "btnPlotOrigSpec"):
- self.spec.btnPlotOrigSpec.setVisible(False)
- if hasattr(self.spec, "btnPlotDemodSpec"):
- self.spec.btnPlotDemodSpec.setVisible(False)
- if hasattr(self.spec, "gbDemodPlot"):
- self.spec.gbDemodPlot.setVisible(False)
- if hasattr(self.spec, "btnDemodulate"):
- self.spec.btnDemodulate.clicked.connect(self._show_demod_plot)
- if hasattr(self.spec, "vlFreq"):
- row1 = QHBoxLayout()
- row2 = QHBoxLayout()
- self.btnExportRawData = QPushButton("Raw data")
- self.btnExportFilterData = QPushButton("Filter data")
- self.btnExportDecDemData = QPushButton("DecDem data")
- self.btnPeakFreq = QPushButton("Peak freq")
- self.btnFwhm = QPushButton("FWHM")
- self.btnMaxAmp = QPushButton("Max amp")
- self.btnExportDecDemData.setEnabled(False)
- self.btnPeakFreq.setEnabled(False)
- self.btnFwhm.setEnabled(False)
- self.btnMaxAmp.setEnabled(False)
- self.btnExportDecDemData.setToolTip("Disabled: endpoint is broken in current spectrum.py backend")
- self.btnPeakFreq.setToolTip("Disabled: depends on broken /export-decdem-data/")
- self.btnFwhm.setToolTip("Disabled: depends on broken /export-decdem-data/")
- self.btnMaxAmp.setToolTip("Disabled: depends on broken /export-decdem-data/")
- row1.addWidget(self.btnExportRawData)
- row1.addWidget(self.btnExportFilterData)
- row1.addWidget(self.btnExportDecDemData)
- row2.addWidget(self.btnPeakFreq)
- row2.addWidget(self.btnFwhm)
- row2.addWidget(self.btnMaxAmp)
- self.spec.vlFreq.addLayout(row1)
- self.spec.vlFreq.addLayout(row2)
- self.btnExportRawData.clicked.connect(self.analysis_export_raw_data)
- self.btnExportFilterData.clicked.connect(self.analysis_export_filter_data)
- self.btnExportDecDemData.clicked.connect(self.analysis_export_decdem_data)
- self.btnPeakFreq.clicked.connect(self.analysis_export_position_freq)
- self.btnFwhm.clicked.connect(self.analysis_export_fwhm)
- self.btnMaxAmp.clicked.connect(self.analysis_export_max_amplitude_freq)
- self.lblSpectrumMetrics = QLabel("Spectrum metrics: -")
- self.lblSpectrumMetrics.setStyleSheet("color: #505050;")
- root_layout.addWidget(self.lblSpectrumMetrics)
- self.spec_log(f"[ANALYSIS] ready. base={self.analysis.base_url}")
- self.analysis_ping_timer.start()
- self._ping_analysis_service()
- def _ensure_image_view(self, container: QWidget, title: str) -> InteractiveImageView:
- lay = container.layout()
- if lay is None:
- lay = QVBoxLayout(container)
- container.setLayout(lay)
- view = InteractiveImageView(title=title, parent=container)
- lay.addWidget(view)
- return view
- def _show_demod_plot(self) -> None:
- if hasattr(self.spec, "gbDemodPlot"):
- self.spec.gbDemodPlot.setVisible(True)
- def _to_float_list(self, value: Any) -> list[float]:
- if isinstance(value, (list, tuple)):
- out: list[float] = []
- for v in value:
- try:
- fv = float(v)
- if math.isfinite(fv):
- out.append(fv)
- except Exception:
- continue
- return out
- try:
- fv = float(value)
- return [fv] if math.isfinite(fv) else []
- except Exception:
- return []
- def _render_line_plot(
- self,
- view: InteractiveImageView,
- title: str,
- y_data: list[float],
- x_data: Optional[list[float]] = None,
- ) -> None:
- if not y_data:
- view.set_placeholder(f"{title}: no numeric data")
- return
- n = len(y_data)
- if x_data is None or len(x_data) != n:
- x_data = [float(i) for i in range(n)]
- max_points = 4000
- if n > max_points:
- step = int(math.ceil(n / max_points))
- y_data = y_data[::step]
- x_data = x_data[::step]
- xmin, xmax = min(x_data), max(x_data)
- ymin, ymax = min(y_data), max(y_data)
- if xmax <= xmin:
- xmax = xmin + 1.0
- if ymax <= ymin:
- ymax = ymin + 1.0
- w, h, m = 1200, 420, 32
- pix = QPixmap(w, h)
- pix.fill(Qt.GlobalColor.white)
- p = QPainter(pix)
- p.setRenderHint(QPainter.RenderHint.Antialiasing, True)
- p.setPen(Qt.GlobalColor.black)
- p.drawRect(m, m, w - 2 * m, h - 2 * m)
- plot_w = float(w - 2 * m)
- plot_h = float(h - 2 * m)
- last = None
- p.setPen(Qt.GlobalColor.blue)
- for x, y in zip(x_data, y_data):
- px = m + (x - xmin) * plot_w / (xmax - xmin)
- py = m + (ymax - y) * plot_h / (ymax - ymin)
- if last is not None:
- p.drawLine(int(last[0]), int(last[1]), int(px), int(py))
- last = (px, py)
- p.setPen(Qt.GlobalColor.darkGray)
- p.drawText(QRectF(m, 4, w - 2 * m, 22), title)
- p.end()
- view.set_pixmap(pix)
- def _set_spectrum_metrics(self, text: str) -> None:
- if hasattr(self, "lblSpectrumMetrics"):
- self.lblSpectrumMetrics.setText(f"Spectrum metrics: {text}")
- def _set_analysis_ping(self, ok: Optional[bool], msg: str = "") -> None:
- if not hasattr(self, "lblAnalysisPing"):
- return
- base = self._analysis_base_url()
- if ok is None:
- self.lblAnalysisPing.setText(f"* Analysis service: checking ({base})")
- self.lblAnalysisPing.setStyleSheet("color: #808080;")
- elif ok:
- self.lblAnalysisPing.setText(f"* Analysis service: online ({base})")
- self.lblAnalysisPing.setStyleSheet("color: #2e7d32;")
- else:
- tail = f" - {msg}" if msg else ""
- self.lblAnalysisPing.setText(f"* Analysis service: offline ({base}){tail}")
- self.lblAnalysisPing.setStyleSheet("color: #c62828;")
- def _ping_analysis_service(self) -> None:
- self._set_analysis_ping(None)
- base = self._analysis_base_url()
- def job() -> Tuple[bool, str]:
- try:
- r = requests.get(base, timeout=2.0)
- return True, f"HTTP {r.status_code}"
- except Exception as e:
- return False, str(e)
- def ok(result: Tuple[bool, str]) -> None:
- state, msg = result
- self._set_analysis_ping(state, msg if not state else "")
- def err(msg: str) -> None:
- self._set_analysis_ping(False, msg)
- run_in_thread(self, job, ok, err)
- def analysis_pick_file(self) -> None:
- path, _ = QFileDialog.getOpenFileName(
- self,
- "Select file for analysis",
- "",
- "Data (*.csv *.json);;CSV (*.csv);;JSON (*.json);;All files (*)",
- )
- if not path:
- return
- self.leAnalysisFile.setText(path)
- self.analysis.last_uploaded_path = path
- self.spec_log(f"[ANALYSIS] file selected: {path}")
- def _get_analysis_upload_path(self) -> Optional[str]:
- path = self.leAnalysisFile.text().strip() or self.analysis.last_uploaded_path
- if not path or not os.path.exists(path):
- QMessageBox.warning(self, "Analysis", "Select a valid file to upload.")
- return None
- if os.path.getsize(path) == 0:
- QMessageBox.warning(self, "Analysis", "Selected file is empty.")
- return None
- if path.lower().endswith(".json"):
- try:
- with open(path, "r", encoding="utf-8") as f:
- json.load(f)
- except Exception as e:
- QMessageBox.warning(self, "Analysis", f"Invalid JSON file: {e}")
- return None
- return path
- def _ensure_session_id(self, on_ready: Callable[[str], None]) -> None:
- sid = (self.analysis.session_id or self.leSessionId.text().strip())
- if sid:
- on_ready(str(sid))
- return
- path = self._get_analysis_upload_path()
- if not path:
- return
- def job() -> str:
- api = self._analysis_api()
- return api.upload(path)
- def ok(session_id: str) -> None:
- self.analysis.session_id = session_id
- self.leSessionId.setText(session_id)
- self.spec_log(f"[ANALYSIS] uploaded. session_id={session_id}")
- on_ready(session_id)
- def err(msg: str) -> None:
- self.spec_log(f"[ANALYSIS] upload error: {msg}")
- self.spec_log("[ANALYSIS] session_id missing, uploading automatically...")
- run_in_thread(self, job, ok, err)
- def analysis_upload(self) -> None:
- path = self._get_analysis_upload_path()
- if not path:
- return
- def job() -> str:
- api = self._analysis_api()
- return api.upload(path)
- def ok(session_id: str) -> None:
- self.analysis.session_id = session_id
- self.leSessionId.setText(session_id)
- self.spec_log(f"[ANALYSIS] uploaded. session_id={session_id}")
- def err(msg: str) -> None:
- self.spec_log(f"[ANALYSIS] upload error: {msg}")
- self.spec_log("[ANALYSIS] uploading...")
- run_in_thread(self, job, ok, err)
- def analysis_filter(self) -> None:
- try:
- dt = float(self.leDt.text().strip())
- center = float(self.leCenter.text().strip())
- lo = float(self.leLo.text().strip())
- hi = float(self.leHi.text().strip())
- low = float(self.leLow.text().strip())
- except Exception:
- QMessageBox.warning(self, "Analysis", "Invalid filter parameters.")
- return
- def proceed(sid: str) -> None:
- def job() -> str:
- api = self._analysis_api()
- api.filter(session_id=sid, dt=dt, center=center, lo=lo, hi=hi, low=low)
- return "ok"
- def ok(_res: str) -> None:
- self.spec_log("[ANALYSIS] filter done.")
- def err(msg: str) -> None:
- self.spec_log(f"[ANALYSIS] filter error: {msg}")
- self.spec_log("[ANALYSIS] filtering...")
- run_in_thread(self, job, ok, err)
- self._ensure_session_id(proceed)
- def analysis_fft(self) -> None:
- c1s = self.leC1.text().strip()
- c2s = self.leC2.text().strip()
- c3s = self.leC3.text().strip()
- c4s = self.leC4.text().strip()
- c1: Optional[int] = None
- c2: Optional[int] = None
- c3: Optional[int] = None
- c4: Optional[int] = None
- if any((c1s, c2s, c3s, c4s)):
- if not all((c1s, c2s, c3s, c4s)):
- QMessageBox.warning(self, "Analysis", "Fill all FFT coefficients or leave all empty.")
- return
- try:
- c1 = int(c1s)
- c2 = int(c2s)
- c3 = int(c3s)
- c4 = int(c4s)
- except Exception:
- QMessageBox.warning(self, "Analysis", "Invalid FFT coefficients.")
- return
- def proceed(sid: str) -> None:
- def job() -> str:
- api = self._analysis_api()
- api.fft(session_id=sid, c1=c1, c2=c2, c3=c3, c4=c4)
- return "ok"
- def ok(_res: str) -> None:
- self.spec_log("[ANALYSIS] fft done.")
- def err(msg: str) -> None:
- self.spec_log(f"[ANALYSIS] fft error: {msg}")
- self.spec_log("[ANALYSIS] fft...")
- run_in_thread(self, job, ok, err)
- self._ensure_session_id(proceed)
- def analysis_result(self) -> None:
- def proceed(sid: str) -> None:
- def job() -> Dict[str, Any]:
- api = self._analysis_api()
- return api.result(session_id=sid)
- def ok(data: Dict[str, Any]) -> None:
- self.spec_log("[ANALYSIS] result:")
- self.spec_log(json.dumps(data, ensure_ascii=False, indent=2)[:5000])
- def err(msg: str) -> None:
- self.spec_log(f"[ANALYSIS] result error: {msg}")
- run_in_thread(self, job, ok, err)
- self._ensure_session_id(proceed)
- def analysis_export(self) -> None:
- def proceed(sid: str) -> None:
- def job() -> Dict[str, Any]:
- api = self._analysis_api()
- return api.export_plots(session_id=sid)
- def ok(data: Dict[str, Any]) -> None:
- self.spec_log("[ANALYSIS] export response received.")
- self.spec_log(json.dumps(data, ensure_ascii=False, indent=2)[:5000])
- self._try_render_images_from_response(data)
- def err(msg: str) -> None:
- self.spec_log(f"[ANALYSIS] export error: {msg}")
- run_in_thread(self, job, ok, err)
- self._ensure_session_id(proceed)
- def analysis_plot_raw(self) -> None:
- def proceed(sid: str) -> None:
- def job() -> Dict[str, Any]:
- api = self._analysis_api()
- return api.plot_raw(session_id=sid)
- def ok(data: Dict[str, Any]) -> None:
- self.spec_log("[ANALYSIS] plot-raw response received.")
- self.spec_log(json.dumps(data, ensure_ascii=False, indent=2)[:5000])
- self._try_render_images_from_response(data)
- def err(msg: str) -> None:
- self.spec_log(f"[ANALYSIS] plot-raw error: {msg}")
- run_in_thread(self, job, ok, err)
- self._ensure_session_id(proceed)
- def _run_filter_fft_and_then(self, sid: str, on_ready: Callable[[], None]) -> None:
- try:
- dt = float(self.leDt.text().strip())
- center = float(self.leCenter.text().strip())
- lo = float(self.leLo.text().strip())
- hi = float(self.leHi.text().strip())
- low = float(self.leLow.text().strip())
- except Exception:
- QMessageBox.warning(self, "Analysis", "Invalid filter parameters.")
- return
- c1s = self.leC1.text().strip()
- c2s = self.leC2.text().strip()
- c3s = self.leC3.text().strip()
- c4s = self.leC4.text().strip()
- c1 = c2 = c3 = c4 = None
- if any([c1s, c2s, c3s, c4s]):
- if not all([c1s, c2s, c3s, c4s]):
- QMessageBox.warning(self, "Analysis", "Fill all FFT coefficients or leave all empty.")
- return
- try:
- c1, c2, c3, c4 = int(c1s), int(c2s), int(c3s), int(c4s)
- except Exception:
- QMessageBox.warning(self, "Analysis", "Invalid FFT coefficients.")
- return
- def job() -> None:
- api = self._analysis_api()
- api.filter(session_id=sid, dt=dt, center=center, lo=lo, hi=hi, low=low)
- api.fft(session_id=sid, c1=c1, c2=c2, c3=c3, c4=c4)
- def ok(_: Any) -> None:
- self.spec_log("[ANALYSIS] filter+fft ready.")
- on_ready()
- def err(msg: str) -> None:
- self.spec_log(f"[ANALYSIS] filter+fft error: {msg}")
- self.spec_log("[ANALYSIS] running filter+fft before metric...")
- run_in_thread(self, job, ok, err)
- def analysis_export_raw_data(self) -> None:
- def proceed(sid: str) -> None:
- def job() -> Dict[str, Any]:
- api = self._analysis_api()
- return api.export_raw_data(session_id=sid)
- def ok(data: Dict[str, Any]) -> None:
- y = self._to_float_list(data.get("data"))
- self._render_line_plot(self.lblRawImg, "Raw signal", y)
- self.spec_log(f"[ANALYSIS] export-raw-data done. points={len(y)}")
- def err(msg: str) -> None:
- self.spec_log(f"[ANALYSIS] export-raw-data error: {msg}")
- run_in_thread(self, job, ok, err)
- self._ensure_session_id(proceed)
- def analysis_export_filter_data(self) -> None:
- def proceed(sid: str) -> None:
- try:
- center = float(self.leCenter.text().strip())
- lo = float(self.leLo.text().strip())
- hi = float(self.leHi.text().strip())
- low = float(self.leLow.text().strip())
- except Exception:
- QMessageBox.warning(self, "Analysis", "Invalid filter frequencies.")
- return
- def job() -> Dict[str, Any]:
- api = self._analysis_api()
- return api.export_filter_data(
- session_id=sid,
- center_freq=center,
- lower_freq=lo,
- higher_freq=hi,
- low_freq=low,
- )
- def ok(data: Dict[str, Any]) -> None:
- y = self._to_float_list(data.get("signal_real"))
- x = self._to_float_list(data.get("time_data_signal"))
- x_use = x if len(x) == len(y) else None
- self._render_line_plot(self.lblRawImg, "Filtered signal (real)", y, x_use)
- self.spec_log(f"[ANALYSIS] export-filter-data done. points={len(y)}")
- def err(msg: str) -> None:
- self.spec_log(f"[ANALYSIS] export-filter-data error: {msg}")
- run_in_thread(self, job, ok, err)
- self._ensure_session_id(proceed)
- def analysis_export_decdem_data(self) -> None:
- def proceed(sid: str) -> None:
- def job() -> Dict[str, Any]:
- api = self._analysis_api()
- return api.export_decdem_data(session_id=sid)
- def ok(data: Dict[str, Any]) -> None:
- y = self._to_float_list(data.get("signal_real"))
- self._render_line_plot(self.lblExtraImg, "Dec/Dem signal (real)", y)
- self._show_demod_plot()
- self.spec_log(f"[ANALYSIS] export-decdem-data done. points={len(y)}")
- def err(msg: str) -> None:
- self.spec_log(f"[ANALYSIS] export-decdem-data error: {msg}")
- run_in_thread(self, job, ok, err)
- self._ensure_session_id(proceed)
- def analysis_export_position_freq(self) -> None:
- def proceed(sid: str) -> None:
- self._run_metric_export(
- sid=sid,
- api_call=lambda api: api.export_position_freq(session_id=sid),
- value_key="peak max amplitude in freq",
- metric_prefix="peak freq",
- log_name="export-position-freq",
- )
- self._ensure_session_id(proceed)
- def analysis_export_fwhm(self) -> None:
- def proceed(sid: str) -> None:
- self._run_metric_export(
- sid=sid,
- api_call=lambda api: api.export_fwhm(session_id=sid),
- value_key="width at half height",
- metric_prefix="FWHM",
- log_name="export-FWHM",
- )
- self._ensure_session_id(proceed)
- def analysis_export_max_amplitude_freq(self) -> None:
- def proceed(sid: str) -> None:
- self._run_metric_export(
- sid=sid,
- api_call=lambda api: api.export_max_amplitude_freq(session_id=sid),
- value_key="max amplitude",
- metric_prefix="max amp",
- log_name="export-max-amplitude-freq",
- )
- self._ensure_session_id(proceed)
- def _run_metric_export(
- self,
- *,
- sid: str,
- api_call: Callable[[LoggedAnalysisApi], Dict[str, Any]],
- value_key: str,
- metric_prefix: str,
- log_name: str,
- ) -> None:
- def run_metric() -> None:
- def job() -> Dict[str, Any]:
- return api_call(self._analysis_api())
- def ok(data: Dict[str, Any]) -> None:
- val = data.get(value_key, "n/a")
- self._set_spectrum_metrics(f"{metric_prefix}={val}")
- self.spec_log(f"[ANALYSIS] {log_name}: {val}")
- def err(msg: str) -> None:
- self.spec_log(f"[ANALYSIS] {log_name} error: {msg}")
- run_in_thread(self, job, ok, err)
- self._run_filter_fft_and_then(sid, run_metric)
- def _try_render_images_from_response(self, data: Dict[str, Any]) -> None:
- if not isinstance(data, dict):
- return
- urls = []
- for k, v in data.items():
- if isinstance(v, str) and v.startswith("/"):
- urls.append((k, v))
- elif isinstance(v, dict):
- for k2, v2 in v.items():
- if isinstance(v2, str) and v2.startswith("/"):
- urls.append((f"{k}.{k2}", v2))
- if not urls:
- self.spec_log("[ANALYSIS] No image urls found in response.")
- return
- targets = [
- (self.lblRawImg, "RAW"),
- (self.lblSpecImg, "SPEC"),
- (self.lblExtraImg, "EXTRA"),
- ]
- for (lbl, tag), (name, url) in zip(targets, urls[:3]):
- self.spec_log(f"[ANALYSIS] rendering {name}: {url}")
- self._download_and_show_image(url, lbl, tag)
- def _download_and_show_image(self, rel_url: str, target_lbl: InteractiveImageView, tag: str) -> None:
- def job() -> bytes:
- api = self._analysis_api()
- return api.download_bytes(rel_url)
- def ok(content: bytes) -> None:
- target_lbl.set_image_bytes(content)
- def err(msg: str) -> None:
- target_lbl.set_placeholder(f"{tag}: download error")
- self.spec_log(f"[ANALYSIS] download error: {msg}")
- run_in_thread(self, job, ok, err)
|