From 0badda9fa1484373955a6ba45611ed53cdeda931 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Wed, 2 Sep 2026 09:24:24 +0200 Subject: [PATCH] docs: drop docs/attic, close audit F3 (E6 phase B) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The attic (24 pre-v2.1 pages) was kept only for harvesting by the rewritten site. The attic audit (campaign phase N) confirmed coverage is ~1:1 and E7 (#938) closed the one content regression it found (F1), so the directory is now dead weight. Nothing in `docs/src/` or `docs/make.jl` references it; the full docs build is unchanged (exit 0, only the six pre-existing @extref items in the tolerated warnonly classes). Also folds in audit finding F3: `solve/choosing-a-method.md` named `OptimalControl.get_strategy_registry()`, which `99-api-coverage.md` §12 says must not be documented — rephrased to "internal and not re-exported". The cahier §12 acceptance walk and F4 (the stale 193 symbol count in `99-api-coverage.md`) are deferred to a dedicated follow-up PR. Co-Authored-By: Claude Sonnet 5 --- docs/attic/README.md | 4 - docs/attic/example-control-and-variable.md | 381 -------- docs/attic/example-control-free.md | 378 -------- .../attic/example-double-integrator-energy.md | 169 ---- docs/attic/example-double-integrator-time.md | 204 ---- docs/attic/example-singular-control.md | 351 ------- docs/attic/example-state-constraint.md | 518 ---------- docs/attic/manual-abstract.md | 646 ------------- docs/attic/manual-ai-llm.md | 202 ---- docs/attic/manual-differential-geometry.md | 634 ------------- docs/attic/manual-flow-ocp.md | 688 -------------- docs/attic/manual-flow-others.md | 108 --- docs/attic/manual-initial-guess.md | 627 ------------ docs/attic/manual-macro-free.md | 898 ------------------ docs/attic/manual-model.md | 721 -------------- docs/attic/manual-plot.md | 454 --------- docs/attic/manual-solution.md | 437 --------- docs/attic/manual-solve-advanced.md | 208 ---- docs/attic/manual-solve-explicit.md | 282 ------ docs/attic/manual-solve-gpu.md | 175 ---- docs/attic/manual-solve.md | 304 ------ docs/attic/public.md | 160 ---- docs/attic/subpackages.md | 10 - docs/attic/tutorial.md | 499 ---------- docs/reports/README.md | 2 +- docs/src/solve/choosing-a-method.md | 5 +- 26 files changed, 3 insertions(+), 9062 deletions(-) delete mode 100644 docs/attic/README.md delete mode 100644 docs/attic/example-control-and-variable.md delete mode 100644 docs/attic/example-control-free.md delete mode 100644 docs/attic/example-double-integrator-energy.md delete mode 100644 docs/attic/example-double-integrator-time.md delete mode 100644 docs/attic/example-singular-control.md delete mode 100644 docs/attic/example-state-constraint.md delete mode 100644 docs/attic/manual-abstract.md delete mode 100644 docs/attic/manual-ai-llm.md delete mode 100644 docs/attic/manual-differential-geometry.md delete mode 100644 docs/attic/manual-flow-ocp.md delete mode 100644 docs/attic/manual-flow-others.md delete mode 100644 docs/attic/manual-initial-guess.md delete mode 100644 docs/attic/manual-macro-free.md delete mode 100644 docs/attic/manual-model.md delete mode 100644 docs/attic/manual-plot.md delete mode 100644 docs/attic/manual-solution.md delete mode 100644 docs/attic/manual-solve-advanced.md delete mode 100644 docs/attic/manual-solve-explicit.md delete mode 100644 docs/attic/manual-solve-gpu.md delete mode 100644 docs/attic/manual-solve.md delete mode 100644 docs/attic/public.md delete mode 100644 docs/attic/subpackages.md delete mode 100644 docs/attic/tutorial.md diff --git a/docs/attic/README.md b/docs/attic/README.md deleted file mode 100644 index 8394551bb..000000000 --- a/docs/attic/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# docs/attic - -Pre-v2.1 documentation pages kept here for harvesting. -This directory is deleted in PR 12. diff --git a/docs/attic/example-control-and-variable.md b/docs/attic/example-control-and-variable.md deleted file mode 100644 index 1343fef77..000000000 --- a/docs/attic/example-control-and-variable.md +++ /dev/null @@ -1,381 +0,0 @@ -# [Problems mixing control and variable](@id example-control-and-variable) - -Problems mixing control and variable are optimal control problems that contain both a control variable and a constant parameter (variable) to optimize. They extend control-free problems by adding an explicit control input to the dynamics, while still optimizing constant parameters. - -Such problems are used for: - -- Identifying unknown parameters and control inputs simultaneously from observed data -- Finding optimal parameters and associated control laws for a given performance criterion - -This page demonstrates two examples that extend the control-free problems by adding a control input and a quadratic control cost term. - -First, we import the necessary packages: - -```@example main-growth-cv -using OptimalControl -using NLPModelsIpopt -using Plots -``` - -## Example 1: Exponential growth rate estimation with control - -Consider a system with exponential growth and an additive control: - -```math -\dot{x}(t) = \lambda \cdot x(t) + u(t), \quad x(0) = 2 -``` - -where $\lambda$ is an unknown growth rate parameter and $u(t)$ is a control input. We have observed data with some perturbations and want to estimate $\lambda$ and the optimal control $u$ by minimizing the squared error plus a quadratic control cost: - -```math -\min_{\lambda, u} \int_0^{2} \bigl( (x(t) - x_{\text{obs}}(t))^2 + \frac{1}{2} u(t)^2 \bigr) \, \mathrm{d}t -``` - -The underlying model has $\lambda = 0.5$, but the observed data includes perturbations. - -### [Problem definition](@id example-control-and-variable-problem-1) - -```@example main-growth-cv -# observed data (analytical solution with λ = 0.5) -λ_true = 0.5 -model(t) = 2 * exp(λ_true * t) -perturbation(t) = 2e-1*sin(4π*t) -data(t) = model(t) + perturbation(t) - -# optimal control problem (parameter estimation with control) -t0 = 0; tf = 2; x0 = 2 -ocp = @def begin - λ ∈ R, variable # growth rate to estimate - t ∈ [t0, tf], time - x ∈ R, state - u ∈ R, control - - x(t0) == x0 - ẋ(t) == λ * x(t) + u(t) - - ∫((x(t) - data(t))^2 + 0.5*u(t)^2) → min # fit to observed data with control cost -end -nothing # hide -``` - -### [Direct method](@id example-control-and-variable-direct-1) - -```@example main-growth-cv -direct_sol = solve(ocp; grid_size=20, display=false) -``` - -```@example main-growth-cv -println("Estimated growth rate: λ = ", variable(direct_sol)) -println("Objective value: ", objective(direct_sol)) -nothing # hide -``` - -```@example main-growth-cv -# plot direct solution -plt = plot(direct_sol; size=(800, 600), label="Direct") - -# Add data on first plot -t_grid = time_grid(direct_sol) -plot!(plt, t_grid, data.(t_grid); subplot=1, line=:dot, lw=2, label="Data", color=:black) -``` - -The estimated parameter should be close to $\lambda \approx 0.5$. - -### [Indirect method](@id example-control-and-variable-indirect-1) - -We now solve the same problem using an indirect shooting method based on Pontryagin's Maximum Principle. First, we import the necessary packages: - -```@example main-growth-cv -using OrdinaryDiffEq # ODE solver -using NonlinearSolve # Nonlinear solver -``` - -For problems mixing control and variable, we use an **augmented Hamiltonian** approach with the maximising control. The pseudo-Hamiltonian for this problem is: - -```math -H(t, x, p, u, \lambda) = p(\lambda x + u) - (x - x_{\text{obs}}(t))^2 - \frac{1}{2} u^2 -``` - -The maximisation condition $\partial H/\partial u = 0$ gives the control in feedback form: - -```math -u(t, x, p, \lambda) = p -``` - -To handle the variable parameter $\lambda$, we treat it as an additional state with zero dynamics. This gives us the augmented system with state $(x, \lambda)$ and costate $(p, p_\lambda)$, where: - -```math -\begin{aligned} -\frac{\mathrm{d}x}{\mathrm{d}t} &= \frac{\partial H}{\partial p} = \lambda x + u \\ -\frac{\mathrm{d}\lambda}{\mathrm{d}t} &= 0 \quad \text{(constant parameter)} \\ -\frac{\mathrm{d}p}{\mathrm{d}t} &= -\frac{\partial H}{\partial x} = -p\lambda + 2(x - x_{\text{obs}}(t)) \\ -\frac{\mathrm{d}p_\lambda}{\mathrm{d}t} &= -\frac{\partial H}{\partial \lambda} = -p x -\end{aligned} -``` - -Using the maximising control $u = p$, the dynamics of $x$ becomes $\dot x = \lambda x + p$. - -The transversality condition for the variable parameter requires $p_\lambda(t_f) - p_\lambda(t_0) = 0$. Assuming $p_\lambda(t_0) = 0$, we have to satisfy: - -```math -p_\lambda(t_f) = -\int_{t_0}^{t_f} \frac{\partial H}{\partial \lambda}(t, x(t), p(t), \lambda) \, \mathrm{d}t = 0 -``` - -We use CTFlows' `augment=true` feature to automatically compute $p_\lambda(t_f)$ without manually constructing the augmented system. - -```@example main-growth-cv -# Maximising control from Hamiltonian (non-autonomous: t is required) -u(t, x, p, λ) = p - -# Create Hamiltonian flow from OCP with control -f = Flow(ocp, u) -nothing # hide -``` - -!!! note - - For more details about the flow construction, see [this page](@ref manual-flow-others). - -The shooting function enforces the transversality conditions $p(t_f) = 0$ and $p_\lambda(t_f) = 0$. Using `augment=true`, the flow automatically returns $(x(t_f), p(t_f), p_\lambda(t_f))$, with $p_\lambda(t_0) = 0$ by construction. - -```@example main-growth-cv -# Shooting function: S(p0, λ) = (p(tf), pλ(tf)) -# We want both components to be zero at tf -function shoot!(s, p0, λ) - _, px_tf, pλ_tf = f(t0, x0, p0, tf, λ; augment=true) - s[1] = px_tf - s[2] = pλ_tf - return nothing -end - -# Auxiliary in-place NLE function -nle!(s, y, _) = shoot!(s, y...) -nothing # hide -``` - -We use the direct solution to initialize the shooting method: - -```@example main-growth-cv -# Extract solution from direct method for initialization -p_direct = costate(direct_sol) -λ_direct = variable(direct_sol) - -# Initial guess -p0_guess = p_direct(t0) -λ_guess = λ_direct - -# NLE problem with initial guess (2 unknowns: p0, λ) -prob_indirect = NonlinearProblem(nle!, [p0_guess, λ_guess]) - -# Solve shooting equations -shooting_sol = solve(prob_indirect; show_trace=Val(false)) -p0_sol, λ_sol = shooting_sol.u - -println("Indirect solution:") -println("Initial costate: p0 = ", p0_sol) -println("Parameter: λ = ", λ_sol) -nothing # hide -``` - -Finally, we compute and plot the indirect solution: - -```@example main-growth-cv -# Compute and plot indirect solution -indirect_sol = f((t0, tf), x0, p0_sol, λ_sol; saveat=range(t0, tf, 200)) -plot!(plt, indirect_sol; linestyle=:dash, lw=2, label="Indirect", color=2) -``` - -The direct and indirect solutions match closely, both fitting the perturbed observed data. - -## Example 2: Harmonic oscillator pulsation optimization with control - -```@setup main-harmonic-cv -using OptimalControl -using NLPModelsIpopt -using Plots -using OrdinaryDiffEq # ODE solver -using NonlinearSolve # Nonlinear solver -``` - -Consider a harmonic oscillator with an additive control: - -```math -\ddot{q}(t) = -\omega^2 q(t) + u(t) -``` - -with initial conditions $q(0) = 1$, $\dot{q}(0) = 0$ and final condition $q(1) = 0$. We want to find the minimal pulsation $\omega$ and optimal control $u$ satisfying these constraints: - -```math - \begin{aligned} - & \text{Minimise} && \omega^2 + \frac{1}{2}\int_0^1 u(t)^2 \, \mathrm{d}t \\ - & \text{subject to} \\ - & && \ddot{q}(t) = -\omega^2 q(t) + u(t), \\[1.0em] - & && q(0) = 1, \quad \dot{q}(0) = 0, \\[0.5em] - & && q(1) = 0. - \end{aligned} -``` - -Without the control term ($u = 0$), the analytical solution is $\omega = \pi/2 \approx 1.5708$, giving $q(t) = \cos(\pi t / 2)$. - -### [Problem definition](@id example-control-and-variable-problem-2) - -```@example main-harmonic-cv -# optimal control problem (pulsation optimization with control) -q0 = 1; v0 = 0 -t0 = 0; tf = 1 -ocp = @def begin - ω ∈ R, variable # pulsation to optimize - t ∈ [t0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - - q(t0) == q0 - v(t0) == v0 - q(tf) == 0.0 # final condition - - ẋ(t) == [v(t), -ω^2 * q(t) + u(t)] - - ω^2 + 0.5∫(u(t)^2) → min # minimize pulsation with control cost -end -nothing # hide -``` - -### [Direct method](@id example-control-and-variable-direct-2) - -```@example main-harmonic-cv -direct_sol = solve(ocp; grid_size=20, display=false) -``` - -```@example main-harmonic-cv -println("Optimal pulsation: ω = ", variable(direct_sol)) -println("Objective value: ", objective(direct_sol)) -nothing # hide -``` - -```@example main-harmonic-cv -plt = plot(direct_sol; size=(800, 600), label="Direct") -``` - -### [Indirect method](@id example-control-and-variable-indirect-2) - -We now solve the same problem using an indirect shooting method. For problems mixing control and variable, we use an **augmented Hamiltonian** approach with the maximising control. The pseudo-Hamiltonian for this problem is: - -```math -H(x, p, u, \omega) = p_1 v + p_2(-\omega^2 q + u) - \frac{1}{2} u^2 -``` - -The maximisation condition $\partial H/\partial u = 0$ gives the control in feedback form: - -```math -u(x, p, \omega) = p_2 -``` - -To handle the variable parameter $\omega$, we treat it as an additional state with zero dynamics. This gives us the augmented system with state $(q, v, \omega)$ and costate $(p_1, p_2, p_\omega)$, where: - -```math -\begin{aligned} -\frac{\mathrm{d}q}{\mathrm{d}t} &= \frac{\partial H}{\partial p_1} = v \\ -\frac{\mathrm{d}v}{\mathrm{d}t} &= \frac{\partial H}{\partial p_2} = -\omega^2 q + u \\ -\frac{\mathrm{d}\omega}{\mathrm{d}t} &= 0 \quad \text{(constant parameter)} \\ -\frac{\mathrm{d}p_1}{\mathrm{d}t} &= -\frac{\partial H}{\partial q} = \omega^2 p_2 \\ -\frac{\mathrm{d}p_2}{\mathrm{d}t} &= -\frac{\partial H}{\partial v} = -p_1 \\ -\frac{\mathrm{d}p_\omega}{\mathrm{d}t} &= -\frac{\partial H}{\partial \omega} = 2\omega q p_2 -\end{aligned} -``` - -Using the maximising control $u = p_2$, the dynamics of $v$ becomes $\dot v = -\omega^2 q + p_2$. - -For this problem with a Mayer cost $g(\omega) = \omega^2$, the transversality condition for the variable parameter is: - -```math -p_\omega(t_f) - p_\omega(t_0)= -\frac{\partial g}{\partial \omega} = -2\omega -``` - -Assuming $p_\omega(t_0) = 0$, we have: - -```math -p_\omega(t_f) = -\int_{t_0}^{t_f} \frac{\partial H}{\partial \omega}(t, x(t), p(t), \omega) \, \mathrm{d}t = -2\omega -``` - -We use CTFlows' `augment=true` feature to automatically compute $p_\omega(t_f)$ without manually constructing the augmented system: - -```@example main-harmonic-cv -# Maximising control from Hamiltonian -u(x, p, ω) = p[2] - -# Create Hamiltonian flow from OCP with control -f = Flow(ocp, u) -nothing # hide -``` - -!!! note - - For more details about the flow construction, see [this page](@ref manual-flow-others). - -The shooting function enforces the conditions: - -- Final condition: $q(t_f) = 0$ -- Free final velocity: $p_2(t_f) = 0$ -- Transversality condition for Mayer cost: $p_\omega(t_f) + 2\omega = 0$ - -Using `augment=true`, the flow automatically returns $(x(t_f), p(t_f), p_\omega(t_f))$, with $p_\omega(t_0) = 0$ by construction. - -```@example main-harmonic-cv -# Shooting function: S(p0, ω) -function shoot!(s, p0, ω) - x_tf, p_tf, pω_tf = f(t0, [q0, v0], p0, tf, ω; augment=true) - q_tf = x_tf[1] - pv_tf = p_tf[2] - s[1] = q_tf # q(tf) = 0 - s[2] = pv_tf # p2(tf) = 0 (free final velocity) - s[3] = pω_tf + 2ω # pω(tf) + 2ω = 0 (Mayer cost transversality) - return nothing -end - -# Auxiliary in-place NLE function -nle!(s, y, _) = shoot!(s, y[1:2], y[3]) -nothing # hide -``` - -We use the direct solution to initialize the shooting method: - -```@example main-harmonic-cv -# Extract solution from direct method for initialization -p_direct = costate(direct_sol) -ω_direct = variable(direct_sol) - -# Initial guess -p0_guess = p_direct(t0) -ω_guess = ω_direct - -# NLE problem with initial guess -prob_indirect = NonlinearProblem(nle!, [p0_guess..., ω_guess]) - -# Solve shooting equations -shooting_sol = solve(prob_indirect; show_trace=Val(false)) -p0_sol, ω_sol = shooting_sol.u[1:2], shooting_sol.u[3] - -println("Indirect solution:") -println("Initial costate: p0 = ", p0_sol) -println("Parameter: ω = ", ω_sol) -nothing # hide -``` - -Finally, we compute and plot the indirect solution: - -```@example main-harmonic-cv -# Compute and plot indirect solution -indirect_sol = f((t0, tf), [q0, v0], p0_sol, ω_sol; saveat=range(t0, tf, 200)) -plot!(plt, indirect_sol; linestyle=:dash, lw=2, label="Indirect", color=2) -``` - -The direct and indirect solutions match closely. - -!!! note "Applications" - - Problems mixing control and variable appear in many contexts: - - **System identification**: simultaneously estimating physical parameters (mass, damping, stiffness) and control inputs from experimental data - - **Optimal design**: finding optimal geometric or physical parameters (length, stiffness, etc.) together with associated control laws - - **Inverse problems**: reconstructing unknown inputs or initial conditions from partial observations while optimizing system parameters - - See the [syntax documentation](@ref manual-abstract-control-free) for more details on defining control-free problems, and the [flow documentation](@ref manual-flow-ocp) for problems with variables and controls. diff --git a/docs/attic/example-control-free.md b/docs/attic/example-control-free.md deleted file mode 100644 index febfd9452..000000000 --- a/docs/attic/example-control-free.md +++ /dev/null @@ -1,378 +0,0 @@ -# [Control-free problems](@id example-control-free) - -Control-free problems are optimal control problems without a control variable. They are used for **optimizing constant parameters in dynamical systems**, such as: - -- Identifying unknown parameters from observed data (parameter estimation) -- Finding optimal parameters for a given performance criterion - -This page demonstrates two simple examples with known analytical solutions. - -First, we import the necessary packages: - -```@example main-growth -using OptimalControl -using NLPModelsIpopt -using Plots -``` - -## Example 1: Exponential growth rate estimation - -Consider a system with exponential growth: - -```math -\dot{x}(t) = \lambda \cdot x(t), \quad x(0) = 2 -``` - -where $\lambda$ is an unknown growth rate parameter. We have observed data with some perturbations and want to estimate $\lambda$ by minimizing the squared error: - -```math -\min_{\lambda} \int_0^{2} (x(t) - x_{\text{obs}}(t))^2 \, \mathrm{d}t -``` - -The underlying model has $\lambda = 0.5$, but the observed data includes perturbations. - -### [Problem definition](@id example-control-free-problem-1) - -```@example main-growth -# observed data (analytical solution with λ = 0.5) -λ_true = 0.5 -model(t) = 2 * exp(λ_true * t) -perturbation(t) = 2e-1*sin(4π*t) -data(t) = model(t) + perturbation(t) - -# optimal control problem (parameter estimation) -t0 = 0; tf = 2; x0 = 2 -ocp = @def begin - λ ∈ R, variable # growth rate to estimate - t ∈ [t0, tf], time - x ∈ R, state - - x(t0) == x0 - ẋ(t) == λ * x(t) - - ∫((x(t) - data(t))^2) → min # fit to observed data -end -nothing # hide -``` - -### Direct method - -```@example main-growth -direct_sol = solve(ocp; grid_size=20, display=false) -``` - -```@example main-growth -println("Estimated growth rate: λ = ", variable(direct_sol)) -println("Objective value: ", objective(direct_sol)) -nothing # hide -``` - -```@example main-growth -# plot direct solution -plt = plot(direct_sol; size=(800, 400), label="Direct") - -# Add data on first plot -t_grid = time_grid(direct_sol) -plot!(plt, t_grid, data.(t_grid); subplot=1, line=:dot, lw=2, label="Data", color=:black) -``` - -The estimated parameter should be close to $\lambda \approx 0.5$. - -### Indirect method - -We now solve the same problem using an indirect shooting method based on Pontryagin's Maximum Principle. First, we import the necessary packages: - -```@example main-growth -using OrdinaryDiffEq # ODE solver -using NonlinearSolve # Nonlinear solver -``` - -For control-free problems with a variable parameter, we use an **augmented Hamiltonian** approach. The Hamiltonian for this problem is: - -```math -H(t, x, p, \lambda) = p \lambda x - (x - x_{\text{obs}}(t))^2 -``` - -To handle the variable parameter $\lambda$, we treat it as an additional state with zero dynamics. This gives us the augmented system with state $(x, \lambda)$ and costate $(p, p_\lambda)$, where: - -```math -\begin{aligned} -\frac{\mathrm{d}x}{\mathrm{d}t} &= \frac{\partial H}{\partial p} = \lambda x \\ -\frac{\mathrm{d}\lambda}{\mathrm{d}t} &= 0 \quad \text{(constant parameter)} \\ -\frac{\mathrm{d}p}{\mathrm{d}t} &= -\frac{\partial H}{\partial x} = -p\lambda + 2(x - x_{\text{obs}}(t)) \\ -\frac{\mathrm{d}p_\lambda}{\mathrm{d}t} &= -\frac{\partial H}{\partial \lambda} = -px -\end{aligned} -``` - -The transversality condition for the variable parameter requires $p_\lambda(t_f) - p_\lambda(t_0) = 0$. Assuming $p_\lambda(t_0) = 0$, we have to satisfy: - -```math -p_\lambda(t_f) = -\int_{t_0}^{t_f} \frac{\partial H}{\partial \lambda}(t, x(t), p(t), \lambda) \, \mathrm{d}t = 0 -``` - -We use CTFlows' `augment=true` feature to automatically compute $p_\lambda(t_f)$ without manually constructing the augmented system. - -```@example main-growth -# Create Hamiltonian flow from OCP -f = Flow(ocp) -nothing # hide -``` - -!!! note - - For more details about the flow construction, see [this page](@ref manual-flow-others). - -The shooting function enforces the transversality conditions $p(t_f) = 0$ and $p_\lambda(t_f) = 0$. Using `augment=true`, the flow automatically returns $(x(t_f), p(t_f), p_\lambda(t_f))$, with $p_\lambda(t_0) = 0$ by construction. - -```@example main-growth -# Shooting function: S(p0, λ) = (p(tf), pλ(tf)) -# We want both components to be zero at tf -function shoot!(s, p0, λ) - _, px_tf, pλ_tf = f(t0, x0, p0, tf, λ; augment=true) - s[1] = px_tf - s[2] = pλ_tf - return nothing -end - -# Auxiliary in-place NLE function -nle!(s, y, _) = shoot!(s, y...) -nothing # hide -``` - -We use the direct solution to initialize the shooting method: - -```@example main-growth -# Extract solution from direct method for initialization -p_direct = costate(direct_sol) -λ_direct = variable(direct_sol) - -# Initial guess -p0_guess = p_direct(t0) -λ_guess = λ_direct - -# NLE problem with initial guess (2 unknowns: p0, λ) -prob_indirect = NonlinearProblem(nle!, [p0_guess, λ_guess]) - -# Solve shooting equations -shooting_sol = solve(prob_indirect; show_trace=Val(false)) -p0_sol, λ_sol = shooting_sol.u - -println("Indirect solution:") -println("Initial costate: p0 = ", p0_sol) -println("Parameter: λ = ", λ_sol) -nothing # hide -``` - -Finally, we compute and plot the indirect solution: - -```@example main-growth -# Compute and plot indirect solution -indirect_sol = f((t0, tf), x0, p0_sol, λ_sol; saveat=range(t0, tf, 200)) -plot!(plt, indirect_sol; linestyle=:dash, lw=2, label="Indirect", color=2) -``` - -The direct and indirect solutions match closely, both fitting the perturbed observed data. - -## Example 2: Harmonic oscillator pulsation optimization - -```@setup main-harmonic -using OptimalControl -using NLPModelsIpopt -using Plots -using OrdinaryDiffEq # ODE solver -using NonlinearSolve # Nonlinear solver -``` - -Consider a harmonic oscillator: - -```math -\ddot{q}(t) = -\omega^2 q(t) -``` - -with initial conditions $q(0) = 1$, $\dot{q}(0) = 0$ and final condition $q(1) = 0$. We want to find the minimal pulsation $\omega$ satisfying these constraints: - -```math - \begin{aligned} - & \text{Minimise} && \omega^2 \\ - & \text{subject to} \\ - & && \ddot{q}(t) = -\omega^2 q(t), \\[1.0em] - & && q(0) = 1, \quad \dot{q}(0) = 0, \\[0.5em] - & && q(1) = 0. - \end{aligned} -``` - -The analytical solution is $\omega = \pi/2 \approx 1.5708$, giving $q(t) = \cos(\pi t / 2)$. - -### [Problem definition](@id example-control-free-problem-2) - -```@example main-harmonic -# optimal control problem (pulsation optimization) -q0 = 1; v0 = 0 -t0 = 0; tf = 1 -ocp = @def begin - ω ∈ R, variable # pulsation to optimize - t ∈ [t0, tf], time - x = (q, v) ∈ R², state - - q(t0) == q0 - v(t0) == v0 - q(tf) == 0.0 # final condition - - ẋ(t) == [v(t), -ω^2 * q(t)] - - ω^2 → min # minimize pulsation -end -nothing # hide -``` - -### [Direct method](@id example-control-free-direct-2) - -```@example main-harmonic -direct_sol = solve(ocp; grid_size=20, display=false) -``` - -```@example main-harmonic -println("Optimal pulsation: ω = ", variable(direct_sol)) -println("Objective value: ω² = ", objective(direct_sol)) -println("Expected: ω = π/2 ≈ 1.5708, ω² ≈ 2.4674") -nothing # hide -``` - -```@example main-harmonic -plot(direct_sol; size=(800, 400)) -``` - -The optimal pulsation should be close to $\omega = \pi/2 \approx 1.5708$, and the objective $\omega^2 \approx 2.4674$. - -### Comparison with analytical solutions - -For the harmonic oscillator, we can compare the numerical solution with the analytical one: - -```@example main-harmonic -# analytical solution -t_analytical = range(0, 1, 100) -q_analytical = cos.(π * t_analytical / 2) -v_analytical = -(π/2) * sin.(π * t_analytical / 2) - -# plot comparison -plt = plot(direct_sol; size=(800, 600), label="Direct") -plot!(plt, t_analytical, q_analytical; - label="q (analytical)", linestyle=:dash, linewidth=2, subplot=1) -plot!(plt, t_analytical, v_analytical; - label="v (analytical)", linestyle=:dash, linewidth=2, subplot=2) -``` - -The numerical and analytical solutions should match very closely. - -### [Indirect method](@id example-control-free-indirect-2) - -We now solve the same problem using an indirect shooting method. For this control-free problem with a variable parameter, we use an **augmented Hamiltonian** approach. The Hamiltonian for this problem is: - -```math -H(x, p, \omega) = p_1 v + p_2 (-\omega^2 q) -``` - -To handle the variable parameter $\omega$, we treat it as an additional state with zero dynamics. This gives us the augmented system with state $(q, v, \omega)$ and costate $(p_1, p_2, p_\omega)$, where: - -```math -\begin{aligned} -\frac{\mathrm{d}q}{\mathrm{d}t} &= \frac{\partial H}{\partial p_1} = v \\ -\frac{\mathrm{d}v}{\mathrm{d}t} &= \frac{\partial H}{\partial p_2} = -\omega^2 q \\ -\frac{\mathrm{d}\omega}{\mathrm{d}t} &= 0 \quad \text{(constant parameter)} \\ -\frac{\mathrm{d}p_1}{\mathrm{d}t} &= -\frac{\partial H}{\partial q} = \omega^2 p_2 \\ -\frac{\mathrm{d}p_2}{\mathrm{d}t} &= -\frac{\partial H}{\partial v} = -p_1 \\ -\frac{\mathrm{d}p_\omega}{\mathrm{d}t} &= -\frac{\partial H}{\partial \omega} = 2\omega q p_2 -\end{aligned} -``` - -For this problem with a Mayer cost $g(\omega) = \omega^2$, the transversality condition for the variable parameter is: - -```math -p_\omega(t_f) - p_\omega(t_0)= -\frac{\partial g}{\partial \omega} = -2\omega -``` - -Assuming $p_\omega(t_0) = 0$, we have: - -```math -p_\omega(t_f) = -\int_{t_0}^{t_f} \frac{\partial H}{\partial \omega}(t, x(t), p(t), \omega) \, \mathrm{d}t = -2\omega -``` - -We use CTFlows' `augment=true` feature to automatically compute $p_\omega(t_f)$ without manually constructing the augmented system: - -```@example main-harmonic -# Create Hamiltonian flow from OCP -f = Flow(ocp) -nothing # hide -``` - -!!! note - - For more details about the flow construction, see [this page](@ref manual-flow-others). - -The shooting function enforces the conditions: - -- Final condition: $q(t_f) = 0$ -- Free final velocity: $p_2(t_f) = 0$ -- Transversality condition for Mayer cost: $p_\omega(t_f) + 2\omega = 0$ - -Using `augment=true`, the flow automatically returns $(x(t_f), p(t_f), p_\omega(t_f))$, with $p_\omega(t_0) = 0$ by construction. - -```@example main-harmonic -# Shooting function: S(p0, ω) -function shoot!(s, p0, ω) - x_tf, p_tf, pω_tf = f(t0, [q0, v0], p0, tf, ω; augment=true) - q_tf = x_tf[1] - pv_tf = p_tf[2] - s[1] = q_tf # q(tf) = 0 - s[2] = pv_tf # p2(tf) = 0 (free final velocity) - s[3] = pω_tf + 2ω # pω(tf) + 2ω = 0 (Mayer cost transversality) - return nothing -end - -# Auxiliary in-place NLE function -nle!(s, y, _) = shoot!(s, y[1:2], y[3]) -nothing # hide -``` - -We use the direct solution to initialize the shooting method: - -```@example main-harmonic -# Extract solution from direct method for initialization -p_direct = costate(direct_sol) -ω_direct = variable(direct_sol) - -# Initial guess -p0_guess = p_direct(t0) -ω_guess = ω_direct - -# NLE problem with initial guess -prob_indirect = NonlinearProblem(nle!, [p0_guess..., ω_guess]) - -# Solve shooting equations -shooting_sol = solve(prob_indirect; show_trace=Val(false)) -p0_sol, ω_sol = shooting_sol.u[1:2], shooting_sol.u[3] - -println("Indirect solution:") -println("Initial costate: p0 = ", p0_sol) -println("Parameter: ω = ", ω_sol) -nothing # hide -``` - -Finally, we compute and plot the indirect solution: - -```@example main-harmonic -# Compute and plot indirect solution -indirect_sol = f((t0, tf), [q0, v0], p0_sol, ω_sol; saveat=range(t0, tf, 200)) -plot!(plt, indirect_sol; linestyle=:dash, lw=2, label="Indirect", color=2) -``` - -The direct and indirect solutions match closely, both finding the optimal pulsation $\omega \approx \pi/2$. - -!!! note "Applications" - - Control-free problems appear in many contexts: - - **System identification**: estimating physical parameters (mass, damping, stiffness) from experimental data - - **Optimal design**: finding optimal geometric or physical parameters (length, stiffness, etc.) - - **Inverse problems**: reconstructing unknown inputs or initial conditions from partial observations - - See the [syntax documentation](@ref manual-abstract-control-free) for more details on defining control-free problems. diff --git a/docs/attic/example-double-integrator-energy.md b/docs/attic/example-double-integrator-energy.md deleted file mode 100644 index 59b67be91..000000000 --- a/docs/attic/example-double-integrator-energy.md +++ /dev/null @@ -1,169 +0,0 @@ -# [Double integrator: energy minimisation](@id example-double-integrator-energy) - -Let us consider a wagon moving along a rail, whose acceleration can be controlled by a force $u$. -We denote by $x = (q, v)$ the state of the wagon, where $q$ is the position and $v$ the velocity. - -```@raw html - -``` - -We assume that the mass is constant and equal to one, and that there is no friction. The dynamics are given by - -```math - \dot q(t) = v(t), \quad \dot v(t) = u(t),\quad u(t) \in \R, -``` - -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 - -```math - \frac{1}{2}\int_{0}^{1} u^2(t) \, \mathrm{d}t -``` - -starting from $x(0) = (-1, 0)$ and aiming to reach the target $x(1) = (0, 0)$. - -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. - -```@example main -using OptimalControl -using NLPModelsIpopt -using Plots -``` - -## Optimal control problem - -Let us define the problem with the [`@def`](@ref) macro: - -```@raw html -
-
-``` - -```@example main -t0 = 0; tf = 1; x0 = [-1, 0]; xf = [0, 0] - -ocp = @def begin - t ∈ [t0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - - x(t0) == x0 - x(tf) == xf - - ẋ(t) == [v(t), u(t)] - - 0.5∫( u(t)^2 ) → min -end -nothing # hide -``` - -```@raw html -
-
-``` - -### Mathematical formulation - -```math - \begin{aligned} - & \text{Minimise} && \frac{1}{2}\int_0^1 u^2(t) \,\mathrm{d}t \\ - & \text{subject to} \\ - & && \dot{x}(t) = [v(t), u(t)], \\[1.0em] - & && x(0) = (-1,0), \\[0.5em] - & && x(1) = (0,0). - \end{aligned} -``` - -```@raw html -
-
-``` - -!!! note "Nota bene" - - 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.* - -## [Solve and plot](@id example-double-integrator-energy-solve-plot) - -### Direct method - -We can [`solve`](@ref) it simply with: - -```@example main -direct_sol = solve(ocp) -nothing # hide -``` - -And [`plot`](@ref) the solution with: - -```@example main -plot(direct_sol) -``` - -!!! note "Nota bene" - - The `solve` function has options, see the [solve tutorial](@ref manual-solve). You can customise the plot, see the [plot tutorial](@ref manual-plot). - -### Indirect method - -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. - -```@example main -using OrdinaryDiffEq # Ordinary Differential Equations (ODE) solver -using NonlinearSolve # Nonlinear Equations (NLE) solver -``` - -To define the shooting function, we must provide the maximising control in feedback form: - -```@example main -# maximising control, H(x, p, u) = p₁v + p₂u - u²/2 -u(x, p) = p[2] - -# Hamiltonian flow -f = Flow(ocp, u) - -# state projection, p being the costate -π((x, p)) = x - -# shooting function -S(p0) = π( f(t0, x0, p0, tf) ) - xf -nothing # hide -``` - -We are now ready to solve the shooting equations. - -```@example main -# auxiliary in-place NLE function -nle!(s, p0, _) = s[:] = S(p0) - -# initial guess for the Newton solver from the direct solution -t = time_grid(direct_sol) # the time grid as a vector -p = costate(direct_sol) # the costate as a function of time -p0_guess = p(t0) # initial costate - -# NLE problem with initial guess -prob = NonlinearProblem(nle!, p0_guess) - -# resolution of S(p0) = 0 -shooting_sol = solve(prob; show_trace=Val(true)) -p0_sol = shooting_sol.u # costate solution - -# print the costate solution and the shooting function evaluation -println("\ncostate: p0 = ", p0_sol) -println("shoot: S(p0) = ", S(p0_sol), "\n") -``` - -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. - -```@example main -indirect_sol = f((t0, tf), x0, p0_sol; saveat=range(t0, tf, 100)) -plot(indirect_sol) -``` - -[^1]: J. T. Betts. Practical methods for optimal control using nonlinear programming. Society for Industrial and Applied Mathematics (SIAM), Philadelphia, PA, 2001. - -!!! note - - - You can use [MINPACK.jl](@extref Tutorials Resolution-of-the-shooting-equation) instead of [NonlinearSolve.jl](https://docs.sciml.ai/NonlinearSolve). - - For more details about the flow construction, visit the [Compute flows from optimal control problems](@ref manual-flow-ocp) page. - - 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. - - For a version with a state constraint on the velocity, see the [State constraint](@ref example-state-constraint) example. diff --git a/docs/attic/example-double-integrator-time.md b/docs/attic/example-double-integrator-time.md deleted file mode 100644 index 5da17658b..000000000 --- a/docs/attic/example-double-integrator-time.md +++ /dev/null @@ -1,204 +0,0 @@ -# [Double integrator: time minimisation](@id example-double-integrator-time) - -The problem consists in minimising the final time $t_f$ for the double integrator system - -```math - \dot x_1(t) = x_2(t), \quad \dot x_2(t) = u(t), \quad u(t) \in [-1,1], -``` - -and the limit conditions - -```math - x(0) = (-1,0), \quad x(t_f) = (0,0). -``` - -This problem can be interpreted as a simple model for a wagon with constant mass moving along a line without friction. - -```@raw html - -``` - -First, we need to 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 also need to import the [Plots.jl](https://docs.juliaplots.org) package to plot the solution. - -```@example main -using OptimalControl -using NLPModelsIpopt -using Plots -``` - -## Optimal control problem - -Let us define the problem: - -```@raw html -
-
-``` - -```@example main -t0 = 0; x0 = [-1, 0]; xf = [0, 0] - -ocp = @def begin - tf ∈ R, variable - t ∈ [t0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - - -1 ≤ u(t) ≤ 1 - - x(t0) == x0 - x(tf) == xf - - ẋ(t) == [v(t), u(t)] - - tf → min -end -nothing # hide -``` - -```@raw html -
-
-``` - -### Mathematical formulation - -```math - \begin{aligned} - & \text{Minimise} && t_f \\[0.5em] - & \text{subject to} \\[0.5em] - & && \dot x(t) = [v(t), u(t)], \\[0.5em] - & && -1 \le u(t) \le 1, \\[0.5em] - & && x(0) = (-1, 0), \\[0.5em] - & && x(t_f) = (0, 0). - \end{aligned} -``` - -```@raw html -
-
-``` - -!!! note "Nota bene" - - 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.* - -## Solve and plot - -### Direct method - -Let us solve it with a direct method (we set the number of time steps to 200): - -```@example main -direct_sol = solve(ocp; grid_size=200) -nothing # hide -``` - -and plot the solution: - -```@example main -plt = plot(direct_sol; label="Direct", size=(800, 600)) -``` - -!!! note "Nota bene" - - The `solve` function has options, see the [solve tutorial](@ref manual-solve). You can customise the plot, see the [plot tutorial](@ref manual-plot). - -### Indirect method - -We now turn to the indirect method, which relies on Pontryagin’s Maximum Principle. The pseudo-Hamiltonian is given by - -```math -H(x, p, u) = p_1 v + p_2 u - 1, -``` - -where $p = (p_1, p_2)$ is the costate vector. The optimal control is of bang–bang type: - -```math -u(t) = \mathrm{sign}(p_2(t)), -``` - -with one switch from $u=+1$ to $u=-1$ at one single time denoted $t_1$. Let us implement this approach. First, we import the necessary packages: - -```@example main -using OrdinaryDiffEq -using NonlinearSolve -``` - -Define the bang–bang control and Hamiltonian flow: - -```@example main -# pseudo-Hamiltonian -H(x, p, u) = p[1]*x[2] + p[2]*u - 1 - -# bang–bang control -u_max = +1 -u_min = -1 - -# Hamiltonian flow -f_max = Flow(ocp, (x, p, tf) -> u_max) -f_min = Flow(ocp, (x, p, tf) -> u_min) -nothing # hide -``` - -The shooting function enforces the conditions: - -```@example main -function shoot!(s, p0, t1, tf) - x_t0, p_t0 = x0, p0 - x_t1, p_t1 = f_max(t0, x_t0, p_t0, t1) - x_tf, p_tf = f_min(t1, x_t1, p_t1, tf) - s[1:2] = x_tf - xf # target conditions - s[3] = p_t1[2] # switching condition - s[4] = H(x_tf, p_tf, -1) # free final time -end -nothing # hide -``` - -We are now ready to solve the shooting equations: - -```@example main -# in-place shooting function -nle!(s, ξ, λ) = shoot!(s, ξ[1:2], ξ[3], ξ[4]) - -# initial guess for the Newton solver from direct method -t = time_grid(direct_sol) # the time grid as a vector -p = costate(direct_sol) # the costate as a function of time -p0_guess = p(t0) # initial costate -tf_guess = variable(direct_sol) # final time - -# find switching time t1 where p2(t) changes sign -p2_values = [p(ti)[2] for ti in t] -t1_guess = t[findfirst(i -> i > 1 && p2_values[i] * p2_values[i-1] < 0, 2:length(p2_values))] - -ξ_guess = [p0_guess[1], p0_guess[2], t1_guess, tf_guess] - -# NLE problem -prob = NonlinearProblem(nle!, ξ_guess) - -# resolution of the shooting equations -shoot_sol = solve(prob; show_trace=Val(true)) -p0, t1, tf = shoot_sol.u[1:2], shoot_sol.u[3], shoot_sol.u[4] - -# print the solution -println("\np0 = ", p0, "\nt1 = ", t1, "\ntf = ", tf) -``` - -Finally, we reconstruct and plot the solution obtained by the indirect method: - -```@example main -# concatenation of the flows -φ = f_max * (t1, f_min) - -# compute the solution: state, costate, control... -indirect_sol = φ((t0, tf), x0, p0; saveat=range(t0, tf, 200)) - -# plot the solution on the previous plot -plot!(plt, indirect_sol; label="Indirect", color=2, linestyle=:dash) -``` - -!!! note - - - You can use [MINPACK.jl](@extref Tutorials Resolution-of-the-shooting-equation) instead of [NonlinearSolve.jl](https://docs.sciml.ai/NonlinearSolve). - - For more details about the flow construction, visit the [Compute flows from optimal control problems](@ref manual-flow-ocp) page. - - 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. diff --git a/docs/attic/example-singular-control.md b/docs/attic/example-singular-control.md deleted file mode 100644 index fc4920ba2..000000000 --- a/docs/attic/example-singular-control.md +++ /dev/null @@ -1,351 +0,0 @@ -# [Singular control](@id example-singular-control) - -For control-affine systems of the form - -```math -\dot{q}(t) = f_0(q(t)) + u(t) f_1(q(t)), \quad u(t) \in [u_{\min}, u_{\max}], -``` - -the pseudo-Hamiltonian is $H = H_0 + u H_1$, where $H_i(q, p) = \langle p, f_i(q) \rangle$ are the Hamiltonian lifts of the vector fields $f_0$ and $f_1$. - -When the **switching function** $H_1$ vanishes on a time interval (i.e., $H_1(q(t), p(t)) = 0$ for $t \in [t_1, t_2]$), the arc is called **singular**. On such arcs, the control cannot be determined directly from the maximization condition and must be computed by successive differentiation of $H_1$ along the flow. - -This page demonstrates how to compute singular controls both by hand and using differential geometry tools from OptimalControl.jl, then verifies the result numerically using direct and indirect methods. - -First, we import the necessary packages: - -```@example main -using OptimalControl -using NLPModelsIpopt -using Plots -``` - -## Problem definition - -We consider a vehicle moving in the plane with drift. The state is $q = (x, y, \theta)$ where $(x, y)$ is the position and $\theta$ is the orientation. The dynamics are: - -```math -\dot{x}(t) = \cos\theta(t), \quad \dot{y}(t) = \sin\theta(t) + x(t), \quad \dot{\theta}(t) = u(t), -``` - -with control constraint $u(t) \in [-1, 1]$. - -We want to find the time-optimal transfer from the origin $(0, 0)$ with free initial orientation to the target position $(1, 0)$ with free final orientation: - -```@example main -ocp = @def begin - - tf ∈ R, variable - t ∈ [0, tf], time - q = (x, y, θ) ∈ R³, state - u ∈ R, control - - -1 ≤ u(t) ≤ 1 # Control bounds - -π/2 ≤ θ(t) ≤ π/2 # State bounds (helps direct method convergence) - - x(0) == 0 - y(0) == 0 - x(tf) == 1 - y(tf) == 0 - - ∂(q)(t) == [cos(θ(t)), sin(θ(t)) + x(t), u(t)] - - tf → min - -end -nothing # hide -``` - -This is a control-affine system with: - -```math -f_0(q) = \begin{pmatrix} \cos\theta \\ \sin\theta + x \\ 0 \end{pmatrix}, \quad -f_1(q) = \begin{pmatrix} 0 \\ 0 \\ 1 \end{pmatrix}. -``` - -## Direct method - -We solve the problem using a direct method: - -```@example main -direct_sol = solve(ocp; display=false) -println("Optimal time: tf = ", variable(direct_sol)) -nothing # hide -``` - -Let's plot the solution: - -```@example main -opt = (state_bounds_style=:none, control_bounds_style=:none) -plt = plot(direct_sol; label="Direct", size=(800, 800), opt...) -``` - -## Singular control by hand - -The pseudo-Hamiltonian for this time-optimal problem is: - -```math -H(q, p, u) = p_1 \cos\theta + p_2(\sin\theta + x) + p_3 u. -``` - -This is control-affine: $H = H_0 + u H_1$ with: - -```math -H_0(q, p) = p_1 \cos\theta + p_2(\sin\theta + x), \quad H_1(q, p) = p_3. -``` - -The switching function is $H_1 = p_3$. On a singular arc, we have $H_1 = 0$ and all its time derivatives must vanish. - -**First derivative:** - -```math -\dot{H}_1 = \{H, H_1\} = \{H_0, H_1\} =: H_{01}. -``` - -Computing the Poisson bracket: - -```math -H_{01} = \frac{\partial H_0}{\partial p_1} \frac{\partial H_1}{\partial x} - \frac{\partial H_0}{\partial x} \frac{\partial H_1}{\partial p_1} - + \frac{\partial H_0}{\partial p_2} \frac{\partial H_1}{\partial y} - \frac{\partial H_0}{\partial y} \frac{\partial H_1}{\partial p_2} - + \frac{\partial H_0}{\partial p_3} \frac{\partial H_1}{\partial \theta} - \frac{\partial H_0}{\partial \theta} \frac{\partial H_1}{\partial p_3}. -``` - -Since $H_1 = p_3$ depends only on $p_3$, the only non-zero contribution comes from the $(\theta, p_3)$ pair: - -```math -H_{01} = \frac{\partial H_0}{\partial \theta} \frac{\partial H_1}{\partial p_3} - \frac{\partial H_0}{\partial p_3} \frac{\partial H_1}{\partial \theta} = (-p_1 \sin\theta + p_2 \cos\theta) \cdot 1 - 0 = -p_1 \sin\theta + p_2 \cos\theta. -``` - -On the singular arc, $H_{01} = 0$, which gives the constraint: - -```math -p_2 \cos\theta = p_1 \sin\theta. -``` - -**Second derivative:** - -```math -\dot{H}_{01} = \{H, H_{01}\} = \{H_0, H_{01}\} + u \{H_1, H_{01}\} =: H_{001} + u H_{101}. -``` - -For the arc to remain singular, $\dot{H}_{01} = 0$, which gives: - -```math -u_s = -\frac{H_{001}}{H_{101}}, -``` - -whenever $H_{101} \neq 0$. Computing $H_{001} = \{H_0, H_{01}\}$ with $H_{01} = -p_1 \sin\theta + p_2 \cos\theta$, the only non-zero contribution comes from the $(x, p_1)$ pair: - -```math -H_{001} = \frac{\partial H_0}{\partial x} \frac{\partial H_{01}}{\partial p_1} - \frac{\partial H_0}{\partial p_1} \frac{\partial H_{01}}{\partial x} = p_2 \cdot (-\sin\theta) - \cos\theta \cdot 0 = -p_2 \sin\theta. -``` - -Computing $H_{101} = \{H_1, H_{01}\}$ with $H_1 = p_3$ and $H_{01} = -p_1 \sin\theta + p_2 \cos\theta$, the only non-zero contribution comes from the $(\theta, p_3)$ pair: - -```math -H_{101} = \frac{\partial H_1}{\partial \theta} \frac{\partial H_{01}}{\partial p_3} - \frac{\partial H_1}{\partial p_3} \frac{\partial H_{01}}{\partial \theta} = 0 - 1 \cdot (-p_1 \cos\theta - p_2 \sin\theta) = p_1 \cos\theta + p_2 \sin\theta. -``` - -Therefore: - -```math -u_s = -\frac{H_{001}}{H_{101}} = \frac{p_2 \sin\theta}{p_1 \cos\theta + p_2 \sin\theta}. -``` - -!!! note "Non-degeneracy condition" - - We can show that $H_{101} \neq 0$ on the singular arc. From the constraint $p_1 \sin\theta = p_2 \cos\theta$, if we had $H_{101} = p_1 \cos\theta + p_2 \sin\theta = 0$, then: - - ```math - \begin{pmatrix} \cos\theta & \sin\theta \\ -\sin\theta & \cos\theta \end{pmatrix} - \begin{pmatrix} p_1 \\ p_2 \end{pmatrix} = \begin{pmatrix} 0 \\ 0 \end{pmatrix}. - ``` - - Since this matrix has determinant 1 (hence is invertible), we would have $p_1 = p_2 = 0$. Combined with $p_3 = 0$ (from $H_1 = 0$), this gives $p = 0$, which is impossible for a time-minimization problem. - -**Simplification using the constraint:** - -Multiply numerator and denominator by $\sin\theta$: - -```math -u_s = \frac{p_2 \sin^2\theta}{p_1 \cos\theta \sin\theta + p_2 \sin^2\theta}. -``` - -From the constraint $p_1 \sin\theta = p_2 \cos\theta$, we have $p_1 \cos\theta \sin\theta = p_2 \cos^2\theta$. Substituting in the denominator: - -```math -u_s = \frac{p_2 \sin^2\theta}{p_2 \cos^2\theta + p_2 \sin^2\theta} = \frac{p_2 \sin^2\theta}{p_2(\cos^2\theta + \sin^2\theta)} = \sin^2\theta. -``` - -So the singular control is: - -```math -u_s(\theta) = \sin^2\theta. -``` - -Let's overlay this on the numerical solution: - -```@example main -T = time_grid(direct_sol) -θ(t) = state(direct_sol)(t)[3] -us(t) = sin(θ(t))^2 -plot!(plt, T, us; subplot=7, line=:dash, lw=2, label="us (hand)") -plot(plt[7]; size=(800, 400)) -``` - -## Singular control via Poisson brackets - -We can compute the same result using the differential geometry tools from OptimalControl.jl. See the [differential geometry tools manual](@ref manual-differential-geometry) for detailed explanations. - -First, define the vector fields: - -```@example main -F0(q) = [cos(q[3]), sin(q[3]) + q[1], 0] -F1(q) = [0, 0, 1] -nothing # hide -``` - -Compute their Hamiltonian lifts: - -```@example main -H0 = Lift(F0) -H1 = Lift(F1) -nothing # hide -``` - -Compute the iterated Poisson brackets: - -```@example main -H01 = @Lie {H0, H1} -H001 = @Lie {H0, H01} -H101 = @Lie {H1, H01} -nothing # hide -``` - -The singular control is: - -```@example main -us_bracket(q, p) = -H001(q, p) / H101(q, p) -nothing # hide -``` - -Let's verify this gives the same result: - -```@example main -q(t) = state(direct_sol)(t) -p(t) = costate(direct_sol)(t) -us_b(t) = us_bracket(q(t), p(t)) -plot!(plt, T, us_b; subplot=7, line=:dashdot, lw=2, label="us (brackets)") -plot(plt[7]; size=(800, 400)) -``` - -Both methods give the same singular control, which matches the numerical solution from the direct method. - -## Indirect shooting method - -We now solve the problem using an indirect shooting method based on the singular control we computed. This approach is similar to the one used in the [double integrator example](@ref example-double-integrator-energy). - -First, import the necessary packages: - -```@example main -using OrdinaryDiffEq -using NonlinearSolve -``` - -Define the singular control in feedback form: - -```@example main -u_indirect(x) = sin(x[3])^2 -nothing # hide -``` - -Build the flow for the singular arc: - -```@example main -f = Flow(ocp, (x, p, tf) -> u_indirect(x)) -nothing # hide -``` - -Define the shooting function. We have 5 unknowns: the initial costate $p_0 \in \mathbb{R}^3$, the initial orientation $\theta_0$, and the final time $t_f$. We must define 5 equations to solve for these unknowns. - -```@example main -t0 = 0 - -function shoot!(s, p0, θ0, tf) - - q_t0, p_t0 = [0, 0, θ0], p0 - q_tf, p_tf = f(t0, q_t0, p_t0, tf) - - s[1] = q_tf[1] - 1 # x(tf) = 1 (boundary condition) - s[2] = q_tf[2] # y(tf) = 0 (boundary condition) - s[3] = p_t0[3] # pθ(0) = 0 (transversality condition) - s[4] = p_tf[3] # pθ(tf) = 0 (transversality condition) - - # H(tf) = 1 (for time-optimal with p^0 = -1) - pxf = p_tf[1] - pyf = p_tf[2] - θf = q_tf[3] - s[5] = pxf * cos(θf) + pyf * (sin(θf) + 1) - 1 - - return nothing -end -nothing # hide -``` - -Use the direct solution to provide an initial guess: - -```@example main -p0 = costate(direct_sol)(t0) -θ0 = state(direct_sol)(t0)[3] -tf = variable(direct_sol) - -println("Initial guess:") -println("p0 = ", p0) -println("θ0 = ", θ0) -println("tf = ", tf) -nothing # hide -``` - -Set up and solve the nonlinear system: - -```@example main -# Auxiliary in-place NLE function -nle!(s, ξ, _) = shoot!(s, ξ[1:3], ξ[4], ξ[5]) - -# Initial guess for the Newton solver -ξ_guess = [p0..., θ0, tf] - -# NLE problem with initial guess -prob = NonlinearProblem(nle!, ξ_guess) - -# Resolution of the shooting equations -shooting_sol = solve(prob; show_trace=Val(false)) -p0_sol, θ0_sol, tf_sol = shooting_sol.u[1:3], shooting_sol.u[4], shooting_sol.u[5] - -println("Shooting solution:") -println("p0 = ", p0_sol) -println("θ0 = ", θ0_sol) -println("tf = ", tf_sol) -nothing # hide -``` - -Reconstruct the indirect solution: - -```@example main -indirect_sol = f((t0, tf_sol), [0, 0, θ0_sol], p0_sol; saveat=range(t0, tf_sol, 100)) -nothing # hide -``` - -Plot the indirect solution alongside the direct solution: - -```@example main -plot!(plt, indirect_sol; label="Indirect", color=2, linestyle=:dash, opt...) -``` - -The indirect and direct solutions match very well, confirming that our singular control computation is correct. - -## See also - -- [Differential geometry tools](@ref manual-differential-geometry) — Mathematical definitions and usage of `Lift`, `Poisson`, `@Lie` -- [Goddard tutorial](@extref Tutorials tutorial-goddard) — More complex example with bang, singular, and boundary arcs -- [Compute flows from optimal control problems](@ref manual-flow-ocp) — Using flows for indirect methods diff --git a/docs/attic/example-state-constraint.md b/docs/attic/example-state-constraint.md deleted file mode 100644 index 5de30d409..000000000 --- a/docs/attic/example-state-constraint.md +++ /dev/null @@ -1,518 +0,0 @@ -# [State constraint](@id example-state-constraint) - -This example illustrates how state constraints of different orders affect the structure of optimal solutions for the double integrator energy minimization problem. It demonstrates both direct and indirect solution approaches. Some examples with state constraints of different orders are solved analytically in Bryson et al.[^1] and Jacobson et al.[^2]. - -Let us consider a wagon moving along a rail, whose acceleration can be controlled by a force $u$. -We denote by $x = (q, v)$ the state of the wagon, where $q$ is the position and $v$ the velocity. - -```@raw html - -``` - -We assume that the mass is constant and equal to one, and that there is no friction. The dynamics are given by - -```math - \dot q(t) = v(t), \quad \dot v(t) = u(t),\quad u(t) \in \R, -``` - -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 - -```math - \frac{1}{2}\int_{0}^{1} u^2(t) \, \mathrm{d}t -``` - -starting from $x(0) = (-1, 0)$ and aiming to reach the target $x(1) = (0, 0)$. - -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. - -```@example main -using OptimalControl -using NLPModelsIpopt -using Plots -``` - -## Optimal control problem - -Let us define the problem with the [`@def`](@ref) macro: - -```@raw html -
-
-``` - -```@example main -t0 = 0; tf = 1; x0 = [-1, 0]; xf = [0, 0] - -ocp = @def begin - t ∈ [t0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - - x(t0) == x0 - x(tf) == xf - - ẋ(t) == [v(t), u(t)] - - 0.5∫( u(t)^2 ) → min -end -nothing # hide -``` - -```@raw html -
-
-``` - -### Mathematical formulation - -```math - \begin{aligned} - & \text{Minimise} && \frac{1}{2}\int_0^1 u^2(t) \,\mathrm{d}t \\ - & \text{subject to} \\ - & && \dot{x}(t) = [v(t), u(t)], \\[1.0em] - & && x(0) = (-1,0), \\[0.5em] - & && x(1) = (0,0). - \end{aligned} -``` - -```@raw html -
-
-``` - -!!! note "Nota bene" - - 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.* - -## First-order state constraint - -We now add a path constraint on the maximal velocity: - -```math - v(t) \le 1.2. -``` - -This is a **first-order state constraint**: differentiating $g(x) = v_{\max} - v$ once already makes the control appear, - -```math - \frac{\mathrm{d}}{\mathrm{d}t}g(x(t)) = -\dot{v}(t) = -u(t), -``` - -which fixes $u = 0$ on the boundary arc. - -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. - -!!! note - - The direct solution can be refined using a finer discretization grid for higher accuracy. - -### Direct method: constrained case - -Let us model, solve and plot the optimal control problem with this constraint. - -```@example main -# the upper bound for v -v_max = 1.2 - -# the optimal control problem -ocp = @def begin - t ∈ [t0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - - v(t) ≤ v_max # state constraint - - x(t0) == x0 - x(tf) == xf - - ẋ(t) == [v(t), u(t)] - - 0.5∫( u(t)^2 ) → min -end - -# solve with a direct method -direct_sol = solve(ocp; grid_size=50) - -# plot the solution -plt = plot(direct_sol; label="Direct", size=(800, 600)) -``` - -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. - -### Indirect method: constrained case - -Under the normal case, the pseudo-Hamiltonian reads: - -```math -H(x, p, u, \mu) = p_1 v + p_2 u - \frac{u^2}{2} + \mu\, g(x), -``` - -where $g(x) = v_{\max} - v$. Along a boundary arc we have $g(x(t)) = 0$; differentiating gives: - -```math - \frac{\mathrm{d}}{\mathrm{d}t}g(x(t)) = -\dot{v}(t) = -u(t) = 0. -``` - -The zero control maximises the Hamiltonian, so $p_2(t) = 0$ along that arc. From the adjoint equation we then have - -```math - \dot{p}_2(t) = -p_1(t) + \mu(t) = 0 \quad \Rightarrow \mu(t) = p_1(t). -``` - -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. - -```@example main -using OrdinaryDiffEq # Ordinary Differential Equations (ODE) solver -using NonlinearSolve # Nonlinear Equations (NLE) solver - -# flow for unconstrained extremals -f_interior = Flow(ocp, (x, p) -> p[2]) - -ub = 0 # boundary control -g(x) = v_max - x[2] # constraint: g(x) ≥ 0 -μ(p) = p[1] # dual variable - -# flow for boundary extremals -f_boundary = Flow(ocp, (x, p) -> ub, (x, u) -> g(x), (x, p) -> μ(p)) - -# shooting function -function shoot!(s, p0, t1, t2) - x_t0, p_t0 = x0, p0 - x_t1, p_t1 = f_interior(t0, x_t0, p_t0, t1) - x_t2, p_t2 = f_boundary(t1, x_t1, p_t1, t2) - x_tf, p_tf = f_interior(t2, x_t2, p_t2, tf) - s[1:2] = x_tf - xf - s[3] = g(x_t1) - s[4] = p_t1[2] - return nothing -end -nothing # hide -``` - -We can derive an initial guess for the costate and the entry/exit times from the direct solution: - -```@example main -t = time_grid(direct_sol) # the time grid as a vector -x = state(direct_sol) # the state as a function of time -p = costate(direct_sol) # the costate as a function of time - -# initial costate -p0 = p(t0) - -# t1, t2: entry and exit of the constrained arc (v ≈ v_max) -active = findall(t -> 0 ≤ g(x(t)) ≤ 1e-3, t) -t1 = t[first(active)] # entry time -t2 = t[last(active)] # exit time -nothing # hide -``` - -We can now solve the shooting equations. - -```@example main -# auxiliary in-place NLE function -nle!(s, ξ, _) = shoot!(s, ξ[1:2], ξ[3], ξ[4]) - -# initial guess for the Newton solver -ξ_guess = [p0..., t1, t2] - -# NLE problem with initial guess -prob = NonlinearProblem(nle!, ξ_guess) - -# resolution of the shooting equations -shooting_sol = solve(prob; show_trace=Val(true)) -p0, t1, t2 = shooting_sol.u[1:2], shooting_sol.u[3], shooting_sol.u[4] - -# print the costate solution and the entry and exit times -println("\np0 = ", p0, "\nt1 = ", t1, "\nt2 = ", t2) -``` - -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$. -This composition yields the full solution (state, costate, and control), which we then plot alongside the direct method for comparison. - -```@example main -# concatenation of the flows -φ = f_interior * (t1, f_boundary) * (t2, f_interior) - -# compute the solution: state, costate, control... -indirect_sol = φ((t0, tf), x0, p0; saveat=range(t0, tf, 100)) - -# plot the solution on the previous plot -plot!(plt, indirect_sol; label="Indirect", color=2, linestyle=:dash) -``` - -!!! note - - - You can use [MINPACK.jl](@extref Tutorials Resolution-of-the-shooting-equation) instead of [NonlinearSolve.jl](https://docs.sciml.ai/NonlinearSolve). - - For more details about the flow construction, visit the [Compute flows from optimal control problems](@ref manual-flow-ocp) page. - - For the unconstrained version of this problem, see the [Energy minimisation](@ref example-double-integrator-energy) example. - -## Second-order state constraint - -We now consider the same double integrator with different boundary conditions and a constraint on the **position** $x_1 = q$:[^1] - -```math - q(t) \le a. -``` - -The boundary conditions are $x(0) = (0, 1)$ and $x(1) = (0, -1)$. - -This is a **second-order state constraint**: the control $u$ appears only after differentiating $g(x) = a - q$ twice, - -```math - \frac{\mathrm{d}}{\mathrm{d}t}g(x(t)) = -\dot{q}(t) = -v(t) \quad \text{(no control)}, -``` - -```math - \frac{\mathrm{d}^2}{\mathrm{d}t^2}g(x(t)) = -\dot{v}(t) = -u(t) \quad \text{(control appears)}. -``` - -On a boundary arc where $g(x(t)) = 0$, both derivatives must vanish, forcing $v(t) = 0$ and $u(t) = 0$. - -### Solution structure - -The unconstrained optimal trajectory for these boundary conditions is $q(t) = t - t^2$, which reaches its maximum $1/4$ at $t = 1/2$. A characteristic feature of second-order state constraints is the existence of an intermediate regime between the unconstrained and boundary-arc cases[^3]. The solution structure depends on $a$: - -- **Unconstrained** ($a \ge 1/4$): the constraint is never active -- **Touch point** ($1/6 \le a \le 1/4$): the trajectory touches $q = a$ at a single instant, without sliding along the boundary -- **Boundary arc** ($a < 1/6$): the trajectory remains on $q = a$ for a finite time interval, during which $v(t) = 0$ and $u(t) = 0$ - -### Direct method - -We compare the two constrained cases using the direct method, taking $a = 0.2$ (touch point) and $a = 0.1$ (boundary arc). - -```@example main -# new boundary conditions -x0_bd = [0.0, 1.0]; xf_bd = [0.0, -1.0] - -# parametric OCP: double integrator with position constraint q(t) ≤ a -function make_ocp(a) - @def begin - t ∈ [t0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - - q(t) ≤ a - - x(t0) == x0_bd - x(tf) == xf_bd - - ẋ(t) == [v(t), u(t)] - - 0.5∫( u(t)^2 ) → min - end -end -nothing # hide -``` - -We now solve both cases using this parametric OCP definition. - -```@example main -sol_touch = solve(make_ocp(0.2); grid_size=100, display=false) # touch point -sol_arc = solve(make_ocp(0.1); grid_size=100, display=false) # boundary arc - -state_style = (legend=false, ) -costate_style = (legend=false, ) -plt_bd = plot( - sol_touch; - label="a = 0.2", - size=(800, 600), - state_style=state_style, - costate_style=costate_style, -) -plot!( - plt_bd, - sol_arc; - label="a = 0.1", - color=2, - linestyle=:dash, - state_style=state_style, - costate_style=costate_style, -) -``` - -### Indirect method: touch point case - -For the touch point case ($a = 0.2$), the optimal solution consists of two unconstrained arcs on $[t_0, t_1]$ and $[t_1, t_f]$, joined at the contact instant $t_1$ where $q(t_1) = a$ and $v(t_1) = 0$. The costate is discontinuous at $t_1$: the first component $p_q$ undergoes a jump $\Delta p_q$ while $p_v$ remains continuous. - -The shooting unknowns are therefore the initial costate $p_0 \in \mathbb{R}^2$, the contact time $t_1$, and the costate jump $\Delta p_q$. The four shooting conditions are: - -```math -x(t_f) = x_f, \quad q(t_1) = a, \quad v(t_1) = 0. -``` - -```@example main -a_touch = 0.2 - -# interior (unconstrained) flow -fs_bd = Flow(make_ocp(a_touch), (x, p) -> p[2]) - -# constraint: g(x) = a - q ≥ 0 -g_bd(x) = a_touch - x[1] - -# shooting function: unknowns p0 (2D), t1 (contact time), Δpq (costate jump) -function shoot_touch!(s, p0, t1, Δpq) - x_t1, p_t1 = fs_bd(t0, x0_bd, p0, t1) # arc 1: t0 → t1 - p_t1_plus = [p_t1[1] + Δpq, p_t1[2]] # costate jump at t1 - x_tf, _ = fs_bd(t1, x_t1, p_t1_plus, tf) # arc 2: t1 → tf - s[1:2] = x_tf - xf_bd # reach target - s[3] = g_bd(x_t1) # touch: q(t1) = a - s[4] = x_t1[2] # tangency: v(t1) = 0 - return nothing -end -nothing # hide -``` - -We extract the initial guess from the direct solution `sol_touch`. - -```@example main -t_grid = time_grid(sol_touch) -x_sol = state(sol_touch) -p_sol = costate(sol_touch) - -p0_guess = p_sol(t0) - -# t1: time where q(t) is closest to the constraint bound a -t1_guess = t_grid[argmin(abs.(g_bd.(x_sol.(t_grid))))] - -# Δpq: estimated costate jump around t1 -ε = 0.05 * (tf - t0) -Δpq_guess = p_sol(t1_guess + ε)[1] - p_sol(t1_guess - ε)[1] - -println("p0 guess = ", p0_guess) -println("t1 guess = ", t1_guess) -println("Δpq guess = ", Δpq_guess) -nothing # hide -``` - -```@example main -nle_touch!(s, ξ, _) = shoot_touch!(s, ξ[1:2], ξ[3], ξ[4]) - -ξ_guess = [p0_guess..., t1_guess, Δpq_guess] -sol_shoot_touch = solve(NonlinearProblem(nle_touch!, ξ_guess); show_trace=Val(true)) - -p0_touch = sol_shoot_touch.u[1:2] -t1_touch = sol_shoot_touch.u[3] -Δpq_touch = sol_shoot_touch.u[4] - -println("\np0 = ", p0_touch, "\nt1 = ", t1_touch, "\nΔpq = ", Δpq_touch) -``` - -The analytical solution gives $t_1 = 1/2$, $p_q = -4.8$ on $[t_0, t_1)$, $p_q = +4.8$ on $(t_1, t_f]$, with a jump of $9.6$ and an optimal cost of $2.24$. - -```@example main -# concatenate: arc 1 → costate jump → arc 2 -f_touch = fs_bd * (t1_touch, [Δpq_touch, 0.0], fs_bd) - -# reconstruct the indirect solution -indirect_touch = f_touch((t0, tf), x0_bd, p0_touch; saveat=range(t0, tf, 100)) - -plt_indirect = plot(indirect_touch; label="Indirect (a = 0.2)", size=(800, 600), - state_style=(legend=false,), costate_style=(legend=false,)) -``` - -### Indirect method: boundary arc case - -For the boundary arc case ($a = 0.1$), the optimal solution consists of three arcs: two unconstrained arcs on $[t_0, t_1]$ and $[t_2, t_f]$, separated by a boundary arc on $[t_1, t_2]$ where $q(t) = a$ and $v(t) = 0$. The pseudo-Hamiltonian is - -```math -H(x, p, u, \mu) = p_q\, v + p_v\, u + 0.5\, p^0 u^2 + \mu\, g(x), -``` - -where $p^0 = -1$ in the normal case and $g(x) = a - q \geq 0$ is the constraint. Along the boundary arc, the control is $u = 0$, since differentiating $g(x) = a - q \geq 0$ twice gives $\ddot{q} = u = 0$. From the maximisation condition, $p_v(t) = 0$ along the arc. Differentiating the adjoint equation $\dot{p}_v = -p_q$ and using $p_v = 0$ yields $p_q = 0$. Differentiating further gives $\mu = \dot{p}_q = 0$. The costate has jumps $(\Delta p_q^1, 0)$ and $(\Delta p_q^2, 0)$ at $t_1$ and $t_2$ respectively. - -The six shooting unknowns are the initial costate $p_0 \in \mathbb{R}^2$, the entry and exit times $t_1$ and $t_2$, and the two jumps $\Delta p_q^1$ and $\Delta p_q^2$. The shooting conditions are: - -```math -x(t_f) = x_f, \quad q(t_1) = a, \quad v(t_1) = 0, \quad p_v(t_1^+) = 0, \quad p_q(t_1^+) = 0. -``` - -```@example main -a_arc = 0.1 - -# interior (unconstrained) flow -fs_arc = Flow(make_ocp(a_arc), (x, p) -> p[2]) - -# boundary arc flow: u = 0, constraint g(x) = a - q ≥ 0, multiplier μ = 0 -fc_bd = Flow(make_ocp(a_arc), (x, p) -> 0, (x, u) -> a_arc - x[1], (x, p) -> 0) - -# constraint function -g_arc(x) = a_arc - x[1] - -# shooting function: unknowns p0 (2D), t1, t2, Δpq1, Δpq2 -function shoot_arc!(s, p0, t1, t2, Δpq1, Δpq2) - x_t1, p_t1 = fs_arc(t0, x0_bd, p0, t1) # arc 1: t0 → t1 - p_t1_plus = [p_t1[1] + Δpq1, p_t1[2]] # costate jump at t1 - x_t2, p_t2 = fc_bd(t1, x_t1, p_t1_plus, t2) # arc 2: t1 → t2 (boundary) - p_t2_plus = [p_t2[1] + Δpq2, p_t2[2]] # costate jump at t2 - x_tf, _ = fs_arc(t2, x_t2, p_t2_plus, tf) # arc 3: t2 → tf - s[1:2] = x_tf - xf_bd # reach target - s[3] = g_arc(x_t1) # touch: q(t1) = a - s[4] = x_t1[2] # tangency: v(t1) = 0 - s[5] = p_t1_plus[2] # switching: pv(t1+) = 0 - s[6] = p_t1_plus[1] # arc condition: pq(t1+) = 0 - return nothing -end -nothing # hide -``` - -We extract the initial guess from the direct solution `sol_arc`. - -```@example main -t_grid_arc = time_grid(sol_arc) -x_sol_arc = state(sol_arc) -p_sol_arc = costate(sol_arc) - -p0_guess_arc = p_sol_arc(t0) - -# t1, t2: entry and exit of the boundary arc (q ≈ a) -active = findall(t -> 0 ≤ g_arc(x_sol_arc(t)) ≤ 1e-3, t_grid_arc) -t1_guess_arc = t_grid_arc[first(active)] -t2_guess_arc = t_grid_arc[last(active)] - -# jumps: costate difference around t1 and t2 -ε_arc = 0.1 * (tf - t0) -Δpq1_guess = p_sol_arc(t1_guess_arc + ε_arc)[1] - p_sol_arc(t1_guess_arc - ε_arc)[1] -Δpq2_guess = p_sol_arc(t2_guess_arc + ε_arc)[1] - p_sol_arc(t2_guess_arc - ε_arc)[1] - -println("p0 guess = ", p0_guess_arc) -println("t1 guess = ", t1_guess_arc) -println("t2 guess = ", t2_guess_arc) -println("Δpq1 guess = ", Δpq1_guess) -println("Δpq2 guess = ", Δpq2_guess) -nothing # hide -``` - -```@example main -nle_arc!(s, ξ, _) = shoot_arc!(s, ξ[1:2], ξ[3], ξ[4], ξ[5], ξ[6]) - -ξ_guess_arc = [p0_guess_arc..., t1_guess_arc, t2_guess_arc, Δpq1_guess, Δpq2_guess] -sol_shoot_arc = solve(NonlinearProblem(nle_arc!, ξ_guess_arc); show_trace=Val(true)) - -p0_arc = sol_shoot_arc.u[1:2] -t1_arc = sol_shoot_arc.u[3] -t2_arc = sol_shoot_arc.u[4] -Δpq1 = sol_shoot_arc.u[5] -Δpq2 = sol_shoot_arc.u[6] - -println("\np0 = ", p0_arc) -println("t1 = ", t1_arc, " (expect ", 3a_arc, ")") -println("t2 = ", t2_arc, " (expect ", 1 - 3a_arc, ")") -println("Δpq1 = ", Δpq1, " Δpq2 = ", Δpq2, " (expect equal by symmetry)") -``` - -```@example main -# concatenate: arc 1 → jump → boundary arc → jump → arc 3 -f_arc = fs_arc * (t1_arc, [Δpq1, 0.0], fc_bd) * (t2_arc, [Δpq2, 0.0], fs_arc) - -# reconstruct the indirect solution -indirect_arc = f_arc((t0, tf), x0_bd, p0_arc; saveat=range(t0, tf, 100)) - -plot!(plt_indirect, indirect_arc; label="Indirect (a = 0.1)", color=2, linestyle=:dash, - state_style=(legend=false,), costate_style=(legend=false,)) -``` - -[^1]: Bryson, A.E., Denham, W.F., & Dreyfus, S.E. (1963). *Optimal programming problems with inequality constraints I: necessary conditions for extremal solutions*. AIAA Journal, 1(11), 2544–2550. [doi.org/10.2514/3.2107](https://doi.org/10.2514/3.2107) - -[^2]: Jacobson, D.H., Lele, M.M., & Speyer, J.L. (1971). *New necessary conditions of optimality for control problems with state-variable inequality constraints*. Journal of Mathematical Analysis and Applications, 35, 255–284. - -[^3]: Bryson, A.E. & Ho, Y.-C. (1975). *Applied Optimal Control: Optimization, Estimation and Control*. CRC Press. diff --git a/docs/attic/manual-abstract.md b/docs/attic/manual-abstract.md deleted file mode 100644 index 2df6b9e64..000000000 --- a/docs/attic/manual-abstract.md +++ /dev/null @@ -1,646 +0,0 @@ -# [The syntax to define an optimal control problem](@id manual-abstract-syntax) - -The full grammar of [OptimalControl.jl](https://control-toolbox.org/OptimalControl.jl) small *Domain Specific Language* is given below. The idea is to use a syntax that is - -- pure Julia (and, as such, effortlessly analysed by the standard Julia parser), -- as close as possible to the mathematical description of an optimal control problem. - -While the syntax will be transparent to those users familiar with Julia expressions (`Expr`'s), we provide examples for every case that should be widely understandable. We rely heavily on [MLStyle.jl](https://thautwarm.github.io/MLStyle.jl) and its pattern matching abilities 👍🏽 both for the syntactic and semantic pass. Abstract definitions use the macro [`@def`](@ref). - -## [Variable](@id manual-abstract-variable) - -```julia -:( $v ∈ R^$q, variable ) -:( $v ∈ R , variable ) -``` - -A variable (only one is allowed) is a finite dimensional vector or reals that will be *optimised* along with state and control values. To define an (almost empty!) optimal control problem, named `ocp`, having a dimension two variable named `v`, do the following: - -```julia -@def begin - v ∈ R², variable - ... -end -``` - -!!! warning - - Note that the full code of the definition above is not provided (hence the `...`) The same is true for most examples below (only those without `...` are indeed complete). - - Also note that problem definitions must at least include definitions for time, state, dynamics and cost. The control declaration is optional (see [Control-free problems](@ref manual-abstract-control-free)). - -Aliases `v₁`, `v₂` (and `v1`, `v2`) are automatically defined and can be used in subsequent expressions instead of `v[1]` and `v[2]`. The user can also define her own aliases for the components (one alias per dimension): - -```julia -@def begin - v = (a, b) ∈ R², variable - ... -end -``` - -A one dimensional variable can be declared according to - -```julia -@def begin - v ∈ R, variable - ... -end -``` - -!!! warning - Aliases during definition of variable, state or control are only allowed for multidimensional (dimension two or more) cases. Something like `u = T ∈ R, control` is not allowed... and useless (directly write `T ∈ R, control`). - -## Time - -```julia -:( $t ∈ [$t0, $tf], time ) -``` - -The independent variable or *time* is a scalar bound to a given interval. Its name is arbitrary. - -```julia -t0 = 1 -tf = 5 -@def begin - t ∈ [t0, tf], time - ... -end -``` - -One (or even the two bounds) can be variable, typically for minimum time problems (see [Mayer cost](@ref manual-abstract-mayer) section): - -```julia -@def begin - v = (T, λ) ∈ R², variable - t ∈ [0, T], time - ... -end -``` - -## [State](@id manual-abstract-state) - -```julia -:( $x ∈ R^$n, state ) -:( $x ∈ R , state ) -``` - -The state declaration defines the name and the dimension of the state: - -```julia -@def begin - x ∈ R⁴, state - ... -end -``` - -As for the variable, there are automatic aliases (`x₁` and `x1` for `x[1]`, *etc.*) and the user can define her own aliases (one per scalar component of the state): - -```julia -@def begin - x = (q₁, q₂, v₁, v₂) ∈ R⁴, state - ... -end -``` - -## [Control](@id manual-abstract-control) - -```julia -:( $u ∈ R^$m, control ) -:( $u ∈ R , control ) -``` - -The control declaration defines the name and the dimension of the control: - -```julia -@def begin - u ∈ R², control - ... -end -``` - -As before, there are automatic aliases (`u₁` and `u1` for `u[1]`, *etc.*) and the user can define her own aliases (one per scalar component of the state): - -```julia -@def begin - u = (α, β) ∈ R², control - ... -end -``` - -!!! note - One dimensional variable, state or control are treated as scalars (`Real`), not vectors (`Vector`). In Julia, for `x::Real`, it is possible to write `x[1]` (and `x[1][1]`...) so it is OK (though useless) to write `x₁`, `x1` or `x[1]` instead of simply `x` to access the corresponding value. Conversely it is *not* OK to use such an `x` as a vector, for instance as in `...f(x)...` where `f(x::Vector{T}) where {T <: Real}`. - -## [Control-free problems](@id manual-abstract-control-free) - -The control declaration is **optional**. You can define problems without control for: - -- **Parameter estimation**: Identify unknown parameters in the dynamics from observed data -- **Dynamic optimization**: Optimize constant parameters subject to ODE constraints - -For example, to estimate a growth rate parameter: - -```julia -@def begin - p ∈ R, variable # parameter to estimate - t ∈ [0, 10], time - x ∈ R, state - x(0) == 2.0 - ẋ(t) == p * x(t) # dynamics depends on p - ∫(x(t) - data(t))² → min # fit to observed data -end -``` - -Or to optimize the pulsation of a harmonic oscillator: - -```julia -@def begin - ω ∈ R, variable # pulsation to optimize - t ∈ [0, 1], time - x = (q, v) ∈ R², state - q(0) == 1.0 - v(0) == 0.0 - q(1) == 0.0 # final condition - ẋ(t) == [v(t), -ω²*q(t)] # harmonic oscillator - ω² → min # minimize pulsation -end -``` - -!!! compat "Upcoming feature" - - Control-free problem syntax (omitting the control declaration) is currently being implemented. For now, use a dummy control with `u ∈ R, control` and `u(t) == 0` as a workaround. See the [Control-free problems example](@ref example-control-free) for executable examples. - -## [Dynamics](@id manual-abstract-dynamics) - -```julia -:( ∂($x)($t) == $e1 ) -``` - -The dynamics is given in the standard vectorial ODE form: - -```math - \dot{x}(t) = f([t, ]x(t)[, u(t)][, v]) -``` - -depending on whether it is autonomous / with a variable or not (the parser will detect time and variable dependences, -which entails that time, state and variable must be declared prior to dynamics - an error will be issued otherwise). The symbol `∂`, or the dotted state name -(`ẋ`), or the keyword `derivative` can be used: - -```julia -@def begin - t ∈ [0, 1], time - x ∈ R², state - u ∈ R, control - ∂(x)(t) == [x₂(t), u(t)] - ... -end -``` - -or - -```julia -@def begin - t ∈ [0, 1], time - x ∈ R², state - u ∈ R, control - ẋ(t) == [x₂(t), u(t)] - ... -end -``` - -or - -```julia -@def begin - t ∈ [0, 1], time - x ∈ R², state - u ∈ R, control - derivative(x)(t) == [x₂(t), u(t)] - ... -end -``` - -Any Julia code can be used, so the following is also OK: - -```julia -ocp = @def begin - t ∈ [0, 1], time - x ∈ R², state - u ∈ R, control - ẋ(t) == F₀(x(t)) + u(t) * F₁(x(t)) - ... -end - -F₀(x) = [x[2], 0] -F₁(x) = [0, 1] -``` - -!!! note - The vector fields `F₀` and `F₁` can be defined afterwards, as they only need to be available when the dynamics will be evaluated. - -While it is also possible to declare the dynamics component after component (see below), one may equivalently use *aliases* (check the relevant [aliases](@ref manual-abstract-aliases) section below): - -```julia -@def damped_integrator begin - tf ∈ R, variable - t ∈ [0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - q̇ = v(t) - v̇ = u(t) - c(t) - ẋ(t) == [q̇, v̇] - ... -end -``` - -## [Dynamics (coordinatewise)](@id manual-abstract-dynamics-coord) - -```julia -:( ∂($x[$i])($t) == $e1 ) -``` - -The dynamics can also be declared coordinate by coordinate. The previous example can be written as - -```julia -@def damped_integrator begin - tf ∈ R, variable - t ∈ [0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - ∂(q)(t) == v(t) - ∂(v)(t) == u(t) - c(t) - ... -end -``` - -## [Constraints](@id manual-abstract-constraints) - -```julia -:( $e1 == $e2 ) -:( $e1 ≤ $e2 ≤ $e3 ) -:( $e2 ≤ $e3 ) -:( $e3 ≥ $e2 ≥ $e1 ) -:( $e2 ≥ $e1 ) -``` - -Admissible constraints can be - -- of five types: boundary, variable, control, state, mixed (the last three ones are *path* constraints, that is constraints evaluated all times) -- linear (ranges) or nonlinear (not ranges), -- equalities or (one or two-sided) inequalities. - -Boundary conditions are detected when the expression contains evaluations of the state at initial and / or final time bounds (*e.g.*, `x(0)`), and may not involve the control. Conversely control, state or mixed constraints will involve control, state or both evaluated at the declared time (*e.g.*, `x(t) + u(t)`). -Other combinations should be detected as incorrect by the parser 🤞🏾. The variable may be involved in any of the four previous constraints. Constraints involving the variable only are variable constraints, either linear or nonlinear. -In the example below, there are - -- two linear boundary constraints, -- one linear variable constraint, -- one linear state constraint, -- one (two-sided) nonlinear control constraint. - -```julia -@def begin - tf ∈ R, variable - t ∈ [0, tf], time - x ∈ R², state - u ∈ R, control - x(0) == [-1, 0] - x(tf) == [0, 0] - ẋ(t) == [x₂(t), u(t)] - tf ≥ 0 - x₂(t) ≤ 1 - 0.1 ≤ u(t)^2 ≤ 1 - ... -end -``` - -!!! note "Duplicate box constraints" - If the same scalar component of the state, control or variable appears in several **box** constraints (linear range constraints), the effective bounds are the **intersection** of all declared bounds: the effective lower bound is the maximum of declared lower bounds, and the effective upper bound is the minimum of declared upper bounds. A warning is emitted, and an error is thrown if the resulting interval is empty. See [Duplicate box constraints](@ref manual-abstract-box-dedup) below. - -!!! note - Symbols like `<=` or `>=` are also authorised: - -```julia -@def begin - tf ∈ R, variable - t ∈ [0, tf], time - x ∈ R², state - u ∈ R, control - x(0) == [-1, 0] - x(tf) == [0, 0] - ẋ(t) == [x₂(t), u(t)] - tf >= 0 - x₂(t) <= 1 - 0.1 ≤ u(t)^2 <= 1 - ... -end -``` - -!!! warning - Write either `u(t)^2` or `(u^2)(t)`, not `u^2(t)` since in Julia the latter means `u^(2t)`. Moreover, - in the case of equalities or of one-sided inequalities, the control and / or the state must belong to the *left-hand side*. The following will error: - - ```@setup main-repl - using OptimalControl - ``` - - ```@repl main-repl - @def begin - t ∈ [0, 2], time - x ∈ R², state - u ∈ R, control - x(0) == [-1, 0] - x(2) == [0, 0] - ẋ(t) == [x₂(t), u(t)] - 1 ≤ x₂(t) - -1 ≤ u(t) ≤ 1 - end - ``` - -!!! warning - Constraint bounds must be *effective*, that is must not depend on a variable. For instance, instead of - - ```julia - o = @def begin - v ∈ R, variable - t ∈ [0, 1], time - x ∈ R², state - u ∈ R, control - -1 ≤ v ≤ 1 - x₁(0) == -1 - x₂(0) == v # wrong: the bound is not effective (as it depends on the variable) - x(1) == [0, 0] - ẋ(t) == [x₂(t), u(t)] - ∫( 0.5u(t)^2 ) → min - end - ``` - - write - - ```julia - o = @def begin - v ∈ R, variable - t ∈ [0, 1], time - x ∈ R², state - u ∈ R, control - -1 ≤ v ≤ 1 - x₁(0) == -1 - x₂(0) - v == 0 # OK: the boundary constraint may involve the variable - x(1) == [0, 0] - ẋ(t) == [x₂(t), u(t)] - ∫( 0.5u(t)^2 ) → min - end - ``` - -### [Duplicate box constraints](@id manual-abstract-box-dedup) - -**Box constraints** are linear range constraints on a single scalar component of the state, control or variable — that is, constraints of the form `lb ≤ x_i(t) ≤ ub`, `u_i(t) ≤ ub`, `v_i ≥ lb`, etc. (nonlinear path constraints are *not* concerned by this section). - -When the **same scalar component** is targeted by several box-constraint declarations, OptimalControl does **not** keep them as separate constraints. Instead, it merges them by taking the **intersection** of all declared bounds: - -- the effective lower bound is `max` of all declared lower bounds, -- the effective upper bound is `min` of all declared upper bounds, -- a single `@warn` is emitted per duplicated component, listing every contributing label, -- all labels that declared the component are preserved as **aliases** (accessible via the `aliases` field of [`state_constraints_box`](@ref), [`control_constraints_box`](@ref) and [`variable_constraints_box`](@ref); see the [manual on the OCP object](@ref manual-model)), -- if the intersection is empty (`max(lbs) > min(ubs)`), an `IncorrectArgument` exception is thrown. - -For instance, - -```julia -@def begin - t ∈ [0, 1], time - x = (q, v) ∈ R², state - u ∈ R, control - 0 ≤ q(t) ≤ 2, (q_wide) - 1 ≤ q(t) ≤ 3, (q_tight) - ẋ(t) == [v(t), u(t)] - ... -end -``` - -yields the effective constraint `1 ≤ q(t) ≤ 2`, with `aliases = [:q_wide, :q_tight]` for that component, and a warning reporting both labels. - -Conversely, the following declares an empty feasible set and raises an error at build time: - -```julia -@def begin - t ∈ [0, 1], time - x = (q, v) ∈ R², state - u ∈ R, control - 0 ≤ q(t) ≤ 1, (low) - 2 ≤ q(t) ≤ 3, (high) # max(lbs)=2 > min(ubs)=1 ⇒ IncorrectArgument - ẋ(t) == [v(t), u(t)] - ... -end -``` - -## [Mayer cost](@id manual-abstract-mayer) - -```julia -:( $e1 → min ) -:( $e1 → max ) -``` - -Mayer costs are defined in a similar way to boundary conditions and follow the same rules. The symbol `→` is used -to denote minimisation or maximisation, the latter being treated by minimising the opposite cost. (The symbol `=>` can also be used.) - -```@repl main-repl -@def begin - tf ∈ R, variable - t ∈ [0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - tf ≥ 0 - -1 ≤ u(t) ≤ 1 - q(0) == 1 - v(0) == 2 - q(tf) == 0 - v(tf) == 0 - 0 ≤ q(t) ≤ 5 - -2 ≤ v(t) ≤ 3 - ẋ(t) == [v(t), u(t)] - tf → min -end -``` - -## Lagrange cost - -```julia -:( ∫($e1) → min ) -:( - ∫($e1) → min ) -:( $e1 * ∫($e2) → min ) -:( ∫($e1) → max ) -:( - ∫($e1) → max ) -:( $e1 * ∫($e2) → max ) -``` - -Lagrange (integral) costs are defined used the symbol `∫`, *with parentheses*. The keyword `integral` can also be used: - -```julia -@def begin - t ∈ [0, 1], time - x = (q, v) ∈ R², state - u ∈ R, control - 0.5∫(q(t) + u(t)^2) → min - ... -end -``` - -or - -```julia -@def begin - t ∈ [0, 1], time - x = (q, v) ∈ R², state - u ∈ R, control - 0.5integral(q(t) + u(t)^2) → min - ... -end -``` - -The integration range is implicitly equal to the time range, so the cost above is to be understood as - -```math -\frac{1}{2} \int_0^1 \left( q(t) + u^2(t) \right) \mathrm{d}t \to \min. -``` - -As for the dynamics, the parser will detect whether the integrand depends or not on time (autonomous / non-autonomous case). - -## Bolza cost - -```julia -:( $e1 + ∫($e2) → min ) -:( $e1 + $e2 * ∫($e3) → min ) -:( $e1 - ∫($e2) → min ) -:( $e1 - $e2 * ∫($e3) → min ) -:( $e1 + ∫($e2) → max ) -:( $e1 + $e2 * ∫($e3) → max ) -:( $e1 - ∫($e2) → max ) -:( $e1 - $e2 * ∫($e3) → max ) -:( ∫($e2) + $e1 → min ) -:( $e2 * ∫($e3) + $e1 → min ) -:( ∫($e2) - $e1 → min ) -:( $e2 * ∫($e3) - $e1 → min ) -:( ∫($e2) + $e1 → max ) -:( $e2 * ∫($e3) + $e1 → max ) -:( ∫($e2) - $e1 → max ) -:( $e2 * ∫($e3) - $e1 → max ) -``` - -Quite readily, Mayer and Lagrange costs can be combined into general Bolza costs. For instance as follows: - -```julia -@def begin - p = (t0, tf) ∈ R², variable - t ∈ [t0, tf], time - x = (q, v) ∈ R², state - u ∈ R², control - (tf - t0) + 0.5∫(c(t) * u(t)^2) → min - ... -end -``` - -!!! warning - The expression must be the sum of two terms (plus, possibly, a scalar factor before the integral), not *more*, so mind the parentheses. For instance, the following errors: - - ```julia - @def begin - p = (t0, tf) ∈ R², variable - t ∈ [t0, tf], time - x = (q, v) ∈ R², state - u ∈ R², control - (tf - t0) + q(tf) + 0.5∫( c(t) * u(t)^2 ) → min - ... - end - ``` - - The correct syntax is - - ```julia - @def begin - p = (t0, tf) ∈ R², variable - t ∈ [t0, tf], time - x = (q, v) ∈ R², state - u ∈ R², control - ((tf - t0) + q(tf)) + 0.5∫( c(t) * u(t)^2 ) → min - ... - end - ``` - -## [Aliases](@id manual-abstract-aliases) - -```julia -:( $a = $e1 ) -``` - -The single `=` symbol is used to define not a constraint but an alias, that is a purely syntactic replacement. There are some automatic aliases, *e.g.* `x₁` and `x1` for `x[1]` if `x` is the state (same for variable and control, for indices comprised between 1 and 9), and we have also seen that the user can define her own aliases when declaring the [variable](@ref manual-abstract-variable), [state](@ref manual-abstract-state) and [control](@ref manual-abstract-control). Arbitrary aliases can be further defined, as below (compare with previous examples in the [dynamics](@ref manual-abstract-dynamics) section): - -```julia -@def begin - t ∈ [0, 1], time - x ∈ R², state - u ∈ R, control - F₀ = [x₂(t), 0] - F₁ = [0, 1] - ẋ(t) == F₀ + u(t) * F₁ - ... -end -``` - -!!! warning - Such aliases do *not* define any additional function and are just replaced textually by the parser. In particular, they cannot be used outside the `@def` `begin ... end` block. Conversely, constants and functions used within the `@def` block must be defined outside and before this block. - -!!! hint - You can rely on a trace mode for the macro `@def` to look at your code after expansions of the aliases using the `@def ocp ...` syntax and adding `true` after your `begin ... end` block: - - ```@repl main-repl - @def damped_integrator begin - tf ∈ R, variable - t ∈ [0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - q̇ = v(t) - v̇ = u(t) - c(t) - ẋ(t) == [q̇, v̇] - end true; - ``` - -!!! warning - The dynamics of an OCP is indeed a particular constraint, be careful to use `==` and not a single `=` that would try to define an alias: - - ```@repl main-repl - double_integrator = @def begin - tf ∈ R, variable - t ∈ [0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - q̇ = v - v̇ = u - ẋ(t) = [q̇, v̇] - end - ``` - -## Misc - -- Declarations (of variable - if any -, time, state and control - if any -) must be done first. Then, dynamics, constraints and cost can be introduced in an arbitrary order. -- It is possible to provide numbers / labels (as in math equations) for the constraints to improve readability (this is mostly for future use, typically to retrieve the Lagrange multiplier associated with the discretisation of a given constraint): - -```julia -@def damped_integrator begin - tf ∈ R, variable - t ∈ [0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - tf ≥ 0, (1) - q(0) == 2, (♡) - q̇ = v(t) - v̇ = u(t) - c(t) - ẋ(t) == [q̇, v̇] - x(t).^2 ≤ [1, 2], (state_con) - ... -end -``` - -- Parsing errors should be explicit enough (with line number in the `@def` `begin ... end` block indicated) 🤞🏾 -- Check tutorials and applications in the documentation for further use. - -## [Known issues](@id manual-abstract-known-issues) - -- [Reverse over forward AD issues with ADNLP](https://github.com/control-toolbox/OptimalControl.jl/issues/481#issuecomment-3471352183) diff --git a/docs/attic/manual-ai-llm.md b/docs/attic/manual-ai-llm.md deleted file mode 100644 index c45df6b29..000000000 --- a/docs/attic/manual-ai-llm.md +++ /dev/null @@ -1,202 +0,0 @@ -# [Use AI](@id manual-ai-llm) - -Using now common models from AI, it is an elementary task to have such an agent learn the syntax of OptimalControl.jl DSL, then use the agent to translate standard math into this DSL. Here is a typical prompt, pointing to the [DSL syntax](@ref manual-abstract-syntax): - -```text -Learn the syntax of OptimalControl.jl DSL described at the link below -to translate math into this DSL (Julia language): -https://control-toolbox.org/OptimalControl.jl/stable/manual-abstract.html. -``` - -```@raw html -
- Try with: - - - ChatGPT - - - - - Claude - - - - - Perplexity - - - - - Mistral - -
-``` - -Now, here is an extract of, *e.g.*, [claude.ai](https://claude.ai) answer (Sonnet 4 model) to the request below: - -```text -Translate the problem below into this DSL: - - minimise the final time tf - under the constraint q''(t) = u(t) - with -1 <= u(t) <= 1, - q(0) = 1, q'(0) = -2, q(tf) = 3, q'(tf) = 1, and q'(t) <= 1. -``` - -```julia -@def begin - tf ∈ R, variable - t ∈ [0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - - # Variable constraint - tf ≥ 0 - - # Control constraint - -1 ≤ u(t) ≤ 1 - - # Initial conditions - q(0) == 1 - v(0) == -2 - - # Final conditions - q(tf) == 3 - v(tf) == 1 - - # Path constraint - v(t) ≤ 1 - - # Dynamics (q'' = u becomes first-order system) - ẋ(t) == [v(t), u(t)] - - # Objective: minimize final time - tf → min -end -``` - -The answer is syntactically correct and can be updated, for instance according to the new request below: - -```text -Use a form compatible with examodels. -``` - -```julia -@def begin - tf ∈ R, variable - t ∈ [0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - - # Variable constraint - tf ≥ 0 - - # Control constraint - -1 ≤ u(t) ≤ 1 - - # Initial conditions - q(0) == 1 - v(0) == -2 - - # Final conditions - q(tf) == 3 - v(tf) == 1 - - # Path constraint - v(t) ≤ 1 - - # Dynamics (coordinate-wise for ExaModels) - ∂(q)(t) == v(t) - ∂(v)(t) == u(t) - - # Objective: minimize final time - tf → min -end -``` - -It is also possible to feed the model directly with math definitions (here from a snapshot), as in the [example](https://control-toolbox.org/OptimalControlProblems.jl/stable/problems/rocket.html) below: - -```text -translate the problem below into OptimalControl.jl DSL: -``` - -```@raw html -rocket-def - -``` - -This prompt generates the syntactically correct code: - -```julia -# Parameters -h₀ = 1 -v₀ = 0 -m₀ = 1 -g₀ = 1 -Tc = 3.5 -hc = 500 -vc = 620 -mc = 0.6 - -# Derived constants -Dc = 0.5 * vc * m₀ / g₀ -mf = mc * m₀ -c = 0.5 * sqrt(g₀ * h₀) -Tmax = Tc * m₀ * g₀ - -# Auxiliary functions -D(h, v) = Dc * v^2 * exp(-hc * (h - h₀) / h₀) -g(h) = g₀ * (h₀ / h)^2 - -rocket = @def begin - # Variable (free final time) - tf ∈ R, variable - - # Time - t ∈ [0, tf], time - - # State: (altitude, velocity, mass) - x = (h, v, m) ∈ R³, state - - # Control: thrust - T ∈ R, control - - # Dynamics - ∂(h)(t) == v(t) - ∂(v)(t) == (T(t) - D(h(t), v(t)) - m(t) * g(h(t))) / m(t) - ∂(m)(t) == -T(t) / c - - # Initial conditions - h(0) == h₀ - v(0) == v₀ - m(0) == m₀ - - # Final condition - m(tf) == mf - - # State constraints - h(t) ≥ h₀ - v(t) ≥ v₀ - mf ≤ m(t) ≤ m₀ - - # Control constraint - 0 ≤ T(t) ≤ Tmax - - # Variable constraint - tf ≥ 0 - - # Objective: maximize final altitude h(tf) - -h(tf) → min -end -``` diff --git a/docs/attic/manual-differential-geometry.md b/docs/attic/manual-differential-geometry.md deleted file mode 100644 index 23e88329a..000000000 --- a/docs/attic/manual-differential-geometry.md +++ /dev/null @@ -1,634 +0,0 @@ -# [Differential geometry tools](@id manual-differential-geometry) - -Optimal control theory relies on differential geometry tools to analyze Hamiltonian systems, compute singular controls, study controllability, and more. This page introduces the main operators available in OptimalControl.jl: Hamiltonian lift, Lie derivatives, Poisson brackets, Lie brackets, and partial time derivatives. - -!!! note "Type qualification" - - Types like `Hamiltonian`, `HamiltonianLift`, `VectorField`, and `HamiltonianVectorField` are **not exported** by OptimalControl.jl. You must qualify them with `OptimalControl.` when using them (e.g., `OptimalControl.VectorField`). Functions and operators (`Lift`, `⋅`, `Lie`, `Poisson`, `@Lie`, `∂ₜ`) are exported and can be used directly. - -First, import the package: - -```@example main -using OptimalControl -``` - -## Hamiltonian lift - -Given a vector field $X: \mathbb{R}^n \to \mathbb{R}^n$, its **Hamiltonian lift** is the function $H_X: \mathbb{R}^n \times (\mathbb{R}^n)^* \to \mathbb{R}$ defined by - -```math -H_X(x, p) = \langle p, X(x) \rangle = \sum_{i=1}^n p_i X_i(x). -``` - -### From plain Julia functions - -The simplest way to compute a Hamiltonian lift is from a plain Julia function. By default, the function is treated as **autonomous** (time-independent) and **non-variable** (no extra parameter): - -```@example main -# Define a vector field as a Julia function -X(x) = [x[2], -x[1]] - -# Compute its Hamiltonian lift -H = Lift(X) - -# Evaluate at a point (x, p) -x = [1, 2] -p = [3, 4] -H(x, p) -``` - -The result is $H(x, p) = p_1 x_2 + p_2 (-x_1) = 3 \times 2 + 4 \times (-1) = 2$. - -### From VectorField type - -You can also use the `OptimalControl.VectorField` type, which allows more control over the function's properties: - -```@example main-1 -using OptimalControl # hide -# Wrap in VectorField (autonomous, non-variable by default) -X = OptimalControl.VectorField(x -> [x[2], -x[1]]) -H = Lift(X) - -# This returns a HamiltonianLift object -H([1, 2], [3, 4]) -``` - -### Non-autonomous case - -For time-dependent vector fields, use `autonomous=false`: - -```@example main-2 -using OptimalControl # hide -# Non-autonomous vector field: X(t, x) = [t*x[2], -x[1]] -X(t, x) = [t * x[2], -x[1]] -H = Lift(X; autonomous=false) - -# Signature is now H(t, x, p) -H(2, [1, 2], [3, 4]) -``` - -### Variable case - -For vector fields depending on an additional parameter $v$, use `variable=true`: - -```@example main-3 -using OptimalControl # hide -# Variable vector field: X(x, v) = [x[2] + v, -x[1]] -X(x, v) = [x[2] + v, -x[1]] -H = Lift(X; variable=true) - -# Signature is now H(x, p, v) -H([1, 2], [3, 4], 1) -``` - -## Lie derivative - -The **Lie derivative** of a function $f: \mathbb{R}^n \to \mathbb{R}$ along a vector field $X$ is defined by - -```math -(X \cdot f)(x) = f'(x) \cdot X(x) = \sum_{i=1}^n \frac{\partial f}{\partial x_i}(x) X_i(x). -``` - -This represents the directional derivative of $f$ along $X$. - -### [From plain Julia functions](@id lie-from-functions) - -When using plain Julia functions, they are treated as autonomous and non-variable: - -```@example main-4 -using OptimalControl # hide -# Vector field and scalar function -X(x) = [x[2], -x[1]] -f(x) = x[1]^2 + x[2]^2 - -# Lie derivative (using dot operator) -Xf = X ⋅ f - -# Evaluate at a point -Xf([1, 2]) -``` - -For the harmonic oscillator with $X(x) = (x_2, -x_1)$ and energy $f(x) = x_1^2 + x_2^2$: - -```math -(X \cdot f)(x) = 2x_1 x_2 + 2x_2(-x_1) = 0, -``` - -which confirms that energy is conserved along trajectories. - -### [From VectorField type](@id lie-from-vectorfield) - -```@example main-5 -using OptimalControl # hide -# Using VectorField type -X = OptimalControl.VectorField(x -> [x[2], -x[1]]) -g(x) = x[1]^2 + x[2]^2 - -# Lie derivative -Xg = X ⋅ g -Xg([1, 2]) -``` - -### Alternative syntax - -The `Lie` function is equivalent to the `⋅` operator: - -```@example main-5 -# These are equivalent -Xg1 = X ⋅ g -Xg2 = Lie(X, g) - -Xg1([1, 2]) == Xg2([1, 2]) -``` - -### With keyword arguments - -For non-autonomous or variable cases, use the `Lie` function with keyword arguments: - -```@example main-6 -using OptimalControl # hide -# Non-autonomous case -X(t, x) = [t + x[2], -x[1]] -f(t, x) = t + x[1]^2 + x[2]^2 - -Xf = Lie(X, f; autonomous=false) -Xf(1, [1, 2]) -``` - -```@example main-7 -using OptimalControl # hide -# Variable case -X(x, v) = [x[2] + v, -x[1]] -f(x, v) = x[1]^2 + x[2]^2 + v - -Xf = Lie(X, f; variable=true) -Xf([1, 2], 1) -``` - -### With VectorField type - -You can also create the VectorField explicitly with the keywords, then use it without keywords in the Lie function: - -```@example main-7a -using OptimalControl # hide -# Non-autonomous VectorField created with keywords -X = OptimalControl.VectorField((t, x) -> [t + x[2], -x[1]]; autonomous=false) -f(t, x) = t + x[1]^2 + x[2]^2 - -# No keywords needed here - the VectorField already knows its properties -Xf = Lie(X, f) -Xf(1, [1, 2]) -``` - -```@example main-7b -using OptimalControl # hide -# Variable VectorField created with keywords -X = OptimalControl.VectorField((x, v) -> [x[2] + v, -x[1]]; variable=true) -f(x, v) = x[1]^2 + x[2]^2 + v - -# No keywords needed here -Xf = Lie(X, f) -Xf([1, 2], 1) -``` - -## Poisson bracket - -For two functions $f, g: \mathbb{R}^n \times (\mathbb{R}^n)^* \to \mathbb{R}$, the **Poisson bracket** is defined by - -```math -\{f, g\}(x, p) = \sum_{i=1}^n \left( \frac{\partial f}{\partial p_i} \frac{\partial g}{\partial x_i} - \frac{\partial f}{\partial x_i} \frac{\partial g}{\partial p_i} \right). -``` - -### Properties - -The Poisson bracket satisfies: - -- **Bilinearity**: $\{af + bg, h\} = a\{f, h\} + b\{g, h\}$ for scalars $a, b$ -- **Antisymmetry**: $\{f, g\} = -\{g, f\}$ -- **Leibniz rule**: $\{fg, h\} = f\{g, h\} + g\{f, h\}$ -- **Jacobi identity**: $\{\{f, g\}, h\} + \{\{h, f\}, g\} + \{\{g, h\}, f\} = 0$ - -### [From plain Julia functions](@id poisson-from-functions) - -```@example main-8 -using OptimalControl # hide -# Define two Hamiltonian functions -f(x, p) = p[1] * x[2] + p[2] * x[1] -g(x, p) = x[1]^2 + p[2]^2 - -# Compute the Poisson bracket -H = Poisson(f, g) - -# Evaluate at a point -x = [1, 2] -p = [3, 4] -H(x, p) -``` - -### Verify antisymmetry - -```@example main-8 -Hfg = Poisson(f, g) -Hgf = Poisson(g, f) - -println("Poisson(f, g) = ", Hfg(x, p)) -println("Poisson(g, f) = ", Hgf(x, p)) -println("Sum = ", Hfg(x, p) + Hgf(x, p)) -``` - -### From Hamiltonian type - -```@example main-8 -# Wrap in Hamiltonian type -F = OptimalControl.Hamiltonian(f) -G = OptimalControl.Hamiltonian(g) - -H = Poisson(F, G) -H(x, p) -``` - -### [With keyword arguments](@id poisson-kwargs) - -```@example main-9 -using OptimalControl # hide -# Non-autonomous case -f(t, x, p) = t + p[1] * x[2] + p[2] * x[1] -g(t, x, p) = t^2 + x[1]^2 + p[2]^2 - -H = Poisson(f, g; autonomous=false) -H(1, [1, 2], [3, 4]) -``` - -### With Hamiltonian type and keywords - -You can also create Hamiltonian objects explicitly with keywords, then use them without keywords in the Poisson function. **Important**: both Hamiltonians must have the same time and variable dependencies: - -```@example main-9a -using OptimalControl # hide -# Non-autonomous Hamiltonians created with keywords -f(t, x, p) = t + p[1] * x[2] + p[2] * x[1] -g(t, x, p) = t^2 + x[1]^2 + p[2]^2 - -F = OptimalControl.Hamiltonian(f; autonomous=false) -G = OptimalControl.Hamiltonian(g; autonomous=false) - -# No keywords needed here - both Hamiltonians are already non-autonomous -H = Poisson(F, G) -H(1, [1, 2], [3, 4]) -``` - -```@example main-9b -using OptimalControl # hide -# Variable Hamiltonians created with keywords -f(x, p, v) = x[1]^2 + p[2]^2 + v -g(x, p, v) = x[2]^2 + p[1]^2 + 2*v - -F = OptimalControl.Hamiltonian(f; variable=true) -G = OptimalControl.Hamiltonian(g; variable=true) - -# Both are variable, so the Poisson bracket is also variable -H = Poisson(F, G) -H([1, 2], [3, 4], 1) -``` - -### Relation to Hamiltonian vector fields - -The Poisson bracket is closely related to the Lie derivative. If $\vec{H} = (\nabla_p H, -\nabla_x H)$ denotes the Hamiltonian vector field associated to $H$, then - -```math -\{H, G\} = \vec{H} \cdot G. -``` - -This means the Poisson bracket of $H$ and $G$ equals the Lie derivative of $G$ along the Hamiltonian vector field of $H$. - -### Poisson bracket of Hamiltonian lifts - -When computing the Poisson bracket of two Hamiltonian lifts, the result is the Hamiltonian lift of the Lie bracket of the underlying vector fields: - -```@example main-10 -using OptimalControl # hide -# Two vector fields -X(x) = [x[1]^2, x[2]^2] -Y(x) = [x[2], -x[1]] - -# Their Hamiltonian lifts -HX = Lift(X) -HY = Lift(Y) - -# Poisson bracket of lifts -H = Poisson(HX, HY) -H([1, 2], [3, 4]) -``` - -This satisfies: $\{H_X, H_Y\} = H_{[X,Y]}$ where $[X,Y]$ is the Lie bracket of vector fields (see next section). - -## Lie bracket of vector fields - -For two vector fields $X, Y: \mathbb{R}^n \to \mathbb{R}^n$, the **Lie bracket** is the vector field $[X, Y]$ defined by - -```math -[X, Y](x) = Y'(x) \cdot X(x) - X'(x) \cdot Y(x), -``` - -where $X'(x)$ denotes the Jacobian matrix of $X$ at $x$. - -### [From VectorField type](@id bracket-from-vectorfield) - -```@example main-11 -using OptimalControl # hide -# Define two vector fields -X = OptimalControl.VectorField(x -> [x[2], -x[1]]) -Y = OptimalControl.VectorField(x -> [x[1], x[2]]) - -# Compute the Lie bracket -Z = Lie(X, Y) - -# Evaluate at a point -Z([1, 2]) -``` - -### Relation to Poisson brackets - -If $H_X = \text{Lift}(X)$ and $H_Y = \text{Lift}(Y)$ are the Hamiltonian lifts, then: - -```math -\{H_X, H_Y\} = H_{[X,Y]}. -``` - -Let's verify this numerically: - -```@example main-11 -# Hamiltonian lifts -HX = Lift(x -> X(x)) -HY = Lift(x -> Y(x)) - -# Poisson bracket of the lifts -HXY = Poisson(HX, HY) - -# Lift of the Lie bracket -HZ = Lift(x -> Z(x)) - -# Compare at a point -x = [1, 2] -p = [3, 4] - -println("Poisson bracket: ", HXY(x, p)) -println("Lift of Lie bracket: ", HZ(x, p)) -``` - -## The `@Lie` macro - -The `@Lie` macro provides a convenient syntax for computing Lie brackets (for vector fields) and Poisson brackets (for Hamiltonians). - -!!! warning "Important distinction" - - - **Square brackets `[...]`** denote **Lie brackets** and work with: - - `VectorField` objects - - Plain Julia functions (automatically wrapped as `VectorField`) - - **Curly braces `{...}`** denote **Poisson brackets** and work with: - - Plain Julia functions (automatically wrapped as `Hamiltonian`) - - `Hamiltonian` objects - - When using **only plain functions** (no `VectorField` or `Hamiltonian` objects), specify `autonomous` and/or `variable` keywords as needed to match your function signature. Keywords are optional - if not specified, they use the default values (`autonomous=true`, `variable=false`). If you mix plain functions with `VectorField` or `Hamiltonian` objects, the keywords are inferred from the `VectorField` or `Hamiltonian` objects. - -### Lie brackets with VectorField - -```@example main-12 -using OptimalControl # hide -# Define vector fields -F1 = OptimalControl.VectorField(x -> [0, -x[3], x[2]]) -F2 = OptimalControl.VectorField(x -> [x[3], 0, -x[1]]) - -# Compute Lie bracket using macro -F12 = @Lie [F1, F2] - -# Evaluate -F12([1, 2, 3]) -``` - -### Nested Lie brackets - -```@example main-12 -F3 = OptimalControl.VectorField(x -> [x[1], x[2], x[3]]) -F123 = @Lie [[F1, F2], F3] -F123([1, 2, 3]) -``` - -### Lie brackets with plain Julia functions - -You can also use plain Julia functions directly with the `@Lie` macro. The functions will be automatically wrapped in `VectorField` objects: - -```@example main-12a -using OptimalControl # hide -# Define plain Julia functions -X(x) = [x[2], -x[1]] -Y(x) = [x[1], x[2]] - -# Compute Lie bracket using macro with plain functions -Z = @Lie [X, Y] - -# Evaluate -Z([1, 2]) -``` - -### With keyword arguments for plain functions - -For non-autonomous or variable cases, specify the keywords: - -```@example main-12b -using OptimalControl # hide -# Non-autonomous plain functions -X(t, x) = [t + x[2], -x[1]] -Y(t, x) = [x[1], t*x[2]] - -# Use autonomous=false keyword -Z = @Lie [X, Y] autonomous=false -Z(1, [1, 2]) -``` - -```@example main-12c -using OptimalControl # hide -# Variable plain functions -X(x, v) = [x[2] + v, -x[1]] -Y(x, v) = [x[1], x[2] + v] - -# Use variable=true keyword -Z = @Lie [X, Y] variable=true -Z([1, 2], 1) -``` - -### Nested brackets with plain functions - -```@example main-12d -using OptimalControl # hide -X(x) = [0, -x[3], x[2]] -Y(x) = [x[3], 0, -x[1]] -Z(x) = [x[1], x[2], x[3]] - -# Nested Lie brackets -nested = @Lie [[X, Y], Z] -nested([1, 2, 3]) -``` - -!!! tip "Plain functions vs VectorField" - - Using plain functions with `@Lie [X, Y]` is convenient for quick computations. However, if you need to reuse the same vector field multiple times or want explicit control over the autonomy/variability, consider creating `VectorField` objects explicitly: - - ```julia - # Explicit VectorField (keywords at creation) - X = OptimalControl.VectorField((t, x) -> [t + x[2], -x[1]]; autonomous=false) - Y = OptimalControl.VectorField((t, x) -> [x[1], t*x[2]]; autonomous=false) - Z = @Lie [X, Y] # No keywords needed - - # Plain functions (keywords at macro call) - X(t, x) = [t + x[2], -x[1]] - Y(t, x) = [x[1], t*x[2]] - Z = @Lie [X, Y] autonomous=false - ``` - -### Poisson brackets from plain functions - -For Hamiltonian functions (plain Julia functions), use curly braces `{_, _}`: - -```@example main-13 -using OptimalControl # hide -# Define Hamiltonian functions -H0(x, p) = p[1] * x[2] + p[2] * (-x[1]) -H1(x, p) = p[2] - -# Compute Poisson bracket -H01 = @Lie {H0, H1} - -# Evaluate -H01([1, 2], [3, 4]) -``` - -### Iterated Poisson brackets - -The macro is particularly useful for computing iterated brackets, which appear in singular control analysis: - -```@example main-13 -# First-order bracket -H01 = @Lie {H0, H1} - -# Second-order brackets -H001 = @Lie {H0, H01} -H101 = @Lie {H1, H01} - -# Evaluate -x = [1, 2] -p = [3, 4] - -println("H01(x, p) = ", H01(x, p)) -println("H001(x, p) = ", H001(x, p)) -println("H101(x, p) = ", H101(x, p)) -``` - -These iterated brackets are used to compute singular controls. For a pseudo-Hamiltonian of the form $H = H_0 + u H_1$, if the switching function $H_1$ vanishes on an interval (singular arc), the control is given by - -```math -u_s = -\frac{H_{001}}{H_{101}}, -``` - -provided $H_{101} \neq 0$. See the [singular control example](@ref example-singular-control) for a complete application. - -### [With keyword arguments](@id macro-kwargs) - -For non-autonomous functions, specify `autonomous=false`: - -```@example main-14 -using OptimalControl # hide -# Non-autonomous Hamiltonians -H0(t, x, p) = t + p[1] * x[2] + p[2] * (-x[1]) -H1(t, x, p) = p[2] - -# Poisson bracket with keyword -H01 = @Lie {H0, H1} autonomous=false - -# Evaluate -H01(1, [1, 2], [3, 4]) -``` - -### Poisson brackets from Hamiltonian type - -```@example main-15 -using OptimalControl # hide -# Using Hamiltonian type -H1 = OptimalControl.Hamiltonian((x, p) -> x[1]^2 + p[2]^2) -H2 = OptimalControl.Hamiltonian((x, p) -> x[2]^2 + p[1]^2) - -# Macro works with Hamiltonian objects too -H12 = @Lie {H1, H2} -H12([1, 1], [3, 2]) -``` - -## Partial time derivative - -For non-autonomous functions $f(t, x, \ldots)$, the **partial derivative with respect to time** is computed using the `∂ₜ` operator: - -```math -(\partial_t f)(t, x, \ldots) = \frac{\partial f}{\partial t}(t, x, \ldots). -``` - -### Basic usage - -```@example main-16 -using OptimalControl # hide -# Define a time-dependent function -f(t, x) = t * x - -# Compute partial time derivative -df = ∂ₜ(f) - -# Evaluate -df(0, 8) -``` - -```@example main-16 -df(2, 3) -``` - -### More complex example - -```@example main-17 -using OptimalControl # hide -# Function with multiple arguments -g(t, x, p) = t^2 + x[1] * p[1] + x[2] * p[2] - -# Partial derivative -dg = ∂ₜ(g) - -# At t=3, ∂g/∂t = 2t = 6 -dg(3, [1, 2], [4, 5]) -``` - -### Relation to total time derivative - -For a non-autonomous Hamiltonian $H(t, x, p)$ and a function $G(t, x, p)$, the **total time derivative** along the Hamiltonian flow is: - -```math -\frac{\mathrm{d}}{\mathrm{d}t} G(t, x(t), p(t)) = \partial_t G + \{H, G\}. -``` - -This is the sum of: - -- The **partial time derivative** $\partial_t G$ (explicit time dependence) -- The **Poisson bracket** $\{H, G\}$ (evolution along the flow) - -This relation is fundamental in non-autonomous optimal control theory. - -## Summary - -| Function/Operator | Mathematical notation | Julia syntax | -| ----------------- | --------------------- | ------------ | -| Hamiltonian lift | $H_X(x,p) = \langle p, X(x) \rangle$ | `H = Lift(X)` | -| Lie derivative | $(X \cdot f)(x) = f'(x) \cdot X(x)$ | `X ⋅ f` or `Lie(X, f)` | -| Poisson bracket | $\{f,g\}(x,p) = \langle \nabla_p f, \nabla_x g \rangle - \langle \nabla_x f, \nabla_p g \rangle$ | `Poisson(f, g)` or `@Lie {f, g}` | -| Lie bracket | $[X,Y](x) = Y'(x) X(x) - X'(x) Y(x)$ | `Lie(X, Y)` or `@Lie [X, Y]` | -| Partial time derivative | $\partial_t f(t, x, \ldots)$ | `∂ₜ(f)` | - -## See also - -- [Compute flows from Hamiltonians and others](@ref manual-flow-others) — Using flows with Hamiltonian vector fields -- [Singular control example](@ref example-singular-control) — Application to computing singular controls -- [Goddard tutorial](@extref Tutorials tutorial-goddard) — Complex example with bang, singular, and boundary arcs diff --git a/docs/attic/manual-flow-ocp.md b/docs/attic/manual-flow-ocp.md deleted file mode 100644 index 10f5c224f..000000000 --- a/docs/attic/manual-flow-ocp.md +++ /dev/null @@ -1,688 +0,0 @@ -# [How to compute flows from optimal control problems](@id manual-flow-ocp) - -In this tutorial, we explain the `Flow` function, in particular to compute flows from an optimal control problem. - -!!! note "Current limitation" - Currently, from an optimal control problem, only **Hamiltonian flows** can be constructed, i.e. the control law must be provided in **feedback form depending on both state and costate**: `u(x, p)`. An active refactoring is under way to extend support to non-Hamiltonian flows, in particular open-loop control laws `u(t)` and state feedback control laws `u(x)`. - -## Basic usage - -Les us define a basic optimal control problem. - -```@example main -using OptimalControl - -t0 = 0 -tf = 1 -x0 = [-1, 0] - -ocp = @def begin - - t ∈ [ t0, tf ], time - x = (q, v) ∈ R², state - u ∈ R, control - - x(t0) == x0 - x(tf) == [0, 0] - ẋ(t) == [v(t), u(t)] - - ∫( 0.5u(t)^2 ) → min - -end -nothing # hide -``` - -The **pseudo-Hamiltonian** of this problem is - -```math - H(x, p, u) = p_q\, v + p_v\, u + p^0 u^2 /2, -``` - -where $p^0 = -1$ since we are in the normal case. From the Pontryagin maximum principle, the maximising control is given in feedback form by - -```math -u(x, p) = p_v -``` - -since $\partial^2_{uu} H = p^0 = - 1 < 0$. - -```@example main -u(x, p) = p[2] -nothing # hide -``` - -Actually, if $(x, u)$ is a solution of the optimal control problem, then, the Pontryagin maximum principle tells us that there exists a costate $p$ such that $u(t) = u(x(t), p(t))$ and such that the pair $(x, p)$ satisfies: - -```math -\begin{array}{l} - \dot{x}(t) = \displaystyle\phantom{-}\nabla_p H(x(t), p(t), u(x(t), p(t))), \\[0.5em] - \dot{p}(t) = \displaystyle - \nabla_x H(x(t), p(t), u(x(t), p(t))). -\end{array} -``` - -The `Flow` function aims to compute $t \mapsto (x(t), p(t))$ from the optimal control problem `ocp` and the control in feedback form `u(x, p)`. - -!!! note "Nota bene" - - Actually, writing $z = (x, p)$, then the pair $(x, p)$ is also solution of - - ```math - \dot{z}(t) = \vec{\mathbf{H}}(z(t)), - ``` - where $\mathbf{H}(z) = H(z, u(z))$ and $\vec{\mathbf{H}} = (\nabla_p \mathbf{H}, -\nabla_x \mathbf{H})$. This is what is actually computed by `Flow`. - -Let us try to get the associated flow: - -```julia -julia> f = Flow(ocp, u) -ERROR: ExtensionError. Please make: julia> using OrdinaryDiffEq -``` - -As you can see, an error occurred since we need the package [OrdinaryDiffEq.jl](https://docs.sciml.ai/DiffEqDocs). This package provides numerical integrators to compute solutions of the ordinary differential equation $\dot{z}(t) = \vec{\mathbf{H}}(z(t))$. - -!!! note "OrdinaryDiffEq.jl" - - The package OrdinaryDiffEq.jl is part of [DifferentialEquations.jl](https://docs.sciml.ai/DiffEqDocs). You can either use one or the other. - -```@example main -using OrdinaryDiffEq -f = Flow(ocp, u) -nothing # hide -``` - -Now we have the flow of the associated Hamiltonian vector field, we can use it. Some simple calculations shows that the initial covector $p(0)$ solution of the Pontryagin maximum principle is $[12, 6]$. Let us check that integrating the flow from $(t_0, x_0, p_0) = (0, [-1, 0], [12, 6])$ to the final time $t_f$ we reach the target $x_f = [0, 0]$. - -```@example main -p0 = [12, 6] -xf, pf = f(t0, x0, p0, tf) -xf -``` - -If you prefer to get the state, costate and control trajectories at any time, you can call the flow like this: - -```@example main -sol = f((t0, tf), x0, p0) -nothing # hide -``` - -In this case, you obtain a data that you can plot exactly like when solving the optimal control problem with the function [`solve`](@ref). See for instance the [basic example](@ref example-double-integrator-energy-solve-plot) or the [plot tutorial](@ref manual-plot). - -```@example main -using Plots -plot(sol) -``` - -You can notice from the graph of `v` that the integrator has made very few steps: - -```@example main -time_grid(sol) -``` - -!!! note "Time grid" - - The function [`time_grid`](@ref) returns the discretised time grid returned by the solver. In this case, the solution has been computed by numerical integration with an adaptive step-length Runge-Kutta scheme. - -To have a better visualisation (the accuracy won't change), you can provide a fine grid. - -```@example main -sol = f((t0, tf), x0, p0; saveat=range(t0, tf, 100)) -plot(sol) -``` - -The argument `saveat` is an option from OrdinaryDiffEq.jl. Please check the [list of common options](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/#solver_options). For instance, one can change the integrator with the keyword argument `alg` or the absolute tolerance with `abstol`. Note that you can set an option when declaring the flow or set an option in a particular call of the flow. In the following example, the integrator will be `BS5()` and the absolute tolerance will be `abstol=1e-8`. - -```@example main -f = Flow(ocp, u; alg=BS5(), abstol=1) # alg=BS5(), abstol=1 -xf, pf = f(t0, x0, p0, tf; abstol=1e-8) # alg=BS5(), abstol=1e-8 -``` - -## Non-autonomous case - -Let us consider the following optimal control problem: - -```@example main -t0 = 0 -tf = π/4 -x0 = 0 -xf = tan(π/4) - 2log(√(2)/2) - -ocp = @def begin - - t ∈ [t0, tf], time - x ∈ R, state - u ∈ R, control - - x(t0) == x0 - x(tf) == xf - ẋ(t) == u(t) * (1 + tan(t)) # The dynamics depend explicitly on t - - 0.5∫( u(t)^2 ) → min - -end -nothing # hide -``` - -The pseudo-Hamiltonian of this problem is - -```math - H(t, x, p, u) = p\, u\, (1+\tan\, t) + p^0 u^2 /2, -``` - -where $p^0 = -1$ since we are in the normal case. We can notice that the pseudo-Hamiltonian is non-autonomous since it explicitly depends on the time $t$. - -```@example main -is_autonomous(ocp) -``` - -From the Pontryagin maximum principle, the maximising control is given in feedback form by - -```math -u(t, x, p) = p\, (1+\tan\, t) -``` - -since $\partial^2_{uu} H = p^0 = - 1 < 0$. - -```@example main -u(t, x, p) = p * (1 + tan(t)) -nothing # hide -``` - -As before, the `Flow` function aims to compute $(x, p)$ from the optimal control problem `ocp` and the control in feedback form `u(t, x, p)`. Since the problem is non-autonomous, we must provide a control law that depends on time. - -```@example main -f = Flow(ocp, u) -nothing # hide -``` - -Now we have the flow of the associated Hamiltonian vector field, we can use it. Some simple calculations shows that the initial covector $p(0)$ solution of the Pontryagin maximum principle is $1$. Let us check that integrating the flow from $(t_0, x_0) = (0, 0)$ to the final time $t_f = \pi/4$ we reach the target $x_f = \tan(\pi/4) - 2 \log(\sqrt{2}/2)$. - -```@example main -p0 = 1 -xf, pf = f(t0, x0, p0, tf) -xf - (tan(π/4) - 2log(√(2)/2)) -``` - -## Variable - -Let us consider an optimal control problem with a (decision / optimisation) variable. - -```@example main -t0 = 0 -x0 = 0 - -ocp = @def begin - - tf ∈ R, variable # the optimisation variable is tf - t ∈ [t0, tf], time - x ∈ R, state - u ∈ R, control - - x(t0) == x0 - x(tf) == 1 - ẋ(t) == tf * u(t) - - tf + 0.5∫(u(t)^2) → min - -end -nothing # hide -``` - -As you can see, the variable is the final time `tf`. Note that the dynamics depends on `tf`. From the Pontryagin maximum principle, the solution is given by: - -```@example main -tf = (3/2)^(1/4) -p0 = 2tf/3 -nothing # hide -``` - -The input arguments of the maximising control are now the state `x`, the costate `p` and the variable `tf`. - -```@example main -u(x, p, tf) = tf * p -nothing # hide -``` - -Let us check that the final condition `x(tf) = 1` is satisfied. - -```@example main -f = Flow(ocp, u) -xf, pf = f(t0, x0, p0, tf, tf) -``` - -The usage of the flow `f` is the following: `f(t0, x0, p0, tf, v)` where `v` is the variable. If one wants to compute the state at time `t1 = 0.5`, then, one must write: - -```@example main -t1 = 0.5 -x1, p1 = f(t0, x0, p0, t1, tf) -``` - -!!! note "Free times" - - In the particular cases: the initial time `t0` is the only variable, the final time `tf` is the only variable, or the initial and final times `t0` and `tf` are the only variables and are in order `v=(t0, tf)`, the times do not need to be repeated in the call of the flow: - - ```@example main - xf, pf = f(t0, x0, p0, tf) - ``` - -Since the variable is the final time, we can make the time-reparameterisation $t = s\, t_f$ to normalise the time $s$ in $[0, 1]$. - -```@example main -ocp = @def begin - - tf ∈ R, variable - s ∈ [0, 1], time - x ∈ R, state - u ∈ R, control - - x(0) == 0 - x(1) == 1 - ẋ(s) == tf^2 * u(s) - - tf + (0.5*tf)*∫(u(s)^2) → min - -end - -f = Flow(ocp, u) -xf, pf = f(0, x0, p0, 1, tf) -``` - -Another possibility is to add a new state variable $t_f(s)$. The problem has no variable anymore. - -```@example main -ocp = @def begin - - s ∈ [0, 1], time - y = (x, tf) ∈ R², state - u ∈ R, control - - x(0) == 0 - x(1) == 1 - dx = tf(s)^2 * u(s) - dtf = 0 * u(s) # 0 - ẏ(s) == [dx, dtf] - - tf(1) + 0.5∫(tf(s) * u(s)^2) → min - -end - -u(y, q) = y[2] * q[1] - -f = Flow(ocp, u) -yf, pf = f(0, [x0, tf], [p0, 0], 1) -``` - -!!! danger "Bug" - - Note that in the previous optimal control problem, we have `dtf = 0 * u(s)` instead of `dtf = 0`. The latter does not work. - -!!! note "Goddard problem" - - In the [Goddard problem](https://control-toolbox.org/Tutorials.jl/stable/tutorial-goddard.html#tutorial-goddard-structure), you may find other constructions of flows, especially for singular and boundary arcs. - -## Augmented costate computation with `augment=true` - -When working with optimal control problems that have variables, it can be useful to compute the costate associated with the variable parameter. The `augment=true` keyword argument provides automatic computation of this costate without requiring manual construction of the augmented Hamiltonian system. - -### Mathematical background - -For an optimal control problem with Hamiltonian $H(t, x, p, v)$, where $x$ is the state, $p$ is the costate, and $v$ is a variable parameter, the **augmented system** treats the variable as an additional state with zero dynamics: - -```math -\begin{aligned} -\frac{\mathrm{d}x}{\mathrm{d}t} &= \frac{\partial H}{\partial p} \\ -\frac{\mathrm{d}v}{\mathrm{d}t} &= 0 \quad \text{(constant parameter)} \\ -\frac{\mathrm{d}p}{\mathrm{d}t} &= -\frac{\partial H}{\partial x} \\ -\frac{\mathrm{d}p_v}{\mathrm{d}t} &= -\frac{\partial H}{\partial v} -\end{aligned} -``` - -With the initial condition $p_v(t_0) = 0$, the final costate $p_v(t_f)$ represents the accumulated sensitivity: - -```math -p_v(t_f) = -\int_{t_0}^{t_f} \frac{\partial H}{\partial v}(t, x(t), p(t), v) \, \mathrm{d}t -``` - -### Usage - -Let us consider a harmonic oscillator problem where the pulsation $\omega$ is a variable parameter appearing in the dynamics: - -```@example main -q0 = 1 -v0 = 0 -t0 = 0 -tf = 1 - -ocp_aug = @def begin - ω ∈ R, variable # pulsation to optimize - t ∈ [t0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - - q(t0) == q0 - v(t0) == v0 - q(tf) == 0.0 - - ẋ(t) == [v(t), -ω^2 * q(t) + u(t)] - - ω^2 + 0.5∫(u(t)^2) → min -end - -# Maximizing control from Pontryagin's principle -u_aug(x, p, ω) = p[2] -f_aug = Flow(ocp_aug, u_aug) -nothing # hide -``` - -Without `augment=true`, the flow returns only the state and costate: - -```@example main -ω_val = π/2 -p0_val = [1.0, 0.5] -xf, pf = f_aug(t0, [q0, v0], p0_val, tf, ω_val) -println("q(tf) = ", xf[1], ", v(tf) = ", xf[2]) -``` - -With `augment=true`, the flow automatically computes and returns the costate associated with the variable `ω`: - -```@example main -xf, pf, pω = f_aug(t0, [q0, v0], p0_val, tf, ω_val; augment=true) -println("q(tf) = ", xf[1], ", v(tf) = ", xf[2], ", p_ω(tf) = ", pω) -``` - -The value `pω` represents the sensitivity of the Hamiltonian with respect to the pulsation parameter: - -```math -p_{ω}(t_f) = -\int_{t_0}^{t_f} \frac{\partial H}{\partial \omega}(t, x(t), p(t), \omega) \, \mathrm{d}t -``` - -with $p_{\omega}(t_0) = 0$ by construction. This is particularly useful for computing transversality conditions in control-free problems. - -### Advantages - -The `augment=true` feature provides several benefits: - -- **No manual work**: No need to manually construct the augmented Hamiltonian or augmented ODEs -- **Type-safe**: Automatic handling of scalar vs vector variables -- **Robust**: Uses the existing, well-tested `Flow(Hamiltonian(...))` infrastructure -- **Mathematical rigor**: Proper initial conditions and transversality handling - -### Error handling - -The `augment=true` option is only available for problems with variables: - -```julia -# This will throw an error (no variable in the problem) -ocp_no_var = @def begin - t ∈ [0, 1], time - x ∈ R, state - u ∈ R, control - x(0) == 0 - ẋ(t) == u(t) - ∫(u(t)^2) → min -end - -f_no_var = Flow(ocp_no_var, (x, p) -> p) -f_no_var(0, 0, 1, 1; augment=true) # ERROR: PreconditionError -``` - -Additionally, `augment=true` only works for point evaluation, not for trajectory computation: - -```julia -# This works (point evaluation) -xf, pf, pvf = f_aug(t0, x0, p0, tf, v; augment=true) - -# This will throw an error (trajectory call) -sol = f_aug((t0, tf), x0, p0, v; augment=true) # ERROR: PreconditionError -``` - -!!! note "Control-free problems" - - The `augment=true` feature is particularly useful for control-free problems where the variable parameter appears in the dynamics. See the [control-free problems example](@ref example-control-free) for detailed applications with transversality conditions. - -## Concatenation of arcs - -In this part, we present how to concatenate several flows. Let us consider the following problem. - -```@example main -t0 = 0 -tf = 1 -x0 = -1 -xf = 0 - -@def ocp begin - - t ∈ [ t0, tf ], time - x ∈ R, state - u ∈ R, control - - x(t0) == x0 - x(tf) == xf - -1 ≤ u(t) ≤ 1 - ẋ(t) == -x(t) + u(t) - - ∫( abs(u(t)) ) → min - -end -nothing # hide -``` - -From the Pontryagin maximum principle, the optimal control is a concatenation of an off arc ($u=0$) followed by a positive bang arc ($u=1$). The initial costate is - -```math -p_0 = \frac{1}{x_0 - (x_f-1) e^{t_f}} -``` - -and the switching time is $t_1 = -\ln(p_0)$. - -```@example main -p0 = 1/( x0 - (xf-1) * exp(tf) ) -t1 = -log(p0) -nothing # hide -``` - -Let us define the two flows and the concatenation. Note that the concatenation of two flows is a flow. - -```@example main -f0 = Flow(ocp, (x, p) -> 0) # off arc: u = 0 -f1 = Flow(ocp, (x, p) -> 1) # positive bang arc: u = 1 - -f = f0 * (t1, f1) # f0 followed by f1 whenever t ≥ t1 -nothing # hide -``` - -Now, we can check that the state reach the target. - -```@example main -sol = f((t0, tf), x0, p0) -plot(sol) -``` - -!!! note "Goddard problem" - - In the [Goddard problem](https://control-toolbox.org/Tutorials.jl/stable/tutorial-goddard.html#tutorial-goddard-plot), you may find more complex concatenations. - -For the moment, this concatenation is not equivalent to an exact concatenation. - -```@example main -f = Flow(x -> x) -g = Flow(x -> -x) - -x0 = 1 -φ(t) = (f * (t/2, g))(0, x0, t) -ψ(t) = g(t/2, f(0, x0, t/2), t) - -println("φ(t) = ", abs(φ(1)-x0)) -println("ψ(t) = ", abs(ψ(1)-x0)) - -t = range(1, 5e2, 201) - -plt = plot(yaxis=:log, legend=:bottomright, title="Comparison of concatenations", xlabel="t") -plot!(plt, t, t->abs(φ(t)-x0), label="OptimalControl") -plot!(plt, t, t->abs(ψ(t)-x0), label="Classical") -``` - -## State constraints - -We consider an optimal control problem with a state constraints of order 1.[^1] - -[^1]: B. Bonnard, L. Faubourg, G. Launay & E. Trélat, Optimal Control With State Constraints And The Space Shuttle Re-entry Problem, J. Dyn. Control Syst., 9 (2003), no. 2, 155–199. - -```@example main -t0 = 0 -tf = 2 -x0 = 1 -xf = 1/2 -lb = 0.1 - -ocp = @def begin - - t ∈ [t0, tf], time - x ∈ R, state - u ∈ R, control - - -1 ≤ u(t) ≤ 1 - x(t0) == x0 - x(tf) == xf - x(t) - lb ≥ 0 # state constraint - ẋ(t) == u(t) - - ∫( x(t)^2 ) → min - -end -nothing # hide -``` - -The pseudo-Hamiltonian of this problem is - -```math - H(x, p, u, \mu) = p\, u + p^0 x^2 + \mu\, c(x), -``` - -where $ p^0 = -1 $ since we are in the normal case, and where $c(x) = x - l_b$. Along a boundary arc, when $c(x(t)) = 0$, we have $x(t) = l_b$, so $ x(\cdot) $ is constant. Differentiating, we obtain $\dot{x}(t) = u(t) = 0$. Hence, along a boundary arc, the control in feedback form is: - -```math -u(x) = 0. -``` - -From the maximisation condition, along a boundary arc, we have $p(t) = 0$. Differentiating, we obtain $\dot{p}(t) = 2 x(t) - \mu(t) = 0$. Hence, along a boundary arc, the dual variable $\mu$ is given in feedback form by: - -```math -\mu(x) = 2x. -``` - -!!! note - - Within OptimalControl.jl, the constraint must be given in the form: - ```julia - c([t, ]x, u[, v]) - ``` - the control law in feedback form must be given as: - ```julia - u([t, ]x, p[, v]) - ``` - and the dual variable: - ```julia - μ([t, ]x, p[, v]) - ``` - The time `t` must be provided when the problem is [non-autonomous](@ref manual-model-time-dependence) and the variable `v` must be given when the optimal control problem contains a [variable](@ref manual-abstract-variable) to optimise. - -The optimal control is a concatenation of 3 arcs: a negative bang arc followed by a boundary arc, followed by a positive bang arc. The initial covector is approximately $p(0)=-0.982237546583301$, the first switching time is $t_1 = 0.9$, and the exit time of the boundary is $t_2 = 1.6$. Let us check this by concatenating the three flows. - -```@example main -u(x) = 0 # boundary control -c(x) = x-lb # constraint -μ(x) = 2x # dual variable - -f1 = Flow(ocp, (x, p) -> -1) -f2 = Flow(ocp, (x, p) -> u(x), (x, u) -> c(x), (x, p) -> μ(x)) -f3 = Flow(ocp, (x, p) -> +1) - -t1 = 0.9 -t2 = 1.6 -f = f1 * (t1, f2) * (t2, f3) - -p0 = -0.982237546583301 -xf, pf = f(t0, x0, p0, tf) -xf -``` - -## Jump on the costate - -Let consider the following problem: - -```@example main -t0=0 -tf=1 -x0=[0, 1] -l = 1/9 -@def ocp begin - t ∈ [ t0, tf ], time - x ∈ R², state - u ∈ R, control - x(t0) == x0 - x(tf) == [0, -1] - x₁(t) ≤ l, (x_con) - ẋ(t) == [x₂(t), u(t)] - 0.5∫(u(t)^2) → min -end -nothing # hide -``` - -The pseudo-Hamiltonian of this problem is - -```math - H(x, p, u, \mu) = p_1\, x_2 + p_2\, u + 0.5\, p^0 u^2 + \mu\, c(x), -``` - -where $ p^0 = -1 $ since we are in the normal case, and where the constraint is $c(x) = l - x_1 \ge 0$. Along a boundary arc, when $c(x(t)) = 0$, we have $x_1(t) = l$, so $\dot{x}_1(t) = x_2(t) = 0$. Differentiating again, we obtain $\dot{x}_2(t) = u(t) = 0$ (the constraint is of order 2). Hence, along a boundary arc, the control in feedback form is: - -```math -u(x, p) = 0. -``` - -From the maximisation condition, along a boundary arc, we have $p_2(t) = 0$. Differentiating, we obtain $\dot{p}_2(t) = -p_1(t) = 0$. Differentiating again, we obtain $\dot{p}_1(t) = \mu(t) = 0$. Hence, along a boundary arc, the Lagrange multiplier $\mu$ is given in feedback form by: - -```math -\mu(x, p) = 0. -``` - -Outside a boundary arc, the maximisation condition gives $u(x, p) = p_2$. A deeper analysis of the problem shows that the optimal solution has 3 arcs, the first and the third ones are interior to the constraint. The second arc is a boundary arc, that is $x_1(t) = l$ along the second arc. We denote by $t_1$ and $t_2$ the two switching times. We have $t_1 = 3l = 1/3$ and $t_2 = 1 - 3l = 2/3$, since $l=1/9$. The initial costate solution is $p(0) = [-18, -6]$. - -!!! danger "Important" - - The costate is discontinuous at $t_1$ and $t_2$ with a jump of $18$. - -Let us compute the solution concatenating the flows with the jumps. - -```@example main -t1 = 3l -t2 = 1 - 3l -p0 = [-18, -6] - -fs = Flow(ocp, - (x, p) -> p[2] # control along regular arc - ) -fc = Flow(ocp, - (x, p) -> 0, # control along boundary arc - (x, u) -> l-x[1], # state constraint - (x, p) -> 0 # Lagrange multiplier - ) - -ν = 18 # jump value of p1 at t1 and t2 - -f = fs * (t1, [ν, 0], fc) * (t2, [ν, 0], fs) - -xf, pf = f(t0, x0, p0, tf) # xf should be [0, -1] -``` - -Let us solve the problem with a direct method to compare with the solution from the flow. - -```@example main -using NLPModelsIpopt - -direct_sol = solve(ocp) -plot(direct_sol; label="direct", size=(800, 700)) - -flow_sol = f((t0, tf), x0, p0; saveat=range(t0, tf, 100)) -plot!(flow_sol; label="flow", state_style=(color=3,), linestyle=:dash) -``` diff --git a/docs/attic/manual-flow-others.md b/docs/attic/manual-flow-others.md deleted file mode 100644 index b8039c617..000000000 --- a/docs/attic/manual-flow-others.md +++ /dev/null @@ -1,108 +0,0 @@ -# [How to compute Hamiltonian flows and trajectories](@id manual-flow-others) - -In this tutorial, we explain the `Flow` function, in particular to compute flows from a Hamiltonian vector fields, but also from general vector fields. - -## Introduction - -Consider the simple optimal control problem from the [basic example page](@ref example-double-integrator-energy). The **pseudo-Hamiltonian** is - -```math - H(x, p, u) = p_q\, v + p_v\, u + p^0 u^2 /2, -``` - -where $x=(q,v)$, $p=(p_q,p_v)$, $p^0 = -1$ since we are in the normal case. From the Pontryagin maximum principle, the maximising control is given in feedback form by - -```math -u(x, p) = p_v -``` - -since $\partial^2_{uu} H = p^0 = - 1 < 0$. - -```@example main -u(x, p) = p[2] -nothing # hide -``` - -Actually, if $(x, u)$ is a solution of the optimal control problem, then, the Pontryagin maximum principle tells us that there exists a costate $p$ such that $u(t) = u(x(t), p(t))$ and such that the pair $(x, p)$ satisfies: - -```math -\begin{array}{l} - \dot{x}(t) = \displaystyle\phantom{-}\nabla_p H(x(t), p(t), u(x(t), p(t))), \\[0.5em] - \dot{p}(t) = \displaystyle - \nabla_x H(x(t), p(t), u(x(t), p(t))). -\end{array} -``` - -!!! note "Nota bene" - - Actually, writing $z = (x, p)$, then the pair $(x, p)$ is also solution of - - ```math - \dot{z}(t) = \vec{\mathbf{H}}(z(t)), - ``` - where $\mathbf{H}(z) = H(z, u(z))$ and $\vec{\mathbf{H}} = (\nabla_p \mathbf{H}, -\nabla_x \mathbf{H})$. - -Let us import the necessary packages. - -```@example main -using OptimalControl -using OrdinaryDiffEq -``` - -The package [OrdinaryDiffEq.jl](https://docs.sciml.ai/DiffEqDocs) provides numerical integrators to compute solutions of ordinary differential equations. - -!!! note "OrdinaryDiffEq.jl" - - The package OrdinaryDiffEq.jl is part of [DifferentialEquations.jl](https://docs.sciml.ai/DiffEqDocs). You can either use one or the other. - -## Extremals from the Hamiltonian - -The pairs $(x, p)$ solution of the Hamitonian vector field are called *extremals*. We can compute some constructing the flow from the optimal control problem and the control in feedback form. Another way to compute extremals is to define explicitly the Hamiltonian. - -```@example main -H(x, p, u) = p[1] * x[2] + p[2] * u - 0.5 * u^2 # pseudo-Hamiltonian -H(x, p) = H(x, p, u(x, p)) # Hamiltonian - -z = Flow(OptimalControl.Hamiltonian(H)) - -t0 = 0 -tf = 1 -x0 = [-1, 0] -p0 = [12, 6] -xf, pf = z(t0, x0, p0, tf) -``` - -## Extremals from the Hamiltonian vector field - -You can also provide the Hamiltonian vector field. - -```@example main -Hv(x, p) = [x[2], p[2]], [0.0, -p[1]] # Hamiltonian vector field - -z = Flow(OptimalControl.HamiltonianVectorField(Hv)) -xf, pf = z(t0, x0, p0, tf) -``` - -Note that if you call the flow on `tspan=(t0, tf)`, then you obtain the output solution from OrdinaryDiffEq.jl. - -```@example main -sol = z((t0, tf), x0, p0) -xf, pf = sol(tf)[1:2], sol(tf)[3:4] -``` - -## Trajectories - -You can also compute trajectories from the control dynamics $(x, u) \mapsto (v, u)$ and a control law $t \mapsto u(t)$. - -```@example main -u(t) = 6-12t -x = Flow((t, x) -> [x[2], u(t)]; autonomous=false) # the vector field depends on t -x(t0, x0, tf) -``` - -Again, giving a `tspan` you get an output solution from OrdinaryDiffEq.jl. - -```@example main -using Plots -sol = x((t0, tf), x0) -plot(sol) -``` diff --git a/docs/attic/manual-initial-guess.md b/docs/attic/manual-initial-guess.md deleted file mode 100644 index 3a1925b0b..000000000 --- a/docs/attic/manual-initial-guess.md +++ /dev/null @@ -1,627 +0,0 @@ -# [Initial guess (or iterate) for the resolution](@id manual-initial-guess) - -We present the different possibilities to provide an initial guess to solve an -optimal control problem with the [OptimalControl.jl](https://control-toolbox.org/OptimalControl.jl) package. - -First, we need to import OptimalControl.jl to define the -optimal control problem and [NLPModelsIpopt.jl](https://jso.dev/NLPModelsIpopt.jl) to solve it. -We also need to import [Plots.jl](https://docs.juliaplots.org) to plot solutions. - -```@example main -using OptimalControl -using NLPModelsIpopt -using Plots -``` - -For the illustrations, we define two optimal control problems to showcase the different ways to specify initial guesses. - -The first problem uses default component labels (`x₁`, `x₂` for the state): - -```@example main -t0 = 0; tf = 10; α = 5 - -ocp1 = @def begin - t ∈ [t0, tf], time - x ∈ R², state - u ∈ R, control - x(t0) == [ -1, 0 ] - x₁(tf) == 0 - ẋ(t) == [ x₂(t), x₁(t) + α*x₁(t)^2 + u(t) ] - x₂(tf)^2 + ∫( 0.5u(t)^2 ) → min -end -nothing # hide -``` - -The second problem uses custom component labels (`q`, `v` for the state, `tf` for the variable) and a different time variable name (`s` instead of `t`): - -```@example main -ocp2 = @def begin - tf ∈ R, variable - s ∈ [0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - -1 ≤ u(s) ≤ 1 - tf ≥ 0 - q(0) == -1 - v(0) == 0 - q(tf) == 0 - v(tf) == 0 - ẋ(s) == [v(s), u(s)] - tf → min -end -nothing # hide -``` - -!!! note "Component labels and time variable in `@init`" - - - The `@init` macro uses the **labels** declared in the `@def` block. For `ocp1`, you can use `x`, `x₁`, `x₂`, and `u`. For `ocp2`, you can use `x`, `q`, `v`, `u`, and `tf`. - - The `@init` macro uses the **time variable name** from the `@def` block. For `ocp1`, use `t` (e.g., `x(t) := ...`). For `ocp2`, use `s` (e.g., `q(s) := ...`). - - When components are **not explicitly named** in `@def` (as in `ocp1` with `x ∈ R²`), they receive **default labels with subscripted indices**: `x₁`, `x₂`, etc. These default names are usable in `@init` just like custom labels. - - This allows for more readable initial guess specifications that match your problem definition. - -## Default initial guess - -We first solve the problem without giving an initial guess. -This will default to initialize all variables to 0.1. - -To visualize the default initial guess before solving, we can run the solver with `max_iter=0`: - -```@example main -# visualize the default initial guess (no iterations) -sol_init = solve(ocp1; init=nothing, max_iter=0, display=false) -plot(sol_init; size=(600, 450)) -``` - -!!! tip "Visualizing any initial guess" - - This technique works with any initial guess specification. By setting `max_iter=0`, the solver stops immediately after initialization, allowing you to visualize the initial guess before the optimization process begins. - -Now let us solve the problem completely: - -```@example main -# solve the optimal control problem without initial guess -sol = solve(ocp1; display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -Let us plot the solution of the optimal control problem. - -```@example main -plot(sol; size=(600, 450)) -``` - -Note that the following formulations are equivalent to not giving an initial guess. - -```@example main -sol = solve(ocp1; init=nothing, display=false) -println("Number of iterations: ", iterations(sol)) - -sol = solve(ocp1; init=(), display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -!!! tip "Interactions with an optimal control solution" - - To get the number of iterations of the solver, check the [`iterations`](@ref) function. - -To reduce the number of iterations and improve the convergence, we can give an initial guess to the solver. -The recommended way is to use the `@init` macro, which provides a clean syntax for specifying initial values. - -## Initial guess with `@init` - -The `@init` macro allows you to specify initial values for state and control using the syntax `label(t) := expression`. -For optimization variables (like `tf`), use `label := value` since they are not functions of time. - -### `@init` at a Glance - -**Complete syntax:** - -```julia -ig = @init ocp begin - # initial guess specifications -end -``` - -The `ocp` argument is required for label validation and context checking. - -**Core syntax:** `label(time_var) := expression` - -| Component | Has `(t)`? | Uses `:=`? | Example | -| ----------- | ----------- | ----------- | --------- | -| State/Control | ✅ Yes | ✅ Yes | `u(t) := 2` | -| Variable | ❌ No | ✅ Yes | `tf := 2.0` | -| Alias | ❌ No | ❌ No (use `=`) | `a = 0.5` | - -**Dimensions:** - -- **1D** → scalar: `u(t) := 2` -- **2D** → vector: `u(t) := [1, 2]` - -**Initialization types:** - -| Type | 1D Example | 2D Example | -| ------ | ------------ | ------------ | -| **Constant** | `u(t) := 2` or `u := 2` | `x(t) := [1, 2]` or `x := [1, 2]` | -| **Function** | `u(t) := sin(t)` | `x(t) := [sin(t), cos(t)]` | -| **Grid** | `u(T) := [0, 1, 2]` | `x(T) := [[0,0], [1,1], [2,2]]` | -| **Grid (matrix)** | — | `x(T) := [0 0; 1 1; 2 2]` | - -where `T = [0.0, 0.5, 1.0]` is the time grid. - -!!! tip "Key rules" - - Use the **same time variable** as in your `@def` block (`t`, `s`, etc.) - - **1D**: scalar values (not `[value]`) - - **2D**: vectors `[v1, v2]` or vector of vectors `[[v1, v2], ...]` or matrix - - **Variables** and **aliases**: no time argument - - **Constant functions**: use either `u(t) := 2` or the simplified `u := 2` - -### Syntax rules - -The left-hand side of `:=` and `=` follow strict rules. - -**Left-hand side of `:=`** : only **labels declared in the optimal control problem**: - -- the **time variable name** declared in `@def` (`t`, `s`, ...), used as the argument of state/control functions -- the **state, control or variable** label, either its global name (`x`, `u`, `tf`) or the name of one of its **components** (`q`, `v`, `x₁`, `x₂`, ...) - -**Left-hand side of `=`** : arbitrary **alias names**, local to the `@init` block. Aliases are not labels of the problem; they are just convenient names to factor out constants or subexpressions. - -**Right-hand side** : any Julia expression, which may reference the time variable, previously defined aliases, and other labels defined earlier in the block (see [Cross-spec substitution](@ref cross-spec-substitution)). - -#### Default component names - -When components are not explicitly named in `@def`, they receive default labels with subscripted indices. For example, `ocp1` declares `x ∈ R²` without naming the components, so the default labels `x₁` and `x₂` are used: - -```@example main -# use default component names x₁, x₂ -ig = @init ocp1 begin - x₁(t) := -1.0 + t/10 - x₂(t) := 0.0 - u(t) := -0.2 -end - -sol = solve(ocp1; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -#### No indexed syntax - -The indexed syntax `x[i](t)` or `x[i:j](t)` is **not supported** on the left-hand side of `:=`. `@init` works at the level of labels, not array indices. - -- ❌ `x[1](t) := ...`, `x[1:2](t) := ...` -- ✅ `x(t) := ...` (global) or `x₁(t) := ...`, `x₂(t) := ...`, `q(t) := ...`, `v(t) := ...` (per component) - -### Constant initial guess - -To initialize with constant values, use constant expressions in the function syntax: - -!!! note "Alternative syntax for constant functions" - For constant functions, you can also use the simplified syntax without the time argument: - - `u(t) := 2` is equivalent to `u := 2` - - `x(t) := [1, 2]` is equivalent to `x := [1, 2]` - - This shorter syntax is only available for constant expressions. - -```@example main -# initialize with constant functions -ig = @init ocp1 begin - x(t) := [-0.2, 0.1] # constant vector function - u(t) := -0.2 # constant scalar function -end - -sol = solve(ocp1; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -Using custom labels makes the initialization more readable: - -```@example main -# initialize individual components with constant values -# note: use 's' as the time variable (matching ocp2 definition) -ig = @init ocp2 begin - q(s) := -0.2 # constant function for q - v(s) := 0.0 # constant function for v - u(s) := 0.1 # constant function for u - tf := 2.0 # variable (not a function) -end - -sol = solve(ocp2; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -### Partial initialization - -You can initialize only some components; missing components will default to 0.1: - -```@example main -# initialize only the control -ig = @init ocp1 begin - u(t) := -0.2 -end - -sol = solve(ocp1; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -```@example main -# initialize only state components and variable -ig = @init ocp2 begin - q(s) := -0.5 - v(s) := 0.2 - tf := 2.0 -end - -sol = solve(ocp2; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -### Time-dependent functions - -For non-constant functions, use any expression involving `t`: - -```@example main -# initialize with time-dependent functions -ig = @init ocp1 begin - x(t) := [-0.2t, 0.1t] # time-dependent vector - u(t) := -0.2t # time-dependent scalar -end - -sol = solve(ocp1; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -```@example main -# initialize individual components with time-dependent functions -ig = @init ocp2 begin - q(s) := sin(s) # time-dependent - v(s) := cos(s) # time-dependent - u(s) := s # time-dependent - tf := 2.0 # variable (constant) -end - -sol = solve(ocp2; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -### Using aliases - -You can define Julia aliases within the `@init` block using `=` (single equals, without time argument): - -```@example main -# use aliases for constants and expressions -ig = @init ocp2 begin - amplitude = 0.5 - phase = sin(amplitude) - φ = 2π * s - q(s) := amplitude * sin(φ) - v(s) := amplitude * cos(φ) - u(s) := phase # constant function using alias - tf := 2.0 # variable -end - -sol = solve(ocp2; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -### [Cross-spec substitution](@id cross-spec-substitution) - -Specifications inside a single `@init` block can **reference each other**, from top to bottom. A label defined on an earlier line can be reused in the right-hand side of a later specification. - -Rules: - -- A reference only resolves to a label (or alias) defined **earlier** in the block. -- Substitution happens by name: the referenced label is replaced by its definition when the later expression is evaluated. -- References across different grid arguments are **not substituted** (see note at the end of this section). - -#### Temporal → temporal - -A time-dependent spec can reference another time-dependent spec: - -```@example main -# v depends on q -ig = @init ocp2 begin - q(s) := sin(s) - v(s) := 1.0 + q(s) - u(s) := 0.0 - tf := 2.0 -end - -sol = solve(ocp2; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -#### Transitive chain - -Substitutions chain transitively: `u` below references `v`, which itself references `q`. - -```@example main -# q → v → u -ig = @init ocp2 begin - q(s) := sin(s) - v(s) := 1.0 + q(s) - u(s) := s + v(s)^2 - tf := 2.0 -end - -sol = solve(ocp2; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -#### Constant → temporal - -A temporal spec can reference a constant-valued component defined earlier: - -```@example main -# v(s) uses the constant value of q -ig = @init ocp2 begin - q := -1.0 - v(s) := q + sin(s) - u(s) := 0.0 - tf := 2.0 -end - -sol = solve(ocp2; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -#### Constant → constant - -A constant spec can reference another constant, including for variable components. Here we define a small OCP whose variable has two components `(tf, a)`: - -```@example main -ocp_var2 = @def begin - w = (tf, a) ∈ R², variable - t ∈ [0, 1], time - x ∈ R, state - u ∈ R, control - x(0) == 0 - x(1) - a == 0 - ẋ(t) == u(t) - ∫(0.5u(t)^2) → min -end - -ig = @init ocp_var2 begin - tf := 1.0 - a := tf + 0.5 -end - -w = variable(ig) -println("tf = ", w[1], ", a = ", w[2]) -nothing # hide -``` - -#### Mixing aliases and cross-spec references - -Aliases (with `=`) and cross-spec references (with `:=`) can be freely combined: - -```@example main -ig = @init ocp2 begin - A = 2.0 # alias - q(s) := A * sin(s) # uses alias - v(s) := q(s) + 1.0 # references q - u(s) := 0.0 - tf := 2.0 -end - -sol = solve(ocp2; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -!!! note "No substitution across grid specs" - - When a spec uses a **grid argument** (e.g. `q(T) := Dq` with `T` a time vector), it is not substituted into other temporal specs written with the time variable (`v(s) := ...`). The two live in different evaluation contexts. Use either temporal functions throughout, or grids throughout, when you need to chain references. - -## Vector initial guess (interpolated) - -You can provide initial values on a time grid using the syntax `label(T) := data`, where `T` is a time vector and `data` contains the corresponding values. - -### Full block initialization on a grid - -```@example main -# define time grid and data -T = [0.0, 5.0, 10.0] -X = [[-1.0, 0.0], [-0.5, 0.5], [0.0, 0.0]] -U = [0.0, -0.5, 0.0] - -ig = @init ocp1 begin - x(T) := X - u(T) := U -end - -sol = solve(ocp1; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -### Per-component grids - -Different components can use different time grids: - -```@example main -# different grids for different components -Sq = [0.0, 1.0, 2.0] -Dq = [-1.0, -0.5, 0.0] -Sv = [0.0, 2.0] -Dv = [0.0, 0.0] -Su = [0.0, 1.0, 2.0] -Du = [0.0, 0.5, 0.0] - -ig = @init ocp2 begin - q(Sq) := Dq - v(Sv) := Dv - u(Su) := Du - tf := 2.0 -end - -sol = solve(ocp2; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -### Matrix format - -For state initialization, you can also use a matrix where each row corresponds to a time point: - -```@example main -# matrix format for state data -T = [0.0, 5.0, 10.0] -Xmat = [ - -1.0 0.0; - -0.5 0.5; - 0.0 0.0 -] -U = [0.0, -0.5, 0.0] - -ig = @init ocp1 begin - x(T) := Xmat - u(T) := U -end - -sol = solve(ocp1; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -## Mixed initial guess - -You can freely mix constant functions, time-dependent functions, and grid-based initializations in a single `@init` block: - -```@example main -# mix different initialization types -T = [0.0, 5.0, 10.0] -X = [[-1.0, 0.0], [-0.5, 0.5], [0.0, 0.0]] - -ig = @init ocp1 begin - x(T) := X # grid-based for state - u(t) := -0.2 * sin(t) # time-dependent function for control -end - -sol = solve(ocp1; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -```@example main -# another mix: constant and time-dependent functions -ig = @init ocp2 begin - q(s) := -1.0 + s/2.0 # time-dependent function - v(s) := 0.0 # constant function - u(s) := 0.1 * s # time-dependent function - tf := 2.0 # variable -end - -sol = solve(ocp2; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -## Solution as initial guess (warm start) - -You can use an existing solution directly as an initial guess. -The dimensions of the state, control and optimization variable must coincide. -This particular feature allows an easy implementation of discrete continuations. - -```@example main -# generate an initial solution -sol_init = solve(ocp1; display=false) - -# solve the problem using solution as initial guess -sol = solve(ocp1; init=sol_init, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -You can also manually extract data from a solution and use it within an `@init` block: - -```@example main -# extract functions from solution -x_fun = state(sol_init) -u_fun = control(sol_init) - -# use them in @init -ig = @init ocp1 begin - x(t) := x_fun(t) - u(t) := u_fun(t) -end - -sol = solve(ocp1; init=ig, display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -!!! tip "Interactions with an optimal control solution" - - Please check [`state`](@ref), [`costate`](@ref), [`control`](@ref) and [`variable`](@ref CTModels.OCP.variable) to get data from the solution. The functions `state`, `costate` and `control` return functions of time and `variable` returns a vector. - -## Costate / multipliers - -For the moment there is no option to provide an initial guess for the costate / multipliers. - -## Legacy: NamedTuple construction - -While the `@init` macro is the recommended approach, you can still construct initial guesses using direct `NamedTuple` syntax for backward compatibility. - -### Basic tuple syntax - -```@example main -# direct tuple construction with constants -sol = solve(ocp1; init=(state=[-0.2, 0.1], control=-0.2), display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -### Using component labels - -The tuple syntax now supports using component labels as keys: - -```@example main -# use component labels in the tuple -sol = solve(ocp2; init=(q=-1.0, v=0.0, u=0.1, tf=2.0), display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -### Grid-based initialization with tuples - -For grid-based initialization, the syntax uses `(time_vector, data)` pairs: - -```@example main -# grid-based with tuple syntax -T = [0.0, 5.0, 10.0] -X = [[-1.0, 0.0], [-0.5, 0.5], [0.0, 0.0]] -U = [0.0, -0.5, 0.0] - -sol = solve(ocp1; init=(state=(T, X), control=(T, U)), display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -You can also mix component labels with grid syntax: - -```@example main -# per-component grids with tuple syntax -Tq = [0.0, 1.0, 2.0] -Dq = [-1.0, -0.5, 0.0] - -sol = solve(ocp2; init=(q=(Tq, Dq), v=0.0, u=0.1, tf=2.0), display=false) -println("Number of iterations: ", iterations(sol)) -nothing # hide -``` - -!!! note "Recommendation" - - While the direct tuple syntax is still supported, we recommend using the `@init` macro for better readability and maintainability, especially for complex initial guess specifications. diff --git a/docs/attic/manual-macro-free.md b/docs/attic/manual-macro-free.md deleted file mode 100644 index 9d3a5b59b..000000000 --- a/docs/attic/manual-macro-free.md +++ /dev/null @@ -1,898 +0,0 @@ -# [Functional API (macro-free)](@id manual-macro-free) - -The [`@def`](@ref manual-abstract-syntax) macro provides a concise DSL to define optimal control problems. An alternative is the **functional API**, which builds the same problem step by step using plain Julia functions. This approach is useful when: - -- generating problems **programmatically** from parameters, data, or loops, -- building **library code** that must not rely on macros, -- interfacing with external tools that process problem structures directly. - -The functional API uses [`OptimalControl.PreModel`](@ref CTModels.PreModel) as a mutable builder, populated by setter calls, then frozen into an immutable [`OptimalControl.Model`](@ref) by [`build`](@ref). - -!!! note - - When a problem is defined with the functional API, [`definition`](@ref)`(ocp)` returns an `EmptyDefinition` — no abstract expression is stored. This contrasts with `@def`, which records the full DSL expression for display and introspection. - -!!! warning "Modeler compatibility" - - Problems built with the functional API can only be solved with the `:adnlp` modeler (the default). The `:exa` modeler (ExaModels, GPU-capable) requires the abstract syntax [`@def`](@ref manual-abstract-syntax). See the [solve manual](@ref manual-solve) for modeler details. - ---- - -**Content** - -```@contents -Pages = ["manual-macro-free.md"] -Depth = 3 -``` - ---- - -## Canvas - -The functional API mirrors the [Mathematical formulation](@ref math-formulation). The correspondence is: - -| Math | Functional API | -| :--- | :--- | -| Dynamics $f(t, x, u)$ | `dyn!` passed to [`dynamics!`](@ref) | -| Lagrange integrand $f^0(t, x, u)$ | `lag` passed to [`objective!`](@ref) | -| Mayer terminal cost $g(x_0, x_f)$ | `may` passed to [`objective!`](@ref) | -| Path constraint $c(t, x, u)$ | `p!` passed to [`constraint!`](@ref)`(pre, :path; ...)` | -| Boundary constraint $b(x_0, x_f)$ | `b!` passed to [`constraint!`](@ref)`(pre, :boundary; ...)` | -| Extra variable $v$ | [`variable!`](@ref) (extra argument to all the callbacks above) | - -```julia -using OptimalControl - -pre = OptimalControl.PreModel() - -# ─── Optional: must come before time! when using indf/ind0 ─────────────────── -variable!(pre, q) # q = variable dimension -# ───────────────────────────────────────────────────────────────────────────── - -time!(pre; t0=..., tf=...) # fixed times -# or: time!(pre; t0=..., indf=i) # free final time at variable index i - -state!(pre, n) # n = state dimension - -# ─── Optional: omit entirely for control-free problems ─────────────────────── -control!(pre, m) # m = control dimension -# ───────────────────────────────────────────────────────────────────────────── - -# Dynamics — in-place, signature: dyn!(dx, t, x, u, v) -# dx : output vector (modified in place), length n -# t : current time (scalar) -# x : state (vector of length n; scalar state ↦ x[1]) -# u : control (vector of length m; scalar control ↦ u[1]; unused if control-free) -# v : variable (vector of length q; scalar variable ↦ v[1]; unused if no variable) -function dyn!(dx, t, x, u, v) - dx[1] = ... - dx[2] = ... -end -dynamics!(pre, dyn!) - -# Lagrange integrand — out-of-place, signature: lag(t, x, u, v) → scalar -lag(t, x, u, v) = ... -# Mayer terminal cost — out-of-place, signature: may(x0, xf, v) → scalar -# x0 : initial state (vector of length n; scalar state ↦ x0[1]) -# xf : final state (vector of length n; scalar state ↦ xf[1]) -may(x0, xf, v) = ... - -objective!(pre, :min; lagrange=lag) # Lagrange cost -# or: objective!(pre, :min; mayer=may) # Mayer cost -# or: objective!(pre, :min; mayer=may, lagrange=lag) # Bolza cost - -# ─── Optional: one call per constraint ─────────────────────────────────────── -# Two families of constraints: -# -# (a) Box constraints on components — :state, :control, :variable -# rg selects the component range i:j, with lb ≤ x[rg] ≤ ub (resp. u, v). -constraint!(pre, :state; rg=i:j, lb=..., ub=..., label=:name) -constraint!(pre, :control; rg=i:j, lb=..., ub=..., label=:name) -constraint!(pre, :variable; rg=i:j, lb=..., ub=..., label=:name) -# -# (b) Non-linear constraints defined by a function — :boundary, :path -# The constraint reads: lb ≤ f(...) ≤ ub (use lb=ub for equality). -# -# Boundary — in-place, signature: b!(val, x0, xf, v) (same shape as Mayer) -# val : output vector (modified in place), length = length(lb) = length(ub) -# x0 : initial state (vector of length n; scalar state ↦ x0[1]) -# xf : final state (vector of length n; scalar state ↦ xf[1]) -# v : variable (vector of length q) -function b!(val, x0, xf, v) - val[1] = ... -end -constraint!(pre, :boundary; f=b!, lb=..., ub=..., label=:name) -# -# Path — in-place, signature: p!(val, t, x, u, v) (same shape as dynamics) -# val : output vector (modified in place), length = length(lb) = length(ub) -# t : current time (scalar) -# x : state (vector of length n) -# u : control (vector of length m) -# v : variable (vector of length q) -function p!(val, t, x, u, v) - val[1] = ... -end -constraint!(pre, :path; f=p!, lb=..., ub=..., label=:name) -# ───────────────────────────────────────────────────────────────────────────── - -# autonomous=true ⟺ time t does NOT appear explicitly in the dynamics, -# the Lagrange integrand, nor in any :path constraint. -# autonomous=false ⟺ at least one of them depends explicitly on t. -time_dependence!(pre; autonomous=true) - -ocp = build(pre) -``` - -**Required:** `time!` · `state!` · `dynamics!` · `objective!` · `time_dependence!` · `build` - -**Optional:** `variable!` · `control!` · `constraint!` (repeatable) - -**Ordering constraints:** - -- `variable!` → before `time!` when using free-time indices (`indf`, `ind0`) -- `variable!` → before `dynamics!` and `objective!` -- `dynamics!` and `objective!` → after `time!` and `state!` - -## Examples - -For each problem below, the [`@def`](@ref) abstract syntax is shown on the left and the equivalent functional API on the right. After `build`, both formulations produce an equivalent model and can be passed directly to [`solve`](@ref manual-solve). - -### [Double integrator: energy minimisation](@id manual-macro-free-energy) - -The simplest case: fixed time interval, boundary constraints, autonomous dynamics, Lagrange cost. -See the [full example](@ref example-double-integrator-energy) for solving and plotting. - -```@example ex-energy -using OptimalControl -using NLPModelsIpopt -t0 = 0.0; tf = 1.0; x0 = [-1.0, 0.0]; xf = [0.0, 0.0] -nothing # hide -``` - -```@raw html -
-
-``` - -**Abstract syntax** - -```@example ex-energy -ocp_macro = @def begin - -t ∈ [t0, tf], time -x = (q, v) ∈ R², state -u ∈ R, control - -x(t0) == x0 -x(tf) == xf - -ẋ(t) == [v(t), u(t)] - -0.5∫( u(t)^2 ) → min - -end -nothing # hide -``` - -```@raw html -
-
-``` - -**Functional API** - -```@example ex-energy -pre = OptimalControl.PreModel() - -time!(pre; t0=t0, tf=tf) -# state "x" with components "q" (position) and "v" (velocity) -state!(pre, 2, "x", ["q", "v"]) -control!(pre, 1) - -function f_energy!(dx, t, x, u, v) - dx[1] = x[2] - dx[2] = u[1] - return nothing -end -dynamics!(pre, f_energy!) - -function boundary_energy!(b, x0_, xf_, v) - b[1] = x0_[1] - x0[1] - b[2] = x0_[2] - x0[2] - b[3] = xf_[1] - xf[1] - b[4] = xf_[2] - xf[2] - return nothing -end -constraint!(pre, - :boundary; - f=boundary_energy!, - lb=zeros(4), ub=zeros(4), - label=:endpoint -) - -lagrange_energy(t, x, u, v) = 0.5 * u[1]^2 -objective!(pre, :min; lagrange=lagrange_energy) - -time_dependence!(pre; autonomous=true) - -ocp_func = build(pre) -nothing # hide -``` - -```@raw html -
-
-``` - -Both formulations produce identical solutions. We solve both and plot them together for verification: - -```@example ex-energy -sol_macro = solve(ocp_macro; display=false) -sol_func = solve(ocp_func; display=false) - -println("Macro: objective = ", objective(sol_macro), ", iterations = ", iterations(sol_macro)) -println("Functional API: objective = ", objective(sol_func), ", iterations = ", iterations(sol_func)) -``` - -```@example ex-energy -plt = plot(sol_macro; label="Macro", color=1, size=(800, 600)) -plot!(plt, sol_func; label="Functional API", color=2, linestyle=:dash) -``` - -The two models are functionally equivalent. The key difference is visible via [`definition`](@ref): the macro records the full DSL expression, whereas the functional API stores an empty definition. - -```@example ex-energy -definition(ocp_macro) -``` - -```@example ex-energy -has_abstract_definition(ocp_func) -``` - -#### Scalar vs vector: a subtlety of the functional API - -In the functional API definition above, the control is declared with `control!(pre, 1)` — it is of **dimension 1**. Yet, inside the callbacks `f_energy!` and `lagrange_energy`, we accessed it as `u[1]`, *not* as `u`. The same applies to the state and the variable: inside callbacks, dimension-1 components must always be indexed. - -This is because the functional API callbacks always receive `x`, `u`, and `v` as **vectors**, regardless of their dimension. This keeps the callback signatures uniform and lets the same code shape work for any dimension. - -However, once the problem is solved, accessing the control (or state, or variable) on the solution returns a **scalar** when the component is of dimension 1 — just like the [`@def`](@ref manual-abstract-syntax) macro convention: - -```@example ex-energy -u_macro = control(sol_macro) -u_func = control(sol_func) -# The callbacks used u[1], yet the solution returns a scalar: -u_macro(t0), u_func(t0) -``` - -```@example ex-energy -typeof(u_macro(t0)), typeof(u_func(t0)) -``` - -!!! warning "Scalar vs vector conventions" - - The functional API uses two different conventions depending on where you are: - - - **Inside callbacks** (`dynamics!`, `objective!`, `constraint!`): `x`, `u`, `v` are **always vectors**. For a dimension-1 component, use `x[1]`, `u[1]`, `v[1]`. - - **On a solution**: `state(sol)(t)`, `control(sol)(t)`, `variable(sol)` return a **scalar** when the corresponding component is of dimension 1. This matches the [`@def`](@ref manual-abstract-syntax) convention (see the [solution manual](@ref manual-solution)). - - This asymmetry is intentional: callbacks are written once for any dimension, while solutions expose the mathematical object (scalar or vector) directly. - -### [Double integrator: time minimisation](@id manual-macro-free-time) - -Free final time as a variable, Mayer cost, control box constraint. -See the [full example](@ref example-double-integrator-time) for solving and plotting. - -```@example ex-time -using OptimalControl -using NLPModelsIpopt -t0 = 0.0; x0 = [-1.0, 0.0]; xf = [0.0, 0.0] -nothing # hide -``` - -```@raw html -
-
-``` - -**Abstract syntax** - -```@example ex-time -ocp_macro = @def begin - -tf ∈ R, variable -t ∈ [t0, tf], time -x = (q, v) ∈ R², state -u ∈ R, control - --1 ≤ u(t) ≤ 1 - -x(t0) == x0 -x(tf) == xf - -ẋ(t) == [v(t), u(t)] - -tf → min - -end -nothing # hide -``` - -```@raw html -
-
-``` - -**Functional API** - -```@example ex-time -pre = OptimalControl.PreModel() - -# variable[1] = final time tf -variable!(pre, 1, "tf") -# free final time: tf = variable[1] -time!(pre; t0=t0, indf=1) -# state "x" with components "q" (position) and "v" (velocity) -state!(pre, 2, "x", ["q", "v"]) -control!(pre, 1) - -function f_time!(dx, t, x, u, v) - dx[1] = x[2] - dx[2] = u[1] - return nothing -end -dynamics!(pre, f_time!) - -# control box constraint: -1 ≤ u ≤ 1 -constraint!(pre, - :control; - rg=1:1, lb=[-1.0], ub=[1.0], - label=:u_bounds -) - -function boundary_time!(b, x0_, xf_, v) - b[1] = x0_[1] - x0[1] - b[2] = x0_[2] - x0[2] - b[3] = xf_[1] - xf[1] - b[4] = xf_[2] - xf[2] - return nothing -end -constraint!(pre, - :boundary; - f=boundary_time!, - lb=zeros(4), ub=zeros(4), - label=:endpoint -) - -# Mayer cost: minimise tf = variable[1] -mayer_time(x0_, xf_, v) = v[1] -objective!(pre, :min; mayer=mayer_time) - -time_dependence!(pre; autonomous=true) - -ocp_func = build(pre) -nothing # hide -``` - -```@raw html -
-
-``` - -Both formulations produce identical solutions: - -```@example ex-time -sol_macro = solve(ocp_macro; display=false) -sol_func = solve(ocp_func; display=false) - -println("Macro: objective = ", objective(sol_macro), ", iterations = ", iterations(sol_macro)) -println("Functional API: objective = ", objective(sol_func), ", iterations = ", iterations(sol_func)) -``` - -```@example ex-time -plt = plot(sol_macro; label="Macro", color=1, size=(800, 600)) -plot!(plt, sol_func; label="Functional API", color=2, linestyle=:dash) -``` - -!!! note - `variable!(pre, 1, "tf")` must be called **before** `time!(pre; indf=1)` so that the free-time index refers to a declared variable. - -### [Control-free problems](@id manual-macro-free-control-free) - -No control variable: `control!` is simply omitted. The dynamics and objective still receive `u` as an argument, but it is a zero-dimensional vector. -See the [full example](@ref example-control-free) for solving and plotting. - -```@example ex-cf -using OptimalControl -using NLPModelsIpopt -λ_true = 0.5 -model_fn(t) = 2 * exp(λ_true * t) -noise_fn(t) = 2e-1 * sin(4π * t) -data_fn(t) = model_fn(t) + noise_fn(t) -t0 = 0.0; tf = 2.0; x0_cf = 2.0 -nothing # hide -``` - -```@raw html -
-
-``` - -**Abstract syntax** - -```@example ex-cf -ocp_macro = @def begin - -λ ∈ R, variable -t ∈ [t0, tf], time -x ∈ R, state - -x(t0) == x0_cf - -ẋ(t) == λ * x(t) - -∫( (x(t) - data_fn(t))^2 ) → min - -end -nothing # hide -``` - -```@raw html -
-
-``` - -**Functional API** - -```@example ex-cf -pre = OptimalControl.PreModel() - -# variable[1] = parameter λ (growth rate) -variable!(pre, 1, "λ") -time!(pre; t0=t0, tf=tf) -# scalar state x -state!(pre, 1, "x") -# no control! — control-free problem - -function f_cf!(dx, t, x, u, v) - # λ = v[1]; u is empty (control-free) - dx[1] = v[1] * x[1] - return nothing -end -dynamics!(pre, f_cf!) - -function boundary_cf!(b, x0_, xf_, v) - b[1] = x0_[1] - x0_cf - return nothing -end -constraint!(pre, - :boundary; - f=boundary_cf!, - lb=[0.0], ub=[0.0], - label=:ic -) - -lagrange_cf(t, x, u, v) = (x[1] - data_fn(t))^2 -objective!(pre, :min; lagrange=lagrange_cf) - -# autonomous=false: data_fn(t) depends on t -time_dependence!(pre; autonomous=false) - -ocp_func = build(pre) -nothing # hide -``` - -```@raw html -
-
-``` - -Both formulations produce identical solutions: - -```@example ex-cf -sol_macro = solve(ocp_macro; display=false) -sol_func = solve(ocp_func; display=false) - -println("Macro: objective = ", objective(sol_macro), ", iterations = ", iterations(sol_macro)) -println("Functional API: objective = ", objective(sol_func), ", iterations = ", iterations(sol_func)) -``` - -```@example ex-cf -plt = plot(sol_macro; label="Macro", color=1, size=(800, 200)) -plot!(plt, sol_func; label="Functional API", color=2, linestyle=:dash) -``` - -!!! note - `time_dependence!(pre; autonomous=false)` is required here because the Lagrange integrand `data_fn(t)` depends explicitly on time `t`. - -### [Problems mixing control and variable](@id manual-macro-free-control-and-variable) - -A variable parameter and an explicit control are used simultaneously. -See the [full example](@ref example-control-and-variable) for solving and plotting. - -```@example ex-cv -using OptimalControl -using NLPModelsIpopt -λ_true = 0.5 -model_fn2(t) = 2 * exp(λ_true * t) -noise_fn2(t) = 2e-1 * sin(4π * t) -data_fn2(t) = model_fn2(t) + noise_fn2(t) -t0 = 0.0; tf = 2.0; x0_cv = 2.0 -nothing # hide -``` - -```@raw html -
-
-``` - -**Abstract syntax** - -```@example ex-cv -ocp_macro = @def begin - -λ ∈ R, variable -t ∈ [t0, tf], time -x ∈ R, state -u ∈ R, control - -x(t0) == x0_cv - -ẋ(t) == λ * x(t) + u(t) - -∫( (x(t) - data_fn2(t))^2 + 0.5*u(t)^2 ) → min - -end -nothing # hide -``` - -```@raw html -
-
-``` - -**Functional API** - -```@example ex-cv -pre = OptimalControl.PreModel() - -# variable[1] = parameter λ (growth rate) -variable!(pre, 1, "λ") -time!(pre; t0=t0, tf=tf) -# scalar state x -state!(pre, 1, "x") -# scalar control u -control!(pre, 1) - -function f_cv!(dx, t, x, u, v) - # λ = v[1] - dx[1] = v[1] * x[1] + u[1] - return nothing -end -dynamics!(pre, f_cv!) - -function boundary_cv!(b, x0_, xf_, v) - b[1] = x0_[1] - x0_cv - return nothing -end -constraint!(pre, - :boundary; - f=boundary_cv!, - lb=[0.0], ub=[0.0], - label=:ic -) - -lagrange_cv(t, x, u, v) = - (x[1] - data_fn2(t))^2 + 0.5 * u[1]^2 -objective!(pre, :min; lagrange=lagrange_cv) - -# autonomous=false: data_fn2(t) depends on t -time_dependence!(pre; autonomous=false) - -ocp_func = build(pre) -nothing # hide -``` - -```@raw html -
-
-``` - -Both formulations produce identical solutions: - -```@example ex-cv -sol_macro = solve(ocp_macro; display=false) -sol_func = solve(ocp_func; display=false) - -println("Macro: objective = ", objective(sol_macro), ", iterations = ", iterations(sol_macro)) -println("Functional API: objective = ", objective(sol_func), ", iterations = ", iterations(sol_func)) -``` - -```@example ex-cv -plt = plot(sol_macro; label="Macro", color=1, size=(800, 400)) -plot!(plt, sol_func; label="Functional API", color=2, linestyle=:dash) -``` - -### [Singular control](@id manual-macro-free-singular) - -Three-dimensional state, free final time, state and control box constraints, Mayer cost. -See the [full example](@ref example-singular-control) for solving and plotting. - -```@example ex-singular -using OptimalControl -using NLPModelsIpopt -nothing # hide -``` - -```@raw html -
-
-``` - -**Abstract syntax** - -```@example ex-singular -ocp_macro = @def begin - -tf ∈ R, variable -t ∈ [0, tf], time -q = (x, y, θ) ∈ R³, state -u ∈ R, control - --1 ≤ u(t) ≤ 1 --π/2 ≤ θ(t) ≤ π/2 - -x(0) == 0 -y(0) == 0 -x(tf) == 1 -y(tf) == 0 - -∂(q)(t) == [cos(θ(t)), sin(θ(t)) + x(t), u(t)] - -tf → min - -end -nothing # hide -``` - -```@raw html -
-
-``` - -**Functional API** - -```@example ex-singular -pre = OptimalControl.PreModel() - -# variable[1] = final time tf -variable!(pre, 1, "tf") -# free final time: tf = variable[1] -time!(pre; t0=0.0, indf=1) -# state "q" with components "x", "y", "θ" -state!(pre, 3, "q", ["x", "y", "θ"]) -control!(pre, 1) - -function f_singular!(dq, t, q, u, v) - dq[1] = cos(q[3]) - dq[2] = sin(q[3]) + q[1] - dq[3] = u[1] - return nothing -end -dynamics!(pre, f_singular!) - -# control box constraint: -1 ≤ u ≤ 1 -constraint!(pre, - :control; - rg=1:1, lb=[-1.0], ub=[1.0], - label=:u_bounds -) -# state box constraint on θ = q[3]: -π/2 ≤ θ ≤ π/2 -constraint!(pre, - :state; - rg=3:3, lb=[-π/2], ub=[π/2], - label=:theta_bounds -) - -function boundary_singular!(b, q0, qf, v) - b[1] = q0[1] # x(0) = 0 - b[2] = q0[2] # y(0) = 0 - b[3] = qf[1] - 1.0 # x(tf) = 1 - b[4] = qf[2] # y(tf) = 0 - return nothing -end -constraint!(pre, - :boundary; - f=boundary_singular!, - lb=zeros(4), ub=zeros(4), - label=:endpoint -) - -# Mayer cost: minimise tf = variable[1] -mayer_singular(q0, qf, v) = v[1] -objective!(pre, :min; mayer=mayer_singular) - -time_dependence!(pre; autonomous=true) - -ocp_func = build(pre) -nothing # hide -``` - -```@raw html -
-
-``` - -Both formulations produce identical solutions: - -```@example ex-singular -sol_macro = solve(ocp_macro; display=false) -sol_func = solve(ocp_func; display=false) - -println("Macro: objective = ", objective(sol_macro), ", iterations = ", iterations(sol_macro)) -println("Functional API: objective = ", objective(sol_func), ", iterations = ", iterations(sol_func)) -``` - -```@example ex-singular -plt = plot(sol_macro; label="Macro", color=1, size=(800, 800)) -plot!(plt, sol_func; label="Functional API", color=2, linestyle=:dash) -``` - -### [State constraint](@id manual-macro-free-state-constraint) - -Same double integrator as the energy minimisation example, with an added upper bound on velocity. -See the [full example](@ref example-state-constraint) for solving and plotting. - -```@example ex-state -using OptimalControl -using NLPModelsIpopt -t0 = 0.0; tf = 1.0; x0 = [-1.0, 0.0]; xf = [0.0, 0.0] -nothing # hide -``` - -```@raw html -
-
-``` - -**Abstract syntax** - -```@example ex-state -ocp_macro = @def begin - -t ∈ [t0, tf], time -x = (q, v) ∈ R², state -u ∈ R, control - -x(t0) == x0 -x(tf) == xf - -v(t) ≤ 1.2 - -ẋ(t) == [v(t), u(t)] - -0.5∫( u(t)^2 ) → min - -end -nothing # hide -``` - -```@raw html -
-
-``` - -**Functional API** - -```@example ex-state -pre = OptimalControl.PreModel() - -time!(pre; t0=t0, tf=tf) -# state "x" with components "q" (position) and "v" (velocity) -state!(pre, 2, "x", ["q", "v"]) -control!(pre, 1) - -function f_state!(dx, t, x, u, v) - dx[1] = x[2] - dx[2] = u[1] - return nothing -end -dynamics!(pre, f_state!) - -function boundary_state!(b, x0_, xf_, v) - b[1] = x0_[1] - x0[1] - b[2] = x0_[2] - x0[2] - b[3] = xf_[1] - xf[1] - b[4] = xf_[2] - xf[2] - return nothing -end -constraint!(pre, - :boundary; - f=boundary_state!, - lb=zeros(4), ub=zeros(4), - label=:endpoint -) - -# state box constraint: v(t) ≤ 1.2, i.e. x[2] ≤ 1.2 -constraint!(pre, - :state; - rg=2:2, lb=[-Inf], ub=[1.2], - label=:v_max -) - -lagrange_state(t, x, u, v) = 0.5 * u[1]^2 -objective!(pre, :min; lagrange=lagrange_state) - -time_dependence!(pre; autonomous=true) - -ocp_func = build(pre) -nothing # hide -``` - -```@raw html -
-
-``` - -Both formulations produce identical solutions: - -```@example ex-state -sol_macro = solve(ocp_macro; display=false) -sol_func = solve(ocp_func; display=false) - -println("Macro: objective = ", objective(sol_macro), ", iterations = ", iterations(sol_macro)) -println("Functional API: objective = ", objective(sol_func), ", iterations = ", iterations(sol_func)) -``` - -```@example ex-state -plt = plot(sol_macro; label="Macro", color=1, size=(800, 600)) -plot!(plt, sol_func; label="Functional API", color=2, linestyle=:dash) -``` - -!!! note - The state box constraint `v(t) ≤ 1.2` is expressed as `constraint!(pre, :state; rg=2:2, lb=[-Inf], ub=[1.2], ...)`, where `rg=2:2` selects the second state component `v`. - -## API Reference - -```@docs; canonical=false -CTModels.PreModel -``` - -```@docs; canonical=false -CTModels.time! -``` - -```@docs; canonical=false -CTModels.state! -``` - -```@docs; canonical=false -CTModels.control! -``` - -```@docs; canonical=false -CTModels.variable! -``` - -```@docs; canonical=false -CTModels.dynamics! -``` - -```@docs; canonical=false -CTModels.objective! -``` - -```@docs; canonical=false -CTModels.constraint! -``` - -```@docs; canonical=false -CTModels.time_dependence! -``` - -```@docs; canonical=false -CTModels.build -``` - -```@docs; canonical=false -CTModels.Model -``` diff --git a/docs/attic/manual-model.md b/docs/attic/manual-model.md deleted file mode 100644 index 9afae3a51..000000000 --- a/docs/attic/manual-model.md +++ /dev/null @@ -1,721 +0,0 @@ -# [The optimal control problem object: structure and usage](@id manual-model) - -In this manual, we'll first recall the **main functionalities** you can use when working with an optimal control problem (OCP). This includes essential operations like: - -* **Solving an OCP**: How to find the optimal solution for your defined problem. -* **Computing flows from an OCP**: Understanding the dynamics and trajectories derived from the optimal solution. -* **Printing an OCP**: How to display a summary of your problem's definition. - -After covering these core functionalities, we'll delve into the **structure of an OCP**. Since an OCP is structured as a [`OptimalControl.Model`](@ref) struct, we'll first explain how to **access its underlying attributes**, such as the problem's dynamics, costs, and constraints. Following this, we'll shift our focus to the **simple properties** inherent to an OCP, learning how to determine aspects like whether the problem: - -* **Is autonomous**: Does its dynamics depend explicitly on time? -* **Has a fixed or free initial/final time**: Is the duration of the control problem predetermined or not? - ---- - -**Content** - -```@contents -Pages = ["manual-model.md"] -Depth = 2 -``` - ---- - -## [Main functionalities](@id manual-model-main-functionalities) - -Let's define a basic optimal control problem. - -```@example main -using OptimalControl - -t0 = 0 -tf = 1 -x0 = [-1, 0] - -ocp = @def begin - t ∈ [ t0, tf ], time - x = (q, v) ∈ R², state - u ∈ R, control - x(t0) == x0 - x(tf) == [0, 0] - ẋ(t) == [v(t), u(t)] - 0.5∫( u(t)^2 ) → min -end -nothing # hide -``` - -To print it, simply: - -```@example main -ocp -``` - -We can now solve the problem (for more details, visit the [solve manual](@ref manual-solve)): - -```@example main -using NLPModelsIpopt -solve(ocp) -nothing # hide -``` - -You can also compute flows (for more details, see the [flow manual](@ref manual-flow-ocp)) from the optimal control problem, providing a control law in feedback form. The **pseudo-Hamiltonian** of this problem is - -```math - H(x, p, u) = p_q\, v + p_v\, u + p^0 \frac{u^2}{2}, -``` - -where $p^0 = -1$ since we are in the normal case. From the Pontryagin maximum principle, the maximising control is given in feedback form by - -```math -u(x, p) = p_v -``` - -since $\partial^2_{uu} H = p^0 = - 1 < 0$. - -```@example main -u = (x, p) -> p[2] # control law in feedback form - -using OrdinaryDiffEq # needed to import numerical integrators -f = Flow(ocp, u) # compute the Hamiltonian flow function - -p0 = [12, 6] # initial covector solution -xf, pf = f(t0, x0, p0, tf) # flow from (x0, p0) at time t0 to tf -xf # should be (0, 0) -``` - -!!! note - - A more advanced feature allows for the discretization of the optimal control problem. From the discretized version, you can obtain a Nonlinear Programming problem (or optimization problem) and solve it using any appropriate NLP solver. For more details, visit the [NLP manipulation tutorial](https://control-toolbox.org/Tutorials.jl/stable/tutorial-nlp.html). - -## [Model struct](@id manual-model-struct) - -The optimal control problem `ocp` is a [`OptimalControl.Model`](@ref) struct. - -```@docs; canonical=false -OptimalControl.Model -``` - -Each field can be accessed directly (`ocp.times`, etc) or by a getter: - -* [`times`](@ref) -* [`state`](@ref) -* [`control`](@ref) -* [`variable`](@ref CTModels.OCP.variable) -* [`dynamics`](@ref) -* [`objective`](@ref CTModels.OCP.objective) -* [`constraints`](@ref) -* [`definition`](@ref) - -For instance, we can retrieve the `times` and `definition` values. - -```@example main -times(ocp) # returns the TimesModel struct containing time information -``` - -```@example main -definition(ocp) -``` - -!!! note - - We refer to the CTModels documentation for more details about this struct and its fields. - -To illustrate the various methods in the sections below, we define a more complex optimal control problem with free final time, variables, and various types of constraints: - -```@example main -ocp = @def begin - v = (w, tf) ∈ R², variable - s ∈ [0, tf], time - q = (x, y) ∈ R², state - u ∈ R, control - 0 ≤ tf ≤ 2, (1) - u(s) ≥ 0, (cons_u) - x(s) + u(s) ≤ 10, (cons_mixed) - w == 0 - x(0) == -1 - y(0) - tf == 0, (cons_bound) - q(tf) == [0, 0] - q̇(s) == [y(s)+w, u(s)] - 0.5∫( u(s)^2 ) → min -end -nothing # hide -``` - -## Times - -The time component defines the temporal domain of the optimal control problem. - -### Times model - -Get the times model: - -```@example main -times(ocp) # returns the TimesModel struct containing time information -``` - -You can also access initial and final times separately: - -```@example main -initial_time(ocp) # returns the initial time value -``` - -For the final time, if it is free (part of the variable), you need to provide the variable value: - -```@example main -v = [1, 2] # example variable values: w=1, tf=2 -final_time(ocp, v) # returns tf value from variable -``` - -If you try to get the final time without providing the variable when it's free, an error occurs: - -```@repl main -final_time(ocp) # error: tf is free, need variable -``` - -### Time variable names - -Get the names of the time variable and time bounds: - -```@example main -time_name(ocp) # returns "s" (the time variable name in this OCP) -``` - -```@example main -initial_time_name(ocp) # returns "0" (initial time is fixed at 0) -``` - -```@example main -final_time_name(ocp) # returns "tf" (final time is a variable) -``` - -### Time fixedness predicates - -Check whether initial or final times are fixed or free: - -```@example main -has_fixed_initial_time(ocp) # true if t0 is fixed -``` - -```@example main -has_free_initial_time(ocp) # true if t0 is free (part of variable) -``` - -!!! note "Variant methods" - Alternative methods with `is_*` prefix are also available and equivalent: - - `is_initial_time_fixed(ocp)` ≡ `has_fixed_initial_time(ocp)` - - `is_initial_time_free(ocp)` ≡ `has_free_initial_time(ocp)` - - `is_final_time_fixed(ocp)` ≡ `has_fixed_final_time(ocp)` - - `is_final_time_free(ocp)` ≡ `has_free_final_time(ocp)` - -Similarly for final time: - -```@example main -has_fixed_final_time(ocp) # false (tf is free in this OCP) -``` - -```@example main -has_free_final_time(ocp) # true (tf is part of variable v) -``` - -### Autonomy - -Check if the dynamics and Lagrange cost are autonomous (time-independent): - -```@example main -is_autonomous(ocp) # false if dynamics or cost depend on time -``` - -For more details on autonomy, see the [Time dependence](@ref manual-model-time-dependence) section below. - -### [Summary table](@id manual-model-summary-time) - -| Method | Returns | Description | -| -------- | --------- | --------- | -| `times(ocp)` | `(Float64, Any)` | Time interval (t0, tf) or (t0, tf_name) | -| `initial_time(ocp)` | `Float64` | Initial time t0 | -| `final_time(ocp)` | `Float64` | Final time tf (error if free) | -| `final_time(ocp, v)` | `Float64` | Final time tf from variable v | -| `time_name(ocp)` | `String` | Time variable name | -| `initial_time_name(ocp)` | `String` | Initial time name or value | -| `final_time_name(ocp)` | `String` | Final time name or value | -| `has_fixed_initial_time(ocp)` | `Bool` | True if t0 is fixed | -| `has_free_initial_time(ocp)` | `Bool` | True if t0 is free | -| `has_fixed_final_time(ocp)` | `Bool` | True if tf is fixed | -| `has_free_final_time(ocp)` | `Bool` | True if tf is free | -| `is_autonomous(ocp)` | `Bool` | True if time-independent | - -## State - -The state component represents the state variables of the optimal control problem. - -### State component information - -Get the name, dimension, and component names of the state: - -```@example main -state_name(ocp) # returns "q" (the state variable name) -``` - -```@example main -state_dimension(ocp) # returns 2 (dimension of state) -``` - -```@example main -state_components(ocp) # returns ["x", "y"] (component names) -``` - -!!! note - - The component names are used when plotting the solution. See the [plot manual](@ref manual-plot). - -### State box constraints - -Get the box constraints on the state (lower and upper bounds): - -```@example main -state_constraints_box(ocp) # returns box constraints if any -``` - -!!! note "Tuple structure" - The returned tuple has the structure `(lb, indices, ub, labels, aliases)` where: - - `lb`: vector of lower bounds - - `indices`: vector of component indices (1-based) - - `ub`: vector of upper bounds - - `labels`: vector of constraint labels - - `aliases`: vector of vectors containing all labels that declared each component - -Get the dimension of state box constraints: - -```@example main -dim_state_constraints_box(ocp) # returns number of box constraints on state -``` - -### [Summary table](@id manual-model-summary-state) - -| Method | Returns | Description | -| -------- | --------- | --------- | -| `state_name(ocp)` | `String` | State variable name | -| `state_dimension(ocp)` | `Int` | State dimension | -| `state_components(ocp)` | `Vector{String}` | State component names | -| `state_constraints_box(ocp)` | Box constraints | State box constraints | -| `dim_state_constraints_box(ocp)` | `Int` | Number of state box constraints | - -## Control - -The control component represents the control variables of the optimal control problem. - -### Control component information - -Get the name, dimension, and component names of the control: - -```@example main -control_name(ocp) # returns "u" (the control variable name) -``` - -```@example main -control_dimension(ocp) # returns 1 (dimension of control) -``` - -```@example main -control_components(ocp) # returns ["u"] (component names) -``` - -### Control box constraints - -Get the box constraints on the control: - -```@example main -control_constraints_box(ocp) # returns box constraints if any -``` - -!!! note "Tuple structure" - The returned tuple has the structure `(lb, indices, ub, labels, aliases)` where: - - `lb`: vector of lower bounds - - `indices`: vector of component indices (1-based) - - `ub`: vector of upper bounds - - `labels`: vector of constraint labels - - `aliases`: vector of vectors containing all labels that declared each component - -Get the dimension of control box constraints: - -```@example main -dim_control_constraints_box(ocp) # returns number of box constraints on control -``` - -### Control presence - -Check whether the problem has a control input: - -```@example main -has_control(ocp) # true if problem has a control input -``` - -!!! note "Variant method" - - - `is_control_free(ocp)` ≡ `!has_control(ocp)` - -### [Summary table](@id manual-model-summary-control) - -| Method | Returns | Description | -| -------- | --------- | --------- | -| `control_name(ocp)` | `String` | Control variable name | -| `control_dimension(ocp)` | `Int` | Control dimension | -| `control_components(ocp)` | `Vector{String}` | Control component names | -| `control_constraints_box(ocp)` | Box constraints | Control box constraints | -| `dim_control_constraints_box(ocp)` | `Int` | Number of control box constraints | -| `has_control(ocp)` | `Bool` | True if problem has a control input | -| `is_control_free(ocp)` | `Bool` | True if problem has no control (`≡ !has_control`) | - -## Variable - -The variable component represents the optimization variables (parameters) of the optimal control problem. - -### Variable component information - -Get the name, dimension, and component names of the variable: - -```@example main -variable_name(ocp) # returns "v" (the variable name) -``` - -```@example main -variable_dimension(ocp) # returns 2 (dimension of variable) -``` - -```@example main -variable_components(ocp) # returns ["w", "tf"] (component names) -``` - -### Variable box constraints - -Get the box constraints on the variable: - -```@example main -variable_constraints_box(ocp) # returns box constraints if any -``` - -!!! note "Tuple structure" - The returned tuple has the structure `(lb, indices, ub, labels, aliases)` where: - - `lb`: vector of lower bounds - - `indices`: vector of component indices (1-based) - - `ub`: vector of upper bounds - - `labels`: vector of constraint labels - - `aliases`: vector of vectors containing all labels that declared each component - -Get the dimension of variable box constraints: - -```@example main -dim_variable_constraints_box(ocp) # returns number of box constraints on variable -``` - -### Variable presence - -Check whether the problem has optimization variables: - -```@example main -has_variable(ocp) # true if problem has optimization variables -``` - -!!! note "Variant methods" - - - `is_variable(ocp)` ≡ `has_variable(ocp)` - - `is_nonvariable(ocp)` ≡ `!has_variable(ocp)` - -### [Summary table](@id manual-model-summary-variable) - -| Method | Returns | Description | -| -------- | --------- | --------- | -| `variable_name(ocp)` | `String` | Variable name | -| `variable_dimension(ocp)` | `Int` | Variable dimension | -| `variable_components(ocp)` | `Vector{String}` | Variable component names | -| `variable_constraints_box(ocp)` | Box constraints | Variable box constraints | -| `dim_variable_constraints_box(ocp)` | `Int` | Number of variable box constraints | -| `has_variable(ocp)` | `Bool` | True if problem has optimization variables | -| `is_variable(ocp)` | `Bool` | Alias for `has_variable` | -| `is_nonvariable(ocp)` | `Bool` | True if problem has no variables (`≡ !has_variable`) | - -## Dynamics - -The dynamics component defines the differential equations governing the state evolution. - -### Dynamics function - -The dynamics are stored as an in-place function of the form `f!(dx, t, x, u, v)`: - -```@example main -f! = dynamics(ocp) -s = 0.5 # time -q = [0.0, 1.0] # state -u = 2.0 # control -v = [1.0, 2.0] # variable -dq = similar(q) -f!(dq, s, q, u, v) -dq # returns the derivative q̇ -``` - -The first argument `dx` is mutated upon call and contains the state derivative. The other arguments are: - -* `t`: time -* `x`: state -* `u`: control -* `v`: variable - -### [Summary table](@id manual-model-summary-dynamics) - -| Method | Returns | Description | -| -------- | --------- | --------- | -| `dynamics(ocp)` | `Function` | In-place dynamics function f!(dx, t, x, u, v) | - -## Objective - -The objective component defines the cost function to minimize or maximize. - -### Criterion - -The criterion indicates whether the problem is a minimization or maximization: - -```@example main -criterion(ocp) # returns :min or :max -``` - -### Objective form - -The objective function can be in Mayer form, Lagrange form, or Bolza form (combination of both): - -* **Mayer**: $g(x(t_0), x(t_f), v) \to \min$ -* **Lagrange**: $\int_{t_0}^{t_f} f^0(t, x(t), u(t), v)\, \mathrm{d}t \to \min$ -* **Bolza**: $g(x(t_0), x(t_f), v) + \int_{t_0}^{t_f} f^0(t, x(t), u(t), v)\, \mathrm{d}t \to \min$ - -Check which form is present: - -```@example main -has_mayer_cost(ocp) # true if Mayer cost exists -``` - -```@example main -has_lagrange_cost(ocp) # true if Lagrange cost exists -``` - -!!! note "Variant methods" - Alternative methods are also available: - - `is_mayer_cost_defined(ocp)` ≡ `has_mayer_cost(ocp)` - - `is_lagrange_cost_defined(ocp)` ≡ `has_lagrange_cost(ocp)` - -### Mayer cost - -Get the Mayer cost function with signature `g(x0, xf, v)`: - -```@repl main -g = mayer(ocp) # error if no Mayer cost -``` - -### Lagrange cost - -Get the Lagrange cost function with signature `f⁰(t, x, u, v)`: - -```@example main -f⁰ = lagrange(ocp) -s = 0.5 -q = [0.0, 1.0] -u = 2.0 -v = [1.0, 2.0] -f⁰(s, q, u, v) # returns the integrand value -``` - -### [Summary table](@id manual-model-summary-objective) - -| Method | Returns | Description | -| -------- | --------- | --------- | -| `criterion(ocp)` | `Symbol` | `:min` or `:max` | -| `has_mayer_cost(ocp)` | `Bool` | True if Mayer cost exists | -| `has_lagrange_cost(ocp)` | `Bool` | True if Lagrange cost exists | -| `mayer(ocp)` | `Function` | Mayer cost function g(x0, xf, v) | -| `lagrange(ocp)` | `Function` | Lagrange cost function f⁰(t, x, u, v) | - -## Constraints - -The constraints component defines the constraints on the optimal control problem. - -### Individual constraints - -Retrieve a specific constraint by its label using the `constraint` function. It returns a tuple `(type, f, lb, ub)`: - -```@example main -(type, f, lb, ub) = constraint(ocp, :eq1) -println("type: ", type) -x0 = [0, 1] -xf = [2, 3] -v = [1, 4] -println("val: ", f(x0, xf, v)) -println("lb: ", lb) -println("ub: ", ub) -``` - -The function signature depends on the constraint type: - -* For `:boundary` and `:variable` constraints: `f(x0, xf, v)` -* For other constraints (`:control`, `:state`, `:mixed`): `f(t, x, u, v)` - -Examples of different constraint types: - -```@example main -(type, f, lb, ub) = constraint(ocp, :cons_bound) -println("type: ", type) -println("val: ", f(x0, xf, v)) -``` - -```@example main -(type, f, lb, ub) = constraint(ocp, :cons_u) -println("type: ", type) -s = 0.5 -q = [1.0, 2.0] -u = 3.0 -println("val: ", f(s, q, u, v)) -``` - -```@example main -(type, f, lb, ub) = constraint(ocp, :cons_mixed) -println("type: ", type) -println("val: ", f(s, q, u, v)) -``` - -### All constraints - -Get all constraints as a collection: - -```@example main -constraints(ocp) # returns all constraints -``` - -### Nonlinear constraints - -Get nonlinear path and boundary constraints: - -```@example main -path_constraints_nl(ocp) # returns nonlinear path constraints -``` - -```@example main -boundary_constraints_nl(ocp) # returns nonlinear boundary constraints -``` - -!!! note "Tuple structure" - The returned tuples have the structure `(lb, f!, ub, labels)` where: - - `lb`: vector of lower bounds - - `f!`: constraint function (in-place) - - `ub`: vector of upper bounds - - `labels`: vector of constraint labels - - The constraint functions have the following signatures: - - Path constraints: `f!(val, t, x, u, v)` where `val` is mutated - - Boundary constraints: `f!(val, x0, xf, v)` where `val` is mutated - -Get the dimensions of nonlinear constraints: - -```@example main -dim_path_constraints_nl(ocp) # number of nonlinear path constraints -``` - -```@example main -dim_boundary_constraints_nl(ocp) # number of nonlinear boundary constraints -``` - -!!! note - - To get the dual variable (or Lagrange multiplier) associated to a constraint, use the [`dual`](@ref) method on a solution. - -### [Summary table](@id manual-model-summary-constraints) - -| Method | Returns | Description | -| -------- | --------- | --------- | -| `constraint(ocp, label)` | `(Symbol, Function, Real, Real)` | Get constraint by label | -| `constraints(ocp)` | Collection | All constraints | -| `path_constraints_nl(ocp)` | Constraints | Nonlinear path constraints | -| `boundary_constraints_nl(ocp)` | Constraints | Nonlinear boundary constraints | -| `dim_path_constraints_nl(ocp)` | `Int` | Number of nonlinear path constraints | -| `dim_boundary_constraints_nl(ocp)` | `Int` | Number of nonlinear boundary constraints | - -## Problem definition - -Get the problem definition as a string: - -```@example main -definition(ocp) # returns the OCP definition as AbstractDefinition -``` - -To extract the expression from the definition, use: - -```@example main -expr = expression(ocp) # returns the Expr from the definition -nothing # hide -``` - -!!! note - - The definition is optional and can be `EmptyDefinition`. Use `has_abstract_definition(ocp)` to check if a definition is present. - -### Definition presence - -Check whether the problem carries an abstract definition: - -```@example main -has_abstract_definition(ocp) # true if definition is present (not EmptyDefinition) -``` - -!!! note "Variant method" - - - `is_abstractly_defined(ocp)` ≡ `has_abstract_definition(ocp)` - -## [Time dependence](@id manual-model-time-dependence) - -Optimal control problems can be **autonomous** or **non-autonomous**. In an autonomous problem, neither the dynamics nor the Lagrange cost explicitly depends on the time variable. - -The following problem is autonomous. - -```@example main -ocp = @def begin - t ∈ [ 0, 1 ], time - x ∈ R, state - u ∈ R, control - ẋ(t) == u(t) # no explicit dependence on t - x(1) + 0.5∫( u(t)^2 ) → min # no explicit dependence on t -end -is_autonomous(ocp) -``` - -The following problem is non-autonomous since the dynamics depends on `t`. - -```@example main -ocp = @def begin - t ∈ [ 0, 1 ], time - x ∈ R, state - u ∈ R, control - ẋ(t) == u(t) + t # explicit dependence on t - x(1) + 0.5∫( u(t)^2 ) → min -end -is_autonomous(ocp) -``` - -Finally, this last problem is non-autonomous because the Lagrange part of the cost depends on `t`. - -```@example main -ocp = @def begin - t ∈ [ 0, 1 ], time - x ∈ R, state - u ∈ R, control - ẋ(t) == u(t) - x(1) + 0.5∫( t + u(t)^2 ) → min # explicit dependence on t -end -is_autonomous(ocp) -``` - -The variant predicate `is_nonautonomous` is also available and returns the opposite of `is_autonomous`: - -```@example main -is_nonautonomous(ocp) # true if dynamics or cost depend on time -``` - -!!! note "Variant method" - - - `is_nonautonomous(ocp)` ≡ `!is_autonomous(ocp)` diff --git a/docs/attic/manual-plot.md b/docs/attic/manual-plot.md deleted file mode 100644 index 988e8957f..000000000 --- a/docs/attic/manual-plot.md +++ /dev/null @@ -1,454 +0,0 @@ -# [How to plot a solution](@id manual-plot) - -In this tutorial, we explain the different options for plotting the solution of an optimal control problem using the `plot` and `plot!` functions, which are extensions of the [Plots.jl](https://docs.juliaplots.org) package. Use `plot` to create a new plot object, and `plot!` to add to an existing one: - -```julia -plot(args...; kw...) # creates a new Plot, and set it to be the `current` -plot!(args...; kw...) # modifies Plot `current()` -plot!(plt, args...; kw...) # modifies Plot `plt` -``` - -More precisely, the signature of `plot`, to plot a solution, is as follows. - -```@docs; canonical=false -plot(::CTModels.Solution, ::Symbol...) -plot!(::CTModels.Solution, ::Symbol...) -plot!(::Plots.Plot, ::CTModels.Solution, ::Symbol...) -``` - -## Argument Overview - -The table below summarizes the main plotting arguments and links to the corresponding documentation sections for detailed explanations: - -| Section | Relevant Arguments | -| :---------------------------------------------------| :-------------------------------------------------------------------------------------------- | -| [Basic concepts](@ref manual-plot-basic) | `size`, `state_style`, `costate_style`, `control_style`, `time_style`, `kwargs...` | -| [Split vs. group layout](@ref manual-plot-layout) | `layout` | -| [Plotting control norm](@ref manual-plot-control) | `control` | -| [Normalised time](@ref manual-plot-time) | `time` | -| [Constraints](@ref manual-plot-constraints) | `state_bounds_style`, `control_bounds_style`, `path_style`, `path_bounds_style`, `dual_style` | -| [What to plot](@ref manual-plot-select) | `description...` | - -You can plot solutions obtained from the `solve` function or from a flow computed using an optimal control problem and a control law. See the [Basic Concepts](@ref manual-plot-basic) and [From Flow function](@ref manual-plot-flow) sections for details. - -To overlay a new plot on an existing one, use the `plot!` function (see [Add a plot](@ref manual-plot-add)). - -If you prefer full control over the visualisation, you can extract the state, costate, and control to create your own plots. Refer to the [Custom plot](@ref manual-plot-custom) section for guidance. You can also access the subplots. - -## The problem and the solution - -Let us start by importing the packages needed to define and solve the problem. - -```@example main -using OptimalControl -using NLPModelsIpopt -``` - -We consider the simple optimal control problem from the [basic example page](@ref example-double-integrator-energy). - -```@example main -t0 = 0 # initial time -tf = 1 # final time -x0 = [-1, 0] # initial condition -xf = [ 0, 0] # final condition - -ocp = @def begin - t ∈ [t0, tf], time - x ∈ R², state - u ∈ R, control - x(t0) == x0 - x(tf) == xf - ẋ(t) == [x₂(t), u(t)] - ∫( 0.5u(t)^2 ) → min -end - -sol = solve(ocp, display=false) -nothing # hide -``` - -## [Basic concepts](@id manual-plot-basic) - -The simplest way to plot the solution is to use the `plot` function with the solution as the only argument. - -!!! caveat - - The `plot` function for a solution of an optimal control problem extends the `plot` function from Plots.jl. Therefore, you need to import this package in order to plot a solution. - -```@example main -using Plots -plot(sol) -``` - -In the figure above, we have a grid of subplots: the left column displays the state component trajectories, the right column shows the costate component trajectories, and the bottom row contains the control component trajectory. - -As in Plots.jl, input data is passed positionally (for example, `sol` in `plot(sol)`), and attributes are passed as keyword arguments (for example, `plot(sol; color = :blue)`). After executing `using Plots` in the REPL, you can use the `plotattr()` function to print a list of all available attributes for series, plots, subplots, or axes. - -```julia -# Valid Operations -plotattr(:Plot) -plotattr(:Series) -plotattr(:Subplot) -plotattr(:Axis) -``` - -Once you have the list of attributes, you can either use the aliases of a specific attribute or inspect a specific attribute to display its aliases and description. - -```@repl main -plotattr("color") # Specific Attribute Example -``` - -!!! warning - - Some attributes have different default values in OptimalControl.jl compared to Plots.jl. For instance, the default figure size is 600x400 in Plots.jl, while in OptimalControl.jl, it depends on the number of states and controls. - -You can also visit the Plot documentation online to get the descriptions of the attributes: - -- To pass attributes to the plot, see the [attributes plot](https://docs.juliaplots.org/latest/generated/attributes_plot/) documentation. For instance, you can specify the size of the figure. - -```@raw html -
List of plot attributes. -``` - -```@example main -for a in Plots.attributes(:Plot) # hide - println(a) # hide -end # hide -``` - -```@raw html -
-``` - -- You can pass attributes to all subplots at once by referring to the [attributes subplot](https://docs.juliaplots.org/latest/generated/attributes_subplot/) documentation. For example, you can specify the location of the legends. - -```@raw html -
List of subplot attributes. -``` - -```@example main -for a in Plots.attributes(:Subplot) # hide - println(a) # hide -end # hide -``` - -```@raw html -
-``` - -- Similarly, you can pass axis attributes to all subplots. See the [attributes axis](https://docs.juliaplots.org/latest/generated/attributes_axis/) documentation. For example, you can remove the grid from every subplot. - -```@raw html -
List of axis attributes. -``` - -```@example main -for a in Plots.attributes(:Axis) # hide - println(a) # hide -end # hide -``` - -```@raw html -
-``` - -- Finally, you can pass series attributes to all subplots. Refer to the [attributes series](https://docs.juliaplots.org/latest/generated/attributes_series/) documentation. For instance, you can set the width of the curves using `linewidth`. - -```@raw html -
List of series attributes. -``` - -```@example main -for a in Plots.attributes(:Series) # hide - println(a) # hide -end # hide -``` - -```@raw html -
-
-``` - -```@example main -plot(sol, size=(700, 450), label="sol", legend=:bottomright, grid=false, linewidth=2) -``` - -To specify series attributes for a specific group of subplots (state, costate or control), you can use the optional keyword arguments `state_style`, `costate_style`, and `control_style`, which correspond to the state, costate, and control trajectories, respectively. - -```@example main -plot(sol; - state_style = (color=:blue,), # style: state trajectory - costate_style = (color=:black, linestyle=:dash), # style: costate trajectory - control_style = (color=:red, linewidth=2)) # style: control trajectory -``` - -Vertical axes at the initial and final times are automatically plotted. The style can me modified with the `time_style` keyword argument. -Additionally, you can choose not to display for instance the state and the costate trajectories by setting their styles to `:none`. You can set to `:none` any style. - -```@example main -plot(sol; - state_style = :none, # do not plot the state - costate_style = :none, # do not plot the costate - control_style = (color = :red,), # plot the control in red - time_style = (color = :green,)) # vertical axes at initial and final times in green -``` - -To select what to display, you can also use the `description` argument by providing a list of symbols such as `:state`, `:costate`, and `:control`. - -```@example main -plot(sol, :state, :control) # plot the state and the control -``` - -!!! note "Select what to plot" - - For more details on how to choose what to plot, see the [What to plot](@ref manual-plot-select) section. - -## [From Flow function](@id manual-plot-flow) - -The previous solution of the optimal control problem was obtained using the [`solve`](@ref) function. If you prefer using an indirect shooting method and solving shooting equations, you may also want to plot the associated solution. To do this, you need to use the [`Flow`](@ref) function to reconstruct the solution. See the manual on [how to compute flows](@ref manual-flow-ocp) for more details. In our case, you must provide the maximizing control $(x, p) \mapsto p_2$ along with the optimal control problem. For an introduction to simple indirect shooting, see the [indirect simple shooting](@extref tutorial-indirect-simple-shooting) tutorial for an example. - -!!! tip "Interactions with an optimal control solution" - - Please check [`state`](@ref), [`costate`](@ref), [`control`](@ref), and [`variable`](@ref CTModels.OCP.variable) to retrieve data from the solution. The functions `state`, `costate`, and `control` return functions of time, while `variable` returns a vector. - -```@example main -using OrdinaryDiffEq - -p = costate(sol) # costate as a function of time -p0 = p(t0) # costate solution at the initial time -f = Flow(ocp, (x, p) -> p[2]) # flow from an ocp and a control law in feedback form - -sol_flow = f((t0, tf), x0, p0) # compute the solution -plot(sol_flow) # plot the solution from a flow -``` - -We may notice that the time grid contains very few points. This is evident from the subplot of $x_2$, or by retrieving the time grid directly from the solution. - -```@example main -time_grid(sol_flow) -``` - -To improve visualisation (without changing the accuracy), you can provide a finer grid. - -```@example main -fine_grid = range(t0, tf, 100) -sol_flow = f((t0, tf), x0, p0; saveat=fine_grid) -plot(sol_flow) -``` - -## [Split vs. group layout](@id manual-plot-layout) - -If you prefer to get a more compact figure, you can use the `layout` optional keyword argument with `:group` value. It will group the state, costate and control trajectories in one subplot for each. - -```@example main -plot(sol; layout=:group) -``` - -The default layout value is `:split` which corresponds to the grid of subplots presented above. - -```@example main -plot(sol; layout=:split) -``` - -## [Add a plot](@id manual-plot-add) - -You can plot the solution of a second optimal control problem on the same figure if it has the same number of states, costates and controls. For instance, consider the same optimal control problem but with a different initial condition. - -```@example main -ocp = @def begin - t ∈ [t0, tf], time - x ∈ R², state - u ∈ R, control - x(t0) == [-0.5, -0.5] - x(tf) == xf - ẋ(t) == [x₂(t), u(t)] - ∫( 0.5u(t)^2 ) → min -end -sol2 = solve(ocp; display=false) -nothing # hide -``` - -We first plot the solution of the first optimal control problem, then, we plot the solution of the second optimal control problem on the same figure, but with dashed lines. - -```@example main -plt = plot(sol; label="sol1", size=(700, 500)) -plot!(plt, sol2; label="sol2", linestyle=:dash) -``` - -You can also, implicitly, use the current plot. - -```@example main -plot(sol; label="sol1", size=(700, 500)) -plot!(sol2; label="sol2", linestyle=:dash) -``` - -## [Plotting the control norm](@id manual-plot-control) - -For some problem, it is interesting to plot the (Euclidean) norm of the control. You can do it by using the `control` optional keyword argument with `:norm` value. - -```@example main -plot(sol; control=:norm, size=(800, 300), layout=:group) -``` - -The default value is `:components`. - -```@example main -plot(sol; control=:components, size=(800, 300), layout=:group) -``` - -You can also plot the control and its norm. - -```@example main -plot(sol; control=:all, layout=:group) -``` - -## [Custom plot and subplots](@id manual-plot-custom) - -You can, of course, create your own plots by extracting the `state`, `costate`, and `control` from the optimal control solution. For instance, let us plot the norm of the control. - -```@example main -using LinearAlgebra -t = time_grid(sol) -u = control(sol) -plot(t, norm∘u; label="‖u‖", xlabel="t") -``` - -You can also get access to the subplots. The order is as follows: state, costate, control, path constraints (if any) and their dual variables. - -```@example main -plt = plot(sol) -plot(plt[1]) # x₁ -``` - -```@example main -plt = plot(sol) -plot(plt[2]) # x₂ -``` - -```@example main -plt = plot(sol) -plot(plt[3]) # p₁ -``` - -```@example main -plot(plt[4]) # p₂ -``` - -```@example main -plot(plt[5]) # u -``` - -## [Normalised time](@id manual-plot-time) - -We consider a [LQR example](@extref tutorial-lqr) and solve the problem for different values of the final time `tf`. Then, we plot the solutions on the same figure using a normalised time $s = (t - t_0) / (t_f - t_0)$, enabled by the keyword argument `time = :normalize` (or `:normalise`) in the `plot` function. - -```@example main -# definition of the problem, parameterised by the final time -function lqr(tf) - - ocp = @def begin - t ∈ [0, tf], time - x ∈ R², state - u ∈ R, control - x(0) == [0, 1] - ẋ(t) == [x₂(t), - x₁(t) + u(t)] - ∫( 0.5(x₁(t)^2 + x₂(t)^2 + u(t)^2) ) → min - end - - return ocp -end - -# solve the problems and store them -solutions = [] -tfs = [3, 5, 30] -for tf ∈ tfs - solution = solve(lqr(tf); display=false) - push!(solutions, solution) -end - -# create plots -plt = plot() -for (tf, sol) ∈ zip(tfs, solutions) - plot!(plt, sol; time=:normalize, label="tf = $tf", xlabel="s") -end - -# make a custom plot: keep only state and control -px1 = plot(plt[1]; legend=false) # x₁ -px2 = plot(plt[2]; legend=true) # x₂ -pu = plot(plt[5]; legend=false) # u - -using Plots.PlotMeasures # for leftmargin, bottommargin -plot(px1, px2, pu; layout=(1, 3), size=(800, 300), leftmargin=5mm, bottommargin=5mm) -``` - -## [Constraints](@id manual-plot-constraints) - -We define an optimal control problem with constraints, solve it and plot the solution. - -```@example main -ocp = @def begin - tf ∈ R, variable - t ∈ [0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - tf ≥ 0 - -1 ≤ u(t) ≤ 1 - q(0) == -1 - v(0) == 0 - q(tf) == 0 - v(tf) == 0 - 1 ≤ v(t)+1 ≤ 1.8, (1) - ẋ(t) == [v(t), u(t)] - tf → min -end -sol = solve(ocp) -plot(sol) -``` - -On the plot, you can see the lower and upper bounds of the path constraint. Additionally, the dual variable associated with the path constraint is displayed alongside it. - -You can customise the plot styles. For style options related to the state, costate, and control, refer to the [Basic Concepts](@ref manual-plot-basic) section. - -```@example main -plot(sol; - state_bounds_style = (linestyle = :dash,), - control_bounds_style = (linestyle = :dash,), - path_style = (color = :green,), - path_bounds_style = (linestyle = :dash,), - dual_style = (color = :red,), - time_style = :none, # do not plot axes at t0 and tf -) -``` - -## [What to plot](@id manual-plot-select) - -You can choose what to plot using the `description` argument. To plot only one subgroup: - -```julia -plot(sol, :state) # plot only the state -plot(sol, :costate) # plot only the costate -plot(sol, :control) # plot only the control -plot(sol, :path) # plot only the path constraint -plot(sol, :dual) # plot only the path constraint dual variable -``` - -You can combine elements to plot exactly what you need: - -```@example main -plot(sol, :state, :control, :path) -``` - -Similarly, you can choose what not to plot passing `:none` to the corresponding style. - -```julia -plot(sol; state_style=:none) # do not plot the state -plot(sol; costate_style=:none) # do not plot the costate -plot(sol; control_style=:none) # do not plot the control -plot(sol; path_style=:none) # do not plot the path constraint -plot(sol; dual_style=:none) # do not plot the path constraint dual variable -``` - -For instance, let's plot everything except the dual variable associated with the path constraint. - -```@example main -plot(sol; dual_style=:none) -``` diff --git a/docs/attic/manual-solution.md b/docs/attic/manual-solution.md deleted file mode 100644 index 07b5a8099..000000000 --- a/docs/attic/manual-solution.md +++ /dev/null @@ -1,437 +0,0 @@ -# [The optimal control solution object: structure and usage](@id manual-solution) - -In this manual, we'll first recall the **main functionalities** you can use when working with a solution of an optimal control problem. This includes essential operations like: - -* **Plotting a solution**: How to plot the optimal solution for your defined problem. -* **Printing a solution**: How to display a summary of your solution. - -After covering these core functionalities, we'll delve into the **structure of a solution**. Since a solution is structured as a [`OptimalControl.Solution`](@ref) struct, we'll first explain how to **access its underlying attributes**. Following this, we'll shift our focus to the **simple properties** inherent to a solution. - ---- - -**Content** - -```@contents -Pages = ["manual-solution.md"] -Depth = 2 -``` - ---- - -## [Main functionalities](@id manual-solution-main-functionalities) - -Let's define a basic optimal control problem. - -```@example main -using OptimalControl - -t0 = 0 -tf = 1 -x0 = [-1, 0] - -ocp = @def begin - t ∈ [ t0, tf ], time - x = (q, v) ∈ R², state - u ∈ R, control - x(t0) == x0 - x(tf) == [0, 0] - ẋ(t) == [v(t), u(t)] - 0.5∫( u(t)^2 ) → min -end -nothing # hide -``` - -We can now solve the problem (for more details, visit the [solve manual](@ref manual-solve)): - -```@example main -using NLPModelsIpopt -sol = solve(ocp) -nothing # hide -``` - -!!! note - - You can export (or save) the solution in a Julia `.jld2` data file and reload it later, and also export a discretised version of the solution in a more portable [JSON](https://en.wikipedia.org/wiki/JSON) format. Note that the optimal control problem is needed when loading a solution. - - See the two functions: - - - [`import_ocp_solution`](@ref), - - [`export_ocp_solution`](@ref). - -To print `sol`, simply: - -```@example main -sol -``` - -For complementary information, you can plot the solution: - -```@example main -using Plots -plot(sol) -``` - -!!! note - - For more details about plotting a solution, visit the [plot manual](@ref manual-plot). - -## [Solution struct](@id manual-solution-struct) - -The solution `sol` is a [`OptimalControl.Solution`](@ref) struct. - -```@docs; canonical=false -OptimalControl.Solution -``` - -Each field can be accessed directly (`sol.state`, etc) but we recommend to use the sophisticated getters we provide: the `state(sol::Solution)` method does not return `sol.state` but a function of time that can be called at any time, not only on the grid `time_grid`. - -```@example main -0.25 ∈ time_grid(sol) -``` - -```@example main -x = state(sol) -x(0.25) -``` - -You can also retrieve the original optimal control problem from the solution: - -```@example main -model(sol) # returns the original OCP model -``` - -## Trajectories - -The trajectory component provides access to the state, control, variable, and costate trajectories. - -### State trajectory - -Get the state trajectory as a function of time: - -```@example main -x = state(sol) # returns a function of time -``` - -Evaluate the state at any time (not just grid points): - -```@example main -t = 0.25 -x(t) # returns state vector at t=0.25 -``` - -The state function can be evaluated at any time within the problem horizon, even if it's not a discretization grid point: - -```@example main -0.25 ∈ time_grid(sol) # false: not a grid point -``` - -```@example main -x(0.25) # still works: interpolated value -``` - -### Control trajectory - -Get the control trajectory as a function of time: - -```@example main -u = control(sol) # returns a function of time -``` - -```@example main -u(t) # returns control value at t -``` - -### Variable values - -Get the optimization variable values: - -```@example main -v = variable(sol) # returns an empty vector if no variable -``` - -### Costate trajectory - -Get the costate (adjoint) trajectory as a function of time: - -```@example main -p = costate(sol) # returns a function of time -``` - -```@example main -p(t) # returns costate vector at t -``` - -### Time information - -Get time-related information from the solution: - -```@example main -time_grid(sol) # returns the discretization time grid -``` - -```@example main -times(sol) # returns the TimesModel struct containing time information -``` - -!!! note "Time grids" - **Unified vs. multiple grids:** - - With a standard collocation method, there is a single time grid that can be retrieved via `time_grid(sol)`. The solution internally uses a `UnifiedTimeGridModel` for memory efficiency. - - For discretization methods that use multiple grids (one per component), the solution uses a `MultipleTimeGridModel`. In this case, you must specify which component's grid you want: - - - `time_grid(sol, :state)` — state trajectory and state box constraint duals - - `time_grid(sol, :control)` — control trajectory and control box constraint duals - - `time_grid(sol, :costate)` — costate trajectory (maps to `:state` grid) - - `time_grid(sol, :path)` — path constraint duals - - Aliases are accepted: `:costate`/`:costates` map to `:state`, `:dual`/`:duals` map to `:path`, and plural forms (`:states`, `:controls`) are also valid. - - All grids must be strictly increasing, finite, and non-empty. - -!!! note "Trajectory data formats" - Trajectories (`state`, `control`, `costate`, `path_constraints_dual`) can be provided either as matrices (rows = time points, columns = components) or as functions `t -> vector` for interpolated or analytical data. - -### [Summary table](@id manual-solution-summary-trajectories) - -| Method | Returns | Description | -| -------- | --------- | ------------- | -| `state(sol)` | `Function` | State trajectory x(t) | -| `control(sol)` | `Function` | Control trajectory u(t) | -| `variable(sol)` | `Vector` | Variable values | -| `costate(sol)` | `Function` | Costate trajectory p(t) | -| `time_grid(sol)` | `Vector{Float64}` | Discretization time grid | -| `times(sol)` | `TimesModel` | TimesModel struct containing time information | - -## Objective - -The objective component provides access to the objective value. - -### Objective value - -Get the optimal objective value: - -```@example main -objective(sol) # returns the objective value -``` - -### [Summary table](@id manual-solution-summary-objective) - -| Method | Returns | Description | -| -------- | --------- | ------------- | -| `objective(sol)` | `Float64` | Objective value | - -## Dual variables - -The dual variables (Lagrange multipliers) provide sensitivity information about constraints. - -To illustrate dual variables, we define a problem with various constraints: - -```@example main -ocp = @def begin - tf ∈ R, variable - t ∈ [0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - tf ≥ 0, (eq_tf) - -1 ≤ u(t) ≤ 1, (eq_u) - v(t) ≤ 0.75, (eq_v) - x(0) == [-1, 0], (eq_x0) - q(tf) == 0 - v(tf) == 0 - ẋ(t) == [v(t), u(t)] - tf → min -end -sol = solve(ocp; display=false) -nothing # hide -``` - -### Dual of labeled constraints - -Get the dual variable for a specific labeled constraint: - -```@example main -dual(sol, ocp, :eq_tf) # dual for variable constraint -``` - -```@example main -dual(sol, ocp, :eq_x0) # dual for boundary constraint -``` - -For path constraints, the dual is a function of time: - -```@example main -μ_u = dual(sol, ocp, :eq_u) -plot(time_grid(sol), μ_u) -``` - -```@example main -μ_v = dual(sol, ocp, :eq_v) -plot(time_grid(sol), μ_v) -``` - -!!! note "Signed multiplier convention" - - In all cases, `dual(sol, ocp, :label)` returns a **signed multiplier** `μ` (scalar, vector, or function of time, depending on the constraint type). The sign convention is, component-wise: - - - `μ > 0` ⇒ the lower-side constraint is active (e.g. `lb ≤ ...`), - - `μ < 0` ⇒ the upper-side constraint is active (e.g. `... ≤ ub`), - - `μ = 0` ⇒ the constraint is inactive (or the component is never constrained). - - For **nonlinear path and boundary constraints**, the solver already returns a signed multiplier natively; CTModels simply forwards it via `path_constraints_dual(sol)` / `boundary_constraints_dual(sol)`. - - For **box constraints** (state/control/variable components), the solver stores lower- and upper-bound multipliers *separately as non-negative quantities*. CTModels combines them into the signed multiplier explicitly: - - ``` - μ = μ_lb − μ_ub - ``` - - computed per targeted primal component. This is the value returned by `dual(sol, ocp, :label)` for a box-constraint label. - - Rationale for box constraints: after intersection of duplicate box declarations, the solver only sees a single effective bound per component, hence a single signed multiplier per component. If several labels target the same component, each label returns the **same** per-component multiplier (via the `aliases` mechanism; see the [OCP manual](@ref manual-model) and [Duplicate box constraints](@ref manual-abstract-box-dedup)). - - The raw non-negative `*_lb_dual(sol)` / `*_ub_dual(sol)` accessors (see below) remain available if the unsigned components are needed separately. - -### Box constraint duals - -Get dual variables for box constraints on state, control, and variable: - -```@example main -state_constraints_lb_dual(sol) # lower bound duals for state -``` - -```@example main -state_constraints_ub_dual(sol) # upper bound duals for state -``` - -```@example main -control_constraints_lb_dual(sol) # lower bound duals for control -``` - -```@example main -control_constraints_ub_dual(sol) # upper bound duals for control -``` - -```@example main -variable_constraints_lb_dual(sol) # lower bound duals for variable -``` - -```@example main -variable_constraints_ub_dual(sol) # upper bound duals for variable -``` - -### Box constraint dual dimensions - -Get the dimensions of the box-constraint dual vectors (one entry per primal component that is box-constrained): - -```@example main -dim_dual_state_constraints_box(sol) # dimension of state box constraint duals -``` - -```@example main -dim_dual_control_constraints_box(sol) # dimension of control box constraint duals -``` - -```@example main -dim_dual_variable_constraints_box(sol) # dimension of variable box constraint duals -``` - -### Nonlinear constraint duals - -Get dual variables for nonlinear path and boundary constraints: - -```@example main -path_constraints_dual(sol) # duals for nonlinear path constraints -``` - -```@example main -boundary_constraints_dual(sol) # duals for nonlinear boundary constraints -``` - -Get the dimensions of the nonlinear constraints: - -```@example main -dim_path_constraints_nl(sol) # number of nonlinear path constraints -``` - -```@example main -dim_boundary_constraints_nl(sol) # number of nonlinear boundary constraints -``` - -### Summary table - -| Method | Returns | Description | -| -------- | --------- | ------------- | -| `dual(sol, ocp, label)` | `Real` or `Function` | Signed dual for labeled constraint | -| `state_constraints_lb_dual(sol)` | Dual values | State lower bound duals (non-negative) | -| `state_constraints_ub_dual(sol)` | Dual values | State upper bound duals (non-negative) | -| `control_constraints_lb_dual(sol)` | Dual values | Control lower bound duals (non-negative) | -| `control_constraints_ub_dual(sol)` | Dual values | Control upper bound duals (non-negative) | -| `variable_constraints_lb_dual(sol)` | Dual values | Variable lower bound duals (non-negative) | -| `variable_constraints_ub_dual(sol)` | Dual values | Variable upper bound duals (non-negative) | -| `dim_dual_state_constraints_box(sol)` | `Int` | Dimension of state box constraint duals | -| `dim_dual_control_constraints_box(sol)` | `Int` | Dimension of control box constraint duals | -| `dim_dual_variable_constraints_box(sol)` | `Int` | Dimension of variable box constraint duals | -| `path_constraints_dual(sol)` | Dual values | Nonlinear path constraint duals (signed) | -| `boundary_constraints_dual(sol)` | Dual values | Nonlinear boundary constraint duals (signed) | -| `dim_path_constraints_nl(sol)` | `Int` | Number of nonlinear path constraints | -| `dim_boundary_constraints_nl(sol)` | `Int` | Number of nonlinear boundary constraints | - -## Solution metadata - -The solution metadata provides information about the solver performance and status. - -### Solver status - -Check if the solution was successful: - -```@example main -successful(sol) # returns true if solver succeeded -``` - -Get the solver status symbol: - -```@example main -status(sol) # returns solver status (e.g., :first_order) -``` - -Get the solver message: - -```@example main -message(sol) # returns solver message string -``` - -### Iteration count - -Get the number of solver iterations: - -```@example main -iterations(sol) # returns iteration count -``` - -### Constraints violation - -Get the maximum constraint violation: - -```@example main -constraints_violation(sol) # returns max violation -``` - -### Additional solver information - -Get additional solver-specific information: - -```@example main -infos(sol) # returns dictionary of solver info -``` - -### [Summary table](@id manual-solution-summary-solver) - -| Method | Returns | Description | -| -------- | --------- | ------------- | -| `successful(sol)` | `Bool` | True if solver succeeded | -| `status(sol)` | `Symbol` | Solver status | -| `message(sol)` | `String` | Solver message | -| `iterations(sol)` | `Int` | Number of iterations | -| `constraints_violation(sol)` | `Float64` | Maximum constraint violation | -| `infos(sol)` | `Dict` | Additional solver information | diff --git a/docs/attic/manual-solve-advanced.md b/docs/attic/manual-solve-advanced.md deleted file mode 100644 index 7f45e8523..000000000 --- a/docs/attic/manual-solve-advanced.md +++ /dev/null @@ -1,208 +0,0 @@ -# [Solve: advanced options](@id manual-solve-advanced) - -This manual covers advanced option management for the [`solve`](@ref) function: how option routing works, how to disambiguate shared options with `route_to`, how to pass unknown options with `bypass`, and how to use introspection tools. - -For basic usage, see [Solve a problem](@ref manual-solve). - -## Option routing system - -When you call `solve` with keyword arguments, OptimalControl.jl automatically routes each option to the appropriate strategy (discretizer, modeler, or solver). - -```@example advanced -using OptimalControl -using NLPModelsIpopt - -t0 = 0 -tf = 1 -x0 = [-1, 0] - -ocp = @def begin - t ∈ [ t0, tf ], time - x = (q, v) ∈ R², state - u ∈ R, control - x(t0) == x0 - x(tf) == [0, 0] - ẋ(t) == [v(t), u(t)] - 0.5∫( u(t)^2 ) → min -end - -# Options are automatically routed -sol = solve(ocp; - grid_size=100, # → Collocation (discretizer) - show_time=true, # → ADNLP (modeler) - max_iter=500, # → Ipopt (solver) - print_level=0 # → Ipopt (solver) -) -nothing # hide -``` - -### How routing works - -Each strategy declares its available options via metadata. When you pass an option: - -1. **Lookup**: The system checks which strategies recognize this option name -2. **Route**: If exactly one strategy family (discretizer/modeler/solver) recognizes it, the option is routed there -3. **Validate**: The option value is validated against the declared type and constraints -4. **Error**: If no strategy recognizes the option, or if multiple families claim it, an error is raised - -You can inspect a strategy's declared options using `describe`: - -```@example advanced -using CUDA -describe(:exa) -``` - -The output shows: - -- **Strategy ID**: The symbol used to reference this strategy (`:exa`) -- **Family**: The abstract type family (`AbstractNLPModeler`) -- **Default parameter**: Default execution backend (`CPU`) -- **Parameters**: Available execution backends (`CPU`, `GPU`) -- **Common options**: Options shared across all parameters - - Option name and type - - Default value - - Description -- **Computed options**: Options that vary by parameter - - Parameter-specific defaults - - Whether the value is computed automatically - -## Ambiguous options and `route_to` - -Ambiguity occurs when an option name exists in multiple strategies **within the same method**. Since a method always has exactly one discretizer, one modeler, and one solver, ambiguity only happens when strategies from different families share an option name. - -For example, suppose `:exa` (modeler) and `:madnlp` (solver) both have an option called `common_option_name`. If you try to use it without disambiguation, you'll get an error: - -```julia -# This will raise an error -solve(ocp, :exa, :madnlp; common_option_name=12) -# ERROR: IncorrectArgument: Option 'common_option_name' is ambiguous... -``` - -### Using `route_to` for disambiguation - -Use `route_to` to explicitly specify which **strategy** should receive the option: - -```julia -# Explicitly route to the :exa strategy -sol = solve(ocp, :exa, :madnlp; - common_option_name=route_to(:exa, 12), - max_iter=500, - print_level=MadNLP.ERROR -) -``` - -The `route_to` function accepts keyword arguments with **strategy names**: - -- `route_to(:collocation, value)` — route to the Collocation discretizer -- `route_to(:adnlp, value)` — route to the ADNLP modeler -- `route_to(:exa, value)` — route to the Exa modeler -- `route_to(:ipopt, value)` — route to the Ipopt solver -- `route_to(:madnlp, value)` — route to the MadNLP solver -- `route_to(:uno, value)` — route to the Uno solver -- `route_to(:madncl, value)` — route to the MadNCL solver -- `route_to(:knitro, value)` — route to the Knitro solver - -### Routing the same option to multiple strategies - -You can also route the same option name to multiple strategies with different values by passing alternating strategy-value pairs: - -```julia -# Route the same option to multiple strategies with different values -sol = solve(ocp, :exa, :madnlp; - common_option_name=route_to(:exa, 12, :madnlp, true), - max_iter=500 -) -``` - -The syntax accepts multiple strategy-value pairs: - -```julia -route_to(strategy_id_1, val_1, strategy_id_2, val_2, ...) -``` - -This is useful when: - -- Different strategies use the same option name for different purposes -- You want to configure the same option differently across strategies -- You need fine-grained control over option routing - -You can use `route_to` even for non-ambiguous options, and combine routed and non-routed options: - -```@example advanced -using MadNLP -sol = solve(ocp, :madnlp; - grid_size=50, # auto-routed to discretizer - max_iter=route_to(:madnlp, 1000), # explicitly routed to solver - print_level=MadNLP.ERROR # auto-routed to solver -) -nothing # hide -``` - -## The `bypass` mechanism - -By default, `solve` uses **strict validation**: any option not recognized by a registered strategy raises an error. This prevents typos and ensures you're using valid options. - -However, NLP solvers have many options, and not all of them are declared in OptimalControl's strategy metadata. For example, Ipopt has an option `mumps_print_level` for controlling MUMPS debug output: - -> `mumps_print_level`: Debug printing level for the linear solver MUMPS -> -> 0: no printing; 1: Error messages only; 2: Error, warning, and main statistic messages; 3: Error and warning messages and terse diagnostics; ≥4: All information. - -This option is not in the Ipopt strategy metadata. If you try to use it directly, you'll get an error: - -```@repl advanced -sol = solve(ocp, :ipopt; - max_iter=100, - mumps_print_level=1) -``` - -To pass undeclared options, combine `route_to` with `bypass`: - -```@repl advanced -sol = solve(ocp, :ipopt; - max_iter=100, - mumps_print_level=route_to(:ipopt, bypass(1))) -``` - -You **must** combine `bypass` with `route_to` because: - -- If the option is unknown, the system needs to know which strategy should receive it -- `bypass` forces the option through without validation - -!!! note "Alias: force = bypass" - You can use `force` as an alias for `bypass`: `route_to(:ipopt, force(1))` - -!!! warning "Use bypass sparingly" - - The `bypass` mechanism skips validation entirely. Use it only when: - - - You need to pass an option to the underlying solver that isn't declared in the strategy metadata - - You're certain the option name and value are correct - - Bypassed options are passed directly to the solver without type checking or validation. - -## Parameter token (CPU/GPU) - -The 4th token in a method description specifies the execution backend: `:cpu` (default) or `:gpu`. - -```@example advanced -# Explicitly request CPU execution (this is the default) -sol = solve(ocp, :collocation, :adnlp, :ipopt, :cpu; - grid_size=50, - print_level=0 -) -nothing # hide -``` - -The parameter token automatically changes default options for GPU-capable strategies. For example: - -- `Exa{GPU}` uses a CUDA backend by default -- `MadNLP{GPU}` uses `CUDSSSolver` as the linear solver by default - -For full GPU usage details, see [Solve on GPU](@ref manual-solve-gpu). - -## See also - -- **[Basic solve](@ref manual-solve)**: descriptive mode basics -- **[Explicit mode](@ref manual-solve-explicit)**: using typed components -- **[GPU solving](@ref manual-solve-gpu)**: GPU parameter and types diff --git a/docs/attic/manual-solve-explicit.md b/docs/attic/manual-solve-explicit.md deleted file mode 100644 index 18312d9b2..000000000 --- a/docs/attic/manual-solve-explicit.md +++ /dev/null @@ -1,282 +0,0 @@ -# [Solve: explicit mode](@id manual-solve-explicit) - -This manual explains the **explicit mode** of the [`solve`](@ref) function, where you pass typed strategy instances directly instead of symbolic tokens. This gives you full control over component configuration and validation. - -For basic usage with symbolic tokens, see [Solve a problem](@ref manual-solve). - -## Overview - -In explicit mode, you create strategy instances with their options, then pass them to `solve`: - -```@example explicit -using OptimalControl -using NLPModelsIpopt - -t0 = 0 -tf = 1 -x0 = [-1, 0] - -ocp = @def begin - t ∈ [ t0, tf ], time - x = (q, v) ∈ R², state - u ∈ R, control - x(t0) == x0 - x(tf) == [0, 0] - ẋ(t) == [v(t), u(t)] - 0.5∫( u(t)^2 ) → min -end - -# Create strategy instances -disc = OptimalControl.Collocation(grid_size=100, scheme=:trapeze) -mod = OptimalControl.ADNLP(backend=:optimized) -sol = OptimalControl.Ipopt(max_iter=1000, print_level=0) - -# Solve with explicit components -result = solve(ocp; discretizer=disc, modeler=mod, solver=sol) -nothing # hide -``` - -The mode is **automatically detected**: if any of `discretizer`, `modeler`, or `solver` keywords contain a typed component (not a symbol), explicit mode is used. - -## Basic usage - -### Creating strategy instances - -Each strategy is constructed with its options as keyword arguments. First, load the required solver packages: - -```julia -# Load solver packages (only what you need) -using NLPModelsIpopt # for Ipopt -using MadNLP # for MadNLP -using UnoSolver # for Uno -using MadNCL # for MadNCL (also requires MadNLP) -using NLPModelsKnitro # for Knitro (commercial license required) -# GPU solving also requires: using CUDA and using MadNLPGPU -``` - -```@example explicit -# Discretizer with custom grid and scheme -disc = OptimalControl.Collocation(grid_size=50, scheme=:midpoint) - -# Modeler with specific backend -mod = OptimalControl.ADNLP(backend=:optimized, show_time=false) - -# Solver with iteration limit and tolerance -sol = OptimalControl.Ipopt(max_iter=500, tol=1e-6, print_level=0) -nothing # hide -``` - -### Passing to solve - -Use the `discretizer`, `modeler`, and `solver` keyword arguments: - -```@example explicit -result = solve(ocp; - discretizer=disc, - modeler=mod, - solver=sol, - display=false -) -nothing # hide -``` - -## Partial components - -You don't need to specify all three components. Missing ones are auto-completed using the default strategy registry, following the **same priority order** as in descriptive mode (see `methods()`): - -```@example explicit -# Only specify the solver -result = solve(ocp; - solver=OptimalControl.Ipopt(max_iter=2000, print_level=0), - display=false -) -nothing # hide -``` - -The completion algorithm searches `methods()` from top to bottom to find the first matching quadruplet, then builds the missing components with their default options. In this case: - -- `discretizer` defaults to `Collocation()` (first discretizer in `methods()`) -- `modeler` defaults to `ADNLP()` (first modeler compatible with `Ipopt`) -- `solver` uses your custom `Ipopt` instance - -!!! note "Priority order matters" - - Just like in descriptive mode, the order in `methods()` determines which defaults are used. For example: - - - `solve(ocp; solver=Ipopt())` → uses `ADNLP()` (first modeler compatible with Ipopt) - - `solve(ocp; modeler=Exa())` → uses `Ipopt()` (first solver in the list) - - `solve(ocp; discretizer=Collocation())` → uses `ADNLP()` and `Ipopt()` (first matching pair) - -You can mix and match: - -```@example explicit -# Custom discretizer and solver, default modeler -result = solve(ocp; - discretizer=OptimalControl.Collocation(grid_size=200, scheme=:trapeze), - solver=OptimalControl.Ipopt(max_iter=100, print_level=0), - display=false -) -nothing # hide -``` - -## Component options - -### Options at construction - -All options are passed when creating the strategy instance: - -```@example explicit -# Configure Collocation -disc = OptimalControl.Collocation( - grid_size=150, - scheme=:gauss_legendre_2 -) - -# Configure ADNLP -mod = OptimalControl.ADNLP( - backend=:optimized, - show_time=true -) - -# Configure Ipopt -sol = OptimalControl.Ipopt( - max_iter=1000, - tol=1e-8, - print_level=5, - acceptable_tol=1e-6 -) -nothing # hide -``` - -### Passing undeclared options - -By default, strategies use **strict validation**: any option not declared in the strategy metadata raises an error. This prevents typos and ensures you're using valid options. - -However, NLP solvers have many options, and not all of them are declared in OptimalControl's strategy metadata. For example, Ipopt has an option `mumps_print_level` for controlling MUMPS debug output, which is not in the Ipopt strategy metadata. - -To pass undeclared options, use `bypass()` (or its alias `force()`): - -```@example explicit -# Bypass validation for mumps_print_level -solver = OptimalControl.Ipopt( - max_iter=500, - print_level=0, - mumps_print_level=bypass(1) # Undeclared option -) -nothing # hide -``` - -!!! note "Alias: force = bypass" - You can use `force` as an alias for `bypass`: `mumps_print_level=force(1)` - -!!! warning "Use bypass sparingly" - - The `bypass` mechanism skips validation for the wrapped option. Use it only when: - - - You need to pass an option to the underlying solver that isn't declared in the strategy metadata - - You're certain the option name and value are correct - - Bypassed options are passed directly to the solver without type checking or validation. - -!!! info "Alternative: permissive mode" - - If you have many undeclared options, you can use `mode=:permissive` to disable validation globally. However, this is not recommended as it will also ignore typos in valid option names. - -### Strategy options must be configured at construction - -In explicit mode, options specific to a strategy must be passed when constructing that strategy. They are not routed from the `solve` call. - -The following syntax is **invalid**: - -```julia -disc = OptimalControl.Collocation() -solve(ocp; disc, backend=:generic) -``` - -Because `disc` is a typed component, this call automatically selects explicit mode. `backend` is not an option of the `solve` action; it is an option of a modeler or solver strategy. The error does not mean that `backend` was lost or not transmitted. It means that an explicit component was combined with descriptive-style option syntax. - -Configure the modeler first, then pass the configured strategy to `solve`: - -```julia -disc = OptimalControl.Collocation() -modeler = OptimalControl.ADNLP(backend=:generic) -result = solve(ocp; discretizer=disc, modeler=modeler) -``` - -If an option is declared by one of the completed strategies, the error identifies that strategy and suggests constructing it with the option. If the option is not declared by any strategy, the error reports an unknown option and lists the options accepted directly by `solve`, such as `initial_guess`, `init`, and `display`. - -`route_to` is only for descriptive mode. Likewise, `bypass` and `force` do not bypass explicit-mode validation. They can be used for undeclared options while constructing a strategy: - -```julia -modeler = OptimalControl.ADNLP(custom_option=bypass(value)) -solve(ocp; modeler=modeler) -``` - -### No routing needed - -In explicit mode, there is no automatic option routing: - -- Strategy options are passed to strategy constructors -- `solve` accepts action options and typed component instances -- `route_to` is only relevant to descriptive mode - -```@example explicit -# Each component gets its own options directly -disc = OptimalControl.Collocation(grid_size=100) -sol = OptimalControl.Ipopt(max_iter=500, tol=1e-6, print_level=0) - -result = solve(ocp; discretizer=disc, solver=sol, display=false) -nothing # hide -``` - -## Mixing modes is forbidden - -You **cannot** mix symbolic tokens and typed components in the same `solve` call: - -```julia -# ERROR: Cannot mix descriptive and explicit modes -solve(ocp, :adnlp, :ipopt; discretizer=OptimalControl.Collocation()) - -# ERROR: Cannot mix modes -solve(ocp, :collocation; solver=OptimalControl.Ipopt()) -``` - -Choose one mode: - -- **Descriptive**: `solve(ocp, :collocation, :adnlp, :ipopt; options...)` -- **Explicit**: `solve(ocp; discretizer=..., modeler=..., solver=...)` - -## Inspecting components - -Use the introspection tools to examine configured components: - -```@example explicit -# Create a configured solver -solver = OptimalControl.Ipopt(max_iter=1000, tol=1e-6, print_level=0) - -# Get its options -opts = options(solver) - -# Check which options are user-set vs defaults -is_user(opts, :max_iter) # true -``` - -```@example explicit -is_default(opts, :mu_strategy) # true (not set by user) -``` - -```@example explicit -# Get option values -opts[:max_iter] -``` - -```@example explicit -# See all option names -keys(opts) -``` - -## See also - -- **[Basic solve (descriptive)](@ref manual-solve)**: symbolic token mode -- **[Advanced options](@ref manual-solve-advanced)**: `route_to`, `bypass`, introspection -- **[GPU solving](@ref manual-solve-gpu)**: `Exa{GPU}()` and `MadNLP{GPU}()` types diff --git a/docs/attic/manual-solve-gpu.md b/docs/attic/manual-solve-gpu.md deleted file mode 100644 index a4b1687c4..000000000 --- a/docs/attic/manual-solve-gpu.md +++ /dev/null @@ -1,175 +0,0 @@ -# [Solve on GPU](@id manual-solve-gpu) - -This manual explains how to solve optimal control problems on GPU using the [`solve`](@ref) function. GPU acceleration is available through [ExaModels.jl](https://exanauts.github.io/ExaModels.jl/stable) and [MadNLPGPU.jl](https://github.com/MadNLP/MadNLP.jl), with current support for NVIDIA GPUs via [CUDA.jl](https://github.com/JuliaGPU/CUDA.jl). - -For basic CPU solving, see [Solve a problem](@ref manual-solve). - -## Prerequisites - -You need to load the GPU-capable packages: - -```@setup gpu -using OptimalControl -using MadNLPGPU -using CUDA -``` - -```julia -using OptimalControl -using MadNLPGPU -using CUDA -``` - -!!! note "Solver requirements" - - For complete solver requirements including CPU solvers, see [Solver requirements](@ref manual-solve-solver-requirements) in the main solving manual. - -!!! warning "CUDA required" - - GPU solving requires a CUDA-capable GPU and properly configured CUDA drivers. Check `CUDA.functional()` to verify your setup. - -## Problem definition - -Consider the following optimal control problem. - -```@example gpu -ocp = @def begin - t ∈ [0, 1], time - x ∈ R², state - u ∈ R, control - v ∈ R, variable - x(0) == [0, 1] - x(1) == [0, -1] - ∂(x₁)(t) == x₂(t) # Coordinate-by-coordinate - ∂(x₂)(t) == u(t) # (not ẋ(t) == [x₂(t), u(t)]) - 0 ≤ x₁(t) + v^2 ≤ 1.1 - -10 ≤ u(t) ≤ 10 - 1 ≤ v ≤ 2 - ∫(u(t)^2 + v) → min -end -nothing # hide -``` - -## Descriptive mode with `:gpu` token - -The simplest way to solve on GPU is using the `:gpu` parameter token: - -```julia -sol = solve(ocp, :exa, :madnlp, :gpu; grid_size=100, print_level=MadNLP.ERROR) -``` - -Or with partial description (auto-completes to `:collocation, :exa, :madnlp, :gpu`): - -```julia -sol = solve(ocp, :gpu; grid_size=100, print_level=MadNLP.ERROR) -``` - -### What the `:gpu` token does - -The `:gpu` parameter automatically selects GPU-optimized defaults: - -- **Exa modeler**: CUDA backend + GPU-optimized automatic differentiation -- **MadNLP solver**: `CUDSSSolver` linear solver (instead of `MumpsSolver`) - -You can inspect which strategies support the GPU parameter: - -```@example gpu -describe(:gpu) -``` - -This shows that only `:exa`, `:madnlp`, and `:madncl` strategies have GPU-parameterized versions. - -The GPU parameter changes default options: - -```@example gpu -# GPU defaults -modeler = OptimalControl.Exa{GPU}() -solver = OptimalControl.MadNLP{GPU}() -println("Exa{GPU} backend: ", options(modeler)[:backend]) -println("MadNLP{GPU} linear_solver: ", options(solver)[:linear_solver]) -``` - -```@example gpu -# CPU defaults (default parameter) -modeler = OptimalControl.Exa() # equivalent to OptimalControl.Exa{CPU}() -solver = OptimalControl.MadNLP() # equivalent to OptimalControl.MadNLP{CPU}() -println("Exa{CPU} backend: ", options(modeler)[:backend]) -println("MadNLP{CPU} linear_solver: ", options(solver)[:linear_solver]) -``` - -## Explicit mode with parameterized types - -For full control, use explicit mode with GPU-parameterized types: - -```julia -disc = OptimalControl.Collocation(grid_size=100, scheme=:midpoint) -mod = OptimalControl.Exa{GPU}() -sol = OptimalControl.MadNLP{GPU}(print_level=MadNLP.ERROR) - -result = solve(ocp; discretizer=disc, modeler=mod, solver=sol) -``` - -This gives you: - -- Explicit type annotations (`Exa{GPU}`, `MadNLP{GPU}`) -- Full control over each component's options -- Type safety at compile time - -## Supported GPU combinations - -Only specific strategy combinations support GPU execution: - -**✅ Supported:** - -- `:collocation` + `:exa` + `:madnlp` + `:gpu` -- `:collocation` + `:exa` + `:madncl` + `:gpu` - -**❌ Not supported:** - -```julia -# ERROR: ADNLP doesn't support GPU -solve(ocp, :adnlp, :madnlp, :gpu) - -# ERROR: Ipopt doesn't support GPU -solve(ocp, :exa, :ipopt, :gpu) -``` - -In explicit mode: - -```julia -# ERROR: ADNLP{GPU} is not defined -mod = OptimalControl.ADNLP{GPU}() - -# ERROR: Ipopt{GPU} is not defined -sol = OptimalControl.Ipopt{GPU}() -``` - -## Performance considerations - -GPU solving is beneficial for: - -- **Large-scale problems**: Thousands of variables and constraints -- **Dense computations**: Problems with many nonlinear constraints -- **Repeated solves**: Amortize GPU initialization overhead - -For small problems, CPU solving may be faster due to GPU overhead. - -### Checking CUDA availability - -```julia -if CUDA.functional() - println("CUDA is available") - sol = solve(ocp, :gpu) -else - println("CUDA not available, using CPU") - sol = solve(ocp, :cpu) -end -``` - -## See also - -- **[Basic solve](@ref manual-solve)**: CPU solving with descriptive mode -- **[Explicit mode](@ref manual-solve-explicit)**: typed components -- **[Advanced options](@ref manual-solve-advanced)**: option routing and introspection -- **[ExaModels.jl](https://exanauts.github.io/ExaModels.jl/stable)**: GPU-capable NLP modeler -- **[MadNLPGPU.jl](https://github.com/MadNLP/MadNLP.jl)**: GPU-accelerated NLP solver diff --git a/docs/attic/manual-solve.md b/docs/attic/manual-solve.md deleted file mode 100644 index 742686ea2..000000000 --- a/docs/attic/manual-solve.md +++ /dev/null @@ -1,304 +0,0 @@ -# [Solve a problem](@id manual-solve) - -This manual explains how to use the [`solve`](@ref) function to solve optimal control problems with OptimalControl.jl. The `solve` function provides a **descriptive mode** where you specify strategies using symbolic tokens, with automatic option routing and validation. - -For advanced usage, see: - -- [Advanced options and disambiguation](@ref manual-solve-advanced) -- [Explicit mode with typed components](@ref manual-solve-explicit) -- [GPU solving](@ref manual-solve-gpu) - -## Quick start - -Let us define a basic optimal control problem: - -```@example main -using OptimalControl - -t0 = 0 -tf = 1 -x0 = [-1, 0] - -ocp = @def begin - t ∈ [ t0, tf ], time - x = (q, v) ∈ R², state - u ∈ R, control - x(t0) == x0 - x(tf) == [0, 0] - ẋ(t) == [v(t), u(t)] - 0.5∫( u(t)^2 ) → min -end -nothing # hide -``` - -The simplest way to solve it is: - -```@example main -using NLPModelsIpopt -sol = solve(ocp) -nothing # hide -``` - -This uses default strategies: collocation discretization, ADNLP modeler, and Ipopt solver, all running on CPU. - -!!! warning "Solver extension required" - - You must load a solver package (e.g., `using NLPModelsIpopt`) before calling `solve`. Otherwise, you'll get: - - ```julia - julia> solve(ocp) - ERROR: ExtensionError. Please make: julia> using NLPModelsIpopt - ``` - -## Display - -Control the configuration display with the `display` option: - -```@example main -# Suppress all output -sol = solve(ocp; display=false) -nothing # hide -``` - -## Initial guess - -Provide an initial guess using `initial_guess` (or the alias `init`): - -```@example main -# Using the @init macro -init = @init ocp begin - u = 0.5 -end - -sol = solve(ocp; initial_guess=init, grid_size=50, display=false) -nothing # hide -``` - -```@example main -# Or using the alias -sol = solve(ocp; init=init, grid_size=50, display=false) -nothing # hide -``` - -For more details on initial guess specification, see [Set an initial guess](@ref manual-initial-guess). - -## Available methods - -OptimalControl.jl provides multiple solving strategies. To see all available combinations, call: - -```@example main -methods() -``` - -Each method is a **quadruplet** `(discretizer, modeler, solver, parameter)`: - -1. **Discretizer** — how to discretize the continuous OCP: - - `:collocation`: collocation method (currently the only option) - -2. **Modeler** — how to build the NLP model: - - `:adnlp`: uses [`ADNLPModels.ADNLPModel`](@extref) with automatic differentiation - - `:exa`: uses [`ExaModels.ExaModel`](@extref) with SIMD optimization (GPU-capable). **Only compatible with problems built via [`@def`](@ref manual-abstract-syntax)**, not with the [functional API](@ref manual-macro-free) - -3. **Solver** — which NLP solver to use: - - `:ipopt`: [Ipopt](https://coin-or.github.io/Ipopt/) interior point solver (CPU-only) - - `:madnlp`: [MadNLP](https://madnlp.github.io/MadNLP.jl/) pure-Julia solver (GPU-capable) - - `:uno`: [Uno](https://unosolver.readthedocs.io) unified nonlinear optimization solver (CPU-only) - - `:madncl`: [MadNCL](https://github.com/MadNLP/MadNCL.jl) (GPU-capable) - - `:knitro`: [Knitro](https://www.artelys.com/solvers/knitro/) commercial solver (license required) - -4. **Parameter** — execution backend: - - `:cpu`: CPU execution (default) - - `:gpu`: GPU execution (only for `:exa` modeler with `:madnlp` or `:madncl` solvers) - -You can inspect which strategies use a given parameter: - -```@example main -describe(:cpu) -``` - -```@example main -describe(:gpu) -``` - -!!! note "Priority order" - - The order of methods in the list above determines the **priority** for auto-completion. When you provide a partial description, the first matching method from top to bottom is selected. This is why the first method `(:collocation, :adnlp, :ipopt, :cpu)` is the default. - -The first method in the list is the default, so: - -```julia -solve(ocp) -``` - -is equivalent to: - -```julia -solve(ocp, :collocation, :adnlp, :ipopt, :cpu) -``` - -## Choosing a method - -You can specify a complete method description: - -```@example main -using MadNLP -sol = solve(ocp, :collocation, :adnlp, :madnlp, :cpu) -nothing # hide -``` - -Or provide a **partial description**. Missing tokens are auto-completed using the **first matching method** from `methods()` (top-to-bottom priority): - -```@example main -# Only specify the solver → defaults to :collocation, :adnlp, :cpu -sol = solve(ocp, :madnlp; print_level=MadNLP.ERROR) -nothing # hide -``` - -The completion algorithm searches `methods()` from top to bottom and selects the first quadruplet that matches all provided tokens. For example: - -- `solve(ocp, :madnlp)` matches `(:collocation, :adnlp, :madnlp, :cpu)` (first match with `:madnlp`) -- `solve(ocp, :exa)` matches `(:collocation, :exa, :ipopt, :cpu)` (first match with `:exa`) -- `solve(ocp, :gpu)` matches `(:collocation, :exa, :madnlp, :gpu)` (first GPU method) - -All of these are equivalent (they all complete to `:collocation, :adnlp, :ipopt, :cpu`): - -```julia -solve(ocp) # empty → use first method -solve(ocp, :collocation) # specify discretizer -solve(ocp, :adnlp) # specify modeler -solve(ocp, :ipopt) # specify solver -solve(ocp, :cpu) # specify parameter -solve(ocp, :collocation, :adnlp) # specify discretizer + modeler -solve(ocp, :collocation, :ipopt) # specify discretizer + solver -solve(ocp, :collocation, :adnlp, :ipopt, :cpu) # complete description -``` - -## [Solver requirements](@id manual-solve-solver-requirements) - -Each solver requires its package to be loaded to provide the solver implementation: - -- **Ipopt**: `using NLPModelsIpopt` -- **MadNLP**: `using MadNLP` (CPU) or `using MadNLPGPU` (GPU) -- **Uno**: `using UnoSolver` -- **MadNCL**: `using MadNCL` and `using MadNLP` (requires both) -- **Knitro**: `using NLPModelsKnitro` (commercial license required) - -For GPU solving with MadNLP or MadNCL, you also need: `using CUDA` - -## Passing options to strategies - -You can pass options as keyword arguments. They are **automatically routed** to the appropriate strategy: - -```@example main -sol = solve(ocp, :madnlp; - grid_size=100, # → discretizer (Collocation) - max_iter=500, # → solver (MadNLP) - print_level=MadNLP.ERROR # → solver (MadNLP) -) -nothing # hide -``` - -The solve function displays the configuration and shows which options were applied: - -```@example main -sol = solve(ocp, :ipopt; - grid_size=50, - scheme=:trapeze, - max_iter=100, - print_level=0 -) -nothing # hide -``` - -Notice the `📦 Configuration` box showing: - -- **Discretizer**: `collocation` with `grid_size = 50, scheme = trapeze` -- **Modeler**: `adnlp` (no custom options) -- **Solver**: `ipopt` with `max_iter = 100, print_level = 0` - -## [Strategy options](@id manual-solve-strategy-options) - -Each strategy declares its available options. You can inspect them using `describe`. - -!!! note "Understanding default values" - When `describe` shows `(default: NotProvided)` for an option, it means OptimalControl does not override the strategy's native default value. For example: - - For Ipopt options with `(default: NotProvided)`, Ipopt's own default values are used - - For MadNLP options with `(default: NotProvided)`, MadNLP's own default values are used - - For other strategies, the same principle applies - - Only options with explicit default values (e.g., `(default: 100)`) are overridden by OptimalControl. - -### Discretizer options - -The collocation discretizer supports multiple integration schemes: - -- `:trapeze` - Trapezoidal rule (second-order accurate) -- `:midpoint` - Midpoint rule (second-order accurate) -- `:euler` or `:euler_explicit` or `:euler_forward` - Explicit Euler method (first-order accurate) -- `:euler_implicit` or `:euler_backward` - Implicit Euler method (first-order accurate, more stable for stiff problems) - -!!! note "Additional schemes with ADNLP modeler" - - When using the `:adnlp` modeler, two additional high-order collocation schemes are available: - - - `:gauss_legendre_2` - 2-point Gauss-Legendre collocation (fourth-order accurate) - - `:gauss_legendre_3` - 3-point Gauss-Legendre collocation (sixth-order accurate) - - These schemes provide higher accuracy but require more computational effort. - -```@example main -describe(:collocation) -``` - -### Modeler options - -```@example main -describe(:adnlp) -``` - -```@example main -using CUDA -describe(:exa) -``` - -### Solver options - -```@example main -using NLPModelsIpopt -describe(:ipopt) -``` - -```@example main -using MadNLPGPU -describe(:madnlp) -``` - -```@example main -using MadNCL -describe(:madncl) -``` - -```@example main -using UnoSolver -describe(:uno) -``` - -### Official documentation - -For complete option lists, see the official documentation: - -- **ADNLP**: [ADNLPModels documentation](https://jso.dev/ADNLPModels.jl/stable/) -- **Exa**: [ExaModels documentation](https://exanauts.github.io/ExaModels.jl/stable/) -- **Ipopt**: [Ipopt options](https://coin-or.github.io/Ipopt/OPTIONS.html) -- **MadNLP**: [MadNLP options](https://madnlp.github.io/MadNLP.jl/stable/options/) -- **Uno**: [Uno documentation](https://unosolver.readthedocs.io) -- **MadNCL**: [MadNCL documentation](https://github.com/MadNLP/MadNCL.jl) -- **Knitro**: [Knitro options](https://www.artelys.com/docs/knitro/3_referenceManual/userOptions.html) - -## See also - -- **[Advanced options](@ref manual-solve-advanced)**: option routing, `route_to` for disambiguation, `bypass` for unknown options, introspection tools -- **[Explicit mode](@ref manual-solve-explicit)**: using typed components (`Collocation()`, `Ipopt()`) instead of symbols -- **[GPU solving](@ref manual-solve-gpu)**: using the `:gpu` parameter or `Exa{GPU}()` / `MadNLP{GPU}()` types -- **[Initial guess](@ref manual-initial-guess)**: detailed guide on the `@init` macro -- **[Solution](@ref manual-solution)**: working with the returned solution object diff --git a/docs/attic/public.md b/docs/attic/public.md deleted file mode 100644 index 4681c5161..000000000 --- a/docs/attic/public.md +++ /dev/null @@ -1,160 +0,0 @@ -# OptimalControl.jl - -```@meta -CollapsedDocStrings = false -``` - -[OptimalControl.jl](https://github.com/control-toolbox/OptimalControl.jl) is the core package of the [control-toolbox ecosystem](https://github.com/control-toolbox). Below, we group together the documentation of all the functions and types exported by OptimalControl. - -!!! tip "Beware!" - - Even if the following functions are prefixed by another package, such as `CTFlows.Lift`, they can all be used with OptimalControl. In fact, all functions prefixed with another package are simply reexported. For example, `Lift` is defined in CTFlows but accessible from OptimalControl. - - ```julia-repl - julia> using OptimalControl - julia> F(x) = 2x - julia> H = Lift(F) - julia> x = 1 - julia> p = 2 - julia> H(x, p) - 4 - ``` - -## Exported functions and types - -```@autodocs -Modules = [OptimalControl] -Order = [:module] -Private = false -``` - -## Documentation - -```@docs; canonical=true -*(::CTFlowsODE.AbstractFlow) -Flow -@Lie -Lie -Lift -Poisson -boundary_constraints_dual -boundary_constraints_nl -bypass -components -CTModels.OCP.constraint -constraints -constraints_violation -control -control_components -control_constraints_box -control_constraints_lb_dual -control_constraints_ub_dual -control_dimension -control_name -costate -criterion -@def -definition -expression -describe -dim_boundary_constraints_nl -dim_control_constraints_box -dim_dual_control_constraints_box -dim_dual_state_constraints_box -dim_dual_variable_constraints_box -dim_path_constraints_nl -dim_state_constraints_box -dim_variable_constraints_box -dimension -CTDirect.discretize -dual -dynamics -export_ocp_solution -final_time -final_time_name -get_build_examodel -has_fixed_final_time -has_fixed_initial_time -has_free_final_time -has_free_initial_time -has_lagrange_cost -has_mayer_cost -has_option -has_variable -is_variable -has_control -is_control_free -has_abstract_definition -id -import_ocp_solution -index -infos -@init -initial_time -initial_time_name -is_autonomous -is_computed -is_default -is_abstractly_defined -is_nonautonomous -is_nonvariable -is_empty -is_empty_time_grid -is_final_time_fixed -is_final_time_free -is_initial_time_fixed -is_initial_time_free -is_lagrange_cost_defined -is_mayer_cost_defined -is_user -iterations -lagrange -mayer -message -metadata -methods() -model -name -nlp_model -CTModels.OCP.objective -ocp_model -ocp_solution -option_default -option_defaults -option_description -option_names -option_source -option_type -option_value -options -path_constraints_dual -path_constraints_nl -plot -plot! -route_to -solve(::CTSolvers.Optimization.AbstractOptimizationProblem, ::Any, ::CTSolvers.Modelers.AbstractNLPModeler, ::CTSolvers.Solvers.AbstractNLPSolver) -solve(::CTModels.OCP.AbstractModel, ::Symbol...) -solve(::CTModels.OCP.AbstractModel, ::CTModels.Init.AbstractInitialGuess, ::CTDirect.AbstractDiscretizer, ::CTSolvers.Modelers.AbstractNLPModeler, ::CTSolvers.Solvers.AbstractNLPSolver) -state -state_components -state_constraints_box -state_constraints_lb_dual -state_constraints_ub_dual -state_dimension -state_name -status -success -successful -time -time_grid -time_name -times -CTModels.OCP.variable -variable_components -variable_constraints_box -variable_constraints_lb_dual -variable_constraints_ub_dual -variable_dimension -variable_name -⋅ -``` diff --git a/docs/attic/subpackages.md b/docs/attic/subpackages.md deleted file mode 100644 index f5d234eda..000000000 --- a/docs/attic/subpackages.md +++ /dev/null @@ -1,10 +0,0 @@ -# Control Toolbox Subpackages - -The control toolbox is composed of several subpackages, each with its own documentation: - -- [CTBase](@extref CTBase index) -- [CTDirect](@extref CTDirect index) -- [CTFlows](@extref CTFlows index) -- [CTModels](@extref CTModels index) -- [CTParser](@extref CTParser index) -- [CTSolvers](@extref CTSolvers index) diff --git a/docs/attic/tutorial.md b/docs/attic/tutorial.md deleted file mode 100644 index ce9a22d74..000000000 --- a/docs/attic/tutorial.md +++ /dev/null @@ -1,499 +0,0 @@ -```@meta -EditURL = "../src-literate/tutorial.jl" -``` - -# OptimalControl.jl — a guided tour - - -This tutorial is a guided tour of [OptimalControl.jl](https://control-toolbox.org/OptimalControl.jl), part of the [control-toolbox](https://control-toolbox.org) ecosystem. We follow two problems end to end: a simple **double integrator** for modelling, initialisation and the **indirect** (Pontryagin) method, and the **Goddard rocket** for the **direct** method in depth, grid continuation and GPU solving. Advanced topics are linked at the end. - -It is written for readers with a background in optimal control, ODEs or optimisation. By the end you will be able to define an optimal control problem, solve it by both the direct and indirect methods, and visualise the result — all in a few lines of code. - -!!! note "Run online" - You can run this tutorial interactively in your browser — no installation required — by clicking the Binder badge below: - - [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/control-toolbox/OptimalControl.jl/paderborn?urlpath=%2Fdoc%2Ftree%2Fdocs%2Fsrc%2Fnotebooks%2Ftutorial.ipynb) - -## The problem, and installing the tools - -An **optimal control problem** (OCP) in Bolza form reads - -```math -J(x, u) = g(x(t_0), x(t_f)) + \int_{t_0}^{t_f} f^{0}(t, x(t), u(t))\,\mathrm{d}t \;\to\; \min, -``` - -subject to the controlled dynamics $\dot{x}(t) = f(t, x(t), u(t))$ and, possibly, box / path / boundary constraints. When $g = 0$ the cost is of **Lagrange** form; when $f^0 = 0$, of **Mayer** form. - -More generally, the times $t_0$ and $t_f$ may be free (optimisation variables), and a vector $v$ of additional parameters can enter the cost, dynamics and constraints. The full problem then reads - -```math -\min_{x,u,v}\; g(x(t_0), x(t_f), v) + \int_{t_0}^{t_f} f^{0}(t, x(t), u(t), v)\,\mathrm{d}t, -``` - -subject to $\dot{x}(t) = f(t, x(t), u(t), v)$, box / path / boundary constraints. - -OptimalControl.jl is the core of the [control-toolbox](https://control-toolbox.org) ecosystem, a modular suite of Julia packages — CTBase (base types & exceptions), CTParser (DSL parsing), CTModels (problem data structures), CTDirect (discretisation & NLP transcription), CTFlows (Hamiltonian flows for indirect methods), and CTSolvers (solver orchestration) — that can also be used individually. - -Installation is a single package: - -```julia -import Pkg -Pkg.add("OptimalControl") -``` - -We load OptimalControl.jl to model the problem, a solver backend ([NLPModelsIpopt.jl](https://jso.dev/NLPModelsIpopt.jl)), and [Plots.jl](https://docs.juliaplots.org). - -````@example tutorial -using OptimalControl -using NLPModelsIpopt -using Plots -```` - -## Defining a problem: `@def` vs macro-free - -Our running example: a wagon of unit mass on a frictionless rail, state $x = (q, v)$ (position, velocity), acceleration controlled by a force $u$. We start at $(-1, 0)$, must reach $(0, 0)$ at $t_f = 1$, and minimise the transfer energy - -```math -\frac{1}{2}\int_0^1 u^2(t)\,\mathrm{d}t, -``` - -subject to the dynamics - -```math -\dot{q}(t) = v(t), \qquad \dot{v}(t) = u(t). -``` - -````@example tutorial -t0 = 0; -tf = 1; -x0 = [-1, 0]; -xf = [0, 0]; -nothing #hide -```` - -### The `@def` macro - -The [`@def`](@ref manual-abstract-syntax) macro lets us write the problem almost exactly as the mathematics: - -Each line of the `@def` block mirrors a piece of the mathematical formulation — time, state, control, dynamics, boundary conditions, then cost — in the same order one would write them on paper. Unicode symbols (`∈`, `R²`, `ẋ`, `∫`, `→`) make the code read like the maths; plain ASCII alternatives (`R^2`, `derivative`, `integral`, `=>`) are available for keyboards or workflows that prefer them. - -````@example tutorial -ocp = @def begin - t ∈ [t0, tf], time - x = (q, v) ∈ R², state - u ∈ R, control - - x(t0) == x0 - x(tf) == xf - - ẋ(t) == [v(t), u(t)] - - 0.5∫(u(t)^2) → min -end -nothing # hide -```` - -### The same problem with the macro-free (functional) API - -The [functional API](@ref manual-macro-free) builds the *same* model step by step with plain functions — useful for programmatic problem generation or macro-free library code. - -````@example tutorial -pre = OptimalControl.PreModel() - -time!(pre; t0=t0, tf=tf) -state!(pre, 2, "x", ["q", "v"]) -control!(pre, 1) - -function f_energy!(dx, t, x, u, v) - dx[1] = x[2] - dx[2] = u[1] - return nothing -end -dynamics!(pre, f_energy!) - -function boundary_energy!(b, x0_, xf_, v) - b[1] = x0_[1] - x0[1] - b[2] = x0_[2] - x0[2] - b[3] = xf_[1] - xf[1] - b[4] = xf_[2] - xf[2] - return nothing -end -constraint!(pre, :boundary; f=boundary_energy!, lb=zeros(4), ub=zeros(4), label=:endpoint) - -lagrange_energy(t, x, u, v) = 0.5 * u[1]^2 -objective!(pre, :min; lagrange=lagrange_energy) - -time_dependence!(pre; autonomous=true) - -ocp_func = build(pre) -nothing # hide -```` - -### What the macro actually does - -**Key message:** `@def` *translates the expression* into the very same functional calls, and **additionally records the symbolic definition**. We can see the difference directly: the macro keeps the DSL expression, whereas the functional API stores an empty definition. - -````@example tutorial -definition(ocp) # the macro records the full DSL expression -```` - -````@example tutorial -has_abstract_definition(ocp_func) # false: functional API stores no abstract definition -```` - -!!! warning "Two things to keep in mind" - - In the functional API, callbacks are **always vector-valued**: even when the control is scalar, one writes `u[1]` — not `u` — inside `f_energy!` or `lagrange_energy`. - - The functional API currently works only with the `:adnlp` modeler; it does **not** support the `:exa` modeler needed for GPU solving — one more reason to prefer `@def` when GPU execution is contemplated (more in the GPU section). - -## First solve, initial guess, and the costate - -Solving is one call, plotting another. - -````@example tutorial -direct_sol = solve(ocp) -nothing # hide -```` - -````@example tutorial -direct_sol # hide -```` - -````@example tutorial -plot(direct_sol; size=(800, 600)) -```` - -### The default initial guess - -With no initial guess, every variable is initialised to `0.1`. We can *see* the initial guess without optimising, by stopping the solver immediately with `max_iter=0`: - -````@example tutorial -sol_init = solve(ocp; init=nothing, max_iter=0, display=false) -plot(sol_init; size=(800, 600)) -```` - -!!! note "Notice the right-hand column: the costate is already there" - Even though we only ever provide the state, control and (optional) variable, the solver initialises the **adjoint** internally. After optimisation, this right-column costate is exactly the **adjoint $p$ of Pontryagin's Maximum Principle** — the same $p$ we will reuse to start the indirect method in the indirect section. This closes the loop between the direct and indirect methods. - -### Providing our own initial guess - -The recommended way to provide an initial guess is the `@init` macro, using the labels from the `@def` block (`q`, `v`, `u` here): - -````@example tutorial -ig = @init ocp begin - q(t) := -1 + t - v(t) := 0 - u(t) := 0 -end - -sol = solve(ocp; init=ig, display=false) -println("iterations, default guess: ", iterations(direct_sol)) -println("iterations, @init guess: ", iterations(sol)) -```` - -In this case both guesses give **1 iteration**: the double integrator is a *linear-quadratic* problem, so the NLP is quadratic and Ipopt solves it in a single step regardless of the starting point. Warm-starting only pays off on genuinely nonlinear problems — we will see this with the **Goddard rocket** in the next section. - -For all the ways to specify an initial guess, see [Set an initial guess](@ref manual-initial-guess). -!!! note - There is currently no way to initialise the costate directly — only state, control and variable can be provided through `@init`. The solver initialises the adjoint internally (as we saw above). Costate initialisation is a planned feature. - -## Direct method in depth: Goddard - -### Discretise optimal control problems - -The **direct** method turns the infinite-dimensional OCP into a finite-dimensional nonlinear program (NLP) by discretising time (Runge–Kutta / collocation) on a grid, then hands the NLP to a solver. It is robust and easy to use. - -Concretely, time is discretised on a uniform grid $t_0 < t_1 < \dots < t_N = t_f$ with step $h = (t_f - t_0)/N$. The (explicit) Euler scheme, for instance, replaces the dynamics by - -```math -x_{i} = x_{i-1} + h\,f(t_{i-1}, x_{i-1}, u_{i-1}), \quad i = 1, \dots, N, -``` - -and the integral cost by the corresponding rectangle sum - -```math -h\sum_{i=0}^{N-1} f^{0}(t_i, x_i, u_i). -``` - -The continuous OCP thus becomes a finite-dimensional NLP in the variables $X = (x_0, \dots, x_N, u_0, \dots, u_N)$, which is passed to an NLP solver such as [Ipopt](https://coin-or.github.io/Ipopt). Higher-order schemes (midpoint, Gauss–Legendre collocation) follow the same principle with different quadrature and interpolation formulas — `solve` defaults to the second-order `:midpoint` scheme, not Euler. - -### The Goddard rocket problem - -To demonstrate convergence behaviour and warm-starting, we need a genuinely nonlinear problem. The **Goddard rocket** — maximise the final altitude, with free final time and a singular arc — is a classic test case. - -````@example tutorial -# Goddard data and dynamics (F0: drift, F1: thrust) -const r0 = 1 -const v0 = 0 -const m0 = 1 -const mf = 0.6 -const Cd = 310 -const Tmax = 3.5 -const β = 500 -const b = 2 - -F0(x) = begin - r, v, m = x - D = Cd * v^2 * exp(-β * (r - 1)) - [v, -D/m - 1/r^2, 0] -end -F1(x) = begin - r, v, m = x - [0, Tmax/m, -b*Tmax] -end - -goddard = @def begin - tf ∈ R, variable - t ∈ [t0, tf], time - x = (r, v, m) ∈ R³, state - u ∈ R, control - - x(t0) == [r0, v0, m0] - m(tf) == mf - 0 ≤ u(t) ≤ 1 - r(t) ≥ r0 - - ẋ(t) == F0(x(t)) + u(t) * F1(x(t)) - - r(tf) → max -end -nothing # hide -```` - -### Choosing a solver is trivial - -`solve` uses the defaults (collocation, ADNLP modeler, Ipopt, CPU). Switching solver is just loading a package and passing a token (see [Solve a problem](@ref manual-solve)): - -````@example tutorial -using MadNLP - -sol_ipopt = solve(goddard; grid_size=250, display=false) -sol_madnlp = solve(goddard, :madnlp; grid_size=250, display=false) - -println("Ipopt : r(tf) = ", objective(sol_ipopt), ", ", iterations(sol_ipopt), " iters") -println("MadNLP : r(tf) = ", objective(sol_madnlp), ", ", iterations(sol_madnlp), " iters") -```` - -The available methods and their options can be inspected with `methods()` and `describe(:collocation)`; we will not dwell on them here. - -### Grid continuation by warm-starting - -A solution can be passed **directly** as the initial guess of another solve — it is interpolated onto the new grid. This makes discrete continuation trivial and ties back to the initialisation above. On this nonlinear problem it genuinely **pays**: we compare reaching a fine grid of 1000 two ways: - -1. **cold start** — solve `grid_size=1000` directly; -2. **cascade** — solve `grid_size=50` first, then `grid_size=1000` warm-started with that solution. - -````@example tutorial -# solutions computed once, reused for iteration counts and the overlay plot -sol_cold = solve(goddard; grid_size=1000, display=false) - -# warm cascade: grid 50 first, then grid 1000 initialised from it -s50 = solve(goddard; grid_size=50, display=false) -s1000 = solve(goddard; grid_size=1000, init=s50, display=false) - -println("cold grid 1000 : ", iterations(sol_cold), " iters") -println("cascade grid 50 (warm-up): ", iterations(s50), " iters") -println("cascade grid 1000 (warm) : ", iterations(s1000), " iters") -```` - -**Message:** what matters is the iteration count *at the expensive grid* — the warm-started `iterations(s1000)` is well below the cold `iterations(sol_cold)`, even though the cheap `grid_size=50` warm-up adds iterations of its own to the running total; since a grid-50 iteration is far cheaper than a grid-1000 iteration, the cascade still wins on wall-clock time. Overlay the successive solutions to watch convergence: - -````@example tutorial -plt = plot(s50; label="50", size=(800, 800)) -plot!(plt, s1000; label="1000") -```` - -This is grid-refinement warm-starting. The very same mechanism drives **parametric** continuation (homotopy on a physical parameter, e.g. maximum thrust): [Discrete continuation](@extref Tutorials tutorial-continuation). - -### Comparison with a bang-bang strategy - -How much better is the optimal solution compared to a naive strategy? We simulate **full thrust until fuel depletion, then coast to apogee** — a bang-bang profile with no optimisation, just two ODE integrations with callbacks. - -````@example tutorial -using OrdinaryDiffEq # ODE solver (callbacks for the bang-bang simulation) - -# Phase 1: u = 1, stop when m = mf (fuel depleted) -bang1!(dx, x, p, t) = (dx[:] = F0(x) + F1(x)) -cb_fuel = ContinuousCallback((u, t, int) -> u[3] - mf, terminate!) -sol_bang1 = solve( - ODEProblem(bang1!, [r0, v0, m0], (t0, 100.0)), - Tsit5(); - callback=cb_fuel, - reltol=1e-8, - abstol=1e-8, -) -t1_bang, x1_bang = sol_bang1.t[end], sol_bang1[:, end] - -# Phase 2: u = 0, stop when v = 0 (apogee) -bang2!(dx, x, p, t) = (dx[:] = F0(x)) -cb_apogee = ContinuousCallback((u, t, int) -> u[2], terminate!) -sol_bang2 = solve( - ODEProblem(bang2!, x1_bang, (t1_bang, 1000.0)), - Tsit5(); - callback=cb_apogee, - reltol=1e-8, - abstol=1e-8, -) -tf_bang, rf_bang = sol_bang2.t[end], sol_bang2[1, end] - -println( - "Bang-bang: r(tf) = ", - round(rf_bang; digits=6), - " (t1=", - round(t1_bang; digits=4), - ", tf=", - round(tf_bang; digits=4), - ")", -) -println( - "Optimal: r(tf) = ", - round(objective(sol_cold); digits=6), - " ( tf=", - round(variable(sol_cold); digits=4), - ")", -) -```` - -The optimal thrust profile uses a **singular arc** — it does not simply push at the maximum. Overlaying the two trajectories on the altitude–velocity plane makes the difference visible: - -````@example tutorial -# assemble the bang-bang trajectory as (t, r, v, m) for plotting -t_bang = [sol_bang1.t; sol_bang2.t] -r_bang = [sol_bang1[1, :]; sol_bang2[1, :]] - -plt_bang = plot(sol_cold; label="optimal", linewidth=2, color=1) -plot!(plt_bang[1], t_bang, r_bang; label="bang-bang", linestyle=:dash, linewidth=2, color=2) -plot(plt_bang[1]; legend=:bottomright, xlabel="time", ylabel="altitude") -```` - -## Solving on a GPU - -Moving to the GPU is a single token, `:gpu`, which auto-completes to `(:collocation, :exa, :madnlp, :gpu)`. It requires the `:exa` modeler (hence `@def`, not the macro-free API — cf. the definition section) plus a CUDA-capable GPU. - -In a seminar or on Binder there is usually **no functional GPU**, so the call is *expected to fail* — that is the pedagogical point: the `:gpu` token needs a specific setup. We wrap it in a `try/catch` so the tour keeps running and shows the raised exception. - -````@example tutorial -using MadNLPGPU -using CUDA - -try - global sol_gpu = solve(goddard, :gpu; grid_size=1000, display=false) - println("GPU solve succeeded — a functional GPU is available.") -catch e - println("GPU solve failed, as expected without a functional GPU.") - println("CUDA.functional() = ", CUDA.functional()) - println("Exception: ", first(sprint(showerror, e), 400)) -end -```` - -For the full GPU setup, see [Solve on GPU](@ref manual-solve-gpu). - -## The indirect method - -We now return to the **double integrator** `ocp` from the earlier sections. Its shooting has just two unknowns and is initialised by the direct costate above, which makes it ideal to *see* the indirect method. (The Goddard shooting is a *structured multi-arc* problem — see the links in the last section.) - -In control-toolbox we systematically pair the direct method with the **indirect** one, based on Pontryagin's Maximum Principle (PMP), with pseudo-Hamiltonian - -```math -H(x,p,u) = p\,f(x,u) + p^0 f^0(x,u) \qquad (\text{normal case } p^0 = -1). -``` - -The PMP gives the maximising control in feedback form - -```math -u(x,p) = \arg\max_u H, -``` - -and the optimal trajectory solves a boundary value problem that we recast as a **shooting equation** - -```math -S(p_0) = 0. -``` - -The indirect method proceeds in three steps: - -1. **Maximising control.** The PMP yields the control in feedback form $u(x, p) = \arg\max_u H(x, p, u)$. Substituting back gives the maximised Hamiltonian - - ```math - \mathbf{H}(x, p) = H(x, p, u(x, p)). - ``` - -2. **Boundary value problem.** The optimal trajectory satisfies the Hamiltonian system - - ```math - \dot{x} = \nabla_p \mathbf{H}, \qquad \dot{p} = -\nabla_x \mathbf{H}, - ``` - - with boundary conditions $x(t_0) = x_0$, $x(t_f) = x_f$. - -3. **Shooting function.** Let $\varphi_{t_0, x_0, p_0}(\cdot)$ denote the flow of the Hamiltonian vector field from $(x_0, p_0)$. The shooting function - - ```math - S(p_0) = \pi(\varphi_{t_0, x_0, p_0}(t_f)) - x_f, \qquad \pi(x, p) = x, - ``` - - measures the miss at $t_f$: solving the BVP reduces to finding $p_0$ such that $S(p_0) = 0$. - -For the energy problem, $H = p_1 v + p_2 u - u^2/2$, so the maximiser is $u = p_2$. - -````@example tutorial -using OrdinaryDiffEq # ODE solver (Hamiltonian flow) -using NonlinearSolve # nonlinear equations (shooting) - -# maximising control in feedback form -u_max(x, p) = p[2] - -# Hamiltonian flow of the OCP -φ = Flow(ocp, u_max); - -# state projection π(x, p) = x -proj((x, p)) = x - -# shooting function -S(p0) = proj(φ(t0, x0, p0, tf)) - xf -nothing # hide -```` - -**The shooting is initialised with the costate of the direct solution** — the very adjoint we highlighted above: - -````@example tutorial -nle!(s, p0, _) = (s[:] = S(p0)) - -p_of_t = costate(direct_sol) # costate as a function of time -p0_guess = p_of_t(t0) # initial costate from the direct method - -prob = NonlinearProblem(nle!, p0_guess) -shooting_sol = solve(prob; show_trace=Val(true)) -p0_sol = shooting_sol.u - -println("costate p0 = ", p0_sol) -println("shoot S(p0) = ", S(p0_sol)) -```` - -Reconstruct the indirect solution from the flow and overlay it with the direct solution: - -````@example tutorial -indirect_sol = φ((t0, tf), x0, p0_sol; saveat=range(t0, tf, 100)) - -plt_compare = plot(direct_sol; label="direct", size=(800, 600)) -plot!(plt_compare, indirect_sol; label="indirect") -```` - -See [Compute flows from optimal control problems](@ref manual-flow-ocp) for the flow construction, and the [indirect simple shooting tutorial](@extref tutorial-indirect-simple-shooting). - -## Going further - -**Variables & parameters.** Beyond the control, one can optimise **parameters** naturally, both in an OCP (the `variable` keyword of the DSL) and in a differential-constraint optimisation problem **without any control** (a *control-free* problem). -See [control-free problems](@ref example-control-free). - -**Advanced examples** (each does both direct and indirect): - -- Singular control (control-affine systems) — [singular control](@ref example-singular-control) -- State constraint — [state constraint](@ref example-state-constraint) -- Goddard problem — free final time, a singular arc, a state constraint and a structured shooting all at once — [Goddard tutorial](@extref Tutorials tutorial-goddard) - -**Discrete continuation** — warm-starting across a family of problems (homotopy on a physical parameter), the grown-up version of the grid continuation above: [Discrete continuation](@extref Tutorials tutorial-continuation). - ---- - -*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* - diff --git a/docs/reports/README.md b/docs/reports/README.md index 37f70e7dd..99301234e 100644 --- a/docs/reports/README.md +++ b/docs/reports/README.md @@ -60,7 +60,7 @@ Status legend: ⬜ not started · 🟡 in progress · ✅ merged | 10 | [`docs: examples`](https://github.com/control-toolbox/OptimalControl.jl/pull/872) | Examples | [`08`](08-examples.md) | 8, 9 | ✅ | | 11 | [`docs: getting started`](https://github.com/control-toolbox/OptimalControl.jl/pull/873) | Getting started + `index.md` | [`02`](02-getting-started.md) | 5–10 | ✅ | | 12 | [`docs: migration page`](https://github.com/control-toolbox/OptimalControl.jl/pull/874) | Migration page (`docs/src/migration.md`) | [`10`](10-migration.md) §2 | all | ✅ | -| 13 | `docs: drop docs/attic` | Delete `docs/attic/` | [`10`](10-migration.md) §2 | 12 | ⬜ | +| 13 | [`docs: drop docs/attic`](https://github.com/control-toolbox/OptimalControl.jl/pull/940) | Delete `docs/attic/` | [`10`](10-migration.md) §2 | 12 | 🟡 | PR 3 is code-only and independent — it can run in parallel with any docs PR. diff --git a/docs/src/solve/choosing-a-method.md b/docs/src/solve/choosing-a-method.md index 3f446560f..c69009ad1 100644 --- a/docs/src/solve/choosing-a-method.md +++ b/docs/src/solve/choosing-a-method.md @@ -167,9 +167,8 @@ Every cell above was checked by solving with that scheme. One of the results nee `strategy_ids`, `type_from_id`, and `available_parameters` (and the [`create_registry`](@ref) used to build one) all operate on a populated `StrategyRegistry`. The one that already knows -about every built-in strategy is internal (`OptimalControl.get_strategy_registry()`, not -re-exported) — these functions are orchestration/extension-authoring tools, not something a -typical solve caller reaches for. For everyday inspection, `methods()` and `describe` (above) +about every built-in strategy is internal and not re-exported — these functions are +orchestration/extension-authoring tools, not something a typical solve caller reaches for. For everyday inspection, `methods()` and `describe` (above) cover the same ground and need nothing extra. ## See also