# How to optimize controls with time symmetrization

Incorporate time symmetry into optimized waveforms

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, forcing control solutions to exhibit time symmetry can yield natural noise robustness while simplifying the optimization problem (by halving the dimensionality of the search space). We will show an easy method to build time-symmetrized pulses here.

## Summary workflow

### 1. Define time-symmetry constraint 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).

To enforce time symmetry, we create the control signals in two steps: we first create a standard signal for the first half of the gate duration, and then we create a copy (depending on the same underlying control parameters) that is reflected and concatenated with the original signal using the symmetrize_pwc graph operation.

pwc_signal = graph.symmetrize_pwc(half_pwc_signal, name="pwc_signal")

### 2. Execute graph-based optimization

With the graph object created, an optimization can be run using the qctrl.functions.calculate_optimization function. The function returns the results of the optimization and should include the symmetrized PWC as an output node of the graph using output_node_names.

## Worked example: Time-symmetrized robust pulses for a single qubit

In this example, we perform a robust optimization of a single qubit using symmetric pulses. The single-qubit system is represented by the following Hamiltonian:

\begin{align*} H(t) &= \frac{\nu}{2} \sigma_{z} + \frac{1}{2}\left(\gamma(t)\sigma_{-} + \gamma^*(t)\sigma_{+}\right) + \frac{\alpha(t)}{2} \sigma_{z} + \beta(t) \sigma_{z} \,, \end{align*}

where $\nu$ is the qubit detuning, $\gamma(t)$ and $\alpha(t)$ are, respectively, complex and real time-dependent pulses, and $\beta(t)$ is a small, slowly-varying stochastic dephasing noise process. Here $\gamma(t)$ and $\alpha(t)$ will be optimized subject to a time-symmetry constraint.

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 standard matrices
sigma_y = np.array([[0, -1j], [1j, 0]])
sigma_z = np.array([[1, 0], [0, -1]])
sigma_m = np.array([[0, 1], [0, 0]])

# Define physical constraints
gamma_max = 2 * np.pi * 8.5e6  # Hz
alpha_max = 2 * np.pi * 8.5e6  # Hz
nu = 2 * np.pi * 6e6  # Hz
segment_count = 50
duration = 154e-9  # s

# Create graph object
graph = qctrl.create_graph()

# Create detuning term.
detuning = nu * sigma_z / 2

# Create a complex PWC signal describing the first half of gamma(t)
half_gamma = graph.complex_pwc_signal(
moduli=graph.optimization_variable(
count=segment_count, lower_bound=0, upper_bound=gamma_max
),
phases=graph.optimization_variable(
count=segment_count,
lower_bound=0,
upper_bound=2 * np.pi,
is_lower_unbounded=True,
is_upper_unbounded=True,
),
duration=duration / 2,
)
# Define gamma(t) by symmetrizing half_gamma
gamma = graph.symmetrize_pwc(half_gamma, name="gamma")
# Create drive term
drive = graph.pwc_operator_hermitian_part(gamma * sigma_m)

# Create alpha(t) similarly
alpha = graph.symmetrize_pwc(
graph.pwc_signal(
values=graph.optimization_variable(
count=segment_count, lower_bound=-alpha_max, upper_bound=alpha_max
),
duration=duration / 2,
),
name="alpha",
)
# Create clock shift term
shift = alpha * sigma_z / 2

# Create dephasing noise term
dephasing = sigma_z / duration

# Create target
target_operator = graph.target(operator=sigma_y)

# Create infidelity
infidelity = graph.infidelity_pwc(
hamiltonian=detuning + drive + shift,
target=target_operator,
noise_operators=[dephasing],
name="infidelity",
)

# Run the optimization
optimization_result = qctrl.functions.calculate_optimization(
cost_node_name="infidelity", output_node_names=["alpha", "gamma"], graph=graph
)

print(f"\nOptimized cost:\t{optimization_result.cost:.3e}")

# Plot the optimized controls
plot_controls(
plt.figure(),
controls={
"$\\alpha$": optimization_result.output["alpha"],
"$\\gamma$": optimization_result.output["gamma"],
},
)
plt.show()

Your task calculate_optimization (action_id="618070") has started. You can use the qctrl.get_result method to retrieve previous results. 