From f8517c54199bc4485587a35de2620e43bca83bc7 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Tue, 1 Sep 2026 22:49:16 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20minimal=201-D=20bang=E2=80=93singular?= =?UTF-8?q?=E2=80=93bang=20example=20(phase=20O)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New examples/turnpike.md: the scalar turnpike ẋ = u, |u| ≤ 1, fixed horizon, ∫x² → min. Solved direct then indirect. Because the cost is linear in u the control is bang except on a singular arc, and here the singular feedback drops straight out of the optimality conditions (p ≡ 0 ⟹ ṗ = 2x = 0 ⟹ x = 0 ⟹ u_s = 0) — no Poisson brackets, unlike examples/singular-control.md. Shooting on [p0, t1, t2] (fixed horizon), with the singular arc read off the direct solution the Goddard-tutorial way (t12 = t[abs.(φ.(t)) .≤ η], switching function ≡ costate here). Three constant-control flows concatenated with *. Solver lands on p0 = -1, t1 = 1, t2 = 3/2, cost 3/8. Wired into make.jl and gallery.md directly before "Singular control"; cross-linked from singular-control.md and shooting.md (§ switching times as unknowns); forward pointer to the Goddard problem for the harder case. Co-Authored-By: Claude Sonnet 5 --- docs/make.jl | 1 + docs/src/examples/gallery.md | 1 + docs/src/examples/singular-control.md | 3 + docs/src/examples/turnpike.md | 169 ++++++++++++++++++++++++++ docs/src/flows/shooting.md | 4 +- 5 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 docs/src/examples/turnpike.md diff --git a/docs/make.jl b/docs/make.jl index 49e8c7776..cb2450ab3 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -336,6 +336,7 @@ with_api_reference(src_dir, ext_dir) do api_pages "Time minimisation (bang–bang)" => "examples/double-integrator-time.md", "Parameter estimation without a control" => "examples/control-free.md", "Control and variable together" => "examples/control-and-variable.md", + "Turnpike (bang–singular–bang)" => "examples/turnpike.md", "Singular control" => "examples/singular-control.md", "State constraint" => "examples/state-constraint.md", "The logo" => "examples/logo.md", diff --git a/docs/src/examples/gallery.md b/docs/src/examples/gallery.md index 49788f43e..5bedd0b45 100644 --- a/docs/src/examples/gallery.md +++ b/docs/src/examples/gallery.md @@ -16,6 +16,7 @@ Read them in order: each one uses something the previous introduced. | [Time minimisation (bang–bang)](@ref examples-double-integrator-time) | The same wagon, transferred as fast as possible. | bang–bang control, flow concatenation, switching-time detection | | [Parameter estimation without a control](@ref examples-control-free) | Fitting a growth rate and an oscillator's pulsation — no control anywhere. | control-free problems, `variable_costate=true` | | [Control and variable together](@ref examples-control-and-variable) | The same two systems, now with a control input to pay for. | estimating a parameter *and* a control at once | +| [Turnpike (bang–singular–bang)](@ref examples-turnpike) | A scalar system whose optimal control is bang, singular, then bang again. | singular arcs, two switching times as shooting unknowns, three-flow concatenation | | [Singular control](@ref examples-singular-control) | A drift system whose optimal control has a singular arc. | `Lift`, `@Lie` Poisson brackets, the Geometry toolkit's payoff | | [State constraint](@ref examples-state-constraint) | The double integrator again, this time with a velocity or position bound. | boundary arcs, costate jumps, `constraint=`/`multiplier=` | diff --git a/docs/src/examples/singular-control.md b/docs/src/examples/singular-control.md index 9846e32a7..a49421148 100644 --- a/docs/src/examples/singular-control.md +++ b/docs/src/examples/singular-control.md @@ -9,6 +9,9 @@ $-1$ on part of the trajectory — a **singular arc**. This is the problem the w [Geometry](@ref geometry-overview) section exists to support: computing that arc's control needs Poisson brackets. +For the minimal case, where the singular control drops out of the optimality conditions with no +brackets at all, see [Turnpike (bang–singular–bang)](@ref examples-turnpike) first. + ```@example main using OptimalControl using NLPModelsIpopt diff --git a/docs/src/examples/turnpike.md b/docs/src/examples/turnpike.md new file mode 100644 index 000000000..5120d92f7 --- /dev/null +++ b/docs/src/examples/turnpike.md @@ -0,0 +1,169 @@ +# [Turnpike (bang–singular–bang)](@id examples-turnpike) + +```@meta +Draft = false +``` + +A scalar system, $\dot x = u$ with $u \in [-1,1]$, driven between two states over a fixed +horizon while minimising $\int x^2$. Because the cost is linear in $u$, the optimal control is +bang — $u = \pm 1$ — except on an interval where the switching function vanishes: a **singular +arc**. This is the smallest problem that shows one, and unlike +[Singular control](@ref examples-singular-control) the singular feedback falls straight out of +the optimality conditions, with no Poisson brackets. + +```@example main +using OptimalControl +using NLPModelsIpopt +using OrdinaryDiffEqTsit5 +using NonlinearSolve +using Plots +``` + +## The problem + +State $x$, control $u \in [-1,1]$, dynamics $\dot x = u$, fixed horizon $t_f = 2$, transfer +from $x(0) = 1$ to $x(t_f) = 1/2$ minimising $\int_0^{2} x(t)^2\,dt$. One dimension, so the +state and the control are scalars, not length-1 vectors. + +## Definition + +```@example main +t0, tf = 0.0, 2.0 +x0, xf = 1.0, 0.5 + +ocp = @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 +``` + +## Direct solution + +```@example main +direct_sol = solve(ocp; grid_size=100, display=false) +plt = plot(direct_sol, :state, :control; label="Direct") +``` + +The state slides down to the origin, holds there, then climbs to the target. The flat middle +stretch — the *turnpike* — is the singular arc, where $u$ leaves the bounds and sits at $0$. + +## The singular control + +The pseudo-Hamiltonian $H(x,p,u) = p\,u - x^2$ is linear in $u$, so the maximising control is +bang, $u = \operatorname{sign}(p)$, driven by the sign of the costate. Where $p$ vanishes on a +whole interval rather than at an isolated instant, that rule says nothing and the control is +*singular*. Differentiate $p \equiv 0$: the adjoint equation is $\dot p = -\partial_x H = 2x$, +so $p \equiv 0$ forces $x \equiv 0$, and then $\dot x = u$ forces + +$$u_{\text{sing}} = 0.$$ + +The whole extremal is therefore bang–singular–bang: + +| arc | interval | $u$ | $x$ | +| --- | --- | --- | --- | +| bang down | $[0,\,t_1]$ | $-1$ | $1 \to 0$ | +| singular | $[t_1,\,t_2]$ | $0$ | $0$ | +| bang up | $[t_2,\,2]$ | $+1$ | $0 \to 1/2$ | + +with $t_1 = 1$, $t_2 = 3/2$ and $p_0 = -1$, all read off by integrating each arc by hand: $x$ +falls at unit rate from $1$, reaching $0$ at $t_1 = 1$; it rises at unit rate to $1/2$, so it +must leave the arc at $t_2 = 3/2$. + +## Indirect solution + +Three constant-control flows, one per arc: + +```@example main +f_minus = Flow(ocp, (x, p) -> -1.0) +f_sing = Flow(ocp, (x, p) -> 0.0) +f_plus = Flow(ocp, (x, p) -> +1.0) +``` + +The unknowns are the initial costate $p_0$ and the two switching times $t_1 < t_2$; the horizon +is fixed. Three conditions close the system — the trajectory enters the singular arc at $x = 0$ +with the switching function already vanishing there ($p = 0$), and it hits the target at $t_f$: + +```@example main +function shoot!(s, ξ) + p0, t1, t2 = ξ[1], ξ[2], ξ[3] + x1, p1 = f_minus(t0, x0, p0, t1) + x2, p2 = f_sing(t1, x1, p1, t2) + xf_, _ = f_plus(t2, x2, p2, tf) + s[1] = x1 # enter the singular arc at x = 0 + s[2] = p1 # switching function vanishes there + s[3] = xf_ - xf # hit the target + return nothing +end +``` + +For a starting point, solve the same problem directly first, then read the singular arc off +that solution. The switching function here is just the costate ($\partial_u H = p$), so the arc +is where $|p|$ stays near zero — the same recipe as the +[Goddard tutorial](https://control-toolbox.org/Tutorials.jl/stable/tutorial-goddard.html#Initial-guess): + +```@example main +t = time_grid(direct_sol) +p = costate(direct_sol) + +φ(τ) = p(τ) # switching function ≡ costate +η = 1e-3 +t12 = t[abs.(φ.(t)) .≤ η] # grid points on the singular arc + +p0_guess = p(t0) +t1_guess = minimum(t12) +t2_guess = maximum(t12) +(p0_guess, t1_guess, t2_guess) +``` + +```@example main +nle!(s, ξ, _) = shoot!(s, ξ) +prob = NonlinearProblem(nle!, [p0_guess, t1_guess, t2_guess]) +shooting_sol = NonlinearSolve.solve(prob; show_trace=Val(false)) +p0_sol, t1_sol, t2_sol = shooting_sol.u +``` + +```@example main +s = zeros(3) +shoot!(s, shooting_sol.u) +s +``` + +The solver lands on $p_0 = -1$, $t_1 = 1$, $t_2 = 3/2$ — the hand computation above. + +## Comparison + +Concatenating the three constant-control flows at the solved switching times rebuilds the whole +trajectory: + +```@example main +φ_bsb = f_minus * (t1_sol, f_sing) * (t2_sol, f_plus) +indirect_sol = φ_bsb((t0, tf), x0, p0_sol) +plot!(plt, indirect_sol, :state, :control; + label="Indirect", linestyle=:dash) +``` + +The two curves overlap, and the objective is the expected $3/8$: + +```@example main +objective(direct_sol) +``` + +## See also + +- [Singular control](@ref examples-singular-control) — a 2-D drift system where the singular + control does need the `Lift`/`@Lie` Poisson-bracket chain. +- [Shooting](@ref flows-shooting) — the shooting method in general; this page is a worked case + of *switching times as unknowns*. +- [Time minimisation (bang–bang)](@ref examples-double-integrator-time) — the bang arcs on their + own, with no singular arc between them. +- [Multi-phase flows](@ref flows-multi-phase) — concatenating arc flows with `*`. +- [Goddard problem](https://control-toolbox.org/Tutorials.jl/stable/tutorial-goddard.html) — + the same direct-then-indirect workflow on a harder instance: a singular arc *and* a + state-constraint boundary arc. +``` diff --git a/docs/src/flows/shooting.md b/docs/src/flows/shooting.md index 7d3e460ac..518e7e149 100644 --- a/docs/src/flows/shooting.md +++ b/docs/src/flows/shooting.md @@ -135,7 +135,9 @@ pvf The example above already carries one: $t_1$ is solved for alongside $p_0$ and $t_f$. Each additional switch adds one more unknown time and one more switching-condition residual — see [Multi-phase flows](@ref flows-multi-phase) for concatenating the corresponding flows once the -times are known (or being solved for). +times are known (or being solved for), and +[Turnpike (bang–singular–bang)](@ref examples-turnpike) for a worked case with two unknown +switching times bracketing a singular arc. ## Getting a starting point from a direct solve