From 05d2e364caeffd615b002e94647dc16b7c067f23 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sun, 23 Aug 2026 19:13:00 +0200 Subject: [PATCH 1/2] docs: make four example blocks show what they claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first CI build confirmed what a local build had suggested: several pages render, publish, and are wrong, without failing the build. Each fix below was checked by running the code in the docs environment, not by reading it. flows/overview.md — the "no integrator loaded" demonstration could not fail. `docs/make.jl` loads OrdinaryDiffEq for the whole site and a Julia extension stays armed for the session, so the `try` block succeeded and the page rendered a fully constructed OptimalControlFlow where it promised an ExtensionError. No page in this build can demonstrate it, so the block becomes an inert transcript captured in a session that loads OptimalControl and NLPModelsIpopt and nothing else, plus a note explaining why it is inert so it is not turned back into an `@example` later. The claim itself holds: `Flow(ocp, law)` throws `CTBase.Exceptions.ExtensionError` at construction time, naming the `using` to add. results/plot.md — the page said a flow call "only accepts variable/unsafe (and augment, for costate augmentation)". `augment` does not exist; CTFlows.jl/src/Flows/calling.jl:92 declares variable, unsafe, variable_costate. The migration page says so twice, and `augment=` is on the banned-spelling list in the cahier des charges §8.2. flows/accessors.md — eight lines of `import CTFlows.Systems:` with `# hide` on every one, needed before #868 re-exported all seven accessors. The page was quietly working around a re-export it should have been demonstrating. Removed; the built page now returns 0.5 from `hamiltonian(f)` with no import at all. migration.md — `LiftedHamiltonianFunction` was spelled two ways eighty-eight lines apart on the same page. Line 51 now matches line 139, the four geometry pages, and the test suite. The transcript further down keeps `CTLie.` because that is verbatim what src/deprecated.jl prints. Co-Authored-By: Claude Opus 5 --- docs/src/flows/accessors.md | 8 -------- docs/src/flows/overview.md | 33 +++++++++++++++++++++++---------- docs/src/migration.md | 2 +- docs/src/results/plot.md | 6 +++--- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/docs/src/flows/accessors.md b/docs/src/flows/accessors.md index 50fb52ce0..c52fb08c6 100644 --- a/docs/src/flows/accessors.md +++ b/docs/src/flows/accessors.md @@ -12,14 +12,6 @@ the map of what you can pull back out, and from which kind of flow. using OptimalControl using OrdinaryDiffEqTsit5 using NLPModelsIpopt -import CTFlows.Systems: # hide - hamiltonian, # hide - hamiltonian_vector_field, # hide - vector_field, # hide - get_hamiltonian_gradient, # hide - get_variable_gradient, # hide - get_pseudo_hamiltonian_gradient, # hide - get_pseudo_variable_gradient # hide nothing # hide ``` diff --git a/docs/src/flows/overview.md b/docs/src/flows/overview.md index f5c1fa287..79adc44fa 100644 --- a/docs/src/flows/overview.md +++ b/docs/src/flows/overview.md @@ -68,6 +68,7 @@ Every flow needs an ODE integrator, and none is a hard dependency — load one, ```@example main using OptimalControl +using OrdinaryDiffEqTsit5 using NLPModelsIpopt t0 = 0 @@ -84,20 +85,32 @@ ocp = @def begin 0.5∫(u(t)^2) → min end -try - Flow(ocp, (x, p) -> p[2]) -catch e - println(e) -end -``` - -```@example main -using OrdinaryDiffEqTsit5 f = Flow(ocp, (x, p) -> p[2]) nothing # hide ``` -Every page in this section opens with `using OrdinaryDiffEqTsit5` — no exceptions. +Every page in this section opens with `using OrdinaryDiffEqTsit5` — no exceptions. Forget it +and `Flow` says so at construction time, naming the `using` to add: + +```julia +julia> f = Flow(ocp, (x, p) -> p[2]) +ERROR: ExtensionError → top-level scope, REPL[6]:1 +│ +│ missing dependencies to access SciML options metadata +│ +│ Missing OrdinaryDiffEqTsit5 +│ +│ Context Load OrdinaryDiffEqTsit5, OrdinaryDiffEq, or DifferentialEquations to activate the CTSolversSciMLIntegrator extension. +│ Hint Run: using OrdinaryDiffEqTsit5 +└─ +``` + +!!! note "Why that block is not executed" + + The documentation build loads `OrdinaryDiffEq` once, for the whole site, and a Julia + extension stays armed for the rest of the session — so no page here can demonstrate this + failure live. The transcript above comes from a session loading `OptimalControl` and + `NLPModelsIpopt` and nothing else. ## Choosing an integrator and its options diff --git a/docs/src/migration.md b/docs/src/migration.md index df05e55fb..7e13fbeed 100644 --- a/docs/src/migration.md +++ b/docs/src/migration.md @@ -48,7 +48,7 @@ never write a flow. See [Installation](@ref getting-started-installation) and | `OptimalControl.VectorField`, `OptimalControl.Hamiltonian`, … | `VectorField`, `Hamiltonian`, … | exported at top level since v2.1.0-beta; the qualified form still works, it's just no longer necessary | | `Lie(X, f)` | `ad(X, f)` | renamed; `Lie(...)` now throws | | `X ⋅ f` | `ad(X, f)` | removed, no operator alias; `X ⋅ f` now throws | -| `HamiltonianLift` | `Lift(f)` to build; `CTLie.LiftedHamiltonianFunction` to name the type | renamed **and** re-parented — see [What changed meaning silently](@ref migration-silent) | +| `HamiltonianLift` | `Lift(f)` to build; `OptimalControl.LiftedHamiltonianFunction` to name the type | renamed **and** re-parented — see [What changed meaning silently](@ref migration-silent) | | `autonomous=`, `variable=`, `inplace=` (constructor keywords) | `is_autonomous=`, `is_variable=`, `is_inplace=` | prefixed, on `VectorField`, `Hamiltonian`, `@Lie`, and friends | | `Flow(f)` with `f::Function` | `Flow(VectorField(f))`, `Flow(Hamiltonian(f))`, `Flow(HamiltonianVectorField(f))` | the bare function no longer says what kind of flow to build | | `Flow(ocp, u, g, μ)` (3 positional) | `Flow(ocp, u; constraint=g, multiplier=μ)` | keywords, and they come as a pair | diff --git a/docs/src/results/plot.md b/docs/src/results/plot.md index 11de3edc2..6f4796daa 100644 --- a/docs/src/results/plot.md +++ b/docs/src/results/plot.md @@ -294,9 +294,9 @@ time_grid(sol_flow) ``` For a denser plot, pass `saveat` **when constructing the flow**, not on the call — the call -itself only accepts `variable`/`unsafe` (and `augment`, for costate augmentation). `dense=false` -is required alongside `saveat`, since dense output and `saveat` conflict at the integrator -level: +itself only accepts `variable`/`unsafe` (and `variable_costate`, for costate augmentation). +`dense=false` is required alongside `saveat`, since dense output and `saveat` conflict at the +integrator level: ```@example main fine_grid = range(t0, tf, 100) From f92ad5dca0caf331267f837ee50853920025a727 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sun, 23 Aug 2026 19:13:19 +0200 Subject: [PATCH 2/2] docs: give choosing-a-method a problem, and correct its scheme table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The page defined no `ocp` anywhere, and both demonstrations wrap their call in try/catch, so the missing binding was swallowed and published as the result. The rendered page read "…so this raises AmbiguousDescription rather than silently picking one:" followed by UndefVarError(:ocp, …), and again eighty lines down under "confirmed live". Give the page a running example. It is a visible `@example`, not a hidden `@setup`: a hidden binding would keep the page from being copy-pasted, which moves the UndefVarError from the built page to the reader's REPL rather than removing it. The dynamics are written coordinatewise so both the `:adnlp` and the `:exa` modeler accept the problem. With the page executing, both outputs could be read for the first time, and the scheme table turned out to be wrong on four rows rather than one. Sweeping all ten schemes against both modelers, each cell obtained by actually solving: - `:exa` accepts only :trapeze, :midpoint, :euler and :euler_implicit. The aliases are rejected where their canonical form is accepted — :euler_forward fails where :euler succeeds, :euler_backward where :euler_implicit succeeds — though under :adnlp the two name one scheme. The table presented them as interchangeable. - `:variable` is rejected under both modelers. It dispatches to CTDirect.VariableStepODE, whose implementation is not compiled in (CTDirect.jl/src/CTDirect.jl:63 has the include commented out), so a solve raises UndefVarError rather than any typed error. The table becomes a scheme × modeler matrix. `:variable` stays in it, marked unavailable on both sides, because `describe(:collocation)` runs ten lines above on the same page and still advertises it — removing the row would leave two adjacent blocks contradicting each other. The Gauss-Legendre demonstration silences ExaModels' `ExaCore()` deprecation warning, visibly and with a comment. This page is the first in the site to run the `:exa` modeler live — solve/gpu.md is Draft = true and solve/options.md's :exa blocks are inert — so without it the published page would carry a warning and the build machine's package path. Co-Authored-By: Claude Opus 5 --- docs/src/solve/choosing-a-method.md | 63 +++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/docs/src/solve/choosing-a-method.md b/docs/src/solve/choosing-a-method.md index 0b96c588e..66cde76aa 100644 --- a/docs/src/solve/choosing-a-method.md +++ b/docs/src/solve/choosing-a-method.md @@ -30,6 +30,25 @@ There are 12 methods: every `{adnlp, exa} × {ipopt, madnlp, uno, madncl, knitro This list is not fixed prose to memorize — it is exactly what `methods()` returns, so it's printed live rather than quoted as a number anywhere on this page. +## A problem to try them on + +Every `solve` call on this page uses the same problem — the double integrator, with its +dynamics written coordinatewise so that both the `:adnlp` and the `:exa` modeler accept it: + +```@example main +ocp = @def begin + t ∈ [0, 1], time + x = (q, v) ∈ R², state + u ∈ R, control + x(0) == [-1, 0] + x(1) == [0, 0] + ∂(q)(t) == v(t) + ∂(v)(t) == u(t) + ∫(0.5u(t)^2) → min +end +nothing # hide +``` + ## Partial descriptions `solve(ocp, :madnlp)` doesn't need the other three tokens — they're completed for you. @@ -112,30 +131,50 @@ describe(:cpu) `:collocation` accepts a `scheme` option (alias `disc_method`): -| Value | Notes | -| --- | --- | -| `:trapeze` | second-order | -| `:midpoint` | second-order, **default** | -| `:euler`, `:euler_explicit`, `:euler_forward` | first-order, explicit | -| `:euler_implicit`, `:euler_backward` | first-order, implicit | -| `:gauss_legendre_2` | fourth-order, **`:adnlp` modeler only** | -| `:gauss_legendre_3` | sixth-order, **`:adnlp` modeler only** | -| `:variable` | variable-step ODE-based discretization | +| Value | Notes | `:adnlp` | `:exa` | +| --- | --- | :-: | :-: | +| `:trapeze` | second-order | ✅ | ✅ | +| `:midpoint` | second-order, **default** | ✅ | ✅ | +| `:euler` | first-order, explicit | ✅ | ✅ | +| `:euler_implicit` | first-order, implicit | ✅ | ✅ | +| `:euler_explicit`, `:euler_forward` | aliases of `:euler` | ✅ | ✗ | +| `:euler_backward` | alias of `:euler_implicit` | ✅ | ✗ | +| `:gauss_legendre_2` | fourth-order | ✅ | ✗ | +| `:gauss_legendre_3` | sixth-order | ✅ | ✗ | +| `:variable` | variable-step, ODE-based | ✗ | ✗ | plus `grid_size` (default `250`) or an explicit, possibly non-uniform, `time_grid`. -!!! warning "Gauss-Legendre schemes are `:adnlp`-only" +Every cell above was checked by solving with that scheme. Two of the results need spelling out. + +!!! warning "Under `:exa`, only the four canonical names work" - `:gauss_legendre_2`/`:gauss_legendre_3` are rejected under `:exa` — confirmed live: + `:exa` takes `:trapeze`, `:midpoint`, `:euler` and `:euler_implicit`, and nothing else. + Both Gauss-Legendre schemes are out — and so are the aliases, which is the surprising part: + `:euler_forward` is rejected where `:euler` is accepted, though under `:adnlp` the two name + the same scheme. Confirmed live: ```@example main + using Logging + # ExaModels warns about a deprecated `ExaCore()` call on every model it + # builds; silenced here so the scheme error is the only output. try - solve(ocp, :exa; scheme=:gauss_legendre_2, display=false) + with_logger(NullLogger()) do + solve(ocp, :exa; scheme=:gauss_legendre_2, display=false) + end catch e println(e) end ``` +!!! warning "`:variable` is advertised but does not run" + + The `describe(:collocation)` output above lists `:variable`, and the registry accepts it, + but no modeler can use it: the scheme dispatches to `CTDirect.VariableStepODE`, whose + implementation is not compiled into CTDirect, so a solve raises + `UndefVarError(:VariableStepODE, …, CTDirect)` rather than any typed error. It is kept in + the table only because the registry listing above advertises it. + ## Advanced: the strategy registry `strategy_ids`, `type_from_id`, and `available_parameters` (and the [`create_registry`](@ref)