UI updates

This commit is contained in:
Ayzen
2026-04-01 20:21:16 +03:00
parent 4abc95c372
commit 669205d8f8
43 changed files with 2055 additions and 973 deletions
+305
View File
@@ -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
+28
View File
@@ -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",
]
+129
View File
@@ -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)