From c17229ce7b9940f04ac51760a486ebe71d2f67d3 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 24 Jul 2026 09:50:38 +1200 Subject: [PATCH 1/4] [Utilities] add RuntimeProductOfSets --- docs/src/submodules/Utilities/reference.md | 5 +- src/Utilities/product_of_sets.jl | 82 ++++++++++++++++++++++ test/Utilities/test_product_of_sets.jl | 62 ++++++++++++++++ 3 files changed, 146 insertions(+), 3 deletions(-) diff --git a/docs/src/submodules/Utilities/reference.md b/docs/src/submodules/Utilities/reference.md index 7b2beb0c8d..745ead9bc0 100644 --- a/docs/src/submodules/Utilities/reference.md +++ b/docs/src/submodules/Utilities/reference.md @@ -142,14 +142,13 @@ Utilities.add_set Utilities.rows Utilities.num_rows Utilities.set_with_dimension -``` - -```@docs Utilities.ProductOfSets Utilities.MixOfScalarSets Utilities.@mix_of_scalar_sets Utilities.OrderedProductOfSets Utilities.@product_of_sets +Utilities.RuntimeProductOfSets +Utilities.add_set_type ``` ## Fallbacks diff --git a/src/Utilities/product_of_sets.jl b/src/Utilities/product_of_sets.jl index 1be7dbf77c..8e807f8c55 100644 --- a/src/Utilities/product_of_sets.jl +++ b/src/Utilities/product_of_sets.jl @@ -320,3 +320,85 @@ function MOI.is_valid( end return 1 <= ci.value <= length(sets.rows[i]) end + +""" + RuntimeProductOfSets{T}() + +An alternative to [`Utilities.@product_of_sets`](@ref), in which the set types +are decided at runtime using [`Utilities.add_set_type`](@ref). + +## Examples + +```jldoctest +julia> import MathOptInterface as MOI + +julia> sets = MOI.Utilities.RuntimeProductOfSets{Int}(); + +julia> MOI.Utilities.set_types(sets) +Type[] + +julia> MOI.Utilities.add_set_type(sets, MOI.Nonpositives) +true + +julia> MOI.Utilities.set_types(sets) +1-element Vector{Type}: + MathOptInterface.Nonpositives +``` +""" +mutable struct RuntimeProductOfSets{T} <: OrderedProductOfSets{T} + rows::Vector{Vector{UnitRange{Int}}} + final_touch::Bool + set_types::Vector{Type} + + function RuntimeProductOfSets{T}() where {T} + return new(Vector{UnitRange{Int}}[], false, Type[]) + end +end + +function set_index( + set::RuntimeProductOfSets, + ::Type{S}, +) where {S<:MOI.AbstractSet} + return findfirst(==(S), set.set_types) +end + +set_types(set::RuntimeProductOfSets) = set.set_types + +""" + add_set_type( + set::RuntimeProductOfSets, + ::Type{S}, + ) where {S<:MOI.AbstractSet} + +Declare that the [`Utilities.RuntimeProductOfSets`](@ref) `set` supports the +[`AbstractSet`](@ref) `S`. + +## Examples + +```jldoctest +julia> import MathOptInterface as MOI + +julia> sets = MOI.Utilities.RuntimeProductOfSets{Int}(); + +julia> MOI.Utilities.set_types(sets) +Type[] + +julia> MOI.Utilities.add_set_type(sets, MOI.Nonpositives) +true + +julia> MOI.Utilities.set_types(sets) +1-element Vector{Type}: + MathOptInterface.Nonpositives +``` +""" +function add_set_type( + set::RuntimeProductOfSets, + ::Type{S}, +) where {S<:MOI.AbstractSet} + if set_index(set, S) === nothing + push!(set.rows, Vector{UnitRange{Int}}[]) + push!(set.set_types, S) + return true + end + return false +end diff --git a/test/Utilities/test_product_of_sets.jl b/test/Utilities/test_product_of_sets.jl index c2a6c4daf3..8202ce939d 100644 --- a/test/Utilities/test_product_of_sets.jl +++ b/test/Utilities/test_product_of_sets.jl @@ -421,6 +421,68 @@ function test_property_num_sets() return end +function test_product_of_sets() + sets = MOI.Utilities.RuntimeProductOfSets{Int}() + @test MOI.Utilities.set_types(sets) == Type[] + MOI.Utilities.add_set_type(sets, MOI.Nonpositives) + @test MOI.Utilities.set_types(sets) == Type[MOI.Nonpositives] + MOI.Utilities.add_set_type(sets, MOI.Nonnegatives) + MOI.Utilities.add_set_type(sets, MOI.EqualTo{Int}) + @test MOI.Utilities.set_types(sets) == + Type[MOI.Nonpositives, MOI.Nonnegatives, MOI.EqualTo{Int}] + i1 = MOI.Utilities.set_index(sets, MOI.Nonpositives) + @test i1 == 1 # The tests below explicitly use this ordering. + i2 = MOI.Utilities.set_index(sets, MOI.Nonnegatives) + @test i2 == 2 # The tests below explicitly use this ordering. + i3 = MOI.Utilities.set_index(sets, MOI.EqualTo{Int}) + @test i3 == 3 # The tests below explicitly use this ordering. + MOI.Utilities.add_set(sets, i1, 0) + MOI.Utilities.add_set(sets, i1, 2) + MOI.Utilities.add_set(sets, i2, 0) + MOI.Utilities.add_set(sets, i2, 1) + MOI.Utilities.add_set(sets, i1, 0) + MOI.Utilities.add_set(sets, i1, 1) + MOI.Utilities.add_set(sets, i3) + MOI.Utilities.add_set(sets, i1, 0) + @test MOI.Utilities.num_rows(sets, MOI.Nonpositives) == 3 + @test MOI.Utilities.num_rows(sets, MOI.Nonnegatives) == 1 + @test MOI.Utilities.num_rows(sets, MOI.EqualTo{Int}) == 1 + MOI.Utilities.final_touch(sets) + # MOI.Nonpositives + F, S = MOI.VectorAffineFunction{Int}, MOI.Nonpositives + @test (F, S) in MOI.get(sets, MOI.ListOfConstraintTypesPresent()) + @test MOI.get(sets, MOI.NumberOfConstraints{F,S}()) == 5 + c = MOI.ConstraintIndex{F,S}.(1:5) + @test MOI.get(sets, MOI.ListOfConstraintIndices{F,S}()) == c + @test all(MOI.is_valid(sets, ci) for ci in c) + @test MOI.Utilities.num_rows(sets, S) == 3 + @test MOI.Utilities.rows(sets, c[1]) == 1:0 + @test MOI.Utilities.rows(sets, c[2]) == 1:2 + @test MOI.Utilities.rows(sets, c[3]) == 3:2 + @test MOI.Utilities.rows(sets, c[4]) == 3:3 + @test MOI.Utilities.rows(sets, c[5]) == 4:3 + # MOI.Nonnegatives + F, S = MOI.VectorAffineFunction{Int}, MOI.Nonnegatives + @test (F, S) in MOI.get(sets, MOI.ListOfConstraintTypesPresent()) + @test MOI.get(sets, MOI.NumberOfConstraints{F,S}()) == 2 + c = MOI.ConstraintIndex{F,S}.(1:2) + @test MOI.get(sets, MOI.ListOfConstraintIndices{F,S}()) == c + @test all(MOI.is_valid(sets, ci) for ci in c) + @test MOI.Utilities.num_rows(sets, S) == 1 + @test MOI.Utilities.rows(sets, c[1]) == 4:3 + @test MOI.Utilities.rows(sets, c[2]) == 4:4 + # MOI.EqualTo + F, S = MOI.ScalarAffineFunction{Int}, MOI.EqualTo{Int} + @test (F, S) in MOI.get(sets, MOI.ListOfConstraintTypesPresent()) + @test MOI.get(sets, MOI.NumberOfConstraints{F,S}()) == 1 + c = MOI.ConstraintIndex{F,S}.(1:1) + @test MOI.get(sets, MOI.ListOfConstraintIndices{F,S}()) == c + @test all(MOI.is_valid(sets, ci) for ci in c) + @test MOI.Utilities.num_rows(sets, S) == 1 + @test MOI.Utilities.rows(sets, c[1]) == 5 + return +end + end TestProductOfSets.runtests() From 8e837cc02485d4eadb0fc43863864ac0ff29819d Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 24 Jul 2026 14:51:26 +1200 Subject: [PATCH 2/4] Apply suggestion from @odow --- src/Utilities/product_of_sets.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utilities/product_of_sets.jl b/src/Utilities/product_of_sets.jl index 8e807f8c55..bac2ad2d58 100644 --- a/src/Utilities/product_of_sets.jl +++ b/src/Utilities/product_of_sets.jl @@ -371,7 +371,7 @@ set_types(set::RuntimeProductOfSets) = set.set_types ) where {S<:MOI.AbstractSet} Declare that the [`Utilities.RuntimeProductOfSets`](@ref) `set` supports the -[`AbstractSet`](@ref) `S`. +[`MOI.AbstractSet`](@ref) `S`. ## Examples From d20519d40b6d8589992dc1eb2e3efc474931928b Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 24 Jul 2026 15:22:40 +1200 Subject: [PATCH 3/4] Update --- src/Utilities/product_of_sets.jl | 3 +-- test/Utilities/test_product_of_sets.jl | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Utilities/product_of_sets.jl b/src/Utilities/product_of_sets.jl index bac2ad2d58..1c15593845 100644 --- a/src/Utilities/product_of_sets.jl +++ b/src/Utilities/product_of_sets.jl @@ -398,7 +398,6 @@ function add_set_type( if set_index(set, S) === nothing push!(set.rows, Vector{UnitRange{Int}}[]) push!(set.set_types, S) - return true end - return false + return end diff --git a/test/Utilities/test_product_of_sets.jl b/test/Utilities/test_product_of_sets.jl index 8202ce939d..c7304705d9 100644 --- a/test/Utilities/test_product_of_sets.jl +++ b/test/Utilities/test_product_of_sets.jl @@ -428,6 +428,9 @@ function test_product_of_sets() @test MOI.Utilities.set_types(sets) == Type[MOI.Nonpositives] MOI.Utilities.add_set_type(sets, MOI.Nonnegatives) MOI.Utilities.add_set_type(sets, MOI.EqualTo{Int}) + @test MOI.Utilities.set_types(sets) == + Type[MOI.Nonpositives, MOI.Nonnegatives, MOI.EqualTo{Int}] + MOI.Utilities.add_set_type(sets, MOI.EqualTo{Int}) @test MOI.Utilities.set_types(sets) == Type[MOI.Nonpositives, MOI.Nonnegatives, MOI.EqualTo{Int}] i1 = MOI.Utilities.set_index(sets, MOI.Nonpositives) From 97eaf483d63a0dca9a18ac9382ab9e13299b536f Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 24 Jul 2026 15:42:18 +1200 Subject: [PATCH 4/4] Update --- src/Utilities/product_of_sets.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Utilities/product_of_sets.jl b/src/Utilities/product_of_sets.jl index 1c15593845..320d119eb6 100644 --- a/src/Utilities/product_of_sets.jl +++ b/src/Utilities/product_of_sets.jl @@ -338,7 +338,6 @@ julia> MOI.Utilities.set_types(sets) Type[] julia> MOI.Utilities.add_set_type(sets, MOI.Nonpositives) -true julia> MOI.Utilities.set_types(sets) 1-element Vector{Type}: @@ -384,7 +383,6 @@ julia> MOI.Utilities.set_types(sets) Type[] julia> MOI.Utilities.add_set_type(sets, MOI.Nonpositives) -true julia> MOI.Utilities.set_types(sets) 1-element Vector{Type}: