How to migrate to the new Boulder Opal package

Upgrade your pre-existing Boulder Opal code by the end of 2023

In this user guide we demonstrate the key points on how you can quickly and efficiently migrate your pre-existing Boulder Opal code from using the qctrl package to the new Boulder Opal client package boulderopal. Migrating to the new Boulder Opal client package comes with many advantages meant to accelerate development pace and help you get the most out of your Boulder Opal subscription.

These benefits include:

  • A simplified interface. The new boulderopal client supports more natural object types (like NumPy arrays) and simpler syntax (for instance, requiring less code), especially for closed-loop optimization and noise reconstruction.
  • Improved error handling. We are now able to detect errors on the client side that we could not previously with the legacy qctrl client, so the error and warnings are surfaced earlier to you, can be more informative, and ultimately save you compute time.

Installing the Boulder Opal Python client

You can install the Boulder Opal Python client using pip on the command line.

pip install boulder-opal

Overview of general interface changes

The motivation behind some of the changes outlined below was to make Boulder Opal accessible using a more Pythonic paradigm. As you will see, the boulderopal package provides a more intuitive and simple interface for accessing Boulder Opal's functionality. To achieve this, several substantial changes were made, as follows.

The Qctrl object

The new boulderopal package no longer features a Qctrl object. You can import it as a regular Python package and use its functions without reference to a session object.

import boulderopal as bo


The first time you call a function from the boulderopal package that involves the cloud, your Boulder Opal session will be authenticated. This may mean that a browser window pops up asking you to log into the account associated with your Boulder Opal subscription, or if you've signed-in recently, it will automatically authenticate from a temporary token which gets saved on your local machine.


If you're in more than one organization, you need to select which organization's resources to use when running calculations on the cloud. This can be done with, with your organization's slug. To find your organization's slug, view your plan details in the Boulder Opal web app."your-organization-slug")

Function input/output changes

You don't need to pass the parameters to functions and class constructors as keyword arguments (as you needed to in the legacy client).

The functions in the Boulder Opal client return a dictionary, instead of a custom Boulder Opal object. This was done to make results more intuitive and easy to work with. For example, to access the output of a graph calculation, use result["output"] rather than the previous result.output.

Graph calculations export PWC nodes as a dictionary with "durations", "values", and "time_dimension" keys (rather than nested lists of "duration"/"value" dictionaries as in the legacy client). The "durations" and "values" of the piecewise-constant function are represented as NumPy arrays, and the "time_dimension" integer indicates the dimension of the values corresponding to time.

Functionality in the new client

Graph-based functionality

Instantiating a graph

You can instantiate a graph by calling its constructor at boulderopal.Graph.

graph = bo.Graph()

Graph operations

Some graph operations have been moved.

Note that graph.filter_and_resample_pwc now accepts a kernel rather than a cutoff_frequency, so you can now use this node with both a graph.gaussian_convolution_kernel or a graph.sinc_convolution_kernel.

Graph calculations

To execute graphs use boulderopal.execute_graph. Its inputs work very similarly to the legacy function qctrl.functions.calculate_graph.

import numpy as np

graph = bo.Graph()

amplitude = np.pi * 1e5  # rad/s
duration = 5e-6  # s

pi_pulse = graph.constant_pwc(amplitude, duration)
infidelity = graph.infidelity_pwc(
    hamiltonian=pi_pulse * graph.pauli_matrix("X"),"X")),

result = bo.execute_graph(graph, "infidelity")
print(f"π-pulse infidelity: {result['output']['infidelity']['value']:.3e}")
Your task (action_id="1812227") is queued.
Your task (action_id="1812227") has started.
Your task (action_id="1812227") has completed.
π-pulse infidelity: 4.441e-16

Graph optimization

To optimize graphs use boulderopal.run_optimization. Its inputs work very similarly to the legacy function qctrl.functions.calculate_optimization.

import qctrlvisualizer as qv

# Define physical constraints.
omega_max = 2 * np.pi * 6e6  # Hz
delta_max = 2 * np.pi * 3e6  # Hz
segment_count = 32
duration = 200e-9  # s

graph = bo.Graph()

# Create optimizable signal.
envelope = graph.signals.cosine_pulse_pwc(
    duration=duration, segment_count=segment_count, amplitude=1.0
omega = envelope * graph.complex_optimizable_pwc_signal(
    segment_count=segment_count, maximum=omega_max, duration=duration
) = r"$\Omega$"

# Create Hamiltonian and define infidelity.
hamiltonian = graph.hermitian_part(omega * graph.pauli_matrix("X"))
infidelity = graph.infidelity_pwc(

# Run the optimization.
result = bo.run_optimization(

print(f"Optimized infidelity: {result['cost']:.3e}")

qv.plot_controls(result["output"], polar=False)
Your task (action_id="1812228") has started.
Your task (action_id="1812228") has completed.
Optimized infidelity: 1.110e-14


You can similarly use the other graph-based optimizers with boulderopal.run_stochastic_optimization or boulderopal.run_gradient_free_optimization.

Closed-loop optimization

The functionality to perform closed-loop optimization is in the boulderopal.closed_loop module, featuring two main functions to perform closed-loop optimizations: boulderopal.closed_loop.optimize and boulderopal.closed_loop.step.

boulderopal.closed_loop.optimize sets up a closed-loop optimization loop and runs it until a stopping criterion is met. It works very similarly to the legacy client's toolkit function qctrl.closed_loop.optimize, although there are a few small differences. See the function's reference page or this user guide for more information.

In those cases where boulderopal.closed_loop.optimize is not flexible enough, you can set up your own loop using boulderopal.closed_loop.step, which performs a single closed-loop optimization step. This works very similarly to the legacy function qctrl.functions.calculate_closed_loop_optimization_step, although we have simplified its inputs to make them more intuitive. See the function's reference page or this user guide for more information.

Noise reconstruction

The noise reconstruction functionality has been rebuilt from the ground up to have more natural inputs. Please look at the reference documentation and this user guide for more details.

Pulse library

The (non-graph-based) pulse library (originally in qctrl.signals) can be accessed through the boulderopal.signals namespace.

cosine_pulse = bo.signals.cosine_pulse(duration=10e-9, amplitude=5e6, drag=0.1)
array([ 122358.70926212-4.85402760e+13j, 1030536.86926882-1.27080092e+14j,
       2500000.        -1.57079633e+14j, 3969463.13073118-1.27080092e+14j,
       4877641.29073788-4.85402760e+13j, 4877641.29073788+4.85402760e+13j,
       3969463.13073118+1.27080092e+14j, 2500000.        +1.57079633e+14j,
       1030536.86926882+1.27080092e+14j,  122358.70926212+4.85402760e+13j])

Ions and superconducting toolkits

The system-specific toolkits (originally in qctrl.ions and qctrl.superconducting) can be found in the boulderopal.ions and boulderopal.superconducting modules.

The functionality remains the same, but some of the function signatures have changed slightly. For example, the parameter order has been tweaked, and the returned object is a direct graph calculation object (so the keys and contents of the resulting dictionary are different). See the reference documentation for the ions and superconducting modules for more information.

Managing calculations

Parallel calculations

Depending on your Boulder Opal plan and the number of available machines in your environment, you can run calculations in parallel. You can preprovision your environment before submitting your tasks by bringing machines online using

You can also group multiple calculations and queue them together with, like with the legacy function qctrl.parallel. The results of the grouped calculations will be returned at the same time.

For more information, you can read the Computational resources in Boulder Opal topic or consult the reference documentation.

Retrieving results

You can retrieve results from previous calculations using This is equivalent to the legacy qctrl.get_result.

Removed functionality

Simulation functions

The legacy simulation functions (qctrl.functions.calculate_colored_noise_simulation, qctrl.functions.calculate_coherent_simulation, and qctrl.functions.calculate_quasi_static_scan) have been replaced by graph calculations. Please use a graph calculation instead. For more information, some relevant user guides about representing quantum systems in graphs, performing noiseless simulations, performing quasi-static scans, and performing simulations involving noise.

Filter functions

The legacy function to calculate filter functions (qctrl.functions.calculate_filter_function) has also been replaced by graph calculations in the new client, using the graph.filter_function operation. Calculating them in a graph is more flexible and faster. You can find more information in this user guide.

Cross-entropy closed-loop optimizer

The cross-entropy method is not available in the new client. You can use any of the other closed-loop optimizers as they're bound to return better results.

Reinforcement learning

The reinforcement learning features from the Q-CTRL Python package have not been migrated to the new client yet.

Was this useful?

cta background

New to Boulder Opal?

Get access to everything you need to automate and optimize quantum hardware performance at scale.