Skip to content

Flow(::ODEFunction) / Flow(::ODEProblem) compatibility

This page is a living compatibility reference for the two SciML-backed flow constructors — Flow(f::SciMLBase.AbstractODEFunction) and Flow(prob::SciMLBase.AbstractODEProblem): which state types and call styles each accepts, each shown with a minimal, executable example. Every ✓ / ⚠ / ✗ in the tables 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 tables are 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     with   (the default SciML integrator, OrdinaryDiffEqTsit5) — the same analytic solution as the Flow(VectorField) page:   .

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.

The two constructors have very different mechanics — one goes through the standard CTFlows pipeline, the other bypasses it — so each gets its own section and its own table.


Flow(::ODEFunction)

Flow(f::SciMLBase.AbstractODEFunction) wraps f in a SciMLFunctionSystem and runs the standard CTFlows pipeline (build_flow), exactly like Flow(VectorField): the integration path (in-place vs out-of-place) is chosen from ismutable(u0), with the same warn-and-fallback for an in-place function called with an immutable x0.

The one structural difference: a SciML function always has the uniform (du, u, p, t) / (u, p, t) signature, so the wrapped flow is always NonAutonomous/NonFixedvariable must be passed explicitly on every call (there is no Fixed-by-default case like VectorField's).

1-D = scalar, end to end (issue #357)

Flow(::ODEFunction) goes through CTFlows' own Systems/Trajectories dispatch, so it follows the same "1-D = scalar" convention as Flow(VectorField): a scalar or length-1 Vector/SVector x0 collapses to a scalar in both call styles. This is not true of Flow(::ODEProblem) below — see the Shape contract page for why the two constructors are allowed to diverge here.

julia
f_oop = SciMLBase.ODEFunction{false}((u, p, t) -> -p .* u)         # out-of-place
flow_f = Flows.Flow(f_oop; reltol=1e-8)
Flow
├─ system: SciMLFunctionSystem
  └─ wraps: ODEFunction: non-autonomous, variable, out-of-place
└─ 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
f_ip = SciMLBase.ODEFunction((du, u, p, t) -> du .= -p .* u)       # in-place
flow_f_ip = Flows.Flow(f_ip; reltol=1e-8)
Flow
├─ system: SciMLFunctionSystem
  └─ wraps: ODEFunction: non-autonomous, variable, in-place
└─ 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

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

works, matches  . works, but emits a performance @warn and falls back to an out-of-place finalize RHS — same fallback mechanism as Flow(VectorField), for the same reason (an immutable x0 cannot receive an in-place write).

Examples

Real, Complex, and Dual states all work with variable=1.0 passed explicitly:

julia
julia> flow_f(0.0, 1.0, 1.0; variable=1.0)                     # scalar Real
0.36787944125650673

julia> flow_f(0.0, [1.0, 2.0], 1.0; variable=1.0)               # Vector Real
2-element Vector{Float64}:
 0.3678794412375269
 0.7357588824750538

julia> flow_f(0.0, 1.0 + 2.0im, 1.0; variable=1.0)              # scalar Complex
0.36787944123358063 + 0.7357588824671613im

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

julia> flow_f(0.0, x0, 1.0; variable=1.0)                       # Dual scalar
Dual{Nothing}(0.36787944125650673,0.36787944125650673)

A length-1 Vector/SVector x0 collapses to a scalar, in both call styles:

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

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

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

Trajectory calls return a Trajectories.VectorFieldTrajectory, exactly like Flow(VectorField):

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

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

The in-place function integrates mutable containers truly in place:

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

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

and falls back with a warning for an immutable SVector/SMatrix, exactly like Flow(VectorField):

julia
julia> flow_f_ip(0.0, SA[1.0, 2.0], 1.0; variable=1.0)   # warns, then returns the correct value
┌ Warning: InPlace SciMLFunction with immutable u0 (e.g. SVector): consider using an out-of-place function for better performance.
└ @ CTFlowsSciMLFlows ~/work/CTFlows.jl/CTFlows.jl/ext/CTFlowsSciMLFlows/sciml_function_system.jl:149
2-element SVector{2, Float64} with indices SOneTo(2):
 0.3678794412375269
 0.7357588824750538

Flow(::ODEProblem)

Flow(prob::SciMLBase.AbstractODEProblem) wraps the fully-assembled problem in a SciMLProblemFlow. This bypasses the CTFlows system pipeline entirely: there is no CTFlows AbstractSystem, no get_ip_rhs/get_oop_rhs. Every call does SciMLBase.remake (to swap in a new u0/tspan/p) followed directly by CommonSolve.solve — the mutability dispatch that protects every other constructor on this page (and on Flow(VectorField)) does not exist here.

Shape-preserving by design, not by oversight (issue #357)

Because this constructor never reaches CTFlows' Systems/Trajectories dispatch, it never applies the "1-D = scalar" coercion either — a length-1 Vector x0 stays a length-1 Vector xf, unlike every other constructor on this site. This is the deliberate line issue #357 drew: the fix applies to constructors that go through CTFlows' own dispatch (Flow(VectorField), Flow(::ODEFunction) above), not to a genuine SciML bypass like this one. See the Shape contract page.

julia
prob_oop = SciMLBase.ODEProblem(f_oop, [1.0], (0.0, 1.0), 1.0)   # built out-of-place
pflow_oop = Flows.Flow(prob_oop; reltol=1e-8)
SciMLProblemFlow
├─ tspan: (0.0, 1.0)
├─ u0: [1.0]
└─ 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
prob_ip = SciMLBase.ODEProblem(f_ip, [1.0], (0.0, 1.0), 1.0)     # built in-place
pflow_ip = Flows.Flow(prob_ip; reltol=1e-8)
SciMLProblemFlow
├─ tspan: (0.0, 1.0)
├─ u0: [1.0]
└─ 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.

Three call styles are supported. The no-arg call solves the problem exactly as constructed — no x0 involved, so it is not part of the table below:

julia
julia> result = pflow_oop();

julia> Integrators.final_state(result)   # ≈ [exp(-1)], from the problem's own u0=[1.0], p=1.0
1-element Vector{Float64}:
 0.36787944125650673

Compatibility table

The columns describe which problem was remade at call time — the problem built from an out-of-place ODEFunction, or the one built from an in-place ODEFunction — for the point and trajectory call styles:

State typeOOP-built pointOOP-built trajIP-built pointIP-built 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

works, matches  . Out-of-place-built problems remake cleanly to any state type. the in-place-built problem's remake fails for every immutable x0 (scalar included — a bare Number cannot be mutated either).

No CTFlows guard on this path — a hard error, not a warning

Every other constructor on this site (Flow(VectorField), Flow(HamiltonianVectorField), and Flow(::ODEFunction) above) reacts to "in-place function + immutable state" with a @warn and a correct, slower fallback — CTFlows' own build_problem chooses that path. SciMLProblemFlow has no such fallback, because it never goes through build_problem: SciMLBase.remake hands the immutable x0 straight to the in-place function's calling convention, and SciML itself raises a hard, native error:

julia
try
    pflow_ip(0.0, 1.0, 1.0; variable=1.0)
catch e
    showerror(stdout, e)
end
Initial condition incompatible with functional form.
Detected an in-place function with an initial condition of type Number or SArray.
This is incompatible because Numbers cannot be mutated, i.e.
`x = 2.0; y = 2.0; x .= y` will error.

If using a immutable initial condition type, please use the out-of-place form.
I.e. define the function `du=f(u,p,t)` instead of attempting to "mutate" the immutable `du`.

If your differential equation function was defined with multiple dispatches and one is
in-place, then the automatic detection will choose in-place. In this case, override the
choice in the problem constructor, i.e. `ODEProblem{false}(f,u0,tspan,p,kwargs...)`.

For a longer discussion on mutability vs immutability and in-place vs out-of-place, see:
<https://docs.sciml.ai/DiffEqDocs/stable/tutorials/faster_ode_example#Example-Accelerating-a-Non-Stiff-Equation:-The-Lorenz-Equation>

The message is actionable (it names the fix: use an out-of-place function, or force ODEProblem{false}), but there is no result — unlike the warn-and-fallback path elsewhere on this page. If you plan to call a Flow(::ODEProblem) with an immutable state, build the underlying ODEProblem from an out-of-place function.

Examples

The out-of-place-built problem accepts any state type, remade at call time:

julia
julia> pflow_oop(0.0, SA[1.0, 2.0], 1.0; variable=1.0)             # SVector, out-of-place-built
2-element SVector{2, Float64} with indices SOneTo(2):
 0.3678794412375269
 0.7357588824750538

julia> pflow_oop(0.0, 1.0 + 2.0im, 1.0; variable=1.0)               # scalar Complex
0.36787944123358074 + 0.7357588824671615im

The in-place-built problem works for any mutable state, remade in place:

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

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

Trajectory calls return a raw Integrators.AbstractIntegrationResult, not a Trajectories.VectorFieldTrajectory — use the low-level accessors (Integrators.times, Integrators.evaluate_at, Integrators.final_state; see Trajectories — Low-level: integration result):

julia
julia> result = pflow_oop((0.0, 1.0), [1.0]; variable=1.0);

julia> Integrators.evaluate_at(result, 0.5)
1-element Vector{Float64}:
 0.6065306597421974

julia> Integrators.final_state(result)
1-element Vector{Float64}:
 0.36787944125650673

See also