Skip to content

Duals & diagnostics

Beyond the primal trajectories, a solution carries the Lagrange multipliers of the constraints (grouped in a DualModel) and the solver diagnostics (a SolverInfos).

A solution with duals

We build a small problem with a boundary constraint and two path constraints, then supply the path-constraint multipliers as a callable t → μ(t). State, control and costate are passed as functions here (the array form is equally accepted):

julia
using CTModels

pre = CTModels.PreModel()
CTModels.variable!(pre, 0)
CTModels.time!(pre; t0=0.0, tf=1.0)
CTModels.state!(pre, 1)
CTModels.control!(pre, 1)
CTModels.dynamics!(pre, (r, t, x, u, v) -> (r[1] = u[1]; nothing))
CTModels.objective!(pre, :min; lagrange=(t, x, u, v) -> -u[1])

CTModels.constraint!(pre, :boundary;
    f=(r, x0, xf, v) -> (r[1] = x0[1] + 1; nothing), lb=[0.0], ub=[0.0], label=:initial_con)
CTModels.constraint!(pre, :path;
    f=(r, t, x, u, v) -> (r[1] = x[1] + u[1]; nothing), lb=[-Inf], ub=[0.0])  # 1 row
CTModels.constraint!(pre, :path;
    f=(r, t, x, u, v) -> (r[1] = x[1] + 1; r[2] = u[1] + 1; nothing),
    lb=[-3.0, 1.0], ub=[1.0, 2.5], label=:con2)                                # 2 rows

CTModels.time_dependence!(pre; autonomous=false)
ocp = CTModels.build(pre)

x(t) = -exp(-t)
p(t) = exp(t - 1) - 1
u(t) = -x(t)
mu(t) = [-(p(t) + 1), 0.0, t]      # one entry per stacked path-constraint row

sol = CTModels.build_solution(ocp, collect(range(0.0, 1.0; length=201)),
    x, u, Float64[], p;
    objective=exp(-1) - 1, iterations=12, constraints_violation=0.0,
    message="Solve_Succeeded", status=:optimal, successful=true,
    path_constraints_dual=mu)

Reading a multiplier by label

dual resolves a constraint label to its multiplier — a callable for time-dependent (path) constraints, a value for boundary/variable ones. The label :con2 covers the two path rows declared with it, so its dual is the 2-vector slice:

julia
d = CTModels.dual(sol, ocp, :con2)
julia
julia> d(0.5)
2-element Vector{Float64}:
 0.0
 0.5

The whole path multiplier is reached directly through the DualModel accessor path_constraints_dual:

julia
julia> CTModels.path_constraints_dual(sol)(0.5)
3-element Vector{Float64}:
 -0.6065306597126334
  0.0
  0.5

Boundary-constraint multipliers are read with boundary_constraints_dual:

julia
julia> CTModels.boundary_constraints_dual(sol)

Box-constraint duals

Box constraints on state, control, and variable components have separate lower-bound and upper-bound dual accessors:

AccessorReturns
state_constraints_lb_dualstate lower-bound duals
state_constraints_ub_dualstate upper-bound duals
control_constraints_lb_dualcontrol lower-bound duals
control_constraints_ub_dualcontrol upper-bound duals
variable_constraints_lb_dualvariable lower-bound duals
variable_constraints_ub_dualvariable upper-bound duals

Their dimensions are queried with dim_dual_state_constraints_box, dim_dual_control_constraints_box, and dim_dual_variable_constraints_box.

Checking for duals

A solution produced by a solver carries duals; a solution built by a flow does not. Use has_duals to test this — when it returns false, the dual model is an EmptyDualModel and all dual accessors return nothing.

Box duals are indexed per component

For state/control/variable box constraints, the stored duals are indexed by primal component. dual(sol, model, :label) returns duals_lb[:, rg] - duals_ub[:, rg] for the component range rg of the label. If several labels target the same component (the alias mechanism of the Constraints page), they share that one multiplier — the solver only sees the intersected bound.

Solver diagnostics

The SolverInfos record is exposed through scalar accessors:

julia
julia> CTModels.iterations(sol)
12

julia> CTModels.status(sol)
:optimal

julia> CTModels.message(sol)
"Solve_Succeeded"

julia> CTModels.successful(sol)
true

julia> CTModels.constraints_violation(sol)
0.0

The original Model can be retrieved from a solution with model, and infos(sol) returns the Dict{Symbol,Any} of any extra solver-specific data. These fields are what Serialization writes to disk alongside the trajectories.