Rework radar system

This commit is contained in:
Ayzen
2026-10-06 12:32:26 +03:00
parent 552cdc688a
commit 5bb86b7165
299 changed files with 13537 additions and 27763 deletions
@@ -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';