1#  ___________________________________________________________________________
2#
3#  Pyomo: Python Optimization Modeling Objects
4#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
5#  Under the terms of Contract DE-NA0003525 with National Technology and
6#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
7#  rights in this software.
8#  This software is distributed under the 3-clause BSD License.
9#  ___________________________________________________________________________
10
11
12from pyomo.environ import (
13    TransformationFactory, ConcreteModel, Constraint, Var, Objective,
14    Block, Any, RangeSet, Expression, value, BooleanVar, SolverFactory,
15    TerminationCondition
16)
17from pyomo.gdp import Disjunct, Disjunction, GDP_Error
18from pyomo.core.base import constraint, ComponentUID
19from pyomo.core.base.block import _BlockData
20from pyomo.repn import generate_standard_repn
21import pyomo.gdp.tests.models as models
22from io import StringIO
23import random
24
25import pyomo.opt
26linear_solvers = pyomo.opt.check_available_solvers(
27    'glpk','cbc','gurobi','cplex')
28
29# utility functions
30
31def check_linear_coef(self, repn, var, coef):
32    # Map logical variables to their Boolean counterparts
33    if isinstance(var, BooleanVar):
34        var = var.get_associated_binary()
35
36    # utility used to check a variable-coefficient pair in a standard_repn
37    var_id = None
38    for i,v in enumerate(repn.linear_vars):
39        if v is var:
40            var_id = i
41    self.assertIsNotNone(var_id)
42    self.assertEqual(repn.linear_coefs[var_id], coef)
43
44def diff_apply_to_and_create_using(self, model, transformation):
45    # compares the pprint from the transformed model after using both apply_to
46    # and create_using to make sure the two do the same thing
47    modelcopy = TransformationFactory(transformation).create_using(model)
48    modelcopy_buf = StringIO()
49    modelcopy.pprint(ostream=modelcopy_buf)
50    modelcopy_output = modelcopy_buf.getvalue()
51
52    # reset the seed for the apply_to call.
53    random.seed(666)
54    TransformationFactory(transformation).apply_to(model)
55    model_buf = StringIO()
56    model.pprint(ostream=model_buf)
57    model_output = model_buf.getvalue()
58    self.assertMultiLineEqual(modelcopy_output, model_output)
59
60def check_relaxation_block(self, m, name, numdisjuncts):
61    # utility for checking the transformation block (this method is generic to
62    # bigm and hull though there is more on the hull transformation block, and
63    # the lbub set differs between the two
64    transBlock = m.component(name)
65    self.assertIsInstance(transBlock, Block)
66    self.assertIsInstance(transBlock.component("relaxedDisjuncts"), Block)
67    self.assertEqual(len(transBlock.relaxedDisjuncts), numdisjuncts)
68
69def checkb0TargetsInactive(self, m):
70    self.assertTrue(m.disjunct1.active)
71    self.assertTrue(m.disjunct1[1,0].active)
72    self.assertTrue(m.disjunct1[1,1].active)
73    self.assertTrue(m.disjunct1[2,0].active)
74    self.assertTrue(m.disjunct1[2,1].active)
75
76    self.assertFalse(m.b[0].disjunct.active)
77    self.assertFalse(m.b[0].disjunct[0].active)
78    self.assertFalse(m.b[0].disjunct[1].active)
79    self.assertTrue(m.b[1].disjunct0.active)
80    self.assertTrue(m.b[1].disjunct1.active)
81
82def checkb0TargetsTransformed(self, m, transformation):
83    trans = TransformationFactory('gdp.%s' % transformation)
84    disjBlock = m.b[0].component(
85        "_pyomo_gdp_%s_reformulation" % transformation).relaxedDisjuncts
86    self.assertEqual(len(disjBlock), 2)
87    self.assertIsInstance(disjBlock[0].component("b[0].disjunct[0].c"),
88                          Constraint)
89    self.assertIsInstance(disjBlock[1].component("b[0].disjunct[1].c"),
90                          Constraint)
91
92    # This relies on the disjunctions being transformed in the same order
93    # every time. This dictionary maps the block index to the list of
94    # pairs of (originalDisjunctIndex, transBlockIndex)
95    pairs = [
96            (0,0),
97            (1,1),
98    ]
99    for i, j in pairs:
100        self.assertIs(m.b[0].disjunct[i].transformation_block(),
101                      disjBlock[j])
102        self.assertIs(trans.get_src_disjunct(disjBlock[j]),
103                      m.b[0].disjunct[i])
104
105# active status checks
106
107def check_user_deactivated_disjuncts(self, transformation):
108    # check that we do not transform a deactivated DisjunctData
109    m = models.makeTwoTermDisj()
110    m.d[0].deactivate()
111    transform = TransformationFactory('gdp.%s' % transformation)
112    transform.apply_to(m, targets=(m,))
113
114    self.assertFalse(m.disjunction.active)
115    self.assertFalse(m.d[1].active)
116
117    rBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation)
118    disjBlock = rBlock.relaxedDisjuncts
119    self.assertEqual(len(disjBlock), 1)
120    self.assertIs(disjBlock[0], m.d[1].transformation_block())
121    self.assertIs(transform.get_src_disjunct(disjBlock[0]), m.d[1])
122
123def check_improperly_deactivated_disjuncts(self, transformation):
124    # check that if a Disjunct is deactivated but its indicator variable is not
125    # fixed to 0, we express our confusion.
126    m = models.makeTwoTermDisj()
127    m.d[0].deactivate()
128    self.assertEqual(value(m.d[0].indicator_var), 0)
129    self.assertTrue(m.d[0].indicator_var.is_fixed())
130    m.d[0].indicator_var.fix(1)
131    self.assertRaisesRegex(
132        GDP_Error,
133        r"The disjunct 'd\[0\]' is deactivated, but the "
134        r"indicator_var is fixed to True. This makes no sense.",
135        TransformationFactory('gdp.%s' % transformation).apply_to,
136        m)
137
138def check_indexed_disjunction_not_transformed(self, m, transformation):
139    # no transformation block, nothing transformed
140    self.assertIsNone(m.component("_pyomo_gdp_%s_transformation"
141                                  % transformation))
142    for idx in m.disjunct:
143        self.assertIsNone(m.disjunct[idx].transformation_block)
144    for idx in m.disjunction:
145        self.assertIsNone(m.disjunction[idx].algebraic_constraint)
146
147def check_do_not_transform_userDeactivated_indexedDisjunction(self,
148                                                              transformation):
149    # check that we do not transform a deactivated disjunction
150    m = models.makeTwoTermIndexedDisjunction()
151    # If you truly want to transform nothing, deactivate everything
152    m.disjunction.deactivate()
153    for idx in m.disjunct:
154        m.disjunct[idx].deactivate()
155    directly = TransformationFactory('gdp.%s' % transformation).create_using(m)
156    check_indexed_disjunction_not_transformed(self, directly, transformation)
157
158    targets = TransformationFactory('gdp.%s' % transformation).create_using(
159        m, targets=(m.disjunction))
160    check_indexed_disjunction_not_transformed(self, targets, transformation)
161
162def check_disjunction_deactivated(self, transformation):
163    # check that we deactivate disjunctions after we transform them
164    m = models.makeTwoTermDisj()
165    TransformationFactory('gdp.%s' % transformation).apply_to(m, targets=(m,))
166
167    oldblock = m.component("disjunction")
168    self.assertIsInstance(oldblock, Disjunction)
169    self.assertFalse(oldblock.active)
170
171def check_disjunctDatas_deactivated(self, transformation):
172    # check that we deactivate disjuncts after we transform them
173    m = models.makeTwoTermDisj()
174    TransformationFactory('gdp.%s' % transformation).apply_to(m, targets=(m,))
175
176    oldblock = m.component("disjunction")
177    self.assertFalse(oldblock.disjuncts[0].active)
178    self.assertFalse(oldblock.disjuncts[1].active)
179
180def check_deactivated_constraints(self, transformation):
181    # test that we deactivate constraints after we transform them
182    m = models.makeTwoTermDisj()
183    TransformationFactory('gdp.%s' % transformation).apply_to(m)
184    oldblock = m.component("d")
185    # old constraints still there, deactivated
186    oldc1 = oldblock[1].component("c1")
187    self.assertIsInstance(oldc1, Constraint)
188    self.assertFalse(oldc1.active)
189
190    oldc2 = oldblock[1].component("c2")
191    self.assertIsInstance(oldc2, Constraint)
192    self.assertFalse(oldc2.active)
193
194    oldc = oldblock[0].component("c")
195    self.assertIsInstance(oldc, Constraint)
196    self.assertFalse(oldc.active)
197
198def check_deactivated_disjuncts(self, transformation):
199    # another test that we deactivated transformed Disjuncts, but this one
200    # includes a SimpleDisjunct as well
201    m = models.makeTwoTermMultiIndexedDisjunction()
202    TransformationFactory('gdp.%s' % transformation).apply_to(m, targets=(m,))
203    # all the disjuncts got transformed, so all should be deactivated
204    for i in m.disjunct.index_set():
205        self.assertFalse(m.disjunct[i].active)
206    self.assertFalse(m.disjunct.active)
207
208def check_deactivated_disjunctions(self, transformation):
209    # another test that we deactivated transformed Disjunctions, but including a
210    # SimpleDisjunction
211    m = models.makeTwoTermMultiIndexedDisjunction()
212    TransformationFactory('gdp.%s' % transformation).apply_to(m, targets=(m,))
213
214    # all the disjunctions got transformed, so they should be
215    # deactivated too
216    for i in m.disjunction.index_set():
217        self.assertFalse(m.disjunction[i].active)
218    self.assertFalse(m.disjunction.active)
219
220def check_do_not_transform_twice_if_disjunction_reactivated(self,
221                                                            transformation):
222    # test that if an already-transformed disjunction is reactivated, we will
223    # not retransform it in a subsequent call to the transformation.
224    m = models.makeTwoTermDisj()
225    # this is a hack, but just diff the pprint from this and from calling
226    # the transformation again.
227    TransformationFactory('gdp.%s' % transformation).apply_to(m)
228    first_buf = StringIO()
229    m.pprint(ostream=first_buf)
230    first_output = first_buf.getvalue()
231
232    TransformationFactory('gdp.%s' % transformation).apply_to(m)
233    second_buf = StringIO()
234    m.pprint(ostream=second_buf)
235    second_output = second_buf.getvalue()
236
237    self.assertMultiLineEqual(first_output, second_output)
238
239    # this is a stupid thing to do, but we should still know not to
240    # retransform because active status is now *not* the source of truth.
241    m.disjunction.activate()
242
243    # This is kind of the wrong error, but I'll live with it: at least we
244    # get an error.
245    self.assertRaisesRegex(
246        GDP_Error,
247        r"The disjunct 'd\[0\]' has been transformed, but a disjunction "
248        r"it appears in has not. Putting the same disjunct in "
249        r"multiple disjunctions is not supported.",
250        TransformationFactory('gdp.%s' % transformation).apply_to,
251        m)
252
253def check_constraints_deactivated_indexedDisjunction(self, transformation):
254    # check that we deactivate transformed constraints
255    m = models.makeTwoTermMultiIndexedDisjunction()
256    TransformationFactory('gdp.%s' % transformation).apply_to(m)
257
258    for i in m.disjunct.index_set():
259        self.assertFalse(m.disjunct[i].c.active)
260
261def check_partial_deactivate_indexed_disjunction(self, transformation):
262    """Test for partial deactivation of an indexed disjunction."""
263    m = ConcreteModel()
264    m.x = Var(bounds=(0, 10))
265    @m.Disjunction([0, 1])
266    def disj(m, i):
267        if i == 0:
268            return [m.x >= 1, m.x >= 2]
269        else:
270            return [m.x >= 3, m.x >= 4]
271
272    m.disj[0].disjuncts[0].indicator_var.fix(1)
273    m.disj[0].disjuncts[1].indicator_var.fix(1)
274    m.disj[0].deactivate()
275    TransformationFactory('gdp.%s' % transformation).apply_to(m)
276    transBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation)
277    self.assertEqual(
278        len(transBlock.disj_xor), 1,
279        "There should only be one XOR constraint generated. Found %s." %
280        len(transBlock.disj_xor))
281
282# transformation block
283
284def check_transformation_block_name_collision(self, transformation):
285    # make sure that if the model already has a block called
286    # _pyomo_gdp_*_relaxation that we come up with a different name for the
287    # transformation block (and put the relaxed disjuncts on it)
288    m = models.makeTwoTermDisj()
289    # add block with the name we are about to try to use
290    m.add_component("_pyomo_gdp_%s_reformulation" % transformation, Block(Any))
291    TransformationFactory('gdp.%s' % transformation).apply_to(m)
292
293    # check that we got a uniquely named block
294    transBlock = m.component("_pyomo_gdp_%s_reformulation_4" % transformation)
295    self.assertIsInstance(transBlock, Block)
296
297    # check that the relaxed disjuncts really are here.
298    disjBlock = transBlock.relaxedDisjuncts
299    self.assertIsInstance(disjBlock, Block)
300    self.assertEqual(len(disjBlock), 2)
301    self.assertIsInstance(disjBlock[0].component("d[0].c"), Constraint)
302    self.assertIsInstance(disjBlock[1].component("d[1].c1"), Constraint)
303    self.assertIsInstance(disjBlock[1].component("d[1].c2"), Constraint)
304
305    # we didn't add to the block that wasn't ours
306    self.assertEqual(len(m.component("_pyomo_gdp_%s_reformulation" %
307                                     transformation)), 0)
308
309# XOR constraints
310
311def check_indicator_vars(self, transformation):
312    # particularly paranoid test checking that the indicator_vars are intact
313    # after transformation
314    m = models.makeTwoTermDisj()
315    TransformationFactory('gdp.%s' % transformation).apply_to(m)
316    oldblock = m.component("d")
317    # have indicator variables on original disjuncts and they are still
318    # active.
319    _binary0 = oldblock[0].binary_indicator_var
320    self.assertIsInstance(_binary0, Var)
321    self.assertTrue(_binary0.active)
322    self.assertTrue(_binary0.is_binary())
323    _binary1 = oldblock[1].binary_indicator_var
324    self.assertIsInstance(_binary1, Var)
325    self.assertTrue(_binary1.active)
326    self.assertTrue(_binary1.is_binary())
327
328def check_xor_constraint(self, transformation):
329    # verify xor constraint for a SimpleDisjunction
330    m = models.makeTwoTermDisj()
331    TransformationFactory('gdp.%s' % transformation).apply_to(m)
332    # make sure we created the xor constraint and put it on the relaxation
333    # block
334    rBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation)
335    xor = rBlock.component("disjunction_xor")
336    self.assertIsInstance(xor, Constraint)
337    self.assertEqual(len(xor), 1)
338    self.assertIs(m.d[0].binary_indicator_var, xor.body.arg(0))
339    self.assertIs(m.d[1].binary_indicator_var, xor.body.arg(1))
340    repn = generate_standard_repn(xor.body)
341    self.assertTrue(repn.is_linear())
342    self.assertEqual(repn.constant, 0)
343    check_linear_coef(self, repn, m.d[0].indicator_var, 1)
344    check_linear_coef(self, repn, m.d[1].indicator_var, 1)
345    self.assertEqual(xor.lower, 1)
346    self.assertEqual(xor.upper, 1)
347
348def check_indexed_xor_constraints(self, transformation):
349    # verify xor constraint for an IndexedDisjunction
350    m = models.makeTwoTermMultiIndexedDisjunction()
351    TransformationFactory('gdp.%s' % transformation).apply_to(m)
352
353    xor = m.component("_pyomo_gdp_%s_reformulation" % transformation).\
354          component("disjunction_xor")
355    self.assertIsInstance(xor, Constraint)
356    for i in m.disjunction.index_set():
357        repn = generate_standard_repn(xor[i].body)
358        self.assertEqual(repn.constant, 0)
359        self.assertTrue(repn.is_linear())
360        self.assertEqual(len(repn.linear_vars), 2)
361        check_linear_coef(
362            self, repn, m.disjunction[i].disjuncts[0].indicator_var, 1)
363        check_linear_coef(
364            self, repn, m.disjunction[i].disjuncts[1].indicator_var, 1)
365        self.assertEqual(xor[i].lower, 1)
366        self.assertEqual(xor[i].upper, 1)
367
368def check_indexed_xor_constraints_with_targets(self, transformation):
369    # check that when we use targets to specfy some DisjunctionDatas in an
370    # IndexedDisjunction, the xor constraint is indexed correctly
371    m = models.makeTwoTermIndexedDisjunction_BoundedVars()
372    TransformationFactory('gdp.%s' % transformation).apply_to(
373        m,
374        targets=[m.disjunction[1],
375                 m.disjunction[3]])
376
377    xorC = m.disjunction[1].algebraic_constraint().parent_component()
378    self.assertIsInstance(xorC, Constraint)
379    self.assertEqual(len(xorC), 2)
380
381    # check the constraints
382    for i in [1,3]:
383        self.assertEqual(xorC[i].lower, 1)
384        self.assertEqual(xorC[i].upper, 1)
385        repn = generate_standard_repn(xorC[i].body)
386        self.assertTrue(repn.is_linear())
387        self.assertEqual(repn.constant, 0)
388        check_linear_coef(self, repn, m.disjunct[i, 0].indicator_var, 1)
389        check_linear_coef(self, repn, m.disjunct[i, 1].indicator_var, 1)
390
391def check_three_term_xor_constraint(self, transformation):
392    # check that the xor constraint has all the indicator variables from a
393    # three-term disjunction
394    m = models.makeThreeTermIndexedDisj()
395    TransformationFactory('gdp.%s' % transformation).apply_to(m)
396
397    xor = m.component("_pyomo_gdp_%s_reformulation" % transformation).\
398          component("disjunction_xor")
399    self.assertIsInstance(xor, Constraint)
400    self.assertEqual(xor[1].lower, 1)
401    self.assertEqual(xor[1].upper, 1)
402    self.assertEqual(xor[2].lower, 1)
403    self.assertEqual(xor[2].upper, 1)
404
405    repn = generate_standard_repn(xor[1].body)
406    self.assertTrue(repn.is_linear())
407    self.assertEqual(repn.constant, 0)
408    self.assertEqual(len(repn.linear_vars), 3)
409    for i in range(3):
410        check_linear_coef(self, repn, m.disjunct[i,1].indicator_var, 1)
411
412    repn = generate_standard_repn(xor[2].body)
413    self.assertTrue(repn.is_linear())
414    self.assertEqual(repn.constant, 0)
415    self.assertEqual(len(repn.linear_vars), 3)
416    for i in range(3):
417        check_linear_coef(self, repn, m.disjunct[i,2].indicator_var, 1)
418
419
420# mappings
421
422def check_xor_constraint_mapping(self, transformation):
423    # test that we correctly map between disjunctions and XOR constraints
424    m = models.makeTwoTermDisj()
425    trans = TransformationFactory('gdp.%s' % transformation)
426    trans.apply_to(m)
427
428    transBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation)
429    self.assertIs( trans.get_src_disjunction(transBlock.disjunction_xor),
430                   m.disjunction)
431    self.assertIs( m.disjunction.algebraic_constraint(),
432                   transBlock.disjunction_xor)
433
434
435def check_xor_constraint_mapping_two_disjunctions(self, transformation):
436    # test that we correctly map between disjunctions and xor constraints when
437    # we have multiple SimpleDisjunctions (probably redundant with the above)
438    m = models.makeDisjunctionOfDisjunctDatas()
439    trans = TransformationFactory('gdp.%s' % transformation)
440    trans.apply_to(m)
441
442    transBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation)
443    transBlock2 = m.component("_pyomo_gdp_%s_reformulation_4" % transformation)
444    self.assertIs( trans.get_src_disjunction(transBlock.disjunction_xor),
445                   m.disjunction)
446    self.assertIs( trans.get_src_disjunction(transBlock2.disjunction2_xor),
447                   m.disjunction2)
448
449    self.assertIs( m.disjunction.algebraic_constraint(),
450                   transBlock.disjunction_xor)
451    self.assertIs( m.disjunction2.algebraic_constraint(),
452                   transBlock2.disjunction2_xor)
453
454def check_disjunct_mapping(self, transformation):
455    # check that we correctly map between Disjuncts and their transformation
456    # blocks
457    m = models.makeTwoTermDisj_Nonlinear()
458    trans = TransformationFactory('gdp.%s' % transformation)
459    trans.apply_to(m)
460
461    disjBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation).\
462                relaxedDisjuncts
463
464    # the disjuncts will always be transformed in the same order,
465    # and d[0] goes first, so we can check in a loop.
466    for i in [0,1]:
467        self.assertIs(disjBlock[i]._srcDisjunct(), m.d[i])
468        self.assertIs(trans.get_src_disjunct(disjBlock[i]), m.d[i])
469
470# targets
471
472def check_only_targets_inactive(self, transformation):
473    # test that we only transform targets (by checking active status)
474    m = models.makeTwoSimpleDisjunctions()
475    TransformationFactory('gdp.%s' % transformation).apply_to(
476        m,
477        targets=[m.disjunction1])
478
479    self.assertFalse(m.disjunction1.active)
480    self.assertIsNotNone(m.disjunction1._algebraic_constraint)
481    # disjunction2 still active
482    self.assertTrue(m.disjunction2.active)
483    self.assertIsNone(m.disjunction2._algebraic_constraint)
484
485    self.assertFalse(m.disjunct1[0].active)
486    self.assertFalse(m.disjunct1[1].active)
487    self.assertFalse(m.disjunct1.active)
488    self.assertTrue(m.disjunct2[0].active)
489    self.assertTrue(m.disjunct2[1].active)
490    self.assertTrue(m.disjunct2.active)
491
492def check_only_targets_get_transformed(self, transformation):
493    # test that we only transform targets (by checking the actual components)
494    m = models.makeTwoSimpleDisjunctions()
495    trans = TransformationFactory('gdp.%s' % transformation)
496    trans.apply_to(
497        m,
498        targets=[m.disjunction1])
499
500    disjBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation).\
501                relaxedDisjuncts
502    # only two disjuncts relaxed
503    self.assertEqual(len(disjBlock), 2)
504    # Note that in hull, these aren't the only components that get created, but
505    # they are a proxy for which disjuncts got relaxed, which is what we want to
506    # check.
507    self.assertIsInstance(disjBlock[0].component("disjunct1[0].c"),
508                          Constraint)
509    self.assertIsInstance(disjBlock[1].component("disjunct1[1].c"),
510                          Constraint)
511
512    pairs = [
513        (0, 0),
514        (1, 1)
515    ]
516    for i, j in pairs:
517        self.assertIs(disjBlock[i], m.disjunct1[j].transformation_block())
518        self.assertIs(trans.get_src_disjunct(disjBlock[i]), m.disjunct1[j])
519
520    self.assertIsNone(m.disjunct2[0].transformation_block)
521    self.assertIsNone(m.disjunct2[1].transformation_block)
522
523def check_target_not_a_component_error(self, transformation):
524    # test error message for crazy targets
525    decoy = ConcreteModel()
526    decoy.block = Block()
527    m = models.makeTwoSimpleDisjunctions()
528    self.assertRaisesRegex(
529        GDP_Error,
530        "Target 'block' is not a component on instance 'unknown'!",
531        TransformationFactory('gdp.%s' % transformation).apply_to,
532        m,
533        targets=[decoy.block])
534
535def check_targets_cannot_be_cuids(self, transformation):
536    # check that we scream if targets are cuids
537    m = models.makeTwoTermDisj()
538    self.assertRaisesRegex(
539        ValueError,
540        r"invalid value for configuration 'targets':\n"
541        r"\tFailed casting \[disjunction\]\n"
542        r"\tto target_list\n"
543        r"\tError: Expected Component or list of Components."
544        r"\n\tReceived %s" % type(ComponentUID(m.disjunction)),
545        TransformationFactory('gdp.%s' % transformation).apply_to,
546        m,
547        targets=[ComponentUID(m.disjunction)])
548
549def check_indexedDisj_targets_inactive(self, transformation):
550    # check that targets are deactivated (when target is IndexedDisjunction)
551    m = models.makeDisjunctionsOnIndexedBlock()
552    TransformationFactory('gdp.%s' % transformation).apply_to(
553        m,
554        targets=[m.disjunction1])
555
556    self.assertFalse(m.disjunction1.active)
557    self.assertFalse(m.disjunction1[1].active)
558    self.assertFalse(m.disjunction1[2].active)
559
560    self.assertFalse(m.disjunct1[1,0].active)
561    self.assertFalse(m.disjunct1[1,1].active)
562    self.assertFalse(m.disjunct1[2,0].active)
563    self.assertFalse(m.disjunct1[2,1].active)
564    self.assertFalse(m.disjunct1.active)
565
566    self.assertTrue(m.b[0].disjunct[0].active)
567    self.assertTrue(m.b[0].disjunct[1].active)
568    self.assertTrue(m.b[1].disjunct0.active)
569    self.assertTrue(m.b[1].disjunct1.active)
570
571def check_indexedDisj_only_targets_transformed(self, transformation):
572    # check that only the targets are transformed (with IndexedDisjunction as
573    # target)
574    m = models.makeDisjunctionsOnIndexedBlock()
575    trans = TransformationFactory('gdp.%s' % transformation)
576    trans.apply_to(
577        m,
578        targets=[m.disjunction1])
579
580    disjBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation).\
581                relaxedDisjuncts
582    self.assertEqual(len(disjBlock), 4)
583    self.assertIsInstance(disjBlock[0].component("disjunct1[1,0].c"),
584                          Constraint)
585    self.assertIsInstance(disjBlock[1].component("disjunct1[1,1].c"),
586                          Constraint)
587    self.assertIsInstance(disjBlock[2].component("disjunct1[2,0].c"),
588                          Constraint)
589    self.assertIsInstance(disjBlock[3].component("disjunct1[2,1].c"),
590                          Constraint)
591
592    # This relies on the disjunctions being transformed in the same order
593    # every time. These are the mappings between the indices of the original
594    # disjuncts and the indices on the indexed block on the transformation
595    # block.
596    pairs = [
597        ((1,0), 0),
598        ((1,1), 1),
599        ((2,0), 2),
600        ((2,1), 3),
601    ]
602    for i, j in pairs:
603        self.assertIs(trans.get_src_disjunct(disjBlock[j]), m.disjunct1[i])
604        self.assertIs(disjBlock[j], m.disjunct1[i].transformation_block())
605
606def check_warn_for_untransformed(self, transformation):
607    # Check that we complain if we find an untransformed Disjunct inside of
608    # another Disjunct we are transforming
609    m = models.makeDisjunctionsOnIndexedBlock()
610    def innerdisj_rule(d, flag):
611        m = d.model()
612        if flag:
613            d.c = Constraint(expr=m.a[1] <= 2)
614        else:
615            d.c = Constraint(expr=m.a[1] >= 65)
616    m.disjunct1[1,1].innerdisjunct = Disjunct([0,1], rule=innerdisj_rule)
617    m.disjunct1[1,1].innerdisjunction = Disjunction([0],
618        rule=lambda a,i: [m.disjunct1[1,1].innerdisjunct[0],
619                          m.disjunct1[1,1].innerdisjunct[1]])
620    # if the disjunction doesn't drive the transformation of the Disjuncts, we
621    # get the error
622    m.disjunct1[1,1].innerdisjunction.deactivate()
623    # This test relies on the order that the component objects of
624    # the disjunct get considered. In this case, the disjunct
625    # causes the error, but in another world, it could be the
626    # disjunction, which is also active.
627    self.assertRaisesRegex(
628        GDP_Error,
629        r"Found active disjunct 'disjunct1\[1,1\].innerdisjunct\[0\]' "
630        r"in disjunct 'disjunct1\[1,1\]'!.*",
631        TransformationFactory('gdp.%s' % transformation).create_using,
632        m,
633        targets=[m.disjunction1[1]])
634    m.disjunct1[1,1].innerdisjunction.activate()
635
636def check_disjData_targets_inactive(self, transformation):
637    # check targets deactivated with DisjunctionData is the target
638    m = models.makeDisjunctionsOnIndexedBlock()
639    TransformationFactory('gdp.%s' % transformation).apply_to(
640        m,
641        targets=[m.disjunction1[2]])
642
643    self.assertIsNotNone(m.disjunction1[2]._algebraic_constraint)
644    self.assertFalse(m.disjunction1[2].active)
645
646    self.assertTrue(m.disjunct1.active)
647    self.assertIsNotNone(m.disjunction1._algebraic_constraint)
648    self.assertTrue(m.disjunct1[1,0].active)
649    self.assertIsNone(m.disjunct1[1,0]._transformation_block)
650    self.assertTrue(m.disjunct1[1,1].active)
651    self.assertIsNone(m.disjunct1[1,1]._transformation_block)
652    self.assertFalse(m.disjunct1[2,0].active)
653    self.assertIsNotNone(m.disjunct1[2,0]._transformation_block)
654    self.assertFalse(m.disjunct1[2,1].active)
655    self.assertIsNotNone(m.disjunct1[2,1]._transformation_block)
656
657    self.assertTrue(m.b[0].disjunct.active)
658    self.assertTrue(m.b[0].disjunct[0].active)
659    self.assertIsNone(m.b[0].disjunct[0]._transformation_block)
660    self.assertTrue(m.b[0].disjunct[1].active)
661    self.assertIsNone(m.b[0].disjunct[1]._transformation_block)
662    self.assertTrue(m.b[1].disjunct0.active)
663    self.assertIsNone(m.b[1].disjunct0._transformation_block)
664    self.assertTrue(m.b[1].disjunct1.active)
665    self.assertIsNone(m.b[1].disjunct1._transformation_block)
666
667def check_disjData_only_targets_transformed(self, transformation):
668    # check that targets are transformed when DisjunctionData is the target
669    m = models.makeDisjunctionsOnIndexedBlock()
670    trans = TransformationFactory('gdp.%s' % transformation)
671    trans.apply_to(
672        m,
673        targets=[m.disjunction1[2]])
674
675    disjBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation).\
676                relaxedDisjuncts
677    self.assertEqual(len(disjBlock), 2)
678    self.assertIsInstance(disjBlock[0].component("disjunct1[2,0].c"),
679                          Constraint)
680    self.assertIsInstance(disjBlock[1].component("disjunct1[2,1].c"),
681                          Constraint)
682
683    # This relies on the disjunctions being transformed in the same order
684    # every time. These are the mappings between the indices of the original
685    # disjuncts and the indices on the indexed block on the transformation
686    # block.
687    pairs = [
688        ((2,0), 0),
689        ((2,1), 1),
690    ]
691    for i, j in pairs:
692        self.assertIs(m.disjunct1[i].transformation_block(), disjBlock[j])
693        self.assertIs(trans.get_src_disjunct(disjBlock[j]), m.disjunct1[i])
694
695def check_indexedBlock_targets_inactive(self, transformation):
696    # check that targets are deactivated when target is an IndexedBlock
697    m = models.makeDisjunctionsOnIndexedBlock()
698    TransformationFactory('gdp.%s' % transformation).apply_to(
699        m,
700        targets=[m.b])
701
702    self.assertTrue(m.disjunct1.active)
703    self.assertTrue(m.disjunct1[1,0].active)
704    self.assertTrue(m.disjunct1[1,1].active)
705    self.assertTrue(m.disjunct1[2,0].active)
706    self.assertTrue(m.disjunct1[2,1].active)
707    self.assertIsNone(m.disjunct1[1,0].transformation_block)
708    self.assertIsNone(m.disjunct1[1,1].transformation_block)
709    self.assertIsNone(m.disjunct1[2,0].transformation_block)
710    self.assertIsNone(m.disjunct1[2,1].transformation_block)
711
712    self.assertFalse(m.b[0].disjunct.active)
713    self.assertFalse(m.b[0].disjunct[0].active)
714    self.assertFalse(m.b[0].disjunct[1].active)
715    self.assertFalse(m.b[1].disjunct0.active)
716    self.assertFalse(m.b[1].disjunct1.active)
717
718def check_indexedBlock_only_targets_transformed(self, transformation):
719    # check that targets are transformed when target is an IndexedBlock
720    m = models.makeDisjunctionsOnIndexedBlock()
721    trans = TransformationFactory('gdp.%s' % transformation)
722    trans.apply_to(
723        m,
724        targets=[m.b])
725
726    disjBlock1 = m.b[0].component(
727        "_pyomo_gdp_%s_reformulation" % transformation).relaxedDisjuncts
728    self.assertEqual(len(disjBlock1), 2)
729    self.assertIsInstance(disjBlock1[0].component("b[0].disjunct[0].c"),
730                          Constraint)
731    self.assertIsInstance(disjBlock1[1].component("b[0].disjunct[1].c"),
732                          Constraint)
733    disjBlock2 = m.b[1].component(
734        "_pyomo_gdp_%s_reformulation" % transformation).relaxedDisjuncts
735    self.assertEqual(len(disjBlock2), 2)
736    self.assertIsInstance(disjBlock2[0].component("b[1].disjunct0.c"),
737                          Constraint)
738    self.assertIsInstance(disjBlock2[1].component("b[1].disjunct1.c"),
739                          Constraint)
740
741    # This relies on the disjunctions being transformed in the same order
742    # every time. This dictionary maps the block index to the list of
743    # pairs of (originalDisjunctIndex, transBlockIndex)
744    pairs = {
745        0:
746        [
747            ('disjunct',0,0),
748            ('disjunct',1,1),
749        ],
750        1:
751        [
752            ('disjunct0',None,0),
753            ('disjunct1',None,1),
754        ]
755    }
756
757    for blocknum, lst in pairs.items():
758        for comp, i, j in lst:
759            original = m.b[blocknum].component(comp)
760            if blocknum == 0:
761                disjBlock = disjBlock1
762            if blocknum == 1:
763                disjBlock = disjBlock2
764            self.assertIs(original[i].transformation_block(), disjBlock[j])
765            self.assertIs(trans.get_src_disjunct(disjBlock[j]), original[i])
766
767def check_blockData_targets_inactive(self, transformation):
768    # test that BlockData target is deactivated
769    m = models.makeDisjunctionsOnIndexedBlock()
770    TransformationFactory('gdp.%s' % transformation).apply_to(
771        m,
772        targets=[m.b[0]])
773
774    checkb0TargetsInactive(self, m)
775
776def check_blockData_only_targets_transformed(self, transformation):
777    # test that BlockData target is transformed
778    m = models.makeDisjunctionsOnIndexedBlock()
779    TransformationFactory('gdp.%s' % transformation).apply_to(
780        m,
781        targets=[m.b[0]])
782    checkb0TargetsTransformed(self, m, transformation)
783
784def check_do_not_transform_deactivated_targets(self, transformation):
785    # test that if a deactivated component is given as a target, we don't
786    # transform it. (This is actually an important test because it is the only
787    # reason to check active status at the beginning of many of the methods in
788    # the transformation like _transform_disjunct and _transform_disjunction. In
789    # the absence of targets, those checks wouldn't be necessary.)
790    m = models.makeDisjunctionsOnIndexedBlock()
791    m.b[1].deactivate()
792    TransformationFactory('gdp.%s' % transformation).apply_to(
793        m,
794        targets=[m.b[0], m.b[1]])
795
796    checkb0TargetsInactive(self, m)
797    checkb0TargetsTransformed(self, m, transformation)
798
799def check_disjunction_data_target(self, transformation):
800    # test that if we transform DisjunctionDatas one at a time, we get what we
801    # expect in terms of using the same transformation block and the indexing of
802    # the xor constraint.
803    m = models.makeThreeTermIndexedDisj()
804    TransformationFactory('gdp.%s' % transformation).apply_to(
805        m, targets=[m.disjunction[2]])
806
807    # we got a transformation block on the model
808    transBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation)
809    self.assertIsInstance(transBlock, Block)
810    self.assertIsInstance(transBlock.component("disjunction_xor"),
811                          Constraint)
812    self.assertIsInstance(transBlock.disjunction_xor[2],
813                          constraint._GeneralConstraintData)
814    self.assertIsInstance(transBlock.component("relaxedDisjuncts"), Block)
815    self.assertEqual(len(transBlock.relaxedDisjuncts), 3)
816
817    # suppose we transform the next one separately
818    TransformationFactory('gdp.%s' % transformation).apply_to(
819        m, targets=[m.disjunction[1]])
820    # we added to the same XOR constraint before
821    self.assertIsInstance(transBlock.disjunction_xor[1],
822                          constraint._GeneralConstraintData)
823    # we used the same transformation block, so we have more relaxed
824    # disjuncts
825    self.assertEqual(len(transBlock.relaxedDisjuncts), 6)
826
827def check_disjunction_data_target_any_index(self, transformation):
828    # check the same as the above, but that it still works when the Disjunction
829    # is indexed by Any.
830    m = ConcreteModel()
831    m.x = Var(bounds=(-100, 100))
832    m.disjunct3 = Disjunct(Any)
833    m.disjunct4 = Disjunct(Any)
834    m.disjunction2=Disjunction(Any)
835    for i in range(2):
836        m.disjunct3[i].cons = Constraint(expr=m.x == 2)
837        m.disjunct4[i].cons = Constraint(expr=m.x <= 3)
838        m.disjunction2[i] = [m.disjunct3[i], m.disjunct4[i]]
839
840        TransformationFactory('gdp.%s' % transformation).apply_to(
841            m, targets=[m.disjunction2[i]])
842
843        if i == 0:
844            check_relaxation_block(self, m, "_pyomo_gdp_%s_reformulation" %
845                                   transformation, 2)
846        if i == 2:
847            check_relaxation_block(self, m, "_pyomo_gdp_%s_reformulation" %
848                                   transformation, 4)
849
850# tests that we treat disjunctions on blocks correctly (the main issue here is
851# that if you were to solve that block post-transformation that you would have
852# the whole transformed model)
853
854def check_xor_constraint_added(self, transformation):
855    # test we put the xor on the transformation block
856    m = models.makeTwoTermDisjOnBlock()
857    TransformationFactory('gdp.%s' % transformation).apply_to(m)
858
859    self.assertIsInstance(
860        m.b.component("_pyomo_gdp_%s_reformulation" % transformation).\
861        component('b.disjunction_xor'), Constraint)
862
863def check_trans_block_created(self, transformation):
864    # check we put the transformation block on the parent block of the
865    # disjunction
866    m = models.makeTwoTermDisjOnBlock()
867    TransformationFactory('gdp.%s' % transformation).apply_to(m)
868
869    # test that the transformation block go created on the model
870    transBlock = m.b.component('_pyomo_gdp_%s_reformulation' % transformation)
871    self.assertIsInstance(transBlock, Block)
872    disjBlock = transBlock.component("relaxedDisjuncts")
873    self.assertIsInstance(disjBlock, Block)
874    self.assertEqual(len(disjBlock), 2)
875    # and that it didn't get created on the model
876    self.assertIsNone(
877        m.component('_pyomo_gdp_%s_reformulation' % transformation))
878
879
880# disjunction generation tests: These all suppose that you are doing some sort
881# of column and constraint generation algorithm, but you are in fact generating
882# Disjunctions and retransforming the model after each addition.
883
884def check_iteratively_adding_to_indexed_disjunction_on_block(self,
885                                                             transformation):
886    # check that we can iteratively add to an IndexedDisjunction and transform
887    # the block it lives on
888    m = ConcreteModel()
889    m.b = Block()
890    m.b.x = Var(bounds=(-100, 100))
891    m.b.firstTerm = Disjunct([1,2])
892    m.b.firstTerm[1].cons = Constraint(expr=m.b.x == 0)
893    m.b.firstTerm[2].cons = Constraint(expr=m.b.x == 2)
894    m.b.secondTerm = Disjunct([1,2])
895    m.b.secondTerm[1].cons = Constraint(expr=m.b.x >= 2)
896    m.b.secondTerm[2].cons = Constraint(expr=m.b.x >= 3)
897    m.b.disjunctionList = Disjunction(Any)
898
899    m.b.obj = Objective(expr=m.b.x)
900
901    for i in range(1,3):
902        m.b.disjunctionList[i] = [m.b.firstTerm[i], m.b.secondTerm[i]]
903
904        TransformationFactory('gdp.%s' % transformation).apply_to(m,
905                                                                  targets=[m.b])
906        m.b.disjunctionList[i] = [m.b.firstTerm[i], m.b.secondTerm[i]]
907
908        TransformationFactory('gdp.%s' % transformation).apply_to(m,
909                                                                  targets=[m.b])
910
911        if i == 1:
912            check_relaxation_block(self, m.b, "_pyomo_gdp_%s_reformulation" %
913                                   transformation, 2)
914        if i == 2:
915            check_relaxation_block(self, m.b, "_pyomo_gdp_%s_reformulation" %
916                                   transformation, 4)
917
918def check_simple_disjunction_of_disjunct_datas(self, transformation):
919    # This is actually a reasonable use case if you are generating
920    # disjunctions with the same structure. So you might have Disjuncts
921    # indexed by Any and disjunctions indexed by Any and be adding a
922    # disjunction of two of the DisjunctDatas in every iteration.
923    m = models.makeDisjunctionOfDisjunctDatas()
924    TransformationFactory('gdp.%s' % transformation).apply_to(m)
925
926    self.check_trans_block_disjunctions_of_disjunct_datas(m)
927    transBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation)
928    self.assertIsInstance( transBlock.component("disjunction_xor"),
929                           Constraint)
930    transBlock2 = m.component("_pyomo_gdp_%s_reformulation_4" % transformation)
931    self.assertIsInstance( transBlock2.component("disjunction2_xor"),
932                           Constraint)
933
934# these tests have different checks for what ends up on the model between bigm
935# and hull, but they have the same structure
936def check_iteratively_adding_disjunctions_transform_container(self,
937                                                              transformation):
938    # Check that we can play the same game with iteratively adding Disjunctions,
939    # but this time just specify the IndexedDisjunction as the argument. Note
940    # that the success of this depends on our rebellion regarding the active
941    # status of containers.
942    model = ConcreteModel()
943    model.x = Var(bounds=(-100, 100))
944    model.disjunctionList = Disjunction(Any)
945    model.obj = Objective(expr=model.x)
946    for i in range(2):
947        firstTermName = "firstTerm[%s]" % i
948        model.add_component(firstTermName, Disjunct())
949        model.component(firstTermName).cons = Constraint(
950            expr=model.x == 2*i)
951        secondTermName = "secondTerm[%s]" % i
952        model.add_component(secondTermName, Disjunct())
953        model.component(secondTermName).cons = Constraint(
954            expr=model.x >= i + 2)
955        model.disjunctionList[i] = [model.component(firstTermName),
956                                    model.component(secondTermName)]
957
958        # we're lazy and we just transform the disjunctionList (and in
959        # theory we are transforming at every iteration because we are
960        # solving at every iteration)
961        TransformationFactory('gdp.%s' % transformation).apply_to(
962            model, targets=[model.disjunctionList])
963        if i == 0:
964            self.check_first_iteration(model)
965
966        if i == 1:
967            self.check_second_iteration(model)
968
969def check_disjunction_and_disjuncts_indexed_by_any(self, transformation):
970    # check that we can play the same game when the Disjuncts also are indexed
971    # by Any
972    model = ConcreteModel()
973    model.x = Var(bounds=(-100, 100))
974
975    model.firstTerm = Disjunct(Any)
976    model.secondTerm = Disjunct(Any)
977    model.disjunctionList = Disjunction(Any)
978
979    model.obj = Objective(expr=model.x)
980
981    for i in range(2):
982        model.firstTerm[i].cons = Constraint(expr=model.x == 2*i)
983        model.secondTerm[i].cons = Constraint(expr=model.x >= i + 2)
984        model.disjunctionList[i] = [model.firstTerm[i], model.secondTerm[i]]
985
986        TransformationFactory('gdp.%s' % transformation).apply_to(model)
987
988        if i == 0:
989            self.check_first_iteration(model)
990
991        if i == 1:
992            self.check_second_iteration(model)
993
994def check_iteratively_adding_disjunctions_transform_model(self, transformation):
995    # Same as above, but transforming whole model in every iteration
996    model = ConcreteModel()
997    model.x = Var(bounds=(-100, 100))
998    model.disjunctionList = Disjunction(Any)
999    model.obj = Objective(expr=model.x)
1000    for i in range(2):
1001        firstTermName = "firstTerm[%s]" % i
1002        model.add_component(firstTermName, Disjunct())
1003        model.component(firstTermName).cons = Constraint(
1004            expr=model.x == 2*i)
1005        secondTermName = "secondTerm[%s]" % i
1006        model.add_component(secondTermName, Disjunct())
1007        model.component(secondTermName).cons = Constraint(
1008            expr=model.x >= i + 2)
1009        model.disjunctionList[i] = [model.component(firstTermName),
1010                                    model.component(secondTermName)]
1011
1012        # we're lazy and we just transform the model (and in
1013        # theory we are transforming at every iteration because we are
1014        # solving at every iteration)
1015        TransformationFactory('gdp.%s' % transformation).apply_to(model)
1016        if i == 0:
1017            self.check_first_iteration(model)
1018
1019        if i == 1:
1020            self.check_second_iteration(model)
1021
1022# transforming blocks
1023
1024# If you transform a block as if it is a model, the transformation should
1025# only modify the block you passed it, else when you solve the block, you
1026# are missing the disjunction you thought was on there.
1027def check_transformation_simple_block(self, transformation):
1028    m = models.makeTwoTermDisjOnBlock()
1029    TransformationFactory('gdp.%s' % transformation).apply_to(m.b)
1030
1031    # transformation block not on m
1032    self.assertIsNone(
1033        m.component("_pyomo_gdp_%s_reformulation" % transformation))
1034
1035    # transformation block on m.b
1036    self.assertIsInstance(m.b.component("_pyomo_gdp_%s_reformulation" %
1037                                        transformation), Block)
1038
1039def check_transform_block_data(self, transformation):
1040    m = models.makeDisjunctionsOnIndexedBlock()
1041    TransformationFactory('gdp.%s' % transformation).apply_to(m.b[0])
1042
1043    self.assertIsNone(
1044        m.component("_pyomo_gdp_%s_reformulation" % transformation))
1045
1046    self.assertIsInstance(m.b[0].component("_pyomo_gdp_%s_reformulation" %
1047                                           transformation), Block)
1048
1049def check_simple_block_target(self, transformation):
1050    m = models.makeTwoTermDisjOnBlock()
1051    TransformationFactory('gdp.%s' % transformation).apply_to(m, targets=[m.b])
1052
1053    # transformation block not on m
1054    self.assertIsNone(
1055        m.component("_pyomo_gdp_%s_reformulation" % transformation))
1056
1057    # transformation block on m.b
1058    self.assertIsInstance(m.b.component("_pyomo_gdp_%s_reformulation" %
1059                                        transformation), Block)
1060
1061def check_block_data_target(self, transformation):
1062    m = models.makeDisjunctionsOnIndexedBlock()
1063    TransformationFactory('gdp.%s' % transformation).apply_to(m,
1064                                                              targets=[m.b[0]])
1065
1066    self.assertIsNone(
1067        m.component("_pyomo_gdp_%s_reformulation" % transformation))
1068
1069    self.assertIsInstance(m.b[0].component("_pyomo_gdp_%s_reformulation" %
1070                                           transformation), Block)
1071
1072def check_indexed_block_target(self, transformation):
1073    m = models.makeDisjunctionsOnIndexedBlock()
1074    TransformationFactory('gdp.%s' % transformation).apply_to(m, targets=[m.b])
1075
1076    # We expect the transformation block on each of the BlockDatas. Because
1077    # it is always going on the parent block of the disjunction.
1078
1079    self.assertIsNone(
1080        m.component("_pyomo_gdp_%s_reformulation" % transformation))
1081
1082    for i in [0,1]:
1083        self.assertIsInstance( m.b[i].component("_pyomo_gdp_%s_reformulation" %
1084                                                transformation), Block)
1085
1086def check_block_targets_inactive(self, transformation):
1087    m = models.makeTwoTermDisjOnBlock()
1088    m = models.add_disj_not_on_block(m)
1089    TransformationFactory('gdp.%s' % transformation).apply_to(
1090        m,
1091        targets=[m.b])
1092
1093    self.assertFalse(m.b.disjunct[0].active)
1094    self.assertFalse(m.b.disjunct[1].active)
1095    self.assertFalse(m.b.disjunct.active)
1096    self.assertTrue(m.simpledisj.active)
1097    self.assertTrue(m.simpledisj2.active)
1098
1099def check_block_only_targets_transformed(self, transformation):
1100    m = models.makeTwoTermDisjOnBlock()
1101    m = models.add_disj_not_on_block(m)
1102    trans = TransformationFactory('gdp.%s' % transformation)
1103    trans.apply_to(
1104        m,
1105        targets=[m.b])
1106
1107    disjBlock = m.b.component("_pyomo_gdp_%s_reformulation" % transformation).\
1108                relaxedDisjuncts
1109    self.assertEqual(len(disjBlock), 2)
1110    self.assertIsInstance(disjBlock[0].component("b.disjunct[0].c"),
1111                          Constraint)
1112    self.assertIsInstance(disjBlock[1].component("b.disjunct[1].c"),
1113                          Constraint)
1114
1115    # this relies on the disjuncts being transformed in the same order every
1116    # time
1117    pairs = [
1118        (0,0),
1119        (1,1),
1120    ]
1121    for i, j in pairs:
1122        self.assertIs(m.b.disjunct[i].transformation_block(), disjBlock[j])
1123        self.assertIs(trans.get_src_disjunct(disjBlock[j]), m.b.disjunct[i])
1124
1125# common error messages
1126
1127def check_transform_empty_disjunction(self, transformation):
1128    m = ConcreteModel()
1129    m.empty = Disjunction(expr=[])
1130
1131    self.assertRaisesRegex(
1132        GDP_Error,
1133        "Disjunction 'empty' is empty. This is likely indicative of a "
1134        "modeling error.*",
1135        TransformationFactory('gdp.%s' % transformation).apply_to,
1136        m)
1137
1138def check_deactivated_disjunct_nonzero_indicator_var(self, transformation):
1139    m = ConcreteModel()
1140    m.x = Var(bounds=(0,8))
1141    m.disjunction = Disjunction(expr=[m.x == 0, m.x >= 4])
1142
1143    m.disjunction.disjuncts[0].deactivate()
1144    m.disjunction.disjuncts[0].indicator_var.fix(1)
1145
1146    self.assertRaisesRegex(
1147        GDP_Error,
1148        r"The disjunct 'disjunction_disjuncts\[0\]' is deactivated, but the "
1149        r"indicator_var is fixed to True. This makes no sense.",
1150        TransformationFactory('gdp.%s' % transformation).apply_to,
1151        m)
1152
1153def check_deactivated_disjunct_unfixed_indicator_var(self, transformation):
1154    m = ConcreteModel()
1155    m.x = Var(bounds=(0,8))
1156    m.disjunction = Disjunction(expr=[m.x == 0, m.x >= 4])
1157
1158    m.disjunction.disjuncts[0].deactivate()
1159    m.disjunction.disjuncts[0].indicator_var.fixed = False
1160
1161    self.assertRaisesRegex(
1162        GDP_Error,
1163        r"The disjunct 'disjunction_disjuncts\[0\]' is deactivated, but the "
1164        r"indicator_var is not fixed and the disjunct does not "
1165        r"appear to have been relaxed. This makes no sense. "
1166        r"\(If the intent is to deactivate the disjunct, fix its "
1167        r"indicator_var to False.\)",
1168        TransformationFactory('gdp.%s' % transformation).apply_to,
1169        m)
1170
1171def check_retrieving_nondisjunctive_components(self, transformation):
1172    m = models.makeTwoTermDisj()
1173    m.b = Block()
1174    m.b.global_cons = Constraint(expr=m.a + m.x >= 8)
1175    m.another_global_cons = Constraint(expr=m.a + m.x <= 11)
1176
1177    trans = TransformationFactory('gdp.%s' % transformation)
1178    trans.apply_to(m)
1179
1180    self.assertRaisesRegex(
1181        GDP_Error,
1182        "Constraint 'b.global_cons' is not on a disjunct and so was not "
1183        "transformed",
1184        trans.get_transformed_constraints,
1185        m.b.global_cons)
1186
1187    self.assertRaisesRegex(
1188        GDP_Error,
1189        "Constraint 'b.global_cons' is not a transformed constraint",
1190        trans.get_src_constraint,
1191        m.b.global_cons)
1192
1193    self.assertRaisesRegex(
1194        GDP_Error,
1195        "Constraint 'another_global_cons' is not a transformed constraint",
1196        trans.get_src_constraint,
1197        m.another_global_cons)
1198
1199    self.assertRaisesRegex(
1200        GDP_Error,
1201        "Block 'b' doesn't appear to be a transformation block for a "
1202        "disjunct. No source disjunct found.",
1203        trans.get_src_disjunct,
1204        m.b)
1205
1206    self.assertRaisesRegex(
1207        GDP_Error,
1208        "It appears that 'another_global_cons' is not an XOR or OR"
1209        " constraint resulting from transforming a Disjunction.",
1210        trans.get_src_disjunction,
1211        m.another_global_cons)
1212
1213def check_silly_target(self, transformation):
1214    m = models.makeTwoTermDisj()
1215    self.assertRaisesRegex(
1216        GDP_Error,
1217        r"Target 'd\[1\].c1' was not a Block, Disjunct, or Disjunction. "
1218        r"It was of type "
1219        r"<class 'pyomo.core.base.constraint.ScalarConstraint'> and "
1220        r"can't be transformed.",
1221        TransformationFactory('gdp.%s' % transformation).apply_to,
1222        m,
1223        targets=[m.d[1].c1])
1224
1225def check_ask_for_transformed_constraint_from_untransformed_disjunct(
1226        self, transformation):
1227    m = models.makeTwoTermIndexedDisjunction()
1228    trans = TransformationFactory('gdp.%s' % transformation)
1229    trans.apply_to(m, targets=m.disjunction[1])
1230
1231    self.assertRaisesRegex(
1232        GDP_Error,
1233        r"Constraint 'disjunct\[2,b\].cons_b' is on a disjunct which has "
1234        r"not been transformed",
1235        trans.get_transformed_constraints,
1236        m.disjunct[2, 'b'].cons_b)
1237
1238def check_error_for_same_disjunct_in_multiple_disjunctions(self, transformation):
1239    m = models.makeDisjunctInMultipleDisjunctions()
1240    self.assertRaisesRegex(
1241        GDP_Error,
1242        r"The disjunct 'disjunct1\[1\]' has been transformed, "
1243        r"but a disjunction it appears in has not. Putting the same "
1244        r"disjunct in multiple disjunctions is not supported.",
1245        TransformationFactory('gdp.%s' % transformation).apply_to,
1246        m)
1247
1248def check_cannot_call_transformation_on_disjunction(self, transformation):
1249    m = models.makeTwoTermIndexedDisjunction()
1250    trans = TransformationFactory('gdp.%s' % transformation)
1251    self.assertRaisesRegex(
1252        GDP_Error,
1253        r"Transformation called on disjunction of type "
1254        r"<class 'pyomo.gdp.disjunct.Disjunction'>. 'instance' "
1255        r"must be a ConcreteModel, Block, or Disjunct \(in "
1256        r"the case of nested disjunctions\).",
1257        trans.apply_to,
1258        m.disjunction,
1259        targets=m.disjunction[1]
1260    )
1261
1262# This is really neurotic, but test that we will create an infeasible XOR
1263# constraint. We have to because in the case of nested disjunctions, our model
1264# is not necessarily infeasible because of this. It just might make a Disjunct
1265# infeasible.
1266def setup_infeasible_xor_because_all_disjuncts_deactivated(self, transformation):
1267    m = ConcreteModel()
1268    m.x = Var(bounds=(0,8))
1269    m.y = Var(bounds=(0,7))
1270    m.disjunction = Disjunction(expr=[m.x == 0, m.x >= 4])
1271    m.disjunction_disjuncts[0].nestedDisjunction = Disjunction(
1272        expr=[m.y == 6, m.y <= 1])
1273    # Note that this fixes the indicator variables to 0, but since the
1274    # disjunction is still active, the XOR constraint will be created. So we
1275    # will have to land in the second disjunct of m.disjunction
1276    m.disjunction.disjuncts[0].nestedDisjunction.disjuncts[0].deactivate()
1277    m.disjunction.disjuncts[0].nestedDisjunction.disjuncts[1].deactivate()
1278    # This should create a 0 = 1 XOR constraint, actually...
1279    TransformationFactory('gdp.%s' % transformation).apply_to(
1280        m,
1281        targets=m.disjunction.disjuncts[0].nestedDisjunction)
1282
1283    # check that our XOR is the bad thing it should be.
1284    transBlock = m.disjunction.disjuncts[0].component(
1285        "_pyomo_gdp_%s_reformulation" % transformation)
1286    xor = transBlock.component(
1287        "disjunction_disjuncts[0].nestedDisjunction_xor")
1288    self.assertIsInstance(xor, Constraint)
1289    self.assertEqual(value(xor.lower), 1)
1290    self.assertEqual(value(xor.upper), 1)
1291    repn = generate_standard_repn(xor.body)
1292    for v in repn.linear_vars:
1293        self.assertTrue(v.is_fixed())
1294        self.assertEqual(value(v), 0)
1295
1296    # make sure when we transform the outer thing, all is well
1297    TransformationFactory('gdp.%s' % transformation).apply_to(m)
1298
1299    return m
1300
1301def check_disjunction_target_err(self, transformation):
1302    m = models.makeNestedDisjunctions()
1303    # deactivate the disjunction that would transform the nested Disjuncts so
1304    # that we see it is possible to get the error.
1305    m.simpledisjunct.innerdisjunction.deactivate()
1306    self.assertRaisesRegex(
1307        GDP_Error,
1308        "Found active disjunct 'simpledisjunct.innerdisjunct0' in "
1309        "disjunct 'simpledisjunct'!.*",
1310        TransformationFactory('gdp.%s' % transformation).apply_to,
1311        m,
1312        targets=[m.disjunction])
1313
1314
1315# nested disjunctions: hull and bigm have very different handling for nested
1316# disjunctions, but these tests check *that* everything is transformed, not how
1317
1318def check_disjuncts_inactive_nested(self, transformation):
1319    m = models.makeNestedDisjunctions()
1320    TransformationFactory('gdp.%s' % transformation).apply_to(m, targets=(m,))
1321
1322    self.assertFalse(m.disjunction.active)
1323    self.assertFalse(m.simpledisjunct.active)
1324    self.assertFalse(m.disjunct[0].active)
1325    self.assertFalse(m.disjunct[1].active)
1326    self.assertFalse(m.disjunct.active)
1327
1328def check_deactivated_disjunct_leaves_nested_disjunct_active(self,
1329                                                             transformation):
1330    m = models.makeNestedDisjunctions_FlatDisjuncts()
1331    m.d1.deactivate()
1332    # Specifying 'targets' prevents the HACK_GDP_Disjunct_Reclassifier
1333    # transformation of Disjuncts to Blocks
1334    TransformationFactory('gdp.%s' % transformation).apply_to(m, targets=[m])
1335
1336    self.assertFalse(m.d1.active)
1337    self.assertTrue(m.d1.indicator_var.fixed)
1338    self.assertEqual(m.d1.indicator_var.value, 0)
1339
1340    self.assertFalse(m.d2.active)
1341    self.assertFalse(m.d2.indicator_var.fixed)
1342
1343    self.assertTrue(m.d3.active)
1344    self.assertFalse(m.d3.indicator_var.fixed)
1345
1346    self.assertTrue(m.d4.active)
1347    self.assertFalse(m.d4.indicator_var.fixed)
1348
1349    m = models.makeNestedDisjunctions_NestedDisjuncts()
1350    m.d1.deactivate()
1351    # Specifying 'targets' prevents the HACK_GDP_Disjunct_Reclassifier
1352    # transformation of Disjuncts to Blocks
1353    TransformationFactory('gdp.%s' % transformation).apply_to(m, targets=[m])
1354
1355    self.assertFalse(m.d1.active)
1356    self.assertTrue(m.d1.indicator_var.fixed)
1357    self.assertEqual(m.d1.indicator_var.value, 0)
1358
1359    self.assertFalse(m.d2.active)
1360    self.assertFalse(m.d2.indicator_var.fixed)
1361
1362    self.assertTrue(m.d1.d3.active)
1363    self.assertFalse(m.d1.d3.indicator_var.fixed)
1364
1365    self.assertTrue(m.d1.d4.active)
1366    self.assertFalse(m.d1.d4.indicator_var.fixed)
1367
1368def check_mappings_between_disjunctions_and_xors(self, transformation):
1369    m = models.makeNestedDisjunctions()
1370    transform = TransformationFactory('gdp.%s' % transformation)
1371    transform.apply_to(m)
1372
1373    transBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation)
1374
1375    disjunctionPairs = [
1376        (m.disjunction, transBlock.disjunction_xor),
1377        (m.disjunct[1].innerdisjunction[0],
1378         m.disjunct[1].component("_pyomo_gdp_%s_reformulation"
1379                                 % transformation).\
1380         component("disjunct[1].innerdisjunction_xor")[0]),
1381        (m.simpledisjunct.innerdisjunction,
1382         m.simpledisjunct.component(
1383             "_pyomo_gdp_%s_reformulation" % transformation).component(
1384                 "simpledisjunct.innerdisjunction_xor"))
1385     ]
1386
1387    # check disjunction mappings
1388    for disjunction, xor in disjunctionPairs:
1389        self.assertIs(disjunction.algebraic_constraint(), xor)
1390        self.assertIs(transform.get_src_disjunction(xor), disjunction)
1391
1392def check_disjunct_targets_inactive(self, transformation):
1393    m = models.makeNestedDisjunctions()
1394    TransformationFactory('gdp.%s' % transformation).apply_to(
1395        m,
1396        targets=[m.simpledisjunct])
1397
1398    self.assertTrue(m.disjunct.active)
1399    self.assertTrue(m.disjunct[0].active)
1400    self.assertTrue(m.disjunct[1].active)
1401    self.assertTrue(m.disjunct[1].innerdisjunct.active)
1402    self.assertTrue(m.disjunct[1].innerdisjunct[0].active)
1403    self.assertTrue(m.disjunct[1].innerdisjunct[1].active)
1404
1405    # We basically just treated simpledisjunct as a block. It
1406    # itself has not been transformed and should not be
1407    # deactivated. We just transformed everything in it.
1408    self.assertTrue(m.simpledisjunct.active)
1409    self.assertFalse(m.simpledisjunct.innerdisjunct0.active)
1410    self.assertFalse(m.simpledisjunct.innerdisjunct1.active)
1411
1412def check_disjunct_only_targets_transformed(self, transformation):
1413    m = models.makeNestedDisjunctions()
1414    transform = TransformationFactory('gdp.%s' % transformation)
1415    transform.apply_to(
1416        m,
1417        targets=[m.simpledisjunct])
1418
1419    disjBlock = m.simpledisjunct.component("_pyomo_gdp_%s_reformulation" %
1420                                           transformation).relaxedDisjuncts
1421    self.assertEqual(len(disjBlock), 2)
1422    self.assertIsInstance(
1423        disjBlock[0].component("simpledisjunct.innerdisjunct0.c"),
1424        Constraint)
1425    self.assertIsInstance(
1426        disjBlock[1].component("simpledisjunct.innerdisjunct1.c"),
1427        Constraint)
1428
1429    # This also relies on the disjuncts being transformed in the same
1430    # order every time.
1431    pairs = [
1432        (0,0),
1433        (1,1),
1434    ]
1435    for i, j in pairs:
1436        self.assertIs(m.simpledisjunct.component('innerdisjunct%d'%i),
1437                      transform.get_src_disjunct(disjBlock[j]))
1438        self.assertIs(disjBlock[j],
1439                      m.simpledisjunct.component(
1440                          'innerdisjunct%d'%i).transformation_block())
1441
1442def check_disjunctData_targets_inactive(self, transformation):
1443    m = models.makeNestedDisjunctions()
1444    TransformationFactory('gdp.%s' % transformation).apply_to(
1445        m,
1446        targets=[m.disjunct[1]])
1447
1448    self.assertTrue(m.disjunct[0].active)
1449    self.assertTrue(m.disjunct[1].active)
1450    self.assertTrue(m.disjunct.active)
1451    self.assertFalse(m.disjunct[1].innerdisjunct[0].active)
1452    self.assertFalse(m.disjunct[1].innerdisjunct[1].active)
1453    self.assertFalse(m.disjunct[1].innerdisjunct.active)
1454
1455    self.assertTrue(m.simpledisjunct.active)
1456    self.assertTrue(m.simpledisjunct.innerdisjunct0.active)
1457    self.assertTrue(m.simpledisjunct.innerdisjunct1.active)
1458
1459def check_disjunctData_only_targets_transformed(self, transformation):
1460    m = models.makeNestedDisjunctions()
1461    # This is so convoluted, but you can treat a disjunct like a block:
1462    transform = TransformationFactory('gdp.%s' % transformation)
1463    transform.apply_to(
1464        m,
1465        targets=[m.disjunct[1]])
1466
1467    disjBlock = m.disjunct[1].component("_pyomo_gdp_%s_reformulation" %
1468                                        transformation).relaxedDisjuncts
1469    self.assertEqual(len(disjBlock), 2)
1470    self.assertIsInstance(
1471        disjBlock[0].component("disjunct[1].innerdisjunct[0].c"),
1472        Constraint)
1473    self.assertIsInstance(
1474        disjBlock[1].component("disjunct[1].innerdisjunct[1].c"),
1475        Constraint)
1476
1477    # This also relies on the disjuncts being transformed in the same
1478    # order every time.
1479    pairs = [
1480        (0,0),
1481        (1,1),
1482    ]
1483    for i, j in pairs:
1484        self.assertIs(transform.get_src_disjunct(disjBlock[j]),
1485                      m.disjunct[1].innerdisjunct[i])
1486        self.assertIs(m.disjunct[1].innerdisjunct[i].transformation_block(),
1487                      disjBlock[j])
1488
1489def check_all_components_transformed(self, m):
1490    # checks that all the disjunctive components claim to be transformed in the
1491    # makeNestedDisjunctions_NestedDisjuncts model.
1492    self.assertIsInstance(m.disj.algebraic_constraint(), Constraint)
1493    self.assertIsInstance(m.d1.disj2.algebraic_constraint(), Constraint)
1494    self.assertIsInstance(m.d1.transformation_block(), _BlockData)
1495    self.assertIsInstance(m.d2.transformation_block(), _BlockData)
1496    self.assertIsInstance(m.d1.d3.transformation_block(), _BlockData)
1497    self.assertIsInstance(m.d1.d4.transformation_block(), _BlockData)
1498
1499def check_transformation_blocks_nestedDisjunctions(self, m, transformation):
1500    disjunctionTransBlock = m.disj.algebraic_constraint().parent_block()
1501    transBlocks = disjunctionTransBlock.relaxedDisjuncts
1502    self.assertTrue(len(transBlocks), 4)
1503    self.assertIs(transBlocks[0], m.d1.transformation_block())
1504    self.assertIs(transBlocks[3], m.d2.transformation_block())
1505    if transformation == 'bigm':
1506        # we moved the blocks up
1507        self.assertIs(transBlocks[1], m.d1.d3.transformation_block())
1508        self.assertIs(transBlocks[2], m.d1.d4.transformation_block())
1509    if transformation == 'hull':
1510        # we only moved the references up, these still point to the inner
1511        # transformation blocks
1512        inner = m.d1.disj2.algebraic_constraint().parent_block().\
1513                relaxedDisjuncts
1514        self.assertIs(inner[0], m.d1.d3.transformation_block())
1515        self.assertIs(inner[1], m.d1.d4.transformation_block())
1516
1517def check_nested_disjunction_target(self, transformation):
1518    m = models.makeNestedDisjunctions_NestedDisjuncts()
1519    transform = TransformationFactory('gdp.%s' % transformation)
1520    transform.apply_to(m, targets=[m.disj])
1521
1522    # the bug that inspired this test throws an error while doing the
1523    # transformation, so we'll just do a quick check that all the GDP
1524    # components think they are transformed.
1525    check_all_components_transformed(self, m)
1526    check_transformation_blocks_nestedDisjunctions(self, m, transformation)
1527
1528def check_target_appears_twice(self, transformation):
1529    m = models.makeNestedDisjunctions_NestedDisjuncts()
1530    # Because of the way we preprocess targets, the result here will be that
1531    # m.d1 appears twice in the list of targets. However, this is fine because
1532    # the transformation will not try to retransform anything that has already
1533    # been transformed.
1534    m1 = TransformationFactory('gdp.%s' % transformation).create_using(
1535        m, targets=[m.d1, m.disj])
1536
1537    check_all_components_transformed(self, m1)
1538    # check we have correct number of transformation blocks
1539    check_transformation_blocks_nestedDisjunctions(self, m1, transformation)
1540
1541    # Now check the same thing, but if the already-transformed disjunct appears
1542    # after its disjunction.
1543    TransformationFactory('gdp.%s' % transformation).apply_to( m,
1544                                                               targets=[m.disj,
1545                                                                        m.d1])
1546    check_all_components_transformed(self, m)
1547    check_transformation_blocks_nestedDisjunctions(self, m, transformation)
1548
1549def check_unique_reference_to_nested_indicator_var(self, transformation):
1550    m = models.makeNestedDisjunctions_NestedDisjuncts()
1551    TransformationFactory('gdp.%s' % transformation).apply_to(m)
1552    # find the references to the nested indicator var
1553    num_references_d3 = 0
1554    num_references_d4 = 0
1555    for v in m.component_data_objects(Var, active=True, descend_into=Block):
1556        if v is m.d1.d3.binary_indicator_var:
1557            num_references_d3 += 1
1558        if v is m.d1.d4.binary_indicator_var:
1559            num_references_d4 += 1
1560    self.assertEqual(num_references_d3, 1)
1561    self.assertEqual(num_references_d4, 1)
1562
1563# checks for handling of benign types that could be on disjuncts we're
1564# transforming
1565
1566def check_RangeSet(self, transformation):
1567    m = models.makeDisjunctWithRangeSet()
1568    TransformationFactory('gdp.%s' % transformation).apply_to(m)
1569    self.assertIsInstance(m.d1.s, RangeSet)
1570
1571def check_Expression(self, transformation):
1572    m = models.makeDisjunctWithExpression()
1573    TransformationFactory('gdp.%s' % transformation).apply_to(m)
1574    self.assertIsInstance(m.d1.e, Expression)
1575
1576def check_untransformed_network_raises_GDPError(self, transformation):
1577    m = models.makeNetworkDisjunction()
1578    if transformation == 'bigm':
1579        error_name = 'BigM'
1580    else:
1581        error_name = 'hull'
1582    self.assertRaisesRegex(
1583        GDP_Error,
1584        "No %s transformation handler registered for modeling "
1585        "components of type <class 'pyomo.network.arc.Arc'>. If "
1586        "your disjuncts contain non-GDP Pyomo components that require "
1587        "transformation, please transform them first." % error_name,
1588        TransformationFactory('gdp.%s' % transformation).apply_to,
1589        m)
1590
1591def check_network_disjucts(self, minimize, transformation):
1592    m = models.makeExpandedNetworkDisjunction(minimize=minimize)
1593    TransformationFactory('gdp.%s' % transformation).apply_to(m)
1594    results = SolverFactory(linear_solvers[0]).solve(m)
1595    self.assertEqual(results.solver.termination_condition,
1596                     TerminationCondition.optimal)
1597    if minimize:
1598        self.assertAlmostEqual(value(m.dest.x), 0.42)
1599    else:
1600        self.assertAlmostEqual(value(m.dest.x), 0.84)
1601