Skip to content

Getting Started

This page gets you from installation to your first integrated trajectory in five minutes.

Installation

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

julia
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 explicit using 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, or SciMLBase.

5-minute walkthrough

Bring the relevant submodules into scope and load a solver:

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

Wrap 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):

julia
vf = Data.VectorField(x -> -x)   # autonomous, fixed
VectorField: autonomous, fixed (no variable), out-of-place
  natural call: f(x)
  uniform call: f(t, x, v)

Build the flow

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

julia
xf = flow(0.0, [1.0, 0.0], 1.0)
2-element Vector{Float64}:
 0.36787944127643124
 0.0

Trajectory form returns the full history:

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

Trajectories.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 :

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

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

julia
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
julia> xf, pf = f(0.0, [1.0], [1.0], 1.0);

julia> xf
0.3678794412026476

julia> pf
2.718281828560478

A trajectory call assembles a full CTModels.Solution — state, costate, and the objective value:

julia
sol = f((0.0, 1.0), [1.0], [1.0])
Solution  ✓ successful
Objective : 0.36787944120264765
Status : Success
  └─ Message : Solution computed by CTFlows OCP flow
julia
julia> CTModels.Components.objective(sol)
0.36787944120264765

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

julia
using Plots
plot(sol)

Where to go next

  • Optimal control — flows built directly from a CTModels problem.

  • Control lawsFlow(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 ODEFunction or ODEProblem.

  • GPU flows — running a flow on a device, and the rules your own functions must follow.