diff --git a/src/MathOptLazy.jl b/src/MathOptLazy.jl index 331dba1..5793037 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 """ @@ -180,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) @@ -206,10 +200,10 @@ 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 @@ -286,7 +280,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 +307,103 @@ 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 + +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 + +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 c261e8e..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) @@ -68,6 +81,87 @@ function test_jump_direct_basics() return end +function _basic_constraint_test_helper( + function_fn::Function, + inner_set::MOI.AbstractScalarSet; + activate::Bool, +) + model = MathOptLazy.Optimizer(HiGHS.Optimizer) + config = MOI.Test.Config() + set = MathOptLazy.LazyScalarSet(inner_set) + N = MOI.dimension(set) + x = MOI.add_variables(model, 3) + constraint_function = function_fn(x) + @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 + @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 + @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)) + 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 + +function test_basic_scalaraffinefunction_greaterthan() + _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(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 + end # TestMathOptLazy TestMathOptLazy.runtests()