From e1dfbec712c282cc6b2decfa26f82d7cf2d220b7 Mon Sep 17 00:00:00 2001 From: awe Date: Fri, 25 Sep 2026 16:42:14 +0300 Subject: [PATCH] sar3d: python plumbing for the new parameters and real replay timing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../live_processing_mixin.py | 24 +-- .../app_window_config/profile_io_mixin.py | 38 +++-- .../app_window_config/state_builders.py | 19 ++- .../sections/processing_section.py | 150 ++++++++++++------ python_app/models/gui_profile_codec.py | 104 +++++++++--- python_app/models/gui_profile_schema.py | 37 +++-- .../orchestration/live_processing_config.py | 57 ++++--- python_app/scripts/replay_raw_producer.py | 40 ++++- python_app/scripts/replay_smoke_run.py | 25 ++- 9 files changed, 345 insertions(+), 149 deletions(-) diff --git a/python_app/gui/controllers/app_window_config/live_processing_mixin.py b/python_app/gui/controllers/app_window_config/live_processing_mixin.py index 0b410c3..627e625 100644 --- a/python_app/gui/controllers/app_window_config/live_processing_mixin.py +++ b/python_app/gui/controllers/app_window_config/live_processing_mixin.py @@ -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: diff --git a/python_app/gui/controllers/app_window_config/profile_io_mixin.py b/python_app/gui/controllers/app_window_config/profile_io_mixin.py index 8fbd169..ee01bef 100644 --- a/python_app/gui/controllers/app_window_config/profile_io_mixin.py +++ b/python_app/gui/controllers/app_window_config/profile_io_mixin.py @@ -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( diff --git a/python_app/gui/controllers/app_window_config/state_builders.py b/python_app/gui/controllers/app_window_config/state_builders.py index e2b8b09..b7c61dc 100644 --- a/python_app/gui/controllers/app_window_config/state_builders.py +++ b/python_app/gui/controllers/app_window_config/state_builders.py @@ -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( diff --git a/python_app/gui/controllers/sections/processing_section.py b/python_app/gui/controllers/sections/processing_section.py index 161d59a..fd7eb8e 100644 --- a/python_app/gui/controllers/sections/processing_section.py +++ b/python_app/gui/controllers/sections/processing_section.py @@ -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) diff --git a/python_app/models/gui_profile_codec.py b/python_app/models/gui_profile_codec.py index 2e73211..40d73a0 100644 --- a/python_app/models/gui_profile_codec.py +++ b/python_app/models/gui_profile_codec.py @@ -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, diff --git a/python_app/models/gui_profile_schema.py b/python_app/models/gui_profile_schema.py index 70d0036..c0f7f66 100644 --- a/python_app/models/gui_profile_schema.py +++ b/python_app/models/gui_profile_schema.py @@ -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: diff --git a/python_app/orchestration/live_processing_config.py b/python_app/orchestration/live_processing_config.py index 1b84cf0..0691abc 100644 --- a/python_app/orchestration/live_processing_config.py +++ b/python_app/orchestration/live_processing_config.py @@ -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), diff --git a/python_app/scripts/replay_raw_producer.py b/python_app/scripts/replay_raw_producer.py index 6ca933d..f0c089e 100644 --- a/python_app/scripts/replay_raw_producer.py +++ b/python_app/scripts/replay_raw_producer.py @@ -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), ) diff --git a/python_app/scripts/replay_smoke_run.py b/python_app/scripts/replay_smoke_run.py index cc8ce29..2d56ab6 100644 --- a/python_app/scripts/replay_smoke_run.py +++ b/python_app/scripts/replay_smoke_run.py @@ -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))