|
| 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