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), simulation (integrate a controlled system under an open-loop or feedback control), and inspection (pull the Hamiltonian, the Hamiltonian vector field, or the control law back out of a flow you built). This page maps the section and recaps just enough PMP to make the rest make sense.
Why flows
The direct methods (Solve) discretize the whole problem into one large nonlinear program. Flows integrate instead: you supply a control law — from the PMP, from a feedback design, from anywhere — and get back an ODE solution. Direct methods win when you don't yet have a control law and want the solver to find one; flows win once you do, whether that law came from solving the PMP by hand, from a direct solve's costate, or from a controller you designed independently of any optimization.
The Pontryagin Maximum Principle, briefly
For an OCP with dynamics
The maximisation condition picks, at each
is the Hamiltonian flow this whole section builds and integrates. Boundary conditions on
From the PMP to a flow
You supply @def problem via Flow(ocp, ...) — and Flow gives you
Three things this section does
Indirect solving — From an OCP, Constrained arcs, Multi-phase, Shooting.
Simulation — Simulating a controlled system: integrate under a given control, no optimization involved.
Inspection — What you can get back from a flow: the Hamiltonian, its vector field, the pseudo-Hamiltonian, the control law you passed in.
From Hamiltonians and vector fields covers the lower-level constructors these three jobs are all built from.
Before you start
Every flow needs an ODE integrator, and none is a hard dependency — load one, most commonly OrdinaryDiffEqTsit5, before building any flow:
using OptimalControl
using OrdinaryDiffEqTsit5
using NLPModelsIpopt
t0 = 0
tf = 1
x0 = [-1, 0]
ocp = @def begin
t ∈ [t0, tf], time
x = (q, v) ∈ R², state
u ∈ R, control
x(t0) == x0
x(tf) == [0, 0]
ẋ(t) == [v(t), u(t)]
0.5∫(u(t)^2) → min
end
f = Flow(ocp, (x, p) -> p[2])Every page in this section opens with using OrdinaryDiffEqTsit5 — no exceptions. Forget it and Flow says so at construction time, naming the using to add:
julia> f = Flow(ocp, (x, p) -> p[2])
ERROR: ExtensionError → top-level scope, REPL[6]:1
│
│ missing dependencies to access SciML options metadata
│
│ Missing OrdinaryDiffEqTsit5
│
│ Context Load OrdinaryDiffEqTsit5, OrdinaryDiffEq, or DifferentialEquations to activate the CTSolversSciMLIntegrator extension.
│ Hint Run: using OrdinaryDiffEqTsit5
└─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: :sciml for the integrator family, :di for the automatic-differentiation backend that builds a Hamiltonian's vector field.
describe(:sciml)SciML (strategy)
├─ id: :sciml
├─ hierarchy: SciML → AbstractSciMLIntegrator → AbstractIntegrator → AbstractStrategy
├─ description: SciML ODE integrator.
│ See: https://docs.sciml.ai/DiffEqDocs
│ Solver options: https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/
├─ family: AbstractIntegrator
├─ default parameter: CPU
├─ parameters: CPU, GPU
│
├─ computed options for CPU: none
│
├─ computed options for GPU: none
│
└─ common options (20 options):
├─ maxiters (max_iters, max_iter, maxiter, max_iterations, maxit)::Integer (default: NotProvided)
│ description: Maximum number of solver iterations.
│
├─ save_everystep::Union{Bool, Symbol} (default: auto)
│ description: Save the solution at every solver step. Set `true`/`false` to force, or `:auto` to resolve to `false` for point integration and `true` for trajectory integration.
│
├─ save_end::Bool (default: NotProvided)
│ description: Whether to force saving the final timepoint.
│
├─ d_discontinuities::AbstractVector{<:Real} (default: NotProvided)
│ description: Locations of discontinuities in low-order derivatives.
│
├─ save_start::Union{Bool, Symbol} (default: auto)
│ description: Save initial condition in solution. Set `true`/`false` to force, or `:auto` to resolve to `false` for point integration and `true` for trajectory integration.
│
├─ force_dtmin::Bool (default: NotProvided)
│ description: Whether to continue forcing minimum dt usage.
│
├─ alg (algorithm, solver)::SciMLBase.AbstractDEAlgorithm (default: Tsit5{typeof(OrdinaryDiffEqCore.trivial_limiter!), typeof(OrdinaryDiffEqCore.trivial_limiter!), FastBroadcast.Serial}(OrdinaryDiffEqCore.trivial_limiter!, OrdinaryDiffEqCore.trivial_limiter!, FastBroadcast.Serial()))
│ description: ODE algorithm (e.g. Tsit5(), Vern6()).
│
├─ saveat (save_at, save_times)::Union{Real, AbstractVector} (default: NotProvided)
│ description: Times at which to save the solution (Vector or range).
│
├─ dtmin (min_dt, dt_min)::Real (default: NotProvided)
│ description: Minimum step size for adaptive timestepping.
│
├─ progress (verbose)::Bool (default: NotProvided)
│ description: Whether to show progress bar.
│
├─ reltol (rtol, rel_tol)::Real (default: 1.0e-8)
│ description: Relative tolerance for the ODE solver.
│
├─ adaptive (adaptive_step, adaptive_stepping)::Bool (default: NotProvided)
│ description: Whether to use adaptive step-size control.
│
├─ dt (dt0, timestep)::Real (default: NotProvided)
│ description: Fixed step size (used when adaptive=false).
│
├─ save_idxs (saveindices, save_indices)::AbstractVector{<:Integer} (default: NotProvided)
│ description: Indices of components to save (Vector of integers).
│
├─ callback (callbacks, cb)::Any (default: NotProvided)
│ description: Callback function for event handling.
│
├─ tstops (t_stops, stop_times)::AbstractVector{<:Real} (default: NotProvided)
│ description: Extra times the solver must step to (for discontinuities).
│
├─ internalnorm (internal_norm, norm)::Function (default: real_norm)
│ description: Internal norm for adaptive step-size control. Defaults to `real_norm`, which extracts the primal (Float64) part of ForwardDiff dual numbers to ensure grid invariance (IND) when ForwardDiff is loaded. Set to `DiffEqBase.ODE_DEFAULT_NORM` to use the SciML default.
│
├─ abstol (atol, abs_tol)::Real (default: 1.0e-8)
│ description: Absolute tolerance for the ODE solver.
│
├─ dense::Union{Bool, Symbol} (default: auto)
│ description: Dense output. Set `true`/`false` to force, or `:auto` to resolve to `false` for point integration and `true` for trajectory integration.
│
└─ dtmax (max_dt, dt_max)::Real (default: NotProvided)
description: Maximum step size for adaptive timestepping.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()).
describe(:di)DifferentiationInterface (strategy)
├─ id: :di
├─ hierarchy: DifferentiationInterface → AbstractADBackend → AbstractStrategy
├─ description: AD backend wrapping DifferentiationInterface.jl backends (e.g., AutoForwardDiff).
├─ family: AbstractADBackend
├─ default parameter: CPU
├─ parameters: CPU, GPU
│
├─ computed options for CPU:
│ └─ ad_backend (backend, ad)::ADTypes.AbstractADType (default: AutoForwardDiff() [computed])
│ description: DifferentiationInterface.jl backend (e.g. AutoForwardDiff() on CPU).
│
└─ computed options for GPU:
└─ ad_backend (backend, ad)::ADTypes.AbstractADType (default: ADTypes.AutoMooncake() [computed])
description: DifferentiationInterface.jl backend for GPU execution. Default: AutoMooncake(), validated end-to-end on CuArray including through a mutating in-place RHS. AutoForwardDiff() does not work on GPU (scalar-indexes a CuArray). AutoZygote() can be selected explicitly; it was found unreliable in some device call contexts (an unexpected KernelException was observed on a non-mutating call).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 — are passed when constructing a flow, not on the call:
f = Flow(ocp, law; method=:gpu) # correct — a constructor keyword
f(t0, x0, p0, tf; method=:gpu) # wrong — MethodError, no such call-time keywordWhere to go
From an OCP — the main path for indirect optimal control.
From Hamiltonians and vector fields — building blocks below the OCP layer.
Simulating a controlled system — no optimization, just integration.
What you can get back from a flow — inspection.
Multi-phase flows and Constrained arcs — assembling several arcs into one trajectory.
Writing a shooting function — the payoff.
Solve overview — the direct-method counterpart to everything here.
Geometry — the Lie-theoretic tools some of these constructors build on.