Experiment Module
The experiment module provides a hierarchy of classes for building, validating, and
executing NMR pulse sequences on the OPX-1000. The base class
Experiment holds the command queue,
configuration, and shared infrastructure. The concrete subclasses
Experiment1D,
Experiment2D, and
Experiment3D
implement the loop structure and data-processing logic appropriate to each
experiment dimensionality.
Designing Experiments
The three concrete subclasses differ only in how many swept parameters they support:
Class |
Swept parameters |
Typical use cases |
|---|---|---|
|
0 |
FID, single-pulse readout |
|
1 |
T1, T2, pulse calibration, Floquet correlation measurements |
|
2 |
Joint relaxation / amplitude scans, Multiple Quantum Coherence (MQC) experiments |
Adding commands
Pulse sequences are built by appending commands before execution. All commands accept either scalar or iterable (NumPy array) arguments for the swept dimension.
expt.add_pulse(name, element, phase=0.0, amplitude=1.0, length=None)
expt.add_delay(duration)
expt.add_align(elements=None)
expt.add_floquet_sequence(phases, delays, repetitions)
expt.add_z_rotation(angle, element) # virtual-Z gate (frame rotation)
expt.add_frame_change(angle, element) # per-pulse frame correction
Scalar arguments produce a fixed value in the compiled QUA program.
Passing a NumPy array registers a loop variable; the array becomes
the sweep vector iterated over in the QUA for_ loop.
Loop layers and QUA variables
Each swept dimension corresponds to one loop layer. The
loop_layer keyword selects which QUA loop variable a command’s
array is bound to. The indexing is 1-based:
|
Internal slot |
QUA variable name |
Sweep speed |
|---|---|---|---|
|
|
|
Outermost swept loop — slowest changing |
|
|
|
Inner swept loop — fastest changing |
The default value loop_layer=-1 instructs the class to automatically
assign the variable to the first unfilled slot. Explicit values are
strongly preferred in 3D experiments to avoid accidental axis transposition.
Designing a 2D experiment
Assign a NumPy array to any add_* argument. All arrays across all
commands in the experiment must be identical or a constant scalar multiple
of each other — they all map to the same QUA variable.
import numpy as np, qeg_nmr_qua as qnmr
from qualang_tools.units import unit
u = unit(coerce_to_integer=True)
amp_list = np.arange(0.93, 1.05, 0.01)
expt = qnmr.Experiment2D(settings=settings, config=cfg)
expt.add_pulse(
name=settings.pi_half_key,
element=settings.res_key,
amplitude=amp_list, # marks amp_list as layer-1 sweep
)
# A second pulse whose amplitude is 2× amp_list is still consistent:
expt.add_pulse(
name=settings.pi_half_key,
element=settings.res_key,
amplitude=2 * amp_list, # scalar-multiple of existing vector — OK
)
expt.update_sweep_axis(amp_list * settings.pulse_amplitude)
expt.update_sweep_label("Pulse Amplitude (Vpp)")
expt.execute_experiment()
Designing a 3D experiment
Two independent arrays must be provided, one per loop layer.
The data shape returned after execute_experiment is
``(n_outer, n_inner, n_time)`` — axes ordered from slowest to fastest varying,
followed by the readout time axis.
Rule summary
loop_layer=1→ stored invar_vec_lst[0]→ compiled into the outer (slow) QUA loop asvar_outer.loop_layer=2→ stored invar_vec_lst[1]→ compiled into the inner (fast) QUA loop asvar_inner.Multiple commands may share the same
loop_layerprovided their arrays are constant multiples of each other.Mixing integer and fixed-point types within the same layer raises a
ValueErrorat build time.
import numpy as np, qeg_nmr_qua as qnmr
from qualang_tools.units import unit
u = unit(coerce_to_integer=True)
# Define the two sweep axes explicitly:
delay_list = np.array([1, 2, 4, 8, 16, 32]) * u.us # outer (slow)
amp_list = np.arange(0.95, 1.06, 0.01) # inner (fast)
expt = qnmr.Experiment3D(settings=settings, config=cfg)
# layer=1 → outer loop (slow sweep, var_outer in QUA)
expt.add_delay(delay_list, loop_layer=1)
# layer=2 → inner loop (fast sweep, var_inner in QUA)
expt.add_pulse(
name=settings.pi_half_key,
element=settings.res_key,
amplitude=amp_list,
loop_layer=2,
)
expt.update_sweep_axis_outer(delay_list / u.us)
expt.update_sweep_label_outer("Delay (µs)")
expt.update_sweep_axis_inner(amp_list * settings.pulse_amplitude)
expt.update_sweep_label_inner("Pulse Amplitude (Vpp)")
expt.execute_experiment()
# I_data shape: (6, 11, n_time)
I_data = expt.save_data_dict["I_data"]
Warning
Incorrect ``loop_layer`` is a silent data-ordering bug. The program
will compile and run without error, but the outer and inner axes of the
data array will be swapped. To verify loop assignments before running
on hardware, use compile_to_qua()
with offline=True and inspect the generated for_ nesting in the
output file.
Variable types: integer vs. fixed-point
QUA distinguishes between integer (int) and fixed-point (fixed) variables.
The type is inferred from the swept parameter:
Amplitudes and phases →
fixed(fractional values).Durations (delays, pulse lengths) →
int(clock cycles, always integer).
The class records the inferred type in use_fixed_lst and declares the
corresponding QUA variable type when the program is compiled. You cannot mix
integer and fixed-point arrays on the same loop layer.
Inspecting the compiled program
compile_to_qua() generates
the full QUA script as a plain-text file without connecting to hardware:
expt.compile_to_qua(offline=True, save_path=Path("my_experiment.qua"))
This is the recommended first step for any new 3D experiment — confirm that
var_outer and var_inner appear in the expected for_ loop positions.
Subclassing the base class
Advanced users can subclass Experiment
directly to implement custom loop structures or non-standard readout schemes. Three
methods must be overridden:
validate_experiment()Called before the QUA program is compiled. Raise
ValueErrorfor any inconsistency that would cause a QUA runtime error (e.g. missing loop vectors, incompatible lengths). This catches user errors before they propagate into hard-to-read QUA tracebacks.create_experiment()Builds and returns the QUA
program()object. Calltranslate_command()for each item inself._commandsto emit the pulse/delay/align QUA instructions. All timings must be expressed in clock cycles (divide nanoseconds by 4). Data streams must be buffered consistently with the expected shape consumed indata_processing.data_processing()Called after the job is launched. Fetch data via
qualang_tools.results.fetching_tool, convert demodulated values to volts, and populateself.save_data_dict.
Safety macros
The macros module provides three macros that must be
called in the correct order to protect the hardware:
drive_mode(switch, amplifier) # open switch, unblank amplifier — before pulse sequence
safe_mode(switch, amplifier) # blank amplifier, open switch — after pulse sequence
readout_mode(switch, amplifier) # close switch, blank amplifier — during readout
All three insert align() calls and explicit wait times (RX_SWITCH_DELAY,
AMPLIFIER_BLANKING_TIME) to ensure hardware state transitions complete before
the next operation.
Base Experiment
- class qeg_nmr_qua.experiment.experiment.Experiment(settings, config=None, connect=True)[source]
Bases:
objectBase class for conducting NMR experiments on the OPX-1000.
This class manages quantum machine configurations, experiment commands (pulses, delays, alignments), and data collection. It provides a structured interface for building and executing pulse sequences with support for looped experiments and real-time data acquisition.
Subclasses should implement experiment-specific logic in:
create_experiment(): Build the QUA program from stored commandsvalidate_experiment(): Validate settings and command consistencydata_processing(): Handle real-time data during hardware execution
Typical workflow:
Create an Experiment subclass instance with relevant settings and config
Build sequence: add pulses, delays, and alignments using
add_*methodsTest: call
simulate_experiment()to verify timing and waveformsExecute: call
execute_experiment()to run on hardwareSave: call
save_data()to persist configuration, settings, and results
- settings
Experiment-specific parameters (frequencies, pulse lengths, etc.)
- config
OPX configuration object
- save_dir
Directory to save experiment data
- save_data_dict
Dictionary which contains data extracted during the experiment for saving
- qmm
QuantumMachinesManagerinstance for managing connection to the OPX-1000
- __init__(settings, config=None, connect=True)[source]
Initialize experiment with configuration and settings.
- Parameters:
settings (
ExperimentSettings) – Experiment-specific parameters (frequencies, pulse lengths, etc.).config (
OPXConfig) – OPX configuration. If None, automatically generated from settings.connect (bool) – Whether to establish a live connection to the OPX-1000 via
QuantumMachinesManager. Set toFalsefor offline compilation or unit testing. Defaults toTrue.
- Raises:
ValueError – If readout delay is too short to accommodate switching times.
- add_pulse(element, shape=None, phase=0.0, amplitude=1.0, length=None, loop_layer=-1)[source]
Adds a pulse command to the experiment. Stores the data to control the pulse in the experiment’s command list, and ensures the command is well defined. Timings are converted from ns and stored in clock cycles (4 ns).
- Parameters:
element (str) – Element to which the pulse is applied. Must be defined in the config.
shape (str) – Shape of the pulse operation, defaults to
settings.pulse_shapeif not provided.phase (float | Iterable) – Phase of the pulse in degrees. Stored as a fraction of 2π.
amplitude (float | Iterable) – Amplitude scale factor for the pulse waveform.
length (int | Iterable) – Length of the pulse in nanoseconds, overriding the waveform default.
loop_layer (int) – Loop layer (1-based) to associate with the swept parameter. Use
-1(default) to auto-assign to the first available layer.
- add_delay(duration, loop_layer=-1)[source]
Adds a delay command to the experiment. Stores the data to control the delay in the experiment’s command list.
- add_align(elements=None)[source]
Adds an align command to the experiment. Stores the data to control the alignment in the experiment’s command list.
- add_floquet_sequence(phases, delays, repetitions, loop_layer=-1, element=None, shape=None)[source]
Adds a Floquet sequence to the experiment. This is a repeating block of evenly-spaced, phase-cycled pulses of the form:
delay[0] — (R(phase[0]) — delay[1]) — (R(phase[1]) — delay[2]) — … repeated N times
The number of repetitions can be a fixed integer or a 1-D array, in which case it becomes the swept loop variable for the given
loop_layer.- Parameters:
phases (list[float]) – Phase of each pulse in the block, in degrees.
delays (list[int]) – Inter-pulse delays in nanoseconds. Must contain exactly
len(phases) + 1entries (one leading delay plus one after each pulse).repetitions (int | Iterable) – Number of times the pulse block is repeated. Pass an array to sweep over this parameter.
loop_layer (int) – Loop layer (1-based) to associate with a swept
repetitionsarray. Use-1(default) to auto-assign.element (str) – Element to apply the sequence to. Defaults to
settings.res_key(the probe channel) whenNone.shape (str) – Pulse shape key, defaults to
settings.pulse_shape.
- Raises:
ValueError – If
len(delays) != len(phases) + 1.ValueError – If
elementorshapeare not recognised in the config.
- add_z_rotation(angle, elements, loop_layer=-1)[source]
Adds a virtual Z rotation to the experiment. This is implemented as a frame rotation in QUA.
- Parameters:
- add_frame_change(angle, elements)[source]
Adds frame change compensation to the experiment, which is implemented as a frame rotation after each applied pi-half pulse. This feature is useful for correcting out-of-phase overrotation errors in pi-half only pulse sequences. In principle, any pulse sequence can be written with only pi-half pulses and z-rotations, allowing for frame change compensation to be applied in all cases.
- remove_initial_delay(remove=True)[source]
Removes the 5 T1 delay from the start of the sequence. Useful for testing with the simulator, but should be generally used to ensure proper thermalization between experiments. By default, the delay is included. The delay can be re-added by calling this method with remove set to False.
- Parameters:
remove (bool) – By default, is True, removes the initial delay. If False, the delay is re-added.
- translate_command(command, vars=None, loop_idx=None)[source]
Translates a command dictionary into QUA code.
- Parameters:
command (dict) – Command dictionary to translate.
vars (Any, optional) – QUA Variables to use for swept parameters.
loop_idx (Any, optional) – QUA Variable to use for loop index.
- Raises:
ValueError – If the command type is unknown.
- create_experiment()[source]
Creates the Quantum Machine program for the experiment, and returns the experiment object as a qua
program. This is used by theexecute_experiment()andsimulate_experiment()methods.- Returns:
The QUA program for the experiment defined by this class’s commands.
- Return type:
program
- validate_experiment()[source]
Function to be implemented by subclasses to validate that the commands and settings for the experiment are consistent and valid. Running this function should return helpful error messages if the experiment is not properly defined.
- compile_to_qua(offline=True, save_path=None)[source]
Compiles the experiment to a QUA program, running all python code in order to generate the final QUA program as a string, which is then saved to disk at the specified path. This is useful for inspecting the generated QUA program to verify that the commands are being translated as expected, and can be used for debugging command translation and config interpreation issues without needing to run the experiment on hardware or in the simulator.
- Parameters:
offline (bool) – Whether to compile offline, or to use the connected Quantum Machine. Defaults to True for offline compilation.
save_path (Path) – The path to save the compiled QUA program. If None, saves to the same directory as this file with the name “compiled_experiment.qua”.
- simulate_experiment(sim_length=40000)[source]
Simulates the experiment using the configured experiment defined by this class based on the current config defined by this instance’s
configattribute. The simulation returns the generated waveforms of the experiment up to the durationsim_lengthin ns. Useful for checking the timings before running on hardware.- Parameters:
sim_length (int, optional) – The duration of the simulation in ns. Defaults to
40_000.
- execute_experiment(live=True, wait_on_close=True, title_prefix='')[source]
Executes the experiment using the configured experiment defined by this class based on the current config defined by this instance’s config attribute. The method handles the execution on hardware, data fetching, and basic plotting of results.
- Parameters:
live (bool) – Passed into data_processing to determine whether to display data live during execution or only after completion, depending on the subclass implementation.
wait_on_close (bool) – Whether to wait for user to close plot window after experiment completes. Only relevant when live=True. Defaults to True.
title_prefix (str) – Prefix to add to plot title for identification. Defaults to empty string.
- Raises:
ValueError – Throws an error if insufficient details about the experiment are defined.
- data_processing(qm, job, live=True, wait_on_close=True, title_prefix='')[source]
Grabs the results of the experiment as it is being executed. This method must be implemented by subclasses to determine how to fetch and plot the data specific to the experiment. The specific handles for the quantum machine and the active job are provided for data fetching. Additionally, this method can (and should) update the save_data_dict attribute with relevant raw data for saving to disk after the experiment completes.
Subclass implementations should make use of the live argument to determine whether to plot data live during execution or only after the experiment completes. Autonomous experiments will typically want to set live to False to avoid blocking execution when waiting for user input to close plots.
- Parameters:
qm (QuantumMachine) – The quantum machine executing the experiment.
job (RunningQmJob) – The job running the experiment.
live (bool) – Whether to display data live during execution or only after completion.
wait_on_close (bool) – Whether to wait for user to close plot after completion. Only relevant when live=True.
title_prefix (str) – Prefix to add to plot titles for identification.
- save_data(experiment_prefix='experiment')[source]
Saves the experiment data to the specified directory using the DataSaver.
Creates a folder with an auto-incremented name based on
experiment_prefix(e.g.,prefix_0001,prefix_0002) containing: - config.json: OPX configuration - settings.json: Experiment settings - commands.json: List of commands executed - data.json: Experimental results and metadataEach experiment is saved in a newly created folder with a simple naming structure for easy loading elsewhere.
- Parameters:
experiment_prefix (str) – Prefix for the experiment folder naming. Defaults to “experiment”. The created folder name will be
<experiment_prefix>_NNNNwith a 4-digit counter.- Raises:
ValueError – If experiment_prefix contains path separators or is invalid.
1D Experiments
- class qeg_nmr_qua.experiment.experiment_1d.Experiment1D(settings, config=None, connect=True)[source]
Bases:
ExperimentClass to create and run 1D NMR experiments using the QUA programming language. Inherits from the base
Experimentclass and implements methods specific to 1D experiments, usually used for measuring free induction decay (FID) signals. The resulting signal is used to calibrate drifts in the nuclear spin frequency phase reference.In solid-state systems, the FID signal typically decays within 100-500 microseconds due to strong dipolar interactions between nuclear spins. Direct fitting of T2* from the FID is often unreliable because of this rapid decay.
- validate_experiment()[source]
Checks to make sure that the experiment contains no variable operations, since it is a 1D experiment. Variable operations require looping which is not supported in 1D experiments.
- Raises:
ValueError – A looping operation was found in the experiment commands.
- create_experiment()[source]
Creates the Quantum Machine program for the experiment, and returns the experiment object as a qua
program. This is used by theexecute_experiment()andsimulate_experiment()methods.- Returns:
The QUA program for the experiment defined by this class’s commands.
- Return type:
program
- data_processing(qm, job, live, wait_on_close=True, title_prefix='')[source]
Handles live data processing for a 1D experiment during execution.
This method fetches data from the Quantum Machine job, processes it into voltage units via digital demodulation, and generates a live plot when live is set to True. The plot shows I and Q as a function of acquisition time. After the experiment completes, the final data is saved into
save_data_dictfor later analysis and storage.- Parameters:
qm (QuantumMachine) – The Quantum Machine instance used to run the experiment.
job (RunningQmJob) – The job object representing the running experiment.
live (bool) – Flag indicating whether to generate live plots during data acquisition.
wait_on_close (bool) – Whether to wait for user to close plot after completion.
title_prefix (str) – Prefix to add to plot title.
2D Experiments
- class qeg_nmr_qua.experiment.experiment_2d.Experiment2D(settings, config=None, connect=True)[source]
Bases:
ExperimentClass to create and run 2D NMR experiments using the QUA programming language. Inherits from the base
Experimentclass and implements methods specific to 2D experiments, which can have a broad range of applications such as measuring relaxation times (T1, T2), performing pulse amplitude sweeps, and performing two-point correlation measurements under Hamiltonian engineering pulse sequences.2D experiments involve sweeping one parameter (e.g., pulse amplitude, delay time, evolution time) while measuring the system’s response. This is typically done by defining a variable vector that contains the values to be swept. The experiment loops over this vector, applying the corresponding parameter value in each iteration. In this class’s implementation, the swept parameter is varied first, then the averaging loop is performed. During longer experiments, this ordering should help mitigate the effects of slow drifts in system parameters.
- __init__(settings, config=None, connect=True)[source]
Initialize experiment with configuration and settings.
- Parameters:
settings (
ExperimentSettings) – Experiment-specific parameters (frequencies, pulse lengths, etc.).config (
OPXConfig) – OPX configuration. If None, automatically generated from settings.connect (bool) – Whether to establish a live connection to the OPX-1000 via
QuantumMachinesManager. Set toFalsefor offline compilation or unit testing. Defaults toTrue.
- Raises:
ValueError – If readout delay is too short to accommodate switching times.
- update_sweep_axis(new_axis)[source]
Updates the sweep axis for live plotting and data saving. If this method is not called, the first element of the variable vector collection
var_vec_listwill be used as the sweep axis by default. It can be convienient to change the sweep axis to a more physically meaningful quantity (e.g., converting pulse amplitude rescaling factor physical Vpp units).
- update_sweep_label(new_label)[source]
Updates the label for the sweep axis in live plotting. If this method is not called, the default label “Swept Variable” will be used.
- validate_experiment()[source]
Checks to make sure that the experiment contains variable operations, since it is a 2D experiment. Variable operations require looping which is supported in 2D experiments.
- Raises:
ValueError – No variable vector was found in the experiment commands.
- create_experiment()[source]
Creates the Quantum Machine program for the experiment, and returns the experiment object as a qua
program(). This is used by theexecute_experiment()andsimulate_experiment()methods.- Returns:
The QUA program for the experiment defined by this class’s commands.
- Return type:
program
- data_processing(qm, job, live, wait_on_close=True, title_prefix='')[source]
Handles live data processing for a 2D experiment during execution. This method fetches data from the Quantum Machine job, processes it into voltage units via digital demodulation, and generates live plots when live is set to True. The plot includes 2D color plots of the I and Q signals as functions of the swept variable and acquisition time, as well as a line plot of the primary signal, determined to be the first element of each FID’s I data. This captures the essential observable for 2D NMR experiments, such as calibrations of T1, T2, pulse amplitude sweeps, and Hamiltonian engineering measurements of two-point correlations.
After the experiment completes, the final data is saved into the
save_data_dictfor later analysis and storage.- Parameters:
qm (QuantumMachine) – The Quantum Machine instance used to run the experiment.
job (RunningQmJob) – The job object representing the running experiment.
live (bool) – Flag indicating whether to generate live plots during data acquisition.
wait_on_close (bool) – Whether to wait for user to close plot after completion.
title_prefix (str) – Prefix to add to plot title.
3D Experiments
- class qeg_nmr_qua.experiment.experiment_3d.Experiment3D(settings, config=None, connect=True)[source]
Bases:
ExperimentClass to create and run 3D NMR experiments using the QUA programming language. Inherits from the base
Experimentclass and implements methods specific to 3D experiments, which can have a broad range of applications such as measuring relaxation times (T1, T2), performing pulse amplitude sweeps, and performing two-point correlation measurements under Hamiltonian engineering pulse sequences.3D experiments involve sweeping two parameters (e.g., pulse amplitude, delay time, evolution time) while measuring the system’s response. This is typically done by defining two variable vectors that contain the values to be swept. The experiment loops over these vectors, applying the corresponding parameter values in each iteration. In this class’s implementation, the swept parameters are varied first, then the averaging loop is performed. During longer experiments, this ordering should help mitigate the effects of slow drifts in system parameters.
- __init__(settings, config=None, connect=True)[source]
Initialize experiment with configuration and settings.
- Parameters:
settings (
ExperimentSettings) – Experiment-specific parameters (frequencies, pulse lengths, etc.).config (
OPXConfig) – OPX configuration. If None, automatically generated from settings.connect (bool) – Whether to establish a live connection to the OPX-1000 via
QuantumMachinesManager. Set toFalsefor offline compilation or unit testing. Defaults toTrue.
- Raises:
ValueError – If readout delay is too short to accommodate switching times.
- update_sweep_axis_inner(new_axis)[source]
Updates the sweep axis for live plotting and data saving. If this method is not called, the variable vector
var_vecwill be used as the sweep axis by default. It can be convenient to change the sweep axis to a more physically meaningful quantity (e.g., converting pulse amplitude rescaling factor physical Vpp units).
- update_sweep_label_inner(new_label)[source]
Updates the label for the sweep axis in live plotting. If this method is not called, the default label “Swept Variable” will be used.
- update_sweep_axis_outer(new_axis)[source]
Updates the sweep axis for live plotting and data saving. If this method is not called, the variable vector
var_vecwill be used as the sweep axis by default. It can be convenient to change the sweep axis to a more physically meaningful quantity (e.g., converting pulse amplitude rescaling factor physical Vpp units).
- update_sweep_label_outer(new_label)[source]
Updates the label for the sweep axis in live plotting. If this method is not called, the default label “Swept Variable” will be used.
- validate_experiment()[source]
Checks to make sure that the experiment contains variable operations, since it is a 3D experiment. Variable operations require looping which is supported in 3D experiments.
- Raises:
ValueError – No variable vector was found in the experiment commands.
- create_experiment()[source]
Creates the Quantum Machine program for the experiment, and returns the experiment object as a qua
program(). This is used by theexecute_experiment()andsimulate_experiment()methods.- Returns:
The QUA program for the experiment defined by this class’s commands.
- Return type:
program
- data_processing(qm, job, live, wait_on_close=True, title_prefix='')[source]
Handles live data processing for a 3D experiment during execution.
Fetched data has shape
(n_outer, n_inner, n_time)after on-FPGA averaging. Two heatmaps are displayed, both with the outer-sweep variable on the y-axis and the inner-sweep variable on the x-axis:Left panel —
I[:, :, 0]: the first sampled time-point of the I quadrature for every (outer, inner) parameter pair, in µV. This is the primary NMR observable for sequences that refocus at time zero.Right panel —
stdofQ[:, :, -20:]: the standard deviation computed over the final 20 readout points of the Q quadrature, in µV. This tracks the noise floor of the tail of the FID.
- Parameters:
qm (QuantumMachine) – The Quantum Machine instance used to run the experiment.
job (RunningQmJob) – The job object representing the running experiment.
live (bool) – Flag indicating whether to generate live plots during data acquisition.
wait_on_close (bool) – Whether to wait for user to close plot after completion.
title_prefix (str) – Prefix to add to plot title.
Experiment Macros
- qeg_nmr_qua.experiment.macros.drive_mode(switch, amplifier)[source]
Configures the hardware such that the receiver switch does not allow signal to pass through, and the amplifier is turned on and unblanked, ready for driving the system. This adds 4 align() calls and 730 clock cycles of wait time.