Refactor radar system

This commit is contained in:
Ayzen
2026-10-05 16:46:28 +03:00
parent 515049d2ed
commit 8f097dfa2e
179 changed files with 8212 additions and 6676 deletions
@@ -4,7 +4,6 @@ from __future__ import annotations
from dataclasses import dataclass, field
import logging
import math
import threading
import time
from typing import TYPE_CHECKING
@@ -89,8 +88,6 @@ class MultiDeviceLibreVnaService:
switch_substage_count: int = 1
_controller: "MultiDeviceVnaController | None" = field(init=False, default=None, repr=False)
_sweep_configuration: SweepConfiguration | None = field(init=False, default=None, repr=False)
_using_mock_backend: bool = field(init=False, default=False, repr=False)
_mock_phase: float = field(init=False, default=0.0, repr=False)
def __post_init__(self) -> None:
"""Validate static topology and backend selection."""
@@ -101,10 +98,9 @@ class MultiDeviceLibreVnaService:
self.recovery_attempts = max(0, int(self.recovery_attempts))
mode = self.backend_mode.strip().lower()
if mode not in {"auto", "native", "mock"}:
if mode not in {"auto", "native"}:
raise ValueError(f"Unsupported multi-device backend mode: {self.backend_mode}")
self.backend_mode = mode
self._using_mock_backend = mode == "mock"
self.switch_substage_count = normalize_switch_substage_count(self.switch_substage_count)
@property
@@ -112,14 +108,9 @@ class MultiDeviceLibreVnaService:
"""Return how many stimulus outputs one acquisition produces."""
return RunConfigModel.MULTI_DEVICE_MASTER_PORTS * self.switch_substage_count
@property
def using_mock_backend(self) -> bool:
"""Return whether this service is generating synthetic data."""
return self._using_mock_backend
def open(self) -> None:
"""Open native device transports when not in mock mode."""
if self._using_mock_backend or self._controller is not None:
"""Open native device transports."""
if self._controller is not None:
return
try:
from python_app.hardware_full.librevna_multi_device_driver.controller import MultiDeviceVnaController
@@ -135,11 +126,6 @@ class MultiDeviceLibreVnaService:
self.master_serial, self.slave_serials,
)
except Exception as exc:
# Never silently latch to synthetic data: a deployed appliance must wait
# for the real device, not record fakes. Synthetic data requires an
# explicit backend_mode='mock' (selected in __post_init__); both 'auto'
# and 'native' re-raise so the producer's wait-for-device retry keeps
# trying until the hardware appears.
logger.warning("Multi-device open failed (backend_mode=%s): %s", self.backend_mode, exc)
raise
@@ -176,8 +162,6 @@ class MultiDeviceLibreVnaService:
sleeping, so a shutdown request aborts the loop immediately; an optional
`deadline_monotonic` caps the total wall-time spent here.
"""
if self._using_mock_backend:
return
self.close()
# Try to reopen immediately first, then back off only after a failure: a
@@ -261,12 +245,9 @@ class MultiDeviceLibreVnaService:
if self._sweep_configuration is None:
raise RuntimeError("Multi-device service is not configured")
capture_start_ns = time.monotonic_ns()
if self._using_mock_backend:
collection = self._acquire_mock_collection(collection_id, capture_start_ns)
else:
collection = self._acquire_native_collection_with_recovery(
collection_id, capture_start_ns, stop_event=stop_event
)
collection = self._acquire_native_collection_with_recovery(
collection_id, capture_start_ns, stop_event=stop_event
)
collection.capture_end_ns = time.monotonic_ns()
return collection
@@ -417,52 +398,6 @@ class MultiDeviceLibreVnaService:
capture_start_ns=capture_start_ns,
)
def _acquire_mock_collection(self, collection_id: int, capture_start_ns: int) -> SweepCollection:
assert self._sweep_configuration is not None
mock_sweep_start_ns = time.monotonic_ns()
points = int(self._sweep_configuration.points)
frequencies = np.linspace(
self._sweep_configuration.start_hz,
self._sweep_configuration.stop_hz,
points,
dtype=np.float32,
)
base_phase = 2.0 * math.pi * np.linspace(0.0, 1.0, points, dtype=np.float32) + self._mock_phase
traces: list[TraceData] = []
for output_pos in range(self.output_position_count):
reflected_phase = base_phase * (0.55 + 0.05 * output_pos) + 0.7 * (output_pos + 1)
s11 = (
(0.22 + 0.04 * output_pos) * np.cos(reflected_phase)
+ 1j * (0.22 + 0.04 * output_pos) * np.sin(reflected_phase)
).astype(np.complex64)
for input_pos in range(RunConfigModel.MULTI_DEVICE_INPUT_POSITIONS):
gain = 0.45 + 0.08 * input_pos + 0.03 * output_pos
phase = base_phase * (1.0 + 0.03 * input_pos) + (0.4 * input_pos + 0.9 * output_pos)
s21 = (gain * np.cos(phase) + 1j * gain * np.sin(phase)).astype(np.complex64)
traces.append(
TraceData(
combo=ComboKey(input=input_pos, output=output_pos),
frequency_hz=frequencies,
s11=s11,
s21=s21,
capture_start_ns=mock_sweep_start_ns,
)
)
# One shared window, as on real hardware: every combo of a collection comes
# out of the same synchronized cycle. Stamping each trace as it is built
# would instead spread the ends over the generation loop, which never
# happens natively and would mislead anyone reading mock captures.
mock_sweep_end_ns = time.monotonic_ns()
for trace in traces:
trace.capture_end_ns = mock_sweep_end_ns
self._mock_phase += 0.05
return SweepCollection(
collection_id=int(collection_id),
monotonic_ns=time.monotonic_ns(),
traces=traces,
capture_start_ns=capture_start_ns,
)
@staticmethod
def _required_s_parameter(s_parameters: dict[str, np.ndarray], name: str) -> np.ndarray:
values = s_parameters.get(name)