From 98c7ac0e25ffa5889ec7ee92b6226bf11abbffb2 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 3 Sep 2026 00:29:36 +0200 Subject: [PATCH 01/57] =?UTF-8?q?docs:=20review=20index.md=20=E2=80=94=20l?= =?UTF-8?q?ogo,=20dark=20mode,=20environment=20download,=20prose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First page of the page-by-page documentation review. index.md - Motivation: one dense paragraph split into four. - Basic usage: a transition line before the "each step has its own guide" list. - Mathematical formulation: the Mayer term reworded as a boundary/pointwise cost on the initial and final times and states and the optimisation variables, not just the final state. - Testing: one dense paragraph split into three. - Reproducibility: dropped the `_downloads_toml` helper and three raw-HTML `
` blocks (one emoji used twice); now a lead paragraph, a two-row table for the two project files, and one `::: details` block holding versioninfo / Pkg.status / Pkg.status(manifest). Logo - `logo.png` -> `logo.svg` (cropped artwork from .extras/logos/logo-gagnant/). - Home page dark mode: an exact light/dark SVG pair. `docs/make.jl` generates `logo-dark.svg` from `logo.svg` (white backdrop -> #1b1b1f, Julia colours unchanged), git-ignored; two with a plain display toggle. - Navbar: single `/logo.svg` recoloured in dark mode by a `.dark` filter (24px, the ~10% desaturation is imperceptible; a light/dark pair double-renders in the DocumenterVitepress navbar title). Environment download (fixes a live 404 on the dev site) - VitePress does not bundle `.toml` referenced from a Markdown link, so the Project.toml / Manifest.toml download links have been 404 on `dev` since the DocumenterVitepress migration. `docs/make.jl` now copies each into `src/public/assets/` (served verbatim) as well as `src/assets/` (Documenter's link check). `src/public/` is git-ignored. Dark-mode figures - `.dark .vp-doc img[alt=""] { filter: invert(0.894) hue-rotate(180deg) }` in style.css: a = 1 - 27/255 lands white on the VitePress dark surface. Catches every local `![]` image (DVP drops their alt); exclude one with a `@raw html` `...`. Plan and per-page log: .reports/documentation-review-2026-09.md (git-ignored). Co-Authored-By: Claude Sonnet 5 --- .gitignore | 4 + docs/make.jl | 29 +++++-- docs/src/.vitepress/theme/style.css | 29 +++++++ docs/src/assets/logo.png | Bin 109478 -> 0 bytes docs/src/assets/logo.svg | 113 ++++++++++++++++++++++++++++ docs/src/examples/logo.md | 4 +- docs/src/index.md | 110 ++++++++++++++++----------- 7 files changed, 239 insertions(+), 50 deletions(-) delete mode 100644 docs/src/assets/logo.png create mode 100644 docs/src/assets/logo.svg diff --git a/.gitignore b/.gitignore index 80dd31e0a..aa986afda 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,10 @@ deps/src/ # Build artifacts for creating documentation generated by the Documenter package docs/build/ docs/site/ +# docs/src/public/ is populated by docs/make.jl (TOML snapshots + logo) for VitePress +docs/src/public/ +# logo-dark.svg is generated from logo.svg by docs/make.jl +docs/src/assets/logo-dark.svg # -------------------- # Julia Environment Files diff --git a/docs/make.jl b/docs/make.jl index 67e85ea63..995c807e7 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -112,15 +112,32 @@ end # ═══════════════════════════════════════════════════════════════════════════════ # Assets for reproducibility # ═══════════════════════════════════════════════════════════════════════════════ +# Each TOML is copied twice: into `src/assets/` so Documenter's link checker +# resolves the `assets/…` download links (no `warnonly` since #952), and into +# `src/public/assets/` so VitePress actually serves the file — it does not bundle +# `.toml`, and DocumenterVitepress's assets→public copy only handles logo/favicon. +# Creating `src/public/` also makes DVP skip that logo copy, so the logo is staged +# into `src/public/` here as well. `src/public/` is git-ignored (build output); +# `src/assets/Manifest.toml` stays tracked (the reviewable snapshot). mkpath(joinpath(@__DIR__, "src", "assets")) -cp( - joinpath(@__DIR__, "Manifest.toml"), - joinpath(@__DIR__, "src", "assets", "Manifest.toml"); - force=true, +mkpath(joinpath(@__DIR__, "src", "public", "assets")) +for f in ("Manifest.toml", "Project.toml") + cp(joinpath(@__DIR__, f), joinpath(@__DIR__, "src", "assets", f); force=true) + cp(joinpath(@__DIR__, f), joinpath(@__DIR__, "src", "public", "assets", f); force=true) +end + +# Logo: `logo.svg` is the tracked light artwork; `logo-dark.svg` is the same file with +# the white backdrop swapped for the VitePress dark surface (#1b1b1f), Julia mark colours +# unchanged — generated here (git-ignored) and Vite-bundled from `src/assets/` for the +# home-page pair. `logo.svg` is also staged into `src/public/` for the navbar (DVP +# fills `logo:` with the single `/logo.svg`; a `.dark` filter recolours it there). +write( + joinpath(@__DIR__, "src", "assets", "logo-dark.svg"), + replace(read(joinpath(@__DIR__, "src", "assets", "logo.svg"), String), "#ffffff" => "#1b1b1f"), ) cp( - joinpath(@__DIR__, "Project.toml"), - joinpath(@__DIR__, "src", "assets", "Project.toml"); + joinpath(@__DIR__, "src", "assets", "logo.svg"), + joinpath(@__DIR__, "src", "public", "logo.svg"); force=true, ) diff --git a/docs/src/.vitepress/theme/style.css b/docs/src/.vitepress/theme/style.css index 0a33c33cf..a434faee3 100644 --- a/docs/src/.vitepress/theme/style.css +++ b/docs/src/.vitepress/theme/style.css @@ -414,3 +414,32 @@ mjx-container svg path { flex: 1 1 100%; } } + +/* ============================================================================ + Dark mode: navbar logo. The home page swaps an exact light/dark SVG pair, but + the navbar logo is a single `` (DocumenterVitepress fills `logo:` with one + src) at 24px, where the filter's ~10% desaturation is imperceptible. Same + `invert(0.894) hue-rotate(180deg)` recipe as the figures below. + ============================================================================ */ +.dark .VPNavBarTitle .VPImage { + filter: invert(0.894) hue-rotate(180deg); +} + +/* ============================================================================ + Dark mode: in-content images rendered on a white ground — figures that are + outputs of Julia `@example` / `@repl` blocks (Plots.jl and CairoMakie, both + forced to SVG in docs/make.jl) and the `examples/logo.md` hero. DocumenterVitepress + renders every *local* image (`![…](file)` and captured plot output alike) with an + empty `alt`, so `img[alt=""]` catches them all; an image that must NOT be flipped + (a photo, a screenshot) is written as `…` in a `@raw html` block — + the `rocket-def.png` pattern in modelling/with-ai.md — and keeps its real `alt`. + `invert(a)` maps white to `255·(1−a)`; a = 1 − 27/255 ≈ 0.894 lands it on the + VitePress dark surface (#1b1b1f). `hue-rotate(180deg)` leaves that neutral grey + untouched (its matrix rows sum to 1) and only rotates the coloured marks back + toward their hue — approximately; data colours come out ~10% flatter, an accepted + trade-off (see .reports/documentation-review-2026-09.md option A). The logo uses an + exact light/dark SVG pair instead (docs/make.jl, config.mts) and is not touched here. + ============================================================================ */ +.dark .vp-doc img[alt=""] { + filter: invert(0.894) hue-rotate(180deg); +} diff --git a/docs/src/assets/logo.png b/docs/src/assets/logo.png deleted file mode 100644 index 706dde78b4c66328e034a7469dd036d63b8b5e5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109478 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%0D#L%KY`DJTR==(hY!mr~jsMDmB_ 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%0AbAofI0GmG%m+-+ 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 + + + + + + + + + + + + + + + + diff --git a/docs/src/examples/logo.md b/docs/src/examples/logo.md index 49f9c7f3e..5f90192f0 100644 --- a/docs/src/examples/logo.md +++ b/docs/src/examples/logo.md @@ -4,7 +4,7 @@ The OptimalControl.jl logo is not a drawing. It is the solution of an optimal co problem — an **energy-minimal low-thrust orbit transfer** — solved once and repeated by symmetry. -![The OptimalControl.jl logo](../assets/logo.png) +![The OptimalControl.jl logo](../assets/logo.svg) A spacecraft spirals from a low circular orbit out to a higher one under continuous weak thrust. Two-body dynamics do not care which way is up, so the same optimal transfer, rotated @@ -168,7 +168,7 @@ scatter!(ax, Point2f(0, 0); color = jl.blue, markersize = 100) fig ``` -That is the figure the site uses as its logo (`docs/src/assets/logo.png`). +That is the figure the site uses as its logo (`docs/src/assets/logo.svg`). The published asset is produced by a slightly more elaborate script — crisper strokes, a few tuning knobs — kept in the repository at `.extras/logos/logo-gagnant/`. The optimal control diff --git a/docs/src/index.md b/docs/src/index.md index 02c216f58..60ecc7045 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -2,9 +2,37 @@ The OptimalControl.jl package is the root package of the [control-toolbox ecosystem](https://github.com/control-toolbox). The control-toolbox ecosystem gathers Julia packages for mathematical control and applications. It aims to provide tools to model and solve optimal control problems with ordinary differential equations by direct and indirect methods, both on CPU and GPU. +```@raw html + + + +``` + ## Motivation -The guiding philosophy of OptimalControl.jl is to offer, to our knowledge, the only Julia package that unifies both direct and indirect methods for optimal control within a single, coherent framework. This fills a gap in a landscape where existing tools are fragmented across programming languages and paradigms, and are usually restricted to a single family of methods. The package provides a domain-specific language that closely matches mathematical notation, together with multiple discretization schemes and shooting methods, and planned support for homotopy continuation methods. Its modeler–solver separation makes it agnostic to the underlying NLP modeling backend and optimization solver, and enables seamless execution on both CPU and GPU with minimal user intervention. Combined with an ecosystem of domain-specific applications, tutorials, and benchmarking tools, this design targets researchers and engineers working in optimal control, control theorists developing new algorithms, and students learning the field through interactive tutorials. +To our knowledge, OptimalControl.jl is the only Julia package that unifies both direct +and indirect methods for optimal control within a single, coherent framework. This fills +a gap in a landscape where existing tools are fragmented across programming languages and +paradigms, and are usually restricted to a single family of methods. + +The package provides a domain-specific language that closely matches mathematical +notation, together with multiple discretization schemes and shooting methods, and planned +support for homotopy continuation methods. + +Its modeler–solver separation makes it agnostic to the underlying NLP modeling backend +and optimization solver, and enables seamless execution on both CPU and GPU with minimal +user intervention. + +Combined with an ecosystem of domain-specific applications, tutorials, and benchmarking +tools, this design targets researchers and engineers working in optimal control, control +theorists developing new algorithms, and students learning the field through interactive +tutorials. ## Installation @@ -34,7 +62,9 @@ sol = solve(ocp) plot(sol) ``` -- For more details, see the [energy minimisation example](@ref examples-double-integrator-energy). +That is the whole program — model, solve, plot. Each step has its own guide: + +- For more details about the example, see the [energy minimisation page](@ref examples-double-integrator-energy). - The `@def` macro defines the problem. See the [abstract syntax guide](@ref modelling-abstract-syntax). - The `solve` function has many options. See the [solve overview](@ref solve-overview). - The `plot` function is flexible. See the [plot guide](@ref results-plot). @@ -49,15 +79,17 @@ plot(sol) | [Results](@ref results-solution) | Read a `Solution` — trajectories, costate, duals, status — plot it, save it, reload it. | | [Flows (indirect)](@ref flows-overview) | The Pontryagin Maximum Principle as code: build, integrate, and shoot with Hamiltonian flows. | | [Geometry](@ref geometry-overview) | The Lie-theoretic tools (`Lift`, `ad`, `Poisson`, `@Lie`) behind singular-control problems. | -| [Examples](@ref examples-gallery) | Six worked problems, direct and indirect, from energy minimisation to state constraints. | +| [Examples](@ref examples-gallery) | A gallery of complete problems worked end to end, direct and indirect, from energy minimisation to state constraints. | | [API reference](@ref api-modelling) | Every re-exported symbol, organised by theme. | | [Migrating to v2.1](@ref migration) | What changed since v2.0 and how to update your code. | ## Mathematical formulation -Optimal control problems are stated in Bolza form — a cost functional combining a terminal -(Mayer) and an integral (Lagrange) term, subject to dynamics and box/path/boundary -constraints, with optionally free times and extra optimisation variables. See +Optimal control problems are stated in Bolza form — a cost functional combining a boundary +term (Mayer), a pointwise cost evaluated on the initial and final times and states and on +the optimisation variables, and an integral term (Lagrange) accumulated along the +trajectory. The problem is subject to dynamics and box/path/boundary constraints, with +optionally free times and extra optimisation variables. See [Formulation](@ref modelling-formulation) for the full mathematical setting. ## Citing us @@ -92,63 +124,57 @@ If you want to ask a question, feel free to start a discussion [here](https://gi ## Testing -OptimalControl.jl is the umbrella package of a multi-repository ecosystem, and testing is organized in layers. Each sub-package (`CTBase`, `CTParser`, `CTModels`, `CTDirect`, `CTSolvers`, `CTFlows`) has its own test suite combining unit tests, integration tests, and code-quality checks (e.g. with [Aqua.jl](https://github.com/JuliaTesting/Aqua.jl)). At the umbrella level, OptimalControl.jl adds strong end-to-end integration tests that solve complete optimal control problems with both direct and indirect methods. Continuous integration runs on Linux, macOS, and Windows, on both CPU and GPU (via a self-hosted CUDA runner), through reusable workflows centralized in [CTActions](https://github.com/control-toolbox/CTActions). Code coverage is tracked on [Codecov](https://codecov.io), downstream packages are guarded against regressions through dedicated breakage tests, and beta versions are distributed during development via a local registry, [ct-registry](https://github.com/control-toolbox/ct-registry). Part of the test code is written with the help of AI agents, always under human review. +OptimalControl.jl is the umbrella package of a multi-repository ecosystem, and testing is +organized in layers. Each sub-package (`CTBase`, `CTDirect`, `CTFlows`, `CTModels`, `CTLie`, +`CTSolvers`, `CTParser`) has its own test suite combining unit tests, integration +tests, and code-quality checks (e.g. with [Aqua.jl](https://github.com/JuliaTesting/Aqua.jl)). +At the umbrella level, OptimalControl.jl adds strong end-to-end integration tests that +solve complete optimal control problems with both direct and indirect methods. + +Continuous integration runs on Linux, macOS, and Windows, on both CPU and GPU (via a +self-hosted CUDA runner), through reusable workflows centralized in +[CTActions](https://github.com/control-toolbox/CTActions). Code coverage is tracked on +[Codecov](https://codecov.io), and downstream packages are guarded against regressions +through dedicated breakage tests. + +Beta versions are distributed during development via a local registry, +[ct-registry](https://github.com/control-toolbox/ct-registry). Part of the test code is +written with the help of AI agents, always under human review. ## Reproducibility ```@setup main using Pkg using InteractiveUtils -using Markdown - -# Download links for the benchmark environment -function _downloads_toml(DIR) - link_manifest = joinpath("assets", DIR, "Manifest.toml") - link_project = joinpath("assets", DIR, "Project.toml") - return Markdown.parse(""" - Download the exact environment used to build these docs: - - 📦 [Project.toml]($link_project) - Package dependencies - - 📋 [Manifest.toml]($link_manifest) - Full dependency tree - """) -end ``` -```@example main -_downloads_toml(".") # hide -``` +Every page on this site executes its code when the documentation is built, against a +single pinned environment. You can inspect that environment below, or download it and +rebuild it locally. -```@raw html -
ℹ️ Version info -``` +| Download | Contents | +| --- | --- | +| [`Project.toml`](assets/Project.toml) | the packages this documentation depends on directly | +| [`Manifest.toml`](assets/Manifest.toml) | the exact version of every package in the resolved dependency tree | + +::: details Environment used to build this documentation + +**Julia version and operating system** ```@example main versioninfo() # hide ``` -```@raw html -
-``` - -```@raw html -
📦 Package status -``` +**Direct dependencies** ```@example main Pkg.status() # hide ``` -```@raw html -
-``` - -```@raw html -
📚 Complete manifest -``` +**Full dependency tree** ```@example main Pkg.status(; mode = PKGMODE_MANIFEST) # hide ``` -```@raw html -
-``` +::: From 0d6a20fea3a24bd87adf24e5ce5f4ca40730f4f8 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 8 Oct 2026 22:08:33 +0200 Subject: [PATCH 02/57] docs: mark installation page as published Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- docs/make.jl | 2 +- docs/src/getting-started/installation.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index 995c807e7..341de73ac 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -154,7 +154,7 @@ cp( Draft = true ``` =# -draft = false +draft = true # ═══════════════════════════════════════════════════════════════════════════════ # Load extensions diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index ead799570..5a7adf5b6 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -1,5 +1,9 @@ # [Installation](@id getting-started-installation) +```@meta +Draft = false +``` + ## Install Open Julia's [interactive session (REPL)](https://docs.julialang.org/en/v1/manual/getting-started) From 47568a8292a5e9d6b07463aad4acdd44a567c9a5 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 8 Oct 2026 22:28:44 +0200 Subject: [PATCH 03/57] docs: update citation to JOSS article Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- README.template.md | 21 +++++++++++++-------- docs/src/index.md | 19 ++++++++++++------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/README.template.md b/README.template.md index 0e971b529..ee1ca957d 100644 --- a/README.template.md +++ b/README.template.md @@ -51,17 +51,22 @@ OptimalControl.jl is the umbrella package of a multi-repository ecosystem with a If you use OptimalControl.jl in your work, please cite us: -> Caillau, J.-B., Cots, O., Gergaud, J., Martinon, P., & Sed, S. *OptimalControl.jl: a Julia package to model and solve optimal control problems with ODE's* [Computer software]. https://doi.org/10.5281/zenodo.13336563 +> Caillau, J.-B., Cots, O., Gergaud, J., Martinon, P., & Sed, S. *OptimalControl.jl: a Julia package to model and solve optimal control problems with ODEs*. Journal of Open Source Software, 11(126), 10630, [doi.org/10.21105/joss.10630](https://doi.org/10.21105/joss.10630) -or in BibTeX format: +or in bibtex format: ```bibtex -@software{OptimalControl_jl, -author = {Caillau, Jean-Baptiste and Cots, Olivier and Gergaud, Joseph and Martinon, Pierre and Sed, Sophia}, -doi = {10.5281/zenodo.13336563}, -license = {["MIT"]}, -title = {{OptimalControl.jl: a Julia package to model and solve optimal control problems with ODE's}}, -url = {https://control-toolbox.org/OptimalControl.jl} +@article{Caillau2026, + doi = {10.21105/joss.10630}, + url = {https://doi.org/10.21105/joss.10630}, + year = {2026}, + publisher = {The Open Journal}, + volume = {11}, + number = {126}, + pages = {10630}, + author = {Caillau, Jean-Baptiste and Cots, Olivier and Gergaud, Joseph and Martinon, Pierre and Sed, Sophia}, + title = {OptimalControl.jl: a Julia package to model and solve optimal control problems with ODEs}, + journal = {Journal of Open Source Software} } ``` diff --git a/docs/src/index.md b/docs/src/index.md index 60ecc7045..631a23236 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -96,17 +96,22 @@ optionally free times and extra optimisation variables. See If you use OptimalControl.jl in your work, please cite us: -> Caillau, J.-B., Cots, O., Gergaud, J., Martinon, P., & Sed, S. *OptimalControl.jl: a Julia package to model and solve optimal control problems with ODE's*. [doi.org/10.5281/zenodo.13336563](https://doi.org/10.5281/zenodo.13336563) +> Caillau, J.-B., Cots, O., Gergaud, J., Martinon, P., & Sed, S. *OptimalControl.jl: a Julia package to model and solve optimal control problems with ODEs*. Journal of Open Source Software, 11(126), 10630, [doi.org/10.21105/joss.10630](https://doi.org/10.21105/joss.10630) or in bibtex format: ```bibtex -@software{OptimalControl_jl, -author = {Caillau, Jean-Baptiste and Cots, Olivier and Gergaud, Joseph and Martinon, Pierre and Sed, Sophia}, -doi = {10.5281/zenodo.13336563}, -license = {MIT}, -title = {{OptimalControl.jl: a Julia package to model and solve optimal control problems with ODE's}}, -url = {https://control-toolbox.org/OptimalControl.jl} +@article{Caillau2026, + doi = {10.21105/joss.10630}, + url = {https://doi.org/10.21105/joss.10630}, + year = {2026}, + publisher = {The Open Journal}, + volume = {11}, + number = {126}, + pages = {10630}, + author = {Caillau, Jean-Baptiste and Cots, Olivier and Gergaud, Joseph and Martinon, Pierre and Sed, Sophia}, + title = {OptimalControl.jl: a Julia package to model and solve optimal control problems with ODEs}, + journal = {Journal of Open Source Software} } ``` From ed8cc9b91dc279a663bbee79c9f24b5ccb311bdb Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 8 Oct 2026 22:48:56 +0200 Subject: [PATCH 04/57] docs: refine installation guide navigation and prose Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- docs/make.jl | 6 +- docs/src/assets/Manifest.toml | 483 ++++++++++++----------- docs/src/getting-started/installation.md | 84 +--- 3 files changed, 275 insertions(+), 298 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 341de73ac..159fadfcb 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -69,7 +69,9 @@ Base.showable(::MIME"image/png", ::CairoMakie.Makie.Figure) = false isempty(str) || push!(rendered_contents, "{" * str * "}") end final_contents = join(rendered_contents, ",\n") - collapse = sidenav === Val(:sidebar) ? "collapsed: true," : "" + is_sidebar = sidenav === Val(:sidebar) + is_getting_started = lowercase(strip(name)) == "getting started" + collapse = is_sidebar && !is_getting_started ? "collapsed: true," : "" return "text: '$(replace(name, "'" => "\\'"))', $collapse items: [\n$(final_contents)\n]" end @@ -151,7 +153,7 @@ cp( # the rest execute, add to its `@meta` block: #= ```@meta -Draft = true +Draft = false ``` =# draft = true diff --git a/docs/src/assets/Manifest.toml b/docs/src/assets/Manifest.toml index 2bc8b3bc7..4a78ef759 100644 --- a/docs/src/assets/Manifest.toml +++ b/docs/src/assets/Manifest.toml @@ -5,10 +5,16 @@ manifest_format = "2.0" project_hash = "a14c68496fdaa17672e818216e5678891cf15975" [[deps.ADNLPModels]] -deps = ["ADTypes", "ForwardDiff", "LinearAlgebra", "NLPModels", "Requires", "ReverseDiff", "SparseArrays", "SparseConnectivityTracer", "SparseMatrixColorings"] -git-tree-sha1 = "c51dc1d4eeb37a40f6aecdd08e5cdcddd8332726" +deps = ["ADTypes", "DifferentiationInterface", "ForwardDiff", "LinearAlgebra", "NLPModels", "ReverseDiff", "SparseArrays", "SparseConnectivityTracer", "SparseMatrixColorings"] +git-tree-sha1 = "614e92a848b8d0e644c63779ba4a3a66909dd465" uuid = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a" -version = "0.8.13" +version = "0.8.14" + + [deps.ADNLPModels.extensions] + ADNLPModelsEnzymeExt = "Enzyme" + + [deps.ADNLPModels.weakdeps] + Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" [[deps.ADTypes]] deps = ["PrecompileTools"] @@ -24,9 +30,9 @@ weakdeps = ["ChainRulesCore", "ConstructionBase", "EnzymeCore"] [[deps.AMD]] deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse_jll"] -git-tree-sha1 = "45a1272e3f809d36431e57ab22703c6896b8908f" +git-tree-sha1 = "344f3d221a6bee75925b5c97a30cfd870f12a138" uuid = "14f7f29c-3bd6-536c-9a0b-7339e30b5a3e" -version = "0.5.3" +version = "0.5.4" [[deps.ANSIColoredPrinters]] git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" @@ -34,10 +40,10 @@ uuid = "a4c015fc-c6ff-483c-b24f-f7ea428134e9" version = "0.0.1" [[deps.ASL_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "6252039f98492252f9e47c312c8ffda0e3b9e78d" +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "21a22cd1a6d45034933824665cfb874ec425a14b" uuid = "ae81ac8f-d209-56e5-92de-9978fef736f9" -version = "0.1.3+0" +version = "0.1.5+0" [[deps.AbstractFFTs]] deps = ["LinearAlgebra"] @@ -81,9 +87,9 @@ version = "0.1.45" [[deps.Adapt]] deps = ["LinearAlgebra"] -git-tree-sha1 = "daa72978cd7a624246e894a4f4f067706d4e17e2" +git-tree-sha1 = "dee8388a9bec89c8713ad176f96960ae2f98598a" uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" -version = "4.7.0" +version = "4.7.4" weakdeps = ["SparseArrays", "StaticArrays"] [deps.Adapt.extensions] @@ -113,9 +119,9 @@ version = "1.1.2" [[deps.ArrayInterface]] deps = ["Adapt", "LinearAlgebra"] -git-tree-sha1 = "daf5b2aab5b1c1fdcb65b05883cdb4b18abac1b9" +git-tree-sha1 = "1aec1ff0dcaa83a484e7f72097562fa3102c6722" uuid = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" -version = "7.30.1" +version = "7.30.2" [deps.ArrayInterface.extensions] ArrayInterfaceAMDGPUExt = "AMDGPU" @@ -156,9 +162,9 @@ version = "1.11.0" [[deps.Atomix]] deps = ["UnsafeAtomics"] -git-tree-sha1 = "b8651b2eb5796a386b0398a20b519a6a6150f75c" +git-tree-sha1 = "1bbcf97714051f302b3beafab65bcdeb6044c4df" uuid = "a9b6321e-bd34-4604-b9c9-b65b8de01458" -version = "1.1.3" +version = "1.2.1" [deps.Atomix.extensions] AtomixCUDAExt = "CUDA" @@ -192,9 +198,9 @@ version = "0.4.8" [[deps.BFloat16s]] deps = ["LinearAlgebra", "Printf", "Random"] -git-tree-sha1 = "e386db8b4753b42caac75ac81d0a4fe161a68a97" +git-tree-sha1 = "e9f53743fbe41a7e794d58f79d7a13699d9748fa" uuid = "ab4f0b2a-ad5b-11e8-123f-65d77653426b" -version = "0.6.1" +version = "0.6.2" [[deps.Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" @@ -213,9 +219,9 @@ version = "1.1.0" [[deps.BracketingNonlinearSolve]] deps = ["CommonSolve", "ConcreteStructs", "NonlinearSolveBase", "PrecompileTools", "Reexport", "SciMLBase", "SciMLLogging"] -git-tree-sha1 = "7cc44d707d20997aefcfc2800d0c6d6b4155a1c2" +git-tree-sha1 = "b1b04c236ff7ed92e69d64b620448b3da00933d4" uuid = "70df07ce-3d50-431d-a3e7-ca6ddb60ac1e" -version = "1.12.6" +version = "1.12.8" weakdeps = ["ChainRulesCore", "ForwardDiff"] [deps.BracketingNonlinearSolve.extensions] @@ -251,9 +257,9 @@ version = "1.0.1+0" [[deps.CTBase]] deps = ["ADTypes", "DocStringExtensions"] -git-tree-sha1 = "0e2f90f2a5d3767c0ce2ded664351781b9f68b0c" +git-tree-sha1 = "b09e22a9bb8884cbc416e5d258e40d609abc1e0b" uuid = "54762871-cc72-4466-b8e8-f6c8b58076cd" -version = "0.30.4-beta" +version = "0.30.5" [deps.CTBase.extensions] CTBaseDifferentiationInterface = ["DifferentiationInterface"] @@ -275,15 +281,15 @@ version = "0.30.4-beta" [[deps.CTDirect]] deps = ["ADNLPModels", "CTBase", "CTModels", "CTParser", "CTSolvers", "DocStringExtensions", "ExaModels", "SolverCore", "SparseArrays", "SparseConnectivityTracer"] -git-tree-sha1 = "f88e02b5c7102b24efa7e89c558c6d8fc67015e5" +git-tree-sha1 = "38f6fbb168b844b7ceac71678b42c96daa20bba5" uuid = "790bbbee-bee9-49ee-8912-a9de031322d5" -version = "1.1.6-beta" +version = "1.1.8-beta" [[deps.CTFlows]] deps = ["ADTypes", "CTBase", "CTModels", "CTSolvers", "CommonSolve", "DocStringExtensions", "GPUArraysCore", "RecipesBase"] -git-tree-sha1 = "ebd5a2f6b860f423091e523325d8e2b1146b25ce" +git-tree-sha1 = "d82116ac4932b8494e7f6f3deb312dce44753969" uuid = "1c39547c-7794-42f7-af83-d98194f657c2" -version = "0.18.0-beta" +version = "0.18.1" weakdeps = ["Makie", "Plots", "SciMLBase", "StaticArrays"] [deps.CTFlows.extensions] @@ -295,15 +301,15 @@ weakdeps = ["Makie", "Plots", "SciMLBase", "StaticArrays"] [[deps.CTLie]] deps = ["CTBase", "DocStringExtensions", "MacroTools"] -git-tree-sha1 = "be205ccc6abefea748b0989a2d634a1e5a025f90" +git-tree-sha1 = "049bbb24dfa8b5d54cff2736352d76ce9e7782ae" uuid = "6880e05b-3a7d-4cac-887c-30cb52c5fdde" -version = "0.2.2-beta" +version = "0.2.3" [[deps.CTModels]] deps = ["CTBase", "DocStringExtensions", "LinearAlgebra", "MLStyle", "MacroTools", "OrderedCollections", "Parameters", "RecipesBase"] -git-tree-sha1 = "007472ded070a4ebfa4dedec8074608a139225f1" +git-tree-sha1 = "d652e0b512a6e2b526047f04335ccf3a61a66331" uuid = "34c4fa32-2049-4079-8329-de33c2a22e2d" -version = "0.19.4-beta" +version = "0.19.5" weakdeps = ["JLD2", "JSON3", "Makie", "Plots"] [deps.CTModels.extensions] @@ -324,9 +330,9 @@ weakdeps = ["ExaModels", "LinearAlgebra"] [[deps.CTSolvers]] deps = ["CTBase", "CTModels", "CommonSolve", "DocStringExtensions", "KernelAbstractions", "SolverCore"] -git-tree-sha1 = "78c93a3c95f3df76d07174fe0c8219a50d23c5b3" +git-tree-sha1 = "3c1574071021d1930a0a67c5478c8b92b1d5caf4" uuid = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" -version = "0.5.6-beta" +version = "0.5.7" [deps.CTSolvers.extensions] CTSolversADNLPModels = "ADNLPModels" @@ -437,15 +443,15 @@ version = "1.1.1" [[deps.CairoMakie]] deps = ["CRC32c", "Cairo", "Cairo_jll", "Colors", "FileIO", "FreeType", "GeometryBasics", "LinearAlgebra", "Makie", "PrecompileTools"] -git-tree-sha1 = "47142129b1777e21da58cff265050b10d8560588" +git-tree-sha1 = "1cda0b7d5abfc95357dae18aca934d401f7869ad" uuid = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" -version = "0.15.13" +version = "0.15.15" [[deps.Cairo_jll]] deps = ["Artifacts", "Bzip2_jll", "CompilerSupportLibraries_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "JLLWrappers", "Libdl", "Pixman_jll", "Xorg_libXext_jll", "Xorg_libXrender_jll", "Zlib_jll", "libpng_jll"] -git-tree-sha1 = "1fa950ebc3e37eccd51c6a8fe1f92f7d86263522" +git-tree-sha1 = "7b841680738c19948120f6e4cf8d0200518bd564" uuid = "83423d85-b0ee-5818-9007-b63ccbeb887a" -version = "1.18.7+0" +version = "1.18.8+0" [[deps.ChainRulesCore]] deps = ["Compat", "LinearAlgebra"] @@ -500,9 +506,9 @@ version = "3.31.0" [[deps.ColorTypes]] deps = ["FixedPointNumbers", "Random"] -git-tree-sha1 = "67e11ee83a43eb71ddc950302c53bf33f0690dfe" +git-tree-sha1 = "61761f58648aa7217445f24f841839b78c712232" uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" -version = "0.12.1" +version = "0.12.3" weakdeps = ["StyledStrings"] [deps.ColorTypes.extensions] @@ -519,10 +525,10 @@ weakdeps = ["SpecialFunctions"] SpecialFunctionsExt = "SpecialFunctions" [[deps.Colors]] -deps = ["ColorTypes", "FixedPointNumbers", "Reexport"] -git-tree-sha1 = "37ea44092930b1811e666c3bc38065d7d87fcc74" +deps = ["ColorTypes", "FixedPointNumbers", "LinearAlgebra", "Reexport"] +git-tree-sha1 = "291665b547f137df070e4dd83e432b5fee8cc4a0" uuid = "5ae59095-9a9b-59fe-a467-6f913c188581" -version = "0.13.1" +version = "0.13.2" [[deps.CommonSolve]] deps = ["PrecompileTools"] @@ -634,9 +640,9 @@ version = "1.16.2+0" [[deps.DelaunayTriangulation]] deps = ["AdaptivePredicates", "EnumX", "ExactPredicates", "Random"] -git-tree-sha1 = "c55f5a9fd67bdbc8e089b5a3111fe4292986a8e8" +git-tree-sha1 = "4ac548adcad90c1d5d677af13568a748af4c952b" uuid = "927a84f5-c5f4-47a5-9785-b46e178433df" -version = "1.6.6" +version = "1.6.7" [[deps.DelimitedFiles]] deps = ["Mmap"] @@ -646,9 +652,9 @@ version = "1.9.1" [[deps.DiffEqBase]] deps = ["ArrayInterface", "BracketingNonlinearSolve", "ConcreteStructs", "DocStringExtensions", "FastBroadcast", "FastClosures", "FastPower", "FunctionWrappers", "FunctionWrappersWrappers", "LinearAlgebra", "Logging", "Markdown", "MuladdMacro", "PrecompileTools", "Printf", "RecursiveArrayTools", "Reexport", "RespecializeParams", "SciMLBase", "SciMLLogging", "SciMLOperators", "SciMLStructures", "Setfield", "StaticArraysCore", "SymbolicIndexingInterface", "TruncatedStacktraces"] -git-tree-sha1 = "58facb456fa8438fcac1321e28824ceb44b6c5a9" +git-tree-sha1 = "4e3938f036cf93f3c6acf7a7008848675d3d130f" uuid = "2b5f629d-d688-5b77-993f-72d75c75574e" -version = "7.20.0" +version = "7.21.3" [deps.DiffEqBase.extensions] DiffEqBaseCUDAExt = "CUDA" @@ -800,9 +806,9 @@ version = "1.1.0" [[deps.DocumenterVitepress]] deps = ["ANSIColoredPrinters", "Base64", "DocInventories", "DocStringExtensions", "Documenter", "IOCapture", "JSON", "Markdown", "NodeJS_20_jll", "REPL", "TOML"] -git-tree-sha1 = "2f3c16a8886bcba40c4cb0dd0fd131dd93afc397" +git-tree-sha1 = "2f8462b3ada00461a19659a65e05608822737708" uuid = "4710194d-e776-4893-9690-8d956a29c365" -version = "0.3.5" +version = "0.3.7" [deps.DocumenterVitepress.extensions] DocumenterVitepressDocumenterCitationsExt = "DocumenterCitations" @@ -827,9 +833,9 @@ uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56" version = "1.0.7" [[deps.EnzymeCore]] -git-tree-sha1 = "971d7831cc85f43bc9f51d615a3f7f21270c2f1d" +git-tree-sha1 = "90ab232666984c3eafa9d14cc85f8ff51cf6c353" uuid = "f151be2c-9106-41f4-ab19-57ee4f262869" -version = "0.8.21" +version = "0.8.22" weakdeps = ["Adapt", "ChainRulesCore"] [deps.EnzymeCore.extensions] @@ -844,9 +850,9 @@ version = "0.0.20230411+1" [[deps.ExaModels]] deps = ["Adapt", "NLPModels", "Printf", "SolverCore"] -git-tree-sha1 = "4768a744999b18de3727364984f327cf98f2cbaa" +git-tree-sha1 = "cd710c6a3e85efd6bcbedab66e03ee549c4b6f29" uuid = "1037b233-b668-4ce9-9b63-f9f681f55dd2" -version = "0.12.0" +version = "0.12.1" [deps.ExaModels.extensions] ExaModelsJuMP = "JuMP" @@ -875,9 +881,9 @@ version = "2.2.9" [[deps.Expat_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "f4d39eee89f1e58c26bf447f1d4156c0125d6838" +git-tree-sha1 = "2bfb1e047e2ad0a5ca94365340bde8005d637568" uuid = "2e619515-83b5-522b-bb60-26c02a35a201" -version = "2.8.3+0" +version = "2.8.4+0" [[deps.ExprTools]] git-tree-sha1 = "d2e49e7efd29719d6f28b891b0e0e159daa9d2b4" @@ -886,15 +892,15 @@ version = "0.1.11" [[deps.FFMPEG]] deps = ["FFMPEG_jll"] -git-tree-sha1 = "95ecf07c2eea562b5adbd0696af6db62c0f52560" +git-tree-sha1 = "7bd13840b4148949e290748071bbe6826b3eabcd" uuid = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" -version = "0.4.5" +version = "0.4.6" [[deps.FFMPEG_jll]] deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "JLLWrappers", "LAME_jll", "Libdl", "Ogg_jll", "OpenSSL_jll", "Opus_jll", "PCRE2_jll", "Zlib_jll", "libaom_jll", "libass_jll", "libfdk_aac_jll", "libva_jll", "libvorbis_jll", "x264_jll", "x265_jll"] -git-tree-sha1 = "7a58e45171b63ed4782f2d36fdee8713a469e6e0" +git-tree-sha1 = "d9d3cd382f2c999f684e6bfa4039fcc16c153b6f" uuid = "b22a6f82-2f65-5046-a5b2-351ab43fb4e5" -version = "8.1.2+0" +version = "9.0.2+0" [[deps.FFTA]] deps = ["AbstractFFTs", "DocStringExtensions", "LinearAlgebra", "MuladdMacro", "Primes", "Random", "Reexport"] @@ -988,9 +994,9 @@ version = "1.11.0" [[deps.FillArrays]] deps = ["LinearAlgebra"] -git-tree-sha1 = "5bad39456d9f0166184fce2248783dd9862645c1" +git-tree-sha1 = "086b5fbd032baf544678cc15b52b015e4c4aceb8" uuid = "1a297f60-69ca-5386-bcde-b61e274b549b" -version = "1.17.0" +version = "1.17.1" weakdeps = ["PDMats", "SparseArrays", "StaticArrays", "Statistics"] [deps.FillArrays.extensions] @@ -1001,9 +1007,17 @@ weakdeps = ["PDMats", "SparseArrays", "StaticArrays", "Statistics"] [[deps.FindFirstFunctions]] deps = ["PrecompileTools"] -git-tree-sha1 = "bba475ce782fc77777f539cf0b7c029207798db1" +git-tree-sha1 = "dcce413ffacb0be72f38b491ae4887730343d451" uuid = "64ca27bc-2ba2-4a57-88aa-44e436879224" -version = "3.2.1" +version = "3.4.0" + + [deps.FindFirstFunctions.extensions] + FindFirstFunctionsEnzymeCoreExt = "EnzymeCore" + FindFirstFunctionsMooncakeExt = "Mooncake" + + [deps.FindFirstFunctions.weakdeps] + EnzymeCore = "f151be2c-9106-41f4-ab19-57ee4f262869" + Mooncake = "da2b9cff-9c12-43a0-ae48-6db2b0edb7d6" [[deps.FiniteDiff]] deps = ["ArrayInterface", "LinearAlgebra", "Setfield"] @@ -1042,9 +1056,9 @@ version = "1.3.7" [[deps.ForwardDiff]] deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "LinearAlgebra", "LogExpFunctions", "NaNMath", "Preferences", "Printf", "Random", "SpecialFunctions"] -git-tree-sha1 = "1b86cca764a61dcac4fef4c5e16e378e5ed6953c" +git-tree-sha1 = "3b0f72e2ffef1a139ac450725c9ecd01c0a3c050" uuid = "f6369f11-7733-5829-9624-2563aa707210" -version = "1.4.5" +version = "1.4.6" weakdeps = ["StaticArrays"] [deps.ForwardDiff.extensions] @@ -1107,9 +1121,9 @@ version = "3.5.1+0" [[deps.GPUArrays]] deps = ["Adapt", "GPUArraysCore", "KernelAbstractions", "LLVM", "LinearAlgebra", "Printf", "Random", "Reexport", "ScopedValues", "Serialization", "SparseArrays", "Statistics"] -git-tree-sha1 = "b6d227f1620b8e2dd8be1717687f173532cde1de" +git-tree-sha1 = "574e443e2545f8622b716676c9358ca8a7ea57b4" uuid = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7" -version = "11.5.13" +version = "11.5.16" weakdeps = ["JLD2"] [deps.GPUArrays.extensions] @@ -1117,9 +1131,9 @@ weakdeps = ["JLD2"] [[deps.GPUArraysCore]] deps = ["Adapt"] -git-tree-sha1 = "83cf05ab16a73219e5f6bd1bdfa9848fa24ac627" +git-tree-sha1 = "0b2b3e8b8fb7c1f5ddbf498be2eb2b3b37c9c0ed" uuid = "46192b85-c4d5-4398-a991-12ede77f4527" -version = "0.2.0" +version = "0.2.1" [[deps.GPUCompiler]] deps = ["ExprTools", "InteractiveUtils", "LLVM", "Libdl", "Logging", "PrecompileTools", "Preferences", "REPL", "Scratch", "Serialization", "TOML", "Tracy", "UUIDs"] @@ -1164,9 +1178,9 @@ version = "1.2.0" [[deps.GeometryBasics]] deps = ["EarCut_jll", "LinearAlgebra", "PrecompileTools", "Random", "StaticArrays"] -git-tree-sha1 = "592cfb5ed8b02804f6a9c04091571c393081f73a" +git-tree-sha1 = "ec46c5825710fa1a15d468acb2d93cc939a7a5fe" uuid = "5c1252a2-5f33-56bf-86c9-59e7332b4326" -version = "0.5.12" +version = "0.5.13" [deps.GeometryBasics.extensions] ExtentsExt = "Extents" @@ -1192,9 +1206,9 @@ version = "9.55.1+0" [[deps.Giflib_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "6570366d757b50fabae9f4315ad74d2e40c0560a" +git-tree-sha1 = "a3efbbc027441271444dcd0c0a46f2d119dc4329" uuid = "59f7168a-df46-5410-90c8-f2779963d0ec" -version = "5.2.3+0" +version = "6.1.3+0" [[deps.Git]] deps = ["Git_LFS_jll", "Git_jll", "JLLWrappers", "OpenSSH_jll"] @@ -1210,9 +1224,9 @@ version = "3.7.1+0" [[deps.Git_jll]] deps = ["Artifacts", "Expat_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "Libiconv_jll", "OpenSSL_jll", "PCRE2_jll", "Zlib_jll"] -git-tree-sha1 = "7b16700f9e313c0d972d1222ede50a2076aa0770" +git-tree-sha1 = "3842d761162baf8efce819cdfb5ea070fe44ac40" uuid = "f8c6e375-362e-5223-8a59-34ff63f689eb" -version = "2.55.0+0" +version = "2.56.0+0" [[deps.Glib_jll]] deps = ["Artifacts", "GettextRuntime_jll", "JLLWrappers", "Libdl", "Libffi_jll", "Libiconv_jll", "Libmount_jll", "PCRE2_jll", "Zlib_jll"] @@ -1234,9 +1248,9 @@ version = "1.3.16+0" [[deps.GridLayoutBase]] deps = ["GeometryBasics", "InteractiveUtils", "Observables"] -git-tree-sha1 = "93d5c27c8de51687a2c70ec0716e6e76f298416f" +git-tree-sha1 = "ef70da5e123a06a29e2d6ddff0f09985bc226491" uuid = "3955a311-db13-416c-9275-1d80ed98e5e9" -version = "0.11.2" +version = "0.11.3" [[deps.HSL_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] @@ -1245,16 +1259,16 @@ uuid = "017b0a0e-03f4-516a-9b91-836bbd1904dd" version = "4.0.8+0" [[deps.HTTP]] -deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"] -git-tree-sha1 = "b0ff6627f1308bc1691528aac3b70ac3abd1d598" +deps = ["Base64", "CodecZlib", "Dates", "EnumX", "Logging", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"] +git-tree-sha1 = "ed18c529d170d8431c9d91537520b6d3dec052ca" uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" -version = "2.6.7" +version = "2.9.0" [[deps.HarfBuzz_jll]] deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "Graphite2_jll", "JLLWrappers", "Libdl", "Libffi_jll"] -git-tree-sha1 = "c3f99f8e7c98031b8845ece9db86dca713ab9bf8" +git-tree-sha1 = "9d9531a9cb63a9edc33836414e82a07e81710de2" uuid = "2e76f6c2-a576-52d4-95c1-20adfe4de566" -version = "100.14003.0+0" +version = "100.14004.0+0" [[deps.HashArrayMappedTries]] git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae" @@ -1263,15 +1277,15 @@ version = "0.2.0" [[deps.HiGHS_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Zlib_jll", "libblastrampoline_jll"] -git-tree-sha1 = "5b9e398d6e4ed7e1f9a801a6f50ecca92dab0e10" +git-tree-sha1 = "d4e63f395d10590fcece8d35005dd7b9a4862635" uuid = "8fd58aa0-07eb-5a78-9b36-339c94fd15ea" -version = "1.15.0+0" +version = "1.15.1+3" [[deps.Hwloc_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "XML2_jll", "Xorg_libpciaccess_jll"] -git-tree-sha1 = "c35847ca5b4997fc8418836354a56c459bcf48d8" +git-tree-sha1 = "cbbd99d516990d05c02aa132aa077b2bf50ae323" uuid = "e33a78d0-f292-5ffc-b300-72abe9b543c8" -version = "2.14.0+0" +version = "2.15.0+0" [[deps.HypergeometricFunctions]] deps = ["Gamma", "LinearAlgebra"] @@ -1360,9 +1374,9 @@ weakdeps = ["ForwardDiff", "Unitful"] [[deps.IntervalArithmetic]] deps = ["CRlibm", "CoreMath", "MacroTools", "OpenBLASConsistentFPCSR_jll", "Printf", "Random", "RoundingEmulator"] -git-tree-sha1 = "ff294afb9a15d31d8d7422da138844641a73135f" +git-tree-sha1 = "1c531bf0f8a5c60a340926e058fd3f209b5eef5d" uuid = "d1acc4aa-44c8-5952-acd4-ba5d80a2a253" -version = "1.0.11" +version = "1.0.12" [deps.IntervalArithmetic.extensions] IntervalArithmeticArblibExt = "Arblib" @@ -1371,6 +1385,7 @@ version = "1.0.11" IntervalArithmeticIntervalSetsExt = "IntervalSets" IntervalArithmeticIrrationalConstantsExt = "IrrationalConstants" IntervalArithmeticLinearAlgebraExt = "LinearAlgebra" + IntervalArithmeticMakieExt = "Makie" IntervalArithmeticRecipesBaseExt = "RecipesBase" IntervalArithmeticSparseArraysExt = "SparseArrays" @@ -1381,16 +1396,18 @@ version = "1.0.11" IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" IrrationalConstants = "92d709cd-6900-40b7-9082-c6be49f344b6" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [[deps.IntervalSets]] -git-tree-sha1 = "79d6bd28c8d9bccc2229784f1bd637689b256377" +git-tree-sha1 = "0db6aea5b64caa1c47e9fdd27394f0296f05e8bf" uuid = "8197267c-284f-5f27-9208-e0e47529a953" -version = "0.7.14" -weakdeps = ["Random", "RecipesBase", "Statistics"] +version = "0.7.15" +weakdeps = ["Makie", "Random", "RecipesBase", "Statistics"] [deps.IntervalSets.extensions] + IntervalSetsMakieExt = "Makie" IntervalSetsRandomExt = "Random" IntervalSetsRecipesBaseExt = "RecipesBase" IntervalSetsStatisticsExt = "Statistics" @@ -1407,9 +1424,9 @@ weakdeps = ["Dates", "Test"] [[deps.Ipopt]] deps = ["Ipopt_jll", "LinearAlgebra", "OpenBLAS32_jll", "PrecompileTools"] -git-tree-sha1 = "f8443766032a81e1f2cddfd4f624a5650067f0d0" +git-tree-sha1 = "d5a4fb77d030c56258a33a7046195506055ce5cc" uuid = "b6b21f68-93f8-5de0-b562-5493be1d77c9" -version = "1.15.0" +version = "1.16.0" [deps.Ipopt.extensions] IpoptMathOptInterfaceExt = "MathOptInterface" @@ -1445,10 +1462,10 @@ uuid = "82899510-4779-5014-852e-03e436cf321d" version = "1.0.0" [[deps.JLD2]] -deps = ["ChunkCodecLibZlib", "ChunkCodecLibZstd", "FileIO", "MacroTools", "Mmap", "OrderedCollections", "PrecompileTools", "ScopedValues"] -git-tree-sha1 = "877edc1d2f51adcef0bfacd19464a19e7cfddddb" +deps = ["ChunkCodecCore", "ChunkCodecLibZlib", "ChunkCodecLibZstd", "FileIO", "MacroTools", "Mmap", "OrderedCollections", "PrecompileTools", "ScopedValues"] +git-tree-sha1 = "9ce2e7c49ae4a7035b7d60db3a553b1f6c16875d" uuid = "033835bb-8acc-5ee8-8aae-3f567f8a3819" -version = "0.6.6" +version = "0.6.7" weakdeps = ["UnPack"] [deps.JLD2.extensions] @@ -1468,9 +1485,9 @@ version = "1.8.0" [[deps.JSON]] deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"] -git-tree-sha1 = "c7345ab1a7ca4dc8a02c9f6510da0d9857bbe513" +git-tree-sha1 = "4657a834b01ce00f2b94546095c4350617b9af2c" uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "1.7.1" +version = "1.10.0" [deps.JSON.extensions] JSONArrowExt = ["ArrowTypes"] @@ -1533,9 +1550,9 @@ version = "16.0.0" [[deps.KernelAbstractions]] deps = ["Adapt", "Atomix", "InteractiveUtils", "MacroTools", "PrecompileTools", "Requires", "StaticArrays", "UUIDs"] -git-tree-sha1 = "a5b87110fa95d711355af44832497745aa93fb52" +git-tree-sha1 = "0d0172d662154a2d8e0150c265e75579029f8baf" uuid = "63c18a36-062a-441e-b654-da1e3ab1ce7c" -version = "0.9.42" +version = "0.9.44" weakdeps = ["EnzymeCore", "LinearAlgebra", "SparseArrays"] [deps.KernelAbstractions.extensions] @@ -1551,9 +1568,9 @@ version = "0.6.12" [[deps.Krylov]] deps = ["LinearAlgebra", "Printf", "SparseArrays"] -git-tree-sha1 = "71e740d00d71cdb15145d7fe0d6000ec70534598" +git-tree-sha1 = "909890a0d1486c392128cf4bd6e038ee0c6a7af4" uuid = "ba0b0d4f-ebba-5204-a429-3ac8c609bfb7" -version = "0.10.9" +version = "0.10.10" [[deps.LAME_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] @@ -1569,9 +1586,9 @@ version = "0.10.2" [[deps.LERC_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "17b94ecafcfa45e8360a4fc9ca6b583b049e4e37" +git-tree-sha1 = "39bca05343661c347aae0bca57a5994a0bf4f08d" uuid = "88015f11-f218-50d7-93a8-a6af411a945d" -version = "4.1.0+0" +version = "4.2.0+0" [[deps.LHLFactorization]] deps = ["LinearAlgebra", "PrecompileTools"] @@ -1590,9 +1607,9 @@ version = "2.2.2" [[deps.LLVM]] deps = ["CEnum", "LLVMExtra_jll", "Libdl", "PrecompileTools", "Preferences", "Printf", "Unicode"] -git-tree-sha1 = "d4bfee24427f4f441bd9212a107e375c39663aab" +git-tree-sha1 = "3bcfa5cd59aecfc0680d8b5b8ea38237b009f112" uuid = "929cbde3-209d-540e-8aea-75f648917ca0" -version = "9.13.1" +version = "9.13.2" weakdeps = ["BFloat16s"] [deps.LLVM.extensions] @@ -1611,9 +1628,9 @@ version = "1.0.0" [[deps.LLVMOpenMP_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "b7970cef8ae1c990ba0c09cd8bdc1145e006632f" +git-tree-sha1 = "e5b100780d4d30d63b4618d7930d48af409c1772" uuid = "1d63c593-3942-5779-bab2-d838dc0a180e" -version = "22.1.7+0" +version = "23.1.1+0" [[deps.LaTeXStrings]] git-tree-sha1 = "f88f3ccef05a6a72a0cf0ed417c8fd68530f4ab2" @@ -1726,9 +1743,9 @@ version = "2.42.0+0" [[deps.LineSearch]] deps = ["ADTypes", "CommonSolve", "ConcreteStructs", "FastClosures", "LinearAlgebra", "MaybeInplace", "PrecompileTools", "SciMLBase", "SciMLJacobianOperators", "StaticArraysCore"] -git-tree-sha1 = "e3f00a2de8e2faf41381f583bd92ce8ebdff5c22" +git-tree-sha1 = "678bdf03a97059e018401d4482385548d61a3c73" uuid = "87fe0de2-c867-4266-b59a-2f0a94fc965b" -version = "0.1.16" +version = "0.1.19" [deps.LineSearch.extensions] LineSearchLineSearchesExt = "LineSearches" @@ -1743,9 +1760,9 @@ version = "1.12.0" [[deps.LinearOperators]] deps = ["FastClosures", "LinearAlgebra", "Printf", "SparseArrays", "TimerOutputs"] -git-tree-sha1 = "74f1bd5c1afef5a2d24a2b7917d3cb4b989a3ec6" +git-tree-sha1 = "99a44fc6443bfe118cfe64ca059136bc85e64ccb" uuid = "5c8ed15e-5a4c-59e4-a42b-c7e8811fb125" -version = "2.14.2" +version = "2.14.3" [deps.LinearOperators.extensions] LinearOperatorsAMDGPUExt = "AMDGPU" @@ -1769,9 +1786,9 @@ version = "2.14.2" [[deps.LinearSolve]] deps = ["AMD", "ArrayInterface", "ConcreteStructs", "DocStringExtensions", "EnumX", "GPUArraysCore", "InteractiveUtils", "Krylov", "LHLFactorization", "Libdl", "LinearAlgebra", "MKL_jll", "Markdown", "OpenBLAS_jll", "PrecompileTools", "Preferences", "PureKLU", "RecursiveArrayTools", "Reexport", "SciMLBase", "SciMLLogging", "SciMLOperators", "SciMLStructures", "Setfield", "SparseArrays", "SparseColumnPivotedQR", "StaticArraysCore"] -git-tree-sha1 = "ecebca90f0b3b123a04c1230daf4dd4a6e2f72c0" +git-tree-sha1 = "308c25a9f0606d5f17770e21dc485fcac7df62cf" uuid = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae" -version = "5.15.1" +version = "5.18.2" [deps.LinearSolve.extensions] LinearSolveAMDGPUExt = "AMDGPU" @@ -1812,6 +1829,7 @@ version = "5.15.1" LinearSolvePureUMFPACKExt = "PureUMFPACK" LinearSolveReactantExt = "Reactant" LinearSolveRecursiveFactorizationExt = ["RecursiveFactorization", "TriangularSolve"] + LinearSolveSLATEExt = "SLATE_jll" LinearSolveSTRUMPACKExt = "STRUMPACK_jll" LinearSolveSparspakExt = "Sparspak" LinearSolveSpecializingFactorizationsExt = "SpecializingFactorizations" @@ -1857,6 +1875,7 @@ version = "5.15.1" PureUMFPACK = "b7e1f0a2-3c4d-4e5f-9a0b-1c2d3e4f5a6b" Reactant = "3c362404-f566-11ee-1572-e11a4b42c853" RecursiveFactorization = "f2c3362d-daeb-58d1-803e-2bc74f2840b4" + SLATE_jll = "cb01ff19-587c-516c-bcf9-532d49da298d" STRUMPACK_jll = "86fbd0b9-476f-557c-b766-62c724b42d8c" SparseMatricesCSR = "a0a7dd2c-ebf4-11e9-1f05-cf50bc540ca1" Sparspak = "e56a9233-b9d6-4f03-8d0f-1825330902ac" @@ -1880,9 +1899,9 @@ version = "1.6.0" [[deps.LogExpFunctions]] deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"] -git-tree-sha1 = "bba2d9aa057d8f126415de240573e86a8f39d2a1" +git-tree-sha1 = "b85e2797b2409570e84c4de46238c0ed5f6476ae" uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" -version = "1.0.1" +version = "1.0.2" [deps.LogExpFunctions.extensions] LogExpFunctionsChainRulesCoreExt = "ChainRulesCore" @@ -1906,9 +1925,9 @@ version = "1.2.0" [[deps.METIS_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "2eefa8baa858871ae7770c98c3c2a7e46daba5b4" +git-tree-sha1 = "5ecd92439c60a50c49bd6e96192e9d486b24b050" uuid = "d00139f3-1899-568f-a2f0-47f597d42d70" -version = "5.1.3+0" +version = "5.1.4+0" [[deps.MIMEs]] git-tree-sha1 = "c64d943587f7187e751162b3b84445bbbd79f691" @@ -1979,9 +1998,9 @@ version = "0.10.2" [[deps.Makie]] deps = ["Animations", "Base64", "CRC32c", "ColorBrewer", "ColorSchemes", "ColorTypes", "Colors", "ComputePipeline", "Contour", "Dates", "DelaunayTriangulation", "Distributions", "DocStringExtensions", "Downloads", "FFMPEG_jll", "FileIO", "FilePaths", "FixedPointNumbers", "Format", "FreeType", "FreeTypeAbstraction", "GeometryBasics", "GridLayoutBase", "ImageBase", "ImageIO", "InteractiveUtils", "Interpolations", "IntervalSets", "InverseFunctions", "Isoband", "KernelDensity", "LaTeXStrings", "LinearAlgebra", "MacroTools", "Markdown", "MathTeXEngine", "Observables", "OffsetArrays", "PNGFiles", "Packing", "Pkg", "PlotUtils", "PolygonOps", "PrecompileTools", "Printf", "REPL", "Random", "RelocatableFolders", "Scratch", "ShaderAbstractions", "SignedDistanceFields", "SparseArrays", "Statistics", "StatsBase", "StatsFuns", "StructArrays", "TriplotBase", "UnicodeFun", "Unitful"] -git-tree-sha1 = "f2c8715d05bf10f9d4dc354e69dee30b6be53239" +git-tree-sha1 = "5f6f5d1b1fb7ff98c9a083bbfd4c661a9808e758" uuid = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" -version = "0.24.13" +version = "0.24.15" [deps.Makie.extensions] MakieDynamicQuantitiesExt = "DynamicQuantities" @@ -2144,9 +2163,9 @@ version = "20.12.2+0" [[deps.NonlinearSolve]] deps = ["ADTypes", "ArrayInterface", "BracketingNonlinearSolve", "CommonSolve", "ConcreteStructs", "DifferentiationInterface", "FastClosures", "FiniteDiff", "ForwardDiff", "LineSearch", "LinearAlgebra", "LinearSolve", "NonlinearSolveBase", "NonlinearSolveFirstOrder", "NonlinearSolveQuasiNewton", "NonlinearSolveSpectralMethods", "PrecompileTools", "Preferences", "Reexport", "SciMLBase", "Setfield", "SimpleNonlinearSolve", "StaticArraysCore", "SymbolicIndexingInterface"] -git-tree-sha1 = "101bf9821e2b90463eefe811e4a85d57befc5775" +git-tree-sha1 = "90afae5b2b33dcd1098e4c1a7b6066f18741bf6f" uuid = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" -version = "4.29.1" +version = "4.32.0" [deps.NonlinearSolve.extensions] NonlinearSolveFastLevenbergMarquardtExt = "FastLevenbergMarquardt" @@ -2177,9 +2196,9 @@ version = "4.29.1" [[deps.NonlinearSolveBase]] deps = ["ADTypes", "Adapt", "ArrayInterface", "CommonSolve", "Compat", "ConcreteStructs", "DifferentiationInterface", "EnumX", "EnzymeCore", "FastClosures", "FunctionWrappers", "FunctionWrappersWrappers", "LinearAlgebra", "LogExpFunctions", "Markdown", "MaybeInplace", "PreallocationTools", "PrecompileTools", "Preferences", "Printf", "RecursiveArrayTools", "RespecializeParams", "SciMLBase", "SciMLJacobianOperators", "SciMLLogging", "SciMLOperators", "SciMLStructures", "Setfield", "StaticArraysCore", "SymbolicIndexingInterface", "TimerOutputs"] -git-tree-sha1 = "eccf99477b471404368ef816a971fb57ae1234fa" +git-tree-sha1 = "f4f5ebf8bfd9bfafdc4116fb3715dfd50fe4a7bd" uuid = "be0214bd-f91f-a760-ac4e-3421ce2b2da0" -version = "2.48.0" +version = "2.54.1" [deps.NonlinearSolveBase.extensions] NonlinearSolveBaseBandedMatricesExt = "BandedMatrices" @@ -2188,7 +2207,7 @@ version = "2.48.0" NonlinearSolveBaseForwardDiffExt = "ForwardDiff" NonlinearSolveBaseLineSearchExt = "LineSearch" NonlinearSolveBaseLinearSolveExt = "LinearSolve" - NonlinearSolveBaseMooncakeExt = "Mooncake" + NonlinearSolveBaseMooncakeExt = ["ChainRulesCore", "Mooncake"] NonlinearSolveBaseReverseDiffExt = "ReverseDiff" NonlinearSolveBaseSparseArraysExt = "SparseArrays" NonlinearSolveBaseSparseMatrixColoringsExt = "SparseMatrixColorings" @@ -2208,10 +2227,10 @@ version = "2.48.0" Tracker = "9f7883ad-71c0-57eb-9f7f-b5c9e6d3789c" [[deps.NonlinearSolveFirstOrder]] -deps = ["ADTypes", "ArrayInterface", "CommonSolve", "ConcreteStructs", "FiniteDiff", "ForwardDiff", "LineSearch", "LinearAlgebra", "LinearSolve", "MaybeInplace", "NonlinearSolveBase", "PrecompileTools", "Reexport", "SciMLBase", "SciMLJacobianOperators", "SciMLLogging", "Setfield", "StaticArraysCore"] -git-tree-sha1 = "17fd2e849e3a280e25c9634c35c48cf0f322ec0f" +deps = ["ADTypes", "ArrayInterface", "CommonSolve", "ConcreteStructs", "FiniteDiff", "ForwardDiff", "LineSearch", "LinearAlgebra", "LinearSolve", "MaybeInplace", "NonlinearSolveBase", "PrecompileTools", "Reexport", "SciMLBase", "SciMLJacobianOperators", "SciMLLogging", "SciMLOperators", "Setfield", "Sobol", "StaticArraysCore"] +git-tree-sha1 = "95a105b647b656dfbd7985ede8eb0eaf4b836505" uuid = "5959db7a-ea39-4486-b5fe-2dd0bf03d60d" -version = "2.5.0" +version = "2.10.0" [[deps.NonlinearSolveQuasiNewton]] deps = ["ArrayInterface", "CommonSolve", "ConcreteStructs", "LinearAlgebra", "LinearSolve", "MaybeInplace", "NonlinearSolveBase", "PrecompileTools", "Reexport", "SciMLBase", "SciMLLogging", "SciMLOperators", "StaticArraysCore"] @@ -2225,9 +2244,9 @@ weakdeps = ["ForwardDiff"] [[deps.NonlinearSolveSpectralMethods]] deps = ["CommonSolve", "ConcreteStructs", "LineSearch", "MaybeInplace", "NonlinearSolveBase", "PrecompileTools", "Reexport", "SciMLBase", "SciMLLogging"] -git-tree-sha1 = "2814dc60afeb19a4908cd1b4fd276bde4a95d540" +git-tree-sha1 = "c3e5a641a867b7f37155a78086cf3f22134c088f" uuid = "26075421-4e9a-44e1-8bd1-420ed7ad02b2" -version = "1.8.1" +version = "1.8.3" weakdeps = ["ForwardDiff"] [deps.NonlinearSolveSpectralMethods.extensions] @@ -2278,9 +2297,9 @@ version = "0.3.3" [[deps.OpenEXR_jll]] deps = ["Artifacts", "Imath_jll", "JLLWrappers", "Libdl", "Zlib_jll"] -git-tree-sha1 = "fd09db52a90efaae33a3d91f0bc71a22c429e8f1" +git-tree-sha1 = "e9ae72527609bb66882d38ad009ffd47ca5713fb" uuid = "18a262bb-aa17-5467-a713-aee519bc75cb" -version = "3.4.14+0" +version = "3.4.16+0" [[deps.OpenLibm_jll]] deps = ["Artifacts", "Libdl"] @@ -2289,9 +2308,9 @@ version = "0.8.7+0" [[deps.OpenSSH_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "OpenSSL_jll", "Zlib_jll"] -git-tree-sha1 = "2da18ab26a6eb38b374c16b157d5a4cc3ab74e02" +git-tree-sha1 = "1f640b82a627df3dbbaaff756a201a9ceb10dd1f" uuid = "9bd350c2-7e96-507f-8002-3f2e150b4e1b" -version = "10.5.1+0" +version = "10.6.1+0" [[deps.OpenSSL_jll]] deps = ["Artifacts", "Libdl"] @@ -2311,9 +2330,9 @@ uuid = "91d4177d-7536-5919-b921-800302f37372" version = "1.6.1+0" [[deps.OrderedCollections]] -git-tree-sha1 = "05f45c2e0de6259db764adbfd2f1dc6d3f8de13c" +git-tree-sha1 = "f9b03759e9ef463718934fbed30820b39997511e" uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -version = "2.0.1" +version = "2.0.2" [[deps.OrdinaryDiffEq]] deps = ["ADTypes", "CommonSolve", "DiffEqBase", "DocStringExtensions", "OrdinaryDiffEqBDF", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "OrdinaryDiffEqRosenbrock", "OrdinaryDiffEqTsit5", "OrdinaryDiffEqVerner", "SciMLBase", "SciMLLogging"] @@ -2323,15 +2342,15 @@ version = "7.8.1" [[deps.OrdinaryDiffEqBDF]] deps = ["ADTypes", "ArrayInterface", "DiffEqBase", "FastBroadcast", "LinearAlgebra", "MacroTools", "MuladdMacro", "OrdinaryDiffEqCore", "OrdinaryDiffEqDifferentiation", "OrdinaryDiffEqNonlinearSolve", "OrdinaryDiffEqSDIRK", "PrecompileTools", "Preferences", "RecursiveArrayTools", "Reexport", "SciMLBase", "TruncatedStacktraces"] -git-tree-sha1 = "aec92fbdb30aad73ec57d37d9688402f18a072c7" +git-tree-sha1 = "42650454ef40bfd71443daa2751c6a6f9aa1120a" uuid = "6ad6398a-0878-4a85-9266-38940aa047c8" -version = "2.4.6" +version = "2.4.12" [[deps.OrdinaryDiffEqCore]] deps = ["ADTypes", "Accessors", "Adapt", "ArrayInterface", "BinaryHeaps", "CommonSolve", "ConstructionBase", "DiffEqBase", "DocStringExtensions", "EnumX", "EnzymeCore", "FastBroadcast", "FastClosures", "FastPower", "FindFirstFunctions", "FunctionWrappers", "FunctionWrappersWrappers", "InteractiveUtils", "LinearAlgebra", "Logging", "MacroTools", "MuladdMacro", "PrecompileTools", "Preferences", "Printf", "Random", "RecursiveArrayTools", "Reexport", "SciMLBase", "SciMLLogging", "SciMLOperators", "SciMLStructures", "SymbolicIndexingInterface", "TruncatedStacktraces"] -git-tree-sha1 = "6cd86bfd733093c7f81e54c110c0a1ac39a21860" +git-tree-sha1 = "2815c99c179c5c9d1a82d63394a975b67ef6b69c" uuid = "bbf590c4-e513-4bbe-9b18-05decba2e5d8" -version = "4.16.0" +version = "4.18.1" [deps.OrdinaryDiffEqCore.extensions] OrdinaryDiffEqCoreMooncakeExt = "Mooncake" @@ -2345,15 +2364,15 @@ version = "4.16.0" [[deps.OrdinaryDiffEqDefault]] deps = ["ADTypes", "DiffEqBase", "EnumX", "LinearAlgebra", "LinearSolve", "OrdinaryDiffEqBDF", "OrdinaryDiffEqCore", "OrdinaryDiffEqRosenbrock", "OrdinaryDiffEqTsit5", "OrdinaryDiffEqVerner", "PrecompileTools", "Preferences", "Reexport", "SciMLBase"] -git-tree-sha1 = "06e73f2843331d77408b9555ad203c154cd748f9" +git-tree-sha1 = "1ce6ba1f52c94059089f835f173fddda90233959" uuid = "50262376-6c5a-4cf5-baba-aaf4f84d72d7" -version = "2.6.0" +version = "2.6.2" [[deps.OrdinaryDiffEqDifferentiation]] deps = ["ADTypes", "ArrayInterface", "ConcreteStructs", "ConstructionBase", "DiffEqBase", "DifferentiationInterface", "FastBroadcast", "FiniteDiff", "ForwardDiff", "FunctionWrappersWrappers", "LinearAlgebra", "LinearSolve", "OrdinaryDiffEqCore", "SciMLBase", "SciMLOperators", "SparseMatrixColorings", "StaticArraysCore"] -git-tree-sha1 = "d44a802aff84db13b6a3cb24d499c95f81776bc5" +git-tree-sha1 = "57ebe5c7130f1def087d4ebc01a4959bba8c7689" uuid = "4302a76b-040a-498a-8c04-15b101fed76b" -version = "3.11.4" +version = "3.12.4" weakdeps = ["SparseArrays"] [deps.OrdinaryDiffEqDifferentiation.extensions] @@ -2361,15 +2380,15 @@ weakdeps = ["SparseArrays"] [[deps.OrdinaryDiffEqNonlinearSolve]] deps = ["ADTypes", "ArrayInterface", "CommonSolve", "ConstructionBase", "DiffEqBase", "FastBroadcast", "FastClosures", "ForwardDiff", "LinearAlgebra", "LinearSolve", "MuladdMacro", "NonlinearSolve", "NonlinearSolveBase", "OrdinaryDiffEqCore", "OrdinaryDiffEqDifferentiation", "PreallocationTools", "RecursiveArrayTools", "SciMLBase", "SciMLOperators", "SciMLPublic", "SimpleNonlinearSolve", "SparseArrays", "StaticArraysCore"] -git-tree-sha1 = "700cd6cb734e5513d6c74435bf9e3a3b73895931" +git-tree-sha1 = "3bc20fcc25eae4d01b2884a1e596af2a14ce7829" uuid = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8" -version = "2.9.4" +version = "2.9.9" [[deps.OrdinaryDiffEqRosenbrock]] deps = ["ADTypes", "ArrayInterface", "DiffEqBase", "DifferentiationInterface", "FastBroadcast", "FiniteDiff", "ForwardDiff", "LinearAlgebra", "LinearSolve", "MacroTools", "MuladdMacro", "OrdinaryDiffEqCore", "OrdinaryDiffEqDifferentiation", "OrdinaryDiffEqRosenbrockTableaus", "PrecompileTools", "Preferences", "RecursiveArrayTools", "Reexport", "SciMLBase"] -git-tree-sha1 = "a82d81b06e0dfe81643a79f0212d0d98d049af4e" +git-tree-sha1 = "4d6354525db068b9d1578389274b7e6cabc663b4" uuid = "43230ef6-c299-4910-a778-202eb28ce4ce" -version = "2.7.1" +version = "2.7.5" [[deps.OrdinaryDiffEqRosenbrockTableaus]] git-tree-sha1 = "bab0d4ccee6fcb89a786b89207774314523e2e5d" @@ -2378,21 +2397,21 @@ version = "2.4.2" [[deps.OrdinaryDiffEqSDIRK]] deps = ["ADTypes", "CommonSolve", "ConstructionBase", "DiffEqBase", "FastBroadcast", "LinearAlgebra", "MacroTools", "MuladdMacro", "OrdinaryDiffEqCore", "OrdinaryDiffEqDifferentiation", "OrdinaryDiffEqNonlinearSolve", "PrecompileTools", "Preferences", "RecursiveArrayTools", "Reexport", "SciMLBase", "TruncatedStacktraces"] -git-tree-sha1 = "cc0d3e36bf6fddbbb6f3b1f807dae8d8e2822b13" +git-tree-sha1 = "693be93138c2e1f13d56a8e4d0114936492dd3db" uuid = "2d112036-d095-4a1e-ab9a-08536f3ecdbf" -version = "2.9.2" +version = "2.9.7" [[deps.OrdinaryDiffEqTsit5]] deps = ["CommonSolve", "DiffEqBase", "FastBroadcast", "LinearAlgebra", "MuladdMacro", "OrdinaryDiffEqCore", "PrecompileTools", "Preferences", "RecursiveArrayTools", "Reexport", "SciMLBase", "TruncatedStacktraces"] -git-tree-sha1 = "2fdf22d85a4c35e913d8b746480167e0206cf0c3" +git-tree-sha1 = "371de3eb5985febd9d7babb0937520653f2b15ca" uuid = "b1df2697-797e-41e3-8120-5422d3b24e4a" -version = "2.1.4" +version = "2.1.5" [[deps.OrdinaryDiffEqVerner]] deps = ["DiffEqBase", "FastBroadcast", "LinearAlgebra", "MuladdMacro", "OrdinaryDiffEqCore", "PrecompileTools", "Preferences", "RecursiveArrayTools", "Reexport", "SciMLBase", "TruncatedStacktraces"] -git-tree-sha1 = "50756dd4eb1a6ae0b3b5f33e5fdfaf1a83e6a8e9" +git-tree-sha1 = "5a66a18325e678f260e849a62840cb4903ece511" uuid = "79d7bb75-1356-48c1-b8c0-6832512096c2" -version = "2.4.1" +version = "2.4.2" [[deps.PCRE2_jll]] deps = ["Artifacts", "Libdl"] @@ -2441,9 +2460,9 @@ version = "0.13.1" [[deps.Parsers]] deps = ["Dates", "PrecompileTools", "UUIDs"] -git-tree-sha1 = "3de8f5e6e90ebfa8d6d1f86997d6cdcd6a912ff3" +git-tree-sha1 = "ba0dc8a8a67cacac4842631f960c046e4e563675" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.8.7" +version = "2.8.8" [[deps.Pixman_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LLVMOpenMP_jll", "Libdl"] @@ -2473,10 +2492,10 @@ uuid = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a" version = "3.3.0" [[deps.PlotUtils]] -deps = ["ColorSchemes", "Colors", "Dates", "PrecompileTools", "Printf", "Random", "Reexport", "StableRNGs", "Statistics"] -git-tree-sha1 = "26ca162858917496748aad52bb5d3be4d26a228a" +deps = ["ColorSchemes", "Colors", "Dates", "PrecompileTools", "Printf", "Reexport", "Statistics"] +git-tree-sha1 = "ddcc36fd83fb4cd459a53c891981697bde0d3de6" uuid = "995b91a9-d308-5afd-9ec6-746e21dbc043" -version = "1.4.4" +version = "1.5.1" [[deps.Plots]] deps = ["Base64", "Contour", "Dates", "Downloads", "FFMPEG", "FixedPointNumbers", "GR", "JLFzf", "JSON", "LaTeXStrings", "Latexify", "LinearAlgebra", "Measures", "NaNMath", "Pkg", "PlotThemes", "PlotUtils", "PrecompileTools", "Printf", "REPL", "Random", "RecipesBase", "RecipesPipeline", "Reexport", "RelocatableFolders", "Requires", "Scratch", "Showoff", "SparseArrays", "Statistics", "StatsBase", "TOML", "UUIDs", "UnicodeFun", "Unzip"] @@ -2523,22 +2542,24 @@ version = "1.3.4" [[deps.Preferences]] deps = ["TOML"] -git-tree-sha1 = "8b770b60760d4451834fe79dd483e318eee709c4" +git-tree-sha1 = "5005266de4bfe50e53ff44a5cb5c540b6e47a254" uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.5.2" +version = "1.6.0" [[deps.PrettyTables]] -deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"] -git-tree-sha1 = "1b8aa19f229b1cea7fc93874a52e49db6a854450" +deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "StyledStrings", "Tables"] +git-tree-sha1 = "99fd8f32ab6728ff205cddd24e2d9ea6cbb498d3" uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" -version = "3.4.8" +version = "3.5.0" [deps.PrettyTables.extensions] PrettyTablesExcelExt = "XLSX" PrettyTablesTypstryExt = "Typstry" + PrettyTablesWriteDocxExt = "WriteDocx" [deps.PrettyTables.weakdeps] Typstry = "f0ed7684-a786-439e-b1e3-3b82803b501e" + WriteDocx = "d049ceea-54ee-41d7-a26f-ba29db3b6599" XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" [[deps.Primes]] @@ -2565,9 +2586,9 @@ version = "1.4.0" [[deps.PureKLU]] deps = ["LinearAlgebra", "PrecompileTools", "SparseArrays"] -git-tree-sha1 = "ef341b8e734ffa12c0464a58ca1c8a214d7a4235" +git-tree-sha1 = "ad0e84647fe7b3c2ae02aaeace1522139aaf6a27" uuid = "0c0d3e7f-3a8b-4f7e-b6f1-9a4d2e7c1f01" -version = "1.4.1" +version = "1.6.0" weakdeps = ["ForwardDiff"] [deps.PureKLU.extensions] @@ -2660,9 +2681,9 @@ weakdeps = ["FixedPointNumbers"] [[deps.RecipesBase]] deps = ["PrecompileTools"] -git-tree-sha1 = "5c3d09cc4f31f5fc6af001c250bf1278733100ff" +git-tree-sha1 = "0c0dcffc464cda2ff33fc8ffc1dd1bdd53cd98be" uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" -version = "1.3.4" +version = "1.4.0" [[deps.RecipesPipeline]] deps = ["Dates", "NaNMath", "PlotUtils", "PrecompileTools", "RecipesBase"] @@ -2672,9 +2693,9 @@ version = "0.6.12" [[deps.RecursiveArrayTools]] deps = ["Adapt", "ArrayInterface", "GPUArraysCore", "LinearAlgebra", "PrecompileTools", "RecipesBase", "SciMLPublic", "SciMLStructures", "StaticArraysCore", "SymbolicIndexingInterface"] -git-tree-sha1 = "775b6023c34c401e35b9a159568d2f6d051280d5" +git-tree-sha1 = "258850fd9188f5b1ac44bb03ab9225b4084500eb" uuid = "731186ca-8d62-57ce-b412-fbd966d074cd" -version = "4.5.1" +version = "4.5.3" [deps.RecursiveArrayTools.extensions] RecursiveArrayToolsCUDAExt = "CUDA" @@ -2735,9 +2756,9 @@ version = "1.3.1" [[deps.Reseau]] deps = ["NetworkOptions", "OpenSSL_jll", "PrecompileTools", "Random", "SHA"] -git-tree-sha1 = "aaee74fab545e9af4b21d10c2203c93c636c4ae0" +git-tree-sha1 = "3c8cb8ecab28eec47a02d56a44d4929b85fbab55" uuid = "802f3686-a58f-41ce-bb0c-3c43c75bba36" -version = "1.4.1" +version = "1.6.0" [[deps.RespecializeParams]] deps = ["FunctionWrappersWrappers", "PrecompileTools"] @@ -2746,10 +2767,14 @@ uuid = "9fe22ead-9e00-4db2-8b46-706a60d40f5e" version = "1.3.0" [[deps.ReverseDiff]] -deps = ["ChainRulesCore", "DiffResults", "DiffRules", "ForwardDiff", "FunctionWrappers", "LinearAlgebra", "LogExpFunctions", "MacroTools", "NaNMath", "Random", "SpecialFunctions", "StaticArrays", "Statistics"] -git-tree-sha1 = "f5f3c09103951beae390666a18e084655996370b" +deps = ["ChainRulesCore", "DiffResults", "DiffRules", "ForwardDiff", "FunctionWrappers", "LinearAlgebra", "LogExpFunctions", "MacroTools", "NaNMath", "Random", "SpecialFunctions", "StaticArrays"] +git-tree-sha1 = "6346ba163a42121dbbbe54831abf918bac65337c" uuid = "37e2e3b7-166d-5795-8a7a-e32c996b4267" -version = "1.17.0" +version = "1.18.4" +weakdeps = ["Statistics"] + + [deps.ReverseDiff.extensions] + StatisticsExt = "Statistics" [[deps.Rmath]] deps = ["Random", "Rmath_jll"] @@ -2765,9 +2790,9 @@ version = "0.5.2+0" [[deps.Roots]] deps = ["Accessors", "CommonSolve", "Printf"] -git-tree-sha1 = "4db094d5e079abbda658acfe1c4d098430417717" +git-tree-sha1 = "e6ffa7bd4f8d0b8744d18f219304738d952f9a37" uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" -version = "3.0.8" +version = "3.0.11" [deps.Roots.extensions] RootsChainRulesCoreExt = "ChainRulesCore" @@ -2792,9 +2817,9 @@ version = "0.2.1" [[deps.RuntimeGeneratedFunctions]] deps = ["ExprTools", "PrecompileTools", "SHA", "Serialization"] -git-tree-sha1 = "6cb9b83354089fcb33c643773acfd3235f2f1bf5" +git-tree-sha1 = "9bb075002b40849582905394bb3031fb77aa102d" uuid = "7e49a35a-f44a-4d26-94aa-eba1b4ca6b47" -version = "0.5.25" +version = "0.5.27" [[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" @@ -2814,9 +2839,9 @@ version = "2025.9.18+1" [[deps.SciMLBase]] deps = ["ADTypes", "Accessors", "Adapt", "ArrayInterface", "CommonSolve", "ConstructionBase", "Distributed", "DocStringExtensions", "EnumX", "FindFirstFunctions", "FunctionWrappersWrappers", "IteratorInterfaceExtensions", "LinearAlgebra", "Logging", "LoggingExtras", "Markdown", "PreallocationTools", "PrecompileTools", "Preferences", "Printf", "Random", "RecipesBase", "RecursiveArrayTools", "RuntimeGeneratedFunctions", "SciMLOperators", "SciMLPublic", "SciMLStructures", "StaticArraysCore", "Statistics", "SymbolicIndexingInterface"] -git-tree-sha1 = "872d3d4426adae016693c1300ba47218b9dcb86e" +git-tree-sha1 = "46345faa1fa3f43c24c241653c3b2719672b8d99" uuid = "0bca4576-84f4-4d90-8ffe-ffa030f20462" -version = "3.50.2" +version = "3.57.0" [deps.SciMLBase.extensions] SciMLBaseChainRulesCoreExt = "ChainRulesCore" @@ -2833,6 +2858,7 @@ version = "3.50.2" SciMLBasePyCallExt = "PyCall" SciMLBasePythonCallExt = "PythonCall" SciMLBaseRCallExt = "RCall" + SciMLBaseReactantCoreExt = "ReactantCore" SciMLBaseReverseDiffExt = ["ReverseDiff", "ForwardDiff"] SciMLBaseStaticArraysExt = "StaticArrays" SciMLBaseTrackerExt = "Tracker" @@ -2856,6 +2882,7 @@ version = "3.50.2" PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" RCall = "6f49c342-dc21-5d91-9882-a32aef131414" + ReactantCore = "a3311ec8-5e00-46d5-b541-4f83e724a433" ReverseDiff = "37e2e3b7-166d-5795-8a7a-e32c996b4267" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Tracker = "9f7883ad-71c0-57eb-9f7f-b5c9e6d3789c" @@ -2864,9 +2891,9 @@ version = "3.50.2" [[deps.SciMLJacobianOperators]] deps = ["ADTypes", "ArrayInterface", "ConcreteStructs", "ConstructionBase", "DifferentiationInterface", "FastClosures", "LinearAlgebra", "SciMLBase", "SciMLOperators"] -git-tree-sha1 = "c944c1ad6bcf8d6999d55c7dcec26edefe4d93a0" +git-tree-sha1 = "e58036956d27635a57276b05bdbbe8b22376a805" uuid = "19f34311-ddf3-4b8b-af20-060888a46c0e" -version = "0.1.18" +version = "0.1.19" [[deps.SciMLLogging]] deps = ["Logging", "LoggingExtras", "PrecompileTools", "Preferences"] @@ -2880,9 +2907,9 @@ weakdeps = ["Tracy"] [[deps.SciMLOperators]] deps = ["Accessors", "Adapt", "ArrayInterface", "DocStringExtensions", "LinearAlgebra", "PrecompileTools", "SciMLPublic"] -git-tree-sha1 = "0b5b895913f1269a8c80b59435c61d5a7d79970c" +git-tree-sha1 = "6c97cd41488ccd362363ff9c883dafcff18da45c" uuid = "c0aeaf25-5076-4817-a8d5-81caf7dfa961" -version = "1.30.0" +version = "1.30.2" [deps.SciMLOperators.extensions] SciMLOperatorsLoopVectorizationExt = "LoopVectorization" @@ -2928,9 +2955,9 @@ version = "1.1.2" [[deps.ShaderAbstractions]] deps = ["ColorTypes", "FixedPointNumbers", "GeometryBasics", "LinearAlgebra", "Observables", "StaticArrays"] -git-tree-sha1 = "818554664a2e01fc3784becb2eb3a82326a604b6" +git-tree-sha1 = "57aa595158717ef165e6f5ab639fe2e3178c0a2b" uuid = "65257c39-d410-5151-9873-9b3e5be5013e" -version = "0.5.0" +version = "0.5.1" [[deps.SharedArrays]] deps = ["Distributed", "Mmap", "Random", "Serialization"] @@ -2951,9 +2978,9 @@ version = "0.4.1" [[deps.SimpleNonlinearSolve]] deps = ["ADTypes", "ArrayInterface", "BracketingNonlinearSolve", "CommonSolve", "ConcreteStructs", "DifferentiationInterface", "FastClosures", "FiniteDiff", "ForwardDiff", "LineSearch", "LinearAlgebra", "MaybeInplace", "NonlinearSolveBase", "PrecompileTools", "Reexport", "SciMLBase", "SciMLLogging", "Setfield", "StaticArraysCore"] -git-tree-sha1 = "72a05aa23361506d9574ff09f86d7d55b6f5af4c" +git-tree-sha1 = "8d9ec24ec7bd126abf97b44ba5bdd6d280572095" uuid = "727e6d20-b764-4bd8-a329-72de5adea6c7" -version = "2.14.1" +version = "2.14.6" [deps.SimpleNonlinearSolve.extensions] SimpleNonlinearSolveChainRulesCoreExt = "ChainRulesCore" @@ -2973,9 +3000,15 @@ version = "0.9.6" [[deps.Sixel]] deps = ["Dates", "FileIO", "ImageCore", "IndirectArrays", "OffsetArrays", "REPL", "libsixel_jll"] -git-tree-sha1 = "0494aed9501e7fb65daba895fb7fd57cc38bc743" +git-tree-sha1 = "2c79185e5261b159474903598c571ddd9f12ff7b" uuid = "45858cf5-a6b0-47a3-bbea-62219f50df47" -version = "0.1.5" +version = "0.1.6" + +[[deps.Sobol]] +deps = ["DelimitedFiles", "Random"] +git-tree-sha1 = "5a74ac22a9daef23705f010f72c81d6925b19df8" +uuid = "ed01d8cd-4d21-5b2a-85b4-cc3bdc58bad4" +version = "1.5.0" [[deps.Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" @@ -3004,9 +3037,9 @@ version = "1.12.0" [[deps.SparseColumnPivotedQR]] deps = ["LinearAlgebra", "PrecompileTools", "SparseArrays"] -git-tree-sha1 = "01ac65d0bf2f42a9491b80c4076f4e02b462a9e6" +git-tree-sha1 = "8cf1a59c78dd6f900feb366e5a574f99ed278ef1" uuid = "a57abbd0-fea5-4d57-96be-5e525945e8e4" -version = "2.1.7" +version = "2.1.8" weakdeps = ["AMD"] [deps.SparseColumnPivotedQR.extensions] @@ -3014,9 +3047,9 @@ weakdeps = ["AMD"] [[deps.SparseConnectivityTracer]] deps = ["ADTypes", "DocStringExtensions", "FillArrays", "LinearAlgebra", "Random", "SparseArrays"] -git-tree-sha1 = "5c127b6b5e36e1ba38556b578c6346b2375ec0b4" +git-tree-sha1 = "277d8929a56912e7342458d4db22ee45e4c0e75c" uuid = "9f842d2f-2579-4b1d-911e-f412cf18a3f5" -version = "1.2.3" +version = "1.2.4" [deps.SparseConnectivityTracer.extensions] SparseConnectivityTracerChainRulesCoreExt = "ChainRulesCore" @@ -3034,9 +3067,9 @@ version = "1.2.3" [[deps.SparseMatrixColorings]] deps = ["ADTypes", "DocStringExtensions", "LinearAlgebra", "PrecompileTools", "Random", "SparseArrays"] -git-tree-sha1 = "f63d76c7b7c329cf11badd564fd8ba877b09c3fe" +git-tree-sha1 = "63e1776f40bbd5e7394edce08e62f852b1384baf" uuid = "0a514795-09f3-496d-8182-132a7b665d35" -version = "0.4.27" +version = "0.4.28" [deps.SparseMatrixColorings.extensions] SparseMatrixColoringsCUDAExt = ["CUDA", "cuSPARSE"] @@ -3064,12 +3097,6 @@ weakdeps = ["ChainRulesCore"] [deps.SpecialFunctions.extensions] SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" -[[deps.StableRNGs]] -deps = ["Random"] -git-tree-sha1 = "4f96c596b8c8258cc7d3b19797854d368f243ddc" -uuid = "860ef19b-820b-49d6-a774-d7a799459cd3" -version = "1.0.4" - [[deps.StackViews]] deps = ["OffsetArrays"] git-tree-sha1 = "be1cf4eb0ac528d96f5115b4ed80c26a8d8ae621" @@ -3078,9 +3105,9 @@ version = "0.1.2" [[deps.StaticArrays]] deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"] -git-tree-sha1 = "e206cf4850fd7ac4255ffd2b98922f563e18ac53" +git-tree-sha1 = "de349d5585559b692f518b6d3fc7c179d64b2364" uuid = "90137ffa-7385-5640-81b9-e52037218182" -version = "1.9.20" +version = "1.9.23" weakdeps = ["ChainRulesCore", "Statistics"] [deps.StaticArrays.extensions] @@ -3126,10 +3153,10 @@ weakdeps = ["ChainRulesCore", "InverseFunctions"] StatsFunsInverseFunctionsExt = "InverseFunctions" [[deps.StringManipulation]] -deps = ["PrecompileTools"] -git-tree-sha1 = "773065c6e0e903924a9d838259be74338422aef2" +deps = ["PrecompileTools", "StyledStrings"] +git-tree-sha1 = "9e3a02d73a8f0a9be04e677dbef758aab40f8bea" uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e" -version = "0.5.0" +version = "0.6.1" [[deps.StructArrays]] deps = ["ConstructionBase", "DataAPI", "Tables"] @@ -3153,16 +3180,18 @@ version = "1.11.0" [[deps.StructUtils]] deps = ["Dates", "UUIDs"] -git-tree-sha1 = "2d0fc55c61321ba245c47be599570d11bac50303" +git-tree-sha1 = "b814d5005d6a529d740ffe06f8a86396f6501138" uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42" -version = "2.8.5" +version = "2.9.2" [deps.StructUtils.extensions] + StructUtilsLazilyInitializedFieldsExt = ["LazilyInitializedFields"] StructUtilsMeasurementsExt = ["Measurements"] StructUtilsStaticArraysCoreExt = ["StaticArraysCore"] StructUtilsTablesExt = ["Tables"] [deps.StructUtils.weakdeps] + LazilyInitializedFields = "0e77f7df-68c5-4e49-93ce-4cd80f5598bf" Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" @@ -3230,10 +3259,10 @@ uuid = "731e570b-9d59-4bfa-96dc-6df516fadf69" version = "0.11.9" [[deps.TimerOutputs]] -deps = ["ExprTools", "Printf"] -git-tree-sha1 = "3748bd928e68c7c346b52125cf41fff0de6937d0" +deps = ["ExprTools", "PrettyTables", "Printf", "Tables"] +git-tree-sha1 = "2244393f04554e24b54896b430b2ab3850a2232d" uuid = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" -version = "0.5.29" +version = "1.2.2" [deps.TimerOutputs.extensions] FlameGraphsExt = "FlameGraphs" @@ -3296,9 +3325,9 @@ version = "0.4.1" [[deps.Unitful]] deps = ["Dates", "LinearAlgebra", "Random"] -git-tree-sha1 = "57e1b2c9de4bd6f40ecb9de4ac1797b81970d008" +git-tree-sha1 = "1f0f9f401753701a7e4113b5056ca38d33875b55" uuid = "1986cc42-f94f-5a68-af5c-568840ba703d" -version = "1.28.0" +version = "1.29.0" weakdeps = ["ConstructionBase", "ForwardDiff", "InverseFunctions", "LaTeXStrings", "Latexify", "NaNMath", "Printf"] [deps.Unitful.extensions] @@ -3311,9 +3340,9 @@ weakdeps = ["ConstructionBase", "ForwardDiff", "InverseFunctions", "LaTeXStrings [[deps.UnoSolver]] deps = ["LinearAlgebra", "OpenBLAS32_jll", "SolverCore", "Uno_jll"] -git-tree-sha1 = "6787f703903f0b2457b80c49ffe1b05c3d965b56" +git-tree-sha1 = "bfa2584aa7f7f56b2f56a4f3592681d8ab33d0bc" uuid = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" -version = "0.3.6" +version = "0.3.7" [deps.UnoSolver.extensions] UnoSolverMathOptInterfaceExt = "MathOptInterface" @@ -3325,9 +3354,9 @@ version = "0.3.6" [[deps.Uno_jll]] deps = ["ASL_jll", "Artifacts", "CompilerSupportLibraries_jll", "HSL_jll", "HiGHS_jll", "JLLWrappers", "LLVMOpenMP_jll", "Libdl", "METIS_jll", "MUMPS_seq_jll", "SPRAL_jll", "libblastrampoline_jll"] -git-tree-sha1 = "e21ef3d2f6bbde8997a682c4874fde8dfbb9a051" +git-tree-sha1 = "4ccf2070eab74ac5e5f1018a681befe6031feabd" uuid = "396d5378-14f1-5ab1-981d-48acd51740ed" -version = "2.8.0+0" +version = "2.9.0+0" [[deps.UnsafeAtomics]] git-tree-sha1 = "21b39bfb1fab6156b61fbcba4c86c57b6216d2c3" @@ -3369,15 +3398,15 @@ version = "1.1.0" [[deps.XML2_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Zlib_jll"] -git-tree-sha1 = "80d3930c6347cfce7ccf96bd3bafdf079d9c0390" +git-tree-sha1 = "b826aedca5fce2e3c91904e338c279e5c1a2da74" uuid = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a" -version = "2.13.9+0" +version = "2.15.4+0" [[deps.XZ_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "b29c22e245d092b8b4e8d3c09ad7baa586d9f573" +git-tree-sha1 = "e52eca002a11c30a858185efdfb15311e1c7a6bf" uuid = "ffd25f8a-64ca-5728-b0f7-c24cf3aae800" -version = "5.8.3+0" +version = "5.8.4+0" [[deps.Xorg_libICE_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] @@ -3600,9 +3629,9 @@ version = "0.2.3+0" [[deps.libaom_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "ef17c47d22224aaecc76e597ab21a072e025cf7b" +git-tree-sha1 = "1210ba774d3427387d307bf1f416d699b7c39417" uuid = "a4ae2306-e953-59d6-aa16-d00cac43593b" -version = "3.14.1+0" +version = "3.15.1+0" [[deps.libass_jll]] deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "HarfBuzz_jll", "JLLWrappers", "Libdl", "Zlib_jll"] @@ -3647,15 +3676,15 @@ version = "1.28.1+0" [[deps.libpng_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Zlib_jll"] -git-tree-sha1 = "e51150d5ab85cee6fc36726850f0e627ad2e4aba" +git-tree-sha1 = "1fcd6cf05d23adab0b5ffc496d020537215751dc" uuid = "b53b4c65-9356-5827-b1ea-8c7a1a84506f" -version = "1.6.58+0" +version = "1.6.59+1" [[deps.libsixel_jll]] deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "Libdl", "libpng_jll"] -git-tree-sha1 = "c1733e347283df07689d71d61e14be986e49e47a" +git-tree-sha1 = "e067c8bae65bb40866552296a2d5b4dc65b8928f" uuid = "075b6546-f08a-558a-be8f-8157d0f608a5" -version = "1.10.5+0" +version = "1.80.702+0" [[deps.libva_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll", "Xorg_libXext_jll", "Xorg_libXfixes_jll", "libdrm_jll"] @@ -3671,9 +3700,9 @@ version = "1.3.8+0" [[deps.libwebp_jll]] deps = ["Artifacts", "Giflib_jll", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Libglvnd_jll", "Libtiff_jll", "libpng_jll"] -git-tree-sha1 = "4e4282c4d846e11dce56d74fa8040130b7a95cb3" +git-tree-sha1 = "52d3b9475133c3bc8c0a7f90f18bfc8cdb5a443a" uuid = "c5f90fcd-3b7e-5836-afba-fc50a0988cb2" -version = "1.6.0+0" +version = "1.6.1+0" [[deps.mtdev_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index 5a7adf5b6..c26f3048b 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -6,8 +6,7 @@ Draft = false ## Install -Open Julia's [interactive session (REPL)](https://docs.julialang.org/en/v1/manual/getting-started) -and use the package manager: +Open Julia's [interactive session (REPL)](https://docs.julialang.org/en/v1/manual/getting-started) and use the package manager: ```julia using Pkg @@ -18,20 +17,17 @@ Pkg.add("OptimalControl") If you are new to Julia, follow [this guideline](https://github.com/orgs/control-toolbox/discussions/64). -`OptimalControl` alone is enough to define a problem with [`@def`](@ref modelling-abstract-syntax) -and describe [solve strategies](@ref solve-choosing-a-method). Everything below is optional — -loaded only when the feature it backs is actually used. +`OptimalControl` alone is enough to define a problem with [`@def`](@ref modelling-abstract-syntax) and describe [solve strategies](@ref solve-choosing-a-method). The features below are optional: each requires an additional package, which you only need to load when you use that feature. ## You will also need a solver -`solve` needs an NLP solver backend loaded. `NLPModelsIpopt` is the default and the one used -throughout this documentation: +`solve` requires an NLP solver backend to be loaded. `NLPModelsIpopt` is the default solver and is used throughout this documentation: ```julia using NLPModelsIpopt ``` -Alternatives exist, each behind its own package: +Other solvers are available through their respective packages: | Solver | Load | | --- | --- | @@ -41,10 +37,7 @@ Alternatives exist, each behind its own package: | `:madncl` | `using MadNCL` **and** `using MadNLP` (both) | | `:knitro` | `using NLPModelsKnitro` (commercial licence required) | -See [Choosing a method](@ref solve-choosing-a-method) for how these combine with a discretizer, -a modeler and a `:cpu`/`:gpu` parameter. Calling `solve` before the matching package is loaded -raises an `ExtensionError` naming exactly which `using` statement to add — the same mechanism -covers every optional piece on this page. +See [Choosing a method](@ref solve-choosing-a-method) to learn how these packages combine with a discretizer, a modeler, and a `:cpu`/`:gpu` parameter. If you call `solve` before loading the appropriate package, it raises an `ExtensionError` that identifies the exact `using` statement you need to add. The same mechanism covers every optional feature described on this page. ## Optional: plotting @@ -52,41 +45,23 @@ covers every optional piece on this page. using Plots ``` -unlocks `plot(sol)`. Without it: - -```julia -julia> using OptimalControl -julia> plot(sol) -ERROR: ExtensionError: missing dependencies to plot solutions -Missing Plots -Hint Run: using Plots -``` +Loading this package enables `plot(sol)`. If it is not loaded, the package raises an `ExtensionError`. See [Plotting](@ref results-plot). ## Optional: flows -Building a [`Flow`](@ref flows-overview) — indirect shooting, simulation, or inspecting a -Hamiltonian vector field — needs an ODE integrator: +Creating a [`Flow`](@ref flows-overview)—for indirect shooting, simulation, or inspecting a Hamiltonian vector field—requires an ODE integrator: ```julia using OrdinaryDiffEqTsit5 ``` -!!! warning "The most common first failure" +Loading this package enables the creation of a `Flow`. If it is not loaded, the package raises an `ExtensionError`. - Every page in the [Flows](@ref flows-overview) section opens with - `using OrdinaryDiffEqTsit5` for this reason: building a `Flow` before it is loaded is the - single most common trap for newcomers to this part of the package. It fails cleanly rather - than silently: +!!! warning "The `DifferentialEquations` ecosystem" - ```julia - julia> using OptimalControl - julia> φ = Flow(ocp, u) - ERROR: ExtensionError: missing dependencies to access SciML options metadata - Missing OrdinaryDiffEqTsit5 - Hint Run: using OrdinaryDiffEqTsit5 - ``` + You can use any ODE solver from the `DifferentialEquations` ecosystem. For example, you can load `OrdinaryDiffEqTsit5` or `OrdinaryDiffEqVern9`. You can also load the ecosystem's umbrella packages directly, such as with `using DifferentialEquations` or `using OrdinaryDiffEq`. ## Optional: saving solutions @@ -95,14 +70,7 @@ using JLD2 # format=:JLD (default) using JSON3 # format=:JSON ``` -unlock `export_ocp_solution`/`import_ocp_solution`. Without the matching one: - -```julia -julia> export_ocp_solution(sol; format=:JLD) -ERROR: ExtensionError: missing dependencies to export solutions to JLD2 format -Missing JLD2 -Hint Run: using JLD2 -``` +Loading either of these packages enables `export_ocp_solution` and `import_ocp_solution`. If neither package is loaded, these functions are not available. See [Save & load](@ref results-save-load). @@ -114,35 +82,13 @@ using CUDA using CUDSS ``` -NVIDIA GPUs only; the problem's dynamics must be written coordinatewise. - -All three are required together — they are what arms the `CTSolversMadNLPGPU` extension. Older -guides list only the first two, because up to MadNLPGPU 0.8 `CUDSS` came in as a hard -dependency; from 0.9 it is weak and has to be loaded explicitly. +Only NVIDIA GPUs are supported. -Like the optional pieces above, a missing one is reported accurately: the `ExtensionError` -names exactly which of the three is absent, so if you're told to load `CUDSS`, that's the one -you forgot. +All three packages must be loaded together to activate the `CTSolversMadNLPGPU` extension. Older guides list only the first two because, up to MadNLPGPU 0.8, `CUDSS` was a regular dependency. Since version 0.9, it is a weak dependency and must be loaded explicitly. -`ExaModels` is not in the list on purpose: it ships as a dependency of OptimalControl, so -`:exa` works without importing it. See [GPU](@ref solve-gpu) for the constraints, and check -`CUDA.functional()` before assuming a `:gpu` solve will actually run on the device. +As with the other optional features above, a missing package is reported precisely: the `ExtensionError` identifies which of the three packages is absent. Thus, if the error asks you to load `CUDSS`, that is the package you need to add. -## Checking your setup - -Loading everything and calling `methods()` is a quick way to confirm the install is sound — -if this runs without error, `OptimalControl` and every optional piece above are wired in: - -```@example main -using OptimalControl -using NLPModelsIpopt -using Plots -using OrdinaryDiffEqTsit5 -using JLD2 -using JSON3 - -methods() -``` +`ExaModels` is intentionally not listed: it is a dependency of `OptimalControl`, so `:exa` works without importing it explicitly. See [GPU](@ref solve-gpu) for the constraints, and check `CUDA.functional()` before assuming that a `:gpu` solve will actually run on the GPU. ## See also From 5e00f5a6755db5001a77cf0e4a3f26eea72bcd94 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 08:50:27 +0200 Subject: [PATCH 05/57] docs(installation): fix integrator advice, solver table and GPU section; add Makie - Flows: OrdinaryDiffEqVern9 does not exist (OrdinaryDiffEqVerner does), and a non-Tsit5 integrator only works with OrdinaryDiffEqTsit5 still loaded (CTSolvers.jl#230). The warning becomes a tip with a verified alg= example. - Solver table: the :madnlp row no longer contradicts the GPU section. - Show a real ExtensionError transcript in the current boxed format. - Plotting: mention Makie; describe the missing-package errors precisely. - GPU: keep the three packages and CUDA.functional(), drop internal extension names and version history (kept on the GPU page). - Remove the page's Draft = false review marker. Co-Authored-By: Claude Opus 5.5 --- docs/src/getting-started/installation.md | 51 ++++++++++++++---------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index c26f3048b..142fde879 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -1,9 +1,5 @@ # [Installation](@id getting-started-installation) -```@meta -Draft = false -``` - ## Install Open Julia's [interactive session (REPL)](https://docs.julialang.org/en/v1/manual/getting-started) and use the package manager: @@ -15,7 +11,7 @@ Pkg.add("OptimalControl") !!! tip - If you are new to Julia, follow [this guideline](https://github.com/orgs/control-toolbox/discussions/64). + If you are new to Julia, read [New to Julia: how to use OptimalControl](https://github.com/orgs/control-toolbox/discussions/64). `OptimalControl` alone is enough to define a problem with [`@def`](@ref modelling-abstract-syntax) and describe [solve strategies](@ref solve-choosing-a-method). The features below are optional: each requires an additional package, which you only need to load when you use that feature. @@ -32,12 +28,29 @@ Other solvers are available through their respective packages: | Solver | Load | | --- | --- | | `:ipopt` (default) | `using NLPModelsIpopt` | -| `:madnlp` | `using MadNLP` (CPU) or `using MadNLPGPU` (GPU) | +| `:madnlp` | `using MadNLP` (on GPU, see [below](@ref getting-started-installation-gpu)) | | `:uno` | `using UnoSolver` | | `:madncl` | `using MadNCL` **and** `using MadNLP` (both) | | `:knitro` | `using NLPModelsKnitro` (commercial licence required) | -See [Choosing a method](@ref solve-choosing-a-method) to learn how these packages combine with a discretizer, a modeler, and a `:cpu`/`:gpu` parameter. If you call `solve` before loading the appropriate package, it raises an `ExtensionError` that identifies the exact `using` statement you need to add. The same mechanism covers every optional feature described on this page. +See [Choosing a method](@ref solve-choosing-a-method) to learn how these packages combine with a discretizer, a modeler, and a `:cpu`/`:gpu` parameter. + +If you call `solve` before loading the appropriate package, it raises an `ExtensionError` that names the missing package and the `using` statement to add: + +```julia +julia> solve(ocp) +ERROR: ExtensionError → _route_descriptive_options, descriptive_routing.jl:259 +│ +│ missing dependencies to access Ipopt{CPU} options metadata +│ +│ Missing NLPModelsIpopt +│ +│ Context Load NLPModelsIpopt extension first: using NLPModelsIpopt +│ Hint Run: using NLPModelsIpopt +└─ +``` + +The same mechanism covers every optional feature described on this page. ## Optional: plotting @@ -45,9 +58,9 @@ See [Choosing a method](@ref solve-choosing-a-method) to learn how these package using Plots ``` -Loading this package enables `plot(sol)`. If it is not loaded, the package raises an `ExtensionError`. +Loading this package enables `plot(sol)`. Without it, `plot(sol)` raises an `ExtensionError` that asks for `using Plots`. See [Plot](@ref results-plot). -See [Plotting](@ref results-plot). +[Makie](https://docs.makie.org) is supported too. Load `CairoMakie` for static figures or `GLMakie` for an interactive window, then call `Makie.plot(sol)`. See [Plot with Makie](@ref results-plot-makie). ## Optional: flows @@ -57,11 +70,11 @@ Creating a [`Flow`](@ref flows-overview)—for indirect shooting, simulation, or using OrdinaryDiffEqTsit5 ``` -Loading this package enables the creation of a `Flow`. If it is not loaded, the package raises an `ExtensionError`. +Loading this package enables the creation of a `Flow`. Without it, `Flow` raises an `ExtensionError` that asks for `using OrdinaryDiffEqTsit5`. -!!! warning "The `DifferentialEquations` ecosystem" +!!! tip "Other integrators" - You can use any ODE solver from the `DifferentialEquations` ecosystem. For example, you can load `OrdinaryDiffEqTsit5` or `OrdinaryDiffEqVern9`. You can also load the ecosystem's umbrella packages directly, such as with `using DifferentialEquations` or `using OrdinaryDiffEq`. + `Tsit5` is the default integrator, so keep `OrdinaryDiffEqTsit5` loaded in every case (see [CTSolvers#230](https://github.com/control-toolbox/CTSolvers.jl/issues/230)). To use another method from the [SciML ODE solvers](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/), load its package as well and pass it when you build the flow, for example `Flow(ocp, law; alg=Vern9())` after `using OrdinaryDiffEqVerner`. The umbrella packages `OrdinaryDiffEq` and `DifferentialEquations` include `Tsit5` and work too. ## Optional: saving solutions @@ -70,11 +83,11 @@ using JLD2 # format=:JLD (default) using JSON3 # format=:JSON ``` -Loading either of these packages enables `export_ocp_solution` and `import_ocp_solution`. If neither package is loaded, these functions are not available. +Loading either of these packages enables `export_ocp_solution` and `import_ocp_solution` in the matching format. Without it, these functions raise an `ExtensionError` that names the package to load. See [Save & load](@ref results-save-load). -See [Save & load](@ref results-save-load). +## [Optional: GPU](@id getting-started-installation-gpu) -## Optional: GPU +Only NVIDIA GPUs are supported. Load these three packages together: ```julia using MadNLPGPU @@ -82,13 +95,11 @@ using CUDA using CUDSS ``` -Only NVIDIA GPUs are supported. - -All three packages must be loaded together to activate the `CTSolversMadNLPGPU` extension. Older guides list only the first two because, up to MadNLPGPU 0.8, `CUDSS` was a regular dependency. Since version 0.9, it is a weak dependency and must be loaded explicitly. +`CUDSS` is the one people forget: `using MadNLPGPU` does not load it, and the GPU solvers do not work without it. If one of the three is missing, the `ExtensionError` names it. -As with the other optional features above, a missing package is reported precisely: the `ExtensionError` identifies which of the three packages is absent. Thus, if the error asks you to load `CUDSS`, that is the package you need to add. +`ExaModels`, the GPU-capable modeler, needs no `using`: it comes with `OptimalControl`, so `:exa` works out of the box. -`ExaModels` is intentionally not listed: it is a dependency of `OptimalControl`, so `:exa` works without importing it explicitly. See [GPU](@ref solve-gpu) for the constraints, and check `CUDA.functional()` before assuming that a `:gpu` solve will actually run on the GPU. +Before relying on a `:gpu` solve, check that `CUDA.functional()` returns `true`. See [GPU](@ref solve-gpu) for the details and for a complete example. ## See also From e134ac7b5a4b1e5332213a983f3180b97c99f272 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 08:57:07 +0200 Subject: [PATCH 06/57] docs(first-problem): annotated walk-through with exact-solution checks The home page already shows the bare code, so this page becomes the annotated version: the wagon story and figure, a line-by-line reading of the @def block, how to read the solve output and the plot panels, and a "Check it" section that compares the numerical solution with the exact one (u = 6 - 12t, J = 6). Hidden @asserts make the build fail if the solution drifts. Also: x = (q, v) in the code as in the maths, no "bounded" force, and the "1-D is a scalar" link now lands on the Control section. Co-Authored-By: Claude Opus 5.5 --- docs/src/getting-started/first-problem.md | 103 +++++++++++++++++----- 1 file changed, 82 insertions(+), 21 deletions(-) diff --git a/docs/src/getting-started/first-problem.md b/docs/src/getting-started/first-problem.md index 19d4acc58..428d7ca94 100644 --- a/docs/src/getting-started/first-problem.md +++ b/docs/src/getting-started/first-problem.md @@ -1,73 +1,134 @@ # [Your first problem](@id getting-started-first-problem) -The shortest complete story: define a problem, solve it, look at the result. Fifteen lines, -no options, no theory. +The shortest complete story: define a problem, solve it, look at the result, and check it +against the exact answer. No options, no theory: every line is explained, and the other pages +go deeper. ## The problem -A point mass moves on a line under a bounded force: state $x = (q, v)$ (position, velocity), -scalar control $u$ (the force). Starting at rest at $q=-1$, reach $q=0$ at rest, in one unit of -time, minimising the energy spent: +A wagon moves along a rail. Its position is $q$, its velocity $v$, and we push it with a force +$u$ (unit mass, no friction). + +```@raw html + +``` + +Starting at rest at $q = -1$, we want it at rest at $q = 0$ one unit of time later, while +spending as little energy as possible: ```math -\min \frac{1}{2}\int_0^1 u(t)^2\,\mathrm dt \quad\text{s.t.}\quad \dot q = v,\ \dot v = u,\ -\ x(0) = (-1, 0),\ x(1) = (0, 0). +\min\ \frac{1}{2}\int_0^1 u(t)^2\,\mathrm dt +\quad\text{s.t.}\quad +\dot q(t) = v(t),\quad \dot v(t) = u(t),\quad +x(0) = (-1, 0),\quad x(1) = (0, 0), ``` +where $x = (q, v)$ is the state. This is the double integrator, the "hello world" of optimal +control. + ## Define it +We load `OptimalControl` to write the problem and `NLPModelsIpopt` to solve it (see +[Installation](@ref getting-started-installation)), then transcribe the formulation with +[`@def`](@ref modelling-abstract-syntax): + ```@example main using OptimalControl using NLPModelsIpopt ocp = @def begin t ∈ [0, 1], time - x ∈ R², state + x = (q, v) ∈ R², state u ∈ R, control x(0) == [-1, 0] x(1) == [0, 0] - ẋ(t) == [x₂(t), u(t)] - ∫(0.5u(t)^2) → min + ẋ(t) == [v(t), u(t)] + 0.5∫(u(t)^2) → min end nothing # hide ``` -See [Abstract syntax (`@def`)](@ref modelling-abstract-syntax) for what each line above means. +Each line mirrors the mathematics: + +| Line | Meaning | +| --- | --- | +| `t ∈ [0, 1], time` | the time variable and its fixed interval | +| `x = (q, v) ∈ R², state` | a two-dimensional state, whose components are named `q` and `v` | +| `u ∈ R, control` | a scalar control | +| `x(0) == [-1, 0]`, `x(1) == [0, 0]` | the initial and final conditions | +| `ẋ(t) == [v(t), u(t)]` | the dynamics | +| `0.5∫(u(t)^2) → min` | the cost to minimise | + +See [Abstract syntax (`@def`)](@ref modelling-abstract-syntax) for every form these lines can +take. ## Solve it +`solve` discretises the problem in time and hands the resulting finite-dimensional problem to +Ipopt: + ```@example main sol = solve(ocp) nothing # hide ``` +The first box names the method that ran: `solve(ocp)` was called without any method, so it +used the default `(:collocation, :adnlp, :ipopt, :cpu)`. The rest is Ipopt's log. `EXIT: +Optimal Solution Found` is the line to look for. Ipopt needs a single iteration here because +the dynamics are linear and the cost is quadratic. + ## Look at it +`Plots` draws the solution: + ```@example main using Plots plot(sol) ``` +The figure shows the state $(q, v)$, the costate $(p_q, p_v)$ and the control $u$. The wagon +accelerates, then brakes: the velocity peaks at mid-time and the control decreases linearly +from about $6$ to about $-6$. The costate comes from the multipliers of the discretised +problem: $p_q$ is constant and $p_v$ coincides with the control, as the maximum principle +predicts (see the energy example below). + +The solution also carries the cost and a few solver diagnostics: + ```@example main objective(sol), iterations(sol), successful(sol) ``` -`control(sol)` is a function of time; because the control here is scalar, calling it returns a -`Number`, not a length-1 vector — [1-D is a scalar](@ref modelling-abstract-syntax) throughout -the package: +The state and the control are functions of time. Because the control is scalar, `control(sol)` +returns a `Number`, not a length-1 vector (see the +[note on dimension one](@ref modelling-abstract-syntax-control)): ```@example main -control(sol)(0.5) +state(sol)(0.5), control(sol)(0.25) ``` -See [Solution](@ref results-solution) for the rest of what a solution carries, and -[Plotting](@ref results-plot) for what else `plot` can show. +See [Solution](@ref results-solution) for everything else a solution carries, and +[Plot](@ref results-plot) for what else `plot` can show. + +## Check it -## What just happened +This problem can be solved by hand: the optimal control is $u(t) = 6 - 12t$, the state at +mid-time is $x(1/2) = (-1/2, 3/2)$, and the minimal cost is $6$. The numerical solution +matches, up to the discretisation error of the default grid (250 steps): + +```@example main +objective(sol) - 6 +``` + +```@example main +@assert successful(sol) # hide +@assert isapprox(objective(sol), 6; rtol=1e-3) # hide +@assert isapprox(state(sol)(0.5), [-0.5, 1.5]; atol=1e-3) # hide +@assert maximum(abs(control(sol)(t) - (6 - 12t)) for t in 0:0.01:1) < 5e-2 # hide +nothing # hide +``` -`solve(ocp)` was called with no method at all, so it ran the default: -`(:collocation, :adnlp, :ipopt, :cpu)`. That default, what the four tokens mean, and how to -override any of them, are the subject of [Choosing a method](@ref solve-choosing-a-method). +The [energy minimisation example](@ref examples-double-integrator-energy) derives this exact +solution with the maximum principle and recovers it by indirect shooting. ## Next From 442db5c57e10134e8035afd5af873bf7fea2750e Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 09:23:11 +0200 Subject: [PATCH 07/57] docs(guided-tour): downloads instead of Binder, review fixes, hidden asserts - Binder badge (dead branch, uncommitted notebook) replaced by downloads: Literate writes the notebook and the script to src/public/assets/, linked in raw HTML because Documenter rewrites ../assets Markdown links to a 404 path. - Notebook links: stable/ (404) -> dev/; tutorial link to tutorial-iss.html. - CTLie in the ecosystem list; Installation link; callback argument v -> var; NLP variables up to u_{N-1}; link texts match page titles. - Goddard: Ipopt and MadNLP reach the same r(tf); grid-continuation prose without hard-coded counts; bang-bang gain computed and explained, with an altitude-vs-time figure built from time_grid/state. - GPU: neutral prose, CUDA warnings silenced, prints CUDA.functional(). - PMP introduced once; u_max -> u_feedback; OrdinaryDiffEqTsit5 loaded once. - Hidden @asserts: J = 6, r(tf), MadNLP vs Ipopt, cascade iterations, bang-bang altitude, p0 = (12, 6). Co-Authored-By: Claude Opus 5.5 --- docs/.gitignore | 2 - docs/make.jl | 12 +-- docs/src-literate/guided-tour.jl | 144 +++++++++++++++++++------------ 3 files changed, 95 insertions(+), 63 deletions(-) diff --git a/docs/.gitignore b/docs/.gitignore index 7779fda19..5773b95d9 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -2,7 +2,5 @@ build/ node_modules/ package-lock.json Manifest.toml -src/notebooks/ -src/scripts/ src/getting-started/guided-tour.md !src/assets/Manifest.toml \ No newline at end of file diff --git a/docs/make.jl b/docs/make.jl index 159fadfcb..441a3e38f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -238,10 +238,10 @@ using Literate LITERATE_DIR = joinpath(@__DIR__, "src-literate") MD_OUTPUT = joinpath(@__DIR__, "src", "getting-started") -NB_OUTPUT = joinpath(@__DIR__, "src", "notebooks") -JL_OUTPUT = joinpath(@__DIR__, "src", "scripts") -mkpath(NB_OUTPUT) -mkpath(JL_OUTPUT) +# The notebook and the script are offered as downloads on the guided tour page, served by +# VitePress from `src/public/assets/` (git-ignored). The page links them in raw HTML, so +# Documenter's link checker does not need a copy in `src/assets/`. +DOWNLOAD_DIR = joinpath(@__DIR__, "src", "public", "assets") for file in ["guided-tour.jl"] INPUT = joinpath(LITERATE_DIR, file) @@ -249,8 +249,8 @@ for file in ["guided-tour.jl"] # `draft = false`. (It once carried an injected `Draft = false` override to run # under a `draft = true` default; that default is gone.) Literate.markdown(INPUT, MD_OUTPUT; name="guided-tour") - Literate.notebook(INPUT, NB_OUTPUT; name="guided-tour", execute=false) - Literate.script(INPUT, JL_OUTPUT; name="guided-tour") + Literate.notebook(INPUT, DOWNLOAD_DIR; name="guided-tour", execute=false) + Literate.script(INPUT, DOWNLOAD_DIR; name="guided-tour") end # ═══════════════════════════════════════════════════════════════════════════════ diff --git a/docs/src-literate/guided-tour.jl b/docs/src-literate/guided-tour.jl index acb5c486d..75b134f70 100644 --- a/docs/src-literate/guided-tour.jl +++ b/docs/src-literate/guided-tour.jl @@ -20,10 +20,14 @@ # # 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. # -#md # !!! note "Run online" -#md # You can run this tutorial interactively in your browser — no installation required — by clicking the Binder badge below: -#md # -#md # [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/control-toolbox/OptimalControl.jl/paderborn?urlpath=%2Fdoc%2Ftree%2Fdocs%2Fsrc%2Fnotebooks%2Fguided-tour.ipynb) +#src The download links are raw HTML on purpose: Documenter rewrites a Markdown link +#src `../assets/…` to `assets/…`, which VitePress then resolves under `getting-started/`. +#md # ```@raw html +#md #
+#md #

Run it yourself

+#md #

This page is generated from a single script. Download it as a Jupyter notebook or as a Julia script and run it on your machine. Besides OptimalControl, it uses NLPModelsIpopt, Plots, MadNLP, OrdinaryDiffEqTsit5 and NonlinearSolve, plus MadNLPGPU, CUDA and CUDSS for the GPU section.

+#md #
+#md # ``` #src ============================================================================ # ## The problem, and installing the tools @@ -45,7 +49,7 @@ # # 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. +# 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), CTLie (Lie and Poisson brackets), and CTSolvers (solver orchestration) — that can also be used individually. # # Installation is a single package: # @@ -54,6 +58,9 @@ # Pkg.add("OptimalControl") # ``` # +#md # The solvers, plotting and ODE integrators are separate packages, loaded only when needed: see [Installation](@ref getting-started-installation). +#nb # The solvers, plotting and ODE integrators are separate packages, loaded only when needed: see [Installation](https://control-toolbox.org/OptimalControl.jl/dev/getting-started/installation). +# # 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). using OptimalControl @@ -84,7 +91,7 @@ xf = [0, 0]; # ### The `@def` macro # #md # The [`@def`](@ref modelling-abstract-syntax) macro lets us write the problem almost exactly as the mathematics: -#nb # The [`@def`](https://control-toolbox.org/OptimalControl.jl/stable/modelling/abstract-syntax) macro lets us write the problem almost exactly as the mathematics: +#nb # The [`@def`](https://control-toolbox.org/OptimalControl.jl/dev/modelling/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. @@ -105,7 +112,7 @@ end # ### The same problem with the macro-free (functional) API # #md # The [functional API](@ref modelling-functional-api) builds the *same* model step by step with plain functions — useful for programmatic problem generation or macro-free library code. -#nb # The [functional API](https://control-toolbox.org/OptimalControl.jl/stable/modelling/functional-api) builds the *same* model step by step with plain functions — useful for programmatic problem generation or macro-free library code. +#nb # The [functional API](https://control-toolbox.org/OptimalControl.jl/dev/modelling/functional-api) builds the *same* model step by step with plain functions — useful for programmatic problem generation or macro-free library code. pre = OptimalControl.PreModel() @@ -113,14 +120,15 @@ time!(pre; t0=t0, tf=tf) state!(pre, 2, "x", ["q", "v"]) control!(pre, 1) -function f_energy!(dx, t, x, u, v) +## the last argument of every callback is the optimisation variable (none here) +function f_energy!(dx, t, x, u, var) dx[1] = x[2] dx[2] = u # scalar control: `u`, not `u[1]` return nothing end dynamics!(pre, f_energy!) -function boundary_energy!(b, x0_, xf_, v) +function boundary_energy!(b, x0_, xf_, var) b[1] = x0_[1] - x0[1] b[2] = x0_[2] - x0[2] b[3] = xf_[1] - xf[1] @@ -129,7 +137,7 @@ function boundary_energy!(b, x0_, xf_, v) end constraint!(pre, :boundary; f=boundary_energy!, lb=zeros(4), ub=zeros(4), label=:endpoint) -lagrange_energy(t, x, u, v) = 0.5 * u^2 +lagrange_energy(t, x, u, var) = 0.5 * u^2 objective!(pre, :min; lagrange=lagrange_energy) time_dependence!(pre; autonomous=true) @@ -168,6 +176,10 @@ direct_sol = solve(ocp) plot(direct_sol; size=(800, 600)) +#- +#md @assert isapprox(objective(direct_sol), 6; rtol=1e-3) # hide +#md nothing # hide + # ### 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`: @@ -195,8 +207,8 @@ 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. -#md # For all the ways to specify an initial guess, see [Set an initial guess](@ref solve-initial-guess). -#nb # For all the ways to specify an initial guess, see [Set an initial guess](https://control-toolbox.org/OptimalControl.jl/stable/solve/initial-guess). +#md # For all the ways to specify an initial guess, see [Initial guess](@ref solve-initial-guess). +#nb # For all the ways to specify an initial guess, see [Initial guess](https://control-toolbox.org/OptimalControl.jl/dev/solve/initial-guess). #md # !!! note #md # 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. #nb # **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. @@ -221,7 +233,7 @@ println("iterations, @init guess: ", iterations(sol)) # 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 continuous OCP thus becomes a finite-dimensional NLP in the variables $X = (x_0, \dots, x_N, u_0, \dots, u_{N-1})$, 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 # @@ -266,8 +278,8 @@ end # ### Choosing a solver is trivial # -#md # `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 solve-overview)): -#nb # `solve` uses the defaults (collocation, ADNLP modeler, Ipopt, CPU). Switching solver is just loading a package and passing a token (see [Solve a problem](https://control-toolbox.org/OptimalControl.jl/stable/solve/choosing-a-method)): +#md # `solve` uses the defaults (collocation, ADNLP modeler, Ipopt, CPU). Switching solver is just loading a package and passing a token (see [Choosing a method](@ref solve-choosing-a-method)): +#nb # `solve` uses the defaults (collocation, ADNLP modeler, Ipopt, CPU). Switching solver is just loading a package and passing a token (see [Choosing a method](https://control-toolbox.org/OptimalControl.jl/dev/solve/choosing-a-method)): using MadNLP @@ -277,6 +289,13 @@ 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") +# Both solvers reach the same final altitude, to about six digits; only the number of iterations differs. + +#- +#md @assert isapprox(objective(sol_ipopt), 1.012837; atol=1e-5) # hide +#md @assert isapprox(objective(sol_madnlp), objective(sol_ipopt); rtol=1e-6) # hide +#md nothing # hide + # 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 @@ -294,7 +313,13 @@ 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: +# Warm-started from the coarse solution, the fine solve needs only a handful of iterations. Even counting the warm-up on grid 50, the cascade uses fewer iterations in total than the cold start, and each grid-50 iteration is much cheaper than a grid-1000 one. + +#- +#md @assert iterations(s50) + iterations(s1000) < iterations(sol_cold) # hide +#md nothing # hide + +# Overlay the two solutions to see that the coarse grid already captures the structure: plt = plot(s50; label="50", size=(800, 800)) plot!(plt, s1000; label="1000") @@ -306,7 +331,7 @@ plot!(plt, s1000; label="1000") # 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. -using OrdinaryDiffEqTsit5 # ODE solver (bang-bang simulation callbacks) +using OrdinaryDiffEqTsit5 # ODE solver: bang-bang simulation here, Hamiltonian flows later ## Phase 1: u = 1, stop when m = mf (fuel depleted) bang1!(dx, x, p, t) = (dx[:] = F0(x) + F1(x)) @@ -349,41 +374,57 @@ println( ")", ) -# 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: +println("Extra altitude gain of the optimal strategy: ", + round(100 * (objective(sol_cold) - rf_bang) / (rf_bang - r0); digits=1), " %") + +# The optimal strategy climbs about 2.7 % higher above $r_0$ than the naive one. It does not simply push at the maximum: after a full-thrust phase it throttles along a **singular arc**, where pushing harder would mostly be lost to drag, then coasts. Overlaying the altitude of both strategies against time makes the difference visible: + +#- +#md @assert isapprox(rf_bang, 1.012505; atol=1e-5) # hide +#md @assert objective(sol_cold) > rf_bang # hide +#md nothing # hide ## 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") +## the optimal altitude, read from the solution on its time grid +t_opt = time_grid(sol_cold) +r_opt = [state(sol_cold)(t)[1] for t in t_opt] + +plot(t_opt, r_opt; label="optimal", linewidth=2, xlabel="time", ylabel="altitude", + legend=:bottomright, size=(800, 400), left_margin=5Plots.mm) +plot!(t_bang, r_bang; label="bang-bang", linestyle=:dash, linewidth=2) + +# The bang-bang rocket climbs faster at first, but it burns all its fuel at full speed in the densest air. The optimal one holds back, keeps fuel for later, and ends higher. #src ============================================================================ # ## Solving on a GPU #src ============================================================================ # -# 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. +# 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 an NVIDIA 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. -# -# The GPU stack is armed by **three** loads, not two: `MadNLPGPU`, `CUDA` and `CUDSS`. Miss the third and the failure you get is a missing package, not a missing device. +# The GPU stack needs **three** packages: `MadNLPGPU`, `CUDA` and `CUDSS`. Whether the solve then runs depends on the machine: without a functional GPU (a laptop, a CI runner without a GPU), it fails. We wrap the call in a `try/catch` so that the tour keeps running either way, and print what happened. +#md Base.CoreLogging.disable_logging(Base.CoreLogging.Warn) # hide using MadNLPGPU using CUDA using CUDSS +println("CUDA.functional() = ", CUDA.functional()) try global sol_gpu = solve(goddard, :gpu; grid_size=1000, display=false) - println("GPU solve succeeded — a functional GPU is available.") + println("GPU solve succeeded: r(tf) = ", objective(sol_gpu)) catch e - println("GPU solve failed, as expected without a functional GPU.") - println("CUDA.functional() = ", CUDA.functional()) - println("Exception: ", first(sprint(showerror, e), 400)) + println("GPU solve failed: ", first(sprint(showerror, e), 400)) end +#md Base.CoreLogging.disable_logging(Base.CoreLogging.BelowMinLevel) # hide +#md nothing # hide + +# The output above tells which case the machine that built this page was in. -#md # For the full GPU setup, see [Solve on GPU](@ref solve-gpu). -#nb # For the full GPU setup, see [Solve on GPU](https://control-toolbox.org/OptimalControl.jl/stable/solve/gpu). +#md # For the full GPU setup, see [GPU](@ref solve-gpu). +#nb # For the full GPU setup, see [GPU](https://control-toolbox.org/OptimalControl.jl/dev/solve/gpu). #src ============================================================================ # ## The indirect method @@ -391,25 +432,13 @@ end # # 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 +# In control-toolbox we systematically pair the direct method with the **indirect** one, based on Pontryagin's Maximum Principle (PMP). With the pseudo-Hamiltonian # # ```math -# H(x,p,u) = p\,f(x,u) + p^0 f^0(x,u) \qquad (\text{normal case } p^0 = -1). +# 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. +# the indirect method proceeds in three steps. # # **Step 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 # @@ -435,14 +464,13 @@ end # # For the energy problem, $H = p_1 v + p_2 u - u^2/2$, so the maximiser is $u = p_2$. -using OrdinaryDiffEqTsit5 # ODE solver (Hamiltonian flow) -using NonlinearSolve # nonlinear equations (shooting) +using NonlinearSolve # nonlinear equations (shooting); the ODE solver is already loaded ## maximising control in feedback form -u_max(x, p) = p[2] +u_feedback(x, p) = p[2] ## Hamiltonian flow of the OCP -φ = Flow(ocp, u_max); +φ = Flow(ocp, u_feedback); ## state projection π(x, p) = x proj((x, p)) = x @@ -465,6 +493,12 @@ p0_sol = shooting_sol.u println("costate p0 = ", p0_sol) println("shoot S(p0) = ", S(p0_sol)) +# The shooting converges to $p_0 = (12, 6)$, the exact initial costate: the costate of the direct solution was already very close to it. + +#- +#md @assert isapprox(p0_sol, [12, 6]; atol=1e-8) # hide +#md nothing # hide + # Reconstruct the indirect solution from the flow and overlay it with the direct solution: indirect_sol = φ((t0, tf), x0, p0_sol) @@ -472,8 +506,8 @@ indirect_sol = φ((t0, tf), x0, p0_sol) plt_compare = plot(direct_sol; label="direct", size=(800, 600)) plot!(plt_compare, indirect_sol; label="indirect") -#md # See [Compute flows from optimal control problems](@ref flows-from-ocp) for the flow construction, and the [indirect simple shooting tutorial](@extref tutorial-indirect-simple-shooting). -#nb # See [Compute flows from optimal control problems](https://control-toolbox.org/OptimalControl.jl/stable/flows/from-ocp) for the flow construction, and the [indirect simple shooting tutorial](https://control-toolbox.org/Tutorials.jl/stable/). +#md # See [From an OCP](@ref flows-from-ocp) for the flow construction, and the [indirect simple shooting tutorial](@extref tutorial-indirect-simple-shooting). +#nb # See [From an OCP](https://control-toolbox.org/OptimalControl.jl/dev/flows/from-ocp) for the flow construction, and the [indirect simple shooting tutorial](https://control-toolbox.org/Tutorials.jl/stable/tutorial-iss.html). #src ============================================================================ # ## Going further @@ -481,7 +515,7 @@ plot!(plt_compare, indirect_sol; label="indirect") # # **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). #md # See [control-free problems](@ref examples-control-free). -#nb # See [control-free problems](https://control-toolbox.org/OptimalControl.jl/stable/examples/control-free). +#nb # See [control-free problems](https://control-toolbox.org/OptimalControl.jl/dev/examples/control-free). #src NOTE(v1): parameter estimation is only *mentioned* here. The worked example is an #src extension (out of v1) — see .reports/tutorial-brainstorming.md "Extensions futures". # @@ -490,8 +524,8 @@ plot!(plt_compare, indirect_sol; label="indirect") #md # - Singular control (control-affine systems) — [singular control](@ref examples-singular-control) #md # - State constraint — [state constraint](@ref examples-state-constraint) #md # - Goddard problem — free final time, a singular arc, a state constraint and a structured shooting all at once — [Goddard tutorial](@extref Tutorials tutorial-goddard) -#nb # - Singular control (control-affine systems) — -#nb # - State constraint — +#nb # - Singular control (control-affine systems) — +#nb # - State constraint — #nb # - Goddard problem — free final time, a singular arc, a state constraint and a structured shooting all at once — # #md # **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). From 6ff57adadb75be9e7553c1d79ed257002b7e445b Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 09:30:01 +0200 Subject: [PATCH 08/57] docs(formulation): define dimensions, add Notation and conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - x ∈ ℝⁿ, u ∈ ℝᵐ, v ∈ ℝ^q are defined (m and k were used undefined). - Name the constraint kinds; mention → max and autonomy. - New "Notation and conventions" section, the single reference for the indirect-method pages: pseudo-Hamiltonian H and maximised Hamiltonian 𝐇 (PseudoHamiltonian / Hamiltonian), maximum principle, state constraints c(x) ≥ 0 with + μ c, minimal time in Mayer form (𝐇(t_f) = 1), and the variable costate. Co-Authored-By: Claude Opus 5.5 --- docs/src/modelling/formulation.md | 99 ++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/docs/src/modelling/formulation.md b/docs/src/modelling/formulation.md index d742004cb..3b9caa758 100644 --- a/docs/src/modelling/formulation.md +++ b/docs/src/modelling/formulation.md @@ -1,18 +1,27 @@ # [Formulation](@id modelling-formulation) -An optimal control problem (OCP) with fixed initial and final times can be described as minimising the cost functional (in Bolza form) +This page fixes the mathematical setting and the notation used throughout the documentation. +The [`@def` syntax](@ref modelling-abstract-syntax) and the +[functional API](@ref modelling-functional-api) both transcribe it into code. + +## The problem + +An optimal control problem (OCP) with fixed initial and final times consists in minimising +the cost functional, written in **Bolza form**, ```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 +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, ``` -where the state $x$ and the control $u$ are functions of time $t$, subject for $t \in [t_0, t_f]$ to the differential constraint +where the **state** $x(t) \in \mathbb{R}^n$ and the **control** $u(t) \in \mathbb{R}^m$ are +functions of time $t \in [t_0, t_f]$. They are subject to the differential constraint (the +**dynamics**) ```math \dot{x}(t) = f(t, x(t), u(t)) ``` -and other constraints such as +and to other constraints such as ```math \begin{array}{llcll} @@ -23,17 +32,27 @@ b_{\mathrm{lower}} & \le & b(x(t_0), x(t_f)) & \le & b_{\mathrm{upper}}. \end{array} ``` +The first two lines are **box constraints**, $c$ is a **path constraint**, and $b$ gathers the +**boundary conditions**. An equality constraint is the case where both bounds are equal. + If $g = 0$, the cost is said to be in **Lagrange form**; if $f^0 = 0$, it is in **Mayer form**. +Maximising $J$ is the same as minimising $-J$, so a cost can be written with `→ max` as well. + +When $f^0$, $f$ and $c$ do not depend on $t$ explicitly, the problem is **autonomous**. See +[Time dependence](@ref modelling-inspect-time-dependence) for how to check it on a model. ## Free times and extra variables -The initial time $t_0$ and the final time $t_f$ may also be free, that is part of the optimisation variables: +The initial time $t_0$ and the final time $t_f$ may also be free, that is, part of the +optimisation variables: ```math J(x, u, t_0, t_f) \to \min. ``` -More generally, a vector $v \in \mathbb{R}^k$ of $k$ additional variables can be introduced (it may contain $t_0$, $t_f$, or any other free parameter). The cost, dynamics, and constraints then all depend on $v$: +More generally, a vector $v \in \mathbb{R}^q$ of $q$ additional **variables** can be optimised +together with $x$ and $u$. It may contain $t_0$, $t_f$, or any other free parameter, and the +cost, the dynamics and the constraints may all depend on it: ```math J(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 \to \min, @@ -51,10 +70,76 @@ v_{\mathrm{lower}} \le v \le v_{\mathrm{upper}}. ## The control-free case -Nothing above requires a control to be present: taking $m = 0$ (no $u$) is a legitimate degenerate case, used to fit or optimise parameters of a dynamical system rather than to steer it. See [No control](@ref modelling-without-control) for how to declare and solve such problems. +Nothing above requires a control: taking $m = 0$ (no $u$) is a legitimate degenerate case. It +is used to fit or optimise the parameters $v$ of a dynamical system rather than to steer it. +See [No control](@ref modelling-without-control) for how to declare and solve such problems. + +## [Notation and conventions](@id modelling-formulation-conventions) + +The pages on the indirect method ([Flows](@ref flows-overview), [Geometry](@ref geometry-overview) +and the [examples](@ref examples-gallery)) use the following conventions. They refer to this +section rather than redefine them. + +**Dimensions.** $x(t) \in \mathbb{R}^n$, $u(t) \in \mathbb{R}^m$, $v \in \mathbb{R}^q$. A +dimension-one quantity is a scalar, in the mathematics as in the code. + +**Pseudo-Hamiltonian.** With the costate $p(t) \in \mathbb{R}^n$ and the constant $p^0 \le 0$, + +```math +H(t, x, p, u, v) = p \cdot f(t, x, u, v) + p^0 f^0(t, x, u, v). +``` + +The normal case is $p^0 = -1$; $p^0 = 0$ is the abnormal case. In the code, $H$ is a +`PseudoHamiltonian`. + +**Maximum principle.** Along an optimal trajectory, the control **maximises** $H$. When this +maximisation gives the control in feedback form $u(t, x, p, v)$, the **maximised Hamiltonian** is + +```math +\mathbf{H}(t, x, p, v) = H(t, x, p, u(t, x, p, v), v), +``` + +a `Hamiltonian` in the code. Its **Hamiltonian vector field** +$\vec{\mathbf{H}} = (\nabla_p \mathbf{H}, -\nabla_x \mathbf{H})$ gives the extremals: + +```math +\dot{x}(t) = \nabla_p \mathbf{H}(t, x(t), p(t), v), \qquad +\dot{p}(t) = -\nabla_x \mathbf{H}(t, x(t), p(t), v). +``` + +**State constraints.** A state constraint is a path constraint that does not depend on the +control. It is written in the normalised form $c(x) \ge 0$. Along a boundary arc, where +$c(x(t)) = 0$, the pseudo-Hamiltonian gains the term $+\,\mu\, c(x)$, with a multiplier +$\mu \ge 0$: + +```math +H(t, x, p, u, v) = p \cdot f(t, x, u, v) + p^0 f^0(t, x, u, v) + \mu\, c(x). +``` + +**Minimal time.** A free final time to minimise is written as a Mayer cost, $t_f \to \min$. +Then $f^0 = 0$, so $H = p \cdot f$, and the transversality condition reads + +```math +\mathbf{H}(t_f) = -p^0 = 1. +``` + +Writing the same cost in Lagrange form, $t_f = \int_{t_0}^{t_f} 1\,\mathrm{d}t$, gives instead +$H = p \cdot f - 1$ and $\mathbf{H} \equiv 0$ for an autonomous problem. Both are correct; the +documentation uses the Mayer form. + +**Variable costate.** When the variable $v$ is optimised, the indirect method treats it as a +constant state ($\dot v = 0$) with its own costate $p_v$: + +```math +\dot{p}_v(t) = -\nabla_v H, \qquad p_v(t_0) = 0, \qquad p_v(t_f) = p^0\, \nabla_v g, +``` + +for a Mayer term $g$ that depends on $v$ and no boundary condition that involves $v$. For +instance, with the cost term $\omega^2$ and $p^0 = -1$, $p_\omega(t_f) = -2\omega$. ## See also - [Abstract syntax (`@def`)](@ref modelling-abstract-syntax) — write this formulation directly as Julia code. - [Functional API](@ref modelling-functional-api) — build the same model without the macro. - [No control](@ref modelling-without-control) — the $m = 0$ case. +- [Flows](@ref flows-overview) — build the Hamiltonian flows of the indirect method. From d4331e393289fe730ac90287071576179b1bc8fd Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 09:40:13 +0200 Subject: [PATCH 09/57] docs(abstract-syntax): structure section, ASCII table, labels, verified rules - New "Structure of a definition": declarations first (the variable before the time when the bounds use it), the two named forms, and when names are read (constants at build time, functions at solve time). This replaces two contradictory notes on defining names before or after the block. - One table for the ASCII alternatives (in, R^2, derivative, integral, <=, =>). - Labels: dual(sol, ocp, :label) works, a number (1) is stored as :eq1 (new anchor results-solution-duals on the solution page). - Constraint kinds named box / general instead of linear / nonlinear; the "damped" integrator is now damped (- c(t) v(t)). - Known upstream problems as one-liners: CTParser#352 (generic hint), #353 (single = in the dynamics), #354 (gensyms in trace mode). - Known issues section removed: the #481 examples all solve with the current versions. - Warnings shown without the local package path; wording fixes. Co-Authored-By: Claude Opus 5.5 --- docs/src/modelling/abstract-syntax.md | 128 ++++++++++++++++---------- docs/src/results/solution.md | 2 +- 2 files changed, 82 insertions(+), 48 deletions(-) diff --git a/docs/src/modelling/abstract-syntax.md b/docs/src/modelling/abstract-syntax.md index 47f329cbb..89e20b048 100644 --- a/docs/src/modelling/abstract-syntax.md +++ b/docs/src/modelling/abstract-syntax.md @@ -5,7 +5,7 @@ The full grammar of OptimalControl.jl's small *Domain Specific Language* is give - 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 (see [Formulation](@ref modelling-formulation)). -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. Abstract definitions use the macro [`@def`](@ref). +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. Abstract definitions use the macro [`@def`](@ref). !!! note "About the code blocks on this page" Blocks written `:( … )` — such as the state pattern `:( $x ∈ R^$n, state )` — are @@ -21,10 +21,40 @@ While the syntax will be transparent to those users familiar with Julia expressi ```@setup abs using OptimalControl +using Logging: with_logger, ConsoleLogger +# show warnings without the source location (a local package path) +quiet_location(f) = with_logger(f, ConsoleLogger(stderr; meta_formatter=(level, args...) -> (:yellow, "Warning:", ""))) data(t) = 2exp(0.5t) # observed-data stub (parameter estimation) -c(t) = 1.0 # coefficient stub (damped integrator) +c(t) = 1.0 # damping coefficient (damped integrator) ``` +## [Structure of a definition](@id modelling-abstract-syntax-structure) + +A definition is a `begin … end` block passed to `@def`: + +- **Declarations come first:** the variable (if any), the time, the state, and the control (if + any), preferably in this order. The variable must come before the time when the time bounds + use it, as in `t ∈ [0, tf], time`. +- **Then, in any order:** the dynamics, the constraints and the cost. +- **Two equivalent forms:** `ocp = @def begin … end` and `@def ocp begin … end` both bind the + model to `ocp`. The second form also accepts a trailing `true` that turns on the + [trace mode](@ref modelling-abstract-syntax-aliases). +- **When names are read:** constants used in the declarations or in the bounds of a + constraint (`t0`, `tf`, `x0`, …) are read when the model is built, so they must be defined + *before* the block. Functions called in the dynamics, the constraints or the cost are only + called when these are evaluated (during `solve`), so they may be defined after the block. + +The symbols have plain ASCII alternatives: + +| Unicode | ASCII | +| --- | --- | +| `t ∈ [0, 1]` | `t in [0, 1]` | +| `R²` | `R^2` | +| `ẋ(t)`, `∂(x)(t)` | `derivative(x)(t)` | +| `∫(…)` | `integral(…)` | +| `≤`, `≥` | `<=`, `>=` | +| `→ min` | `=> min` | + ## [Variable](@id modelling-abstract-syntax-variable) ```julia @@ -95,7 +125,7 @@ end nothing # hide ``` -One (or even the two bounds) can be variable, typically for minimum time problems (see [Mayer cost](@ref modelling-abstract-syntax-mayer) section): +One bound (or both) can be variable, typically for minimum time problems (see [Mayer cost](@ref modelling-abstract-syntax-mayer) section): ```@example abs @def begin @@ -174,8 +204,8 @@ end nothing # hide ``` -!!! 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}`. This same convention applies on the [functional API](@ref modelling-functional-api): a dimension-1 quantity is a scalar, not a length-1 vector — see [Shapes in callbacks](@ref modelling-functional-api-shapes). +!!! note "1-D is a scalar" + A one-dimensional variable, state or control is treated as a scalar (`Real`), not a vector (`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}`. This same convention applies on the [functional API](@ref modelling-functional-api): a dimension-1 quantity is a scalar, not a length-1 vector — see [Shapes in callbacks](@ref modelling-functional-api-shapes). ## [Problems without a control](@id modelling-abstract-syntax-control-free) @@ -291,7 +321,7 @@ nothing # hide ``` !!! note - The vector fields `F₀` and `F₁` can be defined afterwards, as they only need to be available when the dynamics will be evaluated. + The vector fields `F₀` and `F₁` are defined after the block: functions are only called when the dynamics is evaluated, during `solve` (see [Structure of a definition](@ref modelling-abstract-syntax-structure)). While it is also possible to declare the dynamics component after component (see below), one may equivalently use *aliases* (check the relevant [aliases](@ref modelling-abstract-syntax-aliases) section below): @@ -302,7 +332,7 @@ While it is also possible to declare the dynamics component after component (see x = (q, v) ∈ R², state u ∈ R, control q̇ = v(t) - v̇ = u(t) - c(t) + v̇ = u(t) - c(t) * v(t) ẋ(t) == [q̇, v̇] ∫(u(t)^2) → min end @@ -324,7 +354,7 @@ The dynamics can also be declared coordinate by coordinate. The previous example x = (q, v) ∈ R², state u ∈ R, control ∂(q)(t) == v(t) - ∂(v)(t) == u(t) - c(t) + ∂(v)(t) == u(t) - c(t) * v(t) ∫(u(t)^2) → min end nothing # hide @@ -342,18 +372,18 @@ nothing # hide 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. +- of five types: boundary, variable, control, state, mixed (the last three are *path* constraints, that is, constraints evaluated at all times); +- box constraints (a range on a single component, such as `x₂(t) ≤ 1`) or general ones (any expression, such as `u(t)^2`); +- 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. +- two boundary constraints, +- one box constraint on the variable, +- one box constraint on the state, +- one (two-sided) general control constraint. ```@example abs @def begin @@ -378,26 +408,26 @@ nothing # hide !!! note Symbols like `<=` or `>=` are also authorised: -```@example abs -@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 - tf → min -end -nothing # hide -``` + ```@example abs + @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 + tf → min + end + nothing # hide + ``` !!! 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 errors: + 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 errors (the example quoted in the message is generic, see [CTParser#352](https://github.com/control-toolbox/CTParser.jl/issues/352); here, write `x₂(t) ≥ 1`): ```@repl abs try # hide @@ -471,6 +501,7 @@ When the **same scalar component** is targeted by several box-constraint declara For instance, ```@example abs +quiet_location() do # hide @def begin t ∈ [0, 1], time x = (q, v) ∈ R², state @@ -480,6 +511,7 @@ For instance, ẋ(t) == [v(t), u(t)] ∫(u(t)^2) → min end +end # hide nothing # hide ``` @@ -488,6 +520,7 @@ yields the effective constraint `1 ≤ q(t) ≤ 2`, with `aliases = [:q_wide, :q Conversely, the following declares an empty feasible set and raises an error at build time: ```@repl abs +quiet_location() do # hide try # hide @def begin t ∈ [0, 1], time @@ -501,6 +534,7 @@ end catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide +end # hide ``` ## [Mayer cost](@id modelling-abstract-syntax-mayer) @@ -543,7 +577,7 @@ end :( $e1 * ∫($e2) → max ) ``` -Lagrange (integral) costs are defined used the symbol `∫`, *with parentheses*. The keyword `integral` can also be used: +Lagrange (integral) costs are defined using the symbol `∫`, *with parentheses*. The keyword `integral` can also be used: ```@example abs @def begin @@ -650,7 +684,7 @@ nothing # hide :( $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 you can define your own aliases when declaring the [variable](@ref modelling-abstract-syntax-variable), [state](@ref modelling-abstract-syntax-state) and [control](@ref modelling-abstract-syntax-control). Arbitrary aliases can be further defined, as below (compare with previous examples in the [dynamics](@ref modelling-abstract-syntax-dynamics) section): +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 between 1 and 9), and we have also seen that you can define your own aliases when declaring the [variable](@ref modelling-abstract-syntax-variable), [state](@ref modelling-abstract-syntax-state) and [control](@ref modelling-abstract-syntax-control). Arbitrary aliases can be further defined, as below (compare with previous examples in the [dynamics](@ref modelling-abstract-syntax-dynamics) section): ```@example abs @def begin @@ -666,10 +700,10 @@ nothing # hide ``` !!! 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. + 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, the constants and functions used within the block are ordinary Julia names, defined outside it (see [Structure of a definition](@ref modelling-abstract-syntax-structure) for when). !!! 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: + 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. The trace still shows some internal names, such as `var"u##…"[1]` for `u` ([CTParser#354](https://github.com/control-toolbox/CTParser.jl/issues/354)): ```@repl abs @def damped_integrator begin @@ -678,14 +712,14 @@ nothing # hide x = (q, v) ∈ R², state u ∈ R, control q̇ = v(t) - v̇ = u(t) - c(t) + v̇ = u(t) - c(t) * v(t) ẋ(t) == [q̇, v̇] ∫(u(t)^2) → min 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: + The dynamics of an OCP is a constraint: use `==`, not a single `=`, which would try to define an alias. The error does not say so explicitly yet ([CTParser#353](https://github.com/control-toolbox/CTParser.jl/issues/353)): ```@repl abs try # hide @@ -703,10 +737,12 @@ nothing # hide end # hide ``` -## Misc +## [Labels](@id modelling-abstract-syntax-labels) -- 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): +A constraint can carry a label, written after a comma, as in a numbered equation. A label +names the constraint, for instance to retrieve its multiplier with +[`dual(sol, ocp, :label)`](@ref results-solution-duals). A number `(1)` is stored as the +label `:eq1`: ```@example abs @def damped_integrator begin @@ -717,7 +753,7 @@ nothing # hide tf ≥ 0, (1) q(0) == 2, (♡) q̇ = v(t) - v̇ = u(t) - c(t) + v̇ = u(t) - c(t) * v(t) ẋ(t) == [q̇, v̇] x(t).^2 ≤ [1, 2], (state_con) ∫(u(t)^2) → min @@ -725,12 +761,10 @@ end nothing # hide ``` -- 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 modelling-abstract-syntax-known-issues) +Here the labels are `:eq1`, `:♡` and `:state_con`. -- [Reverse over forward AD issues with ADNLP](https://github.com/control-toolbox/OptimalControl.jl/issues/481#issuecomment-3471352183) +Parsing errors report the line of the `begin … end` block where they occur. For complete +problems written with this syntax, see the [example gallery](@ref examples-gallery). ## See also diff --git a/docs/src/results/solution.md b/docs/src/results/solution.md index e4bcd1e97..45f9d225b 100644 --- a/docs/src/results/solution.md +++ b/docs/src/results/solution.md @@ -156,7 +156,7 @@ iterations(sol), constraints_violation(sol) See [Migration](@ref migration) for the full list of renamed spellings. -## Dual variables +## [Dual variables](@id results-solution-duals) Dual variables (Lagrange multipliers) give sensitivity information. A richer problem to show them on: From 11092ee575605c725c5709997925824b20615311 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 09:45:35 +0200 Subject: [PATCH 10/57] docs(assets): restore the 40/60 columns, remove dead custom.css The functional API page lays out each @def model beside its functional twin with .responsive-columns-40-60, a class only defined in custom.css, which the VitePress theme never loads: the columns were stacked on the live site. The rule moves to style.css; narrow screens stack the two columns instead of hiding the second one. custom.css held nothing else that style.css lacks. Co-Authored-By: Claude Opus 5.5 --- docs/src/.vitepress/theme/style.css | 34 ++++++++++++- docs/src/assets/custom.css | 75 ----------------------------- 2 files changed, 33 insertions(+), 76 deletions(-) delete mode 100644 docs/src/assets/custom.css diff --git a/docs/src/.vitepress/theme/style.css b/docs/src/.vitepress/theme/style.css index a434faee3..0cbcbe34e 100644 --- a/docs/src/.vitepress/theme/style.css +++ b/docs/src/.vitepress/theme/style.css @@ -336,7 +336,7 @@ mjx-container svg path { } /* ============================================================================ - Custom layout helpers (migrated from docs/src/assets/custom.css) + Custom layout helpers (two-column layouts used by the examples and the functional API) ============================================================================ */ /* Responsive layout for two-column content */ @@ -415,6 +415,38 @@ mjx-container svg path { } } +/* 40 % / 60 % two-column layout (modelling/functional-api.md: the `@def` model beside its + functional-API twin). Both columns carry content, so narrow screens stack them rather + than hide one, unlike `.responsive-columns-left-priority` above. */ +.responsive-columns-40-60 { + display: flex; + gap: 1rem; + align-items: flex-start; + margin-bottom: 1em; +} + +.responsive-columns-40-60 > div:first-child { + flex: 4; + min-width: 0; +} + +.responsive-columns-40-60 > div:last-child { + flex: 6; + min-width: 0; +} + +@media (max-width: 700px) { + .responsive-columns-40-60 { + flex-direction: column; + } + + .responsive-columns-40-60 > div:first-child, + .responsive-columns-40-60 > div:last-child { + flex: 1 1 auto; + width: 100%; + } +} + /* ============================================================================ Dark mode: navbar logo. The home page swaps an exact light/dark SVG pair, but the navbar logo is a single `` (DocumenterVitepress fills `logo:` with one diff --git a/docs/src/assets/custom.css b/docs/src/assets/custom.css deleted file mode 100644 index 07f3cccb3..000000000 --- a/docs/src/assets/custom.css +++ /dev/null @@ -1,75 +0,0 @@ -/* Responsive layout for two-column content */ -.responsive-columns-left-priority { - display: flex; - gap: 1rem; - align-items: flex-start; - margin-bottom: 1em; -} - -.responsive-columns-left-priority > div { - flex: 1; - min-width: 0; - transition: opacity 0.5s ease-in-out, flex 0.5s ease-in-out, max-width 0.5s ease-in-out, max-height 0.5s ease-in-out; -} - -.responsive-columns-left-priority > div:last-child { - opacity: 1; - max-width: 100%; - max-height: none; -} - -/* Media query for screens smaller than 700px */ -@media (max-width: 700px) { - .responsive-columns-left-priority > div:last-child { - opacity: 0; - max-width: 0; - max-height: 0; - flex: 0 0 0; - overflow: hidden; - margin: 0; - padding: 0; - } - - .responsive-columns-left-priority > div:first-child { - flex: 1 1 100%; - } -} - -/* 50% / 50% two-column layout */ -.responsive-columns-40-60 { - display: flex; - gap: 1rem; - align-items: flex-start; - margin-bottom: 1em; -} - -.responsive-columns-40-60 > div:first-child { - flex: 4; - min-width: 0; - transition: opacity 0.5s ease-in-out, flex 0.5s ease-in-out, max-width 0.5s ease-in-out, max-height 0.5s ease-in-out; -} - -.responsive-columns-40-60 > div:last-child { - flex: 6; - min-width: 0; - opacity: 1; - max-width: 100%; - max-height: none; - transition: opacity 0.5s ease-in-out, flex 0.5s ease-in-out, max-width 0.5s ease-in-out, max-height 0.5s ease-in-out; -} - -@media (max-width: 700px) { - .responsive-columns-40-60 > div:last-child { - opacity: 0; - max-width: 0; - max-height: 0; - flex: 0 0 0; - overflow: hidden; - margin: 0; - padding: 0; - } - - .responsive-columns-40-60 > div:first-child { - flex: 1 1 100%; - } -} \ No newline at end of file From 6ae47426fba6dcc1a69a45750a4d4995a9d97b2e Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 09:45:35 +0200 Subject: [PATCH 11/57] docs(functional-api): box for vmax, load Plots, build-checked equivalence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Problem 7: the velocity bound is a labelled box (v(t) ≤ VMAX, and constraint!(pre, :state; rg=2:2, …)) instead of the "+ 0.0" path constraint, whose justification was wrong. - Problem 1 loads Plots before plotting; its @def body is indented. - Hidden asserts: macro and functional forms reach the same cost (or the same λ) on all seven problems; J = 6, t_f = 2, λ ≈ 0.5 where known. - No typeof output, no test-file path, no "one place on the site" claim; "Mayer (boundary) cost", "general" constraints, "optimised". Co-Authored-By: Claude Opus 5.5 --- docs/src/modelling/functional-api.md | 81 +++++++++++++++++++--------- 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/docs/src/modelling/functional-api.md b/docs/src/modelling/functional-api.md index cb8d1ad4d..05aa86b88 100644 --- a/docs/src/modelling/functional-api.md +++ b/docs/src/modelling/functional-api.md @@ -6,7 +6,7 @@ The functional API uses `OptimalControl.PreModel` as a mutable builder, populate !!! 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. + When a problem is defined with the functional API, [`definition`](@ref)`(ocp)` is empty: no abstract expression is stored. This contrasts with `@def`, which records the full DSL expression for display and introspection. !!! warning "Modeler compatibility" @@ -26,7 +26,7 @@ The functional API mirrors the [Formulation](@ref modelling-formulation). The co | :--- | :--- | | 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) | +| Mayer (boundary) cost $g(x_0, x_f)$ | `may` passed to [`objective!`](@ref) | | Path constraint $c(t, x, u)$ | `p!` passed to [`constraint!`](@ref CTModels.Building.constraint!)`(pre, :path; ...)` | | Boundary constraint $b(x_0, x_f)$ | `b!` passed to [`constraint!`](@ref CTModels.Building.constraint!)`(pre, :boundary; ...)` | | Extra variable $v$ | [`variable!`](@ref) (extra argument to all the callbacks above) | @@ -63,7 +63,7 @@ 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 +# Mayer (boundary) cost — out-of-place, signature: may(x0, xf, v) → scalar # x0 : initial state (scalar if n=1, vector of length n otherwise) # xf : final state (scalar if n=1, vector of length n otherwise) may(x0, xf, v) = ... @@ -81,7 +81,7 @@ 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 +# (b) General constraints defined by a function — :boundary, :path # The constraint reads: lb ≤ f(...) ≤ ub (use lb=ub for equality). # # Boundary — in-place: b!(val, x0, xf, v) (same shape as Mayer) @@ -122,6 +122,7 @@ The simplest case: fixed time interval, boundary constraints, autonomous dynamic ```@example ex-energy using OptimalControl using NLPModelsIpopt +using Plots t0 = 0.0; tf = 1.0; x0 = [-1.0, 0.0]; xf = [0.0, 0.0] nothing # hide ``` @@ -136,16 +137,16 @@ nothing # hide ```@example ex-energy ocp_macro = @def begin -t ∈ [t0, tf], time -x = (q, v) ∈ R², state -u ∈ R, control + t ∈ [t0, tf], time + x = (q, v) ∈ R², state + u ∈ R, control -x(t0) == x0 -x(tf) == xf + x(t0) == x0 + x(tf) == xf -ẋ(t) == [v(t), u(t)] + ẋ(t) == [v(t), u(t)] -0.5∫( u(t)^2 ) → min + 0.5∫( u(t)^2 ) → min end nothing # hide @@ -217,6 +218,12 @@ println( ) ``` +```@example ex-energy +@assert isapprox(objective(sol_macro), objective(sol_func); rtol=1e-8) # hide +@assert isapprox(objective(sol_macro), 6; rtol=1e-3) # hide +nothing # hide +``` + ```@example ex-energy plt = plot( sol_macro, :state, :control; label="Macro", color=1, size=(800, 600) @@ -335,6 +342,12 @@ println( ) ``` +```@example ex-time +@assert isapprox(objective(sol_macro), objective(sol_func); rtol=1e-6) # hide +@assert isapprox(objective(sol_macro), 2; rtol=1e-2) # hide +nothing # hide +``` + Full worked story (direct + indirect): [Time minimisation](@ref examples-double-integrator-time) · [example gallery](@ref examples-gallery). ### 3. Parameter estimation — no control @@ -428,11 +441,17 @@ println( ) ``` +```@example ex-control-free +@assert isapprox(variable(sol_macro), variable(sol_func); rtol=1e-6) # hide +@assert isapprox(variable(sol_macro), 0.5; atol=5e-2) # hide +nothing # hide +``` + 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. +The growth problem again, now with a control input and a quadratic control cost — a **variable** (`λ`) and a **control** (`u`) optimised at once. ```@example ex-control-variable using OptimalControl @@ -524,6 +543,11 @@ println( ) ``` +```@example ex-control-variable +@assert isapprox(objective(sol_macro), objective(sol_func); rtol=1e-6) # hide +nothing # hide +``` + Full worked story (direct + indirect): [Control and variable together](@ref examples-control-and-variable) · [example gallery](@ref examples-gallery). ### 5. Turnpike — bang–singular–bang @@ -617,6 +641,11 @@ println( ) ``` +```@example ex-turnpike +@assert isapprox(objective(sol_macro), objective(sol_func); rtol=1e-6) # hide +nothing # hide +``` + Full worked story (direct + indirect): [Turnpike (bang–singular–bang)](@ref examples-turnpike) · [example gallery](@ref examples-gallery). ### 6. Singular control @@ -719,11 +748,16 @@ println( ) ``` +```@example ex-singular +@assert isapprox(objective(sol_macro), objective(sol_func); rtol=1e-6) # hide +nothing # hide +``` + 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. +The energy-minimal transfer of problem 1, now with an upper bound on the velocity: a **box** constraint on the state component `v`, with the label `vmax` (its multiplier is then `dual(sol, ocp, :vmax)`, see [Labels](@ref modelling-abstract-syntax-labels)). Without it, the velocity would peak at 1.5. ```@example ex-state-constraint using OptimalControl @@ -749,7 +783,7 @@ ocp_macro = @def begin x(t0) == x0 x(tf) == xf - v(t) + 0.0 ≤ VMAX, (vmax) + v(t) ≤ VMAX, (vmax) ẋ(t) == [v(t), u(t)] @@ -791,13 +825,7 @@ 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 -) +constraint!(pre, :state; rg=2:2, lb=[-Inf], ub=[VMAX], label=:vmax) # v(t) ≤ VMAX objective!(pre, :min; lagrange=(t, x, u, v) -> 0.5 * u^2) @@ -821,6 +849,11 @@ println( ) ``` +```@example ex-state-constraint +@assert isapprox(objective(sol_macro), objective(sol_func); rtol=1e-6) # hide +nothing # hide +``` + Full worked story (direct + indirect): [State constraint](@ref examples-state-constraint) · [example gallery](@ref examples-gallery). ## [Shapes in callbacks](@id modelling-functional-api-shapes) @@ -834,7 +867,7 @@ u_macro(t0), u_func(t0) ``` ```@example ex-energy -typeof(u_macro(t0)), typeof(u_func(t0)) +u_macro(t0) isa Real, u_func(t0) isa Real ``` The **in-place output buffer is the one exception**: `dx` (dynamics), `val` (boundary/path constraints) is always a vector of its declared length, written by index, even when that length is 1 — a scalar cannot be mutated in place: @@ -843,8 +876,6 @@ The **in-place output buffer is the one exception**: `dx` (dynamics), `val` (bou f!(r, t, x, u, v) = (r[1] = -x + u; nothing) # r vector; x,u scalars ``` -This is the one place on the site where this is spelled out; every other page just follows it. - ## Order and preconditions - `variable!` → before `time!` when using free-time indices (`indf`, `ind0`) @@ -854,7 +885,7 @@ This is the one place on the site where this is spelled out; every other page ju ## Equivalence -The abstract and functional forms are not just "meant to agree" — it's a tested contract. `test/suite/problems/test_forms_equivalent.jl` builds both forms for every problem in the shared test library and asserts they agree on dimensions, traits (`is_autonomous`, `is_variable`, `is_control_free`), time-horizon status, cost, dynamics, and constraint-dimension accessors. The one deliberate difference the test itself asserts: `has_abstract_definition` is `true` only for the `@def` form, as shown above. +The abstract and functional forms are not just "meant to agree": it is a tested contract. The test suite of OptimalControl builds both forms for every problem of its problem library and checks that they agree on dimensions, traits (`is_autonomous`, `is_variable`, `is_control_free`), time horizon, cost, dynamics and constraint dimensions. The one deliberate difference: `has_abstract_definition` is `true` only for the `@def` form, as shown above. On this page, the build also checks that the two forms of each problem reach the same optimal cost. ## See also From a6676ceab47049e07f5737f4d48df211f86be96b Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 09:49:19 +0200 Subject: [PATCH 12/57] docs(without-control): "Control-free problems", solve both examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Title and sidebar: "Control-free problems" (was "No control"). - Drop the implementation details (EmptyControlModel, dispatch on the control field's type), shown twice; show is_control_free(ocp) instead. - The harmonic oscillator is now solved: ω = π/2 is recovered. - Hidden asserts: λ ≈ 0.5, ω ≈ π/2. - "Adding a control back" removed: it described two Flow guards, which belong to the Flows pages (constrained-arcs already shows the second one). Co-Authored-By: Claude Opus 5.5 --- docs/make.jl | 2 +- docs/src/modelling/without-control.md | 97 ++++++++++++++------------- 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 441a3e38f..e075253db 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -288,7 +288,7 @@ with_api_reference(src_dir, ext_dir) do api_pages "Formulation" => "modelling/formulation.md", "Abstract syntax (@def)" => "modelling/abstract-syntax.md", "Functional API" => "modelling/functional-api.md", - "No control" => "modelling/without-control.md", + "Control-free problems" => "modelling/without-control.md", "Inspect a problem" => "modelling/inspect.md", "With AI" => "modelling/with-ai.md", ], diff --git a/docs/src/modelling/without-control.md b/docs/src/modelling/without-control.md index 0aeec3900..afc16db17 100644 --- a/docs/src/modelling/without-control.md +++ b/docs/src/modelling/without-control.md @@ -1,26 +1,29 @@ -# [No control](@id modelling-without-control) +# [Control-free problems](@id modelling-without-control) ## What this is for -Control-free problems are optimal control problems without a control variable — used for **optimising constant parameters in dynamical systems**, such as: +Control-free problems are optimal control problems without a control. They are used to +**optimise constant parameters of a dynamical system**, for instance to -- identifying unknown parameters from observed data (parameter estimation), -- finding optimal parameters for a given performance criterion. +- identify unknown parameters from observed data (parameter estimation), +- find the parameters that are best for a given performance criterion. -This page is the modelling-side guide: how to declare such a problem and the traps around it. -For the full worked story — both examples below solved **direct and indirect** — see +This page shows how to declare and solve such problems. For the full story of the two +examples below, solved by both the direct and the indirect method, see [Parameter estimation without a control](@ref examples-control-free). ## 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)). The [functional API](@ref modelling-functional-api) page shows the parameter-estimation problem below built both ways, side by side. +There is no dedicated syntax: simply never declare a control. Declare a `variable` (the +parameters), a time, a state, the dynamics and a cost, and omit the control line with +[`@def`](@ref modelling-abstract-syntax), or never call `control!` with the +[functional API](@ref modelling-functional-api). The functional API page builds the +parameter-estimation problem below both ways, side by side. -!!! warning "`control!(pre, 0)` is an error, not a spelling for \"no control\"" +!!! warning "`control!(pre, 0)` is an error" - A control-free problem is reached purely by omission. `control!(pre, 0)` throws - `IncorrectArgument` — a dimension must be positive. Internally, a `PreModel` that never - called `control!` keeps its default `EmptyControlModel`, and `is_control_free`/`has_control` - read that from the *type* of the built model's control field, not from a dimension check. + With the functional API, a control-free problem is obtained by omission only. + `control!(pre, 0)` throws an `IncorrectArgument`: a dimension must be positive. ```@example main using OptimalControl @@ -31,15 +34,15 @@ using Plots ## Example: parameter estimation A system with exponential growth, $\dot{x}(t) = \lambda\, x(t)$, $x(0) = 2$, where $\lambda$ -is an unknown growth rate. We have noisy observed data and estimate $\lambda$ by minimising -the squared error: +is an unknown growth rate. We have noisy observed data $x_{\text{obs}}$ and estimate +$\lambda$ by minimising the squared error: ```math -\min_{\lambda} \int_0^{2} (x(t) - x_{\text{obs}}(t))^2 \, \mathrm{d}t +\min_{\lambda} \int_0^{2} (x(t) - x_{\text{obs}}(t))^2 \, \mathrm{d}t. ``` -The underlying model has $\lambda = 0.5$; the data adds a perturbation. Note there is **no -control line** — only a `variable`: +The data come from the model with $\lambda = 0.5$, plus a perturbation. There is **no +control line**, only a `variable`: ```@example main # observed data (analytical solution with λ = 0.5, plus a perturbation) @@ -60,7 +63,14 @@ end nothing # hide ``` -It solves like any other problem: +The model knows it has no control: + +```@example main +is_control_free(ocp) +``` + +It solves like any other problem. The estimated parameter is the optimal value of the +variable: ```@example main sol = solve(ocp; grid_size=20, display=false) @@ -68,6 +78,13 @@ println("estimated λ = ", variable(sol), " (true value: ", λ_true, ")") nothing # hide ``` +```@example main +@assert isapprox(variable(sol), λ_true; atol=5e-2) # hide +nothing # hide +``` + +The fitted state follows the data, up to the perturbation: + ```@example main plt = plot(sol, :state; size=(800, 400), label="Direct") tg = time_grid(sol) @@ -77,13 +94,11 @@ plot!( ) ``` -The estimate is close to $\lambda \approx 0.5$. The indirect (PMP shooting) solution of the -same problem is worked in full in [Parameter estimation without a control](@ref examples-control-free). - ## Example: harmonic oscillator -The same shape with a Mayer cost — minimise the pulsation $\omega$ of $\ddot q = -\omega^2 q$ -under $q(0) = 1$, $\dot q(0) = 0$, $q(1) = 0$ (analytical solution $\omega = \pi/2$): +The same shape with a Mayer cost: minimise the pulsation $\omega$ of $\ddot q = -\omega^2 q$ +under $q(0) = 1$, $\dot q(0) = 0$ and $q(1) = 0$. The solution is $q(t) = \cos(\omega t)$, and +the smallest pulsation such that $\cos(\omega) = 0$ is $\omega = \pi/2$: ```@example main ocp = @def begin @@ -102,32 +117,18 @@ end nothing # hide ``` -Both the direct and indirect solutions of this one are in -[Parameter estimation without a control](@ref examples-control-free). +Solving it recovers $\pi/2$: -## How the package knows - -`is_control_free(ocp)` and `has_control(ocp)` don't check a dimension — they read the *type* -of the built model's control field. A `PreModel` that never called `control!` keeps its -default `EmptyControlModel`; `build` copies that straight into the immutable `Model`, and -`is_control_free` dispatches on that type. There is nothing to configure: reaching -`ControlFree` is purely a consequence of never calling `control!`. - -## Adding a control back - -To turn either of these examples into a controlled problem, declare a control and give -`Flow` a control law: `Flow(ocp, law)`. Two guards are worth knowing before you try: - -- `Flow(ocp)` — no law — only works on a control-free model. On a model **with** a control - it throws `PreconditionError("Flow from a with-control OCP is not supported")`, suggesting - `Flow(ocp, law)`. -- `constraint=`/`multiplier=` on a control-free `Flow(ocp)` are rejected — `PreconditionError - ("constrained flows are not supported for control-free problems")` — there is no control - law and so no pseudo-Hamiltonian to carry a $\mu \cdot g$ term. Use - `Flow(ocp, law; constraint=…, multiplier=…)` instead. +```@example main +sol = solve(ocp; display=false) +println("ω = ", variable(sol), " (π/2 = ", π / 2, ")") +nothing # hide +``` -See [Control and variable together](@ref examples-control-and-variable) for a worked example -with both. +```@example main +@assert isapprox(variable(sol), π / 2; atol=1e-3) # hide +nothing # hide +``` ## See also @@ -135,4 +136,4 @@ with both. - [Abstract syntax (`@def`)](@ref modelling-abstract-syntax) — the control-free syntax. - [Functional API](@ref modelling-functional-api) — the control-free functional-API form. - [Parameter estimation without a control](@ref examples-control-free) — both problems above, direct **and** indirect, in full. -- [From an OCP](@ref flows-from-ocp) — building flows in general. +- [From an OCP](@ref flows-from-ocp) — building flows, including `Flow(ocp)` for a control-free problem. From ca44c266b173d8a75c8c3d39552d633b0933e231 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 09:53:03 +0200 Subject: [PATCH 13/57] docs(inspect): autonomy includes path constraints, explain :eq1, times vs times - Autonomy: a path constraint depending on t also makes the problem non-autonomous (stated twice, now with an executed example). - The label (1) is stored as :eq1, with a link to Labels. - times(ocp) is a struct, times(sol) a vector: say so. - Evaluation points no longer reuse the model's names q, u, v. - The final_time / mayer error hints are not helpful yet: CTModels#431. - "API trap: time is gone" becomes "times, not time", without history; the alias note moves to the end; no internal type names. Co-Authored-By: Claude Opus 5.5 --- docs/src/modelling/inspect.md | 79 ++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/docs/src/modelling/inspect.md b/docs/src/modelling/inspect.md index a7bde6989..b47644ce5 100644 --- a/docs/src/modelling/inspect.md +++ b/docs/src/modelling/inspect.md @@ -2,15 +2,6 @@ Once a problem is built — via [`@def`](@ref) or the [functional API](@ref modelling-functional-api) — every part of it can be read back: dimensions, names, dynamics, costs, constraints, traits. This page is about reading a model, not solving it: for that, see [Solve overview](@ref solve-overview); for the indirect/PMP route, see [Flows overview](@ref flows-overview). -!!! note "Signatures and `is_*` aliases" - Every accessor on this page is listed with its full signature and return type in the - [Problem API reference](@ref api-problem). Many predicates also have an equivalent `is_*` - alias — `has_control` / `is_control_free`, `has_variable` / `is_variable` / `is_nonvariable`, - `is_autonomous` / `is_nonautonomous`, `has_fixed_final_time` / `is_final_time_fixed` (and the - three other time variants), `has_mayer_cost` / `is_mayer_cost_defined`, - `has_lagrange_cost` / `is_lagrange_cost_defined`, `has_abstract_definition` / - `is_abstractly_defined`. Only the `has_*` / `is_autonomous` forms are shown below. - ```@example main using OptimalControl nothing # hide @@ -57,9 +48,12 @@ nothing # hide ### Times model ```@example main -times(ocp) # the TimesModel struct +times(ocp) # a struct describing the initial and final times ``` +On a solution, `times(sol)` returns something else: the time grid, a vector (see +[Solution object](@ref results-solution)). + Initial and final times separately. The final time needs the variable value when it is free: ```@repl main @@ -67,7 +61,8 @@ initial_time(ocp) final_time(ocp, [1, 2]) # w = 1, tf = 2 ``` -Asking for a free final time without the variable errors: +Asking for a free final time without the variable errors (the hint is not explicit yet, see +[CTModels#431](https://github.com/control-toolbox/CTModels.jl/issues/431)): ```@repl main final_time(ocp) @@ -93,7 +88,7 @@ has_free_final_time(ocp) ### Autonomy ```@example main -is_autonomous(ocp) # false if dynamics or Lagrange cost depend on t +is_autonomous(ocp) # false if the dynamics, the Lagrange cost or a path constraint depend on t ``` See [Time dependence](@ref modelling-inspect-time-dependence) below. @@ -156,7 +151,7 @@ dim_control_constraints_box(ocp) has_control(ocp) # true if the problem has a control input ``` -`is_control_free(ocp) ≡ !has_control(ocp)` — see [No control](@ref modelling-without-control). +`is_control_free(ocp) ≡ !has_control(ocp)`: see [Control-free problems](@ref modelling-without-control). ## Variable @@ -187,9 +182,9 @@ The dynamics are stored as an in-place function `f!(dx, t, x, u, v)` — `dx` is ```@example main f! = dynamics(ocp) -s = 0.5; q = [0.0, 1.0]; u = 2.0; v = [1.0, 2.0] -dq = similar(q) -f!(dq, s, q, u, v) +s1 = 0.5; q1 = [0.0, 1.0]; u1 = 2.0; v1 = [1.0, 2.0] # values at which to evaluate +dq = similar(q1) +f!(dq, s1, q1, u1, v1) dq # the state derivative q̇ ``` @@ -206,7 +201,7 @@ has_mayer_cost(ocp) has_lagrange_cost(ocp) ``` -Get the cost functions — `mayer` has signature `g(x0, xf, v)` and errors when there is no Mayer cost; `lagrange` has signature `f⁰(t, x, u, v)`: +Get the cost functions: `mayer` has signature `g(x0, xf, v)` and errors when there is no Mayer cost (with a hint that does not apply to a built model, see [CTModels#431](https://github.com/control-toolbox/CTModels.jl/issues/431)); `lagrange` has signature `f⁰(t, x, u, v)`: ```@repl main mayer(ocp) @@ -214,36 +209,36 @@ mayer(ocp) ```@example main f⁰ = lagrange(ocp) -f⁰(0.5, [0.0, 1.0], 2.0, [1.0, 2.0]) # the integrand value +f⁰(s1, q1, u1, v1) # the integrand value ``` ## Constraints ### Individual constraints -`constraint(ocp, label)` returns `(type, f, lb, ub)`. The function signature is `f(x0, xf, v)` for `:boundary` and `:variable` constraints, `f(t, x, u, v)` for `:control`, `:state` and `:mixed` ones: +`constraint(ocp, label)` returns `(type, f, lb, ub)`. The function signature is `f(x0, xf, v)` for `:boundary` and `:variable` constraints, `f(t, x, u, v)` for `:control`, `:state` and `:mixed` ones. The constraint `0 ≤ tf ≤ 2` was labelled `(1)`, which is stored as `:eq1` (see [Labels](@ref modelling-abstract-syntax-labels)): ```@example main -x0 = [0, 1]; xf = [2, 3]; v = [1, 4] -s = 0.5; q = [1.0, 2.0]; u = 3.0 +x0 = [0, 1]; xf = [2, 3]; v1 = [1, 4] # boundary values and variable +s1 = 0.5; q1 = [1.0, 2.0]; u1 = 3.0 # time, state and control values (type, f, lb, ub) = constraint(ocp, :eq1) -(type, f(x0, xf, v), lb, ub) +(type, f(x0, xf, v1), lb, ub) ``` ```@example main (type, f, lb, ub) = constraint(ocp, :cons_bound) # a boundary constraint -(type, f(x0, xf, v)) +(type, f(x0, xf, v1)) ``` ```@example main (type, f, lb, ub) = constraint(ocp, :cons_u) # a control constraint -(type, f(s, q, u, v)) +(type, f(s1, q1, u1, v1)) ``` ```@example main (type, f, lb, ub) = constraint(ocp, :cons_mixed) # a mixed path constraint -(type, f(s, q, u, v)) +(type, f(s1, q1, u1, v1)) ``` ### All constraints, and nonlinear ones @@ -266,7 +261,7 @@ dim_boundary_constraints_nl(ocp) ## Problem definition ```@example main -definition(ocp) # the OCP definition, an AbstractDefinition +definition(ocp) # the definition recorded by @def ``` ```@example main @@ -280,7 +275,7 @@ has_abstract_definition(ocp) # false for a functional-API model ## [Time dependence](@id modelling-inspect-time-dependence) -A problem is **autonomous** when neither the dynamics nor the Lagrange cost depends explicitly on the time variable, **non-autonomous** otherwise. +A problem is **autonomous** when neither the dynamics, nor the Lagrange cost, nor any path constraint depends explicitly on the time variable, and **non-autonomous** otherwise. ```@example main ocp = @def begin @@ -306,13 +301,39 @@ end is_autonomous(ocp) ``` +A path constraint that depends on `t` has the same effect: + +```@example main +ocp = @def begin + t ∈ [ 0, 1 ], time + x ∈ R, state + u ∈ R, control + x(t) - t ≤ 1 # explicit dependence on t + ẋ(t) == u(t) + x(1) + 0.5∫( u(t)^2 ) → min +end +is_autonomous(ocp) +``` + ```@example main is_nonautonomous(ocp) # the negation of is_autonomous ``` -## API trap: `time` is gone +## `times`, not `time` + +The time accessor is `times(ocp)`, shown above. `time` is Julia's `Base.time` (the wall-clock +time): calling `time(ocp)` raises an error whose hint is `use times(ocp)`. Coming from v2.0? +See [Migration](@ref migration). + +## Signatures and `is_*` aliases -`time(ocp)` is not the time accessor — `time` is `Base.time` (wall-clock time), extended but not exported. The accessor is `times(ocp)`, shown above. Calling `time(ocp)` throws a migration error pointing here — see [Migrating to v2.1](@ref migration). +Every accessor on this page is listed with its full signature and return type in the +[Problem API reference](@ref api-problem). Many predicates also have an `is_*` counterpart (equivalent, or its negation as for +`has_control` / `is_control_free`): `has_variable` / `is_variable` / `is_nonvariable`, +`is_autonomous` / `is_nonautonomous`, `has_fixed_final_time` / `is_final_time_fixed` (and the +three other time variants), `has_mayer_cost` / `is_mayer_cost_defined`, +`has_lagrange_cost` / `is_lagrange_cost_defined`, `has_abstract_definition` / +`is_abstractly_defined`. Only the `has_*` and `is_autonomous` forms are shown above. ## See also From db7a8e336271ba8b71ee23a538f720fa3887b9c8 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 09:57:27 +0200 Subject: [PATCH 14/57] docs(with-ai): dev URLs, build-checked AI answers, no stale ExaModels claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The prompt and the four "Try with" buttons pointed to stable/, a 404 until the next stable release: now dev/ (no trailing dot in the prompt). - The AI answers are @example blocks: the build constructs and solves them (minimal time tf ≈ 6.5; the rocket reaches the guided tour's Goddard altitude), with hidden asserts. - The "compatible with ExaModels" follow-up is kept as a real exchange, but the page now shows that the vector form solves with :exa too. - No dated model name; the gallery has no rocket, link the guided tour. Co-Authored-By: Claude Opus 5.5 --- docs/src/modelling/with-ai.md | 79 ++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/docs/src/modelling/with-ai.md b/docs/src/modelling/with-ai.md index b71704dc2..7062522ec 100644 --- a/docs/src/modelling/with-ai.md +++ b/docs/src/modelling/with-ai.md @@ -1,39 +1,41 @@ # [With AI](@id modelling-with-ai) -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 [abstract syntax](@ref modelling-abstract-syntax): +An AI assistant can learn the syntax of the OptimalControl.jl DSL from its documentation, and +then translate a problem written in standard mathematics into this DSL. Here is a typical +prompt, pointing to the [abstract syntax](@ref modelling-abstract-syntax) page: ```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/modelling/abstract-syntax. +https://control-toolbox.org/OptimalControl.jl/dev/modelling/abstract-syntax ``` ```@raw html ``` -Now, here is an extract of, *e.g.*, [claude.ai](https://claude.ai) answer (Sonnet 4 model) to the request below: +Here is an extract of the answer of an assistant (here [claude.ai](https://claude.ai)) to the request below: ```text Translate the problem below into this DSL: @@ -44,8 +46,13 @@ Translate the problem below into this DSL: q(0) = 1, q'(0) = -2, q(tf) = 3, q'(tf) = 1, and q'(t) <= 1. ``` -```julia -@def begin +```@setup ai +using OptimalControl +using NLPModelsIpopt +``` + +```@example ai +ocp = @def begin tf ∈ R, variable t ∈ [0, tf], time x = (q, v) ∈ R², state @@ -74,16 +81,26 @@ Translate the problem below into this DSL: # Objective: minimize final time tf → min end +nothing # hide +``` + +The answer is correct: this page builds it when the documentation is built, and it solves: + +```@example ai +sol = solve(ocp; display=false) +println("tf = ", objective(sol)) ``` -The answer is syntactically correct and can be updated, for instance according to the new request below: +The answer can then be refined by further requests, for instance: ```text Use a form compatible with examodels. ``` -```julia -@def begin +The assistant writes the dynamics coordinate by coordinate: + +```@example ai +ocp_exa = @def begin tf ∈ R, variable t ∈ [0, tf], time x = (q, v) ∈ R², state @@ -113,6 +130,23 @@ Use a form compatible with examodels. # Objective: minimize final time tf → min end +nothing # hide +``` + +This form is valid, but it was not needed: the `:exa` modeler (ExaModels) also accepts the +vector form of the first answer. Both solve with it: + +```@example ai +sol_vector = solve(ocp, :exa; display=false) +sol_coord = solve(ocp_exa, :exa; display=false) +println("tf: vector form = ", objective(sol_vector), ", coordinatewise = ", objective(sol_coord)) +``` + +```@example ai +@assert isapprox(objective(sol), 6.5; rtol=1e-3) # hide +@assert isapprox(objective(sol_vector), objective(sol); rtol=1e-6) # hide +@assert isapprox(objective(sol_coord), objective(sol); rtol=1e-6) # hide +nothing # hide ``` 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: @@ -137,9 +171,9 @@ translate the problem below into OptimalControl.jl DSL: ``` -This prompt generates the syntactically correct code: +This prompt generates the following code, which is also built and solved here: -```julia +```@example ai # Parameters h₀ = 1 v₀ = 0 @@ -200,9 +234,24 @@ rocket = @def begin # Objective: maximize final altitude h(tf) -h(tf) → min end +nothing # hide +``` + +```@example ai +sol = solve(rocket; display=false) +println("h(tf) = ", -objective(sol), ", tf = ", variable(sol)) +``` + +```@example ai +@assert isapprox(-objective(sol), 1.012837; atol=1e-5) # hide +nothing # hide ``` +This is the Goddard problem, also solved in the [guided tour](@ref getting-started-guided-tour), +with the same final altitude. + ## See also - [Abstract syntax (`@def`)](@ref modelling-abstract-syntax) — what the AI is being taught. -- [Example gallery](@ref examples-gallery) — more worked problems, including the rocket ascent above. +- [Example gallery](@ref examples-gallery) — more worked problems. +- [Guided tour](@ref getting-started-guided-tour) — the Goddard rocket, solved in depth. From f4e7dbe9f36082a6844349444e1b37ecd18c19e5 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 10:01:52 +0200 Subject: [PATCH 15/57] chore: remove the binder/ environment The guided tour no longer offers a Binder badge: its notebook and script are downloads served with the docs. Nothing else used binder/. CHANGELOG and BREAKING keep their historical mentions. Co-Authored-By: Claude Opus 5.5 --- binder/Project.toml | 10 ---------- binder/apt.txt | 4 ---- binder/postBuild | 5 ----- docs/src-literate/guided-tour.jl | 2 +- 4 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 binder/Project.toml delete mode 100644 binder/apt.txt delete mode 100755 binder/postBuild diff --git a/binder/Project.toml b/binder/Project.toml deleted file mode 100644 index eba666962..000000000 --- a/binder/Project.toml +++ /dev/null @@ -1,10 +0,0 @@ -[deps] -BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" -CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" -CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e" -MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6" -MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598" -NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" -NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" -OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" -Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" diff --git a/binder/apt.txt b/binder/apt.txt deleted file mode 100644 index dd4ad8666..000000000 --- a/binder/apt.txt +++ /dev/null @@ -1,4 +0,0 @@ -build-essential -libgmp-dev -libblas-dev -liblapack-dev diff --git a/binder/postBuild b/binder/postBuild deleted file mode 100755 index 8cd8361ef..000000000 --- a/binder/postBuild +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -set -e - -# Install OptimalControl.jl from the repo root into the Binder environment -julia --project=binder -e 'using Pkg; Pkg.develop(path="."); Pkg.precompile()' diff --git a/docs/src-literate/guided-tour.jl b/docs/src-literate/guided-tour.jl index 75b134f70..c2c8616b0 100644 --- a/docs/src-literate/guided-tour.jl +++ b/docs/src-literate/guided-tour.jl @@ -12,7 +12,7 @@ #src `#md ...` → markdown output only `#nb ...` → notebook output only #src #src LINKS: doc-internal links are split — `#md` emits `@ref`/`@extref` (cross-refs for the -#src Documenter build), `#nb` emits plain https URLs (for the Binder notebook). External +#src Documenter build), `#nb` emits plain https URLs (for the downloadable notebook). External #src homepages (jso.dev, juliaplots, control-toolbox.org root) stay as single plain URLs. #src ============================================================================ # From 5a13293d8dfb94e93c221e6c8f38b092e2e4c3dd Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 10:08:32 +0200 Subject: [PATCH 16/57] docs(solve-overview): real display walk-through, a real failure, H1 "Solve overview" - "Reading the display" described :user/:default/:computed tags that the box never shows. It now walks through the actual Configuration box (as v2.0.4 did): options per strategy, [cpu-dependent] defaults, defaults hidden, describe for the full list. - display=false silences the solver log too: say so. - "Two ways to steer it": the rule for users (don't mix tokens and typed components) instead of the type-based mode detection. - "When it fails" shows a failed solve (max_iter=0: :max_iter, violation 1.1) next to a converged one, with hidden asserts. - H1 "Solve overview" (C8). Co-Authored-By: Claude Opus 5.5 --- docs/src/solve/overview.md | 104 +++++++++++++++++++++++++------------ 1 file changed, 71 insertions(+), 33 deletions(-) diff --git a/docs/src/solve/overview.md b/docs/src/solve/overview.md index 9c0566c72..01f898a0e 100644 --- a/docs/src/solve/overview.md +++ b/docs/src/solve/overview.md @@ -1,8 +1,8 @@ -# [Overview](@id solve-overview) +# [Solve overview](@id solve-overview) `solve` is the entry point for the direct methods: transcribe the problem, hand it to an NLP -solver, get a [`Solution`](@ref results-solution) back. This page shows the quickest way to call it, how to read -what it printed, and the two ways to steer it away from its defaults. +solver, get a [`Solution`](@ref results-solution) back. This page shows the quickest way to +call it, how to read what it prints, and the two ways to steer it away from its defaults. ## Quick start @@ -28,22 +28,41 @@ sol = solve(ocp) nothing # hide ``` -A solver package must be loaded before calling `solve` — here `using NLPModelsIpopt` provides +A solver package must be loaded before calling `solve`: here `using NLPModelsIpopt` provides the default `:ipopt`. Without it, `solve` raises an `ExtensionError` naming the missing -package and the exact `using` statement that fixes it. +package and the `using` statement that fixes it (see [Installation](@ref getting-started-installation)). ## Reading the display -By default `solve` prints a configuration table before running: which discretizer, modeler, -and solver were selected, and every option that ends up applied to each — tagged by where the -value came from: +Before running, `solve` prints the method it is about to use, then a **Configuration** box +with one line per strategy: the discretizer, the modeler and the solver. Each line lists the +options that differ from the strategy's defaults. Above, no option was passed, so the box only +names the strategies. Pass some options, and they appear on the line of the strategy that +receives them: -- `:user` — you passed it explicitly, -- `:default` — the strategy's own default, -- `:computed` — derived from the problem (e.g. a grid size picked from the time span). +```@example main +sol = solve(ocp; grid_size=50, scheme=:trapeze, max_iter=100, print_level=0) +nothing # hide +``` + +The box now shows: + +- **Discretizer**: `collocation`, with `grid_size = 50` and `scheme = trapeze`; +- **Modeler**: `adnlp`, with no option of its own; +- **Solver**: `ipopt`, with `max_iter = 100` and `print_level = 0`. + +Each option was sent to the strategy that declares it: this is the automatic routing described +in [Options](@ref solve-options). Some defaults depend on the `:cpu`/`:gpu` parameter; they are +shown too, tagged `[cpu-dependent]` (or `[gpu-dependent]`). For instance, MadNLP's linear solver: + +```@example main +using MadNLP +sol = solve(ocp, :madnlp; print_level=MadNLP.ERROR) +nothing # hide +``` -This is the fastest way to answer "what did `solve` actually do with the call I just wrote?" -without reading source. +Default values are not shown. To list every option of a strategy with its default, use +`describe`, for instance `describe(:ipopt)` (see [Options](@ref solve-options)). ## Turning the display off @@ -52,7 +71,8 @@ sol = solve(ocp; display=false) nothing # hide ``` -Useful once you trust a configuration and are solving in a loop, a test, or a script. +It silences both the configuration box and the solver's own log. Useful once you trust a +configuration and are solving in a loop, a test, or a script. ## The defaults @@ -62,28 +82,23 @@ Calling `solve(ocp)` with no strategy tokens is equivalent to: solve(ocp, :collocation, :adnlp, :ipopt, :cpu) ``` -This particular quadruplet isn't special-cased — it's simply the first entry of [`methods`](@ref)`()`, -and completion always takes the first match, top to bottom (see -[Choosing a method](@ref solve-choosing-a-method) for the full list and how partial -descriptions are completed). +This quadruplet is the first entry of [`methods`](@ref)`()`, and completion always takes the +first match, top to bottom (see [Choosing a method](@ref solve-choosing-a-method) for the full +list and how partial descriptions are completed). ## Two ways to steer it `solve` can be pointed at a different strategy in two styles: -- **descriptive** — symbolic tokens, e.g. `solve(ocp, :madnlp)` (see +- **descriptive**: symbolic tokens, e.g. `solve(ocp, :madnlp)` (see [Choosing a method](@ref solve-choosing-a-method)), -- **explicit** — typed component instances, e.g. `solve(ocp; solver=OptimalControl.MadNLP())` +- **explicit**: typed components, e.g. `solve(ocp; solver=OptimalControl.MadNLP())` (see [Explicit mode](@ref solve-explicit-mode)). -**The one thing worth knowing before either of those pages**: which mode you're in is decided -by the *type* of a keyword's *value*, never by the keyword's *name*. Any keyword argument whose -value `isa` `AbstractDiscretizer`, `AbstractNLPModeler`, or `AbstractNLPSolver` switches `solve` -into explicit mode, no matter what that keyword is called. Mixing a typed component with a -non-empty symbolic description is rejected outright: +Use one style or the other in a call, not both: a typed component together with symbolic +tokens is rejected. ```@repl main -using MadNLP try # hide solve(ocp, :collocation; solver=OptimalControl.MadNLP()) catch e # hide @@ -93,19 +108,42 @@ end # hide ## When it fails -A solve that doesn't converge still returns a [`Solution`](@ref results-solution) — inspect it rather than -assuming success: +A solve that does not converge still returns a [`Solution`](@ref results-solution). Check it +rather than assuming success. Here we stop Ipopt before its first iteration: + +```@example main +sol = solve(ocp; max_iter=0, display=false, print_level=0) +println("successful: ", successful(sol)) # did the solver report success? +println("status: ", status(sol)) # a Symbol, e.g. :first_order, :max_iter +println("violation: ", constraints_violation(sol)) +``` + +```@example main +@assert !successful(sol) # hide +@assert status(sol) == :max_iter # hide +nothing # hide +``` + +The solution holds the initial guess, which violates the boundary conditions. A converged +solve reports `successful(sol) == true`, the status `:first_order` and a negligible violation: + +```@example main +sol = solve(ocp; display=false, print_level=0) +println("successful: ", successful(sol)) +println("status: ", status(sol)) +println("violation: ", constraints_violation(sol)) +``` ```@example main -println(successful(sol)) # true/false — did the solver report success? -println(status(sol)) # a Symbol, e.g. :first_order, :max_iter -println(message(sol)) # the solver's own message -println(constraints_violation(sol)) +@assert successful(sol) # hide +@assert isapprox(objective(sol), 6; rtol=1e-3) # hide +nothing # hide ``` ## See also - [Choosing a method](@ref solve-choosing-a-method) — the full list of strategies and how partial descriptions are completed. +- [Options](@ref solve-options) — routing, inspection with `describe`, disambiguation. - [Explicit mode](@ref solve-explicit-mode) — build and pass typed components directly. -- [Set an initial guess](@ref solve-initial-guess) — every way to hand `solve` a starting point. +- [Initial guess](@ref solve-initial-guess) — every way to hand `solve` a starting point. From 5b706f2ac56cb29e3adeb9af5829f0077bf37e24 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 10:13:16 +0200 Subject: [PATCH 17/57] docs(initial-guess): short form, cross-spec cases, named tuple, inspect a guess MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The short form `u := 2` is shown (the example did not use it) and the table lists every form, including grids. - v2.0.4 content restored: the labels/time-variable note, and the cross-spec rules with the constant → temporal, constant → constant and alias cases. - New: the named-tuple form (state/control/variable), and "Inspect a guess" with state/control/variable on build_initial_guess output (replaces a typeof printout). - variable on a solution is a scalar for a 1-D variable. - The init/initial_guess inconsistency becomes a one-liner (OptimalControl#964); the "not yet public API" maintainer note moves to OptimalControl#965. Co-Authored-By: Claude Opus 5.5 --- docs/src/solve/initial-guess.md | 200 +++++++++++++++++++------------- 1 file changed, 120 insertions(+), 80 deletions(-) diff --git a/docs/src/solve/initial-guess.md b/docs/src/solve/initial-guess.md index e6041b627..5d5c9e1cb 100644 --- a/docs/src/solve/initial-guess.md +++ b/docs/src/solve/initial-guess.md @@ -44,9 +44,15 @@ end nothing # hide ``` -`@init` uses the **labels declared in `@def`**: for `ocp1` that's `x`, `x₁`, `x₂`, `u` (default -subscripted names, since `x ∈ R²` doesn't name its components); for `ocp2` it's `x`, `q`, `v`, -`u`, `tf`. It also uses the **time variable name** from `@def`: `t` for `ocp1`, `s` for `ocp2`. +!!! note "Component labels and time variable in `@init`" + + - `@init` uses the **labels** declared in `@def`. For `ocp1`, you can use `x`, `x₁`, `x₂` + and `u`; for `ocp2`, `x`, `q`, `v`, `u` and `tf`. + - When components are not named in `@def` (as in `ocp1` with `x ∈ R²`), they receive + **default labels** with subscripted indices, `x₁`, `x₂`, usable in `@init` like custom + labels. + - `@init` uses the **time variable name** of `@def`: `t` for `ocp1` (`x(t) := …`), `s` for + `ocp2` (`q(s) := …`). ## The default guess @@ -83,31 +89,21 @@ ig = @init ocp begin end ``` -| 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` | +| Component | Form | Example | +| --- | --- | --- | +| State / control, function of time | `label(t) := expression` | `u(t) := -0.2t` | +| State / control, constant | `label(t) := value` or `label := value` | `u := 2` | +| State / control, on a grid | `label(T) := data`, `T` a time vector | `u(T) := U` | +| Variable | `label := value` | `tf := 2.0` | +| Alias (local name) | `name = expression` | `a = 0.5` | -1-D components take a scalar (`u(t) := 2`); multi-D components take a vector -(`x(t) := [1, 2]`). The right-hand side of `:=` can be a constant, a function of the time -variable, or a grid `label(T) := data` for a time vector `T`. +1-D components take a scalar (`u(t) := 2`), following the +[1-D is a scalar](@ref modelling-abstract-syntax-control) rule; multi-D components take a +vector (`x(t) := [1, 2]`). The indexed syntax `x[1](t) := ...` is **not supported** — `@init` works at the level of declared labels, not array positions; use `x₁(t) := ...` or a component's own name instead. -Whatever you pass as `init=`/`initial_guess=` — `@init` output, `nothing`, a `Solution`, a -constant — `solve` normalizes it the same way internally, via `build_initial_guess(ocp, ...)`. -Calling it yourself is occasionally useful to inspect what got built: - -```@example main -ig = @init ocp1 begin - u(t) := -0.2 -end -built = build_initial_guess(ocp1, ig) -typeof(built) -``` - ### Constant ```@example main @@ -120,14 +116,14 @@ sol = solve(ocp1; init=ig, display=false) println(iterations(sol), " iterations") ``` -Constant functions also accept the shorter form without the time argument -(`u := 2` instead of `u(t) := 2`): +Constants also accept the shorter form without the time argument (`u := 2` instead of +`u(t) := 2`): ```@example main ig = @init ocp2 begin - q(s) := -0.2 - v(s) := 0.0 - u(s) := 0.1 + q := -0.2 + v := 0.0 + u := 0.1 tf := 2.0 end @@ -178,9 +174,16 @@ sol = solve(ocp2; init=ig, display=false) println(iterations(sol), " iterations") ``` -### Cross-spec references +### [Cross-spec references](@id solve-initial-guess-cross-spec) + +Specifications inside one `@init` block can **reference each other**, from top to bottom: -A spec can reference a label defined earlier in the same block, and references chain: +- 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; +- grid specs are not substituted (see the note below). + +**Temporal → temporal, and chains.** `v` references `q`, and `u` references `v`, hence `q`: ```@example main ig = @init ocp2 begin @@ -194,15 +197,64 @@ sol = solve(ocp2; init=ig, display=false) println(iterations(sol), " iterations") ``` -A grid-based spec (`label(T) := data`, see below) lives in a different evaluation context and -is not substituted into a spec written with the plain time variable, or vice versa — keep one -style per chain of references. +**Constant → temporal.** A function of time can use a constant defined earlier: -## Constants, vectors, functions +```@example main +ig = @init ocp2 begin + q := -1.0 + v(s) := q + sin(s) # uses the constant value of q + u(s) := 0.0 + tf := 2.0 +end -State and control 1-D reads through `@init` follow the "1-D is a scalar" rule, same as -everywhere else — no special case to remember here beyond what's already true on -[functional-API callbacks](@ref modelling-functional-api-shapes) and solutions. +sol = solve(ocp2; init=ig, display=false) +println(iterations(sol), " iterations") +``` + +**Constant → constant.** A constant can use another one, including between the components of +a variable. Here the variable has two components, `(tf, a)`, and the guess is read back with +`variable`: + +```@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 + +variable(ig) +``` + +**With aliases.** Aliases (`=`) and references (`:=`) combine freely: + +```@example main +ig = @init ocp2 begin + A = 2.0 # alias + q(s) := A * sin(s) # uses the 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(iterations(sol), " iterations") +``` + +!!! note "No substitution across grid specs" + + A grid spec (`label(T) := data`, see below) lives in a different evaluation context: it is + not substituted into a spec written with the time variable, or vice versa. Keep one style + per chain of references. ## Vector initial guess (interpolated) @@ -299,62 +351,50 @@ sol = solve(ocp1; init=ig, display=false) println(iterations(sol), " iterations") ``` -`state`, `costate`, and `control` on a solution return functions of time; `variable` returns a -vector. - -## Costate and multipliers +`state`, `costate` and `control` on a solution return functions of time; `variable` returns the +value of the variable (a scalar for a 1-D variable, a vector otherwise). -There is currently no way to seed the costate or the multipliers — only state, control, and -variable accept an initial guess. +## Without `@init`: a named tuple -## `init` or `initial_guess` +`init` also accepts a named tuple with the fields `state`, `control` and `variable`, each a +constant or a function of time (any subset of them): -`init` is an alias for `initial_guess`; use whichever reads better. **In explicit mode**, -supplying both at once is rejected cleanly: - -```@repl main -try # hide -solve( - ocp1; - discretizer=OptimalControl.Collocation(), - init=1, initial_guess=2, - display=false, -) -catch e # hide -showerror(IOContext(stdout, :color => false), e) # hide -end # hide +```@example main +sol = solve(ocp2; init=(state=s -> [-1 + s, 0.0], control=0.1, variable=2.0), display=false) +println(iterations(sol), " iterations") ``` -!!! warning "Same conflict, worse message in descriptive mode" +## Inspect a guess - In descriptive mode (the common case — no `discretizer=`/`modeler=`/`solver=`), supplying - both aliases does **not** raise this same clear error. `initial_guess` is consumed first, - and the leftover `init` falls through to strategy-option routing, where it's rejected as an - *unrecognized solver option* rather than as a conflicting alias — a much more confusing - message for the same mistake: +Whatever you pass as `init`, `solve` turns it into an initial guess with +[`build_initial_guess`](@ref)`(ocp, init)`. You can call it yourself, and read the result back +with `state`, `control` and `variable`, as on a solution. Components that are not specified +take the default value `0.1`: - ```@repl main - try # hide - solve(ocp1; init=1, initial_guess=2, display=false) - catch e # hide - showerror(IOContext(stdout, :color => false), e) # hide - end # hide - ``` +```@example main +ig = build_initial_guess(ocp2, @init(ocp2, begin + q(s) := sin(s) + tf := 2.0 +end)) +state(ig)(0.5), control(ig)(0.5), variable(ig) +``` - This is a real inconsistency between the two code paths, not intentional behavior — don't - rely on either message, just don't pass both. +The same holds for the output of `@init` itself, which is an initial guess already. -## Not (yet) part of the public API +## Costate and multipliers + +There is currently no way to seed the costate or the multipliers — only state, control, and +variable accept an initial guess. + +## `init` or `initial_guess` -`CTModels.jl` has more initial-guess machinery than OptimalControl exposes: -`initial_guess`, `pre_initial_guess`, `validate_initial_guess`, `initial_state`, -`initial_control`, `initial_variable`, and `PreInitialGuess` all exist there but are not -re-exported here — only [`build_initial_guess`](@ref) is. Whether some of these should surface -under `using OptimalControl` is an open question, not a decision this page makes. +`init` is an alias for `initial_guess`; use whichever reads better, but not both in the same +call. In descriptive mode, passing both is reported as an unknown option `:init` rather than +as a conflict ([OptimalControl#964](https://github.com/control-toolbox/OptimalControl.jl/issues/964)). ## See also -- [Overview](@ref solve-overview) — the rest of what `solve` accepts. +- [Solve overview](@ref solve-overview) — the rest of what `solve` accepts. - [Solution object](@ref results-solution) — `state`, `costate`, `control`, `variable` on a returned solution. - [Abstract syntax (`@def`)](@ref modelling-abstract-syntax) — where the labels `@init` uses come from. From 1a3db34ef157ad3bea8f095804fb375fe2bd7a6e Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 10:19:33 +0200 Subject: [PATCH 18/57] docs(probes): check the claims the docs build cannot execute docs/probes/ holds one script per page or section for claims that an executed block cannot show: errors when a package is not loaded, another environment (OrdinaryDiffEqVerner without Tsit5), user-session rules for @def, the absence of output with display=false, external URLs, and the #481 regression that justified removing "Known issues". run.jl runs each in a fresh Julia process with the docs environment. A probe for a known upstream bug checks the current behaviour, so it fails once the fix lands (CTBase#565, CTSolvers#230). Co-Authored-By: Claude Opus 5.5 --- docs/probes/README.md | 33 +++++++++ docs/probes/abstract_syntax_constants_ad.jl | 36 ++++++++++ docs/probes/abstract_syntax_names.jl | 71 ++++++++++++++++++++ docs/probes/common.jl | 63 +++++++++++++++++ docs/probes/installation_gpu_errors.jl | 21 ++++++ docs/probes/installation_integrators.jl | 25 +++++++ docs/probes/installation_missing_packages.jl | 31 +++++++++ docs/probes/links.jl | 34 ++++++++++ docs/probes/run.jl | 27 ++++++++ docs/probes/solve_display.jl | 28 ++++++++ 10 files changed, 369 insertions(+) create mode 100644 docs/probes/README.md create mode 100644 docs/probes/abstract_syntax_constants_ad.jl create mode 100644 docs/probes/abstract_syntax_names.jl create mode 100644 docs/probes/common.jl create mode 100644 docs/probes/installation_gpu_errors.jl create mode 100644 docs/probes/installation_integrators.jl create mode 100644 docs/probes/installation_missing_packages.jl create mode 100644 docs/probes/links.jl create mode 100644 docs/probes/run.jl create mode 100644 docs/probes/solve_display.jl diff --git a/docs/probes/README.md b/docs/probes/README.md new file mode 100644 index 000000000..ca5e5e399 --- /dev/null +++ b/docs/probes/README.md @@ -0,0 +1,33 @@ +# Documentation probes + +The documentation executes its code when it is built, and checks known values with hidden +`@assert`s. That is the first place to check a claim: an executed block, visible or hidden, +or a `@setup` block. + +Some claims cannot be executed by the docs build. The probes in this directory check them: + +- errors raised when a package is **not** loaded (the build process loads them all); +- behaviour that needs another environment (for instance, an integrator package the docs + environment does not have); +- rules about a user session (what must be defined before a `@def` block); +- the absence of output (`display=false`); +- external URLs written in the sources. + +## Running them + +```bash +julia --project=docs docs/probes/run.jl # all probes +julia --project=docs docs/probes/run.jl links # only the probes whose name contains "links" +``` + +Each probe runs in a fresh Julia process. Some download packages into a temporary +environment the first time, and `links.jl` needs the network. They are not run in CI. + +## Writing a probe + +- One file per page or section, named after it: `_.jl`. +- The header names the page, the claim it checks, and, for a known upstream bug, the issue. +- Start with `include("common.jl")`, then check each claim with `check(cond, claim)`, which + prints the claim, or throws when it does not hold. +- A probe for a known bug checks the **current** behaviour. When the bug is fixed upstream, + the probe fails: update the page (drop its one-line note) and the probe. diff --git a/docs/probes/abstract_syntax_constants_ad.jl b/docs/probes/abstract_syntax_constants_ad.jl new file mode 100644 index 000000000..355640204 --- /dev/null +++ b/docs/probes/abstract_syntax_constants_ad.jl @@ -0,0 +1,36 @@ +# Page: modelling/abstract-syntax.md (its former "Known issues" section) +# Claim: a constant inside a vector expression no longer breaks the ADNLP modeler +# (OptimalControl.jl#481, fixed by ADNLPModels 0.8.14). The section was removed on that basis; +# if this probe fails, the bug is back: restore a one-line note with the issue link. + +include("common.jl") +using OptimalControl +using NLPModelsIpopt + +for (name, ocp) in [ + "x(0) - [-1, v] == [0, 0]" => @def(begin + v ∈ R, variable + t ∈ [0, 1], time + x ∈ R², state + u ∈ R, control + -1 ≤ v ≤ 1 + x(0) - [-1, v] == [0, 0] + x(1) == [0, 0] + ẋ(t) == [x₂(t), u(t)] + ∫(0.5u(t)^2) → min + end), + "x(0) - [-one(v), v] == [0, 0]" => @def(begin + v ∈ R, variable + t ∈ [0, 1], time + x ∈ R², state + u ∈ R, control + -1 ≤ v ≤ 1 + x(0) - [-one(v), v] == [0, 0] + x(1) == [0, 0] + ẋ(t) == [x₂(t), u(t)] + ∫(0.5u(t)^2) → min + end), +] + sol = solve(ocp; display=false) + check(successful(sol), "a constant component in `$name` solves with :adnlp") +end diff --git a/docs/probes/abstract_syntax_names.jl b/docs/probes/abstract_syntax_names.jl new file mode 100644 index 000000000..de6ce08b9 --- /dev/null +++ b/docs/probes/abstract_syntax_names.jl @@ -0,0 +1,71 @@ +# Page: modelling/abstract-syntax.md, section "Structure of a definition" +# Claims: constants used in declarations or constraint bounds must be defined before the +# block; functions called in the dynamics may be defined after it; the variable must come +# before the time when the time bounds use it, while the other declarations may come in any +# order. Each statement below is evaluated at top level, in order, as in a user session. + +include("common.jl") +using OptimalControl +using NLPModelsIpopt + +msg = error_text(() -> @eval @def begin + t ∈ [0, T_not_yet_defined], time + x ∈ R, state + u ∈ R, control + ẋ(t) == u(t) + ∫(u(t)^2) → min +end) +check(occursin("UndefVarError", msg), "a constant in the time bounds must be defined before the block") + +msg = error_text(() -> @eval @def begin + t ∈ [0, 1], time + x ∈ R, state + u ∈ R, control + x(0) == x0_not_yet_defined + ẋ(t) == u(t) + ∫(u(t)^2) → min +end) +check(occursin("UndefVarError", msg), "a constant in a boundary condition must be defined before the block") + +ocp = @def begin + t ∈ [0, 1], time + x ∈ R, state + u ∈ R, control + x(0) == 1 + ẋ(t) == G_defined_after(x(t)) + u(t) + ∫(u(t)^2 + x(t)^2) → min +end +G_defined_after(x) = -x +sol = solve(ocp; display=false) +check(successful(sol), "a function called in the dynamics may be defined after the block") + +msg = error_text(() -> @eval @def begin + t ∈ [0, tf], time + tf ∈ R, variable + x ∈ R, state + u ∈ R, control + ẋ(t) == u(t) + tf → min +end) +check(occursin("UndefVarError", msg), "the variable must come before the time when the time bounds use it") + +ocp = @def begin + x ∈ R, state + t ∈ [0, 1], time + u ∈ R, control + ẋ(t) == u(t) + ∫(u(t)^2) → min +end +check(state_dimension(ocp) == 1, "other declarations may come in any order (state before time)") + +ocp = @def begin + t in [0, 1], time + x in R^2, state + u in R, control + x(0) == [-1, 0] + x(1) == [0, 0] + derivative(x)(t) == [x[2](t), u(t)] + u(t) <= 10 + 0.5integral(u(t)^2) => min +end +check(state_dimension(ocp) == 2, "the ASCII alternatives (in, R^2, derivative, <=, integral, =>) are accepted") diff --git a/docs/probes/common.jl b/docs/probes/common.jl new file mode 100644 index 000000000..84f95f67b --- /dev/null +++ b/docs/probes/common.jl @@ -0,0 +1,63 @@ +# Shared helpers for the documentation probes. Each probe `include`s this file first. +# +# A probe checks a claim of the documentation that the docs build cannot execute (see +# README.md). It throws on failure; `run.jl` runs every probe in a fresh Julia process. + +# Use the OptimalControl of this repository, as docs/make.jl does. +const REPO_ROOT = normpath(joinpath(@__DIR__, "..", "..")) +pushfirst!(LOAD_PATH, REPO_ROOT) + +""" + error_text(f) -> String + +Call `f()` and return the text of the exception it throws, without colours. Throw if `f()` +does not throw. +""" +function error_text(f) + try + f() + catch e + return sprint(showerror, e) + end + error("expected an exception, got none") +end + +""" + check(cond, claim) + +Throw with the documented `claim` when `cond` is false; print it when true. +""" +function check(cond::Bool, claim::AbstractString) + cond || error("claim does not hold: " * claim) + println(" ✓ ", claim) + return nothing +end + +""" + temp_env(pkgs...) + +Activate a temporary environment holding `pkgs` and stack the docs environment behind it, +for claims about packages the docs environment does not have. The first call downloads +and precompiles them. +""" +function temp_env(pkgs::AbstractString...) + Pkg = Base.require(Base.PkgId(Base.UUID("44cfe95a-1eb2-52ea-b672-e2afdf69b78f"), "Pkg")) + # Pkg is loaded just above, in a newer world: call it through invokelatest. + Base.invokelatest(Pkg.activate; temp=true, io=devnull) + Base.invokelatest(Pkg.add, collect(pkgs); io=devnull) + push!(LOAD_PATH, joinpath(REPO_ROOT, "docs")) + return nothing +end + +# A small problem used by several probes: the energy-minimal double integrator. +const ENERGY_DEF = quote + @def begin + t ∈ [0, 1], time + x = (q, v) ∈ R², state + u ∈ R, control + x(0) == [-1, 0] + x(1) == [0, 0] + ẋ(t) == [v(t), u(t)] + 0.5∫(u(t)^2) → min + end +end diff --git a/docs/probes/installation_gpu_errors.jl b/docs/probes/installation_gpu_errors.jl new file mode 100644 index 000000000..a7388052d --- /dev/null +++ b/docs/probes/installation_gpu_errors.jl @@ -0,0 +1,21 @@ +# Page: getting-started/installation.md, section "Optional: GPU" +# Claim: a missing GPU package is reported by an ExtensionError. +# Known upstream bug (CTBase.jl#565): with no GPU package at all, the error asks for +# `MadNLP` and mentions `MadNLP{CPU}`. This probe checks the current behaviour, so it fails +# once the bug is fixed: then update the installation page and this probe. + +include("common.jl") +using OptimalControl +using NLPModelsIpopt + +ocp = eval(ENERGY_DEF) + +msg = error_text(() -> solve(ocp, :gpu; display=false)) +check(occursin("ExtensionError", msg), "solve(ocp, :gpu) without GPU packages raises an ExtensionError") +check(occursin("MadNLP{CPU}", msg), + "known bug CTBase.jl#565: the error still mentions MadNLP{CPU} (update the docs when this fails)") + +using MadNLPGPU +msg = error_text(() -> solve(ocp, :gpu; display=false)) +check(occursin("ExtensionError", msg) && occursin("CUDA", msg), + "with MadNLPGPU only, the error asks for CUDA") diff --git a/docs/probes/installation_integrators.jl b/docs/probes/installation_integrators.jl new file mode 100644 index 000000000..ff73f933c --- /dev/null +++ b/docs/probes/installation_integrators.jl @@ -0,0 +1,25 @@ +# Page: getting-started/installation.md, tip "Other integrators" +# Claims: the package is OrdinaryDiffEqVerner (there is no OrdinaryDiffEqVern9), and another +# integrator works with `alg=` as long as OrdinaryDiffEqTsit5 is loaded too. +# Known upstream bug (CTSolvers.jl#230): without OrdinaryDiffEqTsit5, `alg=Vern9()` fails. +# This probe checks the current behaviour, so it fails once the bug is fixed: then relax the +# tip ("keep OrdinaryDiffEqTsit5 loaded in every case") and this probe. + +include("common.jl") +temp_env("OrdinaryDiffEqVerner", "OrdinaryDiffEqTsit5") +using OptimalControl +using OrdinaryDiffEqVerner + +ocp = eval(ENERGY_DEF) +law(x, p) = p[2] + +msg = error_text(() -> Flow(ocp, law; alg=Vern9())) +check(occursin("IncorrectArgument", msg), + "known bug CTSolvers.jl#230: without Tsit5, Flow(ocp, law; alg=Vern9()) fails (update the docs when this fails)") + +using OrdinaryDiffEqTsit5 + +φ = Flow(ocp, law; alg=Vern9()) +xf, pf = φ(0, [-1, 0], [12, 6], 1) +check(isapprox(xf, [0, 0]; atol=1e-10) && isapprox(pf, [12, -6]; atol=1e-10), + "with Tsit5 also loaded, alg=Vern9() works and reaches x(1) = (0, 0)") diff --git a/docs/probes/installation_missing_packages.jl b/docs/probes/installation_missing_packages.jl new file mode 100644 index 000000000..04dbfde0f --- /dev/null +++ b/docs/probes/installation_missing_packages.jl @@ -0,0 +1,31 @@ +# Page: getting-started/installation.md +# Claim: without the package of an optional feature, the call raises an ExtensionError that +# names the missing package. The docs build cannot show this: its process loads them all. + +include("common.jl") +using OptimalControl + +ocp = eval(ENERGY_DEF) + +msg = error_text(() -> solve(ocp; display=false)) +check(occursin("ExtensionError", msg) && occursin("using NLPModelsIpopt", msg), + "solve without a solver package asks for `using NLPModelsIpopt`") + +msg = error_text(() -> solve(ocp, :madncl; display=false)) +check(occursin("MadNCL", msg) && occursin("MadNLP", msg), + ":madncl needs both MadNCL and MadNLP, and the error names both") + +using NLPModelsIpopt +sol = solve(ocp; display=false) + +msg = error_text(() -> plot(sol)) +check(occursin("ExtensionError", msg) && occursin("using Plots", msg), + "plot(sol) without Plots asks for `using Plots`") + +msg = error_text(() -> export_ocp_solution(sol; filename="probe")) +check(occursin("ExtensionError", msg) && occursin("JLD2", msg), + "export_ocp_solution without JLD2 raises an ExtensionError naming JLD2") + +msg = error_text(() -> Flow(ocp, (x, p) -> p[2])) +check(occursin("ExtensionError", msg) && occursin("OrdinaryDiffEqTsit5", msg), + "Flow without an ODE package asks for `using OrdinaryDiffEqTsit5`") diff --git a/docs/probes/links.jl b/docs/probes/links.jl new file mode 100644 index 000000000..268a14d23 --- /dev/null +++ b/docs/probes/links.jl @@ -0,0 +1,34 @@ +# Pages: all hand-written pages (docs/src, docs/src-literate). +# Claim: every control-toolbox.org URL written in the sources answers. External links are not +# checked by Documenter here, and a moved page (such as `stable/` before a release) becomes a +# 404 silently. + +include("common.jl") +using Downloads + +sources = String[] +for dir in (joinpath(REPO_ROOT, "docs", "src"), joinpath(REPO_ROOT, "docs", "src-literate")) + for (root, _, files) in walkdir(dir) + occursin(joinpath("docs", "src", "api"), root) && continue + occursin(joinpath("docs", "src", "public"), root) && continue + for f in files + (endswith(f, ".md") || endswith(f, ".jl")) && push!(sources, joinpath(root, f)) + end + end +end + +urls = Set{String}() +for file in sources, m in eachmatch(r"https://control-toolbox\.org/[^\s\)\]\"'<>`]*", read(file, String)) + push!(urls, rstrip(m.match, ['.', ',', ';', ':'])) +end + +status(url) = try + Downloads.request(url; method="HEAD", timeout=30).status +catch + 0 +end + +for url in sort!(collect(urls)) + code = status(url) + check(code == 200, "$url answers (HTTP $code)") +end diff --git a/docs/probes/run.jl b/docs/probes/run.jl new file mode 100644 index 000000000..641984ad0 --- /dev/null +++ b/docs/probes/run.jl @@ -0,0 +1,27 @@ +# Run every documentation probe, each in a fresh Julia process with the docs environment: +# +# julia --project=docs docs/probes/run.jl # all probes +# julia --project=docs docs/probes/run.jl links # the probes whose name contains "links" +# +# Exit code 0 when all probes pass. See README.md. + +const HERE = @__DIR__ +const DOCS_ENV = normpath(joinpath(HERE, "..")) + +probes = sort!(filter(readdir(HERE)) do f + endswith(f, ".jl") && f ∉ ("run.jl", "common.jl") && + (isempty(ARGS) || any(a -> occursin(a, f), ARGS)) +end) + +failed = String[] +for probe in probes + println("── ", probe) + cmd = `$(Base.julia_cmd()) --startup-file=no --project=$DOCS_ENV $(joinpath(HERE, probe))` + ok = success(pipeline(cmd; stdout=stdout, stderr=stderr)) + ok || push!(failed, probe) + println(ok ? " PASS" : " FAIL") +end + +println() +println(length(probes) - length(failed), "/", length(probes), " probes pass") +isempty(failed) || (println("failed: ", join(failed, ", ")); exit(1)) diff --git a/docs/probes/solve_display.jl b/docs/probes/solve_display.jl new file mode 100644 index 000000000..286241712 --- /dev/null +++ b/docs/probes/solve_display.jl @@ -0,0 +1,28 @@ +# Page: solve/overview.md, section "Turning the display off" +# Claim: display=false silences both the configuration box and the solver's own log. +# The docs build cannot show an absence of output reliably, so it is checked here, on the +# process's standard output (Ipopt writes from C). + +include("common.jl") +using OptimalControl +using NLPModelsIpopt + +ocp = eval(ENERGY_DEF) +solve(ocp; display=false) # first call: compilation, outside the capture + +function captured_output(f) + path, io = mktemp() + redirect_stdout(io) do + f() + Base.Libc.flush_cstdio() + end + close(io) + return read(path, String) +end + +out = captured_output(() -> solve(ocp; display=false)) +check(isempty(strip(out)), "display=false prints nothing, Ipopt log included") + +out = captured_output(() -> solve(ocp)) +check(occursin("Configuration", out) && occursin("Ipopt", out), + "the default display prints the configuration box and Ipopt's log") From 4759e4653b01d5044ed9523ea520f08f278e5518 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 10:26:31 +0200 Subject: [PATCH 19/57] docs(choosing-a-method): vector form for :exa, describe every strategy, real errors - :exa accepts the vector form of the dynamics: the coordinatewise requirement is gone from the page; the shared problem is written in vector form and solved with :exa. - "Ambiguity" becomes "Incompatible tokens": the error is "no description contains all symbols", with the closest matches. - v2.0.4 content restored: the "Understanding default values" (NotProvided) note, describe for every modeler and solver (in details blocks), one-line scheme descriptions, and the official documentation links (ExaModels and MadNLP now under madsuite-org). - :di is the AD backend, :sciml the ODE integrator; the internal registry section is dropped; no authoring remarks. - Known upstream problems as one-liners: CTParser#355 (scheme aliases under :exa, the error shown is now the alias one) and CTDirect#638 (time_grid ignored or crashing under :exa). - Hidden asserts on methods(); probe solve_completion.jl checks the completion examples and describe(:sciml) / describe(:di). Co-Authored-By: Claude Opus 5.5 --- docs/probes/solve_completion.jl | 29 +++++ docs/src/solve/choosing-a-method.md | 177 +++++++++++++++++++--------- 2 files changed, 152 insertions(+), 54 deletions(-) create mode 100644 docs/probes/solve_completion.jl diff --git a/docs/probes/solve_completion.jl b/docs/probes/solve_completion.jl new file mode 100644 index 000000000..b687bc8a8 --- /dev/null +++ b/docs/probes/solve_completion.jl @@ -0,0 +1,29 @@ +# Page: solve/choosing-a-method.md, section "Partial descriptions" +# Claims: a partial description is completed with the first method of `methods()` that +# contains all its tokens; the examples given in a non-executed block. Also: `describe` +# accepts the indirect-method strategies `:sciml` and `:di`. + +include("common.jl") +using OptimalControl + +complete(tokens...) = OptimalControl._complete_description(tokens) # internal: probe only + +check(complete() == methods()[1] == (:collocation, :adnlp, :ipopt, :cpu), + "solve(ocp) uses methods()[1] = (:collocation, :adnlp, :ipopt, :cpu)") +check(complete(:madnlp) == (:collocation, :adnlp, :madnlp, :cpu), ":madnlp → (:collocation, :adnlp, :madnlp, :cpu)") +check(complete(:exa) == (:collocation, :exa, :ipopt, :cpu), ":exa → (:collocation, :exa, :ipopt, :cpu)") +check(complete(:gpu) == (:collocation, :exa, :madnlp, :gpu), ":gpu → (:collocation, :exa, :madnlp, :gpu)") +for tokens in [(:collocation,), (:adnlp,), (:ipopt,), (:cpu,), (:collocation, :adnlp), + (:collocation, :adnlp, :ipopt, :cpu)] + check(complete(tokens...) == methods()[1], "$(tokens) completes to the default") +end + +# describe prints its description +function printed(f) + path, io = mktemp() + redirect_stdout(() -> f(), io) + close(io) + return read(path, String) +end +check(occursin("id: :sciml", printed(() -> describe(:sciml))), "describe(:sciml) describes the ODE integrator") +check(occursin("id: :di", printed(() -> describe(:di))), "describe(:di) describes the AD backend") diff --git a/docs/src/solve/choosing-a-method.md b/docs/src/solve/choosing-a-method.md index 886be79b9..0b7bf57d0 100644 --- a/docs/src/solve/choosing-a-method.md +++ b/docs/src/solve/choosing-a-method.md @@ -6,13 +6,15 @@ any one piece before you commit to it. ## The four families -- **Discretizer** — how the continuous problem is transcribed into a finite-dimensional one. +- **Discretizer**: how the continuous problem is transcribed into a finite-dimensional one. Only `:collocation` exists today. -- **Modeler** — how the resulting NLP is built: `:adnlp` (automatic differentiation via - ADNLPModels) or `:exa` (SIMD-friendly, GPU-capable, via ExaModels — only for problems whose - dynamics are written coordinatewise, see [Abstract syntax (`@def`)](@ref modelling-abstract-syntax)). -- **Solver** — which NLP solver runs: `:ipopt`, `:madnlp`, `:uno`, `:madncl`, `:knitro`. -- **Parameter** — execution backend: `:cpu` or `:gpu`. +- **Modeler**: how the resulting NLP is built: `:adnlp` + ([ADNLPModels](https://jso.dev/ADNLPModels.jl/stable/), automatic differentiation) or + `:exa` ([ExaModels](https://madsuite-org.github.io/ExaModels.jl/stable/), SIMD-friendly + and GPU-capable). `:exa` needs a problem written with [`@def`](@ref modelling-abstract-syntax), + not with the [functional API](@ref modelling-functional-api). +- **Solver**: which NLP solver runs: `:ipopt`, `:madnlp`, `:uno`, `:madncl`, `:knitro`. +- **Parameter**: the execution backend, `:cpu` or `:gpu`. ## What is available @@ -23,13 +25,17 @@ methods() There are 12 methods: every `{adnlp, exa} × {ipopt, madnlp, uno, madncl, knitro}` pair on `:cpu` (10), plus the two GPU-capable combinations `:exa × {:madnlp, :madncl}` on `:gpu` (2). -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. + +```@example main +@assert length(methods()) == 12 # hide +@assert methods()[1] == (:collocation, :adnlp, :ipopt, :cpu) # hide +nothing # hide +``` ## 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: +Every `solve` call on this page uses the same problem, the double integrator. Both modelers +accept it as written, with the dynamics in vector form: ```@example main ocp = @def begin @@ -38,8 +44,7 @@ ocp = @def begin u ∈ R, control x(0) == [-1, 0] x(1) == [0, 0] - ∂(q)(t) == v(t) - ∂(v)(t) == u(t) + ẋ(t) == [v(t), u(t)] ∫(0.5u(t)^2) → min end nothing # hide @@ -47,7 +52,7 @@ nothing # hide ## Partial descriptions -`solve(ocp, :madnlp)` doesn't need the other three tokens — they're completed for you. +`solve(ocp, :madnlp)` does not need the other three tokens: they are completed for you. Completion walks `methods()` from top to bottom and returns the first entry containing every token you gave: @@ -57,9 +62,16 @@ solve(ocp, :exa) # → (:collocation, :exa, :ipopt, :cpu) solve(ocp, :gpu) # → (:collocation, :exa, :madnlp, :gpu) ``` -This first-match-top-to-bottom rule is also why the plain `solve(ocp)` default is -`(:collocation, :adnlp, :ipopt, :cpu)`: it's simply `methods()[1]`. All of these are -equivalent: +The first line of the display shows the completed method. For instance, with `:exa`: + +```@example main +using NLPModelsIpopt +sol = solve(ocp, :exa; print_level=0) +nothing # hide +``` + +This first-match rule is also why the plain `solve(ocp)` default is +`(:collocation, :adnlp, :ipopt, :cpu)`: it is `methods()[1]`. All of these are equivalent: ```julia solve(ocp) # empty description → methods()[1] @@ -71,11 +83,11 @@ solve(ocp, :collocation, :adnlp) solve(ocp, :collocation, :adnlp, :ipopt, :cpu) # the complete description ``` -## Ambiguity +## Incompatible tokens -Two tokens from the *same* family never both fit one method — `:adnlp` and `:exa` can't both -be true of one quadruplet — so this raises `AmbiguousDescription` rather than silently picking -one: +Two tokens of the *same* family never fit one method: `:adnlp` and `:exa` cannot both describe +one quadruplet. No method contains both, so `solve` raises an error rather than silently +picking one. It lists the available methods and the closest matches: ```@repl main try # hide @@ -85,9 +97,6 @@ showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -The exception lists every candidate whose tokens are a superset of what matched, so you can see -what's close. - ## What each solver needs installed | Solver | Load | @@ -97,78 +106,138 @@ what's close. | `:uno` | `using UnoSolver` | | `:madncl` | `using MadNCL` and `using MadNLP` (both) | | `:knitro` | `using NLPModelsKnitro` (commercial licence required) | -| either on `:gpu` | `using MadNLPGPU`, `using CUDA` **and** `using CUDSS` — all three | +| either on `:gpu` | `using MadNLPGPU`, `using CUDA` **and** `using CUDSS`, all three | -Solving without the matching package loaded raises an `ExtensionError` naming exactly which -`using` statement to add — including on the `:gpu` row, which needs all three of `MadNLPGPU`, -`CUDA` and `CUDSS`; see [Solving on GPU](@ref solve-gpu). +Solving without the matching package loaded raises an `ExtensionError` naming the `using` +statement to add; see [Installation](@ref getting-started-installation) and +[GPU](@ref solve-gpu). ## Inspecting a strategy -`describe` works on any strategy id, and covers more than the direct-solve side: it also -describes the indirect-method families (`:di`, `:sciml`) and the two parameters themselves. +`describe` prints a strategy: its family, the parameters it supports, and every option with +its type, its aliases and its default value: ```@example main describe(:collocation) ``` +!!! note "Understanding default values" + + `(default: NotProvided)` means that OptimalControl does not set the option, so the + solver or modeler uses its **own** default. For instance, an Ipopt option shown with + `(default: NotProvided)` takes Ipopt's default value. Only the options with an explicit + default, such as `(default: 1000)` for Ipopt's `max_iter`, are set by OptimalControl. + +The modelers and solvers are described the same way. The solver packages must be loaded +first, as for solving: + +::: details `describe(:adnlp)` and `describe(:exa)` + ```@example main describe(:adnlp) ``` ```@example main -using NLPModelsIpopt +describe(:exa) +``` + +::: + +::: details `describe(:ipopt)` + +```@example main describe(:ipopt) ``` +::: + +::: details `describe(:madnlp)` and `describe(:madncl)` + +```@example main +using MadNLP +describe(:madnlp) +``` + +```@example main +using MadNCL +describe(:madncl) +``` + +::: + +::: details `describe(:uno)` + +```@example main +using UnoSolver +describe(:uno) +``` + +::: + +`describe` also accepts the parameters, which lists the strategies that support them: + ```@example main describe(:cpu) ``` +```@example main +describe(:gpu) +``` + +It covers the strategies of the indirect method too: `describe(:sciml)` for the ODE +integrator of [flows](@ref flows-overview), and `describe(:di)` for the automatic +differentiation backend ([DifferentiationInterface](https://juliadiff.org/DifferentiationInterface.jl/DifferentiationInterface/stable/)). + +### Official documentation + +For the complete list of each package's options, see: + +- **ADNLPModels**: [documentation](https://jso.dev/ADNLPModels.jl/stable/) +- **ExaModels**: [documentation](https://madsuite-org.github.io/ExaModels.jl/stable/) +- **Ipopt**: [options](https://coin-or.github.io/Ipopt/OPTIONS.html) +- **MadNLP**: [options](https://madsuite-org.github.io/MadNLP.jl/stable/options/) +- **Uno**: [documentation](https://unosolver.readthedocs.io) +- **MadNCL**: [repository](https://github.com/MadNLP/MadNCL.jl) +- **Knitro**: [options](https://www.artelys.com/docs/knitro/3_referenceManual/userOptions.html) + ## Discretization schemes `:collocation` accepts a `scheme` option (alias `disc_method`): | Value | Notes | `:adnlp` | `:exa` | | --- | --- | :-: | :-: | -| `:trapeze` | second-order | ✅ | ✅ | -| `:midpoint` | second-order, **default** | ✅ | ✅ | -| `:euler` | first-order, explicit | ✅ | ✅ | -| `:euler_implicit` | first-order, implicit | ✅ | ✅ | +| `:trapeze` | trapezoidal rule, second-order | ✅ | ✅ | +| `:midpoint` | midpoint rule, second-order, **default** | ✅ | ✅ | +| `:euler` | explicit Euler, first-order | ✅ | ✅ | +| `:euler_implicit` | implicit Euler, first-order, more stable for stiff problems | ✅ | ✅ | | `:euler_explicit`, `:euler_forward` | aliases of `:euler` | ✅ | ✗ | | `:euler_backward` | alias of `:euler_implicit` | ✅ | ✗ | -| `:gauss_legendre_2` | fourth-order | ✅ | ✗ | -| `:gauss_legendre_3` | sixth-order | ✅ | ✗ | - -plus `grid_size` (default `250`) or an explicit, possibly non-uniform, `time_grid`. +| `:gauss_legendre_2` | 2-point Gauss–Legendre collocation, fourth-order | ✅ | ✗ | +| `:gauss_legendre_3` | 3-point Gauss–Legendre collocation, sixth-order | ✅ | ✗ | -Every cell above was checked by solving with that scheme. One of the results needs spelling out. +The higher-order Gauss–Legendre schemes are more accurate, at a higher cost per grid step. +The grid is set by `grid_size` (default `250`), or, with `:adnlp`, by an explicit, possibly +non-uniform, `time_grid` (with `:exa`, `time_grid` is not supported yet: +[CTDirect#638](https://github.com/control-toolbox/CTDirect.jl/issues/638)). !!! warning "Under `:exa`, only the four canonical names work" `: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: + Both Gauss–Legendre schemes are out, and so are the aliases: `:euler_forward` is rejected + where `:euler` is accepted, though under `:adnlp` the two name the same scheme + ([CTParser#355](https://github.com/control-toolbox/CTParser.jl/issues/355)): ```@repl main try # hide - solve(ocp, :exa; scheme=:gauss_legendre_2, display=false) + solve(ocp, :exa; scheme=:euler_forward, display=false) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -## Advanced: the strategy registry - -`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 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 -- [Options and routing](@ref solve-options) — how keyword arguments reach the right strategy. -- [Solving on GPU](@ref solve-gpu) — the `:gpu` parameter in full. -- [API reference: Options and strategies](@ref api-options) — every symbol on this page, with full signatures. +- [Options](@ref solve-options): how keyword arguments reach the right strategy. +- [GPU](@ref solve-gpu): the `:gpu` parameter in full. +- [API reference: Options and strategies](@ref api-options): every symbol on this page, with + full signatures. From 2d119df8508fe1964727fd6cc8db4203dc17aef4 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 10:30:18 +0200 Subject: [PATCH 20/57] docs(options): real routing examples, the keyword form of route_to, :computed explained - The routing example uses a quiet modeler option (backend) instead of show_time, which flooded the page with timings. - No placeholder option: say that no two built-in strategies of a method share an option name today; route_to is shown executed, including its keyword form route_to(madnlp=1000). - :computed means "depends on the :cpu/:gpu parameter" (MadNLP's linear_solver), not "computed from the problem"; the display shows :user and :computed options, not defaults. Hidden asserts on option_source. - No internal type names (BypassValue, RoutedOption). Co-Authored-By: Claude Opus 5.5 --- docs/src/solve/options.md | 76 +++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/docs/src/solve/options.md b/docs/src/solve/options.md index 804ab22f7..1f57d9e40 100644 --- a/docs/src/solve/options.md +++ b/docs/src/solve/options.md @@ -1,8 +1,8 @@ # [Options](@id solve-options) -Every keyword argument passed to `solve` ends up on exactly one strategy — the discretizer, +Every keyword argument passed to `solve` ends up on exactly one strategy: the discretizer, the modeler, or the solver. This page covers how that routing works, the two escape hatches -for the cases it doesn't handle automatically, and how to inspect where a value came from. +for the cases it does not handle automatically, and how to inspect where a value came from. ```@example advanced using OptimalControl @@ -31,10 +31,10 @@ the three strategies in play, or it's an error — never a silent no-op. ```@example advanced sol = solve(ocp; - grid_size=100, # → Collocation (discretizer) - show_time=true, # → ADNLP (modeler) - max_iter=500, # → Ipopt (solver) - print_level=0, # → Ipopt (solver) + grid_size=100, # → Collocation (discretizer) + backend=:default, # → ADNLP (modeler) + max_iter=500, # → Ipopt (solver) + print_level=0, # → Ipopt (solver) ) nothing # hide ``` @@ -49,30 +49,13 @@ showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -## Ambiguous options +## Explicit routing with `route_to` -If two strategies from *different* families declare the same option name, using it bare is -ambiguous — `solve` won't guess which one you meant. Disambiguate with `route_to`, which takes -a strategy id and a value: - -```julia -sol = solve(ocp, :exa, :madnlp; - common_option_name=route_to(:exa, 12), - max_iter=500, -) -``` - -`route_to` also accepts alternating id/value pairs, to send the same option name to several -strategies with different values at once: - -```julia -sol = solve(ocp, :exa, :madnlp; - common_option_name=route_to(:exa, 12, :madnlp, true), -) -``` - -`route_to` works even when there's no ambiguity to resolve — it's fine to use it just to be -explicit: +If two strategies of a method declared the same option name, using it bare would be +ambiguous, and `solve` would not guess which one you meant. With the built-in strategies, +this does not happen today: no two strategies of a method share an option name. `route_to` +names the target strategy explicitly. It is required for an ambiguous name, and allowed +everywhere, for instance to be explicit: ```@example advanced using MadNLP @@ -84,6 +67,16 @@ sol = solve(ocp, :madnlp; nothing # hide ``` +`route_to` takes a strategy id and a value, or the same as a keyword argument, +`route_to(madnlp=1000)`. It also accepts several id/value pairs, to send the same option name +to several strategies with different values at once: `route_to(:id1, value1, :id2, value2)`. + +```@example advanced +sol = solve(ocp, :madnlp; max_iter=route_to(madnlp=1000), display=false) # keyword form +@assert successful(sol) # hide +nothing # hide +``` + ## Undeclared solver options The three strategies here declare their own options, but not every option the underlying @@ -104,10 +97,6 @@ the strategy's declared options — `bypass` is what skips that check. `force` i for `bypass` (`force === bypass`); use whichever name reads better: `route_to(:ipopt, force(1))`. -Both `bypass` and `route_to` return values of internal types (`BypassValue`, `RoutedOption`) — -these types are imported but not exported, so only the functions ever appear in your code, not -the type names. - !!! warning "Use `bypass` sparingly" It skips type checking and validation entirely. Reach for it only when you're certain the @@ -115,8 +104,8 @@ the type names. ## Where a value came from -Every option on a built strategy instance knows whether it was set by you, left at its -default, or computed from the problem: +Every option on a built strategy instance knows whether it was set by you (`:user`), left at +its default (`:default`), or computed from the `:cpu`/`:gpu` parameter (`:computed`): ```@example advanced s = OptimalControl.Ipopt(max_iter=200) @@ -131,16 +120,25 @@ println(is_default(opts, :tol)) # true println(is_computed(opts, :max_iter)) # false ``` -`option_source` returns which of the three it was, as a `Symbol`: +`option_source` returns which of the three it was, as a `Symbol`. MadNLP's linear solver is a +computed option: its default depends on the parameter (MUMPS on CPU, cuDSS on GPU): ```@example advanced println(option_source(s, :max_iter)) println(option_source(s, :tol)) +println(option_source(OptimalControl.MadNLP(), :linear_solver)) +``` + +```@example advanced +@assert option_source(s, :max_iter) == :user # hide +@assert option_source(s, :tol) == :default # hide +@assert option_source(OptimalControl.MadNLP(), :linear_solver) == :computed # hide +nothing # hide ``` -This is the same provenance information the `📦 Configuration` table shows when `solve` prints -its display (see [Overview](@ref solve-overview)) — these functions let you query it -programmatically instead of reading it off the printout. +The display of `solve` (see [Solve overview](@ref solve-overview)) shows the `:user` and +`:computed` options of each strategy, the latter tagged `[cpu-dependent]` or +`[gpu-dependent]`; the `:default` ones are not shown. ## Action options vs strategy options From b0321b3db41d5a57f0641fd46fca85e48d6d5157 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 10:32:40 +0200 Subject: [PATCH 21/57] docs(explicit-mode): consistent names, used components, no CTDirect import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - discretizer / modeler / solver / sol everywhere (mod shadowed Base.mod, sol was a solver and result the solution). - "using CTDirect: Collocation would also work" removed: an indirect dependency cannot be loaded from a user environment. - The per-component example now solves with the components it builds (Gauss–Legendre on 50 steps recovers J = 6, hidden assert) instead of building unused ones with show_time=true. - No mode-detection internals; link texts updated. Co-Authored-By: Claude Opus 5.5 --- docs/src/solve/explicit-mode.md | 69 ++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/docs/src/solve/explicit-mode.md b/docs/src/solve/explicit-mode.md index af064cc39..92a765347 100644 --- a/docs/src/solve/explicit-mode.md +++ b/docs/src/solve/explicit-mode.md @@ -1,7 +1,7 @@ # [Explicit mode](@id solve-explicit-mode) -Instead of symbolic tokens, pass `solve` typed strategy instances — full control over each -component's configuration, no completion-order guessing. +Instead of symbolic tokens, pass `solve` typed strategy instances: full control over each +component's configuration, no completion to guess. ## When you want this @@ -30,22 +30,18 @@ ocp = @def begin 0.5∫(u(t)^2) → min end -disc = OptimalControl.Collocation(grid_size=100, scheme=:trapeze) -mod = OptimalControl.ADNLP(backend=:optimized) -sol = OptimalControl.Ipopt(max_iter=1000, print_level=0) +discretizer = OptimalControl.Collocation(grid_size=100, scheme=:trapeze) +modeler = OptimalControl.ADNLP(backend=:optimized) +solver = OptimalControl.Ipopt(max_iter=1000, print_level=0) -result = solve(ocp; discretizer=disc, modeler=mod, solver=sol) +sol = solve(ocp; discretizer=discretizer, modeler=modeler, solver=solver) nothing # hide ``` -`discretizer`, `modeler`, and `solver` are just three of many keyword names — mode detection -never looks at names, only at whether a keyword's *value* is a typed component (see -[Overview](@ref solve-overview)). Any keyword holding a typed instance triggers explicit mode. - -The component types are `import`ed but not `@reexport`ed by `OptimalControl`, which is why -every constructor above is written `OptimalControl.Collocation(...)` rather than bare -`Collocation(...)` — writing `using CTDirect: Collocation` yourself would also work, but the -qualified spelling needs nothing extra loaded beyond `using OptimalControl`. +Each component goes with its keyword: `discretizer`, `modeler`, `solver`. The component types +are not exported, so they are written with the module name: `OptimalControl.Collocation(...)`, +`OptimalControl.Ipopt(...)`. This needs nothing loaded beyond `using OptimalControl` (and the +solver package, as always). ## Partial components @@ -57,7 +53,7 @@ methods()[1] # what a bare solve(ocp) completes to ``` ```@example explicit -result = solve( +sol = solve( ocp; solver=OptimalControl.Ipopt(max_iter=2000, print_level=0), display=true, @@ -70,7 +66,7 @@ modeler compatible with Ipopt) — visible in the printed configuration above. M component with defaults works the same way for any subset: ```@example explicit -result = solve(ocp; +sol = solve(ocp; discretizer=OptimalControl.Collocation(grid_size=200, scheme=:trapeze), solver=OptimalControl.Ipopt(max_iter=100, print_level=0), display=false, @@ -80,18 +76,27 @@ nothing # hide ## Per-component options -Every option a strategy accepts is set when it's constructed — never routed in from `solve` -afterward, unlike descriptive mode: +Every option a strategy accepts is set when it is constructed, never routed in from `solve` +afterwards, unlike descriptive mode. Here, a fourth-order scheme on a coarser grid, and tighter +Ipopt tolerances: ```@example explicit -disc = OptimalControl.Collocation(grid_size=150, scheme=:gauss_legendre_2) -mod = OptimalControl.ADNLP(backend=:optimized, show_time=true) -sol = OptimalControl.Ipopt( - max_iter=1000, tol=1e-8, print_level=5, acceptable_tol=1e-6 -) -nothing # hide +discretizer = OptimalControl.Collocation(grid_size=50, scheme=:gauss_legendre_2) +modeler = OptimalControl.ADNLP(backend=:optimized) +solver = OptimalControl.Ipopt(max_iter=1000, tol=1e-10, acceptable_tol=1e-8, print_level=0) + +sol = solve(ocp; discretizer=discretizer, modeler=modeler, solver=solver) +println("objective = ", objective(sol)) # the exact value is 6 ``` +```@example explicit +@assert isapprox(objective(sol), 6; rtol=1e-6) # hide +nothing # hide +``` + +On 50 steps, the fourth-order scheme recovers the exact cost to about machine precision, where +the default second-order scheme on 250 steps is off by about `1e-4`. + Undeclared options still need `bypass` (or its alias `force`), same reasoning as in descriptive mode — but here it's passed straight into the constructor, not through `route_to`: @@ -118,14 +123,14 @@ showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -The error names the strategy that owns the option and tells you exactly how to fix it: -construct that strategy with the option set, and pass the configured instance in. `route_to` -plays no role here — it only makes sense in descriptive mode, where options don't yet belong to -a concrete instance. +The error names the strategy that owns the option and tells you how to fix it: construct that +strategy with the option set, and pass the configured instance in. `route_to` plays no role +here: it only makes sense in descriptive mode, where options do not yet belong to a concrete +instance. ## Mixing modes is forbidden -Symbolic tokens and typed components can't appear in the same call: +Symbolic tokens and typed components cannot appear in the same call: ```@repl explicit try # hide @@ -165,8 +170,8 @@ collect(keys(opts)) ## See also -- [Overview](@ref solve-overview) — how mode detection decides between the two styles. -- [Options and routing](@ref solve-options) — the descriptive-mode counterpart (`route_to`, - automatic routing) to per-component construction here. +- [Solve overview](@ref solve-overview) — the two styles side by side. +- [Options](@ref solve-options) — the descriptive-mode counterpart (`route_to`, automatic + routing) to per-component construction here. - [Choosing a method](@ref solve-choosing-a-method) — the full strategy catalogue these constructors build from. From b6691ad39052bbe9cf0a666ca598e5aa70b0d33c Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 10:45:45 +0200 Subject: [PATCH 22/57] docs(gpu): vector form works on :exa, CPU vs GPU defaults and timing, madsuite links - "The problem must be coordinatewise" is gone: :exa accepts the vector form of the dynamics and of path constraints (verified, same cost as :adnlp); the example problem uses it. - v2.0.4 content restored: the CPU vs GPU defaults (Exa backend, MadNLP linear solver) and the "beneficial for" list. The timing block now compares :cpu and :gpu at the same grid size on a GPU machine. - CUDSS warning without version history; the ExaModels name clash is still real (#882 closed, clash checked by a probe). - `parameter`, not `strategy`; modeler/solver names. - ExaModels and MadNLP moved to madsuite-org: page links and InterLinks updated, placeholder inventories replaced by the real ones (225 and 157 entries). - Probe solve_gpu.jl: "Missing CUDSS" error and the ExaModels clash. common.jl now mirrors make.jl's load path (docs environment first), which also fixes CUDA failing to load in the probes. Co-Authored-By: Claude Opus 5.5 --- docs/inventories/ExaModels.toml | 763 +++++++++++++++++++++++++++++++- docs/inventories/MadNLP.toml | 565 ++++++++++++++++++++++- docs/make.jl | 8 +- docs/probes/common.jl | 9 +- docs/probes/solve_gpu.jl | 21 + docs/src/solve/gpu.md | 114 +++-- 6 files changed, 1415 insertions(+), 65 deletions(-) create mode 100644 docs/probes/solve_gpu.jl diff --git a/docs/inventories/ExaModels.toml b/docs/inventories/ExaModels.toml index e006ed6c0..c37126d9f 100644 --- a/docs/inventories/ExaModels.toml +++ b/docs/inventories/ExaModels.toml @@ -1,8 +1,759 @@ # DocInventory version 1 -project = "ExaModels" +project = "ExaModels.jl" +version = "0.12.1" -# Placeholder: exanauts.github.io/ExaModels.jl/stable/objects.inv currently 404s -# (checked 2026-08-15), so there is no reachable source to harvest real entries -# from. This empty inventory keeps the build green; @extref ExaModels ... will -# report "not found" (not a build Error) until either the upstream site comes -# back or this file is populated with real entries. +[[jl.function]] +name = "ExaModels._hess_reconstruct_via_hvp!" +uri = "core/#ExaModels._hess_reconstruct_via_hvp%21" +[[jl.function]] +name = "ExaModels._jac_reconstruct_via_jvp!" +uri = "core/#ExaModels._jac_reconstruct_via_jvp%21" + +[[jl.macro]] +name = "ExaModels.@add_con!-Tuple" +uri = "core/#ExaModels.%40add_con%21-Tuple" +[[jl.macro]] +name = "ExaModels.@add_con-Tuple" +uri = "core/#ExaModels.%40add_con-Tuple" +[[jl.macro]] +name = "ExaModels.@add_expr-Tuple" +uri = "core/#ExaModels.%40add_expr-Tuple" +[[jl.macro]] +name = "ExaModels.@add_obj-Tuple" +uri = "core/#ExaModels.%40add_obj-Tuple" +[[jl.macro]] +name = "ExaModels.@add_par-Tuple" +uri = "core/#ExaModels.%40add_par-Tuple" +[[jl.macro]] +name = "ExaModels.@add_var-Tuple" +uri = "core/#ExaModels.%40add_var-Tuple" +[[jl.macro]] +name = "ExaModels.@register_bivariate-NTuple{6, Any}" +uri = "core/#ExaModels.%40register_bivariate-NTuple%7B6%2C%20Any%7D" +[[jl.macro]] +name = "ExaModels.@register_univariate-Tuple{Any, Any, Any}" +uri = "core/#ExaModels.%40register_univariate-Tuple%7BAny%2C%20Any%2C%20Any%7D" + +[[jl.method]] +name = "ExaModels.CompressedNLPModel-Tuple{Any}" +uri = "core/#ExaModels.CompressedNLPModel-Tuple%7BAny%7D" +[[jl.method]] +name = "ExaModels.TimedNLPModel-Tuple{Any}" +uri = "core/#ExaModels.TimedNLPModel-Tuple%7BAny%7D" +[[jl.method]] +name = "ExaModels.TimedNLPModel-Tuple{ExaCore}" +uri = "core/#ExaModels.TimedNLPModel-Tuple%7BExaCore%7D" +[[jl.method]] +name = "ExaModels.TwoStageExaCore-Tuple{Integer}" +uri = "core/#ExaModels.TwoStageExaCore-Tuple%7BInteger%7D" +[[jl.method]] +name = "ExaModels.WrapperNLPModel-Tuple{Any, Any}" +uri = "core/#ExaModels.WrapperNLPModel-Tuple%7BAny%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.WrapperNLPModel-Tuple{Any}" +uri = "core/#ExaModels.WrapperNLPModel-Tuple%7BAny%7D" +[[jl.method]] +name = "ExaModels._anyarg-Tuple{}" +uri = "core/#ExaModels._anyarg-Tuple%7B%7D" +[[jl.method]] +name = "ExaModels._needs_overload-Tuple{Any, Any}" +uri = "core/#ExaModels._needs_overload-Tuple%7BAny%2C%20Any%7D" +[[jl.method]] +name = "ExaModels._with_args-Tuple{Any, Val{0}}" +uri = "core/#ExaModels._with_args-Tuple%7BAny%2C%20Val%7B0%7D%7D" +[[jl.method]] +name = "ExaModels.add_con!-Union{Tuple{C}, Tuple{T}, Tuple{C, Any, Base.Generator}} where {T, C<:(ExaCore{T})}" +uri = "core/#ExaModels.add_con%21-Union%7BTuple%7BC%7D%2C%20Tuple%7BT%7D%2C%20Tuple%7BC%2C%20Any%2C%20Base.Generator%7D%7D%20where%20%7BT%2C%20C%3C%3A%28ExaCore%7BT%7D%29%7D" +[[jl.method]] +name = "ExaModels.add_con!-Union{Tuple{T}, Tuple{ExaCore{T}, Base.Generator}} where T" +uri = "core/#ExaModels.add_con%21-Union%7BTuple%7BT%7D%2C%20Tuple%7BExaCore%7BT%7D%2C%20Base.Generator%7D%7D%20where%20T" +[[jl.method]] +name = "ExaModels.add_con-Union{Tuple{C}, Tuple{S}, Tuple{B}, Tuple{VT}, Tuple{T}, Tuple{C, EachScenario, Vararg{Any}}} where {T, VT<:AbstractVector{T}, B, S<:ExaModels.TwoStageExaModelTag, C<:(TwoStageExaCore{T, VT, B, S})}" +uri = "core/#ExaModels.add_con-Union%7BTuple%7BC%7D%2C%20Tuple%7BS%7D%2C%20Tuple%7BB%7D%2C%20Tuple%7BVT%7D%2C%20Tuple%7BT%7D%2C%20Tuple%7BC%2C%20EachScenario%2C%20Vararg%7BAny%7D%7D%7D%20where%20%7BT%2C%20VT%3C%3AAbstractVector%7BT%7D%2C%20B%2C%20S%3C%3AExaModels.TwoStageExaModelTag%2C%20C%3C%3A%28TwoStageExaCore%7BT%2C%20VT%2C%20B%2C%20S%7D%29%7D" +[[jl.method]] +name = "ExaModels.add_con-Union{Tuple{C}, Tuple{S}, Tuple{B}, Tuple{VT}, Tuple{T}, Tuple{C, Vararg{Any}}} where {T, VT<:AbstractVector{T}, B, S<:ExaModels.TwoStageExaModelTag, C<:(TwoStageExaCore{T, VT, B, S})}" +uri = "core/#ExaModels.add_con-Union%7BTuple%7BC%7D%2C%20Tuple%7BS%7D%2C%20Tuple%7BB%7D%2C%20Tuple%7BVT%7D%2C%20Tuple%7BT%7D%2C%20Tuple%7BC%2C%20Vararg%7BAny%7D%7D%7D%20where%20%7BT%2C%20VT%3C%3AAbstractVector%7BT%7D%2C%20B%2C%20S%3C%3AExaModels.TwoStageExaModelTag%2C%20C%3C%3A%28TwoStageExaCore%7BT%2C%20VT%2C%20B%2C%20S%7D%29%7D" +[[jl.method]] +name = "ExaModels.add_con-Union{Tuple{C}, Tuple{T}, Tuple{C, Vararg{Any}}} where {T, C<:(ExaCore{T})}" +uri = "core/#ExaModels.add_con-Union%7BTuple%7BC%7D%2C%20Tuple%7BT%7D%2C%20Tuple%7BC%2C%20Vararg%7BAny%7D%7D%7D%20where%20%7BT%2C%20C%3C%3A%28ExaCore%7BT%7D%29%7D" +[[jl.method]] +name = "ExaModels.add_eval-Tuple{ExaCore, Tuple, Tuple, Any}" +uri = "core/#ExaModels.add_eval-Tuple%7BExaCore%2C%20Tuple%2C%20Tuple%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.add_expr-Union{Tuple{C}, Tuple{T}, Tuple{C, Base.Generator}} where {T, C<:(ExaCore{T})}" +uri = "core/#ExaModels.add_expr-Union%7BTuple%7BC%7D%2C%20Tuple%7BT%7D%2C%20Tuple%7BC%2C%20Base.Generator%7D%7D%20where%20%7BT%2C%20C%3C%3A%28ExaCore%7BT%7D%29%7D" +[[jl.method]] +name = "ExaModels.add_obj-Union{Tuple{C}, Tuple{T}, Tuple{C, Any}} where {T, C<:(ExaCore{T})}" +uri = "core/#ExaModels.add_obj-Union%7BTuple%7BC%7D%2C%20Tuple%7BT%7D%2C%20Tuple%7BC%2C%20Any%7D%7D%20where%20%7BT%2C%20C%3C%3A%28ExaCore%7BT%7D%29%7D" +[[jl.method]] +name = "ExaModels.add_obj-Union{Tuple{N}, Tuple{C}, Tuple{T}, Tuple{C, N}, Tuple{C, N, Any}} where {T, C<:(ExaCore{T}), N<:ExaModels.AbstractNode}" +uri = "core/#ExaModels.add_obj-Union%7BTuple%7BN%7D%2C%20Tuple%7BC%7D%2C%20Tuple%7BT%7D%2C%20Tuple%7BC%2C%20N%7D%2C%20Tuple%7BC%2C%20N%2C%20Any%7D%7D%20where%20%7BT%2C%20C%3C%3A%28ExaCore%7BT%7D%29%2C%20N%3C%3AExaModels.AbstractNode%7D" +[[jl.method]] +name = "ExaModels.add_par-Union{Tuple{B}, Tuple{VT}, Tuple{T}, Tuple{TwoStageExaCore{T, VT, B}, AbstractArray}} where {T, VT<:AbstractVector{T}, B}" +uri = "core/#ExaModels.add_par-Union%7BTuple%7BB%7D%2C%20Tuple%7BVT%7D%2C%20Tuple%7BT%7D%2C%20Tuple%7BTwoStageExaCore%7BT%2C%20VT%2C%20B%7D%2C%20AbstractArray%7D%7D%20where%20%7BT%2C%20VT%3C%3AAbstractVector%7BT%7D%2C%20B%7D" +[[jl.method]] +name = "ExaModels.add_par-Union{Tuple{B}, Tuple{VT}, Tuple{T}, Tuple{TwoStageExaCore{T, VT, B}, EachScenario, AbstractVector}} where {T, VT<:AbstractVector{T}, B}" +uri = "core/#ExaModels.add_par-Union%7BTuple%7BB%7D%2C%20Tuple%7BVT%7D%2C%20Tuple%7BT%7D%2C%20Tuple%7BTwoStageExaCore%7BT%2C%20VT%2C%20B%7D%2C%20EachScenario%2C%20AbstractVector%7D%7D%20where%20%7BT%2C%20VT%3C%3AAbstractVector%7BT%7D%2C%20B%7D" +[[jl.method]] +name = "ExaModels.add_par-Union{Tuple{C}, Tuple{T}, Tuple{C, AbstractArray}} where {T, C<:(ExaCore{T})}" +uri = "core/#ExaModels.add_par-Union%7BTuple%7BC%7D%2C%20Tuple%7BT%7D%2C%20Tuple%7BC%2C%20AbstractArray%7D%7D%20where%20%7BT%2C%20C%3C%3A%28ExaCore%7BT%7D%29%7D" +[[jl.method]] +name = "ExaModels.add_var-Union{Tuple{B}, Tuple{VT}, Tuple{T}, Tuple{TwoStageExaCore{T, VT, B}, EachScenario, Vararg{Any}}} where {T, VT<:AbstractVector{T}, B}" +uri = "core/#ExaModels.add_var-Union%7BTuple%7BB%7D%2C%20Tuple%7BVT%7D%2C%20Tuple%7BT%7D%2C%20Tuple%7BTwoStageExaCore%7BT%2C%20VT%2C%20B%7D%2C%20EachScenario%2C%20Vararg%7BAny%7D%7D%7D%20where%20%7BT%2C%20VT%3C%3AAbstractVector%7BT%7D%2C%20B%7D" +[[jl.method]] +name = "ExaModels.add_var-Union{Tuple{B}, Tuple{VT}, Tuple{T}, Tuple{TwoStageExaCore{T, VT, B}, Vararg{Any}}} where {T, VT<:AbstractVector{T}, B}" +uri = "core/#ExaModels.add_var-Union%7BTuple%7BB%7D%2C%20Tuple%7BVT%7D%2C%20Tuple%7BT%7D%2C%20Tuple%7BTwoStageExaCore%7BT%2C%20VT%2C%20B%7D%2C%20Vararg%7BAny%7D%7D%7D%20where%20%7BT%2C%20VT%3C%3AAbstractVector%7BT%7D%2C%20B%7D" +[[jl.method]] +name = "ExaModels.add_var-Union{Tuple{C}, Tuple{T}, Tuple{C, Base.Generator}} where {T, C<:(ExaCore{T})}" +uri = "core/#ExaModels.add_var-Union%7BTuple%7BC%7D%2C%20Tuple%7BT%7D%2C%20Tuple%7BC%2C%20Base.Generator%7D%7D%20where%20%7BT%2C%20C%3C%3A%28ExaCore%7BT%7D%29%7D" +[[jl.method]] +name = "ExaModels.add_var-Union{Tuple{C}, Tuple{T}, Tuple{C, Vararg{Any}}} where {T, C<:(ExaCore{T})}" +uri = "core/#ExaModels.add_var-Union%7BTuple%7BC%7D%2C%20Tuple%7BT%7D%2C%20Tuple%7BC%2C%20Vararg%7BAny%7D%7D%7D%20where%20%7BT%2C%20C%3C%3A%28ExaCore%7BT%7D%29%7D" +[[jl.method]] +name = "ExaModels.constraint-Tuple{ExaCore, VectorNonlinearOracle}" +uri = "core/#ExaModels.constraint-Tuple%7BExaCore%2C%20VectorNonlinearOracle%7D" +[[jl.method]] +name = "ExaModels.drpass-Union{Tuple{D}, Tuple{D, Any, Any}} where D<:Union{Real, AdjointNull}" +uri = "core/#ExaModels.drpass-Union%7BTuple%7BD%7D%2C%20Tuple%7BD%2C%20Any%2C%20Any%7D%7D%20where%20D%3C%3AUnion%7BReal%2C%20AdjointNull%7D" +[[jl.method]] +name = "ExaModels.embed_oracle-Tuple{ExaCore, ExaModels.AbstractVariable, Int64}" +uri = "core/#ExaModels.embed_oracle-Tuple%7BExaCore%2C%20ExaModels.AbstractVariable%2C%20Int64%7D" +[[jl.method]] +name = "ExaModels.exa_prod-Tuple{Any, Tuple{}}" +uri = "core/#ExaModels.exa_prod-Tuple%7BAny%2C%20Tuple%7B%7D%7D" +[[jl.method]] +name = "ExaModels.exa_sum-Tuple{Any, Tuple{}}" +uri = "core/#ExaModels.exa_sum-Tuple%7BAny%2C%20Tuple%7B%7D%7D" +[[jl.method]] +name = "ExaModels.fulltype-Tuple{Any}" +uri = "core/#ExaModels.fulltype-Tuple%7BAny%7D" +[[jl.method]] +name = "ExaModels.fulltype_display!-Tuple{Bool}" +uri = "core/#ExaModels.fulltype_display%21-Tuple%7BBool%7D" +[[jl.method]] +name = "ExaModels.get_con_scen-Tuple{TwoStageExaModel}" +uri = "core/#ExaModels.get_con_scen-Tuple%7BTwoStageExaModel%7D" +[[jl.method]] +name = "ExaModels.get_cons-Tuple{Union{ExaCore, ExaModel}, Symbol}" +uri = "core/#ExaModels.get_cons-Tuple%7BUnion%7BExaCore%2C%20ExaModel%7D%2C%20Symbol%7D" +[[jl.method]] +name = "ExaModels.get_cons-Tuple{Union{ExaCore, ExaModel}}" +uri = "core/#ExaModels.get_cons-Tuple%7BUnion%7BExaCore%2C%20ExaModel%7D%7D" +[[jl.method]] +name = "ExaModels.get_lcon-Tuple{ExaModel, ExaModels.Constraint}" +uri = "core/#ExaModels.get_lcon-Tuple%7BExaModel%2C%20ExaModels.Constraint%7D" +[[jl.method]] +name = "ExaModels.get_lvar-Tuple{ExaModel, ExaModels.Variable}" +uri = "core/#ExaModels.get_lvar-Tuple%7BExaModel%2C%20ExaModels.Variable%7D" +[[jl.method]] +name = "ExaModels.get_nscen-Tuple{TwoStageExaModel}" +uri = "core/#ExaModels.get_nscen-Tuple%7BTwoStageExaModel%7D" +[[jl.method]] +name = "ExaModels.get_pars-Tuple{Union{ExaCore, ExaModel}, Symbol}" +uri = "core/#ExaModels.get_pars-Tuple%7BUnion%7BExaCore%2C%20ExaModel%7D%2C%20Symbol%7D" +[[jl.method]] +name = "ExaModels.get_pars-Tuple{Union{ExaCore, ExaModel}}" +uri = "core/#ExaModels.get_pars-Tuple%7BUnion%7BExaCore%2C%20ExaModel%7D%7D" +[[jl.method]] +name = "ExaModels.get_start-Tuple{ExaModel, ExaModels.Variable}" +uri = "core/#ExaModels.get_start-Tuple%7BExaModel%2C%20ExaModels.Variable%7D" +[[jl.method]] +name = "ExaModels.get_start-Tuple{TwoStageExaModel, ExaModels.Constraint{F, I, O, S, <:SecondStageConstraintTag} where {F, I, O, S}, Integer}" +uri = "core/#ExaModels.get_start-Tuple%7BTwoStageExaModel%2C%20ExaModels.Constraint%7BF%2C%20I%2C%20O%2C%20S%2C%20%3C%3ASecondStageConstraintTag%7D%20where%20%7BF%2C%20I%2C%20O%2C%20S%7D%2C%20Integer%7D" +[[jl.method]] +name = "ExaModels.get_start-Tuple{TwoStageExaModel, SecondStageVariable, Integer}" +uri = "core/#ExaModels.get_start-Tuple%7BTwoStageExaModel%2C%20SecondStageVariable%2C%20Integer%7D" +[[jl.method]] +name = "ExaModels.get_ucon-Tuple{ExaModel, ExaModels.Constraint}" +uri = "core/#ExaModels.get_ucon-Tuple%7BExaModel%2C%20ExaModels.Constraint%7D" +[[jl.method]] +name = "ExaModels.get_uvar-Tuple{ExaModel, ExaModels.Variable}" +uri = "core/#ExaModels.get_uvar-Tuple%7BExaModel%2C%20ExaModels.Variable%7D" +[[jl.method]] +name = "ExaModels.get_value-Tuple{ExaCore, ExaModels.Parameter}" +uri = "core/#ExaModels.get_value-Tuple%7BExaCore%2C%20ExaModels.Parameter%7D" +[[jl.method]] +name = "ExaModels.get_value-Tuple{ExaModel, ExaModels.Parameter}" +uri = "core/#ExaModels.get_value-Tuple%7BExaModel%2C%20ExaModels.Parameter%7D" +[[jl.method]] +name = "ExaModels.get_value-Tuple{TwoStageExaModel, ExaModels.Parameter{S, O, <:SecondStageTag} where {S, O}, Integer}" +uri = "core/#ExaModels.get_value-Tuple%7BTwoStageExaModel%2C%20ExaModels.Parameter%7BS%2C%20O%2C%20%3C%3ASecondStageTag%7D%20where%20%7BS%2C%20O%7D%2C%20Integer%7D" +[[jl.method]] +name = "ExaModels.get_var_scen-Tuple{TwoStageExaModel}" +uri = "core/#ExaModels.get_var_scen-Tuple%7BTwoStageExaModel%7D" +[[jl.method]] +name = "ExaModels.get_vars-Tuple{Union{ExaCore, ExaModel}}" +uri = "core/#ExaModels.get_vars-Tuple%7BUnion%7BExaCore%2C%20ExaModel%7D%7D" +[[jl.method]] +name = "ExaModels.gradient!-NTuple{5, Any}" +uri = "core/#ExaModels.gradient%21-NTuple%7B5%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.grpass-Union{Tuple{D}, Tuple{D, Vararg{Any, 5}}} where D<:Union{Real, AdjointNull, DataIndexed{…}}" +uri = "core/#ExaModels.grpass-Union%7BTuple%7BD%7D%2C%20Tuple%7BD%2C%20Vararg%7BAny%2C%205%7D%7D%7D%20where%20D%3C%3AUnion%7BReal%2C%20AdjointNull%2C%20DataIndexed%7B%E2%80%A6%7D%7D" +[[jl.method]] +name = "ExaModels.has_matfree_hess-Tuple{VectorNonlinearOracle}" +uri = "core/#ExaModels.has_matfree_hess-Tuple%7BVectorNonlinearOracle%7D" +[[jl.method]] +name = "ExaModels.has_matfree_jac-Tuple{VectorNonlinearOracle}" +uri = "core/#ExaModels.has_matfree_jac-Tuple%7BVectorNonlinearOracle%7D" +[[jl.method]] +name = "ExaModels.hdrpass-Union{Tuple{T2}, Tuple{T1}, Tuple{T1, T2, Vararg{Any, 6}}} where {T1<:SecondAdjointNode1{…}, T2<:SecondAdjointNode1{…}}" +uri = "core/#ExaModels.hdrpass-Union%7BTuple%7BT2%7D%2C%20Tuple%7BT1%7D%2C%20Tuple%7BT1%2C%20T2%2C%20Vararg%7BAny%2C%206%7D%7D%7D%20where%20%7BT1%3C%3ASecondAdjointNode1%7B%E2%80%A6%7D%2C%20T2%3C%3ASecondAdjointNode1%7B%E2%80%A6%7D%7D" +[[jl.method]] +name = "ExaModels.instantiate-Union{Tuple{N}, Tuple{Any, Vararg{Any, N}}} where N" +uri = "core/#ExaModels.instantiate-Union%7BTuple%7BN%7D%2C%20Tuple%7BAny%2C%20Vararg%7BAny%2C%20N%7D%7D%7D%20where%20N" +[[jl.method]] +name = "ExaModels.jrpass-Union{Tuple{D}, Tuple{D, Vararg{Any, 7}}} where D<:Union{Real, AdjointNull}" +uri = "core/#ExaModels.jrpass-Union%7BTuple%7BD%7D%2C%20Tuple%7BD%2C%20Vararg%7BAny%2C%207%7D%7D%7D%20where%20D%3C%3AUnion%7BReal%2C%20AdjointNull%7D" +[[jl.method]] +name = "ExaModels.multipliers-Tuple{SolverCore.AbstractExecutionStats, ExaModels.Constraint}" +uri = "core/#ExaModels.multipliers-Tuple%7BSolverCore.AbstractExecutionStats%2C%20ExaModels.Constraint%7D" +[[jl.method]] +name = "ExaModels.multipliers_L-Tuple{SolverCore.AbstractExecutionStats, Any}" +uri = "core/#ExaModels.multipliers_L-Tuple%7BSolverCore.AbstractExecutionStats%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.multipliers_U-Tuple{SolverCore.AbstractExecutionStats, Any}" +uri = "core/#ExaModels.multipliers_U-Tuple%7BSolverCore.AbstractExecutionStats%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.objective-Tuple{ExaCore, ScalarNonlinearOracle}" +uri = "core/#ExaModels.objective-Tuple%7BExaCore%2C%20ScalarNonlinearOracle%7D" +[[jl.method]] +name = "ExaModels.set_lcon!-Tuple{ExaModel, ExaModels.Constraint, Any}" +uri = "core/#ExaModels.set_lcon%21-Tuple%7BExaModel%2C%20ExaModels.Constraint%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.set_lvar!-Tuple{ExaModel, ExaModels.Variable, Any}" +uri = "core/#ExaModels.set_lvar%21-Tuple%7BExaModel%2C%20ExaModels.Variable%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.set_start!-Tuple{ExaModel, ExaModels.Variable, Any}" +uri = "core/#ExaModels.set_start%21-Tuple%7BExaModel%2C%20ExaModels.Variable%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.set_start!-Tuple{TwoStageExaModel, ExaModels.Constraint{F, I, O, S, <:SecondStageConstraintTag} where {F, I, O, S}, Integer, Any}" +uri = "core/#ExaModels.set_start%21-Tuple%7BTwoStageExaModel%2C%20ExaModels.Constraint%7BF%2C%20I%2C%20O%2C%20S%2C%20%3C%3ASecondStageConstraintTag%7D%20where%20%7BF%2C%20I%2C%20O%2C%20S%7D%2C%20Integer%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.set_start!-Tuple{TwoStageExaModel, SecondStageVariable, Integer, Any}" +uri = "core/#ExaModels.set_start%21-Tuple%7BTwoStageExaModel%2C%20SecondStageVariable%2C%20Integer%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.set_ucon!-Tuple{ExaModel, ExaModels.Constraint, Any}" +uri = "core/#ExaModels.set_ucon%21-Tuple%7BExaModel%2C%20ExaModels.Constraint%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.set_uvar!-Tuple{ExaModel, ExaModels.Variable, Any}" +uri = "core/#ExaModels.set_uvar%21-Tuple%7BExaModel%2C%20ExaModels.Variable%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.set_value!-Tuple{ExaCore, ExaModels.Parameter, AbstractArray}" +uri = "core/#ExaModels.set_value%21-Tuple%7BExaCore%2C%20ExaModels.Parameter%2C%20AbstractArray%7D" +[[jl.method]] +name = "ExaModels.set_value!-Tuple{ExaModel, ExaModels.Parameter, Any}" +uri = "core/#ExaModels.set_value%21-Tuple%7BExaModel%2C%20ExaModels.Parameter%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.set_value!-Tuple{TwoStageExaModel, ExaModels.Parameter{S, O, <:SecondStageTag} where {S, O}, Integer, Any}" +uri = "core/#ExaModels.set_value%21-Tuple%7BTwoStageExaModel%2C%20ExaModels.Parameter%7BS%2C%20O%2C%20%3C%3ASecondStageTag%7D%20where%20%7BS%2C%20O%7D%2C%20Integer%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.sgradient!-NTuple{5, Any}" +uri = "core/#ExaModels.sgradient%21-NTuple%7B5%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.shessian!-NTuple{7, Any}" +uri = "core/#ExaModels.shessian%21-NTuple%7B7%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.sjacobian!-NTuple{6, Any}" +uri = "core/#ExaModels.sjacobian%21-NTuple%7B6%2C%20Any%7D" +[[jl.method]] +name = "ExaModels.solution-Tuple{SolverCore.AbstractExecutionStats, Any}" +uri = "core/#ExaModels.solution-Tuple%7BSolverCore.AbstractExecutionStats%2C%20Any%7D" + +[[jl.module]] +name = "ExaModels.ExaModels" +uri = "core/#$" + +[[jl.type]] +name = "ExaModels.AbstractAdjointNode" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.AbstractArgNode" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.AbstractExaModel" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.AbstractNode" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.AbstractSecondAdjointNode" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.AdjointNode1" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.AdjointNode2" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.AdjointNodeSource" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.AdjointNodeVar" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.AdjointNull" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.ArgCall" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.ArgIndexed" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.ArgLeaf" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.ArgNode1" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.ArgNode2" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.ArgSource" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.CompressedNLPModel" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.Compressor" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.ConAugPair" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.Constant" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.Constraint" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.ConstraintAugmentation" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.ConstraintSlot" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.DataIndexed" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.DataSource" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.EachScenario" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.ExaModelWithOracle" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.Expression" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.Node1" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.Node2" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.Null" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.Objective" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.OracleEvaluator" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.Parameter" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.ProdNode" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.SIMDFunction" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.ScalarNonlinearOracle" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.SecondAdjointNode1" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.SecondAdjointNode2" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.SecondAdjointNodeSource" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.SecondAdjointNodeVar" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.SecondAdjointNull" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.SumNode" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.TimedNLPModel" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.TwoStageExaCore" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.TwoStageExaModel" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.Var" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.VarSource" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.VectorNonlinearOracle" +uri = "core/#$" +[[jl.type]] +name = "ExaModels.WrapperNLPModel" +uri = "core/#$" + +[[std.doc]] +dispname = "Constraint Augmentation with add_con!" +name = "constraint_augmentation" +uri = "constraint_augmentation/" +[[std.doc]] +dispname = "API Manual" +name = "core" +uri = "core/" +[[std.doc]] +dispname = "Developing Extensions" +name = "develop" +uri = "develop/" +[[std.doc]] +dispname = "Example: Distillation Column" +name = "distillation" +uri = "distillation/" +[[std.doc]] +dispname = "CPU and GPU acceleration" +name = "gpu" +uri = "gpu/" +[[std.doc]] +dispname = "Getting Started" +name = "guide" +uri = "guide/" +[[std.doc]] +dispname = "Introduction" +name = "index" +uri = "" +[[std.doc]] +dispname = "JuMP Interface (experimental)" +name = "jump" +uri = "jump/" +[[std.doc]] +dispname = "Example: Optimal Power Flow" +name = "opf" +uri = "opf/" +[[std.doc]] +dispname = "Oracle Constraints" +name = "oracle" +uri = "oracle/" +[[std.doc]] +dispname = "Parameters" +name = "parameters" +uri = "parameters/" +[[std.doc]] +dispname = "Macro Behavior: Returning core from Functions" +name = "patterns" +uri = "patterns/" +[[std.doc]] +dispname = "Performance Tips" +name = "performance" +uri = "performance/" +[[std.doc]] +dispname = "Example: Quadrotor" +name = "quad" +uri = "quad/" +[[std.doc]] +dispname = "Model recipes: building once, instantiating with data" +name = "recipe" +uri = "recipe/" +[[std.doc]] +dispname = "References" +name = "ref" +uri = "ref/" +[[std.doc]] +dispname = "Mathematical Abstraction" +name = "simd" +uri = "simd/" +[[std.doc]] +dispname = "Upgrading from v0.9 to v0.10" +name = "upgrade" +uri = "upgrade/" + +[[std.label]] +dispname = "1. Model-building function and macro renames" +name = "1.-Model-building-function-and-macro-renames" +uri = "upgrade/#$" +[[std.label]] +dispname = "2. ExaCore is immutable" +name = "2.-ExaCore-is-immutable" +uri = "upgrade/#$" +[[std.label]] +dispname = "3. Mechanical substitution rules" +name = "3.-Mechanical-substitution-rules" +uri = "upgrade/#$" +[[std.label]] +dispname = "4. Complete before/after example" +name = "4.-Complete-before/after-example" +uri = "upgrade/#4.-Complete-before%2Fafter-example" +[[std.label]] +dispname = "Accessing the ExaModel" +name = "Accessing-the-ExaModel" +uri = "jump/#$" +[[std.label]] +dispname = "Argument placeholders" +name = "Argument-placeholders" +uri = "recipe/#$" +[[std.label]] +dispname = "As an Optimizer" +name = "As-an-Optimizer" +uri = "jump/#$" +[[std.label]] +name = "Backends" +uri = "jump/#$" +[[std.label]] +dispname = "Background: Expression Patterns and Compilation" +name = "Background:-Expression-Patterns-and-Compilation" +uri = "constraint_augmentation/#Background%3A-Expression-Patterns-and-Compilation" +[[std.label]] +dispname = "CPU and GPU acceleration" +name = "CPU-and-GPU-acceleration" +uri = "gpu/#$" +[[std.label]] +dispname = "Citing ExaModels.jl" +name = "Citing-ExaModels.jl" +uri = "#$" +[[std.label]] +dispname = "Comparison Results" +name = "Comparison-Results" +uri = "distillation/#$" +[[std.label]] +dispname = "Compiling a recipe into a shared library" +name = "Compiling-a-recipe-into-a-shared-library" +uri = "recipe/#$" +[[std.label]] +name = "Constraints" +uri = "guide/#$" +[[std.label]] +dispname = "Consuming the library" +name = "Consuming-the-library" +uri = "recipe/#$" +[[std.label]] +dispname = "Creating Parametric Models" +name = "Creating-Parametric-Models" +uri = "parameters/#$" +[[std.label]] +dispname = "Developing Extensions" +name = "Developing-Extensions" +uri = "develop/#$" +[[std.label]] +dispname = "Documentation Structure" +name = "Documentation-Structure" +uri = "#$" +[[std.label]] +name = "ExaCore" +uri = "guide/#$" +[[std.label]] +name = "ExaModel" +uri = "guide/#$" +[[std.label]] +name = "ExaModels" +uri = "core/#$" +[[std.label]] +name = "Example" +uri = "oracle/#$" +[[std.label]] +dispname = "Example: Data-Driven Dynamics (Reducing Compilation Blowup)" +name = "Example:-Data-Driven-Dynamics-(Reducing-Compilation-Blowup)" +uri = "constraint_augmentation/#Example%3A-Data-Driven-Dynamics-%28Reducing-Compilation-Blowup%29" +[[std.label]] +dispname = "Example: Power Balance in Optimal Power Flow" +name = "Example:-Power-Balance-in-Optimal-Power-Flow" +uri = "constraint_augmentation/#Example%3A-Power-Balance-in-Optimal-Power-Flow" +[[std.label]] +name = "GPUs" +uri = "jump/#$" +[[std.label]] +dispname = "High-level API: embed_oracle" +name = "High-level-API:-embed_oracle" +uri = "oracle/#High-level-API%3A-embed_oracle" +[[std.label]] +dispname = "How the Macros Work" +name = "How-the-Macros-Work" +uri = "patterns/#$" +[[std.label]] +dispname = "Instantiating it" +name = "Instantiating-it" +uri = "recipe/#$" +[[std.label]] +name = "Introduction" +uri = "#$" +[[std.label]] +dispname = "JuMP Interface" +name = "JuMP-Interface" +uri = "jump/#$" +[[std.label]] +dispname = "Key differences from other algebraic modeling tools" +name = "Key-differences-from-other-algebraic-modeling-tools" +uri = "#$" +[[std.label]] +dispname = "Low-level API: VectorNonlinearOracle" +name = "Low-level-API:-VectorNonlinearOracle" +uri = "oracle/#Low-level-API%3A-VectorNonlinearOracle" +[[std.label]] +dispname = "Make sure your array's eltype is concrete" +name = "Make-sure-your-array's-eltype-is-concrete" +uri = "performance/#Make-sure-your-array%27s-eltype-is-concrete" +[[std.label]] +dispname = "Model with Lifted Subexpressions" +name = "Model-with-Lifted-Subexpressions" +uri = "distillation/#$" +[[std.label]] +name = "Objective" +uri = "guide/#$" +[[std.label]] +dispname = "Original Model (without subexpressions)" +name = "Original-Model-(without-subexpressions)" +uri = "distillation/#Original-Model-%28without-subexpressions%29" +[[std.label]] +dispname = "Performance Highlights" +name = "Performance-Highlights" +uri = "#$" +[[std.label]] +dispname = "Performance Tips" +name = "Performance-Tips" +uri = "performance/#$" +[[std.label]] +name = "References" +uri = "ref/#$" +[[std.label]] +dispname = "Returning core for Further Composition" +name = "Returning-core-for-Further-Composition" +uri = "patterns/#$" +[[std.label]] +dispname = "Running the Models" +name = "Running-the-Models" +uri = "distillation/#$" +[[std.label]] +name = "Solutions" +uri = "guide/#$" +[[std.label]] +name = "Subexpressions" +uri = "guide/#$" +[[std.label]] +dispname = "Subexpressions in ExaModels" +name = "Subexpressions-in-ExaModels" +uri = "distillation/#$" +[[std.label]] +name = "Summary" +uri = "constraint_augmentation/#$" +[[std.label]] +dispname = "Summary for an automated migration agent" +name = "Summary-for-an-automated-migration-agent" +uri = "upgrade/#$" +[[std.label]] +dispname = "Supported Solvers" +name = "Supported-Solvers" +uri = "#$" +[[std.label]] +dispname = "Supporting ExaModels.jl" +name = "Supporting-ExaModels.jl" +uri = "#$" +[[std.label]] +name = "Syntax" +uri = "constraint_augmentation/#$" +[[std.label]] +dispname = "The Pitfall: Functions That Forget to Return core" +name = "The-Pitfall:-Functions-That-Forget-to-Return-core" +uri = "patterns/#The-Pitfall%3A-Functions-That-Forget-to-Return-core" +[[std.label]] +dispname = "Upgrading from v0.9 to v0.10" +name = "Upgrading-from-v0.9-to-v0.10" +uri = "upgrade/#$" +[[std.label]] +dispname = "Use a function to create a model" +name = "Use-a-function-to-create-a-model" +uri = "performance/#$" +[[std.label]] +name = "Variables" +uri = "guide/#$" +[[std.label]] +dispname = "What add_con! Does" +name = "What-add_con!-Does" +uri = "constraint_augmentation/#What-add_con%21-Does" +[[std.label]] +dispname = "What can be deferred, and what should just be data" +name = "What-can-be-deferred,-and-what-should-just-be-data" +uri = "recipe/#What-can-be-deferred%2C-and-what-should-just-be-data" +[[std.label]] +dispname = "What is ExaModels.jl?" +name = "What-is-ExaModels.jl?" +uri = "#What-is-ExaModels.jl%3F" +[[std.label]] +dispname = "What is SIMD abstraction?" +name = "What-is-SIMD-abstraction?" +uri = "simd/#What-is-SIMD-abstraction%3F" +[[std.label]] +dispname = "Why SIMD abstraction?" +name = "Why-SIMD-abstraction?" +uri = "simd/#Why-SIMD-abstraction%3F" +[[std.label]] +dispname = "Why bother" +name = "Why-bother" +uri = "recipe/#$" +[[std.label]] +dispname = "Writing a recipe" +name = "Writing-a-recipe" +uri = "recipe/#$" +[[std.label]] +dispname = "Constraint Augmentation with add_con!" +name = "constraint-augmentation" +uri = "constraint_augmentation/#$" +[[std.label]] +dispname = "Example: Distillation Column" +name = "distillation" +uri = "distillation/#$" +[[std.label]] +dispname = "Getting Started" +name = "guide" +uri = "guide/#$" +[[std.label]] +dispname = "Macro Behavior: Returning core from Functions" +name = "macro-behavior" +uri = "patterns/#$" +[[std.label]] +dispname = "Example: Optimal Power Flow" +name = "opf" +uri = "opf/#$" +[[std.label]] +dispname = "Oracle Constraints" +name = "oracle" +uri = "oracle/#$" +[[std.label]] +dispname = "Parameters" +name = "parameters" +uri = "parameters/#$" +[[std.label]] +dispname = "Example: Quadrotor" +name = "quad" +uri = "quad/#$" +[[std.label]] +dispname = "Model recipes: building once, instantiating with data" +name = "recipe" +uri = "recipe/#$" +[[std.label]] +dispname = "SIMD Abstraction" +name = "simd" +uri = "simd/#$" +[[std.label]] +dispname = "Two-Stage Optimization" +name = "two_stage" +uri = "two_stage/#$" diff --git a/docs/inventories/MadNLP.toml b/docs/inventories/MadNLP.toml index 11733ede5..6edbe7f2d 100644 --- a/docs/inventories/MadNLP.toml +++ b/docs/inventories/MadNLP.toml @@ -1,8 +1,561 @@ # DocInventory version 1 -project = "MadNLP" +project = "MadNLP.jl" +version = "0.9.1" -# Placeholder: madnlp.github.io/MadNLP.jl/stable/objects.inv currently 404s -# (checked 2026-08-15), so there is no reachable source to harvest real entries -# from. This empty inventory keeps the build green; @extref MadNLP ... will -# report "not found" (not a build Error) until either the upstream site comes -# back or this file is populated with real entries. +[[jl.function]] +name = "MadNLP.build_kkt!" +uri = "lib/kkt/#MadNLP.build_kkt%21" +[[jl.function]] +name = "MadNLP.compress_hessian!" +uri = "lib/kkt/#MadNLP.compress_hessian%21" +[[jl.function]] +name = "MadNLP.compress_jacobian!" +uri = "lib/kkt/#MadNLP.compress_jacobian%21" +[[jl.function]] +name = "MadNLP.create_callback" +uri = "lib/callbacks/#$" +[[jl.function]] +name = "MadNLP.create_kkt_system" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.dual" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.dual_lb" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.dual_ub" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.factorize!" +uri = "lib/linear_solvers/#MadNLP.factorize%21" +[[jl.function]] +name = "MadNLP.factorize_kkt!" +uri = "lib/kkt/#MadNLP.factorize_kkt%21" +[[jl.function]] +name = "MadNLP.full" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.get_hessian" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.get_jacobian" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.get_kkt" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.inertia" +uri = "lib/linear_solvers/#$" +[[jl.function]] +name = "MadNLP.initialize!" +uri = "lib/kkt/#MadNLP.initialize%21" +[[jl.function]] +name = "MadNLP.introduce" +uri = "lib/linear_solvers/#$" +[[jl.function]] +name = "MadNLP.is_inertia" +uri = "lib/linear_solvers/#$" +[[jl.function]] +name = "MadNLP.is_inertia_correct" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.jtprod!" +uri = "lib/kkt/#MadNLP.jtprod%21" +[[jl.function]] +name = "MadNLP.madnlp" +uri = "lib/ipm/#$" +[[jl.function]] +name = "MadNLP.nnz_jacobian" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.num_variables" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.number_dual" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.number_primal" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.primal" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.primal_dual" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.regularize_diagonal!" +uri = "lib/kkt/#MadNLP.regularize_diagonal%21" +[[jl.function]] +name = "MadNLP.should_regularize_dual" +uri = "lib/kkt/#$" +[[jl.function]] +name = "MadNLP.solve!" +uri = "lib/ipm/#MadNLP.solve%21" +[[jl.function]] +name = "MadNLP.solve_kkt!" +uri = "lib/kkt/#MadNLP.solve_kkt%21" +[[jl.function]] +name = "MadNLP.solve_linear_system!" +uri = "lib/linear_solvers/#MadNLP.solve_linear_system%21" +[[jl.function]] +name = "MadNLP.solve_refine!" +uri = "lib/linear_solvers/#MadNLP.solve_refine%21" +[[jl.function]] +name = "MadNLP.update_barrier!" +uri = "lib/barrier/#MadNLP.update_barrier%21" + +[[jl.type]] +name = "MadNLP.AbstractAdaptiveUpdate" +uri = "lib/barrier/#$" +[[jl.type]] +name = "MadNLP.AbstractBarrierUpdate" +uri = "lib/barrier/#$" +[[jl.type]] +name = "MadNLP.AbstractCallback" +uri = "lib/callbacks/#$" +[[jl.type]] +name = "MadNLP.AbstractCondensedKKTSystem" +uri = "lib/kkt/#$" +[[jl.type]] +name = "MadNLP.AbstractEqualityTreatment" +uri = "lib/callbacks/#$" +[[jl.type]] +name = "MadNLP.AbstractFixedVariableTreatment" +uri = "lib/callbacks/#$" +[[jl.type]] +name = "MadNLP.AbstractKKTSystem" +uri = "lib/kkt/#$" +[[jl.type]] +name = "MadNLP.AbstractKKTVector" +uri = "lib/kkt/#$" +[[jl.type]] +name = "MadNLP.AbstractLinearSolver" +uri = "lib/linear_solvers/#$" +[[jl.type]] +name = "MadNLP.AbstractReducedKKTSystem" +uri = "lib/kkt/#$" +[[jl.type]] +name = "MadNLP.AbstractUnreducedKKTSystem" +uri = "lib/kkt/#$" +[[jl.type]] +name = "MadNLP.DenseCallback" +uri = "lib/callbacks/#$" +[[jl.type]] +name = "MadNLP.DenseCondensedKKTSystem" +uri = "lib/kkt/#$" +[[jl.type]] +name = "MadNLP.DenseKKTSystem" +uri = "lib/kkt/#$" +[[jl.type]] +name = "MadNLP.EnforceEquality" +uri = "lib/callbacks/#$" +[[jl.type]] +name = "MadNLP.LOQOUpdate" +uri = "lib/barrier/#$" +[[jl.type]] +name = "MadNLP.MadNLPExecutionStats" +uri = "lib/ipm/#$" +[[jl.type]] +name = "MadNLP.MadNLPOptions" +uri = "options/#$" +[[jl.type]] +name = "MadNLP.MadNLPSolver" +uri = "lib/ipm/#$" +[[jl.type]] +name = "MadNLP.MakeParameter" +uri = "lib/callbacks/#$" +[[jl.type]] +name = "MadNLP.MonotoneUpdate" +uri = "lib/barrier/#$" +[[jl.type]] +name = "MadNLP.QualityFunctionUpdate" +uri = "lib/barrier/#$" +[[jl.type]] +name = "MadNLP.RelaxBound" +uri = "lib/callbacks/#$" +[[jl.type]] +name = "MadNLP.RelaxEquality" +uri = "lib/callbacks/#$" +[[jl.type]] +name = "MadNLP.ScaledSparseKKTSystem" +uri = "lib/kkt/#$" +[[jl.type]] +name = "MadNLP.SparseCallback" +uri = "lib/callbacks/#$" +[[jl.type]] +name = "MadNLP.SparseCondensedKKTSystem" +uri = "lib/kkt/#$" +[[jl.type]] +name = "MadNLP.SparseKKTSystem" +uri = "lib/kkt/#$" +[[jl.type]] +name = "MadNLP.SparseUnreducedKKTSystem" +uri = "lib/kkt/#$" +[[jl.type]] +name = "MadNLP.UnreducedKKTVector" +uri = "lib/kkt/#$" + +[[std.doc]] +dispname = "Algorithm" +name = "algorithm" +uri = "algorithm/" +[[std.doc]] +dispname = "Home" +name = "index" +uri = "" +[[std.doc]] +dispname = "Installation" +name = "installation" +uri = "installation/" +[[std.doc]] +dispname = "Barrier strategies" +name = "lib/barrier" +uri = "lib/barrier/" +[[std.doc]] +dispname = "Callback wrappers" +name = "lib/callbacks" +uri = "lib/callbacks/" +[[std.doc]] +dispname = "IPM solver" +name = "lib/ipm" +uri = "lib/ipm/" +[[std.doc]] +dispname = "KKT systems" +name = "lib/kkt" +uri = "lib/kkt/" +[[std.doc]] +dispname = "Linear Solvers" +name = "lib/linear_solvers" +uri = "lib/linear_solvers/" +[[std.doc]] +dispname = "KKT systems" +name = "man/kkt" +uri = "man/kkt/" +[[std.doc]] +dispname = "Linear Solvers" +name = "man/linear_solvers" +uri = "man/linear_solvers/" +[[std.doc]] +dispname = "Options" +name = "options" +uri = "options/" +[[std.doc]] +dispname = "Quickstart" +name = "quickstart" +uri = "quickstart/" +[[std.doc]] +dispname = "GPU acceleration" +name = "tutorials/gpu" +uri = "tutorials/gpu/" +[[std.doc]] +dispname = "Custom KKT system" +name = "tutorials/kktsystem" +uri = "tutorials/kktsystem/" +[[std.doc]] +dispname = "Multi-precision" +name = "tutorials/multiprecision" +uri = "tutorials/multiprecision/" +[[std.doc]] +dispname = "Quasi-Newton" +name = "tutorials/quasi_newton" +uri = "tutorials/quasi_newton/" +[[std.doc]] +dispname = "Warm-start" +name = "tutorials/warmstart" +uri = "tutorials/warmstart/" + +[[std.label]] +dispname = "A brief look at the math" +name = "A-brief-look-at-the-math" +uri = "man/kkt/#$" +[[std.label]] +name = "AbstractKKTSystem" +uri = "lib/kkt/#$" +[[std.label]] +name = "AbstractKKTVector" +uri = "lib/kkt/#$" +[[std.label]] +dispname = "Adaptive strategies" +name = "Adaptive-strategies" +uri = "lib/barrier/#$" +[[std.label]] +dispname = "Advanced solution: keeping the solver in memory" +name = "Advanced-solution:-keeping-the-solver-in-memory" +uri = "tutorials/warmstart/#Advanced-solution%3A-keeping-the-solver-in-memory" +[[std.label]] +dispname = "Assembling a KKT system, step by step" +name = "Assembling-a-KKT-system,-step-by-step" +uri = "man/kkt/#Assembling-a-KKT-system%2C-step-by-step" +[[std.label]] +dispname = "Assembling the KKT matrix" +name = "Assembling-the-KKT-matrix" +uri = "man/kkt/#$" +[[std.label]] +dispname = "Barrier strategies" +name = "Barrier-strategies" +uri = "lib/barrier/#$" +[[std.label]] +name = "Callbacks" +uri = "lib/callbacks/#$" +[[std.label]] +dispname = "Citing MadNLP.jl" +name = "Citing-MadNLP.jl" +uri = "#$" +[[std.label]] +dispname = "Computing the initial primal-dual iterate" +name = "Computing-the-initial-primal-dual-iterate" +uri = "algorithm/#$" +[[std.label]] +dispname = "Creating the KKT system" +name = "Creating-the-KKT-system" +uri = "tutorials/kktsystem/#$" +[[std.label]] +dispname = "Defining a problem in arbitrary precision" +name = "Defining-a-problem-in-arbitrary-precision" +uri = "tutorials/multiprecision/#$" +[[std.label]] +name = "Demonstration" +uri = "tutorials/kktsystem/#$" +[[std.label]] +dispname = "Dense KKT systems" +name = "Dense-KKT-systems" +uri = "lib/kkt/#$" +[[std.label]] +name = "Design" +uri = "#$" +[[std.label]] +dispname = "Direct linear solvers" +name = "Direct-linear-solvers" +uri = "lib/linear_solvers/#$" +[[std.label]] +dispname = "Equality constraints" +name = "Equality-constraints" +uri = "algorithm/#$" +[[std.label]] +dispname = "Evaluating the model on the GPU with ExaModels" +name = "Evaluating-the-model-on-the-GPU-with-ExaModels" +uri = "tutorials/gpu/#$" +[[std.label]] +dispname = "Example: Implementing a new KKT system" +name = "Example:-Implementing-a-new-KKT-system" +uri = "tutorials/kktsystem/#Example%3A-Implementing-a-new-KKT-system" +[[std.label]] +dispname = "Factorization algorithm" +name = "Factorization-algorithm" +uri = "man/linear_solvers/#$" +[[std.label]] +dispname = "Feasility restoration phase" +name = "Feasility-restoration-phase" +uri = "algorithm/#$" +[[std.label]] +dispname = "Fixed variables" +name = "Fixed-variables" +uri = "algorithm/#$" +[[std.label]] +dispname = "GPU support" +name = "GPU-support" +uri = "installation/#$" +[[std.label]] +dispname = "HSL linear solver" +name = "HSL-linear-solver" +uri = "installation/#$" +[[std.label]] +dispname = "How is LBFGS implemented in MadNLP?" +name = "How-is-LBFGS-implemented-in-MadNLP?" +uri = "tutorials/quasi_newton/#How-is-LBFGS-implemented-in-MadNLP%3F" +[[std.label]] +dispname = "How to solve a problem on the GPU with ExaModels?" +name = "How-to-solve-a-problem-on-the-GPU-with-ExaModels?" +uri = "tutorials/gpu/#How-to-solve-a-problem-on-the-GPU-with-ExaModels%3F" +[[std.label]] +dispname = "How to solve on the GPU a JuMP model?" +name = "How-to-solve-on-the-GPU-a-JuMP-model?" +uri = "tutorials/gpu/#How-to-solve-on-the-GPU-a-JuMP-model%3F" +[[std.label]] +dispname = "How to solve on the GPU a legacy model running on the CPU?" +name = "How-to-solve-on-the-GPU-a-legacy-model-running-on-the-CPU?" +uri = "tutorials/gpu/#How-to-solve-on-the-GPU-a-legacy-model-running-on-the-CPU%3F" +[[std.label]] +dispname = "How to tune the options in LBFGS?" +name = "How-to-tune-the-options-in-LBFGS?" +uri = "tutorials/quasi_newton/#How-to-tune-the-options-in-LBFGS%3F" +[[std.label]] +dispname = "How to use BFGS in MadNLP?" +name = "How-to-use-BFGS-in-MadNLP?" +uri = "tutorials/quasi_newton/#How-to-use-BFGS-in-MadNLP%3F" +[[std.label]] +dispname = "How to use LBFGS in MadNLP?" +name = "How-to-use-LBFGS-in-MadNLP?" +uri = "tutorials/quasi_newton/#How-to-use-LBFGS-in-MadNLP%3F" +[[std.label]] +dispname = "Implementing a custom KKT system" +name = "Implementing-a-custom-KKT-system" +uri = "tutorials/kktsystem/#$" +[[std.label]] +name = "Inertia-correction" +uri = "algorithm/#$" +[[std.label]] +dispname = "Inertia detection" +name = "Inertia-detection" +uri = "man/linear_solvers/#$" +[[std.label]] +dispname = "Initializing a KKT system" +name = "Initializing-a-KKT-system" +uri = "man/kkt/#$" +[[std.label]] +name = "Installation" +uri = "installation/#$" +[[std.label]] +dispname = "Interior-point algorithm" +name = "Interior-point-algorithm" +uri = "algorithm/#$" +[[std.label]] +dispname = "Interior-point iterations" +name = "Interior-point-iterations" +uri = "algorithm/#$" +[[std.label]] +dispname = "Iterative refinement" +name = "Iterative-refinement" +uri = "lib/linear_solvers/#$" +[[std.label]] +dispname = "MadNLP's performance" +name = "MadNLP's-performance" +uri = "#MadNLP%27s-performance" +[[std.label]] +dispname = "MadNLP's problem structure" +name = "MadNLP's-problem-structure" +uri = "#MadNLP%27s-problem-structure" +[[std.label]] +dispname = "MadNLP solver" +name = "MadNLP-solver" +uri = "lib/ipm/#$" +[[std.label]] +dispname = "Monotone strategy" +name = "Monotone-strategy" +uri = "lib/barrier/#$" +[[std.label]] +dispname = "Naive solution: starting from the previous solution" +name = "Naive-solution:-starting-from-the-previous-solution" +uri = "tutorials/warmstart/#Naive-solution%3A-starting-from-the-previous-solution" +[[std.label]] +dispname = "Pardiso linear solver" +name = "Pardiso-linear-solver" +uri = "installation/#$" +[[std.label]] +name = "Pre-processing" +uri = "algorithm/#$" +[[std.label]] +dispname = "Problem's reformulation" +name = "Problem's-reformulation" +uri = "algorithm/#Problem%27s-reformulation" +[[std.label]] +dispname = "Quasi-Newton methods" +name = "Quasi-Newton-methods" +uri = "tutorials/quasi_newton/#$" +[[std.label]] +name = "Quickstart" +uri = "quickstart/#$" +[[std.label]] +dispname = "Running MadNLP in arbitrary precision" +name = "Running-MadNLP-in-arbitrary-precision" +uri = "tutorials/multiprecision/#$" +[[std.label]] +dispname = "Running MadNLP on the GPU" +name = "Running-MadNLP-on-the-GPU" +uri = "tutorials/gpu/#$" +[[std.label]] +name = "Scaling" +uri = "algorithm/#$" +[[std.label]] +dispname = "Slack variables" +name = "Slack-variables" +uri = "algorithm/#$" +[[std.label]] +dispname = "Solution of the KKT system" +name = "Solution-of-the-KKT-system" +uri = "man/kkt/#$" +[[std.label]] +dispname = "Solution of the primal-dual KKT system" +name = "Solution-of-the-primal-dual-KKT-system" +uri = "algorithm/#$" +[[std.label]] +dispname = "Solving a KKT system with MadNLP" +name = "Solving-a-KKT-system-with-MadNLP" +uri = "man/linear_solvers/#$" +[[std.label]] +dispname = "Solving a problem in Float128" +name = "Solving-a-problem-in-Float128" +uri = "tutorials/multiprecision/#$" +[[std.label]] +dispname = "Solving a problem in Float32" +name = "Solving-a-problem-in-Float32" +uri = "tutorials/multiprecision/#$" +[[std.label]] +dispname = "Solving an AbstractKKTSystem in MadNLP" +name = "Solving-an-AbstractKKTSystem-in-MadNLP" +uri = "tutorials/kktsystem/#$" +[[std.label]] +dispname = "Solving the problem on the GPU with HyKKT" +name = "Solving-the-problem-on-the-GPU-with-HyKKT" +uri = "tutorials/gpu/#$" +[[std.label]] +dispname = "Solving the problem on the GPU with MadNLP" +name = "Solving-the-problem-on-the-GPU-with-MadNLP" +uri = "tutorials/gpu/#$" +[[std.label]] +dispname = "Sparse KKT systems" +name = "Sparse-KKT-systems" +uri = "lib/kkt/#$" +[[std.label]] +dispname = "Step 1: Computing the first and second-order sensitivities" +name = "Step-1:-Computing-the-first-and-second-order-sensitivities" +uri = "algorithm/#Step-1%3A-Computing-the-first-and-second-order-sensitivities" +[[std.label]] +dispname = "Step 2: Updating the barrier parameter" +name = "Step-2:-Updating-the-barrier-parameter" +uri = "algorithm/#Step-2%3A-Updating-the-barrier-parameter" +[[std.label]] +dispname = "Step 3: Solving the primal-dual KKT system" +name = "Step-3:-Solving-the-primal-dual-KKT-system" +uri = "algorithm/#Step-3%3A-Solving-the-primal-dual-KKT-system" +[[std.label]] +dispname = "Step 4: Finding the next iterate using the filter line-search" +name = "Step-4:-Finding-the-next-iterate-using-the-filter-line-search" +uri = "algorithm/#Step-4%3A-Finding-the-next-iterate-using-the-filter-line-search" +[[std.label]] +dispname = "Structure exploiting methods" +name = "Structure-exploiting-methods" +uri = "tutorials/kktsystem/#$" +[[std.label]] +dispname = "Updating a KKT system" +name = "Updating-a-KKT-system" +uri = "man/kkt/#$" +[[std.label]] +dispname = "Updating the initial guess" +name = "Updating-the-initial-guess" +uri = "tutorials/warmstart/#$" +[[std.label]] +dispname = "Updating the values of the Hessian" +name = "Updating-the-values-of-the-Hessian" +uri = "man/kkt/#$" +[[std.label]] +dispname = "Updating the values of the Jacobian" +name = "Updating-the-values-of-the-Jacobian" +uri = "man/kkt/#$" +[[std.label]] +dispname = "Updating the values of the diagonal matrices" +name = "Updating-the-values-of-the-diagonal-matrices" +uri = "man/kkt/#$" +[[std.label]] +dispname = "Using MadNLP with JuMP" +name = "Using-MadNLP-with-JuMP" +uri = "quickstart/#$" +[[std.label]] +dispname = "Using MadNLP with NLPModels" +name = "Using-MadNLP-with-NLPModels" +uri = "quickstart/#$" +[[std.label]] +dispname = "Warmstarting MadNLP" +name = "Warmstarting-MadNLP" +uri = "tutorials/warmstart/#$" diff --git a/docs/make.jl b/docs/make.jl index e075253db..cd77faae0 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -215,14 +215,14 @@ links = InterLinks( "https://jso.dev/NLPModelsIpopt.jl/stable/objects.inv", ), "ExaModels" => ( - "https://exanauts.github.io/ExaModels.jl/stable/", + "https://madsuite-org.github.io/ExaModels.jl/stable/", joinpath(@__DIR__, "inventories", "ExaModels.toml"), - "https://exanauts.github.io/ExaModels.jl/stable/objects.inv", + "https://madsuite-org.github.io/ExaModels.jl/stable/objects.inv", ), "MadNLP" => ( - "https://madnlp.github.io/MadNLP.jl/stable/", + "https://madsuite-org.github.io/MadNLP.jl/stable/", joinpath(@__DIR__, "inventories", "MadNLP.toml"), - "https://madnlp.github.io/MadNLP.jl/stable/objects.inv", + "https://madsuite-org.github.io/MadNLP.jl/stable/objects.inv", ), "Tutorials" => ( "https://control-toolbox.org/Tutorials.jl/stable/", diff --git a/docs/probes/common.jl b/docs/probes/common.jl index 84f95f67b..40280cd86 100644 --- a/docs/probes/common.jl +++ b/docs/probes/common.jl @@ -3,9 +3,11 @@ # A probe checks a claim of the documentation that the docs build cannot execute (see # README.md). It throws on failure; `run.jl` runs every probe in a fresh Julia process. -# Use the OptimalControl of this repository, as docs/make.jl does. +# Same load path as docs/make.jl: the docs environment first (its Manifest decides every +# version), then this repository for OptimalControl itself. const REPO_ROOT = normpath(joinpath(@__DIR__, "..", "..")) pushfirst!(LOAD_PATH, REPO_ROOT) +pushfirst!(LOAD_PATH, joinpath(REPO_ROOT, "docs")) """ error_text(f) -> String @@ -36,8 +38,8 @@ end """ temp_env(pkgs...) -Activate a temporary environment holding `pkgs` and stack the docs environment behind it, -for claims about packages the docs environment does not have. The first call downloads +Activate a temporary environment holding `pkgs`, for claims about packages the docs +environment does not have. The docs environment stays first in the load path. The first call downloads and precompiles them. """ function temp_env(pkgs::AbstractString...) @@ -45,7 +47,6 @@ function temp_env(pkgs::AbstractString...) # Pkg is loaded just above, in a newer world: call it through invokelatest. Base.invokelatest(Pkg.activate; temp=true, io=devnull) Base.invokelatest(Pkg.add, collect(pkgs); io=devnull) - push!(LOAD_PATH, joinpath(REPO_ROOT, "docs")) return nothing end diff --git a/docs/probes/solve_gpu.jl b/docs/probes/solve_gpu.jl new file mode 100644 index 000000000..e6ebb7ceb --- /dev/null +++ b/docs/probes/solve_gpu.jl @@ -0,0 +1,21 @@ +# Page: solve/gpu.md, section "Prerequisites" +# Claims: with MadNLPGPU and CUDA but without CUDSS, a :gpu solve fails with an +# ExtensionError reporting "Missing CUDSS" and the hint `using CUDSS`; and `using ExaModels` +# makes `objective` and `constraint` ambiguous. The docs build loads all the packages, so it +# cannot show either. Runs without a GPU: the error is raised before the device is used. + +include("common.jl") +using OptimalControl +using MadNLPGPU +using CUDA + +ocp = eval(ENERGY_DEF) + +msg = error_text(() -> solve(ocp, :gpu; display=false)) +check(occursin("ExtensionError", msg) && occursin("CUDSS", msg) && occursin("using CUDSS", msg), + "without CUDSS, the :gpu solve reports Missing CUDSS with the hint `using CUDSS`") + +using ExaModels +msg = error_text(() -> @eval Main objective) +check(occursin("UndefVarError", msg) || occursin("ambigu", msg), + "`using ExaModels` makes `objective` ambiguous with OptimalControl's accessor") diff --git a/docs/src/solve/gpu.md b/docs/src/solve/gpu.md index ccd2f3b7b..f3bb48fc9 100644 --- a/docs/src/solve/gpu.md +++ b/docs/src/solve/gpu.md @@ -1,7 +1,7 @@ # [GPU](@id solve-gpu) -GPU support runs through [ExaModels.jl](https://exanauts.github.io/ExaModels.jl/stable) and -[MadNLPGPU.jl](https://github.com/MadNLP/MadNLP.jl), NVIDIA GPUs only, via +GPU support runs through [ExaModels.jl](https://madsuite-org.github.io/ExaModels.jl/stable/) and +[MadNLPGPU.jl](https://madsuite-org.github.io/MadNLP.jl/stable/), NVIDIA GPUs only, via [CUDA.jl](https://github.com/JuliaGPU/CUDA.jl). !!! note "What you are reading depends on the machine that built this page" @@ -23,31 +23,23 @@ println("CUDA.functional() = ", CUDA.functional()) Check `CUDA.functional()` before assuming a `:gpu` solve will actually run on the device. -!!! warning "All three — and `CUDSS` is the one you will forget" +!!! warning "All three, and `CUDSS` is the one you will forget" - The `CTSolversMadNLPGPU` extension is armed by `MadNLPGPU`, `CUDA` **and** `CUDSS` - together. Load only the first two — the pair every GPU tutorial shows — and the extension - does not load, so the GPU solver strategies are never registered. + The GPU solvers need `MadNLPGPU`, `CUDA` **and** `CUDSS`, loaded together. `CUDSS` (the + sparse linear solver on the GPU) is a weak dependency of `MadNLPGPU`: `using MadNLPGPU` + does not load it. Load only the first two, and the GPU solve fails with an + `ExtensionError` that reports `Missing CUDSS`, with the hint `using CUDSS`. - It used to work by accident. Up to MadNLPGPU 0.8, `CUDSS` was a hard dependency, so - `using MadNLPGPU` pulled it in and the third trigger was satisfied without anyone asking. - From 0.9 onward it is a weak dependency and you must load it yourself. +`ExaModels` needs no `using` of its own: it comes with OptimalControl, and `:exa` works +without it. Importing it explicitly would also bring its `objective` and `constraint` into +scope, which collide with the accessors of the same name. If you need ExaModels' own API, +import it qualified: `using ExaModels: ExaModels`. - The error names exactly which one is missing — load `MadNLPGPU` and `CUDA` but not - `CUDSS`, and it reports `Missing CUDSS` with the hint `using CUDSS`. +## The problem -`ExaModels` needs no `using` of its own. It is a dependency of OptimalControl, so the module is -already bound after `using OptimalControl` and `:exa` works without it. Importing it explicitly -also brings its `objective` and `constraint` into scope, both of which collide with the -accessors of the same name — see -[#882](https://github.com/control-toolbox/OptimalControl.jl/issues/882). If you do need -ExaModels' own API, import it qualified: `using ExaModels: ExaModels`. - -## The problem must be coordinatewise - -`:exa` — the only GPU-capable modeler — requires dynamics (and any path constraint) written -one coordinate at a time, `∂(x₁)(t) == ...`, not `ẋ(t) == [...]`. See -[Abstract syntax (`@def`)](@ref modelling-abstract-syntax) for the two forms side by side. +`:exa`, the only GPU-capable modeler, needs a problem written with +[`@def`](@ref modelling-abstract-syntax). The dynamics and the path constraints can be written +in vector form, as here, or coordinate by coordinate: ```@example gpu ocp = @def begin @@ -57,8 +49,7 @@ ocp = @def begin v ∈ R, variable x(0) == [0, 1] x(1) == [0, -1] - ∂(x₁)(t) == x₂(t) # coordinatewise - ∂(x₂)(t) == u(t) # — not ẋ(t) == [x₂(t), u(t)] + ẋ(t) == [x₂(t), u(t)] 0 ≤ x₁(t) + v^2 ≤ 1.1 -10 ≤ u(t) ≤ 10 1 ≤ v ≤ 2 @@ -97,23 +88,46 @@ Solver verbosity is a separate concern: `display=false` above silences the Optim report, and the underlying solver takes its own options — `print_level=MadNLP.ERROR` for MadNLP, which needs `using MadNLP` in scope. See [Options](@ref solve-options). -`:gpu` changes what a strategy's own defaults are: `Exa{GPU}` uses a CUDA differentiation -backend, `MadNLP{GPU}` uses the `CUDSSSolver` linear solver instead of MUMPS. `describe(:gpu)` -lists every strategy with a GPU-parameterized variant — this call needs nothing GPU-specific -and runs fine on CPU alone: +## What `:gpu` changes + +`describe(:gpu)` lists every strategy with a GPU variant. It needs nothing GPU-specific and +runs on CPU alone: ```@example gpu describe(:gpu) ``` +Only `:exa`, `:madnlp` and `:madncl` have one. The parameter changes the defaults of their +options. Constructing the strategies does not touch the device, so this runs anywhere: + +```@example gpu +Base.CoreLogging.disable_logging(Base.CoreLogging.Warn) # hide +modeler = OptimalControl.Exa{GPU}() +solver = OptimalControl.MadNLP{GPU}() +println("Exa{GPU} backend: ", options(modeler)[:backend]) +println("MadNLP{GPU} linear_solver: ", options(solver)[:linear_solver]) +Base.CoreLogging.disable_logging(Base.CoreLogging.BelowMinLevel) # hide +nothing # hide +``` + +```@example gpu +modeler = OptimalControl.Exa() # same as OptimalControl.Exa{CPU}() +solver = OptimalControl.MadNLP() # same as OptimalControl.MadNLP{CPU}() +println("Exa{CPU} backend: ", options(modeler)[:backend]) +println("MadNLP{CPU} linear_solver: ", options(solver)[:linear_solver]) +``` + +On the GPU, ExaModels evaluates the model with a CUDA backend, and MadNLP factorizes its linear +systems with cuDSS instead of MUMPS. + ## Explicit mode Constructing the components does not touch the device, so this block runs anywhere: ```@example gpu -disc = OptimalControl.Collocation(; grid_size=100, scheme=:midpoint) -mod = OptimalControl.Exa{GPU}() -slv = OptimalControl.MadNLP{GPU}() +discretizer = OptimalControl.Collocation(; grid_size=100, scheme=:midpoint) +modeler = OptimalControl.Exa{GPU}() +solver = OptimalControl.MadNLP{GPU}() nothing # hide ``` @@ -121,8 +135,8 @@ Running them is what needs the hardware: ```@example gpu try - global result = solve(ocp; discretizer=disc, modeler=mod, solver=slv) - println("objective = ", objective(result)) + global sol = solve(ocp; discretizer=discretizer, modeler=modeler, solver=solver) + println("objective = ", objective(sol)) catch e println("Exception: ", first(sprint(showerror, e), 400)) end @@ -168,30 +182,40 @@ end # hide ## Performance notes -GPU solving amortizes best on large-scale problems (thousands of variables/constraints) or -repeated solves in a loop, where the per-call setup overhead is paid once. For small problems, -plain CPU solving is typically faster. +GPU solving is beneficial for: + +- **large-scale problems**: thousands of variables and constraints, for instance fine grids; +- **dense computations**: problems with many nonlinear constraints; +- **repeated solves**: the GPU initialization overhead is paid once. -The idiomatic guard is `CUDA.functional()` — pick the strategy, then solve: +For small problems, CPU solving is usually faster. + +The idiomatic guard is `CUDA.functional()`: pick the parameter, then solve: ```@example gpu -strategy = CUDA.functional() ? :gpu : :cpu -println("strategy = ", strategy) +parameter = CUDA.functional() ? :gpu : :cpu +println("parameter = ", parameter) ``` +On a machine with a functional GPU, this block compares the same method on CPU and GPU, on a +fine grid (each solve is run once first, so that compilation is not timed): + ```@example gpu +using MadNLP if CUDA.functional() - t = @elapsed solve(ocp, :gpu; grid_size=1000, display=false) - println("GPU solve at grid_size=1000: ", round(t; digits=2), " s") + for p in (:cpu, :gpu) + solve(ocp, :exa, :madnlp, p; grid_size=1000, display=false, print_level=MadNLP.ERROR) + t = @elapsed solve(ocp, :exa, :madnlp, p; grid_size=1000, display=false, print_level=MadNLP.ERROR) + println(p, " at grid_size=1000: ", round(t; digits=3), " s") + end else - println("No functional device here, so there is no GPU timing to report.") - println("On a CUDA machine this block prints the :gpu solve time at grid_size=1000.") + println("No functional GPU on the machine that built this page: no timing to compare.") end ``` ## See also -- [Overview](@ref solve-overview) — CPU solving basics. +- [Solve overview](@ref solve-overview) — CPU solving basics. - [Choosing a method](@ref solve-choosing-a-method) — the full method list, GPU entries included. - [Explicit mode](@ref solve-explicit-mode) — typed components in general. - The same `:cpu`/`:gpu` distinction applies to `Flow`; see [Flows overview](@ref flows-overview). From 16b7fdb0c2ee6369f00039bec317ca5917448621 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 11:25:32 +0200 Subject: [PATCH 23/57] docs(solution): analytic checks, staggered costate, real duals, flow solutions - Check the trajectories, the objective and the free final time against the closed forms (hidden asserts); one minimum-time problem (t_f = 25/12) for the horizon and the duals, with labels that are not "eq_" on inequalities. - Explain that the direct costate is shifted by half a step, and that the multiplier of the initial condition gives p(t0). - Evaluate the dual accessors at a time instead of printing CoercedTrajectory; plot the path multipliers; state the box sign convention and its two differences with nonlinear constraints (CTDirect#639), and the zeros under :exa (CTDirect#405). - Restore the v2.0.4 intro, the print and plot of a solution, and the summary tables; variable(sol) is a number, a vector or an empty vector. - Show that a flow returns a Solution, and what it lacks (duals, iterations). - Drop the CTModels >= 0.18 history, the type-annotation paragraph and is_empty_time_grid. - Probe solution_duals.jl for the claims the build cannot execute. Co-Authored-By: Claude Opus 5.5 --- docs/probes/solution_duals.jl | 75 +++++++ docs/src/results/solution.md | 364 ++++++++++++++++++++++++---------- 2 files changed, 332 insertions(+), 107 deletions(-) create mode 100644 docs/probes/solution_duals.jl diff --git a/docs/probes/solution_duals.jl b/docs/probes/solution_duals.jl new file mode 100644 index 000000000..71a1a7f29 --- /dev/null +++ b/docs/probes/solution_duals.jl @@ -0,0 +1,75 @@ +# Page: results/solution.md, note "Sign and scale of the multipliers". +# Claims: the same constraint, written as a box or as a path constraint, gets +# multipliers of opposite signs, and only the path one is divided by the time step +# (CTDirect#639); with :exa, the boundary and path multipliers come back as zeros +# (CTDirect#405). Both are known bugs: this probe checks the current behaviour, and fails +# when they are fixed. + +include("common.jl") +using OptimalControl, NLPModelsIpopt + +box = @def begin + tf ∈ R, variable + t ∈ [0, tf], time + x = (q, v) ∈ R², state + u ∈ R, control + tf ≥ 0 + -1 ≤ u(t) ≤ 1, (u_con) + x(0) == [-1, 0], (start) + x(tf) == [0, 0] + ẋ(t) == [v(t), u(t)] + tf → min +end + +path = @def begin + tf ∈ R, variable + t ∈ [0, tf], time + x = (q, v) ∈ R², state + u ∈ R, control + tf ≥ 0 + -1 ≤ u(t) + v(t) - v(t) ≤ 1, (u_con) + x(0) == [-1, 0], (start) + x(tf) == [0, 0] + ẋ(t) == [v(t), u(t)] + tf → min +end + +# u = 1 on [0, 1), u = -1 on (1, 2], p₂(t) = 1 - t: p₂(0.5) = 0.5, upper bound active. +μ(ocp, N) = dual(solve(ocp; grid_size=N, display=false), ocp, :u_con)(0.5) + +μ_box_100, μ_box_400 = μ(box, 100), μ(box, 400) +μ_path_100, μ_path_400 = μ(path, 100), μ(path, 400) + +check(μ_box_100 < 0 && μ_path_100 > 0, + "box and path multipliers have opposite signs where the upper bound is active " * + "(box $(round(μ_box_100; sigdigits=3)), path $(round(μ_path_100; sigdigits=3)))") +check(isapprox(μ_box_100 / μ_box_400, 4; rtol=0.1), + "the box multiplier scales with the time step (÷ 4 when the grid is 4 times finer)") +check(isapprox(μ_path_100, 0.5; atol=0.02) && isapprox(μ_path_400, 0.5; atol=0.01), + "the path multiplier converges to p₂(0.5) = 0.5") + +sol_exa = solve(box, :exa; display=false) +check(all(iszero, dual(sol_exa, box, :start)), + "with :exa, the multiplier of the initial condition is zero") +check(all(iszero, boundary_constraints_dual(sol_exa)), + "with :exa, all boundary multipliers are zero") +sol_adnlp = solve(box, :adnlp; display=false) +check(isapprox(dual(sol_adnlp, box, :start), [1, 1]; atol=1e-2), + "with :adnlp, the multiplier of the initial condition is p(0) = (1, 1)") + +# Note "The costate is shifted by half a step": p(t_i) ≈ p(t_i + h/2) with :midpoint, and +# p(t_i + h) with Gauss–Legendre (CTDirect#640, known bug). +energy = @def begin + t ∈ [0, 1], time + x = (q, v) ∈ R², state + u ∈ R, control + x(0) == [-1, 0], (start) + x(1) == [0, 0] + ẋ(t) == [v(t), u(t)] + 0.5∫(u(t)^2) → min +end +offset(scheme) = let s = solve(energy; scheme, grid_size=100, display=false) + ((6 - costate(s)(0.5)[2]) / 12 - 0.5) / 0.01 # p₂(t) = 6 - 12t, h = 0.01 +end +check(isapprox(offset(:midpoint), 0.5; atol=0.02), "with :midpoint, the costate at t is p(t + h/2)") +check(isapprox(offset(:gauss_legendre_2), 1; atol=0.02), "with :gauss_legendre_2, the costate at t is p(t + h)") diff --git a/docs/src/results/solution.md b/docs/src/results/solution.md index 45f9d225b..9154cc302 100644 --- a/docs/src/results/solution.md +++ b/docs/src/results/solution.md @@ -1,16 +1,15 @@ # [Solution object](@id results-solution) -Everything you do after something has been computed: read the trajectories, check whether it -converged, read the sensitivities. This page mirrors [Inspect a problem](@ref modelling-inspect) -— that page reads a *model* back, this one reads a *solution* back — and the two sections link -to each other throughout. +This page is about what you do once a problem has been solved: print the solution, plot +it, read its trajectories and its objective, check that the solver converged, and read the +Lagrange multipliers. It mirrors [Inspect a problem](@ref modelling-inspect): that page reads +a *model* back, this one reads a *solution* back. -## What you get back +## A solution to read -`solve` returns a [`Solution`](@ref results-solution); a `Flow` call returns a trajectory. Both -are read with the **same generics** — `state`, `control`, `costate`, `objective`, `time_grid`, -`plot` — so everything on this page also applies to a flow's output (see -[Flows](@ref flows-overview)). +We take the energy-minimal double integrator, whose solution is known in closed form: +u(t) = 6 − 12t, q(t) = −1 + 3t² − 2t³, v(t) = 6t − 6t², and the cost is J = 6 (see +[First problem](@ref getting-started-first-problem)). ```@example main using OptimalControl @@ -34,14 +33,31 @@ sol = solve(ocp; display=false) nothing # hide ``` -`Solution` and `AbstractSolution` are imported, not exported — they exist for dispatch, not for -writing type annotations in your own code, so a signature like `f(sol::Solution) = ...` won't -work as written; you'd need `f(sol::OptimalControl.Solution) = ...` or just not annotate. +A solution prints as a summary: -## Trajectories +```@example main +sol +``` -`state`, `control`, `variable`, and `costate` return **functions of time** (except `variable`, -which is a single vector — variables don't vary with time): +and plots in one line, once a plotting package is loaded (see +[Plot a solution](@ref results-plot)): + +```@example main +using Plots +plot(sol) +``` + +## The `Solution` type + +`solve` returns an `OptimalControl.Solution`. The type is not exported: you read a solution +through the functions below, never through its fields. They return functions of time where +the fields store values on a grid, and they apply the conventions of the rest of the site, +such as "1-D is a scalar". Every function on this page is listed with its signature in the +[API reference](@ref api-solution). + +## [Trajectories](@id results-solution-trajectories) + +`state`, `control` and `costate` return **functions of time**: ```@example main x = state(sol) @@ -50,31 +66,89 @@ p = costate(sol) x(0.25), u(0.25), p(0.25) ``` -These functions interpolate — they can be called anywhere in the time horizon, not just at grid -points: +The closed form gives x(0.25) = (−0.84375, 1.125), u(0.25) = 3 and p₁ = 12. ```@example main -0.25 ∈ time_grid(sol) +@assert isapprox(x(0.25), [-0.84375, 1.125]; atol=1e-3) # hide +@assert isapprox(u(0.25), 3; atol=1e-3) # hide +@assert isapprox(p(0.25)[1], 12; atol=1e-2) # hide +nothing # hide ``` +These functions interpolate: they can be called anywhere in the time horizon, not only at the +grid points. `time_grid(sol)` returns the grid (`times(sol)` is the same vector), 251 points +here since the default `grid_size` is 250 steps: + ```@example main -x(0.25) # still works +length(time_grid(sol)), 0.25 ∈ time_grid(sol) ``` -`time_grid(sol)` returns the discretization nodes, and `times(sol)` is an alias for it. +```@example main +@assert times(sol) == time_grid(sol) # hide +@assert length(time_grid(sol)) == 251 # hide +nothing # hide +``` + +**1-D is a scalar here too**: with a 1-D control, `u(t)` is a `Number`, not a length-1 +vector, as everywhere else on the site ([functional-API callbacks](@ref modelling-functional-api-shapes), +[abstract syntax](@ref modelling-abstract-syntax-control)): + +```@example main +typeof(u(0.25)) +``` + +`variable(sol)` is a value, not a function of time: a `Float64` for a 1-D variable, a vector +otherwise, and an empty vector when the problem has no variable, as here: + +```@example main +variable(sol) +``` + +!!! note "The costate is shifted by half a step" + + The costate of a direct solve is the multiplier of the discretised dynamics: with the + default `:midpoint` scheme, there is one per step, and it approximates p at the + *middle* of the step. So `p(t)` is close to the exact costate at t + h/2, with h the + step. Here p₂(t) = 6 − 12t, h = 0.004, and p₂(0.25) is 6 − 12 × 0.252 = 2.976, not 3: + + ```@example main + p(0.25)[2] + ``` + + The difference vanishes as the grid is refined. With the Gauss–Legendre schemes, the + shift is a full step: `p(t)` is close to the exact costate at t + h + ([CTDirect#640](https://github.com/control-toolbox/CTDirect.jl/issues/640)). For an + accurate initial costate, for instance to start a [shooting method](@ref flows-shooting), + the multiplier of the initial condition is better: see + [Dual variables](@ref results-solution-duals). + +```@example main +@assert isapprox(p(0.25)[2], 6 - 12 * (0.25 + 0.002); atol=1e-3) # hide +nothing # hide +``` + +| Function | Returns | +| --- | --- | +| `state(sol)` | x(t), a function of time | +| `control(sol)` | u(t), a function of time | +| `costate(sol)` | p(t), a function of time | +| `variable(sol)` | the optimisation variable: a number, a vector, or an empty vector | +| `time_grid(sol)`, `times(sol)` | the discretisation grid, a vector | ## The time horizon -`initial_time` and `final_time` read the endpoints back. On a fixed-time problem they give you +`initial_time` and `final_time` read the endpoints back. On a fixed-time problem they give what you wrote in the `@def`: ```@example main initial_time(sol), final_time(sol) ``` -The case worth knowing is the **free** one. When a time is an optimisation variable, its value -is not in the model — the model only records *which* component of the variable holds it. The -accessor resolves that against the solution's variable for you: +When a time is an optimisation variable, its value is not in the model: the model only +records *which* component of the variable holds it. The accessor reads that component of the +solution's variable for you. The second problem of this page is a minimum-time transfer, with +the control in [−1, 1] and the speed capped at 0.75. Its labels are used in +[Dual variables](@ref results-solution-duals) below. ```@example main ocp_free = @def begin @@ -82,9 +156,10 @@ ocp_free = @def begin t ∈ [0, tf], time x = (q, v) ∈ R², state u ∈ R, control - tf ≥ 0 - -1 ≤ u(t) ≤ 1 - x(0) == [-1, 0] + tf ≥ 0, (tf_pos) + -1 ≤ u(t) ≤ 1, (u_box) + v(t) ≤ 0.75, (v_max) + x(0) == [-1, 0], (start) q(tf) == 0 v(tf) == 0 ẋ(t) == [v(t), u(t)] @@ -95,31 +170,20 @@ sol_free = solve(ocp_free; display=false) initial_time(sol_free), final_time(sol_free) ``` -Here `t0` is fixed and `tf` is free, so the second value is the optimal final time. It is the -number the solver put in the optimisation variable, reached through the accessor rather than by -indexing: +The optimal control accelerates at u = 1 until the speed reaches 0.75 (t = 0.75), coasts at +that speed, then brakes at u = −1 for 0.75 time units. Accelerating and braking each cover +0.75²/2 = 0.28125 of the unit distance, which leaves 0.4375 to coast, in 0.4375/0.75 = 7/12. So +t_f = 0.75 + 7/12 + 0.75 = 25/12 ≈ 2.0833. The final time is also the variable, here a number: ```@example main -final_time(sol_free), variable(sol_free) +variable(sol_free) ``` -!!! warning "Requires CTModels ≥ 0.18" - - Before that release, `final_time(sol)` threw a `MethodError` on any problem with a free - final time: the solution-level accessor called `final_time(sol.times)` without passing the - variable through, and no method matched a `FreeTimeModel` alone. `initial_time(sol)` hit - the same wall whenever `t0` was the free one. - - The example above is the regression guard — no page in this site read the horizon back from - a solution, which is how the bug shipped. See - [CTModels#402](https://github.com/control-toolbox/CTModels.jl/pull/402). - -**1-D is a scalar here too**: with a 1-D control, `u(t)` is a `Number`, not a length-1 vector — -same rule as everywhere else on the site ([functional-API callbacks](@ref modelling-functional-api-shapes), -[abstract syntax](@ref modelling-abstract-syntax-control)): - ```@example main -typeof(u(0.25)) +@assert isapprox(final_time(sol_free), 25 / 12; atol=1e-4) # hide +@assert variable(sol_free) == final_time(sol_free) # hide +@assert variable(sol_free) isa Float64 # hide +nothing # hide ``` ## The objective @@ -128,7 +192,18 @@ typeof(u(0.25)) objective(sol) ``` -## Did it work +The closed form gives J = 6. On the minimum-time problem, the objective is the final time. + +```@example main +@assert isapprox(objective(sol), 6; atol=1e-3) # hide +@assert isapprox(objective(sol_free), final_time(sol_free); atol=1e-8) # hide +nothing # hide +``` + +## Did it converge + +`successful` says whether the solver reported success. `status` and `message` give its +verdict, `iterations` and `constraints_violation` two figures to judge it by: ```@example main successful(sol), status(sol), message(sol) @@ -138,13 +213,18 @@ successful(sol), status(sol), message(sol) iterations(sol), constraints_violation(sol) ``` -`infos(sol)` returns a `Dict` of anything else the solver reported. +`infos(sol)` returns a `Dict` of anything else the solver reported (empty with Ipopt). + +```@example main +@assert successful(sol) && successful(sol_free) # hide +@assert status(sol) == :first_order # hide +nothing # hide +``` !!! warning "`success` is not `successful`" - `success(sol)` looks like it should work but doesn't — it isn't and never was a CTModels - method (it resolves to `Base.success`, a process-exit-status function). Calling it on a - `Solution` now throws a migration-pointing error: + `success` is a Julia function, about the exit status of a process. Calling it on a + solution raises an error that points to `successful`: ```@repl main try # hide @@ -154,110 +234,180 @@ iterations(sol), constraints_violation(sol) end # hide ``` - See [Migration](@ref migration) for the full list of renamed spellings. + See [Migration](@ref migration) for every renamed function. + +What to do when a solve fails (a better initial guess, a finer grid, other tolerances) is the +subject of [Initial guess](@ref solve-initial-guess) and [Options](@ref solve-options). + +| Function | Returns | +| --- | --- | +| `objective(sol)` | the value of the cost | +| `successful(sol)` | `true` if the solver reported success | +| `status(sol)` | the solver's status, a `Symbol` such as `:first_order` | +| `message(sol)` | the solver's message, a `String` | +| `iterations(sol)` | the number of iterations | +| `constraints_violation(sol)` | the largest constraint violation | +| `infos(sol)` | a `Dict` of anything else the solver reported | ## [Dual variables](@id results-solution-duals) -Dual variables (Lagrange multipliers) give sensitivity information. A richer problem to show -them on: +The Lagrange multipliers of the constraints measure how much the cost would change if a +constraint were relaxed. A constraint with a [label](@ref modelling-abstract-syntax) is read +with `dual(sol, ocp, :label)`. For a boundary or a variable constraint, the multiplier is a +number or a vector: ```@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(sol_free, ocp_free, :tf_pos) ``` -`dual(sol, ocp, :label)` returns the signed multiplier for a labeled constraint — a scalar for -a variable or boundary constraint, a function of time for a path constraint: +The constraint t_f ≥ 0 is inactive, so its multiplier is zero. ```@example main -dual(sol, ocp, :eq_tf) # variable constraint +dual(sol_free, ocp_free, :start) ``` -```@example main -dual(sol, ocp, :eq_x0) # boundary constraint -``` +The multiplier of the initial condition x(0) = x₀ is the initial costate p(t₀). Here the +costate is p(t) = (4/3, 1 − 4t/3) on the first arc, so p(0) = (4/3, 1). This is a more +accurate value than `costate(sol)(0)`, which is shifted by half a step (see +[Trajectories](@ref results-solution-trajectories)). ```@example main -μ_u = dual(sol, ocp, :eq_u) # path constraint — a function of time -μ_u(0.5) +@assert isapprox(dual(sol_free, ocp_free, :tf_pos), 0; atol=1e-8) # hide +@assert isapprox(dual(sol_free, ocp_free, :start), [4/3, 1]; atol=1e-2) # hide +nothing # hide ``` -!!! note "Sign convention" +For a path constraint, the multiplier is a function of time. It is nonzero only where the +constraint is active: the control bound while u = ±1, the speed bound while coasting. - `μ > 0` means the lower-side constraint is active, `μ < 0` the upper-side, `μ = 0` - inactive. For box constraints the solver reports separate non-negative lower/upper - multipliers internally; `dual` combines them as `μ = μ_lb − μ_ub` per component. The raw, - unsigned, non-negative versions are available separately — see below. +```@example main +μ_u = dual(sol_free, ocp_free, :u_box) +μ_v = dual(sol_free, ocp_free, :v_max) +[(t, μ_u(t), μ_v(t)) for t in (0.3, 1.0, 1.8)] +``` -The box-constraint duals, without going through a label — one accessor per group, no per-label -lookup needed: +```@example main +plot(time_grid(sol_free), [μ_u, μ_v]; label=["μ_u" "μ_v"], xlabel="t") +``` ```@example main -state_constraints_lb_dual(sol), state_constraints_ub_dual(sol) +@assert μ_u(0.3) < -1e-3 && μ_u(1.8) > 1e-3 && abs(μ_u(1.0)) < 1e-6 # hide +@assert μ_v(1.0) < -1e-3 && abs(μ_v(0.3)) < 1e-6 # hide +nothing # hide ``` +!!! note "Sign and scale of the multipliers" + + For a box constraint, on a state, control or variable component, the solver returns + separate non-negative multipliers for the lower and the upper bound. `dual` returns + their difference μ = μ_lb − μ_ub: μ > 0 where the lower bound is active, μ < 0 where + the upper bound is active, μ = 0 where the constraint is inactive. Above, u = 1 on + the first arc, so μ_u < 0, and u = −1 on the last one, so μ_u > 0. + + The multipliers of the **path constraints**, those that are not boxes (such as + `v(t) + u(t) ≤ 1`), differ in two ways + ([CTDirect#639](https://github.com/control-toolbox/CTDirect.jl/issues/639)): + + - they keep the solver's sign, which is the opposite: positive where the upper bound is + active; + - they are divided by the time step, whereas the multipliers of box constraints are not. + So box multipliers are of the order of the step, and decrease when the grid is + refined. + + With `:exa`, the multipliers of the path and boundary constraints are not + available yet, and come back as zeros + ([CTDirect#405](https://github.com/control-toolbox/CTDirect.jl/issues/405)). + +The box multipliers can also be read per group, without a label: one function for the lower +bounds and one for the upper bounds, for the state, the control and the variable. For the +state and the control, they are functions of time, with one entry per component: + ```@example main -control_constraints_lb_dual(sol), control_constraints_ub_dual(sol) +state_constraints_lb_dual(sol_free)(1.0), state_constraints_ub_dual(sol_free)(1.0) ``` ```@example main -variable_constraints_lb_dual(sol), variable_constraints_ub_dual(sol) +control_constraints_lb_dual(sol_free)(1.8), control_constraints_ub_dual(sol_free)(1.8) ``` -with matching dimension accessors: +For the variable, they are values: ```@example main -dim_dual_state_constraints_box(sol), -dim_dual_control_constraints_box(sol), -dim_dual_variable_constraints_box(sol) +variable_constraints_lb_dual(sol_free), variable_constraints_ub_dual(sol_free) ``` -And the nonlinear (path/boundary) duals as a whole, plus their counts: +The multipliers of the path constraints (boxes excluded) and of the boundary constraints, +together: +`path_constraints_dual(sol)` is a function of time, `boundary_constraints_dual(sol)` a vector, +here with the 4 boundary conditions of `ocp_free` (x(0) = x₀ counts for two). ```@example main -path_constraints_dual(sol), boundary_constraints_dual(sol) +boundary_constraints_dual(sol_free) ``` ```@example main -dim_path_constraints_nl(sol), dim_boundary_constraints_nl(sol) +@assert boundary_constraints_dual(sol_free)[1:2] ≈ dual(sol_free, ocp_free, :start) # hide +@assert dim_boundary_constraints_nl(sol_free) == 4 # hide +@assert dim_path_constraints_nl(sol_free) == 0 # hide +@assert dim_dual_state_constraints_box(sol_free) == 2 # hide +nothing # hide ``` +| Function | Returns | +| --- | --- | +| `dual(sol, ocp, :label)` | the multiplier of a labelled constraint: a number, a vector, or a function of time | +| `state_constraints_lb_dual(sol)`, `state_constraints_ub_dual(sol)` | the state box multipliers, functions of time | +| `control_constraints_lb_dual(sol)`, `control_constraints_ub_dual(sol)` | the control box multipliers, functions of time | +| `variable_constraints_lb_dual(sol)`, `variable_constraints_ub_dual(sol)` | the variable box multipliers | +| `path_constraints_dual(sol)` | the path multipliers, boxes excluded, a function of time | +| `boundary_constraints_dual(sol)` | the boundary multipliers, a vector | +| `dim_dual_state_constraints_box(sol)`, `dim_dual_control_constraints_box(sol)`, `dim_dual_variable_constraints_box(sol)` | the number of box-constrained components | +| `dim_path_constraints_nl(sol)`, `dim_boundary_constraints_nl(sol)` | the number of path (boxes excluded) and boundary constraints | + ## Back to the model +`model(sol)` returns the problem the solution was computed from, so everything on +[Inspect a problem](@ref modelling-inspect) works on it: + ```@example main model(sol) === ocp ``` -`model(sol)` gives back the exact OCP the solution was computed from — everything on -[Inspect a problem](@ref modelling-inspect) works on it. +## Solutions from a flow + +A [flow](@ref flows-overview) integrated from a problem returns a `Solution` too. Here the +flow of the energy problem, with the control u = p₂ given by the maximum principle, from the +initial costate p(0) = (12, 6): + +```@example main +using OrdinaryDiffEqTsit5 +f = Flow(ocp, (x, p) -> p[2]) +sol_flow = f((t0, tf), x0, [12, 6]) +state(sol_flow)(tf), objective(sol_flow) +``` + +```@example main +@assert isapprox(state(sol_flow)(tf), [0, 0]; atol=1e-8) # hide +@assert isapprox(objective(sol_flow), 6; atol=1e-8) # hide +nothing # hide +``` -## Empty solutions +The trajectories, `objective`, `time_grid`, `model` and `plot` work as above. What comes from a +solver does not: a flow's solution carries no multipliers, so `dual` raises an error, and +`iterations` and `constraints_violation` are `NotProvided`. ```@example main -is_empty_time_grid(sol) +iterations(sol_flow), constraints_violation(sol_flow) ``` -`false` for anything a normal `solve` produced — it's a defensive check for the placeholder -case (an uninitialized or degenerate solution object carrying no time grid at all), not -something a real solve leaves you needing to handle. +```@example main +@assert iterations(sol_flow) isa OptimalControl.NotProvidedType # hide +nothing # hide +``` ## See also -- [Inspect a problem](@ref modelling-inspect) — the model-side mirror of this page. -- [Plot a solution](@ref results-plot) — draw everything read here. -- [Save and load](@ref results-save-load) — persist a solution to disk and reload it. -- [Migration](@ref migration) — every renamed accessor, `success` → `successful` included. +- [Inspect a problem](@ref modelling-inspect): the model-side mirror of this page. +- [Plot a solution](@ref results-plot): draw everything read here. +- [Save and load](@ref results-save-load): write a solution to disk and read it back. +- [Migration](@ref migration): every renamed function, `success` → `successful` included. From 76286b35839430dd30f612c0969e4a74b252b190 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 13:42:23 +0200 Subject: [PATCH 24/57] docs(plot): argument overview, a real path constraint, a 2-D control, flow caveats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restore the v2.0.4 opening (plot / plot! / plot!(plt, …)) and the argument overview table, with anchors for each section. - Constraints: a speed-limited thrust u + v ≤ 1 (a path constraint, not a box in disguise), with its analytic final time checked; say that boxes are drawn as bounds only. - control=:norm on a 2-D control (u = ((16 − 6t)/13, 6/13), J = 8/13 checked), not on a scalar. - title, xlabel and ylabel are set by plot and user values are ignored (CTBase#569): drop the ineffective xlabel="s", show the subplot workaround (hidden asserts). - Flow: start from the exact p(0) = (12, 6); the 5-point grid (CTFlows#435); saveat needs dense=false, otherwise the call crashes (CTFlows#434), checked by the new probe plot_flow_saveat.jl. - Real ExtensionError transcript; drop the redundant Styling section. Co-Authored-By: Claude Opus 5.5 --- docs/probes/plot_flow_saveat.jl | 28 ++++ docs/src/results/plot.md | 267 ++++++++++++++++++++++---------- 2 files changed, 213 insertions(+), 82 deletions(-) create mode 100644 docs/probes/plot_flow_saveat.jl diff --git a/docs/probes/plot_flow_saveat.jl b/docs/probes/plot_flow_saveat.jl new file mode 100644 index 000000000..150101f23 --- /dev/null +++ b/docs/probes/plot_flow_saveat.jl @@ -0,0 +1,28 @@ +# Page: results/plot.md, section "Plotting a flow". +# Claim: a flow built with `saveat` but without `dense=false` crashes when called on a time +# span (CTFlows#434, known bug: this probe checks the current behaviour, and fails when it is +# fixed); with `dense=false`, it works. + +include("common.jl") + +script(dense) = """ +pushfirst!(LOAD_PATH, $(repr(REPO_ROOT))); pushfirst!(LOAD_PATH, $(repr(joinpath(REPO_ROOT, "docs")))) +using OptimalControl, OrdinaryDiffEqTsit5 +ocp = @def begin + t ∈ [0, 1], time + x ∈ R², state + u ∈ R, control + x(0) == [-1, 0] + x(1) == [0, 0] + ẋ(t) == [x₂(t), u(t)] + ∫(0.5u(t)^2) → min +end +f = Flow(ocp, (x, p) -> p[2]; saveat=range(0, 1, 11)$(dense ? "" : ", dense=false")) +sol = f((0, 1), [-1, 0], [12, 6]) +abs(objective(sol) - 6) < 1e-6 || error("wrong objective") +""" + +run_child(code) = success(pipeline(`$(Base.julia_cmd()) --startup-file=no -e $code`; stdout=devnull, stderr=devnull)) + +check(!run_child(script(true)), "with saveat and the default dense, the trajectory call crashes") +check(run_child(script(false)), "with saveat and dense=false, the trajectory call works (objective 6)") diff --git a/docs/src/results/plot.md b/docs/src/results/plot.md index a408019bd..6b5b941dc 100644 --- a/docs/src/results/plot.md +++ b/docs/src/results/plot.md @@ -1,16 +1,41 @@ # [Plot](@id results-plot) -`plot`/`plot!` extend [Plots.jl](https://docs.juliaplots.org) to draw a [`Solution`](@ref results-solution) -directly — the same call works on a `Flow`-produced trajectory too (last section below). The -full signatures are collected under [Reference](@ref results-plot-reference) at the end. +A [`Solution`](@ref results-solution) is drawn with `plot` and `plot!`, which extend +[Plots.jl](https://docs.juliaplots.org). Use `plot` to create a new figure, and `plot!` to add +to an existing one: -**Choosing a backend.** Plots is the default and what this page documents. A second backend, -[Makie.jl](https://docs.makie.org), draws the same figures at feature parity — worth it for an -interactive window, or if you already draw in Makie elsewhere. See -[Plot with Makie](@ref results-plot-makie). +```julia +plot(sol, description...; kw...) # a new plot, which becomes the current one +plot!(sol, description...; kw...) # adds to the current plot +plot!(plt, sol, description...; kw...) # adds to the plot plt +``` + +The same calls draw the solution of a [`Flow`](@ref flows-overview) (last section). The full +signatures are under [Reference](@ref results-plot-reference) at the end of the page. + +**Choosing a backend.** This page uses Plots, the default. [Makie.jl](https://docs.makie.org) +draws the same figures, with the same keywords. It is useful for an interactive window, or if +you already draw with Makie: see [Plot with Makie](@ref results-plot-makie). + +The table below lists the arguments of `plot`, and the section where each is explained: + +| Section | Arguments | +| :--- | :--- | +| [What gets drawn](@ref results-plot-basic) | `size`, `state_style`, `costate_style`, `control_style`, `time_style`, any Plots.jl attribute | +| [Choosing what to draw](@ref results-plot-select) | `description...`: `:state`, `:costate`, `:control`, `:path`, `:dual` | +| [Layout](@ref results-plot-layout) | `layout` | +| [The control](@ref results-plot-control) | `control` | +| [Normalised time](@ref results-plot-time) | `time` | +| [Constraints](@ref results-plot-constraints) | `state_bounds_style`, `control_bounds_style`, `path_style`, `path_bounds_style`, `dual_style` | + +To overlay a second solution, see [Adding to an existing plot](@ref results-plot-add). To build +your own figure from the trajectories, see [Custom plots](@ref results-plot-custom). ## Getting started +We take the energy-minimal double integrator, whose solution is u(t) = 6 − 12t (see +[First problem](@ref getting-started-first-problem)): + ```@example main using OptimalControl using NLPModelsIpopt @@ -34,27 +59,32 @@ sol = solve(ocp; display=false) nothing # hide ``` -`plot` on a solution is an extension — nothing is drawn until `Plots` itself is loaded: +`plot` draws a solution once Plots is loaded: ```@example main using Plots plot(sol) ``` -Without `Plots` in the session the call throws a clean `ExtensionError` instead: +Without Plots, the call raises an error that names the package to load: ```julia -julia> using OptimalControl julia> plot(sol) -ERROR: ExtensionError: missing dependencies to plot solutions -Missing Plots -Hint Run: using Plots +ERROR: ExtensionError +│ +│ missing dependencies to plot solutions +│ +│ Missing Plots +│ +│ Hint Run: using Plots +└─ ``` -## What gets drawn by default +## [What gets drawn](@id results-plot-basic) -With every group shown, the layout is a grid: state trajectories on the left, costate on the -right, control along the bottom. +With every group shown, the figure is a grid: the state components on the left, the costate +components on the right, and the control at the bottom. The dashed vertical lines mark the +initial and final times. Plots.jl attributes apply to the whole figure: ```@example main plot( @@ -63,8 +93,8 @@ plot( ) ``` -`state_style`, `costate_style`, and `control_style` set series attributes per group (any -Plots.jl attribute, as a `NamedTuple`): +`state_style`, `costate_style` and `control_style` set the attributes of one group, as a +`NamedTuple`: ```@example main plot(sol, :state, :costate, :control; @@ -74,8 +104,8 @@ plot(sol, :state, :costate, :control; ) ``` -Vertical markers at the initial/final times are controlled by `time_style`. Any `*_style` also -accepts `:none` to hide that group entirely: +`time_style` styles the vertical lines at the initial and final times. Every `*_style` also +accepts `:none`, which hides that group: ```@example main plot(sol, :state, :costate, :control; @@ -86,10 +116,27 @@ plot(sol, :state, :costate, :control; ) ``` -## Choosing what to draw +Three attributes are the exception: `title`, `xlabel` and `ylabel` are set by `plot` itself, +from the names in the problem, and the values you pass are ignored without a warning +([CTBase#569](https://github.com/control-toolbox/CTBase.jl/issues/569)). To change them, +edit the subplot afterwards, for instance `xlabel!(plt[3], "s")` (see +[Custom plots](@ref results-plot-custom)). -Positional symbols select which groups appear — `:state`, `:costate`, `:control`, and (with a -path constraint present) `:path`, `:dual`: +```@example main +plt_check = plot(sol, :state; xlabel="s") # hide +@assert plt_check.subplots[end][:xaxis][:guide] == "t" # hide +xlabel!(plt_check[end], "s") # hide +@assert plt_check.subplots[end][:xaxis][:guide] == "s" # hide +nothing # hide +``` + +To look up a Plots.jl attribute and its aliases, use `plotattr("linestyle")` (or any other +name) once Plots is loaded. + +## [Choosing what to draw](@id results-plot-select) + +The positional symbols select the groups to draw: `:state`, `:costate`, `:control`, and, for +a problem with [path constraints](@ref results-plot-constraints), `:path` and `:dual`. ```julia plot(sol, :state) # only the state @@ -97,52 +144,68 @@ plot(sol, :costate) # only the costate plot(sol, :control) # only the control ``` -Combine freely: +They combine freely: ```@example main plot(sol, :state, :control) ``` -## Layout +## [Layout](@id results-plot-layout) -`layout=:group` puts each family (state, costate, control) in one subplot instead of one per -component: +`layout=:group` puts each group (state, costate, control) in a single subplot, instead of one +subplot per component: ```@example main plot(sol; layout=:group) ``` -`:split` (the default) is the per-component grid used everywhere above. +`layout=:split`, the default, is the grid with one subplot per component used above. -## The control +## [The control](@id results-plot-control) -`control=:norm` plots the Euclidean norm of the control instead of its components; -`control=:all` plots both: +For a control with several components, `control=:norm` draws its Euclidean norm instead of +its components, and `control=:all` draws both. Take a problem with a 2-D control: ```@example main -plot(sol; control=:norm, layout=:group, size=(800, 300)) +ocp_u = @def begin + t ∈ [0, 1], time + x ∈ R², state + u ∈ R², control + x(0) == [0, 0] + x(1) == [1, 1] + ẋ(t) == [u₁(t), x₁(t) + u₂(t)] + 0.5∫(u₁(t)^2 + u₂(t)^2) → min +end +sol_u = solve(ocp_u; display=false) +nothing # hide ``` +The maximum principle gives u = p, with p₂ constant and ṗ₁ = −p₂. The final conditions +then give u₁(t) = (16 − 6t)/13 and u₂ = 6/13, so the norm decreases from about 1.31 to 0.90: + ```@example main -plot(sol; control=:components, layout=:group, size=(800, 300)) # default +@assert isapprox(objective(sol_u), 8 / 13; atol=1e-4) # hide +@assert isapprox(control(sol_u)(0.5), [13 / 13, 6 / 13]; atol=1e-2) # hide +nothing # hide ``` ```@example main -plot(sol; control=:all, layout=:group) +plot(sol_u; control=:components, layout=:group, size=(800, 300)) # the default ``` -## Styling +```@example main +plot(sol_u; control=:norm, layout=:group, size=(800, 300)) +``` -Covered above (`state_style`/`costate_style`/`control_style`/`time_style`) — repeated here for -the outline: every one of them is a `NamedTuple` of Plots.jl attributes, or `:none` to hide the -group. Use `plotattr("attribute")` (from `Plots`) to look up any attribute's aliases and -description once `using Plots` is loaded. +```@example main +plot(sol_u; control=:all, layout=:group) +``` -## Normalised time +## [Normalised time](@id results-plot-time) -Solve the same problem for several final times and compare them on a normalised time axis -$s = (t - t_0)/(t_f - t_0)$, via `time=:normalize` (or the British spelling, `:normalise` — -both work): +To compare solutions with different final times, `time=:normalize` (or `:normalise`) draws +them against the normalised time s = (t − t₀)/(t_f − t₀) ∈ [0, 1]. Here the same linear-quadratic +problem is solved for three final times: ```@example main function lqr(tf) @@ -164,7 +227,7 @@ plt = plot() for (tf, sol) in zip(tfs, solutions) plot!( plt, sol, :state, :control; - time=:normalize, label="tf = $tf", xlabel="s", + time=:normalize, label="tf = $tf", ) end @@ -178,9 +241,14 @@ plot( ) ``` -## Constraints +The longer the horizon, the longer the solution stays near the origin: this is the turnpike +behaviour of linear-quadratic problems. -A problem with a box control constraint and a nonlinear path constraint: +## [Constraints](@id results-plot-constraints) + +A minimum-time transfer, with the control in [−1, 1], and a thrust limited by the speed: +u(t) + v(t) ≤ 1. This mixed constraint involves the state and the control, so it is a +[path constraint](@ref modelling-formulation), not a box. ```@example main ocp_c = @def begin @@ -189,12 +257,11 @@ ocp_c = @def begin x = (q, v) ∈ R², state u ∈ R, control tf ≥ 0 - -1 ≤ u(t) ≤ 1 - q(0) == -1 - v(0) == 0 + -1 ≤ u(t) ≤ 1, (u_box) + u(t) + v(t) ≤ 1, (thrust) + x(0) == [-1, 0] q(tf) == 0 v(tf) == 0 - 1 ≤ v(t) + 1 ≤ 1.8, (c1) ẋ(t) == [v(t), u(t)] tf → min end @@ -202,13 +269,38 @@ sol_c = solve(ocp_c; display=false) plot(sol_c, :state, :costate, :control, :path, :dual) ``` -The path constraint's bounds are drawn alongside it, with its dual variable in its own panel. -Style keywords for these two extra groups: `path_style`, `dual_style`, and the bounds -decorations `state_bounds_style`, `control_bounds_style`, `path_bounds_style`: +The vehicle accelerates as hard as the thrust limit allows, u = 1 − v, so v(t) = 1 − e^{−t}, +then brakes at u = −1 until it stops. The `:path` panel shows u + v, equal to its bound 1 on +the first arc, and the `:dual` panel its multiplier, nonzero on that arc only. The box bounds +of the control are drawn on the control panel. + +Switching at t₁ and stopping at t_f = t₁ + v(t₁) requires q(t₁) + v(t₁)²/2 = 0, that is +t₁ − v₁ + v₁²/2 = 1 with v₁ = 1 − e^{−t₁}. This gives t₁ ≈ 1.47 and t_f ≈ 2.24: + +```@example main +final_time(sol_c) +``` + +```@example main +t1 = let t = 1.5 # hide + for _ in 1:20 # hide + v = 1 - exp(-t) # hide + r = t - v + v^2 / 2 - 1 # hide + dr = 1 - exp(-t) + v * exp(-t) # hide + t -= r / dr # hide + end # hide + t # hide +end # hide +@assert isapprox(final_time(sol_c), t1 + 1 - exp(-t1); atol=1e-2) # hide +@assert dim_path_constraints_nl(sol_c) == 1 # hide +nothing # hide +``` + +The style keywords of these groups are `path_style` and `dual_style`, and, for the bounds, +`state_bounds_style`, `control_bounds_style` and `path_bounds_style`: ```@example main plot(sol_c, :state, :costate, :control, :path, :dual; - state_bounds_style=(linestyle=:dash,), control_bounds_style=(linestyle=:dash,), path_style=(color=:green,), path_bounds_style=(linestyle=:dash,), @@ -217,9 +309,13 @@ plot(sol_c, :state, :costate, :control, :path, :dual; ) ``` -## Adding to an existing plot +Box constraints are only drawn as bounds: `:path` and `:dual` show the path constraints, not +the boxes. The multipliers of the boxes are read with `dual` (see +[Dual variables](@ref results-solution-duals)). -`plot!` overlays a second solution — same state/costate/control dimensions required: +## [Adding to an existing plot](@id results-plot-add) + +`plot!` overlays a second solution, with the same state, costate and control dimensions: ```@example main ocp2 = @def begin @@ -237,19 +333,19 @@ plt = plot(sol, :state, :costate, :control; label="sol1", size=(700, 500)) plot!(plt, sol2, :state, :costate, :control; label="sol2", linestyle=:dash) ``` -## Custom subplots +## [Custom plots](@id results-plot-custom) -Extract `state`, `control`, `costate` as plain functions to build your own figure: +`state`, `costate` and `control` return functions of time, which Plots draws directly. Here +the absolute value of the control: ```@example main -using LinearAlgebra t = time_grid(sol) u = control(sol) -plot(t, norm ∘ u; label="‖u‖", xlabel="t") +plot(t, abs ∘ u; label="|u|", xlabel="t") ``` -Or reach into an existing `plot(sol, ...)`'s subplots directly — order follows the -`:state, :costate, :control, :path, :dual` grouping, in the order requested: +The subplots of a `plot(sol, ...)` figure can also be reached directly. They come in the +order of the groups requested, one per component: here x₁, x₂, p₁, p₂, then u. ```@example main plt = plot(sol, :state, :costate, :control) @@ -260,43 +356,49 @@ plot(plt[1]) # x₁ plot(plt[5]) # u ``` -A subplot also accepts native Plots calls directly, to annotate rather than re-plot it — for -example marking a control bound: +A subplot accepts the usual Plots.jl calls, to annotate it rather than redraw it. For +example, to mark two levels of the control: ```@example main plot(plt[5]) -hline!([-1, 1]; linestyle=:dash, color=:red) +hline!([-6, 6]; linestyle=:dash, color=:red, label="") ``` -The same idea, in Makie, needs one extra step — a panel there is an `Axis`, not a subplot — -see [Plot with Makie](@ref results-plot-makie). +In Makie the same takes one more step, since a panel there is an `Axis`, not a subplot: see +[Plot with Makie](@ref results-plot-makie). -## Plotting a flow trajectory +## [Plotting a flow](@id results-plot-flow) -The same `plot` call works on a trajectory produced by [`Flow`](@ref) — see -[Flows](@ref flows-overview) for how to build one; here's the plotting side: +The same `plot` call draws the solution of a [`Flow`](@ref) (see [Flows](@ref flows-overview) +for how to build one). Here the flow of the energy problem, with the control u = p₂ given by +the maximum principle, from the exact initial costate p(0) = (12, 6): ```@example main using OrdinaryDiffEqTsit5 -p = costate(sol) -p0 = p(t0) -f = Flow(ocp, (x, p) -> p[2]) # flow from an ocp + a feedback law - +f = Flow(ocp, (x, p) -> p[2]) +p0 = [12, 6] sol_flow = f((t0, tf), x0, p0) plot(sol_flow) ``` -The default grid can be sparse — the $x_2$ subplot above shows it, or read it directly: +The solution of a flow is kept at the steps of the integrator only, which are few on a smooth +problem ([CTFlows#435](https://github.com/control-toolbox/CTFlows.jl/issues/435)). Here +there are 5 points, so the curves above are polygons: ```@example main 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 `variable_costate`, for costate augmentation). -`dense=false` is required alongside `saveat`, since dense output and `saveat` conflict at the -integrator level: +```@example main +@assert isapprox(state(sol_flow)(tf), xf; atol=1e-8) # hide +nothing # hide +``` + +For a finer curve, pass `saveat` when you **build** the flow, together with `dense=false`. The +call itself only accepts `variable`, `unsafe` and `variable_costate`. Do not leave out +`dense=false`: without it, the call crashes the Julia session +([CTFlows#434](https://github.com/control-toolbox/CTFlows.jl/issues/434)). ```@example main fine_grid = range(t0, tf, 100) @@ -315,6 +417,7 @@ plot!(::Plots.Plot, ::CTModels.Solution, ::Symbol...) ## See also -- [Solution object](@ref results-solution) — the accessors this page draws. -- [Save and load](@ref results-save-load) — persist a solution instead of just plotting it. -- [Flows](@ref flows-overview) — building the `Flow` used in the last section. +- [Solution object](@ref results-solution): the functions this page draws. +- [Plot with Makie](@ref results-plot-makie): the same figures with Makie. +- [Save and load](@ref results-save-load): write a solution to disk instead of plotting it. +- [Flows](@ref flows-overview): how to build the `Flow` of the last section. From 468f443149c94ba28e50b83a1db1d80573b18ea2 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 13:48:05 +0200 Subject: [PATCH 25/57] docs(plot-makie): correct diff block, switch read off the control, real path constraint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The diff block's error line is the bare plot(sol), not Makie.plot(sol) twice (B3). - The switching time of the minimum-time example is 1 + √2/2, read off the control and checked (with t_f = 1 + √2), instead of t_f/2. - OptimalControl exports RecipesBase's plot, not Plots'; the ambiguity hint also lists the modules loaded by the build. - The constraints example uses the speed-limited thrust of the Plot page instead of 1 ≤ v + 1 ≤ 1.8. - Shorten the 3 000-character type in the Makie.plot!(ax, sol) error; a phase portrait for the figure built from scratch; the flow starts from the exact p(0). Co-Authored-By: Claude Opus 5.5 --- docs/src/results/plot-makie.md | 59 +++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/docs/src/results/plot-makie.md b/docs/src/results/plot-makie.md index b02d3e713..82a9f20cd 100644 --- a/docs/src/results/plot-makie.md +++ b/docs/src/results/plot-makie.md @@ -26,8 +26,9 @@ using GLMakie # interactive — a window you can pan and zoom ## Qualify `plot` -`OptimalControl` re-exports `Plots`' `plot`/`plot!`. Loading a Makie backend brings in *its* -`plot`/`plot!` too — same names, different functions — so the bare call becomes ambiguous: +`OptimalControl` exports `plot` and `plot!`: the generic functions of RecipesBase, which +Plots extends. Loading a Makie backend brings in *its* `plot`/`plot!` too — same names, +different functions — so the bare name becomes ambiguous: ```@example main using OptimalControl @@ -44,10 +45,12 @@ showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -Julia's own hint names both modules. The fix is to always qualify the Makie call: +Julia's hint lists the modules that export a `plot`: OptimalControl and CairoMakie, and +here also Plots and GR, which this site's build has loaded. The fix is to always qualify the +Makie call: ```julia -Makie.plot(sol) # [!code error] +plot(sol) # [!code error] Makie.plot(sol) # [!code ++] ``` @@ -85,8 +88,8 @@ Makie.plot(sol) Makie.plot(sol; layout=:group, control=:all) ``` -A problem with a box control constraint and a path constraint, the same one used on -[Plot](@ref results-plot): +A problem with a box constraint on the control and a path constraint, the speed-limited +thrust used on [Plot](@ref results-plot-constraints): ```@example main ocp_c = @def begin @@ -95,12 +98,11 @@ ocp_c = @def begin x = (q, v) ∈ R², state u ∈ R, control tf ≥ 0 - -1 ≤ u(t) ≤ 1 - q(0) == -1 - v(0) == 0 + -1 ≤ u(t) ≤ 1, (u_box) + u(t) + v(t) ≤ 1, (thrust) + x(0) == [-1, 0] q(tf) == 0 v(tf) == 0 - 1 ≤ v(t) + 1 ≤ 1.8, (c1) ẋ(t) == [v(t), u(t)] tf → min end @@ -139,6 +141,8 @@ end # hide `plot`/`plot!` build a whole figure in one call — a convenience on top of the backend, not the backend itself. To add something of your own, reach into the figure and use Makie directly. A time-minimal problem makes this concrete: mark the control bounds and the switching time. +From x(0) = (0, 1), the optimal control is u = −1, then u = +1: the final time is +t_f = 1 + √2, and the switch happens at t = 1 + √2/2 ≈ 1.71, not at t_f/2. ```@example main ocp_bb = @def begin @@ -163,11 +167,23 @@ ax = f.content[3] # the control panel — see "Where the panels are" below f ``` +The switching time is read off the control: the first grid point where u becomes positive. + +```@example main +u_bb = control(sol_bb) +t_switch = first(t for t in time_grid(sol_bb) if u_bb(t) > 0) +``` + +```@example main +@assert isapprox(final_time(sol_bb), 1 + sqrt(2); atol=1e-3) # hide +@assert isapprox(t_switch, 1 + sqrt(2) / 2; atol=2e-2) # hide +nothing # hide +``` + ```@example main Makie.hlines!(ax, [-1.0, 1.0]; color=:red, linestyle=:dash) -tf_bb = variable(sol_bb) -Makie.vlines!(ax, [tf_bb / 2]; color=:green) -Makie.text!(ax, tf_bb / 2, 0.0; text="switch") +Makie.vlines!(ax, [t_switch]; color=:green) +Makie.text!(ax, t_switch, 0.0; text="switch") f ``` @@ -186,7 +202,7 @@ sol)` here. It fails: try # hide Makie.plot!(ax, sol_bb) catch e # hide -showerror(IOContext(stdout, :color => false), e) # hide +print(replace(sprint(showerror, e), r"Solution\{.*"s => "Solution{…}}")) # hide end # hide ``` @@ -215,13 +231,12 @@ When annotating an existing panel is not enough — a custom layout, a different entirely — draw straight from the accessors instead of going through `plot` at all: ```@example main -using LinearAlgebra tg = time_grid(sol_bb) -u = control(sol_bb) +x_bb = state(sol_bb) fig = Figure(size=(500, 300)) -axu = Axis(fig[1, 1]; xlabel="t", ylabel="‖u‖") -Makie.lines!(axu, tg, norm.(u.(tg))) +axq = Axis(fig[1, 1]; xlabel="x₁", ylabel="x₂", title="phase portrait") +Makie.lines!(axq, [x_bb(t)[1] for t in tg], [x_bb(t)[2] for t in tg]) fig ``` @@ -242,14 +257,14 @@ f = Makie.plot(sol) # opens a window; unaffected otherwise ## Flows -The same call works on a trajectory produced by [`Flow`](@ref) — see -[Flows](@ref flows-overview) for how to build one: +The same call works on the solution of a [`Flow`](@ref) — see [Flows](@ref flows-overview) +for how to build one. Here from the exact initial costate p(0) = (12, 6), as on +[Plot](@ref results-plot-flow): ```@example main using OrdinaryDiffEqTsit5 -p = costate(sol) -p0 = p(t0) +p0 = [12, 6] flow = Flow(ocp, (x, p) -> p[2]) sol_flow = flow((t0, tf), x0, p0) Makie.plot(sol_flow) From ecd9839e5500dea9e2390e19e02a10f18cdb613e Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 13:51:41 +0200 Subject: [PATCH 26/57] docs(save-load): no doubled extension, measured round-trips, warm start checked - File names: a name that already ends with the extension is used as is (B4; the doubling was fixed by CTModels#399), checked with isfile and cleaned up. - Both formats give back the same solution value for value (grid, state, control, costate, status): hidden asserts replace the vague claim that JSON is not exact; the choice is about the file (JLD2 compact, JSON readable anywhere, sizes shown). - The model passed to import_ocp_solution must be the original one; the error with another model is obscure (CTModels#435). - Real ExtensionError transcript; the warm start is solved and checked. Co-Authored-By: Claude Opus 5.5 --- docs/src/results/save-load.md | 138 +++++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 45 deletions(-) diff --git a/docs/src/results/save-load.md b/docs/src/results/save-load.md index 65f926516..9f5cedc6a 100644 --- a/docs/src/results/save-load.md +++ b/docs/src/results/save-load.md @@ -1,8 +1,8 @@ # [Save & load](@id results-save-load) -Persist a solution to disk and read it back — for expensive solves you don't want to redo, -warm starts across sessions, or sharing a result with someone who doesn't have the code that -produced it. +A solution can be written to disk and read back later: to keep the result of an expensive +solve, to restart from it in another session, or to share it with someone who does not have +the code that produced it. ```@example main using OptimalControl @@ -26,72 +26,131 @@ sol = solve(ocp; display=false) nothing # hide ``` -Two functions cover the whole API — `export_ocp_solution` and `import_ocp_solution` — each -with a `format=` keyword picking the backend: +Two functions do the work, `export_ocp_solution` and `import_ocp_solution`. The `format` +keyword chooses the file format: ```julia -export_ocp_solution(sol; format::Symbol=:JLD, filename::String="solution") -import_ocp_solution(ocp; format::Symbol=:JLD, filename::String="solution") +export_ocp_solution(sol; format=:JLD, filename="solution") +import_ocp_solution(ocp; format=:JLD, filename="solution") ``` -`import_ocp_solution` takes the **model**, not a filename, as its positional argument — the -solution is reconstructed against it, so the model has to be given explicitly rather than -inferred from the file. +`import_ocp_solution` takes the **model** as its positional argument: the file holds the +values of the solution (grid, trajectories, multipliers, solver information), and the +solution is rebuilt against the model. Pass the model the solution was computed from. With +another model, the import fails with an error that does not say so yet +([CTModels#435](https://github.com/control-toolbox/CTModels.jl/issues/435)). ## JLD2 -Exact round-trip of the underlying Julia values — the format to reach for by default. +[JLD2](https://github.com/JuliaIO/JLD2.jl) writes a binary file, `solution.jld2`: ```@example main using JLD2 export_ocp_solution(sol; format=:JLD, filename="solution") -sol_reloaded = import_ocp_solution(ocp; format=:JLD, filename="solution") -objective(sol_reloaded) == objective(sol) +sol_jld = import_ocp_solution(ocp; format=:JLD, filename="solution") +objective(sol_jld) == objective(sol) ``` -## JSON3 +## JSON -Portable, human-readable, at the cost of not being Julia-specific: +[JSON3](https://github.com/quinnj/JSON3.jl) writes a text file, `solution.json`, which other +languages and tools can read: ```@example main using JSON3 export_ocp_solution(sol; format=:JSON, filename="solution") sol_json = import_ocp_solution(ocp; format=:JSON, filename="solution") -objective(sol_json) ≈ objective(sol) +objective(sol_json) == objective(sol) +``` + +## What comes back + +Both formats give back the same solution, value for value: the time grid, the state, control +and costate on that grid, the variable, the multipliers, and the solver's status, message and +iteration count. The model is the one you pass: + +```@example main +tg = time_grid(sol) +time_grid(sol_json) == tg, +all(state(sol_json)(t) == state(sol)(t) for t in tg), +status(sol_json), iterations(sol_json) == iterations(sol), +model(sol_json) === ocp ``` -## Which format +```@example main +for s in (sol_jld, sol_json) # hide + @assert time_grid(s) == tg # hide + @assert all(state(s)(t) == state(sol)(t) for t in tg) # hide + @assert all(control(s)(t) == control(sol)(t) for t in tg) # hide + @assert all(costate(s)(t) == costate(sol)(t) for t in tg) # hide + @assert successful(s) && status(s) == status(sol) # hide +end # hide +nothing # hide +``` -JLD2 round-trips Julia values exactly — the same types come back out. JSON3 is text, portable -across languages and tools, but floating-point round-tripping through it isn't guaranteed -bit-exact in general the way JLD2's is — use JLD2 unless portability specifically matters. +So the choice is about the file, not the content. JLD2 is compact (here about 35 kB) but +readable from Julia only. JSON is about 3 times larger, and readable anywhere. + +```@example main +filesize("solution.jld2"), filesize("solution.json") +``` ## Reloading as an initial guess -A reloaded solution works as a warm start exactly like a freshly-computed one — see -[Initial guess](@ref solve-initial-guess) for everything else `init=` accepts: +A reloaded solution is a warm start, like a freshly computed one (see +[Initial guess](@ref solve-initial-guess) for everything `init` accepts). Started from the +solution, the solver stops at once, with 0 iterations. From scratch it needs 1 here, since +this problem is a quadratic program; on a nonlinear problem the saving is larger. ```@example main -sol_warm = solve(ocp; init=sol_reloaded, display=false) -nothing # hide +sol_warm = solve(ocp; init=sol_jld, display=false) +iterations(sol), iterations(sol_warm) +``` + +```@example main +@assert successful(sol_warm) # hide +@assert isapprox(objective(sol_warm), objective(sol); atol=1e-8) # hide +nothing # hide ``` -## Without the extension +## File names -Both functions are extensions — nothing works until the matching package is loaded: +`filename` is the name of the file without its extension: `.jld2` or `.json` is added, which +is why the same `filename="solution"` works for both formats above. A name that already ends +with the extension is used as it is: + +```@example main +export_ocp_solution(sol; format=:JLD, filename="sol.jld2") +isfile("sol.jld2") +``` + +```@example main +@assert isfile("sol.jld2") && !isfile("sol.jld2.jld2") # hide +rm.(["sol.jld2", "solution.jld2", "solution.json"]) # hide +nothing # hide +``` + +## Without JLD2 or JSON3 + +Both formats need their package. Without it, the call raises an error that names the +package to load: ```julia -julia> using OptimalControl julia> export_ocp_solution(sol; format=:JLD) -ERROR: ExtensionError: missing dependencies to export solutions to JLD2 format -Missing JLD2 -Hint Run: using JLD2 +ERROR: ExtensionError +│ +│ missing dependencies to export solutions to JLD2 format +│ +│ Missing JLD2 +│ +│ Hint Run: using JLD2 +└─ ``` -Same pattern for `format=:JSON`, naming `JSON3` instead. An invalid `format` is caught before -either extension is even needed: +and the same with `JSON3` for `format=:JSON`. A format other than `:JLD` and `:JSON` is +rejected before any package is needed: ```@repl main try # hide @@ -101,18 +160,7 @@ showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -## Filenames have no extension of their own - -`filename` is a **base name** — the extension is added automatically (`.jld2` or `.json`), -which is why the same `filename=` works unchanged for both formats above. Including the -extension yourself doesn't get stripped — it gets doubled: - -```@example main -export_ocp_solution(sol; format=:JLD, filename="sol.jld2") -isfile("sol.jld2.jld2") -``` - ## See also -- [Solution object](@ref results-solution) — what gets saved and reloaded. -- [Initial guess](@ref solve-initial-guess) — every other way to seed a solve, warm start included. +- [Solution object](@ref results-solution): what is saved and read back. +- [Initial guess](@ref solve-initial-guess): every way to start a solve, warm starts included. From a9b2c91920be91c99248ce34ff28505e2a48b938 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 14:09:04 +0200 Subject: [PATCH 27/57] docs(flows-overview): conventions notation, executed point and trajectory calls, no internals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - H1 "Flows overview" (C8); the PMP recap uses the notation of Formulation (H, 𝐇, p⁰ = −1, the control maximises H) and links to it. - Derive the control law of the example (u = p_v) and execute both calls of the flow: the point call reaches the target with p(1) = (12, −6), the trajectory call returns a Solution with J = 6 (hidden asserts). - Drop the maintainer note on why the ExtensionError is not executed, and the paragraph on the internal get_full_strategy_registry. - describe(:sciml) and describe(:di) in collapsible blocks; saveat needs dense=false (CTFlows#434). - method=:gpu: what it selects (integrator, Mooncake AD backend, device arrays), the CTFlows GPU page, and the obscure errors without a GPU or Mooncake (CTFlows#436). Co-Authored-By: Claude Opus 5.5 --- docs/src/flows/overview.md | 108 ++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 38 deletions(-) diff --git a/docs/src/flows/overview.md b/docs/src/flows/overview.md index 1ddb4e7f9..67f6fca4b 100644 --- a/docs/src/flows/overview.md +++ b/docs/src/flows/overview.md @@ -1,4 +1,4 @@ -# [Overview](@id flows-overview) +# [Flows overview](@id flows-overview) `Flow` is one constructor that does three distinct jobs: **indirect optimal control** (build the Hamiltonian flow of the Pontryagin Maximum Principle, write a shooting function, solve it), @@ -18,31 +18,34 @@ you designed independently of any optimization. ## The Pontryagin Maximum Principle, briefly -For an OCP with dynamics $\dot x = f(t,x,u,v)$ and Lagrange cost $f^0$, the pseudo-Hamiltonian -is +The notation is the one of [Notation and conventions](@ref modelling-formulation-conventions). +For a problem with dynamics $\dot x = f(t,x,u,v)$ and Lagrange cost $f^0$, the +pseudo-Hamiltonian is ```math -\tilde H(t, x, p, u, v) = p \cdot f(t,x,u,v) + p^0 f^0(t,x,u,v). +H(t, x, p, u, v) = p \cdot f(t,x,u,v) + p^0 f^0(t,x,u,v), ``` -The maximisation condition picks, at each $(t,x,p,v)$, the control that extremises -$\tilde H$ — a **control law** $u^*(t,x,p,v)$. Substituting it back gives the true Hamiltonian -$H(t,x,p,v) = \tilde H(t,x,p,u^*(t,x,p,v),v)$, whose Hamiltonian system +with $p^0 = -1$ in the normal case. Along an optimal trajectory, the control **maximises** +$H$. When this maximisation gives the control in feedback form, a **control law** +$u(t,x,p,v)$, substituting it gives the maximised Hamiltonian +$\mathbf{H}(t,x,p,v) = H(t,x,p,u(t,x,p,v),v)$, whose Hamiltonian system ```math -\dot x = \partial H/\partial p, \qquad \dot p = -\partial H/\partial x +\dot x = \nabla_p \mathbf{H}, \qquad \dot p = -\nabla_x \mathbf{H} ``` -is the **Hamiltonian flow** this whole section builds and integrates. Boundary conditions on -$(x,p)$ at $t_0$/$t_f$ (transversality) turn "integrate the flow" into "find the missing -$p_0$" — [shooting](@ref flows-shooting). +gives the extremals. Its flow, $t \mapsto (x(t), p(t))$ from $(x_0, p_0)$, is what `Flow` +computes. The boundary and transversality conditions then turn "integrate the flow" into +"find the missing $p_0$": that is [shooting](@ref flows-shooting). ## From the PMP to a flow -You supply $u^*$ — worked out by hand, or read off a `@def` problem via -[`Flow(ocp, ...)`](@ref flows-from-ocp) — and `Flow` gives you $\exp(t\vec H)$: an object -callable at a point (endpoint only) or over a trajectory (the full path), integrated -numerically. +You supply the control law, worked out by hand, and `Flow` builds the Hamiltonian flow. For +the energy-minimal double integrator below, $H = p_q v + p_v u - u^2/2$ is maximised by +$u = p_v$, the second costate component: the control law is `(x, p) -> p[2]`. +[From an OCP](@ref flows-from-ocp) explains how `Flow(ocp, law)` reads the dynamics and the +cost from the problem. ## Three things this section does @@ -65,7 +68,6 @@ Every flow needs an ODE integrator, and none is a hard dependency — load one, ```@example main using OptimalControl using OrdinaryDiffEqTsit5 -using NLPModelsIpopt t0 = 0 tf = 1 @@ -85,8 +87,30 @@ f = Flow(ocp, (x, p) -> p[2]) nothing # hide ``` -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: +A flow is called in two ways. At a point, it returns the state and the costate at the final +time. From the exact initial costate $p(0) = (12, 6)$, it reaches the target $x(1) = (0, 0)$, +with $p(1) = (12, -6)$: + +```@example main +xf, pf = f(t0, x0, [12, 6], tf) +``` + +On a time span, it returns the whole trajectory, as a [`Solution`](@ref results-solution) +that can be read and plotted like the solution of a direct solve: + +```@example main +sol = f((t0, tf), x0, [12, 6]) +objective(sol) +``` + +```@example main +@assert isapprox(xf, [0, 0]; atol=1e-8) && isapprox(pf, [12, -6]; atol=1e-8) # hide +@assert isapprox(objective(sol), 6; atol=1e-8) # hide +nothing # hide +``` + +Every page in this section opens with `using OrdinaryDiffEqTsit5`. Without it, `Flow` raises +an error at construction, naming the package to load: ```julia julia> f = Flow(ocp, (x, p) -> p[2]) @@ -101,48 +125,54 @@ ERROR: ExtensionError → top-level scope, REPL[6]:1 └─ ``` -!!! 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 `describe` covers the indirect side too, not just the direct-solve strategies from [Choosing a method](@ref solve-choosing-a-method): `:sciml` for the integrator family, `:di` for the automatic-differentiation backend that builds a Hamiltonian's vector field. +::: details `describe(:sciml)` + ```@example main describe(:sciml) ``` -Real option names worth knowing: `alg` (the ODE algorithm, default `Tsit5()`), `reltol`/ -`abstol` (default `1e-8` each), `saveat` (explicit save times), `dense` (dense output, -`:auto` by default — resolves to `false` for a point call, `true` for a trajectory call). -Pass any of them as keywords when constructing a flow, e.g. -`Flow(ocp, law; reltol=1e-10, alg=Tsit5())`. +::: + +The options used most are `alg` (the ODE algorithm, `Tsit5()` by default), `reltol` and +`abstol` (`1e-8` each by default), `saveat` (the times at which to save the trajectory) and +`dense` (dense output, `:auto` by default: `false` for a point call, `true` for a trajectory +call). Pass them as keywords when you build a flow, for example +`Flow(ocp, law; reltol=1e-10, alg=Tsit5())`. With `saveat`, also pass `dense=false` +([CTFlows#434](https://github.com/control-toolbox/CTFlows.jl/issues/434)), see +[Plot](@ref results-plot-flow). + +::: details `describe(:di)` ```@example main describe(:di) ``` -`OptimalControl.get_full_strategy_registry()` — internal, not re-exported — is what `describe` -and the constructor completion machinery actually query for the indirect side: it merges the -direct-solve registry with `CTFlows.Flows.flow_registry()`, so `:sciml`/`:di` show up alongside -`:collocation`/`:adnlp`/`:ipopt` in the same introspection tools. +::: ## CPU and GPU -`method=:cpu`/`:gpu` — the same tokens as [`solve`](@ref solve-overview) — are passed **when -constructing** a flow, not on the call: +The parameters `:cpu` and `:gpu`, the same as for [`solve`](@ref solve-gpu), are passed with +the `method` keyword **when you build** a flow. The call has no such keyword: ```julia -f = Flow(ocp, law; method=:gpu) # correct — a constructor keyword -f(t0, x0, p0, tf; method=:gpu) # wrong — no such call-time keyword +f = Flow(ocp, law; method=:gpu) # a keyword of the constructor +f(t0, x0, p0, tf; method=:gpu) # MethodError: not a keyword of the call ``` +`method=:gpu` selects the GPU variants of the integrator and of the automatic +differentiation backend (Mooncake); the initial state and costate must then be device +arrays (`CuArray`). See the +[GPU flows page of CTFlows](https://control-toolbox.org/CTFlows.jl/stable/flows/gpu) for what +it supports. On a machine without a GPU, or without Mooncake loaded, the error comes at the +first call and does not name the cause yet +([CTFlows#436](https://github.com/control-toolbox/CTFlows.jl/issues/436)). + ## Where to go - [From an OCP](@ref flows-from-ocp) — the main path for indirect optimal control. @@ -155,3 +185,5 @@ f(t0, x0, p0, tf; method=:gpu) # wrong — no such call-time keyword - [Shooting](@ref flows-shooting) — the payoff. - [Solve overview](@ref solve-overview) — the direct-method counterpart to everything here. - [Geometry](@ref geometry-overview) — the Lie-theoretic tools some of these constructors build on. +- [Notation and conventions](@ref modelling-formulation-conventions) — the signs and symbols + used throughout this section. From 76fc0585212fba98727c36b27644cc666d222c30 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 14:55:05 +0200 Subject: [PATCH 28/57] docs: follow the CTBase 0.30.6, CTSolvers 0.5.8 and CTModels 0.19.6 fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Installation: another integrator no longer needs OrdinaryDiffEqTsit5 (CTSolvers#230 fixed); the GPU errors name the missing package, except MadNLPGPU, which is asked for after MadNLP (CTSolvers#234). - Inspect: the final_time and mayer hints are now explicit (CTModels#431 fixed); drop the two notes. - Flows overview: execute a flow with alg=Vern9(); OrdinaryDiffEqVerner joins the docs environment. - Probes: installation_integrators.jl checks Verner without Tsit5; installation_gpu_errors.jl checks the CUDA → CUDSS sequence (CTBase#565 fixed) and the MadNLP detour (CTSolvers#234), one child process per set of loaded packages. Co-Authored-By: Claude Opus 5.5 --- docs/Project.toml | 1 + docs/probes/installation_gpu_errors.jl | 48 ++++++++++++++++-------- docs/probes/installation_integrators.jl | 22 +++++------ docs/src/flows/overview.md | 15 ++++++++ docs/src/getting-started/installation.md | 4 +- docs/src/modelling/inspect.md | 6 +-- 6 files changed, 63 insertions(+), 33 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index 0a0a8505a..30d7aef81 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -31,6 +31,7 @@ NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +OrdinaryDiffEqVerner = "79d7bb75-1356-48c1-b8c0-6832512096c2" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" diff --git a/docs/probes/installation_gpu_errors.jl b/docs/probes/installation_gpu_errors.jl index a7388052d..f094800db 100644 --- a/docs/probes/installation_gpu_errors.jl +++ b/docs/probes/installation_gpu_errors.jl @@ -1,21 +1,37 @@ # Page: getting-started/installation.md, section "Optional: GPU" -# Claim: a missing GPU package is reported by an ExtensionError. -# Known upstream bug (CTBase.jl#565): with no GPU package at all, the error asks for -# `MadNLP` and mentions `MadNLP{CPU}`. This probe checks the current behaviour, so it fails -# once the bug is fixed: then update the installation page and this probe. +# Claim: when one of MadNLPGPU, CUDA, CUDSS is missing, `solve(ocp, :gpu)` raises an +# ExtensionError that names it (CTBase.jl#565, fixed in CTBase 0.30.6). +# Known upstream bug (CTSolvers.jl#234): with CUDA loaded but not MadNLPGPU, the first error +# asks for `MadNLP`, and only the next one for `MadNLPGPU`. This part checks the current +# behaviour, so it fails once the bug is fixed: then drop the one-line note and this part. include("common.jl") -using OptimalControl -using NLPModelsIpopt -ocp = eval(ENERGY_DEF) +# One child process per set of loaded packages: an extension cannot be unloaded. +function gpu_error(pkgs::AbstractString...) + code = """ + pushfirst!(LOAD_PATH, $(repr(REPO_ROOT))); pushfirst!(LOAD_PATH, $(repr(joinpath(REPO_ROOT, "docs")))) + using OptimalControl, NLPModelsIpopt + $(join(("using " * p for p in pkgs), "\n")) + ocp = $(ENERGY_DEF) + try + solve(ocp, :gpu; display=false) + catch e + print(sprint(showerror, e)) + end + """ + return read(pipeline(`$(Base.julia_cmd()) --startup-file=no -e $code`; stderr=devnull), String) +end +missing_pkg(msg) = match(r"Missing\s+(\w+)", msg) -msg = error_text(() -> solve(ocp, :gpu; display=false)) -check(occursin("ExtensionError", msg), "solve(ocp, :gpu) without GPU packages raises an ExtensionError") -check(occursin("MadNLP{CPU}", msg), - "known bug CTBase.jl#565: the error still mentions MadNLP{CPU} (update the docs when this fails)") - -using MadNLPGPU -msg = error_text(() -> solve(ocp, :gpu; display=false)) -check(occursin("ExtensionError", msg) && occursin("CUDA", msg), - "with MadNLPGPU only, the error asks for CUDA") +msg = gpu_error() +check(occursin("ExtensionError", msg) && !occursin("MadNLP{CPU}", msg), + "with no GPU package, an ExtensionError that no longer mentions MadNLP{CPU}") +check(missing_pkg(msg)[1] == "CUDA", "with no GPU package, the error asks for CUDA") +check(missing_pkg(gpu_error("MadNLPGPU"))[1] == "CUDA", "with MadNLPGPU only, the error asks for CUDA") +check(missing_pkg(gpu_error("MadNLPGPU", "CUDA"))[1] == "CUDSS", + "with MadNLPGPU and CUDA, the error asks for CUDSS") +check(missing_pkg(gpu_error("CUDA", "CUDSS"))[1] == "MadNLP", + "known bug CTSolvers.jl#234: with CUDA and CUDSS but not MadNLPGPU, the error asks for MadNLP") +check(missing_pkg(gpu_error("CUDA", "CUDSS", "MadNLP"))[1] == "MadNLPGPU", + "then, with MadNLP loaded, it asks for MadNLPGPU") diff --git a/docs/probes/installation_integrators.jl b/docs/probes/installation_integrators.jl index ff73f933c..923dd4a7f 100644 --- a/docs/probes/installation_integrators.jl +++ b/docs/probes/installation_integrators.jl @@ -1,25 +1,23 @@ # Page: getting-started/installation.md, tip "Other integrators" -# Claims: the package is OrdinaryDiffEqVerner (there is no OrdinaryDiffEqVern9), and another -# integrator works with `alg=` as long as OrdinaryDiffEqTsit5 is loaded too. -# Known upstream bug (CTSolvers.jl#230): without OrdinaryDiffEqTsit5, `alg=Vern9()` fails. -# This probe checks the current behaviour, so it fails once the bug is fixed: then relax the -# tip ("keep OrdinaryDiffEqTsit5 loaded in every case") and this probe. +# Claims: the package is OrdinaryDiffEqVerner (there is no OrdinaryDiffEqVern9); another +# integrator works with `alg=` without OrdinaryDiffEqTsit5 (CTSolvers.jl#230, fixed in +# CTSolvers 0.5.8); without Tsit5 and without `alg`, the error says how to choose one. include("common.jl") -temp_env("OrdinaryDiffEqVerner", "OrdinaryDiffEqTsit5") +temp_env("OrdinaryDiffEqVerner") using OptimalControl using OrdinaryDiffEqVerner ocp = eval(ENERGY_DEF) law(x, p) = p[2] -msg = error_text(() -> Flow(ocp, law; alg=Vern9())) -check(occursin("IncorrectArgument", msg), - "known bug CTSolvers.jl#230: without Tsit5, Flow(ocp, law; alg=Vern9()) fails (update the docs when this fails)") - -using OrdinaryDiffEqTsit5 +check(!isdefined(Main, :Tsit5), "OrdinaryDiffEqTsit5 is not loaded") φ = Flow(ocp, law; alg=Vern9()) xf, pf = φ(0, [-1, 0], [12, 6], 1) check(isapprox(xf, [0, 0]; atol=1e-10) && isapprox(pf, [12, -6]; atol=1e-10), - "with Tsit5 also loaded, alg=Vern9() works and reaches x(1) = (0, 0)") + "without Tsit5, Flow(ocp, law; alg=Vern9()) works and reaches x(1) = (0, 0)") + +msg = error_text(() -> Flow(ocp, law)) +check(occursin("PreconditionError", msg) && occursin("Flow(ocp, law; alg=Vern6())", msg), + "without Tsit5 and without alg, a PreconditionError shows how to pass alg") diff --git a/docs/src/flows/overview.md b/docs/src/flows/overview.md index 67f6fca4b..e241034b8 100644 --- a/docs/src/flows/overview.md +++ b/docs/src/flows/overview.md @@ -147,6 +147,21 @@ call). Pass them as keywords when you build a flow, for example ([CTFlows#434](https://github.com/control-toolbox/CTFlows.jl/issues/434)), see [Plot](@ref results-plot-flow). +Another integrator only needs its package. Here the 9th-order Verner method, with tighter +tolerances: + +```@example main +using OrdinaryDiffEqVerner +f9 = Flow(ocp, (x, p) -> p[2]; alg=Vern9(), reltol=1e-12, abstol=1e-12) +f9(t0, x0, [12, 6], tf) +``` + +```@example main +xf9, pf9 = f9(t0, x0, [12, 6], tf) # hide +@assert isapprox(xf9, [0, 0]; atol=1e-10) && isapprox(pf9, [12, -6]; atol=1e-10) # hide +nothing # hide +``` + ::: details `describe(:di)` ```@example main diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index 142fde879..2a2e5cf44 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -74,7 +74,7 @@ Loading this package enables the creation of a `Flow`. Without it, `Flow` raises !!! tip "Other integrators" - `Tsit5` is the default integrator, so keep `OrdinaryDiffEqTsit5` loaded in every case (see [CTSolvers#230](https://github.com/control-toolbox/CTSolvers.jl/issues/230)). To use another method from the [SciML ODE solvers](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/), load its package as well and pass it when you build the flow, for example `Flow(ocp, law; alg=Vern9())` after `using OrdinaryDiffEqVerner`. The umbrella packages `OrdinaryDiffEq` and `DifferentialEquations` include `Tsit5` and work too. + `Tsit5` is the default integrator. To use another method from the [SciML ODE solvers](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/), load its package and pass the algorithm when you build the flow, for example `Flow(ocp, law; alg=Vern9())` after `using OrdinaryDiffEqVerner`. `OrdinaryDiffEqTsit5` is then not needed; without it, a flow built without `alg` raises an error that shows how to pass one. The umbrella packages `OrdinaryDiffEq` and `DifferentialEquations` include `Tsit5` and work too. ## Optional: saving solutions @@ -95,7 +95,7 @@ using CUDA using CUDSS ``` -`CUDSS` is the one people forget: `using MadNLPGPU` does not load it, and the GPU solvers do not work without it. If one of the three is missing, the `ExtensionError` names it. +`CUDSS` is the one people forget: `using MadNLPGPU` does not load it, and the GPU solvers do not work without it. If one of the three is missing, the `ExtensionError` names it. One exception: when `MadNLPGPU` is the missing one, the error first asks for `MadNLP`, and names `MadNLPGPU` only once `MadNLP` is loaded ([CTSolvers#234](https://github.com/control-toolbox/CTSolvers.jl/issues/234)). `ExaModels`, the GPU-capable modeler, needs no `using`: it comes with `OptimalControl`, so `:exa` works out of the box. diff --git a/docs/src/modelling/inspect.md b/docs/src/modelling/inspect.md index b47644ce5..284ee4efe 100644 --- a/docs/src/modelling/inspect.md +++ b/docs/src/modelling/inspect.md @@ -61,8 +61,8 @@ initial_time(ocp) final_time(ocp, [1, 2]) # w = 1, tf = 2 ``` -Asking for a free final time without the variable errors (the hint is not explicit yet, see -[CTModels#431](https://github.com/control-toolbox/CTModels.jl/issues/431)): +Asking for a free final time without the variable raises an error, whose hint says to pass +the variable: ```@repl main final_time(ocp) @@ -201,7 +201,7 @@ has_mayer_cost(ocp) has_lagrange_cost(ocp) ``` -Get the cost functions: `mayer` has signature `g(x0, xf, v)` and errors when there is no Mayer cost (with a hint that does not apply to a built model, see [CTModels#431](https://github.com/control-toolbox/CTModels.jl/issues/431)); `lagrange` has signature `f⁰(t, x, u, v)`: +Get the cost functions: `mayer` has signature `g(x0, xf, v)` and raises an error when there is no Mayer cost (check first with `has_mayer_cost`); `lagrange` has signature `f⁰(t, x, u, v)`: ```@repl main mayer(ocp) From b6a9ea4015cd67ff77fdd22f3f5ea2fa1d1d00a0 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 15:04:18 +0200 Subject: [PATCH 29/57] docs(abstract-syntax): warn about constant components of the dynamics with :adnlp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADNLPModels 0.8.14 fixed the boundary-constraint cases of OptimalControl#481, but a constant component of the dynamics (`ẋ(t) == [x₂(t), u(t), 0]`) still fails with "Cannot determine ordering of Dual tags", with 0.8.13 as with 0.8.14. One-line warning with the workarounds (0 * u(t), or :exa); the probe checks the current behaviour. Co-Authored-By: Claude Opus 5.5 --- docs/probes/abstract_syntax_constants_ad.jl | 39 ++++++++++++++++++--- docs/src/modelling/abstract-syntax.md | 3 ++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/docs/probes/abstract_syntax_constants_ad.jl b/docs/probes/abstract_syntax_constants_ad.jl index 355640204..f76ff337d 100644 --- a/docs/probes/abstract_syntax_constants_ad.jl +++ b/docs/probes/abstract_syntax_constants_ad.jl @@ -1,7 +1,11 @@ -# Page: modelling/abstract-syntax.md (its former "Known issues" section) -# Claim: a constant inside a vector expression no longer breaks the ADNLP modeler -# (OptimalControl.jl#481, fixed by ADNLPModels 0.8.14). The section was removed on that basis; -# if this probe fails, the bug is back: restore a one-line note with the issue link. +# Page: modelling/abstract-syntax.md +# Claims: +# - a constant inside a vector expression of a boundary constraint no longer breaks the ADNLP +# modeler (OptimalControl.jl#481, fixed for this case by ADNLPModels 0.8.14); +# - known bug (OptimalControl.jl#481, still open): a constant component of the dynamics breaks +# it, `0 * u(t)` works around it, and `:exa` is not affected (warning "A constant component"). +# This part checks the current behaviour, so it fails once the bug is fixed: then drop the +# warning and this part. include("common.jl") using OptimalControl @@ -34,3 +38,30 @@ for (name, ocp) in [ sol = solve(ocp; display=false) check(successful(sol), "a constant component in `$name` solves with :adnlp") end + +constant = @def begin + t ∈ [0, 1], time + x ∈ R³, state + u ∈ R, control + x(0) == [-1, 0, 2] + x₁(1) == 0 + x₂(1) == 0 + ẋ(t) == [x₂(t), u(t), 0] + 0.5∫(u(t)^2) → min +end +workaround = @def begin + t ∈ [0, 1], time + x ∈ R³, state + u ∈ R, control + x(0) == [-1, 0, 2] + x₁(1) == 0 + x₂(1) == 0 + ẋ(t) == [x₂(t), u(t), 0 * u(t)] + 0.5∫(u(t)^2) → min +end + +msg = error_text(() -> solve(constant; display=false)) +check(occursin("ordering of Dual tags", msg), + "known bug OptimalControl.jl#481: ẋ(t) == [x₂(t), u(t), 0] fails with :adnlp") +check(successful(solve(workaround; display=false)), "with 0 * u(t), it solves with :adnlp") +check(successful(solve(constant, :exa; display=false)), "with :exa, the constant component solves") diff --git a/docs/src/modelling/abstract-syntax.md b/docs/src/modelling/abstract-syntax.md index 89e20b048..d3ed18532 100644 --- a/docs/src/modelling/abstract-syntax.md +++ b/docs/src/modelling/abstract-syntax.md @@ -323,6 +323,9 @@ nothing # hide !!! note The vector fields `F₀` and `F₁` are defined after the block: functions are only called when the dynamics is evaluated, during `solve` (see [Structure of a definition](@ref modelling-abstract-syntax-structure)). +!!! warning "A constant component" + With the default modeler `:adnlp`, a component of the dynamics that is a constant, such as the `0` in `ẋ(t) == [x₂(t), u(t), 0]`, makes `solve` fail with "Cannot determine ordering of Dual tags" ([OptimalControl#481](https://github.com/control-toolbox/OptimalControl.jl/issues/481)). Write `0 * u(t)` instead, or solve with `:exa`. + While it is also possible to declare the dynamics component after component (see below), one may equivalently use *aliases* (check the relevant [aliases](@ref modelling-abstract-syntax-aliases) section below): ```@example abs From ef019a8ff3cd324664b131f101b2644c404c2bdd Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 15:08:49 +0200 Subject: [PATCH 30/57] docs(from-ocp): derivations checked, the v2.0.4 free-time formulations, B9 and B9b MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Every example states its analytic solution and checks it (hidden asserts): the energy problem, the non-autonomous one (p₀ = 1 derived), the free final time (t_f⁴ = 3/2, p₀ = 2t_f/3; tf ≥ 0 added, the problem was unbounded below). - Restore the v2.0.4 variable section: the variable is always passed with `variable=`, the two other formulations of a free final time (time normalised to [0, 1], and t_f as a state, whose final costate is −1 by transversality); both guards (missing variable, variable on a fixed problem) executed. - "The costate of the variable" (was "Free final time and the augmented costate", with a free ω): solve the problem directly, start the flow from dual(:start), and check the transversality p_ω(t_f) = −2ω; point call only (CTFlows#437). - B9b: Flow(ocp) on a problem with a control raises the guard, executed. - B9: total vs partial explained (the ∇ᵤH terms, when they vanish, :partial to simulate a given feedback), maximiser, isapprox with atol, and why :total reaches the target here (H̄ differs from 𝐇 by a constant). - Link "Solutions from a flow" (new anchor on the Solution page). Co-Authored-By: Claude Opus 5.5 --- docs/src/flows/from-ocp.md | 314 ++++++++++++++++++++++++----------- docs/src/results/solution.md | 2 +- 2 files changed, 220 insertions(+), 96 deletions(-) diff --git a/docs/src/flows/from-ocp.md b/docs/src/flows/from-ocp.md index 17a8e8b9f..2c378c727 100644 --- a/docs/src/flows/from-ocp.md +++ b/docs/src/flows/from-ocp.md @@ -1,7 +1,10 @@ # [From an OCP](@id flows-from-ocp) -The main path: you've worked out the PMP's maximising control $u^*(t,x,p,v)$ by hand, and want -the Hamiltonian flow it defines. +This is the main path of the indirect method. You have worked out, from the maximum +principle, the control law that maximises the pseudo-Hamiltonian, and you want the +Hamiltonian flow it defines. `Flow(ocp, law)` reads the dynamics and the cost from the +problem, so you only write the law. The notation is the one of +[Notation and conventions](@ref modelling-formulation-conventions). ```@example main using OptimalControl @@ -25,9 +28,10 @@ nothing # hide ## The idea -For this problem, the pseudo-Hamiltonian is $\tilde H(x,p,u) = p_1 v + p_2 u + p^0 u^2/2$ -(with $p^0 = -1$, the normal case). The maximisation condition gives $u^*(x,p) = p_2$, since -$\partial^2_{uu}\tilde H = p^0 < 0$. +For this problem, the pseudo-Hamiltonian is $H(x, p, u) = p_1 v + p_2 u + p^0 u^2/2$, with +$p^0 = -1$ in the normal case. Since $\partial^2_{uu} H = p^0 < 0$, $H$ is maximised where +$\partial_u H = p_2 - u = 0$: the control law is $u(x, p) = p_2$. `Flow` integrates the +Hamiltonian system of $\mathbf{H}(x, p) = H(x, p, u(x, p))$, from an initial state and costate. ## The simplest form @@ -35,30 +39,35 @@ $\partial^2_{uu}\tilde H = p^0 < 0$. f = Flow(ocp, (x, p) -> p[2]) p0 = [12, 6] xf, pf = f(t0, x0, p0, tf) -xf ``` -The costate $p_0 = [12, 6]$ above is the one the PMP predicts for this problem; the flow -reaches the target `[0, 0]` from it. +The maximum principle gives $\dot p_1 = 0$ and $\dot p_2 = -p_1$, so $p(t) = (12, 6 - 12t)$ +for the initial costate $p_0 = (12, 6)$: the flow reaches the target $x(1) = (0, 0)$ with +$p(1) = (12, -6)$. + +```@example main +@assert isapprox(xf, [0, 0]; atol=1e-8) && isapprox(pf, [12, -6]; atol=1e-8) # hide +nothing # hide +``` ## Passing a typed law -`Flow(ocp, u)` above is a convenience overload — it wraps the bare function in -`DynClosedLoop`, the law kind that carries a costate. Writing it explicitly is equivalent and -sometimes clearer: +`Flow(ocp, law)` wraps the function in a `DynClosedLoop` law: a feedback on the state and +the costate. Writing it explicitly gives the same flow: ```@example main f_typed = Flow(ocp, DynClosedLoop((x, p) -> p[2])) -f_typed(t0, x0, p0, tf)[1] ≈ xf +xf_typed, _ = f_typed(t0, x0, p0, tf) +isapprox(xf_typed, xf; atol=1e-12) ``` -Two other law kinds exist — `ClosedLoop(x -> ...)` and `OpenLoop(t -> ...)` — but they build a -*state* flow with no costate, appropriate for simulation rather than indirect solving; see -[Simulation](@ref flows-simulation). +The two other kinds of law, `ClosedLoop(x -> …)` and `OpenLoop(t -> …)`, do not involve the +costate: they build a flow of the state alone, to simulate a given control (see +[Simulation](@ref flows-simulation)). ## Non-autonomous problems -When the dynamics depend on $t$ explicitly, the control law does too — `u(t, x, p)`: +When the dynamics depend on $t$, the control law does too: `u(t, x, p)`. ```@example main t0b = 0 @@ -81,151 +90,266 @@ xf_na, pf_na = fb(t0b, x0b, 1, tfb) xf_na - xfb ``` -## Problems with a variable - -An extra optimisation variable extends the law's arity — `u(x, p, v)` (or `u(t, x, p, v)` if -also non-autonomous): +Here $H = p\,u\,(1 + \tan t) - u^2/2$ gives $u = p\,(1 + \tan t)$, and $p$ is constant since +$H$ does not depend on $x$. Then +$x(t_f) = p_0 \int_0^{\pi/4} (1 + \tan t)^2\,\mathrm{d}t = p_0\,(\tan(\pi/4) - 2\log(\sqrt 2/2))$, +so $p_0 = 1$ reaches the target. ```@example main -t0c = 0 -x0c = 0 +@assert abs(xf_na - xfb) < 1e-8 # hide +nothing # hide +``` + +## [Problems with a variable](@id flows-from-ocp-variable) +With an optimisation variable $v$, the control law takes it as a third argument: +`u(x, p, v)`, or `u(t, x, p, v)` for a non-autonomous problem. Take a problem whose variable +is the final time: + +```@example main ocp_v = @def begin tf ∈ R, variable - t ∈ [t0c, tf], time + t ∈ [0, tf], time x ∈ R, state u ∈ R, control - x(t0c) == x0c + tf ≥ 0 + x(0) == 0 x(tf) == 1 ẋ(t) == tf * u(t) tf + 0.5∫(u(t)^2) → min end -fc = Flow(ocp_v, (x, p, v) -> v * p) +fc = Flow(ocp_v, (x, p, tf) -> tf * p) nothing # hide ``` -**`variable=` is mandatory at call time** — there is no positional slot for it any more: +For a given $t_f$, the cheapest control reaching $x(t_f) = 1$ is the constant +$u = 1/t_f^2$, which costs $t_f + 1/(2t_f^3)$. This is minimal for $t_f^4 = 3/2$. The control +law $u = t_f\,p$ then gives the constant costate $p = u/t_f = 2t_f/3$. + +The value of the variable is always passed with the keyword `variable`, even when it is the +final time: ```@example main tf_val = (3 / 2)^(1 / 4) -p0c = 2 * tf_val / 3 -xf_v, pf_v = fc(t0c, x0c, p0c, tf_val; variable=tf_val) +p0c = 2tf_val / 3 +xf_v, pf_v = fc(0, 0, p0c, tf_val; variable=tf_val) xf_v ``` +```@example main +@assert isapprox(xf_v, 1; atol=1e-8) # hide +nothing # hide +``` + +Without it, the call raises an error: + +```@repl main +try # hide +fc(0, 0, p0c, tf_val) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` + +and so does passing `variable` to the flow of a problem without a variable: + ```@repl main try # hide -fc(t0c, x0c, p0c, tf_val) +f(t0, x0, p0, tf; variable=1) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -The mirror image is also enforced: passing `variable=` to a flow built from a `Fixed` problem -(no `variable` in the `@def`) is rejected the same way, not silently ignored. +### Two other formulations of a free final time + +Since the variable is the final time, the change of time $t = s\,t_f$ gives a problem on +$s \in [0, 1]$, where $t_f$ only appears as a parameter: $\mathrm{d}x/\mathrm{d}s = t_f^2\,u$, +and the cost becomes $t_f + (t_f/2)\int_0^1 u^2\,\mathrm{d}s$. The control law is unchanged: + +```@example main +ocp_s = @def begin + tf ∈ R, variable + s ∈ [0, 1], time + x ∈ R, state + u ∈ R, control + tf ≥ 0 + x(0) == 0 + x(1) == 1 + ẋ(s) == tf^2 * u(s) + tf + (0.5 * tf) * ∫(u(s)^2) → min +end + +fs = Flow(ocp_s, (x, p, tf) -> tf * p) +xf_s, _ = fs(0, 0, p0c, 1; variable=tf_val) +xf_s +``` + +Or $t_f$ becomes a state, with zero dynamics, and the problem has no variable any more. The +state is $y = (x, t_f)$, its costate $(p, p_{t_f})$, and the control law reads +$u = t_f\,p$ from them: + +```@example main +ocp_y = @def begin + s ∈ [0, 1], time + y = (x, tf) ∈ R², state + u ∈ R, control + x(0) == 0 + x(1) == 1 + ẏ(s) == [tf(s)^2 * u(s), 0] + tf(1) + 0.5∫(tf(s) * u(s)^2) → min +end + +fy = Flow(ocp_y, (y, q) -> y[2] * q[1]) +yf, qf = fy(0, [0, tf_val], [p0c, 0], 1) +``` + +The initial costate of $t_f$ is $0$, since $t_f(0)$ is free, and its final costate is +$-1$: the transversality condition of the Mayer term $t_f(1)$, with $p^0 = -1$. + +```@example main +@assert isapprox(xf_s, 1; atol=1e-8) # hide +@assert isapprox(yf, [1, tf_val]; atol=1e-8) && isapprox(qf[2], -1; atol=1e-8) # hide +nothing # hide +``` + +The constant `0` in the dynamics is fine for a flow. To solve this problem with the direct +method and the default modeler, write `0 * u(s)` instead (see +[Dynamics](@ref modelling-abstract-syntax-dynamics)). -## Free final time and the augmented costate +## [The costate of the variable](@id flows-from-ocp-variable-costate) -`variable_costate=true` integrates the extra adjoint $\dot p_v = -\partial H/\partial v$ -alongside $(x,p)$ and returns a 3-tuple `(xf, pf, pvf)` instead of 2 — useful for the -transversality condition on a free variable: +With `variable_costate=true`, the point call also integrates the costate of the variable, +$\dot p_v = -\nabla_v H$ from $p_v(t_0) = 0$, and returns three values, `(xf, pf, pvf)`. +This is what the transversality condition on the variable needs (see +[Notation and conventions](@ref modelling-formulation-conventions)). + +Take a harmonic oscillator whose pulsation $\omega$ is optimised, with the cost +$\omega^2 + \frac12\int u^2$, and solve it with the direct method first: ```@example main -q0 = 1 -v0 = 0 -t0d = 0 -tfd = 1 +using NLPModelsIpopt -ocp_aug = @def begin +ocp_ω = @def begin ω ∈ R, variable - t ∈ [t0d, tfd], time + t ∈ [0, 1], time x = (q, v) ∈ R², state u ∈ R, control - q(t0d) == q0 - v(t0d) == v0 - q(tfd) == 0.0 + x(0) == [1, 0], (start) + q(1) == 0 ẋ(t) == [v(t), -ω^2 * q(t) + u(t)] ω^2 + 0.5∫(u(t)^2) → min end -f_aug = Flow(ocp_aug, (x, p, ω) -> p[2]) -ω_val = π / 2 -p0_val = [1.0, 0.5] +sol_ω = solve(ocp_ω; grid_size=500, display=false) +ω_opt = variable(sol_ω) +p0_ω = dual(sol_ω, ocp_ω, :start) # the initial costate (see the Solution page) +ω_opt, p0_ω +``` + +Then integrate the flow from this initial costate, with the costate of $\omega$: + +```@example main +fω = Flow(ocp_ω, (x, p, ω) -> p[2]) +xf_ω, pf_ω, pω = fω(0, [1, 0], p0_ω, 1; variable=ω_opt, variable_costate=true) +pω, -2ω_opt +``` + +With the Mayer term $\omega^2$ and $p^0 = -1$, transversality requires +$p_\omega(t_f) = -2\omega$: the direct solution satisfies it. The flow also reaches +$q(1) = 0$, and $p_v(1) = 0$ since $v(1)$ is free. -xf_aug, pf_aug, pω = f_aug( - t0d, [q0, v0], p0_val, tfd; variable=ω_val, variable_costate=true -) -pω +```@example main +@assert isapprox(pω, -2ω_opt; atol=1e-4) # hide +@assert abs(xf_ω[1]) < 1e-4 && abs(pf_ω[2]) < 1e-4 # hide +nothing # hide ``` -`variable_costate=true` replaced the old `augment=true` keyword — same idea, new name. +`variable_costate=true` works on the point call only. On a trajectory call +`fω((0, 1), …)`, it raises a `MethodError` that does not say so yet +([CTFlows#437](https://github.com/control-toolbox/CTFlows.jl/issues/437)). ## Control-free problems -`Flow(ocp)` — no law argument — works when the problem has no control at all: see -[No control](@ref modelling-without-control) for how to declare one and what -it means. +`Flow(ocp)`, with no law, builds the flow of a problem without a control: see +[Control-free problems](@ref modelling-without-control). On a problem with a control, it +raises an error that asks for a law: + +```@repl main +try # hide +Flow(ocp) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` ## Total or partial Hamiltonian -`hamiltonian_type=` (default `:total`) chooses how the Hamiltonian flow is built from the -pseudo-Hamiltonian and the law: +The keyword `hamiltonian_type` chooses how the flow is built from $H$ and the law +$u(x, p)$: -- `:total` composes a `ComposedHamiltonian` — substitutes the law into $\tilde H$ and - differentiates *through* it: $\dot x = \partial\tilde H/\partial p + - (\partial\tilde H/\partial u)(\partial u/\partial p)$. -- `:partial` builds a `PseudoHamiltonianSystem` and takes partials of $\tilde H$ at the frozen - feedback value: $\dot x = \partial\tilde H/\partial p\big|_{u=u^*(x,p)}$. +- `:total`, the default, integrates the Hamiltonian system of + $\bar H(x, p) = H(x, p, u(x, p))$, differentiating through the law: + $\dot x = \nabla_p H + (\partial_p u)^\top \nabla_u H$ and + $\dot p = -\nabla_x H - (\partial_x u)^\top \nabla_u H$; +- `:partial` takes the partial derivatives of $H$ with the control frozen at the value of the + law: $\dot x = \nabla_p H$, $\dot p = -\nabla_x H$, at $u = u(x, p)$. -They agree **iff the law is stationary for $\tilde H$** ($\partial\tilde H/\partial u = 0$) — -true of every genuine PMP-optimal law, which is why the two are easy to mistake for redundant. -They are not. On this problem's $\tilde H = p_1 v + p_2 u - \tfrac12 u^2$, the minimiser is -$u^*=p_2$; feed it $u = p_2 + 1$ instead — not stationary — and the two modes give different -dynamics: +The two differ by the terms in $\nabla_u H$. These vanish when $\nabla_u H = 0$ along the +law, as for a control that maximises $H$ inside the control set, and also where the law is +locally constant, as for bang–bang or saturated controls. So for an optimal control law the +two give the same flow. For another law, they do not, and the state of `:partial` is the one +the law actually drives, $\dot x = f(x, u(x, p))$: use `:partial` to simulate a given +feedback. -```@example main -law(x, p) = p[2] + 1.0 # not the PMP minimiser +Take the law $u = p_2 + 1$, which does not maximise $H$: -f_total = Flow(ocp, law; hamiltonian_type=:total) -f_partial = Flow(ocp, law; hamiltonian_type=:partial) +```@example main +law(x, p) = p[2] + 1 -xf_total, _ = f_total(t0, x0, p0, tf) -xf_partial, _ = f_partial(t0, x0, p0, tf) -xf_total ≈ xf_partial +xf_total, _ = Flow(ocp, law; hamiltonian_type=:total)(t0, x0, p0, tf) +xf_partial, _ = Flow(ocp, law; hamiltonian_type=:partial)(t0, x0, p0, tf) +xf_total, xf_partial ``` +With `:partial`, the state is driven by $u = p_2 + 1 = 7 - 12t$, which overshoots the target. +With `:total`, the flow is the one of +$\bar H = p_1 v + p_2 (p_2 + 1) - (p_2 + 1)^2/2 = p_1 v + p_2^2/2 - 1/2$. It differs from the +optimal $\mathbf{H} = p_1 v + p_2^2/2$ by a constant, so it has the same Hamiltonian vector +field, and it reaches the target as the optimal law does. This is a property of this +example, not of `:total`. + ```@example main -# :total reproduces the stationary law's trajectory exactly -xf_stationary, _ = Flow(ocp, (x, p) -> p[2])(t0, x0, p0, tf) -xf_total ≈ xf_stationary +@assert isapprox(xf_total, xf; atol=1e-8) # hide +@assert isapprox(xf_partial, [0.5, 1]; atol=1e-8) # hide +nothing # hide ``` -Both are correct for what they compute; picking the wrong one on a non-stationary law silently -integrates different dynamics, no error — which is why it's worth knowing this exists rather -than assuming the two are interchangeable. - ## What comes back -A point call (`f(t0, x0, p0, tf)`) returns `(xf, pf)` (or `(xf, pf, pvf)` with -`variable_costate=true`). A trajectory call — `f((t0, tf), x0, p0)` — returns a real -[`Solution`](@ref results-solution), exactly the same type `solve` returns: +The point call `f(t0, x0, p0, tf)` returns `(xf, pf)`, or `(xf, pf, pvf)` with +`variable_costate=true`. The trajectory call `f((t0, tf), x0, p0)` returns a +[`Solution`](@ref results-solution), as `solve` does: `state`, `costate`, `control`, +`objective` and `plot` work on it, but it has no multipliers (see +[Solutions from a flow](@ref results-solution-flows)). ```@example main sol = f((t0, tf), x0, p0) -typeof(sol) <: OptimalControl.Solution +objective(sol) +``` + +```@example main +@assert sol isa OptimalControl.Solution && isapprox(objective(sol), 6; atol=1e-8) # hide +nothing # hide ``` -Everything in [Solution object](@ref results-solution) and [Plot](@ref results-plot) applies -verbatim — `state(sol)`, `control(sol)`, `objective(sol)`, `plot(sol)`. This is specific to -flows built **from an OCP**; a flow built without one (a bare `Hamiltonian`, a -`ControlledVectorField`, ...) returns its own trajectory wrapper instead — see -[Simulation](@ref flows-simulation) for that distinction. +This is specific to flows built from a problem. A flow built from a Hamiltonian or a vector +field returns its own trajectory type: see [Simulation](@ref flows-simulation). ## See also -- [Accessors](@ref flows-accessors) — the Hamiltonian, its vector - field, and the law you passed in, pulled back out. -- [Shooting](@ref flows-shooting) — the payoff: turn this into a root-finding - problem for $p_0$. -- [No control](@ref modelling-without-control) — the `Flow(ocp)` case in full. +- [Accessors](@ref flows-accessors): the Hamiltonian, its vector field and the law, read back + from a flow. +- [Shooting](@ref flows-shooting): turn a flow into equations for the initial costate. +- [Control-free problems](@ref modelling-without-control): `Flow(ocp)` in full. diff --git a/docs/src/results/solution.md b/docs/src/results/solution.md index 9154cc302..aa1287692 100644 --- a/docs/src/results/solution.md +++ b/docs/src/results/solution.md @@ -373,7 +373,7 @@ nothing model(sol) === ocp ``` -## Solutions from a flow +## [Solutions from a flow](@id results-solution-flows) A [flow](@ref flows-overview) integrated from a problem returns a `Solution` too. Here the flow of the energy problem, with the control u = p₂ given by the maximum principle, from the From 1df71d69a6495dad6c12fb2094e84ee9b38fdf3f Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 15:21:07 +0200 Subject: [PATCH 31/57] docs(from-hamiltonians): one extremal computed six ways, v2.0.4 introduction restored MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restore the v2.0.4 introduction: pseudo-Hamiltonian, maximised Hamiltonian, definition of extremals, with the notation of Formulation. - Every constructor computes the same extremal of the double integrator from p₀ = (12, 6), checked against x(1) = 0, p(1) = (12, −6) (hidden asserts), instead of unrelated toy systems. - A Hamiltonian of (t, x, p) without is_autonomous=false fails with a MethodError: it does not mis-dispatch silently. - SciML flows: the parameter p of f!(dx, x, p, t) is the variable, hence variable=nothing when unused; shown with a p-dependent example. - Drop the typeof dump and the old-keyword demo; the summary table lists what you give and what a point call returns (the old "Returns" column named internal types). - Anchor "Total or partial Hamiltonian" on From an OCP. Co-Authored-By: Claude Opus 5.5 --- docs/src/flows/from-hamiltonians.md | 212 +++++++++++++++++----------- docs/src/flows/from-ocp.md | 2 +- 2 files changed, 132 insertions(+), 82 deletions(-) diff --git a/docs/src/flows/from-hamiltonians.md b/docs/src/flows/from-hamiltonians.md index f5e6e47dd..8ad74bc01 100644 --- a/docs/src/flows/from-hamiltonians.md +++ b/docs/src/flows/from-hamiltonians.md @@ -1,7 +1,9 @@ # [From Hamiltonians](@id flows-from-hamiltonians) -Every constructor below builds a flow **without** an OCP — the building blocks -[From an OCP](@ref flows-from-ocp) is itself assembled from. One section per constructor. +The constructors on this page build a flow without an optimal control problem: from a +Hamiltonian, a Hamiltonian vector field, a pseudo-Hamiltonian and a control law, or a plain +vector field. They are the building blocks [From an OCP](@ref flows-from-ocp) is made of, and +they are useful when you have worked out the Hamiltonian yourself. ```@example main using OptimalControl @@ -9,148 +11,196 @@ using OrdinaryDiffEqTsit5 nothing # hide ``` -## From a vector field +## Extremals + +We take the energy-minimal double integrator again: $\dot q = v$, $\dot v = u$, cost +$\frac12\int u^2$, from $x_0 = (-1, 0)$ to $(0, 0)$ on $[0, 1]$. With the notation of +[Notation and conventions](@ref modelling-formulation-conventions) and $p^0 = -1$, the +pseudo-Hamiltonian is + +```math +H(x, p, u) = p_1 v + p_2 u - \frac{u^2}{2}, +``` + +maximised by $u(x, p) = p_2$. The maximised Hamiltonian is +$\mathbf{H}(x, p) = p_1 v + p_2^2/2$, and the pairs $(x, p)$ solutions of its Hamiltonian +system -The plain ODE case, $\dot x = f(x)$ (or `f(t, x)` if non-autonomous — see below): +```math +\dot x = \nabla_p \mathbf{H}(x, p), \qquad \dot p = -\nabla_x \mathbf{H}(x, p) +``` + +are the **extremals**. From the initial costate $p_0 = (12, 6)$, the extremal reaches the +target: $x(1) = (0, 0)$ and $p(1) = (12, -6)$. Each constructor below computes it from a +different description of the same system. ```@example main -f = Flow(VectorField(x -> -x)) -f(0.0, 1.0, 1.0) # (t0, x0, tf) → x(tf), exp(-1) +t0, tf = 0, 1 +x0 = [-1, 0] +p0 = [12, 6] +nothing # hide ``` ## From a Hamiltonian -$\vec H = (\partial_p H, -\partial_x H)$ is obtained by automatic differentiation: +Give $\mathbf{H}(x, p)$; its Hamiltonian vector field +$\vec{\mathbf{H}} = (\nabla_p \mathbf{H}, -\nabla_x \mathbf{H})$ is computed by automatic +differentiation: ```@example main -h(x, p) = 0.5 * (x^2 + p^2) -fh = Flow(Hamiltonian(h)) -fh(0.0, 1.0, 0.0, 1.0) # (t0, x0, p0, tf) → (x(tf), p(tf)) +H(x, p) = p[1] * x[2] + p[2]^2 / 2 + +f_h = Flow(Hamiltonian(H)) +xf, pf = f_h(t0, x0, p0, tf) ``` -`Hamiltonian` defaults to the natural 2-arg autonomous form `h(x, p)`. A 3-arg -`h(t, x, p)` silently mis-dispatches unless declared explicitly non-autonomous — see below. +```@example main +@assert isapprox(xf, [0, 0]; atol=1e-8) && isapprox(pf, [12, -6]; atol=1e-8) # hide +nothing # hide +``` + +A Hamiltonian is `H(x, p)` by default. If it depends on time, write `H(t, x, p)` and declare +it with `Hamiltonian(H; is_autonomous=false)`; if it depends on a variable, `H(x, p, v)` +with `is_variable=true`. Without the declaration, the call fails with a `MethodError`. ## From a Hamiltonian vector field -Same system, but $\vec H$ given directly — no AD: +Give $\vec{\mathbf{H}}$ directly, as a function returning the pair $(\dot x, \dot p)$. No +automatic differentiation is involved: ```@example main -hvf(x, p) = (p, -x) -fhvf = Flow(HamiltonianVectorField(hvf)) -fhvf(0.0, 1.0, 0.0, 1.0) +Hv(x, p) = ([x[2], p[2]], [0, -p[1]]) + +f_hv = Flow(HamiltonianVectorField(Hv)) +f_hv(t0, x0, p0, tf) ``` ## From a pseudo-Hamiltonian and a control law -$\tilde H(x,p,u)$ plus a law $u^*(x,p)$ — `Flow` substitutes and differentiates through it -(AD), same as [From an OCP](@ref flows-from-ocp)'s default `hamiltonian_type=:total`: +Give $H(x, p, u)$ and the law $u(x, p)$: the flow is the one of +$\bar H(x, p) = H(x, p, u(x, p))$, differentiated through the law by automatic +differentiation. This is what [From an OCP](@ref flows-from-ocp) does, with the +pseudo-Hamiltonian read from the problem. ```@example main -htilde(x, p, u) = p * u - 0.5 * u^2 -law(x, p) = p # the maximiser of h̃ above -fph = Flow(PseudoHamiltonian(htilde), DynClosedLoop(law)) -fph(0.0, 1.0, 0.0, 1.0) +Hp(x, p, u) = p[1] * x[2] + p[2] * u - u^2 / 2 +law(x, p) = p[2] + +f_hp = Flow(PseudoHamiltonian(Hp), DynClosedLoop(law)) +f_hp(t0, x0, p0, tf) ``` +`hamiltonian_type=:partial` freezes the control instead of differentiating through the law +(see [Total or partial Hamiltonian](@ref flows-from-ocp-total-partial)). + ## From a pseudo-Hamiltonian vector field and a control law -Same idea, vector-field form: $\tilde{\vec H}(x,p,u) = (\partial_p\tilde H, -\partial_x\tilde H)$ -given directly. No `hamiltonian_type=` here — there's no pseudo-Hamiltonian scalar to -differentiate two different ways, so passing it is rejected: +Give $(\nabla_p H, -\nabla_x H)$ as a function of $(x, p, u)$, and the law: ```@example main -htildevf(x, p, u) = (u, 0.0) -fphvf = Flow(PseudoHamiltonianVectorField(htildevf), DynClosedLoop(law)) -fphvf(0.0, 1.0, 0.0, 1.0) -``` +Hpv(x, p, u) = ([x[2], u], [0, -p[1]]) -```@repl main -try # hide -Flow( - PseudoHamiltonianVectorField(htildevf), - DynClosedLoop(law); - hamiltonian_type=:partial, -) -catch e # hide -showerror(IOContext(stdout, :color => false), e) # hide -end # hide +f_hpv = Flow(PseudoHamiltonianVectorField(Hpv), DynClosedLoop(law)) +f_hpv(t0, x0, p0, tf) ``` -## From a SciML problem - -An `ODEFunction`/`ODEProblem` from the SciML ecosystem works directly — reachable through -`OrdinaryDiffEqTsit5` alone, nothing extra to load: - ```@example main -rhs!(dx, x, p, t) = (dx[1] = -x[1]; nothing) -f_fun = Flow(ODEFunction(rhs!)) -nothing # hide +for g in (f_hv, f_hp, f_hpv) # hide + xg, pg = g(t0, x0, p0, tf) # hide + @assert isapprox(xg, [0, 0]; atol=1e-8) && isapprox(pg, [12, -6]; atol=1e-8) # hide +end # hide +nothing # hide ``` -SciML-backed flows are always `NonFixed` — even with no real free variable, a call needs -`variable=nothing`: +Here the partial derivatives are given, so there is nothing to differentiate through the +law, and `hamiltonian_type` is not an option of this flow. Passing it raises an error, which +reads as an unknown integrator option: ```@repl main try # hide -f_fun(0.0, [1.0], 1.0) +Flow(PseudoHamiltonianVectorField(Hpv), DynClosedLoop(law); hamiltonian_type=:partial) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` +## From a vector field + +A flow of the state alone, $\dot x = f(x)$, or $f(t, x)$ when declared with +`is_autonomous=false`. With the optimal control $u(t) = 6 - 12t$ written as a function of +time, the state reaches the target: + ```@example main -f_fun(0.0, [1.0], 1.0; variable=nothing) +f_x = Flow(VectorField((t, x) -> [x[2], 6 - 12t]; is_autonomous=false)) +f_x(t0, x0, tf) ``` ```@example main -prob = ODEProblem(rhs!, [1.0], (0.0, 1.0)) -f_prob = Flow(prob) -typeof(f_prob) +@assert isapprox(f_x(t0, x0, tf), [0, 0]; atol=1e-8) # hide +nothing # hide ``` -## Non-autonomous and variable-dependent forms +To simulate a control system under a given control, open-loop or feedback, see +[Simulation](@ref flows-simulation). -`is_autonomous=` and `is_variable=` (not `autonomous=`, an old spelling that no longer -exists) declare the extra arguments a function needs: +## From a SciML problem + +An `ODEFunction` or an `ODEProblem` of the SciML ecosystem can be used directly, with +`OrdinaryDiffEqTsit5` alone. The parameter `p` of the SciML function `f!(dx, x, p, t)` is the +**variable** of the flow, passed with the keyword `variable`. Here $\dot x = -p\,x$: + +```@example main +rhs!(dx, x, p, t) = (dx[1] = -p * x[1]; nothing) + +f_ode = Flow(ODEFunction(rhs!)) +f_ode(0, [1.0], 1; variable=2) # exp(-2) +``` ```@example main -g(t, x) = -t * x -fg = Flow(VectorField(g; is_autonomous=false)) -fg(0.0, 1.0, 1.0) +@assert isapprox(f_ode(0, [1.0], 1; variable=2)[1], exp(-2); atol=1e-8) # hide +nothing # hide ``` -The stale form fails two ways at once — there is no `Flow(::Function)` at all, and even fixed -to the right type, the keyword itself changed name: +So the keyword is required even when the function does not use `p`: pass +`variable=nothing`. Without it, the call raises an error: ```@repl main try # hide -VectorField(g; autonomous=false) +f_ode(0, [1.0], 1) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -`is_inplace=` similarly declares an in-place (mutating, `f!(dx, x)`) vs out-of-place -(`f(x) -> dx`) function — inferred automatically in most cases, settable explicitly when it -isn't. +An `ODEProblem` works the same way: + +```@example main +f_prob = Flow(ODEProblem(rhs!, [1.0], (0.0, 1.0), 2.0)) +f_prob(0, [1.0], 1; variable=2) +``` + +## Summary -## Summary table +| Constructor | You give | Automatic differentiation | Point call returns | +| --- | --- | --- | --- | +| `Flow(Hamiltonian(H))` | $\mathbf{H}(x, p)$ | yes | `(x(tf), p(tf))` | +| `Flow(HamiltonianVectorField(Hv))` | $(\nabla_p \mathbf{H}, -\nabla_x \mathbf{H})$ | no | `(x(tf), p(tf))` | +| `Flow(PseudoHamiltonian(Hp), law)` | $H(x, p, u)$ and $u(x, p)$ | yes | `(x(tf), p(tf))` | +| `Flow(PseudoHamiltonianVectorField(Hpv), law)` | $(\nabla_p H, -\nabla_x H)$ and $u(x, p)$ | no | `(x(tf), p(tf))` | +| `Flow(VectorField(f))` | $f(x)$ | no | `x(tf)` | +| `Flow(ODEFunction(f!))`, `Flow(ODEProblem(…))` | a SciML function or problem | no | `x(tf)` | -| Constructor | Uses AD? | Returns | -| --- | --- | --- | -| `Flow(VectorField(f))` | no | `StateFlow` | -| `Flow(Hamiltonian(h))` | yes | `HamiltonianFlow` | -| `Flow(HamiltonianVectorField(hvf))` | no | `HamiltonianFlow` | -| `Flow(PseudoHamiltonian(h̃), law)` | yes | `HamiltonianFlow` | -| `Flow(PseudoHamiltonianVectorField(h̃vf), law)` | no | `HamiltonianFlow` | -| `Flow(ODEFunction(...))` / `Flow(ODEProblem(...))` | no | `StateFlow` / `SciMLProblemFlow` | +`Hamiltonian`, `HamiltonianVectorField`, `PseudoHamiltonian`, `PseudoHamiltonianVectorField` +and `VectorField` take the keywords `is_autonomous=false` and `is_variable=true` for a +function of time or of a variable. Every `Flow` takes the integrator options of +[Flows overview](@ref flows-overview). Called on a time +span, `f((t0, tf), x0, p0)` returns a trajectory: see [Simulation](@ref flows-simulation). ## See also -- [From an OCP](@ref flows-from-ocp) — the same constructors, specialised: an OCP supplies - $\tilde H$ automatically, you only supply the law. -- [Accessors](@ref flows-accessors) — which accessor works on which of - the flows built here. -- [Lift and Poisson brackets](@ref geometry-lift) — the differential-geometric layer these - constructors sit on top of. +- [From an OCP](@ref flows-from-ocp): the same flows, with the problem supplying the + pseudo-Hamiltonian; you only write the law. +- [Accessors](@ref flows-accessors): read the Hamiltonian, its vector field and the law back + from a flow. +- [Lift](@ref geometry-lift): the Hamiltonian lift of a vector field, and the other tools of + the geometry section. diff --git a/docs/src/flows/from-ocp.md b/docs/src/flows/from-ocp.md index 2c378c727..38d4e3813 100644 --- a/docs/src/flows/from-ocp.md +++ b/docs/src/flows/from-ocp.md @@ -284,7 +284,7 @@ showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -## Total or partial Hamiltonian +## [Total or partial Hamiltonian](@id flows-from-ocp-total-partial) The keyword `hamiltonian_type` chooses how the flow is built from $H$ and the law $u(x, p)$: From 215f739cb7459290d3391ba000440f835d7d671b Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 15:25:26 +0200 Subject: [PATCH 32/57] docs(simulation): analytic checks, a control evaluated on a problem, what each flow returns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Open and closed loop on ẋ = −x + u, checked against 1 − e⁻¹ and e^{−1/2}. - Evaluate controls on the energy problem: the optimal u = 6 − 12t reaches the target with cost 6, the constant u = 1 costs 1/2 and misses it (asserts); boundary conditions are not enforced by a simulation. - The OpenLoop(() -> 1) trap, shorter, linked to CTBase#570. - A table of what each flow returns on a time span (Solution, state trajectory with or without objective, state and costate trajectory), each row checked by hidden asserts. - Drop the unused NLPModelsIpopt and the internal type name. - Solution and From an OCP: only a flow built from a problem with a law of (x, p) returns a Solution; link the table. Co-Authored-By: Claude Opus 5.5 --- docs/src/flows/from-ocp.md | 5 +- docs/src/flows/simulation.md | 171 ++++++++++++++++++++++------------- docs/src/results/solution.md | 7 +- 3 files changed, 114 insertions(+), 69 deletions(-) diff --git a/docs/src/flows/from-ocp.md b/docs/src/flows/from-ocp.md index 38d4e3813..3df068971 100644 --- a/docs/src/flows/from-ocp.md +++ b/docs/src/flows/from-ocp.md @@ -344,8 +344,9 @@ objective(sol) nothing # hide ``` -This is specific to flows built from a problem. A flow built from a Hamiltonian or a vector -field returns its own trajectory type: see [Simulation](@ref flows-simulation). +This is specific to flows built from a problem with a law of the state and the costate. The +other flows, such as `Flow(ocp, OpenLoop(…))` or `Flow(Hamiltonian(H))`, return their own +trajectories: see [What a flow returns](@ref flows-simulation-returns). ## See also diff --git a/docs/src/flows/simulation.md b/docs/src/flows/simulation.md index 420b6bb09..786bf9ce2 100644 --- a/docs/src/flows/simulation.md +++ b/docs/src/flows/simulation.md @@ -1,74 +1,87 @@ # [Simulation](@id flows-simulation) -Sometimes you don't want an optimum — you have a controlled system and a specific control -(open-loop or feedback), and you want the trajectory it produces. +Sometimes you do not look for an optimum: you have a control system and a given control, +open-loop or feedback, and you want the trajectory it produces, or the cost of that control +for a problem. ```@example main using OptimalControl using OrdinaryDiffEqTsit5 -using NLPModelsIpopt using Plots nothing # hide ``` ## The idea -A controlled vector field $\dot x = f(x, u)$ plus a control — given as a function of time -(open loop) or of state (feedback) — is enough to integrate a trajectory. No cost, no PMP, no -`@def`: just `ControlledVectorField` and a law. +A controlled vector field $\dot x = f(x, u)$ and a control are enough to integrate a +trajectory. The control is given as a function of time (open loop) or of the state +(feedback, closed loop). No cost, no costate: the vector field is a +`ControlledVectorField`, and the control a law `OpenLoop` or `ClosedLoop`. ## Open loop +Take $\dot x = -x + u$ with the constant control $u(t) = 1$. From $x(0) = 0$, the solution is +$x(t) = 1 - e^{-t}$: + ```@example main fc(x, u) = -x + u -f_ol = Flow(ControlledVectorField(fc), OpenLoop(t -> 1.0)) -traj_ol = f_ol((0.0, 1.0), 1.0) -state(traj_ol)(0.5) +f_ol = Flow(ControlledVectorField(fc), OpenLoop(t -> 1)) +f_ol(0, 0.0, 1) # 1 - exp(-1) +``` + +```@example main +@assert isapprox(f_ol(0, 0.0, 1), 1 - exp(-1); atol=1e-8) # hide +nothing # hide ``` -!!! warning "`OpenLoop` is unconditionally non-autonomous" +An open-loop law is always a function of time, `u(t)`, or `u(t, v)` with a variable. A +function without argument, such as `OpenLoop(() -> 1)`, is accepted when the law is built, +but the call then fails with a `MethodError` +([CTBase#570](https://github.com/control-toolbox/CTBase.jl/issues/570)): - The law must be `u(t)` (or `u(t, v)` with a variable) — always a function of time, never a - bare constant closure. `OpenLoop(() -> 1.0)` **builds without error** but fails on the - first call, with a bare `MethodError` far from the actual mistake: +```@repl main +try # hide +Flow(ControlledVectorField(fc), OpenLoop(() -> 1))(0, 0.0, 1) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` - ```@repl main - bad_law = OpenLoop(() -> 1.0); # constructs fine — the trap - try # hide - Flow(ControlledVectorField(fc), bad_law)(0.0, 1.0, 1.0) - catch e # hide - showerror(IOContext(stdout, :color => false), e) # hide - end # hide - ``` +## Closed loop - `OpenLoop(t -> 1.0)` above is the only correct spelling — there is no "autonomous open - loop" any more. See [Migration](@ref migration) for the full list of silent v2.0 → v2.1 - semantics changes. +A feedback is a function of the state, `u(x)`. With $u = x/2$, the system becomes +$\dot x = -x/2$, so from $x(0) = 1$, $x(1) = e^{-1/2}$: -## Closed loop (feedback) +```@example main +f_cl = Flow(ControlledVectorField(fc), ClosedLoop(x -> x / 2)) +f_cl(0, 1.0, 1) # exp(-1/2) +``` ```@example main -f_cl = Flow(ControlledVectorField(fc), ClosedLoop(x -> 0.5x)) -f_cl(0.0, 1.0, 1.0) +@assert isapprox(f_cl(0, 1.0, 1), exp(-1 / 2); atol=1e-8) # hide +nothing # hide ``` -`DynClosedLoop` (the costate-carrying law kind) is rejected on a `ControlledVectorField` — it -has no costate to carry: +A law of the state and the costate, `DynClosedLoop`, is rejected here: the flow of a +controlled vector field has no costate. ```@repl main try # hide -Flow(ControlledVectorField(fc), DynClosedLoop((x, p) -> x)) +Flow(ControlledVectorField(fc), DynClosedLoop((x, p) -> p)) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -## From an optimal control problem +## Evaluating a control on a problem + +Built from an optimal control problem, the simulation also computes the cost of the control. +On the energy-minimal double integrator, the optimal control is $u(t) = 6 - 12t$, with +cost $6$ (see [First problem](@ref getting-started-first-problem)): ```@example main -t0 = 0 -tf = 1 +t0, tf = 0, 1 x0 = [-1, 0] ocp = @def begin @@ -81,58 +94,88 @@ ocp = @def begin 0.5∫(u(t)^2) → min end -f_sim = Flow(ocp, OpenLoop(t -> 1.0)) -traj_ocp = f_sim((t0, tf), x0) -nothing # hide +f_opt = Flow(ocp, OpenLoop(t -> 6 - 12t)) +traj = f_opt((t0, tf), x0) +state(traj)(tf), objective(traj) +``` + +The simulation reaches the target with the optimal cost. Another control costs less, but +misses the target: the constant $u = 1$ costs $1/2$ and ends at $x(1) = (-1/2, 1)$. + +```@example main +traj_1 = Flow(ocp, OpenLoop(t -> 1))((t0, tf), x0) +state(traj_1)(tf), objective(traj_1) +``` + +```@example main +@assert isapprox(state(traj)(tf), [0, 0]; atol=1e-8) && isapprox(objective(traj), 6; atol=1e-8) # hide +@assert isapprox(state(traj_1)(tf), [-0.5, 1]; atol=1e-8) && isapprox(objective(traj_1), 0.5; atol=1e-8) # hide +nothing # hide ``` -`Flow(ocp, OpenLoop(u))` returns a `ControlledFlow` whose trajectory **carries the objective** -— you can evaluate a candidate control's cost directly: +The boundary conditions of the problem are not enforced: the simulation only integrates the +dynamics and the cost from $x_0$. A feedback works the same way, with +`Flow(ocp, ClosedLoop(x -> …))`. + +## Reading and plotting the trajectory + +The trajectory is read with the same functions as a [`Solution`](@ref results-solution): +`state`, `control`, `time_grid`, and `objective` when the flow comes from a problem. + +```@example main +state(traj)(0.5), control(traj)(0.5) +``` ```@example main -objective(traj_ocp) +plot(traj) ``` -A flow built without an OCP has no cost to report — calling `objective` on it is a -`PreconditionError`, not a silent `0.0` or `NaN`: +There is no costate: `costate(traj)` raises an error, and so does `objective` on a flow built +without a problem. ```@repl main try # hide -objective(traj_ol) +objective(f_ol((0, 1), 0.0)) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -## Inspecting the trajectory +## [What a flow returns](@id flows-simulation-returns) -Same accessors as a `solve`-returned [`Solution`](@ref results-solution): - -```@example main -state(traj_ocp)(0.5), control(traj_ocp)(0.5), time_grid(traj_ocp) -``` +Every flow is called in two ways: `f(t0, x0, tf)` returns the final state (with `p0`, the +final state and costate), and `f((t0, tf), x0)` returns the trajectory. The trajectory +depends on how the flow was built: -## Plotting it +| Flow | Trajectory | Costate | Objective | +| --- | --- | :-: | :-: | +| `Flow(ocp, law)`, with a law `u(x, p)` ([From an OCP](@ref flows-from-ocp)) | a [`Solution`](@ref results-solution) | ✓ | ✓ | +| `Flow(ocp, OpenLoop(…))`, `Flow(ocp, ClosedLoop(…))` | a trajectory of the state | ✗ | ✓ | +| `Flow(ControlledVectorField(f), law)`, `Flow(VectorField(f))` | a trajectory of the state | ✗ | ✗ | +| `Flow(Hamiltonian(H))` and the other [Hamiltonian constructors](@ref flows-from-hamiltonians) | a trajectory of the state and costate | ✓ | ✗ | ```@example main -plot(traj_ocp) +raises(f) = try f(); false catch; true end # hide +sol_h = Flow(ocp, (x, p) -> p[2])((t0, tf), x0, [12, 6]) # hide +@assert sol_h isa OptimalControl.Solution && !raises(() -> costate(sol_h)) # hide +@assert !(traj isa OptimalControl.Solution) && raises(() -> costate(traj)) # hide +traj_x = f_ol((0, 1), 0.0) # hide +@assert raises(() -> costate(traj_x)) && raises(() -> objective(traj_x)) # hide +traj_v = Flow(VectorField(x -> -x))((0, 1), 1.0) # hide +@assert raises(() -> objective(traj_v)) # hide +traj_h = Flow(Hamiltonian((x, p) -> p[1] * x[2] + p[2]^2 / 2))((t0, tf), x0, [12, 6]) # hide +@assert !raises(() -> costate(traj_h)(0.5)) && raises(() -> objective(traj_h)) # hide +nothing # hide ``` -See [Plot](@ref results-plot) for every keyword. - -## Point vs trajectory - -`f(t0, x0, tf)` returns the endpoint only; `f((t0, tf), x0)` returns the full trajectory — same -convention as every other flow in this section. - -The trajectory's own type — `CTFlows.Trajectories.StateFlowTrajectory` — is not re-exported; -never name it in your own code, only call the generic accessors on it. +All of them are read with `state`, `costate` (when there is one), `time_grid` and `plot`. The +types of the trajectories are internal: call these functions on them, without naming the +types. ## See also -- [Solution object](@ref results-solution) and [Plot](@ref results-plot) — the accessors used +- [Solution object](@ref results-solution) and [Plot](@ref results-plot): the functions used above, in full. -- [From an OCP](@ref flows-from-ocp) — the indirect-solving counterpart, where the law comes - from the PMP instead of being handed to you. -- [No control](@ref modelling-without-control) — control-free problems, a - related but distinct case. +- [From an OCP](@ref flows-from-ocp): the indirect method, where the law comes from the + maximum principle. +- [Control-free problems](@ref modelling-without-control): problems without a control at all. diff --git a/docs/src/results/solution.md b/docs/src/results/solution.md index aa1287692..27ca52a02 100644 --- a/docs/src/results/solution.md +++ b/docs/src/results/solution.md @@ -375,9 +375,10 @@ model(sol) === ocp ## [Solutions from a flow](@id results-solution-flows) -A [flow](@ref flows-overview) integrated from a problem returns a `Solution` too. Here the -flow of the energy problem, with the control u = p₂ given by the maximum principle, from the -initial costate p(0) = (12, 6): +The Hamiltonian [flow](@ref flows-overview) of a problem, built with a control law of the +state and the costate, returns a `Solution` too (other flows return their own trajectories, +see [Simulation](@ref flows-simulation)). Here the flow of the energy problem, with the +control u = p₂ given by the maximum principle, from the initial costate p(0) = (12, 6): ```@example main using OrdinaryDiffEqTsit5 From 02d7c8a41f8c893f0549cb68ed4d3ddfa9d3e53c Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 15:28:22 +0200 Subject: [PATCH 33/57] docs(accessors): evaluate every accessor on the extremal, honest availability table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - One flow (the energy problem with u = p₂) and one point (x, p) = ((−1, 0), (12, 6)): H = 18, H⃗ = ((0, 6), (0, −12)), u = 6, ∇H = ((0, 12), (0, 6)), checked by asserts, instead of printing the struct-wrapped callables. - The short and full signatures of what the accessors return; gradients only take (t, x, p, v) (CTFlows#438). - The availability table rebuilt from measurements, vector_field included; the MethodError/IncorrectArgument split is reported upstream (CTFlows#438) instead of documented as a feature. - Drop the unused NLPModelsIpopt, the meta remark, and CTFlows.Flows.system. Co-Authored-By: Claude Opus 5.5 --- docs/src/flows/accessors.md | 148 ++++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 64 deletions(-) diff --git a/docs/src/flows/accessors.md b/docs/src/flows/accessors.md index e5bd808b8..67be539cb 100644 --- a/docs/src/flows/accessors.md +++ b/docs/src/flows/accessors.md @@ -1,132 +1,152 @@ # [Accessors](@id flows-accessors) -Building a flow doesn't throw away what it was built from — a flow remembers its Hamiltonian, -its vector field, the control law you passed in, and the underlying integrator. This page is -the map of what you can pull back out, and from which kind of flow. +A flow keeps what it was built from: its Hamiltonian, its vector field, the +pseudo-Hamiltonian and the control law, and its integrator. This page shows how to read them +back, and on which flows. ```@example main using OptimalControl using OrdinaryDiffEqTsit5 -using NLPModelsIpopt nothing # hide ``` -## What a flow remembers +We take the flow of the energy-minimal double integrator, built from the problem and the +control law $u = p_2$ (see [From an OCP](@ref flows-from-ocp)), and the point +$x = (-1, 0)$, $p = (12, 6)$: the start of the optimal extremal. -Every flow wraps a **system** (the mathematical object — a Hamiltonian, a vector field, a -pseudo-Hamiltonian plus a law...) and an **integrator**. The four accessors below read off the -system; the last section reaches the integrator directly. +```@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] + ẋ(t) == [v(t), u(t)] + 0.5∫(u(t)^2) → min +end + +f = Flow(ocp, (x, p) -> p[2]) +x, p = [-1, 0], [12, 6] +nothing # hide +``` ## The Hamiltonian +`hamiltonian(f)` returns the maximised Hamiltonian +$\mathbf{H}(x, p) = H(x, p, u(x, p)) = p_1 v + p_2^2/2$: + ```@example main -h(x, p) = 0.5 * (x^2 + p^2) -f = Flow(Hamiltonian(h)) H = hamiltonian(f) -H(0.0, 1.0, 0.0, 1.0) # H(t, x, p, v) +H(x, p) # 12 × 0 + 6²/2 ``` ## The Hamiltonian vector field +`hamiltonian_vector_field(f)` returns +$\vec{\mathbf{H}} = (\nabla_p \mathbf{H}, -\nabla_x \mathbf{H})$, as the pair +$(\dot x, \dot p) = ((v, p_2), (0, -p_1))$: + ```@example main -hamiltonian_vector_field(f) +Hv = hamiltonian_vector_field(f) +Hv(x, p) ``` -Returns a [`HamiltonianVectorField`](@ref) — $(\partial_p H, -\partial_x H)$ — whether or not -the flow was built with AD. - ## The pseudo-Hamiltonian and the control law -Only available on flows built from a pseudo-Hamiltonian (or an OCP, which is one under the -hood) plus a law: +`pseudo_hamiltonian(f)` returns $H(x, p, u) = p_1 v + p_2 u - u^2/2$, and `control_law(f)` +the law you passed: ```@example main -htilde(x, p, u) = p * u - 0.5 * u^2 -law_fun(x, p) = p -f_ph = Flow(PseudoHamiltonian(htilde), DynClosedLoop(law_fun)) - -H̃ = pseudo_hamiltonian(f_ph) -H̃(0.0, 1.0, 0.0, 1.0, 1.0) # H̃(t, x, p, u, v) +Hp = pseudo_hamiltonian(f) +u = control_law(f) +Hp(x, p, u(x, p)), u(x, p) ``` ```@example main -control_law(f_ph)(1.0, 0.5) # the law you passed in, u(x, p) +@assert H(x, p) == 18 && Hp(x, p, 6) == 18 && u(x, p) == 6 # hide +@assert all(Hv(x, p) .≈ ([0, 6], [0, -12])) # hide +@assert H(0.0, x, p, Float64[]) == 18 && u(0.0, x, p, Float64[]) == 6 # hide +nothing # hide ``` +These functions take the short form of their arguments, as above, or the full form with the +time and the variable: `H(t, x, p, v)`, `Hv(t, x, p, v)`, `Hp(t, x, p, u, v)`, +`u(t, x, p, v)`, with `v = Float64[]` for a problem without a variable. + ## Gradients -Four functions, each returning a struct-wrapped callable (not a bare function or a vector) — -expect `(t, x, p, v)`-shaped arguments when calling what they return: +`get_hamiltonian_gradient(f)` returns the gradient of $\mathbf{H}$ with respect to $x$ and +$p$, and `get_variable_gradient(f)` its gradient with respect to the variable. +`get_pseudo_hamiltonian_gradient` and `get_pseudo_variable_gradient` do the same for $H$, +before the law is substituted. They only take the full form `(t, x, p, v)` +([CTFlows#438](https://github.com/control-toolbox/CTFlows.jl/issues/438)): ```@example main -get_hamiltonian_gradient(f_ph) +∇H = get_hamiltonian_gradient(f) +∇H(0.0, x, p, Float64[]) # (∇ₓH, ∇ₚH) = ((0, 12), (0, 6)) ``` ```@example main -get_variable_gradient(f_ph) +@assert all(∇H(0.0, x, p, Float64[]) .≈ ([0, 12], [0, 6])) # hide +nothing # hide ``` -`get_pseudo_hamiltonian_gradient(f_ph)` and `get_pseudo_variable_gradient(f_ph)` mirror the -two above, taken on $\tilde H$ before the law is substituted in rather than on the composed $H$. - -## Building the Hamiltonian vector field from a Hamiltonian, without a flow +## Without a flow -`hamiltonian_vector_field` also works directly on an `AbstractHamiltonian`, no `Flow` needed: +`hamiltonian_vector_field` also applies to a `Hamiltonian`, without building a flow: ```@example main -hamiltonian_vector_field(Hamiltonian(h)) +Hv2 = hamiltonian_vector_field(Hamiltonian((x, p) -> p[1] * x[2] + p[2]^2 / 2)) +Hv2(x, p) ``` -## What is available on which flow +## On which flow -Not every accessor makes sense on every flow — and the failure mode differs by *why* it -doesn't apply, confirmed live rather than assumed uniform: +What a flow can give back depends on what it was built from: -| Built from | `hamiltonian` | `hamiltonian_vector_field` | `pseudo_hamiltonian` | `control_law` | -| --- | --- | --- | --- | --- | -| `Hamiltonian(h)` | ✅ | ✅ | ✗ `IncorrectArgument` | ✗ `IncorrectArgument` | -| `HamiltonianVectorField(hvf)` | ✗ `IncorrectArgument` | ✅ | ✗ `MethodError` | ✗ `MethodError` | -| `PseudoHamiltonian(h̃), law` | ✅ | ✅ | ✅ | ✅ | -| `ocp, law` | ✅ | ✅ | ✅ | ✅ | -| `VectorField(f)` | ✗ `MethodError` | ✗ `MethodError` (use `vector_field`) | ✗ `MethodError` | ✗ `MethodError` | +| Flow built from | `hamiltonian` | `hamiltonian_vector_field` | `pseudo_hamiltonian`, `control_law` | `vector_field` | +| --- | :-: | :-: | :-: | :-: | +| `ocp, law` with a law `u(x, p)`, or `PseudoHamiltonian(Hp), law` | ✓ | ✓ | ✓ | ✓ | +| `Hamiltonian(H)` | ✓ | ✓ | ✗ | ✓ | +| `HamiltonianVectorField(Hv)` | ✗ | ✓ | ✗ | ✓ | +| `VectorField(f)`, `ControlledVectorField(f), law`, `ocp, OpenLoop(…)` | ✗ | ✗ | ✗ | ✓ | -`IncorrectArgument` shows up where the flow *could* answer in principle but deliberately -doesn't have enough information (a `HamiltonianVectorField`-built flow has no scalar -Hamiltonian to hand back, only its vector field — the error says so and suggests -`hamiltonian_vector_field` instead). `MethodError` shows up where the accessor simply has no -method for that system type at all — a `VectorField`-built flow was never given anything -Hamiltonian-shaped, so none of the four Hamiltonian-side accessors apply; use -`vector_field` instead: +On a flow of the state, `vector_field` returns the vector field that is integrated: ```@example main -vector_field(Flow(VectorField(x -> -x))) +Fx = vector_field(Flow(VectorField(x -> -x))) +Fx(2.0) ``` +An accessor that does not apply raises an error. For a Hamiltonian flow, the error says what +the flow holds and what to use instead: + ```@repl main try # hide -hamiltonian(Flow(HamiltonianVectorField((x, p) -> (p, -x)))) +hamiltonian(Flow(HamiltonianVectorField((x, p) -> ([x[2], p[2]], [0, -p[1]])))) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -## The underlying system and integrator +On a flow of the state, it is a `MethodError` for now +([CTFlows#438](https://github.com/control-toolbox/CTFlows.jl/issues/438)). -Escape hatch, for when nothing above is specific enough — `system`/`integrator` stay -deliberately unexported (too generic a name for a DSL surface), reach them qualified: +## The system and the integrator -```@example main -CTFlows.Flows.system(f) -``` +A flow is made of a **system**, the mathematical object it integrates, and an +**integrator**. Both are reached with qualified names, since they are not exported: ```@example main CTFlows.Flows.integrator(f) ``` +The options of the integrator are those of [Flows overview](@ref flows-overview), given when +the flow is built. + ## See also -- [From an OCP](@ref flows-from-ocp) — the most common source of a flow with all four - accessors available. -- [From Hamiltonians](@ref flows-from-hamiltonians) — every constructor in - the table above, built and shown in full. -- [Geometry](@ref geometry-overview) — the Lie-theoretic tools these systems are built on. +- [From an OCP](@ref flows-from-ocp): the flow used on this page. +- [From Hamiltonians](@ref flows-from-hamiltonians): every constructor in the table above. +- [Geometry overview](@ref geometry-overview): Lie derivatives and brackets of these + Hamiltonians and vector fields. From 24078900b9e216b21cbdd3d4a69f0bda1af54286 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 15:32:44 +0200 Subject: [PATCH 34/57] =?UTF-8?q?docs(multi-phase):=20B2,=20Hamiltonian=20?= =?UTF-8?q?jumps=20stated=20correctly,=20the=20v2.0.4=20off=E2=80=93bang?= =?UTF-8?q?=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - B2: the point call of a concatenated Hamiltonian flow returns one vector [x; p]; the bang–bang example uses zf[1:2], zf[3:4] (CTFlows#439), with its analytic derivation (t₁ = 1, t_f = 2, p(0) = (1, 1) from transversality) and asserts. - On a Hamiltonian flow, the 3-argument vector jump is a costate jump (the state stays continuous), shown and checked on the extremal; the function form maps (x, p). - Restore the v2.0.4 L¹ example (off arc then bang, p₀ = 1/(x₀ − (x_f − 1)e^{t_f}), t₁ = −ln p₀), checked: x(1) = 0, J = t_f − t₁. - Drop the typeof dumps; the v2.0.4 "concatenation is not exact" comparison is not restored: the two computations now agree. Co-Authored-By: Claude Opus 5.5 --- docs/src/flows/multi-phase.md | 205 +++++++++++++++++++++------------- 1 file changed, 130 insertions(+), 75 deletions(-) diff --git a/docs/src/flows/multi-phase.md b/docs/src/flows/multi-phase.md index 73bd2e02a..e7c2e4b52 100644 --- a/docs/src/flows/multi-phase.md +++ b/docs/src/flows/multi-phase.md @@ -1,101 +1,100 @@ # [Multi-phase flows](@id flows-multi-phase) -Bang-bang switchings, jumps at a boundary arc's entry/exit, phase changes of any kind: a -sequence of flows, each active on its own sub-interval, concatenated into one callable object — -itself a flow. +An optimal control is often made of several arcs: bang arcs, off arcs, singular or boundary +arcs, with possibly a jump of the costate at a junction. Each arc has its own flow, and the +flows are concatenated, at the switching times, into one flow. ```@example main using OptimalControl using OrdinaryDiffEqTsit5 +using Plots nothing # hide ``` ## Concatenating two flows -`*` glues two flows together at a switching time — the result is itself a flow: +`f1 * (t1, f2)` follows `f1` until `t1`, then `f2`. The result is itself a flow: ```@example main -f1 = Flow(VectorField(x -> 0.0)) # constant -f2 = Flow(VectorField(x -> 1.0)) # unit slope +f1 = Flow(VectorField(x -> 0)) # ẋ = 0 +f2 = Flow(VectorField(x -> 1)) # ẋ = 1 -f = f1 * (1.0, f2) # f1 until t=1, then f2 -typeof(f) +f = f1 * (1, f2) +f(0, 0.0, 2) # constant until t = 1, then one unit of slope 1 ``` -```@example main -f(0.0, 0.0, 2.0) # one unit of f1's flat, then one of f2's slope-1 -``` +More arcs are added the same way: `f1 * (t1, f2) * (t2, f3)`. ## Jumps -A jump inserts a discontinuity in the state at the switching time — `f1 * (t1, jump, f2)`, -where `jump` is a plain function of the state: - -```@example main -jump(x) = x + 10.0 -fj = f1 * (1.0, jump, f2) -fj(0.0, 0.0, 2.0) # +10 landed at t=1, then one more unit of slope -``` - -On a **Hamiltonian** flow, the jump can act on state and costate separately — a 4-arg form, -`h1 * (t, jump_x, jump_p, h2)`: +A jump at the switching time is given between the time and the second flow. On a flow of the +state, `f1 * (t1, j, f2)` applies `x ← j(x)`, or `x ← x + j` when `j` is a vector: ```@example main -htilde(x, p, u) = p * u - 0.5 * u^2 -law(x, p) = p -h1 = Flow(PseudoHamiltonian(htilde), DynClosedLoop(law)) -h2 = Flow(PseudoHamiltonian(htilde), DynClosedLoop(law)) - -hj = h1 * (1.0, x -> x, p -> p, h2) # identity jumps, written out -typeof(hj) +fj = f1 * (1, x -> x + 10, f2) +fj(0, 0.0, 2) # +10 at t = 1, then one unit of slope 1 ``` -## Jumps as callables - -A jump argument can be a function (as above), `nothing` for the identity (no jump at all), or -— accepted in addition to the two — a plain `Vector` added to the state/costate directly: - ```@example main -h_identity = h1 * (1.0, nothing, nothing, h2) # nothing ≡ no jump -h_vec = h1 * (1.0, [0.0, 0.0], h2) # a bare vector jump works too -nothing # hide +@assert f(0, 0.0, 2) ≈ 1 && fj(0, 0.0, 2) ≈ 11 # hide +nothing # hide ``` -## Calling a multi-phase flow +On a Hamiltonian flow, the state and the costate are integrated together, and a jump can act +on either: -Exactly like any other flow — point form for the endpoint, trajectory form for the full path; -`variable=` still mandatory on a `NonFixed` problem, same rule as everywhere else in this -section. +- `h1 * (t1, j_x, j_p, h2)` applies `j_x` to the state and `j_p` to the costate, each a + function, a vector to add, or `nothing` for no jump; +- `h1 * (t1, j, h2)` with a **vector** `j` is a jump of the **costate**, `p ← p + j`: the + state is continuous, as at the junctions of a boundary arc (see + [Constrained arcs](@ref flows-constrained-arcs)); +- `h1 * (t1, j, h2)` with a **function** `j` maps the pair, `(x, p) ← j(x, p)`. -## Inspecting one +On the extremal of the energy problem, a costate jump of $(10, 0)$ at $t = 1/2$ leaves the +state continuous and shifts $p_1$ from $12$ to $22$: ```@example main -n_phases(f) +h = Flow( + PseudoHamiltonian((x, p, u) -> p[1] * x[2] + p[2] * u - u^2 / 2), + DynClosedLoop((x, p) -> p[2]), +) +h_jump = h * (0.5, [10, 0], h) +traj = h_jump((0, 1), [-1, 0], [12, 6]) +state(traj)(0.5 - 1e-9) ≈ state(traj)(0.5 + 1e-9), costate(traj)(0.25), costate(traj)(0.75) ``` ```@example main -get_switching_time(f, 1) +@assert costate(traj)(0.25) ≈ [12, 3] && costate(traj)(0.75)[1] ≈ 22 # hide +nothing # hide ``` -```@example main -get_switching_times(f) -``` +## Inspecting a concatenation ```@example main -get_flow(f, 1) === f1 +n_phases(f), get_switching_times(f), get_flow(f, 1) === f1 ``` -```@example main -length(get_flows(f)) -``` +`get_flows`, `get_switching_time(f, i)`, `get_jump(f, i)` and `get_jumps(f)` read back the +other parts. + +## The point call returns one vector -`get_jump(mp, i)`/`get_jumps(mp)` read back the jump functions the same way, on a flow built -with jumps. +A concatenation is called like any flow: `f((t0, tf), x0, p0)` for the trajectory, and +`f(t0, x0, p0, tf)` for the final point, with `variable=` on a problem with a variable. One +difference: on Hamiltonian flows, the point call of a concatenation returns **one vector** +`[x; p]`, where a single flow returns the pair `(x, p)` +([CTFlows#439](https://github.com/control-toolbox/CTFlows.jl/issues/439)). Write +`zf = f(…)`, then `zf[1:n]` for the state and `zf[n+1:2n]` for the costate. -## Worked example: bang-bang time-optimal double integrator +## Example: minimum time, bang–bang -Minimise the final time for $\ddot q = u$, $u \in [-1,1]$, from $(q,v)=(-1,0)$ to $(0,0)$. The -optimum is $t_f = 2$, reached by one switch at $t=1$: $u=+1$ then $u=-1$. +Minimise $t_f$ for $\ddot q = u$, $u \in [-1, 1]$, from $(q, v) = (-1, 0)$ to $(0, 0)$. +With $p^0 = -1$, $H = p_1 v + p_2 u$ is maximised by $u = \operatorname{sign}(p_2)$. Here +$p_1$ is constant and $p_2 = p_2(0) - p_1 t$, so there is one switch, where $p_2$ vanishes. +By symmetry it happens at $t_f/2$, and the target gives $t_1 = 1$, $t_f = 2$. The +transversality condition $H(t_f) = 1$ for the Mayer cost $t_f$ (see +[Notation and conventions](@ref modelling-formulation-conventions)), at $u = -1$ and +$v(t_f) = 0$, gives $p_2(t_f) = -1$, so $p(0) = (1, 1)$. ```@example main ocp = @def begin @@ -104,32 +103,88 @@ ocp = @def begin x = (q, v) ∈ R², state u ∈ R, control -1 ≤ u(t) ≤ 1 - q(0) == -1 - v(0) == 0 - q(tf) == 0 - v(tf) == 0 + x(0) == [-1, 0] + x(tf) == [0, 0] ẋ(t) == [v(t), u(t)] tf → min end -f_plus = Flow(ocp, (x, p, v) -> 1.0) -f_minus = Flow(ocp, (x, p, v) -> -1.0) - -t1 = 1.0 -tf = 2.0 -p0 = [1.0, 1.0] # the PMP's predicted initial costate +f_plus = Flow(ocp, (x, p, v) -> 1) +f_minus = Flow(ocp, (x, p, v) -> -1) +t1, tf = 1, 2 f_bb = f_plus * (t1, f_minus) -xf, pf = f_bb(0.0, [-1.0, 0.0], p0, tf; variable=tf) -xf # ≈ [0, 0], the target +zf = f_bb(0, [-1, 0], [1, 1], tf; variable=tf) +xf, pf = zf[1:2], zf[3:4] +``` + +The concatenation reaches the target, with $p(t_f) = (1, -1)$. + +```@example main +@assert isapprox(xf, [0, 0]; atol=1e-8) && isapprox(pf, [1, -1]; atol=1e-8) # hide +nothing # hide +``` + +## Example: off arc, then bang + +This example comes from the v2.0.4 manual. Minimise the $L^1$ norm of the control, +$\int_0^1 |u|$, for $\dot x = -x + u$, $u \in [-1, 1]$, from $x(0) = -1$ to $x(1) = 0$: + +```@example main +t0, tf1 = 0, 1 +x0, xf1 = -1, 0 + +ocp_l1 = @def begin + t ∈ [t0, tf1], time + x ∈ R, state + u ∈ R, control + x(t0) == x0 + x(tf1) == xf1 + -1 ≤ u(t) ≤ 1 + ẋ(t) == -x(t) + u(t) + ∫(abs(u(t))) → min +end +nothing # hide +``` + +With $p^0 = -1$, $H = p(-x + u) - |u|$ is maximised by $u = 0$ when $|p| < 1$, and by +$u = \operatorname{sign}(p)$ when $|p| > 1$. Since $\dot p = p$, $p(t) = p_0 e^t$: the +control is $u = 0$ (off arc) until $p$ reaches $1$, at $t_1 = -\ln p_0$, then $u = 1$ +(bang arc). Integrating $\dot x = -x$ then $\dot x = -x + 1$ and asking $x(1) = 0$ gives + +```math +p_0 = \frac{1}{x_0 - (x_f - 1)e^{t_f}}, \qquad t_1 = -\ln p_0 . +``` + +```@example main +p0 = 1 / (x0 - (xf1 - 1) * exp(tf1)) +t1_l1 = -log(p0) + +f_off = Flow(ocp_l1, (x, p) -> 0) # off arc: u = 0 +f_bang = Flow(ocp_l1, (x, p) -> 1) # bang arc: u = 1 + +f_l1 = f_off * (t1_l1, f_bang) +sol_l1 = f_l1((t0, tf1), x0, p0) +state(sol_l1)(tf1), objective(sol_l1) +``` + +The flow reaches $x(1) = 0$, and its cost is the length of the bang arc, $t_f - t_1$: + +```@example main +@assert abs(state(sol_l1)(tf1)) < 1e-8 # hide +@assert isapprox(objective(sol_l1), tf1 - t1_l1; atol=1e-6) # hide +nothing # hide +``` + +```@example main +plot(sol_l1) ``` ## See also -- [Constrained arcs](@ref flows-constrained-arcs) — assembling arcs around a boundary arc - specifically, where the jump usually comes from a costate discontinuity. -- [Shooting](@ref flows-shooting) — solving for the switching time(s) - themselves, not just assuming them known as here. -- [From an OCP](@ref flows-from-ocp) — the single-arc case this section generalises. -- [Time minimisation (bang–bang)](@ref examples-double-integrator-time) — the full story behind - the double-integrator example used throughout this page. +- [Constrained arcs](@ref flows-constrained-arcs): arcs on the boundary of a state + constraint, with costate jumps at the junctions. +- [Shooting](@ref flows-shooting): solve for the switching times and the initial costate, + instead of taking them from the analysis as here. +- [Time minimisation (bang–bang)](@ref examples-double-integrator-time): the bang–bang example + in full. From 742560fc58f2efab532a93a56e0ce2c0878ad401 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 15:44:38 +0200 Subject: [PATCH 35/57] docs(constrained-arcs): the two v2.0.4 worked examples, function form, Symbol form caveat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restore the first-order example (ẋ = u, x ≥ 0.1, ∫x², Bonnard–Faubourg–Launay– Trélat 2003): derivation of u = 0 and μ = 2x on the arc, three arcs with t₁ = 0.9, t₂ = 1.6; the flow reaches x(2) = 1/2, and the direct method finds the same arc and the same cost (asserts). - Restore the costate-jump example (Bryson–Denham, l = 1/9, order two): derivation, jumps of 18 on p₁ at 1/3 and 2/3, J = 4/(9l) = 4 for both the flow and the direct method, overlaid on a plot. - Function form everywhere (decision C6), with the c([t,]x,u[,v]), u([t,]x,p[,v]), μ([t,]x,p[,v]) signatures; the `+ 0.0` trick is gone. The Symbol form is mentioned with its limitation: it ignores the bound and the side of the constraint (CTFlows#440, derivation and measurements in the issue). - With saveat, a flow's point call returns the value at the last saved time (CTFlows#441): warned on this page, on Plot and on Flows overview. - Drop the arbitrary t1, t2, p1 demo and the positional-form section. Co-Authored-By: Claude Opus 5.5 --- docs/src/flows/constrained-arcs.md | 279 ++++++++++++++++++----------- docs/src/flows/overview.md | 4 +- docs/src/results/plot.md | 4 +- 3 files changed, 184 insertions(+), 103 deletions(-) diff --git a/docs/src/flows/constrained-arcs.md b/docs/src/flows/constrained-arcs.md index bb436cce6..44589f19a 100644 --- a/docs/src/flows/constrained-arcs.md +++ b/docs/src/flows/constrained-arcs.md @@ -1,173 +1,252 @@ # [Constrained arcs](@id flows-constrained-arcs) -A flow along a boundary arc — where a state constraint is active — needs the constraint and -its multiplier, not just the control law. +Along a **boundary arc**, a state constraint is active, and the flow needs more than a +control law: the constraint and its multiplier. This page builds such flows, and assembles +them with the unconstrained arcs, on two problems with known solutions. ```@example main using OptimalControl using OrdinaryDiffEqTsit5 +using NLPModelsIpopt +using Plots nothing # hide ``` ## The setting -A state constraint active on a sub-interval turns the PMP into a three-piece story: -unconstrained arc, boundary arc (where the constraint is tight and pins down both the control -and the multiplier in feedback form), unconstrained arc again. The boundary arc's flow needs -that extra structure. +With the conventions of [Notation and conventions](@ref modelling-formulation-conventions), a +state constraint is written $c(x) \ge 0$, and along a boundary arc, where $c(x(t)) = 0$, the +pseudo-Hamiltonian gains the term $+\mu\, c(x)$, with $\mu \ge 0$: -The full three-arc problem — including the shooting that finds the entry and exit times — is -worked end to end in [State constraint](@ref examples-state-constraint), on this exact OCP. -This page stays with the boundary arc's flow in isolation. +```math +H(x, p, u, \mu) = p \cdot f(x, u) + p^0 f^0(x, u) + \mu\, c(x). +``` + +On the arc, differentiating $c(x(t)) = 0$ gives the control, and the maximisation condition +then gives the multiplier, both in feedback form. `Flow(ocp, law; constraint=c, multiplier=μ)` +integrates the corresponding Hamiltonian flow. The functions have the signatures + +```julia +c([t, ]x, u[, v]) # the constraint, c ≥ 0 +u([t, ]x, p[, v]) # the control law on the arc +μ([t, ]x, p[, v]) # the multiplier on the arc +``` + +with `t` for a non-autonomous problem and `v` for a problem with a variable. + +## A constraint of order one + +This example comes from the v2.0.4 manual, after Bonnard, Faubourg, Launay and +Trélat[^1]. Minimise $\int_0^2 x^2$ for $\dot x = u$, $|u| \le 1$, from $x(0) = 1$ to +$x(2) = 1/2$, under the constraint $x \ge l_b = 0.1$: ```@example main -t0 = 0.0 -tf = 1.0 -x0 = [-1.0, 0.0] -xf = [0.0, 0.0] -VMAX = 1.0 # unconstrained transfer peaks at v = 1.5, so this bites +t0, tf = 0, 2 +x0, xf = 1, 1 / 2 +lb = 0.1 ocp = @def begin t ∈ [t0, tf], time - x = (q, v) ∈ R², state + x ∈ R, state u ∈ R, control + -1 ≤ u(t) ≤ 1 x(t0) == x0 x(tf) == xf - v(t) + 0.0 ≤ VMAX, (vmax) - ẋ(t) == [v(t), u(t)] - 0.5∫(u(t)^2) → min + x(t) - lb ≥ 0, (x_con) + ẋ(t) == u(t) + ∫(x(t)^2) → min end nothing # hide ``` -`v(t) + 0.0 ≤ VMAX` rather than `v(t) ≤ VMAX`: a bare `v(t) ≤ VMAX` parses as a **box bound** -on the state component, which carries no path multiplier and cannot be pulled into a flow by -its label. Adding `+ 0.0` makes it a nonlinear **path** constraint — it now has a dual, and -`constraint=:vmax` below can reach it. - -## Building the constrained flow +Here $c(x) = x - l_b$ and $H = p\,u - x^2 + \mu\,(x - l_b)$. Along a boundary arc, +$x = l_b$ is constant, so $u = 0$. The maximisation condition with $|u| < 1$ gives $p = 0$ on +the arc, and differentiating, $\dot p = 2x - \mu = 0$: the multiplier is $\mu = 2x$. The +solution has three arcs: $u = -1$, the boundary arc, then $u = +1$, with $t_1 = 0.9$, +$t_2 = 1.6$ and $p(0) \approx -0.982237546583301$. ```@example main -g(x) = VMAX - x[2] # ≥ 0 away from the boundary, 0 on it -μ(x, p) = p[1] # feedback multiplier on the boundary arc: μ = p_q -law(x, p) = 0.0 # boundary control: u = 0 ⟹ v̇ = 0 ⟹ v = VMAX +c(x, u) = x - lb # the constraint, c ≥ 0 +u_b(x, p) = 0 # the control on the boundary arc +μ(x, p) = 2x # the multiplier on the boundary arc -f_boundary = Flow(ocp, law; constraint=(x, u) -> g(x), multiplier=μ) +f_minus = Flow(ocp, (x, p) -> -1) +f_boundary = Flow(ocp, u_b; constraint=c, multiplier=μ) +f_plus = Flow(ocp, (x, p) -> 1) -# integrate a segment of the boundary arc: enter at t1 with v = VMAX -t1, t2 = 0.3, 0.7 -p1 = [1.0, 0.4] -xb, pb = f_boundary(t1, [-0.5, VMAX], p1, t2) -xb, g(xb) # v held at VMAX along the arc → g(xb) ≈ 0 +t1, t2 = 0.9, 1.6 +f = f_minus * (t1, f_boundary) * (t2, f_plus) + +p0 = -0.982237546583301 +zf = f(t0, x0, p0, tf) ``` -`constraint=`/`multiplier=` must be given **as a pair** — one without the other is rejected: +The concatenation reaches $x(2) = 1/2$ (its point call returns `[x; p]`, see +[Multi-phase flows](@ref flows-multi-phase)). The direct method finds the same boundary arc, +$[0.9, 1.6]$, and the same cost: + +```@example main +sol_d = solve(ocp; grid_size=400, display=false) +sol_f = f((t0, tf), x0, p0) +objective(sol_d), objective(sol_f) +``` + +```@example main +@assert isapprox(zf[1], xf; atol=1e-8) # hide +@assert isapprox(objective(sol_d), objective(sol_f); atol=1e-3) # hide +tg = time_grid(sol_d) # hide +on_arc = [t for t in tg if state(sol_d)(t) < lb + 1e-3] # hide +@assert isapprox(first(on_arc), t1; atol=1e-2) && isapprox(last(on_arc), t2; atol=1e-2) # hide +nothing # hide +``` + +[^1]: B. Bonnard, L. Faubourg, G. Launay and 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. + +`constraint` and `multiplier` go together: one without the other is rejected. ```@repl main try # hide -Flow(ocp, law; constraint=:vmax) +Flow(ocp, u_b; constraint=c) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -## Three ways to give the constraint - -**A plain `Function`** — the form above, `(x, u) -> g(x)` paired with `multiplier=μ`. +## Other ways to give the constraint -**A typed `Data.StateConstraint`** (or `ControlConstraint`/`MixedConstraint`/`PathConstraint` -for the other shapes) — equivalent, more explicit: +The constraint can also be a typed `StateConstraint` (or `ControlConstraint`, +`MixedConstraint`, `PathConstraint` for constraints that involve the control), with the same +result: ```@example main -f_typed = Flow(ocp, law; constraint=StateConstraint(g), multiplier=μ) -f_typed(t1, [-0.5, VMAX], p1, t2)[1] ≈ xb +f_typed = Flow(ocp, u_b; constraint=StateConstraint(x -> x - lb), multiplier=μ) +f_typed(t1, lb, 0.0, t2), f_boundary(t1, lb, 0.0, t2) ``` -**A `Symbol` naming a `:path` constraint already declared in the OCP** — the standout -capability here, not just a rename: +Several constraints active on the same arc are given as tuples, one multiplier per +constraint: ```@example main -f_sym = Flow(ocp, law; constraint=:vmax, multiplier=μ) -typeof(f_sym) +f_two = Flow(ocp, u_b; + constraint=(c, (x, u) -> 10 - x), # a second constraint, x ≤ 10, inactive + multiplier=(μ, (x, p) -> 0)) +f_two(t1, lb, 0.0, t2) ``` -An unknown label is rejected, not silently accepted: +```@example main +@assert all(f_typed(t1, lb, 0.0, t2) .≈ (lb, 0)) && all(f_two(t1, lb, 0.0, t2) .≈ (lb, 0)) # hide +nothing # hide +``` -```@repl main -try # hide -Flow(ocp, law; constraint=:nope, multiplier=μ) -catch e # hide -showerror(IOContext(stdout, :color => false), e) # hide -end # hide +Finally, `constraint=:x_con` names a path constraint of the problem by its label. It uses the +function stored in the model as it is, without its bound: the result is right only when the +constraint is written as here, `c(x) ≥ 0`. Written `x(t) ≥ lb` or `lb - x(t) ≤ 0`, the flow on +the arc is wrong, without an error +([CTFlows#440](https://github.com/control-toolbox/CTFlows.jl/issues/440)). The examples of +this documentation use the function form. + +## A costate jump: a constraint of order two + +This example also comes from the v2.0.4 manual. It is the Bryson–Denham problem: minimise +$\frac12\int_0^1 u^2$ for $\ddot x_1 = u$, from $x(0) = (0, 1)$ to $x(1) = (0, -1)$, with +$x_1 \le l = 1/9$: + +```@example main +l = 1 / 9 + +ocp_bd = @def begin + t ∈ [0, 1], time + x ∈ R², state + u ∈ R, control + x(0) == [0, 1] + x(1) == [0, -1] + x₁(t) ≤ l, (x_con) + ẋ(t) == [x₂(t), u(t)] + 0.5∫(u(t)^2) → min +end +nothing # hide ``` -!!! warning "The `Symbol` form uses the model's own sign convention" +Here $c(x) = l - x_1 \ge 0$ and $H = p_1 x_2 + p_2 u - u^2/2 + \mu\,(l - x_1)$. Along a +boundary arc, $x_1 = l$, so $\dot x_1 = x_2 = 0$, and differentiating again, $u = 0$: the +constraint is of order two. The maximisation condition gives $p_2 = 0$ on the arc, then +$\dot p_2 = -p_1 = 0$ and $\dot p_1 = \mu = 0$. Off the arc, $u = p_2$. The solution has +three arcs, with $t_1 = 3l = 1/3$, $t_2 = 1 - 3l = 2/3$ and $p(0) = (-18, -6)$, and the +costate **jumps** at $t_1$ and $t_2$: $p_1$ increases by $18$. On a Hamiltonian flow, a vector +jump acts on the costate (see [Multi-phase flows](@ref flows-multi-phase)): - `constraint=:vmax` pulls the constraint straight from the OCP — `v(t) ≤ V_{max}` as - written there — rather than whatever hand-derived $g(x) \geq 0$ you might use in a - shooting function. The two are **not guaranteed to agree numerically** unless you match - conventions yourself; pick one style per problem and stay consistent, don't mix a - hand-rolled `g`/`μ` pair with the model's own label expecting them to be interchangeable. +```@example main +f_free = Flow(ocp_bd, (x, p) -> p[2]) +f_arc = Flow(ocp_bd, (x, p) -> 0; constraint=(x, u) -> l - x[1], multiplier=(x, p) -> 0) -## Several constraints at once +ν = 18 +f_bd = f_free * (3l, [ν, 0], f_arc) * (1 - 3l, [ν, 0], f_free) +zf_bd = f_bd(0, [0, 1], [-18, -6], 1) +``` -Pass matched tuples of functions (or labels) and multipliers, one per active constraint on the -boundary arc, in place of the single values above: +The flow reaches the target $(0, -1)$. Its cost is $4/(9l) = 4$, the value the direct method +finds too: ```@example main -g_q(x) = 10.0 - x[1] # a second bound, q ≤ 10 — slack on this arc -f_two = Flow(ocp, law; - constraint = ((x, u) -> g(x), (x, u) -> g_q(x)), - multiplier = (μ, (x, p) -> 0.0)) -f_two(t1, [-0.5, VMAX], p1, t2)[1] ≈ xb # extra bound is slack +sol_bd = solve(ocp_bd; display=false) +sol_bd_flow = f_bd((0, 1), [0, 1], [-18, -6]) +objective(sol_bd), objective(sol_bd_flow) ``` -## Assembling the arcs +To plot the flow against the direct solution, the same flows are built with `saveat`, so +that the trajectory has enough points (see [Plotting a flow](@ref results-plot-flow)). Use +such flows for trajectories only: their point call returns the value at the last `saveat` +time, not at the final time +([CTFlows#441](https://github.com/control-toolbox/CTFlows.jl/issues/441)). + +```@example main +fine = (saveat=range(0, 1, 101), dense=false) +f_bd_fine = Flow(ocp_bd, (x, p) -> p[2]; fine...) * + (3l, [ν, 0], Flow(ocp_bd, (x, p) -> 0; constraint=(x, u) -> l - x[1], multiplier=(x, p) -> 0, fine...)) * + (1 - 3l, [ν, 0], Flow(ocp_bd, (x, p) -> p[2]; fine...)) +sol_bd_fine = f_bd_fine((0, 1), [0, 1], [-18, -6]) +nothing # hide +``` -A boundary arc is one phase in a larger [multi-phase flow](@ref flows-multi-phase): -unconstrained arc, then the constrained flow from `t1` (constraint activation), then -unconstrained again from `t2` (exit) — with a jump on the costate at each switch if the -constraint order requires one (none here: a first-order constraint keeps the costate -continuous). +```@example main +@assert isapprox(zf_bd[1:2], [0, -1]; atol=1e-8) # hide +@assert isapprox(objective(sol_bd_flow), 4 / (9l); atol=1e-5) # hide +@assert isapprox(objective(sol_bd), 4 / (9l); atol=1e-3) # hide +nothing # hide +``` ```@example main -f_interior = Flow(ocp, (x, p) -> p[2]) # u = p_v away from the boundary -φ = f_interior * (t1, f_boundary) * (t2, f_interior) # 3 arcs -n_phases(φ) +plt = plot(sol_bd; label="direct", size=(800, 600)) +plot!(plt, sol_bd_fine; label="flow", linestyle=:dash) ``` -Solving for `t1` and `t2` rather than fixing them is the shooting problem worked in -[State constraint](@ref examples-state-constraint). +## Partial Hamiltonian -## Positional form is gone +`hamiltonian_type=:partial` (see +[Total or partial Hamiltonian](@ref flows-from-ocp-total-partial)) applies to a constrained +arc too. Along an optimal boundary arc, the law and the multiplier make both forms give the +same flow: -```@repl main -try # hide -Flow(ocp, law, (x, u) -> g(x), μ) -catch e # hide -showerror(IOContext(stdout, :color => false), e) # hide -end # hide +```@example main +f_partial = Flow(ocp, u_b; constraint=c, multiplier=μ, hamiltonian_type=:partial) +f_partial(t1, lb, 0.0, t2) ``` -## Partial Hamiltonian on a constrained arc - -`hamiltonian_type=:partial` (see [From an OCP](@ref flows-from-ocp)) is supported here too — the -boundary arc's law is stationary for the constrained pseudo-Hamiltonian at the optimum, same as -the unconstrained case, so `:total` and `:partial` still agree when the law is genuinely -optimal. +## Control-free problems -## Control-free flows reject constraints +A problem without a control has no boundary arcs in this sense: its flow rejects +`constraint` and `multiplier`. ```@repl main ocp_cf = @def begin - t ∈ [t0, tf], time + t ∈ [0, 1], time x ∈ R, state - x(t0) == 1.0 + x(0) == 1 ẋ(t) == -x(t) ∫(x(t)^2) → min end; try # hide -Flow(ocp_cf; constraint=(x, u) -> 1.0, multiplier=(x, p) -> 1.0) +Flow(ocp_cf; constraint=(x, u) -> 1, multiplier=(x, p) -> 1) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide @@ -175,9 +254,7 @@ end # hide ## See also -- [Multi-phase flows](@ref flows-multi-phase) — assembling several arcs, constrained or not, - into one callable flow. -- [Shooting](@ref flows-shooting) — solving for the switching times - themselves rather than assuming them known. -- [State constraint](@ref examples-state-constraint) — this OCP worked end to end: the - three-arc structure, the costate jump, and the shooting for the entry and exit times. +- [Multi-phase flows](@ref flows-multi-phase): concatenations and jumps. +- [Shooting](@ref flows-shooting): solve for the junction times and the initial costate. +- [State constraint](@ref examples-state-constraint): a speed constraint on the double + integrator, from the direct solution to the shooting. diff --git a/docs/src/flows/overview.md b/docs/src/flows/overview.md index e241034b8..657390f7b 100644 --- a/docs/src/flows/overview.md +++ b/docs/src/flows/overview.md @@ -144,7 +144,9 @@ The options used most are `alg` (the ODE algorithm, `Tsit5()` by default), `relt `dense` (dense output, `:auto` by default: `false` for a point call, `true` for a trajectory call). Pass them as keywords when you build a flow, for example `Flow(ocp, law; reltol=1e-10, alg=Tsit5())`. With `saveat`, also pass `dense=false` -([CTFlows#434](https://github.com/control-toolbox/CTFlows.jl/issues/434)), see +([CTFlows#434](https://github.com/control-toolbox/CTFlows.jl/issues/434)), and use the flow +for trajectories only: its point call returns the value at the last `saveat` time +([CTFlows#441](https://github.com/control-toolbox/CTFlows.jl/issues/441)). See [Plot](@ref results-plot-flow). Another integrator only needs its package. Here the 9th-order Verner method, with tighter diff --git a/docs/src/results/plot.md b/docs/src/results/plot.md index 6b5b941dc..f3a35df6f 100644 --- a/docs/src/results/plot.md +++ b/docs/src/results/plot.md @@ -398,7 +398,9 @@ nothing # hide For a finer curve, pass `saveat` when you **build** the flow, together with `dense=false`. The call itself only accepts `variable`, `unsafe` and `variable_costate`. Do not leave out `dense=false`: without it, the call crashes the Julia session -([CTFlows#434](https://github.com/control-toolbox/CTFlows.jl/issues/434)). +([CTFlows#434](https://github.com/control-toolbox/CTFlows.jl/issues/434)). Use such a flow for trajectories only: its point call `f(t0, x0, p0, tf)` returns the +value at the last `saveat` time before `tf`, not at `tf` +([CTFlows#441](https://github.com/control-toolbox/CTFlows.jl/issues/441)). ```@example main fine_grid = range(t0, tf, 100) From 67d581e95a1fddb81e3d6ebfeb139b69c1bc1ea3 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 15:48:45 +0200 Subject: [PATCH 36/57] docs(shooting): B8, transversality in the Mayer form, the loop closed from the direct solve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Transversality of the free final time written as in Formulation: for the Mayer cost t_f, p(t_f)·f = 1 (the page said H(t_f) = −1); the switching condition is "the switching function p₂ vanishes at t₁", not the circular "H_u = 0". - shoot! uses unsafe=true, as the page recommends. - The shooting starts from the direct solution: p₀ = dual(sol, ocp, :start), t₁ read off the control, t_f = variable(sol); Newton converges to p₀ = (1, 1), t₁ = 1, t_f = 2 (asserts), and the indirect and direct final times agree. - Drop the variable_costate example (p_v ≡ 0 there, since t_f only sets the horizon); link the section where the variable costate matters. Co-Authored-By: Claude Opus 5.5 --- docs/src/flows/shooting.md | 220 ++++++++++++++++++++----------------- 1 file changed, 117 insertions(+), 103 deletions(-) diff --git a/docs/src/flows/shooting.md b/docs/src/flows/shooting.md index 0483123f9..b72d59960 100644 --- a/docs/src/flows/shooting.md +++ b/docs/src/flows/shooting.md @@ -1,32 +1,27 @@ # [Shooting](@id flows-shooting) -The payoff of everything else in this section: turn a flow into a root-finding problem for the -unknown initial costate (and switching times, and free final time), and solve it. +The maximum principle gives the extremals, but not the initial costate, the switching times +or a free final time. **Shooting** finds them: write the conditions the extremal must satisfy +as equations in these unknowns, integrate the flows to evaluate them, and solve the equations +with a nonlinear solver. The direct method gives the starting point. ```@example main using OptimalControl using OrdinaryDiffEqTsit5 using NonlinearSolve +using NLPModelsIpopt nothing # hide ``` -## The shooting equation - -The PMP gives necessary conditions but not $p_0$ directly. Shooting turns "integrate the flow -and check the boundary/transversality conditions" into a root-finding problem: guess the -unknowns (initial costate, switching times, free final time), integrate, measure how far the -result is from satisfying the conditions, and let a nonlinear solver close the gap. +## The problem -Worked example throughout: minimise the final time for $\ddot q = u$, $u \in [-1,1]$, from -$(q,v)=(-1,0)$ to $(0,0)$ — bang-bang, one switch, free $t_f$. The reference solution is -$p_0=(1,1)$, one switch at $t=1$, $t_f=2$. +We take the minimum-time transfer of the double integrator: minimise $t_f$ for +$\ddot q = u$, $u \in [-1, 1]$, from $(q, v) = (-1, 0)$ to $(0, 0)$: ```@example main -t0 = 0.0 -x0 = [-1.0, 0.0] -xf = [0.0, 0.0] -u_max = 1.0 -u_min = -1.0 +t0 = 0 +x0 = [-1, 0] +xf = [0, 0] ocp = @def begin tf ∈ R, variable @@ -34,146 +29,165 @@ ocp = @def begin x = (q, v) ∈ R², state u ∈ R, control -1 ≤ u(t) ≤ 1 - q(0) == -1 - v(0) == 0 - q(tf) == 0 - v(tf) == 0 + x(0) == x0, (start) + x(tf) == xf ẋ(t) == [v(t), u(t)] tf → min end +nothing # hide +``` -f_max = Flow(ocp, (x, p, v) -> u_max) -f_min = Flow(ocp, (x, p, v) -> u_min) +With $p^0 = -1$ and the Mayer cost $t_f$, the pseudo-Hamiltonian is +$H = p_1 v + p_2 u$, maximised by $u = \operatorname{sign}(p_2)$: the **switching function** +is $p_2$. As on [Multi-phase flows](@ref flows-multi-phase), the solution is $u = +1$ then +$u = -1$, with $p_0 = (1, 1)$, $t_1 = 1$ and $t_f = 2$. Shooting will find these values. + +```@example main +f_max = Flow(ocp, (x, p, v) -> 1) +f_min = Flow(ocp, (x, p, v) -> -1) nothing # hide ``` ## The shooting function -The problem has 4 unknowns — $p_0$ (2), the switching time $t_1$, and $t_f$ — and 4 -residuals: the target state (2), the switching condition ($p_2=0$ where the bang-bang control -switches, since $H_u=0$ there), and the free-time transversality $H(t_f)=-1$ (the Mayer cost is -$t_f\to\min$, normal case). Written in place, the way `NonlinearSolve` wants it: +There are four unknowns, $p_0 \in \mathbb{R}^2$, $t_1$ and $t_f$, and four conditions: + +- the target, $x(t_f) = (0, 0)$ (two equations); +- the switch: the switching function vanishes at $t_1$, $p_2(t_1) = 0$; +- the transversality condition of the free final time: for the Mayer cost $t_f$, the + maximised Hamiltonian equals $-p^0 = 1$ at $t_f$, that is + $p(t_f) \cdot f(x(t_f), u(t_f)) = 1$ (see + [Notation and conventions](@ref modelling-formulation-conventions)). + +The shooting function integrates the two arcs and returns the four residuals. It is written in +place, as `NonlinearSolve` expects: ```@example main -H(x, p, u) = p[1] * x[2] + p[2] * u - 1 - -function shoot!(s, ξ) - p0 = ξ[1:2] - t1, tfv = ξ[3], ξ[4] - x1, p1 = f_max(t0, x0, p0, t1; variable=tfv) - xf_, pf = f_min(t1, x1, p1, tfv; variable=tfv) - s[1:2] = xf_ - xf - s[3] = p1[2] - s[4] = H(xf_, pf, u_min) +H(x, p, u) = p[1] * x[2] + p[2] * u # p ⋅ f(x, u) + +function shoot!(s, ξ, _) + p0, t1, tf = ξ[1:2], ξ[3], ξ[4] + x1, p1 = f_max(t0, x0, p0, t1; variable=tf, unsafe=true) + x2, p2 = f_min(t1, x1, p1, tf; variable=tf, unsafe=true) + s[1:2] = x2 - xf # target + s[3] = p1[2] # switching function at t1 + s[4] = H(x2, p2, -1) - 1 # transversality, free final time return nothing end - -s = zeros(4) -shoot!(s, [1.0, 1.0, 1.0, 2.0]) # residual at the reference solution -sqrt(sum(abs2, s)) +nothing # hide ``` -## Solving it +At the solution, the residuals vanish: -`NonlinearSolve` closes the gap from a perturbed guess: +```@example main +s = zeros(4) +shoot!(s, [1, 1, 1, 2], nothing) +s +``` ```@example main -ξ_guess = [1.0, 1.0, 1.0, 2.0] .* 1.1 -prob = NonlinearProblem((s, ξ, _) -> shoot!(s, ξ), ξ_guess) -sol = NonlinearSolve.solve( - prob, SimpleNewtonRaphson(); abstol=1e-10, reltol=1e-10 -) -sol.u, sol.retcode +@assert maximum(abs, s) < 1e-10 # hide +nothing # hide ``` -## `unsafe=true` inside the loop +## `unsafe=true` -A nonlinear solver explores guesses that don't correspond to a real solution — some of them -can make the flow's integration blow up. The default behaviour is to throw, which would abort -the whole solve on the first bad guess: +While it searches, a nonlinear solver tries unknowns that are far from the solution, and the +integration can fail on them. By default a flow then raises an error, which would stop the +search. With `unsafe=true`, the flow returns what the integrator computed instead, and the +residual is just large. On $\dot x = x^2$, which blows up before $t = 1$ from $x(0) = 10$: ```@repl main -f_blowup = Flow(VectorField(x -> x^2)); # ẋ = x², diverges before t=1 +f_blowup = Flow(VectorField(x -> x^2)); try # hide -f_blowup(0.0, 10.0, 1.0) +f_blowup(0, 10.0, 1) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -`unsafe=true` returns whatever the integrator produced instead of throwing — garbage, but a -*value*, letting the shooting residual carry the failure forward as "very wrong" rather than -crashing the solve: - ```@example main -f_blowup(0.0, 10.0, 1.0; unsafe=true) +f_blowup(0, 10.0, 1; unsafe=true) ``` -Inside a `shoot!`, wrap the flow calls with `unsafe=true` so an intermediate failure shows up as -a large residual for the solver to step away from, not an exception that stops the search. +This is why `shoot!` above calls the flows with `unsafe=true`. + +## Starting from the direct solution -## Free final time +A shooting method converges from a good initial guess. The direct method provides one: -The transversality residual `H(xf_, pf, u_min) = -1` above **is** the free-final-time -condition — no separate machinery needed beyond adding it as a residual. For a smooth -(non-switching) free-final-time problem, `variable_costate=true` gives the extra adjoint -directly if the transversality condition is stated in terms of $p_v(t_f)$ instead: +- the initial costate is the multiplier of the initial condition, `dual(sol, ocp, :start)` + (more accurate than `costate(sol)(0)`, see + [Solution object](@ref results-solution-duals)); +- the switching time is read off the control, where it changes sign; +- the final time is the variable. ```@example main -xf_v, pf_v, pvf = f_max( - t0, x0, [1.0, 1.0], 1.0; variable=2.0, variable_costate=true -) -pvf -``` +sol_d = solve(ocp; display=false) + +p0_guess = dual(sol_d, ocp, :start) +tg = time_grid(sol_d) +t1_guess = tg[findfirst(t -> control(sol_d)(t) < 0, tg)] +tf_guess = variable(sol_d) -## Switching times as unknowns +ξ_guess = [p0_guess; t1_guess; tf_guess] +``` -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), and -[Turnpike (bang–singular–bang)](@ref examples-turnpike) for a worked case with two unknown -switching times bracketing a singular arc. +## Solving the shooting equations -## Getting a starting point from a direct solve +```@example main +prob = NonlinearProblem(shoot!, ξ_guess) +sol = NonlinearSolve.solve(prob, SimpleNewtonRaphson(); abstol=1e-10, reltol=1e-10) +sol.u, sol.retcode +``` -Manufacturing a shooting guess by hand doesn't scale — the standard workflow is to solve the -same problem directly first, then read `costate(sol)(t0)` off as the initial guess: +The solver finds $p_0 = (1, 1)$, $t_1 = 1$ and $t_f = 2$. ```@example main -using NLPModelsIpopt -direct_sol = solve(ocp; display=false) -p0_guess = costate(direct_sol)(0.0) -p0_guess +@assert string(sol.retcode) == "Success" # hide +@assert isapprox(sol.u, [1, 1, 1, 2]; atol=1e-8) # hide +nothing # hide ``` -## Checking against the direct solution +## Checking the solution -Rebuild the full bang–bang trajectory from the shooting solution — `f_max`, then `f_min` at -the solved switch — and read its endpoint (a concatenated flow returns the stacked -state–costate vector `[q, v, p_q, p_v]` at the final time): +The two arcs, concatenated at the switching time found, reach the target. The point call of +a concatenation returns `[x; p]` (see [Multi-phase flows](@ref flows-multi-phase)): ```@example main -p0_sol, t1_sol, tf_sol = sol.u[1:2], sol.u[3], sol.u[4] - -f_bb = f_max * (t1_sol, f_min) -zf = f_bb(t0, x0, p0_sol, tf_sol; variable=tf_sol) +p0_s, t1_s, tf_s = sol.u[1:2], sol.u[3], sol.u[4] -zf[1:2] # final state ≈ [0, 0] — the indirect solution hits the target +f_bb = f_max * (t1_s, f_min) +zf = f_bb(t0, x0, p0_s, tf_s; variable=tf_s) +zf[1:2] ``` -The two methods agree on the optimum. The cost here is the final time, so comparing objectives -is comparing $t_f$: +The final time is the cost, so the indirect and the direct solutions agree when their final +times do: ```@example main -tf_sol, objective(direct_sol) +tf_s, objective(sol_d) ``` +```@example main +@assert isapprox(zf[1:2], xf; atol=1e-8) && isapprox(objective(sol_d), tf_s; atol=1e-6) # hide +nothing # hide +``` + +## More switches, other unknowns + +Each additional switch adds an unknown time and a switching condition; a boundary arc adds +its entry and exit times, with the junction conditions (see +[Constrained arcs](@ref flows-constrained-arcs)). When the variable enters the dynamics or +the cost, its transversality condition uses the costate of the variable, computed with +`variable_costate=true` (see [From an OCP](@ref flows-from-ocp-variable-costate)). The +[examples](@ref examples-gallery) solve several such problems, for instance +[Turnpike](@ref examples-turnpike), with two switching times around a singular arc. + ## See also -- [From an OCP](@ref flows-from-ocp) — building the flows a shooting function is made of. -- [Multi-phase flows](@ref flows-multi-phase) — concatenating the arcs once switching times are - known. -- [Solve overview](@ref solve-overview) — the direct-method starting point used above. -- [Time minimisation (bang–bang)](@ref examples-double-integrator-time) — the full story behind - the double-integrator example used throughout this page. +- [From an OCP](@ref flows-from-ocp): the flows a shooting function is made of. +- [Multi-phase flows](@ref flows-multi-phase): concatenations of arcs. +- [Solve overview](@ref solve-overview): the direct method used for the initial guess. +- [Time minimisation (bang–bang)](@ref examples-double-integrator-time): this example in + full. From 8e4a695ea044a24620314628474e5060496a9112 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:09:13 +0200 Subject: [PATCH 37/57] docs(geometry-overview): v2.0.4 intro and summary table restored, bridge identity derived and asserted Co-Authored-By: Claude Opus 5.5 --- docs/src/geometry/overview.md | 109 +++++++++++++++++----------------- 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/docs/src/geometry/overview.md b/docs/src/geometry/overview.md index fed4c52a7..5746795b2 100644 --- a/docs/src/geometry/overview.md +++ b/docs/src/geometry/overview.md @@ -1,10 +1,10 @@ -# [Overview](@id geometry-overview) +# [Geometry overview](@id geometry-overview) -Some flows can't be built directly from an optimal control problem — the control law itself -has to be *derived* first, for example a singular control on an arc where the usual -maximization condition degenerates. Deriving it needs differential-geometry tools: Lie -derivatives, Lie brackets, Poisson brackets. This section is that toolkit. It moved to its own -package, **CTLie**, in v2.1.0-beta. +Optimal control theory relies on tools from differential geometry: to analyse Hamiltonian +systems, compute singular controls, or study controllability. This section presents the +operators that OptimalControl provides for this: the Hamiltonian lift, Lie derivatives, Lie +brackets, Poisson brackets and partial time derivatives. They come from the package CTLie, and +`using OptimalControl` makes them available. ```@example main using OptimalControl @@ -12,74 +12,77 @@ using OptimalControl ## What this is for -- Computing a **singular control** — the standard chain of iterated Poisson brackets - ($H_{01}$, $H_{001}$, $H_{101}$, ...) that gives $u_{\text{sing}}$ on a singular arc. -- Checking **controllability** via the Lie brackets of the system's vector fields. -- Building a **Hamiltonian from a vector field** — the canonical lift used throughout the - indirect-methods section. +- Computing a **singular control**: on an arc where the switching function vanishes, its + successive time derivatives are Poisson brackets ($H_{01}$, $H_{001}$, $H_{101}$, …), and + they give the control (see [Poisson bracket](@ref geometry-poisson-singular)). +- Checking **controllability**, with the Lie brackets of the vector fields of the system. +- Building a **Hamiltonian from a vector field**, the lift, to integrate its flow (see + [From Hamiltonians](@ref flows-from-hamiltonians)). -## The four operations +## Summary -| Operation | Signature | What it computes | -| --- | --- | --- | -| [`Lift`](@ref) | `Lift(X)` | the Hamiltonian $H_X(x,p) = \langle p, X(x)\rangle$ of a vector field $X$ | -| [`ad`](@ref) | `ad(X, f)` / `ad(X, Y)` | Lie derivative of a scalar `f`, or Lie bracket of a vector field `Y`, along $X$ | -| [`Poisson`](@ref) | `Poisson(H, G)` | the Poisson bracket of two Hamiltonians | -| [`∂ₜ`](@ref) | `∂ₜ(f)` | the partial time derivative of a non-autonomous `f` | +| Operation | Notation | Julia | Page | +| --- | --- | --- | --- | +| Hamiltonian lift | $H_X(x, p) = \langle p, X(x) \rangle$ | `Lift(X)` | [Lift](@ref geometry-lift) | +| Lie derivative | $(\mathcal{L}_X f)(x) = f'(x) \cdot X(x)$ | `ad(X, f)` | [Lie derivative and Lie bracket](@ref geometry-ad) | +| Lie bracket | $[X, Y](x) = Y'(x) \cdot X(x) - X'(x) \cdot Y(x)$ | `ad(X, Y)` or `@Lie [X, Y]` | [Lie derivative and Lie bracket](@ref geometry-ad) | +| Poisson bracket | $\{H, G\} = \nabla_p H \cdot \nabla_x G - \nabla_x H \cdot \nabla_p G$ | `Poisson(H, G)` or `@Lie {H, G}` | [Poisson bracket](@ref geometry-poisson) | +| Partial time derivative | $\partial_t f(t, x, \ldots)$ | `∂ₜ(f)` | [Lie derivative and Lie bracket](@ref geometry-ad-time) | -## Two vocabularies +`ad(X, f)` computes a Lie derivative when `f` returns a scalar, and a Lie bracket when it +returns a vector. The macro [`@Lie`](@ref geometry-lie-macro) writes the brackets as on paper. -Two kinds of objects appear throughout: **vector fields** live on the state space -($X : x \mapsto X(x)$), **Hamiltonians** live on the cotangent space -($H : (x, p) \mapsto H(x,p)$). `ad` and its bracket operate on the first vocabulary, `Poisson` -on the second. [`Lift`](@ref) is the bridge from one to the other. +## Two kinds of objects -## The bridge identity +**Vector fields** live on the state space, $X : x \mapsto X(x)$, and **Hamiltonians** on the +cotangent space, $H : (x, p) \mapsto H(x, p)$. `ad` acts on vector fields, `Poisson` on +Hamiltonians, and the lift takes a vector field to a Hamiltonian. -Lifting turns a Lie bracket into a Poisson bracket: $\{H_X, H_Y\} = H_{[X,Y]}$, i.e. -`Poisson(Lift(X), Lift(Y)) ≈ Lift(ad(X, Y))`. It is the single best check that the two halves -of the toolkit agree — not a linear example, where the bracket is trivially zero: +Each can be a plain Julia function or a typed object, `VectorField(f)` or `Hamiltonian(h)`. +On plain functions, the default is a function of the state only (`x` or `(x, p)`): the +keywords `is_autonomous=false` (an argument `t` first) and `is_variable=true` (an argument `v` +last) describe the other cases. A typed object carries this information itself, and the +operations return typed objects, which can be nested. Two operands must have the same +dependence on time and on the variable. + +## [The bridge identity](@id geometry-overview-bridge) + +The lift turns a Lie bracket into a Poisson bracket: + +```math +\{H_X, H_Y\} = H_{[X, Y]}. +``` + +Indeed, $\nabla_p H_X = X$ and $\nabla_x H_Y = Y'^{\top} p$, so +$\{H_X, H_Y\} = \langle p, Y' X \rangle - \langle p, X' Y \rangle = \langle p, [X, Y] \rangle$. +For $X(x) = (x_1^2, x_2^2)$ and $Y(x) = (x_2, -x_1)$, the bracket is +$[X, Y](x) = (x_2^2 - 2x_1 x_2,\ 2 x_1 x_2 - x_1^2)$, so at $x = (1, 2)$, $[X, Y] = (0, 3)$, +and at $p = (3, 4)$ both sides equal $12$: ```@example main X(x) = [x[1]^2, x[2]^2] Y(x) = [x[2], -x[1]] -lhs = Poisson(Lift(X), Lift(Y)) -rhs = Lift(ad(X, Y)) - x, p = [1.0, 2.0], [3.0, 4.0] -lhs(x, p), rhs(x, p) +Poisson(Lift(X), Lift(Y))(x, p), Lift(ad(X, Y))(x, p) ``` -## Autonomous, non-autonomous, variable - -Every operation here takes `is_autonomous::Bool` and `is_variable::Bool` keywords (default -`true`/`false` — time-independent, no extra parameter). Operands that disagree — one -autonomous, one not, say — throw a `PreconditionError` naming both traits explicitly; see -[Lie derivative and Lie bracket](@ref geometry-ad) for the exact message. +```@example main +@assert ad(X, Y)(x) ≈ [0, 3] # hide +@assert Poisson(Lift(X), Lift(Y))(x, p) ≈ 12 && Lift(ad(X, Y))(x, p) ≈ 12 # hide +nothing # hide +``` ## Automatic differentiation -Everything here is AD-backed **except `Lift`**, which is a purely algebraic rearrangement -($H(x,p) = p \cdot X(x)$, no derivative involved). See -[AD backend](@ref geometry-ad-backend) for how the derivatives themselves are -computed and how to change the backend. - -## Coming from v2.0 - -| v2.0 | v2.1 | -| --- | --- | -| `Lie(X, f)` / `Lie(X, Y)` | `ad(X, f)` / `ad(X, Y)` | -| `X ⋅ f` | `ad(X, f)` — no operator replacement | -| `HamiltonianLift` | `LiftedHamiltonianFunction` (written `OptimalControl.LiftedHamiltonianFunction`) | - -See [Migrating to v2.1](@ref migration) for the full picture, including the throwing shims that -catch the old names. +`ad`, `Poisson`, `∂ₜ` and `@Lie` compute their derivatives by automatic differentiation; +`Lift` needs none. [AD backend](@ref geometry-ad-backend) shows how to choose the backend. ## See also -- [Lifting a vector field](@ref geometry-lift) +- [Lift](@ref geometry-lift) - [Lie derivative and Lie bracket](@ref geometry-ad) - [Poisson bracket](@ref geometry-poisson) - [The `@Lie` macro](@ref geometry-lie-macro) - [AD backend](@ref geometry-ad-backend) +- [Singular control](@ref examples-singular-control): these tools on a complete problem. From dfb2aabb4081d5e1961e1ebd8e73e7bae45ac27c Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:09:13 +0200 Subject: [PATCH 38/57] docs(lift): analytic values asserted, the flow of a lift, no typeof dump, CTLie#41 Co-Authored-By: Claude Opus 5.5 --- docs/src/geometry/lift.md | 104 ++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/docs/src/geometry/lift.md b/docs/src/geometry/lift.md index e10484fbc..b659fda28 100644 --- a/docs/src/geometry/lift.md +++ b/docs/src/geometry/lift.md @@ -1,15 +1,17 @@ # [Lift](@id geometry-lift) -Given a vector field $X : \mathbb{R}^n \to \mathbb{R}^n$, its **lift** is the Hamiltonian +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). ``` -It is a purely algebraic construction — no differentiation, no AD. +It is an algebraic construction: no derivative is computed. ```@example main using OptimalControl +using OrdinaryDiffEqTsit5 ``` ## From a plain function @@ -17,93 +19,95 @@ using OptimalControl ```@example main X(x) = [x[2], -x[1]] H = Lift(X) -H([1.0, 2.0], [3.0, 4.0]) + +x, p = [1.0, 2.0], [3.0, 4.0] +H(x, p) # 3 × 2 + 4 × (-1) ``` -`H` is an `OptimalControl.LiftedHamiltonianFunction` — a callable, not a `Hamiltonian`: +The lift of a plain function is a function, not a `Hamiltonian`: ```@example main -typeof(H) +H isa Function, H isa AbstractHamiltonian ``` ## From a typed vector field -Lifting a typed `VectorField` instead gives back a real `Hamiltonian`: +The lift of a `VectorField` is a `Hamiltonian`, with the same dependence on time and on the +variable as the vector field: ```@example main XV = VectorField(x -> [x[2], -x[1]]) HV = Lift(XV) -HV isa AbstractHamiltonian +HV isa AbstractHamiltonian, HV(x, p) ``` +## Time and variable + +For a vector field $X(t, x)$, pass `is_autonomous=false`, and the lift is called as +$H(t, x, p)$. With $X(t, x) = (t x_2, -x_1)$, $H(2, x, p) = 3 \times 4 + 4 \times (-1) = 8$: + ```@example main -HV([1.0, 2.0], [3.0, 4.0]) +Ht = Lift((t, x) -> [t * x[2], -x[1]]; is_autonomous=false) +Ht(2.0, x, p) ``` -## Non-autonomous and variable forms +For a vector field $X(x, v)$, pass `is_variable=true`, and the lift is called as +$H(x, p, v)$. With $X(x, v) = (x_2 + v, -x_1)$, $H(x, p, 1) = 3 \times 3 + 4 \times (-1) = 5$: ```@example main -Xt(t, x) = [t * x[2], -x[1]] -Ht = Lift(Xt; is_autonomous=false) -Ht(2.0, [1.0, 2.0], [3.0, 4.0]) # H(t, x, p) +Hv = Lift((x, v) -> [x[2] + v, -x[1]]; is_variable=true) +Hv(x, p, 1.0) ``` ```@example main -Xv(x, v) = [x[2] + v, -x[1]] -Hv = Lift(Xv; is_variable=true) -Hv([1.0, 2.0], [3.0, 4.0], 1.0) # H(x, p, v) +@assert H(x, p) == 2 && HV(x, p) == 2 && Ht(2.0, x, p) == 8 && Hv(x, p, 1.0) == 5 # hide +@assert H isa Function && !(H isa AbstractHamiltonian) && HV isa AbstractHamiltonian # hide +nothing # hide ``` -## Which one do I get - -| You lift | You get | Signature | +| You lift | You get | Called as | | --- | --- | --- | -| `f::Function` | `OptimalControl.LiftedHamiltonianFunction` (`<: Function`) | `h(x,p)`, `h(t,x,p)`, `h(x,p,v)`, `h(t,x,p,v)` depending on the keywords | -| `X::AbstractVectorField` | `Hamiltonian` | same call signatures, inherited from `X`'s own traits | - -!!! warning "`Lift(f::Function)` is not an `AbstractHamiltonian`" +| a function `X` | a function (`OptimalControl.LiftedHamiltonianFunction`) | `H(x, p)`, `H(t, x, p)`, `H(x, p, v)` or `H(t, x, p, v)`, from the keywords | +| a `VectorField` | a `Hamiltonian` | the same, from the traits of the vector field | - `OptimalControl.LiftedHamiltonianFunction <: Function` only, **not** `<: AbstractHamiltonian` — a change - from v2.0, where the plain-function form and the typed-`VectorField` form both produced the - same kind of object. Any `isa`/`<:` test against the old hierarchy is now quietly wrong: +## The flow of a lift - ```@example main - H = Lift(X) # X::Function - H isa AbstractHamiltonian - ``` +The Hamiltonian flow of $H_X$ integrates $\dot x = X(x)$ together with the adjoint equation +$\dot p = -X'(x)^{\top} p$. For $X(x) = -x$, this gives $x(t) = e^{-t} x_0$ and +$p(t) = e^{t} p_0$. A `Hamiltonian` is needed to build the flow (see +[From Hamiltonians](@ref flows-from-hamiltonians)), so lift a `VectorField`, or wrap the +lift of a function in `Hamiltonian`: - Only the plain-`Function` overload changed — `Lift(X::AbstractVectorField)` still returns a - `Hamiltonian`, confirmed above. See [Migration](@ref migration) for the full list of - silent v2.0 → v2.1 semantics changes. +```@example main +f = Flow(Lift(VectorField(x -> -x))) +xf, pf = f(0, x, p, 1) +``` -## What you can do with it +```@example main +@assert isapprox(xf, exp(-1) * x; rtol=1e-6) && isapprox(pf, exp(1) * p; rtol=1e-6) # hide +@assert all(Flow(Hamiltonian(Lift(x -> -x)))(0, x, p, 1) .≈ (xf, pf)) # hide +nothing # hide +``` -Feed a lift straight into [`Poisson`](@ref) (see -[The bridge identity](@ref geometry-overview)), or into [`Flow`](@ref) to integrate the -associated Hamiltonian system — see -[From Hamiltonians](@ref flows-from-hamiltonians). +The lift is also the bridge between the two brackets: +$\{H_X, H_Y\} = H_{[X, Y]}$ (see [The bridge identity](@ref geometry-overview-bridge)). -## A trap to know about +## A Hamiltonian vector field cannot be lifted -Lifting a `HamiltonianVectorField` doesn't work — it already lives on the cotangent space, so -there's nothing left to lift: +A `HamiltonianVectorField` already lives on the cotangent space, so `Lift` rejects it. The +message names `ad` instead of `Lift` +([CTLie#41](https://github.com/control-toolbox/CTLie.jl/issues/41)): ```@repl main -hvf = HamiltonianVectorField((x, p) -> (p, -x)); try # hide -Lift(hvf) +Lift(HamiltonianVectorField((x, p) -> (p, -x))) catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -The message talks about `ad`, not `Lift` — both operations share the same internal guard -against `HamiltonianVectorField` operands, so the wording doesn't adapt to which one triggered -it. Harmless, but don't be thrown by it: the operation that actually failed is `Lift`. - ## See also -- [Overview](@ref geometry-overview) — the bridge identity this page is one half of. -- [Poisson bracket](@ref geometry-poisson) — what a lift is usually fed into. -- [From Hamiltonians](@ref flows-from-hamiltonians) — turning a lift into a - flow. +- [Geometry overview](@ref geometry-overview): the summary of the operations. +- [Poisson bracket](@ref geometry-poisson): the brackets of lifts. +- [From Hamiltonians](@ref flows-from-hamiltonians): flows built from a Hamiltonian. From 28b92ee44da14dc49906f6a278cff6d38e8bffb6 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:09:13 +0200 Subject: [PATCH 39/57] =?UTF-8?q?docs(ad):=20analytic=20Lie=20derivatives?= =?UTF-8?q?=20and=20brackets=20asserted,=20time=20and=20variable=20forms,?= =?UTF-8?q?=20=E2=88=82=E2=82=9C=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/src/geometry/ad.md | 141 ++++++++++++++++++++++++++-------------- 1 file changed, 92 insertions(+), 49 deletions(-) diff --git a/docs/src/geometry/ad.md b/docs/src/geometry/ad.md index 559dc315c..555590f47 100644 --- a/docs/src/geometry/ad.md +++ b/docs/src/geometry/ad.md @@ -1,76 +1,113 @@ # [Lie derivative and Lie bracket](@id geometry-ad) -`ad(X, foo)` is one function with two meanings, chosen by what `foo` returns. +`ad(X, g)` differentiates along a vector field $X$. It computes the Lie derivative of $g$ when +$g$ returns a scalar, and the Lie bracket $[X, g]$ when $g$ returns a vector. ```@example main using OptimalControl ``` -## One function, two meanings - -- `foo` scalar-valued → the **Lie derivative** of `foo` along `X`. -- `foo` vector-valued → the **Lie bracket** of `X` and `foo`. - ## Lie derivative -For a vector field $X$ and a scalar function $f$, +The **Lie derivative** of a function $f : \mathbb{R}^n \to \mathbb{R}$ along a vector field +$X$ is the derivative of $f$ in the direction of $X$: ```math -(\mathcal{L}_X f)(x) = f'(x) \cdot X(x). +(\mathcal{L}_X f)(x) = f'(x) \cdot X(x) = \sum_{i=1}^n \frac{\partial f}{\partial x_i}(x)\, X_i(x). ``` +For the harmonic oscillator, $X(x) = (x_2, -x_1)$, and its energy +$f(x) = x_1^2 + x_2^2$, $\mathcal{L}_X f = 2x_1 x_2 - 2x_2 x_1 = 0$: the energy is constant +along the trajectories. + ```@example main X(x) = [x[2], -x[1]] -f(x) = x[1]^2 + x[2]^2 # energy of the harmonic oscillator +f(x) = x[1]^2 + x[2]^2 Xf = ad(X, f) Xf([1.0, 2.0]) ``` -Energy is conserved along $X$'s flow, so the Lie derivative vanishes everywhere. - ## Lie bracket -For two vector fields $X, Y$, +The **Lie bracket** of two vector fields $X$ and $Y$ is the vector field ```math -[X, Y](x) = J_Y(x)\,X(x) - J_X(x)\,Y(x), +[X, Y](x) = Y'(x) \cdot X(x) - X'(x) \cdot Y(x), ``` -where $J$ denotes the Jacobian. +where $X'(x)$ is the Jacobian matrix of $X$ at $x$. With $Y(x) = (x_1^2, x_2^2)$, +$Y' X = (2x_1 x_2, -2x_1 x_2)$ and $X' Y = (x_2^2, -x_1^2)$, so +$[X, Y](1, 2) = (4 - 4, -4 + 1) = (0, -3)$: ```@example main Y(x) = [x[1]^2, x[2]^2] + Z = ad(X, Y) Z([1.0, 2.0]) ``` -## Typed operands +```@example main +@assert Xf([1.0, 2.0]) == 0 && Z([1.0, 2.0]) ≈ [0, -3] # hide +nothing # hide +``` + +## Typed operands and nested brackets -Wrapping the inputs as `VectorField`s makes `ad` return a `VectorField` too — so it **nests**: +On `VectorField`s, `ad` returns a `VectorField`, which can be bracketed again. The bracket +$[[X, Y], Y]$ at $x = (1, 2)$ is $(4, -2)$: ```@example main XV = VectorField(x -> [x[2], -x[1]]) YV = VectorField(x -> [x[1]^2, x[2]^2]) ZV = ad(XV, YV) -typeof(ZV) +ZV isa VectorField, ad(ZV, YV)([1.0, 2.0]) ``` ```@example main -ZZV = ad(ZV, YV) # ad(ad(X, Y), Y) — a second-order bracket -ZZV([1.0, 2.0]) +@assert ZV isa VectorField && ad(ZV, YV)([1.0, 2.0]) ≈ [4, -2] # hide +nothing # hide ``` -## Non-autonomous and variable fields +The macro [`@Lie`](@ref geometry-lie-macro) writes the same bracket `@Lie [[XV, YV], YV]`. -`is_autonomous=` and `is_variable=` work exactly as on [`Lift`](@ref); both operands must agree, -or you get a `PreconditionError` naming both traits: +## Time and variable + +The derivatives are taken with respect to $x$ only. For a vector field $X(t, x)$ and a +function $f(t, x)$, pass `is_autonomous=false`. With $X(t, x) = (t + x_2, -x_1)$ and +$f(t, x) = t + x_1^2 + x_2^2$, $\mathcal{L}_X f = 2x_1(t + x_2) - 2x_2 x_1 = 2 t x_1$, which +is $2$ at $t = 1$, $x = (1, 2)$: + +```@example main +Xt(t, x) = [t + x[2], -x[1]] +ft(t, x) = t + x[1]^2 + x[2]^2 + +ad(Xt, ft; is_autonomous=false)(1.0, [1.0, 2.0]) +``` + +For a vector field $X(x, v)$ and a function $f(x, v)$, pass `is_variable=true`. With +$X(x, v) = (x_2 + v, -x_1)$ and $f(x, v) = x_1^2 + x_2^2 + v$, +$\mathcal{L}_X f = 2 x_1 v$: + +```@example main +Xv(x, v) = [x[2] + v, -x[1]] +fv(x, v) = x[1]^2 + x[2]^2 + v + +ad(Xv, fv; is_variable=true)([1.0, 2.0], 1.0) +``` + +```@example main +@assert ad(Xt, ft; is_autonomous=false)(1.0, [1.0, 2.0]) ≈ 2 # hide +@assert ad(Xv, fv; is_variable=true)([1.0, 2.0], 1.0) ≈ 2 # hide +nothing # hide +``` + +Typed operands carry these traits, and must agree: ```@repl main Xa = VectorField(x -> [x[2], -x[1]]); Xb = VectorField((t, x) -> [t + x[2], -x[1]]; is_autonomous=false); - try # hide ad(Xa, Xb) catch e # hide @@ -78,31 +115,47 @@ showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -## Partial time derivative +## [Partial time derivative](@id geometry-ad-time) -`∂ₜ` computes $\partial f / \partial t$ for a non-autonomous `f`, `VectorField`, or -`Hamiltonian` — its result is **always** `NonAutonomous`, regardless of whether the input -already was: +For a function $f(t, x, \ldots)$, `∂ₜ(f)` computes the partial derivative with respect to +time: + +```math +(\partial_t f)(t, x, \ldots) = \frac{\partial f}{\partial t}(t, x, \ldots). +``` + +With $g(t, x) = t^2 + x_1 x_2$, $\partial_t g = 2t = 6$ at $t = 3$: ```@example main g(t, x) = t^2 + x[1] * x[2] dg = ∂ₜ(g) -dg(3.0, [1.0, 2.0]) # ∂g/∂t = 2t = 6 +dg(3.0, [1.0, 2.0]) +``` + +`∂ₜ` also applies to a `VectorField` or a `Hamiltonian`. Its result always depends on time, +even when the input does not, and is then zero: + +```@example main +dXV = ∂ₜ(XV) +dXV(1.0, [1.0, 2.0]) ``` ```@example main -dXV = ∂ₜ(XV) # XV::VectorField from above — still NonAutonomous -typeof(dXV) +@assert dg(3.0, [1.0, 2.0]) ≈ 6 && dXV(1.0, [1.0, 2.0]) == [0, 0] # hide +nothing # hide ``` -## Errors you will meet +Together with the Poisson bracket, `∂ₜ` gives the time derivative of a function along an +extremal (see [Poisson bracket](@ref geometry-poisson-total)). + +## Errors | Situation | Exception | | --- | --- | -| `ad` given an `AbstractHamiltonian` | `IncorrectArgument` — points at `Poisson` | -| time/variable dependence mismatch between operands | `PreconditionError` | -| an `InPlace` operand | `NotImplemented` | -| a `HamiltonianVectorField` passed to `ad` | `NotImplemented` | +| a `Hamiltonian` given to `ad` | `IncorrectArgument`, pointing to `Poisson` | +| operands with different dependences on time or on the variable | `PreconditionError` | +| an in-place `VectorField` | `NotImplemented` | +| a `HamiltonianVectorField` | `NotImplemented` | ```@repl main H = Hamiltonian((x, p) -> x[1] + p[1]); @@ -113,8 +166,6 @@ showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -`ad` also refuses an `InPlace` operand: - ```@repl main Xip = VectorField((dx, x) -> (dx .= [x[2], -x[1]]); is_inplace=true); try # hide @@ -124,17 +175,9 @@ showerror(IOContext(stdout, :color => false), e) # hide end # hide ``` -## Coming from v2.0 - -| v2.0 | v2.1 | -| --- | --- | -| `Lie(X, f)` | `ad(X, f)` | -| `X ⋅ f` | `ad(X, f)` — **no operator replacement**, `⋅` is gone | - -See [Migrating to v2.1](@ref migration) for the throwing shim on `Lie`. - ## See also -- [Poisson bracket](@ref geometry-poisson) — the Hamiltonian-side counterpart, linked by - `Poisson(Lift(X), Lift(Y)) ≈ Lift(ad(X, Y))`. -- [The `@Lie` macro](@ref geometry-lie-macro) — `ad`/`Poisson` with bracket notation. +- [Poisson bracket](@ref geometry-poisson): the bracket of Hamiltonians, and + $\{H_X, H_Y\} = H_{[X, Y]}$. +- [The `@Lie` macro](@ref geometry-lie-macro): the bracket notation. +- [AD backend](@ref geometry-ad-backend): how the derivatives are computed. From 67c0c78fac18202fcf7383c0ad8d2989fee40fac Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:09:13 +0200 Subject: [PATCH 40/57] docs(poisson): v2.0.4 properties and total time derivative restored and checked, singular control derived Co-Authored-By: Claude Opus 5.5 --- docs/src/geometry/poisson.md | 178 ++++++++++++++++++++++++++--------- 1 file changed, 136 insertions(+), 42 deletions(-) diff --git a/docs/src/geometry/poisson.md b/docs/src/geometry/poisson.md index bf7c2bc62..2301e6f25 100644 --- a/docs/src/geometry/poisson.md +++ b/docs/src/geometry/poisson.md @@ -1,102 +1,196 @@ # [Poisson bracket](@id geometry-poisson) -For two Hamiltonians $H, G$, +For two functions $H, G : \mathbb{R}^n \times (\mathbb{R}^n)^* \to \mathbb{R}$, the +**Poisson bracket** is ```math -\{H, G\}(x,p) = \nabla_p H \cdot \nabla_x G - \nabla_x H \cdot \nabla_p G. +\{H, G\}(x, p) = \sum_{i=1}^n \left( \frac{\partial H}{\partial p_i} \frac{\partial G}{\partial x_i} +- \frac{\partial H}{\partial x_i} \frac{\partial G}{\partial p_i} \right) += \nabla_p H \cdot \nabla_x G - \nabla_x H \cdot \nabla_p G. ``` ```@example main using OptimalControl +using OrdinaryDiffEqTsit5 ``` -## On plain functions +## Computing a bracket + +With $H(x, p) = p_1 x_2 + p_2 x_1$ and $G(x, p) = x_1^2 + p_2^2$, +$\nabla_p H = (x_2, x_1)$, $\nabla_x G = (2x_1, 0)$, $\nabla_x H = (p_2, p_1)$ and +$\nabla_p G = (0, 2p_2)$, so $\{H, G\} = 2x_1 x_2 - 2p_1 p_2$, which is $-20$ at +$x = (1, 2)$, $p = (3, 4)$: ```@example main H(x, p) = p[1] * x[2] + p[2] * x[1] G(x, p) = x[1]^2 + p[2]^2 -B = Poisson(H, G) x, p = [1.0, 2.0], [3.0, 4.0] -B(x, p) +Poisson(H, G)(x, p) ``` -Antisymmetry, $\{H,G\} = -\{G,H\}$, checked directly: +On `Hamiltonian`s, `Poisson` returns a `Hamiltonian`, which can be bracketed again: ```@example main -Poisson(H, G)(x, p) + Poisson(G, H)(x, p) -``` +HT = Hamiltonian(H) +GT = Hamiltonian(G) -## On typed Hamiltonians +B = Poisson(HT, GT) +B isa Hamiltonian, Poisson(B, GT)(x, p) # {{H, G}, G} +``` -Wrapping the inputs as `Hamiltonian`s makes `Poisson` return a `Hamiltonian` too — it **nests**: +For functions of $(t, x, p)$, pass `is_autonomous=false`, and for functions of $(x, p, v)$, +`is_variable=true`. The derivatives are taken with respect to $x$ and $p$ only, so adding +terms in $t$ does not change the bracket: ```@example main -HT = Hamiltonian(H) -GT = Hamiltonian(G) +Ht(t, x, p) = t + H(x, p) +Gt(t, x, p) = t^2 + G(x, p) -BT = Poisson(HT, GT) -typeof(BT) +Poisson(Ht, Gt; is_autonomous=false)(1.0, x, p) ``` ```@example main -BTT = Poisson(BT, GT) # {{H, G}, G} — a second-order bracket -BTT(x, p) +@assert Poisson(H, G)(x, p) ≈ -20 && Poisson(Ht, Gt; is_autonomous=false)(1.0, x, p) ≈ -20 # hide +@assert B isa Hamiltonian # hide +nothing # hide ``` -## The bridge to Lie brackets +## Properties + +The Poisson bracket is + +- **bilinear**: $\{aF + bG, K\} = a\{F, K\} + b\{G, K\}$ for scalars $a, b$; +- **antisymmetric**: $\{F, G\} = -\{G, F\}$; +- a **derivation** (Leibniz rule): $\{FG, K\} = F\{G, K\} + G\{F, K\}$; +- and it satisfies the **Jacobi identity**: + $\{\{F, G\}, K\} + \{\{K, F\}, G\} + \{\{G, K\}, F\} = 0$. -$\{H_X, H_Y\} = H_{[X,Y]}$ — lifting turns a Poisson bracket into a Lie bracket, and vice versa: +Checked at the point above, with a third function $K$: ```@example main -X(x) = [x[1]^2, x[2]^2] -Y(x) = [x[2], -x[1]] +K(x, p) = x[1] * p[1] * p[2] + x[2]^3 + +antisymmetry = Poisson(H, G)(x, p) + Poisson(G, H)(x, p) +leibniz = Poisson((x, p) -> H(x, p) * G(x, p), K)(x, p) - + (H(x, p) * Poisson(G, K)(x, p) + G(x, p) * Poisson(H, K)(x, p)) +jacobi = Poisson(Poisson(H, G), K)(x, p) + Poisson(Poisson(K, H), G)(x, p) + + Poisson(Poisson(G, K), H)(x, p) +antisymmetry, leibniz, jacobi +``` + +```@example main +@assert abs(antisymmetry) < 1e-10 && abs(leibniz) < 1e-10 && abs(jacobi) < 1e-10 # hide +nothing # hide +``` + +The bracket of two lifts is the lift of the Lie bracket, $\{H_X, H_Y\} = H_{[X, Y]}$ (see +[The bridge identity](@ref geometry-overview-bridge)). + +## [Time derivative along an extremal](@id geometry-poisson-total) + +The Hamiltonian vector field of $H$ is $\vec{H} = (\nabla_p H, -\nabla_x H)$. The Poisson +bracket is the Lie derivative along it: + +```math +\{H, G\} = \vec{H} \cdot G = \nabla_x G \cdot \nabla_p H - \nabla_p G \cdot \nabla_x H . +``` + +So along an integral curve of $\vec{H}$, that is, an extremal +$\dot x = \nabla_p H$, $\dot p = -\nabla_x H$, a function $G(t, x, p)$ varies as -Poisson(Lift(X), Lift(Y))(x, p), Lift(ad(X, Y))(x, p) +```math +\frac{\mathrm{d}}{\mathrm{d}t}\, G(t, x(t), p(t)) = \partial_t G + \{H, G\}: ``` -## Non-autonomous and variable forms +the partial time derivative, for the explicit dependence on $t$, plus the Poisson bracket, +for the motion along the flow. To check it, integrate the flow of a Hamiltonian that depends +on time, and compare the derivative of $G$ along it, by finite differences, with +$\partial_t G + \{H, G\}$: ```@example main -Ht(t, x, p) = t + p[1] * x[2] + p[2] * x[1] -Gt(t, x, p) = t^2 + x[1]^2 + p[2]^2 +Hn(t, x, p) = p[1] * x[2] + p[2]^2 / 2 + t * x[1] +Gn(t, x, p) = t * p[1] + x[1] * x[2] + +flow = Flow(Hamiltonian(Hn; is_autonomous=false); reltol=1e-12, abstol=1e-12) +G_along(t) = Gn(t, flow(0.0, x, p, t)...) -Bt = Poisson(Ht, Gt; is_autonomous=false) -Bt(1.0, x, p) +t, h = 0.5, 1e-4 +xt, pt = flow(0.0, x, p, t) +lhs = (G_along(t + h) - G_along(t - h)) / 2h +rhs = ∂ₜ(Gn)(t, xt, pt) + Poisson(Hn, Gn; is_autonomous=false)(t, xt, pt) +lhs, rhs ``` -## Application: singular controls +```@example main +@assert isapprox(lhs, rhs; rtol=1e-6) # hide +nothing # hide +``` + +In particular, an autonomous $H$ is constant along its extremals, since $\{H, H\} = 0$. + +## [Application: singular controls](@id geometry-poisson-singular) -For a pseudo-Hamiltonian $H = H_0 + u H_1$, if the switching function $H_1$ vanishes on an -interval (a singular arc), the control there is recovered from the iterated Poisson brackets +Take a system $\dot x = F_0(x) + u F_1(x)$, whose pseudo-Hamiltonian +$H = H_0 + u H_1$ is linear in $u$, with $H_i = \langle p, F_i(x) \rangle$, the lifts of +$F_i$. The control maximises $H$, so it is given by the sign of the **switching function** +$H_1$, except on an arc where $H_1$ vanishes: a **singular arc**. There, the time derivatives +of $H_1$ vanish too. By the formula above, with $H$ autonomous, ```math -H_{01} = \{H_0, H_1\}, \qquad H_{001} = \{H_0, H_{01}\}, \qquad H_{101} = \{H_1, H_{01}\}, +\frac{\mathrm{d}}{\mathrm{d}t} H_1 = \{H_0 + u H_1, H_1\} = H_{01}, \qquad +\frac{\mathrm{d}}{\mathrm{d}t} H_{01} = \{H_0 + u H_1, H_{01}\} = H_{001} + u\, H_{101}, ``` -giving +where $H_{01} = \{H_0, H_1\}$, $H_{001} = \{H_0, H_{01}\}$ and $H_{101} = \{H_1, H_{01}\}$. +On the arc, $H_1 = H_{01} = 0$, and the second derivative gives the **singular control** ```math -u_{\text{sing}} = -\frac{H_{001}}{H_{101}}, \qquad H_{101} \neq 0. +u_s = -\frac{H_{001}}{H_{101}}, \qquad \text{where } H_{101} \neq 0. ``` +For example, take the vehicle of [Singular control](@ref examples-singular-control): +$\dot x = \cos\theta$, $\dot y = \sin\theta + x$, $\dot\theta = u$, so +$H_0 = p_x \cos\theta + p_y (\sin\theta + x)$ and $H_1 = p_\theta$. Then + +```math +H_{01} = p_x \sin\theta - p_y \cos\theta, \qquad +H_{001} = -p_y \sin\theta, \qquad +H_{101} = p_x \cos\theta + p_y \sin\theta . +``` + +On the singular arc, $p_\theta = 0$ and $p_y = p_x \tan\theta$, and the formula gives +$u_s = \sin^2\theta$: + ```@example main -H0(x, p) = p[1] * x[2] + p[2] * (-x[1]) -H1(x, p) = p[2] +H0(q, p) = p[1] * cos(q[3]) + p[2] * (sin(q[3]) + q[1]) +H1(q, p) = p[3] H01 = Poisson(H0, H1) H001 = Poisson(H0, H01) H101 = Poisson(H1, H01) -H001(x, p), H101(x, p) +q = [0.3, -0.1, 0.5] +p_s = [1.7, 1.7 * tan(q[3]), 0] # a point where H₁ = H₀₁ = 0 +H1(q, p_s), H01(q, p_s), -H001(q, p_s) / H101(q, p_s), sin(q[3])^2 +``` + +```@example main +@assert abs(H01(q, p_s)) < 1e-12 # hide +@assert isapprox(-H001(q, p_s) / H101(q, p_s), sin(q[3])^2; atol=1e-12) # hide +pq = [1.7, 1.2, -0.4] # hide +@assert H01(q, pq) ≈ pq[1] * sin(q[3]) - pq[2] * cos(q[3]) # hide +@assert H001(q, pq) ≈ -pq[2] * sin(q[3]) # hide +@assert H101(q, pq) ≈ pq[1] * cos(q[3]) + pq[2] * sin(q[3]) # hide +nothing # hide ``` -See [Singular control](@ref examples-singular-control) for a complete application on a real -problem. +The example page integrates the extremal with this control and checks it against the direct +solution. -## A trap to know about +## Vector fields are not Hamiltonians -`Poisson` is not defined on vector fields — lift them first: +`Poisson` rejects a `VectorField`: lift it first, or use `ad` for the Lie bracket. ```@repl main XV = VectorField(x -> [x[2], -x[1]]); @@ -109,6 +203,6 @@ end # hide ## See also -- [Lie derivative and Lie bracket](@ref geometry-ad) — the vector-field-side counterpart. -- [The `@Lie` macro](@ref geometry-lie-macro) — `{H, G}` notation for this bracket. -- [Singular control](@ref examples-singular-control) — the worked application. +- [Lie derivative and Lie bracket](@ref geometry-ad): the brackets of vector fields, and `∂ₜ`. +- [The `@Lie` macro](@ref geometry-lie-macro): the notation `@Lie {H, G}`. +- [Singular control](@ref examples-singular-control): the singular arc above, solved. From f663c9def20c1a5edd9ce992f67bbf7152737cc7 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:09:13 +0200 Subject: [PATCH 41/57] =?UTF-8?q?docs(lie-macro):=20analytic=20brackets=20?= =?UTF-8?q?asserted,=20the=20parenthesis=20trap=20shown,=20scope=20require?= =?UTF-8?q?ment=20=E2=86=92=20CTLie#40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/src/geometry/lie-macro.md | 135 +++++++++++++++++++++------------ 1 file changed, 85 insertions(+), 50 deletions(-) diff --git a/docs/src/geometry/lie-macro.md b/docs/src/geometry/lie-macro.md index c8b03e0b9..906032e8d 100644 --- a/docs/src/geometry/lie-macro.md +++ b/docs/src/geometry/lie-macro.md @@ -1,19 +1,19 @@ # [The `@Lie` macro](@id geometry-lie-macro) -`@Lie` lets you write brackets the way you'd write them on paper: square brackets for a Lie -bracket, curly braces for a Poisson bracket. +`@Lie` writes brackets as on paper: square brackets for a Lie bracket of vector fields, +`@Lie [X, Y]`, which calls [`ad`](@ref geometry-ad), and curly braces for a Poisson bracket of +Hamiltonians, `@Lie {H, G}`, which calls [`Poisson`](@ref geometry-poisson). ```@example main using OptimalControl ``` -## Why a macro - -`@Lie [X, Y]` reads as $[X, Y]$; `@Lie {H, K}` reads as $\{H, K\}$ — both expand to a call to -[`ad`](@ref) or [`Poisson`](@ref) respectively. - ## Lie brackets +The fields $F_1(x) = (0, -x_3, x_2)$ and $F_2(x) = (x_3, 0, -x_1)$ generate the rotations +about the first two axes. Their bracket is $[F_1, F_2](x) = (x_2, -x_1, 0)$, a rotation about +the third axis: + ```@example main F1 = VectorField(x -> [0, -x[3], x[2]]) F2 = VectorField(x -> [x[3], 0, -x[1]]) @@ -22,47 +22,51 @@ F12 = @Lie [F1, F2] F12([1.0, 2.0, 3.0]) ``` -## Poisson brackets +Brackets nest. A linear vector field commutes with the radial field +$F_3(x) = x$, so $[[F_1, F_2], F_3] = 0$: ```@example main -H0(x, p) = p[1] * x[2] + p[2] * (-x[1]) -H1(x, p) = p[2] - -H01 = @Lie {H0, H1} -H01([1.0, 2.0], [3.0, 4.0]) -``` +F3 = VectorField(x -> x) -## Nesting - -```@example main -F3 = VectorField(x -> [x[1], x[2], x[3]]) F123 = @Lie [[F1, F2], F3] F123([1.0, 2.0, 3.0]) ``` ```@example main -H001 = @Lie {H0, {H0, H1}} -H001([1.0, 2.0], [3.0, 4.0]) +@assert F12([1.0, 2.0, 3.0]) ≈ [2, -1, 0] && F123([1.0, 2.0, 3.0]) ≈ [0, 0, 0] # hide +nothing # hide ``` -## Arithmetic and evaluation points +## Poisson brackets -`@Lie` brackets combine with ordinary arithmetic once evaluated: +With $H_0(x, p) = p_1 x_2 - p_2 x_1$ and $H_1(x, p) = p_2$, $\{H_0, H_1\} = -p_1$ and +$\{H_0, \{H_0, H_1\}\} = -p_2$: ```@example main -x = [1.0, 2.0, 3.0] # F1, F2 are 3-D vector fields, defined above -@Lie [F1, F2](x) + 4 * [F1, F2](x) +H0(x, p) = p[1] * x[2] - p[2] * x[1] +H1(x, p) = p[2] + +H01 = @Lie {H0, H1} +H001 = @Lie {H0, {H0, H1}} + +x, p = [1.0, 2.0], [3.0, 4.0] +H01(x, p), H001(x, p) +``` + +```@example main +@assert H01(x, p) ≈ -3 && H001(x, p) ≈ -4 # hide +nothing # hide ``` -**Parenthesise when you evaluate** — a trailing keyword binds to the macro, not to the call: -write `(@Lie [F, G](x))`, never `@Lie [F, G](x) atol=1e-6`, if you're combining the result with -anything else on the same line. +These iterated brackets give the control on a singular arc (see +[Poisson bracket](@ref geometry-poisson-singular)). ## Keywords -`is_autonomous=`, `is_variable=`, and `ad_backend=` all work exactly as on `ad`/`Poisson`, -inferred from typed `VectorField`/`Hamiltonian` operands or given explicitly for plain -functions: +On plain functions, give the dependence on time and on the variable with the keywords +`is_autonomous=` and `is_variable=`, after the brackets. On typed operands, the traits are +read from the operands. With $X(t, x) = (t + x_2, -x_1)$ and $Y(t, x) = (x_1, t x_2)$, +$[X, Y] = (t + x_2 - t x_2,\ x_1 - t x_1)$, which is $(1, 0)$ at $t = 1$, $x = (1, 2)$: ```@example main X(t, x) = [t + x[2], -x[1]] @@ -72,35 +76,66 @@ Z = @Lie [X, Y] is_autonomous=false Z(1.0, [1.0, 2.0]) ``` -## What it needs in scope +```@example main +@assert Z(1.0, [1.0, 2.0]) ≈ [1, 0] # hide +nothing # hide +``` -The macro's expansion emits fully qualified `CTLie.*` and `CTBase.Traits.*` names, so both -modules must be resolvable at the call site. `using OptimalControl` re-exports both module -aliases: +The keyword `ad_backend=` chooses the AD backend (see [AD backend](@ref geometry-ad-backend)). +Any other keyword, such as `autonomous=false`, raises an `IncorrectArgument` that lists the +accepted ones. ```@example main -CTLie, CTBase +err = try eval(:(@Lie [F1, F2] autonomous=false)); nothing catch e; e end # hide +@assert err isa CTBase.Exceptions.IncorrectArgument # hide +nothing # hide ``` -which is why they appear in `names(OptimalControl)` at all — not because you're expected to use -them directly, but because `@Lie`'s expansion needs them in scope. +## Evaluating a bracket in an expression -## A trap to know about +Inside a bracket expression, `[F1, F2](x)` evaluates the bracket at `x`, and the result +combines with ordinary arithmetic: -The old `autonomous=`/`variable=` keywords are rejected at macro-expansion time, not silently -accepted: +```@example main +y = [1.0, 2.0, 3.0] +@Lie [F1, F2](y) + 4 * [F1, F2](y) +``` -```julia -julia> @Lie [F1, F2] autonomous=false -ERROR: IncorrectArgument: @Lie: unknown keyword argument -Got autonomous -Expected is_autonomous, is_variable, or ad_backend -Context @Lie macro keyword parsing +The macro takes everything that follows it as its argument, commas included. Inside a function +call, put it in parentheses: `isapprox((@Lie [F1, F2])(y), [2, -1, 0])`. Without them, +`isapprox(@Lie [F1, F2](y), [2, -1, 0])` passes both arguments to the macro, and returns a +function instead of a Boolean, without an error. + +```@example main +@assert (@Lie [F1, F2](y) + 4 * [F1, F2](y)) ≈ [10, -5, 0] # hide +@assert isapprox((@Lie [F1, F2])(y), [2, -1, 0]) # hide +@assert !(isapprox(@Lie [F1, F2](y), [2, -1, 0]) isa Bool) # hide +nothing # hide +``` + +## `using OptimalControl` is needed + +The expansion of `@Lie` refers to the modules `CTLie` and `CTBase` by name, which +`using OptimalControl` brings into scope. With `import OptimalControl` or +`using OptimalControl: @Lie` only, the expanded code fails with an `UndefVarError` +([CTLie#40](https://github.com/control-toolbox/CTLie.jl/issues/40)). + +```@example main +m = Module() # hide +Core.eval(m, :(import OptimalControl)) # hide +err = try # hide + Core.eval(m, :((OptimalControl.@Lie [x -> [x[2], -x[1]], x -> x])([1.0, 2.0]))) # hide + nothing # hide +catch e # hide + e # hide +end # hide +@assert err isa UndefVarError # hide +nothing # hide ``` ## See also -- [Lie derivative and Lie bracket](@ref geometry-ad) -- [Poisson bracket](@ref geometry-poisson) -- [Singular control](@ref examples-singular-control) — where iterated `{...}` brackets are used - in practice. +- [Lie derivative and Lie bracket](@ref geometry-ad): `ad`, called by `@Lie [X, Y]`. +- [Poisson bracket](@ref geometry-poisson): `Poisson`, called by `@Lie {H, G}`. +- [Singular control](@ref examples-singular-control): iterated Poisson brackets on a complete + problem. From 612d680b7bf9790c4f12b961d4c04eb3e4f05254 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:09:13 +0200 Subject: [PATCH 42/57] docs(ad-backend): no indirect using, a real backend change, GPU backend constructed Co-Authored-By: Claude Opus 5.5 --- docs/src/geometry/ad-backend.md | 95 ++++++++++++++++----------------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/docs/src/geometry/ad-backend.md b/docs/src/geometry/ad-backend.md index 831a1491e..857dd6bd6 100644 --- a/docs/src/geometry/ad-backend.md +++ b/docs/src/geometry/ad-backend.md @@ -1,90 +1,89 @@ # [AD backend](@id geometry-ad-backend) +`ad`, `Poisson`, `∂ₜ` and `@Lie` compute their derivatives by automatic differentiation (AD); +`Lift` computes none. This page shows which AD backend they use, and how to change it. + ```@example main using OptimalControl ``` -## Everything here is AD-backed, except `Lift` - -[`ad`](@ref), [`Poisson`](@ref), [`∂ₜ`](@ref), and [`@Lie`](@ref) all differentiate under the -hood; [`Lift`](@ref) is purely algebraic and never touches a backend at all. - ## The default ```@example main dg_ad_backend() ``` -`DifferentiationInterface{CPU}` over `ForwardDiff`, the same default used throughout -`CTBase`/`CTLie`. +The default backend calls [DifferentiationInterface](https://github.com/JuliaDiff/DifferentiationInterface.jl) +with ForwardDiff, on the CPU. OptimalControl depends on both packages, so nothing else needs +to be installed. `describe` lists the options of the backend, and their defaults on the CPU +and on the GPU: -## Reading the current backend +::: details `describe(:di)` -`dg_ad_backend()` (above) always returns the backend that will be used when no `ad_backend=` -keyword is given. +```@example main +describe(:di) +``` -## Changing it globally +::: + +## Choosing a backend + +A backend is built with `CTBase.Differentiation.DifferentiationInterface`, whose option +`ad_backend` is a backend type of [ADTypes](https://github.com/SciML/ADTypes.jl). +DifferentiationInterface re-exports these types; add it to your environment to use them. For +example, ForwardDiff with a chunk size of one: ```@example main -using CTBase: Differentiation +using DifferentiationInterface: AutoForwardDiff -default_backend = dg_ad_backend() # keep the default, to restore it below -dg_ad_backend!(Differentiation.DifferentiationInterface()) -dg_ad_backend() +backend = CTBase.Differentiation.DifferentiationInterface(; ad_backend=AutoForwardDiff(chunksize=1)) ``` -## Changing it for one call - -Every operation in this section accepts its own `ad_backend=`, overriding the global setting -just for that call: +Every operation accepts `ad_backend=` for one call: ```@example main X(x) = [x[2], -x[1]] -f(x) = x[1]^2 + x[2]^2 +f(x) = x[1]^2 + x[2]^2 + x[1] -ad(X, f; ad_backend=Differentiation.DifferentiationInterface())([1.0, 2.0]) +ad(X, f; ad_backend=backend)([1.0, 2.0]) # 2x₁x₂ - 2x₂x₁ + x₂ ``` -## GPU - -A GPU-parameterized backend is constructed the same way, with the `GPU` strategy instead of -`CPU`: - -```julia -using CTBase: Differentiation, Strategies +`dg_ad_backend!` changes the default for the rest of the session. Keep the previous one to +restore it: -dg_ad_backend!(Differentiation.DifferentiationInterface{Strategies.GPU}()) +```@example main +previous = dg_ad_backend() +dg_ad_backend!(backend) +ad(X, f)([1.0, 2.0]) ``` -This block is not executed on this page — no CUDA-capable GPU is available in this development -environment or in CI, the same caveat as [GPU](@ref solve-gpu) on the solve side. - -## Introspection +```@example main +dg_ad_backend!(previous) +dg_ad_backend() +``` ```@example main -describe(:di) +@assert ad(X, f; ad_backend=backend)([1.0, 2.0]) ≈ 2 # hide +@assert ad(X, f)([1.0, 2.0]) ≈ 2 # hide +@assert dg_ad_backend() === previous # hide +nothing # hide ``` -## Restoring the default +## GPU -`dg_ad_backend!` is global and persists for the rest of the session — set it back when the -demonstration is over: +The GPU backend is built with the parameter `GPU`. Its default is Mooncake, since ForwardDiff +does not run on GPU arrays: ```@example main -dg_ad_backend!(default_backend) -dg_ad_backend() +CTBase.Differentiation.DifferentiationInterface{GPU}() ``` -## If nothing works - -If `DifferentiationInterface` (and a concrete AD package, such as `ForwardDiff`) isn't loaded, -the extension that actually performs the differentiation never arms, and calling `ad`, -`Poisson`, `∂ₜ`, or `@Lie` fails. `OptimalControl` loads `DifferentiationInterface` and -`ForwardDiff` itself, so this only bites if you're using `CTLie` standalone. +It differentiates functions of `CuArray`s, and needs CUDA and Mooncake to be loaded, as +`Flow(…; method=:gpu)` does (see [Flows overview](@ref flows-overview)). ## See also -- [Overview](@ref geometry-overview) — where each operation sits relative to AD. -- [GPU](@ref solve-gpu) — the same `CPU`/`GPU` strategy split on the solve side. -- [Flows overview](@ref flows-overview) — the `method=:cpu`/`:gpu` construction-time keyword on +- [Geometry overview](@ref geometry-overview): the operations that use this backend. +- [GPU](@ref solve-gpu): solving on a GPU. +- [Flows overview](@ref flows-overview): the `method=:cpu` and `method=:gpu` options of `Flow`. From fdefd9fd10ac2e2841c14e2b90d1375106ce3890 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:28:07 +0200 Subject: [PATCH 43/57] =?UTF-8?q?docs(energy):=20v2.0.4=20story,=20wagon?= =?UTF-8?q?=20and=20formulation=20restored,=20exact=20solution=20derived,?= =?UTF-8?q?=20p=E2=82=80=20=3D=20(12,=206)=20and=20J=20=3D=206=20asserted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/src/examples/double-integrator-energy.md | 176 ++++++++++++++---- 1 file changed, 135 insertions(+), 41 deletions(-) diff --git a/docs/src/examples/double-integrator-energy.md b/docs/src/examples/double-integrator-energy.md index 0ecf00c40..2a7dbfe49 100644 --- a/docs/src/examples/double-integrator-energy.md +++ b/docs/src/examples/double-integrator-energy.md @@ -1,8 +1,26 @@ # [Energy minimisation](@id examples-double-integrator-energy) -The double integrator — a unit mass sliding frictionlessly on a rail, acceleration as the -control — transferred between two rest states at minimal energy. The simplest problem on this -site, and the same one used to introduce the package on the home page. +A wagon moves along a rail, and we control its acceleration with a force $u$. Its state is +$x = (q, v)$, where $q$ is the position and $v$ the velocity. + +```@raw html + +``` + +The mass is one and there is no friction, so the dynamics are + +```math +\dot q(t) = v(t), \quad \dot v(t) = u(t), \quad u(t) \in \mathbb{R}, +``` + +the [double integrator](https://en.wikipedia.org/w/index.php?title=Double_integrator&oldid=1071399674). +We transfer the wagon from rest at $q = -1$, at $t_0 = 0$, to rest at $q = 0$, at $t_f = 1$, +with the least energy, $\frac12 \int_0^1 u(t)^2 \, \mathrm{d}t$. This is the problem of +[Your first problem](@ref getting-started-first-problem): this page solves it by the direct +and the indirect methods, and compares them with the exact solution. + +We use OptimalControl to define the problem, NLPModelsIpopt to solve it by a direct method, +OrdinaryDiffEqTsit5 and NonlinearSolve for the indirect method, and Plots to see the result. ```@example main using OptimalControl @@ -14,84 +32,160 @@ using Plots ## The problem -State $x=(q,v)$ — position and velocity — dynamics $\dot q = v,\ \dot v = u$, transfer from -$x(0)=(-1,0)$ to $x(1)=(0,0)$ minimising the control energy $\int_0^1 \tfrac12u(t)^2\,dt$. - -## Definition - -Character-identical to the problem on the home page — this is the same instance, so the two -should recognisably be the same problem: +```@raw html +
+
+``` ```@example main +t0, tf = 0, 1 +x0, xf = [-1, 0], [0, 0] + ocp = @def begin - t ∈ [0, 1], time - x ∈ R², state + t ∈ [t0, tf], time + x = (q, v) ∈ R², state u ∈ R, control - x(0) == [-1, 0] - x(1) == [0, 0] - ẋ(t) == [x₂(t), u(t)] - ∫( 0.5u(t)^2 ) → min + + x(t0) == x0 + x(tf) == xf + + ẋ(t) == [v(t), u(t)] + + 0.5∫(u(t)^2) → min end +nothing # hide ``` -```@example main -t0, tf = 0, 1 -x0, xf = [-1, 0], [0, 0] -nothing # hide +```@raw html +
+
+``` + +```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)), \\[0.5em] +& && x(0) = (-1, 0), \\[0.5em] +& && x(1) = (0, 0). +\end{aligned} ``` -## Direct solution +```@raw html +
+
+``` + +See [Abstract syntax (`@def`)](@ref modelling-abstract-syntax) for every form these lines can +take. + +## Direct method + +The direct method[^1] discretises the problem in time and solves the resulting nonlinear +program, here with Ipopt: + +```@example main +direct_sol = solve(ocp; display=false) +objective(direct_sol) +``` ```@example main -direct_sol = solve(ocp; grid_size=20, display=false) -plt = plot(direct_sol, :state, :control; label="Direct") +plt = plot(direct_sol; label="direct", size=(800, 600)) ``` -## Indirect solution +## The exact solution + +With the conventions of [Notation and conventions](@ref modelling-formulation-conventions) +($p^0 = -1$), the pseudo-Hamiltonian is + +```math +H(x, p, u) = p_1 v + p_2 u - \frac{u^2}{2}. +``` -The pseudo-Hamiltonian is $H(x,p,u) = p_1v + p_2u - \tfrac12u^2$; maximising over $u$ gives the -feedback law $u(x,p) = p_2$. +It is maximised by $u = p_2$. The adjoint equations, $\dot p_1 = 0$ and $\dot p_2 = -p_1$, give +$p_1 = a$ and $p_2 = b - at$, so the control is affine, $u(t) = b - at$. The final conditions, +$v(1) = b - a/2 = 0$ and $q(1) = -1 + b/2 - a/6 = 0$, give $a = 12$ and $b = 6$: + +```math +p(0) = (12, 6), \qquad u(t) = 6 - 12t, \qquad J = \frac12 \int_0^1 (6 - 12t)^2 \, \mathrm{d}t = 6. +``` + +## Indirect method + +The indirect method, [indirect simple shooting](@extref Tutorials tutorial-iss), computes the +extremal from its initial costate. The flow of the +Hamiltonian system, with the maximising control in feedback form, integrates $(x, p)$ from +$(x_0, p_0)$: ```@example main -u(x, p) = p[2] -f = Flow(ocp, u; saveat=range(t0, tf, 100), dense=false) +# maximising control, H(x, p, u) = p₁v + p₂u - u²/2 +u_max(x, p) = p[2] + +f = Flow(ocp, u_max) +nothing # hide ``` -The shooting function drives the flow's endpoint to the target: +The shooting function measures how far the flow from $(x_0, p_0)$ lands from the target: ```@example main -π_x((x, p)) = x -S(p0) = π_x(f(t0, x0, p0, tf)) - xf +S(p0) = f(t0, x0, p0, tf)[1] - xf +nothing # hide ``` +We solve $S(p_0) = 0$ with a Newton method, starting from the costate of the direct solution: + ```@example main nle!(s, p0, _) = (s .= S(p0)) -t = time_grid(direct_sol) p0_guess = costate(direct_sol)(t0) - prob = NonlinearProblem(nle!, p0_guess) shooting_sol = NonlinearSolve.solve(prob; show_trace=Val(false)) p0_sol = shooting_sol.u ``` +The residual is zero, and the initial costate is the exact one, $(12, 6)$: + ```@example main S(p0_sol) ``` ## Comparison +The extremal integrated from `p0_sol` is a solution of the problem, with its cost: + ```@example main indirect_sol = f((t0, tf), x0, p0_sol) -plot!(plt, indirect_sol, :state, :control; label="Indirect") +objective(direct_sol), objective(indirect_sol) +``` + +For the plot, the flow is built with `saveat`, so that the trajectory has 101 points (see +[Plotting a flow](@ref results-plot-flow)): + +```@example main +f_plot = Flow(ocp, u_max; saveat=range(t0, tf, 101), dense=false) +plot!(plt, f_plot((t0, tf), x0, p0_sol); label="indirect", linestyle=:dash) +``` + +The indirect cost is the exact value, $6$. The direct cost differs by the discretisation error +of the default grid. + +```@example main +@assert isapprox(p0_sol, [12, 6]; atol=1e-8) && maximum(abs, S(p0_sol)) < 1e-8 # hide +@assert isapprox(objective(indirect_sol), 6; atol=1e-8) # hide +@assert isapprox(objective(direct_sol), 6; rtol=1e-3) # hide +@assert isapprox(p0_guess, [12, 6]; rtol=1e-2) # hide +@assert maximum(abs(control(indirect_sol)(t) - (6 - 12t)) for t in 0:0.1:1) < 1e-8 # hide +nothing # hide ``` -The two solutions overlap: the shooting function's residual at `p0_sol` is numerically zero, -confirming the indirect solve reproduces the direct one. +## Notes -## See also +- [MINPACK.jl](@extref Tutorials Resolution-of-the-shooting-equation) can replace + NonlinearSolve to solve the shooting equations. +- [From an OCP](@ref flows-from-ocp) explains the flow of a problem, and + [Shooting](@ref flows-shooting) the shooting method. +- The [Goddard tutorial](@extref Tutorials tutorial-goddard) uses the same steps on a problem + with bang, singular and boundary arcs. +- [State constraint](@ref examples-state-constraint) adds a bound on the velocity or the + position to this problem. -- [From an OCP](@ref flows-from-ocp) — the Hamiltonian-flow construction used above, in full. -- [Shooting](@ref flows-shooting) — the shooting method in general. -- [Time minimisation (bang–bang)](@ref examples-double-integrator-time) — the same wagon, a - harder control constraint. +[^1]: J. T. Betts, *Practical methods for optimal control using nonlinear programming*, SIAM, Philadelphia, PA, 2001. From 2e3966e72df384510fa9525cabe99796566579ac Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:28:07 +0200 Subject: [PATCH 44/57] =?UTF-8?q?docs(time):=20wagon=20and=20formulation?= =?UTF-8?q?=20restored,=20switch=20detected=20from=20p=E2=82=82,=20Mayer?= =?UTF-8?q?=20transversality,=20(1,=201,=201,=202)=20asserted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/src/examples/double-integrator-time.md | 187 +++++++++++++++----- 1 file changed, 141 insertions(+), 46 deletions(-) diff --git a/docs/src/examples/double-integrator-time.md b/docs/src/examples/double-integrator-time.md index d209b87e3..2a403d07a 100644 --- a/docs/src/examples/double-integrator-time.md +++ b/docs/src/examples/double-integrator-time.md @@ -1,8 +1,19 @@ # [Time minimisation (bang–bang)](@id examples-double-integrator-time) -Same wagon as [Energy minimisation](@ref examples-double-integrator-energy), but transferred -**as fast as possible** instead of at minimal energy — a bounded control gives a bang–bang -optimal law with a single switch. +The wagon of [Energy minimisation](@ref examples-double-integrator-energy) again, with a +bounded force this time, $|u| \le 1$: we transfer it from rest at $q = -1$ to rest at $q = 0$ +**as fast as possible**. The dynamics are those of the double integrator, + +```math +\dot q(t) = v(t), \quad \dot v(t) = u(t), \quad u(t) \in [-1, 1], +``` + +with the limit conditions $x(0) = (-1, 0)$ and $x(t_f) = (0, 0)$, where $x = (q, v)$ and the +final time $t_f$ is free. + +```@raw html + +``` ```@example main using OptimalControl @@ -14,15 +25,16 @@ using Plots ## The problem -Same dynamics, $\dot q = v,\ \dot v = u$, now with $u \in [-1,1]$ and the final time $t_f$ free -— minimise $t_f$ instead of the control energy. +The final time is a variable of the problem, and the cost: -## Definition +```@raw html +
+
+``` ```@example main t0 = 0 -x0 = [-1, 0] -xf = [0, 0] +x0, xf = [-1, 0], [0, 0] ocp = @def begin tf ∈ R, variable @@ -39,84 +51,167 @@ ocp = @def begin tf → min end +nothing # hide ``` -## Direct solution +```@raw html +
+
+``` -```@example main -direct_sol = solve(ocp; grid_size=20, display=false) -plt = plot(direct_sol, :state, :control; label="Direct") +```math +\begin{aligned} +& \text{Minimise} && t_f \\[0.5em] +& \text{subject to} && \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} ``` -## Indirect solution +```@raw html +
+
+``` + +## Direct method + +We solve it with 200 time steps: -The pseudo-Hamiltonian $H(x,p,u) = p_1v + p_2u - 1$ is linear in $u$, so the maximising control -is **bang–bang**: $u(t) = \operatorname{sign}(p_2(t))$, with one switch from $u=+1$ to $u=-1$ at -a time $t_1$. +```@example main +direct_sol = solve(ocp; grid_size=200, display=false) +variable(direct_sol) +``` ```@example main -H(x, p, u) = p[1] * x[2] + p[2] * u - 1 +plt = plot(direct_sol; label="direct", size=(800, 600)) +``` -u_max = 1.0 -u_min = -1.0 +The control is $+1$, then $-1$: the wagon accelerates as much as it can, then brakes as much +as it can. -f_max = Flow(ocp, (x, p, v) -> u_max) -f_min = Flow(ocp, (x, p, v) -> u_min) +## The exact solution + +With the conventions of [Notation and conventions](@ref modelling-formulation-conventions), +the cost $t_f$ is a Mayer cost, so the pseudo-Hamiltonian is + +```math +H(x, p, u) = p_1 v + p_2 u, ``` -The free final time makes this a `NonFixed` flow: every call needs `variable=`. +where $p = (p_1, p_2)$ is the costate. It is linear in $u$, and maximised by +$u = \operatorname{sign}(p_2)$: the control is **bang–bang**, and $p_2$ is the **switching +function**. Since $\dot p_1 = 0$ and $\dot p_2 = -p_1$, $p_2$ is affine, so it changes sign at +most once, at a time $t_1$, from $u = +1$ to $u = -1$. + +On $[0, t_1]$, $v = t$ and $q = -1 + t^2/2$. On $[t_1, t_f]$, the wagon brakes, and stops at +$t_f = 2t_1$, at $q = -1 + t_1^2$. The target gives $t_1 = 1$ and $t_f = 2$. + +The switching condition $p_2(t_1) = 0$ gives $p_2(0) = p_1$. For a free final time with a Mayer +cost, the transversality condition is $\mathbf{H}(t_f) = 1$, that is, +$p_2(t_f) \times (-1) = 1$, with $v(t_f) = 0$. Since $p_2(t_f) = p_2(0) - 2p_1 = -p_1$, we get +$p_1 = 1$: + +```math +p(0) = (1, 1), \qquad t_1 = 1, \qquad t_f = 2. +``` + +## Indirect method + +The two arcs have their flows, with the control $+1$ and $-1$. The final time is a variable, +so it is passed to the flows by keyword, `variable=tf`: ```@example main -function shoot!(s, ξ) - p0 = ξ[1:2] - t1, tf = ξ[3], ξ[4] +f_max = Flow(ocp, (x, p, v) -> 1) +f_min = Flow(ocp, (x, p, v) -> -1) +nothing # hide +``` - x1, p1 = f_max(t0, x0, p0, t1; variable=tf) - xf_, pf = f_min(t1, x1, p1, tf; variable=tf) +The shooting function has four unknowns, $p_0$, $t_1$ and $t_f$, and four conditions: the +target, the switch, and the transversality condition: + +```@example main +H(x, p, u) = p[1] * x[2] + p[2] * u # pseudo-Hamiltonian, Mayer form - s[1:2] = xf_ - xf # target reached - s[3] = p1[2] # switching condition - s[4] = H(xf_, pf, u_min) # free final time transversality +function shoot!(s, ξ, _) + p0, t1, tf = ξ[1:2], ξ[3], ξ[4] + x1, p1 = f_max(t0, x0, p0, t1; variable=tf) + x2, p2 = f_min(t1, x1, p1, tf; variable=tf) + s[1:2] = x2 - xf # target + s[3] = p1[2] # switching function at t1 + s[4] = H(x2, p2, -1) - 1 # transversality, free final time return nothing end +nothing # hide ``` +The direct solution gives the initial guess: its costate at $t_0$, its final time, and the +switching time, where its switching function $p_2$ changes sign: + ```@example main -nle!(s, ξ, _) = shoot!(s, ξ) +tg = time_grid(direct_sol) +p_d = costate(direct_sol) -p0_guess = costate(direct_sol)(t0) -t1_guess = 0.9 * variable(direct_sol) +p0_guess = p_d(t0) +t1_guess = tg[findfirst(t -> p_d(t)[2] < 0, tg)] tf_guess = variable(direct_sol) ξ_guess = [p0_guess..., t1_guess, tf_guess] +``` + +We solve the shooting equations: -prob = NonlinearProblem(nle!, ξ_guess) +```@example main +prob = NonlinearProblem(shoot!, ξ_guess) shooting_sol = NonlinearSolve.solve(prob; show_trace=Val(false)) -p0_sol, t1_sol, tf_sol = - shooting_sol.u[1:2], shooting_sol.u[3], shooting_sol.u[4] +p0_sol, t1_sol, tf_sol = shooting_sol.u[1:2], shooting_sol.u[3], shooting_sol.u[4] ``` +These are the exact values, $p_0 = (1, 1)$, $t_1 = 1$ and $t_f = 2$, and the residual is zero: + ```@example main s = zeros(4) -shoot!(s, shooting_sol.u) +shoot!(s, shooting_sol.u, nothing) s ``` +```@example main +@assert isapprox(shooting_sol.u, [1, 1, 1, 2]; atol=1e-8) && maximum(abs, s) < 1e-8 # hide +@assert isapprox(t1_guess, 1; atol=2e-2) && isapprox(tf_guess, 2; atol=1e-3) # hide +nothing # hide +``` + ## Comparison -Concatenating the two constant-control flows at the switching time reconstructs the whole -trajectory: +The concatenation of the two flows at $t_1$ gives the whole extremal (see +[Multi-phase flows](@ref flows-multi-phase)): ```@example main φ = f_max * (t1_sol, f_min) indirect_sol = φ((t0, tf_sol), x0, p0_sol; variable=tf_sol) -plot!(plt, indirect_sol, :state, :control; label="Indirect") +state(indirect_sol)(tf_sol) +``` + +For the plot, the flows are built with `saveat`, so that the trajectory has 201 points (see +[Plotting a flow](@ref results-plot-flow)): + +```@example main +fine = (saveat=range(t0, tf_sol, 201), dense=false) +φ_plot = Flow(ocp, (x, p, v) -> 1; fine...) * (t1_sol, Flow(ocp, (x, p, v) -> -1; fine...)) +plot!(plt, φ_plot((t0, tf_sol), x0, p0_sol; variable=tf_sol); label="indirect", linestyle=:dash) +``` + +The two final times agree up to the discretisation error of the direct method: + +```@example main +variable(direct_sol), tf_sol ``` -## See also +## Notes -- [Multi-phase flows](@ref flows-multi-phase) — flow concatenation `*`, in general. -- [Shooting](@ref flows-shooting) — bang–bang shooting, worked in full detail from this exact - problem. -- [Energy minimisation](@ref examples-double-integrator-energy) — the same wagon, unbounded - control. +- [Shooting](@ref flows-shooting) solves this problem step by step, starting from the + multipliers of the direct solution. +- [MINPACK.jl](@extref Tutorials Resolution-of-the-shooting-equation) can replace + NonlinearSolve to solve the shooting equations. +- The [Goddard tutorial](@extref Tutorials tutorial-goddard) uses the same steps on a problem + with bang, singular and boundary arcs. From 7ab9e791154a9187c338a304ec3189a67f7fe9dc Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:28:07 +0200 Subject: [PATCH 45/57] =?UTF-8?q?docs(control-free):=20augmented=20system?= =?UTF-8?q?=20derived=20for=20both=20problems,=20=CF=89=20=3D=20=CF=80/2?= =?UTF-8?q?=20and=20p=E2=82=80=20=3D=20(0,=20=E2=88=922)=20asserted,=20exa?= =?UTF-8?q?ct=20overlay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/src/examples/control-free.md | 319 ++++++++++++++++++++---------- 1 file changed, 217 insertions(+), 102 deletions(-) diff --git a/docs/src/examples/control-free.md b/docs/src/examples/control-free.md index f5d5dd9da..a3a5328fb 100644 --- a/docs/src/examples/control-free.md +++ b/docs/src/examples/control-free.md @@ -1,8 +1,11 @@ # [Parameter estimation without a control](@id examples-control-free) -Two problems with no control anywhere — only a `variable` to fit to data. See -[No control](@ref modelling-without-control) for the modelling side of this; -this page is the full worked story, direct and indirect, for both. +A problem without a control optimises **constant parameters** of a dynamical system: the +parameters that make a model fit observed data, or the best parameters for a performance +criterion. This page solves two such problems, by the direct and the indirect methods: the +growth rate of an exponential model fitted to data, and the smallest pulsation of an +oscillator that reaches a target. [Control-free problems](@ref modelling-without-control) +explains how to write them. ```@example main using OptimalControl @@ -12,23 +15,25 @@ using NonlinearSolve using Plots ``` -## Growth-rate estimation +## [Growth rate](@id examples-control-free-growth) -### The problem +### [The problem](@id examples-control-free-growth-problem) -Fit $\dot x = \lambda x$, $x(0)=2$, to noisy synthetic data generated with the true rate -$\lambda=0.5$, by minimising the squared error over $t \in [0,2]$. +A quantity grows exponentially, $\dot x(t) = \lambda\, x(t)$, from $x(0) = 2$, at an unknown +rate $\lambda$. We observe it on $[0, 2]$, and the observations $x_{\text{obs}}$ are the model +with $\lambda = 0.5$, perturbed by a small oscillation. We estimate $\lambda$ by least +squares. The estimate is close to $0.5$, but not equal, because of the perturbation. -### Definition +```@raw html +
+
+``` ```@example main λ_true = 0.5 -x_true(t) = 2 * exp(λ_true * t) -data(t) = x_true(t) + 0.2 * sin(4π * t) +x_obs(t) = 2 * exp(λ_true * t) + 0.2 * sin(4π * t) -t0 = 0 -tf = 2 -x0 = 2 +t0, tf, x0 = 0, 2, 2 ocp_growth = @def begin λ ∈ R, variable @@ -39,95 +44,148 @@ ocp_growth = @def begin ẋ(t) == λ * x(t) - ∫((x(t) - data(t))^2) → min + ∫((x(t) - x_obs(t))^2) → min end +nothing # hide +``` + +```@raw html +
+
+``` + +```math +\begin{aligned} +& \text{Minimise} && \int_0^2 \big(x(t) - x_{\text{obs}}(t)\big)^2 \,\mathrm{d}t \\ +& \text{subject to} && \dot x(t) = \lambda\, x(t), \\[0.5em] +& && x(0) = 2, \\[0.5em] +& && \lambda \in \mathbb{R}. +\end{aligned} +``` + +```@raw html +
+
``` -### Direct solution +### [Direct method](@id examples-control-free-growth-direct) + +With a coarse grid of 20 steps: ```@example main growth_sol = solve(ocp_growth; grid_size=20, display=false) -println("estimated λ = ", variable(growth_sol)) +variable(growth_sol), objective(growth_sol) ``` ```@example main -t_grid = time_grid(growth_sol) -plt_growth = plot(growth_sol, :state; label="Direct") -plot!( - plt_growth, t_grid, data.(t_grid); - line=:dot, label="Data", color=:black, -) +t_grid = range(t0, tf, 201) +plt_growth = plot(growth_sol, :state; label="direct", size=(800, 300)) +plot!(plt_growth, t_grid, x_obs.(t_grid); line=:dot, color=:black, label="data") ``` -`model(growth_sol)` gives back the underlying problem the solution was computed from — the same -object `ocp_growth` was `build`-ed into: +### [Indirect method](@id examples-control-free-growth-indirect) -```@example main -model(growth_sol) === ocp_growth +The problem has no control, so the maximum principle gives directly the **Hamiltonian**, with +$p^0 = -1$ (see [Notation and conventions](@ref modelling-formulation-conventions)): + +```math +H(t, x, p, \lambda) = p\, \lambda x - \big(x - x_{\text{obs}}(t)\big)^2 . ``` -### Indirect solution +The parameter $\lambda$ is treated as a state with zero dynamics, $\dot\lambda = 0$, and its +own costate $p_\lambda$. The **augmented system**, with state $(x, \lambda)$ and costate +$(p, p_\lambda)$, is + +```math +\begin{aligned} +\dot x &= \frac{\partial H}{\partial p} = \lambda x, & +\dot p &= -\frac{\partial H}{\partial x} = -p \lambda + 2\big(x - x_{\text{obs}}(t)\big), \\ +\dot \lambda &= 0, & +\dot p_\lambda &= -\frac{\partial H}{\partial \lambda} = -p\, x . +\end{aligned} +``` -The pseudo-Hamiltonian is $H(t,x,p,\lambda) = p\lambda x - (x - x_{\text{data}}(t))^2$; since -there is no control, the flow needs no law: +The final state is free, so $p(t_f) = 0$. The parameter is free too, and does not appear in +the cost, so $p_\lambda(t_0) = 0$ and $p_\lambda(t_f) = 0$, that is, -```@example main -f_growth = Flow(ocp_growth) +```math +p_\lambda(t_f) = -\int_{t_0}^{t_f} \frac{\partial H}{\partial \lambda}\big(t, x(t), p(t), \lambda\big)\,\mathrm{d}t = 0 . ``` -The costate $p$ and the variable's own costate $p_\lambda$ must both vanish at $t_f$ -(transversality, with $p_\lambda(t_0)=0$): +The flow of the problem integrates the system in $(x, p)$, with $\lambda$ passed by keyword. +With `variable_costate=true`, it also integrates $p_\lambda$ from $0$, and returns +$(x(t_f), p(t_f), p_\lambda(t_f))$ (see +[The costate of the variable](@ref flows-from-ocp-variable-costate)): ```@example main -function shoot_growth!(s, ξ) - p0, λ = ξ[1], ξ[2] - _, p_tf, pλ_tf = f_growth( - t0, x0, p0, tf; variable=λ, variable_costate=true - ) - s[1] = p_tf - s[2] = pλ_tf +f_growth = Flow(ocp_growth) + +function shoot_growth!(s, ξ, _) + p0, λ = ξ + _, p_tf, pλ_tf = f_growth(t0, x0, p0, tf; variable=λ, variable_costate=true) + s[1] = p_tf # free final state + s[2] = pλ_tf # free parameter return nothing end +nothing # hide ``` +The direct solution gives the initial guess: + ```@example main -nle_growth!(s, ξ, _) = shoot_growth!(s, ξ) +ξ_guess = [costate(growth_sol)(t0), variable(growth_sol)] +prob_growth = NonlinearProblem(shoot_growth!, ξ_guess) +p0_growth, λ_sol = NonlinearSolve.solve(prob_growth; show_trace=Val(false)).u +``` + +### [Comparison](@id examples-control-free-growth-comparison) -p0_guess = costate(growth_sol)(t0) -λ_guess = variable(growth_sol) +The two estimates differ in the third decimal: the direct method solves a discretised +problem, on a grid of 20 steps, and the indirect method the problem itself. -prob_growth = NonlinearProblem(nle_growth!, [p0_guess, λ_guess]) -shoot_sol_growth = NonlinearSolve.solve(prob_growth; show_trace=Val(false)) -p0_sol_growth, λ_sol = shoot_sol_growth.u +```@example main +variable(growth_sol), λ_sol ``` -### Comparison +For the plot, the flow is built with `saveat`, so that the trajectory has 201 points (see +[Plotting a flow](@ref results-plot-flow)): ```@example main -indirect_growth = f_growth((t0, tf), x0, p0_sol_growth; variable=λ_sol) -plot!( - plt_growth, indirect_growth, :state; - label="Indirect", linestyle=:dash, -) +f_growth_plot = Flow(ocp_growth; saveat=range(t0, tf, 201), dense=false) +indirect_growth = f_growth_plot((t0, tf), x0, p0_growth; variable=λ_sol) +plot!(plt_growth, indirect_growth, :state; label="indirect", linestyle=:dash) ``` -The direct and indirect estimates of $\lambda$ agree, and both curves fit the noisy data -equally well. +The indirect estimate minimises the least-squares error: the error, computed by quadrature +for the model $2e^{\lambda t}$, is larger on both sides of $\lambda_{\text{sol}}$. -## Harmonic-oscillator pulsation +```@example main +ts = range(t0, tf, 2001); w = fill(step(ts), length(ts)); w[[1, end]] ./= 2 # hide +J(λ) = sum(w .* (2 .* exp.(λ .* ts) .- x_obs.(ts)) .^ 2) # hide +@assert J(λ_sol) < J(λ_sol - 1e-3) && J(λ_sol) < J(λ_sol + 1e-3) # hide +@assert isapprox(λ_sol, variable(growth_sol); atol=5e-3) && abs(λ_sol - 0.5) < 1e-2 # hide +s = zeros(2); shoot_growth!(s, [p0_growth, λ_sol], nothing) # hide +@assert maximum(abs, s) < 1e-8 # hide +nothing # hide +``` -### The problem +## [Pulsation of an oscillator](@id examples-control-free-harmonic) -$\ddot q = -\omega^2 q$, $q(0)=1$, $\dot q(0)=0$, $q(1)=0$ — find the pulsation $\omega$ -minimising $\omega^2$. The analytical solution is $\omega=\pi/2$. +### [The problem](@id examples-control-free-harmonic-problem) -### Definition +An oscillator $\ddot q = -\omega^2 q$ starts at rest at $q = 1$. We look for the smallest +pulsation $\omega$ for which it reaches $q = 0$ at $t = 1$. The solution is +$q(t) = \cos(\omega t)$, so $\cos\omega = 0$, and the smallest pulsation is $\omega = \pi/2$, +with $\omega^2 = \pi^2/4 \approx 2.4674$ and $q(t) = \cos(\pi t/2)$. + +```@raw html +
+
+``` ```@example main -q0 = 1 -v0 = 0 -t0h = 0 -tfh = 1 +q0, v0 = 1, 0 +t0h, tfh = 0, 1 ocp_harmonic = @def begin ω ∈ R, variable @@ -136,76 +194,133 @@ ocp_harmonic = @def begin q(t0h) == q0 v(t0h) == v0 - q(tfh) == 0.0 + q(tfh) == 0 ẋ(t) == [v(t), -ω^2 * q(t)] ω^2 → min end +nothing # hide ``` -### Direct solution +```@raw html +
+
+``` + +```math +\begin{aligned} +& \text{Minimise} && \omega^2 \\ +& \text{subject to} && \ddot q(t) = -\omega^2 q(t), \\[0.5em] +& && q(0) = 1, \quad \dot q(0) = 0, \\[0.5em] +& && q(1) = 0. +\end{aligned} +``` + +```@raw html +
+
+``` + +### [Direct method](@id examples-control-free-harmonic-direct) ```@example main harmonic_sol = solve(ocp_harmonic; grid_size=20, display=false) -println( - "estimated ω = ", variable(harmonic_sol), - " (expected π/2 ≈ ", π / 2, ")", -) +variable(harmonic_sol), objective(harmonic_sol) ``` +The direct solution is close to the exact one, $q(t) = \cos(\pi t/2)$ and +$v(t) = -\frac{\pi}{2}\sin(\pi t/2)$: + ```@example main -plot(harmonic_sol, :state; label="Direct") +plt_harmonic = plot(harmonic_sol, :state; label="direct", size=(800, 300)) +plot!(plt_harmonic, t -> cos(π * t / 2), 0, 1; subplot=1, label="exact", color=:black, line=:dot) +plot!(plt_harmonic, t -> -π / 2 * sin(π * t / 2), 0, 1; subplot=2, label="exact", color=:black, line=:dot) +``` + +### [Indirect method](@id examples-control-free-harmonic-indirect) + +The Hamiltonian has no integral term, since the cost is a Mayer cost: + +```math +H(x, p, \omega) = p_1 v - p_2\, \omega^2 q . ``` -### Indirect solution +With the state $(q, v, \omega)$ and the costate $(p_1, p_2, p_\omega)$, the augmented system is + +```math +\begin{aligned} +\dot q &= v, & \dot p_1 &= -\frac{\partial H}{\partial q} = \omega^2 p_2, \\ +\dot v &= -\omega^2 q, & \dot p_2 &= -\frac{\partial H}{\partial v} = -p_1, \\ +\dot \omega &= 0, & \dot p_\omega &= -\frac{\partial H}{\partial \omega} = 2\omega\, q\, p_2 . +\end{aligned} +``` + +The final velocity is free, so $p_2(t_f) = 0$. For the Mayer cost $g(\omega) = \omega^2$, the +transversality condition of the parameter is $p_\omega(t_f) = p^0 g'(\omega) = -2\omega$, +with $p_\omega(t_0) = 0$. + +At the solution, $\omega = \pi/2$, and the adjoint equations with $p_2(1) = 0$ give +$p_2 = -A\cos(\pi t/2)$ and $p_1 = -\dot p_2 = -A\frac{\pi}{2}\sin(\pi t/2)$. Then +$p_\omega(1) = 2\omega \int_0^1 q\,p_2 = -\omega A$, so $A = 2$, and the initial costate is +$p(0) = (0, -2)$. ```@example main f_harmonic = Flow(ocp_harmonic) -function shoot_harmonic!(s, ξ) +function shoot_harmonic!(s, ξ, _) p0, ω = ξ[1:2], ξ[3] - x_tf, p_tf, pω_tf = f_harmonic( - t0h, [q0, v0], p0, tfh; variable=ω, variable_costate=true - ) - s[1] = x_tf[1] # q(tf) = 0 - s[2] = p_tf[2] # free final velocity - s[3] = pω_tf + 2ω # Mayer-cost transversality + x_tf, p_tf, pω_tf = f_harmonic(t0h, [q0, v0], p0, tfh; variable=ω, variable_costate=true) + s[1] = x_tf[1] # q(tf) = 0 + s[2] = p_tf[2] # free final velocity + s[3] = pω_tf + 2ω # transversality of the parameter return nothing end + +ξ_guess_h = [costate(harmonic_sol)(t0h)..., variable(harmonic_sol)] +prob_harmonic = NonlinearProblem(shoot_harmonic!, ξ_guess_h) +shooting_harmonic = NonlinearSolve.solve(prob_harmonic; show_trace=Val(false)) +p0_harmonic, ω_sol = shooting_harmonic.u[1:2], shooting_harmonic.u[3] ``` -```@example main -nle_harmonic!(s, ξ, _) = shoot_harmonic!(s, ξ) +### [Comparison](@id examples-control-free-harmonic-comparison) -p0_guess_h = costate(harmonic_sol)(t0h) -ω_guess = variable(harmonic_sol) +The indirect method finds $\omega = \pi/2$ and $p(0) = (0, -2)$; the direct method is close, +on its grid of 20 steps: -prob_harmonic = NonlinearProblem(nle_harmonic!, [p0_guess_h..., ω_guess]) -shoot_sol_harmonic = NonlinearSolve.solve( - prob_harmonic; show_trace=Val(false) -) -p0_sol_harmonic, ω_sol = shoot_sol_harmonic.u[1:2], shoot_sol_harmonic.u[3] -println("indirect ω = ", ω_sol) +```@example main +variable(harmonic_sol), ω_sol, π / 2 ``` -### Comparison +```@example main +f_harmonic_plot = Flow(ocp_harmonic; saveat=range(t0h, tfh, 101), dense=false) +indirect_harmonic = f_harmonic_plot((t0h, tfh), [q0, v0], p0_harmonic; variable=ω_sol) +plot!(plt_harmonic, indirect_harmonic, :state; label="indirect", linestyle=:dash) +``` ```@example main -indirect_harmonic = f_harmonic( - (t0h, tfh), [q0, v0], p0_sol_harmonic; variable=ω_sol -) -plt_harmonic = plot(harmonic_sol, :state; label="Direct") -plot!( - plt_harmonic, indirect_harmonic, :state; - label="Indirect", linestyle=:dash, -) +@assert isapprox(ω_sol, π / 2; atol=1e-8) && isapprox(p0_harmonic, [0, -2]; atol=1e-7) # hide +@assert isapprox(variable(harmonic_sol), π / 2; atol=1e-2) # hide +@assert isapprox(objective(harmonic_sol), π^2 / 4; atol=3e-2) # hide +@assert maximum(abs(state(indirect_harmonic)(t)[1] - cos(π * t / 2)) for t in 0:0.1:1) < 1e-7 # hide +nothing # hide ``` +## Applications + +Problems without a control appear in many contexts: + +- **system identification**: estimating physical parameters (mass, damping, stiffness) from + experimental data; +- **optimal design**: finding geometric or physical parameters (a length, a stiffness) that + optimise a criterion; +- **inverse problems**: reconstructing unknown inputs or initial conditions from partial + observations. + ## See also -- [No control](@ref modelling-without-control) — the modelling-side guide for - control-free problems. -- [Control and variable together](@ref examples-control-and-variable) — the same two systems, - with a control added back in. -- [From an OCP](@ref flows-from-ocp) — control-free flows (`Flow(ocp)`, no law), in general. +- [Control-free problems](@ref modelling-without-control): how to write them. +- [Control and variable together](@ref examples-control-and-variable): the same two systems, + with a control. +- [From an OCP](@ref flows-from-ocp): the flow of a problem without a control, and the costate + of the variable. From c574216368e7777bdbe1b98d563273753f161c28 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:28:07 +0200 Subject: [PATCH 46/57] =?UTF-8?q?docs(control-and-variable):=20what=20the?= =?UTF-8?q?=20control=20changes,=20the=20=CF=89=20trade-off,=20the=20?= =?UTF-8?q?=CF=89=20=3D=200=20extremal=20and=20why=20it=20is=20not=20optim?= =?UTF-8?q?al?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/src/examples/control-and-variable.md | 281 +++++++++++++++------- 1 file changed, 196 insertions(+), 85 deletions(-) diff --git a/docs/src/examples/control-and-variable.md b/docs/src/examples/control-and-variable.md index 2edd5aaae..fce65551f 100644 --- a/docs/src/examples/control-and-variable.md +++ b/docs/src/examples/control-and-variable.md @@ -1,8 +1,11 @@ # [Control and variable together](@id examples-control-and-variable) -The same two systems as [Parameter estimation without a control](@ref examples-control-free), -now with a control input and a quadratic control cost — estimating a parameter *and* a control -at the same time. Read that page first; this one skips the narration that doesn't change. +Some problems optimise a control and constant parameters at the same time: to identify a +parameter and the input that explains the data, or to find a parameter and the control law +that go best together. This page takes the two systems of +[Parameter estimation without a control](@ref examples-control-free), and adds a control to +their dynamics, with a quadratic cost. The method is the same; this page shows what the +control changes. ```@example main using OptimalControl @@ -12,18 +15,24 @@ using NonlinearSolve using Plots ``` -## Growth-rate estimation, with control +## [Growth rate, with a control](@id examples-control-and-variable-growth) -### Definition +### [The problem](@id examples-control-and-variable-growth-problem) + +The growth model gets an additive control, $\dot x = \lambda x + u$, and the cost a term +$\frac12 u^2$: the control can explain the part of the data that the model does not, at a +price. + +```@raw html +
+
+``` ```@example main λ_true = 0.5 -x_true(t) = 2 * exp(λ_true * t) -data(t) = x_true(t) + 0.2 * sin(4π * t) +x_obs(t) = 2 * exp(λ_true * t) + 0.2 * sin(4π * t) -t0 = 0 -tf = 2 -x0 = 2 +t0, tf, x0 = 0, 2, 2 ocp_growth = @def begin λ ∈ R, variable @@ -35,73 +44,128 @@ ocp_growth = @def begin ẋ(t) == λ * x(t) + u(t) - ∫((x(t) - data(t))^2 + 0.5u(t)^2) → min + ∫((x(t) - x_obs(t))^2 + 0.5u(t)^2) → min end +nothing # hide ``` -### Direct solution +```@raw html +
+
+``` + +```math +\begin{aligned} +& \text{Minimise} && \int_0^2 \Big( \big(x(t) - x_{\text{obs}}(t)\big)^2 + \frac12 u(t)^2 \Big) \,\mathrm{d}t \\ +& \text{subject to} && \dot x(t) = \lambda\, x(t) + u(t), \\[0.5em] +& && x(0) = 2, \\[0.5em] +& && \lambda \in \mathbb{R}. +\end{aligned} +``` + +```@raw html +
+
+``` + +### [Direct method](@id examples-control-and-variable-growth-direct) ```@example main growth_sol = solve(ocp_growth; grid_size=20, display=false) -println("estimated λ = ", variable(growth_sol)) +variable(growth_sol), objective(growth_sol) ``` ```@example main -t_grid = time_grid(growth_sol) -plt_growth = plot(growth_sol, :state, :control; label="Direct") +plt_growth = plot(growth_sol, :state, :control; label="direct", size=(800, 400)) ``` -### Indirect solution +### [Indirect method](@id examples-control-and-variable-growth-indirect) -The data term makes the Lagrange integrand — and so the pseudo-Hamiltonian -$H(t,x,p,u,\lambda) = p(\lambda x+u) - (x-x_{\text{data}}(t))^2 - \tfrac12u^2$ — genuinely -non-autonomous. Maximising over $u$ gives $u(t,x,p,\lambda) = p$: +The pseudo-Hamiltonian now depends on the control: + +```math +H(t, x, p, u, \lambda) = p\,(\lambda x + u) - \big(x - x_{\text{obs}}(t)\big)^2 - \frac12 u^2 . +``` + +It is maximised where $\partial H / \partial u = p - u = 0$, so the control in feedback form +is $u = p$. The augmented system is the one of the control-free problem, with $\dot x$ gaining +the control: + +```math +\begin{aligned} +\dot x &= \lambda x + p, & +\dot p &= -p \lambda + 2\big(x - x_{\text{obs}}(t)\big), \\ +\dot \lambda &= 0, & +\dot p_\lambda &= -p\, x, +\end{aligned} +``` + +with the same conditions, $p(t_f) = 0$ and $p_\lambda(t_f) = 0$ from $p_\lambda(t_0) = 0$. The +flow takes the control law, a function of $(t, x, p, \lambda)$ since the problem depends on +time through the data: ```@example main u_growth(t, x, p, λ) = p f_growth = Flow(ocp_growth, u_growth) -function shoot_growth!(s, ξ) - p0, λ = ξ[1], ξ[2] - _, p_tf, pλ_tf = f_growth( - t0, x0, p0, tf; variable=λ, variable_costate=true - ) - s[1] = p_tf - s[2] = pλ_tf +function shoot_growth!(s, ξ, _) + p0, λ = ξ + _, p_tf, pλ_tf = f_growth(t0, x0, p0, tf; variable=λ, variable_costate=true) + s[1] = p_tf # free final state + s[2] = pλ_tf # free parameter return nothing end + +ξ_guess = [costate(growth_sol)(t0), variable(growth_sol)] +prob_growth = NonlinearProblem(shoot_growth!, ξ_guess) +p0_growth, λ_sol = NonlinearSolve.solve(prob_growth; show_trace=Val(false)).u ``` +### [Comparison](@id examples-control-and-variable-growth-comparison) + ```@example main -nle_growth!(s, ξ, _) = shoot_growth!(s, ξ) +indirect_growth = f_growth((t0, tf), x0, p0_growth; variable=λ_sol) +(variable(growth_sol), λ_sol), (objective(growth_sol), objective(indirect_growth)) +``` -p0_guess = costate(growth_sol)(t0) -λ_guess = variable(growth_sol) +For the plot, the flow is built with `saveat`, so that the trajectory has 201 points (see +[Plotting a flow](@ref results-plot-flow)): -prob_growth = NonlinearProblem(nle_growth!, [p0_guess, λ_guess]) -shoot_sol_growth = NonlinearSolve.solve(prob_growth; show_trace=Val(false)) -p0_sol_growth, λ_sol = shoot_sol_growth.u +```@example main +f_growth_plot = Flow(ocp_growth, u_growth; saveat=range(t0, tf, 201), dense=false) +plot!(plt_growth, f_growth_plot((t0, tf), x0, p0_growth; variable=λ_sol), :state, :control; + label="indirect", linestyle=:dash) ``` -### Comparison +The two methods agree, up to the discretisation error of the 20-step grid. ```@example main -indirect_growth = f_growth((t0, tf), x0, p0_sol_growth; variable=λ_sol) -plot!( - plt_growth, indirect_growth, :state, :control; - label="Indirect", linestyle=:dash, -) +s = zeros(2); shoot_growth!(s, [p0_growth, λ_sol], nothing) # hide +@assert maximum(abs, s) < 1e-8 # hide +@assert isapprox(variable(growth_sol), λ_sol; atol=1e-2) # hide +@assert isapprox(objective(growth_sol), objective(indirect_growth); rtol=1e-2) # hide +nothing # hide ``` -## Harmonic oscillator, with control +## [Pulsation of an oscillator, with a control](@id examples-control-and-variable-harmonic) + +### [The problem](@id examples-control-and-variable-harmonic-problem) -### Definition +The oscillator gets a control force, $\ddot q = -\omega^2 q + u$, and the cost the energy of +the control: $\omega^2 + \frac12 \int_0^1 u^2$. The contrast with the control-free problem is +the point of this example. There, the boundary conditions alone forced $\omega = \pi/2$. Here, +any $\omega$ can reach the target, with the help of the control, and the optimum is a +trade-off: a larger $\omega$ costs more, but the oscillator then does more of the work. Only +$\omega^2$ appears in the problem, so $\omega$ and $-\omega$ give the same cost. + +```@raw html +
+
+``` ```@example main -q0 = 1 -v0 = 0 -t0h = 0 -tfh = 1 +q0, v0 = 1, 0 +t0h, tfh = 0, 1 ocp_harmonic = @def begin ω ∈ R, variable @@ -111,81 +175,128 @@ ocp_harmonic = @def begin q(t0h) == q0 v(t0h) == v0 - q(tfh) == 0.0 + q(tfh) == 0 ẋ(t) == [v(t), -ω^2 * q(t) + u(t)] ω^2 + 0.5∫(u(t)^2) → min end +nothing # hide ``` -$\omega$ only ever appears squared, in both the dynamics and the cost, so its sign carries no -physical meaning here — unlike the control-free version, where $\omega=\pi/2$ was forced by the -boundary conditions alone. With a control now sharing the work of steering $q$ to $0$, the -optimal $|\omega|$ is smaller: the direct and indirect solves below are expected to agree with -each other, not with $\pi/2$. +```@raw html +
+
+``` + +```math +\begin{aligned} +& \text{Minimise} && \omega^2 + \frac12 \int_0^1 u(t)^2 \,\mathrm{d}t \\ +& \text{subject to} && \ddot q(t) = -\omega^2 q(t) + u(t), \\[0.5em] +& && q(0) = 1, \quad \dot q(0) = 0, \\[0.5em] +& && q(1) = 0. +\end{aligned} +``` + +```@raw html +
+
+``` -### Direct solution +### [Direct method](@id examples-control-and-variable-harmonic-direct) ```@example main harmonic_sol = solve(ocp_harmonic; grid_size=20, display=false) -println("estimated ω = ", variable(harmonic_sol)) +variable(harmonic_sol), objective(harmonic_sol) ``` ```@example main -plot(harmonic_sol, :state, :control; label="Direct") +plt_harmonic = plot(harmonic_sol, :state, :control; label="direct", size=(800, 400)) ``` -### Indirect solution +### [Indirect method](@id examples-control-and-variable-harmonic-indirect) -Here neither the dynamics nor the cost depend explicitly on $t$, so the flow is autonomous. -Maximising $H = p_1v + p_2(-\omega^2q+u) - \tfrac12u^2$ over $u$ gives $u(x,p,\omega)=p_2$: +The pseudo-Hamiltonian is + +```math +H(x, p, u, \omega) = p_1 v + p_2 (-\omega^2 q + u) - \frac12 u^2, +``` + +maximised by $u = p_2$. The augmented system is the one of the control-free problem, with +$\dot v = -\omega^2 q + p_2$; the adjoint equations and the conditions do not change: +$p_2(t_f) = 0$, and $p_\omega(t_f) = -2\omega$ from $p_\omega(t_0) = 0$. ```@example main u_harmonic(x, p, ω) = p[2] f_harmonic = Flow(ocp_harmonic, u_harmonic) -function shoot_harmonic!(s, ξ) +function shoot_harmonic!(s, ξ, _) p0, ω = ξ[1:2], ξ[3] - x_tf, p_tf, pω_tf = f_harmonic( - t0h, [q0, v0], p0, tfh; variable=ω, variable_costate=true - ) - s[1] = x_tf[1] - s[2] = p_tf[2] - s[3] = pω_tf + 2ω + x_tf, p_tf, pω_tf = f_harmonic(t0h, [q0, v0], p0, tfh; variable=ω, variable_costate=true) + s[1] = x_tf[1] # q(tf) = 0 + s[2] = p_tf[2] # free final velocity + s[3] = pω_tf + 2ω # transversality of the parameter return nothing end + +ξ_guess_h = [costate(harmonic_sol)(t0h)..., variable(harmonic_sol)] +prob_harmonic = NonlinearProblem(shoot_harmonic!, ξ_guess_h) +shooting_harmonic = NonlinearSolve.solve(prob_harmonic; show_trace=Val(false)) +p0_harmonic, ω_sol = shooting_harmonic.u[1:2], shooting_harmonic.u[3] ``` -```@example main -nle_harmonic!(s, ξ, _) = shoot_harmonic!(s, ξ) +### [Comparison](@id examples-control-and-variable-harmonic-comparison) -p0_guess_h = costate(harmonic_sol)(t0h) -ω_guess = variable(harmonic_sol) +```@example main +indirect_harmonic = f_harmonic((t0h, tfh), [q0, v0], p0_harmonic; variable=ω_sol) +(variable(harmonic_sol), ω_sol), (objective(harmonic_sol), objective(indirect_harmonic)) +``` -prob_harmonic = NonlinearProblem(nle_harmonic!, [p0_guess_h..., ω_guess]) -shoot_sol_harmonic = NonlinearSolve.solve( - prob_harmonic; show_trace=Val(false) -) -p0_sol_harmonic, ω_sol = shoot_sol_harmonic.u[1:2], shoot_sol_harmonic.u[3] +```@example main +f_harmonic_plot = Flow(ocp_harmonic, u_harmonic; saveat=range(t0h, tfh, 101), dense=false) +plot!(plt_harmonic, f_harmonic_plot((t0h, tfh), [q0, v0], p0_harmonic; variable=ω_sol), :state, :control; + label="indirect", linestyle=:dash) ``` -### Comparison +The optimal pulsation is $|\omega| \approx 0.654$, well below $\pi/2$, for a cost of +$\approx 1.457$. + +The shooting equations have another solution: $\omega = 0$, $p(0) = (-3, -3)$. Without +oscillator, the control alone steers the mass, $u(t) = -3(1 - t)$, and the cost is +$\frac12 \int_0^1 9(1 - t)^2 = 3/2$. It is an extremal, but not the minimum: its cost is +larger. The maximum principle gives necessary conditions only, and comparing the costs of the +extremals selects the optimum. + +```@example main +s = zeros(3) +shoot_harmonic!(s, [-3.0, -3.0, 0.0], nothing) +s, objective(f_harmonic((t0h, tfh), [q0, v0], [-3.0, -3.0]; variable=0.0)) +``` ```@example main -indirect_harmonic = f_harmonic( - (t0h, tfh), [q0, v0], p0_sol_harmonic; variable=ω_sol -) -plt_harmonic = plot(harmonic_sol, :state, :control; label="Direct") -plot!( - plt_harmonic, indirect_harmonic, :state, :control; - label="Indirect", linestyle=:dash, -) +@assert isapprox(abs(ω_sol), 0.6537637; atol=1e-6) # hide +@assert isapprox(objective(indirect_harmonic), 1.4571067; atol=1e-6) # hide +@assert isapprox(abs(variable(harmonic_sol)), abs(ω_sol); atol=1e-2) # hide +@assert maximum(abs, s) < 1e-10 # hide +@assert isapprox(objective(f_harmonic((t0h, tfh), [q0, v0], [-3.0, -3.0]; variable=0.0)), 1.5; atol=1e-8) # hide +s3 = zeros(3); shoot_harmonic!(s3, shooting_harmonic.u, nothing) # hide +@assert maximum(abs, s3) < 1e-8 # hide +nothing # hide ``` +## Applications + +Problems with a control and parameters appear in many contexts: + +- **system identification**: estimating physical parameters (mass, damping, stiffness) and the + inputs, from experimental data; +- **optimal design**: finding geometric or physical parameters together with the control law + that goes with them; +- **inverse problems**: reconstructing unknown inputs or initial conditions from partial + observations, while optimising parameters of the system. + ## See also -- [Parameter estimation without a control](@ref examples-control-free) — the same two systems, - without the control input. -- [From an OCP](@ref flows-from-ocp) — the maximising-control construction used above, in - general. +- [Parameter estimation without a control](@ref examples-control-free): the same two systems, + without a control, and the derivation of the augmented system. +- [From an OCP](@ref flows-from-ocp): flows with a control law and a variable. From f3dbf8c5a511cba85597a2ac97fbc569cae61802 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:44:14 +0200 Subject: [PATCH 47/57] docs(gallery): every example listed, the logo included, what each one introduces after the review Co-Authored-By: Claude Opus 5.5 --- docs/src/examples/gallery.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/src/examples/gallery.md b/docs/src/examples/gallery.md index cbb9f623f..39adefe5c 100644 --- a/docs/src/examples/gallery.md +++ b/docs/src/examples/gallery.md @@ -1,22 +1,26 @@ # [Example gallery](@id examples-gallery) A guide answers *"how do I do X"*; an example answers *"what does a real problem look like"*. -Each page here is a complete, self-contained problem worked end to end — a story with a -result, not a feature demonstration. +Each page here is a complete problem worked end to end: the problem, the direct method, the +derivation of the solution from the maximum principle, the indirect method, and the +comparison of the two, checked against the exact values when they are known. -Read them in order: each one uses something the previous introduced. +Read them in order: each one uses something the previous ones introduced. | Example | Story | Introduces | | --- | --- | --- | -| [Energy minimisation](@ref examples-double-integrator-energy) | The double integrator, transferred at minimal energy. | `@def`, `solve`, `plot`, a Hamiltonian flow, shooting | -| [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=` | +| [Energy minimisation](@ref examples-double-integrator-energy) | A wagon on a rail, transferred with the least energy. | `@def`, `solve`, `plot`, a Hamiltonian flow, shooting from the direct solution | +| [Time minimisation (bang–bang)](@ref examples-double-integrator-time) | The same wagon, with a bounded force, transferred as fast as possible. | a free final time, bang–bang control, the switching time read off the direct solution, concatenation of flows | +| [Parameter estimation without a control](@ref examples-control-free) | A growth rate fitted to data, and the pulsation of an oscillator, with no control. | problems without a control, the costate of the variable (`variable_costate=true`) | +| [Control and variable together](@ref examples-control-and-variable) | The same two systems, with a control. | a parameter and a control optimised together, two extremals compared | +| [Turnpike (bang–singular–bang)](@ref examples-turnpike) | A scalar system whose optimal control is bang, singular, then bang. | singular arcs, switching times as shooting unknowns, three flows concatenated | +| [Singular control](@ref examples-singular-control) | A vehicle in a drift field, in minimum time. | the singular control by hand and with `Lift` and `@Lie` | +| [State constraint](@ref examples-state-constraint) | The wagon with a speed limit, then with a bound on its position (Bryson–Denham). | boundary arcs, `constraint=` and `multiplier=`, touch points and costate jumps | +| [The logo](@ref examples-logo) | A low-thrust orbit transfer, drawn three times. | an initial guess from an analytical trajectory, plotting with Makie | ## See also -- [Flows overview](@ref flows-overview) and [Geometry overview](@ref geometry-overview) — the +- [Flows overview](@ref flows-overview) and [Geometry overview](@ref geometry-overview): the tools these examples are built from. +- [Notation and conventions](@ref modelling-formulation-conventions): the conventions of the + derivations. From 1091429e494bb94894a4ec839595b56497c669fe Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:44:14 +0200 Subject: [PATCH 48/57] =?UTF-8?q?docs(turnpike):=20H=20linear=20in=20u=20(?= =?UTF-8?q?not=20the=20cost),=20math=20blocks,=20extref=20links,=20p?= =?UTF-8?q?=E2=82=80=20=3D=20=E2=88=921,=20t=E2=82=81=20=3D=201,=20t?= =?UTF-8?q?=E2=82=82=20=3D=203/2,=20J=20=3D=203/8=20asserted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/src/examples/turnpike.md | 194 ++++++++++++++++++++-------------- 1 file changed, 114 insertions(+), 80 deletions(-) diff --git a/docs/src/examples/turnpike.md b/docs/src/examples/turnpike.md index 838443b5a..7f5d549ac 100644 --- a/docs/src/examples/turnpike.md +++ b/docs/src/examples/turnpike.md @@ -1,11 +1,11 @@ # [Turnpike (bang–singular–bang)](@id examples-turnpike) -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. +A scalar system, $\dot x = u$ with $u \in [-1, 1]$, is driven between two states over a fixed +horizon while minimising $\int x^2$. The dynamics are affine in $u$ and the cost does not +depend on $u$, so the pseudo-Hamiltonian 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 with one, and unlike [Singular control](@ref examples-singular-control), +the singular control follows from the optimality conditions without Poisson brackets. ```@example main using OptimalControl @@ -17,11 +17,13 @@ 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. +The horizon is fixed, $t_f = 2$, and the transfer goes from $x(0) = 1$ to $x(2) = 1/2$. The +state and the control are scalars, not vectors of length one: -## Definition +```@raw html +
+
+``` ```@example main t0, tf = 0.0, 2.0 @@ -37,129 +39,161 @@ ocp = @def begin ẋ(t) == u(t) ∫(x(t)^2) → min end +nothing # hide ``` -## Direct solution +```@raw html +
+
+``` + +```math +\begin{aligned} +& \text{Minimise} && \int_0^2 x(t)^2 \,\mathrm{d}t \\ +& \text{subject to} && \dot x(t) = u(t), \\[0.5em] +& && -1 \le u(t) \le 1, \\[0.5em] +& && x(0) = 1, \quad x(2) = 1/2. +\end{aligned} +``` + +```@raw html +
+
+``` + +## Direct method ```@example main direct_sol = solve(ocp; grid_size=100, display=false) -plt = plot(direct_sol, :state, :control; label="Direct") +plt = plot(direct_sol, :state, :control; label="direct", size=(800, 400)) ``` -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 state goes down to the origin as fast as it can, stays there, then climbs to the target. +The flat middle part, the *turnpike*, is the singular arc, where the control is $0$, inside +its bounds. ## 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 +With $p^0 = -1$ (see [Notation and conventions](@ref modelling-formulation-conventions)), the +pseudo-Hamiltonian is + +```math +H(x, p, u) = p\, u - x^2 . +``` + +It is linear in $u$, and maximised by $u = \operatorname{sign}(p)$: the switching function is +the costate $p$. Where $p$ vanishes on an interval, and not only at isolated times, this rule +does not give the control, which is *singular*. On such an interval, $\dot p = 0$; the adjoint +equation is $\dot p = -\partial_x H = 2x$, so $x = 0$ there, and then $\dot x = u$ gives -$$u_{\text{sing}} = 0.$$ +```math +u_{\text{sing}} = 0 . +``` -The whole extremal is therefore bang–singular–bang: +The extremal is therefore bang–singular–bang: -| arc | interval | $u$ | $x$ | +| 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$ | +| bang | $[0, t_1]$ | $-1$ | from $1$ to $0$ | +| singular | $[t_1, t_2]$ | $0$ | $0$ | +| bang | $[t_2, 2]$ | $+1$ | from $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$. +The state decreases at unit rate from $1$, so it reaches $0$ at $t_1 = 1$; it increases at unit +rate to $1/2$, so it leaves the arc at $t_2 = 3/2$. On the first arc, $\dot p = 2x = 2(1 - t)$ +and $p(t_1) = 0$, so $p(t) = -(1 - t)^2$ and $p(0) = -1$. The cost is +$\int_0^1 (1 - t)^2 \,\mathrm{d}t + \int_{3/2}^2 (t - 3/2)^2 \,\mathrm{d}t = 1/3 + 1/24 = 3/8$. -## Indirect solution +## Indirect method -Three constant-control flows, one per arc: +Each arc has its flow, with a constant control: ```@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) +f_minus = Flow(ocp, (x, p) -> -1) +f_sing = Flow(ocp, (x, p) -> 0) +f_plus = Flow(ocp, (x, p) -> 1) +nothing # hide ``` -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$: +The unknowns are the initial costate $p_0$ and the two switching times; the horizon is fixed. +Three conditions close the system: the trajectory enters the singular arc at $x = 0$, with the +switching function $p = 0$, and it reaches the target at $t_f$: ```@example main -function shoot!(s, ξ) - p0, t1, t2 = ξ[1], ξ[2], ξ[3] +function shoot!(s, ξ, _) + p0, t1, t2 = ξ 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 + x3, _ = f_plus(t2, x2, p2, tf) + s[1] = x1 # enter the singular arc at x = 0 + s[2] = p1 # where the switching function vanishes + s[3] = x3 - xf # reach the target return nothing end +nothing # hide ``` -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): +The direct solution gives the initial guess. The singular arc is where its switching function, +the costate, stays close to zero, as in the [Goddard tutorial](@extref Tutorials 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 +tg = time_grid(direct_sol) +p_d = costate(direct_sol) -p0_guess = p(t0) -t1_guess = minimum(t12) -t2_guess = maximum(t12) -(p0_guess, t1_guess, t2_guess) +t12 = tg[abs.(p_d.(tg)) .≤ 1e-3] # the grid points on the singular arc +ξ_guess = [p_d(t0), minimum(t12), maximum(t12)] ``` ```@example main -nle!(s, ξ, _) = shoot!(s, ξ) -prob = NonlinearProblem(nle!, [p0_guess, t1_guess, t2_guess]) +prob = NonlinearProblem(shoot!, ξ_guess) shooting_sol = NonlinearSolve.solve(prob; show_trace=Val(false)) p0_sol, t1_sol, t2_sol = shooting_sol.u ``` +The solver finds the values computed by hand, $p_0 = -1$, $t_1 = 1$ and $t_2 = 3/2$, with a +zero residual: + ```@example main s = zeros(3) -shoot!(s, shooting_sol.u) +shoot!(s, shooting_sol.u, nothing) 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: +The concatenation of the three flows at the switching times gives the whole extremal, and its +cost is $3/8$, as is the cost of the direct solution, up to the discretisation error: ```@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) +φ = f_minus * (t1_sol, f_sing) * (t2_sol, f_plus) +indirect_sol = φ((t0, tf), x0, p0_sol) +objective(direct_sol), objective(indirect_sol) ``` -The two curves overlap, and the objective is the expected $3/8$: +For the plot, the flows are built with `saveat`, so that the trajectory has 201 points (see +[Plotting a flow](@ref results-plot-flow)): ```@example main -objective(direct_sol) +fine = (saveat=range(t0, tf, 201), dense=false) +φ_plot = Flow(ocp, (x, p) -> -1; fine...) * + (t1_sol, Flow(ocp, (x, p) -> 0; fine...)) * + (t2_sol, Flow(ocp, (x, p) -> 1; fine...)) +plot!(plt, φ_plot((t0, tf), x0, p0_sol), :state, :control; label="indirect", linestyle=:dash) +``` + +```@example main +@assert isapprox(shooting_sol.u, [-1, 1, 3 / 2]; atol=1e-8) && maximum(abs, s) < 1e-10 # hide +@assert isapprox(objective(indirect_sol), 3 / 8; atol=1e-6) # hide +@assert isapprox(objective(direct_sol), 3 / 8; atol=1e-3) # hide +nothing # hide ``` ## 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. -``` +- [Singular control](@ref examples-singular-control): a system with three states, where the + singular control needs Poisson brackets. +- [Shooting](@ref flows-shooting): the shooting method, with switching times as unknowns. +- [Time minimisation (bang–bang)](@ref examples-double-integrator-time): bang arcs, without a + singular arc between them. +- [Multi-phase flows](@ref flows-multi-phase): the concatenation of flows. +- The [Goddard tutorial](@extref Tutorials tutorial-goddard): the same workflow on a harder + problem, with a singular arc and a boundary arc. From d3fe2a80751b8b63a799a8f028c23a9c73af984d Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:44:14 +0200 Subject: [PATCH 49/57] docs(singular-control): v2.0.4 hand computation restored (consistent bracket sign), non-degeneracy, all-singular extremal stated, asserts Co-Authored-By: Claude Opus 5.5 --- docs/src/examples/singular-control.md | 282 ++++++++++++++++++-------- 1 file changed, 197 insertions(+), 85 deletions(-) diff --git a/docs/src/examples/singular-control.md b/docs/src/examples/singular-control.md index 4573ff5b4..8220af60a 100644 --- a/docs/src/examples/singular-control.md +++ b/docs/src/examples/singular-control.md @@ -1,12 +1,21 @@ # [Singular control](@id examples-singular-control) -A vehicle in the plane with drift, time-optimal, whose extremal control is neither $+1$ nor -$-1$ on part of the trajectory — a **singular arc**. This is the problem the whole -[Geometry](@ref geometry-overview) section exists to support: computing that arc's control -needs Poisson brackets. +For a control-affine system -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. +```math +\dot q(t) = f_0(q(t)) + u(t)\, f_1(q(t)), \qquad 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$. The maximising control is given by +the sign of the **switching function** $H_1$. When $H_1$ vanishes on a time interval, the arc +is **singular**: the maximisation does not give the control, which is computed by +differentiating $H_1$ along the extremal. + +This page computes a singular control by hand, then with the tools of +[Geometry](@ref geometry-overview), and checks it with the direct and the indirect methods. +For a simpler case, where the singular control follows from the optimality conditions without +brackets, see [Turnpike (bang–singular–bang)](@ref examples-turnpike). ```@example main using OptimalControl @@ -18,11 +27,16 @@ using Plots ## The problem -State $q=(x,y,\theta)$, dynamics $\dot x=\cos\theta,\ \dot y=\sin\theta+x,\ \dot\theta=u$, -control $u\in[-1,1]$, time-optimal transfer from the origin (free orientation) to $(1,0)$ (free -final orientation too). +A vehicle moves in the plane, in a drift field. Its state is $q = (x, y, \theta)$, where +$(x, y)$ is the position and $\theta$ the heading, and it turns at a bounded rate: + +```math +\dot x(t) = \cos\theta(t), \quad \dot y(t) = \sin\theta(t) + x(t), \quad \dot\theta(t) = u(t), +\quad u(t) \in [-1, 1]. +``` -## Definition +We look for the fastest transfer from the origin to the point $(1, 0)$, with free initial and +final headings: ```@example main ocp = @def begin @@ -32,7 +46,7 @@ ocp = @def begin u ∈ R, control -1 ≤ u(t) ≤ 1 - -π / 2 ≤ θ(t) ≤ π / 2 # helps direct convergence + -π / 2 ≤ θ(t) ≤ π / 2 # helps the direct method x(0) == 0 y(0) == 0 @@ -43,37 +57,123 @@ ocp = @def begin tf → min end +nothing # hide +``` + +The bound on $\theta$ keeps the direct method away from trajectories that turn the long way +round. It is not active at the solution, so the indirect method, which ignores it, finds the +same solution. The system is control-affine, with + +```math +f_0(q) = \begin{pmatrix} \cos\theta \\ \sin\theta + x \\ 0 \end{pmatrix}, \qquad +f_1(q) = \begin{pmatrix} 0 \\ 0 \\ 1 \end{pmatrix}. ``` -The $-\pi/2 \le \theta(t) \le \pi/2$ box only keeps the *direct* solver away from a spurious -branch where the vehicle turns the long way round; the optimal orientation stays well inside -it, so the bound is never active at the solution. The *indirect* solution below is built from -the PMP flow alone and never sees this bound — the two still agree because it is slack. +## Direct method -## Direct solution +```@example main +direct_sol = solve(ocp; display=false) +variable(direct_sol) +``` ```@example main -direct_sol = solve(ocp; grid_size=50, display=false) -plt = plot(direct_sol, :state, :control; label="Direct") +opt = (state_bounds_style=:none, control_bounds_style=:none) +plt = plot(direct_sol; label="direct", size=(800, 800), opt...) +``` + +The control stays strictly inside $[-1, 1]$: the arc is singular. + +## The singular control by hand + +With $p = (p_1, p_2, p_3)$ and the Mayer cost $t_f$ (see +[Notation and conventions](@ref modelling-formulation-conventions)), the pseudo-Hamiltonian is + +```math +H(q, p, u) = p_1 \cos\theta + p_2 (\sin\theta + x) + p_3\, u = H_0 + u H_1, +\qquad H_0 = p_1 \cos\theta + p_2 (\sin\theta + x), \quad H_1 = p_3 . +``` + +On a singular arc, $H_1 = 0$, and so are its time derivatives. Along an extremal, the time +derivative of a function $G(q, p)$ is $\{H, G\}$, a Poisson bracket (see +[Time derivative along an extremal](@ref geometry-poisson-total)), with +$\{F, G\} = \nabla_p F \cdot \nabla_q G - \nabla_q F \cdot \nabla_p G$. + +**First derivative.** Since $\{H_1, H_1\} = 0$, + +```math +\dot H_1 = \{H_0 + u H_1, H_1\} = \{H_0, H_1\} =: H_{01}. +``` + +$H_1 = p_3$ depends only on $p_3$, so only the pair $(\theta, p_3)$ contributes: + +```math +H_{01} = -\frac{\partial H_0}{\partial \theta} \frac{\partial H_1}{\partial p_3} += p_1 \sin\theta - p_2 \cos\theta . +``` + +On the singular arc, $H_{01} = 0$, that is, $p_1 \sin\theta = p_2 \cos\theta$. + +**Second derivative.** + +```math +\dot H_{01} = \{H_0, H_{01}\} + u \{H_1, H_{01}\} =: H_{001} + u H_{101}, +``` + +and it vanishes on the arc, so wherever $H_{101} \neq 0$, + +```math +u_s = -\frac{H_{001}}{H_{101}} . +``` + +In $H_{001} = \{H_0, H_{01}\}$, only the pair $(x, p_1)$ contributes: +$H_{001} = -\frac{\partial H_0}{\partial x} \frac{\partial H_{01}}{\partial p_1} = -p_2 \sin\theta$. +In $H_{101} = \{H_1, H_{01}\}$, only the pair $(\theta, p_3)$ contributes: +$H_{101} = \frac{\partial H_1}{\partial p_3} \frac{\partial H_{01}}{\partial \theta} = p_1 \cos\theta + p_2 \sin\theta$. +Therefore + +```math +u_s = \frac{p_2 \sin\theta}{p_1 \cos\theta + p_2 \sin\theta} . ``` -## Computing the singular control +!!! note "The denominator does not vanish" -The pseudo-Hamiltonian $H = p_x\cos\theta + p_y(\sin\theta+x) + p_\theta u$ is **linear in -$u$** — its sign is driven entirely by $p_\theta$, exactly as in the bang–bang example, except -that here $p_\theta$ vanishes on part of the trajectory instead of only at isolated switching -instants. On that arc the control is *singular*: found from the vanishing switching function's -own derivatives, not from $\operatorname{sign}(p_\theta)$. + If $H_{101} = p_1 \cos\theta + p_2 \sin\theta$ were zero on the arc, then with + $H_{01} = 0$, -Splitting the dynamics into a drift and a control vector field, $F_0(q) = (\cos\theta, -\sin\theta+x, 0)$ and $F_1(q)=(0,0,1)$: + ```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}. + ``` + + This matrix has determinant $-1$, so $p_1 = p_2 = 0$, and with $p_3 = 0$, $p = 0$. Then + $H = 0$, which is impossible: for a minimum-time problem, $H = 1$ along the extremal. + +**Simplification.** Multiply the numerator and the denominator by $\sin\theta$, and use +$p_1 \sin\theta = p_2 \cos\theta$ in the denominator: + +```math +u_s = \frac{p_2 \sin^2\theta}{p_2 \cos^2\theta + p_2 \sin^2\theta} = \sin^2\theta . +``` + +The singular control depends on the heading only. Along the direct solution, it matches the +control: ```@example main -F0(q) = [cos(q[3]), sin(q[3]) + q[1], 0] -F1(q) = [0, 0, 1] +tg = time_grid(direct_sol) +θ(t) = state(direct_sol)(t)[3] +plt_u = plot(direct_sol, :control; label="direct", size=(800, 300), opt...) +plot!(plt_u, tg, t -> sin(θ(t))^2; label="sin²θ", linestyle=:dash) ``` +## The singular control with brackets + +The tools of [Geometry](@ref geometry-overview) compute the same brackets from the vector +fields: their lifts, then the iterated Poisson brackets with [`@Lie`](@ref geometry-lie-macro): + ```@example main +F0(q) = [cos(q[3]), sin(q[3]) + q[1], 0] +F1(q) = [0, 0, 1] + H0 = Lift(F0) H1 = Lift(F1) @@ -82,104 +182,116 @@ H001 = @Lie {H0, H01} H101 = @Lie {H1, H01} us_bracket(q, p) = -H001(q, p) / H101(q, p) +nothing # hide ``` -The classical singular-control formula $u_s = -H_{001}/H_{101}$ (see -[Poisson bracket](@ref geometry-poisson)) simplifies, for this particular drift/control pair, -to a function of the state alone: +At a point where $H_1 = H_{01} = 0$, that is, $p_3 = 0$ and $p_2 = p_1 \tan\theta$, the formula +gives $\sin^2\theta$: ```@example main -u_indirect(x) = sin(x[3])^2 +q_s = [0.3, -0.1, 0.5] +p_s = [1.7, 1.7 * tan(q_s[3]), 0] +us_bracket(q_s, p_s), sin(q_s[3])^2 ``` -This is exact on the singular surface $\{H_1 = 0,\ H_{01} = 0\}$ — here $p_\theta = 0$ and -$p_y = p_x\tan\theta$. Check it against the bracket formula at an arbitrary point of that -surface: - ```@example main -q_s = [0.3, -0.1, 0.5] -p_s = [1.7, 1.7 * tan(q_s[3]), 0.0] # a point of {H₁ = 0, H₀₁ = 0} - -us_bracket(q_s, p_s), u_indirect(q_s) # equal +@assert isapprox(us_bracket(q_s, p_s), sin(q_s[3])^2; atol=1e-12) # hide +nothing # hide ``` -## Indirect solution +## Indirect method + +Here the whole extremal is singular: the headings are free at both ends, so the +transversality conditions give $p_3(0) = p_3(t_f) = 0$, and $H_1 = p_3$ vanishes on the whole +interval. The flow uses the singular control over the whole horizon: ```@example main -f = Flow(ocp, (x, p, v) -> u_indirect(x)) +u_s(q) = sin(q[3])^2 +f = Flow(ocp, (q, p, tf) -> u_s(q)) +nothing # hide ``` -Unknowns: the initial costate $p_0\in\mathbb R^3$, the free initial angle $\theta_0$, and the -free final time $t_f$ — five conditions (two boundary, two transversality on $\theta$, and the -free-final-time condition $H(t_f)=1$): +There are five unknowns, $p_0 \in \mathbb{R}^3$, the initial heading $\theta_0$ and the final +time $t_f$, and five conditions: the target (two equations), the transversality conditions of +the headings, and the transversality condition of the final time, $H(t_f) = 1$, where +$H(t_f) = H_0(t_f)$ since $p_3(t_f) = 0$: ```@example main t0 = 0 -function shoot!(s, ξ) +function shoot!(s, ξ, _) p0, θ0, tf = ξ[1:3], ξ[4], ξ[5] - - q_t0 = [0, 0, θ0] - q_tf, p_tf = f(t0, q_t0, p0, tf; variable=tf) - - s[1] = q_tf[1] - 1 - s[2] = q_tf[2] - s[3] = p0[3] - s[4] = p_tf[3] - - px_tf, py_tf, θf = p_tf[1], p_tf[2], q_tf[3] - s[5] = px_tf * cos(θf) + py_tf * (sin(θf) + 1) - 1 + q_tf, p_tf = f(t0, [0, 0, θ0], p0, tf; variable=tf) + s[1] = q_tf[1] - 1 # x(tf) = 1 + s[2] = q_tf[2] # y(tf) = 0 + s[3] = p0[3] # free initial heading + s[4] = p_tf[3] # free final heading + s[5] = H0(q_tf, p_tf) - 1 # free final time return nothing end +nothing # hide ``` -```@example main -nle!(s, ξ, _) = shoot!(s, ξ) - -p0_guess = costate(direct_sol)(t0) -θ0_guess = state(direct_sol)(t0)[3] -tf_guess = variable(direct_sol) +The direct solution gives the initial guess: -ξ_guess = [p0_guess..., θ0_guess, tf_guess] - -prob = NonlinearProblem(nle!, ξ_guess) +```@example main +ξ_guess = [costate(direct_sol)(t0)..., state(direct_sol)(t0)[3], variable(direct_sol)] +prob = NonlinearProblem(shoot!, ξ_guess) shooting_sol = NonlinearSolve.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] +p0_sol, θ0_sol, tf_sol = shooting_sol.u[1:3], shooting_sol.u[4], shooting_sol.u[5] ``` +The residual is zero: + ```@example main s = zeros(5) -shoot!(s, shooting_sol.u) +shoot!(s, shooting_sol.u, nothing) s ``` ## Comparison +The two methods find the same final time, up to the discretisation error of the direct +method: + ```@example main indirect_sol = f((t0, tf_sol), [0, 0, θ0_sol], p0_sol; variable=tf_sol) -plot!( - plt, indirect_sol, :state, :control; - label="Indirect", linestyle=:dash, -) +variable(direct_sol), tf_sol +``` + +For the plot, the flow is built with `saveat`, so that the trajectory has 101 points (see +[Plotting a flow](@ref results-plot-flow)): + +```@example main +f_plot = Flow(ocp, (q, p, tf) -> u_s(q); saveat=range(t0, tf_sol, 101), dense=false) +plot!(plt, f_plot((t0, tf_sol), [0, 0, θ0_sol], p0_sol; variable=tf_sol); + label="indirect", linestyle=:dash, opt...) ``` -The indirect and direct solutions match closely, confirming the singular-control computation -above is correct — and, since `Lift`/`@Lie` needed no change from the pre-rewrite version, that -the flow half was migrated correctly too. +Along the extremal, the switching function $H_1 = p_3$ stays zero, $\theta$ stays inside +$(-\pi/2, \pi/2)$, and the control computed with the brackets is $\sin^2\theta$: -The same equality, now along the computed extremal rather than a hand-picked surface point: +```@example main +qs, ps = state(indirect_sol), costate(indirect_sol) +ts = range(t0, tf_sol, 5) +[(ps(t)[3], us_bracket(qs(t), ps(t)) - u_s(qs(t))) for t in ts] +``` ```@example main -xs = state(indirect_sol) -ps = costate(indirect_sol) -[us_bracket(xs(t), ps(t)) - u_indirect(xs(t)) - for t in range(t0, tf_sol, 5)] +@assert maximum(abs, s) < 1e-8 # hide +@assert isapprox(variable(direct_sol), tf_sol; rtol=1e-3) # hide +@assert all(abs(ps(t)[3]) < 1e-6 for t in range(t0, tf_sol, 21)) # hide +@assert all(abs(us_bracket(qs(t), ps(t)) - u_s(qs(t))) < 1e-6 for t in range(t0, tf_sol, 21)) # hide +@assert all(abs(qs(t)[3]) < π / 2 - 0.1 for t in range(t0, tf_sol, 21)) # hide +nothing # hide ``` ## See also -- [Poisson bracket](@ref geometry-poisson) — the $H_{01}$/$H_{001}$/$H_{101}$ chain, in general. -- [The `@Lie` macro](@ref geometry-lie-macro) — the `{}` bracket notation used above. -- [Lifting a vector field](@ref geometry-lift) — `Lift`, in general. -- [Shooting](@ref flows-shooting) — the shooting method in general. +- [Poisson bracket](@ref geometry-poisson): the brackets and the singular-control formula, in + general. +- [The `@Lie` macro](@ref geometry-lie-macro): the bracket notation used above. +- [Lift](@ref geometry-lift): the Hamiltonian lift of a vector field. +- [Shooting](@ref flows-shooting): the shooting method. +- The [Goddard tutorial](@extref Tutorials tutorial-goddard): a problem with bang, singular + and boundary arcs. From e9dfaf3be2383ce179bf84886c357a774787db42 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:44:14 +0200 Subject: [PATCH 50/57] =?UTF-8?q?docs(state-constraint):=20B1,=20function?= =?UTF-8?q?=20form,=20derivations=20and=20Bryson=E2=80=93Denham=20regimes?= =?UTF-8?q?=20restored,=20analytic=20values=20asserted,=20references?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/src/examples/state-constraint.md | 496 ++++++++++++++++---------- 1 file changed, 317 insertions(+), 179 deletions(-) diff --git a/docs/src/examples/state-constraint.md b/docs/src/examples/state-constraint.md index 4b0406172..8ae260e37 100644 --- a/docs/src/examples/state-constraint.md +++ b/docs/src/examples/state-constraint.md @@ -1,8 +1,20 @@ # [State constraint](@id examples-state-constraint) -The double integrator again, this time with a state constraint active on part of the -trajectory — a boundary arc, a costate jump, and (for the second problem below) a genuine -multi-arc structure. +This example shows how state constraints of different orders shape the solutions of the +energy-minimal double integrator, and how to compute them by the direct and the indirect +methods. Bryson et al.[^1] and Jacobson et al.[^2] solve such problems analytically. + +A wagon moves along a rail, and we control its acceleration with a force $u$. Its state is +$x = (q, v)$, where $q$ is the position and $v$ the velocity. + +```@raw html + +``` + +The mass is one and there is no friction, so the dynamics are those of the +[double integrator](https://en.wikipedia.org/w/index.php?title=Double_integrator&oldid=1071399674), +$\dot q = v$, $\dot v = u$, and we minimise the energy $\frac12 \int_0^1 u^2(t)\,\mathrm{d}t$. +Without a constraint, this is [Energy minimisation](@ref examples-double-integrator-energy). ```@example main using OptimalControl @@ -12,146 +24,226 @@ using NonlinearSolve using Plots ``` -## First-order constraint: bounding the velocity +## [A first-order constraint: a speed limit](@id examples-state-constraint-first-order) -### The problem +### [The problem](@id examples-state-constraint-first-order-problem) -Same energy-minimal transfer as [Energy minimisation](@ref examples-double-integrator-energy), -now with $v(t) \le v_{\max}$. +The wagon goes from rest at $q = -1$ to rest at $q = 0$, as in the energy example, and its +velocity is bounded, $v(t) \le v_{\max} = 1.2$. Without the bound, the velocity +$v(t) = 6t - 6t^2$ reaches $1.5$ at $t = 1/2$, so the bound is active. -### Definition +```@raw html +
+
+``` ```@example main -t0 = 0.0 -tf = 1.0 -x0 = [-1.0, 0.0] -xf = [0.0, 0.0] -VMAX = 1.2 +t0, tf = 0, 1 +x0, xf = [-1, 0], [0, 0] +v_max = 1.2 ocp = @def begin t ∈ [t0, tf], time x = (q, v) ∈ R², state u ∈ R, control + v(t) ≤ v_max + 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 +
+
+``` + +```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)), \\[0.5em] +& && v(t) \le v_{\max}, \\[0.5em] +& && x(0) = (-1, 0), \quad x(1) = (0, 0). +\end{aligned} +``` + +```@raw html +
+
``` -The `+ 0.0` in `v(t) + 0.0 ≤ VMAX` forces the parser to read this as a nonlinear **path** -constraint (with a dual, reachable by its `:vmax` label) rather than a plain box bound on the -state — see [Constrained arcs](@ref flows-constrained-arcs). +The constraint is of **first order**: with $c(x) = v_{\max} - v \ge 0$, the first derivative +already involves the control, $\frac{\mathrm{d}}{\mathrm{d}t} c(x(t)) = -u(t)$. -### Direct solution +### [Direct method](@id examples-state-constraint-first-order-direct) + +A direct method on a coarse grid shows the structure of the solution, and gives the initial +guess of the indirect method: ```@example main direct_sol = solve(ocp; grid_size=50, display=false) -plt = plot(direct_sol, :state, :control; label="Direct") +plt = plot(direct_sol; label="direct", size=(800, 600)) +``` + +The solution has three arcs: an unconstrained arc, a **boundary arc** where $v = v_{\max}$, +and an unconstrained arc. + +### [The exact solution](@id examples-state-constraint-first-order-exact) + +With the conventions of [Notation and conventions](@ref modelling-formulation-conventions), +the pseudo-Hamiltonian is + +```math +H(x, p, u, \mu) = p_1 v + p_2 u - \frac{u^2}{2} + \mu\, c(x), \qquad c(x) = v_{\max} - v, +``` + +with $\mu = 0$ off the boundary arc. On an unconstrained arc, $u = p_2$. Along the boundary +arc, $c(x(t)) = 0$, so its derivative $-u$ vanishes: $u = 0$. The zero control maximises $H$, +so $p_2 = 0$ along the arc, and the adjoint equation $\dot p_2 = -p_1 + \mu = 0$ gives the +multiplier, $\mu = p_1$. For a first-order constraint, the costate is continuous at the +junctions. + +The costate $p_1$ is constant, so on the first arc, $u = p_2$ is affine, and it vanishes at the +entry time $t_1$: $u(t) = p_1 (t_1 - t)$. The velocity reaches $v_{\max}$ at $t_1$, so +$p_1 t_1^2 / 2 = v_{\max}$, and the last arc is symmetric. The distance covered, +$1 = \frac23 p_1 t_1^3 + v_{\max} (1 - 2t_1)$, then gives + +```math +t_1 = \frac32 \Big( 1 - \frac{1}{v_{\max}} \Big) = 0.25, \qquad t_2 = 1 - t_1 = 0.75, \qquad +p(0) = (38.4,\ 9.6), \qquad \mu = 38.4 . ``` -### Indirect solution +The cost is $\frac12 \cdot 2 \int_0^{t_1} p_1^2 (t_1 - t)^2 \,\mathrm{d}t = p_1^2 t_1^3 / 3 = 7.68$. + +### [Indirect method](@id examples-state-constraint-first-order-indirect) -The pseudo-Hamiltonian is $H = p_qv + p_vu - \tfrac12u^2 + \mu(V_{\max}-v)$. Off the boundary, -$u=p_v$; on it, $u=0$ and the multiplier is $\mu(x,p)=p_q$ (see -[Constrained arcs](@ref flows-constrained-arcs) for this construction in general — same -problem, same constraint): +Each arc has its flow. The flow on the boundary arc takes the constraint $c$ and the +multiplier $\mu$, both as functions (see [Constrained arcs](@ref flows-constrained-arcs)): ```@example main f_interior = Flow(ocp, (x, p) -> p[2]) -g(x) = VMAX - x[2] -μ(x, p) = p[1] +c(x) = v_max - x[2] # the constraint, c ≥ 0 +μ(x, p) = p[1] # the multiplier on the boundary arc -f_boundary = Flow(ocp, (x, p) -> 0.0; constraint=:vmax, multiplier=μ) +f_boundary = Flow(ocp, (x, p) -> 0; constraint=(x, u) -> c(x), multiplier=μ) +nothing # hide ``` -Three arcs — unconstrained, boundary, unconstrained — joined at the entry time $t_1$ and exit -time $t_2$: +Since the costate is continuous, the unknowns are $p_0 \in \mathbb{R}^2$ and the junction +times $t_1$ and $t_2$. The target gives two equations, the entry on the boundary, +$c(x(t_1)) = 0$, a third, and the switching condition $p_2(t_1) = 0$ the fourth: ```@example main -function shoot!(s, ξ) +function shoot!(s, ξ, _) p0, t1, t2 = ξ[1:2], ξ[3], ξ[4] - - x_t1, p_t1 = f_interior(t0, x0, p0, t1) - x_t2, p_t2 = f_boundary(t1, x_t1, p_t1, t2) - x_tf, _ = f_interior(t2, x_t2, p_t2, tf) - - s[1:2] = x_tf - xf - s[3] = g(x_t1) # entry: v(t1) = VMAX - s[4] = p_t1[2] # switching condition + x1, p1 = f_interior(t0, x0, p0, t1) + x2, p2 = f_boundary(t1, x1, p1, t2) + x3, _ = f_interior(t2, x2, p2, tf) + s[1:2] = x3 - xf # target + s[3] = c(x1) # entry on the boundary + s[4] = p1[2] # switching condition return nothing end +nothing # hide ``` -```@example main -nle!(s, ξ, _) = shoot!(s, ξ) - -t_grid = time_grid(direct_sol) -x_of_t = state(direct_sol) -p_of_t = costate(direct_sol) +The direct solution gives the initial guess: its costate at $t_0$, and the first and last grid +points where the constraint is active: -p0_guess = p_of_t(t0) -active = findall(t -> 0 ≤ g(x_of_t(t)) ≤ 1e-2, t_grid) -t1_guess = t_grid[first(active)] -t2_guess = t_grid[last(active)] - -ξ_guess = [p0_guess..., t1_guess, t2_guess] +```@example main +tg = time_grid(direct_sol) +x_d = state(direct_sol) -prob = NonlinearProblem(nle!, ξ_guess) -shooting_sol = NonlinearSolve.solve(prob; show_trace=Val(false)) -p0_sol, t1_sol, t2_sol = - shooting_sol.u[1:2], shooting_sol.u[3], shooting_sol.u[4] +active = findall(t -> 0 ≤ c(x_d(t)) ≤ 1e-3, tg) +ξ_guess = [costate(direct_sol)(t0)..., tg[first(active)], tg[last(active)]] ``` ```@example main -s = zeros(4) -shoot!(s, shooting_sol.u) -s +prob = NonlinearProblem(shoot!, ξ_guess) +shooting_sol = NonlinearSolve.solve(prob; show_trace=Val(false)) +p0_sol, t1_sol, t2_sol = shooting_sol.u[1:2], shooting_sol.u[3], shooting_sol.u[4] ``` -### Comparison +The shooting finds the exact values, $p_0 = (38.4, 9.6)$, $t_1 = 0.25$ and $t_2 = 0.75$. + +### [Comparison](@id examples-state-constraint-first-order-comparison) + +The concatenation of the three flows gives the extremal, with the cost $7.68$; the direct cost +differs by the discretisation error: ```@example main φ = f_interior * (t1_sol, f_boundary) * (t2_sol, f_interior) indirect_sol = φ((t0, tf), x0, p0_sol) -plot!( - plt, indirect_sol, :state, :control; - label="Indirect", linestyle=:dash, -) +objective(direct_sol), objective(indirect_sol) +``` + +For the plot, the flows are built with `saveat`, so that the trajectory has 101 points (see +[Plotting a flow](@ref results-plot-flow)): + +```@example main +fine = (saveat=range(t0, tf, 101), dense=false) +φ_plot = Flow(ocp, (x, p) -> p[2]; fine...) * + (t1_sol, Flow(ocp, (x, p) -> 0; constraint=(x, u) -> c(x), multiplier=μ, fine...)) * + (t2_sol, Flow(ocp, (x, p) -> p[2]; fine...)) +plot!(plt, φ_plot((t0, tf), x0, p0_sol); label="indirect", linestyle=:dash) +``` + +```@example main +@assert isapprox(shooting_sol.u, [38.4, 9.6, 0.25, 0.75]; atol=1e-8) # hide +@assert isapprox(objective(indirect_sol), 7.68; atol=1e-6) # hide +@assert isapprox(objective(direct_sol), 7.68; rtol=1e-2) # hide +s = zeros(4); shoot!(s, shooting_sol.u, nothing) # hide +@assert maximum(abs, s) < 1e-10 # hide +nothing # hide ``` -## Second-order constraint: bounding the position +## [A second-order constraint: the Bryson–Denham problem](@id examples-state-constraint-second-order) -### The problem +### [The problem](@id examples-state-constraint-second-order-problem) -A different pair of boundary conditions, $x(0)=(0,1)$, $x(1)=(0,-1)$, constraining the -*position* $q(t) \le a$ instead of the velocity. Since $q$ doesn't appear in $\dot v$, this is a -**second-order** state constraint — the boundary arc forces $v=0$ (not $u=0$ directly), and the -costate picks up a **jump**, not a nonzero running multiplier. +The wagon now starts at $q = 0$ with the velocity $1$, and must come back to $q = 0$ with the +velocity $-1$: $x(0) = (0, 1)$ and $x(1) = (0, -1)$. Its **position** is bounded, +$q(t) \le a$. This is the Bryson–Denham problem[^1]. The constraint is of **second order**: +with $c(x) = a - q$, the control appears only in the second derivative, + +```math +\frac{\mathrm{d}}{\mathrm{d}t} c(x(t)) = -v(t), \qquad +\frac{\mathrm{d}^2}{\mathrm{d}t^2} c(x(t)) = -u(t), +``` + +so along a boundary arc, $v = 0$ and $u = 0$. + +Without the constraint, $u = -2$ and $q(t) = t - t^2$, whose maximum is $1/4$, at $t = 1/2$. +With the constraint, the solution depends on $a$[^3]: + +- for $a \ge 1/4$, the constraint is never active; +- for $1/6 \le a \le 1/4$, the trajectory **touches** $q = a$ at a single time, $t = 1/2$; +- for $a \le 1/6$, the trajectory stays on $q = a$ along a **boundary arc**, on + $[3a, 1 - 3a]$. + +We take $a = 0.2$ for a touch point, and $a = 0.1$ for a boundary arc: ```@example main -t0b = 0.0 -tfb = 1.0 -x0_bd = [0.0, 1.0] -xf_bd = [0.0, -1.0] - -function make_ocp(a) - @def begin - t ∈ [t0b, tfb], time +x0_bd, xf_bd = [0, 1], [0, -1] + +function bryson_denham(a) + return @def begin + t ∈ [t0, tf], time x = (q, v) ∈ R², state u ∈ R, control - x(t0b) == x0_bd - x(tfb) == xf_bd - q(t) ≤ a + x(t0) == x0_bd + x(tf) == xf_bd + ẋ(t) == [v(t), u(t)] 0.5∫(u(t)^2) → min @@ -160,149 +252,195 @@ end nothing # hide ``` -Two regimes, depending on how tight $a$ is: a **touch point** (the trajectory grazes $q=a$ at a -single instant) for a loose bound, and a genuine **boundary arc** for a tight one. - -### Touch-point case ($a=0.2$) +### [Direct method](@id examples-state-constraint-second-order-direct) ```@example main -a_touch = 0.2 -ocp_touch = make_ocp(a_touch) +a_touch, a_arc = 0.2, 0.1 +ocp_touch, ocp_arc = bryson_denham(a_touch), bryson_denham(a_arc) + sol_touch = solve(ocp_touch; grid_size=100, display=false) +sol_arc = solve(ocp_arc; grid_size=100, display=false) -fs_touch = Flow(ocp_touch, (x, p) -> p[2]) -g_touch(x) = a_touch - x[1] +legend_off = (state_style=(legend=false,), costate_style=(legend=false,)) +plt_bd = plot(sol_touch; label="a = 0.2", size=(800, 600), legend_off...) +plot!(plt_bd, sol_arc; label="a = 0.1", linestyle=:dash, legend_off...) ``` -Two unconstrained arcs meeting at the contact instant $t_1$, where $q(t_1)=a$, $v(t_1)=0$, and -the costate's $q$-component jumps by $\Delta p_q$: +### [The touch point](@id examples-state-constraint-touch) -```@example main -function shoot_touch!(s, ξ) - p0, t1, Δpq = ξ[1:2], ξ[3], ξ[4] +For $a = 0.2$, the solution has two unconstrained arcs, on $[0, t_1]$ and $[t_1, 1]$, joined +at the contact time $t_1$, where $q(t_1) = a$ and $v(t_1) = 0$. At $t_1$, the costate +**jumps**: $p_q$ increases by $\Delta p_q \ge 0$, and $p_v$ is continuous. + +By symmetry, $t_1 = 1/2$. On the first arc, $u = p_v$ is affine, so $q$ is a cubic, and the +conditions $q(0) = 0$, $v(0) = 1$, $q(1/2) = a$ and $v(1/2) = 0$ give +$u(t) = -3.2 + 4.8t$. Since $\dot p_v = -p_q$, $p_q = -4.8$ on $[0, 1/2)$, and by symmetry +$p_q = 4.8$ on $(1/2, 1]$: + +```math +p(0) = (-4.8,\ -3.2), \qquad t_1 = 1/2, \qquad \Delta p_q = 9.6, \qquad J = 2.24 . +``` - x_t1, p_t1 = fs_touch(t0b, x0_bd, p0, t1) - p_t1_plus = [p_t1[1] + Δpq, p_t1[2]] - x_tf, _ = fs_touch(t1, x_t1, p_t1_plus, tfb) +The unknowns of the shooting are $p_0$, $t_1$ and $\Delta p_q$; the target, $q(t_1) = a$ and +$v(t_1) = 0$ give four equations. A vector jump on a Hamiltonian flow is a jump of the costate +(see [Multi-phase flows](@ref flows-multi-phase)): - s[1:2] = x_tf - xf_bd - s[3] = g_touch(x_t1) - s[4] = x_t1[2] +```@example main +f_touch = Flow(ocp_touch, (x, p) -> p[2]) + +function shoot_touch!(s, ξ, _) + p0, t1, Δpq = ξ[1:2], ξ[3], ξ[4] + x1, p1 = f_touch(t0, x0_bd, p0, t1) + x2, _ = f_touch(t1, x1, p1 + [Δpq, 0], tf) # costate jump at t1 + s[1:2] = x2 - xf_bd # target + s[3] = a_touch - x1[1] # contact: q(t1) = a + s[4] = x1[2] # tangency: v(t1) = 0 return nothing end +nothing # hide ``` +The direct solution gives the guess: the contact time is where $q$ is closest to $a$, and the +jump is the variation of its costate $p_q$ around that time: + ```@example main -nle_touch!(s, ξ, _) = shoot_touch!(s, ξ) +tg_t = time_grid(sol_touch) +p_t = costate(sol_touch) -t_grid_t = time_grid(sol_touch) -p_of_t_touch = costate(sol_touch) +t1_guess = tg_t[argmin(abs.(a_touch .- first.(state(sol_touch).(tg_t))))] +Δpq_guess = p_t(t1_guess + 0.05)[1] - p_t(t1_guess - 0.05)[1] +ξ_guess_t = [p_t(t0)..., t1_guess, Δpq_guess] -p0_guess_t = p_of_t_touch(t0b) -t1_guess_t = t_grid_t[argmin(abs.(g_touch.(state(sol_touch).(t_grid_t))))] -ε = 0.05 * (tfb - t0b) -Δpq_guess = - p_of_t_touch(t1_guess_t + ε)[1] - p_of_t_touch(t1_guess_t - ε)[1] +shooting_touch = NonlinearSolve.solve(NonlinearProblem(shoot_touch!, ξ_guess_t); show_trace=Val(false)) +p0_touch, t1_touch, Δpq_touch = shooting_touch.u[1:2], shooting_touch.u[3], shooting_touch.u[4] +``` + +These are the exact values. The concatenation with the jump gives the extremal and its cost, +$2.24$: -prob_touch = NonlinearProblem( - nle_touch!, [p0_guess_t..., t1_guess_t, Δpq_guess] -) -shoot_sol_touch = NonlinearSolve.solve(prob_touch; show_trace=Val(false)) -p0_touch, t1_touch, Δpq_touch = - shoot_sol_touch.u[1:2], shoot_sol_touch.u[3], shoot_sol_touch.u[4] +```@example main +φ_touch = f_touch * (t1_touch, [Δpq_touch, 0], f_touch) +indirect_touch = φ_touch((t0, tf), x0_bd, p0_touch) +objective(sol_touch), objective(indirect_touch) ``` ```@example main -f_touch = fs_touch * (t1_touch, [Δpq_touch, 0.0], fs_touch) -indirect_touch = f_touch((t0b, tfb), x0_bd, p0_touch) -plt2 = plot(indirect_touch, :state, :control; label="Indirect (a = 0.2)") +@assert isapprox(shooting_touch.u, [-4.8, -3.2, 0.5, 9.6]; atol=1e-8) # hide +@assert isapprox(objective(indirect_touch), 2.24; rtol=1e-5) # hide +@assert isapprox(objective(sol_touch), 2.24; rtol=1e-2) # hide +nothing # hide ``` -### Boundary-arc case ($a=0.1$) +### [The boundary arc](@id examples-state-constraint-boundary-arc) -A tighter bound turns the touch point into a genuine third arc, $[t_1,t_2]$, on which $q=a$, -$v=0$, $u=0$ — and the costate jumps twice, once on entry and once on exit: +For $a = 0.1$, the solution has three arcs: unconstrained on $[0, t_1]$, on the boundary +$q = a$ on $[t_1, t_2]$, and unconstrained on $[t_2, 1]$. The pseudo-Hamiltonian is -```@example main -a_arc = 0.1 -ocp_arc = make_ocp(a_arc) -sol_arc = solve(ocp_arc; grid_size=100, display=false) +```math +H(x, p, u, \mu) = p_q v + p_v u - \frac{u^2}{2} + \mu\, (a - q). +``` -fs_arc = Flow(ocp_arc, (x, p) -> p[2]) -g_arc(x) = a_arc - x[1] +Along the boundary arc, $u = 0$, so $p_v = 0$ by the maximisation condition. Then +$\dot p_v = -p_q = 0$, and $\dot p_q = \mu = 0$: the multiplier is zero, and the costate is +zero on the arc. It jumps at the entry and at the exit, by $(\Delta p_q^1, 0)$ and +$(\Delta p_q^2, 0)$. -fc_bd = Flow( - ocp_arc, (x, p) -> 0.0; - constraint=(x, u) -> g_arc(x), multiplier=(x, p) -> 0.0, -) +On the first arc, the conditions $q(t_1) = a$, $v(t_1) = 0$ and $u(t_1) = p_v(t_1) = 0$ give +$t_1 = 3a$ and $u(t) = -\frac{2}{3a}\big(1 - \frac{t}{3a}\big)$, so $p_q = -\frac{2}{9a^2}$; the +last arc is symmetric: + +```math +t_1 = 3a = 0.3, \quad t_2 = 1 - 3a = 0.7, \quad +p(0) = \Big( -\frac{2}{9a^2},\ -\frac{2}{3a} \Big), \quad +\Delta p_q^1 = \Delta p_q^2 = \frac{2}{9a^2}, \quad J = \frac{4}{9a} . ``` +The flow on the boundary arc takes the constraint and its multiplier, here zero: + ```@example main -function shoot_arc!(s, ξ) - p0, t1, t2, Δpq1, Δpq2 = ξ[1:2], ξ[3], ξ[4], ξ[5], ξ[6] - - x_t1, p_t1 = fs_arc(t0b, x0_bd, p0, t1) - p_t1_plus = [p_t1[1] + Δpq1, p_t1[2]] - x_t2, p_t2 = fc_bd(t1, x_t1, p_t1_plus, t2) - p_t2_plus = [p_t2[1] + Δpq2, p_t2[2]] - x_tf, _ = fs_arc(t2, x_t2, p_t2_plus, tfb) - - s[1:2] = x_tf - xf_bd - s[3] = g_arc(x_t1) - s[4] = x_t1[2] - s[5] = p_t1_plus[2] - s[6] = p_t1_plus[1] +f_free = Flow(ocp_arc, (x, p) -> p[2]) +f_arc = Flow(ocp_arc, (x, p) -> 0; constraint=(x, u) -> a_arc - x[1], multiplier=(x, p) -> 0) + +function shoot_arc!(s, ξ, _) + p0, t1, t2, Δ1, Δ2 = ξ[1:2], ξ[3], ξ[4], ξ[5], ξ[6] + x1, p1 = f_free(t0, x0_bd, p0, t1) + p1⁺ = p1 + [Δ1, 0] # jump at the entry + x2, p2 = f_arc(t1, x1, p1⁺, t2) + x3, _ = f_free(t2, x2, p2 + [Δ2, 0], tf) # jump at the exit + s[1:2] = x3 - xf_bd # target + s[3] = a_arc - x1[1] # entry: q(t1) = a + s[4] = x1[2] # tangency: v(t1) = 0 + s[5] = p1⁺[2] # p_v = 0 on the arc + s[6] = p1⁺[1] # p_q = 0 on the arc return nothing end +nothing # hide ``` -```@example main -nle_arc!(s, ξ, _) = shoot_arc!(s, ξ) - -t_grid_a = time_grid(sol_arc) -x_of_t_a = state(sol_arc) -p_of_t_a = costate(sol_arc) +The guess comes from the direct solution, as before: -p0_guess_a = p_of_t_a(t0b) -active_a = findall(t -> 0 ≤ g_arc(x_of_t_a(t)) ≤ 1e-2, t_grid_a) -t1_guess_a = t_grid_a[first(active_a)] -t2_guess_a = t_grid_a[last(active_a)] - -εa = 0.1 * (tfb - t0b) -Δpq1_guess = p_of_t_a(t1_guess_a + εa)[1] - p_of_t_a(t1_guess_a - εa)[1] -Δpq2_guess = p_of_t_a(t2_guess_a + εa)[1] - p_of_t_a(t2_guess_a - εa)[1] - -ξ_guess_a = [p0_guess_a..., t1_guess_a, t2_guess_a, Δpq1_guess, Δpq2_guess] - -prob_arc = NonlinearProblem(nle_arc!, ξ_guess_a) -shoot_sol_arc = NonlinearSolve.solve(prob_arc; show_trace=Val(false)) -p0_arc = shoot_sol_arc.u[1:2] -t1_arc, t2_arc = shoot_sol_arc.u[3], shoot_sol_arc.u[4] -Δpq1, Δpq2 = shoot_sol_arc.u[5], shoot_sol_arc.u[6] +```@example main +tg_a = time_grid(sol_arc) +x_a, p_a = state(sol_arc), costate(sol_arc) + +active_a = findall(t -> 0 ≤ a_arc - x_a(t)[1] ≤ 1e-3, tg_a) +t1_g, t2_g = tg_a[first(active_a)], tg_a[last(active_a)] +Δ1_g = p_a(t1_g + 0.1)[1] - p_a(t1_g - 0.1)[1] +Δ2_g = p_a(t2_g + 0.1)[1] - p_a(t2_g - 0.1)[1] +ξ_guess_a = [p_a(t0)..., t1_g, t2_g, Δ1_g, Δ2_g] + +shooting_arc = NonlinearSolve.solve(NonlinearProblem(shoot_arc!, ξ_guess_a); show_trace=Val(false)) +p0_arc, t1_arc, t2_arc = shooting_arc.u[1:2], shooting_arc.u[3], shooting_arc.u[4] +Δ1, Δ2 = shooting_arc.u[5], shooting_arc.u[6] +(t1_arc, t2_arc), (Δ1, Δ2) ``` +The junction times are $3a$ and $1 - 3a$, and the two jumps are equal, $2/(9a^2) \approx 22.2$. +The cost is $4/(9a) \approx 4.44$: + ```@example main -s = zeros(6) -shoot_arc!(s, shoot_sol_arc.u) -s +φ_arc = f_free * (t1_arc, [Δ1, 0], f_arc) * (t2_arc, [Δ2, 0], f_free) +indirect_arc = φ_arc((t0, tf), x0_bd, p0_arc) +objective(sol_arc), objective(indirect_arc) ``` ```@example main -f_arc = - fs_arc * (t1_arc, [Δpq1, 0.0], fc_bd) * (t2_arc, [Δpq2, 0.0], fs_arc) -indirect_arc = f_arc((t0b, tfb), x0_bd, p0_arc) -plot!(plt2, indirect_arc, :state, :control; label="Indirect (a = 0.1)") +a = a_arc # hide +@assert isapprox(shooting_arc.u, [-2 / (9a^2), -2 / (3a), 3a, 1 - 3a, 2 / (9a^2), 2 / (9a^2)]; atol=1e-7) # hide +@assert isapprox(objective(indirect_arc), 4 / (9a); rtol=1e-5) # hide +@assert isapprox(objective(sol_arc), 4 / (9a); rtol=1e-2) # hide +nothing # hide ``` -By symmetry of the problem, the entry and exit times should be close to $3a$ and $1-3a$: +### [Comparison](@id examples-state-constraint-second-order-comparison) + +The two indirect solutions, plotted with flows built with `saveat`: ```@example main -t1_arc, 3a_arc, t2_arc, 1 - 3a_arc +fine = (saveat=range(t0, tf, 101), dense=false) +ft = Flow(ocp_touch, (x, p) -> p[2]; fine...) +ff = Flow(ocp_arc, (x, p) -> p[2]; fine...) +fa = Flow(ocp_arc, (x, p) -> 0; constraint=(x, u) -> a_arc - x[1], multiplier=(x, p) -> 0, fine...) + +plt_ind = plot((ft * (t1_touch, [Δpq_touch, 0], ft))((t0, tf), x0_bd, p0_touch); + label="a = 0.2", size=(800, 600), legend_off...) +plot!(plt_ind, (ff * (t1_arc, [Δ1, 0], fa) * (t2_arc, [Δ2, 0], ff))((t0, tf), x0_bd, p0_arc); + label="a = 0.1", linestyle=:dash, legend_off...) ``` -## See also +The costate $p_q$ jumps once at the touch point, and twice around the boundary arc, where the +costate is zero. + +## Notes + +- [Constrained arcs](@ref flows-constrained-arcs) explains the flows on a boundary arc, with + another Bryson–Denham instance. +- [Multi-phase flows](@ref flows-multi-phase) explains the concatenation of flows, and the jumps. +- [MINPACK.jl](@extref Tutorials Resolution-of-the-shooting-equation) can replace + NonlinearSolve to solve the shooting equations. + +[^1]: A. E. Bryson, W. F. Denham and S. E. Dreyfus, *Optimal programming problems with inequality constraints I: necessary conditions for extremal solutions*, AIAA Journal 1 (1963), no. 11, 2544–2550. [doi.org/10.2514/3.2107](https://doi.org/10.2514/3.2107) + +[^2]: D. H. Jacobson, M. M. Lele and J. L. Speyer, *New necessary conditions of optimality for control problems with state-variable inequality constraints*, Journal of Mathematical Analysis and Applications 35 (1971), 255–284. -- [Constrained arcs](@ref flows-constrained-arcs) — `constraint=`/`multiplier=`, the three ways - to spell the constraint, in general. -- [Multi-phase flows](@ref flows-multi-phase) — flow concatenation and costate jumps, in general. -- [Energy minimisation](@ref examples-double-integrator-energy) — the same wagon, unconstrained. +[^3]: A. E. Bryson and Y.-C. Ho, *Applied Optimal Control: Optimization, Estimation and Control*, CRC Press, 1975. From 8768fc196788534361f9d3646fd170f23fd8dca1 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:44:14 +0200 Subject: [PATCH 51/57] docs(logo): light/dark logo pair (CSS moved to style.css), CairoMakie and Colors named, boundary values asserted, no .extras path Co-Authored-By: Claude Opus 5.5 --- docs/src/.vitepress/theme/style.css | 7 +++ docs/src/examples/logo.md | 79 +++++++++++++++++------------ docs/src/index.md | 7 --- 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/docs/src/.vitepress/theme/style.css b/docs/src/.vitepress/theme/style.css index 0cbcbe34e..e89f109fa 100644 --- a/docs/src/.vitepress/theme/style.css +++ b/docs/src/.vitepress/theme/style.css @@ -475,3 +475,10 @@ mjx-container svg path { .dark .vp-doc img[alt=""] { filter: invert(0.894) hue-rotate(180deg); } + +/* Logo shown in the page body (home page, logo example): exact light/dark SVG pair */ +.oc-logo { width: 200px; margin: 1.5rem auto; } +.oc-logo--light { display: block; } +.oc-logo--dark { display: none; } +.dark .oc-logo--light { display: none; } +.dark .oc-logo--dark { display: block; } diff --git a/docs/src/examples/logo.md b/docs/src/examples/logo.md index 5f90192f0..a89d9b27a 100644 --- a/docs/src/examples/logo.md +++ b/docs/src/examples/logo.md @@ -1,33 +1,38 @@ # [The logo](@id examples-logo) The OptimalControl.jl logo is not a drawing. It is the solution of an optimal control -problem — an **energy-minimal low-thrust orbit transfer** — solved once and repeated by +problem, an **energy-minimal low-thrust orbit transfer**, solved once and repeated by symmetry. -![The OptimalControl.jl logo](../assets/logo.svg) +```@raw html + + +``` A spacecraft spirals from a low circular orbit out to a higher one under continuous weak thrust. Two-body dynamics do not care which way is up, so the same optimal transfer, rotated -by ``\pm 120°``, gives three trajectories at once — and their three departure points sit +by $\pm 120°$, gives three trajectories at once, and their three departure points sit exactly where the three dots of the Julia logo go, winding onto the outer orbit around the central body. -This page rebuilds that figure from scratch. +This page rebuilds that figure from scratch. Besides OptimalControl and NLPModelsIpopt, it +uses [CairoMakie](https://docs.makie.org) and [Colors](https://github.com/JuliaGraphics/Colors.jl) +for the figures: add them to your environment to run it. ## The transfer problem -Normalised two-body dynamics (gravitational parameter ``\mu = 1``). The state is the +Normalised two-body dynamics (gravitational parameter $\mu = 1$). The state is the spacecraft's position and velocity in the orbital plane, ```math x = (x_1, x_2, x_3, x_4) = (\text{position}, \text{velocity}), ``` -and the control ``u = (u_1, u_2)`` is the thrust acceleration. The spacecraft starts on a -circular orbit of radius ``r_0`` and must reach a circular orbit of radius ``r_f``: at the -final time it has to be at the right radius, at the right (circular) speed, and with **zero -radial velocity** — the final angular position is free. Along the way it minimises the thrust -energy ``\int \|u\|^2``. +and the control $u = (u_1, u_2)$ is the thrust acceleration. The spacecraft starts on a +circular orbit of radius $r_0$ and must reach a circular orbit of radius $r_f$: at the final +time it has to be at the right radius, at the right (circular) speed $1/\sqrt{r_f}$, and with +**zero radial velocity**; the final angular position is free. Along the way it minimises the +thrust energy $\int \|u\|^2$. ```@example logo using OptimalControl @@ -55,14 +60,15 @@ ocp = @def begin ∫(u₁(t)^2 + u₂(t)^2) → min end +nothing # hide ``` ## Solving it Low-thrust transfers are hard to solve from a cold start, so we warm-start with a crude -analytical spiral: let the radius grow linearly from ``r_0`` to ``r_f`` and advance the angle -at the local Keplerian rate ``\dot\theta = r^{-3/2}``. (More on initial guesses in -[Initial guess](@ref solve-initial-guess).) +analytical spiral: the radius grows linearly from $r_0$ to $r_f$, and the angle advances at +the local Keplerian rate $\dot\theta = r^{-3/2}$. The guess is a named tuple of functions of +time (see [Initial guess](@ref solve-initial-guess)): ```@example logo k = (rf - r0) / tf @@ -85,14 +91,23 @@ xf = state(sol)(tf) thrust_energy = objective(sol)) ``` -The insertion radius and speed reach their targets and the radial velocity is zero: the -spacecraft arrives tangent to the outer orbit. +The insertion radius and speed reach their targets, $r_f = 2.2$ and +$1/\sqrt{2.2} \approx 0.674$, and the radial velocity is zero: the spacecraft arrives tangent +to the outer orbit. + +```@example logo +@assert successful(sol) # hide +@assert isapprox(sqrt(xf[1]^2 + xf[2]^2), rf; atol=1e-6) # hide +@assert isapprox(sqrt(xf[3]^2 + xf[4]^2), 1 / sqrt(rf); atol=1e-6) # hide +@assert abs(xf[1] * xf[3] + xf[2] * xf[4]) < 1e-6 # hide +nothing # hide +``` ## Looking at the solution -The site's documentation renders solution plots with [Plots.jl](https://docs.juliaplots.org); -here we use the [Makie](https://docs.makie.org) backend instead — load a Makie package and the -`Makie.plot` method for a solution becomes available. +The other pages plot solutions with [Plots.jl](https://docs.juliaplots.org); this one uses +[Makie](https://docs.makie.org) instead. Loading a Makie package makes `Makie.plot` available +for a solution (see [Plot with Makie](@ref results-plot-makie)): ```@example logo using CairoMakie @@ -100,13 +115,13 @@ using CairoMakie Makie.plot(sol) ``` -The thrust (``u_1, u_2``) stays small throughout — this is a *low-thrust* transfer, nudging -the orbit rather than forcing it. The right-hand column is the costate, the adjoint of -Pontryagin's Maximum Principle, which the solver returns alongside the state and control. +The thrust $(u_1, u_2)$ stays small throughout: this is a *low-thrust* transfer, which nudges +the orbit rather than forcing it. The right-hand column is the costate, which the solver +returns with the state and the control (see [Solution object](@ref results-solution)). ## The trajectory in the plane -The logo lives in the ``(x_1, x_2)`` plane. One transfer is a single spiral from the inner +The logo lives in the $(x_1, x_2)$ plane. One transfer is a single spiral from the inner orbit to the outer one. ```@example logo @@ -127,9 +142,10 @@ fig ## Three-fold symmetry -Two-body dynamics are **rotation-equivariant**: if ``t \mapsto x(t)`` is an optimal transfer, -so is the whole trajectory rotated by any fixed angle — same cost, same constraints. Rotating -the solved spiral by ``0`` and ``\pm 2\pi/3`` gives three transfers for the price of one. +Two-body dynamics are **rotation-equivariant**: if $t \mapsto x(t)$ is an optimal transfer, +so is the whole trajectory rotated by any fixed angle, with the same cost and the same +constraints. Rotating the solved spiral by $0$ and $\pm 2\pi/3$ gives three transfers for +the price of one. Colour them with the Julia logo palette, drop a dot at each departure point, add the target orbit in Julia blue and the central body, and the logo is done. @@ -168,14 +184,11 @@ scatter!(ax, Point2f(0, 0); color = jl.blue, markersize = 100) fig ``` -That is the figure the site uses as its logo (`docs/src/assets/logo.svg`). - -The published asset is produced by a slightly more elaborate script — crisper strokes, a few -tuning knobs — kept in the repository at `.extras/logos/logo-gagnant/`. The optimal control -problem it solves is exactly the one above. +That is the figure the site uses as its logo. The published file is drawn with a few more +tuning knobs, from exactly this optimal control problem. ## See also -- [Initial guess](@ref solve-initial-guess) — the warm-start used here, and the others. -- [Plot](@ref results-plot) — the Plots backend, and what a solution plot shows. -- [Example gallery](@ref examples-gallery) — the other worked problems. +- [Initial guess](@ref solve-initial-guess): the warm start used here, and the other forms. +- [Plot with Makie](@ref results-plot-makie) and [Plot](@ref results-plot): plotting a solution. +- [Example gallery](@ref examples-gallery): the other worked problems. diff --git a/docs/src/index.md b/docs/src/index.md index 631a23236..9a67ee22e 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -3,13 +3,6 @@ The OptimalControl.jl package is the root package of the [control-toolbox ecosystem](https://github.com/control-toolbox). The control-toolbox ecosystem gathers Julia packages for mathematical control and applications. It aims to provide tools to model and solve optimal control problems with ordinary differential equations by direct and indirect methods, both on CPU and GPU. ```@raw html - ``` From 0e7c819a49024ff6b8028dac610a6905d3234e11 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:44:14 +0200 Subject: [PATCH 52/57] docs(ecosystem): the role of each package Co-Authored-By: Claude Opus 5.5 --- docs/src/api/ecosystem.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/src/api/ecosystem.md b/docs/src/api/ecosystem.md index 39db5951b..f3ec3dfb3 100644 --- a/docs/src/api/ecosystem.md +++ b/docs/src/api/ecosystem.md @@ -4,10 +4,12 @@ Everything documented in this API reference is reachable from `using OptimalCont OptimalControl is the user-facing entry point of the control-toolbox ecosystem; it re-exports and wires together the lower-level packages below. -- [CTBase](@extref CTBase index) -- [CTDirect](@extref CTDirect index) -- [CTFlows](@extref CTFlows index) -- [CTLie](@extref CTLie index) -- [CTModels](@extref CTModels index) -- [CTParser](@extref CTParser index) -- [CTSolvers](@extref CTSolvers index) +| Package | Role | +| --- | --- | +| [CTBase](@extref CTBase index) | the foundation: shared types (vector fields, Hamiltonians), exceptions, strategies | +| [CTModels](@extref CTModels index) | the optimal control problem and its solution, with their accessors | +| [CTParser](@extref CTParser index) | the abstract syntax, `@def` | +| [CTDirect](@extref CTDirect index) | the direct method: the discretisation of the problem | +| [CTSolvers](@extref CTSolvers index) | the modelers and the solvers of the discretised problem | +| [CTFlows](@extref CTFlows index) | flows of vector fields and Hamiltonians, for the indirect method | +| [CTLie](@extref CTLie index) | Lie derivatives, Lie and Poisson brackets, lifts | From 201fda2dcf70a1184c4974d4b77b09989ca45a6b Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:44:14 +0200 Subject: [PATCH 53/57] docs(migration): title "Migrating from v2.0", the shim messages executed, augment and positional constraint now caught, user-facing tables Co-Authored-By: Claude Opus 5.5 --- docs/make.jl | 2 +- docs/src/index.md | 2 +- docs/src/migration.md | 388 ++++++++++++++++++++++-------------------- 3 files changed, 210 insertions(+), 182 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index cd77faae0..7d084dbb8 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -336,7 +336,7 @@ with_api_reference(src_dir, ext_dir) do api_pages "The logo" => "examples/logo.md", ], "API Reference" => api_pages_final, - "Migrating to v2.1" => "migration.md", + "Migrating from v2.0" => "migration.md", ], plugins=[links], ) diff --git a/docs/src/index.md b/docs/src/index.md index 9a67ee22e..449aaaf5d 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -74,7 +74,7 @@ That is the whole program — model, solve, plot. Each step has its own guide: | [Geometry](@ref geometry-overview) | The Lie-theoretic tools (`Lift`, `ad`, `Poisson`, `@Lie`) behind singular-control problems. | | [Examples](@ref examples-gallery) | A gallery of complete problems worked end to end, direct and indirect, from energy minimisation to state constraints. | | [API reference](@ref api-modelling) | Every re-exported symbol, organised by theme. | -| [Migrating to v2.1](@ref migration) | What changed since v2.0 and how to update your code. | +| [Migrating from v2.0](@ref migration) | What changed since v2.0 and how to update your code. | ## Mathematical formulation diff --git a/docs/src/migration.md b/docs/src/migration.md index 3d9e9c6d9..4d1339fe4 100644 --- a/docs/src/migration.md +++ b/docs/src/migration.md @@ -1,75 +1,57 @@ -# [Migrating to v2.1](@id migration) +# [Migrating from v2.0](@id migration) -!!! note "This page does not execute" +`OptimalControl.jl` v2.1.0-beta reorganised the ecosystem so that every name has exactly one +owner. Most of the changes are mechanical: a `Lie` becomes an `ad`, a `success` becomes a +`successful`, and the old spelling fails with a message naming its replacement. A few changes +are semantic and do **not** announce themselves; they get their own warnings below. - Unlike every other page on this site, the code blocks here deliberately show **spellings - that error** — that is the whole point. They are plain ```` ```julia ```` fences, not - `@example` blocks, and are not run when the docs build. Every error message quoted below - was verified by running it against the current package; treat the page as accurate - reference, not as tested output. +!!! note "The messages need 2.2.0-beta or newer" -`OptimalControl.jl` v2.1.0-beta reorganised the ecosystem so that every name has exactly one -owner. Most of the fallout is mechanical — a `Lie` becomes an `ad`, a `success` becomes a -`successful` — and every mechanical rename fails loudly with a message naming its replacement, -instead of a bare `UndefVarError`. A few changes are semantic and will **not** announce -themselves; those get their own warnings below. - -!!! warning "The informative messages below need 2.2.0-beta, not just v2.1.0-beta" - - Every spelling on this page **stopped working in v2.1.0-beta**, tagged 2026-07-30 — that - is when each old name was removed. The **shims** that turn the mistake into a named, - actionable `PreconditionError` (the block under - [What you get instead of an error](@ref migration-instead-of-error)) shipped two weeks - later, in **2.2.0-beta** (`src/deprecated.jl`, [#855](https://github.com/control-toolbox/OptimalControl.jl/pull/855)). - A copy pinned to exactly `v2.1.0-beta` gets a bare `UndefVarError` or `MethodError` for - every one of them — the same mistake, none of the guidance below. Use `2.2.0-beta` or - newer. - -The full technical record of every breaking change is [`BREAKING.md`](https://github.com/control-toolbox/OptimalControl.jl/blob/main/BREAKING.md) -in the repository root — this page is its user-facing rendering, organised around "what do I -do about it" rather than "what changed and why." Read `BREAKING.md` for the reasoning; read -this page to fix a script. + The old spellings stopped working in v2.1.0-beta. The messages that name their + replacements ([What you get instead of an error](@ref migration-instead-of-error)) came in + 2.2.0-beta. With exactly v2.1.0-beta, the same mistakes give a bare `UndefVarError` or + `MethodError`. + +The full record of the breaking changes, with the reasons, is +[`BREAKING.md`](https://github.com/control-toolbox/OptimalControl.jl/blob/main/BREAKING.md) in +the repository; this page says what to change in a script. ## Start here: `Flow` needs an integrator -This is the single most common first failure. SciML is no longer a hard dependency of the -stack — the user chooses and loads an ODE integrator explicitly: +This is the most common first failure. The ODE integrators are no longer a dependency of the +package: load one before building a flow. ```julia using OptimalControl -using OrdinaryDiffEqTsit5 # ← new, and required before any Flow(...) +using OrdinaryDiffEqTsit5 # required before any Flow(...) f = Flow(ocp, (x, p) -> p[2]) ``` -Without it, `Flow` fails with a clean `ExtensionError` naming `OrdinaryDiffEqTsit5` (or -`OrdinaryDiffEq`/`DifferentialEquations`) as the missing package — not a cryptic dispatch -error. This is by design: it keeps the install cost of the direct (`solve`) path off users who -never write a flow. See [Installation](@ref getting-started-installation) and +Without it, `Flow` fails with an `ExtensionError` that names the package to load. The direct +method, `solve`, does not need it. See [Installation](@ref getting-started-installation) and [Flows overview](@ref flows-overview). ## What was renamed -| v2.0 | v2.1.0-beta | Notes | +| v2.0 | Now | Notes | | --- | --- | --- | -| `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; `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 | -| `f(t0, x0, p0, tf, λ)` / `f(t0, x0, tf, λ)` | `f(t0, x0, p0, tf; variable=λ)` / `f(t0, x0, tf; variable=λ)` | `variable=` is a mandatory keyword on a `NonFixed` problem, never positional | -| `augment=true` | `variable_costate=true` | renamed; returns `(xf, pf, pvf)` instead of `(xf, pf)` | -| `CTSolvers.Modelers.ADNLP()`, `CTDirect.Collocation()` | `OptimalControl.ADNLP()`, `OptimalControl.Collocation()` | neither module name is re-exported; write against `OptimalControl` | -| `time(ocp)`, `time(sol)` | `times(ocp)`, `time_grid(sol)` | `time` is no longer re-exported (it's `Base.time`) | -| `success(sol)` | `successful(sol)` | `success` is no longer re-exported (it's `Base.success`) | +| `OptimalControl.VectorField`, `OptimalControl.Hamiltonian`, … | `VectorField`, `Hamiltonian`, … | exported; the qualified form still works | +| `Lie(X, f)` | `ad(X, f)` | `Lie` throws, naming `ad` | +| `X ⋅ f` | `ad(X, f)` | no operator; `X ⋅ f` throws, naming `ad`, when `X` is a `VectorField` | +| `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=` | on `VectorField`, `Hamiltonian`, `@Lie`, and the other constructors | +| `Flow(f)` with `f::Function` | `Flow(VectorField(f))`, `Flow(Hamiltonian(f))`, `Flow(HamiltonianVectorField(f))` | a function alone does not say which flow to build | +| `Flow(ocp, u, g, μ)` | `Flow(ocp, u; constraint=g, multiplier=μ)` | two keywords, given together | +| `f(t0, x0, p0, tf, λ)`, `f(t0, x0, tf, λ)` | `f(t0, x0, p0, tf; variable=λ)`, `f(t0, x0, tf; variable=λ)` | `variable=` is a keyword, required on a problem with a variable | +| `augment=true` | `variable_costate=true` | returns `(xf, pf, pvf)` | +| `CTSolvers.Modelers.ADNLP()`, `CTDirect.Collocation()` | `OptimalControl.ADNLP()`, `OptimalControl.Collocation()` | the modules `CTSolvers` and `CTDirect` are not exported | +| `time(ocp)`, `time(sol)` | `times(ocp)`, `time_grid(sol)` | `time` is `Base.time` | +| `success(sol)` | `successful(sol)` | `success` is `Base.success` | ## What changed shape -Three call conventions changed signature, not just spelling. - -**The flow call.** There is no positional slot for the variable any more: +**The flow call.** The variable is passed by keyword, never by position: ```julia f(t0, x0, p0, tf, λ) # [!code --] @@ -78,34 +60,32 @@ f(t0, x0, p0, tf; variable=λ) # [!code ++] f(t0, x0, p0, tf; variable=λ, variable_costate=true) # [!code ++] ``` -`variable=` is mandatory on a `NonFixed` problem — omitting it does not silently default, it -throws naming exactly what to pass. New in v2.1.0-beta: `unsafe=false` — with `unsafe=true` the -ODE return code is not checked and integration failures don't throw, useful inside a shooting -loop where an intermediate failure should surface through the residual instead. Also new: -per-call integrator options (`saveat=`, `abstol=`, `reltol=`, `alg=`, …) no longer exist — the -call signature only accepts `variable`, `unsafe`, `variable_costate`. Pass integrator options -at **construction** time instead, where they still work exactly as before: +On a problem with a variable, `variable=` is required: without it, the call throws, saying what +to pass. The call also accepts `unsafe=true`, which does not check the return code of the +integrator: inside a shooting function, a failed integration then shows in the residual +instead of stopping the solver (see [Shooting](@ref flows-shooting)). The integrator options +(`saveat=`, `abstol=`, `reltol=`, `alg=`, …) are no longer accepted by the call. Pass them when +the flow is built: ```julia -# after — set once, at construction, not per call f = Flow(ocp, (x, p) -> p[2]; abstol=1e-8) f(t0, x0, p0, tf) ``` -**Constrained flows.** Positional arguments became a keyword pair: +**Constrained flows.** The constraint and the multiplier are keywords, given together: ```julia -fb = Flow(ocp, u, g, μ) # 3 positional [!code --] -fb = Flow(ocp, u; constraint=g, multiplier=μ) # paired keywords [!code ++] +fb = Flow(ocp, u, g, μ) # [!code --] +fb = Flow(ocp, u; constraint=g, multiplier=μ) # [!code ++] ``` -The two keywords are a pair: one without the other is an `IncorrectArgument`. `constraint=` -gained a capability along the way — besides a plain `Function` or a `Data.PathConstraint`, it -now also accepts a `Symbol` naming a `:path` constraint already declared in the OCP: -`constraint=:vmax`. +One without the other is an `IncorrectArgument`. `constraint=` accepts a function, a typed +constraint, or a `Symbol` naming a path constraint of the problem, `constraint=:vmax`. The +`Symbol` form uses the constraint function stored in the problem, without its bound: see the +caveat on [Constrained arcs](@ref flows-constrained-arcs). -**Constructor keywords take an `is_` prefix**, on `VectorField`, `Hamiltonian`, `@Lie`, and -every sibling `Data` constructor: +**Constructor keywords take an `is_` prefix**, on `VectorField`, `Hamiltonian`, `@Lie`, and the +other constructors: ```julia VectorField(f; autonomous=false, variable=true) # [!code --] @@ -114,157 +94,205 @@ VectorField(f; is_autonomous=false, is_variable=true) # [!code ++] @Lie [X, Y] is_autonomous=false # [!code ++] ``` -The old spelling on `@Lie` raises an `IncorrectArgument` at macro-expansion time — loud, not -silently ignored. On the plain `Data` constructors it is not shimmed (see below): Julia cannot -dispatch on a keyword *name*, and a workaround would need to duplicate CTBase's own trait -detection across 14 entry points. +The old keyword on `@Lie` raises an `IncorrectArgument` that lists the accepted keywords. On +`VectorField` and `Hamiltonian`, it raises a `MethodError`, "got unsupported keyword argument". ## [What changed meaning silently](@id migration-silent) -Three changes will not announce themselves as an error. Each is confirmed by running it against -the current package. +Three changes do not raise an error. !!! warning "`Lift(f::Function)` is no longer an `AbstractHamiltonian`" - In v2.0, lifting a plain function and lifting a typed `VectorField` produced the same kind - of object. In v2.1.0-beta only the typed path does: + In v2.0, lifting a plain function and lifting a `VectorField` gave the same kind of object. + Now, the lift of a function is a function, `OptimalControl.LiftedHamiltonianFunction`: ```julia - H = Lift(f) # f::Function - H isa AbstractHamiltonian # false in v2.1.0-beta, true in v2.0 + H = Lift(f) # f::Function + H isa AbstractHamiltonian # false now, true in v2.0 ``` - `H` is now an `OptimalControl.LiftedHamiltonianFunction`, `<: Function` only. Any `isa` or - `<:` test against the old hierarchy is quietly wrong. `Lift(X::AbstractVectorField)` is - unaffected — only the bare-function overload changed. See [Lift](@ref geometry-lift). + A test `isa AbstractHamiltonian` on it is now false. The lift of a `VectorField` is still a + `Hamiltonian`. See [Lift](@ref geometry-lift). -!!! warning "`OpenLoop` rejects the wrong arity only when the flow runs, not when it's built" +!!! warning "`OpenLoop` checks the arity of the law when the flow runs" - An open-loop control must be a function of time, `u(t)` (or `u(t, v)`). A bare constant - closure **constructs without error** and only fails once the flow actually runs: + An open-loop control is a function of time, `u(t)`, or `u(t, v)` with a variable. A + function without argument is accepted when the law is built, and fails only when the flow + runs, with a `MethodError` far from the mistake + ([CTBase#570](https://github.com/control-toolbox/CTBase.jl/issues/570)): ```julia - bad_law = OpenLoop(() -> 1.0) # constructs fine — the trap - Flow(ControlledVectorField(fc), bad_law)(t0, x0, tf) # MethodError here, not above [!code error] + bad_law = OpenLoop(() -> 1.0) # accepted + Flow(ControlledVectorField(fc), bad_law)(t0, x0, tf) # MethodError here [!code error] ``` - The `MethodError` points at the call site, far from the actual mistake at construction. - `is_autonomous` is not a real choice for `OpenLoop` either way — passing it emits a `@warn` - that it has no effect, rather than silently doing nothing. See - [Simulation](@ref flows-simulation). + See [Simulation](@ref flows-simulation). -!!! warning "The old 4-positional state-flow call silently misreads on an `OptimalControlFlow`" +!!! warning "The old state-flow call runs on a flow with a costate" - v2.0's state-flow convention was `f(t0, x0, tf, λ)` — 4 positional arguments. v2.1.0-beta's - genuine (non-deprecated) call for a `Fixed`-time `OptimalControlFlow` is also 4 positional: - `f(t0, x0, p0, tf)`. If a v2.0 script calls the old convention against an - `OptimalControlFlow` with a real number in the 4th slot, it **matches** the new signature - instead of erroring — `tf` silently becomes `p0` and `λ` silently becomes `tf`: + In v2.0, a flow of the state was called `f(t0, x0, tf, λ)`, with the variable $\lambda$ in + the 4th position. The flow of a problem without a variable is called + `f(t0, x0, p0, tf)`, also with four arguments. Called the old way, it **runs**, with the + arguments shifted: `tf` is taken as `p0`, and `λ` as `tf`. ```julia - f(t0, x0, tf, λ) # intended: state flow, tf and lambda as before [!code warning] - # actually runs as f(t0, x0, p0=tf, tf=λ) — arguments shifted by one position + f(t0, x0, tf, λ) # runs as f(t0, x0, p0=tf, tf=λ) [!code warning] ``` - It throws only when the 4th argument's type doesn't match `p0`'s expected shape — so - whether this is caught depends entirely on what happens to be in that slot. There is no - shim for this: the deprecated 4-positional signature is disjoint on `AbstractStateFlow` (a - plain vector-field flow, unaffected) but genuinely ambiguous on `AbstractHamiltonianFlow`, - where it collides with the real v2.1.0-beta call. This one is on the caller to catch by eye. + It fails only when the shapes do not match. Check these calls by hand. ## [What you get instead of an error](@id migration-instead-of-error) -Every removed spelling below throws `CTBase.Exceptions.PreconditionError` naming its -replacement — confirmed by running each one against **2.2.0-beta** (the shims are not in the -bare `v2.1.0-beta` tag; see the note near the top of this page): +The removed spellings throw a `PreconditionError` naming their replacement. With a problem +`ocp` with a variable, its solution `sol`, a vector field `X` and a function `f`: -```julia -julia> Lie(X, f) -ERROR: PreconditionError: `Lie(X, f) / Lie(X, Y)` is deprecated -Reason this spelling was removed in v2.1.0-beta -Hint use ad(X, f) or ad(X, Y) - -julia> X ⋅ f -ERROR: PreconditionError: `X \cdot f` is deprecated -Reason this spelling was removed in v2.1.0-beta -Hint use ad(X, f) - -julia> HamiltonianLift(f) -ERROR: PreconditionError: `HamiltonianLift` is deprecated -Reason this spelling was removed in v2.1.0-beta -Hint use Lift(f) for a plain function, or CTLie.LiftedHamiltonianFunction - -julia> time(ocp) -ERROR: PreconditionError: `time(ocp)` is deprecated -Reason this spelling was removed in v2.1.0-beta -Hint use times(ocp) - -julia> time(sol) -ERROR: PreconditionError: `time(sol)` is deprecated -Reason this spelling was removed in v2.1.0-beta -Hint use time_grid(sol) - -julia> success(sol) -ERROR: PreconditionError: `success(sol)` is deprecated -Reason this spelling was removed in v2.1.0-beta -Hint use successful(sol) - -julia> Flow(f) # f::Function -ERROR: PreconditionError: `Flow(f::Function)` is deprecated -Reason this spelling was removed in v2.1.0-beta -Hint use Flow(VectorField(f)), Flow(Hamiltonian(f)), or Flow(HamiltonianVectorField(f)) - -julia> f(t0, x0, p0, tf, λ) # 5-positional, on a Hamiltonian flow -ERROR: PreconditionError: `f(t0, x0, p0, tf, lambda)` is deprecated -Reason this spelling was removed in v2.1.0-beta -Hint use f(t0, x0, p0, tf; variable=lambda) - -julia> f(t0, x0, tf, λ) # 4-positional, on a plain state flow -ERROR: PreconditionError: `f(t0, x0, tf, lambda)` is deprecated -Reason this spelling was removed in v2.1.0-beta -Hint use f(t0, x0, tf; variable=lambda) +```@setup migration +using OptimalControl +using OrdinaryDiffEqTsit5 +using NLPModelsIpopt + +ocp = @def begin + tf ∈ R, variable + t ∈ [0, tf], time + x = (q, v) ∈ R², state + u ∈ R, control + -1 ≤ u(t) ≤ 1 + tf ≥ 0 + x(0) == [-1, 0] + x(tf) == [0, 0] + ẋ(t) == [v(t), u(t)] + tf → min +end +sol = solve(ocp; display=false) + +X = VectorField(x -> [x[2], -x[1]]) +f(x) = x[1]^2 + +t0, x0, p0, tf, λ = 0, [-1.0, 0.0], [1.0, 1.0], 1.0, 2.0 +fh = Flow(ocp, (x, p, v) -> 1) # a flow of the problem +fs = Flow(VectorField((x, v) -> -v * x; is_variable=true)) # a flow of the state, with a variable +g(x, u, v) = x[1] +μ(x, p, v) = 0 ``` -The last row is the disjoint, safe half of the previous section's warning: on a plain -`AbstractStateFlow` (not an `OptimalControlFlow`), the 4-positional call is unambiguous and -shimmed cleanly. The hazard is specific to `OptimalControlFlow`/`AbstractHamiltonianFlow`. +```@repl migration +try # hide +Lie(X, f) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` -## What could not be shimmed, and why +```@repl migration +try # hide +X ⋅ f +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` -Julia dispatches on argument types and arity, not on keyword names or "this call used to mean -something else" — a few removed spellings can't be intercepted without either overwriting a -method CTFlows still needs, or duplicating trait-detection logic that belongs upstream. +```@repl migration +try # hide +HamiltonianLift(f) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` + +```@repl migration +try # hide +time(ocp) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` + +```@repl migration +try # hide +time(sol) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` + +```@repl migration +try # hide +success(sol) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` -| Spelling | What actually happens today | Why it isn't shimmed | +```@repl migration +try # hide +Flow(f) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` + +```@repl migration +try # hide +Flow(ocp, (x, p, v) -> 0, g, μ) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` + +On the flow `fh` of the problem, and on the flow `fs` of a state with a variable: + +```@repl migration +try # hide +fh(t0, x0, p0, tf, λ) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` + +```@repl migration +try # hide +fs(t0, 1.0, tf, λ) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` + +```@repl migration +try # hide +fh(t0, x0, p0, tf; variable=λ, augment=true) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` + +`X ⋅ f` is caught when `X` is a `VectorField`. With a plain function `X`, as in v2.0, it raises +a `MethodError` about `iterate`. + +## What raises another error + +| Spelling | What happens | Write instead | | --- | --- | --- | -| `Flow(ocp, u, g, μ)` (3 positional) | `PreconditionError`, thrown by CTFlows (not by `OptimalControl`), naming the keyword form: `Flow(ocp, u; constraint=g, multiplier=μ)` | A 4-arity shim here would overwrite CTFlows' own method for this exact signature — and it need not, CTFlows guards it directly ([CTFlows#401](https://github.com/control-toolbox/CTFlows.jl/issues/401), fixed). | -| `augment=true` | Bare `MethodError`, not caught anywhere | Same positional signature as the still-valid call — a shim would overwrite CTFlows' own `OptimalControlFlow` method and breaks precompilation (`Method overwriting is not permitted`). Filed as [CTFlows#402](https://github.com/control-toolbox/CTFlows.jl/issues/402). | -| `CTSolvers.Modelers.ADNLP()`, `CTDirect.Collocation()` | `UndefVarError` on `CTSolvers` / `CTDirect` — the module name is not bound under `using OptimalControl` | `CTSolvers`, `CTDirect` and `CTParser` are implementation packages and are deliberately **not** re-exported (unlike `CTBase`, `CTModels`, `CTFlows`, `CTLie`), so there is no bound name to attach a throwing method to. The constructors themselves *are* re-exported: write `OptimalControl.ADNLP()` / `OptimalControl.Collocation()`. | -| Per-call integrator options (`saveat=`, `abstol=`, `reltol=`, `alg=`, …) | Bare `MethodError` | Same reason: the call signature is closed to `variable`/`unsafe`/`variable_costate` only; a shim would overwrite CTFlows' own call method. Unlike the two rows above, this looks like a deliberate design choice (options belong at construction, baked into the flow's type), not a bug — not filed upstream. Construction-time options are unaffected. | -| `@Lie [X, Y] autonomous=false` | `IncorrectArgument`, raised at macro-expansion time | Already thrown upstream in CTLie — nothing to add. | -| `autonomous=`/`variable=`/`inplace=` on `VectorField`, `Hamiltonian`, and other `Data` constructors | Silently ignored or `MethodError`, depending on the constructor | Julia cannot dispatch on a keyword's *name*; a shim would mean re-detecting the trait by hand at all 14 entry points, duplicating logic CTBase already owns. | +| `f(t0, x0, p0, tf; saveat=…)`, and the other integrator options on a call | `MethodError`, "unsupported keyword argument" | `Flow(…; saveat=…)`, when the flow is built | +| `CTSolvers.Modelers.ADNLP()`, `CTDirect.Collocation()` | `UndefVarError` on `CTSolvers` or `CTDirect` | `OptimalControl.ADNLP()`, `OptimalControl.Collocation()` | +| `@Lie [X, Y] autonomous=false` | `IncorrectArgument`, with the accepted keywords | `@Lie [X, Y] is_autonomous=false` | +| `VectorField(f; autonomous=false)`, `Hamiltonian(h; variable=true)` | `MethodError`, "unsupported keyword argument" | `is_autonomous=`, `is_variable=` | -## Legacy initial guesses +## Initial guesses as named tuples -The `@init` macro ([Initial guess](@ref solve-initial-guess)) is the documented way to build an -initial guess in v2.1.0-beta. The older direct `NamedTuple` construction still works — -it was never removed — it's just no longer shown anywhere in the current docs, so there is no -way to discover it still works without this page: +[`@init`](@ref solve-initial-guess) is the recommended way to write an initial guess. The +named tuple of v2.0 still works, with the fields `state`, `control` and `variable` (see +[Initial guess](@ref solve-initial-guess)), and also with the labels of the components: ```julia -# still works, not the documented path sol = solve(ocp; init=(state=[-0.2, 0.1], control=-0.2)) - -# component labels work too sol = solve(ocp; init=(q=-1.0, v=0.0, u=0.1, tf=2.0)) ``` -Prefer `@init` for new code — it validates labels against the problem and reads closer to the -mathematics. There is still no way to provide an initial guess for the costate/multipliers -directly; that remains true in both forms. +`@init` checks the labels against the problem. In both forms, the costate and the multipliers +cannot be given a guess. -## v1.x → v2.0 +## From v1.x to v2.0 -For the older v1.x → v2.0 migration (the direct-mode API rewrite, option routing, the methods -system), see the second section of [`BREAKING.md`](https://github.com/control-toolbox/OptimalControl.jl/blob/main/BREAKING.md#breaking-changes-v1x--v20) -directly — it predates this page and isn't duplicated here. +For the older migration from v1.x to v2.0 (the direct-mode API, option routing, the methods), +see the second section of +[`BREAKING.md`](https://github.com/control-toolbox/OptimalControl.jl/blob/main/BREAKING.md#breaking-changes-v1x--v20). From 1ecccde7beb2dc345a2c0cb3e89ef922ae8bd2f8 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 16:45:25 +0200 Subject: [PATCH 54/57] docs: C8 link-text sweep, link texts are the target titles Co-Authored-By: Claude Opus 5.5 --- docs/src/flows/shooting.md | 2 +- docs/src/flows/simulation.md | 2 +- docs/src/getting-started/first-problem.md | 2 +- docs/src/modelling/abstract-syntax.md | 4 ++-- docs/src/modelling/formulation.md | 4 ++-- docs/src/modelling/functional-api.md | 10 +++++----- docs/src/modelling/inspect.md | 2 +- docs/src/results/plot.md | 4 ++-- docs/src/results/solution.md | 12 ++++++------ 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/src/flows/shooting.md b/docs/src/flows/shooting.md index b72d59960..d592fac0f 100644 --- a/docs/src/flows/shooting.md +++ b/docs/src/flows/shooting.md @@ -182,7 +182,7 @@ its entry and exit times, with the junction conditions (see the cost, its transversality condition uses the costate of the variable, computed with `variable_costate=true` (see [From an OCP](@ref flows-from-ocp-variable-costate)). The [examples](@ref examples-gallery) solve several such problems, for instance -[Turnpike](@ref examples-turnpike), with two switching times around a singular arc. +[Turnpike (bang–singular–bang)](@ref examples-turnpike), with two switching times around a singular arc. ## See also diff --git a/docs/src/flows/simulation.md b/docs/src/flows/simulation.md index 786bf9ce2..fe676b946 100644 --- a/docs/src/flows/simulation.md +++ b/docs/src/flows/simulation.md @@ -78,7 +78,7 @@ end # hide Built from an optimal control problem, the simulation also computes the cost of the control. On the energy-minimal double integrator, the optimal control is $u(t) = 6 - 12t$, with -cost $6$ (see [First problem](@ref getting-started-first-problem)): +cost $6$ (see [Your first problem](@ref getting-started-first-problem)): ```@example main t0, tf = 0, 1 diff --git a/docs/src/getting-started/first-problem.md b/docs/src/getting-started/first-problem.md index 428d7ca94..bd32818ce 100644 --- a/docs/src/getting-started/first-problem.md +++ b/docs/src/getting-started/first-problem.md @@ -106,7 +106,7 @@ returns a `Number`, not a length-1 vector (see the state(sol)(0.5), control(sol)(0.25) ``` -See [Solution](@ref results-solution) for everything else a solution carries, and +See [Solution object](@ref results-solution) for everything else a solution carries, and [Plot](@ref results-plot) for what else `plot` can show. ## Check it diff --git a/docs/src/modelling/abstract-syntax.md b/docs/src/modelling/abstract-syntax.md index d3ed18532..9df8e3a86 100644 --- a/docs/src/modelling/abstract-syntax.md +++ b/docs/src/modelling/abstract-syntax.md @@ -248,7 +248,7 @@ nothing # hide There is no dedicated syntax for "no control": omitting the control declaration entirely *is* the syntax — do not declare a dummy `u ∈ R, control` with `u(t) == 0`, and never write `control!(pre, 0)` on the [functional API](@ref modelling-functional-api), which is rejected -as an error. See [No control](@ref modelling-without-control) for the full +as an error. See [Control-free problems](@ref modelling-without-control) for the full worked examples above, solved both directly and indirectly. ## [Dynamics](@id modelling-abstract-syntax-dynamics) @@ -773,5 +773,5 @@ problems written with this syntax, see the [example gallery](@ref examples-galle - [Formulation](@ref modelling-formulation) — the mathematics this syntax encodes. - [Functional API](@ref modelling-functional-api) — build the same model without the macro. -- [No control](@ref modelling-without-control) — the control-free case, worked in full. +- [Control-free problems](@ref modelling-without-control) — the control-free case, worked in full. - [Inspect a problem](@ref modelling-inspect) — read a built model back. diff --git a/docs/src/modelling/formulation.md b/docs/src/modelling/formulation.md index 3b9caa758..335368733 100644 --- a/docs/src/modelling/formulation.md +++ b/docs/src/modelling/formulation.md @@ -72,7 +72,7 @@ v_{\mathrm{lower}} \le v \le v_{\mathrm{upper}}. Nothing above requires a control: taking $m = 0$ (no $u$) is a legitimate degenerate case. It is used to fit or optimise the parameters $v$ of a dynamical system rather than to steer it. -See [No control](@ref modelling-without-control) for how to declare and solve such problems. +See [Control-free problems](@ref modelling-without-control) for how to declare and solve such problems. ## [Notation and conventions](@id modelling-formulation-conventions) @@ -141,5 +141,5 @@ instance, with the cost term $\omega^2$ and $p^0 = -1$, $p_\omega(t_f) = -2\omeg - [Abstract syntax (`@def`)](@ref modelling-abstract-syntax) — write this formulation directly as Julia code. - [Functional API](@ref modelling-functional-api) — build the same model without the macro. -- [No control](@ref modelling-without-control) — the $m = 0$ case. +- [Control-free problems](@ref modelling-without-control) — the $m = 0$ case. - [Flows](@ref flows-overview) — build the Hamiltonian flows of the indirect method. diff --git a/docs/src/modelling/functional-api.md b/docs/src/modelling/functional-api.md index 05aa86b88..21fc5939c 100644 --- a/docs/src/modelling/functional-api.md +++ b/docs/src/modelling/functional-api.md @@ -348,11 +348,11 @@ println( nothing # hide ``` -Full worked story (direct + indirect): [Time minimisation](@ref examples-double-integrator-time) · [example gallery](@ref examples-gallery). +Full worked story (direct + indirect): [Time minimisation (bang–bang)](@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**. +No control anywhere: `control!` is simply never called (see [Control-free problems](@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 @@ -447,7 +447,7 @@ println( nothing # hide ``` -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). +Full worked story (direct + indirect): [Parameter estimation without a control](@ref examples-control-free), [Control-free problems](@ref modelling-without-control) · [example gallery](@ref examples-gallery). ### 4. Control and variable together @@ -881,7 +881,7 @@ f!(r, t, x, u, v) = (r[1] = -x + u; nothing) # r vector; x,u scalars - `variable!` → before `time!` when using free-time indices (`indf`, `ind0`) - `variable!` → before `dynamics!` and `objective!` - `dynamics!` and `objective!` → after `time!` and `state!` -- `control!` is optional and, when used, has no ordering constraint of its own beyond needing `state!` first (component-count validation) — see [No control](@ref modelling-without-control) for what omitting it means. +- `control!` is optional and, when used, has no ordering constraint of its own beyond needing `state!` first (component-count validation) — see [Control-free problems](@ref modelling-without-control) for what omitting it means. ## Equivalence @@ -891,5 +891,5 @@ The abstract and functional forms are not just "meant to agree": it is a tested - [Formulation](@ref modelling-formulation) — the mathematics this API builds. - [Abstract syntax (`@def`)](@ref modelling-abstract-syntax) — the macro alternative. -- [No control](@ref modelling-without-control) — omitting `control!` entirely. +- [Control-free problems](@ref modelling-without-control) — omitting `control!` entirely. - [Inspect a problem](@ref modelling-inspect) — read a built model back. diff --git a/docs/src/modelling/inspect.md b/docs/src/modelling/inspect.md index 284ee4efe..b9d4fa9e1 100644 --- a/docs/src/modelling/inspect.md +++ b/docs/src/modelling/inspect.md @@ -323,7 +323,7 @@ is_nonautonomous(ocp) # the negation of is_autonomous The time accessor is `times(ocp)`, shown above. `time` is Julia's `Base.time` (the wall-clock time): calling `time(ocp)` raises an error whose hint is `use times(ocp)`. Coming from v2.0? -See [Migration](@ref migration). +See [Migrating from v2.0](@ref migration). ## Signatures and `is_*` aliases diff --git a/docs/src/results/plot.md b/docs/src/results/plot.md index f3a35df6f..6a16eae88 100644 --- a/docs/src/results/plot.md +++ b/docs/src/results/plot.md @@ -34,7 +34,7 @@ your own figure from the trajectories, see [Custom plots](@ref results-plot-cust ## Getting started We take the energy-minimal double integrator, whose solution is u(t) = 6 − 12t (see -[First problem](@ref getting-started-first-problem)): +[Your first problem](@ref getting-started-first-problem)): ```@example main using OptimalControl @@ -421,5 +421,5 @@ plot!(::Plots.Plot, ::CTModels.Solution, ::Symbol...) - [Solution object](@ref results-solution): the functions this page draws. - [Plot with Makie](@ref results-plot-makie): the same figures with Makie. -- [Save and load](@ref results-save-load): write a solution to disk instead of plotting it. +- [Save & load](@ref results-save-load): write a solution to disk instead of plotting it. - [Flows](@ref flows-overview): how to build the `Flow` of the last section. diff --git a/docs/src/results/solution.md b/docs/src/results/solution.md index 27ca52a02..85e1d1836 100644 --- a/docs/src/results/solution.md +++ b/docs/src/results/solution.md @@ -9,7 +9,7 @@ a *model* back, this one reads a *solution* back. We take the energy-minimal double integrator, whose solution is known in closed form: u(t) = 6 − 12t, q(t) = −1 + 3t² − 2t³, v(t) = 6t − 6t², and the cost is J = 6 (see -[First problem](@ref getting-started-first-problem)). +[Your first problem](@ref getting-started-first-problem)). ```@example main using OptimalControl @@ -40,7 +40,7 @@ sol ``` and plots in one line, once a plotting package is loaded (see -[Plot a solution](@ref results-plot)): +[Plot](@ref results-plot)): ```@example main using Plots @@ -234,7 +234,7 @@ nothing # hide end # hide ``` - See [Migration](@ref migration) for every renamed function. + See [Migrating from v2.0](@ref migration) for every renamed function. What to do when a solve fails (a better initial guess, a finer grid, other tolerances) is the subject of [Initial guess](@ref solve-initial-guess) and [Options](@ref solve-options). @@ -409,6 +409,6 @@ nothing # hide ## See also - [Inspect a problem](@ref modelling-inspect): the model-side mirror of this page. -- [Plot a solution](@ref results-plot): draw everything read here. -- [Save and load](@ref results-save-load): write a solution to disk and read it back. -- [Migration](@ref migration): every renamed function, `success` → `successful` included. +- [Plot](@ref results-plot): draw everything read here. +- [Save & load](@ref results-save-load): write a solution to disk and read it back. +- [Migrating from v2.0](@ref migration): every renamed function, `success` → `successful` included. From 0e1a325bd7531dd1ecf2fae21c88a32fdb3ab99d Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 18:47:06 +0200 Subject: [PATCH 55/57] docs(shooting): hide the SciML log of the failed integration, keep the SolverFailure Co-Authored-By: Claude Opus 5.5 --- docs/src/flows/shooting.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/flows/shooting.md b/docs/src/flows/shooting.md index d592fac0f..b0549d4bc 100644 --- a/docs/src/flows/shooting.md +++ b/docs/src/flows/shooting.md @@ -100,7 +100,9 @@ residual is just large. On $\dot x = x^2$, which blows up before $t = 1$ from $x ```@repl main f_blowup = Flow(VectorField(x -> x^2)); try # hide +Base.CoreLogging.with_logger(Base.CoreLogging.NullLogger()) do # hide f_blowup(0, 10.0, 1) +end # hide catch e # hide showerror(IOContext(stdout, :color => false), e) # hide end # hide From 22a64de2f7d2d21917739ebaf078fa2d1f3af61f Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 18:47:06 +0200 Subject: [PATCH 56/57] docs: draft = false again, the review is complete (full build green) Co-Authored-By: Claude Opus 5.5 --- docs/make.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 7d084dbb8..42564b326 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -153,10 +153,10 @@ cp( # the rest execute, add to its `@meta` block: #= ```@meta -Draft = false +Draft = true ``` =# -draft = true +draft = false # ═══════════════════════════════════════════════════════════════════════════════ # Load extensions From d3cc96dcc7c5244f2db8d0c0cca32e4e3f59c709 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Fri, 9 Oct 2026 19:12:25 +0200 Subject: [PATCH 57/57] docs: refresh the reproducibility snapshot (CTBase 0.30.6, CTModels 0.19.6, CTSolvers 0.5.8, OrdinaryDiffEqVerner) Co-Authored-By: Claude Sonnet 5.5 --- docs/src/assets/Manifest.toml | 14 +++++++------- docs/src/assets/Project.toml | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/src/assets/Manifest.toml b/docs/src/assets/Manifest.toml index 4a78ef759..22c47be96 100644 --- a/docs/src/assets/Manifest.toml +++ b/docs/src/assets/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.12.6" manifest_format = "2.0" -project_hash = "a14c68496fdaa17672e818216e5678891cf15975" +project_hash = "7cc6ba0d8258886de4bdb36930b6810e9d0d399e" [[deps.ADNLPModels]] deps = ["ADTypes", "DifferentiationInterface", "ForwardDiff", "LinearAlgebra", "NLPModels", "ReverseDiff", "SparseArrays", "SparseConnectivityTracer", "SparseMatrixColorings"] @@ -257,9 +257,9 @@ version = "1.0.1+0" [[deps.CTBase]] deps = ["ADTypes", "DocStringExtensions"] -git-tree-sha1 = "b09e22a9bb8884cbc416e5d258e40d609abc1e0b" +git-tree-sha1 = "5fc0998b8071b07552d0ed721030c1fa9c7527e3" uuid = "54762871-cc72-4466-b8e8-f6c8b58076cd" -version = "0.30.5" +version = "0.30.6" [deps.CTBase.extensions] CTBaseDifferentiationInterface = ["DifferentiationInterface"] @@ -307,9 +307,9 @@ version = "0.2.3" [[deps.CTModels]] deps = ["CTBase", "DocStringExtensions", "LinearAlgebra", "MLStyle", "MacroTools", "OrderedCollections", "Parameters", "RecipesBase"] -git-tree-sha1 = "d652e0b512a6e2b526047f04335ccf3a61a66331" +git-tree-sha1 = "5de55c28bac7630e10ddad883f77ef2c6ed84482" uuid = "34c4fa32-2049-4079-8329-de33c2a22e2d" -version = "0.19.5" +version = "0.19.6" weakdeps = ["JLD2", "JSON3", "Makie", "Plots"] [deps.CTModels.extensions] @@ -330,9 +330,9 @@ weakdeps = ["ExaModels", "LinearAlgebra"] [[deps.CTSolvers]] deps = ["CTBase", "CTModels", "CommonSolve", "DocStringExtensions", "KernelAbstractions", "SolverCore"] -git-tree-sha1 = "3c1574071021d1930a0a67c5478c8b92b1d5caf4" +git-tree-sha1 = "50e6edf7726eb7d620323fb4cc3c2e87392022c3" uuid = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6" -version = "0.5.7" +version = "0.5.8" [deps.CTSolvers.extensions] CTSolversADNLPModels = "ADNLPModels" diff --git a/docs/src/assets/Project.toml b/docs/src/assets/Project.toml index 0a0a8505a..30d7aef81 100644 --- a/docs/src/assets/Project.toml +++ b/docs/src/assets/Project.toml @@ -31,6 +31,7 @@ NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" +OrdinaryDiffEqVerner = "79d7bb75-1356-48c1-b8c0-6832512096c2" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05"