Schedules and Devices¶
Schedule
and Circuit
are the two major container classes for
quantum circuits. In contrast to Circuit
, a Schedule
includes
detailed information about the timing and duration of the gates.
Conceptually a Schedule
is made up of a set of ScheduledOperations
as well as a description of the Device
on which the schedule is
intended to be run. Each ScheduledOperation
is made up of a time
when the operation starts and a duration
describing how long the
operation takes, in addition to the Operation
itself (like in a
Circuit
an Operation
is made up of a Gate
and the QubitIds
upon which the gate acts.)
Devices¶
The Device
class is an abstract class which encapsulates constraints
(or lack thereof) that come when running a circuit on actual hardware.
For instance, most hardware only allows certain gates to be enacted
on qubits. Or, as another example, some gates may be constrained to not
be able to run at the same time as neighboring gates. Further the
Device
class knows more about the scheduling of Operations
.
Here for example is a Device
made up of 10 qubits on a line:
import cirq
from cirq.devices import GridQubit
class Xmon10Device(cirq.Device):
def __init__(self):
self.qubits = [GridQubit(i, 0) for i in range(10)]
def duration_of(self, operation):
# Wouldn't it be nice if everything took 10ns?
return cirq.Duration(nanos=10)
def validate_operation(self, operation):
if not isinstance(operation, cirq.GateOperation):
raise ValueError('{!r} is not a supported operation'.format(operation))
if not isinstance(operation.gate, (cirq.CZPowGate,
cirq.XPowGate,
cirq.PhasedXPowGate,
cirq.YPowGate)):
raise ValueError('{!r} is not a supported gate'.format(operation.gate))
if len(operation.qubits) == 2:
p, q = operation.qubits
if not p.is_adjacent(q):
raise ValueError('Non-local interaction: {}'.format(repr(operation)))
def validate_scheduled_operation(self, schedule, scheduled_operation):
self.validate_operation(scheduled_operation.operation)
def validate_circuit(self, circuit):
for moment in circuit:
for operation in moment.operations:
self.validate_operation(operation)
def validate_schedule(self, schedule):
for scheduled_operation in schedule.scheduled_operations:
self.validate_scheduled_operation(schedule, scheduled_operation)
This device, for example, knows that two qubit gates between next-nearest-neighbors is not valid:
device = Xmon10Device()
circuit = cirq.Circuit()
circuit.append([cirq.CZ(device.qubits[0], device.qubits[2])])
try:
device.validate_circuit(circuit)
except ValueError as e:
print(e)
# prints something like
# ValueError: Non-local interaction: Operation(cirq.CZ, (GridQubit(0, 0), GridQubit(2, 0)))
Schedules¶
A Schedule
contains more timing information above and beyond
that which is provided by the Moment
structure of a Circuit
.
This can be used both for fine grained timing control, but also to
optimize a circuit for a particular device. One can work directly
with Schedules
or, more common, use a custom scheduler that
converts a Circuit
to a Schedule
. A simple example of
such a scheduler is the moment_by_moment_schedule
method of
schedulers.py
. This scheduler attempts to keep the Moment
structure of the underlying Circuit
as much as possible:
each Operation
in a Moment
is scheduled to start at the
same time (such a schedule may not be possible, in which case this
method raises an exception.)
Here, for example, is a simple Circuit
on the Xmon10Device
defined above
circuit = cirq.Circuit()
circuit.append([cirq.CZ(device.qubits[0], device.qubits[1]), cirq.X(device.qubits[0])])
print(circuit)
# prints:
# (0, 0): ───@───X───
# │
# (1, 0): ───@───────
This can be converted over into a schedule using the moment by moment schedule
schedule = cirq.moment_by_moment_schedule(device, circuit)
Schedules have an attributed scheduled_operations
which contains
all the scheduled operations in a SortedListWithKey
, where the
key is the start time of the SortedOperation
. Schedules
support
nice helpers for querying about the time-space layout of the schedule.
For instance, the Schedule
behaves as if it has an index corresponding
to time. So, we can look up which operations occur at a specific time
print(schedule[cirq.Timestamp(nanos=15)])
# prints something like
# [ScheduledOperation(Timestamp(picos=10000), Duration(picos=10000),...)]
or even a start and end time using slicing notation
slice = schedule[cirq.Timestamp(nanos=5):cirq.Timestamp(nanos=15)]
slice_schedule = cirq.Schedule(device, slice)
print(slice_schedule == schedule)
# prints True
More complicated queries across Schedules
can be done using the
query
.
Schedules
are usually built by converting from Circuits
,
but one can also directly manipulate the schedule using the
include
and exclude
methods. include
will check
if there are any collisions with other schedule operations.