per-point stimulus switching: host side
Матрица 4x4 за одну развёртку: каждый порт мастера измеряется на каждой частоте дважды, с двумя положениями выходного ключа. Ключ дёргает контроллер в триггерной петле, поэтому расбери переключателями больше не управляет — SwitchedMatrixRadarService уходит из тракта, все 16 комбинаций снимаются в пределах одной точки вместо разброса между тремя развёртками. Выходы нумеруются позиция-мажорно, стадии порт-мажорно (старшие биты счётчика стадий выбирают порт, младший — положение ключа). Из этого следует, что положение ключа = младший бит номера стадии, а период 2 нацело делит 4 стадии — поэтому контроллеру достаточно инвертировать ногу на каждый цикл триггера, без абсолютного счётчика. Выходы 0 и 1 при этом сохраняют прежний смысл. Приёмники слейвов пронумерованы от числа выходов + 1, а не от фиксированного 3: иначе при 4 выходах прямой член «приёмник 3 <- выход 3» назывался бы так же, как отражение выхода 3. При двух выходах даёт прежние порты 3..6. Слейвам порт-стадии выставляются в 7: стоковый приём «одна за последней стадией» перестаёт работать, когда порт выбирается старшими битами. Всё за флагом multi_device.stimulus_switch_positions (1 = прежнее поведение, байт в байт). Спецификация правок FPGA/STM в device_firmware/per_point_switching.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,10 @@ import numpy as np
|
||||
|
||||
from python_app.hardware_full.librevna_multi_device_driver.cycle_collection import LIBREVNA_NATIVE_SWEEP_TIMEOUT_SECONDS
|
||||
from python_app.hardware_full.librevna_multi_device_driver.exceptions import TransientCollectionError
|
||||
from python_app.hardware_full.librevna_multi_device_driver.models import SweepConfiguration
|
||||
from python_app.hardware_full.librevna_multi_device_driver.models import (
|
||||
SweepConfiguration,
|
||||
normalize_switch_substage_count,
|
||||
)
|
||||
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
||||
from python_app.models.run_config_model import RadarSweepModel, RunConfigModel
|
||||
|
||||
@@ -43,10 +46,33 @@ _MAX_RECOVERY_WALL_SECONDS: float = 10.0
|
||||
# previously caused the periodic freeze.
|
||||
_MAX_TRANSIENT_COLLECTION_RETRIES: int = 2
|
||||
|
||||
_INPUT_S_PARAMETERS_BY_OUTPUT: dict[int, tuple[str, ...]] = {
|
||||
0: ("s31", "s41", "s51", "s61"),
|
||||
1: ("s32", "s42", "s52", "s62"),
|
||||
}
|
||||
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:
|
||||
@@ -57,6 +83,10 @@ class MultiDeviceLibreVnaService:
|
||||
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.
|
||||
switch_substage_count: int = 1
|
||||
_controller: "MultiDeviceVnaController | None" = field(init=False, default=None, repr=False)
|
||||
_sweep_configuration: SweepConfiguration | None = field(init=False, default=None, repr=False)
|
||||
_using_mock_backend: bool = field(init=False, default=False, repr=False)
|
||||
@@ -75,6 +105,12 @@ class MultiDeviceLibreVnaService:
|
||||
raise ValueError(f"Unsupported multi-device backend mode: {self.backend_mode}")
|
||||
self.backend_mode = mode
|
||||
self._using_mock_backend = mode == "mock"
|
||||
self.switch_substage_count = normalize_switch_substage_count(self.switch_substage_count)
|
||||
|
||||
@property
|
||||
def output_position_count(self) -> int:
|
||||
"""Return how many stimulus outputs one acquisition produces."""
|
||||
return RunConfigModel.MULTI_DEVICE_MASTER_PORTS * self.switch_substage_count
|
||||
|
||||
@property
|
||||
def using_mock_backend(self) -> bool:
|
||||
@@ -92,6 +128,7 @@ class MultiDeviceLibreVnaService:
|
||||
master_serial_number=self.master_serial,
|
||||
slave_serial_numbers=self.slave_serials,
|
||||
force_external_reference=self.force_external_reference,
|
||||
switch_substage_count=self.switch_substage_count,
|
||||
)
|
||||
logger.info(
|
||||
"Multi-device controller opened (master=%s, slaves=%s)",
|
||||
@@ -198,6 +235,7 @@ class MultiDeviceLibreVnaService:
|
||||
points=int(sweep.points),
|
||||
if_bandwidth=int(round(float(sweep.if_bandwidth_hz))),
|
||||
power_dbm=float(sweep.power_dbm),
|
||||
dwell_us=int(sweep.dwell_us),
|
||||
)
|
||||
logger.debug(
|
||||
"Multi-device configured: %s-%s Hz, %s points, IFBW=%s Hz, %s dBm",
|
||||
@@ -347,14 +385,17 @@ class MultiDeviceLibreVnaService:
|
||||
}
|
||||
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(RunConfigModel.MULTI_DEVICE_OUTPUT_POSITIONS):
|
||||
for output_pos in range(self.output_position_count):
|
||||
reflection = self._required_s_parameter(
|
||||
normalized_s_parameters,
|
||||
"s11" if output_pos == 0 else "s22",
|
||||
reflection_by_output[output_pos],
|
||||
)
|
||||
|
||||
for input_pos, s_parameter_name in enumerate(_INPUT_S_PARAMETERS_BY_OUTPUT[output_pos]):
|
||||
for input_pos, s_parameter_name in enumerate(forward_by_output[output_pos]):
|
||||
traces.append(
|
||||
TraceData(
|
||||
combo=ComboKey(input=input_pos, output=output_pos),
|
||||
@@ -388,7 +429,7 @@ class MultiDeviceLibreVnaService:
|
||||
)
|
||||
base_phase = 2.0 * math.pi * np.linspace(0.0, 1.0, points, dtype=np.float32) + self._mock_phase
|
||||
traces: list[TraceData] = []
|
||||
for output_pos in range(RunConfigModel.MULTI_DEVICE_OUTPUT_POSITIONS):
|
||||
for output_pos in range(self.output_position_count):
|
||||
reflected_phase = base_phase * (0.55 + 0.05 * output_pos) + 0.7 * (output_pos + 1)
|
||||
s11 = (
|
||||
(0.22 + 0.04 * output_pos) * np.cos(reflected_phase)
|
||||
|
||||
Reference in New Issue
Block a user