Skip to content

Commit 1129e58

Browse files
authored
Utility for relaxing integrality (#2275)
* Utility for relaxing integrality Closes #1611 * typo fix * error on semi-integer and semi-continuous
1 parent 42668d1 commit 1129e58

File tree

3 files changed

+195
-3
lines changed

3 files changed

+195
-3
lines changed

docs/src/variables.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -448,13 +448,13 @@ julia> @variable(model, y[A], container=Array)
448448
JuMP now creates a vector of JuMP variables instead of a DenseAxisArray. Note
449449
that choosing an invalid container type will throw an error.
450450

451-
## Integrality shortcuts
451+
## Integrality utilities
452452

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

457-
#### Binary (ZeroOne) constraints
457+
### Binary (ZeroOne) constraints
458458

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

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

513+
### Relaxing integrality
514+
515+
The [`relax_integrality`](@ref) function relaxes all integrality constraints in
516+
the model, returning a function that can be called to undo the operation later
517+
on.
518+
513519
## Semidefinite variables
514520

515521
JuMP also supports modeling with semidefinite variables. A square symmetric
@@ -794,6 +800,8 @@ set_binary
794800
unset_binary
795801
BinaryRef
796802
803+
relax_integrality
804+
797805
index(::VariableRef)
798806
optimizer_index(::VariableRef)
799807

src/variables.jl

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,3 +995,119 @@ end
995995

996996
value(::_MA.Zero) = 0.0
997997
value(x::Number) = x
998+
999+
function _info_from_variable(v::VariableRef)
1000+
has_lb = has_lower_bound(v)
1001+
lb = has_lb ? lower_bound(v) : -Inf
1002+
has_ub = has_upper_bound(v)
1003+
ub = has_ub ? upper_bound(v) : Inf
1004+
has_fix = is_fixed(v)
1005+
fixed_value = has_fix ? fix_value(v) : NaN
1006+
start_or_nothing = start_value(v)
1007+
has_start = !(start_or_nothing isa Nothing)
1008+
start = has_start ? start_or_nothing : NaN
1009+
has_start = start !== Nothing
1010+
binary = is_binary(v)
1011+
integer = is_integer(v)
1012+
return VariableInfo(has_lb, lb, has_ub, ub, has_fix, fixed_value,
1013+
has_start, start, binary, integer)
1014+
end
1015+
1016+
"""
1017+
relax_integrality(model::Model)
1018+
1019+
Modifies `model` to "relax" all binary and integrality constraints on
1020+
variables. Specifically,
1021+
1022+
- Binary constraints are deleted, and variable bounds are tightened if
1023+
necessary to ensure the variable is constrained to the interval ``[0, 1]``.
1024+
- Integrality constraints are deleted without modifying variable bounds.
1025+
- An error is thrown if semi-continuous or semi-integer constraints are
1026+
present (support may be added for these in the future).
1027+
- All other constraints are ignored (left in place). This includes discrete
1028+
constraints like SOS and indicator constraints.
1029+
1030+
Returns a function that can be called without any arguments to restore the
1031+
original model. The behavior of this function is undefined if additional
1032+
changes are made to the affected variables in the meantime.
1033+
1034+
# Example
1035+
```jldoctest
1036+
julia> model = Model();
1037+
1038+
julia> @variable(model, x, Bin);
1039+
1040+
julia> @variable(model, 1 <= y <= 10, Int);
1041+
1042+
julia> @objective(model, Min, x + y);
1043+
1044+
julia> undo_relax = relax_integrality(model);
1045+
1046+
julia> print(model)
1047+
Min x + y
1048+
Subject to
1049+
x ≥ 0.0
1050+
y ≥ 1.0
1051+
x ≤ 1.0
1052+
y ≤ 10.0
1053+
1054+
julia> undo_relax()
1055+
1056+
julia> print(model)
1057+
Min x + y
1058+
Subject to
1059+
y ≥ 1.0
1060+
y ≤ 10.0
1061+
y integer
1062+
x binary
1063+
```
1064+
"""
1065+
function relax_integrality(model::Model)
1066+
semicont_type = _MOICON{MOI.SingleVariable, MOI.Semicontinuous{Float64}}
1067+
semiint_type = _MOICON{MOI.SingleVariable, MOI.Semiinteger{Float64}}
1068+
for v in all_variables(model)
1069+
if MOI.is_valid(backend(model), semicont_type(index(v).value))
1070+
error("Support for relaxing semicontinuous constraints is not " *
1071+
"yet implemented.")
1072+
elseif MOI.is_valid(backend(model), semiint_type(index(v).value))
1073+
error("Support for relaxing semi-integer constraints is not " *
1074+
"yet implemented.")
1075+
end
1076+
end
1077+
1078+
info_pre_relaxation = map(v -> (v, _info_from_variable(v)),
1079+
all_variables(model))
1080+
# We gather the info first because some solvers perform poorly when you
1081+
# interleave queries and changes. See, e.g.,
1082+
# https://github.com/jump-dev/Gurobi.jl/pull/301.
1083+
for (v, info) in info_pre_relaxation
1084+
if info.integer
1085+
unset_integer(v)
1086+
elseif info.binary
1087+
unset_binary(v)
1088+
set_lower_bound(v, max(0.0, info.lower_bound))
1089+
set_upper_bound(v, min(1.0, info.upper_bound))
1090+
end
1091+
end
1092+
function unrelax()
1093+
for (v, info) in info_pre_relaxation
1094+
if info.integer
1095+
set_integer(v)
1096+
elseif info.binary
1097+
set_binary(v)
1098+
if info.has_lb
1099+
set_lower_bound(v, info.lower_bound)
1100+
else
1101+
delete_lower_bound(v)
1102+
end
1103+
if info.has_ub
1104+
set_upper_bound(v, info.upper_bound)
1105+
else
1106+
delete_upper_bound(v)
1107+
end
1108+
end
1109+
end
1110+
return
1111+
end
1112+
return unrelax
1113+
end

test/variable.jl

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,3 +797,71 @@ end
797797
@test value(1.0) === 1.0
798798
@test value(JuMP._MA.Zero()) === 0.0
799799
end
800+
801+
@testset "relax_integrality" begin
802+
model = Model()
803+
@variable(model, x, Bin)
804+
@variable(model, -1 <= y <= 2, Bin)
805+
@variable(model, 0.1 <= z <= 0.6, Bin)
806+
@variable(model, a, Int)
807+
@variable(model, -1 <= b <= 2, Int)
808+
unrelax = relax_integrality(model)
809+
810+
@test !is_binary(x)
811+
@test lower_bound(x) == 0.0
812+
@test upper_bound(x) == 1.0
813+
814+
@test !is_binary(y)
815+
@test lower_bound(y) == 0.0
816+
@test upper_bound(y) == 1.0
817+
818+
@test !is_binary(z)
819+
@test lower_bound(z) == 0.1
820+
@test upper_bound(z) == 0.6
821+
822+
@test !is_integer(a)
823+
@test !has_lower_bound(a)
824+
@test !has_upper_bound(a)
825+
826+
@test !is_integer(b)
827+
@test lower_bound(b) == -1.0
828+
@test upper_bound(b) == 2.0
829+
830+
unrelax()
831+
832+
@test is_binary(x)
833+
@test !has_lower_bound(x)
834+
@test !has_upper_bound(x)
835+
836+
@test is_binary(y)
837+
@test lower_bound(y) == -1.0
838+
@test upper_bound(y) == 2.0
839+
840+
@test is_binary(z)
841+
@test lower_bound(z) == 0.1
842+
@test upper_bound(z) == 0.6
843+
844+
@test is_integer(a)
845+
@test !has_lower_bound(a)
846+
@test !has_upper_bound(a)
847+
848+
@test is_integer(b)
849+
@test lower_bound(b) == -1.0
850+
@test upper_bound(b) == 2.0
851+
end
852+
853+
@testset "relax_integrality error cases" begin
854+
model = Model()
855+
@variable(model, x)
856+
@constraint(model, x in MOI.Semicontinuous(1.0, 2.0))
857+
err = ErrorException("Support for relaxing semicontinuous constraints " *
858+
"is not yet implemented.")
859+
@test_throws err relax_integrality(model)
860+
861+
model = Model()
862+
@variable(model, x)
863+
@constraint(model, x in MOI.Semiinteger(1.0, 2.0))
864+
err = ErrorException("Support for relaxing semi-integer constraints " *
865+
"is not yet implemented.")
866+
@test_throws err relax_integrality(model)
867+
end

0 commit comments

Comments
 (0)