{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to optimize controls using gradient-free optimization\n",
"**Perform graph-based optimizations when gradients are costly**\n",
"\n",
"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.\n",
"\n",
"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.\n",
"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).\n",
"Also since the gradient is not computed, the memory requirements for the gradient-free optimizer are much lower.\n",
"\n",
"The optimization engine from Boulder Opal allows the user to express their system Hamiltonians as almost-arbitrary functions of the controllable parameters.\n",
"The underlying structure of this map is a [graph](https://docs.q-ctrl.com/boulder-opal/topics/understanding-graphs-in-boulder-opal), which defines the cost function and can be efficiently evaluated.\n",
"The resulting optimized controls thus achieve the desired objectives within the constraints imposed by the user-defined Hamiltonian structure.\n",
"\n",
"The example in this user guide illustrates how to optimize multiple controls under different constraints in a single system using the gradient-free optimizer."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Summary workflow\n",
"### 1. Define the computational graph\n",
"The Boulder Opal optimization engine expresses all optimization problems as [data flow graphs](https://docs.q-ctrl.com/boulder-opal/topics/understanding-graphs-in-boulder-opal), which you can create with the `qctrl.create_graph` function.\n",
"The methods of the graph object allow you to represent the mathematical structure of the problem that you want to solve.\n",
"\n",
"For an optimization, a typical workflow is to:\n",
"- Create \"signals\", or scalar-valued functions of time, which typically represent control pulses.\n",
"- Create \"operators\", or matrix-valued functions of time, by modulating constant operators by signals. These typically represent terms of a Hamiltonian.\n",
"- Combine the operators into a single Hamiltonian operator.\n",
"- Calculate the optimization cost function (typically an infidelity) from the Hamiltonian.\n",
"\n",
"### 2. Execute graph-based gradient-free optimization\n",
"\n",
"You can calculate an optimization from an input graph using the `qctrl.functions.calculate_gradient_free_optimization` function.\n",
"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. \n",
"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. \n",
"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)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example: Optimal control of a single qubit\n",
"\n",
"This example shows how to optimize a Hamiltonian with multiple controls.\n",
"Specifically, consider a single-qubit system represented by the following Hamiltonian:\n",
"\n",
"$$H(t) = \\frac{\\nu}{2} \\sigma_{z} + \\frac{1}{2}\\left[\\gamma(t)\\sigma_{-} + \\gamma^*(t)\\sigma_{+}\\right] + \\frac{\\alpha(t)}{2} \\sigma_{z} ,$$\n",
"\n",
"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.\n",
"\n",
"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."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"pycharm": {
"is_executing": true,
"name": "#%%\n"
}
},
"outputs": [],
"source": [
"import numpy as np\n",
"from qctrlvisualizer import plot_controls\n",
"\n",
"from qctrl import Qctrl\n",
"\n",
"# Start a Boulder Opal session.\n",
"qctrl = Qctrl()"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"pycharm": {
"is_executing": true,
"name": "#%%\n"
}
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
" 0%| | 0/100 [00:00"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# Define physical constants.\n",
"nu = 2 * np.pi * 5e5 # rad/s\n",
"gamma_max = 2 * np.pi * 3e5 # rad/s\n",
"alpha_max = 2 * np.pi * 1e5 # rad/s\n",
"cutoff_frequency = 5e6 # Hz\n",
"segment_count = 50\n",
"duration = 10e-6 # s\n",
"\n",
"# Create the graph describing the system.\n",
"graph = qctrl.create_graph()\n",
"\n",
"# Create the time-independent detuning term.\n",
"detuning = nu * graph.pauli_matrix(\"Z\") / 2\n",
"\n",
"# Add a cosine envelope to the signals, to ensure\n",
"# they go to zero at the beginning and end of pulse.\n",
"cos_envelope = graph.signals.cosine_pulse_pwc(\n",
" duration=duration, segment_count=256, amplitude=1.0\n",
")\n",
"\n",
"# Create an optimizable complex-valued piecewise-constant (PWC) signal.\n",
"rough_gamma = graph.utils.complex_optimizable_pwc_signal(\n",
" segment_count=segment_count, maximum=gamma_max, duration=duration\n",
")\n",
"# Smooth the signal.\n",
"gamma = graph.utils.filter_and_resample_pwc(\n",
" pwc=rough_gamma, segment_count=256, cutoff_frequency=cutoff_frequency\n",
")\n",
"gamma = gamma * cos_envelope\n",
"gamma.name = r\"$\\gamma$\"\n",
"\n",
"# Create a PWC operator representing the drive term.\n",
"drive = graph.hermitian_part(gamma * graph.pauli_matrix(\"M\"))\n",
"\n",
"# Create an optimizable real-valued PWC signal.\n",
"rough_alpha = graph.utils.real_optimizable_pwc_signal(\n",
" segment_count=segment_count,\n",
" minimum=-alpha_max,\n",
" maximum=alpha_max,\n",
" duration=duration,\n",
")\n",
"# Smooth the signal.\n",
"alpha = graph.utils.filter_and_resample_pwc(\n",
" pwc=rough_alpha, segment_count=256, cutoff_frequency=cutoff_frequency\n",
")\n",
"alpha = alpha * cos_envelope\n",
"alpha.name = r\"$\\alpha$\"\n",
"\n",
"# Create a PWC operator representing the clock shift term.\n",
"shift = alpha * cos_envelope * graph.pauli_matrix(\"Z\") / 2\n",
"\n",
"# Define the total Hamiltonian and the target operation.\n",
"hamiltonian = detuning + drive + shift\n",
"target = graph.target(graph.pauli_matrix(\"Y\"))\n",
"\n",
"# Create the infidelity.\n",
"infidelity = graph.infidelity_pwc(hamiltonian, target, name=\"infidelity\")\n",
"\n",
"# Run the optimization.\n",
"result = qctrl.functions.calculate_gradient_free_optimization(\n",
" graph=graph,\n",
" cost_node_name=\"infidelity\",\n",
" output_node_names=[r\"$\\alpha$\", r\"$\\gamma$\"],\n",
" target_cost=5e-3,\n",
")\n",
"print(f\"Optimized cost:\\t{result.cost:.3e}\")\n",
"\n",
"# Plot the optimized controls.\n",
"plot_controls(result.output)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"| Package | Version |\n",
"| --------------------- | ------------ |\n",
"| Python | 3.10.8 |\n",
"| matplotlib | 3.6.3 |\n",
"| numpy | 1.24.1 |\n",
"| scipy | 1.10.0 |\n",
"| qctrl | 20.1.1 |\n",
"| qctrl-commons | 17.7.0 |\n",
"| boulder-opal-toolkits | 2.0.0-beta.3 |\n",
"| qctrl-visualizer | 4.4.0 |\n"
]
}
],
"source": [
"from qctrl.utils import print_environment_related_packages\n",
"\n",
"print_environment_related_packages()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.8"
}
},
"nbformat": 4,
"nbformat_minor": 4
}