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