sar3d: GPU port of the notebook with four views

The sar3d processor is rewritten as a checked port of Multi_antenna_3D_clean_v1.ipynb:
NVRTC kernels, timing from the trace stamps, notebook parameter names and one cubic
SAR_VOXEL_M. It adds front, side, top and 3D views, drops gpr_cpp and the parity tools,
and the GUI speed now applies only while the socket speed is ignored.
This commit is contained in:
Ayzen
2026-10-02 16:05:22 +03:00
parent 61ef8720e1
commit 45861f4047
50 changed files with 1737 additions and 8144 deletions
@@ -2,6 +2,7 @@
#include <atomic>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <unordered_map>
@@ -40,11 +41,10 @@ class DataProcessor {
[[nodiscard]] auto resolve_processor(const ProcessingLiveConfig& live_config) -> ProcessorInterface&;
[[nodiscard]] auto should_replay_entire_history(const ProcessingLiveConfig& live_config) const -> bool;
// Merge live config with the latest socket-supplied speed (if any and if
// not suppressed by `ignore_socket_speed`). The returned config is what
// actually drives processing for this tick.
[[nodiscard]] auto resolve_effective_live_config(const ProcessingLiveConfig& live_config) const
-> ProcessingLiveConfig;
// Speed streamed by a locator client (`vlc`), which then replaces
// `gpr_speed_m_s`; nullopt when `ignore_socket_speed` is set or no fresh
// value exists, so the speed from the live config stays in force.
[[nodiscard]] auto socket_speed(const ProcessingLiveConfig& live_config) const -> std::optional<double>;
// Convert one processed collection into a locator filter spec, derived
// from the live config and current processor mode.
@@ -90,51 +90,17 @@ struct ProcessingLiveConfig {
float gpr_visible_x_max_m = 2.0F;
float gpr_visible_z_min_m = 0.0F;
float gpr_visible_z_max_m = 14.0F;
// 3D SAR (sar3d processor) parameters; defaults mirror the reference
// notebook Multi_antenna_3D_clean_v1.ipynb (cells 0/4). Frequency band,
// depth gate, antenna selection and motion (speed, look angle, direction,
// phase correction) are shared with the gpr_* fields.
// 3D SAR (sar3d processor) parameters, named after the reference notebook
// Multi_antenna_3D_clean_v1.ipynb (SAR_FRAME_RADIUS, BP_GROUND_DEPTH_M, ...).
// The band, depth gate, antenna selection, speed and look angle are the
// shared gpr_* fields above.
std::uint32_t sar3d_frame_radius = 0U;
float sar3d_edge_frame_weight = 0.8F;
// Frame pitch for ground coordinates and inter-frame offsets. Override
// disabled -> the pitch follows gpr_look_angle_deg (notebook
// BP_GROUND_PITCH_DEG = None).
bool sar3d_pitch_override_enabled = false;
float sar3d_pitch_deg = 17.5F;
// Height of the LOWER antenna row above the ground; scene Y=0 is the ground.
float sar3d_ground_depth_m = 1.25F;
// Scene grid bounds (metres): X across, Y height above the ground, Z along
// the ground away from the radar. Voxels whose radar depth falls outside
// [gpr_min_depth, gpr_max_depth] are zeroed by the mask regardless.
// Doubles, not floats: the parity tool feeds the notebook-derived bounds
// through here and a float roundtrip shifts the grid by ~1e-7 m, flipping
// knife-edge voxels of the depth mask against the reference volumes.
double sar3d_x_min_m = -2.0;
double sar3d_x_max_m = 2.0;
double sar3d_y_min_m = -0.5;
double sar3d_y_max_m = 2.0;
double sar3d_z_min_m = 0.0;
double sar3d_z_max_m = 8.0;
float sar3d_voxel_x_m = 0.025F;
float sar3d_voxel_y_m = 0.02F;
float sar3d_voxel_z_m = 0.02F;
// Volume finalization: normalization ignores this much of the grid's Y
// edges (constant-range ridge), then Gaussian smoothing over (Z, X).
float sar3d_norm_edge_margin_m = 0.3F;
float sar3d_smooth_sigma = 1.5F;
// Geometric spreading compensation 1/(Rtx*Rrx)^power, capped.
// Edge of the cubic voxel: the notebook's SAR_VOXEL_M with one value for X, Y, Z.
float sar3d_voxel_m = 0.02F;
float sar3d_bp_ground_depth_m = 1.25F;
float sar3d_comp_range_power = 0.3F;
// Fallback frame segmentation when per-trace capture stamps are absent.
std::uint32_t sar3d_tx_per_segment = 2U;
bool sar3d_use_compensation = true;
bool sar3d_normalize_pairs = true;
// Debug: when non-empty, the processor dumps every finished volume to this
// directory (raw float32 + json header). Used by the parity tool only.
std::string sar3d_volume_dump_dir{};
// Bottom-view projection angle around the vertical Y axis, degrees:
// 0 = front view (X horizontal), 90 = side view (Z horizontal),
// intermediate values project along the rotated horizontal direction.
float sar3d_bottom_view_deg = 0.0F;
bool sar3d_normalize_each_frame = true;
// When true, the data_processor ignores socket-supplied `vlc` updates and
// keeps using `gpr_speed_m_s` from this file. Mirrored from the GUI's
// "ignore socket speed" checkbox.
@@ -73,19 +73,16 @@ void DataProcessor::run(const std::atomic<bool>& stop_requested) {
while (!stop_requested.load(std::memory_order_relaxed)) {
try {
const auto live_config_raw = live_config_loader_.refresh_if_needed();
const auto live_config = resolve_effective_live_config(live_config_raw);
// The socket speed is read once per tick: it both drives this tick's
// processing and, when it changed, triggers a reprocess on its own,
// even when the live revision is unchanged.
auto live_config = live_config_loader_.refresh_if_needed();
const std::optional<double> current_socket_speed = socket_speed(live_config);
if (current_socket_speed.has_value()) {
live_config.gpr_speed_m_s = static_cast<float>(*current_socket_speed);
}
const auto live_revision = live_config_loader_.revision();
auto& processor = resolve_processor(live_config);
// The effective config folds in latest_socket_speed(); a change there is
// a reprocess trigger on its own, even when the live revision is unchanged.
// Mirror the gating in resolve_effective_live_config(): only consider the
// socket speed when it is actually applied to the effective config.
const std::optional<double> current_socket_speed =
(live_config_raw.ignore_socket_speed || locator_server_ == nullptr)
? std::nullopt
: locator_server_->latest_socket_speed();
const bool socket_speed_dirty = current_socket_speed != last_reprocessed_socket_speed;
if (live_revision != last_replayed_revision || socket_speed_dirty) {
@@ -213,18 +210,11 @@ auto DataProcessor::should_replay_entire_history(const ProcessingLiveConfig& liv
return requested_mode == "bscan";
}
auto DataProcessor::resolve_effective_live_config(const ProcessingLiveConfig& live_config) const
-> ProcessingLiveConfig {
auto DataProcessor::socket_speed(const ProcessingLiveConfig& live_config) const -> std::optional<double> {
if (live_config.ignore_socket_speed || locator_server_ == nullptr) {
return live_config;
return std::nullopt;
}
const auto socket_speed = locator_server_->latest_socket_speed();
if (!socket_speed.has_value()) {
return live_config;
}
ProcessingLiveConfig effective = live_config;
effective.gpr_speed_m_s = static_cast<float>(*socket_speed);
return effective;
return locator_server_->latest_socket_speed();
}
auto DataProcessor::build_locator_filter(const ProcessingLiveConfig& live_config) const
@@ -390,16 +390,9 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s
config.sar3d_frame_radius = parse_u32_number(*found, "processing.sar3d_frame_radius");
}
for (const auto& [key, target] : {
std::pair{"sar3d_edge_frame_weight", &config.sar3d_edge_frame_weight},
std::pair{"sar3d_pitch_deg", &config.sar3d_pitch_deg},
std::pair{"sar3d_ground_depth_m", &config.sar3d_ground_depth_m},
std::pair{"sar3d_voxel_x_m", &config.sar3d_voxel_x_m},
std::pair{"sar3d_voxel_y_m", &config.sar3d_voxel_y_m},
std::pair{"sar3d_voxel_z_m", &config.sar3d_voxel_z_m},
std::pair{"sar3d_norm_edge_margin_m", &config.sar3d_norm_edge_margin_m},
std::pair{"sar3d_smooth_sigma", &config.sar3d_smooth_sigma},
std::pair{"sar3d_voxel_m", &config.sar3d_voxel_m},
std::pair{"sar3d_bp_ground_depth_m", &config.sar3d_bp_ground_depth_m},
std::pair{"sar3d_comp_range_power", &config.sar3d_comp_range_power},
std::pair{"sar3d_bottom_view_deg", &config.sar3d_bottom_view_deg},
}) {
if (const auto found = root.find(key); found != root.end()) {
if (!found->is_number()) {
@@ -409,30 +402,8 @@ void apply_legacy_gpr_algorithm_alias(ProcessingLiveConfig& config, const std::s
}
}
for (const auto& [key, target] : {
std::pair{"sar3d_x_min_m", &config.sar3d_x_min_m},
std::pair{"sar3d_x_max_m", &config.sar3d_x_max_m},
std::pair{"sar3d_y_min_m", &config.sar3d_y_min_m},
std::pair{"sar3d_y_max_m", &config.sar3d_y_max_m},
std::pair{"sar3d_z_min_m", &config.sar3d_z_min_m},
std::pair{"sar3d_z_max_m", &config.sar3d_z_max_m},
}) {
if (const auto found = root.find(key); found != root.end()) {
if (!found->is_number()) {
throw std::runtime_error(std::string("processing.") + key + " must be number");
}
*target = found->get<double>();
}
}
if (const auto found = root.find("sar3d_tx_per_segment"); found != root.end()) {
config.sar3d_tx_per_segment = parse_u32_number(*found, "processing.sar3d_tx_per_segment");
if (config.sar3d_tx_per_segment == 0U) {
throw std::runtime_error("processing.sar3d_tx_per_segment must be >= 1");
}
}
for (const auto& [key, target] : {
std::pair{"sar3d_pitch_override_enabled", &config.sar3d_pitch_override_enabled},
std::pair{"sar3d_use_compensation", &config.sar3d_use_compensation},
std::pair{"sar3d_normalize_pairs", &config.sar3d_normalize_pairs},
std::pair{"sar3d_normalize_each_frame", &config.sar3d_normalize_each_frame},
}) {
if (const auto found = root.find(key); found != root.end()) {
if (!found->is_boolean()) {