Rework radar system
This commit is contained in:
@@ -17,7 +17,7 @@ from python_app.hardware_full.librevna_multi_device_driver.models import (
|
||||
normalize_switch_substage_count,
|
||||
)
|
||||
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
||||
from python_app.models.run_config_model import RadarSweepModel, RunConfigModel
|
||||
from python_app.models.run_config_schema import RadarSweepModel, RunConfigModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from python_app.hardware_full.librevna_multi_device_driver.controller import MultiDeviceVnaController
|
||||
@@ -45,34 +45,6 @@ _MAX_RECOVERY_WALL_SECONDS: float = 10.0
|
||||
# previously caused the periodic freeze.
|
||||
_MAX_TRANSIENT_COLLECTION_RETRIES: int = 2
|
||||
|
||||
def _build_s_parameter_layout(
|
||||
output_count: int, slave_count: int
|
||||
) -> tuple[dict[int, tuple[str, ...]], dict[int, str]]:
|
||||
"""Return the (forward, reflection) trace names for each output position.
|
||||
|
||||
Mirrors the naming in ``cycle_collection``: outputs are virtual ports 1..N and
|
||||
the slaves' receivers are numbered directly above them, so a 2-output setup
|
||||
keeps the historical ``s31``/``s41``/``s51``/``s61`` layout while a 4-output one
|
||||
(per-point stimulus switching) shifts the receivers to ports 5..8 to stay
|
||||
collision-free.
|
||||
"""
|
||||
receiver_ports = [
|
||||
output_count + 2 * slave_index + port_offset
|
||||
for slave_index in range(slave_count)
|
||||
for port_offset in (1, 2)
|
||||
]
|
||||
forward_by_output = {
|
||||
output_index: tuple(
|
||||
f"s{receiver_port}{output_index + 1}" for receiver_port in receiver_ports
|
||||
)
|
||||
for output_index in range(output_count)
|
||||
}
|
||||
reflection_by_output = {
|
||||
output_index: f"s{output_index + 1}{output_index + 1}"
|
||||
for output_index in range(output_count)
|
||||
}
|
||||
return forward_by_output, reflection_by_output
|
||||
|
||||
@dataclass(slots=True)
|
||||
class MultiDeviceLibreVnaService:
|
||||
"""Acquire a virtual 2x4 switch matrix from synchronized LibreVNA devices."""
|
||||
@@ -81,7 +53,6 @@ class MultiDeviceLibreVnaService:
|
||||
slave_serials: list[str]
|
||||
force_external_reference: bool = True
|
||||
recovery_attempts: int = 3
|
||||
backend_mode: str = "auto"
|
||||
# Stimulus switch positions stepped through WITHIN one frequency point. 1 keeps the
|
||||
# stock 2-output matrix; 2 doubles the outputs (4) without a second sweep, at the
|
||||
# cost of requiring the modified master bitstream and the trigger-loop controller.
|
||||
@@ -97,10 +68,6 @@ class MultiDeviceLibreVnaService:
|
||||
raise ValueError("LibreVNA multi-device mode requires exactly two slave serial numbers")
|
||||
self.recovery_attempts = max(0, int(self.recovery_attempts))
|
||||
|
||||
mode = self.backend_mode.strip().lower()
|
||||
if mode not in {"auto", "native"}:
|
||||
raise ValueError(f"Unsupported multi-device backend mode: {self.backend_mode}")
|
||||
self.backend_mode = mode
|
||||
self.switch_substage_count = normalize_switch_substage_count(self.switch_substage_count)
|
||||
|
||||
@property
|
||||
@@ -126,7 +93,7 @@ class MultiDeviceLibreVnaService:
|
||||
self.master_serial, self.slave_serials,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Multi-device open failed (backend_mode=%s): %s", self.backend_mode, exc)
|
||||
logger.warning("Multi-device open failed: %s", exc)
|
||||
raise
|
||||
|
||||
def close(self) -> None:
|
||||
@@ -234,6 +201,7 @@ class MultiDeviceLibreVnaService:
|
||||
self,
|
||||
collection_id: int = 1,
|
||||
*,
|
||||
combo: ComboKey | None = None,
|
||||
stop_event: threading.Event | None = None,
|
||||
) -> SweepCollection:
|
||||
"""Acquire one complete virtual 2x4 matrix in canonical combo order.
|
||||
@@ -244,17 +212,11 @@ class MultiDeviceLibreVnaService:
|
||||
"""
|
||||
if self._sweep_configuration is None:
|
||||
raise RuntimeError("Multi-device service is not configured")
|
||||
capture_start_ns = time.monotonic_ns()
|
||||
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
|
||||
return self._acquire_native_collection_with_recovery(collection_id, stop_event=stop_event)
|
||||
|
||||
def _acquire_native_collection_with_recovery(
|
||||
self,
|
||||
collection_id: int,
|
||||
capture_start_ns: int,
|
||||
*,
|
||||
stop_event: threading.Event | None = None,
|
||||
) -> SweepCollection:
|
||||
@@ -265,7 +227,7 @@ class MultiDeviceLibreVnaService:
|
||||
for attempt_index in range(self.recovery_attempts + 1):
|
||||
try:
|
||||
return self._acquire_native_collection_with_transient_retries(
|
||||
collection_id, capture_start_ns, stop_event=stop_event
|
||||
collection_id, stop_event=stop_event
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
last_error = exc
|
||||
@@ -311,7 +273,6 @@ class MultiDeviceLibreVnaService:
|
||||
def _acquire_native_collection_with_transient_retries(
|
||||
self,
|
||||
collection_id: int,
|
||||
capture_start_ns: int,
|
||||
*,
|
||||
stop_event: threading.Event | None = None,
|
||||
) -> SweepCollection:
|
||||
@@ -328,7 +289,7 @@ class MultiDeviceLibreVnaService:
|
||||
transient_error: TransientCollectionError | None = None
|
||||
for transient_attempt in range(_MAX_TRANSIENT_COLLECTION_RETRIES + 1):
|
||||
try:
|
||||
return self._acquire_native_collection(collection_id, capture_start_ns)
|
||||
return self._acquire_native_collection(collection_id)
|
||||
except TransientCollectionError as exc:
|
||||
transient_error = exc
|
||||
if transient_attempt >= _MAX_TRANSIENT_COLLECTION_RETRIES:
|
||||
@@ -345,62 +306,32 @@ class MultiDeviceLibreVnaService:
|
||||
assert transient_error is not None # loop either returns or raises
|
||||
raise transient_error
|
||||
|
||||
def _acquire_native_collection(self, collection_id: int, capture_start_ns: int) -> SweepCollection:
|
||||
def _acquire_native_collection(self, collection_id: int) -> SweepCollection:
|
||||
if self._controller is None:
|
||||
raise RuntimeError("Multi-device controller is not open")
|
||||
assert self._sweep_configuration is not None
|
||||
|
||||
self._controller.configure_continuous_sweep(self._sweep_configuration)
|
||||
# Bound the sweep itself rather than reusing `capture_start_ns`: the latter
|
||||
# is taken before any retry/recovery, so it would overstate how long the
|
||||
# traces below took to measure.
|
||||
sweep_start_ns = time.monotonic_ns()
|
||||
result = self._controller.collect_running_sweep_cycles(
|
||||
1,
|
||||
result = self._controller.collect_running_sweep_cycle(
|
||||
datapoint_timeout_seconds=LIBREVNA_NATIVE_SWEEP_TIMEOUT_SECONDS,
|
||||
)
|
||||
sweep_end_ns = time.monotonic_ns()
|
||||
normalized_s_parameters = {
|
||||
str(name).lower(): np.asarray(values, dtype=np.complex64)
|
||||
for name, values in result.s_parameters.items()
|
||||
}
|
||||
frequencies = np.asarray(result.frequencies_hz, dtype=np.float32)
|
||||
|
||||
forward_by_output, reflection_by_output = _build_s_parameter_layout(
|
||||
self.output_position_count, len(self.slave_serials)
|
||||
)
|
||||
traces: list[TraceData] = []
|
||||
for output_pos in range(self.output_position_count):
|
||||
reflection = self._required_s_parameter(
|
||||
normalized_s_parameters,
|
||||
reflection_by_output[output_pos],
|
||||
traces = [
|
||||
TraceData(
|
||||
combo=ComboKey(input_pos, output_pos),
|
||||
frequency_hz=frequencies,
|
||||
s21=np.asarray(result.s_parameters[input_pos, output_pos], dtype=np.complex64),
|
||||
capture_start_ns=result.capture_start_ns,
|
||||
capture_end_ns=result.capture_end_ns,
|
||||
)
|
||||
|
||||
for input_pos, s_parameter_name in enumerate(forward_by_output[output_pos]):
|
||||
traces.append(
|
||||
TraceData(
|
||||
combo=ComboKey(input=input_pos, output=output_pos),
|
||||
frequency_hz=frequencies,
|
||||
s11=reflection,
|
||||
s21=self._required_s_parameter(normalized_s_parameters, s_parameter_name),
|
||||
# Every combo comes out of the same synchronized cycle, so
|
||||
# they all share one window — no combo was measured earlier
|
||||
# or later than another here.
|
||||
capture_start_ns=sweep_start_ns,
|
||||
capture_end_ns=sweep_end_ns,
|
||||
)
|
||||
)
|
||||
for output_pos in range(self.output_position_count)
|
||||
for input_pos in range(2 * len(self.slave_serials))
|
||||
]
|
||||
|
||||
return SweepCollection(
|
||||
collection_id=int(collection_id),
|
||||
monotonic_ns=time.monotonic_ns(),
|
||||
traces=traces,
|
||||
capture_start_ns=capture_start_ns,
|
||||
capture_start_ns=result.capture_start_ns,
|
||||
capture_end_ns=result.capture_end_ns,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _required_s_parameter(s_parameters: dict[str, np.ndarray], name: str) -> np.ndarray:
|
||||
values = s_parameters.get(name)
|
||||
if values is None:
|
||||
raise RuntimeError(f"Multi-device sweep is missing required {name.upper()} trace")
|
||||
return values
|
||||
|
||||
Reference in New Issue
Block a user