Skip to content

Commit 9fd504a

Browse files
authored
Merge pull request Pyomo#3314 from emma58/mbm-baron-bug
GDP: Don't transform known-to-be infeasible Disjuncts in multiple BigM
2 parents 2d138e3 + 6f0d954 commit 9fd504a

File tree

2 files changed

+32
-18
lines changed

2 files changed

+32
-18
lines changed

pyomo/gdp/plugins/multiple_bigm.py

+25-6
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import itertools
1313
import logging
1414

15-
from pyomo.common.collections import ComponentMap
15+
from pyomo.common.collections import ComponentMap, ComponentSet
1616
from pyomo.common.config import ConfigDict, ConfigValue
1717
from pyomo.common.gc_manager import PauseGC
1818
from pyomo.common.modeling import unique_component_name
@@ -310,9 +310,12 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, root_disjunct)
310310

311311
arg_Ms = self._config.bigM if self._config.bigM is not None else {}
312312

313+
# ESJ: I am relying on the fact that the ComponentSet is going to be
314+
# ordered here, but using a set because I will remove infeasible
315+
# Disjuncts from it if I encounter them calculating M's.
316+
active_disjuncts = ComponentSet(disj for disj in obj.disjuncts if disj.active)
313317
# First handle the bound constraints if we are dealing with them
314318
# separately
315-
active_disjuncts = [disj for disj in obj.disjuncts if disj.active]
316319
transformed_constraints = set()
317320
if self._config.reduce_bound_constraints:
318321
transformed_constraints = self._transform_bound_constraints(
@@ -585,7 +588,7 @@ def _calculate_missing_M_values(
585588
):
586589
if disjunct is other_disjunct:
587590
continue
588-
if id(other_disjunct) in scratch_blocks:
591+
elif id(other_disjunct) in scratch_blocks:
589592
scratch = scratch_blocks[id(other_disjunct)]
590593
else:
591594
scratch = scratch_blocks[id(other_disjunct)] = Block()
@@ -631,15 +634,21 @@ def _calculate_missing_M_values(
631634
scratch.obj.expr = constraint.body - constraint.lower
632635
scratch.obj.sense = minimize
633636
lower_M = self._solve_disjunct_for_M(
634-
other_disjunct, scratch, unsuccessful_solve_msg
637+
other_disjunct,
638+
scratch,
639+
unsuccessful_solve_msg,
640+
active_disjuncts,
635641
)
636642
if constraint.upper is not None and upper_M is None:
637643
# last resort: calculate
638644
if upper_M is None:
639645
scratch.obj.expr = constraint.body - constraint.upper
640646
scratch.obj.sense = maximize
641647
upper_M = self._solve_disjunct_for_M(
642-
other_disjunct, scratch, unsuccessful_solve_msg
648+
other_disjunct,
649+
scratch,
650+
unsuccessful_solve_msg,
651+
active_disjuncts,
643652
)
644653
arg_Ms[constraint, other_disjunct] = (lower_M, upper_M)
645654
transBlock._mbm_values[constraint, other_disjunct] = (lower_M, upper_M)
@@ -651,9 +660,18 @@ def _calculate_missing_M_values(
651660
return arg_Ms
652661

653662
def _solve_disjunct_for_M(
654-
self, other_disjunct, scratch_block, unsuccessful_solve_msg
663+
self, other_disjunct, scratch_block, unsuccessful_solve_msg, active_disjuncts
655664
):
665+
if not other_disjunct.active:
666+
# If a Disjunct is infeasible, we will discover that and deactivate
667+
# it when we are calculating the M values. We remove that disjunct
668+
# from active_disjuncts inside of the loop in
669+
# _calculate_missing_M_values. So that means that we might have
670+
# deactivated Disjuncts here that we should skip over.
671+
return 0
672+
656673
solver = self._config.solver
674+
657675
results = solver.solve(other_disjunct, load_solutions=False)
658676
if results.solver.termination_condition is TerminationCondition.infeasible:
659677
# [2/18/24]: TODO: After the solver rewrite is complete, we will not
@@ -669,6 +687,7 @@ def _solve_disjunct_for_M(
669687
"Disjunct '%s' is infeasible, deactivating." % other_disjunct.name
670688
)
671689
other_disjunct.deactivate()
690+
active_disjuncts.remove(other_disjunct)
672691
M = 0
673692
else:
674693
# This is a solver that might report

pyomo/gdp/tests/test_mbigm.py

+7-12
Original file line numberDiff line numberDiff line change
@@ -1019,39 +1019,34 @@ def test_calculate_Ms_infeasible_Disjunct(self):
10191019
out.getvalue().strip(),
10201020
)
10211021

1022-
# We just fixed the infeasible by to False
1022+
# We just fixed the infeasible disjunct to False
10231023
self.assertFalse(m.disjunction.disjuncts[0].active)
10241024
self.assertTrue(m.disjunction.disjuncts[0].indicator_var.fixed)
10251025
self.assertFalse(value(m.disjunction.disjuncts[0].indicator_var))
10261026

1027+
# We didn't actually transform the infeasible disjunct
1028+
self.assertIsNone(m.disjunction.disjuncts[0].transformation_block)
1029+
10271030
# the remaining constraints are transformed correctly.
10281031
cons = mbm.get_transformed_constraints(m.disjunction.disjuncts[1].constraint[1])
10291032
self.assertEqual(len(cons), 1)
10301033
assertExpressionsEqual(
10311034
self,
10321035
cons[0].expr,
1033-
21 + m.x - m.y
1034-
<= 0 * m.disjunction.disjuncts[0].binary_indicator_var
1035-
+ 12.0 * m.disjunction.disjuncts[2].binary_indicator_var,
1036+
21 + m.x - m.y <= 12.0 * m.disjunction.disjuncts[2].binary_indicator_var,
10361037
)
10371038

10381039
cons = mbm.get_transformed_constraints(m.disjunction.disjuncts[2].constraint[1])
10391040
self.assertEqual(len(cons), 2)
1040-
print(cons[0].expr)
1041-
print(cons[1].expr)
10421041
assertExpressionsEqual(
10431042
self,
10441043
cons[0].expr,
1045-
0.0 * m.disjunction_disjuncts[0].binary_indicator_var
1046-
- 12.0 * m.disjunction_disjuncts[1].binary_indicator_var
1047-
<= m.x - (m.y - 9),
1044+
-12.0 * m.disjunction_disjuncts[1].binary_indicator_var <= m.x - (m.y - 9),
10481045
)
10491046
assertExpressionsEqual(
10501047
self,
10511048
cons[1].expr,
1052-
m.x - (m.y - 9)
1053-
<= 0.0 * m.disjunction_disjuncts[0].binary_indicator_var
1054-
- 12.0 * m.disjunction_disjuncts[1].binary_indicator_var,
1049+
m.x - (m.y - 9) <= -12.0 * m.disjunction_disjuncts[1].binary_indicator_var,
10551050
)
10561051

10571052
@unittest.skipUnless(

0 commit comments

Comments
 (0)