Skip to content

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   and Lagrange cost , the pseudo-Hamiltonian is

The maximisation condition picks, at each , the control that extremises — a control law . Substituting it back gives the true Hamiltonian  , whose Hamiltonian system

is the Hamiltonian flow this whole section builds and integrates. Boundary conditions on at / (transversality) turn "integrate the flow" into "find the missing " — shooting.

From the PMP to a flow ​

You supply — worked out by hand, or read off a @def problem via Flow(ocp, ...) — and Flow gives you : an object callable at a point (endpoint only) or over a trajectory (the full path), integrated numerically.

Three things this section does ​

From Hamiltonians 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:

julia
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
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.

julia
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()).

julia
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:

julia
f = Flow(ocp, law; method=:gpu)   # correct — a constructor keyword
f(t0, x0, p0, tf; method=:gpu)    # wrong — no such call-time keyword

Where to go ​