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:
Ayzen
2026-09-09 21:26:56 +03:00
co-authored by Claude Opus 5
parent d2338ff10c
commit 6693ece0aa
11 changed files with 645 additions and 64 deletions
@@ -13,6 +13,7 @@ from unittest import mock
import numpy as np
from python_app.hardware_full.matrix_radar_service import create_matrix_radar_service
from python_app.hardware_full.multi_device_service import MultiDeviceLibreVnaService
from python_app.hardware_full.switched_matrix_radar_service import SwitchedMatrixRadarService
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
@@ -213,5 +214,40 @@ class ManualComboCaptureUsesTargetedAcquisitionTest(unittest.TestCase):
self.assertEqual(inner_acquire.call_count, 2)
class PerPointStimulusSwitchingConfigTest(unittest.TestCase):
"""Per-point stimulus switching widens the matrix without a host-driven switch."""
@staticmethod
def _config(stimulus_switch_positions: int) -> RunConfigModel:
config = RunConfigModel()
config.radar.model = RunConfigModel.LIBREVNA_MULTI_MODEL
config.radar.driver_mode = "mock"
config.radar.multi_device.slave_serials = ["SLAVE_A", "SLAVE_B"]
config.radar.multi_device.stimulus_switch_positions = stimulus_switch_positions
config.apply_device_model_constraints()
return config
def test_two_positions_double_the_output_axis(self) -> None:
config = self._config(2)
self.assertEqual(config.matrix_virtual_output_positions, 4)
self.assertEqual(config.output_switch.positions, 4)
self.assertEqual(config.input_switch.positions, 4)
self.assertEqual(len(config.combos), 16)
def test_host_does_not_drive_any_switch(self) -> None:
# The switch is stepped by the controller in the hardware trigger loop, so the
# host must acquire the whole 4x4 matrix in ONE sweep and never wrap the
# service in the sweep-per-step widener.
service = create_matrix_radar_service(self._config(2))
self.assertIsInstance(service, MultiDeviceLibreVnaService)
self.assertNotIsInstance(service, SwitchedMatrixRadarService)
self.assertEqual(service.output_position_count, 4)
def test_disabled_switching_keeps_the_two_output_matrix(self) -> None:
config = self._config(1)
self.assertEqual(config.matrix_virtual_output_positions, 2)
self.assertEqual(len(config.combos), 8)
if __name__ == "__main__":
unittest.main()