Skip to content

Commit ffb4509

Browse files
committed
Refine discretisation tutorial
- Change disc_method parameter to scheme - Remove const from parameters and wrap OCP in goddard_problem() function - Add comments to dynamics functions explaining physics - Add successful(sol) checks after solve calls - Update AD backend section to use explicit solve mode with strategy instances - Add documentation links using @extref syntax - Simplify plotting code with enumerate and color indexing - Add :external_cross_references to warnonly in make.jl
1 parent 53bcaec commit ffb4509

3 files changed

Lines changed: 62 additions & 44 deletions

File tree

docs/make.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,14 @@ draft = true # Draft mode: if true, @example blocks in markdown are not execute
5555
# ═══════════════════════════════════════════════════════════════════════════════
5656
makedocs(;
5757
draft=draft,
58-
warnonly=[:cross_references, :autodocs_block],
58+
warnonly=[:cross_references, :autodocs_block, :external_cross_references],
5959
sitename="Tutorials",
6060
format=Documenter.HTML(;
6161
repolink="https://" * repo_url,
6262
prettyurls=false,
63-
size_threshold_ignore=["tutorial-discretisation.md"],
63+
size_threshold_ignore=[
64+
"tutorial-discretisation.md",
65+
],
6466
assets=[
6567
asset("https://control-toolbox.org/assets/css/documentation.css"),
6668
asset("https://control-toolbox.org/assets/js/documentation.js"),

docs/src/tutorial-continuation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ using Printf
1717
using Plots
1818
```
1919

20-
The `init` parameter of the `solve` function allows providing an initial guess. See the [initial guess documentation](https://control-toolbox.org/OptimalControl.jl/stable/manual-initial-guess.html) for more details.
20+
The `init` parameter of the `solve` function allows providing an initial guess. See the [initial guess documentation](@extref OptimalControl manual-initial-guess) for more details.
2121

2222
We write a function that returns the OCP for a given final time:
2323

docs/src/tutorial-discretisation.md

Lines changed: 57 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@ using NLPModels # to retrieve information about the NLP problem
1313
using NLPModelsIpopt # to solve the problem via a direct method
1414
using Plots # to plot the solution
1515
16-
const t0 = 0 # initial time
17-
const r0 = 1 # initial altitude
18-
const v0 = 0 # initial speed
19-
const m0 = 1 # initial mass
20-
const vmax = 0.1 # maximal authorized speed
21-
const mf = 0.6 # final mass to target
16+
t0 = 0 # initial time
17+
r0 = 1 # initial altitude
18+
v0 = 0 # initial speed
19+
m0 = 1 # initial mass
20+
vmax = 0.1 # maximal authorized speed
21+
mf = 0.6 # final mass to target
2222
23-
ocp = @def begin # definition of the optimal control problem
23+
# Goddard problem function
24+
function goddard_problem()
25+
ocp = @def begin # definition of the optimal control problem
2426
2527
tf ∈ R, variable
2628
t ∈ [t0, tf], time
@@ -37,39 +39,46 @@ ocp = @def begin # definition of the optimal control problem
3739
3840
r(tf) → max
3941
40-
end;
42+
end
43+
return ocp
44+
end
45+
46+
ocp = goddard_problem()
4147
4248
# Dynamics
43-
const Cd = 310
44-
const Tmax = 3.5
45-
const β = 500
46-
const b = 2
49+
Cd = 310
50+
Tmax = 3.5
51+
β = 500
52+
b = 2
4753
4854
F0(x) = begin
55+
# Uncontrolled dynamics: gravity and drag
4956
r, v, m = x
50-
D = Cd * v^2 * exp(-β*(r - 1)) # Drag force
51-
return [ v, -D/m - 1/r^2, 0 ]
57+
D = Cd * v^2 * exp(-β*(r - 1)) # Aerodynamic drag
58+
return [ v, -D/m - 1/r^2, 0 ] # [dr/dt, dv/dt, dm/dt]
5259
end
5360
5461
F1(x) = begin
62+
# Control dynamics: thrust contribution
5563
r, v, m = x
56-
return [ 0, Tmax/m, -b*Tmax ]
64+
return [ 0, Tmax/m, -b*Tmax ] # [dr/dt, dv/dt, dm/dt] due to thrust
5765
end
5866
nothing # hide
5967
```
6068

6169
## Discretisation schemes
6270

63-
When calling `solve`, the option `disc_method=...` can be used to specify the discretization scheme. In addition to the default implicit `:midpoint` method, other available options include the implicit `:trapeze` method (also known as Crank–Nicolson), and Gauss–Legendre collocation schemes with 2 and 3 stages: `:gauss_legendre_2` and `:gauss_legendre_3`, of order 4 and 6 respectively. Note that higher-order methods typically result in larger NLP problems for the same number of time steps, and their accuracy also depends on the smoothness of the problem.
71+
When calling `solve`, the option `scheme=...` can be used to specify the discretization scheme. In addition to the default implicit `:midpoint` method, other available options include the implicit `:trapeze` method (also known as Crank–Nicolson), and Gauss–Legendre collocation schemes with 2 and 3 stages: `:gauss_legendre_2` and `:gauss_legendre_3`, of order 4 and 6 respectively. Note that higher-order methods typically result in larger NLP problems for the same number of time steps, and their accuracy also depends on the smoothness of the problem.
6472

6573
Let us first solve the problem with the default `:midpoint` method and display the solution.
6674

6775
```@example main-disc
68-
sol = solve(ocp; disc_method=:midpoint, display=false)
76+
sol = solve(ocp; scheme=:midpoint, display=false)
77+
@assert successful(sol) "Solution failed"
6978
plot(sol; size=(800, 800))
7079
```
7180

72-
Let us now compare different discretization schemes to evaluate their accuracy and performance.
81+
Let us now compare different discretization schemes to evaluate their accuracy and performance. See the [Strategy options](@id manual-solve-strategy-options) for information on default values for parameters like `tol`.
7382

7483
```@example main-disc
7584
# Solve the problem with different discretization methods
@@ -89,8 +98,9 @@ schemes = [
8998
:gauss_legendre_3
9099
]
91100
for scheme in schemes
92-
bt = @btimed solve($ocp; disc_method=$scheme, tol=1e-8, display=false)
101+
bt = @btimed solve($ocp; scheme=$scheme, tol=1e-8, display=false)
93102
local sol = bt.value
103+
#@assert successful(sol) "Solution failed for scheme=$scheme"
94104
push!(solutions, (scheme, sol))
95105
push!(data, (
96106
Scheme=scheme,
@@ -109,24 +119,28 @@ p_style = (legend=:none,)
109119
u_style = (legend=:topright,)
110120
styles = (state_style=x_style, costate_style=p_style, control_style=u_style)
111121
112-
scheme, sol = solutions[1]
113-
plt = plot(sol; label=string(scheme), styles...)
114-
for (scheme, sol) in solutions[2:end]
115-
plt = plot!(sol; label=string(scheme), styles...)
122+
plt = plot()
123+
for (i, (scheme, sol)) in enumerate(solutions)
124+
plt = plot!(sol; label=string(scheme), color=i, styles...)
116125
end
117126
plot(plt; size=(800, 800))
118127
```
119128

120129
## Large problems and AD backend
121130

122-
For some large problems, you may notice that the solver takes a long time before the iterations actually begin. This is due to the computation of sparse derivatives — specifically, the Jacobian of the constraints and the Hessian of the Lagrangian — which can be quite costly. One possible alternative is to set the option `adnlp_backend=:manual`, which uses simpler sparsity patterns. The resulting matrices are faster to compute but are also less sparse, so this represents a trade-off between automatic differentiation (AD) preparation time and the efficiency of the optimization itself.
131+
For some large problems, you may notice that the solver takes a long time before the iterations actually begin. This is due to the computation of sparse derivatives — specifically, the Jacobian of the constraints and the Hessian of the Lagrangian — which can be quite costly. One possible alternative is to set the option `backend=:manual` in the modeler, which uses simpler sparsity patterns. The resulting matrices are faster to compute but are also less sparse, so this represents a trade-off between automatic differentiation (AD) preparation time and the efficiency of the optimization itself.
123132

124133
```@example main-disc
125-
solve(ocp; disc_method=:gauss_legendre_3, grid_size=1000, adnlp_backend=:manual)
134+
disc = OptimalControl.Collocation(grid_size=1000, scheme=:gauss_legendre_3)
135+
mod = OptimalControl.ADNLP(backend=:manual)
136+
sol = OptimalControl.Ipopt()
137+
solve(ocp; discretizer=disc, modeler=mod, solver=sol)
126138
nothing # hide
127139
```
128140

129-
Let us now compare the performance of the two backends. We will use the `@btimed` macro from the `BenchmarkTools` package to measure the time taken for both the preparation of the NLP problem and the execution of the solver. Besides, we will also collect the number of non-zero elements in the Jacobian and Hessian matrices, which can be useful to understand the sparsity of the problem, thanks to the functions `get_nnzo`, `get_nnzj`, and `get_nnzj` from the `NLPModels` package. The problem is first discretized with the `direct_transcription` method and then solved with the `ipopt` solver, see the [tutorial on direct transcription](@ref tutorial-nlp) for more details.
141+
This explicit approach with strategy instances is less high-level than the descriptive method used earlier. It provides more control over the solving process and is detailed in the [explicit mode documentation](@extref OptimalControl manual-solve-explicit).
142+
143+
Let us now compare the performance of the two backends. We will use the `@btimed` macro from the `BenchmarkTools` package to measure the time taken for both the preparation of the NLP problem and the execution of the solver. Separating these measurements allows us to understand whether the bottleneck is in the discretization/modeling phase (which depends on the AD backend) or in the solver phase (which depends on the sparsity of the matrices). We use a lower-level approach with explicit strategy instances to separately measure the discretization/modeling time and the solver time.
130144

131145
```@example main-disc
132146
# DataFrame to store the results
@@ -147,30 +161,32 @@ backends = [:optimized, :manual]
147161
# Loop over the backends
148162
for adnlp_backend in backends
149163
150-
# Discretize the problem with a large grid size and Gauss-Legendre method
151-
bt = @btimed direct_transcription($ocp;
152-
disc_method=:gauss_legendre_3,
153-
grid_size=1000,
154-
adnlp_backend=$adnlp_backend,
155-
)
156-
docp = bt.value
157-
nlp = nlp_model(docp)
164+
# Create strategy instances
165+
discretizer = OptimalControl.Collocation(grid_size=1000, scheme=:gauss_legendre_3)
166+
modeler = OptimalControl.ADNLP(backend=adnlp_backend)
167+
solver = OptimalControl.Ipopt(print_level=0)
168+
169+
# Initial guess
170+
init = OptimalControl.build_initial_guess(ocp, nothing)
171+
172+
# Discretize and build NLP model
173+
# This lower-level approach with discretize/nlp_model/solve is detailed in the [tutorial on direct transcription](@ref tutorial-nlp)
174+
bt = @btimed begin
175+
docp = discretize($ocp, $discretizer)
176+
nlp_model(docp, $init, $modeler)
177+
end
158178
prepa_time = bt.time
179+
nlp = bt.value
159180
160181
# Get the number of non-zero elements
161182
nnzo = get_nnzo(nlp) # Gradient of the Objective
162183
nnzj = get_nnzj(nlp) # Jacobian of the constraints
163184
nnzh = get_nnzh(nlp) # Hessian of the Lagrangian
164185
165186
# Solve the problem
166-
bt = @btimed ipopt($nlp;
167-
print_level=0,
168-
mu_strategy="adaptive",
169-
tol=1e-8,
170-
sb="yes",
171-
)
172-
nlp_sol = bt.value
187+
bt = @btimed solve($nlp, $solver; display=false)
173188
exec_time = bt.time
189+
nlp_sol = bt.value
174190
175191
# Store the results in the DataFrame
176192
push!(data,

0 commit comments

Comments
 (0)