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