Turnpike (bang–singular–bang)
A scalar system,
using OptimalControl
using NLPModelsIpopt
using OrdinaryDiffEqTsit5
using NonlinearSolve
using PlotsThe problem
The horizon is fixed,
t0, tf = 0.0, 2.0
x0, xf = 1.0, 0.5
ocp = @def begin
t ∈ [t0, tf], time
x ∈ R, state
u ∈ R, control
-1 ≤ u(t) ≤ 1
x(t0) == x0
x(tf) == xf
ẋ(t) == u(t)
∫(x(t)^2) → min
endDirect method
direct_sol = solve(ocp; grid_size=100, display=false)
plt = plot(direct_sol, :state, :control; label="direct", size=(800, 400))The state goes down to the origin as fast as it can, stays there, then climbs to the target. The flat middle part, the turnpike, is the singular arc, where the control is
The singular control
With
It is linear in
The extremal is therefore bang–singular–bang:
| Arc | Interval | ||
|---|---|---|---|
| bang | from | ||
| singular | |||
| bang | from |
The state decreases at unit rate from
Indirect method
Each arc has its flow, with a constant control:
f_minus = Flow(ocp, (x, p) -> -1)
f_sing = Flow(ocp, (x, p) -> 0)
f_plus = Flow(ocp, (x, p) -> 1)The unknowns are the initial costate
function shoot!(s, ξ, _)
p0, t1, t2 = ξ
x1, p1 = f_minus(t0, x0, p0, t1)
x2, p2 = f_sing(t1, x1, p1, t2)
x3, _ = f_plus(t2, x2, p2, tf)
s[1] = x1 # enter the singular arc at x = 0
s[2] = p1 # where the switching function vanishes
s[3] = x3 - xf # reach the target
return nothing
endThe direct solution gives the initial guess. The singular arc is where its switching function, the costate, stays close to zero, as in the Goddard tutorial:
tg = time_grid(direct_sol)
p_d = costate(direct_sol)
t12 = tg[abs.(p_d.(tg)) .≤ 1e-3] # the grid points on the singular arc
ξ_guess = [p_d(t0), minimum(t12), maximum(t12)]3-element Vector{Float64}:
-0.9801999456479715
0.96
1.52prob = NonlinearProblem(shoot!, ξ_guess)
shooting_sol = NonlinearSolve.solve(prob; show_trace=Val(false))
p0_sol, t1_sol, t2_sol = shooting_sol.u3-element Vector{Float64}:
-1.0000000000000036
1.0000000000000002
1.4999999999999998The solver finds the values computed by hand,
s = zeros(3)
shoot!(s, shooting_sol.u, nothing)
s3-element Vector{Float64}:
8.034495536742733e-17
4.696458266519542e-16
2.220446049250313e-16Comparison
The concatenation of the three flows at the switching times gives the whole extremal, and its cost is
φ = f_minus * (t1_sol, f_sing) * (t2_sol, f_plus)
indirect_sol = φ((t0, tf), x0, p0_sol)
objective(direct_sol), objective(indirect_sol)(0.3749500107307793, 0.3749998537000679)For the plot, the flows are built with saveat, so that the trajectory has 201 points (see Plotting a flow):
fine = (saveat=range(t0, tf, 201), dense=false)
φ_plot = Flow(ocp, (x, p) -> -1; fine...) *
(t1_sol, Flow(ocp, (x, p) -> 0; fine...)) *
(t2_sol, Flow(ocp, (x, p) -> 1; fine...))
plot!(plt, φ_plot((t0, tf), x0, p0_sol), :state, :control; label="indirect", linestyle=:dash)See also
Singular control: a system with three states, where the singular control needs Poisson brackets.
Shooting: the shooting method, with switching times as unknowns.
Time minimisation (bang–bang): bang arcs, without a singular arc between them.
Multi-phase flows: the concatenation of flows.
The Goddard tutorial: the same workflow on a harder problem, with a singular arc and a boundary arc.