Skip to content

Commit

Permalink
Utility for relaxing integrality (#2275)
Browse files Browse the repository at this point in the history
* Utility for relaxing integrality

Closes #1611

* typo fix

* error on semi-integer and semi-continuous
  • Loading branch information
mlubin authored Jul 13, 2020
1 parent 42668d1 commit 1129e58
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 3 deletions.
14 changes: 11 additions & 3 deletions docs/src/variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,13 +448,13 @@ julia> @variable(model, y[A], container=Array)
JuMP now creates a vector of JuMP variables instead of a DenseAxisArray. Note
that choosing an invalid container type will throw an error.

## Integrality shortcuts
## Integrality utilities

Adding integrality constraints to a model such as `@constraint(model, x in MOI.ZeroOne())`
and `@constraint(model, x in MOI.Integer())` is a common operation. Therefore,
JuMP supports two shortcuts for adding such constraints.

#### Binary (ZeroOne) constraints
### Binary (ZeroOne) constraints

Binary optimization variables are constrained to the set ``x \in \{0, 1\}``. (The
`MOI.ZeroOne` set in MathOptInterface.) Binary optimization variables can be
Expand Down Expand Up @@ -482,7 +482,7 @@ keyword to `true`.
julia> @variable(model, x, binary=true)
x
```
#### Integer constraints
### Integer constraints

Integer optimization variables are constrained to the set ``x \in \mathbb{Z}``.
(The `MOI.Integer` set in MathOptInterface.) Integer optimization variables can
Expand Down Expand Up @@ -510,6 +510,12 @@ julia> is_integer(x)
false
```

### Relaxing integrality

The [`relax_integrality`](@ref) function relaxes all integrality constraints in
the model, returning a function that can be called to undo the operation later
on.

## Semidefinite variables

JuMP also supports modeling with semidefinite variables. A square symmetric
Expand Down Expand Up @@ -794,6 +800,8 @@ set_binary
unset_binary
BinaryRef
relax_integrality
index(::VariableRef)
optimizer_index(::VariableRef)
Expand Down
116 changes: 116 additions & 0 deletions src/variables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -995,3 +995,119 @@ end

value(::_MA.Zero) = 0.0
value(x::Number) = x

function _info_from_variable(v::VariableRef)
has_lb = has_lower_bound(v)
lb = has_lb ? lower_bound(v) : -Inf
has_ub = has_upper_bound(v)
ub = has_ub ? upper_bound(v) : Inf
has_fix = is_fixed(v)
fixed_value = has_fix ? fix_value(v) : NaN
start_or_nothing = start_value(v)
has_start = !(start_or_nothing isa Nothing)
start = has_start ? start_or_nothing : NaN
has_start = start !== Nothing
binary = is_binary(v)
integer = is_integer(v)
return VariableInfo(has_lb, lb, has_ub, ub, has_fix, fixed_value,
has_start, start, binary, integer)
end

"""
relax_integrality(model::Model)
Modifies `model` to "relax" all binary and integrality constraints on
variables. Specifically,
- Binary constraints are deleted, and variable bounds are tightened if
necessary to ensure the variable is constrained to the interval ``[0, 1]``.
- Integrality constraints are deleted without modifying variable bounds.
- An error is thrown if semi-continuous or semi-integer constraints are
present (support may be added for these in the future).
- All other constraints are ignored (left in place). This includes discrete
constraints like SOS and indicator constraints.
Returns a function that can be called without any arguments to restore the
original model. The behavior of this function is undefined if additional
changes are made to the affected variables in the meantime.
# Example
```jldoctest
julia> model = Model();
julia> @variable(model, x, Bin);
julia> @variable(model, 1 <= y <= 10, Int);
julia> @objective(model, Min, x + y);
julia> undo_relax = relax_integrality(model);
julia> print(model)
Min x + y
Subject to
x ≥ 0.0
y ≥ 1.0
x ≤ 1.0
y ≤ 10.0
julia> undo_relax()
julia> print(model)
Min x + y
Subject to
y ≥ 1.0
y ≤ 10.0
y integer
x binary
```
"""
function relax_integrality(model::Model)
semicont_type = _MOICON{MOI.SingleVariable, MOI.Semicontinuous{Float64}}
semiint_type = _MOICON{MOI.SingleVariable, MOI.Semiinteger{Float64}}
for v in all_variables(model)
if MOI.is_valid(backend(model), semicont_type(index(v).value))
error("Support for relaxing semicontinuous constraints is not " *
"yet implemented.")
elseif MOI.is_valid(backend(model), semiint_type(index(v).value))
error("Support for relaxing semi-integer constraints is not " *
"yet implemented.")
end
end

info_pre_relaxation = map(v -> (v, _info_from_variable(v)),
all_variables(model))
# We gather the info first because some solvers perform poorly when you
# interleave queries and changes. See, e.g.,
# https://github.com/jump-dev/Gurobi.jl/pull/301.
for (v, info) in info_pre_relaxation
if info.integer
unset_integer(v)
elseif info.binary
unset_binary(v)
set_lower_bound(v, max(0.0, info.lower_bound))
set_upper_bound(v, min(1.0, info.upper_bound))
end
end
function unrelax()
for (v, info) in info_pre_relaxation
if info.integer
set_integer(v)
elseif info.binary
set_binary(v)
if info.has_lb
set_lower_bound(v, info.lower_bound)
else
delete_lower_bound(v)
end
if info.has_ub
set_upper_bound(v, info.upper_bound)
else
delete_upper_bound(v)
end
end
end
return
end
return unrelax
end
68 changes: 68 additions & 0 deletions test/variable.jl
Original file line number Diff line number Diff line change
Expand Up @@ -797,3 +797,71 @@ end
@test value(1.0) === 1.0
@test value(JuMP._MA.Zero()) === 0.0
end

@testset "relax_integrality" begin
model = Model()
@variable(model, x, Bin)
@variable(model, -1 <= y <= 2, Bin)
@variable(model, 0.1 <= z <= 0.6, Bin)
@variable(model, a, Int)
@variable(model, -1 <= b <= 2, Int)
unrelax = relax_integrality(model)

@test !is_binary(x)
@test lower_bound(x) == 0.0
@test upper_bound(x) == 1.0

@test !is_binary(y)
@test lower_bound(y) == 0.0
@test upper_bound(y) == 1.0

@test !is_binary(z)
@test lower_bound(z) == 0.1
@test upper_bound(z) == 0.6

@test !is_integer(a)
@test !has_lower_bound(a)
@test !has_upper_bound(a)

@test !is_integer(b)
@test lower_bound(b) == -1.0
@test upper_bound(b) == 2.0

unrelax()

@test is_binary(x)
@test !has_lower_bound(x)
@test !has_upper_bound(x)

@test is_binary(y)
@test lower_bound(y) == -1.0
@test upper_bound(y) == 2.0

@test is_binary(z)
@test lower_bound(z) == 0.1
@test upper_bound(z) == 0.6

@test is_integer(a)
@test !has_lower_bound(a)
@test !has_upper_bound(a)

@test is_integer(b)
@test lower_bound(b) == -1.0
@test upper_bound(b) == 2.0
end

@testset "relax_integrality error cases" begin
model = Model()
@variable(model, x)
@constraint(model, x in MOI.Semicontinuous(1.0, 2.0))
err = ErrorException("Support for relaxing semicontinuous constraints " *
"is not yet implemented.")
@test_throws err relax_integrality(model)

model = Model()
@variable(model, x)
@constraint(model, x in MOI.Semiinteger(1.0, 2.0))
err = ErrorException("Support for relaxing semi-integer constraints " *
"is not yet implemented.")
@test_throws err relax_integrality(model)
end

0 comments on commit 1129e58

Please sign in to comment.