added capture time for every sweep
This commit is contained in:
@@ -332,10 +332,15 @@ class MultiDeviceLibreVnaService:
|
||||
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,
|
||||
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()
|
||||
@@ -356,6 +361,11 @@ class MultiDeviceLibreVnaService:
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -368,6 +378,7 @@ class MultiDeviceLibreVnaService:
|
||||
|
||||
def _acquire_mock_collection(self, collection_id: int, capture_start_ns: int) -> SweepCollection:
|
||||
assert self._sweep_configuration is not None
|
||||
mock_sweep_start_ns = time.monotonic_ns()
|
||||
points = int(self._sweep_configuration.points)
|
||||
frequencies = np.linspace(
|
||||
self._sweep_configuration.start_hz,
|
||||
@@ -393,6 +404,8 @@ class MultiDeviceLibreVnaService:
|
||||
frequency_hz=frequencies,
|
||||
s11=s11,
|
||||
s21=s21,
|
||||
capture_start_ns=mock_sweep_start_ns,
|
||||
capture_end_ns=time.monotonic_ns(),
|
||||
)
|
||||
)
|
||||
self._mock_phase += 0.05
|
||||
|
||||
@@ -183,7 +183,11 @@ class Sn9000Service:
|
||||
capture_start_ns = time.monotonic_ns()
|
||||
|
||||
s_parameters = self._query_sweep_s_parameters(points)
|
||||
traces = self._assemble_traces(s_parameters)
|
||||
traces = self._assemble_traces(
|
||||
s_parameters,
|
||||
sweep_start_ns=capture_start_ns,
|
||||
sweep_end_ns=time.monotonic_ns(),
|
||||
)
|
||||
|
||||
return SweepCollection(
|
||||
collection_id=int(collection_id),
|
||||
@@ -256,7 +260,13 @@ class Sn9000Service:
|
||||
def _uses_pyvisa_py_backend(self) -> bool:
|
||||
return self.visa_library == "@py" or self.visa_library.endswith("@py")
|
||||
|
||||
def _assemble_traces(self, s_parameters: dict[str, np.ndarray]) -> list[TraceData]:
|
||||
def _assemble_traces(
|
||||
self,
|
||||
s_parameters: dict[str, np.ndarray],
|
||||
*,
|
||||
sweep_start_ns: int,
|
||||
sweep_end_ns: int,
|
||||
) -> list[TraceData]:
|
||||
frequency_hz = self._require_frequency_axis()
|
||||
traces: list[TraceData] = []
|
||||
for output_position, output_port in enumerate(_OUTPUT_PORT_BY_INDEX):
|
||||
@@ -269,6 +279,10 @@ class Sn9000Service:
|
||||
frequency_hz=frequency_hz,
|
||||
s11=reflection,
|
||||
s21=transmission,
|
||||
# One triggered sweep produces every port pair at once, so
|
||||
# all combos share the sweep's window.
|
||||
capture_start_ns=int(sweep_start_ns),
|
||||
capture_end_ns=int(sweep_end_ns),
|
||||
)
|
||||
)
|
||||
return traces
|
||||
|
||||
@@ -140,7 +140,13 @@ class SwitchedMatrixRadarService:
|
||||
)
|
||||
|
||||
def _acquire_step_traces(self, out_k: int, in_k: int, collection_id: int) -> list[TraceData]:
|
||||
"""Drive both switches to one step, settle, and collect its widened traces."""
|
||||
"""Drive both switches to one step, settle, and collect its widened traces.
|
||||
|
||||
Every returned trace carries the monotonic window of the inner collection
|
||||
that produced it, so a consumer can tell when each combo of a switched
|
||||
matrix was really measured instead of only when the whole cycle began and
|
||||
ended. The switch drive and settling are deliberately outside the window.
|
||||
"""
|
||||
step_start_ns = time.monotonic_ns()
|
||||
if self.output_switch is not None:
|
||||
self.output_switch.switch_to(out_k)
|
||||
@@ -178,6 +184,11 @@ class SwitchedMatrixRadarService:
|
||||
input=in_k * self.inner_input_positions + int(trace.combo.input),
|
||||
output=out_k * self.inner_output_positions + int(trace.combo.output),
|
||||
),
|
||||
# Keep the inner service's own per-trace window when it reports one
|
||||
# (it knows its internal port order better than this step does);
|
||||
# otherwise fall back to the window of this inner collection.
|
||||
capture_start_ns=int(trace.capture_start_ns) or settled_ns,
|
||||
capture_end_ns=int(trace.capture_end_ns) or inner_end_ns,
|
||||
)
|
||||
for trace in sub.traces
|
||||
]
|
||||
|
||||
@@ -32,12 +32,22 @@ class ComboKey:
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TraceData:
|
||||
"""One frequency-domain trace set for a specific switch combination."""
|
||||
"""One frequency-domain trace set for a specific switch combination.
|
||||
|
||||
``capture_start_ns``/``capture_end_ns`` bound the monotonic window in which
|
||||
THIS trace's sweep was measured, excluding the switch drive and settling that
|
||||
preceded it. In switched modes a collection is assembled combo by combo over
|
||||
many milliseconds, so the collection-level window says nothing about when any
|
||||
individual combo was measured — these do. Zero on both means the producer did
|
||||
not report per-trace timing.
|
||||
"""
|
||||
|
||||
combo: ComboKey
|
||||
frequency_hz: np.ndarray
|
||||
s11: np.ndarray
|
||||
s21: np.ndarray
|
||||
capture_start_ns: int = 0
|
||||
capture_end_ns: int = 0
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
||||
@@ -54,12 +54,27 @@ def decode_trace_collection(payload: bytes, expected_magic: int) -> SweepCollect
|
||||
)
|
||||
)
|
||||
|
||||
# Optional trailer, written after the trace blocks by newer producers: the
|
||||
# collection capture window, then a per-trace window table. Both stages are
|
||||
# optional so payloads from an older producer still decode (the timestamps
|
||||
# simply stay zero).
|
||||
capture_start_ns = 0
|
||||
capture_end_ns = 0
|
||||
if cursor.remaining_bytes() == 16:
|
||||
if cursor.remaining_bytes() != 0:
|
||||
if cursor.remaining_bytes() < 16:
|
||||
raise ValueError("Truncated capture window in trace collection")
|
||||
capture_start_ns = cursor.read_u64()
|
||||
capture_end_ns = cursor.read_u64()
|
||||
elif cursor.remaining_bytes() != 0:
|
||||
|
||||
if cursor.remaining_bytes() != 0:
|
||||
trace_time_count = cursor.read_u32()
|
||||
if trace_time_count != len(traces):
|
||||
raise ValueError("Per-trace capture window count does not match trace count")
|
||||
for trace in traces:
|
||||
trace.capture_start_ns = cursor.read_u64()
|
||||
trace.capture_end_ns = cursor.read_u64()
|
||||
|
||||
if cursor.remaining_bytes() != 0:
|
||||
raise ValueError("Unexpected trailing bytes in trace collection")
|
||||
|
||||
return SweepCollection(
|
||||
|
||||
@@ -23,6 +23,9 @@ class TraceRecord:
|
||||
stage_index: int
|
||||
frequency_hz: np.ndarray
|
||||
samples: np.ndarray
|
||||
# End of this trace's own sweep, from the snapshot's per-trace metadata; 0 for a
|
||||
# snapshot recorded before per-trace timing existed.
|
||||
capture_end_ns: int = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -128,6 +131,7 @@ def _load_stage_records(
|
||||
stage_index=_parse_stage_index(collection_dir.name, fallback_idx),
|
||||
frequency_hz=frequency_hz,
|
||||
samples=samples,
|
||||
capture_end_ns=int(trace_meta.get("capture_end_ns", 0)),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -214,7 +218,15 @@ def _build_sweep_history(
|
||||
|
||||
start_freq_hz = float(base.frequency_hz[0])
|
||||
stop_freq_hz = float(base.frequency_hz[-1])
|
||||
timestamp_sec = float(base.monotonic_ns) / 1_000_000_000.0 if base.monotonic_ns > 0 else float(fallback_index)
|
||||
# Prefer this trace's own sweep time: with a switching matrix the combos of
|
||||
# one collection are measured milliseconds apart, so the collection
|
||||
# timestamp misplaces every combo but the last.
|
||||
if base.capture_end_ns > 0:
|
||||
timestamp_sec = float(base.capture_end_ns) / 1_000_000_000.0
|
||||
elif base.monotonic_ns > 0:
|
||||
timestamp_sec = float(base.monotonic_ns) / 1_000_000_000.0
|
||||
else:
|
||||
timestamp_sec = float(fallback_index)
|
||||
|
||||
history.append(
|
||||
{
|
||||
|
||||
@@ -184,12 +184,16 @@ def main() -> int:
|
||||
if collector_driven:
|
||||
# The collector already switched and tagged the sweep; just
|
||||
# read the clean capture for this combination.
|
||||
sweep_start_ns = time.monotonic_ns()
|
||||
sweep = radar.acquire(combo=(combo.input, combo.output))
|
||||
else:
|
||||
output_switch.switch_to(combo.output)
|
||||
input_switch.switch_to(combo.input)
|
||||
if config.runtime.settling_ms > 0:
|
||||
time.sleep(config.runtime.settling_ms / 1000.0)
|
||||
# Stamped after switching and settling so the window covers
|
||||
# the sweep alone, not the dead time before it.
|
||||
sweep_start_ns = time.monotonic_ns()
|
||||
sweep = radar.acquire()
|
||||
traces.append(
|
||||
TraceData(
|
||||
@@ -197,6 +201,8 @@ def main() -> int:
|
||||
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(),
|
||||
)
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 — reconnect forever, never give up
|
||||
|
||||
@@ -22,7 +22,14 @@ def _write_interleaved_complex(buffer: bytearray, values: np.ndarray) -> None:
|
||||
|
||||
|
||||
def serialize_trace_collection(collection: SweepCollection, magic: int) -> bytes:
|
||||
"""Serialize one raw/preprocessed trace collection into ring-compatible binary format."""
|
||||
"""Serialize one raw/preprocessed trace collection into ring-compatible binary format.
|
||||
|
||||
The trailer is appended after the trace blocks so older readers, which stop at
|
||||
the last block, still decode the traces: first the collection capture window,
|
||||
then a per-trace window table (one ``(start_ns, end_ns)`` pair per trace, in
|
||||
trace order). See :func:`python_app.orchestration.shm.decoder.decode_trace_collection`
|
||||
and ``read_trace_collection`` in ``common_cpp/ipc/src/shared_types.cpp``.
|
||||
"""
|
||||
buffer = bytearray()
|
||||
buffer.extend(struct.pack("<IQQI", magic, collection.collection_id, collection.monotonic_ns, len(collection.traces)))
|
||||
|
||||
@@ -48,6 +55,11 @@ def serialize_trace_collection(collection: SweepCollection, magic: int) -> bytes
|
||||
int(collection.capture_end_ns),
|
||||
)
|
||||
)
|
||||
buffer.extend(struct.pack("<I", len(collection.traces)))
|
||||
for trace in collection.traces:
|
||||
buffer.extend(
|
||||
struct.pack("<QQ", int(trace.capture_start_ns), int(trace.capture_end_ns))
|
||||
)
|
||||
return bytes(buffer)
|
||||
|
||||
|
||||
|
||||
@@ -124,6 +124,17 @@ def save_trace_history_binary(stage_dir: Path, history: list[SweepCollection], m
|
||||
"capture_start_ns": int(collection.capture_start_ns),
|
||||
"capture_end_ns": int(collection.capture_end_ns),
|
||||
"trace_count": len(collection.traces),
|
||||
# Also in the .bin trailer; repeated here so per-combo timing is
|
||||
# readable without decoding the binary payload.
|
||||
"traces": [
|
||||
{
|
||||
"input": int(trace.combo.input),
|
||||
"output": int(trace.combo.output),
|
||||
"capture_start_ns": int(trace.capture_start_ns),
|
||||
"capture_end_ns": int(trace.capture_end_ns),
|
||||
}
|
||||
for trace in collection.traces
|
||||
],
|
||||
},
|
||||
indent=2,
|
||||
),
|
||||
@@ -182,6 +193,10 @@ def save_trace_history_numpy(
|
||||
"input": int(trace.combo.input),
|
||||
"output": int(trace.combo.output),
|
||||
"points": int(freq.size),
|
||||
# When each combo was measured, which in a switched matrix is
|
||||
# spread across the collection window rather than aligned with it.
|
||||
"capture_start_ns": int(trace.capture_start_ns),
|
||||
"capture_end_ns": int(trace.capture_end_ns),
|
||||
"freq_file": f"{tag}_freq.npy",
|
||||
"s11_file": f"{tag}_s11.npy",
|
||||
"s21_file": f"{tag}_s21.npy",
|
||||
|
||||
@@ -117,6 +117,8 @@ class NpzStore(StoreApi):
|
||||
{
|
||||
"input": trace.combo.input,
|
||||
"output": trace.combo.output,
|
||||
"capture_start_ns": int(trace.capture_start_ns),
|
||||
"capture_end_ns": int(trace.capture_end_ns),
|
||||
"freq_key": freq_key,
|
||||
"s11_key": s11_key,
|
||||
"s21_key": s21_key,
|
||||
@@ -177,6 +179,8 @@ class NpzStore(StoreApi):
|
||||
frequency_hz=freq,
|
||||
s11=s11,
|
||||
s21=s21,
|
||||
capture_start_ns=int(combo.get("capture_start_ns", 0)),
|
||||
capture_end_ns=int(combo.get("capture_end_ns", 0)),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@ class TraceRecord:
|
||||
stage_index: int
|
||||
frequency_hz: np.ndarray
|
||||
samples: np.ndarray
|
||||
# End of this trace's own sweep, or 0 when the producer reported no per-trace
|
||||
# timing. Preferred over the collection timestamp for the exported sweep time:
|
||||
# in a switched matrix each combo is measured at a different instant.
|
||||
capture_end_ns: int = 0
|
||||
|
||||
|
||||
def _normalize_channel(channel: str) -> str:
|
||||
@@ -85,6 +89,7 @@ def _build_stage_records(
|
||||
stage_index=int(stage_index),
|
||||
frequency_hz=frequency_hz,
|
||||
samples=samples,
|
||||
capture_end_ns=int(trace.capture_end_ns),
|
||||
)
|
||||
)
|
||||
return records
|
||||
@@ -137,7 +142,15 @@ def _build_sweep_history(
|
||||
|
||||
start_freq_hz = float(base.frequency_hz[0])
|
||||
stop_freq_hz = float(base.frequency_hz[-1])
|
||||
timestamp_sec = float(base.monotonic_ns) / 1_000_000_000.0 if base.monotonic_ns > 0 else float(fallback_index)
|
||||
# Prefer the exported trace's own sweep time: with a switching matrix the
|
||||
# combos of one collection are measured milliseconds apart, so the
|
||||
# collection timestamp misplaces every combo but the last.
|
||||
if base.capture_end_ns > 0:
|
||||
timestamp_sec = float(base.capture_end_ns) / 1_000_000_000.0
|
||||
elif base.monotonic_ns > 0:
|
||||
timestamp_sec = float(base.monotonic_ns) / 1_000_000_000.0
|
||||
else:
|
||||
timestamp_sec = float(fallback_index)
|
||||
|
||||
history.append(
|
||||
{
|
||||
|
||||
@@ -39,12 +39,19 @@ from python_app.orchestration.shm.ring_writer import ShmRingWriter
|
||||
from python_app.storage.npz.serialize import serialize_result_collection, serialize_trace_collection
|
||||
|
||||
|
||||
def _trace(in_pos: int, out_pos: int, n: int) -> TraceData:
|
||||
def _trace(in_pos: int, out_pos: int, n: int, *, capture_ns: tuple[int, int] = (0, 0)) -> TraceData:
|
||||
"""Build a trace with float32-exact data so round-trips compare exactly."""
|
||||
freq = np.arange(n, dtype=np.float32) + 1.0
|
||||
s11 = (np.arange(n, dtype=np.float32) + 0.5j * np.arange(n, dtype=np.float32)).astype(np.complex64)
|
||||
s21 = (-np.arange(n, dtype=np.float32) + 2.0j * np.arange(n, dtype=np.float32)).astype(np.complex64)
|
||||
return TraceData(combo=ComboKey(input=in_pos, output=out_pos), frequency_hz=freq, s11=s11, s21=s21)
|
||||
return TraceData(
|
||||
combo=ComboKey(input=in_pos, output=out_pos),
|
||||
frequency_hz=freq,
|
||||
s11=s11,
|
||||
s21=s21,
|
||||
capture_start_ns=capture_ns[0],
|
||||
capture_end_ns=capture_ns[1],
|
||||
)
|
||||
|
||||
|
||||
class TraceCollectionRoundTripTest(unittest.TestCase):
|
||||
@@ -52,7 +59,7 @@ class TraceCollectionRoundTripTest(unittest.TestCase):
|
||||
collection = SweepCollection(
|
||||
collection_id=7,
|
||||
monotonic_ns=123,
|
||||
traces=[_trace(0, 0, 4), _trace(3, 1, 2)],
|
||||
traces=[_trace(0, 0, 4, capture_ns=(11, 13)), _trace(3, 1, 2, capture_ns=(15, 19))],
|
||||
capture_start_ns=10,
|
||||
capture_end_ns=20,
|
||||
)
|
||||
@@ -66,6 +73,10 @@ class TraceCollectionRoundTripTest(unittest.TestCase):
|
||||
self.assertTrue(np.array_equal(got.frequency_hz, original.frequency_hz))
|
||||
self.assertTrue(np.array_equal(got.s11, original.s11))
|
||||
self.assertTrue(np.array_equal(got.s21, original.s21))
|
||||
self.assertEqual(
|
||||
(got.capture_start_ns, got.capture_end_ns),
|
||||
(original.capture_start_ns, original.capture_end_ns),
|
||||
)
|
||||
|
||||
def test_raw_round_trips(self) -> None:
|
||||
self._assert_round_trips(RAW_MAGIC)
|
||||
@@ -78,6 +89,34 @@ class TraceCollectionRoundTripTest(unittest.TestCase):
|
||||
decoded = decode_trace_collection(serialize_trace_collection(collection, RAW_MAGIC), RAW_MAGIC)
|
||||
self.assertEqual(decoded.traces, [])
|
||||
|
||||
def test_payload_without_per_trace_window_table_still_decodes(self) -> None:
|
||||
# A producer built before per-trace timing stops after the collection
|
||||
# window; its traces must still decode, with the timestamps left at zero.
|
||||
collection = SweepCollection(
|
||||
collection_id=4,
|
||||
monotonic_ns=5,
|
||||
traces=[_trace(1, 0, 3, capture_ns=(7, 9))],
|
||||
capture_start_ns=6,
|
||||
capture_end_ns=10,
|
||||
)
|
||||
full = serialize_trace_collection(collection, RAW_MAGIC)
|
||||
legacy = full[: -(4 + 16 * len(collection.traces))]
|
||||
|
||||
decoded = decode_trace_collection(legacy, RAW_MAGIC)
|
||||
self.assertEqual((decoded.capture_start_ns, decoded.capture_end_ns), (6, 10))
|
||||
self.assertEqual(len(decoded.traces), 1)
|
||||
self.assertEqual((decoded.traces[0].capture_start_ns, decoded.traces[0].capture_end_ns), (0, 0))
|
||||
|
||||
def test_per_trace_window_count_mismatch_is_rejected(self) -> None:
|
||||
collection = SweepCollection(
|
||||
collection_id=4, monotonic_ns=5, traces=[_trace(1, 0, 3, capture_ns=(7, 9))]
|
||||
)
|
||||
payload = serialize_trace_collection(collection, RAW_MAGIC)
|
||||
# Overwrite the window-table count (u32 before the single 16-byte pair).
|
||||
corrupt = payload[:-20] + struct.pack("<I", 2) + payload[-16:]
|
||||
with self.assertRaises(ValueError):
|
||||
decode_trace_collection(corrupt, RAW_MAGIC)
|
||||
|
||||
|
||||
class ResultCollectionRoundTripTest(unittest.TestCase):
|
||||
def test_all_payload_kinds_round_trip(self) -> None:
|
||||
|
||||
@@ -140,6 +140,36 @@ class SwitchedMatrixComboAcquisitionTest(unittest.TestCase):
|
||||
expected_combos,
|
||||
)
|
||||
|
||||
def test_each_switch_step_stamps_its_traces_with_its_own_capture_window(self) -> None:
|
||||
# The whole point of per-trace timing: three switch steps are measured one
|
||||
# after another, so their traces must NOT all share the collection window.
|
||||
service, _inner, _input_switch = _switched_service(input_steps=3)
|
||||
|
||||
collection = service.acquire_collection(collection_id=7)
|
||||
|
||||
windows_by_step: dict[int, set[tuple[int, int]]] = {}
|
||||
for trace in collection.traces:
|
||||
step = int(trace.combo.input) // _INNER_INPUTS
|
||||
windows_by_step.setdefault(step, set()).add(
|
||||
(int(trace.capture_start_ns), int(trace.capture_end_ns))
|
||||
)
|
||||
|
||||
self.assertEqual(sorted(windows_by_step), [0, 1, 2])
|
||||
for step, windows in windows_by_step.items():
|
||||
self.assertEqual(len(windows), 1, f"step {step} traces disagree on their window")
|
||||
start_ns, end_ns = next(iter(windows))
|
||||
self.assertGreater(start_ns, 0)
|
||||
self.assertGreaterEqual(end_ns, start_ns)
|
||||
# Each step's window sits inside the collection's.
|
||||
self.assertGreaterEqual(start_ns, collection.capture_start_ns)
|
||||
self.assertLessEqual(end_ns, collection.capture_end_ns)
|
||||
|
||||
# Steps are strictly ordered in time — the whole reason the collection-level
|
||||
# window cannot stand in for a per-combo timestamp.
|
||||
step_starts = [next(iter(windows_by_step[step]))[0] for step in sorted(windows_by_step)]
|
||||
self.assertEqual(step_starts, sorted(step_starts))
|
||||
self.assertGreater(len(set(step_starts)), 1)
|
||||
|
||||
|
||||
class ManualComboCaptureUsesTargetedAcquisitionTest(unittest.TestCase):
|
||||
"""The per-combo capture session must not sweep the full widened matrix."""
|
||||
|
||||
@@ -232,6 +232,7 @@ class MultiRadarSequentialCaptureSession:
|
||||
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(
|
||||
@@ -239,6 +240,8 @@ class MultiRadarSequentialCaptureSession:
|
||||
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)
|
||||
|
||||
@@ -202,6 +202,7 @@ class SequentialCaptureSession:
|
||||
|
||||
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(
|
||||
@@ -209,6 +210,8 @@ class SequentialCaptureSession:
|
||||
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)
|
||||
@@ -385,11 +388,16 @@ def combine_traces_via_median(traces: list[TraceData]) -> TraceData:
|
||||
s21_median = (
|
||||
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]
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user