Neutral atom device class

View on QuantumAI Run in Google Colab View source on GitHub Download notebook

This tutorial provides an introduction to making circuits that are compatible with neutral atom devices.

Neutral atom devices implement quantum gates in one of two ways. One method is by hitting the entire qubit array with microwaves to simultaneously act on every qubit. This method implements global $XY$ gates which take up to $100$ microseconds to perform. Alternatively, we can shine laser light on some fraction of the array. Gates of this type typically take around $1$ microsecond to perform. This method can act on one or more qubits at a time up to some limit dictated by the available laser power and the beam steering system used to address the qubits. Each category in the native gate set has its own limit, discussed more below.

try:
    import cirq
except ImportError:
    print("installing cirq...")
    !pip install cirq --quiet
    print("installed cirq.")
from math import pi

import cirq

Defining a NeutralAtomDevice

To define a NeutralAtomDevice, we specify

  • The set of qubits in the device.
  • The maximum duration of gates and measurements.
  • max_parallel_z: The maximum number of single qubit $Z$ rotations that can be applied in parallel.
  • max_parallel_xy: The maximum number of single qubit $XY$ rotations that can be applied in parallel.
  • max_parallel_c: The maximum number of atoms that can be affected by controlled gates simultaneously.
    • Note that max_parallel_c must be less than or equal to the minimum of max_parallel_z and max_parallel_xy.
  • control_radius: The maximum allowed distance between atoms acted on by controlled gates.

We show an example of defining a NeutralAtomDevice below.

"""Defining a NeutralAtomDevice."""
# Define milliseconds and microseconds for convenience.
ms = cirq.Duration(nanos=10**6)
us = cirq.Duration(nanos=10**3)

# Create a NeutralAtomDevice
neutral_atom_device = cirq.NeutralAtomDevice(
    qubits=cirq.GridQubit.rect(2, 3),
    measurement_duration=5 * ms,
    gate_duration=100 * us,
    max_parallel_z=3,
    max_parallel_xy=3,
    max_parallel_c=3,
    control_radius=2
)

Note that all above arguments are required to instantiate a NeutralAtomDevice. The example device above has the following properties:

  • The device is defined on a $3 \times 3$ grid of qubits.
  • Measurements take $5$ milliseconds.
  • Gates may take as long as $100$ microseconds if we utilize global microwave gates. Otherwise, a more reasonable bound would be $1$ microsecond.
  • A maximum of $3$ qubits may be simultaneously acted on by any gate category (max_parallel_c = 3).
  • Controlled gates have next-nearest neighbor connectivity (control_radius = 2).

We can see some properties of the device as follows.

"""View some properties of the device."""
# Display the neutral atom device.
print("Neutral atom device:", neutral_atom_device, sep="\n")

# Get the neighbors of a qubit.
qubit = cirq.GridQubit(0, 1)
print(f"\nNeighbors of qubit {qubit}:")
print(neutral_atom_device.neighbors_of(qubit))
Neutral atom device:
(0, 0)───(0, 1)───(0, 2)
│        │        │
│        │        │
(1, 0)───(1, 1)───(1, 2)

Neighbors of qubit (0, 1):
[cirq.GridQubit(1, 1), cirq.GridQubit(0, 2), cirq.GridQubit(0, 0)]

Native gate set

The gates supported by the NeutralAtomDevice class can be placed into three categories:

  1. Single-qubit rotations about the $Z$ axis.
  2. Single-qubit rotations about an arbitrary axis in the $X$-$Y$ plane. We refer to these as $XY$ gates in this tutorial.
  3. Controlled gates: CZ, CNOT, CCZ, and CCNOT (TOFFOLI).

Any rotation angle is allowed for single-qubit rotations. Some examples of valid single-qubit rotations are shown below.

"""Examples of valid single-qubit gates."""
# Single qubit Z rotations with any angle are valid.
neutral_atom_device.validate_gate(cirq.rz(pi / 5))

# Single qubit rotations about the X-Y axis with any angle are valid.
neutral_atom_device.validate_gate(
    cirq.PhasedXPowGate(phase_exponent=pi / 3, exponent=pi / 7)
)

A Hadamard gate is invalid because it is a rotation in the $X$-$Z$ plane instead of the $X$-$Y$ plane.

"""Example of an invalid single-qubit gate."""
invalid_gate = cirq.H

try:
    neutral_atom_device.validate_gate(invalid_gate)
except ValueError as e:
    print(f"As expected, {invalid_gate} is invalid!", e)
As expected, H is invalid! Unsupported gate: cirq.H

For controlled gates, the rotation must be a multiple of $\pi$ due to the physical implementation of the gates. In Cirq, this means the exponent of a controlled gate must be an integer. The next cell shows two examples of valid controlled gates.

"""Examples of valid multi-qubit gates."""
# Controlled gates with integer exponents are valid.
neutral_atom_device.validate_gate(cirq.CNOT)

# Controlled NOT gates with two controls are valid.
neutral_atom_device.validate_gate(cirq.TOFFOLI)

Any controlled gate with non-integer exponent is invalid.

"""Example of an invalid controlled gate."""
invalid_gate = cirq.CNOT ** 1.5

try:
    neutral_atom_device.validate_gate(invalid_gate)
except ValueError as e:
    print(f"As expected, {invalid_gate} is invalid!", e)
As expected, CNOT**1.5 is invalid! controlled gates must have integer exponents

Multiple controls are allowed as long as every pair of atoms (qubits) acted on by the controlled gate are close enough to each other. We can see this by using the validate_operation (or validate_circuit) method, as follows.

"""Examples of valid and invalid multi-controlled gates."""
# This TOFFOLI is valid because all qubits involved are close enough to each other.
valid_toffoli = cirq.TOFFOLI.on(cirq.GridQubit(0, 0), cirq.GridQubit(0, 1), cirq.GridQubit(0, 2))
neutral_atom_device.validate_operation(valid_toffoli)

# This TOFFOLI is invalid because all qubits involved are not close enough to each other.
invalid_toffoli = cirq.TOFFOLI.on(cirq.GridQubit(0, 0), cirq.GridQubit(1, 0), cirq.GridQubit(0, 2))

try:
    neutral_atom_device.validate_operation(invalid_toffoli)
except ValueError as e:
    print(f"As expected, {invalid_toffoli} is invalid!", e)
As expected, TOFFOLI((0, 0), (1, 0), (0, 2)) is invalid! Qubits cirq.GridQubit(1, 0), cirq.GridQubit(0, 2) are too far away

NeutralAtomDevices do not currently support gates with more than two controls although these are in principle allowed by the physical realizations.

"""Any gate with more than two controls is invalid."""
invalid_gate = cirq.ControlledGate(cirq.TOFFOLI)

try:
    neutral_atom_device.validate_gate(invalid_gate)
except ValueError as e:
    print(f"As expected, {invalid_gate} is invalid!", e)
As expected, CTOFFOLI is invalid! Unsupported gate: cirq.ControlledGate(sub_gate=cirq.TOFFOLI)

Finally, we note that the duration of any operation can be determined via the duration_of method.

"""Example of getting the duration of a valid operation."""
neutral_atom_device.duration_of(valid_toffoli)
cirq.Duration(micros=100)

Moment and circuit rules

In addition to consisting of valid operations as discussed above, valid moments on a NeutralAtomDevice must satisfy the following criteria:

  1. Only max_parallel_c gates of the same category may be performed in the same moment.
  2. All instances of gates in the same category in the same moment must be identical.
  3. Controlled gates cannot be applied in parallel with other gate types.
    • Physically, this is because controlled gates make use of all types of light used to implement gates.
  4. Qubits acted on by different controlled gates in parallel must be farther apart than the control_radius.
    • Physically, this is so that the entanglement mechanism doesn't cause the gates to interfere with one another.
  5. All measurements must be terminal.

Moments can be validated with the validate_moment method. Some examples are given below.

"""Example of a valid moment with single qubit gates."""
qubits = sorted(neutral_atom_device.qubits)

# Get a valid moment.
valid_moment = cirq.Moment(cirq.Z.on_each(qubits[:3]) + cirq.X.on_each(qubits[3:6]))

# Display it.
print("Example of a valid moment with single-qubit gates:", cirq.Circuit(valid_moment), sep="\n\n")

# Verify it is valid.
neutral_atom_device.validate_moment(valid_moment)
Example of a valid moment with single-qubit gates:

(0, 0): ───Z───

(0, 1): ───Z───

(0, 2): ───Z───

(1, 0): ───X───

(1, 1): ───X───

(1, 2): ───X───

Recall that we defined max_parallel_z = 3 in our device. Thus, if we tried to do 4 $Z$ gates in the same moment, this would be invalid.

"""Example of an invalid moment with single qubit gates."""
# Get an invalid moment.
invalid_moment = cirq.Moment(cirq.Z.on_each(qubits[:4]))

# Display it.
print("Example of an invalid moment with single-qubit gates:", cirq.Circuit(invalid_moment), sep="\n\n")

# Uncommenting raises ValueError: Too many simultaneous Z gates.
# neutral_atom_device.validate_moment(invalid_moment)
Example of an invalid moment with single-qubit gates:

(0, 0): ───Z───

(0, 1): ───Z───

(0, 2): ───Z───

(1, 0): ───Z───

This is also true for 4 $XY$ gates since we set max_parallel_xy = 3. However, there is an exception for $XY$ gates acting on every qubit, as illustrated below.

"""An XY gate can be performed on every qubit in the device simultaneously.

If the XY gate does not act on every qubit, it must act on <= max_parallel_xy qubits.
"""
valid_moment = cirq.Moment(cirq.X.on_each(qubits))
neutral_atom_device.validate_moment(valid_moment)

Although both $Z$ and $Z^{1.5}$ are valid gates, they cannot be performed simultaneously because all gates "of the same type" must be identical in the same moment.

"""Example of an invalid moment with single qubit gates."""
# Get an invalid moment.
invalid_moment = cirq.Moment(cirq.Z(qubits[0]), cirq.Z(qubits[1]) ** 1.5)

# Display it.
print("Example of an invalid moment with single-qubit gates:", cirq.Circuit(invalid_moment), sep="\n\n")

# Uncommenting raises ValueError: Non-identical simultaneous Z gates.
# neutral_atom_device.validate_moment(invalid_moment)
Example of an invalid moment with single-qubit gates:

(0, 0): ───Z──────

(0, 1): ───S^-1───

Appending operations

A common pattern for constructing circuits is to append a sequence of operations instead of explicitly creating moments. For a circuit defined on a NeutralAtomDevice, Cirq will respect the above rules for creating valid moments.

For example, if we append $Z$ and $Z^{1.5}$ from the previous example, Cirq will place them into two moments as shown below.

"""Cirq satisfies device restrictions automatically when appending operations."""
# Create a circuit for a NeutralAtomDevice.
circuit = cirq.Circuit(device=neutral_atom_device)

# Append two gates which cannot be in the same moment.
circuit.append([cirq.Z(qubits[0]), cirq.Z(qubits[1]) ** 1.5])

# Display the circuit.
print(circuit)
(0, 0): ───Z──────────

(0, 1): ───────S^-1───

This is true for all device rules. As another example, we can see how Cirq separates controlled gates from other gate types (the third rule above).

"""Cirq satisfies device restrictions automatically when appending operations."""
# Create a circuit for a NeutralAtomDevice.
circuit = cirq.Circuit(device=neutral_atom_device)

# Append two gates which cannot be in the same moment.
circuit.append([cirq.Z(qubits[0]), cirq.CNOT(*qubits[1: 3])])

# Display the circuit.
print(circuit)
(0, 0): ───Z───────

(0, 1): ───────@───
               │
(0, 2): ───────X───

Without any device restrictions, the Z and CNOT operations could be in the same moment, but because the circuit is defined on a NeutralAtomDevice, the CNOT is placed into a new moment.

Exercise: Multiple controlled gates in the same moment

Construct a NeutralAtomDevice which is capable of implementing two CNOTs in the same moment. Verify that these operations can indeed be performed in parallel by calling the validate_moment method or showing that Cirq inserts the operations into the same moment.

# Your code here!

Solution

"""Example solution for creating a device which allows two CNOTs in the same moment."""
# Create a NeutralAtomDevice.
device = cirq.NeutralAtomDevice(
    qubits=cirq.GridQubit.rect(2, 3),
    measurement_duration=5 * cirq.Duration(nanos=10**6),
    gate_duration=100 * cirq.Duration(nanos=10**3),
    max_parallel_z=4,
    max_parallel_xy=4,
    max_parallel_c=4,
    control_radius=1
)
print("Device:")
print(device)

# Create a circuit for a NeutralAtomDevice.
circuit = cirq.Circuit(device=device)

# Append two CNOTs that can be in the same moment.
circuit.append(
    [cirq.CNOT(cirq.GridQubit(0, 0), cirq.GridQubit(1, 0)), 
     cirq.CNOT(cirq.GridQubit(0, 2), cirq.GridQubit(1, 2))]
)

# Append two CNOTs that cannot be in the same moment.
circuit.append(
    [cirq.CNOT(cirq.GridQubit(0, 0), cirq.GridQubit(1, 0)), 
     cirq.CNOT(cirq.GridQubit(0, 1), cirq.GridQubit(1, 1))]
)

# Display the circuit.
print("\nCircuit:")
print(circuit)
Device:
(0, 0)───(0, 1)───(0, 2)
│        │        │
│        │        │
(1, 0)───(1, 1)───(1, 2)

Circuit:
           ┌──┐
(0, 0): ────@─────@───────
            │     │
(0, 1): ────┼─────┼───@───
            │     │   │
(0, 2): ────┼@────┼───┼───
            ││    │   │
(1, 0): ────X┼────X───┼───
             │        │
(1, 1): ─────┼────────X───
             │
(1, 2): ─────X────────────
           └──┘

Note that the square brackets above/below the circuit indicate the first two CNOTs are in the same moment.

Decomposing operations and circuits

Invalid operations can be decomposed into valid operations via the decompose_operation method. For example, we saw above that cirq.H was an invalid gate for a NeutralAtomDevice. This can be decomposed into valid operations as follows.

"""Example of decomposing an operation."""
# Decompose a Hadamard operation.
ops = neutral_atom_device.decompose_operation(cirq.H.on(qubits[0]))

# Display the circuit.
print("Circuit for H on a NeutralAtomDevice:\n")
cirq.Circuit(ops, device=neutral_atom_device)
Circuit for H on a NeutralAtomDevice:

Two-qubit and other operations can be decomposed in an analogous manner, for example the FSimGate below.

"""Another example of decomposing an operation."""
# Decompose an FSimGate operation.
ops = neutral_atom_device.decompose_operation(
    cirq.FSimGate(theta=0.1, phi=0.3).on(cirq.GridQubit(0, 0), cirq.GridQubit(1, 0))
)

# Display the circuit.
print("Circuit for FSim on a NeutralAtomDevice:\n")
cirq.Circuit(ops, device=neutral_atom_device)
Circuit for FSim on a NeutralAtomDevice: