init commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Domain models for configuration and dataset payloads."""
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Data models for sweep and processing payload collections."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ComboKey:
|
||||
"""Switch combination key: input position + output position."""
|
||||
|
||||
input_pos: int
|
||||
output_pos: int
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TraceData:
|
||||
"""One frequency-domain S21 trace for a specific switch combination."""
|
||||
|
||||
combo: ComboKey
|
||||
frequency_hz: np.ndarray
|
||||
s21: np.ndarray
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SweepCollection:
|
||||
"""Raw or preprocessed collection containing multiple combo traces."""
|
||||
|
||||
collection_id: int
|
||||
monotonic_ns: int
|
||||
traces: list[TraceData] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ResultPayload:
|
||||
"""Processed payload item (trace-like or scalar) produced by processor stage."""
|
||||
|
||||
processing_name: str
|
||||
kind: int
|
||||
frequency_hz: np.ndarray
|
||||
trace: np.ndarray
|
||||
scalar_value: float = 0.0
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ResultBlock:
|
||||
"""Result payload block grouped by one switch combination."""
|
||||
|
||||
combo: ComboKey
|
||||
payloads: list[ResultPayload] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ResultCollection:
|
||||
"""Collection of processed payload blocks for one acquisition cycle."""
|
||||
|
||||
collection_id: int
|
||||
monotonic_ns: int
|
||||
blocks: list[ResultBlock] = field(default_factory=list)
|
||||
@@ -0,0 +1,177 @@
|
||||
"""Encoding and decoding logic for :mod:`python_app.models.run_config_schema`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from python_app.models.run_config_schema import ComboModel, RunConfigModel
|
||||
from python_app.models.run_config_validation import as_dict, load_ring_payload, load_switch_payload
|
||||
|
||||
|
||||
def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
|
||||
"""Decode JSON-like payload into :class:`RunConfigModel`."""
|
||||
# Schema carries only minimal-safe fallbacks; operational defaults live in run_config.json.
|
||||
model = RunConfigModel()
|
||||
|
||||
radar_payload = as_dict(payload.get("radar"), "radar")
|
||||
sweep_payload = as_dict(radar_payload.get("sweep"), "radar.sweep")
|
||||
switches_payload = as_dict(payload.get("switches"), "switches")
|
||||
port1_payload = as_dict(switches_payload.get("port1"), "switches.port1")
|
||||
port2_payload = as_dict(switches_payload.get("port2"), "switches.port2")
|
||||
run_payload = as_dict(payload.get("run"), "run")
|
||||
preprocess_payload = as_dict(payload.get("preprocess"), "preprocess")
|
||||
rings_payload = as_dict(payload.get("rings"), "rings")
|
||||
raw_ring_payload = as_dict(rings_payload.get("raw"), "rings.raw")
|
||||
raw_tap_ring_payload = as_dict(rings_payload.get("raw_tap"), "rings.raw_tap")
|
||||
pre_ring_payload = as_dict(rings_payload.get("preprocessed"), "rings.preprocessed")
|
||||
pre_tap_ring_payload = as_dict(rings_payload.get("preprocessed_tap"), "rings.preprocessed_tap")
|
||||
result_ring_payload = as_dict(rings_payload.get("results"), "rings.results")
|
||||
|
||||
model.radar.model = str(radar_payload.get("model", model.radar.model))
|
||||
model.radar.serial = str(radar_payload.get("serial", model.radar.serial))
|
||||
model.radar.driver_mode = str(radar_payload.get("driver_mode", model.radar.driver_mode))
|
||||
model.radar.mock_signal_hz = float(radar_payload.get("mock_signal_hz", model.radar.mock_signal_hz))
|
||||
|
||||
model.radar.sweep.start_hz = float(sweep_payload.get("start_hz", model.radar.sweep.start_hz))
|
||||
model.radar.sweep.stop_hz = float(sweep_payload.get("stop_hz", model.radar.sweep.stop_hz))
|
||||
model.radar.sweep.points = int(sweep_payload.get("points", model.radar.sweep.points))
|
||||
model.radar.sweep.if_bandwidth_hz = float(
|
||||
sweep_payload.get("if_bandwidth_hz", model.radar.sweep.if_bandwidth_hz)
|
||||
)
|
||||
model.radar.sweep.power_dbm = float(sweep_payload.get("stimulus_power_dbm", model.radar.sweep.power_dbm))
|
||||
|
||||
load_switch_payload(port1_payload, model.output_switch)
|
||||
load_switch_payload(port2_payload, model.input_switch)
|
||||
|
||||
model.runtime.settling_ms = int(run_payload.get("settling_ms", model.runtime.settling_ms))
|
||||
model.runtime.idle_sleep_ms = int(run_payload.get("idle_sleep_ms", model.runtime.idle_sleep_ms))
|
||||
model.runtime.continuous = bool(run_payload.get("continuous", model.runtime.continuous))
|
||||
model.runtime.processing_live_config_path = str(
|
||||
run_payload.get("processing_live_config_path", model.runtime.processing_live_config_path)
|
||||
)
|
||||
|
||||
model.preprocess.calibration_set = str(preprocess_payload.get("calibration_set", model.preprocess.calibration_set))
|
||||
model.preprocess.reference_set = str(preprocess_payload.get("reference_set", model.preprocess.reference_set))
|
||||
model.preprocess.calibration_bundle_path = str(
|
||||
preprocess_payload.get("calibration_bundle_path", model.preprocess.calibration_bundle_path)
|
||||
)
|
||||
model.preprocess.reference_bundle_path = str(
|
||||
preprocess_payload.get("reference_bundle_path", model.preprocess.reference_bundle_path)
|
||||
)
|
||||
|
||||
load_ring_payload(raw_ring_payload, model.rings.raw)
|
||||
load_ring_payload(raw_tap_ring_payload, model.rings.raw_tap)
|
||||
load_ring_payload(pre_ring_payload, model.rings.preprocessed)
|
||||
load_ring_payload(pre_tap_ring_payload, model.rings.preprocessed_tap)
|
||||
load_ring_payload(result_ring_payload, model.rings.results)
|
||||
|
||||
combos_payload = run_payload.get("combos", [])
|
||||
model.combos = []
|
||||
if isinstance(combos_payload, list):
|
||||
for combo in combos_payload:
|
||||
combo_payload = as_dict(combo, "run.combos[]")
|
||||
model.combos.append(
|
||||
ComboModel(
|
||||
input=int(combo_payload.get("input", 0)),
|
||||
output=int(combo_payload.get("output", 0)),
|
||||
)
|
||||
)
|
||||
|
||||
model.ensure_combos()
|
||||
return model
|
||||
|
||||
|
||||
def load_run_config(path: Path) -> RunConfigModel:
|
||||
"""Load JSON config from path and decode into :class:`RunConfigModel`."""
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError(f"Config root must be JSON object: {path}")
|
||||
return run_config_from_dict(payload)
|
||||
|
||||
|
||||
def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]:
|
||||
"""Encode :class:`RunConfigModel` to C++ pipeline-compatible JSON structure."""
|
||||
model.ensure_combos()
|
||||
return {
|
||||
"radar": {
|
||||
"model": model.radar.model,
|
||||
"serial": model.radar.serial,
|
||||
"driver_mode": model.radar.driver_mode,
|
||||
"mock_signal_hz": model.radar.mock_signal_hz,
|
||||
"sweep": {
|
||||
"start_hz": model.radar.sweep.start_hz,
|
||||
"stop_hz": model.radar.sweep.stop_hz,
|
||||
"points": model.radar.sweep.points,
|
||||
"if_bandwidth_hz": model.radar.sweep.if_bandwidth_hz,
|
||||
"stimulus_power_dbm": model.radar.sweep.power_dbm,
|
||||
},
|
||||
},
|
||||
"switches": {
|
||||
"port1": {
|
||||
"name": model.output_switch.name,
|
||||
"driver_mode": model.output_switch.driver_mode,
|
||||
"driver": model.output_switch.driver,
|
||||
"radar_port": model.output_switch.radar_port,
|
||||
"positions": model.output_switch.positions,
|
||||
"default_position": model.output_switch.default_position,
|
||||
"gpio_chip": model.output_switch.gpio_chip,
|
||||
"pin_a": model.output_switch.pin_a,
|
||||
"pin_b": model.output_switch.pin_b,
|
||||
"invert_logic": model.output_switch.invert_logic,
|
||||
},
|
||||
"port2": {
|
||||
"name": model.input_switch.name,
|
||||
"driver_mode": model.input_switch.driver_mode,
|
||||
"driver": model.input_switch.driver,
|
||||
"radar_port": model.input_switch.radar_port,
|
||||
"positions": model.input_switch.positions,
|
||||
"default_position": model.input_switch.default_position,
|
||||
"gpio_chip": model.input_switch.gpio_chip,
|
||||
"pin_a": model.input_switch.pin_a,
|
||||
"pin_b": model.input_switch.pin_b,
|
||||
"invert_logic": model.input_switch.invert_logic,
|
||||
},
|
||||
},
|
||||
"run": {
|
||||
"settling_ms": model.runtime.settling_ms,
|
||||
"idle_sleep_ms": model.runtime.idle_sleep_ms,
|
||||
"continuous": model.runtime.continuous,
|
||||
"processing_live_config_path": model.runtime.processing_live_config_path,
|
||||
"combos": [{"input": combo.input, "output": combo.output} for combo in model.combos],
|
||||
},
|
||||
"preprocess": {
|
||||
"calibration_set": model.preprocess.calibration_set,
|
||||
"reference_set": model.preprocess.reference_set,
|
||||
"calibration_bundle_path": model.preprocess.calibration_bundle_path,
|
||||
"reference_bundle_path": model.preprocess.reference_bundle_path,
|
||||
},
|
||||
"rings": {
|
||||
"raw": {
|
||||
"name": model.rings.raw.name,
|
||||
"capacity": model.rings.raw.capacity,
|
||||
"slot_size_bytes": model.rings.raw.slot_size_bytes,
|
||||
},
|
||||
"raw_tap": {
|
||||
"name": model.rings.raw_tap.name,
|
||||
"capacity": model.rings.raw_tap.capacity,
|
||||
"slot_size_bytes": model.rings.raw_tap.slot_size_bytes,
|
||||
},
|
||||
"preprocessed": {
|
||||
"name": model.rings.preprocessed.name,
|
||||
"capacity": model.rings.preprocessed.capacity,
|
||||
"slot_size_bytes": model.rings.preprocessed.slot_size_bytes,
|
||||
},
|
||||
"preprocessed_tap": {
|
||||
"name": model.rings.preprocessed_tap.name,
|
||||
"capacity": model.rings.preprocessed_tap.capacity,
|
||||
"slot_size_bytes": model.rings.preprocessed_tap.slot_size_bytes,
|
||||
},
|
||||
"results": {
|
||||
"name": model.rings.results.name,
|
||||
"capacity": model.rings.results.capacity,
|
||||
"slot_size_bytes": model.rings.results.slot_size_bytes,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Facade module for run configuration schema, codec, and validation helpers."""
|
||||
|
||||
from python_app.models.run_config_codec import load_run_config, run_config_from_dict, run_config_to_dict
|
||||
from python_app.models.run_config_schema import (
|
||||
ComboModel,
|
||||
PreprocessModel,
|
||||
RadarModel,
|
||||
RadarSweepModel,
|
||||
RingEndpointModel,
|
||||
RingsModel,
|
||||
RunConfigModel,
|
||||
RuntimeModel,
|
||||
SwitchModel,
|
||||
)
|
||||
from python_app.models.run_config_validation import (
|
||||
as_dict,
|
||||
load_ring_payload,
|
||||
load_switch_payload,
|
||||
parse_combos_from_text,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ComboModel",
|
||||
"PreprocessModel",
|
||||
"RadarModel",
|
||||
"RadarSweepModel",
|
||||
"RingEndpointModel",
|
||||
"RingsModel",
|
||||
"RunConfigModel",
|
||||
"RuntimeModel",
|
||||
"SwitchModel",
|
||||
"as_dict",
|
||||
"load_ring_payload",
|
||||
"load_run_config",
|
||||
"load_switch_payload",
|
||||
"parse_combos_from_text",
|
||||
"run_config_from_dict",
|
||||
"run_config_to_dict",
|
||||
]
|
||||
@@ -0,0 +1,146 @@
|
||||
"""Dataclass schema for runtime configuration used by Python pipeline tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ComboModel:
|
||||
"""One switch combination used for an acquisition sweep."""
|
||||
|
||||
input: int
|
||||
output: int
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RadarSweepModel:
|
||||
"""Sweep settings for LibreVNA acquisition."""
|
||||
|
||||
# Keep schema defaults minimal/safe; operational values come from run_config.json.
|
||||
start_hz: float = 0.0
|
||||
stop_hz: float = 0.0
|
||||
points: int = 1
|
||||
if_bandwidth_hz: float = 1.0
|
||||
power_dbm: float = -30.0
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RadarModel:
|
||||
"""Radar section of run configuration."""
|
||||
|
||||
model: str = ""
|
||||
serial: str = ""
|
||||
driver_mode: str = "mock"
|
||||
mock_signal_hz: float = 1_000_000.0
|
||||
sweep: RadarSweepModel = field(default_factory=RadarSweepModel)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SwitchModel:
|
||||
"""Generic switch section of run configuration."""
|
||||
|
||||
name: str
|
||||
driver_mode: str = "mock"
|
||||
driver: str = ""
|
||||
radar_port: int = 0
|
||||
positions: int = 1
|
||||
default_position: int = 0
|
||||
gpio_chip: str = ""
|
||||
pin_a: int = -1
|
||||
pin_b: int = -1
|
||||
invert_logic: bool = False
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RingEndpointModel:
|
||||
"""Shared-memory ring endpoint description."""
|
||||
|
||||
name: str
|
||||
capacity: int = 1
|
||||
slot_size_bytes: int = 4096
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RingsModel:
|
||||
"""Ring endpoints used by orchestration pipeline."""
|
||||
|
||||
raw: RingEndpointModel = field(default_factory=lambda: RingEndpointModel(name=""))
|
||||
raw_tap: RingEndpointModel = field(default_factory=lambda: RingEndpointModel(name=""))
|
||||
preprocessed: RingEndpointModel = field(default_factory=lambda: RingEndpointModel(name=""))
|
||||
preprocessed_tap: RingEndpointModel = field(default_factory=lambda: RingEndpointModel(name=""))
|
||||
results: RingEndpointModel = field(default_factory=lambda: RingEndpointModel(name=""))
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RuntimeModel:
|
||||
"""Runtime process behavior and paths."""
|
||||
|
||||
settling_ms: int = 0
|
||||
idle_sleep_ms: int = 2
|
||||
continuous: bool = False
|
||||
processing_live_config_path: str = ""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PreprocessModel:
|
||||
"""Selected preprocessing artifacts for live acquisition."""
|
||||
|
||||
calibration_set: str = ""
|
||||
reference_set: str = ""
|
||||
calibration_bundle_path: str = ""
|
||||
reference_bundle_path: str = ""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RunConfigModel:
|
||||
"""Top-level runtime config model consumed by C++ processes and GUI."""
|
||||
|
||||
radar: RadarModel = field(default_factory=RadarModel)
|
||||
input_switch: SwitchModel = field(default_factory=lambda: SwitchModel(name=""))
|
||||
output_switch: SwitchModel = field(default_factory=lambda: SwitchModel(name=""))
|
||||
rings: RingsModel = field(default_factory=RingsModel)
|
||||
runtime: RuntimeModel = field(default_factory=RuntimeModel)
|
||||
preprocess: PreprocessModel = field(default_factory=PreprocessModel)
|
||||
combos: list[ComboModel] = field(default_factory=list)
|
||||
|
||||
@staticmethod
|
||||
def build_full_combos(input_positions: int, output_positions: int) -> list[ComboModel]:
|
||||
"""Build full Cartesian product of input/output switch positions."""
|
||||
return [
|
||||
ComboModel(input=input_pos, output=output_pos)
|
||||
for output_pos in range(output_positions)
|
||||
for input_pos in range(input_positions)
|
||||
]
|
||||
|
||||
def ensure_combos(self) -> None:
|
||||
"""Populate combos with full matrix when no explicit run combos are set."""
|
||||
if self.combos:
|
||||
return
|
||||
self.combos = self.build_full_combos(self.input_switch.positions, self.output_switch.positions)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, payload: dict[str, Any]) -> RunConfigModel:
|
||||
"""Build model from JSON-like payload using codec layer."""
|
||||
from python_app.models.run_config_codec import run_config_from_dict
|
||||
|
||||
return run_config_from_dict(payload)
|
||||
|
||||
@classmethod
|
||||
def load_from_path(cls, path: Path) -> RunConfigModel:
|
||||
"""Load JSON file from disk and decode into model."""
|
||||
from python_app.models.run_config_codec import load_run_config
|
||||
|
||||
return load_run_config(path)
|
||||
|
||||
def clone(self) -> RunConfigModel:
|
||||
"""Create deep copy through codec round-trip."""
|
||||
return RunConfigModel.from_dict(self.to_dict())
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Encode model into JSON-serializable dictionary."""
|
||||
from python_app.models.run_config_codec import run_config_to_dict
|
||||
|
||||
return run_config_to_dict(self)
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Validation and normalization helpers for run configuration payloads."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from python_app.models.run_config_schema import ComboModel, RingEndpointModel, SwitchModel
|
||||
|
||||
|
||||
def as_dict(value: Any, context: str) -> dict[str, Any]:
|
||||
"""Validate that a payload node is a JSON object and return it."""
|
||||
if value is None:
|
||||
return {}
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{context} must be a JSON object")
|
||||
return value
|
||||
|
||||
|
||||
def load_switch_payload(
|
||||
payload: dict[str, Any],
|
||||
target: SwitchModel,
|
||||
) -> None:
|
||||
"""Populate switch model from payload preserving defaults for missing values."""
|
||||
target.name = str(payload.get("name", target.name))
|
||||
target.driver_mode = str(payload.get("driver_mode", target.driver_mode))
|
||||
target.driver = str(payload.get("driver", target.driver))
|
||||
target.radar_port = int(payload.get("radar_port", target.radar_port))
|
||||
target.positions = int(payload.get("positions", target.positions))
|
||||
target.default_position = int(payload.get("default_position", target.default_position))
|
||||
target.gpio_chip = str(payload.get("gpio_chip", target.gpio_chip))
|
||||
target.pin_a = int(payload.get("pin_a", target.pin_a))
|
||||
target.pin_b = int(payload.get("pin_b", target.pin_b))
|
||||
target.invert_logic = bool(payload.get("invert_logic", target.invert_logic))
|
||||
|
||||
|
||||
def load_ring_payload(payload: dict[str, Any], target: RingEndpointModel) -> None:
|
||||
"""Populate ring endpoint model from payload preserving defaults."""
|
||||
target.name = str(payload.get("name", target.name))
|
||||
target.capacity = int(payload.get("capacity", target.capacity))
|
||||
target.slot_size_bytes = int(payload.get("slot_size_bytes", target.slot_size_bytes))
|
||||
|
||||
|
||||
def parse_combos_from_text(text: str) -> list[ComboModel]:
|
||||
"""Parse UI combos string in `input:output,input:output` format."""
|
||||
cleaned = text.strip()
|
||||
if not cleaned:
|
||||
return []
|
||||
|
||||
combos: list[ComboModel] = []
|
||||
for item in cleaned.split(","):
|
||||
pair = item.strip()
|
||||
if not pair:
|
||||
continue
|
||||
if ":" not in pair:
|
||||
raise ValueError(f"Invalid combo syntax: {pair!r}. Expected input:output")
|
||||
|
||||
input_text, output_text = pair.split(":", 1)
|
||||
combos.append(ComboModel(input=int(input_text.strip()), output=int(output_text.strip())))
|
||||
|
||||
if not combos:
|
||||
raise ValueError("No valid combos were provided")
|
||||
return combos
|
||||
Reference in New Issue
Block a user