@@ -286,14 +286,13 @@ function test_basic_step_errors()
286286 other = disjunction (m4, C)
287287 @test_throws ErrorException apply_basic_step (m4, [inner, other])
288288 @test_throws ErrorException apply_basic_step (m4, [outer, other])
289- # indicators shared between inputs or with an outside disjunction
289+ # input indicators used by an outside disjunction
290290 m5 = GDPModel ()
291291 @variable (m5, P[1 : 2 ], Logical)
292292 @variable (m5, Q[1 : 2 ], Logical)
293293 d5 = disjunction (m5, P)
294294 d6 = disjunction (m5, Q)
295295 d7 = disjunction (m5, [P[1 ], Q[2 ]])
296- @test_throws ErrorException apply_basic_step (m5, [d5, d7])
297296 @test_throws ErrorException apply_basic_step (m5, [d5, d6])
298297 # variable bound refs and reformulation constraints as globals
299298 m6, x6, _, _, d8, d9, _ = _basic_step_gdp (optimizer = nothing )
@@ -304,6 +303,59 @@ function test_basic_step_errors()
304303 rc = first (DP. _reformulation_constraints (m6))
305304 @test_throws ErrorException apply_basic_step (
306305 m6, [d8, d9], constraints = [rc])
306+ # vector global with an unsupported set
307+ m7 = GDPModel ()
308+ @variable (m7, z[1 : 2 ])
309+ @variable (m7, R[1 : 2 ], Logical)
310+ @constraint (m7, z[1 ] <= 1 , Disjunct (R[1 ]))
311+ @constraint (m7, z[1 ] >= 2 , Disjunct (R[2 ]))
312+ d10 = disjunction (m7, R)
313+ g_soc = @constraint (m7, [1.0 + z[1 ], z[2 ]] in SecondOrderCone ())
314+ @test_throws ErrorException apply_basic_step (
315+ m7, [d10], constraints = [g_soc])
316+ end
317+
318+ function test_basic_step_shared_indicators ()
319+ # d1 = [Y1, Y2] and d2 = [Y1, Y3] couple through Y1: the logic
320+ # admits {Y1} (obj 11) or {Y2, Y3} (obj 12)
321+ function shared_gdp ()
322+ model = GDPModel (HiGHS. Optimizer)
323+ set_attribute (model, MOI. Silent (), true )
324+ @variable (model, 0 <= x[1 : 2 ] <= 10 )
325+ @variable (model, Y[1 : 3 ], Logical)
326+ @objective (model, Max, x[1 ] + x[2 ])
327+ @constraint (model, x[1 ] <= 1 , Disjunct (Y[1 ]))
328+ @constraint (model, x[1 ] >= 3 , Disjunct (Y[2 ]))
329+ @constraint (model, x[2 ] <= 2 , Disjunct (Y[3 ]))
330+ d1 = disjunction (model, [Y[1 ], Y[2 ]])
331+ d2 = disjunction (model, [Y[1 ], Y[3 ]])
332+ return model, d1, d2
333+ end
334+ # the unpruned product is built with a warning
335+ model, d1, d2 = shared_gdp ()
336+ new_dref = @test_logs (:warn , r" share indicator" ) apply_basic_step (
337+ model, [d1, d2], name = " bs" )
338+ @test is_valid (model, new_dref)
339+ W = constraint_object (new_dref). indicators
340+ @test length (W) == 4
341+ # the diagonal cell gets the shared parent's constraints only once
342+ @test length (DP. _indicator_to_constraints (model)[W[1 ]]) == 1
343+ @test length (DP. _indicator_to_constraints (model)[W[2 ]]) == 2
344+ # product exactly1 + both marginal links for Y1 + one each for
345+ # Y2, Y3 (the double marginal is what forces dead cells off)
346+ @test length (DP. _logical_constraints (model)) == 5
347+ # solve equivalence before and after the basic step
348+ for method in (BigM (), Hull ())
349+ m, e1, e2 = shared_gdp ()
350+ optimize! (m, gdp_method = method)
351+ @test objective_value (m) ≈ 12
352+ m, e1, e2 = shared_gdp ()
353+ @test_logs (:warn , r" share indicator" ) apply_basic_step (
354+ m, [e1, e2])
355+ optimize! (m, gdp_method = method)
356+ @test termination_status (m) == MOI. OPTIMAL
357+ @test objective_value (m) ≈ 12
358+ end
307359end
308360
309361function test_basic_step_solve_equivalence ()
407459 test_basic_step_relax_products ()
408460 test_basic_step_complement ()
409461 test_basic_step_errors ()
462+ test_basic_step_shared_indicators ()
410463 test_basic_step_solve_equivalence ()
411464 test_basic_step_hull_tightness ()
412465 test_basic_step_repeated ()
0 commit comments