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
2 changes: 1 addition & 1 deletion src/CTModels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,4 @@ include(joinpath(@__DIR__, "nlp", "discretized_ocp.jl"))
include(joinpath(@__DIR__, "nlp", "model_api.jl"))
include(joinpath(@__DIR__, "init", "initial_guess.jl"))

end
end
11 changes: 11 additions & 0 deletions src/ocp/model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,18 @@ Appends box constraint data to the provided vectors.
# Notes
- All input vectors (`rg`, `lb`, `ub`) must have the same length.
- The function modifies the `inds`, `lbs`, `ubs`, and `labels` vectors in-place.
- If a component index already exists in `inds`, a warning is emitted indicating that the
previous bound will be overwritten by the new constraint. The dual variable dimension
remains equal to the state/control/variable dimension, not the number of constraint declarations.
"""
function append_box_constraints!(inds, lbs, ubs, labels, rg, lb, ub, label)
# Check for duplicate indices and emit warning
for idx in rg
if idx in inds
@warn "Overwriting bound for component $idx (label: $label). Previous value will be discarded. " *
"Note: dual variable dimension equals the state/control/variable dimension, not the number of constraints."
end
end
append!(inds, rg)
append!(lbs, lb)
append!(ubs, ub)
Expand All @@ -26,6 +36,7 @@ function append_box_constraints!(inds, lbs, ubs, labels, rg, lb, ub, label)
end
end


"""
$(TYPEDSIGNATURES)

Expand Down
8 changes: 8 additions & 0 deletions src/ocp/solution.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@ Build a solution from the optimal control problem, the time grid, the state, con

- `sol::Solution`: the optimal control solution.

# Notes

The dimensions of box constraint dual variables (`state_constraints_*_dual`, `control_constraints_*_dual`,
`variable_constraints_*_dual`) correspond to the **state/control/variable dimension**, not the number of
constraint declarations. If multiple constraints are declared on the same component (e.g., `x₂(t) ≤ 1.2`
and `x₂(t) ≤ 2.0`), only the last bound value is retained, and a warning is emitted during model construction.

"""

function build_solution(
ocp::Model,
T::Vector{Float64},
Expand Down
66 changes: 66 additions & 0 deletions test/ocp/test_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,70 @@ function test_constraints()
# test with :variable constraint and range
CTModels.constraint!(ocp_set, :variable; rg=1:1, lb=[1], ub=[1], label=:variable_rg)
@test ocp_set.constraints[:variable_rg] == (:variable, 1:1, [1], [1])

# -----------------------------------------------------------------------
# Test duplicate constraint warning (Issue #105)
# When multiple constraints are declared on the same component index,
# a warning should be emitted during model build.
# Applies to: state, control, and variable constraints.
# -----------------------------------------------------------------------
@testset "duplicate constraint warning" begin
# --- State constraints ---
@testset "state" begin
ocp_dup = CTModels.PreModel()
CTModels.time!(ocp_dup; t0=0.0, tf=1.0)
CTModels.state!(ocp_dup, 2)
CTModels.control!(ocp_dup, 1)
dynamics!(r, t, x, u, v) = r .= [x[1], u[1]]
CTModels.dynamics!(ocp_dup, dynamics!)
CTModels.objective!(ocp_dup, :min; mayer=(x0, xf, v) -> xf[1])
CTModels.definition!(ocp_dup, quote end)
CTModels.time_dependence!(ocp_dup; autonomous=false)

# Add constraints on state component 1
CTModels.constraint!(ocp_dup, :state; rg=1:1, lb=[0.0], ub=[1.0], label=:s1)
CTModels.constraint!(ocp_dup, :state; rg=1:1, lb=[0.5], ub=[1.5], label=:s2)

@test_warn "Overwriting bound for component 1" CTModels.build(ocp_dup)
end

# --- Control constraints ---
@testset "control" begin
ocp_dup = CTModels.PreModel()
CTModels.time!(ocp_dup; t0=0.0, tf=1.0)
CTModels.state!(ocp_dup, 2)
CTModels.control!(ocp_dup, 2) # 2 controls to allow duplicate on component 1
dynamics!(r, t, x, u, v) = r .= [x[1], u[1]]
CTModels.dynamics!(ocp_dup, dynamics!)
CTModels.objective!(ocp_dup, :min; mayer=(x0, xf, v) -> xf[1])
CTModels.definition!(ocp_dup, quote end)
CTModels.time_dependence!(ocp_dup; autonomous=false)

# Add constraints on control component 1
CTModels.constraint!(ocp_dup, :control; rg=1:1, lb=[0.0], ub=[1.0], label=:c1)
CTModels.constraint!(ocp_dup, :control; rg=1:1, lb=[0.5], ub=[1.5], label=:c2)

@test_warn "Overwriting bound for component 1" CTModels.build(ocp_dup)
end

# --- Variable constraints ---
@testset "variable" begin
ocp_dup = CTModels.PreModel()
CTModels.time!(ocp_dup; t0=0.0, tf=1.0)
CTModels.state!(ocp_dup, 2)
CTModels.control!(ocp_dup, 1)
CTModels.variable!(ocp_dup, 2) # 2 variables to allow duplicate on component 1
dynamics!(r, t, x, u, v) = r .= [x[1], u[1]]
CTModels.dynamics!(ocp_dup, dynamics!)
CTModels.objective!(ocp_dup, :min; mayer=(x0, xf, v) -> xf[1])
CTModels.definition!(ocp_dup, quote end)
CTModels.time_dependence!(ocp_dup; autonomous=false)

# Add constraints on variable component 1
CTModels.constraint!(ocp_dup, :variable; rg=1:1, lb=[0.0], ub=[1.0], label=:v1)
CTModels.constraint!(ocp_dup, :variable; rg=1:1, lb=[0.5], ub=[1.5], label=:v2)

@test_warn "Overwriting bound for component 1" CTModels.build(ocp_dup)
end
end
end
Loading