walkthrough_1_introduction

# Walkthrough 1: Introduction to Oqtant and Oqtant Quantum Matter Services (QMS)¶

## Introduction¶

Welcome to `Oqtant`, a python-based object-oriented interface for creating, submitting, and retrieving results from experiments with ultracold quantum matter carried out on Infleqtion's Oqtant Quantum Matter Service (QMS) platform. In this introductory walkthrough, we will explore the basics of how you, the user, interact with Oqtant QMS using Oqtant. The general workflow is as follows:

1. Create an Oqtant account: https://oqtant.infleqtion.com
2. Download and install Oqtant (this!): https://pypi.org/project/oqtant/
3. Instantiate an instance of the `OqtantClient`, aka the client, which facilitates communication with QMS (requires authenticating your account).
4. Create a user-defined `QuantumMatter`, aka matter, object for making and/or manipulating quantum matter. This is accomplished with the help of a `QuantumMatterFactory`.
5. Submit your matter object to the client. This creates a `OqtantJob`, aka a job, that runs on Oqtant's hardware platform(s) and returns a `job id` (UUID or id) that can be used for checking job status and retrieving results.
6. When your job is complete, retrieve and analyze results.

For more information, please refer to our documentation: https://gitlab.com/infleqtion/albert/oqtant/-/blob/main/README.md
Also see our companion web application https://oqtant.infleqtion.com/ for quick access to job creation, results, and account management.
Support, feature requests, and bug reports can be submitted here: https://oqtant.infleqtion.com/support

This walkthrough focuses on how to use Oqtant in conjunction with Oqtant QMS. Follow-on walkthroughs will explore user options and Oqtant data structures in significantly more detail.

### Imports and instantiation of the OqtantClient (requires account authentication)¶

The following block of code will import the needed modules and instantiate a client object that communicates with Oqtant's server(s). Once authenticated, you can safely close that tab and return to this notebook.

NOTE: instantiating the client creates a temporary web server on your machine to handle authenticating with Oqtant QMS. By default, this server will run on port 8080. If that port is unavailable, you can edit default port by passing an port parameter with any value in the range of 8080--8085.

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)

## Your first QuantumMatter object and OqtantJob¶

Basic usage of Oqtant involves the creation of a QuantumMatter object, which captures the user-defined inputs that control the behavior of Oqtant hardware. We will explore the available options and underlying data structures in detail in following walkthroughs.

### Instantiate a QuantumMatter object¶

For now, we will create a simple QuantumMatter object that will generate quantum matter at a target temperature (in nK). We will also give our object a (hopefully meaningful, but optional) name. Creating the object is accomplished using the imported `QuantumMatterFactory.create_quantum_matter()` method:

In [ ]:

    matter = qmf.create_quantum_matter(temperature=100, name="my first quantum matter")

### Submit your QuantumMatter object to the OqtantClient to generate an OqtantJob¶

The next step is to submit our QuantumMatter object to the client using the `OqtantClient.submit()` method. This will create an `OqtantJob` that will enter the QMS job queue and run on Oqtant hardware when it becomes available. The method itself returns a unique job id (32 character UUID string). Further managment of the job, including retrieving results, will require this id. Here, we will explicitly track the status of our job in the queue by passing the track=True option. The default value is False.

In [ ]:

    # submission returns the job uuid, e.g. "b26095ac-58bc-4308-a096-13a2544be722"
    my_job_id = client.submit(matter, track=True)

Typically, we can see the job progress through being submitted to the queue and receiving an id, being executed on the hardware (RUNNING) and finishing (COMPLETE). The amount of time this sequence takes will depend on the current load on QMS and whether your account is deducting from priority (paid) or free quotas.

### Retrieve/fetch the COMPLETE job¶

Once a job is complete, we can proceed to fetching the results. This is accomplished by using the client to get the job (including the results) from the Oqtant server by passing the job id as a parameter to the `OqtantClient.get_job()` method. If your job is not yet complete, you can still fetch the job from the server but the output fields will not be populated.

In [ ]:

    # get job from server, including results if available
    my_job = client.get_job(my_job_id)

### Extracting, visualizing, and analyzing job results¶

#### Output data fields¶

Once we have a complete job object, we can then extract the result(s) using the `OqtantJob.output` field, or list the available output contents using `OqtantJob.output_fields`. The contents of the output data depends on the initially constructed QuantumMatter object. In this example, which uses the default time-of-flight imaging option, the output data contains the following items:

In [ ]:

    print(my_job.output_fields)

In this case, we can see results from a fluorescence image of the magneto-optical trap (mot), a cooling stage early in the sequence that produces the quantum matter that is useful for diagnosing number drifts etc., a time-of-flight (tof) absorption image with pixel data, row/column counts, and a pixel calibration (pixcal, in microns/pixel), a fit version of the tof image based on the calculated thermal/condensed atom populations and the associated calculated temperature. These latter results are derived from fitting a "bimodal" distribution, consisting of a sum of Gaussian (thermal/classical phase) and Thomas-Fermi (condensed phase) distributions, to the resulting (time of flight) image of the atoms. Data corresponding to the bimodal fit is included in the tof_fit field. All tof image results are given in terms of the optical depth (OD). Advanced users may wish to implement their own temperature and atom population calculations using the included raw image data.

Any of the output data contents can be accessed programmatically, e.g.

In [ ]:

    print("temperature (nK):", my_job.output.temperature_nk)
    print("thermal atom population:", my_job.output.thermal_atom_number)
    print("condensed atom population:", my_job.output.condensed_atom_number)

#### Extracting output image data¶

Of course, you may want to calculate the quantities above, or any number of others, using the data from the tof (absorption) image of the atoms directly. Raw image data can be accessed programatically, as above, or using the `OqtantJob.get_image_data()` helper method. The type of image data to be fetched is passed as an input parameter. For our simple example, the image options are "MOT", "TIME_OF_FLIGHT", and "TIME_OF_FLIGHT_FIT".

In [ ]:

    tof_image_data = my_job.get_image_data(image="TIME_OF_FLIGHT")
    tof_fit_image_data = my_job.get_image_data(image="TIME_OF_FLIGHT_FIT")

In either approach, image data is returned as a 2D numpy array of values for integer-valued pixel positions. These pixel positions can be converted to position space by using their pixel calibration value via the `OqtantJob.get_image_pixcal` helper function, which returns the units of microns/pixel for each image type.

In [ ]:

    print(my_job.get_image_pixcal("TIME_OF_FLIGHT"))

We also have access to pre-calculated cuts of the time-of-flight image along the detected center/peak of the atom ensemble:

In [ ]:

    x_slice = my_job.get_slice(axis="x")
    print(x_slice[0:5])
    y_slice = my_job.get_slice(axis="y")
    print(y_slice[0:5])

#### Visualizing results¶

There are a number of tools/methods built into Oqtant for visualizing job outputs.

In [ ]:

    my_job.plot_tof(figsize=(6, 6))
    my_job.plot_slice(axis="x")

These in-built plots show the image of the atoms taken at the end of the experiment, in units of optical depth, as well as cuts along the x and y axes. These latter two plots include solid curves corresponding to the bimodal fit that includes the contributions from the quantum matter's thermal fraction (red curve) and thermal + condensed combination (blue curve).

Of course, the raw data can be used directly for further customization and analysis:

In [ ]:

    import matplotlib.pyplot as plt

    TOF_data = my_job.get_image_data("TIME_OF_FLIGHT")
    plt.figure(figsize=(6, 6))
    plt.title("Customized plot of the same OD results")
    TOF_plot = plt.imshow(TOF_data, origin="upper", cmap="nipy_spectral")
    plt.grid(visible=True)
    plt.xlabel("x position (pixels)")
    plt.ylabel("y position (pixels)")
    plt.colorbar(TOF_plot, shrink=0.8)
    plt.show()

## Job management using the local filesystem¶

While it is completely possible to always retrieve jobs from the Oqtant server, provided they have not been deleted or removed by the user, when dealing with large sets of jobs, or without network connectivity, it is often more convenient and efficient to work with the job data on your local machine.

### Saving a job to file¶

To easily reference OqtantJobs from the current or previous sessions it is useful to write their information to a local file. For this, you can use the `client.write_job_to_file()` method:

In [ ]:

    client.write_job_to_file(my_job)

By default, this method will save the job data in a newly created local file with the name `*job id*.txt` in the same directory as the current walkthrough / jupyter notebook. Alternatively, you can customize the resulting filename as follows:

In [ ]:

    client.write_job_to_file(
        my_job, file_name=my_job.name  # use job name as filename instead
    )

Full control of data saving is made available by providing the full path of the desired output file:

In [ ]:

    import os

    home = os.path.expanduser("~")

    client.write_job_to_file(
        my_job, file_path=home + "/Documents/" + str(my_job.id) + ".txt"
    )

Take care not to accidentally write over existing local job data files, especially when specifying custom filenames. Using the default of the job id should help reduce this possibility since that field is unique for each job.

### Loading a job from file¶

Retrieving a job from a local file is accomplished by specifying the (local directory) filename or the full file path:

In [ ]:

    job_from_file = client.load_job_from_file(str(my_job.id) + ".txt")

### Automatically saving a job to file¶

## Additional details, features, and discussion¶

### Including user notes¶

Every QuantumMatter object, and hence also every associated OqtantJob, can hold a note up to 500 characters long. This can be used to add context and additional information. A note remains tied to the job and can be referenced later. The note can be added at the point of creating the QuantumMatter object:

In [ ]:

    matter_with_note = qmf.create_quantum_matter(
        temperature=100,
        name="matter with a note",
        note="This is something special that I would like to remember.",
    )

### Mapping of the QuantumMatter object to a `JobType`¶

Each QuantumMatter object submitted to the client is mapped to a `JobType` that depends on what features and options are specified. This allows Oqtant to enable or disable certain hardware features depending on what the user requires for that particular run (a run corresponds to a single execution of the sequence that executes user-defined inputs on the hardware and captures the resulting outputs). We will explore the different job types in future walkthroughs. If you are using the web app, jobs will be identified by the associated job type. For our example shown here, our simple matter object maps to a so-called BEC job:

In [ ]:

    print(my_job.job_type)

### Searching for previous jobs¶

If you need to go back in time to see a history of jobs submitted in previous sessions you can use the `OqtantClient.search_jobs()` method. This method provides a way to ask Oqtant for jobs associated to your account that match certain search criteria.

The supported search values are:

- job_type
- name
- submit_start
- submit_end
- notes

Note: submit_start and submit_end must be in ISO format to properly work (YYYY-MM-DD or YYYY-MM-DDThh:mm:ss)

Using these search values Oqtant will return to you any jobs that are associated to your account and match the provided criteria.

In [ ]:

    found_jobs = client.search_jobs(name="my first", limit=3, show_results=True)

### Showing the current status of your jobs in the queue¶

When executing many jobs on Oqtant QMS, it is useful to be able to check the status of your submitted jobs without fetching each job individually and checking the status field. This can be accomplished using the OqtantClient.show_queue_status() method, which accepts the same filters as the OqtantClient.search_jobs() method explored above.

In [ ]:

    from datetime import datetime, timedelta

    # set a date range to find jobs submitted within, in this case jobs submitted today
    today = datetime.today()
    submit_start = today.strftime("%Y%m%d")
    submit_end = (today + timedelta(days=1)).strftime("%Y%m%d")

    # search for jobs that have yet to complete
    client.show_queue_status(submit_start=submit_start, submit_end=submit_end)

If you would like to also see completed jobs, you can pass the include_complete=True option:

In [ ]:

    # search for jobs submitted today, including those that have already completed
    client.show_queue_status(
        submit_start=submit_start,
        submit_end=submit_end,
        include_complete=True,
        limit=20,  # limit the number of results shown (100 max)
    )

### Querying your current job quota¶

Currently, each user of Oqtant QMS has limits on the number of jobs they can submit per day, etc. Detailed management of your allocated job quota can be accomplished using the web app. From Oqtant, you can show your current limits as follows:

In [ ]:

    client.show_job_limits()

### Units¶

Oqtant uses units that are natural choices for dealing with the quantum matter produced and manipulated by the Oqtant platform. The table below is a summary of relevant units used throughout this and following walkthroughs. Some of these objects/quantities have not yet been encountered, but will be shortly.

Quantity | Units | Notes
--- | --- | ---
time | milliseconds (ms, $10^{-3}$ m) |
frequency | megahertz (MHz, $10^6$ Hz) | Relative to the energetic trap bottom, $\geq 0$
powers | milliwatts (mW, $10^{-3}$ W) | Power radiated by an RF loop antenna adjacent to the cold atom sample, $\geq 0$
temperature | nanokelvin (nK, $10^{-9}$ K) |
barrier height | kilohertz (kHz, $10^{3}$ Hz) | Energetic height / Planck's constant
barrier position | microns ($\mu m$, $10^{-6}$ m) | Center of barrier
barrier width | microns ($\mu m$, $10^{-6}$ m) | Shape-dependent width parameter
landscape potential | kilohertz (kHz, $10^{3}$ Hz) | Energetic height / Planck's constant