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