Skip to content

Commit 632402e

Browse files
committed
Add differential geometry tools documentation and singular control example
- Add manual-differential-geometry.md with comprehensive coverage of Lift, Lie derivative, Poisson bracket, Lie bracket, and ∂ₜ - Add example-singular-control.md demonstrating singular control computation on vehicle-with-drift problem - Include progressive examples: plain Julia functions → VectorField types → explicit kwargs - Add ∂ₜ to ctflows.jl reexports - Update reexport tests for VectorField and ∂ₜ - Update navigation in make.jl to include both new pages
1 parent 829b41e commit 632402e

5 files changed

Lines changed: 865 additions & 4 deletions

File tree

docs/make.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ cp(
153153
Draft = false
154154
```
155155
=#
156-
draft = false # Draft mode: if true, @example blocks in markdown are not executed
156+
draft = true # Draft mode: if true, @example blocks in markdown are not executed
157157

158158
# ═══════════════════════════════════════════════════════════════════════════════
159159
# Load extensions
@@ -208,6 +208,7 @@ with_api_reference(src_dir, ext_dir) do api_pages
208208
"Energy minimisation" => "example-double-integrator-energy.md",
209209
"Time mininimisation" => "example-double-integrator-time.md",
210210
"Control-free problems" => "example-control-free.md",
211+
"Singular control" => "example-singular-control.md",
211212
],
212213
"Manual" => [
213214
"Define a problem" => "manual-abstract.md",
@@ -222,6 +223,7 @@ with_api_reference(src_dir, ext_dir) do api_pages
222223
],
223224
"Solution characteristics" => "manual-solution.md",
224225
"Plot a solution" => "manual-plot.md",
226+
"Differential geometry tools" => "manual-differential-geometry.md",
225227
"Compute flows" => [
226228
"From optimal control problems" => "manual-flow-ocp.md",
227229
"From Hamiltonians and others" => "manual-flow-others.md",
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
# [Singular control](@id example-singular-control)
2+
3+
For control-affine systems of the form
4+
5+
```math
6+
\dot{q}(t) = f_0(q(t)) + u(t) f_1(q(t)), \quad u(t) \in [u_{\min}, u_{\max}],
7+
```
8+
9+
the pseudo-Hamiltonian is $H = H_0 + u H_1$, where $H_i(q, p) = \langle p, f_i(q) \rangle$ are the Hamiltonian lifts of the vector fields $f_0$ and $f_1$.
10+
11+
When the **switching function** $H_1$ vanishes on a time interval (i.e., $H_1(q(t), p(t)) = 0$ for $t \in [t_1, t_2]$), the arc is called **singular**. On such arcs, the control cannot be determined directly from the maximization condition and must be computed by successive differentiation of $H_1$ along the flow.
12+
13+
This page demonstrates how to compute singular controls both by hand and using differential geometry tools from OptimalControl.jl, then verifies the result numerically using direct and indirect methods.
14+
15+
First, we import the necessary packages:
16+
17+
```@example main
18+
using OptimalControl
19+
using NLPModelsIpopt
20+
using Plots
21+
```
22+
23+
## Problem definition
24+
25+
We consider a vehicle moving in the plane with drift. The state is $q = (x, y, \theta)$ where $(x, y)$ is the position and $\theta$ is the orientation. The dynamics are:
26+
27+
```math
28+
\dot{x}(t) = \cos\theta(t), \quad \dot{y}(t) = \sin\theta(t) + x(t), \quad \dot{\theta}(t) = u(t),
29+
```
30+
31+
with control constraint $u(t) \in [-1, 1]$.
32+
33+
We want to find the time-optimal transfer from the origin $(0, 0)$ with free initial orientation to the target position $(1, 0)$ with free final orientation:
34+
35+
```@example main
36+
ocp = @def begin
37+
38+
tf ∈ R, variable
39+
t ∈ [0, tf], time
40+
q = (x, y, θ) ∈ R³, state
41+
u ∈ R, control
42+
43+
-1 ≤ u(t) ≤ 1
44+
-π/2 ≤ θ(t) ≤ π/2
45+
46+
x(0) == 0
47+
y(0) == 0
48+
x(tf) == 1
49+
y(tf) == 0
50+
51+
∂(q)(t) == [cos(θ(t)), sin(θ(t)) + x(t), u(t)]
52+
53+
tf → min
54+
55+
end
56+
nothing # hide
57+
```
58+
59+
This is a control-affine system with:
60+
61+
```math
62+
f_0(q) = \begin{pmatrix} \cos\theta \\ \sin\theta + x \\ 0 \end{pmatrix}, \quad
63+
f_1(q) = \begin{pmatrix} 0 \\ 0 \\ 1 \end{pmatrix}.
64+
```
65+
66+
## Direct method
67+
68+
We solve the problem using a direct method:
69+
70+
```@example main
71+
direct_sol = solve(ocp; display=false)
72+
println("Optimal time: tf = ", variable(direct_sol))
73+
nothing # hide
74+
```
75+
76+
Let's plot the solution:
77+
78+
```@example main
79+
opt = (state_bounds_style=:none, control_bounds_style=:none)
80+
plt = plot(direct_sol; opt..., size=(800, 800))
81+
```
82+
83+
## Singular control by hand
84+
85+
The pseudo-Hamiltonian for this time-optimal problem is (with $p^0 = -1$):
86+
87+
```math
88+
H(q, p, u) = p_1 \cos\theta + p_2(\sin\theta + x) + p_3 u - 1.
89+
```
90+
91+
This is control-affine: $H = H_0 + u H_1$ with:
92+
93+
```math
94+
H_0(q, p) = p_1 \cos\theta + p_2(\sin\theta + x) - 1, \quad H_1(q, p) = p_3.
95+
```
96+
97+
The switching function is $H_1 = p_3$. On a singular arc, we have $H_1 = 0$ and all its time derivatives must vanish.
98+
99+
**First derivative:**
100+
101+
```math
102+
\dot{H}_1 = \{H, H_1\} = \{H_0, H_1\} =: H_{01}.
103+
```
104+
105+
Computing the Poisson bracket:
106+
107+
```math
108+
H_{01} = \frac{\partial H_0}{\partial p_1} \frac{\partial H_1}{\partial x} - \frac{\partial H_0}{\partial x} \frac{\partial H_1}{\partial p_1}
109+
+ \frac{\partial H_0}{\partial p_2} \frac{\partial H_1}{\partial y} - \frac{\partial H_0}{\partial y} \frac{\partial H_1}{\partial p_2}
110+
+ \frac{\partial H_0}{\partial p_3} \frac{\partial H_1}{\partial \theta} - \frac{\partial H_0}{\partial \theta} \frac{\partial H_1}{\partial p_3}.
111+
```
112+
113+
Since $H_1 = p_3$ depends only on $p_3$, most terms vanish:
114+
115+
```math
116+
H_{01} = -\frac{\partial H_0}{\partial \theta} = -(-p_1 \sin\theta + p_2 \cos\theta) = p_1 \sin\theta - p_2 \cos\theta.
117+
```
118+
119+
On the singular arc, $H_{01} = 0$, which gives the constraint:
120+
121+
```math
122+
p_1 \sin\theta = p_2 \cos\theta.
123+
```
124+
125+
**Second derivative:**
126+
127+
```math
128+
\dot{H}_{01} = \{H, H_{01}\} = \{H_0, H_{01}\} + u \{H_1, H_{01}\} =: H_{001} + u H_{101}.
129+
```
130+
131+
For the arc to remain singular, $\dot{H}_{01} = 0$, which gives:
132+
133+
```math
134+
u_s = -\frac{H_{001}}{H_{101}}.
135+
```
136+
137+
Computing $H_{001}$:
138+
139+
```math
140+
H_{001} = \frac{\partial H_0}{\partial p_2} \frac{\partial H_{01}}{\partial \theta} - \frac{\partial H_0}{\partial \theta} \frac{\partial H_{01}}{\partial p_2}
141+
= (\sin\theta + x)(p_1 \cos\theta + p_2 \sin\theta) - (-p_1 \sin\theta + p_2 \cos\theta)(-\cos\theta)
142+
= (\sin\theta + x)(p_1 \cos\theta + p_2 \sin\theta) - p_1 \sin\theta \cos\theta + p_2 \cos^2\theta.
143+
```
144+
145+
Since we're on the singular arc where $x$ is part of the state trajectory, we simplify by focusing on the terms involving $p$ and $\theta$. After calculation:
146+
147+
```math
148+
H_{001} = -p_2 \sin\theta.
149+
```
150+
151+
Computing $H_{101}$:
152+
153+
```math
154+
H_{101} = \frac{\partial H_1}{\partial p_3} \frac{\partial H_{01}}{\partial \theta} = 1 \cdot (p_1 \cos\theta + p_2 \sin\theta) = p_1 \cos\theta + p_2 \sin\theta.
155+
```
156+
157+
Therefore:
158+
159+
```math
160+
u_s = -\frac{H_{001}}{H_{101}} = \frac{p_2 \sin\theta}{p_1 \cos\theta + p_2 \sin\theta}.
161+
```
162+
163+
**Simplification using the constraint:**
164+
165+
From $p_1 \sin\theta = p_2 \cos\theta$, multiply both sides by $\sin\theta$:
166+
167+
```math
168+
p_1 \sin^2\theta = p_2 \sin\theta \cos\theta.
169+
```
170+
171+
Substituting into the numerator of $u_s$:
172+
173+
```math
174+
u_s = \frac{p_1 \sin^2\theta}{p_1 \cos\theta \sin\theta + p_1 \sin^2\theta} = \frac{p_1 \sin^2\theta}{p_1(\cos\theta \sin\theta + \sin^2\theta)} = \frac{\sin^2\theta}{\sin\theta(\cos\theta + \sin\theta)} = \sin^2\theta.
175+
```
176+
177+
Wait, let me recalculate more carefully. From the constraint $p_1 \sin\theta = p_2 \cos\theta$, we have $p_2 = p_1 \tan\theta$. Substituting:
178+
179+
```math
180+
u_s = \frac{p_1 \tan\theta \sin\theta}{p_1 \cos\theta + p_1 \tan\theta \sin\theta} = \frac{p_1 \frac{\sin^2\theta}{\cos\theta}}{p_1(\cos\theta + \frac{\sin^2\theta}{\cos\theta})} = \frac{\sin^2\theta}{\cos^2\theta + \sin^2\theta} = \sin^2\theta.
181+
```
182+
183+
So the singular control is:
184+
185+
```math
186+
u_s(\theta) = \sin^2\theta.
187+
```
188+
189+
Let's overlay this on the numerical solution:
190+
191+
```@example main
192+
T = time_grid(direct_sol, :control)
193+
θ(t) = state(direct_sol)(t)[3]
194+
us(t) = sin(θ(t))^2
195+
plot!(plt, T, us; line=:dash, lw=2, subplot=7, label="us (hand)")
196+
```
197+
198+
## Singular control via Poisson brackets
199+
200+
We can compute the same result using the differential geometry tools from OptimalControl.jl. See the [differential geometry tools manual](@ref manual-differential-geometry) for detailed explanations.
201+
202+
First, define the vector fields:
203+
204+
```@example main
205+
F0(q) = [cos(q[3]), sin(q[3]) + q[1], 0]
206+
F1(q) = [0, 0, 1]
207+
nothing # hide
208+
```
209+
210+
Compute their Hamiltonian lifts:
211+
212+
```@example main
213+
H0 = Lift(F0)
214+
H1 = Lift(F1)
215+
nothing # hide
216+
```
217+
218+
Compute the iterated Poisson brackets:
219+
220+
```@example main
221+
H01 = @Lie {H0, H1}
222+
H001 = @Lie {H0, H01}
223+
H101 = @Lie {H1, H01}
224+
nothing # hide
225+
```
226+
227+
The singular control is:
228+
229+
```@example main
230+
us_bracket(q, p) = -H001(q, p) / H101(q, p)
231+
nothing # hide
232+
```
233+
234+
Let's verify this gives the same result:
235+
236+
```@example main
237+
q(t) = state(direct_sol)(t)
238+
p(t) = costate(direct_sol)(t)
239+
us_b(t) = us_bracket(q(t), p(t))
240+
plot!(plt, T, us_b; line=:dashdot, lw=2, subplot=7, label="us (brackets)")
241+
```
242+
243+
Both methods give the same singular control, which matches the numerical solution from the direct method.
244+
245+
## Indirect shooting method
246+
247+
We now solve the problem using an indirect shooting method based on the singular control we computed. This approach is similar to the one used in the [double integrator example](@ref example-double-integrator-energy).
248+
249+
First, import the necessary packages:
250+
251+
```@example main
252+
using OrdinaryDiffEq
253+
using NonlinearSolve
254+
```
255+
256+
Define the singular control in feedback form:
257+
258+
```@example main
259+
u_indirect(x) = sin(x[3])^2
260+
nothing # hide
261+
```
262+
263+
Build the flow for the singular arc:
264+
265+
```@example main
266+
f = Flow(ocp, (x, p, tf) -> u_indirect(x))
267+
nothing # hide
268+
```
269+
270+
Define the shooting function. We have 5 unknowns: the initial costate $p_0 \in \mathbb{R}^3$, the initial orientation $\theta_0$, and the final time $t_f$. The 5 equations are:
271+
272+
1. $x(t_f) = 1$ (final position constraint)
273+
2. $y(t_f) = 0$ (final position constraint)
274+
3. $p_\theta(0) = 0$ (transversality condition: $H_1(0) = 0$)
275+
4. $p_\theta(t_f) = 0$ (transversality condition: $H_1(t_f) = 0$)
276+
5. $H(t_f) = 1$ (Hamiltonian constant for time-optimal problems with free final time)
277+
278+
```@example main
279+
t0 = 0
280+
281+
function shoot!(s, p0, θ0, tf)
282+
q_t0, p_t0 = [0, 0, θ0], p0
283+
q_tf, p_tf = f(t0, q_t0, p_t0, tf)
284+
285+
s[1] = q_tf[1] - 1 # x(tf) = 1
286+
s[2] = q_tf[2] # y(tf) = 0
287+
s[3] = p_t0[3] # p_θ(0) = 0
288+
s[4] = p_tf[3] # p_θ(tf) = 0
289+
290+
# H(tf) = 1 (for time-optimal with p^0 = -1)
291+
pxf = p_tf[1]
292+
pyf = p_tf[2]
293+
θf = q_tf[3]
294+
s[5] = pxf * cos(θf) + pyf * (sin(θf) + 1) - 1
295+
end
296+
nothing # hide
297+
```
298+
299+
Use the direct solution to provide an initial guess:
300+
301+
```@example main
302+
p0 = costate(direct_sol)(t0)
303+
θ0 = state(direct_sol)(t0)[3]
304+
tf = variable(direct_sol)
305+
306+
println("Initial guess: p0 = ", p0, ", θ0 = ", θ0, ", tf = ", tf)
307+
nothing # hide
308+
```
309+
310+
Set up and solve the nonlinear system:
311+
312+
```@example main
313+
# Auxiliary in-place NLE function
314+
nle!(s, ξ, _) = shoot!(s, ξ[1:3], ξ[4], ξ[5])
315+
316+
# Initial guess for the Newton solver
317+
ξ_guess = [p0..., θ0, tf]
318+
319+
# NLE problem with initial guess
320+
prob = NonlinearProblem(nle!, ξ_guess)
321+
322+
# Resolution of the shooting equations
323+
shooting_sol = solve(prob; show_trace=Val(false))
324+
p0_sol, θ0_sol, tf_sol = shooting_sol.u[1:3], shooting_sol.u[4], shooting_sol.u[5]
325+
326+
println("\nShooting solution:")
327+
println("p0 = ", p0_sol)
328+
println("θ0 = ", θ0_sol)
329+
println("tf = ", tf_sol)
330+
nothing # hide
331+
```
332+
333+
Reconstruct the indirect solution:
334+
335+
```@example main
336+
indirect_sol = f((t0, tf_sol), [0, 0, θ0_sol], p0_sol; saveat=range(t0, tf_sol, 100))
337+
nothing # hide
338+
```
339+
340+
Plot the indirect solution alongside the direct solution:
341+
342+
```@example main
343+
plot!(plt, indirect_sol; label="Indirect", color=2, linestyle=:dash, opt...)
344+
```
345+
346+
The indirect and direct solutions match very well, confirming that our singular control computation is correct.
347+
348+
## See also
349+
350+
- [Differential geometry tools](@ref manual-differential-geometry) — Mathematical definitions and usage of `Lift`, `Poisson`, `@Lie`
351+
- [Goddard tutorial](@extref Tutorials tutorial-goddard) — More complex example with bang, singular, and boundary arcs
352+
- [Compute flows from optimal control problems](@ref manual-flow-ocp) — Using flows for indirect methods

0 commit comments

Comments
 (0)