Skip to content

Flow(h̃, law) compatibility

This page is a living compatibility reference for Flow(h̃, law), built directly from a Data.PseudoHamiltonian 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 a DynClosedLoop law is accepted here — OpenLoop and ClosedLoop are rejected, since a pseudo-Hamiltonian needs the costate p that those laws do not provide. Unlike Flow(ocp, law), there is no OCP involved: Systems.PseudoHamiltonianSystem/Data.ComposedHamiltonian wrap the user's function directly, with no OCP-derived fixed-size buffer — so, as measured below, this constructor's compatibility profile matches Flow(Hamiltonian), not the more restricted Flow(ocp, law).

The dynamics used on this page generalize Control laws's scalar example (   ) to any container:    , with the law   — still the stationary point (   ). This gives  , hence

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.

The flow is built once, in both hamiltonian_type modes:

julia
= Data.PseudoHamiltonian((x, p, u) -> sum(p .* u) - 0.5 * sum(abs2, u))
law = Data.DynClosedLoop((x, p) -> p)

hflow_total = Flows.Flow(h̃, law; reltol=1e-8)                                  # :total (default)
hflow_partial = Flows.Flow(h̃, law; hamiltonian_type=:partial, reltol=1e-8)
Flow
├─ system: PseudoHamiltonianSystem
  ├─ time_dependence: Autonomous
  ├─ variable_dependence: Fixed
  ├─ PseudoHamiltonian: autonomous, fixed (no variable)
    natural call: h̃(x, p, u)
    uniform call: h̃(t, x, p, u, v)
  ├─ ControlLaw: dyn-closed-loop, autonomous, fixed (no variable)
    natural call: u(x, p)
    uniform call: u(t, x, p, v)
  └─ backend: DifferentiationInterface{CPU}(ad_backend=AutoForwardDiff)
└─ 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

Because the feedback is stationary, :total (AD through the law) and :partial (AD at fixed u) agree exactly — the two pairs of columns are a genuine consistency cross-check, not two independent measurements, exactly as on Flow(ocp, law)'s table:

State/costate type:total point:total traj:partial point:partial traj
Scalar Real
Vector/MVector/SVector Real
Matrix/MMatrix/SMatrix Real (batch)
Complex (any container)✗ (a)✗ (a)✗ (a)✗ (a)
ForwardDiff.Dual (any container)✗ (b)✗ (b)✗ (b)✗ (b)

The full Matrix family works here — measured, not assumed: confirms the "wraps directly" hypothesis above. Flow(ocp, law) now matches this (fixed as of 2026-07-24, #358) — before that fix, its OCP-derived fixed-size buffer rejected Matrix-family states, unlike the buffer-free wrapping used here.

(a) Complex is not supported

Same cause as Flow(Hamiltonian)'s note (a): the default AutoForwardDiff backend cannot differentiate through a Complex input.

(b) A hand-built ForwardDiff.Dual collides with the flow's own AD

Same cause as Flow(Hamiltonian)'s nested-AD section: Flow(h̃, law) is itself AutoForwardDiff-backed, so passing a hand-built Dual as x0/p0 raises DualMismatchError rather than working. To differentiate the flow itself (e.g. for a shooting method), wrap the flow call in an outer ForwardDiff.jacobian/gradient instead — the same pattern documented there.


Real states

julia
julia> x0, p0 = 1.0, 0.5;

julia> xf, pf = hflow_total(0.0, x0, p0, 1.0);

julia> xf  x0 + p0, pf  p0
(true, true)
julia
julia> hflow_partial(0.0, x0, p0, 1.0)   # matches hflow_total exactly — stationary feedback
(1.5, 0.5)
julia
julia> hflow_total(0.0, SA[1.0, 2.0], SA[0.5, 0.3], 1.0)
([1.5, 2.3], [0.5, 0.3])
julia
julia> sol = hflow_total((0.0, 1.0), [1.0, 2.0], [0.5, 0.3]);

julia> Trajectories.state(sol)(1.0), Trajectories.costate(sol)(1.0)
([1.4999999999999976, 2.2999999999999985], [0.5, 0.3])
julia
julia> hflow_total(0.0, MMatrix{2,2}(1.0, 3.0, 2.0, 4.0), MMatrix{2,2}(0.0, 1.0, 0.0, 1.0), 1.0)   # mutable
([1.0 2.0; 4.0 5.0], [0.0 0.0; 1.0 1.0])

julia> hflow_total(0.0, SMatrix{2,2}(1.0, 3.0, 2.0, 4.0), SMatrix{2,2}(0.0, 1.0, 0.0, 1.0), 1.0)   # immutable
([1.0 2.0; 4.0 5.0], [0.0 0.0; 1.0 1.0])

Complex and Dual states — not supported

julia
julia> try
           hflow_total(0.0, 1.0 + 2.0im, 0.0 + 0.0im, 1.0)
       catch e
           showerror(stdout, e)
       end
ArgumentError: Cannot create a dual over scalar type ComplexF64. If the type behaves as a scalar, define ForwardDiff.can_dual(::Type{ComplexF64}) = true.
julia
julia> try
           hflow_total(0.0, ForwardDiff.Dual(1.0, 1.0), ForwardDiff.Dual(0.0, 0.0), 1.0)
       catch e
           showerror(stdout, e)
       end
Cannot determine ordering of Dual tags Nothing and ForwardDiff.Tag{CTBaseDifferentiationInterface.var"#h_x#h_x##0"{CTBase.Data.ComposedHamiltonian{CTBase.Traits.Autonomous, CTBase.Traits.Fixed, CTBase.Data.PseudoHamiltonian{Main.var"#2#3", CTBase.Traits.Autonomous, CTBase.Traits.Fixed}, CTBase.Data.ControlLaw{Main.var"#5#6", CTBase.Traits.DynClosedLoopFeedback, CTBase.Traits.Autonomous, CTBase.Traits.Fixed}}, Float64, ForwardDiff.Dual{Nothing, Float64, 1}, Nothing}, ForwardDiff.Dual{Nothing, Float64, 1}}

See notes (a)/(b) above — for Complex, rewrite the system on real/imaginary components; for differentiating the flow, wrap the call in an outer ForwardDiff.jacobian/gradient as shown on Flow(Hamiltonian)'s page.


OpenLoop/ClosedLoop — rejected

A pseudo-Hamiltonian depends on the costate , which OpenLoop/ClosedLoop control laws do not provide:

julia
julia> try
           Flows.Flow(h̃, Data.ClosedLoop(x -> -x); reltol=1e-8)
       catch e
           showerror(stdout, e)
       end
PreconditionError  #_flow_from_pseudo_hamiltonian#42, building.jl:315

Flow(h̃, law) requires a DynClosedLoop control law

│  Reason   a pseudo-Hamiltonian (t,x,p,u,v) depends on the costate p, but OpenLoop u(t,v) and ClosedLoop u(t,x,v) control laws do not take p

│  Context  Flow(h̃::PseudoHamiltonian, law::ControlLaw) — feedback dispatch
│  Hint     use DynClosedLoop(u) to construct a control law u(t,x,p,v)
└─

See also