Refactor radar system
This commit is contained in:
@@ -6,11 +6,12 @@ from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
import time
|
||||
from typing import cast
|
||||
|
||||
import numpy as np
|
||||
|
||||
from python_app.hardware_full.matrix_radar_service import MatrixRadarService, create_matrix_radar_service
|
||||
from python_app.hardware_full.single_radar_service import create_single_radar_service
|
||||
from python_app.hardware_full.single_radar_service import SingleRadarService, create_single_radar_service
|
||||
from python_app.hardware_full.switch_service import SwitchService
|
||||
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
||||
from python_app.models.run_config_model import ComboModel, RunConfigModel
|
||||
@@ -22,6 +23,13 @@ MATRIX_RADAR_MANUAL_CAPTURE_KINDS = frozenset({"s21_calibration", "s11_open", "s
|
||||
DEFAULT_CALIBRATION_MEDIAN_SWEEP_COUNT = 5
|
||||
|
||||
|
||||
def trace_capture_window(traces: list[TraceData]) -> tuple[int, int]:
|
||||
"""Return the total measured window, retaining zero when timing was not reported."""
|
||||
starts = [int(trace.capture_start_ns) for trace in traces if int(trace.capture_start_ns) > 0]
|
||||
ends = [int(trace.capture_end_ns) for trace in traces if int(trace.capture_end_ns) > 0]
|
||||
return min(starts, default=0), max(ends, default=0)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SequentialCaptureState:
|
||||
"""Snapshot of sequential capture progress for the GUI/controller."""
|
||||
@@ -81,8 +89,9 @@ class SequentialCaptureSession:
|
||||
self._median_sweep_count,
|
||||
)
|
||||
|
||||
self._radar: MatrixRadarService | SingleRadarService
|
||||
if self._is_matrix_radar:
|
||||
self._radar: MatrixRadarService = create_matrix_radar_service(config)
|
||||
self._radar = create_matrix_radar_service(config)
|
||||
self._input_switch = None
|
||||
self._output_switch = None
|
||||
else:
|
||||
@@ -121,14 +130,10 @@ class SequentialCaptureSession:
|
||||
def close(self) -> None:
|
||||
"""Close all opened hardware resources."""
|
||||
was_open = self._opened
|
||||
with suppress(Exception):
|
||||
if self._output_switch is not None:
|
||||
self._output_switch.close()
|
||||
with suppress(Exception):
|
||||
if self._input_switch is not None:
|
||||
self._input_switch.close()
|
||||
with suppress(Exception):
|
||||
self._radar.close()
|
||||
for resource in (self._output_switch, self._input_switch, self._radar):
|
||||
if resource is not None:
|
||||
with suppress(Exception):
|
||||
resource.close()
|
||||
self._opened = False
|
||||
if was_open:
|
||||
logger.info("Sequential capture session closed (kind=%s set=%s)", self._kind, self._set_name)
|
||||
@@ -156,12 +161,13 @@ class SequentialCaptureSession:
|
||||
raise RuntimeError("Capture session is already complete")
|
||||
|
||||
if self._is_matrix_radar:
|
||||
radar = cast(MatrixRadarService, self._radar)
|
||||
if self._manual_matrix_radar_capture:
|
||||
# Only this combo is kept, so acquire the smallest collection that
|
||||
# contains it instead of the full (switch-widened) matrix.
|
||||
per_sweep_traces = [
|
||||
select_trace_for_combo(
|
||||
acquire_matrix_combo_collection(self._radar, combo), combo
|
||||
acquire_matrix_combo_collection(radar, combo), combo
|
||||
)
|
||||
for _ in range(self._median_sweep_count)
|
||||
]
|
||||
@@ -173,7 +179,7 @@ class SequentialCaptureSession:
|
||||
|
||||
collections: list[SweepCollection] = []
|
||||
for _ in range(self._median_sweep_count):
|
||||
collection = self._radar.acquire_collection(collection_id=1)
|
||||
collection = radar.acquire_collection(collection_id=1)
|
||||
if not collection.traces:
|
||||
raise RuntimeError("Matrix radar capture returned no traces")
|
||||
collections.append(collection)
|
||||
@@ -189,32 +195,7 @@ class SequentialCaptureSession:
|
||||
self._input_switch.switch_to(combo.input)
|
||||
if self._config.runtime.settling_ms > 0:
|
||||
time.sleep(self._config.runtime.settling_ms / 1000.0)
|
||||
|
||||
# A free-running streaming radar (Kamil ADC) keeps a sweep captured in the
|
||||
# previous combination buffered, and may have a transition-straddling sweep
|
||||
# in flight. Drop those so this capture holds data from the new switch state
|
||||
# — otherwise the trace is labelled with this combo but carries the previous
|
||||
# one's data (an off-by-one across the sequence). Discrete radars (LibreVNA)
|
||||
# acquire a fresh sweep per call and expose no such method, so skip them.
|
||||
drain_after_switch = getattr(self._radar, "drain_after_switch", None)
|
||||
if callable(drain_after_switch):
|
||||
drain_after_switch()
|
||||
|
||||
sweep_traces: list[TraceData] = []
|
||||
for _ in range(self._median_sweep_count):
|
||||
sweep_start_ns = time.monotonic_ns()
|
||||
sweep = self._radar.acquire()
|
||||
sweep_traces.append(
|
||||
TraceData(
|
||||
combo=ComboKey(input=combo.input, output=combo.output),
|
||||
frequency_hz=np.asarray(sweep.x, dtype=np.float32),
|
||||
s11=np.asarray(sweep.trace("s11"), dtype=np.complex64),
|
||||
s21=np.asarray(sweep.trace("s21"), dtype=np.complex64),
|
||||
capture_start_ns=sweep_start_ns,
|
||||
capture_end_ns=time.monotonic_ns(),
|
||||
)
|
||||
)
|
||||
trace = combine_traces_via_median(sweep_traces)
|
||||
trace = capture_single_radar_combo(cast(SingleRadarService, self._radar), combo, self._median_sweep_count)
|
||||
self._traces.append(trace)
|
||||
self._next_index += 1
|
||||
logger.debug(
|
||||
@@ -279,10 +260,13 @@ class SequentialCaptureSession:
|
||||
if not self.is_complete():
|
||||
raise RuntimeError("Capture session is not complete")
|
||||
|
||||
capture_start_ns, capture_end_ns = trace_capture_window(self._traces)
|
||||
collection = SweepCollection(
|
||||
collection_id=1,
|
||||
monotonic_ns=time.monotonic_ns(),
|
||||
traces=list(self._traces),
|
||||
capture_start_ns=capture_start_ns,
|
||||
capture_end_ns=capture_end_ns,
|
||||
)
|
||||
radar_key = radar_key_from_config(
|
||||
model_name=self._config.radar.model,
|
||||
@@ -311,6 +295,32 @@ class SequentialCaptureSession:
|
||||
return self._combos[self._next_index]
|
||||
|
||||
|
||||
def capture_single_radar_combo(
|
||||
radar: SingleRadarService, combo: ComboModel, median_sweep_count: int,
|
||||
) -> TraceData:
|
||||
"""Capture the selected antenna, discarding buffered data from the previous one."""
|
||||
set_active_combo = getattr(radar, "set_active_combo", None)
|
||||
if callable(set_active_combo):
|
||||
set_active_combo(ComboKey(combo.input, combo.output))
|
||||
# Streaming Kamil sweeps may predate or straddle the switch transition.
|
||||
drain_after_switch = getattr(radar, "drain_after_switch", None)
|
||||
if callable(drain_after_switch):
|
||||
drain_after_switch()
|
||||
traces = []
|
||||
for _ in range(median_sweep_count):
|
||||
sweep_start_ns = time.monotonic_ns()
|
||||
sweep = radar.acquire()
|
||||
traces.append(TraceData(
|
||||
combo=ComboKey(combo.input, combo.output),
|
||||
frequency_hz=np.asarray(sweep.x, dtype=np.float32),
|
||||
s11=np.asarray(sweep.trace("s11"), dtype=np.complex64),
|
||||
s21=np.asarray(sweep.trace("s21"), dtype=np.complex64),
|
||||
capture_start_ns=sweep.capture_start_ns or sweep_start_ns,
|
||||
capture_end_ns=sweep.capture_end_ns or time.monotonic_ns(),
|
||||
))
|
||||
return combine_traces_via_median(traces)
|
||||
|
||||
|
||||
def acquire_matrix_combo_collection(
|
||||
radar: MatrixRadarService,
|
||||
combo: ComboModel,
|
||||
@@ -325,11 +335,11 @@ def acquire_matrix_combo_collection(
|
||||
"""
|
||||
acquire_combo = getattr(radar, "acquire_combo_collection", None)
|
||||
if callable(acquire_combo):
|
||||
collection = acquire_combo(
|
||||
collection = cast(SweepCollection, acquire_combo(
|
||||
input_pos=int(combo.input),
|
||||
output_pos=int(combo.output),
|
||||
collection_id=collection_id,
|
||||
)
|
||||
))
|
||||
else:
|
||||
collection = radar.acquire_collection(collection_id=collection_id)
|
||||
if not collection.traces:
|
||||
@@ -379,6 +389,8 @@ def combine_traces_via_median(traces: list[TraceData]) -> TraceData:
|
||||
f"Median combine point-count mismatch at sweep {index}: "
|
||||
f"{trace.frequency_hz.size} vs {point_count}"
|
||||
)
|
||||
if not np.array_equal(trace.frequency_hz, first.frequency_hz):
|
||||
raise RuntimeError(f"Median combine frequency-axis mismatch at sweep {index}")
|
||||
|
||||
s11_stack = np.stack([np.asarray(t.s11, dtype=np.complex64) for t in traces], axis=0)
|
||||
s21_stack = np.stack([np.asarray(t.s21, dtype=np.complex64) for t in traces], axis=0)
|
||||
@@ -389,15 +401,14 @@ def combine_traces_via_median(traces: list[TraceData]) -> TraceData:
|
||||
np.median(s21_stack.real, axis=0) + 1j * np.median(s21_stack.imag, axis=0)
|
||||
).astype(np.complex64)
|
||||
# The median is built from every input sweep, so its window spans all of them.
|
||||
capture_starts = [int(t.capture_start_ns) for t in traces if int(t.capture_start_ns) > 0]
|
||||
capture_ends = [int(t.capture_end_ns) for t in traces if int(t.capture_end_ns) > 0]
|
||||
capture_start_ns, capture_end_ns = trace_capture_window(traces)
|
||||
return TraceData(
|
||||
combo=ComboKey(input=int(combo.input), output=int(combo.output)),
|
||||
frequency_hz=np.asarray(first.frequency_hz, dtype=np.float32),
|
||||
s11=s11_median,
|
||||
s21=s21_median,
|
||||
capture_start_ns=min(capture_starts) if capture_starts else 0,
|
||||
capture_end_ns=max(capture_ends) if capture_ends else 0,
|
||||
capture_start_ns=capture_start_ns,
|
||||
capture_end_ns=capture_end_ns,
|
||||
)
|
||||
|
||||
|
||||
@@ -409,15 +420,14 @@ def combine_collections_via_median(collections: list[SweepCollection]) -> SweepC
|
||||
return collections[0]
|
||||
|
||||
reference = collections[0]
|
||||
for index, collection in enumerate(collections[1:], start=1):
|
||||
if len(collection.traces) != len(reference.traces):
|
||||
raise RuntimeError(f"Median collections have mismatched trace counts at sweep {index}")
|
||||
expected_combos = [(trace.combo.input, trace.combo.output) for trace in reference.traces]
|
||||
medianed_traces: list[TraceData] = []
|
||||
for combo_index, (input_pos, output_pos) in enumerate(expected_combos):
|
||||
per_sweep_traces: list[TraceData] = []
|
||||
for collection_index, collection in enumerate(collections):
|
||||
if combo_index >= len(collection.traces):
|
||||
raise RuntimeError(
|
||||
f"Median collections have mismatched trace counts at sweep {collection_index}"
|
||||
)
|
||||
trace = collection.traces[combo_index]
|
||||
if (
|
||||
int(trace.combo.input) != int(input_pos)
|
||||
@@ -434,6 +444,6 @@ def combine_collections_via_median(collections: list[SweepCollection]) -> SweepC
|
||||
collection_id=int(reference.collection_id),
|
||||
monotonic_ns=int(reference.monotonic_ns),
|
||||
traces=medianed_traces,
|
||||
capture_start_ns=int(reference.capture_start_ns),
|
||||
capture_end_ns=int(reference.capture_end_ns),
|
||||
capture_start_ns=min((int(item.capture_start_ns) for item in collections if item.capture_start_ns > 0), default=0),
|
||||
capture_end_ns=max((int(item.capture_end_ns) for item in collections if item.capture_end_ns > 0), default=0),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user