diff --git a/docs/src/submodules/Bridges/list_of_bridges.md b/docs/src/submodules/Bridges/list_of_bridges.md index 60e3236e65..7a2d339ee3 100644 --- a/docs/src/submodules/Bridges/list_of_bridges.md +++ b/docs/src/submodules/Bridges/list_of_bridges.md @@ -79,6 +79,9 @@ Bridges.Constraint.CountDistinctToMILPBridge Bridges.Constraint.ReifiedCountDistinctToMILPBridge Bridges.Constraint.CountGreaterThanToMILPBridge Bridges.Constraint.TableToMILPBridge +Bridges.Constraint.SOS1ToMILPBridge +Bridges.Constraint.SOS2ToMILPBridge +Bridges.Constraint.IndicatorToMILPBridge ``` ## [Objective bridges](@id objective_bridges_ref) diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index 8251eb0d32..c19658d2a6 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -256,6 +256,7 @@ function runtests( variable_start = 1.2, constraint_start = 1.2, eltype = Float64, + print_inner_model::Bool = false, ) # Load model and bridge it inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{eltype}()) @@ -264,6 +265,9 @@ function runtests( final_touch(model) # Should be able to call final_touch multiple times. final_touch(model) + if print_inner_model + print(inner) + end # Load a non-bridged input model, and check that getters are the same. test = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{eltype}()) MOI.Utilities.loadfromstring!(test, input) diff --git a/src/Bridges/Constraint/Constraint.jl b/src/Bridges/Constraint/Constraint.jl index 576bec6cd3..f15adf6cd7 100644 --- a/src/Bridges/Constraint/Constraint.jl +++ b/src/Bridges/Constraint/Constraint.jl @@ -62,6 +62,9 @@ include("bridges/set_dot_scaling.jl") include("bridges/table.jl") include("bridges/vectorize.jl") include("bridges/zero_one.jl") +include("bridges/sos1_to_milp.jl") +include("bridges/sos2_to_milp.jl") +include("bridges/indicator_to_milp.jl") """ add_all_bridges(bridged_model, ::Type{T}) where {T} @@ -142,6 +145,9 @@ function add_all_bridges(bridged_model, ::Type{T}) where {T} MOI.Bridges.add_bridge(bridged_model, ReifiedCountDistinctToMILPBridge{T}) MOI.Bridges.add_bridge(bridged_model, CountGreaterThanToMILPBridge{T}) MOI.Bridges.add_bridge(bridged_model, TableToMILPBridge{T}) + MOI.Bridges.add_bridge(bridged_model, SOS1ToMILPBridge{T}) + MOI.Bridges.add_bridge(bridged_model, SOS2ToMILPBridge{T}) + MOI.Bridges.add_bridge(bridged_model, IndicatorToMILPBridge{T}) return end diff --git a/src/Bridges/Constraint/bridges/bin_packing.jl b/src/Bridges/Constraint/bridges/bin_packing.jl index c65102d383..70803c2f49 100644 --- a/src/Bridges/Constraint/bridges/bin_packing.jl +++ b/src/Bridges/Constraint/bridges/bin_packing.jl @@ -194,43 +194,6 @@ end MOI.Bridges.needs_final_touch(::BinPackingToMILPBridge) = true -# We use the bridge as the first argument to avoid type piracy of other methods. -function _get_bounds( - bridge::BinPackingToMILPBridge{T}, - model::MOI.ModelLike, - bounds::Dict{MOI.VariableIndex,NTuple{2,T}}, - f::MOI.ScalarAffineFunction{T}, -) where {T} - lb = ub = f.constant - for term in f.terms - ret = _get_bounds(bridge, model, bounds, term.variable) - if ret === nothing - return nothing - end - lb += term.coefficient * ret[1] - ub += term.coefficient * ret[2] - end - return lb, ub -end - -# We use the bridge as the first argument to avoid type piracy of other methods. -function _get_bounds( - ::BinPackingToMILPBridge{T}, - model::MOI.ModelLike, - bounds::Dict{MOI.VariableIndex,NTuple{2,T}}, - x::MOI.VariableIndex, -) where {T} - if haskey(bounds, x) - return bounds[x] - end - ret = MOI.Utilities.get_bounds(model, T, x) - if ret == (typemin(T), typemax(T)) - return nothing - end - bounds[x] = ret - return ret -end - function MOI.Bridges.final_touch( bridge::BinPackingToMILPBridge{T,F}, model::MOI.ModelLike, @@ -240,7 +203,7 @@ function MOI.Bridges.final_touch( bounds = Dict{MOI.VariableIndex,NTuple{2,T}}() for i in 1:length(scalars) x = scalars[i] - ret = _get_bounds(bridge, model, bounds, x) + ret = MOI.Utilities.get_bounds(model, bounds, x) if ret === nothing error( "Unable to use $(typeof(bridge)) because an element in the " * diff --git a/src/Bridges/Constraint/bridges/count_belongs.jl b/src/Bridges/Constraint/bridges/count_belongs.jl index e68b917941..dc28b16781 100644 --- a/src/Bridges/Constraint/bridges/count_belongs.jl +++ b/src/Bridges/Constraint/bridges/count_belongs.jl @@ -189,41 +189,6 @@ end MOI.Bridges.needs_final_touch(::CountBelongsToMILPBridge) = true -function _get_bounds( - bridge::CountBelongsToMILPBridge{T}, - model::MOI.ModelLike, - bounds::Dict{MOI.VariableIndex,Tuple{T,T}}, - f::MOI.ScalarAffineFunction{T}, -) where {T} - lb = ub = f.constant - for term in f.terms - ret = _get_bounds(bridge, model, bounds, term.variable) - if ret === nothing - return nothing - end - lb += term.coefficient * ret[1] - ub += term.coefficient * ret[2] - end - return lb, ub -end - -function _get_bounds( - ::CountBelongsToMILPBridge{T}, - model::MOI.ModelLike, - bounds::Dict{MOI.VariableIndex,Tuple{T,T}}, - x::MOI.VariableIndex, -) where {T} - if haskey(bounds, x) - return bounds[x] - end - ret = MOI.Utilities.get_bounds(model, T, x) - if ret == (typemin(T), typemax(T)) - return nothing - end - bounds[x] = ret - return ret -end - """ _unit_expansion( ::CountBelongsToMILPBridge{T}, @@ -243,7 +208,7 @@ function _unit_expansion( bounds = Dict{MOI.VariableIndex,Tuple{T,T}}() ci = MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}[] for i in 1:length(f) - ret = _get_bounds(bridge, model, bounds, f[i]) + ret = MOI.Utilities.get_bounds(model, bounds, f[i]) if ret === nothing BT = typeof(bridge) error( diff --git a/src/Bridges/Constraint/bridges/count_distinct.jl b/src/Bridges/Constraint/bridges/count_distinct.jl index b8602695b4..b1c2ddb73d 100644 --- a/src/Bridges/Constraint/bridges/count_distinct.jl +++ b/src/Bridges/Constraint/bridges/count_distinct.jl @@ -228,43 +228,6 @@ end MOI.Bridges.needs_final_touch(::CountDistinctToMILPBridge) = true -# We use the bridge as the first argument to avoid type piracy of other methods. -function _get_bounds( - bridge::CountDistinctToMILPBridge{T}, - model::MOI.ModelLike, - bounds::Dict{MOI.VariableIndex,NTuple{2,T}}, - f::MOI.ScalarAffineFunction{T}, -) where {T} - lb = ub = f.constant - for term in f.terms - ret = _get_bounds(bridge, model, bounds, term.variable) - if ret === nothing - return nothing - end - lb += term.coefficient * ret[1] - ub += term.coefficient * ret[2] - end - return lb, ub -end - -# We use the bridge as the first argument to avoid type piracy of other methods. -function _get_bounds( - ::CountDistinctToMILPBridge{T}, - model::MOI.ModelLike, - bounds::Dict{MOI.VariableIndex,NTuple{2,T}}, - x::MOI.VariableIndex, -) where {T} - if haskey(bounds, x) - return bounds[x] - end - ret = MOI.Utilities.get_bounds(model, T, x) - if ret == (typemin(T), typemax(T)) - return nothing - end - bounds[x] = ret - return ret -end - function MOI.Bridges.final_touch( bridge::CountDistinctToMILPBridge{T,F}, model::MOI.ModelLike, @@ -274,7 +237,7 @@ function MOI.Bridges.final_touch( bounds = Dict{MOI.VariableIndex,NTuple{2,T}}() for i in 2:length(scalars) x = scalars[i] - ret = _get_bounds(bridge, model, bounds, x) + ret = MOI.Utilities.get_bounds(model, bounds, x) if ret === nothing error( "Unable to use CountDistinctToMILPBridge because element $i " * diff --git a/src/Bridges/Constraint/bridges/count_distinct_reif.jl b/src/Bridges/Constraint/bridges/count_distinct_reif.jl index ed357c33ad..f4538b8ae9 100644 --- a/src/Bridges/Constraint/bridges/count_distinct_reif.jl +++ b/src/Bridges/Constraint/bridges/count_distinct_reif.jl @@ -246,43 +246,6 @@ end MOI.Bridges.needs_final_touch(::ReifiedCountDistinctToMILPBridge) = true -# We use the bridge as the first argument to avoid type piracy of other methods. -function _get_bounds( - bridge::ReifiedCountDistinctToMILPBridge{T}, - model::MOI.ModelLike, - bounds::Dict{MOI.VariableIndex,NTuple{2,T}}, - f::MOI.ScalarAffineFunction{T}, -) where {T} - lb = ub = f.constant - for term in f.terms - ret = _get_bounds(bridge, model, bounds, term.variable) - if ret === nothing - return nothing - end - lb += term.coefficient * ret[1] - ub += term.coefficient * ret[2] - end - return lb, ub -end - -# We use the bridge as the first argument to avoid type piracy of other methods. -function _get_bounds( - ::ReifiedCountDistinctToMILPBridge{T}, - model::MOI.ModelLike, - bounds::Dict{MOI.VariableIndex,NTuple{2,T}}, - x::MOI.VariableIndex, -) where {T} - if haskey(bounds, x) - return bounds[x] - end - ret = MOI.Utilities.get_bounds(model, T, x) - if ret == (typemin(T), typemax(T)) - return nothing - end - bounds[x] = ret - return ret -end - function MOI.Bridges.final_touch( bridge::ReifiedCountDistinctToMILPBridge{T,F}, model::MOI.ModelLike, @@ -292,7 +255,7 @@ function MOI.Bridges.final_touch( bounds = Dict{MOI.VariableIndex,NTuple{2,T}}() for i in 3:length(scalars) x = scalars[i] - ret = _get_bounds(bridge, model, bounds, x) + ret = MOI.Utilities.get_bounds(model, bounds, x) if ret === nothing error( "Unable to use ReifiedCountDistinctToMILPBridge because " * diff --git a/src/Bridges/Constraint/bridges/count_greater_than.jl b/src/Bridges/Constraint/bridges/count_greater_than.jl index 0a2a8b6906..c76881e039 100644 --- a/src/Bridges/Constraint/bridges/count_greater_than.jl +++ b/src/Bridges/Constraint/bridges/count_greater_than.jl @@ -182,43 +182,6 @@ end MOI.Bridges.needs_final_touch(::CountGreaterThanToMILPBridge) = true -# We use the bridge as the first argument to avoid type piracy of other methods. -function _get_bounds( - bridge::CountGreaterThanToMILPBridge{T}, - model::MOI.ModelLike, - bounds::Dict{MOI.VariableIndex,NTuple{2,T}}, - f::MOI.ScalarAffineFunction{T}, -) where {T} - lb = ub = f.constant - for term in f.terms - ret = _get_bounds(bridge, model, bounds, term.variable) - if ret === nothing - return nothing - end - lb += term.coefficient * ret[1] - ub += term.coefficient * ret[2] - end - return lb, ub -end - -# We use the bridge as the first argument to avoid type piracy of other methods. -function _get_bounds( - ::CountGreaterThanToMILPBridge{T}, - model::MOI.ModelLike, - bounds::Dict{MOI.VariableIndex,NTuple{2,T}}, - x::MOI.VariableIndex, -) where {T} - if haskey(bounds, x) - return bounds[x] - end - ret = MOI.Utilities.get_bounds(model, T, x) - if ret == (typemin(T), typemax(T)) - return nothing - end - bounds[x] = ret - return ret -end - function _add_unit_expansion( bridge::CountGreaterThanToMILPBridge{T,F}, model::MOI.ModelLike, @@ -227,7 +190,7 @@ function _add_unit_expansion( x, i, ) where {T,F} - ret = _get_bounds(bridge, model, bounds, x) + ret = MOI.Utilities.get_bounds(model, bounds, x) if ret === nothing error( "Unable to use $(typeof(bridge)) because an element in the " * diff --git a/src/Bridges/Constraint/bridges/indicator_to_milp.jl b/src/Bridges/Constraint/bridges/indicator_to_milp.jl new file mode 100644 index 0000000000..7b31e03d9f --- /dev/null +++ b/src/Bridges/Constraint/bridges/indicator_to_milp.jl @@ -0,0 +1,284 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +""" + IndicatorToMILPBridge{T,F,A,S} <: Bridges.Constraint.AbstractBridge + +`IndicatorToMILPBridge` implements the following reformulation: + + * ``x \\in \\textsf{Indicator}(s)`` into a mixed-integer linear program. + +## Source node + +`IndicatorToMILPBridge` supports: + + * `F` in [`MOI.Indicator{A,S}`](@ref) + +where `F` is [`MOI.VectorOfVariables`](@ref) or +[`MOI.VectorAffineFunction{T}`](@ref). + +## Target nodes + +`IndicatorToMILPBridge` creates: + + * [`MOI.VariableIndex`](@ref) in [`MOI.ZeroOne`](@ref) + * [`MOI.ScalarAffineFunction{T}`](@ref) in `S` +""" +mutable struct IndicatorToMILPBridge{ + T, + F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}, + A, + S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}, +} <: AbstractBridge + f::F + s::MOI.Indicator{A,S} + slack::Union{Nothing,MOI.VariableIndex} + constraint::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S} + slack_bounds::Vector{ + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, + } + bounds::NTuple{2,T} + function IndicatorToMILPBridge{T}( + f::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}, + s::MOI.Indicator{A,S}, + ) where {T,A,S} + return new{T,typeof(f),A,S}( + f, + s, + nothing, + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S}(0), + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}[], + (typemin(T), typemax(T)), + ) + end +end + +const IndicatorToMILP{T,OT<:MOI.ModelLike} = + SingleBridgeOptimizer{IndicatorToMILPBridge{T},OT} + +function bridge_constraint( + ::Type{IndicatorToMILPBridge{T,F,A,S}}, + model::MOI.ModelLike, + f::F, + s::MOI.Indicator{A,S}, +) where {T,F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}},A,S} + # !!! info + # Postpone rest of creation until final_touch. + return IndicatorToMILPBridge{T}(f, s) +end + +function MOI.supports_constraint( + ::Type{<:IndicatorToMILPBridge{T}}, + ::Type{<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}}, + ::Type{<:MOI.Indicator{A,S}}, +) where {T,A,S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}} + return true +end + +function MOI.Bridges.added_constrained_variable_types( + ::Type{<:IndicatorToMILPBridge}, +) + return Tuple{Type}[(MOI.Reals,)] +end + +function MOI.Bridges.added_constraint_types( + ::Type{<:IndicatorToMILPBridge{T,F,A,S}}, +) where {T,F,A,S} + return Tuple{Type,Type}[ + (MOI.ScalarAffineFunction{T}, S), + (MOI.ScalarAffineFunction{T}, MOI.LessThan{T}), + ] +end + +function concrete_bridge_type( + ::Type{<:IndicatorToMILPBridge{T}}, + ::Type{F}, + ::Type{MOI.Indicator{A,S}}, +) where { + T, + F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}, + A, + S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}, +} + return IndicatorToMILPBridge{T,F,A,S} +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintFunction, + bridge::IndicatorToMILPBridge, +) + return copy(bridge.f) +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintSet, + bridge::IndicatorToMILPBridge, +) + return bridge.s +end + +function MOI.delete( + model::MOI.ModelLike, + bridge::IndicatorToMILPBridge{T}, +) where {T} + if bridge.slack === nothing + return # final_touch not called, so we can safely skip + end + MOI.delete.(model, bridge.slack_bounds) + MOI.delete(model, bridge.constraint) + MOI.delete(model, bridge.slack::MOI.VariableIndex) + bridge.slack = nothing + empty(bridge.slack_bounds) + bridge.bounds = (typemin(T), typemax(T)) + return +end + +MOI.get(bridge::IndicatorToMILPBridge, ::MOI.NumberOfVariables)::Int64 = 1 + +function MOI.get(bridge::IndicatorToMILPBridge, ::MOI.ListOfVariableIndices) + return [bridge.slack::MOI.VariableIndex] +end + +function MOI.get( + bridge::IndicatorToMILPBridge{T,F,A,S}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},S}, +)::Int64 where {T,F,A,S<:Union{MOI.GreaterThan,MOI.EqualTo}} + return 1 +end + +function MOI.get( + bridge::IndicatorToMILPBridge{T,F,A,S}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},S}, +) where {T,F,A,S} + return [bridge.constraint] +end + +function MOI.get( + bridge::IndicatorToMILPBridge{T,F,A,<:Union{MOI.GreaterThan,MOI.EqualTo}}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, +)::Int64 where {T,F,A} + return length(bridge.slack_bounds) +end + +function MOI.get( + bridge::IndicatorToMILPBridge{T,F,A,<:Union{MOI.GreaterThan,MOI.EqualTo}}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, +) where {T,F,A} + return copy(bridge.slack_bounds) +end + +function MOI.get( + bridge::IndicatorToMILPBridge{T,F,A,MOI.LessThan{T}}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, +)::Int64 where {T,F,A} + return 1 + length(bridge.slack_bounds) +end + +function MOI.get( + bridge::IndicatorToMILPBridge{T,F,A,MOI.LessThan{T}}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, +) where {T,F,A} + return vcat(bridge.constraint, bridge.slack_bounds) +end + +MOI.Bridges.needs_final_touch(::IndicatorToMILPBridge) = true + +function MOI.Bridges.final_touch( + bridge::IndicatorToMILPBridge{T,F}, + model::MOI.ModelLike, +) where {T,F} + bounds = Dict{MOI.VariableIndex,NTuple{2,T}}() + scalars = collect(MOI.Utilities.eachscalar(bridge.f)) + fi = scalars[2] + ret = MOI.Utilities.get_bounds(model, bounds, fi) + if ret === nothing + error( + "Unable to use IndicatorToMILPBridge because element 2 " * + "in the function has a non-finite domain: $fi", + ) + end + if bridge.slack === nothing + # This is the first time calling final_touch + bridge.bounds = ret + elseif bridge.bounds == ret + # We've called final_touch before, and the bounds match. No need to + # reformulate a second time. + return + elseif bridge.bounds != ret + # There is a stored bound, and the current bounds do not match. This + # means the model has been modified since the previous call to + # final_touch. We need to delete the bridge and start again. + MOI.delete(model, bridge) + MOI.Bridges.final_touch(bridge, model) + return + end + bridge.slack = MOI.add_variable(model) + bridge.constraint = MOI.Utilities.normalize_and_add_constraint( + model, + MOI.Utilities.operate(+, T, fi, bridge.slack), + bridge.s.set; + allow_modify_function = true, + ) + _bound_slack(model, bridge, scalars[1], bridge.s) + return +end + +function _to_rhs(A::MOI.ActivationCondition, ::Type{T}, z) where {T} + if A == MOI.ACTIVATE_ON_ZERO + return z + end + return MOI.Utilities.operate(-, T, one(T), z) +end + +function _bound_slack( + model::MOI.ModelLike, + bridge::IndicatorToMILPBridge{T}, + z, + set::MOI.Indicator{A,MOI.GreaterThan{T}}, +) where {T,A} + # {f(x) + y >= b} => {y <= (b - ⌊f(x)⌋)₊ * rhs} + M = max(zero(T), set.set.lower - bridge.bounds[1]) + c = MOI.Utilities.normalize_and_add_constraint( + model, + MOI.Utilities.operate(-, T, bridge.slack, M * _to_rhs(A, T, z)), + MOI.LessThan(zero(T)); + allow_modify_function = true, + ) + push!(bridge.slack_bounds, c) + return +end + +function _bound_slack( + model::MOI.ModelLike, + bridge::IndicatorToMILPBridge{T}, + z, + set::MOI.Indicator{A,MOI.LessThan{T}}, +) where {T,A} + # {f(x) + y <= b} => {y >= (b - ⌈f(x)⌉)₋ * rhs} + M = min(zero(T), set.set.upper - bridge.bounds[2]) + c = MOI.Utilities.normalize_and_add_constraint( + model, + MOI.Utilities.operate!(-, T, M * _to_rhs(A, T, z), bridge.slack), + MOI.LessThan(zero(T)); + allow_modify_function = true, + ) + push!(bridge.slack_bounds, c) + return +end + +function _bound_slack( + model::MOI.ModelLike, + bridge::IndicatorToMILPBridge{T}, + z, + set::MOI.Indicator{A,MOI.EqualTo{T}}, +) where {T,A} + b = set.set.value + _bound_slack(model, bridge, z, MOI.Indicator{A}(MOI.LessThan(b))) + _bound_slack(model, bridge, z, MOI.Indicator{A}(MOI.GreaterThan(b))) + return +end diff --git a/src/Bridges/Constraint/bridges/sos1_to_milp.jl b/src/Bridges/Constraint/bridges/sos1_to_milp.jl new file mode 100644 index 0000000000..a704e86755 --- /dev/null +++ b/src/Bridges/Constraint/bridges/sos1_to_milp.jl @@ -0,0 +1,254 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +""" + SOS1ToMILPBridge{T,F} <: Bridges.Constraint.AbstractBridge + +`SOS1ToMILPBridge` implements the following reformulation: + + * ``x \\in \\textsf{SOS1}(d)`` into a mixed-integer linear program. + +## Source node + +`SOS1ToMILPBridge` supports: + + * `F` in [`MOI.SOS1`](@ref) + +where `F` is [`MOI.VectorOfVariables`](@ref) or +[`MOI.VectorAffineFunction{T}`](@ref). + +## Target nodes + +`SOS1ToMILPBridge` creates: + + * [`MOI.VariableIndex`](@ref) in [`MOI.ZeroOne`](@ref) + * [`MOI.ScalarAffineFunction{T}`](@ref) in [`MOI.EqualTo{T}`](@ref) + * [`MOI.ScalarAffineFunction{T}`](@ref) in [`MOI.LessThan{T}`](@ref) +""" +mutable struct SOS1ToMILPBridge{ + T, + F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}, +} <: AbstractBridge + f::F + s::MOI.SOS1{T} + variables::Vector{MOI.VariableIndex} + # ∑_i z_i == 1 + equal_to::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}} + # x_i - u_i z_i <= 0 ∀i + # l_i z_i - x_i <= 0 ∀i + less_than::Vector{ + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, + } + bounds::Vector{NTuple{2,T}} + function SOS1ToMILPBridge{T}( + f::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}, + s::MOI.SOS1{T}, + ) where {T} + return new{T,typeof(f)}( + f, + s, + MOI.VariableIndex[], + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}(0), + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}[], + NTuple{2,T}[], + ) + end +end + +const SOS1ToMILP{T,OT<:MOI.ModelLike} = + SingleBridgeOptimizer{SOS1ToMILPBridge{T},OT} + +function bridge_constraint( + ::Type{SOS1ToMILPBridge{T,F}}, + model::MOI.ModelLike, + f::F, + s::MOI.SOS1, +) where {T,F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}} + # !!! info + # Postpone rest of creation until final_touch. + return SOS1ToMILPBridge{T}(f, s) +end + +function MOI.supports_constraint( + ::Type{<:SOS1ToMILPBridge{T}}, + ::Type{<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}}, + ::Type{MOI.SOS1{T}}, +) where {T} + return true +end + +function MOI.Bridges.added_constrained_variable_types( + ::Type{<:SOS1ToMILPBridge}, +) + return Tuple{Type}[(MOI.ZeroOne,)] +end + +function MOI.Bridges.added_constraint_types( + ::Type{<:SOS1ToMILPBridge{T}}, +) where {T} + return Tuple{Type,Type}[ + (MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}), + (MOI.ScalarAffineFunction{T}, MOI.LessThan{T}), + ] +end + +function concrete_bridge_type( + ::Type{<:SOS1ToMILPBridge{T}}, + ::Type{F}, + ::Type{MOI.SOS1{T}}, +) where {T,F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}} + return SOS1ToMILPBridge{T,F} +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintFunction, + bridge::SOS1ToMILPBridge, +) + return copy(bridge.f) +end + +function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet, bridge::SOS1ToMILPBridge) + return bridge.s +end + +function MOI.delete(model::MOI.ModelLike, bridge::SOS1ToMILPBridge) + if isempty(bridge.variables) + return + end + MOI.delete(model, bridge.equal_to) + for ci in bridge.less_than + MOI.delete(model, ci) + end + empty!(bridge.less_than) + for x in bridge.variables + MOI.delete(model, x) + end + empty!(bridge.variables) + empty!(bridge.bounds) + return +end + +function MOI.get(bridge::SOS1ToMILPBridge, ::MOI.NumberOfVariables)::Int64 + return length(bridge.variables) +end + +function MOI.get(bridge::SOS1ToMILPBridge, ::MOI.ListOfVariableIndices) + return copy(bridge.variables) +end + +function MOI.get( + bridge::SOS1ToMILPBridge, + ::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.ZeroOne}, +)::Int64 + return length(bridge.variables) +end + +function MOI.get( + bridge::SOS1ToMILPBridge, + ::MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}, +) + return MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}[ + MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}(x.value) for + x in bridge.variables + ] +end + +function MOI.get( + bridge::SOS1ToMILPBridge{T}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, +)::Int64 where {T} + return 1 +end + +function MOI.get( + bridge::SOS1ToMILPBridge{T}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, +) where {T} + return [bridge.equal_to] +end + +function MOI.get( + bridge::SOS1ToMILPBridge{T}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, +)::Int64 where {T} + return length(bridge.less_than) +end + +function MOI.get( + bridge::SOS1ToMILPBridge{T}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, +) where {T} + return copy(bridge.less_than) +end + +MOI.Bridges.needs_final_touch(::SOS1ToMILPBridge) = true + +function MOI.Bridges.final_touch( + bridge::SOS1ToMILPBridge{T,F}, + model::MOI.ModelLike, +) where {T,F} + bounds = Dict{MOI.VariableIndex,NTuple{2,T}}() + scalars = collect(MOI.Utilities.eachscalar(bridge.f)) + new_bounds = false + for (i, x) in enumerate(scalars) + ret = MOI.Utilities.get_bounds(model, bounds, x) + if ret === nothing + error( + "Unable to use SOS1ToMILPBridge because element $i " * + "in the function has a non-finite domain: $x", + ) + end + if length(bridge.bounds) < i + # This is the first time calling final_touch + push!(bridge.bounds, ret) + new_bounds = true + elseif bridge.bounds[i] == ret + # We've called final_touch before, and the bounds match. No need to + # reformulate a second time. + continue + elseif bridge.bounds[i] != ret + # There is a stored bound, and the current bounds do not match. This + # means the model has been modified since the previous call to + # final_touch. We need to delete the bridge and start again. + MOI.delete(model, bridge) + MOI.Bridges.final_touch(bridge, model) + return + end + end + if new_bounds === false + return # Already called + end + terms = MOI.ScalarAffineTerm{T}[] + for i in 1:MOI.output_dimension(bridge.f) + z, _ = MOI.add_constrained_variable(model, MOI.ZeroOne()) + push!(bridge.variables, z) + push!(terms, MOI.ScalarAffineTerm(one(T), z)) + end + g = MOI.ScalarAffineFunction(terms, zero(T)) + bridge.equal_to = MOI.add_constraint(model, g, MOI.EqualTo(one(T))) + for (fi, (l, u), z) in zip(scalars, bridge.bounds, bridge.variables) + push!( + bridge.less_than, + MOI.Utilities.normalize_and_add_constraint( + model, + MOI.Utilities.operate!(-, T, l * z, fi), + MOI.LessThan(zero(T)); + allow_modify_function = true, + ), + ) + push!( + bridge.less_than, + MOI.Utilities.normalize_and_add_constraint( + model, + MOI.Utilities.operate(-, T, fi, u * z), + MOI.LessThan(zero(T)); + allow_modify_function = true, + ), + ) + end + return +end diff --git a/src/Bridges/Constraint/bridges/sos2_to_milp.jl b/src/Bridges/Constraint/bridges/sos2_to_milp.jl new file mode 100644 index 0000000000..2e25b961fa --- /dev/null +++ b/src/Bridges/Constraint/bridges/sos2_to_milp.jl @@ -0,0 +1,262 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +""" + SOS2ToMILPBridge{T,F} <: Bridges.Constraint.AbstractBridge + +`SOS2ToMILPBridge` implements the following reformulation: + + * ``x \\in \\textsf{SOS2}(d)`` into a mixed-integer linear program. + +## Source node + +`SOS2ToMILPBridge` supports: + + * `F` in [`MOI.SOS2`](@ref) + +where `F` is [`MOI.VectorOfVariables`](@ref) or +[`MOI.VectorAffineFunction{T}`](@ref). + +## Target nodes + +`SOS2ToMILPBridge` creates: + + * [`MOI.VariableIndex`](@ref) in [`MOI.ZeroOne`](@ref) + * [`MOI.ScalarAffineFunction{T}`](@ref) in [`MOI.EqualTo{T}`](@ref) + * [`MOI.ScalarAffineFunction{T}`](@ref) in [`MOI.LessThan{T}`](@ref) +""" +mutable struct SOS2ToMILPBridge{ + T, + F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}, +} <: AbstractBridge + f::F + s::MOI.SOS2{T} + variables::Vector{MOI.VariableIndex} + # ∑_i z_i == 1 + equal_to::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}} + # x_i - u_i (z_i + z_j) <= 0 ∀i + # l_i (z_i + z_j) - x_i <= 0 ∀i + less_than::Vector{ + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, + } + bounds::Vector{NTuple{2,T}} + function SOS2ToMILPBridge{T}( + f::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}, + s::MOI.SOS2{T}, + ) where {T} + return new{T,typeof(f)}( + f, + s, + MOI.VariableIndex[], + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}(0), + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}[], + NTuple{2,T}[], + ) + end +end + +const SOS2ToMILP{T,OT<:MOI.ModelLike} = + SingleBridgeOptimizer{SOS2ToMILPBridge{T},OT} + +function bridge_constraint( + ::Type{SOS2ToMILPBridge{T,F}}, + model::MOI.ModelLike, + f::F, + s::MOI.SOS2, +) where {T,F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}} + # !!! info + # Postpone rest of creation until final_touch. + return SOS2ToMILPBridge{T}(f, s) +end + +function MOI.supports_constraint( + ::Type{<:SOS2ToMILPBridge{T}}, + ::Type{<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}}, + ::Type{MOI.SOS2{T}}, +) where {T} + return true +end + +function MOI.Bridges.added_constrained_variable_types( + ::Type{<:SOS2ToMILPBridge}, +) + return Tuple{Type}[(MOI.ZeroOne,)] +end + +function MOI.Bridges.added_constraint_types( + ::Type{<:SOS2ToMILPBridge{T}}, +) where {T} + return Tuple{Type,Type}[ + (MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}), + (MOI.ScalarAffineFunction{T}, MOI.LessThan{T}), + ] +end + +function concrete_bridge_type( + ::Type{<:SOS2ToMILPBridge{T}}, + ::Type{F}, + ::Type{MOI.SOS2{T}}, +) where {T,F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}} + return SOS2ToMILPBridge{T,F} +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintFunction, + bridge::SOS2ToMILPBridge, +) + return copy(bridge.f) +end + +function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet, bridge::SOS2ToMILPBridge) + return bridge.s +end + +function MOI.delete(model::MOI.ModelLike, bridge::SOS2ToMILPBridge) + if isempty(bridge.variables) + return + end + MOI.delete(model, bridge.equal_to) + for ci in bridge.less_than + MOI.delete(model, ci) + end + empty!(bridge.less_than) + for x in bridge.variables + MOI.delete(model, x) + end + empty!(bridge.variables) + empty!(bridge.bounds) + return +end + +function MOI.get(bridge::SOS2ToMILPBridge, ::MOI.NumberOfVariables)::Int64 + return length(bridge.variables) +end + +function MOI.get(bridge::SOS2ToMILPBridge, ::MOI.ListOfVariableIndices) + return copy(bridge.variables) +end + +function MOI.get( + bridge::SOS2ToMILPBridge, + ::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.ZeroOne}, +)::Int64 + return length(bridge.variables) +end + +function MOI.get( + bridge::SOS2ToMILPBridge, + ::MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}, +) + return MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}[ + MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}(x.value) for + x in bridge.variables + ] +end + +function MOI.get( + bridge::SOS2ToMILPBridge{T}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, +)::Int64 where {T} + return 1 +end + +function MOI.get( + bridge::SOS2ToMILPBridge{T}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, +) where {T} + return [bridge.equal_to] +end + +function MOI.get( + bridge::SOS2ToMILPBridge{T}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, +)::Int64 where {T} + return length(bridge.less_than) +end + +function MOI.get( + bridge::SOS2ToMILPBridge{T}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, +) where {T} + return copy(bridge.less_than) +end + +MOI.Bridges.needs_final_touch(::SOS2ToMILPBridge) = true + +function MOI.Bridges.final_touch( + bridge::SOS2ToMILPBridge{T,F}, + model::MOI.ModelLike, +) where {T,F} + bounds = Dict{MOI.VariableIndex,NTuple{2,T}}() + scalars = collect(MOI.Utilities.eachscalar(bridge.f)) + new_bounds = false + for (i, x) in enumerate(scalars) + ret = MOI.Utilities.get_bounds(model, bounds, x) + if ret === nothing + error( + "Unable to use SOS2ToMILPBridge because element $i " * + "in the function has a non-finite domain: $x", + ) + end + if length(bridge.bounds) < i + # This is the first time calling final_touch + push!(bridge.bounds, ret) + new_bounds = true + elseif bridge.bounds[i] == ret + # We've called final_touch before, and the bounds match. No need to + # reformulate a second time. + continue + elseif bridge.bounds[i] != ret + # There is a stored bound, and the current bounds do not match. This + # means the model has been modified since the previous call to + # final_touch. We need to delete the bridge and start again. + MOI.delete(model, bridge) + MOI.Bridges.final_touch(bridge, model) + return + end + end + if new_bounds === false + return # Already called + end + terms = MOI.ScalarAffineTerm{T}[] + for i in 2:MOI.output_dimension(bridge.f) + z, _ = MOI.add_constrained_variable(model, MOI.ZeroOne()) + push!(bridge.variables, z) + push!(terms, MOI.ScalarAffineTerm(one(T), z)) + end + g = MOI.ScalarAffineFunction(terms, zero(T)) + bridge.equal_to = MOI.add_constraint(model, g, MOI.EqualTo(one(T))) + for (i, pi) in enumerate(sortperm(bridge.s.weights)) + fi, (l, u) = scalars[pi], bridge.bounds[pi] + z = if i == 1 + bridge.variables[i] + elseif i == length(bridge.s.weights) + bridge.variables[i-1] + else + MOI.Utilities.operate(+, T, bridge.variables[i], bridge.variables[i-1]) + end + push!( + bridge.less_than, + MOI.Utilities.normalize_and_add_constraint( + model, + MOI.Utilities.operate!(-, T, l * z, fi), + MOI.LessThan(zero(T)); + allow_modify_function = true, + ), + ) + push!( + bridge.less_than, + MOI.Utilities.normalize_and_add_constraint( + model, + MOI.Utilities.operate(-, T, fi, u * z), + MOI.LessThan(zero(T)); + allow_modify_function = true, + ), + ) + end + return +end diff --git a/src/Test/test_linear.jl b/src/Test/test_linear.jl index 89d6cb69a7..1dd66ebc02 100644 --- a/src/Test/test_linear.jl +++ b/src/Test/test_linear.jl @@ -2813,6 +2813,7 @@ function test_linear_SOS1_integration( @requires MOI.supports_constraint(model, MOI.VectorOfVariables, MOI.SOS1{T}) @requires MOI.supports_constraint(model, MOI.VariableIndex, MOI.LessThan{T}) v = MOI.add_variables(model, 3) + MOI.add_constraint.(model, v, MOI.GreaterThan(zero(T))) @test MOI.get(model, MOI.NumberOfVariables()) == 3 vc1 = MOI.add_constraint(model, v[1], MOI.LessThan(T(1))) @test vc1.value == v[1].value @@ -3236,6 +3237,8 @@ function test_linear_Indicator_integration( x2 = MOI.add_variable(model) z1 = MOI.add_variable(model) z2 = MOI.add_variable(model) + MOI.add_constraint(model, x1, MOI.Interval(T(0), T(10))) + MOI.add_constraint(model, x2, MOI.Interval(T(0), T(10))) MOI.add_constraint(model, z1, MOI.ZeroOne()) MOI.add_constraint(model, z2, MOI.ZeroOne()) f1 = MOI.VectorAffineFunction( @@ -3334,6 +3337,8 @@ function test_linear_Indicator_ON_ONE( x2 = MOI.add_variable(model) z1 = MOI.add_variable(model) z2 = MOI.add_variable(model) + MOI.add_constraint(model, x1, MOI.Interval(T(0), T(10))) + MOI.add_constraint(model, x2, MOI.Interval(T(0), T(10))) MOI.add_constraint(model, z1, MOI.ZeroOne()) MOI.add_constraint(model, z2, MOI.ZeroOne()) f1 = MOI.VectorAffineFunction( @@ -3449,6 +3454,8 @@ function test_linear_Indicator_ON_ZERO( x2 = MOI.add_variable(model) z1 = MOI.add_variable(model) z2 = MOI.add_variable(model) + MOI.add_constraint(model, x1, MOI.Interval(T(0), T(10))) + MOI.add_constraint(model, x2, MOI.Interval(T(0), T(10))) vc1 = MOI.add_constraint(model, z1, MOI.ZeroOne()) @test vc1.value == z1.value vc2 = MOI.add_constraint(model, z2, MOI.ZeroOne()) @@ -3567,6 +3574,8 @@ function test_linear_Indicator_constant_term( x2 = MOI.add_variable(model) z1 = MOI.add_variable(model) z2 = MOI.add_variable(model) + MOI.add_constraint(model, x1, MOI.Interval(T(0), T(10))) + MOI.add_constraint(model, x2, MOI.Interval(T(0), T(10))) MOI.add_constraint(model, z1, MOI.ZeroOne()) MOI.add_constraint(model, z2, MOI.ZeroOne()) f1 = MOI.VectorAffineFunction( diff --git a/src/Utilities/variables.jl b/src/Utilities/variables.jl index 626b9a263a..7f0ed67149 100644 --- a/src/Utilities/variables.jl +++ b/src/Utilities/variables.jl @@ -13,45 +13,115 @@ Return a tuple `(lb, ub)` of type `Tuple{T, T}`, where `lb` and `ub` are lower function get_bounds( model::MOI.ModelLike, ::Type{T}, - x::MOI.VariableIndex, -) where {T} - xval = x.value - c_lt = MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{T}}(xval) - c_gt = MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{T}}(xval) - c_int = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{T}}(xval) - c_eq = MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{T}}(xval) - c_sc = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Semicontinuous{T}}(xval) - c_si = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Semiinteger{T}}(xval) - if MOI.is_valid(model, c_int) - # It is assumed that none of the other ConstraintIndexs are valid - int::MOI.Interval{T} = MOI.get(model, MOI.ConstraintSet(), c_int) - return int.lower, int.upper - elseif MOI.is_valid(model, c_eq) - # It is assumed that none of the other ConstraintIndexs are valid - eq::MOI.EqualTo{T} = MOI.get(model, MOI.ConstraintSet(), c_eq) - return eq.value, eq.value - elseif MOI.is_valid(model, c_sc) - # It is assumed that none of the other ConstraintIndexs are valid - sc::MOI.Semicontinuous{T} = MOI.get(model, MOI.ConstraintSet(), c_sc) - return min(zero(T), sc.lower), max(zero(T), sc.upper) - elseif MOI.is_valid(model, c_si) - # It is assumed that none of the other ConstraintIndexs are valid + x::F, +) where {T,F<:MOI.VariableIndex} + # MOI.Interval + c_interval = MOI.ConstraintIndex{F,MOI.Interval{T}}(x.value) + if MOI.is_valid(model, c_interval) + s_interval = MOI.get(model, MOI.ConstraintSet(), c_interval) + return s_interval.lower, s_interval.upper + end + # MOI.EqualTo + c_equal_to = MOI.ConstraintIndex{F,MOI.EqualTo{T}}(x.value) + if MOI.is_valid(model, c_equal_to) + s_equal_to = MOI.get(model, MOI.ConstraintSet(), c_equal_to) + return s_equal_to.value, s_equal_to.value + end + # MOI.Semicontinuous + c_semicontinuous = MOI.ConstraintIndex{F,MOI.Semicontinuous{T}}(x.value) + if MOI.is_valid(model, c_semicontinuous) + s_semicontinuous = MOI.get(model, MOI.ConstraintSet(), c_semicontinuous) + l = min(zero(T), s_semicontinuous.lower) + u = max(zero(T), s_semicontinuous.upper) + return l, u + end + # MOI.Semiinteger + c_si = MOI.ConstraintIndex{F,MOI.Semiinteger{T}}(x.value) + if MOI.is_valid(model, c_si) si::MOI.Semiinteger{T} = MOI.get(model, MOI.ConstraintSet(), c_si) return min(zero(T), si.lower), max(zero(T), si.upper) - elseif MOI.is_valid(model, c_lt) - lt::MOI.LessThan{T} = MOI.get(model, MOI.ConstraintSet(), c_lt) - # It is valid to have both LessThan and GreaterThan constraints on the - # same variable. - if MOI.is_valid(model, c_gt) - gt_1::MOI.GreaterThan{T} = MOI.get(model, MOI.ConstraintSet(), c_gt) - return gt_1.lower, lt.upper + end + l, u = typemin(T), typemax(T) + # MOI.LessThan + c_less_than = MOI.ConstraintIndex{F,MOI.LessThan{T}}(x.value) + if MOI.is_valid(model, c_less_than) + s_less_than = MOI.get(model, MOI.ConstraintSet(), c_less_than) + u = min(u, s_less_than.upper) + end + # MOI.GreaterThan + c_greater_than = MOI.ConstraintIndex{F,MOI.GreaterThan{T}}(x.value) + if MOI.is_valid(model, c_greater_than) + s_greater_than = MOI.get(model, MOI.ConstraintSet(), c_greater_than) + l = max(l, s_greater_than.lower) + end + return l, u +end + +""" + get_bounds( + model::MOI.ModelLike, + bounds_cache::Dict{MOI.VariableIndex,NTuple{2,T}}, + f::MOI.ScalarAffineFunction{T}, + ) where {T} --> Union{Nothing,NTuple{2,T}} + +Return the lower and upper bound of `f` as a tuple. If the domain is not bounded, +return `nothing`. +""" +function get_bounds( + model::MOI.ModelLike, + bounds_cache::Dict{MOI.VariableIndex,NTuple{2,T}}, + f::MOI.ScalarAffineFunction{T}, +) where {T} + if !is_canonical(f) + f = canonical(f) + end + lb = ub = f.constant + for term in f.terms + ret = get_bounds(model, bounds_cache, term.variable) + if ret === nothing + return nothing + end + if term.coefficient >= 0 + lb += term.coefficient * ret[1] + ub += term.coefficient * ret[2] else - return typemin(T), lt.upper + lb += term.coefficient * ret[2] + ub += term.coefficient * ret[1] end - elseif MOI.is_valid(model, c_gt) - gt_2::MOI.GreaterThan{T} = MOI.get(model, MOI.ConstraintSet(), c_gt) - return gt_2.lower, typemax(T) - else - return typemin(T), typemax(T) end + return lb, ub +end + +""" + get_bounds( + model::MOI.ModelLike, + bounds_cache::Dict{MOI.VariableIndex,NTuple{2,T}}, + x::MOI.VariableIndex, + ) where {T} --> Union{Nothing,NTuple{2,T}} + +Return the lower and upper bound of `x` as a tuple. If the domain is not bounded, +return `nothing`. + +Similar to `get_bounds(::MOI.ModelLike, ::Type{T}, ::MOI.VariableIndex)`, except +that the second argument is a cache which maps variables to their bounds and +avoids repeated lookups. +""" +function get_bounds( + model::MOI.ModelLike, + bounds_cache::Dict{MOI.VariableIndex,NTuple{2,T}}, + x::MOI.VariableIndex, +) where {T} + if haskey(bounds_cache, x) + return bounds_cache[x] + end + l, u = get_bounds(model, T, x) + ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}(x.value) + if MOI.is_valid(model, ci) + l, u = max(l, zero(T)), min(u, one(T)) + end + if l == typemin(T) || u == typemax(T) + return nothing + end + bounds_cache[x] = (l, u) + return (l, u) end diff --git a/test/Bridges/Constraint/indicator_to_milp.jl b/test/Bridges/Constraint/indicator_to_milp.jl new file mode 100644 index 0000000000..3b2093e42d --- /dev/null +++ b/test/Bridges/Constraint/indicator_to_milp.jl @@ -0,0 +1,293 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +module TestConstraintIndicatorToMILP + +using Test + +import MathOptInterface as MOI + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +function test_runtests_VectorOfVariables_ON_ONE() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IndicatorToMILPBridge, + """ + variables: x, y + [x, y] in Indicator{ACTIVATE_ON_ONE}(LessThan(2.0)) + x in ZeroOne() + y in Interval(1.0, 3.0) + """, + """ + variables: x, y, a + x in ZeroOne() + y in Interval(1.0, 3.0) + 1.0 * y + 1.0 * a <= 2.0 + 1.0 * x + -1.0 * a <= 1.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IndicatorToMILPBridge, + """ + variables: x, y + [x, y] in Indicator{ACTIVATE_ON_ONE}(LessThan(4.0)) + x in ZeroOne() + y in Interval(1.0, 3.0) + """, + """ + variables: x, y, a + x in ZeroOne() + y in Interval(1.0, 3.0) + 1.0 * y + 1.0 * a <= 4.0 + -1.0 * a <= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IndicatorToMILPBridge, + """ + variables: x, y + [x, y] in Indicator{ACTIVATE_ON_ONE}(GreaterThan(1.5)) + x in ZeroOne() + y in Interval(1.0, 3.0) + """, + """ + variables: x, y, a + x in ZeroOne() + y in Interval(1.0, 3.0) + 1.0 * y + 1.0 * a >= 1.5 + 0.5 * x + 1.0 * a <= 0.5 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IndicatorToMILPBridge, + """ + variables: x, y + [x, y] in Indicator{ACTIVATE_ON_ONE}(EqualTo(1.25)) + x in ZeroOne() + y in Interval(1.0, 3.0) + """, + """ + variables: x, y, a + x in ZeroOne() + y in Interval(1.0, 3.0) + 1.0 * y + 1.0 * a == 1.25 + 0.25 * x + 1.0 * a <= 0.25 + 1.75 * x + -1.0 * a <= 1.75 + """, + ) + return +end + +function test_runtests_VectorOfVariables_ON_ZERO() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IndicatorToMILPBridge, + """ + variables: x, y + [x, y] in Indicator{ACTIVATE_ON_ZERO}(LessThan(2.0)) + x in ZeroOne() + y in Interval(1.0, 3.0) + """, + """ + variables: x, y, a + x in ZeroOne() + y in Interval(1.0, 3.0) + 1.0 * y + 1.0 * a <= 2.0 + -1.0 * x + -1.0 * a <= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IndicatorToMILPBridge, + """ + variables: x, y + [x, y] in Indicator{ACTIVATE_ON_ZERO}(LessThan(4.0)) + x in ZeroOne() + y in Interval(1.0, 3.0) + """, + """ + variables: x, y, a + x in ZeroOne() + y in Interval(1.0, 3.0) + 1.0 * y + 1.0 * a <= 4.0 + -1.0 * a <= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IndicatorToMILPBridge, + """ + variables: x, y + [x, y] in Indicator{ACTIVATE_ON_ZERO}(GreaterThan(1.5)) + x in ZeroOne() + y in Interval(1.0, 3.0) + """, + """ + variables: x, y, a + x in ZeroOne() + y in Interval(1.0, 3.0) + 1.0 * y + 1.0 * a >= 1.5 + 1.0 * a + -0.5 * x <= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IndicatorToMILPBridge, + """ + variables: x, y + [x, y] in Indicator{ACTIVATE_ON_ZERO}(EqualTo(1.25)) + x in ZeroOne() + y in Interval(1.0, 3.0) + """, + """ + variables: x, y, a + x in ZeroOne() + y in Interval(1.0, 3.0) + 1.0 * y + 1.0 * a == 1.25 + -0.25 * x + 1.0 * a <= 0.0 + -1.75 * x + -1.0 * a <= 0.0 + """, + ) + return +end + +function test_runtests_VectorAffineFunction_ON_ZERO() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IndicatorToMILPBridge, + """ + variables: x, y + [x, 2.0 * y + 1.0] in Indicator{ACTIVATE_ON_ZERO}(LessThan(2.0)) + x in ZeroOne() + y in Interval(1.0, 3.0) + """, + """ + variables: x, y, a + x in ZeroOne() + y in Interval(1.0, 3.0) + 2.0 * y + 1.0 * a <= 1.0 + -5.0 * x + -1.0 * a <= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IndicatorToMILPBridge, + """ + variables: x, y + [x, 2.0 * y + 1.0] in Indicator{ACTIVATE_ON_ZERO}(LessThan(4.0)) + x in ZeroOne() + y in Interval(1.0, 3.0) + """, + """ + variables: x, y, a + x in ZeroOne() + y in Interval(1.0, 3.0) + 2.0 * y + 1.0 * a <= 3.0 + -3.0 * x + -1.0 * a <= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IndicatorToMILPBridge, + """ + variables: x, y + [x, 2.0 * y + 1.0] in Indicator{ACTIVATE_ON_ZERO}(GreaterThan(1.5)) + x in ZeroOne() + y in Interval(1.0, 3.0) + """, + """ + variables: x, y, a + x in ZeroOne() + y in Interval(1.0, 3.0) + 2.0 * y + 1.0 * a >= 0.5 + 1.0 * a <= 0.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IndicatorToMILPBridge, + """ + variables: x, y + [x, 0.5 * y + 0.25] in Indicator{ACTIVATE_ON_ZERO}(EqualTo(1.25)) + x in ZeroOne() + y in Interval(1.0, 3.0) + """, + """ + variables: x, y, a + x in ZeroOne() + y in Interval(1.0, 3.0) + 0.5 * y + 1.0 * a == 1.0 + -0.5 * x + 1.0 * a <= 0.0 + -0.5 * x + -1.0 * a <= 0.0 + """, + ) + return +end + +function test_resolve_with_modified() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.IndicatorToMILP{Int}(inner) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, x[1], MOI.ZeroOne()) + c = MOI.add_constraint(model, x[2], MOI.Interval(1, 2)) + MOI.add_constraint( + model, + MOI.VectorOfVariables(x), + MOI.Indicator{MOI.ACTIVATE_ON_ZERO}(MOI.GreaterThan(2)), + ) + @test MOI.get(inner, MOI.NumberOfVariables()) == 2 + MOI.Bridges.final_touch(model) + @test MOI.get(inner, MOI.NumberOfVariables()) == 3 + MOI.set(model, MOI.ConstraintSet(), c, MOI.Interval(3, 4)) + MOI.Bridges.final_touch(model) + @test MOI.get(inner, MOI.NumberOfVariables()) == 3 + return +end + +function test_runtests_error_variable() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.IndicatorToMILP{Int}(inner) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, x[1], MOI.ZeroOne()) + MOI.add_constraint( + model, + MOI.VectorOfVariables(x), + MOI.Indicator{MOI.ACTIVATE_ON_ZERO}(MOI.GreaterThan(2)), + ) + @test_throws( + ErrorException( + "Unable to use IndicatorToMILPBridge because element 2 in " * + "the function has a non-finite domain: $(x[2])", + ), + MOI.Bridges.final_touch(model), + ) + return +end + +function test_runtests_error_affine() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.IndicatorToMILP{Int}(inner) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, x[1], MOI.ZeroOne()) + MOI.add_constraint( + model, + MOI.Utilities.operate(vcat, Int, x[1], 2 * x[2]), + MOI.Indicator{MOI.ACTIVATE_ON_ZERO}(MOI.GreaterThan(2)), + ) + @test_throws( + ErrorException( + "Unable to use IndicatorToMILPBridge because element 2 in " * + "the function has a non-finite domain: $(2 * x[2])", + ), + MOI.Bridges.final_touch(model), + ) + return +end + +end # module + +TestConstraintIndicatorToMILP.runtests() diff --git a/test/Bridges/Constraint/sos1_to_milp.jl b/test/Bridges/Constraint/sos1_to_milp.jl new file mode 100644 index 0000000000..031bb37cae --- /dev/null +++ b/test/Bridges/Constraint/sos1_to_milp.jl @@ -0,0 +1,124 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +module TestConstraintSOS1ToMILP + +using Test + +import MathOptInterface as MOI + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +function test_runtests_VectorOfVariables() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.SOS1ToMILPBridge, + """ + variables: x, y + [x, y] in SOS1([1.0, 2.0]) + x in Interval(0.0, 2.0) + y >= -1.0 + y <= 3.0 + """, + """ + variables: x, y, z1, z2 + 1.0 * z1 + 1.0 * z2 == 1.0 + x in Interval(0.0, 2.0) + y >= -1.0 + y <= 3.0 + -1.0 * x <= 0.0 + 1.0 * x + -2.0 * z1 <= 0.0 + -1.0 * y + -1.0 * z2 <= 0.0 + 1.0 * y + -3.0 * z2 <= 0.0 + z1 in ZeroOne() + z2 in ZeroOne() + """, + ) + return +end + +function test_runtests_VectorOfVariables() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.SOS1ToMILPBridge, + """ + variables: x, y + [1.0 * x + 2.0, y] in SOS1([1.0, 2.0]) + x in ZeroOne() + y in ZeroOne() + """, + """ + variables: x, y, z1, z2 + 1.0 * z1 + 1.0 * z2 == 1.0 + -1.0 * x + 2.0 * z1 <= 2.0 + 1.0 * x + -3.0 * z1 <= -2.0 + -1.0 * y <= 0.0 + 1.0 * y + -1.0 * z2 <= 0.0 + x in ZeroOne() + y in ZeroOne() + z1 in ZeroOne() + z2 in ZeroOne() + """, + ) + return +end + +function test_resolve_with_modified() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.SOS1ToMILP{Int}(inner) + x = MOI.add_variables(model, 3) + c = MOI.add_constraint.(model, x, MOI.Interval(0, 2)) + MOI.add_constraint(model, MOI.VectorOfVariables(x), MOI.SOS1([1, 2, 3])) + @test MOI.get(inner, MOI.NumberOfVariables()) == 3 + MOI.Bridges.final_touch(model) + @test MOI.get(inner, MOI.NumberOfVariables()) == 6 + MOI.set(model, MOI.ConstraintSet(), c[3], MOI.Interval(0, 1)) + MOI.Bridges.final_touch(model) + @test MOI.get(inner, MOI.NumberOfVariables()) == 6 + return +end + +function test_runtests_error_variable() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.SOS1ToMILP{Int}(inner) + x = MOI.add_variables(model, 3) + MOI.add_constraint(model, MOI.VectorOfVariables(x), MOI.SOS1([1, 2, 3])) + @test_throws( + ErrorException( + "Unable to use SOS1ToMILPBridge because element 1 in " * + "the function has a non-finite domain: $(x[1])", + ), + MOI.Bridges.final_touch(model), + ) + return +end + +function test_runtests_error_affine() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.SOS1ToMILP{Int}(inner) + x = MOI.add_variables(model, 2) + f = MOI.Utilities.operate(vcat, Int, 2, 1 * x[1], x[2]) + MOI.add_constraint(model, f, MOI.SOS1([1, 2, 3])) + @test_throws( + ErrorException( + "Unable to use SOS1ToMILPBridge because element 2 in " * + "the function has a non-finite domain: $(1 * x[1])", + ), + MOI.Bridges.final_touch(model), + ) + return +end + +end # module + +TestConstraintSOS1ToMILP.runtests() diff --git a/test/Bridges/Constraint/sos2_to_milp.jl b/test/Bridges/Constraint/sos2_to_milp.jl new file mode 100644 index 0000000000..5c6d1b8bc7 --- /dev/null +++ b/test/Bridges/Constraint/sos2_to_milp.jl @@ -0,0 +1,134 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +module TestConstraintSOS2ToMILP + +using Test + +import MathOptInterface as MOI + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +function test_runtests_VectorOfVariables() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.SOS2ToMILPBridge, + """ + variables: x, y, z + [x, y, z] in SOS2([0.5, 1.0, 0.75]) + x in Interval(0.0, 2.0) + y >= -1.0 + y <= 3.0 + z in ZeroOne() + """, + """ + variables: x, y, z, a1, a2 + x in Interval(0.0, 2.0) + y >= -1.0 + y <= 3.0 + z in ZeroOne() + a1 in ZeroOne() + a2 in ZeroOne() + 1.0 * a1 + 1.0 * a2 == 1.0 + -1.0 * x <= 0.0 + 1.0 * x + -2.0 * a1 <= 0.0 + -1.0 * z <= 0.0 + 1.0 * z + -1.0 * a1 + -1.0 * a2 <= 0.0 + -1.0 * y + -1.0 * a2 <= 0.0 + 1.0 * y + -3.0 * a2 <= 0.0 + """, + ) + return +end + +function test_runtests_VectorOfVariables() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.SOS2ToMILPBridge, + """ + variables: x, y, z + [1.0 * x + 1.0, y, 2.0 * z] in SOS2([0.5, 1.0, 0.75]) + x in Interval(0.0, 2.0) + y >= -1.0 + y <= 3.0 + z in ZeroOne() + """, + """ + variables: x, y, z, a1, a2 + x in Interval(0.0, 2.0) + y >= -1.0 + y <= 3.0 + z in ZeroOne() + a1 in ZeroOne() + a2 in ZeroOne() + 1.0 * a1 + 1.0 * a2 == 1.0 + -1.0 * x + 1.0 * a1 <= 1.0 + 1.0 * x + -3.0 * a1 <= -1.0 + -2.0 * z <= 0.0 + 2.0 * z + -2.0 * a1 + -2.0 * a2 <= 0.0 + -1.0 * y + -1.0 * a2 <= 0.0 + 1.0 * y + -3.0 * a2 <= 0.0 + """, + ) + return +end + +function test_resolve_with_modified() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.SOS2ToMILP{Int}(inner) + x = MOI.add_variables(model, 3) + c = MOI.add_constraint.(model, x, MOI.Interval(0, 2)) + MOI.add_constraint(model, MOI.VectorOfVariables(x), MOI.SOS2([1, 2, 3])) + @test MOI.get(inner, MOI.NumberOfVariables()) == 3 + MOI.Bridges.final_touch(model) + @test MOI.get(inner, MOI.NumberOfVariables()) == 5 + MOI.set(model, MOI.ConstraintSet(), c[3], MOI.Interval(0, 1)) + MOI.Bridges.final_touch(model) + @test MOI.get(inner, MOI.NumberOfVariables()) == 5 + return +end + +function test_runtests_error_variable() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.SOS2ToMILP{Int}(inner) + x = MOI.add_variables(model, 3) + MOI.add_constraint(model, MOI.VectorOfVariables(x), MOI.SOS2([1, 2, 3])) + @test_throws( + ErrorException( + "Unable to use SOS2ToMILPBridge because element 1 in " * + "the function has a non-finite domain: $(x[1])", + ), + MOI.Bridges.final_touch(model), + ) + return +end + +function test_runtests_error_affine() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.SOS2ToMILP{Int}(inner) + x = MOI.add_variables(model, 2) + f = MOI.Utilities.operate(vcat, Int, 2, 1 * x[1], x[2]) + MOI.add_constraint(model, f, MOI.SOS2([1, 2, 3])) + @test_throws( + ErrorException( + "Unable to use SOS2ToMILPBridge because element 2 in " * + "the function has a non-finite domain: $(1 * x[1])", + ), + MOI.Bridges.final_touch(model), + ) + return +end + +end # module + +TestConstraintSOS2ToMILP.runtests() diff --git a/test/Utilities/variables.jl b/test/Utilities/variables.jl index 8438f48286..b7b71a2900 100644 --- a/test/Utilities/variables.jl +++ b/test/Utilities/variables.jl @@ -99,6 +99,29 @@ function test_get_bounds_UInt128() @test 5 == @inferred MOIU.get_bounds(model, T, y_v)[2] end +function test_get_bounds_scalar_affine() + model = MOI.Utilities.Model{Float64}() + x, _ = MOI.add_constrained_variable(model, MOI.Interval(1.0, 2.0)) + y = MOI.add_variable(model) + z, _ = MOI.add_constrained_variable(model, MOI.ZeroOne()) + cache = Dict{MOI.VariableIndex,NTuple{2,Float64}}() + @test MOI.Utilities.get_bounds(model, cache, x) == (1.0, 2.0) + @test MOI.Utilities.get_bounds(model, cache, y) == nothing + @test MOI.Utilities.get_bounds(model, cache, z) == (0.0, 1.0) + @test MOI.Utilities.get_bounds(model, cache, 2.0 * x + 1.0) == (3.0, 5.0) + @test MOI.Utilities.get_bounds(model, cache, 2.0 * x + x) == (3.0, 6.0) + @test MOI.Utilities.get_bounds(model, cache, x - 2.0 * x) == (-2.0, -1.0) + @test MOI.Utilities.get_bounds(model, cache, -1.0 * x) == (-2.0, -1.0) + @test MOI.Utilities.get_bounds(model, cache, 1.5 * x + z) == (1.5, 4.0) + @test MOI.Utilities.get_bounds(model, cache, -1.0 * y) == nothing + @test MOI.Utilities.get_bounds(model, cache, 1.0 * x + y) == nothing + MOI.add_constraint(model, y, MOI.GreaterThan(2.0)) + @test MOI.Utilities.get_bounds(model, cache, y) == nothing + MOI.add_constraint(model, y, MOI.LessThan(3.0)) + @test MOI.Utilities.get_bounds(model, cache, y) == (2.0, 3.0) + return +end + end # module TestVariables.runtests()