new
This commit is contained in:
5
rfg_adc_plotter/gui/__init__.py
Normal file
5
rfg_adc_plotter/gui/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""GUI backends."""
|
||||
|
||||
from rfg_adc_plotter.gui.pyqtgraph_backend import run_pyqtgraph
|
||||
|
||||
__all__ = ["run_pyqtgraph"]
|
||||
913
rfg_adc_plotter/gui/pyqtgraph_backend.py
Normal file
913
rfg_adc_plotter/gui/pyqtgraph_backend.py
Normal file
@ -0,0 +1,913 @@
|
||||
"""PyQtGraph realtime backend for the ADC sweep plotter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import signal
|
||||
import threading
|
||||
import time
|
||||
from queue import Empty, Queue
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from rfg_adc_plotter.constants import FFT_LEN, SWEEP_FREQ_MAX_GHZ, SWEEP_FREQ_MIN_GHZ
|
||||
from rfg_adc_plotter.io.sweep_reader import SweepReader
|
||||
from rfg_adc_plotter.processing.calibration import (
|
||||
calibrate_freqs,
|
||||
get_calibration_base,
|
||||
get_calibration_coeffs,
|
||||
set_calibration_base_value,
|
||||
)
|
||||
from rfg_adc_plotter.processing.fft import compute_fft_row, fft_mag_to_db
|
||||
from rfg_adc_plotter.processing.formatting import compute_auto_ylim, format_status_kv, parse_spec_clip
|
||||
from rfg_adc_plotter.processing.normalization import normalize_by_calib
|
||||
from rfg_adc_plotter.processing.peaks import (
|
||||
find_peak_width_markers,
|
||||
find_top_peaks_over_ref,
|
||||
rolling_median_ref,
|
||||
)
|
||||
from rfg_adc_plotter.state import RingBuffer, RuntimeState
|
||||
from rfg_adc_plotter.types import SweepAuxCurves, SweepInfo, SweepPacket
|
||||
|
||||
|
||||
def _visible_levels_pyqtgraph(data: np.ndarray, plot_item) -> Optional[Tuple[float, float]]:
|
||||
"""Compute vmin/vmax from the currently visible part of an ImageItem."""
|
||||
if data.size == 0:
|
||||
return None
|
||||
ny, nx = data.shape[0], data.shape[1]
|
||||
try:
|
||||
(x0, x1), (y0, y1) = plot_item.viewRange()
|
||||
except Exception:
|
||||
x0, x1 = 0.0, float(nx - 1)
|
||||
y0, y1 = 0.0, float(ny - 1)
|
||||
xmin, xmax = sorted((float(x0), float(x1)))
|
||||
ymin, ymax = sorted((float(y0), float(y1)))
|
||||
ix0 = max(0, min(nx - 1, int(np.floor(xmin))))
|
||||
ix1 = max(0, min(nx - 1, int(np.ceil(xmax))))
|
||||
iy0 = max(0, min(ny - 1, int(np.floor(ymin))))
|
||||
iy1 = max(0, min(ny - 1, int(np.ceil(ymax))))
|
||||
if ix1 < ix0:
|
||||
ix1 = ix0
|
||||
if iy1 < iy0:
|
||||
iy1 = iy0
|
||||
sub = data[iy0 : iy1 + 1, ix0 : ix1 + 1]
|
||||
finite = np.isfinite(sub)
|
||||
if not finite.any():
|
||||
return None
|
||||
vals = sub[finite]
|
||||
vmin = float(np.nanpercentile(vals, 5))
|
||||
vmax = float(np.nanpercentile(vals, 95))
|
||||
if not (np.isfinite(vmin) and np.isfinite(vmax)) or vmin == vmax:
|
||||
return None
|
||||
return (vmin, vmax)
|
||||
|
||||
|
||||
def run_pyqtgraph(args) -> None:
|
||||
"""Start the PyQtGraph GUI."""
|
||||
peak_calibrate_mode = bool(getattr(args, "calibrate", False))
|
||||
peak_search_enabled = bool(getattr(args, "peak_search", False))
|
||||
try:
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.Qt import QtCore, QtWidgets # type: ignore
|
||||
except Exception as exc:
|
||||
raise RuntimeError(
|
||||
"pyqtgraph и совместимый Qt backend не найдены. Установите: pip install pyqtgraph PyQt5"
|
||||
) from exc
|
||||
|
||||
queue: Queue[SweepPacket] = Queue(maxsize=1000)
|
||||
stop_event = threading.Event()
|
||||
reader = SweepReader(
|
||||
args.port,
|
||||
args.baud,
|
||||
queue,
|
||||
stop_event,
|
||||
fancy=bool(args.fancy),
|
||||
bin_mode=bool(args.bin_mode),
|
||||
logscale=bool(args.logscale),
|
||||
parser_16_bit_x2=bool(args.parser_16_bit_x2),
|
||||
parser_test=bool(args.parser_test),
|
||||
)
|
||||
reader.start()
|
||||
|
||||
max_sweeps = int(max(10, args.max_sweeps))
|
||||
max_fps = max(1.0, float(args.max_fps))
|
||||
interval_ms = int(1000.0 / max_fps)
|
||||
fft_bins = FFT_LEN // 2 + 1
|
||||
spec_clip = parse_spec_clip(getattr(args, "spec_clip", None))
|
||||
spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0))
|
||||
norm_type = str(getattr(args, "norm_type", "projector")).strip().lower()
|
||||
runtime = RuntimeState(ring=RingBuffer(max_sweeps))
|
||||
|
||||
pg.setConfigOptions(
|
||||
useOpenGL=not peak_calibrate_mode,
|
||||
antialias=False,
|
||||
imageAxisOrder="row-major",
|
||||
)
|
||||
app = QtWidgets.QApplication.instance()
|
||||
if app is None:
|
||||
app = QtWidgets.QApplication([])
|
||||
try:
|
||||
app.setApplicationName(str(args.title))
|
||||
app.setQuitOnLastWindowClosed(True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
main_window = QtWidgets.QWidget()
|
||||
try:
|
||||
main_window.setWindowTitle(str(args.title))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(main_window)
|
||||
main_layout.setContentsMargins(6, 6, 6, 6)
|
||||
main_layout.setSpacing(6)
|
||||
|
||||
win = pg.GraphicsLayoutWidget(show=False, title=args.title)
|
||||
main_layout.addWidget(win)
|
||||
|
||||
settings_widget = QtWidgets.QWidget()
|
||||
settings_layout = QtWidgets.QVBoxLayout(settings_widget)
|
||||
settings_layout.setContentsMargins(6, 6, 6, 6)
|
||||
settings_layout.setSpacing(8)
|
||||
try:
|
||||
settings_widget.setMinimumWidth(130)
|
||||
settings_widget.setMaximumWidth(150)
|
||||
except Exception:
|
||||
pass
|
||||
main_layout.addWidget(settings_widget)
|
||||
|
||||
p_line = win.addPlot(row=0, col=0, title="Сырые данные")
|
||||
p_line.showGrid(x=True, y=True, alpha=0.3)
|
||||
curve_avg1 = p_line.plot(pen=pg.mkPen((170, 170, 170), width=1))
|
||||
curve_avg2 = p_line.plot(pen=pg.mkPen((110, 110, 110), width=1))
|
||||
curve = p_line.plot(pen=pg.mkPen((80, 120, 255), width=1))
|
||||
curve_calib = p_line.plot(pen=pg.mkPen((220, 60, 60), width=1))
|
||||
curve_norm = p_line.plot(pen=pg.mkPen((60, 180, 90), width=1))
|
||||
p_line.setLabel("bottom", "ГГц")
|
||||
p_line.setLabel("left", "Y")
|
||||
ch_text = pg.TextItem("", anchor=(1, 1))
|
||||
ch_text.setZValue(10)
|
||||
p_line.addItem(ch_text)
|
||||
|
||||
p_img = win.addPlot(row=0, col=1, title="Сырые данные водопад")
|
||||
p_img.invertY(False)
|
||||
p_img.showGrid(x=False, y=False)
|
||||
p_img.setLabel("bottom", "Время, с (новое справа)")
|
||||
try:
|
||||
p_img.getAxis("bottom").setStyle(showValues=False)
|
||||
except Exception:
|
||||
pass
|
||||
p_img.setLabel("left", "ГГц")
|
||||
img = pg.ImageItem()
|
||||
p_img.addItem(img)
|
||||
|
||||
p_fft = win.addPlot(row=1, col=0, title="FFT")
|
||||
p_fft.showGrid(x=True, y=True, alpha=0.3)
|
||||
curve_fft = p_fft.plot(pen=pg.mkPen((255, 120, 80), width=1))
|
||||
curve_fft_ref = p_fft.plot(pen=pg.mkPen((255, 0, 0), width=1))
|
||||
peak_pen = pg.mkPen((255, 0, 0), width=1)
|
||||
peak_box_pen = pg.mkPen((0, 170, 0), width=3)
|
||||
fft_peak_boxes = [p_fft.plot(pen=peak_box_pen) for _ in range(3)]
|
||||
fft_bg_line = pg.InfiniteLine(angle=0, movable=False, pen=peak_pen)
|
||||
fft_left_line = pg.InfiniteLine(angle=90, movable=False, pen=peak_pen)
|
||||
fft_right_line = pg.InfiniteLine(angle=90, movable=False, pen=peak_pen)
|
||||
curve_fft_ref.setVisible(False)
|
||||
for box in fft_peak_boxes:
|
||||
box.setVisible(False)
|
||||
p_fft.addItem(fft_bg_line)
|
||||
p_fft.addItem(fft_left_line)
|
||||
p_fft.addItem(fft_right_line)
|
||||
fft_bg_line.setVisible(False)
|
||||
fft_left_line.setVisible(False)
|
||||
fft_right_line.setVisible(False)
|
||||
p_fft.setLabel("bottom", "Расстояние, м")
|
||||
p_fft.setLabel("left", "дБ")
|
||||
|
||||
p_spec = win.addPlot(row=1, col=1, title="B-scan (дБ)")
|
||||
p_spec.invertY(False)
|
||||
p_spec.showGrid(x=False, y=False)
|
||||
p_spec.setLabel("bottom", "Время, с (новое справа)")
|
||||
try:
|
||||
p_spec.getAxis("bottom").setStyle(showValues=False)
|
||||
except Exception:
|
||||
pass
|
||||
p_spec.setLabel("left", "Расстояние, м")
|
||||
img_fft = pg.ImageItem()
|
||||
p_spec.addItem(img_fft)
|
||||
spec_left_line = pg.InfiniteLine(angle=0, movable=False, pen=peak_pen)
|
||||
spec_right_line = pg.InfiniteLine(angle=0, movable=False, pen=peak_pen)
|
||||
p_spec.addItem(spec_left_line)
|
||||
p_spec.addItem(spec_right_line)
|
||||
spec_left_line.setVisible(False)
|
||||
spec_right_line.setVisible(False)
|
||||
|
||||
calib_cb = QtWidgets.QCheckBox("нормировка")
|
||||
bg_compute_cb = QtWidgets.QCheckBox("расчет фона")
|
||||
bg_subtract_cb = QtWidgets.QCheckBox("вычет фона")
|
||||
fft_bg_subtract_cb = QtWidgets.QCheckBox("FFT вычет фона")
|
||||
peak_search_cb = QtWidgets.QCheckBox("поиск пиков")
|
||||
try:
|
||||
settings_layout.addWidget(QtWidgets.QLabel("Настройки"))
|
||||
except Exception:
|
||||
pass
|
||||
settings_layout.addWidget(calib_cb)
|
||||
settings_layout.addWidget(bg_compute_cb)
|
||||
settings_layout.addWidget(bg_subtract_cb)
|
||||
settings_layout.addWidget(fft_bg_subtract_cb)
|
||||
settings_layout.addWidget(peak_search_cb)
|
||||
|
||||
status = pg.LabelItem(justify="left")
|
||||
win.addItem(status, row=3, col=0, colspan=2)
|
||||
|
||||
calib_enabled = False
|
||||
bg_compute_enabled = True
|
||||
bg_subtract_enabled = False
|
||||
fft_bg_subtract_enabled = False
|
||||
fixed_ylim: Optional[Tuple[float, float]] = None
|
||||
if args.ylim:
|
||||
try:
|
||||
y0, y1 = args.ylim.split(",")
|
||||
fixed_ylim = (float(y0), float(y1))
|
||||
except Exception:
|
||||
fixed_ylim = None
|
||||
if fixed_ylim is not None:
|
||||
p_line.setYRange(fixed_ylim[0], fixed_ylim[1], padding=0)
|
||||
|
||||
def ensure_buffer(sweep_width: int) -> None:
|
||||
changed = runtime.ring.ensure_init(sweep_width)
|
||||
if not changed:
|
||||
return
|
||||
img.setImage(runtime.ring.get_display_raw(), autoLevels=False)
|
||||
img.setRect(0, SWEEP_FREQ_MIN_GHZ, max_sweeps, SWEEP_FREQ_MAX_GHZ - SWEEP_FREQ_MIN_GHZ)
|
||||
p_img.setRange(
|
||||
xRange=(0, max_sweeps - 1),
|
||||
yRange=(SWEEP_FREQ_MIN_GHZ, SWEEP_FREQ_MAX_GHZ),
|
||||
padding=0,
|
||||
)
|
||||
p_line.setXRange(SWEEP_FREQ_MIN_GHZ, SWEEP_FREQ_MAX_GHZ, padding=0)
|
||||
img_fft.setImage(runtime.ring.get_display_fft_linear(), autoLevels=False)
|
||||
img_fft.setRect(0, 0.0, max_sweeps, 1.0)
|
||||
p_spec.setRange(xRange=(0, max_sweeps - 1), yRange=(0.0, 1.0), padding=0)
|
||||
p_fft.setXRange(0.0, 1.0, padding=0)
|
||||
|
||||
def update_physical_axes() -> None:
|
||||
if runtime.current_freqs is not None and runtime.current_freqs.size > 0:
|
||||
finite_f = runtime.current_freqs[np.isfinite(runtime.current_freqs)]
|
||||
if finite_f.size > 0:
|
||||
f_min = float(np.min(finite_f))
|
||||
f_max = float(np.max(finite_f))
|
||||
if f_max <= f_min:
|
||||
f_max = f_min + 1.0
|
||||
img.setRect(0, f_min, max_sweeps, f_max - f_min)
|
||||
p_img.setRange(xRange=(0, max_sweeps - 1), yRange=(f_min, f_max), padding=0)
|
||||
|
||||
distance_axis = runtime.ring.distance_axis
|
||||
if distance_axis is not None and distance_axis.size > 0:
|
||||
d_min = float(distance_axis[0])
|
||||
d_max = float(distance_axis[-1]) if distance_axis.size > 1 else float(distance_axis[0] + 1.0)
|
||||
if d_max <= d_min:
|
||||
d_max = d_min + 1.0
|
||||
img_fft.setRect(0, d_min, max_sweeps, d_max - d_min)
|
||||
p_spec.setRange(xRange=(0, max_sweeps - 1), yRange=(d_min, d_max), padding=0)
|
||||
|
||||
def normalize_sweep(raw: np.ndarray, calib: np.ndarray) -> np.ndarray:
|
||||
return normalize_by_calib(raw, calib, norm_type=norm_type)
|
||||
|
||||
def set_calib_enabled() -> None:
|
||||
nonlocal calib_enabled
|
||||
try:
|
||||
calib_enabled = bool(calib_cb.isChecked())
|
||||
except Exception:
|
||||
calib_enabled = False
|
||||
if calib_enabled and runtime.current_sweep_raw is not None and runtime.last_calib_sweep is not None:
|
||||
runtime.current_sweep_norm = normalize_sweep(runtime.current_sweep_raw, runtime.last_calib_sweep)
|
||||
else:
|
||||
runtime.current_sweep_norm = None
|
||||
runtime.mark_dirty()
|
||||
|
||||
def set_bg_compute_enabled() -> None:
|
||||
nonlocal bg_compute_enabled
|
||||
try:
|
||||
bg_compute_enabled = bool(bg_compute_cb.isChecked())
|
||||
except Exception:
|
||||
bg_compute_enabled = False
|
||||
runtime.mark_dirty()
|
||||
|
||||
def set_bg_subtract_enabled() -> None:
|
||||
nonlocal bg_subtract_enabled
|
||||
try:
|
||||
bg_subtract_enabled = bool(bg_subtract_cb.isChecked())
|
||||
except Exception:
|
||||
bg_subtract_enabled = False
|
||||
runtime.mark_dirty()
|
||||
|
||||
def set_fft_bg_subtract_enabled() -> None:
|
||||
nonlocal fft_bg_subtract_enabled
|
||||
try:
|
||||
fft_bg_subtract_enabled = bool(fft_bg_subtract_cb.isChecked())
|
||||
except Exception:
|
||||
fft_bg_subtract_enabled = False
|
||||
runtime.mark_dirty()
|
||||
|
||||
try:
|
||||
bg_compute_cb.setChecked(True)
|
||||
except Exception:
|
||||
pass
|
||||
set_bg_compute_enabled()
|
||||
|
||||
try:
|
||||
calib_cb.stateChanged.connect(lambda _v: set_calib_enabled())
|
||||
bg_compute_cb.stateChanged.connect(lambda _v: set_bg_compute_enabled())
|
||||
bg_subtract_cb.stateChanged.connect(lambda _v: set_bg_subtract_enabled())
|
||||
fft_bg_subtract_cb.stateChanged.connect(lambda _v: set_fft_bg_subtract_enabled())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
peak_group = None
|
||||
peak_window_edit = None
|
||||
peak_params_label = None
|
||||
peak_ref_window = float(getattr(args, "peak_ref_window", 1.0))
|
||||
if (not np.isfinite(peak_ref_window)) or peak_ref_window <= 0.0:
|
||||
peak_ref_window = 1.0
|
||||
|
||||
def refresh_peak_params_label(peaks: List[Dict[str, float]]) -> None:
|
||||
if peak_params_label is None:
|
||||
return
|
||||
lines = []
|
||||
for idx in range(3):
|
||||
if idx < len(peaks):
|
||||
peak = peaks[idx]
|
||||
lines.append(f"peak {idx + 1}:")
|
||||
lines.append(f" X: {peak['x']:.4g} m")
|
||||
lines.append(f" H: {peak['height']:.4g} dB")
|
||||
lines.append(f" W: {peak['width']:.4g} m")
|
||||
else:
|
||||
lines.append(f"peak {idx + 1}:")
|
||||
lines.append(" X: - m")
|
||||
lines.append(" H: - dB")
|
||||
lines.append(" W: - m")
|
||||
if idx != 2:
|
||||
lines.append("")
|
||||
peak_params_label.setText("\n".join(lines))
|
||||
|
||||
try:
|
||||
peak_group = QtWidgets.QGroupBox("Поиск пиков")
|
||||
peak_layout = QtWidgets.QFormLayout(peak_group)
|
||||
peak_layout.setContentsMargins(6, 6, 6, 6)
|
||||
peak_layout.setSpacing(6)
|
||||
peak_window_edit = QtWidgets.QLineEdit(f"{peak_ref_window:.6g}")
|
||||
peak_layout.addRow("Окно медианы, ГГц", peak_window_edit)
|
||||
peak_params_label = QtWidgets.QLabel("")
|
||||
try:
|
||||
peak_params_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||
except Exception:
|
||||
pass
|
||||
peak_layout.addRow("Параметры", peak_params_label)
|
||||
settings_layout.addWidget(peak_group)
|
||||
|
||||
def apply_peak_window() -> None:
|
||||
nonlocal peak_ref_window
|
||||
if peak_window_edit is None:
|
||||
return
|
||||
try:
|
||||
value = float(peak_window_edit.text().strip())
|
||||
if np.isfinite(value) and value > 0.0:
|
||||
peak_ref_window = value
|
||||
runtime.mark_dirty()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
peak_window_edit.setText(f"{peak_ref_window:.6g}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
peak_window_edit.editingFinished.connect(apply_peak_window)
|
||||
refresh_peak_params_label([])
|
||||
except Exception:
|
||||
peak_group = None
|
||||
peak_window_edit = None
|
||||
peak_params_label = None
|
||||
|
||||
def set_peak_search_enabled() -> None:
|
||||
nonlocal peak_search_enabled
|
||||
try:
|
||||
peak_search_enabled = bool(peak_search_cb.isChecked())
|
||||
except Exception:
|
||||
peak_search_enabled = False
|
||||
try:
|
||||
if peak_group is not None:
|
||||
peak_group.setEnabled(peak_search_enabled)
|
||||
except Exception:
|
||||
pass
|
||||
if not peak_search_enabled:
|
||||
runtime.peak_candidates = []
|
||||
refresh_peak_params_label([])
|
||||
runtime.mark_dirty()
|
||||
|
||||
try:
|
||||
peak_search_cb.setChecked(peak_search_enabled)
|
||||
peak_search_cb.stateChanged.connect(lambda _v: set_peak_search_enabled())
|
||||
except Exception:
|
||||
pass
|
||||
set_peak_search_enabled()
|
||||
|
||||
calib_window = None
|
||||
c_edits = []
|
||||
c_value_labels = []
|
||||
if peak_calibrate_mode:
|
||||
try:
|
||||
calib_window = QtWidgets.QWidget()
|
||||
try:
|
||||
calib_window.setWindowTitle(f"{args.title} freq calibration")
|
||||
except Exception:
|
||||
pass
|
||||
calib_layout = QtWidgets.QFormLayout(calib_window)
|
||||
calib_layout.setContentsMargins(8, 8, 8, 8)
|
||||
|
||||
def refresh_c_value_labels() -> None:
|
||||
coeffs = get_calibration_coeffs()
|
||||
for idx, label in enumerate(c_value_labels):
|
||||
try:
|
||||
label.setText(f"{float(coeffs[idx]):.6g}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def apply_c_value(idx: int, edit) -> None:
|
||||
try:
|
||||
set_calibration_base_value(idx, float(edit.text().strip()))
|
||||
runtime.mark_dirty()
|
||||
except Exception:
|
||||
try:
|
||||
edit.setText(f"{float(get_calibration_base()[idx]):.6g}")
|
||||
except Exception:
|
||||
pass
|
||||
refresh_c_value_labels()
|
||||
|
||||
def apply_all_c_values() -> None:
|
||||
for idx, edit in enumerate(c_edits):
|
||||
apply_c_value(idx, edit)
|
||||
|
||||
for idx in range(3):
|
||||
edit = QtWidgets.QLineEdit(f"{float(get_calibration_base()[idx]):.6g}")
|
||||
try:
|
||||
edit.setMaximumWidth(120)
|
||||
edit.editingFinished.connect(lambda i=idx, e=edit: apply_c_value(i, e))
|
||||
except Exception:
|
||||
pass
|
||||
calib_layout.addRow(f"C{idx}", edit)
|
||||
c_edits.append(edit)
|
||||
try:
|
||||
update_btn = QtWidgets.QPushButton("Update")
|
||||
update_btn.clicked.connect(lambda _checked=False: apply_all_c_values())
|
||||
calib_layout.addRow(update_btn)
|
||||
calib_layout.addRow(QtWidgets.QLabel("Working C"), QtWidgets.QLabel(""))
|
||||
except Exception:
|
||||
pass
|
||||
for idx in range(3):
|
||||
label = QtWidgets.QLabel(f"{float(get_calibration_coeffs()[idx]):.6g}")
|
||||
calib_layout.addRow(f"C*{idx}", label)
|
||||
c_value_labels.append(label)
|
||||
refresh_c_value_labels()
|
||||
try:
|
||||
calib_window.show()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
calib_window = None
|
||||
|
||||
try:
|
||||
settings_layout.addStretch(1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def visible_bg_fft(disp_fft: np.ndarray, force: bool = False) -> Optional[np.ndarray]:
|
||||
nonlocal bg_compute_enabled, bg_subtract_enabled
|
||||
need_bg = bool(bg_subtract_enabled or force)
|
||||
if (not need_bg) or disp_fft.size == 0:
|
||||
return None
|
||||
ny, nx = disp_fft.shape
|
||||
if ny <= 0 or nx <= 0:
|
||||
return runtime.bg_spec_cache
|
||||
if runtime.bg_spec_cache is not None and runtime.bg_spec_cache.size != ny:
|
||||
runtime.bg_spec_cache = None
|
||||
if not bg_compute_enabled:
|
||||
return runtime.bg_spec_cache
|
||||
try:
|
||||
(x0, x1), _ = p_spec.viewRange()
|
||||
except Exception:
|
||||
x0, x1 = 0.0, float(nx - 1)
|
||||
xmin, xmax = sorted((float(x0), float(x1)))
|
||||
ix0 = max(0, min(nx - 1, int(np.floor(xmin))))
|
||||
ix1 = max(0, min(nx - 1, int(np.ceil(xmax))))
|
||||
if ix1 < ix0:
|
||||
ix1 = ix0
|
||||
window = disp_fft[:, ix0 : ix1 + 1]
|
||||
if window.size == 0:
|
||||
return runtime.bg_spec_cache
|
||||
try:
|
||||
bg_spec = np.nanmedian(window, axis=1)
|
||||
except Exception:
|
||||
return runtime.bg_spec_cache
|
||||
if not np.any(np.isfinite(bg_spec)):
|
||||
return runtime.bg_spec_cache
|
||||
runtime.bg_spec_cache = np.nan_to_num(bg_spec, nan=0.0).astype(np.float32, copy=False)
|
||||
return runtime.bg_spec_cache
|
||||
|
||||
def drain_queue() -> int:
|
||||
drained = 0
|
||||
while True:
|
||||
try:
|
||||
sweep, info, aux_curves = queue.get_nowait()
|
||||
except Empty:
|
||||
break
|
||||
drained += 1
|
||||
calibrated = calibrate_freqs(
|
||||
{
|
||||
"F": np.linspace(SWEEP_FREQ_MIN_GHZ, SWEEP_FREQ_MAX_GHZ, sweep.size, dtype=np.float64),
|
||||
"I": sweep,
|
||||
}
|
||||
)
|
||||
runtime.current_freqs = calibrated["F"]
|
||||
runtime.current_sweep_raw = calibrated["I"]
|
||||
runtime.current_aux_curves = aux_curves
|
||||
runtime.current_info = info
|
||||
|
||||
channel = 0
|
||||
try:
|
||||
channel = int(info.get("ch", 0)) if isinstance(info, dict) else 0
|
||||
except Exception:
|
||||
channel = 0
|
||||
|
||||
if channel == 0:
|
||||
runtime.last_calib_sweep = runtime.current_sweep_raw
|
||||
runtime.current_sweep_norm = None
|
||||
sweep_for_processing = runtime.current_sweep_raw
|
||||
else:
|
||||
if calib_enabled and runtime.last_calib_sweep is not None:
|
||||
runtime.current_sweep_norm = normalize_sweep(runtime.current_sweep_raw, runtime.last_calib_sweep)
|
||||
sweep_for_processing = runtime.current_sweep_norm
|
||||
else:
|
||||
runtime.current_sweep_norm = None
|
||||
sweep_for_processing = runtime.current_sweep_raw
|
||||
|
||||
ensure_buffer(runtime.current_sweep_raw.size)
|
||||
runtime.ring.push(sweep_for_processing, runtime.current_freqs)
|
||||
runtime.current_distances = runtime.ring.distance_axis
|
||||
runtime.current_fft_db = runtime.ring.last_fft_db
|
||||
if drained > 0:
|
||||
update_physical_axes()
|
||||
return drained
|
||||
|
||||
try:
|
||||
cm_mod = getattr(pg, "colormap", None)
|
||||
if cm_mod is not None:
|
||||
colormap = cm_mod.get(args.cmap)
|
||||
lut = colormap.getLookupTable(0.0, 1.0, 256)
|
||||
img.setLookupTable(lut)
|
||||
img_fft.setLookupTable(lut)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def update() -> None:
|
||||
nonlocal peak_ref_window
|
||||
if peak_calibrate_mode and any(edit.hasFocus() for edit in c_edits):
|
||||
return
|
||||
if peak_search_enabled and peak_window_edit is not None and peak_window_edit.hasFocus():
|
||||
return
|
||||
|
||||
changed = drain_queue() > 0
|
||||
redraw_needed = changed or runtime.plot_dirty
|
||||
bg_fft_for_line = None
|
||||
if redraw_needed and fft_bg_subtract_enabled and runtime.ring.ring_fft is not None:
|
||||
try:
|
||||
bg_fft_for_line = visible_bg_fft(runtime.ring.get_display_fft_linear(), force=True)
|
||||
except Exception:
|
||||
bg_fft_for_line = None
|
||||
|
||||
if redraw_needed and runtime.current_sweep_raw is not None:
|
||||
xs = None
|
||||
if runtime.current_freqs is not None and runtime.current_freqs.size == runtime.current_sweep_raw.size:
|
||||
xs = runtime.current_freqs
|
||||
elif runtime.ring.x_shared is not None and runtime.current_sweep_raw.size <= runtime.ring.x_shared.size:
|
||||
xs = runtime.ring.x_shared[: runtime.current_sweep_raw.size]
|
||||
else:
|
||||
xs = np.arange(runtime.current_sweep_raw.size)
|
||||
|
||||
curve.setData(xs, runtime.current_sweep_raw, autoDownsample=True)
|
||||
if runtime.current_aux_curves is not None:
|
||||
avg_1_curve, avg_2_curve = runtime.current_aux_curves
|
||||
curve_avg1.setData(xs[: avg_1_curve.size], avg_1_curve, autoDownsample=True)
|
||||
curve_avg2.setData(xs[: avg_2_curve.size], avg_2_curve, autoDownsample=True)
|
||||
else:
|
||||
curve_avg1.setData([], [])
|
||||
curve_avg2.setData([], [])
|
||||
|
||||
if runtime.last_calib_sweep is not None:
|
||||
curve_calib.setData(xs[: runtime.last_calib_sweep.size], runtime.last_calib_sweep, autoDownsample=True)
|
||||
else:
|
||||
curve_calib.setData([], [])
|
||||
|
||||
if runtime.current_sweep_norm is not None:
|
||||
curve_norm.setData(xs[: runtime.current_sweep_norm.size], runtime.current_sweep_norm, autoDownsample=True)
|
||||
else:
|
||||
curve_norm.setData([], [])
|
||||
|
||||
if fixed_ylim is None:
|
||||
y_series = [runtime.current_sweep_raw, runtime.last_calib_sweep, runtime.current_sweep_norm]
|
||||
if runtime.current_aux_curves is not None:
|
||||
y_series.extend(runtime.current_aux_curves)
|
||||
y_limits = compute_auto_ylim(*y_series)
|
||||
if y_limits is not None:
|
||||
p_line.setYRange(y_limits[0], y_limits[1], padding=0)
|
||||
|
||||
if isinstance(xs, np.ndarray) and xs.size > 0:
|
||||
finite_x = xs[np.isfinite(xs)]
|
||||
if finite_x.size > 0:
|
||||
p_line.setXRange(float(np.min(finite_x)), float(np.max(finite_x)), padding=0)
|
||||
|
||||
sweep_for_fft = runtime.current_sweep_norm if runtime.current_sweep_norm is not None else runtime.current_sweep_raw
|
||||
distance_axis = runtime.current_distances if runtime.current_distances is not None else runtime.ring.distance_axis
|
||||
if sweep_for_fft.size > 0 and distance_axis is not None:
|
||||
if runtime.current_fft_db is None or runtime.current_fft_db.size != distance_axis.size or runtime.plot_dirty:
|
||||
runtime.current_fft_db = compute_fft_row(sweep_for_fft, runtime.current_freqs, distance_axis.size)
|
||||
fft_vals = runtime.current_fft_db
|
||||
xs_fft = distance_axis[: fft_vals.size]
|
||||
if fft_bg_subtract_enabled and bg_fft_for_line is not None:
|
||||
n_bg = int(min(fft_vals.size, bg_fft_for_line.size))
|
||||
if n_bg > 0:
|
||||
num = np.maximum(
|
||||
np.power(10.0, np.asarray(fft_vals[:n_bg], dtype=np.float64) / 20.0),
|
||||
0.0,
|
||||
)
|
||||
den = np.maximum(np.asarray(bg_fft_for_line[:n_bg], dtype=np.float64), 0.0)
|
||||
fft_vals = (20.0 * np.log10((num + 1e-9) / (den + 1e-9))).astype(np.float32, copy=False)
|
||||
xs_fft = xs_fft[:n_bg]
|
||||
curve_fft.setData(xs_fft, fft_vals)
|
||||
finite_x = xs_fft[np.isfinite(xs_fft)]
|
||||
if finite_x.size > 0:
|
||||
p_fft.setXRange(float(np.min(finite_x)), float(np.max(finite_x)), padding=0)
|
||||
|
||||
finite_fft = np.isfinite(xs_fft) & np.isfinite(fft_vals)
|
||||
y_for_range = fft_vals[finite_fft]
|
||||
if peak_search_enabled:
|
||||
fft_ref = rolling_median_ref(xs_fft, fft_vals, peak_ref_window)
|
||||
finite_ref = np.isfinite(xs_fft) & np.isfinite(fft_ref)
|
||||
if np.any(finite_ref):
|
||||
curve_fft_ref.setData(xs_fft[finite_ref], fft_ref[finite_ref])
|
||||
curve_fft_ref.setVisible(True)
|
||||
y_for_range = np.concatenate((y_for_range, fft_ref[finite_ref]))
|
||||
else:
|
||||
curve_fft_ref.setVisible(False)
|
||||
runtime.peak_candidates = find_top_peaks_over_ref(xs_fft, fft_vals, fft_ref, top_n=3)
|
||||
refresh_peak_params_label(runtime.peak_candidates)
|
||||
for idx, box in enumerate(fft_peak_boxes):
|
||||
if idx < len(runtime.peak_candidates):
|
||||
peak = runtime.peak_candidates[idx]
|
||||
box.setData(
|
||||
[peak["left"], peak["left"], peak["right"], peak["right"], peak["left"]],
|
||||
[peak["ref"], peak["peak_y"], peak["peak_y"], peak["ref"], peak["ref"]],
|
||||
)
|
||||
box.setVisible(True)
|
||||
else:
|
||||
box.setVisible(False)
|
||||
else:
|
||||
runtime.peak_candidates = []
|
||||
refresh_peak_params_label([])
|
||||
curve_fft_ref.setVisible(False)
|
||||
for box in fft_peak_boxes:
|
||||
box.setVisible(False)
|
||||
|
||||
if fft_bg_subtract_enabled and bg_fft_for_line is not None:
|
||||
p_fft.setYRange(-10.0, 30.0, padding=0)
|
||||
else:
|
||||
finite_y = y_for_range[np.isfinite(y_for_range)]
|
||||
if finite_y.size > 0:
|
||||
y0 = float(np.min(finite_y))
|
||||
y1 = float(np.max(finite_y))
|
||||
if y1 <= y0:
|
||||
y1 = y0 + 1e-3
|
||||
p_fft.setYRange(y0, y1, padding=0)
|
||||
|
||||
if peak_calibrate_mode:
|
||||
markers = find_peak_width_markers(xs_fft, fft_vals)
|
||||
if markers is not None:
|
||||
fft_bg_line.setValue(markers["background"])
|
||||
fft_left_line.setValue(markers["left"])
|
||||
fft_right_line.setValue(markers["right"])
|
||||
spec_left_line.setValue(markers["left"])
|
||||
spec_right_line.setValue(markers["right"])
|
||||
fft_bg_line.setVisible(True)
|
||||
fft_left_line.setVisible(True)
|
||||
fft_right_line.setVisible(True)
|
||||
spec_left_line.setVisible(True)
|
||||
spec_right_line.setVisible(True)
|
||||
runtime.current_peak_width = markers["width"]
|
||||
runtime.current_peak_amplitude = markers["amplitude"]
|
||||
else:
|
||||
fft_bg_line.setVisible(False)
|
||||
fft_left_line.setVisible(False)
|
||||
fft_right_line.setVisible(False)
|
||||
spec_left_line.setVisible(False)
|
||||
spec_right_line.setVisible(False)
|
||||
runtime.current_peak_width = None
|
||||
runtime.current_peak_amplitude = None
|
||||
else:
|
||||
fft_bg_line.setVisible(False)
|
||||
fft_left_line.setVisible(False)
|
||||
fft_right_line.setVisible(False)
|
||||
spec_left_line.setVisible(False)
|
||||
spec_right_line.setVisible(False)
|
||||
runtime.current_peak_width = None
|
||||
runtime.current_peak_amplitude = None
|
||||
else:
|
||||
curve_fft_ref.setVisible(False)
|
||||
for box in fft_peak_boxes:
|
||||
box.setVisible(False)
|
||||
runtime.peak_candidates = []
|
||||
refresh_peak_params_label([])
|
||||
runtime.plot_dirty = False
|
||||
|
||||
if changed and runtime.ring.ring is not None:
|
||||
disp = runtime.ring.get_display_raw()
|
||||
levels = _visible_levels_pyqtgraph(disp, p_img)
|
||||
if levels is not None:
|
||||
img.setImage(disp, autoLevels=False, levels=levels)
|
||||
else:
|
||||
img.setImage(disp, autoLevels=False)
|
||||
|
||||
if changed and runtime.current_info:
|
||||
try:
|
||||
status_payload = dict(runtime.current_info)
|
||||
if peak_calibrate_mode and runtime.current_peak_width is not None:
|
||||
status_payload["peak_w"] = runtime.current_peak_width
|
||||
if peak_calibrate_mode and runtime.current_peak_amplitude is not None:
|
||||
status_payload["peak_a"] = runtime.current_peak_amplitude
|
||||
status.setText(format_status_kv(status_payload))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
chs = runtime.current_info.get("chs") if isinstance(runtime.current_info, dict) else None
|
||||
if chs is None:
|
||||
chs = runtime.current_info.get("ch") if isinstance(runtime.current_info, dict) else None
|
||||
if chs is None:
|
||||
ch_text.setText("")
|
||||
else:
|
||||
if isinstance(chs, (list, tuple, set)):
|
||||
ch_list = sorted(int(v) for v in chs)
|
||||
ch_text_val = ", ".join(str(v) for v in ch_list)
|
||||
else:
|
||||
ch_text_val = str(int(chs))
|
||||
ch_text.setText(f"chs {ch_text_val}")
|
||||
(x0, x1), (y0, y1) = p_line.viewRange()
|
||||
dx = 0.01 * max(1.0, float(x1 - x0))
|
||||
dy = 0.01 * max(1.0, float(y1 - y0))
|
||||
ch_text.setPos(float(x1 - dx), float(y1 - dy))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if changed and runtime.ring.ring_fft is not None:
|
||||
disp_fft_lin = runtime.ring.get_display_fft_linear()
|
||||
if spec_mean_sec > 0.0:
|
||||
disp_times = runtime.ring.get_display_times()
|
||||
if disp_times is not None:
|
||||
now_t = time.time()
|
||||
mask = np.isfinite(disp_times) & (disp_times >= (now_t - spec_mean_sec))
|
||||
if np.any(mask):
|
||||
try:
|
||||
mean_spec = np.nanmean(disp_fft_lin[:, mask], axis=1)
|
||||
mean_spec = np.nan_to_num(mean_spec, nan=0.0)
|
||||
disp_fft_lin = disp_fft_lin - mean_spec[:, None]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
bg_spec = visible_bg_fft(disp_fft_lin)
|
||||
if bg_spec is not None:
|
||||
num = np.maximum(disp_fft_lin, 0.0).astype(np.float32, copy=False) + 1e-9
|
||||
den = bg_spec[:, None] + 1e-9
|
||||
disp_fft = (20.0 * np.log10(num / den)).astype(np.float32, copy=False)
|
||||
else:
|
||||
disp_fft = fft_mag_to_db(disp_fft_lin)
|
||||
|
||||
levels = None
|
||||
if bg_spec is not None:
|
||||
try:
|
||||
p5 = float(np.nanpercentile(disp_fft, 5))
|
||||
p95 = float(np.nanpercentile(disp_fft, 95))
|
||||
span = max(abs(p5), abs(p95))
|
||||
if np.isfinite(span) and span > 0.0:
|
||||
levels = (-span, span)
|
||||
except Exception:
|
||||
levels = None
|
||||
else:
|
||||
try:
|
||||
mean_spec = np.nanmean(disp_fft, axis=1)
|
||||
vmin_v = float(np.nanmin(mean_spec))
|
||||
vmax_v = float(np.nanmax(mean_spec))
|
||||
if np.isfinite(vmin_v) and np.isfinite(vmax_v) and vmin_v != vmax_v:
|
||||
levels = (vmin_v, vmax_v)
|
||||
except Exception:
|
||||
levels = None
|
||||
if levels is None and spec_clip is not None:
|
||||
try:
|
||||
vmin_v = float(np.nanpercentile(disp_fft, spec_clip[0]))
|
||||
vmax_v = float(np.nanpercentile(disp_fft, spec_clip[1]))
|
||||
if np.isfinite(vmin_v) and np.isfinite(vmax_v) and vmin_v != vmax_v:
|
||||
levels = (vmin_v, vmax_v)
|
||||
except Exception:
|
||||
levels = None
|
||||
if (
|
||||
levels is None
|
||||
and runtime.ring.y_min_fft is not None
|
||||
and runtime.ring.y_max_fft is not None
|
||||
and np.isfinite(runtime.ring.y_min_fft)
|
||||
and np.isfinite(runtime.ring.y_max_fft)
|
||||
and runtime.ring.y_min_fft != runtime.ring.y_max_fft
|
||||
):
|
||||
levels = (runtime.ring.y_min_fft, runtime.ring.y_max_fft)
|
||||
if levels is not None:
|
||||
img_fft.setImage(disp_fft, autoLevels=False, levels=levels)
|
||||
else:
|
||||
img_fft.setImage(disp_fft, autoLevels=False)
|
||||
|
||||
timer = pg.QtCore.QTimer()
|
||||
timer.timeout.connect(update)
|
||||
timer.start(interval_ms)
|
||||
|
||||
sigint_requested = threading.Event()
|
||||
sigint_timer = pg.QtCore.QTimer()
|
||||
sigint_timer.setInterval(50)
|
||||
sigint_timer.timeout.connect(lambda: app.quit() if sigint_requested.is_set() else None)
|
||||
sigint_timer.start()
|
||||
|
||||
cleanup_done = False
|
||||
|
||||
def on_quit() -> None:
|
||||
nonlocal cleanup_done
|
||||
if cleanup_done:
|
||||
return
|
||||
cleanup_done = True
|
||||
try:
|
||||
timer.stop()
|
||||
sigint_timer.stop()
|
||||
except Exception:
|
||||
pass
|
||||
stop_event.set()
|
||||
reader.join(timeout=1.0)
|
||||
try:
|
||||
main_window.close()
|
||||
except Exception:
|
||||
pass
|
||||
if calib_window is not None:
|
||||
try:
|
||||
calib_window.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def handle_sigint(_signum, _frame) -> None:
|
||||
sigint_requested.set()
|
||||
|
||||
prev_sigint = signal.getsignal(signal.SIGINT)
|
||||
try:
|
||||
signal.signal(signal.SIGINT, handle_sigint)
|
||||
except Exception:
|
||||
prev_sigint = None
|
||||
|
||||
orig_close_event = getattr(main_window, "closeEvent", None)
|
||||
|
||||
def close_event(event) -> None:
|
||||
try:
|
||||
if callable(orig_close_event):
|
||||
orig_close_event(event)
|
||||
else:
|
||||
event.accept()
|
||||
except Exception:
|
||||
try:
|
||||
event.accept()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
app.quit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
main_window.closeEvent = close_event # type: ignore[method-assign]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
app.aboutToQuit.connect(on_quit)
|
||||
try:
|
||||
main_window.resize(1200, 680)
|
||||
except Exception:
|
||||
pass
|
||||
main_window.show()
|
||||
exec_fn = getattr(app, "exec_", None) or getattr(app, "exec", None)
|
||||
try:
|
||||
exec_fn()
|
||||
finally:
|
||||
on_quit()
|
||||
if prev_sigint is not None:
|
||||
try:
|
||||
signal.signal(signal.SIGINT, prev_sigint)
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user