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_channel order.

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_data accepts an optional acq_channel_order. When provided, the output follows that order; otherwise it falls back to the previous sorting (fully backward compatible).
  • ScheduleGettable derives 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 to None and the previous behavior.
  • Widened the schedule parameter of get_acq_info_by_uuid and extract_acquisition_metadata_from_schedule from Schedule to ScheduleBase, so a CompiledSchedule is accepted (the helpers already only use ScheduleBase members).

Tests

  • test_process_acquired_data_preserves_declared_channel_order - string channels with an explicit declared order map to the expected y variables.
  • 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 where list(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)

Edited by Ashmit JaiSarita Gupta

Merge request reports

Loading
Loading