Devices

View on QuantumAI Run in Google Colab View source on GitHub Download notebook
try:
    import cirq
except ImportError:
    print("installing cirq...")
    !pip install --quiet cirq
    print("installed cirq.")
    import cirq

Validation basics

When you are looking to run an algorithm on a real quantum computer (not a simulated one), there are often many additional constraints placed on the circuits you would like to run. Qubit connectivity, algorithm layout and the types of gates used in the circuit all become much more important. Cirq uses the abstract class Device to represent the constraints of an actual quantum processor. An example implementation of a device can be seen in the cirq_google.Sycamore class:

import cirq_google
import networkx as nx

my_device = cirq_google.Sycamore
print(my_device)
(0, 5)───(0, 6)
                                             │        │
                                             │        │
                                    (1, 4)───(1, 5)───(1, 6)───(1, 7)
                                    │        │        │        │
                                    │        │        │        │
                           (2, 3)───(2, 4)───(2, 5)───(2, 6)───(2, 7)───(2, 8)
                           │        │        │        │        │        │
                           │        │        │        │        │        │
                  (3, 2)───(3, 3)───(3, 4)───(3, 5)───(3, 6)───(3, 7)───(3, 8)───(3, 9)
                  │        │        │        │        │        │        │        │
                  │        │        │        │        │        │        │        │
         (4, 1)───(4, 2)───(4, 3)───(4, 4)───(4, 5)───(4, 6)───(4, 7)───(4, 8)───(4, 9)
         │        │        │        │        │        │        │        │
         │        │        │        │        │        │        │        │
(5, 0)───(5, 1)───(5, 2)───(5, 3)───(5, 4)───(5, 5)───(5, 6)───(5, 7)───(5, 8)
         │        │        │        │        │        │        │
         │        │        │        │        │        │        │
         (6, 1)───(6, 2)───(6, 3)───(6, 4)───(6, 5)───(6, 6)───(6, 7)
                  │        │        │        │        │
                  │        │        │        │        │
                  (7, 2)───(7, 3)───(7, 4)───(7, 5)───(7, 6)
                           │        │        │
                           │        │        │
                           (8, 3)───(8, 4)───(8, 5)
                                    │
                                    │
                                    (9, 4)

This string representation of the device indicates the structure of the device and the connectivity of the qubits. In Sycamore's case, two-qubit gates can only be executed on qubits that are adjacent in the grid. Other constraints, like supported gates, are not shown in this representation.

You can access all of the constraints indirectly by validating moments, operations and circuits with the validate_*** method to verify if that structure would work on the device or not. In general, the validate_*** method will tell you what part of your operation/moment/circuit does not fit the device's constraints, and why. All devices support this functionality. For the Sycamore device:

op1 = cirq.X(cirq.GridQubit(7, 7))

try:
    my_device.validate_operation(op1)
except Exception as e:
    print(e)
Qubit not on device: cirq.GridQubit(7, 7).

The previous example used a qubit that wasn't on the device, making the operation invalid. Most validate_operation implementations also take into account things like supported gates and connectivity as well:

q1, q2, q3 = cirq.GridQubit(7, 4), cirq.GridQubit(7, 5), cirq.GridQubit(7, 6)
op1 = cirq.H(q1)
op2 = cirq_google.SYC(q1, q3)

try:
    my_device.validate_operation(op1)
except Exception as e:
    print(e)

try:
    my_device.validate_operation(op2)
except Exception as e:
    print(e)
Operation H(q(7, 4)) contains a gate which is not supported.
Qubit pair is not valid on device: (cirq.GridQubit(7, 4), cirq.GridQubit(7, 6)).

These validation operations can also be used with moments of operations and full circuits:

op1 = cirq.X(q2)
op2 = cirq_google.SYC(q1, q3)
try:
    my_device.validate_moment(cirq.Moment([op1, op2]))
except Exception as e:
    print(e)

my_circuit = cirq.Circuit(
    cirq.PhasedXPowGate(phase_exponent=0.3)(q1),
    cirq.PhasedXPowGate(phase_exponent=0.3)(q2),
    cirq_google.SYC(q1, q2),
    cirq_google.SYC(q2, q3),
)
my_device.validate_circuit(my_circuit)
Qubit pair is not valid on device: (cirq.GridQubit(7, 4), cirq.GridQubit(7, 6)).

op1 is allowed on qubit q2, but op2 has the same invalid qubit target error as before. validate_moment finds this error by iterating the moment and stopping once the invalid operation is found. On the other hand, my_circuit satisfies all the device constraints and could be run on a Sycamore device, so validate_circuit does not throw an exception for it.

Metadata features

Some devices will also expose additional information via the metadata property. Metadata is usually exposed via the an instance (or subclass instance) of the cirq.DeviceMetadata class. You can access the metadata information of the Sycamore device with the metadata attribute:

metadata = my_device.metadata
print(type(metadata))
<class 'cirq.devices.grid_device_metadata.GridDeviceMetadata'>
issubclass(type(metadata), cirq.DeviceMetadata)
True

The Sycamore device is a 2d grid device that exposes a cirq.GridDeviceMetadata with a uniform set of gates across all the qubits as well as a planar nearest neighbor connectivity graph. You can explore the properties below, starting with qubit_set and nx_graph, which are common to all instances and subclasses of the cirq.DeviceMetadata class.

First, the set of qubits available are available in the qubit_set attribute.

print(metadata.qubit_set)
frozenset({cirq.GridQubit(3, 8), cirq.GridQubit(7, 5), cirq.GridQubit(2, 6), cirq.GridQubit(3, 3), cirq.GridQubit(1, 6), cirq.GridQubit(4, 1), cirq.GridQubit(5, 6), cirq.GridQubit(3, 9), cirq.GridQubit(2, 7), cirq.GridQubit(3, 4), cirq.GridQubit(7, 6), cirq.GridQubit(8, 3), cirq.GridQubit(0, 5), cirq.GridQubit(2, 8), cirq.GridQubit(5, 7), cirq.GridQubit(9, 4), cirq.GridQubit(6, 4), cirq.GridQubit(5, 2), cirq.GridQubit(3, 5), cirq.GridQubit(7, 2), cirq.GridQubit(4, 6), cirq.GridQubit(2, 3), cirq.GridQubit(5, 8), cirq.GridQubit(3, 6), cirq.GridQubit(4, 7), cirq.GridQubit(2, 4), cirq.GridQubit(6, 5), cirq.GridQubit(5, 3), cirq.GridQubit(6, 6), cirq.GridQubit(1, 7), cirq.GridQubit(4, 2), cirq.GridQubit(2, 5), cirq.GridQubit(5, 4), cirq.GridQubit(8, 4), cirq.GridQubit(6, 1), cirq.GridQubit(4, 8), cirq.GridQubit(3, 2), cirq.GridQubit(0, 6), cirq.GridQubit(4, 3), cirq.GridQubit(6, 7), cirq.GridQubit(5, 5), cirq.GridQubit(6, 2), cirq.GridQubit(4, 9), cirq.GridQubit(5, 0), cirq.GridQubit(8, 5), cirq.GridQubit(7, 3), cirq.GridQubit(4, 4), cirq.GridQubit(3, 7), cirq.GridQubit(1, 4), cirq.GridQubit(6, 3), cirq.GridQubit(7, 4), cirq.GridQubit(5, 1), cirq.GridQubit(4, 5), cirq.GridQubit(1, 5)})

The nx_graph attribute details which of the 54 different qubits are connected to one another. Connected qubit pairs can execute two-qubit gates between them.

print(metadata.nx_graph)
Graph with 54 nodes and 88 edges

cirq.GridDeviceMetadata has some attributes that are not automatically included in cirq.DeviceMetadata, including gateset, which indicates the types and families of Cirq gates that are accepted by all qubits across the device.

print(metadata.gateset)
Gateset: 
Type GateFamily: cirq_google.experimental.ops.coupler_pulse.CouplerPulse
Accepts `cirq.Gate` instances `g` s.t. `isinstance(g, cirq_google.experimental.ops.coupler_pulse.CouplerPulse)`

Type GateFamily: cirq.ops.measurement_gate.MeasurementGate
Accepts `cirq.Gate` instances `g` s.t. `isinstance(g, cirq.ops.measurement_gate.MeasurementGate)`

Type GateFamily: cirq.ops.phased_x_z_gate.PhasedXZGate
Accepts `cirq.Gate` instances `g` s.t. `isinstance(g, cirq.ops.phased_x_z_gate.PhasedXZGate)`

Type GateFamily: cirq.ops.common_gates.XPowGate
Accepts `cirq.Gate` instances `g` s.t. `isinstance(g, cirq.ops.common_gates.XPowGate)`

Type GateFamily: cirq.ops.common_gates.YPowGate
Accepts `cirq.Gate` instances `g` s.t. `isinstance(g, cirq.ops.common_gates.YPowGate)`

Type GateFamily: cirq.ops.phased_x_gate.PhasedXPowGate
Accepts `cirq.Gate` instances `g` s.t. `isinstance(g, cirq.ops.phased_x_gate.PhasedXPowGate)`

Type GateFamily: cirq.ops.common_gates.ZPowGate
Accepts `cirq.Gate` instances `g` s.t. `isinstance(g, cirq.ops.common_gates.ZPowGate)`
Accepted tags: [cirq_google.PhysicalZTag()]

FSimGateFamily: allow_symbol=False; atol=1e-06
`cirq_google.FSimGateFamily` which accepts any instance of gate types in
gate_types_to_check: [cirq.ops.fsim_gate.FSimGate,cirq.ops.fsim_gate.PhasedFSimGate,cirq.ops.swap_gates.ISwapPowGate,cirq.ops.phased_iswap_gate.PhasedISwapPowGate,cirq.ops.common_gates.CZPowGate,cirq.ops.identity.IdentityGate]
which matches (across types), via instance check / value equality, a gate in
gates_to_accept: [ISWAP**0.5]

FSimGateFamily: allow_symbol=False; atol=1e-06
`cirq_google.FSimGateFamily` which accepts any instance of gate types in
gate_types_to_check: [cirq.ops.fsim_gate.FSimGate,cirq.ops.fsim_gate.PhasedFSimGate,cirq.ops.swap_gates.ISwapPowGate,cirq.ops.phased_iswap_gate.PhasedISwapPowGate,cirq.ops.common_gates.CZPowGate,cirq.ops.identity.IdentityGate]
which matches (across types), via instance check / value equality, a gate in
gates_to_accept: [ISWAP**-0.5]

FSimGateFamily: allow_symbol=False; atol=1e-06
`cirq_google.FSimGateFamily` which accepts any instance of gate types in
gate_types_to_check: [cirq.ops.fsim_gate.FSimGate,cirq.ops.fsim_gate.PhasedFSimGate,cirq.ops.swap_gates.ISwapPowGate,cirq.ops.phased_iswap_gate.PhasedISwapPowGate,cirq.ops.common_gates.CZPowGate,cirq.ops.identity.IdentityGate]
which matches (across types), via instance check / value equality, a gate in
gates_to_accept: [SYC]

Type GateFamily: cirq.ops.common_gates.ZPowGate
Accepts `cirq.Gate` instances `g` s.t. `isinstance(g, cirq.ops.common_gates.ZPowGate)`
Ignored tags: [cirq_google.PhysicalZTag()]

Type GateFamily: cirq.ops.wait_gate.WaitGate
Accepts `cirq.Gate` instances `g` s.t. `isinstance(g, cirq.ops.wait_gate.WaitGate)`

These metadata features can be useful when designing/building algorithms around certain device information in order to tailor them for that device.

The cirq.Device interface

For advanced users (such as vendors) it is also possible to implement your own Device with its own unique constraints and metadata information. Below is an example of a fictitious custom device:

class MyDevice(cirq.Device):
    """Five qubits on a line, supporting X/Y/Z and CZ between neighbors."""

    def __init__(self):
        # Specify the qubits available to the device
        self._qubits = set(cirq.LineQubit.range(5))
        # Specify which gates are valid
        self._supported_gates = cirq.Gateset(
            cirq.XPowGate, cirq.YPowGate, cirq.ZPowGate, cirq.CZPowGate
        )

    def validate_operation(self, operation):
        """Check to make sure `operation` is valid.

        `operation` must be on qubits found on the device
        and if it is a two qubit gate the qubits must be adjacent

        Raises:
          ValueError: if operation acts on qubits not found on the device.
          ValueError: if two qubit gates have non-local interactions.
          ValueError: if the operation is not in the supported gates.
        """
        # Ensure that the operation's qubits are available on the device
        if any(x not in self._qubits for x in operation.qubits):
            raise ValueError("Using qubits not found on device.")

        # Ensure that the operation's qubits are adjacent if there are two of them
        if len(operation.qubits) == 2:
            p, q = operation.qubits
            if not p.is_adjacent(q):
                raise ValueError('Non-local interaction: {}'.format(repr(operation)))

        # Ensure that the operation itself is a supported one
        if operation not in self._supported_gates:
            raise ValueError("Unsupported operation type.")

    def validate_circuit(self, circuit):
        """Check to make sure `circuit` is valid.

        Calls validate_operation on all operations as well as imposing
        a global limit on the total number of CZ gates.

        Raises:
          ValueError: if `validate_operation` raises for any operation in the
            circuit.
          ValueError: if there are more than 10 CZ gates in the entire circuit.
        """
        # Call Device's `validate_operation`, which calls the `validate_operation`
        #   function specified above on each operation in the circuit
        super().validate_circuit(circuit)
        # Ensure that no more than 10 two-qubit CZ gates exist in the circuit
        cz_count = sum(1 for mom in circuit for op in mom if len(op.qubits) == 2)
        if cz_count > 10:
            raise ValueError("Too many total CZs")

    @property
    def metadata(self):
        """MyDevice GridDeviceMetadata."""
        # Since `MyDevice` is planar it is a good idea to subclass the
        # GridDeviceMetadata class to communicate additional device information to
        # the user.
        return cirq.GridDeviceMetadata(
            qubit_pairs=[(p, q) for p in self._qubits for q in self._qubits if p.is_adjacent(q)],
            gateset=self._supported_gates,
        )

At absolute minimum, when creating a custom Device, you should inherit from cirq.Device and overwrite the __init__ and validate_operation methods.

This custom device can now be used to validate circuits:

my_custom_device = MyDevice()

my_circuit = cirq.Circuit(
    cirq.X(cirq.LineQubit(0)),
    cirq.X(cirq.LineQubit(2)),
    cirq.X(cirq.LineQubit(4)),
    cirq.CZ(*cirq.LineQubit.range(2)),
)
too_many_czs = cirq.Circuit(cirq.CZ(*cirq.LineQubit.range(2)) for _ in range(11))

# my_circuit is valid for my_custom_device.
my_custom_device.validate_circuit(my_circuit)

# each operation of too_many_czs is valid individually...
for moment in too_many_czs:
    for op in moment:
        my_custom_device.validate_operation(op)

# But the device has global constraints which the circuit does not meet:
try:
    my_custom_device.validate_circuit(too_many_czs)
except Exception as e:
    print(e)
Too many total CZs

By default, the validate_circuit method of the cirq.Device class simply calls validate_moment on all the moments, which calls validate_operation on all the operations. It is advisable to maintain this behavior in your custom device, which can be implemented as above, by calling super().validate_*** when writing each method.

Depending on the scoping of constraints the custom device has, certain less local constraints might be better placed in validate_moment and certain global constraints might belong in validate_circuit. In addition to this, you can also add metadata options to your device. You can define a metadata subclass of cirq.DeviceMetadata or you can use an inbuilt metadata class like cirq.GridDeviceMetadata:

my_metadata = my_custom_device.metadata

# Display device graph:
nx.draw(my_metadata.nx_graph)

png

Use in a virtual Engine

Devices can also be used to specify cirq.SimulatedLocalEngine instances, which let you validate and simulate circuits using the same interface that the quantum hardware does. Read more in the Virtual Engine Interface page.

Additionally, these virtual Engines can be combined with noisy simulation that attempts to mimic existing hardware devices with the Quantum Virtual Machine.

Summary

Devices in Cirq are used to specify constraints on circuits that are imposed by quantum hardware devices. You can check that an operation, moment, or circuit is valid on a particular cirq.Device by using validate_operation, validate_moment, or validate_circuit respectively. You can also create your own custom device objects to specify constraints for a new or changed device. Device objects, custom and otherwise, also can carry around metadata that may be useful for the validation process or other processes.