added pass through s11

This commit is contained in:
Ayzen
2026-03-26 17:21:25 +03:00
parent 24f7ebb2fb
commit 9ddbde22bd
38 changed files with 945 additions and 291 deletions
@@ -75,10 +75,14 @@ struct RuntimeConfig {
struct PreprocessConfig {
// Names and bundle paths selected by Python GUI layer.
std::string calibration_set{};
std::string reference_set{};
std::string calibration_bundle_path{};
std::string reference_bundle_path{};
std::string s21_calibration_set{};
std::string s21_reference_set{};
std::string s21_calibration_bundle_path{};
std::string s21_reference_bundle_path{};
std::string s11_open_calibration_bundle_path{};
std::string s11_short_calibration_bundle_path{};
std::string s11_load_calibration_bundle_path{};
std::string s11_reference_bundle_path{};
};
struct GprTxGeometry {
@@ -438,10 +438,18 @@ auto load_run_config(const std::string& path) -> RunConfig {
{
const auto* preprocess_obj = as_object(required_field(*root_obj, "preprocess"), "preprocess");
config.preprocess.calibration_set = optional_string(*preprocess_obj, "calibration_set", "");
config.preprocess.reference_set = optional_string(*preprocess_obj, "reference_set", "");
config.preprocess.calibration_bundle_path = optional_string(*preprocess_obj, "calibration_bundle_path", "");
config.preprocess.reference_bundle_path = optional_string(*preprocess_obj, "reference_bundle_path", "");
config.preprocess.s21_calibration_set = optional_string(*preprocess_obj, "s21_calibration_set", "");
config.preprocess.s21_reference_set = optional_string(*preprocess_obj, "s21_reference_set", "");
config.preprocess.s21_calibration_bundle_path =
optional_string(*preprocess_obj, "s21_calibration_bundle_path", "");
config.preprocess.s21_reference_bundle_path = optional_string(*preprocess_obj, "s21_reference_bundle_path", "");
config.preprocess.s11_open_calibration_bundle_path =
optional_string(*preprocess_obj, "s11_open_calibration_bundle_path", "");
config.preprocess.s11_short_calibration_bundle_path =
optional_string(*preprocess_obj, "s11_short_calibration_bundle_path", "");
config.preprocess.s11_load_calibration_bundle_path =
optional_string(*preprocess_obj, "s11_load_calibration_bundle_path", "");
config.preprocess.s11_reference_bundle_path = optional_string(*preprocess_obj, "s11_reference_bundle_path", "");
}
if (const auto* gpr_value = optional_field(*root_obj, "gpr"); gpr_value != nullptr) {
@@ -1,32 +1,51 @@
#pragma once
#include <memory>
#include <span>
#include <string>
#include <unordered_map>
#include <vector>
#include "calibrator_interface.hpp"
#include "channel_bundle_support.hpp"
#include "shared_types.hpp"
namespace radar::preprocessing {
class CalibrationMaster {
public:
// `calibrator` encapsulates the actual calibration algorithm (v1: through).
explicit CalibrationMaster(std::unique_ptr<CalibratorInterface> calibrator);
// `s21_calibrator` encapsulates the S21 calibration algorithm.
explicit CalibrationMaster(std::unique_ptr<CalibratorInterface> s21_calibrator);
// Loads a serialized raw sweep bundle with one calibration standard per combo.
void load_bundle(const std::string& path);
// Loads a serialized raw sweep bundle with one S21 calibration standard per combo.
void load_s21_calibration_bundle(const std::string& path);
// Loads optional one-port OSL calibration data for S11 from raw sweep bundles.
void load_s11_calibration_bundle(
const std::string& open_path,
const std::string& short_path,
const std::string& load_path
);
// Ensures all runtime combos are present in loaded standards.
void validate_combos(const std::vector<ipc::ComboKey>& combos) const;
// Applies calibration standard corresponding to measured trace combo.
[[nodiscard]] auto apply(const ipc::SweepTraceBlock& measured_trace) const -> ipc::SweepTraceBlock;
// Applies both channel calibrations to one measured trace.
[[nodiscard]] auto apply_to_trace(const ipc::SweepTraceBlock& measured_trace) const -> ipc::SweepTraceBlock;
[[nodiscard]] auto apply_s21(
const ipc::ComboKey& combo,
std::span<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:
std::unique_ptr<CalibratorInterface> calibrator_impl_;
std::unordered_map<ipc::ComboKey, ipc::SweepTraceBlock, ipc::ComboKeyHash> standards_by_combo_{};
std::unique_ptr<CalibratorInterface> s21_calibrator_;
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
@@ -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
@@ -1,107 +1,60 @@
#include "calibration_master.hpp"
#include <cstdint>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <vector>
namespace radar::preprocessing {
namespace {
[[nodiscard]] auto combo_to_string(const ipc::ComboKey& combo) -> std::string {
return "input=" + std::to_string(combo.input_pos) + " output=" + std::to_string(combo.output_pos);
}
[[nodiscard]] auto read_binary_file(const std::string& path, const std::string& bundle_label)
-> std::vector<std::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_) {
CalibrationMaster::CalibrationMaster(std::unique_ptr<CalibratorInterface> s21_calibrator)
: s21_calibrator_(std::move(s21_calibrator)) {
if (!s21_calibrator_) {
throw std::runtime_error("CalibrationMaster requires a non-null calibrator");
}
}
void CalibrationMaster::load_bundle(const std::string& path) {
if (path.empty()) {
throw std::runtime_error("Calibration bundle path must not be empty");
}
void CalibrationMaster::load_s21_calibration_bundle(const std::string& path) {
s21_calibration_bundle_.load(path);
}
const auto bytes = read_binary_file(path, "calibration");
if (bytes.empty()) {
throw std::runtime_error("Calibration bundle is empty: " + path);
}
const auto collection = ipc::deserialize_raw_collection(bytes);
standards_by_combo_.clear();
standards_by_combo_.reserve(collection.traces.size());
for (const auto& trace : collection.traces) {
validate_trace_layout(trace, "Calibration standard");
standards_by_combo_.insert_or_assign(trace.combo, trace);
}
if (standards_by_combo_.empty()) {
throw std::runtime_error("Calibration bundle does not contain traces: " + path);
}
void CalibrationMaster::load_s11_calibration_bundle(
const std::string& open_path,
const std::string& short_path,
const std::string& load_path
) {
s11_calibration_bundle_.load(open_path, short_path, load_path);
}
void CalibrationMaster::validate_combos(const std::vector<ipc::ComboKey>& combos) const {
for (const auto& combo : combos) {
if (!standards_by_combo_.contains(combo)) {
throw std::runtime_error("Calibration data is missing for combo " + combo_to_string(combo));
}
}
s21_calibration_bundle_.validate_combos(combos);
s11_calibration_bundle_.validate_combos(combos);
}
auto CalibrationMaster::apply(const ipc::SweepTraceBlock& measured_trace) const -> ipc::SweepTraceBlock {
validate_trace_layout(measured_trace, "Measured trace");
const auto found = standards_by_combo_.find(measured_trace.combo);
if (found == standards_by_combo_.end()) {
throw std::runtime_error(
"Calibration standard is missing for measured combo " + combo_to_string(measured_trace.combo)
);
}
const auto& standard = found->second;
validate_trace_layout(standard, "Calibration standard");
if (measured_trace.s21.size() != standard.s21.size()) {
throw std::runtime_error(
"Calibration standard point count mismatch for combo " + combo_to_string(measured_trace.combo)
);
}
auto CalibrationMaster::apply_to_trace(const ipc::SweepTraceBlock& measured_trace) const -> ipc::SweepTraceBlock {
ipc::SweepTraceBlock output{};
output.combo = measured_trace.combo;
output.frequency_hz = measured_trace.frequency_hz;
output.s11 = measured_trace.s11;
output.s21 = calibrator_impl_->apply(measured_trace.s21, standard.s21);
output.s21 = apply_s21(measured_trace.combo, measured_trace.frequency_hz, measured_trace.s21);
output.s11 = apply_s11(measured_trace.combo, measured_trace.frequency_hz, measured_trace.s11);
return output;
}
auto CalibrationMaster::apply_s21(
const ipc::ComboKey& combo,
std::span<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
@@ -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
auto make_through_calibrator() -> std::unique_ptr<CalibratorInterface> {
auto make_s21_through_calibrator() -> std::unique_ptr<CalibratorInterface> {
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) {
// Pipeline order is fixed: calibration first, then reference subtraction.
const auto calibrated = calibration_master_.apply(raw_trace);
const auto referenced = reference_master_.apply(calibrated);
const auto calibrated = calibration_master_.apply_to_trace(raw_trace);
const auto referenced = reference_master_.apply_to_trace(calibrated);
preprocessed.traces.push_back(referenced);
}
@@ -62,12 +62,20 @@ int main(int argc, char** argv) {
);
// Load preprocessing assets once before entering run loop.
radar::preprocessing::CalibrationMaster calibration_master(radar::preprocessing::make_through_calibrator());
calibration_master.load_bundle(config.preprocess.calibration_bundle_path);
radar::preprocessing::CalibrationMaster calibration_master(
radar::preprocessing::make_s21_through_calibrator()
);
calibration_master.load_s21_calibration_bundle(config.preprocess.s21_calibration_bundle_path);
calibration_master.load_s11_calibration_bundle(
config.preprocess.s11_open_calibration_bundle_path,
config.preprocess.s11_short_calibration_bundle_path,
config.preprocess.s11_load_calibration_bundle_path
);
radar::preprocessing::ReferenceMaster reference_master;
reference_master.load_bundle(config.preprocess.reference_bundle_path);
reference_master.prepare_calibrated(calibration_master);
reference_master.load_s21_reference_bundle(config.preprocess.s21_reference_bundle_path);
reference_master.load_s11_reference_bundle(config.preprocess.s11_reference_bundle_path);
reference_master.prepare_calibrated(calibration_master, config.run_combos);
radar::preprocessing::DataPreprocessor preprocessor(
config,
@@ -1,9 +1,11 @@
#pragma once
#include <span>
#include <string>
#include <unordered_map>
#include <vector>
#include "channel_bundle_support.hpp"
#include "shared_types.hpp"
namespace radar::preprocessing {
@@ -12,19 +14,33 @@ class CalibrationMaster;
class ReferenceMaster {
public:
// Loads a serialized raw sweep bundle with one reference trace per combo.
void load_bundle(const std::string& path);
// Loads a serialized raw sweep bundle with one S21 reference trace per combo.
void load_s21_reference_bundle(const std::string& path);
// Loads optional raw S11 reference data.
void load_s11_reference_bundle(const std::string& path);
// Builds calibrated references in memory using currently loaded raw references.
// Must be called after load_bundle() and after calibration standards are loaded.
void prepare_calibrated(const CalibrationMaster& calibration_master);
// Must be called after load_s21_reference_bundle() and after calibration standards are loaded.
void prepare_calibrated(const CalibrationMaster& calibration_master, const std::vector<ipc::ComboKey>& combos);
// Ensures all runtime combos are present in loaded references.
void validate_combos(const std::vector<ipc::ComboKey>& combos) const;
// Subtracts per-combo reference trace from calibrated trace.
[[nodiscard]] auto apply(const ipc::SweepTraceBlock& calibrated_trace) const -> ipc::SweepTraceBlock;
// Subtracts both channel references from one calibrated trace.
[[nodiscard]] auto apply_to_trace(const ipc::SweepTraceBlock& calibrated_trace) const -> ipc::SweepTraceBlock;
[[nodiscard]] auto apply_s21(
const ipc::ComboKey& combo,
std::span<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:
std::unordered_map<ipc::ComboKey, ipc::SweepTraceBlock, ipc::ComboKeyHash> raw_references_by_combo_{};
std::unordered_map<ipc::ComboKey, ipc::SweepTraceBlock, ipc::ComboKeyHash> calibrated_references_by_combo_{};
S21ReferenceBundle raw_s21_reference_bundle_{};
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
@@ -1,8 +1,8 @@
#include "reference_master.hpp"
#include <cstdint>
#include <fstream>
#include <iterator>
#include <algorithm>
#include <cmath>
#include <span>
#include <stdexcept>
#include <string>
#include <utility>
@@ -15,132 +15,195 @@
namespace radar::preprocessing {
namespace {
constexpr float kFrequencyRelativeTolerance = 1e-5F;
constexpr float kFrequencyAbsoluteTolerance = 1e-3F;
[[nodiscard]] auto combo_to_string(const ipc::ComboKey& combo) -> std::string {
return "input=" + std::to_string(combo.input_pos) + " output=" + std::to_string(combo.output_pos);
}
[[nodiscard]] auto read_binary_file(const std::string& path, const std::string& bundle_label)
-> std::vector<std::uint8_t> {
std::ifstream stream(path, std::ios::binary);
if (!stream.is_open()) {
throw std::runtime_error("Failed to open " + bundle_label + " bundle: " + path);
void validate_channel_trace_layout(
const ChannelTrace& trace,
const std::string& trace_label,
const ipc::ComboKey& combo
) {
if (trace.frequency_hz.size() != trace.samples.size()) {
throw std::runtime_error(
trace_label + " frequency/channel vector size mismatch for combo " + combo_to_string(combo)
);
}
return std::vector<std::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)
);
[[nodiscard]] auto frequency_axes_match(std::span<const float> left, std::span<const float> right) -> bool {
if (left.size() != right.size()) {
return false;
}
if (trace.frequency_hz.size() != trace.s11.size()) {
throw std::runtime_error(
trace_label + " frequency/S11 vector size mismatch for combo " + combo_to_string(trace.combo)
);
for (std::size_t index = 0; index < left.size(); ++index) {
const auto delta = std::fabs(left[index] - right[index]);
const auto scale = std::max(std::fabs(left[index]), std::fabs(right[index]));
const auto tolerance = std::max(kFrequencyAbsoluteTolerance, scale * kFrequencyRelativeTolerance);
if (delta > tolerance) {
return false;
}
}
return true;
}
} // namespace
void ReferenceMaster::load_bundle(const std::string& path) {
if (path.empty()) {
throw std::runtime_error("Reference bundle path must not be empty");
}
const auto bytes = read_binary_file(path, "reference");
if (bytes.empty()) {
throw std::runtime_error("Reference bundle is empty: " + path);
}
const auto collection = ipc::deserialize_raw_collection(bytes);
raw_references_by_combo_.clear();
raw_references_by_combo_.reserve(collection.traces.size());
calibrated_references_by_combo_.clear();
for (const auto& trace : collection.traces) {
validate_trace_layout(trace, "Reference trace");
raw_references_by_combo_.insert_or_assign(trace.combo, trace);
}
if (raw_references_by_combo_.empty()) {
throw std::runtime_error("Reference bundle does not contain traces: " + path);
}
void ReferenceMaster::load_s21_reference_bundle(const std::string& path) {
raw_s21_reference_bundle_.load(path);
calibrated_s21_references_by_combo_.clear();
}
void ReferenceMaster::prepare_calibrated(const CalibrationMaster& calibration_master) {
if (raw_references_by_combo_.empty()) {
void ReferenceMaster::load_s11_reference_bundle(const std::string& path) {
raw_s11_reference_bundle_.load(path);
calibrated_s11_references_by_combo_.clear();
}
void ReferenceMaster::prepare_calibrated(
const CalibrationMaster& calibration_master,
const std::vector<ipc::ComboKey>& combos
) {
if (!raw_s21_reference_bundle_.is_enabled()) {
throw std::runtime_error("Reference bundle must be loaded before prepare_calibrated()");
}
calibrated_references_by_combo_.clear();
calibrated_references_by_combo_.reserve(raw_references_by_combo_.size());
for (const auto& [combo, raw_reference] : raw_references_by_combo_) {
auto calibrated = calibration_master.apply(raw_reference);
calibrated.combo = combo;
calibrated_references_by_combo_.insert_or_assign(combo, std::move(calibrated));
calibrated_s21_references_by_combo_.clear();
calibrated_s21_references_by_combo_.reserve(combos.size());
for (const auto& combo : combos) {
const auto& raw_reference = raw_s21_reference_bundle_.resolve(combo);
ChannelTrace calibrated{};
calibrated.frequency_hz = raw_reference.frequency_hz;
calibrated.samples = calibration_master.apply_s21(combo, raw_reference.frequency_hz, raw_reference.samples);
calibrated_s21_references_by_combo_.insert_or_assign(combo, std::move(calibrated));
}
calibrated_s11_references_by_combo_.clear();
if (!raw_s11_reference_bundle_.is_enabled()) {
return;
}
if (!calibration_master.has_s11_calibration()) {
throw std::runtime_error("S11 reference bundle requires S11 calibration bundle");
}
calibrated_s11_references_by_combo_.reserve(combos.size());
for (const auto& combo : combos) {
const auto& raw_reference = raw_s11_reference_bundle_.resolve(combo);
ChannelTrace calibrated{};
calibrated.frequency_hz = raw_reference.frequency_hz;
calibrated.samples = calibration_master.apply_s11(combo, raw_reference.frequency_hz, raw_reference.samples);
calibrated_s11_references_by_combo_.insert_or_assign(combo, std::move(calibrated));
}
}
void ReferenceMaster::validate_combos(const std::vector<ipc::ComboKey>& combos) const {
if (calibrated_references_by_combo_.empty()) {
if (calibrated_s21_references_by_combo_.empty()) {
throw std::runtime_error(
"Calibrated references are not prepared. Call ReferenceMaster::prepare_calibrated() at startup."
);
}
raw_s21_reference_bundle_.validate_combos(combos);
for (const auto& combo : combos) {
if (!raw_references_by_combo_.contains(combo)) {
throw std::runtime_error("Raw reference data is missing for combo " + combo_to_string(combo));
if (!calibrated_s21_references_by_combo_.contains(combo)) {
throw std::runtime_error("Calibrated S21 reference data is missing for combo " + combo_to_string(combo));
}
if (!calibrated_references_by_combo_.contains(combo)) {
throw std::runtime_error("Calibrated reference data is missing for combo " + combo_to_string(combo));
}
raw_s11_reference_bundle_.validate_combos(combos);
if (!raw_s11_reference_bundle_.is_enabled()) {
return;
}
if (calibrated_s11_references_by_combo_.empty()) {
throw std::runtime_error("Calibrated S11 references are not prepared");
}
for (const auto& combo : combos) {
if (!calibrated_s11_references_by_combo_.contains(combo)) {
throw std::runtime_error("Calibrated S11 reference data is missing for combo " + combo_to_string(combo));
}
}
}
auto ReferenceMaster::apply(const ipc::SweepTraceBlock& calibrated_trace) const -> ipc::SweepTraceBlock {
validate_trace_layout(calibrated_trace, "Calibrated trace");
const auto found = calibrated_references_by_combo_.find(calibrated_trace.combo);
if (found == calibrated_references_by_combo_.end()) {
throw std::runtime_error(
"Calibrated reference trace is missing for combo " + combo_to_string(calibrated_trace.combo)
);
}
const auto& reference = found->second;
validate_trace_layout(reference, "Calibrated reference trace");
if (calibrated_trace.s21.size() != reference.s21.size()) {
throw std::runtime_error(
"Reference point count mismatch for combo " + combo_to_string(calibrated_trace.combo)
);
}
auto ReferenceMaster::apply_to_trace(const ipc::SweepTraceBlock& calibrated_trace) const -> ipc::SweepTraceBlock {
ipc::SweepTraceBlock output{};
output.combo = calibrated_trace.combo;
output.frequency_hz = calibrated_trace.frequency_hz;
output.s11 = calibrated_trace.s11;
output.s21.resize(calibrated_trace.s21.size());
output.s21 = apply_s21(calibrated_trace.combo, calibrated_trace.frequency_hz, calibrated_trace.s21);
output.s11 = apply_s11(calibrated_trace.combo, calibrated_trace.frequency_hz, calibrated_trace.s11);
return output;
}
auto ReferenceMaster::apply_s21(
const ipc::ComboKey& combo,
std::span<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");
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(
reinterpret_cast<const float*>(calibrated_trace.s21.data()),
reinterpret_cast<const float*>(calibrated_s21.data()),
point_count,
2
);
Eigen::Map<const InterleavedComplexView> reference_view(
reinterpret_cast<const float*>(reference.s21.data()),
reinterpret_cast<const float*>(reference.samples.data()),
point_count,
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;
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;
}
@@ -18,6 +18,7 @@ struct ProcessingLiveConfig {
std::string processor_mode = "pass_through";
float gain_db = 0.0F;
float phase_deg = 0.0F;
std::string pass_through_channel = "s21";
bool pass_through_fixed_y_enabled = false;
float pass_through_y_min_db = -100.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");
}
[[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 {
if (!value.is_number()) {
throw std::runtime_error(field_name + " must be number");
@@ -101,6 +108,12 @@ using Json = nlohmann::json;
}
config.phase_deg = static_cast<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 (!found->is_boolean()) {
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.kind = ipc::ResultKind::TraceComplex;
payload.frequency_hz = trace.frequency_hz;
payload.trace = trace.s21;
if (live_config.pass_through_channel == "s11") {
payload.trace = trace.s11;
} else {
payload.trace = trace.s21;
}
const float linear_gain = std::pow(10.0F, live_config.gain_db / 20.0F);
const float phase_rad = live_config.phase_deg * (kPi / 180.0F);