Files
radar_system/python_app/models/gui_profile_codec.py
T
2026-04-01 20:21:16 +03:00

306 lines
12 KiB
Python

"""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