Conditional Playback#

The conditional playback feature introduces a method for dynamic qubit state management.

Example: Conditional Reset#

Overview#

This feature is centered around the ConditionalReset operation, which measures the state of a qubit and applies a corrective action based on the measurement outcome.

Conditional Reset Operation#

The ConditionalReset operation functions as follows:

  • It first measures the state of a qubit, using a ThresholdedAcquisition.

  • If the qubit is in an excited state, an X gate (e.g. a DRAG pulse for transmons) is applied to bring the qubit back to the ground state.

  • Conversely, if the qubit is already in the ground state, the operation simply waits for the duration of the X gate.

  • This ensures that the total duration of the ConditionalReset operation remains consistent, regardless of the qubit’s initial state.

Usage#

The ConditionalReset is used by adding it to a schedule:

from quantify_scheduler import Schedule
from quantify_scheduler.qblox.operations import ConditionalReset

schedule = Schedule("")
schedule.add(ConditionalReset("q0"))
schedule.add(...)

Internally, ConditionalReset performs a ThresholdedAcquisition. If a schedule includes multiple acquisitions on the same qubit, each ConditionalReset and ThresholdedAcquisition must have a unique acq_index.

For example:

schedule = Schedule("conditional")
schedule.add(ConditionalReset("q0", acq_index=0))
schedule.add(Measure("q0", acq_index=1, acq_protocol="ThresholdedAcquisition"))

When using multiple consecutive ConditionalReset on the same qubit, increment the acq_index for each:

schedule = Schedule()
schedule.add(ConditionalReset("q0"))
schedule.add(...)
schedule.add(ConditionalReset("q0", acq_index=1))
schedule.add(...)
schedule.add(ConditionalReset("q0", acq_index=2))

Since the ConditionalReset performs a ThresholdedAcquisition, the measured state of the qubit (0 or 1) will be saved in the acquisition dataset.

Qblox backend implementation#

The ConditionalReset is implemented as a new subschedule in the operations library:

class ConditionalReset(Schedule):
    def __init__(self, qubit_name):
        super().__init__("conditional reset")
        self.add(
            Measure(
                qubit_name,
                acq_protocol="ThresholdedAcquisition",
                feedback_trigger_label=qubit_name,
            )
        )
        cond_schedule = Schedule("conditional subschedule")
        cond_schedule.add(X(qubit_name))
        self.add(
            ConditionalOperation(body=cond_schedule, qubit_name=qubit_name),
            rel_time=TRIGGER_DELAY,
        )

where the Measure operation is given an additional trigger label feedback_trigger_label=qubit_name. The label needs to match the label given to ConditionalOperation to match relevant triggers with the conditional schedule. It is set here to be equal to qubit_name, but could be any string.

In the Qblox hardware, a finite delay is added between the end of the acquisition and the start of the conditional operation. This delay is specified in TRIGGER_DELAY. Furthermore, the ConditionalReset operation adds an additional 4 ns of idle time at the end the operation that is passed to body (in this case, the X gate). The total duration of the ConditionalOperation is then the duration of the body ( X in this case ) plus 4 ns.

The total duration (t) of the ConditionalReset is calculated as the sum of various time components, typically shorter than the standard idle reset duration (qubit.reset.duration). For example, in our test suite’s mock_setup_basic_transmon_with_standard_params fixture, the following values are used:

  • q0.measure.acq_delay() = 100 ns

  • q0.measure.integration_time() = 1000 ns

  • q0.rxy.duration() = 20 ns

  • TRIGGER_DELAY = 364 ns

  • buffer_time = 4 ns

  • q0.reset.duration() = 200000 ns

So that the total duration of the ConditionalReset is 1488 ns, compared to the standard idle reset duration of 200000 ns.

Conditional Playback#

Conditionally playing a subschedule is implemented using the ConditionalOperation operation. The previous example on implementing a ConditionalReset can be extended by simply replacing the X gate with the subschedule to be conditionally played:

cond_schedule = Schedule("conditional subschedule")
cond_schedule.add(X(qubit_name))
cond_schedule.add(...)

schedule.add(
    Measure(qubit_name, 
    acq_protocol="ThresholdedAcquisition", 
    feedback_trigger_label=qubit_name))
schedule.add(
    ConditionalOperation(body=cond_schedule, qubit_name=qubit_name),
    rel_time=TRIGGER_DELAY,
)

There are a few rules to follow for the cond_schedule:

  1. The cond_schedule must have a duration of at least 4 ns.

  2. The cond_schedule must have a duration of max 65532 ns (per repetition)

  3. Nested conditional operations are not allowed.

Limitations#

  • Currently only implemented for the Qblox backend.

  • Triggers cannot be sent more frequently than once every 252 ns.

  • The interval between the end of an acquisition and the start of a conditional operation must be at least 364 ns, as specified by TRIGGER_DELAY. See the qblox documentation for details.

  • The measurement result of ConditionalReset is saved inside the dataset as well, potentially obscuring interesting data.

  • Currently, it is not possible to use ConditionalReset with MeasurementControl class, when using BinMode.APPEND, but it works correctly when using BinMode.AVERAGE.

Example: Obscured measurement data#

For example, including the ConditionalReset inside a basic Rabi schedule, we could have the following implementation:

schedule = Schedule("Rabi", repetitions)
schedule.add_resource(ClockResource(name=clock, freq=frequency))

for i, (amp, duration) in enumerate(zip(amps, durations)):
    schedule.add(ConditionalReset("q0", acq_index = 2*i), label=f"Reset {i}")
    schedule.add(
        DRAGPulse(
            duration=duration,
            G_amp=amp,
            D_amp=0,
            port=port,
            clock=clock,
            phase=0,
        ),
        label=f"Rabi_pulse {i}",
    )
    schedule.add(Measure("q0", acq_index=2*i+1), label=f"Measurement {i}")

where the relevant data is saved in the odd (2*i+1) acquisition indices. Currently, it is not possible to disregard the measurement data related to ConditionalReset.

A possible workaround is to introduce an extra port-clock combination to the hardware configuration, and an extra qubit to the device configuration to separate “conditional” qubits, from regular ones. For example:

hardware_config = {
    "portclock_configs": [
        {"port": "q0:res", "clock": "q0.ro", "interm_freq": 200000000.0},
        {"port": "q0c:res", "clock": "q0.ro", "interm_freq": 200000000.0},
    ],
    ... : ...
}

# define qubit parameters
q0c = BasicTransmonElement("q0c")
...

schedule = Schedule("Rabi", repetitions)
schedule.add_resource(ClockResource(name=clock, freq=frequency))

for i, (amp, duration) in enumerate(zip(amps, durations)):
    schedule.add(ConditionalReset("q0c", acq_index=i), label=f"Reset {i}")
    schedule.add(
        DRAGPulse(
            duration=duration,
            G_amp=amp,
            D_amp=0,
            port=port,
            clock=clock,
            phase=0,
        ),
        label=f"Rabi_pulse {i}",
    )
    schedule.add(Measure("q0", acq_index=i), label=f"Measurement {i}")

Here, qubit_c and qubit will correspond to the same physical qubit and are controlled by the same port on the hardware, but the measurements will use two different sequencers and the data will be stored in two different acquisition channels. Note that this will limit your ability to do multiplexed readout.

Example: Parallel conditional operations and acquisitions#