Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 23 additions & 6 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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 <img> 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,
)

Expand Down
29 changes: 29 additions & 0 deletions docs/src/.vitepress/theme/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<img>` (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 `<img alt="…">` 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);
}
Binary file removed docs/src/assets/logo.png
Binary file not shown.
113 changes: 113 additions & 0 deletions docs/src/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docs/src/examples/logo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
110 changes: 68 additions & 42 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<style>
.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; }
</style>
<img src="./assets/logo.svg" alt="OptimalControl.jl logo" class="oc-logo oc-logo--light" />
<img src="./assets/logo-dark.svg" alt="OptimalControl.jl logo" class="oc-logo oc-logo--dark" />
```

## 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

Expand Down Expand Up @@ -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).
Expand All @@ -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
Expand Down Expand Up @@ -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
<details style="margin-bottom: 0.5em; margin-top: 1em;"><summary style="margin-bottom: 0px; margin-top: 0px;">ℹ️ Version info</summary>
```
| 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
</details>
```

```@raw html
<details style="margin-bottom: 0.5em;"><summary style="margin-bottom: 0px; margin-top: 0px;">📦 Package status</summary>
```
**Direct dependencies**

```@example main
Pkg.status() # hide
```

```@raw html
</details>
```

```@raw html
<details style="margin-bottom: 0.5em;"><summary style="margin-bottom: 0px; margin-top: 0px;">📚 Complete manifest</summary>
```
**Full dependency tree**

```@example main
Pkg.status(; mode = PKGMODE_MANIFEST) # hide
```

```@raw html
</details>
```
:::
Loading