diff --git a/src/Bridges/Constraint/interval.jl b/src/Bridges/Constraint/interval.jl index 8efddccdab..43f3d646ce 100644 --- a/src/Bridges/Constraint/interval.jl +++ b/src/Bridges/Constraint/interval.jl @@ -1,5 +1,19 @@ _lower_set(set::MOI.Interval) = MOI.GreaterThan(set.lower) +function _lower_set(set::MOI.Interval{T}) where {T<:AbstractFloat} + if set.lower == typemin(T) + return nothing + else + return MOI.GreaterThan(set.lower) + end +end _upper_set(set::MOI.Interval) = MOI.LessThan(set.upper) +function _upper_set(set::MOI.Interval{T}) where {T<:AbstractFloat} + if set.upper == typemax(T) + return nothing + else + return MOI.LessThan(set.upper) + end +end _lower_set(set::MOI.EqualTo) = MOI.GreaterThan(set.value) _upper_set(set::MOI.EqualTo) = MOI.LessThan(set.value) _lower_set(set::MOI.Zeros) = MOI.Nonnegatives(set.dimension) @@ -17,16 +31,31 @@ a `F`-in-`US` constraint where we have either: For instance, if `F` is `MOI.ScalarAffineFunction` and `S` is `MOI.Interval`, it transforms the constraint ``l ≤ ⟨a, x⟩ + α ≤ u`` into the constraints ``⟨a, x⟩ + α ≥ l`` and ``⟨a, x⟩ + α ≤ u``. + +!!! note + If `T<:AbstractFloat` and `S` is `MOI.Interval{T}` then no lower (resp. + upper) bound constraint is created if the lower (resp. upper) bound is + `typemin(T)` (resp. `typemax(T)`). Similarly, when + [`MathOptInterface.ConstraintSet`](@ref) is set, a lower or upper bound + constraint may be deleted or created accordingly. """ -struct SplitIntervalBridge{ +mutable struct SplitIntervalBridge{ T, F<:MOI.AbstractFunction, S<:MOI.AbstractSet, LS<:MOI.AbstractSet, US<:MOI.AbstractSet, } <: AbstractBridge - lower::MOI.ConstraintIndex{F,LS} - upper::MOI.ConstraintIndex{F,US} + lower::Union{Nothing,MOI.ConstraintIndex{F,LS}} + upper::Union{Nothing,MOI.ConstraintIndex{F,US}} + # To allow the user to do + # ```jl + # x = MOI.add_variable(model) + # c = MOI.add_constraint(model, x, MOI.Interval(-Inf, Inf)) + # MOI.set(model, MOI.ConstraintSet(), c, MOI.Interval(0.0, Inf)) + # ``` + # we need to store the function to create the lower bound constraint. + func::Union{Nothing,F} end function bridge_constraint( @@ -35,9 +64,24 @@ function bridge_constraint( f::F, set::S, ) where {T,F,S,LS,US} - lower = MOI.add_constraint(model, f, _lower_set(set)) - upper = MOI.add_constraint(model, f, _upper_set(set)) - return SplitIntervalBridge{T,F,S,LS,US}(lower, upper) + lower_set = _lower_set(set) + if lower_set === nothing + lower = nothing + else + lower = MOI.add_constraint(model, f, _lower_set(set)) + end + upper_set = _upper_set(set) + if upper_set === nothing + upper = nothing + else + upper = MOI.add_constraint(model, f, _upper_set(set)) + end + if lower === nothing && upper === nothing + func = f + else + func = nothing + end + return SplitIntervalBridge{T,F,S,LS,US}(lower, upper, func) end function MOI.supports_constraint( @@ -85,36 +129,56 @@ function concrete_bridge_type( end function MOI.get( - ::SplitIntervalBridge{T,F,S,LS}, + bridge::SplitIntervalBridge{T,F,S,LS}, ::MOI.NumberOfConstraints{F,LS}, )::Int64 where {T,F,S,LS} - return 1 + if bridge.lower === nothing + return 0 + else + return 1 + end end function MOI.get( - ::SplitIntervalBridge{T,F,S,LS,US}, + bridge::SplitIntervalBridge{T,F,S,LS,US}, ::MOI.NumberOfConstraints{F,US}, )::Int64 where {T,F,S,LS,US} - return 1 + if bridge.upper === nothing + return 0 + else + return 1 + end end function MOI.get( bridge::SplitIntervalBridge{T,F,S,LS}, ::MOI.ListOfConstraintIndices{F,LS}, ) where {T,F,S,LS} - return [bridge.lower] + if bridge.lower === nothing + return MOI.ConstraintIndex{F,LS}[] + else + return [bridge.lower] + end end function MOI.get( bridge::SplitIntervalBridge{T,F,S,LS,US}, ::MOI.ListOfConstraintIndices{F,US}, ) where {T,F,S,LS,US} - return [bridge.upper] + if bridge.upper === nothing + return MOI.ConstraintIndex{F,US}[] + else + return [bridge.upper] + end end function MOI.delete(model::MOI.ModelLike, bridge::SplitIntervalBridge) - MOI.delete(model, bridge.lower) - MOI.delete(model, bridge.upper) + if bridge.lower !== nothing + MOI.delete(model, bridge.lower) + end + if bridge.upper !== nothing + MOI.delete(model, bridge.upper) + end return end @@ -128,13 +192,25 @@ function MOI.supports( return MOI.supports(model, attr, ci_1) && MOI.supports(model, attr, ci_2) end +function _error_double_inf(attr) + return error( + "Cannot get `$attr` for a constraint in the interval `[-Inf, Inf]`.", + ) +end + function MOI.get( model::MOI.ModelLike, attr::Union{MOI.ConstraintPrimal,MOI.ConstraintPrimalStart}, bridge::SplitIntervalBridge, ) # lower and upper should give the same value - return MOI.get(model, attr, bridge.lower) + if bridge.lower !== nothing + return MOI.get(model, attr, bridge.lower) + end + if bridge.upper !== nothing + return MOI.get(model, attr, bridge.upper) + end + return _error_double_inf(attr) end function MOI.set( @@ -143,8 +219,12 @@ function MOI.set( bridge::SplitIntervalBridge, value, ) - MOI.set(model, attr, bridge.lower, value) - MOI.set(model, attr, bridge.upper, value) + if bridge.lower !== nothing + MOI.set(model, attr, bridge.lower, value) + end + if bridge.upper !== nothing + MOI.set(model, attr, bridge.upper, value) + end return end @@ -157,10 +237,22 @@ end function MOI.get( model::MOI.ModelLike, attr::Union{MOI.ConstraintDual,MOI.ConstraintDualStart}, - bridge::SplitIntervalBridge, -) - return MOI.get(model, attr, bridge.lower) + - MOI.get(model, attr, bridge.upper) + bridge::SplitIntervalBridge{T}, +) where {T} + if bridge.lower === nothing + if bridge.upper === nothing + return zero(T) + else + return MOI.get(model, attr, bridge.upper) + end + else + if bridge.upper === nothing + return MOI.get(model, attr, bridge.lower) + else + return MOI.get(model, attr, bridge.lower) + + MOI.get(model, attr, bridge.upper) + end + end end function _split_dual_start(value) @@ -185,27 +277,50 @@ function MOI.set( bridge::SplitIntervalBridge{T}, value, ) where {T} - lower, upper = _split_dual_start(value) - MOI.set(model, attr, bridge.lower, lower) - MOI.set(model, attr, bridge.upper, upper) + if bridge.lower === nothing + if bridge.upper !== nothing + MOI.set(model, attr, bridge.upper, value) + end + else + if bridge.upper === nothing + MOI.set(model, attr, bridge.lower, value) + else + lower, upper = _split_dual_start(value) + MOI.set(model, attr, bridge.lower, lower) + MOI.set(model, attr, bridge.upper, upper) + end + end return end function MOI.get( model::MOI.ModelLike, - ::MOI.ConstraintBasisStatus, + attr::MOI.ConstraintBasisStatus, bridge::SplitIntervalBridge, ) - lower_stat = MOI.get(model, MOI.ConstraintBasisStatus(), bridge.lower) - if lower_stat == MOI.NONBASIC - return MOI.NONBASIC_AT_LOWER + if bridge.upper !== nothing + upper_stat = MOI.get(model, attr, bridge.upper) + if upper_stat == MOI.NONBASIC + return MOI.NONBASIC_AT_UPPER + end end - upper_stat = MOI.get(model, MOI.ConstraintBasisStatus(), bridge.upper) - if upper_stat == MOI.NONBASIC - return MOI.NONBASIC_AT_UPPER + if bridge.lower === nothing + if bridge.upper === nothing + # The only case where the interval `[-∞, ∞]` is allowed is for + # `VariableIndex` constraints but `ConstraintBasisStatus` is not + # defined for `VariableIndex` constraints. + _error_double_inf(attr) + else + return upper_stat + end + else + lower_stat = MOI.get(model, attr, bridge.lower) + if lower_stat == MOI.NONBASIC + return MOI.NONBASIC_AT_LOWER + end + # Both statuses must be BASIC or SUPER_BASIC, so just return the lower. + return lower_stat end - # Both statuses must be BASIC or SUPER_BASIC, so just return the lower. - return lower_stat end function MOI.modify( @@ -213,8 +328,12 @@ function MOI.modify( bridge::SplitIntervalBridge, change::MOI.AbstractFunctionModification, ) - MOI.modify(model, bridge.lower, change) - MOI.modify(model, bridge.upper, change) + if bridge.lower !== nothing + MOI.modify(model, bridge.lower, change) + end + if bridge.upper !== nothing + MOI.modify(model, bridge.upper, change) + end return end @@ -224,8 +343,12 @@ function MOI.set( bridge::SplitIntervalBridge{T,F}, func::F, ) where {T,F} - MOI.set(model, MOI.ConstraintFunction(), bridge.lower, func) - MOI.set(model, MOI.ConstraintFunction(), bridge.upper, func) + if bridge.lower !== nothing + MOI.set(model, MOI.ConstraintFunction(), bridge.lower, func) + end + if bridge.upper !== nothing + MOI.set(model, MOI.ConstraintFunction(), bridge.upper, func) + end return end @@ -235,8 +358,39 @@ function MOI.set( bridge::SplitIntervalBridge{T,F,S}, change::S, ) where {T,F,S} - MOI.set(model, MOI.ConstraintSet(), bridge.lower, _lower_set(change)) - MOI.set(model, MOI.ConstraintSet(), bridge.upper, _upper_set(change)) + lower_set = _lower_set(change) + upper_set = _upper_set(change) + if lower_set === nothing && upper_set === nothing + # The constraints are going to be deleted, we store the function before + # it is lost. + bridge.func = MOI.get(model, MOI.ConstraintFunction(), bridge) + end + if bridge.lower === nothing + if lower_set !== nothing + func = MOI.get(model, MOI.ConstraintFunction(), bridge) + bridge.lower = MOI.add_constraint(model, func, lower_set) + end + else + if lower_set === nothing + MOI.delete(model, bridge.lower) + bridge.lower = nothing + else + MOI.set(model, MOI.ConstraintSet(), bridge.lower, lower_set) + end + end + if bridge.upper === nothing + if upper_set !== nothing + func = MOI.get(model, MOI.ConstraintFunction(), bridge) + bridge.upper = MOI.add_constraint(model, func, upper_set) + end + else + if upper_set === nothing + MOI.delete(model, bridge.upper) + bridge.upper = nothing + else + MOI.set(model, MOI.ConstraintSet(), bridge.upper, upper_set) + end + end return end @@ -245,7 +399,13 @@ function MOI.get( attr::MOI.ConstraintFunction, bridge::SplitIntervalBridge, ) - return MOI.get(model, attr, bridge.lower) + if bridge.lower !== nothing + return MOI.get(model, attr, bridge.lower) + end + if bridge.upper !== nothing + return MOI.get(model, attr, bridge.upper) + end + return bridge.func end function MOI.get( @@ -253,10 +413,17 @@ function MOI.get( attr::MOI.ConstraintSet, bridge::SplitIntervalBridge{T,F,MOI.Interval{T}}, ) where {T,F} - return MOI.Interval( - MOI.get(model, attr, bridge.lower).lower, - MOI.get(model, attr, bridge.upper).upper, - ) + if bridge.lower === nothing + lower = typemin(T) + else + lower = MOI.get(model, attr, bridge.lower).lower + end + if bridge.upper === nothing + upper = typemax(T) + else + upper = MOI.get(model, attr, bridge.upper).upper + end + return MOI.Interval(lower, upper) end function MOI.get( diff --git a/src/Bridges/Constraint/ltgt_to_interval.jl b/src/Bridges/Constraint/ltgt_to_interval.jl index f40c281c9b..7615528fa9 100644 --- a/src/Bridges/Constraint/ltgt_to_interval.jl +++ b/src/Bridges/Constraint/ltgt_to_interval.jl @@ -12,8 +12,8 @@ field by convention. !!! warning It is required that `T` be a `AbstractFloat` type because otherwise - typemin and typemax would either be not implemented (e.g. BigInt) - or would not give infinite value (e.g. Int). For this reason, + `typemin` and `typemax` would either be not implemented (e.g. `BigInt`) + or would not give infinite value (e.g. `Int`). For this reason, this bridge is only added to [`MathOptInterface.Bridges.full_bridge_optimizer`](@ref). diff --git a/test/Bridges/Constraint/interval.jl b/test/Bridges/Constraint/interval.jl index 352542e8fd..6a0888f73a 100644 --- a/test/Bridges/Constraint/interval.jl +++ b/test/Bridges/Constraint/interval.jl @@ -264,6 +264,107 @@ function test_conic_linear_VectorOfVariables() return end +function _test_interval( + mock, + bridged_mock, + set::MOI.Interval{T}, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{T}}, +) where {T} + haslb = set.lower != typemin(T) + hasub = set.upper != typemax(T) + @test MOI.Bridges.is_bridged(bridged_mock, ci) + bridge = MOI.Bridges.bridge(bridged_mock, ci) + if haslb + @test bridge.lower !== nothing + MOI.set(mock, MOI.ConstraintBasisStatus(), bridge.lower, MOI.BASIC) + else + @test bridge.lower === nothing + end + if hasub + @test bridge.upper !== nothing + MOI.set(mock, MOI.ConstraintBasisStatus(), bridge.upper, MOI.BASIC) + else + @test bridge.upper === nothing + end + @test set == MOI.get(bridged_mock, MOI.ConstraintSet(), ci) + attr = MOI.NumberOfConstraints{MOI.VariableIndex,MOI.GreaterThan{T}}() + @test 0 == MOI.get(bridged_mock, attr) + @test (haslb ? 1 : 0) == MOI.get(mock, attr) + attr = MOI.NumberOfConstraints{MOI.VariableIndex,MOI.LessThan{T}}() + @test 0 == MOI.get(bridged_mock, attr) + @test (hasub ? 1 : 0) == MOI.get(mock, attr) + attr = MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.GreaterThan{T}}() + @test isempty(MOI.get(bridged_mock, attr)) + @test (haslb ? 1 : 0) == length(MOI.get(mock, attr)) + attr = MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.LessThan{T}}() + @test isempty(MOI.get(bridged_mock, attr)) + @test (hasub ? 1 : 0) == length(MOI.get(mock, attr)) + if haslb || hasub + @test one(T) == MOI.get(bridged_mock, MOI.ConstraintPrimal(), ci) + @test MOI.BASIC == + MOI.get(bridged_mock, MOI.ConstraintBasisStatus(), ci) + for attr in [MOI.ConstraintPrimalStart(), MOI.ConstraintDualStart()] + MOI.set(bridged_mock, attr, ci, T(2)) + @test T(2) == MOI.get(bridged_mock, attr, ci) + end + else + for attr in [ + MOI.ConstraintPrimal(), + MOI.ConstraintPrimalStart(), + MOI.ConstraintBasisStatus(), + ] + err = ErrorException( + "Cannot get `$attr` for a constraint " * + "in the interval `[-Inf, Inf]`.", + ) + @test_throws err MOI.get(bridged_mock, attr, ci) + end + @test zero(T) == MOI.get(bridged_mock, MOI.ConstraintDualStart(), ci) + end + @test zero(T) == MOI.get(bridged_mock, MOI.ConstraintDual(), ci) +end + +function test_infinite_bounds(::Type{T} = Float64) where {T<:AbstractFloat} + mock = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + ) + bridged_mock = MOI.Bridges.Constraint.SplitInterval{T}(mock) + x = MOI.add_variable(bridged_mock) + MOI.set(mock, MOI.VariablePrimal(), x, one(T)) + set = MOI.Interval(typemin(T), one(T)) + ci = MOI.add_constraint(bridged_mock, x, set) + _test_interval(mock, bridged_mock, set, ci) + set = MOI.Interval(zero(T), typemax(T)) + MOI.set(bridged_mock, MOI.ConstraintSet(), ci, set) + _test_interval(mock, bridged_mock, set, ci) + set = MOI.Interval(typemin(T), typemax(T)) + MOI.set(bridged_mock, MOI.ConstraintSet(), ci, set) + _test_interval(mock, bridged_mock, set, ci) + _test_delete_bridge( + bridged_mock, + ci, + 1, + ( + (MOI.VariableIndex, MOI.GreaterThan{T}, 0), + (MOI.VariableIndex, MOI.LessThan{T}, 0), + ), + ) + ci = MOI.add_constraint(bridged_mock, x, set) + _test_interval(mock, bridged_mock, set, ci) + _test_delete_bridge( + bridged_mock, + ci, + 1, + ( + (MOI.VariableIndex, MOI.GreaterThan{T}, 0), + (MOI.VariableIndex, MOI.LessThan{T}, 0), + ), + ) + set = MOI.Interval(zero(T), typemax(T)) + ci = MOI.add_constraint(bridged_mock, x, set) + return _test_interval(mock, bridged_mock, set, ci) +end + end # module TestConstraintSplitInterval.runtests()