205 lines
6.8 KiB
Python
205 lines
6.8 KiB
Python
"""Dataclass schema for full GUI config profiles."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from copy import deepcopy
|
|
from dataclasses import dataclass, field
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from python_app.models.run_config_model import RunConfigModel
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class GuiSwitchStateModel:
|
|
"""UI-only state for switch selection controls."""
|
|
|
|
combo_mode: str = "text"
|
|
combos_text: str = ""
|
|
single_input: str = "0"
|
|
single_output: str = "0"
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class GuiPassThroughStateModel:
|
|
"""UI-only defaults for pass-through rendering."""
|
|
|
|
show_magnitude: bool = True
|
|
show_phase: bool = True
|
|
unwrap_phase: bool = False
|
|
combo_filter: str = ""
|
|
fixed_y_enabled: bool = False
|
|
y_min_db: float = -100.0
|
|
y_max_db: float = 0.0
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class GuiBscanStateModel:
|
|
"""UI-only defaults for B-scan live settings."""
|
|
|
|
axis: str = "abs"
|
|
cut_m: float = 0.824
|
|
max_depth_m: float = 1.0
|
|
gain: float = 1.0
|
|
start_freq_mhz: float = 100.0
|
|
stop_freq_mhz: float = 8800.0
|
|
subtract_mean_ascan: bool = False
|
|
# How many past sweeps the B-scan heatmap renders once acquisition is stopped.
|
|
# While running the window stays at the C++ replay window (see
|
|
# `_cpp_bscan_replay_window_for_config`); this only widens the stopped-mode view.
|
|
history_window_scans: int = 50
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class GuiGprStateModel:
|
|
"""UI-only defaults for coherent backprojection GPR live settings."""
|
|
|
|
input_positions: str = ""
|
|
output_positions: str = ""
|
|
min_depth_m: float = 2.0
|
|
max_depth_m: float = 14.0
|
|
range_comp_power: float = 0.1
|
|
# BP object-detection stop level, fraction of the global peak (Horns_motion_3libre.py
|
|
# BP_OBJECT_MIN_FRAC). Angle compensation and permittivity are fixed for coherent BP
|
|
# (Python 0.3 block), so they are not exposed here.
|
|
object_min_frac: float = 0.7
|
|
score_mode: str = "combined"
|
|
motion_mode: str = "int_minus"
|
|
# Intra-sweep motion-correction inputs. Sweep time is derived from acquisition
|
|
# metadata; speed comes from the socket unless `ignore_socket_speed_enabled`,
|
|
# in which case `speed_m_s` from here is used.
|
|
look_angle_deg: float = 0.0
|
|
direction_sign: float = 1.0
|
|
speed_m_s: float = 0.0
|
|
ignore_socket_speed_enabled: bool = False
|
|
max_detected_objects_to_draw: int = 5
|
|
draw_top_m_objects: int = 2
|
|
# Cross-frame approach filter: show an object only after it persists as a
|
|
# motion-consistent track this many consecutive frames (<= 1 disables it).
|
|
object_approach_min_frames: int = 3
|
|
start_freq_mhz: float = 3000.0
|
|
stop_freq_mhz: float = 6000.0
|
|
background_subtract_enabled: bool = True
|
|
background_mean_count: int = 10
|
|
remove_sidelobe_objects_enabled: bool = True
|
|
imaging_plane_y_m: float = 0.0
|
|
render_mode: str = "heatmap"
|
|
visible_x_min_m: float = -2.0
|
|
visible_x_max_m: float = 2.0
|
|
visible_z_min_m: float = 0.0
|
|
visible_z_max_m: float = 14.0
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class GuiLegacyGprStateModel:
|
|
"""UI-only defaults for legacy point/extended GPR live settings."""
|
|
|
|
mode: str = "point"
|
|
input_positions: str = ""
|
|
output_positions: str = ""
|
|
min_depth_m: float = 2.0
|
|
max_depth_m: float = 14.0
|
|
comp_power: float = 0.2
|
|
start_freq_mhz: float = 3000.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
|
|
apply_freq_phase_correction: bool = True
|
|
reference_mode: str = "frame_center"
|
|
snr_thresh: float = 4.5
|
|
snr_comp_max: float = 25.0
|
|
background_subtract_enabled: bool = True
|
|
background_mean_count: int = 10
|
|
render_mode: str = "heatmap"
|
|
min_visible_pair_count: int = 1
|
|
visible_x_min_m: float = -2.0
|
|
visible_x_max_m: float = 2.0
|
|
visible_z_min_m: float = 0.0
|
|
visible_z_max_m: float = 14.0
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class GuiProcessingStateModel:
|
|
"""UI-only processing-section defaults."""
|
|
|
|
selected_mode: str = "pass_through"
|
|
pass_through: GuiPassThroughStateModel = field(default_factory=GuiPassThroughStateModel)
|
|
bscan: GuiBscanStateModel = field(default_factory=GuiBscanStateModel)
|
|
gpr: GuiGprStateModel = field(default_factory=GuiGprStateModel)
|
|
legacy_gpr: GuiLegacyGprStateModel = field(default_factory=GuiLegacyGprStateModel)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class GuiDataActionsStateModel:
|
|
"""UI-only defaults for snapshot/export controls."""
|
|
|
|
save_count: int = 10
|
|
save_path: str = ""
|
|
save_name: str = "snapshot_manual"
|
|
# How many freshly acquired measurements the "Start + Record" action writes to
|
|
# disk before it stops recording (acquisition keeps running).
|
|
record_count: int = 100
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class GuiPreprocessDialogStateModel:
|
|
"""UI-only defaults for preprocessing dialog controls."""
|
|
|
|
set_name: str = "set_001"
|
|
radar_config_dir: str = ""
|
|
use_all_radar_configs: bool = False
|
|
median_sweep_count: int = 5
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class GuiStateModel:
|
|
"""GUI-only profile payload stored under top-level `gui`."""
|
|
|
|
version: int = 1
|
|
switches: GuiSwitchStateModel = field(default_factory=GuiSwitchStateModel)
|
|
processing: GuiProcessingStateModel = field(default_factory=GuiProcessingStateModel)
|
|
data_actions: GuiDataActionsStateModel = field(default_factory=GuiDataActionsStateModel)
|
|
preprocess_dialog: GuiPreprocessDialogStateModel = field(default_factory=GuiPreprocessDialogStateModel)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class GuiProfileModel:
|
|
"""Full GUI profile with pipeline-compatible root config and optional UI state."""
|
|
|
|
run_config: RunConfigModel = field(default_factory=RunConfigModel)
|
|
gui: GuiStateModel | None = None
|
|
|
|
@staticmethod
|
|
def from_dict(payload: dict[str, Any]) -> GuiProfileModel:
|
|
"""Build profile from JSON-like payload using codec layer."""
|
|
from python_app.models.gui_profile_codec import gui_profile_from_dict
|
|
|
|
return gui_profile_from_dict(payload)
|
|
|
|
@classmethod
|
|
def load_from_path(cls, path: Path) -> GuiProfileModel:
|
|
"""Load a JSON file from disk and decode it into a profile model.
|
|
|
|
Raises ValueError when the file's JSON root is not an object.
|
|
"""
|
|
logger.debug("Loading GUI profile from %s", path)
|
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
if not isinstance(payload, dict):
|
|
raise ValueError(f"Config profile root must be JSON object: {path}")
|
|
return cls.from_dict(payload)
|
|
|
|
def clone(self) -> GuiProfileModel:
|
|
"""Create deep copy while preserving whether `gui` is absent."""
|
|
return deepcopy(self)
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
"""Encode profile into JSON-serializable dictionary."""
|
|
from python_app.models.gui_profile_codec import gui_profile_to_dict
|
|
|
|
return gui_profile_to_dict(self)
|