Skip to content

Flow(fc, law) compatibility

This page is a living compatibility reference for Flow(fc, law), built directly from a Data.ControlledVectorField and a control law — no optimal control problem involved. Every ✓ / ✗ in the table below is demonstrated by a code block re-run on every documentation build.

Scope: CPU, default AD backend. The table is generated from probe/cpu/probe_cpu.jl. GPU compatibility is a separate effort — see probe/gpu.

As documented in Control laws, only an OpenLoop or ClosedLoop law is accepted here — DynClosedLoop is rejected, since it needs the costate p, which a state flow does not have. The control is eliminated via Data.ComposedVectorField  , integrated as a plain state flow — no AD anywhere, and no OCP-derived buffer to size: Data.ControlledVectorField has no in-place variant at all, so g is always out-of-place, and Systems.build_system(g) builds exactly the same kind of VectorFieldSystem as Flow(VectorField)'s out-of-place path.

The dynamics used on this page are the same as Control laws/Flow(ocp, law)'s example,   , with both feedback kinds:

at  .

Last probed: 2026-07-23

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.

julia
fc = Data.ControlledVectorField((x, u) -> u .- x)

law_cl = Data.ClosedLoop(x -> -x)
law_ol = Data.OpenLoop(t -> 1.0)

flow_cl = Flows.Flow(fc, law_cl; reltol=1e-8)
flow_ol = Flows.Flow(fc, law_ol; reltol=1e-8)
ControlledFlow
├─ system: VectorFieldSystem
  ├─ time_dependence: NonAutonomous
  ├─ variable_dependence: Fixed
  └─ ComposedVectorField: non-autonomous, fixed (no variable), out-of-place
       natural call: f(t, 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.

Compatibility table

State typepointtraj
Scalar Real
Vector/MVector/SVector Real
Matrix/MMatrix/SMatrix Real (batch)
Scalar/Vector/MVector/SVector/Matrix/SMatrix Complex
ForwardDiff.Dual scalar/Vector/MVector/SVector

Fully green, measured on ClosedLoop — no unsupported combination, matching Flow(ocp, law)'s OpenLoop/ClosedLoop table on every state-shape axis since the Matrix/SVector fix there (#358). The one remaining difference is the trajectory-Dual case: that table's OCP objective computation is not Dual-transparent, whereas Flow(fc, law) computes no objective at all, so it has no counterpart restriction. OpenLoop shares the same mechanics as ClosedLoop here (only the control-law arity differs — u(t,v) vs u(t,x,v)) and is spot-checked below rather than re-measured cell by cell.


Real, Complex, and Dual states — ClosedLoop

julia
julia> flow_cl(0.0, 1.0, 1.0)   # scalar, ≈ exp(-2)
0.13533528340900094
julia
julia> flow_cl(0.0, SA[1.0, 2.0], 1.0)
2-element SVector{2, Float64} with indices SOneTo(2):
 0.1353352833695603
 0.2706705667391206

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

julia> Trajectories.state(sol)(1.0)
2-element Vector{Float64}:
 0.1353352833695603
 0.2706705667391206
julia
julia> flow_cl(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.135335  0.270671
 0.406006  0.541341

julia> flow_cl(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.135335  0.270671
 0.406006  0.541341
julia
julia> flow_cl(0.0, 1.0 + 2.0im, 1.0)
0.13533528334369263 + 0.27067056668738526im
julia
julia> x0 = ForwardDiff.Dual(1.0, 1.0)
Dual{Nothing}(1.0,1.0)

julia> xf = flow_cl(0.0, x0, 1.0)
Dual{Nothing}(0.13533528340900094,0.13533528340900094)

julia> (ForwardDiff.value(xf), ForwardDiff.partials(xf, 1))   # ≈ (exp(-2), exp(-2)) — no AD collision
(0.13533528340900094, 0.13533528340900094)

Unlike Flow(h̃, law) or Flow(ocp, law)'s DynClosedLoop path, there is no internal AD here, so a hand-built Dual never collides with anything — it just propagates a sensitivity through the integration, exactly like Flow(VectorField).


OpenLoop

julia
julia> flow_ol(0.0, 1.0, 1.0)   # ≈ 1 + (1.0 - 1) * exp(-1) = 1.0
1.0

julia> flow_ol(0.0, 0.0, 1.0)   # ≈ 1 + (0.0 - 1) * exp(-1)
0.632120558757575
julia
julia> flow_ol(0.0, SA[1.0 + 2.0im, 0.0 + 0.0im], 1.0)
2-element SVector{2, ComplexF64} with indices SOneTo(2):
                1.0 + 0.7357588824910012im
 0.6321205587544994 + 0.0im

DynClosedLoop — rejected

julia
julia> try
           Flows.Flow(fc, Data.DynClosedLoop((x, p) -> p); reltol=1e-8)
       catch e
           showerror(stdout, e)
       end
PreconditionError  #_flow_from_controlled_vf#48, building.jl:518

Flow(fc, law) requires an OpenLoop or ClosedLoop control law

│  Reason   a DynClosedLoop law u(t,x,p,v) needs the costate p, which a state flow of a vector field does not have

│  Context  Flow(fc::ControlledVectorField, law::ControlLaw) — feedback dispatch
│  Hint     use OpenLoop(u) or ClosedLoop(u); for a DynClosedLoop law use Flow(h̃, law) or Flow(ocp, law)
└─

See also