Indirect method
We introduce the pseudo-Hamiltonian
\[ h(x,p,p^0,u) = p^0 x + p u.\]
For the sake of simplicity, we assume that the BC-extremals associated to the solution of the studied problem is normal, and so we fix $p^0 = -1$. According to the Pontryagin maximum principle, the maximizing control is given by $u(x,p) \to \mathrm{sign}(p)$. This function is non-differentiable, and may lead to numerical issues.
Let's start by defining the problem.
using OptimalControl
using Plots
using ForwardDiff
using DifferentialEquations
using MINPACK
t0 = 0 # initial time
x0 = 0 # initial state
tf = 5 # final time
xf = 0 # final state
@def ocp begin # problem definition
t ∈ [ t0, tf ], time
x ∈ R, state
u ∈ R, control
x(t0) == x0
x(tf) == xf
ẋ(t) == u(t)
∫( x(t) ) → min
end
Thanks to the control-toolbox, the flow $\varphi$ of the (true) Hamiltonian
\[ H(x,p) = h(x,p,-1, u(x,p)) = p^0 x + \lvert p \rvert \]
is given by the function $\texttt{Flow}$. The shooting function $S \colon \mathbb{R} \to \mathbb{R}$ is defined by
\[ S(p_0) = \pi \big( \varphi(t_0, x_0, p_0, t_f) \big) - x_f\]
where $\pi (x,p) = x$ is the classical $x$-space projection.
ϕ = Flow(ocp, (x,p) -> sign(p)) # flow with maximizing control
π((x,p)) = x; # projection on state space
S(p0) = π( ϕ(t0, x0, p0, tf) ) - xf; # shooting function
nle = p0 -> [S(p0[1])] # auxiliary function
# Plot
plot(range(-7, 2, 500), S, xlim = [-7, 2])
plot!([-7,2], [0,0], color = :black)
plot!(xlabel = "p0", ylabel = "S", legend=false)
Finite difference method
The main goal now is to find the zero of $S$. To this purpose, we use the numerical solver $\texttt{hybrd1}$ given in the package $\texttt{MINPACK.jl}$. If we don't provide the Jacobian $J_S$ of $S$ to the solver, the finite difference method is used to approximate it.
ξ = [-1.0] # initial guess
S!(s, ξ) = (s[:] .= S(ξ[1]); nothing) # auxiliary function
p0_sol = fsolve(S!, ξ, show_trace = true) # solve
println(p0_sol)
Iter f(x) inf-norm Step 2-norm Step time
------ -------------- -------------- --------------
1 3.000000e+00 0.000000e+00 0.263597
2 3.077457e+00 9.233870e+00 0.217593
3 9.829505e-09 2.367685e+00 0.000455
4 6.491184e-09 2.415479e-17 0.000424
5 5.237606e-08 9.132615e-17 0.000455
6 5.775412e-09 7.229585e-17 0.000405
7 5.237605e-08 7.229558e-17 0.000462
8 5.201817e-09 5.864837e-17 0.000398
9 5.237605e-08 5.864847e-17 0.000455
10 4.731865e-09 4.853008e-17 0.000397
11 5.237606e-08 4.853029e-17 0.000475
12 4.339792e-09 4.082120e-17 0.000415
13 5.237607e-08 4.082138e-17 0.000457
14 4.007716e-09 3.481324e-17 0.000395
15 5.237604e-08 3.481267e-17 0.000434
16 3.722853e-09 3.003963e-17 0.000404
17 5.237606e-08 3.004001e-17 0.000441
18 3.475794e-09 2.618526e-17 0.000406
19 5.237604e-08 2.618500e-17 0.000445
20 3.259488e-09 2.302731e-17 0.000392
21 5.237605e-08 2.302742e-17 0.000445
22 3.068525e-09 2.040827e-17 0.000389
23 5.237604e-08 2.040814e-17 0.000443
24 2.898702e-09 1.821171e-17 0.000392
25 5.237605e-08 1.821185e-17 0.000449
26 2.746691e-09 1.635181e-17 0.000393
27 5.237610e-08 1.635236e-17 0.000437
28 2.609823e-09 1.476333e-17 0.000404
29 2.090234e-09 5.851517e-19 0.000741
30 5.237606e-08 9.469727e-18 0.000461
31 2.010020e-09 8.756841e-18 0.000437
32 1.609840e-09 3.470936e-19 0.000769
33 5.237604e-08 5.616972e-18 0.000516
34 1.561837e-09 5.286974e-18 0.000435
35 1.250892e-09 2.095646e-19 0.000780
36 5.237606e-08 3.391488e-18 0.000447
37 1.221712e-09 3.235115e-18 0.000417
38 9.784849e-10 1.282284e-19 0.000745
39 5.237607e-08 2.075235e-18 0.000443
40 9.605377e-10 1.999817e-18 0.000392
41 3.304990e-08 7.926384e-20 0.000785
42 9.551371e-10 7.484981e-20 0.000412
43 5.237611e-08 1.977570e-18 0.000469
44 9.380301e-10 1.907369e-18 0.000399
45 5.237606e-08 1.907158e-18 0.000457
46 3.289768e-08 1.840638e-18 0.000440
Results of Nonlinear Solver Algorithm
* Algorithm: Modified Powell
* Starting Point: [-1.0]
* Zero: [-2.499999988327954]
* Inf-norm of residuals: 0.000000
* Convergence: true
* Message: algorithm estimates that the relative error between x and the solution is at most tol
* Total time: 0.501815 seconds
* Function Calls: 46
* Jacobian Calls (df/dx): 6
sol = ϕ((t0, tf), x0, p0_sol.x) # get the optimal trajectory
plot(sol) # plot
Automatic differentiation (wrong way)
Now, we want to provide $J_S$ to the solver, thanks to the $\texttt{ForwardDiff.jl}$ package. This Jacobian is computed with the variational equation, and leads to a false result in our case.
ξ = [-1.0] # initial guess
JS(ξ) = ForwardDiff.jacobian(p -> [S(p[1])], ξ) # compute jacobian by forward differentiation
println("ξ = ", ξ[1])
println("JS(ξ) : ", JS(ξ)[1])
ξ = -1.0
JS(ξ) : 0.0
However, the solver $\texttt{hybrd1}$ uses rank 1 approximations to actualize the Jacobian insted of compute it at each iteration, which imply that it still converges to the solution even if the given Jacobian is completely false.
JS!(js, ξ) = (js[:] .= JS(ξ); nothing) # auxiliary function
p0_sol = fsolve(S!, JS!, ξ, show_trace = true) # solve
println(p0_sol)
Iter f(x) inf-norm Step 2-norm Step time
------ -------------- -------------- --------------
1 3.000000e+00 0.000000e+00 0.007267
2 5.000000e+00 1.000000e+04 0.057913
3 5.000000e+00 3.906250e+03 0.000141
4 5.000000e+00 3.906250e+03 0.000721
5 5.000000e+00 3.906250e+03 0.000080
6 5.000000e+00 5.493164e+02 0.000066
7 5.000000e+00 7.724762e+01 0.000065
8 9.550782e-01 1.086295e+01 0.000506
9 5.144214e-08 2.280436e-01 0.000348
10 3.266365e-09 6.615735e-16 0.000412
11 2.081820e-09 3.041236e-18 0.000401
12 5.237605e-08 9.393622e-18 0.000443
13 2.002238e-09 8.689149e-18 0.000414
Results of Nonlinear Solver Algorithm
* Algorithm: Modified Powell (User Jac, Expert)
* Starting Point: [-1.0]
* Zero: [-2.499999989894703]
* Inf-norm of residuals: 0.000000
* Convergence: true
* Message: algorithm estimates that the relative error between x and the solution is at most tol
* Total time: 0.068793 seconds
* Function Calls: 13
* Jacobian Calls (df/dx): 2
sol = ϕ((t0, tf), x0, p0_sol.x) # get the optimal trajectory
plt = plot(sol) # plot
Automatic differentiation (good way)
The goal is to provide the true Jacobian of $S$ by using the $\texttt{ForwardDiff}$ package, and so we need to indicate to the solver that the dynamic of the system change when $p = 0$.
To understand why we need to give this information to the solver, see the following details.
To provide this change of dynamic to the solver, we need to use a callback during the integration that will execute the function $\texttt{affect!}$ when $\texttt{condition(x,p)} = 0$.
For us, the condition is given by $(x,p) \to p$. For the $\texttt{affect!}$ function, we use a global parameter $\alpha$. This parameter will be set to $\pm 1$ at the beginning of the integration and it sign will change with the $\texttt{affect!}$ function.
Thanks to the $\texttt{control-toolbox}$ package, the created callback can be easily pass to the integrator throught the $\texttt{Flow}$ function.
global α # parameter: ̇p(t) = α with α = ±1
function condition(z, t, integrator) # event when condition(x,p) == 0
x,p = z
return p
end
function affect!(integrator) # action when condition == 0
global α = -α
nothing
end
cb = ContinuousCallback(condition, affect!) # callback
φ_ = Flow(ocp, (x,p) -> α, callback = cb) # intermediate flow
function φ(t0, x0, p0, tf; kwargs...) # flow
global α = sign(p0)
return φ_(t0, x0, p0, tf; kwargs...)
end
function φ((t0, tf), x0, p0; kwargs...) # flow for plot
global α = sign(p0)
return φ_((t0, tf), x0, p0; kwargs...)
end
Shoot(p0) = π( φ(t0, x0, p0, tf) ) - xf # shooting function
ξ = [-1.0] # initial guess
JShoot(ξ) = ForwardDiff.jacobian(p -> [Shoot(p[1])], ξ) # compute jacobian by forward differentiation
println("ξ = ", ξ[1])
println("JS(ξ) : ", JShoot(ξ)[1])
Shoot!(shoot, ξ) = (shoot[:] .= Shoot(ξ[1]); nothing) # auxiliary function
JShoot!(jshoot, ξ) = (jshoot[:] .= JShoot(ξ); nothing) # auxiliary function
p0_sol = fsolve(Shoot!, JShoot!, ξ, show_trace = true) # solve
println(p0_sol)
ξ = -1.0
JS(ξ) : 2.0
Iter f(x) inf-norm Step 2-norm Step time
------ -------------- -------------- --------------
1 3.000000e+00 0.000000e+00 3.724867
2 2.553513e-15 2.250000e+00 0.008959
3 7.771561e-16 1.774937e-30 0.000158
Results of Nonlinear Solver Algorithm
* Algorithm: Modified Powell (User Jac, Expert)
* Starting Point: [-1.0]
* Zero: [-2.5000000000000013]
* Inf-norm of residuals: 0.000000
* Convergence: true
* Message: algorithm estimates that the relative error between x and the solution is at most tol
* Total time: 3.734005 seconds
* Function Calls: 3
* Jacobian Calls (df/dx): 1
# get optimal trajectory
sol = φ((t0, tf), x0, p0_sol.x[1], saveat=range(t0, tf, 500))
# plot
t = time_grid(sol)
x = state(sol)
p = costate(sol)
u = sign ∘ p
plt_x = plot(t, x, label = "x")
plt_p = plot(t, p, label = "p")
plt_u = plot(t, u, label = "u")
plt_xp = plot(plt_x, plt_p, layout=(1, 2))
plot(plt_xp, plt_u, layout = (2, 1))