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.
187 lines
9.0 KiB
Python
187 lines
9.0 KiB
Python
"""Live processing settings model and atomic JSON writer."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class ProcessingLiveConfig:
|
|
"""Runtime-adjustable processing parameters shared with data processor."""
|
|
|
|
processor_mode: str = "pass_through"
|
|
pass_through_channel: str = "s21"
|
|
pass_through_fixed_y_enabled: bool = False
|
|
pass_through_y_min_db: float = -100.0
|
|
pass_through_y_max_db: float = 0.0
|
|
bscan_axis: str = "abs"
|
|
bscan_channel: str = "s21"
|
|
bscan_cut_m: float = 0.824
|
|
bscan_max_depth_m: float = 1.0
|
|
bscan_gain: float = 1.0
|
|
bscan_start_freq_mhz: float = 100.0
|
|
bscan_stop_freq_mhz: float = 8800.0
|
|
legacy_gpr_mode: str = "point"
|
|
gpr_input_positions: list[int] | None = None
|
|
gpr_output_positions: list[int] | None = None
|
|
gpr_min_depth_m: float = 2.0
|
|
gpr_max_depth_m: float = 14.0
|
|
gpr_range_comp_power: float = 0.1
|
|
gpr_comp_power: float = 0.2
|
|
gpr_object_min_frac: float = 0.7
|
|
gpr_score_mode: str = "combined"
|
|
# Backprojection intra-sweep speed-correction mode: "int_minus" (full
|
|
# correction) or "int_focus" (focusing residual only). Mirrors Python
|
|
# Horns_motion_3libre.py MOTION_CORRECTION_MODE.
|
|
gpr_motion_mode: str = "int_minus"
|
|
gpr_max_detected_objects_to_draw: int = 5
|
|
gpr_draw_top_m_objects: int = 2
|
|
gpr_object_approach_min_frames: int = 3
|
|
gpr_speed_m_s: float = 0.0
|
|
gpr_look_angle_deg: float = 0.0
|
|
# Motion-model knobs for the legacy GPR pipeline. `direction_sign` flips
|
|
# which way later events appear deeper (+1) vs shallower (-1) along Z.
|
|
# `apply_freq_phase_correction` enables intra-sweep frequency-domain phase
|
|
# compensation that fires *before* the IFFT — needed when the radar moves
|
|
# appreciably during one sweep.
|
|
gpr_direction_sign: float = 1.0
|
|
gpr_apply_freq_phase_correction: bool = True
|
|
# Anchor for the motion model's per-event `dt_ref`: 'frame_center' (default)
|
|
# or 'first_tx_event'. Mirrors Python `MOTION_CONFIG.reference_mode`.
|
|
gpr_reference_mode: str = "frame_center"
|
|
gpr_snr_thresh: float = 4.5
|
|
gpr_snr_comp_max: float = 25.0
|
|
gpr_start_freq_mhz: float = 3000.0
|
|
gpr_stop_freq_mhz: float = 6000.0
|
|
gpr_background_subtract_enabled: bool = True
|
|
gpr_background_mean_count: int = 10
|
|
gpr_remove_sidelobe_objects_enabled: bool = True
|
|
gpr_imaging_plane_y_m: float = 0.0
|
|
# Locator filter parameter consumed by the C++ TCP locator server. Coherent BP
|
|
# objects are already finalized in the processor (no score threshold); only legacy
|
|
# GPR still thresholds, on a pair count.
|
|
legacy_gpr_min_visible_pair_count: float = 0.0
|
|
# Visible X/Z window (metres). The locator and the desktop plot both clip
|
|
# detected objects to this window, so the socket broadcasts only what is shown.
|
|
gpr_visible_x_min_m: float = -2.0
|
|
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, 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.
|
|
sar3d_frame_radius: int = 0
|
|
sar3d_voxel_m: float = 0.02 # SAR_VOXEL_M, one edge for X, Y and Z
|
|
sar3d_bp_ground_depth_m: float = 1.25
|
|
sar3d_comp_range_power: float = 0.3
|
|
sar3d_use_compensation: bool = True
|
|
sar3d_normalize_each_frame: bool = True
|
|
# When true, the C++ data_processor ignores socket-supplied vlc updates
|
|
# and keeps using `gpr_speed_m_s` from this file.
|
|
ignore_socket_speed: bool = False
|
|
reprocess_current_result: bool = True
|
|
history_command_seq: int = 0
|
|
history_command: str = "none"
|
|
|
|
def __post_init__(self) -> None:
|
|
"""Normalize optional list fields to concrete integer lists."""
|
|
if self.gpr_input_positions is None:
|
|
self.gpr_input_positions = []
|
|
else:
|
|
self.gpr_input_positions = [int(value) for value in self.gpr_input_positions]
|
|
if self.gpr_output_positions is None:
|
|
self.gpr_output_positions = []
|
|
else:
|
|
self.gpr_output_positions = [int(value) for value in self.gpr_output_positions]
|
|
|
|
def to_dict(self) -> dict[str, object]:
|
|
"""Convert live config to JSON-serializable dictionary."""
|
|
return {
|
|
"processor_mode": str(self.processor_mode),
|
|
"pass_through_channel": str(self.pass_through_channel),
|
|
"pass_through_fixed_y_enabled": bool(self.pass_through_fixed_y_enabled),
|
|
"pass_through_y_min_db": float(self.pass_through_y_min_db),
|
|
"pass_through_y_max_db": float(self.pass_through_y_max_db),
|
|
"bscan_axis": str(self.bscan_axis),
|
|
"bscan_channel": str(self.bscan_channel),
|
|
"bscan_cut_m": float(self.bscan_cut_m),
|
|
"bscan_max_depth_m": float(self.bscan_max_depth_m),
|
|
"bscan_gain": float(self.bscan_gain),
|
|
"bscan_start_freq_mhz": float(self.bscan_start_freq_mhz),
|
|
"bscan_stop_freq_mhz": float(self.bscan_stop_freq_mhz),
|
|
"legacy_gpr_mode": str(self.legacy_gpr_mode),
|
|
"gpr_input_positions": [int(value) for value in self.gpr_input_positions],
|
|
"gpr_output_positions": [int(value) for value in self.gpr_output_positions],
|
|
"gpr_min_depth_m": float(self.gpr_min_depth_m),
|
|
"gpr_max_depth_m": float(self.gpr_max_depth_m),
|
|
"gpr_range_comp_power": float(self.gpr_range_comp_power),
|
|
"gpr_comp_power": float(self.gpr_comp_power),
|
|
"gpr_object_min_frac": float(self.gpr_object_min_frac),
|
|
"gpr_score_mode": str(self.gpr_score_mode),
|
|
"gpr_motion_mode": str(self.gpr_motion_mode),
|
|
"gpr_max_detected_objects_to_draw": int(self.gpr_max_detected_objects_to_draw),
|
|
"gpr_draw_top_m_objects": int(self.gpr_draw_top_m_objects),
|
|
"gpr_object_approach_min_frames": int(self.gpr_object_approach_min_frames),
|
|
"gpr_speed_m_s": float(self.gpr_speed_m_s),
|
|
"gpr_look_angle_deg": float(self.gpr_look_angle_deg),
|
|
"gpr_direction_sign": float(self.gpr_direction_sign),
|
|
"gpr_apply_freq_phase_correction": bool(self.gpr_apply_freq_phase_correction),
|
|
"gpr_reference_mode": str(self.gpr_reference_mode),
|
|
"gpr_snr_thresh": float(self.gpr_snr_thresh),
|
|
"gpr_snr_comp_max": float(self.gpr_snr_comp_max),
|
|
"gpr_start_freq_mhz": float(self.gpr_start_freq_mhz),
|
|
"gpr_stop_freq_mhz": float(self.gpr_stop_freq_mhz),
|
|
"gpr_background_subtract_enabled": bool(self.gpr_background_subtract_enabled),
|
|
"gpr_background_mean_count": int(self.gpr_background_mean_count),
|
|
"gpr_remove_sidelobe_objects_enabled": bool(self.gpr_remove_sidelobe_objects_enabled),
|
|
"gpr_imaging_plane_y_m": float(self.gpr_imaging_plane_y_m),
|
|
"legacy_gpr_min_visible_pair_count": float(self.legacy_gpr_min_visible_pair_count),
|
|
"gpr_visible_x_min_m": float(self.gpr_visible_x_min_m),
|
|
"gpr_visible_x_max_m": float(self.gpr_visible_x_max_m),
|
|
"gpr_visible_z_min_m": float(self.gpr_visible_z_min_m),
|
|
"gpr_visible_z_max_m": float(self.gpr_visible_z_max_m),
|
|
"sar3d_frame_radius": int(self.sar3d_frame_radius),
|
|
"sar3d_voxel_m": float(self.sar3d_voxel_m),
|
|
"sar3d_bp_ground_depth_m": float(self.sar3d_bp_ground_depth_m),
|
|
"sar3d_comp_range_power": float(self.sar3d_comp_range_power),
|
|
"sar3d_use_compensation": bool(self.sar3d_use_compensation),
|
|
"sar3d_normalize_each_frame": bool(self.sar3d_normalize_each_frame),
|
|
"ignore_socket_speed": bool(self.ignore_socket_speed),
|
|
"reprocess_current_result": bool(self.reprocess_current_result),
|
|
"history_command_seq": int(self.history_command_seq),
|
|
"history_command": str(self.history_command),
|
|
}
|
|
|
|
|
|
class ProcessingLiveConfigWriter:
|
|
"""Atomic writer for processing live-config file."""
|
|
|
|
def __init__(self, config_path: Path) -> None:
|
|
"""Create writer targeting `config_path`."""
|
|
self._config_path = config_path
|
|
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
@property
|
|
def path(self) -> Path:
|
|
"""Return destination config path."""
|
|
return self._config_path
|
|
|
|
def write(self, config: ProcessingLiveConfig) -> Path:
|
|
"""Atomically write config by temp-file replace."""
|
|
# Hot path: rewritten on every live knob change, so keep this at DEBUG.
|
|
logger.debug(
|
|
"Writing live processing config (mode=%s) to %s",
|
|
config.processor_mode,
|
|
self._config_path,
|
|
)
|
|
temp_path = self._config_path.with_suffix(self._config_path.suffix + ".tmp")
|
|
temp_path.write_text(json.dumps(config.to_dict(), indent=2), encoding="utf-8")
|
|
temp_path.replace(self._config_path)
|
|
return self._config_path
|