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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,14 @@ model = Model(() -> MathOptLazy.Optimizer(HiGHS.Optimizer))
@variable(model, x[1:10] >= 0)
@constraint(model, [i in 1:10], x[i] <= 1, MathOptLazy.Lazy())
```

## Algorithm

Control the algorithm used to handle the lazy constraints by setting the
`MathOptLazy.Algorithm` attribute. See the docstring for details. The supoprted
values are:

* `MathOptLazy.Iterative()` [default]
* `MathOptLazy.Callback()`

See their docstrings for details.
97 changes: 93 additions & 4 deletions src/MathOptLazy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,46 @@ struct _LazyData{F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet}
end
end

### Algorithm

"""
Algorithm() <: MOI.AbstractOptimizerAttribute

An `MOI.AbstractOptimizerAttribute` to control which algorithm we use to solve
the lazy constraints.

Supported values are

* `Iterative()` [default]
* `Callback()`
"""
struct Algorithm <: MOI.AbstractOptimizerAttribute end

abstract type AbstractAlgorithm end

"""
Iterative()

This algorithm iteratively solves a sequence of problems that iteratively add
violated lazy constraints to the main problem.

This algorithm works for all problem types, including continuous problems with
no discrete variables. The downside is that it may not re-use information
between solves.
"""
struct Iterative <: AbstractAlgorithm end

"""
Callback()

This algorithm uses a `MOI.LazyConstraintCallback` to add violated laz
constraints to the main problem.

This algorithm works only for problems with discrete variables and only if the
solver supports `MOI.LazyConstraintCallback`.
"""
struct Callback <: AbstractAlgorithm end

### Optimizer

"""
Expand Down Expand Up @@ -112,17 +152,34 @@ MathOptLazy.Optimizer{Float64, MOIB.LazyBridgeOptimizer{HiGHS.Optimizer}}
└ NumberOfConstraints: 0
```
"""
struct Optimizer{OT} <: MOI.AbstractOptimizer
mutable struct Optimizer{OT<:MOI.ModelLike} <: MOI.AbstractOptimizer
inner::OT

algorithm::AbstractAlgorithm
lazy::Dict{Tuple{Type,Type},_LazyData}

function Optimizer(inner_fn; kwargs...)
inner = MOI.instantiate(inner_fn; kwargs...)
return new{typeof(inner)}(inner, Dict{Tuple{Type,Type},_LazyData}())
return new{typeof(inner)}(
inner,
Iterative(),
Dict{Tuple{Type,Type},_LazyData}(),
)
end
end

### Algorithm

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

MOI.get(model::Optimizer, ::Algorithm) = model.algorithm

function MOI.set(model::Optimizer, ::Algorithm, value::AbstractAlgorithm)
model.algorithm = value
return
end

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

### Fallbacks

function MOI.empty!(model::Optimizer)
Expand Down Expand Up @@ -407,7 +464,9 @@ end

### MOI.optimize!

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

function _optimize!(model::Optimizer, ::Iterative)
needs_solve = true
x = MOI.get(model, MOI.ListOfVariableIndices())
# TODO(odow): if the solver supports VariablePrimalStart, we will update the
Expand Down Expand Up @@ -481,4 +540,34 @@ function _add_if_feasible(
return needs_solve
end

function _optimize!(model::Optimizer, ::Callback)
function callback(cb_data)
x = MOI.get(model, MOI.ListOfVariableIndices())
X = Dict(
xi => MOI.get(model.inner, MOI.CallbackVariablePrimal(cb_data), xi) for xi in x
)
# We don't check `.is_active` in this loop because callbacks are weird.
# In some solvers, callbacks may be called at a point that was
# previously cut off because the added cut was later removed. The only
# guarantee is that the solver won't terminate until this loop produces
# no new cuts.
for data in values(model.lazy)
for (i, (f, s)) in enumerate(data.data)
y = MOI.Utilities.eval_variables(
Base.Fix1(getindex, X),
model.inner,
f,
)
if MOI.Utilities.distance_to_set(y, s) > 0
MOI.submit(model.inner, MOI.LazyConstraint(cb_data), f, s)
end
end
end
return
end
MOI.set(model.inner, MOI.LazyConstraintCallback(), callback)
MOI.optimize!(model.inner)
return
end

end # module MathOptLazy
2 changes: 2 additions & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
[deps]
GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6"
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
MathOptLazy = "5d5fe9b5-b0a4-4485-81f6-7b1b939155e1"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[compat]
GLPK = "1"
HiGHS = "1"
22 changes: 22 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module TestMathOptLazy
using JuMP
using Test

import GLPK
import HiGHS
import MathOptInterface as MOI
import MathOptLazy
Expand Down Expand Up @@ -203,6 +204,27 @@ function test_lazy_bounds_knapsack()
return
end

function test_jump_glpk_callback()
N = 10
model = Model(() -> MathOptLazy.Optimizer(GLPK.Optimizer))
opt = unsafe_backend(model)
@test MOI.supports(opt, MathOptLazy.Algorithm())
@test MOI.get(opt, MathOptLazy.Algorithm()) == MathOptLazy.Iterative()
set_attribute(model, MathOptLazy.Algorithm(), MathOptLazy.Callback())
@test MOI.get(opt, MathOptLazy.Algorithm()) == MathOptLazy.Callback()
set_silent(model)
@variable(model, x[1:N] >= 0, Int)
@constraint(model, c[i in 1:N], x[i] <= 1, MathOptLazy.Lazy())
@test endswith(sprint(show, c[1]), " [lazy]")
@constraint(model, sum(abs(cos(i)) * x[i] for i in 1:N) <= 0.1 * N)
@objective(model, Max, sum(abs(sin(i)) * x[i] for i in 1:N))
optimize!(model)
@test termination_status(model) == OPTIMAL
@test primal_status(model) == FEASIBLE_POINT
@test all(<=(1 + 1e-6), value(x))
return
end

end # TestMathOptLazy

TestMathOptLazy.runtests()
Loading