diff --git a/src/Bridges/Variable/map.jl b/src/Bridges/Variable/map.jl index 806d4b939a..392f6b51a9 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,130 @@ 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_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, + ::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) + # `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) + 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 has_bridges(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..99aad953d7 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -464,12 +464,277 @@ function MOI.Utilities.final_touch(b::AbstractBridgeOptimizer, index_map) end # References +""" + outer_to_inner(b::AbstractBridgeOptimizer, idx::MOI.Index)::typeof(idx) + +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`, 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. +""" +function outer_to_inner( + b::AbstractBridgeOptimizer, + vi::MOI.VariableIndex, +) + map = Variable.bridges(b) + if Variable.has_bridges(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 Variable.has_bridges(map) && + 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 + +""" + inner_to_outer(b::AbstractBridgeOptimizer, idx::MOI.Index)::typeof(idx) + +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 inner_to_outer( + b::AbstractBridgeOptimizer, + vi::MOI.VariableIndex, +) + map = Variable.bridges(b) + if Variable.has_bridges(map) + return map.inner_to_outer[vi] + end + return vi +end + +function inner_to_outer( + b::AbstractBridgeOptimizer, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + map = Variable.bridges(b) + if Variable.has_bridges(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) + if Variable.has_bridges(Variable.bridges(b)) && 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) + if Variable.has_bridges(Variable.bridges(b)) + 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) + if Variable.has_bridges(Variable.bridges(b)) + 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 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)) + 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) - else - return MOI.is_valid(b.model, vi) end + map = Variable.bridges(b) + 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. + haskey(map.outer_to_inner, vi) || return false + end + return MOI.is_valid(b.model, outer_to_inner(b, vi)) end function MOI.is_valid( @@ -510,8 +775,56 @@ 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 Variable.has_bridges(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 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] + 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( @@ -701,7 +1014,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 @@ -733,11 +1048,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 !Variable.has_bridges(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}, @@ -770,7 +1149,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 @@ -784,7 +1165,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 @@ -795,12 +1180,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[] @@ -826,9 +1211,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 @@ -837,22 +1222,36 @@ 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 Variable.has_bridges(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 @@ -1035,7 +1434,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( @@ -1088,7 +1487,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 @@ -1261,7 +1660,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( @@ -1353,6 +1756,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, @@ -1361,7 +1804,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 @@ -1397,10 +1840,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( @@ -1413,7 +1856,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 @@ -1438,7 +1883,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 @@ -1452,7 +1902,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 @@ -1508,7 +1961,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 @@ -1528,6 +1981,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) @@ -1535,7 +1994,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 @@ -1563,7 +2026,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 @@ -1602,7 +2065,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. @@ -1641,13 +2104,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( @@ -1672,7 +2135,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( @@ -1791,11 +2254,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 @@ -1808,13 +2273,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 @@ -1827,7 +2294,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 @@ -1849,7 +2316,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 @@ -1871,6 +2338,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 !Variable.has_bridges(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, @@ -1881,6 +2381,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 @@ -1914,6 +2418,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( @@ -1937,12 +2445,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 @@ -1960,19 +2473,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 @@ -2068,6 +2607,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, @@ -2089,7 +2636,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 @@ -2106,15 +2664,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( @@ -2138,6 +2701,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, @@ -2159,7 +2761,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 @@ -2202,7 +2804,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 @@ -2230,30 +2836,58 @@ 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 end +# Variables + +""" + _record_inner_variable!(b::AbstractBridgeOptimizer, inner_vi::MOI.VariableIndex) + +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 !Variable.has_bridges(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 +2918,22 @@ 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 + ] + 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)) + # 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 @@ -2295,7 +2941,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)), ) @@ -2323,10 +2973,20 @@ 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, 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)) + # 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( @@ -2375,11 +3035,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