diff --git a/src/Bridges/Constraint/map.jl b/src/Bridges/Constraint/map.jl index 38cfbb11d7..9f6a179db2 100644 --- a/src/Bridges/Constraint/map.jl +++ b/src/Bridges/Constraint/map.jl @@ -274,19 +274,39 @@ end bridge::AbstractBridge, func::MOI.AbstractFunction, set::MOI.AbstractSet, + is_available::Function, ) Return a new constraint index `ci` and store the mapping `ci => bridge`. +If `func isa MOI.VectorOfVariables` then `ci` is such that `is_available(ci)`, +otherwise, `is_available` is ignored because there can only be a clash of +indices between variable and constraint bridge mapping for `MOI.VectorOfVariables`. """ function add_key_for_bridge end +_ensure_available(::Map, ::Type, ::Type, ::Function) = nothing +function _ensure_available( + map::Map, + F::Type{MOI.VectorOfVariables}, + ::Type{S}, + is_available::Function, +) where {S} + while !is_available(MOI.ConstraintIndex{F,S}(-length(map.bridges) - 1)) + push!(map.bridges, nothing) + push!(map.constraint_types, (F, S)) + end + return +end + function add_key_for_bridge( map::Map, bridge::AbstractBridge, ::F, ::S, + is_available::Function, ) where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet} _register_for_final_touch(map, bridge) + _ensure_available(map, F, S, is_available) push!(map.bridges, bridge) push!(map.constraint_types, (F, S)) return _index(length(map.bridges), F, S) @@ -297,6 +317,7 @@ function add_key_for_bridge( bridge::AbstractBridge, func::MOI.VariableIndex, ::S, + ::Function, ) where {S<:MOI.AbstractScalarSet} _register_for_final_touch(map, bridge) map.single_variable_constraints[(func.value, S)] = bridge diff --git a/src/Bridges/Variable/map.jl b/src/Bridges/Variable/map.jl index 7b9e98adb0..8f18f3edb6 100644 --- a/src/Bridges/Variable/map.jl +++ b/src/Bridges/Variable/map.jl @@ -13,7 +13,8 @@ mutable struct Map <: AbstractDict{MOI.VariableIndex,AbstractBridge} # Bridged constrained variables # `i` -> `0`: `VariableIndex(-i)` was added with `add_constrained_variable`. # `i` -> `-j`: `VariableIndex(-i)` was the first variable of - # `add_constrained_variables` with a set of dimension `j`. + # `add_constrained_variables` with a + # `ConstraintIndex{MOI.VectorOfVariables}(-j)`. # `i` -> `j`: `VariableIndex(-i)` was the `j`th variable of # ` add_constrained_variables`. info::Vector{Int64} @@ -36,6 +37,13 @@ mutable struct Map <: AbstractDict{MOI.VariableIndex,AbstractBridge} current_context::Int64 # Context of constraint bridged by constraint bridges constraint_context::Dict{MOI.ConstraintIndex,Int64} + # `(ci::ConstraintIndex{MOI.VectorOfVariables}).value` -> + # the first variable index + # and `0` if it is the index of a constraint bridge + vector_of_variables_map::Vector{Int64} + # `(ci::ConstraintIndex{MOI.VectorOfVariables}).value` -> + # the dimension of the set + vector_of_variables_length::Vector{Int64} end function Map() @@ -48,6 +56,8 @@ function Map() Int64[], 0, Dict{MOI.ConstraintIndex,Int64}(), + Int64[], + Int64[], ) end @@ -76,6 +86,8 @@ function Base.empty!(map::Map) empty!(map.parent_index) map.current_context = 0 empty!(map.constraint_context) + empty!(map.vector_of_variables_map) + empty!(map.vector_of_variables_length) return map end @@ -108,7 +120,7 @@ function Base.delete!(map::Map, vi::MOI.VariableIndex) delete!(map, [vi]) else # Delete variable in vector and resize vector - map.info[bridge_index(map, vi)] += 1 + map.vector_of_variables_length[-map.info[bridge_index(map, vi)]] -= 1 for i in (-vi.value):length(map.index_in_vector) if map.index_in_vector[i] == -1 continue @@ -207,10 +219,59 @@ function number_with_set(map::Map, S::Type{<:MOI.AbstractSet}) return count(isequal(S), map.sets) end +""" + first_variable(::Map, ci::MOI.ConstraintIndex{MOI.VariableIndex}) + +Return the `MOI.VariableIndex` of the `MOI.ConstraintFunction` of `ci`. +""" +function first_variable(::Map, ci::MOI.ConstraintIndex{MOI.VariableIndex}) + return MOI.VariableIndex(ci.value) +end + +""" + first_variable(::Map, ci::MOI.ConstraintIndex{MOI.VariableIndex}) + +Return the first `MOI.VariableIndex` of the `MOI.ConstraintFunction` of `ci`. +""" +function first_variable( + map::Map, + ci::MOI.ConstraintIndex{MOI.VectorOfVariables}, +) + return MOI.VariableIndex(map.vector_of_variables_map[-ci.value]) +end + function constraint(map::Map, vi::MOI.VariableIndex) S = constrained_set(map, vi)::Type{<:MOI.AbstractSet} F = MOI.Utilities.variable_function_type(S) - return MOI.ConstraintIndex{F,S}(-bridge_index(map, vi)) + index = bridge_index(map, vi) + constraint_index = map.info[index] + if iszero(constraint_index) + constraint_index = -index + end + return MOI.ConstraintIndex{F,S}(constraint_index) +end + +function MOI.is_valid( + map::Map, + ci::MOI.ConstraintIndex{MOI.VectorOfVariables,S}, +) where {S} + if !(-ci.value in eachindex(map.vector_of_variables_map)) + return false + end + index = -map.vector_of_variables_map[-ci.value] + return index in eachindex(map.bridges) && + !isnothing(map.bridges[index]) && + map.sets[index] === S +end + +function MOI.is_valid( + map::Map, + ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, +) where {S} + index = -ci.value + return index in eachindex(map.bridges) && + !isnothing(map.bridges[index]) && + map.sets[index] === S end """ @@ -220,8 +281,8 @@ Return the list of constraints corresponding to bridged variables in `S`. """ function constraints_with_set(map::Map, S::Type{<:MOI.AbstractSet}) F = MOI.Utilities.variable_function_type(S) - return [ - MOI.ConstraintIndex{F,S}(-i) for + return MOI.ConstraintIndex{F,S}[ + constraint(map, MOI.VariableIndex(-i)) for i in eachindex(map.sets) if map.sets[i] == S ] end @@ -272,7 +333,12 @@ If `vi` was bridged in a scalar set, it returns 0. Otherwise, it returns the dimension of the set. """ function length_of_vector_of_variables(map::Map, vi::MOI.VariableIndex) - return -map.info[bridge_index(map, vi)] + info = map.info[bridge_index(map, vi)] + if iszero(info) + return 0 + else + return map.vector_of_variables_length[-info] + end end """ @@ -338,26 +404,42 @@ function add_key_for_bridge( end """ - add_keys_for_bridge(map::Map, bridge_fun::Function, - set::MOI.AbstractVectorSet) + function add_keys_for_bridge( + map::Map, + bridge_fun::Function, + set::MOI.AbstractVectorSet, + is_available::Function, + ) Create vector of variable indices `variables`, stores the mapping `vi => bridge` for each `vi ∈ variables` and associate `variables` to -`typeof(set)`. It returns a tuple with `variables` and the constraint index -`MOI.ConstraintIndex{MOI.VectorOfVariables, typeof(set)}(first(variables).value)`. +`typeof(set)`. It returns a tuple with `variables` and a constraint index +`ci::MOI.ConstraintIndex{MOI.VectorOfVariables, typeof(set)}` such that +`is_available(ci)`. """ function add_keys_for_bridge( map::Map, bridge_fun::Function, - set::MOI.AbstractVectorSet, -) + set::S, + is_available::Function, +) where {S<:MOI.AbstractVectorSet} if iszero(MOI.dimension(set)) return MOI.VariableIndex[], MOI.ConstraintIndex{MOI.VectorOfVariables,typeof(set)}(0) end push!(map.parent_index, map.current_context) bridge_index = Int64(length(map.parent_index)) - push!(map.info, -MOI.dimension(set)) + F = MOI.VectorOfVariables + while !is_available( + MOI.ConstraintIndex{F,S}(-length(map.vector_of_variables_map) - 1), + ) + push!(map.vector_of_variables_map, 0) + push!(map.vector_of_variables_length, 0) + end + push!(map.vector_of_variables_map, -bridge_index) + push!(map.vector_of_variables_length, MOI.dimension(set)) + constraint_index = -length(map.vector_of_variables_map) + push!(map.info, constraint_index) push!(map.index_in_vector, 1) push!(map.bridges, nothing) push!(map.sets, typeof(set)) @@ -386,9 +468,7 @@ function add_keys_for_bridge( end end end - index = first(variables).value - return variables, - MOI.ConstraintIndex{MOI.VectorOfVariables,typeof(set)}(index) + return variables, MOI.ConstraintIndex{F,S}(constraint_index) end """ @@ -408,12 +488,13 @@ Return `MOI.VectorOfVariables(vis)` where `vis` is the vector of bridged variables corresponding to `ci`. """ function function_for(map::Map, ci::MOI.ConstraintIndex{MOI.VectorOfVariables}) + index = map.vector_of_variables_map[-ci.value] variables = MOI.VariableIndex[] - for i in ci.value:-1:-length(map.bridges) + for i in index:-1:-length(map.bridges) vi = MOI.VariableIndex(i) if map.index_in_vector[-vi.value] == -1 continue - elseif bridge_index(map, vi) == -ci.value + elseif bridge_index(map, vi) == -index push!(variables, vi) else break @@ -555,3 +636,5 @@ end register_context(::EmptyMap, ::MOI.ConstraintIndex) = nothing call_in_context(::EmptyMap, ::MOI.ConstraintIndex, f::Function) = f() + +MOI.is_valid(::EmptyMap, ::MOI.ConstraintIndex) = false diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index e0100e6344..67b5661732 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -261,7 +261,8 @@ Return the `AbstractBridge` used to bridge the constraint with index `ci`. """ function bridge(b::AbstractBridgeOptimizer, ci::MOI.ConstraintIndex) if is_variable_bridged(b, ci) - return bridge(b, MOI.VariableIndex(ci.value)) + map = Variable.bridges(b)::Variable.Map + return bridge(b, Variable.first_variable(map, ci)) else return Constraint.bridges(b)[ci] end @@ -312,7 +313,8 @@ function call_in_context( f::Function, ) if is_variable_bridged(b, ci) - return call_in_context(b, MOI.VariableIndex(ci.value), f) + map = Variable.bridges(b)::Variable.Map + return call_in_context(b, Variable.first_variable(map, ci), f) else return Variable.call_in_context( Variable.bridges(b), @@ -483,10 +485,8 @@ function MOI.is_valid( ) where {F,S} if is_bridged(b, ci) if is_variable_bridged(b, ci) - vi = MOI.VariableIndex(ci.value) v_map = Variable.bridges(b)::Variable.Map - return MOI.is_valid(b, vi) && - Variable.constrained_set(v_map, vi) == S + return MOI.is_valid(v_map, ci) else return haskey(Constraint.bridges(b), ci) end @@ -1716,11 +1716,17 @@ end function add_bridged_constraint(b, BridgeType, f, s) bridge = Constraint.bridge_constraint(BridgeType, recursive_model(b), f, s) + # `MOI.VectorOfVariables` constraint indices have negative indices + # to distinguish between the indices of the inner model. + # However, they can clash between the indices created by the variable + # so we use the last argument to inform the constraint bridge mapping about + # indices already taken by variable bridges. ci = Constraint.add_key_for_bridge( Constraint.bridges(b)::Constraint.Map, bridge, f, s, + !Base.Fix1(MOI.is_valid, Variable.bridges(b)), ) Variable.register_context(Variable.bridges(b), ci) return ci @@ -1996,10 +2002,16 @@ function MOI.add_constrained_variables( end if set isa MOI.Reals || is_variable_bridged(b, typeof(set)) BridgeType = Variable.concrete_bridge_type(b, typeof(set)) + # `MOI.VectorOfVariables` constraint indices have negative indices + # to distinguish between the indices of the inner model. + # However, they can clash between the indices created by the variable + # so we use the last argument to inform the variable bridge mapping about + # indices already taken by constraint bridges. return Variable.add_keys_for_bridge( Variable.bridges(b)::Variable.Map, () -> Variable.bridge_constrained_variable(BridgeType, b, set), set, + !Base.Fix1(haskey, Constraint.bridges(b)), ) else variables = MOI.add_variables(b, MOI.dimension(set)) diff --git a/test/Bridges/Constraint/map.jl b/test/Bridges/Constraint/map.jl index 3671259c47..26df06122b 100644 --- a/test/Bridges/Constraint/map.jl +++ b/test/Bridges/Constraint/map.jl @@ -34,7 +34,13 @@ function test_map() x = MOI.VariableIndex(1) y = MOI.VariableIndex(2) b1 = ConstraintDummyBridge(1) - c1 = MOI.Bridges.Constraint.add_key_for_bridge(map, b1, x, MOI.EqualTo(0.0)) + c1 = MOI.Bridges.Constraint.add_key_for_bridge( + map, + b1, + x, + MOI.EqualTo(0.0), + ci -> true, + ) @test c1.value == x.value @test haskey(map, c1) @@ -53,6 +59,7 @@ function test_map() b2, vov, MOI.SecondOrderCone(2), + ci -> true, ) @test c2.value == -1 @test haskey(map, c2) @@ -70,6 +77,7 @@ function test_map() b3, 1.0x + 2.0, MOI.EqualTo(0.0), + ci -> true, ) @test haskey(map, c3) @test map[c3] == b3 diff --git a/test/Bridges/Variable/map.jl b/test/Bridges/Variable/map.jl index be2d67f848..249fa6099c 100644 --- a/test/Bridges/Variable/map.jl +++ b/test/Bridges/Variable/map.jl @@ -53,12 +53,14 @@ function test_map() F1 = MOI.VariableIndex S1 = typeof(set1) v1, c1 = MOI.Bridges.Variable.add_key_for_bridge(map, () -> b1, set1) + MOI.is_valid(map, c1) cannot_unbridge_err = ErrorException( "Cannot unbridge function because some variables are bridged by variable" * " bridges that do not support reverse mapping, e.g., `ZerosBridge`.", ) @test v1.value == c1.value == -1 @test MOI.Bridges.Variable.constraint(map, v1) == c1 + @test MOI.Bridges.Variable.length_of_vector_of_variables(map, v1) == 0 @test haskey(map, v1) @test map[v1] == b1 @test MOI.Bridges.Variable.constrained_set(map, v1) == S1 @@ -82,8 +84,15 @@ function test_map() set2 = MOI.Zeros(4) F2 = MOI.VectorOfVariables S2 = typeof(set2) - v2, c2 = MOI.Bridges.Variable.add_keys_for_bridge(map, () -> b2, set2) - @test v2[1].value == c2.value == -2 + v2, c2 = MOI.Bridges.Variable.add_keys_for_bridge( + map, + () -> b2, + set2, + ci -> true, + ) + MOI.is_valid(map, c2) + @test v2[1].value == -2 + @test c2.value == -1 @test MOI.Bridges.Variable.has_keys(map, v2) @test !MOI.Bridges.Variable.has_keys(map, v2[4:-1:1]) for i in 1:4 @@ -117,7 +126,12 @@ function test_map() b3 = VariableDummyBridge(3) set3 = MOI.Zeros(0) - v3, c3 = MOI.Bridges.Variable.add_keys_for_bridge(map, () -> b3, set3) + v3, c3 = MOI.Bridges.Variable.add_keys_for_bridge( + map, + () -> b3, + set3, + ci -> true, + ) @test isempty(v3) @test c3.value == 0 @@ -257,6 +271,10 @@ function test_EmptyMap() @test iszero(MOI.Bridges.Variable.number_with_set(map, S2)) @test isempty(MOI.Bridges.Variable.constraints_with_set(map, S1)) @test isempty(MOI.Bridges.Variable.constraints_with_set(map, S2)) + c1 = MOI.ConstraintIndex{MOI.VariableIndex,S1}(1) + @test !MOI.is_valid(map, c1) + c2 = MOI.ConstraintIndex{MOI.VectorOfVariables,S2}(1) + @test !MOI.is_valid(map, c2) @test sprint(show, map) == "" return end diff --git a/test/Bridges/bridge_optimizer.jl b/test/Bridges/bridge_optimizer.jl index 2e8df8af12..daf11fffe3 100644 --- a/test/Bridges/bridge_optimizer.jl +++ b/test/Bridges/bridge_optimizer.jl @@ -1129,6 +1129,35 @@ function test_first_bridge() return end +function test_variable_bridge_constraint_attribute() + mock = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + ) + model = MOI.Bridges.Variable.NonposToNonneg{Float64}(mock) + x, cx = MOI.add_constrained_variables(model, MOI.Nonpositives(2)) + y, cy = MOI.add_constrained_variables(model, MOI.Nonpositives(2)) + @test MOI.Bridges.bridge(model, x[1]) === MOI.Bridges.bridge(model, cx) + @test MOI.Bridges.bridge(model, x[2]) === MOI.Bridges.bridge(model, cx) + @test MOI.Bridges.bridge(model, y[1]) === MOI.Bridges.bridge(model, cy) + @test MOI.Bridges.bridge(model, y[2]) === MOI.Bridges.bridge(model, cy) + var = [x; y] + val = float.(collect(1:4)) + mock_var = MOI.get(mock, MOI.ListOfVariableIndices()) + MOI.set(mock, MOI.VariablePrimal(), mock_var, -val) + @test MOI.get(model, MOI.VariablePrimal(), var) == val + @test MOI.get(model, MOI.ConstraintPrimal(), cx) == val[1:2] + @test MOI.get(model, MOI.ConstraintPrimal(), cy) == val[3:4] + for v in var + @test MOI.is_valid(model, v) + end + @test MOI.is_valid(model, cx) + @test MOI.is_valid(model, cy) + MOI.delete(model, x) + @test !MOI.is_valid(model, cx) + @test MOI.is_valid(model, cy) + return +end + end # module TestBridgeOptimizer.runtests() diff --git a/test/Bridges/lazy_bridge_optimizer.jl b/test/Bridges/lazy_bridge_optimizer.jl index 8c27bd80f9..327f1b2289 100644 --- a/test/Bridges/lazy_bridge_optimizer.jl +++ b/test/Bridges/lazy_bridge_optimizer.jl @@ -332,6 +332,50 @@ function test_MOI_runtests_GeometricSDPAModel() return end +function test_index_constraint_conflict() + optimizer = StandardSDPAModel{Float64}() + model = MOI.Bridges.full_bridge_optimizer(optimizer, Float64) + x, cx = MOI.add_constrained_variables(model, MOI.Nonpositives(1)) + @test MOI.is_valid(model, x[1]) + @test MOI.is_valid(model, cx) + y, cy = MOI.add_constrained_variables(model, MOI.Nonpositives(1)) + @test MOI.is_valid(model, y[1]) + @test MOI.is_valid(model, cy) + c = MOI.add_constraint(model, MOI.VectorOfVariables(y), MOI.Nonpositives(1)) + MOI.is_valid(model, c) + b1 = MOI.Bridges.bridge(model, c) + b2 = MOI.Bridges.bridge(model, b1.constraint) + @test c != b2.slack_in_set + return +end + +function _test_index_variable_conflict(set) + optimizer = StandardSDPAModel{Float64}() + model = MOI.Bridges.full_bridge_optimizer(optimizer, Float64) + x = MOI.add_variables(model, MOI.dimension(set)) + c = MOI.add_constraint(model, MOI.VectorOfVariables(x), set) + @test MOI.is_valid(model, c) + w, cw = MOI.add_constrained_variables(model, set) + @test MOI.is_valid(model, cw) + @test cw != c + y, cy = MOI.add_constrained_variables(model, set) + @test MOI.is_valid(model, cy) + @test cy != c + z, cz = MOI.add_constrained_variables(model, set) + @test MOI.is_valid(model, cz) + @test cz != c + return +end + +function test_index_variable_conflict() + _test_index_variable_conflict(MOI.Nonpositives(3)) + _test_index_variable_conflict(MOI.SecondOrderCone(3)) + _test_index_variable_conflict(MOI.RotatedSecondOrderCone(3)) + _test_index_variable_conflict(MOI.RotatedSecondOrderCone(2)) + _test_index_variable_conflict(MOI.ScaledPositiveSemidefiniteConeTriangle(2)) + return +end + function test_show_SPDA() model = StandardSDPAModel{Float64}() model_str = sprint(MOI.Utilities.print_with_acronym, string(typeof(model)))