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”)#
Supported by
Qblox
andZurich Instruments
backends.
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': 'ff54d644-5252-4bd7-a1b1-25bdc3a742fb', 'operation_id': '6421710904551040989', 'timing_constraints': [{'rel_time': 0, 'ref_schedulable': None, 'ref_pt_new': None, 'ref_pt': None}], 'label': 'ff54d644-5252-4bd7-a1b1-25bdc3a742fb'}
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#
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
andSquarePulse
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': 'b6a9789d-964a-49fe-b010-c613d328fe67', 'operation_id': '-4498715431015246759', 'timing_constraints': [{'rel_time': 0, 'ref_schedulable': None, 'ref_pt_new': None, 'ref_pt': None}], 'label': 'b6a9789d-964a-49fe-b010-c613d328fe67', 'control_flow': {'name': 'Loop', 'gate_info': {}, 'pulse_info': [], 'acquisition_info': [], 'logic_info': {}, 'control_flow_info': {'t0': 0, 'repetitions': 3}}}
Repetition loops act on all port-clock combinations present in the circuit. This means that both
X("q0")
andY90("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': 'a6d46b92-a361-4156-8b04-6d2aad2ebe0f', 'operation_id': '-6811162511495749844', 'timing_constraints': [{'rel_time': 0, 'ref_schedulable': 'b4dccc31-5213-4fbb-9edd-57344972e109', 'ref_pt_new': None, 'ref_pt': 'start'}], 'label': 'a6d46b92-a361-4156-8b04-6d2aad2ebe0f'}
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': '06086a20-9dac-4347-81ab-e57a0355fcca', 'operation_id': '-8693818772045047602', 'timing_constraints': [{'rel_time': 0, 'ref_schedulable': None, 'ref_pt_new': None, 'ref_pt': None}], 'label': '06086a20-9dac-4347-81ab-e57a0355fcca'}