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 Oraqle hardware.

Imports and user authentication¶

In [ ]:
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.

In [ ]:
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:

In [ ]:
snapshot.show_potential()
No description has been provided for this image

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.

In [ ]:
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()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

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.

In [ ]:
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:

In [ ]:
landscape.show_potential(times=[2, 3, 4, 5])
No description has been provided for this image

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:

In [ ]:
# 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:

In [ ]:
matter.show_potential(times=[2, 4, 8, 16])
No description has been provided for this image

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¶

In [ ]:
my_job = client.convert_matter_to_job(matter=matter)
In [ ]:
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}
In [ ]:
print(my_job.job_type)
PAINT_1D
In [ ]:
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://oraqle-dev.infleqtion.com/api/jobs

The above exception was the direct cause of the following exception:

OraqleRequestError                        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.OraqleRequestError(
    377         "Failed to submit job to Oraqle"
    378     ) from err
    379 response_data = response.json()
    380 if write:

OraqleRequestError: Failed to submit job to Oraqle

Retrieving results¶

We retrieve the results of our job in the normal way:

In [ ]:
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 Oraqle BARRIER job. Instead, it gets mapped to a PAINT_1D job type. We can double check this by evaluating the following:

In [ ]:
print(my_job.job_type)