diff --git a/docs/src/submodules/Bridges/list_of_bridges.md b/docs/src/submodules/Bridges/list_of_bridges.md index 755a29624a..8dcfd5ce00 100644 --- a/docs/src/submodules/Bridges/list_of_bridges.md +++ b/docs/src/submodules/Bridges/list_of_bridges.md @@ -52,6 +52,7 @@ Bridges.Constraint.IndicatorSOS1Bridge Bridges.Constraint.SemiToBinaryBridge Bridges.Constraint.ZeroOneBridge Bridges.Constraint.BinPackingToMILPBridge +Bridges.Constraint.CountDistinctToMILPBridge Bridges.Constraint.TableToMILPBridge ``` diff --git a/docs/src/submodules/Bridges/reference.md b/docs/src/submodules/Bridges/reference.md index 0fa5a081e5..04dbbdc176 100644 --- a/docs/src/submodules/Bridges/reference.md +++ b/docs/src/submodules/Bridges/reference.md @@ -27,6 +27,8 @@ get(::Bridges.AbstractBridge, ::NumberOfVariables) get(::Bridges.AbstractBridge, ::ListOfVariableIndices) get(::Bridges.AbstractBridge, ::NumberOfConstraints) get(::Bridges.AbstractBridge, ::ListOfConstraintIndices) +Bridges.needs_final_touch +Bridges.final_touch ``` ## Constraint bridge API diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index b4f31a9cea..d243140070 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -249,6 +249,7 @@ function runtests(Bridge::Type{<:AbstractBridge}, input::String, output::String) inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) model = _bridged_model(Bridge, inner) MOI.Utilities.loadfromstring!(model, input) + final_touch(model) # Load a non-bridged input model, and check that getters are the same. test = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) MOI.Utilities.loadfromstring!(test, input) diff --git a/src/Bridges/Constraint/Constraint.jl b/src/Bridges/Constraint/Constraint.jl index 98f23eceb2..acccf47809 100644 --- a/src/Bridges/Constraint/Constraint.jl +++ b/src/Bridges/Constraint/Constraint.jl @@ -8,7 +8,7 @@ module Constraint import LinearAlgebra import MathOptInterface -import OrderedCollections: OrderedDict +import OrderedCollections: OrderedDict, OrderedSet import SparseArrays const MOI = MathOptInterface @@ -19,6 +19,7 @@ include("set_map.jl") include("single_bridge_optimizer.jl") include("bridges/bin_packing.jl") +include("bridges/count_distinct.jl") include("bridges/det.jl") include("bridges/flip_sign.jl") include("bridges/functionize.jl") @@ -95,6 +96,7 @@ function add_all_bridges(bridged_model, ::Type{T}) where {T} # TODO(odow): this reformulation assumes the bins are numbered 1..N. We # should fix this to use the variable bounds before adding automatically. # MOI.Bridges.add_bridge(bridged_model, BinPackingToMILPBridge{T}) + MOI.Bridges.add_bridge(bridged_model, CountDistinctToMILPBridge{T}) MOI.Bridges.add_bridge(bridged_model, TableToMILPBridge{T}) return end diff --git a/src/Bridges/Constraint/bridges/count_distinct.jl b/src/Bridges/Constraint/bridges/count_distinct.jl new file mode 100644 index 0000000000..70d4a8a8c0 --- /dev/null +++ b/src/Bridges/Constraint/bridges/count_distinct.jl @@ -0,0 +1,349 @@ +# 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. + +""" + CountDistinctToMILPBridge{T,F} <: Bridges.Constraint.AbstractBridge + +`CountDistinctToMILPBridge` implements the following reformulation: + + * ``(n, x) \\in \\textsf{CountDistinct}(1+d)`` into a mixed-integer linear program. + +## Reformulation + +The reformulation is non-trivial, and it depends on the finite domain of each +variable ``x_i``, which we as define ``S_i = \\{l_i,\\ldots,u_i\\}``. + +First, we introduce new binary variables ``z_{ij}``, which are ``1`` if variable +``x_i`` takes the value ``j`` in the optimal solution and ``0`` otherwise: +```math +\\begin{aligned} +z_{ij} \\in \\{0, 1\\} & \\;\\; \\forall i \\in 1\\ldots d, j \\in S_i \\\\ +x_i - \\sum\\limits_{j\\in S_i} j \\cdot z_{ij} = 0 & \\;\\; \\forall i \\in 1\\ldots d \\\\ +\\sum\\limits_{j\\in S_i} z_{ij} = 1 & \\;\\; \\forall i \\in 1\\ldots d \\\\ +\\end{aligned} +``` + +Then, we introduce new binary variables ``y_j``, which are ``1`` if a variable +takes the value ``j`` in the optimal solution and ``0`` otherwise. +```math +\\begin{aligned} +y_{j} \\in \\{0, 1\\} & \\;\\; \\forall j \\in \\bigcup_{i=1,\\ldots,d} S_i \\\\ +y_j \\le \\sum\\limits_{i \\in 1\\ldots d: j \\in S_i} z_{ij} \\le M y_j & \\;\\; \\forall j \\in \\bigcup_{i=1,\\ldots,d} S_i\\\\ +\\end{aligned} +``` + +Finally, ``n`` is constrained to be the number of ``y_j`` elements that are +non-zero: +```math +n - \\sum\\limits_{j \\in \\bigcup_{i=1,\\ldots,d} S_i} y_{j} = 0 +``` + +## Source node + +`CountDistinctToMILPBridge` supports: + + * `F` in [`MOI.CountDistinct`](@ref) + +where `F` is [`MOI.VectorOfVariables`](@ref) or +[`MOI.VectorAffineFunction{T}`](@ref). + +## Target nodes + +`CountDistinctToMILPBridge` 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 CountDistinctToMILPBridge{ + T, + F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}, +} <: AbstractBridge + f::F + # A mix of z and α, which are added as needed. We need to store the vector + # so we can delete them later. The exact structure of which index maps to + # which variable doesn't matter. + variables::Vector{MOI.VariableIndex} + # ∑_j a_j + -1.0 * n == 0.0 + # x_i - ∑_j z_ij = 0 ∀i + # ∑_j z_ij = 1 ∀i + equal_to::Vector{ + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, + } + # ∑_i z_ij - |I| α_j <= 0 ∀j + # α_j - ∑_i z_ij <= 0 ∀j + less_than::Vector{ + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, + } + function CountDistinctToMILPBridge{T}( + f::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}, + ) where {T} + return new{T,typeof(f)}( + f, + MOI.VariableIndex[], + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}[], + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}[], + ) + end +end + +const CountDistinctToMILP{T,OT<:MOI.ModelLike} = + SingleBridgeOptimizer{CountDistinctToMILPBridge{T},OT} + +function bridge_constraint( + ::Type{CountDistinctToMILPBridge{T,F}}, + model::MOI.ModelLike, + f::F, + s::MOI.CountDistinct, +) where {T,F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}} + # !!! info + # Postpone creation until final_touch. + return CountDistinctToMILPBridge{T}(f) +end + +function MOI.supports_constraint( + ::Type{<:CountDistinctToMILPBridge{T}}, + ::Type{<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}}, + ::Type{MOI.CountDistinct}, +) where {T} + return true +end + +function MOI.Bridges.added_constrained_variable_types( + ::Type{<:CountDistinctToMILPBridge}, +) + return Tuple{Type}[(MOI.ZeroOne,)] +end + +function MOI.Bridges.added_constraint_types( + ::Type{<:CountDistinctToMILPBridge{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{<:CountDistinctToMILPBridge{T}}, + ::Type{F}, + ::Type{MOI.CountDistinct}, +) where {T,F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}} + return CountDistinctToMILPBridge{T,F} +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintFunction, + bridge::CountDistinctToMILPBridge, +) + return bridge.f +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintSet, + bridge::CountDistinctToMILPBridge, +) + return MOI.CountDistinct(MOI.output_dimension(bridge.f)) +end + +function MOI.delete(model::MOI.ModelLike, bridge::CountDistinctToMILPBridge) + for ci in bridge.equal_to + MOI.delete(model, ci) + end + empty!(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) + return +end + +function MOI.get( + bridge::CountDistinctToMILPBridge, + ::MOI.NumberOfVariables, +)::Int64 + return length(bridge.variables) +end + +function MOI.get(bridge::CountDistinctToMILPBridge, ::MOI.ListOfVariableIndices) + return copy(bridge.variables) +end + +function MOI.get( + bridge::CountDistinctToMILPBridge, + ::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.ZeroOne}, +)::Int64 + return length(bridge.variables) +end + +function MOI.get( + bridge::CountDistinctToMILPBridge, + ::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::CountDistinctToMILPBridge{T}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, +)::Int64 where {T} + return length(bridge.equal_to) +end + +function MOI.get( + bridge::CountDistinctToMILPBridge{T}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, +) where {T} + return copy(bridge.equal_to) +end + +function MOI.get( + bridge::CountDistinctToMILPBridge{T}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, +)::Int64 where {T} + return length(bridge.less_than) +end + +function MOI.get( + bridge::CountDistinctToMILPBridge{T}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}, +) where {T} + return copy(bridge.less_than) +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, +) where {T,F} + # Clear any existing reformulations! + MOI.delete(model, bridge) + S = Dict{T,Vector{MOI.VariableIndex}}() + scalars = collect(MOI.Utilities.eachscalar(bridge.f)) + bounds = Dict{MOI.VariableIndex,NTuple{2,T}}() + for i in 2:length(scalars) + x = scalars[i] + ret = _get_bounds(bridge, model, bounds, x) + if ret === nothing + error( + "Unable to use CountDistinctToMILPBridge because element $i " * + "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] = 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))), + ) + end + count_terms = MOI.ScalarAffineTerm{T}[] + # We use a sort so that the model order is deterministic. + for s in sort!(collect(keys(S))) + terms = S[s] + new_var, _ = MOI.add_constrained_variable(model, MOI.ZeroOne()) + push!(bridge.variables, new_var) + push!(count_terms, MOI.ScalarAffineTerm(one(T), new_var)) + big_M_terms = [MOI.ScalarAffineTerm(T(1), z) for z in terms] + push!(big_M_terms, MOI.ScalarAffineTerm(T(-length(terms)), new_var)) + push!( + bridge.less_than, + MOI.add_constraint( + model, + MOI.ScalarAffineFunction(big_M_terms, zero(T)), + MOI.LessThan(zero(T)), + ), + ) + big_M_terms_upper = [MOI.ScalarAffineTerm(T(-1), z) for z in terms] + push!(big_M_terms_upper, MOI.ScalarAffineTerm(T(1), new_var)) + push!( + bridge.less_than, + MOI.add_constraint( + model, + MOI.ScalarAffineFunction(big_M_terms_upper, zero(T)), + MOI.LessThan(zero(T)), + ), + ) + end + count_f = MOI.ScalarAffineFunction(count_terms, zero(T)) + MOI.Utilities.operate!(-, T, count_f, scalars[1]) + push!( + bridge.equal_to, + MOI.Utilities.normalize_and_add_constraint( + model, + count_f, + MOI.EqualTo(zero(T)); + allow_modify_function = true, + ), + ) + return +end diff --git a/src/Bridges/Constraint/map.jl b/src/Bridges/Constraint/map.jl index 7f945b1b54..24695f1c1c 100644 --- a/src/Bridges/Constraint/map.jl +++ b/src/Bridges/Constraint/map.jl @@ -20,12 +20,14 @@ struct Map <: AbstractDict{MOI.ConstraintIndex,AbstractBridge} # of creation so we need `OrderedDict` and not `Dict`. # For `VariableIndex` constraints: (variable, set type) -> bridge single_variable_constraints::OrderedDict{Tuple{Int64,Type},AbstractBridge} + needs_final_touch::OrderedDict{Type,OrderedSet} function Map() return new( Union{Nothing,AbstractBridge}[], Tuple{Type,Type}[], OrderedDict{Tuple{Int64,Type},AbstractBridge}(), + OrderedDict{Type,OrderedSet}(), ) end end @@ -48,6 +50,7 @@ function Base.empty!(map::Map) empty!(map.bridges) empty!(map.constraint_types) empty!(map.single_variable_constraints) + empty!(map.needs_final_touch) return map end @@ -83,6 +86,7 @@ function Base.getindex( end function Base.delete!(map::Map, ci::MOI.ConstraintIndex) + _unregister_for_final_touch(map, map.bridges[_index(ci)]) map.bridges[_index(ci)] = nothing return map end @@ -91,6 +95,8 @@ function Base.delete!( map::Map, ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, ) where {S} + bridge = map.single_variable_constraints[(ci.value, S)] + _unregister_for_final_touch(map, bridge) delete!(map.single_variable_constraints, (ci.value, S)) return map end @@ -278,6 +284,7 @@ function add_key_for_bridge( ::F, ::S, ) where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet} + _register_for_final_touch(map, bridge) push!(map.bridges, bridge) push!(map.constraint_types, (F, S)) return _index(length(map.bridges), F, S) @@ -289,10 +296,43 @@ function add_key_for_bridge( func::MOI.VariableIndex, ::S, ) where {S<:MOI.AbstractScalarSet} + _register_for_final_touch(map, bridge) map.single_variable_constraints[(func.value, S)] = bridge return MOI.ConstraintIndex{MOI.VariableIndex,S}(func.value) end +function _register_for_final_touch(map::Map, bridge::BT) where {BT} + if MOI.Bridges.needs_final_touch(bridge) + if !haskey(map.needs_final_touch, BT) + map.needs_final_touch[BT] = OrderedSet{BT}() + end + push!(map.needs_final_touch[BT], bridge) + end + return +end + +function _unregister_for_final_touch(b::Map, bridge::BT) where {BT} + if MOI.Bridges.needs_final_touch(bridge) + delete!(b.needs_final_touch[BT], bridge) + end + return +end + +# Function barrier to iterate over bridges of the same type in an efficient way. +function _final_touch(bridges, model) + for bridge in bridges + MOI.Bridges.final_touch(bridge, model) + end + return +end + +function MOI.Bridges.final_touch(map::Map, model::MOI.ModelLike) + for bridges in values(map.needs_final_touch) + _final_touch(bridges, model) + end + return +end + """ EmptyMap <: AbstractDict{MOI.ConstraintIndex, AbstractBridge} @@ -315,3 +355,5 @@ Base.values(::EmptyMap) = MOI.Utilities.EmptyVector{AbstractBridge}() has_bridges(::EmptyMap) = false number_of_type(::EmptyMap, ::Type{<:MOI.ConstraintIndex}) = 0 + +MOI.Bridges.final_touch(::EmptyMap, ::MOI.ModelLike) = nothing diff --git a/src/Bridges/bridge.jl b/src/Bridges/bridge.jl index fe5ad3d337..a644cd644e 100644 --- a/src/Bridges/bridge.jl +++ b/src/Bridges/bridge.jl @@ -253,3 +253,27 @@ MathOptInterface.ScalarAffineFunction{Float64} ``` """ function set_objective_function_type end + +""" + needs_final_touch(bridge::AbstractBridge)::Bool + +Return whether [`final_touch`](@ref) is implemented by `bridge`. +""" +needs_final_touch(::AbstractBridge) = false + +""" + final_touch(bridge::AbstractBridge, model::MOI.ModelLike)::Nothing + +A function that is called immediately prior to [`MOI.optimize!`](@ref) to allow +bridges to modify their reformulations with repsect to other variables and +constraints in `model`. + +For example, if the correctness of `bridge` depends on the bounds of a variable +or the fact that variables are integer, then the bridge can implement +[`final_touch`](@ref) to check assumptions immediately before a call to +[`MOI.optimize!`](@ref). + +If you implement this method, you must also implement +[`needs_final_touch`](@ref). +""" +function final_touch end diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 3968327748..019a582e59 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -374,7 +374,11 @@ end # Implementation of the MOI interface for AbstractBridgeOptimizer # By convention, the model should be stored in a `model` field -MOI.optimize!(b::AbstractBridgeOptimizer) = MOI.optimize!(b.model) +function MOI.optimize!(b::AbstractBridgeOptimizer) + final_touch(b) + MOI.optimize!(b.model) + return +end function MOI.is_empty(b::AbstractBridgeOptimizer) return isempty(Variable.bridges(b)) && @@ -453,8 +457,16 @@ end function MOI.supports_incremental_interface(b::AbstractBridgeOptimizer) return MOI.supports_incremental_interface(b.model) end -function MOI.Utilities.final_touch(uf::AbstractBridgeOptimizer, index_map) - return MOI.Utilities.final_touch(uf.model, index_map) + +function final_touch(b::AbstractBridgeOptimizer) + final_touch(Constraint.bridges(b), recursive_model(b)) + return +end + +function MOI.Utilities.final_touch(b::AbstractBridgeOptimizer, index_map) + final_touch(b) + MOI.Utilities.final_touch(b.model, index_map) + return end # References diff --git a/test/Bridges/Constraint/count_distinct.jl b/test/Bridges/Constraint/count_distinct.jl new file mode 100644 index 0000000000..fb31c4f4d5 --- /dev/null +++ b/test/Bridges/Constraint/count_distinct.jl @@ -0,0 +1,135 @@ +# 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 TestConstraintCountDistinct + +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.CountDistinctToMILPBridge, + """ + variables: n, x, y + [n, x, y] in CountDistinct(3) + x in Interval(1.0, 2.0) + y >= 2.0 + y <= 3.0 + """, + """ + variables: n, x, y, z_x1, z_x2, z_y2, z_y3, a_1, a_2, a_3 + 1.0 * x + -1.0 * z_x1 + -2.0 * z_x2 == 0.0 + 1.0 * y + -2.0 * z_y2 + -3.0 * z_y3 == 0.0 + -1.0 * n + a_1 + a_2 + a_3 == 0.0 + z_x1 + z_x2 == 1.0 + z_y2 + z_y3 == 1.0 + z_x1 + -1.0 * a_1 <= 0.0 + z_x2 + z_y2 + -2.0 * a_2 <= 0.0 + z_y3 + -1.0 * a_3 <= 0.0 + a_1 + -1.0 * z_x1 <= 0.0 + a_2 + -1.0 * z_x2 + -1.0 * z_y2 <= 0.0 + a_3 + -1.0 * z_y3 <= 0.0 + x in Interval(1.0, 2.0) + y >= 2.0 + y <= 3.0 + z_x1 in ZeroOne() + z_x2 in ZeroOne() + z_y2 in ZeroOne() + z_y3 in ZeroOne() + a_1 in ZeroOne() + a_2 in ZeroOne() + a_3 in ZeroOne() + """, + ) + return +end + +function test_runtests_VectorAffineFunction() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.CountDistinctToMILPBridge, + """ + variables: x, y + [2.0, 2.0 * x + -1.0, y] in CountDistinct(3) + x in Interval(1.0, 2.0) + y >= 2.0 + y <= 3.0 + """, + """ + variables: x, y, z_x1, z_x2, z_x3, z_y2, z_y3, a_1, a_2, a_3 + 2.0 * x + -1.0 * z_x1 + -2.0 * z_x2 + -3.0 * z_x3 == 1.0 + 1.0 * y + -2.0 * z_y2 + -3.0 * z_y3 == 0.0 + a_1 + a_2 + a_3 == 2.0 + z_x1 + z_x2 + z_x3 == 1.0 + z_y2 + z_y3 == 1.0 + z_x1 + -1.0 * a_1 <= 0.0 + z_x2 + z_y2 + -2.0 * a_2 <= 0.0 + z_x3 + z_y3 + -2.0 * a_3 <= 0.0 + a_1 + -1.0 * z_x1 <= 0.0 + a_2 + -1.0 * z_x2 + -1.0 * z_y2 <= 0.0 + a_3 + -1.0 * z_x3 + -1.0 * z_y3 <= 0.0 + x in Interval(1.0, 2.0) + y >= 2.0 + y <= 3.0 + z_x1 in ZeroOne() + z_x2 in ZeroOne() + z_x3 in ZeroOne() + z_y2 in ZeroOne() + z_y3 in ZeroOne() + a_1 in ZeroOne() + a_2 in ZeroOne() + a_3 in ZeroOne() + """, + ) + return +end + +function test_runtests_error_variable() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.CountDistinctToMILP{Int}(inner) + x = MOI.add_variables(model, 3) + MOI.add_constraint(model, MOI.VectorOfVariables(x), MOI.CountDistinct(3)) + @test_throws( + ErrorException( + "Unable to use CountDistinctToMILPBridge 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.CountDistinctToMILP{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.CountDistinct(3)) + @test_throws( + ErrorException( + "Unable to use CountDistinctToMILPBridge because element 2 in " * + "the function has a non-finite domain: $(1 * x[1])", + ), + MOI.Bridges.final_touch(model), + ) + return +end + +end # module + +TestConstraintCountDistinct.runtests()