walkthrough_2_making_quantum_matter

# Walkthrough 2: Making quantum matter¶

## Introduction¶

The simplest operation that can be accomplished using Oqtant and Oqtant QMS is the creation of quantum matter in the form of a Bose-Einstein Condensate (BEC). Using Oqtant, users have control of the final cooling step to quantum degeneracy -- the forced radiofrequency evaporation that cools atoms to the ultracold quantum regime. We saw the most basic form of this control in the previous walkthrough, where we controlled the final state by specifying a target atomic ensemble temperature.

Each run of the Oqtant hardware, as defined by the executed OqtantJob created by the user, is separated into two distinct phases -- an evaporation phase where forced radiofrequency evaporation takes place that cools the atoms to quantum degeneracy, and a manipulation phase where that matter can be held and/or manipulated. In this walthrough, we will focus mostly on the evaporation phase, leaving exploration of manipulation of the quantum matter for later walkthroughs.

### 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
    from oqtant.schemas.rf import RfEvap, RfShield

    oqtant_account = notebook_login()
    oqtant_account

In [ ]:

    client = get_oqtant_client(oqtant_account.access_token)

## Creating a QuantumMatter object¶

### Controlling QuantumMatter lifetime and imaging time of flight¶

As seen in walkthrough 1, the simplest way to create a QuantumMatter object is to provide the target temperature as an input parameter, along with an (optional) name. Additionally, we can control how long the atom ensemble is held after evaporation is complete by passing a lifetime parameter, with the units of milliseconds. Note that this usage of the term lifetime is not to be confused with the common usage in physics of an exponentially decaying quantity's $1/e$ time constant, which is also commonly called the lifetime. In this context, after the hold time (lifetime) is complete, a destructive image will be taken of the ensemble. In this example, as was the case in walkthrough 1, we will take a "time of flight" image (the default) where the ensemble is released from its trap and allowed to freely fall under gravity, expanding according to its effective trap temperature, before the image is taken. How long the cloud falls before the image is captured is controlled by the time_of_flight input parameter, also with the units of milliseconds.

Let us begin by constructing a QuantumMatter object that produces a resulting ensemble with a (target) temperature of 100 nK, a lifetime of 10 ms, and a time of flight of 8 ms. Again, we will make use of our import QuantumMatterFactory:

In [ ]:

    matter = qmf.create_quantum_matter(
        temperature=100,
        lifetime=10,
        time_of_flight=8,
        name="QM w/ lifetime + tof control",
    )

Control of the time of flight (TOF) is useful for revealing some of the unique features of a BEC. Specifically, the quantum properties of the atoms in the condensate lead to a distinct expansion as a large, slowly expanding peak that suddenly forms below the transition temperature. As this peak expands during TOF, features of quantum matter such as shape inversion can be observed. Additionally, the (classical) behavior of the thermal fraction, those atoms that are not condensed, can be studied. Hotter thermal clouds expand faster and become more diffuse more quickly, while colder clouds expand more slowly. The details of the cloud shape during TOF depend on the initial sample temperature, density, population, and condensed fraction, all of which can be controlled by the user using Oqtant and Oqtant.

### Visualizing forced radiofrequency evaporation dynamic behavior¶

After creating your QuantumMatter object, you can use some helper methods to aid in visualizing what behavior will be carried out on Oqtant's hardware. In our simple example, the only dynamic quantities are the radiofrequency power and frequency (detuning, relative to the energetic bottom of the trap that holds the atom ensemble) applied to the atoms during evaporation. When specifying a target temperature, as in our current example, these dynamics are automatically calculated in the backend. We will see below how the evaporation sequence can be customized further and how radiofrequency radiation can also be applied during the manipulation phase.

In [ ]:

    matter.show_rf_dynamics()

Looking closely at the plot above, we notice that all the times associated with the forced evaporation sequence have been offset to be negative. This is a quirk of Oqtant hardware platforms, where negative times correspond to the evaporation phase and positive times to the manipulation phase. This offset is taken care of in the backend and will still occur even with fully user-defined evaporation curves (see below). In the plot above, the vertical dashed line indicates the separation of the overall program into the evaporation (t $\leq$ 0) and manipulation (t $>$ 0) phases.

It is important to note that the radiofrequency evaporation frequency is referenced to the energetic trap bottom. Therefore, the frequency can be interpreted as a detuning, where a detuning of zero means we will remove all atoms from the trap. The closer we get to zero detuning during evaporation, the colder our resulting ensemble will be. This, of course, comes at the expense of atom population as more atoms need to be removed via evaporation in order to achieve a colder final temperature. These tradeoffs, and the interplay between evaporation dynamic RF power and frequency and the produced ensemble characteristics, are easily explored using Oqtant.

### Full control of the forced evaporation sequence¶

Further control and exploration of the final temperature, population, and condensed/thermal fractions of the produced atom ensemble is made possible by passing a custom `RfEvap` object as an optional `rf_evap` parameter to the QuantumMatter object being instantiated.

#### Specifying custom RF evaporation using the RfEvap object¶

Custom forced evaporation sequences can be created by first creating an RfEvap object, which takes equal length lists of times, powers, and frequencies, along with an interpolation option, discussed below. This data represents ordered pairs of (times, frequencies) and (times, powers) that are evaluated, according to the interpolation choice, over the time of the evaporation sequence.

In [ ]:

    evap = qmf.create_rf_evap(
        times=[0, 400, 800, 1200, 1600],
        powers=[600, 800, 600, 400, 200],
        frequencies=[20, 10, 5, 2.5, 1.25],
        interpolation="LINEAR",
    )

We can use our explicitly composed RfEvap object to compose a QuantumMatter object:

In [ ]:

    matter = qmf.create_quantum_matter(
        rf_evap=evap, name="making quantum matter with direct rf control"
    )

We can now see the customized evaporation curves:

In [ ]:

    matter.show_rf_dynamics()

#### RfEvap interpolation options¶

Aside from the "LINEAR" interpolation option specified above for our RfEvap object, if desired custom rf evaporation construction can also use the "OFF" option to give stepwise behavior of the evaporation frequency and power in time. This causes the frequency or power value to be held constant at the value of the previous datapoint. Both frequency and power share the same interpolation behavior. Let us see what this other interpolation option gives for the evaporation sequence:

In [ ]:

    # try out a different (temporal) interpolation option
    evaporation = qmf.create_rf_evap(
        times=[0, 400, 800, 1200, 1600],
        powers=[600, 750, 500, 400, 200],
        frequencies=[20, 10, 5, 2.5, 1.25],
        interpolation="OFF",
    )

    matter = qmf.create_quantum_matter(
        rf_evap=evaporation, name="direct rf control without interpolation"
    )

    matter.show_rf_dynamics()

With interpolation turned off, the last point of frequency and power is effectively not used as the instantaneous frequency and power are determined by the previous datapoint.

#### Specifying a custom RfEvap object and target temperature simulaneously¶

If a target temperature is specified at the same time as a customized `RfEvap` object, the final frequency of radiation applied to the atoms will be tweaked to attempt to achieve the desired temperature. If this behavior is undesireable, avoid passing both parameters together.

In [ ]:

    evaporation = qmf.create_rf_evap(
        times=[0, 400, 800, 1200, 1600],
        powers=[600, 750, 500, 400, 200],
        frequencies=[20, 10, 5, 2.5, 0.1],
        interpolation="LINEAR",
    )

    # specify only the custom rf_evap input parameter
    matter = qmf.create_quantum_matter(rf_evap=evaporation)
    print("the final frequency is", matter.rf_evaporation.frequencies_mhz[-1], "mHz")

    # specify both the custom rf_evap input parameter as well as a target temperature
    matter = qmf.create_quantum_matter(temperature=1000, rf_evap=evaporation)
    print("the final frequency is", matter.rf_evaporation.frequencies_mhz[-1], "mHz")

### Applying a radio frequency "shield" during the manipulation phase¶

Much like we were able to create a quantum matter program above by passing a RfEvap object as the `rf_evap` parameter, we can also apply forced radiofrequency evaporation (at fixed power and frequency/detuning) during the manipulation phase. To accomplish this, we pass an additional `rf_shield` parameter, consisting of an RfShield object. Our new object is so-called because it can act to throw away any atoms that are heated above a certain temperature, shielding the rest of the ensemble from destructive collisions with hot atoms. This feature will be most useful when we start manipulating the quantum matter sample with, e.g., externally applied optical fields. We will encounter this more advanced level of manipulation in the following walkthrough.

We can create a `RfShield` object, and pass it to our QuantumMatter object, as follows:

NOTE: to apply a shield, you must specify a finite (>0) lifetime such that the manipulation phase has a finite time over which the shield can be applied. The default lifetime value, which will be used if not explicit lifetime parameter is provided, is 10 ms.

In [ ]:

    evaporation = qmf.create_rf_evap(
        times=[0, 400, 800, 1200, 1600],
        powers=[600, 750, 500, 400, 200],
        frequencies=[20, 10, 5, 2.5, 0.1],
        interpolation="LINEAR",
    )

    shield = qmf.create_rf_shield(power=100, frequency=10)

    matter = qmf.create_quantum_matter(
        rf_evap=evaporation,
        rf_shield=shield,
        lifetime=80,
        name="matter with shield during manipulation phase",
    )

And we can see the application of the rf shield being included in the resulting rf dynamics:

In [ ]:

    matter.show_rf_dynamics()

### In-trap imaging¶

In addition to the time-of-flight imaging option discussed above, where the atom ensemble is released from the trapping potential some time before the image of the atoms is taken, Oqtant also supports the ability to take the image of the atoms in-trap. Such an image will inherently show the shape of the atom ensemble in the trap, which will be particularly useful once additional manipulation is applied to the atoms (see subsequent walkthroughs, which explore using light to manipulate the atom ensemble).

NOTE: While in-trap imaging is useful for revealing the spatial distribution of atoms within the ensemble at the time they are imaged, it is natively less useful for extracting quantitative information such as temperature, condensed fraction, etc. Hence, these calculations are not done when this imaging option is specified.

In [ ]:

    matter = qmf.create_quantum_matter(
        temperature=200,
        name="now with in-trap imaging!",
        image="IN_TRAP",  # NEW!, the default used above was image="TIME_OF_FLIGHT"
        lifetime=20,  # in-trap image will be taken 20 ms into manipulation phase
    )

    my_in_trap_job_id = client.submit(matter, track=True)

The output data for jobs that use the in-trap imaging option is somewhat different than those that use the default time-of-flight imaging option:

In [ ]:

    # fetch job results
    my_in_trap_job = client.get_job(my_in_trap_job_id)

    # show the output data fields
    print(my_in_trap_job.output_fields)

In [ ]:

    my_in_trap_job.plot_it()

### Visualizing the (magnetic) trapping potential¶

During the forced RF evaporation sequence, and subsequently during the manipulation stage, the ultracold quantum matter is trapped in three dimensions by magnetic fields produced by an atom chip that forms part of the ultra-high vacuum cell walls. This trapping potential is asymmetric, having cylindrical symmetry with two equal high-frequency (radial) trap axes and one relatively weak axial trap frequency. The trapping frequencies of the strong- and weak-trapping axis are around 400 and 50 Hz, respectively. This arrangement gives rise to "sausage"- or "cigar"-shaped condensates. For "1D" style programs, which are the focus of this and the next few walkthroughs, we will be concerned only with the long-axis of the cloud as a controllable position/parameter for painted optical potentials etc. Thus, all positions will correspond to this weak-trap axis until we explicitly lift this restriction. Here, we can view the trapping potential energy as a function of this position:

In [ ]:

    matter.show_potential()

The potential energy as a function of position will be of more interest later, particularly once we demonstrate so-called "painted potential" capabilities, where the trapping potential can be dynamically altered by the user using far-detuned light projected onto the quantum matter sample.

## Advanced topics and discussion¶

### Understanding actual delivered RF power¶

The figures above showing the user-provided RF evaporation and shield dynamics are in some ways misleading. In the hardware implementation, the radiofrequency radiation applied to the ultracold atoms is transmitted through an RF loop optimized for a frequency of approximately 40 MHz. This allows for maximum RF power to be delivered at relative high frequencies (detunings) where the evaporation process is more power hungry, but it also means that the delivered RF power decays as the frequency is lowered towards the energetic trap bottom (0 frequency, in the data structures explored here). This means that the actual delivered RF power is not, in reality, what is provided in the user-defined RfEvap object passed to the program constructor. To view the actual behavior of the delivered RF power, within multiplication by an arbitrary constant, you can pass the `corrected = True` option to the `show_rf_dynamics()` method:

In [ ]:

    matter.show_rf_dynamics(corrected=True)

### Peeking under the hood¶

In the background of Oqtant, each program, and in fact each primitive constituent object, e.g. RfShield and RfEvap objects, gets converted to a data structure that can be consumed by Oqtant QMS. This structure can be revealed using the inbuilt .dict() method for each Oqtant object.

In [ ]:

    matter = qmf.create_quantum_matter(temperature=100, name="implementation details!")
    print(matter.model_dump())

For advanced creation and manipulation, the data fields of a program can be accessed and/or edited programmatically using this structure, e.g.

In [ ]:

    matter.name = "new name!"
    matter.temperature = 250
    matter.input.time_of_flight_ms = 20
    print(matter.model_dump())

### Data limits and validation¶

All Oqtant objects are validated to ensure that they represent and contain valid parameters before being submitted to Oqtant hardware. Thus, it should be expected to sometimes encounter error messages when trying to construct objects that violate the underlying rules. For example, the manipulation phase lifetime is limited, target temperatures are required to be non-negative, etc. If these rules are violated, we encounter a validation error:

In [ ]:

    matter = qmf.create_quantum_matter(
        temperature=100,
        lifetime=1000,  # too long!
        time_of_flight=8,
        name="lifetime is too long!",
    )