UI updates
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
"""Encoding and decoding logic for :mod:`python_app.models.gui_profile_schema`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from python_app.models.gui_profile_schema import (
|
||||
GuiBscanStateModel,
|
||||
GuiDataActionsStateModel,
|
||||
GuiGprStateModel,
|
||||
GuiPassThroughStateModel,
|
||||
GuiPreprocessDialogStateModel,
|
||||
GuiProcessingStateModel,
|
||||
GuiProfileModel,
|
||||
GuiStateModel,
|
||||
GuiSwitchStateModel,
|
||||
)
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
|
||||
|
||||
def _as_dict(value: Any, context: str) -> dict[str, Any]:
|
||||
"""Validate payload node is object-like, treating missing values as empty object."""
|
||||
if value is None:
|
||||
return {}
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{context} must be a JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def _optional_string(object_payload: dict[str, Any], key: str, fallback: str, context: str) -> str:
|
||||
"""Return optional string field with type validation."""
|
||||
raw_value = object_payload.get(key, fallback)
|
||||
if not isinstance(raw_value, str):
|
||||
raise ValueError(f"{context}.{key} must be a JSON string")
|
||||
return raw_value
|
||||
|
||||
|
||||
def _optional_bool(object_payload: dict[str, Any], key: str, fallback: bool, context: str) -> bool:
|
||||
"""Return optional boolean field with type validation."""
|
||||
raw_value = object_payload.get(key, fallback)
|
||||
if not isinstance(raw_value, bool):
|
||||
raise ValueError(f"{context}.{key} must be a JSON bool")
|
||||
return raw_value
|
||||
|
||||
|
||||
def _optional_int(object_payload: dict[str, Any], key: str, fallback: int, context: str) -> int:
|
||||
"""Return optional integer field with type validation."""
|
||||
raw_value = object_payload.get(key, fallback)
|
||||
if isinstance(raw_value, bool) or not isinstance(raw_value, int):
|
||||
raise ValueError(f"{context}.{key} must be a JSON integer")
|
||||
return int(raw_value)
|
||||
|
||||
|
||||
def _optional_float(object_payload: dict[str, Any], key: str, fallback: float, context: str) -> float:
|
||||
"""Return optional numeric field with type validation."""
|
||||
raw_value = object_payload.get(key, fallback)
|
||||
if isinstance(raw_value, bool) or not isinstance(raw_value, (int, float)):
|
||||
raise ValueError(f"{context}.{key} must be a JSON number")
|
||||
return float(raw_value)
|
||||
|
||||
|
||||
def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
|
||||
"""Decode JSON-like payload into :class:`GuiProfileModel`."""
|
||||
profile = GuiProfileModel(run_config=RunConfigModel.from_dict(payload), gui=None)
|
||||
gui_payload = payload.get("gui")
|
||||
if gui_payload is None:
|
||||
return profile
|
||||
|
||||
gui_object = _as_dict(gui_payload, "gui")
|
||||
gui = GuiStateModel()
|
||||
gui.version = _optional_int(gui_object, "version", gui.version, "gui")
|
||||
if gui.version != 1:
|
||||
raise ValueError(f"Unsupported gui.version: {gui.version}")
|
||||
|
||||
switches_object = _as_dict(gui_object.get("switches"), "gui.switches")
|
||||
gui.switches = GuiSwitchStateModel(
|
||||
combo_mode=_optional_string(switches_object, "combo_mode", gui.switches.combo_mode, "gui.switches"),
|
||||
combos_text=_optional_string(switches_object, "combos_text", gui.switches.combos_text, "gui.switches"),
|
||||
single_input=_optional_string(switches_object, "single_input", gui.switches.single_input, "gui.switches"),
|
||||
single_output=_optional_string(
|
||||
switches_object,
|
||||
"single_output",
|
||||
gui.switches.single_output,
|
||||
"gui.switches",
|
||||
),
|
||||
)
|
||||
if gui.switches.combo_mode not in {"text", "single"}:
|
||||
raise ValueError("gui.switches.combo_mode must be either 'text' or 'single'")
|
||||
|
||||
processing_object = _as_dict(gui_object.get("processing"), "gui.processing")
|
||||
pass_through_object = _as_dict(processing_object.get("pass_through"), "gui.processing.pass_through")
|
||||
bscan_object = _as_dict(processing_object.get("bscan"), "gui.processing.bscan")
|
||||
gpr_object = _as_dict(processing_object.get("gpr"), "gui.processing.gpr")
|
||||
gui.processing = GuiProcessingStateModel(
|
||||
selected_mode=_optional_string(
|
||||
processing_object,
|
||||
"selected_mode",
|
||||
gui.processing.selected_mode,
|
||||
"gui.processing",
|
||||
),
|
||||
pass_through=GuiPassThroughStateModel(
|
||||
show_magnitude=_optional_bool(
|
||||
pass_through_object,
|
||||
"show_magnitude",
|
||||
gui.processing.pass_through.show_magnitude,
|
||||
"gui.processing.pass_through",
|
||||
),
|
||||
show_phase=_optional_bool(
|
||||
pass_through_object,
|
||||
"show_phase",
|
||||
gui.processing.pass_through.show_phase,
|
||||
"gui.processing.pass_through",
|
||||
),
|
||||
fixed_y_enabled=_optional_bool(
|
||||
pass_through_object,
|
||||
"fixed_y_enabled",
|
||||
gui.processing.pass_through.fixed_y_enabled,
|
||||
"gui.processing.pass_through",
|
||||
),
|
||||
y_min_db=_optional_float(
|
||||
pass_through_object,
|
||||
"y_min_db",
|
||||
gui.processing.pass_through.y_min_db,
|
||||
"gui.processing.pass_through",
|
||||
),
|
||||
y_max_db=_optional_float(
|
||||
pass_through_object,
|
||||
"y_max_db",
|
||||
gui.processing.pass_through.y_max_db,
|
||||
"gui.processing.pass_through",
|
||||
),
|
||||
),
|
||||
bscan=GuiBscanStateModel(
|
||||
axis=_optional_string(bscan_object, "axis", gui.processing.bscan.axis, "gui.processing.bscan"),
|
||||
cut_m=_optional_float(bscan_object, "cut_m", gui.processing.bscan.cut_m, "gui.processing.bscan"),
|
||||
max_depth_m=_optional_float(
|
||||
bscan_object,
|
||||
"max_depth_m",
|
||||
gui.processing.bscan.max_depth_m,
|
||||
"gui.processing.bscan",
|
||||
),
|
||||
gain=_optional_float(bscan_object, "gain", gui.processing.bscan.gain, "gui.processing.bscan"),
|
||||
start_freq_mhz=_optional_float(
|
||||
bscan_object,
|
||||
"start_freq_mhz",
|
||||
gui.processing.bscan.start_freq_mhz,
|
||||
"gui.processing.bscan",
|
||||
),
|
||||
stop_freq_mhz=_optional_float(
|
||||
bscan_object,
|
||||
"stop_freq_mhz",
|
||||
gui.processing.bscan.stop_freq_mhz,
|
||||
"gui.processing.bscan",
|
||||
),
|
||||
),
|
||||
gpr=GuiGprStateModel(
|
||||
input_positions=_optional_string(
|
||||
gpr_object,
|
||||
"input_positions",
|
||||
gui.processing.gpr.input_positions,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
output_positions=_optional_string(
|
||||
gpr_object,
|
||||
"output_positions",
|
||||
gui.processing.gpr.output_positions,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
min_depth_m=_optional_float(
|
||||
gpr_object,
|
||||
"min_depth_m",
|
||||
gui.processing.gpr.min_depth_m,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
max_depth_m=_optional_float(
|
||||
gpr_object,
|
||||
"max_depth_m",
|
||||
gui.processing.gpr.max_depth_m,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
comp_power=_optional_float(
|
||||
gpr_object,
|
||||
"comp_power",
|
||||
gui.processing.gpr.comp_power,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
start_freq_mhz=_optional_float(
|
||||
gpr_object,
|
||||
"start_freq_mhz",
|
||||
gui.processing.gpr.start_freq_mhz,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
stop_freq_mhz=_optional_float(
|
||||
gpr_object,
|
||||
"stop_freq_mhz",
|
||||
gui.processing.gpr.stop_freq_mhz,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
background_subtract_enabled=_optional_bool(
|
||||
gpr_object,
|
||||
"background_subtract_enabled",
|
||||
gui.processing.gpr.background_subtract_enabled,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
background_mean_count=_optional_int(
|
||||
gpr_object,
|
||||
"background_mean_count",
|
||||
gui.processing.gpr.background_mean_count,
|
||||
"gui.processing.gpr",
|
||||
),
|
||||
),
|
||||
)
|
||||
if gui.processing.selected_mode not in {"pass_through", "bscan", "gpr"}:
|
||||
raise ValueError("gui.processing.selected_mode must be one of: pass_through, bscan, gpr")
|
||||
if gui.processing.bscan.axis not in {"abs", "real", "phase"}:
|
||||
raise ValueError("gui.processing.bscan.axis must be one of: abs, real, phase")
|
||||
|
||||
data_actions_object = _as_dict(gui_object.get("data_actions"), "gui.data_actions")
|
||||
gui.data_actions = GuiDataActionsStateModel(
|
||||
save_count=_optional_int(
|
||||
data_actions_object,
|
||||
"save_count",
|
||||
gui.data_actions.save_count,
|
||||
"gui.data_actions",
|
||||
),
|
||||
save_path=_optional_string(
|
||||
data_actions_object,
|
||||
"save_path",
|
||||
gui.data_actions.save_path,
|
||||
"gui.data_actions",
|
||||
),
|
||||
save_name=_optional_string(
|
||||
data_actions_object,
|
||||
"save_name",
|
||||
gui.data_actions.save_name,
|
||||
"gui.data_actions",
|
||||
),
|
||||
)
|
||||
|
||||
preprocess_dialog_object = _as_dict(gui_object.get("preprocess_dialog"), "gui.preprocess_dialog")
|
||||
gui.preprocess_dialog = GuiPreprocessDialogStateModel(
|
||||
set_name=_optional_string(
|
||||
preprocess_dialog_object,
|
||||
"set_name",
|
||||
gui.preprocess_dialog.set_name,
|
||||
"gui.preprocess_dialog",
|
||||
),
|
||||
)
|
||||
|
||||
profile.gui = gui
|
||||
return profile
|
||||
|
||||
|
||||
def gui_profile_to_dict(model: GuiProfileModel) -> dict[str, Any]:
|
||||
"""Encode :class:`GuiProfileModel` into JSON-serializable dictionary."""
|
||||
payload = model.run_config.to_dict()
|
||||
gui = model.gui if model.gui is not None else GuiStateModel()
|
||||
payload["gui"] = {
|
||||
"version": gui.version,
|
||||
"switches": {
|
||||
"combo_mode": gui.switches.combo_mode,
|
||||
"combos_text": gui.switches.combos_text,
|
||||
"single_input": gui.switches.single_input,
|
||||
"single_output": gui.switches.single_output,
|
||||
},
|
||||
"processing": {
|
||||
"selected_mode": gui.processing.selected_mode,
|
||||
"pass_through": {
|
||||
"show_magnitude": gui.processing.pass_through.show_magnitude,
|
||||
"show_phase": gui.processing.pass_through.show_phase,
|
||||
"fixed_y_enabled": gui.processing.pass_through.fixed_y_enabled,
|
||||
"y_min_db": gui.processing.pass_through.y_min_db,
|
||||
"y_max_db": gui.processing.pass_through.y_max_db,
|
||||
},
|
||||
"bscan": {
|
||||
"axis": gui.processing.bscan.axis,
|
||||
"cut_m": gui.processing.bscan.cut_m,
|
||||
"max_depth_m": gui.processing.bscan.max_depth_m,
|
||||
"gain": gui.processing.bscan.gain,
|
||||
"start_freq_mhz": gui.processing.bscan.start_freq_mhz,
|
||||
"stop_freq_mhz": gui.processing.bscan.stop_freq_mhz,
|
||||
},
|
||||
"gpr": {
|
||||
"input_positions": gui.processing.gpr.input_positions,
|
||||
"output_positions": gui.processing.gpr.output_positions,
|
||||
"min_depth_m": gui.processing.gpr.min_depth_m,
|
||||
"max_depth_m": gui.processing.gpr.max_depth_m,
|
||||
"comp_power": gui.processing.gpr.comp_power,
|
||||
"start_freq_mhz": gui.processing.gpr.start_freq_mhz,
|
||||
"stop_freq_mhz": gui.processing.gpr.stop_freq_mhz,
|
||||
"background_subtract_enabled": gui.processing.gpr.background_subtract_enabled,
|
||||
"background_mean_count": gui.processing.gpr.background_mean_count,
|
||||
},
|
||||
},
|
||||
"data_actions": {
|
||||
"save_count": gui.data_actions.save_count,
|
||||
"save_path": gui.data_actions.save_path,
|
||||
"save_name": gui.data_actions.save_name,
|
||||
},
|
||||
"preprocess_dialog": {
|
||||
"set_name": gui.preprocess_dialog.set_name,
|
||||
},
|
||||
}
|
||||
return payload
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Facade module for GUI profile schema and codec helpers."""
|
||||
|
||||
from python_app.models.gui_profile_codec import gui_profile_from_dict, gui_profile_to_dict
|
||||
from python_app.models.gui_profile_schema import (
|
||||
GuiBscanStateModel,
|
||||
GuiDataActionsStateModel,
|
||||
GuiGprStateModel,
|
||||
GuiPassThroughStateModel,
|
||||
GuiPreprocessDialogStateModel,
|
||||
GuiProcessingStateModel,
|
||||
GuiProfileModel,
|
||||
GuiStateModel,
|
||||
GuiSwitchStateModel,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"GuiBscanStateModel",
|
||||
"GuiDataActionsStateModel",
|
||||
"GuiGprStateModel",
|
||||
"GuiPassThroughStateModel",
|
||||
"GuiPreprocessDialogStateModel",
|
||||
"GuiProcessingStateModel",
|
||||
"GuiProfileModel",
|
||||
"GuiStateModel",
|
||||
"GuiSwitchStateModel",
|
||||
"gui_profile_from_dict",
|
||||
"gui_profile_to_dict",
|
||||
]
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Dataclass schema for full GUI config profiles."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, field
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
|
||||
|
||||
@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
|
||||
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
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GuiGprStateModel:
|
||||
"""UI-only defaults for GPR live settings."""
|
||||
|
||||
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
|
||||
background_subtract_enabled: bool = True
|
||||
background_mean_count: int = 10
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@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"
|
||||
|
||||
|
||||
@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 JSON file from disk and decode into profile model."""
|
||||
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)
|
||||
Reference in New Issue
Block a user