From 9ddbde22bdbab2e0159319467a40b3b37fd4b332 Mon Sep 17 00:00:00 2001 From: Ayzen Date: Thu, 26 Mar 2026 17:21:25 +0300 Subject: [PATCH] added pass through s11 --- Makefile | 1 + .../common_cpp/config/include/run_config.hpp | 12 +- .../common_cpp/config/src/run_config.cpp | 16 +- .../include/calibration_master.hpp | 39 +- .../include/channel_bundle_support.hpp | 91 ++++ .../src/calibration_master.cpp | 121 ++---- .../src/channel_bundle_support.cpp | 405 ++++++++++++++++++ .../src/through_calibrator.cpp | 2 +- .../src/data_preprocessor.cpp | 4 +- .../data_preprocessor/src/main.cpp | 16 +- .../include/reference_master.hpp | 32 +- .../reference_master/src/reference_master.cpp | 219 ++++++---- .../testdata/s11_load_calibration_bundle.bin | Bin 0 -> 32280 bytes .../testdata/s11_open_calibration_bundle.bin | Bin 0 -> 32280 bytes .../testdata/s11_reference_bundle.bin | Bin 0 -> 32280 bytes .../testdata/s11_short_calibration_bundle.bin | Bin 0 -> 32280 bytes .../include/processing_live_config.hpp | 1 + .../src/processing_live_config.cpp | 13 + .../processors/src/passthrough_processor.cpp | 6 +- python_app/gui/app_window.py | 4 +- .../controllers/app_window_config_mixin.py | 5 +- .../controllers/app_window_pipeline_mixin.py | 16 +- .../gui/controllers/app_window_plot_mixin.py | 3 + .../app_window_preprocess_mixin.py | 22 +- .../gui/controllers/app_window_ui_mixin.py | 6 +- .../sections/data_actions_section.py | 34 +- .../sections/hardware_actions_section.py | 17 +- .../controllers/sections/pipeline_section.py | 14 +- .../sections/processing_section.py | 5 + python_app/gui/main.py | 2 +- python_app/gui/runtime/history.py | 4 +- python_app/models/run_config_codec.py | 59 ++- python_app/models/run_config_schema.py | 12 +- python_app/orchestration/config_writer.py | 14 +- .../orchestration/live_processing_config.py | 2 + python_app/runtime/run_config_smoke.json | 12 +- python_app/scripts/manual_smoke_run.py | 15 +- run_config.json | 12 +- 38 files changed, 945 insertions(+), 291 deletions(-) create mode 100644 data_acq_and_processing/preprocessing/calibration_master/include/channel_bundle_support.hpp create mode 100644 data_acq_and_processing/preprocessing/calibration_master/src/channel_bundle_support.cpp create mode 100644 data_acq_and_processing/preprocessing/testdata/s11_load_calibration_bundle.bin create mode 100644 data_acq_and_processing/preprocessing/testdata/s11_open_calibration_bundle.bin create mode 100644 data_acq_and_processing/preprocessing/testdata/s11_reference_bundle.bin create mode 100644 data_acq_and_processing/preprocessing/testdata/s11_short_calibration_bundle.bin diff --git a/Makefile b/Makefile index 21cdd78..a208742 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,7 @@ ORCH_SOURCES := \ PREPROC_SOURCES := \ data_acq_and_processing/preprocessing/calibration_master/src/calibration_master.cpp \ + data_acq_and_processing/preprocessing/calibration_master/src/channel_bundle_support.cpp \ data_acq_and_processing/preprocessing/calibration_master/src/through_calibrator.cpp \ data_acq_and_processing/preprocessing/reference_master/src/reference_master.cpp \ data_acq_and_processing/preprocessing/data_preprocessor/src/data_preprocessor.cpp \ 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 eea690c..b5a4af7 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 @@ -75,10 +75,14 @@ struct RuntimeConfig { struct PreprocessConfig { // Names and bundle paths selected by Python GUI layer. - std::string calibration_set{}; - std::string reference_set{}; - std::string calibration_bundle_path{}; - std::string reference_bundle_path{}; + 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{}; }; 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 1cec13d..7d389fe 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 @@ -438,10 +438,18 @@ auto load_run_config(const std::string& path) -> RunConfig { { const auto* preprocess_obj = as_object(required_field(*root_obj, "preprocess"), "preprocess"); - config.preprocess.calibration_set = optional_string(*preprocess_obj, "calibration_set", ""); - config.preprocess.reference_set = optional_string(*preprocess_obj, "reference_set", ""); - config.preprocess.calibration_bundle_path = optional_string(*preprocess_obj, "calibration_bundle_path", ""); - config.preprocess.reference_bundle_path = optional_string(*preprocess_obj, "reference_bundle_path", ""); + 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", ""); } if (const auto* gpr_value = optional_field(*root_obj, "gpr"); gpr_value != nullptr) { diff --git a/data_acq_and_processing/preprocessing/calibration_master/include/calibration_master.hpp b/data_acq_and_processing/preprocessing/calibration_master/include/calibration_master.hpp index a0489fc..5508d1a 100644 --- a/data_acq_and_processing/preprocessing/calibration_master/include/calibration_master.hpp +++ b/data_acq_and_processing/preprocessing/calibration_master/include/calibration_master.hpp @@ -1,32 +1,51 @@ #pragma once #include +#include #include -#include #include #include "calibrator_interface.hpp" +#include "channel_bundle_support.hpp" #include "shared_types.hpp" namespace radar::preprocessing { class CalibrationMaster { public: - // `calibrator` encapsulates the actual calibration algorithm (v1: through). - explicit CalibrationMaster(std::unique_ptr calibrator); + // `s21_calibrator` encapsulates the S21 calibration algorithm. + explicit CalibrationMaster(std::unique_ptr s21_calibrator); - // Loads a serialized raw sweep bundle with one calibration standard per combo. - void load_bundle(const std::string& path); + // Loads a serialized raw sweep bundle with one S21 calibration standard per combo. + void load_s21_calibration_bundle(const std::string& path); + // Loads optional one-port OSL calibration data for S11 from raw sweep bundles. + void load_s11_calibration_bundle( + const std::string& open_path, + const std::string& short_path, + const std::string& load_path + ); // Ensures all runtime combos are present in loaded standards. void validate_combos(const std::vector& combos) const; - // Applies calibration standard corresponding to measured trace combo. - [[nodiscard]] auto apply(const ipc::SweepTraceBlock& measured_trace) const -> ipc::SweepTraceBlock; + // Applies both channel calibrations to one measured trace. + [[nodiscard]] auto apply_to_trace(const ipc::SweepTraceBlock& measured_trace) const -> ipc::SweepTraceBlock; + [[nodiscard]] auto apply_s21( + const ipc::ComboKey& combo, + std::span frequency_hz, + const std::vector& measured_s21 + ) const -> std::vector; + [[nodiscard]] auto apply_s11( + const ipc::ComboKey& combo, + std::span frequency_hz, + const std::vector& measured_s11 + ) const -> std::vector; + [[nodiscard]] auto has_s11_calibration() const noexcept -> bool; private: - std::unique_ptr calibrator_impl_; - std::unordered_map standards_by_combo_{}; + std::unique_ptr s21_calibrator_; + S21CalibrationBundle s21_calibration_bundle_{}; + S11CalibrationBundle s11_calibration_bundle_{}; }; -[[nodiscard]] auto make_through_calibrator() -> std::unique_ptr; +[[nodiscard]] auto make_s21_through_calibrator() -> std::unique_ptr; } // namespace radar::preprocessing diff --git a/data_acq_and_processing/preprocessing/calibration_master/include/channel_bundle_support.hpp b/data_acq_and_processing/preprocessing/calibration_master/include/channel_bundle_support.hpp new file mode 100644 index 0000000..4a85258 --- /dev/null +++ b/data_acq_and_processing/preprocessing/calibration_master/include/channel_bundle_support.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include + +#include "shared_types.hpp" + +namespace radar::preprocessing { + +class CalibratorInterface; + +struct ChannelTrace { + std::vector frequency_hz{}; + std::vector samples{}; +}; + +class S21CalibrationBundle { + public: + void load(const std::string& path); + + [[nodiscard]] auto is_enabled() const noexcept -> bool; + void validate_combos(const std::vector& combos) const; + [[nodiscard]] auto apply( + const ipc::ComboKey& combo, + std::span frequency_hz, + const std::vector& measured, + const CalibratorInterface& calibrator + ) const -> std::vector; + + private: + [[nodiscard]] auto resolve(const ipc::ComboKey& combo) const -> const ChannelTrace&; + + std::unordered_map traces_by_combo_{}; +}; + +class S21ReferenceBundle { + public: + void load(const std::string& path); + + [[nodiscard]] auto is_enabled() const noexcept -> bool; + void validate_combos(const std::vector& combos) const; + [[nodiscard]] auto resolve(const ipc::ComboKey& combo) const -> const ChannelTrace&; + + private: + std::unordered_map traces_by_combo_{}; +}; + +class S11CalibrationBundle { + public: + struct Coefficients { + std::vector frequency_hz{}; + std::vector directivity{}; + std::vector source_match{}; + std::vector reflection_tracking{}; + }; + + void load( + const std::string& open_path, + const std::string& short_path, + const std::string& load_path + ); + + [[nodiscard]] auto is_enabled() const noexcept -> bool; + void validate_combos(const std::vector& combos) const; + [[nodiscard]] auto apply( + const ipc::ComboKey& combo, + std::span frequency_hz, + const std::vector& measured + ) const -> std::vector; + + private: + [[nodiscard]] auto resolve(const ipc::ComboKey& combo) const -> const Coefficients&; + + std::unordered_map coefficients_by_combo_{}; +}; + +class S11ReferenceBundle { + public: + void load(const std::string& path); + + [[nodiscard]] auto is_enabled() const noexcept -> bool; + void validate_combos(const std::vector& combos) const; + [[nodiscard]] auto resolve(const ipc::ComboKey& combo) const -> const ChannelTrace&; + + private: + std::unordered_map traces_by_combo_{}; +}; + +} // namespace radar::preprocessing diff --git a/data_acq_and_processing/preprocessing/calibration_master/src/calibration_master.cpp b/data_acq_and_processing/preprocessing/calibration_master/src/calibration_master.cpp index 126f61c..f9ae484 100644 --- a/data_acq_and_processing/preprocessing/calibration_master/src/calibration_master.cpp +++ b/data_acq_and_processing/preprocessing/calibration_master/src/calibration_master.cpp @@ -1,107 +1,60 @@ #include "calibration_master.hpp" -#include -#include -#include #include -#include -#include namespace radar::preprocessing { -namespace { -[[nodiscard]] auto combo_to_string(const ipc::ComboKey& combo) -> std::string { - return "input=" + std::to_string(combo.input_pos) + " output=" + std::to_string(combo.output_pos); -} - -[[nodiscard]] auto read_binary_file(const std::string& path, const std::string& bundle_label) - -> std::vector { - std::ifstream stream(path, std::ios::binary); - if (!stream.is_open()) { - throw std::runtime_error("Failed to open " + bundle_label + " bundle: " + path); - } - - return std::vector(std::istreambuf_iterator(stream), std::istreambuf_iterator()); -} - -void validate_trace_layout(const ipc::SweepTraceBlock& trace, const std::string& trace_label) { - if (trace.frequency_hz.size() != trace.s21.size()) { - throw std::runtime_error( - trace_label + " frequency/complex vector size mismatch for combo " + combo_to_string(trace.combo) - ); - } - if (trace.frequency_hz.size() != trace.s11.size()) { - throw std::runtime_error( - trace_label + " frequency/S11 vector size mismatch for combo " + combo_to_string(trace.combo) - ); - } -} - -} // namespace - -CalibrationMaster::CalibrationMaster(std::unique_ptr calibrator) - : calibrator_impl_(std::move(calibrator)) { - if (!calibrator_impl_) { +CalibrationMaster::CalibrationMaster(std::unique_ptr s21_calibrator) + : s21_calibrator_(std::move(s21_calibrator)) { + if (!s21_calibrator_) { throw std::runtime_error("CalibrationMaster requires a non-null calibrator"); } } -void CalibrationMaster::load_bundle(const std::string& path) { - if (path.empty()) { - throw std::runtime_error("Calibration bundle path must not be empty"); - } +void CalibrationMaster::load_s21_calibration_bundle(const std::string& path) { + s21_calibration_bundle_.load(path); +} - const auto bytes = read_binary_file(path, "calibration"); - if (bytes.empty()) { - throw std::runtime_error("Calibration bundle is empty: " + path); - } - - const auto collection = ipc::deserialize_raw_collection(bytes); - standards_by_combo_.clear(); - standards_by_combo_.reserve(collection.traces.size()); - for (const auto& trace : collection.traces) { - validate_trace_layout(trace, "Calibration standard"); - standards_by_combo_.insert_or_assign(trace.combo, trace); - } - - if (standards_by_combo_.empty()) { - throw std::runtime_error("Calibration bundle does not contain traces: " + path); - } +void CalibrationMaster::load_s11_calibration_bundle( + const std::string& open_path, + const std::string& short_path, + const std::string& load_path +) { + s11_calibration_bundle_.load(open_path, short_path, load_path); } void CalibrationMaster::validate_combos(const std::vector& combos) const { - for (const auto& combo : combos) { - if (!standards_by_combo_.contains(combo)) { - throw std::runtime_error("Calibration data is missing for combo " + combo_to_string(combo)); - } - } + s21_calibration_bundle_.validate_combos(combos); + s11_calibration_bundle_.validate_combos(combos); } -auto CalibrationMaster::apply(const ipc::SweepTraceBlock& measured_trace) const -> ipc::SweepTraceBlock { - validate_trace_layout(measured_trace, "Measured trace"); - - const auto found = standards_by_combo_.find(measured_trace.combo); - if (found == standards_by_combo_.end()) { - throw std::runtime_error( - "Calibration standard is missing for measured combo " + combo_to_string(measured_trace.combo) - ); - } - - const auto& standard = found->second; - validate_trace_layout(standard, "Calibration standard"); - if (measured_trace.s21.size() != standard.s21.size()) { - throw std::runtime_error( - "Calibration standard point count mismatch for combo " + combo_to_string(measured_trace.combo) - ); - } - +auto CalibrationMaster::apply_to_trace(const ipc::SweepTraceBlock& measured_trace) const -> ipc::SweepTraceBlock { ipc::SweepTraceBlock output{}; output.combo = measured_trace.combo; output.frequency_hz = measured_trace.frequency_hz; - output.s11 = measured_trace.s11; - output.s21 = calibrator_impl_->apply(measured_trace.s21, standard.s21); - + output.s21 = apply_s21(measured_trace.combo, measured_trace.frequency_hz, measured_trace.s21); + output.s11 = apply_s11(measured_trace.combo, measured_trace.frequency_hz, measured_trace.s11); return output; } +auto CalibrationMaster::apply_s21( + const ipc::ComboKey& combo, + std::span frequency_hz, + const std::vector& measured_s21 +) const -> std::vector { + return s21_calibration_bundle_.apply(combo, frequency_hz, measured_s21, *s21_calibrator_); +} + +auto CalibrationMaster::apply_s11( + const ipc::ComboKey& combo, + std::span frequency_hz, + const std::vector& measured_s11 +) const -> std::vector { + return s11_calibration_bundle_.apply(combo, frequency_hz, measured_s11); +} + +auto CalibrationMaster::has_s11_calibration() const noexcept -> bool { + return s11_calibration_bundle_.is_enabled(); +} + } // namespace radar::preprocessing diff --git a/data_acq_and_processing/preprocessing/calibration_master/src/channel_bundle_support.cpp b/data_acq_and_processing/preprocessing/calibration_master/src/channel_bundle_support.cpp new file mode 100644 index 0000000..631a17e --- /dev/null +++ b/data_acq_and_processing/preprocessing/calibration_master/src/channel_bundle_support.cpp @@ -0,0 +1,405 @@ +#include "channel_bundle_support.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "calibrator_interface.hpp" + +namespace radar::preprocessing { +namespace { + +enum class RawTraceChannel { + S11, + S21, +}; + +constexpr float kFrequencyRelativeTolerance = 1e-5F; +constexpr float kFrequencyAbsoluteTolerance = 1e-3F; +constexpr float kComplexMagnitudeSquaredEpsilon = 1e-12F; + +[[nodiscard]] auto combo_to_string(const ipc::ComboKey& combo) -> std::string { + return "input=" + std::to_string(combo.input_pos) + " output=" + std::to_string(combo.output_pos); +} + +[[nodiscard]] auto read_binary_file(const std::string& path, const std::string& bundle_label) + -> std::vector { + std::ifstream stream(path, std::ios::binary); + if (!stream.is_open()) { + throw std::runtime_error("Failed to open " + bundle_label + " bundle: " + path); + } + + return std::vector(std::istreambuf_iterator(stream), std::istreambuf_iterator()); +} + +void validate_raw_trace_layout(const ipc::SweepTraceBlock& trace, const std::string& trace_label) { + if (trace.frequency_hz.size() != trace.s11.size()) { + throw std::runtime_error( + trace_label + " frequency/S11 vector size mismatch for combo " + combo_to_string(trace.combo) + ); + } + if (trace.frequency_hz.size() != trace.s21.size()) { + throw std::runtime_error( + trace_label + " frequency/S21 vector size mismatch for combo " + combo_to_string(trace.combo) + ); + } +} + +void validate_channel_trace_layout( + const ChannelTrace& trace, + const std::string& trace_label, + const ipc::ComboKey& combo +) { + if (trace.frequency_hz.size() != trace.samples.size()) { + throw std::runtime_error( + trace_label + " frequency/channel vector size mismatch for combo " + combo_to_string(combo) + ); + } +} + +[[nodiscard]] auto frequency_axes_match(std::span left, std::span right) -> bool { + if (left.size() != right.size()) { + return false; + } + + for (std::size_t index = 0; index < left.size(); ++index) { + const auto delta = std::fabs(left[index] - right[index]); + const auto scale = std::max(std::fabs(left[index]), std::fabs(right[index])); + const auto tolerance = std::max(kFrequencyAbsoluteTolerance, scale * kFrequencyRelativeTolerance); + if (delta > tolerance) { + return false; + } + } + return true; +} + +void ensure_frequency_axis_match( + std::span expected, + std::span actual, + const std::string& label +) { + if (!frequency_axes_match(expected, actual)) { + throw std::runtime_error(label + " frequency axis mismatch"); + } +} + +[[nodiscard]] auto to_std_complex(const ipc::Complex32& value) -> std::complex { + return std::complex(value.re, value.im); +} + +[[nodiscard]] auto from_std_complex(const std::complex& value) -> ipc::Complex32 { + return ipc::Complex32{ + .re = value.real(), + .im = value.imag(), + }; +} + +[[nodiscard]] auto pick_channel_samples(const ipc::SweepTraceBlock& trace, RawTraceChannel channel) + -> const std::vector& { + if (channel == RawTraceChannel::S11) { + return trace.s11; + } + return trace.s21; +} + +[[nodiscard]] auto to_channel_trace( + const ipc::SweepTraceBlock& trace, + RawTraceChannel channel, + const std::string& bundle_label +) -> ChannelTrace { + validate_raw_trace_layout(trace, bundle_label + " trace"); + + ChannelTrace channel_trace{}; + channel_trace.frequency_hz = trace.frequency_hz; + channel_trace.samples = pick_channel_samples(trace, channel); + validate_channel_trace_layout(channel_trace, bundle_label + " trace", trace.combo); + return channel_trace; +} + +[[nodiscard]] auto load_channel_traces( + const std::string& path, + const std::string& bundle_label, + RawTraceChannel channel +) -> std::unordered_map { + const auto bytes = read_binary_file(path, bundle_label); + if (bytes.empty()) { + throw std::runtime_error(bundle_label + " bundle is empty: " + path); + } + + const auto collection = ipc::deserialize_raw_collection(bytes); + std::unordered_map traces_by_combo{}; + traces_by_combo.reserve(collection.traces.size()); + for (const auto& trace : collection.traces) { + traces_by_combo.insert_or_assign(trace.combo, to_channel_trace(trace, channel, bundle_label)); + } + + if (traces_by_combo.empty()) { + throw std::runtime_error(bundle_label + " bundle does not contain traces: " + path); + } + return traces_by_combo; +} + +void validate_bundle_combos( + const std::unordered_map& traces_by_combo, + const std::vector& combos, + const std::string& bundle_label +) { + if (traces_by_combo.empty()) { + return; + } + + for (const auto& combo : combos) { + if (!traces_by_combo.contains(combo)) { + throw std::runtime_error(bundle_label + " is missing combo " + combo_to_string(combo)); + } + } +} + +template +auto resolve_required( + const std::unordered_map& values_by_combo, + const ipc::ComboKey& combo, + const std::string& value_label +) -> const TValue& { + const auto found = values_by_combo.find(combo); + if (found == values_by_combo.end()) { + throw std::runtime_error(value_label + " is missing for combo " + combo_to_string(combo)); + } + return found->second; +} + +void ensure_combo_coverage( + const std::unordered_map& expected, + const std::unordered_map& actual, + const std::string& expected_label, + const std::string& actual_label +) { + for (const auto& entry : expected) { + const auto& combo = entry.first; + if (!actual.contains(combo)) { + throw std::runtime_error( + actual_label + " is missing combo " + combo_to_string(combo) + + " required by " + expected_label + ); + } + } +} + +[[nodiscard]] auto solve_osl_coefficients( + const ipc::ComboKey& combo, + const ChannelTrace& open_trace, + const ChannelTrace& short_trace, + const ChannelTrace& load_trace +) -> S11CalibrationBundle::Coefficients { + validate_channel_trace_layout(open_trace, "S11 open calibration", combo); + validate_channel_trace_layout(short_trace, "S11 short calibration", combo); + validate_channel_trace_layout(load_trace, "S11 load calibration", combo); + ensure_frequency_axis_match(open_trace.frequency_hz, short_trace.frequency_hz, "S11 short calibration"); + ensure_frequency_axis_match(open_trace.frequency_hz, load_trace.frequency_hz, "S11 load calibration"); + + S11CalibrationBundle::Coefficients coefficients{}; + coefficients.frequency_hz = open_trace.frequency_hz; + coefficients.directivity.resize(open_trace.frequency_hz.size()); + coefficients.source_match.resize(open_trace.frequency_hz.size()); + coefficients.reflection_tracking.resize(open_trace.frequency_hz.size()); + + for (std::size_t index = 0; index < open_trace.frequency_hz.size(); ++index) { + const auto load = to_std_complex(load_trace.samples[index]); + const auto open_delta = to_std_complex(open_trace.samples[index]) - load; + const auto short_delta = to_std_complex(short_trace.samples[index]) - load; + const auto denominator = open_delta - short_delta; + + std::complex source_match = std::complex(0.0F, 0.0F); + std::complex reflection_tracking = std::complex(1.0F, 0.0F); + if (std::norm(denominator) > kComplexMagnitudeSquaredEpsilon) { + source_match = (open_delta + short_delta) / denominator; + reflection_tracking = open_delta * (std::complex(1.0F, 0.0F) - source_match); + } + + coefficients.directivity[index] = load_trace.samples[index]; + coefficients.source_match[index] = from_std_complex(source_match); + coefficients.reflection_tracking[index] = from_std_complex(reflection_tracking); + } + + return coefficients; +} + +} // namespace + +void S21CalibrationBundle::load(const std::string& path) { + traces_by_combo_.clear(); + + if (path.empty()) { + throw std::runtime_error("S21 calibration bundle path must not be empty"); + } + + traces_by_combo_ = load_channel_traces(path, "S21 calibration", RawTraceChannel::S21); +} + +auto S21CalibrationBundle::is_enabled() const noexcept -> bool { + return !traces_by_combo_.empty(); +} + +void S21CalibrationBundle::validate_combos(const std::vector& combos) const { + validate_bundle_combos(traces_by_combo_, combos, "S21 calibration bundle"); +} + +auto S21CalibrationBundle::apply( + const ipc::ComboKey& combo, + std::span frequency_hz, + const std::vector& measured, + const CalibratorInterface& calibrator +) const -> std::vector { + const auto& calibration_trace = resolve(combo); + ensure_frequency_axis_match(calibration_trace.frequency_hz, frequency_hz, "S21 calibration"); + if (measured.size() != calibration_trace.samples.size()) { + throw std::runtime_error("S21 calibration vector size mismatch"); + } + return calibrator.apply(measured, calibration_trace.samples); +} + +auto S21CalibrationBundle::resolve(const ipc::ComboKey& combo) const -> const ChannelTrace& { + return resolve_required(traces_by_combo_, combo, "S21 calibration trace"); +} + +void S21ReferenceBundle::load(const std::string& path) { + traces_by_combo_.clear(); + + if (path.empty()) { + throw std::runtime_error("S21 reference bundle path must not be empty"); + } + + traces_by_combo_ = load_channel_traces(path, "S21 reference", RawTraceChannel::S21); +} + +auto S21ReferenceBundle::is_enabled() const noexcept -> bool { + return !traces_by_combo_.empty(); +} + +void S21ReferenceBundle::validate_combos(const std::vector& combos) const { + validate_bundle_combos(traces_by_combo_, combos, "S21 reference bundle"); +} + +auto S21ReferenceBundle::resolve(const ipc::ComboKey& combo) const -> const ChannelTrace& { + return resolve_required(traces_by_combo_, combo, "S21 reference trace"); +} + +void S11CalibrationBundle::load( + const std::string& open_path, + const std::string& short_path, + const std::string& load_path +) { + coefficients_by_combo_.clear(); + + if (open_path.empty() && short_path.empty() && load_path.empty()) { + return; + } + if (open_path.empty() || short_path.empty() || load_path.empty()) { + throw std::runtime_error("S11 calibration requires open, short, and load raw bundle paths"); + } + + const auto open_traces = load_channel_traces(open_path, "S11 open calibration", RawTraceChannel::S11); + const auto short_traces = load_channel_traces(short_path, "S11 short calibration", RawTraceChannel::S11); + const auto load_traces = load_channel_traces(load_path, "S11 load calibration", RawTraceChannel::S11); + + ensure_combo_coverage(open_traces, short_traces, "S11 open calibration", "S11 short calibration"); + ensure_combo_coverage(open_traces, load_traces, "S11 open calibration", "S11 load calibration"); + ensure_combo_coverage(short_traces, open_traces, "S11 short calibration", "S11 open calibration"); + ensure_combo_coverage(load_traces, open_traces, "S11 load calibration", "S11 open calibration"); + + coefficients_by_combo_.reserve(open_traces.size()); + for (const auto& [combo, open_trace] : open_traces) { + coefficients_by_combo_.insert_or_assign( + combo, + solve_osl_coefficients(combo, open_trace, short_traces.at(combo), load_traces.at(combo)) + ); + } +} + +auto S11CalibrationBundle::is_enabled() const noexcept -> bool { + return !coefficients_by_combo_.empty(); +} + +void S11CalibrationBundle::validate_combos(const std::vector& combos) const { + if (!is_enabled()) { + return; + } + + for (const auto& combo : combos) { + if (!coefficients_by_combo_.contains(combo)) { + throw std::runtime_error("S11 calibration data is missing for combo " + combo_to_string(combo)); + } + } +} + +auto S11CalibrationBundle::apply( + const ipc::ComboKey& combo, + std::span frequency_hz, + const std::vector& measured +) const -> std::vector { + if (!is_enabled()) { + return measured; + } + if (frequency_hz.size() != measured.size()) { + throw std::runtime_error("S11 calibration input frequency/sample size mismatch"); + } + + const auto& coefficients = resolve(combo); + ensure_frequency_axis_match(coefficients.frequency_hz, frequency_hz, "S11 calibration"); + if (measured.size() != coefficients.directivity.size()) { + throw std::runtime_error("S11 calibration vector size mismatch"); + } + + std::vector corrected{}; + corrected.reserve(measured.size()); + for (std::size_t index = 0; index < measured.size(); ++index) { + const auto numerator = to_std_complex(measured[index]) - to_std_complex(coefficients.directivity[index]); + const auto denominator = + to_std_complex(coefficients.reflection_tracking[index]) + + (to_std_complex(coefficients.source_match[index]) * numerator); + + if (std::norm(denominator) > kComplexMagnitudeSquaredEpsilon) { + corrected.push_back(from_std_complex(numerator / denominator)); + } else { + corrected.push_back(from_std_complex(numerator)); + } + } + return corrected; +} + +auto S11CalibrationBundle::resolve(const ipc::ComboKey& combo) const -> const Coefficients& { + return resolve_required(coefficients_by_combo_, combo, "S11 calibration coefficients"); +} + +void S11ReferenceBundle::load(const std::string& path) { + traces_by_combo_.clear(); + + if (path.empty()) { + return; + } + + traces_by_combo_ = load_channel_traces(path, "S11 reference", RawTraceChannel::S11); +} + +auto S11ReferenceBundle::is_enabled() const noexcept -> bool { + return !traces_by_combo_.empty(); +} + +void S11ReferenceBundle::validate_combos(const std::vector& combos) const { + validate_bundle_combos(traces_by_combo_, combos, "S11 reference bundle"); +} + +auto S11ReferenceBundle::resolve(const ipc::ComboKey& combo) const -> const ChannelTrace& { + return resolve_required(traces_by_combo_, combo, "S11 reference trace"); +} + +} // namespace radar::preprocessing diff --git a/data_acq_and_processing/preprocessing/calibration_master/src/through_calibrator.cpp b/data_acq_and_processing/preprocessing/calibration_master/src/through_calibrator.cpp index 5aff8cd..a146397 100644 --- a/data_acq_and_processing/preprocessing/calibration_master/src/through_calibrator.cpp +++ b/data_acq_and_processing/preprocessing/calibration_master/src/through_calibrator.cpp @@ -69,7 +69,7 @@ class ThroughCalibrator final : public CalibratorInterface { } // namespace -auto make_through_calibrator() -> std::unique_ptr { +auto make_s21_through_calibrator() -> std::unique_ptr { return std::make_unique(); } diff --git a/data_acq_and_processing/preprocessing/data_preprocessor/src/data_preprocessor.cpp b/data_acq_and_processing/preprocessing/data_preprocessor/src/data_preprocessor.cpp index 3729de4..9b2c05a 100644 --- a/data_acq_and_processing/preprocessing/data_preprocessor/src/data_preprocessor.cpp +++ b/data_acq_and_processing/preprocessing/data_preprocessor/src/data_preprocessor.cpp @@ -74,8 +74,8 @@ auto DataPreprocessor::preprocess_collection(const ipc::RawSweepCollection& raw_ for (const auto& raw_trace : raw_collection.traces) { // Pipeline order is fixed: calibration first, then reference subtraction. - const auto calibrated = calibration_master_.apply(raw_trace); - const auto referenced = reference_master_.apply(calibrated); + const auto calibrated = calibration_master_.apply_to_trace(raw_trace); + const auto referenced = reference_master_.apply_to_trace(calibrated); preprocessed.traces.push_back(referenced); } 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 48f671a..db4745a 100644 --- a/data_acq_and_processing/preprocessing/data_preprocessor/src/main.cpp +++ b/data_acq_and_processing/preprocessing/data_preprocessor/src/main.cpp @@ -62,12 +62,20 @@ int main(int argc, char** argv) { ); // Load preprocessing assets once before entering run loop. - radar::preprocessing::CalibrationMaster calibration_master(radar::preprocessing::make_through_calibrator()); - calibration_master.load_bundle(config.preprocess.calibration_bundle_path); + 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_s11_calibration_bundle( + config.preprocess.s11_open_calibration_bundle_path, + config.preprocess.s11_short_calibration_bundle_path, + config.preprocess.s11_load_calibration_bundle_path + ); radar::preprocessing::ReferenceMaster reference_master; - reference_master.load_bundle(config.preprocess.reference_bundle_path); - reference_master.prepare_calibrated(calibration_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.prepare_calibrated(calibration_master, config.run_combos); radar::preprocessing::DataPreprocessor preprocessor( config, diff --git a/data_acq_and_processing/preprocessing/reference_master/include/reference_master.hpp b/data_acq_and_processing/preprocessing/reference_master/include/reference_master.hpp index cb23ca1..2d3ae70 100644 --- a/data_acq_and_processing/preprocessing/reference_master/include/reference_master.hpp +++ b/data_acq_and_processing/preprocessing/reference_master/include/reference_master.hpp @@ -1,9 +1,11 @@ #pragma once +#include #include #include #include +#include "channel_bundle_support.hpp" #include "shared_types.hpp" namespace radar::preprocessing { @@ -12,19 +14,33 @@ class CalibrationMaster; class ReferenceMaster { public: - // Loads a serialized raw sweep bundle with one reference trace per combo. - void load_bundle(const std::string& path); + // Loads a serialized raw sweep bundle with one S21 reference trace per combo. + void load_s21_reference_bundle(const std::string& path); + // Loads optional raw S11 reference data. + void load_s11_reference_bundle(const std::string& path); // Builds calibrated references in memory using currently loaded raw references. - // Must be called after load_bundle() and after calibration standards are loaded. - void prepare_calibrated(const CalibrationMaster& calibration_master); + // Must be called after load_s21_reference_bundle() and after calibration standards are loaded. + void prepare_calibrated(const CalibrationMaster& calibration_master, const std::vector& combos); // Ensures all runtime combos are present in loaded references. void validate_combos(const std::vector& combos) const; - // Subtracts per-combo reference trace from calibrated trace. - [[nodiscard]] auto apply(const ipc::SweepTraceBlock& calibrated_trace) const -> ipc::SweepTraceBlock; + // Subtracts both channel references from one calibrated trace. + [[nodiscard]] auto apply_to_trace(const ipc::SweepTraceBlock& calibrated_trace) const -> ipc::SweepTraceBlock; + [[nodiscard]] auto apply_s21( + const ipc::ComboKey& combo, + std::span frequency_hz, + const std::vector& calibrated_s21 + ) const -> std::vector; + [[nodiscard]] auto apply_s11( + const ipc::ComboKey& combo, + std::span frequency_hz, + const std::vector& calibrated_s11 + ) const -> std::vector; private: - std::unordered_map raw_references_by_combo_{}; - std::unordered_map calibrated_references_by_combo_{}; + S21ReferenceBundle raw_s21_reference_bundle_{}; + std::unordered_map calibrated_s21_references_by_combo_{}; + S11ReferenceBundle raw_s11_reference_bundle_{}; + std::unordered_map calibrated_s11_references_by_combo_{}; }; } // namespace radar::preprocessing diff --git a/data_acq_and_processing/preprocessing/reference_master/src/reference_master.cpp b/data_acq_and_processing/preprocessing/reference_master/src/reference_master.cpp index 53d487d..afea007 100644 --- a/data_acq_and_processing/preprocessing/reference_master/src/reference_master.cpp +++ b/data_acq_and_processing/preprocessing/reference_master/src/reference_master.cpp @@ -1,8 +1,8 @@ #include "reference_master.hpp" -#include -#include -#include +#include +#include +#include #include #include #include @@ -15,132 +15,195 @@ namespace radar::preprocessing { namespace { +constexpr float kFrequencyRelativeTolerance = 1e-5F; +constexpr float kFrequencyAbsoluteTolerance = 1e-3F; + [[nodiscard]] auto combo_to_string(const ipc::ComboKey& combo) -> std::string { return "input=" + std::to_string(combo.input_pos) + " output=" + std::to_string(combo.output_pos); } -[[nodiscard]] auto read_binary_file(const std::string& path, const std::string& bundle_label) - -> std::vector { - std::ifstream stream(path, std::ios::binary); - if (!stream.is_open()) { - throw std::runtime_error("Failed to open " + bundle_label + " bundle: " + path); +void validate_channel_trace_layout( + const ChannelTrace& trace, + const std::string& trace_label, + const ipc::ComboKey& combo +) { + if (trace.frequency_hz.size() != trace.samples.size()) { + throw std::runtime_error( + trace_label + " frequency/channel vector size mismatch for combo " + combo_to_string(combo) + ); } - - return std::vector(std::istreambuf_iterator(stream), std::istreambuf_iterator()); } -void validate_trace_layout(const ipc::SweepTraceBlock& trace, const std::string& trace_label) { - if (trace.frequency_hz.size() != trace.s21.size()) { - throw std::runtime_error( - trace_label + " frequency/complex vector size mismatch for combo " + combo_to_string(trace.combo) - ); +[[nodiscard]] auto frequency_axes_match(std::span left, std::span right) -> bool { + if (left.size() != right.size()) { + return false; } - if (trace.frequency_hz.size() != trace.s11.size()) { - throw std::runtime_error( - trace_label + " frequency/S11 vector size mismatch for combo " + combo_to_string(trace.combo) - ); + + for (std::size_t index = 0; index < left.size(); ++index) { + const auto delta = std::fabs(left[index] - right[index]); + const auto scale = std::max(std::fabs(left[index]), std::fabs(right[index])); + const auto tolerance = std::max(kFrequencyAbsoluteTolerance, scale * kFrequencyRelativeTolerance); + if (delta > tolerance) { + return false; + } } + return true; } } // namespace -void ReferenceMaster::load_bundle(const std::string& path) { - if (path.empty()) { - throw std::runtime_error("Reference bundle path must not be empty"); - } - - const auto bytes = read_binary_file(path, "reference"); - if (bytes.empty()) { - throw std::runtime_error("Reference bundle is empty: " + path); - } - - const auto collection = ipc::deserialize_raw_collection(bytes); - raw_references_by_combo_.clear(); - raw_references_by_combo_.reserve(collection.traces.size()); - calibrated_references_by_combo_.clear(); - for (const auto& trace : collection.traces) { - validate_trace_layout(trace, "Reference trace"); - raw_references_by_combo_.insert_or_assign(trace.combo, trace); - } - - if (raw_references_by_combo_.empty()) { - throw std::runtime_error("Reference bundle does not contain traces: " + path); - } +void ReferenceMaster::load_s21_reference_bundle(const std::string& path) { + raw_s21_reference_bundle_.load(path); + calibrated_s21_references_by_combo_.clear(); } -void ReferenceMaster::prepare_calibrated(const CalibrationMaster& calibration_master) { - if (raw_references_by_combo_.empty()) { +void ReferenceMaster::load_s11_reference_bundle(const std::string& path) { + raw_s11_reference_bundle_.load(path); + calibrated_s11_references_by_combo_.clear(); +} + +void ReferenceMaster::prepare_calibrated( + const CalibrationMaster& calibration_master, + const std::vector& combos +) { + if (!raw_s21_reference_bundle_.is_enabled()) { throw std::runtime_error("Reference bundle must be loaded before prepare_calibrated()"); } - calibrated_references_by_combo_.clear(); - calibrated_references_by_combo_.reserve(raw_references_by_combo_.size()); - for (const auto& [combo, raw_reference] : raw_references_by_combo_) { - auto calibrated = calibration_master.apply(raw_reference); - calibrated.combo = combo; - calibrated_references_by_combo_.insert_or_assign(combo, std::move(calibrated)); + calibrated_s21_references_by_combo_.clear(); + calibrated_s21_references_by_combo_.reserve(combos.size()); + for (const auto& combo : combos) { + const auto& raw_reference = raw_s21_reference_bundle_.resolve(combo); + ChannelTrace calibrated{}; + calibrated.frequency_hz = raw_reference.frequency_hz; + calibrated.samples = calibration_master.apply_s21(combo, raw_reference.frequency_hz, raw_reference.samples); + calibrated_s21_references_by_combo_.insert_or_assign(combo, std::move(calibrated)); + } + + calibrated_s11_references_by_combo_.clear(); + if (!raw_s11_reference_bundle_.is_enabled()) { + return; + } + if (!calibration_master.has_s11_calibration()) { + throw std::runtime_error("S11 reference bundle requires S11 calibration bundle"); + } + + calibrated_s11_references_by_combo_.reserve(combos.size()); + for (const auto& combo : combos) { + const auto& raw_reference = raw_s11_reference_bundle_.resolve(combo); + ChannelTrace calibrated{}; + calibrated.frequency_hz = raw_reference.frequency_hz; + calibrated.samples = calibration_master.apply_s11(combo, raw_reference.frequency_hz, raw_reference.samples); + calibrated_s11_references_by_combo_.insert_or_assign(combo, std::move(calibrated)); } } void ReferenceMaster::validate_combos(const std::vector& combos) const { - if (calibrated_references_by_combo_.empty()) { + if (calibrated_s21_references_by_combo_.empty()) { throw std::runtime_error( "Calibrated references are not prepared. Call ReferenceMaster::prepare_calibrated() at startup." ); } + raw_s21_reference_bundle_.validate_combos(combos); for (const auto& combo : combos) { - if (!raw_references_by_combo_.contains(combo)) { - throw std::runtime_error("Raw reference data is missing for combo " + combo_to_string(combo)); + if (!calibrated_s21_references_by_combo_.contains(combo)) { + throw std::runtime_error("Calibrated S21 reference data is missing for combo " + combo_to_string(combo)); } - if (!calibrated_references_by_combo_.contains(combo)) { - throw std::runtime_error("Calibrated reference data is missing for combo " + combo_to_string(combo)); + } + + raw_s11_reference_bundle_.validate_combos(combos); + if (!raw_s11_reference_bundle_.is_enabled()) { + return; + } + if (calibrated_s11_references_by_combo_.empty()) { + throw std::runtime_error("Calibrated S11 references are not prepared"); + } + for (const auto& combo : combos) { + if (!calibrated_s11_references_by_combo_.contains(combo)) { + throw std::runtime_error("Calibrated S11 reference data is missing for combo " + combo_to_string(combo)); } } } -auto ReferenceMaster::apply(const ipc::SweepTraceBlock& calibrated_trace) const -> ipc::SweepTraceBlock { - validate_trace_layout(calibrated_trace, "Calibrated trace"); - - const auto found = calibrated_references_by_combo_.find(calibrated_trace.combo); - if (found == calibrated_references_by_combo_.end()) { - throw std::runtime_error( - "Calibrated reference trace is missing for combo " + combo_to_string(calibrated_trace.combo) - ); - } - - const auto& reference = found->second; - validate_trace_layout(reference, "Calibrated reference trace"); - if (calibrated_trace.s21.size() != reference.s21.size()) { - throw std::runtime_error( - "Reference point count mismatch for combo " + combo_to_string(calibrated_trace.combo) - ); - } - +auto ReferenceMaster::apply_to_trace(const ipc::SweepTraceBlock& calibrated_trace) const -> ipc::SweepTraceBlock { ipc::SweepTraceBlock output{}; output.combo = calibrated_trace.combo; output.frequency_hz = calibrated_trace.frequency_hz; - output.s11 = calibrated_trace.s11; - output.s21.resize(calibrated_trace.s21.size()); + output.s21 = apply_s21(calibrated_trace.combo, calibrated_trace.frequency_hz, calibrated_trace.s21); + output.s11 = apply_s11(calibrated_trace.combo, calibrated_trace.frequency_hz, calibrated_trace.s11); + return output; +} + +auto ReferenceMaster::apply_s21( + const ipc::ComboKey& combo, + std::span frequency_hz, + const std::vector& calibrated_s21 +) const -> std::vector { + const auto found = calibrated_s21_references_by_combo_.find(combo); + if (found == calibrated_s21_references_by_combo_.end()) { + throw std::runtime_error("Calibrated S21 reference is missing for combo " + combo_to_string(combo)); + } + + const auto& reference = found->second; + validate_channel_trace_layout(reference, "Calibrated S21 reference", combo); + if (calibrated_s21.size() != reference.samples.size()) { + throw std::runtime_error("S21 reference point count mismatch for combo " + combo_to_string(combo)); + } + if (!frequency_axes_match(frequency_hz, reference.frequency_hz)) { + throw std::runtime_error("S21 reference frequency axis mismatch for combo " + combo_to_string(combo)); + } + + std::vector output(calibrated_s21.size()); static_assert(sizeof(ipc::Complex32) == sizeof(float) * 2U, "Complex32 layout must be two contiguous floats"); using InterleavedComplexView = Eigen::Matrix; - const auto point_count = static_cast(calibrated_trace.s21.size()); + const auto point_count = static_cast(calibrated_s21.size()); Eigen::Map calibrated_view( - reinterpret_cast(calibrated_trace.s21.data()), + reinterpret_cast(calibrated_s21.data()), point_count, 2 ); Eigen::Map reference_view( - reinterpret_cast(reference.s21.data()), + reinterpret_cast(reference.samples.data()), point_count, 2 ); - Eigen::Map output_view(reinterpret_cast(output.s21.data()), point_count, 2); + Eigen::Map output_view(reinterpret_cast(output.data()), point_count, 2); output_view = calibrated_view - reference_view; + return output; +} +auto ReferenceMaster::apply_s11( + const ipc::ComboKey& combo, + std::span frequency_hz, + const std::vector& calibrated_s11 +) const -> std::vector { + if (!raw_s11_reference_bundle_.is_enabled()) { + return calibrated_s11; + } + + const auto found = calibrated_s11_references_by_combo_.find(combo); + if (found == calibrated_s11_references_by_combo_.end()) { + throw std::runtime_error("Calibrated S11 reference is missing for combo " + combo_to_string(combo)); + } + + const auto& reference = found->second; + validate_channel_trace_layout(reference, "Calibrated S11 reference", combo); + if (calibrated_s11.size() != reference.samples.size()) { + throw std::runtime_error("S11 reference point count mismatch for combo " + combo_to_string(combo)); + } + if (!frequency_axes_match(frequency_hz, reference.frequency_hz)) { + throw std::runtime_error("S11 reference frequency axis mismatch for combo " + combo_to_string(combo)); + } + + std::vector output(calibrated_s11.size()); + for (std::size_t index = 0; index < calibrated_s11.size(); ++index) { + output[index].re = calibrated_s11[index].re - reference.samples[index].re; + output[index].im = calibrated_s11[index].im - reference.samples[index].im; + } return output; } diff --git a/data_acq_and_processing/preprocessing/testdata/s11_load_calibration_bundle.bin b/data_acq_and_processing/preprocessing/testdata/s11_load_calibration_bundle.bin new file mode 100644 index 0000000000000000000000000000000000000000..6cee6b2623df57ae2a74939cf4d9bcd24ddda7c2 GIT binary patch literal 32280 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 literal 0 HcmV?d00001 diff --git a/data_acq_and_processing/preprocessing/testdata/s11_open_calibration_bundle.bin b/data_acq_and_processing/preprocessing/testdata/s11_open_calibration_bundle.bin new file mode 100644 index 0000000000000000000000000000000000000000..283d22a6c90433c1fc9ab7017ad4c542c60ab1ca GIT binary patch literal 32280 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 literal 0 HcmV?d00001 diff --git a/data_acq_and_processing/preprocessing/testdata/s11_reference_bundle.bin b/data_acq_and_processing/preprocessing/testdata/s11_reference_bundle.bin new file mode 100644 index 0000000000000000000000000000000000000000..1d64a3b7aa95851423316dc8ae6dda6c61e88b2b GIT binary patch literal 32280 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 std::string { + if (value == "s21" || value == "s11") { + return value; + } + throw std::runtime_error("processing.pass_through_channel must be one of: s21, s11"); +} + [[nodiscard]] auto parse_u64_number(const Json& value, const std::string& field_name) -> std::uint64_t { if (!value.is_number()) { throw std::runtime_error(field_name + " must be number"); @@ -101,6 +108,12 @@ using Json = nlohmann::json; } config.phase_deg = static_cast(found->get()); } + if (const auto found = root.find("pass_through_channel"); found != root.end()) { + 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()); + } if (const auto found = root.find("pass_through_fixed_y_enabled"); found != root.end()) { if (!found->is_boolean()) { throw std::runtime_error("processing.pass_through_fixed_y_enabled must be bool"); diff --git a/data_acq_and_processing/processing/processors/src/passthrough_processor.cpp b/data_acq_and_processing/processing/processors/src/passthrough_processor.cpp index d55244a..247737a 100644 --- a/data_acq_and_processing/processing/processors/src/passthrough_processor.cpp +++ b/data_acq_and_processing/processing/processors/src/passthrough_processor.cpp @@ -29,7 +29,11 @@ auto PassThroughProcessor::process_collection( payload.processing_name = name(); payload.kind = ipc::ResultKind::TraceComplex; payload.frequency_hz = trace.frequency_hz; - payload.trace = trace.s21; + if (live_config.pass_through_channel == "s11") { + payload.trace = trace.s11; + } else { + payload.trace = trace.s21; + } const float linear_gain = std::pow(10.0F, live_config.gain_db / 20.0F); const float phase_rad = live_config.phase_deg * (kPi / 180.0F); diff --git a/python_app/gui/app_window.py b/python_app/gui/app_window.py index c4a188b..af6b1b9 100644 --- a/python_app/gui/app_window.py +++ b/python_app/gui/app_window.py @@ -78,8 +78,8 @@ class AppWindow( def _init_preprocess_state(self) -> None: """Initialize preprocessing dialog and selected set names.""" self._preprocess_dialog: PreprocessDialog | None = None - self._selected_calibration_set = str(self._defaults_config.preprocess.calibration_set) - self._selected_reference_set = str(self._defaults_config.preprocess.reference_set) + 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) 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 bee62aa..7389e17 100644 --- a/python_app/gui/controllers/app_window_config_mixin.py +++ b/python_app/gui/controllers/app_window_config_mixin.py @@ -118,8 +118,8 @@ class AppWindowConfigMixin: if self._switches_are_effectively_static(config): config.combos = [ComboModel(input=0, output=0)] - config.preprocess.calibration_set = self._selected_calibration_set - config.preprocess.reference_set = self._selected_reference_set + config.preprocess.s21_calibration_set = self._selected_s21_calibration_set + config.preprocess.s21_reference_set = self._selected_s21_reference_set 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()) @@ -153,6 +153,7 @@ class AppWindowConfigMixin: processor_mode=self._processing_mode.currentText(), gain_db=float(self._processing_gain_db.value()), phase_deg=float(self._processing_phase_deg.value()), + pass_through_channel=self._pass_through_channel.currentText(), pass_through_fixed_y_enabled=bool(self._pass_through_fixed_y_enabled.isChecked()), pass_through_y_min_db=min(y_min_db, y_max_db), pass_through_y_max_db=max(y_min_db, y_max_db), diff --git a/python_app/gui/controllers/app_window_pipeline_mixin.py b/python_app/gui/controllers/app_window_pipeline_mixin.py index 3628249..9c3550d 100644 --- a/python_app/gui/controllers/app_window_pipeline_mixin.py +++ b/python_app/gui/controllers/app_window_pipeline_mixin.py @@ -38,28 +38,28 @@ class AppWindowPipelineMixin: run_signature = self._build_run_history_signature(config) radar_key = self._radar_key(config) - if not config.preprocess.calibration_set or not config.preprocess.reference_set: + 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") 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.calibration_set, combo_keys + "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.reference_set, combo_keys + "reference", radar_key, config.preprocess.s21_reference_set, combo_keys ): raise RuntimeError("Selected reference set does not cover requested run combos") - calibration_bundle, reference_bundle = self._config_writer.prepare_bundles( + calibration_bundle, reference_bundle = self._config_writer.prepare_s21_bundles( self._store, radar_key, - config.preprocess.calibration_set, - config.preprocess.reference_set, + config.preprocess.s21_calibration_set, + config.preprocess.s21_reference_set, ) - config.preprocess.calibration_bundle_path = str(calibration_bundle) - config.preprocess.reference_bundle_path = str(reference_bundle) + config.preprocess.s21_calibration_bundle_path = str(calibration_bundle) + config.preprocess.s21_reference_bundle_path = str(reference_bundle) 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 7b85246..0aa0fe8 100644 --- a/python_app/gui/controllers/app_window_plot_mixin.py +++ b/python_app/gui/controllers/app_window_plot_mixin.py @@ -106,6 +106,7 @@ class AppWindowPlotMixin: show_phase = self._show_phase_curves() magnitude_plot = self._trace_magnitude_plot phase_plot = self._trace_phase_plot + pass_through_channel = self._pass_through_channel.currentText().upper() magnitude_plot.setVisible(show_magnitude) phase_plot.setVisible(show_phase) @@ -119,6 +120,7 @@ class AppWindowPlotMixin: mag_item.showAxis("left", show=True) mag_item.showAxis("bottom", show=not show_phase) magnitude_plot.setLabel("left", "Magnitude", units="dB") + magnitude_plot.setTitle(f"Pass-Through {pass_through_channel}") if not show_phase: magnitude_plot.setLabel("bottom", "Frequency", units="Hz") @@ -130,6 +132,7 @@ class AppWindowPlotMixin: phase_item.showAxis("bottom", show=True) phase_plot.setLabel("left", "Phase", units="deg") phase_plot.setLabel("bottom", "Frequency", units="Hz") + phase_plot.setTitle(f"Pass-Through {pass_through_channel}") palette = [ "#4cc9f0", diff --git a/python_app/gui/controllers/app_window_preprocess_mixin.py b/python_app/gui/controllers/app_window_preprocess_mixin.py index 6fa05d1..946d074 100644 --- a/python_app/gui/controllers/app_window_preprocess_mixin.py +++ b/python_app/gui/controllers/app_window_preprocess_mixin.py @@ -40,14 +40,14 @@ class AppWindowPreprocessMixin: def _on_preprocess_selection_changed(self, calibration_set: str, reference_set: str) -> None: """Persist selected preprocessing set names from dialog.""" - self._selected_calibration_set = calibration_set.strip() - self._selected_reference_set = reference_set.strip() + self._selected_s21_calibration_set = calibration_set.strip() + self._selected_s21_reference_set = reference_set.strip() 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_calibration_set or "") - self._selected_reference_label.setText(self._selected_reference_set or "") + self._selected_calibration_label.setText(self._selected_s21_calibration_set or "") + self._selected_reference_label.setText(self._selected_s21_reference_set or "") def _refresh_sets(self) -> None: """Refresh calibration/reference set lists for current radar key.""" @@ -60,12 +60,12 @@ class AppWindowPreprocessMixin: dialog.set_calibration_sets(calibration_sets) dialog.set_reference_sets(reference_sets) - if self._selected_calibration_set not in calibration_sets: - self._selected_calibration_set = calibration_sets[0] if calibration_sets else "" - if self._selected_reference_set not in reference_sets: - self._selected_reference_set = reference_sets[0] if reference_sets else "" + 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 "" - dialog.set_selected_sets(self._selected_calibration_set, self._selected_reference_set) + dialog.set_selected_sets(self._selected_s21_calibration_set, self._selected_s21_reference_set) self._refresh_preprocess_summary_labels() self._log(f"Set lists refreshed for key={radar_key}") @@ -147,9 +147,9 @@ class AppWindowPreprocessMixin: self._cleanup_capture_session() if kind == "calibration": - self._selected_calibration_set = set_name + self._selected_s21_calibration_set = set_name else: - self._selected_reference_set = set_name + self._selected_s21_reference_set = set_name self._refresh_sets() dialog.set_status(f"{kind.title()} set saved: {set_name} ({len(collection.traces)} traces)") diff --git a/python_app/gui/controllers/app_window_ui_mixin.py b/python_app/gui/controllers/app_window_ui_mixin.py index f500fa3..f184e08 100644 --- a/python_app/gui/controllers/app_window_ui_mixin.py +++ b/python_app/gui/controllers/app_window_ui_mixin.py @@ -79,7 +79,7 @@ class AppWindowUiMixin: # Default view on startup is pass-through traces. self._plot_stack.setCurrentWidget(self._trace_plots_container) - root_layout.addWidget(self._plot_stack, stretch=11) + root_layout.addWidget(self._plot_stack, stretch=12) def _build_bscan_plot_page(self) -> None: """Create B-scan page in plot stack.""" @@ -141,7 +141,7 @@ class AppWindowUiMixin: def _build_settings_panel(self, root_layout: QHBoxLayout, root: QWidget) -> None: """Build right settings panel with controls, status labels, and log.""" self._settings_panel = QWidget(root) - self._settings_panel.setMinimumWidth(659) + self._settings_panel.setMinimumWidth(530) right_layout = QVBoxLayout(self._settings_panel) right_layout.setContentsMargins(0, 0, 0, 0) right_layout.setSpacing(10) @@ -163,7 +163,7 @@ class AppWindowUiMixin: right_layout.addWidget(self._history_label) right_layout.addWidget(self._log_box, stretch=0) - root_layout.addWidget(self._settings_panel, stretch=8) + root_layout.addWidget(self._settings_panel, stretch=6) def _build_settings_scroll(self) -> QScrollArea: """Build scroll area with all control groups in display order.""" diff --git a/python_app/gui/controllers/sections/data_actions_section.py b/python_app/gui/controllers/sections/data_actions_section.py index e1c5b7d..8a68f69 100644 --- a/python_app/gui/controllers/sections/data_actions_section.py +++ b/python_app/gui/controllers/sections/data_actions_section.py @@ -2,6 +2,7 @@ from __future__ import annotations +from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QSpinBox, QVBoxLayout @@ -11,16 +12,13 @@ def build_data_actions_group(owner) -> QGroupBox: layout = QVBoxLayout(group) layout.setSpacing(8) - save_row = QHBoxLayout() - save_row.setSpacing(8) - - save_button = QPushButton("Save Numpy Snapshot") + save_button = QPushButton("Save Snapshot") save_button.clicked.connect(owner._save_snapshot) - save_vna_json_button = QPushButton("Save VNA History JSON") + save_vna_json_button = QPushButton("Save VNA JSON") save_vna_json_button.clicked.connect(owner._save_vna_history_json) - remove_last_button = QPushButton("Remove Last Runtime Measurement") + remove_last_button = QPushButton("Remove Last Measurement") remove_last_button.clicked.connect(owner._remove_last_runtime_history) - clear_history_button = QPushButton("Clear ALL Runtime History") + clear_history_button = QPushButton("Clear Runtime History") clear_history_button.clicked.connect(owner._clear_all_runtime_history) owner._save_count = QSpinBox() owner._save_count.setMinimum(1) @@ -35,14 +33,20 @@ def build_data_actions_group(owner) -> QGroupBox: owner._vna_json_output_index.setMaximum(65_535) owner._vna_json_output_index.setValue(0) - save_row.addWidget(save_button) - save_row.addWidget(save_vna_json_button) - save_row.addWidget(remove_last_button) - save_row.addWidget(clear_history_button) - save_row.addWidget(QLabel("Last N")) - save_row.addWidget(owner._save_count) - save_row.addStretch(1) - layout.addLayout(save_row) + button_column = QVBoxLayout() + button_column.setSpacing(8) + button_column.addWidget(save_button, alignment=Qt.AlignmentFlag.AlignLeft) + button_column.addWidget(save_vna_json_button, alignment=Qt.AlignmentFlag.AlignLeft) + button_column.addWidget(remove_last_button, alignment=Qt.AlignmentFlag.AlignLeft) + button_column.addWidget(clear_history_button, alignment=Qt.AlignmentFlag.AlignLeft) + layout.addLayout(button_column) + + count_row = QHBoxLayout() + count_row.setSpacing(8) + count_row.addWidget(QLabel("Last N")) + count_row.addWidget(owner._save_count) + count_row.addStretch(1) + layout.addLayout(count_row) json_row = QHBoxLayout() json_row.setSpacing(8) diff --git a/python_app/gui/controllers/sections/hardware_actions_section.py b/python_app/gui/controllers/sections/hardware_actions_section.py index 70d5c87..db9b9ac 100644 --- a/python_app/gui/controllers/sections/hardware_actions_section.py +++ b/python_app/gui/controllers/sections/hardware_actions_section.py @@ -2,24 +2,25 @@ from __future__ import annotations -from PyQt6.QtWidgets import QGroupBox, QHBoxLayout, QPushButton +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QGroupBox, QPushButton, QVBoxLayout def build_hardware_actions_group(owner) -> QGroupBox: """Create hardware action buttons section.""" group = QGroupBox("Hardware Actions") - layout = QHBoxLayout(group) + layout = QVBoxLayout(group) layout.setSpacing(8) - apply_radar_button = QPushButton("Apply Radar Settings") + apply_radar_button = QPushButton("Apply Radar") apply_radar_button.clicked.connect(owner._apply_radar_settings) - layout.addWidget(apply_radar_button) + layout.addWidget(apply_radar_button, alignment=Qt.AlignmentFlag.AlignLeft) - save_config_button = QPushButton("Save Current Config") + save_config_button = QPushButton("Save Config") save_config_button.clicked.connect(owner._save_current_config) - layout.addWidget(save_config_button) + layout.addWidget(save_config_button, alignment=Qt.AlignmentFlag.AlignLeft) - preprocess_button = QPushButton("Preprocessing Panel") + preprocess_button = QPushButton("Preprocessing") preprocess_button.clicked.connect(owner._open_preprocess_panel) - layout.addWidget(preprocess_button) + layout.addWidget(preprocess_button, alignment=Qt.AlignmentFlag.AlignLeft) return group diff --git a/python_app/gui/controllers/sections/pipeline_section.py b/python_app/gui/controllers/sections/pipeline_section.py index 65ee644..ab6be87 100644 --- a/python_app/gui/controllers/sections/pipeline_section.py +++ b/python_app/gui/controllers/sections/pipeline_section.py @@ -2,7 +2,8 @@ from __future__ import annotations -from PyQt6.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QPushButton, QVBoxLayout +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QGroupBox, QLabel, QPushButton, QVBoxLayout def build_pipeline_group(owner) -> QGroupBox: @@ -11,22 +12,17 @@ def build_pipeline_group(owner) -> QGroupBox: layout = QVBoxLayout(group) layout.setSpacing(8) - action_row = QHBoxLayout() - action_row.setSpacing(8) - start_button = QPushButton("Start") start_button.clicked.connect(owner._start_run) - action_row.addWidget(start_button) + layout.addWidget(start_button, alignment=Qt.AlignmentFlag.AlignLeft) single_button = QPushButton("Single Capture") single_button.clicked.connect(owner._start_single_capture) - action_row.addWidget(single_button) + layout.addWidget(single_button, alignment=Qt.AlignmentFlag.AlignLeft) stop_button = QPushButton("Stop") stop_button.clicked.connect(owner._stop_run) - action_row.addWidget(stop_button) - - layout.addLayout(action_row) + layout.addWidget(stop_button, alignment=Qt.AlignmentFlag.AlignLeft) hint = QLabel("Start continuous run or single processed collection capture.") hint.setObjectName("hintLabel") diff --git a/python_app/gui/controllers/sections/processing_section.py b/python_app/gui/controllers/sections/processing_section.py index 611a872..0d0cb63 100644 --- a/python_app/gui/controllers/sections/processing_section.py +++ b/python_app/gui/controllers/sections/processing_section.py @@ -61,6 +61,9 @@ def build_processing_group(owner) -> QGroupBox: owner._processing_phase_deg.setSingleStep(1.0) owner._processing_phase_deg.setValue(0.0) + owner._pass_through_channel = QComboBox() + owner._pass_through_channel.addItems(["s21", "s11"]) + owner._show_magnitude_checkbox = QCheckBox("Show magnitude") owner._show_magnitude_checkbox.setChecked(True) @@ -91,6 +94,7 @@ def build_processing_group(owner) -> QGroupBox: pass_through_form.addRow("Gain dB (live)", owner._processing_gain_db) pass_through_form.addRow("Phase deg (live)", owner._processing_phase_deg) + pass_through_form.addRow("Channel", owner._pass_through_channel) pass_through_form.addRow(owner._show_magnitude_checkbox) pass_through_form.addRow(owner._show_phase_checkbox) pass_through_form.addRow(owner._pass_through_fixed_y_enabled) @@ -206,6 +210,7 @@ def build_processing_group(owner) -> QGroupBox: owner._processing_mode.currentTextChanged.connect(owner._on_processing_mode_changed) owner._processing_gain_db.valueChanged.connect(owner._on_processing_live_settings_changed) owner._processing_phase_deg.valueChanged.connect(owner._on_processing_live_settings_changed) + owner._pass_through_channel.currentTextChanged.connect(owner._on_processing_live_settings_changed) owner._show_magnitude_checkbox.toggled.connect(owner._on_trace_visibility_changed) owner._show_phase_checkbox.toggled.connect(owner._on_trace_visibility_changed) owner._pass_through_fixed_y_enabled.toggled.connect(sync_pass_through_y_controls) diff --git a/python_app/gui/main.py b/python_app/gui/main.py index 5bc6bc9..4aec4c6 100644 --- a/python_app/gui/main.py +++ b/python_app/gui/main.py @@ -23,7 +23,7 @@ def main() -> int: apply_dark_theme(app) pg.setConfigOptions(antialias=True, foreground="#dbe4f1") window = AppWindow(PROJECT_ROOT) - window.show() + window.showMaximized() return app.exec() diff --git a/python_app/gui/runtime/history.py b/python_app/gui/runtime/history.py index e4790ae..1d7c25e 100644 --- a/python_app/gui/runtime/history.py +++ b/python_app/gui/runtime/history.py @@ -79,8 +79,8 @@ def build_run_history_signature( str(config.output_switch.driver), int(config.output_switch.positions), bool(config.output_switch.invert_logic), - str(config.preprocess.calibration_set), - str(config.preprocess.reference_set), + str(config.preprocess.s21_calibration_set), + str(config.preprocess.s21_reference_set), combos_signature, ) diff --git a/python_app/models/run_config_codec.py b/python_app/models/run_config_codec.py index c423d90..4d32564 100644 --- a/python_app/models/run_config_codec.py +++ b/python_app/models/run_config_codec.py @@ -65,13 +65,47 @@ 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.calibration_set = str(preprocess_payload.get("calibration_set", model.preprocess.calibration_set)) - model.preprocess.reference_set = str(preprocess_payload.get("reference_set", model.preprocess.reference_set)) - model.preprocess.calibration_bundle_path = str( - preprocess_payload.get("calibration_bundle_path", model.preprocess.calibration_bundle_path) + model.preprocess.s21_calibration_set = str( + preprocess_payload.get("s21_calibration_set", model.preprocess.s21_calibration_set) ) - model.preprocess.reference_bundle_path = str( - preprocess_payload.get("reference_bundle_path", model.preprocess.reference_bundle_path) + model.preprocess.s21_reference_set = str( + preprocess_payload.get("s21_reference_set", model.preprocess.s21_reference_set) + ) + model.preprocess.s21_calibration_bundle_path = str( + preprocess_payload.get( + "s21_calibration_bundle_path", + model.preprocess.s21_calibration_bundle_path, + ) + ) + model.preprocess.s21_reference_bundle_path = str( + preprocess_payload.get( + "s21_reference_bundle_path", + model.preprocess.s21_reference_bundle_path, + ) + ) + model.preprocess.s11_open_calibration_bundle_path = str( + preprocess_payload.get( + "s11_open_calibration_bundle_path", + model.preprocess.s11_open_calibration_bundle_path, + ) + ) + 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, + ) ) model.gpr.mode = str(gpr_payload.get("mode", model.gpr.mode)) @@ -127,6 +161,7 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel: model.ensure_combos() return model + def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]: """Encode :class:`RunConfigModel` to C++ pipeline-compatible JSON structure.""" model.ensure_combos() @@ -178,10 +213,14 @@ def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]: "combos": [{"input": combo.input, "output": combo.output} for combo in model.combos], }, "preprocess": { - "calibration_set": model.preprocess.calibration_set, - "reference_set": model.preprocess.reference_set, - "calibration_bundle_path": model.preprocess.calibration_bundle_path, - "reference_bundle_path": model.preprocess.reference_bundle_path, + "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, }, "gpr": { "mode": model.gpr.mode, diff --git a/python_app/models/run_config_schema.py b/python_app/models/run_config_schema.py index 995bd62..89bb5ef 100644 --- a/python_app/models/run_config_schema.py +++ b/python_app/models/run_config_schema.py @@ -89,10 +89,14 @@ class RuntimeModel: class PreprocessModel: """Selected preprocessing artifacts for live acquisition.""" - calibration_set: str = "" - reference_set: str = "" - calibration_bundle_path: str = "" - reference_bundle_path: str = "" + 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 = "" @dataclass(slots=True) diff --git a/python_app/orchestration/config_writer.py b/python_app/orchestration/config_writer.py index adc5c3d..f356c93 100644 --- a/python_app/orchestration/config_writer.py +++ b/python_app/orchestration/config_writer.py @@ -17,19 +17,19 @@ class ConfigWriter: self._runtime_dir = runtime_dir self._runtime_dir.mkdir(parents=True, exist_ok=True) - def prepare_bundles( + def prepare_s21_bundles( self, store: NpzStore, radar_key: str, - calibration_set: str, - reference_set: 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 / "calibration_bundle.bin" - reference_bundle = self._runtime_dir / "reference_bundle.bin" + 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, calibration_set, calibration_bundle) - store.export_set_bundle("reference", radar_key, reference_set, reference_bundle) + 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 def write(self, config: RunConfigModel, output_path: Path) -> Path: diff --git a/python_app/orchestration/live_processing_config.py b/python_app/orchestration/live_processing_config.py index 1b67761..2ae98c2 100644 --- a/python_app/orchestration/live_processing_config.py +++ b/python_app/orchestration/live_processing_config.py @@ -14,6 +14,7 @@ class ProcessingLiveConfig: processor_mode: str = "pass_through" gain_db: float = 0.0 phase_deg: float = 0.0 + pass_through_channel: str = "s21" pass_through_fixed_y_enabled: bool = False pass_through_y_min_db: float = -100.0 pass_through_y_max_db: float = 0.0 @@ -52,6 +53,7 @@ class ProcessingLiveConfig: "processor_mode": str(self.processor_mode), "gain_db": float(self.gain_db), "phase_deg": float(self.phase_deg), + "pass_through_channel": str(self.pass_through_channel), "pass_through_fixed_y_enabled": bool(self.pass_through_fixed_y_enabled), "pass_through_y_min_db": float(self.pass_through_y_min_db), "pass_through_y_max_db": float(self.pass_through_y_max_db), diff --git a/python_app/runtime/run_config_smoke.json b/python_app/runtime/run_config_smoke.json index cb081c4..16410ba 100644 --- a/python_app/runtime/run_config_smoke.json +++ b/python_app/runtime/run_config_smoke.json @@ -79,10 +79,14 @@ ] }, "preprocess": { - "calibration_set": "smoke_cal", - "reference_set": "smoke_ref", - "calibration_bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/calibration_bundle.bin", - "reference_bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/reference_bundle.bin" + "s21_calibration_set": "smoke_cal", + "s21_reference_set": "smoke_ref", + "s21_calibration_bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s21_calibration_bundle.bin", + "s21_reference_bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s21_reference_bundle.bin", + "s11_open_calibration_bundle_path": "/home/europa/Documents/radar_system/data_acq_and_processing/preprocessing/testdata/s11_open_calibration_bundle.bin", + "s11_short_calibration_bundle_path": "/home/europa/Documents/radar_system/data_acq_and_processing/preprocessing/testdata/s11_short_calibration_bundle.bin", + "s11_load_calibration_bundle_path": "/home/europa/Documents/radar_system/data_acq_and_processing/preprocessing/testdata/s11_load_calibration_bundle.bin", + "s11_reference_bundle_path": "/home/europa/Documents/radar_system/data_acq_and_processing/preprocessing/testdata/s11_reference_bundle.bin" }, "gpr": { "mode": "point", diff --git a/python_app/scripts/manual_smoke_run.py b/python_app/scripts/manual_smoke_run.py index c6eb350..a9b1107 100644 --- a/python_app/scripts/manual_smoke_run.py +++ b/python_app/scripts/manual_smoke_run.py @@ -113,11 +113,16 @@ def main() -> int: store.save_set("calibration", radar_key, "smoke_cal", calibration_set) store.save_set("reference", radar_key, "smoke_ref", reference_set) - calibration_bundle, reference_bundle = config_writer.prepare_bundles(store, radar_key, "smoke_cal", "smoke_ref") - config.preprocess.calibration_set = "smoke_cal" - config.preprocess.reference_set = "smoke_ref" - config.preprocess.calibration_bundle_path = str(calibration_bundle) - config.preprocess.reference_bundle_path = str(reference_bundle) + 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_path = config_writer.write(config, project_root / "python_app/runtime/run_config_smoke.json") diff --git a/run_config.json b/run_config.json index 5c95a31..0bf5510 100644 --- a/run_config.json +++ b/run_config.json @@ -55,10 +55,14 @@ ] }, "preprocess": { - "calibration_set": "", - "reference_set": "", - "calibration_bundle_path": "python_app/runtime/calibration_bundle.bin", - "reference_bundle_path": "python_app/runtime/reference_bundle.bin" + "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": "" }, "gpr": { "mode": "point",