Add object calibration
This commit is contained in:
@@ -17,12 +17,14 @@ from PyQt6.QtCore import QTimer # noqa: E402
|
||||
|
||||
from python_app.gui.app_window import AppWindow # noqa: E402
|
||||
from python_app.gui.controllers.app_window_config.profile_io_mixin import AppWindowConfigProfileIOMixin # noqa: E402
|
||||
from python_app.gui.controllers.app_window_config.live_processing_mixin import AppWindowLiveProcessingMixin # noqa: E402
|
||||
from python_app.gui.controllers.app_window_pipeline_mixin import AppWindowPipelineMixin # noqa: E402
|
||||
from python_app.gui.controllers.app_window_preprocess_mixin import AppWindowPreprocessMixin # noqa: E402
|
||||
from python_app.gui.controllers.app_window_snapshot_mixin import AppWindowSnapshotMixin # noqa: E402
|
||||
from python_app.gui.controllers.app_window_recording_mixin import AppWindowRecordingMixin # noqa: E402
|
||||
from python_app.models.dataset_model import ResultCollection, SweepCollection # noqa: E402
|
||||
from python_app.models.gui_profile_model import GuiProfileModel # noqa: E402
|
||||
from python_app.models.run_config_model import RunConfigModel # noqa: E402
|
||||
from python_app.models.gui_profile_model import GuiProfileModel, GuiStateModel # noqa: E402
|
||||
from python_app.models.run_config_model import ComboModel, GprRxGeometryModel, GprTxGeometryModel, RunConfigModel # noqa: E402
|
||||
from python_app.storage.npz_store import NpzStore # noqa: E402
|
||||
from python_app.workflows.kamil_adc_neutral_preprocess import build_neutral_s21_sets # noqa: E402
|
||||
|
||||
@@ -264,6 +266,111 @@ class CaptureBatchControlTest(unittest.TestCase):
|
||||
|
||||
|
||||
class GuiProfileWriteTest(unittest.TestCase):
|
||||
def test_dataset_profile_uses_actual_preprocessing_and_preserves_current_gui(self) -> None:
|
||||
for applied, selected in (("", "next_run"), ("applied", "")):
|
||||
with self.subTest(applied=applied), tempfile.TemporaryDirectory() as folder:
|
||||
pending = GuiProfileModel(gui=GuiStateModel())
|
||||
pending.gui.processing.bscan.max_depth_m = 7.5
|
||||
pending.run_config.gpr.tx_geometry = [GprTxGeometryModel(output_pos=0, x_m=.4, y_m=.2, z_m=.1)]
|
||||
pending.run_config.preprocess.s21.object_calibration.set_name = selected
|
||||
active = pending.run_config.clone()
|
||||
active.gpr.tx_geometry[0].x_m = -.4
|
||||
active.radar.serial = "acquired_A"
|
||||
active.radar.sweep.points = 129
|
||||
active.input_switch.positions = 2
|
||||
active.output_switch.positions = 2
|
||||
active.combos = [ComboModel(input=1, output=1)]
|
||||
pending.run_config.radar.serial = "pending_B"
|
||||
pending.run_config.radar.sweep.points = 257
|
||||
active.preprocess.s21.object_calibration.set_name = applied
|
||||
active.preprocess.s21.object_calibration.bundle_path = "/bundles/object" if applied else ""
|
||||
active.preprocess.s21.calibration.set_name = "actual_through"
|
||||
active.preprocess.s21.calibration.bundle_path = "/bundles/through"
|
||||
active.preprocess.s21.reference.set_name = "actual_reference"
|
||||
active.preprocess.s21.reference.bundle_path = "/bundles/reference"
|
||||
active.preprocess.s11.calibration.open.set_name = "actual_open"
|
||||
active.preprocess.s11.calibration.open.bundle_path = "/bundles/open"
|
||||
active.preprocess.s11.reference.set_name = "actual_s11_reference"
|
||||
active.preprocess.notch.enabled = True
|
||||
active.preprocess.notch.bands_hz = [(2e9, 2.1e9)]
|
||||
writer = AppWindowConfigProfileIOMixin()
|
||||
writer._build_gui_profile = pending.clone
|
||||
writer._active_run_config = active
|
||||
dataset_path, settings_path = Path(folder) / "dataset.json", Path(folder) / "settings.json"
|
||||
saved = writer._write_gui_profile_to_path(dataset_path, dataset=True)
|
||||
writer._write_gui_profile_to_path(settings_path)
|
||||
dataset = GuiProfileModel.load_from_path(dataset_path).to_dict()
|
||||
expected = pending.to_dict()
|
||||
acquired = active.to_dict()
|
||||
for key in ("radar", "switches", "run", "preprocess"):
|
||||
expected[key] = acquired[key]
|
||||
expected["gpr"]["tx_geometry"] = acquired["gpr"]["tx_geometry"]
|
||||
expected["gpr"]["rx_geometry"] = acquired["gpr"]["rx_geometry"]
|
||||
self.assertEqual(dataset, expected)
|
||||
self.assertEqual(GuiProfileModel.load_from_path(settings_path).to_dict(), pending.to_dict())
|
||||
saved.run_config.preprocess.s21.calibration.set_name = "mutated_return_value"
|
||||
self.assertEqual(active.preprocess.s21.calibration.set_name, "actual_through")
|
||||
|
||||
def test_dataset_profile_retains_original_device_and_geometry_after_loading_another_profile(self) -> None:
|
||||
acquired = GuiProfileModel()
|
||||
acquired.run_config.ensure_combos()
|
||||
acquired.run_config.radar.serial = "device_A"
|
||||
acquired.run_config.gpr.tx_geometry = [GprTxGeometryModel(output_pos=0, x_m=-.4)]
|
||||
acquired.run_config.gpr.rx_geometry = [GprRxGeometryModel(input_pos=0, x_m=.4)]
|
||||
pending = acquired.clone()
|
||||
pending.run_config.radar.model = "librevna_multi"
|
||||
pending.run_config.radar.serial = "device_B"
|
||||
pending.run_config.gpr.tx_geometry[0].x_m = 2.
|
||||
pending.run_config.gpr.rx_geometry[0].x_m = -2.
|
||||
writer = AppWindowConfigProfileIOMixin()
|
||||
writer._build_gui_profile = pending.clone
|
||||
writer._active_run_config = acquired.run_config
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / "dataset.json"
|
||||
writer._write_gui_profile_to_path(path, dataset=True)
|
||||
saved = GuiProfileModel.load_from_path(path)
|
||||
self.assertEqual(saved.run_config.radar, acquired.run_config.radar)
|
||||
self.assertEqual(saved.run_config.gpr, acquired.run_config.gpr)
|
||||
self.assertEqual(saved.run_config.combos, acquired.run_config.combos)
|
||||
|
||||
def test_applied_geometry_tracks_successful_live_writes_only_while_acquiring(self) -> None:
|
||||
for running, failed_write in ((True, False), (False, False), (True, True)):
|
||||
with self.subTest(running=running, failed_write=failed_write):
|
||||
active = RunConfigModel()
|
||||
active.gpr.tx_geometry = [GprTxGeometryModel(output_pos=0, x_m=-.4)]
|
||||
active.gpr.rx_geometry = [GprRxGeometryModel(input_pos=0, x_m=.4)]
|
||||
original = active.clone()
|
||||
writer = SimpleNamespace(
|
||||
_active_run_config=active,
|
||||
_supervisor=SimpleNamespace(is_running=lambda: running),
|
||||
_live_config_writer=SimpleNamespace(write=mock.Mock(side_effect=OSError("disk full") if failed_write else None)),
|
||||
_live_processing_config=mock.Mock(return_value=object()),
|
||||
_can_reprocess_history=lambda: False,
|
||||
_gpr_tx_geometry_input=SimpleNamespace(records=lambda: [dict(output_pos=0, x_m=1., y_m=2., z_m=3.)]),
|
||||
_gpr_rx_geometry_input=SimpleNamespace(records=lambda: [dict(input_pos=0, x_m=-1., y_m=-2., z_m=-3.)]),
|
||||
)
|
||||
if failed_write:
|
||||
with self.assertRaises(OSError):
|
||||
AppWindowLiveProcessingMixin._write_live_processing_config(writer)
|
||||
else:
|
||||
AppWindowLiveProcessingMixin._write_live_processing_config(writer)
|
||||
if running and not failed_write:
|
||||
self.assertEqual(active.gpr.tx_geometry, [GprTxGeometryModel(0, 1., 2., 3.)])
|
||||
self.assertEqual(active.gpr.rx_geometry, [GprRxGeometryModel(0, -1., -2., -3.)])
|
||||
else:
|
||||
self.assertEqual(active.gpr, original.gpr)
|
||||
|
||||
def test_dataset_profile_without_active_run_preserves_ui_selection(self) -> None:
|
||||
writer = AppWindowConfigProfileIOMixin()
|
||||
pending = GuiProfileModel()
|
||||
pending.run_config.preprocess.s21.object_calibration.set_name = "selected"
|
||||
writer._build_gui_profile = pending.clone
|
||||
writer._active_run_config = None
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / "dataset.json"
|
||||
writer._write_gui_profile_to_path(path, dataset=True)
|
||||
self.assertEqual(GuiProfileModel.load_from_path(path).to_dict(), pending.to_dict())
|
||||
|
||||
def test_failed_profile_replace_preserves_existing_profile_and_cleans_temporary(self) -> None:
|
||||
writer = AppWindowConfigProfileIOMixin()
|
||||
writer._build_gui_profile = lambda: GuiProfileModel()
|
||||
@@ -293,5 +400,95 @@ class HistoryCommandRestoreTest(unittest.TestCase):
|
||||
self.assertEqual(AppWindow._load_history_command_seq(path), 9876543210)
|
||||
|
||||
|
||||
class PreprocessHistoryRestartTest(unittest.TestCase):
|
||||
def _runtime(self):
|
||||
w = _Runtime()
|
||||
config = RunConfigModel.from_dict({"radar": {"model": "librevna", "driver_mode": "mock"}})
|
||||
config.input_switch.driver = config.output_switch.driver = "h7992"
|
||||
config.input_switch.radar_port, config.output_switch.radar_port = 1, 2
|
||||
config.preprocess.s21.calibration.set_name = "through"
|
||||
config.preprocess.s21.reference.set_name = "empty"
|
||||
w._build_config.return_value = config
|
||||
w._processor_run_signature = w._build_processor_run_signature(config)
|
||||
w._history_run_signature = w._build_run_history_signature(config)
|
||||
w._project_root = Path("/tmp/object-calibration-test")
|
||||
w._store = mock.Mock()
|
||||
w._store.has_combo_coverage.return_value = True
|
||||
w._config_writer = mock.Mock()
|
||||
w._config_writer.write.return_value = w._project_root / "runtime.json"
|
||||
w._radar_key = mock.Mock(return_value="radar")
|
||||
w._supervisor.pids.return_value = {}
|
||||
w._bscan_reprocess_timer = mock.Mock()
|
||||
for name in ("_validate_processing_mode_constraints", "_prepare_radar_for_native_acquisition",
|
||||
"_close_replay_ring_writer", "_discard_bscan_replay_results", "_close_readers",
|
||||
"_drop_pending_ring_payloads", "_reset_runtime_history", "_stop_all_processes",
|
||||
"_write_live_processing_config"):
|
||||
setattr(w, name, mock.Mock())
|
||||
return w, config
|
||||
|
||||
def test_changed_preprocess_clears_native_history_before_both_start_modes(self) -> None:
|
||||
for single_capture in (False, True):
|
||||
with self.subTest(single_capture=single_capture):
|
||||
w, config = self._runtime()
|
||||
config.preprocess.s21.object_calibration.set_name = "sphere"
|
||||
events = []
|
||||
w._write_live_processing_config.side_effect = lambda **_kwargs: events.append("clear")
|
||||
w._supervisor.start.side_effect = lambda *_args, **_kwargs: events.append("start")
|
||||
with mock.patch("python_app.gui.controllers.app_window_pipeline_mixin.ShmRingReader"):
|
||||
w._start_run(single_capture=single_capture)
|
||||
w._show_exception.assert_not_called()
|
||||
w._write_live_processing_config.assert_called_once_with(
|
||||
history_command="clear_all", bump_history_seq=True)
|
||||
self.assertEqual(events, ["clear", "start"])
|
||||
w._reset_runtime_history.assert_called_once()
|
||||
self.assertEqual(w._history_run_signature, w._build_run_history_signature(config))
|
||||
|
||||
def test_unchanged_preprocess_retains_history_and_existing_processor(self) -> None:
|
||||
w, config = self._runtime()
|
||||
with mock.patch("python_app.gui.controllers.app_window_pipeline_mixin.ShmRingReader"):
|
||||
w._start_run(single_capture=True)
|
||||
w._show_exception.assert_not_called()
|
||||
w._write_live_processing_config.assert_not_called()
|
||||
w._reset_runtime_history.assert_not_called()
|
||||
w._stop_all_processes.assert_not_called()
|
||||
|
||||
def test_capture_override_is_cloned_and_skips_display_mode_constraints(self) -> None:
|
||||
w, config = self._runtime()
|
||||
w._object_capture_job = object()
|
||||
with mock.patch("python_app.gui.controllers.app_window_pipeline_mixin.ShmRingReader"):
|
||||
w._start_run(config_override=config)
|
||||
w._show_exception.assert_not_called()
|
||||
w._build_config.assert_not_called()
|
||||
w._validate_processing_mode_constraints.assert_not_called()
|
||||
exported = w._config_writer.prepare_preprocess_bundles.call_args.args[2]
|
||||
self.assertIsNot(exported, config)
|
||||
self.assertFalse(config.runtime.continuous)
|
||||
self.assertTrue(exported.runtime.continuous)
|
||||
|
||||
def test_regular_start_and_other_hardware_actions_wait_for_object_capture(self) -> None:
|
||||
for action, args in (("_start_run", ()), ("_capture_tmp_reference", ()),
|
||||
("_create_neutral_sets", ()), ("_start_capture_sequence", ("s21_reference",)),
|
||||
("_apply_radar_settings", ()), ("_save_snapshot", ()),
|
||||
("_save_vna_history_json", ()), ("_remove_last_runtime_history", ()),
|
||||
("_clear_all_runtime_history", ())):
|
||||
with self.subTest(action=action):
|
||||
w, _ = self._runtime()
|
||||
w._object_capture_job = object()
|
||||
getattr(w, action)(*args)
|
||||
w._build_config.assert_not_called()
|
||||
w._show_error.assert_called_once()
|
||||
|
||||
def test_profile_load_and_recording_wait_for_object_capture_even_before_pipeline_start(self) -> None:
|
||||
for action in (AppWindowConfigProfileIOMixin._ensure_ready_to_load_config,
|
||||
AppWindowRecordingMixin._start_run_with_recording):
|
||||
with self.subTest(action=action.__name__):
|
||||
w, _ = self._runtime()
|
||||
w._object_capture_job = object()
|
||||
self.assertFalse(action(w))
|
||||
w._build_config.assert_not_called()
|
||||
w._supervisor.is_running.assert_not_called()
|
||||
w._show_error.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user