Refactor radar system
This commit is contained in:
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user