Singular control
A vehicle in the plane with drift, time-optimal, whose extremal control is neither
using OptimalControl
using NLPModelsIpopt
using OrdinaryDiffEqTsit5
using NonlinearSolve
using PlotsThe problem
State
Definition
ocp = @def begin
tf ∈ R, variable
t ∈ [0, tf], time
q = (x, y, θ) ∈ R³, state
u ∈ R, control
-1 ≤ u(t) ≤ 1
-π / 2 ≤ θ(t) ≤ π / 2 # helps direct convergence
x(0) == 0
y(0) == 0
x(tf) == 1
y(tf) == 0
∂(q)(t) == [cos(θ(t)), sin(θ(t)) + x(t), u(t)]
tf → min
endAbstract definition:
tf ∈ R, variable
t ∈ [0, tf], time
q = ((x, y, θ) ∈ R³, state)
u ∈ R, control
-1 ≤ u(t) ≤ 1
-π / 2 ≤ θ(t) ≤ π / 2
x(0) == 0
y(0) == 0
x(tf) == 1
y(tf) == 0
(∂(q))(t) == [cos(θ(t)), sin(θ(t)) + x(t), u(t)]
tf → min
The (autonomous) optimal control problem is of the form:
minimize J(q, u, tf) = g(q(0), q(tf), tf)
subject to
q̇(t) = f(q(t), u(t), tf), t in [0, tf] a.e.,
ϕ₋ ≤ ϕ(q(0), q(tf), tf) ≤ ϕ₊,
x₋ ≤ q(t) ≤ x₊,
u₋ ≤ u(t) ≤ u₊,
where q(t) = (x(t), y(t), θ(t)) ∈ R³, u(t) ∈ R and tf ∈ R.Direct solution
direct_sol = solve(ocp; grid_size=50, display=false)
plt = plot(direct_sol, :state, :control; label="Direct")
Computing the singular control
The pseudo-Hamiltonian
Splitting the dynamics into a drift and a control vector field,
F0(q) = [cos(q[3]), sin(q[3]) + q[1], 0]
F1(q) = [0, 0, 1]F1 (generic function with 1 method)H0 = Lift(F0)
H1 = Lift(F1)
H01 = @Lie {H0, H1}
H001 = @Lie {H0, H01}
H101 = @Lie {H1, H01}
us_bracket(q, p) = -H001(q, p) / H101(q, p)us_bracket (generic function with 1 method)The classical singular-control formula
u_indirect(x) = sin(x[3])^2u_indirect (generic function with 1 method)Indirect solution
f = Flow(ocp, (x, p, v) -> u_indirect(x))OptimalControlFlow
├─ system: HamiltonianSystem
│ ├─ time_dependence: Autonomous
│ ├─ variable_dependence: NonFixed
│ ├─ ComposedHamiltonian: autonomous, variable
│ │ natural call: h(x, p, v)
│ │ uniform call: h(t, x, p, v)
│ └─ backend: DifferentiationInterface{CPU}(ad_backend=AutoForwardDiff)
└─ integrator: SciML{CPU} (instance, id=:sciml)
├─ internalnorm = real_norm [default]
├─ alg = Tsit5 [default]
├─ reltol = 1.0e-8 [default]
├─ save_everystep = auto [default]
├─ abstol = 1.0e-8 [default]
├─ save_start = auto [default]
└─ dense = auto [default]
Tip: use describe(SciML{CPU}) to see all available options.Unknowns: the initial costate
t0 = 0
function shoot!(s, ξ)
p0, θ0, tf = ξ[1:3], ξ[4], ξ[5]
q_t0 = [0, 0, θ0]
q_tf, p_tf = f(t0, q_t0, p0, tf; variable=tf)
s[1] = q_tf[1] - 1
s[2] = q_tf[2]
s[3] = p0[3]
s[4] = p_tf[3]
px_tf, py_tf, θf = p_tf[1], p_tf[2], q_tf[3]
s[5] = px_tf * cos(θf) + py_tf * (sin(θf) + 1) - 1
return nothing
endshoot! (generic function with 1 method)nle!(s, ξ, _) = shoot!(s, ξ)
p0_guess = costate(direct_sol)(t0)
θ0_guess = state(direct_sol)(t0)[3]
tf_guess = variable(direct_sol)
ξ_guess = [p0_guess..., θ0_guess, tf_guess]
prob = NonlinearProblem(nle!, ξ_guess)
shooting_sol = NonlinearSolve.solve(prob; show_trace=Val(false))
p0_sol, θ0_sol, tf_sol = shooting_sol.u[1:3], shooting_sol.u[4], shooting_sol.u[5]([0.7826328346326092, -0.6224836112084414, 0.0], -0.6719121189244027, 1.149730885756096)s = zeros(5)
shoot!(s, shooting_sol.u)
s5-element Vector{Float64}:
-6.661338147750939e-16
-1.5974973277118073e-16
0.0
-1.9556226864272655e-13
-1.346700528870315e-13Comparison
indirect_sol = f((t0, tf_sol), [0, 0, θ0_sol], p0_sol; variable=tf_sol)
plot!(plt, indirect_sol, :state, :control; label="Indirect", linestyle=:dash)
The indirect and direct solutions match closely, confirming the singular-control computation above is correct — and, since Lift/@Lie needed no change from the pre-rewrite version, that the flow half was migrated correctly too.
The bracket formula and the simplified state-only one agree along this extremal — not at an arbitrary
[us_bracket(state(indirect_sol)(t), costate(indirect_sol)(t)) - u_indirect(state(indirect_sol)(t))
for t in range(t0, tf_sol, 5)]5-element Vector{Float64}:
2.927402764640874e-11
2.0201063044567036e-11
1.7152529396824434e-11
-1.507088898122788e-11
-9.912654030941326e-12See also
Poisson bracket — the
/ / chain, in general. The
@Liemacro — the{}bracket notation used above.Lifting a vector field —
Lift, in general.Shooting — the shooting method in general.