Time minimisation (bang–bang)
The wagon of Energy minimisation again, with a bounded force this time,
with the limit conditions
using OptimalControl
using NLPModelsIpopt
using OrdinaryDiffEqTsit5
using NonlinearSolve
using PlotsThe problem
The final time is a variable of the problem, and the cost:
t0 = 0
x0, xf = [-1, 0], [0, 0]
ocp = @def begin
tf ∈ R, variable
t ∈ [t0, tf], time
x = (q, v) ∈ R², state
u ∈ R, control
-1 ≤ u(t) ≤ 1
x(t0) == x0
x(tf) == xf
ẋ(t) == [v(t), u(t)]
tf → min
endDirect method
We solve it with 200 time steps:
direct_sol = solve(ocp; grid_size=200, display=false)
variable(direct_sol)1.9999999920052862plt = plot(direct_sol; label="direct", size=(800, 600))The control is
The exact solution
With the conventions of Notation and conventions, the cost
where
On
The switching condition
Indirect method
The two arcs have their flows, with the control variable=tf:
f_max = Flow(ocp, (x, p, v) -> 1)
f_min = Flow(ocp, (x, p, v) -> -1)The shooting function has four unknowns,
H(x, p, u) = p[1] * x[2] + p[2] * u # pseudo-Hamiltonian, Mayer form
function shoot!(s, ξ, _)
p0, t1, tf = ξ[1:2], ξ[3], ξ[4]
x1, p1 = f_max(t0, x0, p0, t1; variable=tf)
x2, p2 = f_min(t1, x1, p1, tf; variable=tf)
s[1:2] = x2 - xf # target
s[3] = p1[2] # switching function at t1
s[4] = H(x2, p2, -1) - 1 # transversality, free final time
return nothing
endThe direct solution gives the initial guess: its costate at
tg = time_grid(direct_sol)
p_d = costate(direct_sol)
p0_guess = p_d(t0)
t1_guess = tg[findfirst(t -> p_d(t)[2] < 0, tg)]
tf_guess = variable(direct_sol)
ξ_guess = [p0_guess..., t1_guess, tf_guess]4-element Vector{Float64}:
0.999999996002643
0.993999298650952
0.9999999960026431
1.9999999920052862We solve the shooting equations:
prob = NonlinearProblem(shoot!, ξ_guess)
shooting_sol = NonlinearSolve.solve(prob; show_trace=Val(false))
p0_sol, t1_sol, tf_sol = shooting_sol.u[1:2], shooting_sol.u[3], shooting_sol.u[4]([1.0, 1.0], 1.0000000000000002, 2.0000000000000004)These are the exact values,
s = zeros(4)
shoot!(s, shooting_sol.u, nothing)
s4-element Vector{Float64}:
-8.891964458725137e-17
-3.218871821797339e-17
-4.0463145620773896e-17
0.0Comparison
The concatenation of the two flows at
φ = f_max * (t1_sol, f_min)
indirect_sol = φ((t0, tf_sol), x0, p0_sol; variable=tf_sol)
state(indirect_sol)(tf_sol)2-element Vector{Float64}:
-8.891964458725137e-17
-3.218871821797339e-17For the plot, the flows are built with saveat, so that the trajectory has 201 points (see Plotting a flow):
fine = (saveat=range(t0, tf_sol, 201), dense=false)
φ_plot = Flow(ocp, (x, p, v) -> 1; fine...) * (t1_sol, Flow(ocp, (x, p, v) -> -1; fine...))
plot!(plt, φ_plot((t0, tf_sol), x0, p0_sol; variable=tf_sol); label="indirect", linestyle=:dash)The two final times agree up to the discretisation error of the direct method:
variable(direct_sol), tf_sol(1.9999999920052862, 2.0000000000000004)Notes
Shooting solves this problem step by step, starting from the multipliers of the direct solution.
MINPACK.jl can replace NonlinearSolve to solve the shooting equations.
The Goddard tutorial uses the same steps on a problem with bang, singular and boundary arcs.