Skip to content

Commit 90c7e5e

Browse files
authored
Merge pull request #736 from control-toolbox/indirect-tests
Add indirect method tests
2 parents 0f84eec + f88f7f9 commit 90c7e5e

8 files changed

Lines changed: 1034 additions & 3 deletions
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
# [Double integrator: energy minimisation](@id example-double-integrator-energy)
2+
3+
Let us consider a wagon moving along a rail, whose acceleration can be controlled by a force $u$.
4+
We denote by $x = (q, v)$ the state of the wagon, where $q$ is the position and $v$ the velocity.
5+
6+
```@raw html
7+
<img src="./assets/chariot_q.svg" style="display: block; margin: 0 auto 20px auto;" width="400px">
8+
```
9+
10+
We assume that the mass is constant and equal to one, and that there is no friction. The dynamics are given by
11+
12+
```math
13+
\dot q(t) = v(t), \quad \dot v(t) = u(t),\quad u(t) \in \R,
14+
```
15+
16+
which is simply the [double integrator](https://en.wikipedia.org/w/index.php?title=Double_integrator&oldid=1071399674) system. Let us consider a transfer starting at time $t_0 = 0$ and ending at time $t_f = 1$, for which we want to minimise the transfer energy
17+
18+
```math
19+
\frac{1}{2}\int_{0}^{1} u^2(t) \, \mathrm{d}t
20+
```
21+
22+
starting from $x(0) = (-1, 0)$ and aiming to reach the target $x(1) = (0, 0)$.
23+
24+
First, we need to import the [OptimalControl.jl](https://control-toolbox.org/OptimalControl.jl) package to define the optimal control problem, [NLPModelsIpopt.jl](https://jso.dev/NLPModelsIpopt.jl) to solve it, and [Plots.jl](https://docs.juliaplots.org) to visualise the solution.
25+
26+
```@example main
27+
using OptimalControl
28+
using NLPModelsIpopt
29+
using Plots
30+
```
31+
32+
## Optimal control problem
33+
34+
Let us define the problem with the [`@def`](@ref) macro:
35+
36+
```@raw html
37+
<div class="responsive-columns-left-priority">
38+
<div>
39+
```
40+
41+
```@example main
42+
t0 = 0; tf = 1; x0 = [-1, 0]; xf = [0, 0]
43+
44+
ocp = @def begin
45+
t ∈ [t0, tf], time
46+
x = (q, v) ∈ R², state
47+
u ∈ R, control
48+
49+
x(t0) == x0
50+
x(tf) == xf
51+
52+
∂(q)(t) == v(t)
53+
∂(v)(t) == u(t)
54+
55+
0.5∫( u(t)^2 ) → min
56+
end
57+
nothing # hide
58+
```
59+
60+
```@raw html
61+
</div>
62+
<div>
63+
```
64+
65+
### Mathematical formulation
66+
67+
```math
68+
\begin{aligned}
69+
& \text{Minimise} && \frac{1}{2}\int_0^1 u^2(t) \,\mathrm{d}t \\
70+
& \text{subject to} \\
71+
& && \dot{q}(t) = v(t), \\[0.5em]
72+
& && \dot{v}(t) = u(t), \\[1.0em]
73+
& && x(0) = (-1,0), \\[0.5em]
74+
& && x(1) = (0,0).
75+
\end{aligned}
76+
```
77+
78+
```@raw html
79+
</div>
80+
</div>
81+
```
82+
83+
!!! note "Nota bene"
84+
85+
For a comprehensive introduction to the syntax used above to define the optimal control problem, see [this abstract syntax tutorial](@ref manual-abstract-syntax). In particular, non-Unicode alternatives are available for derivatives, integrals, *etc.*
86+
87+
## [Solve and plot](@id example-double-integrator-energy-solve-plot)
88+
89+
### Direct method
90+
91+
We can [`solve`](@ref) it simply with:
92+
93+
```@example main
94+
direct_sol = solve(ocp)
95+
nothing # hide
96+
```
97+
98+
<!-- Objective value: 6.0000960e+00 -->
99+
100+
And [`plot`](@ref) the solution with:
101+
102+
```@example main
103+
plot(direct_sol)
104+
```
105+
106+
!!! note "Nota bene"
107+
108+
The `solve` function has options, see the [solve tutorial](@ref manual-solve). You can customise the plot, see the [plot tutorial](@ref manual-plot).
109+
110+
### Indirect method
111+
112+
The first solution was obtained using the so-called direct method.[^1] Another approach is to use an [indirect simple shooting](@extref tutorial-indirect-simple-shooting) method. We begin by importing the necessary packages.
113+
114+
```@example main
115+
using OrdinaryDiffEq # Ordinary Differential Equations (ODE) solver
116+
using NonlinearSolve # Nonlinear Equations (NLE) solver
117+
```
118+
119+
To define the shooting function, we must provide the maximising control in feedback form:
120+
121+
```@example main
122+
# maximising control, H(x, p, u) = p₁v + p₂u - u²/2
123+
u(x, p) = p[2]
124+
125+
# Hamiltonian flow
126+
f = Flow(ocp, u)
127+
128+
# state projection, p being the costate
129+
π((x, p)) = x
130+
131+
# shooting function
132+
S(p0) = π( f(t0, x0, p0, tf) ) - xf
133+
nothing # hide
134+
```
135+
136+
We are now ready to solve the shooting equations.
137+
138+
```@example main
139+
# auxiliary in-place NLE function
140+
nle!(s, p0, _) = s[:] = S(p0)
141+
142+
# initial guess for the Newton solver
143+
p0_guess = [1, 1]
144+
145+
# NLE problem with initial guess
146+
prob = NonlinearProblem(nle!, p0_guess)
147+
148+
# resolution of S(p0) = 0
149+
shooting_sol = solve(prob; show_trace=Val(true))
150+
p0_sol = shooting_sol.u # costate solution
151+
152+
# print the costate solution and the shooting function evaluation
153+
println("\ncostate: p0 = ", p0_sol)
154+
println("shoot: S(p0) = ", S(p0_sol), "\n")
155+
```
156+
157+
<!-- costate: p0 = [12.000000000000195, 6.000000000000072]
158+
shoot: S(p0) = [1.9076974876250146e-15, -1.2233533481964473e-14] -->
159+
160+
To plot the solution obtained by the indirect method, we need to build the solution of the optimal control problem. This is done using the costate solution and the flow function.
161+
162+
```@example main
163+
indirect_sol = f((t0, tf), x0, p0_sol; saveat=range(t0, tf, 100))
164+
plot(indirect_sol)
165+
```
166+
167+
[^1]: J. T. Betts. Practical methods for optimal control using nonlinear programming. Society for Industrial and Applied Mathematics (SIAM), Philadelphia, PA, 2001.
168+
169+
!!! note
170+
171+
- You can use [MINPACK.jl](@extref Tutorials Resolution-of-the-shooting-equation) instead of [NonlinearSolve.jl](https://docs.sciml.ai/NonlinearSolve).
172+
- For more details about the flow construction, visit the [Compute flows from optimal control problems](@ref manual-flow-ocp) page.
173+
- In this simple example, we have set an arbitrary initial guess. It can be helpful to use the solution of the direct method to initialise the shooting method. See the [Goddard tutorial](@extref Tutorials tutorial-goddard) for such a concrete application.
174+
175+
## State constraint
176+
177+
The following example illustrates both direct and indirect solution approaches for the energy minimization problem with a state constraint on the maximal velocity. The workflow demonstrates a practical strategy: a direct method on a coarse grid first identifies the problem structure and provides an initial guess for the indirect method, which then computes a precise solution via shooting based on Pontryagin's Maximum Principle.
178+
179+
!!! note
180+
181+
The direct solution can be refined using a finer discretization grid for higher accuracy.
182+
183+
### Direct method: constrained case
184+
185+
We add the path constraint
186+
187+
```math
188+
v(t) \le 1.2.
189+
```
190+
191+
Let us model, solve and plot the optimal control problem with this constraint.
192+
193+
```@example main
194+
# the upper bound for v
195+
v_max = 1.2
196+
197+
# the optimal control problem
198+
ocp = @def begin
199+
t ∈ [t0, tf], time
200+
x = (q, v) ∈ R², state
201+
u ∈ R, control
202+
203+
v(t) ≤ v_max # state constraint
204+
205+
x(t0) == x0
206+
x(tf) == xf
207+
208+
∂(q)(t) == v(t)
209+
∂(v)(t) == u(t)
210+
211+
0.5∫( u(t)^2 ) → min
212+
end
213+
214+
# solve with a direct method
215+
direct_sol = solve(ocp; grid_size=50)
216+
217+
# plot the solution
218+
plt = plot(direct_sol; label="Direct", size=(800, 600))
219+
```
220+
221+
The solution has three phases (unconstrained-constrained-unconstrained arcs), requiring definition of Hamiltonian flows for each phase and a shooting function to enforce boundary and switching conditions.
222+
223+
### Indirect method: constrained case
224+
225+
Under the normal case, the pseudo-Hamiltonian reads:
226+
227+
```math
228+
H(x, p, u, \mu) = p_1 v + p_2 u - \frac{u^2}{2} + \mu\, g(x),
229+
```
230+
231+
where $g(x) = v_{\max} - v$. Along a boundary arc we have $g(x(t)) = 0$; differentiating gives:
232+
233+
```math
234+
\frac{\mathrm{d}}{\mathrm{d}t}g(x(t)) = -\dot{v}(t) = -u(t) = 0.
235+
```
236+
237+
The zero control maximises the Hamiltonian, so $p_2(t) = 0$ along that arc. From the adjoint equation we then have
238+
239+
```math
240+
\dot{p}_2(t) = -p_1(t) + \mu(t) = 0 \quad \Rightarrow \mu(t) = p_1(t).
241+
```
242+
243+
Because the adjoint vector is continuous at both the entry time $t_1$ and the exit time $t_2$, the unknowns are $p_0 \in \mathbb{R}^2$ together with $t_1$ and $t_2$. The target condition supplies two equations, $g(x(t_1)) = 0$ enforces the state constraint, and $p_2(t_1) = 0$ encodes the switching condition.
244+
245+
```@example main
246+
# flow for unconstrained extremals
247+
f_interior = Flow(ocp, (x, p) -> p[2])
248+
249+
ub = 0 # boundary control
250+
g(x) = v_max - x[2] # constraint: g(x) ≥ 0
251+
μ(p) = p[1] # dual variable
252+
253+
# flow for boundary extremals
254+
f_boundary = Flow(ocp, (x, p) -> ub, (x, u) -> g(x), (x, p) -> μ(p))
255+
256+
# shooting function
257+
function shoot!(s, p0, t1, t2)
258+
x_t0, p_t0 = x0, p0
259+
x_t1, p_t1 = f_interior(t0, x_t0, p_t0, t1)
260+
x_t2, p_t2 = f_boundary(t1, x_t1, p_t1, t2)
261+
x_tf, p_tf = f_interior(t2, x_t2, p_t2, tf)
262+
s[1:2] = x_tf - xf
263+
s[3] = g(x_t1)
264+
s[4] = p_t1[2]
265+
end
266+
nothing # hide
267+
```
268+
269+
We can derive an initial guess for the costate and the entry/exit times from the direct solution:
270+
271+
```@example main
272+
t = time_grid(direct_sol) # the time grid as a vector
273+
x = state(direct_sol) # the state as a function of time
274+
p = costate(direct_sol) # the costate as a function of time
275+
276+
# initial costate
277+
p0 = p(t0)
278+
279+
# times where constraint is active
280+
t12 = t[ 0 .≤ (g ∘ x).(t) .≤ 1e-3 ]
281+
282+
# entry and exit times
283+
t1 = minimum(t12) # entry time
284+
t2 = maximum(t12) # exit time
285+
nothing # hide
286+
```
287+
288+
We can now solve the shooting equations.
289+
290+
```@example main
291+
# auxiliary in-place NLE function
292+
nle!(s, ξ, _) = shoot!(s, ξ[1:2], ξ[3], ξ[4])
293+
294+
# initial guess for the Newton solver
295+
ξ_guess = [p0..., t1, t2]
296+
297+
# NLE problem with initial guess
298+
prob = NonlinearProblem(nle!, ξ_guess)
299+
300+
# resolution of the shooting equations
301+
shooting_sol = solve(prob; show_trace=Val(true))
302+
p0, t1, t2 = shooting_sol.u[1:2], shooting_sol.u[3], shooting_sol.u[4]
303+
304+
# print the costate solution and the entry and exit times
305+
println("\np0 = ", p0, "\nt1 = ", t1, "\nt2 = ", t2)
306+
```
307+
308+
<!-- p0 = [38.40000000000056, 9.600000000000053]
309+
t1 = 0.2499999999999979
310+
t2 = 0.7500000000000011 -->
311+
312+
To reconstruct the constrained trajectory, concatenate the flows as follows: an unconstrained arc until $t_1$, a boundary arc from $t_1$ to $t_2$, and a final unconstrained arc from $t_2$ to $t_f$.
313+
This composition yields the full solution (state, costate, and control), which we then plot alongside the direct method for comparison.
314+
315+
```@example main
316+
# concatenation of the flows
317+
φ = f_interior * (t1, f_boundary) * (t2, f_interior)
318+
319+
# compute the solution: state, costate, control...
320+
indirect_sol = φ((t0, tf), x0, p0; saveat=range(t0, tf, 100))
321+
322+
# plot the solution on the previous plot
323+
plot!(plt, indirect_sol; label="Indirect", color=2, linestyle=:dash)
324+
```

0 commit comments

Comments
 (0)