@@ -2477,6 +2477,232 @@ function test_issue_2870_relative_entropy()
24772477 return
24782478end
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+
24802706end # module
24812707
24822708TestBridgesLazyBridgeOptimizer. runtests ()
0 commit comments