added pass through s11
This commit is contained in:
@@ -37,6 +37,7 @@ ORCH_SOURCES := \
|
|||||||
|
|
||||||
PREPROC_SOURCES := \
|
PREPROC_SOURCES := \
|
||||||
data_acq_and_processing/preprocessing/calibration_master/src/calibration_master.cpp \
|
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/calibration_master/src/through_calibrator.cpp \
|
||||||
data_acq_and_processing/preprocessing/reference_master/src/reference_master.cpp \
|
data_acq_and_processing/preprocessing/reference_master/src/reference_master.cpp \
|
||||||
data_acq_and_processing/preprocessing/data_preprocessor/src/data_preprocessor.cpp \
|
data_acq_and_processing/preprocessing/data_preprocessor/src/data_preprocessor.cpp \
|
||||||
|
|||||||
@@ -75,10 +75,14 @@ struct RuntimeConfig {
|
|||||||
|
|
||||||
struct PreprocessConfig {
|
struct PreprocessConfig {
|
||||||
// Names and bundle paths selected by Python GUI layer.
|
// Names and bundle paths selected by Python GUI layer.
|
||||||
std::string calibration_set{};
|
std::string s21_calibration_set{};
|
||||||
std::string reference_set{};
|
std::string s21_reference_set{};
|
||||||
std::string calibration_bundle_path{};
|
std::string s21_calibration_bundle_path{};
|
||||||
std::string reference_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 {
|
struct GprTxGeometry {
|
||||||
|
|||||||
@@ -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");
|
const auto* preprocess_obj = as_object(required_field(*root_obj, "preprocess"), "preprocess");
|
||||||
config.preprocess.calibration_set = optional_string(*preprocess_obj, "calibration_set", "");
|
config.preprocess.s21_calibration_set = optional_string(*preprocess_obj, "s21_calibration_set", "");
|
||||||
config.preprocess.reference_set = optional_string(*preprocess_obj, "reference_set", "");
|
config.preprocess.s21_reference_set = optional_string(*preprocess_obj, "s21_reference_set", "");
|
||||||
config.preprocess.calibration_bundle_path = optional_string(*preprocess_obj, "calibration_bundle_path", "");
|
config.preprocess.s21_calibration_bundle_path =
|
||||||
config.preprocess.reference_bundle_path = optional_string(*preprocess_obj, "reference_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) {
|
if (const auto* gpr_value = optional_field(*root_obj, "gpr"); gpr_value != nullptr) {
|
||||||
|
|||||||
+29
-10
@@ -1,32 +1,51 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "calibrator_interface.hpp"
|
#include "calibrator_interface.hpp"
|
||||||
|
#include "channel_bundle_support.hpp"
|
||||||
#include "shared_types.hpp"
|
#include "shared_types.hpp"
|
||||||
|
|
||||||
namespace radar::preprocessing {
|
namespace radar::preprocessing {
|
||||||
|
|
||||||
class CalibrationMaster {
|
class CalibrationMaster {
|
||||||
public:
|
public:
|
||||||
// `calibrator` encapsulates the actual calibration algorithm (v1: through).
|
// `s21_calibrator` encapsulates the S21 calibration algorithm.
|
||||||
explicit CalibrationMaster(std::unique_ptr<CalibratorInterface> calibrator);
|
explicit CalibrationMaster(std::unique_ptr<CalibratorInterface> s21_calibrator);
|
||||||
|
|
||||||
// Loads a serialized raw sweep bundle with one calibration standard per combo.
|
// Loads a serialized raw sweep bundle with one S21 calibration standard per combo.
|
||||||
void load_bundle(const std::string& path);
|
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.
|
// Ensures all runtime combos are present in loaded standards.
|
||||||
void validate_combos(const std::vector<ipc::ComboKey>& combos) const;
|
void validate_combos(const std::vector<ipc::ComboKey>& combos) const;
|
||||||
// Applies calibration standard corresponding to measured trace combo.
|
// Applies both channel calibrations to one measured trace.
|
||||||
[[nodiscard]] auto apply(const ipc::SweepTraceBlock& measured_trace) const -> ipc::SweepTraceBlock;
|
[[nodiscard]] auto apply_to_trace(const ipc::SweepTraceBlock& measured_trace) const -> ipc::SweepTraceBlock;
|
||||||
|
[[nodiscard]] auto apply_s21(
|
||||||
|
const ipc::ComboKey& combo,
|
||||||
|
std::span<const float> frequency_hz,
|
||||||
|
const std::vector<ipc::Complex32>& measured_s21
|
||||||
|
) const -> std::vector<ipc::Complex32>;
|
||||||
|
[[nodiscard]] auto apply_s11(
|
||||||
|
const ipc::ComboKey& combo,
|
||||||
|
std::span<const float> frequency_hz,
|
||||||
|
const std::vector<ipc::Complex32>& measured_s11
|
||||||
|
) const -> std::vector<ipc::Complex32>;
|
||||||
|
[[nodiscard]] auto has_s11_calibration() const noexcept -> bool;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<CalibratorInterface> calibrator_impl_;
|
std::unique_ptr<CalibratorInterface> s21_calibrator_;
|
||||||
std::unordered_map<ipc::ComboKey, ipc::SweepTraceBlock, ipc::ComboKeyHash> standards_by_combo_{};
|
S21CalibrationBundle s21_calibration_bundle_{};
|
||||||
|
S11CalibrationBundle s11_calibration_bundle_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
[[nodiscard]] auto make_through_calibrator() -> std::unique_ptr<CalibratorInterface>;
|
[[nodiscard]] auto make_s21_through_calibrator() -> std::unique_ptr<CalibratorInterface>;
|
||||||
|
|
||||||
} // namespace radar::preprocessing
|
} // namespace radar::preprocessing
|
||||||
|
|||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "shared_types.hpp"
|
||||||
|
|
||||||
|
namespace radar::preprocessing {
|
||||||
|
|
||||||
|
class CalibratorInterface;
|
||||||
|
|
||||||
|
struct ChannelTrace {
|
||||||
|
std::vector<float> frequency_hz{};
|
||||||
|
std::vector<ipc::Complex32> samples{};
|
||||||
|
};
|
||||||
|
|
||||||
|
class S21CalibrationBundle {
|
||||||
|
public:
|
||||||
|
void load(const std::string& path);
|
||||||
|
|
||||||
|
[[nodiscard]] auto is_enabled() const noexcept -> bool;
|
||||||
|
void validate_combos(const std::vector<ipc::ComboKey>& combos) const;
|
||||||
|
[[nodiscard]] auto apply(
|
||||||
|
const ipc::ComboKey& combo,
|
||||||
|
std::span<const float> frequency_hz,
|
||||||
|
const std::vector<ipc::Complex32>& measured,
|
||||||
|
const CalibratorInterface& calibrator
|
||||||
|
) const -> std::vector<ipc::Complex32>;
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[nodiscard]] auto resolve(const ipc::ComboKey& combo) const -> const ChannelTrace&;
|
||||||
|
|
||||||
|
std::unordered_map<ipc::ComboKey, ChannelTrace, ipc::ComboKeyHash> 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<ipc::ComboKey>& combos) const;
|
||||||
|
[[nodiscard]] auto resolve(const ipc::ComboKey& combo) const -> const ChannelTrace&;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<ipc::ComboKey, ChannelTrace, ipc::ComboKeyHash> traces_by_combo_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
class S11CalibrationBundle {
|
||||||
|
public:
|
||||||
|
struct Coefficients {
|
||||||
|
std::vector<float> frequency_hz{};
|
||||||
|
std::vector<ipc::Complex32> directivity{};
|
||||||
|
std::vector<ipc::Complex32> source_match{};
|
||||||
|
std::vector<ipc::Complex32> 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<ipc::ComboKey>& combos) const;
|
||||||
|
[[nodiscard]] auto apply(
|
||||||
|
const ipc::ComboKey& combo,
|
||||||
|
std::span<const float> frequency_hz,
|
||||||
|
const std::vector<ipc::Complex32>& measured
|
||||||
|
) const -> std::vector<ipc::Complex32>;
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[nodiscard]] auto resolve(const ipc::ComboKey& combo) const -> const Coefficients&;
|
||||||
|
|
||||||
|
std::unordered_map<ipc::ComboKey, Coefficients, ipc::ComboKeyHash> 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<ipc::ComboKey>& combos) const;
|
||||||
|
[[nodiscard]] auto resolve(const ipc::ComboKey& combo) const -> const ChannelTrace&;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<ipc::ComboKey, ChannelTrace, ipc::ComboKeyHash> traces_by_combo_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace radar::preprocessing
|
||||||
+37
-84
@@ -1,107 +1,60 @@
|
|||||||
#include "calibration_master.hpp"
|
#include "calibration_master.hpp"
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <fstream>
|
|
||||||
#include <iterator>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace radar::preprocessing {
|
namespace radar::preprocessing {
|
||||||
namespace {
|
|
||||||
|
|
||||||
[[nodiscard]] auto combo_to_string(const ipc::ComboKey& combo) -> std::string {
|
CalibrationMaster::CalibrationMaster(std::unique_ptr<CalibratorInterface> s21_calibrator)
|
||||||
return "input=" + std::to_string(combo.input_pos) + " output=" + std::to_string(combo.output_pos);
|
: s21_calibrator_(std::move(s21_calibrator)) {
|
||||||
}
|
if (!s21_calibrator_) {
|
||||||
|
|
||||||
[[nodiscard]] auto read_binary_file(const std::string& path, const std::string& bundle_label)
|
|
||||||
-> std::vector<std::uint8_t> {
|
|
||||||
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::uint8_t>(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
|
||||||
}
|
|
||||||
|
|
||||||
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<CalibratorInterface> calibrator)
|
|
||||||
: calibrator_impl_(std::move(calibrator)) {
|
|
||||||
if (!calibrator_impl_) {
|
|
||||||
throw std::runtime_error("CalibrationMaster requires a non-null calibrator");
|
throw std::runtime_error("CalibrationMaster requires a non-null calibrator");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CalibrationMaster::load_bundle(const std::string& path) {
|
void CalibrationMaster::load_s21_calibration_bundle(const std::string& path) {
|
||||||
if (path.empty()) {
|
s21_calibration_bundle_.load(path);
|
||||||
throw std::runtime_error("Calibration bundle path must not be empty");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const auto bytes = read_binary_file(path, "calibration");
|
void CalibrationMaster::load_s11_calibration_bundle(
|
||||||
if (bytes.empty()) {
|
const std::string& open_path,
|
||||||
throw std::runtime_error("Calibration bundle is empty: " + path);
|
const std::string& short_path,
|
||||||
}
|
const std::string& load_path
|
||||||
|
) {
|
||||||
const auto collection = ipc::deserialize_raw_collection(bytes);
|
s11_calibration_bundle_.load(open_path, short_path, load_path);
|
||||||
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::validate_combos(const std::vector<ipc::ComboKey>& combos) const {
|
void CalibrationMaster::validate_combos(const std::vector<ipc::ComboKey>& combos) const {
|
||||||
for (const auto& combo : combos) {
|
s21_calibration_bundle_.validate_combos(combos);
|
||||||
if (!standards_by_combo_.contains(combo)) {
|
s11_calibration_bundle_.validate_combos(combos);
|
||||||
throw std::runtime_error("Calibration data is missing for combo " + combo_to_string(combo));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto CalibrationMaster::apply(const ipc::SweepTraceBlock& measured_trace) const -> ipc::SweepTraceBlock {
|
auto CalibrationMaster::apply_to_trace(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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ipc::SweepTraceBlock output{};
|
ipc::SweepTraceBlock output{};
|
||||||
output.combo = measured_trace.combo;
|
output.combo = measured_trace.combo;
|
||||||
output.frequency_hz = measured_trace.frequency_hz;
|
output.frequency_hz = measured_trace.frequency_hz;
|
||||||
output.s11 = measured_trace.s11;
|
output.s21 = apply_s21(measured_trace.combo, measured_trace.frequency_hz, measured_trace.s21);
|
||||||
output.s21 = calibrator_impl_->apply(measured_trace.s21, standard.s21);
|
output.s11 = apply_s11(measured_trace.combo, measured_trace.frequency_hz, measured_trace.s11);
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto CalibrationMaster::apply_s21(
|
||||||
|
const ipc::ComboKey& combo,
|
||||||
|
std::span<const float> frequency_hz,
|
||||||
|
const std::vector<ipc::Complex32>& measured_s21
|
||||||
|
) const -> std::vector<ipc::Complex32> {
|
||||||
|
return s21_calibration_bundle_.apply(combo, frequency_hz, measured_s21, *s21_calibrator_);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto CalibrationMaster::apply_s11(
|
||||||
|
const ipc::ComboKey& combo,
|
||||||
|
std::span<const float> frequency_hz,
|
||||||
|
const std::vector<ipc::Complex32>& measured_s11
|
||||||
|
) const -> std::vector<ipc::Complex32> {
|
||||||
|
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
|
} // namespace radar::preprocessing
|
||||||
|
|||||||
+405
@@ -0,0 +1,405 @@
|
|||||||
|
#include "channel_bundle_support.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <complex>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <span>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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::uint8_t> {
|
||||||
|
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::uint8_t>(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
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<const float> left, std::span<const float> 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<const float> expected,
|
||||||
|
std::span<const float> 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<float> {
|
||||||
|
return std::complex<float>(value.re, value.im);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto from_std_complex(const std::complex<float>& 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<ipc::Complex32>& {
|
||||||
|
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<ipc::ComboKey, ChannelTrace, ipc::ComboKeyHash> {
|
||||||
|
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<ipc::ComboKey, ChannelTrace, ipc::ComboKeyHash> 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<ipc::ComboKey, ChannelTrace, ipc::ComboKeyHash>& traces_by_combo,
|
||||||
|
const std::vector<ipc::ComboKey>& 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 <typename TValue>
|
||||||
|
auto resolve_required(
|
||||||
|
const std::unordered_map<ipc::ComboKey, TValue, ipc::ComboKeyHash>& 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<ipc::ComboKey, ChannelTrace, ipc::ComboKeyHash>& expected,
|
||||||
|
const std::unordered_map<ipc::ComboKey, ChannelTrace, ipc::ComboKeyHash>& 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<float> source_match = std::complex<float>(0.0F, 0.0F);
|
||||||
|
std::complex<float> reflection_tracking = std::complex<float>(1.0F, 0.0F);
|
||||||
|
if (std::norm(denominator) > kComplexMagnitudeSquaredEpsilon) {
|
||||||
|
source_match = (open_delta + short_delta) / denominator;
|
||||||
|
reflection_tracking = open_delta * (std::complex<float>(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<ipc::ComboKey>& combos) const {
|
||||||
|
validate_bundle_combos(traces_by_combo_, combos, "S21 calibration bundle");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto S21CalibrationBundle::apply(
|
||||||
|
const ipc::ComboKey& combo,
|
||||||
|
std::span<const float> frequency_hz,
|
||||||
|
const std::vector<ipc::Complex32>& measured,
|
||||||
|
const CalibratorInterface& calibrator
|
||||||
|
) const -> std::vector<ipc::Complex32> {
|
||||||
|
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<ipc::ComboKey>& 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<ipc::ComboKey>& 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<const float> frequency_hz,
|
||||||
|
const std::vector<ipc::Complex32>& measured
|
||||||
|
) const -> std::vector<ipc::Complex32> {
|
||||||
|
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<ipc::Complex32> 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<ipc::ComboKey>& 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
|
||||||
@@ -69,7 +69,7 @@ class ThroughCalibrator final : public CalibratorInterface {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto make_through_calibrator() -> std::unique_ptr<CalibratorInterface> {
|
auto make_s21_through_calibrator() -> std::unique_ptr<CalibratorInterface> {
|
||||||
return std::make_unique<ThroughCalibrator>();
|
return std::make_unique<ThroughCalibrator>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ auto DataPreprocessor::preprocess_collection(const ipc::RawSweepCollection& raw_
|
|||||||
|
|
||||||
for (const auto& raw_trace : raw_collection.traces) {
|
for (const auto& raw_trace : raw_collection.traces) {
|
||||||
// Pipeline order is fixed: calibration first, then reference subtraction.
|
// Pipeline order is fixed: calibration first, then reference subtraction.
|
||||||
const auto calibrated = calibration_master_.apply(raw_trace);
|
const auto calibrated = calibration_master_.apply_to_trace(raw_trace);
|
||||||
const auto referenced = reference_master_.apply(calibrated);
|
const auto referenced = reference_master_.apply_to_trace(calibrated);
|
||||||
preprocessed.traces.push_back(referenced);
|
preprocessed.traces.push_back(referenced);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,12 +62,20 @@ int main(int argc, char** argv) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Load preprocessing assets once before entering run loop.
|
// Load preprocessing assets once before entering run loop.
|
||||||
radar::preprocessing::CalibrationMaster calibration_master(radar::preprocessing::make_through_calibrator());
|
radar::preprocessing::CalibrationMaster calibration_master(
|
||||||
calibration_master.load_bundle(config.preprocess.calibration_bundle_path);
|
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;
|
radar::preprocessing::ReferenceMaster reference_master;
|
||||||
reference_master.load_bundle(config.preprocess.reference_bundle_path);
|
reference_master.load_s21_reference_bundle(config.preprocess.s21_reference_bundle_path);
|
||||||
reference_master.prepare_calibrated(calibration_master);
|
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(
|
radar::preprocessing::DataPreprocessor preprocessor(
|
||||||
config,
|
config,
|
||||||
|
|||||||
+24
-8
@@ -1,9 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "channel_bundle_support.hpp"
|
||||||
#include "shared_types.hpp"
|
#include "shared_types.hpp"
|
||||||
|
|
||||||
namespace radar::preprocessing {
|
namespace radar::preprocessing {
|
||||||
@@ -12,19 +14,33 @@ class CalibrationMaster;
|
|||||||
|
|
||||||
class ReferenceMaster {
|
class ReferenceMaster {
|
||||||
public:
|
public:
|
||||||
// Loads a serialized raw sweep bundle with one reference trace per combo.
|
// Loads a serialized raw sweep bundle with one S21 reference trace per combo.
|
||||||
void load_bundle(const std::string& path);
|
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.
|
// Builds calibrated references in memory using currently loaded raw references.
|
||||||
// Must be called after load_bundle() and after calibration standards are loaded.
|
// Must be called after load_s21_reference_bundle() and after calibration standards are loaded.
|
||||||
void prepare_calibrated(const CalibrationMaster& calibration_master);
|
void prepare_calibrated(const CalibrationMaster& calibration_master, const std::vector<ipc::ComboKey>& combos);
|
||||||
// Ensures all runtime combos are present in loaded references.
|
// Ensures all runtime combos are present in loaded references.
|
||||||
void validate_combos(const std::vector<ipc::ComboKey>& combos) const;
|
void validate_combos(const std::vector<ipc::ComboKey>& combos) const;
|
||||||
// Subtracts per-combo reference trace from calibrated trace.
|
// Subtracts both channel references from one calibrated trace.
|
||||||
[[nodiscard]] auto apply(const ipc::SweepTraceBlock& calibrated_trace) const -> ipc::SweepTraceBlock;
|
[[nodiscard]] auto apply_to_trace(const ipc::SweepTraceBlock& calibrated_trace) const -> ipc::SweepTraceBlock;
|
||||||
|
[[nodiscard]] auto apply_s21(
|
||||||
|
const ipc::ComboKey& combo,
|
||||||
|
std::span<const float> frequency_hz,
|
||||||
|
const std::vector<ipc::Complex32>& calibrated_s21
|
||||||
|
) const -> std::vector<ipc::Complex32>;
|
||||||
|
[[nodiscard]] auto apply_s11(
|
||||||
|
const ipc::ComboKey& combo,
|
||||||
|
std::span<const float> frequency_hz,
|
||||||
|
const std::vector<ipc::Complex32>& calibrated_s11
|
||||||
|
) const -> std::vector<ipc::Complex32>;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unordered_map<ipc::ComboKey, ipc::SweepTraceBlock, ipc::ComboKeyHash> raw_references_by_combo_{};
|
S21ReferenceBundle raw_s21_reference_bundle_{};
|
||||||
std::unordered_map<ipc::ComboKey, ipc::SweepTraceBlock, ipc::ComboKeyHash> calibrated_references_by_combo_{};
|
std::unordered_map<ipc::ComboKey, ChannelTrace, ipc::ComboKeyHash> calibrated_s21_references_by_combo_{};
|
||||||
|
S11ReferenceBundle raw_s11_reference_bundle_{};
|
||||||
|
std::unordered_map<ipc::ComboKey, ChannelTrace, ipc::ComboKeyHash> calibrated_s11_references_by_combo_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace radar::preprocessing
|
} // namespace radar::preprocessing
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "reference_master.hpp"
|
#include "reference_master.hpp"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <algorithm>
|
||||||
#include <fstream>
|
#include <cmath>
|
||||||
#include <iterator>
|
#include <span>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -15,132 +15,195 @@
|
|||||||
namespace radar::preprocessing {
|
namespace radar::preprocessing {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
constexpr float kFrequencyRelativeTolerance = 1e-5F;
|
||||||
|
constexpr float kFrequencyAbsoluteTolerance = 1e-3F;
|
||||||
|
|
||||||
[[nodiscard]] auto combo_to_string(const ipc::ComboKey& combo) -> std::string {
|
[[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);
|
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)
|
void validate_channel_trace_layout(
|
||||||
-> std::vector<std::uint8_t> {
|
const ChannelTrace& trace,
|
||||||
std::ifstream stream(path, std::ios::binary);
|
const std::string& trace_label,
|
||||||
if (!stream.is_open()) {
|
const ipc::ComboKey& combo
|
||||||
throw std::runtime_error("Failed to open " + bundle_label + " bundle: " + path);
|
) {
|
||||||
|
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::uint8_t>(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void validate_trace_layout(const ipc::SweepTraceBlock& trace, const std::string& trace_label) {
|
[[nodiscard]] auto frequency_axes_match(std::span<const float> left, std::span<const float> right) -> bool {
|
||||||
if (trace.frequency_hz.size() != trace.s21.size()) {
|
if (left.size() != right.size()) {
|
||||||
throw std::runtime_error(
|
return false;
|
||||||
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(
|
for (std::size_t index = 0; index < left.size(); ++index) {
|
||||||
trace_label + " frequency/S11 vector size mismatch for combo " + combo_to_string(trace.combo)
|
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
|
} // namespace
|
||||||
|
|
||||||
void ReferenceMaster::load_bundle(const std::string& path) {
|
void ReferenceMaster::load_s21_reference_bundle(const std::string& path) {
|
||||||
if (path.empty()) {
|
raw_s21_reference_bundle_.load(path);
|
||||||
throw std::runtime_error("Reference bundle path must not be empty");
|
calibrated_s21_references_by_combo_.clear();
|
||||||
}
|
|
||||||
|
|
||||||
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::prepare_calibrated(const CalibrationMaster& calibration_master) {
|
void ReferenceMaster::load_s11_reference_bundle(const std::string& path) {
|
||||||
if (raw_references_by_combo_.empty()) {
|
raw_s11_reference_bundle_.load(path);
|
||||||
|
calibrated_s11_references_by_combo_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReferenceMaster::prepare_calibrated(
|
||||||
|
const CalibrationMaster& calibration_master,
|
||||||
|
const std::vector<ipc::ComboKey>& combos
|
||||||
|
) {
|
||||||
|
if (!raw_s21_reference_bundle_.is_enabled()) {
|
||||||
throw std::runtime_error("Reference bundle must be loaded before prepare_calibrated()");
|
throw std::runtime_error("Reference bundle must be loaded before prepare_calibrated()");
|
||||||
}
|
}
|
||||||
|
|
||||||
calibrated_references_by_combo_.clear();
|
calibrated_s21_references_by_combo_.clear();
|
||||||
calibrated_references_by_combo_.reserve(raw_references_by_combo_.size());
|
calibrated_s21_references_by_combo_.reserve(combos.size());
|
||||||
for (const auto& [combo, raw_reference] : raw_references_by_combo_) {
|
for (const auto& combo : combos) {
|
||||||
auto calibrated = calibration_master.apply(raw_reference);
|
const auto& raw_reference = raw_s21_reference_bundle_.resolve(combo);
|
||||||
calibrated.combo = combo;
|
ChannelTrace calibrated{};
|
||||||
calibrated_references_by_combo_.insert_or_assign(combo, std::move(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<ipc::ComboKey>& combos) const {
|
void ReferenceMaster::validate_combos(const std::vector<ipc::ComboKey>& combos) const {
|
||||||
if (calibrated_references_by_combo_.empty()) {
|
if (calibrated_s21_references_by_combo_.empty()) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"Calibrated references are not prepared. Call ReferenceMaster::prepare_calibrated() at startup."
|
"Calibrated references are not prepared. Call ReferenceMaster::prepare_calibrated() at startup."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
raw_s21_reference_bundle_.validate_combos(combos);
|
||||||
for (const auto& combo : combos) {
|
for (const auto& combo : combos) {
|
||||||
if (!raw_references_by_combo_.contains(combo)) {
|
if (!calibrated_s21_references_by_combo_.contains(combo)) {
|
||||||
throw std::runtime_error("Raw reference data is missing for combo " + combo_to_string(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 {
|
auto ReferenceMaster::apply_to_trace(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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ipc::SweepTraceBlock output{};
|
ipc::SweepTraceBlock output{};
|
||||||
output.combo = calibrated_trace.combo;
|
output.combo = calibrated_trace.combo;
|
||||||
output.frequency_hz = calibrated_trace.frequency_hz;
|
output.frequency_hz = calibrated_trace.frequency_hz;
|
||||||
output.s11 = calibrated_trace.s11;
|
output.s21 = apply_s21(calibrated_trace.combo, calibrated_trace.frequency_hz, calibrated_trace.s21);
|
||||||
output.s21.resize(calibrated_trace.s21.size());
|
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<const float> frequency_hz,
|
||||||
|
const std::vector<ipc::Complex32>& calibrated_s21
|
||||||
|
) const -> std::vector<ipc::Complex32> {
|
||||||
|
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<ipc::Complex32> output(calibrated_s21.size());
|
||||||
|
|
||||||
static_assert(sizeof(ipc::Complex32) == sizeof(float) * 2U, "Complex32 layout must be two contiguous floats");
|
static_assert(sizeof(ipc::Complex32) == sizeof(float) * 2U, "Complex32 layout must be two contiguous floats");
|
||||||
using InterleavedComplexView = Eigen::Matrix<float, Eigen::Dynamic, 2, Eigen::RowMajor>;
|
using InterleavedComplexView = Eigen::Matrix<float, Eigen::Dynamic, 2, Eigen::RowMajor>;
|
||||||
const auto point_count = static_cast<Eigen::Index>(calibrated_trace.s21.size());
|
const auto point_count = static_cast<Eigen::Index>(calibrated_s21.size());
|
||||||
|
|
||||||
Eigen::Map<const InterleavedComplexView> calibrated_view(
|
Eigen::Map<const InterleavedComplexView> calibrated_view(
|
||||||
reinterpret_cast<const float*>(calibrated_trace.s21.data()),
|
reinterpret_cast<const float*>(calibrated_s21.data()),
|
||||||
point_count,
|
point_count,
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
Eigen::Map<const InterleavedComplexView> reference_view(
|
Eigen::Map<const InterleavedComplexView> reference_view(
|
||||||
reinterpret_cast<const float*>(reference.s21.data()),
|
reinterpret_cast<const float*>(reference.samples.data()),
|
||||||
point_count,
|
point_count,
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
Eigen::Map<InterleavedComplexView> output_view(reinterpret_cast<float*>(output.s21.data()), point_count, 2);
|
Eigen::Map<InterleavedComplexView> output_view(reinterpret_cast<float*>(output.data()), point_count, 2);
|
||||||
|
|
||||||
output_view = calibrated_view - reference_view;
|
output_view = calibrated_view - reference_view;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ReferenceMaster::apply_s11(
|
||||||
|
const ipc::ComboKey& combo,
|
||||||
|
std::span<const float> frequency_hz,
|
||||||
|
const std::vector<ipc::Complex32>& calibrated_s11
|
||||||
|
) const -> std::vector<ipc::Complex32> {
|
||||||
|
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<ipc::Complex32> 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;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -18,6 +18,7 @@ struct ProcessingLiveConfig {
|
|||||||
std::string processor_mode = "pass_through";
|
std::string processor_mode = "pass_through";
|
||||||
float gain_db = 0.0F;
|
float gain_db = 0.0F;
|
||||||
float phase_deg = 0.0F;
|
float phase_deg = 0.0F;
|
||||||
|
std::string pass_through_channel = "s21";
|
||||||
bool pass_through_fixed_y_enabled = false;
|
bool pass_through_fixed_y_enabled = false;
|
||||||
float pass_through_y_min_db = -100.0F;
|
float pass_through_y_min_db = -100.0F;
|
||||||
float pass_through_y_max_db = 0.0F;
|
float pass_through_y_max_db = 0.0F;
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ using Json = nlohmann::json;
|
|||||||
throw std::runtime_error("processing.history_command must be one of: none, remove_last, clear_all");
|
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 {
|
||||||
|
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 {
|
[[nodiscard]] auto parse_u64_number(const Json& value, const std::string& field_name) -> std::uint64_t {
|
||||||
if (!value.is_number()) {
|
if (!value.is_number()) {
|
||||||
throw std::runtime_error(field_name + " must be number");
|
throw std::runtime_error(field_name + " must be number");
|
||||||
@@ -101,6 +108,12 @@ using Json = nlohmann::json;
|
|||||||
}
|
}
|
||||||
config.phase_deg = static_cast<float>(found->get<double>());
|
config.phase_deg = static_cast<float>(found->get<double>());
|
||||||
}
|
}
|
||||||
|
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<std::string>());
|
||||||
|
}
|
||||||
if (const auto found = root.find("pass_through_fixed_y_enabled"); found != root.end()) {
|
if (const auto found = root.find("pass_through_fixed_y_enabled"); found != root.end()) {
|
||||||
if (!found->is_boolean()) {
|
if (!found->is_boolean()) {
|
||||||
throw std::runtime_error("processing.pass_through_fixed_y_enabled must be bool");
|
throw std::runtime_error("processing.pass_through_fixed_y_enabled must be bool");
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ auto PassThroughProcessor::process_collection(
|
|||||||
payload.processing_name = name();
|
payload.processing_name = name();
|
||||||
payload.kind = ipc::ResultKind::TraceComplex;
|
payload.kind = ipc::ResultKind::TraceComplex;
|
||||||
payload.frequency_hz = trace.frequency_hz;
|
payload.frequency_hz = trace.frequency_hz;
|
||||||
|
if (live_config.pass_through_channel == "s11") {
|
||||||
|
payload.trace = trace.s11;
|
||||||
|
} else {
|
||||||
payload.trace = trace.s21;
|
payload.trace = trace.s21;
|
||||||
|
}
|
||||||
|
|
||||||
const float linear_gain = std::pow(10.0F, live_config.gain_db / 20.0F);
|
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);
|
const float phase_rad = live_config.phase_deg * (kPi / 180.0F);
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ class AppWindow(
|
|||||||
def _init_preprocess_state(self) -> None:
|
def _init_preprocess_state(self) -> None:
|
||||||
"""Initialize preprocessing dialog and selected set names."""
|
"""Initialize preprocessing dialog and selected set names."""
|
||||||
self._preprocess_dialog: PreprocessDialog | None = None
|
self._preprocess_dialog: PreprocessDialog | None = None
|
||||||
self._selected_calibration_set = str(self._defaults_config.preprocess.calibration_set)
|
self._selected_s21_calibration_set = str(self._defaults_config.preprocess.s21_calibration_set)
|
||||||
self._selected_reference_set = str(self._defaults_config.preprocess.reference_set)
|
self._selected_s21_reference_set = str(self._defaults_config.preprocess.s21_reference_set)
|
||||||
|
|
||||||
def _init_capture_state(self) -> None:
|
def _init_capture_state(self) -> None:
|
||||||
"""Initialize one-shot capture and sequence-control flags."""
|
"""Initialize one-shot capture and sequence-control flags."""
|
||||||
|
|||||||
@@ -118,8 +118,8 @@ class AppWindowConfigMixin:
|
|||||||
if self._switches_are_effectively_static(config):
|
if self._switches_are_effectively_static(config):
|
||||||
config.combos = [ComboModel(input=0, output=0)]
|
config.combos = [ComboModel(input=0, output=0)]
|
||||||
|
|
||||||
config.preprocess.calibration_set = self._selected_calibration_set
|
config.preprocess.s21_calibration_set = self._selected_s21_calibration_set
|
||||||
config.preprocess.reference_set = self._selected_reference_set
|
config.preprocess.s21_reference_set = self._selected_s21_reference_set
|
||||||
config.gpr.mode = self._gpr_config_mode.currentText()
|
config.gpr.mode = self._gpr_config_mode.currentText()
|
||||||
config.gpr.relative_permittivity = float(self._gpr_relative_permittivity.value())
|
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())
|
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(),
|
processor_mode=self._processing_mode.currentText(),
|
||||||
gain_db=float(self._processing_gain_db.value()),
|
gain_db=float(self._processing_gain_db.value()),
|
||||||
phase_deg=float(self._processing_phase_deg.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_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_min_db=min(y_min_db, y_max_db),
|
||||||
pass_through_y_max_db=max(y_min_db, y_max_db),
|
pass_through_y_max_db=max(y_min_db, y_max_db),
|
||||||
|
|||||||
@@ -38,28 +38,28 @@ class AppWindowPipelineMixin:
|
|||||||
run_signature = self._build_run_history_signature(config)
|
run_signature = self._build_run_history_signature(config)
|
||||||
radar_key = self._radar_key(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")
|
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]
|
combo_keys = [ComboKey(input_pos=combo.input, output_pos=combo.output) for combo in config.combos]
|
||||||
|
|
||||||
if not self._store.has_combo_coverage(
|
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")
|
raise RuntimeError("Selected calibration set does not cover requested run combos")
|
||||||
if not self._store.has_combo_coverage(
|
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")
|
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,
|
self._store,
|
||||||
radar_key,
|
radar_key,
|
||||||
config.preprocess.calibration_set,
|
config.preprocess.s21_calibration_set,
|
||||||
config.preprocess.reference_set,
|
config.preprocess.s21_reference_set,
|
||||||
)
|
)
|
||||||
config.preprocess.calibration_bundle_path = str(calibration_bundle)
|
config.preprocess.s21_calibration_bundle_path = str(calibration_bundle)
|
||||||
config.preprocess.reference_bundle_path = str(reference_bundle)
|
config.preprocess.s21_reference_bundle_path = str(reference_bundle)
|
||||||
config.runtime.continuous = not single_capture
|
config.runtime.continuous = not single_capture
|
||||||
|
|
||||||
if not single_capture:
|
if not single_capture:
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ class AppWindowPlotMixin:
|
|||||||
show_phase = self._show_phase_curves()
|
show_phase = self._show_phase_curves()
|
||||||
magnitude_plot = self._trace_magnitude_plot
|
magnitude_plot = self._trace_magnitude_plot
|
||||||
phase_plot = self._trace_phase_plot
|
phase_plot = self._trace_phase_plot
|
||||||
|
pass_through_channel = self._pass_through_channel.currentText().upper()
|
||||||
|
|
||||||
magnitude_plot.setVisible(show_magnitude)
|
magnitude_plot.setVisible(show_magnitude)
|
||||||
phase_plot.setVisible(show_phase)
|
phase_plot.setVisible(show_phase)
|
||||||
@@ -119,6 +120,7 @@ class AppWindowPlotMixin:
|
|||||||
mag_item.showAxis("left", show=True)
|
mag_item.showAxis("left", show=True)
|
||||||
mag_item.showAxis("bottom", show=not show_phase)
|
mag_item.showAxis("bottom", show=not show_phase)
|
||||||
magnitude_plot.setLabel("left", "Magnitude", units="dB")
|
magnitude_plot.setLabel("left", "Magnitude", units="dB")
|
||||||
|
magnitude_plot.setTitle(f"Pass-Through {pass_through_channel}")
|
||||||
if not show_phase:
|
if not show_phase:
|
||||||
magnitude_plot.setLabel("bottom", "Frequency", units="Hz")
|
magnitude_plot.setLabel("bottom", "Frequency", units="Hz")
|
||||||
|
|
||||||
@@ -130,6 +132,7 @@ class AppWindowPlotMixin:
|
|||||||
phase_item.showAxis("bottom", show=True)
|
phase_item.showAxis("bottom", show=True)
|
||||||
phase_plot.setLabel("left", "Phase", units="deg")
|
phase_plot.setLabel("left", "Phase", units="deg")
|
||||||
phase_plot.setLabel("bottom", "Frequency", units="Hz")
|
phase_plot.setLabel("bottom", "Frequency", units="Hz")
|
||||||
|
phase_plot.setTitle(f"Pass-Through {pass_through_channel}")
|
||||||
|
|
||||||
palette = [
|
palette = [
|
||||||
"#4cc9f0",
|
"#4cc9f0",
|
||||||
|
|||||||
@@ -40,14 +40,14 @@ class AppWindowPreprocessMixin:
|
|||||||
|
|
||||||
def _on_preprocess_selection_changed(self, calibration_set: str, reference_set: str) -> None:
|
def _on_preprocess_selection_changed(self, calibration_set: str, reference_set: str) -> None:
|
||||||
"""Persist selected preprocessing set names from dialog."""
|
"""Persist selected preprocessing set names from dialog."""
|
||||||
self._selected_calibration_set = calibration_set.strip()
|
self._selected_s21_calibration_set = calibration_set.strip()
|
||||||
self._selected_reference_set = reference_set.strip()
|
self._selected_s21_reference_set = reference_set.strip()
|
||||||
self._refresh_preprocess_summary_labels()
|
self._refresh_preprocess_summary_labels()
|
||||||
|
|
||||||
def _refresh_preprocess_summary_labels(self) -> None:
|
def _refresh_preprocess_summary_labels(self) -> None:
|
||||||
"""Update compact summary labels in the main window."""
|
"""Update compact summary labels in the main window."""
|
||||||
self._selected_calibration_label.setText(self._selected_calibration_set or "<not selected>")
|
self._selected_calibration_label.setText(self._selected_s21_calibration_set or "<not selected>")
|
||||||
self._selected_reference_label.setText(self._selected_reference_set or "<not selected>")
|
self._selected_reference_label.setText(self._selected_s21_reference_set or "<not selected>")
|
||||||
|
|
||||||
def _refresh_sets(self) -> None:
|
def _refresh_sets(self) -> None:
|
||||||
"""Refresh calibration/reference set lists for current radar key."""
|
"""Refresh calibration/reference set lists for current radar key."""
|
||||||
@@ -60,12 +60,12 @@ class AppWindowPreprocessMixin:
|
|||||||
dialog.set_calibration_sets(calibration_sets)
|
dialog.set_calibration_sets(calibration_sets)
|
||||||
dialog.set_reference_sets(reference_sets)
|
dialog.set_reference_sets(reference_sets)
|
||||||
|
|
||||||
if self._selected_calibration_set not in calibration_sets:
|
if self._selected_s21_calibration_set not in calibration_sets:
|
||||||
self._selected_calibration_set = calibration_sets[0] if calibration_sets else ""
|
self._selected_s21_calibration_set = calibration_sets[0] if calibration_sets else ""
|
||||||
if self._selected_reference_set not in reference_sets:
|
if self._selected_s21_reference_set not in reference_sets:
|
||||||
self._selected_reference_set = reference_sets[0] if reference_sets else ""
|
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._refresh_preprocess_summary_labels()
|
||||||
self._log(f"Set lists refreshed for key={radar_key}")
|
self._log(f"Set lists refreshed for key={radar_key}")
|
||||||
|
|
||||||
@@ -147,9 +147,9 @@ class AppWindowPreprocessMixin:
|
|||||||
self._cleanup_capture_session()
|
self._cleanup_capture_session()
|
||||||
|
|
||||||
if kind == "calibration":
|
if kind == "calibration":
|
||||||
self._selected_calibration_set = set_name
|
self._selected_s21_calibration_set = set_name
|
||||||
else:
|
else:
|
||||||
self._selected_reference_set = set_name
|
self._selected_s21_reference_set = set_name
|
||||||
|
|
||||||
self._refresh_sets()
|
self._refresh_sets()
|
||||||
dialog.set_status(f"{kind.title()} set saved: {set_name} ({len(collection.traces)} traces)")
|
dialog.set_status(f"{kind.title()} set saved: {set_name} ({len(collection.traces)} traces)")
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class AppWindowUiMixin:
|
|||||||
|
|
||||||
# Default view on startup is pass-through traces.
|
# Default view on startup is pass-through traces.
|
||||||
self._plot_stack.setCurrentWidget(self._trace_plots_container)
|
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:
|
def _build_bscan_plot_page(self) -> None:
|
||||||
"""Create B-scan page in plot stack."""
|
"""Create B-scan page in plot stack."""
|
||||||
@@ -141,7 +141,7 @@ class AppWindowUiMixin:
|
|||||||
def _build_settings_panel(self, root_layout: QHBoxLayout, root: QWidget) -> None:
|
def _build_settings_panel(self, root_layout: QHBoxLayout, root: QWidget) -> None:
|
||||||
"""Build right settings panel with controls, status labels, and log."""
|
"""Build right settings panel with controls, status labels, and log."""
|
||||||
self._settings_panel = QWidget(root)
|
self._settings_panel = QWidget(root)
|
||||||
self._settings_panel.setMinimumWidth(659)
|
self._settings_panel.setMinimumWidth(530)
|
||||||
right_layout = QVBoxLayout(self._settings_panel)
|
right_layout = QVBoxLayout(self._settings_panel)
|
||||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
right_layout.setSpacing(10)
|
right_layout.setSpacing(10)
|
||||||
@@ -163,7 +163,7 @@ class AppWindowUiMixin:
|
|||||||
right_layout.addWidget(self._history_label)
|
right_layout.addWidget(self._history_label)
|
||||||
|
|
||||||
right_layout.addWidget(self._log_box, stretch=0)
|
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:
|
def _build_settings_scroll(self) -> QScrollArea:
|
||||||
"""Build scroll area with all control groups in display order."""
|
"""Build scroll area with all control groups in display order."""
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt6.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QSpinBox, QVBoxLayout
|
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 = QVBoxLayout(group)
|
||||||
layout.setSpacing(8)
|
layout.setSpacing(8)
|
||||||
|
|
||||||
save_row = QHBoxLayout()
|
save_button = QPushButton("Save Snapshot")
|
||||||
save_row.setSpacing(8)
|
|
||||||
|
|
||||||
save_button = QPushButton("Save Numpy Snapshot")
|
|
||||||
save_button.clicked.connect(owner._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)
|
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)
|
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)
|
clear_history_button.clicked.connect(owner._clear_all_runtime_history)
|
||||||
owner._save_count = QSpinBox()
|
owner._save_count = QSpinBox()
|
||||||
owner._save_count.setMinimum(1)
|
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.setMaximum(65_535)
|
||||||
owner._vna_json_output_index.setValue(0)
|
owner._vna_json_output_index.setValue(0)
|
||||||
|
|
||||||
save_row.addWidget(save_button)
|
button_column = QVBoxLayout()
|
||||||
save_row.addWidget(save_vna_json_button)
|
button_column.setSpacing(8)
|
||||||
save_row.addWidget(remove_last_button)
|
button_column.addWidget(save_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||||
save_row.addWidget(clear_history_button)
|
button_column.addWidget(save_vna_json_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||||
save_row.addWidget(QLabel("Last N"))
|
button_column.addWidget(remove_last_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||||
save_row.addWidget(owner._save_count)
|
button_column.addWidget(clear_history_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||||
save_row.addStretch(1)
|
layout.addLayout(button_column)
|
||||||
layout.addLayout(save_row)
|
|
||||||
|
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 = QHBoxLayout()
|
||||||
json_row.setSpacing(8)
|
json_row.setSpacing(8)
|
||||||
|
|||||||
@@ -2,24 +2,25 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
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:
|
def build_hardware_actions_group(owner) -> QGroupBox:
|
||||||
"""Create hardware action buttons section."""
|
"""Create hardware action buttons section."""
|
||||||
group = QGroupBox("Hardware Actions")
|
group = QGroupBox("Hardware Actions")
|
||||||
layout = QHBoxLayout(group)
|
layout = QVBoxLayout(group)
|
||||||
layout.setSpacing(8)
|
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)
|
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)
|
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)
|
preprocess_button.clicked.connect(owner._open_preprocess_panel)
|
||||||
layout.addWidget(preprocess_button)
|
layout.addWidget(preprocess_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||||
return group
|
return group
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
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:
|
def build_pipeline_group(owner) -> QGroupBox:
|
||||||
@@ -11,22 +12,17 @@ def build_pipeline_group(owner) -> QGroupBox:
|
|||||||
layout = QVBoxLayout(group)
|
layout = QVBoxLayout(group)
|
||||||
layout.setSpacing(8)
|
layout.setSpacing(8)
|
||||||
|
|
||||||
action_row = QHBoxLayout()
|
|
||||||
action_row.setSpacing(8)
|
|
||||||
|
|
||||||
start_button = QPushButton("Start")
|
start_button = QPushButton("Start")
|
||||||
start_button.clicked.connect(owner._start_run)
|
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 = QPushButton("Single Capture")
|
||||||
single_button.clicked.connect(owner._start_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 = QPushButton("Stop")
|
||||||
stop_button.clicked.connect(owner._stop_run)
|
stop_button.clicked.connect(owner._stop_run)
|
||||||
action_row.addWidget(stop_button)
|
layout.addWidget(stop_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||||
|
|
||||||
layout.addLayout(action_row)
|
|
||||||
|
|
||||||
hint = QLabel("Start continuous run or single processed collection capture.")
|
hint = QLabel("Start continuous run or single processed collection capture.")
|
||||||
hint.setObjectName("hintLabel")
|
hint.setObjectName("hintLabel")
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ def build_processing_group(owner) -> QGroupBox:
|
|||||||
owner._processing_phase_deg.setSingleStep(1.0)
|
owner._processing_phase_deg.setSingleStep(1.0)
|
||||||
owner._processing_phase_deg.setValue(0.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 = QCheckBox("Show magnitude")
|
||||||
owner._show_magnitude_checkbox.setChecked(True)
|
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("Gain dB (live)", owner._processing_gain_db)
|
||||||
pass_through_form.addRow("Phase deg (live)", owner._processing_phase_deg)
|
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_magnitude_checkbox)
|
||||||
pass_through_form.addRow(owner._show_phase_checkbox)
|
pass_through_form.addRow(owner._show_phase_checkbox)
|
||||||
pass_through_form.addRow(owner._pass_through_fixed_y_enabled)
|
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_mode.currentTextChanged.connect(owner._on_processing_mode_changed)
|
||||||
owner._processing_gain_db.valueChanged.connect(owner._on_processing_live_settings_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._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_magnitude_checkbox.toggled.connect(owner._on_trace_visibility_changed)
|
||||||
owner._show_phase_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)
|
owner._pass_through_fixed_y_enabled.toggled.connect(sync_pass_through_y_controls)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ def main() -> int:
|
|||||||
apply_dark_theme(app)
|
apply_dark_theme(app)
|
||||||
pg.setConfigOptions(antialias=True, foreground="#dbe4f1")
|
pg.setConfigOptions(antialias=True, foreground="#dbe4f1")
|
||||||
window = AppWindow(PROJECT_ROOT)
|
window = AppWindow(PROJECT_ROOT)
|
||||||
window.show()
|
window.showMaximized()
|
||||||
return app.exec()
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ def build_run_history_signature(
|
|||||||
str(config.output_switch.driver),
|
str(config.output_switch.driver),
|
||||||
int(config.output_switch.positions),
|
int(config.output_switch.positions),
|
||||||
bool(config.output_switch.invert_logic),
|
bool(config.output_switch.invert_logic),
|
||||||
str(config.preprocess.calibration_set),
|
str(config.preprocess.s21_calibration_set),
|
||||||
str(config.preprocess.reference_set),
|
str(config.preprocess.s21_reference_set),
|
||||||
combos_signature,
|
combos_signature,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
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.s21_calibration_set = str(
|
||||||
model.preprocess.reference_set = str(preprocess_payload.get("reference_set", model.preprocess.reference_set))
|
preprocess_payload.get("s21_calibration_set", model.preprocess.s21_calibration_set)
|
||||||
model.preprocess.calibration_bundle_path = str(
|
)
|
||||||
preprocess_payload.get("calibration_bundle_path", model.preprocess.calibration_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.preprocess.reference_bundle_path = str(
|
|
||||||
preprocess_payload.get("reference_bundle_path", model.preprocess.reference_bundle_path)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
model.gpr.mode = str(gpr_payload.get("mode", model.gpr.mode))
|
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()
|
model.ensure_combos()
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]:
|
def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]:
|
||||||
"""Encode :class:`RunConfigModel` to C++ pipeline-compatible JSON structure."""
|
"""Encode :class:`RunConfigModel` to C++ pipeline-compatible JSON structure."""
|
||||||
model.ensure_combos()
|
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],
|
"combos": [{"input": combo.input, "output": combo.output} for combo in model.combos],
|
||||||
},
|
},
|
||||||
"preprocess": {
|
"preprocess": {
|
||||||
"calibration_set": model.preprocess.calibration_set,
|
"s21_calibration_set": model.preprocess.s21_calibration_set,
|
||||||
"reference_set": model.preprocess.reference_set,
|
"s21_reference_set": model.preprocess.s21_reference_set,
|
||||||
"calibration_bundle_path": model.preprocess.calibration_bundle_path,
|
"s21_calibration_bundle_path": model.preprocess.s21_calibration_bundle_path,
|
||||||
"reference_bundle_path": model.preprocess.reference_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": {
|
"gpr": {
|
||||||
"mode": model.gpr.mode,
|
"mode": model.gpr.mode,
|
||||||
|
|||||||
@@ -89,10 +89,14 @@ class RuntimeModel:
|
|||||||
class PreprocessModel:
|
class PreprocessModel:
|
||||||
"""Selected preprocessing artifacts for live acquisition."""
|
"""Selected preprocessing artifacts for live acquisition."""
|
||||||
|
|
||||||
calibration_set: str = ""
|
s21_calibration_set: str = ""
|
||||||
reference_set: str = ""
|
s21_reference_set: str = ""
|
||||||
calibration_bundle_path: str = ""
|
s21_calibration_bundle_path: str = ""
|
||||||
reference_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)
|
@dataclass(slots=True)
|
||||||
|
|||||||
@@ -17,19 +17,19 @@ class ConfigWriter:
|
|||||||
self._runtime_dir = runtime_dir
|
self._runtime_dir = runtime_dir
|
||||||
self._runtime_dir.mkdir(parents=True, exist_ok=True)
|
self._runtime_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
def prepare_bundles(
|
def prepare_s21_bundles(
|
||||||
self,
|
self,
|
||||||
store: NpzStore,
|
store: NpzStore,
|
||||||
radar_key: str,
|
radar_key: str,
|
||||||
calibration_set: str,
|
s21_calibration_set: str,
|
||||||
reference_set: str,
|
s21_reference_set: str,
|
||||||
) -> tuple[Path, Path]:
|
) -> tuple[Path, Path]:
|
||||||
"""Export calibration/reference sets into binary bundles for preprocessor."""
|
"""Export calibration/reference sets into binary bundles for preprocessor."""
|
||||||
calibration_bundle = self._runtime_dir / "calibration_bundle.bin"
|
calibration_bundle = self._runtime_dir / "s21_calibration_bundle.bin"
|
||||||
reference_bundle = self._runtime_dir / "reference_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("calibration", radar_key, s21_calibration_set, calibration_bundle)
|
||||||
store.export_set_bundle("reference", radar_key, reference_set, reference_bundle)
|
store.export_set_bundle("reference", radar_key, s21_reference_set, reference_bundle)
|
||||||
return calibration_bundle, reference_bundle
|
return calibration_bundle, reference_bundle
|
||||||
|
|
||||||
def write(self, config: RunConfigModel, output_path: Path) -> Path:
|
def write(self, config: RunConfigModel, output_path: Path) -> Path:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class ProcessingLiveConfig:
|
|||||||
processor_mode: str = "pass_through"
|
processor_mode: str = "pass_through"
|
||||||
gain_db: float = 0.0
|
gain_db: float = 0.0
|
||||||
phase_deg: float = 0.0
|
phase_deg: float = 0.0
|
||||||
|
pass_through_channel: str = "s21"
|
||||||
pass_through_fixed_y_enabled: bool = False
|
pass_through_fixed_y_enabled: bool = False
|
||||||
pass_through_y_min_db: float = -100.0
|
pass_through_y_min_db: float = -100.0
|
||||||
pass_through_y_max_db: float = 0.0
|
pass_through_y_max_db: float = 0.0
|
||||||
@@ -52,6 +53,7 @@ class ProcessingLiveConfig:
|
|||||||
"processor_mode": str(self.processor_mode),
|
"processor_mode": str(self.processor_mode),
|
||||||
"gain_db": float(self.gain_db),
|
"gain_db": float(self.gain_db),
|
||||||
"phase_deg": float(self.phase_deg),
|
"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_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_min_db": float(self.pass_through_y_min_db),
|
||||||
"pass_through_y_max_db": float(self.pass_through_y_max_db),
|
"pass_through_y_max_db": float(self.pass_through_y_max_db),
|
||||||
|
|||||||
@@ -79,10 +79,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"preprocess": {
|
"preprocess": {
|
||||||
"calibration_set": "smoke_cal",
|
"s21_calibration_set": "smoke_cal",
|
||||||
"reference_set": "smoke_ref",
|
"s21_reference_set": "smoke_ref",
|
||||||
"calibration_bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/calibration_bundle.bin",
|
"s21_calibration_bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s21_calibration_bundle.bin",
|
||||||
"reference_bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/reference_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": {
|
"gpr": {
|
||||||
"mode": "point",
|
"mode": "point",
|
||||||
|
|||||||
@@ -113,11 +113,16 @@ def main() -> int:
|
|||||||
store.save_set("calibration", radar_key, "smoke_cal", calibration_set)
|
store.save_set("calibration", radar_key, "smoke_cal", calibration_set)
|
||||||
store.save_set("reference", radar_key, "smoke_ref", reference_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")
|
calibration_bundle, reference_bundle = config_writer.prepare_s21_bundles(
|
||||||
config.preprocess.calibration_set = "smoke_cal"
|
store,
|
||||||
config.preprocess.reference_set = "smoke_ref"
|
radar_key,
|
||||||
config.preprocess.calibration_bundle_path = str(calibration_bundle)
|
"smoke_cal",
|
||||||
config.preprocess.reference_bundle_path = str(reference_bundle)
|
"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")
|
config_path = config_writer.write(config, project_root / "python_app/runtime/run_config_smoke.json")
|
||||||
|
|
||||||
|
|||||||
+8
-4
@@ -55,10 +55,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"preprocess": {
|
"preprocess": {
|
||||||
"calibration_set": "",
|
"s21_calibration_set": "",
|
||||||
"reference_set": "",
|
"s21_reference_set": "",
|
||||||
"calibration_bundle_path": "python_app/runtime/calibration_bundle.bin",
|
"s21_calibration_bundle_path": "python_app/runtime/s21_calibration_bundle.bin",
|
||||||
"reference_bundle_path": "python_app/runtime/reference_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": {
|
"gpr": {
|
||||||
"mode": "point",
|
"mode": "point",
|
||||||
|
|||||||
Reference in New Issue
Block a user