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
11import pyomo.common.unittest as unittest
12from pyomo.common.log import LoggingIntercept
13import logging
14
15from pyomo.environ import (TransformationFactory, Block, Set, Constraint, Var,
16                           RealSet, ComponentMap, value, log, ConcreteModel,
17                           Any, Suffix, SolverFactory, RangeSet, Param,
18                           Objective, TerminationCondition, Reference)
19from pyomo.repn import generate_standard_repn
20
21from pyomo.gdp import Disjunct, Disjunction, GDP_Error
22import pyomo.gdp.tests.models as models
23import pyomo.gdp.tests.common_tests as ct
24
25import random
26from io import StringIO
27
28EPS = TransformationFactory('gdp.hull').CONFIG.EPS
29linear_solvers = ct.linear_solvers
30
31class CommonTests:
32    def setUp(self):
33        # set seed so we can test name collisions predictably
34        random.seed(666)
35
36    def diff_apply_to_and_create_using(self, model):
37        ct.diff_apply_to_and_create_using(self, model, 'gdp.hull')
38
39class TwoTermDisj(unittest.TestCase, CommonTests):
40    def setUp(self):
41        # set seed to test unique namer
42        random.seed(666)
43
44    def test_transformation_block(self):
45        m = models.makeTwoTermDisj_Nonlinear()
46        TransformationFactory('gdp.hull').apply_to(m)
47
48        transBlock = m._pyomo_gdp_hull_reformulation
49        self.assertIsInstance(transBlock, Block)
50        lbub = transBlock.lbub
51        self.assertIsInstance(lbub, Set)
52        self.assertEqual(lbub, ['lb', 'ub', 'eq'])
53
54        disjBlock = transBlock.relaxedDisjuncts
55        self.assertIsInstance(disjBlock, Block)
56        self.assertEqual(len(disjBlock), 2)
57
58    def test_transformation_block_name_collision(self):
59        ct.check_transformation_block_name_collision(self, 'hull')
60
61    def test_disaggregated_vars(self):
62        m = models.makeTwoTermDisj_Nonlinear()
63        TransformationFactory('gdp.hull').apply_to(m)
64
65        transBlock = m._pyomo_gdp_hull_reformulation
66        disjBlock = transBlock.relaxedDisjuncts
67        # same on both disjuncts
68        for i in [0,1]:
69            relaxationBlock = disjBlock[i]
70            x = relaxationBlock.disaggregatedVars.x
71            if i == 1: # this disjunct as x, w, and no y
72                w = relaxationBlock.disaggregatedVars.w
73                y = transBlock._disaggregatedVars[0]
74            elif i == 0: # this disjunct as x, y, and no w
75                y = relaxationBlock.disaggregatedVars.y
76                w = transBlock._disaggregatedVars[1]
77            # variables created (w and y can be Vars or VarDatas depending on
78            # the disjunct)
79            self.assertIs(w.ctype, Var)
80            self.assertIsInstance(x, Var)
81            self.assertIs(y.ctype, Var)
82            # the are in reals
83            self.assertIsInstance(w.domain, RealSet)
84            self.assertIsInstance(x.domain, RealSet)
85            self.assertIsInstance(y.domain, RealSet)
86            # they don't have bounds
87            self.assertEqual(w.lb, 0)
88            self.assertEqual(w.ub, 7)
89            self.assertEqual(x.lb, 0)
90            self.assertEqual(x.ub, 8)
91            self.assertEqual(y.lb, -10)
92            self.assertEqual(y.ub, 0)
93
94    def check_furman_et_al_denominator(self, expr, ind_var):
95        self.assertEqual(expr._const, EPS)
96        self.assertEqual(len(expr._args), 1)
97        self.assertEqual(len(expr._coef), 1)
98        self.assertEqual(expr._coef[0], 1 - EPS)
99        self.assertIs(expr._args[0], ind_var)
100
101    def test_transformed_constraint_nonlinear(self):
102        m = models.makeTwoTermDisj_Nonlinear()
103        TransformationFactory('gdp.hull').apply_to(m)
104
105        disjBlock = m._pyomo_gdp_hull_reformulation.relaxedDisjuncts
106
107        # the only constraint on the first block is the non-linear one
108        disj1c = disjBlock[0].component("d[0].c")
109        self.assertIsInstance(disj1c, Constraint)
110        # we only have an upper bound
111        self.assertEqual(len(disj1c), 1)
112        cons = disj1c['ub']
113        self.assertIsNone(cons.lower)
114        self.assertEqual(cons.upper, 0)
115        repn = generate_standard_repn(cons.body)
116        self.assertFalse(repn.is_linear())
117        self.assertEqual(len(repn.linear_vars), 1)
118        # This is a weak test, but as good as any to ensure that the
119        # substitution was done correctly
120        EPS_1 = 1-EPS
121        self.assertEqual(
122            str(cons.body),
123            "(%s*d[0].binary_indicator_var + %s)*("
124            "_pyomo_gdp_hull_reformulation.relaxedDisjuncts[0]."
125            "disaggregatedVars.x"
126            "/(%s*d[0].binary_indicator_var + %s) + "
127            "(_pyomo_gdp_hull_reformulation.relaxedDisjuncts[0]."
128            "disaggregatedVars.y/"
129            "(%s*d[0].binary_indicator_var + %s))**2) - "
130            "%s*(0.0 + 0.0**2)*(1 - d[0].binary_indicator_var) "
131            "- 14.0*d[0].binary_indicator_var"
132            % (EPS_1, EPS, EPS_1, EPS, EPS_1, EPS, EPS))
133
134    def test_transformed_constraints_linear(self):
135        m = models.makeTwoTermDisj_Nonlinear()
136        TransformationFactory('gdp.hull').apply_to(m)
137
138        disjBlock = m._pyomo_gdp_hull_reformulation.relaxedDisjuncts
139
140        # the only constraint on the first block is the non-linear one
141        c1 = disjBlock[1].component("d[1].c1")
142        # has only lb
143        self.assertEqual(len(c1), 1)
144        cons = c1['lb']
145        self.assertIsNone(cons.lower)
146        self.assertEqual(cons.upper, 0)
147        repn = generate_standard_repn(cons.body)
148        self.assertTrue(repn.is_linear())
149        self.assertEqual(len(repn.linear_vars), 2)
150        ct.check_linear_coef(self, repn, disjBlock[1].disaggregatedVars.x, -1)
151        ct.check_linear_coef(self, repn, m.d[1].indicator_var, 2)
152        self.assertEqual(repn.constant, 0)
153        self.assertEqual(disjBlock[1].disaggregatedVars.x.lb, 0)
154        self.assertEqual(disjBlock[1].disaggregatedVars.x.ub, 8)
155
156        c2 = disjBlock[1].component("d[1].c2")
157        # 'eq' is preserved
158        self.assertEqual(len(c2), 1)
159        cons = c2['eq']
160        self.assertEqual(cons.lower, 0)
161        self.assertEqual(cons.upper, 0)
162        repn = generate_standard_repn(cons.body)
163        self.assertTrue(repn.is_linear())
164        self.assertEqual(len(repn.linear_vars), 2)
165        ct.check_linear_coef(self, repn, disjBlock[1].disaggregatedVars.w, 1)
166        ct.check_linear_coef(self, repn, m.d[1].indicator_var, -3)
167        self.assertEqual(repn.constant, 0)
168        self.assertEqual(disjBlock[1].disaggregatedVars.w.lb, 0)
169        self.assertEqual(disjBlock[1].disaggregatedVars.w.ub, 7)
170
171        c3 = disjBlock[1].component("d[1].c3")
172        # bounded inequality is split
173        self.assertEqual(len(c3), 2)
174        cons = c3['lb']
175        self.assertIsNone(cons.lower)
176        self.assertEqual(cons.upper, 0)
177        repn = generate_standard_repn(cons.body)
178        self.assertTrue(repn.is_linear())
179        self.assertEqual(len(repn.linear_vars), 2)
180        ct.check_linear_coef(self, repn, disjBlock[1].disaggregatedVars.x, -1)
181        ct.check_linear_coef(self, repn, m.d[1].indicator_var, 1)
182        self.assertEqual(repn.constant, 0)
183
184        cons = c3['ub']
185        self.assertIsNone(cons.lower)
186        self.assertEqual(cons.upper, 0)
187        repn = generate_standard_repn(cons.body)
188        self.assertTrue(repn.is_linear())
189        self.assertEqual(len(repn.linear_vars), 2)
190        ct.check_linear_coef(self, repn, disjBlock[1].disaggregatedVars.x, 1)
191        ct.check_linear_coef(self, repn, m.d[1].indicator_var, -3)
192        self.assertEqual(repn.constant, 0)
193
194    def check_bound_constraints_on_disjBlock(self, cons, disvar, indvar, lb, ub):
195        self.assertIsInstance(cons, Constraint)
196
197        # both lb and ub
198        self.assertEqual(len(cons), 2)
199        varlb = cons['lb']
200        self.assertIsNone(varlb.lower)
201        self.assertEqual(varlb.upper, 0)
202        repn = generate_standard_repn(varlb.body)
203        self.assertTrue(repn.is_linear())
204        self.assertEqual(repn.constant, 0)
205        self.assertEqual(len(repn.linear_vars), 2)
206        ct.check_linear_coef(self, repn, indvar, lb)
207        ct.check_linear_coef(self, repn, disvar, -1)
208
209        varub = cons['ub']
210        self.assertIsNone(varub.lower)
211        self.assertEqual(varub.upper, 0)
212        repn = generate_standard_repn(varub.body)
213        self.assertTrue(repn.is_linear())
214        self.assertEqual(repn.constant, 0)
215        self.assertEqual(len(repn.linear_vars), 2)
216        ct.check_linear_coef(self, repn, indvar, -ub)
217        ct.check_linear_coef(self, repn, disvar, 1)
218
219    def check_bound_constraints_on_disjunctionBlock(self, varlb, varub, disvar,
220                                                    indvar, lb, ub):
221        self.assertIsNone(varlb.lower)
222        self.assertEqual(varlb.upper, 0)
223        repn = generate_standard_repn(varlb.body)
224        self.assertTrue(repn.is_linear())
225        self.assertEqual(repn.constant, lb)
226        self.assertEqual(len(repn.linear_vars), 2)
227        ct.check_linear_coef(self, repn, indvar, -lb)
228        ct.check_linear_coef(self, repn, disvar, -1)
229
230        self.assertIsNone(varub.lower)
231        self.assertEqual(varub.upper, 0)
232        repn = generate_standard_repn(varub.body)
233        self.assertTrue(repn.is_linear())
234        self.assertEqual(repn.constant, -ub)
235        self.assertEqual(len(repn.linear_vars), 2)
236        ct.check_linear_coef(self, repn, indvar, ub)
237        ct.check_linear_coef(self, repn, disvar, 1)
238
239    def test_disaggregatedVar_bounds(self):
240        m = models.makeTwoTermDisj_Nonlinear()
241        TransformationFactory('gdp.hull').apply_to(m)
242
243        transBlock = m._pyomo_gdp_hull_reformulation
244        disjBlock = transBlock.relaxedDisjuncts
245        for i in [0,1]:
246            # check bounds constraints for each variable on each of the two
247            # disjuncts.
248            self.check_bound_constraints_on_disjBlock(
249                disjBlock[i].x_bounds,
250                disjBlock[i].disaggregatedVars.x,
251                m.d[i].indicator_var, 1, 8)
252            if i == 1: # this disjunct has x, w, and no y
253                self.check_bound_constraints_on_disjBlock(
254                    disjBlock[i].w_bounds,
255                    disjBlock[i].disaggregatedVars.w,
256                    m.d[i].indicator_var, 2, 7)
257                self.check_bound_constraints_on_disjunctionBlock(
258                    transBlock._boundsConstraints[0,'lb'],
259                    transBlock._boundsConstraints[0,'ub'],
260                    transBlock._disaggregatedVars[0],
261                    m.d[0].indicator_var, -10, -3)
262            elif i == 0: # this disjunct has x, y, and no w
263                self.check_bound_constraints_on_disjBlock(
264                    disjBlock[i].y_bounds,
265                    disjBlock[i].disaggregatedVars.y,
266                    m.d[i].indicator_var, -10, -3)
267                self.check_bound_constraints_on_disjunctionBlock(
268                    transBlock._boundsConstraints[1,'lb'],
269                    transBlock._boundsConstraints[1,'ub'],
270                    transBlock._disaggregatedVars[1],
271                    m.d[1].indicator_var, 2, 7)
272
273    def test_error_for_or(self):
274        m = models.makeTwoTermDisj_Nonlinear()
275        m.disjunction.xor = False
276
277        self.assertRaisesRegex(
278            GDP_Error,
279            "Cannot do hull reformulation for Disjunction "
280            "'disjunction' with OR constraint.  Must be an XOR!*",
281            TransformationFactory('gdp.hull').apply_to,
282            m)
283
284    def check_disaggregation_constraint(self, cons, var, disvar1, disvar2):
285        repn = generate_standard_repn(cons.body)
286        self.assertEqual(cons.lower, 0)
287        self.assertEqual(cons.upper, 0)
288        self.assertEqual(len(repn.linear_vars), 3)
289        ct.check_linear_coef(self, repn, var, 1)
290        ct.check_linear_coef(self, repn, disvar1, -1)
291        ct.check_linear_coef(self, repn, disvar2, -1)
292
293    def test_disaggregation_constraint(self):
294        m = models.makeTwoTermDisj_Nonlinear()
295        hull = TransformationFactory('gdp.hull')
296        hull.apply_to(m)
297        transBlock = m._pyomo_gdp_hull_reformulation
298        disjBlock = transBlock.relaxedDisjuncts
299
300        self.check_disaggregation_constraint(
301            hull.get_disaggregation_constraint(m.w, m.disjunction), m.w,
302            disjBlock[1].disaggregatedVars.w, transBlock._disaggregatedVars[1])
303        self.check_disaggregation_constraint(
304            hull.get_disaggregation_constraint(m.x, m.disjunction), m.x,
305            disjBlock[0].disaggregatedVars.x, disjBlock[1].disaggregatedVars.x)
306        self.check_disaggregation_constraint(
307            hull.get_disaggregation_constraint(m.y, m.disjunction), m.y,
308            disjBlock[0].disaggregatedVars.y, transBlock._disaggregatedVars[0])
309
310    def test_xor_constraint_mapping(self):
311        ct.check_xor_constraint_mapping(self, 'hull')
312
313    def test_xor_constraint_mapping_two_disjunctions(self):
314        ct.check_xor_constraint_mapping_two_disjunctions(self, 'hull')
315
316    def test_transformed_disjunct_mappings(self):
317        ct.check_disjunct_mapping(self, 'hull')
318
319    def test_transformed_constraint_mappings(self):
320        # ESJ: Letting bigm and hull test their own constraint mappings
321        # because, though the paradigm is the same, hull doesn't always create
322        # a transformed constraint when it can instead accomplish an x == 0
323        # constraint by fixing the disaggregated variable.
324        m = models.makeTwoTermDisj_Nonlinear()
325        hull = TransformationFactory('gdp.hull')
326        hull.apply_to(m)
327
328        disjBlock = m._pyomo_gdp_hull_reformulation.relaxedDisjuncts
329
330        # first disjunct
331        orig1 = m.d[0].c
332        trans1 = disjBlock[0].component("d[0].c")
333        self.assertIs(hull.get_src_constraint(trans1), orig1)
334        self.assertIs(hull.get_src_constraint(trans1['ub']), orig1)
335        trans_list = hull.get_transformed_constraints(orig1)
336        self.assertEqual(len(trans_list), 1)
337        self.assertIs(trans_list[0], trans1['ub'])
338
339        # second disjunct
340
341        # first constraint
342        orig1 = m.d[1].c1
343        trans1 = disjBlock[1].component("d[1].c1")
344        self.assertIs(hull.get_src_constraint(trans1), orig1)
345        self.assertIs(hull.get_src_constraint(trans1['lb']), orig1)
346        trans_list = hull.get_transformed_constraints(orig1)
347        self.assertEqual(len(trans_list), 1)
348        self.assertIs(trans_list[0], trans1['lb'])
349
350        # second constraint
351        orig2 = m.d[1].c2
352        trans2 = disjBlock[1].component("d[1].c2")
353        self.assertIs(hull.get_src_constraint(trans2), orig2)
354        self.assertIs(hull.get_src_constraint(trans2['eq']), orig2)
355        trans_list = hull.get_transformed_constraints(orig2)
356        self.assertEqual(len(trans_list), 1)
357        self.assertIs(trans_list[0], trans2['eq'])
358
359        # third constraint
360        orig3 = m.d[1].c3
361        trans3 = disjBlock[1].component("d[1].c3")
362        self.assertIs(hull.get_src_constraint(trans3), orig3)
363        self.assertIs(hull.get_src_constraint(trans3['lb']), orig3)
364        self.assertIs(hull.get_src_constraint(trans3['ub']), orig3)
365        trans_list = hull.get_transformed_constraints(orig3)
366        self.assertEqual(len(trans_list), 2)
367        self.assertIs(trans_list[0], trans3['lb'])
368        self.assertIs(trans_list[1], trans3['ub'])
369
370    def test_disaggregatedVar_mappings(self):
371        m = models.makeTwoTermDisj_Nonlinear()
372        hull = TransformationFactory('gdp.hull')
373        hull.apply_to(m)
374
375        transBlock = m._pyomo_gdp_hull_reformulation
376        disjBlock = transBlock.relaxedDisjuncts
377
378        for i in [0,1]:
379            mappings = ComponentMap()
380            mappings[m.x] = disjBlock[i].disaggregatedVars.x
381            if i == 1: # this disjunct as x, w, and no y
382                mappings[m.w] = disjBlock[i].disaggregatedVars.w
383                mappings[m.y] = transBlock._disaggregatedVars[0]
384            elif i == 0: # this disjunct as x, y, and no w
385                mappings[m.y] = disjBlock[i].disaggregatedVars.y
386                mappings[m.w] = transBlock._disaggregatedVars[1]
387
388            for orig, disagg in mappings.items():
389                self.assertIs(hull.get_src_var(disagg), orig)
390                self.assertIs(hull.get_disaggregated_var(orig, m.d[i]), disagg)
391
392    def test_bigMConstraint_mappings(self):
393        m = models.makeTwoTermDisj_Nonlinear()
394        hull = TransformationFactory('gdp.hull')
395        hull.apply_to(m)
396
397        transBlock = m._pyomo_gdp_hull_reformulation
398        disjBlock = transBlock.relaxedDisjuncts
399
400        for i in [0,1]:
401            mappings = ComponentMap()
402            mappings[disjBlock[i].disaggregatedVars.x] = disjBlock[i].x_bounds
403            if i == 1: # this disjunct has x, w, and no y
404                mappings[disjBlock[i].disaggregatedVars.w] = disjBlock[i].\
405                                                             w_bounds
406                mappings[transBlock._disaggregatedVars[0]] = Reference(
407                    transBlock._boundsConstraints[0,...])
408            elif i == 0: # this disjunct has x, y, and no w
409                mappings[disjBlock[i].disaggregatedVars.y] = disjBlock[i].\
410                                                             y_bounds
411                mappings[transBlock._disaggregatedVars[1]] = Reference(
412                    transBlock._boundsConstraints[1,...])
413            for var, cons in mappings.items():
414                returned_cons =  hull.get_var_bounds_constraint(var)
415                # This sometimes refers a reference to the right part of a
416                # larger indexed constraint, so the indexed constraints
417                # themselves might not be the same object. The ConstraintDatas
418                # are though:
419                for key, constraintData in cons.items():
420                    self.assertIs(returned_cons[key], constraintData)
421
422    def test_create_using_nonlinear(self):
423        m = models.makeTwoTermDisj_Nonlinear()
424        self.diff_apply_to_and_create_using(m)
425
426    # [ESJ 02/14/2020] In order to match bigm and the (unfortunate) expectation
427    # we have established, we never decide something is local based on where it
428    # is declared. We treat variables declared on Disjuncts as if they are
429    # declared globally. We need to use the bounds as if they are global and
430    # also disaggregate the variable
431    def test_locally_declared_var_bounds_used_globally(self):
432        m = models.localVar()
433        hull = TransformationFactory('gdp.hull')
434        hull.apply_to(m)
435
436        # check that we used the bounds on the local variable as if they are
437        # global. Which means checking the bounds constraints...
438        y_disagg = m.disj2.transformation_block().disaggregatedVars.y
439        cons = hull.get_var_bounds_constraint(y_disagg)
440        lb = cons['lb']
441        self.assertIsNone(lb.lower)
442        self.assertEqual(value(lb.upper), 0)
443        repn = generate_standard_repn(lb.body)
444        self.assertTrue(repn.is_linear())
445        ct.check_linear_coef(self, repn, m.disj2.indicator_var, 1)
446        ct.check_linear_coef(self, repn, y_disagg, -1)
447
448        ub = cons['ub']
449        self.assertIsNone(ub.lower)
450        self.assertEqual(value(ub.upper), 0)
451        repn = generate_standard_repn(ub.body)
452        self.assertTrue(repn.is_linear())
453        ct.check_linear_coef(self, repn, y_disagg, 1)
454        ct.check_linear_coef(self, repn, m.disj2.indicator_var, -3)
455
456    def test_locally_declared_variables_disaggregated(self):
457        m = models.localVar()
458
459        hull = TransformationFactory('gdp.hull')
460        hull.apply_to(m)
461
462        # two birds one stone: test the mappings too
463        disj1y = hull.get_disaggregated_var(m.disj2.y, m.disj1)
464        disj2y = hull.get_disaggregated_var(m.disj2.y, m.disj2)
465        self.assertIs(disj1y,
466                      m.disj1._transformation_block().parent_block().\
467                      _disaggregatedVars[0])
468        self.assertIs(disj2y,
469                      m.disj2._transformation_block().disaggregatedVars.y)
470        self.assertIs(hull.get_src_var(disj1y), m.disj2.y)
471        self.assertIs(hull.get_src_var(disj2y), m.disj2.y)
472
473    def test_global_vars_local_to_a_disjunction_disaggregated(self):
474        # The point of this is that where a variable is declared has absolutely
475        # nothing to do with whether or not it should be disaggregated. With the
476        # only exception being that we can tell disaggregated variables and we
477        # know they are really and truly local to only one disjunct (EVER, in the
478        # whole model) because we declared them.
479
480        # So here, for some perverse reason, we declare the variables on disj1,
481        # but we use them in disj2. Both of them need to be disaggregated in
482        # both disjunctions though: Neither is local. (And, unless we want to do
483        # a search of the whole model (or disallow this kind of insanity) we
484        # can't be smarter because what if you transformed this one disjunction
485        # at a time? You can never assume a variable isn't used elsewhere in the
486        # model, and if it is, you must disaggregate it.)
487        m = ConcreteModel()
488        m.disj1 = Disjunct()
489        m.disj1.x = Var(bounds=(1, 10))
490        m.disj1.y = Var(bounds=(2, 11))
491        m.disj1.cons1 = Constraint(expr=m.disj1.x + m.disj1.y <= 5)
492        m.disj2 = Disjunct()
493        m.disj2.cons = Constraint(expr=m.disj1.y >= 8)
494        m.disjunction1 = Disjunction(expr=[m.disj1, m.disj2])
495
496        m.disj3 = Disjunct()
497        m.disj3.cons = Constraint(expr=m.disj1.x >= 7)
498        m.disj4 = Disjunct()
499        m.disj4.cons = Constraint(expr=m.disj1.y == 3)
500        m.disjunction2 = Disjunction(expr=[m.disj3, m.disj4])
501
502        hull = TransformationFactory('gdp.hull')
503        hull.apply_to(m)
504        # check that all the variables are disaggregated
505        # disj1 has both x and y
506        disj = m.disj1
507        transBlock = disj.transformation_block()
508        varBlock = transBlock.disaggregatedVars
509        self.assertEqual(len([v for v in
510                              varBlock.component_data_objects(Var)]), 2)
511        x = varBlock.component("x")
512        y = varBlock.component("y")
513        self.assertIsInstance(x, Var)
514        self.assertIsInstance(y, Var)
515        self.assertIs(hull.get_disaggregated_var(m.disj1.x, disj), x)
516        self.assertIs(hull.get_src_var(x), m.disj1.x)
517        self.assertIs(hull.get_disaggregated_var(m.disj1.y, disj), y)
518        self.assertIs(hull.get_src_var(y), m.disj1.y)
519        # disj2 and disj4 have just y
520        for disj in [m.disj2, m.disj4]:
521            transBlock = disj.transformation_block()
522            varBlock = transBlock.disaggregatedVars
523            self.assertEqual(len([v for v in
524                                  varBlock.component_data_objects(Var)]), 1)
525            y = varBlock.component("y")
526            self.assertIsInstance(y, Var)
527            self.assertIs(hull.get_disaggregated_var(m.disj1.y, disj), y)
528            self.assertIs(hull.get_src_var(y), m.disj1.y)
529        # disj3 has just x
530        disj = m.disj3
531        transBlock = disj.transformation_block()
532        varBlock = transBlock.disaggregatedVars
533        self.assertEqual(len([v for v in
534                              varBlock.component_data_objects(Var)]), 1)
535        x = varBlock.component("x")
536        self.assertIsInstance(x, Var)
537        self.assertIs(hull.get_disaggregated_var(m.disj1.x, disj), x)
538        self.assertIs(hull.get_src_var(x), m.disj1.x)
539
540        # there is a spare x on disjunction1's block
541        x2 = m.disjunction1.algebraic_constraint().parent_block().\
542             _disaggregatedVars[0]
543        self.assertIs(hull.get_disaggregated_var(m.disj1.x, m.disj2), x2)
544        self.assertIs(hull.get_src_var(x2), m.disj1.x)
545
546        # and both a spare x and y on disjunction2's block
547        x2 = m.disjunction2.algebraic_constraint().parent_block().\
548             _disaggregatedVars[0]
549        y1 = m.disjunction2.algebraic_constraint().parent_block().\
550             _disaggregatedVars[1]
551        self.assertIs(hull.get_disaggregated_var(m.disj1.x, m.disj4), x2)
552        self.assertIs(hull.get_src_var(x2), m.disj1.x)
553        self.assertIs(hull.get_disaggregated_var(m.disj1.y, m.disj3), y1)
554        self.assertIs(hull.get_src_var(y1), m.disj1.y)
555
556    def check_name_collision_disaggregated_vars(self, m, disj, name):
557        hull = TransformationFactory('gdp.hull')
558        transBlock = disj.transformation_block()
559        varBlock = transBlock.disaggregatedVars
560        self.assertEqual(len([v for v in
561                              varBlock.component_data_objects(Var)]), 2)
562        x = varBlock.component("x")
563        x2 = varBlock.component(name)
564        self.assertIsInstance(x, Var)
565        self.assertIsInstance(x2, Var)
566        self.assertIs(hull.get_disaggregated_var(m.disj1.x, disj), x)
567        self.assertIs(hull.get_src_var(x), m.disj1.x)
568        self.assertIs(hull.get_disaggregated_var(m.x, disj), x2)
569        self.assertIs(hull.get_src_var(x2), m.x)
570
571    def test_disaggregated_var_name_collision(self):
572        # same model as the test above, but now I am putting what was disj1.y as
573        # m.x, just to invite disaster, and adding constraints that involve all
574        # the variables so they will all be disaggregated on the Disjunct
575        m = ConcreteModel()
576        m.x = Var(bounds=(2, 11))
577        m.disj1 = Disjunct()
578        m.disj1.x = Var(bounds=(1, 10))
579        m.disj1.cons1 = Constraint(expr=m.disj1.x + m.x <= 5)
580        m.disj2 = Disjunct()
581        m.disj2.cons = Constraint(expr=m.x >= 8)
582        m.disj2.cons1 = Constraint(expr=m.disj1.x == 3)
583        m.disjunction1 = Disjunction(expr=[m.disj1, m.disj2])
584
585        m.disj3 = Disjunct()
586        m.disj3.cons = Constraint(expr=m.disj1.x >= 7)
587        m.disj3.cons1 = Constraint(expr=m.x >= 10)
588        m.disj4 = Disjunct()
589        m.disj4.cons = Constraint(expr=m.x == 3)
590        m.disj4.cons1 = Constraint(expr=m.disj1.x == 4)
591        m.disjunction2 = Disjunction(expr=[m.disj3, m.disj4])
592
593        hull = TransformationFactory('gdp.hull')
594        hull.apply_to(m)
595        for disj, nm in ((m.disj1, "x_4"), (m.disj2, "x_9"),
596                         (m.disj3, "x_5"), (m.disj4, "x_8")):
597            self.check_name_collision_disaggregated_vars(m, disj, nm)
598
599    def test_do_not_transform_user_deactivated_disjuncts(self):
600        ct.check_user_deactivated_disjuncts(self, 'hull')
601
602    def test_improperly_deactivated_disjuncts(self):
603        ct.check_improperly_deactivated_disjuncts(self, 'hull')
604
605    def test_do_not_transform_userDeactivated_IndexedDisjunction(self):
606        ct.check_do_not_transform_userDeactivated_indexedDisjunction(self,
607                                                                     'hull')
608
609    def test_disjunction_deactivated(self):
610        ct.check_disjunction_deactivated(self, 'hull')
611
612    def test_disjunctDatas_deactivated(self):
613        ct.check_disjunctDatas_deactivated(self, 'hull')
614
615    def test_deactivated_constraints(self):
616        ct.check_deactivated_constraints(self, 'hull')
617
618    def check_no_double_transformation(self):
619        ct.check_do_not_transform_twice_if_disjunction_reactivated(self,
620                                                                   'hull')
621
622    def test_indicator_vars(self):
623        ct.check_indicator_vars(self, 'hull')
624
625    def test_xor_constraints(self):
626        ct.check_xor_constraint(self, 'hull')
627
628    def test_unbounded_var_error(self):
629        m = models.makeTwoTermDisj_Nonlinear()
630        # no bounds
631        m.w.setlb(None)
632        m.w.setub(None)
633        self.assertRaisesRegex(
634            GDP_Error,
635            "Variables that appear in disjuncts must be "
636            "bounded in order to use the hull "
637            "transformation! Missing bound for w.*",
638            TransformationFactory('gdp.hull').apply_to,
639            m)
640
641    def check_threeTermDisj_IndexedConstraints(self, m, lb):
642        transBlock = m._pyomo_gdp_hull_reformulation
643
644        # 2 blocks: the original Disjunct and the transformation block
645        self.assertEqual(
646            len(list(m.component_objects(Block, descend_into=False))), 1)
647        self.assertEqual(
648            len(list(m.component_objects(Disjunct))), 1)
649
650        # Each relaxed disjunct should have i disaggregated vars and i "d[i].c"
651        # Constraints
652        for i in [1,2,3]:
653            relaxed = transBlock.relaxedDisjuncts[i-1]
654            self.assertEqual(
655                len(list(relaxed.disaggregatedVars.component_objects( Var))), i)
656            self.assertEqual(
657                len(list(relaxed.disaggregatedVars.component_data_objects(
658                    Var))), i)
659            # we always have the x[1] bounds constraint, then however many
660            # original constraints were on the Disjunct
661            self.assertEqual(
662                len(list(relaxed.component_objects(Constraint))), 1+i)
663            if lb == 0:
664                # i bounds constraints and i transformed constraints
665                self.assertEqual(
666                    len(list(relaxed.component_data_objects(Constraint))), i+i)
667            else:
668                # 2*i bounds constraints and i transformed constraints
669                self.assertEqual(
670                    len(list(relaxed.component_data_objects(Constraint))), 2*i+i)
671
672            self.assertEqual(len(relaxed.component('d[%s].c'%i)), i)
673
674        # the remaining disaggregated variables are on the disjunction
675        # transformation block
676        self.assertEqual(len(list(transBlock.component_objects(
677            Var, descend_into=False))), 1)
678        self.assertEqual(len(list(transBlock.component_data_objects(
679            Var, descend_into=False))), 2)
680        # as are the XOR, reaggregation and their bounds constraints
681        self.assertEqual(len(list(transBlock.component_objects(
682            Constraint, descend_into=False))), 3)
683
684        if lb == 0:
685            # 3 reaggregation + 2 bounds + 1 xor (because one bounds constraint
686            # is on the parent transformation block, and we don't need lb
687            # constraints if lb = 0)
688            self.assertEqual(len(list(transBlock.component_data_objects(
689                Constraint, descend_into=False))), 6)
690        else:
691            # 3 reaggregation + 4 bounds + 1 xor
692            self.assertEqual(len(list(transBlock.component_data_objects(
693                Constraint, descend_into=False))), 8)
694
695    def test_indexed_constraints_in_disjunct(self):
696        m = models.makeThreeTermDisj_IndexedConstraints()
697
698        TransformationFactory('gdp.hull').apply_to(m)
699
700        self.check_threeTermDisj_IndexedConstraints(m, lb=0)
701
702    def test_virtual_indexed_constraints_in_disjunct(self):
703        m = ConcreteModel()
704        m.I = [1,2,3]
705        m.x = Var(m.I, bounds=(-1,10))
706        def d_rule(d,j):
707            m = d.model()
708            d.c = Constraint(Any)
709            for k in range(j):
710                d.c[k+1] = m.x[k+1] >= k+1
711        m.d = Disjunct(m.I, rule=d_rule)
712        m.disjunction = Disjunction(expr=[m.d[i] for i in m.I])
713
714        TransformationFactory('gdp.hull').apply_to(m)
715
716        self.check_threeTermDisj_IndexedConstraints(m, lb=-1)
717
718    def test_do_not_transform_deactivated_constraintDatas(self):
719        m = models.makeTwoTermDisj_IndexedConstraints()
720        m.a[1].setlb(0)
721        m.a[1].setub(100)
722        m.a[2].setlb(0)
723        m.a[2].setub(100)
724        m.b.simpledisj1.c[1].deactivate()
725        hull = TransformationFactory('gdp.hull')
726        hull.apply_to(m)
727        # can't ask for simpledisj1.c[1]: it wasn't transformed
728        log = StringIO()
729        with LoggingIntercept(log, 'pyomo.gdp', logging.ERROR):
730            self.assertRaisesRegex(
731                KeyError,
732                r".*b.simpledisj1.c\[1\]",
733                hull.get_transformed_constraints,
734                m.b.simpledisj1.c[1])
735        self.assertRegex(log.getvalue(),
736                         r".*Constraint 'b.simpledisj1.c\[1\]' has not "
737                         r"been transformed.")
738
739        # this fixes a[2] to 0, so we should get the disggregated var
740        transformed = hull.get_transformed_constraints(m.b.simpledisj1.c[2])
741        self.assertEqual(len(transformed), 1)
742        disaggregated_a2 = hull.get_disaggregated_var(m.a[2], m.b.simpledisj1)
743        self.assertIs(transformed[0], disaggregated_a2)
744        self.assertIsInstance(disaggregated_a2, Var)
745        self.assertTrue(disaggregated_a2.is_fixed())
746        self.assertEqual(value(disaggregated_a2), 0)
747
748        transformed = hull.get_transformed_constraints(m.b.simpledisj2.c[1])
749        # simpledisj2.c[1] is a <= constraint
750        self.assertEqual(len(transformed), 1)
751        self.assertIs(transformed[0],
752                      m.b.simpledisj2.transformation_block().\
753                      component("b.simpledisj2.c")[(1,'ub')])
754
755        transformed = hull.get_transformed_constraints(m.b.simpledisj2.c[2])
756        # simpledisj2.c[2] is a <= constraint
757        self.assertEqual(len(transformed), 1)
758        self.assertIs(transformed[0],
759                      m.b.simpledisj2.transformation_block().\
760                      component("b.simpledisj2.c")[(2,'ub')])
761
762
763class MultiTermDisj(unittest.TestCase, CommonTests):
764    def test_xor_constraint(self):
765        ct.check_three_term_xor_constraint(self, 'hull')
766
767    def test_create_using(self):
768        m = models.makeThreeTermIndexedDisj()
769        self.diff_apply_to_and_create_using(m)
770
771    def test_do_not_disaggregate_more_than_necessary(self):
772        m = models.makeThreeTermDisjunctionWithOneVarInOneDisjunct()
773        hull = TransformationFactory('gdp.hull')
774        hull.apply_to(m)
775
776        # check that there are only two disaggregated copies of x
777        x1 = hull.get_disaggregated_var(m.x, m.d1)
778        self.assertEqual(x1.lb, -2)
779        self.assertEqual(x1.ub, 8)
780        self.assertIs(hull.get_src_var(x1), m.x)
781
782        x2 = m.disjunction.algebraic_constraint().parent_block().\
783             _disaggregatedVars[0]
784        self.assertIs(hull.get_src_var(x2), m.x)
785        self.assertIs(hull.get_disaggregated_var(m.x, m.d2), x2)
786        self.assertIs(hull.get_disaggregated_var(m.x, m.d3), x2)
787
788        # check the bounds constraints for the second copy of x
789        bounds = hull.get_var_bounds_constraint(x2)
790        self.assertEqual(len(bounds), 2)
791        # -2(1 - d1.indicator_var) <= x2
792        self.assertIsNone(bounds['lb'].lower)
793        self.assertEqual(bounds['lb'].upper, 0)
794        repn = generate_standard_repn(bounds['lb'].body)
795        self.assertTrue(repn.is_linear())
796        self.assertEqual(len(repn.linear_vars), 2)
797        self.assertIs(repn.linear_vars[1], x2)
798        self.assertIs(repn.linear_vars[0],
799                      m.d1.indicator_var.get_associated_binary())
800        self.assertEqual(repn.linear_coefs[0], 2)
801        self.assertEqual(repn.linear_coefs[1], -1)
802        self.assertEqual(repn.constant, -2)
803        # x2 <= 8(1 - d1.indicator_var)
804        self.assertIsNone(bounds['ub'].lower)
805        self.assertEqual(bounds['ub'].upper, 0)
806        repn = generate_standard_repn(bounds['ub'].body)
807        self.assertTrue(repn.is_linear())
808        self.assertEqual(len(repn.linear_vars), 2)
809        self.assertIs(repn.linear_vars[0], x2)
810        self.assertIs(repn.linear_vars[1],
811                      m.d1.indicator_var.get_associated_binary())
812        self.assertEqual(repn.linear_coefs[1], 8)
813        self.assertEqual(repn.linear_coefs[0], 1)
814        self.assertEqual(repn.constant, -8)
815
816        # check the disaggregation constraint
817        c = hull.get_disaggregation_constraint(m.x, m.disjunction)
818        self.assertEqual(c.lower, 0)
819        self.assertEqual(c.upper, 0)
820        repn = generate_standard_repn(c.body)
821        self.assertTrue(repn.is_linear())
822        self.assertEqual(len(repn.linear_vars), 3)
823        self.assertIs(repn.linear_vars[0], m.x)
824        self.assertIs(repn.linear_vars[1], x2)
825        self.assertIs(repn.linear_vars[2], x1)
826        self.assertEqual(repn.linear_coefs[0], 1)
827        self.assertEqual(repn.linear_coefs[1], -1)
828        self.assertEqual(repn.linear_coefs[2], -1)
829        self.assertEqual(repn.constant, 0)
830
831class IndexedDisjunction(unittest.TestCase, CommonTests):
832    def setUp(self):
833        # set seed so we can test name collisions predictably
834        random.seed(666)
835
836    def test_disaggregation_constraints(self):
837        m = models.makeTwoTermIndexedDisjunction()
838        hull = TransformationFactory('gdp.hull')
839        hull.apply_to(m)
840        relaxedDisjuncts = m._pyomo_gdp_hull_reformulation.relaxedDisjuncts
841
842        disaggregatedVars = {
843            1: [relaxedDisjuncts[0].disaggregatedVars.component('x[1]'),
844                relaxedDisjuncts[1].disaggregatedVars.component('x[1]')],
845            2: [relaxedDisjuncts[2].disaggregatedVars.component('x[2]'),
846                relaxedDisjuncts[3].disaggregatedVars.component('x[2]')],
847            3: [relaxedDisjuncts[4].disaggregatedVars.component('x[3]'),
848                relaxedDisjuncts[5].disaggregatedVars.component('x[3]')],
849        }
850
851        for i, disVars in disaggregatedVars.items():
852            cons = hull.get_disaggregation_constraint(m.x[i],
853                                                       m.disjunction[i])
854            self.assertEqual(cons.lower, 0)
855            self.assertEqual(cons.upper, 0)
856            repn = generate_standard_repn(cons.body)
857            self.assertTrue(repn.is_linear())
858            self.assertEqual(repn.constant, 0)
859            self.assertEqual(len(repn.linear_vars), 3)
860            ct.check_linear_coef(self, repn, m.x[i], 1)
861            ct.check_linear_coef(self, repn, disVars[0], -1)
862            ct.check_linear_coef(self, repn, disVars[1], -1)
863
864    def test_disaggregation_constraints_tuple_indices(self):
865        m = models.makeTwoTermMultiIndexedDisjunction()
866        hull = TransformationFactory('gdp.hull')
867        hull.apply_to(m)
868        relaxedDisjuncts = m._pyomo_gdp_hull_reformulation.relaxedDisjuncts
869
870        disaggregatedVars = {
871            (1,'A'): [relaxedDisjuncts[0].disaggregatedVars.component('a[1,A]'),
872                      relaxedDisjuncts[1].disaggregatedVars.component('a[1,A]')],
873            (1,'B'): [relaxedDisjuncts[2].disaggregatedVars.component('a[1,B]'),
874                      relaxedDisjuncts[3].disaggregatedVars.component('a[1,B]')],
875            (2,'A'): [relaxedDisjuncts[4].disaggregatedVars.component('a[2,A]'),
876                      relaxedDisjuncts[5].disaggregatedVars.component('a[2,A]')],
877            (2,'B'): [relaxedDisjuncts[6].disaggregatedVars.component('a[2,B]'),
878                      relaxedDisjuncts[7].disaggregatedVars.component('a[2,B]')],
879        }
880
881        for i, disVars in disaggregatedVars.items():
882            cons = hull.get_disaggregation_constraint(m.a[i],
883                                                       m.disjunction[i])
884            self.assertEqual(cons.lower, 0)
885            self.assertEqual(cons.upper, 0)
886            # NOTE: fixed variables are evaluated here.
887            repn = generate_standard_repn(cons.body)
888            self.assertTrue(repn.is_linear())
889            self.assertEqual(repn.constant, 0)
890            # The flag=1 disjunct disaggregated variable is fixed to 0, so the
891            # below is actually correct:
892            self.assertEqual(len(repn.linear_vars), 2)
893            ct.check_linear_coef(self, repn, m.a[i], 1)
894            ct.check_linear_coef(self, repn, disVars[0], -1)
895            self.assertTrue(disVars[1].is_fixed())
896            self.assertEqual(value(disVars[1]), 0)
897
898    def test_xor_constraints(self):
899        ct.check_indexed_xor_constraints(self, 'hull')
900
901    def test_xor_constraints_with_targets(self):
902        ct.check_indexed_xor_constraints_with_targets(self, 'hull')
903
904    def test_create_using(self):
905        m = models.makeTwoTermMultiIndexedDisjunction()
906        ct.diff_apply_to_and_create_using(self, m, 'gdp.hull')
907
908    def test_deactivated_constraints(self):
909        ct.check_constraints_deactivated_indexedDisjunction(self, 'hull')
910
911    def test_deactivated_disjuncts(self):
912        ct.check_deactivated_disjuncts(self, 'hull')
913
914    def test_deactivated_disjunctions(self):
915        ct.check_deactivated_disjunctions(self, 'hull')
916
917    def test_partial_deactivate_indexed_disjunction(self):
918        ct.check_partial_deactivate_indexed_disjunction(self, 'hull')
919
920    def test_disjunction_data_target(self):
921        ct.check_disjunction_data_target(self, 'hull')
922
923    def test_disjunction_data_target_any_index(self):
924        ct.check_disjunction_data_target_any_index(self, 'hull')
925
926    def test_cannot_call_transformation_on_disjunction(self):
927        ct.check_cannot_call_transformation_on_disjunction(self, 'hull')
928
929    def check_trans_block_disjunctions_of_disjunct_datas(self, m):
930        transBlock1 = m.component("_pyomo_gdp_hull_reformulation")
931        self.assertIsInstance(transBlock1, Block)
932        self.assertIsInstance(transBlock1.component("relaxedDisjuncts"), Block)
933        # We end up with a transformation block for every SimpleDisjunction or
934        # IndexedDisjunction.
935        self.assertEqual(len(transBlock1.relaxedDisjuncts), 2)
936        self.assertIsInstance(transBlock1.relaxedDisjuncts[0].disaggregatedVars.\
937                              component("x"), Var)
938        self.assertTrue(transBlock1.relaxedDisjuncts[0].disaggregatedVars.x.\
939                        is_fixed())
940        self.assertEqual(value(transBlock1.relaxedDisjuncts[0].\
941                               disaggregatedVars.x), 0)
942        self.assertIsInstance(transBlock1.relaxedDisjuncts[0].component(
943            "firstTerm[1].cons"), Constraint)
944        # No constraint becuase disaggregated variable fixed to 0
945        self.assertEqual(len(transBlock1.relaxedDisjuncts[0].component(
946            "firstTerm[1].cons")), 0)
947        self.assertIsInstance(transBlock1.relaxedDisjuncts[0].component(
948            "x_bounds"), Constraint)
949        self.assertEqual(len(transBlock1.relaxedDisjuncts[0].component(
950            "x_bounds")), 2)
951
952        self.assertIsInstance(transBlock1.relaxedDisjuncts[1].disaggregatedVars.\
953                              component("x"), Var)
954        self.assertIsInstance(transBlock1.relaxedDisjuncts[1].component(
955            "secondTerm[1].cons"), Constraint)
956        self.assertEqual(len(transBlock1.relaxedDisjuncts[1].component(
957            "secondTerm[1].cons")), 1)
958        self.assertIsInstance(transBlock1.relaxedDisjuncts[1].component(
959            "x_bounds"), Constraint)
960        self.assertEqual(len(transBlock1.relaxedDisjuncts[1].component(
961            "x_bounds")), 2)
962
963        transBlock2 = m.component("_pyomo_gdp_hull_reformulation_4")
964        self.assertIsInstance(transBlock2, Block)
965        self.assertIsInstance(transBlock2.component("relaxedDisjuncts"), Block)
966        self.assertEqual(len(transBlock2.relaxedDisjuncts), 2)
967        self.assertIsInstance(transBlock2.relaxedDisjuncts[0].disaggregatedVars.\
968                              component("x"), Var)
969        self.assertIsInstance(transBlock2.relaxedDisjuncts[0].component(
970            "firstTerm[2].cons"), Constraint)
971        # we have an equality constraint
972        self.assertEqual(len(transBlock2.relaxedDisjuncts[0].component(
973            "firstTerm[2].cons")), 1)
974        self.assertIsInstance(transBlock2.relaxedDisjuncts[0].component(
975            "x_bounds"), Constraint)
976        self.assertEqual(len(transBlock2.relaxedDisjuncts[0].component(
977            "x_bounds")), 2)
978
979        self.assertIsInstance(transBlock2.relaxedDisjuncts[1].disaggregatedVars.\
980                              component("x"), Var)
981        self.assertIsInstance(transBlock2.relaxedDisjuncts[1].component(
982            "secondTerm[2].cons"), Constraint)
983        self.assertEqual(len(transBlock2.relaxedDisjuncts[1].component(
984            "secondTerm[2].cons")), 1)
985        self.assertIsInstance(transBlock2.relaxedDisjuncts[1].component(
986            "x_bounds"), Constraint)
987        self.assertEqual(len(transBlock2.relaxedDisjuncts[1].component(
988            "x_bounds")), 2)
989
990    def test_simple_disjunction_of_disjunct_datas(self):
991        ct.check_simple_disjunction_of_disjunct_datas(self, 'hull')
992
993    def test_any_indexed_disjunction_of_disjunct_datas(self):
994        m = models.makeAnyIndexedDisjunctionOfDisjunctDatas()
995        TransformationFactory('gdp.hull').apply_to(m)
996
997        transBlock = m.component("_pyomo_gdp_hull_reformulation")
998        self.assertIsInstance(transBlock, Block)
999        self.assertIsInstance(transBlock.component("relaxedDisjuncts"), Block)
1000        self.assertEqual(len(transBlock.relaxedDisjuncts), 4)
1001        self.assertIsInstance(transBlock.relaxedDisjuncts[0].disaggregatedVars.\
1002                              component("x"), Var)
1003        self.assertTrue(transBlock.relaxedDisjuncts[0].disaggregatedVars.\
1004                        x.is_fixed())
1005        self.assertEqual(value(transBlock.relaxedDisjuncts[0].disaggregatedVars.\
1006                               x), 0)
1007        self.assertIsInstance(transBlock.relaxedDisjuncts[0].component(
1008            "firstTerm[1].cons"), Constraint)
1009        # No constraint becuase disaggregated variable fixed to 0
1010        self.assertEqual(len(transBlock.relaxedDisjuncts[0].component(
1011            "firstTerm[1].cons")), 0)
1012        self.assertIsInstance(transBlock.relaxedDisjuncts[0].component(
1013            "x_bounds"), Constraint)
1014        self.assertEqual(len(transBlock.relaxedDisjuncts[0].component(
1015            "x_bounds")), 2)
1016
1017        self.assertIsInstance(transBlock.relaxedDisjuncts[1].disaggregatedVars.\
1018                              component("x"), Var)
1019        self.assertIsInstance(transBlock.relaxedDisjuncts[1].component(
1020            "secondTerm[1].cons"), Constraint)
1021        self.assertEqual(len(transBlock.relaxedDisjuncts[1].component(
1022            "secondTerm[1].cons")), 1)
1023        self.assertIsInstance(transBlock.relaxedDisjuncts[1].component(
1024            "x_bounds"), Constraint)
1025        self.assertEqual(len(transBlock.relaxedDisjuncts[1].component(
1026            "x_bounds")), 2)
1027
1028        self.assertIsInstance(transBlock.relaxedDisjuncts[2].disaggregatedVars.\
1029                              component("x"), Var)
1030        self.assertIsInstance(transBlock.relaxedDisjuncts[2].component(
1031            "firstTerm[2].cons"), Constraint)
1032        # we have an equality constraint
1033        self.assertEqual(len(transBlock.relaxedDisjuncts[2].component(
1034            "firstTerm[2].cons")), 1)
1035        self.assertIsInstance(transBlock.relaxedDisjuncts[2].component(
1036            "x_bounds"), Constraint)
1037        self.assertEqual(len(transBlock.relaxedDisjuncts[2].component(
1038            "x_bounds")), 2)
1039
1040        self.assertIsInstance(transBlock.relaxedDisjuncts[3].disaggregatedVars.\
1041                              component("x"), Var)
1042        self.assertIsInstance(transBlock.relaxedDisjuncts[3].component(
1043            "secondTerm[2].cons"), Constraint)
1044        self.assertEqual(len(transBlock.relaxedDisjuncts[3].component(
1045            "secondTerm[2].cons")), 1)
1046        self.assertIsInstance(transBlock.relaxedDisjuncts[3].component(
1047            "x_bounds"), Constraint)
1048        self.assertEqual(len(transBlock.relaxedDisjuncts[3].component(
1049            "x_bounds")), 2)
1050
1051        self.assertIsInstance(transBlock.component("disjunction_xor"),
1052                              Constraint)
1053        self.assertEqual(len(transBlock.component("disjunction_xor")), 2)
1054
1055    def check_first_iteration(self, model):
1056        transBlock = model.component("_pyomo_gdp_hull_reformulation")
1057        self.assertIsInstance(transBlock, Block)
1058        self.assertIsInstance(
1059            transBlock.component("disjunctionList_xor"), Constraint)
1060        self.assertEqual(len(transBlock.disjunctionList_xor), 1)
1061        self.assertFalse(model.disjunctionList[0].active)
1062
1063        if model.component('firstTerm') is None:
1064            firstTerm = "'firstTerm[0]'.cons"
1065            secondTerm = "'secondTerm[0]'.cons"
1066        else:
1067            firstTerm = "firstTerm[0].cons"
1068            secondTerm = "secondTerm[0].cons"
1069
1070        self.assertIsInstance(transBlock.relaxedDisjuncts, Block)
1071        self.assertEqual(len(transBlock.relaxedDisjuncts), 2)
1072
1073        self.assertIsInstance(transBlock.relaxedDisjuncts[0].disaggregatedVars.x,
1074                              Var)
1075        self.assertTrue(transBlock.relaxedDisjuncts[0].disaggregatedVars.x.\
1076                        is_fixed())
1077        self.assertEqual(value(transBlock.relaxedDisjuncts[0].disaggregatedVars.\
1078                               x), 0)
1079        self.assertIsInstance(transBlock.relaxedDisjuncts[0].component(
1080            firstTerm), Constraint)
1081        self.assertEqual(len(transBlock.relaxedDisjuncts[0].component(
1082            firstTerm)), 0)
1083        self.assertIsInstance(transBlock.relaxedDisjuncts[0].x_bounds,
1084                              Constraint)
1085        self.assertEqual(len(transBlock.relaxedDisjuncts[0].x_bounds), 2)
1086
1087        self.assertIsInstance(transBlock.relaxedDisjuncts[1].disaggregatedVars.x,
1088                              Var)
1089        self.assertFalse(transBlock.relaxedDisjuncts[1].disaggregatedVars.\
1090                         x.is_fixed())
1091        self.assertIsInstance(transBlock.relaxedDisjuncts[1].component(
1092            secondTerm), Constraint)
1093        self.assertEqual(len(transBlock.relaxedDisjuncts[1].component(
1094            secondTerm)), 1)
1095        self.assertIsInstance(transBlock.relaxedDisjuncts[1].x_bounds,
1096                              Constraint)
1097        self.assertEqual(len(transBlock.relaxedDisjuncts[1].x_bounds), 2)
1098
1099    def check_second_iteration(self, model):
1100        transBlock = model.component("_pyomo_gdp_hull_reformulation")
1101        self.assertIsInstance(transBlock, Block)
1102        self.assertIsInstance(transBlock.component("relaxedDisjuncts"), Block)
1103        self.assertEqual(len(transBlock.relaxedDisjuncts), 4)
1104
1105        if model.component('firstTerm') is None:
1106            firstTerm = "'firstTerm[1]'.cons"
1107            secondTerm = "'secondTerm[1]'.cons"
1108        else:
1109            firstTerm = "firstTerm[1].cons"
1110            secondTerm = "secondTerm[1].cons"
1111
1112        self.assertIsInstance(transBlock.relaxedDisjuncts[2].component(
1113            firstTerm), Constraint)
1114        self.assertEqual(len(transBlock.relaxedDisjuncts[2].component(
1115            firstTerm)), 1)
1116        self.assertIsInstance(transBlock.relaxedDisjuncts[3].component(
1117            secondTerm), Constraint)
1118        self.assertEqual(len(transBlock.relaxedDisjuncts[3].component(
1119            secondTerm)), 1)
1120        self.assertEqual(
1121            len(transBlock.disjunctionList_xor), 2)
1122        self.assertFalse(model.disjunctionList[1].active)
1123        self.assertFalse(model.disjunctionList[0].active)
1124
1125    def test_disjunction_and_disjuncts_indexed_by_any(self):
1126        ct.check_disjunction_and_disjuncts_indexed_by_any(self, 'hull')
1127
1128    def test_iteratively_adding_disjunctions_transform_container(self):
1129        ct.check_iteratively_adding_disjunctions_transform_container(self,
1130                                                                     'hull')
1131
1132    def test_iteratively_adding_disjunctions_transform_model(self):
1133        ct.check_iteratively_adding_disjunctions_transform_model(self, 'hull')
1134
1135    def test_iteratively_adding_to_indexed_disjunction_on_block(self):
1136        ct.check_iteratively_adding_to_indexed_disjunction_on_block(self,
1137                                                                    'hull')
1138
1139class TestTargets_SingleDisjunction(unittest.TestCase, CommonTests):
1140    def test_only_targets_inactive(self):
1141        ct.check_only_targets_inactive(self, 'hull')
1142
1143    def test_only_targets_transformed(self):
1144        ct.check_only_targets_get_transformed(self, 'hull')
1145
1146    def test_target_not_a_component_err(self):
1147        ct.check_target_not_a_component_error(self, 'hull')
1148
1149    def test_targets_cannot_be_cuids(self):
1150        ct.check_targets_cannot_be_cuids(self, 'hull')
1151
1152class TestTargets_IndexedDisjunction(unittest.TestCase, CommonTests):
1153    # There are a couple tests for targets above, but since I had the patience
1154    # to make all these for bigm also, I may as well reap the benefits here too.
1155    def test_indexedDisj_targets_inactive(self):
1156        ct.check_indexedDisj_targets_inactive(self, 'hull')
1157
1158    def test_indexedDisj_only_targets_transformed(self):
1159        ct.check_indexedDisj_only_targets_transformed(self, 'hull')
1160
1161    def test_warn_for_untransformed(self):
1162        ct.check_warn_for_untransformed(self, 'hull')
1163
1164    def test_disjData_targets_inactive(self):
1165        ct.check_disjData_targets_inactive(self, 'hull')
1166        m = models.makeDisjunctionsOnIndexedBlock()
1167
1168    def test_disjData_only_targets_transformed(self):
1169        ct.check_disjData_only_targets_transformed(self, 'hull')
1170
1171    def test_indexedBlock_targets_inactive(self):
1172        ct.check_indexedBlock_targets_inactive(self, 'hull')
1173
1174    def test_indexedBlock_only_targets_transformed(self):
1175        ct.check_indexedBlock_only_targets_transformed(self, 'hull')
1176
1177    def test_blockData_targets_inactive(self):
1178        ct.check_blockData_targets_inactive(self, 'hull')
1179
1180    def test_blockData_only_targets_transformed(self):
1181        ct.check_blockData_only_targets_transformed(self, 'hull')
1182
1183    def test_do_not_transform_deactivated_targets(self):
1184        ct.check_do_not_transform_deactivated_targets(self, 'hull')
1185
1186    def test_create_using(self):
1187        m = models.makeDisjunctionsOnIndexedBlock()
1188        ct.diff_apply_to_and_create_using(self, m, 'gdp.hull')
1189
1190class DisaggregatedVarNamingConflict(unittest.TestCase):
1191    @staticmethod
1192    def makeModel():
1193        m = ConcreteModel()
1194        m.b = Block()
1195        m.b.x = Var(bounds=(0, 10))
1196        m.add_component("b.x", Var(bounds=(-9, 9)))
1197        def disjunct_rule(d, i):
1198            m = d.model()
1199            if i:
1200                d.cons_block = Constraint(expr=m.b.x >= 5)
1201                d.cons_model = Constraint(expr=m.component("b.x")==0)
1202            else:
1203                d.cons_model = Constraint(expr=m.component("b.x") <= -5)
1204        m.disjunct = Disjunct([0,1], rule=disjunct_rule)
1205        m.disjunction = Disjunction(expr=[m.disjunct[0], m.disjunct[1]])
1206
1207        return m
1208
1209    def test_disaggregation_constraints(self):
1210        m = self.makeModel()
1211        hull = TransformationFactory('gdp.hull')
1212        hull.apply_to(m)
1213
1214        disaggregationConstraints = m._pyomo_gdp_hull_reformulation.\
1215                                    disaggregationConstraints
1216        consmap = [
1217            (m.component("b.x"), disaggregationConstraints[(0, None)]),
1218            (m.b.x, disaggregationConstraints[(1, None)])
1219        ]
1220
1221        for v, cons in consmap:
1222            disCons = hull.get_disaggregation_constraint(v, m.disjunction)
1223            self.assertIs(disCons, cons)
1224
1225class DisjunctInMultipleDisjunctions(unittest.TestCase, CommonTests):
1226    def test_error_for_same_disjunct_in_multiple_disjunctions(self):
1227        ct.check_error_for_same_disjunct_in_multiple_disjunctions(self, 'hull')
1228
1229class NestedDisjunction(unittest.TestCase, CommonTests):
1230    def setUp(self):
1231        # set seed so we can test name collisions predictably
1232        random.seed(666)
1233
1234    def test_disjuncts_inactive(self):
1235        ct.check_disjuncts_inactive_nested(self, 'hull')
1236
1237    def test_deactivated_disjunct_leaves_nested_disjuncts_active(self):
1238        ct.check_deactivated_disjunct_leaves_nested_disjunct_active(self,
1239                                                                    'hull')
1240
1241    def test_mappings_between_disjunctions_and_xors(self):
1242        # For the sake of not second-guessing anyone, we will let the inner
1243        # disjunction point to its original XOR constraint. This constraint
1244        # itself will be transformed by the outer disjunction, so if you want to
1245        # find what it became you will have to follow its map to the transformed
1246        # version. (But this behaves the same as bigm)
1247        ct.check_mappings_between_disjunctions_and_xors(self, 'hull')
1248
1249    def test_unique_reference_to_nested_indicator_var(self):
1250        ct.check_unique_reference_to_nested_indicator_var(self, 'hull')
1251
1252    def test_disjunct_targets_inactive(self):
1253        ct.check_disjunct_targets_inactive(self, 'hull')
1254
1255    def test_disjunct_only_targets_transformed(self):
1256        ct.check_disjunct_only_targets_transformed(self, 'hull')
1257
1258    def test_disjunctData_targets_inactive(self):
1259        ct.check_disjunctData_targets_inactive(self, 'hull')
1260
1261    def test_disjunctData_only_targets_transformed(self):
1262        ct.check_disjunctData_only_targets_transformed(self, 'hull')
1263
1264    def test_disjunction_target_err(self):
1265        ct.check_disjunction_target_err(self, 'hull')
1266
1267    def test_nested_disjunction_target(self):
1268        ct.check_nested_disjunction_target(self, 'hull')
1269
1270    def test_target_appears_twice(self):
1271        ct.check_target_appears_twice(self, 'hull')
1272
1273    @unittest.skipIf(not linear_solvers, "No linear solver available")
1274    def test_relaxation_feasibility(self):
1275        m = models.makeNestedDisjunctions_FlatDisjuncts()
1276        TransformationFactory('gdp.hull').apply_to(m)
1277
1278        solver = SolverFactory(linear_solvers[0])
1279
1280        cases = [
1281            (1,1,1,1,None),
1282            (0,0,0,0,None),
1283            (1,0,0,0,None),
1284            (0,1,0,0,1.1),
1285            (0,0,1,0,None),
1286            (0,0,0,1,None),
1287            (1,1,0,0,None),
1288            (1,0,1,0,1.2),
1289            (1,0,0,1,1.3),
1290            (1,0,1,1,None),
1291            ]
1292        for case in cases:
1293            m.d1.indicator_var.fix(case[0])
1294            m.d2.indicator_var.fix(case[1])
1295            m.d3.indicator_var.fix(case[2])
1296            m.d4.indicator_var.fix(case[3])
1297            results = solver.solve(m)
1298            if case[4] is None:
1299                self.assertEqual(results.solver.termination_condition,
1300                                 TerminationCondition.infeasible)
1301            else:
1302                self.assertEqual(results.solver.termination_condition,
1303                                 TerminationCondition.optimal)
1304                self.assertEqual(value(m.obj), case[4])
1305
1306    @unittest.skipIf(not linear_solvers, "No linear solver available")
1307    def test_relaxation_feasibility_transform_inner_first(self):
1308        # This test is identical to the above except that the
1309        # reference_indicator_var transformation will be called on m.d1
1310        # first. So this makes sure that we are still doing the right thing even
1311        # if the indicator_var references already exist.
1312        m = models.makeNestedDisjunctions_FlatDisjuncts()
1313        TransformationFactory('gdp.hull').apply_to(m.d1)
1314        TransformationFactory('gdp.hull').apply_to(m)
1315
1316        solver = SolverFactory(linear_solvers[0])
1317
1318        cases = [
1319            (1,1,1,1,None),
1320            (0,0,0,0,None),
1321            (1,0,0,0,None),
1322            (0,1,0,0,1.1),
1323            (0,0,1,0,None),
1324            (0,0,0,1,None),
1325            (1,1,0,0,None),
1326            (1,0,1,0,1.2),
1327            (1,0,0,1,1.3),
1328            (1,0,1,1,None),
1329            ]
1330        for case in cases:
1331            m.d1.indicator_var.fix(case[0])
1332            m.d2.indicator_var.fix(case[1])
1333            m.d3.indicator_var.fix(case[2])
1334            m.d4.indicator_var.fix(case[3])
1335            results = solver.solve(m)
1336            if case[4] is None:
1337                self.assertEqual(results.solver.termination_condition,
1338                                 TerminationCondition.infeasible)
1339            else:
1340                self.assertEqual(results.solver.termination_condition,
1341                                 TerminationCondition.optimal)
1342                self.assertEqual(value(m.obj), case[4])
1343
1344    def test_create_using(self):
1345        m = models.makeNestedDisjunctions_FlatDisjuncts()
1346        self.diff_apply_to_and_create_using(m)
1347
1348    def check_outer_disaggregation_constraint(self, cons, var, disj1, disj2):
1349        hull = TransformationFactory('gdp.hull')
1350        self.assertTrue(cons.active)
1351        self.assertEqual(cons.lower, 0)
1352        self.assertEqual(cons.upper, 0)
1353        repn = generate_standard_repn(cons.body)
1354        self.assertTrue(repn.is_linear())
1355        self.assertEqual(repn.constant, 0)
1356        ct.check_linear_coef(self, repn, var, 1)
1357        ct.check_linear_coef(self, repn, hull.get_disaggregated_var(var, disj1),
1358                             -1)
1359        ct.check_linear_coef(self, repn, hull.get_disaggregated_var(var, disj2),
1360                             -1)
1361
1362    def check_bounds_constraint_ub(self, constraint, ub, dis_var, ind_var):
1363        hull = TransformationFactory('gdp.hull')
1364        self.assertIsInstance(constraint, Constraint)
1365        self.assertTrue(constraint.active)
1366        self.assertEqual(len(constraint), 1)
1367        self.assertTrue(constraint['ub'].active)
1368        self.assertEqual(constraint['ub'].upper, 0)
1369        self.assertIsNone(constraint['ub'].lower)
1370        repn = generate_standard_repn(constraint['ub'].body)
1371        self.assertTrue(repn.is_linear())
1372        self.assertEqual(repn.constant, 0)
1373        self.assertEqual(len(repn.linear_vars), 2)
1374        ct.check_linear_coef(self, repn, dis_var, 1)
1375        ct.check_linear_coef(self, repn, ind_var, -ub)
1376        self.assertIs(constraint, hull.get_var_bounds_constraint(dis_var))
1377
1378    def check_inner_disaggregated_var_bounds(self, cons, dis, ind_var,
1379                                             original_cons):
1380        hull = TransformationFactory('gdp.hull')
1381        self.assertIsInstance(cons, Constraint)
1382        self.assertTrue(cons.active)
1383        self.assertEqual(len(cons), 1)
1384        self.assertTrue(cons[('ub', 'ub')].active)
1385        self.assertIsNone(cons[('ub', 'ub')].lower)
1386        self.assertEqual(cons[('ub', 'ub')].upper, 0)
1387        repn = generate_standard_repn(cons[('ub', 'ub')].body)
1388        self.assertTrue(repn.is_linear())
1389        self.assertEqual(repn.constant, 0)
1390        self.assertEqual(len(repn.linear_vars), 2)
1391        ct.check_linear_coef(self, repn, dis, 1)
1392        ct.check_linear_coef(self, repn, ind_var, -2)
1393
1394        self.assertIs(hull.get_var_bounds_constraint(dis), original_cons)
1395        transformed_list = hull.get_transformed_constraints(original_cons['ub'])
1396        self.assertEqual(len(transformed_list), 1)
1397        self.assertIs(transformed_list[0], cons[('ub', 'ub')])
1398
1399    def check_inner_transformed_constraint(self, cons, dis, lb, ind_var,
1400                                           first_transformed, original):
1401        hull = TransformationFactory('gdp.hull')
1402        self.assertIsInstance(cons, Constraint)
1403        self.assertTrue(cons.active)
1404        self.assertEqual(len(cons), 1)
1405        # Ha, this really isn't lovely, but its just chance that it's ub the
1406        # second time.
1407        self.assertTrue(cons[('lb', 'ub')].active)
1408        self.assertIsNone(cons[('lb', 'ub')].lower)
1409        self.assertEqual(cons[('lb', 'ub')].upper, 0)
1410        repn = generate_standard_repn(cons[('lb', 'ub')].body)
1411        self.assertTrue(repn.is_linear())
1412        self.assertEqual(repn.constant, 0)
1413        self.assertEqual(len(repn.linear_vars), 2)
1414        ct.check_linear_coef(self, repn, dis, -1)
1415        ct.check_linear_coef(self, repn, ind_var, lb)
1416
1417        self.assertIs(hull.get_src_constraint(first_transformed),
1418                      original)
1419        trans_list = hull.get_transformed_constraints(original)
1420        self.assertEqual(len(trans_list), 1)
1421        self.assertIs(trans_list[0], first_transformed['lb'])
1422        self.assertIs(hull.get_src_constraint(first_transformed['lb']),
1423                      original)
1424        self.assertIs(hull.get_src_constraint(cons), first_transformed)
1425        trans_list = hull.get_transformed_constraints(first_transformed['lb'])
1426        self.assertEqual(len(trans_list), 1)
1427        self.assertIs(trans_list[0], cons[('lb', 'ub')])
1428        self.assertIs(hull.get_src_constraint(cons[('lb', 'ub')]),
1429                      first_transformed['lb'])
1430
1431    def check_outer_transformed_constraint(self, cons, dis, lb, ind_var):
1432        hull = TransformationFactory('gdp.hull')
1433        self.assertIsInstance(cons, Constraint)
1434        self.assertTrue(cons.active)
1435        self.assertEqual(len(cons), 1)
1436        self.assertTrue(cons['lb'].active)
1437        self.assertIsNone(cons['lb'].lower)
1438        self.assertEqual(cons['lb'].upper, 0)
1439        repn = generate_standard_repn(cons['lb'].body)
1440        self.assertTrue(repn.is_linear())
1441        self.assertEqual(repn.constant, 0)
1442        self.assertEqual(len(repn.linear_vars), 2)
1443        ct.check_linear_coef(self, repn, dis, -1)
1444        ct.check_linear_coef(self, repn, ind_var, lb)
1445
1446        orig = ind_var.parent_block().c
1447        self.assertIs(hull.get_src_constraint(cons), orig)
1448        trans_list = hull.get_transformed_constraints(orig)
1449        self.assertEqual(len(trans_list), 1)
1450        self.assertIs(trans_list[0], cons['lb'])
1451
1452    def test_transformed_model_nestedDisjuncts(self):
1453        # This test tests *everything* for a simple nested disjunction case.
1454        m = models.makeNestedDisjunctions_NestedDisjuncts()
1455
1456        hull = TransformationFactory('gdp.hull')
1457        hull.apply_to(m)
1458
1459        transBlock = m._pyomo_gdp_hull_reformulation
1460        self.assertTrue(transBlock.active)
1461
1462        # outer xor should be on this block
1463        xor = transBlock.disj_xor
1464        self.assertIsInstance(xor, Constraint)
1465        self.assertTrue(xor.active)
1466        self.assertEqual(xor.lower, 1)
1467        self.assertEqual(xor.upper, 1)
1468        repn = generate_standard_repn(xor.body)
1469        self.assertTrue(repn.is_linear())
1470        self.assertEqual(repn.constant, 0)
1471        ct.check_linear_coef(self, repn, m.d1.indicator_var, 1)
1472        ct.check_linear_coef(self, repn, m.d2.indicator_var, 1)
1473        self.assertIs(xor, m.disj.algebraic_constraint())
1474        self.assertIs(m.disj, hull.get_src_disjunction(xor))
1475
1476        # so should the outer disaggregation constraint
1477        dis = transBlock.disaggregationConstraints
1478        self.assertIsInstance(dis, Constraint)
1479        self.assertTrue(dis.active)
1480        self.assertEqual(len(dis), 3)
1481        self.check_outer_disaggregation_constraint(dis[0,None], m.x, m.d1,
1482                                                   m.d2)
1483        self.assertIs(hull.get_disaggregation_constraint(m.x, m.disj),
1484                      dis[0, None])
1485        self.check_outer_disaggregation_constraint(
1486            dis[1,None],
1487            m.d1.d3.binary_indicator_var,
1488            m.d1,
1489            m.d2)
1490        self.assertIs(hull.get_disaggregation_constraint(
1491            m.d1.d3.binary_indicator_var,
1492            m.disj), dis[1,None])
1493        self.check_outer_disaggregation_constraint(
1494            dis[2,None],
1495            m.d1.d4.binary_indicator_var,
1496            m.d1,
1497            m.d2)
1498        self.assertIs(hull.get_disaggregation_constraint(
1499            m.d1.d4.binary_indicator_var,
1500            m.disj), dis[2,None])
1501
1502        # we should have four disjunct transformation blocks: 2 real ones and
1503        # then two that are just home to indicator_var and disaggregated var
1504        # References.
1505        disjBlocks = transBlock.relaxedDisjuncts
1506        self.assertTrue(disjBlocks.active)
1507        self.assertEqual(len(disjBlocks), 4)
1508
1509        disj1 = disjBlocks[0]
1510        self.assertTrue(disj1.active)
1511        self.assertIs(disj1, m.d1.transformation_block())
1512        self.assertIs(m.d1, hull.get_src_disjunct(disj1))
1513
1514        # check the disaggregated vars are here
1515        self.assertIsInstance(disj1.disaggregatedVars.x, Var)
1516        self.assertEqual(disj1.disaggregatedVars.x.lb, 0)
1517        self.assertEqual(disj1.disaggregatedVars.x.ub, 2)
1518        self.assertIs(disj1.disaggregatedVars.x,
1519                      hull.get_disaggregated_var(m.x, m.d1))
1520        self.assertIs(m.x, hull.get_src_var(disj1.disaggregatedVars.x))
1521        d3 = disj1.disaggregatedVars.component("d1.d3.binary_indicator_var")
1522        self.assertEqual(d3.lb, 0)
1523        self.assertEqual(d3.ub, 1)
1524        self.assertIsInstance(d3, Var)
1525        self.assertIs(d3, hull.get_disaggregated_var(
1526            m.d1.d3.binary_indicator_var, m.d1))
1527        self.assertIs(m.d1.d3.binary_indicator_var, hull.get_src_var(d3))
1528        d4 = disj1.disaggregatedVars.component("d1.d4.binary_indicator_var")
1529        self.assertIsInstance(d4, Var)
1530        self.assertEqual(d4.lb, 0)
1531        self.assertEqual(d4.ub, 1)
1532        self.assertIs(d4, hull.get_disaggregated_var(
1533            m.d1.d4.binary_indicator_var, m.d1))
1534        self.assertIs(m.d1.d4.binary_indicator_var, hull.get_src_var(d4))
1535
1536        # check inner disjunction disaggregated vars
1537        x3 = m.d1._pyomo_gdp_hull_reformulation.relaxedDisjuncts[0].\
1538             disaggregatedVars.x
1539        self.assertIsInstance(x3, Var)
1540        self.assertEqual(x3.lb, 0)
1541        self.assertEqual(x3.ub, 2)
1542        self.assertIs(hull.get_disaggregated_var(m.x, m.d1.d3), x3)
1543        self.assertIs(hull.get_src_var(x3), m.x)
1544
1545        x4 = m.d1._pyomo_gdp_hull_reformulation.relaxedDisjuncts[1].\
1546             disaggregatedVars.x
1547        self.assertIsInstance(x4, Var)
1548        self.assertEqual(x4.lb, 0)
1549        self.assertEqual(x4.ub, 2)
1550        self.assertIs(hull.get_disaggregated_var(m.x, m.d1.d4), x4)
1551        self.assertIs(hull.get_src_var(x4), m.x)
1552
1553        # check the bounds constraints
1554        self.check_bounds_constraint_ub(disj1.x_bounds, 2,
1555                                        disj1.disaggregatedVars.x,
1556                                        m.d1.indicator_var)
1557        self.check_bounds_constraint_ub(
1558            disj1.component("d1.d3.binary_indicator_var_bounds"), 1,
1559            disj1.disaggregatedVars.component("d1.d3.binary_indicator_var"),
1560            m.d1.indicator_var)
1561        self.check_bounds_constraint_ub(
1562            disj1.component("d1.d4.binary_indicator_var_bounds"), 1,
1563            disj1.disaggregatedVars.component("d1.d4.binary_indicator_var"),
1564            m.d1.indicator_var)
1565
1566        # check the transformed constraints
1567
1568        # transformed xor
1569        xor = disj1.component("d1._pyomo_gdp_hull_reformulation.'d1.disj2_xor'")
1570        self.assertIsInstance(xor, Constraint)
1571        self.assertTrue(xor.active)
1572        self.assertEqual(len(xor), 1)
1573        self.assertTrue(xor['eq'].active)
1574        self.assertEqual(xor['eq'].lower, 0)
1575        self.assertEqual(xor['eq'].upper, 0)
1576        repn = generate_standard_repn(xor['eq'].body)
1577        self.assertTrue(repn.is_linear())
1578        self.assertEqual(repn.constant, 0)
1579        self.assertEqual(len(repn.linear_vars), 3)
1580        ct.check_linear_coef(
1581            self, repn,
1582            disj1.disaggregatedVars.component("d1.d3.binary_indicator_var"), 1)
1583        ct.check_linear_coef(
1584            self, repn,
1585            disj1.disaggregatedVars.component("d1.d4.binary_indicator_var"), 1)
1586        ct.check_linear_coef(self, repn, m.d1.indicator_var, -1)
1587
1588        # inner disjunction disaggregation constraint
1589        dis_cons_inner_disjunction = disj1.component(
1590            "d1._pyomo_gdp_hull_reformulation.disaggregationConstraints")
1591        self.assertIsInstance(dis_cons_inner_disjunction, Constraint)
1592        self.assertTrue(dis_cons_inner_disjunction.active)
1593        self.assertEqual(len(dis_cons_inner_disjunction), 1)
1594        self.assertTrue(dis_cons_inner_disjunction[(0,None,'eq')].active)
1595        self.assertEqual(dis_cons_inner_disjunction[(0,None,'eq')].lower, 0)
1596        self.assertEqual(dis_cons_inner_disjunction[(0,None,'eq')].upper, 0)
1597        repn = generate_standard_repn(dis_cons_inner_disjunction[(0, None,
1598                                                                  'eq')].body)
1599        self.assertTrue(repn.is_linear())
1600        self.assertEqual(repn.constant, 0)
1601        self.assertEqual(len(repn.linear_vars), 3)
1602        ct.check_linear_coef(self, repn, x3, -1)
1603        ct.check_linear_coef(self, repn, x4, -1)
1604        ct.check_linear_coef(self, repn, disj1.disaggregatedVars.x, 1)
1605
1606        # disaggregated d3.x bounds constraints
1607        x3_bounds = disj1.component(
1608            "d1._pyomo_gdp_hull_reformulation.relaxedDisjuncts[0].x_bounds")
1609        original_cons = m.d1._pyomo_gdp_hull_reformulation.relaxedDisjuncts[0].\
1610                        x_bounds
1611        self.check_inner_disaggregated_var_bounds(
1612            x3_bounds, x3,
1613            disj1.disaggregatedVars.component("d1.d3.binary_indicator_var"),
1614            original_cons)
1615
1616
1617        # disaggregated d4.x bounds constraints
1618        x4_bounds = disj1.component(
1619            "d1._pyomo_gdp_hull_reformulation.relaxedDisjuncts[1].x_bounds")
1620        original_cons = m.d1._pyomo_gdp_hull_reformulation.relaxedDisjuncts[1].\
1621                        x_bounds
1622        self.check_inner_disaggregated_var_bounds(
1623            x4_bounds, x4,
1624            disj1.disaggregatedVars.component("d1.d4.binary_indicator_var"),
1625            original_cons)
1626
1627        # transformed x >= 1.2
1628        cons = disj1.component(
1629            "d1._pyomo_gdp_hull_reformulation.relaxedDisjuncts[0].'d1.d3.c'")
1630        first_transformed = m.d1._pyomo_gdp_hull_reformulation.\
1631                            relaxedDisjuncts[0].component("d1.d3.c")
1632        original = m.d1.d3.c
1633        self.check_inner_transformed_constraint(
1634            cons, x3, 1.2,
1635            disj1.disaggregatedVars.component("d1.d3.binary_indicator_var"),
1636            first_transformed, original)
1637
1638        # transformed x >= 1.3
1639        cons = disj1.component(
1640            "d1._pyomo_gdp_hull_reformulation.relaxedDisjuncts[1].'d1.d4.c'")
1641        first_transformed = m.d1._pyomo_gdp_hull_reformulation.\
1642                            relaxedDisjuncts[1].component("d1.d4.c")
1643        original = m.d1.d4.c
1644        self.check_inner_transformed_constraint(
1645            cons, x4, 1.3,
1646            disj1.disaggregatedVars.component("d1.d4.binary_indicator_var"),
1647            first_transformed, original)
1648
1649        # outer disjunction transformed constraint
1650        cons = disj1.component("d1.c")
1651        self.check_outer_transformed_constraint(cons, disj1.disaggregatedVars.x,
1652                                                1, m.d1.indicator_var)
1653
1654        # and last, check the second transformed outer disjunct
1655        disj2 = disjBlocks[3]
1656        self.assertTrue(disj2.active)
1657        self.assertIs(disj2, m.d2.transformation_block())
1658        self.assertIs(m.d2, hull.get_src_disjunct(disj2))
1659
1660        # disaggregated var
1661        x2 = disj2.disaggregatedVars.x
1662        self.assertIsInstance(x2, Var)
1663        self.assertEqual(x2.lb, 0)
1664        self.assertEqual(x2.ub, 2)
1665        self.assertIs(hull.get_disaggregated_var(m.x, m.d2), x2)
1666        self.assertIs(hull.get_src_var(x2), m.x)
1667
1668        # bounds constraint
1669        x_bounds = disj2.x_bounds
1670        self.check_bounds_constraint_ub(x_bounds, 2, x2, m.d2.indicator_var)
1671
1672        # transformed constraint x >= 1.1
1673        cons = disj2.component("d2.c")
1674        self.check_outer_transformed_constraint(cons, x2, 1.1,
1675                                                m.d2.indicator_var)
1676
1677        # check inner xor mapping: Note that this maps to a now deactivated
1678        # (transformed again) constraint, but that it is possible to go full
1679        # circle, like so:
1680        orig_inner_xor = m.d1._pyomo_gdp_hull_reformulation.component(
1681            "d1.disj2_xor")
1682        self.assertIs(m.d1.disj2.algebraic_constraint(), orig_inner_xor)
1683        self.assertFalse(orig_inner_xor.active)
1684        trans_list = hull.get_transformed_constraints(orig_inner_xor)
1685        self.assertEqual(len(trans_list), 1)
1686        self.assertIs(trans_list[0], xor['eq'])
1687        self.assertIs(hull.get_src_constraint(xor), orig_inner_xor)
1688        self.assertIs(hull.get_src_disjunction(orig_inner_xor), m.d1.disj2)
1689
1690        # the same goes for the disaggregation constraint
1691        orig_dis_container = m.d1._pyomo_gdp_hull_reformulation.\
1692                             disaggregationConstraints
1693        orig_dis = orig_dis_container[0,None]
1694        self.assertIs(hull.get_disaggregation_constraint(m.x, m.d1.disj2),
1695                      orig_dis)
1696        self.assertFalse(orig_dis.active)
1697        transformedList = hull.get_transformed_constraints(orig_dis)
1698        self.assertEqual(len(transformedList), 1)
1699        self.assertIs(transformedList[0], dis_cons_inner_disjunction[(0, None,
1700                                                                      'eq')])
1701
1702        self.assertIs(hull.get_src_constraint(
1703            dis_cons_inner_disjunction[(0, None, 'eq')]), orig_dis)
1704        self.assertIs(hull.get_src_constraint( dis_cons_inner_disjunction),
1705                      orig_dis_container)
1706        # though we don't have a map back from the disaggregation constraint to
1707        # the variable because I'm not sure why you would... The variable is in
1708        # the constraint.
1709
1710        # check the inner disjunct mappings
1711        self.assertIs(m.d1.d3.transformation_block(),
1712                      m.d1._pyomo_gdp_hull_reformulation.relaxedDisjuncts[0])
1713        self.assertIs(hull.get_src_disjunct(
1714            m.d1._pyomo_gdp_hull_reformulation.relaxedDisjuncts[0]), m.d1.d3)
1715        self.assertIs(m.d1.d4.transformation_block(),
1716                      m.d1._pyomo_gdp_hull_reformulation.relaxedDisjuncts[1])
1717        self.assertIs(hull.get_src_disjunct(
1718            m.d1._pyomo_gdp_hull_reformulation.relaxedDisjuncts[1]), m.d1.d4)
1719
1720    @unittest.skipIf(not linear_solvers, "No linear solver available")
1721    def test_solve_nested_model(self):
1722        # This is really a test that our variable references have all been moved
1723        # up correctly.
1724        m = models.makeNestedDisjunctions_NestedDisjuncts()
1725
1726        hull = TransformationFactory('gdp.hull')
1727        m_hull = hull.create_using(m)
1728
1729        SolverFactory(linear_solvers[0]).solve(m_hull)
1730
1731        # check solution
1732        self.assertEqual(value(m_hull.d1.binary_indicator_var), 0)
1733        self.assertEqual(value(m_hull.d2.binary_indicator_var), 1)
1734        self.assertEqual(value(m_hull.x), 1.1)
1735
1736        # transform inner problem with bigm, outer with hull and make sure it
1737        # still works
1738        TransformationFactory('gdp.bigm').apply_to(m, targets=(m.d1.disj2))
1739        hull.apply_to(m)
1740
1741        SolverFactory(linear_solvers[0]).solve(m)
1742
1743        # check solution
1744        self.assertEqual(value(m.d1.binary_indicator_var), 0)
1745        self.assertEqual(value(m.d2.binary_indicator_var), 1)
1746        self.assertEqual(value(m.x), 1.1)
1747
1748    @unittest.skipIf(not linear_solvers, "No linear solver available")
1749    def test_disaggregated_vars_are_set_to_0_correctly(self):
1750        m = models.makeNestedDisjunctions_FlatDisjuncts()
1751        hull = TransformationFactory('gdp.hull')
1752        hull.apply_to(m)
1753
1754        # this should be a feasible integer solution
1755        m.d1.indicator_var.fix(0)
1756        m.d2.indicator_var.fix(1)
1757        m.d3.indicator_var.fix(0)
1758        m.d4.indicator_var.fix(0)
1759
1760        results = SolverFactory(linear_solvers[0]).solve(m)
1761        self.assertEqual(results.solver.termination_condition,
1762                         TerminationCondition.optimal)
1763        self.assertEqual(value(m.x), 1.1)
1764
1765        self.assertEqual(value(hull.get_disaggregated_var(m.x, m.d1)), 0)
1766        self.assertEqual(value(hull.get_disaggregated_var(m.x, m.d2)), 1.1)
1767        self.assertEqual(value(hull.get_disaggregated_var(m.x, m.d3)), 0)
1768        self.assertEqual(value(hull.get_disaggregated_var(m.x, m.d4)), 0)
1769
1770        # and what if one of the inner disjuncts is true?
1771        m.d1.indicator_var.fix(1)
1772        m.d2.indicator_var.fix(0)
1773        m.d3.indicator_var.fix(1)
1774        m.d4.indicator_var.fix(0)
1775
1776        results = SolverFactory(linear_solvers[0]).solve(m)
1777        self.assertEqual(results.solver.termination_condition,
1778                         TerminationCondition.optimal)
1779        self.assertEqual(value(m.x), 1.2)
1780
1781        self.assertEqual(value(hull.get_disaggregated_var(m.x, m.d1)), 1.2)
1782        self.assertEqual(value(hull.get_disaggregated_var(m.x, m.d2)), 0)
1783        self.assertEqual(value(hull.get_disaggregated_var(m.x, m.d3)), 1.2)
1784        self.assertEqual(value(hull.get_disaggregated_var(m.x, m.d4)), 0)
1785
1786class TestSpecialCases(unittest.TestCase):
1787    def test_local_vars(self):
1788        """ checks that if nothing is marked as local, we assume it is all
1789        global. We disaggregate everything to be safe."""
1790        m = ConcreteModel()
1791        m.x = Var(bounds=(5,100))
1792        m.y = Var(bounds=(0,100))
1793        m.d1 = Disjunct()
1794        m.d1.c = Constraint(expr=m.y >= m.x)
1795        m.d2 = Disjunct()
1796        m.d2.z = Var()
1797        m.d2.c = Constraint(expr=m.y >= m.d2.z)
1798        m.disj = Disjunction(expr=[m.d1, m.d2])
1799
1800        self.assertRaisesRegex(
1801            GDP_Error,
1802            ".*Missing bound for d2.z.*",
1803            TransformationFactory('gdp.hull').create_using,
1804            m)
1805        m.d2.z.setlb(7)
1806        self.assertRaisesRegex(
1807            GDP_Error,
1808            ".*Missing bound for d2.z.*",
1809            TransformationFactory('gdp.hull').create_using,
1810            m)
1811        m.d2.z.setub(9)
1812
1813        i = TransformationFactory('gdp.hull').create_using(m)
1814        rd = i._pyomo_gdp_hull_reformulation.relaxedDisjuncts[1]
1815        varBlock = rd.disaggregatedVars
1816        # z should be disaggregated because we can't be sure it's not somewhere
1817        # else on the model. (Note however that the copy of x corresponding to
1818        # this disjunct is on the disjunction block)
1819        self.assertEqual(sorted(varBlock.component_map(Var)), ['y','z'])
1820        # constraint on the disjunction block
1821        self.assertEqual(len(rd.component_map(Constraint)), 3)
1822        # bounds haven't changed on original
1823        self.assertEqual(i.d2.z.bounds, (7,9))
1824        # check disaggregated variable
1825        self.assertIsInstance(varBlock.component("z"), Var)
1826        self.assertEqual(varBlock.z.bounds, (0,9))
1827        self.assertEqual(len(rd.z_bounds), 2)
1828        self.assertEqual(rd.z_bounds['lb'].lower, None)
1829        self.assertEqual(rd.z_bounds['lb'].upper, 0)
1830        self.assertEqual(rd.z_bounds['ub'].lower, None)
1831        self.assertEqual(rd.z_bounds['ub'].upper, 0)
1832        i.d2.indicator_var = 1
1833        varBlock.z = 2
1834        self.assertEqual(rd.z_bounds['lb'].body(), 5)
1835        self.assertEqual(rd.z_bounds['ub'].body(), -7)
1836
1837        m.d2.z.setlb(-9)
1838        m.d2.z.setub(-7)
1839        i = TransformationFactory('gdp.hull').create_using(m)
1840        rd = i._pyomo_gdp_hull_reformulation.relaxedDisjuncts[1]
1841        varBlock = rd.disaggregatedVars
1842        self.assertEqual(sorted(varBlock.component_map(Var)), ['y','z'])
1843        self.assertEqual(len(rd.component_map(Constraint)), 3)
1844        # original bounds unchanged
1845        self.assertEqual(i.d2.z.bounds, (-9,-7))
1846        # check disaggregated variable
1847        self.assertIsInstance(varBlock.component("z"), Var)
1848        self.assertEqual(varBlock.z.bounds, (-9,0))
1849        self.assertEqual(len(rd.z_bounds), 2)
1850        self.assertEqual(rd.z_bounds['lb'].lower, None)
1851        self.assertEqual(rd.z_bounds['lb'].upper, 0)
1852        self.assertEqual(rd.z_bounds['ub'].lower, None)
1853        self.assertEqual(rd.z_bounds['ub'].upper, 0)
1854        i.d2.indicator_var = 1
1855        varBlock.z = 2
1856        self.assertEqual(rd.z_bounds['lb'].body(), -11)
1857        self.assertEqual(rd.z_bounds['ub'].body(), 9)
1858
1859    def test_local_var_suffix(self):
1860        hull = TransformationFactory('gdp.hull')
1861
1862        model = ConcreteModel()
1863        model.x = Var(bounds=(5,100))
1864        model.y = Var(bounds=(0,100))
1865        model.d1 = Disjunct()
1866        model.d1.c = Constraint(expr=model.y >= model.x)
1867        model.d2 = Disjunct()
1868        model.d2.z = Var(bounds=(-9, -7))
1869        model.d2.c = Constraint(expr=model.y >= model.d2.z)
1870        model.disj = Disjunction(expr=[model.d1, model.d2])
1871
1872        # we don't declare z local
1873        m = hull.create_using(model)
1874        self.assertEqual(m.d2.z.lb, -9)
1875        self.assertEqual(m.d2.z.ub, -7)
1876        z_disaggregated = m.d2.transformation_block().disaggregatedVars.\
1877                          component("z")
1878        self.assertIsInstance(z_disaggregated, Var)
1879        self.assertIs(z_disaggregated,
1880                      hull.get_disaggregated_var(m.d2.z, m.d2))
1881
1882        # we do declare z local
1883        model.d2.LocalVars = Suffix(direction=Suffix.LOCAL)
1884        model.d2.LocalVars[model.d2] = [model.d2.z]
1885
1886        m = hull.create_using(model)
1887
1888        # make sure we did not disaggregate z
1889        self.assertEqual(m.d2.z.lb, -9)
1890        self.assertEqual(m.d2.z.ub, 0)
1891        # it is its own disaggregated variable
1892        self.assertIs(hull.get_disaggregated_var(m.d2.z, m.d2), m.d2.z)
1893        # it does not exist on the transformation block
1894        self.assertIsNone(m.d2.transformation_block().disaggregatedVars.\
1895                          component("z"))
1896
1897class UntransformableObjectsOnDisjunct(unittest.TestCase):
1898    def test_RangeSet(self):
1899        ct.check_RangeSet(self, 'hull')
1900
1901    def test_Expression(self):
1902        ct.check_Expression(self, 'hull')
1903
1904class TransformABlock(unittest.TestCase, CommonTests):
1905    def test_transformation_simple_block(self):
1906        ct.check_transformation_simple_block(self, 'hull')
1907
1908    def test_transform_block_data(self):
1909        ct.check_transform_block_data(self, 'hull')
1910
1911    def test_simple_block_target(self):
1912        ct.check_simple_block_target(self, 'hull')
1913
1914    def test_block_data_target(self):
1915        ct.check_block_data_target(self, 'hull')
1916
1917    def test_indexed_block_target(self):
1918        ct.check_indexed_block_target(self, 'hull')
1919
1920    def test_block_targets_inactive(self):
1921        ct.check_block_targets_inactive(self, 'hull')
1922
1923    def test_block_only_targets_transformed(self):
1924        ct.check_block_only_targets_transformed(self, 'hull')
1925
1926    def test_create_using(self):
1927        m = models.makeTwoTermDisjOnBlock()
1928        ct.diff_apply_to_and_create_using(self, m, 'gdp.hull')
1929
1930class DisjOnBlock(unittest.TestCase, CommonTests):
1931    # when the disjunction is on a block, we want all of the stuff created by
1932    # the transformation to go on that block also so that solving the block
1933    # maintains its meaning
1934
1935    def test_xor_constraint_added(self):
1936        ct.check_xor_constraint_added(self, 'hull')
1937
1938    def test_trans_block_created(self):
1939        ct.check_trans_block_created(self, 'hull')
1940
1941class TestErrors(unittest.TestCase):
1942    def setUp(self):
1943        # set seed so we can test name collisions predictably
1944        random.seed(666)
1945
1946    def test_ask_for_transformed_constraint_from_untransformed_disjunct(self):
1947        ct.check_ask_for_transformed_constraint_from_untransformed_disjunct(
1948            self, 'hull')
1949
1950    def test_silly_target(self):
1951        ct.check_silly_target(self, 'hull')
1952
1953    def test_retrieving_nondisjunctive_components(self):
1954        ct.check_retrieving_nondisjunctive_components(self, 'hull')
1955
1956    def test_transform_empty_disjunction(self):
1957        ct.check_transform_empty_disjunction(self, 'hull')
1958
1959    def test_deactivated_disjunct_nonzero_indicator_var(self):
1960        ct.check_deactivated_disjunct_nonzero_indicator_var(self,
1961                                                            'hull')
1962
1963    def test_deactivated_disjunct_unfixed_indicator_var(self):
1964        ct.check_deactivated_disjunct_unfixed_indicator_var(self, 'hull')
1965
1966    def test_infeasible_xor_because_all_disjuncts_deactivated(self):
1967        m = ct.setup_infeasible_xor_because_all_disjuncts_deactivated(self,
1968                                                                      'hull')
1969        hull = TransformationFactory('gdp.hull')
1970        transBlock = m.component("_pyomo_gdp_hull_reformulation")
1971        self.assertIsInstance(transBlock, Block)
1972        self.assertEqual(len(transBlock.relaxedDisjuncts), 2)
1973        self.assertIsInstance(transBlock.component("disjunction_xor"),
1974                              Constraint)
1975        disjunct1 = transBlock.relaxedDisjuncts[0]
1976        # we disaggregated the (deactivated) indicator variables
1977        d3_ind = m.disjunction_disjuncts[0].nestedDisjunction_disjuncts[0].\
1978                 binary_indicator_var
1979        d4_ind = m.disjunction_disjuncts[0].nestedDisjunction_disjuncts[1].\
1980                 binary_indicator_var
1981        self.assertIs(hull.get_disaggregated_var(d3_ind,
1982                                                 m.disjunction_disjuncts[0]),
1983                      disjunct1.disaggregatedVars.binary_indicator_var)
1984        self.assertIs(hull.get_src_var(
1985            disjunct1.disaggregatedVars.binary_indicator_var), d3_ind)
1986        self.assertIs(hull.get_disaggregated_var(d4_ind,
1987                                                  m.disjunction_disjuncts[0]),
1988                      disjunct1.disaggregatedVars.binary_indicator_var_4)
1989        self.assertIs(hull.get_src_var(
1990            disjunct1.disaggregatedVars.binary_indicator_var_4), d4_ind)
1991
1992        relaxed_xor = disjunct1.component(
1993            "disjunction_disjuncts[0]._pyomo_gdp_hull_reformulation."
1994            "'disjunction_disjuncts[0].nestedDisjunction_xor'")
1995        self.assertIsInstance(relaxed_xor, Constraint)
1996        self.assertEqual(len(relaxed_xor), 1)
1997        repn = generate_standard_repn(relaxed_xor['eq'].body)
1998        self.assertEqual(relaxed_xor['eq'].lower, 0)
1999        self.assertEqual(relaxed_xor['eq'].upper, 0)
2000        self.assertTrue(repn.is_linear())
2001        self.assertEqual(len(repn.linear_vars), 3)
2002        # constraint says that the disaggregated indicator variables of the
2003        # nested disjuncts sum to the indicator variable of the outer disjunct.
2004        ct.check_linear_coef(
2005            self, repn, m.disjunction.disjuncts[0].indicator_var, -1)
2006        ct.check_linear_coef(
2007            self, repn, disjunct1.disaggregatedVars.binary_indicator_var, 1)
2008        ct.check_linear_coef(
2009            self, repn, disjunct1.disaggregatedVars.binary_indicator_var_4, 1)
2010        self.assertEqual(repn.constant, 0)
2011
2012        # but the disaggregation constraints are going to force them to 0 (which
2013        # will in turn force the outer disjunct indicator variable to 0, which
2014        # is what we want)
2015        d3_ind_dis = transBlock.disaggregationConstraints[1, None]
2016        self.assertEqual(d3_ind_dis.lower, 0)
2017        self.assertEqual(d3_ind_dis.upper, 0)
2018        repn = generate_standard_repn(d3_ind_dis.body)
2019        self.assertTrue(repn.is_linear())
2020        self.assertEqual(len(repn.linear_vars), 2)
2021        self.assertEqual(repn.constant, 0)
2022        ct.check_linear_coef(
2023            self, repn, disjunct1.disaggregatedVars.binary_indicator_var, -1)
2024        ct.check_linear_coef(self, repn, transBlock._disaggregatedVars[0], -1)
2025        d4_ind_dis = transBlock.disaggregationConstraints[2, None]
2026        self.assertEqual(d4_ind_dis.lower, 0)
2027        self.assertEqual(d4_ind_dis.upper, 0)
2028        repn = generate_standard_repn(d4_ind_dis.body)
2029        self.assertTrue(repn.is_linear())
2030        self.assertEqual(len(repn.linear_vars), 2)
2031        self.assertEqual(repn.constant, 0)
2032        ct.check_linear_coef(
2033            self, repn,
2034            disjunct1.disaggregatedVars.binary_indicator_var_4, -1)
2035        ct.check_linear_coef( self, repn, transBlock._disaggregatedVars[1], -1)
2036
2037    def test_mapping_method_errors(self):
2038        m = models.makeTwoTermDisj_Nonlinear()
2039        hull = TransformationFactory('gdp.hull')
2040        hull.apply_to(m)
2041
2042        log = StringIO()
2043        with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR):
2044            self.assertRaisesRegex(
2045                AttributeError,
2046                "'NoneType' object has no attribute 'parent_block'",
2047                hull.get_var_bounds_constraint,
2048                m.w)
2049        self.assertRegex(
2050            log.getvalue(),
2051            ".*Either 'w' is not a disaggregated variable, "
2052            "or the disjunction that disaggregates it has "
2053            "not been properly transformed.")
2054
2055        log = StringIO()
2056        with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR):
2057            self.assertRaisesRegex(
2058                KeyError,
2059                r".*_pyomo_gdp_hull_reformulation.relaxedDisjuncts\[1\]."
2060                r"disaggregatedVars.w",
2061                hull.get_disaggregation_constraint,
2062                m.d[1].transformation_block().disaggregatedVars.w,
2063                m.disjunction)
2064        self.assertRegex(log.getvalue(), ".*It doesn't appear that "
2065                         r"'_pyomo_gdp_hull_reformulation."
2066                         r"relaxedDisjuncts\[1\].disaggregatedVars.w' "
2067                         r"is a variable that was disaggregated by "
2068                         r"Disjunction 'disjunction'")
2069
2070        log = StringIO()
2071        with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR):
2072            self.assertRaisesRegex(
2073                AttributeError,
2074                "'NoneType' object has no attribute 'parent_block'",
2075                hull.get_src_var,
2076                m.w)
2077        self.assertRegex(
2078            log.getvalue(),
2079            ".*'w' does not appear to be a disaggregated variable")
2080
2081        log = StringIO()
2082        with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR):
2083            self.assertRaisesRegex(
2084                KeyError,
2085                r".*_pyomo_gdp_hull_reformulation.relaxedDisjuncts\[1\]."
2086                r"disaggregatedVars.w",
2087                hull.get_disaggregated_var,
2088                m.d[1].transformation_block().disaggregatedVars.w,
2089                m.d[1])
2090        self.assertRegex(log.getvalue(),
2091                         r".*It does not appear "
2092                         r"'_pyomo_gdp_hull_reformulation."
2093                         r"relaxedDisjuncts\[1\].disaggregatedVars.w' "
2094                         r"is a variable which appears in disjunct "
2095                         r"'d\[1\]'")
2096
2097        m.random_disjunction = Disjunction(expr=[m.w == 2, m.w >= 7])
2098        self.assertRaisesRegex(
2099            GDP_Error,
2100            "Disjunction 'random_disjunction' has not been properly "
2101            "transformed: None of its disjuncts are transformed.",
2102            hull.get_disaggregation_constraint,
2103            m.w,
2104            m.random_disjunction)
2105
2106        self.assertRaisesRegex(
2107            GDP_Error,
2108            r"Disjunct 'random_disjunction_disjuncts\[0\]' has not been "
2109            r"transformed",
2110            hull.get_disaggregated_var,
2111            m.w,
2112            m.random_disjunction.disjuncts[0])
2113
2114    def test_untransformed_arcs(self):
2115        ct.check_untransformed_network_raises_GDPError(self, 'hull')
2116
2117class BlocksOnDisjuncts(unittest.TestCase):
2118    def setUp(self):
2119        # set seed so we can test name collisions predictably
2120        random.seed(666)
2121
2122    def makeModel(self):
2123        # I'm going to multi-task and also check some types of constraints
2124        # whose expressions need to be tested
2125        m = ConcreteModel()
2126        m.x = Var(bounds=(1, 5))
2127        m.y = Var(bounds=(0, 9))
2128        m.disj1 = Disjunct()
2129        m.disj1.add_component("b.any_index", Constraint(expr=m.x >= 1.5))
2130        m.disj1.b = Block()
2131        m.disj1.b.any_index = Constraint(Any)
2132        m.disj1.b.any_index['local'] = m.x <= 2
2133        m.disj1.b.LocalVars = Suffix(direction=Suffix.LOCAL)
2134        m.disj1.b.LocalVars[m.disj1] = [m.x]
2135        m.disj1.b.any_index['nonlin-ub'] = m.y**2 <= 4
2136        m.disj2 = Disjunct()
2137        m.disj2.non_lin_lb = Constraint(expr=log(1 + m.y) >= 1)
2138        m.disjunction = Disjunction(expr=[m.disj1, m.disj2])
2139        return m
2140
2141    def test_transformed_constraint_name_conflict(self):
2142        m = self.makeModel()
2143
2144        hull = TransformationFactory('gdp.hull')
2145        hull.apply_to(m)
2146
2147        transBlock = m.disj1.transformation_block()
2148        self.assertIsInstance(transBlock.component("disj1.b.any_index"),
2149                              Constraint)
2150        self.assertIsInstance(transBlock.component("disj1.'b.any_index'"),
2151                              Constraint)
2152        xformed = hull.get_transformed_constraints(
2153            m.disj1.component("b.any_index"))
2154        self.assertEqual(len(xformed), 1)
2155        self.assertIs(xformed[0],
2156                      transBlock.component("disj1.'b.any_index'")['lb'])
2157
2158        xformed = hull.get_transformed_constraints(m.disj1.b.any_index['local'])
2159        self.assertEqual(len(xformed), 1)
2160        self.assertIs(xformed[0],
2161                      transBlock.component("disj1.b.any_index")[
2162                          ('local','ub')])
2163        xformed = hull.get_transformed_constraints(
2164            m.disj1.b.any_index['nonlin-ub'])
2165        self.assertEqual(len(xformed), 1)
2166        self.assertIs(xformed[0],
2167                      transBlock.component("disj1.b.any_index")[
2168                          ('nonlin-ub','ub')])
2169
2170    def test_local_var_handled_correctly(self):
2171        m = self.makeModel()
2172
2173        hull = TransformationFactory('gdp.hull')
2174        hull.apply_to(m)
2175
2176        # test the local variable was handled correctly.
2177        self.assertIs(hull.get_disaggregated_var(m.x, m.disj1), m.x)
2178        self.assertEqual(m.x.lb, 0)
2179        self.assertEqual(m.x.ub, 5)
2180        self.assertIsNone(m.disj1.transformation_block().disaggregatedVars.\
2181                          component("x"))
2182        self.assertIsInstance(m.disj1.transformation_block().disaggregatedVars.\
2183                              component("y"), Var)
2184
2185    # this doesn't require the block, I'm just coopting this test to make sure
2186    # of some nonlinear expressions.
2187    def test_transformed_constraints(self):
2188        m = self.makeModel()
2189
2190        hull = TransformationFactory('gdp.hull')
2191        hull.apply_to(m)
2192
2193        # test the transformed nonlinear constraints
2194        nonlin_ub_list = hull.get_transformed_constraints(
2195            m.disj1.b.any_index['nonlin-ub'])
2196        self.assertEqual(len(nonlin_ub_list), 1)
2197        cons = nonlin_ub_list[0]
2198        self.assertEqual(cons.index(), ('nonlin-ub', 'ub'))
2199        self.assertIs(cons.ctype, Constraint)
2200        self.assertIsNone(cons.lower)
2201        self.assertEqual(value(cons.upper), 0)
2202        repn = generate_standard_repn(cons.body)
2203        self.assertEqual(str(repn.nonlinear_expr),
2204                         "(0.9999*disj1.binary_indicator_var + 0.0001)*"
2205                         "(_pyomo_gdp_hull_reformulation.relaxedDisjuncts[0]."
2206                         "disaggregatedVars.y/"
2207                         "(0.9999*disj1.binary_indicator_var + 0.0001))**2")
2208        self.assertEqual(len(repn.nonlinear_vars), 2)
2209        self.assertIs(repn.nonlinear_vars[0], m.disj1.binary_indicator_var)
2210        self.assertIs(repn.nonlinear_vars[1],
2211                      hull.get_disaggregated_var(m.y, m.disj1))
2212        self.assertEqual(repn.constant, 0)
2213        self.assertEqual(len(repn.linear_vars), 1)
2214        self.assertIs(repn.linear_vars[0], m.disj1.binary_indicator_var)
2215        self.assertEqual(repn.linear_coefs[0], -4)
2216
2217        nonlin_lb_list = hull.get_transformed_constraints(m.disj2.non_lin_lb)
2218        self.assertEqual(len(nonlin_lb_list), 1)
2219        cons = nonlin_lb_list[0]
2220        self.assertEqual(cons.index(), 'lb')
2221        self.assertIs(cons.ctype, Constraint)
2222        self.assertIsNone(cons.lower)
2223        self.assertEqual(value(cons.upper), 0)
2224        repn = generate_standard_repn(cons.body)
2225        self.assertEqual(str(repn.nonlinear_expr),
2226                         "- ((0.9999*disj2.binary_indicator_var + 0.0001)*"
2227                         "log(1 + "
2228                         "_pyomo_gdp_hull_reformulation.relaxedDisjuncts[1]."
2229                         "disaggregatedVars.y/"
2230                         "(0.9999*disj2.binary_indicator_var + 0.0001)))")
2231        self.assertEqual(len(repn.nonlinear_vars), 2)
2232        self.assertIs(repn.nonlinear_vars[0], m.disj2.binary_indicator_var)
2233        self.assertIs(repn.nonlinear_vars[1],
2234                      hull.get_disaggregated_var(m.y, m.disj2))
2235        self.assertEqual(repn.constant, 0)
2236        self.assertEqual(len(repn.linear_vars), 1)
2237        self.assertIs(repn.linear_vars[0], m.disj2.binary_indicator_var)
2238        self.assertEqual(repn.linear_coefs[0], 1)
2239
2240class DisaggregatingFixedVars(unittest.TestCase):
2241    def test_disaggregate_fixed_variables(self):
2242        m = models.makeTwoTermDisj()
2243        m.x.fix(6)
2244        hull = TransformationFactory('gdp.hull')
2245        hull.apply_to(m)
2246        # check that we did indeed disaggregate x
2247        transBlock = m.d[1]._transformation_block()
2248        self.assertIsInstance(transBlock.disaggregatedVars.component("x"), Var)
2249        self.assertIs(hull.get_disaggregated_var(m.x, m.d[1]),
2250                      transBlock.disaggregatedVars.x)
2251        self.assertIs(hull.get_src_var(transBlock.disaggregatedVars.x), m.x)
2252
2253    def test_do_not_disaggregate_fixed_variables(self):
2254        m = models.makeTwoTermDisj()
2255        m.x.fix(6)
2256        hull = TransformationFactory('gdp.hull')
2257        hull.apply_to(m, assume_fixed_vars_permanent=True)
2258        # check that we didn't disaggregate x
2259        transBlock = m.d[1]._transformation_block()
2260        self.assertIsNone(transBlock.disaggregatedVars.component("x"))
2261
2262class NameDeprecationTest(unittest.TestCase):
2263    def test_name_deprecated(self):
2264        m = models.makeTwoTermDisj()
2265        output = StringIO()
2266        with LoggingIntercept(output, 'pyomo.gdp', logging.WARNING):
2267            TransformationFactory('gdp.chull').apply_to(m)
2268        self.assertIn("DEPRECATED: The 'gdp.chull' name is deprecated. "
2269                      "Please use the more apt 'gdp.hull' instead.",
2270                      output.getvalue().replace('\n', ' '))
2271
2272    def test_hull_chull_equivalent(self):
2273        m = models.makeTwoTermDisj()
2274        out1 = StringIO()
2275        out2 = StringIO()
2276        m1 = TransformationFactory('gdp.hull').create_using(m)
2277        m2 = TransformationFactory('gdp.chull').create_using(m)
2278        m1.pprint(ostream=out1)
2279        m2.pprint(ostream=out2)
2280        self.assertMultiLineEqual(out1.getvalue(), out2.getvalue())
2281
2282class KmeansTest(unittest.TestCase):
2283    @unittest.skipIf('gurobi' not in linear_solvers,
2284                     "Gurobi solver not available")
2285    def test_optimal_soln_feasible(self):
2286        m = ConcreteModel()
2287        m.Points = RangeSet(3)
2288        m.Centroids = RangeSet(2)
2289
2290        m.X = Param(m.Points, initialize={1:0.3672, 2:0.8043, 3:0.3059})
2291
2292        m.cluster_center = Var(m.Centroids, bounds=(0,2))
2293        m.distance = Var(m.Points, bounds=(0,2))
2294        m.t = Var(m.Points, m.Centroids, bounds=(0,2))
2295
2296        @m.Disjunct(m.Points, m.Centroids)
2297        def AssignPoint(d, i, k):
2298            m = d.model()
2299            d.LocalVars = Suffix(direction=Suffix.LOCAL)
2300            d.LocalVars[d] = [m.t[i,k]]
2301            def distance1(d):
2302                return m.t[i,k] >= m.X[i] - m.cluster_center[k]
2303            def distance2(d):
2304                return m.t[i,k] >= - (m.X[i] - m.cluster_center[k])
2305            d.dist1 = Constraint(rule=distance1)
2306            d.dist2 = Constraint(rule=distance2)
2307            d.define_distance = Constraint(expr=m.distance[i] == m.t[i,k])
2308
2309        @m.Disjunction(m.Points)
2310        def OneCentroidPerPt(m, i):
2311            return [m.AssignPoint[i, k] for k in m.Centroids]
2312
2313        m.obj = Objective(expr=sum(m.distance[i] for i in m.Points))
2314
2315        TransformationFactory('gdp.hull').apply_to(m)
2316
2317        # fix an optimal solution
2318        m.AssignPoint[1,1].indicator_var.fix(1)
2319        m.AssignPoint[1,2].indicator_var.fix(0)
2320        m.AssignPoint[2,1].indicator_var.fix(0)
2321        m.AssignPoint[2,2].indicator_var.fix(1)
2322        m.AssignPoint[3,1].indicator_var.fix(1)
2323        m.AssignPoint[3,2].indicator_var.fix(0)
2324
2325        m.cluster_center[1].fix(0.3059)
2326        m.cluster_center[2].fix(0.8043)
2327
2328        m.distance[1].fix(0.0613)
2329        m.distance[2].fix(0)
2330        m.distance[3].fix(0)
2331
2332        m.t[1,1].fix(0.0613)
2333        m.t[1,2].fix(0)
2334        m.t[2,1].fix(0)
2335        m.t[2,2].fix(0)
2336        m.t[3,1].fix(0)
2337        m.t[3,2].fix(0)
2338
2339        results = SolverFactory('gurobi').solve(m)
2340
2341        self.assertEqual(results.solver.termination_condition,
2342                         TerminationCondition.optimal)
2343
2344        TOL = 1e-8
2345        for c in m.component_data_objects(Constraint, active=True):
2346            if c.lower is not None:
2347                self.assertGreaterEqual(value(c.body) + TOL, value(c.lower))
2348            if c.upper is not None:
2349                self.assertLessEqual(value(c.body) - TOL, value(c.upper))
2350
2351class NetworkDisjuncts(unittest.TestCase, CommonTests):
2352
2353    @unittest.skipIf(not ct.linear_solvers, "No linear solver available")
2354    def test_solution_maximize(self):
2355        ct.check_network_disjucts(self, minimize=False, transformation='hull')
2356
2357    @unittest.skipIf(not ct.linear_solvers, "No linear solver available")
2358    def test_solution_minimize(self):
2359        ct.check_network_disjucts(self, minimize=True, transformation='hull')
2360