From aab90bcee06e867c397fbefb7afa8fb023c9c938 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 7 Sep 2026 15:09:42 +1200 Subject: [PATCH 1/2] [Bridges] fix ConstraintPrimalStart when there are bridged variables --- src/Bridges/bridge_optimizer.jl | 52 +++++++++++++------ test/Bridges/Variable/test_VectorizeBridge.jl | 20 +++++++ 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 3d813e19b0..9e4686b601 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -1652,27 +1652,49 @@ end function MOI.get( b::AbstractBridgeOptimizer, - attr::MOI.ConstraintPrimal, + attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, ci::MOI.ConstraintIndex{F,S}, ) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} if is_bridged(b, ci) MOI.throw_if_not_valid(b, ci) return call_in_context(MOI.get, b, ci, attr) end - if Variable.has_bridges(Variable.bridges(b)) - # In this case, the scalar constraint might contain bridged variables - # that include constants, like `x >= 1` being mapped to `y >= 0`, - # `x := y + 1`. If this is true, then `normalize_and_add_constraint` - # may move the constants into the set, and querying `ConstraintPrimal` - # from `b.model` will return the value of `y`, not `y + 1`. As a - # work-around, we use `get_fallback`, which first queries - # `ConstraintFunction` and then evaluates the function at the point - # defined by `VariablePrimal`. Querying `ConstraintFunction` accounts - # for the case where constants were moved to the set, so we return the - # correct value. - return MOI.Utilities.get_fallback(b, attr, ci) - end - return MOI.get(b.model, attr, ci) + ret = MOI.get(b.model, attr, ci) + if !Variable.has_bridges(Variable.bridges(b)) || ret === nothing + return ret + end + # In this case, the scalar constraint might contain bridged variables + # that include constants, like `x >= 1` being mapped to `y >= 0`, + # `x := y + 1`. If this is true, then `normalize_and_add_constraint` may + # move the constants into the set, so we need to convert the primal value + # from `y`-space to `x`-space by adding back the constant offset. + f = MOI.get(b, MOI.ConstraintFunction(), ci) + offset = MOI.constant(bridged_function(b, f), eltype(ret)) + return ret .+ offset +end + +function MOI.set( + b::AbstractBridgeOptimizer, + attr::MOI.ConstraintPrimalStart, + ci::MOI.ConstraintIndex{F,S}, + value, +) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} + if is_bridged(b, ci) + MOI.throw_if_not_valid(b, ci) + return call_in_context(MOI.get, b, ci, attr) + elseif !Variable.has_bridges(Variable.bridges(b)) || value === nothing + MOI.set(b.model, attr, ci, value) + return + end + # In this case, the scalar constraint might contain bridged variables + # that include constants, like `x >= 1` being mapped to `y >= 0`, + # `x := y + 1`. If this is true, then `normalize_and_add_constraint` may + # move the constants into the set, so we need to convert the primal start + # from `x`-space to `y`-space by subtracting the constant offset. + f = MOI.get(b, MOI.ConstraintFunction(), ci) + offset = MOI.constant(bridged_function(b, f), eltype(value)) + MOI.set(b.model, attr, ci, value - offset) + return end function MOI.supports( diff --git a/test/Bridges/Variable/test_VectorizeBridge.jl b/test/Bridges/Variable/test_VectorizeBridge.jl index e24570ddc1..33936de912 100644 --- a/test/Bridges/Variable/test_VectorizeBridge.jl +++ b/test/Bridges/Variable/test_VectorizeBridge.jl @@ -311,6 +311,26 @@ function test_variable_primal_ray() return end +function test_constraint_primal_start_offset() + inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + model = MOI.Bridges.Variable.Vectorize{Float64}(inner) + x, cx = MOI.add_constrained_variable(model, MOI.GreaterThan(3.0)) + ci = MOI.add_constraint(model, 1.0 * x, MOI.LessThan(10.0)) + MOI.set(model, MOI.ConstraintPrimalStart(), ci, 7.0) + @test MOI.get(model, MOI.ConstraintPrimalStart(), ci) == 7.0 + # Outer model is: + # x >= 3 + # 1.0 * x <= 10 [primal_start = 7] + # Inner model is: + # y >= 0 x := y + 3 + # 1.0 * y <= 7.0 [primal_start = 4] + # x = y + 3, y >= 0 + F, S = MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64} + ci_inner = only(MOI.get(inner, MOI.ListOfConstraintIndices{F,S}())) + @test MOI.get(inner, MOI.ConstraintPrimalStart(), ci_inner) == 4.0 + return +end + end # module TestVariableVectorize.runtests() From ca64c265cb6224bae17ff70997de603f0858a5ba Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Mon, 7 Sep 2026 16:17:35 +1200 Subject: [PATCH 2/2] Update --- src/Bridges/bridge_optimizer.jl | 2 +- test/Bridges/Constraint/test_VectorizeBridge.jl | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 9e4686b601..15a97836ae 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -1681,7 +1681,7 @@ function MOI.set( ) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} if is_bridged(b, ci) MOI.throw_if_not_valid(b, ci) - return call_in_context(MOI.get, b, ci, attr) + return call_in_context(MOI.set, b, ci, attr, value) elseif !Variable.has_bridges(Variable.bridges(b)) || value === nothing MOI.set(b.model, attr, ci, value) return diff --git a/test/Bridges/Constraint/test_VectorizeBridge.jl b/test/Bridges/Constraint/test_VectorizeBridge.jl index e17e9c4e42..205c0ab3ab 100644 --- a/test/Bridges/Constraint/test_VectorizeBridge.jl +++ b/test/Bridges/Constraint/test_VectorizeBridge.jl @@ -301,6 +301,19 @@ function test_constraint_primal_ray() return end +function test_constraint_primal_start() + mock = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + ) + model = MOI.Bridges.Constraint.Vectorize{Float64}(mock) + x, c = MOI.add_constrained_variables(model, MOI.Nonnegatives(2)) + ci = MOI.add_constraint(model, 1.0 * x[1] + x[2], MOI.EqualTo(1.0)) + @test MOI.supports(model, MOI.ConstraintPrimalStart(), typeof(ci)) + MOI.set(model, MOI.ConstraintPrimalStart(), ci, 1.0) + @test MOI.get(model, MOI.ConstraintPrimalStart(), ci) == 1.0 + return +end + end # module TestConstraintVectorize.runtests()