Shooting
The maximum principle gives the extremals, but not the initial costate, the switching times or a free final time. Shooting finds them: write the conditions the extremal must satisfy as equations in these unknowns, integrate the flows to evaluate them, and solve the equations with a nonlinear solver. The direct method gives the starting point.
using OptimalControl
using OrdinaryDiffEqTsit5
using NonlinearSolve
using NLPModelsIpoptThe problem
We take the minimum-time transfer of the double integrator: minimise
t0 = 0
x0 = [-1, 0]
xf = [0, 0]
ocp = @def begin
tf ∈ R, variable
t ∈ [0, tf], time
x = (q, v) ∈ R², state
u ∈ R, control
-1 ≤ u(t) ≤ 1
x(0) == x0, (start)
x(tf) == xf
ẋ(t) == [v(t), u(t)]
tf → min
endWith
f_max = Flow(ocp, (x, p, v) -> 1)
f_min = Flow(ocp, (x, p, v) -> -1)The shooting function
There are four unknowns,
the target,
(two equations);the switch: the switching function vanishes at
, ;the transversality condition of the free final time: for the Mayer cost
, the maximised Hamiltonian equals at, that is (see Notation and conventions).
The shooting function integrates the two arcs and returns the four residuals. It is written in place, as NonlinearSolve expects:
H(x, p, u) = p[1] * x[2] + p[2] * u # p ⋅ f(x, u)
function shoot!(s, ξ, _)
p0, t1, tf = ξ[1:2], ξ[3], ξ[4]
x1, p1 = f_max(t0, x0, p0, t1; variable=tf, unsafe=true)
x2, p2 = f_min(t1, x1, p1, tf; variable=tf, unsafe=true)
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
endAt the solution, the residuals vanish:
s = zeros(4)
shoot!(s, [1, 1, 1, 2], nothing)
s4-element Vector{Float64}:
-2.313802281140895e-16
-3.425505599574657e-17
1.8158145930425736e-16
-3.3306690738754696e-16unsafe=true
While it searches, a nonlinear solver tries unknowns that are far from the solution, and the integration can fail on them. By default a flow then raises an error, which would stop the search. With unsafe=true, the flow returns what the integrator computed instead, and the residual is just large. On
julia> f_blowup = Flow(VectorField(x -> x^2));
julia> f_blowup(0, 10.0, 1)
SolverFailure → #67, REPL[2]:3
│
│ ODE integration failed
│
│ Retcode Unstable
│
│ Context SciML solve
│ Hint Try tightening tolerances (reltol, abstol) or changing the solver algorithm.
└─f_blowup(0, 10.0, 1; unsafe=true)7.141089928711383e15This is why shoot! above calls the flows with unsafe=true.
Starting from the direct solution
A shooting method converges from a good initial guess. The direct method provides one:
the initial costate is the multiplier of the initial condition,
dual(sol, ocp, :start)(more accurate thancostate(sol)(0), see Solution object);the switching time is read off the control, where it changes sign;
the final time is the variable.
sol_d = solve(ocp; display=false)
p0_guess = dual(sol_d, ocp, :start)
tg = time_grid(sol_d)
t1_guess = tg[findfirst(t -> control(sol_d)(t) < 0, tg)]
tf_guess = variable(sol_d)
ξ_guess = [p0_guess; t1_guess; tf_guess]4-element Vector{Float64}:
0.9999999963782983
0.9982579851391655
0.9999999963782911
1.9999999927565821Solving the shooting equations
prob = NonlinearProblem(shoot!, ξ_guess)
sol = NonlinearSolve.solve(prob, SimpleNewtonRaphson(); abstol=1e-10, reltol=1e-10)
sol.u, sol.retcode([1.0, 1.0, 1.0000000000000002, 2.0000000000000004], SciMLBase.ReturnCode.Success)The solver finds
Checking the solution
The two arcs, concatenated at the switching time found, reach the target. The point call of a concatenation returns [x; p] (see Multi-phase flows):
p0_s, t1_s, tf_s = sol.u[1:2], sol.u[3], sol.u[4]
f_bb = f_max * (t1_s, f_min)
zf = f_bb(t0, x0, p0_s, tf_s; variable=tf_s)
zf[1:2]2-element Vector{Float64}:
-8.891964458725137e-17
-3.218871821797339e-17The final time is the cost, so the indirect and the direct solutions agree when their final times do:
tf_s, objective(sol_d)(2.0000000000000004, 1.9999999927565821)More switches, other unknowns
Each additional switch adds an unknown time and a switching condition; a boundary arc adds its entry and exit times, with the junction conditions (see Constrained arcs). When the variable enters the dynamics or the cost, its transversality condition uses the costate of the variable, computed with variable_costate=true (see From an OCP). The examples solve several such problems, for instance Turnpike (bang–singular–bang), with two switching times around a singular arc.
See also
From an OCP: the flows a shooting function is made of.
Multi-phase flows: concatenations of arcs.
Solve overview: the direct method used for the initial guess.
Time minimisation (bang–bang): this example in full.