Rework radar system
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "shared_types.hpp"
|
||||
|
||||
namespace radar::preprocessing {
|
||||
|
||||
struct ChannelTrace {
|
||||
std::vector<float> frequency_hz{};
|
||||
std::vector<ipc::Complex32> samples{};
|
||||
};
|
||||
|
||||
class S21CalibrationBundle {
|
||||
public:
|
||||
void load(const std::string& path);
|
||||
|
||||
void validate_combos(const std::vector<ipc::ComboKey>& combos) const;
|
||||
void apply(
|
||||
const ipc::ComboKey& combo,
|
||||
std::span<const float> frequency_hz,
|
||||
std::span<ipc::Complex32> measured
|
||||
) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<ipc::ComboKey, ChannelTrace, ipc::ComboKeyHash> traces_by_combo_{};
|
||||
};
|
||||
|
||||
class S21ReferenceBundle {
|
||||
public:
|
||||
// Load and through-calibrate the active references once at startup.
|
||||
void load(const std::string& path, const S21CalibrationBundle& calibration,
|
||||
const std::vector<ipc::ComboKey>& combos);
|
||||
void apply(ipc::SweepTraceBlock& trace) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<ipc::ComboKey, ChannelTrace, ipc::ComboKeyHash> traces_by_combo_{};
|
||||
};
|
||||
|
||||
// Object-derived MIMO correction, exported on each pair's runtime frequency grid.
|
||||
// An empty path disables it; enabled bundles must cover every active pair.
|
||||
class S21ObjectCalibrationBundle {
|
||||
public:
|
||||
void load(const std::string& path);
|
||||
void validate_combos(const std::vector<ipc::ComboKey>& combos) const;
|
||||
void apply(ipc::SweepTraceBlock& trace) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<ipc::ComboKey, ChannelTrace, ipc::ComboKeyHash> factors_by_combo_{};
|
||||
};
|
||||
|
||||
} // namespace radar::preprocessing
|
||||
@@ -0,0 +1,255 @@
|
||||
#include "channel_bundle_support.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <complex>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return std::vector<std::uint8_t>(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
for (std::size_t i = 0; i < trace.samples.size(); ++i) {
|
||||
if (!std::isfinite(trace.frequency_hz[i]) || !std::isfinite(trace.samples[i].re)
|
||||
|| !std::isfinite(trace.samples[i].im)) {
|
||||
throw std::runtime_error(trace_label + " contains non-finite values 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) {
|
||||
if (!std::isfinite(left[index]) || !std::isfinite(right[index])) {
|
||||
return false;
|
||||
}
|
||||
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_channel_trace(
|
||||
const ipc::SweepTraceBlock& trace,
|
||||
const std::string& bundle_label
|
||||
) -> ChannelTrace {
|
||||
if (trace.frequency_hz.empty()) {
|
||||
throw std::runtime_error(bundle_label + " trace contains no frequency samples");
|
||||
}
|
||||
|
||||
ChannelTrace channel_trace{};
|
||||
channel_trace.frequency_hz = trace.frequency_hz;
|
||||
channel_trace.samples = trace.s21;
|
||||
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
|
||||
) -> 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) {
|
||||
// #47: reject duplicate combos instead of silently overwriting an earlier trace.
|
||||
const auto inserted =
|
||||
traces_by_combo.insert({trace.combo, to_channel_trace(trace, bundle_label)});
|
||||
if (!inserted.second) {
|
||||
throw std::runtime_error(
|
||||
bundle_label + " bundle contains duplicate combo " + combo_to_string(trace.combo) + ": " + path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 char* value_label
|
||||
) -> const TValue& {
|
||||
const auto found = values_by_combo.find(combo);
|
||||
if (found == values_by_combo.end()) {
|
||||
throw std::runtime_error(std::string(value_label) + " is missing for combo " + combo_to_string(combo));
|
||||
}
|
||||
return found->second;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void S21CalibrationBundle::load(const std::string& path) {
|
||||
if (path.empty()) {
|
||||
throw std::runtime_error("S21 calibration bundle path must not be empty");
|
||||
}
|
||||
|
||||
traces_by_combo_ = load_channel_traces(path, "S21 calibration");
|
||||
}
|
||||
|
||||
void S21CalibrationBundle::validate_combos(const std::vector<ipc::ComboKey>& combos) const {
|
||||
validate_bundle_combos(traces_by_combo_, combos, "S21 calibration bundle");
|
||||
}
|
||||
|
||||
void S21CalibrationBundle::apply(
|
||||
const ipc::ComboKey& combo,
|
||||
std::span<const float> frequency_hz,
|
||||
std::span<ipc::Complex32> measured
|
||||
) const {
|
||||
const auto& calibration_trace = resolve_required(traces_by_combo_, combo, "S21 calibration trace");
|
||||
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");
|
||||
}
|
||||
constexpr float epsilon = 1e-12F;
|
||||
for (std::size_t index = 0; index < measured.size(); ++index) {
|
||||
const auto standard = calibration_trace.samples[index];
|
||||
const float magnitude_squared = standard.re * standard.re + standard.im * standard.im;
|
||||
if (magnitude_squared > epsilon) {
|
||||
const auto value = measured[index];
|
||||
measured[index] = {
|
||||
(value.re * standard.re + value.im * standard.im) / magnitude_squared,
|
||||
(value.im * standard.re - value.re * standard.im) / magnitude_squared,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void S21ReferenceBundle::load(
|
||||
const std::string& path,
|
||||
const S21CalibrationBundle& calibration,
|
||||
const std::vector<ipc::ComboKey>& combos
|
||||
) {
|
||||
const auto raw = load_channel_traces(path, "S21 reference");
|
||||
decltype(traces_by_combo_) calibrated;
|
||||
for (const auto& combo : combos) {
|
||||
auto trace = resolve_required(raw, combo, "S21 reference");
|
||||
calibration.apply(combo, trace.frequency_hz, trace.samples);
|
||||
validate_channel_trace_layout(trace, "Calibrated S21 reference", combo);
|
||||
calibrated.emplace(combo, std::move(trace));
|
||||
}
|
||||
traces_by_combo_ = std::move(calibrated);
|
||||
}
|
||||
|
||||
void S21ReferenceBundle::apply(ipc::SweepTraceBlock& trace) const {
|
||||
const auto& reference = resolve_required(traces_by_combo_, trace.combo, "Calibrated S21 reference");
|
||||
ensure_frequency_axis_match(reference.frequency_hz, trace.frequency_hz, "S21 reference");
|
||||
if (trace.s21.size() != reference.samples.size()) {
|
||||
throw std::runtime_error("S21 reference point count mismatch");
|
||||
}
|
||||
for (std::size_t index = 0; index < trace.s21.size(); ++index) {
|
||||
trace.s21[index].re -= reference.samples[index].re;
|
||||
trace.s21[index].im -= reference.samples[index].im;
|
||||
}
|
||||
}
|
||||
|
||||
void S21ObjectCalibrationBundle::load(const std::string& path) {
|
||||
if (path.empty()) {
|
||||
factors_by_combo_.clear();
|
||||
return;
|
||||
}
|
||||
factors_by_combo_ = load_channel_traces(path, "S21 object calibration");
|
||||
}
|
||||
|
||||
void S21ObjectCalibrationBundle::validate_combos(const std::vector<ipc::ComboKey>& combos) const {
|
||||
validate_bundle_combos(factors_by_combo_, combos, "S21 object calibration bundle");
|
||||
}
|
||||
|
||||
void S21ObjectCalibrationBundle::apply(ipc::SweepTraceBlock& trace) const {
|
||||
if (factors_by_combo_.empty()) { return; }
|
||||
const auto& factors = resolve_required(factors_by_combo_, trace.combo, "S21 object calibration");
|
||||
ensure_frequency_axis_match(factors.frequency_hz, trace.frequency_hz, "S21 object calibration");
|
||||
if (trace.s21.size() != factors.samples.size()) {
|
||||
throw std::runtime_error("S21 object calibration vector size mismatch");
|
||||
}
|
||||
for (std::size_t index = 0; index < trace.s21.size(); ++index) {
|
||||
const auto value = trace.s21[index];
|
||||
const auto factor = factors.samples[index];
|
||||
const auto corrected = std::complex<double>(value.re, value.im) * std::complex<double>(factor.re, factor.im);
|
||||
if (!std::isfinite(corrected.real()) || !std::isfinite(corrected.imag()) ||
|
||||
std::abs(corrected.real()) > std::numeric_limits<float>::max() ||
|
||||
std::abs(corrected.imag()) > std::numeric_limits<float>::max()) {
|
||||
throw std::runtime_error("S21 object calibration result must fit finite float32");
|
||||
}
|
||||
trace.s21[index] = {.re = static_cast<float>(corrected.real()), .im = static_cast<float>(corrected.imag())};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace radar::preprocessing
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "calibrator_interface.hpp"
|
||||
#include "channel_bundle_support.hpp"
|
||||
#include "shared_types.hpp"
|
||||
|
||||
namespace radar::preprocessing {
|
||||
|
||||
class CalibrationMaster {
|
||||
public:
|
||||
// `s21_calibrator` encapsulates the S21 calibration algorithm.
|
||||
explicit CalibrationMaster(std::unique_ptr<CalibratorInterface> s21_calibrator);
|
||||
|
||||
// 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 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> s21_calibrator_;
|
||||
S21CalibrationBundle s21_calibration_bundle_{};
|
||||
S11CalibrationBundle s11_calibration_bundle_{};
|
||||
};
|
||||
|
||||
[[nodiscard]] auto make_s21_through_calibrator() -> std::unique_ptr<CalibratorInterface>;
|
||||
|
||||
} // namespace radar::preprocessing
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "shared_types.hpp"
|
||||
|
||||
namespace radar::preprocessing {
|
||||
|
||||
class CalibratorInterface {
|
||||
public:
|
||||
virtual ~CalibratorInterface() = default;
|
||||
|
||||
[[nodiscard]] virtual auto name() const -> std::string = 0;
|
||||
[[nodiscard]] virtual auto apply(
|
||||
const std::vector<ipc::Complex32>& measured,
|
||||
const std::vector<ipc::Complex32>& calibration
|
||||
) const -> std::vector<ipc::Complex32> = 0;
|
||||
};
|
||||
|
||||
} // namespace radar::preprocessing
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
#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{};
|
||||
};
|
||||
|
||||
void validate_channel_trace_layout(const ChannelTrace& trace, const std::string& label, const ipc::ComboKey& combo);
|
||||
[[nodiscard]] auto frequency_axes_match(std::span<const float> left, std::span<const float> right) -> bool;
|
||||
|
||||
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_{};
|
||||
};
|
||||
|
||||
// Object-derived MIMO correction, exported on each pair's runtime frequency grid.
|
||||
// An empty path disables it; enabled bundles must cover every active pair.
|
||||
class S21ObjectCalibrationBundle {
|
||||
public:
|
||||
void load(const std::string& path);
|
||||
void validate_combos(const std::vector<ipc::ComboKey>& combos) const;
|
||||
void apply(ipc::SweepTraceBlock& trace) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<ipc::ComboKey, ChannelTrace, ipc::ComboKeyHash> factors_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,63 +0,0 @@
|
||||
#include "calibration_master.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace radar::preprocessing {
|
||||
|
||||
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_s21_calibration_bundle(const std::string& path) {
|
||||
s21_calibration_bundle_.load(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 {
|
||||
s21_calibration_bundle_.validate_combos(combos);
|
||||
s11_calibration_bundle_.validate_combos(combos);
|
||||
}
|
||||
|
||||
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.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);
|
||||
// Calibration reshapes the samples, not when they were measured.
|
||||
output.capture_start_ns = measured_trace.capture_start_ns;
|
||||
output.capture_end_ns = measured_trace.capture_end_ns;
|
||||
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
|
||||
-494
@@ -1,494 +0,0 @@
|
||||
#include "channel_bundle_support.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <complex>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#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;
|
||||
// #30: OSL denominator must be well conditioned relative to the magnitudes of the terms forming it.
|
||||
constexpr float kOslRelativeConditioning = 1e-6F;
|
||||
// #30: maximum fraction of degenerate (fallback) OSL points tolerated per combo before load fails.
|
||||
constexpr float kOslMaxDegenerateFraction = 0.05F;
|
||||
|
||||
[[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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
for (std::size_t i = 0; i < trace.samples.size(); ++i) {
|
||||
if (!std::isfinite(trace.frequency_hz[i]) || !std::isfinite(trace.samples[i].re)
|
||||
|| !std::isfinite(trace.samples[i].im)) {
|
||||
throw std::runtime_error(trace_label + " contains non-finite values 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) {
|
||||
if (!std::isfinite(left[index]) || !std::isfinite(right[index])) {
|
||||
return false;
|
||||
}
|
||||
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 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");
|
||||
if (trace.frequency_hz.empty()) {
|
||||
throw std::runtime_error(bundle_label + " trace contains no frequency samples");
|
||||
}
|
||||
|
||||
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) {
|
||||
// #47: reject duplicate combos instead of silently overwriting an earlier trace.
|
||||
const auto inserted =
|
||||
traces_by_combo.insert({trace.combo, to_channel_trace(trace, channel, bundle_label)});
|
||||
if (!inserted.second) {
|
||||
throw std::runtime_error(
|
||||
bundle_label + " bundle contains duplicate combo " + combo_to_string(trace.combo) + ": " + path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 char* value_label
|
||||
) -> const TValue& {
|
||||
const auto found = values_by_combo.find(combo);
|
||||
if (found == values_by_combo.end()) {
|
||||
throw std::runtime_error(std::string(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());
|
||||
|
||||
// #30: track degenerate points so silently-substituted fallback coefficients cannot pass unnoticed.
|
||||
std::size_t degenerate_points = 0;
|
||||
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;
|
||||
|
||||
// #30: require the denominator to be well conditioned both absolutely and relative to the
|
||||
// magnitudes of the open/short deltas it is formed from, so near-cancellations are rejected.
|
||||
const auto denominator_norm = std::norm(denominator);
|
||||
const auto relative_scale = std::max(std::norm(open_delta), std::norm(short_delta));
|
||||
const auto relative_threshold = relative_scale * (kOslRelativeConditioning * kOslRelativeConditioning);
|
||||
|
||||
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 (denominator_norm > kComplexMagnitudeSquaredEpsilon && denominator_norm > relative_threshold) {
|
||||
source_match = (open_delta + short_delta) / denominator;
|
||||
reflection_tracking = open_delta * (std::complex<float>(1.0F, 0.0F) - source_match);
|
||||
} else {
|
||||
// #30: degenerate point — log the offending combo/frequency and fall back to a pass-through.
|
||||
++degenerate_points;
|
||||
std::cerr << "S11 calibration: degenerate OSL point for combo " << combo_to_string(combo)
|
||||
<< " at " << open_trace.frequency_hz[index] << " Hz (ill-conditioned denominator)\n";
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// #30: refuse to load a calibration whose per-combo fallback fraction exceeds the tolerated threshold.
|
||||
if (!open_trace.frequency_hz.empty()) {
|
||||
const auto degenerate_fraction =
|
||||
static_cast<float>(degenerate_points) / static_cast<float>(open_trace.frequency_hz.size());
|
||||
if (degenerate_fraction > kOslMaxDegenerateFraction) {
|
||||
throw std::runtime_error(
|
||||
"S11 calibration for combo " + combo_to_string(combo) + " has " +
|
||||
std::to_string(degenerate_points) + " of " +
|
||||
std::to_string(open_trace.frequency_hz.size()) +
|
||||
" degenerate OSL points (fraction exceeds tolerance)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void S21CalibrationBundle::load(const std::string& path) {
|
||||
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) {
|
||||
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 S21ObjectCalibrationBundle::load(const std::string& path) {
|
||||
if (path.empty()) {
|
||||
factors_by_combo_.clear();
|
||||
return;
|
||||
}
|
||||
factors_by_combo_ = load_channel_traces(path, "S21 object calibration", RawTraceChannel::S21);
|
||||
}
|
||||
|
||||
void S21ObjectCalibrationBundle::validate_combos(const std::vector<ipc::ComboKey>& combos) const {
|
||||
validate_bundle_combos(factors_by_combo_, combos, "S21 object calibration bundle");
|
||||
}
|
||||
|
||||
void S21ObjectCalibrationBundle::apply(ipc::SweepTraceBlock& trace) const {
|
||||
if (factors_by_combo_.empty()) { return; }
|
||||
const auto& factors = resolve_required(factors_by_combo_, trace.combo, "S21 object calibration");
|
||||
ensure_frequency_axis_match(factors.frequency_hz, trace.frequency_hz, "S21 object calibration");
|
||||
if (trace.s21.size() != factors.samples.size()) {
|
||||
throw std::runtime_error("S21 object calibration vector size mismatch");
|
||||
}
|
||||
for (std::size_t index = 0; index < trace.s21.size(); ++index) {
|
||||
const auto value = trace.s21[index];
|
||||
const auto factor = factors.samples[index];
|
||||
const auto corrected = std::complex<double>(value.re, value.im) * std::complex<double>(factor.re, factor.im);
|
||||
if (!std::isfinite(corrected.real()) || !std::isfinite(corrected.imag()) ||
|
||||
std::abs(corrected.real()) > std::numeric_limits<float>::max() ||
|
||||
std::abs(corrected.imag()) > std::numeric_limits<float>::max()) {
|
||||
throw std::runtime_error("S21 object calibration result must fit finite float32");
|
||||
}
|
||||
trace.s21[index] = {.re = static_cast<float>(corrected.real()), .im = static_cast<float>(corrected.imag())};
|
||||
}
|
||||
}
|
||||
|
||||
void S11CalibrationBundle::load(
|
||||
const std::string& open_path,
|
||||
const std::string& short_path,
|
||||
const std::string& load_path
|
||||
) {
|
||||
if (open_path.empty() && short_path.empty() && load_path.empty()) {
|
||||
coefficients_by_combo_.clear();
|
||||
// #38: S11 calibration disabled because no OSL paths were provided; surface it instead of silently skipping.
|
||||
std::cerr << "S11 calibration: no open/short/load bundle paths provided; reflection correction disabled\n";
|
||||
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");
|
||||
|
||||
decltype(coefficients_by_combo_) coefficients;
|
||||
coefficients.reserve(open_traces.size());
|
||||
for (const auto& [combo, open_trace] : open_traces) {
|
||||
coefficients.insert_or_assign(
|
||||
combo,
|
||||
solve_osl_coefficients(combo, open_trace, short_traces.at(combo), load_traces.at(combo))
|
||||
);
|
||||
}
|
||||
coefficients_by_combo_ = std::move(coefficients);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (path.empty()) {
|
||||
traces_by_combo_.clear();
|
||||
// #38: S11 reference disabled because no path was provided; surface it instead of silently skipping.
|
||||
std::cerr << "S11 reference: no bundle path provided; reference normalization disabled\n";
|
||||
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
|
||||
@@ -1,76 +0,0 @@
|
||||
#include "calibration_master.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
#include <Eigen/Core>
|
||||
|
||||
namespace radar::preprocessing {
|
||||
namespace {
|
||||
|
||||
class ThroughCalibrator final : public CalibratorInterface {
|
||||
public:
|
||||
[[nodiscard]] auto name() const -> std::string override {
|
||||
return "through";
|
||||
}
|
||||
|
||||
[[nodiscard]] auto apply(
|
||||
const std::vector<ipc::Complex32>& measured,
|
||||
const std::vector<ipc::Complex32>& calibration
|
||||
) const -> std::vector<ipc::Complex32> override {
|
||||
if (measured.size() != calibration.size()) {
|
||||
throw std::runtime_error("Through calibration vector size mismatch");
|
||||
}
|
||||
|
||||
static_assert(sizeof(ipc::Complex32) == sizeof(float) * 2U, "Complex32 layout must be two contiguous floats");
|
||||
|
||||
constexpr float epsilon = 1e-12F;
|
||||
using InterleavedComplexView = Eigen::Matrix<float, Eigen::Dynamic, 2, Eigen::RowMajor>;
|
||||
|
||||
const auto point_count = static_cast<Eigen::Index>(measured.size());
|
||||
Eigen::Map<const InterleavedComplexView> measured_view(
|
||||
reinterpret_cast<const float*>(measured.data()),
|
||||
point_count,
|
||||
2
|
||||
);
|
||||
Eigen::Map<const InterleavedComplexView> calibration_view(
|
||||
reinterpret_cast<const float*>(calibration.data()),
|
||||
point_count,
|
||||
2
|
||||
);
|
||||
|
||||
const auto measured_re = measured_view.col(0).array();
|
||||
const auto measured_im = measured_view.col(1).array();
|
||||
const auto calibration_re = calibration_view.col(0).array();
|
||||
const auto calibration_im = calibration_view.col(1).array();
|
||||
|
||||
const Eigen::ArrayXf magnitude_squared = calibration_re.square() + calibration_im.square();
|
||||
const auto stable_division_mask = (magnitude_squared > epsilon);
|
||||
|
||||
const Eigen::ArrayXf corrected_re = stable_division_mask.select(
|
||||
((measured_re * calibration_re) + (measured_im * calibration_im)) / magnitude_squared,
|
||||
measured_re
|
||||
);
|
||||
const Eigen::ArrayXf corrected_im = stable_division_mask.select(
|
||||
((measured_im * calibration_re) - (measured_re * calibration_im)) / magnitude_squared,
|
||||
measured_im
|
||||
);
|
||||
|
||||
std::vector<ipc::Complex32> corrected(measured.size());
|
||||
Eigen::Map<InterleavedComplexView> corrected_view(reinterpret_cast<float*>(corrected.data()), point_count, 2);
|
||||
corrected_view.col(0) = corrected_re.matrix();
|
||||
corrected_view.col(1) = corrected_im.matrix();
|
||||
|
||||
return corrected;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
auto make_s21_through_calibrator() -> std::unique_ptr<CalibratorInterface> {
|
||||
return std::make_unique<ThroughCalibrator>();
|
||||
}
|
||||
|
||||
} // namespace radar::preprocessing
|
||||
+13
-13
@@ -2,21 +2,27 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "calibration_master.hpp"
|
||||
#include "reference_master.hpp"
|
||||
#include "channel_bundle_support.hpp"
|
||||
#include "run_config.hpp"
|
||||
#include "shm_ring.hpp"
|
||||
|
||||
namespace radar::preprocessing {
|
||||
|
||||
// Per-point notch multipliers: 0 inside every band, 1 outside; with the cosine
|
||||
// taper a raised-cosine shoulder of taper_width_hz falls from 1 to 0 towards
|
||||
// each band edge.
|
||||
[[nodiscard]] auto build_notch_mask(
|
||||
std::span<const float> frequency_hz,
|
||||
const config::PreprocessNotchConfig& notch
|
||||
) -> std::vector<float>;
|
||||
|
||||
class DataPreprocessor {
|
||||
public:
|
||||
DataPreprocessor(
|
||||
const config::RunConfig& config,
|
||||
CalibrationMaster& calibration_master,
|
||||
ReferenceMaster& reference_master,
|
||||
ipc::ShmRing& raw_ring,
|
||||
ipc::ShmRing& preprocessed_ring,
|
||||
ipc::ShmRing* preprocessed_tap_ring = nullptr
|
||||
@@ -26,19 +32,13 @@ class DataPreprocessor {
|
||||
void run(const std::atomic<bool>& stop_requested);
|
||||
|
||||
private:
|
||||
void validate_startup_prerequisites() const;
|
||||
[[nodiscard]] auto try_pop_raw_collection(
|
||||
std::vector<std::uint8_t>& serialized_raw,
|
||||
ipc::RawSweepCollection* out_collection
|
||||
) -> bool;
|
||||
void publish_preprocessed_collection(const ipc::PreprocessedCollection& collection);
|
||||
|
||||
[[nodiscard]] auto preprocess_collection(const ipc::RawSweepCollection& raw_collection) const
|
||||
-> ipc::PreprocessedCollection;
|
||||
void preprocess_collection(ipc::RawSweepCollection& collection) const;
|
||||
|
||||
const config::RunConfig& config_;
|
||||
CalibrationMaster& calibration_master_;
|
||||
ReferenceMaster& reference_master_;
|
||||
S21CalibrationBundle calibration_;
|
||||
S21ReferenceBundle reference_;
|
||||
S21ObjectCalibrationBundle object_calibration_;
|
||||
ipc::ShmRing& raw_ring_;
|
||||
ipc::ShmRing& preprocessed_ring_;
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
#include "data_preprocessor.hpp"
|
||||
#include "runtime_wait.hpp"
|
||||
#include "pipeline_runtime.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <numbers>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace radar::preprocessing {
|
||||
namespace {
|
||||
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
|
||||
[[nodiscard]] auto should_apply_notch(const config::PreprocessNotchConfig& notch) -> bool {
|
||||
return notch.enabled && !notch.bands_hz.empty();
|
||||
}
|
||||
|
||||
[[nodiscard]] auto build_notch_mask(
|
||||
} // namespace
|
||||
|
||||
auto build_notch_mask(
|
||||
std::span<const float> frequency_hz,
|
||||
const config::PreprocessNotchConfig& notch
|
||||
) -> std::vector<float> {
|
||||
@@ -51,15 +50,16 @@ constexpr double kPi = 3.14159265358979323846;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Raised-cosine shoulders: 1 at the outer edge, 0 where the band starts.
|
||||
if (frequency >= trans_low_hz && frequency < low_hz) {
|
||||
const double arg = kPi * (frequency - trans_low_hz) / taper_width_hz;
|
||||
mask[index] *= static_cast<float>(0.5 * (1.0 - std::cos(arg)));
|
||||
const double arg = std::numbers::pi * (frequency - trans_low_hz) / taper_width_hz;
|
||||
mask[index] *= static_cast<float>(0.5 * (1.0 + std::cos(arg)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (frequency > high_hz && frequency <= trans_high_hz) {
|
||||
const double arg = kPi * (frequency - high_hz) / taper_width_hz;
|
||||
mask[index] *= static_cast<float>(0.5 * (1.0 + std::cos(arg)));
|
||||
const double arg = std::numbers::pi * (frequency - high_hz) / taper_width_hz;
|
||||
mask[index] *= static_cast<float>(0.5 * (1.0 - std::cos(arg)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,8 @@ constexpr double kPi = 3.14159265358979323846;
|
||||
return mask;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void apply_notch_mask(std::vector<ipc::Complex32>& samples, std::span<const float> mask) {
|
||||
const std::size_t point_count = std::min(samples.size(), mask.size());
|
||||
for (std::size_t index = 0U; index < point_count; ++index) {
|
||||
@@ -75,132 +77,72 @@ void apply_notch_mask(std::vector<ipc::Complex32>& samples, std::span<const floa
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] auto apply_notch_filter(
|
||||
const ipc::SweepTraceBlock& trace,
|
||||
const config::PreprocessNotchConfig& notch
|
||||
) -> ipc::SweepTraceBlock {
|
||||
if (!should_apply_notch(notch)) {
|
||||
return trace;
|
||||
}
|
||||
|
||||
ipc::SweepTraceBlock filtered = trace;
|
||||
const auto mask = build_notch_mask(filtered.frequency_hz, notch);
|
||||
apply_notch_mask(filtered.s21, mask);
|
||||
apply_notch_mask(filtered.s11, mask);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DataPreprocessor::DataPreprocessor(
|
||||
const config::RunConfig& config,
|
||||
CalibrationMaster& calibration_master,
|
||||
ReferenceMaster& reference_master,
|
||||
ipc::ShmRing& raw_ring,
|
||||
ipc::ShmRing& preprocessed_ring,
|
||||
ipc::ShmRing* preprocessed_tap_ring
|
||||
)
|
||||
: config_(config),
|
||||
calibration_master_(calibration_master),
|
||||
reference_master_(reference_master),
|
||||
raw_ring_(raw_ring),
|
||||
preprocessed_ring_(preprocessed_ring),
|
||||
preprocessed_tap_ring_(preprocessed_tap_ring) {
|
||||
calibration_.load(config_.preprocess.s21.calibration.bundle_path);
|
||||
calibration_.validate_combos(config_.run_combos);
|
||||
reference_.load(config_.preprocess.s21.reference.bundle_path, calibration_, config_.run_combos);
|
||||
object_calibration_.load(config_.preprocess.s21.object_calibration.bundle_path);
|
||||
}
|
||||
|
||||
void DataPreprocessor::validate_startup_prerequisites() const {
|
||||
calibration_master_.validate_combos(config_.run_combos);
|
||||
reference_master_.validate_combos(config_.run_combos);
|
||||
object_calibration_.validate_combos(config_.run_combos);
|
||||
}
|
||||
|
||||
auto DataPreprocessor::try_pop_raw_collection(
|
||||
std::vector<std::uint8_t>& serialized_raw,
|
||||
ipc::RawSweepCollection* out_collection
|
||||
) -> bool {
|
||||
if (!raw_ring_.pop(serialized_raw)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*out_collection = ipc::deserialize_raw_collection(serialized_raw);
|
||||
return true;
|
||||
}
|
||||
|
||||
void DataPreprocessor::publish_preprocessed_collection(const ipc::PreprocessedCollection& collection) {
|
||||
const auto serialized_preprocessed = ipc::serialize_preprocessed_collection(collection);
|
||||
if (!preprocessed_ring_.push(serialized_preprocessed)) {
|
||||
throw std::runtime_error("Preprocessed ring slot is too small for serialized collection");
|
||||
throw runtime::FatalPipelineError("Preprocessed ring slot is too small for serialized collection");
|
||||
}
|
||||
if (preprocessed_tap_ring_ != nullptr && !preprocessed_tap_ring_->push(serialized_preprocessed)) {
|
||||
throw std::runtime_error("Preprocessed tap ring slot is too small for serialized collection");
|
||||
throw runtime::FatalPipelineError("Preprocessed tap ring slot is too small for serialized collection");
|
||||
}
|
||||
}
|
||||
|
||||
void DataPreprocessor::run(const std::atomic<bool>& stop_requested) {
|
||||
validate_startup_prerequisites();
|
||||
|
||||
std::vector<std::uint8_t> serialized_raw{};
|
||||
ipc::RawSweepCollection raw_collection{};
|
||||
std::uint64_t error_count = 0;
|
||||
std::uint64_t consecutive_errors = 0;
|
||||
runtime::CollectionErrors errors;
|
||||
while (!stop_requested.load(std::memory_order_relaxed)) {
|
||||
try {
|
||||
if (!try_pop_raw_collection(serialized_raw, &raw_collection)) {
|
||||
if (!raw_ring_.pop(serialized_raw)) {
|
||||
(void)config::wait_or_stop(config_.runtime.idle_sleep_ms, stop_requested);
|
||||
continue;
|
||||
}
|
||||
const auto preprocessed_collection = preprocess_collection(raw_collection);
|
||||
publish_preprocessed_collection(preprocessed_collection);
|
||||
consecutive_errors = 0; // made progress; reset the escalation counter
|
||||
auto raw_collection = ipc::deserialize_raw_collection(serialized_raw);
|
||||
preprocess_collection(raw_collection);
|
||||
publish_preprocessed_collection(raw_collection);
|
||||
errors.progress();
|
||||
} catch (const runtime::FatalPipelineError&) {
|
||||
throw;
|
||||
} catch (const std::exception& exc) {
|
||||
// A ring-slot-too-small failure is a permanent config error (the slot
|
||||
// cannot hold a serialized collection): swallowing it would silently
|
||||
// drop every collection forever, so fail fast and let the operator fix it.
|
||||
if (std::string(exc.what()).find("slot is too small") != std::string::npos) {
|
||||
throw;
|
||||
}
|
||||
// A single malformed or transiently-bad collection must not kill the
|
||||
// long-running preprocessor: drop it and keep serving the next sweep.
|
||||
++error_count;
|
||||
++consecutive_errors;
|
||||
// Escalate a PERSISTENT failure loudly so a deterministic config error
|
||||
// (which drops every collection forever) is not hidden by the throttle.
|
||||
if (consecutive_errors % 500 == 0) {
|
||||
std::cerr << "data_preprocessor: " << consecutive_errors
|
||||
<< " consecutive failures — likely a permanent configuration error: "
|
||||
<< exc.what() << '\n';
|
||||
} else if (error_count % 100 == 1) {
|
||||
std::cerr << "data_preprocessor: dropped collection after error (count="
|
||||
<< error_count << "): " << exc.what() << '\n';
|
||||
}
|
||||
// Back-off floored at 1ms (the empty-ring sleep is never reached while a
|
||||
// persistent error keeps the ring non-empty) so the catch cannot busy-spin
|
||||
// a CPU core even when idle_sleep_ms is configured to 0.
|
||||
errors.report("data_preprocessor", exc);
|
||||
(void)config::wait_or_stop(std::max(1U, config_.runtime.idle_sleep_ms), stop_requested);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto DataPreprocessor::preprocess_collection(const ipc::RawSweepCollection& raw_collection) const
|
||||
-> ipc::PreprocessedCollection {
|
||||
ipc::PreprocessedCollection preprocessed{};
|
||||
preprocessed.collection_id = raw_collection.collection_id;
|
||||
preprocessed.monotonic_ns = ipc::current_monotonic_ns();
|
||||
preprocessed.capture_start_ns = raw_collection.capture_start_ns;
|
||||
preprocessed.capture_end_ns = raw_collection.capture_end_ns;
|
||||
preprocessed.traces.reserve(raw_collection.traces.size());
|
||||
|
||||
for (const auto& raw_trace : raw_collection.traces) {
|
||||
// Object-derived corrections act on the completed preprocessing output.
|
||||
const auto calibrated = calibration_master_.apply_to_trace(raw_trace);
|
||||
const auto referenced = reference_master_.apply_to_trace(calibrated);
|
||||
auto filtered = apply_notch_filter(referenced, config_.preprocess.notch);
|
||||
object_calibration_.apply(filtered);
|
||||
preprocessed.traces.push_back(std::move(filtered));
|
||||
void DataPreprocessor::preprocess_collection(ipc::RawSweepCollection& collection) const {
|
||||
if (!collection.capture_start_ns || collection.capture_end_ns < collection.capture_start_ns) {
|
||||
throw std::runtime_error("Raw collection requires a valid acquisition window");
|
||||
}
|
||||
|
||||
return preprocessed;
|
||||
for (auto& trace : collection.traces) {
|
||||
calibration_.apply(trace.combo, trace.frequency_hz, trace.s21);
|
||||
reference_.apply(trace);
|
||||
if (should_apply_notch(config_.preprocess.notch)) {
|
||||
const auto mask = build_notch_mask(trace.frequency_hz, config_.preprocess.notch);
|
||||
apply_notch_mask(trace.s21, mask);
|
||||
}
|
||||
// Object-derived corrections act on the completed preprocessing output.
|
||||
object_calibration_.apply(trace);
|
||||
}
|
||||
collection.monotonic_ns = ipc::current_monotonic_ns();
|
||||
}
|
||||
|
||||
} // namespace radar::preprocessing
|
||||
|
||||
@@ -1,49 +1,14 @@
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
|
||||
#include "calibration_master.hpp"
|
||||
#include "data_preprocessor.hpp"
|
||||
#include "reference_master.hpp"
|
||||
#include "run_config.hpp"
|
||||
#include "pipeline_runtime.hpp"
|
||||
#include "shm_ring.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kDefaultConfigPath = "run_config.json";
|
||||
|
||||
std::atomic<bool> g_stop_requested{false};
|
||||
|
||||
void signal_handler(int /*signal*/) {
|
||||
g_stop_requested.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void install_signal_handlers() {
|
||||
std::signal(SIGINT, signal_handler);
|
||||
std::signal(SIGTERM, signal_handler);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto read_config_path(int argc, char** argv) -> std::string {
|
||||
std::string config_path = kDefaultConfigPath;
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
const std::string arg = argv[index];
|
||||
if (arg == "--config" && (index + 1) < argc) {
|
||||
config_path = argv[++index];
|
||||
}
|
||||
}
|
||||
return config_path;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
install_signal_handlers();
|
||||
|
||||
try {
|
||||
const auto config_path = read_config_path(argc, argv);
|
||||
const auto config = radar::config::load_run_config(config_path);
|
||||
const auto config = radar::runtime::initialize(argc, argv);
|
||||
|
||||
auto raw_ring = radar::ipc::ShmRing::open_or_create(
|
||||
config.rings.raw.name,
|
||||
@@ -55,37 +20,22 @@ int main(int argc, char** argv) {
|
||||
config.rings.preprocessed.capacity,
|
||||
config.rings.preprocessed.slot_size_bytes
|
||||
);
|
||||
auto preprocessed_tap_ring = radar::ipc::ShmRing::open_or_create(
|
||||
config.rings.preprocessed_tap.name,
|
||||
config.rings.preprocessed_tap.capacity,
|
||||
config.rings.preprocessed_tap.slot_size_bytes
|
||||
);
|
||||
|
||||
// Load preprocessing assets once before entering run loop.
|
||||
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.calibration.open.bundle_path,
|
||||
config.preprocess.s11.calibration.short_standard.bundle_path,
|
||||
config.preprocess.s11.calibration.load.bundle_path
|
||||
);
|
||||
|
||||
radar::preprocessing::ReferenceMaster reference_master;
|
||||
reference_master.load_s21_reference_bundle(config.preprocess.s21.reference.bundle_path);
|
||||
reference_master.load_s11_reference_bundle(config.preprocess.s11.reference.bundle_path);
|
||||
reference_master.prepare_calibrated(calibration_master, config.run_combos);
|
||||
std::optional<radar::ipc::ShmRing> preprocessed_tap_ring;
|
||||
if (!config.rings.preprocessed_tap.name.empty()) {
|
||||
preprocessed_tap_ring = radar::ipc::ShmRing::open_or_create(
|
||||
config.rings.preprocessed_tap.name,
|
||||
config.rings.preprocessed_tap.capacity,
|
||||
config.rings.preprocessed_tap.slot_size_bytes
|
||||
);
|
||||
}
|
||||
|
||||
radar::preprocessing::DataPreprocessor preprocessor(
|
||||
config,
|
||||
calibration_master,
|
||||
reference_master,
|
||||
raw_ring,
|
||||
preprocessed_ring,
|
||||
&preprocessed_tap_ring
|
||||
preprocessed_tap_ring ? &*preprocessed_tap_ring : nullptr
|
||||
);
|
||||
preprocessor.run(g_stop_requested);
|
||||
preprocessor.run(radar::runtime::stop_requested);
|
||||
return 0;
|
||||
} catch (const std::exception& exception) {
|
||||
std::cerr << "data_preprocessor error: " << exception.what() << '\n';
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "channel_bundle_support.hpp"
|
||||
#include "shared_types.hpp"
|
||||
|
||||
namespace radar::preprocessing {
|
||||
|
||||
class CalibrationMaster;
|
||||
|
||||
class ReferenceMaster {
|
||||
public:
|
||||
// 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_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 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:
|
||||
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,180 +0,0 @@
|
||||
#include "reference_master.hpp"
|
||||
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <Eigen/Core>
|
||||
|
||||
#include "calibration_master.hpp"
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ReferenceMaster::load_s21_reference_bundle(const std::string& path) {
|
||||
raw_s21_reference_bundle_.load(path);
|
||||
calibrated_s21_references_by_combo_.clear();
|
||||
}
|
||||
|
||||
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_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_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 (!calibrated_s21_references_by_combo_.contains(combo)) {
|
||||
throw std::runtime_error("Calibrated S21 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_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.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);
|
||||
// Reference subtraction reshapes the samples, not when they were measured.
|
||||
output.capture_start_ns = calibrated_trace.capture_start_ns;
|
||||
output.capture_end_ns = calibrated_trace.capture_end_ns;
|
||||
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_s21.size());
|
||||
|
||||
Eigen::Map<const InterleavedComplexView> calibrated_view(
|
||||
reinterpret_cast<const float*>(calibrated_s21.data()),
|
||||
point_count,
|
||||
2
|
||||
);
|
||||
Eigen::Map<const InterleavedComplexView> reference_view(
|
||||
reinterpret_cast<const float*>(reference.samples.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;
|
||||
}
|
||||
|
||||
} // namespace radar::preprocessing
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user