Tutorials
qcr:2606.69705.1

Anatomy of Quantum Circuits and Tasks in Braket

This tutorial takes a deeper look under the hood of Amazon Braket, explaining the anatomy of the objects that make up a quantum program and how a circuit becomes a result. It dissects the Circuit object and its building blocks, the Instruction (a gate applied to specific qubits), the Gate and Moment abstractions, and the qubit and result-type containers, showing how they compose into a full program. It then explains the lifecycle of a quantum task: how a circuit is serialized, sent to a device (simulator or QPU), queued, executed, and returned as a structured result. A key focus is result types, the different quantities you can ask a device to return, such as measurement samples, probabilities, expectation values, variances, and the full state vector or density matrix where supported, and how requesting the right result type shapes both what you get back and how the device runs your circuit. By making these internals explicit, the tutorial gives intermediate users the mental model they need to write more sophisticated Braket programs and to debug them when results are not what they expect.
Qubit
Circuit-based
Uploaded 4 days ago
11
Views
GitHub582
Citing this entry? Use this QCR ID
Uploaded by
QL
QCR Librarian

Overview

amazon-braket/amazon-braket-examples
582261
In [ ]:
# --- Setup cell added by QCR (not part of the original tutorial) ---
# Source: amazon-braket/amazon-braket-examples @ 0c0818f315479aab9deebed7e7ed7533ac581923, Apache License 2.0.
# Installs the example's dependencies. If a later cell still reports a missing
# package, restart the runtime/kernel and run again from the top.
%pip install -q amazon-braket-sdk==1.117.3 matplotlib

Anatomy of Quantum Circuits and Quantum Tasks in Amazon Braket

In [1]:
# Use Braket SDK Cost Tracking to estimate the cost to run this example
from braket.tracking import Tracker

t = Tracker().start()

In this tutorial we discuss in detail the anatomy of quantum circuits in Amazon Braket's SDK. Specifically, we learn how to build (parametrized) circuits and display them graphically, how to append circuits to each other, and discuss the associated circuit depth and circuit size. Finally we show how to execute our circuit on a device of our choice (defining a quantum task). We then learn how to efficiently track, log, recover or cancel such a quantum task.

IMPORT STATEMENTS

First we import some modules we will need.

In [2]:
# general imports
import asyncio

import matplotlib.pyplot as plt
import numpy as np

# magic word for producing visualizations in notebook
%matplotlib inline
import logging
import string
from datetime import datetime

# AWS imports: Import Braket SDK modules
from braket.aws import AwsDevice, AwsQuantumTask
from braket.circuits import Circuit, Gate, circuit
from braket.circuits.observables import H, X, Y, Z
from braket.devices import Devices, LocalSimulator
from braket.parametric import FreeParameter

CIRCUIT DEFINITION

Let us get started a sample circuit for four qubits (labelled q0, q1, q2 and q3) consisting of standard single-qubit Hadamard gates and two-qubit CNOT gates; for a full list of available gates see below. We can then visualize our circuit by simply calling the print function.

In [3]:
# define circuit with 4 qubits
my_circuit = Circuit().h(range(4)).cnot(control=0, target=2).cnot(control=1, target=3)
print(my_circuit)
T  : │  0  │     1     │
      ┌───┐             
q0 : ─┤ H ├───●─────────
      └───┘   │         
      ┌───┐   │         
q1 : ─┤ H ├───┼─────●───
      └───┘   │     │   
      ┌───┐ ┌─┴─┐   │   
q2 : ─┤ H ├─┤ X ├───┼───
      └───┘ └───┘   │   
      ┌───┐       ┌─┴─┐ 
q3 : ─┤ H ├───────┤ X ├─
      └───┘       └───┘ 
T  : │  0  │     1     │

Here, time is sliced up into moments. The circuit above consists of just two moments. First, we apply a Hadamard gate to every qubit in moment 0 and then we apply two CNOT gates. Since the latter can be run in parallel as they involve different sets of qubits, they only use up one moment of time. For better readability they are displayed next to each other with some small offset.

In [4]:
# show moments of our quantum circuit
my_moments = my_circuit.moments
for moment in my_moments:
    print(moment)
MomentsKey(time=0, qubits=QubitSet([Qubit(0)]), moment_type=<MomentType.GATE: 'gate'>, noise_index=0, subindex=0)
MomentsKey(time=0, qubits=QubitSet([Qubit(1)]), moment_type=<MomentType.GATE: 'gate'>, noise_index=0, subindex=0)
MomentsKey(time=0, qubits=QubitSet([Qubit(2)]), moment_type=<MomentType.GATE: 'gate'>, noise_index=0, subindex=0)
MomentsKey(time=0, qubits=QubitSet([Qubit(3)]), moment_type=<MomentType.GATE: 'gate'>, noise_index=0, subindex=0)
MomentsKey(time=1, qubits=QubitSet([Qubit(0), Qubit(2)]), moment_type=<MomentType.GATE: 'gate'>, noise_index=0, subindex=0)
MomentsKey(time=1, qubits=QubitSet([Qubit(1), Qubit(3)]), moment_type=<MomentType.GATE: 'gate'>, noise_index=0, subindex=0)
In [5]:
# list all instructions/gates making up our circuit
my_instructions = my_circuit.instructions
for instruction in my_instructions:
    print(instruction)
Instruction('operator': H('qubit_count': 1), 'target': QubitSet([Qubit(0)]), 'control': QubitSet([]), 'control_state': (), 'power': 1)
Instruction('operator': H('qubit_count': 1), 'target': QubitSet([Qubit(1)]), 'control': QubitSet([]), 'control_state': (), 'power': 1)
Instruction('operator': H('qubit_count': 1), 'target': QubitSet([Qubit(2)]), 'control': QubitSet([]), 'control_state': (), 'power': 1)
Instruction('operator': H('qubit_count': 1), 'target': QubitSet([Qubit(3)]), 'control': QubitSet([]), 'control_state': (), 'power': 1)
Instruction('operator': CNot('qubit_count': 2), 'target': QubitSet([Qubit(0), Qubit(2)]), 'control': QubitSet([]), 'control_state': (), 'power': 1)
Instruction('operator': CNot('qubit_count': 2), 'target': QubitSet([Qubit(1), Qubit(3)]), 'control': QubitSet([]), 'control_state': (), 'power': 1)

Next, let us build a parametrized circuit where we have to supply numerical parameter values to fully define the circuit, as is the case for example for single-qubit rotations (as described here) and the two-qubit cnot as described in the source code here. The specific parameter values are shown in circuit diagram.

In [6]:
# define circuit with some parametrized gates
my_circuit = (
    Circuit().rx(0, 0.15).ry(1, 0.2).rz(2, 0.25).h(3).cnot(control=0, target=2).cnot(1, 3).x([1, 3])
)
print(my_circuit)
T  : │     0      │     1     │  2  │
      ┌──────────┐                   
q0 : ─┤ Rx(0.15) ├───●───────────────
      └──────────┘   │               
      ┌──────────┐   │         ┌───┐ 
q1 : ─┤ Ry(0.20) ├───┼─────●───┤ X ├─
      └──────────┘   │     │   └───┘ 
      ┌──────────┐ ┌─┴─┐   │         
q2 : ─┤ Rz(0.25) ├─┤ X ├───┼─────────
      └──────────┘ └───┘   │         
         ┌───┐           ┌─┴─┐ ┌───┐ 
q3 : ────┤ H ├───────────┤ X ├─┤ X ├─
         └───┘           └───┘ └───┘ 
T  : │     0      │     1     │  2  │

We can also create a Circuit with gates which depend on a FreeParameter, the value of which can be set later, either by fixing it in the circuit itself, or when the circuit is run on a device.

In [7]:
# define circuit with some parametrized gates and free parameters
alpha = FreeParameter("alpha")
beta = FreeParameter("beta")
gamma = FreeParameter("gamma")
my_circuit = (
    Circuit()
    .rx(0, alpha)
    .ry(1, beta)
    .rz(2, gamma)
    .h(3)
    .cnot(control=0, target=2)
    .cnot(1, 3)
    .x([1, 3])
)
print(my_circuit)
T  : │      0      │     1     │  2  │
      ┌───────────┐                   
q0 : ─┤ Rx(alpha) ├───●───────────────
      └───────────┘   │               
      ┌──────────┐    │         ┌───┐ 
q1 : ─┤ Ry(beta) ├────┼─────●───┤ X ├─
      └──────────┘    │     │   └───┘ 
      ┌───────────┐ ┌─┴─┐   │         
q2 : ─┤ Rz(gamma) ├─┤ X ├───┼─────────
      └───────────┘ └───┘   │         
          ┌───┐           ┌─┴─┐ ┌───┐ 
q3 : ─────┤ H ├───────────┤ X ├─┤ X ├─
          └───┘           └───┘ └───┘ 
T  : │      0      │     1     │  2  │

Unassigned parameters: [alpha, beta, gamma].

GATE SET: Below we list all gates currently available in our SDK. Moreover, we can build custom gates as shown below for a general single-qubit rotation.

In [8]:
# print all available gates currently available within SDK
gate_set = [attr for attr in dir(Gate) if attr[0] in string.ascii_uppercase]
print(gate_set)
['CCNot', 'CNot', 'CPhaseShift', 'CPhaseShift00', 'CPhaseShift01', 'CPhaseShift10', 'CSwap', 'CV', 'CY', 'CZ', 'ECR', 'GPhase', 'GPi', 'GPi2', 'H', 'I', 'ISwap', 'MS', 'PRx', 'PSwap', 'PhaseShift', 'PulseGate', 'Rx', 'Ry', 'Rz', 'S', 'Si', 'Swap', 'T', 'Ti', 'U', 'Unitary', 'V', 'Vi', 'X', 'XX', 'XY', 'Y', 'YY', 'Z', 'ZZ']
In [9]:
# helper function to build custom gate
def u3(alpha, theta, phi):
    """Function to return matrix for general single qubit rotation
    rotation is given by exp(-i sigma*n/2*alpha) where alpha is rotation angle
    and n defines rotation axis as n=(sin(theta)cos(phi), sin(theta)sin(phi), cos(theta))
    sigma is vector of Pauli matrices
    """
    u11 = np.cos(alpha / 2) - 1j * np.sin(alpha / 2) * np.cos(theta)
    u12 = -1j * (np.exp(-1j * phi)) * np.sin(theta) * np.sin(alpha / 2)
    u21 = -1j * (np.exp(1j * phi)) * np.sin(theta) * np.sin(alpha / 2)
    u22 = np.cos(alpha / 2) + 1j * np.sin(alpha / 2) * np.cos(theta)

    return np.array([[u11, u12], [u21, u22]])
In [10]:
# define and print custom unitary
my_u3 = u3(np.pi / 2, 0, 0)
# print(my_u3)
# define example circuit applying custom U to the first qubit
circ = Circuit().unitary(matrix=my_u3, targets=[0]).h(1).cnot(control=0, target=1)
print(circ)
T  : │  0  │  1  │
      ┌───┐       
q0 : ─┤ U ├───●───
      └───┘   │   
      ┌───┐ ┌─┴─┐ 
q1 : ─┤ H ├─┤ X ├─
      └───┘ └───┘ 
T  : │  0  │  1  │

Here, in the circuit diagram our custom unitary is depicted with the general symbol U. In addition, we can use Braket's circuit.subroutine functionality, which allows us to use custom-built gates as any other built-in gates.

In [11]:
# helper function to build custom gate
@circuit.subroutine(register=True)
def u3(target, angles):
    """Function to return the matrix for a general single qubit rotation,
    given by exp(-i sigma*n/2*alpha), where alpha is the rotation angle,
    n defines the rotation axis via n=(sin(theta)cos(phi), sin(theta)sin(phi), cos(theta)),
    and sigma is the vector of Pauli matrices
    """
    # get angles
    alpha = angles[0]
    theta = angles[1]
    phi = angles[2]

    # set 2x2 matrix entries
    u11 = np.cos(alpha / 2) - 1j * np.sin(alpha / 2) * np.cos(theta)
    u12 = -1j * (np.exp(-1j * phi)) * np.sin(theta) * np.sin(alpha / 2)
    u21 = -1j * (np.exp(1j * phi)) * np.sin(theta) * np.sin(alpha / 2)
    u22 = np.cos(alpha / 2) + 1j * np.sin(alpha / 2) * np.cos(theta)

    # define unitary as numpy matrix
    u = np.array([[u11, u12], [u21, u22]])
    # print('Unitary:', u)

    # define custom Braket gate
    circ = Circuit()
    circ.unitary(matrix=u, targets=target)

    return circ
In [12]:
# define example circuit applying custom single-qubit gate U to the first qubit
angles = [np.pi / 2, np.pi / 2, np.pi / 2]
angles = [np.pi / 4, 0, 0]

# build circuit using custom u3 gate
circ2 = Circuit().u3([0], angles).cnot(control=0, target=1)
print(circ2)
T  : │  0  │  1  │
      ┌───┐       
q0 : ─┤ U ├───●───
      └───┘   │   
            ┌─┴─┐ 
q1 : ───────┤ X ├─
            └───┘ 
T  : │  0  │  1  │

CIRCUIT DEPTH AND CIRCUIT SIZE

We can get the circuit depth (the number of moments defining our circuit) with circuit.depth as shown below.

In [13]:
# define circuit with parametrized gates
my_circuit = (
    Circuit().rx(0, 0.15).ry(1, 0.2).rz(2, 0.25).h(3).cnot(control=0, target=2).cnot(1, 3).x(0)
)
circuit_depth = my_circuit.depth
print(my_circuit)
print()
print("Total circuit depth:", circuit_depth)
T  : │     0      │     1     │  2  │
      ┌──────────┐             ┌───┐ 
q0 : ─┤ Rx(0.15) ├───●─────────┤ X ├─
      └──────────┘   │         └───┘ 
      ┌──────────┐   │               
q1 : ─┤ Ry(0.20) ├───┼─────●─────────
      └──────────┘   │     │         
      ┌──────────┐ ┌─┴─┐   │         
q2 : ─┤ Rz(0.25) ├─┤ X ├───┼─────────
      └──────────┘ └───┘   │         
         ┌───┐           ┌─┴─┐       
q3 : ────┤ H ├───────────┤ X ├───────
         └───┘           └───┘       
T  : │     0      │     1     │  2  │

Total circuit depth: 3

The total circuit depth of the circuit above is three (moments 0, 1, 2). It is three because we have added a single qubit X gate applied to qubit 0 in the final layer. However, note that gates are applied as early as possible in time, provided that this is not in conflict with any other gate that has to be applied before. See below an example where we add one qubit to which we only apply one single qubit X gate. This circuit is shallower as its circuit depth is only two. The X gate is applied to qubit 4 as early as possible even though we have applied the corresponding command at the end of our circuit definition.

In [14]:
# define circuit with parameterized gates
my_circuit = (
    Circuit().rx(0, 0.15).ry(1, 0.2).rz(2, 0.25).h(3).cnot(control=0, target=2).cnot(1, 3).x(4)
)
# get circuit depth
circuit_depth = my_circuit.depth
# get qubit number
qubit_count = my_circuit.qubit_count
# get approx. estimate of circuit size
circuit_size = circuit_depth * qubit_count
# print circuit
print(my_circuit)
print()
# print characteristics of our circuit
print("Total circuit depth:", circuit_depth)
print("Number of qubits:", qubit_count)
print("Circuit size:", circuit_size)
T  : │     0      │     1     │
      ┌──────────┐             
q0 : ─┤ Rx(0.15) ├───●─────────
      └──────────┘   │         
      ┌──────────┐   │         
q1 : ─┤ Ry(0.20) ├───┼─────●───
      └──────────┘   │     │   
      ┌──────────┐ ┌─┴─┐   │   
q2 : ─┤ Rz(0.25) ├─┤ X ├───┼───
      └──────────┘ └───┘   │   
         ┌───┐           ┌─┴─┐ 
q3 : ────┤ H ├───────────┤ X ├─
         └───┘           └───┘ 
         ┌───┐                 
q4 : ────┤ X ├─────────────────
         └───┘                 
T  : │     0      │     1     │

Total circuit depth: 2
Number of qubits: 5
Circuit size: 10

In the example above we have also introduced the concept of circuit size. Intuitively, the circuit size is a metric that reflects the complexity of our circuit. The circuit size accounts for both quantity (the number of qubits) and quality (as captured by the depth of the circuit); here we have used a very simple definition multiplying the qubit number with the circuit depth (that is the area of our diagram). In practice, in the absence of quantum error correction, on real quantum machines the depth is limited by noise so we can only faithfully run circuits whose depth is within the quality bounds of our machine. Simply speaking, this means: The larger the circuit size, the harder it is to simulate on a classical device and the more powerful the quantum machine is that is able to faithfully execute this circuit.

APPENDING CIRCUITS

We can extend existing circuits by adding instructions or just appending circuits to each other, as shown below. In the most simple and straightforward fashion we can just append gates to existing circuits (for example, my_circuit.y(4)).

In [15]:
# simple circuit extension by appending gates (here Y on qubit 4)
my_circuit = my_circuit.y(4)
# get circuit depth
circuit_depth = my_circuit.depth
# get qubit number
qubit_count = my_circuit.qubit_count
# get circuit size
circuit_size = circuit_depth * qubit_count
# print circuit
print(my_circuit)
print()
print("Total circuit depth:", circuit_depth)
print("Number of qubits:", qubit_count)
print("Circuit size:", circuit_size)
T  : │     0      │     1     │
      ┌──────────┐             
q0 : ─┤ Rx(0.15) ├───●─────────
      └──────────┘   │         
      ┌──────────┐   │         
q1 : ─┤ Ry(0.20) ├───┼─────●───
      └──────────┘   │     │   
      ┌──────────┐ ┌─┴─┐   │   
q2 : ─┤ Rz(0.25) ├─┤ X ├───┼───
      └──────────┘ └───┘   │   
         ┌───┐           ┌─┴─┐ 
q3 : ────┤ H ├───────────┤ X ├─
         └───┘           └───┘ 
         ┌───┐     ┌───┐       
q4 : ────┤ X ├─────┤ Y ├───────
         └───┘     └───┘       
T  : │     0      │     1     │

Total circuit depth: 2
Number of qubits: 5
Circuit size: 10

Alternatively, we can define a gate as an Instruction and use the add_instruction(...) method to add this gate to an existing circuit object.

In [16]:
# add instruction to circuit
my_circuit.cnot(0, 1)
# get circuit depth
circuit_depth = my_circuit.depth
# get qubit number
qubit_count = my_circuit.qubit_count
# get circuit size
circuit_size = circuit_depth * qubit_count
# print circuit
print(my_circuit)
print()
print("Total circuit depth:", circuit_depth)
print("Number of qubits:", qubit_count)
print("Circuit size:", circuit_size)
T  : │     0      │     1     │  2  │
      ┌──────────┐                   
q0 : ─┤ Rx(0.15) ├───●───────────●───
      └──────────┘   │           │   
      ┌──────────┐   │         ┌─┴─┐ 
q1 : ─┤ Ry(0.20) ├───┼─────●───┤ X ├─
      └──────────┘   │     │   └───┘ 
      ┌──────────┐ ┌─┴─┐   │         
q2 : ─┤ Rz(0.25) ├─┤ X ├───┼─────────
      └──────────┘ └───┘   │         
         ┌───┐           ┌─┴─┐       
q3 : ────┤ H ├───────────┤ X ├───────
         └───┘           └───┘       
         ┌───┐     ┌───┐             
q4 : ────┤ X ├─────┤ Y ├─────────────
         └───┘     └───┘             
T  : │     0      │     1     │  2  │

Total circuit depth: 3
Number of qubits: 5
Circuit size: 15

We can append entire circuits to each other with add_circuit().

In [17]:
# append two circuits with add_circuit() functionality
my_circuit2 = Circuit().rz(0, 0.1).rz(1, 0.2).rz(3, 0.3).rz(4, 0.4)
my_circuit.add_circuit(my_circuit2)

# get circuit depth
circuit_depth = my_circuit.depth
# get qubit number
qubit_count = my_circuit.qubit_count
# get circuit size
circuit_size = circuit_depth * qubit_count
# print circuit
print(my_circuit)
print()
# print characteristics of our circuit
print("Total circuit depth:", circuit_depth)
print("Number of qubits:", qubit_count)
print("Circuit size:", circuit_size)
T  : │     0      │     1     │     2      │     3      │
      ┌──────────┐                          ┌──────────┐ 
q0 : ─┤ Rx(0.15) ├───●──────────────●───────┤ Rz(0.10) ├─
      └──────────┘   │              │       └──────────┘ 
      ┌──────────┐   │            ┌─┴─┐     ┌──────────┐ 
q1 : ─┤ Ry(0.20) ├───┼─────●──────┤ X ├─────┤ Rz(0.20) ├─
      └──────────┘   │     │      └───┘     └──────────┘ 
      ┌──────────┐ ┌─┴─┐   │                             
q2 : ─┤ Rz(0.25) ├─┤ X ├───┼─────────────────────────────
      └──────────┘ └───┘   │                             
         ┌───┐           ┌─┴─┐ ┌──────────┐              
q3 : ────┤ H ├───────────┤ X ├─┤ Rz(0.30) ├──────────────
         └───┘           └───┘ └──────────┘              
         ┌───┐     ┌───┐       ┌──────────┐              
q4 : ────┤ X ├─────┤ Y ├───────┤ Rz(0.40) ├──────────────
         └───┘     └───┘       └──────────┘              
T  : │     0      │     1     │     2      │     3      │

Total circuit depth: 4
Number of qubits: 5
Circuit size: 20

Again, note that the single qubit rotations we have appended to our circuit are applied as early as possible. This helps keeping the circuit as short as possible, as required in the presence of decoherence.

CIRCUIT EXECUTION AND QUANTUM TASK TRACKING

Finally, let us run our circuit on a device of our choice. We do so by defining a classical device object below and calling the method device.run(my_circuit). Additional task creation arguments can be provided to the run() method of the device object; in particular the optional “shots” argument refers to the number of desired measurement shots (default = 1000).

The command device.run(...) defines a quantum task (with a unique quantum task ID), the status of which can be queried and tracked with task.state() as shown below. Once the quantum task completes (which may take some time, specifically for the QPU devices, depending on the length of the queue), one can retrieve the results from the S3 bucket as specified below; you can check for "Quantum Task Status” under Quantum Tasks within your Braket console. Note that task = device.run() is an asynchronous operation. This means you can keep working while the system in the background polls for the results. You can always check the quantum task status with task.state(). When you call task.result(), this becomes a blocking call that will throw an error if within the timeout period you will not get a result. We show below how to set this timeout period.

By calling result() on a quantum task, you get the quantum task result by polling Amazon Braket to see if the quantum task is completed. Once the quantum task is completed, the result is retrieved from S3 and returned as a QuantumTaskResult. As opposed to async_result(), this method is a blocking thread call and synchronously returns a result.

In [18]:
# Create the device. The experiment value must be unique among any devices in use at the time
device = AwsDevice(Devices.Amazon.SV1)

# set up the device to be the Rigetti quantum computer
# device = AwsDevice(Devices.Rigetti.Cepheus1108Q)

# set up the device to be the IonQ quantum computer
# device = AwsDevice(Devices.IonQ.ForteEnterprise1)

# set up the device to be the IQM quantum computer
# device = AwsDevice(Devices.IQM.Garnet)

We can check out the set of gates this device supports as follows:

In [19]:
# show the properties of the device
device_properties = device.properties
# show supportedQuantumOperations (supported gates for a device)
device_operations = device_properties.dict()["action"]["braket.ir.openqasm.program"][
    "supportedOperations"
]
# Note: This field also exists for other devices like the QPUs
print("Quantum Gates supported by this device:\n", device_operations)
Quantum Gates supported by this device:
 ['ccnot', 'cnot', 'cphaseshift', 'cphaseshift00', 'cphaseshift01', 'cphaseshift10', 'cswap', 'cy', 'cz', 'ecr', 'h', 'i', 'iswap', 'pswap', 'phaseshift', 'rx', 'ry', 'rz', 's', 'si', 'swap', 't', 'ti', 'unitary', 'v', 'vi', 'x', 'xx', 'xy', 'y', 'yy', 'z', 'zz']

POLLING PARAMETERS: With the run(...) method we can set two important parameters:

  • poll_timeout_seconds is the number of seconds you want to wait and poll the task before it times out; the default value is 5 days (that is seconds).
  • poll_interval_seconds is the frequency how often the task is polled, e.g., how often you call the Braket API to get the status; the default value is 1 second.
In [20]:
# define quantum task (asynchronous)
task = device.run(my_circuit, poll_timeout_seconds=100, shots=1000)

# get id and status of submitted quantum task
task_id = task.id
status = task.state()
# print('ID of quantum task:', task_id)
print("Status of quantum task:", status)

# wait for job to complete
while status != "COMPLETED":
    status = task.state()
    print("Status:", status)

# get results of quantum task
result = task.result()

# get measurement shots
counts = result.measurement_counts

# print counts
print(counts)

# plot using Counter
plt.bar(counts.keys(), counts.values())
plt.xlabel("bitstrings")
plt.ylabel("counts");
Status of quantum task: QUEUED
Status: QUEUED
Status: QUEUED
Status: QUEUED
Status: QUEUED
Status: QUEUED
Status: QUEUED
Status: QUEUED
Status: RUNNING
Status: RUNNING
Status: RUNNING
Status: RUNNING
Status: RUNNING
Status: RUNNING
Status: COMPLETED
Counter({'00010': 508, '00000': 480, '01010': 6, '11100': 3, '01000': 2, '11110': 1})

The on-demand simulators SV1 and DM1 also support running parametrized tasks. The value of any free parameters can be fixed when the circuit is run using the optional inputs argument to run. inputs should be a dict of string-float pairs.

In [21]:
# define circuit with some parametrized gates and free parameters
alpha = FreeParameter("alpha")
beta = FreeParameter("beta")
gamma = FreeParameter("gamma")
my_circuit = (
    Circuit()
    .rx(0, alpha)
    .ry(1, beta)
    .rz(2, gamma)
    .h(3)
    .cnot(control=0, target=2)
    .cnot(1, 3)
    .x([1, 3])
)
print(my_circuit)
# define quantum task (asynchronous)
task = device.run(
    my_circuit,
    poll_timeout_seconds=100,
    shots=1000,
    inputs={"alpha": 0.1, "beta": 0.2, "gamma": 0.3},
)

# get id and status of submitted quantum task
task_id = task.id
status = task.state()
# print('ID of quantum task:', task_id)
print("Status of quantum task:", status)

# wait for job to complete
while status != "COMPLETED":
    status = task.state()
    print("Status:", status)

# get results of task
result = task.result()

# get measurement shots
counts = result.measurement_counts

# print counts
print(counts)

# plot using Counter
plt.bar(counts.keys(), counts.values())
plt.xlabel("bitstrings")
plt.ylabel("counts");
T  : │      0      │     1     │  2  │
      ┌───────────┐                   
q0 : ─┤ Rx(alpha) ├───●───────────────
      └───────────┘   │               
      ┌──────────┐    │         ┌───┐ 
q1 : ─┤ Ry(beta) ├────┼─────●───┤ X ├─
      └──────────┘    │     │   └───┘ 
      ┌───────────┐ ┌─┴─┐   │         
q2 : ─┤ Rz(gamma) ├─┤ X ├───┼─────────
      └───────────┘ └───┘   │         
          ┌───┐           ┌─┴─┐ ┌───┐ 
q3 : ─────┤ H ├───────────┤ X ├─┤ X ├─
          └───┘           └───┘ └───┘ 
T  : │      0      │     1     │  2  │

Unassigned parameters: [alpha, beta, gamma].
Status of quantum task: QUEUED
Status: QUEUED
Status: QUEUED
Status: QUEUED
Status: QUEUED
Status: QUEUED
Status: QUEUED
Status: QUEUED
Status: QUEUED
Status: RUNNING
Status: RUNNING
Status: RUNNING
Status: RUNNING
Status: RUNNING
Status: RUNNING
Status: COMPLETED
Counter({'0101': 522, '0100': 461, '0000': 10, '0001': 3, '1110': 3, '1111': 1})

TASK METADATA: You can access a range of metadata associated with your task object, as shown below.

In [22]:
# get all metadata of submitted quantum task
metadata = task.metadata()
# example for metadata
shots = metadata["shots"]
date = metadata["ResponseMetadata"]["HTTPHeaders"]["date"]
# print example metadata
print(f"{shots} shots taken on {date}.")
1000 shots taken on Fri, 06 Sep 2024 23:25:50 GMT.

TASK RECONSTRUCTION: Imagine your kernel dies after you have submitted the task, or you simply close your notebook. As recovery method, here is how you can reconstruct the task object (given the corresponding unique arn). You can reconstruct the task object using task = AwsQuantumTask(arn=...); then you can simply call task.result() to get the result from S3.

In [23]:
# restore quantum task from unique arn
task_load = AwsQuantumTask(arn=task_id)
# print status
status = task_load.state()
print("Status of (reconstructed) quantum task:", status)
Status of (reconstructed) quantum task: COMPLETED
In [24]:
# get results of quantum task
result = task_load.result()

# get measurement shots
counts = result.measurement_counts

# print counts
print(counts)

# plot using Counter
plt.bar(counts.keys(), counts.values(), color="g")
plt.xlabel("bitstrings")
plt.ylabel("counts");
Counter({'0101': 522, '0100': 461, '0000': 10, '0001': 3, '1110': 3, '1111': 1})

TASK CANCELLATION: Finally, we can also cancel existing tasks by calling the cancel() method.

In [25]:
# define quantum task
task = device.run(my_circuit, shots=1000, inputs={"alpha": 0.1, "beta": 0.2, "gamma": 0.3})

# get id and status of submitted quantum task
task_id = task.id
status = task.state()
# print('ID of quantum task:', task_id)
print("Status of quantum task:", status)

# cancel task
task.cancel()
status = task.state()
print("Status of quantum task:", status)
Status of quantum task: RUNNING
Status of quantum task: COMPLETED

PARTIAL MEASUREMENT

So far, we have measured on all qubits in the quantum circuit. However, it is possible to measure individual qubits or a subset of qubits by adding a measure instruction with the target qubits to the end of our circuit. In the following example, we will create a Bell state circuit and measure only qubit 0.

In [26]:
# Use the local state vector simulator
device = LocalSimulator()

# define an example bell circuit and measure qubit 0
circuit = Circuit().h(0).cnot(0, 1).measure(0)

# Run the circuit
task = device.run(circuit, shots=10)

# Get the results
result = task.result()

# Print the circuit and measured qubits
print(circuit)
print()
print("Measured qubits: ", result.measured_qubits)
T  : │  0  │  1  │  2  │
      ┌───┐       ┌───┐ 
q0 : ─┤ H ├───●───┤ M ├─
      └───┘   │   └───┘ 
            ┌─┴─┐       
q1 : ───────┤ X ├───────
            └───┘       
T  : │  0  │  1  │  2  │

Measured qubits:  [0]

We can see that the measure instruction, represented by M, was added to our circuit, and only on qubit 0.

Now, we can see the measurement counts from qubit 0 on this bell circuit:

In [27]:
# get measurement shots
counts = result.measurement_counts

# print counts
print(counts)

# plot using Counter
plt.bar(counts.keys(), counts.values(), color="g")
plt.xlabel("bitstrings")
plt.ylabel("counts");
Counter({'1': 5, '0': 5})

DEMONSTRATION OF RESULT TYPES: Expectation Values and Observables

So far, we have only taken measurements in the computational basis. However, it is also possible to measure in other bases, as well as estimate important statistics like expectation value and variance. We do this by adding ResultTypes to our circuit; in the following example, we will make measurements in the basis of the observable (this is the tensor product ):

In [28]:
# define example circuit
circ = Circuit().rx(0, 0.15).ry(1, 0.2).rz(2, 0.25).h(3).cnot(control=0, target=2).cnot(1, 3).x(4)
# add expectation value
obs = X(0) @ Y(1)
circ.expectation(obs)
# add variance
circ.variance(obs)
# add samples

circ.sample(obs)
print(circ)
T  : │     0      │     1     │                     Result Types                     │
      ┌──────────┐             ┌──────────────────┐ ┌───────────────┐ ┌─────────────┐ 
q0 : ─┤ Rx(0.15) ├───●─────────┤ Expectation(X@Y) ├─┤ Variance(X@Y) ├─┤ Sample(X@Y) ├─
      └──────────┘   │         └────────┬─────────┘ └───────┬───────┘ └──────┬──────┘ 
      ┌──────────┐   │         ┌────────┴─────────┐ ┌───────┴───────┐ ┌──────┴──────┐ 
q1 : ─┤ Ry(0.20) ├───┼─────●───┤ Expectation(X@Y) ├─┤ Variance(X@Y) ├─┤ Sample(X@Y) ├─
      └──────────┘   │     │   └──────────────────┘ └───────────────┘ └─────────────┘ 
      ┌──────────┐ ┌─┴─┐   │                                                          
q2 : ─┤ Rz(0.25) ├─┤ X ├───┼──────────────────────────────────────────────────────────
      └──────────┘ └───┘   │                                                          
         ┌───┐           ┌─┴─┐                                                        
q3 : ────┤ H ├───────────┤ X ├────────────────────────────────────────────────────────
         └───┘           └───┘                                                        
         ┌───┐                                                                        
q4 : ────┤ X ├────────────────────────────────────────────────────────────────────────
         └───┘                                                                        
T  : │     0      │     1     │                     Result Types                     │

Note: sample is only valid when shots>0.

As shown above, results types are part of the print information. We now run this circuit on the local simulator above and output these results.

In [29]:
# set up device
device = LocalSimulator()
# run the circuit and output the results specified above
task = device.run(circ, shots=100)
result = task.result()
print("Expectation value for <X0*Y1>:", result.values[0])
print("Variance for <X0*Y1>:", result.values[1])
print("Measurement samples for X0*Y1:", result.values[2])
Expectation value for <X0*Y1>: 0.08
Variance for <X0*Y1>: 0.9936
Measurement samples for X0*Y1: [-1.  1.  1.  1.  1.  1.  1.  1.  1.  1. -1.  1.  1. -1.  1. -1.  1. -1.
 -1. -1. -1. -1. -1. -1. -1. -1.  1. -1. -1.  1. -1. -1. -1.  1. -1.  1.
  1. -1.  1.  1.  1. -1.  1. -1. -1. -1.  1.  1.  1.  1. -1. -1. -1.  1.
 -1. -1.  1.  1. -1. -1. -1. -1.  1.  1.  1.  1.  1. -1. -1.  1. -1.  1.
 -1. -1.  1.  1. -1.  1. -1.  1.  1.  1.  1. -1. -1. -1.  1.  1.  1.  1.
 -1.  1.  1.  1. -1. -1.  1.  1.  1.  1.]

We can verify that we get the same estimate for the expectation value if we compute it by hand from the samples:

In [30]:
samples = result.values[2]
sum_of_samples = samples.sum()
total_counts = len(samples)
expect_from_samples = sum_of_samples / total_counts
print("Expectation value from samples:", expect_from_samples)
Expectation value from samples: 0.08

So far, we have measured only one observable, namely . However, it is possible to measure multiple observables at once, provided that any observables with overlapping qubits have the same factor acting on each qubit:

Note The following code requires amazon-braket-sdk>=1.5.0 and amazon-braket-default-simulator>=1.1.0
In [31]:
circ = Circuit().rx(0, 0.15).ry(1, 0.2).rz(2, 0.25).h(3).cnot(control=0, target=2).cnot(1, 3).x(4)
circ.expectation(X(0) @ Y(1))
circ.expectation(Z(2) @ H(4))
# Overlaps on qubits 1 and 4, but Y and H are the same factors that have been applied to each, respectively
circ.expectation(Y(1) @ X(3) @ H(4))

# run circuit
task = device.run(circ, shots=1000)
result = task.result()
print("Expectation value for <X0*Y1>:", result.values[0])
print("Expectation value for <Z2*H4>:", result.values[1])
print("Expectation value for <Y1*X3*H4>::", result.values[2])
Expectation value for <X0*Y1>: -0.02
Expectation value for <Z2*H4>: -0.716
Expectation value for <Y1*X3*H4>:: 0.004

This is possible because we only need to measure in at most one basis for each qubit. For instance in the example above, on qubit 1 we only measure in the Y basis.

RESULT TYPES FOR shots=0

Note This section requires amazon-braket-sdk>=1.8.0 and amazon-braket-default-simulator>=1.3.0

In all examples discussed so far we have set the parameter shots>0, thereby mimicking the behavior of actual quantum hardware. However, on a classical simulator we do have access to the full state vector when shots=0. We will illustrate this functionality in more detail in this section.

Note that the full state vector and amplitudes can only be requested when shots=0 for a classical simulator. When shots=0 for a simulator, probability and expectation values are the exact values, as derived from the full wavefunction. When shots>0 we cannot access the full state vector, but we can still get approximate expectation values as taken from measurement samples. Note that probability, sample, expectation, and variance are also supported for QPU devices.

In the following example we output the state vector, the exact expectation values of and , the amplitude of the state , and the marginal probability of qubit . Notice in particular that the two observables share qubit but don't have the same factor acting on it; this is allowed because simulators can directly compute expectation values using the state vector, and don't have to measure in a common basis.

In [32]:
# add result types
circ = my_circuit
# add the state_vector ResultType available for shots=0
circ.state_vector()
# add single qubit expectation value
obs1 = Y(1) @ X(2)
circ.expectation(obs1)
# add the two-qubit Z0*Z1 expectation value
obs2 = Z(0) @ Z(1)
circ.expectation(obs2)
# add the amplitude for |0...0>
bitstring = "0" * circ.qubit_count
circ.amplitude(state=[bitstring])
# add marginal probability
circ.probability(target=[3])
print(circ)
T  : │      0      │     1     │  2  │              Result Types               │
      ┌───────────┐                                        ┌──────────────────┐ 
q0 : ─┤ Rx(alpha) ├───●────────────────────────────────────┤ Expectation(Z@Z) ├─
      └───────────┘   │                                    └────────┬─────────┘ 
      ┌──────────┐    │         ┌───┐ ┌──────────────────┐ ┌────────┴─────────┐ 
q1 : ─┤ Ry(beta) ├────┼─────●───┤ X ├─┤ Expectation(Y@X) ├─┤ Expectation(Z@Z) ├─
      └──────────┘    │     │   └───┘ └────────┬─────────┘ └──────────────────┘ 
      ┌───────────┐ ┌─┴─┐   │         ┌────────┴─────────┐                      
q2 : ─┤ Rz(gamma) ├─┤ X ├───┼─────────┤ Expectation(Y@X) ├──────────────────────
      └───────────┘ └───┘   │         └──────────────────┘                      
          ┌───┐           ┌─┴─┐ ┌───┐   ┌─────────────┐                         
q3 : ─────┤ H ├───────────┤ X ├─┤ X ├───┤ Probability ├─────────────────────────
          └───┘           └───┘ └───┘   └─────────────┘                         
T  : │      0      │     1     │  2  │              Result Types               │

Additional result types: StateVector, Amplitude(0000)

Unassigned parameters: [alpha, beta, gamma].

As shown above, results types are part of the print information. We now run this circuit on the local simulator above and output these results.

In [33]:
# set up device
device = LocalSimulator()
# run the circuit and output the results specified above
task = device.run(circ, shots=0, inputs={"alpha": 0.1, "beta": 0.2, "gamma": 0.3})
result = task.result()
print("Final state vector:\n", result.values[0])
print("Expectation value <Y1X2>", result.values[1])
print("Expectation value <Z0Z1>:", result.values[2])
print("Amplitude <00000|Final state>:", result.values[3])
print("Marginal probability for target qubit 3 in computational basis:", result.values[4])
Final state vector:
 [ 6.97129718e-02-0.01053609j  6.97129718e-02-0.01053609j
  0.00000000e+00+0.j          0.00000000e+00+0.j
  6.94804402e-01-0.10500941j  6.94804402e-01-0.10500941j
  0.00000000e+00+0.j          0.00000000e+00+0.j
  0.00000000e+00+0.j          0.00000000e+00+0.j
 -5.27243703e-04-0.00348856j -5.27243703e-04-0.00348856j
  0.00000000e+00+0.j          0.00000000e+00+0.j
 -5.25485051e-03-0.0347692j  -5.25485051e-03-0.0347692j ]
Expectation value <Y1X2> 0.0
Expectation value <Z0Z1>: -0.9751703272018155
Amplitude <00000|Final state>: {'0000': (0.06971297180671754-0.010536085195500033j)}
Marginal probability for target qubit 3 in computational basis: [0.5 0.5]

ADVANCED LOGGING

Below we provide an example for advanced logging. Here, we change the poll_timeout_seconds and poll_interval_seconds parameters, such that a task can be long-running and the task status will be continuously logged to a file. You can also transfer this code to a python script instead of a Jupyter notebook, and the script can run as a process in the background so that your laptop can go to sleep and the script will still run. These advanced logging techniques allow you to see the background polling and create a record for later debugging.

In [34]:
# set filename for logs
log_file = "device_logs-" + datetime.strftime(datetime.now(), "%Y%m%d%H%M%S") + ".txt"
print("Quantum task info will be logged in:", log_file)

# create new logger object
logger = logging.getLogger("newLogger")
# configure to log to file device_logs.txt in the appending mode
logger.addHandler(logging.FileHandler(filename=log_file, mode="a"))
# add to file all log messages with level DEBUG or above
logger.setLevel(logging.DEBUG)
Quantum task info will be logged in: device_logs-20240906162552.txt
In [35]:
# define circuit
circ_log = (
    Circuit().rx(0, 0.15).ry(1, 0.2).rz(2, 0.25).h(3).cnot(control=0, target=2).cnot(1, 3).x(4)
)
print(circ_log)
# define device
device = AwsDevice(Devices.Amazon.SV1)
# define what info to log
logger.info(
    device.run(
        circ_log,
        poll_timeout_seconds=1200,
        poll_interval_seconds=0.25,
        logger=logger,
        shots=1000,
    )
    .result()
    .measurement_counts,
)
T  : │     0      │     1     │
      ┌──────────┐             
q0 : ─┤ Rx(0.15) ├───●─────────
      └──────────┘   │         
      ┌──────────┐   │         
q1 : ─┤ Ry(0.20) ├───┼─────●───
      └──────────┘   │     │   
      ┌──────────┐ ┌─┴─┐   │   
q2 : ─┤ Rz(0.25) ├─┤ X ├───┼───
      └──────────┘ └───┘   │   
         ┌───┐           ┌─┴─┐ 
q3 : ────┤ H ├───────────┤ X ├─
         └───┘           └───┘ 
         ┌───┐                 
q4 : ────┤ X ├─────────────────
         └───┘                 
T  : │     0      │     1     │
In [36]:
# print logs
! cat {log_file}
Task arn:aws:braket:us-west-2:667256736152:quantum-task/258106dd-3fc2-4f72-b651-f4c21903aa2e: start polling for completion
Task is in Normal queue position: 1
Task arn:aws:braket:us-west-2:667256736152:quantum-task/258106dd-3fc2-4f72-b651-f4c21903aa2e: task status QUEUED
Task is in Normal queue position: 1
Task arn:aws:braket:us-west-2:667256736152:quantum-task/258106dd-3fc2-4f72-b651-f4c21903aa2e: task status QUEUED
Task arn:aws:braket:us-west-2:667256736152:quantum-task/258106dd-3fc2-4f72-b651-f4c21903aa2e: task status RUNNING
Task arn:aws:braket:us-west-2:667256736152:quantum-task/258106dd-3fc2-4f72-b651-f4c21903aa2e: task status RUNNING
Task arn:aws:braket:us-west-2:667256736152:quantum-task/258106dd-3fc2-4f72-b651-f4c21903aa2e: task status COMPLETED
Counter({'00001': 504, '00011': 480, '01011': 5, '01001': 5, '10101': 4, '10111': 2})
In [37]:
# parse log file for arn
with open(log_file) as openfile:
    for line in openfile:
        for part in line.split():
            if "arn:" in part:
                arn = part
                break
# remove final semicolon in logs
arn = arn[:-1]
# print(arn)

# with this arn we can restore again task from unique arn
task_load = AwsQuantumTask(arn=arn)

# get results of quantum task
result = task_load.result()

# get measurement shots
counts = result.measurement_counts

# plot using Counter
plt.bar(counts.keys(), counts.values(), color="tab:orange")
plt.xlabel("bitstrings")
plt.ylabel("counts");

APPENDIX

APPENDIX: ADVANCED FUNCTIONALITY WITH ASYNCHRONOUS EXECUTION

ASYNCHRONOUS EXECUTION: When replacing the result() call on the task object above with async_result(), we can get the quantum task result asynchronously. Consecutive calls to this method return the result cached from the most recent request. See here for source code implementation.

While result() is a blocking call that waits for the result, async_result() is a non-blocking call. For example, in Jupyter as shown here, if you run result(), the notebook will stop and wait at this cell for a certain polling time (set as poll_timeout_seconds with default of 5 days) till the polling returns the result object or times out. If you run async_result(), the notebook immediately goes to the next cell, not waiting for polling to complete. Calling result() on the async_result object before it has completed, an asyncio.exceptions.InvalidStateError will be raised. This is expected behavior. Later, you can call result() and get the actual result from the task.

Alternatively, we have provided a basic asyncio waiter function wait_on_result(), which will create a blocking call, wait for the result, and then return that result for downstream use. We have defaulted the notebook to leverage this call in favor of avoiding the InvalidStateError, but we have left the non-blocking async_result.result() call example as an option.

In [38]:
# asyncio waiter function to leverage Task.async_result() object
async def wait_on_result(async_result):
    print("Waiting on task.")
    await async_result

    print(f"Final task state: {async_result._state}")
    res = async_result.result()

    return res
In [39]:
# example with async_result - immediately returns asyncio Future object
async_result = device.run(circ2, shots=100).async_result()
In [40]:
# async_result.result() then returns the actual result (once completed)
# Non-blocking call. Will raise an InvalidStateError if this is run before async task is complete:
# async_res = async_result.result()
# Blocking call, leveraging asyncio.run and await
async_res = asyncio.run(wait_on_result(async_result))
# get measurement shots
counts = async_res.measurement_counts
print(counts)
Waiting on task.
Final task state: FINISHED
Counter({'00': 100})

One can also define custom callbacks to be invoked when the Future is completed.

In [41]:
# async_result returns back a Future.
# Details on Future: https://docs.python.org/3.8/library/asyncio-future.html#asyncio.Future
future = device.run(circ2, shots=100).async_result()


# this is invoked when the Future is done. i.e. task is in a terminal state.
# This will print out to STDOUT when its done.
def call_back_function(future):
    print(f"Custom task Result: {future.result().measurement_probabilities}")


# attached the callback function to the future.
future.add_done_callback(call_back_function)
In [42]:
print("Quantum Task Summary")
print(t.quantum_tasks_statistics())
print(
    "Note: Charges shown are estimates based on your Amazon Braket simulator and quantum processing unit (QPU) task usage. Estimated charges shown may differ from your actual charges. Estimated charges do not factor in any discounts or credits, and you may experience additional charges based on your use of other services such as Amazon Elastic Compute Cloud (Amazon EC2).",
)
print(
    f"Estimated cost to run this example: {t.qpu_tasks_cost() + t.simulator_tasks_cost():.2f} USD",
)
Quantum Task Summary
{<_Amazon.SV1: 'arn:aws:braket:::device/quantum-simulator/amazon/sv1'>: {'shots': 4200, 'tasks': {'COMPLETED': 5, 'QUEUED': 1}, 'execution_duration': datetime.timedelta(microseconds=13000), 'billed_execution_duration': datetime.timedelta(seconds=12)}}
Note: Charges shown are estimates based on your Amazon Braket simulator and quantum processing unit (QPU) task usage. Estimated charges shown may differ from your actual charges. Estimated charges do not factor in any discounts or credits, and you may experience additional charges based on your use of other services such as Amazon Elastic Compute Cloud (Amazon EC2).
Estimated cost to run this example: 0.02 USD
Custom task Result: {'00': 1.0}

Join the Discussion

Comments (0)

No comments yet. Be the first to share your thoughts!

Indexed by QCR Librarian

This entry was created automatically from publicly available records. QCR links to public sources and only stores repository content where the license permits redistribution.

Versions

v1 Latest
Jun 15, 2026
qcr:2606.69705.1

Cite all versions? Use the base QCR ID to always reference the latest version of this entry.

Tools used

Amazon Braket SDK

Keywords

circuits
quantum-tasks
result-types
braket
internals

You may also like5