From f36b92f417c1ac8d9194cff0de92db4658b5c37d Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 13 May 2026 10:54:33 +1200 Subject: [PATCH 1/4] More improvements to the MOI API --- src/MathOptLazy.jl | 55 ++++++++++++++++++++++++++++++++++++++++---- test/runtests.jl | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/src/MathOptLazy.jl b/src/MathOptLazy.jl index 331dba1..3d54125 100644 --- a/src/MathOptLazy.jl +++ b/src/MathOptLazy.jl @@ -75,6 +75,10 @@ struct _LazyData{F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} end end +Base.length(x::_LazyData) = length(x.data) + +Base.isempty(x::_LazyData) = isempty(x.data) + ### Optimizer """ @@ -286,7 +290,22 @@ function MOI.is_valid( ci::MOI.ConstraintIndex{F,LazyScalarSet{S}}, ) where {F,S} data = _data(model, F, S) - return data !== nothing && 1 <= ci.value <= length(data.data) + return data !== nothing && 1 <= ci.value <= length(data) +end + +function MOI.get( + model::Optimizer, + ::MOI.ListOfConstraintIndices{F,LazyScalarSet{S}}, +) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} + n = length(_data(model, F, S)) + return [MOI.ConstraintIndex{F,LazyScalarSet{S}}(i) for i in 1:n] +end + +function MOI.get( + model::Optimizer, + ::MOI.NumberOfConstraints{F,LazyScalarSet{S}}, +) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} + return length(_data(model, F, S)) end function MOI.add_constraint( @@ -298,25 +317,53 @@ function MOI.add_constraint( push!(data.data, (f, s.set)) push!(data.active, false) push!(data.index, MOI.ConstraintIndex{F,S}(0)) - return MOI.ConstraintIndex{F,LazyScalarSet{S}}(length(data.data)) + return MOI.ConstraintIndex{F,LazyScalarSet{S}}(length(data)) end function MOI.get( model::Optimizer, ::MOI.ConstraintFunction, ci::MOI.ConstraintIndex{F,LazyScalarSet{S}}, -) where {F,S} +) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} return _data(model, F, S).data[ci.value][1] end +function MOI.get( + model::Optimizer, + ::MOI.CanonicalConstraintFunction, + ci::MOI.ConstraintIndex{F,LazyScalarSet{S}}, +) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} + return MOI.Utilities.canonical(MOI.get(model, MOI.ConstraintFunction(), ci)) +end + function MOI.get( model::Optimizer, ::MOI.ConstraintSet, ci::MOI.ConstraintIndex{F,LazyScalarSet{S}}, -) where {F,S} +) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} return LazyScalarSet(_data(model, F, S).data[ci.value][2]) end +function MOI.get( + model::Optimizer{T}, + ::MOI.ListOfConstraintTypesPresent, +) where {T} + ret = MOI.get(model.inner, MOI.ListOfConstraintTypesPresent()) + if !isempty(model.saf_gt) + push!( + ret, + (MOI.ScalarAffineFunction{T}, LazyScalarSet{MOI.GreaterThan{T}}), + ) + end + if !isempty(model.saf_lt) + push!( + ret, + (MOI.ScalarAffineFunction{T}, LazyScalarSet{MOI.LessThan{T}}), + ) + end + return ret +end + ### MOI.optimize! function MOI.optimize!(model::Optimizer{T}) where {T} diff --git a/test/runtests.jl b/test/runtests.jl index c261e8e..7fb38d0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -68,6 +68,63 @@ function test_jump_direct_basics() return end +function _basic_constraint_test_helper( + function_fn::Function, + set::MOI.AbstractScalarSet, +) + model = MathOptLazy.Optimizer(HiGHS.Optimizer) + config = MOI.Test.Config() + set = MathOptLazy.LazyScalarSet(set) + N = MOI.dimension(set) + x = MOI.add_variables(model, 3) + constraint_function = function_fn(x) + @assert MOI.output_dimension(constraint_function) == N + F, S = typeof(constraint_function), typeof(set) + @test MOI.supports_constraint(model, F, S) + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 0 + c = MOI.add_constraint(model, constraint_function, set) + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 + c_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + @test c_indices == [c] + @test (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) + @test MOI.is_valid(model, c) + @test !MOI.is_valid(model, typeof(c)(c.value + 1)) isa Bool + @test !MOI.is_valid(model, typeof(c)(c.value - 1)) isa Bool + @test !MOI.is_valid(model, typeof(c)(c.value + 12345)) + # Don't compare directly, because `f` might not be canonicalized. + f = MOI.get(model, MOI.ConstraintFunction(), c) + @test isapprox(f, constraint_function, config) + cf = MOI.get(model, MOI.CanonicalConstraintFunction(), c) + @test isapprox(cf, constraint_function, config) + @test MOI.get(model, MOI.ConstraintSet(), c) == set + MOI.add_constraints( + model, + [constraint_function, constraint_function], + [set, set], + ) + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 3 + @test length(MOI.get(model, MOI.ListOfConstraintIndices{F,S}())) == 3 + c_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + @test all(MOI.is_valid.(model, c_indices)) + return +end + +function test_basic_scalaraffinefunction_greaterthan() + _basic_constraint_test_helper( + x -> sum(sin(i) * x[i] for i in 1:length(x)), + MOI.GreaterThan(1.0) + ) + return +end + +function test_basic_scalaraffinefunction_lessthan() + _basic_constraint_test_helper( + x -> sum(sin(i) * x[i] for i in 1:length(x)), + MOI.LessThan(1.0) + ) + return +end + end # TestMathOptLazy TestMathOptLazy.runtests() From 08d885c2552bf2a66f0e531a7b6ace34fe156837 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 13 May 2026 11:17:55 +1200 Subject: [PATCH 2/4] Update --- src/MathOptLazy.jl | 23 +++++++++++++++++++++++ test/runtests.jl | 42 ++++++++++++++++++++++++++++-------------- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/MathOptLazy.jl b/src/MathOptLazy.jl index 3d54125..5a3ff10 100644 --- a/src/MathOptLazy.jl +++ b/src/MathOptLazy.jl @@ -364,6 +364,29 @@ function MOI.get( return ret end +function MOI.get( + model::Optimizer, + attr::MOI.NumberOfConstraints{F,S}, +) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} + n = MOI.get(model.inner, attr) + if (data = _data(model, F, S)) !== nothing + n -= sum(data.active) + end + return n +end + +function MOI.get( + model::Optimizer, + attr::MOI.ListOfConstraintIndices{F,S}, +) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} + ret = MOI.get(model.inner, attr) + if (data = _data(model, F, S)) !== nothing + in_model = Set(ci for (ci, z) in zip(data.index, data.active) if z) + ret = filter!(ci -> !(ci in in_model), ret) + end + return ret +end + ### MOI.optimize! function MOI.optimize!(model::Optimizer{T}) where {T} diff --git a/test/runtests.jl b/test/runtests.jl index 7fb38d0..b6cebe8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -70,22 +70,32 @@ end function _basic_constraint_test_helper( function_fn::Function, - set::MOI.AbstractScalarSet, + inner_set::MOI.AbstractScalarSet; + activate::Bool, ) model = MathOptLazy.Optimizer(HiGHS.Optimizer) config = MOI.Test.Config() - set = MathOptLazy.LazyScalarSet(set) + set = MathOptLazy.LazyScalarSet(inner_set) N = MOI.dimension(set) x = MOI.add_variables(model, 3) constraint_function = function_fn(x) - @assert MOI.output_dimension(constraint_function) == N - F, S = typeof(constraint_function), typeof(set) + @test MOI.output_dimension(constraint_function) == N + F, S, IS = typeof(constraint_function), typeof(set), typeof(inner_set) @test MOI.supports_constraint(model, F, S) @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 0 c = MOI.add_constraint(model, constraint_function, set) + if activate + data = MathOptLazy._data(model, F, IS) + for (i, (f, s)) in enumerate(data.data) + data.index[i] = MOI.add_constraint(model.inner, f, s) + data.active[i] = true + end + end + c_inner = MOI.add_constraint(model, constraint_function, inner_set) @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 - c_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) - @test c_indices == [c] + @test MOI.get(model, MOI.NumberOfConstraints{F,IS}()) == 1 + @test MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) == [c] + @test MOI.get(model, MOI.ListOfConstraintIndices{F,IS}()) == [c_inner] @test (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) @test MOI.is_valid(model, c) @test !MOI.is_valid(model, typeof(c)(c.value + 1)) isa Bool @@ -110,18 +120,22 @@ function _basic_constraint_test_helper( end function test_basic_scalaraffinefunction_greaterthan() - _basic_constraint_test_helper( - x -> sum(sin(i) * x[i] for i in 1:length(x)), - MOI.GreaterThan(1.0) - ) + _basic_constraint_test_helper(MOI.GreaterThan(1.0); activate = true) do x + return sum(sin(i) * x[i] for i in 1:length(x)) + end + _basic_constraint_test_helper(MOI.GreaterThan(1.0); activate = false) do x + return sum(sin(i) * x[i] for i in 1:length(x)) + end return end function test_basic_scalaraffinefunction_lessthan() - _basic_constraint_test_helper( - x -> sum(sin(i) * x[i] for i in 1:length(x)), - MOI.LessThan(1.0) - ) + _basic_constraint_test_helper(MOI.LessThan(1.0); activate = true) do x + return sum(sin(i) * x[i] for i in 1:length(x)) + end + _basic_constraint_test_helper(MOI.LessThan(1.0); activate = false) do x + return sum(sin(i) * x[i] for i in 1:length(x)) + end return end From bd540feeccf36c4cece551faf02d2656f89da2c6 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 13 May 2026 11:37:40 +1200 Subject: [PATCH 3/4] Update --- src/MathOptLazy.jl | 48 ++++++++++++++++++++++++++++++++-------------- test/runtests.jl | 27 ++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/MathOptLazy.jl b/src/MathOptLazy.jl index 5a3ff10..66c4e27 100644 --- a/src/MathOptLazy.jl +++ b/src/MathOptLazy.jl @@ -184,16 +184,6 @@ function MOI.supports( return MOI.supports(model.inner, arg, MOI.VariableIndex) end -function MOI.set( - model::Optimizer, - attr::MOI.AbstractVariableAttribute, - indices::Vector{<:MOI.VariableIndex}, - args::Vector{T}, -) where {T} - MOI.set.(model, attr, indices, args) - return -end - ### AbstractConstraintAttribute function MOI.is_valid(model::Optimizer, ci::MOI.ConstraintIndex) @@ -210,10 +200,13 @@ end function MOI.set( model::Optimizer, - attr::MOI.AbstractConstraintAttribute, - indices::Vector{<:MOI.ConstraintIndex}, - args::Vector{T}, -) where {T} + attr::Union{ + MOI.AbstractConstraintAttribute, + MOI.AbstractVariableAttribute, + }, + indices::Vector, + args::Vector, +) MOI.set.(model, attr, indices, args) return end @@ -387,6 +380,33 @@ function MOI.get( return ret end +function MOI.supports( + model::Optimizer, + ::MOI.AbstractConstraintAttribute, + ::Type{MOI.ConstraintIndex{F,LazyScalarSet{S}}}, +) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} + return false +end + +function MOI.get( + model::Optimizer, + attr::MOI.AbstractConstraintAttribute, + ::MOI.ConstraintIndex{F,LazyScalarSet{S}}, +) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} + msg = "This attribute is not supported for lazy constraints" + return throw(MOI.GetAttributeNotAllowed(attr, msg)) +end + +function MOI.set( + model::Optimizer, + attr::MOI.AbstractConstraintAttribute, + ::MOI.ConstraintIndex{F,LazyScalarSet{S}}, + value::Any, +) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} + msg = "This attribute is not supported for lazy constraints" + return throw(MOI.SetAttributeNotAllowed(attr, msg)) +end + ### MOI.optimize! function MOI.optimize!(model::Optimizer{T}) where {T} diff --git a/test/runtests.jl b/test/runtests.jl index b6cebe8..d9e4fd8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -33,7 +33,8 @@ function test_jump_cached_knapsack() model = Model(() -> MathOptLazy.Optimizer(HiGHS.Optimizer)) set_silent(model) @variable(model, x[1:N] >= 0, Int) - @constraint(model, [i in 1:N], x[i] <= 1, MathOptLazy.Lazy()) + @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) @@ -58,6 +59,18 @@ function test_jump_direct_knapsack() return end +function test_jump_broadcast() + model = Model(() -> MathOptLazy.Optimizer(HiGHS.Optimizer)) + @variable(model, x[1:3]) + c = @constraint(model, x .<= 1:3, MathOptLazy.Lazy()) + @test c isa Vector && length(c) == 3 + for (i, ci) in enumerate(c) + @test constraint_object(ci).set == + MathOptLazy.LazyScalarSet(MOI.LessThan{Float64}(i)) + end + return +end + function test_jump_direct_basics() model = direct_model(MathOptLazy.Optimizer(HiGHS.Optimizer)) @variable(model, x) @@ -116,6 +129,16 @@ function _basic_constraint_test_helper( @test length(MOI.get(model, MOI.ListOfConstraintIndices{F,S}())) == 3 c_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) @test all(MOI.is_valid.(model, c_indices)) + MOI.set(model, MOI.ConstraintName(), [c_inner], ["c"]) + @test MOI.get(model, MOI.ConstraintName(), [c_inner]) == ["c"] + @test_throws( + MOI.SetAttributeNotAllowed, + MOI.set(model, MOI.ConstraintName(), [c], ["c"]), + ) + @test_throws( + MOI.GetAttributeNotAllowed, + MOI.get(model, MOI.ConstraintName(), [c]), + ) return end @@ -133,7 +156,7 @@ function test_basic_scalaraffinefunction_lessthan() _basic_constraint_test_helper(MOI.LessThan(1.0); activate = true) do x return sum(sin(i) * x[i] for i in 1:length(x)) end - _basic_constraint_test_helper(MOI.LessThan(1.0); activate = false) do x + _basic_constraint_test_helper(MOI.LessThan(1.0); activate = false) do x return sum(sin(i) * x[i] for i in 1:length(x)) end return From f064257b1b21e466a84a1b326155536b5ba6b368 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 13 May 2026 11:42:35 +1200 Subject: [PATCH 4/4] Update --- src/MathOptLazy.jl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/MathOptLazy.jl b/src/MathOptLazy.jl index 66c4e27..5793037 100644 --- a/src/MathOptLazy.jl +++ b/src/MathOptLazy.jl @@ -200,10 +200,7 @@ end function MOI.set( model::Optimizer, - attr::Union{ - MOI.AbstractConstraintAttribute, - MOI.AbstractVariableAttribute, - }, + attr::Union{MOI.AbstractConstraintAttribute,MOI.AbstractVariableAttribute}, indices::Vector, args::Vector, )