Verbatim Compilation with Amazon Braket
Run circuits on Amazon Braket without compiler modifications using verbatim compilation. Understand native gate sets and topology requirements.
Overview
By default, Amazon Braket compiles your circuit before sending it to hardware. The compiler transpiles abstract gates into native gates, optimizes sequences, and maps logical qubits to physical ones. Verbatim compilation bypasses this pipeline entirely: the circuit you write is the circuit that runs. This gives you precise control for benchmarking, noise characterization, and custom calibration experiments.
This tutorial walks through the full workflow: understanding what the compiler normally does, writing verbatim circuits for Rigetti and IonQ hardware, manually decomposing abstract gates into native form, validating connectivity constraints, and avoiding common mistakes that cause hardware rejections.
Why Verbatim Compilation Matters
When you let the compiler optimize, the hardware sees a different circuit than you wrote. Consider a concrete example. Suppose you design a five-gate circuit for a randomized benchmarking experiment:
H(0) -> CNOT(0,1) -> Rz(pi/4, 0) -> CNOT(0,1) -> H(0)
You intend to measure the fidelity of this exact sequence. But the Braket compiler processes your circuit before it reaches the QPU. It might:
- Decompose each
Hgate into the device’s native gates (for Rigetti, that becomesRZ(pi/2) RX(pi/2) RZ(pi/2)). - Fuse the trailing
RZ(pi/2)from the first Hadamard decomposition with theRz(pi/4)that follows, producing a singleRZ(3*pi/4). - Reorder gate operations to reduce circuit depth.
- Map your logical qubits 0 and 1 to physical qubits 7 and 12 because those happen to have better calibration today.
The circuit that actually runs on the QPU bears little resemblance to what you wrote. When you analyze the results, you cannot determine which gates actually executed, in what order, or on which physical qubits. Your benchmarking data becomes uninterpretable.
Verbatim mode eliminates this ambiguity. The gates you specify are the gates that run, on the qubits you specify, in the order you specify. This is essential for:
- Randomized benchmarking (standard and interleaved): You need to know the exact sequence of Cliffords applied to extract gate fidelities.
- Custom calibration experiments: You need to test specific gate sequences at specific rotation angles to characterize pulse-level behavior.
- Cross-resonance interaction studies: You need to apply specific two-qubit gate sequences without the compiler inserting additional single-qubit corrections.
- Native gate fidelity measurement: You need to run individual native gates in isolation, without the compiler merging them with neighboring operations.
Key constraints you must satisfy when using verbatim mode:
- Gates must be drawn from the device’s native gate set.
- Two-qubit gates must connect physically adjacent qubits (for devices with limited connectivity).
- No gate reordering or merging is performed, so your circuit must already be valid for the hardware topology.
What the Compiler Normally Does
To understand what verbatim mode bypasses, it helps to know the standard compilation pipeline. When you submit a circuit to Braket without verbatim mode, it passes through four stages:
Gate Decomposition
Abstract gates like H, T, CNOT, and Rx(theta) do not correspond to physical operations on any QPU. The compiler decomposes each abstract gate into an equivalent sequence of native gates supported by the target device. A Hadamard gate on a Rigetti device becomes three native gates: RZ(pi/2) RX(pi/2) RZ(pi/2). The same Hadamard on an IonQ device becomes a different native sequence using GPi and GPi2 gates.
Qubit Mapping
Your circuit uses logical qubits numbered 0, 1, 2, and so on. The compiler maps these to physical qubits on the device. Logical qubit 0 might get assigned to physical qubit 7 because that qubit currently has better T1 coherence time or lower gate error rates. This mapping changes between calibration cycles, so the same circuit may run on different physical qubits on different days.
Gate Optimization
The compiler looks for optimization opportunities. Two consecutive single-qubit rotations around the same axis get merged into one rotation. An RZ(pi/4) followed by RZ(pi/2) becomes a single RZ(3*pi/4). Gate pairs that cancel (like two consecutive X gates) get removed entirely. These optimizations reduce circuit depth and total gate count, which reduces the accumulated error on noisy hardware.
Routing
If your circuit applies a two-qubit gate between qubits that are not physically adjacent on the device, the compiler inserts SWAP gates to move the qubit states to neighboring positions. A single CNOT between non-adjacent qubits can expand into three or more two-qubit gates after routing. This is the most expensive compilation step in terms of added noise.
Verbatim mode skips all four stages. Your circuit goes directly to the QPU control system without modification. This means you are fully responsible for ensuring correctness: native gates only, valid qubit pairs, and a gate ordering that respects the hardware topology.
Native Gate Sets by Provider
Each hardware provider exposes a different native gate set, determined by the underlying physics of the qubit technology. Understanding what each gate does physically helps you write correct verbatim circuits.
Rigetti (Superconducting Qubits)
Rigetti devices use transmon qubits on a grid topology. The native gate set consists of:
| Gate | Parameters | Physical Operation |
|---|---|---|
RZ(theta) | theta: rotation angle | Z-axis rotation. This is a virtual gate implemented by shifting the reference frame of subsequent pulses rather than applying an actual microwave pulse. It has zero duration and effectively zero error. |
RX(pi/2) | fixed angle only | Half-turn rotation around the X axis. Implemented as a single calibrated microwave pulse. This is the only single-qubit gate that requires a physical pulse on Rigetti hardware. |
CZ | none | Controlled-Z entangling gate between adjacent qubits. Uses a flux-tuning interaction where one qubit’s frequency is temporarily shifted to bring the |
CPHASESHIFT | theta: phase angle | Controlled phase shift, a generalization of CZ. Available on some Rigetti devices. |
Because RZ is virtual (zero error, zero duration), you should prefer circuit constructions that maximize RZ usage and minimize RX counts.
from braket.aws import AwsDevice
# Rigetti native gates: RZ, RX(pi/2), CZ, CPHASESHIFT
# IonQ native gates: GPi, GPi2, MS (Molmer-Sorensen)
# OQC native gates: RZ, SX, ECR
# Inspect programmatically
# device = AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2")
# print(device.properties.paradigm.nativeGateSet)
IonQ (Trapped Ions)
IonQ devices use trapped ytterbium ions held in a linear RF trap. The native gate set consists of:
| Gate | Parameters | Physical Operation |
|---|---|---|
GPi(phi) | phi: rotation angle | A single-qubit rotation by pi (a full half-turn) around an axis in the X-Y plane of the Bloch sphere, where phi specifies the azimuthal angle of that axis. Implemented with a pair of Raman laser beams. |
GPi2(phi) | phi: rotation angle | A single-qubit rotation by pi/2 (a quarter-turn) around the same X-Y plane axis specified by phi. Also implemented with Raman laser beams, but with half the pulse area. |
MS(phi0, phi1, theta) | phi0, phi1: axis angles; theta: entangling angle | The Molmer-Sorensen entangling gate. Uses laser-driven coupling through shared motional modes of the ion chain to create entanglement between two ions. The angles phi0 and phi1 set the interaction axes for each qubit, and theta controls the degree of entanglement (theta = pi/4 gives a maximally entangling gate). |
A critical difference from superconducting hardware: trapped-ion qubits are all-to-all connected. The MS gate can entangle any pair of ions in the trap without routing or SWAP insertion. This simplifies verbatim circuit construction significantly, since you never need to worry about qubit adjacency.
Native Gate Summary Table
| Provider | Technology | Single-Qubit Gates | Entangling Gate | Connectivity |
|---|---|---|---|---|
| Rigetti | Superconducting transmon | RZ(theta), RX(pi/2) | CZ | Grid (nearest-neighbor) |
| IonQ | Trapped ion | GPi(phi), GPi2(phi) | MS(phi0, phi1, theta) | All-to-all |
| OQC | Superconducting transmon | RZ(theta), SX | ECR | Limited (device-specific) |
Translating Abstract Gates to Native Gates
When writing verbatim circuits, you must manually decompose abstract gates into the target device’s native gates. The compiler normally handles this, but in verbatim mode, you are responsible.
Hadamard on Rigetti
The Hadamard gate maps |0> to |+> and |1> to |->. In matrix form:
H = (1/sqrt(2)) * [[1, 1],
[1, -1]]
On Rigetti hardware, the decomposition is:
H = RZ(pi/2) * RX(pi/2) * RZ(pi/2)
Reading right to left (as matrix multiplication): rotate pi/2 around Z, then pi/2 around X, then pi/2 around Z. In circuit code:
from braket.circuits import Circuit
import numpy as np
# Hadamard decomposed into Rigetti native gates
hadamard_rigetti = (
Circuit()
.rz(0, np.pi / 2)
.rx(0, np.pi / 2)
.rz(0, np.pi / 2)
)
CNOT on Rigetti
The CNOT (controlled-X) gate is not native on Rigetti. It decomposes into single-qubit rotations around the CZ gate:
CNOT(control, target) = [I x RX(-pi/2)] * CZ * [I x RX(pi/2)] * [I x RZ(pi)]
In practice, the decomposition for a CNOT with control qubit 0 and target qubit 1 on Rigetti hardware is:
# CNOT decomposed into Rigetti native gates
cnot_rigetti = (
Circuit()
.rz(1, np.pi) # RZ(pi) on target
.rx(1, np.pi / 2) # RX(pi/2) on target
.cz(0, 1) # CZ on adjacent pair
.rx(1, -np.pi / 2) # RX(-pi/2) on target
)
Hadamard on IonQ
On IonQ hardware, the Hadamard decomposes using GPi and GPi2:
# Hadamard decomposed into IonQ native gates
# H = GPi2(pi/2) * GPi(0)
hadamard_ionq = (
Circuit()
.gpi(0, 0.0)
.gpi2(0, np.pi / 2)
)
The GPi(0) applies a pi rotation around the X axis (equivalent to a Pauli X up to global phase), and the subsequent GPi2(pi/2) applies a pi/2 rotation around the Y axis. Together they produce the Hadamard transformation.
Writing a Verbatim Circuit for Rigetti
Rigetti devices use RZ, RX(pi/2), and CZ as their native gate set with a grid topology.
from braket.circuits import Circuit
# Verbatim circuit for Rigetti -- all gates must be native
# GridQubit (0,0) and (0,1) are adjacent on Ankaa-2
circuit = (
Circuit()
.add_verbatim_box(
Circuit()
.rx(0, 1.5707963) # RX(pi/2) on qubit 0
.rz(0, 3.14159) # RZ(pi) on qubit 0
.rx(1, 1.5707963) # RX(pi/2) on qubit 1
.cz(0, 1) # CZ on adjacent pair
.rz(0, -1.5707963)
.rx(1, -1.5707963)
)
.probability(target=[0, 1])
)
print(circuit)
The add_verbatim_box wrapper signals to Braket that no compilation should be applied to the enclosed gates.
Note that you can mix verbatim and compiled sections in a single circuit. Gates outside the verbatim box go through normal compilation, while gates inside are sent to hardware exactly as written. This is useful when only part of your circuit requires gate-level precision.
Writing a Verbatim Circuit for IonQ
IonQ trapped-ion devices use GPi, GPi2, and the MS (Molmer-Sorensen) entangling gate. Because ions in a trap are all-to-all connected, there are no adjacency constraints.
from braket.circuits import Circuit
from braket.circuits.gates import GPi, GPi2, MS
import numpy as np
# IonQ verbatim circuit
ion_circuit = (
Circuit()
.add_verbatim_box(
Circuit()
.gpi2(0, np.pi / 2) # GPi2 gate on qubit 0
.gpi2(1, 0.0) # GPi2 gate on qubit 1
.ms(0, 1, 0.0, 0.0, np.pi / 4) # MS entangling gate
.gpi2(0, -np.pi / 2)
)
.probability(target=[0, 1])
)
print(ion_circuit)
The MS gate with theta = pi/4 is a maximally entangling gate, producing a Bell state when applied to two qubits in the |00> state (with appropriate single-qubit rotations). Adjusting theta gives partial entanglement, which is useful for variational circuits where you want to parameterize the entangling strength.
Verifying Verbatim Constraints
Before submitting a verbatim circuit to hardware, you must verify that it satisfies all device constraints. The hardware will reject circuits that violate these constraints at runtime, costing you queue time and potentially task charges.
Check 1: All Gates Are in the Native Gate Set
Query the device properties to get the exact set of supported native gates:
from braket.aws import AwsDevice
device = AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2")
native_gates = device.properties.paradigm.nativeGateSet
print("Native gates:", native_gates)
# Expected output for Rigetti: ['cz', 'rx', 'rz', 'cphaseshift']
Compare every gate in your circuit against this set before submission.
Check 2: Two-Qubit Gates Target Adjacent Qubits
For devices with limited connectivity (Rigetti, OQC), every two-qubit gate must target a pair of qubits that share a physical connection. Retrieve the full connectivity graph programmatically:
# Get the device topology as a networkx-compatible structure
topology = device.topology_graph
print("Edges:", list(topology.edges))
# Returns all valid qubit pairs, e.g., [(0, 1), (1, 2), (0, 5), ...]
# Check if a specific pair is connected
def validate_connectivity(circuit_pairs, topology):
"""Verify all two-qubit gate pairs exist in the device topology."""
edges = set(topology.edges) | {(b, a) for a, b in topology.edges}
invalid = []
for q0, q1 in circuit_pairs:
if (q0, q1) not in edges:
invalid.append((q0, q1))
return invalid
# Example: validate pairs used in your circuit
invalid_pairs = validate_connectivity([(0, 1), (0, 2), (5, 6)], topology)
if invalid_pairs:
print(f"ERROR: Non-adjacent pairs: {invalid_pairs}")
else:
print("All pairs are valid.")
Check 3: Qubit Indices Exist on the Device
Not all integer qubit indices correspond to active qubits on the device. Some qubits may be disabled due to calibration issues. Check the set of available qubits:
# Get the list of active qubits
active_qubits = device.properties.paradigm.qubitCount
connectivity = device.topology_graph
active_nodes = set(connectivity.nodes)
print("Active qubits:", sorted(active_nodes))
# Verify your circuit qubits are all active
circuit_qubits = {0, 1, 5, 6}
missing = circuit_qubits - active_nodes
if missing:
print(f"WARNING: Qubits {missing} are not active on this device")
Validating Qubit Connectivity
Before submitting to hardware, verify your qubit pairs are adjacent for the target device.
# Rigetti Ankaa-2 adjacency -- example subset
# Full connectivity map available via device.topology_graph
ankaa2_edges = [
(0, 1), (1, 2), (2, 3),
(0, 5), (1, 6), (2, 7), (3, 8),
(5, 6), (6, 7), (7, 8),
]
def is_adjacent(q0: int, q1: int, edges) -> bool:
return (q0, q1) in edges or (q1, q0) in edges
print(is_adjacent(0, 1, ankaa2_edges)) # True
print(is_adjacent(0, 2, ankaa2_edges)) # False -- not adjacent
For IonQ devices, you can skip adjacency checks entirely. All qubit pairs are valid targets for the MS gate.
Submitting to Hardware
from braket.aws import AwsDevice, AwsQuantumTask
# Uncomment to run on real hardware
# device = AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2")
# task = device.run(circuit, shots=1000)
# result = task.result()
# print(result.measurement_probabilities)
# Local simulation for development (ignores verbatim constraints)
from braket.devices import LocalSimulator
sim = LocalSimulator()
task = sim.run(
Circuit().rx(0, 1.5707963).cz(0, 1).probability(target=[0, 1]),
shots=1000,
)
print(task.result().measurement_probabilities)
Polling for Task Completion
Hardware tasks do not complete instantly. They enter a queue, wait for the QPU to become available, execute, and then return results. Use the task ID to poll for status:
from braket.aws import AwsDevice, AwsQuantumTask
import time
# Submit the task
# device = AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2")
# task = device.run(circuit, shots=1000)
# task_id = task.id
# print(f"Task submitted: {task_id}")
# Poll for completion
# while task.state() not in ["COMPLETED", "FAILED", "CANCELLED"]:
# print(f"Status: {task.state()}")
# time.sleep(10)
# Handle the result
# if task.state() == "COMPLETED":
# result = task.result()
# print("Measurement probabilities:", result.measurement_probabilities)
# print("Measurement counts:", result.measurement_counts)
# elif task.state() == "FAILED":
# print("Task FAILED. Possible causes:")
# print(" - Gate not in native set")
# print(" - Two-qubit gate on non-adjacent qubits")
# print(" - Qubit index does not exist on device")
# print(" - Device went offline during execution")
# print(f"Failure reason: {task.metadata().get('failureReason', 'unknown')}")
# elif task.state() == "CANCELLED":
# print("Task was cancelled.")
A FAILED state on a verbatim circuit almost always means your circuit violates a hardware constraint. The most common causes are using a non-native gate, targeting non-adjacent qubits with a two-qubit gate, or referencing a qubit index that is not active on the device. Check the failure reason in the task metadata for specifics.
Recovering a Task by ID
If your Python session disconnects, you can recover a previously submitted task using its ARN:
# Recover a task from a previous session
# recovered_task = AwsQuantumTask(arn="arn:aws:braket:us-west-1:123456789:quantum-task/abc-123")
# print(recovered_task.state())
# if recovered_task.state() == "COMPLETED":
# result = recovered_task.result()
When to Use Verbatim Compilation
| Use Case | Verbatim? | Rationale |
|---|---|---|
| Production VQE or QAOA | No | Let the compiler optimize gate count and depth to minimize accumulated noise. The compiler’s optimizations directly improve variational algorithm performance. |
| Standard randomized benchmarking | Yes | You must apply a known sequence of Clifford gates and measure the decay of fidelity with sequence length. Any compiler reordering or merging invalidates the benchmark. |
| Interleaved randomized benchmarking | Yes | You interleave a specific gate of interest between random Cliffords. The interleaved gate must execute exactly as specified to isolate its error rate. |
| Noise characterization (T1, T2) | Yes | Specific gate sequences with known idle times are needed to extract coherence parameters. Compiler optimization would change the timing structure. |
| Custom gate calibration | Yes | You are testing specific pulse sequences at specific rotation angles. The compiler must not substitute equivalent gate sequences. |
| Cross-resonance calibration | Yes | Characterizing the cross-resonance interaction requires applying the CZ or CR gate in isolation, without single-qubit corrections the compiler might add. |
| Native gate fidelity measurement | Yes | Measuring the error rate of a single native gate (e.g., CZ fidelity) requires running that gate without the compiler merging it with adjacent operations. |
| General algorithm development | No | The compiler handles gate decomposition, qubit mapping, and routing. Manual optimization is unnecessary and usually produces worse results than the compiler. |
| Quantum error correction experiments | Sometimes | Syndrome extraction circuits may need precise gate ordering, but the compiler can often optimize ancilla preparation without affecting the code distance. Evaluate per circuit. |
Common Mistakes
These are the errors that most frequently cause verbatim circuit failures or produce misleading results.
Using a Non-Native Gate
If you include a gate that is not in the device’s native set, the hardware rejects the circuit at submission time. The error message is often opaque. For example, applying an H gate inside a verbatim box on Rigetti hardware fails because H is not a native gate. You must decompose it into RZ(pi/2) RX(pi/2) RZ(pi/2) yourself.
# WRONG: H is not a Rigetti native gate
bad_circuit = Circuit().add_verbatim_box(
Circuit().h(0) # This will fail on Rigetti hardware
)
# CORRECT: Decompose H into native gates
good_circuit = Circuit().add_verbatim_box(
Circuit()
.rz(0, np.pi / 2)
.rx(0, np.pi / 2)
.rz(0, np.pi / 2)
)
Targeting Non-Adjacent Qubits on Rigetti
The CZ gate on Rigetti requires physically adjacent qubits. If qubits 0 and 2 are not directly connected on the device topology, applying CZ(0, 2) inside a verbatim box causes a hardware rejection. In normal compilation mode, the compiler would insert SWAP gates to route around this. In verbatim mode, you must either choose adjacent qubits or manually insert the SWAP decomposition.
Trusting the Local Simulator for Constraint Validation
The Braket LocalSimulator does not enforce verbatim constraints. It happily runs circuits with non-native gates, non-adjacent qubit pairs, and arbitrary qubit indices. A circuit that runs perfectly on the local simulator can fail immediately on hardware. Always validate your circuit against the device’s native gate set and topology before submitting to a QPU. Use the programmatic checks shown in the “Verifying Verbatim Constraints” section above.
Confusing Angle Conventions
IonQ’s GPi and GPi2 gates take angles in radians in the Braket SDK. However, some IonQ documentation and papers express these angles in units of turns (multiples of 2*pi) or units of pi. If you are translating a circuit from an IonQ research paper, verify the angle convention before writing your verbatim circuit. A GPi(0.5) in a paper using pi-units means GPi(0.5 * pi) in Braket, which is GPi(1.5707963...).
Similarly, Rigetti’s RX gate in verbatim mode only accepts pi/2 and -pi/2 as valid rotation angles on most devices. Arbitrary RX(theta) values are not supported, even though the Braket SDK does not prevent you from specifying them. The circuit will fail at the hardware level.
Assuming Qubit 0 Is Well-Calibrated
In normal compilation mode, the compiler maps your logical qubits to whichever physical qubits currently have the best calibration data. In verbatim mode, qubit 0 in your circuit is physical qubit 0 on the device. Physical qubit 0 might have significantly worse error rates than other qubits. Before choosing qubit indices for a verbatim circuit, check the device’s latest calibration data:
# Check calibration data before choosing qubits
# device = AwsDevice("arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2")
# calibration = device.properties.provider
# print(calibration)
# Look for single-qubit gate fidelities, T1/T2 times, and readout fidelities
# Choose qubits with the best metrics for your experiment
Forgetting to Account for Measurement Errors
Verbatim mode controls gate execution, but measurement errors still apply. If you are running a benchmarking experiment, consider that readout assignment errors (where |0> is misread as |1> or vice versa) can bias your results independently of gate errors. For precise experiments, apply readout error mitigation as a classical post-processing step, or include calibration circuits that measure the readout confusion matrix.
Summary
Verbatim compilation removes the Braket compiler from the execution path, giving you exact control over which gates run and on which qubits. To use it effectively:
- Understand what the compiler normally does (decomposition, mapping, optimization, routing) so you know what you are bypassing.
- Learn the native gate set for your target device and understand the physical operation each gate performs.
- Manually decompose abstract gates into native form, since the compiler will not do it for you.
- Validate all constraints before submission: native gates only, valid qubit pairs, active qubit indices.
- Use
add_verbatim_boxto mark the circuit region that must not be modified. - Poll for task completion and handle FAILED states, which almost always indicate a constraint violation.
- Reserve verbatim mode for experiments where gate-level precision is required. For general algorithm work, the compiler produces better results than manual optimization.
Was this tutorial helpful?