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 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.
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.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 type | OOP point | OOP traj | IP point | IP 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 @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> 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> 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.3678794412565068Trajectory calls return a Trajectories.VectorFieldTrajectory, exactly like Flow(VectorField):
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.213061319475509The in-place function integrates mutable containers truly in place:
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.7357588824750538and falls back with a warning for an immutable SVector/SMatrix, exactly like Flow(VectorField):
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.7357588824750538Flow(::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.
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.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> 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.36787944125650673Compatibility 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 type | OOP-built point | OOP-built traj | IP-built point | IP-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 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:
try
pflow_ip(0.0, 1.0, 1.0; variable=1.0)
catch e
showerror(stdout, e)
endInitial 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> 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.7357588824671615imThe in-place-built problem works for any mutable state, remade in place:
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.7357588824750538Trajectory 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> 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.36787944125650673See also
SciML flows — the guide page this reference expands on.
Building a flow — the shortcut constructor and explicit pipeline.
Integrating — call styles, variable parameters, and integrator options.
Trajectories — the low-level
AbstractIntegrationResultaccessors.Compatibility overview — the per-constructor feature matrix and the other flow types.
Shape contract — the cross-constructor "1-D = scalar" reference, including why
Flow(::ODEFunction)andFlow(::ODEProblem)diverge here.CTFlows.Flows.Flow,CTFlowsSciMLFlows.SciMLProblemFlow,CTFlowsSciMLFlows.SciMLFunctionSystem— the extension types.