Flow(ocp, law) compatibility
Which state/costate types and call styles Flow(ocp, law) accepts, for an OCP with control and each of the three feedback kinds (DynClosedLoop, OpenLoop, ClosedLoop), on CPU with the default AD backend. Every cell has an executed example; see Compatibility for how the pages stay true.
As on Flow(ocp)'s page, the state dimension is fixed at OCP construction, so this page uses two problems built once — scalar (n=1) and vector (n=2) — and every table tests the scalar containers against the first, the vector-family ones against the second.
As documented in Control laws, the constructor dispatches on the law's feedback trait into two structurally different flow types:
| Law type | Flow type | Dynamics | Internal AD |
|---|---|---|---|
DynClosedLoop | OptimalControlFlow (Hamiltonian) | state + costate | yes (AutoForwardDiff) |
OpenLoop / ClosedLoop | ControlledFlow (state flow) | state only | no |
The OCP used on this page (the same one as Control laws):
function build_ocp_ctrl(n)
pre = CTModels.Building.PreModel()
CTModels.Building.time_dependence!(pre; autonomous=true)
CTModels.Building.time!(pre; t0=0.0, tf=1.0)
CTModels.Building.state!(pre, n)
CTModels.Building.control!(pre, n)
CTModels.Building.dynamics!(pre, (r, t, x, u, v) -> (r .= u .- x; nothing))
CTModels.Building.objective!(pre, :min; lagrange=(t, x, u, v) -> 0.5 * sum(abs2, u))
return CTModels.Building.build(pre)
end
ocp1 = build_ocp_ctrl(1)
ocp2 = build_ocp_ctrl(2)The (autonomous) optimal control problem is of the form:
minimize J(x, u) = ∫ f⁰(x(t), u(t)) dt, over [0.0, 1.0]
subject to
ẋ(t) = f(x(t), u(t)), t in [0.0, 1.0] a.e.,
where x(t) ∈ R² and u(t) ∈ R².DynClosedLoop — Hamiltonian flow, :total vs :partial
The law u = p is the stationary point of the pseudo-Hamiltonian
law = Data.DynClosedLoop((x, p) -> p)
f1_total = Flows.Flow(ocp1, law; reltol=1e-8) # :total (default)
f2_total = Flows.Flow(ocp2, law; reltol=1e-8)
f1_partial = Flows.Flow(ocp1, law; hamiltonian_type=:partial, reltol=1e-8)
f2_partial = Flows.Flow(ocp2, law; hamiltonian_type=:partial, reltol=1e-8)OptimalControlFlow
├─ 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.Because the feedback is stationary, :total (AD through the law) and :partial (AD at fixed u) agree exactly — the two columns below are a genuine consistency cross-check, not two independent measurements:
| State type | :total point | :total traj | :partial point | :partial traj |
|---|---|---|---|---|
Scalar Real | ✓ | ✓ | ✓ | ✓ |
Vector Real | ✓ | ✓ | ✓ | ✓ |
MVector Real | ✓ | ✓ | ✓ | ✓ |
SVector Real | ✓ | ✓ | ✓ | ✓ |
Matrix/MMatrix/SMatrix Real | ✓ | ✓ | ✓ | ✓ |
Complex (any container) | ✗ (a) | ✗ (a) | ✗ (a) | ✗ (a) |
ForwardDiff.Dual (any container) | ✗ (c) | ✗ (c) | ✗ (c) | ✗ (c) |
Notes (a)/(c) are exactly Flow(ocp)'s notes (a)/(c): Complex fails the same AutoForwardDiff way, and a hand-built Dual collides with the flow's own AD the same way — neither is specific to having a control law.
julia> x0, p0 = 1.0, 0.5;
julia> xf, pf = f1_total(0.0, x0, p0, 1.0);
julia> xf ≈ x0/exp(1.0) + (p0/2)*(exp(1.0) - 1/exp(1.0)), pf ≈ p0*exp(1.0)
(true, true)julia> f1_partial(0.0, x0, p0, 1.0) # matches f1_total exactly — stationary feedback
(0.9554800381294778, 1.3591409143553999)julia> f2_total(0.0, SA[1.0, 2.0], SA[0.5, 0.3], 1.0)
([0.9554800381223812, 1.0883192406416295], [1.3591409143502615, 0.8154845486101568])
julia> f2_total(0.0, [1.0 2.0; 3.0 4.0], [0.5 0.3; 0.2 0.1], 1.0)
([0.9554800381100732 1.0883192406213191; 1.3386785625038269 1.5890378843863338], [1.3591409143394944 0.815484548603696; 0.5436563657357977 0.27182818286789884])DynClosedLoop, constrained — does adding constraint=/multiplier= keep working?
Using a constant constraint=(x,u)->1.0 and multiplier=(x,p)->0.3: the product
f1c_total = Flows.Flow(
ocp1, law; constraint=(x, u) -> 1.0, multiplier=(x, p) -> 0.3, reltol=1e-8
)
f2c_total = Flows.Flow(
ocp2, law; constraint=(x, u) -> 1.0, multiplier=(x, p) -> 0.3, reltol=1e-8
)OptimalControlFlow
├─ system: HamiltonianSystem
│ ├─ time_dependence: Autonomous
│ ├─ variable_dependence: Fixed
│ ├─ ComposedHamiltonian: autonomous, fixed (no variable)
│ │ natural call: h(x, p)
│ │ uniform call: h(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.| State type | :total point | :total traj | :partial point | :partial traj |
|---|---|---|---|---|
Scalar Real | ✓ | ✓ | ✓ | ✓ |
Vector/MVector/SVector Real | ✓ | ✓ | ✓ | ✓ |
Matrix-family Real | ✓ | ✓ | ✓ | ✓ |
Complex (any container) | ✗ (a) | ✗ (a) | ✗ (a) | ✗ (a) |
ForwardDiff.Dual (any container) | ✗ (c) | ✗ (c) | ✗ (c) | ✗ (c) |
Measured: the ✓/✗ footprint is identical to the unconstrained table above, cell for cell. Adding a path constraint does not change what state types the flow accepts.
julia> f1c_total(0.0, x0, p0, 1.0) == f1_total(0.0, x0, p0, 1.0)
trueOpenLoop / ClosedLoop — state flow, no internal AD
Flow(ocp, ClosedLoop(x -> -x)) eliminates the control (
law_cl = Data.ClosedLoop(x -> -x)
fcl1 = Flows.Flow(ocp1, law_cl; reltol=1e-8)
fcl2 = Flows.Flow(ocp2, law_cl; reltol=1e-8)ControlledFlow
├─ system: VectorFieldSystem
│ ├─ time_dependence: Autonomous
│ ├─ variable_dependence: Fixed
│ └─ ComposedVectorField: autonomous, fixed (no variable), out-of-place
│ natural call: f(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.| State type | point | traj |
|---|---|---|
Scalar Real | ✓ | ✓ |
Vector/MVector/SVector Real | ✓ | ✓ |
Matrix-family Real | ✓ | ✓ |
Scalar/Vector/MVector/SVector Complex | ✓ | ✓ |
Matrix-family Complex | ✓ | ✓ |
ForwardDiff.Dual scalar/Vector/MVector/SVector | ✓ | ✗ (e) |
Complex and Dual work here (no AD in the RHS), like Flow(ocp)'s basic call, and so does every container. One asymmetry:
(e) Dual works at a point, but not in a trajectory
ForwardDiff.Dual states integrate correctly for the point call, but the trajectory call raises MethodError: no method matching Float64(::ForwardDiff.Dual{...}). The trajectory path additionally computes the OCP objective (Mayer + Lagrange) from the reconstructed control and state, and that computation is not Dual-transparent — unlike the point call, which only integrates the state. If you need a trajectory with a Dual state, this is the concrete blocker to work around.
julia> xf = fcl1(0.0, x0, 1.0)
0.13533528340900092
julia> xf ≈ x0 * exp(-2.0)
truejulia> fcl1(0.0, ForwardDiff.Dual(1.0, 1.0), 1.0) # point call: works
Dual{Nothing}(0.13533528340900092,0.13533528340900092)julia> fcl1((0.0, 1.0), ForwardDiff.Dual(1.0, 1.0)) # trajectory call: fails (note e)
ERROR: MethodError: no method matching Float64(::ForwardDiff.Dual{Nothing, Float64, 1})
The type `Float64` exists, but no method is defined for this combination of argument types when trying to construct it.
Closest candidates are:
(::Type{T})(::Real, ::RoundingMode) where T<:AbstractFloat
@ Base rounding.jl:265
(::Type{T})(::T) where T<:Number
@ Core boot.jl:965
Float64(::IrrationalConstants.Sqrt2)
@ IrrationalConstants ~/.julia/packages/IrrationalConstants/sUNmT/src/macro.jl:111
...julia> sol = fcl2((0.0, 1.0), [1.0, 2.0]);
julia> Trajectories.state(sol)(1.0), Trajectories.control(sol)(1.0)
([0.1353352833695604, 0.2706705667391208], [-0.1353352833695604, -0.2706705667391208])SVector and Matrix (batch) both work, point and trajectory:
julia> fcl2(0.0, SA[1.0, 2.0], 1.0)
2-element SVector{2, Float64} with indices SOneTo(2):
0.13533528336956038
0.27067056673912077
julia> fcl2(0.0, [1.0 2.0; 3.0 4.0], 1.0)
2×2 Matrix{Float64}:
0.135335 0.270671
0.406006 0.541341See also
Control laws — the guide page this reference expands on (
DynClosedLoop/OpenLoop/ClosedLoop,hamiltonian_type, the convenience constructor).Constrained flows — the
constraint=/multiplier=API this page's constrained table exercises.Flow(ocp)compatibility — the control-free counterpart, and the source of notes (a)/(c) reused verbatim here.Flow(Hamiltonian)compatibility — the sharedComplex/nested-Dualroot cause.Compatibility overview — the per-constructor feature matrix and the other flow types.
CTFlows.Flows.OptimalControlFlow,CTFlows.Flows.ControlledFlow— the flow types.