From 077542cbd01ab0e0e56de8ccbc26b87955bebb52 Mon Sep 17 00:00:00 2001 From: Ayzen Date: Thu, 26 Mar 2026 18:29:42 +0300 Subject: [PATCH] added s11! --- .../common_cpp/config/include/run_config.hpp | 33 +++- .../common_cpp/config/src/run_config.cpp | 40 +++-- .../data_preprocessor/src/main.cpp | 12 +- .../include/processing_live_config.hpp | 1 + .../src/processing_live_config.cpp | 13 +- .../processors/src/bscan_processor.cpp | 49 ++++-- python_app/gui/app_window.py | 7 +- .../controllers/app_window_config_mixin.py | 10 +- .../controllers/app_window_pipeline_mixin.py | 35 ++-- .../gui/controllers/app_window_plot_mixin.py | 12 +- .../app_window_preprocess_mixin.py | 80 ++++----- .../sections/preprocess_summary_section.py | 14 +- .../sections/processing_section.py | 5 + python_app/gui/plotting/bscan_history.py | 11 +- python_app/gui/preprocess_dialog.py | 156 +++++++++--------- python_app/gui/runtime/history.py | 5 +- python_app/hardware_full/librevna_backends.py | 38 +++-- python_app/hardware_full/librevna_service.py | 9 +- python_app/models/dataset_model.py | 3 +- python_app/models/run_config_codec.py | 103 +++++++----- python_app/models/run_config_model.py | 8 + python_app/models/run_config_schema.py | 43 ++++- python_app/orchestration/config_writer.py | 22 +-- .../orchestration/live_processing_config.py | 2 + python_app/orchestration/preprocess_assets.py | 95 +++++++++++ python_app/orchestration/shm/decoder.py | 10 +- python_app/runtime/run_config_smoke.json | 92 ++++++++--- .../runtime/s11_load_calibration_bundle.bin | Bin 0 -> 64536 bytes .../runtime/s11_open_calibration_bundle.bin | Bin 0 -> 64536 bytes python_app/runtime/s11_reference_bundle.bin | Bin 0 -> 64536 bytes .../runtime/s11_short_calibration_bundle.bin | Bin 0 -> 64536 bytes python_app/runtime/s21_calibration_bundle.bin | Bin 0 -> 64536 bytes python_app/runtime/s21_reference_bundle.bin | Bin 0 -> 64536 bytes python_app/scripts/check_snapshot_numpy.py | 12 +- python_app/scripts/manual_smoke_run.py | 44 +++-- python_app/storage/npz/serialize.py | 7 +- python_app/storage/npz/snapshot_numpy.py | 3 + python_app/storage/npz/store.py | 11 +- python_app/storage/store_api.py | 2 +- python_app/workflows/calibration_workflow.py | 9 +- python_app/workflows/reference_workflow.py | 9 +- .../workflows/sequential_capture_workflow.py | 17 +- run_config.json | 38 ++++- 43 files changed, 713 insertions(+), 347 deletions(-) create mode 100644 python_app/orchestration/preprocess_assets.py create mode 100644 python_app/runtime/s11_load_calibration_bundle.bin create mode 100644 python_app/runtime/s11_open_calibration_bundle.bin create mode 100644 python_app/runtime/s11_reference_bundle.bin create mode 100644 python_app/runtime/s11_short_calibration_bundle.bin create mode 100644 python_app/runtime/s21_calibration_bundle.bin create mode 100644 python_app/runtime/s21_reference_bundle.bin diff --git a/data_acq_and_processing/common_cpp/config/include/run_config.hpp b/data_acq_and_processing/common_cpp/config/include/run_config.hpp index b5a4af7..5342590 100644 --- a/data_acq_and_processing/common_cpp/config/include/run_config.hpp +++ b/data_acq_and_processing/common_cpp/config/include/run_config.hpp @@ -73,16 +73,31 @@ struct RuntimeConfig { std::string processing_live_config_path = "python_app/runtime/processing_live.json"; }; +struct PreprocessAssetConfig { + std::string set_name{}; + std::string bundle_path{}; +}; + +struct S21PreprocessConfig { + PreprocessAssetConfig calibration{}; + PreprocessAssetConfig reference{}; +}; + +struct S11CalibrationPreprocessConfig { + PreprocessAssetConfig open{}; + PreprocessAssetConfig short_standard{}; + PreprocessAssetConfig load{}; +}; + +struct S11PreprocessConfig { + S11CalibrationPreprocessConfig calibration{}; + PreprocessAssetConfig reference{}; +}; + struct PreprocessConfig { - // Names and bundle paths selected by Python GUI layer. - std::string s21_calibration_set{}; - std::string s21_reference_set{}; - std::string s21_calibration_bundle_path{}; - std::string s21_reference_bundle_path{}; - std::string s11_open_calibration_bundle_path{}; - std::string s11_short_calibration_bundle_path{}; - std::string s11_load_calibration_bundle_path{}; - std::string s11_reference_bundle_path{}; + // Channel-specific preprocessing assets selected by Python GUI layer. + S21PreprocessConfig s21{}; + S11PreprocessConfig s11{}; }; struct GprTxGeometry { diff --git a/data_acq_and_processing/common_cpp/config/src/run_config.cpp b/data_acq_and_processing/common_cpp/config/src/run_config.cpp index 7d389fe..7d9217f 100644 --- a/data_acq_and_processing/common_cpp/config/src/run_config.cpp +++ b/data_acq_and_processing/common_cpp/config/src/run_config.cpp @@ -140,6 +140,20 @@ using Json = nlohmann::json; return fallback; } +[[nodiscard]] auto parse_preprocess_asset( + const Json& object, + const std::string& key, + const std::string& context +) -> PreprocessAssetConfig { + const auto asset_context = context + "." + key; + const auto* asset_obj = as_object(required_field(object, key), asset_context); + + PreprocessAssetConfig asset{}; + asset.set_name = optional_string(*asset_obj, "set_name", ""); + asset.bundle_path = optional_string(*asset_obj, "bundle_path", ""); + return asset; +} + [[nodiscard]] auto parse_driver_mode(const std::string& value) -> DriverMode { if (value == "mock") { return DriverMode::Mock; @@ -438,18 +452,20 @@ auto load_run_config(const std::string& path) -> RunConfig { { const auto* preprocess_obj = as_object(required_field(*root_obj, "preprocess"), "preprocess"); - config.preprocess.s21_calibration_set = optional_string(*preprocess_obj, "s21_calibration_set", ""); - config.preprocess.s21_reference_set = optional_string(*preprocess_obj, "s21_reference_set", ""); - config.preprocess.s21_calibration_bundle_path = - optional_string(*preprocess_obj, "s21_calibration_bundle_path", ""); - config.preprocess.s21_reference_bundle_path = optional_string(*preprocess_obj, "s21_reference_bundle_path", ""); - config.preprocess.s11_open_calibration_bundle_path = - optional_string(*preprocess_obj, "s11_open_calibration_bundle_path", ""); - config.preprocess.s11_short_calibration_bundle_path = - optional_string(*preprocess_obj, "s11_short_calibration_bundle_path", ""); - config.preprocess.s11_load_calibration_bundle_path = - optional_string(*preprocess_obj, "s11_load_calibration_bundle_path", ""); - config.preprocess.s11_reference_bundle_path = optional_string(*preprocess_obj, "s11_reference_bundle_path", ""); + const auto* s21_obj = as_object(required_field(*preprocess_obj, "s21"), "preprocess.s21"); + config.preprocess.s21.calibration = parse_preprocess_asset(*s21_obj, "calibration", "preprocess.s21"); + config.preprocess.s21.reference = parse_preprocess_asset(*s21_obj, "reference", "preprocess.s21"); + + const auto* s11_obj = as_object(required_field(*preprocess_obj, "s11"), "preprocess.s11"); + const auto* s11_calibration_obj = + as_object(required_field(*s11_obj, "calibration"), "preprocess.s11.calibration"); + config.preprocess.s11.calibration.open = + parse_preprocess_asset(*s11_calibration_obj, "open", "preprocess.s11.calibration"); + config.preprocess.s11.calibration.short_standard = + parse_preprocess_asset(*s11_calibration_obj, "short", "preprocess.s11.calibration"); + config.preprocess.s11.calibration.load = + parse_preprocess_asset(*s11_calibration_obj, "load", "preprocess.s11.calibration"); + config.preprocess.s11.reference = parse_preprocess_asset(*s11_obj, "reference", "preprocess.s11"); } if (const auto* gpr_value = optional_field(*root_obj, "gpr"); gpr_value != nullptr) { diff --git a/data_acq_and_processing/preprocessing/data_preprocessor/src/main.cpp b/data_acq_and_processing/preprocessing/data_preprocessor/src/main.cpp index db4745a..9cc8143 100644 --- a/data_acq_and_processing/preprocessing/data_preprocessor/src/main.cpp +++ b/data_acq_and_processing/preprocessing/data_preprocessor/src/main.cpp @@ -65,16 +65,16 @@ int main(int argc, char** argv) { radar::preprocessing::CalibrationMaster calibration_master( radar::preprocessing::make_s21_through_calibrator() ); - calibration_master.load_s21_calibration_bundle(config.preprocess.s21_calibration_bundle_path); + calibration_master.load_s21_calibration_bundle(config.preprocess.s21.calibration.bundle_path); calibration_master.load_s11_calibration_bundle( - config.preprocess.s11_open_calibration_bundle_path, - config.preprocess.s11_short_calibration_bundle_path, - config.preprocess.s11_load_calibration_bundle_path + config.preprocess.s11.calibration.open.bundle_path, + config.preprocess.s11.calibration.short_standard.bundle_path, + config.preprocess.s11.calibration.load.bundle_path ); radar::preprocessing::ReferenceMaster reference_master; - reference_master.load_s21_reference_bundle(config.preprocess.s21_reference_bundle_path); - reference_master.load_s11_reference_bundle(config.preprocess.s11_reference_bundle_path); + reference_master.load_s21_reference_bundle(config.preprocess.s21.reference.bundle_path); + reference_master.load_s11_reference_bundle(config.preprocess.s11.reference.bundle_path); reference_master.prepare_calibrated(calibration_master, config.run_combos); radar::preprocessing::DataPreprocessor preprocessor( diff --git a/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp b/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp index 935fc26..5024564 100644 --- a/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp +++ b/data_acq_and_processing/processing/data_processor/include/processing_live_config.hpp @@ -23,6 +23,7 @@ struct ProcessingLiveConfig { float pass_through_y_min_db = -100.0F; float pass_through_y_max_db = 0.0F; std::string bscan_axis = "abs"; + std::string bscan_channel = "s21"; float bscan_cut_m = 0.824F; float bscan_max_depth_m = 1.0F; float bscan_gain = 1.0F; diff --git a/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp b/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp index 3d1989b..589fd43 100644 --- a/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp +++ b/data_acq_and_processing/processing/data_processor/src/processing_live_config.cpp @@ -30,11 +30,11 @@ using Json = nlohmann::json; throw std::runtime_error("processing.history_command must be one of: none, remove_last, clear_all"); } -[[nodiscard]] auto parse_pass_through_channel(const std::string& value) -> std::string { +[[nodiscard]] auto parse_s_parameter_channel(const std::string& value, const std::string& field_name) -> std::string { if (value == "s21" || value == "s11") { return value; } - throw std::runtime_error("processing.pass_through_channel must be one of: s21, s11"); + throw std::runtime_error(field_name + " must be one of: s21, s11"); } [[nodiscard]] auto parse_u64_number(const Json& value, const std::string& field_name) -> std::uint64_t { @@ -112,7 +112,8 @@ using Json = nlohmann::json; if (!found->is_string()) { throw std::runtime_error("processing.pass_through_channel must be string"); } - config.pass_through_channel = parse_pass_through_channel(found->get()); + config.pass_through_channel = + parse_s_parameter_channel(found->get(), "processing.pass_through_channel"); } if (const auto found = root.find("pass_through_fixed_y_enabled"); found != root.end()) { if (!found->is_boolean()) { @@ -138,6 +139,12 @@ using Json = nlohmann::json; } config.bscan_axis = found->get(); } + if (const auto found = root.find("bscan_channel"); found != root.end()) { + if (!found->is_string()) { + throw std::runtime_error("processing.bscan_channel must be string"); + } + config.bscan_channel = parse_s_parameter_channel(found->get(), "processing.bscan_channel"); + } if (const auto found = root.find("bscan_cut_m"); found != root.end()) { if (!found->is_number()) { throw std::runtime_error("processing.bscan_cut_m must be number"); diff --git a/data_acq_and_processing/processing/processors/src/bscan_processor.cpp b/data_acq_and_processing/processing/processors/src/bscan_processor.cpp index b78f932..e797977 100644 --- a/data_acq_and_processing/processing/processors/src/bscan_processor.cpp +++ b/data_acq_and_processing/processing/processors/src/bscan_processor.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -92,15 +93,28 @@ void fft_inplace(std::vector>& values, bool inverse) { return std::abs(sample); } -[[nodiscard]] auto fallback_profile(const ipc::SweepTraceBlock& trace) -> BScanProfile { - const std::size_t point_count = std::min(trace.frequency_hz.size(), trace.s21.size()); +[[nodiscard]] auto selected_trace_samples( + const ipc::SweepTraceBlock& trace, + const ProcessingLiveConfig& live_config +) -> std::span { + if (live_config.bscan_channel == "s11") { + return trace.s11; + } + return trace.s21; +} + +[[nodiscard]] auto fallback_profile( + const ipc::SweepTraceBlock& trace, + std::span selected_samples +) -> BScanProfile { + const std::size_t point_count = std::min(trace.frequency_hz.size(), selected_samples.size()); BScanProfile fallback{}; fallback.depth_m.reserve(point_count); fallback.response.reserve(point_count); const float denominator = point_count > 1U ? static_cast(point_count - 1U) : 1.0F; for (std::size_t index = 0U; index < point_count; ++index) { - const auto& sample = trace.s21[index]; + const auto& sample = selected_samples[index]; fallback.depth_m.push_back(static_cast(static_cast(index) / denominator)); fallback.response.push_back(std::sqrt(sample.re * sample.re + sample.im * sample.im)); } @@ -111,9 +125,10 @@ void fft_inplace(std::vector>& values, bool inverse) { const ipc::SweepTraceBlock& trace, const ProcessingLiveConfig& live_config ) -> BScanProfile { - const std::size_t point_count = std::min(trace.frequency_hz.size(), trace.s21.size()); + const auto selected_samples = selected_trace_samples(trace, live_config); + const std::size_t point_count = std::min(trace.frequency_hz.size(), selected_samples.size()); if (point_count < 2U) { - return fallback_profile(trace); + return fallback_profile(trace, selected_samples); } const double configured_start_hz = static_cast(live_config.bscan_start_freq_mhz) * 1'000'000.0; @@ -122,55 +137,55 @@ void fft_inplace(std::vector>& values, bool inverse) { const double stop_hz = std::max(configured_start_hz, configured_stop_hz); std::vector filtered_freq_hz{}; - std::vector> filtered_s21{}; + std::vector> filtered_samples{}; filtered_freq_hz.reserve(point_count); - filtered_s21.reserve(point_count); + filtered_samples.reserve(point_count); for (std::size_t index = 0U; index < point_count; ++index) { const double frequency_hz = static_cast(trace.frequency_hz[index]); if (frequency_hz < start_hz || frequency_hz > stop_hz) { continue; } - const auto& sample = trace.s21[index]; + const auto& sample = selected_samples[index]; filtered_freq_hz.push_back(frequency_hz); - filtered_s21.emplace_back(static_cast(sample.re), static_cast(sample.im)); + filtered_samples.emplace_back(static_cast(sample.re), static_cast(sample.im)); } if (filtered_freq_hz.size() < 2U) { - return fallback_profile(trace); + return fallback_profile(trace, selected_samples); } const std::size_t filtered_count = filtered_freq_hz.size(); const double df = (filtered_freq_hz.back() - filtered_freq_hz.front()) / static_cast(filtered_count - 1U); if (df <= 0.0) { - return fallback_profile(trace); + return fallback_profile(trace, selected_samples); } const auto start_bin = static_cast(std::llround(filtered_freq_hz.front() / df)); if (start_bin < 0) { - return fallback_profile(trace); + return fallback_profile(trace, selected_samples); } const auto start_index = static_cast(start_bin); if (start_index > (std::numeric_limits::max() / 2U)) { - return fallback_profile(trace); + return fallback_profile(trace, selected_samples); } if (start_index > (std::numeric_limits::max() - filtered_count + 1U)) { - return fallback_profile(trace); + return fallback_profile(trace, selected_samples); } const std::size_t min_fft_len = 2U * (start_index + filtered_count - 1U); const std::size_t fft_len = next_power_of_two(min_fft_len); if (fft_len < min_fft_len || (fft_len & (fft_len - 1U)) != 0U) { - return fallback_profile(trace); + return fallback_profile(trace, selected_samples); } if (start_index > fft_len || filtered_count > (fft_len - start_index)) { - return fallback_profile(trace); + return fallback_profile(trace, selected_samples); } std::vector> spectrum(fft_len, std::complex(0.0, 0.0)); for (std::size_t index = 0U; index < filtered_count; ++index) { - spectrum[start_index + index] = filtered_s21[index]; + spectrum[start_index + index] = filtered_samples[index]; } fft_inplace(spectrum, true); diff --git a/python_app/gui/app_window.py b/python_app/gui/app_window.py index af6b1b9..23640d7 100644 --- a/python_app/gui/app_window.py +++ b/python_app/gui/app_window.py @@ -24,6 +24,7 @@ from python_app.models.dataset_model import ResultCollection, SweepCollection from python_app.models.run_config_model import RunConfigModel from python_app.orchestration.config_writer import ConfigWriter from python_app.orchestration.live_processing_config import ProcessingLiveConfigWriter +from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, preprocess_asset_model from python_app.orchestration.process_supervisor import ProcessSupervisor from python_app.orchestration.shm_reader import ShmRingReader from python_app.storage.npz_store import NpzStore @@ -78,8 +79,10 @@ class AppWindow( def _init_preprocess_state(self) -> None: """Initialize preprocessing dialog and selected set names.""" self._preprocess_dialog: PreprocessDialog | None = None - self._selected_s21_calibration_set = str(self._defaults_config.preprocess.s21_calibration_set) - self._selected_s21_reference_set = str(self._defaults_config.preprocess.s21_reference_set) + self._selected_preprocess_sets = { + key: str(preprocess_asset_model(self._defaults_config, key).set_name) + for key in PREPROCESS_ASSET_KEYS + } def _init_capture_state(self) -> None: """Initialize one-shot capture and sequence-control flags.""" diff --git a/python_app/gui/controllers/app_window_config_mixin.py b/python_app/gui/controllers/app_window_config_mixin.py index 7389e17..989ff48 100644 --- a/python_app/gui/controllers/app_window_config_mixin.py +++ b/python_app/gui/controllers/app_window_config_mixin.py @@ -7,6 +7,7 @@ from python_app.models.run_config_model import ComboModel, GprRxGeometryModel, G from python_app.models.run_config_validation import validate_gpr_model from python_app.orchestration.config_writer import parse_combos_from_text from python_app.orchestration.live_processing_config import ProcessingLiveConfig +from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, preprocess_asset_model from python_app.storage.npz_store import radar_key_from_config @@ -118,8 +119,10 @@ class AppWindowConfigMixin: if self._switches_are_effectively_static(config): config.combos = [ComboModel(input=0, output=0)] - config.preprocess.s21_calibration_set = self._selected_s21_calibration_set - config.preprocess.s21_reference_set = self._selected_s21_reference_set + for key in PREPROCESS_ASSET_KEYS: + asset = preprocess_asset_model(config, key) + asset.set_name = self._selected_preprocess_sets[key] + asset.bundle_path = "" config.gpr.mode = self._gpr_config_mode.currentText() config.gpr.relative_permittivity = float(self._gpr_relative_permittivity.value()) config.gpr.tx_geometry = self._parse_gpr_tx_geometry_text(self._gpr_tx_geometry_input.toPlainText()) @@ -132,7 +135,7 @@ class AppWindowConfigMixin: return config def _radar_key(self, config: RunConfigModel) -> str: - """Build radar key used by calibration/reference storage lookup.""" + """Build radar key used by preprocess-set storage lookup.""" return radar_key_from_config( model_name=config.radar.model, serial=config.radar.serial, @@ -158,6 +161,7 @@ class AppWindowConfigMixin: pass_through_y_min_db=min(y_min_db, y_max_db), pass_through_y_max_db=max(y_min_db, y_max_db), bscan_axis=self._bscan_axis.currentText(), + bscan_channel=self._bscan_channel.currentText(), bscan_cut_m=float(self._bscan_cut_m.value()), bscan_max_depth_m=float(self._bscan_max_depth_m.value()), bscan_gain=float(self._bscan_gain.value()), diff --git a/python_app/gui/controllers/app_window_pipeline_mixin.py b/python_app/gui/controllers/app_window_pipeline_mixin.py index 9c3550d..5ba26a7 100644 --- a/python_app/gui/controllers/app_window_pipeline_mixin.py +++ b/python_app/gui/controllers/app_window_pipeline_mixin.py @@ -9,6 +9,7 @@ from python_app.gui.runtime.history import build_run_history_signature, record_r from python_app.hardware_full.librevna_service import LibreVnaService from python_app.models.dataset_model import ComboKey, ResultCollection, SweepCollection from python_app.models.run_config_model import RunConfigModel +from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, PREPROCESS_ASSET_SPECS, preprocess_asset_model from python_app.orchestration.shm_reader import ShmRingReader @@ -38,28 +39,26 @@ class AppWindowPipelineMixin: run_signature = self._build_run_history_signature(config) radar_key = self._radar_key(config) - if not config.preprocess.s21_calibration_set or not config.preprocess.s21_reference_set: - raise RuntimeError("Select calibration and reference sets in Preprocessing Panel before Start") + missing_assets = [ + PREPROCESS_ASSET_SPECS[key].display_name + for key in PREPROCESS_ASSET_KEYS + if not preprocess_asset_model(config, key).set_name + ] + if missing_assets: + raise RuntimeError( + "Select all required preprocess sets in Preprocessing Panel before Start: " + + ", ".join(missing_assets) + ) combo_keys = [ComboKey(input_pos=combo.input, output_pos=combo.output) for combo in config.combos] - if not self._store.has_combo_coverage( - "calibration", radar_key, config.preprocess.s21_calibration_set, combo_keys - ): - raise RuntimeError("Selected calibration set does not cover requested run combos") - if not self._store.has_combo_coverage( - "reference", radar_key, config.preprocess.s21_reference_set, combo_keys - ): - raise RuntimeError("Selected reference set does not cover requested run combos") + for key in PREPROCESS_ASSET_KEYS: + spec = PREPROCESS_ASSET_SPECS[key] + asset = preprocess_asset_model(config, key) + if not self._store.has_combo_coverage(spec.set_kind, radar_key, asset.set_name, combo_keys): + raise RuntimeError(f"Selected {spec.display_name} set does not cover requested run combos") - calibration_bundle, reference_bundle = self._config_writer.prepare_s21_bundles( - self._store, - radar_key, - config.preprocess.s21_calibration_set, - config.preprocess.s21_reference_set, - ) - config.preprocess.s21_calibration_bundle_path = str(calibration_bundle) - config.preprocess.s21_reference_bundle_path = str(reference_bundle) + self._config_writer.prepare_preprocess_bundles(self._store, radar_key, config) config.runtime.continuous = not single_capture if not single_capture: diff --git a/python_app/gui/controllers/app_window_plot_mixin.py b/python_app/gui/controllers/app_window_plot_mixin.py index 0aa0fe8..7d7a421 100644 --- a/python_app/gui/controllers/app_window_plot_mixin.py +++ b/python_app/gui/controllers/app_window_plot_mixin.py @@ -389,7 +389,10 @@ class AppWindowPlotMixin: self._bscan_plot.addItem(image_item) self._bscan_plot.setXRange(x_min, x_max, padding=0.02) self._bscan_plot.setYRange(depth_min, depth_max, padding=0.02) - self._bscan_plot.setTitle(f"B-scan in{display_key[0]}/out{display_key[1]} | sweeps={sweep_count}") + bscan_channel = self._bscan_channel.currentText().upper() + self._bscan_plot.setTitle( + f"B-scan {bscan_channel} in{display_key[0]}/out{display_key[1]} | sweeps={sweep_count}" + ) return True def _sync_bscan_history_from_results(self) -> None: @@ -793,12 +796,13 @@ class AppWindowPlotMixin: return True return False - def _draw_single_trace(self, trace: TraceData, title: str) -> None: + def _draw_single_trace(self, trace: TraceData, title: str, *, channel: str = "s21") -> None: """Draw one trace on stacked magnitude/phase plots.""" show_magnitude = self._show_magnitude_curves() show_phase = self._show_phase_curves() magnitude_plot = self._trace_magnitude_plot phase_plot = self._trace_phase_plot + samples = trace.s11 if channel == "s11" else trace.s21 magnitude_plot.setVisible(show_magnitude) phase_plot.setVisible(show_phase) @@ -822,7 +826,7 @@ class AppWindowPlotMixin: phase_plot.setTitle(title) if show_magnitude: - magnitude_db = 20.0 * np.log10(np.maximum(np.abs(trace.s21), 1e-12)) + magnitude_db = 20.0 * np.log10(np.maximum(np.abs(samples), 1e-12)) magnitude_curve = pg.PlotCurveItem( trace.frequency_hz, magnitude_db, @@ -834,7 +838,7 @@ class AppWindowPlotMixin: ] = magnitude_curve if show_phase: - phase_deg = np.degrees(np.angle(trace.s21)) + phase_deg = np.degrees(np.angle(samples)) phase_curve = pg.PlotCurveItem( trace.frequency_hz, phase_deg, diff --git a/python_app/gui/controllers/app_window_preprocess_mixin.py b/python_app/gui/controllers/app_window_preprocess_mixin.py index 946d074..d3ad35b 100644 --- a/python_app/gui/controllers/app_window_preprocess_mixin.py +++ b/python_app/gui/controllers/app_window_preprocess_mixin.py @@ -3,11 +3,17 @@ from __future__ import annotations from python_app.gui.preprocess_dialog import PreprocessDialog +from python_app.orchestration.preprocess_assets import ( + PREPROCESS_ASSET_KEYS, + PREPROCESS_ASSET_SPECS, + preprocess_asset_channel, + preprocess_asset_display_name, +) from python_app.workflows.sequential_capture_workflow import SequentialCaptureSession class AppWindowPreprocessMixin: - """Handles calibration/reference set management and capture workflow.""" + """Handles preprocess set management and sequential capture workflows.""" def _open_preprocess_panel(self) -> None: """Open preprocessing dialog and refresh available sets.""" @@ -38,39 +44,39 @@ class AppWindowPreprocessMixin: self._update_capture_dialog_state() return dialog - def _on_preprocess_selection_changed(self, calibration_set: str, reference_set: str) -> None: + def _on_preprocess_selection_changed(self) -> None: """Persist selected preprocessing set names from dialog.""" - self._selected_s21_calibration_set = calibration_set.strip() - self._selected_s21_reference_set = reference_set.strip() + dialog = self._ensure_preprocess_dialog() + self._selected_preprocess_sets = dialog.selection_snapshot() self._refresh_preprocess_summary_labels() def _refresh_preprocess_summary_labels(self) -> None: """Update compact summary labels in the main window.""" - self._selected_calibration_label.setText(self._selected_s21_calibration_set or "") - self._selected_reference_label.setText(self._selected_s21_reference_set or "") + for key in PREPROCESS_ASSET_KEYS: + self._selected_preprocess_labels[key].setText(self._selected_preprocess_sets.get(key, "") or "") def _refresh_sets(self) -> None: - """Refresh calibration/reference set lists for current radar key.""" + """Refresh preprocess set lists for current radar key.""" config = self._build_config() radar_key = self._radar_key(config) - calibration_sets = self._store.list_sets("calibration", radar_key) - reference_sets = self._store.list_sets("reference", radar_key) - dialog = self._ensure_preprocess_dialog() - dialog.set_calibration_sets(calibration_sets) - dialog.set_reference_sets(reference_sets) - if self._selected_s21_calibration_set not in calibration_sets: - self._selected_s21_calibration_set = calibration_sets[0] if calibration_sets else "" - if self._selected_s21_reference_set not in reference_sets: - self._selected_s21_reference_set = reference_sets[0] if reference_sets else "" + available_sets = { + key: self._store.list_sets(PREPROCESS_ASSET_SPECS[key].set_kind, radar_key) + for key in PREPROCESS_ASSET_KEYS + } + dialog.set_available_sets(available_sets) - dialog.set_selected_sets(self._selected_s21_calibration_set, self._selected_s21_reference_set) + for key, names in available_sets.items(): + if self._selected_preprocess_sets.get(key, "") not in names: + self._selected_preprocess_sets[key] = names[0] if names else "" + + dialog.set_selected_sets(self._selected_preprocess_sets) self._refresh_preprocess_summary_labels() - self._log(f"Set lists refreshed for key={radar_key}") + self._log(f"Preprocess set lists refreshed for key={radar_key}") def _start_capture_sequence(self, kind: str) -> None: - """Start sequential capture session for requested preprocessing kind.""" + """Start sequential capture session for requested preprocess asset.""" if self._capture_session is not None: self._show_error("Another capture sequence is already active") return @@ -91,18 +97,19 @@ class AppWindowPreprocessMixin: try: config = self._build_config() radar_key = self._radar_key(config) - existing_sets = self._store.list_sets(kind, radar_key) + 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 {kind} and cannot be overwritten") + raise RuntimeError(f"Set '{set_name}' already exists for {display_name} and cannot be overwritten") session = SequentialCaptureSession(config=config, kind=kind, set_name=set_name) session.open() self._capture_session = session dialog.clear_capture_log() - dialog.set_status(f"{kind.title()} sequence started") + dialog.set_status(f"{display_name} sequence started") self._update_capture_dialog_state() - self._log(f"{kind.title()} sequence started for set={set_name}; fill all N*M combos") + self._log(f"{display_name} sequence started for set={set_name}; fill all N*M combos") except Exception as exc: # noqa: BLE001 self._cleanup_capture_session() self._show_error(f"Failed to start {kind} sequence: {exc}") @@ -121,9 +128,11 @@ class AppWindowPreprocessMixin: trace = session.capture_current_combo() state = session.state() tx_label, rx_label = dialog.antenna_labels() + display_name = preprocess_asset_display_name(session.kind) + channel = preprocess_asset_channel(session.kind) dialog.append_capture_log_entry( - kind=session.kind, + kind=display_name, captured_count=state.captured_count, total_count=state.total_count, input_pos=trace.combo.input_pos, @@ -132,11 +141,11 @@ class AppWindowPreprocessMixin: rx_label=rx_label, ) - dialog.draw_last_trace(trace, title=f"{session.kind.title()} captured") - self._draw_single_trace(trace, title=f"{session.kind.title()} last trace") + 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._log( - f"{session.kind.title()} 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}" ) @@ -144,16 +153,13 @@ class AppWindowPreprocessMixin: radar_key, collection = session.finalize(self._store) set_name = session.set_name kind = session.kind + display_name = preprocess_asset_display_name(kind) self._cleanup_capture_session() - if kind == "calibration": - self._selected_s21_calibration_set = set_name - else: - self._selected_s21_reference_set = set_name - + self._selected_preprocess_sets[kind] = set_name self._refresh_sets() - dialog.set_status(f"{kind.title()} set saved: {set_name} ({len(collection.traces)} traces)") - self._log(f"{kind.title()} sequence completed and saved: set={set_name}, key={radar_key}") + 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() else: self._update_capture_dialog_state() @@ -166,11 +172,11 @@ class AppWindowPreprocessMixin: if self._capture_session is None: return - kind = self._capture_session.kind + display_name = preprocess_asset_display_name(self._capture_session.kind) self._cleanup_capture_session() dialog = self._ensure_preprocess_dialog() - dialog.set_status(f"{kind.title()} sequence aborted") - self._log(f"{kind.title()} sequence aborted") + dialog.set_status(f"{display_name} sequence aborted") + self._log(f"{display_name} sequence aborted") if resume_pipeline: self._resume_pipeline_if_needed() diff --git a/python_app/gui/controllers/sections/preprocess_summary_section.py b/python_app/gui/controllers/sections/preprocess_summary_section.py index 7f49521..13458d9 100644 --- a/python_app/gui/controllers/sections/preprocess_summary_section.py +++ b/python_app/gui/controllers/sections/preprocess_summary_section.py @@ -4,16 +4,18 @@ from __future__ import annotations from PyQt6.QtWidgets import QFormLayout, QGroupBox, QLabel +from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, preprocess_asset_display_name + def build_preprocess_summary_group(owner) -> QGroupBox: - """Create selected calibration/reference summary section.""" + """Create selected preprocess-set summary section.""" group = QGroupBox("Selected Preprocess Sets") form = QFormLayout(group) form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) - owner._selected_calibration_label = QLabel("") - owner._selected_reference_label = QLabel("") - - form.addRow("Calibration", owner._selected_calibration_label) - form.addRow("Reference", owner._selected_reference_label) + owner._selected_preprocess_labels = {} + for key in PREPROCESS_ASSET_KEYS: + label = QLabel("") + owner._selected_preprocess_labels[key] = label + form.addRow(preprocess_asset_display_name(key), label) return group diff --git a/python_app/gui/controllers/sections/processing_section.py b/python_app/gui/controllers/sections/processing_section.py index 0d0cb63..d8e0650 100644 --- a/python_app/gui/controllers/sections/processing_section.py +++ b/python_app/gui/controllers/sections/processing_section.py @@ -110,6 +110,9 @@ def build_processing_group(owner) -> QGroupBox: owner._bscan_axis = QComboBox() owner._bscan_axis.addItems(["abs", "real", "phase"]) + owner._bscan_channel = QComboBox() + owner._bscan_channel.addItems(["s21", "s11"]) + owner._bscan_cut_m = QDoubleSpinBox() owner._bscan_cut_m.setDecimals(3) owner._bscan_cut_m.setRange(0.0, 2.0) @@ -141,6 +144,7 @@ def build_processing_group(owner) -> QGroupBox: owner._bscan_stop_freq_mhz.setValue(8800.0) bscan_form.addRow("Axis", owner._bscan_axis) + bscan_form.addRow("Channel", owner._bscan_channel) bscan_form.addRow("Cut m", owner._bscan_cut_m) bscan_form.addRow("Max depth m", owner._bscan_max_depth_m) bscan_form.addRow("Gain", owner._bscan_gain) @@ -221,6 +225,7 @@ def build_processing_group(owner) -> QGroupBox: form.addRow(owner._processing_mode_pages) owner._bscan_axis.currentTextChanged.connect(owner._on_processing_live_settings_changed) + owner._bscan_channel.currentTextChanged.connect(owner._on_processing_live_settings_changed) owner._bscan_cut_m.valueChanged.connect(owner._on_processing_live_settings_changed) owner._bscan_max_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed) owner._bscan_gain.valueChanged.connect(owner._on_processing_live_settings_changed) diff --git a/python_app/gui/plotting/bscan_history.py b/python_app/gui/plotting/bscan_history.py index 2b277f3..ecd31ef 100644 --- a/python_app/gui/plotting/bscan_history.py +++ b/python_app/gui/plotting/bscan_history.py @@ -22,15 +22,17 @@ def _result_tail( for collection in result_history[-history_limit:] if int(collection.collection_id) > int(floor_collection_id) ] - unique_tail: list[ResultCollection] = [] + unique_reversed_tail: list[ResultCollection] = [] seen_keys: set[tuple[int, int]] = set() - for collection in filtered: + for collection in reversed(filtered): key = (int(collection.collection_id), int(collection.monotonic_ns)) if key in seen_keys: continue seen_keys.add(key) - unique_tail.append(collection) - return unique_tail + unique_reversed_tail.append(collection) + + unique_reversed_tail.reverse() + return unique_reversed_tail def build_bscan_signature( @@ -47,6 +49,7 @@ def build_bscan_signature( ) return ( str(live_config.bscan_axis), + str(live_config.bscan_channel), float(live_config.bscan_cut_m), float(live_config.bscan_max_depth_m), float(live_config.bscan_gain), diff --git a/python_app/gui/preprocess_dialog.py b/python_app/gui/preprocess_dialog.py index 6d68c6b..1c2f9a2 100644 --- a/python_app/gui/preprocess_dialog.py +++ b/python_app/gui/preprocess_dialog.py @@ -1,4 +1,4 @@ -"""Dialog for calibration/reference set selection and sequential capture.""" +"""Dialog for preprocess set selection and sequential capture.""" from __future__ import annotations @@ -6,6 +6,7 @@ from PyQt6.QtCore import pyqtSignal from PyQt6.QtWidgets import ( QComboBox, QDialog, + QFormLayout, QGridLayout, QGroupBox, QHBoxLayout, @@ -15,23 +16,24 @@ from PyQt6.QtWidgets import ( QPushButton, QVBoxLayout, ) -import pyqtgraph as pg import numpy as np +import pyqtgraph as pg from python_app.models.dataset_model import TraceData +from python_app.orchestration.preprocess_assets import ( + PREPROCESS_ASSET_KEYS, + PREPROCESS_ASSET_SPECS, + S11_PREPROCESS_ASSET_KEYS, + S21_PREPROCESS_ASSET_KEYS, + preprocess_asset_display_name, +) class PreprocessDialog(QDialog): - """Standalone dialog for preprocessing capture workflows. - - The dialog combines three concerns: - 1. Set selection (calibration/reference). - 2. Sequential capture controls for filling all N*M combinations. - 3. Quick preview of the last captured trace. - """ + """Standalone dialog for preprocess set selection and capture workflows.""" refresh_requested = pyqtSignal() - selection_changed = pyqtSignal(str, str) + selection_changed = pyqtSignal() start_sequence_requested = pyqtSignal(str) capture_next_requested = pyqtSignal() abort_sequence_requested = pyqtSignal() @@ -39,13 +41,14 @@ class PreprocessDialog(QDialog): def __init__(self, parent=None) -> None: """Initialize window metadata and compose dialog UI.""" super().__init__(parent) + self._set_combos: dict[str, QComboBox] = {} self._init_window() self._build_ui() def _init_window(self) -> None: """Set static window properties.""" self.setWindowTitle("Preprocessing Setup") - self.resize(1040, 760) + self.resize(1120, 820) def _build_ui(self) -> None: """Build root dialog layout and all sections.""" @@ -57,28 +60,33 @@ class PreprocessDialog(QDialog): def _build_sets_group(self) -> QGroupBox: """Build set-management controls used for preprocessing snapshots.""" - group = QGroupBox("Calibration / Reference Sets", self) - layout = QGridLayout(group) + group = QGroupBox("Preprocess Sets", self) + layout = QVBoxLayout(group) + header_row = QHBoxLayout() self._set_name_input = QLineEdit("set_001", group) - self._calibration_combo = QComboBox(group) - self._reference_combo = QComboBox(group) - refresh_button = QPushButton("Refresh Sets", group) refresh_button.clicked.connect(self.refresh_requested.emit) + header_row.addWidget(QLabel("Set name")) + header_row.addWidget(self._set_name_input, stretch=1) + header_row.addWidget(refresh_button) + layout.addLayout(header_row) - self._calibration_combo.currentTextChanged.connect(self._emit_selection_changed) - self._reference_combo.currentTextChanged.connect(self._emit_selection_changed) + layout.addWidget(self._build_selector_group("S21", S21_PREPROCESS_ASSET_KEYS, group)) + layout.addWidget(self._build_selector_group("S11", S11_PREPROCESS_ASSET_KEYS, group)) + return group - layout.addWidget(QLabel("Set name"), 0, 0) - layout.addWidget(self._set_name_input, 0, 1) - layout.addWidget(refresh_button, 0, 2) + def _build_selector_group(self, title: str, keys: tuple[str, ...], parent: QGroupBox) -> QGroupBox: + """Build one selector subgroup for a channel family.""" + group = QGroupBox(title, parent) + form = QFormLayout(group) + form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) - layout.addWidget(QLabel("Calibration set"), 1, 0) - layout.addWidget(self._calibration_combo, 1, 1, 1, 2) - - layout.addWidget(QLabel("Reference set"), 2, 0) - layout.addWidget(self._reference_combo, 2, 1, 1, 2) + for key in keys: + combo = QComboBox(group) + combo.currentTextChanged.connect(self._emit_selection_changed) + self._set_combos[key] = combo + form.addRow(self._asset_row_label(key), combo) return group def _build_sequence_group(self) -> QGroupBox: @@ -95,8 +103,6 @@ class PreprocessDialog(QDialog): self._tx_antenna_label_input.setPlaceholderText("e.g. TX_A") self._rx_antenna_label_input.setPlaceholderText("e.g. RX_B") - button_row = self._build_sequence_button_row(group) - layout.addWidget(QLabel("Active type"), 0, 0) layout.addWidget(self._active_kind_label, 0, 1) layout.addWidget(QLabel("Progress"), 1, 0) @@ -107,22 +113,26 @@ class PreprocessDialog(QDialog): layout.addWidget(self._tx_antenna_label_input, 3, 1) layout.addWidget(QLabel("RX antenna label"), 4, 0) layout.addWidget(self._rx_antenna_label_input, 4, 1) - layout.addLayout(button_row, 5, 0, 1, 2) + layout.addLayout(self._build_sequence_button_grid(group), 5, 0, 1, 2) + layout.addLayout(self._build_sequence_action_row(group), 6, 0, 1, 2) self._capture_log = QPlainTextEdit(group) self._capture_log.setReadOnly(True) self._capture_log.setPlaceholderText("Capture history per combo") - layout.addWidget(self._capture_log, 6, 0, 1, 2) + layout.addWidget(self._capture_log, 7, 0, 1, 2) return group - def _build_sequence_button_row(self, parent: QGroupBox) -> QHBoxLayout: - """Build action buttons for sequence flow control.""" - start_calibration_button = QPushButton("Start Calibration Sequence", parent) - start_calibration_button.clicked.connect(lambda: self.start_sequence_requested.emit("calibration")) - - start_reference_button = QPushButton("Start Reference Sequence", parent) - start_reference_button.clicked.connect(lambda: self.start_sequence_requested.emit("reference")) + def _build_sequence_button_grid(self, parent: QGroupBox) -> QGridLayout: + """Build per-asset capture start buttons.""" + layout = QGridLayout() + for index, key in enumerate(PREPROCESS_ASSET_KEYS): + button = QPushButton(f"Start {preprocess_asset_display_name(key)}", parent) + button.clicked.connect(lambda _checked=False, asset_key=key: self.start_sequence_requested.emit(asset_key)) + layout.addWidget(button, index // 2, index % 2) + return layout + def _build_sequence_action_row(self, parent: QGroupBox) -> QHBoxLayout: + """Build capture/abort row for active sequence control.""" self._capture_next_button = QPushButton("Capture Current Combo", parent) self._capture_next_button.clicked.connect(self.capture_next_requested.emit) self._capture_next_button.setEnabled(False) @@ -131,12 +141,11 @@ class PreprocessDialog(QDialog): self._abort_button.clicked.connect(self.abort_sequence_requested.emit) self._abort_button.setEnabled(False) - button_row = QHBoxLayout() - button_row.addWidget(start_calibration_button) - button_row.addWidget(start_reference_button) - button_row.addWidget(self._capture_next_button) - button_row.addWidget(self._abort_button) - return button_row + layout = QHBoxLayout() + layout.addWidget(self._capture_next_button) + layout.addWidget(self._abort_button) + layout.addStretch(1) + return layout def _build_status_line(self, root_layout: QVBoxLayout) -> None: """Build one-line status output for dialog operations.""" @@ -155,13 +164,9 @@ class PreprocessDialog(QDialog): """Return requested target set name.""" return self._set_name_input.text().strip() - def calibration_set(self) -> str: - """Return currently selected calibration set.""" - return self._calibration_combo.currentText().strip() - - def reference_set(self) -> str: - """Return currently selected reference set.""" - return self._reference_combo.currentText().strip() + def selection_snapshot(self) -> dict[str, str]: + """Return currently selected set names keyed by preprocess asset key.""" + return {key: self._set_combos[key].currentText().strip() for key in PREPROCESS_ASSET_KEYS} def antenna_labels(self) -> tuple[str, str]: """Return optional TX/RX user labels used in capture logs.""" @@ -209,7 +214,8 @@ class PreprocessDialog(QDialog): self._abort_button.setEnabled(False) return - self._active_kind_label.setText(kind) + active_label = preprocess_asset_display_name(kind) if kind in PREPROCESS_ASSET_SPECS else kind + self._active_kind_label.setText(active_label) self._progress_label.setText(f"{captured_count} / {total_count}") if next_input is None or next_output is None: self._combo_label.setText("") @@ -220,35 +226,32 @@ class PreprocessDialog(QDialog): self._capture_next_button.setEnabled(True) self._abort_button.setEnabled(True) - def set_calibration_sets(self, names: list[str]) -> None: - """Replace calibration set choices while preserving current selection when possible.""" - self._set_combo_items(self._calibration_combo, names, self.calibration_set()) + def set_available_sets(self, available_sets: dict[str, list[str]]) -> None: + """Replace combo-box choices for all preprocess assets.""" + for key in PREPROCESS_ASSET_KEYS: + combo = self._set_combos[key] + self._set_combo_items(combo, available_sets.get(key, []), combo.currentText().strip()) - def set_reference_sets(self, names: list[str]) -> None: - """Replace reference set choices while preserving current selection when possible.""" - self._set_combo_items(self._reference_combo, names, self.reference_set()) - - def set_selected_sets(self, calibration_set: str, reference_set: str) -> None: - """Apply selected set names to both comboboxes and emit selection update.""" - if calibration_set: - index = self._calibration_combo.findText(calibration_set) + def set_selected_sets(self, selected_sets: dict[str, str]) -> None: + """Apply selected set names to all comboboxes and emit selection update.""" + for key in PREPROCESS_ASSET_KEYS: + selected_value = selected_sets.get(key, "") + if not selected_value: + continue + combo = self._set_combos[key] + index = combo.findText(selected_value) if index >= 0: - self._calibration_combo.setCurrentIndex(index) - - if reference_set: - index = self._reference_combo.findText(reference_set) - if index >= 0: - self._reference_combo.setCurrentIndex(index) - + combo.setCurrentIndex(index) self._emit_selection_changed() def set_status(self, message: str) -> None: """Set short human-readable status line.""" self._status_label.setText(message) - def draw_last_trace(self, trace: TraceData, title: str) -> None: - """Draw the latest captured sweep trace in dB scale.""" - magnitude_db = 20.0 * np.log10(np.maximum(np.abs(trace.s21), 1e-12)) + def draw_last_trace(self, trace: TraceData, title: str, *, channel: str) -> None: + """Draw the latest captured sweep trace for the requested channel in dB scale.""" + samples = trace.s11 if channel == "s11" else trace.s21 + magnitude_db = 20.0 * np.log10(np.maximum(np.abs(samples), 1e-12)) self._preview_plot.clear() self._preview_plot.plot( trace.frequency_hz, @@ -261,8 +264,13 @@ class PreprocessDialog(QDialog): ) def _emit_selection_changed(self) -> None: - """Emit current calibration/reference selection.""" - self.selection_changed.emit(self.calibration_set(), self.reference_set()) + """Emit current selection snapshot change.""" + self.selection_changed.emit() + + @staticmethod + def _asset_row_label(key: str) -> str: + """Return short selector label for one preprocess asset.""" + return preprocess_asset_display_name(key) @staticmethod def _set_combo_items(combo: QComboBox, names: list[str], current_text: str) -> None: diff --git a/python_app/gui/runtime/history.py b/python_app/gui/runtime/history.py index 1d7c25e..9441fe1 100644 --- a/python_app/gui/runtime/history.py +++ b/python_app/gui/runtime/history.py @@ -7,6 +7,7 @@ from typing import TypeVar from python_app.models.dataset_model import ResultCollection, SweepCollection from python_app.models.run_config_model import RunConfigModel +from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, preprocess_asset_model THistoryCollection = TypeVar("THistoryCollection", SweepCollection, ResultCollection) @@ -63,6 +64,7 @@ def build_run_history_signature( ) -> tuple[object, ...]: """Build deterministic signature to detect run-settings changes (excluding live processing params).""" combos_signature = tuple((int(combo.input), int(combo.output)) for combo in config.combos) + preprocess_signature = tuple(preprocess_asset_model(config, key).set_name for key in PREPROCESS_ASSET_KEYS) return ( str(config.radar.driver_mode), str(config.radar.serial), @@ -79,8 +81,7 @@ def build_run_history_signature( str(config.output_switch.driver), int(config.output_switch.positions), bool(config.output_switch.invert_logic), - str(config.preprocess.s21_calibration_set), - str(config.preprocess.s21_reference_set), + preprocess_signature, combos_signature, ) diff --git a/python_app/hardware_full/librevna_backends.py b/python_app/hardware_full/librevna_backends.py index 3bfd97c..61d66bb 100644 --- a/python_app/hardware_full/librevna_backends.py +++ b/python_app/hardware_full/librevna_backends.py @@ -8,6 +8,7 @@ from typing import Any, Protocol import numpy as np +from python_app.hardware_full.librevna_driver.models import SweepResult from python_app.models.run_config_model import RadarSweepModel @@ -30,8 +31,8 @@ class LibreVnaBackend(Protocol): def read_device_limits(self) -> dict[str, float | int]: """Query runtime device limits.""" - def acquire_s21(self) -> tuple[np.ndarray, np.ndarray]: - """Acquire one S21 trace.""" + def acquire(self) -> SweepResult: + """Acquire one sweep with all available traces.""" @dataclass(slots=True) @@ -108,15 +109,21 @@ class NativeLibreVnaBackend: "max_power_dbm": float(limits.max_power_dbm), } - def acquire_s21(self) -> tuple[np.ndarray, np.ndarray]: - """Acquire one S21 sweep from hardware.""" + def acquire(self) -> SweepResult: + """Acquire one sweep from hardware.""" if self._settings is None: raise RuntimeError("Radar service is not configured") if self._device is None: raise RuntimeError("Device not found") result = self._device.vna.acquire(expected_points=self._settings.points, timeout_s=20.0) - return np.asarray(result.x, dtype=np.float32), np.asarray(result.trace("s21"), dtype=np.complex64) + return SweepResult( + x=np.asarray(result.x, dtype=np.float32), + traces={ + "s11": np.asarray(result.trace("s11"), dtype=np.complex64), + "s21": np.asarray(result.trace("s21"), dtype=np.complex64), + }, + ) @dataclass(slots=True) @@ -149,15 +156,26 @@ class MockLibreVnaBackend: """Mock backend does not support native device limits queries.""" raise RuntimeError("LibreVNA Python driver is not available") - def acquire_s21(self) -> tuple[np.ndarray, np.ndarray]: - """Generate synthetic S21 values using deterministic phase envelope.""" + def acquire(self) -> SweepResult: + """Generate synthetic S11 and S21 values using deterministic envelopes.""" if self._settings is None: raise RuntimeError("Radar service is not configured") points = self._settings.points freq = np.linspace(self._settings.start_hz, self._settings.stop_hz, points, dtype=np.float32) phase = (2.0 * math.pi * np.linspace(0.0, 1.0, points, dtype=np.float32)) + self._mock_phase - envelope = 0.6 + 0.4 * np.sin(phase * 0.5) - s21 = (envelope * np.cos(phase) + 1j * envelope * np.sin(phase)).astype(np.complex64) + s21_envelope = 0.6 + 0.4 * np.sin(phase * 0.5) + s11_envelope = 0.25 + 0.15 * np.cos(phase * 0.75) + s21 = (s21_envelope * np.cos(phase) + 1j * s21_envelope * np.sin(phase)).astype(np.complex64) + reflected_phase = phase * 0.6 + 0.8 + s11 = ( + s11_envelope * np.cos(reflected_phase) + 1j * s11_envelope * np.sin(reflected_phase) + ).astype(np.complex64) self._mock_phase += 0.05 - return freq, s21 + return SweepResult( + x=freq, + traces={ + "s11": s11, + "s21": s21, + }, + ) diff --git a/python_app/hardware_full/librevna_service.py b/python_app/hardware_full/librevna_service.py index f0c584d..1bb7dd8 100644 --- a/python_app/hardware_full/librevna_service.py +++ b/python_app/hardware_full/librevna_service.py @@ -4,9 +4,8 @@ from __future__ import annotations from dataclasses import dataclass, field -import numpy as np - from python_app.hardware_full.librevna_backends import LibreVnaBackend, MockLibreVnaBackend, NativeLibreVnaBackend +from python_app.hardware_full.librevna_driver.models import SweepResult from python_app.models.run_config_model import RadarSweepModel @@ -89,10 +88,10 @@ class LibreVnaService: if opened_here: self.close() - def acquire_s21(self) -> tuple[np.ndarray, np.ndarray]: - """Acquire one S21 trace from currently selected backend.""" + def acquire(self) -> SweepResult: + """Acquire one sweep with all available traces from active backend.""" if self._backend is None: raise RuntimeError("LibreVNA backend is not initialized") if self._using_mock_backend and not self._driver_available and self.backend_mode != "mock": raise RuntimeError("Device not found") - return self._backend.acquire_s21() + return self._backend.acquire() diff --git a/python_app/models/dataset_model.py b/python_app/models/dataset_model.py index 3e7fe24..78ab8ae 100644 --- a/python_app/models/dataset_model.py +++ b/python_app/models/dataset_model.py @@ -32,10 +32,11 @@ class ComboKey: @dataclass(slots=True) class TraceData: - """One frequency-domain S21 trace for a specific switch combination.""" + """One frequency-domain trace set for a specific switch combination.""" combo: ComboKey frequency_hz: np.ndarray + s11: np.ndarray s21: np.ndarray diff --git a/python_app/models/run_config_codec.py b/python_app/models/run_config_codec.py index 4d32564..c7b8aed 100644 --- a/python_app/models/run_config_codec.py +++ b/python_app/models/run_config_codec.py @@ -8,6 +8,7 @@ from python_app.models.run_config_schema import ( ComboModel, GprRxGeometryModel, GprTxGeometryModel, + PreprocessAssetModel, RunConfigModel, ) from python_app.models.run_config_validation import load_ring_payload, load_switch_payload, validate_gpr_model @@ -22,6 +23,12 @@ def _as_dict(value: Any, context: str) -> dict[str, Any]: return value +def _load_preprocess_asset(payload: dict[str, Any], target: PreprocessAssetModel) -> None: + """Load preprocess asset fields into target model.""" + target.set_name = str(payload.get("set_name", target.set_name)) + target.bundle_path = str(payload.get("bundle_path", target.bundle_path)) + + def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel: """Decode JSON-like payload into :class:`RunConfigModel`.""" # Schema carries only minimal-safe fallbacks; operational defaults live in run_config.json. @@ -65,47 +72,33 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel: run_payload.get("processing_live_config_path", model.runtime.processing_live_config_path) ) - model.preprocess.s21_calibration_set = str( - preprocess_payload.get("s21_calibration_set", model.preprocess.s21_calibration_set) + s21_preprocess_payload = _as_dict(preprocess_payload.get("s21"), "preprocess.s21") + _load_preprocess_asset( + _as_dict(s21_preprocess_payload.get("calibration"), "preprocess.s21.calibration"), + model.preprocess.s21.calibration, ) - model.preprocess.s21_reference_set = str( - preprocess_payload.get("s21_reference_set", model.preprocess.s21_reference_set) + _load_preprocess_asset( + _as_dict(s21_preprocess_payload.get("reference"), "preprocess.s21.reference"), + model.preprocess.s21.reference, ) - model.preprocess.s21_calibration_bundle_path = str( - preprocess_payload.get( - "s21_calibration_bundle_path", - model.preprocess.s21_calibration_bundle_path, - ) + + s11_preprocess_payload = _as_dict(preprocess_payload.get("s11"), "preprocess.s11") + s11_calibration_payload = _as_dict(s11_preprocess_payload.get("calibration"), "preprocess.s11.calibration") + _load_preprocess_asset( + _as_dict(s11_calibration_payload.get("open"), "preprocess.s11.calibration.open"), + model.preprocess.s11.calibration.open, ) - model.preprocess.s21_reference_bundle_path = str( - preprocess_payload.get( - "s21_reference_bundle_path", - model.preprocess.s21_reference_bundle_path, - ) + _load_preprocess_asset( + _as_dict(s11_calibration_payload.get("short"), "preprocess.s11.calibration.short"), + model.preprocess.s11.calibration.short, ) - model.preprocess.s11_open_calibration_bundle_path = str( - preprocess_payload.get( - "s11_open_calibration_bundle_path", - model.preprocess.s11_open_calibration_bundle_path, - ) + _load_preprocess_asset( + _as_dict(s11_calibration_payload.get("load"), "preprocess.s11.calibration.load"), + model.preprocess.s11.calibration.load, ) - model.preprocess.s11_short_calibration_bundle_path = str( - preprocess_payload.get( - "s11_short_calibration_bundle_path", - model.preprocess.s11_short_calibration_bundle_path, - ) - ) - model.preprocess.s11_load_calibration_bundle_path = str( - preprocess_payload.get( - "s11_load_calibration_bundle_path", - model.preprocess.s11_load_calibration_bundle_path, - ) - ) - model.preprocess.s11_reference_bundle_path = str( - preprocess_payload.get( - "s11_reference_bundle_path", - model.preprocess.s11_reference_bundle_path, - ) + _load_preprocess_asset( + _as_dict(s11_preprocess_payload.get("reference"), "preprocess.s11.reference"), + model.preprocess.s11.reference, ) model.gpr.mode = str(gpr_payload.get("mode", model.gpr.mode)) @@ -213,14 +206,36 @@ def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]: "combos": [{"input": combo.input, "output": combo.output} for combo in model.combos], }, "preprocess": { - "s21_calibration_set": model.preprocess.s21_calibration_set, - "s21_reference_set": model.preprocess.s21_reference_set, - "s21_calibration_bundle_path": model.preprocess.s21_calibration_bundle_path, - "s21_reference_bundle_path": model.preprocess.s21_reference_bundle_path, - "s11_open_calibration_bundle_path": model.preprocess.s11_open_calibration_bundle_path, - "s11_short_calibration_bundle_path": model.preprocess.s11_short_calibration_bundle_path, - "s11_load_calibration_bundle_path": model.preprocess.s11_load_calibration_bundle_path, - "s11_reference_bundle_path": model.preprocess.s11_reference_bundle_path, + "s21": { + "calibration": { + "set_name": model.preprocess.s21.calibration.set_name, + "bundle_path": model.preprocess.s21.calibration.bundle_path, + }, + "reference": { + "set_name": model.preprocess.s21.reference.set_name, + "bundle_path": model.preprocess.s21.reference.bundle_path, + }, + }, + "s11": { + "calibration": { + "open": { + "set_name": model.preprocess.s11.calibration.open.set_name, + "bundle_path": model.preprocess.s11.calibration.open.bundle_path, + }, + "short": { + "set_name": model.preprocess.s11.calibration.short.set_name, + "bundle_path": model.preprocess.s11.calibration.short.bundle_path, + }, + "load": { + "set_name": model.preprocess.s11.calibration.load.set_name, + "bundle_path": model.preprocess.s11.calibration.load.bundle_path, + }, + }, + "reference": { + "set_name": model.preprocess.s11.reference.set_name, + "bundle_path": model.preprocess.s11.reference.bundle_path, + }, + }, }, "gpr": { "mode": model.gpr.mode, diff --git a/python_app/models/run_config_model.py b/python_app/models/run_config_model.py index ecf8686..cb92aba 100644 --- a/python_app/models/run_config_model.py +++ b/python_app/models/run_config_model.py @@ -6,6 +6,7 @@ from python_app.models.run_config_schema import ( GprModel, GprRxGeometryModel, GprTxGeometryModel, + PreprocessAssetModel, PreprocessModel, RadarModel, RadarSweepModel, @@ -13,6 +14,9 @@ from python_app.models.run_config_schema import ( RingsModel, RunConfigModel, RuntimeModel, + S11CalibrationModel, + S11PreprocessModel, + S21PreprocessModel, SwitchModel, ) from python_app.models.run_config_validation import ( @@ -26,6 +30,7 @@ __all__ = [ "GprModel", "GprRxGeometryModel", "GprTxGeometryModel", + "PreprocessAssetModel", "PreprocessModel", "RadarModel", "RadarSweepModel", @@ -33,6 +38,9 @@ __all__ = [ "RingsModel", "RunConfigModel", "RuntimeModel", + "S11CalibrationModel", + "S11PreprocessModel", + "S21PreprocessModel", "SwitchModel", "load_ring_payload", "load_switch_payload", diff --git a/python_app/models/run_config_schema.py b/python_app/models/run_config_schema.py index 89bb5ef..f209267 100644 --- a/python_app/models/run_config_schema.py +++ b/python_app/models/run_config_schema.py @@ -85,18 +85,45 @@ class RuntimeModel: processing_live_config_path: str = "" +@dataclass(slots=True) +class PreprocessAssetModel: + """One preprocessing asset selected for live acquisition.""" + + set_name: str = "" + bundle_path: str = "" + + +@dataclass(slots=True) +class S21PreprocessModel: + """Two-port S21 preprocessing assets.""" + + calibration: PreprocessAssetModel = field(default_factory=PreprocessAssetModel) + reference: PreprocessAssetModel = field(default_factory=PreprocessAssetModel) + + +@dataclass(slots=True) +class S11CalibrationModel: + """One-port S11 OSL calibration assets.""" + + open: PreprocessAssetModel = field(default_factory=PreprocessAssetModel) + short: PreprocessAssetModel = field(default_factory=PreprocessAssetModel) + load: PreprocessAssetModel = field(default_factory=PreprocessAssetModel) + + +@dataclass(slots=True) +class S11PreprocessModel: + """One-port S11 preprocessing assets.""" + + calibration: S11CalibrationModel = field(default_factory=S11CalibrationModel) + reference: PreprocessAssetModel = field(default_factory=PreprocessAssetModel) + + @dataclass(slots=True) class PreprocessModel: """Selected preprocessing artifacts for live acquisition.""" - s21_calibration_set: str = "" - s21_reference_set: str = "" - s21_calibration_bundle_path: str = "" - s21_reference_bundle_path: str = "" - s11_open_calibration_bundle_path: str = "" - s11_short_calibration_bundle_path: str = "" - s11_load_calibration_bundle_path: str = "" - s11_reference_bundle_path: str = "" + s21: S21PreprocessModel = field(default_factory=S21PreprocessModel) + s11: S11PreprocessModel = field(default_factory=S11PreprocessModel) @dataclass(slots=True) diff --git a/python_app/orchestration/config_writer.py b/python_app/orchestration/config_writer.py index f356c93..242b929 100644 --- a/python_app/orchestration/config_writer.py +++ b/python_app/orchestration/config_writer.py @@ -6,6 +6,7 @@ import json from pathlib import Path from python_app.models.run_config_model import RunConfigModel, parse_combos_from_text +from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, PREPROCESS_ASSET_SPECS, preprocess_asset_model from python_app.storage.npz_store import NpzStore @@ -17,20 +18,19 @@ class ConfigWriter: self._runtime_dir = runtime_dir self._runtime_dir.mkdir(parents=True, exist_ok=True) - def prepare_s21_bundles( + def prepare_preprocess_bundles( self, store: NpzStore, radar_key: str, - s21_calibration_set: str, - s21_reference_set: str, - ) -> tuple[Path, Path]: - """Export calibration/reference sets into binary bundles for preprocessor.""" - calibration_bundle = self._runtime_dir / "s21_calibration_bundle.bin" - reference_bundle = self._runtime_dir / "s21_reference_bundle.bin" - - store.export_set_bundle("calibration", radar_key, s21_calibration_set, calibration_bundle) - store.export_set_bundle("reference", radar_key, s21_reference_set, reference_bundle) - return calibration_bundle, reference_bundle + config: RunConfigModel, + ) -> None: + """Export selected preprocess sets into runtime bundles and update config paths.""" + for key in PREPROCESS_ASSET_KEYS: + spec = PREPROCESS_ASSET_SPECS[key] + asset = preprocess_asset_model(config, key) + bundle_path = self._runtime_dir / spec.runtime_filename + store.export_set_bundle(spec.set_kind, radar_key, asset.set_name, bundle_path) + asset.bundle_path = str(bundle_path) def write(self, config: RunConfigModel, output_path: Path) -> Path: """Write run configuration JSON file.""" diff --git a/python_app/orchestration/live_processing_config.py b/python_app/orchestration/live_processing_config.py index 2ae98c2..0902368 100644 --- a/python_app/orchestration/live_processing_config.py +++ b/python_app/orchestration/live_processing_config.py @@ -19,6 +19,7 @@ class ProcessingLiveConfig: pass_through_y_min_db: float = -100.0 pass_through_y_max_db: float = 0.0 bscan_axis: str = "abs" + bscan_channel: str = "s21" bscan_cut_m: float = 0.824 bscan_max_depth_m: float = 1.0 bscan_gain: float = 1.0 @@ -58,6 +59,7 @@ class ProcessingLiveConfig: "pass_through_y_min_db": float(self.pass_through_y_min_db), "pass_through_y_max_db": float(self.pass_through_y_max_db), "bscan_axis": str(self.bscan_axis), + "bscan_channel": str(self.bscan_channel), "bscan_cut_m": float(self.bscan_cut_m), "bscan_max_depth_m": float(self.bscan_max_depth_m), "bscan_gain": float(self.bscan_gain), diff --git a/python_app/orchestration/preprocess_assets.py b/python_app/orchestration/preprocess_assets.py new file mode 100644 index 0000000..b0d3727 --- /dev/null +++ b/python_app/orchestration/preprocess_assets.py @@ -0,0 +1,95 @@ +"""Canonical preprocess asset definitions shared by GUI and runtime orchestration.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from python_app.models.run_config_model import PreprocessAssetModel, RunConfigModel + + +@dataclass(frozen=True, slots=True) +class PreprocessAssetSpec: + """Stable description of one preprocess asset.""" + + key: str + display_name: str + set_kind: str + runtime_filename: str + channel: str + + +PREPROCESS_ASSET_SPECS = { + "s21_calibration": PreprocessAssetSpec( + key="s21_calibration", + display_name="S21 Calibration", + set_kind="s21_calibration", + runtime_filename="s21_calibration_bundle.bin", + channel="s21", + ), + "s21_reference": PreprocessAssetSpec( + key="s21_reference", + display_name="S21 Reference", + set_kind="s21_reference", + runtime_filename="s21_reference_bundle.bin", + channel="s21", + ), + "s11_open": PreprocessAssetSpec( + key="s11_open", + display_name="S11 Open", + set_kind="s11_open", + runtime_filename="s11_open_calibration_bundle.bin", + channel="s11", + ), + "s11_short": PreprocessAssetSpec( + key="s11_short", + display_name="S11 Short", + set_kind="s11_short", + runtime_filename="s11_short_calibration_bundle.bin", + channel="s11", + ), + "s11_load": PreprocessAssetSpec( + key="s11_load", + display_name="S11 Load", + set_kind="s11_load", + runtime_filename="s11_load_calibration_bundle.bin", + channel="s11", + ), + "s11_reference": PreprocessAssetSpec( + key="s11_reference", + display_name="S11 Reference", + set_kind="s11_reference", + runtime_filename="s11_reference_bundle.bin", + channel="s11", + ), +} + +PREPROCESS_ASSET_KEYS = tuple(PREPROCESS_ASSET_SPECS.keys()) +S21_PREPROCESS_ASSET_KEYS = ("s21_calibration", "s21_reference") +S11_PREPROCESS_ASSET_KEYS = ("s11_open", "s11_short", "s11_load", "s11_reference") + + +def preprocess_asset_model(config: RunConfigModel, key: str) -> PreprocessAssetModel: + """Return nested preprocess asset model by canonical asset key.""" + if key == "s21_calibration": + return config.preprocess.s21.calibration + if key == "s21_reference": + return config.preprocess.s21.reference + if key == "s11_open": + return config.preprocess.s11.calibration.open + if key == "s11_short": + return config.preprocess.s11.calibration.short + if key == "s11_load": + return config.preprocess.s11.calibration.load + if key == "s11_reference": + return config.preprocess.s11.reference + raise KeyError(f"Unknown preprocess asset key: {key}") + + +def preprocess_asset_display_name(key: str) -> str: + """Return human-readable label for preprocess asset.""" + return PREPROCESS_ASSET_SPECS[key].display_name + + +def preprocess_asset_channel(key: str) -> str: + """Return associated trace channel for preprocess asset.""" + return PREPROCESS_ASSET_SPECS[key].channel diff --git a/python_app/orchestration/shm/decoder.py b/python_app/orchestration/shm/decoder.py index a6935be..147fa84 100644 --- a/python_app/orchestration/shm/decoder.py +++ b/python_app/orchestration/shm/decoder.py @@ -40,16 +40,16 @@ def decode_trace_collection(payload: bytes, expected_magic: int) -> SweepCollect freq = np.frombuffer(cursor.read_bytes(freq_bytes), dtype="ckj3q=$=D9ysz`)p89fz2LU42ohQ#uhc#Mg)Tjii!#<_6CSG zcJzjFW}S@%M6sYoi6n|8ibk)HV2y9^u7Ugh5B#ur9`wN3XUg}xW*$GTSu@iAE6;is zwO>cV8rsijWnp3Szn{flm&G4l76f%%{3>MK;pu8%;&C+~@0sfI$vU*t*50Ac9zmhb zSEq(<@LC>fvphL8`TCwv>yF1llV{|HT4fc6CN(SxwHo;}G%2+zbk#cxZKy*N%`(7R zE0}4gC2VxihGciqEX#Un1&zG4gx^|EYr2~JN9cX zZiltFyrbHqQ71M3@=Pr~`X{ZPRjxKU9kg@a=d@0R7q#W#h1#RnMVf!y4K3aFwpQ=R zU2U>ds-3%WU+Wb4NL%*qckSVt=bE3xD=qC*g=P^@rA;b%r{&H3TXU}dN6Ra;VBc-3 z&z#3MWW}zQY*|HP);H%1_Ass)+n}~!eshlu%(GzSf9~dS(!t3mR#P0DW`j}ifA{M8qkL^D-ZU$#FL#& z_hu7k^<&mP1DIYtkj*VLFsDs2yAiIirLO+0PsJcsmL0&72xCV= z+2?tTos>qfiI$_7b@6Cs-Z6&Fi40?oZsXYXci}8HZvt~qoX8%Gn#__MBAK!LYxero zH*9b8bfyK&WYsOcWhY8zvxxM$%xc!ZnAv+in^V1jITkKr*EfF8V#A}EyXzA6;PnqI zDSIh%I=76yKd_wLNQ`5L=Ek$7qgSx62Cig%91~dM2CGFIeil=Nl0NQXKiMyhHqgJK3iG8_S=}% z=WXoSlkLo0xPxsyzLU+_w2KXi+RYrt?_ux!_Ok1)sqA3WG!|Qt#>V`d&fIhMG0VLf z>_Oaq_S3WjEJ;1czVSH3jCO~a-KWFsb=eVCaPddB_sCJUDCro}<{xKW!%nbj=_I?` z=@dI*d734>Jk27CGuaubnN)Q%NOq}yZ!moqEQf!4&(Qv#VEKGOuAxCxh&;e%w_(PE z5V>q?v?0Jtm3P0B3`q-B*(I^s*LXmc+uFqYjySK%2jqVZxOi2S=Y5=(yX~SXH(p$k zd*z5K7tHlBO_YIX#+zRBqVTk-g{%F1|KT!U#Ka!W2 z0_428^LS9&VA*%mGF~}vko@@JDqim8FMD=Q=IRqgUJ|yIOUsRN`(=Chy%w?@v130E zSYwb+WgO*cRRiUzdoy{f&;jzVt8;kXmVWZADS7IVBOMQ{@kcJ^j}~rUn;hOg3GPs4M{EG zQ%f7!L1_bRLtDtb9^1jVn5J@kR6D4@r?LF3x&w^PY$)4(?+DvW_2jb0&R{d|AE|-T z87jYgFAZPk3L%~Ul#;G=gY42jq*Wh#z_lg6OD)><2HU0&q?g^@p=W%F^k?_JaNv2d zRAui8&W0KByH+ZB;twh1@sfYL!SJ@rV(C9p0JQjerlf0uaA$qE zl&TGZQyHohB@KnU2W834ei+=@?k;6M8U|0Jouta`L9jrxk?zUCP}!!QG`=_(2B$tT z%o!d6Ri7^!EX*NrLpx|_)=q`_NvjMAlU0b1A7PleO@-DatqqyEDopY{<~y!Ph4B05 z2S#32!6w}{x9F@2+eZ}SrmR%C7Grk4uSqxkC~qL3xQ{g zi%gZ7!O+U~57X+7!4MPLkn8h}K#|eLHYVX#zWU+e6B+MsT!y2e_S6ANCw{fUrfMxxvmEN+y5g zL!w;ZP`@gkc)1I(x3Bom&AY+8z0dd`em!7==R>}CYA^UQ^B&KNb_3>kn}4ya4>XCo z%BL^%0B^|WspGsL>`yaK?BfGH+hp_iRsF!&^)%NH4S??5f8=_wFC1u}!Ha$~fM4}4 zetv=sS+_Uy*lR}EziT}&>*WXg$0YE=@BN{&X)L!pI|!zZn9EPz9}I4BlX%qI07%?B zf{*?b2ykc+_pBHKqf@>3+v1_{LqZq6J`FvOu;sr`2!fa%mi%RtVEAz3Kc-FT!Qk5e zhN-Aq2pn5`%Cu%z2y`jlXgdFYArNZyjcHDZ3Psbpn^vq+!8!6yuEl8;91Fd2%Py&4 zT9+`e<5d-UcHiTh11dCT%?(+*RoL}UkimY23O`+qH#~7sAuVa2p~ICBci{Hyj*?Xcr~ZL($V9EvC&5_Wy;<+#!F56D!*O(Y_zHDrG!T`Qp%5X zQ|!(*RlXkBSt)pCtz@J+DjnapQEuFCSMx1ii{FX5Yq@fq=1(S~SCv?cSKJtS&skwsheMwyk3y!V6t^f_>{a z2H}OCkDlPzbR44*$ExF)g%`RBJvlcbSocOxaBjLcdV+J+an44ZyN((NFLV)~sEsb- z6}8gy&=b^7M-7drrEZ6wptkyN=m~1A$Dk*uz1|c(K`-=rn^Vt-pTp1-@&11FL_B{7 zJrSR`Mo&b0cIb&{AJCH~*k51tMD%|YJ;B(}F-FW7D>}xE@IuG+A-vErrp#hK7-MFP zH63HljIpO<44TFKF(%Czn>xm*xd`=fL{Bhw^{>$rq2B}a#0vEskDfe7eQnW`si=28 zda@t&Z;zf7qaWAt+W}wi!{1%P_agC`Hu&rd{LEPVY)fpT1lzidZO*{98(<%uu&*oF zXC?OSietEhV-aIIh+}JxW87IAKe`#mF6Pi5=OR4mUmI`whuZklm)6Fkeg?Hzj+zXp zjaNOtHhy&n)Gi-2bgGSS{rB2<*Vom?zm9v8SP!~?XLnEGNx$0lqNmiZAN_gldeU9c z%P{oB3B48V6`w{&74tF@oc(i5VPkO$_1qYhnuLQxju2znYlC`5MK1 zasD+iiFz1?9<~^(*HE7c7`shSFVxQ{^g}%rp(pCA2z}8DMRCw?b>Cw?b>Cw?b>Cw?b>Cw?b>Cw?b>Cw{NH z{9a%DCop~yzZ1U`zZ1U`zZ1U`zZ1U`zZ1U`zZ1U`zZ1U`zt>%UZ%~Wx#P4)|p3cwH z`FT1&Pv__9{5+kXr}Oi4exAHK^hK0hz;U7Vq({5$2}DgRFScgnw0 z{+;sglz*rEJLTUg|4#XL%D+?oo$~K>H~%i!o%o&ZucQ0x=>9sozmD#&qxx`~G=R|4#f){7(E%{7(E%{7(E%{7(E%{7(E%{7(E%{7(E{xA`6M-9qHxss5en z->Lqc>ffpUo$BAI{+;UIss5en-|753ou8-k^K^cm&d=A~^YbG2PW(>!cgnw0{+;sg zlz*rEJLTUg|4#XL%D+?oo$~LLf2aI=-Oj%Yb|-!(ekXn>ekXn>ekXn>ekXn>ekXn> zekXn>ekXpf+x#x---+Le--+Le--+Le--+Le--+Le--+Le--+Le--+MrHoqgjTL^}y z`gf{-r}}rQf2aC)s(+{YcdCD<`gf{-r}}rQf2aC)s(+{Y_qtvGPW(>!cgnw0{+;sg zlz*rEJLTUg|4#XL%D+?oo$~LLf2aI=-Oj&@v-8C7#P7uK#P7uK#P7uK#P7uK#P7uK z#P7uK#P7uKb(`M>yA!_?zZ1U`zZ1U`zZ1U`zZ1U`zZ1U`zZ1U`zZ1XLZGQhRh-!gu literal 0 HcmV?d00001 diff --git a/python_app/runtime/s11_open_calibration_bundle.bin b/python_app/runtime/s11_open_calibration_bundle.bin new file mode 100644 index 0000000000000000000000000000000000000000..3484277c9c27a2eeef66934f6d131835f0886868 GIT binary patch literal 64536 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 R{LcK&{LcK&{9d#9{l9`|^LGFM literal 0 HcmV?d00001 diff --git a/python_app/runtime/s11_reference_bundle.bin b/python_app/runtime/s11_reference_bundle.bin new file mode 100644 index 0000000000000000000000000000000000000000..5722cae2855b03cdf6682fb24443a7c26159b200 GIT binary patch literal 64536 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 XJMla5JMla5JMla5JMnwV=J)>s8Jy~9 literal 0 HcmV?d00001 diff --git a/python_app/runtime/s21_calibration_bundle.bin b/python_app/runtime/s21_calibration_bundle.bin new file mode 100644 index 0000000000000000000000000000000000000000..7917932a5b74a48d602cd971dc3d9ccc6dff1507 GIT binary patch literal 64536 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 literal 0 HcmV?d00001 diff --git a/python_app/runtime/s21_reference_bundle.bin b/python_app/runtime/s21_reference_bundle.bin new file mode 100644 index 0000000000000000000000000000000000000000..415b014a42e3c080bdc2b34c34ac7ae378b43ed9 GIT binary patch literal 64536 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 literal 0 HcmV?d00001 diff --git a/python_app/scripts/check_snapshot_numpy.py b/python_app/scripts/check_snapshot_numpy.py index d1b2524..2da91c7 100644 --- a/python_app/scripts/check_snapshot_numpy.py +++ b/python_app/scripts/check_snapshot_numpy.py @@ -46,12 +46,22 @@ def _all_traces_from_raw_or_pre(collection_dir: Path) -> list[tuple[np.ndarray, raise ValueError(f"Invalid trace record in {collection_dir / 'meta.json'}") freq_file = str(trace_meta.get("freq_file", "")) + s11_file = str(trace_meta.get("s11_file", "")) s21_file = str(trace_meta.get("s21_file", "")) freq = np.load(collection_dir / freq_file) + s11 = np.load(collection_dir / s11_file) s21 = np.load(collection_dir / s21_file) + if freq.shape != s11.shape: + raise ValueError(f"Shape mismatch freq/s11 in {collection_dir}") if freq.shape != s21.shape: raise ValueError(f"Shape mismatch freq/s21 in {collection_dir}") - if not (np.isfinite(freq).all() and np.isfinite(np.real(s21)).all() and np.isfinite(np.imag(s21)).all()): + if not ( + np.isfinite(freq).all() + and np.isfinite(np.real(s11)).all() + and np.isfinite(np.imag(s11)).all() + and np.isfinite(np.real(s21)).all() + and np.isfinite(np.imag(s21)).all() + ): raise ValueError(f"Non-finite values in {collection_dir}") label = f"i{int(trace_meta.get('input', 0))}_o{int(trace_meta.get('output', 0))}" diff --git a/python_app/scripts/manual_smoke_run.py b/python_app/scripts/manual_smoke_run.py index a9b1107..42a306f 100644 --- a/python_app/scripts/manual_smoke_run.py +++ b/python_app/scripts/manual_smoke_run.py @@ -49,7 +49,13 @@ def _shm_unlink(name: str) -> None: raise OSError(err, f"shm_unlink failed for {name}") -def build_synthetic_collection(config: RunConfigModel, value_scale: float) -> SweepCollection: +def build_synthetic_collection( + config: RunConfigModel, + value_scale: float, + *, + s11_scale: float, + s11_phase_offset: float, +) -> SweepCollection: """Build synthetic sweep collection for all configured switch combos.""" traces: list[TraceData] = [] combos = RunConfigModel.build_full_combos(config.input_switch.positions, config.output_switch.positions) @@ -62,12 +68,15 @@ def build_synthetic_collection(config: RunConfigModel, value_scale: float) -> Sw dtype=np.float32, ) phase = np.linspace(0.0, np.pi * 2.0, config.radar.sweep.points, dtype=np.float32) + reflected_phase = (phase * 0.6) + s11_phase_offset + s11 = (s11_scale * (np.cos(reflected_phase) + 1j * np.sin(reflected_phase))).astype(np.complex64) s21 = value_scale * (np.cos(phase) + 1j * np.sin(phase)).astype(np.complex64) traces.append( TraceData( combo=ComboKey(input_pos=combo.input, output_pos=combo.output), frequency_hz=frequency_hz, + s11=s11, s21=s21, ) ) @@ -107,22 +116,27 @@ def main() -> int: power_dbm=config.radar.sweep.power_dbm, ) - calibration_set = build_synthetic_collection(config, value_scale=1.0) - reference_set = build_synthetic_collection(config, value_scale=0.3) + s21_calibration_set = build_synthetic_collection(config, value_scale=1.0, s11_scale=0.15, s11_phase_offset=0.4) + s21_reference_set = build_synthetic_collection(config, value_scale=0.3, s11_scale=0.08, s11_phase_offset=0.9) + s11_open_set = build_synthetic_collection(config, value_scale=0.85, s11_scale=0.95, s11_phase_offset=0.1) + s11_short_set = build_synthetic_collection(config, value_scale=0.85, s11_scale=0.95, s11_phase_offset=3.2) + s11_load_set = build_synthetic_collection(config, value_scale=0.85, s11_scale=0.05, s11_phase_offset=1.4) + s11_reference_set = build_synthetic_collection(config, value_scale=0.3, s11_scale=0.18, s11_phase_offset=1.1) - store.save_set("calibration", radar_key, "smoke_cal", calibration_set) - store.save_set("reference", radar_key, "smoke_ref", reference_set) + store.save_set("s21_calibration", radar_key, "smoke_cal", s21_calibration_set) + store.save_set("s21_reference", radar_key, "smoke_ref", s21_reference_set) + store.save_set("s11_open", radar_key, "smoke_open", s11_open_set) + store.save_set("s11_short", radar_key, "smoke_short", s11_short_set) + store.save_set("s11_load", radar_key, "smoke_load", s11_load_set) + store.save_set("s11_reference", radar_key, "smoke_s11_ref", s11_reference_set) - calibration_bundle, reference_bundle = config_writer.prepare_s21_bundles( - store, - radar_key, - "smoke_cal", - "smoke_ref", - ) - config.preprocess.s21_calibration_set = "smoke_cal" - config.preprocess.s21_reference_set = "smoke_ref" - config.preprocess.s21_calibration_bundle_path = str(calibration_bundle) - config.preprocess.s21_reference_bundle_path = str(reference_bundle) + config.preprocess.s21.calibration.set_name = "smoke_cal" + config.preprocess.s21.reference.set_name = "smoke_ref" + config.preprocess.s11.calibration.open.set_name = "smoke_open" + config.preprocess.s11.calibration.short.set_name = "smoke_short" + config.preprocess.s11.calibration.load.set_name = "smoke_load" + config.preprocess.s11.reference.set_name = "smoke_s11_ref" + config_writer.prepare_preprocess_bundles(store, radar_key, config) config_path = config_writer.write(config, project_root / "python_app/runtime/run_config_smoke.json") diff --git a/python_app/storage/npz/serialize.py b/python_app/storage/npz/serialize.py index 5ee4df0..855f5a6 100644 --- a/python_app/storage/npz/serialize.py +++ b/python_app/storage/npz/serialize.py @@ -28,13 +28,12 @@ def serialize_trace_collection(collection: SweepCollection, magic: int) -> bytes for trace in collection.traces: freq = np.asarray(trace.frequency_hz, dtype=np.float32) + s11 = np.asarray(trace.s11, dtype=np.complex64) s21 = np.asarray(trace.s21, dtype=np.complex64) + if freq.size != s11.size: + raise ValueError("Trace frequency and S11 sizes must match") if freq.size != s21.size: raise ValueError("Trace frequency and S21 sizes must match") - # Python workflows still operate on S21 only. Emit a zero-filled S11 - # channel so C++ trace bundles keep the same wire format as runtime - # rings while the Python layer remains unchanged. - s11 = np.zeros(freq.size, dtype=np.complex64) buffer.extend(struct.pack(" for trace in collection.traces: tag = f"i{trace.combo.input_pos}_o{trace.combo.output_pos}" freq = np.asarray(trace.frequency_hz, dtype=np.float32) + s11 = np.asarray(trace.s11, dtype=np.complex64) s21 = np.asarray(trace.s21, dtype=np.complex64) np.save(collection_dir / f"{tag}_freq.npy", freq) + np.save(collection_dir / f"{tag}_s11.npy", s11) np.save(collection_dir / f"{tag}_s21.npy", s21) traces_meta.append( { @@ -166,6 +168,7 @@ def save_trace_history_numpy(stage_dir: Path, history: list[SweepCollection]) -> "output": int(trace.combo.output_pos), "points": int(freq.size), "freq_file": f"{tag}_freq.npy", + "s11_file": f"{tag}_s11.npy", "s21_file": f"{tag}_s21.npy", } ) diff --git a/python_app/storage/npz/store.py b/python_app/storage/npz/store.py index c614f07..b68b38e 100644 --- a/python_app/storage/npz/store.py +++ b/python_app/storage/npz/store.py @@ -24,7 +24,7 @@ from python_app.storage.store_api import StoreApi class NpzStore(StoreApi): - """Persist calibration/reference sets and runtime snapshots using NumPy files.""" + """Persist preprocess sets and runtime snapshots using NumPy files.""" def __init__(self, root_dir: Path) -> None: """Create store rooted at `root_dir`.""" @@ -32,7 +32,7 @@ class NpzStore(StoreApi): self._root_dir.mkdir(parents=True, exist_ok=True) def save_set(self, kind: str, radar_key: str, set_name: str, collection: SweepCollection) -> None: - """Persist named calibration/reference set as NPZ and metadata JSON.""" + """Persist named preprocess set as NPZ and metadata JSON.""" set_dir = self._set_dir(kind, radar_key) set_dir.mkdir(parents=True, exist_ok=True) @@ -45,14 +45,17 @@ class NpzStore(StoreApi): for trace in collection.traces: suffix = f"i{trace.combo.input_pos}_o{trace.combo.output_pos}" freq_key = f"freq_{suffix}" + s11_key = f"s11_{suffix}" s21_key = f"s21_{suffix}" payload[freq_key] = np.asarray(trace.frequency_hz, dtype=np.float32) + payload[s11_key] = np.asarray(trace.s11, dtype=np.complex64) payload[s21_key] = np.asarray(trace.s21, dtype=np.complex64) combo_records.append( { "input": trace.combo.input_pos, "output": trace.combo.output_pos, "freq_key": freq_key, + "s11_key": s11_key, "s21_key": s21_key, } ) @@ -66,7 +69,7 @@ class NpzStore(StoreApi): meta_path.write_text(json.dumps(meta, indent=2), encoding="utf-8") def load_set(self, kind: str, radar_key: str, set_name: str) -> SweepCollection: - """Load named calibration/reference set from NPZ representation.""" + """Load named preprocess set from NPZ representation.""" set_dir = self._set_dir(kind, radar_key) npz_path = set_dir / f"{set_name}.npz" meta_path = set_dir / f"{set_name}.json" @@ -80,11 +83,13 @@ class NpzStore(StoreApi): traces: list[TraceData] = [] for combo in meta["combos"]: freq = np.asarray(arrays[combo["freq_key"]], dtype=np.float32) + s11 = np.asarray(arrays[combo["s11_key"]], dtype=np.complex64) s21 = np.asarray(arrays[combo["s21_key"]], dtype=np.complex64) traces.append( TraceData( combo=ComboKey(input_pos=int(combo["input"]), output_pos=int(combo["output"])), frequency_hz=freq, + s11=s11, s21=s21, ) ) diff --git a/python_app/storage/store_api.py b/python_app/storage/store_api.py index e52d838..4fbd374 100644 --- a/python_app/storage/store_api.py +++ b/python_app/storage/store_api.py @@ -1,4 +1,4 @@ -"""Abstract storage API for calibration/reference sets.""" +"""Abstract storage API for preprocess sets.""" from __future__ import annotations diff --git a/python_app/workflows/calibration_workflow.py b/python_app/workflows/calibration_workflow.py index 0eaa3cf..3d00981 100644 --- a/python_app/workflows/calibration_workflow.py +++ b/python_app/workflows/calibration_workflow.py @@ -54,12 +54,13 @@ def capture_calibration_set( if config.runtime.settling_ms > 0: time.sleep(config.runtime.settling_ms / 1000.0) - frequency_hz, s21 = radar.acquire_s21() + sweep = radar.acquire() traces.append( TraceData( combo=ComboKey(input_pos=combo.input, output_pos=combo.output), - frequency_hz=frequency_hz, - s21=s21, + frequency_hz=sweep.x, + s11=sweep.trace("s11"), + s21=sweep.trace("s21"), ) ) finally: @@ -77,5 +78,5 @@ def capture_calibration_set( ifbw_hz=config.radar.sweep.if_bandwidth_hz, power_dbm=config.radar.sweep.power_dbm, ) - store.save_set("calibration", radar_key, set_name, collection) + store.save_set("s21_calibration", radar_key, set_name, collection) return radar_key, collection diff --git a/python_app/workflows/reference_workflow.py b/python_app/workflows/reference_workflow.py index 0915936..5002573 100644 --- a/python_app/workflows/reference_workflow.py +++ b/python_app/workflows/reference_workflow.py @@ -54,12 +54,13 @@ def capture_reference_set( if config.runtime.settling_ms > 0: time.sleep(config.runtime.settling_ms / 1000.0) - frequency_hz, s21 = radar.acquire_s21() + sweep = radar.acquire() traces.append( TraceData( combo=ComboKey(input_pos=combo.input, output_pos=combo.output), - frequency_hz=frequency_hz, - s21=s21, + frequency_hz=sweep.x, + s11=sweep.trace("s11"), + s21=sweep.trace("s21"), ) ) finally: @@ -77,5 +78,5 @@ def capture_reference_set( ifbw_hz=config.radar.sweep.if_bandwidth_hz, power_dbm=config.radar.sweep.power_dbm, ) - store.save_set("reference", radar_key, set_name, collection) + store.save_set("s21_reference", radar_key, set_name, collection) return radar_key, collection diff --git a/python_app/workflows/sequential_capture_workflow.py b/python_app/workflows/sequential_capture_workflow.py index 3910c72..525842c 100644 --- a/python_app/workflows/sequential_capture_workflow.py +++ b/python_app/workflows/sequential_capture_workflow.py @@ -1,4 +1,4 @@ -"""Sequential capture workflow for calibration/reference dataset creation.""" +"""Sequential capture workflow for preprocess asset dataset creation.""" from __future__ import annotations @@ -6,6 +6,8 @@ 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 @@ -28,8 +30,8 @@ class SequentialCaptureSession: """Manage hardware and switch stepping for full combo capture sequence.""" def __init__(self, config: RunConfigModel, kind: str, set_name: str) -> None: - """Create capture session for a calibration or reference set.""" - if kind not in {"calibration", "reference"}: + """Create capture session for one preprocess asset set.""" + 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") @@ -69,7 +71,7 @@ class SequentialCaptureSession: @property def kind(self) -> str: - """Return capture kind (`calibration` or `reference`).""" + """Return canonical preprocess asset key for this capture session.""" return self._kind @property @@ -125,11 +127,12 @@ class SequentialCaptureSession: if self._config.runtime.settling_ms > 0: time.sleep(self._config.runtime.settling_ms / 1000.0) - frequency_hz, s21 = self._radar.acquire_s21() + sweep = self._radar.acquire() trace = TraceData( combo=ComboKey(input_pos=combo.input, output_pos=combo.output), - frequency_hz=frequency_hz, - s21=s21, + 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), ) self._traces.append(trace) self._next_index += 1 diff --git a/run_config.json b/run_config.json index 0bf5510..752c74a 100644 --- a/run_config.json +++ b/run_config.json @@ -55,14 +55,36 @@ ] }, "preprocess": { - "s21_calibration_set": "", - "s21_reference_set": "", - "s21_calibration_bundle_path": "python_app/runtime/s21_calibration_bundle.bin", - "s21_reference_bundle_path": "python_app/runtime/s21_reference_bundle.bin", - "s11_open_calibration_bundle_path": "", - "s11_short_calibration_bundle_path": "", - "s11_load_calibration_bundle_path": "", - "s11_reference_bundle_path": "" + "s21": { + "calibration": { + "set_name": "", + "bundle_path": "" + }, + "reference": { + "set_name": "", + "bundle_path": "" + } + }, + "s11": { + "calibration": { + "open": { + "set_name": "", + "bundle_path": "" + }, + "short": { + "set_name": "", + "bundle_path": "" + }, + "load": { + "set_name": "", + "bundle_path": "" + } + }, + "reference": { + "set_name": "", + "bundle_path": "" + } + } }, "gpr": { "mode": "point",