Skip to content

Integrating

Once a flow is built, calling it integrates the underlying ODE. There are two call styles depending on whether you need the full trajectory or just the final state.


Call styles

Point integration — final state only

julia
flow(t0, x0, tf)            # returns xf::Vector (StateFlow)
hflow(t0, x0, p0, tf)       # returns (xf, pf) (HamiltonianFlow)
julia
x0 = [1.0, 0.0]
xf = flow(0.0, x0, 1.0)
2-element Vector{Float64}:
 0.36787944127643124
 0.0
julia
p0 = [0.0, 1.0]
xf, pf = hflow(0.0, x0, p0, 1.0)
(xf, pf)
([0.5403023057842606, 0.8414709847533869], [-0.8414709847533869, 0.5403023057842606])

Trajectory integration — full time history

julia
flow((t0, tf), x0)          # returns VectorFieldTrajectory
hflow((t0, tf), x0, p0)     # returns HamiltonianVectorFieldTrajectory
julia
sol = flow((0.0, 1.0), x0)
VectorFieldTrajectory
├─ result: SciMLIntegrationResult
├─ tspan: (0.0, 1.0)
├─ time points: 16
└─ final state: [0.36787944127643124, 0.0]
julia
hsol = hflow((0.0, 1.0), x0, p0)
HamiltonianVectorFieldTrajectory
├─ result: SciMLIntegrationResult
├─ tspan: (0.0, 1.0)
├─ time points: 18
├─ final state: [0.5403023057842606, 0.8414709847533869]
└─ final costate: [-0.8414709847533869, 0.5403023057842606]

Variable parameters

For a NonFixed flow, pass the variable via the variable keyword:

julia
vf_v = Data.VectorField((x, v) -> -v[1] .* x; is_variable=true)
flow_v = Flows.Flow(vf_v)

xf_v = flow_v(0.0, [1.0, 0.0], 1.0; variable=[2.0])
2-element Vector{Float64}:
 0.13533528348480484
 0.0

The variable argument is required when is_variable(flow) is true, and silently ignored for Fixed flows.


Variable costate

For a NonFixed HamiltonianFlow, pass variable_costate=true to also integrate the augmented adjoint    (initialized at  ) alongside the state and costate. The point call then returns a triple (xf, pf, pvf) instead of (xf, pf):

julia
h_v = Data.Hamiltonian((x, p, v) -> v[1] * p^2 / 2; is_variable=true)
hflow_v = Flows.Flow(h_v)

xf, pf, pvf = hflow_v(0.0, 1.0, 0.5, 1.0; variable=[2.0], variable_costate=true)
(xf, pf, pvf)
(1.9999999999999998, 0.5, -0.12499999999999997)

This is only available for point evaluation, and only when variable is provided (NonFixed flows require it).

Free times

The positional tf argument is the evaluation time; it is independent of the variable value, even when a variable component represents a free initial or final time. Passing flow(t0, x0, p0, t1; variable=v) with t1 ≠ v is valid.

When a variable component is a free time, the flow keeps integrating the same naive adjoint — no special-casing — with the augmented costate started at   (the convention variable_costate=true always uses). This zero start is exactly what makes the mitigated free-time transversality conditions valid: written at , they are

where H is obtained from CTFlows.Systems.hamiltonian(flow); a nonzero would shift both sides. The shooting method writes these conditions by hand.

See test/suite/flows/test_variable_costate_free_time.jl for worked shooting residuals built on this mechanism (free , free , and both at once). The Goddard tests (test/suite/integration/test_goddard.jl) instead close their free final time with the classical   condition — not the adjoint. This settles issues #231 and #183.


Hamiltonian / pseudo-Hamiltonian getters

Any Hamiltonian flow exposes its underlying Hamiltonian and — when the flow was built from a control law — its pseudo-Hamiltonian, control law, and their gradients:

julia
Systems.hamiltonian(hflow_v)          # the callable H(t, x, p, v)
Systems.hamiltonian_gradient(hflow_v) # functor: (t, x, p, v) -> (∂H/∂x, ∂H/∂p)
Systems.variable_gradient(hflow_v)    # functor: (t, x, p, v) -> ∂H/∂v
CTFlows.Systems.HamiltonianVariableGradient{CTBase.Data.Hamiltonian{Main.var"#11#12", CTBase.Traits.Autonomous, CTBase.Traits.NonFixed}, CTBase.Differentiation.DifferentiationInterface{CTBase.Strategies.StrategyOptions{@NamedTuple{ad_backend::CTBase.Options.OptionValue{AutoForwardDiff{nothing, Nothing}}}}}}(Hamiltonian: autonomous, variable
  natural call: h(x, p, v)
  uniform call: h(t, x, p, v), DifferentiationInterface(ad_backend=AutoForwardDiff()))

Systems.pseudo_hamiltonian, Systems.control_law, Systems.pseudo_hamiltonian_gradient and Systems.pseudo_variable_gradient are available on flows built from a pseudo-Hamiltonian (or an OCP) and a control law — see Control laws. Calling them on a flow with no associated control law throws IncorrectArgument.


Configuration objects

The convenience call signatures above internally build configuration objects that bundle the integration parameters. You can also construct them explicitly and pass them to Flows._invoke_flow:

Config typeUsageArguments
StateEndPointConfigstate final value(t0, x0, tf)
StateTrajectoryConfigstate full trajectory(tspan, x0)
HamiltonianEndPointConfigstate+costate final value(t0, x0, p0, tf)
HamiltonianTrajectoryConfigstate+costate trajectory(tspan, x0, p0)
AugmentedHamiltonianEndPointConfigstate+costate+variable-costate final value(t0, x0, p0, pv0, tf)
julia
cfg = Configs.StateTrajectoryConfig((0.0, 1.0), [1.0, 0.0])
Configs.tspan(cfg)
(0.0, 1.0)

Configuration objects separate what to integrate from how to integrate (the flow). This separation is useful when the same config must be passed to several flows.


Integrator options

Options are passed as keyword arguments to Flows.Flow(data; opts...) or Integrators.build_integrator(; opts...).

The default integrator is SciML backed by OrdinaryDiffEqTsit5 (loaded when import OrdinaryDiffEqTsit5 appears in your session).

Common options

OptionDefaultDescription
reltol1e-6Relative tolerance
abstol1e-8Absolute tolerance
algTsit5()ODE algorithm (any SciML algorithm)
saveat[]Extra time points to save
densetrueDense output for interpolation
julia
# Tighter tolerances
flow_tight = Flows.Flow(vf; reltol=1e-12, abstol=1e-12)
Flow
├─ system: VectorFieldSystem
          ├─ wraps: VectorField: autonomous, fixed (no variable), out-of-place
          └─ rhs: IPVFOoPRHS (out-of-place VF → in-place interface)
└─ integrator: SciML (abstol = 1.0e-12, reltol = 1.0e-12)

Different algorithm (requires the matching OrdinaryDiffEq package to be loaded).

julia
using OrdinaryDiffEqRosenbrock
flow_rodas = Flows.Flow(vf; alg=Rodas4())
Flow
├─ system: VectorFieldSystem
          ├─ wraps: VectorField: autonomous, fixed (no variable), out-of-place
          └─ rhs: IPVFOoPRHS (out-of-place VF → in-place interface)
└─ integrator: SciML (alg = OrdinaryDiffEqRosenbrock.Rodas4{AutoForwardDiff{nothing, Nothing}, Nothing, typeof(OrdinaryDiffEqCore.trivial_limiter!), typeof(OrdinaryDiffEqCore.trivial_limiter!), Nothing}(nothing, OrdinaryDiffEqCore.trivial_limiter!, OrdinaryDiffEqCore.trivial_limiter!, AutoForwardDiff(), nothing, 20, 0.03))

Unsafe mode

By default, a SolverFailure exception is thrown if the ODE solver returns a non-success retcode. Pass unsafe=true to suppress this check:

julia
xf_unsafe = flow(0.0, [1.0, 0.0], 1.0; unsafe=true)
2-element Vector{Float64}:
 0.36787944127643124
 0.0

Use unsafe=true inside shooting methods or optimisation loops where you want to handle failures gracefully instead of relying on exceptions.


SciML integrator internals

The SciML integrator strategy itself — its options, construction, and the CommonSolve.solve method that wraps SciML's solve — is provided by CTSolvers.Integrators. CTFlows contributes only the glue: Integrators.build_problem turns a system and a configuration into a SciML ODEProblem, and Integrators.build_options selects the integrator's cached option bundle for the configuration. Integration is then CommonSolve.solve(prob, integ). Keeping build_problem separate from the solve step lets the same problem definition be re-solved with different parameters efficiently.

julia
integ = Integrators.build_integrator(; reltol=1e-8)
SciML (instance, id=:sciml)
├─ internalnorm = real_norm  [default]
├─ alg = Tsit5{typeof(OrdinaryDiffEqCore.trivial_limiter!), typeof(OrdinaryDiffEqCore.trivial_limiter!), FastBroadcast.Serial}(OrdinaryDiffEqCore.trivial_limiter!, OrdinaryDiffEqCore.trivial_limiter!, FastBroadcast.Serial())  [default]
├─ reltol = 1.0e-8  [user]
├─ save_everystep = auto  [default]
├─ abstol = 1.0e-8  [default]
├─ save_start = auto  [default]
└─ dense = auto  [default]
Tip: use describe(SciML) to see all available options.

See also