# Control Methods

All optimizations in the `QuantumControl`

package are done by calling `QuantumControl.optimize`

, or preferably the high-level wrapper `@optimize_or_load`

. The actual control methods are implemented in separate packages. The module implementing a particular method should be passed to `optimize`

as the `method`

keyword argument.

`QuantumControl.optimize`

— MethodOptimize a quantum control problem.

```
result = optimize(
problem;
method, # mandatory keyword argument
check=true,
callback=nothing,
print_iters=true,
kwargs...
)
```

optimizes towards a solution of given `problem`

with the given `method`

, which should be a `Module`

implementing the method, e.g.,

```
using Krotov
result = optimize(problem; method=Krotov)
```

If `check`

is true (default), the `initial_state`

and `generator`

of each trajectory is checked with `check_state`

and `check_generator`

. Any other keyword argument temporarily overrides the corresponding keyword argument in `problem`

. These arguments are available to the optimizer, see each optimization package's documentation for details.

The `callback`

can be given as a function to be called after each iteration in order to analyze the progress of the optimization or to modify the state of the optimizer or the current controls. The signature of `callback`

is method-specific, but callbacks should receive a workspace objects as the first parameter as the first argument, the iteration number as the second parameter, and then additional method-specific parameters.

The `callback`

function may return a tuple of values, and an optimization method should store these values fore each iteration in a `records`

field in their `Result`

object. The `callback`

should be called once with an iteration number of `0`

before the first iteration. The `callback`

can also be given as a tuple of vector of functions, which are automatically combined via `chain_callbacks`

.

If `print_iters`

is `true`

(default), an automatic `callback`

is created via the method-specific `make_print_iters`

to print the progress of the optimization after each iteration. This automatic callback runs after any manually given `callback`

.

All remaining keyword argument are method-specific. To obtain the documentation for which options a particular method uses, run, e.g.,

`? optimize(problem, ::Val{:Krotov})`

where `:Krotov`

is the name of the module implementing the method. The above is also the method signature that a `Module`

wishing to implement a control method must define.

The returned `result`

object is specific to the optimization method.

The following methods of optimal control are implemented by packages in the JuliaQuantumControl organization:

## Krotov's Method

See the documentation of the `Krotov`

package for more details.

`QuantumControl.optimize`

— Method```
using Krotov
result = optimize(problem; method=Krotov, kwargs...)
```

optimizes the given control `problem`

using Krotov's method, returning a `KrotovResult`

.

Keyword arguments that control the optimization are taken from the keyword arguments used in the instantiation of `problem`

; any of these can be overridden with explicit keyword arguments to `optimize`

.

**Required problem keyword arguments**

`J_T`

: A function`J_T(Ψ, trajectories)`

that evaluates the final time functional from a list`Ψ`

of forward-propagated states and`problem.trajectories`

. The function`J_T`

may also take a keyword argument`tau`

. If it does, a vector containing the complex overlaps of the target states (`target_state`

property of each trajectory in`problem.trajectories`

) with the propagated states will be passed to`J_T`

.

**Recommended problem keyword arguments**

`lambda_a=1.0`

: The inverse Krotov step width λₐ for every pulse.`update_shape=(t->1.0)`

: A function`S(t)`

for the "update shape" that scales the update for every pulse.

If different controls require different `lambda_a`

or `update_shape`

, a dict `pulse_options`

must be given instead of a global `lambda_a`

and `update_shape`

; see below.

**Optional problem keyword arguments**

The following keyword arguments are supported (with default values):

`pulse_options`

: A dictionary that maps every control (as obtained by`get_controls`

from the`problem.trajectories`

) to the following dict:`:lambda_a`

: The value for inverse Krotov step width λₐ.`:update_shape`

: A function`S(t)`

for the "update shape" that scales the Krotov pulse update.

This overrides the global

`lambda_a`

and`update_shape`

arguments.`chi`

: A function`chi(Ψ, trajectories)`

that receives a list`Ψ`

of the forward propagated states and returns a vector of states $|χₖ⟩ = -∂J_T/∂⟨Ψₖ|$. If not given, it will be automatically determined from`J_T`

via`make_chi`

with the default parameters. Similarly to`J_T`

, if`chi`

accepts a keyword argument`tau`

, it will be passed a vector of complex overlaps.`sigma=nothing`

: A function that calculates the second-order contribution. If not given, the first-order Krotov method is used.`iter_start=0`

: The initial iteration number.`iter_stop=5000`

: The maximum iteration number.`prop_method`

: The propagation method to use for each trajectory; see below.`print_iters=true`

: Whether to print information after each iteration.`store_iter_info=Set()`

: Which fields from`print_iters`

to store in`result.records`

. A subset of`Set(["iter.", "J_T", "∫gₐ(t)dt", "J", "ΔJ_T", "ΔJ", "secs"])`

.`callback`

: A function (or tuple of functions) that receives the Krotov workspace, the iteration number, the list of updated pulses, and the list of guess pulses as positional arguments. The function may return a tuple of values which are stored in the`KrotovResult`

object`result.records`

. The function can also mutate any of its arguments, in particular the updated pulses. This may be used, e.g., to apply a spectral filter to the updated pulses or to perform similar manipulations. Note that`print_iters=true`

(default) adds an automatic callback to print information after each iteration. With`store_iter_info`

, that callback automatically stores a subset of the printed information.`check_convergence`

: A function to check whether convergence has been reached. Receives a`KrotovResult`

object`result`

, and should set`result.converged`

to`true`

and`result.message`

to an appropriate string in case of convergence. Multiple convergence checks can be performed by chaining functions with`∘`

. The convergence check is performed after any`callback`

.`verbose=false`

: If`true`

, print information during initialization.`rethrow_exceptions`

: By default, any exception ends the optimization but still returns a`KrotovResult`

that captures the message associated with the exception. This is to avoid losing results from a long-running optimization when an exception occurs in a later iteration. If`rethrow_exceptions=true`

, instead of capturing the exception, it will be thrown normally.

**Trajectory propagation**

Krotov's method involves the forward and backward propagation for every `Trajectory`

in the `problem`

. The keyword arguments for each propagation (see `propagate`

) are determined from any properties of each `Trajectory`

that have a `prop_`

prefix, cf. `init_prop_trajectory`

.

In situations where different parameters are required for the forward and backward propagation, instead of the `prop_`

prefix, the `fw_prop_`

and `bw_prop_`

prefixes can be used, respectively. These override any setting with the `prop_`

prefix. This applies both to the properties of each `Trajectory`

and the problem keyword arguments.

Note that the propagation method for each propagation must be specified. In most cases, it is sufficient (and recommended) to pass a global `prop_method`

problem keyword argument.

## GRAPE

The Gradient Ascent Pulse Engineering (GRAPE) method is implemented in the `GRAPE`

package. See the `GRAPE`

documentation for details.

`QuantumControl.optimize`

— Method```
using GRAPE
result = optimize(problem; method=GRAPE, kwargs...)
```

optimizes the given control `problem`

via the GRAPE method, by minimizing the functional

\[J(\{ϵ_{nl}\}) = J_T(\{|ϕ_k(T)⟩\}) + λ_a J_a(\{ϵ_{nl}\})\]

where the final time functional $J_T$ depends explicitly on the forward-propagated states and the running cost $J_a$ depends explicitly on pulse values $ϵ_{nl}$ of the l'th control discretized on the n'th interval of the time grid.

Returns a `GrapeResult`

.

Keyword arguments that control the optimization are taken from the keyword arguments used in the instantiation of `problem`

; any of these can be overridden with explicit keyword arguments to `optimize`

.

**Required problem keyword arguments**

`J_T`

: A function`J_T(Ψ, trajectories)`

that evaluates the final time functional from a list`Ψ`

of forward-propagated states and`problem.trajectories`

. The function`J_T`

may also take a keyword argument`tau`

. If it does, a vector containing the complex overlaps of the target states (`target_state`

property of each trajectory in`problem.trajectories`

) with the propagated states will be passed to`J_T`

.

**Optional problem keyword arguments**

`chi`

: A function`chi(Ψ, trajectories)`

that receives a list`Ψ`

of the forward propagated states and returns a vector of states $|χₖ⟩ = -∂J_T/∂⟨Ψₖ|$. If not given, it will be automatically determined from`J_T`

via`make_chi`

with the default parameters. Similarly to`J_T`

, if`chi`

accepts a keyword argument`tau`

, it will be passed a vector of complex overlaps.`J_a`

: A function`J_a(pulsevals, tlist)`

that evaluates running costs over the pulse values, where`pulsevals`

are the vectorized values $ϵ_{nl}$, where`n`

are in indices of the time intervals and`l`

are the indices over the controls, i.e.,`[ϵ₁₁, ϵ₂₁, …, ϵ₁₂, ϵ₂₂, …]`

(the pulse values for each control are contiguous). If not given, the optimization will not include a running cost.`gradient_method=:gradgen`

: One of`:gradgen`

(default) or`:taylor`

. With`gradient_method=:gradgen`

, the gradient is calculated using QuantumGradientGenerators. With`gradient_method=:taylor`

, it is evaluated via a Taylor series, see Eq. (20) in Kuprov and Rogers, J. Chem. Phys. 131, 234108 (2009) [17].`taylor_grad_max_order=100`

: If given with`gradient_method=:taylor`

, the maximum number of terms in the Taylor series. If`taylor_grad_check_convergence=true`

(default), if the Taylor series does not convergence within the given number of terms, throw an an error. With`taylor_grad_check_convergence=true`

, this is the exact order of the Taylor series.`taylor_grad_tolerance=1e-16`

: If given with`gradient_method=:taylor`

and`taylor_grad_check_convergence=true`

, stop the Taylor series when the norm of the term falls below the given tolerance. Ignored if`taylor_grad_check_convergence=false`

.`taylor_grad_check_convergence=true`

: If given as`true`

(default), check the convergence after each term in the Taylor series an stop as soon as the norm of the term drops below the given number. If`false`

, stop after exactly`taylor_grad_max_order`

terms.`lambda_a=1`

: A weight for the running cost`J_a`

.`grad_J_a`

: A function to calculate the gradient of`J_a`

. If not given, it will be automatically determined. See`make_grad_J_a`

for the required interface.`upper_bound`

: An upper bound for the value of any optimized control. Time-dependent upper bounds can be specified via`pulse_options`

.`lower_bound`

: A lower bound for the value of any optimized control. Time-dependent lower bounds can be specified via`pulse_options`

.`pulse_options`

: A dictionary that maps every control (as obtained by`get_controls`

from the`problem.trajectories`

) to a dict with the following possible keys:`:upper_bounds`

: A vector of upper bound values, one for each intervals of the time grid. Values of`Inf`

indicate an unconstrained upper bound for that time interval, respectively the global`upper_bound`

, if given.`:lower_bounds`

: A vector of lower bound values. Values of`-Inf`

indicate an unconstrained lower bound for that time interval,

`print_iters=true`

: Whether to print information after each iteration.`store_iter_info=Set()`

: Which fields from`print_iters`

to store in`result.records`

. A subset of`Set(["iter.", "J_T", "|∇J_T|", "ΔJ_T", "FG(F)", "secs"])`

.`callback`

: A function (or tuple of functions) that receives the GRAPE workspace and the iteration number. The function may return a tuple of values which are stored in the`GrapeResult`

object`result.records`

. The function can also mutate the workspace, in particular the updated`pulsevals`

. This may be used, e.g., to apply a spectral filter to the updated pulses or to perform similar manipulations. Note that`print_iters=true`

(default) adds an automatic callback to print information after each iteration. With`store_iter_info`

, that callback automatically stores a subset of the printed information.`check_convergence`

: A function to check whether convergence has been reached. Receives a`GrapeResult`

object`result`

, and should set`result.converged`

to`true`

and`result.message`

to an appropriate string in case of convergence. Multiple convergence checks can be performed by chaining functions with`∘`

. The convergence check is performed after any`callback`

.`x_tol`

: Parameter for Optim.jl`f_tol`

: Parameter for Optim.jl`g_tol`

: Parameter for Optim.jl`show_trace`

: Parameter for Optim.jl`extended_trace`

: Parameter for Optim.jl`show_every`

: Parameter for Optim.jl`allow_f_increases`

: Parameter for Optim.jl`optimizer`

: An optional Optim.jl optimizer (`Optim.AbstractOptimizer`

instance). If not given, an L-BFGS-B optimizer will be used.`prop_method`

: The propagation method to use for each trajectory, see below.`verbose=false`

: If`true`

, print information during initialization`rethrow_exceptions`

: By default, any exception ends the optimization, but still returns a`GrapeResult`

that captures the message associated with the exception. This is to avoid losing results from a long-running optimization when an exception occurs in a later iteration. If`rethrow_exceptions=true`

, instead of capturing the exception, it will be thrown normally.

**Trajectory propagation**

GRAPE may involve three types of propagation:

- A forward propagation for every
`Trajectory`

in the`problem`

- A backward propagation for every trajectory
- A backward propagation of a gradient generator for every trajectory.

The keyword arguments for each propagation (see `propagate`

) are determined from any properties of each `Trajectory`

that have a `prop_`

prefix, cf. `init_prop_trajectory`

.

In situations where different parameters are required for the forward and backward propagation, instead of the `prop_`

prefix, the `fw_prop_`

and `bw_prop_`

prefix can be used, respectively. These override any setting with the `prop_`

prefix. Similarly, properties for the backward propagation of the gradient generators can be set with properties that have a `grad_prop_`

prefix. These prefixes apply both to the properties of each `Trajectory`

and the problem keyword arguments.

Note that the propagation method for each propagation must be specified. In most cases, it is sufficient (and recommended) to pass a global `prop_method`

problem keyword argument.