Using ScheduleBuilder#

This section describes how to use ScheduleBuilder to compose pulse schedules. ScheduleBuilder encapsulates registered QuantumOps, the calibration information for them, QPU components and their topology, and control channel properties.

In the context of IQM Pulla and EXA, an instance of ScheduleBuilder is given by the framework, and it contains all the necessary information to execute against a particular quantum computer instance. Here, we assume that user has an instance of ScheduleBuilder builder to work with.

Creating TimeBoxes#

A typical workflow begins with calling get_implementation() with the name of a desired QuantumOp and the locus for the operation. The locus is the logical target of the operation, usually a sequence of qubits or computational resonators. This outputs a an instance of GateImplementation, which is capable of producing TimeBoxes with the correct calibration for that locus.

For example, here we ask for the GateImplementation of prx, cz, and measure, all of which are standard gates recognized by IQM Pulse, for qubits QB1 and QB3. Any gate which is registered in the runtime can be requested this way.

qubits = ["QB1", "QB3"]
cz_gate_impl = builder.get_implementation("cz", qubits)
prx_gate_impl = builder.get_implementation("prx", ["QB1"])
measure_gate_impl = builder.get_implementation("measure", qubits)

# equivalent shortcuts:
cz_gate_impl = builder.cz(qubits)
prx_gate_impl = builder.prx(["QB1"])
measure_gate_impl = builder.measure(qubits)

Notice how the number of qubits matches the operation: CZ acts on 2 qubits, while PRX acts on only one. Measure can act on any number of qubits.

There might be several available implementations for an operation. get_implementation gives the implementation that is set as the default, unless a specific implementation is requested with a keyword argument.

To instantiate some concrete TimeBoxes, we call time implementation with the logical parameters of the operations, as defined by the QuantumOps. PRX has 2 parameters: the 2 angles of a phased rotation. CZ does not have any parameters.

import numpy as np
x180 = prx_gate_impl(np.pi, 0)
y90 = prx_gate_impl(np.pi/2, np.pi/2)
cz = cz_gate_impl()
measure = measure_gate_impl()

# equivalent shortcuts for prx:
x180 = prx_gate_impl.rx(np.pi)
y90 = prx_gate_impl.ry(np.pi/2)

One important method is the wait(), which blocks the control channels of the given components for a certain time:

wait = builder.wait(qubits, duration=100e-9, rounding=True)  # Duration in seconds

In all of the examples above, the TimeBoxes are atomic, which can be organized into composite TimeBoxes to decide their relative order.

Composing TimeBoxes#

TimeBoxes can be concatenated with the following rules:

  • Addition (+) concatenates the children of the operands into a single TimeBox. Use addition to allow gates on disjoint loci to execute simultaneously, for example doing a PRX on all qubits.

  • The pipe operation (|) groups two TimeBoxes together without concatenating. This results in composite TimeBox with two children, the operands, which are scheduled separately. Use the pipe to ensure that certain operations execute before some others.

  • Iterables of TimeBoxes are treated as the sum of the elements.

This would execute 2 PRX gates on QB1 and QB2 simultaneously:

p1 = builder.prx(["QB1"]).rx(0.1)
p2 = builder.prx(["QB2"]).rx(0.2)
p1 + p2 + p1 + p2

If the last operator was | instead, the second gate on QB2 would execute only after the first 3 gates.

p1 + p2 + p1 | p2

# equivalent:
from iqm.pulse.timebox.TimeBox import TimeBox
TimeBox.composite([p1 + p2 + p1, p2])

Together, these rules provide a handy way of defining complex circuits easily:

# Do Y90, wait, CZ, X180 in this order, and right-align everything to be as close to measure as possible:
circuit1 = (y90 + wait + cz + x180).set_alap() | measure

# Do X180, then repeat (Y90, wait) 5 times, then measure:
circuit2 = x180 + [y90, wait] * 5 + measure

# Concatenate boxes, preserving their internal alignment:
circuit3 = circuit1 | circuit2 | circuit1

Resolving TimeBoxes into a Schedule#

A TimeBox are made atomic by resolving it using ScheduleBuilder.resolve_timebox(). When using a framework like IQM Pulla or Exa, the framework will take care of the resolving as part of compilation, so the user does not need to do it explicitly.

TimeBoxes are resolved recursively: The children of a TimeBox are resolved, and resulting (sub-)Schedules are aligned according to the SchedulingStrategy (ASAP or ALAP) of the TimeBox. The time duration of a TimeBox is determined by its contents and the way they are scheduled during the resolution. Finally, all channels are padded with Waits so that the total duration of Instructions on every channel is equal. In other words, the Schedule becomes a “rectangle”.

An important part of the scheduling are the blocking rules, that is, whether the contents of two TimeBoxes block or slide past each other. The rules are:

  • An atomic TimeBox is considered to act on a component if it has instructions on any of the non-virtual channels related to that component.

  • A composite TimeBox acts on the union of its children’s locus components.

  • A TimeBox blocks all channels related to any component it acts on.

  • When scheduling two TimeBoxes, the instructions will not overlap in time if the TimeBoxes block overlapping channels.

In addition to blocking the channels actually present in a TimeBox, it is possible to block neighbouring channels as well (for example in order to limit cross-talk). The applied neighbourhood is specified in ScheduleBuilder.resolve_timebox(). The neighbourhood is defined as an integer such that 0 means “block only the involved channels”, 1 means “block the involved channels and the channels of any neighbouring couplers”, 2 means “the same as in (1) but in addition block all channels connected by those couplers”, and so on. The blocking rules do not add actual Wait or Block instructions are added to the neighbourhood channels, and two overlapping neighbourhoods do not block each other. The blocking comes in question only when actual content would be added to those neighbourhood channels.

In practice, the rules and default GateImplementations ensure that the user can concatenate arbitrary gates without worrying that the gates have an adverse effect on each other. For example, the pulse of a PRX gate playing at the same time as a CZ gate or a measurement would ruin both operations. If overlapping of such gates is desired, the best way is to arrange the Instructions on the Schedule level and wrap the schedule into an atomic TimeBox.

Note

Virtual channels are special channels that exist only to aid the scheduling algorithm. Examples are tracking the phases of the MOVE gate, and timing of fast feedback. These channels are removed when the Playlist is finalized.

Miscellaneous features#

You are encouraged to discover the many features of ScheduleBuilder and TimeBox by reading the API: ScheduleBuilder, TimeBox.

A quick reference of selected features is provided in the examples below.

Finding information about the target system:

# Find component information:
all_qubits = builder.chip_topology.qubits

# Find topology information, such as native qubit connectivity:
builder.chip_topology.get_neighbor_locus_components(["QB3"])

# Find control channels and their properties
channel_name = builder.get_drive_channel("QB3")
properties = builder.channels[channel_name]

# Modify any calibration value:
builder.calibration["prx"]["drag_gaussian"][("QB1",)]["duration"] = 160e-9

Working with TimeBoxes:

# Access children of a composite box:
circuit1.children[0] == circuit1[0]

# Access the schedule and instructions of an atomic box:
cz_schedule = cz.atom

# Print contents of a box
circuit3.print()

# Ask for a non-default implementation:
builder.get_implementation("prx", ["QB1"], implementation="drag_gaussian")

# Override calibration in one specific box:
builder.get_implementation("prx", ["QB1"], priority_calibration={"duration": 80e-9})

Common pitfalls#

Some typical errors that are easy to make with the syntax:

  • Not calling the implementation to get a TimeBox. The call is easy to forget especially if there are no parameters to give:

    cz = builder.cz(qubits)
    cz + cz  # Error!
    cz() + cz()  # Correct
    
  • Giving a single component as locus:

    builder.prx("QB3")  # Error! "Q" is not a valid component
    builder.prx(["QB3"])  # Correct