From 0686f5d3a124ced00bccba59b311db25cfed3adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sun, 7 Jun 2026 15:55:03 +0000 Subject: [PATCH 1/3] [Bridges] add reserve_variable_index and reserve_constraint_index API Introduce MOI.Bridges.reserve_variable_index(model) and reserve_constraint_index(model, F, S) so that bridge layers can allocate identities that don't collide with the inner model's namespace. The default reserve_variable_index adds a variable and immediately deletes it. The default reserve_constraint_index adds a dummy F-in-S constraint (building zero/empty function and a default S instance) and deletes it. For VariableIndex/VectorOfVariables constraints, dummy variables are also cleaned up. MOI models don't recycle deleted indices, so the returned index is guaranteed unique in the inner namespace. AbstractBridgeOptimizer cascades both calls down to b.model, so reservation propagates all the way to the leaf model. This is the foundation for stacking SingleBridgeOptimizers without a caching layer in between. No existing behavior is changed. --- src/Bridges/bridge_optimizer.jl | 141 ++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index a13ab5093d..4dde113090 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -218,6 +218,147 @@ function supports_bridging_objective_function( return false end +""" + reserve_variable_index(model::MOI.ModelLike)::MOI.VariableIndex + +Return a `MOI.VariableIndex` that is guaranteed not to collide with any +variable currently in `model` nor any variable that will be added to `model` +later. + +This is used by bridge optimizers to allocate identities for bridged +variables that do not collide with the inner model's namespace, so that +multiple bridge layers can be stacked. + +The default implementation adds a variable and immediately deletes it, +relying on the fact that MOI models do not recycle deleted variable +indices. +""" +function reserve_variable_index(model::MOI.ModelLike) + vi = MOI.add_variable(model) + MOI.delete(model, vi) + return vi +end + +function reserve_variable_index(b::AbstractBridgeOptimizer) + return reserve_variable_index(b.model) +end + +""" + reserve_constraint_index( + model::MOI.ModelLike, + ::Type{F}, + ::Type{S}, + )::MOI.ConstraintIndex{F,S} where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet} + +Return a `MOI.ConstraintIndex{F,S}` that is guaranteed not to collide with +any constraint currently in `model` nor any constraint of the same `(F, S)` +type that will be added later. + +This is used by bridge optimizers to allocate identities for force-bridged +or constraint-bridged `(F, S)` constraints that do not collide with the +inner model's namespace, so that multiple bridge layers can be stacked. + +The default implementation adds a dummy `F`-in-`S` constraint and +immediately deletes it (along with any temporary variables created to build +the dummy), relying on the fact that MOI models do not recycle deleted +constraint indices. + +Reservation can be skipped when the inner model supports neither `(F, S)` +as a constraint nor constrained-variables-in-`S`, in which case no +colliding `ConstraintIndex{F,S}` can ever be produced by the inner model. + +For function types `F` and set types `S` not handled by the default, +specialize this method on the model type. +""" +function reserve_constraint_index( + model::MOI.ModelLike, + ::Type{F}, + ::Type{S}, +) where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet} + return _default_reserve_constraint_index(model, F, S) +end + +function reserve_constraint_index( + b::AbstractBridgeOptimizer, + ::Type{F}, + ::Type{S}, +) where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet} + return reserve_constraint_index(b.model, F, S) +end + +function _default_reserve_constraint_index( + model::MOI.ModelLike, + ::Type{MOI.VariableIndex}, + ::Type{S}, +) where {S<:MOI.AbstractScalarSet} + vi = MOI.add_variable(model) + ci = MOI.add_constraint(model, vi, _dummy_set(S)) + MOI.delete(model, vi) # also deletes ci + return ci +end + +function _default_reserve_constraint_index( + model::MOI.ModelLike, + ::Type{MOI.VectorOfVariables}, + ::Type{S}, +) where {S<:MOI.AbstractVectorSet} + set = _dummy_set(S) + vis = MOI.add_variables(model, MOI.dimension(set)) + ci = MOI.add_constraint(model, MOI.VectorOfVariables(vis), set) + MOI.delete(model, ci) + for vi in vis + MOI.delete(model, vi) + end + return ci +end + +function _default_reserve_constraint_index( + model::MOI.ModelLike, + ::Type{F}, + ::Type{S}, +) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} + ci = MOI.add_constraint(model, zero(F), _dummy_set(S)) + MOI.delete(model, ci) + return ci +end + +function _default_reserve_constraint_index( + model::MOI.ModelLike, + ::Type{F}, + ::Type{S}, +) where {F<:MOI.AbstractVectorFunction,S<:MOI.AbstractVectorSet} + set = _dummy_set(S) + f = MOI.Utilities.zero_with_output_dimension(F, MOI.dimension(set)) + ci = MOI.add_constraint(model, f, set) + MOI.delete(model, ci) + return ci +end + +""" + _dummy_set(::Type{S}) where {S<:MOI.AbstractSet} + +Return a trivial instance of `S` used as a placeholder when reserving a +`ConstraintIndex{F,S}` via [`_reserve_dummy_constraint`](@ref). Sets whose +constructor requires parameters that cannot be defaulted must specialize +this method (or skip reservation entirely if the inner model does not +support the corresponding constraint). +""" +function _dummy_set end + +_dummy_set(::Type{S}) where {T,S<:MOI.EqualTo{T}} = S(zero(T)) +_dummy_set(::Type{S}) where {T,S<:MOI.GreaterThan{T}} = S(zero(T)) +_dummy_set(::Type{S}) where {T,S<:MOI.LessThan{T}} = S(zero(T)) +_dummy_set(::Type{S}) where {T,S<:MOI.Interval{T}} = S(zero(T), zero(T)) +_dummy_set(::Type{MOI.Integer}) = MOI.Integer() +_dummy_set(::Type{MOI.ZeroOne}) = MOI.ZeroOne() +_dummy_set(::Type{S}) where {T,S<:MOI.Semicontinuous{T}} = S(zero(T), zero(T)) +_dummy_set(::Type{S}) where {T,S<:MOI.Semiinteger{T}} = S(zero(T), zero(T)) +_dummy_set(::Type{S}) where {T,S<:MOI.Parameter{T}} = S(zero(T)) +_dummy_set(::Type{MOI.Reals}) = MOI.Reals(1) +_dummy_set(::Type{MOI.Zeros}) = MOI.Zeros(1) +_dummy_set(::Type{MOI.Nonnegatives}) = MOI.Nonnegatives(1) +_dummy_set(::Type{MOI.Nonpositives}) = MOI.Nonpositives(1) + """ bridge_type( b::AbstractBridgeOptimizer, From 0cbaead64a0249a21628bd7b2655f345dfc7fd9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sun, 7 Jun 2026 19:21:18 +0100 Subject: [PATCH 2/3] Rewrite map --- src/Bridges/Variable/map.jl | 427 +++++++++++++++++++------------- src/Bridges/bridge_optimizer.jl | 197 ++++++++------- 2 files changed, 363 insertions(+), 261 deletions(-) diff --git a/src/Bridges/Variable/map.jl b/src/Bridges/Variable/map.jl index 806d4b939a..275ec4b9e4 100644 --- a/src/Bridges/Variable/map.jl +++ b/src/Bridges/Variable/map.jl @@ -8,58 +8,82 @@ Map <: AbstractDict{MOI.VariableIndex, AbstractBridge} Mapping between bridged variables and the bridge that bridged the variable. + +# Internal structure + +`vi.value`s of bridged variables are allocated via +[`MOI.Bridges.reserve_variable_index`](@ref) so that they don't collide +with the inner model's namespace. They are not contiguous integers. + +Internally, the `Map` allocates 1-based `slot` indices in insertion order +and uses them to index `Vector`-backed data (`bridges`, `info`, +`index_in_vector`, `sets`, `parent_index`, `set_mask`, +`slot_to_variable`). The Dict `variable_to_slot` maps the user-facing +`vi.value` to the internal `slot`. + +For a vector of bridged variables of dimension `n`, the n variables get +consecutive `slot`s (the first slot is where the bridge is stored). The +`vi.value`s do not need to be consecutive. + +The semantics of `info[slot]`: + + * `0`: `slot_to_variable[slot]` is a scalar variable added with + `add_constrained_variable`. + * `-x` with `x > 0`: `slot_to_variable[slot]` is the first variable of a + vector added with `add_constrained_variables`, whose + `ConstraintIndex{MOI.VectorOfVariables, S}.value` is `x`. + * `k > 0`: `slot_to_variable[slot]` is the `k`-th variable of a vector + (whose first-variable `slot` is `slot - k + 1`). + +The semantics of `index_in_vector[slot]`: + + * `-1`: variable was deleted + * `0`: scalar variable + * `j > 0`: `j`-th variable of a vector (taking deletion into account) """ 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 - # `ConstraintIndex{MOI.VectorOfVariables}(-j)`. - # `i` -> `j`: `VariableIndex(-i)` was the `j`th variable of - # ` add_constrained_variables`. + # Forward mapping: user-facing vi.value -> internal slot + variable_to_slot::Dict{Int64,Int64} + # Reverse mapping: internal slot -> user-facing vi.value + slot_to_variable::Vector{Int64} + # (S, ConstraintIndex{VectorOfVariables, S}.value) -> first slot of that vector + # Keyed per-S because reservation from the inner model can return the same + # `.value` for different `S` types (each `(F, S)` has its own namespace). + vov_to_slot::Dict{Tuple{Type,Int64},Int64} + # (S, ci.value) -> dimension of the set + vov_length::Dict{Tuple{Type,Int64},Int64} + # See docstring above info::Vector{Int64} - # `i` -> `-1`: `VariableIndex(-i)` was deleted. - # `i` -> `0`: `VariableIndex(-i)` was added with `add_constrained_variable`. - # `i` -> `j`: `VariableIndex(-i)` is the `j`th variable of a constrained - # vector of variables, taking deletion into account. index_in_vector::Vector{Int64} - # `i` -> `bridge`: `VariableIndex(-i)` was bridged by `bridge`. bridges::Vector{Union{Nothing,AbstractBridge}} sets::Vector{Union{Nothing,Type}} + # Parent context (slot) of the bridge that created this slot, 0 if root + parent_index::Vector{Int64} + current_context::Int64 + constraint_context::Dict{MOI.ConstraintIndex,Int64} # If `nothing`, it cannot be computed because some bridges does not support it unbridged_function::Union{ Nothing, Dict{MOI.VariableIndex,Tuple{Int64,MOI.AbstractScalarFunction}}, } - # Bridge that created this bridge, 0 if it is no bridge. - parent_index::Vector{Int64} - # Current bridge, 0 otherwise. - 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} # Same as in `MOI.Utilities.VariablesContainer` set_mask::Vector{UInt16} end function Map() return Map( + Dict{Int64,Int64}(), + Int64[], + Dict{Tuple{Type,Int64},Int64}(), + Dict{Tuple{Type,Int64},Int64}(), Int64[], Int64[], Union{Nothing,AbstractBridge}[], Union{Nothing,Type}[], - Dict{MOI.VariableIndex,MOI.AbstractScalarFunction}(), Int64[], 0, Dict{MOI.ConstraintIndex,Int64}(), - Int64[], - Int64[], + Dict{MOI.VariableIndex,Tuple{Int64,MOI.AbstractScalarFunction}}(), UInt16[], ) end @@ -69,38 +93,55 @@ end Base.isempty(map::Map) = all(bridge -> bridge === nothing, map.bridges) function Base.empty!(map::Map) + empty!(map.variable_to_slot) + empty!(map.slot_to_variable) + empty!(map.vov_to_slot) + empty!(map.vov_length) empty!(map.info) empty!(map.index_in_vector) empty!(map.bridges) empty!(map.sets) + empty!(map.parent_index) + map.current_context = 0 + empty!(map.constraint_context) if map.unbridged_function === nothing map.unbridged_function = Dict{MOI.VariableIndex,Tuple{Int64,MOI.AbstractScalarFunction}}() else empty!(something(map.unbridged_function)) end - empty!(map.parent_index) - map.current_context = 0 - empty!(map.constraint_context) - empty!(map.vector_of_variables_map) - empty!(map.vector_of_variables_length) empty!(map.set_mask) return map end +""" + bridge_index(map::Map, vi::MOI.VariableIndex) + +Return the internal slot that stores the bridge for the variable bridge that +created `vi` (the first slot of the vector if `vi` is in a vector). +""" function bridge_index(map::Map, vi::MOI.VariableIndex) - index = map.info[-vi.value] - if index ≤ 0 - return -vi.value + slot = map.variable_to_slot[vi.value] + return _first_slot(map, slot) +end + +# Given any slot, return the first-slot of the (possibly vector) bridge that owns it +function _first_slot(map::Map, slot::Integer) + info = map.info[slot] + if info <= 0 + return Int64(slot) else - return -vi.value - index + 1 + return Int64(slot) - info + 1 end end function Base.haskey(map::Map, vi::MOI.VariableIndex) - return -length(map.bridges) ≤ vi.value ≤ -1 && - map.bridges[bridge_index(map, vi)] !== nothing && - map.index_in_vector[-vi.value] != -1 + slot = get(map.variable_to_slot, vi.value, 0) + if slot == 0 + return false + end + first_s = _first_slot(map, slot) + return map.bridges[first_s] !== nothing && map.index_in_vector[slot] != -1 end function Base.getindex(map::Map, vi::MOI.VariableIndex) @@ -108,29 +149,34 @@ function Base.getindex(map::Map, vi::MOI.VariableIndex) end function Base.delete!(map::Map, vi::MOI.VariableIndex) - if iszero(map.info[-vi.value]) + slot = map.variable_to_slot[vi.value] + first_s = _first_slot(map, slot) + info_first = map.info[first_s] + if iszero(info_first) # Delete scalar variable - index = bridge_index(map, vi) - map.bridges[index] = nothing - map.sets[index] = nothing + map.bridges[first_s] = nothing + map.sets[first_s] = nothing elseif has_keys(map, [vi]) # Delete whole vector delete!(map, [vi]) else - # Delete variable in vector and resize vector - 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 + # Delete a single variable in the vector and shift positions + S = map.sets[first_s]::Type + vov_ci_value = -info_first + map.vov_length[(S, vov_ci_value)] -= 1 + # Walk through subsequent slots in the same vector and decrement positions + for s in slot:length(map.index_in_vector) + if map.index_in_vector[s] == -1 continue - elseif bridge_index(map, vi) != - bridge_index(map, MOI.VariableIndex(-i)) + end + if _first_slot(map, s) != first_s break end - map.index_in_vector[i] -= 1 + map.index_in_vector[s] -= 1 end end - map.set_mask[-vi.value] = MOI.Utilities._DELETED_VARIABLE - map.index_in_vector[-vi.value] = -1 + map.set_mask[slot] = MOI.Utilities._DELETED_VARIABLE + map.index_in_vector[slot] = -1 return map end @@ -143,11 +189,13 @@ function Base.delete!(map::Map, vis::Vector{MOI.VariableIndex}) ) end for vi in vis - map.set_mask[-vi.value] = MOI.Utilities._DELETED_VARIABLE - map.index_in_vector[-vi.value] = -1 + slot = map.variable_to_slot[vi.value] + map.set_mask[slot] = MOI.Utilities._DELETED_VARIABLE + map.index_in_vector[slot] = -1 end - map.bridges[bridge_index(map, first(vis))] = nothing - map.sets[bridge_index(map, first(vis))] = nothing + first_s = bridge_index(map, vis[1]) + map.bridges[first_s] = nothing + map.sets[first_s] = nothing return map end @@ -156,7 +204,7 @@ function Base.keys(map::Map) vi -> haskey(map, vi), MOI.Utilities.lazy_map( MOI.VariableIndex, - i -> MOI.VariableIndex(-i), + i -> MOI.VariableIndex(map.slot_to_variable[i]), eachindex(map.bridges), ), ) @@ -171,7 +219,8 @@ function number_of_variables(map::Map) if iszero(map.info[i]) num += 1 else - num += length_of_vector_of_variables(map, MOI.VariableIndex(-i)) + vi = MOI.VariableIndex(map.slot_to_variable[i]) + num += length_of_vector_of_variables(map, vi) end end end @@ -180,7 +229,7 @@ end function Base.values(map::Map) # We don't use `filter` as it would compute the resulting array which - # is not necessary if the caller just wants to iterater over `values`. + # is not necessary if the caller just wants to iterate over `values`. return Base.Iterators.Filter(bridge -> bridge !== nothing, map.bridges) end @@ -191,7 +240,8 @@ function Base.iterate(map::Map, state = 1) if state > length(map.bridges) return nothing else - return MOI.VariableIndex(-state) => map.bridges[state], state + 1 + vi = MOI.VariableIndex(map.slot_to_variable[state]) + return vi => map.bridges[state], state + 1 end end @@ -228,49 +278,47 @@ function first_variable(::Map, ci::MOI.ConstraintIndex{MOI.VariableIndex}) end """ - first_variable(::Map, ci::MOI.ConstraintIndex{MOI.VariableIndex}) + first_variable(::Map, ci::MOI.ConstraintIndex{MOI.VectorOfVariables}) 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]) + ci::MOI.ConstraintIndex{MOI.VectorOfVariables,S}, +) where {S} + return MOI.VariableIndex(map.slot_to_variable[map.vov_to_slot[(S, 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) - index = bridge_index(map, vi) - constraint_index = map.info[index] - if iszero(constraint_index) - constraint_index = -index + first_s = bridge_index(map, vi) + info_first = map.info[first_s] + if iszero(info_first) + # Scalar: ci.value == vi.value (by MOI convention) + return MOI.ConstraintIndex{F,S}(map.slot_to_variable[first_s]) + else + # Vector: ci.value stored as -info_first + return MOI.ConstraintIndex{F,S}(-info_first) 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 + first_slot = get(map.vov_to_slot, (S, ci.value), 0) + first_slot == 0 && return false + return !isnothing(map.bridges[first_slot]) && map.sets[first_slot] === 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 + slot = get(map.variable_to_slot, ci.value, 0) + slot == 0 && return false + return !isnothing(map.bridges[slot]) && map.sets[slot] === S end """ @@ -303,11 +351,11 @@ function MOI.add_constraint( ::S, ) where {T,S<:_BOUNDED_VARIABLE_SCALAR_SETS{T}} flag = MOI.Utilities._single_variable_flag(S) - index = -vi.value - mask = map.set_mask[index] + slot = map.variable_to_slot[vi.value] + mask = map.set_mask[slot] MOI.Utilities._throw_if_lower_bound_set(vi, S, mask, T) MOI.Utilities._throw_if_upper_bound_set(vi, S, mask, T) - map.set_mask[index] = mask | flag + map.set_mask[slot] = mask | flag return end @@ -329,7 +377,8 @@ function MOI.delete( ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, ) where {T,S<:_BOUNDED_VARIABLE_SCALAR_SETS{T}} flag = MOI.Utilities._single_variable_flag(S) - map.set_mask[-ci.value] &= ~flag + slot = map.variable_to_slot[ci.value] + map.set_mask[slot] &= ~flag return end @@ -341,7 +390,7 @@ 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}[ - constraint(map, MOI.VariableIndex(-i)) for + constraint(map, MOI.VariableIndex(map.slot_to_variable[i])) for i in eachindex(map.sets) if map.sets[i] == S ] end @@ -374,15 +423,28 @@ Return a `Bool` indicating whether `vis` was returned by [`add_keys_for_bridge`](@ref) and has not been deleted yet. """ function has_keys(map::Map, vis::Vector{MOI.VariableIndex}) - return isempty(vis) || ( - length_of_vector_of_variables(map, first(vis)) == length(vis) && - all( - vi -> bridge_index(map, vi) == bridge_index(map, first(vis)), - vis, - ) && - all(vi -> haskey(map, vi), vis) && - all(i -> vis[i].value < vis[i-1].value, 2:length(vis)) - ) + if isempty(vis) + return true + end + head = vis[1] + if !haskey(map, head) + return false + end + n = length_of_vector_of_variables(map, head) + if n != length(vis) + return false + end + head_slot = bridge_index(map, head) + for (k, vi) in enumerate(vis) + slot = get(map.variable_to_slot, vi.value, 0) + if slot != head_slot + k - 1 + return false + end + if !haskey(map, vi) + return false + end + end + return true end """ @@ -392,11 +454,13 @@ 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) - info = map.info[bridge_index(map, vi)] + first_s = bridge_index(map, vi) + info = map.info[first_s] if iszero(info) return 0 else - return map.vector_of_variables_length[-info] + S = map.sets[first_s]::Type + return map.vov_length[(S, -info)] end end @@ -409,7 +473,8 @@ end Return the index of `vi` in the vector of variables in which it was bridged. """ function index_in_vector_of_variables(map::Map, vi::MOI.VariableIndex) - return MOI.Bridges.IndexInVector(map.index_in_vector[-vi.value]) + slot = map.variable_to_slot[vi.value] + return MOI.Bridges.IndexInVector(map.index_in_vector[slot]) end """ @@ -421,117 +486,143 @@ returns `false` even if all bridges were deleted while `isempty` would return by [`MOI.Bridges.AbstractBridgeOptimizer`](@ref) to shortcut operations in case variable bridges are not used. """ -has_bridges(map::Map) = !isempty(map.info) +has_bridges(map::Map) = !isempty(map.bridges) """ - add_key_for_bridge(map::Map, bridge_fun::Function, - set::MOI.AbstractScalarSet) + add_key_for_bridge( + map::Map, + bridge_fun::Function, + set::MOI.AbstractScalarSet, + variable::MOI.VariableIndex, + ) -Create a new variable index `vi`, store the mapping `vi => bridge` and -associate `vi` to `typeof(set)`. It returns a tuple with `vi` and the +Register `variable` (whose `.value` must already be reserved from the inner +model via [`MOI.Bridges.reserve_variable_index`](@ref)) as a bridged variable +in `map`, store the mapping `variable => bridge_fun()` and associate +`variable` to `typeof(set)`. Returns a tuple with `variable` and the constraint index -`MOI.ConstraintIndex{MOI.VariableIndex, typeof(set)}(vi.value)`. +`MOI.ConstraintIndex{MOI.VariableIndex, typeof(set)}(variable.value)`. """ function add_key_for_bridge( map::Map, bridge_fun::Function, set::MOI.AbstractScalarSet, + variable::MOI.VariableIndex, ) push!(map.parent_index, map.current_context) - bridge_index = Int64(length(map.parent_index)) + slot = Int64(length(map.parent_index)) push!(map.info, 0) push!(map.index_in_vector, 0) push!(map.bridges, nothing) push!(map.sets, typeof(set)) push!(map.set_mask, 0x0000) - map.bridges[bridge_index] = call_in_context(map, bridge_index, bridge_fun) - index = -bridge_index - variable = MOI.VariableIndex(index) + push!(map.slot_to_variable, variable.value) + map.variable_to_slot[variable.value] = slot + map.bridges[slot] = call_in_context(map, slot, bridge_fun) if map.unbridged_function !== nothing - mappings = unbridged_map(something(map.bridges[bridge_index]), variable) + mappings = unbridged_map(something(map.bridges[slot]), variable) if mappings === nothing map.unbridged_function = nothing else for mapping in mappings push!( something(map.unbridged_function), - mapping.first => (bridge_index, mapping.second), + mapping.first => (slot, mapping.second), ) end end end MOI.add_constraint(map, variable, set) - return variable, MOI.ConstraintIndex{MOI.VariableIndex,typeof(set)}(index) + return variable, MOI.ConstraintIndex{MOI.VariableIndex,typeof(set)}(variable.value) end """ - function add_keys_for_bridge( + add_keys_for_bridge( map::Map, bridge_fun::Function, set::MOI.AbstractVectorSet, - is_available::Function, + variables::Vector{MOI.VariableIndex}, + ci::MOI.ConstraintIndex{MOI.VectorOfVariables}, ) -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 a constraint index -`ci::MOI.ConstraintIndex{MOI.VectorOfVariables, typeof(set)}` such that -`is_available(ci)`. +Register `variables` (whose `.value`s must already be reserved from the inner +model via [`MOI.Bridges.reserve_variable_index`](@ref)) as bridged variables +in `map`, store the mapping `vi => bridge_fun()` for each `vi ∈ variables` +and associate them to `typeof(set)`. `ci` must also have been reserved via +[`MOI.Bridges.reserve_constraint_index`](@ref). """ function add_keys_for_bridge( map::Map, bridge_fun::Function, set::S, - is_available::Function, + variables::Vector{MOI.VariableIndex}, + ci::MOI.ConstraintIndex{MOI.VectorOfVariables,S}, ) where {S<:MOI.AbstractVectorSet} - if iszero(MOI.dimension(set)) - return MOI.VariableIndex[], - MOI.ConstraintIndex{MOI.VectorOfVariables,typeof(set)}(0) + if isempty(variables) + return variables, ci end - push!(map.parent_index, map.current_context) - bridge_index = Int64(length(map.parent_index)) - 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) + dim = length(variables) + @assert dim == MOI.dimension(set) + # If `ci.value` is 0, the caller signals that no reservation was made + # (the inner model could not produce a colliding CI{VOV, S}). Allocate + # the next available local positive value for this `S`. + ci_value = if iszero(ci.value) + _next_local_vov_value(map, S) + else + ci.value 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.parent_index, map.current_context) + first_slot = Int64(length(map.parent_index)) + map.vov_to_slot[(S, ci_value)] = first_slot + map.vov_length[(S, ci_value)] = dim + push!(map.info, -ci_value) push!(map.index_in_vector, 1) push!(map.bridges, nothing) push!(map.sets, typeof(set)) push!(map.set_mask, 0x0000) - for i in 2:MOI.dimension(set) + push!(map.slot_to_variable, variables[1].value) + map.variable_to_slot[variables[1].value] = first_slot + for i in 2:dim push!(map.parent_index, 0) push!(map.info, i) push!(map.index_in_vector, i) push!(map.bridges, nothing) push!(map.sets, nothing) push!(map.set_mask, 0x0000) + push!(map.slot_to_variable, variables[i].value) + map.variable_to_slot[variables[i].value] = first_slot + i - 1 end - map.bridges[bridge_index] = call_in_context(map, bridge_index, bridge_fun) - variables = MOI.VariableIndex[ - MOI.VariableIndex(-(bridge_index - 1 + i)) for i in 1:MOI.dimension(set) - ] + map.bridges[first_slot] = call_in_context(map, first_slot, bridge_fun) if map.unbridged_function !== nothing - mappings = - unbridged_map(something(map.bridges[bridge_index]), variables) + mappings = unbridged_map(something(map.bridges[first_slot]), variables) if mappings === nothing map.unbridged_function = nothing else for mapping in mappings push!( something(map.unbridged_function), - mapping.first => (bridge_index, mapping.second), + mapping.first => (first_slot, mapping.second), ) end end end - return variables, MOI.ConstraintIndex{F,S}(constraint_index) + return variables, MOI.ConstraintIndex{MOI.VectorOfVariables,S}(ci_value) +end + +""" + _next_local_vov_value(map::Map, S::Type) + +Return the next positive integer that, combined with `S`, is not already a +key in `map.vov_to_slot`. Used by [`add_keys_for_bridge`](@ref) when the +caller did not reserve a `CI{VOV, S}` value because the inner model +cannot produce a colliding one. +""" +function _next_local_vov_value(map::Map, S::Type) + val = 1 + while haskey(map.vov_to_slot, (S, val)) + val += 1 + end + return val end """ @@ -550,18 +641,21 @@ end 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] +function function_for( + map::Map, + ci::MOI.ConstraintIndex{MOI.VectorOfVariables,S}, +) where {S} + first_slot = map.vov_to_slot[(S, ci.value)] + dim = map.vov_length[(S, ci.value)] variables = MOI.VariableIndex[] - for i in index:-1:(-length(map.bridges)) - vi = MOI.VariableIndex(i) - if map.index_in_vector[-vi.value] == -1 + for s in first_slot:(first_slot + dim - 1) + if s > length(map.index_in_vector) || map.index_in_vector[s] == -1 continue - elseif bridge_index(map, vi) == -index - push!(variables, vi) - else + end + if _first_slot(map, s) != first_slot break end + push!(variables, MOI.VariableIndex(map.slot_to_variable[s])) end return MOI.VectorOfVariables(variables) end @@ -594,12 +688,12 @@ function unbridged_function(map::Map, vi::MOI.VariableIndex) if context_func === nothing return nothing end - bridge_index, func = context_func - # If the bridge bridging `vi` has index `bridge_index` or directly or + bridge_slot, func = context_func + # If the bridge bridging `vi` has slot `bridge_slot` or directly or # indirectly created this bridge then we don't unbridge the variable. context = map.current_context while !iszero(context) - if bridge_index == context + if bridge_slot == context return nothing end context = map.parent_index[context] @@ -608,20 +702,21 @@ function unbridged_function(map::Map, vi::MOI.VariableIndex) end """ - call_in_context(map::Map, bridge_index::Int64, f::Function) + call_in_context(map::Map, bridge_slot::Int64, f::Function) -Call function `f` in the context of the variable bridge of index `bridge_index`. -That is, the variable indices bridged by this bridge or the bridges that -created it will not be unbridged in [`unbridged_function`](@ref). +Call function `f` in the context of the variable bridge of slot +`bridge_slot`. That is, the variable indices bridged by this bridge or the +bridges that created it will not be unbridged in +[`unbridged_function`](@ref). """ -function call_in_context(map::Map, bridge_index::Int64, f::Function) +function call_in_context(map::Map, bridge_slot::Int64, f::Function) # This is a shortcut that is used in particular in the common case where # no variable bridge is used. - if iszero(bridge_index) && iszero(map.current_context) + if iszero(bridge_slot) && iszero(map.current_context) return f() end previous_context = map.current_context - map.current_context = bridge_index + map.current_context = bridge_slot output = nothing try output = f() @@ -634,18 +729,18 @@ end """ call_in_context(map::Map, vi::MOI.VariableIndex, f::Function) -Shortcut for `call_in_context(map, bridge_index, () -> f(bridge))` where -`vi` is bridged by `bridge` with index `bridge_index`. +Shortcut for `call_in_context(map, bridge_slot, () -> f(bridge))` where +`vi` is bridged by `bridge` with slot `bridge_slot`. """ function call_in_context(map::Map, vi::MOI.VariableIndex, f::Function) - idx = bridge_index(map, vi) - return call_in_context(map, idx, () -> f(map.bridges[idx])) + slot = bridge_index(map, vi) + return call_in_context(map, slot, () -> f(map.bridges[slot])) end """ call_in_context(map::Map, ci::MOI.ConstraintIndex, f::Function) -Shortcut for `call_in_context(map, bridge_index, f)` where `bridge_index` is the +Shortcut for `call_in_context(map, bridge_slot, f)` where `bridge_slot` is the variable bridge that created `ci` (directly or indirectly) or 0 otherwise. """ function call_in_context(map::Map, ci::MOI.ConstraintIndex, f::Function) diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 4dde113090..63ef4cfb5f 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -65,7 +65,14 @@ function is_bridged end Return a `Bool` indicating whether `vi` is bridged. The variable is said to be bridged if it is a variable of `b` but not a variable of `b.model`. """ -is_bridged(::AbstractBridgeOptimizer, vi::MOI.VariableIndex) = vi.value < 0 +function is_bridged(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex) + map = Variable.bridges(b) + # Fast path: when no variable bridges are in use, the answer is `false` + # without any dictionary lookup. This preserves the cost of the no-bridges + # case from before the negative-index convention was removed. + Variable.has_bridges(map) || return false + return haskey(map, vi) +end """ is_bridged(b::AbstractBridgeOptimizer, ci::MOI.ConstraintIndex) @@ -84,40 +91,33 @@ function is_bridged( b::AbstractBridgeOptimizer, ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, ) where {S} - # There are a few cases for which we should return `false`: - # 1) It was added as variables constrained on creation to `b.model`, - # In this case, `is_bridged(b, S)` is `false` and `ci.value >= 0`. - # 2) It was added as constraint on a non-bridged variable to `b.model`, - # In this case, `is_bridged(b, F, S)` is `false` and `ci.value >= 0`. - # and a few cases for which we should return `true`: - # 3) It was added with a variable bridge, - # In this case, `is_bridged(b, S)` is `true` and `ci.value < 0`. - # 4) It was added as constraint on a bridged variable so it was force-bridged, - # In this case, `ci.value < 0`. - # 5) It was added with a constraint bridge, - # In this case, `is_bridged(b, F, S)` is `true` and `ci.value >= 0` (the variable is non-bridged, otherwise, the constraint would have been force-bridged). - # So - # * if, `ci.value < 0` then it is case 3) or 4) and we return `true`. - # * Otherwise, - # - if `is_bridged(b, S)` and `is_bridged(b, F, S)` then 1) and 2) are - # not possible so we are in case 5) and we return `true`. - # - if `!is_bridged(b, F, S)`, then 5) is not possible and we return `false`. - # - if `!is_bridged(b, S)` and `is_bridged(b, F, S)`, then it is either case 1) - # or 5). They cannot both be the cases as one cannot add two `VariableIndex` - # with the same set type on the same variable (this is ensured by - # `_check_double_single_variable`). Therefore, we can safely determine - # whether it is bridged with `haskey(Constraint.bridges(b), ci)`. - return ci.value < 0 || ( - is_bridged(b, MOI.VariableIndex, S) && - (is_bridged(b, S) || haskey(Constraint.bridges(b), ci)) - ) + # Three possibilities (the historical 5-case logic collapses with the + # removal of the negative-index convention): + # + # * cases 3 & 4 — variable bridge involvement: by MOI convention the + # `ci.value` of a `VariableIndex` constraint equals the constrained + # variable's `vi.value`. If that variable is bridged, the constraint + # was either created by `add_key_for_bridge` (case 3) or + # force-bridged because it was added on a bridged variable (case 4). + # * case 5 — constraint bridge: looked up in `Constraint.bridges`. + # * cases 1 & 2 — passes through to the inner model. + map = Variable.bridges(b) + if Variable.has_bridges(map) && + haskey(map, MOI.VariableIndex(ci.value)) + return true + end + return haskey(Constraint.bridges(b), ci) end function is_bridged( - ::AbstractBridgeOptimizer, + b::AbstractBridgeOptimizer, ci::MOI.ConstraintIndex{MOI.VectorOfVariables,S}, ) where {S} - return ci.value < 0 + map = Variable.bridges(b) + if Variable.has_bridges(map) && haskey(map.vov_to_slot, (S, ci.value)) + return true + end + return haskey(Constraint.bridges(b), ci) end """ @@ -168,21 +168,34 @@ end """ is_variable_bridged(b::AbstractBridgeOptimizer, ci::MOI.ConstraintIndex) -Returns whether `ci` is the constraint of a bridged constrained variable. That -is, if it was returned by `Variable.add_key_for_bridge` or -`Variable.add_keys_for_bridge`. Note that it is not equivalent to -`ci.value < 0` as, it can also simply be a constraint on a bridged variable. +Returns whether `ci` is a constraint whose owning bridge is in +[`Variable.bridges(b)`](@ref). That is, if it was returned by +`Variable.add_key_for_bridge`, `Variable.add_keys_for_bridge`, or is a +constraint added on a bridged variable (force-bridged). """ is_variable_bridged(::AbstractBridgeOptimizer, ::MOI.ConstraintIndex) = false function is_variable_bridged( b::AbstractBridgeOptimizer, - ci::MOI.ConstraintIndex{<:Union{MOI.VariableIndex,MOI.VectorOfVariables}}, + ci::MOI.ConstraintIndex{MOI.VariableIndex}, ) - # It can be a constraint corresponding to bridged constrained variables so - # we `check` with `haskey(Constraint.bridges(b), ci)` whether this is the - # case. - return ci.value < 0 && !haskey(Constraint.bridges(b), ci) + map = Variable.bridges(b) + Variable.has_bridges(map) || return false + # `ci` is a variable-bridge-owned constraint iff the variable is bridged + # AND `ci` was *not* stored as a constraint bridge (which would mean it + # was force-bridged on a bridged variable via `add_bridged_constraint`). + return haskey(map, MOI.VariableIndex(ci.value)) && + !haskey(Constraint.bridges(b), ci) +end + +function is_variable_bridged( + b::AbstractBridgeOptimizer, + ci::MOI.ConstraintIndex{MOI.VectorOfVariables,S}, +) where {S} + map = Variable.bridges(b) + Variable.has_bridges(map) || return false + return haskey(map.vov_to_slot, (S, ci.value)) && + !haskey(Constraint.bridges(b), ci) end """ @@ -247,7 +260,7 @@ end reserve_constraint_index( model::MOI.ModelLike, ::Type{F}, - ::Type{S}, + set::S, )::MOI.ConstraintIndex{F,S} where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet} Return a `MOI.ConstraintIndex{F,S}` that is guaranteed not to collide with @@ -258,41 +271,42 @@ This is used by bridge optimizers to allocate identities for force-bridged or constraint-bridged `(F, S)` constraints that do not collide with the inner model's namespace, so that multiple bridge layers can be stacked. -The default implementation adds a dummy `F`-in-`S` constraint and -immediately deletes it (along with any temporary variables created to build -the dummy), relying on the fact that MOI models do not recycle deleted -constraint indices. +`set` must be a valid instance of `S` (e.g., the same instance the caller +intends to constrain). It is used to build a dummy `F`-in-`S` constraint +that is immediately deleted (along with any temporary variables created +to build the dummy), relying on the fact that MOI models do not recycle +deleted constraint indices. Reservation can be skipped when the inner model supports neither `(F, S)` as a constraint nor constrained-variables-in-`S`, in which case no colliding `ConstraintIndex{F,S}` can ever be produced by the inner model. -For function types `F` and set types `S` not handled by the default, +For function types `F` not handled by the default implementation, specialize this method on the model type. """ function reserve_constraint_index( model::MOI.ModelLike, ::Type{F}, - ::Type{S}, -) where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet} - return _default_reserve_constraint_index(model, F, S) + set::MOI.AbstractSet, +) where {F<:MOI.AbstractFunction} + return _default_reserve_constraint_index(model, F, set) end function reserve_constraint_index( b::AbstractBridgeOptimizer, ::Type{F}, - ::Type{S}, -) where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet} - return reserve_constraint_index(b.model, F, S) + set::MOI.AbstractSet, +) where {F<:MOI.AbstractFunction} + return reserve_constraint_index(b.model, F, set) end function _default_reserve_constraint_index( model::MOI.ModelLike, ::Type{MOI.VariableIndex}, - ::Type{S}, -) where {S<:MOI.AbstractScalarSet} + set::MOI.AbstractScalarSet, +) vi = MOI.add_variable(model) - ci = MOI.add_constraint(model, vi, _dummy_set(S)) + ci = MOI.add_constraint(model, vi, set) MOI.delete(model, vi) # also deletes ci return ci end @@ -300,9 +314,16 @@ end function _default_reserve_constraint_index( model::MOI.ModelLike, ::Type{MOI.VectorOfVariables}, - ::Type{S}, -) where {S<:MOI.AbstractVectorSet} - set = _dummy_set(S) + set::MOI.AbstractVectorSet, +) + S = typeof(set) + if !MOI.supports_constraint(model, MOI.VectorOfVariables, S) && + !MOI.supports_add_constrained_variables(model, S) + # The inner model cannot produce a colliding `CI{VOV, S}`, so no + # reservation is needed. The sentinel `ci.value == 0` signals to + # the caller that it must allocate a local index. + return MOI.ConstraintIndex{MOI.VectorOfVariables,S}(0) + end vis = MOI.add_variables(model, MOI.dimension(set)) ci = MOI.add_constraint(model, MOI.VectorOfVariables(vis), set) MOI.delete(model, ci) @@ -315,9 +336,9 @@ end function _default_reserve_constraint_index( model::MOI.ModelLike, ::Type{F}, - ::Type{S}, -) where {F<:MOI.AbstractScalarFunction,S<:MOI.AbstractScalarSet} - ci = MOI.add_constraint(model, zero(F), _dummy_set(S)) + set::MOI.AbstractScalarSet, +) where {F<:MOI.AbstractScalarFunction} + ci = MOI.add_constraint(model, zero(F), set) MOI.delete(model, ci) return ci end @@ -325,40 +346,14 @@ end function _default_reserve_constraint_index( model::MOI.ModelLike, ::Type{F}, - ::Type{S}, -) where {F<:MOI.AbstractVectorFunction,S<:MOI.AbstractVectorSet} - set = _dummy_set(S) + set::MOI.AbstractVectorSet, +) where {F<:MOI.AbstractVectorFunction} f = MOI.Utilities.zero_with_output_dimension(F, MOI.dimension(set)) ci = MOI.add_constraint(model, f, set) MOI.delete(model, ci) return ci end -""" - _dummy_set(::Type{S}) where {S<:MOI.AbstractSet} - -Return a trivial instance of `S` used as a placeholder when reserving a -`ConstraintIndex{F,S}` via [`_reserve_dummy_constraint`](@ref). Sets whose -constructor requires parameters that cannot be defaulted must specialize -this method (or skip reservation entirely if the inner model does not -support the corresponding constraint). -""" -function _dummy_set end - -_dummy_set(::Type{S}) where {T,S<:MOI.EqualTo{T}} = S(zero(T)) -_dummy_set(::Type{S}) where {T,S<:MOI.GreaterThan{T}} = S(zero(T)) -_dummy_set(::Type{S}) where {T,S<:MOI.LessThan{T}} = S(zero(T)) -_dummy_set(::Type{S}) where {T,S<:MOI.Interval{T}} = S(zero(T), zero(T)) -_dummy_set(::Type{MOI.Integer}) = MOI.Integer() -_dummy_set(::Type{MOI.ZeroOne}) = MOI.ZeroOne() -_dummy_set(::Type{S}) where {T,S<:MOI.Semicontinuous{T}} = S(zero(T), zero(T)) -_dummy_set(::Type{S}) where {T,S<:MOI.Semiinteger{T}} = S(zero(T), zero(T)) -_dummy_set(::Type{S}) where {T,S<:MOI.Parameter{T}} = S(zero(T)) -_dummy_set(::Type{MOI.Reals}) = MOI.Reals(1) -_dummy_set(::Type{MOI.Zeros}) = MOI.Zeros(1) -_dummy_set(::Type{MOI.Nonnegatives}) = MOI.Nonnegatives(1) -_dummy_set(::Type{MOI.Nonpositives}) = MOI.Nonpositives(1) - """ bridge_type( b::AbstractBridgeOptimizer, @@ -898,7 +893,8 @@ function MOI.delete( else delete!(Constraint.bridges(b)::Constraint.Map, ci) end - if F === MOI.VariableIndex && ci.value < 0 + if F === MOI.VariableIndex && + haskey(Variable.bridges(b), MOI.VariableIndex(ci.value)) # Constraint on a bridged variable so we need to remove the flag # if it is a bound MOI.delete(Variable.bridges(b), ci) @@ -2429,16 +2425,21 @@ 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. + # Reserve a vector of variable indices and a VectorOfVariables + # constraint index from the inner model so that they don't collide + # with anything in the inner namespace (including bridged variables + # from any inner bridge layer). + dim = MOI.dimension(set) + variables = MOI.VariableIndex[ + reserve_variable_index(b.model) for _ in 1:dim + ] + ci = reserve_constraint_index(b.model, MOI.VectorOfVariables, set) 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)), + variables, + ci, ) else variables = MOI.add_variables(b, MOI.dimension(set)) @@ -2468,6 +2469,11 @@ function MOI.add_constrained_variable( end if is_variable_bridged(b, typeof(set)) BridgeType = Variable.concrete_bridge_type(b, typeof(set)) + # Reserve a variable index from the inner model so it doesn't + # collide with the inner namespace. The corresponding scalar + # constraint shares `ci.value == vi.value` by MOI convention, so + # no separate constraint reservation is needed. + variable = reserve_variable_index(b.model) return Variable.add_key_for_bridge( Variable.bridges(b)::Variable.Map, () -> Variable.bridge_constrained_variable( @@ -2476,6 +2482,7 @@ function MOI.add_constrained_variable( set, ), set, + variable, ) else variable = MOI.add_variable(b) From 761740ebccd72f0fe1da92b029b884d93c8651e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 10 Jun 2026 18:23:07 +0100 Subject: [PATCH 3/3] Reorder --- src/Bridges/Variable/map.jl | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Bridges/Variable/map.jl b/src/Bridges/Variable/map.jl index 275ec4b9e4..3b7506d545 100644 --- a/src/Bridges/Variable/map.jl +++ b/src/Bridges/Variable/map.jl @@ -57,15 +57,17 @@ mutable struct Map <: AbstractDict{MOI.VariableIndex,AbstractBridge} index_in_vector::Vector{Int64} bridges::Vector{Union{Nothing,AbstractBridge}} sets::Vector{Union{Nothing,Type}} - # Parent context (slot) of the bridge that created this slot, 0 if root - parent_index::Vector{Int64} - current_context::Int64 - constraint_context::Dict{MOI.ConstraintIndex,Int64} # If `nothing`, it cannot be computed because some bridges does not support it unbridged_function::Union{ Nothing, Dict{MOI.VariableIndex,Tuple{Int64,MOI.AbstractScalarFunction}}, } + # Parent context (slot) of the bridge that created this slot, 0 if root + parent_index::Vector{Int64} + # Current bridge, 0 otherwise. + current_context::Int64 + # Context of constraint bridged by constraint bridges + constraint_context::Dict{MOI.ConstraintIndex,Int64} # Same as in `MOI.Utilities.VariablesContainer` set_mask::Vector{UInt16} end @@ -80,10 +82,10 @@ function Map() Int64[], Union{Nothing,AbstractBridge}[], Union{Nothing,Type}[], + Dict{MOI.VariableIndex,Tuple{Int64,MOI.AbstractScalarFunction}}(), Int64[], 0, Dict{MOI.ConstraintIndex,Int64}(), - Dict{MOI.VariableIndex,Tuple{Int64,MOI.AbstractScalarFunction}}(), UInt16[], ) end