# How to optimize controls with nonlinear dependences

**Incorporate nonlinear Hamiltonian dependences on control signals**

The Q-CTRL Python package exposes a highly-flexible optimization engine for general-purpose gradient-based optimization. It can be directly applied to model-based control optimization for arbitrary-dimensional quantum systems. In some cases the relationship between a control parameter and Hamiltonian term is nonlinear, presenting a major challenge for standard optimization routines.

In this notebook we demonstrate how to simply incorporate arbitrary nonlinear relationships between control parameters such that it is possible to directly perform an optimization which returns proper lab-frame control signals to generate and output. To learn the basics about control optimization, you can follow our robust optimization tutorial.

## Summary workflow

### 1. Define nonlinearity in computational graph

The flexible Q-CTRL optimization engine expresses all optimization problems as data flow graphs, which describe how optimization variables (variables that can be tuned by the optimizer) are transformed into the cost function (the objective that the optimizer attempts to minimize). Typical costs involve minimization of an infidelity metric captured using the `graph.infidelity_pwc`

graph operation.

Expressing non-linear dependence between control parameters and Hamiltonians is achieved by performing standard arithmetical operations on the control parameters prior to constructing signals which are employed in the graph. For instance:

```
signal_1 = graph.pwc_signal(values=signal_values, duration=duration, name="signal_1")
signal_1_squared = signal_1 ** 2 / signal_max
signal_1_squared.name = "signal_1_squared"
```

### 2. Execute graph-based optimization

With the graph object created, an optimization can be run using the `qctrl.functions.calculate_optimization`

function. The cost, the outputs, and the graph must be provided. The function returns the results of the optimization. Note this example code block uses naming that should be replaced with the naming used in your graph.

## Worked example: Optimized controls with a nonlinear relationship to a single-qubit Hamiltonian

We present an example showing how a system with non-linear dependence between controls and Hamiltonian may be optimized. We consider an artificial single-qubit system represented by the following Hamiltonian:

\begin{align*} H(t) &= \alpha_1(t)\sigma_x + \frac{\alpha_1(t)^2}{\alpha_{\text{max}}}\sigma_y + \alpha_2(t)\sigma_z \,, \end{align*}where $\alpha_1(t)$ and $\alpha_2(t)$ are real time-dependent pulses, and we note the non-linear dependence of the Hamiltonian on $\alpha_1(t)$.

While here we have chosen a simple artificial non-linearity for the sake of convenience and brevity, the approach we demonstrate extends trivially to far more complex, physically-relevant situations.

Expressing non-linear dependence between control parameters and Hamiltonians is achieved by performing standard arithmetical operations on the control parameters prior to constructing signals. In this case the operations are very simple - we simply square and re-scale the parameters describing the $\alpha_1(t)$ values - but in general any combination of supported primitive operations may be used.

```
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 Pauli matrices
sigma_x = np.array([[0, 1], [1, 0]])
sigma_y = np.array([[0, -1j], [1j, 0]])
sigma_z = np.array([[1, 0], [0, -1]])
# Define physical constraints
alpha_max = 2 * np.pi * 6e6 # Hz
segment_count = 40
duration = 200e-9 # s
# Create graph object
graph = qctrl.create_graph()
# Create the values backing the alpha_1(t) signal
alpha_1_values = graph.optimization_variable(
count=segment_count, lower_bound=-alpha_max, upper_bound=alpha_max
)
# Create the alpha_1(t) signal
alpha_1 = graph.pwc_signal(values=alpha_1_values, duration=duration, name="alpha_1")
# Apply a non-linear transformation to create the alpha_1_squared(t) signal
alpha_1_squared = alpha_1 ** 2 / alpha_max
alpha_1_squared.name = "alpha_1_squared"
# Create the alpha_2(t) signal
alpha_2 = graph.pwc_signal(
values=graph.optimization_variable(
count=segment_count, lower_bound=-alpha_max, upper_bound=alpha_max
),
duration=duration,
name="alpha_2",
)
# Create Hamiltonian terms
x_term = alpha_1 * sigma_x
y_term = alpha_1_squared * sigma_y
z_term = alpha_2 * sigma_z
target_operator = graph.target(operator=sigma_x)
# Create infidelity
infidelity = graph.infidelity_pwc(
hamiltonian=x_term + y_term + z_term, target=target_operator, name="infidelity"
)
# Run the optimization
optimization_result = qctrl.functions.calculate_optimization(
graph=graph,
cost_node_name="infidelity",
output_node_names=["alpha_1", "alpha_1_squared", "alpha_2"],
)
print(f"\nOptimized cost:\t{optimization_result.cost:.3e}")
# Plot the optimized controls
plot_controls(
plt.figure(),
controls={
r"$\alpha_1$": optimization_result.output["alpha_1"],
r"$\alpha_1^2$": optimization_result.output["alpha_1_squared"],
r"$\alpha_2$": optimization_result.output["alpha_2"],
},
)
plt.show()
```