From b35678ca10effd04f98e384e1b5e4729fe2fcf60 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 29 Jun 2022 14:28:47 +1200 Subject: [PATCH 1/3] [Bridges] add CountGreaterThanToMILPBridge --- .../src/submodules/Bridges/list_of_bridges.md | 1 + src/Bridges/Constraint/Constraint.jl | 2 + .../Constraint/bridges/count_greater_than.jl | 297 ++++++++++++++++++ test/Bridges/Constraint/count_greater_than.jl | 145 +++++++++ 4 files changed, 445 insertions(+) create mode 100644 src/Bridges/Constraint/bridges/count_greater_than.jl create mode 100644 test/Bridges/Constraint/count_greater_than.jl diff --git a/docs/src/submodules/Bridges/list_of_bridges.md b/docs/src/submodules/Bridges/list_of_bridges.md index ba37176b9a..f684fb43ec 100644 --- a/docs/src/submodules/Bridges/list_of_bridges.md +++ b/docs/src/submodules/Bridges/list_of_bridges.md @@ -56,6 +56,7 @@ Bridges.Constraint.BinPackingToMILPBridge Bridges.Constraint.CountAtLeastToCountBelongsBridge Bridges.Constraint.CountBelongsToMILPBridge Bridges.Constraint.CountDistinctToMILPBridge +Bridges.Constraint.CountGreaterThanToMILPBridge Bridges.Constraint.TableToMILPBridge ``` diff --git a/src/Bridges/Constraint/Constraint.jl b/src/Bridges/Constraint/Constraint.jl index 9cff116be6..988a43fb80 100644 --- a/src/Bridges/Constraint/Constraint.jl +++ b/src/Bridges/Constraint/Constraint.jl @@ -23,6 +23,7 @@ include("bridges/bin_packing.jl") include("bridges/count_at_least.jl") include("bridges/count_belongs.jl") include("bridges/count_distinct.jl") +include("bridges/count_greater_than.jl") include("bridges/det.jl") include("bridges/flip_sign.jl") include("bridges/functionize.jl") @@ -101,6 +102,7 @@ function add_all_bridges(bridged_model, ::Type{T}) where {T} MOI.Bridges.add_bridge(bridged_model, CountAtLeastToCountBelongsBridge{T}) MOI.Bridges.add_bridge(bridged_model, CountBelongsToMILPBridge{T}) MOI.Bridges.add_bridge(bridged_model, CountDistinctToMILPBridge{T}) + MOI.Bridges.add_bridge(bridged_model, CountGreaterThanToMILPBridge{T}) MOI.Bridges.add_bridge(bridged_model, TableToMILPBridge{T}) return end diff --git a/src/Bridges/Constraint/bridges/count_greater_than.jl b/src/Bridges/Constraint/bridges/count_greater_than.jl new file mode 100644 index 0000000000..715d662311 --- /dev/null +++ b/src/Bridges/Constraint/bridges/count_greater_than.jl @@ -0,0 +1,297 @@ +# 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. + +""" + CountGreaterThanToMILPBridge{T,F} <: Bridges.Constraint.AbstractBridge + +`CountGreaterThanToMILPBridge` implements the following reformulation: + + * ``(c, y, x) \\in CountGreaterThan()`` into a mixed-integer linear program. + +## Source node + +`CountGreaterThanToMILPBridge` supports: + + * `F` in [`MOI.CountGreaterThan`](@ref) + +## Target nodes + +`CountGreaterThanToMILPBridge` creates: + + * [`MOI.VariableIndex`](@ref) in [`MOI.ZeroOne`](@ref) + * [`MOI.ScalarAffineFunction{T}`](@ref) in [`MOI.EqualTo{T}`](@ref) + * [`MOI.ScalarAffineFunction{T}`](@ref) in [`MOI.GreaterThan{T}`](@ref) +""" +struct CountGreaterThanToMILPBridge{ + T, + F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}, +} <: AbstractBridge + f::F + s::MOI.CountGreaterThan + variables::Vector{MOI.VariableIndex} + greater_than::Vector{ + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.GreaterThan{T}}, + } + equal_to::Vector{ + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, + } +end + +const CountGreaterThanToMILP{T,OT<:MOI.ModelLike} = + SingleBridgeOptimizer{CountGreaterThanToMILPBridge{T},OT} + +function bridge_constraint( + ::Type{CountGreaterThanToMILPBridge{T,F}}, + model::MOI.ModelLike, + f::F, + s::MOI.CountGreaterThan, +) where {T,F} + return CountGreaterThanToMILPBridge{T,F}( + f, + s, + MOI.VariableIndex[], + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.GreaterThan{T}}[], + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}[], + ) +end + +function MOI.supports_constraint( + ::Type{<:CountGreaterThanToMILPBridge{T}}, + ::Type{F}, + ::Type{MOI.CountGreaterThan}, +) where {T,F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}} + return true +end + +function MOI.Bridges.added_constrained_variable_types( + ::Type{<:CountGreaterThanToMILPBridge}, +) + return Tuple{Type}[(MOI.ZeroOne,)] +end + +function MOI.Bridges.added_constraint_types( + ::Type{<:CountGreaterThanToMILPBridge{T}}, +) where {T} + return Tuple{Type,Type}[ + (MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}), + (MOI.ScalarAffineFunction{T}, MOI.GreaterThan{T}), + ] +end + +function concrete_bridge_type( + ::Type{<:CountGreaterThanToMILPBridge{T}}, + ::Type{F}, + ::Type{MOI.CountGreaterThan}, +) where {T,F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}} + return CountGreaterThanToMILPBridge{T,F} +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintFunction, + bridge::CountGreaterThanToMILPBridge, +) + return bridge.f +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintSet, + bridge::CountGreaterThanToMILPBridge, +) + return bridge.s +end + +function MOI.delete(model::MOI.ModelLike, bridge::CountGreaterThanToMILPBridge) + MOI.delete.(model, bridge.greater_than) + empty!(bridge.greater_than) + MOI.delete.(model, bridge.equal_to) + empty!(bridge.equal_to) + MOI.delete.(model, bridge.variables) + empty!(bridge.variables) + return +end + +function MOI.get( + bridge::CountGreaterThanToMILPBridge, + ::MOI.NumberOfVariables, +)::Int64 + return length(bridge.variables) +end + +function MOI.get( + bridge::CountGreaterThanToMILPBridge, + ::MOI.ListOfVariableIndices, +) + return copy(bridge.variables) +end + +function MOI.get( + bridge::CountGreaterThanToMILPBridge, + ::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.ZeroOne}, +)::Int64 + return length(bridge.variables) +end + +function MOI.get( + bridge::CountGreaterThanToMILPBridge, + ::MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}, +) + return [ + MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}.(z.value) for + z in bridge.variables + ] +end + +function MOI.get( + bridge::CountGreaterThanToMILPBridge{T}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, +)::Int64 where {T} + return length(bridge.equal_to) +end + +function MOI.get( + bridge::CountGreaterThanToMILPBridge{T}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, +) where {T} + return copy(bridge.equal_to) +end + +function MOI.get( + bridge::CountGreaterThanToMILPBridge{T}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.GreaterThan{T}}, +)::Int64 where {T} + return length(bridge.greater_than) +end + +function MOI.get( + bridge::CountGreaterThanToMILPBridge{T}, + ::MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{T}, + MOI.GreaterThan{T}, + }, +) where {T} + return copy(bridge.greater_than) +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, + S, + bounds, + x, +) where {T,F} + ret = _get_bounds(bridge, model, bounds, x) + if ret === nothing + error( + "Unable to use $(typeof(bridge)) because an element in the " * + "function has a non-finite domain: $x", + ) + end + unit_f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{T}[], zero(T)) + convex_f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{T}[], zero(T)) + for xi in ret[1]::T:ret[2]::T + new_var, _ = MOI.add_constrained_variable(model, MOI.ZeroOne()) + push!(bridge.variables, new_var) + if !haskey(S, xi) + S[xi] = Tuple{Float64,MOI.VariableIndex}[] + end + push!(S[xi], new_var) + push!(unit_f.terms, MOI.ScalarAffineTerm(T(-xi), new_var)) + push!(convex_f.terms, MOI.ScalarAffineTerm(one(T), new_var)) + end + push!( + bridge.equal_to, + MOI.Utilities.normalize_and_add_constraint( + model, + MOI.Utilities.operate(+, T, x, unit_f), + MOI.EqualTo(zero(T)); + allow_modify_function = true, + ), + ) + push!( + bridge.equal_to, + MOI.add_constraint(model, convex_f, MOI.EqualTo(one(T))), + ) + return +end + +function MOI.Bridges.final_touch( + bridge::CountGreaterThanToMILPBridge{T,F}, + model::MOI.ModelLike, +) where {T,F} + # Clear any existing reformulations! + MOI.delete(model, bridge) + Sx = Dict{T,Vector{MOI.VariableIndex}}() + Sy = Dict{T,Vector{MOI.VariableIndex}}() + scalars = collect(MOI.Utilities.eachscalar(bridge.f)) + bounds = Dict{MOI.VariableIndex,NTuple{2,T}}() + _add_unit_expansion(bridge, model, Sy, bounds, scalars[2]) + for i in 3:length(scalars) + _add_unit_expansion(bridge, model, Sx, bounds, scalars[i]) + end + # We use a sort so that the model order is deterministic. + for s in sort!(collect(keys(Sy))) + if haskey(Sx, s) + M = length(Sx[s]) + terms = [MOI.ScalarAffineTerm(one(T), x) for x in Sx[s]] + push!(terms, MOI.ScalarAffineTerm(T(M), first(Sy[s]))) + f = MOI.Utilities.operate( + -, + T, + scalars[1], + MOI.ScalarAffineFunction(terms, zero(T)), + ) + ci = MOI.Utilities.normalize_and_add_constraint( + model, + f, + MOI.GreaterThan(T(1 - M)); + allow_modify_function = true, + ) + push!(bridge.greater_than, ci) + end + end + return +end diff --git a/test/Bridges/Constraint/count_greater_than.jl b/test/Bridges/Constraint/count_greater_than.jl new file mode 100644 index 0000000000..08f151c882 --- /dev/null +++ b/test/Bridges/Constraint/count_greater_than.jl @@ -0,0 +1,145 @@ +# 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 TestConstraintCountGreaterThan + +using Test + +using MathOptInterface +const MOI = MathOptInterface + +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.CountGreaterThanToMILPBridge, + """ + variables: c, y, a, b + [c, y, a, b] in CountGreaterThan(4) + a in Interval(1.0, 3.0) + b in Interval(1.0, 3.0) + y >= 1.0 + y <= 2.0 + """, + """ + variables: c, y, a, b, y1, y2, a1, a2, a3, b1, b2, b3 + y + -1.0 * y1 + -2.0 * y2 == 0.0 + y1 + y2 == 1.0 + a + -1.0 * a1 + -2.0 * a2 + -3.0 * a3 == 0.0 + a1 + a2 + a3 == 1.0 + b + -1.0 * b1 + -2.0 * b2 + -3.0 * b3 == 0.0 + b1 + b2 + b3 == 1.0 + c + -1.0 * a1 + -1.0 * b1 + -2.0 * y1 >= -1.0 + c + -1.0 * a2 + -1.0 * b2 + -2.0 * y2 >= -1.0 + y1 in ZeroOne() + y2 in ZeroOne() + a1 in ZeroOne() + a2 in ZeroOne() + a3 in ZeroOne() + b1 in ZeroOne() + b2 in ZeroOne() + b3 in ZeroOne() + a in Interval(1.0, 3.0) + b in Interval(1.0, 3.0) + y >= 1.0 + y <= 2.0 + + """, + ) + return +end + +function test_runtests_VectorAffineFunction() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.CountGreaterThanToMILPBridge, + """ + variables: c, y, a, b + [2.0, y, a, b] in CountGreaterThan(4) + a in Interval(1.0, 3.0) + b in Interval(1.0, 3.0) + y >= 1.0 + y <= 2.0 + """, + """ + variables: c, y, a, b, y1, y2, a1, a2, a3, b1, b2, b3 + y + -1.0 * y1 + -2.0 * y2 == 0.0 + y1 + y2 == 1.0 + a + -1.0 * a1 + -2.0 * a2 + -3.0 * a3 == 0.0 + a1 + a2 + a3 == 1.0 + b + -1.0 * b1 + -2.0 * b2 + -3.0 * b3 == 0.0 + b1 + b2 + b3 == 1.0 + -1.0 * a1 + -1.0 * b1 + -2.0 * y1 >= -3.0 + -1.0 * a2 + -1.0 * b2 + -2.0 * y2 >= -3.0 + y1 in ZeroOne() + y2 in ZeroOne() + a1 in ZeroOne() + a2 in ZeroOne() + a3 in ZeroOne() + b1 in ZeroOne() + b2 in ZeroOne() + b3 in ZeroOne() + a in Interval(1.0, 3.0) + b in Interval(1.0, 3.0) + y >= 1.0 + y <= 2.0 + + """, + ) + return +end + +function test_runtests_error_variable() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.CountGreaterThanToMILP{Int}(inner) + x = MOI.add_variables(model, 3) + f = MOI.VectorOfVariables(x) + MOI.add_constraint(model, f, MOI.CountGreaterThan(3)) + BT = MOI.Bridges.Constraint.CountGreaterThanToMILPBridge{ + Int, + MOI.VectorOfVariables, + } + @test_throws( + ErrorException( + "Unable to use $BT because an element 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.CountGreaterThanToMILP{Int}(inner) + x = MOI.add_variables(model, 2) + f = MOI.Utilities.operate(vcat, Int, 2, x[1], 1 * x[1], x[2]) + MOI.add_constraint(model, f, MOI.CountGreaterThan(3)) + BT = MOI.Bridges.Constraint.CountGreaterThanToMILPBridge{ + Int, + MOI.VectorAffineFunction{Int}, + } + @test_throws( + ErrorException( + "Unable to use $BT because an element in " * + "the function has a non-finite domain: $(1 * x[1])", + ), + MOI.Bridges.final_touch(model), + ) + return +end + +end # module + +TestConstraintCountGreaterThan.runtests() From 9c112e31c1016ad11a8b3e2b94e4fb508a598dc7 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 29 Jun 2022 14:36:29 +1200 Subject: [PATCH 2/3] Fix test_cpsat_CountGreaterThan --- src/Test/test_cpsat.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Test/test_cpsat.jl b/src/Test/test_cpsat.jl index c99f66fb50..09364c4b65 100644 --- a/src/Test/test_cpsat.jl +++ b/src/Test/test_cpsat.jl @@ -207,6 +207,8 @@ function test_cpsat_CountGreaterThan( c, _ = MOI.add_constrained_variable(model, MOI.Integer()) y, _ = MOI.add_constrained_variable(model, MOI.Integer()) x = [MOI.add_constrained_variable(model, MOI.Integer())[1] for _ in 1:3] + MOI.add_constraint.(model, x, MOI.Interval(T(0), T(4))) + MOI.add_constraint(model, y, MOI.Interval(T(0), T(4))) MOI.add_constraint( model, MOI.VectorOfVariables([c; y; x]), From 36b34e743c1074f4af25f8dc8f094a45a710aaa4 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 29 Jun 2022 16:01:09 +1200 Subject: [PATCH 3/3] Update count_greater_than.jl --- test/Bridges/Constraint/count_greater_than.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/Bridges/Constraint/count_greater_than.jl b/test/Bridges/Constraint/count_greater_than.jl index 08f151c882..d76a23377e 100644 --- a/test/Bridges/Constraint/count_greater_than.jl +++ b/test/Bridges/Constraint/count_greater_than.jl @@ -65,15 +65,16 @@ function test_runtests_VectorAffineFunction() MOI.Bridges.runtests( MOI.Bridges.Constraint.CountGreaterThanToMILPBridge, """ - variables: c, y, a, b + variables: z, c, y, a, b [2.0, y, a, b] in CountGreaterThan(4) a in Interval(1.0, 3.0) b in Interval(1.0, 3.0) y >= 1.0 y <= 2.0 + z in ZeroOne() """, """ - variables: c, y, a, b, y1, y2, a1, a2, a3, b1, b2, b3 + variables: z, c, y, a, b, y1, y2, a1, a2, a3, b1, b2, b3 y + -1.0 * y1 + -2.0 * y2 == 0.0 y1 + y2 == 1.0 a + -1.0 * a1 + -2.0 * a2 + -3.0 * a3 == 0.0 @@ -94,7 +95,7 @@ function test_runtests_VectorAffineFunction() b in Interval(1.0, 3.0) y >= 1.0 y <= 2.0 - + z in ZeroOne() """, ) return