Switching Function¶
So far we have considered the situation in which we know the control amplitudes but not the evolution of the state. The opposite problem is often of interest: which control amplitudes produce a desired evolution. This is the subject of optimal control. A common solution is to optimise for the control amplitudes with respect to a cost function. A building block of this optimisation procedure is the switching function: The variational derivative of the cost function with respect to the control amplitudes.
In what follows we will consider Mayer problems: problems in which the cost function \(J\) is a function of the final state \(\psi[\vec a(t);T]\). More specifically, we will take the cost function to be an expectation value of an observable \(\hat O\) with respect to the final state:
Using variational calculus we find the switching function is given by
where \(U(t\to T)\) is the unitary that evolves the system from time \(t\) to time \(T\).
However, our evolution is being approximated with a Suzuki-Trotter expansion—see previous section. Thus, in order to optimise our control amplitudes we should calculate our gradients by differentiating the Suzuki-Trotter expansion:
where for numerical efficiency we replace \(e^{-ia_{ik}H_k\Delta t}\) with \(U_ke^{-ia_{ik}D_k\Delta t}U_k^\dagger\) as in previous section.
Example¶
Numerically, we can compute \(\tilde \phi_j(n\Delta t)\) using switching_function(). As a worked example we will extend our Rabi oscillation example with a Pauli-y drive and we let
To simulate this we can execute the following program:
// examples/compute_switching_function.cpp
#include <cmath>
#include <iostream>
#include <Eigen/Dense>
#include <Suzuki-Trotter-Evolver/UnitaryEvolver.hpp>
using namespace Suzuki_Trotter_Evolver;
using Eigen::MatrixXcd;
// Defining the Pauli matrices
const complex<double> i(0, 1);
const MatrixXcd X {{0, 1}, // Pauli X
{1, 0}};
const MatrixXcd Y {{0, -i}, // Pauli Y
{i, 0}};
const MatrixXcd Z {{1, 0}, // Pauli Z
{0, -1}};
int main() {
// Parameters
double v = 1;
double f = 1;
double T = 1;
int N = 20;
MatrixXcd initial_state {{1},
{0}};
MatrixXcd O {{1, 1},
{1, -1}};
// Setting up the evolver
MatrixXcd drift_hamiltonian = v/2 * Z;
MatrixXcd control_hamiltonians(4, 2);
control_hamiltonians << X, Y;
UnitaryEvolver evolver(drift_hamiltonian, control_hamiltonians);
// Specifying the control amplitudes
double dt = T / N;
MatrixXcd ctrl_amp(N, 2);
for (int n = 0; n < N; n++) {
ctrl_amp(n, 0) = f*std::cos(v * n * dt);
}
// Computing the expectation value and the switching function
std::tuple<complex<double>, MatrixXcd> output =
evolver.switching_function(ctrl_amp, initial_state, dt, O);
// Unpacking results
complex<double> expectation_value = std::get<0>(output);
MatrixXcd switching_function = std::get<1>(output);
// Outputting the results
std::cout<<"Expectation value: "<<expectation_value.real()<<std::endl;
std::cout<<"Switching function:"<<std::endl<<switching_function<<std::endl;
return 0;
}
Output:
Expectation value: 0.577827
Switching function:
(-0.861075,0) (2.42157,0)
(-0.981027,0) (2.33944,0)
(-1.09672,0) (2.22863,0)
(-1.20674,0) (2.09098,0)
(-1.30974,0) (1.92885,0)
(-1.4045,0) (1.74503,0)
(-1.48996,0) (1.54262,0)
(-1.5652,0) (1.32497,0)
(-1.62946,0) (1.09558,0)
(-1.68218,0) (0.857956,0)
(-1.72296,0) (0.615554,0)
(-1.75157,0) (0.371674,0)
(-1.76796,0) (0.129392,0)
(-1.77222,0) (-0.108504,0)
(-1.76458,0) (-0.33956,0)
(-1.7454,0) (-0.561681,0)
(-1.71515,0) (-0.77315,0)
(-1.67436,0) (-0.972622,0)
(-1.62366,0) (-1.15911,0)
(-1.5637,0) (-1.33197,0)
The switching function output is a tuple. The first entry is the cost function (expectation value). Note we can compute the expectation value alone using evolved_expectation_value(). The second entry in the tuple is the switching function itself as a \(\texttt{length}\times N\) matrix.