ortools.math_opt.python.variables
Define Variables and Linear Expressions.
1# Copyright 2010-2025 Google LLC 2# Licensed under the Apache License, Version 2.0 (the "License"); 3# you may not use this file except in compliance with the License. 4# You may obtain a copy of the License at 5# 6# http://www.apache.org/licenses/LICENSE-2.0 7# 8# Unless required by applicable law or agreed to in writing, software 9# distributed under the License is distributed on an "AS IS" BASIS, 10# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11# See the License for the specific language governing permissions and 12# limitations under the License. 13 14"""Define Variables and Linear Expressions.""" 15 16import abc 17import collections 18import dataclasses 19import math 20import typing 21from typing import ( 22 Any, 23 DefaultDict, 24 Deque, 25 Generic, 26 Iterable, 27 Mapping, 28 NoReturn, 29 Optional, 30 Protocol, 31 Tuple, 32 Type, 33 TypeVar, 34 Union, 35) 36 37import immutabledict 38 39from ortools.math_opt.elemental.python import enums 40from ortools.math_opt.python import bounded_expressions 41from ortools.math_opt.python import from_model 42from ortools.math_opt.python.elemental import elemental 43 44 45LinearTypes = Union[int, float, "LinearBase"] 46QuadraticTypes = Union[int, float, "LinearBase", "QuadraticBase"] 47LinearTypesExceptVariable = Union[ 48 float, int, "LinearTerm", "LinearExpression", "LinearSum", "LinearProduct" 49] 50 51 52_EXPRESSION_COMP_EXPRESSION_MESSAGE = ( 53 "This error can occur when adding " 54 "inequalities of the form `(a <= b) <= " 55 "c` where (a, b, c) includes two or more" 56 " non-constant linear expressions" 57) 58 59 60def _raise_binary_operator_type_error( 61 operator: str, 62 lhs: Type[Any], 63 rhs: Type[Any], 64 extra_message: Optional[str] = None, 65) -> NoReturn: 66 """Raises TypeError on unsupported operators.""" 67 message = ( 68 f"unsupported operand type(s) for {operator}: {lhs.__name__!r} and" 69 f" {rhs.__name__!r}" 70 ) 71 if extra_message is not None: 72 message += "\n" + extra_message 73 raise TypeError(message) 74 75 76def _raise_ne_not_supported() -> NoReturn: 77 raise TypeError("!= constraints are not supported") 78 79 80LowerBoundedLinearExpression = bounded_expressions.LowerBoundedExpression["LinearBase"] 81UpperBoundedLinearExpression = bounded_expressions.UpperBoundedExpression["LinearBase"] 82BoundedLinearExpression = bounded_expressions.BoundedExpression["LinearBase"] 83 84 85class VarEqVar: 86 """The result of the equality comparison between two Variable. 87 88 We use an object here to delay the evaluation of equality so that we can use 89 the operator== in two use-cases: 90 91 1. when the user want to test that two Variable values references the same 92 variable. This is supported by having this object support implicit 93 conversion to bool. 94 95 2. when the user want to use the equality to create a constraint of equality 96 between two variables. 97 """ 98 99 __slots__ = "_first_variable", "_second_variable" 100 101 def __init__( 102 self, 103 first_variable: "Variable", 104 second_variable: "Variable", 105 ) -> None: 106 self._first_variable: "Variable" = first_variable 107 self._second_variable: "Variable" = second_variable 108 109 @property 110 def first_variable(self) -> "Variable": 111 return self._first_variable 112 113 @property 114 def second_variable(self) -> "Variable": 115 return self._second_variable 116 117 def __bool__(self) -> bool: 118 return ( 119 self._first_variable.elemental is self._second_variable.elemental 120 and self._first_variable.id == self._second_variable.id 121 ) 122 123 def __str__(self): 124 return f"{self.first_variable!s} == {self._second_variable!s}" 125 126 def __repr__(self): 127 return f"{self.first_variable!r} == {self._second_variable!r}" 128 129 130BoundedLinearTypesList = ( 131 LowerBoundedLinearExpression, 132 UpperBoundedLinearExpression, 133 BoundedLinearExpression, 134 VarEqVar, 135) 136BoundedLinearTypes = Union[BoundedLinearTypesList] 137 138LowerBoundedQuadraticExpression = bounded_expressions.LowerBoundedExpression[ 139 "QuadraticBase" 140] 141UpperBoundedQuadraticExpression = bounded_expressions.UpperBoundedExpression[ 142 "QuadraticBase" 143] 144BoundedQuadraticExpression = bounded_expressions.BoundedExpression["QuadraticBase"] 145 146BoundedQuadraticTypesList = ( 147 LowerBoundedQuadraticExpression, 148 UpperBoundedQuadraticExpression, 149 BoundedQuadraticExpression, 150) 151BoundedQuadraticTypes = Union[BoundedQuadraticTypesList] 152 153 154# TODO(b/231426528): consider using a frozen dataclass. 155class QuadraticTermKey: 156 """An id-ordered pair of variables used as a key for quadratic terms.""" 157 158 __slots__ = "_first_var", "_second_var" 159 160 def __init__(self, a: "Variable", b: "Variable"): 161 """Variables a and b will be ordered internally by their ids.""" 162 self._first_var: "Variable" = a 163 self._second_var: "Variable" = b 164 if self._first_var.id > self._second_var.id: 165 self._first_var, self._second_var = self._second_var, self._first_var 166 167 @property 168 def first_var(self) -> "Variable": 169 return self._first_var 170 171 @property 172 def second_var(self) -> "Variable": 173 return self._second_var 174 175 def __eq__(self, other: "QuadraticTermKey") -> bool: 176 return bool( 177 self._first_var == other._first_var 178 and self._second_var == other._second_var 179 ) 180 181 def __hash__(self) -> int: 182 return hash((self._first_var, self._second_var)) 183 184 def __str__(self): 185 return f"{self._first_var!s} * {self._second_var!s}" 186 187 def __repr__(self): 188 return f"QuadraticTermKey({self._first_var!r}, {self._second_var!r})" 189 190 191@dataclasses.dataclass 192class _ProcessedElements: 193 """Auxiliary data class for LinearBase._flatten_once_and_add_to().""" 194 195 terms: DefaultDict["Variable", float] = dataclasses.field( 196 default_factory=lambda: collections.defaultdict(float) 197 ) 198 offset: float = 0.0 199 200 201@dataclasses.dataclass 202class _QuadraticProcessedElements(_ProcessedElements): 203 """Auxiliary data class for QuadraticBase._quadratic_flatten_once_and_add_to().""" 204 205 quadratic_terms: DefaultDict["QuadraticTermKey", float] = dataclasses.field( 206 default_factory=lambda: collections.defaultdict(float) 207 ) 208 209 210class _ToProcessElements(Protocol): 211 """Auxiliary to-process stack interface for LinearBase._flatten_once_and_add_to() and QuadraticBase._quadratic_flatten_once_and_add_to().""" 212 213 __slots__ = () 214 215 def append(self, term: "LinearBase", scale: float) -> None: 216 """Add a linear object and scale to the to-process stack.""" 217 218 219_T = TypeVar("_T", "LinearBase", Union["LinearBase", "QuadraticBase"]) 220 221 222class _ToProcessElementsImplementation(Generic[_T]): 223 """Auxiliary data class for LinearBase._flatten_once_and_add_to().""" 224 225 __slots__ = ("_queue",) 226 227 def __init__(self, term: _T, scale: float) -> None: 228 self._queue: Deque[Tuple[_T, float]] = collections.deque([(term, scale)]) 229 230 def append(self, term: _T, scale: float) -> None: 231 self._queue.append((term, scale)) 232 233 def pop(self) -> Tuple[_T, float]: 234 return self._queue.popleft() 235 236 def __bool__(self) -> bool: 237 return bool(self._queue) 238 239 240_LinearToProcessElements = _ToProcessElementsImplementation["LinearBase"] 241_QuadraticToProcessElements = _ToProcessElementsImplementation[ 242 Union["LinearBase", "QuadraticBase"] 243] 244 245 246class LinearBase(metaclass=abc.ABCMeta): 247 """Interface for types that can build linear expressions with +, -, * and /. 248 249 Classes derived from LinearBase (plus float and int scalars) are used to 250 build expression trees describing a linear expression. Operations nodes of the 251 expression tree include: 252 253 * LinearSum: describes a deferred sum of LinearTypes objects. 254 * LinearProduct: describes a deferred product of a scalar and a 255 LinearTypes object. 256 257 Leaf nodes of the expression tree include: 258 259 * float and int scalars. 260 * Variable: a single variable. 261 * LinearTerm: the product of a scalar and a Variable object. 262 * LinearExpression: the sum of a scalar and LinearTerm objects. 263 264 LinearBase objects/expression-trees can be used directly to create 265 constraints or objective functions. However, to facilitate their inspection, 266 any LinearTypes object can be flattened to a LinearExpression 267 through: 268 269 as_flat_linear_expression(value: LinearTypes) -> LinearExpression: 270 271 In addition, all LinearBase objects are immutable. 272 273 Performance notes: 274 275 Using an expression tree representation instead of an eager construction of 276 LinearExpression objects reduces known inefficiencies associated with the 277 use of operator overloading to construct linear expressions. In particular, we 278 expect the runtime of as_flat_linear_expression() to be linear in the size of 279 the expression tree. Additional performance can gained by using LinearSum(c) 280 instead of sum(c) for a container c, as the latter creates len(c) LinearSum 281 objects. 282 """ 283 284 __slots__ = () 285 286 # TODO(b/216492143): explore requirements for this function so calculation of 287 # coefficients and offsets follow expected associativity rules (so approximate 288 # float calculations are as expected). 289 # TODO(b/216492143): add more details of what subclasses need to do in 290 # developers guide. 291 @abc.abstractmethod 292 def _flatten_once_and_add_to( 293 self, 294 scale: float, 295 processed_elements: _ProcessedElements, 296 target_stack: _ToProcessElements, 297 ) -> None: 298 """Flatten one level of tree if needed and add to targets. 299 300 Classes derived from LinearBase only need to implement this function 301 to enable transformation to LinearExpression through 302 as_flat_linear_expression(). 303 304 Args: 305 scale: multiply elements by this number when processing or adding to 306 stack. 307 processed_elements: where to add LinearTerms and scalars that can be 308 processed immediately. 309 target_stack: where to add LinearBase elements that are not scalars or 310 LinearTerms (i.e. elements that need further flattening). 311 Implementations should append() to this stack to avoid being recursive. 312 """ 313 314 def __eq__( 315 self, rhs: LinearTypes 316 ) -> ( 317 BoundedLinearExpression 318 ): # pytype: disable=signature-mismatch # overriding-return-type-checks 319 # Note: when rhs is a QuadraticBase, this will cause rhs.__eq__(self) to be 320 # invoked, which is defined. 321 if isinstance(rhs, QuadraticBase): 322 return NotImplemented 323 if isinstance(rhs, (int, float)): 324 return BoundedLinearExpression(rhs, self, rhs) 325 if not isinstance(rhs, LinearBase): 326 _raise_binary_operator_type_error("==", type(self), type(rhs)) 327 return BoundedLinearExpression(0.0, self - rhs, 0.0) 328 329 def __ne__( 330 self, rhs: LinearTypes 331 ) -> ( 332 NoReturn 333 ): # pytype: disable=signature-mismatch # overriding-return-type-checks 334 _raise_ne_not_supported() 335 336 @typing.overload 337 def __le__(self, rhs: float) -> "UpperBoundedLinearExpression": ... 338 339 @typing.overload 340 def __le__(self, rhs: "LinearBase") -> "BoundedLinearExpression": ... 341 342 @typing.overload 343 def __le__(self, rhs: "BoundedLinearExpression") -> NoReturn: ... 344 345 def __le__(self, rhs): 346 # Note: when rhs is a QuadraticBase, this will cause rhs.__ge__(self) to be 347 # invoked, which is defined. 348 if isinstance(rhs, QuadraticBase): 349 return NotImplemented 350 if isinstance(rhs, (int, float)): 351 return UpperBoundedLinearExpression(self, rhs) 352 if isinstance(rhs, LinearBase): 353 return BoundedLinearExpression(-math.inf, self - rhs, 0.0) 354 if isinstance(rhs, bounded_expressions.BoundedExpression): 355 _raise_binary_operator_type_error( 356 "<=", type(self), type(rhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE 357 ) 358 _raise_binary_operator_type_error("<=", type(self), type(rhs)) 359 360 @typing.overload 361 def __ge__(self, lhs: float) -> "LowerBoundedLinearExpression": ... 362 363 @typing.overload 364 def __ge__(self, lhs: "LinearBase") -> "BoundedLinearExpression": ... 365 366 @typing.overload 367 def __ge__(self, lhs: "BoundedLinearExpression") -> NoReturn: ... 368 369 def __ge__(self, lhs): 370 # Note: when lhs is a QuadraticBase, this will cause lhs.__le__(self) to be 371 # invoked, which is defined. 372 if isinstance(lhs, QuadraticBase): 373 return NotImplemented 374 if isinstance(lhs, (int, float)): 375 return LowerBoundedLinearExpression(self, lhs) 376 if isinstance(lhs, LinearBase): 377 return BoundedLinearExpression(0.0, self - lhs, math.inf) 378 if isinstance(lhs, bounded_expressions.BoundedExpression): 379 _raise_binary_operator_type_error( 380 ">=", type(self), type(lhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE 381 ) 382 _raise_binary_operator_type_error(">=", type(self), type(lhs)) 383 384 def __add__(self, expr: LinearTypes) -> "LinearSum": 385 if not isinstance(expr, (int, float, LinearBase)): 386 return NotImplemented 387 return LinearSum((self, expr)) 388 389 def __radd__(self, expr: LinearTypes) -> "LinearSum": 390 if not isinstance(expr, (int, float, LinearBase)): 391 return NotImplemented 392 return LinearSum((expr, self)) 393 394 def __sub__(self, expr: LinearTypes) -> "LinearSum": 395 if not isinstance(expr, (int, float, LinearBase)): 396 return NotImplemented 397 return LinearSum((self, -expr)) 398 399 def __rsub__(self, expr: LinearTypes) -> "LinearSum": 400 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 401 return NotImplemented 402 return LinearSum((expr, -self)) 403 404 @typing.overload 405 def __mul__(self, other: float) -> "LinearProduct": ... 406 407 @typing.overload 408 def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ... 409 410 def __mul__(self, other): 411 if not isinstance(other, (int, float, LinearBase)): 412 return NotImplemented 413 if isinstance(other, LinearBase): 414 return LinearLinearProduct(self, other) 415 return LinearProduct(other, self) 416 417 def __rmul__(self, constant: float) -> "LinearProduct": 418 if not isinstance(constant, (int, float)): 419 return NotImplemented 420 return LinearProduct(constant, self) 421 422 # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. 423 def __truediv__(self, constant: float) -> "LinearProduct": 424 if not isinstance(constant, (int, float)): 425 return NotImplemented 426 return LinearProduct(1.0 / constant, self) 427 428 def __neg__(self) -> "LinearProduct": 429 return LinearProduct(-1.0, self) 430 431 432class QuadraticBase(metaclass=abc.ABCMeta): 433 """Interface for types that can build quadratic expressions with +, -, * and /. 434 435 Classes derived from QuadraticBase and LinearBase (plus float and int scalars) 436 are used to build expression trees describing a quadratic expression. 437 Operations nodes of the expression tree include: 438 439 * QuadraticSum: describes a deferred sum of QuadraticTypes objects. 440 * QuadraticProduct: describes a deferred product of a scalar and a 441 QuadraticTypes object. 442 * LinearLinearProduct: describes a deferred product of two LinearTypes 443 objects. 444 445 Leaf nodes of the expression tree include: 446 447 * float and int scalars. 448 * Variable: a single variable. 449 * LinearTerm: the product of a scalar and a Variable object. 450 * LinearExpression: the sum of a scalar and LinearTerm objects. 451 * QuadraticTerm: the product of a scalar and two Variable objects. 452 * QuadraticExpression: the sum of a scalar, LinearTerm objects and 453 QuadraticTerm objects. 454 455 QuadraticBase objects/expression-trees can be used directly to create 456 objective functions. However, to facilitate their inspection, any 457 QuadraticTypes object can be flattened to a QuadraticExpression 458 through: 459 460 as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression: 461 462 In addition, all QuadraticBase objects are immutable. 463 464 Performance notes: 465 466 Using an expression tree representation instead of an eager construction of 467 QuadraticExpression objects reduces known inefficiencies associated with the 468 use of operator overloading to construct quadratic expressions. In particular, 469 we expect the runtime of as_flat_quadratic_expression() to be linear in the 470 size of the expression tree. Additional performance can gained by using 471 QuadraticSum(c) instead of sum(c) for a container c, as the latter creates 472 len(c) QuadraticSum objects. 473 """ 474 475 __slots__ = () 476 477 # TODO(b/216492143): explore requirements for this function so calculation of 478 # coefficients and offsets follow expected associativity rules (so approximate 479 # float calculations are as expected). 480 # TODO(b/216492143): add more details of what subclasses need to do in 481 # developers guide. 482 @abc.abstractmethod 483 def _quadratic_flatten_once_and_add_to( 484 self, 485 scale: float, 486 processed_elements: _QuadraticProcessedElements, 487 target_stack: _QuadraticToProcessElements, 488 ) -> None: 489 """Flatten one level of tree if needed and add to targets. 490 491 Classes derived from QuadraticBase only need to implement this function 492 to enable transformation to QuadraticExpression through 493 as_flat_quadratic_expression(). 494 495 Args: 496 scale: multiply elements by this number when processing or adding to 497 stack. 498 processed_elements: where to add linear terms, quadratic terms and scalars 499 that can be processed immediately. 500 target_stack: where to add LinearBase and QuadraticBase elements that are 501 not scalars or linear terms or quadratic terms (i.e. elements that need 502 further flattening). Implementations should append() to this stack to 503 avoid being recursive. 504 """ 505 506 def __eq__( 507 self, rhs: QuadraticTypes 508 ) -> ( 509 BoundedQuadraticExpression 510 ): # pytype: disable=signature-mismatch # overriding-return-type-checks 511 if isinstance(rhs, (int, float)): 512 return BoundedQuadraticExpression(rhs, self, rhs) 513 if not isinstance(rhs, (LinearBase, QuadraticBase)): 514 _raise_binary_operator_type_error("==", type(self), type(rhs)) 515 return BoundedQuadraticExpression(0.0, self - rhs, 0.0) 516 517 def __ne__( 518 self, rhs: QuadraticTypes 519 ) -> ( 520 NoReturn 521 ): # pytype: disable=signature-mismatch # overriding-return-type-checks 522 _raise_ne_not_supported() 523 524 @typing.overload 525 def __le__(self, rhs: float) -> UpperBoundedQuadraticExpression: ... 526 527 @typing.overload 528 def __le__( 529 self, rhs: Union[LinearBase, "QuadraticBase"] 530 ) -> BoundedQuadraticExpression: ... 531 532 @typing.overload 533 def __le__(self, rhs: BoundedQuadraticExpression) -> NoReturn: ... 534 535 def __le__(self, rhs): 536 if isinstance(rhs, (int, float)): 537 return UpperBoundedQuadraticExpression(self, rhs) 538 if isinstance(rhs, (LinearBase, QuadraticBase)): 539 return BoundedQuadraticExpression(-math.inf, self - rhs, 0.0) 540 if isinstance(rhs, bounded_expressions.BoundedExpression): 541 _raise_binary_operator_type_error( 542 "<=", type(self), type(rhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE 543 ) 544 _raise_binary_operator_type_error("<=", type(self), type(rhs)) 545 546 @typing.overload 547 def __ge__(self, lhs: float) -> LowerBoundedQuadraticExpression: ... 548 549 @typing.overload 550 def __ge__( 551 self, lhs: Union[LinearBase, "QuadraticBase"] 552 ) -> BoundedQuadraticExpression: ... 553 554 @typing.overload 555 def __ge__(self, lhs: BoundedQuadraticExpression) -> NoReturn: ... 556 557 def __ge__(self, lhs): 558 if isinstance(lhs, (int, float)): 559 return LowerBoundedQuadraticExpression(self, lhs) 560 if isinstance(lhs, (LinearBase, QuadraticBase)): 561 return BoundedQuadraticExpression(0.0, self - lhs, math.inf) 562 if isinstance(lhs, bounded_expressions.BoundedExpression): 563 _raise_binary_operator_type_error( 564 ">=", type(self), type(lhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE 565 ) 566 _raise_binary_operator_type_error(">=", type(self), type(lhs)) 567 568 def __add__(self, expr: QuadraticTypes) -> "QuadraticSum": 569 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 570 return NotImplemented 571 return QuadraticSum((self, expr)) 572 573 def __radd__(self, expr: QuadraticTypes) -> "QuadraticSum": 574 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 575 return NotImplemented 576 return QuadraticSum((expr, self)) 577 578 def __sub__(self, expr: QuadraticTypes) -> "QuadraticSum": 579 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 580 return NotImplemented 581 return QuadraticSum((self, -expr)) 582 583 def __rsub__(self, expr: QuadraticTypes) -> "QuadraticSum": 584 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 585 return NotImplemented 586 return QuadraticSum((expr, -self)) 587 588 def __mul__(self, other: float) -> "QuadraticProduct": 589 if not isinstance(other, (int, float)): 590 return NotImplemented 591 return QuadraticProduct(other, self) 592 593 def __rmul__(self, other: float) -> "QuadraticProduct": 594 if not isinstance(other, (int, float)): 595 return NotImplemented 596 return QuadraticProduct(other, self) 597 598 # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. 599 def __truediv__(self, constant: float) -> "QuadraticProduct": 600 if not isinstance(constant, (int, float)): 601 return NotImplemented 602 return QuadraticProduct(1.0 / constant, self) 603 604 def __neg__(self) -> "QuadraticProduct": 605 return QuadraticProduct(-1.0, self) 606 607 608class Variable(LinearBase, from_model.FromModel): 609 """A decision variable for an optimization model. 610 611 A decision variable takes a value from a domain, either the real numbers or 612 the integers, and restricted to be in some interval [lb, ub] (where lb and ub 613 can be infinite). The case of lb == ub is allowed, this means the variable 614 must take a single value. The case of lb > ub is also allowed, this implies 615 that the problem is infeasible. 616 617 A Variable is configured as follows: 618 * lower_bound: a float property, lb above. Should not be NaN nor +inf. 619 * upper_bound: a float property, ub above. Should not be NaN nor -inf. 620 * integer: a bool property, if the domain is integer or continuous. 621 622 The name is optional, read only, and used only for debugging. Non-empty names 623 should be distinct. 624 625 Every Variable is associated with a Model (defined below). Note that data 626 describing the variable (e.g. lower_bound) is owned by Model.storage, this 627 class is simply a reference to that data. Do not create a Variable directly, 628 use Model.add_variable() instead. 629 """ 630 631 __slots__ = "_elemental", "_id" 632 633 def __init__(self, elem: elemental.Elemental, vid: int) -> None: 634 """Internal only, prefer Model functions (add_variable() and get_variable()).""" 635 if not isinstance(vid, int): 636 raise TypeError(f"vid type should be int, was:{type(vid)}") 637 self._elemental: elemental.Elemental = elem 638 self._id: int = vid 639 640 @property 641 def lower_bound(self) -> float: 642 return self._elemental.get_attr( 643 enums.DoubleAttr1.VARIABLE_LOWER_BOUND, (self._id,) 644 ) 645 646 @lower_bound.setter 647 def lower_bound(self, value: float) -> None: 648 self._elemental.set_attr( 649 enums.DoubleAttr1.VARIABLE_LOWER_BOUND, 650 (self._id,), 651 value, 652 ) 653 654 @property 655 def upper_bound(self) -> float: 656 return self._elemental.get_attr( 657 enums.DoubleAttr1.VARIABLE_UPPER_BOUND, (self._id,) 658 ) 659 660 @upper_bound.setter 661 def upper_bound(self, value: float) -> None: 662 self._elemental.set_attr( 663 enums.DoubleAttr1.VARIABLE_UPPER_BOUND, 664 (self._id,), 665 value, 666 ) 667 668 @property 669 def integer(self) -> bool: 670 return self._elemental.get_attr(enums.BoolAttr1.VARIABLE_INTEGER, (self._id,)) 671 672 @integer.setter 673 def integer(self, value: bool) -> None: 674 self._elemental.set_attr(enums.BoolAttr1.VARIABLE_INTEGER, (self._id,), value) 675 676 @property 677 def name(self) -> str: 678 return self._elemental.get_element_name(enums.ElementType.VARIABLE, self._id) 679 680 @property 681 def id(self) -> int: 682 return self._id 683 684 @property 685 def elemental(self) -> elemental.Elemental: 686 """Internal use only.""" 687 return self._elemental 688 689 def __str__(self): 690 """Returns the name, or a string containing the id if the name is empty.""" 691 return self.name if self.name else f"variable_{self.id}" 692 693 def __repr__(self): 694 return f"<Variable id: {self.id}, name: {self.name!r}>" 695 696 @typing.overload 697 def __eq__(self, rhs: "Variable") -> "VarEqVar": ... 698 699 @typing.overload 700 def __eq__(self, rhs: LinearTypesExceptVariable) -> "BoundedLinearExpression": ... 701 702 def __eq__(self, rhs): 703 if isinstance(rhs, Variable): 704 return VarEqVar(self, rhs) 705 return super().__eq__(rhs) 706 707 @typing.overload 708 def __ne__(self, rhs: "Variable") -> bool: ... 709 710 @typing.overload 711 def __ne__(self, rhs: LinearTypesExceptVariable) -> NoReturn: ... 712 713 def __ne__(self, rhs): 714 if isinstance(rhs, Variable): 715 return not self == rhs 716 _raise_ne_not_supported() 717 718 def __hash__(self) -> int: 719 return hash(self._id) 720 721 @typing.overload 722 def __mul__(self, other: float) -> "LinearTerm": ... 723 724 @typing.overload 725 def __mul__(self, other: Union["Variable", "LinearTerm"]) -> "QuadraticTerm": ... 726 727 @typing.overload 728 def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ... 729 730 def __mul__(self, other): 731 if not isinstance(other, (int, float, LinearBase)): 732 return NotImplemented 733 if isinstance(other, Variable): 734 return QuadraticTerm(QuadraticTermKey(self, other), 1.0) 735 if isinstance(other, LinearTerm): 736 return QuadraticTerm( 737 QuadraticTermKey(self, other.variable), other.coefficient 738 ) 739 if isinstance(other, LinearBase): 740 return LinearLinearProduct(self, other) # pytype: disable=bad-return-type 741 return LinearTerm(self, other) 742 743 def __rmul__(self, constant: float) -> "LinearTerm": 744 if not isinstance(constant, (int, float)): 745 return NotImplemented 746 return LinearTerm(self, constant) 747 748 # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. 749 def __truediv__(self, constant: float) -> "LinearTerm": 750 if not isinstance(constant, (int, float)): 751 return NotImplemented 752 return LinearTerm(self, 1.0 / constant) 753 754 def __neg__(self) -> "LinearTerm": 755 return LinearTerm(self, -1.0) 756 757 def _flatten_once_and_add_to( 758 self, 759 scale: float, 760 processed_elements: _ProcessedElements, 761 target_stack: _ToProcessElements, 762 ) -> None: 763 processed_elements.terms[self] += scale 764 765 766class LinearTerm(LinearBase): 767 """The product of a scalar and a variable. 768 769 This class is immutable. 770 """ 771 772 __slots__ = "_variable", "_coefficient" 773 774 def __init__(self, variable: Variable, coefficient: float) -> None: 775 self._variable: Variable = variable 776 self._coefficient: float = coefficient 777 778 @property 779 def variable(self) -> Variable: 780 return self._variable 781 782 @property 783 def coefficient(self) -> float: 784 return self._coefficient 785 786 def _flatten_once_and_add_to( 787 self, 788 scale: float, 789 processed_elements: _ProcessedElements, 790 target_stack: _ToProcessElements, 791 ) -> None: 792 processed_elements.terms[self._variable] += self._coefficient * scale 793 794 @typing.overload 795 def __mul__(self, other: float) -> "LinearTerm": ... 796 797 @typing.overload 798 def __mul__(self, other: Union["Variable", "LinearTerm"]) -> "QuadraticTerm": ... 799 800 @typing.overload 801 def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ... 802 803 def __mul__(self, other): 804 if not isinstance(other, (int, float, LinearBase)): 805 return NotImplemented 806 if isinstance(other, Variable): 807 return QuadraticTerm( 808 QuadraticTermKey(self._variable, other), self._coefficient 809 ) 810 if isinstance(other, LinearTerm): 811 return QuadraticTerm( 812 QuadraticTermKey(self.variable, other.variable), 813 self._coefficient * other.coefficient, 814 ) 815 if isinstance(other, LinearBase): 816 return LinearLinearProduct(self, other) # pytype: disable=bad-return-type 817 return LinearTerm(self._variable, self._coefficient * other) 818 819 def __rmul__(self, constant: float) -> "LinearTerm": 820 if not isinstance(constant, (int, float)): 821 return NotImplemented 822 return LinearTerm(self._variable, self._coefficient * constant) 823 824 def __truediv__(self, constant: float) -> "LinearTerm": 825 if not isinstance(constant, (int, float)): 826 return NotImplemented 827 return LinearTerm(self._variable, self._coefficient / constant) 828 829 def __neg__(self) -> "LinearTerm": 830 return LinearTerm(self._variable, -self._coefficient) 831 832 def __str__(self): 833 return f"{self._coefficient} * {self._variable}" 834 835 def __repr__(self): 836 return f"LinearTerm({self._variable!r}, {self._coefficient!r})" 837 838 839class QuadraticTerm(QuadraticBase): 840 """The product of a scalar and two variables. 841 842 This class is immutable. 843 """ 844 845 __slots__ = "_key", "_coefficient" 846 847 def __init__(self, key: QuadraticTermKey, coefficient: float) -> None: 848 self._key: QuadraticTermKey = key 849 self._coefficient: float = coefficient 850 851 @property 852 def key(self) -> QuadraticTermKey: 853 return self._key 854 855 @property 856 def coefficient(self) -> float: 857 return self._coefficient 858 859 def _quadratic_flatten_once_and_add_to( 860 self, 861 scale: float, 862 processed_elements: _QuadraticProcessedElements, 863 target_stack: _ToProcessElements, 864 ) -> None: 865 processed_elements.quadratic_terms[self._key] += self._coefficient * scale 866 867 def __mul__(self, constant: float) -> "QuadraticTerm": 868 if not isinstance(constant, (int, float)): 869 return NotImplemented 870 return QuadraticTerm(self._key, self._coefficient * constant) 871 872 def __rmul__(self, constant: float) -> "QuadraticTerm": 873 if not isinstance(constant, (int, float)): 874 return NotImplemented 875 return QuadraticTerm(self._key, self._coefficient * constant) 876 877 def __truediv__(self, constant: float) -> "QuadraticTerm": 878 if not isinstance(constant, (int, float)): 879 return NotImplemented 880 return QuadraticTerm(self._key, self._coefficient / constant) 881 882 def __neg__(self) -> "QuadraticTerm": 883 return QuadraticTerm(self._key, -self._coefficient) 884 885 def __str__(self): 886 return f"{self._coefficient} * {self._key!s}" 887 888 def __repr__(self): 889 return f"QuadraticTerm({self._key!r}, {self._coefficient})" 890 891 892class LinearExpression(LinearBase): 893 """For variables x, an expression: b + sum_{i in I} a_i * x_i. 894 895 This class is immutable. 896 """ 897 898 __slots__ = "__weakref__", "_terms", "_offset" 899 900 # TODO(b/216492143): consider initializing from a dictionary. 901 def __init__(self, /, other: LinearTypes = 0) -> None: 902 self._offset: float = 0.0 903 if isinstance(other, (int, float)): 904 self._offset = float(other) 905 self._terms: Mapping[Variable, float] = immutabledict.immutabledict() 906 return 907 908 to_process: _LinearToProcessElements = _LinearToProcessElements(other, 1.0) 909 processed_elements = _ProcessedElements() 910 while to_process: 911 linear, coef = to_process.pop() 912 linear._flatten_once_and_add_to(coef, processed_elements, to_process) 913 # TODO(b/216492143): explore avoiding this copy. 914 self._terms: Mapping[Variable, float] = immutabledict.immutabledict( 915 processed_elements.terms 916 ) 917 self._offset = processed_elements.offset 918 919 @property 920 def terms(self) -> Mapping[Variable, float]: 921 return self._terms 922 923 @property 924 def offset(self) -> float: 925 return self._offset 926 927 def evaluate(self, variable_values: Mapping[Variable, float]) -> float: 928 """Returns the value of this expression for given variable values. 929 930 E.g. if this is 3 * x + 4 and variable_values = {x: 2.0}, then 931 evaluate(variable_values) equals 10.0. 932 933 See also mathopt.evaluate_expression(), which works on any type in 934 QuadraticTypes. 935 936 Args: 937 variable_values: Must contain a value for every variable in expression. 938 939 Returns: 940 The value of this expression when replacing variables by their value. 941 """ 942 result = self._offset 943 for var, coef in sorted( 944 self._terms.items(), key=lambda var_coef_pair: var_coef_pair[0].id 945 ): 946 result += coef * variable_values[var] 947 return result 948 949 def _flatten_once_and_add_to( 950 self, 951 scale: float, 952 processed_elements: _ProcessedElements, 953 target_stack: _ToProcessElements, 954 ) -> None: 955 for var, val in self._terms.items(): 956 processed_elements.terms[var] += val * scale 957 processed_elements.offset += scale * self.offset 958 959 # TODO(b/216492143): change __str__ to match C++ implementation in 960 # cl/421649402. 961 def __str__(self): 962 """Returns the name, or a string containing the id if the name is empty.""" 963 result = str(self.offset) 964 sorted_keys = sorted(self._terms.keys(), key=str) 965 for var in sorted_keys: 966 # TODO(b/216492143): consider how to better deal with `NaN` and try to 967 # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in 968 # linear_expression_test.py. 969 coefficient = self._terms[var] 970 if coefficient == 0.0: 971 continue 972 if coefficient > 0: 973 result += " + " 974 else: 975 result += " - " 976 result += str(abs(coefficient)) + " * " + str(var) 977 return result 978 979 def __repr__(self): 980 result = f"LinearExpression({self.offset}, " + "{" 981 result += ", ".join( 982 f"{var!r}: {coefficient}" for var, coefficient in self._terms.items() 983 ) 984 result += "})" 985 return result 986 987 988class QuadraticExpression(QuadraticBase): 989 """For variables x, an expression: b + sum_{i in I} a_i * x_i + sum_{i,j in I, i<=j} a_i,j * x_i * x_j. 990 991 This class is immutable. 992 """ 993 994 __slots__ = "__weakref__", "_linear_terms", "_quadratic_terms", "_offset" 995 996 # TODO(b/216492143): consider initializing from a dictionary. 997 def __init__(self, other: QuadraticTypes) -> None: 998 self._offset: float = 0.0 999 if isinstance(other, (int, float)): 1000 self._offset = float(other) 1001 self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict() 1002 self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( 1003 immutabledict.immutabledict() 1004 ) 1005 return 1006 1007 to_process: _QuadraticToProcessElements = _QuadraticToProcessElements( 1008 other, 1.0 1009 ) 1010 processed_elements = _QuadraticProcessedElements() 1011 while to_process: 1012 linear_or_quadratic, coef = to_process.pop() 1013 if isinstance(linear_or_quadratic, LinearBase): 1014 linear_or_quadratic._flatten_once_and_add_to( 1015 coef, processed_elements, to_process 1016 ) 1017 else: 1018 linear_or_quadratic._quadratic_flatten_once_and_add_to( 1019 coef, processed_elements, to_process 1020 ) 1021 # TODO(b/216492143): explore avoiding this copy. 1022 self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict( 1023 processed_elements.terms 1024 ) 1025 self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( 1026 immutabledict.immutabledict(processed_elements.quadratic_terms) 1027 ) 1028 self._offset = processed_elements.offset 1029 1030 @property 1031 def linear_terms(self) -> Mapping[Variable, float]: 1032 return self._linear_terms 1033 1034 @property 1035 def quadratic_terms(self) -> Mapping[QuadraticTermKey, float]: 1036 return self._quadratic_terms 1037 1038 @property 1039 def offset(self) -> float: 1040 return self._offset 1041 1042 def evaluate(self, variable_values: Mapping[Variable, float]) -> float: 1043 """Returns the value of this expression for given variable values. 1044 1045 E.g. if this is 3 * x * x + 4 and variable_values = {x: 2.0}, then 1046 evaluate(variable_values) equals 16.0. 1047 1048 See also mathopt.evaluate_expression(), which works on any type in 1049 QuadraticTypes. 1050 1051 Args: 1052 variable_values: Must contain a value for every variable in expression. 1053 1054 Returns: 1055 The value of this expression when replacing variables by their value. 1056 """ 1057 result = self._offset 1058 for var, coef in sorted( 1059 self._linear_terms.items(), 1060 key=lambda var_coef_pair: var_coef_pair[0].id, 1061 ): 1062 result += coef * variable_values[var] 1063 for key, coef in sorted( 1064 self._quadratic_terms.items(), 1065 key=lambda quad_coef_pair: ( 1066 quad_coef_pair[0].first_var.id, 1067 quad_coef_pair[0].second_var.id, 1068 ), 1069 ): 1070 result += ( 1071 coef * variable_values[key.first_var] * variable_values[key.second_var] 1072 ) 1073 return result 1074 1075 def _quadratic_flatten_once_and_add_to( 1076 self, 1077 scale: float, 1078 processed_elements: _QuadraticProcessedElements, 1079 target_stack: _QuadraticToProcessElements, 1080 ) -> None: 1081 for var, val in self._linear_terms.items(): 1082 processed_elements.terms[var] += val * scale 1083 for key, val in self._quadratic_terms.items(): 1084 processed_elements.quadratic_terms[key] += val * scale 1085 processed_elements.offset += scale * self.offset 1086 1087 # TODO(b/216492143): change __str__ to match C++ implementation in 1088 # cl/421649402. 1089 def __str__(self): 1090 result = str(self.offset) 1091 sorted_linear_keys = sorted(self._linear_terms.keys(), key=str) 1092 for var in sorted_linear_keys: 1093 # TODO(b/216492143): consider how to better deal with `NaN` and try to 1094 # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in 1095 # linear_expression_test.py. 1096 coefficient = self._linear_terms[var] 1097 if coefficient == 0.0: 1098 continue 1099 if coefficient > 0: 1100 result += " + " 1101 else: 1102 result += " - " 1103 result += str(abs(coefficient)) + " * " + str(var) 1104 sorted_quadratic_keys = sorted(self._quadratic_terms.keys(), key=str) 1105 for key in sorted_quadratic_keys: 1106 # TODO(b/216492143): consider how to better deal with `NaN` and try to 1107 # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in 1108 # linear_expression_test.py. 1109 coefficient = self._quadratic_terms[key] 1110 if coefficient == 0.0: 1111 continue 1112 if coefficient > 0: 1113 result += " + " 1114 else: 1115 result += " - " 1116 result += str(abs(coefficient)) + " * " + str(key) 1117 return result 1118 1119 def __repr__(self): 1120 result = f"QuadraticExpression({self.offset}, " + "{" 1121 result += ", ".join( 1122 f"{var!r}: {coefficient}" for var, coefficient in self._linear_terms.items() 1123 ) 1124 result += "}, {" 1125 result += ", ".join( 1126 f"{key!r}: {coefficient}" 1127 for key, coefficient in self._quadratic_terms.items() 1128 ) 1129 result += "})" 1130 return result 1131 1132 1133class LinearSum(LinearBase): 1134 # TODO(b/216492143): consider what details to move elsewhere and/or replace 1135 # by examples, and do complexity analysis. 1136 """A deferred sum of LinearBase objects. 1137 1138 LinearSum objects are automatically created when two linear objects are added 1139 and, as noted in the documentation for Linear, can reduce the inefficiencies. 1140 In particular, they are created when calling sum(iterable) when iterable is 1141 an Iterable[LinearTypes]. However, using LinearSum(iterable) instead 1142 can result in additional performance improvements: 1143 1144 * sum(iterable): creates a nested set of LinearSum objects (e.g. 1145 `sum([a, b, c])` is `LinearSum(0, LinearSum(a, LinearSum(b, c)))`). 1146 * LinearSum(iterable): creates a single LinearSum that saves a tuple with 1147 all the LinearTypes objects in iterable (e.g. 1148 `LinearSum([a, b, c])` does not create additional objects). 1149 1150 This class is immutable. 1151 """ 1152 1153 __slots__ = "__weakref__", "_elements" 1154 1155 # Potentially unsafe use of Iterable argument is handled by immediate local 1156 # storage as tuple. 1157 def __init__(self, iterable: Iterable[LinearTypes]) -> None: 1158 """Creates a LinearSum object. A copy of iterable is saved as a tuple.""" 1159 1160 self._elements = tuple(iterable) 1161 for item in self._elements: 1162 if not isinstance(item, (LinearBase, int, float)): 1163 raise TypeError( 1164 "unsupported type in iterable argument for " 1165 f"LinearSum: {type(item).__name__!r}" 1166 ) 1167 1168 @property 1169 def elements(self) -> Tuple[LinearTypes, ...]: 1170 return self._elements 1171 1172 def _flatten_once_and_add_to( 1173 self, 1174 scale: float, 1175 processed_elements: _ProcessedElements, 1176 target_stack: _ToProcessElements, 1177 ) -> None: 1178 for term in self._elements: 1179 if isinstance(term, (int, float)): 1180 processed_elements.offset += scale * float(term) 1181 else: 1182 target_stack.append(term, scale) 1183 1184 def __str__(self): 1185 return str(as_flat_linear_expression(self)) 1186 1187 def __repr__(self): 1188 result = "LinearSum((" 1189 result += ", ".join(repr(linear) for linear in self._elements) 1190 result += "))" 1191 return result 1192 1193 1194class QuadraticSum(QuadraticBase): 1195 # TODO(b/216492143): consider what details to move elsewhere and/or replace 1196 # by examples, and do complexity analysis. 1197 """A deferred sum of QuadraticTypes objects. 1198 1199 QuadraticSum objects are automatically created when a quadratic object is 1200 added to quadratic or linear objects and, as has performance optimizations 1201 similar to LinearSum. 1202 1203 This class is immutable. 1204 """ 1205 1206 __slots__ = "__weakref__", "_elements" 1207 1208 # Potentially unsafe use of Iterable argument is handled by immediate local 1209 # storage as tuple. 1210 def __init__(self, iterable: Iterable[QuadraticTypes]) -> None: 1211 """Creates a QuadraticSum object. A copy of iterable is saved as a tuple.""" 1212 1213 self._elements = tuple(iterable) 1214 for item in self._elements: 1215 if not isinstance(item, (LinearBase, QuadraticBase, int, float)): 1216 raise TypeError( 1217 "unsupported type in iterable argument for " 1218 f"QuadraticSum: {type(item).__name__!r}" 1219 ) 1220 1221 @property 1222 def elements(self) -> Tuple[QuadraticTypes, ...]: 1223 return self._elements 1224 1225 def _quadratic_flatten_once_and_add_to( 1226 self, 1227 scale: float, 1228 processed_elements: _QuadraticProcessedElements, 1229 target_stack: _QuadraticToProcessElements, 1230 ) -> None: 1231 for term in self._elements: 1232 if isinstance(term, (int, float)): 1233 processed_elements.offset += scale * float(term) 1234 else: 1235 target_stack.append(term, scale) 1236 1237 def __str__(self): 1238 return str(as_flat_quadratic_expression(self)) 1239 1240 def __repr__(self): 1241 result = "QuadraticSum((" 1242 result += ", ".join(repr(element) for element in self._elements) 1243 result += "))" 1244 return result 1245 1246 1247class LinearProduct(LinearBase): 1248 """A deferred multiplication computation for linear expressions. 1249 1250 This class is immutable. 1251 """ 1252 1253 __slots__ = "_scalar", "_linear" 1254 1255 def __init__(self, scalar: float, linear: LinearBase) -> None: 1256 if not isinstance(scalar, (float, int)): 1257 raise TypeError( 1258 "unsupported type for scalar argument in " 1259 f"LinearProduct: {type(scalar).__name__!r}" 1260 ) 1261 if not isinstance(linear, LinearBase): 1262 raise TypeError( 1263 "unsupported type for linear argument in " 1264 f"LinearProduct: {type(linear).__name__!r}" 1265 ) 1266 self._scalar: float = float(scalar) 1267 self._linear: LinearBase = linear 1268 1269 @property 1270 def scalar(self) -> float: 1271 return self._scalar 1272 1273 @property 1274 def linear(self) -> LinearBase: 1275 return self._linear 1276 1277 def _flatten_once_and_add_to( 1278 self, 1279 scale: float, 1280 processed_elements: _ProcessedElements, 1281 target_stack: _ToProcessElements, 1282 ) -> None: 1283 target_stack.append(self._linear, self._scalar * scale) 1284 1285 def __str__(self): 1286 return str(as_flat_linear_expression(self)) 1287 1288 def __repr__(self): 1289 result = f"LinearProduct({self._scalar!r}, " 1290 result += f"{self._linear!r})" 1291 return result 1292 1293 1294class QuadraticProduct(QuadraticBase): 1295 """A deferred multiplication computation for quadratic expressions. 1296 1297 This class is immutable. 1298 """ 1299 1300 __slots__ = "_scalar", "_quadratic" 1301 1302 def __init__(self, scalar: float, quadratic: QuadraticBase) -> None: 1303 if not isinstance(scalar, (float, int)): 1304 raise TypeError( 1305 "unsupported type for scalar argument in " 1306 f"QuadraticProduct: {type(scalar).__name__!r}" 1307 ) 1308 if not isinstance(quadratic, QuadraticBase): 1309 raise TypeError( 1310 "unsupported type for linear argument in " 1311 f"QuadraticProduct: {type(quadratic).__name__!r}" 1312 ) 1313 self._scalar: float = float(scalar) 1314 self._quadratic: QuadraticBase = quadratic 1315 1316 @property 1317 def scalar(self) -> float: 1318 return self._scalar 1319 1320 @property 1321 def quadratic(self) -> QuadraticBase: 1322 return self._quadratic 1323 1324 def _quadratic_flatten_once_and_add_to( 1325 self, 1326 scale: float, 1327 processed_elements: _QuadraticProcessedElements, 1328 target_stack: _QuadraticToProcessElements, 1329 ) -> None: 1330 target_stack.append(self._quadratic, self._scalar * scale) 1331 1332 def __str__(self): 1333 return str(as_flat_quadratic_expression(self)) 1334 1335 def __repr__(self): 1336 return f"QuadraticProduct({self._scalar}, {self._quadratic!r})" 1337 1338 1339class LinearLinearProduct(QuadraticBase): 1340 """A deferred multiplication of two linear expressions. 1341 1342 This class is immutable. 1343 """ 1344 1345 __slots__ = "_first_linear", "_second_linear" 1346 1347 def __init__(self, first_linear: LinearBase, second_linear: LinearBase) -> None: 1348 if not isinstance(first_linear, LinearBase): 1349 raise TypeError( 1350 "unsupported type for first_linear argument in " 1351 f"LinearLinearProduct: {type(first_linear).__name__!r}" 1352 ) 1353 if not isinstance(second_linear, LinearBase): 1354 raise TypeError( 1355 "unsupported type for second_linear argument in " 1356 f"LinearLinearProduct: {type(second_linear).__name__!r}" 1357 ) 1358 self._first_linear: LinearBase = first_linear 1359 self._second_linear: LinearBase = second_linear 1360 1361 @property 1362 def first_linear(self) -> LinearBase: 1363 return self._first_linear 1364 1365 @property 1366 def second_linear(self) -> LinearBase: 1367 return self._second_linear 1368 1369 def _quadratic_flatten_once_and_add_to( 1370 self, 1371 scale: float, 1372 processed_elements: _QuadraticProcessedElements, 1373 target_stack: _QuadraticToProcessElements, 1374 ) -> None: 1375 # A recursion is avoided here because as_flat_linear_expression() must never 1376 # call _quadratic_flatten_once_and_add_to(). 1377 first_expression = as_flat_linear_expression(self._first_linear) 1378 second_expression = as_flat_linear_expression(self._second_linear) 1379 processed_elements.offset += ( 1380 first_expression.offset * second_expression.offset * scale 1381 ) 1382 for first_var, first_val in first_expression.terms.items(): 1383 processed_elements.terms[first_var] += ( 1384 second_expression.offset * first_val * scale 1385 ) 1386 for second_var, second_val in second_expression.terms.items(): 1387 processed_elements.terms[second_var] += ( 1388 first_expression.offset * second_val * scale 1389 ) 1390 1391 for first_var, first_val in first_expression.terms.items(): 1392 for second_var, second_val in second_expression.terms.items(): 1393 processed_elements.quadratic_terms[ 1394 QuadraticTermKey(first_var, second_var) 1395 ] += (first_val * second_val * scale) 1396 1397 def __str__(self): 1398 return str(as_flat_quadratic_expression(self)) 1399 1400 def __repr__(self): 1401 result = "LinearLinearProduct(" 1402 result += f"{self._first_linear!r}, " 1403 result += f"{self._second_linear!r})" 1404 return result 1405 1406 1407def as_flat_linear_expression(value: LinearTypes) -> LinearExpression: 1408 """Converts floats, ints and Linear objects to a LinearExpression.""" 1409 if isinstance(value, LinearExpression): 1410 return value 1411 return LinearExpression(value) 1412 1413 1414def as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression: 1415 """Converts floats, ints, LinearBase and QuadraticBase objects to a QuadraticExpression.""" 1416 if isinstance(value, QuadraticExpression): 1417 return value 1418 return QuadraticExpression(value)
86class VarEqVar: 87 """The result of the equality comparison between two Variable. 88 89 We use an object here to delay the evaluation of equality so that we can use 90 the operator== in two use-cases: 91 92 1. when the user want to test that two Variable values references the same 93 variable. This is supported by having this object support implicit 94 conversion to bool. 95 96 2. when the user want to use the equality to create a constraint of equality 97 between two variables. 98 """ 99 100 __slots__ = "_first_variable", "_second_variable" 101 102 def __init__( 103 self, 104 first_variable: "Variable", 105 second_variable: "Variable", 106 ) -> None: 107 self._first_variable: "Variable" = first_variable 108 self._second_variable: "Variable" = second_variable 109 110 @property 111 def first_variable(self) -> "Variable": 112 return self._first_variable 113 114 @property 115 def second_variable(self) -> "Variable": 116 return self._second_variable 117 118 def __bool__(self) -> bool: 119 return ( 120 self._first_variable.elemental is self._second_variable.elemental 121 and self._first_variable.id == self._second_variable.id 122 ) 123 124 def __str__(self): 125 return f"{self.first_variable!s} == {self._second_variable!s}" 126 127 def __repr__(self): 128 return f"{self.first_variable!r} == {self._second_variable!r}"
The result of the equality comparison between two Variable.
We use an object here to delay the evaluation of equality so that we can use the operator== in two use-cases:
when the user want to test that two Variable values references the same variable. This is supported by having this object support implicit conversion to bool.
when the user want to use the equality to create a constraint of equality between two variables.
156class QuadraticTermKey: 157 """An id-ordered pair of variables used as a key for quadratic terms.""" 158 159 __slots__ = "_first_var", "_second_var" 160 161 def __init__(self, a: "Variable", b: "Variable"): 162 """Variables a and b will be ordered internally by their ids.""" 163 self._first_var: "Variable" = a 164 self._second_var: "Variable" = b 165 if self._first_var.id > self._second_var.id: 166 self._first_var, self._second_var = self._second_var, self._first_var 167 168 @property 169 def first_var(self) -> "Variable": 170 return self._first_var 171 172 @property 173 def second_var(self) -> "Variable": 174 return self._second_var 175 176 def __eq__(self, other: "QuadraticTermKey") -> bool: 177 return bool( 178 self._first_var == other._first_var 179 and self._second_var == other._second_var 180 ) 181 182 def __hash__(self) -> int: 183 return hash((self._first_var, self._second_var)) 184 185 def __str__(self): 186 return f"{self._first_var!s} * {self._second_var!s}" 187 188 def __repr__(self): 189 return f"QuadraticTermKey({self._first_var!r}, {self._second_var!r})"
An id-ordered pair of variables used as a key for quadratic terms.
161 def __init__(self, a: "Variable", b: "Variable"): 162 """Variables a and b will be ordered internally by their ids.""" 163 self._first_var: "Variable" = a 164 self._second_var: "Variable" = b 165 if self._first_var.id > self._second_var.id: 166 self._first_var, self._second_var = self._second_var, self._first_var
Variables a and b will be ordered internally by their ids.
247class LinearBase(metaclass=abc.ABCMeta): 248 """Interface for types that can build linear expressions with +, -, * and /. 249 250 Classes derived from LinearBase (plus float and int scalars) are used to 251 build expression trees describing a linear expression. Operations nodes of the 252 expression tree include: 253 254 * LinearSum: describes a deferred sum of LinearTypes objects. 255 * LinearProduct: describes a deferred product of a scalar and a 256 LinearTypes object. 257 258 Leaf nodes of the expression tree include: 259 260 * float and int scalars. 261 * Variable: a single variable. 262 * LinearTerm: the product of a scalar and a Variable object. 263 * LinearExpression: the sum of a scalar and LinearTerm objects. 264 265 LinearBase objects/expression-trees can be used directly to create 266 constraints or objective functions. However, to facilitate their inspection, 267 any LinearTypes object can be flattened to a LinearExpression 268 through: 269 270 as_flat_linear_expression(value: LinearTypes) -> LinearExpression: 271 272 In addition, all LinearBase objects are immutable. 273 274 Performance notes: 275 276 Using an expression tree representation instead of an eager construction of 277 LinearExpression objects reduces known inefficiencies associated with the 278 use of operator overloading to construct linear expressions. In particular, we 279 expect the runtime of as_flat_linear_expression() to be linear in the size of 280 the expression tree. Additional performance can gained by using LinearSum(c) 281 instead of sum(c) for a container c, as the latter creates len(c) LinearSum 282 objects. 283 """ 284 285 __slots__ = () 286 287 # TODO(b/216492143): explore requirements for this function so calculation of 288 # coefficients and offsets follow expected associativity rules (so approximate 289 # float calculations are as expected). 290 # TODO(b/216492143): add more details of what subclasses need to do in 291 # developers guide. 292 @abc.abstractmethod 293 def _flatten_once_and_add_to( 294 self, 295 scale: float, 296 processed_elements: _ProcessedElements, 297 target_stack: _ToProcessElements, 298 ) -> None: 299 """Flatten one level of tree if needed and add to targets. 300 301 Classes derived from LinearBase only need to implement this function 302 to enable transformation to LinearExpression through 303 as_flat_linear_expression(). 304 305 Args: 306 scale: multiply elements by this number when processing or adding to 307 stack. 308 processed_elements: where to add LinearTerms and scalars that can be 309 processed immediately. 310 target_stack: where to add LinearBase elements that are not scalars or 311 LinearTerms (i.e. elements that need further flattening). 312 Implementations should append() to this stack to avoid being recursive. 313 """ 314 315 def __eq__( 316 self, rhs: LinearTypes 317 ) -> ( 318 BoundedLinearExpression 319 ): # pytype: disable=signature-mismatch # overriding-return-type-checks 320 # Note: when rhs is a QuadraticBase, this will cause rhs.__eq__(self) to be 321 # invoked, which is defined. 322 if isinstance(rhs, QuadraticBase): 323 return NotImplemented 324 if isinstance(rhs, (int, float)): 325 return BoundedLinearExpression(rhs, self, rhs) 326 if not isinstance(rhs, LinearBase): 327 _raise_binary_operator_type_error("==", type(self), type(rhs)) 328 return BoundedLinearExpression(0.0, self - rhs, 0.0) 329 330 def __ne__( 331 self, rhs: LinearTypes 332 ) -> ( 333 NoReturn 334 ): # pytype: disable=signature-mismatch # overriding-return-type-checks 335 _raise_ne_not_supported() 336 337 @typing.overload 338 def __le__(self, rhs: float) -> "UpperBoundedLinearExpression": ... 339 340 @typing.overload 341 def __le__(self, rhs: "LinearBase") -> "BoundedLinearExpression": ... 342 343 @typing.overload 344 def __le__(self, rhs: "BoundedLinearExpression") -> NoReturn: ... 345 346 def __le__(self, rhs): 347 # Note: when rhs is a QuadraticBase, this will cause rhs.__ge__(self) to be 348 # invoked, which is defined. 349 if isinstance(rhs, QuadraticBase): 350 return NotImplemented 351 if isinstance(rhs, (int, float)): 352 return UpperBoundedLinearExpression(self, rhs) 353 if isinstance(rhs, LinearBase): 354 return BoundedLinearExpression(-math.inf, self - rhs, 0.0) 355 if isinstance(rhs, bounded_expressions.BoundedExpression): 356 _raise_binary_operator_type_error( 357 "<=", type(self), type(rhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE 358 ) 359 _raise_binary_operator_type_error("<=", type(self), type(rhs)) 360 361 @typing.overload 362 def __ge__(self, lhs: float) -> "LowerBoundedLinearExpression": ... 363 364 @typing.overload 365 def __ge__(self, lhs: "LinearBase") -> "BoundedLinearExpression": ... 366 367 @typing.overload 368 def __ge__(self, lhs: "BoundedLinearExpression") -> NoReturn: ... 369 370 def __ge__(self, lhs): 371 # Note: when lhs is a QuadraticBase, this will cause lhs.__le__(self) to be 372 # invoked, which is defined. 373 if isinstance(lhs, QuadraticBase): 374 return NotImplemented 375 if isinstance(lhs, (int, float)): 376 return LowerBoundedLinearExpression(self, lhs) 377 if isinstance(lhs, LinearBase): 378 return BoundedLinearExpression(0.0, self - lhs, math.inf) 379 if isinstance(lhs, bounded_expressions.BoundedExpression): 380 _raise_binary_operator_type_error( 381 ">=", type(self), type(lhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE 382 ) 383 _raise_binary_operator_type_error(">=", type(self), type(lhs)) 384 385 def __add__(self, expr: LinearTypes) -> "LinearSum": 386 if not isinstance(expr, (int, float, LinearBase)): 387 return NotImplemented 388 return LinearSum((self, expr)) 389 390 def __radd__(self, expr: LinearTypes) -> "LinearSum": 391 if not isinstance(expr, (int, float, LinearBase)): 392 return NotImplemented 393 return LinearSum((expr, self)) 394 395 def __sub__(self, expr: LinearTypes) -> "LinearSum": 396 if not isinstance(expr, (int, float, LinearBase)): 397 return NotImplemented 398 return LinearSum((self, -expr)) 399 400 def __rsub__(self, expr: LinearTypes) -> "LinearSum": 401 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 402 return NotImplemented 403 return LinearSum((expr, -self)) 404 405 @typing.overload 406 def __mul__(self, other: float) -> "LinearProduct": ... 407 408 @typing.overload 409 def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ... 410 411 def __mul__(self, other): 412 if not isinstance(other, (int, float, LinearBase)): 413 return NotImplemented 414 if isinstance(other, LinearBase): 415 return LinearLinearProduct(self, other) 416 return LinearProduct(other, self) 417 418 def __rmul__(self, constant: float) -> "LinearProduct": 419 if not isinstance(constant, (int, float)): 420 return NotImplemented 421 return LinearProduct(constant, self) 422 423 # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. 424 def __truediv__(self, constant: float) -> "LinearProduct": 425 if not isinstance(constant, (int, float)): 426 return NotImplemented 427 return LinearProduct(1.0 / constant, self) 428 429 def __neg__(self) -> "LinearProduct": 430 return LinearProduct(-1.0, self)
Interface for types that can build linear expressions with +, -, * and /.
Classes derived from LinearBase (plus float and int scalars) are used to build expression trees describing a linear expression. Operations nodes of the expression tree include:
- LinearSum: describes a deferred sum of LinearTypes objects.
- LinearProduct: describes a deferred product of a scalar and a LinearTypes object.
Leaf nodes of the expression tree include:
- float and int scalars.
- Variable: a single variable.
- LinearTerm: the product of a scalar and a Variable object.
- LinearExpression: the sum of a scalar and LinearTerm objects.
LinearBase objects/expression-trees can be used directly to create constraints or objective functions. However, to facilitate their inspection, any LinearTypes object can be flattened to a LinearExpression through:
as_flat_linear_expression(value: LinearTypes) -> LinearExpression:
In addition, all LinearBase objects are immutable.
Performance notes:
Using an expression tree representation instead of an eager construction of LinearExpression objects reduces known inefficiencies associated with the use of operator overloading to construct linear expressions. In particular, we expect the runtime of as_flat_linear_expression() to be linear in the size of the expression tree. Additional performance can gained by using LinearSum(c) instead of sum(c) for a container c, as the latter creates len(c) LinearSum objects.
433class QuadraticBase(metaclass=abc.ABCMeta): 434 """Interface for types that can build quadratic expressions with +, -, * and /. 435 436 Classes derived from QuadraticBase and LinearBase (plus float and int scalars) 437 are used to build expression trees describing a quadratic expression. 438 Operations nodes of the expression tree include: 439 440 * QuadraticSum: describes a deferred sum of QuadraticTypes objects. 441 * QuadraticProduct: describes a deferred product of a scalar and a 442 QuadraticTypes object. 443 * LinearLinearProduct: describes a deferred product of two LinearTypes 444 objects. 445 446 Leaf nodes of the expression tree include: 447 448 * float and int scalars. 449 * Variable: a single variable. 450 * LinearTerm: the product of a scalar and a Variable object. 451 * LinearExpression: the sum of a scalar and LinearTerm objects. 452 * QuadraticTerm: the product of a scalar and two Variable objects. 453 * QuadraticExpression: the sum of a scalar, LinearTerm objects and 454 QuadraticTerm objects. 455 456 QuadraticBase objects/expression-trees can be used directly to create 457 objective functions. However, to facilitate their inspection, any 458 QuadraticTypes object can be flattened to a QuadraticExpression 459 through: 460 461 as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression: 462 463 In addition, all QuadraticBase objects are immutable. 464 465 Performance notes: 466 467 Using an expression tree representation instead of an eager construction of 468 QuadraticExpression objects reduces known inefficiencies associated with the 469 use of operator overloading to construct quadratic expressions. In particular, 470 we expect the runtime of as_flat_quadratic_expression() to be linear in the 471 size of the expression tree. Additional performance can gained by using 472 QuadraticSum(c) instead of sum(c) for a container c, as the latter creates 473 len(c) QuadraticSum objects. 474 """ 475 476 __slots__ = () 477 478 # TODO(b/216492143): explore requirements for this function so calculation of 479 # coefficients and offsets follow expected associativity rules (so approximate 480 # float calculations are as expected). 481 # TODO(b/216492143): add more details of what subclasses need to do in 482 # developers guide. 483 @abc.abstractmethod 484 def _quadratic_flatten_once_and_add_to( 485 self, 486 scale: float, 487 processed_elements: _QuadraticProcessedElements, 488 target_stack: _QuadraticToProcessElements, 489 ) -> None: 490 """Flatten one level of tree if needed and add to targets. 491 492 Classes derived from QuadraticBase only need to implement this function 493 to enable transformation to QuadraticExpression through 494 as_flat_quadratic_expression(). 495 496 Args: 497 scale: multiply elements by this number when processing or adding to 498 stack. 499 processed_elements: where to add linear terms, quadratic terms and scalars 500 that can be processed immediately. 501 target_stack: where to add LinearBase and QuadraticBase elements that are 502 not scalars or linear terms or quadratic terms (i.e. elements that need 503 further flattening). Implementations should append() to this stack to 504 avoid being recursive. 505 """ 506 507 def __eq__( 508 self, rhs: QuadraticTypes 509 ) -> ( 510 BoundedQuadraticExpression 511 ): # pytype: disable=signature-mismatch # overriding-return-type-checks 512 if isinstance(rhs, (int, float)): 513 return BoundedQuadraticExpression(rhs, self, rhs) 514 if not isinstance(rhs, (LinearBase, QuadraticBase)): 515 _raise_binary_operator_type_error("==", type(self), type(rhs)) 516 return BoundedQuadraticExpression(0.0, self - rhs, 0.0) 517 518 def __ne__( 519 self, rhs: QuadraticTypes 520 ) -> ( 521 NoReturn 522 ): # pytype: disable=signature-mismatch # overriding-return-type-checks 523 _raise_ne_not_supported() 524 525 @typing.overload 526 def __le__(self, rhs: float) -> UpperBoundedQuadraticExpression: ... 527 528 @typing.overload 529 def __le__( 530 self, rhs: Union[LinearBase, "QuadraticBase"] 531 ) -> BoundedQuadraticExpression: ... 532 533 @typing.overload 534 def __le__(self, rhs: BoundedQuadraticExpression) -> NoReturn: ... 535 536 def __le__(self, rhs): 537 if isinstance(rhs, (int, float)): 538 return UpperBoundedQuadraticExpression(self, rhs) 539 if isinstance(rhs, (LinearBase, QuadraticBase)): 540 return BoundedQuadraticExpression(-math.inf, self - rhs, 0.0) 541 if isinstance(rhs, bounded_expressions.BoundedExpression): 542 _raise_binary_operator_type_error( 543 "<=", type(self), type(rhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE 544 ) 545 _raise_binary_operator_type_error("<=", type(self), type(rhs)) 546 547 @typing.overload 548 def __ge__(self, lhs: float) -> LowerBoundedQuadraticExpression: ... 549 550 @typing.overload 551 def __ge__( 552 self, lhs: Union[LinearBase, "QuadraticBase"] 553 ) -> BoundedQuadraticExpression: ... 554 555 @typing.overload 556 def __ge__(self, lhs: BoundedQuadraticExpression) -> NoReturn: ... 557 558 def __ge__(self, lhs): 559 if isinstance(lhs, (int, float)): 560 return LowerBoundedQuadraticExpression(self, lhs) 561 if isinstance(lhs, (LinearBase, QuadraticBase)): 562 return BoundedQuadraticExpression(0.0, self - lhs, math.inf) 563 if isinstance(lhs, bounded_expressions.BoundedExpression): 564 _raise_binary_operator_type_error( 565 ">=", type(self), type(lhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE 566 ) 567 _raise_binary_operator_type_error(">=", type(self), type(lhs)) 568 569 def __add__(self, expr: QuadraticTypes) -> "QuadraticSum": 570 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 571 return NotImplemented 572 return QuadraticSum((self, expr)) 573 574 def __radd__(self, expr: QuadraticTypes) -> "QuadraticSum": 575 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 576 return NotImplemented 577 return QuadraticSum((expr, self)) 578 579 def __sub__(self, expr: QuadraticTypes) -> "QuadraticSum": 580 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 581 return NotImplemented 582 return QuadraticSum((self, -expr)) 583 584 def __rsub__(self, expr: QuadraticTypes) -> "QuadraticSum": 585 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 586 return NotImplemented 587 return QuadraticSum((expr, -self)) 588 589 def __mul__(self, other: float) -> "QuadraticProduct": 590 if not isinstance(other, (int, float)): 591 return NotImplemented 592 return QuadraticProduct(other, self) 593 594 def __rmul__(self, other: float) -> "QuadraticProduct": 595 if not isinstance(other, (int, float)): 596 return NotImplemented 597 return QuadraticProduct(other, self) 598 599 # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. 600 def __truediv__(self, constant: float) -> "QuadraticProduct": 601 if not isinstance(constant, (int, float)): 602 return NotImplemented 603 return QuadraticProduct(1.0 / constant, self) 604 605 def __neg__(self) -> "QuadraticProduct": 606 return QuadraticProduct(-1.0, self)
Interface for types that can build quadratic expressions with +, -, * and /.
Classes derived from QuadraticBase and LinearBase (plus float and int scalars) are used to build expression trees describing a quadratic expression.
Operations nodes of the expression tree include:
- QuadraticSum: describes a deferred sum of QuadraticTypes objects.
- QuadraticProduct: describes a deferred product of a scalar and a QuadraticTypes object.
- LinearLinearProduct: describes a deferred product of two LinearTypes objects.
Leaf nodes of the expression tree include:
- float and int scalars.
- Variable: a single variable.
- LinearTerm: the product of a scalar and a Variable object.
- LinearExpression: the sum of a scalar and LinearTerm objects.
- QuadraticTerm: the product of a scalar and two Variable objects.
- QuadraticExpression: the sum of a scalar, LinearTerm objects and QuadraticTerm objects.
QuadraticBase objects/expression-trees can be used directly to create objective functions. However, to facilitate their inspection, any QuadraticTypes object can be flattened to a QuadraticExpression through:
as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression:
In addition, all QuadraticBase objects are immutable.
Performance notes:
Using an expression tree representation instead of an eager construction of QuadraticExpression objects reduces known inefficiencies associated with the use of operator overloading to construct quadratic expressions. In particular, we expect the runtime of as_flat_quadratic_expression() to be linear in the size of the expression tree. Additional performance can gained by using QuadraticSum(c) instead of sum(c) for a container c, as the latter creates len(c) QuadraticSum objects.
609class Variable(LinearBase, from_model.FromModel): 610 """A decision variable for an optimization model. 611 612 A decision variable takes a value from a domain, either the real numbers or 613 the integers, and restricted to be in some interval [lb, ub] (where lb and ub 614 can be infinite). The case of lb == ub is allowed, this means the variable 615 must take a single value. The case of lb > ub is also allowed, this implies 616 that the problem is infeasible. 617 618 A Variable is configured as follows: 619 * lower_bound: a float property, lb above. Should not be NaN nor +inf. 620 * upper_bound: a float property, ub above. Should not be NaN nor -inf. 621 * integer: a bool property, if the domain is integer or continuous. 622 623 The name is optional, read only, and used only for debugging. Non-empty names 624 should be distinct. 625 626 Every Variable is associated with a Model (defined below). Note that data 627 describing the variable (e.g. lower_bound) is owned by Model.storage, this 628 class is simply a reference to that data. Do not create a Variable directly, 629 use Model.add_variable() instead. 630 """ 631 632 __slots__ = "_elemental", "_id" 633 634 def __init__(self, elem: elemental.Elemental, vid: int) -> None: 635 """Internal only, prefer Model functions (add_variable() and get_variable()).""" 636 if not isinstance(vid, int): 637 raise TypeError(f"vid type should be int, was:{type(vid)}") 638 self._elemental: elemental.Elemental = elem 639 self._id: int = vid 640 641 @property 642 def lower_bound(self) -> float: 643 return self._elemental.get_attr( 644 enums.DoubleAttr1.VARIABLE_LOWER_BOUND, (self._id,) 645 ) 646 647 @lower_bound.setter 648 def lower_bound(self, value: float) -> None: 649 self._elemental.set_attr( 650 enums.DoubleAttr1.VARIABLE_LOWER_BOUND, 651 (self._id,), 652 value, 653 ) 654 655 @property 656 def upper_bound(self) -> float: 657 return self._elemental.get_attr( 658 enums.DoubleAttr1.VARIABLE_UPPER_BOUND, (self._id,) 659 ) 660 661 @upper_bound.setter 662 def upper_bound(self, value: float) -> None: 663 self._elemental.set_attr( 664 enums.DoubleAttr1.VARIABLE_UPPER_BOUND, 665 (self._id,), 666 value, 667 ) 668 669 @property 670 def integer(self) -> bool: 671 return self._elemental.get_attr(enums.BoolAttr1.VARIABLE_INTEGER, (self._id,)) 672 673 @integer.setter 674 def integer(self, value: bool) -> None: 675 self._elemental.set_attr(enums.BoolAttr1.VARIABLE_INTEGER, (self._id,), value) 676 677 @property 678 def name(self) -> str: 679 return self._elemental.get_element_name(enums.ElementType.VARIABLE, self._id) 680 681 @property 682 def id(self) -> int: 683 return self._id 684 685 @property 686 def elemental(self) -> elemental.Elemental: 687 """Internal use only.""" 688 return self._elemental 689 690 def __str__(self): 691 """Returns the name, or a string containing the id if the name is empty.""" 692 return self.name if self.name else f"variable_{self.id}" 693 694 def __repr__(self): 695 return f"<Variable id: {self.id}, name: {self.name!r}>" 696 697 @typing.overload 698 def __eq__(self, rhs: "Variable") -> "VarEqVar": ... 699 700 @typing.overload 701 def __eq__(self, rhs: LinearTypesExceptVariable) -> "BoundedLinearExpression": ... 702 703 def __eq__(self, rhs): 704 if isinstance(rhs, Variable): 705 return VarEqVar(self, rhs) 706 return super().__eq__(rhs) 707 708 @typing.overload 709 def __ne__(self, rhs: "Variable") -> bool: ... 710 711 @typing.overload 712 def __ne__(self, rhs: LinearTypesExceptVariable) -> NoReturn: ... 713 714 def __ne__(self, rhs): 715 if isinstance(rhs, Variable): 716 return not self == rhs 717 _raise_ne_not_supported() 718 719 def __hash__(self) -> int: 720 return hash(self._id) 721 722 @typing.overload 723 def __mul__(self, other: float) -> "LinearTerm": ... 724 725 @typing.overload 726 def __mul__(self, other: Union["Variable", "LinearTerm"]) -> "QuadraticTerm": ... 727 728 @typing.overload 729 def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ... 730 731 def __mul__(self, other): 732 if not isinstance(other, (int, float, LinearBase)): 733 return NotImplemented 734 if isinstance(other, Variable): 735 return QuadraticTerm(QuadraticTermKey(self, other), 1.0) 736 if isinstance(other, LinearTerm): 737 return QuadraticTerm( 738 QuadraticTermKey(self, other.variable), other.coefficient 739 ) 740 if isinstance(other, LinearBase): 741 return LinearLinearProduct(self, other) # pytype: disable=bad-return-type 742 return LinearTerm(self, other) 743 744 def __rmul__(self, constant: float) -> "LinearTerm": 745 if not isinstance(constant, (int, float)): 746 return NotImplemented 747 return LinearTerm(self, constant) 748 749 # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. 750 def __truediv__(self, constant: float) -> "LinearTerm": 751 if not isinstance(constant, (int, float)): 752 return NotImplemented 753 return LinearTerm(self, 1.0 / constant) 754 755 def __neg__(self) -> "LinearTerm": 756 return LinearTerm(self, -1.0) 757 758 def _flatten_once_and_add_to( 759 self, 760 scale: float, 761 processed_elements: _ProcessedElements, 762 target_stack: _ToProcessElements, 763 ) -> None: 764 processed_elements.terms[self] += scale
A decision variable for an optimization model.
A decision variable takes a value from a domain, either the real numbers or the integers, and restricted to be in some interval [lb, ub] (where lb and ub can be infinite). The case of lb == ub is allowed, this means the variable must take a single value. The case of lb > ub is also allowed, this implies that the problem is infeasible.
A Variable is configured as follows:
- lower_bound: a float property, lb above. Should not be NaN nor +inf.
- upper_bound: a float property, ub above. Should not be NaN nor -inf.
- integer: a bool property, if the domain is integer or continuous.
The name is optional, read only, and used only for debugging. Non-empty names should be distinct.
Every Variable is associated with a Model (defined below). Note that data describing the variable (e.g. lower_bound) is owned by Model.storage, this class is simply a reference to that data. Do not create a Variable directly, use Model.add_variable() instead.
634 def __init__(self, elem: elemental.Elemental, vid: int) -> None: 635 """Internal only, prefer Model functions (add_variable() and get_variable()).""" 636 if not isinstance(vid, int): 637 raise TypeError(f"vid type should be int, was:{type(vid)}") 638 self._elemental: elemental.Elemental = elem 639 self._id: int = vid
Internal only, prefer Model functions (add_variable() and get_variable()).
767class LinearTerm(LinearBase): 768 """The product of a scalar and a variable. 769 770 This class is immutable. 771 """ 772 773 __slots__ = "_variable", "_coefficient" 774 775 def __init__(self, variable: Variable, coefficient: float) -> None: 776 self._variable: Variable = variable 777 self._coefficient: float = coefficient 778 779 @property 780 def variable(self) -> Variable: 781 return self._variable 782 783 @property 784 def coefficient(self) -> float: 785 return self._coefficient 786 787 def _flatten_once_and_add_to( 788 self, 789 scale: float, 790 processed_elements: _ProcessedElements, 791 target_stack: _ToProcessElements, 792 ) -> None: 793 processed_elements.terms[self._variable] += self._coefficient * scale 794 795 @typing.overload 796 def __mul__(self, other: float) -> "LinearTerm": ... 797 798 @typing.overload 799 def __mul__(self, other: Union["Variable", "LinearTerm"]) -> "QuadraticTerm": ... 800 801 @typing.overload 802 def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ... 803 804 def __mul__(self, other): 805 if not isinstance(other, (int, float, LinearBase)): 806 return NotImplemented 807 if isinstance(other, Variable): 808 return QuadraticTerm( 809 QuadraticTermKey(self._variable, other), self._coefficient 810 ) 811 if isinstance(other, LinearTerm): 812 return QuadraticTerm( 813 QuadraticTermKey(self.variable, other.variable), 814 self._coefficient * other.coefficient, 815 ) 816 if isinstance(other, LinearBase): 817 return LinearLinearProduct(self, other) # pytype: disable=bad-return-type 818 return LinearTerm(self._variable, self._coefficient * other) 819 820 def __rmul__(self, constant: float) -> "LinearTerm": 821 if not isinstance(constant, (int, float)): 822 return NotImplemented 823 return LinearTerm(self._variable, self._coefficient * constant) 824 825 def __truediv__(self, constant: float) -> "LinearTerm": 826 if not isinstance(constant, (int, float)): 827 return NotImplemented 828 return LinearTerm(self._variable, self._coefficient / constant) 829 830 def __neg__(self) -> "LinearTerm": 831 return LinearTerm(self._variable, -self._coefficient) 832 833 def __str__(self): 834 return f"{self._coefficient} * {self._variable}" 835 836 def __repr__(self): 837 return f"LinearTerm({self._variable!r}, {self._coefficient!r})"
The product of a scalar and a variable.
This class is immutable.
840class QuadraticTerm(QuadraticBase): 841 """The product of a scalar and two variables. 842 843 This class is immutable. 844 """ 845 846 __slots__ = "_key", "_coefficient" 847 848 def __init__(self, key: QuadraticTermKey, coefficient: float) -> None: 849 self._key: QuadraticTermKey = key 850 self._coefficient: float = coefficient 851 852 @property 853 def key(self) -> QuadraticTermKey: 854 return self._key 855 856 @property 857 def coefficient(self) -> float: 858 return self._coefficient 859 860 def _quadratic_flatten_once_and_add_to( 861 self, 862 scale: float, 863 processed_elements: _QuadraticProcessedElements, 864 target_stack: _ToProcessElements, 865 ) -> None: 866 processed_elements.quadratic_terms[self._key] += self._coefficient * scale 867 868 def __mul__(self, constant: float) -> "QuadraticTerm": 869 if not isinstance(constant, (int, float)): 870 return NotImplemented 871 return QuadraticTerm(self._key, self._coefficient * constant) 872 873 def __rmul__(self, constant: float) -> "QuadraticTerm": 874 if not isinstance(constant, (int, float)): 875 return NotImplemented 876 return QuadraticTerm(self._key, self._coefficient * constant) 877 878 def __truediv__(self, constant: float) -> "QuadraticTerm": 879 if not isinstance(constant, (int, float)): 880 return NotImplemented 881 return QuadraticTerm(self._key, self._coefficient / constant) 882 883 def __neg__(self) -> "QuadraticTerm": 884 return QuadraticTerm(self._key, -self._coefficient) 885 886 def __str__(self): 887 return f"{self._coefficient} * {self._key!s}" 888 889 def __repr__(self): 890 return f"QuadraticTerm({self._key!r}, {self._coefficient})"
The product of a scalar and two variables.
This class is immutable.
893class LinearExpression(LinearBase): 894 """For variables x, an expression: b + sum_{i in I} a_i * x_i. 895 896 This class is immutable. 897 """ 898 899 __slots__ = "__weakref__", "_terms", "_offset" 900 901 # TODO(b/216492143): consider initializing from a dictionary. 902 def __init__(self, /, other: LinearTypes = 0) -> None: 903 self._offset: float = 0.0 904 if isinstance(other, (int, float)): 905 self._offset = float(other) 906 self._terms: Mapping[Variable, float] = immutabledict.immutabledict() 907 return 908 909 to_process: _LinearToProcessElements = _LinearToProcessElements(other, 1.0) 910 processed_elements = _ProcessedElements() 911 while to_process: 912 linear, coef = to_process.pop() 913 linear._flatten_once_and_add_to(coef, processed_elements, to_process) 914 # TODO(b/216492143): explore avoiding this copy. 915 self._terms: Mapping[Variable, float] = immutabledict.immutabledict( 916 processed_elements.terms 917 ) 918 self._offset = processed_elements.offset 919 920 @property 921 def terms(self) -> Mapping[Variable, float]: 922 return self._terms 923 924 @property 925 def offset(self) -> float: 926 return self._offset 927 928 def evaluate(self, variable_values: Mapping[Variable, float]) -> float: 929 """Returns the value of this expression for given variable values. 930 931 E.g. if this is 3 * x + 4 and variable_values = {x: 2.0}, then 932 evaluate(variable_values) equals 10.0. 933 934 See also mathopt.evaluate_expression(), which works on any type in 935 QuadraticTypes. 936 937 Args: 938 variable_values: Must contain a value for every variable in expression. 939 940 Returns: 941 The value of this expression when replacing variables by their value. 942 """ 943 result = self._offset 944 for var, coef in sorted( 945 self._terms.items(), key=lambda var_coef_pair: var_coef_pair[0].id 946 ): 947 result += coef * variable_values[var] 948 return result 949 950 def _flatten_once_and_add_to( 951 self, 952 scale: float, 953 processed_elements: _ProcessedElements, 954 target_stack: _ToProcessElements, 955 ) -> None: 956 for var, val in self._terms.items(): 957 processed_elements.terms[var] += val * scale 958 processed_elements.offset += scale * self.offset 959 960 # TODO(b/216492143): change __str__ to match C++ implementation in 961 # cl/421649402. 962 def __str__(self): 963 """Returns the name, or a string containing the id if the name is empty.""" 964 result = str(self.offset) 965 sorted_keys = sorted(self._terms.keys(), key=str) 966 for var in sorted_keys: 967 # TODO(b/216492143): consider how to better deal with `NaN` and try to 968 # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in 969 # linear_expression_test.py. 970 coefficient = self._terms[var] 971 if coefficient == 0.0: 972 continue 973 if coefficient > 0: 974 result += " + " 975 else: 976 result += " - " 977 result += str(abs(coefficient)) + " * " + str(var) 978 return result 979 980 def __repr__(self): 981 result = f"LinearExpression({self.offset}, " + "{" 982 result += ", ".join( 983 f"{var!r}: {coefficient}" for var, coefficient in self._terms.items() 984 ) 985 result += "})" 986 return result
For variables x, an expression: b + sum_{i in I} a_i * x_i.
This class is immutable.
902 def __init__(self, /, other: LinearTypes = 0) -> None: 903 self._offset: float = 0.0 904 if isinstance(other, (int, float)): 905 self._offset = float(other) 906 self._terms: Mapping[Variable, float] = immutabledict.immutabledict() 907 return 908 909 to_process: _LinearToProcessElements = _LinearToProcessElements(other, 1.0) 910 processed_elements = _ProcessedElements() 911 while to_process: 912 linear, coef = to_process.pop() 913 linear._flatten_once_and_add_to(coef, processed_elements, to_process) 914 # TODO(b/216492143): explore avoiding this copy. 915 self._terms: Mapping[Variable, float] = immutabledict.immutabledict( 916 processed_elements.terms 917 ) 918 self._offset = processed_elements.offset
928 def evaluate(self, variable_values: Mapping[Variable, float]) -> float: 929 """Returns the value of this expression for given variable values. 930 931 E.g. if this is 3 * x + 4 and variable_values = {x: 2.0}, then 932 evaluate(variable_values) equals 10.0. 933 934 See also mathopt.evaluate_expression(), which works on any type in 935 QuadraticTypes. 936 937 Args: 938 variable_values: Must contain a value for every variable in expression. 939 940 Returns: 941 The value of this expression when replacing variables by their value. 942 """ 943 result = self._offset 944 for var, coef in sorted( 945 self._terms.items(), key=lambda var_coef_pair: var_coef_pair[0].id 946 ): 947 result += coef * variable_values[var] 948 return result
Returns the value of this expression for given variable values.
E.g. if this is 3 * x + 4 and variable_values = {x: 2.0}, then evaluate(variable_values) equals 10.0.
See also mathopt.evaluate_expression(), which works on any type in QuadraticTypes.
Arguments:
- variable_values: Must contain a value for every variable in expression.
Returns:
The value of this expression when replacing variables by their value.
989class QuadraticExpression(QuadraticBase): 990 """For variables x, an expression: b + sum_{i in I} a_i * x_i + sum_{i,j in I, i<=j} a_i,j * x_i * x_j. 991 992 This class is immutable. 993 """ 994 995 __slots__ = "__weakref__", "_linear_terms", "_quadratic_terms", "_offset" 996 997 # TODO(b/216492143): consider initializing from a dictionary. 998 def __init__(self, other: QuadraticTypes) -> None: 999 self._offset: float = 0.0 1000 if isinstance(other, (int, float)): 1001 self._offset = float(other) 1002 self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict() 1003 self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( 1004 immutabledict.immutabledict() 1005 ) 1006 return 1007 1008 to_process: _QuadraticToProcessElements = _QuadraticToProcessElements( 1009 other, 1.0 1010 ) 1011 processed_elements = _QuadraticProcessedElements() 1012 while to_process: 1013 linear_or_quadratic, coef = to_process.pop() 1014 if isinstance(linear_or_quadratic, LinearBase): 1015 linear_or_quadratic._flatten_once_and_add_to( 1016 coef, processed_elements, to_process 1017 ) 1018 else: 1019 linear_or_quadratic._quadratic_flatten_once_and_add_to( 1020 coef, processed_elements, to_process 1021 ) 1022 # TODO(b/216492143): explore avoiding this copy. 1023 self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict( 1024 processed_elements.terms 1025 ) 1026 self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( 1027 immutabledict.immutabledict(processed_elements.quadratic_terms) 1028 ) 1029 self._offset = processed_elements.offset 1030 1031 @property 1032 def linear_terms(self) -> Mapping[Variable, float]: 1033 return self._linear_terms 1034 1035 @property 1036 def quadratic_terms(self) -> Mapping[QuadraticTermKey, float]: 1037 return self._quadratic_terms 1038 1039 @property 1040 def offset(self) -> float: 1041 return self._offset 1042 1043 def evaluate(self, variable_values: Mapping[Variable, float]) -> float: 1044 """Returns the value of this expression for given variable values. 1045 1046 E.g. if this is 3 * x * x + 4 and variable_values = {x: 2.0}, then 1047 evaluate(variable_values) equals 16.0. 1048 1049 See also mathopt.evaluate_expression(), which works on any type in 1050 QuadraticTypes. 1051 1052 Args: 1053 variable_values: Must contain a value for every variable in expression. 1054 1055 Returns: 1056 The value of this expression when replacing variables by their value. 1057 """ 1058 result = self._offset 1059 for var, coef in sorted( 1060 self._linear_terms.items(), 1061 key=lambda var_coef_pair: var_coef_pair[0].id, 1062 ): 1063 result += coef * variable_values[var] 1064 for key, coef in sorted( 1065 self._quadratic_terms.items(), 1066 key=lambda quad_coef_pair: ( 1067 quad_coef_pair[0].first_var.id, 1068 quad_coef_pair[0].second_var.id, 1069 ), 1070 ): 1071 result += ( 1072 coef * variable_values[key.first_var] * variable_values[key.second_var] 1073 ) 1074 return result 1075 1076 def _quadratic_flatten_once_and_add_to( 1077 self, 1078 scale: float, 1079 processed_elements: _QuadraticProcessedElements, 1080 target_stack: _QuadraticToProcessElements, 1081 ) -> None: 1082 for var, val in self._linear_terms.items(): 1083 processed_elements.terms[var] += val * scale 1084 for key, val in self._quadratic_terms.items(): 1085 processed_elements.quadratic_terms[key] += val * scale 1086 processed_elements.offset += scale * self.offset 1087 1088 # TODO(b/216492143): change __str__ to match C++ implementation in 1089 # cl/421649402. 1090 def __str__(self): 1091 result = str(self.offset) 1092 sorted_linear_keys = sorted(self._linear_terms.keys(), key=str) 1093 for var in sorted_linear_keys: 1094 # TODO(b/216492143): consider how to better deal with `NaN` and try to 1095 # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in 1096 # linear_expression_test.py. 1097 coefficient = self._linear_terms[var] 1098 if coefficient == 0.0: 1099 continue 1100 if coefficient > 0: 1101 result += " + " 1102 else: 1103 result += " - " 1104 result += str(abs(coefficient)) + " * " + str(var) 1105 sorted_quadratic_keys = sorted(self._quadratic_terms.keys(), key=str) 1106 for key in sorted_quadratic_keys: 1107 # TODO(b/216492143): consider how to better deal with `NaN` and try to 1108 # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in 1109 # linear_expression_test.py. 1110 coefficient = self._quadratic_terms[key] 1111 if coefficient == 0.0: 1112 continue 1113 if coefficient > 0: 1114 result += " + " 1115 else: 1116 result += " - " 1117 result += str(abs(coefficient)) + " * " + str(key) 1118 return result 1119 1120 def __repr__(self): 1121 result = f"QuadraticExpression({self.offset}, " + "{" 1122 result += ", ".join( 1123 f"{var!r}: {coefficient}" for var, coefficient in self._linear_terms.items() 1124 ) 1125 result += "}, {" 1126 result += ", ".join( 1127 f"{key!r}: {coefficient}" 1128 for key, coefficient in self._quadratic_terms.items() 1129 ) 1130 result += "})" 1131 return result
For variables x, an expression: b + sum_{i in I} a_i * x_i + sum_{i,j in I, i<=j} a_i,j * x_i * x_j.
This class is immutable.
998 def __init__(self, other: QuadraticTypes) -> None: 999 self._offset: float = 0.0 1000 if isinstance(other, (int, float)): 1001 self._offset = float(other) 1002 self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict() 1003 self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( 1004 immutabledict.immutabledict() 1005 ) 1006 return 1007 1008 to_process: _QuadraticToProcessElements = _QuadraticToProcessElements( 1009 other, 1.0 1010 ) 1011 processed_elements = _QuadraticProcessedElements() 1012 while to_process: 1013 linear_or_quadratic, coef = to_process.pop() 1014 if isinstance(linear_or_quadratic, LinearBase): 1015 linear_or_quadratic._flatten_once_and_add_to( 1016 coef, processed_elements, to_process 1017 ) 1018 else: 1019 linear_or_quadratic._quadratic_flatten_once_and_add_to( 1020 coef, processed_elements, to_process 1021 ) 1022 # TODO(b/216492143): explore avoiding this copy. 1023 self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict( 1024 processed_elements.terms 1025 ) 1026 self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( 1027 immutabledict.immutabledict(processed_elements.quadratic_terms) 1028 ) 1029 self._offset = processed_elements.offset
1043 def evaluate(self, variable_values: Mapping[Variable, float]) -> float: 1044 """Returns the value of this expression for given variable values. 1045 1046 E.g. if this is 3 * x * x + 4 and variable_values = {x: 2.0}, then 1047 evaluate(variable_values) equals 16.0. 1048 1049 See also mathopt.evaluate_expression(), which works on any type in 1050 QuadraticTypes. 1051 1052 Args: 1053 variable_values: Must contain a value for every variable in expression. 1054 1055 Returns: 1056 The value of this expression when replacing variables by their value. 1057 """ 1058 result = self._offset 1059 for var, coef in sorted( 1060 self._linear_terms.items(), 1061 key=lambda var_coef_pair: var_coef_pair[0].id, 1062 ): 1063 result += coef * variable_values[var] 1064 for key, coef in sorted( 1065 self._quadratic_terms.items(), 1066 key=lambda quad_coef_pair: ( 1067 quad_coef_pair[0].first_var.id, 1068 quad_coef_pair[0].second_var.id, 1069 ), 1070 ): 1071 result += ( 1072 coef * variable_values[key.first_var] * variable_values[key.second_var] 1073 ) 1074 return result
Returns the value of this expression for given variable values.
E.g. if this is 3 * x * x + 4 and variable_values = {x: 2.0}, then evaluate(variable_values) equals 16.0.
See also mathopt.evaluate_expression(), which works on any type in QuadraticTypes.
Arguments:
- variable_values: Must contain a value for every variable in expression.
Returns:
The value of this expression when replacing variables by their value.
1134class LinearSum(LinearBase): 1135 # TODO(b/216492143): consider what details to move elsewhere and/or replace 1136 # by examples, and do complexity analysis. 1137 """A deferred sum of LinearBase objects. 1138 1139 LinearSum objects are automatically created when two linear objects are added 1140 and, as noted in the documentation for Linear, can reduce the inefficiencies. 1141 In particular, they are created when calling sum(iterable) when iterable is 1142 an Iterable[LinearTypes]. However, using LinearSum(iterable) instead 1143 can result in additional performance improvements: 1144 1145 * sum(iterable): creates a nested set of LinearSum objects (e.g. 1146 `sum([a, b, c])` is `LinearSum(0, LinearSum(a, LinearSum(b, c)))`). 1147 * LinearSum(iterable): creates a single LinearSum that saves a tuple with 1148 all the LinearTypes objects in iterable (e.g. 1149 `LinearSum([a, b, c])` does not create additional objects). 1150 1151 This class is immutable. 1152 """ 1153 1154 __slots__ = "__weakref__", "_elements" 1155 1156 # Potentially unsafe use of Iterable argument is handled by immediate local 1157 # storage as tuple. 1158 def __init__(self, iterable: Iterable[LinearTypes]) -> None: 1159 """Creates a LinearSum object. A copy of iterable is saved as a tuple.""" 1160 1161 self._elements = tuple(iterable) 1162 for item in self._elements: 1163 if not isinstance(item, (LinearBase, int, float)): 1164 raise TypeError( 1165 "unsupported type in iterable argument for " 1166 f"LinearSum: {type(item).__name__!r}" 1167 ) 1168 1169 @property 1170 def elements(self) -> Tuple[LinearTypes, ...]: 1171 return self._elements 1172 1173 def _flatten_once_and_add_to( 1174 self, 1175 scale: float, 1176 processed_elements: _ProcessedElements, 1177 target_stack: _ToProcessElements, 1178 ) -> None: 1179 for term in self._elements: 1180 if isinstance(term, (int, float)): 1181 processed_elements.offset += scale * float(term) 1182 else: 1183 target_stack.append(term, scale) 1184 1185 def __str__(self): 1186 return str(as_flat_linear_expression(self)) 1187 1188 def __repr__(self): 1189 result = "LinearSum((" 1190 result += ", ".join(repr(linear) for linear in self._elements) 1191 result += "))" 1192 return result
A deferred sum of LinearBase objects.
LinearSum objects are automatically created when two linear objects are added and, as noted in the documentation for Linear, can reduce the inefficiencies. In particular, they are created when calling sum(iterable) when iterable is an Iterable[LinearTypes]. However, using LinearSum(iterable) instead can result in additional performance improvements:
- sum(iterable): creates a nested set of LinearSum objects (e.g.
sum([a, b, c])
isLinearSum(0, LinearSum(a, LinearSum(b, c)))
). - LinearSum(iterable): creates a single LinearSum that saves a tuple with
all the LinearTypes objects in iterable (e.g.
LinearSum([a, b, c])
does not create additional objects).
This class is immutable.
1158 def __init__(self, iterable: Iterable[LinearTypes]) -> None: 1159 """Creates a LinearSum object. A copy of iterable is saved as a tuple.""" 1160 1161 self._elements = tuple(iterable) 1162 for item in self._elements: 1163 if not isinstance(item, (LinearBase, int, float)): 1164 raise TypeError( 1165 "unsupported type in iterable argument for " 1166 f"LinearSum: {type(item).__name__!r}" 1167 )
Creates a LinearSum object. A copy of iterable is saved as a tuple.
1195class QuadraticSum(QuadraticBase): 1196 # TODO(b/216492143): consider what details to move elsewhere and/or replace 1197 # by examples, and do complexity analysis. 1198 """A deferred sum of QuadraticTypes objects. 1199 1200 QuadraticSum objects are automatically created when a quadratic object is 1201 added to quadratic or linear objects and, as has performance optimizations 1202 similar to LinearSum. 1203 1204 This class is immutable. 1205 """ 1206 1207 __slots__ = "__weakref__", "_elements" 1208 1209 # Potentially unsafe use of Iterable argument is handled by immediate local 1210 # storage as tuple. 1211 def __init__(self, iterable: Iterable[QuadraticTypes]) -> None: 1212 """Creates a QuadraticSum object. A copy of iterable is saved as a tuple.""" 1213 1214 self._elements = tuple(iterable) 1215 for item in self._elements: 1216 if not isinstance(item, (LinearBase, QuadraticBase, int, float)): 1217 raise TypeError( 1218 "unsupported type in iterable argument for " 1219 f"QuadraticSum: {type(item).__name__!r}" 1220 ) 1221 1222 @property 1223 def elements(self) -> Tuple[QuadraticTypes, ...]: 1224 return self._elements 1225 1226 def _quadratic_flatten_once_and_add_to( 1227 self, 1228 scale: float, 1229 processed_elements: _QuadraticProcessedElements, 1230 target_stack: _QuadraticToProcessElements, 1231 ) -> None: 1232 for term in self._elements: 1233 if isinstance(term, (int, float)): 1234 processed_elements.offset += scale * float(term) 1235 else: 1236 target_stack.append(term, scale) 1237 1238 def __str__(self): 1239 return str(as_flat_quadratic_expression(self)) 1240 1241 def __repr__(self): 1242 result = "QuadraticSum((" 1243 result += ", ".join(repr(element) for element in self._elements) 1244 result += "))" 1245 return result
A deferred sum of QuadraticTypes objects.
QuadraticSum objects are automatically created when a quadratic object is added to quadratic or linear objects and, as has performance optimizations similar to LinearSum.
This class is immutable.
1211 def __init__(self, iterable: Iterable[QuadraticTypes]) -> None: 1212 """Creates a QuadraticSum object. A copy of iterable is saved as a tuple.""" 1213 1214 self._elements = tuple(iterable) 1215 for item in self._elements: 1216 if not isinstance(item, (LinearBase, QuadraticBase, int, float)): 1217 raise TypeError( 1218 "unsupported type in iterable argument for " 1219 f"QuadraticSum: {type(item).__name__!r}" 1220 )
Creates a QuadraticSum object. A copy of iterable is saved as a tuple.
1248class LinearProduct(LinearBase): 1249 """A deferred multiplication computation for linear expressions. 1250 1251 This class is immutable. 1252 """ 1253 1254 __slots__ = "_scalar", "_linear" 1255 1256 def __init__(self, scalar: float, linear: LinearBase) -> None: 1257 if not isinstance(scalar, (float, int)): 1258 raise TypeError( 1259 "unsupported type for scalar argument in " 1260 f"LinearProduct: {type(scalar).__name__!r}" 1261 ) 1262 if not isinstance(linear, LinearBase): 1263 raise TypeError( 1264 "unsupported type for linear argument in " 1265 f"LinearProduct: {type(linear).__name__!r}" 1266 ) 1267 self._scalar: float = float(scalar) 1268 self._linear: LinearBase = linear 1269 1270 @property 1271 def scalar(self) -> float: 1272 return self._scalar 1273 1274 @property 1275 def linear(self) -> LinearBase: 1276 return self._linear 1277 1278 def _flatten_once_and_add_to( 1279 self, 1280 scale: float, 1281 processed_elements: _ProcessedElements, 1282 target_stack: _ToProcessElements, 1283 ) -> None: 1284 target_stack.append(self._linear, self._scalar * scale) 1285 1286 def __str__(self): 1287 return str(as_flat_linear_expression(self)) 1288 1289 def __repr__(self): 1290 result = f"LinearProduct({self._scalar!r}, " 1291 result += f"{self._linear!r})" 1292 return result
A deferred multiplication computation for linear expressions.
This class is immutable.
1256 def __init__(self, scalar: float, linear: LinearBase) -> None: 1257 if not isinstance(scalar, (float, int)): 1258 raise TypeError( 1259 "unsupported type for scalar argument in " 1260 f"LinearProduct: {type(scalar).__name__!r}" 1261 ) 1262 if not isinstance(linear, LinearBase): 1263 raise TypeError( 1264 "unsupported type for linear argument in " 1265 f"LinearProduct: {type(linear).__name__!r}" 1266 ) 1267 self._scalar: float = float(scalar) 1268 self._linear: LinearBase = linear
1295class QuadraticProduct(QuadraticBase): 1296 """A deferred multiplication computation for quadratic expressions. 1297 1298 This class is immutable. 1299 """ 1300 1301 __slots__ = "_scalar", "_quadratic" 1302 1303 def __init__(self, scalar: float, quadratic: QuadraticBase) -> None: 1304 if not isinstance(scalar, (float, int)): 1305 raise TypeError( 1306 "unsupported type for scalar argument in " 1307 f"QuadraticProduct: {type(scalar).__name__!r}" 1308 ) 1309 if not isinstance(quadratic, QuadraticBase): 1310 raise TypeError( 1311 "unsupported type for linear argument in " 1312 f"QuadraticProduct: {type(quadratic).__name__!r}" 1313 ) 1314 self._scalar: float = float(scalar) 1315 self._quadratic: QuadraticBase = quadratic 1316 1317 @property 1318 def scalar(self) -> float: 1319 return self._scalar 1320 1321 @property 1322 def quadratic(self) -> QuadraticBase: 1323 return self._quadratic 1324 1325 def _quadratic_flatten_once_and_add_to( 1326 self, 1327 scale: float, 1328 processed_elements: _QuadraticProcessedElements, 1329 target_stack: _QuadraticToProcessElements, 1330 ) -> None: 1331 target_stack.append(self._quadratic, self._scalar * scale) 1332 1333 def __str__(self): 1334 return str(as_flat_quadratic_expression(self)) 1335 1336 def __repr__(self): 1337 return f"QuadraticProduct({self._scalar}, {self._quadratic!r})"
A deferred multiplication computation for quadratic expressions.
This class is immutable.
1303 def __init__(self, scalar: float, quadratic: QuadraticBase) -> None: 1304 if not isinstance(scalar, (float, int)): 1305 raise TypeError( 1306 "unsupported type for scalar argument in " 1307 f"QuadraticProduct: {type(scalar).__name__!r}" 1308 ) 1309 if not isinstance(quadratic, QuadraticBase): 1310 raise TypeError( 1311 "unsupported type for linear argument in " 1312 f"QuadraticProduct: {type(quadratic).__name__!r}" 1313 ) 1314 self._scalar: float = float(scalar) 1315 self._quadratic: QuadraticBase = quadratic
1340class LinearLinearProduct(QuadraticBase): 1341 """A deferred multiplication of two linear expressions. 1342 1343 This class is immutable. 1344 """ 1345 1346 __slots__ = "_first_linear", "_second_linear" 1347 1348 def __init__(self, first_linear: LinearBase, second_linear: LinearBase) -> None: 1349 if not isinstance(first_linear, LinearBase): 1350 raise TypeError( 1351 "unsupported type for first_linear argument in " 1352 f"LinearLinearProduct: {type(first_linear).__name__!r}" 1353 ) 1354 if not isinstance(second_linear, LinearBase): 1355 raise TypeError( 1356 "unsupported type for second_linear argument in " 1357 f"LinearLinearProduct: {type(second_linear).__name__!r}" 1358 ) 1359 self._first_linear: LinearBase = first_linear 1360 self._second_linear: LinearBase = second_linear 1361 1362 @property 1363 def first_linear(self) -> LinearBase: 1364 return self._first_linear 1365 1366 @property 1367 def second_linear(self) -> LinearBase: 1368 return self._second_linear 1369 1370 def _quadratic_flatten_once_and_add_to( 1371 self, 1372 scale: float, 1373 processed_elements: _QuadraticProcessedElements, 1374 target_stack: _QuadraticToProcessElements, 1375 ) -> None: 1376 # A recursion is avoided here because as_flat_linear_expression() must never 1377 # call _quadratic_flatten_once_and_add_to(). 1378 first_expression = as_flat_linear_expression(self._first_linear) 1379 second_expression = as_flat_linear_expression(self._second_linear) 1380 processed_elements.offset += ( 1381 first_expression.offset * second_expression.offset * scale 1382 ) 1383 for first_var, first_val in first_expression.terms.items(): 1384 processed_elements.terms[first_var] += ( 1385 second_expression.offset * first_val * scale 1386 ) 1387 for second_var, second_val in second_expression.terms.items(): 1388 processed_elements.terms[second_var] += ( 1389 first_expression.offset * second_val * scale 1390 ) 1391 1392 for first_var, first_val in first_expression.terms.items(): 1393 for second_var, second_val in second_expression.terms.items(): 1394 processed_elements.quadratic_terms[ 1395 QuadraticTermKey(first_var, second_var) 1396 ] += (first_val * second_val * scale) 1397 1398 def __str__(self): 1399 return str(as_flat_quadratic_expression(self)) 1400 1401 def __repr__(self): 1402 result = "LinearLinearProduct(" 1403 result += f"{self._first_linear!r}, " 1404 result += f"{self._second_linear!r})" 1405 return result
A deferred multiplication of two linear expressions.
This class is immutable.
1348 def __init__(self, first_linear: LinearBase, second_linear: LinearBase) -> None: 1349 if not isinstance(first_linear, LinearBase): 1350 raise TypeError( 1351 "unsupported type for first_linear argument in " 1352 f"LinearLinearProduct: {type(first_linear).__name__!r}" 1353 ) 1354 if not isinstance(second_linear, LinearBase): 1355 raise TypeError( 1356 "unsupported type for second_linear argument in " 1357 f"LinearLinearProduct: {type(second_linear).__name__!r}" 1358 ) 1359 self._first_linear: LinearBase = first_linear 1360 self._second_linear: LinearBase = second_linear
1408def as_flat_linear_expression(value: LinearTypes) -> LinearExpression: 1409 """Converts floats, ints and Linear objects to a LinearExpression.""" 1410 if isinstance(value, LinearExpression): 1411 return value 1412 return LinearExpression(value)
Converts floats, ints and Linear objects to a LinearExpression.
1415def as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression: 1416 """Converts floats, ints, LinearBase and QuadraticBase objects to a QuadraticExpression.""" 1417 if isinstance(value, QuadraticExpression): 1418 return value 1419 return QuadraticExpression(value)
Converts floats, ints, LinearBase and QuadraticBase objects to a QuadraticExpression.