sar3d: port the Multi_antenna_3D_clean_v1 notebook algorithm

The old sar3d core was a port of the superseded Horns_3D notebook and
produced wrong volumes. The compute core now follows the reference
notebook exactly:

- segmented_interleaved frame timing rebuilt from per-trace capture
  stamps (uniform tx_per_segment split as the no-stamps fallback), with
  the coarse inter-segment dtau and the intra-sweep dtau applied as one
  phase ramp before the IFFT;
- ground scene coordinates with the ground_depth_m lift (scene Y=0 is
  the ground), full-region grid instead of a window around a center
  depth, geo_ref compensation anchored on the ground plane, power 0.3;
- notebook finalization in C++: |acc/cnt|, depth mask, normalization
  without the Y-edge planes, separable Gaussian over (Z, X), level;
- CUDA kernel computes geometry in double: float32 loses ~3e-4 of an
  A-scan sample in tau and drifts ~1e-4 from the float64 reference.

Live-config keys follow: the center_depth/half_width/grid_n window knobs
are replaced by ground/voxel/y-range/margin/sigma/power/tx_per_segment.

Parity against the notebook npz exports (frames 0034-0036 of the
20260922_mine_fast_true dataset): max|diff| <= 1.6e-6 off the depth-mask
knife edge, identical argmax, level rel diff <= 7e-7. Verified via
make sar3d_parity --dump-dir + scripts/sar3d_volume_compare.py; the
executable spec lives in scripts/sar3d_reference_check.py (bit-tight
against the npz, including the animation shifted-grid arithmetic).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
awe
2026-09-25 16:42:01 +03:00
co-authored by Claude Fable 5
parent e9a0b43ed3
commit b8cb572bc4
17 changed files with 1264 additions and 353 deletions
@@ -1,4 +1,6 @@
// Препроцессинг S21 → комплексные A-сканы (host, float64). Порт preprocess.py.
// Препроцессинг S21 → комплексные A-сканы (host, float64).
// Порт ячейки 0 Multi_antenna_3D_clean_v1.ipynb (segmented_interleaved тайминг,
// frequency-domain коррекция движения, k0-IFFT, нормировка пар).
#pragma once
#include <map>
#include <string>
@@ -12,15 +14,34 @@ namespace gpr {
using PairKey = std::pair<int, int>;
// Тайминги пары (нужны slot/n_slots/t_center для фазовой коррекции).
// Тайминг пары в модели segmented_interleaved: пара живёт в сегменте своего Tx
// (окно [segment_start, segment_start+segment_time] от начала акквизиции),
// внутри сегмента передатчики чередуются по частоте (slot из n_slots).
struct PairTiming {
int tx_interleave_slot = 0;
int tx_interleave_slots = 1;
double t_center_s = 0.0;
double segment_start_s = 0.0;
double segment_time_s = 0.0;
double t_center_s = 0.0; // центр окна сегмента
// Грубый межсегментный сдвиг: dtau центра сегмента относительно центра
// акквизиции кадра (2*dz/v, dz = dir*speed*dt*cos(look)).
double dtau_pair_s = 0.0;
};
using PairTimestamps = std::map<PairKey, PairTiming>;
PairTimestamps compute_pair_timestamps(const MotionConfig&, const GeometryConfig&);
// Окна захвата по Tx (output_pos → [start,end] сек от начала акквизиции).
using TxWindows = std::map<int, std::pair<double, double>>;
// Fallback без per-trace меток: сегменты по tx_per_segment штук делят кадр
// [0, acq_s] поровну (медианная модель ноутбука compute_pair_timestamps).
TxWindows uniform_tx_windows(const std::vector<int>& tx_order, double acq_s, int tx_per_segment);
// Порт compute_pair_timestamps_for_snapshot: Tx с одинаковым окном — один
// сегмент (сегменты по возрастанию окна), слот — порядок в tx_order (полный
// аппаратный порядок из run.combos; выбранные каналы его не меняют).
PairTimestamps compute_pair_timestamps(const TxWindows& tx_windows_s, double acq_s,
const std::vector<int>& tx_order,
const GeometryConfig&, const MotionConfig&);
// A-сканы одного снапшота, общая ось времени у всех пар (assert).
struct AScanSet {
@@ -32,7 +53,7 @@ struct AScanSet {
const c128* pair_ptr(int p) const { return H.data() + (size_t)p * n_fft; }
};
// Порт compute_pair_ascans: bg-subtract → phase correction → k0-IFFT → норма.
// Порт sar_pair_ascans: bg-subtract → phase correction → k0-IFFT → норма.
// s21/freq/background — по ключам (i_tx,i_rx); background может быть nullptr.
AScanSet preprocess_snapshot(
const std::map<PairKey, std::vector<c128>>& s21,
@@ -42,10 +63,12 @@ AScanSet preprocess_snapshot(
const PairTimestamps&, bool normalize = true);
// Отдельные шаги (для тестов/переиспользования):
// Полная фазовая модель движения до IFFT: dtau_intra(f) + dtau_pair одной
// фазой exp(sign*j*2*pi*f*dtau_total) (порт apply_intra_sweep_phase_correction).
std::vector<c128> apply_intra_sweep_phase_correction(
const std::vector<c128>& s21, const std::vector<double>& freq_full,
const PairTiming&, const MotionConfig&, double wave_speed,
double f_start, double f_stop, int n_slots_default);
double f_start, double f_stop);
// Возвращает t (ось), h (комплексный A-скан). n_fft через out-параметр.
void compute_ascan_bp(const std::vector<c128>& s21, const std::vector<double>& freq,
@@ -1,4 +1,5 @@
// Конфигурация счёт-ядра. Дефолты 1:1 повторяют gpr_core/config.py.
// Конфигурация счёт-ядра. Дефолты 1:1 повторяют Multi_antenna_3D_clean_v1.ipynb
// (ячейки 0 и 4) — эталонный ноутбук 3D SAR.
#pragma once
#include <algorithm>
#include <array>
@@ -11,13 +12,13 @@ namespace gpr {
struct GeometryConfig {
std::vector<int> input_idx{0, 1, 2, 3};
std::vector<int> output_idx{0, 1};
// (x,y,z) метры; порядок по физическим индексам как в config.py
std::vector<std::array<double, 3>> tx_positions{{0.785, 0.0, 0.0}, {-0.785, 0.0, 0.0}};
std::vector<int> output_idx{0, 1, 2, 3};
// (x,y,z) метры; порядок соответствует input_idx/output_idx
std::vector<std::array<double, 3>> tx_positions{
{-0.88, 0.0, 0.0}, {0.89, 0.0, 0.0}, {-0.33, 0.445, 0.0}, {0.33, 0.445, 0.0}};
std::vector<std::array<double, 3>> rx_positions{
{-0.42, 0.0, 0.0}, {0.48, 0.0, 0.0}, {0.18, 0.0, 0.0}, {-0.12, 0.0, 0.0}};
{0.23, 0.0, 0.0}, {0.54, 0.15, 0.0}, {-0.55, 0.3, 0.0}, {-0.23, 0.0, 0.0}};
double eps_r = 1.0;
double bp_plane_y = 0.0;
// --- производные (заполняются finalize) ---
std::vector<double> x_tx, y_tx, z_tx; // отсортированы по phys-индексу
@@ -50,53 +51,61 @@ struct GeometryConfig {
};
struct BPConfig {
double f_start = 20 * 1e8; // 2 ГГц
double f_start = 2.0e9;
double f_stop = 6.0e9;
double min_depth = 3.2;
double max_depth = 10.0;
double grid_x_pad = 2.0;
double z_min = 0.1;
int nx = 300;
int nz = 300;
double min_depth = 2.5;
double max_depth = 8.0;
int oversample = 8;
bool window = true;
bool window = true; // окно Ханна на полосе
bool pair_normalize = true;
double pair_norm_percentile = 50.0;
double pair_norm_depth_min = 3.2;
double pair_norm_depth_max = 10.0;
double pair_norm_depth_min = 2.5;
double pair_norm_depth_max = 8.0;
double pair_norm_eps = 1e-15;
double comp_range_power = 0.25;
// Компенсация геометрического расхождения 1/(Rtx*Rrx), нормированная на
// точку под виртуальным центром пары на глубине comp_ref_depth НА ПЛОСКОСТИ
// ЗЕМЛИ (высота (-ground_depth + d*sin p)/cos p).
double comp_range_power = 0.3;
double comp_weight_max = 5.0;
double comp_ref_depth = 5.0;
double smooth_sigma = 1.5;
bool bg_subtract = true;
double cf_eps = 1e-12;
};
struct MotionConfig {
double speed_m_s = 0.854;
double look_angle_deg = 6.0;
double pitch_deg = 6.0;
double tx_sweep_time_s = 0.079;
double tx_switch_time_s = 1e-5;
// frequency_timing_mode всегда interleaved_tx_by_frequency
double speed_m_s = 1.45;
double look_angle_deg = 17.5;
// Наклон рамы для ground-координат и смещений кадров; в ноутбуке
// BP_GROUND_PITCH_DEG=None означает «равен look_angle».
double pitch_deg = 17.5;
double direction_sign = +1.0;
// Внутрисвиповая (по частоте) фазовая коррекция движения.
bool apply_freq_phase_correction = true;
// Грубая межсегментная коррекция (dtau сегмента относительно центра кадра),
// вносится той же фазовой моделью до IFFT (режим 'frequency_domain').
bool apply_pair_motion_correction = true;
double motion_phase_sign = -1.0;
double frame_time_s(int n_tx) const {
return n_tx * tx_sweep_time_s + std::max(0, n_tx - 1) * tx_switch_time_s;
}
// Fallback-разбиение кадра на сегменты, когда per-trace меток захвата нет.
int tx_per_segment = 2;
};
struct SARConfig {
int frame_radius = 6;
double edge_frame_weight = 0.75;
std::array<double, 3> half_width_m{1.8, 1.0, 1.8};
std::array<int, 3> grid{150, 120, 160}; // NX, NY, NZ
bool coord_mode_ground = true; // 'ground' vs 'radar_local'
int frame_radius = 0; // 0 = один снимок (физическая вертикальная база)
double edge_frame_weight = 0.8; // вес крайнего кадра апертуры
bool coord_mode_ground = true; // 'ground' vs 'radar_local'
bool use_compensation = true;
bool normalize_each_frame = true;
// Высота НИЖНЕГО ряда антенн над землёй; Y=0 сцены — поверхность земли.
double ground_depth_m = 1.25;
// Полнообластная сетка: X — размах антенн ±x_margin_m, Y — высота над
// землёй, Z — полоса, покрывающая радарные глубины [min,max]_depth на всех
// высотах Y (см. build_full_region_grid).
double x_margin_m = 2.0;
double y_min_m = -0.5;
double y_max_m = 2.0;
std::array<double, 3> voxel_m{0.025, 0.02, 0.02}; // шаг (X, Y, Z)
// Финализация объёма: нормировка без верхних/нижних norm_edge_margin_m по Y
// (там ложный гребень постоянной дальности), сглаживание по (Z,X) в каждом
// Y-плане.
double norm_edge_margin_m = 0.3;
double smooth_sigma = 1.5;
};
} // namespace gpr
@@ -18,10 +18,18 @@ std::vector<FrameOffset> frame_offsets(const std::vector<double>& frame_times, d
// Косинусные веса апертуры (порт sar_frame_weights); dt берётся относительно центра.
std::vector<double> frame_weights(const std::vector<FrameOffset>& offsets, double edge_weight);
// Сетка вокруг точки сцены (порт sar_build_grid_scene).
Grid build_grid_scene(const std::array<double, 3>& center_scene, const SARConfig&);
// Высота плоскости земли (радарные оси) на радарной глубине depth:
// y = (-ground_depth + depth*sin(pitch)) / cos(pitch) (порт bp_plane_y).
double ground_plane_y(double depth, double ground_depth_m, double pitch_rad);
// point_to_scene: оси радара → сцена (поворот на pitch, если ground).
// Полнообластная сетка сцены (порт sar_build_full_region_grid):
// X — размах антенн ±x_margin, Y — [y_min, y_max] над землёй, Z — полоса,
// покрывающая радарные глубины [min_depth, max_depth] на всех высотах Y.
Grid build_full_region_grid(const GeometryConfig&, const BPConfig&, const SARConfig&,
double pitch_deg);
// point_to_scene: оси радара → сцена (поворот на pitch + подъём на ground_depth,
// если ground; Y=0 сцены — земля).
std::array<double, 3> point_to_scene(double x, double y, double z, const MotionConfig&,
const SARConfig&);
@@ -19,13 +19,24 @@ struct ApertureFrame {
double weight;
};
// Сырой результат объёма (Python делает finish: abs+gate+gaussian+normalize).
// Сырой результат объёма (до finish_volume).
struct VolumeRaw {
std::vector<c64> acc; // [NY*NZ*NX]
std::vector<float> cnt; // [NY*NZ*NX]
int NX = 0, NY = 0, NZ = 0;
};
// Финальный объём (порт sar_finish_volume): |acc/cnt| → маска глубины →
// нормировка по Y-планам без краёв → гаусс по (Z,X) → нормировка → clip.
struct FinishedVolume {
std::vector<float> volume; // [NY*NZ*NX], нормирован на ~[0,1]
double level = 0.0; // масштаб, на который поделён объём
int NX = 0, NY = 0, NZ = 0;
};
FinishedVolume finish_volume(const VolumeRaw&, const Grid&, double pitch_deg,
const BPConfig&, const SARConfig&);
class SarEngine {
public:
SarEngine(const GeometryConfig&, const BPConfig&, const MotionConfig&, const SARConfig&);