Rework radar system
This commit is contained in:
+15
-21
@@ -10,12 +10,6 @@
|
||||
|
||||
namespace radar::processing {
|
||||
|
||||
enum class HistoryCommand {
|
||||
None,
|
||||
RemoveLast,
|
||||
ClearAll,
|
||||
};
|
||||
|
||||
enum class LegacyGprMode {
|
||||
Point,
|
||||
Extended,
|
||||
@@ -29,16 +23,16 @@ enum class LegacyGprReferenceMode {
|
||||
FirstTxEvent,
|
||||
};
|
||||
|
||||
struct HistoryTarget {
|
||||
std::uint64_t collection_id = 0;
|
||||
std::uint64_t capture_start_ns = 0;
|
||||
};
|
||||
|
||||
struct ProcessingLiveConfig {
|
||||
std::string processor_mode = "pass_through";
|
||||
std::string pass_through_channel = "s21";
|
||||
bool pass_through_fixed_y_enabled = false;
|
||||
float pass_through_y_min_db = -100.0F;
|
||||
float pass_through_y_max_db = 0.0F;
|
||||
std::string bscan_axis = "abs";
|
||||
std::string bscan_channel = "s21";
|
||||
float bscan_cut_m = 0.0F;
|
||||
float bscan_max_depth_m = 8.0F;
|
||||
float bscan_cut_m = 0.824F;
|
||||
float bscan_max_depth_m = 1.0F;
|
||||
float bscan_gain = 1.0F;
|
||||
float bscan_start_freq_mhz = 100.0F;
|
||||
float bscan_stop_freq_mhz = 8800.0F;
|
||||
@@ -51,13 +45,12 @@ struct ProcessingLiveConfig {
|
||||
float gpr_max_depth_m = 14.0F;
|
||||
float gpr_range_comp_power = 0.1F;
|
||||
float gpr_comp_power = 0.2F;
|
||||
// BP object-detection stop level, as a fraction of the global peak (Python
|
||||
// Horns_motion_3libre.py BP_OBJECT_MIN_FRAC); peaks below it are not objects.
|
||||
// BP object-detection stop level, as a fraction of the global peak; peaks
|
||||
// below it are not objects.
|
||||
float gpr_object_min_frac = 0.7F;
|
||||
std::string gpr_score_mode = "combined";
|
||||
// Backprojection intra-sweep speed-correction mode: "int_minus" (full
|
||||
// correction) or "int_focus" (focusing residual only). Mirrors the Python
|
||||
// Horns_motion_3libre.py MOTION_CORRECTION_MODE selector.
|
||||
// correction) or "int_focus" (focusing residual only).
|
||||
std::string gpr_motion_mode = "int_minus";
|
||||
float gpr_speed_m_s = 0.0F;
|
||||
float gpr_look_angle_deg = 0.0F;
|
||||
@@ -81,8 +74,8 @@ struct ProcessingLiveConfig {
|
||||
// Default 0 keeps legacy 1D antenna layouts imaging in the antenna plane.
|
||||
float gpr_imaging_plane_y_m = 0.0F;
|
||||
// Coherent BP object visibility (window + the draw limits below) is applied in
|
||||
// the processor itself, matching Horns_motion_3libre.py — there is NO score
|
||||
// threshold for it. Only legacy GPR still thresholds, on a pair count.
|
||||
// the processor itself, with no score threshold. Only legacy GPR thresholds,
|
||||
// on a pair count.
|
||||
float legacy_gpr_min_visible_pair_count = 0.0F;
|
||||
std::uint32_t gpr_max_detected_objects_to_draw = 0;
|
||||
std::uint32_t gpr_draw_top_m_objects = 0;
|
||||
@@ -111,8 +104,9 @@ struct ProcessingLiveConfig {
|
||||
// "ignore socket speed" checkbox.
|
||||
bool ignore_socket_speed = false;
|
||||
bool reprocess_current_result = true;
|
||||
std::uint64_t history_command_seq = 0;
|
||||
HistoryCommand history_command = HistoryCommand::None;
|
||||
// Persistent desired history: normal settings writes must preserve deletions.
|
||||
std::uint64_t history_clear_before_ns = 0;
|
||||
std::vector<HistoryTarget> history_removed{};
|
||||
std::uint64_t settings_revision = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "data_processor.hpp"
|
||||
#include "runtime_wait.hpp"
|
||||
#include "pipeline_runtime.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@@ -69,9 +70,7 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
|
||||
std::vector<ipc::PreprocessedCollection> preprocessed_history{};
|
||||
const std::size_t history_limit = replay_history_limit(config_);
|
||||
std::uint64_t last_replayed_revision = live_config_loader_.revision();
|
||||
std::uint64_t last_applied_history_command_seq = 0;
|
||||
std::uint64_t error_count = 0;
|
||||
std::uint64_t consecutive_errors = 0;
|
||||
runtime::CollectionErrors errors;
|
||||
std::uint64_t oversized_results = 0;
|
||||
// track the socket-fed speed used for the last reprocess so a change
|
||||
// arriving without a live-config revision bump still triggers a reprocess of
|
||||
@@ -95,17 +94,19 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
|
||||
auto& processor = resolve_processor(live_config);
|
||||
const bool socket_speed_dirty = current_socket_speed != last_reprocessed_socket_speed;
|
||||
|
||||
if (live_revision != last_replayed_revision || socket_speed_dirty) {
|
||||
if (live_config.history_command_seq > last_applied_history_command_seq) {
|
||||
if (live_config.history_command == HistoryCommand::RemoveLast) {
|
||||
if (!preprocessed_history.empty()) {
|
||||
preprocessed_history.pop_back();
|
||||
}
|
||||
} else if (live_config.history_command == HistoryCommand::ClearAll) {
|
||||
preprocessed_history.clear();
|
||||
}
|
||||
last_applied_history_command_seq = live_config.history_command_seq;
|
||||
const auto was_removed = [&](const ipc::PreprocessedCollection& collection) {
|
||||
if (live_config.history_clear_before_ns &&
|
||||
collection.capture_start_ns <= live_config.history_clear_before_ns) {
|
||||
return true;
|
||||
}
|
||||
return std::any_of(live_config.history_removed.begin(), live_config.history_removed.end(),
|
||||
[&](const HistoryTarget& target) {
|
||||
return collection.collection_id == target.collection_id &&
|
||||
collection.capture_start_ns == target.capture_start_ns;
|
||||
});
|
||||
};
|
||||
if (live_revision != last_replayed_revision || socket_speed_dirty) {
|
||||
std::erase_if(preprocessed_history, was_removed);
|
||||
|
||||
if (!live_config.reprocess_current_result) {
|
||||
// Socket-fed speed updates should affect only future preprocessed collections,
|
||||
@@ -139,6 +140,12 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
|
||||
|
||||
if (preprocessed_ring_.pop(bytes)) {
|
||||
auto preprocessed = ipc::deserialize_preprocessed_collection(bytes);
|
||||
if (!preprocessed.capture_start_ns || preprocessed.capture_end_ns < preprocessed.capture_start_ns) {
|
||||
throw std::runtime_error("Preprocessed collection requires a valid acquisition window");
|
||||
}
|
||||
if (was_removed(preprocessed)) {
|
||||
continue; // A deletion also applies to queued input and later GUI replays.
|
||||
}
|
||||
// Sweeps the GUI feeds back to recompute its B-scan keep their original
|
||||
// timestamps, so they are no newer than the newest retained sweep. They
|
||||
// are processed like any other but stay out of the history, which must
|
||||
@@ -146,7 +153,7 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
|
||||
// background from it), and away from the locator, which reports the
|
||||
// current scene.
|
||||
const bool fed_back = !preprocessed_history.empty() &&
|
||||
preprocessed.monotonic_ns <= preprocessed_history.back().monotonic_ns;
|
||||
preprocessed.capture_start_ns <= preprocessed_history.back().capture_start_ns;
|
||||
if (!fed_back) {
|
||||
preprocessed_history.push_back(std::move(preprocessed));
|
||||
while (preprocessed_history.size() > history_limit) {
|
||||
@@ -157,7 +164,7 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
|
||||
const auto earlier = std::partition_point(
|
||||
preprocessed_history.begin(),
|
||||
preprocessed_history.end(),
|
||||
[&](const ipc::PreprocessedCollection& retained) { return retained.monotonic_ns < current.monotonic_ns; }
|
||||
[&](const ipc::PreprocessedCollection& retained) { return retained.capture_start_ns < current.capture_start_ns; }
|
||||
);
|
||||
|
||||
const auto result_collection = process_collection(
|
||||
@@ -170,29 +177,13 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
|
||||
if (!fed_back) {
|
||||
publish_locator(result_collection, live_config);
|
||||
}
|
||||
consecutive_errors = 0; // made progress; reset the escalation counter
|
||||
errors.progress();
|
||||
continue;
|
||||
}
|
||||
|
||||
(void)config::wait_or_stop(config_.runtime.idle_sleep_ms, stop_requested);
|
||||
} catch (const std::exception& exc) {
|
||||
// A single bad collection (torn ring slot, decode/processing error) must
|
||||
// not kill the long-running processor: drop it and keep going.
|
||||
++error_count;
|
||||
++consecutive_errors;
|
||||
// Escalate a PERSISTENT failure (a deterministic config error drops every
|
||||
// collection forever): log it loudly and distinctly — not buried in the
|
||||
// every-100 throttle — so liveness-only supervision still surfaces it.
|
||||
if (consecutive_errors % 500 == 0) {
|
||||
std::cerr << "data_processor: " << consecutive_errors
|
||||
<< " consecutive failures — likely a permanent configuration error: "
|
||||
<< exc.what() << '\n';
|
||||
} else if (error_count % 100 == 1) {
|
||||
std::cerr << "data_processor: dropped collection after error (count="
|
||||
<< error_count << "): " << exc.what() << '\n';
|
||||
}
|
||||
// Back-off (floored at 1ms even if idle_sleep_ms==0) so a persistent error
|
||||
// cannot busy-spin a core or flood the log.
|
||||
errors.report("data_processor", exc);
|
||||
(void)config::wait_or_stop(std::max(1U, config_.runtime.idle_sleep_ms), stop_requested);
|
||||
}
|
||||
}
|
||||
@@ -210,15 +201,13 @@ auto DataProcessor::process_collection(
|
||||
result.processing_duration_ns = static_cast<std::uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(finished_at - started_at).count()
|
||||
);
|
||||
// Four 16-bit words retain the uint64 exactly in the existing float32 table format.
|
||||
ipc::ResultPayload revision{};
|
||||
revision.processing_name = "processing_revision";
|
||||
revision.kind = ipc::ResultKind::TableF32;
|
||||
revision.table_columns = 4U;
|
||||
for (unsigned shift = 0; shift < 64; shift += 16) {
|
||||
revision.table_values.push_back(static_cast<float>((live_config.settings_revision >> shift) & 0xffffU));
|
||||
}
|
||||
result.collection_payloads.push_back(std::move(revision));
|
||||
result.collection_id = preprocessed.collection_id;
|
||||
result.monotonic_ns = ipc::current_monotonic_ns();
|
||||
result.settings_revision = live_config.settings_revision;
|
||||
result.capture_start_ns = preprocessed.capture_start_ns;
|
||||
result.capture_end_ns = preprocessed.capture_end_ns;
|
||||
// Socket speed can change without a GUI revision; retain the actual value per result.
|
||||
result.speed_m_s = live_config.gpr_speed_m_s;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -268,7 +257,7 @@ auto DataProcessor::build_locator_filter(const ProcessingLiveConfig& live_config
|
||||
filter.draw_limits.reset();
|
||||
}
|
||||
// Coherent BP already emits the FINAL visible object set from the processor
|
||||
// (window + N/M, no score threshold — matching Horns_motion_3libre.py), so the
|
||||
// (window + N/M, no score threshold), so the
|
||||
// socket forwards it verbatim. The default FilterParams passes everything through
|
||||
// (it only drops non-finite rows), keeping the filtering logic in one place.
|
||||
return filter;
|
||||
|
||||
@@ -1,43 +1,14 @@
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "data_processor.hpp"
|
||||
#include "locator/tcp_server.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);
|
||||
// Writing to a peer-closed socket would otherwise terminate the process.
|
||||
std::signal(SIGPIPE, SIG_IGN);
|
||||
}
|
||||
|
||||
[[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;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto start_locator_server(const radar::config::RunConfig& config)
|
||||
-> std::unique_ptr<radar::locator::TcpServer> {
|
||||
if (!config.locator_server.enabled) {
|
||||
@@ -56,11 +27,8 @@ void install_signal_handlers() {
|
||||
} // 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 preprocessed_ring = radar::ipc::ShmRing::open_or_create(
|
||||
config.rings.preprocessed.name,
|
||||
@@ -82,7 +50,7 @@ int main(int argc, char** argv) {
|
||||
radar::processing::create_default_processors(),
|
||||
locator_server.get()
|
||||
);
|
||||
processor.run(g_stop_requested);
|
||||
processor.run(radar::runtime::stop_requested);
|
||||
return 0;
|
||||
} catch (const std::exception& exception) {
|
||||
std::cerr << "data_processor error: " << exception.what() << '\n';
|
||||
|
||||
@@ -19,19 +19,6 @@ namespace {
|
||||
|
||||
using Json = nlohmann::json;
|
||||
|
||||
[[nodiscard]] auto parse_history_command(const std::string& value) -> HistoryCommand {
|
||||
if (value == "none") {
|
||||
return HistoryCommand::None;
|
||||
}
|
||||
if (value == "remove_last") {
|
||||
return HistoryCommand::RemoveLast;
|
||||
}
|
||||
if (value == "clear_all") {
|
||||
return HistoryCommand::ClearAll;
|
||||
}
|
||||
throw std::runtime_error("processing.history_command must be one of: none, remove_last, clear_all");
|
||||
}
|
||||
|
||||
[[nodiscard]] auto parse_legacy_gpr_mode(const std::string& value, const std::string& field_name) -> LegacyGprMode {
|
||||
if (value == "point") {
|
||||
return LegacyGprMode::Point;
|
||||
@@ -86,13 +73,6 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] auto parse_s_parameter_channel(const std::string& value, const std::string& field_name) -> std::string {
|
||||
if (value == "s21" || value == "s11") {
|
||||
return value;
|
||||
}
|
||||
throw std::runtime_error(field_name + " must be one of: s21, s11");
|
||||
}
|
||||
|
||||
[[nodiscard]] auto parse_f32(const Json& value, const std::string& field_name) -> float {
|
||||
if (!value.is_number()) {
|
||||
throw std::runtime_error(field_name + " must be number");
|
||||
@@ -226,23 +206,7 @@ auto parse_geometry(const Json& value, const char* position_field, std::uint32_t
|
||||
config::validate_gpr_config(*config.gpr, run_config);
|
||||
}
|
||||
assign_optional(root, "processor_mode", config.processor_mode);
|
||||
if (const auto found = root.find("pass_through_channel"); found != root.end()) {
|
||||
if (!found->is_string()) {
|
||||
throw std::runtime_error("processing.pass_through_channel must be string");
|
||||
}
|
||||
config.pass_through_channel =
|
||||
parse_s_parameter_channel(found->get<std::string>(), "processing.pass_through_channel");
|
||||
}
|
||||
assign_optional(root, "pass_through_fixed_y_enabled", config.pass_through_fixed_y_enabled);
|
||||
assign_optional(root, "pass_through_y_min_db", config.pass_through_y_min_db);
|
||||
assign_optional(root, "pass_through_y_max_db", config.pass_through_y_max_db);
|
||||
assign_optional(root, "bscan_axis", config.bscan_axis);
|
||||
if (const auto found = root.find("bscan_channel"); found != root.end()) {
|
||||
if (!found->is_string()) {
|
||||
throw std::runtime_error("processing.bscan_channel must be string");
|
||||
}
|
||||
config.bscan_channel = parse_s_parameter_channel(found->get<std::string>(), "processing.bscan_channel");
|
||||
}
|
||||
assign_optional(root, "bscan_cut_m", config.bscan_cut_m);
|
||||
assign_optional(root, "bscan_max_depth_m", config.bscan_max_depth_m);
|
||||
assign_optional(root, "bscan_gain", config.bscan_gain);
|
||||
@@ -344,14 +308,23 @@ auto parse_geometry(const Json& value, const char* position_field, std::uint32_t
|
||||
}
|
||||
assign_optional(root, "ignore_socket_speed", config.ignore_socket_speed);
|
||||
assign_optional(root, "reprocess_current_result", config.reprocess_current_result);
|
||||
if (const auto found = root.find("history_command_seq"); found != root.end()) {
|
||||
config.history_command_seq = parse_u64_number(*found, "processing.history_command_seq");
|
||||
if (const auto found = root.find("history_clear_before_ns"); found != root.end()) {
|
||||
config.history_clear_before_ns = parse_u64_number(*found, "processing.history_clear_before_ns");
|
||||
}
|
||||
if (const auto found = root.find("history_command"); found != root.end()) {
|
||||
if (!found->is_string()) {
|
||||
throw std::runtime_error("processing.history_command must be string");
|
||||
if (const auto found = root.find("history_removed"); found != root.end()) {
|
||||
if (!found->is_array()) {
|
||||
throw std::runtime_error("processing.history_removed must be an array");
|
||||
}
|
||||
for (const auto& value : *found) {
|
||||
HistoryTarget target{
|
||||
parse_u64_number(value.at("collection_id"), "history_removed.collection_id"),
|
||||
parse_u64_number(value.at("capture_start_ns"), "history_removed.capture_start_ns"),
|
||||
};
|
||||
if (!target.capture_start_ns) {
|
||||
throw std::runtime_error("history_removed.capture_start_ns must be positive");
|
||||
}
|
||||
config.history_removed.push_back(target);
|
||||
}
|
||||
config.history_command = parse_history_command(found->get<std::string>());
|
||||
}
|
||||
return config;
|
||||
}
|
||||
@@ -393,9 +366,10 @@ auto ProcessingLiveConfigLoader::refresh_if_needed() -> ProcessingLiveConfig {
|
||||
return current_;
|
||||
}
|
||||
|
||||
current_ = parse_live_config(json_text, path_, run_config_);
|
||||
// Remember rejected content too; retry only when the file actually changes.
|
||||
last_json_text_ = json_text;
|
||||
has_last_json_text_ = true;
|
||||
current_ = parse_live_config(json_text, path_, run_config_);
|
||||
++revision_;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "data_processor warning: failed to refresh live processing config: " << error.what() << '\n';
|
||||
|
||||
@@ -36,7 +36,6 @@ class ClientQueue {
|
||||
// Wake any waiter and reject further pushes. Idempotent.
|
||||
void close();
|
||||
|
||||
[[nodiscard]] auto is_closed() const -> bool;
|
||||
|
||||
private:
|
||||
mutable std::mutex mutex_{};
|
||||
|
||||
@@ -197,11 +197,6 @@ void ClientQueue::close() {
|
||||
not_empty_.notify_all();
|
||||
}
|
||||
|
||||
auto ClientQueue::is_closed() const -> bool {
|
||||
std::lock_guard<std::mutex> guard(mutex_);
|
||||
return closed_;
|
||||
}
|
||||
|
||||
// ----- ClientSession --------------------------------------------------------
|
||||
|
||||
ClientSession::ClientSession(
|
||||
|
||||
@@ -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