Skip to content

Commit 0490bea

Browse files
bfpcodow
authored andcommitted
Enforce [0,1] bounds on binary variables for LP relaxation (#236)
1 parent 1e88935 commit 0490bea

File tree

2 files changed

+158
-14
lines changed

2 files changed

+158
-14
lines changed

src/algorithm.jl

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,48 @@ function termination_status(model::PolicyGraph)
686686
end
687687

688688
"""
689-
relax_integrality(model::PolicyGraph)::NTuple{Vector{VariableRef}, 2}
689+
relax_integrality(model::JuMP.Model)
690+
691+
Relax all binary and integer constraints in `model`.
692+
Returns two vectors:
693+
the first containing a list of binary variables and previous bounds,
694+
and the second containing a list of integer variables.
695+
696+
See also [`enforce_integrality`](@ref).
697+
"""
698+
function relax_integrality(m::JuMP.Model)
699+
# Note: if integrality restriction is added via @constraint then this loop doesn't catch it.
700+
binaries = Tuple{JuMP.VariableRef, Float64, Float64}[]
701+
integers = JuMP.VariableRef[]
702+
# Run through all variables on model and unset integrality
703+
for x in JuMP.all_variables(m)
704+
if JuMP.is_binary(x)
705+
x_lb, x_ub = NaN, NaN
706+
JuMP.unset_binary(x)
707+
# Store upper and lower bounds
708+
if JuMP.has_lower_bound(x)
709+
x_lb = JuMP.lower_bound(x)
710+
JuMP.set_lower_bound(x, max(x_lb, 0.0))
711+
else
712+
JuMP.set_lower_bound(x, 0.0)
713+
end
714+
if JuMP.has_upper_bound(x)
715+
x_ub = JuMP.upper_bound(x)
716+
JuMP.set_upper_bound(x, min(x_ub, 1.0))
717+
else
718+
JuMP.set_upper_bound(x, 1.0)
719+
end
720+
push!(binaries, (x, x_lb, x_ub))
721+
elseif JuMP.is_integer(x)
722+
JuMP.unset_integer(x)
723+
push!(integers, x)
724+
end
725+
end
726+
return binaries, integers
727+
end
728+
729+
"""
730+
relax_integrality(model::PolicyGraph)
690731
691732
Relax all binary and integer constraints in all subproblems in `model`. Return
692733
two vectors, the first containing a list of binary variables, and the second
@@ -695,35 +736,44 @@ containing a list of integer variables.
695736
See also [`enforce_integrality`](@ref).
696737
"""
697738
function relax_integrality(model::PolicyGraph)
698-
binaries = JuMP.VariableRef[]
739+
binaries = Tuple{JuMP.VariableRef, Float64, Float64}[]
699740
integers = JuMP.VariableRef[]
700741
for (key, node) in model.nodes
701-
for x in JuMP.all_variables(node.subproblem)
702-
if JuMP.is_binary(x)
703-
JuMP.unset_binary(x)
704-
push!(binaries, x)
705-
elseif JuMP.is_integer(x)
706-
JuMP.unset_integer(x)
707-
push!(integers, x)
708-
end
709-
end
742+
bins, ints = relax_integrality(node.subproblem)
743+
append!(binaries, bins)
744+
append!(integers, ints)
710745
end
711746
return binaries, integers
712747
end
713748

714749
"""
715750
enforce_integrality(
716-
binaries::Vector{VariableRef}, integers::Vector{VariableRef})
751+
binaries::Vector{Tuple{JuMP.VariableRef, Float64, Float64}},
752+
integers::Vector{VariableRef})
717753
718754
Set all variables in `binaries` to `SingleVariable-in-ZeroOne()`, and all
719755
variables in `integers` to `SingleVariable-in-Integer()`.
720756
721757
See also [`relax_integrality`](@ref).
722758
"""
723759
function enforce_integrality(
724-
binaries::Vector{VariableRef}, integers::Vector{VariableRef})
760+
binaries::Vector{Tuple{JuMP.VariableRef, Float64, Float64}},
761+
integers::Vector{VariableRef}
762+
)
725763
JuMP.set_integer.(integers)
726-
JuMP.set_binary.(binaries)
764+
for (x, x_lb, x_ub) in binaries
765+
if isnan(x_lb)
766+
JuMP.delete_lower_bound(x)
767+
else
768+
JuMP.set_lower_bound(x, x_lb)
769+
end
770+
if isnan(x_ub)
771+
JuMP.delete_upper_bound(x)
772+
else
773+
JuMP.set_upper_bound(x, x_ub)
774+
end
775+
JuMP.set_binary(x)
776+
end
727777
return
728778
end
729779

test/algorithm.jl

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,97 @@ end
175175
@test pre_optimize_called == 1
176176
@test post_optimize_called == 3
177177
end
178+
179+
@testset "relax_integrality and enforce_integrality" begin
180+
model = SDDP.LinearPolicyGraph(
181+
stages = 2, lower_bound = 0.0,
182+
direct_mode = false
183+
) do sp, t
184+
@variable(sp, x, SDDP.State, initial_value = 2.0)
185+
@variable(sp, b1, Bin)
186+
@variable(sp, 0.2 <= b2, Bin)
187+
@variable(sp, 0.5 <= b3 <= 1.2, Bin)
188+
@variable(sp, i1, Int)
189+
@variable(sp, 6.2 >= i2, Int)
190+
@variable(sp, -8 <= i3 <= 2, Int)
191+
@stageobjective(sp, b1 + b2 + b2 + i3 + i1)
192+
end
193+
194+
for node in [model[1], model[2]]
195+
@test JuMP.is_binary(node.subproblem[:b1])
196+
@test !JuMP.has_lower_bound(node.subproblem[:b1])
197+
@test !JuMP.has_upper_bound(node.subproblem[:b1])
198+
199+
@test JuMP.is_binary(node.subproblem[:b2])
200+
@test JuMP.lower_bound(node.subproblem[:b2]) == 0.2
201+
@test !JuMP.has_upper_bound(node.subproblem[:b2])
202+
203+
@test JuMP.is_binary(node.subproblem[:b3])
204+
@test JuMP.lower_bound(node.subproblem[:b3]) == 0.5
205+
@test JuMP.upper_bound(node.subproblem[:b3]) == 1.2
206+
207+
@test JuMP.is_integer(node.subproblem[:i1])
208+
@test !JuMP.has_lower_bound(node.subproblem[:i1])
209+
@test !JuMP.has_upper_bound(node.subproblem[:i1])
210+
211+
@test JuMP.is_integer(node.subproblem[:i2])
212+
@test JuMP.upper_bound(node.subproblem[:i2]) == 6.2
213+
@test !JuMP.has_lower_bound(node.subproblem[:i2])
214+
215+
@test JuMP.is_integer(node.subproblem[:i3])
216+
@test JuMP.lower_bound(node.subproblem[:i3]) == -8
217+
@test JuMP.upper_bound(node.subproblem[:i3]) == 2
218+
end
219+
binaries, integers = SDDP.relax_integrality(model)
220+
for node in [model[1], model[2]]
221+
@test !JuMP.is_binary(node.subproblem[:b1])
222+
@test JuMP.lower_bound(node.subproblem[:b1]) == 0.0
223+
@test JuMP.upper_bound(node.subproblem[:b1]) == 1.0
224+
225+
@test !JuMP.is_binary(node.subproblem[:b2])
226+
@test JuMP.lower_bound(node.subproblem[:b2]) == 0.2
227+
@test JuMP.upper_bound(node.subproblem[:b2]) == 1.0
228+
229+
@test !JuMP.is_binary(node.subproblem[:b3])
230+
@test JuMP.lower_bound(node.subproblem[:b3]) == 0.5
231+
@test JuMP.upper_bound(node.subproblem[:b3]) == 1.0
232+
233+
@test !JuMP.is_integer(node.subproblem[:i1])
234+
@test !JuMP.has_lower_bound(node.subproblem[:i1])
235+
@test !JuMP.has_upper_bound(node.subproblem[:i1])
236+
237+
@test !JuMP.is_integer(node.subproblem[:i2])
238+
@test JuMP.upper_bound(node.subproblem[:i2]) == 6.2
239+
@test !JuMP.has_lower_bound(node.subproblem[:i2])
240+
241+
@test !JuMP.is_integer(node.subproblem[:i3])
242+
@test JuMP.lower_bound(node.subproblem[:i3]) == -8
243+
@test JuMP.upper_bound(node.subproblem[:i3]) == 2
244+
end
245+
SDDP.enforce_integrality(binaries, integers)
246+
for node in [model[1], model[2]]
247+
@test JuMP.is_binary(node.subproblem[:b1])
248+
@test !JuMP.has_lower_bound(node.subproblem[:b1])
249+
@test !JuMP.has_upper_bound(node.subproblem[:b1])
250+
251+
@test JuMP.is_binary(node.subproblem[:b2])
252+
@test JuMP.lower_bound(node.subproblem[:b2]) == 0.2
253+
@test !JuMP.has_upper_bound(node.subproblem[:b2])
254+
255+
@test JuMP.is_binary(node.subproblem[:b3])
256+
@test JuMP.lower_bound(node.subproblem[:b3]) == 0.5
257+
@test JuMP.upper_bound(node.subproblem[:b3]) == 1.2
258+
259+
@test JuMP.is_integer(node.subproblem[:i1])
260+
@test !JuMP.has_lower_bound(node.subproblem[:i1])
261+
@test !JuMP.has_upper_bound(node.subproblem[:i1])
262+
263+
@test JuMP.is_integer(node.subproblem[:i2])
264+
@test JuMP.upper_bound(node.subproblem[:i2]) == 6.2
265+
@test !JuMP.has_lower_bound(node.subproblem[:i2])
266+
267+
@test JuMP.is_integer(node.subproblem[:i3])
268+
@test JuMP.lower_bound(node.subproblem[:i3]) == -8
269+
@test JuMP.upper_bound(node.subproblem[:i3]) == 2
270+
end
271+
end

0 commit comments

Comments
 (0)