From 8dcac6c1885a661932d0490dca14b5c1dc3b21ba Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 22 Nov 2022 14:02:43 -0700 Subject: [PATCH 1/8] bigm just creates the constraint map and never has to check for it --- pyomo/gdp/plugins/bigm.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index 670adf70370..cae8a46dd29 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -366,6 +366,12 @@ def _transform_disjunct(self, obj, bigM, root_disjunct): # information for both.) relaxationBlock.bigm_src = {} relaxationBlock.localVarReferences = Block() + # add the map that will link back and forth between transformed + # constraints and their originals. + relaxationBlock._constraintMap = { + 'srcConstraints': ComponentMap(), + 'transformedConstraints': ComponentMap() + } relaxationBlock.transformedConstraints = Constraint(Any) obj._transformation_block = weakref_ref(relaxationBlock) relaxationBlock._src_disjunct = weakref_ref(obj) @@ -436,19 +442,12 @@ def _warn_for_active_disjunct(self, innerdisjunct, outerdisjunct, bigMargs, arg_list, suffix_list): _warn_for_active_disjunct(innerdisjunct, outerdisjunct) - def _get_constraint_map_dict(self, transBlock): - if not hasattr(transBlock, "_constraintMap"): - transBlock._constraintMap = { - 'srcConstraints': ComponentMap(), - 'transformedConstraints': ComponentMap()} - return transBlock._constraintMap - def _transform_constraint(self, obj, disjunct, bigMargs, arg_list, disjunct_suffix_list): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() bigm_src = transBlock.bigm_src - constraintMap = self._get_constraint_map_dict(transBlock) + constraintMap = transBlock._constraintMap disjunctionRelaxationBlock = transBlock.parent_block() From e9715f6483548db10a060940306f928ad89f7864 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 22 Nov 2022 14:29:20 -0700 Subject: [PATCH 2/8] Breaking out the part of transform constraint that actually generates the constraint expressions with the Ms --- pyomo/gdp/plugins/bigm.py | 79 +++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index cae8a46dd29..20f4c40e148 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -456,18 +456,9 @@ def _transform_constraint(self, obj, disjunct, bigMargs, arg_list, # products is kind of expensive (and redundant since we have the # original model) newConstraint = transBlock.transformedConstraints - # Since we are both combining components from multiple blocks and using - # local names, we need to make sure that the first index for - # transformedConstraints is guaranteed to be unique. We just grab the - # current length of the list here since that will be monotonically - # increasing and hence unique. We'll append it to the - # slightly-more-human-readable constraint name for something familiar - # but unique. - unique = len(newConstraint) for i in sorted(obj.keys()): c = obj[i] - name = c.local_name + "_%s" % unique if not c.active: continue @@ -518,37 +509,53 @@ def _transform_constraint(self, obj, disjunct, bigMargs, arg_list, # save the source information bigm_src[c] = (lower, upper) - if c.lower is not None: - if M[0] is None: - raise GDP_Error("Cannot relax disjunctive constraint '%s' " - "because M is not defined." % name) - M_expr = M[0] * (1 - disjunct.binary_indicator_var) - newConstraint.add((name, i, 'lb'), c.lower <= c. body - M_expr) - constraintMap[ - 'transformedConstraints'][c] = [ - newConstraint[name, i, 'lb']] - constraintMap['srcConstraints'][ - newConstraint[name, i, 'lb']] = c - if c.upper is not None: - if M[1] is None: - raise GDP_Error("Cannot relax disjunctive constraint '%s' " - "because M is not defined." % name) - M_expr = M[1] * (1 - disjunct.binary_indicator_var) - newConstraint.add((name, i, 'ub'), c.body - M_expr <= c.upper) - transformed = constraintMap['transformedConstraints'].get(c) - if transformed is not None: - constraintMap['transformedConstraints'][ - c].append(newConstraint[name, i, 'ub']) - else: - constraintMap[ - 'transformedConstraints'][c] = [ - newConstraint[name, i, 'ub']] - constraintMap['srcConstraints'][ - newConstraint[name, i, 'ub']] = c + self._add_constraint_expressions(c, i, M, + disjunct.binary_indicator_var, + newConstraint, constraintMap) # deactivate because we relaxed c.deactivate() + def _add_constraint_expressions(self, c, i, M, indicator_var, newConstraint, + constraintMap): + # Since we are both combining components from multiple blocks and using + # local names, we need to make sure that the first index for + # transformedConstraints is guaranteed to be unique. We just grab the + # current length of the list here since that will be monotonically + # increasing and hence unique. We'll append it to the + # slightly-more-human-readable constraint name for something familiar + # but unique. + unique = len(newConstraint) + name = c.local_name + "_%s" % unique + + if c.lower is not None: + if M[0] is None: + raise GDP_Error("Cannot relax disjunctive constraint '%s' " + "because M is not defined." % name) + M_expr = M[0] * (1 - indicator_var) + newConstraint.add((name, i, 'lb'), c.lower <= c. body - M_expr) + constraintMap[ + 'transformedConstraints'][c] = [ + newConstraint[name, i, 'lb']] + constraintMap['srcConstraints'][ + newConstraint[name, i, 'lb']] = c + if c.upper is not None: + if M[1] is None: + raise GDP_Error("Cannot relax disjunctive constraint '%s' " + "because M is not defined." % name) + M_expr = M[1] * (1 - indicator_var) + newConstraint.add((name, i, 'ub'), c.body - M_expr <= c.upper) + transformed = constraintMap['transformedConstraints'].get(c) + if transformed is not None: + constraintMap['transformedConstraints'][ + c].append(newConstraint[name, i, 'ub']) + else: + constraintMap[ + 'transformedConstraints'][c] = [ + newConstraint[name, i, 'ub']] + constraintMap['srcConstraints'][ + newConstraint[name, i, 'ub']] = c + def _process_M_value(self, m, lower, upper, need_lower, need_upper, src, key, constraint, from_args=False): m = _convert_M_to_tuple(m, constraint) From 11feb7197eb601f8595f05ae5a6ca157eb6bd75a Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 22 Nov 2022 14:40:23 -0700 Subject: [PATCH 3/8] Having multiple big-m's transform constraint calling a now-more-modular part of bigm when we are only mbigming bound constraints --- pyomo/gdp/plugins/bigm.py | 3 +- pyomo/gdp/plugins/multiple_bigm.py | 114 +++++++++++++++++++++++------ pyomo/gdp/tests/test_mbigm.py | 24 ++++++ 3 files changed, 117 insertions(+), 24 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index 20f4c40e148..993bb7fe3da 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -524,7 +524,8 @@ def _add_constraint_expressions(self, c, i, M, indicator_var, newConstraint, # current length of the list here since that will be monotonically # increasing and hence unique. We'll append it to the # slightly-more-human-readable constraint name for something familiar - # but unique. + # but unique. (Note that we really could do this outside of the loop + # over the constraint indices, but I don't think it matters a lot.) unique = len(newConstraint) name = c.local_name + "_%s" % unique diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index caa3acc1893..dfe92278547 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -18,7 +18,7 @@ from pyomo.common.modeling import unique_component_name from pyomo.core import ( - Binary, Block, BooleanVar, Connector, Constraint, Expression, + Any, Binary, Block, BooleanVar, Connector, Constraint, Expression, ExternalFunction, maximize, minimize, NonNegativeIntegers, Objective, Param, RangeSet, Set, SetOf, SortComponents, Suffix, value, Var ) @@ -140,6 +140,25 @@ class MultipleBigMTransformation(Transformation): Techniques," SIAM Review, vol. 57, no. 1, 2015, pp. 3-57. """ )) + CONFIG.declare('only_mbigm_bound_constraints', ConfigValue( + default=False, + domain=bool, + description="Flag indicating if only bound constraints should be " + "transformed with multiple-bigm, or if all the disjunctive " + "constraints should.", + doc=""" + Sometimes it is only computationally advantageous to apply multiple- + bigm to disjunctive constraints with the special structure: + + [l_1 <= x <= u_1] v [l_2 <= x <= u_2] v ... v [l_K <= x <= u_K], + + and transform other disjunctive constraints with the traditional + big-M transformation. This flag is used to set the above behavior. + + Note that the reduce_bound_constraints flag must also be True when + this flag is set to True. + """ + )) def __init__(self): super(MultipleBigMTransformation, self).__init__() @@ -170,6 +189,7 @@ def __init__(self): # the network.expand_arcs transformation } self._transformation_blocks = {} + self._arg_list = {} def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() @@ -178,6 +198,7 @@ def _apply_to(self, instance, **kwds): finally: self.used_args.clear() self._transformation_blocks.clear() + self._arg_list.clear() def _apply_to_impl(self, instance, **kwds): if not instance.ctype in (Block, Disjunct): @@ -189,6 +210,15 @@ def _apply_to_impl(self, instance, **kwds): self._config = self.CONFIG(kwds.pop('options', {})) self._config.set_value(kwds) + if (self._config.only_mbigm_bound_constraints and not + self._config.reduce_bound_constraints): + raise GDP_Error("The 'only_mbigm_bound_constraints' option is set " + "to True, but the 'reduce_bound_constraints' " + "option is not. This is not supported--please also " + "set 'reduce_bound_constraints' to True if you " + "only wish to transform the bound constraints with " + "multiple bigm.") + targets = self._config.targets knownBlocks = {} if targets is None: @@ -270,9 +300,12 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, transformed_constraints = self._transform_bound_constraints( active_disjuncts, transBlock, arg_Ms) - Ms = transBlock.calculated_missing_m_values = self.\ - _calculate_missing_M_values(active_disjuncts, arg_Ms, transBlock, - transformed_constraints) + Ms = arg_Ms + if not self._config.only_mbigm_bound_constraints: + Ms = transBlock.calculated_missing_m_values = self.\ + _calculate_missing_M_values(active_disjuncts, arg_Ms, + transBlock, + transformed_constraints) # Now we can deactivate the constraints we deferred, so that we don't # re-transform them @@ -394,31 +427,62 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): name = unique_component_name(relaxationBlock, obj.getname( fully_qualified=False)) - newConstraint = Constraint(obj.index_set(), transBlock.lbub) + newConstraint = Constraint(Any) relaxationBlock.add_component(name, newConstraint) + bigm = TransformationFactory('gdp.bigm') + bigm.assume_fixed_vars_permanent = self._config.\ + assume_fixed_vars_permanent for i in sorted(obj.keys()): c = obj[i] if not c.active: continue - transformed = [] - if c.lower is not None: - rhs = sum(Ms[c, - disj][0]*disj.indicator_var.get_associated_binary() - for disj in active_disjuncts if disj is not disjunct) - newConstraint.add((i, 'lb'), c.body - c.lower >= rhs) - transformed.append(newConstraint[i, 'lb']) - - if c.upper is not None: - rhs = sum(Ms[c, - disj][1]*disj.indicator_var.get_associated_binary() - for disj in active_disjuncts if disj is not disjunct) - newConstraint.add((i, 'ub'), c.body - c.upper <= rhs) - transformed.append(newConstraint[i, 'ub']) - for c_new in transformed: - constraintMap['srcConstraints'][c_new] = [c] - constraintMap['transformedConstraints'][c] = transformed + if not self._config.only_mbigm_bound_constraints: + transformed = [] + if c.lower is not None: + rhs = sum( + Ms[c, + disj][0]*disj.indicator_var.get_associated_binary() + for disj in active_disjuncts if disj is not disjunct) + newConstraint.add((i, 'lb'), c.body - c.lower >= rhs) + transformed.append(newConstraint[i, 'lb']) + + if c.upper is not None: + rhs = sum( + Ms[c, + disj][1]*disj.indicator_var.get_associated_binary() + for disj in active_disjuncts if disj is not disjunct) + newConstraint.add((i, 'ub'), c.body - c.upper <= rhs) + transformed.append(newConstraint[i, 'ub']) + for c_new in transformed: + constraintMap['srcConstraints'][c_new] = [c] + constraintMap['transformedConstraints'][c] = transformed + else: + lower = (None, None, None) + upper = (None, None, None) + + if disjunct not in self._arg_list: + self._arg_list[disjunct] = bigm._get_bigm_arg_list( + self._config.bigM, disjunct) + arg_list = self._arg_list[disjunct] + + # first, we see if an M value was specified in the arguments. + # (This returns None if not) + lower, upper = bigm._get_M_from_args(c, Ms, arg_list, lower, + upper) + M = (lower[0], upper[0]) + + # estimate if we don't have what we need + if c.lower is not None and M[0] is None: + M = (bigm._estimate_M(c.body, c)[0] - c.lower, M[1]) + lower = (M[0], None, None) + if c.upper is not None and M[1] is None: + M = (M[0], bigm._estimate_M(c.body, c)[1] - c.upper) + upper = (M[1], None, None) + bigm._add_constraint_expressions( + c, i, M, disjunct.indicator_var.get_associated_binary(), + newConstraint, constraintMap) # deactivate now that we have transformed c.deactivate() @@ -517,6 +581,10 @@ def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): for (c, disj) in lower_bound_constraints_by_var[v]: relaxationBlock._constraintMap['srcConstraints'][ transformed[idx, 'lb']].append(c) + print(disj) + print(disj.transformation_block.name) + print(c) + print('===============') disj.transformation_block._constraintMap[ 'transformedConstraints'][c] = [transformed[idx, 'lb']] if len(upper_dict) > 0: @@ -547,7 +615,7 @@ def _add_transformation_block(self, block): # transformed components transBlockName = unique_component_name( block, - '_pyomo_gdp_hull_reformulation') + '_pyomo_gdp_mbigm_reformulation') transBlock = Block() block.add_component(transBlockName, transBlock) self._transformation_blocks[block] = transBlock diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index e8cee3e87e8..ec8e97810c2 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -643,3 +643,27 @@ def test_logical_constraints_on_disjuncts(self): check_linear_coef(self, repn, z, -1) check_linear_coef(self, repn, m.d2.binary_indicator_var, -1) check_linear_coef(self, repn, m.d3.binary_indicator_var, -1) + + def test_only_multiple_bigm_bound_constraints(self): + m = self.make_model() + mbm = TransformationFactory('gdp.mbigm') + mbm.apply_to(m, only_mbigm_bound_constraints=True) + + cons = mbm.get_transformed_constraints(m.d1.x1_bounds) + self.assertEqual(len(cons), 2) + self.check_pretty_bound_constraints(cons[0], m.x1, {m.d1: 0.5, m.d2: + 0.65, m.d3: 2}, + lb=True) + self.check_pretty_bound_constraints(cons[1], m.x1, {m.d1: 2, m.d2: 3, + m.d3: 10}, lb=False) + + cons = mbm.get_transformed_constraints(m.d1.x2_bounds) + self.assertEqual(len(cons), 2) + self.check_pretty_bound_constraints(cons[0], m.x2, {m.d1: 0.75, m.d2: 3, + m.d3: 0.55}, + lb=True) + self.check_pretty_bound_constraints(cons[1], m.x2, {m.d1: 3, m.d2: 10, + m.d3: 1}, lb=False) + + # TODO: now we check that the other constraints were transformed with + # normal bigm From d1d2baa6ffee695811c559737e45378823a74079 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 23 Nov 2022 12:01:35 -0700 Subject: [PATCH 4/8] Testing the normally bigm-ed constraints --- pyomo/gdp/tests/test_mbigm.py | 64 +++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index ec8e97810c2..e9b7d4e594d 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -16,6 +16,7 @@ from pyomo.common.fileutils import import_file, PYOMO_ROOT_DIR from pyomo.common.log import LoggingIntercept import pyomo.common.unittest as unittest +from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.environ import ( BooleanVar, ConcreteModel, Constraint, LogicalConstraint, @@ -665,5 +666,64 @@ def test_only_multiple_bigm_bound_constraints(self): self.check_pretty_bound_constraints(cons[1], m.x2, {m.d1: 3, m.d2: 10, m.d3: 1}, lb=False) - # TODO: now we check that the other constraints were transformed with - # normal bigm + cons = mbm.get_transformed_constraints(m.d1.func) + self.assertEqual(len(cons), 2) + lb = cons[0] + ub = cons[1] + assertExpressionsEqual(self, lb.expr, 0.0 <= m.x1 + m.x2 - m.d - + (-1030.0)*(1 - m.d1.binary_indicator_var)) + # [ESJ 11/23/22]: It's really hard to use assertExpressionsEqual on the + # ub constraints because SumExpressions are sharing args, I think. So + # when they get constructed in the transformation (because they come + # after the lb constraints), there are nested SumExpressions. Instead of + # trying to reproduce them I am just building a "flat" SumExpression + # with generate_standard_repn and comparing that. + self.assertIsNone(ub.lower) + self.assertEqual(ub.upper, 0) + repn = generate_standard_repn(ub.body) + self.assertTrue(repn.is_linear()) + simplified = repn.constant + sum( + repn.linear_coefs[i]*repn.linear_vars[i] + for i in range(len(repn.linear_vars))) + assertExpressionsEqual( + self, + simplified, + m.x1 + m.x2 - m.d + 1030.0*m.d1.binary_indicator_var - 1030.0) + + cons = mbm.get_transformed_constraints(m.d2.func) + self.assertEqual(len(cons), 2) + lb = cons[0] + ub = cons[1] + print(lb.expr) + assertExpressionsEqual(self, lb.expr, 0.0 <= 2*m.x1 + 4*m.x2 + 7 - m.d - + (-1093.0)*(1 - m.d2.binary_indicator_var)) + self.assertIsNone(ub.lower) + self.assertEqual(ub.upper, 0) + repn = generate_standard_repn(ub.body) + self.assertTrue(repn.is_linear()) + simplified = repn.constant + sum( + repn.linear_coefs[i]*repn.linear_vars[i] + for i in range(len(repn.linear_vars))) + assertExpressionsEqual( + self, + simplified, + 2*m.x1 + 4*m.x2 - m.d + 1107.0*m.d2.binary_indicator_var - 1100.0) + + cons = mbm.get_transformed_constraints(m.d3.func) + self.assertEqual(len(cons), 2) + lb = cons[0] + ub = cons[1] + print(lb.expr) + assertExpressionsEqual(self, lb.expr, 0.0 <= m.x1 - 5*m.x2 - 3 - m.d - + (-1113.0)*(1 - m.d3.binary_indicator_var)) + self.assertIsNone(ub.lower) + self.assertEqual(ub.upper, 0) + repn = generate_standard_repn(ub.body) + self.assertTrue(repn.is_linear()) + simplified = repn.constant + sum( + repn.linear_coefs[i]*repn.linear_vars[i] + for i in range(len(repn.linear_vars))) + assertExpressionsEqual( + self, + simplified, + m.x1 - 5*m.x2 - m.d + 1107.0*m.d3.binary_indicator_var - 1110.0) From 3d5500f2a897086a6d18aaa2bd2c7caf74d91578 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 23 Nov 2022 12:13:31 -0700 Subject: [PATCH 5/8] Removing debugging --- pyomo/gdp/plugins/multiple_bigm.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index dfe92278547..1003144a542 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -581,10 +581,6 @@ def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): for (c, disj) in lower_bound_constraints_by_var[v]: relaxationBlock._constraintMap['srcConstraints'][ transformed[idx, 'lb']].append(c) - print(disj) - print(disj.transformation_block.name) - print(c) - print('===============') disj.transformation_block._constraintMap[ 'transformedConstraints'][c] = [transformed[idx, 'lb']] if len(upper_dict) > 0: From 45c7b4886761acb335a73d37289bfb06d1072cdf Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 23 Nov 2022 13:01:13 -0700 Subject: [PATCH 6/8] Bugfix so that mbm can accept bigm-style bigM arguments for the constraints that will be transformed with bigm --- pyomo/gdp/plugins/multiple_bigm.py | 1 + pyomo/gdp/tests/test_mbigm.py | 108 +++++++++++++++++++---------- 2 files changed, 71 insertions(+), 38 deletions(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 1003144a542..24e3cd9e7f6 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -432,6 +432,7 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): bigm = TransformationFactory('gdp.bigm') bigm.assume_fixed_vars_permanent = self._config.\ assume_fixed_vars_permanent + bigm.used_args = self.used_args for i in sorted(obj.keys()): c = obj[i] diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index e9b7d4e594d..6d2208a5035 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -645,33 +645,13 @@ def test_logical_constraints_on_disjuncts(self): check_linear_coef(self, repn, m.d2.binary_indicator_var, -1) check_linear_coef(self, repn, m.d3.binary_indicator_var, -1) - def test_only_multiple_bigm_bound_constraints(self): - m = self.make_model() - mbm = TransformationFactory('gdp.mbigm') - mbm.apply_to(m, only_mbigm_bound_constraints=True) - - cons = mbm.get_transformed_constraints(m.d1.x1_bounds) - self.assertEqual(len(cons), 2) - self.check_pretty_bound_constraints(cons[0], m.x1, {m.d1: 0.5, m.d2: - 0.65, m.d3: 2}, - lb=True) - self.check_pretty_bound_constraints(cons[1], m.x1, {m.d1: 2, m.d2: 3, - m.d3: 10}, lb=False) - - cons = mbm.get_transformed_constraints(m.d1.x2_bounds) - self.assertEqual(len(cons), 2) - self.check_pretty_bound_constraints(cons[0], m.x2, {m.d1: 0.75, m.d2: 3, - m.d3: 0.55}, - lb=True) - self.check_pretty_bound_constraints(cons[1], m.x2, {m.d1: 3, m.d2: 10, - m.d3: 1}, lb=False) - + def check_traditionally_bigmed_constraints(self, m, mbm, Ms): cons = mbm.get_transformed_constraints(m.d1.func) self.assertEqual(len(cons), 2) lb = cons[0] ub = cons[1] assertExpressionsEqual(self, lb.expr, 0.0 <= m.x1 + m.x2 - m.d - - (-1030.0)*(1 - m.d1.binary_indicator_var)) + Ms[m.d1][0]*(1 - m.d1.binary_indicator_var)) # [ESJ 11/23/22]: It's really hard to use assertExpressionsEqual on the # ub constraints because SumExpressions are sharing args, I think. So # when they get constructed in the transformation (because they come @@ -685,18 +665,16 @@ def test_only_multiple_bigm_bound_constraints(self): simplified = repn.constant + sum( repn.linear_coefs[i]*repn.linear_vars[i] for i in range(len(repn.linear_vars))) - assertExpressionsEqual( - self, - simplified, - m.x1 + m.x2 - m.d + 1030.0*m.d1.binary_indicator_var - 1030.0) + assertExpressionsEqual(self, simplified, m.x1 + m.x2 - m.d + + Ms[m.d1][1]*m.d1.binary_indicator_var - + Ms[m.d1][1]) cons = mbm.get_transformed_constraints(m.d2.func) self.assertEqual(len(cons), 2) lb = cons[0] ub = cons[1] - print(lb.expr) assertExpressionsEqual(self, lb.expr, 0.0 <= 2*m.x1 + 4*m.x2 + 7 - m.d - - (-1093.0)*(1 - m.d2.binary_indicator_var)) + Ms[m.d2][0]*(1 - m.d2.binary_indicator_var)) self.assertIsNone(ub.lower) self.assertEqual(ub.upper, 0) repn = generate_standard_repn(ub.body) @@ -704,18 +682,16 @@ def test_only_multiple_bigm_bound_constraints(self): simplified = repn.constant + sum( repn.linear_coefs[i]*repn.linear_vars[i] for i in range(len(repn.linear_vars))) - assertExpressionsEqual( - self, - simplified, - 2*m.x1 + 4*m.x2 - m.d + 1107.0*m.d2.binary_indicator_var - 1100.0) + assertExpressionsEqual(self, simplified, 2*m.x1 + 4*m.x2 - m.d + + Ms[m.d2][1]*m.d2.binary_indicator_var - + (Ms[m.d2][1] - 7)) cons = mbm.get_transformed_constraints(m.d3.func) self.assertEqual(len(cons), 2) lb = cons[0] ub = cons[1] - print(lb.expr) assertExpressionsEqual(self, lb.expr, 0.0 <= m.x1 - 5*m.x2 - 3 - m.d - - (-1113.0)*(1 - m.d3.binary_indicator_var)) + Ms[m.d3][0]*(1 - m.d3.binary_indicator_var)) self.assertIsNone(ub.lower) self.assertEqual(ub.upper, 0) repn = generate_standard_repn(ub.body) @@ -723,7 +699,63 @@ def test_only_multiple_bigm_bound_constraints(self): simplified = repn.constant + sum( repn.linear_coefs[i]*repn.linear_vars[i] for i in range(len(repn.linear_vars))) - assertExpressionsEqual( - self, - simplified, - m.x1 - 5*m.x2 - m.d + 1107.0*m.d3.binary_indicator_var - 1110.0) + assertExpressionsEqual(self, simplified, m.x1 - 5*m.x2 - m.d + + Ms[m.d3][1]*m.d3.binary_indicator_var - + (Ms[m.d3][1] + 3)) + + def test_only_multiple_bigm_bound_constraints(self): + m = self.make_model() + mbm = TransformationFactory('gdp.mbigm') + mbm.apply_to(m, only_mbigm_bound_constraints=True) + + cons = mbm.get_transformed_constraints(m.d1.x1_bounds) + self.assertEqual(len(cons), 2) + self.check_pretty_bound_constraints(cons[0], m.x1, {m.d1: 0.5, m.d2: + 0.65, m.d3: 2}, + lb=True) + self.check_pretty_bound_constraints(cons[1], m.x1, {m.d1: 2, m.d2: 3, + m.d3: 10}, lb=False) + + cons = mbm.get_transformed_constraints(m.d1.x2_bounds) + self.assertEqual(len(cons), 2) + self.check_pretty_bound_constraints(cons[0], m.x2, {m.d1: 0.75, m.d2: 3, + m.d3: 0.55}, + lb=True) + self.check_pretty_bound_constraints(cons[1], m.x2, {m.d1: 3, m.d2: 10, + m.d3: 1}, lb=False) + + self.check_traditionally_bigmed_constraints( + m, + mbm, + {m.d1: (-1030.0, 1030.0), + m.d2: (-1093.0, 1107.0), + m.d3: (-1113.0, 1107.0)}) + + def test_only_multiple_bigm_bound_constraints_arg_Ms(self): + m = self.make_model() + mbm = TransformationFactory('gdp.mbigm') + Ms = {m.d1: 1050, m.d2.func: (-2000, 1200), None: 4000} + mbm.apply_to(m, only_mbigm_bound_constraints=True, bigM=Ms) + + cons = mbm.get_transformed_constraints(m.d1.x1_bounds) + self.assertEqual(len(cons), 2) + self.check_pretty_bound_constraints(cons[0], m.x1, {m.d1: 0.5, m.d2: + 0.65, m.d3: 2}, + lb=True) + self.check_pretty_bound_constraints(cons[1], m.x1, {m.d1: 2, m.d2: 3, + m.d3: 10}, lb=False) + + cons = mbm.get_transformed_constraints(m.d1.x2_bounds) + self.assertEqual(len(cons), 2) + self.check_pretty_bound_constraints(cons[0], m.x2, {m.d1: 0.75, m.d2: 3, + m.d3: 0.55}, + lb=True) + self.check_pretty_bound_constraints(cons[1], m.x2, {m.d1: 3, m.d2: 10, + m.d3: 1}, lb=False) + + self.check_traditionally_bigmed_constraints( + m, + mbm, + {m.d1: (-1050, 1050), + m.d2: (-2000, 1200), + m.d3: (-4000, 4000)}) From 65e684aae19a16551e00c1826f2f4ac38022c388 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 23 Nov 2022 13:07:01 -0700 Subject: [PATCH 7/8] Adding some documentation for bigM arg --- pyomo/gdp/plugins/multiple_bigm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 24e3cd9e7f6..087bf398baa 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -105,7 +105,9 @@ class MultipleBigMTransformation(Transformation): Note: Unlike in the bigm transformation, we require the keys in this mapping specify the components the M value applies to exactly in order - to avoid ambiguity. + to avoid ambiguity. However, if the 'only_mbigm_bound_constraints' + option is True, this argument can be used as it would be in the + traditional bigm transformation for the non-bound constraints. """ )) CONFIG.declare('reduce_bound_constraints', ConfigValue( From 882e61a6cc52e2cdc5caabcfd84eb35e2f50e86a Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 28 Nov 2022 13:55:57 -0700 Subject: [PATCH 8/8] Fixing merge conflicts --- pyomo/gdp/plugins/multiple_bigm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 75f0804f0d2..f306aad8279 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -192,6 +192,7 @@ def __init__(self): } self._transformation_blocks = {} self._algebraic_constraints = {} + self._arg_list = {} def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() @@ -201,6 +202,7 @@ def _apply_to(self, instance, **kwds): self.used_args.clear() self._transformation_blocks.clear() self._algebraic_constraints.clear() + self._arg_list.clear() def _apply_to_impl(self, instance, **kwds): if not instance.ctype in (Block, Disjunct):