diff --git a/src/Bridges/lazy_bridge_optimizer.jl b/src/Bridges/lazy_bridge_optimizer.jl index 37255c56b9..0b25e67987 100644 --- a/src/Bridges/lazy_bridge_optimizer.jl +++ b/src/Bridges/lazy_bridge_optimizer.jl @@ -255,7 +255,9 @@ function node( @nospecialize(b::LazyBridgeOptimizer), @nospecialize(S::Type{<:MOI.AbstractSet}), ) - # If we support the set, the node is 0. + # If we support the set, the node is 0 unless the inner model reports a + # non-zero `VariableBridgingCost` (which can happen when the inner model is + # itself a bridge optimizer that needs to bridge `S`). if ( S <: MOI.AbstractScalarSet && MOI.supports_add_constrained_variable(b.model, S) @@ -263,7 +265,22 @@ function node( S <: MOI.AbstractVectorSet && MOI.supports_add_constrained_variables(b.model, S) ) - return VariableNode(0) + inner_cost = MOI.get(b.model, MOI.VariableBridgingCost{S}())::Float64 + if iszero(inner_cost) + return VariableNode(0) + end + # The inner model supports `S` but with a non-zero bridging cost. + # Create a leaf node whose distance is `inner_cost` so that bridges + # that emit constrained variables in `S` account for it. + cached = get(b.variable_node, (S,), nothing) + if cached !== nothing + return cached + end + new_node = add_node(b.graph, VariableNode) + b.variable_node[(S,)] = new_node + push!(b.variable_types, (S,)) + b.graph.variable_dist[new_node.index] = inner_cost + return new_node end # If (S,) is stored in .variable_node, we've already added the node # previously. @@ -315,9 +332,27 @@ function node( @nospecialize(F::Type{<:MOI.AbstractFunction}), @nospecialize(S::Type{<:MOI.AbstractSet}), ) - # If we support the constraint type, the node is 0. + # If we support the constraint type, the node is 0 unless the inner model + # reports a non-zero `ConstraintBridgingCost` (which can happen when the + # inner model is itself a bridge optimizer that needs to bridge `F`-in-`S`). if MOI.supports_constraint(b.model, F, S) - return ConstraintNode(0) + inner_cost = + MOI.get(b.model, MOI.ConstraintBridgingCost{F,S}())::Float64 + if iszero(inner_cost) + return ConstraintNode(0) + end + # The inner model supports `F`-in-`S` but with a non-zero bridging cost. + # Create a leaf node whose distance is `inner_cost` so that bridges + # that emit `F`-in-`S` constraints account for it. + cached = get(b.constraint_node, (F, S), nothing) + if cached !== nothing + return cached + end + new_node = add_node(b.graph, ConstraintNode) + b.constraint_node[(F, S)] = new_node + push!(b.constraint_types, (F, S)) + b.graph.constraint_dist[new_node.index] = inner_cost + return new_node end # If (F, S) is stored in .constraint_node, we've already added the node # previously. diff --git a/src/Utilities/cachingoptimizer.jl b/src/Utilities/cachingoptimizer.jl index f3392bbf07..0df5e5863f 100644 --- a/src/Utilities/cachingoptimizer.jl +++ b/src/Utilities/cachingoptimizer.jl @@ -897,6 +897,16 @@ function MOI.get(model::CachingOptimizer, attr::MOI.AbstractModelAttribute) return _get_model_attribute(model, attr) end +function MOI.get( + model::CachingOptimizer, + attr::Union{MOI.VariableBridgingCost,MOI.ConstraintBridgingCost}, +)::Float64 + if state(model) == NO_OPTIMIZER + return MOI.get(model.model_cache, attr) + end + return MOI.get(model.optimizer, attr) +end + function MOI.get( model::CachingOptimizer, attr::MOI.TerminationStatus, diff --git a/test/Bridges/General/test_lazy_bridge_optimizer.jl b/test/Bridges/General/test_lazy_bridge_optimizer.jl index 2d28d77a45..a2b0d90a21 100644 --- a/test/Bridges/General/test_lazy_bridge_optimizer.jl +++ b/test/Bridges/General/test_lazy_bridge_optimizer.jl @@ -2477,6 +2477,46 @@ function test_issue_2870_relative_entropy() return end +MOI.Utilities.@model( + NonnegOnlyModel, + (), + (), + (MOI.Nonnegatives,), + (), + (), + (), + (MOI.VectorOfVariables,), + (MOI.VectorAffineFunction,) +) + +function test_nested_lazy_bridge_optimizer_cost() + # When the inner model is itself a `LazyBridgeOptimizer` that needs to + # bridge a set, the outer `LazyBridgeOptimizer` must take the inner + # bridging cost into account when computing edge costs in its own graph, + # not assume zero cost just because the inner reports `supports`. + T = Float64 + # Solver supporting only `Nonnegatives`-constrained variables. + inner = MOI.Bridges.LazyBridgeOptimizer(NonnegOnlyModel{T}()) + MOI.Bridges.add_bridge( + inner, + MOI.Bridges.Variable.NonposToNonnegBridge{T}, + ) + @test MOI.get(inner, MOI.VariableBridgingCost{MOI.Nonnegatives}()) == 0.0 + @test MOI.get(inner, MOI.VariableBridgingCost{MOI.Nonpositives}()) == 1.0 + cache = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()), + inner, + ) + @test MOI.get(cache, MOI.VariableBridgingCost{MOI.Nonpositives}()) == 1.0 + outer = MOI.Bridges.LazyBridgeOptimizer(cache) + @test MOI.get(outer, MOI.VariableBridgingCost{MOI.Nonpositives}()) == 1.0 + @test MOI.Bridges.bridging_cost( + outer.graph, + MOI.Bridges.node(outer, MOI.Nonpositives), + ) == 1.0 + return +end + end # module TestBridgesLazyBridgeOptimizer.runtests()