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 ​