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