# ---
# 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]
# # 1D Z2 State Preparation
# ## Introduction
# In this example we show how to create the Z2 ordered phase on a 1D chain of atoms and
# how to perform a scan over the sweep time to understand the behavior of an adiabatic
# sweep and the effect of the Rydberg blockade on a many-body system.
# %% [markdown]
# Let's import all the tools we'll need.
# %%
import os
import numpy as np
import matplotlib.pyplot as plt
from bloqade.analog import load, save
from bloqade.analog.atom_arrangement import Chain
if not os.path.isdir("data"):
os.mkdir("data")
# %% [markdown]
# ## Program Definition
# We define a program where our geometry is a chain of 11 atoms
# with a distance of 6.1 micrometers between atoms.
# The pulse schedule presented here should be reminiscent of the Two Qubit Adiabatic
# Sweep example although we've opted to reserve variable usage for values that will
# actually have their parameters swept.
# %%
# Define relevant parameters for the lattice geometry and pulse schedule
n_atoms = 11
lattice_spacing = 6.1
min_time_step = 0.05
# %% [markdown]
# We choose a maximum Rabi amplitude of 15.8 MHz.
# Pushing the Rabi amplitude as high as we can minimizes the protocol duration,
# but maintains the same pulse area, $\Omega t$. For this reason, in many cases,
# maximizing the Rabi frequency is considered good practice for minimizing decoherence effects.
# %%
rabi_amplitude_values = [0.0, 15.8, 15.8, 0.0]
# %% [markdown]
# The lattice spacing and Rabi amplitudes give us a nearest neighbor interaction strength:
# $$V_{{i},{i+1}} = \frac{C_6}{a^6} \approx 105.21 \, \text{MHz} \gg \Omega = 15.8 \, \text{MHz}$$
# where $C_6 = 2\pi \times 862690 \, \text{MHz} \, \mu \text{m}^6$ is our van der Waals coefficient
# for Aquila hardware and $a$ is the lattice spacing we defined earlier.
# Our interaction strength for next-nearest neighbors is quite low comparatively:
# $$V_{{i},{i+2}} = \frac{C_6}{(2a)^6} \approx 1.64 \, \text{MHz} \ll \Omega = 15.8 \, \text{MHz}$$
# The Rydberg interaction term dominates for nearest neighbor spacing, while the Rabi coupling dominates
# for next-nearest neighbors.
# This increases the probability of realizing a Rydberg blockade for nearest neighbors,
# but decreases the probability of Rydberg interaction between next-nearest neighbors.
# So far, we're in a good position for creating a Z2 phase.
# Next, we define our detuning values.
# %%
rabi_detuning_values = [-16.33, -16.33, 16.33, 16.33]
# %% [markdown]
# We start at large negative detuning values where all atoms are in the ground state.
# Then, we transition to large positive detuning values where the Rydberg state
# becomes energetically favorable and inter-atomic interactions become more important.
# The maximum absolute detuning value of $16.33 \, \text{MHz}$ gives us a Rydberg blockade radius
# $$R_b = \Bigl(\frac{C_6}{\sqrt{\Delta^2+\Omega^2}}\Bigr)^{1/6} \approx 7.88 \mu \text{m}$$
# Typically, we define the lattice spacing such that $a < R_b < 2a$ for a good blockade approximation
# and Z2 state probability.
# Lastly, we define a set of test durations over which to execute our pulses
# and write the instructions for our program.
# %%
durations = [0.8, "sweep_time", 0.8]
# Note the addition of a "sweep_time" variable
# for performing sweeps of time values.
time_sweep_z2_prog = (
Chain(n_atoms, lattice_spacing=lattice_spacing)
.rydberg.rabi.amplitude.uniform.piecewise_linear(durations, rabi_amplitude_values)
.detuning.uniform.piecewise_linear(durations, rabi_detuning_values)
)
# Allow "sweep_time" to assume values from 0.05 to 2.4 microseconds for a total of
# 20 possible values.
# Starting at exactly 0.0 isn't feasible so we use the `min_time_step` defined
# previously.
time_sweep_z2_job = time_sweep_z2_prog.batch_assign(
sweep_time=np.linspace(min_time_step, 2.4, 20)
)
# %% [markdown]
# ## Running on the Emulator and Hardware
# With our program properly composed we can now easily send it off to both the emulator
# and hardware.
# We select the Braket emulator and tell it that for each variation of the "time_sweep"
# variable we'd like to run 10000 shots. For the hardware we take advantage of the fact
# that 11 atoms takes up so little space on the machine we can duplicate that geometry
# multiple times to get more data per shot. We set a distance of 24 micrometers between
# copies to minimize potential interactions between them.
# For both cases, to allow us to submit our program without having to wait on immediate
# results from hardware (which could take a while considering queueing and window
# restrictions), we save the necessary metadata to a file that can then be reloaded
# later and results fetched when they are available.
#
#
#
Hardware Execution Cost
#
#
# For this particular program, 20 tasks are generated with each task having 100 shots, amounting to
# __USD \\$26.00__ on AWS Braket.
#
#
#
# %%
emu_filename = os.path.join(os.path.abspath(""), "data", "time-sweep-emulation.json")
if not os.path.isfile(emu_filename):
emu_future = time_sweep_z2_job.bloqade.python().run(shots=10000)
save(emu_future, emu_filename)
filename = os.path.join(os.path.abspath(""), "data", "time-sweep-job.json")
if not os.path.isfile(filename):
future = time_sweep_z2_job.parallelize(24).braket.aquila().run_async(shots=100)
save(future, filename)
# %% [markdown]
# ## Plotting the Results
# To make our lives easier we define a trivial function to
# extract the probability of the Z2 phase from each of the tasks generated from the
# parameter sweep. The counts are obtained from the `report`of the batch object.
# %%
def get_z2_probabilities(report):
z2_probabilities = []
for count in report.counts():
z2_probability = count["01010101010"] / sum(list(count.values()))
z2_probabilities.append(z2_probability)
return z2_probabilities
# %% [markdown]
# ## Extracting Counts And Probabilities
# We will now extract the counts and probabilities
# from the emulator and hardware runs. We will then plot the results. First we load the
# data from the files:
# %%
# retrieve results from HW
emu_batch = load(emu_filename)
hardware_batch = load(filename)
# Uncomment lines below to fetch results from Braket
# hardware_batch = hardware_batch.fetch()
# save(hardware_batch, filename)
# %% [markdown]
# To get the counts we need to get a report from the batch objects. Then with the
# report we can get the counts. The counts are a dictionary that maps the bitstring to
# the number of times that bitstring was measured.
# %%
emu_report = emu_batch.report()
hardware_report = hardware_batch.report()
emu_probabilities = get_z2_probabilities(emu_report)
hardware_probabilities = get_z2_probabilities(hardware_report)
emu_sweep_times = emu_report.list_param("sweep_time")
hardware_sweep_times = hardware_report.list_param("sweep_time")
plt.plot(emu_sweep_times, emu_probabilities, label="Emulator", color="#878787")
plt.plot(hardware_sweep_times, hardware_probabilities, label="QPU", color="#6437FF")
plt.legend()
plt.show()
# %% [markdown]
# We can also plot the emulated Z2 ordered phase for a specific sweep time.
# Here, we extract data for a sweep time of $0.67\mu s$ or a total pulse duration of $2.27\mu s$.
# %%
densities = emu_report.rydberg_densities()
site_indices = densities.loc[0].index.values
rydberg_densities_67_sweep = densities.loc[5, 0:10].values
plt.bar(site_indices, rydberg_densities_67_sweep, color="#C8447C")
plt.xticks(site_indices)
plt.title("Z2 Phase Rydberg Densities for 2.27$\mu$s Total Pulse Duration")
plt.xlabel("Atom Site Index")
plt.ylabel("Rydberg Density")
plt.show()
# %% [markdown]
# Similarly, we can visualize the emulated Rydberg densities of each site index as
# the sweep time increases and we approach adiabatic evolution.
# %%
rydberg_densities = densities.values.transpose()
im = plt.imshow(rydberg_densities)
plt.xticks(rotation=90)
plt.xticks(
[x for x in range(len(emu_sweep_times))], [round(dur, 2) for dur in emu_sweep_times]
)
plt.yticks(site_indices)
plt.xlabel("Sweep Time ($\mu$s)")
plt.ylabel("Atom Site Index")
plt.colorbar(im, shrink=0.6)
plt.show()
# %% [markdown]
## Analysis
# As expected, we see that if we allow the pulse schedule to run for a longer
# and longer period of time (more "adiabatically") we have an increasing
# probability of creating the Z2 phase.