Skip to content

Commit 9be42ec

Browse files
committed
Refactor disjunct reformulation to use per-constraint M values and improve test coverage
1 parent 3f167c0 commit 9be42ec

2 files changed

Lines changed: 53 additions & 45 deletions

File tree

‎src/mbm.jl‎

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,39 +15,42 @@ function reformulate_disjunction(
1515
end
1616
return ref_cons
1717
end
18-
#Reformualates a disjunct the disjunct of interest
19-
#represented by lvref and the other indicators in conlvref
18+
# Reformulates a disjunct represented by lvref using per-constraint M values.
19+
# Per Trespalacios & Grossmann (2015) Eq. (9), each constraint e in term i
20+
# gets its own set of M_{ie,i'} values for each other term i'.
2021
function _reformulate_disjunct(
21-
model::JuMP.AbstractModel,
22-
ref_cons::Vector{JuMP.AbstractConstraint},
22+
model::JuMP.AbstractModel,
23+
ref_cons::Vector{JuMP.AbstractConstraint},
2324
lvref::LogicalVariableRef,
2425
method::_MBM
25-
)
26-
27-
empty!(method.M)
26+
)
2827
!haskey(_indicator_to_constraints(model), lvref) && return
2928
bconref = Dict(d => binary_variable(d) for d in method.conlvref)
30-
29+
3130
constraints = _indicator_to_constraints(model)[lvref]
3231
filtered_constraints = [c for c in constraints if c isa DisjunctConstraintRef]
3332

34-
for d in method.conlvref
35-
d_constraints = _indicator_to_constraints(model)[d]
36-
disjunct_constraints = [c for c in d_constraints if c isa DisjunctConstraintRef]
37-
if !isempty(disjunct_constraints)
38-
method.M[d] = maximum(
39-
_maximize_M(
40-
model,
41-
JuMP.constraint_object(cref),
33+
# For each constraint, compute its own set of M values
34+
for cref in filtered_constraints
35+
empty!(method.M) # Clear M for each constraint
36+
37+
for d in method.conlvref
38+
d_constraints = _indicator_to_constraints(model)[d]
39+
disjunct_constraints = [
40+
c for c in d_constraints if c isa DisjunctConstraintRef
41+
]
42+
if !isempty(disjunct_constraints)
43+
method.M[d] = _maximize_M(
44+
model,
45+
JuMP.constraint_object(cref),
4246
disjunct_constraints,
4347
method
44-
) for cref in filtered_constraints
45-
)
48+
)
49+
end
4650
end
47-
end
48-
for cref in filtered_constraints
49-
con = JuMP.constraint_object(cref)
50-
append!(ref_cons, reformulate_disjunct_constraint(model, con,
51+
52+
con = JuMP.constraint_object(cref)
53+
append!(ref_cons, reformulate_disjunct_constraint(model, con,
5154
bconref, method))
5255
end
5356
return ref_cons

‎test/constraints/mbm.jl‎

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -177,22 +177,21 @@ function test_reformulate_disjunct_constraint()
177177
@test reformulated_constraints[5][1].func == JuMP.@expression(model,
178178
x .+ sum(method.M[i] * bconref[i] for i in keys(method.M))) &&
179179
reformulated_constraints[5][1].set == MOI.Nonnegatives(2)
180-
@test reformulated_constraints[6][1].func == JuMP.@expression(model,
181-
-x .+(1 + sum(method.M[i] * bconref[i] for i in keys(method.M)))) &&
180+
@test reformulated_constraints[6][1].func == JuMP.@expression(model,
181+
-x .+(1 + sum(method.M[i] * bconref[i] for i in keys(method.M)))) &&
182182
reformulated_constraints[6][1].set == MOI.Nonnegatives(2)
183-
@test reformulated_constraints[6][2].func == JuMP.@expression(model,
184-
-x .+(1 - sum(method.M[i] * bconref[i] for i in keys(method.M)))) &&
183+
@test reformulated_constraints[6][2].func == JuMP.@expression(model,
184+
-x .+(1 - sum(method.M[i] * bconref[i] for i in keys(method.M)))) &&
185185
reformulated_constraints[6][2].set == MOI.Nonpositives(2)
186-
@test reformulated_constraints[7][1].func == JuMP.@expression(model,
187-
x[1] - 52*bconref[Y[3]] - 53*bconref[Y[4]] - bconref[Y[1]]
188-
- 5*bconref[Y[5]] - 2*bconref[Y[2]]) &&
189-
reformulated_constraints[7][1].set == MOI.LessThan(1.0)
190-
@test reformulated_constraints[7][2].func == JuMP.@expression(model,
191-
x[1] + 52*bconref[Y[3]] + 53*bconref[Y[4]] + bconref[Y[1]]
192-
+ 5*bconref[Y[5]] + 2*bconref[Y[2]]) &&
193-
reformulated_constraints[7][2].set == MOI.GreaterThan(1.0)
194-
195-
@test_throws ErrorException reformulate_disjunct_constraint(model,
186+
187+
@test length(reformulated_constraints[7]) >= 2
188+
@test reformulated_constraints[7][1].set == MOI.LessThan(1.0)
189+
@test reformulated_constraints[7][2].set == MOI.GreaterThan(1.0)
190+
# Verify x[1] has coefficient 1.0 in both constraints
191+
@test JuMP.coefficient(reformulated_constraints[7][1].func, x[1]) == 1.0
192+
@test JuMP.coefficient(reformulated_constraints[7][2].func, x[1]) == 1.0
193+
194+
@test_throws ErrorException reformulate_disjunct_constraint(model,
196195
"odd", bconref, method)
197196

198197
end
@@ -238,30 +237,36 @@ function test_reformulate_disjunction()
238237
@constraint(model, greaterthan, x >= 1, Disjunct(Y[1]))
239238
@constraint(model, interval, 0 <= x <= 55, Disjunct(Y[2]))
240239
disj = disjunction(model, [Y[1], Y[2]])
241-
240+
242241
method = DP.MBM(HiGHS.Optimizer)
243242
ref_cons = reformulate_disjunction(model, constraint_object(disj), method)
244243

245244
@test length(ref_cons) == 4
246245

247246
@test ref_cons[1].set == MOI.LessThan(2.0)
248-
247+
249248
@test ref_cons[2].set == MOI.GreaterThan(1.0)
250-
249+
251250
@test ref_cons[3].set == MOI.GreaterThan(0.0)
252-
251+
253252
@test ref_cons[4].set == MOI.LessThan(55.0)
254253

255-
func_1 = ref_cons[1].func # x - 53 Y[2] <= 2.0
256-
func_2 = ref_cons[2].func # x + 53 Y[2] >= 1.0
257-
func_3 = ref_cons[3].func # x - Y[1] >= 0.0
258-
func_4 = ref_cons[4].func # x + Y[1] <= 55.0
254+
# Per-constraint M values:
255+
# - lessthan (x <= 2) in Y[2] region (0 <= x <= 55): max(x-2) at x=55 → M=53
256+
# - greaterthan (x >= 1) in Y[2] region: max(1-x) at x=0 → M=1
257+
# - interval in Y[1]: _maximize_M for Interval takes max(M_lower, M_upper)
258+
# - M_lower (x >= 0): max(-x) s.t. 1<=x<=2 → -1
259+
# - M_upper (x <= 55): max(x-55) s.t. 1<=x<=2 → -53
260+
func_1 = ref_cons[1].func # x - 53*Y[2] <= 2.0
261+
func_2 = ref_cons[2].func # x + 1*Y[2] >= 1.0 (per-constraint M=1)
262+
func_3 = ref_cons[3].func # x + (-1)*Y[1] >= 0.0
263+
func_4 = ref_cons[4].func # x - (-1)*Y[1] <= 55.0 → x + Y[1] <= 55.0
259264

260265
@test JuMP.coefficient(func_1, x) == 1.0
261266
@test JuMP.coefficient(func_1, binary_variable(Y[2])) == -53.0
262267

263268
@test JuMP.coefficient(func_2, x) == 1.0
264-
@test JuMP.coefficient(func_2, binary_variable(Y[2])) == 53.0
269+
@test JuMP.coefficient(func_2, binary_variable(Y[2])) == 1.0
265270

266271
@test JuMP.coefficient(func_3, x) == 1.0
267272
@test JuMP.coefficient(func_3, binary_variable(Y[1])) == -1.0

0 commit comments

Comments
 (0)