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"""Various conic constraint implementations.""" 11 12from pyomo.core.expr.numvalue import is_numeric_data 13from pyomo.core.expr.current import (value, 14 exp) 15from pyomo.core.kernel.block import block 16from pyomo.core.kernel.variable import (IVariable, 17 variable, 18 variable_tuple) 19from pyomo.core.kernel.constraint import (IConstraint, 20 linear_constraint, 21 constraint, 22 constraint_tuple) 23 24def _build_linking_constraints(v, v_aux): 25 assert len(v) == len(v_aux) 26 c_aux = [] 27 for vi, vi_aux in zip(v, v_aux): 28 assert vi_aux.ctype is IVariable 29 if vi is None: 30 continue 31 elif is_numeric_data(vi): 32 c_aux.append( 33 linear_constraint(variables=(vi_aux,), 34 coefficients=(1,), 35 rhs=vi)) 36 elif isinstance(vi, IVariable): 37 c_aux.append( 38 linear_constraint(variables=(vi_aux, vi), 39 coefficients=(1, -1), 40 rhs=0)) 41 else: 42 c_aux.append( 43 constraint(body=vi_aux - vi, 44 rhs=0)) 45 return constraint_tuple(c_aux) 46 47class _ConicBase(IConstraint): 48 """Base class for a few conic constraints that 49 implements some shared functionality. Derived classes 50 are expected to declare any necessary slots.""" 51 _ctype = IConstraint 52 _linear_canonical_form = False 53 __slots__ = () 54 55 def __init__(self): 56 self._parent = None 57 self._storage_key = None 58 self._active = True 59 # the body expression is only built if necessary 60 # (i.e., when someone asks for it via the body 61 # property method) 62 self._body = None 63 64 @classmethod 65 def as_domain(cls, *args, **kwds): 66 """Builds a conic domain""" 67 raise NotImplementedError #pragma:nocover 68 69 def _body_function(self, *args): 70 """A function that defines the body expression""" 71 raise NotImplementedError #pragma:nocover 72 73 def _body_function_variables(self, values=False): 74 """Returns variables in the order they should be 75 passed to the body function. If values is True, then 76 return the current value of each variable in place 77 of the variables themselves.""" 78 raise NotImplementedError #pragma:nocover 79 80 def check_convexity_conditions(self, relax=False): 81 """Returns True if all convexity conditions for the 82 conic constraint are satisfied. If relax is True, 83 then variable domains are ignored and it is assumed 84 that all variables are continuous.""" 85 raise NotImplementedError #pragma:nocover 86 87 # 88 # Define the IConstraint abstract methods 89 # 90 91 @property 92 def body(self): 93 """The body of the constraint""" 94 if self._body is None: 95 self._body = self._body_function( 96 *self._body_function_variables(values=False)) 97 return self._body 98 99 @property 100 def lb(self): 101 """The lower bound of the constraint""" 102 return None 103 104 @property 105 def ub(self): 106 """The upper bound of the constraint""" 107 return 0.0 108 109 @property 110 def rhs(self): 111 """The right-hand side of the constraint""" 112 raise ValueError( 113 "The rhs property can not be read because this " 114 "is not an equality constraint") 115 116 @property 117 def equality(self): 118 return False 119 120 # 121 # Override a the default __call__ method on IConstraint 122 # to avoid building the body expression, if possible 123 # 124 125 def __call__(self, exception=True): 126 try: 127 # we wrap the result with value(...) as the 128 # alpha term used by some of the constraints 129 # may be a parameter 130 return value(self._body_function( 131 *self._body_function_variables(values=True))) 132 except (ValueError, TypeError): 133 if exception: 134 raise ValueError("one or more terms " 135 "could not be evaluated") 136 return None 137 138class quadratic(_ConicBase): 139 """A quadratic conic constraint of the form: 140 141 x[0]^2 + ... + x[n-1]^2 <= r^2, 142 143 which is recognized as convex for r >= 0. 144 145 Parameters 146 ---------- 147 r : :class:`variable` 148 A variable. 149 x : list[:class:`variable`] 150 An iterable of variables. 151 """ 152 __slots__ = ("_parent", 153 "_storage_key", 154 "_active", 155 "_body", 156 "_r", 157 "_x", 158 "__weakref__") 159 def __init__(self, r, x): 160 super(quadratic, self).__init__() 161 self._r = r 162 self._x = tuple(x) 163 assert isinstance(self._r, IVariable) 164 assert all(isinstance(xi, IVariable) 165 for xi in self._x) 166 167 @classmethod 168 def as_domain(cls, r, x): 169 """Builds a conic domain. Input arguments take the 170 same form as those of the conic constraint, but in 171 place of each variable, one can optionally supply a 172 constant, linear expression, or None. 173 174 Returns 175 ------- 176 block 177 A block object with the core conic constraint 178 (block.q) expressed using auxiliary variables 179 (block.r, block.x) linked to the input arguments 180 through auxiliary constraints (block.c). 181 """ 182 b = block() 183 b.r = variable(lb=0) 184 b.x = variable_tuple( 185 [variable() for i in range(len(x))]) 186 b.c = _build_linking_constraints([r] + list(x), 187 [b.r] + list(b.x)) 188 b.q = cls(r=b.r, x=b.x) 189 return b 190 191 @property 192 def r(self): 193 return self._r 194 195 @property 196 def x(self): 197 return self._x 198 199 # 200 # Define the _ConicBase abstract methods 201 # 202 203 def _body_function(self, r, x): 204 """A function that defines the body expression""" 205 return sum(xi**2 for xi in x) - r**2 206 207 def _body_function_variables(self, values=False): 208 """Returns variables in the order they should be 209 passed to the body function. If values is True, then 210 return the current value of each variable in place 211 of the variables themselves.""" 212 if not values: 213 return self.r, self.x 214 else: 215 return self.r.value, tuple(xi.value for xi in self.x) 216 217 def check_convexity_conditions(self, relax=False): 218 """Returns True if all convexity conditions for the 219 conic constraint are satisfied. If relax is True, 220 then variable domains are ignored and it is assumed 221 that all variables are continuous.""" 222 return (relax or \ 223 (self.r.is_continuous() and \ 224 all(xi.is_continuous() for xi in self.x))) and \ 225 (self.r.has_lb() and value(self.r.lb) >= 0) 226 227class rotated_quadratic(_ConicBase): 228 """A rotated quadratic conic constraint of the form: 229 230 x[0]^2 + ... + x[n-1]^2 <= 2*r1*r2, 231 232 which is recognized as convex for r1,r2 >= 0. 233 234 Parameters 235 ---------- 236 r1 : :class:`variable` 237 A variable. 238 r2 : :class:`variable` 239 A variable. 240 x : list[:class:`variable`] 241 An iterable of variables. 242 """ 243 __slots__ = ("_parent", 244 "_storage_key", 245 "_active", 246 "_body", 247 "_r1", 248 "_r2", 249 "_x", 250 "__weakref__") 251 252 def __init__(self, r1, r2, x): 253 super(rotated_quadratic, self).__init__() 254 self._r1 = r1 255 self._r2 = r2 256 self._x = tuple(x) 257 assert isinstance(self._r1, IVariable) 258 assert isinstance(self._r2, IVariable) 259 assert all(isinstance(xi, IVariable) 260 for xi in self._x) 261 262 @classmethod 263 def as_domain(cls, r1, r2, x): 264 """Builds a conic domain. Input arguments take the 265 same form as those of the conic constraint, but in 266 place of each variable, one can optionally supply a 267 constant, linear expression, or None. 268 269 Returns 270 ------- 271 block 272 A block object with the core conic constraint 273 (block.q) expressed using auxiliary variables 274 (block.r1, block.r2, block.x) linked to the 275 input arguments through auxiliary constraints 276 (block.c). 277 """ 278 b = block() 279 b.r1 = variable(lb=0) 280 b.r2 = variable(lb=0) 281 b.x = variable_tuple( 282 [variable() for i in range(len(x))]) 283 b.c = _build_linking_constraints([r1,r2] + list(x), 284 [b.r1,b.r2] + list(b.x)) 285 b.q = cls(r1=b.r1, r2=b.r2, x=b.x) 286 return b 287 288 @property 289 def r1(self): 290 return self._r1 291 292 @property 293 def r2(self): 294 return self._r2 295 296 @property 297 def x(self): 298 return self._x 299 300 # 301 # Define the _ConicBase abstract methods 302 # 303 304 def _body_function(self, r1, r2, x): 305 """A function that defines the body expression""" 306 return sum(xi**2 for xi in x) - 2*r1*r2 307 308 def _body_function_variables(self, values=False): 309 """Returns variables in the order they should be 310 passed to the body function. If values is True, then 311 return the current value of each variable in place 312 of the variables themselves.""" 313 if not values: 314 return self.r1, self.r2, self.x 315 else: 316 return self.r1.value, self.r2.value, \ 317 tuple(xi.value for xi in self.x) 318 319 def check_convexity_conditions(self, relax=False): 320 """Returns True if all convexity conditions for the 321 conic constraint are satisfied. If relax is True, 322 then variable domains are ignored and it is assumed 323 that all variables are continuous.""" 324 return (relax or \ 325 (self.r1.is_continuous() and \ 326 self.r2.is_continuous() and \ 327 all(xi.is_continuous() for xi in self.x))) and \ 328 (self.r1.has_lb() and value(self.r1.lb) >= 0) and \ 329 (self.r2.has_lb() and value(self.r2.lb) >= 0) 330 331class primal_exponential(_ConicBase): 332 """A primal exponential conic constraint of the form: 333 334 x1*exp(x2/x1) <= r, 335 336 which is recognized as convex for x1,r >= 0. 337 338 Parameters 339 ---------- 340 r : :class:`variable` 341 A variable. 342 x1 : :class:`variable` 343 A variable. 344 x2 : :class:`variable` 345 A variable. 346 """ 347 __slots__ = ("_parent", 348 "_storage_key", 349 "_active", 350 "_body", 351 "_r", 352 "_x1", 353 "_x2", 354 "__weakref__") 355 356 def __init__(self, r, x1, x2): 357 super(primal_exponential, self).__init__() 358 self._r = r 359 self._x1 = x1 360 self._x2 = x2 361 assert isinstance(self._r, IVariable) 362 assert isinstance(self._x1, IVariable) 363 assert isinstance(self._x2, IVariable) 364 365 @classmethod 366 def as_domain(cls, r, x1, x2): 367 """Builds a conic domain. Input arguments take the 368 same form as those of the conic constraint, but in 369 place of each variable, one can optionally supply a 370 constant, linear expression, or None. 371 372 Returns 373 ------- 374 block 375 A block object with the core conic constraint 376 (block.q) expressed using auxiliary variables 377 (block.r, block.x1, block.x2) linked to the 378 input arguments through auxiliary constraints 379 (block.c). 380 """ 381 b = block() 382 b.r = variable(lb=0) 383 b.x1 = variable(lb=0) 384 b.x2 = variable() 385 b.c = _build_linking_constraints([r,x1,x2], 386 [b.r,b.x1,b.x2]) 387 b.q = cls(r=b.r, x1=b.x1, x2=b.x2) 388 return b 389 390 @property 391 def r(self): 392 return self._r 393 394 @property 395 def x1(self): 396 return self._x1 397 398 @property 399 def x2(self): 400 return self._x2 401 402 # 403 # Define the _ConicBase abstract methods 404 # 405 406 def _body_function(self, r, x1, x2): 407 """A function that defines the body expression""" 408 return x1*exp(x2/x1) - r 409 410 def _body_function_variables(self, values=False): 411 """Returns variables in the order they should be 412 passed to the body function. If values is True, then 413 return the current value of each variable in place 414 of the variables themselves.""" 415 if not values: 416 return self.r, self.x1, self.x2 417 else: 418 return self.r.value, self.x1.value, self.x2.value 419 420 def check_convexity_conditions(self, relax=False): 421 """Returns True if all convexity conditions for the 422 conic constraint are satisfied. If relax is True, 423 then variable domains are ignored and it is assumed 424 that all variables are continuous.""" 425 return (relax or \ 426 (self.x1.is_continuous() and \ 427 self.x2.is_continuous() and \ 428 self.r.is_continuous())) and \ 429 (self.x1.has_lb() and value(self.x1.lb) >= 0) and \ 430 (self.r.has_lb() and value(self.r.lb) >= 0) 431 432class primal_power(_ConicBase): 433 """A primal power conic constraint of the form: 434 sqrt(x[0]^2 + ... + x[n-1]^2) <= (r1^alpha)*(r2^(1-alpha)) 435 436 which is recognized as convex for r1,r2 >= 0 437 and 0 < alpha < 1. 438 439 Parameters 440 ---------- 441 r1 : :class:`variable` 442 A variable. 443 r2 : :class:`variable` 444 A variable. 445 x : list[:class:`variable`] 446 An iterable of variables. 447 alpha : float, :class:`parameter`, etc. 448 A constant term. 449 """ 450 __slots__ = ("_parent", 451 "_storage_key", 452 "_active", 453 "_body", 454 "_r1", 455 "_r2", 456 "_x", 457 "_alpha", 458 "__weakref__") 459 460 def __init__(self, r1, r2, x, alpha): 461 super(primal_power, self).__init__() 462 self._r1 = r1 463 self._r2 = r2 464 self._x = tuple(x) 465 self._alpha = alpha 466 assert isinstance(self._r1, IVariable) 467 assert isinstance(self._r2, IVariable) 468 assert all(isinstance(xi, IVariable) 469 for xi in self._x) 470 if not is_numeric_data(self._alpha): 471 raise TypeError( 472 "The type of the alpha parameter of a conic " 473 "constraint is restricted numeric data or " 474 "objects that store numeric data.") 475 476 @classmethod 477 def as_domain(cls, r1, r2, x, alpha): 478 """Builds a conic domain. Input arguments take the 479 same form as those of the conic constraint, but in 480 place of each variable, one can optionally supply a 481 constant, linear expression, or None. 482 483 Returns 484 ------- 485 block 486 A block object with the core conic constraint 487 (block.q) expressed using auxiliary variables 488 (block.r1, block.r2, block.x) linked to the 489 input arguments through auxiliary constraints 490 (block.c). 491 """ 492 b = block() 493 b.r1 = variable(lb=0) 494 b.r2 = variable(lb=0) 495 b.x = variable_tuple( 496 [variable() for i in range(len(x))]) 497 b.c = _build_linking_constraints([r1,r2] + list(x), 498 [b.r1,b.r2] + list(b.x)) 499 b.q = cls(r1=b.r1, r2=b.r2, x=b.x, alpha=alpha) 500 return b 501 502 @property 503 def r1(self): 504 return self._r1 505 506 @property 507 def r2(self): 508 return self._r2 509 510 @property 511 def x(self): 512 return self._x 513 514 @property 515 def alpha(self): 516 return self._alpha 517 518 # 519 # Define the _ConicBase abstract methods 520 # 521 522 def _body_function(self, r1, r2, x): 523 """A function that defines the body expression""" 524 alpha = self.alpha 525 return (sum(xi**2 for xi in x)**0.5) - \ 526 (r1**alpha) * \ 527 (r2**(1-alpha)) 528 529 def _body_function_variables(self, values=False): 530 """Returns variables in the order they should be 531 passed to the body function. If values is True, then 532 return the current value of each variable in place 533 of the variables themselves.""" 534 if not values: 535 return self.r1, self.r2, self.x 536 else: 537 return self.r1.value, self.r2.value, \ 538 tuple(xi.value for xi in self.x) 539 540 def check_convexity_conditions(self, relax=False): 541 """Returns True if all convexity conditions for the 542 conic constraint are satisfied. If relax is True, 543 then variable domains are ignored and it is assumed 544 that all variables are continuous.""" 545 alpha = value(self.alpha, exception=False) 546 return (relax or \ 547 (self.r1.is_continuous() and \ 548 self.r2.is_continuous() and \ 549 all(xi.is_continuous() for xi in self.x))) and \ 550 (self.r1.has_lb() and value(self.r1.lb) >= 0) and \ 551 (self.r2.has_lb() and value(self.r2.lb) >= 0) and \ 552 ((alpha is not None) and (0 < alpha < 1)) 553 554class dual_exponential(_ConicBase): 555 """A dual exponential conic constraint of the form: 556 557 -x2*exp((x1/x2)-1) <= r 558 559 which is recognized as convex for x2 <= 0 and r >= 0. 560 561 Parameters 562 ---------- 563 r : :class:`variable` 564 A variable. 565 x1 : :class:`variable` 566 A variable. 567 x2 : :class:`variable` 568 A variable. 569 """ 570 __slots__ = ("_parent", 571 "_storage_key", 572 "_active", 573 "_body", 574 "_r", 575 "_x1", 576 "_x2", 577 "__weakref__") 578 579 def __init__(self, r, x1, x2): 580 super(dual_exponential, self).__init__() 581 self._r = r 582 self._x1 = x1 583 self._x2 = x2 584 assert isinstance(self._r, IVariable) 585 assert isinstance(self._x1, IVariable) 586 assert isinstance(self._x2, IVariable) 587 588 @classmethod 589 def as_domain(cls, r, x1, x2): 590 """Builds a conic domain. Input arguments take the 591 same form as those of the conic constraint, but in 592 place of each variable, one can optionally supply a 593 constant, linear expression, or None. 594 595 Returns 596 ------- 597 block 598 A block object with the core conic constraint 599 (block.q) expressed using auxiliary variables 600 (block.r, block.x1, block.x2) linked to the 601 input arguments through auxiliary constraints 602 (block.c). 603 """ 604 b = block() 605 b.r = variable(lb=0) 606 b.x1 = variable() 607 b.x2 = variable(ub=0) 608 b.c = _build_linking_constraints([r,x1,x2], 609 [b.r,b.x1,b.x2]) 610 b.q = cls(r=b.r, x1=b.x1, x2=b.x2) 611 return b 612 613 @property 614 def r(self): 615 return self._r 616 617 @property 618 def x1(self): 619 return self._x1 620 621 @property 622 def x2(self): 623 return self._x2 624 625 # 626 # Define the _ConicBase abstract methods 627 # 628 629 def _body_function(self, r, x1, x2): 630 """A function that defines the body expression""" 631 return -x2*exp((x1/x2) - 1) - r 632 633 def _body_function_variables(self, values=False): 634 """Returns variables in the order they should be 635 passed to the body function. If values is True, then 636 return the current value of each variable in place 637 of the variables themselves.""" 638 if not values: 639 return self.r, self.x1, self.x2 640 else: 641 return self.r.value, self.x1.value, self.x2.value 642 643 def check_convexity_conditions(self, relax=False): 644 """Returns True if all convexity conditions for the 645 conic constraint are satisfied. If relax is True, 646 then variable domains are ignored and it is assumed 647 that all variables are continuous.""" 648 return (relax or \ 649 (self.x1.is_continuous() and \ 650 self.x2.is_continuous() and \ 651 self.r.is_continuous())) and \ 652 (self.x2.has_ub() and value(self.x2.ub) <= 0) and \ 653 (self.r.has_lb() and value(self.r.lb) >= 0) 654 655class dual_power(_ConicBase): 656 """A dual power conic constraint of the form: 657 658 sqrt(x[0]^2 + ... + x[n-1]^2) <= ((r1/alpha)^alpha) * \ 659 ((r2/(1-alpha))^(1-alpha)) 660 661 which is recognized as convex for r1,r2 >= 0 662 and 0 < alpha < 1. 663 664 Parameters 665 ---------- 666 r1 : :class:`variable` 667 A variable. 668 r2 : :class:`variable` 669 A variable. 670 x : list[:class:`variable`] 671 An iterable of variables. 672 alpha : float, :class:`parameter`, etc. 673 A constant term. 674 """ 675 __slots__ = ("_parent", 676 "_storage_key", 677 "_active", 678 "_body", 679 "_r1", 680 "_r2", 681 "_x", 682 "_alpha", 683 "__weakref__") 684 685 def __init__(self, r1, r2, x, alpha): 686 super(dual_power, self).__init__() 687 self._r1 = r1 688 self._r2 = r2 689 self._x = tuple(x) 690 self._alpha = alpha 691 assert isinstance(self._r1, IVariable) 692 assert isinstance(self._r2, IVariable) 693 assert all(isinstance(xi, IVariable) 694 for xi in self._x) 695 if not is_numeric_data(self._alpha): 696 raise TypeError( 697 "The type of the alpha parameter of a conic " 698 "constraint is restricted numeric data or " 699 "objects that store numeric data.") 700 701 @classmethod 702 def as_domain(cls, r1, r2, x, alpha): 703 """Builds a conic domain. Input arguments take the 704 same form as those of the conic constraint, but in 705 place of each variable, one can optionally supply a 706 constant, linear expression, or None. 707 708 Returns 709 ------- 710 block 711 A block object with the core conic constraint 712 (block.q) expressed using auxiliary variables 713 (block.r1, block.r2, block.x) linked to the 714 input arguments through auxiliary constraints 715 (block.c). 716 """ 717 b = block() 718 b.r1 = variable(lb=0) 719 b.r2 = variable(lb=0) 720 b.x = variable_tuple( 721 [variable() for i in range(len(x))]) 722 b.c = _build_linking_constraints([r1,r2] + list(x), 723 [b.r1,b.r2] + list(b.x)) 724 b.q = cls(r1=b.r1, r2=b.r2, x=b.x, alpha=alpha) 725 return b 726 727 @property 728 def r1(self): 729 return self._r1 730 731 @property 732 def r2(self): 733 return self._r2 734 735 @property 736 def x(self): 737 return self._x 738 739 @property 740 def alpha(self): 741 return self._alpha 742 743 # 744 # Define the _ConicBase abstract methods 745 # 746 747 def _body_function(self, r1, r2, x): 748 """A function that defines the body expression""" 749 alpha = self.alpha 750 return (sum(xi**2 for xi in x)**0.5) - \ 751 ((r1/alpha)**alpha) * \ 752 ((r2/(1-alpha))**(1-alpha)) 753 754 def _body_function_variables(self, values=False): 755 """Returns variables in the order they should be 756 passed to the body function. If values is True, then 757 return the current value of each variable in place 758 of the variables themselves.""" 759 if not values: 760 return self.r1, self.r2, self.x 761 else: 762 return self.r1.value, self.r2.value, \ 763 tuple(xi.value for xi in self.x) 764 765 def check_convexity_conditions(self, relax=False): 766 """Returns True if all convexity conditions for the 767 conic constraint are satisfied. If relax is True, 768 then variable domains are ignored and it is assumed 769 that all variables are continuous.""" 770 alpha = value(self.alpha, exception=False) 771 return (relax or \ 772 (self.r1.is_continuous() and \ 773 self.r2.is_continuous() and \ 774 all(xi.is_continuous() for xi in self.x))) and \ 775 (self.r1.has_lb() and value(self.r1.lb) >= 0) and \ 776 (self.r2.has_lb() and value(self.r2.lb) >= 0) and \ 777 ((alpha is not None) and (0 < alpha < 1)) 778