multi config calibration added
This commit is contained in:
@@ -35,6 +35,8 @@ from python_app.orchestration.preprocess_assets import VISIBLE_PREPROCESS_ASSET_
|
|||||||
from python_app.orchestration.process_supervisor import ProcessSupervisor
|
from python_app.orchestration.process_supervisor import ProcessSupervisor
|
||||||
from python_app.orchestration.shm_reader import ShmRingReader
|
from python_app.orchestration.shm_reader import ShmRingReader
|
||||||
from python_app.storage.npz_store import NpzStore
|
from python_app.storage.npz_store import NpzStore
|
||||||
|
from python_app.workflows.multi_radar_capture_workflow import MultiRadarSequentialCaptureSession
|
||||||
|
from python_app.workflows.radar_config_variants import RadarConfigScanSummary, RadarConfigVariant
|
||||||
from python_app.workflows.sequential_capture_workflow import SequentialCaptureSession
|
from python_app.workflows.sequential_capture_workflow import SequentialCaptureSession
|
||||||
|
|
||||||
|
|
||||||
@@ -122,6 +124,17 @@ class AppWindow(
|
|||||||
"""Initialize preprocessing dialog and selected set names."""
|
"""Initialize preprocessing dialog and selected set names."""
|
||||||
self._preprocess_dialog: PreprocessDialog | None = None
|
self._preprocess_dialog: PreprocessDialog | None = None
|
||||||
self._preprocess_set_name = str(self._gui_defaults.preprocess_dialog.set_name)
|
self._preprocess_set_name = str(self._gui_defaults.preprocess_dialog.set_name)
|
||||||
|
self._preprocess_radar_config_dir = str(self._gui_defaults.preprocess_dialog.radar_config_dir)
|
||||||
|
self._preprocess_use_all_radar_configs = bool(self._gui_defaults.preprocess_dialog.use_all_radar_configs)
|
||||||
|
self._preprocess_radar_variants: list[RadarConfigVariant] = []
|
||||||
|
self._preprocess_radar_scan_summary = RadarConfigScanSummary(
|
||||||
|
directory_path=self._preprocess_radar_config_dir,
|
||||||
|
json_file_count=0,
|
||||||
|
valid_variant_count=0,
|
||||||
|
skipped_file_count=0,
|
||||||
|
duplicate_variant_count=0,
|
||||||
|
issues=(),
|
||||||
|
)
|
||||||
self._selected_preprocess_sets = {
|
self._selected_preprocess_sets = {
|
||||||
key: str(preprocess_asset_model(self._defaults_config, key).set_name)
|
key: str(preprocess_asset_model(self._defaults_config, key).set_name)
|
||||||
for key in VISIBLE_PREPROCESS_ASSET_KEYS
|
for key in VISIBLE_PREPROCESS_ASSET_KEYS
|
||||||
@@ -130,7 +143,7 @@ class AppWindow(
|
|||||||
|
|
||||||
def _init_capture_state(self) -> None:
|
def _init_capture_state(self) -> None:
|
||||||
"""Initialize one-shot capture and sequence-control flags."""
|
"""Initialize one-shot capture and sequence-control flags."""
|
||||||
self._capture_session: SequentialCaptureSession | None = None
|
self._capture_session: SequentialCaptureSession | MultiRadarSequentialCaptureSession | None = None
|
||||||
self._resume_pipeline_after_capture = False
|
self._resume_pipeline_after_capture = False
|
||||||
self._single_capture_active = False
|
self._single_capture_active = False
|
||||||
self._single_capture_start_ns: int | None = None
|
self._single_capture_start_ns: int | None = None
|
||||||
|
|||||||
@@ -274,6 +274,8 @@ class AppWindowConfigProfileIOMixin:
|
|||||||
self._selected_preprocess_sets = selected_preprocess_sets
|
self._selected_preprocess_sets = selected_preprocess_sets
|
||||||
self._selected_preprocess_radar_key = self._radar_key(config)
|
self._selected_preprocess_radar_key = self._radar_key(config)
|
||||||
self._preprocess_set_name = str(gui_state.preprocess_dialog.set_name)
|
self._preprocess_set_name = str(gui_state.preprocess_dialog.set_name)
|
||||||
|
self._preprocess_radar_config_dir = str(gui_state.preprocess_dialog.radar_config_dir)
|
||||||
|
self._preprocess_use_all_radar_configs = bool(gui_state.preprocess_dialog.use_all_radar_configs)
|
||||||
self._apply_history_limit_from_config(config)
|
self._apply_history_limit_from_config(config)
|
||||||
self._gpr_geometry_signature = None
|
self._gpr_geometry_signature = None
|
||||||
self._gpr_selected_geometry = None
|
self._gpr_selected_geometry = None
|
||||||
@@ -283,12 +285,18 @@ class AppWindowConfigProfileIOMixin:
|
|||||||
if self._preprocess_dialog is not None:
|
if self._preprocess_dialog is not None:
|
||||||
with ExitStack() as dialog_blockers:
|
with ExitStack() as dialog_blockers:
|
||||||
dialog_blockers.enter_context(QSignalBlocker(self._preprocess_dialog._set_name_input))
|
dialog_blockers.enter_context(QSignalBlocker(self._preprocess_dialog._set_name_input))
|
||||||
|
dialog_blockers.enter_context(QSignalBlocker(self._preprocess_dialog._radar_config_dir_input))
|
||||||
|
dialog_blockers.enter_context(QSignalBlocker(self._preprocess_dialog._use_all_radar_configs_checkbox))
|
||||||
for combo in self._preprocess_dialog._set_combos.values():
|
for combo in self._preprocess_dialog._set_combos.values():
|
||||||
dialog_blockers.enter_context(QSignalBlocker(combo))
|
dialog_blockers.enter_context(QSignalBlocker(combo))
|
||||||
self._preprocess_dialog.set_set_name(self._preprocess_set_name)
|
self._preprocess_dialog.set_set_name(self._preprocess_set_name)
|
||||||
|
self._preprocess_dialog.set_radar_config_dir(self._preprocess_radar_config_dir)
|
||||||
|
self._preprocess_dialog.set_use_all_radar_configs(self._preprocess_use_all_radar_configs)
|
||||||
self._preprocess_dialog.set_selected_sets(self._selected_preprocess_sets, emit_signal=False)
|
self._preprocess_dialog.set_selected_sets(self._selected_preprocess_sets, emit_signal=False)
|
||||||
|
|
||||||
self._apply_initial_radar_limits()
|
self._apply_initial_radar_limits()
|
||||||
|
if self._preprocess_dialog is not None:
|
||||||
|
self._refresh_sets()
|
||||||
self._reload_locator_service_from_config()
|
self._reload_locator_service_from_config()
|
||||||
self._on_processing_mode_changed(gui_state.processing.selected_mode)
|
self._on_processing_mode_changed(gui_state.processing.selected_mode)
|
||||||
self._update_history_indicator()
|
self._update_history_indicator()
|
||||||
|
|||||||
@@ -185,7 +185,11 @@ class AppWindowConfigStateBuildersMixin:
|
|||||||
save_path=str(self._project_root / "python_app/data/snapshots"),
|
save_path=str(self._project_root / "python_app/data/snapshots"),
|
||||||
save_name="snapshot_manual",
|
save_name="snapshot_manual",
|
||||||
),
|
),
|
||||||
preprocess_dialog=GuiPreprocessDialogStateModel(set_name="set_001"),
|
preprocess_dialog=GuiPreprocessDialogStateModel(
|
||||||
|
set_name="set_001",
|
||||||
|
radar_config_dir="",
|
||||||
|
use_all_radar_configs=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _current_preprocess_set_name(self) -> str:
|
def _current_preprocess_set_name(self) -> str:
|
||||||
@@ -194,6 +198,18 @@ class AppWindowConfigStateBuildersMixin:
|
|||||||
self._preprocess_set_name = self._preprocess_dialog.set_name()
|
self._preprocess_set_name = self._preprocess_dialog.set_name()
|
||||||
return self._preprocess_set_name
|
return self._preprocess_set_name
|
||||||
|
|
||||||
|
def _current_preprocess_radar_config_dir(self) -> str:
|
||||||
|
"""Return current preprocess dialog radar-config directory path."""
|
||||||
|
if self._preprocess_dialog is not None:
|
||||||
|
self._preprocess_radar_config_dir = self._preprocess_dialog.radar_config_dir()
|
||||||
|
return self._preprocess_radar_config_dir
|
||||||
|
|
||||||
|
def _current_preprocess_use_all_radar_configs(self) -> bool:
|
||||||
|
"""Return whether multi-radar preprocessing is enabled in the dialog."""
|
||||||
|
if self._preprocess_dialog is not None:
|
||||||
|
self._preprocess_use_all_radar_configs = self._preprocess_dialog.use_all_radar_configs()
|
||||||
|
return bool(self._preprocess_use_all_radar_configs)
|
||||||
|
|
||||||
def _build_gui_state(self) -> GuiStateModel:
|
def _build_gui_state(self) -> GuiStateModel:
|
||||||
"""Build GUI-only persistent state from current widget values."""
|
"""Build GUI-only persistent state from current widget values."""
|
||||||
return GuiStateModel(
|
return GuiStateModel(
|
||||||
@@ -249,6 +265,8 @@ class AppWindowConfigStateBuildersMixin:
|
|||||||
),
|
),
|
||||||
preprocess_dialog=GuiPreprocessDialogStateModel(
|
preprocess_dialog=GuiPreprocessDialogStateModel(
|
||||||
set_name=self._current_preprocess_set_name(),
|
set_name=self._current_preprocess_set_name(),
|
||||||
|
radar_config_dir=self._current_preprocess_radar_config_dir(),
|
||||||
|
use_all_radar_configs=self._current_preprocess_use_all_radar_configs(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ from python_app.orchestration.preprocess_assets import (
|
|||||||
preprocess_asset_channel,
|
preprocess_asset_channel,
|
||||||
preprocess_asset_display_name,
|
preprocess_asset_display_name,
|
||||||
)
|
)
|
||||||
|
from python_app.workflows.multi_radar_capture_workflow import (
|
||||||
|
MultiRadarCaptureBatch,
|
||||||
|
MultiRadarSequentialCaptureSession,
|
||||||
|
)
|
||||||
|
from python_app.workflows.radar_config_variants import scan_radar_config_variants
|
||||||
from python_app.workflows.sequential_capture_workflow import SequentialCaptureSession
|
from python_app.workflows.sequential_capture_workflow import SequentialCaptureSession
|
||||||
|
|
||||||
|
|
||||||
@@ -16,10 +21,18 @@ class AppWindowPreprocessMixin:
|
|||||||
"""Handles preprocess set management and sequential capture workflows."""
|
"""Handles preprocess set management and sequential capture workflows."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _capture_log_entries_for_session(session: SequentialCaptureSession) -> list[str]:
|
def _capture_log_entries_for_session(session: SequentialCaptureSession | MultiRadarSequentialCaptureSession) -> list[str]:
|
||||||
"""Build capture-log rows from current session traces."""
|
"""Build capture-log rows from current session traces."""
|
||||||
display_name = preprocess_asset_display_name(session.kind)
|
display_name = preprocess_asset_display_name(session.kind)
|
||||||
total_count = session.state().total_count
|
total_count = session.state().total_count
|
||||||
|
if isinstance(session, MultiRadarSequentialCaptureSession):
|
||||||
|
return [
|
||||||
|
f"{display_name}: {index}/{total_count} | "
|
||||||
|
f"input={batch.combo.input} output={batch.combo.output} | "
|
||||||
|
f"radar_configs={session.radar_variant_count()}"
|
||||||
|
for index, batch in enumerate(session.captured_batches(), start=1)
|
||||||
|
]
|
||||||
|
|
||||||
entries: list[str] = []
|
entries: list[str] = []
|
||||||
for index, trace in enumerate(session.captured_traces(), start=1):
|
for index, trace in enumerate(session.captured_traces(), start=1):
|
||||||
entries.append(
|
entries.append(
|
||||||
@@ -98,18 +111,36 @@ class AppWindowPreprocessMixin:
|
|||||||
dialog = PreprocessDialog(self)
|
dialog = PreprocessDialog(self)
|
||||||
self._preprocess_dialog = dialog
|
self._preprocess_dialog = dialog
|
||||||
dialog.set_set_name(self._preprocess_set_name)
|
dialog.set_set_name(self._preprocess_set_name)
|
||||||
|
dialog.set_radar_config_dir(self._preprocess_radar_config_dir)
|
||||||
|
dialog.set_use_all_radar_configs(self._preprocess_use_all_radar_configs)
|
||||||
dialog.set_selected_sets(self._selected_preprocess_sets, emit_signal=False)
|
dialog.set_selected_sets(self._selected_preprocess_sets, emit_signal=False)
|
||||||
dialog.refresh_requested.connect(self._refresh_sets)
|
dialog.refresh_requested.connect(self._refresh_sets)
|
||||||
dialog.selection_changed.connect(self._on_preprocess_selection_changed)
|
dialog.selection_changed.connect(self._on_preprocess_selection_changed)
|
||||||
|
dialog.radar_config_dir_changed.connect(self._on_preprocess_radar_config_inputs_changed)
|
||||||
|
dialog.multi_radar_option_changed.connect(self._on_preprocess_radar_config_inputs_changed)
|
||||||
dialog.start_sequence_requested.connect(self._start_capture_sequence)
|
dialog.start_sequence_requested.connect(self._start_capture_sequence)
|
||||||
dialog.capture_next_requested.connect(self._capture_next_combo)
|
dialog.capture_next_requested.connect(self._capture_next_combo)
|
||||||
dialog.capture_all_requested.connect(self._capture_all_remaining)
|
dialog.capture_all_requested.connect(self._capture_all_remaining)
|
||||||
dialog.undo_last_requested.connect(self._undo_last_capture)
|
dialog.undo_last_requested.connect(self._undo_last_capture)
|
||||||
dialog.finalize_sequence_requested.connect(self._finalize_capture_sequence)
|
dialog.finalize_sequence_requested.connect(self._finalize_capture_sequence)
|
||||||
dialog.abort_sequence_requested.connect(self._abort_capture_sequence)
|
dialog.abort_sequence_requested.connect(self._abort_capture_sequence)
|
||||||
|
dialog.set_radar_config_summary(
|
||||||
|
directory_path=self._preprocess_radar_scan_summary.directory_path,
|
||||||
|
json_file_count=self._preprocess_radar_scan_summary.json_file_count,
|
||||||
|
valid_variant_count=self._preprocess_radar_scan_summary.valid_variant_count,
|
||||||
|
skipped_file_count=self._preprocess_radar_scan_summary.skipped_file_count,
|
||||||
|
duplicate_variant_count=self._preprocess_radar_scan_summary.duplicate_variant_count,
|
||||||
|
)
|
||||||
self._update_capture_dialog_state()
|
self._update_capture_dialog_state()
|
||||||
return dialog
|
return dialog
|
||||||
|
|
||||||
|
def _on_preprocess_radar_config_inputs_changed(self) -> None:
|
||||||
|
"""Persist preprocess radar-config directory options and refresh the scan result."""
|
||||||
|
dialog = self._ensure_preprocess_dialog()
|
||||||
|
self._preprocess_radar_config_dir = dialog.radar_config_dir()
|
||||||
|
self._preprocess_use_all_radar_configs = dialog.use_all_radar_configs()
|
||||||
|
self._refresh_sets()
|
||||||
|
|
||||||
def _on_preprocess_selection_changed(self) -> None:
|
def _on_preprocess_selection_changed(self) -> None:
|
||||||
"""Persist selected preprocessing set names from dialog."""
|
"""Persist selected preprocessing set names from dialog."""
|
||||||
dialog = self._ensure_preprocess_dialog()
|
dialog = self._ensure_preprocess_dialog()
|
||||||
@@ -139,9 +170,12 @@ class AppWindowPreprocessMixin:
|
|||||||
radar_key = self._radar_key_from_ui()
|
radar_key = self._radar_key_from_ui()
|
||||||
self._selected_preprocess_radar_key = radar_key
|
self._selected_preprocess_radar_key = radar_key
|
||||||
dialog = self._ensure_preprocess_dialog()
|
dialog = self._ensure_preprocess_dialog()
|
||||||
|
self._preprocess_radar_config_dir = dialog.radar_config_dir()
|
||||||
|
self._preprocess_use_all_radar_configs = dialog.use_all_radar_configs()
|
||||||
|
|
||||||
available_sets = self._available_preprocess_sets_for_radar_key(radar_key)
|
available_sets = self._available_preprocess_sets_for_radar_key(radar_key)
|
||||||
dialog.set_available_sets(available_sets)
|
dialog.set_available_sets(available_sets)
|
||||||
|
self._refresh_preprocess_radar_variants()
|
||||||
|
|
||||||
unavailable_selections: list[str] = []
|
unavailable_selections: list[str] = []
|
||||||
for key, names in available_sets.items():
|
for key, names in available_sets.items():
|
||||||
@@ -166,6 +200,32 @@ class AppWindowPreprocessMixin:
|
|||||||
details="\n".join(unavailable_selections),
|
details="\n".join(unavailable_selections),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _refresh_preprocess_radar_variants(self) -> None:
|
||||||
|
"""Reload valid radar sweep variants from the preprocess dialog directory."""
|
||||||
|
config = self._build_config()
|
||||||
|
variants, summary = scan_radar_config_variants(
|
||||||
|
self._preprocess_radar_config_dir,
|
||||||
|
base_config=config,
|
||||||
|
)
|
||||||
|
self._preprocess_radar_variants = variants
|
||||||
|
self._preprocess_radar_scan_summary = summary
|
||||||
|
|
||||||
|
if self._preprocess_dialog is not None:
|
||||||
|
self._preprocess_dialog.set_radar_config_summary(
|
||||||
|
directory_path=summary.directory_path,
|
||||||
|
json_file_count=summary.json_file_count,
|
||||||
|
valid_variant_count=summary.valid_variant_count,
|
||||||
|
skipped_file_count=summary.skipped_file_count,
|
||||||
|
duplicate_variant_count=summary.duplicate_variant_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
if summary.issues:
|
||||||
|
self._log_warning(
|
||||||
|
"Some radar config JSON files were skipped during preprocess scan.",
|
||||||
|
details="\n".join(summary.issues),
|
||||||
|
once_key=f"preprocess_radar_config_scan_{summary.directory_path}",
|
||||||
|
)
|
||||||
|
|
||||||
def _start_capture_sequence(self, kind: str) -> None:
|
def _start_capture_sequence(self, kind: str) -> None:
|
||||||
"""Start sequential capture session for requested preprocess asset."""
|
"""Start sequential capture session for requested preprocess asset."""
|
||||||
if self._capture_session is not None:
|
if self._capture_session is not None:
|
||||||
@@ -187,13 +247,20 @@ class AppWindowPreprocessMixin:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
config = self._build_config()
|
config = self._build_config()
|
||||||
radar_key = self._radar_key(config)
|
|
||||||
existing_sets = self._store.list_sets(PREPROCESS_ASSET_SPECS[kind].set_kind, radar_key)
|
|
||||||
display_name = preprocess_asset_display_name(kind)
|
display_name = preprocess_asset_display_name(kind)
|
||||||
if set_name in existing_sets:
|
if self._preprocess_use_all_radar_configs:
|
||||||
raise RuntimeError(f"Set '{set_name}' already exists for {display_name} and cannot be overwritten")
|
session = self._build_multi_radar_capture_session(config=config, kind=kind, set_name=set_name)
|
||||||
|
radar_summary = (
|
||||||
|
f"radar_variants={session.radar_variant_count()}, "
|
||||||
|
f"combos={session.state().total_count}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
session = self._build_single_radar_capture_session(config=config, kind=kind, set_name=set_name)
|
||||||
|
radar_summary = (
|
||||||
|
f"radar_key={self._radar_key(config)}, "
|
||||||
|
f"combos={session.state().total_count}"
|
||||||
|
)
|
||||||
|
|
||||||
session = SequentialCaptureSession(config=config, kind=kind, set_name=set_name)
|
|
||||||
session.open()
|
session.open()
|
||||||
self._capture_session = session
|
self._capture_session = session
|
||||||
|
|
||||||
@@ -203,14 +270,59 @@ class AppWindowPreprocessMixin:
|
|||||||
self._clear_trace_plots()
|
self._clear_trace_plots()
|
||||||
self._update_capture_dialog_state()
|
self._update_capture_dialog_state()
|
||||||
self._log(
|
self._log(
|
||||||
f"{display_name} sequence started: set={set_name}, radar_key={radar_key}, "
|
f"{display_name} sequence started: set={set_name}, {radar_summary}"
|
||||||
f"combos={session.state().total_count}"
|
|
||||||
)
|
)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
self._cleanup_capture_session()
|
self._cleanup_capture_session()
|
||||||
self._show_exception(f"Failed to start {kind} sequence", exc)
|
self._show_exception(f"Failed to start {kind} sequence", exc)
|
||||||
self._resume_pipeline_if_needed()
|
self._resume_pipeline_if_needed()
|
||||||
|
|
||||||
|
def _build_single_radar_capture_session(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
config,
|
||||||
|
kind: str,
|
||||||
|
set_name: str,
|
||||||
|
) -> SequentialCaptureSession:
|
||||||
|
"""Validate and create the existing single-radar capture session."""
|
||||||
|
radar_key = self._radar_key(config)
|
||||||
|
existing_sets = self._store.list_sets(PREPROCESS_ASSET_SPECS[kind].set_kind, radar_key)
|
||||||
|
display_name = preprocess_asset_display_name(kind)
|
||||||
|
if set_name in existing_sets:
|
||||||
|
raise RuntimeError(f"Set '{set_name}' already exists for {display_name} and cannot be overwritten")
|
||||||
|
return SequentialCaptureSession(config=config, kind=kind, set_name=set_name)
|
||||||
|
|
||||||
|
def _build_multi_radar_capture_session(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
config,
|
||||||
|
kind: str,
|
||||||
|
set_name: str,
|
||||||
|
) -> MultiRadarSequentialCaptureSession:
|
||||||
|
"""Validate and create the multi-radar capture session."""
|
||||||
|
self._refresh_preprocess_radar_variants()
|
||||||
|
if not self._preprocess_radar_variants:
|
||||||
|
raise RuntimeError("No valid radar config variants were found in the selected directory")
|
||||||
|
|
||||||
|
display_name = preprocess_asset_display_name(kind)
|
||||||
|
duplicate_set_keys: list[str] = []
|
||||||
|
for variant in self._preprocess_radar_variants:
|
||||||
|
existing_sets = self._store.list_sets(PREPROCESS_ASSET_SPECS[kind].set_kind, variant.radar_key)
|
||||||
|
if set_name in existing_sets:
|
||||||
|
duplicate_set_keys.append(f"{variant.display_name} -> {variant.radar_key}")
|
||||||
|
if duplicate_set_keys:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Set '{set_name}' already exists for {display_name} in these radar variants:\n"
|
||||||
|
+ "\n".join(duplicate_set_keys)
|
||||||
|
)
|
||||||
|
|
||||||
|
return MultiRadarSequentialCaptureSession(
|
||||||
|
base_config=config,
|
||||||
|
kind=kind,
|
||||||
|
set_name=set_name,
|
||||||
|
radar_variants=self._preprocess_radar_variants,
|
||||||
|
)
|
||||||
|
|
||||||
def _capture_next_combo(self) -> None:
|
def _capture_next_combo(self) -> None:
|
||||||
"""Capture next combo in active sequential capture session."""
|
"""Capture next combo in active sequential capture session."""
|
||||||
session = self._capture_session
|
session = self._capture_session
|
||||||
@@ -219,8 +331,8 @@ class AppWindowPreprocessMixin:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
trace = session.capture_current_combo()
|
capture_result = session.capture_current_combo()
|
||||||
self._record_preprocess_capture(session, trace)
|
self._record_preprocess_capture(session, capture_result)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
self._show_exception("Failed to capture preprocess combo", exc)
|
self._show_exception("Failed to capture preprocess combo", exc)
|
||||||
self._abort_capture_sequence()
|
self._abort_capture_sequence()
|
||||||
@@ -247,31 +359,47 @@ class AppWindowPreprocessMixin:
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
while not session.is_complete():
|
while not session.is_complete():
|
||||||
trace = session.capture_current_combo()
|
capture_result = session.capture_current_combo()
|
||||||
self._record_preprocess_capture(session, trace)
|
self._record_preprocess_capture(session, capture_result)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
self._show_exception("Failed to capture preprocess combo", exc)
|
self._show_exception("Failed to capture preprocess combo", exc)
|
||||||
self._abort_capture_sequence()
|
self._abort_capture_sequence()
|
||||||
|
|
||||||
def _record_preprocess_capture(self, session: SequentialCaptureSession, trace) -> None:
|
def _record_preprocess_capture(
|
||||||
|
self,
|
||||||
|
session: SequentialCaptureSession | MultiRadarSequentialCaptureSession,
|
||||||
|
capture_result,
|
||||||
|
) -> None:
|
||||||
"""Update UI, preview, and logs after one successful preprocess capture."""
|
"""Update UI, preview, and logs after one successful preprocess capture."""
|
||||||
dialog = self._ensure_preprocess_dialog()
|
dialog = self._ensure_preprocess_dialog()
|
||||||
state = session.state()
|
state = session.state()
|
||||||
display_name = preprocess_asset_display_name(session.kind)
|
display_name = preprocess_asset_display_name(session.kind)
|
||||||
channel = preprocess_asset_channel(session.kind)
|
channel = preprocess_asset_channel(session.kind)
|
||||||
|
if isinstance(session, MultiRadarSequentialCaptureSession):
|
||||||
|
assert isinstance(capture_result, MultiRadarCaptureBatch)
|
||||||
|
trace = capture_result.display_trace
|
||||||
|
input_pos = capture_result.combo.input
|
||||||
|
output_pos = capture_result.combo.output
|
||||||
|
extra_details = f" | radar_configs={len(capture_result.traces)}"
|
||||||
|
else:
|
||||||
|
trace = capture_result
|
||||||
|
input_pos = trace.combo.input_pos
|
||||||
|
output_pos = trace.combo.output_pos
|
||||||
|
extra_details = ""
|
||||||
|
|
||||||
dialog.append_capture_log_entry(
|
dialog.append_capture_log_entry(
|
||||||
kind=display_name,
|
kind=display_name,
|
||||||
captured_count=state.captured_count,
|
captured_count=state.captured_count,
|
||||||
total_count=state.total_count,
|
total_count=state.total_count,
|
||||||
input_pos=trace.combo.input_pos,
|
input_pos=input_pos,
|
||||||
output_pos=trace.combo.output_pos,
|
output_pos=output_pos,
|
||||||
|
extra_details=extra_details,
|
||||||
)
|
)
|
||||||
dialog.draw_last_trace(trace, title=f"{display_name} captured", channel=channel)
|
dialog.draw_last_trace(trace, title=f"{display_name} captured", channel=channel)
|
||||||
self._draw_single_trace(trace, title=f"{display_name} last trace", channel=channel)
|
self._draw_single_trace(trace, title=f"{display_name} last trace", channel=channel)
|
||||||
self._log(
|
self._log(
|
||||||
f"{display_name} capture: {state.captured_count}/{state.total_count} | "
|
f"{display_name} capture: {state.captured_count}/{state.total_count} | "
|
||||||
f"input={trace.combo.input_pos} output={trace.combo.output_pos}"
|
f"input={input_pos} output={output_pos}{extra_details}"
|
||||||
)
|
)
|
||||||
|
|
||||||
self._update_capture_dialog_state()
|
self._update_capture_dialog_state()
|
||||||
@@ -291,10 +419,19 @@ class AppWindowPreprocessMixin:
|
|||||||
|
|
||||||
dialog = self._ensure_preprocess_dialog()
|
dialog = self._ensure_preprocess_dialog()
|
||||||
try:
|
try:
|
||||||
removed_trace = session.undo_last_capture()
|
removed_capture = session.undo_last_capture()
|
||||||
state = session.state()
|
state = session.state()
|
||||||
display_name = preprocess_asset_display_name(session.kind)
|
display_name = preprocess_asset_display_name(session.kind)
|
||||||
channel = preprocess_asset_channel(session.kind)
|
channel = preprocess_asset_channel(session.kind)
|
||||||
|
if isinstance(session, MultiRadarSequentialCaptureSession):
|
||||||
|
assert isinstance(removed_capture, MultiRadarCaptureBatch)
|
||||||
|
removed_input = removed_capture.combo.input
|
||||||
|
removed_output = removed_capture.combo.output
|
||||||
|
extra_details = f" | radar_configs={len(removed_capture.traces)}"
|
||||||
|
else:
|
||||||
|
removed_input = removed_capture.combo.input_pos
|
||||||
|
removed_output = removed_capture.combo.output_pos
|
||||||
|
extra_details = ""
|
||||||
|
|
||||||
dialog.set_capture_log_entries(self._capture_log_entries_for_session(session))
|
dialog.set_capture_log_entries(self._capture_log_entries_for_session(session))
|
||||||
last_trace = session.last_captured_trace()
|
last_trace = session.last_captured_trace()
|
||||||
@@ -309,8 +446,8 @@ class AppWindowPreprocessMixin:
|
|||||||
|
|
||||||
self._update_capture_dialog_state()
|
self._update_capture_dialog_state()
|
||||||
self._log(
|
self._log(
|
||||||
f"{display_name} undo last capture: removed input={removed_trace.combo.input_pos} "
|
f"{display_name} undo last capture: removed input={removed_input} "
|
||||||
f"output={removed_trace.combo.output_pos}; remaining={state.captured_count}/{state.total_count}"
|
f"output={removed_output}{extra_details}; remaining={state.captured_count}/{state.total_count}"
|
||||||
)
|
)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
self._show_exception("Failed to undo last preprocess capture", exc)
|
self._show_exception("Failed to undo last preprocess capture", exc)
|
||||||
@@ -333,16 +470,33 @@ class AppWindowPreprocessMixin:
|
|||||||
|
|
||||||
dialog = self._ensure_preprocess_dialog()
|
dialog = self._ensure_preprocess_dialog()
|
||||||
try:
|
try:
|
||||||
radar_key, collection = session.finalize(self._store)
|
|
||||||
set_name = session.set_name
|
set_name = session.set_name
|
||||||
kind = session.kind
|
kind = session.kind
|
||||||
display_name = preprocess_asset_display_name(kind)
|
display_name = preprocess_asset_display_name(kind)
|
||||||
|
if isinstance(session, MultiRadarSequentialCaptureSession):
|
||||||
|
saved_sets = session.finalize(self._store)
|
||||||
|
else:
|
||||||
|
radar_key, collection = session.finalize(self._store)
|
||||||
self._cleanup_capture_session()
|
self._cleanup_capture_session()
|
||||||
|
|
||||||
self._selected_preprocess_sets[kind] = set_name
|
self._selected_preprocess_sets[kind] = set_name
|
||||||
self._refresh_sets()
|
self._refresh_sets()
|
||||||
dialog.set_status(f"{display_name} set saved: {set_name} ({len(collection.traces)} traces)")
|
if isinstance(session, MultiRadarSequentialCaptureSession):
|
||||||
self._log(f"{display_name} sequence completed and saved: set={set_name}, key={radar_key}")
|
assert isinstance(saved_sets, list)
|
||||||
|
dialog.set_status(
|
||||||
|
f"{display_name} set saved: {set_name} ({len(saved_sets)} radar variants)"
|
||||||
|
)
|
||||||
|
saved_summary = ", ".join(
|
||||||
|
f"{saved.display_name}:{saved.trace_count}"
|
||||||
|
for saved in saved_sets
|
||||||
|
)
|
||||||
|
self._log(
|
||||||
|
f"{display_name} sequence completed and saved: set={set_name}, "
|
||||||
|
f"radar_variants={len(saved_sets)} [{saved_summary}]"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
dialog.set_status(f"{display_name} set saved: {set_name} ({len(collection.traces)} traces)")
|
||||||
|
self._log(f"{display_name} sequence completed and saved: set={set_name}, key={radar_key}")
|
||||||
self._resume_pipeline_if_needed()
|
self._resume_pipeline_if_needed()
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
self._show_exception("Failed to save preprocess set", exc)
|
self._show_exception("Failed to save preprocess set", exc)
|
||||||
@@ -375,6 +529,7 @@ class AppWindowPreprocessMixin:
|
|||||||
can_undo=False,
|
can_undo=False,
|
||||||
can_finalize=False,
|
can_finalize=False,
|
||||||
can_capture_all=False,
|
can_capture_all=False,
|
||||||
|
variant_count=1,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -394,6 +549,7 @@ class AppWindowPreprocessMixin:
|
|||||||
can_undo=state.can_undo,
|
can_undo=state.can_undo,
|
||||||
can_finalize=state.is_complete,
|
can_finalize=state.is_complete,
|
||||||
can_capture_all=(not state.is_complete and state.current_combo is not None),
|
can_capture_all=(not state.is_complete and state.current_combo is not None),
|
||||||
|
variant_count=state.variant_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _cleanup_capture_session(self) -> None:
|
def _cleanup_capture_session(self) -> None:
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from PyQt6.QtCore import QSignalBlocker, pyqtSignal
|
from PyQt6.QtCore import QSignalBlocker, pyqtSignal
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
|
QCheckBox,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
QDialog,
|
QDialog,
|
||||||
|
QFileDialog,
|
||||||
QFormLayout,
|
QFormLayout,
|
||||||
QGridLayout,
|
QGridLayout,
|
||||||
QGroupBox,
|
QGroupBox,
|
||||||
@@ -34,6 +36,8 @@ class PreprocessDialog(QDialog):
|
|||||||
|
|
||||||
refresh_requested = pyqtSignal()
|
refresh_requested = pyqtSignal()
|
||||||
selection_changed = pyqtSignal()
|
selection_changed = pyqtSignal()
|
||||||
|
radar_config_dir_changed = pyqtSignal()
|
||||||
|
multi_radar_option_changed = pyqtSignal()
|
||||||
start_sequence_requested = pyqtSignal(str)
|
start_sequence_requested = pyqtSignal(str)
|
||||||
capture_next_requested = pyqtSignal()
|
capture_next_requested = pyqtSignal()
|
||||||
capture_all_requested = pyqtSignal()
|
capture_all_requested = pyqtSignal()
|
||||||
@@ -88,10 +92,39 @@ class PreprocessDialog(QDialog):
|
|||||||
header_row.addWidget(self._set_name_input, stretch=1)
|
header_row.addWidget(self._set_name_input, stretch=1)
|
||||||
header_row.addWidget(refresh_button)
|
header_row.addWidget(refresh_button)
|
||||||
layout.addLayout(header_row)
|
layout.addLayout(header_row)
|
||||||
|
layout.addWidget(self._build_radar_config_group(group))
|
||||||
|
|
||||||
layout.addWidget(self._build_selector_group("S21", VISIBLE_PREPROCESS_ASSET_KEYS, group))
|
layout.addWidget(self._build_selector_group("S21", VISIBLE_PREPROCESS_ASSET_KEYS, group))
|
||||||
return group
|
return group
|
||||||
|
|
||||||
|
def _build_radar_config_group(self, parent: QGroupBox) -> QGroupBox:
|
||||||
|
"""Build radar-config directory controls for multi-radar preprocessing."""
|
||||||
|
group = QGroupBox("Radar Config Variants", parent)
|
||||||
|
layout = QVBoxLayout(group)
|
||||||
|
|
||||||
|
dir_row = QHBoxLayout()
|
||||||
|
self._radar_config_dir_input = QLineEdit(group)
|
||||||
|
self._radar_config_dir_input.setPlaceholderText("Directory with radar JSON configs")
|
||||||
|
self._radar_config_dir_input.editingFinished.connect(self.radar_config_dir_changed.emit)
|
||||||
|
browse_button = QPushButton("Browse...", group)
|
||||||
|
browse_button.clicked.connect(self._choose_radar_config_dir)
|
||||||
|
dir_row.addWidget(QLabel("Directory"))
|
||||||
|
dir_row.addWidget(self._radar_config_dir_input, stretch=1)
|
||||||
|
dir_row.addWidget(browse_button)
|
||||||
|
layout.addLayout(dir_row)
|
||||||
|
|
||||||
|
self._radar_config_summary_label = QLabel("No radar config directory selected.", group)
|
||||||
|
self._radar_config_summary_label.setWordWrap(True)
|
||||||
|
layout.addWidget(self._radar_config_summary_label)
|
||||||
|
|
||||||
|
self._use_all_radar_configs_checkbox = QCheckBox(
|
||||||
|
"Capture current combo for all found radar configs",
|
||||||
|
group,
|
||||||
|
)
|
||||||
|
self._use_all_radar_configs_checkbox.toggled.connect(self.multi_radar_option_changed.emit)
|
||||||
|
layout.addWidget(self._use_all_radar_configs_checkbox)
|
||||||
|
return group
|
||||||
|
|
||||||
def _build_selector_group(self, title: str, keys: tuple[str, ...], parent: QGroupBox) -> QGroupBox:
|
def _build_selector_group(self, title: str, keys: tuple[str, ...], parent: QGroupBox) -> QGroupBox:
|
||||||
"""Build one selector subgroup for a channel family."""
|
"""Build one selector subgroup for a channel family."""
|
||||||
group = QGroupBox(title, parent)
|
group = QGroupBox(title, parent)
|
||||||
@@ -201,6 +234,41 @@ class PreprocessDialog(QDialog):
|
|||||||
"""Replace requested target set name."""
|
"""Replace requested target set name."""
|
||||||
self._set_name_input.setText(value)
|
self._set_name_input.setText(value)
|
||||||
|
|
||||||
|
def radar_config_dir(self) -> str:
|
||||||
|
"""Return configured radar-config directory path."""
|
||||||
|
return self._radar_config_dir_input.text().strip()
|
||||||
|
|
||||||
|
def set_radar_config_dir(self, value: str) -> None:
|
||||||
|
"""Replace configured radar-config directory path."""
|
||||||
|
self._radar_config_dir_input.setText(value)
|
||||||
|
|
||||||
|
def use_all_radar_configs(self) -> bool:
|
||||||
|
"""Return whether multi-radar capture mode is enabled."""
|
||||||
|
return bool(self._use_all_radar_configs_checkbox.isChecked())
|
||||||
|
|
||||||
|
def set_use_all_radar_configs(self, enabled: bool) -> None:
|
||||||
|
"""Update multi-radar capture checkbox state."""
|
||||||
|
self._use_all_radar_configs_checkbox.setChecked(bool(enabled))
|
||||||
|
|
||||||
|
def set_radar_config_summary(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
directory_path: str,
|
||||||
|
json_file_count: int,
|
||||||
|
valid_variant_count: int,
|
||||||
|
skipped_file_count: int,
|
||||||
|
duplicate_variant_count: int,
|
||||||
|
) -> None:
|
||||||
|
"""Update one-line radar-config scan summary."""
|
||||||
|
if not directory_path:
|
||||||
|
self._radar_config_summary_label.setText("No radar config directory selected.")
|
||||||
|
return
|
||||||
|
self._radar_config_summary_label.setText(
|
||||||
|
f"Directory: {directory_path} | "
|
||||||
|
f"json={json_file_count}, valid={valid_variant_count}, "
|
||||||
|
f"skipped={skipped_file_count}, duplicates={duplicate_variant_count}"
|
||||||
|
)
|
||||||
|
|
||||||
def selection_snapshot(self) -> dict[str, str]:
|
def selection_snapshot(self) -> dict[str, str]:
|
||||||
"""Return currently selected set names keyed by preprocess asset key."""
|
"""Return currently selected set names keyed by preprocess asset key."""
|
||||||
return {key: self._set_combos[key].currentText().strip() for key in VISIBLE_PREPROCESS_ASSET_KEYS}
|
return {key: self._set_combos[key].currentText().strip() for key in VISIBLE_PREPROCESS_ASSET_KEYS}
|
||||||
@@ -221,11 +289,12 @@ class PreprocessDialog(QDialog):
|
|||||||
total_count: int,
|
total_count: int,
|
||||||
input_pos: int,
|
input_pos: int,
|
||||||
output_pos: int,
|
output_pos: int,
|
||||||
|
extra_details: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Append one capture progress row to dialog log."""
|
"""Append one capture progress row to dialog log."""
|
||||||
self._capture_log.appendPlainText(
|
self._capture_log.appendPlainText(
|
||||||
f"{kind}: {captured_count}/{total_count} | "
|
f"{kind}: {captured_count}/{total_count} | "
|
||||||
f"input={input_pos} output={output_pos}"
|
f"input={input_pos} output={output_pos}{extra_details}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_capture_state(
|
def set_capture_state(
|
||||||
@@ -239,6 +308,7 @@ class PreprocessDialog(QDialog):
|
|||||||
can_undo: bool,
|
can_undo: bool,
|
||||||
can_finalize: bool,
|
can_finalize: bool,
|
||||||
can_capture_all: bool,
|
can_capture_all: bool,
|
||||||
|
variant_count: int = 1,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update sequence progress/status widgets."""
|
"""Update sequence progress/status widgets."""
|
||||||
if kind is None:
|
if kind is None:
|
||||||
@@ -265,7 +335,10 @@ class PreprocessDialog(QDialog):
|
|||||||
self._combo_label.setText("<complete>")
|
self._combo_label.setText("<complete>")
|
||||||
self._capture_next_button.setEnabled(False)
|
self._capture_next_button.setEnabled(False)
|
||||||
else:
|
else:
|
||||||
self._combo_label.setText(f"input={next_input}, output={next_output}")
|
combo_text = f"input={next_input}, output={next_output}"
|
||||||
|
if int(variant_count) > 1:
|
||||||
|
combo_text += f" | radar configs={int(variant_count)}"
|
||||||
|
self._combo_label.setText(combo_text)
|
||||||
self._capture_next_button.setEnabled(True)
|
self._capture_next_button.setEnabled(True)
|
||||||
|
|
||||||
def set_available_sets(self, available_sets: dict[str, list[str]]) -> None:
|
def set_available_sets(self, available_sets: dict[str, list[str]]) -> None:
|
||||||
@@ -360,6 +433,18 @@ class PreprocessDialog(QDialog):
|
|||||||
"""Emit current selection snapshot change."""
|
"""Emit current selection snapshot change."""
|
||||||
self.selection_changed.emit()
|
self.selection_changed.emit()
|
||||||
|
|
||||||
|
def _choose_radar_config_dir(self) -> None:
|
||||||
|
"""Pick radar-config directory and notify controller code."""
|
||||||
|
selected_dir = QFileDialog.getExistingDirectory(
|
||||||
|
self,
|
||||||
|
"Select Radar Config Directory",
|
||||||
|
self.radar_config_dir(),
|
||||||
|
)
|
||||||
|
if not selected_dir:
|
||||||
|
return
|
||||||
|
self._radar_config_dir_input.setText(selected_dir)
|
||||||
|
self.radar_config_dir_changed.emit()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _asset_row_label(key: str) -> str:
|
def _asset_row_label(key: str) -> str:
|
||||||
"""Return short selector label for one preprocess asset."""
|
"""Return short selector label for one preprocess asset."""
|
||||||
|
|||||||
@@ -313,6 +313,18 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
|
|||||||
gui.preprocess_dialog.set_name,
|
gui.preprocess_dialog.set_name,
|
||||||
"gui.preprocess_dialog",
|
"gui.preprocess_dialog",
|
||||||
),
|
),
|
||||||
|
radar_config_dir=_optional_string(
|
||||||
|
preprocess_dialog_object,
|
||||||
|
"radar_config_dir",
|
||||||
|
gui.preprocess_dialog.radar_config_dir,
|
||||||
|
"gui.preprocess_dialog",
|
||||||
|
),
|
||||||
|
use_all_radar_configs=_optional_bool(
|
||||||
|
preprocess_dialog_object,
|
||||||
|
"use_all_radar_configs",
|
||||||
|
gui.preprocess_dialog.use_all_radar_configs,
|
||||||
|
"gui.preprocess_dialog",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
profile.gui = gui
|
profile.gui = gui
|
||||||
@@ -377,6 +389,8 @@ def gui_profile_to_dict(model: GuiProfileModel) -> dict[str, Any]:
|
|||||||
},
|
},
|
||||||
"preprocess_dialog": {
|
"preprocess_dialog": {
|
||||||
"set_name": gui.preprocess_dialog.set_name,
|
"set_name": gui.preprocess_dialog.set_name,
|
||||||
|
"radar_config_dir": gui.preprocess_dialog.radar_config_dir,
|
||||||
|
"use_all_radar_configs": gui.preprocess_dialog.use_all_radar_configs,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return payload
|
return payload
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ class GuiPreprocessDialogStateModel:
|
|||||||
"""UI-only defaults for preprocessing dialog controls."""
|
"""UI-only defaults for preprocessing dialog controls."""
|
||||||
|
|
||||||
set_name: str = "set_001"
|
set_name: str = "set_001"
|
||||||
|
radar_config_dir: str = ""
|
||||||
|
use_all_radar_configs: bool = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
"""Sequential capture workflow that captures each combo across multiple radar sweep variants."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from python_app.hardware_full.librevna_service import LibreVnaService
|
||||||
|
from python_app.hardware_full.switch_service import SwitchService
|
||||||
|
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
||||||
|
from python_app.models.run_config_model import ComboModel, RunConfigModel
|
||||||
|
from python_app.storage.npz_store import NpzStore
|
||||||
|
from python_app.workflows.radar_config_variants import RadarConfigVariant
|
||||||
|
from python_app.workflows.sequential_capture_workflow import SequentialCaptureState
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class MultiRadarCaptureBatch:
|
||||||
|
"""One completed combo capture across all configured radar variants."""
|
||||||
|
|
||||||
|
combo: ComboModel
|
||||||
|
traces: tuple[TraceData, ...]
|
||||||
|
variant_labels: tuple[str, ...]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_trace(self) -> TraceData:
|
||||||
|
"""Return the last trace in the batch for preview rendering."""
|
||||||
|
return self.traces[-1]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class MultiRadarSavedSet:
|
||||||
|
"""One preprocess set persisted for one radar variant."""
|
||||||
|
|
||||||
|
display_name: str
|
||||||
|
radar_key: str
|
||||||
|
trace_count: int
|
||||||
|
|
||||||
|
|
||||||
|
class MultiRadarSequentialCaptureSession:
|
||||||
|
"""Capture a full combo matrix for one preprocess asset across multiple radar sweep variants."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
base_config: RunConfigModel,
|
||||||
|
kind: str,
|
||||||
|
set_name: str,
|
||||||
|
radar_variants: list[RadarConfigVariant],
|
||||||
|
) -> None:
|
||||||
|
"""Create capture session for one preprocess asset set and multiple radar variants."""
|
||||||
|
if kind not in {"s21_calibration", "s21_reference", "s11_open", "s11_short", "s11_load", "s11_reference"}:
|
||||||
|
raise RuntimeError(f"Unsupported capture kind: {kind}")
|
||||||
|
if not set_name:
|
||||||
|
raise RuntimeError("Set name is required")
|
||||||
|
if not radar_variants:
|
||||||
|
raise RuntimeError("At least one radar variant is required")
|
||||||
|
|
||||||
|
self._base_config = base_config
|
||||||
|
self._kind = kind
|
||||||
|
self._set_name = set_name
|
||||||
|
self._radar_variants = list(radar_variants)
|
||||||
|
self._combos = RunConfigModel.build_full_combos(
|
||||||
|
base_config.input_switch.positions,
|
||||||
|
base_config.output_switch.positions,
|
||||||
|
)
|
||||||
|
if not self._combos:
|
||||||
|
raise RuntimeError("No switch combinations available for capture")
|
||||||
|
|
||||||
|
self._captured_batches: list[MultiRadarCaptureBatch] = []
|
||||||
|
self._traces_by_radar_key = {
|
||||||
|
variant.radar_key: []
|
||||||
|
for variant in self._radar_variants
|
||||||
|
}
|
||||||
|
self._next_index = 0
|
||||||
|
self._opened = False
|
||||||
|
|
||||||
|
self._radar = LibreVnaService(serial=base_config.radar.serial or None)
|
||||||
|
self._input_switch = SwitchService(
|
||||||
|
name=base_config.input_switch.name,
|
||||||
|
positions=base_config.input_switch.positions,
|
||||||
|
mode=base_config.input_switch.driver_mode,
|
||||||
|
driver=base_config.input_switch.driver,
|
||||||
|
gpio_chip=base_config.input_switch.gpio_chip,
|
||||||
|
pin_a=base_config.input_switch.pin_a,
|
||||||
|
pin_b=base_config.input_switch.pin_b,
|
||||||
|
invert_logic=base_config.input_switch.invert_logic,
|
||||||
|
)
|
||||||
|
self._output_switch = SwitchService(
|
||||||
|
name=base_config.output_switch.name,
|
||||||
|
positions=base_config.output_switch.positions,
|
||||||
|
mode=base_config.output_switch.driver_mode,
|
||||||
|
driver=base_config.output_switch.driver,
|
||||||
|
gpio_chip=base_config.output_switch.gpio_chip,
|
||||||
|
pin_a=base_config.output_switch.pin_a,
|
||||||
|
pin_b=base_config.output_switch.pin_b,
|
||||||
|
invert_logic=base_config.output_switch.invert_logic,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kind(self) -> str:
|
||||||
|
"""Return canonical preprocess asset key for this capture session."""
|
||||||
|
return self._kind
|
||||||
|
|
||||||
|
@property
|
||||||
|
def set_name(self) -> str:
|
||||||
|
"""Return destination set name."""
|
||||||
|
return self._set_name
|
||||||
|
|
||||||
|
def open(self) -> None:
|
||||||
|
"""Open radar and switch resources."""
|
||||||
|
if self._opened:
|
||||||
|
return
|
||||||
|
self._opened = True
|
||||||
|
try:
|
||||||
|
self._radar.open()
|
||||||
|
self._radar.configure(self._base_config.radar.sweep)
|
||||||
|
self._input_switch.open()
|
||||||
|
self._output_switch.open()
|
||||||
|
except Exception:
|
||||||
|
self.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Close all opened hardware resources."""
|
||||||
|
with suppress(Exception):
|
||||||
|
self._output_switch.close()
|
||||||
|
with suppress(Exception):
|
||||||
|
self._input_switch.close()
|
||||||
|
with suppress(Exception):
|
||||||
|
self._radar.close()
|
||||||
|
self._opened = False
|
||||||
|
|
||||||
|
def state(self) -> SequentialCaptureState:
|
||||||
|
"""Return current progress snapshot."""
|
||||||
|
current_combo = self._current_combo()
|
||||||
|
return SequentialCaptureState(
|
||||||
|
kind=self._kind,
|
||||||
|
set_name=self._set_name,
|
||||||
|
captured_count=len(self._captured_batches),
|
||||||
|
total_count=len(self._combos),
|
||||||
|
current_combo=current_combo,
|
||||||
|
can_undo=bool(self._captured_batches),
|
||||||
|
is_complete=self.is_complete(),
|
||||||
|
variant_count=len(self._radar_variants),
|
||||||
|
)
|
||||||
|
|
||||||
|
def capture_current_combo(self) -> MultiRadarCaptureBatch:
|
||||||
|
"""Capture the current combo across all radar variants and advance the combo cursor."""
|
||||||
|
if not self._opened:
|
||||||
|
raise RuntimeError("Capture session is not opened")
|
||||||
|
combo = self._current_combo()
|
||||||
|
if combo is None:
|
||||||
|
raise RuntimeError("Capture session is already complete")
|
||||||
|
|
||||||
|
self._output_switch.switch_to(combo.output)
|
||||||
|
self._input_switch.switch_to(combo.input)
|
||||||
|
if self._base_config.runtime.settling_ms > 0:
|
||||||
|
time.sleep(self._base_config.runtime.settling_ms / 1000.0)
|
||||||
|
|
||||||
|
traces: list[TraceData] = []
|
||||||
|
variant_labels: list[str] = []
|
||||||
|
for variant in self._radar_variants:
|
||||||
|
self._radar.configure(variant.config.radar.sweep)
|
||||||
|
if self._base_config.runtime.settling_ms > 0:
|
||||||
|
time.sleep(self._base_config.runtime.settling_ms / 1000.0)
|
||||||
|
sweep = self._radar.acquire()
|
||||||
|
trace = TraceData(
|
||||||
|
combo=ComboKey(input_pos=combo.input, output_pos=combo.output),
|
||||||
|
frequency_hz=np.asarray(sweep.x, dtype=np.float32),
|
||||||
|
s11=np.asarray(sweep.trace("s11"), dtype=np.complex64),
|
||||||
|
s21=np.asarray(sweep.trace("s21"), dtype=np.complex64),
|
||||||
|
)
|
||||||
|
traces.append(trace)
|
||||||
|
variant_labels.append(variant.display_name)
|
||||||
|
self._traces_by_radar_key[variant.radar_key].append(trace)
|
||||||
|
|
||||||
|
batch = MultiRadarCaptureBatch(
|
||||||
|
combo=combo,
|
||||||
|
traces=tuple(traces),
|
||||||
|
variant_labels=tuple(variant_labels),
|
||||||
|
)
|
||||||
|
self._captured_batches.append(batch)
|
||||||
|
self._next_index += 1
|
||||||
|
return batch
|
||||||
|
|
||||||
|
def undo_last_capture(self) -> MultiRadarCaptureBatch:
|
||||||
|
"""Remove the most recently captured combo batch and rewind the cursor."""
|
||||||
|
if not self._opened:
|
||||||
|
raise RuntimeError("Capture session is not opened")
|
||||||
|
if not self._captured_batches or self._next_index <= 0:
|
||||||
|
raise RuntimeError("No captured combo is available to undo")
|
||||||
|
|
||||||
|
expected_combo = self._combos[self._next_index - 1]
|
||||||
|
removed_batch = self._captured_batches[-1]
|
||||||
|
if (
|
||||||
|
int(removed_batch.combo.input) != int(expected_combo.input)
|
||||||
|
or int(removed_batch.combo.output) != int(expected_combo.output)
|
||||||
|
):
|
||||||
|
raise RuntimeError("Capture session state is inconsistent; last batch does not match rewind combo")
|
||||||
|
|
||||||
|
for variant in self._radar_variants:
|
||||||
|
traces = self._traces_by_radar_key[variant.radar_key]
|
||||||
|
if not traces:
|
||||||
|
raise RuntimeError("Capture session state is inconsistent; missing trace during undo")
|
||||||
|
traces.pop()
|
||||||
|
|
||||||
|
self._next_index -= 1
|
||||||
|
self._captured_batches.pop()
|
||||||
|
return removed_batch
|
||||||
|
|
||||||
|
def last_captured_trace(self) -> TraceData | None:
|
||||||
|
"""Return the most recent trace from the most recent combo batch, if any."""
|
||||||
|
if not self._captured_batches:
|
||||||
|
return None
|
||||||
|
return self._captured_batches[-1].display_trace
|
||||||
|
|
||||||
|
def captured_batches(self) -> list[MultiRadarCaptureBatch]:
|
||||||
|
"""Return completed combo batches in capture order."""
|
||||||
|
return list(self._captured_batches)
|
||||||
|
|
||||||
|
def radar_variant_count(self) -> int:
|
||||||
|
"""Return how many radar variants are captured per combo."""
|
||||||
|
return len(self._radar_variants)
|
||||||
|
|
||||||
|
def is_complete(self) -> bool:
|
||||||
|
"""Return `True` when all combos were captured."""
|
||||||
|
return self._next_index >= len(self._combos)
|
||||||
|
|
||||||
|
def finalize(self, store: NpzStore) -> list[MultiRadarSavedSet]:
|
||||||
|
"""Persist completed captures as one preprocess set per radar variant."""
|
||||||
|
if not self.is_complete():
|
||||||
|
raise RuntimeError("Capture session is not complete")
|
||||||
|
|
||||||
|
monotonic_ns = time.monotonic_ns()
|
||||||
|
saved_sets: list[MultiRadarSavedSet] = []
|
||||||
|
for variant in self._radar_variants:
|
||||||
|
traces = list(self._traces_by_radar_key[variant.radar_key])
|
||||||
|
collection = SweepCollection(
|
||||||
|
collection_id=1,
|
||||||
|
monotonic_ns=monotonic_ns,
|
||||||
|
traces=traces,
|
||||||
|
)
|
||||||
|
store.save_set(self._kind, variant.radar_key, self._set_name, collection)
|
||||||
|
saved_sets.append(
|
||||||
|
MultiRadarSavedSet(
|
||||||
|
display_name=variant.display_name,
|
||||||
|
radar_key=variant.radar_key,
|
||||||
|
trace_count=len(traces),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return saved_sets
|
||||||
|
|
||||||
|
def _current_combo(self) -> ComboModel | None:
|
||||||
|
"""Return next combo to capture, or `None` if session is complete."""
|
||||||
|
if self._next_index >= len(self._combos):
|
||||||
|
return None
|
||||||
|
return self._combos[self._next_index]
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
"""Helpers for loading radar sweep variants from a directory of JSON files."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from python_app.models.run_config_model import RunConfigModel
|
||||||
|
from python_app.storage.npz_store import radar_key_from_config
|
||||||
|
|
||||||
|
_RADAR_SWEEP_KEYS = (
|
||||||
|
"start_hz",
|
||||||
|
"stop_hz",
|
||||||
|
"points",
|
||||||
|
"if_bandwidth_hz",
|
||||||
|
"stimulus_power_dbm",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RadarConfigVariant:
|
||||||
|
"""One capture-ready radar sweep variant loaded from JSON."""
|
||||||
|
|
||||||
|
source_path: Path
|
||||||
|
display_name: str
|
||||||
|
config: RunConfigModel
|
||||||
|
radar_key: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class RadarConfigScanSummary:
|
||||||
|
"""Scan results for one radar-config directory refresh."""
|
||||||
|
|
||||||
|
directory_path: str
|
||||||
|
json_file_count: int
|
||||||
|
valid_variant_count: int
|
||||||
|
skipped_file_count: int
|
||||||
|
duplicate_variant_count: int
|
||||||
|
issues: tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
|
def scan_radar_config_variants(
|
||||||
|
directory_path: str,
|
||||||
|
*,
|
||||||
|
base_config: RunConfigModel,
|
||||||
|
) -> tuple[list[RadarConfigVariant], RadarConfigScanSummary]:
|
||||||
|
"""Load valid radar sweep variants from `directory_path` using `base_config` as the baseline."""
|
||||||
|
normalized_path = str(directory_path).strip()
|
||||||
|
if not normalized_path:
|
||||||
|
return [], RadarConfigScanSummary(
|
||||||
|
directory_path="",
|
||||||
|
json_file_count=0,
|
||||||
|
valid_variant_count=0,
|
||||||
|
skipped_file_count=0,
|
||||||
|
duplicate_variant_count=0,
|
||||||
|
issues=(),
|
||||||
|
)
|
||||||
|
|
||||||
|
directory = Path(normalized_path).expanduser()
|
||||||
|
if not directory.exists():
|
||||||
|
return [], RadarConfigScanSummary(
|
||||||
|
directory_path=str(directory),
|
||||||
|
json_file_count=0,
|
||||||
|
valid_variant_count=0,
|
||||||
|
skipped_file_count=0,
|
||||||
|
duplicate_variant_count=0,
|
||||||
|
issues=(f"Directory does not exist: {directory}",),
|
||||||
|
)
|
||||||
|
if not directory.is_dir():
|
||||||
|
return [], RadarConfigScanSummary(
|
||||||
|
directory_path=str(directory),
|
||||||
|
json_file_count=0,
|
||||||
|
valid_variant_count=0,
|
||||||
|
skipped_file_count=0,
|
||||||
|
duplicate_variant_count=0,
|
||||||
|
issues=(f"Path is not a directory: {directory}",),
|
||||||
|
)
|
||||||
|
|
||||||
|
variants: list[RadarConfigVariant] = []
|
||||||
|
issues: list[str] = []
|
||||||
|
duplicate_variant_count = 0
|
||||||
|
seen_radar_keys: set[str] = set()
|
||||||
|
json_paths = sorted(path for path in directory.glob("*.json") if path.is_file())
|
||||||
|
|
||||||
|
for path in json_paths:
|
||||||
|
try:
|
||||||
|
variant = _load_radar_config_variant(path, base_config=base_config)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
issues.append(f"{path.name}: {type(exc).__name__}: {exc}")
|
||||||
|
continue
|
||||||
|
if variant.radar_key in seen_radar_keys:
|
||||||
|
duplicate_variant_count += 1
|
||||||
|
issues.append(
|
||||||
|
f"{path.name}: duplicate radar variant key {variant.radar_key}; keeping the first matching file only"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
seen_radar_keys.add(variant.radar_key)
|
||||||
|
variants.append(variant)
|
||||||
|
|
||||||
|
return variants, RadarConfigScanSummary(
|
||||||
|
directory_path=str(directory),
|
||||||
|
json_file_count=len(json_paths),
|
||||||
|
valid_variant_count=len(variants),
|
||||||
|
skipped_file_count=max(0, len(json_paths) - len(variants) - duplicate_variant_count),
|
||||||
|
duplicate_variant_count=duplicate_variant_count,
|
||||||
|
issues=tuple(issues),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_radar_config_variant(path: Path, *, base_config: RunConfigModel) -> RadarConfigVariant:
|
||||||
|
"""Load one radar sweep variant by overlaying JSON `radar.sweep` onto `base_config`."""
|
||||||
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise ValueError("JSON root must be an object")
|
||||||
|
|
||||||
|
radar_payload = payload.get("radar")
|
||||||
|
if not isinstance(radar_payload, dict):
|
||||||
|
raise ValueError("Missing radar section")
|
||||||
|
|
||||||
|
sweep_payload = radar_payload.get("sweep")
|
||||||
|
if not isinstance(sweep_payload, dict):
|
||||||
|
raise ValueError("Missing radar.sweep section")
|
||||||
|
if not any(key in sweep_payload for key in _RADAR_SWEEP_KEYS):
|
||||||
|
raise ValueError("radar.sweep does not contain any supported sweep keys")
|
||||||
|
|
||||||
|
config = base_config.clone()
|
||||||
|
sweep = config.radar.sweep
|
||||||
|
if "start_hz" in sweep_payload:
|
||||||
|
sweep.start_hz = float(sweep_payload["start_hz"])
|
||||||
|
if "stop_hz" in sweep_payload:
|
||||||
|
sweep.stop_hz = float(sweep_payload["stop_hz"])
|
||||||
|
if "points" in sweep_payload:
|
||||||
|
sweep.points = int(sweep_payload["points"])
|
||||||
|
if "if_bandwidth_hz" in sweep_payload:
|
||||||
|
sweep.if_bandwidth_hz = float(sweep_payload["if_bandwidth_hz"])
|
||||||
|
if "stimulus_power_dbm" in sweep_payload:
|
||||||
|
sweep.power_dbm = float(sweep_payload["stimulus_power_dbm"])
|
||||||
|
|
||||||
|
radar_key = radar_key_from_config(
|
||||||
|
model_name=config.radar.model,
|
||||||
|
serial=config.radar.serial,
|
||||||
|
sweep_start_hz=config.radar.sweep.start_hz,
|
||||||
|
sweep_stop_hz=config.radar.sweep.stop_hz,
|
||||||
|
sweep_points=config.radar.sweep.points,
|
||||||
|
ifbw_hz=config.radar.sweep.if_bandwidth_hz,
|
||||||
|
power_dbm=config.radar.sweep.power_dbm,
|
||||||
|
)
|
||||||
|
return RadarConfigVariant(
|
||||||
|
source_path=path,
|
||||||
|
display_name=path.stem,
|
||||||
|
config=config,
|
||||||
|
radar_key=radar_key,
|
||||||
|
)
|
||||||
@@ -26,6 +26,7 @@ class SequentialCaptureState:
|
|||||||
current_combo: ComboModel | None
|
current_combo: ComboModel | None
|
||||||
can_undo: bool
|
can_undo: bool
|
||||||
is_complete: bool
|
is_complete: bool
|
||||||
|
variant_count: int = 1
|
||||||
|
|
||||||
|
|
||||||
class SequentialCaptureSession:
|
class SequentialCaptureSession:
|
||||||
|
|||||||
Reference in New Issue
Block a user