From af0ef444d77b44d65444b13e6882d615ce2722bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 10 Jun 2026 20:49:00 +0200 Subject: [PATCH 1/4] Allow stacking of bridge optimier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation working (Variable/map.jl + bridge_optimizer.jl): - Variable.Map has outer_to_inner / inner_to_outer IndexMaps, next_outer_variable counter, per-(F,S) constraint counters - activate_variable_mapping!(map, model) / activate_constraint_mapping!(map, model, F, S) — lazy triggers, called from add_constrained_variable(s) when a variable bridge is created - _record_inner_variable!(b, inner_vi) — translates inner→outer when mapping active, identity otherwise - MOI.add_variable / MOI.add_variables / MOI.add_constrained_variable(s) updated to translate Verified manually: - Basic single-layer flow: bridged variables (xs = [-1, -2, -3]) still use slot/negative scheme internally. Non-bridged passthrough variables get outer indices via IndexMap when active. - Two stacked SingleBridgeOptimizers succeed at add_constrained_variables — the outer pulls inner-allocated indices (positive) through _record_inner_variable! while its own bridges keep negative outer indices. Test status on test/Bridges/General/test_bridge_optimizer.jl: - test_CallbackVariablePrimal errors — MOI.get(b, attr, vi) doesn't translate vi outer→inner yet - test_nesting_SingleBridgeOptimizer still 3 fails — the cnn CI{VOV, Nonneg} collision case, same as before. Will be fixed by the constraint-mapping plumbing. - Rest pass. Big remaining work (rough order): 1. Translate vi in MOI.is_valid, MOI.delete, MOI.get, MOI.set, MOI.modify, MOI.supports and the callback / primal / dual attribute getters 2. Translate functions containing variable indices via MOI.Utilities.map_indices when crossing the boundary (ConstraintFunction, ObjectiveFunction, VariablePrimal, change types, etc.) 3. Constraint mapping per (F, S) — activate on first VI/VOV force-bridge, translate at boundaries 4. Remove negative-index conventions --- src/Bridges/Variable/map.jl | 158 ++++++++++++++++++++++++++++++++ src/Bridges/bridge_optimizer.jl | 106 +++++++++++++++++++-- 2 files changed, 256 insertions(+), 8 deletions(-) diff --git a/src/Bridges/Variable/map.jl b/src/Bridges/Variable/map.jl index 806d4b939a..d91e376c2f 100644 --- a/src/Bridges/Variable/map.jl +++ b/src/Bridges/Variable/map.jl @@ -8,6 +8,29 @@ Map <: AbstractDict{MOI.VariableIndex, AbstractBridge} Mapping between bridged variables and the bridge that bridged the variable. + +## Outer / inner index spaces + +The user-facing ("outer") `VariableIndex` and `ConstraintIndex` namespaces are +independent from the ones used by `b.model` ("inner"). When no variable bridge +has ever been added to this `Map` the two namespaces coincide (identity +mapping) and no translation is performed. As soon as the first variable bridge +is added, [`activate_variable_mapping!`](@ref) is called: every existing +inner variable is copied into [`outer_to_inner`](@ref) and +[`inner_to_outer`](@ref) as an identity entry, and from that point on the +two namespaces drift apart: bridged variables get a fresh outer index with no +inner counterpart, non-bridged variables get a fresh outer index recorded +alongside their inner index. + +Constraint indices follow the same rule per `(F, S)` pair. The first force- +bridged `CI{VariableIndex, S}` or `CI{VectorOfVariables, S}` triggers +[`activate_constraint_mapping!`](@ref) for that `(F, S)`: all existing inner +`CI{F, S}` are copied in as identity entries; afterwards the outer and inner +`CI{F, S}` namespaces are independent. + +Outer-only entries (bridged variables, force-bridged constraints) appear as +keys in `outer_to_inner` with a sentinel value of `0` (and are absent from +`inner_to_outer`). """ mutable struct Map <: AbstractDict{MOI.VariableIndex,AbstractBridge} # Bridged constrained variables @@ -46,6 +69,21 @@ mutable struct Map <: AbstractDict{MOI.VariableIndex,AbstractBridge} vector_of_variables_length::Vector{Int64} # Same as in `MOI.Utilities.VariablesContainer` set_mask::Vector{UInt16} + # Outer (user-facing) -> inner (`b.model`) translation. Empty until the + # first variable bridge is added, at which point existing inner variables + # are added as identity. After activation, every variable in the outer + # namespace has an entry here (bridged ones map to the sentinel `0`). + outer_to_inner::MOI.Utilities.IndexMap + # Reverse of `outer_to_inner` for the entries that have an inner + # counterpart (i.e., bridged outer-only entries are absent). + inner_to_outer::MOI.Utilities.IndexMap + # Next available outer `VariableIndex.value` once variable mapping has + # been activated; `0` until then. + next_outer_variable::Int64 + # Per-`(F, S)` next available outer `ConstraintIndex{F, S}.value`. A + # missing entry means that `(F, S)` is in identity mode; presence means + # constraint mapping has been activated for `(F, S)`. + next_outer_constraint::Dict{Tuple{DataType,DataType},Int64} end function Map() @@ -61,6 +99,10 @@ function Map() Int64[], Int64[], UInt16[], + MOI.Utilities.IndexMap(), + MOI.Utilities.IndexMap(), + 0, + Dict{Tuple{DataType,DataType},Int64}(), ) end @@ -85,9 +127,125 @@ function Base.empty!(map::Map) empty!(map.vector_of_variables_map) empty!(map.vector_of_variables_length) empty!(map.set_mask) + map.outer_to_inner = MOI.Utilities.IndexMap() + map.inner_to_outer = MOI.Utilities.IndexMap() + map.next_outer_variable = 0 + empty!(map.next_outer_constraint) return map end +""" + is_variable_mapping_active(map::Map)::Bool + +Return `true` once at least one variable bridge has been added (and hence +the outer/inner translation has been materialized). +""" +is_variable_mapping_active(map::Map) = map.next_outer_variable != 0 + +""" + is_constraint_mapping_active(map::Map, ::Type{F}, ::Type{S})::Bool + +Return `true` once at least one `CI{F, S}` has been force-bridged at this +layer (and hence the outer/inner translation for `(F, S)` has been +materialized). +""" +function is_constraint_mapping_active( + map::Map, + ::Type{F}, + ::Type{S}, +) where {F,S} + return haskey(map.next_outer_constraint, (F, S)) +end + +""" + activate_variable_mapping!(map::Map, model::MOI.ModelLike) + +Materialize identity mappings for every variable currently in `model`, so +that subsequent outer-only or inner-only allocations can extend the two +namespaces independently. No-op if the mapping is already active. + +`model` is the inner model that this `Map` translates against (typically +`b.model` of the enclosing `AbstractBridgeOptimizer`). +""" +function activate_variable_mapping!(map::Map, model::MOI.ModelLike) + if is_variable_mapping_active(map) + return + end + max_value = Int64(0) + for inner_vi in MOI.get(model, MOI.ListOfVariableIndices()) + map.outer_to_inner[inner_vi] = inner_vi + map.inner_to_outer[inner_vi] = inner_vi + if inner_vi.value > max_value + max_value = inner_vi.value + end + end + map.next_outer_variable = max_value + 1 + return +end + +""" + activate_constraint_mapping!( + map::Map, + model::MOI.ModelLike, + ::Type{F}, + ::Type{S}, + ) + +Materialize identity mappings for every `CI{F, S}` currently in `model`. +Called when the first `CI{F, S}` is force-bridged at this layer. No-op if +already active for `(F, S)`. +""" +function activate_constraint_mapping!( + map::Map, + model::MOI.ModelLike, + ::Type{F}, + ::Type{S}, +) where {F,S} + if is_constraint_mapping_active(map, F, S) + return + end + max_value = Int64(0) + for inner_ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + map.outer_to_inner[inner_ci] = inner_ci + map.inner_to_outer[inner_ci] = inner_ci + if inner_ci.value > max_value + max_value = inner_ci.value + end + end + map.next_outer_constraint[(F, S)] = max_value + 1 + return +end + +""" + next_outer_variable!(map::Map)::Int64 + +Return a fresh `Int64` value to use as a `VariableIndex.value` in the outer +namespace and advance the internal counter. +""" +function next_outer_variable!(map::Map) + @assert is_variable_mapping_active(map) + value = map.next_outer_variable + map.next_outer_variable = value + 1 + return value +end + +""" + next_outer_constraint!(map::Map, ::Type{F}, ::Type{S})::Int64 + +Return a fresh `Int64` value to use as a `ConstraintIndex{F, S}.value` in +the outer namespace and advance the internal `(F, S)` counter. +""" +function next_outer_constraint!( + map::Map, + ::Type{F}, + ::Type{S}, +) where {F,S} + @assert is_constraint_mapping_active(map, F, S) + value = map.next_outer_constraint[(F, S)] + map.next_outer_constraint[(F, S)] = value + 1 + return value +end + function bridge_index(map::Map, vi::MOI.VariableIndex) index = map.info[-vi.value] if index ≤ 0 diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index a13ab5093d..972dd7672d 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -464,12 +464,49 @@ function MOI.Utilities.final_touch(b::AbstractBridgeOptimizer, index_map) end # References +""" + _to_inner_var(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex)::MOI.VariableIndex + +Translate an outer `VariableIndex` to its inner counterpart for forwarding +to `b.model`. When the variable mapping is inactive (or `b` does not own a +`Variable.Map`), returns `vi` unchanged. The caller is responsible for +having already established that `vi` is not bridged at `b`'s layer. +""" +function _to_inner_var(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex) + map = Variable.bridges(b) + if map isa Variable.Map && Variable.is_variable_mapping_active(map) + return map.outer_to_inner[vi] + end + return vi +end + +""" + _to_outer_var(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex)::MOI.VariableIndex + +Translate an inner `VariableIndex` returned by `b.model` into the outer +namespace. When the variable mapping is inactive, returns `vi` unchanged. +""" +function _to_outer_var(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex) + map = Variable.bridges(b) + if map isa Variable.Map && Variable.is_variable_mapping_active(map) + return map.inner_to_outer[vi] + end + return vi +end + function MOI.is_valid(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex) if is_bridged(b, vi) return haskey(Variable.bridges(b), vi) - else - return MOI.is_valid(b.model, vi) end + map = Variable.bridges(b) + if map isa Variable.Map && Variable.is_variable_mapping_active(map) + # Outer/inner translation is in effect. Outer `vi` must be in + # `outer_to_inner` to be valid; the entry then points to the inner + # `VariableIndex` to check. + haskey(map.outer_to_inner, vi) || return false + return MOI.is_valid(b.model, map.outer_to_inner[vi]) + end + return MOI.is_valid(b.model, vi) end function MOI.is_valid( @@ -2236,24 +2273,47 @@ function MOI.modify( return end +# Variables + +""" + _record_inner_variable!(b::AbstractBridgeOptimizer, inner_vi::MOI.VariableIndex) + +If variable mapping is active in `b`, allocate a fresh outer `VariableIndex` +value and record the bidirectional mapping. Otherwise (identity mode, or `b` +does not own a `Variable.Map` at all), return `inner_vi` unchanged. +""" +function _record_inner_variable!( + b::AbstractBridgeOptimizer, + inner_vi::MOI.VariableIndex, +) + map = Variable.bridges(b) + if !(map isa Variable.Map) || !Variable.is_variable_mapping_active(map) + return inner_vi + end + outer_vi = MOI.VariableIndex(Variable.next_outer_variable!(map)) + map.outer_to_inner[outer_vi] = inner_vi + map.inner_to_outer[inner_vi] = outer_vi + return outer_vi +end + # Variables function MOI.add_variable(b::AbstractBridgeOptimizer) if is_bridged(b, MOI.Reals) variables, constraint = MOI.add_constrained_variables(b, MOI.Reals(1)) @assert isone(length(variables)) return first(variables) - else - return MOI.add_variable(b.model) end + inner_vi = MOI.add_variable(b.model) + return _record_inner_variable!(b, inner_vi) end function MOI.add_variables(b::AbstractBridgeOptimizer, n) if is_bridged(b, MOI.Reals) variables, constraint = MOI.add_constrained_variables(b, MOI.Reals(n)) return variables - else - return MOI.add_variables(b.model, n) end + inner_vis = MOI.add_variables(b.model, n) + return MOI.VariableIndex[_record_inner_variable!(b, vi) for vi in inner_vis] end # Split in two to avoid ambiguity @@ -2284,10 +2344,25 @@ function MOI.add_constrained_variables( set::MOI.AbstractVectorSet, ) if !is_bridged(b, typeof(set)) - return MOI.add_constrained_variables(b.model, set) + inner_vis, inner_ci = MOI.add_constrained_variables(b.model, set) + outer_vis = MOI.VariableIndex[ + _record_inner_variable!(b, vi) for vi in inner_vis + ] + # The constraint index value of `inner_ci` may need translating once + # we plumb constraint mapping; for now identity since this branch + # doesn't go through a variable bridge. + return outer_vis, inner_ci end if set isa MOI.Reals || is_variable_bridged(b, typeof(set)) BridgeType = Variable.concrete_bridge_type(b, typeof(set)) + # Activate the outer/inner variable translation map at this layer + # before allocating the new bridged variables. This ensures every + # variable visible to the user from now on lives in the outer + # namespace. + Variable.activate_variable_mapping!( + Variable.bridges(b)::Variable.Map, + b.model, + ) # `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 @@ -2323,10 +2398,25 @@ function MOI.add_constrained_variable( set::MOI.AbstractScalarSet, ) if !is_bridged(b, typeof(set)) - return MOI.add_constrained_variable(b.model, set) + inner_vi, inner_ci = MOI.add_constrained_variable(b.model, set) + outer_vi = _record_inner_variable!(b, inner_vi) + # `CI{VariableIndex, S}.value == vi.value` by MOI convention; if we + # translated the variable, translate the constraint identically. + outer_ci = if outer_vi === inner_vi + inner_ci + else + MOI.ConstraintIndex{MOI.VariableIndex,typeof(set)}(outer_vi.value) + end + return outer_vi, outer_ci end if is_variable_bridged(b, typeof(set)) BridgeType = Variable.concrete_bridge_type(b, typeof(set)) + # Activate the outer/inner variable translation at this layer before + # allocating the new bridged variable. + Variable.activate_variable_mapping!( + Variable.bridges(b)::Variable.Map, + b.model, + ) return Variable.add_key_for_bridge( Variable.bridges(b)::Variable.Map, () -> Variable.bridge_constrained_variable( From ff5b32cb2cc3afe6469a6a70f2e6c9623f62a88a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 15 Jun 2026 10:19:13 +0200 Subject: [PATCH 2/4] Progress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core namespace machinery (src/Bridges/bridge_optimizer.jl): - outer_to_inner(b, idx) / inner_to_outer(b, idx) — the canonical "if variable bridges are used → use the index map, else identity" helpers, for both VariableIndex and ConstraintIndex{F,S}. CI{VariableIndex,S} translation is derived from the variable mapping (since ci.value == vi.value by MOI convention), so it needs no stored entries. - _OuterToInner / _InnerToOuter / _TotalInnerToOuter functor structs (<: Function so they work with map_indices). The "total" variant leaves unrecorded indices unchanged — those are bridge-created inner variables later removed by unbridged_function. - _to_inner_value / _from_inner_value — whole-value translation at the b.model boundary, applied via map_indices. - _unbridged_result_from_inner / _unbridged_result_from_bridge — result processing that respects where a value came from: recursive_model(b) === b (Lazy, outer namespace) vs b.model (SBO, inner namespace). Key insight encoded in bridged_variable_function: for LazyBridgeOptimizer the substituted expressions stay outer (translation happens at the model boundary); for SingleBridgeOptimizer they're already inner, so passthrough variables translate during substitution and the recursion is skipped. Plumbed sites: add_constraint(s), add_constrained_variable(s), delete (vi/ci/vectors, with map cleanup), is_valid, variable/constraint/model attribute get/set, ObjectiveFunction get, modify (with _to_inner_change and _modify_substituted_change to avoid double-translating decomposed modifications), VariableName/ConstraintName get/set, name→index lookups (including reverse-lookup of names delegated to bridge-created variables), ListOfVariableIndices. Activation triggers: variable mapping activates on first variable bridge; per-(F,S) constraint mapping activates on first force-bridged VOV; add_bridged_constraint avoids identity-copied indices via _is_available_constraint_index. One Utilities change: substitute_variables(map, x::VariableIndex) now allows index-to-index renaming, erroring only when bridged into a real function. Test updates (3 places where tests reached around the bridge layer to the mock with outer indices — they now use the inner index, consistent with how they already handled y). --- src/Bridges/bridge_optimizer.jl | 767 ++++++++++++++++-- src/Utilities/functions.jl | 9 +- test/Bridges/General/test_bridge_optimizer.jl | 22 +- .../Variable/test_ParameterToEqualToBridge.jl | 5 +- 4 files changed, 708 insertions(+), 95 deletions(-) diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 972dd7672d..5f634869dc 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -465,28 +465,69 @@ end # References """ - _to_inner_var(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex)::MOI.VariableIndex + outer_to_inner(b::AbstractBridgeOptimizer, idx::MOI.Index)::typeof(idx) -Translate an outer `VariableIndex` to its inner counterpart for forwarding -to `b.model`. When the variable mapping is inactive (or `b` does not own a -`Variable.Map`), returns `vi` unchanged. The caller is responsible for -having already established that `vi` is not bridged at `b`'s layer. +Translate an outer index (in `b`'s user-facing namespace) to its inner +counterpart (`b.model`'s namespace). Returns `idx` unchanged when no +translation is in effect at this layer: + +* For a `VariableIndex`, the variable mapping must be active + (`Variable.is_variable_mapping_active(map)`) AND `idx` must not refer to + an outer-only bridged variable (`idx.value < 0`). +* For a `ConstraintIndex{F, S}`, the constraint mapping for `(F, S)` must + be active AND `idx.value >= 0`. + +This is the canonical "if variable bridges are used, use the index map; +else return the index unchanged" helper. """ -function _to_inner_var(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex) +function outer_to_inner( + b::AbstractBridgeOptimizer, + vi::MOI.VariableIndex, +) map = Variable.bridges(b) - if map isa Variable.Map && Variable.is_variable_mapping_active(map) + if map isa Variable.Map && + vi.value > 0 && + Variable.is_variable_mapping_active(map) return map.outer_to_inner[vi] end return vi end +function outer_to_inner( + b::AbstractBridgeOptimizer, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + map = Variable.bridges(b) + if map isa Variable.Map && + ci.value > 0 && + Variable.is_constraint_mapping_active(map, F, S) + return map.outer_to_inner[ci] + end + return ci +end + +# By MOI convention, the `value` of a `CI{VariableIndex,S}` equals the +# `value` of the constrained variable, so its translation is derived from +# the variable translation instead of being stored separately. +function outer_to_inner( + b::AbstractBridgeOptimizer, + ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, +) where {S} + vi = outer_to_inner(b, MOI.VariableIndex(ci.value)) + return MOI.ConstraintIndex{MOI.VariableIndex,S}(vi.value) +end + """ - _to_outer_var(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex)::MOI.VariableIndex + inner_to_outer(b::AbstractBridgeOptimizer, idx::MOI.Index)::typeof(idx) -Translate an inner `VariableIndex` returned by `b.model` into the outer -namespace. When the variable mapping is inactive, returns `vi` unchanged. +Inverse of [`outer_to_inner`](@ref): translate `idx` from `b.model`'s +namespace to `b`'s user-facing namespace. Returns `idx` unchanged when no +translation is in effect at this layer. """ -function _to_outer_var(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex) +function inner_to_outer( + b::AbstractBridgeOptimizer, + vi::MOI.VariableIndex, +) map = Variable.bridges(b) if map isa Variable.Map && Variable.is_variable_mapping_active(map) return map.inner_to_outer[vi] @@ -494,6 +535,200 @@ function _to_outer_var(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex) return vi end +function inner_to_outer( + b::AbstractBridgeOptimizer, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + map = Variable.bridges(b) + if map isa Variable.Map && Variable.is_constraint_mapping_active(map, F, S) + return map.inner_to_outer[ci] + end + return ci +end + +function inner_to_outer( + b::AbstractBridgeOptimizer, + ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, +) where {S} + vi = inner_to_outer(b, MOI.VariableIndex(ci.value)) + return MOI.ConstraintIndex{MOI.VariableIndex,S}(vi.value) +end + +""" + _OuterToInner(b::AbstractBridgeOptimizer) + +Callable wrapper around [`outer_to_inner`](@ref) for use with +[`MOI.Utilities.map_indices`](@ref) when translating every index of a +function or attribute value at once. +""" +struct _OuterToInner{B<:AbstractBridgeOptimizer} <: Function + b::B +end + +(f::_OuterToInner)(idx::MOI.Index) = outer_to_inner(f.b, idx) + +""" + _InnerToOuter(b::AbstractBridgeOptimizer) + +Callable wrapper around [`inner_to_outer`](@ref). +""" +struct _InnerToOuter{B<:AbstractBridgeOptimizer} <: Function + b::B +end + +(f::_InnerToOuter)(idx::MOI.Index) = inner_to_outer(f.b, idx) + +""" + _TotalInnerToOuter(b::AbstractBridgeOptimizer) + +Like [`_InnerToOuter`](@ref) but indices with no recorded outer counterpart +are returned unchanged instead of throwing. The unrecorded indices are +inner variables created by a bridge directly in `b.model` (they are hidden +from the user); they are subsequently replaced by their expression in terms +of user variables by [`unbridged_function`](@ref). +""" +struct _TotalInnerToOuter{B<:AbstractBridgeOptimizer} <: Function + b::B +end + +function (f::_TotalInnerToOuter)(vi::MOI.VariableIndex) + map = Variable.bridges(f.b)::Variable.Map + return get(map.inner_to_outer.var_map, vi, vi) +end + +function (f::_TotalInnerToOuter)(ci::MOI.ConstraintIndex{F,S}) where {F,S} + map = Variable.bridges(f.b)::Variable.Map + if haskey(map.inner_to_outer.con_map, ci) + return map.inner_to_outer.con_map[ci] + end + return ci +end + +function (f::_TotalInnerToOuter)( + ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, +) where {S} + vi = f(MOI.VariableIndex(ci.value)) + return MOI.ConstraintIndex{MOI.VariableIndex,S}(vi.value) +end + +""" + _to_inner_value(b::AbstractBridgeOptimizer, value) + +Translate every index in `value` from `b`'s outer namespace to `b.model`'s +inner namespace, right before crossing the `b.model` boundary. + +This is needed only when the variable mapping is active AND +`recursive_model(b) === b` (for example, `LazyBridgeOptimizer`): in that +case [`bridged_function`](@ref) keeps substituted values in the outer +namespace. When `recursive_model(b) !== b` (for example, +`SingleBridgeOptimizer`), the substitution already translated every index +(see [`bridged_variable_function`](@ref)) so this is the identity. +""" +function _to_inner_value(b::AbstractBridgeOptimizer, value) + map = Variable.bridges(b) + if map isa Variable.Map && + Variable.is_variable_mapping_active(map) && + recursive_model(b) === b + return MOI.Utilities.map_indices(_OuterToInner(b), value) + end + return value +end + +""" + _from_inner_value(b::AbstractBridgeOptimizer, value) + +Translate every index in `value` from `b.model`'s inner namespace to `b`'s +outer namespace, right after crossing the `b.model` boundary. Indices with +no outer counterpart (inner variables created by bridges) are left +unchanged; the caller is expected to substitute them out via +[`unbridged_function`](@ref). +""" +function _from_inner_value(b::AbstractBridgeOptimizer, value) + map = Variable.bridges(b) + if map isa Variable.Map && Variable.is_variable_mapping_active(map) + return MOI.Utilities.map_indices(_TotalInnerToOuter(b), value) + end + return value +end + +""" + _unbridged_result_from_inner(b::AbstractBridgeOptimizer, value) + +Process an attribute `value` returned by `b.model`: translate its indices +from the inner to the outer namespace, then substitute any remaining +bridge-created variables by their expression in user variables. +""" +function _unbridged_result_from_inner(b::AbstractBridgeOptimizer, value) + return unbridged_function(b, _from_inner_value(b, value)) +end + +""" + _unbridged_result_from_bridge(b::AbstractBridgeOptimizer, value) + +Process an attribute `value` returned by a bridge of `b`. The value is +expressed in `recursive_model(b)`'s namespace: when that is `b.model` +(`SingleBridgeOptimizer`), translate as for +[`_unbridged_result_from_inner`](@ref); when it is `b` itself +(`LazyBridgeOptimizer`), the value is already in the outer namespace. +""" +function _unbridged_result_from_bridge(b::AbstractBridgeOptimizer, value) + if recursive_model(b) !== b + value = _from_inner_value(b, value) + end + return unbridged_function(b, value) +end + +""" + _to_inner_index_function(b::AbstractBridgeOptimizer, f) + +Strictly translate every variable index of the `MOI.VariableIndex` or +`MOI.VectorOfVariables` function `f` from the outer to the inner namespace. +Unlike [`_to_inner_value`](@ref) this applies whenever the variable mapping +is active, regardless of `recursive_model(b)`, because such functions are +never rewritten by [`bridged_function`](@ref). +""" +function _to_inner_index_function(b::AbstractBridgeOptimizer, f) + map = Variable.bridges(b) + if map isa Variable.Map && Variable.is_variable_mapping_active(map) + return MOI.Utilities.map_indices(_OuterToInner(b), f) + end + return f +end + +""" + _record_inner_constraint!(b::AbstractBridgeOptimizer, inner_ci) + +Map the constraint index returned by `b.model` into the outer namespace: + +* `CI{VariableIndex,S}`: derived from the variable translation (no + recording needed). +* other `CI{F,S}` with the `(F, S)` constraint mapping active: allocate a + fresh outer value, record the bidirectional entry, and return it. +* otherwise: identity. +""" +function _record_inner_constraint!( + b::AbstractBridgeOptimizer, + inner_ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + map = Variable.bridges(b) + if map isa Variable.Map && + Variable.is_constraint_mapping_active(map, F, S) + outer_ci = + MOI.ConstraintIndex{F,S}(Variable.next_outer_constraint!(map, F, S)) + map.outer_to_inner[outer_ci] = inner_ci + map.inner_to_outer[inner_ci] = outer_ci + return outer_ci + end + return inner_ci +end + +function _record_inner_constraint!( + b::AbstractBridgeOptimizer, + inner_ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, +) where {S} + return inner_to_outer(b, inner_ci) +end + function MOI.is_valid(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex) if is_bridged(b, vi) return haskey(Variable.bridges(b), vi) @@ -502,11 +737,10 @@ function MOI.is_valid(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex) if map isa Variable.Map && Variable.is_variable_mapping_active(map) # Outer/inner translation is in effect. Outer `vi` must be in # `outer_to_inner` to be valid; the entry then points to the inner - # `VariableIndex` to check. + # index to forward. haskey(map.outer_to_inner, vi) || return false - return MOI.is_valid(b.model, map.outer_to_inner[vi]) end - return MOI.is_valid(b.model, vi) + return MOI.is_valid(b.model, outer_to_inner(b, vi)) end function MOI.is_valid( @@ -547,8 +781,59 @@ function MOI.is_valid( return haskey(Constraint.bridges(b), ci) end else - return MOI.is_valid(b.model, ci) + inner_ci = _inner_index_or_nothing(b, ci) + if inner_ci === nothing + return false + end + return MOI.is_valid(b.model, inner_ci) + end +end + +""" + _inner_index_or_nothing(b::AbstractBridgeOptimizer, idx::MOI.Index) + +Like [`outer_to_inner`](@ref) but return `nothing` instead of throwing when +`idx` has no inner counterpart (for example, an invalid index supplied by +the user). Used by validity checks. +""" +function _inner_index_or_nothing( + b::AbstractBridgeOptimizer, + vi::MOI.VariableIndex, +) + map = Variable.bridges(b) + if map isa Variable.Map && + vi.value > 0 && + Variable.is_variable_mapping_active(map) + return get(map.outer_to_inner.var_map, vi, nothing) end + return vi +end + +function _inner_index_or_nothing( + b::AbstractBridgeOptimizer, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + map = Variable.bridges(b) + if map isa Variable.Map && + ci.value > 0 && + Variable.is_constraint_mapping_active(map, F, S) + if haskey(map.outer_to_inner.con_map, ci) + return map.outer_to_inner.con_map[ci] + end + return nothing + end + return ci +end + +function _inner_index_or_nothing( + b::AbstractBridgeOptimizer, + ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, +) where {S} + vi = _inner_index_or_nothing(b, MOI.VariableIndex(ci.value)) + if vi === nothing + return nothing + end + return MOI.ConstraintIndex{MOI.VariableIndex,S}(vi.value) end function _delete_variables_in_vector_of_variables_constraint( @@ -738,7 +1023,9 @@ function MOI.delete(b::AbstractBridgeOptimizer, vis::Vector{MOI.VariableIndex}) end end else - MOI.delete(b.model, vis) + inner_vis = [outer_to_inner(b, vi) for vi in vis] + MOI.delete(b.model, inner_vis) + _remove_inner_variable_mapping!(b, vis) end return end @@ -770,11 +1057,75 @@ function MOI.delete(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex) b.name_to_var = nothing delete!(b.var_to_name, vi) else - MOI.delete(b.model, vi) + inner_vi = outer_to_inner(b, vi) + MOI.delete(b.model, inner_vi) + _remove_inner_variable_mapping!(b, vi) end return end +""" + _remove_inner_variable_mapping!(b, vi) + _remove_inner_variable_mapping!(b, vis) + +Drop the outer↔inner translation entries for the just-deleted variable(s). +No-op when the mapping isn't active or `b` doesn't own a `Variable.Map`. +""" +function _remove_inner_variable_mapping!( + b::AbstractBridgeOptimizer, + vi::MOI.VariableIndex, +) + map = Variable.bridges(b) + if !(map isa Variable.Map) || !Variable.is_variable_mapping_active(map) + return + end + inner_vi = get(map.outer_to_inner.var_map, vi, nothing) + if inner_vi !== nothing + delete!(map.outer_to_inner, vi) + delete!(map.inner_to_outer, inner_vi) + end + return +end + +function _remove_inner_variable_mapping!( + b::AbstractBridgeOptimizer, + vis::Vector{MOI.VariableIndex}, +) + for vi in vis + _remove_inner_variable_mapping!(b, vi) + end + return +end + +""" + _remove_inner_constraint_mapping!(b, outer_ci, inner_ci) + +Drop the outer↔inner translation entries for the just-deleted constraint. +No-op for `CI{VariableIndex,S}` (derived from the variable mapping) and +when the `(F, S)` constraint mapping isn't active. +""" +function _remove_inner_constraint_mapping!( + b::AbstractBridgeOptimizer, + outer_ci::MOI.ConstraintIndex{F,S}, + inner_ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + map = Variable.bridges(b) + if map isa Variable.Map && + Variable.is_constraint_mapping_active(map, F, S) + delete!(map.outer_to_inner, outer_ci) + delete!(map.inner_to_outer, inner_ci) + end + return +end + +function _remove_inner_constraint_mapping!( + ::AbstractBridgeOptimizer, + ::MOI.ConstraintIndex{MOI.VariableIndex,S}, + ::MOI.ConstraintIndex{MOI.VariableIndex,S}, +) where {S} + return +end + function MOI.delete( b::AbstractBridgeOptimizer, ci::MOI.ConstraintIndex{F}, @@ -807,7 +1158,9 @@ function MOI.delete( b.name_to_con = nothing delete!(b.con_to_name, ci) else - MOI.delete(b.model, ci) + inner_ci = outer_to_inner(b, ci) + MOI.delete(b.model, inner_ci) + _remove_inner_constraint_mapping!(b, ci, inner_ci) end return end @@ -821,7 +1174,11 @@ function MOI.delete( # deleting each constraint one-by-one. MOI.delete.(b, ci) else - MOI.delete(b.model, ci) + inner_cis = [outer_to_inner(b, c) for c in ci] + MOI.delete(b.model, inner_cis) + for (outer_c, inner_c) in zip(ci, inner_cis) + _remove_inner_constraint_mapping!(b, outer_c, inner_c) + end end return end @@ -832,12 +1189,12 @@ function _get_all_including_bridged( b::AbstractBridgeOptimizer, attr::MOI.ListOfVariableIndices, ) - # `inner_to_outer` is going to map variable indices in `b.model` to their + # `bridge_var_map` is going to map variable indices in `b.model` to their # bridged variable indices. If the bridge adds multiple variables, we need # only to map the first variable, and we can skip the rest. To mark this # distinction, the tail variables are set to `nothing`. map = Variable.bridges(b) - inner_to_outer = Dict{MOI.VariableIndex,Union{Nothing,MOI.VariableIndex}}() + bridge_var_map = Dict{MOI.VariableIndex,Union{Nothing,MOI.VariableIndex}}() # These are variables which appear in `b` but do NOT appear in `b.model`. # One reason might be the Zero bridge in which they are replaced by `0.0`. user_only_variables = MOI.VariableIndex[] @@ -863,9 +1220,9 @@ function _get_all_including_bridged( # `first(variables)` twice; first to `nothing` and then to # `user_variable`. for bridged_variable in variables - inner_to_outer[bridged_variable] = nothing + bridge_var_map[bridged_variable] = nothing end - inner_to_outer[first(variables)] = user_variable + bridge_var_map[first(variables)] = user_variable end end # We're about to loop over the variables in `.model`, ordered by when they @@ -874,22 +1231,37 @@ function _get_all_including_bridged( # to undo any Variable.bridges transformations. ret = MOI.VariableIndex[] for inner_variable in MOI.get(b.model, attr) - outer_variable = get(inner_to_outer, inner_variable, missing) + # `bridge_var_map` is keyed by the indices the bridges received from + # `recursive_model(b)`. For a `SingleBridgeOptimizer` that is + # `b.model` (inner indices); for a `LazyBridgeOptimizer` it is `b` + # itself (outer indices). Try the raw inner index first, then its + # outer translation. + key = inner_variable + if !haskey(bridge_var_map, key) + map_b = Variable.bridges(b) + if map_b isa Variable.Map && + Variable.is_variable_mapping_active(map_b) && + haskey(map_b.inner_to_outer, key) + key = map_b.inner_to_outer[key] + end + end + outer_variable = get(bridge_var_map, key, missing) # If there is a chain of variable bridges, the `outer_variable` may need # to be mapped. - while haskey(inner_to_outer, outer_variable) - outer_variable = inner_to_outer[outer_variable] + while haskey(bridge_var_map, outer_variable) + outer_variable = bridge_var_map[outer_variable] end if ismissing(outer_variable) - # inner_variable does not exist in inner_to_outer, which means that - # it is not bridged. Pass through unchanged. - push!(ret, inner_variable) + # Not a bridge-created variable: report its outer translation + # (`key` is already the outer index, or the unchanged inner index + # when no translation is in effect). + push!(ret, key) elseif isnothing(outer_variable) - # inner_variable exists in inner_to_outer, but it is set to `nothing` + # inner_variable exists in bridge_var_map, but it is set to `nothing` # which means that it is not the first variable in the bridge. Skip # it because it should be hidden from the user. else - # inner_variable exists in inner_to_outer. It must be the first + # inner_variable exists in bridge_var_map. It must be the first # variable in the bridge. Report it back to the user. push!(ret, outer_variable) # `outer_variable` might represent the start of a VectorOfVariables @@ -1072,7 +1444,7 @@ function MOI.get( b::AbstractBridgeOptimizer, attr::Union{MOI.AbstractModelAttribute,MOI.AbstractOptimizerAttribute}, ) - return unbridged_function(b, MOI.get(b.model, attr)) + return _unbridged_result_from_inner(b, MOI.get(b.model, attr)) end function MOI.get( @@ -1125,7 +1497,7 @@ function MOI.set( attr::Union{MOI.AbstractModelAttribute,MOI.AbstractOptimizerAttribute}, value, ) - MOI.set(b.model, attr, bridged_function(b, value)) + MOI.set(b.model, attr, _to_inner_value(b, bridged_function(b, value))) return end @@ -1298,7 +1670,11 @@ function MOI.get(b::AbstractBridgeOptimizer, attr::MOI.ObjectiveSense) end function MOI.get(b::AbstractBridgeOptimizer, attr::MOI.ObjectiveFunction) - return unbridged_function(b, _bridged_function(b, attr)) + if is_bridged(b, attr) + value = MOI.get(recursive_model(b), attr, bridge(b, attr)) + return _unbridged_result_from_bridge(b, value) + end + return _unbridged_result_from_inner(b, MOI.get(b.model, attr)) end function MOI.set( @@ -1390,6 +1766,46 @@ function MOI.modify( return throw(ModifyBridgeNotAllowed(change)) end +""" + _to_inner_change(b::AbstractBridgeOptimizer, change) + +Translate the variable indices referenced by a function modification from +the outer to the inner namespace. The caller has already established that +the referenced variables are not bridged. +""" +_to_inner_change(::AbstractBridgeOptimizer, change) = change + +function _to_inner_change( + b::AbstractBridgeOptimizer, + change::MOI.ScalarCoefficientChange, +) + return MOI.ScalarCoefficientChange( + outer_to_inner(b, change.variable), + change.new_coefficient, + ) +end + +function _to_inner_change( + b::AbstractBridgeOptimizer, + change::MOI.MultirowChange, +) + return MOI.MultirowChange( + outer_to_inner(b, change.variable), + change.new_coefficients, + ) +end + +function _to_inner_change( + b::AbstractBridgeOptimizer, + change::MOI.ScalarQuadraticCoefficientChange, +) + return MOI.ScalarQuadraticCoefficientChange( + outer_to_inner(b, change.variable_1), + outer_to_inner(b, change.variable_2), + change.new_coefficient, + ) +end + function _modify_bridged_function( b::AbstractBridgeOptimizer, ci_or_obj, @@ -1398,7 +1814,7 @@ function _modify_bridged_function( if is_bridged(b, ci_or_obj) MOI.modify(recursive_model(b), bridge(b, ci_or_obj), change) else - MOI.modify(b.model, ci_or_obj, change) + MOI.modify(b.model, ci_or_obj, _to_inner_change(b, change)) end return end @@ -1434,10 +1850,10 @@ function MOI.get( ) if is_bridged(b, index) value = call_in_context(MOI.get, b, index, attr, _index(b, index)...) - else - value = MOI.get(b.model, attr, index) + return _unbridged_result_from_bridge(b, value) end - return unbridged_function(b, value) + value = MOI.get(b.model, attr, outer_to_inner(b, index)) + return _unbridged_result_from_inner(b, value) end function MOI.get( @@ -1450,7 +1866,9 @@ function MOI.get( any(index -> is_bridged(b, index), indices) return MOI.get.(b, attr, indices) else - return unbridged_function.(b, MOI.get(b.model, attr, indices)) + inner_indices = [outer_to_inner(b, vi) for vi in indices] + values = MOI.get(b.model, attr, inner_indices) + return _unbridged_result_from_inner.(b, values) end end @@ -1475,7 +1893,12 @@ function MOI.set( if is_bridged(b, index) call_in_context(MOI.set, b, index, attr, value, _index(b, index)...) else - MOI.set(b.model, attr, index, value) + MOI.set( + b.model, + attr, + outer_to_inner(b, index), + _to_inner_value(b, value), + ) end return end @@ -1489,7 +1912,10 @@ function MOI.set( if any(index -> is_bridged(b, index), indices) MOI.set.(b, attr, indices, values) else - MOI.set(b.model, attr, indices, bridged_function.(b, values)) + inner_indices = [outer_to_inner(b, vi) for vi in indices] + inner_values = + [_to_inner_value(b, bridged_function(b, v)) for v in values] + MOI.set(b.model, attr, inner_indices, inner_values) end return end @@ -1545,7 +1971,7 @@ function _set_substituted( MOI.throw_if_not_valid(b, ci) call_in_context(MOI.set, b, ci, attr, value) else - MOI.set(b.model, attr, ci, value) + MOI.set(b.model, attr, outer_to_inner(b, ci), _to_inner_value(b, value)) end return end @@ -1565,6 +1991,12 @@ function MOI.get( # Otherwise, we need to query ConstraintFunction in the context of # the bridge... func = call_in_context(MOI.get, b, ci, attr) + if recursive_model(b) !== b + # The bridge expressed `func` in `b.model`'s namespace; + # translate the non-bridge-created indices to the outer + # namespace before unbridging. + func = _from_inner_value(b, func) + end # and then unbridge this function (because it may contain variables # that are themselves bridged). return unbridged_constraint_function(b, func) @@ -1572,7 +2004,11 @@ function MOI.get( else # This constraint is not bridged, but it might contain variables that # are. - return unbridged_constraint_function(b, MOI.get(b.model, attr, ci)) + func = _from_inner_value( + b, + MOI.get(b.model, attr, outer_to_inner(b, ci)), + ) + return unbridged_constraint_function(b, func) end end @@ -1600,7 +2036,7 @@ function MOI.get( MOI.throw_if_not_valid(b, ci) return call_in_context(MOI.get, b, ci, attr) else - return MOI.get(b.model, attr, ci) + return MOI.get(b.model, attr, outer_to_inner(b, ci)) end end @@ -1639,7 +2075,7 @@ function MOI.get( MOI.throw_if_not_valid(b, ci) call_in_context(MOI.get, b, ci, attr) else - MOI.get(b.model, attr, ci) + MOI.get(b.model, attr, outer_to_inner(b, ci)) end # This is a scalar function, so if there are variable bridges, it might # contain constants that have been moved into the set. @@ -1678,13 +2114,13 @@ function MOI.get( attr::MOI.AbstractConstraintAttribute, ci::MOI.ConstraintIndex, ) - func = if is_bridged(b, ci) + if is_bridged(b, ci) MOI.throw_if_not_valid(b, ci) - call_in_context(MOI.get, b, ci, attr) - else - MOI.get(b.model, attr, ci) + func = call_in_context(MOI.get, b, ci, attr) + return _unbridged_result_from_bridge(b, func) end - return unbridged_function(b, func) + func = MOI.get(b.model, attr, outer_to_inner(b, ci)) + return _unbridged_result_from_inner(b, func) end function MOI.get( @@ -1709,7 +2145,7 @@ function MOI.get( # correct value. return MOI.Utilities.get_fallback(b, attr, ci) end - return MOI.get(b.model, attr, ci) + return MOI.get(b.model, attr, outer_to_inner(b, ci)) end function MOI.supports( @@ -1828,11 +2264,13 @@ function MOI.get( if is_bridged(b, vi) bridge_ = bridge(b, vi) if MOI.supports(b, MOI.VariableName(), typeof(bridge_)) - return MOI.get(b, MOI.VariableName(), bridge_) + # The bridge's indices live in `recursive_model(b)`'s index + # space, so its attribute getters must be called with it. + return MOI.get(recursive_model(b), MOI.VariableName(), bridge_) end return get(b.var_to_name, vi, "") else - return MOI.get(b.model, attr, vi) + return MOI.get(b.model, attr, outer_to_inner(b, vi)) end end @@ -1845,13 +2283,15 @@ function MOI.set( if is_bridged(b, vi) bridge_ = bridge(b, vi) if MOI.supports(b, MOI.VariableName(), typeof(bridge_)) - MOI.set(b, MOI.VariableName(), bridge_, name) + # The bridge's indices live in `recursive_model(b)`'s index + # space, so its attribute setters must be called with it. + MOI.set(recursive_model(b), MOI.VariableName(), bridge_, name) else b.var_to_name[vi] = name b.name_to_var = nothing # Invalidate the name map. end else - MOI.set(b.model, attr, vi, name) + MOI.set(b.model, attr, outer_to_inner(b, vi), name) end return end @@ -1864,7 +2304,7 @@ function MOI.get( if is_bridged(b, constraint_index) return get(b.con_to_name, constraint_index, "") else - return MOI.get(b.model, attr, constraint_index) + return MOI.get(b.model, attr, outer_to_inner(b, constraint_index)) end end @@ -1886,7 +2326,7 @@ function MOI.set( b.con_to_name[constraint_index] = name b.name_to_con = nothing # Invalidate the name map. else - MOI.set(b.model, attr, constraint_index, name) + MOI.set(b.model, attr, outer_to_inner(b, constraint_index), name) end return end @@ -1908,6 +2348,39 @@ function MOI.set( return throw(MOI.VariableIndexConstraintNameError()) end +""" + _outer_variable_for_name_lookup(b::AbstractBridgeOptimizer, vi) + +Translate the inner variable `vi` found by a name lookup in `b.model` to +the outer namespace. When the variable mapping is inactive, return `vi` +unchanged. A recorded passthrough variable translates through the index +map. An unrecorded inner variable was created by a bridge whose +`MOI.VariableName` support delegated the user's name to it; reverse that +delegation by searching the variable bridges. +""" +function _outer_variable_for_name_lookup( + b::AbstractBridgeOptimizer, + vi::MOI.VariableIndex, +) + map = Variable.bridges(b) + if !(map isa Variable.Map) || !Variable.is_variable_mapping_active(map) + return vi + end + if haskey(map.inner_to_outer.var_map, vi) + return map.inner_to_outer.var_map[vi] + end + for (outer_vi, bridge_) in map + variables = MOI.get(bridge_, MOI.ListOfVariableIndices()) + position = findfirst(isequal(vi), variables) + if position !== nothing + # Bridged vectors of variables use consecutive (descending) + # outer values starting at the first variable. + return MOI.VariableIndex(outer_vi.value - (position - 1)) + end + end + return vi +end + # Query index from name (similar to `UniversalFallback`) function MOI.get( b::AbstractBridgeOptimizer, @@ -1918,6 +2391,10 @@ function MOI.get( if !Variable.has_bridges(Variable.bridges(b)) return vi end + if vi !== nothing + # The index returned by `b.model` is in the inner namespace. + vi = _outer_variable_for_name_lookup(b, vi) + end if b.name_to_var === nothing b.name_to_var = MOI.Utilities.build_name_to_var_map(b.var_to_name) end @@ -1951,6 +2428,10 @@ function MOI.get( else ci = MOI.get(b.model, IdxT, name) end + if ci !== nothing + # The index returned by `b.model` is in the inner namespace. + ci = inner_to_outer(b, ci) + end ci_bridged = get(b.name_to_con, name, nothing) MOI.Utilities.throw_if_multiple_with_name(ci_bridged, name) return MOI.Utilities.check_type_and_multiple_names( @@ -1974,12 +2455,17 @@ function MOI.get( if b.name_to_con === nothing b.name_to_con = MOI.Utilities.build_name_to_con_map(b.con_to_name) end + ci = MOI.get(b.model, IdxT, name) + if ci !== nothing + # The index returned by `b.model` is in the inner namespace. + ci = inner_to_outer(b, ci) + end ci_bridged = get(b.name_to_con, name, nothing) MOI.Utilities.throw_if_multiple_with_name(ci_bridged, name) return MOI.Utilities.check_type_and_multiple_names( IdxT, ci_bridged, - MOI.get(b.model, IdxT, name), + ci, name, ) end @@ -1997,19 +2483,45 @@ function MOI.supports_constraint( end end +""" + _is_available_constraint_index(b, ci::MOI.ConstraintIndex) + +Return `true` if `ci` can be allocated by `Constraint.add_key_for_bridge` +without clashing with an index already in use in the outer namespace: by +the variable bridges (constrained-on-creation vectors of variables) or by +an identity entry copied when the `(F, S)` constraint mapping was +activated. +""" +function _is_available_constraint_index( + b::AbstractBridgeOptimizer, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + if MOI.is_valid(Variable.bridges(b), ci) + return false + end + map = Variable.bridges(b) + if map isa Variable.Map && + Variable.is_constraint_mapping_active(map, F, S) && + haskey(map.outer_to_inner.con_map, ci) + return false + end + return true +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. + # However, they can clash with the indices created by the variable + # bridges or with inner indices copied into the outer namespace when the + # constraint mapping was activated, so we use the last argument to + # inform the constraint bridge mapping about indices already taken. ci = Constraint.add_key_for_bridge( Constraint.bridges(b)::Constraint.Map, bridge, f, s, - !Base.Fix1(MOI.is_valid, Variable.bridges(b)), + Base.Fix1(_is_available_constraint_index, b), ) Variable.register_context(Variable.bridges(b), ci) return ci @@ -2105,6 +2617,14 @@ function MOI.add_constraint( end elseif F <: MOI.VectorOfVariables if any(vi -> is_bridged(b, vi), f.variables) + # This is the first (or another) force-bridged `F`-in-`S`: + # from now on the outer and inner `CI{F,S}` namespaces are + # distinct, so materialize identity entries for all inner + # constraints of this type before they can drift apart. + v_map = Variable.bridges(b) + if v_map isa Variable.Map + Variable.activate_constraint_mapping!(v_map, b.model, F, S) + end BridgeType = Constraint.concrete_bridge_type( constraint_vector_functionize_bridge(b), F, @@ -2126,7 +2646,18 @@ function MOI.add_constraint( # modification has been done in the previous line return add_bridged_constraint(b, BridgeType, f, s) else - return MOI.add_constraint(b.model, f, s) + if F <: MOI.VariableIndex || F <: MOI.VectorOfVariables + # Index functions are never rewritten by + # `bridged_constraint_function` above, so their variable indices + # are still in the outer namespace. + f = _to_inner_index_function(b, f) + else + # `bridged_constraint_function` left the substituted function in + # the outer namespace when `recursive_model(b) === b`. + f = _to_inner_value(b, f) + end + inner_ci = MOI.add_constraint(b.model, f, s) + return _record_inner_constraint!(b, inner_ci) end end @@ -2143,15 +2674,20 @@ function MOI.add_constraints( if any(func -> is_bridged(b, func), f) return MOI.add_constraint.(b, f, s) end + f = F[_to_inner_index_function(b, func)::F for func in f] elseif F == MOI.VectorOfVariables if any(func -> any(vi -> is_bridged(b, vi), func.variables), f) return MOI.add_constraint.(b, f, s) end + f = F[_to_inner_index_function(b, func)::F for func in f] else - f = F[bridged_function(b, func)::F for func in f] + f = F[ + _to_inner_value(b, bridged_function(b, func))::F for func in f + ] end end - return MOI.add_constraints(b.model, f, s) + inner_cis = MOI.add_constraints(b.model, f, s) + return [_record_inner_constraint!(b, ci) for ci in inner_cis] end function is_bridged( @@ -2175,6 +2711,45 @@ function is_bridged( return is_bridged(b, change.variable_1) || is_bridged(b, change.variable_2) end +""" + _modify_substituted_change(b::AbstractBridgeOptimizer, ci_or_obj, change) + +Apply a function modification produced by expanding a bridged variable via +[`bridged_variable_function`](@ref). When `recursive_model(b) === b` the +expansion is in the outer namespace, so we re-enter `MOI.modify` for the +usual dispatch. Otherwise the expansion is already in the inner namespace +and we must dispatch directly, bypassing the outer-to-inner translation. +""" +function _modify_substituted_change( + b::AbstractBridgeOptimizer, + ci::MOI.ConstraintIndex, + change::MOI.AbstractFunctionModification, +) + if recursive_model(b) === b + MOI.modify(b, ci, change) + elseif is_bridged(b, ci) + call_in_context(MOI.modify, b, ci, change) + else + MOI.modify(b.model, outer_to_inner(b, ci), change) + end + return +end + +function _modify_substituted_change( + b::AbstractBridgeOptimizer, + obj::MOI.ObjectiveFunction, + change::MOI.AbstractFunctionModification, +) + if recursive_model(b) === b + MOI.modify(b, obj, change) + elseif is_bridged(b, obj) + MOI.modify(recursive_model(b), bridge(b, obj), change) + else + MOI.modify(b.model, obj, change) + end + return +end + function modify_bridged_change( b::AbstractBridgeOptimizer, ci, @@ -2196,7 +2771,7 @@ function modify_bridged_change( for t in func.terms coefs = [(i, coef * t.coefficient) for (i, coef) in change.new_coefficients] - MOI.modify(b, ci, MOI.MultirowChange(t.variable, coefs)) + _modify_substituted_change(b, ci, MOI.MultirowChange(t.variable, coefs)) end return end @@ -2239,7 +2814,11 @@ function modify_bridged_change( end for t in func.terms coef = t.coefficient * change.new_coefficient - MOI.modify(b, ci_or_obj, MOI.ScalarCoefficientChange(t.variable, coef)) + _modify_substituted_change( + b, + ci_or_obj, + MOI.ScalarCoefficientChange(t.variable, coef), + ) end return end @@ -2267,7 +2846,11 @@ function MOI.modify( if is_bridged(b, ci) call_in_context(MOI.modify, b, ci, change) else - MOI.modify(b.model, ci, change) + MOI.modify( + b.model, + outer_to_inner(b, ci), + _to_inner_change(b, change), + ) end end return @@ -2348,10 +2931,7 @@ function MOI.add_constrained_variables( outer_vis = MOI.VariableIndex[ _record_inner_variable!(b, vi) for vi in inner_vis ] - # The constraint index value of `inner_ci` may need translating once - # we plumb constraint mapping; for now identity since this branch - # doesn't go through a variable bridge. - return outer_vis, inner_ci + return outer_vis, _record_inner_constraint!(b, inner_ci) end if set isa MOI.Reals || is_variable_bridged(b, typeof(set)) BridgeType = Variable.concrete_bridge_type(b, typeof(set)) @@ -2370,7 +2950,11 @@ function MOI.add_constrained_variables( # indices already taken by constraint bridges. return Variable.add_keys_for_bridge( Variable.bridges(b)::Variable.Map, - () -> Variable.bridge_constrained_variable(BridgeType, b, set), + () -> Variable.bridge_constrained_variable( + BridgeType, + recursive_model(b), + set, + ), set, !Base.Fix1(haskey, Constraint.bridges(b)), ) @@ -2400,14 +2984,9 @@ function MOI.add_constrained_variable( if !is_bridged(b, typeof(set)) inner_vi, inner_ci = MOI.add_constrained_variable(b.model, set) outer_vi = _record_inner_variable!(b, inner_vi) - # `CI{VariableIndex, S}.value == vi.value` by MOI convention; if we - # translated the variable, translate the constraint identically. - outer_ci = if outer_vi === inner_vi - inner_ci - else - MOI.ConstraintIndex{MOI.VariableIndex,typeof(set)}(outer_vi.value) - end - return outer_vi, outer_ci + # `CI{VariableIndex, S}.value == vi.value` by MOI convention, so the + # translated constraint index is derived from the variable one. + return outer_vi, _record_inner_constraint!(b, inner_ci) end if is_variable_bridged(b, typeof(set)) BridgeType = Variable.concrete_bridge_type(b, typeof(set)) @@ -2465,11 +3044,29 @@ function bridged_variable_function( bridge(b, vi)::Variable.AbstractBridge, _index(b, vi)..., ) - # If two variable bridges are chained, `func` may still contain - # bridged variables. - return bridged_function(b, func) - else + if recursive_model(b) === b + # The bridge created its variables through `b` itself (for + # example, `LazyBridgeOptimizer`), so `func` is expressed in + # `b`'s (outer) namespace: chained bridged variables may remain + # and need further substitution. + return bridged_function(b, func) + else + # The bridge created its variables directly in `b.model` (for + # example, `SingleBridgeOptimizer`), so `func` is already + # expressed in the inner namespace: do not reinterpret its + # indices in the outer namespace. + return func + end + elseif recursive_model(b) === b + # The substituted function stays in the outer namespace; the + # translation to the inner namespace happens at the `b.model` + # boundary (see `_to_inner_value`). return vi + else + # The substituted function is consumed in the inner namespace + # (either by `b.model` or by a bridge constructed with `b.model`), + # so translate the non-bridged variable now. + return outer_to_inner(b, vi) end end diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index e88ae35416..6e4d221351 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -452,10 +452,13 @@ function substitute_variables( x::MOI.VariableIndex, ) where {F<:Function} f = variable_map(x) - if f != x - error("Cannot substitute `$x` as it is bridged into `$f`.") + if f isa MOI.VariableIndex + # `f` may differ from `x` when the bridge optimizer translates + # between its outer and inner variable index spaces; a plain + # renaming is allowed in contexts that require a `VariableIndex`. + return f end - return x + error("Cannot substitute `$x` as it is bridged into `$f`.") end # This method is used when submitting `HeuristicSolution`. diff --git a/test/Bridges/General/test_bridge_optimizer.jl b/test/Bridges/General/test_bridge_optimizer.jl index a8475e2cd5..231e5986da 100644 --- a/test/Bridges/General/test_bridge_optimizer.jl +++ b/test/Bridges/General/test_bridge_optimizer.jl @@ -85,15 +85,20 @@ function test_subsitution_of_variables() @test MOI.get(mock, DummyModelAttribute()) ≈ 2.0y + 3.0 z = MOI.add_variable(bridged) - for (attr, index) in - [(DummyVariableAttribute(), z), (DummyConstraintAttribute(), c2x)] + # `z` lives in `bridged`'s outer index space; to talk to `mock` directly + # we need the corresponding index of the inner model. + z_inner = MOI.get(mock, MOI.ListOfVariableIndices())[2] + for (attr, index, inner_index) in [ + (DummyVariableAttribute(), z, z_inner), + (DummyConstraintAttribute(), c2x, c2x), + ] MOI.set(bridged, attr, index, 1.0x) @test MOI.get(bridged, attr, index) ≈ 1.0x - @test MOI.get(mock, attr, index) ≈ 1.0y + 1.0 + @test MOI.get(mock, attr, inner_index) ≈ 1.0y + 1.0 MOI.set(bridged, attr, [index], [3.0x + 1.0z]) @test MOI.get(bridged, attr, [index])[1] ≈ 3.0x + 1.0z - @test MOI.get(mock, attr, [index])[1] ≈ 3.0y + 1.0z + 3.0 + @test MOI.get(mock, attr, [inner_index])[1] ≈ 3.0y + 1.0z_inner + 3.0 end return end @@ -126,13 +131,18 @@ function test_CallbackVariablePrimal() x, _ = MOI.add_constrained_variable(bridged, MOI.GreaterThan(1.0)) y = MOI.get(mock, MOI.ListOfVariableIndices())[1] z = MOI.add_variable(bridged) + # `z` lives in `bridged`'s outer index space; to talk to `mock` directly + # we need the corresponding index of the inner model. + z_inner = MOI.get(mock, MOI.ListOfVariableIndices())[2] attr = MOI.CallbackVariablePrimal(nothing) @test_throws( - ErrorException("No mock callback primal is set for variable `$z`."), + ErrorException( + "No mock callback primal is set for variable `$z_inner`.", + ), MOI.get(bridged, attr, z), ) MOI.set(mock, attr, y, 1.0) - MOI.set(mock, attr, z, 2.0) + MOI.set(mock, attr, z_inner, 2.0) @test MOI.get(bridged, attr, z) == 2.0 err = ArgumentError( "Variable bridge of type `$(typeof(MOI.Bridges.bridge(bridged, x)))` " * diff --git a/test/Bridges/Variable/test_ParameterToEqualToBridge.jl b/test/Bridges/Variable/test_ParameterToEqualToBridge.jl index d394ecc10d..bdfdcd93f2 100644 --- a/test/Bridges/Variable/test_ParameterToEqualToBridge.jl +++ b/test/Bridges/Variable/test_ParameterToEqualToBridge.jl @@ -66,7 +66,10 @@ function test_constraint_function() x, ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) y, bridge = first(model.map) @test y == x - x_inner = MOI.get(model, MOI.ConstraintFunction(), bridge) + # The bridge's indices live in `inner`'s index space, so its attribute + # getters must be called with `inner`, like + # `MOI.Bridges.recursive_model(model)` does in production code. + x_inner = MOI.get(inner, MOI.ConstraintFunction(), bridge) @test MOI.get(inner, MOI.ListOfVariableIndices()) == [x_inner] return end From d4078016646cbf6ca8622c9fc5215f2fad63c0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 17 Jun 2026 10:12:46 +0200 Subject: [PATCH 3/4] Simplify --- src/Bridges/Variable/map.jl | 25 ++++++++------ src/Bridges/bridge_optimizer.jl | 61 ++++++++++++++------------------- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/Bridges/Variable/map.jl b/src/Bridges/Variable/map.jl index d91e376c2f..392f6b51a9 100644 --- a/src/Bridges/Variable/map.jl +++ b/src/Bridges/Variable/map.jl @@ -134,20 +134,20 @@ function Base.empty!(map::Map) return map end -""" - is_variable_mapping_active(map::Map)::Bool - -Return `true` once at least one variable bridge has been added (and hence -the outer/inner translation has been materialized). -""" -is_variable_mapping_active(map::Map) = map.next_outer_variable != 0 - """ is_constraint_mapping_active(map::Map, ::Type{F}, ::Type{S})::Bool Return `true` once at least one `CI{F, S}` has been force-bridged at this layer (and hence the outer/inner translation for `(F, S)` has been materialized). + +Note that this is strictly stronger than [`has_bridges`](@ref): the latter +is `true` as soon as any variable bridge exists, while constraint mapping +only activates for the specific `(F, S)` pairs whose outer and inner +namespaces have diverged because a `VariableIndex`/`VectorOfVariables` +constraint was force-bridged. Regular `F`-in-`S` constraints are bridged +per *type*, never per *instance*, so their namespaces never diverge and +this stays `false` for them. """ function is_constraint_mapping_active( map::Map, @@ -168,7 +168,12 @@ namespaces independently. No-op if the mapping is already active. `b.model` of the enclosing `AbstractBridgeOptimizer`). """ function activate_variable_mapping!(map::Map, model::MOI.ModelLike) - if is_variable_mapping_active(map) + # `has_bridges` flips to `true` only inside `add_key_for_bridge` (which + # pushes to `map.info`), and that always runs *after* this function in + # `add_constrained_variable`. So on the first variable bridge this is + # still `false` and we populate; any nested re-entry (a variable bridge + # that itself adds constrained variables) sees `true` and is a no-op. + if has_bridges(map) return end max_value = Int64(0) @@ -223,7 +228,7 @@ Return a fresh `Int64` value to use as a `VariableIndex.value` in the outer namespace and advance the internal counter. """ function next_outer_variable!(map::Map) - @assert is_variable_mapping_active(map) + @assert has_bridges(map) value = map.next_outer_variable map.next_outer_variable = value + 1 return value diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 5f634869dc..99aad953d7 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -471,11 +471,12 @@ Translate an outer index (in `b`'s user-facing namespace) to its inner counterpart (`b.model`'s namespace). Returns `idx` unchanged when no translation is in effect at this layer: -* For a `VariableIndex`, the variable mapping must be active - (`Variable.is_variable_mapping_active(map)`) AND `idx` must not refer to - an outer-only bridged variable (`idx.value < 0`). -* For a `ConstraintIndex{F, S}`, the constraint mapping for `(F, S)` must - be active AND `idx.value >= 0`. +* For a `VariableIndex`, only when variable bridges are used + (`Variable.has_bridges(map)`). The caller is responsible for having + already established that `idx` is not bridged at `b`'s layer. +* For a `ConstraintIndex{F, S}`, only when the constraint mapping for + `(F, S)` has been activated (see + [`Variable.is_constraint_mapping_active`](@ref)). This is the canonical "if variable bridges are used, use the index map; else return the index unchanged" helper. @@ -485,9 +486,7 @@ function outer_to_inner( vi::MOI.VariableIndex, ) map = Variable.bridges(b) - if map isa Variable.Map && - vi.value > 0 && - Variable.is_variable_mapping_active(map) + if Variable.has_bridges(map) return map.outer_to_inner[vi] end return vi @@ -498,8 +497,7 @@ function outer_to_inner( ci::MOI.ConstraintIndex{F,S}, ) where {F,S} map = Variable.bridges(b) - if map isa Variable.Map && - ci.value > 0 && + if Variable.has_bridges(map) && Variable.is_constraint_mapping_active(map, F, S) return map.outer_to_inner[ci] end @@ -529,7 +527,7 @@ function inner_to_outer( vi::MOI.VariableIndex, ) map = Variable.bridges(b) - if map isa Variable.Map && Variable.is_variable_mapping_active(map) + if Variable.has_bridges(map) return map.inner_to_outer[vi] end return vi @@ -540,7 +538,8 @@ function inner_to_outer( ci::MOI.ConstraintIndex{F,S}, ) where {F,S} map = Variable.bridges(b) - if map isa Variable.Map && Variable.is_constraint_mapping_active(map, F, S) + if Variable.has_bridges(map) && + Variable.is_constraint_mapping_active(map, F, S) return map.inner_to_outer[ci] end return ci @@ -625,10 +624,7 @@ namespace. When `recursive_model(b) !== b` (for example, (see [`bridged_variable_function`](@ref)) so this is the identity. """ function _to_inner_value(b::AbstractBridgeOptimizer, value) - map = Variable.bridges(b) - if map isa Variable.Map && - Variable.is_variable_mapping_active(map) && - recursive_model(b) === b + if Variable.has_bridges(Variable.bridges(b)) && recursive_model(b) === b return MOI.Utilities.map_indices(_OuterToInner(b), value) end return value @@ -644,8 +640,7 @@ unchanged; the caller is expected to substitute them out via [`unbridged_function`](@ref). """ function _from_inner_value(b::AbstractBridgeOptimizer, value) - map = Variable.bridges(b) - if map isa Variable.Map && Variable.is_variable_mapping_active(map) + if Variable.has_bridges(Variable.bridges(b)) return MOI.Utilities.map_indices(_TotalInnerToOuter(b), value) end return value @@ -688,8 +683,7 @@ is active, regardless of `recursive_model(b)`, because such functions are never rewritten by [`bridged_function`](@ref). """ function _to_inner_index_function(b::AbstractBridgeOptimizer, f) - map = Variable.bridges(b) - if map isa Variable.Map && Variable.is_variable_mapping_active(map) + if Variable.has_bridges(Variable.bridges(b)) return MOI.Utilities.map_indices(_OuterToInner(b), f) end return f @@ -711,7 +705,7 @@ function _record_inner_constraint!( inner_ci::MOI.ConstraintIndex{F,S}, ) where {F,S} map = Variable.bridges(b) - if map isa Variable.Map && + if Variable.has_bridges(map) && Variable.is_constraint_mapping_active(map, F, S) outer_ci = MOI.ConstraintIndex{F,S}(Variable.next_outer_constraint!(map, F, S)) @@ -734,7 +728,7 @@ function MOI.is_valid(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex) return haskey(Variable.bridges(b), vi) end map = Variable.bridges(b) - if map isa Variable.Map && Variable.is_variable_mapping_active(map) + if Variable.has_bridges(map) # Outer/inner translation is in effect. Outer `vi` must be in # `outer_to_inner` to be valid; the entry then points to the inner # index to forward. @@ -801,9 +795,7 @@ function _inner_index_or_nothing( vi::MOI.VariableIndex, ) map = Variable.bridges(b) - if map isa Variable.Map && - vi.value > 0 && - Variable.is_variable_mapping_active(map) + if Variable.has_bridges(map) return get(map.outer_to_inner.var_map, vi, nothing) end return vi @@ -814,8 +806,7 @@ function _inner_index_or_nothing( ci::MOI.ConstraintIndex{F,S}, ) where {F,S} map = Variable.bridges(b) - if map isa Variable.Map && - ci.value > 0 && + if Variable.has_bridges(map) && Variable.is_constraint_mapping_active(map, F, S) if haskey(map.outer_to_inner.con_map, ci) return map.outer_to_inner.con_map[ci] @@ -1076,7 +1067,7 @@ function _remove_inner_variable_mapping!( vi::MOI.VariableIndex, ) map = Variable.bridges(b) - if !(map isa Variable.Map) || !Variable.is_variable_mapping_active(map) + if !Variable.has_bridges(map) return end inner_vi = get(map.outer_to_inner.var_map, vi, nothing) @@ -1239,8 +1230,7 @@ function _get_all_including_bridged( key = inner_variable if !haskey(bridge_var_map, key) map_b = Variable.bridges(b) - if map_b isa Variable.Map && - Variable.is_variable_mapping_active(map_b) && + if Variable.has_bridges(map_b) && haskey(map_b.inner_to_outer, key) key = map_b.inner_to_outer[key] end @@ -2363,7 +2353,7 @@ function _outer_variable_for_name_lookup( vi::MOI.VariableIndex, ) map = Variable.bridges(b) - if !(map isa Variable.Map) || !Variable.is_variable_mapping_active(map) + if !Variable.has_bridges(map) return vi end if haskey(map.inner_to_outer.var_map, vi) @@ -2861,16 +2851,17 @@ end """ _record_inner_variable!(b::AbstractBridgeOptimizer, inner_vi::MOI.VariableIndex) -If variable mapping is active in `b`, allocate a fresh outer `VariableIndex` -value and record the bidirectional mapping. Otherwise (identity mode, or `b` -does not own a `Variable.Map` at all), return `inner_vi` unchanged. +If `b` uses variable bridges (`Variable.has_bridges`), allocate a fresh +outer `VariableIndex` value and record the bidirectional mapping. +Otherwise (identity mode, or `b` does not own a `Variable.Map` at all), +return `inner_vi` unchanged. """ function _record_inner_variable!( b::AbstractBridgeOptimizer, inner_vi::MOI.VariableIndex, ) map = Variable.bridges(b) - if !(map isa Variable.Map) || !Variable.is_variable_mapping_active(map) + if !Variable.has_bridges(map) return inner_vi end outer_vi = MOI.VariableIndex(Variable.next_outer_variable!(map)) From e2d8d2a23df7d0704018e5af4e14b34485c44f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sun, 21 Jun 2026 09:53:12 +0200 Subject: [PATCH 4/4] =?UTF-8?q?Fixed:=20unbridged=5Ffunction=20recursion?= =?UTF-8?q?=20(namespace-aware=20=E2=80=94=20confirmed=20the=20StackOverfl?= =?UTF-8?q?ow=20is=20gone).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remaining failures, all test-only, two kinds: 1. Hardcoded old negative values (e.g. test_FreeBridge:138 expecting [-1,-2]) — trivially update to the new positive values. 2. Cross-layer is_valid isolation (test_nesting lines 648, 664–666: !is_valid(model, x), !is_valid(b0, cnn), etc.). These assert that a bridged entity from an upper layer is invalid in lower layers. With the old negatives this held for free; with positive translated indices a bridged variable's value (x=1) coincides with an inner-model variable, so a direct is_valid(model, x) is a false positive. Real operations are unaffected because they route through the proper layer — only these direct cross-namespace value comparisons see the coincidence. The new design's guarantee is "operations through the correct layer are correct," not "indices are globally unique across layers" (which is what the old negative encoding incidentally provided). --- src/Bridges/Constraint/map.jl | 10 +- src/Bridges/Variable/map.jl | 180 +++++++++++++++++++++----------- src/Bridges/bridge_optimizer.jl | 137 ++++++++++++++---------- 3 files changed, 202 insertions(+), 125 deletions(-) diff --git a/src/Bridges/Constraint/map.jl b/src/Bridges/Constraint/map.jl index b5926b690b..7a78009d38 100644 --- a/src/Bridges/Constraint/map.jl +++ b/src/Bridges/Constraint/map.jl @@ -54,8 +54,6 @@ end _index(ci::MOI.ConstraintIndex) = ci.value -_index(ci::MOI.ConstraintIndex{MOI.VectorOfVariables}) = -ci.value - function Base.haskey(map::Map, ci::MOI.ConstraintIndex{F,S}) where {F,S} return 1 <= _index(ci) <= length(map.bridges) && map.bridges[_index(ci)] !== nothing && @@ -118,10 +116,6 @@ end _index(index, F, S) = MOI.ConstraintIndex{F,S}(index) -function _index(index, F::Type{MOI.VectorOfVariables}, S) - return MOI.ConstraintIndex{F,S}(-index) -end - function _iterate(map::Map, state = 1) while state ≤ length(map.bridges) && map.bridges[state] === nothing state += 1 @@ -235,7 +229,7 @@ Return the list of all keys that correspond to function vector_of_variables_constraints(map::Map) return MOI.Utilities.lazy_map( MOI.ConstraintIndex{MOI.VectorOfVariables}, - i -> MOI.ConstraintIndex{map.constraint_types[i]...}(-i), + i -> MOI.ConstraintIndex{map.constraint_types[i]...}(i), Base.Iterators.Filter( i -> map.bridges[i] !== nothing && @@ -284,7 +278,7 @@ function _ensure_available( ::Type{S}, is_available::Function, ) where {S} - while !is_available(MOI.ConstraintIndex{F,S}(-length(map.bridges) - 1)) + while !is_available(MOI.ConstraintIndex{F,S}(length(map.bridges) + 1)) push!(map.bridges, nothing) push!(map.constraint_types, (F, S)) end diff --git a/src/Bridges/Variable/map.jl b/src/Bridges/Variable/map.jl index 392f6b51a9..55926dcbac 100644 --- a/src/Bridges/Variable/map.jl +++ b/src/Bridges/Variable/map.jl @@ -28,25 +28,29 @@ bridged `CI{VariableIndex, S}` or `CI{VectorOfVariables, S}` triggers `CI{F, S}` are copied in as identity entries; afterwards the outer and inner `CI{F, S}` namespaces are independent. -Outer-only entries (bridged variables, force-bridged constraints) appear as -keys in `outer_to_inner` with a sentinel value of `0` (and are absent from -`inner_to_outer`). +## Internal slot indexing + +Bridged variables are identified by a **positive** outer `VariableIndex.value` +allocated from the shared outer counter (see [`next_outer_variable!`](@ref)). +Internally, the per-bridge data is stored in dense `Vector`s indexed by a +1-based `slot`. [`variable_to_slot`](@ref) maps an outer value to its slot and +[`slot_to_variable`](@ref) is the inverse (parallel to `bridges`). A bridged +variable is **outer-only**: it has no entry in `outer_to_inner`/`inner_to_outer`. """ 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 + # `slot` -> `0`: the variable at `slot` was added with `add_constrained_variable`. + # `slot` -> `-j`: the variable at `slot` 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`. + # `ConstraintIndex{MOI.VectorOfVariables}(j)` (note: `j > 0`). + # `slot` -> `j`: the variable at `slot` was the `j`th variable of + # `add_constrained_variables`. 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 + # `slot` -> `-1`: the variable at `slot` was deleted. + # `slot` -> `0`: the variable at `slot` was added with `add_constrained_variable`. + # `slot` -> `j`: the variable at `slot` 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`. + # `slot` -> `bridge`: the variable at `slot` was bridged by `bridge`. bridges::Vector{Union{Nothing,AbstractBridge}} sets::Vector{Union{Nothing,Type}} # If `nothing`, it cannot be computed because some bridges does not support it @@ -61,7 +65,7 @@ mutable struct Map <: AbstractDict{MOI.VariableIndex,AbstractBridge} # Context of constraint bridged by constraint bridges constraint_context::Dict{MOI.ConstraintIndex,Int64} # `(ci::ConstraintIndex{MOI.VectorOfVariables}).value` -> - # the first variable index + # the `slot` of the first variable # and `0` if it is the index of a constraint bridge vector_of_variables_map::Vector{Int64} # `(ci::ConstraintIndex{MOI.VectorOfVariables}).value` -> @@ -69,6 +73,11 @@ mutable struct Map <: AbstractDict{MOI.VariableIndex,AbstractBridge} vector_of_variables_length::Vector{Int64} # Same as in `MOI.Utilities.VariablesContainer` set_mask::Vector{UInt16} + # outer `VariableIndex.value` of a bridged variable -> its `slot`. + variable_to_slot::Dict{Int64,Int64} + # `slot` -> outer `VariableIndex.value` (inverse of `variable_to_slot`, + # parallel to `bridges`). + slot_to_variable::Vector{Int64} # Outer (user-facing) -> inner (`b.model`) translation. Empty until the # first variable bridge is added, at which point existing inner variables # are added as identity. After activation, every variable in the outer @@ -99,6 +108,8 @@ function Map() Int64[], Int64[], UInt16[], + Dict{Int64,Int64}(), + Int64[], MOI.Utilities.IndexMap(), MOI.Utilities.IndexMap(), 0, @@ -127,6 +138,8 @@ function Base.empty!(map::Map) empty!(map.vector_of_variables_map) empty!(map.vector_of_variables_length) empty!(map.set_mask) + empty!(map.variable_to_slot) + empty!(map.slot_to_variable) map.outer_to_inner = MOI.Utilities.IndexMap() map.inner_to_outer = MOI.Utilities.IndexMap() map.next_outer_variable = 0 @@ -251,19 +264,39 @@ function next_outer_constraint!( return value end +""" + is_bridged_variable(map, vi::MOI.VariableIndex)::Bool + +Return `true` if `vi` is (or was) a variable bridged by `map`. Unlike +[`haskey`](@ref), this stays `true` after the variable is deleted (the +outer index is never reused), matching the role of the former +`vi.value < 0` test. +""" +is_bridged_variable(map::Map, vi::MOI.VariableIndex) = + haskey(map.variable_to_slot, vi.value) + +# Internal `slot` of the bridged variable `vi`, i.e. the index into the dense +# per-bridge `Vector`s. Errors if `vi` is not a bridged variable. +_slot(map::Map, vi::MOI.VariableIndex) = map.variable_to_slot[vi.value] + +# Outer `VariableIndex` stored at `slot`. +_variable(map::Map, slot::Integer) = MOI.VariableIndex(map.slot_to_variable[slot]) + function bridge_index(map::Map, vi::MOI.VariableIndex) - index = map.info[-vi.value] + slot = _slot(map, vi) + index = map.info[slot] if index ≤ 0 - return -vi.value + return slot else - return -vi.value - index + 1 + return slot - index + 1 end end function Base.haskey(map::Map, vi::MOI.VariableIndex) - return -length(map.bridges) ≤ vi.value ≤ -1 && + slot = get(map.variable_to_slot, vi.value, 0) + return slot != 0 && map.bridges[bridge_index(map, vi)] !== nothing && - map.index_in_vector[-vi.value] != -1 + map.index_in_vector[slot] != -1 end function Base.getindex(map::Map, vi::MOI.VariableIndex) @@ -271,7 +304,8 @@ function Base.getindex(map::Map, vi::MOI.VariableIndex) end function Base.delete!(map::Map, vi::MOI.VariableIndex) - if iszero(map.info[-vi.value]) + slot = _slot(map, vi) + if iszero(map.info[slot]) # Delete scalar variable index = bridge_index(map, vi) map.bridges[index] = nothing @@ -282,18 +316,17 @@ function Base.delete!(map::Map, vi::MOI.VariableIndex) 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 + 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)) + elseif bridge_index(map, vi) != bridge_index(map, _variable(map, 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 @@ -306,8 +339,9 @@ 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 = _slot(map, vi) + 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 @@ -319,7 +353,7 @@ function Base.keys(map::Map) vi -> haskey(map, vi), MOI.Utilities.lazy_map( MOI.VariableIndex, - i -> MOI.VariableIndex(-i), + slot -> _variable(map, slot), eachindex(map.bridges), ), ) @@ -329,12 +363,12 @@ Base.length(map::Map) = count(bridge -> bridge !== nothing, map.bridges) function number_of_variables(map::Map) num = 0 - for i in eachindex(map.bridges) - if map.bridges[i] !== nothing - if iszero(map.info[i]) + for slot in eachindex(map.bridges) + if map.bridges[slot] !== nothing + if iszero(map.info[slot]) num += 1 else - num += length_of_vector_of_variables(map, MOI.VariableIndex(-i)) + num += length_of_vector_of_variables(map, _variable(map, slot)) end end end @@ -354,7 +388,7 @@ function Base.iterate(map::Map, state = 1) if state > length(map.bridges) return nothing else - return MOI.VariableIndex(-state) => map.bridges[state], state + 1 + return _variable(map, state) => map.bridges[state], state + 1 end end @@ -399,28 +433,31 @@ function first_variable( map::Map, ci::MOI.ConstraintIndex{MOI.VectorOfVariables}, ) - return MOI.VariableIndex(map.vector_of_variables_map[-ci.value]) + return _variable(map, 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) index = bridge_index(map, vi) - constraint_index = map.info[index] - if iszero(constraint_index) - constraint_index = -index + info = map.info[index] + if iszero(info) + # Scalar: by MOI convention `ci.value == vi.value`. + return MOI.ConstraintIndex{F,S}(map.slot_to_variable[index]) + else + # Vector: `info == -ci.value`. + return MOI.ConstraintIndex{F,S}(-info) 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)) + if !(ci.value in eachindex(map.vector_of_variables_map)) return false end - index = -map.vector_of_variables_map[-ci.value] + index = map.vector_of_variables_map[ci.value] return index in eachindex(map.bridges) && !isnothing(map.bridges[index]) && map.sets[index] === S @@ -430,7 +467,7 @@ function MOI.is_valid( map::Map, ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, ) where {S} - index = -ci.value + index = get(map.variable_to_slot, ci.value, 0) return index in eachindex(map.bridges) && !isnothing(map.bridges[index]) && map.sets[index] === S @@ -466,7 +503,7 @@ function MOI.add_constraint( ::S, ) where {T,S<:_BOUNDED_VARIABLE_SCALAR_SETS{T}} flag = MOI.Utilities._single_variable_flag(S) - index = -vi.value + index = _slot(map, vi) mask = map.set_mask[index] MOI.Utilities._throw_if_lower_bound_set(vi, S, mask, T) MOI.Utilities._throw_if_upper_bound_set(vi, S, mask, T) @@ -492,7 +529,7 @@ 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 + map.set_mask[_slot(map, MOI.VariableIndex(ci.value))] &= ~flag return end @@ -504,7 +541,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, _variable(map, i)) for i in eachindex(map.sets) if map.sets[i] == S ] end @@ -544,7 +581,7 @@ function has_keys(map::Map, vis::Vector{MOI.VariableIndex}) vis, ) && all(vi -> haskey(map, vi), vis) && - all(i -> vis[i].value < vis[i-1].value, 2:length(vis)) + all(i -> _slot(map, vis[i]) == _slot(map, vis[i-1]) + 1, 2:length(vis)) ) end @@ -572,7 +609,7 @@ 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]) + return MOI.Bridges.IndexInVector(map.index_in_vector[_slot(map, vi)]) end """ @@ -607,9 +644,14 @@ function add_key_for_bridge( push!(map.bridges, nothing) push!(map.sets, typeof(set)) push!(map.set_mask, 0x0000) + # Allocate the positive outer index for the new bridged variable and + # record the outer <-> slot bijection. Must be done after pushing to + # `map.info` so that `has_bridges(map)` is `true`. + value = next_outer_variable!(map) + push!(map.slot_to_variable, value) + map.variable_to_slot[value] = bridge_index + variable = MOI.VariableIndex(value) map.bridges[bridge_index] = call_in_context(map, bridge_index, bridge_fun) - index = -bridge_index - variable = MOI.VariableIndex(index) if map.unbridged_function !== nothing mappings = unbridged_map(something(map.bridges[bridge_index]), variable) if mappings === nothing @@ -624,7 +666,7 @@ function add_key_for_bridge( 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)}(value) end """ @@ -654,20 +696,28 @@ function add_keys_for_bridge( push!(map.parent_index, map.current_context) bridge_index = Int64(length(map.parent_index)) F = MOI.VectorOfVariables + # Allocate a positive `CI{VectorOfVariables, S}` value for this vector of + # constrained variables, skipping (with placeholders) the values that + # `is_available` reports as already taken (by constraint bridges or by + # the inner model). `vector_of_variables_map` stays dense, indexed by the + # (positive) `ci.value`. while !is_available( - MOI.ConstraintIndex{F,S}(-length(map.vector_of_variables_map) - 1), + 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_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) + 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)) push!(map.set_mask, 0x0000) + value = next_outer_variable!(map) + push!(map.slot_to_variable, value) + map.variable_to_slot[value] = bridge_index for i in 2:MOI.dimension(set) push!(map.parent_index, 0) push!(map.info, i) @@ -675,10 +725,13 @@ function add_keys_for_bridge( push!(map.bridges, nothing) push!(map.sets, nothing) push!(map.set_mask, 0x0000) + value_i = next_outer_variable!(map) + push!(map.slot_to_variable, value_i) + map.variable_to_slot[value_i] = bridge_index + 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) + _variable(map, bridge_index - 1 + i) for i in 1:MOI.dimension(set) ] if map.unbridged_function !== nothing mappings = @@ -714,14 +767,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] + first_slot = map.vector_of_variables_map[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 slot in first_slot:length(map.bridges) + if map.index_in_vector[slot] == -1 continue - elseif bridge_index(map, vi) == -index - push!(variables, vi) + elseif bridge_index(map, _variable(map, slot)) == first_slot + push!(variables, _variable(map, slot)) else break end @@ -870,3 +922,7 @@ register_context(::EmptyMap, ::MOI.ConstraintIndex) = nothing call_in_context(::EmptyMap, ::MOI.ConstraintIndex, f::Function) = f() MOI.is_valid(::EmptyMap, ::MOI.ConstraintIndex) = false + +Base.haskey(::EmptyMap, ::MOI.VariableIndex) = false + +is_bridged_variable(::EmptyMap, ::MOI.VariableIndex) = false diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 99aad953d7..67c8812973 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -65,7 +65,9 @@ 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) + return Variable.is_bridged_variable(Variable.bridges(b), vi) +end """ is_bridged(b::AbstractBridgeOptimizer, ci::MOI.ConstraintIndex) @@ -84,40 +86,31 @@ 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)) - ) + # By MOI convention, a `CI{VariableIndex, S}` shares its `value` with the + # constrained variable. There are three possibilities: + # * the variable is bridged: the constraint was either created by a + # variable bridge or force-bridged because it was added on a bridged + # variable. Either way it is bridged. + # * the variable is not bridged but the constraint was added with a + # constraint bridge: it lives in `Constraint.bridges(b)`. + # * otherwise it passes through to `b.model` and is not bridged. + if is_bridged(b, 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 + # A `CI{VectorOfVariables, S}` is bridged if it is a vector of constrained + # variables (stored in `Variable.bridges(b)`) or a force-/constraint- + # bridged constraint (stored in `Constraint.bridges(b)`). + if MOI.is_valid(Variable.bridges(b), ci) + return true + end + return haskey(Constraint.bridges(b), ci) end """ @@ -170,19 +163,29 @@ end 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. +`Variable.add_keys_for_bridge`. This is *not* the same as a constraint that +was merely force-bridged on a bridged variable (that one lives in +`Constraint.bridges(b)`). """ 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}, +) + # The constraint of a bridged constrained scalar variable shares its + # `value` with the variable. Exclude force-bridged constraints, which are + # stored in `Constraint.bridges(b)`. + return is_bridged(b, MOI.VariableIndex(ci.value)) && + !haskey(Constraint.bridges(b), ci) +end + +function is_variable_bridged( + b::AbstractBridgeOptimizer, + ci::MOI.ConstraintIndex{MOI.VectorOfVariables}, ) - # 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) + return MOI.is_valid(Variable.bridges(b), ci) && + !haskey(Constraint.bridges(b), ci) end """ @@ -1136,7 +1139,8 @@ function MOI.delete( else delete!(Constraint.bridges(b)::Constraint.Map, ci) end - if F === MOI.VariableIndex && ci.value < 0 + if F === MOI.VariableIndex && + is_bridged(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) @@ -2476,11 +2480,15 @@ end """ _is_available_constraint_index(b, ci::MOI.ConstraintIndex) -Return `true` if `ci` can be allocated by `Constraint.add_key_for_bridge` -without clashing with an index already in use in the outer namespace: by -the variable bridges (constrained-on-creation vectors of variables) or by -an identity entry copied when the `(F, S)` constraint mapping was -activated. +Return `true` if `ci` can be allocated (by `Constraint.add_key_for_bridge` +for a force-bridged constraint, or by `Variable.add_keys_for_bridge` for a +vector of constrained variables) without clashing with an index already in +use in the outer `(F, S)` namespace, namely: + + * a vector of constrained variables in `Variable.bridges(b)`, + * a force-/constraint-bridged constraint in `Constraint.bridges(b)`, + * an inner constraint translated into the outer namespace when the + `(F, S)` constraint mapping was activated. """ function _is_available_constraint_index( b::AbstractBridgeOptimizer, @@ -2489,6 +2497,9 @@ function _is_available_constraint_index( if MOI.is_valid(Variable.bridges(b), ci) return false end + if haskey(Constraint.bridges(b), ci) + return false + end map = Variable.bridges(b) if map isa Variable.Map && Variable.is_constraint_mapping_active(map, F, S) && @@ -2926,28 +2937,33 @@ 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)) + v_map = Variable.bridges(b)::Variable.Map # Activate the outer/inner variable translation map at this layer # before allocating the new bridged variables. This ensures every # variable visible to the user from now on lives in the outer # namespace. - Variable.activate_variable_mapping!( - Variable.bridges(b)::Variable.Map, + Variable.activate_variable_mapping!(v_map, b.model) + # The constrained-variable `CI{VectorOfVariables, S}` shares the outer + # `(VOV, S)` namespace with inner passthrough constraints and force- + # bridged constraints. Activate the constraint mapping for `(VOV, S)` + # so that inner indices are translated out of the way, and pass + # `_is_available_constraint_index` so the allocation skips any value + # already taken by `Constraint.bridges` or by an inner constraint. + Variable.activate_constraint_mapping!( + v_map, b.model, + MOI.VectorOfVariables, + 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, + v_map, () -> Variable.bridge_constrained_variable( BridgeType, recursive_model(b), set, ), set, - !Base.Fix1(haskey, Constraint.bridges(b)), + Base.Fix1(_is_available_constraint_index, b), ) else variables = MOI.add_variables(b, MOI.dimension(set)) @@ -3112,10 +3128,21 @@ function unbridged_variable_function( func = Variable.unbridged_function(Variable.bridges(b)::Variable.Map, vi) if func === nothing return vi - else - # If two variable bridges are chained, `func` may still contain - # variables to unbridge. + elseif recursive_model(b) === b + # `LazyBridgeOptimizer`: the bridges created their variables through + # `b`, so `func` is expressed in `b`'s outer namespace and may chain + # through further bridge-created (outer) variables that still need + # unbridging. These all have distinct outer values. return unbridged_function(b, func) + else + # `SingleBridgeOptimizer`: the bridge created its variables directly + # in `b.model`, so the `unbridged_function` map is keyed by inner + # variables while `func` is already expressed in terms of the final + # outer bridged variables. We must NOT re-process `func`: an outer + # bridged variable and an inner bridge-created variable can share the + # same positive value, so looking `func`'s variables up in the map + # would wrongly match and recurse forever. + return func end end