Skip to content
58 changes: 45 additions & 13 deletions src/MOI_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,23 @@ scip_vartype(::Type{MOI.ZeroOne}) = SCIP_VARTYPE_BINARY
scip_vartype(::Type{MOI.Integer}) = SCIP_VARTYPE_INTEGER
function MOI.add_constraint(o::Optimizer, func::SVF, set::S) where {S <: VAR_TYPES}
allow_modification(o)
v = var(o, func.variable)
vi = func.variable
v = var(o, vi)
infeasible = Ref{Ptr{SCIP_Bool}}
@SC SCIPchgVarType(scip(o), v, scip_vartype(S), infeasible[])
if S <: MOI.ZeroOne
# need to adjust bounds for SCIP?!
@SC SCIPchgVarLb(scip(o), v, 0.0)
@SC SCIPchgVarUb(scip(o), v, 1.0)
# Need to adjust bounds for SCIP, which fails with an error otherwise.
# Check for conflicts with existing bounds first:
lb, ub = SCIPvarGetLbOriginal(v), SCIPvarGetUbOriginal(v)
if lb >= 0.0 && ub <= 1.0
# nothing to be done
elseif lb == -SCIPinfinity(scip(o)) && ub == SCIPinfinity(scip(o))
@debug "Implicitly setting bounds [0,1] for binary variable at $(vi.value)!"
@SC SCIPchgVarLb(scip(o), v, 0.0)
@SC SCIPchgVarUb(scip(o), v, 1.0)
else
error("Existing bounds [$lb,$ub] conflict for binary variable at $(vi.value)!")
end
end
# use var index for cons index of this type
i = func.variable.value
Expand All @@ -171,10 +181,33 @@ end

function MOI.add_constraint(o::Optimizer, func::SVF, set::S) where S <: BOUNDS
allow_modification(o)
v = var(o, func.variable)
lb, ub = bounds(set)
lb == nothing || @SC SCIPchgVarLb(scip(o), v, lb)
ub == nothing || @SC SCIPchgVarUb(scip(o), v, ub)
s = scip(o)
vi = func.variable
v = var(o, vi)
inf = SCIPinfinity(s)

newlb, newub = bounds(set)
newlb = newlb == nothing ? -inf : newlb
newub = newub == nothing ? inf : newub

# Check for existing bounds first.
oldlb, oldub = SCIPvarGetLbOriginal(v), SCIPvarGetUbOriginal(v)
if (oldlb != -inf || oldub != inf)
if oldlb == newlb && oldub == newub
@debug "Variable at $(vi.value) already has these bounds, skipping new constraint!"
elseif oldlb == 0.0 && oldub == 1.0 && SCIPvarGetType(v) == SCIP_VARTYPE_BINARY
if newlb >= 0.0 && newlb <= newub && newub <= 1.0
@debug "Overwriting existing bounds [0.0,1.0] with [$newlb,$newub] for binary variable at $(vi.value)!"
else
error("Invalid bounds [$newlb,$newub] for binary variable at $(vi.value)!")
end
else
error("Already have bounds [$oldlb,$oldub] for variable at $(vi.value)!")
end
end

@SC SCIPchgVarLb(scip(o), v, newlb)
@SC SCIPchgVarUb(scip(o), v, newub)
# use var index for cons index of this type
i = func.variable.value
return register!(o, CI{SVF, S}(i))
Expand All @@ -184,8 +217,8 @@ function MOI.set(o::SCIP.Optimizer, ::MOI.ConstraintSet, ci::CI{SVF,S}, set::S)
allow_modification(o)
v = var(o, VI(ci.value)) # cons index is actually var index
lb, ub = bounds(set)
lb == nothing || @SC SCIPchgVarLb(scip(o), v, lb)
ub == nothing || @SC SCIPchgVarUb(scip(o), v, ub)
@SC SCIPchgVarLb(scip(o), v, lb == nothing ? -SCIPinfinity(scip(o)) : lb)
@SC SCIPchgVarUb(scip(o), v, ub == nothing ? SCIPinfinity(scip(o)) : ub)
return nothing
end

Expand All @@ -195,8 +228,7 @@ end

function MOI.add_constraint(o::Optimizer, func::SAF, set::S) where {S <: BOUNDS}
if func.constant != 0.0
msg = "SCIP does not support linear constraints with a constant offset."
throw(MOI.AddConstraintNotAllowed{SAF, S}(msg))
error("SCIP does not support linear constraints with a constant offset.")
end

allow_modification(o)
Expand Down Expand Up @@ -241,7 +273,7 @@ function MOI.get(o::Optimizer, ::MOI.ConstraintFunction, ci::CI{SVF, S}) where S
end

function MOI.get(o::Optimizer, ::MOI.ConstraintSet, ci::CI{SVF, S}) where S <: BOUNDS
v = var(o.mscip, ci.value)
v = var(o, VI(ci.value))
lb, ub = SCIPvarGetLbOriginal(v), SCIPvarGetUbOriginal(v)
return from_bounds(S, lb, ub)
end
Expand Down
185 changes: 185 additions & 0 deletions test/MOI_additional.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
using MathOptInterface
const MOI = MathOptInterface

const VI = MOI.VariableIndex
const CI = MOI.ConstraintIndex
const SVF = MOI.SingleVariable

function var_bounds(o::SCIP.Optimizer, vi::VI)
return MOI.get(o, MOI.ConstraintSet(), CI{SVF,MOI.Interval{Float64}}(vi.value))
end

function chg_bounds(o::SCIP.Optimizer, vi::VI, set::S) where S
ci = CI{SVF,S}(vi.value)
MOI.set(o, MOI.ConstraintSet(), ci, set)
return nothing
end

@testset "Binary variables and explicit bounds" begin
optimizer = SCIP.Optimizer()

# Should work: binary variable without explicit bounds
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
t = MOI.add_constraint(optimizer, x, MOI.ZeroOne())
@test var_bounds(optimizer, x) == MOI.Interval(0.0, 1.0)

# Should work: binary variable with [0, 1] bounds
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
b = MOI.add_constraint(optimizer, x, MOI.Interval(0.0, 1.0))
t = MOI.add_constraint(optimizer, x, MOI.ZeroOne())
@test var_bounds(optimizer, x) == MOI.Interval(0.0, 1.0)

# Should work: binary variable with [0, 1] bounds (order should not matter)
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
t = MOI.add_constraint(optimizer, x, MOI.ZeroOne())
b = MOI.add_constraint(optimizer, x, MOI.Interval(0.0, 1.0))
@test var_bounds(optimizer, x) == MOI.Interval(0.0, 1.0)

# Should work: binary variable fixed to 0.
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
t = MOI.add_constraint(optimizer, x, MOI.ZeroOne())
b = MOI.add_constraint(optimizer, x, MOI.EqualTo(0.0))
@test var_bounds(optimizer, x) == MOI.Interval(0.0, 0.0)

# Should work: binary variable fixed to 0 (different order).
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
b = MOI.add_constraint(optimizer, x, MOI.EqualTo(0.0))
t = MOI.add_constraint(optimizer, x, MOI.ZeroOne())
@test var_bounds(optimizer, x) == MOI.Interval(0.0, 0.0)

# Should work: binary variable fixed to 1.
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
t = MOI.add_constraint(optimizer, x, MOI.ZeroOne())
b = MOI.add_constraint(optimizer, x, MOI.EqualTo(1.0))
@test var_bounds(optimizer, x) == MOI.Interval(1.0, 1.0)

# Should work: binary variable fixed to 1 (different order).
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
b = MOI.add_constraint(optimizer, x, MOI.EqualTo(1.0))
t = MOI.add_constraint(optimizer, x, MOI.ZeroOne())
@test var_bounds(optimizer, x) == MOI.Interval(1.0, 1.0)

# Is an error: binary variable with too wide bounds.
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
b = MOI.add_constraint(optimizer, x, MOI.Interval(-1.0, 2.0))
@test_throws ErrorException t = MOI.add_constraint(optimizer, x, MOI.ZeroOne())

# Is an error: binary variable with too wide bounds (different order).
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
t = MOI.add_constraint(optimizer, x, MOI.ZeroOne())
@test_throws ErrorException b = MOI.add_constraint(optimizer, x, MOI.Interval(-1.0, 2.0))

# Is an error: binary variable with conflicting bounds (infeasible).
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
b = MOI.add_constraint(optimizer, x, MOI.Interval(2.0, 3.0))
@test_throws ErrorException t = MOI.add_constraint(optimizer, x, MOI.ZeroOne())

# Is an error: binary variable with conflicting bounds (infeasible, different order).
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
t = MOI.add_constraint(optimizer, x, MOI.ZeroOne())
@test_throws ErrorException b = MOI.add_constraint(optimizer, x, MOI.Interval(2.0, 3.0))
end

@testset "Bound constraints for a general variable." begin
optimizer = SCIP.Optimizer()
inf = SCIP.SCIPinfinity(SCIP.scip(optimizer))

# Should work: variable without explicit bounds
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
@test var_bounds(optimizer, x) == MOI.Interval(-inf, inf)

# Should work: variable with range bounds, but only once!
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
b = MOI.add_constraint(optimizer, x, MOI.Interval(2.0, 3.0))
@test var_bounds(optimizer, x) == MOI.Interval(2.0, 3.0)
@test b == MOI.add_constraint(optimizer, x, MOI.Interval(2.0, 3.0))
@test_throws ErrorException MOI.add_constraint(optimizer, x, MOI.Interval(3.0, 4.0))

# Should work: variable with lower bound, but only once!
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
b = MOI.add_constraint(optimizer, x, MOI.GreaterThan(2.0))
@test var_bounds(optimizer, x) == MOI.Interval(2.0, inf)
@test b == MOI.add_constraint(optimizer, x, MOI.GreaterThan(2.0))
@test_throws ErrorException MOI.add_constraint(optimizer, x, MOI.GreaterThan(3.0))

# Should work: variable with lower bound, but only once!
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
b = MOI.add_constraint(optimizer, x, MOI.LessThan(2.0))
@test var_bounds(optimizer, x) == MOI.Interval(-inf, 2.0)
@test b == MOI.add_constraint(optimizer, x, MOI.LessThan(2.0))
@test_throws ErrorException MOI.add_constraint(optimizer, x, MOI.LessThan(3.0))

# Should work: fixed variable, but only once!
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
b = MOI.add_constraint(optimizer, x, MOI.EqualTo(2.0))
@test var_bounds(optimizer, x) == MOI.Interval(2.0, 2.0)
@test b == MOI.add_constraint(optimizer, x, MOI.EqualTo(2.0))
@test_throws ErrorException MOI.add_constraint(optimizer, x, MOI.EqualTo(3.0))

# Mixed constraint types will fail!
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
lb = MOI.add_constraint(optimizer, x, MOI.GreaterThan(2.0))
@test_throws ErrorException ub = MOI.add_constraint(optimizer, x, MOI.LessThan(3.0))
end

@testset "Changing bounds for variable." begin
optimizer = SCIP.Optimizer()
inf = SCIP.SCIPinfinity(SCIP.scip(optimizer))

# change interval bounds
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
b = MOI.add_constraint(optimizer, x, MOI.Interval(2.0, 3.0))
@test var_bounds(optimizer, x) == MOI.Interval(2.0, 3.0)
chg_bounds(optimizer, x, MOI.Interval(4.0, 5.0))
@test var_bounds(optimizer, x) == MOI.Interval(4.0, 5.0)

# change lower bound
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
b = MOI.add_constraint(optimizer, x, MOI.GreaterThan(2.0))
@test var_bounds(optimizer, x) == MOI.Interval(2.0, inf)
chg_bounds(optimizer, x, MOI.GreaterThan(4.0))
@test var_bounds(optimizer, x) == MOI.Interval(4.0, inf)

# change upper bound
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
b = MOI.add_constraint(optimizer, x, MOI.LessThan(3.0))
@test var_bounds(optimizer, x) == MOI.Interval(-inf, 3.0)
chg_bounds(optimizer, x, MOI.LessThan(5.0))
@test var_bounds(optimizer, x) == MOI.Interval(-inf, 5.0)

# change fixed value
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
b = MOI.add_constraint(optimizer, x, MOI.EqualTo(2.5))
@test var_bounds(optimizer, x) == MOI.Interval(2.5, 2.5)
chg_bounds(optimizer, x, MOI.EqualTo(4.5))
@test var_bounds(optimizer, x) == MOI.Interval(4.5, 4.5)

# change mixed
MOI.empty!(optimizer)
x = MOI.add_variable(optimizer)
b = MOI.add_constraint(optimizer, x, MOI.GreaterThan(2.0))
@test var_bounds(optimizer, x) == MOI.Interval(2.0, inf)
chg_bounds(optimizer, x, MOI.LessThan(4.0))
@test var_bounds(optimizer, x) == MOI.Interval(-inf, 4.0)
end
4 changes: 4 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ end
@testset "MathOptInterface tests" begin
include("MOI_wrapper.jl")
end

@testset "MathOptInterface additional tests" begin
include("MOI_additional.jl")
end