Rework radar system
This commit is contained in:
@@ -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())};
|
||||
|
||||
Reference in New Issue
Block a user