diff --git a/docs/src-literate/guided-tour.jl b/docs/src-literate/guided-tour.jl index 948dddd18..0207b576b 100644 --- a/docs/src-literate/guided-tour.jl +++ b/docs/src-literate/guided-tour.jl @@ -115,7 +115,7 @@ control!(pre, 1) function f_energy!(dx, t, x, u, v) dx[1] = x[2] - dx[2] = u[1] + dx[2] = u # scalar control: `u`, not `u[1]` return nothing end dynamics!(pre, f_energy!) @@ -132,7 +132,7 @@ constraint!( f=boundary_energy!, lb=zeros(4), ub=zeros(4), label=:endpoint, ) -lagrange_energy(t, x, u, v) = 0.5 * u[1]^2 +lagrange_energy(t, x, u, v) = 0.5 * u^2 objective!(pre, :min; lagrange=lagrange_energy) time_dependence!(pre; autonomous=true) @@ -151,9 +151,9 @@ definition(ocp) # the macro records the full DSL expression has_abstract_definition(ocp_func) # false: no symbolic definition #md # !!! warning "Two things to keep in mind" -#md # - 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`. +#md # - A dimension-1 `x`, `u` or `v` reaches a callback as a **scalar** — write `u`, not `u[1]` — exactly as on a solution and in `@def`. The one exception is the in-place output buffer (`dx` here, `val` for constraints): always a vector, written by index. See [Shapes in callbacks](@ref modelling-functional-api-shapes). #md # - 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). -#nb # **Two things to keep in mind:** (1) 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`; (2) the functional API currently works only with the `:adnlp` modeler, **not** `:exa` (needed for GPU solving) — one more reason to prefer `@def` when GPU execution is contemplated (more in the GPU section). +#nb # **Two things to keep in mind:** (1) a dimension-1 `x`, `u` or `v` reaches a callback as a scalar — write `u`, not `u[1]` — while the in-place output buffer (`dx`, `val`) is always a vector written by index; (2) the functional API currently works only with the `:adnlp` modeler, **not** `:exa` (needed for GPU solving) — one more reason to prefer `@def` when GPU execution is contemplated (more in the GPU section). #src ============================================================================ # ## First solve, initial guess, and the costate diff --git a/docs/src/modelling/functional-api.md b/docs/src/modelling/functional-api.md index 210073c73..51de300dc 100644 --- a/docs/src/modelling/functional-api.md +++ b/docs/src/modelling/functional-api.md @@ -115,9 +115,13 @@ ocp = build(pre) **Optional:** `variable!` · `control!` · `constraint!` (repeatable) -## A worked example: double integrator, energy minimisation +## Worked problems -The simplest case: fixed time interval, boundary constraints, autonomous dynamics, Lagrange cost. The [`@def`](@ref) abstract syntax is shown on the left and the equivalent functional API on the right. See the [example gallery](@ref examples-gallery) for more problems worked both ways, including a control-free one (see also [No control](@ref modelling-without-control)). +Seven problems, each shown as [`@def`](@ref) (left) and the equivalent functional API (right). Every one has a full worked story — direct *and* indirect solution — on its [example gallery](@ref examples-gallery) page; here the focus is the model, built both ways and solved once to check the two agree. The callbacks follow the scalar convention of [Shapes in callbacks](@ref modelling-functional-api-shapes): a dimension-1 `x`, `u` or `v` is a `Real`, only the in-place output buffers (`dx`, `val`) are written by index. + +### 1. Double integrator — energy minimisation + +The simplest case: fixed time interval, boundary constraints, autonomous dynamics, Lagrange cost. ```@example ex-energy using OptimalControl @@ -237,6 +241,592 @@ definition(ocp_macro) has_abstract_definition(ocp_func) ``` +Full worked story (direct + indirect): [Energy minimisation](@ref examples-double-integrator-energy) · [example gallery](@ref examples-gallery). + +### 2. Double integrator — time minimisation + +The same wagon, transferred as fast as possible: the control is box-bounded and the final time `tf` is a free **variable** (`variable!` before `time!`, then `indf=1`). The cost is a Mayer term (`tf`), not a Lagrange integral. + +```@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!(pre, 1, "tf") # free final time — before time! +time!(pre; t0=t0, indf=1) # tf is variable 1 +state!(pre, 2, "x", ["q", "v"]) +control!(pre, 1) + +function f_time!(dx, t, x, u, v) + dx[1] = x[2] + dx[2] = u + return nothing +end +dynamics!(pre, f_time!) + +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 +) + +constraint!(pre, :control; rg=1:1, lb=[-1.0], ub=[1.0], label=:u_box) + +objective!(pre, :min; mayer=(x0_, xf_, v) -> v) + +time_dependence!(pre; autonomous=true) + +ocp_func = build(pre) +nothing # hide +``` + +```@raw html +
+
+``` + +```@example ex-time +sol_macro = solve(ocp_macro; grid_size=20, display=false) +sol_func = solve(ocp_func; grid_size=20, display=false) +println( + "objective: macro = ", objective(sol_macro), + ", functional = ", objective(sol_func), +) +``` + +Full worked story (direct + indirect): [Time minimisation](@ref examples-double-integrator-time) · [example gallery](@ref examples-gallery). + +### 3. Parameter estimation — no control + +No control anywhere: `control!` is simply never called (see [No control](@ref modelling-without-control)). Only a **variable** — the growth rate `λ` — is optimised, fitting the state to data. The Lagrange integrand reads `t` through `data(t)`, so the problem is **non-autonomous**. + +```@example ex-control-free +using OptimalControl +using NLPModelsIpopt + +λ_true = 0.5 +x_true(t) = 2 * exp(λ_true * t) +data(t) = x_true(t) + 0.2 * sin(4π * t) + +t0 = 0.0; tf = 2.0; x0 = 2.0 +nothing # hide +``` + +```@raw html +
+
+``` + +**Abstract syntax** + +```@example ex-control-free +ocp_macro = @def begin + + λ ∈ R, variable + t ∈ [t0, tf], time + x ∈ R, state + + x(t0) == x0 + + ẋ(t) == λ * x(t) + + ∫( (x(t) - data(t))^2 ) → min + +end +nothing # hide +``` + +```@raw html +
+
+``` + +**Functional API** + +```@example ex-control-free +pre = OptimalControl.PreModel() + +variable!(pre, 1, "λ") +time!(pre; t0=t0, tf=tf) +state!(pre, 1, "x") +# no control! — this problem is control-free + +function f_cf!(dx, t, x, u, v) + dx[1] = v * x # v: the scalar variable λ; x: the scalar state + return nothing +end +dynamics!(pre, f_cf!) + +objective!(pre, :min; lagrange=(t, x, u, v) -> (x - data(t))^2) + +function boundary_cf!(b, x0_, xf_, v) + b[1] = x0_ - x0 # 1-D state ⇒ x0_ is a scalar + return nothing +end +constraint!(pre, + :boundary; f=boundary_cf!, lb=[0.0], ub=[0.0], label=:ic +) + +time_dependence!(pre; autonomous=false) # data(t) depends on t + +ocp_func = build(pre) +nothing # hide +``` + +```@raw html +
+
+``` + +```@example ex-control-free +sol_macro = solve(ocp_macro; grid_size=20, display=false) +sol_func = solve(ocp_func; grid_size=20, display=false) +println( + "estimated λ: macro = ", variable(sol_macro), + ", functional = ", variable(sol_func), +) +``` + +Full worked story (direct + indirect): [Parameter estimation without a control](@ref examples-control-free), [No control](@ref modelling-without-control) · [example gallery](@ref examples-gallery). + +### 4. Control and variable together + +The growth problem again, now with a control input and a quadratic control cost — a **variable** (`λ`) and a **control** (`u`) estimated at once. + +```@example ex-control-variable +using OptimalControl +using NLPModelsIpopt + +λ_true = 0.5 +x_true(t) = 2 * exp(λ_true * t) +data(t) = x_true(t) + 0.2 * sin(4π * t) + +t0 = 0.0; tf = 2.0; x0 = 2.0 +nothing # hide +``` + +```@raw html +
+
+``` + +**Abstract syntax** + +```@example ex-control-variable +ocp_macro = @def begin + + λ ∈ R, variable + t ∈ [t0, tf], time + x ∈ R, state + u ∈ R, control + + x(t0) == x0 + + ẋ(t) == λ * x(t) + u(t) + + ∫( (x(t) - data(t))^2 + 0.5u(t)^2 ) → min + +end +nothing # hide +``` + +```@raw html +
+
+``` + +**Functional API** + +```@example ex-control-variable +pre = OptimalControl.PreModel() + +variable!(pre, 1, "λ") +time!(pre; t0=t0, tf=tf) +state!(pre, 1, "x") +control!(pre, 1) + +function f_cv!(dx, t, x, u, v) + dx[1] = v * x + u + return nothing +end +dynamics!(pre, f_cv!) + +objective!(pre, :min; + lagrange=(t, x, u, v) -> (x - data(t))^2 + 0.5 * u^2, +) + +function boundary_cv!(b, x0_, xf_, v) + b[1] = x0_ - x0 + return nothing +end +constraint!(pre, + :boundary; f=boundary_cv!, lb=[0.0], ub=[0.0], label=:ic +) + +time_dependence!(pre; autonomous=false) + +ocp_func = build(pre) +nothing # hide +``` + +```@raw html +
+
+``` + +```@example ex-control-variable +sol_macro = solve(ocp_macro; grid_size=20, display=false) +sol_func = solve(ocp_func; grid_size=20, display=false) +println( + "objective: macro = ", objective(sol_macro), + ", functional = ", objective(sol_func), +) +``` + +Full worked story (direct + indirect): [Control and variable together](@ref examples-control-and-variable) · [example gallery](@ref examples-gallery). + +### 5. Turnpike — bang–singular–bang + +The smallest problem with a singular arc: a scalar state *and* a scalar control, fixed horizon, one control box, a Lagrange cost. Nothing here is a length-1 vector — the callbacks are pure scalar arithmetic. + +```@example ex-turnpike +using OptimalControl +using NLPModelsIpopt +t0 = 0.0; tf = 2.0; x0 = 1.0; xf = 0.5 +nothing # hide +``` + +```@raw html +
+
+``` + +**Abstract syntax** + +```@example ex-turnpike +ocp_macro = @def begin + + t ∈ [t0, tf], time + x ∈ R, state + u ∈ R, control + + -1 ≤ u(t) ≤ 1 + + x(t0) == x0 + x(tf) == xf + + ẋ(t) == u(t) + + ∫( x(t)^2 ) → min + +end +nothing # hide +``` + +```@raw html +
+
+``` + +**Functional API** + +```@example ex-turnpike +pre = OptimalControl.PreModel() + +time!(pre; t0=t0, tf=tf) +state!(pre, 1, "x") +control!(pre, 1) + +function f_turnpike!(dx, t, x, u, v) + dx[1] = u # x, u both scalars + return nothing +end +dynamics!(pre, f_turnpike!) + +function boundary_turnpike!(b, x0_, xf_, v) + b[1] = x0_ - x0 # 1-D state ⇒ x0_, xf_ are scalars + b[2] = xf_ - xf + return nothing +end +constraint!(pre, + :boundary; f=boundary_turnpike!, lb=zeros(2), ub=zeros(2), label=:endpoint +) + +constraint!(pre, :control; rg=1:1, lb=[-1.0], ub=[1.0], label=:u_box) + +objective!(pre, :min; lagrange=(t, x, u, v) -> x^2) + +time_dependence!(pre; autonomous=true) + +ocp_func = build(pre) +nothing # hide +``` + +```@raw html +
+
+``` + +```@example ex-turnpike +sol_macro = solve(ocp_macro; grid_size=100, display=false) +sol_func = solve(ocp_func; grid_size=100, display=false) +println( + "objective: macro = ", objective(sol_macro), + ", functional = ", objective(sol_func), +) +``` + +Full worked story (direct + indirect): [Turnpike (bang–singular–bang)](@ref examples-turnpike) · [example gallery](@ref examples-gallery). + +### 6. Singular control + +A planar vehicle with drift, time-optimal: three-dimensional state, free final time, and box bounds on both the control and one state component. The optimal control has a singular arc — the example page computes it with the Geometry toolkit. + +```@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!(pre, 1, "tf") +time!(pre; t0=0.0, indf=1) +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 + return nothing +end +dynamics!(pre, f_singular!) + +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 +) + +constraint!(pre, :control; rg=1:1, lb=[-1.0], ub=[1.0], label=:u_box) +constraint!(pre, :state; rg=3:3, lb=[-π / 2], ub=[π / 2], label=:θ_box) + +objective!(pre, :min; mayer=(q0, qf, v) -> v) + +time_dependence!(pre; autonomous=true) + +ocp_func = build(pre) +nothing # hide +``` + +```@raw html +
+
+``` + +```@example ex-singular +sol_macro = solve(ocp_macro; grid_size=50, display=false) +sol_func = solve(ocp_func; grid_size=50, display=false) +println( + "objective: macro = ", objective(sol_macro), + ", functional = ", objective(sol_func), +) +``` + +Full worked story (direct + indirect): [Singular control](@ref examples-singular-control) · [example gallery](@ref examples-gallery). + +### 7. State constraint + +The energy-minimal transfer of problem 1, now with an upper bound on the velocity written as a nonlinear **path** constraint — so it carries a dual, reachable by its label — rather than a box on the state component. + +```@example ex-state-constraint +using OptimalControl +using NLPModelsIpopt +t0 = 0.0; tf = 1.0; x0 = [-1.0, 0.0]; xf = [0.0, 0.0]; VMAX = 1.2 +nothing # hide +``` + +```@raw html +
+
+``` + +**Abstract syntax** + +```@example ex-state-constraint +ocp_macro = @def begin + + t ∈ [t0, tf], time + x = (q, v) ∈ R², state + u ∈ R, control + + x(t0) == x0 + x(tf) == xf + + v(t) + 0.0 ≤ VMAX, (vmax) + + ẋ(t) == [v(t), u(t)] + + 0.5∫( u(t)^2 ) → min + +end +nothing # hide +``` + +```@raw html +
+
+``` + +**Functional API** + +```@example ex-state-constraint +pre = OptimalControl.PreModel() + +time!(pre; t0=t0, tf=tf) +state!(pre, 2, "x", ["q", "v"]) +control!(pre, 1) + +function f_sc!(dx, t, x, u, v) + dx[1] = x[2] + dx[2] = u + return nothing +end +dynamics!(pre, f_sc!) + +function boundary_sc!(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_sc!, lb=zeros(4), ub=zeros(4), label=:endpoint +) + +function path_sc!(c, t, x, u, v) + c[1] = x[2] # v(t) ≤ VMAX + return nothing +end +constraint!(pre, + :path; f=path_sc!, lb=[-Inf], ub=[VMAX], label=:vmax +) + +objective!(pre, :min; lagrange=(t, x, u, v) -> 0.5 * u^2) + +time_dependence!(pre; autonomous=true) + +ocp_func = build(pre) +nothing # hide +``` + +```@raw html +
+
+``` + +```@example ex-state-constraint +sol_macro = solve(ocp_macro; grid_size=50, display=false) +sol_func = solve(ocp_func; grid_size=50, display=false) +println( + "objective: macro = ", objective(sol_macro), + ", functional = ", objective(sol_func), +) +``` + +Full worked story (direct + indirect): [State constraint](@ref examples-state-constraint) · [example gallery](@ref examples-gallery). + ## [Shapes in callbacks](@id modelling-functional-api-shapes) The control above is declared with `control!(pre, 1)` — dimension 1 — and inside the callbacks `f_energy!` and `lagrange_energy` it is used as a bare `u`, not `u[1]`. This is not a special case: **1-D state, control and variable components arrive as scalars in every functional-API callback** (`dynamics!`, `objective!`, `constraint!`), exactly as on a solution and exactly as the `@def` convention (see the note on [Control](@ref modelling-abstract-syntax-control)). There is no asymmetry between "inside a callback" and "on a solution" — a dimension-1 quantity is a `Real` everywhere: diff --git a/docs/src/modelling/without-control.md b/docs/src/modelling/without-control.md index 5659ab15b..f0f599131 100644 --- a/docs/src/modelling/without-control.md +++ b/docs/src/modelling/without-control.md @@ -17,7 +17,7 @@ For the full worked story — both examples below solved **direct and indirect** ## How to declare it -There is no dedicated syntax for "no control": simply never declare one. Declare a `variable`, a time, a state, dynamics, and a cost, and omit the control line entirely (on the [abstract syntax](@ref modelling-abstract-syntax)) or never call `control!` (on the [functional API](@ref modelling-functional-api)). +There is no dedicated syntax for "no control": simply never declare one. Declare a `variable`, a time, a state, dynamics, and a cost, and omit the control line entirely (on the [abstract syntax](@ref modelling-abstract-syntax)) or never call `control!` (on the [functional API](@ref modelling-functional-api)). The [functional API](@ref modelling-functional-api) page shows the parameter-estimation problem below built both ways, side by side. !!! warning "`control!(pre, 0)` is an error, not a spelling for \"no control\""