Q-CTRL logo
Get the notebook

How to format and export control solutions for hardware implementation

Prepare optimized controls for hardware implementation

Boulder Opal provides flexible optimization tools to obtain robust model-based or model-free optimized controls. These controls are returned as Python dictionaries or arrays, making them easy to integrate into most experimental workflows. In some cases, however, the control outputs may need to be adapted. In this notebook, we demonstrate how to transform optimized controls into a hardware-ready form and export the result.

Summary workflow

1. Obtain or import optimized controls

Optimized controls can be obtained from Boulder Opal’s optimization tools. In the worked example below we import the control generated in this user guide.

For model-based optimization, the optimization result is obtained from calculations of the following form, where the output_node_names include the controls:

optimization_result = qctrl.functions.calculate_optimization(
    output_node_names=["alpha", "gamma"],

In this instance, the controls are extracted using optimization_result.output["alpha"] for the control labeled alpha. This provides a list of dictionaries for each pulse segment, where each segment has a value and a duration. You can extract arrays with the values and durations of the controls with qctrl.utils.pwc_pairs_to_arrays.

qctrl.utils is part of the Boulder Opal Toolkits which are currently in beta phase of development. Breaking changes may be introduced.

For automated optimization, the controls are returned as a NumPy array with a pre-defined duration and segment_count.

2. Ensure that the sampling rate is hardware-compatible

To implement a control pulse in a hardware device, we need to make sure that the pulse segment durations match the sampling rate of the corresponding arbitrary waveform generator (AWG). This can be enforced in the optimization design, as shown in our notebook for robust single qubit gates on IBM devices. If the optimized pulse segments are coarser than the AWG time resolution (dt), you will need to subdivide the pulse into an integer number of dt, when possible, or you can filter and discretize the pulse appropriately within your optimization.

3. Format the control pulses for the target hardware

Different devices have different input format requirements. The optimized control type can be manipulated or the format transformed (for instance taking real and imaginary parts).

4. Export the controls

Finally, the controls can be saved to your preferred file type, for example JSON, CSV or pickle. The saved controls are then ready to be imported and applied on your hardware.

Worked example: Formatting and exporting a model-based optimized control pulse

We present a detailed worked example of how to export an optimized control in a format that is ready for hardware implementation.

Specifically, we consider a pulse optimized for trapped ions by following the How to optimize error-robust Mølmer–Sørensen gates for trapped ions user guide. We first load the pre-saved model-based optimization result from the qctrl.functions.calculate_optimization function, and extract the control pulse ion_drive. Next, we adapt the 4 µs resolution of the control pulse to match a 1 µs pulse sampling period requirement for the pulse generation hardware. Finally, we extract the real and imaginary parts of the pulse and export them to the CSV file format.

# Import the relevant packages and functions
import csv
import jsonpickle
import numpy as np

from qctrl import Qctrl

# Starting a session with the API
qctrl = Qctrl()

def load_var(file_name):
    # Return a variable from a json file
    file_path = "./resources/"
    with open(file_path + file_name, "r+") as f:
        encoded = f.read()
        decoded = jsonpickle.decode(encoded)
    return decoded
# Import and extract the control pulse
optimization_result = load_var("ion_optimization_result")
control_pulse = optimization_result.output["ion_drive"]
original_sampling_period = control_pulse[0]["duration"]
print("Original sampling period (s):", original_sampling_period)

# Repeat the pulse values to match the target sampling period
target_sampling_period = 1e-6
scaling = original_sampling_period / target_sampling_period
control_pulse_values = np.repeat(
    qctrl.utils.pwc_pairs_to_arrays(control_pulse)[1], scaling
control_pulse_duration = np.sum([segment["duration"] for segment in control_pulse])
    f"{len(control_pulse_values)} segments matches the target "
    f"{control_pulse_duration / target_sampling_period:.1f} samples for a "
    f"{control_pulse_duration * 1e6:.1f} µs pulse."

# Take the real and imaginary parts of the control pulse values
control_pulse_values = [np.real(control_pulse_values), np.imag(control_pulse_values)]

# Export the controls to CSV, with real and imaginary arrays on subsequent lines
save_filename = "resources/exported_pulse.csv"
with open(save_filename, "w", newline="") as f:
    writer = csv.writer(f)

# Reload pulse data and check that it is unchanged
with open(save_filename, newline="") as f:
    reader = csv.reader(f)
    data = [row for row in reader]
real_data = np.array(data[0], dtype=np.float64)
imag_data = np.array(data[1], dtype=np.float64)
    "Pulse data is accurately reloaded:",
    np.all(real_data == control_pulse_values[0]),
    np.all(imag_data == control_pulse_values[1]),
Original sampling period (s): 4e-06
200 segments matches the target 200.0 samples for a 200.0 µs pulse.
Pulse data is accurately reloaded: True True

This notebook was run using the following package versions. It should also be compatible with newer versions of the Q-CTRL Python package.

Package version
Python 3.9.12
jsonpickle 1.5.2
numpy 1.21.5
scipy 1.7.3
qctrl 19.1.0
qctrlcommons 17.1.1
qctrltoolkit 1.5.0

Next up

Continue learning about Boulder Opal

How to cite Boulder Opal

Cite relevant Boulder Opal articles and specific documentation pages