In this tutorial we present the indirect simple shooting method on a simple example.
Let us start by importing the necessary packages.
using OptimalControl # to define the optimal control problem and its flow
using OrdinaryDiffEq # to get the Flow function from OptimalControl
using NonlinearSolve # interface to NLE solvers
using MINPACK # NLE solver: use to solve the shooting equation
using Plots # to plot the solution
Let us consider the following optimal control problem:
with
const t0 = 0
const tf = 1
const x0 = -1
const xf = 0
const α = 1.5
ocp = @def begin
t ∈ [t0, tf], time
x ∈ R, state
u ∈ R, control
x(t0) == x0
x(tf) == xf
ẋ(t) == -x(t) + α * x(t)^2 + u(t)
∫( 0.5u(t)^2 ) → min
end
nothing # hide
The pseudo-Hamiltonian of this problem is
where
since
where
!!! note "Our goal"
Our goal is to solve this (BVP). Solving (BVP) consists in solving the Pontryagin Maximum Principle which provides necessary conditions of optimality.
To achive our goal, let us first introduce the pseudo-Hamiltonian vector field
and then denote by
Our goal becomes to solve
where
u(x, p) = p
φ = Flow(ocp, u)
nothing # hide
We define also the projection function on the state space.
π((x, p)) = x
nothing # hide
!!! note "Nota bene"
Actually, $\varphi_{t_0, x_0, p_0}(\cdot)$ is also solution of
```math
\dot{z}(t) = \vec{\mathbf{H}}(z(t)), \quad z(t_0) = (x_0, p_0),
```
where $\mathbf{H}(z) = H(z, u(z))$ and $\vec{\mathbf{H}} = (\nabla_p \mathbf{H}, -\nabla_x \mathbf{H})$. This is what is actually computed by `Flow`.
Now, to solve the (BVP) we introduce the shooting function:
S(p0) = π( φ(t0, x0, p0, tf) ) - xf # shooting function
nothing # hide
At the end, solving (BVP) is equivalent to solve
ξ = [0.1] # initial guess
nothing # hide
We can use NonlinearSolve.jl package or, instead, MINPACK.jl to solve the shooting equation. To compute the Jacobian of the shooting function we use DifferentiationInterface.jl with ForwardDiff.jl backend.
using MINPACK
function fsolve(f, j, x; kwargs...)
try
MINPACK.fsolve(f, j, x; kwargs...)
catch e
println("Erreur using MINPACK")
println(e)
println("hybrj not supported. Replaced by hybrd even if it is not visible on the doc.")
MINPACK.fsolve(f, x; kwargs...)
end
end
using DifferentiationInterface
import ForwardDiff
backend = AutoForwardDiff()
nothing # hide
Let us define the problem to solve.
nle! = ( s, ξ) -> s[1] = S(ξ[1]) # auxiliary function
jnle! = (js, ξ) -> jacobian!(nle!, similar(ξ), js, backend, ξ) # Jacobian of nle
nothing # hide
We are now in position to solve the problem with the hybrj solver from MINPACK.jl through the fsolve function, providing the Jacobian.
Let us solve the problem and retrieve the initial costate solution.
indirect_sol = fsolve(nle!, jnle!, ξ; show_trace=true) # resolution of S(p0) = 0
p0_sol = indirect_sol.x[1] # costate solution
println("\ncostate: p0 = ", p0_sol)
println("shoot: |S(p0)| = ", abs(S(p0_sol)), "\n")
The solution can be plot calling first the flow.
sol = φ((t0, tf), x0, p0_sol)
plot(sol)
In the indirect shooting method, the research of the optimal control is replaced by the computation of its associated extremal. This computation is equivalent to finding the initial covector solution to the shooting function. Let us plot the extremal in the phase space and the shooting function with the solution.
<article class="docstring">
<header>
<a class="docstring-article-toggle-button fa-solid fa-chevron-right" href="javascript:;" title="Expand docstring"> </a>
<code>pretty_plot</code> — <span class="docstring-category">Function</span>
</header>
<section style="display: none;"><div><pre><code class="language-julia hljs">using Plots.PlotMeasures
function pretty_plot(S, p0; Np0=20, kwargs...)
# times for wavefronts
times = range(t0, tf, length=3)
# times for trajectories
tspan = range(t0, tf, length=100)
# interval of initial covector
p0_min = -0.5
p0_max = 2
# covector solution
p0_sol = p0
# plot of the flow in phase space
plt_flow = plot()
p0s = range(p0_min, p0_max, length=Np0)
for i ∈ eachindex(p0s)
sol = φ((t0, tf), x0, p0s[i])
x = state(sol).(tspan)
p = costate(sol).(tspan)
label = i==1 ? "extremals" : false
plot!(plt_flow, x, p, color=:blue, label=label)
end
# plot of wavefronts in phase space
p0s = range(p0_min, p0_max, length=200)
xs = zeros(length(p0s), length(times))
ps = zeros(length(p0s), length(times))
for i ∈ eachindex(p0s)
sol = φ((t0, tf), x0, p0s[i], saveat=times)
xs[i, :] .= state(sol).(times)
ps[i, :] .= costate(sol).(times)
end
for j ∈ eachindex(times)
label = j==1 ? "flow at times" : false
plot!(plt_flow, xs[:, j], ps[:, j], color=:green, linewidth=2, label=label)
end
#
plot!(plt_flow, xlims=(-1.1, 1), ylims=(p0_min, p0_max))
plot!(plt_flow, [0, 0], [p0_min, p0_max], color=:black, xlabel="x", ylabel="p", label="x=xf")
# solution
sol = φ((t0, tf), x0, p0_sol)
x = state(sol).(tspan)
p = costate(sol).(tspan)
plot!(plt_flow, x, p, color=:red, linewidth=2, label="extremal solution")
plot!(plt_flow, [x[end]], [p[end]], seriestype=:scatter, color=:green, label=false)
# plot of the shooting function
p0s = range(p0_min, p0_max, length=200)
plt_shoot = plot(xlims=(p0_min, p0_max), ylims=(-2, 4), xlabel="p₀", ylabel="y")
plot!(plt_shoot, p0s, S, linewidth=2, label="S(p₀)", color=:green)
plot!(plt_shoot, [p0_min, p0_max], [0, 0], color=:black, label="y=0")
plot!(plt_shoot, [p0_sol, p0_sol], [-2, 0], color=:black, label="p₀ solution", linestyle=:dash)
plot!(plt_shoot, [p0_sol], [0], seriestype=:scatter, color=:green, label=false)
# final plot
plot(plt_flow, plt_shoot; layout=(1,2), leftmargin=15px, bottommargin=15px, kwargs...)
end</code><button class="copy-button fa-solid fa-copy" aria-label="Copy this code ;opblock" title="Copy"></button></pre></div>
</section>
</article>
using Plots.PlotMeasures # hide
function pretty_plot(S, p0; Np0=20, kwargs...) # hide
# hide
# times for wavefronts# hide
times = range(t0, tf, length=3)# hide
# hide
# times for trajectories# hide
tspan = range(t0, tf, length=100)# hide
# hide
# interval of initial covector# hide
p0_min = -0.5 # hide
p0_max = 2 # hide
# hide
# covector solution# hide
p0_sol = p0 # hide
# hide
# plot of the flow in phase space# hide
plt_flow = plot() # hide
p0s = range(p0_min, p0_max, length=Np0) # hide
for i ∈ eachindex(p0s) # hide
sol = φ((t0, tf), x0, p0s[i])# hide
x = state(sol).(tspan)# hide
p = costate(sol).(tspan)# hide
label = i==1 ? "extremals" : false # hide
plot!(plt_flow, x, p, color=:blue, label=label) # hide
end # hide
# hide
# plot of wavefronts in phase space # hide
p0s = range(p0_min, p0_max, length=200) # hide
xs = zeros(length(p0s), length(times)) # hide
ps = zeros(length(p0s), length(times)) # hide
for i ∈ eachindex(p0s) # hide
sol = φ((t0, tf), x0, p0s[i], saveat=times)# hide
xs[i, :] .= state(sol).(times) # hide
ps[i, :] .= costate(sol).(times) # hide
end # hide
for j ∈ eachindex(times) # hide
label = j==1 ? "flow at times" : false # hide
plot!(plt_flow, xs[:, j], ps[:, j], color=:green, linewidth=2, label=label) # hide
end # hide
# hide
# # hide
plot!(plt_flow, xlims=(-1.1, 1), ylims=(p0_min, p0_max)) # hide
plot!(plt_flow, [0, 0], [p0_min, p0_max], color=:black, xlabel="x", ylabel="p", label="x=xf") # hide
# hide
# solution # hide
sol = φ((t0, tf), x0, p0_sol)# hide
x = state(sol).(tspan)# hide
p = costate(sol).(tspan)# hide
plot!(plt_flow, x, p, color=:red, linewidth=2, label="extremal solution") # hide
plot!(plt_flow, [x[end]], [p[end]], seriestype=:scatter, color=:green, label=false) # hide
# hide
# plot of the shooting function # hide
p0s = range(p0_min, p0_max, length=200) # hide
plt_shoot = plot(xlims=(p0_min, p0_max), ylims=(-2, 4), xlabel="p₀", ylabel="y") # hide
plot!(plt_shoot, p0s, S, linewidth=2, label="S(p₀)", color=:green) # hide
plot!(plt_shoot, [p0_min, p0_max], [0, 0], color=:black, label="y=0") # hide
plot!(plt_shoot, [p0_sol, p0_sol], [-2, 0], color=:black, label="p₀ solution", linestyle=:dash) # hide
plot!(plt_shoot, [p0_sol], [0], seriestype=:scatter, color=:green, label=false) # hide
# hide
# final plot # hide
plot(plt_flow, plt_shoot; layout=(1,2), leftmargin=15px, bottommargin=15px, kwargs...) # hide
# hide
end# hide
nothing # hide
pretty_plot(S, p0_sol; size=(800, 450))