Getting Started
This page gets you from installation to your first integrated trajectory in five minutes.
Installation
using Pkg: Pkg
Pkg.add("CTFlows")To integrate anything you also need an ODE solver backend. The default (and unique) strategy is SciML, activated by loading an OrdinaryDiffEq solver package:
Pkg.add("OrdinaryDiffEqTsit5")Mental model
Three ideas explain most of the API:
No top-level exports. CTFlows exports nothing at the package level. Every symbol lives in a submodule and is reached via a qualified path (
CTFlows.Flows.Flow) or an explicitusing CTFlows: Flows. The same holds for the data layer, which lives in CTBase (CTBase.Data.VectorField).A pipeline of small layers. Data (your functions, wrapped) → Systems (ODE right-hand side) → Integrators (solver strategy) → Flows (the callable object) → Trajectories (the result). The shortcut
Flows.Flow(data; opts...)runs the whole pipeline in one call.Extension-backed features. The actual ODE solving, plotting, and SciML interoperability are Julia package extensions: they activate when you load
OrdinaryDiffEqTsit5(or another solver),Plots, orSciMLBase.
5-minute walkthrough
Bring the relevant submodules into scope and load a solver:
using CTFlows
using CTBase: Data # VectorField, Hamiltonian, HamiltonianVectorField
using CTFlows: Flows # Flow
using CTFlows: Trajectories # time_grid, state, costate
using OrdinaryDiffEqTsit5: OrdinaryDiffEqTsit5 # activates the SciML integrator extensionWrap the dynamics
A vector field is any function of the state. Wrapping it as a Data.VectorField records its traits (autonomous or not, with or without a variable parameter):
vf = Data.VectorField(x -> -x) # autonomous, fixedVectorField: autonomous, fixed (no variable), out-of-place
natural call: f(x)
uniform call: f(t, x, v)Build the flow
flow = Flows.Flow(vf; reltol=1e-8, abstol=1e-8)Flow
├─ system: VectorFieldSystem
│ ├─ time_dependence: Autonomous
│ ├─ variable_dependence: Fixed
│ └─ VectorField: autonomous, fixed (no variable), out-of-place
│ natural call: f(x)
│ uniform call: f(t, x, v)
└─ integrator: SciML{CPU} (instance, id=:sciml)
├─ internalnorm = real_norm [default]
├─ alg = Tsit5 [default]
├─ reltol = 1.0e-8 [user]
├─ save_everystep = auto [default]
├─ abstol = 1.0e-8 [user]
├─ save_start = auto [default]
└─ dense = auto [default]
Tip: use describe(SciML{CPU}) to see all available options.Integrate
Point form returns only the final state:
xf = flow(0.0, [1.0, 0.0], 1.0)2-element Vector{Float64}:
0.36787944127643124
0.0Trajectory form returns the full history:
sol = flow((0.0, 1.0), [1.0, 0.0])VectorFieldTrajectory
├─ result: SciMLIntegrationResult
├─ tspan: (0.0, 1.0)
├─ time points: 16
└─ final state: [0.36787944127643124, 0.0]Read the result
julia> ts = Trajectories.time_grid(sol);
julia> (ts[1], ts[end])
(0.0, 1.0)
julia> x = Trajectories.state(sol);
julia> x(0.5)
2-element Vector{Float64}:
0.6065306592843308
0.0Trajectories.state(sol) is callable and interpolates: x(t) gives the state at any t inside the integration interval.
Hamiltonian systems
The same API drives Hamiltonian dynamics on state–costate pairs
hvf = Data.HamiltonianVectorField((x, p) -> (p, -x))HamiltonianVectorField: autonomous, fixed (no variable), out-of-place
natural call: f(x, p)
uniform call: f(t, x, p, v)hflow = Flows.Flow(hvf; reltol=1e-10)Flow
├─ system: HamiltonianVectorFieldSystem
│ ├─ time_dependence: Autonomous
│ ├─ variable_dependence: Fixed
│ └─ HamiltonianVectorField: autonomous, fixed (no variable), out-of-place
│ natural call: f(x, p)
│ uniform call: f(t, x, p, v)
└─ integrator: SciML{CPU} (instance, id=:sciml)
├─ internalnorm = real_norm [default]
├─ alg = Tsit5 [default]
├─ reltol = 1.0e-10 [user]
├─ save_everystep = auto [default]
├─ abstol = 1.0e-8 [default]
├─ save_start = auto [default]
└─ dense = auto [default]
Tip: use describe(SciML{CPU}) to see all available options.julia> xf, pf = hflow(0.0, [1.0, 0.0], [0.0, 1.0], 1.0);
julia> xf
2-element Vector{Float64}:
0.5403023057842606
0.8414709847533869
julia> pf
2-element Vector{Float64}:
-0.8414709847533869
0.5403023057842606You can also start from a scalar Hamiltonian and let automatic differentiation derive the vector field — see Building a flow.
Optimal control problems
This is the entry point most users of the control-toolbox ecosystem actually reach for: Flow(ocp) builds a flow directly from an optimal control problem — a CTModels.Models.Model — with no Hamiltonian to write by hand.
using CTModels
pre = CTModels.Building.PreModel()
CTModels.Building.time_dependence!(pre; autonomous=true)
CTModels.Building.time!(pre; t0=0.0, tf=1.0)
CTModels.Building.state!(pre, 1)
CTModels.Building.dynamics!(pre, (r, t, x, u, v) -> (r[1] = -x[1]; nothing))
CTModels.Building.objective!(pre, :min; mayer=(x0, xf, v) -> xf[1])
ocp = CTModels.Building.build(pre)
f = Flows.Flow(ocp; reltol=1e-10)OptimalControlFlow
├─ system: HamiltonianSystem
│ ├─ time_dependence: Autonomous
│ ├─ variable_dependence: Fixed
│ ├─ Hamiltonian: autonomous, fixed (no variable)
│ │ natural call: h(x, p)
│ │ uniform call: h(t, x, p, v)
│ └─ backend: DifferentiationInterface{CPU}(ad_backend=AutoForwardDiff)
└─ integrator: SciML{CPU} (instance, id=:sciml)
├─ internalnorm = real_norm [default]
├─ alg = Tsit5 [default]
├─ reltol = 1.0e-10 [user]
├─ save_everystep = auto [default]
├─ abstol = 1.0e-8 [default]
├─ save_start = auto [default]
└─ dense = auto [default]
Tip: use describe(SciML{CPU}) to see all available options.Point evaluation returns the final state–costate pair (Hamiltonian semantics):
julia> xf, pf = f(0.0, [1.0], [1.0], 1.0);
julia> xf
0.3678794412026476
julia> pf
2.718281828560478A trajectory call assembles a full CTModels.Solution — state, costate, and the objective value:
sol = f((0.0, 1.0), [1.0], [1.0])Solution ✓ successful
│ Objective : 0.36787944120264765
│ Status : Success
└─ Message : Solution computed by CTFlows OCP flowjulia> CTModels.Components.objective(sol)
0.36787944120264765This is a control-free problem (no u in the dynamics). For a problem with a control, pass a control law — Flow(ocp, law) — see Control laws. See Optimal control for the full picture, including the basic no-costate call f(t0, x0, tf) for direct shooting.
Plotting the result
Load Plots and any solution object draws directly — here the state and costate of the CTModels.Solution on a shared time axis:
using Plots
plot(sol)Where to go next
Optimal control — flows built directly from a
CTModelsproblem.Control laws —
Flow(ocp, law),OpenLoop/ClosedLoop/DynClosedLoop.Flows overview — the full pipeline and the mathematical setting.
Integrating — call styles, variable parameters, solver options.
Multi-phase flows — concatenation with switching times and jumps.
SciML flows — wrap an existing
ODEFunctionorODEProblem.GPU flows — running a flow on a device, and the rules your own functions must follow.