# ---
# jupyter:
# jupytext:
# formats: ipynb,py:percent
# hide_notebook_metadata: false
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.14.5
# kernelspec:
# display_name: .venv
# language: python
# name: python3
# ---
# %% [markdown]
# Download Script
# Download Job
#
#
#
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).
#
#
#
# %% [markdown]
# # 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](https://arxiv.org/abs/2306.11727). 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.
# %%
import os
import numpy as np
import matplotlib.pyplot as plt
from bloqade.analog import load, save, start
from bloqade.analog.atom_arrangement import Chain, Square
if not os.path.isdir("data"):
os.mkdir("data")
# %% [markdown]
# ## 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),
]
),
}
# %% [markdown]
# ## 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()
# %% [markdown]
# ## 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))
)
# %% [markdown]
# ## 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)
# %% [markdown]
# ## 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)
# %% [markdown]
# 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()