Skip to content

Commit 06ec202

Browse files
committed
Refine tutorial-nlp: reframe as low-level decomposition, fix terminology (canonical/explicit/descriptive), add cross-references
1 parent a5e6c6a commit 06ec202

2 files changed

Lines changed: 89 additions & 102 deletions

File tree

docs/src/tutorial-mpc.md

Lines changed: 68 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
# Navigation problem, MPC approach
22

3-
```@meta
4-
Draft = false
5-
```
6-
73
We consider a ship in a constant current $w=(w_x,w_y)$, where $\|w\|<1$. The [heading angle](https://en.wikipedia.org/wiki/Heading) is controlled, leading to the following differential equations:
84

95
```math
@@ -14,11 +10,11 @@ We consider a ship in a constant current $w=(w_x,w_y)$, where $\|w\|<1$. The [he
1410
\end{array}
1511
```
1612

17-
The state variables represent:
13+
The state and control variables represent:
1814

19-
- $(x, y)$: the ship's position in the plane
20-
- $\theta$: the heading angle (direction of the ship's velocity relative to the x-axis)
21-
- $u$: the angular velocity (rate of change of the heading angle)
15+
- the ship's position in the plane $(x, y)$
16+
- the heading angle $\theta$ (direction of the ship's velocity relative to the x-axis)
17+
- the angular velocity $u$ (rate of change of the heading angle)
2218

2319
The angular velocity is limited and normalized: $\|u(t)\| \leq 1$. There are boundary conditions at the initial time $t=0$ and at the final time $t=t_f$, on the position $(x,y)$ and on the angle $\theta$. The objective is to minimize the final time.
2420

@@ -68,6 +64,7 @@ function current(x, y) # current as a function of position
6864
end
6965
return w
7066
end
67+
nothing # hide
7168
```
7269

7370
!!! note "Plotting utility functions"
@@ -77,58 +74,58 @@ end
7774
```
7875

7976
```@example main-mpc
80-
function plot_state!(plt, x, y, θ; color=1)
81-
plot!(plt, [x], [y], marker=:circle, legend=false, color=color, markerstrokecolor=color, markersize=5, z_order=:front)
82-
quiver!(plt, [x], [y], quiver=([cos(θ)], [sin(θ)]), color=color, linewidth=2, z_order=:front)
83-
return plt
84-
end
77+
function plot_state!(plt, x, y, θ; color=1)
78+
plot!(plt, [x], [y], marker=:circle, legend=false, color=color, markerstrokecolor=color, markersize=5, z_order=:front)
79+
quiver!(plt, [x], [y], quiver=([cos(θ)], [sin(θ)]), color=color, linewidth=2, z_order=:front)
80+
return plt
81+
end
8582

86-
function plot_current!(plt; current=current, N=10, scaling=1)
87-
for x ∈ range(xlims(plt)..., N)
88-
for y ∈ range(ylims(plt)..., N)
89-
w = scaling*current(x, y)
90-
quiver!(plt, [x], [y], quiver=([w[1]], [w[2]]), color=:black, linewidth=0.5, z_order=:back)
83+
function plot_current!(plt; current=current, N=10, scaling=1)
84+
for x ∈ range(xlims(plt)..., N)
85+
for y ∈ range(ylims(plt)..., N)
86+
w = scaling*current(x, y)
87+
quiver!(plt, [x], [y], quiver=([w[1]], [w[2]]), color=:black, linewidth=0.5, z_order=:back)
88+
end
9189
end
90+
return plt
9291
end
93-
return plt
94-
end
9592

96-
function plot_trajectory!(plt, t, x, y, θ; N=5) # N: number of points where we will display θ
93+
function plot_trajectory!(plt, t, x, y, θ; N=5) # N: number of points where we will display θ
9794

98-
# trajectory
99-
plot!(plt, x.(t), y.(t), legend=false, color=1, linewidth=2, z_order=:front)
95+
# trajectory
96+
plot!(plt, x.(t), y.(t), legend=false, color=1, linewidth=2, z_order=:front)
10097

101-
if N > 0
98+
if N > 0
10299

103-
# length of the path
104-
s = 0
105-
for i ∈ 2:length(t)
106-
s += norm([x(t[i]), y(t[i])] - [x(t[i-1]), y(t[i-1])])
107-
end
100+
# length of the path
101+
s = 0
102+
for i ∈ 2:length(t)
103+
s += norm([x(t[i]), y(t[i])] - [x(t[i-1]), y(t[i-1])])
104+
end
108105

109-
# interval of length
110-
Δs = s/(N+1)
111-
tis = []
112-
s = 0
113-
for i ∈ 2:length(t)
114-
s += norm([x(t[i]), y(t[i])] - [x(t[i-1]), y(t[i-1])])
115-
if s > Δs && length(tis) < N
116-
push!(tis, t[i])
117-
s = 0
106+
# interval of length
107+
Δs = s/(N+1)
108+
tis = []
109+
s = 0
110+
for i ∈ 2:length(t)
111+
s += norm([x(t[i]), y(t[i])] - [x(t[i-1]), y(t[i-1])])
112+
if s > Δs && length(tis) < N
113+
push!(tis, t[i])
114+
s = 0
115+
end
116+
end
117+
118+
# display intermediate points
119+
for ti ∈ tis
120+
plot_state!(plt, x(ti), y(ti), θ(ti); color=1)
118121
end
119-
end
120122

121-
# display intermediate points
122-
for ti ∈ tis
123-
plot_state!(plt, x(ti), y(ti), θ(ti); color=1)
124123
end
125124

125+
return plt
126+
126127
end
127-
128-
return plt
129-
130-
end
131-
nothing # hide
128+
nothing # hide
132129
```
133130

134131
```@raw html
@@ -270,32 +267,32 @@ In the previous simulation, we assumed that the current is constant. However, fr
270267
<details><summary>Click to unfold the simulation code.</summary>
271268
```
272269

273-
```@example main-mpc
274-
function realistic_trajectory(tf, t0, x0, y0, θ0, u, current; abstol=1e-12, reltol=1e-12, saveat=[])
275-
276-
function rhs!(dq, q, dummy, t)
277-
x, y, θ = q
278-
w = current(x, y)
279-
dq[1] = w[1] + cos(θ)
280-
dq[2] = w[2] + sin(θ)
281-
dq[3] = u(t)
282-
end
283-
284-
q0 = [x0, y0, θ0]
285-
tspan = (t0, tf)
286-
ode = ODEProblem(rhs!, q0, tspan)
287-
sol = OrdinaryDiffEq.solve(ode, Tsit5(), abstol=abstol, reltol=reltol, saveat=saveat)
270+
```@example main-mpc
271+
function realistic_trajectory(tf, t0, x0, y0, θ0, u, current; abstol=1e-12, reltol=1e-12, saveat=[])
272+
273+
function rhs!(dq, q, dummy, t)
274+
x, y, θ = q
275+
w = current(x, y)
276+
dq[1] = w[1] + cos(θ)
277+
dq[2] = w[2] + sin(θ)
278+
dq[3] = u(t)
279+
end
280+
281+
q0 = [x0, y0, θ0]
282+
tspan = (t0, tf)
283+
ode = ODEProblem(rhs!, q0, tspan)
284+
sol = OrdinaryDiffEq.solve(ode, Tsit5(), abstol=abstol, reltol=reltol, saveat=saveat)
288285

289-
t = sol.t
290-
x = t -> sol(t)[1]
291-
y = t -> sol(t)[2]
292-
θ = t -> sol(t)[3]
286+
t = sol.t
287+
x = t -> sol(t)[1]
288+
y = t -> sol(t)[2]
289+
θ = t -> sol(t)[3]
293290

294-
return t, x, y, θ
295-
296-
end
297-
nothing # hide
298-
```
291+
return t, x, y, θ
292+
293+
end
294+
nothing # hide
295+
```
299296

300297
```@raw html
301298
</details>

docs/src/tutorial-nlp.md

Lines changed: 21 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
# [NLP and DOCP manipulations](@id tutorial-nlp)
22

33
```@meta
4-
Draft = false
4+
Draft = true
55
CurrentModule = OptimalControl
66
```
77

8-
We describe here some more advanced operations related to the discretized optimal control problem. When calling `solve(ocp)`, three steps are performed internally:
8+
We describe here low-level operations related to the discretized optimal control problem. The standard way to solve an OCP is to call `solve(ocp)`, available in three modes of increasing abstraction:
99

10-
1. The OCP is discretized into a DOCP (discretized optimal control problem)
11-
2. An NLP (nonlinear programming) model is built from the DOCP
12-
3. The NLP is solved, and a functional solution of the OCP is rebuilt from the NLP solution
10+
- **Descriptive mode**: `solve(ocp; grid_size=100, ...)` — highest level, uses symbols and plain values (see [basic usage](@extref OptimalControl manual-solve))
11+
- **Explicit mode**: `solve(ocp; discretizer=disc, modeler=mod, solver=sol)` — uses strategy instances as keyword arguments, missing components are filled in automatically (see [explicit mode](@extref OptimalControl manual-solve-explicit))
12+
- **Canonical mode**: `solve(ocp, init, disc, mod, solv)` — all components passed as positional arguments
1313

14-
These steps can also be done manually, which gives you more control over the solving process. This is useful when you want to:
14+
This tutorial goes one level below the canonical mode and exposes its elementary steps manually. This is useful when you want to:
1515

16-
- Use your own NLP solver
16+
- Use your own NLP solver directly
1717
- Access and inspect the NLP model
1818
- Benchmark different components separately
1919
- Fine-tune each step of the resolution
@@ -51,7 +51,7 @@ We now perform the resolution step by step, using OptimalControl strategy instan
5151

5252
### Step 1: Build the initial guess
5353

54-
First, we build an initial guess for the problem. Here we pass `nothing` to use the default initialization.
54+
First, we build an initial guess for the problem. Here we pass `nothing` to use the default initialization. For more options (constant values, time-dependent functions, warm start from a previous solution), see the [initial guess documentation](@extref OptimalControl manual-initial-guess) and the `@init` macro.
5555

5656
```@example main-nlp
5757
init = build_initial_guess(ocp, nothing)
@@ -80,7 +80,7 @@ nlp = nlp_model(docp, init, modeler)
8080
nothing # hide
8181
```
8282

83-
The `nlp` is an `ADNLPModel` (from the NLPModels ecosystem) representing the discretized nonlinear programming problem.
83+
The `nlp` is an `ADNLPModel` (from the NLPModels ecosystem) representing the discretized nonlinear programming problem. The full list of options for each strategy (`Collocation`, `ADNLP`, etc.) is described in [Strategy options](@extref OptimalControl manual-solve-strategy-options).
8484

8585
### Step 4: Solve the NLP
8686

@@ -114,6 +114,8 @@ nlp_sol_madnlp = madnlp(nlp; print_level=MadNLP.ERROR, tol=1e-8)
114114
nothing # hide
115115
```
116116

117+
Note that MadNLP can also be used via the OptimalControl wrapper `OptimalControl.MadNLP(...)`, just like `OptimalControl.Ipopt(...)` above. The full list of available solvers is given in the [explicit mode documentation](@extref OptimalControl manual-solve-explicit).
118+
117119
### Step 5: Build the OCP solution
118120

119121
Finally, we build the optimal control solution from the NLP solution and plot it. Note that the multipliers from the NLP solver are used to compute the costate.
@@ -123,40 +125,28 @@ sol = ocp_solution(docp, nlp_sol, modeler)
123125
plot(sol)
124126
```
125127

126-
## All-in-one with explicit strategies
128+
## Canonical solve
127129

128-
All the steps above can also be performed in a single `solve` call by passing the strategy instances explicitly:
130+
All the steps above are exactly what the canonical `solve` performs internally. They can be condensed into a single call:
129131

130132
```@example main-nlp
131133
sol = solve(ocp, init, discretizer, modeler, solver; display=false)
132134
plot(sol)
133135
```
134136

135-
This is the explicit mode of `solve`, which gives you full control over each component while keeping the interface simple. See the [explicit mode documentation](@extref OptimalControl manual-solve-explicit) for more details.
137+
This is the **canonical mode** of `solve` (Layer 3): all components are fully specified and passed as positional arguments — no defaults, no auto-completion. The two higher-level modes sit above this:
138+
139+
- **Explicit mode** — pass strategy instances as keyword arguments; missing components are resolved automatically: `solve(ocp; discretizer=disc, modeler=mod, solver=sol)`. See [explicit mode](@extref OptimalControl manual-solve-explicit).
140+
- **Descriptive mode** — pass plain values and symbols; everything is built internally: `solve(ocp; grid_size=100, tol=1e-8)`. See [basic usage](@extref OptimalControl manual-solve).
136141

137142
## Initial guess and warm start
138143

139-
An initial guess can be passed at different stages of the process.
144+
For a detailed presentation of all the ways to build an initial guess — constant values, time-dependent functions, warm start from a previous solution, or using the `@init` macro — see the [initial guess documentation](@extref OptimalControl manual-initial-guess).
140145

141-
If you have a previous solution, you can use it to warm-start a new resolution:
146+
In the step-by-step approach, the initial guess is passed to `nlp_model` via `build_initial_guess`. In canonical mode, it is passed as a positional argument:
142147

143148
```@example main-nlp
144-
# Use the previous solution as initial guess
145149
init_warm = build_initial_guess(ocp, sol)
146-
147-
# Discretize and solve with warm start
148-
docp_warm = discretize(ocp, discretizer)
149-
nlp_warm = nlp_model(docp_warm, init_warm, modeler)
150-
nlp_sol_warm = solve(nlp_warm, solver; display=false)
151-
sol_warm = ocp_solution(docp_warm, nlp_sol_warm, modeler)
152-
153-
println("Iterations with warm start: ", iterations(sol_warm))
154-
println("Iterations without warm start: ", iterations(sol))
155-
```
156-
157-
You can also pass the initial guess directly to `solve` in explicit mode:
158-
159-
```@example main-nlp
160-
sol_warm2 = solve(ocp, init_warm, discretizer, modeler, solver; display=false)
161-
println("Objective value: ", objective(sol_warm2))
150+
sol_warm = solve(ocp, init_warm, discretizer, modeler, solver; display=false)
151+
println("Objective value: ", objective(sol_warm))
162152
```

0 commit comments

Comments
 (0)