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/NonFixed — variable 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 ​