From 554b88fa93cebb768e6bd18a91760c78b42a37aa Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 30 Sep 2020 17:46:23 +1300 Subject: [PATCH] Add indicator constraint support --- src/Gurobi.jl | 1 + src/MOI_indicator_constraint.jl | 206 ++++++++++++++++++++++++++++++++ src/MOI_wrapper.jl | 10 +- test/MOI/MOI_wrapper.jl | 101 +++++++++++++++- 4 files changed, 311 insertions(+), 7 deletions(-) create mode 100644 src/MOI_indicator_constraint.jl diff --git a/src/Gurobi.jl b/src/Gurobi.jl index 9c42ec90..c1b41b3c 100644 --- a/src/Gurobi.jl +++ b/src/Gurobi.jl @@ -46,6 +46,7 @@ end include("MOI_wrapper.jl") include("MOI_callbacks.jl") include("MOI_multi_objective.jl") +include("MOI_indicator_constraint.jl") # Gurobi exports all `GRBXXX` symbols. If you don't want all of these symbols in # your environment, then use `import Gurobi` instead of `using Gurobi`. diff --git a/src/MOI_indicator_constraint.jl b/src/MOI_indicator_constraint.jl new file mode 100644 index 00000000..36fb3b08 --- /dev/null +++ b/src/MOI_indicator_constraint.jl @@ -0,0 +1,206 @@ +function _info( + model::Optimizer, + c::MOI.ConstraintIndex{ + MOI.VectorAffineFunction{Float64}, <:MOI.IndicatorSet + }, +) + if haskey(model.indicator_constraint_info, c.value) + return model.indicator_constraint_info[c.value] + end + throw(MOI.InvalidIndex(c)) +end + + +function MOI.supports_constraint( + ::Optimizer, + ::Type{MOI.VectorAffineFunction{Float64}}, + ::Type{<:MOI.IndicatorSet{A, S}}, +) where {A, S <: _SUPPORTED_SCALAR_SETS} + return true +end + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.VectorAffineFunction{Float64}, S}, +) where {S <: MOI.IndicatorSet} + info = get(model.indicator_constraint_info, c.value, nothing) + if info === nothing + return false + end + return isa(info.set, S) +end + +function MOI.get( + model::Optimizer, + ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{<:MOI.VectorAffineFunction, <:MOI.IndicatorSet}, +) + MOI.throw_if_not_valid(model, c) + return _info(model, c).set +end + +function MOI.get( + model::Optimizer, + ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{<:MOI.VectorAffineFunction, <:MOI.IndicatorSet}, +) + MOI.throw_if_not_valid(model, c) + _update_if_necessary(model) + info = _info(model, c) + binvarP, nvarsP = Ref{Cint}(), Ref{Cint}() + ret = GRBgetgenconstrIndicator( + model, + Cint(info.row - 1), + binvarP, + C_NULL, + nvarsP, + C_NULL, + C_NULL, + C_NULL, + C_NULL, + ) + _check_ret(model, ret) + vars = Vector{Cint}(undef, nvarsP[]) + vals = Vector{Cdouble}(undef, nvarsP[]) + rhsP = Ref{Cdouble}() + ret = GRBgetgenconstrIndicator( + model, + Cint(info.row - 1), + C_NULL, + C_NULL, + C_NULL, + vars, + vals, + C_NULL, + rhsP, + ) + _check_ret(model, ret) + terms = Vector{MOI.VectorAffineTerm{Float64}}(undef, nvarsP[] + 1) + x = model.variable_info[CleverDicts.LinearIndex(binvarP[] + 1)].index + terms[1] = MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x)) + for i = 1:nvarsP[] + x = model.variable_info[CleverDicts.LinearIndex(vars[i] + 1)].index + terms[i + 1] = MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(vals[i], x)) + end + _, rhs = _sense_and_rhs(info.set.set) + return MOI.VectorAffineFunction(terms, [0.0, rhs - rhsP[]]) +end + +function MOI.add_constraint( + model::Optimizer, + func::MOI.VectorAffineFunction{Float64}, + s::MOI.IndicatorSet{A, S}, +) where {A, S <: _SUPPORTED_SCALAR_SETS} + if !iszero(func.constants[1]) + error("Constant in output_index 1 should be 0. Got $(func.constants[1])") + end + vars = Vector{Cint}(undef, length(func.terms) - 1) + vals = Vector{Cdouble}(undef, length(func.terms) - 1) + binvar = Cint(-1) + i = 1 + for vector_term in func.terms + row = vector_term.output_index + term = vector_term.scalar_term + if row == 1 + if binvar !== Cint(-1) + error("There should be exactly one term in output_index 1") + elseif !isapprox(term.coefficient, 1.0) + error( + "Expected coefficient in front of indicator variable to " * + "be 1.0. Got $(term.coefficient)." + ) + end + binvar = Cint(column(model, term.variable_index) - 1) + else + @assert row == 2 + vars[i] = Cint(column(model, term.variable_index) - 1) + vals[i] = term.coefficient + i += 1 + end + end + sense, rhs = _sense_and_rhs(s.set) + rhs -= func.constants[2] + ret = GRBaddgenconstrIndicator( + model, + "", + binvar, + A == MOI.ACTIVATE_ON_ONE ? Cint(1) : Cint(0), + length(vars), + vars, + vals, + sense, + rhs, + ) + _check_ret(model, ret) + model.last_constraint_index += 1 + info = _ConstraintInfo( + length(model.indicator_constraint_info) + 1, s + ) + model.indicator_constraint_info[model.last_constraint_index] = info + _require_update(model) + return MOI.ConstraintIndex{typeof(func), typeof(s)}(model.last_constraint_index) +end + +function MOI.get( + model::Optimizer, + ::MOI.ListOfConstraintIndices{<:MOI.VectorAffineFunction, S}, +) where {S <: MOI.IndicatorSet} + indices = MOI.ConstraintIndex{MOI.VectorAffineFunction{Float64}, S}[ + MOI.ConstraintIndex{MOI.VectorAffineFunction{Float64}, S}(key) + for (key, info) in model.indicator_constraint_info if isa(info.set, S) + ] + return sort!(indices, by = x -> x.value) +end + +function MOI.get( + model::Optimizer, + ::MOI.NumberOfConstraints{MOI.VectorAffineFunction{Float64}, S}, +) where {S <: MOI.IndicatorSet} + return count(x -> isa(x.set, S), values(model.indicator_constraint_info)) +end + +function MOI.get( + model::Optimizer, + ::MOI.ConstraintName, + c::MOI.ConstraintIndex{<:MOI.VectorAffineFunction, <:MOI.IndicatorSet}, +) + MOI.throw_if_not_valid(model, c) + return _info(model, c).name +end + +function MOI.set( + model::Optimizer, + ::MOI.ConstraintName, + c::MOI.ConstraintIndex{<:MOI.VectorAffineFunction, S}, + name::String, +) where {S <: MOI.IndicatorSet} + MOI.throw_if_not_valid(model, c) + _update_if_necessary(model) + info = _info(model, c) + info.name = name + if !isempty(name) + row = Cint(_info(model, c).row - 1) + ret = GRBsetstrattrelement(model, "GenConstrName", row, name) + _check_ret(model, ret) + end + return +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{<:MOI.VectorAffineFunction, <:MOI.IndicatorSet}, +) + MOI.throw_if_not_valid(model, c) + row = _info(model, c).row + ind = Ref{Cint}(row - 1) + ret = GRBdelgenconstrs(model, 1, ind) + _check_ret(model, ret) + delete!(model.indicator_constraint_info, c.value) + for info in values(model.indicator_constraint_info) + if info.row > row + info.row -= 1 + end + end + _require_update(model) + return +end diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index c7820a8d..1a737aaf 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -166,6 +166,8 @@ mutable struct Optimizer <: MOI.AbstractOptimizer quadratic_constraint_info::Dict{Int, _ConstraintInfo} # VectorOfVariables-in-Set storage. sos_constraint_info::Dict{Int, _ConstraintInfo} + # VectorAffineFunction-in-Set storage. + indicator_constraint_info::Dict{Int, _ConstraintInfo} # Note: we do not have a singlevariable_constraint_info dictionary. Instead, # data associated with these constraints are stored in the _VariableInfo # objects. @@ -236,6 +238,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer model.affine_constraint_info = Dict{Int, _ConstraintInfo}() model.quadratic_constraint_info = Dict{Int, _ConstraintInfo}() model.sos_constraint_info = Dict{Int, _ConstraintInfo}() + model.indicator_constraint_info = Dict{Int, _ConstraintInfo}() model.callback_variable_primal = Float64[] MOI.empty!(model) finalizer(model) do m @@ -331,6 +334,7 @@ function MOI.empty!(model::Optimizer) empty!(model.affine_constraint_info) empty!(model.quadratic_constraint_info) empty!(model.sos_constraint_info) + empty!(model.indicator_constraint_info) model.name_to_variable = nothing model.name_to_constraint_index = nothing model.has_unbounded_ray = false @@ -624,9 +628,9 @@ function _indices_and_coefficients( return indices, coefficients, I, J, V end -_sense_and_rhs(s::MOI.LessThan{Float64}) = (Cchar('<'), s.upper) -_sense_and_rhs(s::MOI.GreaterThan{Float64}) = (Cchar('>'), s.lower) -_sense_and_rhs(s::MOI.EqualTo{Float64}) = (Cchar('='), s.value) +_sense_and_rhs(s::MOI.LessThan{Float64}) = (GRB_LESS_EQUAL, s.upper) +_sense_and_rhs(s::MOI.GreaterThan{Float64}) = (GRB_GREATER_EQUAL, s.lower) +_sense_and_rhs(s::MOI.EqualTo{Float64}) = (GRB_EQUAL, s.value) ### ### Variables diff --git a/test/MOI/MOI_wrapper.jl b/test/MOI/MOI_wrapper.jl index a96fcd17..a31cf47d 100644 --- a/test/MOI/MOI_wrapper.jl +++ b/test/MOI/MOI_wrapper.jl @@ -95,10 +95,7 @@ function test_conictest() end function test_intlinear() - MOIT.intlineartest(OPTIMIZER, CONFIG, [ - # Indicator sets not supported. - "indicator1", "indicator2", "indicator3", "indicator4" - ]) + MOIT.intlineartest(OPTIMIZER, CONFIG) end function test_solvername() @@ -1025,6 +1022,102 @@ function test_Attributes() @test MOI.get(model, MOI.SimplexIterations()) == 0 end +function test_indicator_name() + MOI.empty!(OPTIMIZER) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, MOI.SingleVariable(x[1]), MOI.ZeroOne()) + f = MOI.VectorAffineFunction( + [ + MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x[1])), + MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(2.0, x[2])), + ], + [0.0, 0.0], + ) + s = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.GreaterThan(1.0)) + c = MOI.add_constraint(model, f, s) + MOI.set(model, MOI.ConstraintName(), c, "my_indicator") + @test MOI.get(model, MOI.ConstraintName(), c) == "my_indicator" +end + +function test_indicator_on_one() + MOI.empty!(OPTIMIZER) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, MOI.SingleVariable(x[1]), MOI.ZeroOne()) + f = MOI.VectorAffineFunction( + [ + MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x[1])), + MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(2.0, x[2])), + ], + [0.0, 0.0], + ) + s = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.GreaterThan(1.0)) + c = MOI.add_constraint(model, f, s) + @test MOI.get(model, MOI.ConstraintSet(), c) == s + @test isapprox(MOI.get(model, MOI.ConstraintFunction(), c), f) +end + +function test_indicator_on_zero() + MOI.empty!(OPTIMIZER) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, MOI.SingleVariable(x[1]), MOI.ZeroOne()) + f = MOI.VectorAffineFunction( + [ + MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x[1])), + MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(2.0, x[2])), + ], + [0.0, 0.0], + ) + s = MOI.IndicatorSet{MOI.ACTIVATE_ON_ZERO}(MOI.GreaterThan(1.0)) + c = MOI.add_constraint(model, f, s) + @test MOI.get(model, MOI.ConstraintSet(), c) == s + @test isapprox(MOI.get(model, MOI.ConstraintFunction(), c), f) +end + +function test_indicator_nonconstant_x() + MOI.empty!(OPTIMIZER) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, MOI.SingleVariable(x[1]), MOI.ZeroOne()) + f = MOI.VectorAffineFunction( + [ + MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(-1.0, x[1])), + MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(2.0, x[2])), + ], + [0.0, 0.0], + ) + s = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.GreaterThan(1.0)) + @test_throws ErrorException MOI.add_constraint(model, f, s) +end + +function test_indicator_too_many_indicators() + MOI.empty!(OPTIMIZER) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, MOI.SingleVariable(x[1]), MOI.ZeroOne()) + f = MOI.VectorAffineFunction( + [ + MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(-1.0, x[1])), + MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(2.0, x[2])), + ], + [0.0, 0.0], + ) + s = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.GreaterThan(1.0)) + @test_throws ErrorException MOI.add_constraint(model, f, s) +end + +function test_indicator_nonconstant() + MOI.empty!(OPTIMIZER) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, MOI.SingleVariable(x[1]), MOI.ZeroOne()) + f = MOI.VectorAffineFunction( + [ + MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, x[1])), + MOI.VectorAffineTerm(2, MOI.ScalarAffineTerm(2.0, x[2])), + ], + [1.0, 0.0], + ) + s = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.GreaterThan(1.0)) + @test_throws ErrorException MOI.add_constraint(model, f, s) +end + end runtests(TestMOIWrapper)