Constrained arcs
A flow along a boundary arc — where a state constraint is active — needs the constraint and its multiplier, not just the control law.
using OptimalControl
using OrdinaryDiffEqTsit5The setting
A state constraint active on a sub-interval turns the PMP into a three-piece story: unconstrained arc, boundary arc (where the constraint is tight and pins down both the control and the multiplier in feedback form), unconstrained arc again. The boundary arc's flow needs that extra structure.
t0 = 0.0
tf = 1.0
x0 = [-1.0, 0.0]
VMAX = 1.2
ocp = @def begin
t ∈ [t0, tf], time
x = (q, v) ∈ R², state
u ∈ R, control
x(t0) == x0
v(t) + 0.0 ≤ VMAX, (vmax)
ẋ(t) == [v(t), u(t)]
0.5∫(u(t)^2) → min
endBuilding the constrained flow
g(x) = VMAX - x[2] # ≥ 0 while the constraint holds
μ(x, p) = p[1] # multiplier in feedback form on the boundary
law(x, p) = 0.0 # control on the boundary arc
f_boundary = Flow(ocp, law; constraint=(x, u) -> g(x), multiplier=μ)
xf, pf = f_boundary(t0, x0, [12.0, 6.0], tf)
xf2-element Vector{Float64}:
0.19999999999999973
0.0constraint=/multiplier= must be given as a pair — one without the other is rejected:
julia> Flow(ocp, law; constraint=:vmax)
IncorrectArgument → top-level scope, REPL[1]:2
│
│ `constraint` and `multiplier` must be given together
│
│ Got only `constraint`
│ Expected both `constraint` and `multiplier`, or neither
│
│ Context Flow(ocp, law; constraint=…, multiplier=…) — pairing check
└─Three ways to give the constraint
A plain Function — the form above, (x, u) -> g(x) paired with multiplier=μ.
A typed Data.StateConstraint (or ControlConstraint/MixedConstraint/PathConstraint for the other shapes) — equivalent, more explicit:
f_typed = Flow(ocp, law; constraint=StateConstraint(g), multiplier=μ)
f_typed(t0, x0, [12.0, 6.0], tf)[1] ≈ xftrueA Symbol naming a :path constraint already declared in the OCP — the standout capability here, not just a rename:
f_sym = Flow(ocp, law; constraint=:vmax, multiplier=μ)
typeof(f_sym)CTFlows.Flows.OptimalControlFlow{CTBase.Traits.Autonomous, CTBase.Traits.Fixed, CTFlows.Flows.HamiltonianFlow{CTBase.Traits.Autonomous, CTBase.Traits.Fixed, CTFlows.Systems.HamiltonianSystem{CTBase.Traits.Autonomous, CTBase.Traits.Fixed, ComposedHamiltonian{CTBase.Traits.Autonomous, CTBase.Traits.Fixed, PseudoHamiltonian{CTFlows.Flows.ConstrainedPseudoHamiltonianFunction{CTBase.Traits.Autonomous, CTBase.Traits.Fixed, CTFlows.Flows.OCPPseudoHamiltonianFunction{CTBase.Traits.Autonomous, CTBase.Traits.Fixed, Main.var"#fun##1072#27", Main.var"#fun##1077#28"}, PathConstraint{CTBase.Core.var"#f#3"{CTBase.Core.var"#f#2#4"{Type{Float64}, CTModels.Models.SubPathConstraint{Tuple{Vector{Float64}, Main.var"#fun##1065#26", Vector{Float64}, Vector{Symbol}}, Vector{Int64}}, Int64}}, CTBase.Traits.MixedConstraintKind, CTBase.Traits.NonAutonomous, CTBase.Traits.NonFixed}, Multiplier{typeof(Main.μ), CTBase.Traits.Autonomous, CTBase.Traits.Fixed}}, CTBase.Traits.Autonomous, CTBase.Traits.Fixed}, ControlLaw{typeof(Main.law), CTBase.Traits.DynClosedLoopFeedback, CTBase.Traits.Autonomous, CTBase.Traits.Fixed}}, CTBase.Differentiation.DifferentiationInterface{CPU, CTBase.Strategies.StrategyOptions{@NamedTuple{ad_backend::CTBase.Options.OptionValue{AutoForwardDiff{nothing, Nothing}}}}}}, SciML{CPU, CTBase.Strategies.StrategyOptions{@NamedTuple{internalnorm::CTBase.Options.OptionValue{typeof(CTSolvers.Integrators.real_norm)}, alg::CTBase.Options.OptionValue{Tsit5{typeof(OrdinaryDiffEqCore.trivial_limiter!), typeof(OrdinaryDiffEqCore.trivial_limiter!), FastBroadcast.Serial}}, reltol::CTBase.Options.OptionValue{Float64}, save_everystep::CTBase.Options.OptionValue{Symbol}, abstol::CTBase.Options.OptionValue{Float64}, save_start::CTBase.Options.OptionValue{Symbol}, dense::CTBase.Options.OptionValue{Symbol}}}, Dict{Symbol, Any}, Dict{Symbol, Any}}}, CTModels.Models.Model{CTBase.Traits.Autonomous, CTModels.Components.TimesModel{CTModels.Components.FixedTimeModel{Float64}, CTModels.Components.FixedTimeModel{Float64}}, CTModels.Components.StateModel, CTModels.Components.ControlModel, CTModels.Components.EmptyVariableModel, Main.var"#fun##1072#27", CTModels.Components.LagrangeObjectiveModel{Main.var"#fun##1077#28"}, CTModels.Components.ConstraintsModel{Tuple{Vector{Float64}, Main.var"#fun##1065#26", Vector{Float64}, Vector{Symbol}}, Tuple{Vector{Float64}, Main.var"#fun##1060#25", Vector{Float64}, Vector{Symbol}}, Tuple{Vector{Float64}, Vector{Int64}, Vector{Float64}, Vector{Symbol}, Vector{Vector{Symbol}}}, Tuple{Vector{Float64}, Vector{Int64}, Vector{Float64}, Vector{Symbol}, Vector{Vector{Symbol}}}, Tuple{Vector{Float64}, Vector{Int64}, Vector{Float64}, Vector{Symbol}, Vector{Vector{Symbol}}}}, CTModels.Components.Definition, Main.var"#29#30"}, ControlLaw{typeof(Main.law), CTBase.Traits.DynClosedLoopFeedback, CTBase.Traits.Autonomous, CTBase.Traits.Fixed}, Nothing}An unknown label is rejected, not silently accepted:
julia> Flow(ocp, law; constraint=:nope, multiplier=μ)
IncorrectArgument → top-level scope, REPL[1]:2
│
│ Constraint label not found
│
│ Got label :nope
│ Expected existing constraint label in the model
│
│ Context constraint lookup by label
│ Hint Check available constraint labels or add a constraint with this label first
└─The Symbol form uses the model's own sign convention
constraint=:vmax pulls the constraint straight from the OCP — v(t) ≤ V_{max} as written there — rather than whatever hand-derived g/μ pair with the model's own label expecting them to be interchangeable.
Several constraints at once
Pass matched tuples of functions (or labels) and multipliers, one per active constraint on the boundary arc, in place of the single values above.
Assembling the arcs
A boundary arc is one phase in a larger multi-phase flow: unconstrained arc, then the constrained flow from t1 (constraint activation), then unconstrained again from t2 (exit) — with a jump on the costate at each switch if the constraint order requires one.
Positional form is gone
julia> Flow(ocp, law, (x, u) -> g(x), μ)
PreconditionError → top-level scope, REPL[1]:2
│
│ Flow(ocp, …) with extra positional arguments is not supported
│
│ Reason passing a control law, state constraint or multiplier as a positional argument is not handled by the OCP flow constructor
│
│ Context Flow(ocp::CTModels.Models.Model) — positional-argument guard
│ Hint call Flow(ocp; kwargs…) — the OCP flow takes no positional argument beyond the model
└─Partial Hamiltonian on a constrained arc
hamiltonian_type=:partial (see From an OCP) is supported here too — the boundary arc's law is stationary for the constrained pseudo-Hamiltonian at the optimum, same as the unconstrained case, so :total and :partial still agree when the law is genuinely optimal.
Control-free flows reject constraints
julia> ocp_cf = @def begin
t ∈ [t0, tf], time
x ∈ R, state
x(t0) == 1.0
ẋ(t) == -x(t)
∫(x(t)^2) → min
end;
julia> Flow(ocp_cf; constraint=(x, u) -> 1.0, multiplier=(x, p) -> 1.0)
PreconditionError → top-level scope, REPL[2]:2
│
│ constrained flows are not supported for control-free problems
│
│ Reason a `constraint`/`multiplier` pair augments the pseudo-Hamiltonian H̃ + μ·g with the control; a control-free Flow(ocp) has no control law and no pseudo-Hamiltonian to carry the constraint term
│
│ Context Flow(ocp; constraint=…, multiplier=…) — control-free constraint guard
│ Hint use Flow(ocp, law; constraint=…, multiplier=…) with a control law
└─See also
Multi-phase flows — assembling several arcs, constrained or not, into one callable flow.
Writing a shooting function — solving for the switching times themselves rather than assuming them known.