Building 1D Rydberg Crystals

The following notebook shows a study of many-body dynamics on a 1D system. It is based on 1707.04344. The authors of that paper studied the preparation of symmetry-breaking states in antiferromagnetic Ising chains, by tuning the interaction and driving parameters accross the phase diagram. In this notebook, we reproduce some results of this paper. Since this is a particular experiment not based on certified devices, we will use the MockDevice class to allow for a wide range of configuration settings.

[1]:
import numpy as np
import matplotlib.pyplot as plt
import qutip

from pulser import Pulse, Sequence, Register
from pulser_simulation import QutipEmulator
from pulser.waveforms import CompositeWaveform, RampWaveform, ConstantWaveform
from pulser.devices import MockDevice

1. Rydberg Blockade at Resonant Driving

We first consider clusters of \(1, 2\) and \(3\) atoms under resonant (\(\delta = 0\)) driving. If all the atoms are placed within each other’s blockade volume, only one excitation per group will be possible at a time. The Rabi frequency will be enhanced by \(\sqrt{N}\)

[2]:
def occupation(reg, j):
    r = qutip.basis(2, 0)
    N = len(reg.qubits)
    prod = [qutip.qeye(2) for _ in range(N)]
    prod[j] = r * r.dag()
    return qutip.tensor(prod)

Given a value of the maximum Rabi Frequency applied to the atoms, we can calculate the corresponding blockade radius using the rydberg_blockade_radius() method from MockDevice. We use this to arrange clusters of atoms which will experience this blockade effect:

[3]:
Omega_max = 2 * 2 * np.pi
R_blockade = MockDevice.rydberg_blockade_radius(Omega_max)
print(f"Blockade Radius is: {R_blockade}µm.")
groups = 3


def blockade_cluster(N):
    atom_coords = [
        ((R_blockade / N) * x + 40 * group, 0)
        for group in range(groups)
        for x in range(1, N + 1)
    ]
    reg = Register.from_coordinates(atom_coords, prefix="q")
    reg.draw(
        blockade_radius=R_blockade, draw_half_radius=True, draw_graph=False
    )

    resonant_pulse = Pulse.ConstantPulse(1500, Omega_max, 0.0, 0.0)

    seq = Sequence(reg, MockDevice)
    seq.declare_channel("ising", "rydberg_global")
    seq.add(resonant_pulse, "ising")

    simul = QutipEmulator.from_sequence(seq, sampling_rate=0.2)

    obs = [
        sum(occupation(reg, j) for j in range(i, i + N))
        for i in range(0, groups * N, N)
    ]

    res = simul.run(progress_bar=True, method="bdf")
    return res.expect(obs)
Blockade Radius is: 8.692279598222772µm.

Next we run blockade_cluster(N), which runs the simulation, for clusters of sizes \(N \in \{1,2,3\}\):

[4]:
data = [blockade_cluster(N) for N in [1, 2, 3]]
../_images/tutorials_1D_crystals_9_0.png
10.0%. Run time:   0.01s. Est. time left: 00:00:00:00
20.0%. Run time:   0.02s. Est. time left: 00:00:00:00
30.0%. Run time:   0.02s. Est. time left: 00:00:00:00
40.0%. Run time:   0.03s. Est. time left: 00:00:00:00
50.0%. Run time:   0.04s. Est. time left: 00:00:00:00
60.0%. Run time:   0.05s. Est. time left: 00:00:00:00
70.0%. Run time:   0.07s. Est. time left: 00:00:00:00
80.0%. Run time:   0.07s. Est. time left: 00:00:00:00
90.0%. Run time:   0.08s. Est. time left: 00:00:00:00
Total run time:   0.08s
../_images/tutorials_1D_crystals_9_2.png
10.0%. Run time:   0.02s. Est. time left: 00:00:00:00
20.0%. Run time:   0.05s. Est. time left: 00:00:00:00
30.0%. Run time:   0.07s. Est. time left: 00:00:00:00
40.0%. Run time:   0.13s. Est. time left: 00:00:00:00
50.0%. Run time:   0.17s. Est. time left: 00:00:00:00
60.0%. Run time:   0.22s. Est. time left: 00:00:00:00
70.0%. Run time:   0.27s. Est. time left: 00:00:00:00
80.0%. Run time:   0.34s. Est. time left: 00:00:00:00
90.0%. Run time:   0.39s. Est. time left: 00:00:00:00
Total run time:   0.45s
../_images/tutorials_1D_crystals_9_4.png
10.0%. Run time:   1.02s. Est. time left: 00:00:00:09
20.0%. Run time:   1.92s. Est. time left: 00:00:00:07
30.0%. Run time:   2.74s. Est. time left: 00:00:00:06
40.0%. Run time:   3.70s. Est. time left: 00:00:00:05
50.0%. Run time:   4.74s. Est. time left: 00:00:00:04
60.0%. Run time:   5.74s. Est. time left: 00:00:00:03
70.0%. Run time:   7.16s. Est. time left: 00:00:00:03
80.0%. Run time:   8.58s. Est. time left: 00:00:00:02
90.0%. Run time:  10.25s. Est. time left: 00:00:00:01
Total run time:  12.29s

We now plot the probability that a Rydberg state withing the cluster is occupied (by summing the expectation values of the \(|r\rangle\langle r|_i\) operators for each cluster) as it evolves in time, revealing the Rabi frequency of each configuration:

[5]:
fig, ax = plt.subplots(1, 3, figsize=(18, 3))
for N, expectation in enumerate(data):
    ax[N].set_xlabel(r"Time ($µs$)", fontsize=10)
    ax[N].set_ylabel(r"Probability of $|r\rangle$", fontsize=10)
    ax[N].set_title(f"Atoms per cluster N = {N+1}", fontsize=12)
    avg = sum(expectation) / groups
    ax[N].plot(np.arange(len(avg)) / 1000, avg)
../_images/tutorials_1D_crystals_11_0.png

Only one excitation will be shared between the atoms on each cluster. Notice how the Rabi frequency increases by a factor of \(\sqrt{N}\)

2. Ordered Crystalline phases

The pulse sequence that we will prepare is based on the following parameters:

[6]:
# Parameters in rad/µs and ns
delta_0 = -6 * 2 * np.pi
delta_f = 10 * 2 * np.pi
Omega_max = 2 * 2 * np.pi
t_rise = 500
t_stop = 4500

We calculate the blockade radius from the maximal applied Rabi frequency:

[7]:
R_blockade = MockDevice.rydberg_blockade_radius(Omega_max)
a = 7.0

reg = Register.rectangle(1, 11, spacing=a, prefix="q")
print(f"Blockade Radius is: {R_blockade}µm.")
reg.draw(blockade_radius=R_blockade, draw_half_radius=True)
Blockade Radius is: 8.692279598222772µm.
../_images/tutorials_1D_crystals_17_1.png

Create the pulses using Pulser objects:

[8]:
hold = ConstantWaveform(t_rise, delta_0)
excite = RampWaveform(t_stop - t_rise, delta_0, delta_f)
sweep = Pulse.ConstantAmplitude(
    Omega_max, CompositeWaveform(hold, excite), 0.0
)
[9]:
seq = Sequence(reg, MockDevice)
seq.declare_channel("ising", "rydberg_global")

seq.add(sweep, "ising")

seq.draw()
../_images/tutorials_1D_crystals_20_0.png

The pulse sequence we just created corresponds a path in the Phase space of the ground state, which we represent schematically with the following function:

[10]:
def phase_diagram(seq):
    ratio = []
    for x in seq._schedule["ising"]:
        if isinstance(x.type, Pulse):
            ratio += list(x.type.detuning.samples / Omega_max)

    fig, ax = plt.subplots()
    ax.grid(True, which="both")

    ax.set_ylabel(r"Interaction Range $R_b/a$", fontsize=14)
    ax.set_xlabel(r"Detuning $\delta/\Omega$", fontsize=14)
    ax.set_xlim(-4, 6)
    ax.set_ylim(0, 3.2)
    ax.axhline(y=0, color="k")
    ax.axvline(x=0, color="k")

    y = np.arange(0.0, 5, 0.01)
    x = 2 * (0.6 + 8 * (y - 1.2) ** 2)
    ax.fill_between(x, y, alpha=0.4)

    y = np.arange(0.0, 5, 0.01)
    x = 2 * (0.8 + 50 * (y - 2.45) ** 2)
    ax.fill_between(x, y, alpha=0.4)

    y = np.arange(0.0, 5, 0.01)
    x = 2 * (1.0 + 170 * (y - 3.06) ** 2)
    ax.fill_between(x, y, alpha=0.4)

    ax.plot(np.array(ratio), np.full(len(ratio), R_blockade / a), "red", lw=2)
    plt.show()
[11]:
phase_diagram(seq)
../_images/tutorials_1D_crystals_23_0.png

2.1 Simulation

We run our simulation, for a list of observables corresponding to \(|r\rangle \langle r|_j\) for each atom in the register:

[12]:
simul = QutipEmulator.from_sequence(seq, sampling_rate=0.1)

occup_list = [occupation(reg, j) for j in range(len(reg.qubits))]

res = simul.run(progress_bar=True)
occupations = res.expect(occup_list)
10.0%. Run time:   0.76s. Est. time left: 00:00:00:06
20.0%. Run time:   1.61s. Est. time left: 00:00:00:06
30.0%. Run time:   2.55s. Est. time left: 00:00:00:05
40.0%. Run time:   3.27s. Est. time left: 00:00:00:04
50.0%. Run time:   3.86s. Est. time left: 00:00:00:03
60.0%. Run time:   4.40s. Est. time left: 00:00:00:02
70.0%. Run time:   4.99s. Est. time left: 00:00:00:02
80.0%. Run time:   5.60s. Est. time left: 00:00:00:01
90.0%. Run time:   6.35s. Est. time left: 00:00:00:00
Total run time:   7.21s

The following function plots the evolution of the expectation values with respect to time:

[13]:
def plot_evolution(results):
    plt.figure(figsize=(10, 5))
    plt.xlabel("Time (µs)", fontsize=14)
    plt.ylabel("Rydberg Occupation Probabilty", fontsize=14)
    for expv in results:
        plt.plot(np.arange(len(expv)) / 1000, expv)

    plt.show()
[14]:
plot_evolution(occupations)
../_images/tutorials_1D_crystals_29_0.png

We finally plot the probability of occupation of the Rydberg level with respect to the values of detuning, for each atom in the array:

[15]:
def heat_detuning(data, start, end):
    N = len(reg.qubits)
    time_window = []
    x = []
    detunings = simul.samples_obj.to_nested_dict()["Global"]["ground-rydberg"][
        "det"
    ][[int(1000 * t) for t in simul.evaluation_times[:-1]]]

    for t, d in enumerate(detunings):
        if start <= d <= end:
            time_window.append(t)
            x.append(d / (2 * np.pi))

    y = np.arange(1, N + 1)

    X, Y = np.meshgrid(x, y)
    Z = np.array(data)[:, time_window]

    plt.figure(figsize=(14, 3))
    plt.pcolormesh(X, Y, Z, cmap="hot", shading="auto")
    plt.xlabel("Detuning/2π (MHz)", fontsize=14)
    plt.ylabel("Atom in array", fontsize=14)
    plt.yticks(range(1, N + 1), [f"q{i}" for i in range(N)], va="center")
    plt.colorbar(fraction=0.047, pad=0.015)

    plt.show()
[16]:
heat_detuning(occupations, delta_0, delta_f)
../_images/tutorials_1D_crystals_32_0.png

2.2 Rydberg Crystals: \(Z_3\) Order

To arrive at a different phase, we reduce the interatomic distance \(a\), thus increasing the interaction range between the atoms. This will lead to a \(Z_3\) ordered phase:

[17]:
a = 3.5
reg = Register.rectangle(1, 10, spacing=a, prefix="q")

delta_0 = -4 * 2 * np.pi
delta_f = 10 * 2 * np.pi
Omega_max = 2.0 * 2 * np.pi  # btw 1.8-2 * 2pi MHz
t_rise = 600
t_stop = 2500
R_blockade = MockDevice.rydberg_blockade_radius(Omega_max)
reg.draw(blockade_radius=R_blockade, draw_half_radius=True)

#
hold = ConstantWaveform(t_rise, delta_0)
excite = RampWaveform(t_stop - t_rise, delta_0, delta_f)
sweep = Pulse.ConstantAmplitude(
    Omega_max, CompositeWaveform(hold, excite), 0.0
)
#
seq = Sequence(reg, MockDevice)
seq.declare_channel("ising", "rydberg_global")
seq.add(sweep, "ising")

phase_diagram(seq)

simul = QutipEmulator.from_sequence(seq, sampling_rate=0.1)

occup_list = [occupation(reg, j) for j in range(len(reg.qubits))]

res = simul.run(progress_bar=True, method="bdf")

occupations = res.expect(occup_list)

plot_evolution(occupations)
heat_detuning(occupations, delta_0, delta_f)
../_images/tutorials_1D_crystals_35_0.png
../_images/tutorials_1D_crystals_35_1.png
10.0%. Run time:   2.20s. Est. time left: 00:00:00:19
20.0%. Run time:   4.31s. Est. time left: 00:00:00:17
30.0%. Run time:   6.38s. Est. time left: 00:00:00:14
40.0%. Run time:   8.46s. Est. time left: 00:00:00:12
50.0%. Run time:  10.56s. Est. time left: 00:00:00:10
60.0%. Run time:  12.80s. Est. time left: 00:00:00:08
70.0%. Run time:  14.89s. Est. time left: 00:00:00:06
80.0%. Run time:  16.99s. Est. time left: 00:00:00:04
90.0%. Run time:  19.08s. Est. time left: 00:00:00:02
Total run time:  21.14s
../_images/tutorials_1D_crystals_35_3.png
../_images/tutorials_1D_crystals_35_4.png

2.3 Rydberg Crystals: \(Z_4\) Order

Decreasing even more the interatomic distance leads to a \(Z_4\) order. The magnitude of the Rydberg interaction with respect to that of the applied pulses means our solver has to control terms with a wider range, thus leading to longer simulation times:

[18]:
a = 2.8
reg = Register.rectangle(1, 9, spacing=a, prefix="q")

# Parameters in rad/µs and ns

delta_0 = -4 * 2 * np.pi
delta_f = 10 * 2 * np.pi
Omega_max = 2.0 * 2 * np.pi  # btw 1.8-2 2pi*MHz
t_rise = 600
t_stop = 2500
R_blockade = MockDevice.rydberg_blockade_radius(Omega_max)
reg.draw(blockade_radius=R_blockade, draw_half_radius=True)

#
hold = ConstantWaveform(t_rise, delta_0)
excite = RampWaveform(t_stop - t_rise, delta_0, delta_f)
sweep = Pulse.ConstantAmplitude(
    Omega_max, CompositeWaveform(hold, excite), 0.0
)

#
seq = Sequence(reg, MockDevice)
seq.declare_channel("ising", "rydberg_global")
seq.add(sweep, "ising")

phase_diagram(seq)

simul = QutipEmulator.from_sequence(seq, sampling_rate=0.4)

occup_list = [occupation(reg, j) for j in range(len(reg.qubits))]

#
res = simul.run(progress_bar=True, method="bdf")
occupations = res.expect(occup_list)

plot_evolution(occupations)
heat_detuning(occupations, delta_0, delta_f)
../_images/tutorials_1D_crystals_38_0.png
../_images/tutorials_1D_crystals_38_1.png
10.0%. Run time:   4.70s. Est. time left: 00:00:00:42
20.0%. Run time:  10.75s. Est. time left: 00:00:00:42
30.0%. Run time:  16.96s. Est. time left: 00:00:00:39
40.0%. Run time:  24.73s. Est. time left: 00:00:00:37
50.0%. Run time:  32.96s. Est. time left: 00:00:00:32
60.0%. Run time:  39.45s. Est. time left: 00:00:00:26
70.0%. Run time:  44.43s. Est. time left: 00:00:00:19
80.0%. Run time:  49.02s. Est. time left: 00:00:00:12
90.0%. Run time:  53.25s. Est. time left: 00:00:00:05
Total run time:  58.02s
../_images/tutorials_1D_crystals_38_3.png
../_images/tutorials_1D_crystals_38_4.png