number stimulus outputs in device stage order
The output index now equals the device stage, so outputs follow the order the device measures them: master port 1 through its switch positions, then port 2. The previous position-major numbering permuted the middle two against the physical order, so data captured before this needs its calibration re-captured.
This commit is contained in:
@@ -73,13 +73,24 @@ def build_output_channels(
|
||||
) -> tuple[OutputChannel, ...]:
|
||||
"""Map every (master port, switch position) pair to an output and a device stage.
|
||||
|
||||
Outputs are numbered switch-position-major, so positions of the FIRST switch
|
||||
setting keep the output indices they have without per-point switching (with
|
||||
``switch_substage_count=1`` this is the identity). Stages are numbered
|
||||
port-major, because the master bitstream takes the excited port from the upper
|
||||
bits of the stage counter and the switch position from the lower bits — which
|
||||
is also what makes the switch line a plain alternation the controller can drive
|
||||
without tracking an absolute stage number.
|
||||
**Output index equals the device stage**, so outputs are numbered in exactly the
|
||||
order the device measures them: port 1 through all switch positions, then port 2
|
||||
through all of them. That is the order an operator sees on a scope while hanging
|
||||
antennas, and matching it is worth more than any renumbering cleverness.
|
||||
|
||||
Both numbers are port-major because the master bitstream takes the excited port
|
||||
from the UPPER bits of the stage counter and the switch position from the lower
|
||||
bits. The low bit being the switch position is what lets the trigger-loop
|
||||
controller just invert its pin every cycle instead of tracking an absolute stage.
|
||||
|
||||
Before 2026-09-17 outputs were numbered switch-position-major, which kept
|
||||
outputs 0/1 meaning the same thing with and without per-point switching but
|
||||
permuted the middle two relative to the physical order (slots 0,1,2,3 landed on
|
||||
outputs 0,2,1,3). Data captured under that layout is NOT comparable with data
|
||||
captured now: outputs 1 and 2 swapped meaning, so calibration and reference sets
|
||||
from before the change have to be re-captured.
|
||||
|
||||
With ``switch_substage_count=1`` the layout is the stock one either way.
|
||||
|
||||
This is the single source of truth shared by the protocol builder, the cycle
|
||||
collector and the acquisition service, so the mapping cannot drift between them.
|
||||
@@ -94,12 +105,13 @@ def build_output_channels(
|
||||
)
|
||||
return tuple(
|
||||
OutputChannel(
|
||||
index=substage * len(stimulus_ports) + port_index,
|
||||
# Один и тот же номер: выход — это и есть стадия прибора.
|
||||
index=port_index * substage_count + substage,
|
||||
stimulus_port=port,
|
||||
stage_index=port_index * substage_count + substage,
|
||||
)
|
||||
for substage in range(substage_count)
|
||||
for port_index, port in enumerate(stimulus_ports)
|
||||
for substage in range(substage_count)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -399,20 +399,32 @@ class PerPointStimulusSwitchingTest(unittest.TestCase):
|
||||
|
||||
def test_two_positions_give_four_outputs(self) -> None:
|
||||
channels = build_output_channels((1, 2), 2)
|
||||
# Outputs are switch-position-major, so outputs 0/1 stay exactly what they are
|
||||
# without switching; stages are port-major, matching the bitstream taking the
|
||||
# port from the stage counter's UPPER bits.
|
||||
# Port-major in BOTH numbers: the output index is the device stage, so outputs
|
||||
# come in the order the device measures them — port 1 through its switch
|
||||
# positions, then port 2. Verified against a scope on the switch line.
|
||||
self.assertEqual(
|
||||
[(c.index, c.stimulus_port, c.stage_index) for c in channels],
|
||||
[(0, 1, 0), (1, 2, 2), (2, 1, 1), (3, 2, 3)],
|
||||
[(0, 1, 0), (1, 1, 1), (2, 2, 2), (3, 2, 3)],
|
||||
)
|
||||
|
||||
def test_output_index_is_the_device_stage(self) -> None:
|
||||
# The whole point of the 2026-09-17 renumbering: an operator hanging antennas
|
||||
# sees stage order on a scope, and the output numbering must not permute it.
|
||||
for substage_count in (1, 2, 4):
|
||||
channels = build_output_channels((1, 2), substage_count)
|
||||
self.assertEqual(
|
||||
[c.index for c in channels],
|
||||
[c.stage_index for c in channels],
|
||||
msg=f"substages={substage_count}",
|
||||
)
|
||||
self.assertEqual([c.index for c in channels], list(range(len(channels))))
|
||||
|
||||
def test_switch_position_is_the_low_stage_bit(self) -> None:
|
||||
# This is what lets the controller just invert its pin on every trigger cycle
|
||||
# instead of tracking an absolute stage number.
|
||||
channels = build_output_channels((1, 2), 2)
|
||||
for channel in channels:
|
||||
expected_position = channel.index // 2 # switch-position-major output order
|
||||
expected_position = channel.index & 1
|
||||
self.assertEqual(channel.stage_index & 1, expected_position)
|
||||
|
||||
def test_stage_budget_is_enforced(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user