Walkthrough 3: Optical barriers¶

Introduction¶

The next level of control of quantum matter is made possible by altering the potential energy as a function of position by applying far-detuned light fields to the atom ensemble during the manipulation phase. In the current hardware implementation, the "painted" light introduced to the atom ensemble is blue-detuned from resonance, and thus represents a repulsive potential energy to the atoms via the AC Stark shift. The spatially-dependent potential energy depends on the local intensity of the light field, thus dynamic control of the local intensity alters the potential energy landscape in which the atoms are trapped. In an abstract sense, through Oqtant users have control of this light field by creating and manipulating one or more Barrier objects, which are superimposed on the background magnetic potential that traps the atoms.

In this walkthrough, we will explore application of quasi-1D optical barriers on Oqtant's cigar-shaped atomic ensembles. Users have control over the dynamic position (in microns), width (in microns), energetic height (in kHz), and shape of the barrier objects along the long dimension of the ensemble. The effective 1D potential is achieved by rastering the applied light along one short axis, while the light propagates along the other short axis. Application of these barriers allows for the ensemble to be split, recombined, swept to one side, or any number of other desired manipulations.

Imports and user authentication¶

In [ ]:
from oqtant.oqtant_client import get_oqtant_client
from oqtant.util.auth import notebook_login
from oqtant.schemas.quantum_matter import QuantumMatterFactory as qmf

oqtant_account = notebook_login()
oqtant_account
In [ ]:
client = get_oqtant_client(oqtant_account.access_token)

Barrier objects¶

Barriers are included in QuantumMatter and associated OqtantJob objects by passing a list of Barrier objects during instantiation. We begin by exploring how these (effectively 1D) objects are described and instantiated.

Creating a Barrier object¶

Let us begin by creating a Gaussian-shaped barrier centered at a position of 10 (microns) with an energetic height of 30 (kHz) and a width of 3 (microns). Let us plan on having a lifetime of 10 ms for the manipulation phase of our QuantumMatter object, with our barrier being applied starting at t = 2.5 ms and lasting until t = 7.5 ms, i.e. a barrier lifetime of 5 ms. Recall that the manipulation phase starts, by definition, at t = 0. Just as for QuantumMatter objects themselves, we create Barrier objects using our QuantumMatterFactory (qmf):

In [ ]:
barrier = qmf.create_barrier(
    position=10,
    height=30,
    width=3,
    birth=2.5,  # barrier initially applied here
    lifetime=5,  # before being destroyed after this amount of time
    shape="GAUSSIAN",
)

Visualizing a Barrier object at fixed times¶

What does our barrier look like? We can use the Barrier.show_potential() method to find out. This method accepts a list of times for which to plot the position-dependent potential energy represented by the barrier:

In [ ]:
barrier.show_potential(times=[0, 5])

We can see that at t = 0, our barrier hasn't been "born" yet, and the potential energy is zero everywhere. However, at t = 5 our barrier is now present (and will remain present until the end of its lifetime at t = 7.5).

Visualizing a Barrier's dynamics¶

We can also view a barrier's dynamics over the time of the manipulation phase.

In [ ]:
barrier.show_dynamics()

We see the constructed values (of position, width, and height) for times between our barrier's "birth" (at t = 2.5) and its "death" (at t = 7.5). Outside of its lifetime, this barrier will not exist (no light will be projected onto the atoms, at least from this particular barrier).

Scripting a Barrier's behavior¶

So far, our barrier is static, i.e. its position, width, and height are all constants during its lifetime. If we want to add some dynamics to the barrier, we can use the Barrier.evolve() method, which allows us to generically script a barrier's behavior in time.

In [ ]:
barrier = qmf.create_barrier(
    position=-10,
    height=50,
    width=3,
    birth=1,  # start with these parameters at t = 1
    lifetime=2,  # and hold with those conditions for 2 ms
    shape="GAUSSIAN",
)

# add dynamics, automatically increasing the barrier lifetime
barrier.evolve(duration=2.5, position=25, height=25, width=5)

# and more dynamics after that
barrier.evolve(duration=5, position=-25, height=5, width=1)

# show the barrier at the following times, along with the overall dynamics
barrier.show_dynamics()
barrier.show_potential(times=[2, 5, 9])

We can see that we now have a highly dynamic barrier whose position, height, and width change over the course of its lifetime. Any parameters that are to remain unchanged during a scripted evolution step can be omitted from the call to Barrier.evolve().

NOTE: a barrier object's shape is not dynamically controllable. However, such behavior is supported by so-called Landscape objects, introduced in the following walkthrough.

NOTE: A barrier object's lifetime will automatically be extended by the duration parameter passed to the Barrier.evolve() method.

If you wish to immediately start evolving the barrier after creation, you can omit the lifetime parameter passed to the instantiation and then ensure to evolve the barrier as desired:

In [ ]:
# start with these conditions
barrier = qmf.create_barrier(position=0, height=25, width=7, shape="GAUSSIAN", birth=1)

# and immediately start evolving
barrier.evolve(duration=2.5, position=25, height=25, width=5)

# show me
barrier.show_dynamics()

Available Barrier shapes¶

Supported Barrier shape options include GAUSSIAN, SQUARE, and LORENTZIAN. In all three cases, the width parameter means something slightly different, but for consistency generally refers to the appropriate half width of the functional form. The below table summarizes the potential energy ($U(x)$) as a function of position ($x$), barrier height $h$ , position $x_0$, and width $w$, for the currently supported barrier shapes:

specified shape $U(x)$ functional form width ($w$) interpretation
GAUSSIAN $ h e^{-\frac{(x - x_0)^2}{2 w^2}} $ gaussian width
LORENTZIAN $ \frac{h}{1 + (x - x_0)^{2} / w^{2}} $ half-width half-max
SQUARE $h$ if $abs(x-x_0) \leq w$, else $0$ half width

Barriers of different shape can be created as follows:

In [ ]:
shapes = ["GAUSSIAN", "LORENTZIAN", "SQUARE"]
for shape in shapes:
    barrier = qmf.create_barrier(
        position=0, height=50, width=3, shape=shape, lifetime=10
    )
    barrier.show_potential([5])

Adding Barriers to QuantumMatter objects¶

All that we need to do to add one or more Barrier objects to a QuantumMatter object is pass a list of them as a barriers input parameter during instantiation:

Instantiation¶

In [ ]:
# define the first barrier, with initial hold + evolution
barrier1 = qmf.create_barrier(
    position=10, height=30, width=3, birth=2.5, lifetime=5, shape="GAUSSIAN"
)
barrier1.evolve(duration=2, position=20, height=20, width=5)

# and a second one, also with initial hold + evolution
barrier2 = qmf.create_barrier(
    position=-10, height=30, width=3, birth=2.5, lifetime=7.5, shape="SQUARE"
)
barrier2.evolve(duration=4, position=-25, height=5, width=5)

# and construct the desired QuantumMatter object
barrier_matter = qmf.create_quantum_matter(
    temperature=100,
    lifetime=15,
    time_of_flight=10,
    barriers=[barrier1, barrier2],
    name="Now with barriers!",
)

Visualizing multi-barrier dynamics of the QuantumMatter object¶

The dynamics of all barrier objects in a QuantumMatter object can be visualized simultaneously using the QuantumMatter.show_barrier_dynamics() method:

In [ ]:
barrier_matter.show_barrier_dynamics()

Visualizing the total potential energy¶

It is also useful to visualize the overall total spatial energy profile at specific times in the manipulation phase, including contributions from both the (magnetic) trapping potential as well as the (optical) barrier contributions. This can be achieved using the QuantumMatter.show_potential() method. Just like the similar method available to Barrier objects themselves, this method accepts a list of times for which to plot the potential energy:

In [ ]:
barrier_matter.show_potential(times=[0, 5, 14])

As we can see, at t = 0 the potential energy profile is identical to the purely-magnetic trapping potential seen in the last walkthrough as no barriers have yet been created. At t = 5, we have two barriers of different shapes centered at +/- 10 microns, superimposed onto the background magnetic trapping potential. At t = 14 ms, barrier 1 has completed its lifetime and is no longer present, leaving only barrier 2, which by that time has shifted over to a position of -25, increased its width, and reduced its height.

WARNING: The total optical potential energy at a single position is limited to be 100 kHz in the current hardware implementation. Tall barriers that are close enough to spatially overlap can encounter this limit.

In-trap imaging option with barriers¶

The real power of in-trap imaging, initially encountered in the previous walkthough, is combining this option with the presence of barriers. The combination allows for direct imaging of atom ensembles that are swept, split, or shifted, while or shortly after barriers are being applied. These manipulations can act as the basis for studying quantum dynamics, atomtronics, and developing quantum sensors.

In [ ]:
# create a simple static barrier
barrier = qmf.create_barrier(
    position=-10, height=30, width=3, shape="GAUSSIAN", birth=2.5, lifetime=7.5
)

# and QuantumMatter with a lifetime so that the barrier exists right up to the end
barrier_matter = qmf.create_quantum_matter(
    temperature=100,
    lifetime=barrier.death,
    barriers=[barrier],
    image="IN_TRAP",  # take an in-trap image to directly image barrier effects
    note="in-trap imaging with barrier",
)

job_id = client.submit(barrier_matter, track=True)
In [ ]:
my_barrier_job = client.get_job(job_id)
In [ ]:
my_barrier_job.plot_it()

Advanced topics and discussion¶

Hardware implementation limits and effects¶

Projected barrier resolution¶

In the hardware implementation, the optical projection system (that projects the barrier light) has finite resolution. In its current form, the smallest diameter feature that can be projected, in terms of the 1/e^2 diameter, is around 2.5 microns. Barrier programs with very thin barriers can reveal these hardware design choices, resulting in, among other effects, barrier shapes losing fidelity as the barrier width becomes narrower.

Spatial resolution of barrier positions and widths¶

Currently, the effective one-dimensional barriers are calculated and projected on an spatial grid with a spacing of 1 micron. This means, for instance, that if a smoothly increasing barrier width is specified that it will actually increase in discrete steps -- both due to the temporal update time of recalculating barrier parameters every 100 microseconds (dynamics limitations, discussed above) and due to this spatial calculation grid. The largest jumps in, e.g. width, will occur for very narrow barriers as their width transitions across integer values (depending on the barrier center) -- at the next timestep the barrier width will suddenly have a new position/frequency contribution and it will increase in a discrete manner. A similar phenomenon occurs as a barrier is scanned in position.

Dynamics limitations¶

Another hardware/implementation limit can be observed if very fast dynamics are programmed into barrier objects. The optical projection system has finite bandwidth and, as a result, dynamic barrier widths/positions/heights are recalculated every 100 microseconds. Between these regular dynamic updates, the instantaneous barrier parameters, determined by the dynamics of the projected light, are static.

We can show this "update rate" limitation in the visualization tools for barrier dynamics etc. by passing the optional corrected boolean flag, which defaults to False, to our show() methods. This can be applied at the level of a Barrier object or an entire program:

In [ ]:
# define the first barrier, with initial hold + evolution
barrier1 = qmf.create_barrier(
    position=10, height=30, width=3, birth=2.5, lifetime=5, shape="GAUSSIAN"
)
barrier1.evolve(duration=2, position=20, height=20, width=5)

# and a second one, also with initial hold + evolution
barrier2 = qmf.create_barrier(
    position=-10, height=30, width=3, birth=2.5, lifetime=7.5, shape="SQUARE"
)
barrier2.evolve(duration=4, position=-25, height=5, width=5)

# and construct the desired QuantumMatter object
barrier_matter = qmf.create_quantum_matter(
    temperature=100,
    lifetime=15,
    time_of_flight=10,
    barriers=[barrier1, barrier2],
    name="Now with barriers!",
)

barrier_matter.show_barrier_dynamics(corrected=True)

You can observe the discrete steps of barrier parameters at the update period of 100 microseconds. These updates are more obvious when one or more of the parameters are changing rapidly.

Peeking under the hood¶

In the background of Oqtant, barrier programs have nearly identical structure as quantum programs. The only difference is that the optical_barriers field has been populated. As before, the underlying data structure can be useful for advanced program construction or manipulation.

In [ ]:
print(barrier_matter.model_dump())