Control flow#

Complex schedules can be constructed from pulses, gates and schedules using control flow. When adding an Operation or Schedule to another Schedule, a control_flow argument can be specified.

Adding schedules to a schedule (“subschedules”)#

A schedule can be added to a schedule just like an operation. This does not require the use of the control_flow argument.

This is useful e.g. to define a custom composite gate:

from quantify_scheduler.operations.gate_library import X, Y90
from quantify_scheduler import Schedule

def hadamard(qubit: str) -> Schedule:
    hadamard_sched = Schedule("hadamard")
    hadamard_sched.add(X(qubit))
    hadamard_sched.add(Y90(qubit))
    return hadamard_sched

my_schedule = Schedule("nice_experiment")
my_schedule.add(X("q1"))
my_schedule.add(hadamard("q1"))
{'name': '6e26c46c-6593-4898-beda-056d8b6cf4f0', 'operation_id': '3241240986467124045', 'timing_constraints': [{'rel_time': 0, 'ref_schedulable': None, 'ref_pt_new': None, 'ref_pt': None}], 'label': '6e26c46c-6593-4898-beda-056d8b6cf4f0'}

Note: The repetitions argument of all but the outermost Schedules is ignored. Schedules can be nested arbitrarily. Timing constraints relative to an inner schedule interpret the inner schedule as one continuous operation. It is not possible to use an operation within a subschedule from outside as reference operation.

Repetition loops#

  • Supported by Qblox backend.

If the control_flow argument of Schedule.add receives an instance of the Loop operation, the added Operation or Schedule will be repeated as specified.

This can be used to efficiently implement sequential averaging without running over the instruction limit of the hardware:

import numpy as np
from typing import Union
from quantify_scheduler.operations.control_flow_library import Loop
from quantify_scheduler.operations.gate_library import Reset, Measure

def t1_sched_sequential(
    times: Union[np.ndarray, float],
    qubit: str,
    repetitions: int = 1,
) -> Schedule:
    times = np.asarray(times)
    times = times.reshape(times.shape or (1,))

    schedule = Schedule("T1")
    for i, tau in enumerate(times):
        inner = Schedule(f"inner_{i}")
        inner.add(Reset(qubit), label=f"Reset {i}")
        inner.add(X(qubit), label=f"pi {i}")
        inner.add(
            Measure(qubit, acq_index=i),
            ref_pt="start",
            rel_time=tau,
            label=f"Measurement {i}",
        )
        schedule.add(inner, control_flow=Loop(repetitions))
    return schedule

Hardware averaging works as expected. In BinMode.APPEND binning mode, the data is returned in chronological order.

Note

Loops are an experimental feature and come with several limitations at this time, see below.

Limitations#

  1. The time order for zero-duration assembly instructions with the same timing may be incorrect, so verify the compiled schedule (via the generated assembly code). Using loops to implement sequential averaging for qubit spectroscopy is verified to work as expected. Known issues occur in using SetClockFrequency and SquarePulse with duration > 1us at the beginning or end of a loop, for example:

from quantify_scheduler.operations.pulse_library import SquarePulse

schedule = Schedule("T1")
schedule.add(
    SquarePulse(
        amp=0.3,
        port="q0:res",
        duration=2e-6,
        clock="q0.ro",
    ),
    control_flow=Loop(3),
)
/usr/local/lib/python3.9/site-packages/quantify_scheduler/schedules/schedule.py:865: UserWarning: Loops and Conditionals are an experimental feature. Please refer to the documentation: https://quantify-os.org/docs/quantify-scheduler/reference/control_flow.html
  warnings.warn(
{'name': '9a817843-9d99-47e9-9e36-6bd73cc18cd2', 'operation_id': '-5929298324256264280', 'timing_constraints': [{'rel_time': 0, 'ref_schedulable': None, 'ref_pt_new': None, 'ref_pt': None}], 'label': '9a817843-9d99-47e9-9e36-6bd73cc18cd2', 'control_flow': {'name': 'Loop', 'gate_info': {}, 'pulse_info': [], 'acquisition_info': [], 'logic_info': {}, 'control_flow_info': {'t0': 0, 'repetitions': 3}}}
  1. Repetition loops act on all port-clock combinations present in the circuit. This means that both X("q0") and Y90("q1") in the following circuit are repeated three times:

schedule = Schedule("T1")
x = schedule.add(X("q0"), control_flow=Loop(3))
schedule.add(Y90("q1"), ref_op=x, ref_pt="start", rel_time=0)
{'name': 'cdfd3aa9-a058-4320-8ae9-b549532b8a19', 'operation_id': '-6166865521450726691', 'timing_constraints': [{'rel_time': 0, 'ref_schedulable': '23288ddb-af54-44da-82a1-9da0243bc1b8', 'ref_pt_new': None, 'ref_pt': 'start'}], 'label': 'cdfd3aa9-a058-4320-8ae9-b549532b8a19'}

Safe use with the limitations#

To avoid the limitations mentioned above, it is strongly recommended to use loops only with subschedules, with no operations overlapping with the subschedule. Adding wait times before and after loops ensures that everything works as expected:

from quantify_scheduler.operations.pulse_library import IdlePulse, SquarePulse

inner_schedule = Schedule("inner")
inner_schedule.add(IdlePulse(16e-9))
# anything can go here
inner_schedule.add(
    SquarePulse(
        amp=0.3,
        port="q0:res",
        duration=2e-6,
        clock="q0.ro",
    )
)
# End the inner schedule with a wait time
inner_schedule.add(IdlePulse(16e-9))

outer_schedule = Schedule("outer")
# anything can go here
outer_schedule.add(IdlePulse(16e-9))
outer_schedule.add(inner_schedule, control_flow = Loop(5))
outer_schedule.add(IdlePulse(16e-9))
# anything can go here
{'name': 'ffb4c3d3-7bd3-4037-8768-383be2d67118', 'operation_id': '7187159171465382734', 'timing_constraints': [{'rel_time': 0, 'ref_schedulable': None, 'ref_pt_new': None, 'ref_pt': None}], 'label': 'ffb4c3d3-7bd3-4037-8768-383be2d67118'}