# Optimal Control

This section provides a brief introduction to the package's interface for sequence optimization. We use the Cramer-Rao bound (CRB) to assess a sequence's performance and optimize the amplitudes ($ω_1$) and durations ($T_\text{RF}$) of RF-pulses to reduce the CRB, assuming a Balanced Hybrid-State Free Precession Pulse Sequence. For computational efficiency, the derivatives of the CRB wrt. $ω_1$ and $T_\text{RF}$ are calculated with the adjoint state method common in the optimal control literature.

For this tutorial, we use the following packages:

```
using MRIgeneralizedBloch
using LinearAlgebra
BLAS.set_num_threads(1) # single threaded is faster in this case
using Optim # provides the optimization algorithm
using Plots
```

Here, we optimize the pulse sequence for a predefined set of parameters:

```
m0s = 0.15
R1f = 0.5 # 1/s
R2f = 15 # 1/s
Rx = 30 # 1/s
R1s = 3 # 1/s
T2s = 10e-6 # s
ω0 = 0 # rad/s
B1 = 1; # in units of B1_nominal
```

and we optimize

`Npulses = 200;`

pulses, spaced

`TR = 3.5e-3; # s`

apart. The cyle duration of

`Npulses * TR`

`0.7000000000000001`

seconds is shorter than the optimal duration, which is in the range of 4-10s. We here use a small `Npulses`

to speed up the computations. The Linear Approximation of the generalized Bloch model is precomputed with

`R2slT = precompute_R2sl();`

In the calculation of the CRB, we account for following gradients:

`grad_list = (grad_m0s(), grad_R1f(), grad_R2f(), grad_Rx(), grad_R1s(), grad_T2s(), grad_ω0(), grad_B1());`

and we sum up the CRB of all parameters, weighted by the following vector:

`weights = transpose([0, 1, 0, 0, 0, 0, 0, 0, 0]);`

Note that the vector `weights`

has one more entry compared to the `grad_list`

vector, as the first derivative is always wrt. $M_0$, regardless of `grad_list`

. Here, we only optimize for the CRB of $m_0^s$, while accounting for a fit of all 9 model parameters.

We take some initial guess for the pulse train:

`α = abs.(sin.((1:Npulses) * 2π/Npulses));`

initialize with a constant `TRF = 300μs`

:

`TRF = 300e-6 .* one.(α);`

and define the first RF pulse as a 500μs inversion pulse by modifying vectors accordingly and by creating a bit vector that indicates the position of the inversion pulse:

```
α[1] = π
TRF[1] = 500e-6
isInversionPulse = [true; falses(length(α)-1)];
```

We note that inversion pulses are not optimized by this toolbox. We calculate the initial $ω_1$

`ω1 = α ./ TRF;`

and plot the initial control:

```
p1 = plot(TR*(1:Npulses), α ./ π, ylabel="α/π")
p2 = plot(TR*(1:Npulses), 1e6TRF, ylim=(0, 1e3), xlabel="t (s)", ylabel="TRF (μs)")
p = plot(p1, p2, layout=(2, 1), legend=:none)
```

With above defined weights, the function `MRIgeneralizedBloch.CRB_gradient_OCT`

returns the CRB

```
(CRBm0s, grad_ω1, grad_TRF) = MRIgeneralizedBloch.CRB_gradient_OCT(ω1, TRF, TR, ω0, B1, m0s, R1f, R2f, Rx, R1s, T2s, R2slT, grad_list, weights, isInversionPulse=isInversionPulse)
CRBm0s
```

`6522.939062533227`

along with the gradients:

```
p1 = plot(TR*(1:Npulses), grad_ω1 .* ((-1) .^ (1:Npulses)), ylabel="∂CRB(m0s) / ∂ω1 (s/rad)")
p2 = plot(TR*(1:Npulses), grad_TRF .* ((-1) .^ (1:Npulses)), ylabel="∂CRB(m0s) / ∂TRF (1/s)", xlabel="t (s)")
p = plot(p1, p2, layout=(2, 1), legend=:none)
```

Note that we remove the oscillating nature of the gradient for the display.

In this example, we limit the control to the following bounds

```
ω1_min = 0 # rad/s
ω1_max = 2e3π # rad/s
TRF_min = 100e-6 # s
TRF_max = 500e-6; # s
```

and the function `MRIgeneralizedBloch.bound_ω1_TRF!`

modifies `ω1`

and `TRF`

to comply with these bounds and returns a single vector in the range `[-Inf, Inf]`

that relates to the bounded control by a `tanh`

transformation:

`x0 = MRIgeneralizedBloch.bound_ω1_TRF!(ω1, TRF; ω1_min = ω1_min, ω1_max = ω1_max, TRF_min = TRF_min, TRF_max = TRF_max)`

```
400-element Vector{Float64}:
Inf
-1.683988287115545
-1.4729988693565415
-1.3209392186669002
-1.2011921567736858
-1.1019049796994895
-1.0167581958717071
-0.9419826140711725
-0.8751499388454517
-0.814605017748036
⋮
-1.1102230246251565e-16
-1.1102230246251565e-16
-1.1102230246251565e-16
-1.1102230246251565e-16
-1.1102230246251565e-16
-1.1102230246251565e-16
-1.1102230246251565e-16
-1.1102230246251565e-16
-1.1102230246251565e-16
```

Further, we initialize a gradient of the same length:

`G = similar(x0);`

and define the cost function:

```
function fg!(F, G, x)
ω1, TRF = MRIgeneralizedBloch.get_bounded_ω1_TRF(x; ω1_min = ω1_min, ω1_max = ω1_max, TRF_min = TRF_min, TRF_max = TRF_max)
(F, grad_ω1, grad_TRF) = MRIgeneralizedBloch.CRB_gradient_OCT(ω1, TRF, TR, ω0, B1, m0s, R1f, R2f, Rx, R1s, T2s, R2slT, grad_list, weights, isInversionPulse=isInversionPulse)
F = abs(F)
F += MRIgeneralizedBloch.second_order_α!(grad_ω1, grad_TRF, ω1, TRF; idx = .!isInversionPulse, λ=1e4)
F += MRIgeneralizedBloch.RF_power!(grad_ω1, grad_TRF, ω1, TRF; idx = .!isInversionPulse, λ=1e-3, Pmax=3e6, TR=TR)
F += MRIgeneralizedBloch.TRF_TV!(grad_TRF, ω1, TRF; idx = .!isInversionPulse, λ=1e3)
MRIgeneralizedBloch.apply_bounds_to_grad!(G, x, grad_ω1, grad_TRF; ω1_min = ω1_min, ω1_max = ω1_max, TRF_min = TRF_min, TRF_max = TRF_max)
return F
end;
```

We perform the optimization with the package Optim.jl, which requires the cost function `fg!(F, G, x)`

to take the cost, the gradient, and the control as input variables and to over-write the gradient in place. The cost function calculates the gradient of the CRB with above described optimal control code and we, further, add some regularization terms: `MRIgeneralizedBloch.second_order_α!`

penalizes the curvature of α, which smoothes the flip angle train and helps ensuring the hybrid state conditions. The penalty `MRIgeneralizedBloch.RF_power!`

penalizes the power deposition of the RF-pulse train if $\Sigma_i(ω_1^2[i] ⋅ T_{\text{RF}}[i]) / T_{\text{cycle}} ≥ P_{\max}$ and helps with compliance to safety limits. Assuming a reasonable `λ`

, the optimization will converge to an average RF-power deposition equal to or less than `Pmax`

in units of (rad/s)². Heuristically, the value `Pmax=3e6`

(rad/s)² proofed to be a reasonable choice for 3T systems. The penalty `MRIgeneralizedBloch.TRF_TV!`

penalizes fast fluctuations of $T_\text{RF}$. This penalty is justified by the knowledge that fluctuations of the control have negligible effect if they are fast compared to the biophysical time constants. We note, however, that this penalty is not required and rather ensure *beauty* of the result and speeds up convergence.

With all this in place, we can start the actual optimization

```
result = optimize(Optim.only_fg!(fg!), # cost function
x0, # initialization
BFGS(), # algorithm
Optim.Options(
iterations=10_000, # larger number as we use a time limit
time_limit=(15*60) # in seconds
)
)
```

```
* Status: failure (exceeded time limit of 900.0)
* Candidate solution
Final objective value: 1.060121e+02
* Found with
Algorithm: BFGS
* Convergence measures
|x - x'| = 8.88e-16 ≰ 0.0e+00
|x - x'|/|x'| = 0.00e+00 ≤ 0.0e+00
|f(x) - f(x')| = 4.39e-11 ≰ 0.0e+00
|f(x) - f(x')|/|f(x')| = 4.14e-13 ≰ 0.0e+00
|g(x)| = 7.68e+00 ≰ 1.0e-08
* Work counters
Seconds run: 909 (vs limit 900)
Iterations: 907
f(x) calls: 7004
∇f(x) calls: 7004
```

After transforming the optimized control back into the space of bounded $ω_1$ and $T_\text{RF}$ values

```
ω1, TRF = MRIgeneralizedBloch.get_bounded_ω1_TRF(result.minimizer; ω1_min = ω1_min, ω1_max = ω1_max, TRF_min = TRF_min, TRF_max = TRF_max)
α = ω1 .* TRF;
```

we analyze the CRB(m0s):

```
(CRBm0s, grad_ω1, grad_TRF) = MRIgeneralizedBloch.CRB_gradient_OCT(ω1, TRF, TR, ω0, B1, m0s, R1f, R2f, Rx, R1s, T2s, R2slT, grad_list, weights, isInversionPulse=isInversionPulse)
CRBm0s
```

`78.59616770629515`

and observe a substantial reduction. Further, we plot the optimized control:

```
p1 = plot(TR*(1:Npulses), α ./ π, ylabel="α/π")
p2 = plot(TR*(1:Npulses), 1e6TRF, ylim=(0, 1e3), xlabel="t (s)", ylabel="TRF (μs)")
p = plot(p1, p2, layout=(2, 1), legend=:none)
```

To further analyze the results, we can calculate and plot all magnetization components:

```
m = calculatesignal_linearapprox(α, TRF, TR, ω0, B1, m0s, R1f, R2f, Rx, R1s, T2s, R2slT; output=:realmagnetization)
m = vec(m)
xf = [m[i][1] for i=1:length(m)]
yf = [m[i][2] for i=1:length(m)]
zf = [m[i][3] for i=1:length(m)]
xs = [m[i][4] for i=1:length(m)]
zs = [m[i][5] for i=1:length(m)]
p = plot(xlabel="t (s)", ylabel="m (normalized)")
plot!(p, TR*(1:Npulses), xf ./(1-m0s), label="xᶠ")
plot!(p, TR*(1:Npulses), yf ./(1-m0s), label="yᶠ")
plot!(p, TR*(1:Npulses), zf ./(1-m0s), label="zᶠ")
plot!(p, TR*(1:Npulses), xs ./ m0s , label="xˢ")
plot!(p, TR*(1:Npulses), zs ./ m0s , label="zˢ")
```

And we can also plot the dynamics of the free spin pool on the Bloch sphere:

`p = plot(xf, zf, xlabel="xf", ylabel="zf", framestyle = :zerolines, legend=:none)`

As yᶠ is close to zero in this particular case, we neglect it in this 2D plot.

*This page was generated using Literate.jl.*