Skip to content

Commit 8aeb94a

Browse files
committed
_set_support refactor + warning on shared indicators
1 parent cf72a87 commit 8aeb94a

3 files changed

Lines changed: 76 additions & 9 deletions

File tree

‎src/basicsteps.jl‎

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ function _check_global_constraint(model::JuMP.AbstractModel, cref)
2121
"cannot be intersected into a disjunction.")
2222
end
2323
supported = con isa JuMP.ScalarConstraint ||
24-
(con isa JuMP.VectorConstraint && JuMP.moi_set(con) isa
25-
Union{_MOI.Nonnegatives, _MOI.Nonpositives, _MOI.Zeros})
24+
(con isa JuMP.VectorConstraint &&
25+
_set_support(JuMP.moi_set(con)))
2626
supported || error("Global constraint `$cref` with set " *
2727
"`$(JuMP.moi_set(con))` cannot be intersected into a disjunction.")
2828
_check_expression(JuMP.jump_function(con))
@@ -67,8 +67,11 @@ function _check_basic_step_input(
6767
end
6868
all_indicators = [lv for d in disjunctions
6969
for lv in JuMP.constraint_object(d).indicators]
70-
allunique(all_indicators) || error(
71-
"The `disjunctions` for a basic step share indicator variables.")
70+
allunique(all_indicators) || @warn "The `disjunctions` for a basic " *
71+
"step share indicator variables, so some product disjuncts are " *
72+
"logically infeasible. Their indicators are forced off by the " *
73+
"linking constraints, so results remain correct, but the " *
74+
"product disjunction is larger than its pruned equivalent."
7275
indicator_set = Set(all_indicators)
7376
input_set = Set(disjunctions)
7477
for (idx, disj) in _disjunctions(model)
@@ -150,8 +153,10 @@ function _add_product_disjuncts(
150153
products[idx] = w
151154
_product_to_parents(model)[w] = parents
152155
# constraint objects are shared across product disjuncts; safe
153-
# since reformulations never mutate stored functions
154-
for parent in parents, (con, cname) in disjunct_snapshot[parent]
156+
# since reformulations never mutate stored functions. unique:
157+
# a shared parent indicator appears on multiple axes
158+
for parent in unique(parents),
159+
(con, cname) in disjunct_snapshot[parent]
155160
JuMP.add_constraint(model, _DisjunctConstraint(con, w), cname)
156161
end
157162
_intersect_globals(model, w, global_snapshot)
@@ -240,7 +245,11 @@ parents of each product indicator can be queried with
240245
241246
Each input disjunction must use `exactly1 = true` (or a
242247
logical-complement pair) and must not be nested or contain nested
243-
disjunctions. The number of product disjuncts is the product of the
248+
disjunctions. Input disjunctions may share indicator variables (a
249+
warning is emitted): the product then contains logically infeasible
250+
disjuncts whose indicators the linking constraints force off, so
251+
results are correct but the model is larger than its pruned
252+
equivalent. The number of product disjuncts is the product of the
244253
input disjunction sizes, so repeated basic steps grow the model
245254
multiplicatively (a warning is emitted above 100).
246255

‎src/constraints.jl‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ function JuMP.build_constraint(
221221
return _DisjunctConstraint(constr, tag.indicator)
222222
end
223223

224+
_set_support(::_MOI.AbstractSet) = false
225+
_set_support(
226+
::Union{_MOI.Nonnegatives, _MOI.Nonpositives, _MOI.Zeros}
227+
) = true
228+
224229
# Allows for building DisjunctConstraints for VectorConstraints since these get parsed differently by JuMP (JuMP changes the set to a MOI.AbstractScalarSet)
225230
for SetType in (
226231
JuMP.Nonnegatives, JuMP.Nonpositives, JuMP.Zeros,

‎test/constraints/basicsteps.jl‎

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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
307359
end
308360

309361
function test_basic_step_solve_equivalence()
@@ -407,6 +459,7 @@ end
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

Comments
 (0)