Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,35 @@ This document describes breaking changes in CTModels releases and how to migrate

## [0.9.15] - 2026-04-18

### No Breaking Changes
### Breaking Changes: Dual Dimension Function Renaming

The following functions have been renamed to clarify that they return dual dimension, not constraint dimension:

- `dim_variable_constraints_box(sol)` → `dim_dual_variable_constraints_box(sol)`
- `dim_state_constraints_box(sol)` → `dim_dual_state_constraints_box(sol)`
- `dim_control_constraints_box(sol)` → `dim_dual_control_constraints_box(sol)`

#### Migration Guide

```julia
# Before (old function names)
dim = dim_state_constraints_box(sol)

# After (new function names)
dim = dim_dual_state_constraints_box(sol)
```

#### Rationale

The old function names were misleading because they returned the dimension of dual multipliers, not the dimension of constraints declared in the model. The new names clarify this distinction.

#### Note

The functions `dim_*_constraints_box(ocp::Model)` (for Model, not Solution) remain unchanged and still refer to constraint dimension in the model.

### Non-Breaking Changes

This release introduces consistent variable and control checking functions without breaking existing functionality:
This release also introduces consistent variable and control checking functions without breaking existing functionality:

#### New Functions (Non-Breaking)

Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### 🚀 Enhancements

#### Box Constraint Aliases for Label Resolution

- **Unified source of truth**: Removed `original_dict` from `ConstraintsModel` and `original_constraints()` accessor
- **Aliases field**: Added `aliases::Vector{Vector{Symbol}}` as 5th element in box constraint tuples
- **Label resolution**: `constraint(model, label)` and `dual(sol, model, label)` now resolve labels via aliases
- **Effective bounds**: Returns intersected bounds when multiple constraints declared on same component
- **Per-component duals**: Dual matrices sized by state/control/variable dimension, not by number of declarations
- **Deduplication warning**: Single warning per component when duplicate bounds are declared

#### Dual Dimension Function Clarification

- **Renamed functions**: `dim_*_constraints_box(sol)` → `dim_dual_*_constraints_box(sol)` for clarity
- **Multiple dispatch**: `_dual_dimension` uses dispatch on `Nothing` (→ 0) and `Function` (→ length at t0)
- **Display improvement**: Dual variables only displayed if model has declared constraints
- **New exports**: `dim_dual_state_constraints_box`, `dim_dual_control_constraints_box`, `dim_dual_variable_constraints_box`

#### Consistent Variable and Control Checking Functions

- **New functions**: Added `is_variable()` and `is_control_free()` for checking problem properties
Expand Down
76 changes: 49 additions & 27 deletions src/OCP/Building/dual_model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ defined in the model and returns the corresponding dual value from the solution.
# Returns
A function of time `t` for time-dependent constraints, or a scalar/vector for time-invariant duals.
If the label is not found, throws an `IncorrectArgument` exception.

# Notes
- For path/boundary constraints, duals are indexed per declaration (one column per
row of the stacked nonlinear constraint vector).
- For box constraints (state/control/variable), the dual matrices/vectors stored
in the `Solution` are indexed **by primal component** (i.e.
`state_dimension(model)` columns for state, etc.), following the CTDirect
convention. For a label targeting component indices `rg`, this function
returns `duals_lb[:, rg] - duals_ub[:, rg]` (or the time-independent analogue
for variables). Components never constrained carry a zero multiplier.
- If several labels target the same component, `dual(sol, model, :label)` returns
the (same) per-component multiplier for each: CTModels does not track which
declaration "owns" the multiplier, because the solver only sees the effective
(intersected) bound.
"""
function dual(sol::Solution, model::Model, label::Symbol)

Expand Down Expand Up @@ -52,51 +66,59 @@ function dual(sol::Solution, model::Model, label::Symbol)
end
end

# check if the label is in the state constraints
# Box constraints: resolve `label` via the `aliases` field (cp[5]) of each
# box tuple. `cp[2][k]` gives the primal component index for row `k`, which
# is used to slice the per-component dual matrix/vector.
function _box_idxs(cp)
aliases = cp[5]
out = Int[]
for k in eachindex(aliases)
if label in aliases[k]
push!(out, cp[2][k])
end
end
return out
end

# state box
cp = state_constraints_box(model)
labels = cp[4] # vector of labels
if label in labels
# get all the indices of the label
indices = findall(x -> x == label, labels)
# get the corresponding dual values
idxs = _box_idxs(cp)
if !isempty(idxs)
duals_lb = state_constraints_lb_dual(sol)
duals_ub = state_constraints_ub_dual(sol)
if length(indices) == 1
return t -> (duals_lb(t)[indices[1]] - duals_ub(t)[indices[1]])
if length(idxs) == 1
i = idxs[1]
return t -> (duals_lb(t)[i] - duals_ub(t)[i])
else
return t -> (duals_lb(t)[indices] - duals_ub(t)[indices])
return t -> (duals_lb(t)[idxs] - duals_ub(t)[idxs])
end
end

# check if the label is in the control constraints
# control box
cp = control_constraints_box(model)
labels = cp[4] # vector of labels
if label in labels
# get all the indices of the label
indices = findall(x -> x == label, labels)
# get the corresponding dual values, either lower or upper bound
idxs = _box_idxs(cp)
if !isempty(idxs)
duals_lb = control_constraints_lb_dual(sol)
duals_ub = control_constraints_ub_dual(sol)
if length(indices) == 1
return t -> (duals_lb(t)[indices[1]] - duals_ub(t)[indices[1]])
if length(idxs) == 1
i = idxs[1]
return t -> (duals_lb(t)[i] - duals_ub(t)[i])
else
return t -> (duals_lb(t)[indices] - duals_ub(t)[indices])
return t -> (duals_lb(t)[idxs] - duals_ub(t)[idxs])
end
end

# check if the label is in the variable constraints
# variable box
cp = variable_constraints_box(model)
labels = cp[4] # vector of labels
if label in labels
# get all the indices of the label
indices = findall(x -> x == label, labels)
# get the corresponding dual values, either lower or upper bound
idxs = _box_idxs(cp)
if !isempty(idxs)
duals_lb = variable_constraints_lb_dual(sol)
duals_ub = variable_constraints_ub_dual(sol)
if length(indices) == 1
return duals_lb[indices[1]] - duals_ub[indices[1]]
if length(idxs) == 1
i = idxs[1]
return duals_lb[i] - duals_ub[i]
else
return duals_lb[indices] - duals_ub[indices]
return duals_lb[idxs] - duals_ub[idxs]
end
end

Expand Down
6 changes: 3 additions & 3 deletions src/OCP/Building/interpolation_helpers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ function _interpolate_from_data(
# Dimension validation if expected_dim provided
if !isnothing(expected_dim) && !isnothing(dim)
actual_dim = size(data, 2)
@ensure actual_dim >= dim Exceptions.IncorrectArgument(
@ensure actual_dim == dim Exceptions.IncorrectArgument(
"Matrix dimension mismatch",
got="$actual_dim columns",
expected="at least $dim columns",
suggestion="Provide a matrix with at least $dim columns or adjust expected_dim parameter",
expected="exactly $dim columns",
suggestion="Provide a matrix with exactly $dim columns (pad with zeros for unconstrained components if dual).",
context="_interpolate_from_data - validating matrix dimensions",
)
end
Expand Down
Loading
Loading