Job Files for Complete Examples
To be able to run the complete examples without having to submit your program to hardware and wait, you'll need to download the associated job files. These files contain the results of running the program on the quantum hardware.
You can download the job files by clicking the "Download Job" button above. You'll then need to place
the job file in the data
directory that was created for you when you ran the import
part of the script
(alternatively you can make the directory yourself, it should live at the same level as wherever you put this script).
Multi-qubit Blockaded Rabi Oscillations¶
Introduction¶
In this tutorial we will show you how to compose geometries with pulse sequences to perform multi-qubit blockaded Rabi oscillations. The Physics here is described in detail in the whitepaper. But in short, we can use the Rydberg blockade to change the effective Rabi frequency of the entire system by adding more atoms to the cluster.
from bloqade import start, save, load
from bloqade.atom_arrangement import Chain, Square
import numpy as np
import matplotlib.pyplot as plt
import os
if not os.path.isdir("data"):
os.mkdir("data")
Defining the Geometry¶
We will start by defining the geometry of the atoms. The idea here is to cluster
the atoms so that they are all blockaded from each other. Using a combination of the
Chain
and Square
classes, as a base, one can add additional atoms to the geometry
using the add_position
method. This method takes a list of tuples, or a single
tuple, of the form (x,y)
where x
and y
are the coordinates of the atom in units
of the lattice constant.
distance = 4.0
inv_sqrt_2_rounded = 2.6
geometries = {
1: Chain(1),
2: Chain(2, lattice_spacing=distance),
3: start.add_position(
[(-inv_sqrt_2_rounded, 0.0), (inv_sqrt_2_rounded, 0.0), (0, distance)]
),
4: Square(2, lattice_spacing=distance),
7: start.add_position(
[
(0, 0),
(distance, 0),
(-0.5 * distance, distance),
(0.5 * distance, distance),
(1.5 * distance, distance),
(0, 2 * distance),
(distance, 2 * distance),
]
),
}
Defining the Pulse Sequence¶
Next, we will define the pulse sequence. We start from the start
object, which is
an empty list of atom locations. In this case, we do not need atoms to build the pulse
sequence, but to extract the sequence, we need to call the parse_sequence
method.
This creates a Sequence
object that we can apply to multiple geometries.
sequence = start.rydberg.rabi.amplitude.uniform.piecewise_linear(
durations=["ramp_time", "run_time", "ramp_time"],
values=[0.0, "rabi_drive", "rabi_drive", 0.0],
).parse_sequence()
Defining the Program¶
Now, all that is left to do is to compose the geometry and the Pulse sequence into a
fully defined program. We can do this by calling the apply
method on the geometry
and passing in the sequence. This method will return an object that can then be
assigned parameters.
batch = (
geometries[7]
.apply(sequence)
.assign(ramp_time=0.06, rabi_drive=5)
.batch_assign(run_time=0.05 * np.arange(21))
)
Run Emulator and Hardware¶
Again, we run the program on the emulator and Aquila and save the results to a file so we can use them later.
Hardware Execution Cost
For this particular program, 21 tasks are generated with each task having 100 shots, amounting to USD \$27.30 on AWS Braket.
emu_filename = os.path.join(
os.path.abspath(""), "data", "multi-qubit-blockaded-emulation.json"
)
if not os.path.isfile(emu_filename):
emu_batch = batch.bloqade.python().run(10000, interaction_picture=True)
save(emu_batch, emu_filename)
filename = os.path.join(os.path.abspath(""), "data", "multi-qubit-blockaded-job.json")
if not os.path.isfile(filename):
hardware_batch = batch.parallelize(24).braket.aquila().run_async(shots=100)
save(hardware_batch, filename)
Plotting the Results¶
First, we load the results from the file.
emu_batch = load(emu_filename)
hardware_batch = load(filename)
# hardware_batch.fetch()
# save(filename, hardware_batch)
The quantity of interest here is the total Rydberg density of the cluster defined as
the sum of the Rydberg densities of each atom. We can extract this from the results
and plot it as a function of time. We will do this for both the emulator and the
hardware. We can use the rydberg_densities
function to extract the densities from
the Report
of the batch
object.
emu_report = emu_batch.report()
emu_densities = emu_report.rydberg_densities()
emu_densities_summed = emu_densities.sum(axis=1)
hardware_report = hardware_batch.report()
hardware_densities = hardware_report.rydberg_densities()
hardware_densities_summed = hardware_densities.sum(axis=1)
emu_run_times = emu_report.list_param("run_time")
hw_run_times = hardware_report.list_param("run_time")
fig, ax = plt.subplots()
ax.set_xlabel("Time")
ax.set_ylabel("Sum of Rydberg Densities")
# emulation
ax.plot(emu_run_times, emu_densities_summed, label="Emulator", color="#878787")
# hardware
ax.plot(hw_run_times, hardware_densities_summed, label="QPU", color="#6437FF")
ax.legend()
ax.set_xlabel("Time ($\mu s$)")
ax.set_ylabel("Sum of Rydberg Densities")
plt.show()