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 806d4b939a..55926dcbac 100644 --- a/src/Bridges/Variable/map.jl +++ b/src/Bridges/Variable/map.jl @@ -8,22 +8,49 @@ 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. + +## 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 @@ -38,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` -> @@ -46,6 +73,26 @@ 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 + # 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 +108,12 @@ function Map() Int64[], Int64[], UInt16[], + Dict{Int64,Int64}(), + Int64[], + MOI.Utilities.IndexMap(), + MOI.Utilities.IndexMap(), + 0, + Dict{Tuple{DataType,DataType},Int64}(), ) end @@ -85,22 +138,165 @@ 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 + 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 + +""" + 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) @@ -108,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 @@ -119,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 @@ -143,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 @@ -156,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), ), ) @@ -166,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 @@ -191,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 @@ -236,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 @@ -267,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 @@ -303,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) @@ -329,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 @@ -341,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 @@ -381,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 @@ -409,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 """ @@ -444,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 @@ -461,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 """ @@ -491,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) @@ -512,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 = @@ -551,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 @@ -707,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 a13ab5093d..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}, ) - # 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) + # 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}, +) + return MOI.is_valid(Variable.bridges(b), ci) && + !haskey(Constraint.bridges(b), ci) end """ @@ -464,12 +467,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,10 +778,58 @@ 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( b::AbstractBridgeOptimizer, vis::Vector{MOI.VariableIndex}, @@ -701,7 +1017,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 +1051,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}, @@ -757,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) @@ -770,7 +1153,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 +1169,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 +1184,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 +1215,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 +1226,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 +1438,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 +1491,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 +1664,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 +1760,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 +1808,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 +1844,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 +1860,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 +1887,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 +1906,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 +1965,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 +1985,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 +1998,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 +2030,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 +2069,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 +2108,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 +2139,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 +2258,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 +2277,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 +2298,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 +2320,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 +2342,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 +2385,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 +2422,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 +2449,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 +2477,52 @@ 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` +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, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + 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) && + 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 +2618,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 +2647,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 +2675,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 +2712,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 +2772,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 +2815,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 +2847,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,20 +2929,41 @@ 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)) - # `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. + 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!(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), + ) return Variable.add_keys_for_bridge( - Variable.bridges(b)::Variable.Map, - () -> Variable.bridge_constrained_variable(BridgeType, b, set), + 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)) @@ -2323,10 +2989,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 +3051,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 @@ -2434,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 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