# How to optimize controls using gradient-free optimization

Perform graph-based optimizations when gradients are costly

In addition to a highly-flexible optimization engine for general-purpose gradient-based optimization, Boulder Opal also features a gradient-free optimizer which can be directly applied to model-based control optimization for arbitrary-dimensional quantum systems.

The gradient-free optimizer is exposed by the `qctrl.functions.calculate_gradient_free_optimization`

function, which works similarly to `qctrl.functions.calculate_optimization`

, with most of the parameters overlapping between the two functions.
While the gradient-based optimization is more likely to find better results quicker, the gradient-free optimizer is useful in cases where the gradient is either very costly to compute or inaccessible (for example if the graph includes a node that does not allow gradients).
Also since the gradient is not computed, the memory requirements for the gradient-free optimizer are much lower.

The optimization engine from Boulder Opal allows the user to express their system Hamiltonians as almost-arbitrary functions of the controllable parameters. The underlying structure of this map is a graph, which defines the cost function and can be efficiently evaluated. The resulting optimized controls thus achieve the desired objectives within the constraints imposed by the user-defined Hamiltonian structure.

The example in this user guide illustrates how to optimize multiple controls under different constraints in a single system using the gradient-free optimizer.

## Summary workflow

### 1. Define the computational graph

The Boulder Opal optimization engine expresses all optimization problems as data flow graphs, which you can create with the `qctrl.create_graph`

function.
The methods of the graph object allow you to represent the mathematical structure of the problem that you want to solve.

For an optimization, a typical workflow is to:

- Create "signals", or scalar-valued functions of time, which typically represent control pulses.
- Create "operators", or matrix-valued functions of time, by modulating constant operators by signals. These typically represent terms of a Hamiltonian.
- Combine the operators into a single Hamiltonian operator.
- Calculate the optimization cost function (typically an infidelity) from the Hamiltonian.

### 2. Execute graph-based gradient-free optimization

You can calculate an optimization from an input graph using the `qctrl.functions.calculate_gradient_free_optimization`

function.
Provide the name of the node of the graph that represents the cost, and this function will return the optimized value of the output nodes that you requested.
Unlike `qctrl.functions.calculate_optimization`

, which uses the gradient and halts when it has converged to a minimum, `qctrl.functions.calculate_gradient_free_optimization`

cannot rely on such convergence criteria.
Instead, the function stops after performing `iteration_count`

iterations (which defaults to 100), unless the cost reaches a value below `target_cost`

(if you provide one).

## 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:

$$H(t) = \frac{\nu}{2} \sigma_{z} + \frac{1}{2}\left[\gamma(t)\sigma_{-} + \gamma^*(t)\sigma_{+}\right] + \frac{\alpha(t)}{2} \sigma_{z} ,$$

where $\nu$ is the qubit detuning, $\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.

The functions of time $\gamma(t)$ and $\alpha(t)$ are not predetermined, and instead are optimized by Boulder Opal in order to achieve the target operation, which in this case is a Y-gate.

```
import numpy as np
from qctrlvisualizer import plot_controls
from qctrl import Qctrl
# Start a Boulder Opal session.
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 = 50
duration = 10e-6 # s
# Create the graph describing the system.
graph = qctrl.create_graph()
# Create the time-independent detuning term.
detuning = nu * graph.pauli_matrix("Z") / 2
# Add a cosine envelope to the signals, to ensure
# they go to zero at the beginning and end of pulse.
cos_envelope = graph.signals.cosine_pulse_pwc(
duration=duration, segment_count=256, amplitude=1.0
)
# Create an optimizable complex-valued piecewise-constant (PWC) signal.
rough_gamma = graph.utils.complex_optimizable_pwc_signal(
segment_count=segment_count, maximum=gamma_max, duration=duration
)
# Smooth the signal.
gamma = graph.utils.filter_and_resample_pwc(
pwc=rough_gamma, segment_count=256, cutoff_frequency=cutoff_frequency
)
gamma = gamma * cos_envelope
gamma.name = r"$\gamma$"
# 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=duration,
)
# Smooth the signal.
alpha = graph.utils.filter_and_resample_pwc(
pwc=rough_alpha, segment_count=256, cutoff_frequency=cutoff_frequency
)
alpha = alpha * cos_envelope
alpha.name = r"$\alpha$"
# Create a PWC operator representing the clock shift term.
shift = alpha * cos_envelope * graph.pauli_matrix("Z") / 2
# Define the total Hamiltonian and the target operation.
hamiltonian = detuning + drive + shift
target = graph.target(graph.pauli_matrix("Y"))
# Create the infidelity.
infidelity = graph.infidelity_pwc(hamiltonian, target, name="infidelity")
# Run the optimization.
result = qctrl.functions.calculate_gradient_free_optimization(
graph=graph,
cost_node_name="infidelity",
output_node_names=[r"$\alpha$", r"$\gamma$"],
target_cost=5e-3,
)
print(f"Optimized cost:\t{result.cost:.3e}")
# Plot the optimized controls.
plot_controls(result.output)
```

```
Your task calculate_gradient_free_optimization (action_id="1599305") has started.
Your task calculate_gradient_free_optimization (action_id="1599305") has completed.
Optimized cost: 8.862e-03
```

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.10.8 |

matplotlib | 3.6.3 |

numpy | 1.24.1 |

scipy | 1.10.0 |

qctrl | 20.1.1 |

qctrl-commons | 17.7.0 |

boulder-opal-toolkits | 2.0.0-beta.3 |

qctrl-visualizer | 4.4.0 |