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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- uses: julia-actions/cache@v3
- shell: bash
run: echo "${{ secrets.GUROBI_LICENSE }}" > ~/gurobi.lic
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
Expand Down
3 changes: 3 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ authors = ["Oscar Dowson <o.dowson@gmail.com>"]
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"

[weakdeps]
Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"

[extensions]
MathOptLazyGurobiExt = "Gurobi"
MathOptLazyJuMPExt = "JuMP"

[compat]
Gurobi = "1"
JuMP = "1"
MathOptInterface = "1"
julia = "1.10"
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ using JuMP
import HiGHS
import MathOptLazy
model = Model(() -> MathOptLazy.Optimizer(HiGHS.Optimizer))
set_attribute(model, MathOptLazy.Algorithm(), MathOptLazy.Iterative())
@variable(model, x[1:10] >= 0)
@constraint(model, [i in 1:10], x[i] <= 1, MathOptLazy.Lazy())
```
Expand All @@ -44,5 +45,6 @@ values are:

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

See their docstrings for details.
37 changes: 37 additions & 0 deletions ext/MathOptLazyGurobiExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright (c) 2026 Oscar Dowson, and contributors
#
# Use of this source code is governed by an MIT-style license that can be found
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

module MathOptLazyGurobiExt

import Gurobi
import MathOptInterface as MOI
import MathOptLazy

function _add_constraints!(
model::MathOptLazy.Optimizer{Gurobi.Optimizer},
data::MathOptLazy._LazyData,
)
for (i, (f, s)) in enumerate(data.data)
if !data.active[i]
c = data.index[i] = MOI.add_constraint(model.inner, f, s)
MOI.set(model.inner, Gurobi.ConstraintAttribute("Lazy"), c, 1)
data.active[i] = true
end
end
return
end

function MathOptLazy._optimize!(
model::MathOptLazy.Optimizer{Gurobi.Optimizer},
::MathOptLazy.SolverSpecific,
)
for data in values(model.lazy)
_add_constraints!(model, data)
end
MOI.optimize!(model.inner)
return
end

end # module MathOptLazyJuMPExt
42 changes: 40 additions & 2 deletions src/MathOptLazy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ Supported values are

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

Expand All @@ -114,14 +115,25 @@ struct Iterative <: AbstractAlgorithm end
"""
Callback()

This algorithm uses a `MOI.LazyConstraintCallback` to add violated laz
constraints to the main problem.
This algorithm uses a `MOI.LazyConstraintCallback` to add violated lazy
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

"""
SolverSpecific()

This algorithm uses a solver-specific extension to add lazy constraints.

The following solvers are supported:

* `Gurobi.jl`
"""
struct SolverSpecific <: AbstractAlgorithm end

### Optimizer

"""
Expand Down Expand Up @@ -570,4 +582,30 @@ function _optimize!(model::Optimizer, ::Callback)
return
end

function _optimize!(model::Optimizer{T}, ::SolverSpecific) where {T}
return error(
"""
The `SolverSpecific` algorithm is not supported by the current solver.

The current solver type is: `$T`

The supported solvers are:

* `Gurobi.Optimizer`

## Example

```julia
import Gurobi
import MathOptInterface as MOI
import MathOptLazy
optimizer = MOI.OptimizerWithAttributes(
() -> MathOptLazy.Optimizer(Gurobi.Optimizer),
MathOptLazy.Algorithm() => MathOptLazy.SolverSpecific(),
)
```
""",
)
end

end # module MathOptLazy
1 change: 1 addition & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[deps]
GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6"
Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b"
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
Expand Down
85 changes: 85 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ using JuMP
using Test

import GLPK
import Gurobi
import HiGHS
import MathOptInterface as MOI
import MathOptLazy
Expand Down Expand Up @@ -62,6 +63,7 @@ end

function test_jump_broadcast()
model = Model(() -> MathOptLazy.Optimizer(HiGHS.Optimizer))
set_silent(model)
@variable(model, x[1:3])
c = @constraint(model, x .<= 1:3, MathOptLazy.Lazy())
@test c isa Vector && length(c) == 3
Expand All @@ -74,6 +76,7 @@ end

function test_jump_direct_basics()
model = direct_model(MathOptLazy.Optimizer(HiGHS.Optimizer))
set_silent(model)
@variable(model, x)
c = @constraint(model, x <= 1, MathOptLazy.Lazy())
o = constraint_object(c)
Expand All @@ -88,6 +91,7 @@ function _basic_constraint_test_helper(
activate::Bool,
)
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOI.Silent(), true)
config = MOI.Test.Config()
set = MathOptLazy.LazyScalarSet(inner_set)
N = MOI.dimension(set)
Expand Down Expand Up @@ -165,6 +169,7 @@ end

function test_writing_mof_file()
src = MathOptLazy.Optimizer(HiGHS.Optimizer)
MOI.set(src, MOI.Silent(), true)
x = MOI.add_variable(src)
c = MOI.add_constraint(src, x, MathOptLazy.LazyScalarSet(MOI.ZeroOne()))
dest = MOI.FileFormats.MOF.Model()
Expand All @@ -177,6 +182,7 @@ end

function test_lazy_bounds()
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variable(model)
set = MathOptLazy.LazyScalarSet(MOI.GreaterThan(0.0))
MOI.add_constraint(model, x, set)
Expand All @@ -191,6 +197,7 @@ end

function test_lazy_bounds_knapsack()
model = MathOptLazy.Optimizer(HiGHS.Optimizer)
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variables(model, 22)
set = MathOptLazy.LazyScalarSet(MOI.GreaterThan(0.0))
MOI.add_constraint.(model, x, set)
Expand Down Expand Up @@ -225,6 +232,84 @@ function test_jump_glpk_callback()
return
end

function test_gurobi_solver_specific()
N = 10
model = MathOptLazy.Optimizer(Gurobi.Optimizer)
MOI.set(model, MathOptLazy.Algorithm(), MathOptLazy.SolverSpecific())
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variables(model, N)
MOI.add_constraint.(model, x, MOI.Integer())
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
MOI.add_constraint.(
model,
1.0 .* x,
MathOptLazy.LazyScalarSet(MOI.LessThan(1.0)),
)
MOI.add_constraint(
model,
sum(abs(cos(i)) * x[i] for i in 1:N),
MOI.LessThan(0.1 * N),
)
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
f = sum(abs(sin(i)) * x[i] for i in 1:N)
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
MOI.optimize!(model)
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL
@test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
@test all(<=(1 + 1e-6), MOI.get(model, MOI.VariablePrimal(), x))
return
end

function test_glpk_solver_specific()
N = 10
model = MathOptLazy.Optimizer(GLPK.Optimizer)
MOI.set(model, MathOptLazy.Algorithm(), MathOptLazy.SolverSpecific())
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variables(model, N)
MOI.add_constraint.(model, x, MOI.Integer())
MOI.add_constraint.(model, x, MOI.GreaterThan(0.0))
MOI.add_constraint.(
model,
1.0 .* x,
MathOptLazy.LazyScalarSet(MOI.LessThan(1.0)),
)
MOI.add_constraint(
model,
sum(abs(cos(i)) * x[i] for i in 1:N),
MOI.LessThan(0.1 * N),
)
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
f = sum(abs(sin(i)) * x[i] for i in 1:N)
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
@test_throws(
ErrorException(
"""
The `SolverSpecific` algorithm is not supported by the current solver.

The current solver type is: `$(GLPK.Optimizer)`

The supported solvers are:

* `Gurobi.Optimizer`

## Example

```julia
import Gurobi
import MathOptInterface as MOI
import MathOptLazy
optimizer = MOI.OptimizerWithAttributes(
() -> MathOptLazy.Optimizer(Gurobi.Optimizer),
MathOptLazy.Algorithm() => MathOptLazy.SolverSpecific(),
)
```
""",
),
MOI.optimize!(model),
)
return
end

end # TestMathOptLazy

TestMathOptLazy.runtests()
Loading