Skip to content

Commit 09a0659

Browse files
blegatodow
authored andcommitted
[Bridges] fix use of BridgeCost when computing supports in LazyBridgeOptimizer
1 parent f0e9fef commit 09a0659

3 files changed

Lines changed: 272 additions & 4 deletions

File tree

src/Bridges/lazy_bridge_optimizer.jl

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,15 +255,22 @@ function node(
255255
@nospecialize(b::LazyBridgeOptimizer),
256256
@nospecialize(S::Type{<:MOI.AbstractSet}),
257257
)
258-
# If we support the set, the node is 0.
258+
# If we support the set, the node is 0 unless the inner model reports a
259+
# non-zero `VariableBridgingCost` (which can happen when the inner model is
260+
# itself a bridge optimizer that needs to bridge `S`).
259261
if (
260262
S <: MOI.AbstractScalarSet &&
261263
MOI.supports_add_constrained_variable(b.model, S)
262264
) || (
263265
S <: MOI.AbstractVectorSet &&
264266
MOI.supports_add_constrained_variables(b.model, S)
265267
)
266-
return VariableNode(0)
268+
inner_cost = MOI.get(b.model, MOI.VariableBridgingCost{S}())::Float64
269+
if iszero(inner_cost)
270+
return VariableNode(0)
271+
end
272+
else
273+
inner_cost = nothing
267274
end
268275
# If (S,) is stored in .variable_node, we've already added the node
269276
# previously.
@@ -275,6 +282,13 @@ function node(
275282
variable_node = add_node(b.graph, VariableNode)
276283
b.variable_node[(S,)] = variable_node
277284
push!(b.variable_types, (S,))
285+
if !isnothing(inner_cost)
286+
# The inner model supports `S` but with a non-zero bridging cost.
287+
# Create a leaf node whose distance is `inner_cost` so that bridges
288+
# that emit constrained variables in `S` account for it.
289+
b.graph.variable_dist[variable_node.index] = inner_cost
290+
return variable_node
291+
end
278292
F = MOI.Utilities.variable_function_type(S)
279293
if is_bridged(b, MOI.Reals)
280294
# The solver doesn't support adding free variables.
@@ -315,9 +329,17 @@ function node(
315329
@nospecialize(F::Type{<:MOI.AbstractFunction}),
316330
@nospecialize(S::Type{<:MOI.AbstractSet}),
317331
)
318-
# If we support the constraint type, the node is 0.
332+
# If we support the constraint type, the node is 0 unless the inner model
333+
# reports a non-zero `ConstraintBridgingCost` (which can happen when the
334+
# inner model is itself a bridge optimizer that needs to bridge `F`-in-`S`).
319335
if MOI.supports_constraint(b.model, F, S)
320-
return ConstraintNode(0)
336+
inner_cost =
337+
MOI.get(b.model, MOI.ConstraintBridgingCost{F,S}())::Float64
338+
if iszero(inner_cost)
339+
return ConstraintNode(0)
340+
end
341+
else
342+
inner_cost = nothing
321343
end
322344
# If (F, S) is stored in .constraint_node, we've already added the node
323345
# previously.
@@ -329,6 +351,13 @@ function node(
329351
constraint_node = add_node(b.graph, ConstraintNode)
330352
b.constraint_node[(F, S)] = constraint_node
331353
push!(b.constraint_types, (F, S))
354+
if !isnothing(inner_cost)
355+
# The inner model supports `F`-in-`S` but with a non-zero bridging cost.
356+
# Create a leaf node whose distance is `inner_cost` so that bridges
357+
# that emit `F`-in-`S` constraints account for it.
358+
b.graph.constraint_dist[constraint_node.index] = inner_cost
359+
return constraint_node
360+
end
332361
for (i, BT) in enumerate(b.constraint_bridge_types)
333362
if MOI.supports_constraint(BT, F, S)
334363
edge = _edge(b, i, Constraint.concrete_bridge_type(BT, F, S))::Edge

test/Bridges/General/test_lazy_bridge_optimizer.jl

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2477,6 +2477,232 @@ function test_issue_2870_relative_entropy()
24772477
return
24782478
end
24792479

2480+
MOI.Utilities.@model(
2481+
NonnegOnlyModel,
2482+
(),
2483+
(),
2484+
(MOI.Nonnegatives,),
2485+
(),
2486+
(),
2487+
(),
2488+
(MOI.VectorOfVariables,),
2489+
(MOI.VectorAffineFunction,)
2490+
)
2491+
2492+
function test_nested_lazy_bridge_optimizer_cost()
2493+
# When the inner model is itself a `LazyBridgeOptimizer` that needs to
2494+
# bridge a set, the outer `LazyBridgeOptimizer` must take the inner
2495+
# bridging cost into account when computing edge costs in its own graph,
2496+
# not assume zero cost just because the inner reports `supports`.
2497+
T = Float64
2498+
# Solver supporting only `Nonnegatives`-constrained variables and
2499+
# `VAF`-in-`Nonnegatives` constraints. `Nonpositives` is bridged via
2500+
# `NonposToNonneg` in both forms, so the inner reports `supports` for
2501+
# `Nonpositives` but with a `1.0` bridging cost.
2502+
inner = MOI.Bridges.LazyBridgeOptimizer(NonnegOnlyModel{T}())
2503+
MOI.Bridges.add_bridge(inner, MOI.Bridges.Variable.NonposToNonnegBridge{T})
2504+
MOI.Bridges.add_bridge(
2505+
inner,
2506+
MOI.Bridges.Constraint.NonposToNonnegBridge{T},
2507+
)
2508+
@test MOI.get(inner, MOI.VariableBridgingCost{MOI.Nonnegatives}()) == 0.0
2509+
@test MOI.get(inner, MOI.VariableBridgingCost{MOI.Nonpositives}()) == 1.0
2510+
@test MOI.get(
2511+
inner,
2512+
MOI.ConstraintBridgingCost{
2513+
MOI.VectorAffineFunction{T},
2514+
MOI.Nonpositives,
2515+
}(),
2516+
) == 1.0
2517+
cache = MOI.Utilities.CachingOptimizer(
2518+
MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()),
2519+
inner,
2520+
)
2521+
@test MOI.get(cache, MOI.VariableBridgingCost{MOI.Nonpositives}()) == 1.0
2522+
@test MOI.get(
2523+
cache,
2524+
MOI.ConstraintBridgingCost{
2525+
MOI.VectorAffineFunction{T},
2526+
MOI.Nonpositives,
2527+
}(),
2528+
) == 1.0
2529+
outer = MOI.Bridges.LazyBridgeOptimizer(cache)
2530+
@test MOI.get(outer, MOI.VariableBridgingCost{MOI.Nonpositives}()) == 1.0
2531+
@test MOI.Bridges.bridging_cost(
2532+
outer.graph,
2533+
MOI.Bridges.node(outer, MOI.Nonpositives),
2534+
) == 1.0
2535+
# Add a constraint bridge in `outer` whose target is
2536+
# `VAF-in-Nonpositives`. The bridge's edge cost in `outer.graph` must
2537+
# reflect the inner cost (1.0) of `VAF-in-Nonpositives`, so bridging
2538+
# `SAF-in-LessThan{T}` costs 1.0 (bridge) + 1.0 (inner) = 2.0. Without
2539+
# the fix it would be wrongly reported as 1.0.
2540+
MOI.Bridges.add_bridge(outer, MOI.Bridges.Constraint.VectorizeBridge{T})
2541+
@test MOI.get(
2542+
outer,
2543+
MOI.ConstraintBridgingCost{
2544+
MOI.ScalarAffineFunction{T},
2545+
MOI.LessThan{T},
2546+
}(),
2547+
) == 2.0
2548+
@test MOI.Bridges.bridging_cost(
2549+
outer.graph,
2550+
MOI.Bridges.node(outer, MOI.ScalarAffineFunction{T}, MOI.LessThan{T}),
2551+
) == 2.0
2552+
@test MOI.Bridges.is_bridged(
2553+
outer,
2554+
MOI.ScalarAffineFunction{T},
2555+
MOI.LessThan{T},
2556+
)
2557+
# Sanity check: with `MOI.Utilities.Model` as inner (which natively
2558+
# supports `SAF-in-LessThan{T}` so the inner bridging cost is `0`), the
2559+
# choice differs: `outer_native` does not need to bridge
2560+
# `SAF-in-LessThan{T}` and the cost is `0.0`, while `outer` above must
2561+
# use `Constraint.VectorizeBridge` and pays `2.0`.
2562+
outer_native = MOI.Bridges.LazyBridgeOptimizer(MOI.Utilities.Model{T}())
2563+
MOI.Bridges.add_bridge(
2564+
outer_native,
2565+
MOI.Bridges.Constraint.VectorizeBridge{T},
2566+
)
2567+
@test MOI.get(
2568+
outer_native,
2569+
MOI.ConstraintBridgingCost{
2570+
MOI.ScalarAffineFunction{T},
2571+
MOI.LessThan{T},
2572+
}(),
2573+
) == 0.0
2574+
@test !MOI.Bridges.is_bridged(
2575+
outer_native,
2576+
MOI.ScalarAffineFunction{T},
2577+
MOI.LessThan{T},
2578+
)
2579+
return
2580+
end
2581+
2582+
# A minimal model whose only purpose is to report custom non-zero values for
2583+
# `VariableBridgingCost` and `ConstraintBridgingCost`. Its constructor takes
2584+
# dictionaries that map set types (resp. `(F, S)` tuples) to a `Float64` cost,
2585+
# so tests can vary the inner costs and observe how the bridge selection in an
2586+
# outer `LazyBridgeOptimizer` changes.
2587+
mutable struct CostModel{T} <: MOI.ModelLike
2588+
var_costs::Dict{Type,Float64}
2589+
con_costs::Dict{Tuple{Type,Type},Float64}
2590+
end
2591+
2592+
function CostModel{T}(;
2593+
var_costs::Dict{Type,Float64} = Dict{Type,Float64}(),
2594+
con_costs::Dict{Tuple{Type,Type},Float64} = Dict{Tuple{Type,Type},Float64}(),
2595+
) where {T}
2596+
return CostModel{T}(var_costs, con_costs)
2597+
end
2598+
2599+
function MOI.supports_add_constrained_variable(
2600+
model::CostModel,
2601+
::Type{S},
2602+
) where {S<:MOI.AbstractScalarSet}
2603+
return haskey(model.var_costs, S)
2604+
end
2605+
2606+
function MOI.supports_add_constrained_variables(
2607+
model::CostModel,
2608+
::Type{S},
2609+
) where {S<:MOI.AbstractVectorSet}
2610+
return haskey(model.var_costs, S)
2611+
end
2612+
2613+
function MOI.supports_add_constrained_variables(
2614+
model::CostModel,
2615+
::Type{MOI.Reals},
2616+
)
2617+
return haskey(model.var_costs, MOI.Reals)
2618+
end
2619+
2620+
function MOI.supports_constraint(
2621+
model::CostModel,
2622+
::Type{F},
2623+
::Type{S},
2624+
) where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet}
2625+
return haskey(model.con_costs, (F, S))
2626+
end
2627+
2628+
function MOI.get(
2629+
model::CostModel,
2630+
::MOI.VariableBridgingCost{S},
2631+
) where {S<:MOI.AbstractSet}
2632+
return get(model.var_costs, S, Inf)
2633+
end
2634+
2635+
function MOI.get(
2636+
model::CostModel,
2637+
::MOI.ConstraintBridgingCost{F,S},
2638+
) where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet}
2639+
return get(model.con_costs, (F, S), Inf)
2640+
end
2641+
2642+
function test_custom_cost_model_bridge_selection()
2643+
# Outer wraps a `CostModel` that supports `VAF-in-RSOC` and `VAF-in-PSD`
2644+
# but NOT `VAF-in-SOC`. With both `SOCtoRSOCBridge` (cost 1) and
2645+
# `SOCtoPSDBridge` (cost 10) in `outer`, the choice for `VAF-in-SOC`
2646+
# depends on the inner costs of `RSOC` and `PSD`: when both inner costs
2647+
# are 0, `SOCtoRSOC` wins (1 < 10); when the inner cost of `RSOC` is
2648+
# high enough, `SOCtoPSD` becomes cheaper.
2649+
T = Float64
2650+
F = MOI.VectorAffineFunction{T}
2651+
# Case 1: both inner costs are 0. `SOCtoRSOC` should be selected.
2652+
model1 = CostModel{T}(;
2653+
con_costs = Dict{Tuple{Type,Type},Float64}(
2654+
(F, MOI.RotatedSecondOrderCone) => 0.0,
2655+
(F, MOI.PositiveSemidefiniteConeTriangle) => 0.0,
2656+
),
2657+
)
2658+
outer1 = MOI.Bridges.LazyBridgeOptimizer(model1)
2659+
MOI.Bridges.add_bridge(outer1, MOI.Bridges.Constraint.SOCtoRSOCBridge{T})
2660+
MOI.Bridges.add_bridge(outer1, MOI.Bridges.Constraint.SOCtoPSDBridge{T})
2661+
@test MOI.get(
2662+
outer1,
2663+
MOI.ConstraintBridgingCost{F,MOI.SecondOrderCone}(),
2664+
) == 1.0
2665+
@test MOI.Bridges.bridge_type(outer1, F, MOI.SecondOrderCone) <:
2666+
MOI.Bridges.Constraint.SOCtoRSOCBridge{T}
2667+
# Case 2: inner cost of `RSOC` is 15. The path via `SOCtoRSOC` becomes
2668+
# 1 + 15 = 16, which is more expensive than `SOCtoPSD` at 10 + 0 = 10,
2669+
# so the bridge selection flips to `SOCtoPSD`.
2670+
model2 = CostModel{T}(;
2671+
con_costs = Dict{Tuple{Type,Type},Float64}(
2672+
(F, MOI.RotatedSecondOrderCone) => 15.0,
2673+
(F, MOI.PositiveSemidefiniteConeTriangle) => 0.0,
2674+
),
2675+
)
2676+
outer2 = MOI.Bridges.LazyBridgeOptimizer(model2)
2677+
MOI.Bridges.add_bridge(outer2, MOI.Bridges.Constraint.SOCtoRSOCBridge{T})
2678+
MOI.Bridges.add_bridge(outer2, MOI.Bridges.Constraint.SOCtoPSDBridge{T})
2679+
@test MOI.get(
2680+
outer2,
2681+
MOI.ConstraintBridgingCost{F,MOI.SecondOrderCone}(),
2682+
) == 10.0
2683+
@test MOI.Bridges.bridge_type(outer2, F, MOI.SecondOrderCone) <:
2684+
MOI.Bridges.Constraint.SOCtoPSDBridge{T}
2685+
# Case 3: same bridges, but now `PSD` is also expensive (cost 12). The
2686+
# path via `SOCtoRSOC` is 1 + 5 = 6 and the path via `SOCtoPSD` is
2687+
# 10 + 12 = 22, so `SOCtoRSOC` wins again.
2688+
model3 = CostModel{T}(;
2689+
con_costs = Dict{Tuple{Type,Type},Float64}(
2690+
(F, MOI.RotatedSecondOrderCone) => 5.0,
2691+
(F, MOI.PositiveSemidefiniteConeTriangle) => 12.0,
2692+
),
2693+
)
2694+
outer3 = MOI.Bridges.LazyBridgeOptimizer(model3)
2695+
MOI.Bridges.add_bridge(outer3, MOI.Bridges.Constraint.SOCtoRSOCBridge{T})
2696+
MOI.Bridges.add_bridge(outer3, MOI.Bridges.Constraint.SOCtoPSDBridge{T})
2697+
@test MOI.get(
2698+
outer3,
2699+
MOI.ConstraintBridgingCost{F,MOI.SecondOrderCone}(),
2700+
) == 6.0
2701+
@test MOI.Bridges.bridge_type(outer3, F, MOI.SecondOrderCone) <:
2702+
MOI.Bridges.Constraint.SOCtoRSOCBridge{T}
2703+
return
2704+
end
2705+
24802706
end # module
24812707

24822708
TestBridgesLazyBridgeOptimizer.runtests()

test/FileFormats/NL/test_NL.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,19 @@ function test_write_complements_VectorNonlinearFunction()
15321532
return
15331533
end
15341534

1535+
function test_VariableBridgingCost()
1536+
model = NL.Model()
1537+
S = MOI.LessThan{Float64}
1538+
attr = MOI.VariableBridgingCost{S}()
1539+
@test MOI.get(model, attr) == 0
1540+
attr = MOI.ConstraintBridgingCost{MOI.VariableIndex,S}()
1541+
@test MOI.get(model, attr) == 0
1542+
S = MOI.SecondOrderCone
1543+
attr = MOI.VariableBridgingCost{S}()
1544+
@test isinf(MOI.get(model, attr))
1545+
return
1546+
end
1547+
15351548
function test_unsupported_kwarg()
15361549
@test_throws(
15371550
ErrorException(

0 commit comments

Comments
 (0)