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
@@ -10,8 +10,7 @@
namespace radar::processing {
// Temporal "approach" filter for coherent-BP detections — a port of the objects-only
// filter in Horns_motion_3libre's demonstrate notebook.
// Temporal "approach" filter for coherent-BP detections.
//
// It keeps only objects that persist as a *motion-consistent track* across at least
// `min_frames` consecutive frames: as the radar moves, a real target reappears at a
@@ -30,12 +29,8 @@ namespace radar::processing {
// inter-frame gap (`kMaxFrameGapSeconds`), so a stale history from a previous run cannot
// spuriously confirm objects.
//
// IDEMPOTENT under reprocessing: the data_processor re-runs the last collection (or
// replays the whole window) whenever live settings or the socket speed change, with no
// new sweep. The history is therefore keyed by `frame_id` (the strictly increasing
// collection id): the same id replaces its entry (reprocess of the current frame), a
// smaller id rebuilds from scratch (a replay restart), so repeated reprocessing never
// duplicates frames or falsely confirms a track.
// Reprocessing replaces the same (collection_id, capture_start_ns) entry. Earlier
// capture times rebuild the history when the processor replays its retained window.
class ObjectApproachFilter {
public:
struct Point {
@@ -43,20 +38,16 @@ class ObjectApproachFilter {
double z_m;
};
// Record `objects` as frame `frame_id` and return, per object, whether it is
// confirmed (ends a >= `min_frames` motion-consistent track). `frame_id` is the
// collection id (identity/order, survives reprocessing); `frame_time_seconds` is the
// frame's wall-clock timestamp (drives the inter-frame interval); `speed_m_s`/
// `look_angle_deg` are the live motion estimate. `min_frames <= 1` disables filtering.
// Acquisition time drives both ordering and motion intervals. min_frames <= 1 disables filtering.
[[nodiscard]] auto confirm(
const std::vector<Point>& objects,
std::uint64_t frame_id,
double frame_time_seconds,
std::uint64_t capture_start_ns,
double speed_m_s,
double look_angle_deg,
std::size_t min_frames
) -> std::vector<bool> {
record_frame(Frame{frame_id, frame_time_seconds, objects});
record_frame(Frame{frame_id, capture_start_ns, objects});
const std::size_t history_depth = std::max<std::size_t>(min_frames, 1U);
while (history_.size() > history_depth) {
history_.pop_front();
@@ -79,17 +70,14 @@ class ObjectApproachFilter {
private:
struct Frame {
std::uint64_t id;
double time_seconds;
std::uint64_t capture_start_ns;
std::vector<Point> objects;
};
// Append a genuinely new frame, replace the current one on reprocessing (same id),
// or rebuild from scratch when the id steps backward (a replay restart). This keeps
// the history one entry per distinct collection no matter how often settings change.
void record_frame(Frame frame) {
if (history_.empty() || frame.id > history_.back().id) {
if (history_.empty() || frame.capture_start_ns > history_.back().capture_start_ns) {
history_.push_back(std::move(frame));
} else if (frame.id == history_.back().id) {
} else if (frame.id == history_.back().id && frame.capture_start_ns == history_.back().capture_start_ns) {
history_.back() = std::move(frame);
} else {
history_.clear();
@@ -97,7 +85,7 @@ class ObjectApproachFilter {
}
}
// Fixed matching tolerances (Horns_motion notebook 0.2 block). Range decreases as the
// Fixed matching tolerances. Range decreases as the
// radar approaches the target, hence the negative Z sign.
static constexpr double kXToleranceM = 0.45;
static constexpr double kRangeToleranceFraction = 0.85;
@@ -122,8 +110,10 @@ class ObjectApproachFilter {
for (std::size_t step = 1U; step < min_frames; ++step) {
const std::size_t earlier_index = newest - step;
const Frame& earlier = history_[earlier_index];
const double dt = history_[earlier_index + 1U].time_seconds - earlier.time_seconds;
if (!(dt > 0.0) || dt > kMaxFrameGapSeconds) {
const auto later_ns = history_[earlier_index + 1U].capture_start_ns;
if (later_ns <= earlier.capture_start_ns) { return false; }
const double dt = static_cast<double>(later_ns - earlier.capture_start_ns) * 1e-9;
if (dt > kMaxFrameGapSeconds) {
return false; // non-monotonic time, or a gap that breaks the track
}
const double expected_range_shift = range_step_per_second * dt;
@@ -93,16 +93,6 @@ void fft_inplace(std::vector<std::complex<double>>& values, bool inverse) {
return std::abs(sample);
}
[[nodiscard]] auto selected_trace_samples(
const ipc::SweepTraceBlock& trace,
const ProcessingLiveConfig& live_config
) -> std::span<const ipc::Complex32> {
if (live_config.bscan_channel == "s11") {
return trace.s11;
}
return trace.s21;
}
[[nodiscard]] auto fallback_profile(
const ipc::SweepTraceBlock& trace,
std::span<const ipc::Complex32> selected_samples
@@ -125,7 +115,7 @@ void fft_inplace(std::vector<std::complex<double>>& values, bool inverse) {
const ipc::SweepTraceBlock& trace,
const ProcessingLiveConfig& live_config
) -> BScanProfile {
const auto selected_samples = selected_trace_samples(trace, live_config);
const auto selected_samples = std::span<const ipc::Complex32>(trace.s21);
const std::size_t point_count = std::min(trace.frequency_hz.size(), selected_samples.size());
if (point_count < 2U) {
return fallback_profile(trace, selected_samples);
@@ -237,8 +227,6 @@ auto BScanProcessor::process_collection(
const ProcessingLiveConfig& live_config
) -> ipc::ResultCollection {
ipc::ResultCollection results{};
results.collection_id = collection.collection_id;
results.monotonic_ns = collection.monotonic_ns;
results.blocks.reserve(collection.traces.size());
for (const auto& trace : collection.traces) {
@@ -51,7 +51,7 @@ constexpr double kScoreCfEps = 1e-12;
using PairKey = std::uint64_t;
// Frequency-domain speed correction mode (Horns_motion_3libre.py MOTION_CONFIG):
// Frequency-domain speed correction mode:
// IntMinus — full intra-frequency correction to the interleaved-sweep centre.
// IntFocus — remove the constant + linear-in-frequency part of phi(f), leaving
// the focusing residual without a net Z shift.
@@ -653,9 +653,7 @@ void validate_collection_trace_order(
return traces;
}
// Frequency-domain speed correction applied to S21 *before* the IFFT, ported
// from Horns_motion_3libre.py (apply_intra_sweep_phase_correction +
// compute_frequency_sample_times). The measurement is interleaved Tx-by-frequency
// Frequency-domain speed correction applied to S21 *before* the IFFT. The measurement is interleaved Tx-by-frequency
// — Tx0(f0), Tx1(f0), Tx0(f1), ... — so each Tx occupies one interleave slot and
// its frequency k is sampled at global step (slot_count*k + slot). Each in-band
// frequency's phase is rotated back to the centre of the (full) interleaved sweep.
@@ -1740,7 +1738,7 @@ void add_bp_score_metrics(
}
}
// Select the FINAL visible objects exactly as Horns_motion_3libre.py does, so the
// Select the FINAL visible objects here, so the
// processor is the single source of truth: the GUI plot and the locator socket both
// consume this set verbatim (no second, duplicated filter). Steps, in order:
// 1. drop sidelobe candidates (when enabled), then sort by score (peak tie-break);
@@ -1844,8 +1842,6 @@ void add_bp_score_metrics(
ObjectApproachFilter& approach_filter
) -> ipc::ResultCollection {
ipc::ResultCollection results{};
results.collection_id = collection.collection_id;
results.monotonic_ns = collection.monotonic_ns;
const auto selection = build_geometry_selection(run_config, live_config);
if (selection.input_positions.empty() || selection.output_positions.empty()) {
@@ -1859,9 +1855,8 @@ void add_bp_score_metrics(
return results;
}
// Coherent BP fixes the medium to vacuum/air (eps_r = 1), matching the Python
// reference Horns_motion_3libre.py (its 0.3 block hardcodes eps_r = 1.0). The
// configurable relative_permittivity stays a legacy-GPR-only knob.
// Coherent BP fixes the medium to vacuum/air (eps_r = 1); the configurable
// relative_permittivity is a legacy-GPR-only knob.
const double velocity_mps = kSpeedOfLightMetersPerSec;
const double start_hz = static_cast<double>(live_config.gpr_start_freq_mhz) * 1'000'000.0;
const double stop_hz = static_cast<double>(live_config.gpr_stop_freq_mhz) * 1'000'000.0;
@@ -1948,7 +1943,7 @@ void add_bp_score_metrics(
min_depth_m,
max_depth_m,
std::max(0.0, static_cast<double>(live_config.gpr_range_comp_power)),
0.0 // angle compensation fixed off (Python Horns_motion_3libre.py COMP_ANGLE_POWER = 0.0)
0.0 // angle compensation is fixed off for coherent BP
);
if (bp.image.empty()) {
return results;
@@ -1980,7 +1975,7 @@ void add_bp_score_metrics(
results.collection_payloads.push_back(build_image_payload("gpr_accumulator", grid.x_grid, grid.z_grid, display_map));
// Final visible set (window + N/M), then the cross-frame approach filter: keep only
// objects confirmed as a >= min_frames motion-consistent track (Horns_motion notebook).
// objects confirmed as a >= min_frames motion-consistent track.
const auto visible = select_visible_objects(objects, live_config);
std::vector<ObjectApproachFilter::Point> visible_points{};
visible_points.reserve(visible.size());
@@ -1990,7 +1985,7 @@ void add_bp_score_metrics(
const auto confirmed = approach_filter.confirm(
visible_points,
collection.collection_id,
static_cast<double>(collection.monotonic_ns) * 1e-9,
collection.capture_start_ns,
static_cast<double>(live_config.gpr_speed_m_s),
static_cast<double>(live_config.gpr_look_angle_deg),
static_cast<std::size_t>(live_config.gpr_object_approach_min_frames)
@@ -853,8 +853,6 @@ void apply_legacy_motion_correction(
const ProcessingLiveConfig& live_config
) -> ipc::ResultCollection {
ipc::ResultCollection results{};
results.collection_id = collection.collection_id;
results.monotonic_ns = collection.monotonic_ns;
const auto selection = build_geometry_selection(run_config, live_config);
if (selection.input_positions.empty() || selection.output_positions.empty()) {
@@ -881,7 +879,7 @@ void apply_legacy_motion_correction(
// Motion timing is computed once per collection. Matrix radars get one
// event per Tx (parallel Rx); sequential radars get one event per pair.
const bool matrix_radar = is_matrix_radar_model(run_config.radar.model);
const bool matrix_radar = is_matrix_radar_model(run_config.radar_model);
const auto motion_timing = compute_legacy_motion_timing(
selected_traces,
matrix_radar,
@@ -10,11 +10,9 @@ auto PassThroughProcessor::process_collection(
const config::RunConfig& /*run_config*/,
const ipc::PreprocessedCollection& collection,
std::span<const ipc::PreprocessedCollection> /*previous_collections*/,
const ProcessingLiveConfig& live_config
const ProcessingLiveConfig& /*live_config*/
) -> ipc::ResultCollection {
ipc::ResultCollection results{};
results.collection_id = collection.collection_id;
results.monotonic_ns = collection.monotonic_ns;
results.blocks.reserve(collection.traces.size());
for (const auto& trace : collection.traces) {
@@ -22,11 +20,7 @@ auto PassThroughProcessor::process_collection(
payload.processing_name = name();
payload.kind = ipc::ResultKind::TraceComplex;
payload.frequency_hz = trace.frequency_hz;
if (live_config.pass_through_channel == "s11") {
payload.trace = trace.s11;
} else {
payload.trace = trace.s21;
}
payload.trace = trace.s21;
ipc::ResultBlock block{};
block.combo = trace.combo;
@@ -297,9 +297,8 @@ void check(nvrtcResult result, const char* what) {
}
}
if (target == 0) {
throw std::runtime_error(
"GPU sm_" + std::to_string(device_arch) + " is older than every architecture this NVRTC supports"
);
throw CudaUnavailable("CUDA GPU sm_" + std::to_string(device_arch) +
" is older than every architecture this NVRTC supports; rebuild with NVRTC 12");
}
const bool native = target == device_arch;
const std::string arch_option =
@@ -387,7 +386,7 @@ void launch(CUfunction kernel, int items, Args... args) {
} // namespace
struct CudaBackend {
struct Gpu::Impl {
// Declared first, destroyed last: buffers and the module need the context.
struct Context {
CUdevice device = 0;
@@ -395,10 +394,10 @@ struct CudaBackend {
Context() {
check(cuInit(0U), "init", true);
check(cuDeviceGet(&device, 0), "device", true);
check(cuDevicePrimaryCtxRetain(&handle, device), "context");
check(cuDevicePrimaryCtxRetain(&handle, device), "context", true);
if (const CUresult result = cuCtxSetCurrent(handle); result != CUDA_SUCCESS) {
cuDevicePrimaryCtxRelease(device);
check(result, "context");
check(result, "context", true);
}
}
~Context() { cuDevicePrimaryCtxRelease(device); }
@@ -430,8 +429,8 @@ struct CudaBackend {
DeviceBuffer images;
DeviceBuffer peak_stats;
~CudaBackend() { cuCtxSetCurrent(context.handle); }
explicit CudaBackend(std::span<const float> weights) {
~Impl() { cuCtxSetCurrent(context.handle); }
explicit Impl(std::span<const float> weights) {
smooth_radius = static_cast<int>(weights.size() / 2U);
taps.upload(weights);
}
@@ -439,13 +438,13 @@ struct CudaBackend {
auto project(const VolumeRequest&) -> Projections;
};
auto CudaBackend::inverse_dft(
auto Gpu::Impl::inverse_dft(
std::span<const std::complex<float>> bands,
std::size_t points,
std::size_t first_bin,
std::size_t size
) -> std::vector<std::complex<float>> {
CudaBackend& gpu = *this;
Impl& gpu = *this;
check(cuCtxSetCurrent(gpu.context.handle), "context");
const std::size_t samples = bands.size() / points * size;
const int items = checked_int(samples, "A-scan stack");
@@ -456,8 +455,8 @@ auto CudaBackend::inverse_dft(
return download<std::complex<float>>(output, samples);
}
auto CudaBackend::project(const VolumeRequest& request) -> Projections {
CudaBackend& gpu = *this;
auto Gpu::Impl::project(const VolumeRequest& request) -> Projections {
Impl& gpu = *this;
check(cuCtxSetCurrent(gpu.context.handle), "context");
const int nx = checked_int(request.x.size(), "grid");
@@ -524,17 +523,15 @@ auto CudaBackend::project(const VolumeRequest& request) -> Projections {
return result;
}
#else
struct Gpu::Impl {};
#endif
struct Engine::Impl {
Gpu::Gpu(double smooth_sigma_voxels) {
#ifdef SAR3D_WITH_CUDA
std::unique_ptr<CudaBackend> cuda;
#endif
};
Engine::Engine(double smooth_sigma_voxels) : impl_(std::make_unique<Impl>()) {
#ifdef SAR3D_WITH_CUDA
impl_->cuda = std::make_unique<CudaBackend>(gaussian_taps(smooth_sigma_voxels));
impl_ = std::make_unique<Impl>(gaussian_taps(smooth_sigma_voxels));
#else
(void)smooth_sigma_voxels;
throw CudaUnavailable("SAR3D requires an NVIDIA GPU with CUDA; this program was built without "
@@ -542,23 +539,24 @@ Engine::Engine(double smooth_sigma_voxels) : impl_(std::make_unique<Impl>()) {
#endif
}
Engine::~Engine() = default;
Gpu::~Gpu() = default;
auto Engine::inverse_dft(std::span<const std::complex<float>> bands, std::size_t points,
std::size_t first_bin, std::size_t size) -> std::vector<std::complex<float>> {
// Without CUDA the constructor throws, so the #else branches below only satisfy the compiler.
auto Gpu::inverse_dft(std::span<const std::complex<float>> bands, std::size_t points,
std::size_t first_bin, std::size_t size) -> std::vector<std::complex<float>> {
if (points == 0U || bands.empty() || bands.size() % points != 0U || !std::has_single_bit(size) ||
size > kMaxIndexedItems || first_bin >= size || points > size - first_bin) {
throw std::runtime_error("sar3d invalid inverse FFT dimensions");
}
(void)checked_product(bands.size() / points, size, kMaxIndexedItems, "A-scan stack");
#ifdef SAR3D_WITH_CUDA
return impl_->cuda->inverse_dft(bands, points, first_bin, size);
return impl_->inverse_dft(bands, points, first_bin, size);
#else
throw CudaUnavailable("SAR3D requires an NVIDIA GPU with CUDA");
#endif
}
auto Engine::project(const VolumeRequest& request) -> Projections {
auto Gpu::project(const VolumeRequest& request) -> Projections {
const auto nx = request.x.size(), ny = request.y.size(), nz = request.z.size();
const auto plane = checked_product(nx, nz, kMaxIndexedItems, "grid plane");
(void)checked_product(plane, ny, kMaxIndexedItems, "grid (32-bit indexing; increase SAR_VOXEL_M)");
@@ -581,11 +579,6 @@ auto Engine::project(const VolumeRequest& request) -> Projections {
}
}
}
for (const auto& sample : request.ascans) {
if (!std::isfinite(sample.real()) || !std::isfinite(sample.imag())) {
throw std::runtime_error("sar3d A-scan contains a non-finite sample");
}
}
for (const auto& term : request.contributions) {
if (!std::isfinite(term.weight) || term.weight < 0.0F ||
!std::isfinite(term.inv_reference_ranges) || term.inv_reference_ranges <= 0.0F) {
@@ -600,7 +593,7 @@ auto Engine::project(const VolumeRequest& request) -> Projections {
(void)checked_product(static_cast<std::size_t>(view.nu), static_cast<std::size_t>(view.nv),
kMaxIndexedItems, "oblique image");
#ifdef SAR3D_WITH_CUDA
return impl_->cuda->project(request);
return impl_->project(request);
#else
throw CudaUnavailable("SAR3D requires an NVIDIA GPU with CUDA");
#endif
@@ -8,7 +8,7 @@
#include <stdexcept>
#include <vector>
// Numerical backend of the sar3d processor: A-scans of the band spectra, and the
// CUDA backend of the sar3d processor: A-scans of the band spectra, and the
// backprojection of an aperture into the 3D volume, smoothed and projected.
// Only the projections and peak leave the backend.
namespace radar::processing::sar3d {
@@ -74,12 +74,12 @@ struct Projections {
// CUDA context and runtime-compiled kernels. A missing device raises
// CudaUnavailable; computation and compilation failures remain ordinary errors.
class Engine {
class Gpu {
public:
explicit Engine(double smooth_sigma_voxels);
~Engine();
Engine(const Engine&) = delete;
auto operator=(const Engine&) -> Engine& = delete;
explicit Gpu(double smooth_sigma_voxels);
~Gpu();
Gpu(const Gpu&) = delete;
auto operator=(const Gpu&) -> Gpu& = delete;
// numpy.fft.ifft of each band spectrum placed at bins [first_bin, first_bin +
// points) of a `size`-point spectrum; `bands` holds the bands back to back.
@@ -41,10 +41,10 @@ constexpr float kCompRangeWeightMax = 5.0F; // COMP_RANGE_WEIGHT_MAX
constexpr double kEdgeFrameWeight = 0.8; // SAR_EDGE_FRAME_WEIGHT
constexpr double kSmoothSigma = 1.5; // BP_SMOOTH_SIGMA, voxels
constexpr double kNormEdgeMargin = 0.3; // SAR_NORM_EDGE_MARGIN_M
// Scene window: X across, Y height above the ground (SAR_Y_RANGE_M). Z along the
// ground follows the depth gate.
constexpr double kSceneXMin = -2.0;
constexpr double kSceneXMax = 2.0;
// Scene window: X across spans the selected antennas plus a margin on each side
// (x_ant.min() - 2 ... x_ant.max() + 2), Y is the height above the ground
// (SAR_Y_RANGE_M), Z along the ground follows the depth gate.
constexpr double kSceneXMargin = 2.0;
constexpr double kSceneYMin = -0.5;
constexpr double kSceneYMax = 2.0;
static_assert(kSceneYMax - kSceneYMin > 2.5 * kNormEdgeMargin, "else sar_norm_reference_mask keeps every plane");
@@ -169,8 +169,7 @@ struct TraceClock {
};
struct CollectionTiming {
std::uint64_t center_ns = 0U; // integer centre avoids overflow and loss of relative timing
double center_fraction_ns = 0.0;
std::uint64_t center_ns = 0U; // centre of the collection's capture window
std::vector<TraceClock> clocks; // per collection trace
};
@@ -202,7 +201,6 @@ struct CollectionTiming {
CollectionTiming timing{};
const auto duration_ns = last - first;
timing.center_ns = first + duration_ns / 2U;
timing.center_fraction_ns = 0.5 * static_cast<double>(duration_ns % 2U);
for (const auto& trace : collection.traces) {
const auto& stages = sweep_outputs.at({trace.capture_start_ns, trace.capture_end_ns});
const auto stage = std::find(stages.begin(), stages.end(), trace.combo.output_pos) - stages.begin();
@@ -315,7 +313,6 @@ void normalize_ascan(std::span<std::complex<float>> ascan, double dt_s, const Se
// A-scans of every selected Tx/Rx pair of one collection, (Tx, Rx) order.
struct Frame {
std::uint64_t center_ns = 0U;
double center_fraction_ns = 0.0;
double dt_s = 0.0;
std::size_t samples = 0U; // per A-scan
std::vector<std::complex<float>> ascans{}; // [pair][sample]
@@ -324,7 +321,7 @@ struct Frame {
[[nodiscard]] auto compute_frame(
const ipc::PreprocessedCollection& collection,
const Settings& settings,
sar3d::Engine& backend
sar3d::Gpu& backend
) -> Frame {
const auto timing = collection_timing(collection);
std::vector<Band> bands{};
@@ -354,7 +351,6 @@ struct Frame {
}
Frame frame{};
frame.center_ns = timing.center_ns;
frame.center_fraction_ns = timing.center_fraction_ns;
frame.samples = first.size;
frame.dt_s = 1.0 / (static_cast<double>(first.size) * first.df_hz);
frame.ascans = backend.inverse_dft(spectra, first.spectrum.size(), first.first_bin, first.size);
@@ -399,6 +395,15 @@ struct Grid {
if (std::abs(cos_p) < 1e-6) {
throw std::runtime_error("SAR grid is undefined at a radar look angle of 90 degrees");
}
// The pitch turns the radar about X, so antenna X is already the scene X.
double x_min = std::numeric_limits<double>::infinity();
double x_max = -std::numeric_limits<double>::infinity();
for (const auto* antennas : {&settings.tx, &settings.rx}) {
for (const auto& antenna : *antennas) {
x_min = std::min(x_min, antenna[0] - kSceneXMargin);
x_max = std::max(x_max, antenna[0] + kSceneXMargin);
}
}
double z_min = std::numeric_limits<double>::infinity();
double z_max = -std::numeric_limits<double>::infinity();
for (const double height : {kSceneYMin, kSceneYMax}) {
@@ -414,13 +419,13 @@ struct Grid {
const auto axis_size = [&](double span) {
return checked_axis_size(std::max(std::nearbyint(span / settings.voxel_m) + 1.0, 2.0));
};
const auto nx = axis_size(kSceneXMax - kSceneXMin);
const auto nx = axis_size(x_max - x_min);
const auto ny = axis_size(kSceneYMax - kSceneYMin);
const auto nz = axis_size(z_max - z_min);
if (nx > static_cast<std::size_t>(std::numeric_limits<int>::max()) / ny / nz) {
throw std::runtime_error("SAR grid exceeds 32-bit kernel indexing; increase SAR_VOXEL_M");
}
return {linspace(kSceneXMin, kSceneXMax, nx), linspace(kSceneYMin, kSceneYMax, ny),
return {linspace(x_min, x_max, nx), linspace(kSceneYMin, kSceneYMax, ny),
linspace(z_min, z_max, nz)};
}
@@ -571,11 +576,11 @@ struct Oblique {
} // namespace
struct Sar3dProcessor::State {
// A sweep is its id with its timestamp: ids start again from 1 on every run,
// Acquisition identity is (id, capture start): ids restart on every run,
// while the processor and this cache outlive runs.
using SweepKey = std::pair<std::uint64_t, std::uint64_t>;
[[nodiscard]] static auto sweep_key(const ipc::PreprocessedCollection& collection) -> SweepKey {
return {collection.collection_id, collection.monotonic_ns};
return {collection.collection_id, collection.capture_start_ns};
}
struct CachedFrame {
@@ -584,12 +589,12 @@ struct Sar3dProcessor::State {
};
std::map<SweepKey, CachedFrame> frames{};
AscanKey frames_key{};
std::unique_ptr<sar3d::Engine> engine{};
std::exception_ptr engine_error{};
std::unique_ptr<sar3d::Gpu> device{};
std::exception_ptr device_error{}; // why the GPU could not be opened; not retried
std::string last_error{};
std::uint64_t repeated_errors = 0U;
auto frame(const ipc::PreprocessedCollection& collection, const Settings& settings, sar3d::Engine& backend)
auto frame(const ipc::PreprocessedCollection& collection, const Settings& settings, sar3d::Gpu& backend)
-> std::shared_ptr<const Frame> {
if (const auto found = frames.find(sweep_key(collection)); found != frames.end()) {
if (found->second.value == nullptr) { report_error(found->second.error); }
@@ -607,13 +612,13 @@ struct Sar3dProcessor::State {
return frame;
}
auto numerical_engine() -> sar3d::Engine& {
if (engine == nullptr && engine_error == nullptr) {
try { engine = std::make_unique<sar3d::Engine>(kSmoothSigma); }
catch (...) { engine_error = std::current_exception(); }
auto gpu() -> sar3d::Gpu& {
if (device == nullptr && device_error == nullptr) {
try { device = std::make_unique<sar3d::Gpu>(kSmoothSigma); }
catch (...) { device_error = std::current_exception(); }
}
if (engine == nullptr) { std::rethrow_exception(engine_error); }
return *engine;
if (device == nullptr) { std::rethrow_exception(device_error); }
return *device;
}
auto render(
@@ -621,14 +626,12 @@ struct Sar3dProcessor::State {
std::span<const ipc::PreprocessedCollection> previous,
const Settings& settings
) -> std::vector<ipc::ResultPayload> {
auto& backend = numerical_engine();
auto& backend = gpu();
if (settings.ascan_key() != frames_key) {
frames.clear();
frames_key = settings.ascan_key();
}
const auto history_count = settings.frame_radius >= previous.size() / 2U + previous.size() % 2U
? previous.size() : 2U * settings.frame_radius;
const auto recent = previous.last(history_count);
const auto recent = previous.last(std::min(previous.size(), 2U * settings.frame_radius));
std::set<SweepKey> retained{sweep_key(collection)};
for (const auto& member : recent) { retained.insert(sweep_key(member)); }
// Prune even when the newest frame is invalid, so a bad input stream
@@ -657,10 +660,9 @@ struct Sar3dProcessor::State {
std::vector<double> offsets_s{};
for (const auto& member : aperture) {
const double delta_ns = member->center_ns >= centre.center_ns
? static_cast<double>(member->center_ns - centre.center_ns)
: -static_cast<double>(centre.center_ns - member->center_ns);
offsets_s.push_back(1e-9 * (delta_ns + member->center_fraction_ns - centre.center_fraction_ns));
// Unsigned difference reinterpreted as signed: exact for any two timestamps.
const auto delta_ns = static_cast<std::int64_t>(member->center_ns - centre.center_ns);
offsets_s.push_back(1e-9 * static_cast<double>(delta_ns));
}
const auto weights = frame_weights(offsets_s);
const Grid grid = scene_grid(settings);
@@ -797,11 +799,18 @@ auto Sar3dProcessor::process_collection(
const ProcessingLiveConfig& live_config
) -> ipc::ResultCollection {
ipc::ResultCollection results{};
results.collection_id = collection.collection_id;
results.monotonic_ns = collection.monotonic_ns;
try {
results.collection_payloads =
state_->render(collection, previous_collections, read_settings(run_config, live_config));
// RSL3 includes the typed metadata in its fixed header.
const auto bytes = ipc::serialize_result_collection(results).size();
const std::size_t slot = run_config.rings.results.slot_size_bytes;
if (bytes > slot) {
throw std::runtime_error("the views need " + std::to_string(bytes / 1024U) +
" KiB but a results ring slot holds " + std::to_string(slot / 1024U) +
" KiB; increase SAR_VOXEL_M, narrow the depth range or raise "
"rings.results.slot_size_bytes");
}
} catch (const sar3d::CudaUnavailable& error) {
state_->report_error(error.what());
results.collection_payloads = {status_payload(3.0F, error.what())};