184 lines
5.6 KiB
Python
184 lines
5.6 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
|
|
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
|
|
|
|
|
|
@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.28
|
|
angle_comp_power: float = 0.10
|
|
score_mode: str = "combined"
|
|
max_detected_objects_to_draw: int = 5
|
|
draw_top_m_objects: int = 2
|
|
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"
|
|
min_visible_score: float = 0.0
|
|
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"
|
|
|
|
|
|
@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)
|