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
@@ -0,0 +1,121 @@
"""One moving point reflector shared by single and multi-radar mock acquisition."""
from __future__ import annotations
from dataclasses import dataclass, field
import time
import numpy as np
from python_app.hardware_full.librevna_driver.models import SweepResult
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
from python_app.models.run_config_model import RadarSweepModel, RunConfigModel
_WAVE_SPEED_M_S = 3e8
_FRAME_PERIOD_S = 0.05
_MOTION_PERIOD_S = 8.0
@dataclass(slots=True)
class PointTargetMockService:
"""Simulate bistatic propagation from configured antennas to one moving target.
The target moves back and forth along the array and between depths 3.25–4.25 m.
One monotonic sample time determines every channel of a collection; channel
numbers never contribute an arbitrary gain, range, or phase offset.
"""
config: RunConfigModel
_frequency_hz: np.ndarray | None = field(init=False, default=None, repr=False)
_phase_per_m: np.ndarray | None = field(init=False, default=None, repr=False)
_last_capture_ns: int = field(init=False, default=0, repr=False)
_epoch_ns: int = field(init=False, repr=False)
_combos: tuple[ComboKey, ...] = field(init=False, repr=False)
_active_combo: ComboKey = field(init=False, repr=False)
def __post_init__(self) -> None:
self._epoch_ns = time.monotonic_ns()
self._combos = tuple(ComboKey(c.input, c.output) for c in (
self.config.combos or self.config.build_runtime_combos()
))
self._active_combo = self._combos[0]
def open(self) -> None:
"""Mock acquisition owns no hardware resources."""
def close(self) -> None:
"""Mock acquisition owns no hardware resources."""
def recover(self) -> None:
"""Mock acquisition has no hardware connection to recover."""
def read_device_limits(self) -> dict[str, float | int]:
raise RuntimeError("Mock radar has no hardware device limits")
def configure(self, sweep: RadarSweepModel) -> None:
self._frequency_hz = np.linspace(sweep.start_hz, sweep.stop_hz, sweep.points, dtype=np.float32)
self._phase_per_m = -2j * np.pi * self._frequency_hz.astype(np.float64) / _WAVE_SPEED_M_S
def set_active_combo(self, combo: ComboKey) -> None:
if not (0 <= combo.input < self.config.input_switch.positions
and 0 <= combo.output < self.config.output_switch.positions):
raise ValueError(f"Mock combo is outside the antenna matrix: {combo}")
self._active_combo = ComboKey(combo.input, combo.output)
def acquire(self) -> SweepResult:
collection = self._capture((self._active_combo,), 1)
trace = collection.traces[0]
return SweepResult(
x=trace.frequency_hz, traces={"s11": trace.s11, "s21": trace.s21},
capture_start_ns=trace.capture_start_ns, capture_end_ns=trace.capture_end_ns,
)
def acquire_collection(self, collection_id: int = 1) -> SweepCollection:
return self._capture(self._combos, collection_id)
def acquire_combo_collection(
self, *, input_pos: int, output_pos: int, collection_id: int = 1,
) -> SweepCollection:
self.set_active_combo(ComboKey(input_pos, output_pos))
return self._capture((self._active_combo,), collection_id)
def _target_position(
self, sample_ns: int,
tx: dict[int, tuple[float, float, float]], rx: dict[int, tuple[float, float, float]],
) -> np.ndarray:
coordinates = [*tx.values(), *rx.values()] or [(0.0, 0.0, 0.0)]
xs = [position[0] for position in coordinates]
displacement = np.sin(2 * np.pi * (sample_ns - self._epoch_ns) / 1e9 / _MOTION_PERIOD_S)
return np.array([
(min(xs) + max(xs)) / 2 + max(0.75, (max(xs) - min(xs)) / 2 + 0.25) * displacement,
0.25, 3.75 + 0.5 * displacement,
])
def _capture(self, combos: tuple[ComboKey, ...], collection_id: int) -> SweepCollection:
if self._frequency_hz is None or self._phase_per_m is None:
raise RuntimeError("Mock radar is not configured")
delay_s = _FRAME_PERIOD_S - (time.monotonic_ns() - self._last_capture_ns) / 1e9
if delay_s > 0:
time.sleep(delay_s)
capture_start_ns = time.monotonic_ns()
tx = {p.output_pos: (p.x_m, p.y_m, p.z_m) for p in self.config.gpr.tx_geometry}
rx = {p.input_pos: (p.x_m, p.y_m, p.z_m) for p in self.config.gpr.rx_geometry}
target = self._target_position(capture_start_ns, tx, rx)
traces = []
for combo in combos:
r_tx = float(np.linalg.norm(target - tx.get(combo.output, (0.0, 0.0, 0.0))))
r_rx = float(np.linalg.norm(target - rx.get(combo.input, (0.0, 0.0, 0.0))))
traces.append(TraceData(
combo=combo, frequency_hz=self._frequency_hz,
s11=np.asarray(0.5 / max(1.0, r_tx * r_tx) * np.exp(self._phase_per_m * 2 * r_tx), dtype=np.complex64),
s21=np.asarray(0.5 / max(1.0, r_tx * r_rx) * np.exp(self._phase_per_m * (r_tx + r_rx)), dtype=np.complex64),
capture_start_ns=capture_start_ns,
))
capture_end_ns = time.monotonic_ns()
for trace in traces:
trace.capture_end_ns = capture_end_ns
self._last_capture_ns = capture_start_ns
return SweepCollection(
collection_id=collection_id, monotonic_ns=capture_end_ns, traces=traces,
capture_start_ns=capture_start_ns, capture_end_ns=capture_end_ns,
)