diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 3d813e19b0..15a97836ae 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.set, b, ci, attr, value) + 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/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() 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()