added median sweep and fixed multi device issue
This commit is contained in:
@@ -5,13 +5,18 @@ from __future__ import annotations
|
||||
from python_app.models.dataset_model import SweepCollection
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
from python_app.storage.npz_store import NpzStore
|
||||
from python_app.workflows.sequential_capture_workflow import SequentialCaptureSession
|
||||
from python_app.workflows.sequential_capture_workflow import (
|
||||
DEFAULT_CALIBRATION_MEDIAN_SWEEP_COUNT,
|
||||
SequentialCaptureSession,
|
||||
)
|
||||
|
||||
|
||||
def capture_calibration_set(
|
||||
config: RunConfigModel,
|
||||
set_name: str,
|
||||
store: NpzStore,
|
||||
*,
|
||||
median_sweep_count: int = DEFAULT_CALIBRATION_MEDIAN_SWEEP_COUNT,
|
||||
) -> tuple[str, SweepCollection]:
|
||||
"""Capture all switch combinations and persist them as calibration set."""
|
||||
if config.is_multi_device:
|
||||
@@ -21,7 +26,12 @@ def capture_calibration_set(
|
||||
"and captured explicitly."
|
||||
)
|
||||
|
||||
session = SequentialCaptureSession(config=config, kind="s21_calibration", set_name=set_name)
|
||||
session = SequentialCaptureSession(
|
||||
config=config,
|
||||
kind="s21_calibration",
|
||||
set_name=set_name,
|
||||
median_sweep_count=median_sweep_count,
|
||||
)
|
||||
try:
|
||||
session.open()
|
||||
while not session.is_complete():
|
||||
|
||||
@@ -62,7 +62,7 @@ def _neutral_collection(
|
||||
point_count = int(frequency_hz.size)
|
||||
traces.append(
|
||||
TraceData(
|
||||
combo=ComboKey(input_pos=int(combo.input), output_pos=int(combo.output)),
|
||||
combo=ComboKey(input=int(combo.input), output=int(combo.output)),
|
||||
frequency_hz=frequency_hz.copy(),
|
||||
s11=np.zeros(point_count, dtype=np.complex64),
|
||||
s21=np.full(point_count, s21_value, dtype=np.complex64),
|
||||
|
||||
@@ -18,6 +18,8 @@ from python_app.workflows.radar_config_variants import RadarConfigVariant
|
||||
from python_app.workflows.sequential_capture_workflow import (
|
||||
MULTI_DEVICE_MANUAL_CAPTURE_KINDS,
|
||||
SequentialCaptureState,
|
||||
combine_collections_via_median,
|
||||
combine_traces_via_median,
|
||||
select_trace_for_combo,
|
||||
)
|
||||
|
||||
@@ -55,6 +57,7 @@ class MultiRadarSequentialCaptureSession:
|
||||
kind: str,
|
||||
set_name: str,
|
||||
radar_variants: list[RadarConfigVariant],
|
||||
median_sweep_count: int = 1,
|
||||
) -> None:
|
||||
"""Create capture session for one preprocess asset set and multiple radar variants."""
|
||||
if kind not in {"s21_calibration", "s21_reference", "s11_open", "s11_short", "s11_load", "s11_reference"}:
|
||||
@@ -63,11 +66,14 @@ class MultiRadarSequentialCaptureSession:
|
||||
raise RuntimeError("Set name is required")
|
||||
if not radar_variants:
|
||||
raise RuntimeError("At least one radar variant is required")
|
||||
if int(median_sweep_count) < 1:
|
||||
raise RuntimeError("median_sweep_count must be >= 1")
|
||||
|
||||
self._base_config = base_config
|
||||
self._kind = kind
|
||||
self._set_name = set_name
|
||||
self._radar_variants = list(radar_variants)
|
||||
self._median_sweep_count = int(median_sweep_count)
|
||||
self._is_multi_device = base_config.is_multi_device
|
||||
self._manual_multi_device_capture = self._is_multi_device and kind in MULTI_DEVICE_MANUAL_CAPTURE_KINDS
|
||||
self._combos = (
|
||||
@@ -196,16 +202,23 @@ class MultiRadarSequentialCaptureSession:
|
||||
self._radar.configure(variant.config.radar.sweep)
|
||||
if self._base_config.runtime.settling_ms > 0:
|
||||
time.sleep(self._base_config.runtime.settling_ms / 1000.0)
|
||||
collection = self._radar.acquire_collection(collection_id=1)
|
||||
if not collection.traces:
|
||||
raise RuntimeError(f"Multi-device variant {variant.display_name} returned no traces")
|
||||
collections: list[SweepCollection] = []
|
||||
for _ in range(self._median_sweep_count):
|
||||
collection = self._radar.acquire_collection(collection_id=1)
|
||||
if not collection.traces:
|
||||
raise RuntimeError(
|
||||
f"Multi-device variant {variant.display_name} returned no traces"
|
||||
)
|
||||
collections.append(collection)
|
||||
if self._manual_multi_device_capture:
|
||||
trace = select_trace_for_combo(collection, combo)
|
||||
per_sweep_traces = [select_trace_for_combo(collection, combo) for collection in collections]
|
||||
trace = combine_traces_via_median(per_sweep_traces)
|
||||
pending_traces_by_radar_key[variant.radar_key] = [trace]
|
||||
display_traces.append(trace)
|
||||
else:
|
||||
pending_traces_by_radar_key[variant.radar_key] = list(collection.traces)
|
||||
display_traces.append(collection.traces[-1])
|
||||
combined_collection = combine_collections_via_median(collections)
|
||||
pending_traces_by_radar_key[variant.radar_key] = list(combined_collection.traces)
|
||||
display_traces.append(combined_collection.traces[-1])
|
||||
variant_labels.append(variant.display_name)
|
||||
else:
|
||||
assert self._input_switch is not None
|
||||
@@ -219,13 +232,18 @@ class MultiRadarSequentialCaptureSession:
|
||||
self._radar.configure(variant.config.radar.sweep)
|
||||
if self._base_config.runtime.settling_ms > 0:
|
||||
time.sleep(self._base_config.runtime.settling_ms / 1000.0)
|
||||
sweep = self._radar.acquire()
|
||||
trace = TraceData(
|
||||
combo=ComboKey(input_pos=combo.input, output_pos=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),
|
||||
)
|
||||
sweep_traces: list[TraceData] = []
|
||||
for _ in range(self._median_sweep_count):
|
||||
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),
|
||||
)
|
||||
)
|
||||
trace = combine_traces_via_median(sweep_traces)
|
||||
pending_traces_by_radar_key[variant.radar_key] = [trace]
|
||||
display_traces.append(trace)
|
||||
variant_labels.append(variant.display_name)
|
||||
|
||||
@@ -5,16 +5,26 @@ from __future__ import annotations
|
||||
from python_app.models.dataset_model import SweepCollection
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
from python_app.storage.npz_store import NpzStore
|
||||
from python_app.workflows.sequential_capture_workflow import SequentialCaptureSession
|
||||
from python_app.workflows.sequential_capture_workflow import (
|
||||
DEFAULT_CALIBRATION_MEDIAN_SWEEP_COUNT,
|
||||
SequentialCaptureSession,
|
||||
)
|
||||
|
||||
|
||||
def capture_reference_set(
|
||||
config: RunConfigModel,
|
||||
set_name: str,
|
||||
store: NpzStore,
|
||||
*,
|
||||
median_sweep_count: int = DEFAULT_CALIBRATION_MEDIAN_SWEEP_COUNT,
|
||||
) -> tuple[str, SweepCollection]:
|
||||
"""Capture all switch combinations and persist them as reference set."""
|
||||
session = SequentialCaptureSession(config=config, kind="s21_reference", set_name=set_name)
|
||||
session = SequentialCaptureSession(
|
||||
config=config,
|
||||
kind="s21_reference",
|
||||
set_name=set_name,
|
||||
median_sweep_count=median_sweep_count,
|
||||
)
|
||||
try:
|
||||
session.open()
|
||||
while not session.is_complete():
|
||||
|
||||
@@ -16,6 +16,7 @@ from python_app.models.run_config_model import ComboModel, RunConfigModel
|
||||
from python_app.storage.npz_store import NpzStore, radar_key_from_config
|
||||
|
||||
MULTI_DEVICE_MANUAL_CAPTURE_KINDS = frozenset({"s21_calibration", "s11_open", "s11_short", "s11_load"})
|
||||
DEFAULT_CALIBRATION_MEDIAN_SWEEP_COUNT = 5
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -36,16 +37,26 @@ class SequentialCaptureState:
|
||||
class SequentialCaptureSession:
|
||||
"""Manage hardware and switch stepping for full combo capture sequence."""
|
||||
|
||||
def __init__(self, config: RunConfigModel, kind: str, set_name: str) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
config: RunConfigModel,
|
||||
kind: str,
|
||||
set_name: str,
|
||||
*,
|
||||
median_sweep_count: int = 1,
|
||||
) -> None:
|
||||
"""Create capture session for one preprocess asset set."""
|
||||
if kind not in {"s21_calibration", "s21_reference", "s11_open", "s11_short", "s11_load", "s11_reference"}:
|
||||
raise RuntimeError(f"Unsupported capture kind: {kind}")
|
||||
if not set_name:
|
||||
raise RuntimeError("Set name is required")
|
||||
if int(median_sweep_count) < 1:
|
||||
raise RuntimeError("median_sweep_count must be >= 1")
|
||||
|
||||
self._config = config
|
||||
self._kind = kind
|
||||
self._set_name = set_name
|
||||
self._median_sweep_count = int(median_sweep_count)
|
||||
self._is_multi_device = config.is_multi_device
|
||||
self._manual_multi_device_capture = self._is_multi_device and kind in MULTI_DEVICE_MANUAL_CAPTURE_KINDS
|
||||
self._combos = (
|
||||
@@ -154,18 +165,23 @@ class SequentialCaptureSession:
|
||||
raise RuntimeError("Capture session is already complete")
|
||||
|
||||
if self._is_multi_device:
|
||||
collection = self._radar.acquire_collection(collection_id=1)
|
||||
if not collection.traces:
|
||||
raise RuntimeError("Multi-device capture returned no traces")
|
||||
collections: list[SweepCollection] = []
|
||||
for _ in range(self._median_sweep_count):
|
||||
collection = self._radar.acquire_collection(collection_id=1)
|
||||
if not collection.traces:
|
||||
raise RuntimeError("Multi-device capture returned no traces")
|
||||
collections.append(collection)
|
||||
if self._manual_multi_device_capture:
|
||||
trace = select_trace_for_combo(collection, combo)
|
||||
per_sweep_traces = [select_trace_for_combo(collection, combo) for collection in collections]
|
||||
trace = combine_traces_via_median(per_sweep_traces)
|
||||
self._traces.append(trace)
|
||||
self._next_index += 1
|
||||
return trace
|
||||
|
||||
self._traces.extend(collection.traces)
|
||||
combined_collection = combine_collections_via_median(collections)
|
||||
self._traces.extend(combined_collection.traces)
|
||||
self._next_index = len(self._combos)
|
||||
return collection.traces[-1]
|
||||
return combined_collection.traces[-1]
|
||||
|
||||
assert self._input_switch is not None
|
||||
assert self._output_switch is not None
|
||||
@@ -174,13 +190,18 @@ class SequentialCaptureSession:
|
||||
if self._config.runtime.settling_ms > 0:
|
||||
time.sleep(self._config.runtime.settling_ms / 1000.0)
|
||||
|
||||
sweep = self._radar.acquire()
|
||||
trace = TraceData(
|
||||
combo=ComboKey(input_pos=combo.input, output_pos=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),
|
||||
)
|
||||
sweep_traces: list[TraceData] = []
|
||||
for _ in range(self._median_sweep_count):
|
||||
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),
|
||||
)
|
||||
)
|
||||
trace = combine_traces_via_median(sweep_traces)
|
||||
self._traces.append(trace)
|
||||
self._next_index += 1
|
||||
return trace
|
||||
@@ -203,8 +224,8 @@ class SequentialCaptureSession:
|
||||
expected_combo = self._combos[self._next_index - 1]
|
||||
removed_trace = self._traces[-1]
|
||||
if (
|
||||
int(removed_trace.combo.input_pos) != int(expected_combo.input)
|
||||
or int(removed_trace.combo.output_pos) != int(expected_combo.output)
|
||||
int(removed_trace.combo.input) != int(expected_combo.input)
|
||||
or int(removed_trace.combo.output) != int(expected_combo.output)
|
||||
):
|
||||
raise RuntimeError("Capture session state is inconsistent; last trace does not match rewind combo")
|
||||
self._next_index -= 1
|
||||
@@ -259,8 +280,94 @@ def select_trace_for_combo(collection: SweepCollection, combo: ComboModel) -> Tr
|
||||
"""Return the trace matching a virtual combo from a full multi-device capture."""
|
||||
for trace in collection.traces:
|
||||
if (
|
||||
int(trace.combo.input_pos) == int(combo.input)
|
||||
and int(trace.combo.output_pos) == int(combo.output)
|
||||
int(trace.combo.input) == int(combo.input)
|
||||
and int(trace.combo.output) == int(combo.output)
|
||||
):
|
||||
return trace
|
||||
raise RuntimeError(f"Multi-device capture is missing trace for input={combo.input}, output={combo.output}")
|
||||
|
||||
|
||||
def combine_traces_via_median(traces: list[TraceData]) -> TraceData:
|
||||
"""Return one trace whose S11/S21 are the per-point median of the inputs.
|
||||
|
||||
A single-element input is returned unchanged. With multiple inputs, the real
|
||||
and imaginary parts of each complex sample are medianed independently so a
|
||||
single bad sweep (e.g. an outlier with random phase) is rejected without
|
||||
corrupting the saved calibration trace.
|
||||
"""
|
||||
if not traces:
|
||||
raise RuntimeError("Cannot combine empty sweep list")
|
||||
if len(traces) == 1:
|
||||
return traces[0]
|
||||
|
||||
first = traces[0]
|
||||
combo = first.combo
|
||||
point_count = first.frequency_hz.size
|
||||
for index, trace in enumerate(traces[1:], start=1):
|
||||
if (
|
||||
int(trace.combo.input) != int(combo.input)
|
||||
or int(trace.combo.output) != int(combo.output)
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"Median combine combo mismatch at sweep {index}: "
|
||||
f"({trace.combo.input},{trace.combo.output}) "
|
||||
f"vs ({combo.input},{combo.output})"
|
||||
)
|
||||
if trace.frequency_hz.size != point_count:
|
||||
raise RuntimeError(
|
||||
f"Median combine point-count mismatch at sweep {index}: "
|
||||
f"{trace.frequency_hz.size} vs {point_count}"
|
||||
)
|
||||
|
||||
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)
|
||||
s11_median = (
|
||||
np.median(s11_stack.real, axis=0) + 1j * np.median(s11_stack.imag, axis=0)
|
||||
).astype(np.complex64)
|
||||
s21_median = (
|
||||
np.median(s21_stack.real, axis=0) + 1j * np.median(s21_stack.imag, axis=0)
|
||||
).astype(np.complex64)
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
def combine_collections_via_median(collections: list[SweepCollection]) -> SweepCollection:
|
||||
"""Combine multi-device matrix captures into one collection with per-combo medians."""
|
||||
if not collections:
|
||||
raise RuntimeError("Cannot combine empty collection list")
|
||||
if len(collections) == 1:
|
||||
return collections[0]
|
||||
|
||||
reference = collections[0]
|
||||
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)
|
||||
or int(trace.combo.output) != int(output_pos)
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"Median collections trace order mismatch at sweep {collection_index}, "
|
||||
f"combo index {combo_index}"
|
||||
)
|
||||
per_sweep_traces.append(trace)
|
||||
medianed_traces.append(combine_traces_via_median(per_sweep_traces))
|
||||
|
||||
return SweepCollection(
|
||||
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),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user