Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add indicator constraint support #352

Merged
merged 1 commit into from
Oct 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Gurobi.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
206 changes: 206 additions & 0 deletions src/MOI_indicator_constraint.jl
Original file line number Diff line number Diff line change
@@ -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
10 changes: 7 additions & 3 deletions src/MOI_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
101 changes: 97 additions & 4 deletions test/MOI/MOI_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)