new kamil adc

This commit is contained in:
Ayzen
2026-06-11 13:02:02 +03:00
parent 21f76d7cd2
commit 9661504e51
46 changed files with 12097 additions and 1063 deletions
@@ -419,6 +419,7 @@ class AppWindowConfigProfileIOMixin:
self._set_combo_current_text(self._processing_mode, gui_state.processing.selected_mode)
self._show_magnitude_checkbox.setChecked(bool(gui_state.processing.pass_through.show_magnitude))
self._show_phase_checkbox.setChecked(bool(gui_state.processing.pass_through.show_phase))
self._unwrap_phase_checkbox.setChecked(bool(gui_state.processing.pass_through.unwrap_phase))
self._pass_through_combo_filter_input.setText(str(gui_state.processing.pass_through.combo_filter))
self._pass_through_fixed_y_enabled.setChecked(bool(gui_state.processing.pass_through.fixed_y_enabled))
self._pass_through_y_min_db.setValue(float(gui_state.processing.pass_through.y_min_db))
@@ -203,6 +203,7 @@ class AppWindowConfigStateBuildersMixin:
pass_through=GuiPassThroughStateModel(
show_magnitude=True,
show_phase=True,
unwrap_phase=False,
combo_filter="",
fixed_y_enabled=False,
y_min_db=-100.0,
@@ -323,6 +324,7 @@ class AppWindowConfigStateBuildersMixin:
pass_through=GuiPassThroughStateModel(
show_magnitude=bool(self._show_magnitude_checkbox.isChecked()),
show_phase=bool(self._show_phase_checkbox.isChecked()),
unwrap_phase=bool(self._unwrap_phase_checkbox.isChecked()),
combo_filter=self._pass_through_combo_filter_input.text().strip(),
fixed_y_enabled=bool(self._pass_through_fixed_y_enabled.isChecked()),
y_min_db=float(self._pass_through_y_min_db.value()),
@@ -12,7 +12,7 @@ from python_app.gui.runtime.history import (
build_run_history_signature,
record_result_history,
)
from python_app.hardware_full.kamil_adc_service import apply_kamil_adc_laser_control
from python_app.hardware_full.kamil_adc import apply_kamil_adc_laser_control
from python_app.hardware_full.single_radar_service import create_single_radar_service
from python_app.models.dataset_model import ComboKey, ResultCollection, SweepCollection
from python_app.models.run_config_model import RunConfigModel
@@ -21,6 +21,10 @@ class AppWindowTracePlotMixin:
"""Return whether phase curves should be rendered."""
return self._show_phase_checkbox.isChecked()
def _unwrap_phase_enabled(self) -> bool:
"""Return whether the phase trace should be unwrapped (cumulative)."""
return self._unwrap_phase_checkbox.isChecked()
def _pass_through_fixed_y_range(self) -> tuple[bool, float, float]:
"""Return normalized magnitude Y-range override for pass-through mode."""
y_min = float(self._pass_through_y_min_db.value())
@@ -48,13 +52,29 @@ class AppWindowTracePlotMixin:
return None
def _configure_pass_through_magnitude_axis(self, plot: pg.PlotWidget) -> None:
"""Apply pass-through magnitude-axis autorange or fixed Y window."""
"""Apply the magnitude Y-axis: a fixed window or autorange.
The X axis is set explicitly from the data band by the caller, so this
never re-enables X autorange (which would scan all curves every frame).
"""
fixed_y_enabled, y_min, y_max = self._pass_through_fixed_y_range()
view_box = plot.getViewBox()
view_box.invertY(False)
view_box.enableAutoRange(x=True, y=not fixed_y_enabled)
if fixed_y_enabled:
view_box.enableAutoRange(y=False)
plot.setYRange(y_min, y_max, padding=0.0)
else:
view_box.enableAutoRange(y=True)
def _configure_pass_through_phase_axis(self, plot: pg.PlotWidget) -> None:
"""Apply the phase Y-axis: fixed ±180° when wrapped, autorange when unwrapped."""
view_box = plot.getViewBox()
view_box.invertY(False)
if self._unwrap_phase_enabled():
view_box.enableAutoRange(y=True)
else:
view_box.enableAutoRange(y=False)
plot.setYRange(-180.0, 180.0, padding=0.02)
def _on_trace_visibility_changed(self, *_args) -> None:
"""Redraw pass-through traces when magnitude/phase toggles changed."""
@@ -116,7 +136,6 @@ class AppWindowTracePlotMixin:
if show_magnitude:
mag_item = magnitude_plot.getPlotItem()
self._configure_pass_through_magnitude_axis(magnitude_plot)
mag_item.showAxis("left", show=True)
mag_item.showAxis("bottom", show=not show_phase)
magnitude_plot.setLabel("left", "Magnitude", units="dB")
@@ -126,8 +145,6 @@ class AppWindowTracePlotMixin:
if show_phase:
phase_item = phase_plot.getPlotItem()
phase_plot.getViewBox().invertY(False)
phase_plot.getViewBox().enableAutoRange(x=True, y=False)
phase_item.showAxis("left", show=True)
phase_item.showAxis("bottom", show=True)
phase_plot.setLabel("left", "Phase", units="deg")
@@ -179,16 +196,18 @@ class AppWindowTracePlotMixin:
x_max = max(x_max, local_x_max)
if show_magnitude:
magnitude_values = 20.0 * np.log10(np.maximum(np.abs(payload.trace), 1e-12))
mag_x, magnitude_values = self._magnitude_display_arrays(
payload.frequency_hz, payload.trace
)
active_magnitude_keys.add(curve_key)
magnitude_curve = self._trace_magnitude_curves.get(curve_key)
if magnitude_curve is None:
magnitude_curve = pg.PlotCurveItem(pen=pg.mkPen(color, width=1.4))
magnitude_curve = pg.PlotCurveItem(pen=pg.mkPen(color, width=1.4), antialias=False)
self._trace_magnitude_curves[curve_key] = magnitude_curve
magnitude_plot.addItem(magnitude_curve)
else:
magnitude_curve.setPen(pg.mkPen(color, width=1.4))
magnitude_curve.setData(payload.frequency_hz, magnitude_values)
magnitude_curve.setData(mag_x, magnitude_values)
legend_source_magnitude.setdefault(combo_key, magnitude_curve)
has_data = True
@@ -196,7 +215,7 @@ class AppWindowTracePlotMixin:
active_phase_keys.add(curve_key)
phase_curve = self._trace_phase_curves.get(curve_key)
if phase_curve is None:
phase_curve = pg.PlotCurveItem(pen=pg.mkPen(color, width=1.2))
phase_curve = pg.PlotCurveItem(pen=pg.mkPen(color, width=1.2), antialias=False)
self._trace_phase_curves[curve_key] = phase_curve
phase_plot.addItem(phase_curve)
else:
@@ -231,16 +250,17 @@ class AppWindowTracePlotMixin:
phase_sources=legend_source_phase,
)
if has_data:
if np.isfinite(x_min) and np.isfinite(x_max):
if show_magnitude:
magnitude_plot.setXRange(x_min, x_max, padding=0.02)
if show_phase:
phase_plot.setXRange(x_min, x_max, padding=0.02)
if show_phase:
phase_plot.setYRange(-180.0, 180.0, padding=0.02)
# X is the (constant) frequency band: set it explicitly rather than
# autoranging every frame. Y is configured once per draw.
if has_data and np.isfinite(x_min) and np.isfinite(x_max):
if show_magnitude:
self._configure_pass_through_magnitude_axis(magnitude_plot)
magnitude_plot.setXRange(x_min, x_max, padding=0.02)
if show_phase:
phase_plot.setXRange(x_min, x_max, padding=0.02)
if show_magnitude:
self._configure_pass_through_magnitude_axis(magnitude_plot)
if show_phase:
self._configure_pass_through_phase_axis(phase_plot)
return has_data
@staticmethod
@@ -268,15 +288,40 @@ class AppWindowTracePlotMixin:
plot.removeItem(curve)
cache.clear()
def _phase_display_arrays(self, frequency_hz: np.ndarray, trace: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Return phase display arrays with decimation for faster rendering."""
max_points = int(getattr(self, "_trace_phase_render_max_points", 1200))
if max_points > 0 and trace.size > max_points:
step = max(1, int(np.ceil(trace.size / max_points)))
frequency_hz = frequency_hz[::step]
trace = trace[::step]
phase_values = np.arctan2(trace.imag, trace.real) * (180.0 / np.pi)
return frequency_hz, phase_values
def _render_decimation_step(self, size: int) -> int:
"""Stride that caps a rendered trace at the configured maximum point count.
Beyond ~1 point per screen pixel there is no visual gain, so decimating
keeps the line plots responsive even with many combos on screen.
"""
max_points = int(getattr(self, "_trace_render_max_points", 800))
if max_points > 0 and size > max_points:
return max(1, int(np.ceil(size / max_points)))
return 1
def _magnitude_display_arrays(
self, frequency_hz: np.ndarray, trace: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
"""Return decimated (frequency, magnitude-dB) arrays for fast rendering."""
step = self._render_decimation_step(int(trace.size))
frequency_hz = frequency_hz[::step]
magnitude_db = 20.0 * np.log10(np.maximum(np.abs(trace[::step]), 1e-12))
return frequency_hz, magnitude_db
def _phase_display_arrays(
self, frequency_hz: np.ndarray, trace: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
"""Return decimated (frequency, phase-deg) arrays, unwrapped when enabled.
Unwrapping runs at full resolution (before decimation) so the cumulative
phase is correct, then both arrays are decimated together for rendering.
"""
phase = np.angle(trace)
if self._unwrap_phase_enabled():
phase = np.unwrap(phase)
phase_deg = np.degrees(phase)
step = self._render_decimation_step(int(trace.size))
return frequency_hz[::step], phase_deg[::step]
def _sync_trace_legends(
self,
@@ -363,26 +408,24 @@ class AppWindowTracePlotMixin:
return
if show_magnitude:
self._configure_pass_through_magnitude_axis(magnitude_plot)
magnitude_plot.getPlotItem().showAxis("bottom", show=not show_phase)
magnitude_plot.setLabel("left", "Magnitude", units="dB")
magnitude_plot.setTitle(title)
if not show_phase:
magnitude_plot.setLabel("bottom", "Frequency", units="Hz")
if show_phase:
phase_plot.getViewBox().invertY(False)
phase_plot.getViewBox().enableAutoRange(x=True, y=False)
phase_plot.getPlotItem().showAxis("bottom", show=True)
phase_plot.setLabel("left", "Phase", units="deg")
phase_plot.setLabel("bottom", "Frequency", units="Hz")
phase_plot.setTitle(title)
if show_magnitude:
magnitude_db = 20.0 * np.log10(np.maximum(np.abs(samples), 1e-12))
mag_x, magnitude_db = self._magnitude_display_arrays(trace.frequency_hz, samples)
magnitude_curve = pg.PlotCurveItem(
trace.frequency_hz,
mag_x,
magnitude_db,
pen=pg.mkPen("#ffd166", width=1.8),
antialias=False,
)
magnitude_plot.addItem(magnitude_curve)
self._trace_magnitude_curves[
@@ -390,23 +433,26 @@ class AppWindowTracePlotMixin:
] = magnitude_curve
if show_phase:
phase_values = np.degrees(np.angle(samples))
phase_x, phase_values = self._phase_display_arrays(trace.frequency_hz, samples)
phase_curve = pg.PlotCurveItem(
trace.frequency_hz,
phase_x,
phase_values,
pen=pg.mkPen("#80ed99", width=1.4, style=Qt.PenStyle.DashLine),
antialias=False,
)
phase_plot.addItem(phase_curve)
self._trace_phase_curves[
(int(trace.combo.input), int(trace.combo.output), 0, "__single_trace__")
] = phase_curve
phase_plot.setYRange(-180.0, 180.0, padding=0.02)
if np.size(trace.frequency_hz) > 1:
x_min = float(np.min(trace.frequency_hz))
x_max = float(np.max(trace.frequency_hz))
if show_magnitude:
magnitude_plot.setXRange(x_min, x_max, padding=0.02)
self._configure_pass_through_magnitude_axis(magnitude_plot)
if show_phase:
phase_plot.setXRange(x_min, x_max, padding=0.02)
if show_magnitude:
self._configure_pass_through_magnitude_axis(magnitude_plot)
if show_phase:
self._configure_pass_through_phase_axis(phase_plot)
@@ -3,7 +3,6 @@
from __future__ import annotations
from python_app.gui.preprocess_dialog import PreprocessDialog
from python_app.hardware_full.kamil_adc_service import KamilAdcService
from python_app.orchestration.preprocess_assets import (
PREPROCESS_ASSET_SPECS,
VISIBLE_PREPROCESS_ASSET_KEYS,
@@ -429,8 +428,8 @@ class AppWindowPreprocessMixin:
self._stop_run()
pipeline_was_paused = True
point_count = self._read_kamil_adc_point_count(config)
calibration, reference = build_kamil_adc_neutral_s21_sets(config, point_count)
calibration, reference = build_kamil_adc_neutral_s21_sets(config)
point_count = config.radar.kamil_adc.band.points
self._store.save_set("s21_calibration", radar_key, set_name, calibration)
self._store.save_set("s21_reference", radar_key, set_name, reference)
@@ -454,25 +453,6 @@ class AppWindowPreprocessMixin:
if pipeline_was_paused:
self._start_run()
def _read_kamil_adc_point_count(self, config) -> int:
"""Read one Kamil ADC sweep and return its actual point count."""
dialog = self._ensure_preprocess_dialog()
dialog.set_status("Reading one Kamil ADC sweep to detect point count...")
self._log("Reading one Kamil ADC sweep to detect neutral-set point count")
radar = KamilAdcService(config)
try:
radar.open()
radar.configure(config.radar.sweep)
sweep = radar.acquire()
finally:
radar.close()
point_count = int(sweep.x.size)
if point_count <= 0:
raise RuntimeError("Kamil ADC returned an empty sweep while detecting point count")
return point_count
def _build_single_radar_capture_session(
self,
*,
@@ -125,7 +125,10 @@ class AppWindowUiMixin:
self._trace_phase_legend_combo_keys = set()
self._trace_magnitude_curves = {}
self._trace_phase_curves = {}
self._trace_phase_render_max_points = 400
# Cap rendered points per trace (magnitude and phase alike): beyond ~1
# point/pixel there is no visual gain, and decimation keeps pass-through
# responsive with many combos on screen.
self._trace_render_max_points = 800
self._plot_stack.addWidget(self._trace_plots_container)
@@ -80,6 +80,13 @@ def build_processing_group(owner) -> QGroupBox:
owner._show_phase_checkbox = QCheckBox("Show phase")
owner._show_phase_checkbox.setChecked(bool(pass_defaults.show_phase))
owner._unwrap_phase_checkbox = QCheckBox("Unwrap phase")
owner._unwrap_phase_checkbox.setToolTip(
"Plot the cumulative (unwrapped) phase instead of wrapping to ±180°; "
"the phase axis auto-scales to the unwrapped range."
)
owner._unwrap_phase_checkbox.setChecked(bool(pass_defaults.unwrap_phase))
owner._pass_through_combo_filter_input = QLineEdit(str(pass_defaults.combo_filter))
owner._pass_through_combo_filter_input.setPlaceholderText("empty = all, e.g. 0:0,1:0")
@@ -105,12 +112,13 @@ def build_processing_group(owner) -> QGroupBox:
[
owner._show_magnitude_checkbox,
owner._show_phase_checkbox,
owner._unwrap_phase_checkbox,
("Switch combos", owner._pass_through_combo_filter_input),
owner._pass_through_fixed_y_enabled,
("Y min dB", owner._pass_through_y_min_db),
("Y max dB", owner._pass_through_y_max_db),
],
split_index=4,
split_index=5,
)
owner._processing_mode_pages.addWidget(pass_through_page)
@@ -492,6 +500,7 @@ def build_processing_group(owner) -> QGroupBox:
owner._processing_mode.currentTextChanged.connect(owner._on_processing_mode_changed)
owner._show_magnitude_checkbox.toggled.connect(owner._on_trace_visibility_changed)
owner._show_phase_checkbox.toggled.connect(owner._on_trace_visibility_changed)
owner._unwrap_phase_checkbox.toggled.connect(owner._on_trace_visibility_changed)
owner._pass_through_combo_filter_input.editingFinished.connect(owner._on_trace_visibility_changed)
owner._pass_through_fixed_y_enabled.toggled.connect(owner._sync_pass_through_y_controls)
owner._pass_through_fixed_y_enabled.toggled.connect(owner._on_processing_live_settings_changed)