Walkthrough 4: Optical landscapes¶
Introduction¶
While Barrier objects explored in the last
walkthrough are quite powerful, there are instances where the
limits imposed on the user by this abstraction are not flexible
enough (e.g., the barrier shapes are pre-determined, etc.). In
this walkthrough, we explore a more customizable way to alter
the potential energy, as a function of position, experienced by
the atom ensemble during the experiment phase. Our new objects
consist of LandscapeSnapshots , which are optical
potentials specified by providing a spatially-dependent
potential energy at given positions and a fixed time during the
experiment phase. Dynamics are supported by allowing the user to
specify many such snapshots, each with a unique time, along with
control over how those snapshots are connected/interpolated
point-by-point. This construction is called a
Landscape and is the focus of this walkthrough.
NOTE: Landscape-type potentials are sourced from the same laser light as barriers and represent another abstraction available for the user to explore the "painted light" capabilities of Oqtant hardware.
Imports and user authentication¶
from oqtant.oqtant_client import get_client
from oqtant.schemas.quantum_matter import (
Barrier,
LandscapeSnapshot,
Landscape,
QuantumMatter,
)
client = get_client()
LandscapeSnapshot objects¶
LandscapeSnapshot objects are the basis of our new
abstraction. They represent the desired
instantaneous optical potential applied to the atom
ensemble at a particular time. Here, we will often refer to them
only as snapshots.
Object creation¶
We can construct such a snapshot by passing equal length lists of positions (in microns) and corresponding potentials (in kHz), the time (in ms) at which this snapshot should be realized, and an interpolation parameter that communicates how to connect the given data in space.
snapshot = LandscapeSnapshot(
time=2,
positions=[-10, -5, 0, 5, 10],
potentials=[0, 10, 20, 15, 0],
interpolation="LINEAR",
)
Visualizing the instantaneous potential energy contribution¶
Much like for barriers, we can use the
LandscapeSnapshot.show_potential() method to
visualize the potential energy contribution of a particular
snapshot:
snapshot.show_potential()
We can see the underlying data used to instantiate the landscape object (orange points) and how the potential energy at other positions will be calculated based on that data (blue line) according to the interpolation choice.
Spatial interpolation options¶
The interpolation parameter passed to the constructor of our snapshot object controls how the given points formed by the equal length lists of (positions, potentials) are connected spatially (as opposed to the previously encountered rf evaporation and barrier interpolation inputs, which controlled how points were connected in time.) Options for spatial interpolation include those options familiar to users of the scipy library and are summarized in the following table:
| Interpolation parameter value | notes |
|---|---|
| "ZERO" | Spline interpolation at zeroth order |
| "SLINEAR" | Spline interpolation at first order |
| "QUADRATIC" | Spline interpolation at second order |
| "CUBIC" or "SMOOTH" | Spline interpolation at third order |
| "OFF" or "STEP" or "PREVIOUS" | Assumes value of previous data point |
| "NEXT" | Assumes value of next data point |
| "LINEAR" | Linear interpolation between points |
Note: The total optical potential applied to the quantum matter sample is bounded below by 0 kHz (no painted light at that position) and above by 100 kHz, which under certain circumstances can alter the expected potential energy landscape. This is particularly true for high-order interpolation options, e.g. cubic, which tend to overshoot or undershoot these bounds for points close in proximity to them. Also, just as for multiple barriers that overlap, the presence of snapshots/landscapes and barriers together can lead to optical potentials that sample this energetic ceiling.
options = ["OFF", "LINEAR", "CUBIC"]
for option in options:
snapshot = LandscapeSnapshot(
time=2,
positions=[-10, -5, 0, 5, 10, 25, 30, 39],
potentials=[0, 10, 20, 15, 0, 10, 15, 2],
interpolation=option,
)
snapshot.show_potential()
As shown, the snapshot data structure allows for very flexible potential-energy profiles.
Landscape objects¶
Landscape objects represent the dynamic potential
energy as a function of position realized by connecting a series
of snapshots together in time. A valid landscape needs at least
two constituent snapshots to define the potential at, in this
case, the start and end time that the landscape should be
applied. At intermediate times between the provided snapshots,
the overall landscape / potential energy as a function of
position is linearly interpolated point-by-point (in
position). This interpolation behavior is not user configurable.
NOTE: The time values of individual snapshots that make up the overall Landscape object must be at least 1 ms apart.
Let us demonstrate the instantiation of an landscape that evolves between a narrow/short barrier-like object to a wider/taller one with a different spatial interpolation style.
snapshot1 = LandscapeSnapshot(
time=2,
positions=[-10, -5, 0, 5, 10],
potentials=[0, 10, 20, 15, 0],
interpolation="LINEAR",
)
snapshot2 = LandscapeSnapshot(
time=5,
positions=[-20, -10, 0, 10, 20],
potentials=[0, 15, 40, 10, 0],
interpolation="CUBIC",
)
landscape = Landscape(snapshots=[snapshot2, snapshot1])
initializing Landscape... copying to optical landscape copying to optical landscape optical landscapes length: 2
We can observe this applied potential energy derived from our
landscape object at any particular time using the
Landscape.show_potential() method:
landscape.show_potential(times=[2, 3, 4, 5])
Adding a Landscape object to
QuantumMatter¶
Similar to how we added barriers, we can also add landscapes to
our quantum matter objects, in this case by providing a
landscape parameter during instantiation. In the below
example, we include both a simple Barrier as well
as our new Landscape:
# define a simple barrier, just as an example, that lives until t = 13
barrier = Barrier(
position=30, height=30, width=3, shape="GAUSSIAN", birth=3, lifetime=7
)
barrier.evolve(duration=3, height=15, position=-30)
# and the dynamic landscape, consisting of two snapshots for this example
snapshot1 = LandscapeSnapshot(
time=0,
positions=[-10, -5, 0, 5, 10],
potentials=[0, 10, 20, 15, 0],
interpolation="LINEAR",
)
snapshot2 = LandscapeSnapshot(
time=15,
positions=[-20, -10, 0, 10, 20],
potentials=[0, 15, 40, 10, 0],
interpolation="LINEAR",
)
landscape = Landscape(snapshots=[snapshot1, snapshot2])
# and construct the program
matter = QuantumMatter(
name="paint 1d job trial",
temperature=100,
lifetime=20,
time_of_flight=10,
barriers=[barrier],
landscape=landscape,
)
initializing Landscape... copying to optical landscape copying to optical landscape optical landscapes length: 2
Visualizing the overall potential energy¶
Just as in our examples in previous walkthroughs that did not
include the optional landscape parameter, we can show
the total spatial potential energy as a function of position for
various experiment times using the
QuantumMatter.show_potential() method. The total
potential will include contributions from both our QuantumMatter
object's constituent barriers, the new landscape, as well as the
background magnetic trapping field:
matter.show_potential(times=[2, 4, 8, 16])
As shown, the total potential includes both the evolving landscape as well as the scanning barrier (in addition to the magnetic trapping potential).
Submitting to QMS¶
my_job = client.convert_matter_to_job(matter=matter)
print(my_job.model_dump())
{'name': 'paint 1d job trial', 'origin': None, 'status': <JobStatus.PENDING: 'PENDING'>, 'display': True, 'qpu_name': <QPUName.UNDEFINED: 'UNDEFINED'>, 'inputs': [{'job_id': None, 'run': 1, 'values': {'end_time_ms': 20.0, 'image_type': <ImageType.TIME_OF_FLIGHT: 'TIME_OF_FLIGHT'>, 'time_of_flight_ms': 10.0, 'rf_evaporation': {'times_ms': [-1100.0, -1050.0, -800.0, -300.0, 0.0], 'frequencies_mhz': [21.12, 12.12, 5.12, 0.62, 0.01], 'powers_mw': [600.0, 800.0, 600.0, 400.0, 400.0], 'interpolation': <RfInterpolationType.LINEAR: 'LINEAR'>}, 'optical_barriers': [{'times_ms': [3.0, 10.0, 13.0], 'positions_um': [30.0, 30.0, -30.0], 'heights_khz': [30.0, 30.0, 15.0], 'widths_um': [3.0, 3.0, 3.0], 'interpolation': <InterpolationType.LINEAR: 'LINEAR'>, 'shape': <ShapeType.GAUSSIAN: 'GAUSSIAN'>}], 'optical_landscape': {'interpolation': <InterpolationType.LINEAR: 'LINEAR'>, 'landscapes': [{'time_ms': 0.0, 'potentials_khz': [0.0, 10.0, 20.0, 15.0, 0.0], 'positions_um': [-10.0, -5.0, 0.0, 5.0, 10.0], 'spatial_interpolation': <InterpolationType.LINEAR: 'LINEAR'>}, {'time_ms': 15.0, 'potentials_khz': [0.0, 15.0, 40.0, 10.0, 0.0], 'positions_um': [-20.0, -10.0, 0.0, 10.0, 20.0], 'spatial_interpolation': <InterpolationType.LINEAR: 'LINEAR'>}]}, 'lasers': None}, 'output': None, 'notes': None}], 'active_run': 1, 'external_id': None, 'time_submit': None, 'pix_cal': 8.71, 'job_type': <JobType.PAINT_1D: 'PAINT_1D'>, 'input_count': 1}
print(my_job.job_type)
PAINT_1D
my_job_id = client.submit(matter, track=True)
Submitting 1 job(s):
--------------------------------------------------------------------------- HTTPError Traceback (most recent call last) File ~/Documents/dev/pybert/oqtant/oqtant_client.py:374, in OqtantClient.submit_job(self, job, write) 373 try: --> 374 response.raise_for_status() 375 except RequestException as err: File ~/Documents/dev/pybert/.venv/lib/python3.10/site-packages/requests/models.py:1021, in Response.raise_for_status(self) 1020 if http_error_msg: -> 1021 raise HTTPError(http_error_msg, response=self) HTTPError: 500 Server Error: Internal Server Error for url: https://oqtant.infleqtion.com/api/jobs The above exception was the direct cause of the following exception: OqtantRequestError Traceback (most recent call last) Cell In[12], line 1 ----> 1 my_job_id = client.submit(matter, track=True) File ~/Documents/dev/pybert/oqtant/oqtant_client.py:130, in OqtantClient.submit(self, matter, track, write, filename, target) 116 """ 117 Submits a QuantumMatter object for execution, returns the resulting job id. 118 (...) 127 str: The resulting Job ID of the resulting job. 128 """ 129 job = self.convert_matter_to_job(matter) --> 130 return self.run_jobs(job_list=[job], track_status=track)[0] File ~/Documents/dev/pybert/oqtant/oqtant_client.py:411, in OqtantClient.run_jobs(self, job_list, track_status, write) 409 self.__print(f"Submitting {len(job_list)} job(s):") 410 for job in job_list: --> 411 response = self.submit_job(job=job, write=write) 412 external_id = response["job_id"] 413 queue_position = response["queue_position"] File ~/Documents/dev/pybert/oqtant/oqtant_client.py:376, in OqtantClient.submit_job(self, job, write) 374 response.raise_for_status() 375 except RequestException as err: --> 376 raise api_exceptions.OqtantRequestError( 377 "Failed to submit job to Oqtant" 378 ) from err 379 response_data = response.json() 380 if write: OqtantRequestError: Failed to submit job to Oqtant
Retrieving results¶
We retrieve the results of our job in the normal way:
my_job = client.get_job(my_job_id)
Advanced options and discussion¶
Mapped job type¶
Our QuantumMatter object that includes a non-null input for the
landscape exceeds the capabilities of an Oqtant
BARRIER job. Instead, it gets mapped to a
PAINT_1D job type. We can double check this by
evaluating the following:
print(my_job.job_type)