Skip to content

Commit ce5a00d

Browse files
committed
Improve tutorial-goddard: rename variables, add references, improve documentation
- Rename variables for clarity: indirect_sol → minpack_sol, sol → sciml_sol, flow_sol → indirect_sol - Move BenchmarkTools import to benchmarking section - Add cross-reference to tutorial-iss for indirect shooting introduction - Add @extref links to differential geometry tools and singular control example - Explain transversality conditions (r(tf) and v(tf) free) - Document shooting function structure (7 unknowns, 7 equations) - Add summary of optimal structure (bang → singular → boundary → off) - Replace benchmarking conclusion with neutral text - Add @Assert for shooting function norm validation - Add comparison between MINPACK and NonlinearSolve solver results - Add L2 norm comparison between direct and indirect solutions in collapsible note
1 parent bb710cf commit ce5a00d

1 file changed

Lines changed: 133 additions & 13 deletions

File tree

docs/src/tutorial-goddard.md

Lines changed: 133 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# [Direct and indirect methods for the Goddard problem](@id tutorial-goddard)
22

3+
```@meta
4+
Draft = false
5+
```
6+
37
## Introduction
48

59
```@raw html
@@ -32,7 +36,6 @@ and subject to the control constraint $u(t) \in [0,1]$ and the state constraint
3236
We import the [OptimalControl.jl](https://control-toolbox.org/OptimalControl.jl) package to define the optimal control problem and [NLPModelsIpopt.jl](https://jso.dev/NLPModelsIpopt.jl) to solve it. We import the [Plots.jl](https://docs.juliaplots.org) package to plot the solution. The [OrdinaryDiffEq.jl](https://docs.sciml.ai/OrdinaryDiffEq) package is used to define the shooting function for the indirect method and the [MINPACK.jl](https://github.com/sglyon/MINPACK.jl) package permits to solve the shooting equation.
3337

3438
```@example main-goddard
35-
using BenchmarkTools
3639
using OptimalControl # to define the optimal control problem and more
3740
using NLPModelsIpopt # to solve the problem via a direct method
3841
using OrdinaryDiffEq # to get the Flow function from OptimalControl
@@ -107,7 +110,7 @@ plt = plot(direct_sol, label="direct", size=(800, 800))
107110

108111
## [Structure of the solution](@id tutorial-goddard-structure)
109112

110-
We first determine visually the structure of the optimal solution which is composed of a bang arc with maximal control, followed by a singular arc, then by a boundary arc and the final arc is with zero control. Note that the switching function vanishes along the singular and boundary arcs.
113+
We first determine visually the structure of the optimal solution which is composed of a bang arc with maximal control, followed by a singular arc, then by a boundary arc and the final arc is with zero control. In summary, the structure is: **bang** ($u=1$) → **singular****boundary** ($v=v_{\max}$) → **off** ($u=0$). Note that the switching function vanishes along the singular and boundary arcs.
111114

112115
```@example main-goddard
113116
t = time_grid(direct_sol) # the time grid as a vector
@@ -126,7 +129,7 @@ g_plot = plot(t, g ∘ x, linewidth=2, label = "g(x(t))")
126129
plot(u_plot, H1_plot, g_plot, layout=(3,1), size=(600, 600))
127130
```
128131

129-
We are now in position to solve the problem by an indirect shooting method. We first define the four control laws in feedback form and their associated flows. For this we need to compute some Lie derivatives, namely [Poisson brackets](https://en.wikipedia.org/wiki/Poisson_bracket) of Hamiltonians (themselves obtained as lifts to the cotangent bundle of vector fields), or derivatives of functions along a vector field. For instance, the control along the *minimal order* singular arcs is obtained as the quotient
132+
We are now in position to solve the problem by an indirect shooting method. For an introduction to the indirect simple shooting method, see the [Indirect simple shooting](@ref tutorial-indirect-simple-shooting) tutorial. We first define the four control laws in feedback form and their associated flows. For this we need to compute some Lie derivatives, namely [Poisson brackets](https://en.wikipedia.org/wiki/Poisson_bracket) of Hamiltonians (themselves obtained as lifts to the cotangent bundle of vector fields), or derivatives of functions along a vector field. For instance, the control along the *minimal order* singular arcs is obtained as the quotient
130133

131134
```math
132135
u_s = -\frac{H_{001}}{H_{101}}
@@ -172,7 +175,7 @@ as well as the associated multiplier for the *order one* state constraint on the
172175

173176
which is the reason why we use the `@Lie` macro to compute Poisson brackets below.
174177

175-
With the help of differential geometry primitives, these expressions are straightforwardly translated into Julia code:
178+
With the help of differential geometry primitives, these expressions are straightforwardly translated into Julia code. For more details on the differential geometry tools, see [Differential geometry tools](@extref OptimalControl manual-differential-geometry). For a simpler example involving a minimal order singular arc, see [Singular control](@extref OptimalControl example-singular-control).
176179

177180
```@example main-goddard
178181
# Controls
@@ -198,7 +201,7 @@ nothing # hide
198201

199202
## Shooting function
200203

201-
Then, we define the shooting function according to the optimal structure we have determined, that is a concatenation of four arcs.
204+
Then, we define the shooting function according to the optimal structure we have determined, that is a concatenation of four arcs. The shooting function has 7 unknowns (3 components of the initial costate `p0` and 4 times: `t1`, `t2`, `t3`, `tf`) and 7 equations.
202205

203206
```@example main-goddard
204207
x0 = [r0, v0, m0] # initial state
@@ -211,7 +214,8 @@ function shoot!(s, p0, t1, t2, t3, tf)
211214
xf, pf = f0(t3, x3, p3, tf)
212215
213216
s[1] = xf[3] - mf # final mass constraint
214-
s[2:3] = pf[1:2] - [1, 0] # transversality conditions
217+
s[2:3] = pf[1:2] - [1, 0] # transversality conditions: r(tf) and v(tf) are free
218+
# objective is -r(tf) → p_r(tf) = 1, and v(tf) free → p_v(tf) = 0
215219
s[4] = H1(x1, p1) # H1 = H01 = 0
216220
s[5] = H01(x1, p1) # at the entrance of the singular arc
217221
s[6] = g(x2) # g = 0 when entering the boundary arc
@@ -302,10 +306,10 @@ We are now in position to solve the problem with the `hybrj` solver from MINPACK
302306

303307
```@example main-goddard
304308
# resolution of S(ξ) = 0
305-
indirect_sol = fsolve(shoot!, jshoot!, ξ_guess, show_trace=true)
309+
minpack_sol = fsolve(shoot!, jshoot!, ξ_guess, show_trace=true)
306310
307311
# we retrieve the costate solution together with the times
308-
ξ = indirect_sol.x
312+
ξ = minpack_sol.x
309313
p0, t1, t2, t3, tf = ξ[1:3], ξ[4:end]...
310314
311315
println("MINPACK results:")
@@ -319,6 +323,7 @@ println("tf = ", tf)
319323
s = similar(p0, 7)
320324
shoot!(s, p0, t1, t2, t3, tf)
321325
println("\nNorm of the shooting function: ‖s‖ = ", norm(s), "\n")
326+
@assert norm(s) < 1e-6 "Indirect shooting failed for MINPACK"
322327
```
323328

324329
### NonlinearSolve.jl
@@ -335,10 +340,10 @@ Now, let us solve the problem and retrieve the initial costate and times.
335340

336341
```@example main-goddard
337342
# resolution of S(ξ) = 0
338-
sol = solve(prob; show_trace=Val(true))
343+
sciml_sol = solve(prob; show_trace=Val(true))
339344
340345
# we retrieve the costate solution together with the times
341-
ξ = sol.u
346+
ξ = sciml_sol.u
342347
p0, t1, t2, t3, tf = ξ[1:3], ξ[4:end]...
343348
344349
println("\nNonlinearSolve results:")
@@ -352,12 +357,32 @@ println("tf = ", tf)
352357
s = similar(p0, 7)
353358
shoot!(s, p0, t1, t2, t3, tf)
354359
println("\nNorm of the shooting function: ‖s‖ = ", norm(s), "\n")
360+
@assert norm(s) < 1e-6 "Indirect shooting failed for NonlinearSolve"
361+
```
362+
363+
### Comparison of the two solvers
364+
365+
Let us compare the results obtained by the two solvers.
366+
367+
```@example main-goddard
368+
ξ_minpack = minpack_sol.x
369+
ξ_sciml = sciml_sol.u
370+
371+
println("Comparison of the two solvers:")
372+
println("MINPACK solution: ξ = ", ξ_minpack)
373+
println("SciML solution: ξ = ", ξ_sciml)
374+
println("Difference: Δξ = ", ξ_minpack - ξ_sciml)
375+
println("Relative error: ‖Δξ‖/‖ξ‖ = ", norm(ξ_minpack - ξ_sciml) / norm(ξ_minpack))
355376
```
356377

357378
### Benchmarking
358379

359380
The results found for by the two solvers are extremely close, so now, lets benchmark these two resolutions to compare their performances.
360381

382+
```@example main-goddard
383+
using BenchmarkTools
384+
```
385+
361386
```@example main-goddard
362387
@benchmark fsolve(shoot!, jshoot!, ξ_guess; tol=1e-8, show_trace=false) #MINPACK
363388
```
@@ -372,17 +397,112 @@ According to the NonlinearSolve documentation, for small nonlinear systems, it c
372397
@benchmark solve(prob, SimpleNewtonRaphson(); abstol=1e-8, reltol=1e-8, show_trace=Val(false)) # NonlinearSolve
373398
```
374399

375-
MINPACK.jl (fsolve) is much faster than NonlinearSolve.jl (solve) on this problem. It also uses less memory. Both methods have a similar GC overhead, but MINPACK.jl is overall more efficient here.
400+
There exist different alternatives to solve the shooting equation, as shown in the benchmarks above.
376401

377402
## [Plot of the solution](@id tutorial-goddard-plot)
378403

379404
We plot the solution of the indirect solution (in red) over the solution of the direct method (in blue).
380405

381406
```@example main-goddard
382407
f = f1 * (t1, fs) * (t2, fb) * (t3, f0) # concatenation of the flows
383-
flow_sol = f((t0, tf), x0, p0) # compute the solution: state, costate, control...
408+
indirect_sol = f((t0, tf), x0, p0) # compute the solution: state, costate, control...
409+
410+
plot!(plt, indirect_sol; label="indirect", color=2)
411+
```
384412

385-
plot!(plt, flow_sol; label="indirect", color=2)
413+
!!! note "Numerical comparison between direct and indirect solutions"
414+
415+
```@raw html
416+
<details><summary>Click to unfold and get the code for numerical comparisons.</summary>
417+
```
418+
419+
```@example main-goddard
420+
using Printf
421+
422+
function L2_norm(T, X)
423+
# T and X are supposed to be one dimensional
424+
s = 0.0
425+
for i in 1:(length(T) - 1)
426+
s += 0.5 * (X[i]^2 + X[i + 1]^2) * (T[i + 1] - T[i])
427+
end
428+
return √(s)
429+
end
430+
431+
function print_numerical_comparisons(direct_sol, indirect_sol)
432+
433+
# get relevant data from direct solution
434+
t_dir = time_grid(direct_sol)
435+
x_dir = state(direct_sol).(t_dir)
436+
u_dir = control(direct_sol).(t_dir)
437+
v_dir = variable(direct_sol)
438+
o_dir = objective(direct_sol)
439+
i_dir = iterations(direct_sol)
440+
441+
# get relevant data from indirect solution
442+
t_ind = time_grid(indirect_sol)
443+
x_ind = state(indirect_sol).(t_ind)
444+
u_ind = control(indirect_sol).(t_ind)
445+
v_ind = variable(indirect_sol)
446+
o_ind = objective(indirect_sol)
447+
448+
x_vars = ["r", "v", "m"]
449+
u_vars = ["u"]
450+
v_vars = ["tf"]
451+
452+
println("┌─ Goddard problem: direct vs indirect")
453+
println("│")
454+
println("├─ Number of Iterations")
455+
@printf("│ Direct: %d\\n", i_dir)
456+
457+
# States
458+
println("├─ States (L2 Norms)")
459+
for i in eachindex(x_vars)
460+
xi_dir = [x_dir[k][i] for k in eachindex(t_dir)]
461+
xi_ind = [x_ind[k][i] for k in eachindex(t_ind)]
462+
L2_ae = L2_norm(t_dir, xi_dir - xi_ind)
463+
L2_re = L2_ae / (0.5 * (L2_norm(t_dir, xi_dir) + L2_norm(t_dir, xi_ind)))
464+
@printf("│ %-6s Abs: %.3e Rel: %.3e\\n", x_vars[i], L2_ae, L2_re)
465+
end
466+
467+
# Controls
468+
println("├─ Controls (L2 Norms)")
469+
for i in eachindex(u_vars)
470+
ui_dir = [u_dir[k][i] for k in eachindex(t_dir)]
471+
ui_ind = [u_ind[k][i] for k in eachindex(t_ind)]
472+
L2_ae = L2_norm(t_dir, ui_dir - ui_ind)
473+
L2_re = L2_ae / (0.5 * (L2_norm(t_dir, ui_dir) + L2_norm(t_dir, ui_ind)))
474+
@printf("│ %-6s Abs: %.3e Rel: %.3e\\n", u_vars[i], L2_ae, L2_re)
475+
end
476+
477+
# Variables
478+
println("├─ Variables")
479+
for i in eachindex(v_vars)
480+
vi_dir = v_dir[i]
481+
vi_ind = v_ind[i]
482+
vi_ae = abs(vi_dir - vi_ind)
483+
vi_re = vi_ae / (0.5 * (abs(vi_dir) + abs(vi_ind)))
484+
@printf("│ %-6s Abs: %.3e Rel: %.3e\\n", v_vars[i], vi_ae, vi_re)
485+
end
486+
487+
# Objective
488+
o_ae = abs(o_dir - o_ind)
489+
o_re = o_ae / (0.5 * (abs(o_dir) + abs(o_ind)))
490+
println("├─ Objective")
491+
@printf("│ Abs: %.3e Rel: %.3e\\n", o_ae, o_re)
492+
println("└─")
493+
return nothing
494+
end
495+
nothing # hide
496+
```
497+
498+
```@raw html
499+
</details>
500+
```
501+
502+
We now compare numerically the direct and indirect solutions. The comparison includes L2 norms for the state and control variables, as well as absolute and relative errors for the variable (final time) and the objective. The L2 norm is computed using the trapezoidal rule over the time grid of the direct solution.
503+
504+
```@example main-goddard
505+
print_numerical_comparisons(direct_sol, indirect_sol)
386506
```
387507

388508
## References

0 commit comments

Comments
 (0)