Preserve declared acquisition-channel order in MeasurementControl output (#151)
What does this MR do?
Fixes #151 (closed) - MeasurementControl output variables (y0, y1, ...) no longer
follow a sorted channel order, but the order in which acquisition channels were
declared in the schedule.
Problem
MeasurementControl._process_acquired_data ordered acquisition channels with
sorted(acquired_data.data_vars). For numeric channels this happens to match the
declared order, but for non-numeric channels it does not:
Measure("b1", "a1", acq_channel=("b1", "a1"))was expected to yield y0, y1 for b1 (I, Q) and y2, y3 for a1 (I, Q), but
sorted() put a1 first, swapping the qubits. This breaks two-qubit experiments
(e.g. conditional oscillations) that rely on a fixed target/spectator ordering.
Root cause
The acquisition dataset is assembled in InstrumentCoordinator.retrieve_acquisition
via xarray.Dataset.merge, which does not preserve insertion order. The sorted()
call was a determinism workaround that discards the user's intent for non-numeric
channels.
Why derive the order from the schedule (rather than trusting the dataset)
The minimal fix would be to drop sorted() and keep list(acquired_data.data_vars).
Before going that route I checked whether the dataset's own order is reliable, and it
is not. InstrumentCoordinator.retrieve_acquisition builds the dataset by merging one
sub-dataset per instrument:
for instrument_name in compiled_instructions:
component_acquisitions = self.get_component(...).retrieve_acquisition()
acquisitions = acquisitions.merge(component_acquisitions)compiled_instructions is keyed by instrument, and xarray.Dataset.merge preserves
insertion order (verified - it does not sort). So:
- single readout module -> variable order follows the schedule and looks correct;
- two qubits on separate readout modules (the conditional-oscillation case in #151 (closed)) ->
order follows instrument iteration, which is not guaranteed to match the declared
acq_channelorder.
This is almost certainly why sorted() was there: the incoming order was treated as
unreliable, and sorting at least made it deterministic. The schedule's acquisition
metadata is the only source that always reflects the declared order, which is why this
MR reconstructs the order from there instead of trusting list(data_vars). The unit
test test_process_acquired_data_multi_instrument_merge_order reproduces the
merge-in-non-declared-order case.
Fix
MeasurementControl._process_acquired_dataaccepts an optionalacq_channel_order. When provided, the output follows that order; otherwise it falls back to the previous sorting (fully backward compatible).ScheduleGettablederives the declared order from the compiled schedule's acquisition metadata (extract_acquisition_metadata_from_schedule, which keys channels by first appearance) and threads it through. If the order cannot be determined (e.g. mixed protocols) it falls back toNoneand the previous behavior.- Widened the
scheduleparameter ofget_acq_info_by_uuidandextract_acquisition_metadata_from_schedulefromScheduletoScheduleBase, so aCompiledScheduleis accepted (the helpers already only useScheduleBasemembers).
Tests
test_process_acquired_data_preserves_declared_channel_order- string channels with an explicit declared order map to the expectedyvariables.test_process_acquired_data_sorts_without_declared_order- back-compat: no declared order falls back to sorting.test_process_acquired_data_multi_instrument_merge_order- builds the dataset via the same per-instrument.merge()the instrument coordinator uses, in a non-declared order, and asserts the declared order is restored (the case wherelist(data_vars)/sorting would be wrong).test_schedule_gettable_preserves_declared_acq_channel_order- end-to-end: a two-channel schedule ("q_b"then"q_a") is compiled and run with the instrument coordinator mocked to return the channels in reversed (sorted) order; the output still follows the declared order.
Checklist
-
uv run pytest tests/measurement tests/scheduler/test_gettables.py(89 passed, 1 skipped) -
uv run ruff check ./uv run ruff format . -
uv run pyright(0 errors on changed files) - CHANGELOG updated
Closes #151 (closed)