Skip to content

Commit 930c43e

Browse files
committed
Fix support for models that are unbounded without lazy constraints
1 parent 3a23fcf commit 930c43e

2 files changed

Lines changed: 60 additions & 8 deletions

File tree

src/MathOptLazy.jl

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -421,13 +421,17 @@ function MOI.optimize!(model::Optimizer)
421421
while needs_solve
422422
needs_solve = false
423423
MOI.optimize!(model.inner)
424-
if MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
424+
if MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE
425+
# The problem is unbounded, but it might not be if we add more
426+
# constraints.
427+
for v in values(model.lazy)
428+
needs_solve |= _add_if_unbounded(model, v)
429+
end
430+
elseif MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
425431
X = Dict(xi => MOI.get(model, MOI.VariablePrimal(), xi) for xi in x)
426-
constraints_added = 0
427432
for v in values(model.lazy)
428-
constraints_added += _add_if_necessary(model, v, X)
433+
needs_solve |= _add_if_feasible(model, v, X)
429434
end
430-
needs_solve = constraints_added > 0
431435
if start && needs_solve
432436
for (xi, v) in X
433437
MOI.set(model, MOI.VariablePrimalStart(), xi, v)
@@ -438,12 +442,31 @@ function MOI.optimize!(model::Optimizer)
438442
return
439443
end
440444

441-
function _add_if_necessary(
445+
function _add_if_unbounded(model::Optimizer, data::_LazyData)
446+
# Strategy: add 1/3 of the total constraints. This is arbitrary. If a model
447+
# is unbounded with lazy constraints, it's not a great model, and it has a
448+
# high likelihood that it requires _all_ the constraints to be added. I'm
449+
# imagining something like 0 <= x <= Lazy(1) in a knapsack problem.
450+
n = div(length(data.data), 3, RoundUp)
451+
constraints_added = 0
452+
for (i, (f, s)) in enumerate(data.data)
453+
if constraints_added >= n
454+
break
455+
elseif !data.active[i]
456+
data.index[i] = MOI.add_constraint(model.inner, f, s)
457+
data.active[i] = true
458+
constraints_added += 1
459+
end
460+
end
461+
return constraints_added > 0
462+
end
463+
464+
function _add_if_feasible(
442465
model::Optimizer,
443466
data::_LazyData,
444467
x::Dict{MOI.VariableIndex},
445468
)
446-
constraints_added = 0
469+
needs_solve = false
447470
for (i, (f, s)) in enumerate(data.data)
448471
if data.active[i]
449472
continue
@@ -452,10 +475,10 @@ function _add_if_necessary(
452475
if MOI.Utilities.distance_to_set(y, s) > 0
453476
data.index[i] = MOI.add_constraint(model.inner, f, s)
454477
data.active[i] = true
455-
constraints_added += 1
478+
needs_solve = true
456479
end
457480
end
458-
return constraints_added
481+
return needs_solve
459482
end
460483

461484
end # module MathOptLazy

test/runtests.jl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,35 @@ function test_writing_mof_file()
174174
return
175175
end
176176

177+
function test_lazy_bounds()
178+
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
179+
x = MOI.add_variable(model)
180+
set = MathOptLazy.LazyScalarSet(MOI.GreaterThan(0.0))
181+
MOI.add_constraint(model, x, set)
182+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
183+
f = 1.0 * x
184+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
185+
MOI.optimize!(model)
186+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL
187+
@test MOI.get(model, MOI.VariablePrimal(), x) == 0.0
188+
return
189+
end
190+
191+
function test_lazy_bounds_knapsack()
192+
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
193+
x = MOI.add_variables(model, 22)
194+
set = MathOptLazy.LazyScalarSet(MOI.GreaterThan(0.0))
195+
MOI.add_constraint.(model, x, set)
196+
set = MathOptLazy.LazyScalarSet(MOI.LessThan(1.0))
197+
MOI.add_constraint.(model, x, set)
198+
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
199+
f = rand(22)' * x
200+
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
201+
MOI.optimize!(model)
202+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL
203+
return
204+
end
205+
177206
end # TestMathOptLazy
178207

179208
TestMathOptLazy.runtests()

0 commit comments

Comments
 (0)