Refactor radar system
This commit is contained in:
@@ -1,46 +0,0 @@
|
||||
"""One-shot workflow for capturing a full calibration set."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
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 (
|
||||
DEFAULT_CALIBRATION_MEDIAN_SWEEP_COUNT,
|
||||
SequentialCaptureSession,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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."""
|
||||
logger.info("Starting one-shot calibration capture: set=%s", set_name)
|
||||
if config.is_matrix_radar:
|
||||
raise RuntimeError(
|
||||
"Matrix-radar S21 through calibration is not supported by this one-shot full-set helper. "
|
||||
"Use the sequential preprocess capture flow so each virtual combo can be connected through "
|
||||
"and captured explicitly."
|
||||
)
|
||||
|
||||
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():
|
||||
session.capture_current_combo()
|
||||
return session.finalize(store)
|
||||
finally:
|
||||
session.close()
|
||||
@@ -6,9 +6,8 @@ reference is subtracted, so applying both leaves the measured S21 untouched. Tha
|
||||
lets an operator start the pipeline before any real calibration exists, which the
|
||||
required-asset check in `_start_run` would otherwise refuse.
|
||||
|
||||
Supported models: Kamil ADC (axis from the ADC processing grid) and every
|
||||
VNA-style model, including synchronized multi-device LibreVNA (axis from the
|
||||
configured linear sweep grid).
|
||||
The shortcut is enabled for verified axes: Kamil ADC's processing grid,
|
||||
synchronized multi-device LibreVNA, and the physical single LibreVNA mock.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -29,26 +28,14 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def supports_neutral_preprocess_sets(config: RunConfigModel) -> bool:
|
||||
"""Return whether neutral S21 sets can be generated for this radar model.
|
||||
|
||||
Enabled for the Kamil ADC and for synchronized multi-device LibreVNA, the two
|
||||
models whose emitted frequency axis is fully derivable from the config alone.
|
||||
Other models still work through `build_neutral_s21_sets`, but are kept out of the
|
||||
UI shortcut until their axis has been verified against real hardware.
|
||||
"""
|
||||
return bool(config.is_kamil_adc or config.is_multi_device)
|
||||
"""Enable the shortcut only when the emitted frequency grid is verified."""
|
||||
return bool(config.is_kamil_adc or config.is_multi_device or (
|
||||
config.radar.model == RunConfigModel.LIBREVNA_MODEL and config.radar.driver_mode == "mock"
|
||||
))
|
||||
|
||||
|
||||
def neutral_frequency_grid_hz(config: RunConfigModel) -> np.ndarray:
|
||||
"""Return the exact per-trace frequency axis the configured radar emits.
|
||||
|
||||
Neutral sets must line up sample-for-sample with live sweeps, so the axis comes
|
||||
from the same source the acquisition path uses: the ADC processing grid for Kamil
|
||||
ADC, and the configured linear sweep grid for every VNA-style model (LibreVNA
|
||||
single and multi-device, SN9000, Compact-M). The C++ preprocessor re-checks this
|
||||
axis against the measured one within a tolerance, so a mismatch fails loudly
|
||||
instead of silently corrupting the correction.
|
||||
"""
|
||||
"""Derive the neutral axis; the preprocessor verifies it against measured data."""
|
||||
if config.is_kamil_adc:
|
||||
# Single source of truth for the axis: the same grid the processor emits.
|
||||
processor = KamilAdcSweepProcessor(
|
||||
@@ -59,8 +46,6 @@ def neutral_frequency_grid_hz(config: RunConfigModel) -> np.ndarray:
|
||||
points = int(config.radar.sweep.points)
|
||||
if points < 1:
|
||||
raise ValueError("Neutral sets require radar.sweep.points >= 1")
|
||||
if points == 1:
|
||||
return np.array([float(config.radar.sweep.start_hz)], dtype=np.float32)
|
||||
# Mirrors both acquisition paths: the native collector seeds this same linspace
|
||||
# and the mock backend generates it outright.
|
||||
return np.linspace(
|
||||
@@ -110,19 +95,6 @@ def build_neutral_s21_sets(
|
||||
return calibration, reference
|
||||
|
||||
|
||||
def build_kamil_adc_neutral_s21_sets(
|
||||
config: RunConfigModel,
|
||||
) -> tuple[SweepCollection, SweepCollection]:
|
||||
"""Build neutral S21 sets, rejecting anything but the Kamil ADC radar.
|
||||
|
||||
Kept as the model-checked entry point for the ADC path; new callers that must
|
||||
work for several radar models should use `build_neutral_s21_sets` instead.
|
||||
"""
|
||||
if not config.is_kamil_adc:
|
||||
raise ValueError("Neutral Kamil ADC sets require radar.model='kamil_adc'")
|
||||
return build_neutral_s21_sets(config)
|
||||
|
||||
|
||||
def _neutral_collection(
|
||||
*,
|
||||
combos: list[ComboModel],
|
||||
|
||||
@@ -6,13 +6,12 @@ from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from typing import cast
|
||||
|
||||
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.dataset_model import SweepCollection, TraceData
|
||||
from python_app.models.run_config_model import ComboModel, RunConfigModel
|
||||
from python_app.storage.npz_store import NpzStore
|
||||
from python_app.workflows.radar_config_variants import RadarConfigVariant
|
||||
@@ -20,9 +19,11 @@ from python_app.workflows.sequential_capture_workflow import (
|
||||
MATRIX_RADAR_MANUAL_CAPTURE_KINDS,
|
||||
SequentialCaptureState,
|
||||
acquire_matrix_combo_collection,
|
||||
capture_single_radar_combo,
|
||||
combine_collections_via_median,
|
||||
combine_traces_via_median,
|
||||
select_trace_for_combo,
|
||||
trace_capture_window,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -103,8 +104,9 @@ class MultiRadarSequentialCaptureSession:
|
||||
self._is_matrix_radar,
|
||||
)
|
||||
|
||||
self._radar: MatrixRadarService | SingleRadarService
|
||||
if self._is_matrix_radar:
|
||||
self._radar: MatrixRadarService = create_matrix_radar_service(base_config)
|
||||
self._radar = create_matrix_radar_service(base_config)
|
||||
self._input_switch = None
|
||||
self._output_switch = None
|
||||
else:
|
||||
@@ -145,14 +147,10 @@ class MultiRadarSequentialCaptureSession:
|
||||
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("Multi-radar capture session closed (kind=%s set=%s)", self._kind, self._set_name)
|
||||
@@ -189,6 +187,7 @@ class MultiRadarSequentialCaptureSession:
|
||||
variant_labels: list[str] = []
|
||||
|
||||
if self._is_matrix_radar:
|
||||
radar = cast(MatrixRadarService, self._radar)
|
||||
for variant in self._radar_variants:
|
||||
self._radar.configure(variant.config.radar.sweep)
|
||||
if self._base_config.runtime.settling_ms > 0:
|
||||
@@ -198,7 +197,7 @@ class MultiRadarSequentialCaptureSession:
|
||||
# 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)
|
||||
]
|
||||
@@ -208,7 +207,7 @@ class MultiRadarSequentialCaptureSession:
|
||||
else:
|
||||
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(
|
||||
f"Matrix radar variant {variant.display_name} returned no traces"
|
||||
@@ -230,21 +229,9 @@ 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_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,
|
||||
)
|
||||
pending_traces_by_radar_key[variant.radar_key] = [trace]
|
||||
display_traces.append(trace)
|
||||
variant_labels.append(variant.display_name)
|
||||
@@ -343,10 +330,13 @@ class MultiRadarSequentialCaptureSession:
|
||||
saved_sets: list[MultiRadarSavedSet] = []
|
||||
for variant in self._radar_variants:
|
||||
traces = list(self._traces_by_radar_key[variant.radar_key])
|
||||
capture_start_ns, capture_end_ns = trace_capture_window(traces)
|
||||
collection = SweepCollection(
|
||||
collection_id=1,
|
||||
monotonic_ns=monotonic_ns,
|
||||
traces=traces,
|
||||
capture_start_ns=capture_start_ns,
|
||||
capture_end_ns=capture_end_ns,
|
||||
)
|
||||
store.save_set(self._kind, variant.radar_key, self._set_name, collection)
|
||||
saved_sets.append(
|
||||
|
||||
@@ -143,18 +143,9 @@ def _load_radar_config_variant(path: Path, *, base_config: RunConfigModel) -> Ra
|
||||
if not any(key in sweep_payload for key in _RADAR_SWEEP_KEYS):
|
||||
raise ValueError("radar.sweep does not contain any supported sweep keys")
|
||||
|
||||
config = base_config.clone()
|
||||
sweep = config.radar.sweep
|
||||
if "start_hz" in sweep_payload:
|
||||
sweep.start_hz = float(sweep_payload["start_hz"])
|
||||
if "stop_hz" in sweep_payload:
|
||||
sweep.stop_hz = float(sweep_payload["stop_hz"])
|
||||
if "points" in sweep_payload:
|
||||
sweep.points = int(sweep_payload["points"])
|
||||
if "if_bandwidth_hz" in sweep_payload:
|
||||
sweep.if_bandwidth_hz = float(sweep_payload["if_bandwidth_hz"])
|
||||
if "stimulus_power_dbm" in sweep_payload:
|
||||
sweep.power_dbm = float(sweep_payload["stimulus_power_dbm"])
|
||||
config_payload = base_config.to_dict()
|
||||
config_payload["radar"]["sweep"].update(sweep_payload)
|
||||
config = RunConfigModel.from_dict(config_payload)
|
||||
|
||||
radar_key = radar_key_from_config(
|
||||
model_name=config.radar.model,
|
||||
|
||||
@@ -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