Zoned Neutral Atom Compiler

Successful quantum computation requires advanced software, especially compilers that optimize quantum algorithms for hardware execution. Zoned neutral atom architectures execute operations in designated spatially separated zones. The zones facilitate higher coherence times and overall fidelity of the quantum computation. However, the zones also require the rearrangement of atoms during the quantum computation. MQT QMAP provides two tools to compile a quantum circuit to target-specific instructions for zoned quantum computing architectures based on neutral atoms:

  • a reuse-aware compiler based on [8], and

  • a routing-aware compiler based on [9].

Note

The second, i.e., routing-aware compiler also implements the reuse-aware compilation approach. Specifically, it exchanges the placement component of the reuse-aware compiler with a routing-aware placer. Hence, in the following, the first compiler is referred to as the routing-agnostic compiler and the second one as the routing-aware compiler.

Example: GHZ State on Neutral Atom Architecture

In this example, we will demonstrate how to use the zoned neutral atom compiler to generate a sequence of target-specific instructions for a quantum circuit. For this purpose, we employ a circuit that prepares a Greenberger-Horne-Zeilinger (GHZ) state of 8 qubits. Compared to the usual GHZ circuit that applies the CX-gates in a chain, this circuit applies the CX-gates in a tree-like manner. First, it brings one qubit into maximal superposition and then applies the first CX-gate as usual. Afterward, it applies two CX-gates in parallel controlled on the qubits already in superposition instead of applying them serially.

 1from qiskit import QuantumCircuit
 2from numpy import pi
 3
 4qc = QuantumCircuit(8)
 5qc.h(0)
 6qc.cx(0, 4)
 7qc.cx(0, 2)
 8qc.cx(4, 6)
 9qc.cx(0, 1)
10qc.cx(2, 3)
11qc.cx(4, 5)
12qc.cx(6, 7)
13
14qc.draw(output="mpl")
_images/5e8c81006a29e8f547f4be798e753d8b7a8909901af64ea9a37f0e8df1885b3b.svg

This circuit is not compatible with the native gate set of the zoned neutral atom architecture that consists of global ry-gates, local rz-gates, and controlled z-gates. The following circuit is equivalent to the previous one but decomposes the circuit into the native gate set of the zoned neutral atom architecture.

Note

Even though other single-qubit gates may not be supported by the hardware, the compiler can handle arbitrary single-qubit gates. It will translate them to generic u3 gates and include them in the output.

 1from qiskit import QuantumCircuit
 2from numpy import pi
 3
 4def global_ry(theta, num_qubits):
 5    """:returns: a global ry gate"""
 6    qc = QuantumCircuit(num_qubits)
 7    qc.ry(theta, range(num_qubits))
 8    return qc.to_gate(label = f"Ry({theta:.3f})")
 9
10
11qc = QuantumCircuit(8)
12qc.append(global_ry(-pi/4, 8), range(8))
13qc.z(range(8))
14qc.append(global_ry(pi/4, 8), range(8))
15qc.cz(0, 4)
16qc.append(global_ry(-pi/4, 8), range(8))
17qc.z(4)
18qc.append(global_ry(pi/4, 8), range(8))
19qc.cz(0, 2)
20qc.cz(4, 6)
21qc.append(global_ry(-pi/4, 8), range(8))
22qc.z([2, 6])
23qc.append(global_ry(pi/4, 8), range(8))
24qc.cz(0, 1)
25qc.cz(2, 3)
26qc.cz(4, 5)
27qc.cz(6, 7)
28qc.append(global_ry(-pi/4, 8), range(8))
29qc.z([1,3,5,7])
30qc.append(global_ry(pi/4, 8), range(8))
31
32qc.draw(output="mpl")
_images/8a91272c11b153a848a7672ad13b3fcd33fb3159ad6706b488d381efa0fc8a6b.svg

On the considered architecture, the single-qubit gates, i.e., the global ry and local rz-gates can be executed everywhere. However, the controlled z-gates can only be executed between nearby atoms in the so-called entanglement zone. This entanglement zone is spatially separated from the storage zone, where all atoms not involved in a cz-gate are stored.

Zoned Neutral Atom Architecture

To find an optimized sequence of target-specific instructions, we use one of the zoned neutral atom compilers. Each compiler requires first a specification of the architecture.

 1from mqt.qmap.na.zoned import ZonedNeutralAtomArchitecture
 2
 3arch = ZonedNeutralAtomArchitecture.from_json_string("""{
 4  "name": "Architecture with one entanglement and one storage zone",
 5  "operation_duration": {"rydberg_gate": 0.36, "single_qubit_gate": 52, "atom_transfer": 15},
 6  "operation_fidelity": {"rydberg_gate": 0.995, "single_qubit_gate": 0.9997, "atom_transfer": 0.999},
 7  "qubit_spec": {"T": 1.5e6},
 8  "storage_zones": [{
 9    "zone_id": 0,
10    "slms": [{"id": 0, "site_separation": [3, 3], "r": 20, "c": 100, "location": [0, 0]}],
11    "offset": [0, 0],
12    "dimension": [297, 57]
13  }],
14  "entanglement_zones": [{
15    "zone_id": 0,
16    "slms": [
17      {"id": 1, "site_separation": [12, 10], "r": 7, "c": 20, "location": [35, 67]},
18      {"id": 2, "site_separation": [12, 10], "r": 7, "c": 20, "location": [37, 67]}
19    ],
20    "offset": [35, 67],
21    "dimension": [230, 60]
22  }],
23  "aods": [{"id": 0, "site_separation": 2, "r": 100, "c": 100}],
24  "rydberg_range": [[[30, 62], [270, 132]]]
25}""")

In the following, we will first create a compiler with default settings. Those can later be fine-tuned to fit the needs of the user, see further down.

1from mqt.qmap.na.zoned import RoutingAgnosticCompiler, RoutingAwareCompiler
2
3compiler = RoutingAwareCompiler(arch)
4# or if you want to use the routing-agnostic compiler:
5# compiler = RoutingAgnosticCompiler(arch)

Now, the created compiler can be used to compile the circuit from above. The output is in the .naviz format that can be read by the MQT NAViz tool at github.com/cda-tum/mqt-naviz. This tool allows visualizing the resulting quantum computation. To import the architecture used for the compilation into MQT NAViz, you can use the to_namachine_file method to export the architecture to the .namachine format accepted by MQT NAViz.

1from mqt.core import load
2
3circ = load(qc)
4code = compiler.compile(circ)
5print(code)
atom (0.000, 57.000) atom0
atom (3.000, 57.000) atom1
atom (6.000, 57.000) atom2
atom (9.000, 57.000) atom3
atom (12.000, 57.000) atom4
atom (15.000, 57.000) atom5
atom (18.000, 57.000) atom6
atom (21.000, 57.000) atom7
@+ ry -0.78540 global
@+ rz 3.14159 atom0
@+ rz 3.14159 atom1
@+ rz 3.14159 atom2
@+ rz 3.14159 atom3
@+ rz 3.14159 atom4
@+ rz 3.14159 atom5
@+ rz 3.14159 atom6
@+ rz 3.14159 atom7
@+ ry 0.78540 global
@+ load [
    atom0
    atom4
]
@+ move [
    (1.000, 58.000) atom0
    (13.000, 58.000) atom4
]
@+ move [
    (32.000, 62.000) atom0
    (40.000, 62.000) atom4
]
@+ move [
    (35.000, 67.000) atom0
    (37.000, 67.000) atom4
]
@+ store [
    atom0
    atom4
]
@+ cz zone_cz0
@+ load atom4
@+ move (40.000, 62.000) atom4
@+ move (34.000, 58.000) atom4
@+ move (33.000, 57.000) atom4
@+ store atom4
@+ ry -0.78540 global
@+ rz 3.14159 atom4
@+ ry 0.78540 global
@+ load atom2
@+ move (7.000, 58.000) atom2
@+ move (40.000, 62.000) atom2
@+ move (37.000, 67.000) atom2
@+ store atom2
@+ load [
    atom4
    atom6
]
@+ move [
    (34.000, 58.000) atom4
    (19.000, 58.000) atom6
]
@+ move [
    (40.000, 72.000) atom4
    (32.000, 72.000) atom6
]
@+ move [
    (37.000, 77.000) atom4
    (35.000, 77.000) atom6
]
@+ store [
    atom4
    atom6
]
@+ cz zone_cz0
@+ load atom2
@+ move (40.000, 62.000) atom2
@+ load atom6
@+ move (32.000, 72.000) atom6
@+ move [
    (28.000, 55.000) atom2
    (25.000, 58.000) atom6
]
@+ move (24.000, 57.000) atom6
@+ store atom6
@+ move (27.000, 54.000) atom2
@+ store atom2
@+ ry -0.78540 global
@+ rz 3.14159 atom2
@+ rz 3.14159 atom6
@+ ry 0.78540 global
@+ load [
    atom1
    atom3
]
@+ move [
    (4.000, 58.000) atom1
    (10.000, 58.000) atom3
]
@+ move [
    (40.000, 62.000) atom1
    (44.000, 62.000) atom3
]
@+ move [
    (37.000, 67.000) atom1
    (47.000, 67.000) atom3
]
@+ store [
    atom1
    atom3
]
@+ load atom2
@+ move (28.000, 55.000) atom2
@+ load [
    atom6
    atom7
]
@+ move [
    (25.000, 58.000) atom6
    (22.000, 58.000) atom7
]
@+ move [
    (52.000, 62.000) atom2
    (40.000, 82.000) atom6
    (32.000, 82.000) atom7
]
@+ move (49.000, 67.000) atom2
@+ store atom2
@+ move [
    (37.000, 87.000) atom6
    (35.000, 87.000) atom7
]
@+ store [
    atom6
    atom7
]
@+ load atom5
@+ move (16.000, 58.000) atom5
@+ move (32.000, 72.000) atom5
@+ move (35.000, 77.000) atom5
@+ store atom5
@+ cz zone_cz0
@+ load [
    atom0
    atom1
    atom2
    atom3
]
@+ move [
    (32.000, 62.000) atom0
    (40.000, 62.000) atom1
    (52.000, 62.000) atom2
    (44.000, 62.000) atom3
]
@+ move [
    (35.000, 62.000) atom0
    (37.000, 62.000) atom1
]
@+ load [
    atom4
    atom5
    atom6
    atom7
]
@+ move [
    (32.000, 62.000) atom0
    (40.000, 62.000) atom1
    (40.000, 72.000) atom4
    (32.000, 72.000) atom5
    (40.000, 82.000) atom6
    (32.000, 82.000) atom7
]
@+ move [
    (34.000, 52.000) atom0
    (37.000, 52.000) atom1
    (49.000, 52.000) atom2
    (43.000, 52.000) atom3
    (37.000, 55.000) atom4
    (34.000, 55.000) atom5
    (37.000, 58.000) atom6
    (34.000, 58.000) atom7
]
@+ move [
    (33.000, 51.000) atom0
    (36.000, 51.000) atom1
    (48.000, 51.000) atom2
    (42.000, 51.000) atom3
    (36.000, 55.000) atom4
    (33.000, 55.000) atom5
    (36.000, 58.000) atom6
    (33.000, 58.000) atom7
]
@+ store [
    atom0
    atom1
    atom2
    atom3
]
@+ move [
    (37.000, 55.000) atom4
    (34.000, 55.000) atom5
    (37.000, 58.000) atom6
    (34.000, 58.000) atom7
]
@+ move [
    (36.000, 54.000) atom4
    (33.000, 54.000) atom5
    (36.000, 57.000) atom6
    (33.000, 57.000) atom7
]
@+ store [
    atom4
    atom5
    atom6
    atom7
]
@+ ry -0.78540 global
@+ rz 3.14159 atom1
@+ rz 3.14159 atom3
@+ rz 3.14159 atom5
@+ rz 3.14159 atom7
@+ ry 0.78540 global

Note

The A* search in the placer of the routing-aware compiler is quite memory intensive. Right now, the maximum number of nodes considered in the A* search is limited to 50M. If this limit is hit, you will get an error message. You can freely adapt this limit by setting the argument max_nodes in the constructor of the RoutingAwareCompiler, see below.

Above, we have used the default settings for the compiler. However, the different stages of the compiler can also be configured, e.g., the deepening factor of the A*-placer:

1compiler = RoutingAwareCompiler(arch, deepening_factor = 0.6)
2
3circ = load(qc)
4code = compiler.compile(circ)
5print(code)
atom (0.000, 57.000) atom0
atom (3.000, 57.000) atom1
atom (6.000, 57.000) atom2
atom (9.000, 57.000) atom3
atom (12.000, 57.000) atom4
atom (15.000, 57.000) atom5
atom (18.000, 57.000) atom6
atom (21.000, 57.000) atom7
@+ ry -0.78540 global
@+ rz 3.14159 atom0
@+ rz 3.14159 atom1
@+ rz 3.14159 atom2
@+ rz 3.14159 atom3
@+ rz 3.14159 atom4
@+ rz 3.14159 atom5
@+ rz 3.14159 atom6
@+ rz 3.14159 atom7
@+ ry 0.78540 global
@+ load [
    atom0
    atom4
]
@+ move [
    (1.000, 58.000) atom0
    (13.000, 58.000) atom4
]
@+ move [
    (32.000, 62.000) atom0
    (40.000, 62.000) atom4
]
@+ move [
    (35.000, 67.000) atom0
    (37.000, 67.000) atom4
]
@+ store [
    atom0
    atom4
]
@+ cz zone_cz0
@+ load atom4
@+ move (40.000, 62.000) atom4
@+ move (34.000, 58.000) atom4
@+ move (33.000, 57.000) atom4
@+ store atom4
@+ ry -0.78540 global
@+ rz 3.14159 atom4
@+ ry 0.78540 global
@+ load atom2
@+ move (7.000, 58.000) atom2
@+ move (40.000, 62.000) atom2
@+ move (37.000, 67.000) atom2
@+ store atom2
@+ load [
    atom4
    atom6
]
@+ move [
    (34.000, 58.000) atom4
    (19.000, 58.000) atom6
]
@+ move [
    (40.000, 72.000) atom4
    (32.000, 72.000) atom6
]
@+ move [
    (37.000, 77.000) atom4
    (35.000, 77.000) atom6
]
@+ store [
    atom4
    atom6
]
@+ cz zone_cz0
@+ load atom2
@+ move (40.000, 62.000) atom2
@+ load atom6
@+ move (32.000, 72.000) atom6
@+ move [
    (28.000, 55.000) atom2
    (25.000, 58.000) atom6
]
@+ move (24.000, 57.000) atom6
@+ store atom6
@+ move (27.000, 54.000) atom2
@+ store atom2
@+ ry -0.78540 global
@+ rz 3.14159 atom2
@+ rz 3.14159 atom6
@+ ry 0.78540 global
@+ load [
    atom1
    atom3
]
@+ move [
    (4.000, 58.000) atom1
    (10.000, 58.000) atom3
]
@+ move [
    (40.000, 62.000) atom1
    (44.000, 62.000) atom3
]
@+ move [
    (37.000, 67.000) atom1
    (47.000, 67.000) atom3
]
@+ store [
    atom1
    atom3
]
@+ load atom2
@+ move (28.000, 55.000) atom2
@+ load [
    atom6
    atom7
]
@+ move [
    (25.000, 58.000) atom6
    (22.000, 58.000) atom7
]
@+ move [
    (52.000, 62.000) atom2
    (40.000, 82.000) atom6
    (32.000, 82.000) atom7
]
@+ move (49.000, 67.000) atom2
@+ store atom2
@+ move [
    (37.000, 87.000) atom6
    (35.000, 87.000) atom7
]
@+ store [
    atom6
    atom7
]
@+ load atom5
@+ move (16.000, 58.000) atom5
@+ move (32.000, 72.000) atom5
@+ move (35.000, 77.000) atom5
@+ store atom5
@+ cz zone_cz0
@+ load [
    atom0
    atom1
    atom2
    atom3
]
@+ move [
    (32.000, 62.000) atom0
    (40.000, 62.000) atom1
    (52.000, 62.000) atom2
    (44.000, 62.000) atom3
]
@+ move [
    (35.000, 62.000) atom0
    (37.000, 62.000) atom1
]
@+ load [
    atom4
    atom5
    atom6
    atom7
]
@+ move [
    (32.000, 62.000) atom0
    (40.000, 62.000) atom1
    (40.000, 72.000) atom4
    (32.000, 72.000) atom5
    (40.000, 82.000) atom6
    (32.000, 82.000) atom7
]
@+ move [
    (34.000, 52.000) atom0
    (37.000, 52.000) atom1
    (49.000, 52.000) atom2
    (43.000, 52.000) atom3
    (37.000, 55.000) atom4
    (34.000, 55.000) atom5
    (37.000, 58.000) atom6
    (34.000, 58.000) atom7
]
@+ move [
    (33.000, 51.000) atom0
    (36.000, 51.000) atom1
    (48.000, 51.000) atom2
    (42.000, 51.000) atom3
    (36.000, 55.000) atom4
    (33.000, 55.000) atom5
    (36.000, 58.000) atom6
    (33.000, 58.000) atom7
]
@+ store [
    atom0
    atom1
    atom2
    atom3
]
@+ move [
    (37.000, 55.000) atom4
    (34.000, 55.000) atom5
    (37.000, 58.000) atom6
    (34.000, 58.000) atom7
]
@+ move [
    (36.000, 54.000) atom4
    (33.000, 54.000) atom5
    (36.000, 57.000) atom6
    (33.000, 57.000) atom7
]
@+ store [
    atom4
    atom5
    atom6
    atom7
]
@+ ry -0.78540 global
@+ rz 3.14159 atom1
@+ rz 3.14159 atom3
@+ rz 3.14159 atom5
@+ rz 3.14159 atom7
@+ ry 0.78540 global