sar3d: python plumbing for the new parameters and real replay timing

- replay_raw_producer carries the RECORDED per-trace capture windows
  from each snapshot's meta.json (re-anchored by the same shift as the
  frame time) instead of one synthetic window for all traces — without
  this the segmented timing model degenerates in replay;
- GUI/profile/orchestration swap the removed window knobs
  (center_depth, half_width_*, grid_n*) for the notebook parameters
  (ground depth, pitch override, voxel pitch, Y range, X margin, norm
  edge margin, smooth sigma, comp power, tx per segment).

Headless replay smoke on the 20260922_mine_fast_true dataset passes:
51 sar3d result collections, transparent preprocessor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
awe
2026-09-25 16:42:14 +03:00
co-authored by Claude Fable 5
parent b8cb572bc4
commit e1dfbec712
9 changed files with 345 additions and 149 deletions
@@ -90,14 +90,19 @@ _WEB_LIVE_SCHEMA = [
("gpr_speed_m_s", "Motion", _GPR_LIKE_MODES, _dual("_gpr_speed_m_s", {"legacy_gpr": "_legacy_gpr_speed_m_s", "sar3d": "_sar3d_speed_m_s"})),
("sar3d_frame_radius", "3D SAR", ("sar3d",), _attr("_sar3d_frame_radius")),
("sar3d_edge_frame_weight", "3D SAR", ("sar3d",), _attr("_sar3d_edge_frame_weight")),
("sar3d_pitch_override_enabled", "3D SAR", ("sar3d",), _attr("_sar3d_pitch_override_enabled")),
("sar3d_pitch_deg", "3D SAR", ("sar3d",), _attr("_sar3d_pitch_deg")),
("sar3d_center_depth_m", "3D SAR", ("sar3d",), _attr("_sar3d_center_depth_m")),
("sar3d_half_width_x_m", "3D SAR", ("sar3d",), _attr("_sar3d_half_width_x_m")),
("sar3d_half_width_y_m", "3D SAR", ("sar3d",), _attr("_sar3d_half_width_y_m")),
("sar3d_half_width_z_m", "3D SAR", ("sar3d",), _attr("_sar3d_half_width_z_m")),
("sar3d_grid_nx", "3D SAR", ("sar3d",), _attr("_sar3d_grid_nx")),
("sar3d_grid_ny", "3D SAR", ("sar3d",), _attr("_sar3d_grid_ny")),
("sar3d_grid_nz", "3D SAR", ("sar3d",), _attr("_sar3d_grid_nz")),
("sar3d_ground_depth_m", "3D SAR", ("sar3d",), _attr("_sar3d_ground_depth_m")),
("sar3d_x_margin_m", "3D SAR", ("sar3d",), _attr("_sar3d_x_margin_m")),
("sar3d_y_min_m", "3D SAR", ("sar3d",), _attr("_sar3d_y_min_m")),
("sar3d_y_max_m", "3D SAR", ("sar3d",), _attr("_sar3d_y_max_m")),
("sar3d_voxel_x_m", "3D SAR", ("sar3d",), _attr("_sar3d_voxel_x_m")),
("sar3d_voxel_y_m", "3D SAR", ("sar3d",), _attr("_sar3d_voxel_y_m")),
("sar3d_voxel_z_m", "3D SAR", ("sar3d",), _attr("_sar3d_voxel_z_m")),
("sar3d_norm_edge_margin_m", "3D SAR", ("sar3d",), _attr("_sar3d_norm_edge_margin_m")),
("sar3d_smooth_sigma", "3D SAR", ("sar3d",), _attr("_sar3d_smooth_sigma")),
("sar3d_comp_range_power", "3D SAR", ("sar3d",), _attr("_sar3d_comp_range_power")),
("sar3d_tx_per_segment", "3D SAR", ("sar3d",), _attr("_sar3d_tx_per_segment")),
("sar3d_use_compensation", "3D SAR", ("sar3d",), _attr("_sar3d_use_compensation")),
("sar3d_normalize_pairs", "3D SAR", ("sar3d",), _attr("_sar3d_normalize_pairs")),
("sar3d_bottom_view_deg", "3D SAR", ("sar3d",), _attr("_sar3d_bottom_view")),
@@ -553,9 +558,8 @@ class AppWindowLiveProcessingMixin:
f"depth={self._sar3d_min_depth_m.value():g}..{self._sar3d_max_depth_m.value():g} m, "
f"freq={self._sar3d_start_freq_mhz.value():g}..{self._sar3d_stop_freq_mhz.value():g} MHz, "
f"frame_radius={self._sar3d_frame_radius.value()}, "
f"grid={self._sar3d_grid_nx.value()}x{self._sar3d_grid_ny.value()}x{self._sar3d_grid_nz.value()}, "
f"center_depth={self._sar3d_center_depth_m.value():g} m, "
f"pitch={self._sar3d_pitch_deg.value():g} deg)"
f"voxel={self._sar3d_voxel_x_m.value():g}x{self._sar3d_voxel_y_m.value():g}x{self._sar3d_voxel_z_m.value():g} m, "
f"ground_depth={self._sar3d_ground_depth_m.value():g} m)"
)
def _clear_history_mode_caches(self) -> None:
@@ -350,14 +350,19 @@ class AppWindowConfigProfileIOMixin:
self._sar3d_apply_freq_phase_correction,
self._sar3d_frame_radius,
self._sar3d_edge_frame_weight,
self._sar3d_pitch_override_enabled,
self._sar3d_pitch_deg,
self._sar3d_center_depth_m,
self._sar3d_half_width_x_m,
self._sar3d_half_width_y_m,
self._sar3d_half_width_z_m,
self._sar3d_grid_nx,
self._sar3d_grid_ny,
self._sar3d_grid_nz,
self._sar3d_ground_depth_m,
self._sar3d_x_margin_m,
self._sar3d_y_min_m,
self._sar3d_y_max_m,
self._sar3d_voxel_x_m,
self._sar3d_voxel_y_m,
self._sar3d_voxel_z_m,
self._sar3d_norm_edge_margin_m,
self._sar3d_smooth_sigma,
self._sar3d_comp_range_power,
self._sar3d_tx_per_segment,
self._sar3d_use_compensation,
self._sar3d_normalize_pairs,
self._save_count,
@@ -562,14 +567,19 @@ class AppWindowConfigProfileIOMixin:
self._sar3d_apply_freq_phase_correction.setChecked(bool(sar3d_state.apply_freq_phase_correction))
self._sar3d_frame_radius.setValue(int(sar3d_state.frame_radius))
self._sar3d_edge_frame_weight.setValue(float(sar3d_state.edge_frame_weight))
self._sar3d_pitch_override_enabled.setChecked(bool(sar3d_state.pitch_override_enabled))
self._sar3d_pitch_deg.setValue(float(sar3d_state.pitch_deg))
self._sar3d_center_depth_m.setValue(float(sar3d_state.center_depth_m))
self._sar3d_half_width_x_m.setValue(float(sar3d_state.half_width_x_m))
self._sar3d_half_width_y_m.setValue(float(sar3d_state.half_width_y_m))
self._sar3d_half_width_z_m.setValue(float(sar3d_state.half_width_z_m))
self._sar3d_grid_nx.setValue(int(sar3d_state.grid_nx))
self._sar3d_grid_ny.setValue(int(sar3d_state.grid_ny))
self._sar3d_grid_nz.setValue(int(sar3d_state.grid_nz))
self._sar3d_ground_depth_m.setValue(float(sar3d_state.ground_depth_m))
self._sar3d_x_margin_m.setValue(float(sar3d_state.x_margin_m))
self._sar3d_y_min_m.setValue(float(sar3d_state.y_min_m))
self._sar3d_y_max_m.setValue(float(sar3d_state.y_max_m))
self._sar3d_voxel_x_m.setValue(float(sar3d_state.voxel_x_m))
self._sar3d_voxel_y_m.setValue(float(sar3d_state.voxel_y_m))
self._sar3d_voxel_z_m.setValue(float(sar3d_state.voxel_z_m))
self._sar3d_norm_edge_margin_m.setValue(float(sar3d_state.norm_edge_margin_m))
self._sar3d_smooth_sigma.setValue(float(sar3d_state.smooth_sigma))
self._sar3d_comp_range_power.setValue(float(sar3d_state.comp_range_power))
self._sar3d_tx_per_segment.setValue(int(sar3d_state.tx_per_segment))
self._sar3d_use_compensation.setChecked(bool(sar3d_state.use_compensation))
self._sar3d_normalize_pairs.setChecked(bool(sar3d_state.normalize_pairs))
self._set_combo_current_text(
@@ -438,14 +438,19 @@ class AppWindowConfigStateBuildersMixin:
apply_freq_phase_correction=bool(self._sar3d_apply_freq_phase_correction.isChecked()),
frame_radius=int(self._sar3d_frame_radius.value()),
edge_frame_weight=float(self._sar3d_edge_frame_weight.value()),
pitch_override_enabled=bool(self._sar3d_pitch_override_enabled.isChecked()),
pitch_deg=float(self._sar3d_pitch_deg.value()),
center_depth_m=float(self._sar3d_center_depth_m.value()),
half_width_x_m=float(self._sar3d_half_width_x_m.value()),
half_width_y_m=float(self._sar3d_half_width_y_m.value()),
half_width_z_m=float(self._sar3d_half_width_z_m.value()),
grid_nx=int(self._sar3d_grid_nx.value()),
grid_ny=int(self._sar3d_grid_ny.value()),
grid_nz=int(self._sar3d_grid_nz.value()),
ground_depth_m=float(self._sar3d_ground_depth_m.value()),
x_margin_m=float(self._sar3d_x_margin_m.value()),
y_min_m=float(self._sar3d_y_min_m.value()),
y_max_m=float(self._sar3d_y_max_m.value()),
voxel_x_m=float(self._sar3d_voxel_x_m.value()),
voxel_y_m=float(self._sar3d_voxel_y_m.value()),
voxel_z_m=float(self._sar3d_voxel_z_m.value()),
norm_edge_margin_m=float(self._sar3d_norm_edge_margin_m.value()),
smooth_sigma=float(self._sar3d_smooth_sigma.value()),
comp_range_power=float(self._sar3d_comp_range_power.value()),
tx_per_segment=int(self._sar3d_tx_per_segment.value()),
use_compensation=bool(self._sar3d_use_compensation.isChecked()),
normalize_pairs=bool(self._sar3d_normalize_pairs.isChecked()),
bottom_view_deg=sar3d_bottom_view_deg_from_label(
@@ -660,49 +660,97 @@ def build_processing_group(owner) -> QGroupBox:
"Weight of the outermost aperture frames (1 = flat window, 0 = strong taper)."
)
owner._sar3d_pitch_override_enabled = QCheckBox("Override pitch")
owner._sar3d_pitch_override_enabled.setChecked(bool(sar3d_defaults.pitch_override_enabled))
owner._sar3d_pitch_override_enabled.setToolTip(
"When unchecked, the frame pitch follows the look angle (notebook "
"BP_GROUND_PITCH_DEG = None); check to set it separately below."
)
owner._sar3d_pitch_deg = QDoubleSpinBox()
owner._sar3d_pitch_deg.setDecimals(2)
owner._sar3d_pitch_deg.setRange(-90.0, 90.0)
owner._sar3d_pitch_deg.setSingleStep(0.5)
owner._sar3d_pitch_deg.setValue(float(sar3d_defaults.pitch_deg))
owner._sar3d_pitch_deg.setToolTip("Antenna pitch above the horizon (deg); tilts the imaged volume.")
owner._sar3d_pitch_deg.setToolTip(
"Frame pitch (deg) used for ground coordinates and aperture offsets when the override is on."
)
owner._sar3d_center_depth_m = QDoubleSpinBox()
owner._sar3d_center_depth_m.setDecimals(2)
owner._sar3d_center_depth_m.setRange(0.0, 50.0)
owner._sar3d_center_depth_m.setSingleStep(0.1)
owner._sar3d_center_depth_m.setValue(float(sar3d_defaults.center_depth_m))
owner._sar3d_center_depth_m.setToolTip("Range distance to the center of the imaged volume (m).")
owner._sar3d_ground_depth_m = QDoubleSpinBox()
owner._sar3d_ground_depth_m.setDecimals(2)
owner._sar3d_ground_depth_m.setRange(0.0, 10.0)
owner._sar3d_ground_depth_m.setSingleStep(0.05)
owner._sar3d_ground_depth_m.setValue(float(sar3d_defaults.ground_depth_m))
owner._sar3d_ground_depth_m.setToolTip(
"Height of the lower antenna row above the ground (m); scene Y=0 is the ground."
)
owner._sar3d_half_width_x_m = QDoubleSpinBox()
owner._sar3d_half_width_x_m.setDecimals(2)
owner._sar3d_half_width_x_m.setRange(0.01, 50.0)
owner._sar3d_half_width_x_m.setSingleStep(0.1)
owner._sar3d_half_width_x_m.setValue(float(sar3d_defaults.half_width_x_m))
owner._sar3d_x_margin_m = QDoubleSpinBox()
owner._sar3d_x_margin_m.setDecimals(2)
owner._sar3d_x_margin_m.setRange(0.0, 20.0)
owner._sar3d_x_margin_m.setSingleStep(0.1)
owner._sar3d_x_margin_m.setValue(float(sar3d_defaults.x_margin_m))
owner._sar3d_x_margin_m.setToolTip("Grid X margin beyond the antenna span (m).")
owner._sar3d_half_width_y_m = QDoubleSpinBox()
owner._sar3d_half_width_y_m.setDecimals(2)
owner._sar3d_half_width_y_m.setRange(0.01, 50.0)
owner._sar3d_half_width_y_m.setSingleStep(0.1)
owner._sar3d_half_width_y_m.setValue(float(sar3d_defaults.half_width_y_m))
owner._sar3d_y_min_m = QDoubleSpinBox()
owner._sar3d_y_min_m.setDecimals(2)
owner._sar3d_y_min_m.setRange(-10.0, 10.0)
owner._sar3d_y_min_m.setSingleStep(0.1)
owner._sar3d_y_min_m.setValue(float(sar3d_defaults.y_min_m))
owner._sar3d_half_width_z_m = QDoubleSpinBox()
owner._sar3d_half_width_z_m.setDecimals(2)
owner._sar3d_half_width_z_m.setRange(0.01, 50.0)
owner._sar3d_half_width_z_m.setSingleStep(0.1)
owner._sar3d_half_width_z_m.setValue(float(sar3d_defaults.half_width_z_m))
owner._sar3d_y_max_m = QDoubleSpinBox()
owner._sar3d_y_max_m.setDecimals(2)
owner._sar3d_y_max_m.setRange(-10.0, 10.0)
owner._sar3d_y_max_m.setSingleStep(0.1)
owner._sar3d_y_max_m.setValue(float(sar3d_defaults.y_max_m))
owner._sar3d_grid_nx = QSpinBox()
owner._sar3d_grid_nx.setRange(2, 1024)
owner._sar3d_grid_nx.setValue(int(sar3d_defaults.grid_nx))
owner._sar3d_voxel_x_m = QDoubleSpinBox()
owner._sar3d_voxel_x_m.setDecimals(3)
owner._sar3d_voxel_x_m.setRange(0.005, 1.0)
owner._sar3d_voxel_x_m.setSingleStep(0.005)
owner._sar3d_voxel_x_m.setValue(float(sar3d_defaults.voxel_x_m))
owner._sar3d_grid_ny = QSpinBox()
owner._sar3d_grid_ny.setRange(2, 1024)
owner._sar3d_grid_ny.setValue(int(sar3d_defaults.grid_ny))
owner._sar3d_voxel_y_m = QDoubleSpinBox()
owner._sar3d_voxel_y_m.setDecimals(3)
owner._sar3d_voxel_y_m.setRange(0.005, 1.0)
owner._sar3d_voxel_y_m.setSingleStep(0.005)
owner._sar3d_voxel_y_m.setValue(float(sar3d_defaults.voxel_y_m))
owner._sar3d_grid_nz = QSpinBox()
owner._sar3d_grid_nz.setRange(2, 1024)
owner._sar3d_grid_nz.setValue(int(sar3d_defaults.grid_nz))
owner._sar3d_voxel_z_m = QDoubleSpinBox()
owner._sar3d_voxel_z_m.setDecimals(3)
owner._sar3d_voxel_z_m.setRange(0.005, 1.0)
owner._sar3d_voxel_z_m.setSingleStep(0.005)
owner._sar3d_voxel_z_m.setValue(float(sar3d_defaults.voxel_z_m))
owner._sar3d_norm_edge_margin_m = QDoubleSpinBox()
owner._sar3d_norm_edge_margin_m.setDecimals(2)
owner._sar3d_norm_edge_margin_m.setRange(0.0, 5.0)
owner._sar3d_norm_edge_margin_m.setSingleStep(0.05)
owner._sar3d_norm_edge_margin_m.setValue(float(sar3d_defaults.norm_edge_margin_m))
owner._sar3d_norm_edge_margin_m.setToolTip(
"Grid Y edges excluded from volume normalization (constant-range ridge)."
)
owner._sar3d_smooth_sigma = QDoubleSpinBox()
owner._sar3d_smooth_sigma.setDecimals(2)
owner._sar3d_smooth_sigma.setRange(0.0, 10.0)
owner._sar3d_smooth_sigma.setSingleStep(0.25)
owner._sar3d_smooth_sigma.setValue(float(sar3d_defaults.smooth_sigma))
owner._sar3d_smooth_sigma.setToolTip("Gaussian smoothing sigma over (Z, X) planes, voxels.")
owner._sar3d_comp_range_power = QDoubleSpinBox()
owner._sar3d_comp_range_power.setDecimals(2)
owner._sar3d_comp_range_power.setRange(0.0, 2.0)
owner._sar3d_comp_range_power.setSingleStep(0.05)
owner._sar3d_comp_range_power.setValue(float(sar3d_defaults.comp_range_power))
owner._sar3d_comp_range_power.setToolTip("Geometric spreading compensation power.")
owner._sar3d_tx_per_segment = QSpinBox()
owner._sar3d_tx_per_segment.setRange(1, 64)
owner._sar3d_tx_per_segment.setValue(int(sar3d_defaults.tx_per_segment))
owner._sar3d_tx_per_segment.setToolTip(
"Fallback Tx-per-segment split of a frame when per-trace capture stamps are absent."
)
owner._sar3d_use_compensation = QCheckBox("Use per-voxel compensation")
owner._sar3d_use_compensation.setChecked(bool(sar3d_defaults.use_compensation))
@@ -738,19 +786,24 @@ def build_processing_group(owner) -> QGroupBox:
owner._sar3d_apply_freq_phase_correction,
("Frame radius", owner._sar3d_frame_radius),
("Edge frame weight", owner._sar3d_edge_frame_weight),
owner._sar3d_pitch_override_enabled,
("Pitch deg", owner._sar3d_pitch_deg),
("Center depth m", owner._sar3d_center_depth_m),
("Half width X m", owner._sar3d_half_width_x_m),
("Half width Y m", owner._sar3d_half_width_y_m),
("Half width Z m", owner._sar3d_half_width_z_m),
("Grid Nx", owner._sar3d_grid_nx),
("Grid Ny", owner._sar3d_grid_ny),
("Grid Nz", owner._sar3d_grid_nz),
("Ground depth m", owner._sar3d_ground_depth_m),
("X margin m", owner._sar3d_x_margin_m),
("Y min m", owner._sar3d_y_min_m),
("Y max m", owner._sar3d_y_max_m),
("Voxel X m", owner._sar3d_voxel_x_m),
("Voxel Y m", owner._sar3d_voxel_y_m),
("Voxel Z m", owner._sar3d_voxel_z_m),
("Norm edge margin m", owner._sar3d_norm_edge_margin_m),
("Smooth sigma", owner._sar3d_smooth_sigma),
("Comp range power", owner._sar3d_comp_range_power),
("Tx per segment", owner._sar3d_tx_per_segment),
("Bottom view", owner._sar3d_bottom_view),
owner._sar3d_use_compensation,
owner._sar3d_normalize_pairs,
],
split_index=11,
split_index=13,
)
owner._processing_mode_pages.addWidget(sar3d_page)
@@ -839,14 +892,19 @@ def build_processing_group(owner) -> QGroupBox:
owner._sar3d_apply_freq_phase_correction.toggled.connect(owner._on_processing_live_settings_changed)
owner._sar3d_frame_radius.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_edge_frame_weight.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_pitch_override_enabled.toggled.connect(owner._on_processing_live_settings_changed)
owner._sar3d_pitch_deg.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_center_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_half_width_x_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_half_width_y_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_half_width_z_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_grid_nx.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_grid_ny.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_grid_nz.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_ground_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_x_margin_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_y_min_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_y_max_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_voxel_x_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_voxel_y_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_voxel_z_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_norm_edge_margin_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_smooth_sigma.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_comp_range_power.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_tx_per_segment.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._sar3d_use_compensation.toggled.connect(owner._on_processing_live_settings_changed)
owner._sar3d_normalize_pairs.toggled.connect(owner._on_processing_live_settings_changed)
owner._sar3d_bottom_view.currentTextChanged.connect(owner._on_processing_live_settings_changed)
+80 -24
View File
@@ -533,39 +533,84 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
gui.processing.sar3d.edge_frame_weight,
"gui.processing.sar3d",
),
pitch_override_enabled=_optional_bool(
sar3d_object,
"pitch_override_enabled",
gui.processing.sar3d.pitch_override_enabled,
"gui.processing.sar3d",
),
pitch_deg=_optional_float(
sar3d_object,
"pitch_deg",
gui.processing.sar3d.pitch_deg,
"gui.processing.sar3d",
),
center_depth_m=_optional_float(
ground_depth_m=_optional_float(
sar3d_object,
"center_depth_m",
gui.processing.sar3d.center_depth_m,
"ground_depth_m",
gui.processing.sar3d.ground_depth_m,
"gui.processing.sar3d",
),
half_width_x_m=_optional_float(
x_margin_m=_optional_float(
sar3d_object,
"half_width_x_m",
gui.processing.sar3d.half_width_x_m,
"x_margin_m",
gui.processing.sar3d.x_margin_m,
"gui.processing.sar3d",
),
half_width_y_m=_optional_float(
y_min_m=_optional_float(
sar3d_object,
"half_width_y_m",
gui.processing.sar3d.half_width_y_m,
"y_min_m",
gui.processing.sar3d.y_min_m,
"gui.processing.sar3d",
),
half_width_z_m=_optional_float(
y_max_m=_optional_float(
sar3d_object,
"half_width_z_m",
gui.processing.sar3d.half_width_z_m,
"y_max_m",
gui.processing.sar3d.y_max_m,
"gui.processing.sar3d",
),
voxel_x_m=_optional_float(
sar3d_object,
"voxel_x_m",
gui.processing.sar3d.voxel_x_m,
"gui.processing.sar3d",
),
voxel_y_m=_optional_float(
sar3d_object,
"voxel_y_m",
gui.processing.sar3d.voxel_y_m,
"gui.processing.sar3d",
),
voxel_z_m=_optional_float(
sar3d_object,
"voxel_z_m",
gui.processing.sar3d.voxel_z_m,
"gui.processing.sar3d",
),
norm_edge_margin_m=_optional_float(
sar3d_object,
"norm_edge_margin_m",
gui.processing.sar3d.norm_edge_margin_m,
"gui.processing.sar3d",
),
smooth_sigma=_optional_float(
sar3d_object,
"smooth_sigma",
gui.processing.sar3d.smooth_sigma,
"gui.processing.sar3d",
),
comp_range_power=_optional_float(
sar3d_object,
"comp_range_power",
gui.processing.sar3d.comp_range_power,
"gui.processing.sar3d",
),
tx_per_segment=_optional_int(
sar3d_object,
"tx_per_segment",
gui.processing.sar3d.tx_per_segment,
"gui.processing.sar3d",
),
grid_nx=_optional_int(sar3d_object, "grid_nx", gui.processing.sar3d.grid_nx, "gui.processing.sar3d"),
grid_ny=_optional_int(sar3d_object, "grid_ny", gui.processing.sar3d.grid_ny, "gui.processing.sar3d"),
grid_nz=_optional_int(sar3d_object, "grid_nz", gui.processing.sar3d.grid_nz, "gui.processing.sar3d"),
use_compensation=_optional_bool(
sar3d_object,
"use_compensation",
@@ -628,8 +673,14 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
raise ValueError("gui.processing.sar3d.frame_radius must be >= 0")
if not 0.0 <= gui.processing.sar3d.edge_frame_weight <= 1.0:
raise ValueError("gui.processing.sar3d.edge_frame_weight must be within [0, 1]")
if min(gui.processing.sar3d.grid_nx, gui.processing.sar3d.grid_ny, gui.processing.sar3d.grid_nz) < 2:
raise ValueError("gui.processing.sar3d.grid_nx/ny/nz must each be >= 2")
if min(gui.processing.sar3d.voxel_x_m, gui.processing.sar3d.voxel_y_m, gui.processing.sar3d.voxel_z_m) <= 0.0:
raise ValueError("gui.processing.sar3d.voxel_x/y/z_m must each be > 0")
if gui.processing.sar3d.y_max_m <= gui.processing.sar3d.y_min_m:
raise ValueError("gui.processing.sar3d.y_max_m must be > y_min_m")
if gui.processing.sar3d.ground_depth_m < 0.0:
raise ValueError("gui.processing.sar3d.ground_depth_m must be >= 0")
if gui.processing.sar3d.tx_per_segment < 1:
raise ValueError("gui.processing.sar3d.tx_per_segment must be >= 1")
data_actions_object = _as_dict(gui_object.get("data_actions"), "gui.data_actions")
gui.data_actions = GuiDataActionsStateModel(
@@ -795,14 +846,19 @@ def gui_profile_to_dict(model: GuiProfileModel) -> dict[str, Any]:
"apply_freq_phase_correction": gui.processing.sar3d.apply_freq_phase_correction,
"frame_radius": gui.processing.sar3d.frame_radius,
"edge_frame_weight": gui.processing.sar3d.edge_frame_weight,
"pitch_override_enabled": gui.processing.sar3d.pitch_override_enabled,
"pitch_deg": gui.processing.sar3d.pitch_deg,
"center_depth_m": gui.processing.sar3d.center_depth_m,
"half_width_x_m": gui.processing.sar3d.half_width_x_m,
"half_width_y_m": gui.processing.sar3d.half_width_y_m,
"half_width_z_m": gui.processing.sar3d.half_width_z_m,
"grid_nx": gui.processing.sar3d.grid_nx,
"grid_ny": gui.processing.sar3d.grid_ny,
"grid_nz": gui.processing.sar3d.grid_nz,
"ground_depth_m": gui.processing.sar3d.ground_depth_m,
"x_margin_m": gui.processing.sar3d.x_margin_m,
"y_min_m": gui.processing.sar3d.y_min_m,
"y_max_m": gui.processing.sar3d.y_max_m,
"voxel_x_m": gui.processing.sar3d.voxel_x_m,
"voxel_y_m": gui.processing.sar3d.voxel_y_m,
"voxel_z_m": gui.processing.sar3d.voxel_z_m,
"norm_edge_margin_m": gui.processing.sar3d.norm_edge_margin_m,
"smooth_sigma": gui.processing.sar3d.smooth_sigma,
"comp_range_power": gui.processing.sar3d.comp_range_power,
"tx_per_segment": gui.processing.sar3d.tx_per_segment,
"use_compensation": gui.processing.sar3d.use_compensation,
"normalize_pairs": gui.processing.sar3d.normalize_pairs,
"bottom_view_deg": gui.processing.sar3d.bottom_view_deg,
+21 -16
View File
@@ -128,31 +128,36 @@ class GuiSar3dStateModel:
"""UI-only defaults for the 3D SAR (sar3d) live settings.
The band, depth gate, antenna selection and motion knobs mirror the shared
gpr_* live-config fields; the sar3d_* knobs mirror the VNA_AlGo gpr_core
defaults used by the C++ sar3d processor.
gpr_* live-config fields; the sar3d_* knobs mirror the reference notebook
(Multi_antenna_3D_clean_v1.ipynb) defaults used by the C++ sar3d processor.
"""
input_positions: str = ""
output_positions: str = ""
min_depth_m: float = 2.0
max_depth_m: float = 14.0
start_freq_mhz: float = 3000.0
min_depth_m: float = 2.5
max_depth_m: float = 8.0
start_freq_mhz: float = 2000.0
stop_freq_mhz: float = 6000.0
speed_m_s: float = 0.0
ignore_socket_speed_enabled: bool = False
look_angle_deg: float = 0.0
look_angle_deg: float = 17.5
direction_sign: float = 1.0
apply_freq_phase_correction: bool = True
frame_radius: int = 6
edge_frame_weight: float = 0.75
pitch_deg: float = 6.0
center_depth_m: float = 5.6
half_width_x_m: float = 1.8
half_width_y_m: float = 1.0
half_width_z_m: float = 1.8
grid_nx: int = 150
grid_ny: int = 120
grid_nz: int = 160
frame_radius: int = 0
edge_frame_weight: float = 0.8
pitch_override_enabled: bool = False
pitch_deg: float = 17.5
ground_depth_m: float = 1.25
x_margin_m: float = 2.0
y_min_m: float = -0.5
y_max_m: float = 2.0
voxel_x_m: float = 0.025
voxel_y_m: float = 0.02
voxel_z_m: float = 0.02
norm_edge_margin_m: float = 0.3
smooth_sigma: float = 1.5
comp_range_power: float = 0.3
tx_per_segment: int = 2
use_compensation: bool = True
normalize_pairs: bool = True
# Bottom-view projection angle around the vertical Y axis, degrees:
@@ -72,19 +72,31 @@ class ProcessingLiveConfig:
gpr_visible_x_max_m: float = 2.0
gpr_visible_z_min_m: float = 0.0
gpr_visible_z_max_m: float = 14.0
# 3D SAR (sar3d processor) parameters; defaults mirror VNA_AlGo gpr_core.
# The frequency band, depth gate, antenna selection and motion parameters
# are shared with the gpr_* fields above.
sar3d_frame_radius: int = 6
sar3d_edge_frame_weight: float = 0.75
sar3d_pitch_deg: float = 6.0
sar3d_center_depth_m: float = 5.6
sar3d_half_width_x_m: float = 1.8
sar3d_half_width_y_m: float = 1.0
sar3d_half_width_z_m: float = 1.8
sar3d_grid_nx: int = 150
sar3d_grid_ny: int = 120
sar3d_grid_nz: int = 160
# 3D SAR (sar3d processor) parameters; defaults mirror the reference
# notebook Multi_antenna_3D_clean_v1.ipynb. The frequency band, depth gate,
# antenna selection and motion parameters are shared with the gpr_* fields.
sar3d_frame_radius: int = 0
sar3d_edge_frame_weight: float = 0.8
# Frame pitch override; disabled -> the pitch follows gpr_look_angle_deg.
sar3d_pitch_override_enabled: bool = False
sar3d_pitch_deg: float = 17.5
# Height of the lower antenna row above the ground (scene Y=0 = ground).
sar3d_ground_depth_m: float = 1.25
# Full-region grid: X spans the antennas +- margin, Y is height above the
# ground, Z covers the radar depth gate over the Y range.
sar3d_x_margin_m: float = 2.0
sar3d_y_min_m: float = -0.5
sar3d_y_max_m: float = 2.0
sar3d_voxel_x_m: float = 0.025
sar3d_voxel_y_m: float = 0.02
sar3d_voxel_z_m: float = 0.02
# Volume finalization: Y-edge margin excluded from normalization, Gaussian
# smoothing sigma over (Z, X) planes.
sar3d_norm_edge_margin_m: float = 0.3
sar3d_smooth_sigma: float = 1.5
sar3d_comp_range_power: float = 0.3
# Fallback frame segmentation when per-trace capture stamps are absent.
sar3d_tx_per_segment: int = 2
sar3d_use_compensation: bool = True
sar3d_normalize_pairs: bool = True
# Bottom-view projection angle around the vertical Y axis, degrees:
@@ -156,14 +168,19 @@ class ProcessingLiveConfig:
"gpr_visible_z_max_m": float(self.gpr_visible_z_max_m),
"sar3d_frame_radius": int(self.sar3d_frame_radius),
"sar3d_edge_frame_weight": float(self.sar3d_edge_frame_weight),
"sar3d_pitch_override_enabled": bool(self.sar3d_pitch_override_enabled),
"sar3d_pitch_deg": float(self.sar3d_pitch_deg),
"sar3d_center_depth_m": float(self.sar3d_center_depth_m),
"sar3d_half_width_x_m": float(self.sar3d_half_width_x_m),
"sar3d_half_width_y_m": float(self.sar3d_half_width_y_m),
"sar3d_half_width_z_m": float(self.sar3d_half_width_z_m),
"sar3d_grid_nx": int(self.sar3d_grid_nx),
"sar3d_grid_ny": int(self.sar3d_grid_ny),
"sar3d_grid_nz": int(self.sar3d_grid_nz),
"sar3d_ground_depth_m": float(self.sar3d_ground_depth_m),
"sar3d_x_margin_m": float(self.sar3d_x_margin_m),
"sar3d_y_min_m": float(self.sar3d_y_min_m),
"sar3d_y_max_m": float(self.sar3d_y_max_m),
"sar3d_voxel_x_m": float(self.sar3d_voxel_x_m),
"sar3d_voxel_y_m": float(self.sar3d_voxel_y_m),
"sar3d_voxel_z_m": float(self.sar3d_voxel_z_m),
"sar3d_norm_edge_margin_m": float(self.sar3d_norm_edge_margin_m),
"sar3d_smooth_sigma": float(self.sar3d_smooth_sigma),
"sar3d_comp_range_power": float(self.sar3d_comp_range_power),
"sar3d_tx_per_segment": int(self.sar3d_tx_per_segment),
"sar3d_use_compensation": bool(self.sar3d_use_compensation),
"sar3d_normalize_pairs": bool(self.sar3d_normalize_pairs),
"sar3d_bottom_view_deg": float(self.sar3d_bottom_view_deg),
+35 -5
View File
@@ -20,6 +20,7 @@ never see time jump backwards. ``collection_id`` increases forever.
from __future__ import annotations
import argparse
import json
import logging
from pathlib import Path
import re
@@ -75,6 +76,22 @@ def _load_dataset(dataset_dir: Path, combos: list[ComboKey], expected_points: in
timestamps_ns: list[int] = []
for name, timestamp_ns in named:
folder = dataset_dir / name
# Recorded per-trace capture windows from the snapshot's meta.json: the
# sar3d segmented timing model reconstructs the acquisition segments
# from them. Missing/partial meta leaves the zero "unmeasured" sentinel
# and the consumer falls back to its uniform-segment model.
windows: dict[tuple[int, int], tuple[int, int]] = {}
meta_path = folder / "meta.json"
if meta_path.is_file():
try:
meta = json.loads(meta_path.read_text())
for entry in meta.get("traces", []):
windows[(int(entry["input"]), int(entry["output"]))] = (
int(entry["capture_start_ns"]),
int(entry["capture_end_ns"]),
)
except (OSError, ValueError, KeyError, TypeError):
windows = {}
traces: list[TraceData] = []
for combo in combos:
s21_path = folder / f"i{combo.input}_o{combo.output}_s21.npy"
@@ -94,12 +111,15 @@ def _load_dataset(dataset_dir: Path, combos: list[ComboKey], expected_points: in
f"{s21_path}: {s21.size} points, but radar.sweep.points = "
f"{expected_points}. Align the profile's sweep with the dataset."
)
window = windows.get((combo.input, combo.output), (0, 0))
traces.append(
TraceData(
combo=combo,
frequency_hz=freq,
s11=np.zeros_like(s21),
s21=s21,
capture_start_ns=window[0],
capture_end_ns=window[1],
)
)
snapshots.append(traces)
@@ -114,28 +134,38 @@ def _build_collection(
collection_id: int,
frame_ns: int,
) -> SweepCollection:
"""Wrap one preloaded snapshot in a SweepCollection at the given frame time."""
"""Wrap one preloaded snapshot in a SweepCollection at the given frame time.
Recorded per-trace capture windows are re-anchored by the same shift as the
frame time, preserving the real intra-frame segment layout. Traces without
recorded windows get a synthetic window spanning the median frame gap.
"""
span_ns = dataset.median_gap_ns
start_ns = frame_ns - span_ns // 2
end_ns = frame_ns + span_ns // 2
shift_ns = frame_ns - dataset.timestamps_ns[index]
traces = []
for source in dataset.snapshots[index]:
if source.capture_end_ns > source.capture_start_ns:
capture = (source.capture_start_ns + shift_ns, source.capture_end_ns + shift_ns)
else:
capture = (start_ns, end_ns)
traces.append(
TraceData(
combo=source.combo,
frequency_hz=source.frequency_hz,
s11=source.s11,
s21=source.s21,
capture_start_ns=start_ns,
capture_end_ns=end_ns,
capture_start_ns=capture[0],
capture_end_ns=capture[1],
)
)
return SweepCollection(
collection_id=collection_id,
monotonic_ns=frame_ns,
traces=traces,
capture_start_ns=start_ns,
capture_end_ns=end_ns,
capture_start_ns=min(t.capture_start_ns for t in traces),
capture_end_ns=max(t.capture_end_ns for t in traces),
)
+18 -7
View File
@@ -84,14 +84,25 @@ def _sar3d_live_config(profile_path: Path) -> ProcessingLiveConfig:
live.sar3d_edge_frame_weight = float(
sar3d.get("edge_frame_weight", live.sar3d_edge_frame_weight)
)
live.sar3d_pitch_override_enabled = bool(
sar3d.get("pitch_override_enabled", live.sar3d_pitch_override_enabled)
)
live.sar3d_pitch_deg = float(sar3d.get("pitch_deg", live.sar3d_pitch_deg))
live.sar3d_center_depth_m = float(sar3d.get("center_depth_m", live.sar3d_center_depth_m))
live.sar3d_half_width_x_m = float(sar3d.get("half_width_x_m", live.sar3d_half_width_x_m))
live.sar3d_half_width_y_m = float(sar3d.get("half_width_y_m", live.sar3d_half_width_y_m))
live.sar3d_half_width_z_m = float(sar3d.get("half_width_z_m", live.sar3d_half_width_z_m))
live.sar3d_grid_nx = int(sar3d.get("grid_nx", live.sar3d_grid_nx))
live.sar3d_grid_ny = int(sar3d.get("grid_ny", live.sar3d_grid_ny))
live.sar3d_grid_nz = int(sar3d.get("grid_nz", live.sar3d_grid_nz))
live.sar3d_ground_depth_m = float(sar3d.get("ground_depth_m", live.sar3d_ground_depth_m))
live.sar3d_x_margin_m = float(sar3d.get("x_margin_m", live.sar3d_x_margin_m))
live.sar3d_y_min_m = float(sar3d.get("y_min_m", live.sar3d_y_min_m))
live.sar3d_y_max_m = float(sar3d.get("y_max_m", live.sar3d_y_max_m))
live.sar3d_voxel_x_m = float(sar3d.get("voxel_x_m", live.sar3d_voxel_x_m))
live.sar3d_voxel_y_m = float(sar3d.get("voxel_y_m", live.sar3d_voxel_y_m))
live.sar3d_voxel_z_m = float(sar3d.get("voxel_z_m", live.sar3d_voxel_z_m))
live.sar3d_norm_edge_margin_m = float(
sar3d.get("norm_edge_margin_m", live.sar3d_norm_edge_margin_m)
)
live.sar3d_smooth_sigma = float(sar3d.get("smooth_sigma", live.sar3d_smooth_sigma))
live.sar3d_comp_range_power = float(
sar3d.get("comp_range_power", live.sar3d_comp_range_power)
)
live.sar3d_tx_per_segment = int(sar3d.get("tx_per_segment", live.sar3d_tx_per_segment))
live.sar3d_use_compensation = bool(sar3d.get("use_compensation", live.sar3d_use_compensation))
live.sar3d_normalize_pairs = bool(sar3d.get("normalize_pairs", live.sar3d_normalize_pairs))
live.sar3d_bottom_view_deg = float(sar3d.get("bottom_view_deg", live.sar3d_bottom_view_deg))