diff --git a/src/CTModels.jl b/src/CTModels.jl index beaba002..ae95101c 100644 --- a/src/CTModels.jl +++ b/src/CTModels.jl @@ -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 \ No newline at end of file diff --git a/src/ocp/model.jl b/src/ocp/model.jl index 05b62fd2..56313d36 100644 --- a/src/ocp/model.jl +++ b/src/ocp/model.jl @@ -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) @@ -26,6 +36,7 @@ function append_box_constraints!(inds, lbs, ubs, labels, rg, lb, ub, label) end end + """ $(TYPEDSIGNATURES) diff --git a/src/ocp/solution.jl b/src/ocp/solution.jl index 4b6dba78..09f6c618 100644 --- a/src/ocp/solution.jl +++ b/src/ocp/solution.jl @@ -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}, diff --git a/test/ocp/test_constraints.jl b/test/ocp/test_constraints.jl index 94d3e25b..4b592140 100644 --- a/test/ocp/test_constraints.jl +++ b/test/ocp/test_constraints.jl @@ -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