new kamil adc
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user