Files
radar_system/python_app/hardware_full/point_target_mock_service.py
T
2026-10-06 12:32:26 +03:00

114 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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_schema 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 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 prepare_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={"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, *, combo: ComboKey | None = None) -> SweepCollection:
if combo is not None:
self.prepare_combo(combo)
return self._capture(self._combos if combo is None else (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,
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,
)