Using Aquila on Braket

The AWS Braket interface allows you to send tasks from your laptop to quantum processors at QuEra via the AWS cloud infrastructure. This tutorial will give you a first taste of what this workflow looks like. It's simple: Define the arrangement of atoms you want to use and specify your Hamiltonian parameters of choice to govern the behavior of those atoms. Submit the program and analyze the results.

Below we go through the code that can be found here. Please note that ahs_utils can be found in the directory HelloWorld as ahs_utils.py.

Ready to explore what's possible with Aquila? Let's dive straight in.

Goal

To say hello to the world of neutral atoms, let's investigate one of the central phenomena in quantum many-body physics: the emergence of ordered phases of matter. In this tutorial, we'll show how Aquila can prepare the simplest of such ordered phases, namely an antiferromagnetic phase (aka Z2 phase) in both one- and two-dimensional arrays of atoms. To make this possible, we will make use of the Rydberg blockade - the mechanism at the heart of our quantum computing architecture.

Register

Let's start off with the 1D case. To achieve our goal, we first need to define our Rydberg-atom quantum computer register. We arrange them in a line with a separation of 6.1 μm between each pair of atoms.


from braket.ahs.atom_arrangement import AtomArrangement
import numpy as np
from ahs_utils import show_register

a = 6.1e-6  # meters
N_atoms = 11

register = AtomArrangement()
for i in range(N_atoms):
    register.add([0.0, i*a])

fig = show_register(register)
Copy
1-d array of qubits

Hamiltonian

The next component we need to specify is the Hamiltonian. It's the energy function that governs the behavior of our atoms, including their interactions. In the lab, the Hamiltonian is implemented by applying lasers to the atoms.


from braket.ahs.hamiltonian import Hamiltonian

H = Hamiltonian()
Copy
Specifically, the Hamiltonian governing our system of atoms takes the following form:
\[H(t) = \sum\limits_{j} \frac{\Omega(t)}{2} \left( e^{i \phi(t) } | g_j \rangle  \langle r_j | + e^{-i \phi(t) } | r_j \rangle  \langle g_j | \right) - \sum\limits_{j} \Delta(t) \hat{n}_j + \sum\limits_{j < k} V_{jk} \hat{n}_j \hat{n}_k\]

where \(\Omega\), \(\phi\) and \(\Delta\) denote the Rabi frequency, laser phase, and the detuning of the driving laser field coupling the ground states \(\ket{g_j}\) and excited Rydberg state \(\ket{r_j}\) of the j-th atom. Our task is to specify these Hamiltonian parameters (\(\Omega\), \(\phi\) and \(\Delta\)). Here, they will be time-dependent in order to help us carry out a protocol called adiabatic state preparation. In case this sounds unfamiliar, feel free to read up on the physics background on our open source platform Bloqade.

In our current case, we only want to tell the system to activate the set of lasers controlling the Rabi frequency and the global detuning. In particular, we will choose a constant Rabi frequency of \(\Omega = 2.5 \times 10^6 \) and a linearly increasing detuning from \(\Delta = -9 \times 10^6\) to \(\Delta = 7 \times 10^6 \), together with a total time duration of t=4 μs. The phase parameter will simply be set to zero. The rest of the code ensures that \(\Omega\) and \(\Delta\) are ramped on and off in a way compatible with the experimental setup.


from ahs_utils import show_global_drive, get_drive

omega_min = 0       
omega_max = 2.5e6 * 2 * np.pi
detuning_min = -9e6 * 2 * np.pi
detuning_max = 7e6 * 2 * np.pi

time_max = 4e-6
time_ramp = 0.15*time_max

time_points = [0, time_ramp, time_max - time_ramp, time_max]
omega_values = [omega_min, omega_max, omega_max, omega_min]
detuning_values = [detuning_min, detuning_min, detuning_max, detuning_max]
phase_values = [0, 0, 0, 0]

drive = get_drive(time_points, omega_values, detuning_values, phase_values)
H += drive

show_global_drive(drive)
Copy
Global drive parameters of QuEra's 256-qubit quantum computer
However, that's not the full story. These drive terms alone don't tell us how the atoms interact. But interactions are at the core of many-body quantum physics! That's where the Rydberg blockade comes in. Mathematically, it is described by the term
\[H_{interaction} = \sum\limits_{j < k}V_{jk} \hat{n}_j \hat{n}_k \]

in the Rydberg Hamiltonian. The key idea is that within a certain distance - the so-called blockade radius - only one atom will be excited into an \(\ket{r}\) state. Having more excitations will cost too much energy. This means that the states of two neighboring atoms will depend on each other and exhibit specific patterns which we will plot below. Creating these ordered states is non-trivial. Following the adiabatic protocol mentioned previously is crucial since it allows us to slowly introduce these complex interactions into the system.

Note: In the current version of the AHS module in Braket, this Rydberg interaction term is automatically calculated from the atom positions. Hence, we don't need to worry about specifying it explicitly. Our Hamiltonian is all set!

Defining the program (1D case)

Now, we can combine the register and Hamiltonian into a program. In particular, this program falls within the class of Analog Hamiltonian Simulation (AHS). If you're curious about other types of quantum computing, take a look at gate-based circuits or tutorials on quantum annealing.

from braket.ahs.analog_hamiltonian_simulation import AnalogHamiltonianSimulation

ahs_program = AnalogHamiltonianSimulation(
    register=register, 
    hamiltonian=H
)
Copy

Simulation on classical hardware

Before submitting the task to run on actual quantum hardware, let's first check our program by running a local simulation on one of AWS's classical servers.

from braket.devices import LocalSimulator

classical_device = LocalSimulator("braket_ahs")

nshots = 1000
task = classical_device.run(ahs_program, shots=nshots)

# The result can be downloaded directly into an object in the python session:
result = task.result()
Copy
What phase of matter did we create? Let's have a look at the average density of atoms on each site:

from ahs_utils import get_avg_density, plot_avg_density_2D

n_rydberg = get_avg_density(result)
plot_avg_density_2D(n_rydberg, register)
Copy
simulating a 1-d qubit array using Bloqade
Indeed, we already see from this classical simulation that a pattern emerges: The alternating occupation density in the chain of atoms indicates a so-called Z2 phase.

Simulation on Aquila

And now for the truly exciting part - let's bring Aquila into the game.

Note: Running this program on the Aquila processor will incur a cost of 0.30 USD for submitting the program and 0.01 USD per shot. For scientific purposes, one would run this notebook with 1000 shots per task. To simply get a feel for the workflow of using Braket, however, we recommend reducing the number of shots. Furthermore, AWS offers 1 hour of free simulation time per month.

from braket.aws import AwsDevice, AwsSession
from boto3 import Session

boto_session = Session(region_name="us-east-1")
aws_session = AwsSession(boto_session)

aquila = AwsDevice("arn:aws:braket:us-east-1::device/qpu/quera/Aquila",aws_session)

# To make the program compatible with the quantum hardware, we still need to slice it into discrete time steps:
discretized_ahs_program = ahs_program.discretize(aquila)

task = aquila.run(discretized_ahs_program, shots=nshots)
result = task.result()
f,ax = show_final_avg_density(result)
plt.show()
Copy
1-d qubit array
Success! We can clearly observe an alternating occupation density, i.e. the Z2 phase, just as in the previous classical simulation.

2D Case

Now that we are familiar with the basic workflow, we can follow the same steps to extend our investigation to the 2D case.

Let's start off with a simple square lattice with 3x3 atoms. Why? This system is again small enough to first simulate it on classical hardware and thus benchmark the quantum device.

b = 6.7e-6  # meters
N_x = N_y = 3

register_2D = AtomArrangement()
for i in range(N_x):
    for j in range(N_y):
        register_2D.add([i*b, j * b])

show_register(register_2D)
Copy
tightly-spaced 2-d array of qubits

H_2D = Hamiltonian()

omega_min = 0       
omega_max = 2.5e6 * 2 * np.pi
detuning_min = -8.75e6 * 2 * np.pi
detuning_max = 8.75e6 * 2 * np.pi

time_max = 3e-6
time_ramp = 0.25e-6

time_points = [0, time_ramp, time_max - time_ramp, time_max]
omega_values = [omega_min, omega_max, omega_max, omega_min]
detuning_values = [detuning_min, detuning_min, detuning_max, detuning_max]
phase_values = [0, 0, 0, 0]

drive_2D = get_drive(time_points, omega_values, detuning_values, phase_values)
H_2D += drive_2D
Copy
Now, we run the simulation on classical hardware:

ahs_program_2D = AnalogHamiltonianSimulation(
    register=register_2D, 
    hamiltonian=H_2D
)

result_2D = classical_device.run(ahs_program_2D, shots=nshots).result()

plot_avg_density_2D(get_avg_density(result_2D), register_2D)
Copy
Simulating a 2D qubit array using Bloqade
We see a pattern emerging, similar to the Z2 phase in the 1D case. In 2D, this pattern is typically referred to as the checkerboard phase.
But will we observe the same behavior on Aquila?

ahs_program_finale = AnalogHamiltonianSimulation(
    register=register_finale, 
    hamiltonian=H_2D
)

result_2D = aquila.run(ahs_program_2D, shots=nshots).result()

plot_avg_density_2D(get_avg_density(result_2D), register_2D);
Copy
Running a 2D qubit array on the Aquila quantum computer

Finale: Moving beyond classical limits

So what's the big picture? We've walked through two best practice examples of simulating the order in a quantum many-body system, first on classical hardware and then on an actual quantum device, namely Aquila.

But what's the advantage in using Aquila? Well, having gained confidence in Aquila, we can use this quantum device to move into a regime that is not so easy to simulate classically. To give you an impression, let's make one final plot showing the order of an 11x11 square lattice of neutral atoms.

b = 6.7e-6  # meters
N_x = N_y = 11

register_finale = AtomArrangement()
for i in range(N_x):
    for j in range(N_y):
        register_finale.add([j*b, i * b])
Copy

ahs_program_finale = AnalogHamiltonianSimulation(
    register=register_finale, 
    hamiltonian=H_2D
)

result_2D = aquila.run(ahs_program_2D, shots=nshots).result()

plot_avg_density_2D(get_avg_density(result_2D), register_2D);
Copy
A qubit array with 100+ qubits
Indeed, the checkerboard phase clearly emerges. We have successfully created a complex many-body quantum state in which 121 atoms interact in Aquila to create an ordered phase of matter!

Connecting to Braket

Now that you've understood the basic workflow of using the Braket SDK you are ready to start submitting your own tasks to Aquila for real. Before heading over to the full Braket documentation page, here's an outline of the five most important steps you will be taken through there:
  1. Create an AWS account for free
  2. Open the Braket console
  3. Head to the Permissions & Settings subpage to enable Braket
  4. Stay on the Permissions & Settings subpage to enable third-party devices (such as QuEra's Aquila)
  5. Get started on your first notebook (All the code above is also available as one complete notebook in QuEra's public GitHub repository here. You can copy-paste the url into an AWS notebook instance if you want to try the code out for real)

Aquila is at your fingertips!