diff --git a/src/Bridges/Constraint/map.jl b/src/Bridges/Constraint/map.jl index eb35c6beda..ef0d378625 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 && @@ -126,10 +124,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 @@ -243,7 +237,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 && @@ -292,7 +286,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..81cb909e66 100644 --- a/src/Bridges/Variable/map.jl +++ b/src/Bridges/Variable/map.jl @@ -8,22 +8,53 @@ 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, but the largest inner index seen so +far is recorded (see [`record_identity!`](@ref)). As soon as the first +variable bridge is added, [`activate_variable_mapping!`](@ref) is called: the +indices up to that largest one keep being translated by the identity (see +[`is_identity_index`](@ref)), 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 in `outer_to_inner` and `inner_to_outer`. + +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)`: the existing inner +`CI{F, S}` keep being translated by the identity; afterwards the outer and +inner `CI{F, S}` namespaces are independent. + +As the inner model is never queried for its existing indices, it must be +empty when wrapped by the bridge optimizer and only be modified through it. + +## 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 +69,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 +77,32 @@ 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 of the + # non-bridged indices allocated after the activation of the mapping. + # The indices allocated before are translated by the identity and have no + # entry here, see `is_identity_index`. + outer_to_inner::MOI.Utilities.IndexMap + # Reverse of `outer_to_inner`. + inner_to_outer::MOI.Utilities.IndexMap + # Largest inner `VariableIndex.value` allocated before the first variable + # bridge was added. It is updated by `record_identity!` until then. + last_identity_variable::Int64 + # Same as `last_identity_variable` but for `ConstraintIndex{F, S}`, until + # the constraint mapping is activated for `(F, S)`. + last_identity_constraint::Dict{Tuple{DataType,DataType},Int64} + # Next available outer `VariableIndex.value`. + # `activate_variable_mapping!` sets it after `last_identity_variable` + # when the first variable bridge is added. + 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 +118,14 @@ function Map() Int64[], Int64[], UInt16[], + Dict{Int64,Int64}(), + Int64[], + MOI.Utilities.IndexMap(), + MOI.Utilities.IndexMap(), + 0, + Dict{Tuple{DataType,DataType},Int64}(), + 1, + Dict{Tuple{DataType,DataType},Int64}(), ) end @@ -85,22 +150,215 @@ 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.last_identity_variable = 0 + empty!(map.last_identity_constraint) + map.next_outer_variable = 1 + 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 + +""" + record_identity!(map::Map, idx::MOI.Index) + +Record that the inner model allocated `idx` while its type is translated by +the identity, so that [`activate_variable_mapping!`](@ref) and +[`activate_constraint_mapping!`](@ref) can later allocate the outer indices +after it. +""" +function record_identity!(map::Map, vi::MOI.VariableIndex) + map.last_identity_variable = max(map.last_identity_variable, vi.value) + return +end + +function record_identity!(map::Map, ci::MOI.ConstraintIndex{F,S}) where {F,S} + last = get(map.last_identity_constraint, (F, S), Int64(0)) + map.last_identity_constraint[(F, S)] = max(last, ci.value) + return +end + +""" + is_identity_index(map::Map, idx::MOI.Index)::Bool + +Return `true` if `idx` was allocated before the activation of the mapping for +its type. Such an index is translated by the identity in both directions and +has no entry in `map.outer_to_inner` nor `map.inner_to_outer`. +""" +function is_identity_index(map::Map, vi::MOI.VariableIndex) + return vi.value <= map.last_identity_variable +end + +function is_identity_index(map::Map, ci::MOI.ConstraintIndex{F,S}) where {F,S} + return ci.value <= get(map.last_identity_constraint, (F, S), 0) +end + +""" + outer_to_inner(map::Map, idx::MOI.Index[, default]) + +Return the inner index corresponding to the outer index `idx`, assuming that +the mapping is active for the type of `idx`. If there is none, return +`default` or throw `MOI.InvalidIndex(idx)` if `default` is not given. +""" +function outer_to_inner(map::Map, idx::MOI.Index) + inner = outer_to_inner(map, idx, nothing) + if inner === nothing + throw(MOI.InvalidIndex(idx)) + end + return inner +end + +function outer_to_inner(map::Map, idx::MOI.Index, default) + if is_identity_index(map, idx) + return idx + end + return haskey(map.outer_to_inner, idx) ? map.outer_to_inner[idx] : default +end + +""" + inner_to_outer(map::Map, idx::MOI.Index[, default]) + +Inverse of [`outer_to_inner`](@ref). +""" +function inner_to_outer(map::Map, idx::MOI.Index) + return is_identity_index(map, idx) ? idx : map.inner_to_outer[idx] +end + +function inner_to_outer(map::Map, idx::MOI.Index, default) + if is_identity_index(map, idx) + return idx + end + return haskey(map.inner_to_outer, idx) ? map.inner_to_outer[idx] : default +end + +""" + activate_variable_mapping!(map::Map) + +Start allocating outer variable indices after the ones allocated so far by the +inner model, so that subsequent outer-only or inner-only allocations can +extend the two namespaces independently. No-op if the mapping is already +active. +""" +function activate_variable_mapping!(map::Map) + # `has_bridges` flips to `true` only inside `add_key_for_bridge` (which + # pushes to `map.info`), and that always runs *after* this function in + # `add_constrained_variable`. So on the first variable bridge this is + # still `false` and we activate; 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 + map.next_outer_variable = map.last_identity_variable + 1 + return +end + +""" + activate_constraint_mapping!(map::Map, ::Type{F}, ::Type{S}) + +Start allocating outer `CI{F, S}` after the ones allocated so far by the inner +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, + ::Type{F}, + ::Type{S}, +) where {F,S} + if is_constraint_mapping_active(map, F, S) + return + end + last = get(map.last_identity_constraint, (F, S), Int64(0)) + map.next_outer_constraint[(F, S)] = last + 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. +""" +function is_bridged_variable(map::Map, vi::MOI.VariableIndex) + return haskey(map.variable_to_slot, vi.value) +end + +# Internal `slot` of the bridged variable `vi`, that is, 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`. +function _variable(map::Map, slot::Integer) + return MOI.VariableIndex(map.slot_to_variable[slot]) +end + 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 +366,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 +378,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 +401,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 +415,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 +425,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 +450,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 +495,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 +529,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 +565,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 +591,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 +603,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 +643,9 @@ 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)) + # Members of a vector have strictly ascending outer values (they are + # consecutive at creation; a subset stays ascending after deletions). + all(i -> vis[i].value > vis[i-1].value, 2:length(vis)) ) end @@ -409,7 +673,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 +708,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 +730,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 +760,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 +789,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 +831,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 diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 15a97836ae..0a109dc703 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -65,7 +65,9 @@ function is_bridged end Return a `Bool` indicating whether `vi` is bridged. The variable is said to be bridged if it is a variable of `b` but not a variable of `b.model`. """ -is_bridged(::AbstractBridgeOptimizer, vi::MOI.VariableIndex) = vi.value < 0 +function is_bridged(b::AbstractBridgeOptimizer, vi::MOI.VariableIndex) + return Variable.is_bridged_variable(Variable.bridges(b), vi) +end """ is_bridged(b::AbstractBridgeOptimizer, ci::MOI.ConstraintIndex) @@ -84,40 +86,31 @@ function is_bridged( b::AbstractBridgeOptimizer, ci::MOI.ConstraintIndex{MOI.VariableIndex,S}, ) where {S} - # There are a few cases for which we should return `false`: - # 1) It was added as variables constrained on creation to `b.model`, - # In this case, `is_bridged(b, S)` is `false` and `ci.value >= 0`. - # 2) It was added as constraint on a non-bridged variable to `b.model`, - # In this case, `is_bridged(b, F, S)` is `false` and `ci.value >= 0`. - # and a few cases for which we should return `true`: - # 3) It was added with a variable bridge, - # In this case, `is_bridged(b, S)` is `true` and `ci.value < 0`. - # 4) It was added as constraint on a bridged variable so it was force-bridged, - # In this case, `ci.value < 0`. - # 5) It was added with a constraint bridge, - # In this case, `is_bridged(b, F, S)` is `true` and `ci.value >= 0` (the variable is non-bridged, otherwise, the constraint would have been force-bridged). - # So - # * if, `ci.value < 0` then it is case 3) or 4) and we return `true`. - # * Otherwise, - # - if `is_bridged(b, S)` and `is_bridged(b, F, S)` then 1) and 2) are - # not possible so we are in case 5) and we return `true`. - # - if `!is_bridged(b, F, S)`, then 5) is not possible and we return `false`. - # - if `!is_bridged(b, S)` and `is_bridged(b, F, S)`, then it is either case 1) - # or 5). They cannot both be the cases as one cannot add two `VariableIndex` - # with the same set type on the same variable (this is ensured by - # `_check_double_single_variable`). Therefore, we can safely determine - # whether it is bridged with `haskey(Constraint.bridges(b), ci)`. - return ci.value < 0 || ( - is_bridged(b, MOI.VariableIndex, S) && - (is_bridged(b, S) || haskey(Constraint.bridges(b), ci)) - ) + # By MOI convention, a `CI{VariableIndex, S}` shares its `value` with the + # constrained variable. There are three possibilities: + # * the variable is bridged: the constraint was either created by a + # variable bridge or force-bridged because it was added on a bridged + # variable. Either way it is bridged. + # * the variable is not bridged but the constraint was added with a + # constraint bridge: it lives in `Constraint.bridges(b)`. + # * otherwise it passes through to `b.model` and is not bridged. + if is_bridged(b, MOI.VariableIndex(ci.value)) + return true + end + return haskey(Constraint.bridges(b), ci) end function is_bridged( - ::AbstractBridgeOptimizer, + b::AbstractBridgeOptimizer, ci::MOI.ConstraintIndex{MOI.VectorOfVariables,S}, ) where {S} - return ci.value < 0 + # A `CI{VectorOfVariables, S}` is bridged if it is a vector of constrained + # variables (stored in `Variable.bridges(b)`) or a force-/constraint- + # bridged constraint (stored in `Constraint.bridges(b)`). + if MOI.is_valid(Variable.bridges(b), ci) + return true + end + return haskey(Constraint.bridges(b), ci) end """ @@ -170,19 +163,29 @@ end Returns whether `ci` is the constraint of a bridged constrained variable. That is, if it was returned by `Variable.add_key_for_bridge` or -`Variable.add_keys_for_bridge`. Note that it is not equivalent to -`ci.value < 0` as, it can also simply be a constraint on a bridged variable. +`Variable.add_keys_for_bridge`. This is *not* the same as a constraint that +was merely force-bridged on a bridged variable (that one lives in +`Constraint.bridges(b)`). """ is_variable_bridged(::AbstractBridgeOptimizer, ::MOI.ConstraintIndex) = false function is_variable_bridged( b::AbstractBridgeOptimizer, - ci::MOI.ConstraintIndex{<:Union{MOI.VariableIndex,MOI.VectorOfVariables}}, + ci::MOI.ConstraintIndex{MOI.VariableIndex}, +) + # The constraint of a bridged constrained scalar variable shares its + # `value` with the variable. Exclude force-bridged constraints, which are + # stored in `Constraint.bridges(b)`. + return is_bridged(b, MOI.VariableIndex(ci.value)) && + !haskey(Constraint.bridges(b), ci) +end + +function is_variable_bridged( + b::AbstractBridgeOptimizer, + ci::MOI.ConstraintIndex{MOI.VectorOfVariables}, ) - # It can be a constraint corresponding to bridged constrained variables so - # we `check` with `haskey(Constraint.bridges(b), ci)` whether this is the - # case. - return ci.value < 0 && !haskey(Constraint.bridges(b), ci) + return MOI.is_valid(Variable.bridges(b), ci) && + !haskey(Constraint.bridges(b), ci) end """ @@ -464,12 +467,266 @@ 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 Variable.outer_to_inner(map, 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 Variable.outer_to_inner(map, 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 Variable.inner_to_outer(map, 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 Variable.inner_to_outer(map, 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) + +""" + _TotalInnerToOuter(b::AbstractBridgeOptimizer) + +Callable wrapper around [`inner_to_outer`](@ref) for use with +[`MOI.Utilities.map_indices`](@ref), except that 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 Variable.inner_to_outer(map, vi, vi) +end + +function (f::_TotalInnerToOuter)(ci::MOI.ConstraintIndex{F,S}) where {F,S} + map = Variable.bridges(f.b)::Variable.Map + return Variable.inner_to_outer(map, ci, 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)) + # Skip the values already taken by bridged constraints. + while !_is_available_constraint_index(b, outer_ci) + outer_ci = MOI.ConstraintIndex{F,S}( + Variable.next_outer_constraint!(map, F, S), + ) + end + map.outer_to_inner[outer_ci] = inner_ci + map.inner_to_outer[inner_ci] = outer_ci + return outer_ci + end + # Only `VectorOfVariables` constraints can be force-bridged, so the + # constraint mapping is never activated for other functions. + if F == MOI.VectorOfVariables && map isa Variable.Map + Variable.record_identity!(map, inner_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 + inner_vi = _inner_index_or_nothing(b, vi) + if inner_vi === nothing + return false + end + return MOI.is_valid(b.model, inner_vi) end function MOI.is_valid( @@ -510,10 +767,55 @@ 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 Variable.outer_to_inner(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) + return Variable.outer_to_inner(map, ci, 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 +1003,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 +1037,74 @@ 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 +1124,7 @@ 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 +1137,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 +1153,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 +1168,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[] @@ -814,9 +1187,11 @@ function _get_all_including_bridged( push!(user_only_variables, user_variable) n = Variable.length_of_vector_of_variables(map, user_variable) for i in 1:(n-1) + # Members of a bridged vector of variables have consecutive + # ascending outer values. push!( user_only_variables, - MOI.VariableIndex(user_variable.value - i), + MOI.VariableIndex(user_variable.value + i), ) end else @@ -826,9 +1201,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,29 +1212,54 @@ 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). Translate `inner_variable` to the outer + # namespace (identity for a bridge-created inner variable, which has + # no outer counterpart) before the lookup. We must NOT "try the raw + # inner index first": under positive indexing an inner variable may + # coincidentally share a value with an unrelated outer key, which + # would produce a wrong (duplicate) match. + map_b = Variable.bridges(b) + key = if Variable.has_bridges(map_b) + Variable.inner_to_outer(map_b, inner_variable, inner_variable) + else + inner_variable + 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] + # to be mapped further. This only happens for a `LazyBridgeOptimizer` + # (`recursive_model(b) === b`), where the chained variables all live in + # the distinct outer namespace. For a `SingleBridgeOptimizer` the map + # is a single hop from an inner created variable to its outer user + # variable, and following the chain would loop forever when the two + # share a value (for example, an identity bridge: `bridge_var_map[1] + # == 1`). + if recursive_model(b) === b + while haskey(bridge_var_map, outer_variable) + outer_variable = bridge_var_map[outer_variable] + end 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 - # if multiple user-variables were bridged. Add them all. + # if multiple user-variables were bridged. Add them all. Members + # have consecutive ascending outer values. n = Variable.length_of_vector_of_variables(map, outer_variable) for i in 1:(n-1) - push!(ret, MOI.VariableIndex(outer_variable.value - i)) + push!(ret, MOI.VariableIndex(outer_variable.value + i)) end end end @@ -912,6 +1312,21 @@ function _remove_bridged(list, bridge, attr) return end +# Filtering applied to the variable bridges only (`Variable.bridges(b)`). For +# `ListOfConstraintIndices` it removes the constraints those bridges added, +# like the generic method. For `ListOfVariableIndices` it is a no-op: +# `_get_all_including_bridged` already maps `b.model`'s variables back to the +# outer namespace and excludes the variables created by variable bridges. +# (Removing them here would be incorrect under positive indexing, where such a +# variable may share a value with an outer user variable; under the old +# negative indexing this was a no-op because the namespaces were +# value-disjoint.) +function _remove_variable_bridged(list, bridge, attr) + return _remove_bridged(list, bridge, attr) +end + +_remove_variable_bridged(list, bridge, ::MOI.ListOfVariableIndices) = nothing + # The tactic for this function is to first query all possible indices, and then # to filter out the indices that have been bridged. # @@ -923,7 +1338,7 @@ function MOI.get( ) list = _get_all_including_bridged(b, attr) for bridge in values(Variable.bridges(b)) - _remove_bridged(list, bridge, attr) + _remove_variable_bridged(list, bridge, attr) end for bridge in values(Constraint.bridges(b)) _remove_bridged(list, bridge, attr) @@ -1035,7 +1450,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 +1503,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 +1676,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( @@ -1323,7 +1742,7 @@ function MOI.set( BridgeType = Objective.concrete_bridge_type(b, typeof(func)) _bridge_objective(b, BridgeType, func) else - MOI.set(b.model, attr, func) + MOI.set(b.model, attr, _to_inner_value(b, func)) end return end @@ -1353,6 +1772,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 +1820,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 +1856,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 +1872,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 +1899,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 +1918,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 +1977,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 +1997,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 +2010,9 @@ 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 +2040,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 +2079,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 +2118,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( @@ -1659,7 +2136,7 @@ function MOI.get( MOI.throw_if_not_valid(b, ci) return call_in_context(MOI.get, b, ci, attr) end - ret = MOI.get(b.model, attr, ci) + ret = MOI.get(b.model, attr, outer_to_inner(b, ci)) if !Variable.has_bridges(Variable.bridges(b)) || ret === nothing return ret end @@ -1683,7 +2160,7 @@ function MOI.set( MOI.throw_if_not_valid(b, ci) return call_in_context(MOI.set, b, ci, attr, value) elseif !Variable.has_bridges(Variable.bridges(b)) || value === nothing - MOI.set(b.model, attr, ci, value) + MOI.set(b.model, attr, outer_to_inner(b, ci), value) return end # In this case, the scalar constraint might contain bridged variables @@ -1693,7 +2170,7 @@ function MOI.set( # from `x`-space to `y`-space by subtracting the constant offset. f = MOI.get(b, MOI.ConstraintFunction(), ci) offset = MOI.constant(bridged_function(b, f), eltype(value)) - MOI.set(b.model, attr, ci, value - offset) + MOI.set(b.model, attr, outer_to_inner(b, ci), value - offset) return end @@ -1813,11 +2290,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 @@ -1830,13 +2309,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 @@ -1849,7 +2330,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 @@ -1871,7 +2352,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 @@ -1893,6 +2374,38 @@ 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, assuming that the variable mapping is active. 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) + outer_vi = Variable.inner_to_outer(map, vi, nothing) + if outer_vi !== nothing + return outer_vi + end + # An unrecorded inner variable is always created by a variable bridge. + # `outer_vi` is the first variable bridged by that bridge. + outer_vi = findfirst(map) do bridge_ + return vi in MOI.get(bridge_, MOI.ListOfVariableIndices()) + end + @assert outer_vi !== nothing + variables = MOI.get(map[outer_vi], MOI.ListOfVariableIndices()) + # Bridged vectors of variables use consecutive ascending outer values + # starting at the first variable. + return MOI.VariableIndex( + outer_vi.value + findfirst(isequal(vi), variables) - 1, + ) +end + # Query index from name (similar to `UniversalFallback`) function MOI.get( b::AbstractBridgeOptimizer, @@ -1903,6 +2416,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 @@ -1936,6 +2453,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( @@ -1959,12 +2480,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 @@ -1982,6 +2508,38 @@ 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) && + Variable.outer_to_inner(map, ci, nothing) !== nothing + return false + end + return true +end + function add_bridged_constraint(b, BridgeType, f, s) map = Constraint.bridges(b)::Constraint.Map # Okay, this part is a bit tricky. `Constraint.bridge_constraint` might add @@ -2001,15 +2559,16 @@ function add_bridged_constraint(b, BridgeType, f, s) end # `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( map, bridge, f, s, - !Base.Fix1(MOI.is_valid, Variable.bridges(b)), + Base.Fix1(_is_available_constraint_index, b), ) Variable.register_context(Variable.bridges(b), ci) return ci @@ -2105,6 +2664,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 the outer indices of new constraints of this + # type are allocated after the existing inner ones. + v_map = Variable.bridges(b) + if v_map isa Variable.Map + Variable.activate_constraint_mapping!(v_map, F, S) + end BridgeType = Constraint.concrete_bridge_type( constraint_vector_functionize_bridge(b), F, @@ -2126,7 +2693,18 @@ function MOI.add_constraint( # modification has been done in the previous line return add_bridged_constraint(b, BridgeType, f, s) else - return MOI.add_constraint(b.model, f, s) + if F <: MOI.VariableIndex || F <: MOI.VectorOfVariables + # Index functions are never rewritten by + # `bridged_constraint_function` above, so their variable indices + # are still in the outer namespace. + f = _to_inner_index_function(b, f) + else + # `bridged_constraint_function` left the substituted function in + # the outer namespace when `recursive_model(b) === b`. + f = _to_inner_value(b, f) + end + inner_ci = MOI.add_constraint(b.model, f, s) + return _record_inner_constraint!(b, inner_ci) end end @@ -2143,15 +2721,20 @@ function MOI.add_constraints( if any(func -> is_bridged(b, func), f) return MOI.add_constraint.(b, f, s) end + f = F[_to_inner_index_function(b, func)::F for func in f] elseif F == MOI.VectorOfVariables if any(func -> any(vi -> is_bridged(b, vi), func.variables), f) return MOI.add_constraint.(b, f, s) end + f = F[_to_inner_index_function(b, func)::F for func in f] else - f = F[bridged_function(b, func)::F for func in f] + f = F[ + _to_inner_value(b, bridged_function(b, func))::F for func in f + ] end end - return MOI.add_constraints(b.model, f, s) + inner_cis = MOI.add_constraints(b.model, f, s) + return [_record_inner_constraint!(b, ci) for ci in inner_cis] end function is_bridged( @@ -2175,6 +2758,46 @@ 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. + +The only bridge optimizer with bridged variables and +`recursive_model(b) !== b` is `Variable.SingleBridgeOptimizer`, which bridges +neither the objective nor any constraint that can be modified, so +`ci_or_obj` is never bridged in that case. +""" +function _modify_substituted_change( + b::AbstractBridgeOptimizer, + ci::MOI.ConstraintIndex, + change::MOI.AbstractFunctionModification, +) + if recursive_model(b) === b + 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) + else + MOI.modify(b.model, obj, change) + end + return +end + function modify_bridged_change( b::AbstractBridgeOptimizer, ci, @@ -2196,7 +2819,7 @@ function modify_bridged_change( for t in func.terms coefs = [(i, coef * t.coefficient) for (i, coef) in change.new_coefficients] - MOI.modify(b, ci, MOI.MultirowChange(t.variable, coefs)) + _modify_substituted_change(b, ci, MOI.MultirowChange(t.variable, coefs)) end return end @@ -2239,7 +2862,11 @@ function modify_bridged_change( end for t in func.terms coef = t.coefficient * change.new_coefficient - MOI.modify(b, ci_or_obj, MOI.ScalarCoefficientChange(t.variable, coef)) + _modify_substituted_change( + b, + ci_or_obj, + MOI.ScalarCoefficientChange(t.variable, coef), + ) end return end @@ -2267,30 +2894,62 @@ 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, recording it with `Variable.record_identity!` +if `b` owns a `Variable.Map`. +""" +function _record_inner_variable!( + b::AbstractBridgeOptimizer, + inner_vi::MOI.VariableIndex, +) + map = Variable.bridges(b) + if !Variable.has_bridges(map) + if map isa Variable.Map + Variable.record_identity!(map, inner_vi) + end + 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 @@ -2321,20 +2980,40 @@ 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) + # 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, + 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)) @@ -2360,10 +3039,17 @@ 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) return Variable.add_key_for_bridge( Variable.bridges(b)::Variable.Map, () -> Variable.bridge_constrained_variable( @@ -2412,11 +3098,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 @@ -2471,10 +3175,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 ba69f0893e..25febc7d25 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -467,10 +467,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 + return error("Cannot substitute `$x` as it is bridged into `$f`.") end # This method is used when submitting `HeuristicSolution`. diff --git a/test/Bridges/Constraint/test_map.jl b/test/Bridges/Constraint/test_map.jl index d6d9289a52..7657b98797 100644 --- a/test/Bridges/Constraint/test_map.jl +++ b/test/Bridges/Constraint/test_map.jl @@ -61,7 +61,7 @@ function test_map() MOI.SecondOrderCone(2), ci -> true, ) - @test c2.value == -1 + @test c2.value == 1 @test haskey(map, c2) @test map[c2] == b2 @test MOI.Bridges.Constraint.number_of_type(map, typeof(c2)) == 1 diff --git a/test/Bridges/General/test_bridge_optimizer.jl b/test/Bridges/General/test_bridge_optimizer.jl index 351c69fe88..b701e97056 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)))` " * @@ -635,7 +645,10 @@ function test_nesting_SingleBridgeOptimizer() @test !MOI.Bridges.is_bridged(b1, x) @test MOI.is_valid(b0, x) @test MOI.Bridges.is_bridged(b0, x) - @test !MOI.is_valid(model, x) + # `x` is a bridged variable of `b0`; its positive outer index lives in a + # namespace independent of `model`'s and may coincide with a real `model` + # variable, so a raw `is_valid(model, x)` is no longer a meaningful + # isolation check (operations are isolated by routing through the layers). clt = MOI.add_constraint(b, x, MOI.LessThan(one(T))) @test MOI.is_valid(b, clt) @test !MOI.Bridges.is_bridged(b, clt) @@ -651,9 +664,9 @@ function test_nesting_SingleBridgeOptimizer() @test !MOI.Bridges.is_bridged(b, cnn) @test MOI.is_valid(b2, cnn) @test MOI.Bridges.is_bridged(b2, cnn) - @test !MOI.is_valid(b1, cnn) - @test !MOI.is_valid(b0, cnn) - @test !MOI.is_valid(model, cnn) + # `cnn` is bridged at `b2`; its positive outer index may coincide with a + # real constraint index of a lower layer, so raw cross-layer `is_valid` + # checks are not meaningful isolation checks anymore (see the note above). obj = x attr = MOI.ObjectiveFunction{typeof(obj)}() MOI.set(b, attr, obj) @@ -1592,6 +1605,106 @@ function test_issue_2762_fail() return end +function _zeros_bridge_optimizer() + return MOI.Bridges.Variable.Zeros{Float64}(MOI.Utilities.Model{Float64}()) +end + +function test_variable_mapping_vector_of_variables_constraint() + model = _zeros_bridge_optimizer() + x, cx = MOI.add_constrained_variables(model, MOI.Zeros(2)) + y = MOI.add_variables(model, 2) + c = MOI.add_constraint(model, MOI.VectorOfVariables(y), MOI.Zeros(2)) + @test c != cx + @test MOI.get(model, MOI.ConstraintFunction(), c) == + MOI.VectorOfVariables(y) + MOI.set(model, MOI.ConstraintName(), c, "c") + @test MOI.get(model, MOI.ConstraintIndex, "c") == c + @test MOI.get(model, typeof(c), "c") == c + MOI.delete(model, c) + @test !MOI.is_valid(model, c) + @test_throws MOI.InvalidIndex(c) MOI.delete(model, c) + @test MOI.is_valid(model, cx) + return +end + +function test_variable_mapping_total_inner_to_outer() + model = _zeros_bridge_optimizer() + x, _ = MOI.add_constrained_variables(model, MOI.Zeros(2)) + y = MOI.add_variable(model) + c = MOI.add_constraint(model, MOI.VectorOfVariables([y]), MOI.Zeros(1)) + f = MOI.Bridges._TotalInnerToOuter(model) + @test f(MOI.Bridges.outer_to_inner(model, c)) == c + inner_y = MOI.Bridges.outer_to_inner(model, y) + @test inner_y != y + CI = MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}} + @test f(CI(inner_y.value)) == CI(y.value) + return +end + +function test_variable_mapping_add_constraints() + model = _zeros_bridge_optimizer() + x, _ = MOI.add_constrained_variables(model, MOI.Zeros(1)) + y = MOI.add_variables(model, 2) + sets = [MOI.GreaterThan(0.0), MOI.GreaterThan(1.0)] + c = MOI.add_constraints(model, y, sets) + @test MOI.get.(model, MOI.ConstraintFunction(), c) == y + @test MOI.get.(model, MOI.ConstraintSet(), c) == sets + funcs = [MOI.VectorOfVariables([yi]) for yi in y] + d = MOI.add_constraints(model, funcs, [MOI.Nonnegatives(1) for _ in y]) + @test MOI.get.(model, MOI.ConstraintFunction(), d) == funcs + return +end + +function test_variable_mapping_quadratic_coefficient_change() + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Variable.Zeros{Float64}(inner) + x, _ = MOI.add_constrained_variables(model, MOI.Zeros(1)) + y = MOI.add_variable(model) + inner_y = MOI.Bridges.outer_to_inner(model, y) + @test inner_y != y + function f(coef, v) + return MOI.ScalarQuadraticFunction( + [MOI.ScalarQuadraticTerm(coef, v, v)], + [MOI.ScalarAffineTerm(1.0, v)], + 0.0, + ) + end + change = MOI.ScalarQuadraticCoefficientChange(y, y, 3.0) + c = MOI.add_constraint(model, f(1.0, y), MOI.LessThan(1.0)) + MOI.modify(model, c, change) + inner_c = MOI.Bridges.outer_to_inner(model, c) + @test MOI.get(inner, MOI.ConstraintFunction(), inner_c) ≈ f(3.0, inner_y) + attr = MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}() + MOI.set(model, attr, f(1.0, y)) + MOI.modify(model, attr, change) + @test MOI.get(inner, attr) ≈ f(3.0, inner_y) + return +end + +function test_variable_mapping_name_of_bridged_variable() + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Variable.ParameterToEqualTo{Float64}(inner) + p, _ = MOI.add_constrained_variable(model, MOI.Parameter(1.0)) + y = MOI.add_variable(model) + MOI.set(model, MOI.VariableName(), p, "p") + MOI.set(model, MOI.VariableName(), y, "y") + @test MOI.get(model, MOI.VariableIndex, "p") == p + @test MOI.get(model, MOI.VariableIndex, "y") == y + return +end + +function test_variable_mapping_modify_objective_bridged_variable() + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Variable.NonposToNonneg{Float64}(inner) + x, _ = MOI.add_constrained_variables(model, MOI.Nonpositives(1)) + y = MOI.add_variable(model) + attr = MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}() + MOI.set(model, attr, 1.0 * x[1] + 1.0 * y) + MOI.modify(model, attr, MOI.ScalarCoefficientChange(x[1], 2.0)) + @test MOI.get(model, attr) ≈ 2.0 * x[1] + 1.0 * y + return +end + end # module TestBridgeOptimizer.runtests() diff --git a/test/Bridges/General/test_debug.jl b/test/Bridges/General/test_debug.jl index 8f5bcee3de..95ae4cad6e 100644 --- a/test/Bridges/General/test_debug.jl +++ b/test/Bridges/General/test_debug.jl @@ -373,6 +373,11 @@ function MOI.get( return ret end +function MOI.get(model::ModelPOIIssue201, ::MOI.ListOfVariableIndices) + n = length(model.parameter) + length(model.equal_to) + return MOI.VariableIndex.(1:n) +end + function MOI.get( model::ModelPOIIssue201{T}, ::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.Parameter{T}}, @@ -392,7 +397,7 @@ function MOI.add_constrained_variable( set::MOI.Parameter{T}, ) where {T} push!(model.parameter, set.value) - x = MOI.VariableIndex(length(model.parameter)) + x = MOI.VariableIndex(length(model.parameter) + length(model.equal_to)) return x, MOI.ConstraintIndex{MOI.VariableIndex,typeof(set)}(x.value) end @@ -401,7 +406,7 @@ function MOI.add_constrained_variable( set::MOI.EqualTo{T}, ) where {T} push!(model.equal_to, set.value) - x = MOI.VariableIndex(length(model.equal_to)) + x = MOI.VariableIndex(length(model.parameter) + length(model.equal_to)) return x, MOI.ConstraintIndex{MOI.VariableIndex,typeof(set)}(x.value) end diff --git a/test/Bridges/General/test_lazy_bridge_optimizer.jl b/test/Bridges/General/test_lazy_bridge_optimizer.jl index 65c5ad9b37..1662cad32b 100644 --- a/test/Bridges/General/test_lazy_bridge_optimizer.jl +++ b/test/Bridges/General/test_lazy_bridge_optimizer.jl @@ -1255,7 +1255,7 @@ function test_StandardSDPAModel_with_bridges_and_caching() MOI.Utilities.reset_optimizer(cached, bridged) MOI.Utilities.attach_optimizer(cached) vi_bridged = first(MOI.get(bridged, MOI.ListOfVariableIndices())) - @test vi_bridged == MOI.VariableIndex(-1) + @test vi_bridged == MOI.VariableIndex(1) @test cached.model_to_optimizer_map[vi_cache] == vi_bridged @test cached.optimizer_to_model_map[vi_bridged] == vi_cache vis_sdpa = MOI.get(model, MOI.ListOfVariableIndices()) @@ -1501,20 +1501,31 @@ function _test_context_substitution(T) @test MOI.Bridges.call_in_context(bridged, x, bridge -> f(z)) ≈ f(z) @test MOI.Bridges.call_in_context(bridged, y, bridge -> f(z)) === nothing attr = InvariantUnderFunctionConversionAttribute() - for func in [T(2) * x, T(2) * y, T(2) * z] + # `func` is set directly on the inner `model`, so its variables live in the + # inner namespace. Getting it back through `bridged` translates them to the + # outer namespace (the inner variable becomes the outer `z`; values that are + # not variables of the inner model are left as-is), while `call_in_context` + # suppresses the variable-bridge substitution. Under the former negative + # index encoding the outer and inner namespaces never shared a value, so + # this translation was not observable; with positive indices the inner + # variable's value can coincide with an outer one, hence the explicit + # `(func, expected)` pairs below. + inner_z = only(MOI.get(model, MOI.ListOfVariableIndices())) + for (func, expected) in + [(T(2) * inner_z, T(2) * z), (T(2) * y, T(2) * y), (T(2) * z, T(2) * z)] MOI.set(model, attr, aff_ci, func) @test MOI.get(model, attr, aff_ci) ≈ func @test MOI.Bridges.call_in_context( bridged, vov_ci, bridge -> MOI.get(bridged, attr, vov_ci), - ) ≈ func + ) ≈ expected @test MOI.Bridges.call_in_context( bridged, y, bridge -> MOI.get(bridged, attr, vov_ci), - ) ≈ func - #@test MOI.Bridges.call_in_context(bridged, z, bridge -> MOI.get(bridged, attr, vov_ci)) ≈ func + ) ≈ expected + #@test MOI.Bridges.call_in_context(bridged, z, bridge -> MOI.get(bridged, attr, vov_ci)) ≈ expected end # One Variable bridge in context model = NoVariableModel{T}() @@ -2363,7 +2374,7 @@ function test_issue_2696() F, S = MOI.VectorOfVariables, MOI.Nonpositives # See MathOptInterface.jl#2696 d = MOI.ConstraintIndex{F,S}(c.value + 1) - @test_broken !MOI.is_valid(b, d) + @test !MOI.is_valid(b, d) @test MOI.get(b, MOI.ListOfConstraintTypesPresent()) == [(F, S)] @test only(MOI.get(b, MOI.ListOfConstraintIndices{F,S}())) == c @test MOI.get(b, MOI.NumberOfConstraints{F,S}()) == 1 diff --git a/test/Bridges/Variable/test_FreeBridge.jl b/test/Bridges/Variable/test_FreeBridge.jl index 71fbf2a8d7..0a8fa90e47 100644 --- a/test/Bridges/Variable/test_FreeBridge.jl +++ b/test/Bridges/Variable/test_FreeBridge.jl @@ -135,7 +135,7 @@ function test_linear_LessThan_and_GreaterThan() @test MOI.get(mock, MOI.NumberOfVariables()) == 4 @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 2 vis = MOI.get(bridged_mock, MOI.ListOfVariableIndices()) - @test vis == MOI.VariableIndex.([-1, -2]) + @test vis == MOI.VariableIndex.([1, 2]) cx = MOI.ConstraintIndex{MOI.VectorOfVariables,MOI.Reals}(vis[1].value) @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), cx) == [100.0] @@ -232,7 +232,7 @@ function test_linear_transform() MOI.Test.test_linear_transform(bridged_mock, MOI.Test.Config()) vis = MOI.get(bridged_mock, MOI.ListOfVariableIndices()) - @test vis == MOI.VariableIndex.([-1, -2]) + @test vis == MOI.VariableIndex.([1, 2]) MOI.set( bridged_mock, diff --git a/test/Bridges/Variable/test_NonposToNonnegBridge.jl b/test/Bridges/Variable/test_NonposToNonnegBridge.jl index ca69c192a5..aa74738424 100644 --- a/test/Bridges/Variable/test_NonposToNonnegBridge.jl +++ b/test/Bridges/Variable/test_NonposToNonnegBridge.jl @@ -44,7 +44,10 @@ function test_NonposToNonneg() # x in R^1, y in R_-^1, z in R^1, s in R^1 vis = MOI.get(bridged_mock, MOI.ListOfVariableIndices()) y = vis[2] - @test y.value == -1 + # `y` is the bridged `Nonpositives` variable. Its outer index lives in a + # namespace independent of the inner model, so we check that it is bridged + # rather than asserting a specific value. + @test MOI.Bridges.is_bridged(bridged_mock, y) @test MOI.supports( bridged_mock, @@ -155,7 +158,7 @@ function test_conic_linear_INFEASIBLE_2() @test first(MOI.get(mock, MOI.ListOfVariableIndices())).value ≥ 0 @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 1 vis = MOI.get(bridged_mock, MOI.ListOfVariableIndices()) - @test vis == [MOI.VariableIndex(-1)] + @test vis == [MOI.VariableIndex(1)] _test_delete_bridged_variable( bridged_mock, vis[1], 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 diff --git a/test/Bridges/Variable/test_VectorizeBridge.jl b/test/Bridges/Variable/test_VectorizeBridge.jl index 33936de912..d35883e77f 100644 --- a/test/Bridges/Variable/test_VectorizeBridge.jl +++ b/test/Bridges/Variable/test_VectorizeBridge.jl @@ -107,7 +107,10 @@ function test_exp3_with_add_constrained_variable_y() @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 1 xc = MOI.add_constraint(bridged_mock, 2.0x, MOI.LessThan(4.0)) y, yc = MOI.add_constrained_variable(bridged_mock, MOI.LessThan(5.0)) - @test yc.value == y.value == -1 + # `y` is a bridged variable: its (positive) outer index is allocated + # independently of the inner model, and the `VariableIndex` constraint + # `yc` shares its value. + @test yc.value == y.value @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 2 @test length(MOI.get(bridged_mock, MOI.ListOfVariableIndices())) == 2 @test Set(MOI.get(bridged_mock, MOI.ListOfVariableIndices())) == Set([x, y]) @@ -130,7 +133,7 @@ function test_exp3_with_add_constrained_variable_y() err = MOI.AddConstraintNotAllowed{MOI.VariableIndex,MOI.LessThan{Float64}}( "Cannot add two `VariableIndex`-in-`MathOptInterface.LessThan{Float64}`" * - " on the same variable MOI.VariableIndex(-1).", + " on the same variable $(y).", ) @test_throws err MOI.add_constraint(bridged_mock, y, MOI.LessThan(4.0)) @@ -170,7 +173,7 @@ function test_exp3_with_add_constrained_variable_y() change = MOI.ScalarCoefficientChange(y, 0.0) attr = MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}() message = - "The change MathOptInterface.ScalarCoefficientChange{Float64}(MOI.VariableIndex(-1), 0.0)" * + "The change $(change)" * " contains variables bridged into a function with nonzero constant." err = MOI.ModifyObjectiveNotAllowed(change, message) @test_throws err MOI.modify(bridged_mock, attr, change) diff --git a/test/Bridges/Variable/test_map.jl b/test/Bridges/Variable/test_map.jl index ca28f10642..381d0a9c76 100644 --- a/test/Bridges/Variable/test_map.jl +++ b/test/Bridges/Variable/test_map.jl @@ -59,7 +59,7 @@ function test_map() "Cannot unbridge function because some variables are bridged by variable" * " bridges that do not support reverse mapping, for example, `ZerosBridge`.", ) - @test v1.value == c1.value == -1 + @test v1.value == c1.value == 1 @test MOI.Bridges.Variable.constraint(map, v1) == c1 @test MOI.Bridges.Variable.length_of_vector_of_variables(map, v1) == 0 @test haskey(map, v1) @@ -92,8 +92,8 @@ function test_map() ci -> true, ) MOI.is_valid(map, c2) - @test v2[1].value == -2 - @test c2.value == -1 + @test v2[1].value == 2 + @test c2.value == 1 @test MOI.Bridges.Variable.has_keys(map, v2) @test !MOI.Bridges.Variable.has_keys(map, v2[4:-1:1]) for i in 1:4