Add object calibration

This commit is contained in:
Ayzen
2026-10-05 17:39:40 +03:00
parent 8f097dfa2e
commit 552cdc688a
33 changed files with 3141 additions and 517 deletions
@@ -17,6 +17,7 @@ from PyQt6.QtWidgets import (
from python_app.gui.runtime.constraints import validate_processing_mode_constraints
from python_app.gui.antenna_geometry_editor import AntennaGeometryEditor
from python_app.models.run_config_model import GprRxGeometryModel, GprTxGeometryModel
from python_app.orchestration.live_processing_config import ProcessingLiveConfig
_GPR_MODES = ("gpr", "legacy_gpr")
@@ -320,6 +321,10 @@ class AppWindowLiveProcessingMixin:
reprocess_current_result=not self._can_reprocess_history(),
)
)
active = getattr(self, "_active_run_config", None)
if active is not None and self._supervisor.is_running():
active.gpr.tx_geometry = [GprTxGeometryModel(**entry) for entry in self._gpr_tx_geometry_input.records()]
active.gpr.rx_geometry = [GprRxGeometryModel(**entry) for entry in self._gpr_rx_geometry_input.records()]
if getattr(self, "_await_processing_revision", False):
self._required_processing_revision = self._processing_settings_revision
@@ -379,6 +384,8 @@ class AppWindowLiveProcessingMixin:
finally:
self._suppress_live_settings_handler = False
self._sync_socket_speed_controls()
if {"tx_geometry", "rx_geometry"} & settings.keys():
self._invalidate_object_calibration_after_geometry_change()
if {"tx_geometry", "rx_geometry", "relative_permittivity"} & settings.keys():
self._await_processing_revision = True
if "processor_mode" in settings:
@@ -395,9 +402,49 @@ class AppWindowLiveProcessingMixin:
def _on_gpr_geometry_changed(self) -> None:
if getattr(self, "_suppress_live_settings_handler", False):
return
self._invalidate_object_calibration_after_geometry_change()
self._await_processing_revision = True
self._on_processing_live_settings_changed()
def _invalidate_object_calibration_after_geometry_change(self) -> None:
"""Disable a correction tied to another antenna layout and reload the preprocessor."""
selected = self._selected_preprocess_sets.get("s21_object_calibration", "")
was_running = self._supervisor.is_running()
active_config = getattr(self, "_active_run_config", None)
active = active_config.preprocess.s21.object_calibration.set_name if was_running and active_config else ""
if not selected and not active:
return
from python_app.workflows.mimo_calibration import validate_for_config
geometry = {"gpr": {
"tx_geometry": self._gpr_tx_geometry_input.records(),
"rx_geometry": self._gpr_rx_geometry_input.records(),
}}
selections = [(selected, self._selected_preprocess_radar_key)]
if active:
selections.append((active, self._radar_key(active_config)))
invalid = set()
for name, radar_key in dict.fromkeys(selections):
if not name:
continue
try:
validate_for_config(self._store.load_object_calibration(radar_key, name), geometry)
except Exception as exc: # noqa: BLE001 - reject unavailable or incompatible saved corrections
invalid.add(name)
self._log_warning(f"Object calibration {name} disabled for changed antenna geometry: {exc}")
if not invalid:
return
resume_seq = getattr(self, "_stop_request_seq", 0) + 1
if active in invalid:
self._stop_run()
if selected in invalid:
self._selected_preprocess_sets["s21_object_calibration"] = ""
self._refresh_preprocess_summary_labels()
if self._preprocess_dialog is not None:
self._preprocess_dialog.set_selected_sets(self._selected_preprocess_sets, emit_signal=False)
if active in invalid and self._pipeline_resume_allowed(resume_seq):
self._start_run()
def _on_processing_live_settings_changed(self, *_args) -> None:
"""Handle live-processing setting changes and trigger redraw when needed."""
# Web-apply sets many widgets at once; their individual change signals are
@@ -4,6 +4,7 @@ from __future__ import annotations
from collections import deque
from contextlib import ExitStack
from copy import deepcopy
import json
from pathlib import Path
@@ -23,12 +24,23 @@ from python_app.orchestration.preprocess_assets import (
class AppWindowConfigProfileIOMixin:
"""Persist and restore full GUI profiles without restarting the app."""
def _write_gui_profile_to_path(self, output_path: Path, *, allow_overwrite: bool = True) -> GuiProfileModel:
"""Serialize current full GUI profile to `output_path` and return the persisted model."""
def _write_gui_profile_to_path(
self, output_path: Path, *, allow_overwrite: bool = True, dataset: bool = False,
) -> GuiProfileModel:
"""Save UI settings, preserving the acquisition provenance for recorded data."""
if not allow_overwrite and output_path.exists():
raise FileExistsError(f"Config profile output already exists: {output_path}")
profile = self._build_gui_profile()
active = getattr(self, "_active_run_config", None)
if dataset and active is not None:
profile.run_config.radar = deepcopy(active.radar)
profile.run_config.input_switch = deepcopy(active.input_switch)
profile.run_config.output_switch = deepcopy(active.output_switch)
profile.run_config.combos = deepcopy(active.combos)
profile.run_config.preprocess = deepcopy(active.preprocess)
profile.run_config.gpr.tx_geometry = deepcopy(active.gpr.tx_geometry)
profile.run_config.gpr.rx_geometry = deepcopy(active.gpr.rx_geometry)
atomic_write_json(output_path, profile.to_dict(), durable=True)
return profile
@@ -206,6 +218,9 @@ class AppWindowConfigProfileIOMixin:
capture sequence or the pipeline is active — the operator must stop first.
Shared by the desktop dialog and the web loader so both honor the same rule.
"""
if getattr(self, "_object_capture_job", None) is not None:
self._show_error("Wait for object-calibration capture to finish")
return False
if self._capture_session is not None:
self._show_error(
"Cannot load config during active capture sequence",