Skip to content

Commit 1bd9e6f

Browse files
authored
Merge pull request #2624 from emma58/only-mbm-bound-constraints
Add option to only use multiple-bigm on bound constraints, normal bigm on everything else
2 parents 2ba2606 + beba2b7 commit 1bd9e6f

File tree

3 files changed

+258
-68
lines changed

3 files changed

+258
-68
lines changed

pyomo/gdp/plugins/bigm.py

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,12 @@ def _transform_disjunct(self, obj, bigM, root_disjunct):
368368
# information for both.)
369369
relaxationBlock.bigm_src = {}
370370
relaxationBlock.localVarReferences = Block()
371+
# add the map that will link back and forth between transformed
372+
# constraints and their originals.
373+
relaxationBlock._constraintMap = {
374+
'srcConstraints': ComponentMap(),
375+
'transformedConstraints': ComponentMap()
376+
}
371377
relaxationBlock.transformedConstraints = Constraint(Any)
372378
obj._transformation_block = weakref_ref(relaxationBlock)
373379
relaxationBlock._src_disjunct = weakref_ref(obj)
@@ -438,19 +444,12 @@ def _warn_for_active_disjunct(self, innerdisjunct, outerdisjunct, bigMargs,
438444
arg_list, suffix_list):
439445
_warn_for_active_disjunct(innerdisjunct, outerdisjunct)
440446

441-
def _get_constraint_map_dict(self, transBlock):
442-
if not hasattr(transBlock, "_constraintMap"):
443-
transBlock._constraintMap = {
444-
'srcConstraints': ComponentMap(),
445-
'transformedConstraints': ComponentMap()}
446-
return transBlock._constraintMap
447-
448447
def _transform_constraint(self, obj, disjunct, bigMargs, arg_list,
449448
disjunct_suffix_list):
450449
# add constraint to the transformation block, we'll transform it there.
451450
transBlock = disjunct._transformation_block()
452451
bigm_src = transBlock.bigm_src
453-
constraintMap = self._get_constraint_map_dict(transBlock)
452+
constraintMap = transBlock._constraintMap
454453

455454
disjunctionRelaxationBlock = transBlock.parent_block()
456455

@@ -459,18 +458,9 @@ def _transform_constraint(self, obj, disjunct, bigMargs, arg_list,
459458
# products is kind of expensive (and redundant since we have the
460459
# original model)
461460
newConstraint = transBlock.transformedConstraints
462-
# Since we are both combining components from multiple blocks and using
463-
# local names, we need to make sure that the first index for
464-
# transformedConstraints is guaranteed to be unique. We just grab the
465-
# current length of the list here since that will be monotonically
466-
# increasing and hence unique. We'll append it to the
467-
# slightly-more-human-readable constraint name for something familiar
468-
# but unique.
469-
unique = len(newConstraint)
470461

471462
for i in sorted(obj.keys()):
472463
c = obj[i]
473-
name = c.local_name + "_%s" % unique
474464
if not c.active:
475465
continue
476466

@@ -521,37 +511,54 @@ def _transform_constraint(self, obj, disjunct, bigMargs, arg_list,
521511
# save the source information
522512
bigm_src[c] = (lower, upper)
523513

524-
if c.lower is not None:
525-
if M[0] is None:
526-
raise GDP_Error("Cannot relax disjunctive constraint '%s' "
527-
"because M is not defined." % name)
528-
M_expr = M[0] * (1 - disjunct.binary_indicator_var)
529-
newConstraint.add((name, i, 'lb'), c.lower <= c. body - M_expr)
530-
constraintMap[
531-
'transformedConstraints'][c] = [
532-
newConstraint[name, i, 'lb']]
533-
constraintMap['srcConstraints'][
534-
newConstraint[name, i, 'lb']] = c
535-
if c.upper is not None:
536-
if M[1] is None:
537-
raise GDP_Error("Cannot relax disjunctive constraint '%s' "
538-
"because M is not defined." % name)
539-
M_expr = M[1] * (1 - disjunct.binary_indicator_var)
540-
newConstraint.add((name, i, 'ub'), c.body - M_expr <= c.upper)
541-
transformed = constraintMap['transformedConstraints'].get(c)
542-
if transformed is not None:
543-
constraintMap['transformedConstraints'][
544-
c].append(newConstraint[name, i, 'ub'])
545-
else:
546-
constraintMap[
547-
'transformedConstraints'][c] = [
548-
newConstraint[name, i, 'ub']]
549-
constraintMap['srcConstraints'][
550-
newConstraint[name, i, 'ub']] = c
514+
self._add_constraint_expressions(c, i, M,
515+
disjunct.binary_indicator_var,
516+
newConstraint, constraintMap)
551517

552518
# deactivate because we relaxed
553519
c.deactivate()
554520

521+
def _add_constraint_expressions(self, c, i, M, indicator_var, newConstraint,
522+
constraintMap):
523+
# Since we are both combining components from multiple blocks and using
524+
# local names, we need to make sure that the first index for
525+
# transformedConstraints is guaranteed to be unique. We just grab the
526+
# current length of the list here since that will be monotonically
527+
# increasing and hence unique. We'll append it to the
528+
# slightly-more-human-readable constraint name for something familiar
529+
# but unique. (Note that we really could do this outside of the loop
530+
# over the constraint indices, but I don't think it matters a lot.)
531+
unique = len(newConstraint)
532+
name = c.local_name + "_%s" % unique
533+
534+
if c.lower is not None:
535+
if M[0] is None:
536+
raise GDP_Error("Cannot relax disjunctive constraint '%s' "
537+
"because M is not defined." % name)
538+
M_expr = M[0] * (1 - indicator_var)
539+
newConstraint.add((name, i, 'lb'), c.lower <= c. body - M_expr)
540+
constraintMap[
541+
'transformedConstraints'][c] = [
542+
newConstraint[name, i, 'lb']]
543+
constraintMap['srcConstraints'][
544+
newConstraint[name, i, 'lb']] = c
545+
if c.upper is not None:
546+
if M[1] is None:
547+
raise GDP_Error("Cannot relax disjunctive constraint '%s' "
548+
"because M is not defined." % name)
549+
M_expr = M[1] * (1 - indicator_var)
550+
newConstraint.add((name, i, 'ub'), c.body - M_expr <= c.upper)
551+
transformed = constraintMap['transformedConstraints'].get(c)
552+
if transformed is not None:
553+
constraintMap['transformedConstraints'][
554+
c].append(newConstraint[name, i, 'ub'])
555+
else:
556+
constraintMap[
557+
'transformedConstraints'][c] = [
558+
newConstraint[name, i, 'ub']]
559+
constraintMap['srcConstraints'][
560+
newConstraint[name, i, 'ub']] = c
561+
555562
def _process_M_value(self, m, lower, upper, need_lower, need_upper, src,
556563
key, constraint, from_args=False):
557564
m = _convert_M_to_tuple(m, constraint)

pyomo/gdp/plugins/multiple_bigm.py

Lines changed: 91 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from pyomo.common.modeling import unique_component_name
1919

2020
from pyomo.core import (
21-
Binary, Block, BooleanVar, Connector, Constraint, Expression,
21+
Any, Binary, Block, BooleanVar, Connector, Constraint, Expression,
2222
ExternalFunction, maximize, minimize, NonNegativeIntegers, Objective,
2323
Param, RangeSet, Set, SetOf, SortComponents, Suffix, value, Var
2424
)
@@ -105,7 +105,9 @@ class MultipleBigMTransformation(Transformation):
105105
106106
Note: Unlike in the bigm transformation, we require the keys in this
107107
mapping specify the components the M value applies to exactly in order
108-
to avoid ambiguity.
108+
to avoid ambiguity. However, if the 'only_mbigm_bound_constraints'
109+
option is True, this argument can be used as it would be in the
110+
traditional bigm transformation for the non-bound constraints.
109111
"""
110112
))
111113
CONFIG.declare('reduce_bound_constraints', ConfigValue(
@@ -140,6 +142,25 @@ class MultipleBigMTransformation(Transformation):
140142
Techniques," SIAM Review, vol. 57, no. 1, 2015, pp. 3-57.
141143
"""
142144
))
145+
CONFIG.declare('only_mbigm_bound_constraints', ConfigValue(
146+
default=False,
147+
domain=bool,
148+
description="Flag indicating if only bound constraints should be "
149+
"transformed with multiple-bigm, or if all the disjunctive "
150+
"constraints should.",
151+
doc="""
152+
Sometimes it is only computationally advantageous to apply multiple-
153+
bigm to disjunctive constraints with the special structure:
154+
155+
[l_1 <= x <= u_1] v [l_2 <= x <= u_2] v ... v [l_K <= x <= u_K],
156+
157+
and transform other disjunctive constraints with the traditional
158+
big-M transformation. This flag is used to set the above behavior.
159+
160+
Note that the reduce_bound_constraints flag must also be True when
161+
this flag is set to True.
162+
"""
163+
))
143164

144165
def __init__(self):
145166
super(MultipleBigMTransformation, self).__init__()
@@ -171,6 +192,7 @@ def __init__(self):
171192
}
172193
self._transformation_blocks = {}
173194
self._algebraic_constraints = {}
195+
self._arg_list = {}
174196

175197
def _apply_to(self, instance, **kwds):
176198
self.used_args = ComponentMap()
@@ -180,6 +202,7 @@ def _apply_to(self, instance, **kwds):
180202
self.used_args.clear()
181203
self._transformation_blocks.clear()
182204
self._algebraic_constraints.clear()
205+
self._arg_list.clear()
183206

184207
def _apply_to_impl(self, instance, **kwds):
185208
if not instance.ctype in (Block, Disjunct):
@@ -191,6 +214,15 @@ def _apply_to_impl(self, instance, **kwds):
191214
self._config = self.CONFIG(kwds.pop('options', {}))
192215
self._config.set_value(kwds)
193216

217+
if (self._config.only_mbigm_bound_constraints and not
218+
self._config.reduce_bound_constraints):
219+
raise GDP_Error("The 'only_mbigm_bound_constraints' option is set "
220+
"to True, but the 'reduce_bound_constraints' "
221+
"option is not. This is not supported--please also "
222+
"set 'reduce_bound_constraints' to True if you "
223+
"only wish to transform the bound constraints with "
224+
"multiple bigm.")
225+
194226
targets = self._config.targets
195227
knownBlocks = {}
196228
if targets is None:
@@ -272,9 +304,12 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct,
272304
transformed_constraints = self._transform_bound_constraints(
273305
active_disjuncts, transBlock, arg_Ms)
274306

275-
Ms = transBlock.calculated_missing_m_values = self.\
276-
_calculate_missing_M_values(active_disjuncts, arg_Ms, transBlock,
277-
transformed_constraints)
307+
Ms = arg_Ms
308+
if not self._config.only_mbigm_bound_constraints:
309+
Ms = transBlock.calculated_missing_m_values = self.\
310+
_calculate_missing_M_values(active_disjuncts, arg_Ms,
311+
transBlock,
312+
transformed_constraints)
278313

279314
# Now we can deactivate the constraints we deferred, so that we don't
280315
# re-transform them
@@ -396,31 +431,63 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms):
396431
name = unique_component_name(relaxationBlock, obj.getname(
397432
fully_qualified=False))
398433

399-
newConstraint = Constraint(obj.index_set(), transBlock.lbub)
434+
newConstraint = Constraint(Any)
400435
relaxationBlock.add_component(name, newConstraint)
436+
bigm = TransformationFactory('gdp.bigm')
437+
bigm.assume_fixed_vars_permanent = self._config.\
438+
assume_fixed_vars_permanent
439+
bigm.used_args = self.used_args
401440

402441
for i in sorted(obj.keys()):
403442
c = obj[i]
404443
if not c.active:
405444
continue
406445

407-
transformed = []
408-
if c.lower is not None:
409-
rhs = sum(Ms[c,
410-
disj][0]*disj.indicator_var.get_associated_binary()
411-
for disj in active_disjuncts if disj is not disjunct)
412-
newConstraint.add((i, 'lb'), c.body - c.lower >= rhs)
413-
transformed.append(newConstraint[i, 'lb'])
414-
415-
if c.upper is not None:
416-
rhs = sum(Ms[c,
417-
disj][1]*disj.indicator_var.get_associated_binary()
418-
for disj in active_disjuncts if disj is not disjunct)
419-
newConstraint.add((i, 'ub'), c.body - c.upper <= rhs)
420-
transformed.append(newConstraint[i, 'ub'])
421-
for c_new in transformed:
422-
constraintMap['srcConstraints'][c_new] = [c]
423-
constraintMap['transformedConstraints'][c] = transformed
446+
if not self._config.only_mbigm_bound_constraints:
447+
transformed = []
448+
if c.lower is not None:
449+
rhs = sum(
450+
Ms[c,
451+
disj][0]*disj.indicator_var.get_associated_binary()
452+
for disj in active_disjuncts if disj is not disjunct)
453+
newConstraint.add((i, 'lb'), c.body - c.lower >= rhs)
454+
transformed.append(newConstraint[i, 'lb'])
455+
456+
if c.upper is not None:
457+
rhs = sum(
458+
Ms[c,
459+
disj][1]*disj.indicator_var.get_associated_binary()
460+
for disj in active_disjuncts if disj is not disjunct)
461+
newConstraint.add((i, 'ub'), c.body - c.upper <= rhs)
462+
transformed.append(newConstraint[i, 'ub'])
463+
for c_new in transformed:
464+
constraintMap['srcConstraints'][c_new] = [c]
465+
constraintMap['transformedConstraints'][c] = transformed
466+
else:
467+
lower = (None, None, None)
468+
upper = (None, None, None)
469+
470+
if disjunct not in self._arg_list:
471+
self._arg_list[disjunct] = bigm._get_bigm_arg_list(
472+
self._config.bigM, disjunct)
473+
arg_list = self._arg_list[disjunct]
474+
475+
# first, we see if an M value was specified in the arguments.
476+
# (This returns None if not)
477+
lower, upper = bigm._get_M_from_args(c, Ms, arg_list, lower,
478+
upper)
479+
M = (lower[0], upper[0])
480+
481+
# estimate if we don't have what we need
482+
if c.lower is not None and M[0] is None:
483+
M = (bigm._estimate_M(c.body, c)[0] - c.lower, M[1])
484+
lower = (M[0], None, None)
485+
if c.upper is not None and M[1] is None:
486+
M = (M[0], bigm._estimate_M(c.body, c)[1] - c.upper)
487+
upper = (M[1], None, None)
488+
bigm._add_constraint_expressions(
489+
c, i, M, disjunct.indicator_var.get_associated_binary(),
490+
newConstraint, constraintMap)
424491

425492
# deactivate now that we have transformed
426493
c.deactivate()
@@ -549,7 +616,7 @@ def _add_transformation_block(self, block):
549616
# transformed components
550617
transBlockName = unique_component_name(
551618
block,
552-
'_pyomo_gdp_hull_reformulation')
619+
'_pyomo_gdp_mbigm_reformulation')
553620
transBlock = Block()
554621
block.add_component(transBlockName, transBlock)
555622
self._transformation_blocks[block] = transBlock

0 commit comments

Comments
 (0)