Skip to content

Commit f15a1da

Browse files
authored
Merge pull request #3213 from emma58/fix-bigm-nested-bug
Fix a bug in gdp.bigm transformation for nested GDPs
2 parents 69082ac + a3d6a43 commit f15a1da

File tree

7 files changed

+223
-85
lines changed

7 files changed

+223
-85
lines changed

pyomo/contrib/gdpopt/tests/test_LBB.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def test_infeasible_GDP(self):
5959
self.assertIsNone(m.d.disjuncts[0].indicator_var.value)
6060
self.assertIsNone(m.d.disjuncts[1].indicator_var.value)
6161

62+
@unittest.skipUnless(z3_available, "Z3 SAT solver is not available")
6263
def test_infeasible_GDP_check_sat(self):
6364
"""Test for infeasible GDP with check_sat option True."""
6465
m = ConcreteModel()

pyomo/contrib/gdpopt/tests/test_gdpopt.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from pyomo.common.collections import Bunch
2323
from pyomo.common.config import ConfigDict, ConfigValue
2424
from pyomo.common.fileutils import import_file, PYOMO_ROOT_DIR
25-
from pyomo.contrib.appsi.solvers.gurobi import Gurobi
2625
from pyomo.contrib.gdpopt.create_oa_subproblems import (
2726
add_util_block,
2827
add_disjunct_list,
@@ -767,6 +766,9 @@ def test_time_limit(self):
767766
results.solver.termination_condition, TerminationCondition.maxTimeLimit
768767
)
769768

769+
@unittest.skipUnless(
770+
license_available, "No BARON license--8PP logical problem exceeds demo size"
771+
)
770772
def test_LOA_8PP_logical_default_init(self):
771773
"""Test logic-based outer approximation with 8PP."""
772774
exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py'))
@@ -870,6 +872,9 @@ def test_LOA_8PP_maxBinary(self):
870872
)
871873
ct.check_8PP_solution(self, eight_process, results)
872874

875+
@unittest.skipUnless(
876+
license_available, "No BARON license--8PP logical problem exceeds demo size"
877+
)
873878
def test_LOA_8PP_logical_maxBinary(self):
874879
"""Test logic-based OA with max_binary initialization."""
875880
exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py'))
@@ -1050,7 +1055,11 @@ def assert_correct_disjuncts_active(
10501055

10511056
self.assertTrue(fabs(value(eight_process.profit.expr) - 68) <= 1e-2)
10521057

1053-
@unittest.skipUnless(Gurobi().available(), "APPSI Gurobi solver is not available")
1058+
@unittest.skipUnless(
1059+
SolverFactory('appsi_gurobi').available(exception_flag=False)
1060+
and SolverFactory('appsi_gurobi').license_is_valid(),
1061+
"Legacy APPSI Gurobi solver is not available",
1062+
)
10541063
def test_auto_persistent_solver(self):
10551064
exfile = import_file(join(exdir, 'eight_process', 'eight_proc_model.py'))
10561065
m = exfile.build_eight_process_flowsheet()
@@ -1126,6 +1135,9 @@ def test_RIC_8PP_default_init(self):
11261135
)
11271136
ct.check_8PP_solution(self, eight_process, results)
11281137

1138+
@unittest.skipUnless(
1139+
license_available, "No BARON license--8PP logical problem exceeds demo size"
1140+
)
11291141
def test_RIC_8PP_logical_default_init(self):
11301142
"""Test logic-based outer approximation with 8PP."""
11311143
exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py'))

pyomo/contrib/gdpopt/util.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,13 @@ def _add_bigm_constraint_to_transformed_model(m, constraint, block):
553553
# making a Reference to the ComponentData so that it will look like an
554554
# indexed component for now. If I redesign bigm at some point, then this
555555
# could be prettier.
556-
bigm._transform_constraint(Reference(constraint), parent_disjunct, None, [], [])
556+
bigm._transform_constraint(
557+
Reference(constraint),
558+
parent_disjunct,
559+
None,
560+
[],
561+
[],
562+
1 - parent_disjunct.binary_indicator_var,
563+
)
557564
# Now get rid of it because this is a class attribute!
558565
del bigm._config

pyomo/gdp/plugins/bigm.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -213,21 +213,15 @@ def _apply_to_impl(self, instance, **kwds):
213213
bigM = self._config.bigM
214214
for t in preprocessed_targets:
215215
if t.ctype is Disjunction:
216-
self._transform_disjunctionData(
217-
t,
218-
t.index(),
219-
bigM,
220-
parent_disjunct=gdp_tree.parent(t),
221-
root_disjunct=gdp_tree.root_disjunct(t),
222-
)
216+
self._transform_disjunctionData(t, t.index(), bigM, gdp_tree)
223217

224218
# issue warnings about anything that was in the bigM args dict that we
225219
# didn't use
226220
_warn_for_unused_bigM_args(bigM, self.used_args, logger)
227221

228-
def _transform_disjunctionData(
229-
self, obj, index, bigM, parent_disjunct=None, root_disjunct=None
230-
):
222+
def _transform_disjunctionData(self, obj, index, bigM, gdp_tree):
223+
parent_disjunct = gdp_tree.parent(obj)
224+
root_disjunct = gdp_tree.root_disjunct(obj)
231225
(transBlock, xorConstraint) = self._setup_transform_disjunctionData(
232226
obj, root_disjunct
233227
)
@@ -236,7 +230,7 @@ def _transform_disjunctionData(
236230
or_expr = 0
237231
for disjunct in obj.disjuncts:
238232
or_expr += disjunct.binary_indicator_var
239-
self._transform_disjunct(disjunct, bigM, transBlock)
233+
self._transform_disjunct(disjunct, bigM, transBlock, gdp_tree)
240234

241235
if obj.xor:
242236
xorConstraint[index] = or_expr == 1
@@ -249,7 +243,7 @@ def _transform_disjunctionData(
249243
# and deactivate for the writers
250244
obj.deactivate()
251245

252-
def _transform_disjunct(self, obj, bigM, transBlock):
246+
def _transform_disjunct(self, obj, bigM, transBlock, gdp_tree):
253247
# We're not using the preprocessed list here, so this could be
254248
# inactive. We've already done the error checking in preprocessing, so
255249
# we just skip it here.
@@ -261,6 +255,12 @@ def _transform_disjunct(self, obj, bigM, transBlock):
261255

262256
relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock)
263257

258+
indicator_expression = 0
259+
node = obj
260+
while node is not None:
261+
indicator_expression += 1 - node.binary_indicator_var
262+
node = gdp_tree.parent_disjunct(node)
263+
264264
# This is crazy, but if the disjunction has been previously
265265
# relaxed, the disjunct *could* be deactivated. This is a big
266266
# deal for Hull, as it uses the component_objects /
@@ -270,13 +270,21 @@ def _transform_disjunct(self, obj, bigM, transBlock):
270270
# comparing the two relaxations.
271271
#
272272
# Transform each component within this disjunct
273-
self._transform_block_components(obj, obj, bigM, arg_list, suffix_list)
273+
self._transform_block_components(
274+
obj, obj, bigM, arg_list, suffix_list, indicator_expression
275+
)
274276

275277
# deactivate disjunct to keep the writers happy
276278
obj._deactivate_without_fixing_indicator()
277279

278280
def _transform_constraint(
279-
self, obj, disjunct, bigMargs, arg_list, disjunct_suffix_list
281+
self,
282+
obj,
283+
disjunct,
284+
bigMargs,
285+
arg_list,
286+
disjunct_suffix_list,
287+
indicator_expression,
280288
):
281289
# add constraint to the transformation block, we'll transform it there.
282290
transBlock = disjunct._transformation_block()
@@ -348,7 +356,13 @@ def _transform_constraint(
348356
bigm_src[c] = (lower, upper)
349357

350358
self._add_constraint_expressions(
351-
c, i, M, disjunct.binary_indicator_var, newConstraint, constraint_map
359+
c,
360+
i,
361+
M,
362+
disjunct.binary_indicator_var,
363+
newConstraint,
364+
constraint_map,
365+
indicator_expression=indicator_expression,
352366
)
353367

354368
# deactivate because we relaxed

pyomo/gdp/plugins/bigm_mixin.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,14 @@ def _estimate_M(self, expr, constraint):
232232
return tuple(M)
233233

234234
def _add_constraint_expressions(
235-
self, c, i, M, indicator_var, newConstraint, constraint_map
235+
self,
236+
c,
237+
i,
238+
M,
239+
indicator_var,
240+
newConstraint,
241+
constraint_map,
242+
indicator_expression=None,
236243
):
237244
# Since we are both combining components from multiple blocks and using
238245
# local names, we need to make sure that the first index for
@@ -244,14 +251,16 @@ def _add_constraint_expressions(
244251
# over the constraint indices, but I don't think it matters a lot.)
245252
unique = len(newConstraint)
246253
name = c.local_name + "_%s" % unique
254+
if indicator_expression is None:
255+
indicator_expression = 1 - indicator_var
247256

248257
if c.lower is not None:
249258
if M[0] is None:
250259
raise GDP_Error(
251260
"Cannot relax disjunctive constraint '%s' "
252261
"because M is not defined." % name
253262
)
254-
M_expr = M[0] * (1 - indicator_var)
263+
M_expr = M[0] * indicator_expression
255264
newConstraint.add((name, i, 'lb'), c.lower <= c.body - M_expr)
256265
constraint_map.transformed_constraints[c].append(
257266
newConstraint[name, i, 'lb']
@@ -263,7 +272,7 @@ def _add_constraint_expressions(
263272
"Cannot relax disjunctive constraint '%s' "
264273
"because M is not defined." % name
265274
)
266-
M_expr = M[1] * (1 - indicator_var)
275+
M_expr = M[1] * indicator_expression
267276
newConstraint.add((name, i, 'ub'), c.body - M_expr <= c.upper)
268277
constraint_map.transformed_constraints[c].append(
269278
newConstraint[name, i, 'ub']

0 commit comments

Comments
 (0)