Q-CTRL logo

How to find time-optimal controls

Optimizing over the duration of your controls

Boulder Opal provides a highly-flexible optimization engine for general-purpose gradient-based optimization, which can be directly applied to model-based control optimization in arbitrary quantum systems. By expressing the problem as a graph which defines the cost function to be minimized, one can obtain optimized controls which achieve the desired objectives within the constraints imposed by the system.

Boulder Opal is also capable of generating time-optimal controls by aiming to minimize the duration of the generated pulses. This allows you to obtain not only pulses which minimize a target infidelity, but also do so in a short timescale.

Summary workflow

1. Create an optimization variable for the optimized duration

optimizable_duration = graph.optimization_variable(1, 0, max_duration)[0]
optimizable_duration.name = "optimizable_duration"

2. Scale the values in your calculation

Set up your optimization graph similarly to how you would otherwise, but multiply the energies and frequencies by the duration, and divide the times by the duration. For example, multiply the Hamiltonian by optimizable_duration, and set the duration to 1.

This will lead to the same evolution, as the dynamics driven by a Hamiltonian $H$ for a time $T$ are equivalent to those driven by a (unitless) Hamiltonian $\tilde{H} = H T$ for a (unitless) time $\tilde{T} = 1$.

3. Define the optimization cost

As now your optimization has two targets (minimizing infidelity and duration), you need to combine them in a single node. One way of doing this is with a weighted sum of both items, for instance,

cost = infidelity + 0.1 * optimizable_duration / max_duration
cost.name = "cost"

You can alter the incentives of the optimizer to minimize the duration or the infidelity by changing the weight of each term in the sum (or using a different form for the cost function).

3. Execute graph-based optimization

You can now run the optimization as usual using, for instance qctrl.functions.calculate_optimization. Note that some of the output graph values might need to be rescaled back by a factor of optimizable_duration (or its inverse).

Worked example: Optimal control of a single qubit

This example shows how to optimize a Hamiltonian with multiple controls. Specifically, consider a single-qubit system represented by the following Hamiltonian: \begin{equation} H(t) = \frac{1 + \beta(t)}{2} \left[\gamma(t)\sigma_{-} + \gamma^*(t)\sigma_{+}\right] + \frac{\alpha(t)}{2} \sigma_{z} \, , \end{equation} where $\gamma(t)$ and $\alpha(t)$ are, respectively, complex and real time-dependent pulses, $\sigma_{\pm}$ are the qubit ladder operators, and $\sigma_{z}$ is the Pauli-Z operator. $\beta(t)$ is a small, slowly-varying amplitude noise acting on $\gamma(t)$.

We will obtain optimal pulses for $\gamma(t)$ and $\alpha(t)$, robust to the amplitude noise $\beta(t)$, to achieve a target Y-gate operation. Moreover, we will aim to obtain a pulse that is as short as possible by adding a penalty to the cost function that increases with the pulse duration.

import matplotlib.pyplot as plt
import numpy as np
from qctrlvisualizer import plot_controls

from qctrl import Qctrl

# Starting a session with the API.
qctrl = Qctrl()
# Define physical constants.
nu = 2 * np.pi * 5e5  # rad/s
gamma_max = 2 * np.pi * 3e5  # rad/s
alpha_max = 2 * np.pi * 1e5  # rad/s
cutoff_frequency = 5e6  # Hz
segment_count = 16
max_duration = 10e-6  # s

# Crate the graph describing the system.
graph = qctrl.create_graph()

# Define optimizable duration.
duration = graph.optimization_variable(1, 0, max_duration)[0]
duration.name = "duration"

# Create the time-independent detuning term.
detuning = nu * graph.pauli_matrix("Z") / 2

# Create a optimizable complex-valued piecewise-constant (PWC) signal.
rough_gamma = graph.utils.complex_optimizable_pwc_signal(
    segment_count=segment_count, maximum=gamma_max, duration=1.0
# Smooth the signal.
gamma = graph.utils.filter_and_resample_pwc(
    cutoff_frequency=cutoff_frequency * duration,

# Create a PWC operator representing the drive term.
drive = graph.hermitian_part(gamma * graph.pauli_matrix("M"))

# Create an optimizable real-valued PWC signal.
rough_alpha = graph.utils.real_optimizable_pwc_signal(
    segment_count=segment_count, minimum=-alpha_max, maximum=alpha_max, duration=1.0
# Smooth the signal.
alpha = graph.utils.filter_and_resample_pwc(
    cutoff_frequency=cutoff_frequency * duration,
# Create a PWC operator representing the clock shift term.
shift = alpha * graph.pauli_matrix("Z") / 2

# Define the total Hamiltonian.
hamiltonian = drive + shift  # + detuning

# Create the infidelity.
infidelity = graph.infidelity_pwc(
    hamiltonian=hamiltonian * duration,
    noise_operators=[drive * duration],

cost = infidelity + 0.1 * duration / max_duration
cost.name = "cost"

# Run the optimization.
result = qctrl.functions.calculate_optimization(
    output_node_names=[r"$\alpha$", r"$\gamma$", "duration", "infidelity"],
print(f"Optimized cost:\t\t{result.cost:.3e}")
print(f"Optimized infidelity:\t{result.output['infidelity']['value']:.3e}")
optimized_duration = result.output["duration"]["value"]
print(f"Optimized duration:\t{optimized_duration:.3e}")

# Plot the optimized controls, scaling back the durations.
gamma_durations, gamma_values, _ = qctrl.utils.pwc_pairs_to_arrays(
gamma_durations = gamma_durations * optimized_duration

alpha_durations, alpha_values, _ = qctrl.utils.pwc_pairs_to_arrays(
alpha_durations = alpha_durations * optimized_duration

        r"$\gamma$": {"durations": gamma_durations, "values": gamma_values},
        r"$\alpha$": {"durations": alpha_durations, "values": alpha_values},
Your task calculate_optimization (action_id="1169978") has started. You can use the `qctrl.get_result` method to retrieve previous results.
Your task calculate_optimization (action_id="1169978") has completed.
Optimized cost:		4.466e-02
Optimized infidelity:	5.919e-05
Optimized duration:	4.460e-06


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
matplotlib 3.5.1
numpy 1.21.5
scipy 1.7.3
qctrl 19.1.0
qctrlcommons 17.1.1
qctrltoolkit 1.5.0
qctrlvisualizer 3.2.1