Skip to content

Flow(VectorField) compatibility

This page is a living compatibility reference for the state flow built from a Data.VectorField: which state types and call styles it accepts, each shown with a minimal, executable example. Every ✓ / ⚠ in the table below is demonstrated by a code block on this page and is re-run on every documentation build, so the page cannot drift from the code.

Scope: CPU only. The table is generated from probe/cpu/probe_cpu.jl (run it locally with julia --project=probe/cpu probe/cpu/probe_cpu.jl). GPU compatibility is a separate effort — see probe/gpu.

All examples integrate the scalar exponential decay    (autonomous, fixed) with the default SciML integrator (OrdinaryDiffEqTsit5). The analytic solution is   , so integrating from   to   maps every initial condition to  .

Last probed: 2026-07-24

This page's table shape — which containers and axes are tested — reflects probe/cpu/probe_cpu.jl as of this date. Every ✓/⚠ cell below is still re-executed on every documentation build regardless (see Compatibility overview) — only the scope of what's tested can go stale, not the results shown.

Two flows are built once — one from an out-of-place vector field, one from an in-place one — and reused by every example below, so each example isolates the one thing that varies: the initial condition x0.

julia
vf   = Data.VectorField(x -> -x)   # out-of-place: x -> -x
flow = Flows.Flow(vf; reltol=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  [default]
   ├─ save_start = auto  [default]
   └─ dense = auto  [default]
   Tip: use describe(SciML{CPU}) to see all available options.
julia
vf_ip   = Data.VectorField((du, x) -> (du .= -x); is_autonomous=true, is_variable=false)   # in-place
flow_ip = Flows.Flow(vf_ip; reltol=1e-8)
Flow
├─ system: VectorFieldSystem
  ├─ time_dependence: Autonomous
  ├─ variable_dependence: Fixed
  └─ VectorField: autonomous, fixed (no variable), in-place
       natural call: f(dx, x)
       uniform call: f(dx, 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  [default]
   ├─ save_start = auto  [default]
   └─ dense = auto  [default]
   Tip: use describe(SciML{CPU}) to see all available options.

Compatibility table

The two call styles are the point call flow(t0, x0, tf) → final state xf, and the trajectory call flow((t0, tf), x0) → a Trajectories.VectorFieldTrajectory. The OOP / IP columns are the vector-field kind — an out-of-place x -> -x versus an in-place (du, x) -> (du .= -x) — independent of the state container. (Which integration path is taken is chosen automatically from the mutability of x0; see In-place vector fields.)

State typeOOP pointOOP trajIP pointIP traj
Scalar Real
Vector Real
MVector Real
SVector Real
Matrix Real (batch)
MMatrix Real
SMatrix Real
Scalar Complex
Vector Complex
MVector Complex
SVector Complex
Matrix Complex
SMatrix Complex
ForwardDiff.Dual scalar
ForwardDiff.Dual Vector
ForwardDiff.Dual MVector
ForwardDiff.Dual SVector

Legend

  • — works; the result matches   and is executed on this page.

  • — works, but emits a performance warning; see note (a).

Every state type is supported on CPU — there is no unsupported (✗) combination. Two behaviours are worth calling out:

(a) In-place vector field + immutable initial condition

An in-place vector field with an immutable x0 (SVector, SMatrix) emits a performance @warn and falls back to an out-of-place finalize RHS. The result is correct but the path is slower. Prefer an out-of-place vector field, or a mutable MVector / MMatrix, for static states.

1-D = scalar, end to end

A scalar x0 stays a scalar in both call styles, and a length-1 vector or SVector x0 collapses to a scalar too — the point call returns a scalar xf, and the trajectory accessor state(sol)(t) also returns a scalar, not a length-1 vector. This is the ecosystem's "1-D = scalar" convention, matching what Flow(HamiltonianVectorField) already did — see the Shape contract page for the full cross-constructor picture, and #357 for the fix that brought this constructor in line with it.


Real states

Scalar

The point call returns a scalar:

julia
julia> flow(0.0, 1.0, 1.0)   # ≈ exp(-1)
0.36787944125650673

The trajectory call returns a VectorFieldTrajectory; its state accessor is a scalar too, at each time:

julia
julia> sol = flow((0.0, 1.0), 1.0);

julia> Trajectories.state(sol)(1.0)   # ≈ exp(-1)
0.3678794412565068

Length-1 Vector/SVector (collapses to scalar)

A length-1 container is not a "real" vector — it collapses to a scalar in both call styles, the same way a bare scalar x0 does:

julia
julia> flow(0.0, [1.0], 1.0)         # ≈ exp(-1), a Real — not a length-1 Vector
0.36787944125650673

julia> flow(0.0, SA[1.0], 1.0)       # ≈ exp(-1), a Real — not a length-1 SVector
0.36787944125650673

julia> sol = flow((0.0, 1.0), [1.0]);

julia> Trajectories.state(sol)(1.0)   # ≈ exp(-1), a Real
0.3678794412565068

Vector

A length ≥ 2 vector is unaffected by the collapse above and keeps its container shape:

julia
julia> flow(0.0, [1.0, 2.0], 1.0)
2-element Vector{Float64}:
 0.3678794412375269
 0.7357588824750538

The trajectory's state accessor interpolates at any time in the span:

julia
julia> sol = flow((0.0, 1.0), [1.0, 2.0]);

julia> Trajectories.state(sol)(0.5)
2-element Vector{Float64}:
 0.6065306597377546
 1.213061319475509

MVector and SVector

Mutable and immutable static vectors both integrate out-of-place:

julia
julia> flow(0.0, MVector{2}(1.0, 2.0), 1.0)
2-element MVector{2, Float64} with indices SOneTo(2):
 0.3678794412375269
 0.7357588824750538

julia> flow(0.0, SA[1.0, 2.0], 1.0)
2-element SVector{2, Float64} with indices SOneTo(2):
 0.3678794412375269
 0.7357588824750538

Matrix (batch)

A matrix state integrates every column as an independent trajectory of   :

julia
julia> flow(0.0, [1.0 2.0; 3.0 4.0], 1.0)
2×2 Matrix{Float64}:
 0.367879  0.735759
 1.10364   1.47152

julia> flow(0.0, MMatrix{2,2}(1.0, 3.0, 2.0, 4.0), 1.0)   # mutable
2×2 MMatrix{2, 2, Float64, 4} with indices SOneTo(2)×SOneTo(2):
 0.367879  0.735759
 1.10364   1.47152

julia> flow(0.0, SMatrix{2,2}(1.0, 3.0, 2.0, 4.0), 1.0)   # immutable
2×2 SMatrix{2, 2, Float64, 4} with indices SOneTo(2)×SOneTo(2):
 0.367879  0.735759
 1.10364   1.47152

Complex states

Complex initial conditions work with the same real vector field — the scalar case:

julia
julia> flow(0.0, 1.0 + 2.0im, 1.0)   # ≈ (1 + 2im) * exp(-1)
0.36787944123358063 + 0.7357588824671613im

vectors:

julia
julia> flow(0.0, [1.0 + 2.0im, 3.0 + 4.0im], 1.0)
2-element Vector{ComplexF64}:
 0.36787944122283733 + 0.7357588824456747im
  1.1036383236685123 + 1.4715177648913493im

and matrices:

julia
julia> flow(0.0, [1.0+2.0im 5.0+6.0im; 3.0+4.0im 7.0+8.0im], 1.0)
2×2 Matrix{ComplexF64}:
 0.367879+0.735759im   1.8394+2.20728im
  1.10364+1.47152im   2.57516+2.94304im

Automatic differentiation (ForwardDiff.Dual)

A ForwardDiff.Dual initial condition propagates a sensitivity through the integration — the basis for differentiating a flow with respect to its initial state:

julia
julia> x0 = ForwardDiff.Dual(1.0, 1.0)   # value 1.0, seed 1.0
Dual{Nothing}(1.0,1.0)

julia> xf = flow(0.0, x0, 1.0)
Dual{Nothing}(0.36787944125650673,0.36787944125650673)

julia> (ForwardDiff.value(xf), ForwardDiff.partials(xf, 1))   # ≈ (exp(-1), exp(-1))
(0.36787944125650673, 0.36787944125650673)

The value tracks   and the dual part tracks  . Vector duals work the same way:

julia
julia> x0 = [ForwardDiff.Dual(1.0, 1.0), ForwardDiff.Dual(2.0, 0.0)]
2-element Vector{ForwardDiff.Dual{Nothing, Float64, 1}}:
 Dual{Nothing}(1.0,1.0)
 Dual{Nothing}(2.0,0.0)

julia> flow(0.0, x0, 1.0)
2-element Vector{ForwardDiff.Dual{Nothing, Float64, 1}}:
 Dual{Nothing}(0.3678794412375269,0.3678794412375269)
 Dual{Nothing}(0.7357588824750538,0.0)

Grid invariance

The SciML integrator's internalnorm uses a real_norm that ignores dual parts, so the integration grid is identical whether the initial condition is a Real or a Dual. See test/suite/extensions/test_forwarddiff_extension.jl.

Recommended: differentiate the flow, not one hand-seeded Dual

Seeding a single Dual per component (as above) only gives one partial derivative at a time. In practice, wrap the flow call in an outer ForwardDiff.jacobian / ForwardDiff.gradient instead — one call returns every partial at once. Flow(VectorField) has no internal AD backend, so there is no nesting concern here (unlike Flow(Hamiltonian), whose own gradient computation is itself AutoForwardDiff):

julia
julia> ForwardDiff.jacobian(x0 -> flow(0.0, x0, 1.0), [1.0, 2.0])   # ≈ e⁻¹·I
2×2 Matrix{Float64}:
 0.367879  0.0
 0.0       0.367879

In-place vector fields

An in-place vector field (du, x) -> (du .= -x) produces the same results. The flow picks the integration path from the mutability of x0: a mutable container (Vector, MVector, Matrix, MMatrix) is integrated truly in place, with no allocation per step:

julia
julia> flow_ip(0.0, [1.0, 2.0], 1.0)
2-element Vector{Float64}:
 0.3678794412375269
 0.7357588824750538

julia> flow_ip(0.0, MVector{2}(1.0, 2.0), 1.0)
2-element MVector{2, Float64} with indices SOneTo(2):
 0.3678794412375269
 0.7357588824750538

A scalar initial condition is also accepted: it is promoted to a length-1 vector before the mutability dispatch, so the in-place path applies.

julia
julia> flow_ip(0.0, 1.0, 1.0)   # scalar promoted internally
0.36787944125650673

Immutable initial conditions (SVector / SMatrix)

An immutable static array cannot be written in place, so an in-place vector field falls back to an out-of-place finalize RHS and emits a performance warning (note (a)). The result is still correct:

julia
julia> flow_ip(0.0, SA[1.0, 2.0], 1.0)   # warns, then returns the correct value
┌ Warning: InPlace VectorField with immutable u0 (e.g. SVector): consider using an out-of-place function for better performance.
└ @ CTFlows.Systems ~/work/CTFlows.jl/CTFlows.jl/src/Systems/vector_field_system.jl:182
2-element SVector{2, Float64} with indices SOneTo(2):
 0.3678794412375269
 0.7357588824750538

For static states, prefer the out-of-place vector field (flow) shown above, or use a mutable MVector / MMatrix.


See also