"""Helpers to assemble high-level sweep results from packet streams.""" from __future__ import annotations import logging import math from typing import Iterable import numpy as np from ..exceptions import IncompleteSweepError, ParseError from ..enums import SweepKind from ..models import VNADatapointPacket, VNARawPoint, VNASweepSettings, SweepResult logger = logging.getLogger(__name__) _REFERENCE_FLAG = 0x10 def _extract_stage(flags: int) -> int: """Return stage index encoded inside datapoint flag byte.""" return flags >> 5 def _find_vna_value( datapoint: VNADatapointPacket, *, stage: int, port_index: int, reference: bool, ) -> complex: """Find one complex receiver value for stage/port/ref tuple.""" source_mask = 1 << port_index if reference: source_mask |= _REFERENCE_FLAG for idx, flags in enumerate(datapoint.flags): if _extract_stage(int(flags)) != stage: continue if (int(flags) & source_mask) != source_mask: continue return complex(float(datapoint.real[idx]), float(datapoint.imag[idx])) kind = "reference" if reference else "receiver" raise ParseError( f"Missing {kind} value for stage={stage}, port_index={port_index}, point={datapoint.point_number}" ) def datapoint_to_raw_point( datapoint: VNADatapointPacket, settings: VNASweepSettings, *, num_ports: int, ) -> VNARawPoint: """Convert low-level VNADatapoint packet into callback-friendly object.""" measurements: dict[str, complex] = {} stage_by_excited_port = {port: stage for stage, port in enumerate(settings.excited_ports)} for excited_port, stage in stage_by_excited_port.items(): ref = _find_vna_value(datapoint, stage=stage, port_index=excited_port - 1, reference=True) for receiver_port in range(1, num_ports + 1): measured = _find_vna_value( datapoint, stage=stage, port_index=receiver_port - 1, reference=False, ) measurements[f"S{receiver_port}{excited_port}"] = measured / ref zero_span = settings.f_start_hz == settings.f_stop_hz and math.isclose( settings.power_start_dbm, settings.power_stop_dbm, rel_tol=0.0, abs_tol=0.0, ) if zero_span: return VNARawPoint( point_number=datapoint.point_number, frequency_hz=None, time_s=float(datapoint.frequency_or_time) * 1e-6, power_dbm=None, measurements=measurements, ) return VNARawPoint( point_number=datapoint.point_number, frequency_hz=float(datapoint.frequency_or_time), time_s=None, power_dbm=float(datapoint.cdbm) / 100.0, measurements=measurements, ) def assemble_vna_sweep( datapoints: Iterable[VNADatapointPacket], settings: VNASweepSettings, *, num_ports: int, expected_points: int, ) -> SweepResult: """Build a complete VNA sweep from raw datapoints.""" if expected_points <= 0: raise ValueError("expected_points must be > 0") x = np.empty(expected_points, dtype=np.float64) seen = np.zeros(expected_points, dtype=bool) stage_by_excited_port = {port: stage for stage, port in enumerate(settings.excited_ports)} receiver_limit = min(num_ports, 2) trace_names = [ f"s{receiver_port}{excited_port}" for excited_port in settings.excited_ports for receiver_port in range(1, receiver_limit + 1) if receiver_port <= 2 and excited_port <= 2 ] traces: dict[str, np.ndarray] = { name: np.empty(expected_points, dtype=np.complex128) for name in trace_names } zero_span = settings.f_start_hz == settings.f_stop_hz and math.isclose( settings.power_start_dbm, settings.power_stop_dbm, rel_tol=0.0, abs_tol=0.0, ) power_sweep = settings.kind == SweepKind.POWER for datapoint in datapoints: idx = datapoint.point_number if idx >= expected_points: raise ParseError(f"Received out-of-range point index {idx}, expected < {expected_points}") if zero_span: x[idx] = float(datapoint.frequency_or_time) * 1e-6 elif power_sweep: x[idx] = float(datapoint.cdbm) / 100.0 else: x[idx] = float(datapoint.frequency_or_time) for excited_port, stage in stage_by_excited_port.items(): ref = _find_vna_value(datapoint, stage=stage, port_index=excited_port - 1, reference=True) for receiver_port in range(1, num_ports + 1): if receiver_port > 2 or excited_port > 2: # Public SweepResult intentionally exposes 2-port canonical traces. continue measured = _find_vna_value( datapoint, stage=stage, port_index=receiver_port - 1, reference=False, ) trace_name = f"s{receiver_port}{excited_port}" trace = traces.get(trace_name) if trace is not None: trace[idx] = measured / ref seen[idx] = True missing = np.flatnonzero(~seen) if missing.size > 0: raise IncompleteSweepError( f"Sweep is incomplete: received {int(seen.sum())}/{expected_points} points" ) x_label = "frequency_hz" if zero_span: x_label = "time_s" elif power_sweep: x_label = "power_dbm" result = SweepResult(x=x, traces=traces, x_label=x_label) logger.debug( "Assembled VNA sweep: points=%d x_label=%s traces=%s", len(result.x), result.x_label, sorted(result.traces.keys()), ) return result