Skip to content

Commit 6c94772

Browse files
authored
Add Algorithm attribute and the ::Callback algorithm using lazy constraints (#11)
1 parent 42dd65a commit 6c94772

4 files changed

Lines changed: 128 additions & 4 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,14 @@ model = Model(() -> MathOptLazy.Optimizer(HiGHS.Optimizer))
3535
@variable(model, x[1:10] >= 0)
3636
@constraint(model, [i in 1:10], x[i] <= 1, MathOptLazy.Lazy())
3737
```
38+
39+
## Algorithm
40+
41+
Control the algorithm used to handle the lazy constraints by setting the
42+
`MathOptLazy.Algorithm` attribute. See the docstring for details. The supoprted
43+
values are:
44+
45+
* `MathOptLazy.Iterative()` [default]
46+
* `MathOptLazy.Callback()`
47+
48+
See their docstrings for details.

src/MathOptLazy.jl

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,46 @@ struct _LazyData{F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet}
8282
end
8383
end
8484

85+
### Algorithm
86+
87+
"""
88+
Algorithm() <: MOI.AbstractOptimizerAttribute
89+
90+
An `MOI.AbstractOptimizerAttribute` to control which algorithm we use to solve
91+
the lazy constraints.
92+
93+
Supported values are
94+
95+
* `Iterative()` [default]
96+
* `Callback()`
97+
"""
98+
struct Algorithm <: MOI.AbstractOptimizerAttribute end
99+
100+
abstract type AbstractAlgorithm end
101+
102+
"""
103+
Iterative()
104+
105+
This algorithm iteratively solves a sequence of problems that iteratively add
106+
violated lazy constraints to the main problem.
107+
108+
This algorithm works for all problem types, including continuous problems with
109+
no discrete variables. The downside is that it may not re-use information
110+
between solves.
111+
"""
112+
struct Iterative <: AbstractAlgorithm end
113+
114+
"""
115+
Callback()
116+
117+
This algorithm uses a `MOI.LazyConstraintCallback` to add violated laz
118+
constraints to the main problem.
119+
120+
This algorithm works only for problems with discrete variables and only if the
121+
solver supports `MOI.LazyConstraintCallback`.
122+
"""
123+
struct Callback <: AbstractAlgorithm end
124+
85125
### Optimizer
86126

87127
"""
@@ -112,17 +152,34 @@ MathOptLazy.Optimizer{Float64, MOIB.LazyBridgeOptimizer{HiGHS.Optimizer}}
112152
└ NumberOfConstraints: 0
113153
```
114154
"""
115-
struct Optimizer{OT} <: MOI.AbstractOptimizer
155+
mutable struct Optimizer{OT<:MOI.ModelLike} <: MOI.AbstractOptimizer
116156
inner::OT
117-
157+
algorithm::AbstractAlgorithm
118158
lazy::Dict{Tuple{Type,Type},_LazyData}
119159

120160
function Optimizer(inner_fn; kwargs...)
121161
inner = MOI.instantiate(inner_fn; kwargs...)
122-
return new{typeof(inner)}(inner, Dict{Tuple{Type,Type},_LazyData}())
162+
return new{typeof(inner)}(
163+
inner,
164+
Iterative(),
165+
Dict{Tuple{Type,Type},_LazyData}(),
166+
)
123167
end
124168
end
125169

170+
### Algorithm
171+
172+
MOI.supports(::Optimizer, ::Algorithm) = true
173+
174+
MOI.get(model::Optimizer, ::Algorithm) = model.algorithm
175+
176+
function MOI.set(model::Optimizer, ::Algorithm, value::AbstractAlgorithm)
177+
model.algorithm = value
178+
return
179+
end
180+
181+
MOI.Utilities.map_indices(::Function, algorithm::AbstractAlgorithm) = algorithm
182+
126183
### Fallbacks
127184

128185
function MOI.empty!(model::Optimizer)
@@ -407,7 +464,9 @@ end
407464

408465
### MOI.optimize!
409466

410-
function MOI.optimize!(model::Optimizer)
467+
MOI.optimize!(model::Optimizer) = _optimize!(model, model.algorithm)
468+
469+
function _optimize!(model::Optimizer, ::Iterative)
411470
needs_solve = true
412471
x = MOI.get(model, MOI.ListOfVariableIndices())
413472
# TODO(odow): if the solver supports VariablePrimalStart, we will update the
@@ -481,4 +540,34 @@ function _add_if_feasible(
481540
return needs_solve
482541
end
483542

543+
function _optimize!(model::Optimizer, ::Callback)
544+
function callback(cb_data)
545+
x = MOI.get(model, MOI.ListOfVariableIndices())
546+
X = Dict(
547+
xi => MOI.get(model.inner, MOI.CallbackVariablePrimal(cb_data), xi) for xi in x
548+
)
549+
# We don't check `.is_active` in this loop because callbacks are weird.
550+
# In some solvers, callbacks may be called at a point that was
551+
# previously cut off because the added cut was later removed. The only
552+
# guarantee is that the solver won't terminate until this loop produces
553+
# no new cuts.
554+
for data in values(model.lazy)
555+
for (i, (f, s)) in enumerate(data.data)
556+
y = MOI.Utilities.eval_variables(
557+
Base.Fix1(getindex, X),
558+
model.inner,
559+
f,
560+
)
561+
if MOI.Utilities.distance_to_set(y, s) > 0
562+
MOI.submit(model.inner, MOI.LazyConstraint(cb_data), f, s)
563+
end
564+
end
565+
end
566+
return
567+
end
568+
MOI.set(model.inner, MOI.LazyConstraintCallback(), callback)
569+
MOI.optimize!(model.inner)
570+
return
571+
end
572+
484573
end # module MathOptLazy

test/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
[deps]
2+
GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6"
23
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
34
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
45
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
56
MathOptLazy = "5d5fe9b5-b0a4-4485-81f6-7b1b939155e1"
67
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
78

89
[compat]
10+
GLPK = "1"
911
HiGHS = "1"

test/runtests.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module TestMathOptLazy
88
using JuMP
99
using Test
1010

11+
import GLPK
1112
import HiGHS
1213
import MathOptInterface as MOI
1314
import MathOptLazy
@@ -203,6 +204,27 @@ function test_lazy_bounds_knapsack()
203204
return
204205
end
205206

207+
function test_jump_glpk_callback()
208+
N = 10
209+
model = Model(() -> MathOptLazy.Optimizer(GLPK.Optimizer))
210+
opt = unsafe_backend(model)
211+
@test MOI.supports(opt, MathOptLazy.Algorithm())
212+
@test MOI.get(opt, MathOptLazy.Algorithm()) == MathOptLazy.Iterative()
213+
set_attribute(model, MathOptLazy.Algorithm(), MathOptLazy.Callback())
214+
@test MOI.get(opt, MathOptLazy.Algorithm()) == MathOptLazy.Callback()
215+
set_silent(model)
216+
@variable(model, x[1:N] >= 0, Int)
217+
@constraint(model, c[i in 1:N], x[i] <= 1, MathOptLazy.Lazy())
218+
@test endswith(sprint(show, c[1]), " [lazy]")
219+
@constraint(model, sum(abs(cos(i)) * x[i] for i in 1:N) <= 0.1 * N)
220+
@objective(model, Max, sum(abs(sin(i)) * x[i] for i in 1:N))
221+
optimize!(model)
222+
@test termination_status(model) == OPTIMAL
223+
@test primal_status(model) == FEASIBLE_POINT
224+
@test all(<=(1 + 1e-6), value(x))
225+
return
226+
end
227+
206228
end # TestMathOptLazy
207229

208230
TestMathOptLazy.runtests()

0 commit comments

Comments
 (0)