Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 44 additions & 10 deletions src/MathOptLazy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,15 @@ mutable struct Optimizer{OT<:MOI.ModelLike} <: MOI.AbstractOptimizer
inner::OT
algorithm::AbstractAlgorithm
lazy::Dict{Tuple{Type,Type},_LazyData}
silent::Bool

function Optimizer(inner_fn; kwargs...)
inner = MOI.instantiate(inner_fn; kwargs...)
return new{typeof(inner)}(
inner,
Iterative(),
Dict{Tuple{Type,Type},_LazyData}(),
false,
)
end
end
Expand All @@ -220,6 +222,20 @@ end

MOI.Utilities.map_indices(::Function, algorithm::AbstractAlgorithm) = algorithm

### MOI.Silent

MOI.supports(::Optimizer, ::MOI.Silent) = true

MOI.get(model::Optimizer, ::MOI.Silent) = model.silent

function MOI.set(model::Optimizer, ::MOI.Silent, value::Bool)
model.silent = value
if MOI.supports(model.inner, MOI.Silent())
MOI.set(model.inner, MOI.Silent(), value)
end
return
end

### Fallbacks

function MOI.empty!(model::Optimizer)
Expand Down Expand Up @@ -544,17 +560,25 @@ end

MOI.optimize!(model::Optimizer) = _optimize!(model, model.algorithm)

### MathOptLazy.Iterative

function _optimize!(model::Optimizer, ::Iterative)
if (undo = _relax_integrality(model.inner)) !== nothing
if !model.silent
println("[MathOptLazy] relaxing binary and integer variables")
end
_iterate(model; start = false)
if !model.silent
println("[MathOptLazy] re-enforcing binary and integer variables")
end
undo()
end
_iterate(model; start = true)
return
end

function _iterate(model::Optimizer; start::Bool)
needs_solve = true
constraints_added = 1 # A white lie to enter the iteration loop
x = MOI.get(model, MOI.ListOfVariableIndices())
# TODO(odow): if the solver supports VariablePrimalStart, we will update the
# primal starts during the solve process. This is destructive and in-place.
Expand All @@ -564,26 +588,32 @@ function _iterate(model::Optimizer; start::Bool)
# ::Optimizer, but this is a hassle and no one probably cares. Revisit this
# decision if it ever becomes a problem.
start &= MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex)
while needs_solve
needs_solve = false
while constraints_added > 0
constraints_added = 0
if !model.silent
println("[MathOptLazy] solving current subproblem\n")
end
MOI.optimize!(model.inner)
if MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE
# The problem is unbounded, but it might not be if we add more
# constraints.
for v in values(model.lazy)
needs_solve |= _add_if_unbounded(model, v)
constraints_added += _add_if_unbounded(model, v)
end
elseif MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
X = Dict(xi => MOI.get(model, MOI.VariablePrimal(), xi) for xi in x)
for v in values(model.lazy)
needs_solve |= _add_if_feasible(model, v, X)
constraints_added += _add_if_feasible(model, v, X)
end
if start && needs_solve
if start && constraints_added > 0
for (xi, v) in X
MOI.set(model, MOI.VariablePrimalStart(), xi, v)
end
end
end
if !model.silent
println("\n[MathOptLazy] added $(constraints_added) constraints")
end
end
return
end
Expand Down Expand Up @@ -707,15 +737,15 @@ function _add_if_unbounded(model::Optimizer, data::_LazyData)
constraints_added += 1
end
end
return constraints_added > 0
return constraints_added
end

function _add_if_feasible(
model::Optimizer,
data::_LazyData,
x::Dict{MOI.VariableIndex},
)
needs_solve = false
constraints_added = 0
for (i, (f, s)) in enumerate(data.data)
if data.status[i] != _kLAZY_CONSTRAINT_INACTIVE
continue
Expand All @@ -724,12 +754,14 @@ function _add_if_feasible(
if MOI.Utilities.distance_to_set(y, s) > 0
data.index[i] = MOI.add_constraint(model.inner, f, s)
data.status[i] = _kLAZY_CONSTRAINT_ACTIVE
needs_solve = true
constraints_added += 1
end
end
return needs_solve
return constraints_added
end

### MathOptLazy.Callback

function _optimize!(model::Optimizer, ::Callback)
function callback(cb_data)
x = MOI.get(model, MOI.ListOfVariableIndices())
Expand Down Expand Up @@ -760,6 +792,8 @@ function _optimize!(model::Optimizer, ::Callback)
return
end

### MathOptLazy.SolverSpecific

function _optimize!(model::Optimizer{T}, ::SolverSpecific) where {T}
return error(
"""
Expand Down
59 changes: 59 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ end

function test_delete_bound()
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variable(model)
c1 = MOI.add_constraint(model, x, MOI.ZeroOne())
@test MOI.is_valid(model, c1)
Expand Down Expand Up @@ -423,6 +424,7 @@ end

function test_empty()
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variable(model)
set = MathOptLazy.LazyScalarSet(MOI.EqualTo(1.0))
c = MOI.add_constraint(model, 1.0 * x, set)
Expand All @@ -438,6 +440,7 @@ end

function test_is_valid()
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variable(model)
set = MathOptLazy.LazyScalarSet(MOI.EqualTo(1.0))
c = MOI.add_constraint(model, 1.0 * x, set)
Expand All @@ -453,6 +456,7 @@ end

function test_list_of_constraint_attributes_set()
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variables(model, 2)
set = MathOptLazy.LazyScalarSet(MOI.EqualTo(1.0))
c = MOI.add_constraint(model, 1.0 * x[1], set)
Expand All @@ -468,11 +472,13 @@ end

function test_relax_integrality_equal_to()
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variable(model)
c_z = MOI.add_constraint(model, x, MOI.ZeroOne())
MOI.add_constraint(model, x, MOI.EqualTo(0.5))
undo = MathOptLazy._relax_integrality(model.inner)
target = HiGHS.Optimizer()
MOI.set(target, MOI.Silent(), true)
y, _ = MOI.add_constrained_variable(target, MOI.EqualTo(0.5))
@test sprint(print, model.inner) == sprint(print, target)
undo()
Expand All @@ -484,11 +490,13 @@ end

function test_relax_integrality_interval()
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variable(model)
c_z = MOI.add_constraint(model, x, MOI.ZeroOne())
MOI.add_constraint(model, x, MOI.Interval(-1.0, 2.0))
undo = MathOptLazy._relax_integrality(model.inner)
target = HiGHS.Optimizer()
MOI.set(target, MOI.Silent(), true)
y, cy = MOI.add_constrained_variable(target, MOI.Interval(0.0, 1.0))
@test sprint(print, model.inner) == sprint(print, target)
undo()
Expand All @@ -501,10 +509,12 @@ end

function test_relax_integrality_no_bound()
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variable(model)
c_z = MOI.add_constraint(model, x, MOI.ZeroOne())
undo = MathOptLazy._relax_integrality(model.inner)
target = HiGHS.Optimizer()
MOI.set(target, MOI.Silent(), true)
y = MOI.add_variable(target)
c_l = MOI.add_constraint(target, y, MOI.GreaterThan(0.0))
c_u = MOI.add_constraint(target, y, MOI.LessThan(1.0))
Expand All @@ -520,11 +530,13 @@ end

function test_relax_integrality_greater_than()
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variable(model)
MOI.add_constraint(model, x, MOI.GreaterThan(-0.5))
c_z = MOI.add_constraint(model, x, MOI.ZeroOne())
undo = MathOptLazy._relax_integrality(model.inner)
target = HiGHS.Optimizer()
MOI.set(target, MOI.Silent(), true)
y, cy = MOI.add_constrained_variable(target, MOI.GreaterThan(0.0))
c_u = MOI.add_constraint(target, y, MOI.LessThan(1.0))
@test sprint(print, model.inner) == sprint(print, target)
Expand All @@ -539,11 +551,13 @@ end

function test_relax_integrality_less_than()
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variable(model)
MOI.add_constraint(model, x, MOI.LessThan(1.5))
c_z = MOI.add_constraint(model, x, MOI.ZeroOne())
undo = MathOptLazy._relax_integrality(model.inner)
target = HiGHS.Optimizer()
MOI.set(target, MOI.Silent(), true)
y, cy = MOI.add_constrained_variable(target, MOI.LessThan(1.0))
c_l = MOI.add_constraint(target, y, MOI.GreaterThan(0.0))
@test sprint(print, model.inner) == sprint(print, target)
Expand All @@ -558,11 +572,13 @@ end

function test_relax_integrality_semi_integer()
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variable(model)
c_z = MOI.add_constraint(model, x, MOI.ZeroOne())
t, _ = MOI.add_constrained_variable(model, MOI.Semiinteger(3.0, 4.0))
undo = MathOptLazy._relax_integrality(model.inner)
target = HiGHS.Optimizer()
MOI.set(target, MOI.Silent(), true)
y = MOI.add_variable(target)
c_l = MOI.add_constraint(target, y, MOI.GreaterThan(0.0))
c_u = MOI.add_constraint(target, y, MOI.LessThan(1.0))
Expand All @@ -577,6 +593,49 @@ function test_relax_integrality_semi_integer()
return
end

function test_print_log()
N = 10
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
@test MOI.supports(model, MOI.Silent())
@test MOI.get(model, MOI.Silent()) == false
MOI.set(model, MOI.Silent(), true)
@test MOI.get(model, MOI.Silent()) == true
MOI.set(model, MOI.Silent(), false)
@test MOI.get(model, MOI.Silent()) == false
x = MOI.add_variables(model, N)
for i in 1:N
MOI.add_constraint(model, x[i], MOI.GreaterThan(0.0))
MOI.add_constraint(model, x[i], MOI.Integer())
set = MathOptLazy.LazyScalarSet(MOI.LessThan(1.0))
MOI.add_constraint(model, 1.0 * x[i], set)
end
f = sum(abs(cos(i)) * x[i] for i in 1:N)
MOI.add_constraint(model, f, MOI.LessThan(1.0))
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
g = sum(abs(sin(i)) * x[i] for i in 1:N)
MOI.set(model, MOI.ObjectiveFunction{typeof(g)}(), g)
dir = mktempdir()
open(joinpath(dir, "out.log"), "w") do io
return redirect_stdout(() -> MOI.optimize!(model), io)
end
contents = read(joinpath(dir, "out.log"), String)
for line in [
"[MathOptLazy] relaxing binary and integer variables",
"[MathOptLazy] solving current subproblem",
"[MathOptLazy] added 1 constraints",
"[MathOptLazy] added 0 constraints",
"[MathOptLazy] re-enforcing binary and integer variables",
]
@test occursin(line, contents)
end
MOI.set(model, MOI.Silent(), true)
open(joinpath(dir, "out.log"), "w") do io
return redirect_stdout(() -> MOI.optimize!(model), io)
end
@test isempty(read(joinpath(dir, "out.log"), String))
return
end

end # TestMathOptLazy

TestMathOptLazy.runtests()
Loading