Amazon Braket Hello World
Run your first quantum circuit with Amazon Braket. Build a Bell state on the local simulator, then learn how to submit jobs to real quantum hardware on AWS.
Amazon Braket is AWS’s managed quantum computing service. Its key value proposition is multi-hardware access through a single SDK. While Qiskit connects you to IBM hardware and Cirq targets Google hardware, Braket gives you unified access to quantum processors built on fundamentally different qubit technologies:
- IonQ (trapped ions): long coherence times and all-to-all qubit connectivity, meaning any qubit can interact directly with any other qubit. This eliminates the need for SWAP gates that plague limited-connectivity hardware.
- IQM and Rigetti (superconducting qubits): fast gate operations (nanosecond scale), but shorter coherence times and limited connectivity between qubits. Circuits that require non-adjacent qubit interactions need extra SWAP gates.
- QuEra (neutral atoms): particularly well suited for analog quantum simulation, where you define a Hamiltonian and let the system evolve rather than specifying individual gates.
This multi-hardware model matters in practice. Different algorithms perform better on different qubit technologies, and Braket lets you benchmark the same circuit across hardware types with minimal code changes. You swap out a single device ARN string and rerun your code.
You can run the local simulator for free without an AWS account. Real hardware tasks are billed per shot, with pricing that varies by device.
Installation
pip install amazon-braket-sdk amazon-braket-default-simulator
The amazon-braket-sdk package provides the Circuit class and device interfaces. The amazon-braket-default-simulator package provides local simulators (statevector, density matrix) so you can develop and test without any AWS credentials.
Your First Circuit
Braket circuits use a fluent interface pattern (also called method chaining). Each gate method modifies the circuit in place and then returns the circuit object itself, so you can chain operations on a single line or stack them across multiple lines.
from braket.circuits import Circuit
from braket.devices import LocalSimulator
# Build a Bell state circuit
circuit = Circuit()
circuit.h(0) # Hadamard on qubit 0
circuit.cnot(0, 1) # CNOT: control=0, target=1
print(circuit)
The multi-line style above is identical to the chained style:
circuit = Circuit().h(0).cnot(0, 1)
Both produce the exact same circuit. Use whichever reads more clearly for your use case. For short circuits, chaining fits neatly on one line. For longer circuits with comments explaining each gate, the multi-line style is easier to follow.
Reading the Circuit Diagram
When you print a Braket circuit, you get an ASCII diagram:
T : |0|1|
q0 : -H-C-
|
q1 : ---X-
T : |0|1|
Here is how to read it:
T : |0|1|shows the time steps (called “Moments”). Each numbered column represents a parallel gate layer. Moment 0 contains the Hadamard; moment 1 contains the CNOT.q0,q1label the qubit wires. Read each wire left to right to see the sequence of gates applied to that qubit.His the Hadamard gate.CandXconnected by a vertical line represent the CNOT (controlled-NOT), whereCis the control qubit andXis the target.- Gates in the same moment column execute in the same time step. The Braket compiler automatically groups independent gates into parallel moments when possible.
Running on the Local Simulator
from braket.circuits import Circuit
from braket.devices import LocalSimulator
circuit = Circuit().h(0).cnot(0, 1)
device = LocalSimulator()
task = device.run(circuit, shots=1000)
result = task.result()
print(result.measurement_counts)
# Counter({'00': 503, '11': 497})
The only outcomes are 00 and 11 in roughly equal proportions. That is the Bell state: the two qubits are maximally entangled, so they always produce the same measurement outcome. You never see 01 or 10. The roughly 50/50 split between 00 and 11 reflects the inherent quantum randomness of the superposition.
Braket vs Other Frameworks
If you are coming from another quantum framework, this comparison highlights the practical differences:
| Braket | Qiskit | Cirq | |
|---|---|---|---|
| Hardware access | IonQ, IQM, Rigetti, QuEra | IBM Quantum | Google Sycamore |
| Gate parameter order | qubit first, then angle | angle first, then qubit | gate then qubit |
| Free tier | Local simulator, no AWS account needed | Simulators + free IBM hardware | Local simulator |
| Variational algorithms | Hybrid Jobs | Qiskit Runtime | Local only |
| Best for | Multi-hardware comparison, AWS integration | IBM hardware, education | Google hardware, research |
The most common source of confusion when switching between frameworks is the gate parameter ordering, covered in detail below.
Gate Parameter Ordering
Braket gate methods take the qubit index first, then any rotation angles. This is the opposite of Qiskit, where angles come first. Getting this wrong produces a circuit that compiles without errors but applies the wrong rotation to the wrong qubit.
Side-by-side comparison for an RX(pi/2) gate on qubit 0:
# Braket: qubit first, then angle
circuit.rx(0, 1.5708) # RX(pi/2) on qubit 0
# Qiskit: angle first, then qubit
circuit.rx(1.5708, 0) # RX(pi/2) on qubit 0
If you accidentally write circuit.rx(1.5708, 0) in Braket, the SDK interprets 1.5708 as a qubit index (rounded to qubit 1) and 0 as the angle. You get zero rotation on qubit 1, which is a silent, hard-to-debug error. Always double-check parameter order when porting circuits between frameworks.
More Gates
circuit = Circuit()
circuit.x(0) # Pauli-X (NOT)
circuit.y(1) # Pauli-Y
circuit.z(2) # Pauli-Z
circuit.s(0) # S gate
circuit.t(1) # T gate
circuit.rx(0, 1.5708) # Rx(pi/2) - qubit first, then angle
circuit.ry(1, 0.7854) # Ry(pi/4)
circuit.rz(2, 3.1416) # Rz(pi)
circuit.cz(0, 1) # Controlled-Z
circuit.swap(0, 1) # SWAP
circuit.ccnot(0, 1, 2) # Toffoli
All gate methods follow the same convention: qubit indices first, then any parameters (angles, phases). Multi-qubit gates list the control qubits before the target qubit.
Statevector (No Shots)
Pass shots=0 to get the full statevector instead of sampled measurements. This runs a deterministic simulation that returns the exact quantum state, with no sampling noise:
circuit = Circuit().h(0).cnot(0, 1)
device = LocalSimulator()
result = device.run(circuit, shots=0).result()
# result.values[0] is the statevector
sv = result.values[0]
print(sv)
# [ 0.70710678+0.j 0. +0.j 0. +0.j 0.70710678+0.j]
The statevector has 2^n entries for n qubits. In this Bell state, the non-zero amplitudes at indices 0 (|00>) and 3 (|11>) each have magnitude 1/sqrt(2), confirming equal probability of measuring either outcome.
Statevector mode is useful for verifying circuit correctness, debugging algorithms, and computing expectation values analytically. Keep in mind that statevector simulation requires all operations to be unitary (standard quantum gates). If your circuit includes noise channels, you must use the density matrix simulator instead.
Parameterised Circuits
from braket.circuits import Circuit, FreeParameter
angle = FreeParameter('angle')
circuit = Circuit().h(0).rx(0, angle).cnot(0, 1)
device = LocalSimulator()
result = device.run(circuit, shots=500, inputs={'angle': 1.5708}).result()
print(result.measurement_counts)
FreeParameter creates a symbolic placeholder that you bind to a concrete value at runtime through the inputs dictionary. This is essential for variational algorithms like VQE and QAOA, where a classical optimizer repeatedly evaluates circuits at different parameter values. By defining the circuit once and rebinding parameters, you avoid the overhead of rebuilding the circuit object on each iteration.
AWS Cloud Execution
Running circuits on AWS (either managed simulators or real hardware) requires several setup steps before your first task:
- AWS account: Create one at aws.amazon.com if you do not already have one.
- Enable Braket: Open the Amazon Braket console in your region to activate the service.
- IAM credentials: Configure AWS credentials on your machine using
aws configureor by attaching an IAM role if you are running on an EC2 instance or SageMaker notebook. - S3 bucket: Create an S3 bucket (or designate an existing one) to store task results. Braket writes results to S3 because tasks run asynchronously; your circuit enters a queue and executes when the device is available, which can take seconds for simulators or minutes for real hardware.
import boto3
from braket.aws import AwsDevice
# AWS credentials must be configured (aws configure or IAM role)
device = AwsDevice("arn:aws:braket:::device/quantum-simulator/amazon/sv1")
circuit = Circuit().h(0).cnot(0, 1)
task = device.run(
circuit,
s3_destination_folder=('your-braket-bucket', 'results'),
shots=100
)
print(task.id) # task ARN
print(task.state()) # QUEUED, RUNNING, COMPLETED, FAILED
result = task.result()
print(result.measurement_counts)
Checking Task Status
Because tasks run asynchronously, you may want to poll for completion rather than blocking on task.result():
import time
task = device.run(circuit, s3_destination_folder=('your-braket-bucket', 'results'), shots=100)
while task.state() not in ['COMPLETED', 'FAILED', 'CANCELLED']:
print(f"Status: {task.state()}")
time.sleep(10)
if task.state() == 'COMPLETED':
result = task.result()
print(result.measurement_counts)
else:
print(f"Task ended with status: {task.state()}")
Calling task.result() blocks until the task completes, which is simpler for interactive use. The polling approach is better for scripts where you want to log progress or implement timeouts.
Real Quantum Hardware
from braket.aws import AwsDevice
# IonQ Aria (trapped ion)
device = AwsDevice("arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1")
# IQM Garnet (superconducting)
device = AwsDevice("arn:aws:braket:eu-north-1::device/qpu/iqm/Garnet")
task = device.run(circuit, shots=100)
result = task.result()
Tasks on real hardware queue and run when the machine is available. Small jobs typically take a few minutes. Note that each hardware provider has its own pricing, availability windows, and native gate set. Braket automatically transpiles your circuit to the target device’s native gates, but understanding the native gate set helps you write circuits that compile more efficiently.
Reading Results
The result object provides three views of the measurement data, each suited to different analysis needs:
result = task.result()
result.measurement_counts # dict: bitstring -> count
result.measurements # numpy array, shape (shots, n_qubits)
result.measurement_probabilities # dict: bitstring -> probability
Here is what each attribute gives you:
-
measurement_counts: A dictionary (PythonCounter) mapping bitstrings to integer counts. For example,{'00': 503, '11': 497}means bitstring00appeared 503 times out of 1000 shots. This is the most commonly used format for sampling experiments, histogram plots, and quick sanity checks. -
measurements: A raw 2D NumPy array with shape(shots, n_qubits). Each row is a single shot, and each column is a qubit measurement (0 or 1). This format is useful when you need to post-process individual shots, compute correlations between specific qubits, or feed raw data into classical machine learning pipelines. -
measurement_probabilities: A dictionary mapping bitstrings to float probabilities (normalized so they sum to 1.0). For example,{'00': 0.503, '11': 0.497}. This is useful when you want relative frequencies directly, for instance when computing expectation values or comparing against theoretical predictions.
Local Noise Simulation
Before running circuits on expensive real hardware, you can estimate how much noise affects your results using the local density matrix simulator. This helps you decide whether your circuit is shallow enough to produce meaningful results on a given device, or whether you need error mitigation techniques.
The density matrix simulator (braket_dm) propagates noise channels through the circuit, modeling realistic errors. Common noise channels include:
- BitFlip: Applies a random X (bit-flip) error with a given probability. Models readout errors and classical bit corruption.
- PhaseFlip: Applies a random Z (phase-flip) error. Models dephasing, where the relative phase between
|0>and|1>is randomly flipped. - Depolarizing: Applies a random Pauli error (X, Y, or Z) with equal probability. This is the most commonly used noise model because it captures generic gate errors.
- AmplitudeDamping: Models energy relaxation (T1 decay), where an excited
|1>state spontaneously decays to|0>. This is a physically motivated noise model for superconducting qubits.
from braket.circuits import Circuit
from braket.circuits.noise import BitFlip, Depolarizing
from braket.devices import LocalSimulator
circuit = Circuit().h(0)
circuit.bit_flip(0, 0.01) # 1% bit flip error
circuit.cnot(0, 1)
circuit.depolarizing(0, 0.005)
device = LocalSimulator('braket_dm')
result = device.run(circuit, shots=1000).result()
print(result.measurement_counts)
The string 'braket_dm' tells the LocalSimulator to use the density matrix backend instead of the default statevector backend. Density matrix simulation is slower (memory scales as 2^(2n) instead of 2^n), but it can represent mixed states and noise channels that statevector simulation cannot.
Common Mistakes
These are the issues that trip up most Braket beginners:
1. Reversed gate parameter order. Braket takes qubit first, then angle: circuit.rx(0, 1.5708). If you write it in Qiskit order (circuit.rx(1.5708, 0)), the SDK silently interprets the angle as a qubit index and the qubit as the angle. Always verify parameter order when porting code between frameworks.
2. Submitting to real hardware without an S3 bucket configured. All AWS task results are written to S3. If you forget the s3_destination_folder parameter, or if the bucket does not exist, the task submission fails. Create your S3 bucket before your first hardware submission.
3. Forgetting that the local simulator ignores hardware connectivity constraints. The local simulator treats all qubits as fully connected. A CNOT between any two qubits works fine locally, but the same circuit may require extra SWAP gates on real hardware with limited connectivity (like superconducting devices). If you want to validate connectivity, check the device’s topology using device.properties before submitting.
4. Using shots=0 with noise channels. Statevector mode (shots=0) requires all operations to be unitary. Noise channels (BitFlip, Depolarizing, etc.) are non-unitary, so they break statevector simulation. If your circuit includes noise, use the density matrix simulator (LocalSimulator('braket_dm')) with shots > 0.
What to Try Next
- Try the Amazon Braket examples on GitHub
- Read the Braket Reference Dictionary for a full API cheat sheet
- Explore Braket Hybrid Jobs for long-running variational algorithms with classical processing
Was this tutorial helpful?