ortools.math_opt.python.model
A solver independent library for modeling optimization problems.
Example use to model the optimization problem:
max 2.0 * x + y s.t. x + y <= 1.5 x in {0.0, 1.0} y in [0.0, 2.5]
model = mathopt.Model(name='my_model') x = model.add_binary_variable(name='x') y = model.add_variable(lb=0.0, ub=2.5, name='y')
We can directly use linear combinations of variables ...
model.add_linear_constraint(x + y <= 1.5, name='c')
... or build them incrementally.
objective_expression = 0 objective_expression += 2 * x objective_expression += y model.maximize(objective_expression)
May raise a RuntimeError on invalid input or internal solver errors.
result = mathopt.solve(model, mathopt.SolverType.GSCIP)
if result.termination.reason not in (mathopt.TerminationReason.OPTIMAL, mathopt.TerminationReason.FEASIBLE): raise RuntimeError(f'model failed to solve: {result.termination}')
print(f'Objective value: {result.objective_value()}') print(f'Value for variable x: {result.variable_values()[x]}')
1# Copyright 2010-2024 Google LLC 2# Licensed under the Apache License, Version 2.0 (the "License"); 3# you may not use this file except in compliance with the License. 4# You may obtain a copy of the License at 5# 6# http://www.apache.org/licenses/LICENSE-2.0 7# 8# Unless required by applicable law or agreed to in writing, software 9# distributed under the License is distributed on an "AS IS" BASIS, 10# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11# See the License for the specific language governing permissions and 12# limitations under the License. 13 14"""A solver independent library for modeling optimization problems. 15 16Example use to model the optimization problem: 17 max 2.0 * x + y 18 s.t. x + y <= 1.5 19 x in {0.0, 1.0} 20 y in [0.0, 2.5] 21 22 model = mathopt.Model(name='my_model') 23 x = model.add_binary_variable(name='x') 24 y = model.add_variable(lb=0.0, ub=2.5, name='y') 25 # We can directly use linear combinations of variables ... 26 model.add_linear_constraint(x + y <= 1.5, name='c') 27 # ... or build them incrementally. 28 objective_expression = 0 29 objective_expression += 2 * x 30 objective_expression += y 31 model.maximize(objective_expression) 32 33 # May raise a RuntimeError on invalid input or internal solver errors. 34 result = mathopt.solve(model, mathopt.SolverType.GSCIP) 35 36 if result.termination.reason not in (mathopt.TerminationReason.OPTIMAL, 37 mathopt.TerminationReason.FEASIBLE): 38 raise RuntimeError(f'model failed to solve: {result.termination}') 39 40 print(f'Objective value: {result.objective_value()}') 41 print(f'Value for variable x: {result.variable_values()[x]}') 42""" 43 44import abc 45import collections 46import dataclasses 47import math 48import typing 49from typing import ( 50 Any, 51 DefaultDict, 52 Deque, 53 Generic, 54 Iterable, 55 Iterator, 56 Mapping, 57 NamedTuple, 58 NoReturn, 59 Optional, 60 Protocol, 61 Tuple, 62 Type, 63 TypeVar, 64 Union, 65) 66import weakref 67 68import immutabledict 69 70from ortools.math_opt import model_pb2 71from ortools.math_opt import model_update_pb2 72from ortools.math_opt.python import hash_model_storage 73from ortools.math_opt.python import model_storage 74 75Storage = model_storage.ModelStorage 76StorageClass = model_storage.ModelStorageImplClass 77 78LinearTypes = Union[int, float, "LinearBase"] 79QuadraticTypes = Union[int, float, "LinearBase", "QuadraticBase"] 80LinearTypesExceptVariable = Union[ 81 float, int, "LinearTerm", "LinearExpression", "LinearSum", "LinearProduct" 82] 83 84_CHAINED_COMPARISON_MESSAGE = ( 85 "If you were trying to create a two-sided or " 86 "ranged linear inequality of the form `lb <= " 87 "expr <= ub`, try `(lb <= expr) <= ub` instead" 88) 89_EXPRESSION_COMP_EXPRESSION_MESSAGE = ( 90 "This error can occur when adding " 91 "inequalities of the form `(a <= b) <= " 92 "c` where (a, b, c) includes two or more" 93 " non-constant linear expressions" 94) 95 96 97def _raise_binary_operator_type_error( 98 operator: str, 99 lhs: Type[Any], 100 rhs: Type[Any], 101 extra_message: Optional[str] = None, 102) -> NoReturn: 103 """Raises TypeError on unsupported operators.""" 104 message = ( 105 f"unsupported operand type(s) for {operator}: {lhs.__name__!r} and" 106 f" {rhs.__name__!r}" 107 ) 108 if extra_message is not None: 109 message += "\n" + extra_message 110 raise TypeError(message) 111 112 113def _raise_ne_not_supported() -> NoReturn: 114 raise TypeError("!= constraints are not supported") 115 116 117class UpperBoundedLinearExpression: 118 """An inequality of the form expression <= upper_bound. 119 120 Where: 121 * expression is a linear expression, and 122 * upper_bound is a float 123 """ 124 125 __slots__ = "_expression", "_upper_bound" 126 127 def __init__(self, expression: "LinearBase", upper_bound: float) -> None: 128 """Operator overloading can be used instead: e.g. `x + y <= 2.0`.""" 129 self._expression: "LinearBase" = expression 130 self._upper_bound: float = upper_bound 131 132 @property 133 def expression(self) -> "LinearBase": 134 return self._expression 135 136 @property 137 def upper_bound(self) -> float: 138 return self._upper_bound 139 140 def __ge__(self, lhs: float) -> "BoundedLinearExpression": 141 if isinstance(lhs, (int, float)): 142 return BoundedLinearExpression(lhs, self.expression, self.upper_bound) 143 _raise_binary_operator_type_error(">=", type(self), type(lhs)) 144 145 def __bool__(self) -> bool: 146 raise TypeError( 147 "__bool__ is unsupported for UpperBoundedLinearExpression" 148 + "\n" 149 + _CHAINED_COMPARISON_MESSAGE 150 ) 151 152 def __str__(self): 153 return f"{self._expression!s} <= {self._upper_bound}" 154 155 def __repr__(self): 156 return f"{self._expression!r} <= {self._upper_bound}" 157 158 159class LowerBoundedLinearExpression: 160 """An inequality of the form expression >= lower_bound. 161 162 Where: 163 * expression is a linear expression, and 164 * lower_bound is a float 165 """ 166 167 __slots__ = "_expression", "_lower_bound" 168 169 def __init__(self, expression: "LinearBase", lower_bound: float) -> None: 170 """Operator overloading can be used instead: e.g. `x + y >= 2.0`.""" 171 self._expression: "LinearBase" = expression 172 self._lower_bound: float = lower_bound 173 174 @property 175 def expression(self) -> "LinearBase": 176 return self._expression 177 178 @property 179 def lower_bound(self) -> float: 180 return self._lower_bound 181 182 def __le__(self, rhs: float) -> "BoundedLinearExpression": 183 if isinstance(rhs, (int, float)): 184 return BoundedLinearExpression(self.lower_bound, self.expression, rhs) 185 _raise_binary_operator_type_error("<=", type(self), type(rhs)) 186 187 def __bool__(self) -> bool: 188 raise TypeError( 189 "__bool__ is unsupported for LowerBoundedLinearExpression" 190 + "\n" 191 + _CHAINED_COMPARISON_MESSAGE 192 ) 193 194 def __str__(self): 195 return f"{self._expression!s} >= {self._lower_bound}" 196 197 def __repr__(self): 198 return f"{self._expression!r} >= {self._lower_bound}" 199 200 201class BoundedLinearExpression: 202 """An inequality of the form lower_bound <= expression <= upper_bound. 203 204 Where: 205 * expression is a linear expression 206 * lower_bound is a float, and 207 * upper_bound is a float 208 209 Note: Because of limitations related to Python's handling of chained 210 comparisons, bounded expressions cannot be directly created usign 211 overloaded comparisons as in `lower_bound <= expression <= upper_bound`. 212 One solution is to wrap one of the inequalities in parenthesis as in 213 `(lower_bound <= expression) <= upper_bound`. 214 """ 215 216 __slots__ = "_expression", "_lower_bound", "_upper_bound" 217 218 def __init__( 219 self, lower_bound: float, expression: "LinearBase", upper_bound: float 220 ) -> None: 221 self._expression: "LinearBase" = expression 222 self._lower_bound: float = lower_bound 223 self._upper_bound: float = upper_bound 224 225 @property 226 def expression(self) -> "LinearBase": 227 return self._expression 228 229 @property 230 def lower_bound(self) -> float: 231 return self._lower_bound 232 233 @property 234 def upper_bound(self) -> float: 235 return self._upper_bound 236 237 def __bool__(self) -> bool: 238 raise TypeError( 239 "__bool__ is unsupported for BoundedLinearExpression" 240 + "\n" 241 + _CHAINED_COMPARISON_MESSAGE 242 ) 243 244 def __str__(self): 245 return f"{self._lower_bound} <= {self._expression!s} <= {self._upper_bound}" 246 247 def __repr__(self): 248 return f"{self._lower_bound} <= {self._expression!r} <= {self._upper_bound}" 249 250 251class VarEqVar: 252 """The result of the equality comparison between two Variable. 253 254 We use an object here to delay the evaluation of equality so that we can use 255 the operator== in two use-cases: 256 257 1. when the user want to test that two Variable values references the same 258 variable. This is supported by having this object support implicit 259 conversion to bool. 260 261 2. when the user want to use the equality to create a constraint of equality 262 between two variables. 263 """ 264 265 __slots__ = "_first_variable", "_second_variable" 266 267 def __init__( 268 self, 269 first_variable: "Variable", 270 second_variable: "Variable", 271 ) -> None: 272 self._first_variable: "Variable" = first_variable 273 self._second_variable: "Variable" = second_variable 274 275 @property 276 def first_variable(self) -> "Variable": 277 return self._first_variable 278 279 @property 280 def second_variable(self) -> "Variable": 281 return self._second_variable 282 283 def __bool__(self) -> bool: 284 return ( 285 self._first_variable.model is self._second_variable.model 286 and self._first_variable.id == self._second_variable.id 287 ) 288 289 def __str__(self): 290 return f"{self.first_variable!s} == {self._second_variable!s}" 291 292 def __repr__(self): 293 return f"{self.first_variable!r} == {self._second_variable!r}" 294 295 296BoundedLinearTypesList = ( 297 LowerBoundedLinearExpression, 298 UpperBoundedLinearExpression, 299 BoundedLinearExpression, 300 VarEqVar, 301) 302BoundedLinearTypes = Union[BoundedLinearTypesList] 303 304 305# TODO(b/231426528): consider using a frozen dataclass. 306class QuadraticTermKey: 307 """An id-ordered pair of variables used as a key for quadratic terms.""" 308 309 __slots__ = "_first_var", "_second_var" 310 311 def __init__(self, a: "Variable", b: "Variable"): 312 """Variables a and b will be ordered internally by their ids.""" 313 self._first_var: "Variable" = a 314 self._second_var: "Variable" = b 315 if self._first_var.id > self._second_var.id: 316 self._first_var, self._second_var = self._second_var, self._first_var 317 318 @property 319 def first_var(self) -> "Variable": 320 return self._first_var 321 322 @property 323 def second_var(self) -> "Variable": 324 return self._second_var 325 326 def __eq__(self, other: "QuadraticTermKey") -> bool: 327 return bool( 328 self._first_var == other._first_var 329 and self._second_var == other._second_var 330 ) 331 332 def __hash__(self) -> int: 333 return hash((self._first_var, self._second_var)) 334 335 def __str__(self): 336 return f"{self._first_var!s} * {self._second_var!s}" 337 338 def __repr__(self): 339 return f"QuadraticTermKey({self._first_var!r}, {self._second_var!r})" 340 341 342@dataclasses.dataclass 343class _ProcessedElements: 344 """Auxiliary data class for LinearBase._flatten_once_and_add_to().""" 345 346 terms: DefaultDict["Variable", float] = dataclasses.field( 347 default_factory=lambda: collections.defaultdict(float) 348 ) 349 offset: float = 0.0 350 351 352@dataclasses.dataclass 353class _QuadraticProcessedElements(_ProcessedElements): 354 """Auxiliary data class for QuadraticBase._quadratic_flatten_once_and_add_to().""" 355 356 quadratic_terms: DefaultDict["QuadraticTermKey", float] = dataclasses.field( 357 default_factory=lambda: collections.defaultdict(float) 358 ) 359 360 361class _ToProcessElements(Protocol): 362 """Auxiliary to-process stack interface for LinearBase._flatten_once_and_add_to() and QuadraticBase._quadratic_flatten_once_and_add_to().""" 363 364 __slots__ = () 365 366 def append(self, term: "LinearBase", scale: float) -> None: 367 """Add a linear object and scale to the to-process stack.""" 368 369 370_T = TypeVar("_T", "LinearBase", Union["LinearBase", "QuadraticBase"]) 371 372 373class _ToProcessElementsImplementation(Generic[_T]): 374 """Auxiliary data class for LinearBase._flatten_once_and_add_to().""" 375 376 __slots__ = ("_queue",) 377 378 def __init__(self, term: _T, scale: float) -> None: 379 self._queue: Deque[Tuple[_T, float]] = collections.deque([(term, scale)]) 380 381 def append(self, term: _T, scale: float) -> None: 382 self._queue.append((term, scale)) 383 384 def pop(self) -> Tuple[_T, float]: 385 return self._queue.popleft() 386 387 def __bool__(self) -> bool: 388 return bool(self._queue) 389 390 391_LinearToProcessElements = _ToProcessElementsImplementation["LinearBase"] 392_QuadraticToProcessElements = _ToProcessElementsImplementation[ 393 Union["LinearBase", "QuadraticBase"] 394] 395 396 397class LinearBase(metaclass=abc.ABCMeta): 398 """Interface for types that can build linear expressions with +, -, * and /. 399 400 Classes derived from LinearBase (plus float and int scalars) are used to 401 build expression trees describing a linear expression. Operations nodes of the 402 expression tree include: 403 404 * LinearSum: describes a deferred sum of LinearTypes objects. 405 * LinearProduct: describes a deferred product of a scalar and a 406 LinearTypes object. 407 408 Leaf nodes of the expression tree include: 409 410 * float and int scalars. 411 * Variable: a single variable. 412 * LinearTerm: the product of a scalar and a Variable object. 413 * LinearExpression: the sum of a scalar and LinearTerm objects. 414 415 LinearBase objects/expression-trees can be used directly to create 416 constraints or objective functions. However, to facilitate their inspection, 417 any LinearTypes object can be flattened to a LinearExpression 418 through: 419 420 as_flat_linear_expression(value: LinearTypes) -> LinearExpression: 421 422 In addition, all LinearBase objects are immutable. 423 424 Performance notes: 425 426 Using an expression tree representation instead of an eager construction of 427 LinearExpression objects reduces known inefficiencies associated with the 428 use of operator overloading to construct linear expressions. In particular, we 429 expect the runtime of as_flat_linear_expression() to be linear in the size of 430 the expression tree. Additional performance can gained by using LinearSum(c) 431 instead of sum(c) for a container c, as the latter creates len(c) LinearSum 432 objects. 433 """ 434 435 __slots__ = () 436 437 # TODO(b/216492143): explore requirements for this function so calculation of 438 # coefficients and offsets follow expected associativity rules (so approximate 439 # float calculations are as expected). 440 # TODO(b/216492143): add more details of what subclasses need to do in 441 # developers guide. 442 @abc.abstractmethod 443 def _flatten_once_and_add_to( 444 self, 445 scale: float, 446 processed_elements: _ProcessedElements, 447 target_stack: _ToProcessElements, 448 ) -> None: 449 """Flatten one level of tree if needed and add to targets. 450 451 Classes derived from LinearBase only need to implement this function 452 to enable transformation to LinearExpression through 453 as_flat_linear_expression(). 454 455 Args: 456 scale: multiply elements by this number when processing or adding to 457 stack. 458 processed_elements: where to add LinearTerms and scalars that can be 459 processed immediately. 460 target_stack: where to add LinearBase elements that are not scalars or 461 LinearTerms (i.e. elements that need further flattening). 462 Implementations should append() to this stack to avoid being recursive. 463 """ 464 465 def __eq__( 466 self, rhs: LinearTypes 467 ) -> ( 468 "BoundedLinearExpression" 469 ): # pytype: disable=signature-mismatch # overriding-return-type-checks 470 if isinstance(rhs, (int, float)): 471 return BoundedLinearExpression(rhs, self, rhs) 472 if not isinstance(rhs, LinearBase): 473 _raise_binary_operator_type_error("==", type(self), type(rhs)) 474 return BoundedLinearExpression(0.0, self - rhs, 0.0) 475 476 def __ne__( 477 self, rhs: LinearTypes 478 ) -> ( 479 NoReturn 480 ): # pytype: disable=signature-mismatch # overriding-return-type-checks 481 _raise_ne_not_supported() 482 483 @typing.overload 484 def __le__(self, rhs: float) -> "UpperBoundedLinearExpression": ... 485 486 @typing.overload 487 def __le__(self, rhs: "LinearBase") -> "BoundedLinearExpression": ... 488 489 @typing.overload 490 def __le__(self, rhs: "BoundedLinearExpression") -> NoReturn: ... 491 492 def __le__(self, rhs): 493 if isinstance(rhs, (int, float)): 494 return UpperBoundedLinearExpression(self, rhs) 495 if isinstance(rhs, LinearBase): 496 return BoundedLinearExpression(-math.inf, self - rhs, 0.0) 497 if isinstance(rhs, BoundedLinearExpression): 498 _raise_binary_operator_type_error( 499 "<=", type(self), type(rhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE 500 ) 501 _raise_binary_operator_type_error("<=", type(self), type(rhs)) 502 503 @typing.overload 504 def __ge__(self, lhs: float) -> "LowerBoundedLinearExpression": ... 505 506 @typing.overload 507 def __ge__(self, lhs: "LinearBase") -> "BoundedLinearExpression": ... 508 509 @typing.overload 510 def __ge__(self, lhs: "BoundedLinearExpression") -> NoReturn: ... 511 512 def __ge__(self, lhs): 513 if isinstance(lhs, (int, float)): 514 return LowerBoundedLinearExpression(self, lhs) 515 if isinstance(lhs, LinearBase): 516 return BoundedLinearExpression(0.0, self - lhs, math.inf) 517 if isinstance(lhs, BoundedLinearExpression): 518 _raise_binary_operator_type_error( 519 ">=", type(self), type(lhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE 520 ) 521 _raise_binary_operator_type_error(">=", type(self), type(lhs)) 522 523 def __add__(self, expr: LinearTypes) -> "LinearSum": 524 if not isinstance(expr, (int, float, LinearBase)): 525 return NotImplemented 526 return LinearSum((self, expr)) 527 528 def __radd__(self, expr: LinearTypes) -> "LinearSum": 529 if not isinstance(expr, (int, float, LinearBase)): 530 return NotImplemented 531 return LinearSum((expr, self)) 532 533 def __sub__(self, expr: LinearTypes) -> "LinearSum": 534 if not isinstance(expr, (int, float, LinearBase)): 535 return NotImplemented 536 return LinearSum((self, -expr)) 537 538 def __rsub__(self, expr: LinearTypes) -> "LinearSum": 539 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 540 return NotImplemented 541 return LinearSum((expr, -self)) 542 543 @typing.overload 544 def __mul__(self, other: float) -> "LinearProduct": ... 545 546 @typing.overload 547 def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ... 548 549 def __mul__(self, other): 550 if not isinstance(other, (int, float, LinearBase)): 551 return NotImplemented 552 if isinstance(other, LinearBase): 553 return LinearLinearProduct(self, other) 554 return LinearProduct(other, self) 555 556 def __rmul__(self, constant: float) -> "LinearProduct": 557 if not isinstance(constant, (int, float)): 558 return NotImplemented 559 return LinearProduct(constant, self) 560 561 # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. 562 def __truediv__(self, constant: float) -> "LinearProduct": 563 if not isinstance(constant, (int, float)): 564 return NotImplemented 565 return LinearProduct(1.0 / constant, self) 566 567 def __neg__(self) -> "LinearProduct": 568 return LinearProduct(-1.0, self) 569 570 571class QuadraticBase(metaclass=abc.ABCMeta): 572 """Interface for types that can build quadratic expressions with +, -, * and /. 573 574 Classes derived from QuadraticBase and LinearBase (plus float and int scalars) 575 are used to build expression trees describing a quadratic expression. 576 Operations nodes of the expression tree include: 577 578 * QuadraticSum: describes a deferred sum of QuadraticTypes objects. 579 * QuadraticProduct: describes a deferred product of a scalar and a 580 QuadraticTypes object. 581 * LinearLinearProduct: describes a deferred product of two LinearTypes 582 objects. 583 584 Leaf nodes of the expression tree include: 585 586 * float and int scalars. 587 * Variable: a single variable. 588 * LinearTerm: the product of a scalar and a Variable object. 589 * LinearExpression: the sum of a scalar and LinearTerm objects. 590 * QuadraticTerm: the product of a scalar and two Variable objects. 591 * QuadraticExpression: the sum of a scalar, LinearTerm objects and 592 QuadraticTerm objects. 593 594 QuadraticBase objects/expression-trees can be used directly to create 595 objective functions. However, to facilitate their inspection, any 596 QuadraticTypes object can be flattened to a QuadraticExpression 597 through: 598 599 as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression: 600 601 In addition, all QuadraticBase objects are immutable. 602 603 Performance notes: 604 605 Using an expression tree representation instead of an eager construction of 606 QuadraticExpression objects reduces known inefficiencies associated with the 607 use of operator overloading to construct quadratic expressions. In particular, 608 we expect the runtime of as_flat_quadratic_expression() to be linear in the 609 size of the expression tree. Additional performance can gained by using 610 QuadraticSum(c) instead of sum(c) for a container c, as the latter creates 611 len(c) QuadraticSum objects. 612 """ 613 614 __slots__ = () 615 616 # TODO(b/216492143): explore requirements for this function so calculation of 617 # coefficients and offsets follow expected associativity rules (so approximate 618 # float calculations are as expected). 619 # TODO(b/216492143): add more details of what subclasses need to do in 620 # developers guide. 621 @abc.abstractmethod 622 def _quadratic_flatten_once_and_add_to( 623 self, 624 scale: float, 625 processed_elements: _QuadraticProcessedElements, 626 target_stack: _QuadraticToProcessElements, 627 ) -> None: 628 """Flatten one level of tree if needed and add to targets. 629 630 Classes derived from QuadraticBase only need to implement this function 631 to enable transformation to QuadraticExpression through 632 as_flat_quadratic_expression(). 633 634 Args: 635 scale: multiply elements by this number when processing or adding to 636 stack. 637 processed_elements: where to add linear terms, quadratic terms and scalars 638 that can be processed immediately. 639 target_stack: where to add LinearBase and QuadraticBase elements that are 640 not scalars or linear terms or quadratic terms (i.e. elements that need 641 further flattening). Implementations should append() to this stack to 642 avoid being recursive. 643 """ 644 645 def __add__(self, expr: QuadraticTypes) -> "QuadraticSum": 646 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 647 return NotImplemented 648 return QuadraticSum((self, expr)) 649 650 def __radd__(self, expr: QuadraticTypes) -> "QuadraticSum": 651 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 652 return NotImplemented 653 return QuadraticSum((expr, self)) 654 655 def __sub__(self, expr: QuadraticTypes) -> "QuadraticSum": 656 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 657 return NotImplemented 658 return QuadraticSum((self, -expr)) 659 660 def __rsub__(self, expr: QuadraticTypes) -> "QuadraticSum": 661 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 662 return NotImplemented 663 return QuadraticSum((expr, -self)) 664 665 def __mul__(self, other: float) -> "QuadraticProduct": 666 if not isinstance(other, (int, float)): 667 return NotImplemented 668 return QuadraticProduct(other, self) 669 670 def __rmul__(self, other: float) -> "QuadraticProduct": 671 if not isinstance(other, (int, float)): 672 return NotImplemented 673 return QuadraticProduct(other, self) 674 675 # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. 676 def __truediv__(self, constant: float) -> "QuadraticProduct": 677 if not isinstance(constant, (int, float)): 678 return NotImplemented 679 return QuadraticProduct(1.0 / constant, self) 680 681 def __neg__(self) -> "QuadraticProduct": 682 return QuadraticProduct(-1.0, self) 683 684 685class Variable(LinearBase): 686 """A decision variable for an optimization model. 687 688 A decision variable takes a value from a domain, either the real numbers or 689 the integers, and restricted to be in some interval [lb, ub] (where lb and ub 690 can be infinite). The case of lb == ub is allowed, this means the variable 691 must take a single value. The case of lb > ub is also allowed, this implies 692 that the problem is infeasible. 693 694 A Variable is configured as follows: 695 * lower_bound: a float property, lb above. Should not be NaN nor +inf. 696 * upper_bound: a float property, ub above. Should not be NaN nor -inf. 697 * integer: a bool property, if the domain is integer or continuous. 698 699 The name is optional, read only, and used only for debugging. Non-empty names 700 should be distinct. 701 702 Every Variable is associated with a Model (defined below). Note that data 703 describing the variable (e.g. lower_bound) is owned by Model.storage, this 704 class is simply a reference to that data. Do not create a Variable directly, 705 use Model.add_variable() instead. 706 """ 707 708 __slots__ = "__weakref__", "_model", "_id" 709 710 def __init__(self, model: "Model", vid: int) -> None: 711 """Do not invoke directly, use Model.add_variable().""" 712 self._model: "Model" = model 713 self._id: int = vid 714 715 @property 716 def lower_bound(self) -> float: 717 return self.model.storage.get_variable_lb(self._id) 718 719 @lower_bound.setter 720 def lower_bound(self, value: float) -> None: 721 self.model.storage.set_variable_lb(self._id, value) 722 723 @property 724 def upper_bound(self) -> float: 725 return self.model.storage.get_variable_ub(self._id) 726 727 @upper_bound.setter 728 def upper_bound(self, value: float) -> None: 729 self.model.storage.set_variable_ub(self._id, value) 730 731 @property 732 def integer(self) -> bool: 733 return self.model.storage.get_variable_is_integer(self._id) 734 735 @integer.setter 736 def integer(self, value: bool) -> None: 737 self.model.storage.set_variable_is_integer(self._id, value) 738 739 @property 740 def name(self) -> str: 741 return self.model.storage.get_variable_name(self._id) 742 743 @property 744 def id(self) -> int: 745 return self._id 746 747 @property 748 def model(self) -> "Model": 749 return self._model 750 751 def __str__(self): 752 """Returns the name, or a string containing the id if the name is empty.""" 753 return self.name if self.name else f"variable_{self.id}" 754 755 def __repr__(self): 756 return f"<Variable id: {self.id}, name: {self.name!r}>" 757 758 @typing.overload 759 def __eq__(self, rhs: "Variable") -> "VarEqVar": ... 760 761 @typing.overload 762 def __eq__(self, rhs: LinearTypesExceptVariable) -> "BoundedLinearExpression": ... 763 764 def __eq__(self, rhs): 765 if isinstance(rhs, Variable): 766 return VarEqVar(self, rhs) 767 return super().__eq__(rhs) 768 769 @typing.overload 770 def __ne__(self, rhs: "Variable") -> bool: ... 771 772 @typing.overload 773 def __ne__(self, rhs: LinearTypesExceptVariable) -> NoReturn: ... 774 775 def __ne__(self, rhs): 776 if isinstance(rhs, Variable): 777 return not self == rhs 778 _raise_ne_not_supported() 779 780 def __hash__(self) -> int: 781 return hash((self.model, self.id)) 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(QuadraticTermKey(self, other), 1.0) 797 if isinstance(other, LinearTerm): 798 return QuadraticTerm( 799 QuadraticTermKey(self, other.variable), other.coefficient 800 ) 801 if isinstance(other, LinearBase): 802 return LinearLinearProduct(self, other) # pytype: disable=bad-return-type 803 return LinearTerm(self, other) 804 805 def __rmul__(self, constant: float) -> "LinearTerm": 806 if not isinstance(constant, (int, float)): 807 return NotImplemented 808 return LinearTerm(self, constant) 809 810 # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. 811 def __truediv__(self, constant: float) -> "LinearTerm": 812 if not isinstance(constant, (int, float)): 813 return NotImplemented 814 return LinearTerm(self, 1.0 / constant) 815 816 def __neg__(self) -> "LinearTerm": 817 return LinearTerm(self, -1.0) 818 819 def _flatten_once_and_add_to( 820 self, 821 scale: float, 822 processed_elements: _ProcessedElements, 823 target_stack: _ToProcessElements, 824 ) -> None: 825 processed_elements.terms[self] += scale 826 827 828class LinearTerm(LinearBase): 829 """The product of a scalar and a variable. 830 831 This class is immutable. 832 """ 833 834 __slots__ = "_variable", "_coefficient" 835 836 def __init__(self, variable: Variable, coefficient: float) -> None: 837 self._variable: Variable = variable 838 self._coefficient: float = coefficient 839 840 @property 841 def variable(self) -> Variable: 842 return self._variable 843 844 @property 845 def coefficient(self) -> float: 846 return self._coefficient 847 848 def _flatten_once_and_add_to( 849 self, 850 scale: float, 851 processed_elements: _ProcessedElements, 852 target_stack: _ToProcessElements, 853 ) -> None: 854 processed_elements.terms[self._variable] += self._coefficient * scale 855 856 @typing.overload 857 def __mul__(self, other: float) -> "LinearTerm": ... 858 859 @typing.overload 860 def __mul__(self, other: Union["Variable", "LinearTerm"]) -> "QuadraticTerm": ... 861 862 @typing.overload 863 def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ... 864 865 def __mul__(self, other): 866 if not isinstance(other, (int, float, LinearBase)): 867 return NotImplemented 868 if isinstance(other, Variable): 869 return QuadraticTerm( 870 QuadraticTermKey(self._variable, other), self._coefficient 871 ) 872 if isinstance(other, LinearTerm): 873 return QuadraticTerm( 874 QuadraticTermKey(self.variable, other.variable), 875 self._coefficient * other.coefficient, 876 ) 877 if isinstance(other, LinearBase): 878 return LinearLinearProduct(self, other) # pytype: disable=bad-return-type 879 return LinearTerm(self._variable, self._coefficient * other) 880 881 def __rmul__(self, constant: float) -> "LinearTerm": 882 if not isinstance(constant, (int, float)): 883 return NotImplemented 884 return LinearTerm(self._variable, self._coefficient * constant) 885 886 def __truediv__(self, constant: float) -> "LinearTerm": 887 if not isinstance(constant, (int, float)): 888 return NotImplemented 889 return LinearTerm(self._variable, self._coefficient / constant) 890 891 def __neg__(self) -> "LinearTerm": 892 return LinearTerm(self._variable, -self._coefficient) 893 894 def __str__(self): 895 return f"{self._coefficient} * {self._variable}" 896 897 def __repr__(self): 898 return f"LinearTerm({self._variable!r}, {self._coefficient!r})" 899 900 901class QuadraticTerm(QuadraticBase): 902 """The product of a scalar and two variables. 903 904 This class is immutable. 905 """ 906 907 __slots__ = "_key", "_coefficient" 908 909 def __init__(self, key: QuadraticTermKey, coefficient: float) -> None: 910 self._key: QuadraticTermKey = key 911 self._coefficient: float = coefficient 912 913 @property 914 def key(self) -> QuadraticTermKey: 915 return self._key 916 917 @property 918 def coefficient(self) -> float: 919 return self._coefficient 920 921 def _quadratic_flatten_once_and_add_to( 922 self, 923 scale: float, 924 processed_elements: _QuadraticProcessedElements, 925 target_stack: _ToProcessElements, 926 ) -> None: 927 processed_elements.quadratic_terms[self._key] += self._coefficient * scale 928 929 def __mul__(self, constant: float) -> "QuadraticTerm": 930 if not isinstance(constant, (int, float)): 931 return NotImplemented 932 return QuadraticTerm(self._key, self._coefficient * constant) 933 934 def __rmul__(self, constant: float) -> "QuadraticTerm": 935 if not isinstance(constant, (int, float)): 936 return NotImplemented 937 return QuadraticTerm(self._key, self._coefficient * constant) 938 939 def __truediv__(self, constant: float) -> "QuadraticTerm": 940 if not isinstance(constant, (int, float)): 941 return NotImplemented 942 return QuadraticTerm(self._key, self._coefficient / constant) 943 944 def __neg__(self) -> "QuadraticTerm": 945 return QuadraticTerm(self._key, -self._coefficient) 946 947 def __str__(self): 948 return f"{self._coefficient} * {self._key!s}" 949 950 def __repr__(self): 951 return f"QuadraticTerm({self._key!r}, {self._coefficient})" 952 953 954class LinearExpression(LinearBase): 955 """For variables x, an expression: b + sum_{i in I} a_i * x_i. 956 957 This class is immutable. 958 """ 959 960 __slots__ = "__weakref__", "_terms", "_offset" 961 962 # TODO(b/216492143): consider initializing from a dictionary. 963 def __init__(self, /, other: LinearTypes = 0) -> None: 964 self._offset: float = 0.0 965 if isinstance(other, (int, float)): 966 self._offset = float(other) 967 self._terms: Mapping[Variable, float] = immutabledict.immutabledict() 968 return 969 970 to_process: _LinearToProcessElements = _LinearToProcessElements(other, 1.0) 971 processed_elements = _ProcessedElements() 972 while to_process: 973 linear, coef = to_process.pop() 974 linear._flatten_once_and_add_to(coef, processed_elements, to_process) 975 # TODO(b/216492143): explore avoiding this copy. 976 self._terms: Mapping[Variable, float] = immutabledict.immutabledict( 977 processed_elements.terms 978 ) 979 self._offset = processed_elements.offset 980 981 @property 982 def terms(self) -> Mapping[Variable, float]: 983 return self._terms 984 985 @property 986 def offset(self) -> float: 987 return self._offset 988 989 def evaluate(self, variable_values: Mapping[Variable, float]) -> float: 990 """Returns the value of this expression for given variable values. 991 992 E.g. if this is 3 * x + 4 and variable_values = {x: 2.0}, then 993 evaluate(variable_values) equals 10.0. 994 995 See also mathopt.evaluate_expression(), which works on any type in 996 QuadraticTypes. 997 998 Args: 999 variable_values: Must contain a value for every variable in expression. 1000 1001 Returns: 1002 The value of this expression when replacing variables by their value. 1003 """ 1004 result = self._offset 1005 for var, coef in sorted( 1006 self._terms.items(), key=lambda var_coef_pair: var_coef_pair[0].id 1007 ): 1008 result += coef * variable_values[var] 1009 return result 1010 1011 def _flatten_once_and_add_to( 1012 self, 1013 scale: float, 1014 processed_elements: _ProcessedElements, 1015 target_stack: _ToProcessElements, 1016 ) -> None: 1017 for var, val in self._terms.items(): 1018 processed_elements.terms[var] += val * scale 1019 processed_elements.offset += scale * self.offset 1020 1021 # TODO(b/216492143): change __str__ to match C++ implementation in 1022 # cl/421649402. 1023 def __str__(self): 1024 """Returns the name, or a string containing the id if the name is empty.""" 1025 result = str(self.offset) 1026 sorted_keys = sorted(self._terms.keys(), key=str) 1027 for variable in sorted_keys: 1028 # TODO(b/216492143): consider how to better deal with `NaN` and try to 1029 # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in 1030 # linear_expression_test.py. 1031 coefficient = self._terms[variable] 1032 if coefficient == 0.0: 1033 continue 1034 if coefficient > 0: 1035 result += " + " 1036 else: 1037 result += " - " 1038 result += str(abs(coefficient)) + " * " + str(variable) 1039 return result 1040 1041 def __repr__(self): 1042 result = f"LinearExpression({self.offset}, " + "{" 1043 result += ", ".join( 1044 f"{variable!r}: {coefficient}" 1045 for variable, coefficient in self._terms.items() 1046 ) 1047 result += "})" 1048 return result 1049 1050 1051class QuadraticExpression(QuadraticBase): 1052 """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. 1053 1054 This class is immutable. 1055 """ 1056 1057 __slots__ = "__weakref__", "_linear_terms", "_quadratic_terms", "_offset" 1058 1059 # TODO(b/216492143): consider initializing from a dictionary. 1060 def __init__(self, other: QuadraticTypes) -> None: 1061 self._offset: float = 0.0 1062 if isinstance(other, (int, float)): 1063 self._offset = float(other) 1064 self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict() 1065 self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( 1066 immutabledict.immutabledict() 1067 ) 1068 return 1069 1070 to_process: _QuadraticToProcessElements = _QuadraticToProcessElements( 1071 other, 1.0 1072 ) 1073 processed_elements = _QuadraticProcessedElements() 1074 while to_process: 1075 linear_or_quadratic, coef = to_process.pop() 1076 if isinstance(linear_or_quadratic, LinearBase): 1077 linear_or_quadratic._flatten_once_and_add_to( 1078 coef, processed_elements, to_process 1079 ) 1080 else: 1081 linear_or_quadratic._quadratic_flatten_once_and_add_to( 1082 coef, processed_elements, to_process 1083 ) 1084 # TODO(b/216492143): explore avoiding this copy. 1085 self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict( 1086 processed_elements.terms 1087 ) 1088 self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( 1089 immutabledict.immutabledict(processed_elements.quadratic_terms) 1090 ) 1091 self._offset = processed_elements.offset 1092 1093 @property 1094 def linear_terms(self) -> Mapping[Variable, float]: 1095 return self._linear_terms 1096 1097 @property 1098 def quadratic_terms(self) -> Mapping[QuadraticTermKey, float]: 1099 return self._quadratic_terms 1100 1101 @property 1102 def offset(self) -> float: 1103 return self._offset 1104 1105 def evaluate(self, variable_values: Mapping[Variable, float]) -> float: 1106 """Returns the value of this expression for given variable values. 1107 1108 E.g. if this is 3 * x * x + 4 and variable_values = {x: 2.0}, then 1109 evaluate(variable_values) equals 16.0. 1110 1111 See also mathopt.evaluate_expression(), which works on any type in 1112 QuadraticTypes. 1113 1114 Args: 1115 variable_values: Must contain a value for every variable in expression. 1116 1117 Returns: 1118 The value of this expression when replacing variables by their value. 1119 """ 1120 result = self._offset 1121 for var, coef in sorted( 1122 self._linear_terms.items(), 1123 key=lambda var_coef_pair: var_coef_pair[0].id, 1124 ): 1125 result += coef * variable_values[var] 1126 for key, coef in sorted( 1127 self._quadratic_terms.items(), 1128 key=lambda quad_coef_pair: ( 1129 quad_coef_pair[0].first_var.id, 1130 quad_coef_pair[0].second_var.id, 1131 ), 1132 ): 1133 result += ( 1134 coef * variable_values[key.first_var] * variable_values[key.second_var] 1135 ) 1136 return result 1137 1138 def _quadratic_flatten_once_and_add_to( 1139 self, 1140 scale: float, 1141 processed_elements: _QuadraticProcessedElements, 1142 target_stack: _QuadraticToProcessElements, 1143 ) -> None: 1144 for var, val in self._linear_terms.items(): 1145 processed_elements.terms[var] += val * scale 1146 for key, val in self._quadratic_terms.items(): 1147 processed_elements.quadratic_terms[key] += val * scale 1148 processed_elements.offset += scale * self.offset 1149 1150 # TODO(b/216492143): change __str__ to match C++ implementation in 1151 # cl/421649402. 1152 def __str__(self): 1153 result = str(self.offset) 1154 sorted_linear_keys = sorted(self._linear_terms.keys(), key=str) 1155 for variable in sorted_linear_keys: 1156 # TODO(b/216492143): consider how to better deal with `NaN` and try to 1157 # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in 1158 # linear_expression_test.py. 1159 coefficient = self._linear_terms[variable] 1160 if coefficient == 0.0: 1161 continue 1162 if coefficient > 0: 1163 result += " + " 1164 else: 1165 result += " - " 1166 result += str(abs(coefficient)) + " * " + str(variable) 1167 sorted_quadratic_keys = sorted(self._quadratic_terms.keys(), key=str) 1168 for key in sorted_quadratic_keys: 1169 # TODO(b/216492143): consider how to better deal with `NaN` and try to 1170 # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in 1171 # linear_expression_test.py. 1172 coefficient = self._quadratic_terms[key] 1173 if coefficient == 0.0: 1174 continue 1175 if coefficient > 0: 1176 result += " + " 1177 else: 1178 result += " - " 1179 result += str(abs(coefficient)) + " * " + str(key) 1180 return result 1181 1182 def __repr__(self): 1183 result = f"QuadraticExpression({self.offset}, " + "{" 1184 result += ", ".join( 1185 f"{variable!r}: {coefficient}" 1186 for variable, coefficient in self._linear_terms.items() 1187 ) 1188 result += "}, {" 1189 result += ", ".join( 1190 f"{key!r}: {coefficient}" 1191 for key, coefficient in self._quadratic_terms.items() 1192 ) 1193 result += "})" 1194 return result 1195 1196 1197class LinearConstraint: 1198 """A linear constraint for an optimization model. 1199 1200 A LinearConstraint adds the following restriction on feasible solutions to an 1201 optimization model: 1202 lb <= sum_{i in I} a_i * x_i <= ub 1203 where x_i are the decision variables of the problem. lb == ub is allowed, this 1204 models the equality constraint: 1205 sum_{i in I} a_i * x_i == b 1206 lb > ub is also allowed, but the optimization problem will be infeasible. 1207 1208 A LinearConstraint can be configured as follows: 1209 * lower_bound: a float property, lb above. Should not be NaN nor +inf. 1210 * upper_bound: a float property, ub above. Should not be NaN nor -inf. 1211 * set_coefficient() and get_coefficient(): get and set the a_i * x_i 1212 terms. The variable must be from the same model as this constraint, and 1213 the a_i must be finite and not NaN. The coefficient for any variable not 1214 set is 0.0, and setting a coefficient to 0.0 removes it from I above. 1215 1216 The name is optional, read only, and used only for debugging. Non-empty names 1217 should be distinct. 1218 1219 Every LinearConstraint is associated with a Model (defined below). Note that 1220 data describing the linear constraint (e.g. lower_bound) is owned by 1221 Model.storage, this class is simply a reference to that data. Do not create a 1222 LinearConstraint directly, use Model.add_linear_constraint() instead. 1223 """ 1224 1225 __slots__ = "__weakref__", "_model", "_id" 1226 1227 def __init__(self, model: "Model", cid: int) -> None: 1228 """Do not invoke directly, use Model.add_linear_constraint().""" 1229 self._model: "Model" = model 1230 self._id: int = cid 1231 1232 @property 1233 def lower_bound(self) -> float: 1234 return self.model.storage.get_linear_constraint_lb(self._id) 1235 1236 @lower_bound.setter 1237 def lower_bound(self, value: float) -> None: 1238 self.model.storage.set_linear_constraint_lb(self._id, value) 1239 1240 @property 1241 def upper_bound(self) -> float: 1242 return self.model.storage.get_linear_constraint_ub(self._id) 1243 1244 @upper_bound.setter 1245 def upper_bound(self, value: float) -> None: 1246 self.model.storage.set_linear_constraint_ub(self._id, value) 1247 1248 @property 1249 def name(self) -> str: 1250 return self.model.storage.get_linear_constraint_name(self._id) 1251 1252 @property 1253 def id(self) -> int: 1254 return self._id 1255 1256 @property 1257 def model(self) -> "Model": 1258 return self._model 1259 1260 def set_coefficient(self, variable: Variable, coefficient: float) -> None: 1261 self.model.check_compatible(variable) 1262 self.model.storage.set_linear_constraint_coefficient( 1263 self._id, variable.id, coefficient 1264 ) 1265 1266 def get_coefficient(self, variable: Variable) -> float: 1267 self.model.check_compatible(variable) 1268 return self.model.storage.get_linear_constraint_coefficient( 1269 self._id, variable.id 1270 ) 1271 1272 def terms(self) -> Iterator[LinearTerm]: 1273 """Yields the variable/coefficient pairs with nonzero coefficient for this linear constraint.""" 1274 for variable in self.model.row_nonzeros(self): 1275 yield LinearTerm( 1276 variable=variable, 1277 coefficient=self.model.storage.get_linear_constraint_coefficient( 1278 self._id, variable.id 1279 ), 1280 ) 1281 1282 def as_bounded_linear_expression(self) -> BoundedLinearExpression: 1283 """Returns the bounded expression from lower_bound, upper_bound and terms.""" 1284 return BoundedLinearExpression( 1285 self.lower_bound, LinearSum(self.terms()), self.upper_bound 1286 ) 1287 1288 def __str__(self): 1289 """Returns the name, or a string containing the id if the name is empty.""" 1290 return self.name if self.name else f"linear_constraint_{self.id}" 1291 1292 def __repr__(self): 1293 return f"<LinearConstraint id: {self.id}, name: {self.name!r}>" 1294 1295 1296class Objective: 1297 """The objective for an optimization model. 1298 1299 An objective is either of the form: 1300 min o + sum_{i in I} c_i * x_i + sum_{i, j in I, i <= j} q_i,j * x_i * x_j 1301 or 1302 max o + sum_{i in I} c_i * x_i + sum_{(i, j) in Q} q_i,j * x_i * x_j 1303 where x_i are the decision variables of the problem and where all pairs (i, j) 1304 in Q satisfy i <= j. The values of o, c_i and q_i,j should be finite and not 1305 NaN. 1306 1307 The objective can be configured as follows: 1308 * offset: a float property, o above. Should be finite and not NaN. 1309 * is_maximize: a bool property, if the objective is to maximize or minimize. 1310 * set_linear_coefficient and get_linear_coefficient control the c_i * x_i 1311 terms. The variables must be from the same model as this objective, and 1312 the c_i must be finite and not NaN. The coefficient for any variable not 1313 set is 0.0, and setting a coefficient to 0.0 removes it from I above. 1314 * set_quadratic_coefficient and get_quadratic_coefficient control the 1315 q_i,j * x_i * x_j terms. The variables must be from the same model as this 1316 objective, and the q_i,j must be finite and not NaN. The coefficient for 1317 any pair of variables not set is 0.0, and setting a coefficient to 0.0 1318 removes the associated (i,j) from Q above. 1319 1320 Every Objective is associated with a Model (defined below). Note that data 1321 describing the objective (e.g. offset) is owned by Model.storage, this class 1322 is simply a reference to that data. Do not create an Objective directly, 1323 use Model.objective to access the objective instead. 1324 1325 The objective will be linear if only linear coefficients are set. This can be 1326 useful to avoid solve-time errors with solvers that do not accept quadratic 1327 objectives. To facilitate this linear objective guarantee we provide three 1328 functions to add to the objective: 1329 * add(), which accepts linear or quadratic expressions, 1330 * add_quadratic(), which also accepts linear or quadratic expressions and 1331 can be used to signal a quadratic objective is possible, and 1332 * add_linear(), which only accepts linear expressions and can be used to 1333 guarantee the objective remains linear. 1334 """ 1335 1336 __slots__ = ("_model",) 1337 1338 def __init__(self, model: "Model") -> None: 1339 """Do no invoke directly, use Model.objective.""" 1340 self._model: "Model" = model 1341 1342 @property 1343 def is_maximize(self) -> bool: 1344 return self.model.storage.get_is_maximize() 1345 1346 @is_maximize.setter 1347 def is_maximize(self, is_maximize: bool) -> None: 1348 self.model.storage.set_is_maximize(is_maximize) 1349 1350 @property 1351 def offset(self) -> float: 1352 return self.model.storage.get_objective_offset() 1353 1354 @offset.setter 1355 def offset(self, value: float) -> None: 1356 self.model.storage.set_objective_offset(value) 1357 1358 @property 1359 def model(self) -> "Model": 1360 return self._model 1361 1362 def set_linear_coefficient(self, variable: Variable, coef: float) -> None: 1363 self.model.check_compatible(variable) 1364 self.model.storage.set_linear_objective_coefficient(variable.id, coef) 1365 1366 def get_linear_coefficient(self, variable: Variable) -> float: 1367 self.model.check_compatible(variable) 1368 return self.model.storage.get_linear_objective_coefficient(variable.id) 1369 1370 def linear_terms(self) -> Iterator[LinearTerm]: 1371 """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.""" 1372 yield from self.model.linear_objective_terms() 1373 1374 def add(self, objective: QuadraticTypes) -> None: 1375 """Adds the provided expression `objective` to the objective function. 1376 1377 To ensure the objective remains linear through type checks, use 1378 add_linear(). 1379 1380 Args: 1381 objective: the expression to add to the objective function. 1382 """ 1383 if isinstance(objective, (LinearBase, int, float)): 1384 self.add_linear(objective) 1385 elif isinstance(objective, QuadraticBase): 1386 self.add_quadratic(objective) 1387 else: 1388 raise TypeError( 1389 "unsupported type in objective argument for " 1390 f"Objective.add(): {type(objective).__name__!r}" 1391 ) 1392 1393 def add_linear(self, objective: LinearTypes) -> None: 1394 """Adds the provided linear expression `objective` to the objective function.""" 1395 if not isinstance(objective, (LinearBase, int, float)): 1396 raise TypeError( 1397 "unsupported type in objective argument for " 1398 f"Objective.add_linear(): {type(objective).__name__!r}" 1399 ) 1400 objective_expr = as_flat_linear_expression(objective) 1401 self.offset += objective_expr.offset 1402 for var, coefficient in objective_expr.terms.items(): 1403 self.set_linear_coefficient( 1404 var, self.get_linear_coefficient(var) + coefficient 1405 ) 1406 1407 def add_quadratic(self, objective: QuadraticTypes) -> None: 1408 """Adds the provided quadratic expression `objective` to the objective function.""" 1409 if not isinstance(objective, (QuadraticBase, LinearBase, int, float)): 1410 raise TypeError( 1411 "unsupported type in objective argument for " 1412 f"Objective.add(): {type(objective).__name__!r}" 1413 ) 1414 objective_expr = as_flat_quadratic_expression(objective) 1415 self.offset += objective_expr.offset 1416 for var, coefficient in objective_expr.linear_terms.items(): 1417 self.set_linear_coefficient( 1418 var, self.get_linear_coefficient(var) + coefficient 1419 ) 1420 for key, coefficient in objective_expr.quadratic_terms.items(): 1421 self.set_quadratic_coefficient( 1422 key.first_var, 1423 key.second_var, 1424 self.get_quadratic_coefficient(key.first_var, key.second_var) 1425 + coefficient, 1426 ) 1427 1428 def set_quadratic_coefficient( 1429 self, first_variable: Variable, second_variable: Variable, coef: float 1430 ) -> None: 1431 self.model.check_compatible(first_variable) 1432 self.model.check_compatible(second_variable) 1433 self.model.storage.set_quadratic_objective_coefficient( 1434 first_variable.id, second_variable.id, coef 1435 ) 1436 1437 def get_quadratic_coefficient( 1438 self, first_variable: Variable, second_variable: Variable 1439 ) -> float: 1440 self.model.check_compatible(first_variable) 1441 self.model.check_compatible(second_variable) 1442 return self.model.storage.get_quadratic_objective_coefficient( 1443 first_variable.id, second_variable.id 1444 ) 1445 1446 def quadratic_terms(self) -> Iterator[QuadraticTerm]: 1447 """Yields quadratic terms with nonzero objective coefficient in undefined order.""" 1448 yield from self.model.quadratic_objective_terms() 1449 1450 def as_linear_expression(self) -> LinearExpression: 1451 if any(self.quadratic_terms()): 1452 raise TypeError("Cannot get a quadratic objective as a linear expression") 1453 return as_flat_linear_expression(self.offset + LinearSum(self.linear_terms())) 1454 1455 def as_quadratic_expression(self) -> QuadraticExpression: 1456 return as_flat_quadratic_expression( 1457 self.offset 1458 + LinearSum(self.linear_terms()) 1459 + QuadraticSum(self.quadratic_terms()) 1460 ) 1461 1462 def clear(self) -> None: 1463 """Clears objective coefficients and offset. Does not change direction.""" 1464 self.model.storage.clear_objective() 1465 1466 1467class LinearConstraintMatrixEntry(NamedTuple): 1468 linear_constraint: LinearConstraint 1469 variable: Variable 1470 coefficient: float 1471 1472 1473class UpdateTracker: 1474 """Tracks updates to an optimization model from a ModelStorage. 1475 1476 Do not instantiate directly, instead create through 1477 ModelStorage.add_update_tracker(). 1478 1479 Querying an UpdateTracker after calling Model.remove_update_tracker will 1480 result in a model_storage.UsedUpdateTrackerAfterRemovalError. 1481 1482 Example: 1483 mod = Model() 1484 x = mod.add_variable(0.0, 1.0, True, 'x') 1485 y = mod.add_variable(0.0, 1.0, True, 'y') 1486 tracker = mod.add_update_tracker() 1487 mod.set_variable_ub(x, 3.0) 1488 tracker.export_update() 1489 => "variable_updates: {upper_bounds: {ids: [0], values[3.0] }" 1490 mod.set_variable_ub(y, 2.0) 1491 tracker.export_update() 1492 => "variable_updates: {upper_bounds: {ids: [0, 1], values[3.0, 2.0] }" 1493 tracker.advance_checkpoint() 1494 tracker.export_update() 1495 => None 1496 mod.set_variable_ub(y, 4.0) 1497 tracker.export_update() 1498 => "variable_updates: {upper_bounds: {ids: [1], values[4.0] }" 1499 tracker.advance_checkpoint() 1500 mod.remove_update_tracker(tracker) 1501 """ 1502 1503 def __init__(self, storage_update_tracker: model_storage.StorageUpdateTracker): 1504 """Do not invoke directly, use Model.add_update_tracker() instead.""" 1505 self.storage_update_tracker = storage_update_tracker 1506 1507 def export_update(self) -> Optional[model_update_pb2.ModelUpdateProto]: 1508 """Returns changes to the model since last call to checkpoint/creation.""" 1509 return self.storage_update_tracker.export_update() 1510 1511 def advance_checkpoint(self) -> None: 1512 """Track changes to the model only after this function call.""" 1513 return self.storage_update_tracker.advance_checkpoint() 1514 1515 1516class Model: 1517 """An optimization model. 1518 1519 The objective function of the model can be linear or quadratic, and some 1520 solvers can only handle linear objective functions. For this reason Model has 1521 three versions of all objective setting functions: 1522 * A generic one (e.g. maximize()), which accepts linear or quadratic 1523 expressions, 1524 * a quadratic version (e.g. maximize_quadratic_objective()), which also 1525 accepts linear or quadratic expressions and can be used to signal a 1526 quadratic objective is possible, and 1527 * a linear version (e.g. maximize_linear_objective()), which only accepts 1528 linear expressions and can be used to avoid solve time errors for solvers 1529 that do not accept quadratic objectives. 1530 1531 Attributes: 1532 name: A description of the problem, can be empty. 1533 objective: A function to maximize or minimize. 1534 storage: Implementation detail, do not access directly. 1535 _variable_ids: Maps variable ids to Variable objects. 1536 _linear_constraint_ids: Maps linear constraint ids to LinearConstraint 1537 objects. 1538 """ 1539 1540 __slots__ = "storage", "_variable_ids", "_linear_constraint_ids", "_objective" 1541 1542 def __init__( 1543 self, 1544 *, 1545 name: str = "", 1546 storage_class: StorageClass = hash_model_storage.HashModelStorage, 1547 ) -> None: 1548 self.storage: Storage = storage_class(name) 1549 # Weak references are used to eliminate reference cycles (so that Model will 1550 # be destroyed when we reach zero references, instead of relying on GC cycle 1551 # detection). Do not access a variable or constraint from these maps 1552 # directly, as they may no longer exist, use _get_or_make_variable() and 1553 # _get_or_make_linear_constraint() defined below instead. 1554 self._variable_ids: weakref.WeakValueDictionary[int, Variable] = ( 1555 weakref.WeakValueDictionary() 1556 ) 1557 self._linear_constraint_ids: weakref.WeakValueDictionary[ 1558 int, LinearConstraint 1559 ] = weakref.WeakValueDictionary() 1560 self._objective: Objective = Objective(self) 1561 1562 @property 1563 def name(self) -> str: 1564 return self.storage.name 1565 1566 @property 1567 def objective(self) -> Objective: 1568 return self._objective 1569 1570 def add_variable( 1571 self, 1572 *, 1573 lb: float = -math.inf, 1574 ub: float = math.inf, 1575 is_integer: bool = False, 1576 name: str = "", 1577 ) -> Variable: 1578 """Adds a decision variable to the optimization model. 1579 1580 Args: 1581 lb: The new variable must take at least this value (a lower bound). 1582 ub: The new variable must be at most this value (an upper bound). 1583 is_integer: Indicates if the variable can only take integer values 1584 (otherwise, the variable can take any continuous value). 1585 name: For debugging purposes only, but nonempty names must be distinct. 1586 1587 Returns: 1588 A reference to the new decision variable. 1589 """ 1590 variable_id = self.storage.add_variable(lb, ub, is_integer, name) 1591 result = Variable(self, variable_id) 1592 self._variable_ids[variable_id] = result 1593 return result 1594 1595 def add_integer_variable( 1596 self, *, lb: float = -math.inf, ub: float = math.inf, name: str = "" 1597 ) -> Variable: 1598 return self.add_variable(lb=lb, ub=ub, is_integer=True, name=name) 1599 1600 def add_binary_variable(self, *, name: str = "") -> Variable: 1601 return self.add_variable(lb=0.0, ub=1.0, is_integer=True, name=name) 1602 1603 def get_variable(self, var_id: int) -> Variable: 1604 """Returns the Variable for the id var_id, or raises KeyError.""" 1605 if not self.storage.variable_exists(var_id): 1606 raise KeyError(f"variable does not exist with id {var_id}") 1607 return self._get_or_make_variable(var_id) 1608 1609 def delete_variable(self, variable: Variable) -> None: 1610 self.check_compatible(variable) 1611 self.storage.delete_variable(variable.id) 1612 del self._variable_ids[variable.id] 1613 1614 def variables(self) -> Iterator[Variable]: 1615 """Yields the variables in the order of creation.""" 1616 for var_id in self.storage.get_variables(): 1617 yield self._get_or_make_variable(var_id) 1618 1619 def maximize(self, objective: QuadraticTypes) -> None: 1620 """Sets the objective to maximize the provided expression `objective`.""" 1621 self.set_objective(objective, is_maximize=True) 1622 1623 def maximize_linear_objective(self, objective: LinearTypes) -> None: 1624 """Sets the objective to maximize the provided linear expression `objective`.""" 1625 self.set_linear_objective(objective, is_maximize=True) 1626 1627 def maximize_quadratic_objective(self, objective: QuadraticTypes) -> None: 1628 """Sets the objective to maximize the provided quadratic expression `objective`.""" 1629 self.set_quadratic_objective(objective, is_maximize=True) 1630 1631 def minimize(self, objective: QuadraticTypes) -> None: 1632 """Sets the objective to minimize the provided expression `objective`.""" 1633 self.set_objective(objective, is_maximize=False) 1634 1635 def minimize_linear_objective(self, objective: LinearTypes) -> None: 1636 """Sets the objective to minimize the provided linear expression `objective`.""" 1637 self.set_linear_objective(objective, is_maximize=False) 1638 1639 def minimize_quadratic_objective(self, objective: QuadraticTypes) -> None: 1640 """Sets the objective to minimize the provided quadratic expression `objective`.""" 1641 self.set_quadratic_objective(objective, is_maximize=False) 1642 1643 def set_objective(self, objective: QuadraticTypes, *, is_maximize: bool) -> None: 1644 """Sets the objective to optimize the provided expression `objective`.""" 1645 if isinstance(objective, (LinearBase, int, float)): 1646 self.set_linear_objective(objective, is_maximize=is_maximize) 1647 elif isinstance(objective, QuadraticBase): 1648 self.set_quadratic_objective(objective, is_maximize=is_maximize) 1649 else: 1650 raise TypeError( 1651 "unsupported type in objective argument for " 1652 f"set_objective: {type(objective).__name__!r}" 1653 ) 1654 1655 def set_linear_objective( 1656 self, objective: LinearTypes, *, is_maximize: bool 1657 ) -> None: 1658 """Sets the objective to optimize the provided linear expression `objective`.""" 1659 if not isinstance(objective, (LinearBase, int, float)): 1660 raise TypeError( 1661 "unsupported type in objective argument for " 1662 f"set_linear_objective: {type(objective).__name__!r}" 1663 ) 1664 self.storage.clear_objective() 1665 self._objective.is_maximize = is_maximize 1666 objective_expr = as_flat_linear_expression(objective) 1667 self._objective.offset = objective_expr.offset 1668 for var, coefficient in objective_expr.terms.items(): 1669 self._objective.set_linear_coefficient(var, coefficient) 1670 1671 def set_quadratic_objective( 1672 self, objective: QuadraticTypes, *, is_maximize: bool 1673 ) -> None: 1674 """Sets the objective to optimize the provided linear expression `objective`.""" 1675 if not isinstance(objective, (QuadraticBase, LinearBase, int, float)): 1676 raise TypeError( 1677 "unsupported type in objective argument for " 1678 f"set_quadratic_objective: {type(objective).__name__!r}" 1679 ) 1680 self.storage.clear_objective() 1681 self._objective.is_maximize = is_maximize 1682 objective_expr = as_flat_quadratic_expression(objective) 1683 self._objective.offset = objective_expr.offset 1684 for var, coefficient in objective_expr.linear_terms.items(): 1685 self._objective.set_linear_coefficient(var, coefficient) 1686 for key, coefficient in objective_expr.quadratic_terms.items(): 1687 self._objective.set_quadratic_coefficient( 1688 key.first_var, key.second_var, coefficient 1689 ) 1690 1691 # TODO(b/227214976): Update the note below and link to pytype bug number. 1692 # Note: bounded_expr's type includes bool only as a workaround to a pytype 1693 # issue. Passing a bool for bounded_expr will raise an error in runtime. 1694 def add_linear_constraint( 1695 self, 1696 bounded_expr: Optional[Union[bool, BoundedLinearTypes]] = None, 1697 *, 1698 lb: Optional[float] = None, 1699 ub: Optional[float] = None, 1700 expr: Optional[LinearTypes] = None, 1701 name: str = "", 1702 ) -> LinearConstraint: 1703 """Adds a linear constraint to the optimization model. 1704 1705 The simplest way to specify the constraint is by passing a one-sided or 1706 two-sided linear inequality as in: 1707 * add_linear_constraint(x + y + 1.0 <= 2.0), 1708 * add_linear_constraint(x + y >= 2.0), or 1709 * add_linear_constraint((1.0 <= x + y) <= 2.0). 1710 1711 Note the extra parenthesis for two-sided linear inequalities, which is 1712 required due to some language limitations (see 1713 https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). 1714 If the parenthesis are omitted, a TypeError will be raised explaining the 1715 issue (if this error was not raised the first inequality would have been 1716 silently ignored because of the noted language limitations). 1717 1718 The second way to specify the constraint is by setting lb, ub, and/o expr as 1719 in: 1720 * add_linear_constraint(expr=x + y + 1.0, ub=2.0), 1721 * add_linear_constraint(expr=x + y, lb=2.0), 1722 * add_linear_constraint(expr=x + y, lb=1.0, ub=2.0), or 1723 * add_linear_constraint(lb=1.0). 1724 Omitting lb is equivalent to setting it to -math.inf and omiting ub is 1725 equivalent to setting it to math.inf. 1726 1727 These two alternatives are exclusive and a combined call like: 1728 * add_linear_constraint(x + y <= 2.0, lb=1.0), or 1729 * add_linear_constraint(x + y <= 2.0, ub=math.inf) 1730 will raise a ValueError. A ValueError is also raised if expr's offset is 1731 infinite. 1732 1733 Args: 1734 bounded_expr: a linear inequality describing the constraint. Cannot be 1735 specified together with lb, ub, or expr. 1736 lb: The constraint's lower bound if bounded_expr is omitted (if both 1737 bounder_expr and lb are omitted, the lower bound is -math.inf). 1738 ub: The constraint's upper bound if bounded_expr is omitted (if both 1739 bounder_expr and ub are omitted, the upper bound is math.inf). 1740 expr: The constraint's linear expression if bounded_expr is omitted. 1741 name: For debugging purposes only, but nonempty names must be distinct. 1742 1743 Returns: 1744 A reference to the new linear constraint. 1745 """ 1746 normalized_inequality = as_normalized_linear_inequality( 1747 bounded_expr, lb=lb, ub=ub, expr=expr 1748 ) 1749 lin_con_id = self.storage.add_linear_constraint( 1750 normalized_inequality.lb, normalized_inequality.ub, name 1751 ) 1752 result = LinearConstraint(self, lin_con_id) 1753 self._linear_constraint_ids[lin_con_id] = result 1754 for var, coefficient in normalized_inequality.coefficients.items(): 1755 result.set_coefficient(var, coefficient) 1756 return result 1757 1758 def get_linear_constraint(self, lin_con_id: int) -> LinearConstraint: 1759 """Returns the LinearConstraint for the id lin_con_id.""" 1760 if not self.storage.linear_constraint_exists(lin_con_id): 1761 raise KeyError(f"linear constraint does not exist with id {lin_con_id}") 1762 return self._get_or_make_linear_constraint(lin_con_id) 1763 1764 def delete_linear_constraint(self, linear_constraint: LinearConstraint) -> None: 1765 self.check_compatible(linear_constraint) 1766 self.storage.delete_linear_constraint(linear_constraint.id) 1767 del self._linear_constraint_ids[linear_constraint.id] 1768 1769 def linear_constraints(self) -> Iterator[LinearConstraint]: 1770 """Yields the linear constraints in the order of creation.""" 1771 for lin_con_id in self.storage.get_linear_constraints(): 1772 yield self._get_or_make_linear_constraint(lin_con_id) 1773 1774 def row_nonzeros(self, linear_constraint: LinearConstraint) -> Iterator[Variable]: 1775 """Yields the variables with nonzero coefficient for this linear constraint.""" 1776 for var_id in self.storage.get_variables_for_linear_constraint( 1777 linear_constraint.id 1778 ): 1779 yield self._get_or_make_variable(var_id) 1780 1781 def column_nonzeros(self, variable: Variable) -> Iterator[LinearConstraint]: 1782 """Yields the linear constraints with nonzero coefficient for this variable.""" 1783 for lin_con_id in self.storage.get_linear_constraints_with_variable( 1784 variable.id 1785 ): 1786 yield self._get_or_make_linear_constraint(lin_con_id) 1787 1788 def linear_objective_terms(self) -> Iterator[LinearTerm]: 1789 """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.""" 1790 for term in self.storage.get_linear_objective_coefficients(): 1791 yield LinearTerm( 1792 variable=self._get_or_make_variable(term.variable_id), 1793 coefficient=term.coefficient, 1794 ) 1795 1796 def quadratic_objective_terms(self) -> Iterator[QuadraticTerm]: 1797 """Yields the quadratic terms with nonzero objective coefficient in undefined order.""" 1798 for term in self.storage.get_quadratic_objective_coefficients(): 1799 var1 = self._get_or_make_variable(term.id_key.id1) 1800 var2 = self._get_or_make_variable(term.id_key.id2) 1801 yield QuadraticTerm( 1802 key=QuadraticTermKey(var1, var2), coefficient=term.coefficient 1803 ) 1804 1805 def linear_constraint_matrix_entries( 1806 self, 1807 ) -> Iterator[LinearConstraintMatrixEntry]: 1808 """Yields the nonzero elements of the linear constraint matrix in undefined order.""" 1809 for entry in self.storage.get_linear_constraint_matrix_entries(): 1810 yield LinearConstraintMatrixEntry( 1811 linear_constraint=self._get_or_make_linear_constraint( 1812 entry.linear_constraint_id 1813 ), 1814 variable=self._get_or_make_variable(entry.variable_id), 1815 coefficient=entry.coefficient, 1816 ) 1817 1818 def export_model(self) -> model_pb2.ModelProto: 1819 return self.storage.export_model() 1820 1821 def add_update_tracker(self) -> UpdateTracker: 1822 """Creates an UpdateTracker registered on this model to view changes.""" 1823 return UpdateTracker(self.storage.add_update_tracker()) 1824 1825 def remove_update_tracker(self, tracker: UpdateTracker): 1826 """Stops tracker from getting updates on changes to this model. 1827 1828 An error will be raised if tracker was not created by this Model or if 1829 tracker has been previously removed. 1830 1831 Using (via checkpoint or update) an UpdateTracker after it has been removed 1832 will result in an error. 1833 1834 Args: 1835 tracker: The UpdateTracker to unregister. 1836 1837 Raises: 1838 KeyError: The tracker was created by another model or was already removed. 1839 """ 1840 self.storage.remove_update_tracker(tracker.storage_update_tracker) 1841 1842 def check_compatible( 1843 self, var_or_constraint: Union[Variable, LinearConstraint] 1844 ) -> None: 1845 """Raises a ValueError if the model of var_or_constraint is not self.""" 1846 if var_or_constraint.model is not self: 1847 raise ValueError( 1848 f"{var_or_constraint} is from model {var_or_constraint.model} and" 1849 f" cannot be used with model {self}" 1850 ) 1851 1852 def _get_or_make_variable(self, variable_id: int) -> Variable: 1853 result = self._variable_ids.get(variable_id) 1854 if result: 1855 return result 1856 result = Variable(self, variable_id) 1857 self._variable_ids[variable_id] = result 1858 return result 1859 1860 def _get_or_make_linear_constraint( 1861 self, linear_constraint_id: int 1862 ) -> LinearConstraint: 1863 result = self._linear_constraint_ids.get(linear_constraint_id) 1864 if result: 1865 return result 1866 result = LinearConstraint(self, linear_constraint_id) 1867 self._linear_constraint_ids[linear_constraint_id] = result 1868 return result 1869 1870 1871class LinearSum(LinearBase): 1872 # TODO(b/216492143): consider what details to move elsewhere and/or replace 1873 # by examples, and do complexity analysis. 1874 """A deferred sum of LinearBase objects. 1875 1876 LinearSum objects are automatically created when two linear objects are added 1877 and, as noted in the documentation for Linear, can reduce the inefficiencies. 1878 In particular, they are created when calling sum(iterable) when iterable is 1879 an Iterable[LinearTypes]. However, using LinearSum(iterable) instead 1880 can result in additional performance improvements: 1881 1882 * sum(iterable): creates a nested set of LinearSum objects (e.g. 1883 `sum([a, b, c])` is `LinearSum(0, LinearSum(a, LinearSum(b, c)))`). 1884 * LinearSum(iterable): creates a single LinearSum that saves a tuple with 1885 all the LinearTypes objects in iterable (e.g. 1886 `LinearSum([a, b, c])` does not create additional objects). 1887 1888 This class is immutable. 1889 """ 1890 1891 __slots__ = "__weakref__", "_elements" 1892 1893 # Potentially unsafe use of Iterable argument is handled by immediate local 1894 # storage as tuple. 1895 def __init__(self, iterable: Iterable[LinearTypes]) -> None: 1896 """Creates a LinearSum object. A copy of iterable is saved as a tuple.""" 1897 1898 self._elements = tuple(iterable) 1899 for item in self._elements: 1900 if not isinstance(item, (LinearBase, int, float)): 1901 raise TypeError( 1902 "unsupported type in iterable argument for " 1903 f"LinearSum: {type(item).__name__!r}" 1904 ) 1905 1906 @property 1907 def elements(self) -> Tuple[LinearTypes, ...]: 1908 return self._elements 1909 1910 def _flatten_once_and_add_to( 1911 self, 1912 scale: float, 1913 processed_elements: _ProcessedElements, 1914 target_stack: _ToProcessElements, 1915 ) -> None: 1916 for term in self._elements: 1917 if isinstance(term, (int, float)): 1918 processed_elements.offset += scale * float(term) 1919 else: 1920 target_stack.append(term, scale) 1921 1922 def __str__(self): 1923 return str(as_flat_linear_expression(self)) 1924 1925 def __repr__(self): 1926 result = "LinearSum((" 1927 result += ", ".join(repr(linear) for linear in self._elements) 1928 result += "))" 1929 return result 1930 1931 1932class QuadraticSum(QuadraticBase): 1933 # TODO(b/216492143): consider what details to move elsewhere and/or replace 1934 # by examples, and do complexity analysis. 1935 """A deferred sum of QuadraticTypes objects. 1936 1937 QuadraticSum objects are automatically created when a quadratic object is 1938 added to quadratic or linear objects and, as has performance optimizations 1939 similar to LinearSum. 1940 1941 This class is immutable. 1942 """ 1943 1944 __slots__ = "__weakref__", "_elements" 1945 1946 # Potentially unsafe use of Iterable argument is handled by immediate local 1947 # storage as tuple. 1948 def __init__(self, iterable: Iterable[QuadraticTypes]) -> None: 1949 """Creates a QuadraticSum object. A copy of iterable is saved as a tuple.""" 1950 1951 self._elements = tuple(iterable) 1952 for item in self._elements: 1953 if not isinstance(item, (LinearBase, QuadraticBase, int, float)): 1954 raise TypeError( 1955 "unsupported type in iterable argument for " 1956 f"QuadraticSum: {type(item).__name__!r}" 1957 ) 1958 1959 @property 1960 def elements(self) -> Tuple[QuadraticTypes, ...]: 1961 return self._elements 1962 1963 def _quadratic_flatten_once_and_add_to( 1964 self, 1965 scale: float, 1966 processed_elements: _QuadraticProcessedElements, 1967 target_stack: _QuadraticToProcessElements, 1968 ) -> None: 1969 for term in self._elements: 1970 if isinstance(term, (int, float)): 1971 processed_elements.offset += scale * float(term) 1972 else: 1973 target_stack.append(term, scale) 1974 1975 def __str__(self): 1976 return str(as_flat_quadratic_expression(self)) 1977 1978 def __repr__(self): 1979 result = "QuadraticSum((" 1980 result += ", ".join(repr(element) for element in self._elements) 1981 result += "))" 1982 return result 1983 1984 1985class LinearProduct(LinearBase): 1986 """A deferred multiplication computation for linear expressions. 1987 1988 This class is immutable. 1989 """ 1990 1991 __slots__ = "_scalar", "_linear" 1992 1993 def __init__(self, scalar: float, linear: LinearBase) -> None: 1994 if not isinstance(scalar, (float, int)): 1995 raise TypeError( 1996 "unsupported type for scalar argument in " 1997 f"LinearProduct: {type(scalar).__name__!r}" 1998 ) 1999 if not isinstance(linear, LinearBase): 2000 raise TypeError( 2001 "unsupported type for linear argument in " 2002 f"LinearProduct: {type(linear).__name__!r}" 2003 ) 2004 self._scalar: float = float(scalar) 2005 self._linear: LinearBase = linear 2006 2007 @property 2008 def scalar(self) -> float: 2009 return self._scalar 2010 2011 @property 2012 def linear(self) -> LinearBase: 2013 return self._linear 2014 2015 def _flatten_once_and_add_to( 2016 self, 2017 scale: float, 2018 processed_elements: _ProcessedElements, 2019 target_stack: _ToProcessElements, 2020 ) -> None: 2021 target_stack.append(self._linear, self._scalar * scale) 2022 2023 def __str__(self): 2024 return str(as_flat_linear_expression(self)) 2025 2026 def __repr__(self): 2027 result = f"LinearProduct({self._scalar!r}, " 2028 result += f"{self._linear!r})" 2029 return result 2030 2031 2032class QuadraticProduct(QuadraticBase): 2033 """A deferred multiplication computation for quadratic expressions. 2034 2035 This class is immutable. 2036 """ 2037 2038 __slots__ = "_scalar", "_quadratic" 2039 2040 def __init__(self, scalar: float, quadratic: QuadraticBase) -> None: 2041 if not isinstance(scalar, (float, int)): 2042 raise TypeError( 2043 "unsupported type for scalar argument in " 2044 f"QuadraticProduct: {type(scalar).__name__!r}" 2045 ) 2046 if not isinstance(quadratic, QuadraticBase): 2047 raise TypeError( 2048 "unsupported type for linear argument in " 2049 f"QuadraticProduct: {type(quadratic).__name__!r}" 2050 ) 2051 self._scalar: float = float(scalar) 2052 self._quadratic: QuadraticBase = quadratic 2053 2054 @property 2055 def scalar(self) -> float: 2056 return self._scalar 2057 2058 @property 2059 def quadratic(self) -> QuadraticBase: 2060 return self._quadratic 2061 2062 def _quadratic_flatten_once_and_add_to( 2063 self, 2064 scale: float, 2065 processed_elements: _QuadraticProcessedElements, 2066 target_stack: _QuadraticToProcessElements, 2067 ) -> None: 2068 target_stack.append(self._quadratic, self._scalar * scale) 2069 2070 def __str__(self): 2071 return str(as_flat_quadratic_expression(self)) 2072 2073 def __repr__(self): 2074 return f"QuadraticProduct({self._scalar}, {self._quadratic!r})" 2075 2076 2077class LinearLinearProduct(QuadraticBase): 2078 """A deferred multiplication of two linear expressions. 2079 2080 This class is immutable. 2081 """ 2082 2083 __slots__ = "_first_linear", "_second_linear" 2084 2085 def __init__(self, first_linear: LinearBase, second_linear: LinearBase) -> None: 2086 if not isinstance(first_linear, LinearBase): 2087 raise TypeError( 2088 "unsupported type for first_linear argument in " 2089 f"LinearLinearProduct: {type(first_linear).__name__!r}" 2090 ) 2091 if not isinstance(second_linear, LinearBase): 2092 raise TypeError( 2093 "unsupported type for second_linear argument in " 2094 f"LinearLinearProduct: {type(second_linear).__name__!r}" 2095 ) 2096 self._first_linear: LinearBase = first_linear 2097 self._second_linear: LinearBase = second_linear 2098 2099 @property 2100 def first_linear(self) -> LinearBase: 2101 return self._first_linear 2102 2103 @property 2104 def second_linear(self) -> LinearBase: 2105 return self._second_linear 2106 2107 def _quadratic_flatten_once_and_add_to( 2108 self, 2109 scale: float, 2110 processed_elements: _QuadraticProcessedElements, 2111 target_stack: _QuadraticToProcessElements, 2112 ) -> None: 2113 # A recursion is avoided here because as_flat_linear_expression() must never 2114 # call _quadratic_flatten_once_and_add_to(). 2115 first_expression = as_flat_linear_expression(self._first_linear) 2116 second_expression = as_flat_linear_expression(self._second_linear) 2117 processed_elements.offset += ( 2118 first_expression.offset * second_expression.offset * scale 2119 ) 2120 for first_var, first_val in first_expression.terms.items(): 2121 processed_elements.terms[first_var] += ( 2122 second_expression.offset * first_val * scale 2123 ) 2124 for second_var, second_val in second_expression.terms.items(): 2125 processed_elements.terms[second_var] += ( 2126 first_expression.offset * second_val * scale 2127 ) 2128 2129 for first_var, first_val in first_expression.terms.items(): 2130 for second_var, second_val in second_expression.terms.items(): 2131 processed_elements.quadratic_terms[ 2132 QuadraticTermKey(first_var, second_var) 2133 ] += (first_val * second_val * scale) 2134 2135 def __str__(self): 2136 return str(as_flat_quadratic_expression(self)) 2137 2138 def __repr__(self): 2139 result = "LinearLinearProduct(" 2140 result += f"{self._first_linear!r}, " 2141 result += f"{self._second_linear!r})" 2142 return result 2143 2144 2145def as_flat_linear_expression(value: LinearTypes) -> LinearExpression: 2146 """Converts floats, ints and Linear objects to a LinearExpression.""" 2147 if isinstance(value, LinearExpression): 2148 return value 2149 return LinearExpression(value) 2150 2151 2152def as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression: 2153 """Converts floats, ints, LinearBase and QuadraticBase objects to a QuadraticExpression.""" 2154 if isinstance(value, QuadraticExpression): 2155 return value 2156 return QuadraticExpression(value) 2157 2158 2159@dataclasses.dataclass 2160class NormalizedLinearInequality: 2161 """Represents an inequality lb <= expr <= ub where expr's offset is zero. 2162 2163 The inequality is of the form: 2164 lb <= sum_{x in V} coefficients[x] * x <= ub 2165 where V is the set of keys of coefficients. 2166 """ 2167 2168 lb: float 2169 ub: float 2170 coefficients: Mapping[Variable, float] 2171 2172 def __init__(self, *, lb: float, ub: float, expr: LinearTypes) -> None: 2173 """Raises a ValueError if expr's offset is infinite.""" 2174 flat_expr = as_flat_linear_expression(expr) 2175 if math.isinf(flat_expr.offset): 2176 raise ValueError( 2177 "Trying to create a linear constraint whose expression has an" 2178 " infinite offset." 2179 ) 2180 self.lb = lb - flat_expr.offset 2181 self.ub = ub - flat_expr.offset 2182 self.coefficients = flat_expr.terms 2183 2184 2185# TODO(b/227214976): Update the note below and link to pytype bug number. 2186# Note: bounded_expr's type includes bool only as a workaround to a pytype 2187# issue. Passing a bool for bounded_expr will raise an error in runtime. 2188def as_normalized_linear_inequality( 2189 bounded_expr: Optional[Union[bool, BoundedLinearTypes]] = None, 2190 *, 2191 lb: Optional[float] = None, 2192 ub: Optional[float] = None, 2193 expr: Optional[LinearTypes] = None, 2194) -> NormalizedLinearInequality: 2195 """Converts a linear constraint to a NormalizedLinearInequality. 2196 2197 The simplest way to specify the constraint is by passing a one-sided or 2198 two-sided linear inequality as in: 2199 * as_normalized_linear_inequality(x + y + 1.0 <= 2.0), 2200 * as_normalized_linear_inequality(x + y >= 2.0), or 2201 * as_normalized_linear_inequality((1.0 <= x + y) <= 2.0). 2202 2203 Note the extra parenthesis for two-sided linear inequalities, which is 2204 required due to some language limitations (see 2205 https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). 2206 If the parenthesis are omitted, a TypeError will be raised explaining the 2207 issue (if this error was not raised the first inequality would have been 2208 silently ignored because of the noted language limitations). 2209 2210 The second way to specify the constraint is by setting lb, ub, and/o expr as 2211 in: 2212 * as_normalized_linear_inequality(expr=x + y + 1.0, ub=2.0), 2213 * as_normalized_linear_inequality(expr=x + y, lb=2.0), 2214 * as_normalized_linear_inequality(expr=x + y, lb=1.0, ub=2.0), or 2215 * as_normalized_linear_inequality(lb=1.0). 2216 Omitting lb is equivalent to setting it to -math.inf and omiting ub is 2217 equivalent to setting it to math.inf. 2218 2219 These two alternatives are exclusive and a combined call like: 2220 * as_normalized_linear_inequality(x + y <= 2.0, lb=1.0), or 2221 * as_normalized_linear_inequality(x + y <= 2.0, ub=math.inf) 2222 will raise a ValueError. A ValueError is also raised if expr's offset is 2223 infinite. 2224 2225 Args: 2226 bounded_expr: a linear inequality describing the constraint. Cannot be 2227 specified together with lb, ub, or expr. 2228 lb: The constraint's lower bound if bounded_expr is omitted (if both 2229 bounder_expr and lb are omitted, the lower bound is -math.inf). 2230 ub: The constraint's upper bound if bounded_expr is omitted (if both 2231 bounder_expr and ub are omitted, the upper bound is math.inf). 2232 expr: The constraint's linear expression if bounded_expr is omitted. 2233 2234 Returns: 2235 A NormalizedLinearInequality representing the linear constraint. 2236 """ 2237 if bounded_expr is None: 2238 if lb is None: 2239 lb = -math.inf 2240 if ub is None: 2241 ub = math.inf 2242 if expr is None: 2243 return NormalizedLinearInequality(lb=lb, ub=ub, expr=0) 2244 if not isinstance(expr, (LinearBase, int, float)): 2245 raise TypeError( 2246 f"unsupported type for expr argument: {type(expr).__name__!r}" 2247 ) 2248 return NormalizedLinearInequality(lb=lb, ub=ub, expr=expr) 2249 2250 if isinstance(bounded_expr, bool): 2251 raise TypeError( 2252 "unsupported type for bounded_expr argument:" 2253 " bool. This error can occur when trying to add != constraints " 2254 "(which are not supported) or inequalities/equalities with constant " 2255 "left-hand-side and right-hand-side (which are redundant or make a " 2256 "model infeasible)." 2257 ) 2258 if not isinstance( 2259 bounded_expr, 2260 ( 2261 LowerBoundedLinearExpression, 2262 UpperBoundedLinearExpression, 2263 BoundedLinearExpression, 2264 VarEqVar, 2265 ), 2266 ): 2267 raise TypeError( 2268 f"unsupported type for bounded_expr: {type(bounded_expr).__name__!r}" 2269 ) 2270 if lb is not None: 2271 raise ValueError("lb cannot be specified together with a linear inequality") 2272 if ub is not None: 2273 raise ValueError("ub cannot be specified together with a linear inequality") 2274 if expr is not None: 2275 raise ValueError("expr cannot be specified together with a linear inequality") 2276 2277 if isinstance(bounded_expr, VarEqVar): 2278 return NormalizedLinearInequality( 2279 lb=0.0, 2280 ub=0.0, 2281 expr=bounded_expr.first_variable - bounded_expr.second_variable, 2282 ) 2283 2284 if isinstance( 2285 bounded_expr, (LowerBoundedLinearExpression, BoundedLinearExpression) 2286 ): 2287 lb = bounded_expr.lower_bound 2288 else: 2289 lb = -math.inf 2290 if isinstance( 2291 bounded_expr, (UpperBoundedLinearExpression, BoundedLinearExpression) 2292 ): 2293 ub = bounded_expr.upper_bound 2294 else: 2295 ub = math.inf 2296 return NormalizedLinearInequality(lb=lb, ub=ub, expr=bounded_expr.expression)
118class UpperBoundedLinearExpression: 119 """An inequality of the form expression <= upper_bound. 120 121 Where: 122 * expression is a linear expression, and 123 * upper_bound is a float 124 """ 125 126 __slots__ = "_expression", "_upper_bound" 127 128 def __init__(self, expression: "LinearBase", upper_bound: float) -> None: 129 """Operator overloading can be used instead: e.g. `x + y <= 2.0`.""" 130 self._expression: "LinearBase" = expression 131 self._upper_bound: float = upper_bound 132 133 @property 134 def expression(self) -> "LinearBase": 135 return self._expression 136 137 @property 138 def upper_bound(self) -> float: 139 return self._upper_bound 140 141 def __ge__(self, lhs: float) -> "BoundedLinearExpression": 142 if isinstance(lhs, (int, float)): 143 return BoundedLinearExpression(lhs, self.expression, self.upper_bound) 144 _raise_binary_operator_type_error(">=", type(self), type(lhs)) 145 146 def __bool__(self) -> bool: 147 raise TypeError( 148 "__bool__ is unsupported for UpperBoundedLinearExpression" 149 + "\n" 150 + _CHAINED_COMPARISON_MESSAGE 151 ) 152 153 def __str__(self): 154 return f"{self._expression!s} <= {self._upper_bound}" 155 156 def __repr__(self): 157 return f"{self._expression!r} <= {self._upper_bound}"
An inequality of the form expression <= upper_bound.
Where:
- expression is a linear expression, and
- upper_bound is a float
128 def __init__(self, expression: "LinearBase", upper_bound: float) -> None: 129 """Operator overloading can be used instead: e.g. `x + y <= 2.0`.""" 130 self._expression: "LinearBase" = expression 131 self._upper_bound: float = upper_bound
Operator overloading can be used instead: e.g. x + y <= 2.0
.
160class LowerBoundedLinearExpression: 161 """An inequality of the form expression >= lower_bound. 162 163 Where: 164 * expression is a linear expression, and 165 * lower_bound is a float 166 """ 167 168 __slots__ = "_expression", "_lower_bound" 169 170 def __init__(self, expression: "LinearBase", lower_bound: float) -> None: 171 """Operator overloading can be used instead: e.g. `x + y >= 2.0`.""" 172 self._expression: "LinearBase" = expression 173 self._lower_bound: float = lower_bound 174 175 @property 176 def expression(self) -> "LinearBase": 177 return self._expression 178 179 @property 180 def lower_bound(self) -> float: 181 return self._lower_bound 182 183 def __le__(self, rhs: float) -> "BoundedLinearExpression": 184 if isinstance(rhs, (int, float)): 185 return BoundedLinearExpression(self.lower_bound, self.expression, rhs) 186 _raise_binary_operator_type_error("<=", type(self), type(rhs)) 187 188 def __bool__(self) -> bool: 189 raise TypeError( 190 "__bool__ is unsupported for LowerBoundedLinearExpression" 191 + "\n" 192 + _CHAINED_COMPARISON_MESSAGE 193 ) 194 195 def __str__(self): 196 return f"{self._expression!s} >= {self._lower_bound}" 197 198 def __repr__(self): 199 return f"{self._expression!r} >= {self._lower_bound}"
An inequality of the form expression >= lower_bound.
Where:
- expression is a linear expression, and
- lower_bound is a float
170 def __init__(self, expression: "LinearBase", lower_bound: float) -> None: 171 """Operator overloading can be used instead: e.g. `x + y >= 2.0`.""" 172 self._expression: "LinearBase" = expression 173 self._lower_bound: float = lower_bound
Operator overloading can be used instead: e.g. x + y >= 2.0
.
202class BoundedLinearExpression: 203 """An inequality of the form lower_bound <= expression <= upper_bound. 204 205 Where: 206 * expression is a linear expression 207 * lower_bound is a float, and 208 * upper_bound is a float 209 210 Note: Because of limitations related to Python's handling of chained 211 comparisons, bounded expressions cannot be directly created usign 212 overloaded comparisons as in `lower_bound <= expression <= upper_bound`. 213 One solution is to wrap one of the inequalities in parenthesis as in 214 `(lower_bound <= expression) <= upper_bound`. 215 """ 216 217 __slots__ = "_expression", "_lower_bound", "_upper_bound" 218 219 def __init__( 220 self, lower_bound: float, expression: "LinearBase", upper_bound: float 221 ) -> None: 222 self._expression: "LinearBase" = expression 223 self._lower_bound: float = lower_bound 224 self._upper_bound: float = upper_bound 225 226 @property 227 def expression(self) -> "LinearBase": 228 return self._expression 229 230 @property 231 def lower_bound(self) -> float: 232 return self._lower_bound 233 234 @property 235 def upper_bound(self) -> float: 236 return self._upper_bound 237 238 def __bool__(self) -> bool: 239 raise TypeError( 240 "__bool__ is unsupported for BoundedLinearExpression" 241 + "\n" 242 + _CHAINED_COMPARISON_MESSAGE 243 ) 244 245 def __str__(self): 246 return f"{self._lower_bound} <= {self._expression!s} <= {self._upper_bound}" 247 248 def __repr__(self): 249 return f"{self._lower_bound} <= {self._expression!r} <= {self._upper_bound}"
An inequality of the form lower_bound <= expression <= upper_bound.
Where:
- expression is a linear expression
- lower_bound is a float, and
- upper_bound is a float
Note: Because of limitations related to Python's handling of chained
comparisons, bounded expressions cannot be directly created usign
overloaded comparisons as in lower_bound <= expression <= upper_bound
.
One solution is to wrap one of the inequalities in parenthesis as in
(lower_bound <= expression) <= upper_bound
.
252class VarEqVar: 253 """The result of the equality comparison between two Variable. 254 255 We use an object here to delay the evaluation of equality so that we can use 256 the operator== in two use-cases: 257 258 1. when the user want to test that two Variable values references the same 259 variable. This is supported by having this object support implicit 260 conversion to bool. 261 262 2. when the user want to use the equality to create a constraint of equality 263 between two variables. 264 """ 265 266 __slots__ = "_first_variable", "_second_variable" 267 268 def __init__( 269 self, 270 first_variable: "Variable", 271 second_variable: "Variable", 272 ) -> None: 273 self._first_variable: "Variable" = first_variable 274 self._second_variable: "Variable" = second_variable 275 276 @property 277 def first_variable(self) -> "Variable": 278 return self._first_variable 279 280 @property 281 def second_variable(self) -> "Variable": 282 return self._second_variable 283 284 def __bool__(self) -> bool: 285 return ( 286 self._first_variable.model is self._second_variable.model 287 and self._first_variable.id == self._second_variable.id 288 ) 289 290 def __str__(self): 291 return f"{self.first_variable!s} == {self._second_variable!s}" 292 293 def __repr__(self): 294 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.
307class QuadraticTermKey: 308 """An id-ordered pair of variables used as a key for quadratic terms.""" 309 310 __slots__ = "_first_var", "_second_var" 311 312 def __init__(self, a: "Variable", b: "Variable"): 313 """Variables a and b will be ordered internally by their ids.""" 314 self._first_var: "Variable" = a 315 self._second_var: "Variable" = b 316 if self._first_var.id > self._second_var.id: 317 self._first_var, self._second_var = self._second_var, self._first_var 318 319 @property 320 def first_var(self) -> "Variable": 321 return self._first_var 322 323 @property 324 def second_var(self) -> "Variable": 325 return self._second_var 326 327 def __eq__(self, other: "QuadraticTermKey") -> bool: 328 return bool( 329 self._first_var == other._first_var 330 and self._second_var == other._second_var 331 ) 332 333 def __hash__(self) -> int: 334 return hash((self._first_var, self._second_var)) 335 336 def __str__(self): 337 return f"{self._first_var!s} * {self._second_var!s}" 338 339 def __repr__(self): 340 return f"QuadraticTermKey({self._first_var!r}, {self._second_var!r})"
An id-ordered pair of variables used as a key for quadratic terms.
312 def __init__(self, a: "Variable", b: "Variable"): 313 """Variables a and b will be ordered internally by their ids.""" 314 self._first_var: "Variable" = a 315 self._second_var: "Variable" = b 316 if self._first_var.id > self._second_var.id: 317 self._first_var, self._second_var = self._second_var, self._first_var
Variables a and b will be ordered internally by their ids.
398class LinearBase(metaclass=abc.ABCMeta): 399 """Interface for types that can build linear expressions with +, -, * and /. 400 401 Classes derived from LinearBase (plus float and int scalars) are used to 402 build expression trees describing a linear expression. Operations nodes of the 403 expression tree include: 404 405 * LinearSum: describes a deferred sum of LinearTypes objects. 406 * LinearProduct: describes a deferred product of a scalar and a 407 LinearTypes object. 408 409 Leaf nodes of the expression tree include: 410 411 * float and int scalars. 412 * Variable: a single variable. 413 * LinearTerm: the product of a scalar and a Variable object. 414 * LinearExpression: the sum of a scalar and LinearTerm objects. 415 416 LinearBase objects/expression-trees can be used directly to create 417 constraints or objective functions. However, to facilitate their inspection, 418 any LinearTypes object can be flattened to a LinearExpression 419 through: 420 421 as_flat_linear_expression(value: LinearTypes) -> LinearExpression: 422 423 In addition, all LinearBase objects are immutable. 424 425 Performance notes: 426 427 Using an expression tree representation instead of an eager construction of 428 LinearExpression objects reduces known inefficiencies associated with the 429 use of operator overloading to construct linear expressions. In particular, we 430 expect the runtime of as_flat_linear_expression() to be linear in the size of 431 the expression tree. Additional performance can gained by using LinearSum(c) 432 instead of sum(c) for a container c, as the latter creates len(c) LinearSum 433 objects. 434 """ 435 436 __slots__ = () 437 438 # TODO(b/216492143): explore requirements for this function so calculation of 439 # coefficients and offsets follow expected associativity rules (so approximate 440 # float calculations are as expected). 441 # TODO(b/216492143): add more details of what subclasses need to do in 442 # developers guide. 443 @abc.abstractmethod 444 def _flatten_once_and_add_to( 445 self, 446 scale: float, 447 processed_elements: _ProcessedElements, 448 target_stack: _ToProcessElements, 449 ) -> None: 450 """Flatten one level of tree if needed and add to targets. 451 452 Classes derived from LinearBase only need to implement this function 453 to enable transformation to LinearExpression through 454 as_flat_linear_expression(). 455 456 Args: 457 scale: multiply elements by this number when processing or adding to 458 stack. 459 processed_elements: where to add LinearTerms and scalars that can be 460 processed immediately. 461 target_stack: where to add LinearBase elements that are not scalars or 462 LinearTerms (i.e. elements that need further flattening). 463 Implementations should append() to this stack to avoid being recursive. 464 """ 465 466 def __eq__( 467 self, rhs: LinearTypes 468 ) -> ( 469 "BoundedLinearExpression" 470 ): # pytype: disable=signature-mismatch # overriding-return-type-checks 471 if isinstance(rhs, (int, float)): 472 return BoundedLinearExpression(rhs, self, rhs) 473 if not isinstance(rhs, LinearBase): 474 _raise_binary_operator_type_error("==", type(self), type(rhs)) 475 return BoundedLinearExpression(0.0, self - rhs, 0.0) 476 477 def __ne__( 478 self, rhs: LinearTypes 479 ) -> ( 480 NoReturn 481 ): # pytype: disable=signature-mismatch # overriding-return-type-checks 482 _raise_ne_not_supported() 483 484 @typing.overload 485 def __le__(self, rhs: float) -> "UpperBoundedLinearExpression": ... 486 487 @typing.overload 488 def __le__(self, rhs: "LinearBase") -> "BoundedLinearExpression": ... 489 490 @typing.overload 491 def __le__(self, rhs: "BoundedLinearExpression") -> NoReturn: ... 492 493 def __le__(self, rhs): 494 if isinstance(rhs, (int, float)): 495 return UpperBoundedLinearExpression(self, rhs) 496 if isinstance(rhs, LinearBase): 497 return BoundedLinearExpression(-math.inf, self - rhs, 0.0) 498 if isinstance(rhs, BoundedLinearExpression): 499 _raise_binary_operator_type_error( 500 "<=", type(self), type(rhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE 501 ) 502 _raise_binary_operator_type_error("<=", type(self), type(rhs)) 503 504 @typing.overload 505 def __ge__(self, lhs: float) -> "LowerBoundedLinearExpression": ... 506 507 @typing.overload 508 def __ge__(self, lhs: "LinearBase") -> "BoundedLinearExpression": ... 509 510 @typing.overload 511 def __ge__(self, lhs: "BoundedLinearExpression") -> NoReturn: ... 512 513 def __ge__(self, lhs): 514 if isinstance(lhs, (int, float)): 515 return LowerBoundedLinearExpression(self, lhs) 516 if isinstance(lhs, LinearBase): 517 return BoundedLinearExpression(0.0, self - lhs, math.inf) 518 if isinstance(lhs, BoundedLinearExpression): 519 _raise_binary_operator_type_error( 520 ">=", type(self), type(lhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE 521 ) 522 _raise_binary_operator_type_error(">=", type(self), type(lhs)) 523 524 def __add__(self, expr: LinearTypes) -> "LinearSum": 525 if not isinstance(expr, (int, float, LinearBase)): 526 return NotImplemented 527 return LinearSum((self, expr)) 528 529 def __radd__(self, expr: LinearTypes) -> "LinearSum": 530 if not isinstance(expr, (int, float, LinearBase)): 531 return NotImplemented 532 return LinearSum((expr, self)) 533 534 def __sub__(self, expr: LinearTypes) -> "LinearSum": 535 if not isinstance(expr, (int, float, LinearBase)): 536 return NotImplemented 537 return LinearSum((self, -expr)) 538 539 def __rsub__(self, expr: LinearTypes) -> "LinearSum": 540 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 541 return NotImplemented 542 return LinearSum((expr, -self)) 543 544 @typing.overload 545 def __mul__(self, other: float) -> "LinearProduct": ... 546 547 @typing.overload 548 def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ... 549 550 def __mul__(self, other): 551 if not isinstance(other, (int, float, LinearBase)): 552 return NotImplemented 553 if isinstance(other, LinearBase): 554 return LinearLinearProduct(self, other) 555 return LinearProduct(other, self) 556 557 def __rmul__(self, constant: float) -> "LinearProduct": 558 if not isinstance(constant, (int, float)): 559 return NotImplemented 560 return LinearProduct(constant, self) 561 562 # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. 563 def __truediv__(self, constant: float) -> "LinearProduct": 564 if not isinstance(constant, (int, float)): 565 return NotImplemented 566 return LinearProduct(1.0 / constant, self) 567 568 def __neg__(self) -> "LinearProduct": 569 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.
572class QuadraticBase(metaclass=abc.ABCMeta): 573 """Interface for types that can build quadratic expressions with +, -, * and /. 574 575 Classes derived from QuadraticBase and LinearBase (plus float and int scalars) 576 are used to build expression trees describing a quadratic expression. 577 Operations nodes of the expression tree include: 578 579 * QuadraticSum: describes a deferred sum of QuadraticTypes objects. 580 * QuadraticProduct: describes a deferred product of a scalar and a 581 QuadraticTypes object. 582 * LinearLinearProduct: describes a deferred product of two LinearTypes 583 objects. 584 585 Leaf nodes of the expression tree include: 586 587 * float and int scalars. 588 * Variable: a single variable. 589 * LinearTerm: the product of a scalar and a Variable object. 590 * LinearExpression: the sum of a scalar and LinearTerm objects. 591 * QuadraticTerm: the product of a scalar and two Variable objects. 592 * QuadraticExpression: the sum of a scalar, LinearTerm objects and 593 QuadraticTerm objects. 594 595 QuadraticBase objects/expression-trees can be used directly to create 596 objective functions. However, to facilitate their inspection, any 597 QuadraticTypes object can be flattened to a QuadraticExpression 598 through: 599 600 as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression: 601 602 In addition, all QuadraticBase objects are immutable. 603 604 Performance notes: 605 606 Using an expression tree representation instead of an eager construction of 607 QuadraticExpression objects reduces known inefficiencies associated with the 608 use of operator overloading to construct quadratic expressions. In particular, 609 we expect the runtime of as_flat_quadratic_expression() to be linear in the 610 size of the expression tree. Additional performance can gained by using 611 QuadraticSum(c) instead of sum(c) for a container c, as the latter creates 612 len(c) QuadraticSum objects. 613 """ 614 615 __slots__ = () 616 617 # TODO(b/216492143): explore requirements for this function so calculation of 618 # coefficients and offsets follow expected associativity rules (so approximate 619 # float calculations are as expected). 620 # TODO(b/216492143): add more details of what subclasses need to do in 621 # developers guide. 622 @abc.abstractmethod 623 def _quadratic_flatten_once_and_add_to( 624 self, 625 scale: float, 626 processed_elements: _QuadraticProcessedElements, 627 target_stack: _QuadraticToProcessElements, 628 ) -> None: 629 """Flatten one level of tree if needed and add to targets. 630 631 Classes derived from QuadraticBase only need to implement this function 632 to enable transformation to QuadraticExpression through 633 as_flat_quadratic_expression(). 634 635 Args: 636 scale: multiply elements by this number when processing or adding to 637 stack. 638 processed_elements: where to add linear terms, quadratic terms and scalars 639 that can be processed immediately. 640 target_stack: where to add LinearBase and QuadraticBase elements that are 641 not scalars or linear terms or quadratic terms (i.e. elements that need 642 further flattening). Implementations should append() to this stack to 643 avoid being recursive. 644 """ 645 646 def __add__(self, expr: QuadraticTypes) -> "QuadraticSum": 647 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 648 return NotImplemented 649 return QuadraticSum((self, expr)) 650 651 def __radd__(self, expr: QuadraticTypes) -> "QuadraticSum": 652 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 653 return NotImplemented 654 return QuadraticSum((expr, self)) 655 656 def __sub__(self, expr: QuadraticTypes) -> "QuadraticSum": 657 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 658 return NotImplemented 659 return QuadraticSum((self, -expr)) 660 661 def __rsub__(self, expr: QuadraticTypes) -> "QuadraticSum": 662 if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): 663 return NotImplemented 664 return QuadraticSum((expr, -self)) 665 666 def __mul__(self, other: float) -> "QuadraticProduct": 667 if not isinstance(other, (int, float)): 668 return NotImplemented 669 return QuadraticProduct(other, self) 670 671 def __rmul__(self, other: float) -> "QuadraticProduct": 672 if not isinstance(other, (int, float)): 673 return NotImplemented 674 return QuadraticProduct(other, self) 675 676 # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. 677 def __truediv__(self, constant: float) -> "QuadraticProduct": 678 if not isinstance(constant, (int, float)): 679 return NotImplemented 680 return QuadraticProduct(1.0 / constant, self) 681 682 def __neg__(self) -> "QuadraticProduct": 683 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.
686class Variable(LinearBase): 687 """A decision variable for an optimization model. 688 689 A decision variable takes a value from a domain, either the real numbers or 690 the integers, and restricted to be in some interval [lb, ub] (where lb and ub 691 can be infinite). The case of lb == ub is allowed, this means the variable 692 must take a single value. The case of lb > ub is also allowed, this implies 693 that the problem is infeasible. 694 695 A Variable is configured as follows: 696 * lower_bound: a float property, lb above. Should not be NaN nor +inf. 697 * upper_bound: a float property, ub above. Should not be NaN nor -inf. 698 * integer: a bool property, if the domain is integer or continuous. 699 700 The name is optional, read only, and used only for debugging. Non-empty names 701 should be distinct. 702 703 Every Variable is associated with a Model (defined below). Note that data 704 describing the variable (e.g. lower_bound) is owned by Model.storage, this 705 class is simply a reference to that data. Do not create a Variable directly, 706 use Model.add_variable() instead. 707 """ 708 709 __slots__ = "__weakref__", "_model", "_id" 710 711 def __init__(self, model: "Model", vid: int) -> None: 712 """Do not invoke directly, use Model.add_variable().""" 713 self._model: "Model" = model 714 self._id: int = vid 715 716 @property 717 def lower_bound(self) -> float: 718 return self.model.storage.get_variable_lb(self._id) 719 720 @lower_bound.setter 721 def lower_bound(self, value: float) -> None: 722 self.model.storage.set_variable_lb(self._id, value) 723 724 @property 725 def upper_bound(self) -> float: 726 return self.model.storage.get_variable_ub(self._id) 727 728 @upper_bound.setter 729 def upper_bound(self, value: float) -> None: 730 self.model.storage.set_variable_ub(self._id, value) 731 732 @property 733 def integer(self) -> bool: 734 return self.model.storage.get_variable_is_integer(self._id) 735 736 @integer.setter 737 def integer(self, value: bool) -> None: 738 self.model.storage.set_variable_is_integer(self._id, value) 739 740 @property 741 def name(self) -> str: 742 return self.model.storage.get_variable_name(self._id) 743 744 @property 745 def id(self) -> int: 746 return self._id 747 748 @property 749 def model(self) -> "Model": 750 return self._model 751 752 def __str__(self): 753 """Returns the name, or a string containing the id if the name is empty.""" 754 return self.name if self.name else f"variable_{self.id}" 755 756 def __repr__(self): 757 return f"<Variable id: {self.id}, name: {self.name!r}>" 758 759 @typing.overload 760 def __eq__(self, rhs: "Variable") -> "VarEqVar": ... 761 762 @typing.overload 763 def __eq__(self, rhs: LinearTypesExceptVariable) -> "BoundedLinearExpression": ... 764 765 def __eq__(self, rhs): 766 if isinstance(rhs, Variable): 767 return VarEqVar(self, rhs) 768 return super().__eq__(rhs) 769 770 @typing.overload 771 def __ne__(self, rhs: "Variable") -> bool: ... 772 773 @typing.overload 774 def __ne__(self, rhs: LinearTypesExceptVariable) -> NoReturn: ... 775 776 def __ne__(self, rhs): 777 if isinstance(rhs, Variable): 778 return not self == rhs 779 _raise_ne_not_supported() 780 781 def __hash__(self) -> int: 782 return hash((self.model, self.id)) 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(QuadraticTermKey(self, other), 1.0) 798 if isinstance(other, LinearTerm): 799 return QuadraticTerm( 800 QuadraticTermKey(self, other.variable), other.coefficient 801 ) 802 if isinstance(other, LinearBase): 803 return LinearLinearProduct(self, other) # pytype: disable=bad-return-type 804 return LinearTerm(self, other) 805 806 def __rmul__(self, constant: float) -> "LinearTerm": 807 if not isinstance(constant, (int, float)): 808 return NotImplemented 809 return LinearTerm(self, constant) 810 811 # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. 812 def __truediv__(self, constant: float) -> "LinearTerm": 813 if not isinstance(constant, (int, float)): 814 return NotImplemented 815 return LinearTerm(self, 1.0 / constant) 816 817 def __neg__(self) -> "LinearTerm": 818 return LinearTerm(self, -1.0) 819 820 def _flatten_once_and_add_to( 821 self, 822 scale: float, 823 processed_elements: _ProcessedElements, 824 target_stack: _ToProcessElements, 825 ) -> None: 826 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.
711 def __init__(self, model: "Model", vid: int) -> None: 712 """Do not invoke directly, use Model.add_variable().""" 713 self._model: "Model" = model 714 self._id: int = vid
Do not invoke directly, use Model.add_variable().
829class LinearTerm(LinearBase): 830 """The product of a scalar and a variable. 831 832 This class is immutable. 833 """ 834 835 __slots__ = "_variable", "_coefficient" 836 837 def __init__(self, variable: Variable, coefficient: float) -> None: 838 self._variable: Variable = variable 839 self._coefficient: float = coefficient 840 841 @property 842 def variable(self) -> Variable: 843 return self._variable 844 845 @property 846 def coefficient(self) -> float: 847 return self._coefficient 848 849 def _flatten_once_and_add_to( 850 self, 851 scale: float, 852 processed_elements: _ProcessedElements, 853 target_stack: _ToProcessElements, 854 ) -> None: 855 processed_elements.terms[self._variable] += self._coefficient * scale 856 857 @typing.overload 858 def __mul__(self, other: float) -> "LinearTerm": ... 859 860 @typing.overload 861 def __mul__(self, other: Union["Variable", "LinearTerm"]) -> "QuadraticTerm": ... 862 863 @typing.overload 864 def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ... 865 866 def __mul__(self, other): 867 if not isinstance(other, (int, float, LinearBase)): 868 return NotImplemented 869 if isinstance(other, Variable): 870 return QuadraticTerm( 871 QuadraticTermKey(self._variable, other), self._coefficient 872 ) 873 if isinstance(other, LinearTerm): 874 return QuadraticTerm( 875 QuadraticTermKey(self.variable, other.variable), 876 self._coefficient * other.coefficient, 877 ) 878 if isinstance(other, LinearBase): 879 return LinearLinearProduct(self, other) # pytype: disable=bad-return-type 880 return LinearTerm(self._variable, self._coefficient * other) 881 882 def __rmul__(self, constant: float) -> "LinearTerm": 883 if not isinstance(constant, (int, float)): 884 return NotImplemented 885 return LinearTerm(self._variable, self._coefficient * constant) 886 887 def __truediv__(self, constant: float) -> "LinearTerm": 888 if not isinstance(constant, (int, float)): 889 return NotImplemented 890 return LinearTerm(self._variable, self._coefficient / constant) 891 892 def __neg__(self) -> "LinearTerm": 893 return LinearTerm(self._variable, -self._coefficient) 894 895 def __str__(self): 896 return f"{self._coefficient} * {self._variable}" 897 898 def __repr__(self): 899 return f"LinearTerm({self._variable!r}, {self._coefficient!r})"
The product of a scalar and a variable.
This class is immutable.
902class QuadraticTerm(QuadraticBase): 903 """The product of a scalar and two variables. 904 905 This class is immutable. 906 """ 907 908 __slots__ = "_key", "_coefficient" 909 910 def __init__(self, key: QuadraticTermKey, coefficient: float) -> None: 911 self._key: QuadraticTermKey = key 912 self._coefficient: float = coefficient 913 914 @property 915 def key(self) -> QuadraticTermKey: 916 return self._key 917 918 @property 919 def coefficient(self) -> float: 920 return self._coefficient 921 922 def _quadratic_flatten_once_and_add_to( 923 self, 924 scale: float, 925 processed_elements: _QuadraticProcessedElements, 926 target_stack: _ToProcessElements, 927 ) -> None: 928 processed_elements.quadratic_terms[self._key] += self._coefficient * scale 929 930 def __mul__(self, constant: float) -> "QuadraticTerm": 931 if not isinstance(constant, (int, float)): 932 return NotImplemented 933 return QuadraticTerm(self._key, self._coefficient * constant) 934 935 def __rmul__(self, constant: float) -> "QuadraticTerm": 936 if not isinstance(constant, (int, float)): 937 return NotImplemented 938 return QuadraticTerm(self._key, self._coefficient * constant) 939 940 def __truediv__(self, constant: float) -> "QuadraticTerm": 941 if not isinstance(constant, (int, float)): 942 return NotImplemented 943 return QuadraticTerm(self._key, self._coefficient / constant) 944 945 def __neg__(self) -> "QuadraticTerm": 946 return QuadraticTerm(self._key, -self._coefficient) 947 948 def __str__(self): 949 return f"{self._coefficient} * {self._key!s}" 950 951 def __repr__(self): 952 return f"QuadraticTerm({self._key!r}, {self._coefficient})"
The product of a scalar and two variables.
This class is immutable.
955class LinearExpression(LinearBase): 956 """For variables x, an expression: b + sum_{i in I} a_i * x_i. 957 958 This class is immutable. 959 """ 960 961 __slots__ = "__weakref__", "_terms", "_offset" 962 963 # TODO(b/216492143): consider initializing from a dictionary. 964 def __init__(self, /, other: LinearTypes = 0) -> None: 965 self._offset: float = 0.0 966 if isinstance(other, (int, float)): 967 self._offset = float(other) 968 self._terms: Mapping[Variable, float] = immutabledict.immutabledict() 969 return 970 971 to_process: _LinearToProcessElements = _LinearToProcessElements(other, 1.0) 972 processed_elements = _ProcessedElements() 973 while to_process: 974 linear, coef = to_process.pop() 975 linear._flatten_once_and_add_to(coef, processed_elements, to_process) 976 # TODO(b/216492143): explore avoiding this copy. 977 self._terms: Mapping[Variable, float] = immutabledict.immutabledict( 978 processed_elements.terms 979 ) 980 self._offset = processed_elements.offset 981 982 @property 983 def terms(self) -> Mapping[Variable, float]: 984 return self._terms 985 986 @property 987 def offset(self) -> float: 988 return self._offset 989 990 def evaluate(self, variable_values: Mapping[Variable, float]) -> float: 991 """Returns the value of this expression for given variable values. 992 993 E.g. if this is 3 * x + 4 and variable_values = {x: 2.0}, then 994 evaluate(variable_values) equals 10.0. 995 996 See also mathopt.evaluate_expression(), which works on any type in 997 QuadraticTypes. 998 999 Args: 1000 variable_values: Must contain a value for every variable in expression. 1001 1002 Returns: 1003 The value of this expression when replacing variables by their value. 1004 """ 1005 result = self._offset 1006 for var, coef in sorted( 1007 self._terms.items(), key=lambda var_coef_pair: var_coef_pair[0].id 1008 ): 1009 result += coef * variable_values[var] 1010 return result 1011 1012 def _flatten_once_and_add_to( 1013 self, 1014 scale: float, 1015 processed_elements: _ProcessedElements, 1016 target_stack: _ToProcessElements, 1017 ) -> None: 1018 for var, val in self._terms.items(): 1019 processed_elements.terms[var] += val * scale 1020 processed_elements.offset += scale * self.offset 1021 1022 # TODO(b/216492143): change __str__ to match C++ implementation in 1023 # cl/421649402. 1024 def __str__(self): 1025 """Returns the name, or a string containing the id if the name is empty.""" 1026 result = str(self.offset) 1027 sorted_keys = sorted(self._terms.keys(), key=str) 1028 for variable in sorted_keys: 1029 # TODO(b/216492143): consider how to better deal with `NaN` and try to 1030 # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in 1031 # linear_expression_test.py. 1032 coefficient = self._terms[variable] 1033 if coefficient == 0.0: 1034 continue 1035 if coefficient > 0: 1036 result += " + " 1037 else: 1038 result += " - " 1039 result += str(abs(coefficient)) + " * " + str(variable) 1040 return result 1041 1042 def __repr__(self): 1043 result = f"LinearExpression({self.offset}, " + "{" 1044 result += ", ".join( 1045 f"{variable!r}: {coefficient}" 1046 for variable, coefficient in self._terms.items() 1047 ) 1048 result += "})" 1049 return result
For variables x, an expression: b + sum_{i in I} a_i * x_i.
This class is immutable.
964 def __init__(self, /, other: LinearTypes = 0) -> None: 965 self._offset: float = 0.0 966 if isinstance(other, (int, float)): 967 self._offset = float(other) 968 self._terms: Mapping[Variable, float] = immutabledict.immutabledict() 969 return 970 971 to_process: _LinearToProcessElements = _LinearToProcessElements(other, 1.0) 972 processed_elements = _ProcessedElements() 973 while to_process: 974 linear, coef = to_process.pop() 975 linear._flatten_once_and_add_to(coef, processed_elements, to_process) 976 # TODO(b/216492143): explore avoiding this copy. 977 self._terms: Mapping[Variable, float] = immutabledict.immutabledict( 978 processed_elements.terms 979 ) 980 self._offset = processed_elements.offset
990 def evaluate(self, variable_values: Mapping[Variable, float]) -> float: 991 """Returns the value of this expression for given variable values. 992 993 E.g. if this is 3 * x + 4 and variable_values = {x: 2.0}, then 994 evaluate(variable_values) equals 10.0. 995 996 See also mathopt.evaluate_expression(), which works on any type in 997 QuadraticTypes. 998 999 Args: 1000 variable_values: Must contain a value for every variable in expression. 1001 1002 Returns: 1003 The value of this expression when replacing variables by their value. 1004 """ 1005 result = self._offset 1006 for var, coef in sorted( 1007 self._terms.items(), key=lambda var_coef_pair: var_coef_pair[0].id 1008 ): 1009 result += coef * variable_values[var] 1010 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.
1052class QuadraticExpression(QuadraticBase): 1053 """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. 1054 1055 This class is immutable. 1056 """ 1057 1058 __slots__ = "__weakref__", "_linear_terms", "_quadratic_terms", "_offset" 1059 1060 # TODO(b/216492143): consider initializing from a dictionary. 1061 def __init__(self, other: QuadraticTypes) -> None: 1062 self._offset: float = 0.0 1063 if isinstance(other, (int, float)): 1064 self._offset = float(other) 1065 self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict() 1066 self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( 1067 immutabledict.immutabledict() 1068 ) 1069 return 1070 1071 to_process: _QuadraticToProcessElements = _QuadraticToProcessElements( 1072 other, 1.0 1073 ) 1074 processed_elements = _QuadraticProcessedElements() 1075 while to_process: 1076 linear_or_quadratic, coef = to_process.pop() 1077 if isinstance(linear_or_quadratic, LinearBase): 1078 linear_or_quadratic._flatten_once_and_add_to( 1079 coef, processed_elements, to_process 1080 ) 1081 else: 1082 linear_or_quadratic._quadratic_flatten_once_and_add_to( 1083 coef, processed_elements, to_process 1084 ) 1085 # TODO(b/216492143): explore avoiding this copy. 1086 self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict( 1087 processed_elements.terms 1088 ) 1089 self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( 1090 immutabledict.immutabledict(processed_elements.quadratic_terms) 1091 ) 1092 self._offset = processed_elements.offset 1093 1094 @property 1095 def linear_terms(self) -> Mapping[Variable, float]: 1096 return self._linear_terms 1097 1098 @property 1099 def quadratic_terms(self) -> Mapping[QuadraticTermKey, float]: 1100 return self._quadratic_terms 1101 1102 @property 1103 def offset(self) -> float: 1104 return self._offset 1105 1106 def evaluate(self, variable_values: Mapping[Variable, float]) -> float: 1107 """Returns the value of this expression for given variable values. 1108 1109 E.g. if this is 3 * x * x + 4 and variable_values = {x: 2.0}, then 1110 evaluate(variable_values) equals 16.0. 1111 1112 See also mathopt.evaluate_expression(), which works on any type in 1113 QuadraticTypes. 1114 1115 Args: 1116 variable_values: Must contain a value for every variable in expression. 1117 1118 Returns: 1119 The value of this expression when replacing variables by their value. 1120 """ 1121 result = self._offset 1122 for var, coef in sorted( 1123 self._linear_terms.items(), 1124 key=lambda var_coef_pair: var_coef_pair[0].id, 1125 ): 1126 result += coef * variable_values[var] 1127 for key, coef in sorted( 1128 self._quadratic_terms.items(), 1129 key=lambda quad_coef_pair: ( 1130 quad_coef_pair[0].first_var.id, 1131 quad_coef_pair[0].second_var.id, 1132 ), 1133 ): 1134 result += ( 1135 coef * variable_values[key.first_var] * variable_values[key.second_var] 1136 ) 1137 return result 1138 1139 def _quadratic_flatten_once_and_add_to( 1140 self, 1141 scale: float, 1142 processed_elements: _QuadraticProcessedElements, 1143 target_stack: _QuadraticToProcessElements, 1144 ) -> None: 1145 for var, val in self._linear_terms.items(): 1146 processed_elements.terms[var] += val * scale 1147 for key, val in self._quadratic_terms.items(): 1148 processed_elements.quadratic_terms[key] += val * scale 1149 processed_elements.offset += scale * self.offset 1150 1151 # TODO(b/216492143): change __str__ to match C++ implementation in 1152 # cl/421649402. 1153 def __str__(self): 1154 result = str(self.offset) 1155 sorted_linear_keys = sorted(self._linear_terms.keys(), key=str) 1156 for variable in sorted_linear_keys: 1157 # TODO(b/216492143): consider how to better deal with `NaN` and try to 1158 # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in 1159 # linear_expression_test.py. 1160 coefficient = self._linear_terms[variable] 1161 if coefficient == 0.0: 1162 continue 1163 if coefficient > 0: 1164 result += " + " 1165 else: 1166 result += " - " 1167 result += str(abs(coefficient)) + " * " + str(variable) 1168 sorted_quadratic_keys = sorted(self._quadratic_terms.keys(), key=str) 1169 for key in sorted_quadratic_keys: 1170 # TODO(b/216492143): consider how to better deal with `NaN` and try to 1171 # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in 1172 # linear_expression_test.py. 1173 coefficient = self._quadratic_terms[key] 1174 if coefficient == 0.0: 1175 continue 1176 if coefficient > 0: 1177 result += " + " 1178 else: 1179 result += " - " 1180 result += str(abs(coefficient)) + " * " + str(key) 1181 return result 1182 1183 def __repr__(self): 1184 result = f"QuadraticExpression({self.offset}, " + "{" 1185 result += ", ".join( 1186 f"{variable!r}: {coefficient}" 1187 for variable, coefficient in self._linear_terms.items() 1188 ) 1189 result += "}, {" 1190 result += ", ".join( 1191 f"{key!r}: {coefficient}" 1192 for key, coefficient in self._quadratic_terms.items() 1193 ) 1194 result += "})" 1195 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.
1061 def __init__(self, other: QuadraticTypes) -> None: 1062 self._offset: float = 0.0 1063 if isinstance(other, (int, float)): 1064 self._offset = float(other) 1065 self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict() 1066 self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( 1067 immutabledict.immutabledict() 1068 ) 1069 return 1070 1071 to_process: _QuadraticToProcessElements = _QuadraticToProcessElements( 1072 other, 1.0 1073 ) 1074 processed_elements = _QuadraticProcessedElements() 1075 while to_process: 1076 linear_or_quadratic, coef = to_process.pop() 1077 if isinstance(linear_or_quadratic, LinearBase): 1078 linear_or_quadratic._flatten_once_and_add_to( 1079 coef, processed_elements, to_process 1080 ) 1081 else: 1082 linear_or_quadratic._quadratic_flatten_once_and_add_to( 1083 coef, processed_elements, to_process 1084 ) 1085 # TODO(b/216492143): explore avoiding this copy. 1086 self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict( 1087 processed_elements.terms 1088 ) 1089 self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( 1090 immutabledict.immutabledict(processed_elements.quadratic_terms) 1091 ) 1092 self._offset = processed_elements.offset
1106 def evaluate(self, variable_values: Mapping[Variable, float]) -> float: 1107 """Returns the value of this expression for given variable values. 1108 1109 E.g. if this is 3 * x * x + 4 and variable_values = {x: 2.0}, then 1110 evaluate(variable_values) equals 16.0. 1111 1112 See also mathopt.evaluate_expression(), which works on any type in 1113 QuadraticTypes. 1114 1115 Args: 1116 variable_values: Must contain a value for every variable in expression. 1117 1118 Returns: 1119 The value of this expression when replacing variables by their value. 1120 """ 1121 result = self._offset 1122 for var, coef in sorted( 1123 self._linear_terms.items(), 1124 key=lambda var_coef_pair: var_coef_pair[0].id, 1125 ): 1126 result += coef * variable_values[var] 1127 for key, coef in sorted( 1128 self._quadratic_terms.items(), 1129 key=lambda quad_coef_pair: ( 1130 quad_coef_pair[0].first_var.id, 1131 quad_coef_pair[0].second_var.id, 1132 ), 1133 ): 1134 result += ( 1135 coef * variable_values[key.first_var] * variable_values[key.second_var] 1136 ) 1137 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.
1198class LinearConstraint: 1199 """A linear constraint for an optimization model. 1200 1201 A LinearConstraint adds the following restriction on feasible solutions to an 1202 optimization model: 1203 lb <= sum_{i in I} a_i * x_i <= ub 1204 where x_i are the decision variables of the problem. lb == ub is allowed, this 1205 models the equality constraint: 1206 sum_{i in I} a_i * x_i == b 1207 lb > ub is also allowed, but the optimization problem will be infeasible. 1208 1209 A LinearConstraint can be configured as follows: 1210 * lower_bound: a float property, lb above. Should not be NaN nor +inf. 1211 * upper_bound: a float property, ub above. Should not be NaN nor -inf. 1212 * set_coefficient() and get_coefficient(): get and set the a_i * x_i 1213 terms. The variable must be from the same model as this constraint, and 1214 the a_i must be finite and not NaN. The coefficient for any variable not 1215 set is 0.0, and setting a coefficient to 0.0 removes it from I above. 1216 1217 The name is optional, read only, and used only for debugging. Non-empty names 1218 should be distinct. 1219 1220 Every LinearConstraint is associated with a Model (defined below). Note that 1221 data describing the linear constraint (e.g. lower_bound) is owned by 1222 Model.storage, this class is simply a reference to that data. Do not create a 1223 LinearConstraint directly, use Model.add_linear_constraint() instead. 1224 """ 1225 1226 __slots__ = "__weakref__", "_model", "_id" 1227 1228 def __init__(self, model: "Model", cid: int) -> None: 1229 """Do not invoke directly, use Model.add_linear_constraint().""" 1230 self._model: "Model" = model 1231 self._id: int = cid 1232 1233 @property 1234 def lower_bound(self) -> float: 1235 return self.model.storage.get_linear_constraint_lb(self._id) 1236 1237 @lower_bound.setter 1238 def lower_bound(self, value: float) -> None: 1239 self.model.storage.set_linear_constraint_lb(self._id, value) 1240 1241 @property 1242 def upper_bound(self) -> float: 1243 return self.model.storage.get_linear_constraint_ub(self._id) 1244 1245 @upper_bound.setter 1246 def upper_bound(self, value: float) -> None: 1247 self.model.storage.set_linear_constraint_ub(self._id, value) 1248 1249 @property 1250 def name(self) -> str: 1251 return self.model.storage.get_linear_constraint_name(self._id) 1252 1253 @property 1254 def id(self) -> int: 1255 return self._id 1256 1257 @property 1258 def model(self) -> "Model": 1259 return self._model 1260 1261 def set_coefficient(self, variable: Variable, coefficient: float) -> None: 1262 self.model.check_compatible(variable) 1263 self.model.storage.set_linear_constraint_coefficient( 1264 self._id, variable.id, coefficient 1265 ) 1266 1267 def get_coefficient(self, variable: Variable) -> float: 1268 self.model.check_compatible(variable) 1269 return self.model.storage.get_linear_constraint_coefficient( 1270 self._id, variable.id 1271 ) 1272 1273 def terms(self) -> Iterator[LinearTerm]: 1274 """Yields the variable/coefficient pairs with nonzero coefficient for this linear constraint.""" 1275 for variable in self.model.row_nonzeros(self): 1276 yield LinearTerm( 1277 variable=variable, 1278 coefficient=self.model.storage.get_linear_constraint_coefficient( 1279 self._id, variable.id 1280 ), 1281 ) 1282 1283 def as_bounded_linear_expression(self) -> BoundedLinearExpression: 1284 """Returns the bounded expression from lower_bound, upper_bound and terms.""" 1285 return BoundedLinearExpression( 1286 self.lower_bound, LinearSum(self.terms()), self.upper_bound 1287 ) 1288 1289 def __str__(self): 1290 """Returns the name, or a string containing the id if the name is empty.""" 1291 return self.name if self.name else f"linear_constraint_{self.id}" 1292 1293 def __repr__(self): 1294 return f"<LinearConstraint id: {self.id}, name: {self.name!r}>"
A linear constraint for an optimization model.
A LinearConstraint adds the following restriction on feasible solutions to an optimization model: lb <= sum_{i in I} a_i * x_i <= ub where x_i are the decision variables of the problem. lb == ub is allowed, this models the equality constraint: sum_{i in I} a_i * x_i == b lb > ub is also allowed, but the optimization problem will be infeasible.
A LinearConstraint can be 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.
- set_coefficient() and get_coefficient(): get and set the a_i * x_i terms. The variable must be from the same model as this constraint, and the a_i must be finite and not NaN. The coefficient for any variable not set is 0.0, and setting a coefficient to 0.0 removes it from I above.
The name is optional, read only, and used only for debugging. Non-empty names should be distinct.
Every LinearConstraint is associated with a Model (defined below). Note that data describing the linear constraint (e.g. lower_bound) is owned by Model.storage, this class is simply a reference to that data. Do not create a LinearConstraint directly, use Model.add_linear_constraint() instead.
1228 def __init__(self, model: "Model", cid: int) -> None: 1229 """Do not invoke directly, use Model.add_linear_constraint().""" 1230 self._model: "Model" = model 1231 self._id: int = cid
Do not invoke directly, use Model.add_linear_constraint().
1273 def terms(self) -> Iterator[LinearTerm]: 1274 """Yields the variable/coefficient pairs with nonzero coefficient for this linear constraint.""" 1275 for variable in self.model.row_nonzeros(self): 1276 yield LinearTerm( 1277 variable=variable, 1278 coefficient=self.model.storage.get_linear_constraint_coefficient( 1279 self._id, variable.id 1280 ), 1281 )
Yields the variable/coefficient pairs with nonzero coefficient for this linear constraint.
1283 def as_bounded_linear_expression(self) -> BoundedLinearExpression: 1284 """Returns the bounded expression from lower_bound, upper_bound and terms.""" 1285 return BoundedLinearExpression( 1286 self.lower_bound, LinearSum(self.terms()), self.upper_bound 1287 )
Returns the bounded expression from lower_bound, upper_bound and terms.
1297class Objective: 1298 """The objective for an optimization model. 1299 1300 An objective is either of the form: 1301 min o + sum_{i in I} c_i * x_i + sum_{i, j in I, i <= j} q_i,j * x_i * x_j 1302 or 1303 max o + sum_{i in I} c_i * x_i + sum_{(i, j) in Q} q_i,j * x_i * x_j 1304 where x_i are the decision variables of the problem and where all pairs (i, j) 1305 in Q satisfy i <= j. The values of o, c_i and q_i,j should be finite and not 1306 NaN. 1307 1308 The objective can be configured as follows: 1309 * offset: a float property, o above. Should be finite and not NaN. 1310 * is_maximize: a bool property, if the objective is to maximize or minimize. 1311 * set_linear_coefficient and get_linear_coefficient control the c_i * x_i 1312 terms. The variables must be from the same model as this objective, and 1313 the c_i must be finite and not NaN. The coefficient for any variable not 1314 set is 0.0, and setting a coefficient to 0.0 removes it from I above. 1315 * set_quadratic_coefficient and get_quadratic_coefficient control the 1316 q_i,j * x_i * x_j terms. The variables must be from the same model as this 1317 objective, and the q_i,j must be finite and not NaN. The coefficient for 1318 any pair of variables not set is 0.0, and setting a coefficient to 0.0 1319 removes the associated (i,j) from Q above. 1320 1321 Every Objective is associated with a Model (defined below). Note that data 1322 describing the objective (e.g. offset) is owned by Model.storage, this class 1323 is simply a reference to that data. Do not create an Objective directly, 1324 use Model.objective to access the objective instead. 1325 1326 The objective will be linear if only linear coefficients are set. This can be 1327 useful to avoid solve-time errors with solvers that do not accept quadratic 1328 objectives. To facilitate this linear objective guarantee we provide three 1329 functions to add to the objective: 1330 * add(), which accepts linear or quadratic expressions, 1331 * add_quadratic(), which also accepts linear or quadratic expressions and 1332 can be used to signal a quadratic objective is possible, and 1333 * add_linear(), which only accepts linear expressions and can be used to 1334 guarantee the objective remains linear. 1335 """ 1336 1337 __slots__ = ("_model",) 1338 1339 def __init__(self, model: "Model") -> None: 1340 """Do no invoke directly, use Model.objective.""" 1341 self._model: "Model" = model 1342 1343 @property 1344 def is_maximize(self) -> bool: 1345 return self.model.storage.get_is_maximize() 1346 1347 @is_maximize.setter 1348 def is_maximize(self, is_maximize: bool) -> None: 1349 self.model.storage.set_is_maximize(is_maximize) 1350 1351 @property 1352 def offset(self) -> float: 1353 return self.model.storage.get_objective_offset() 1354 1355 @offset.setter 1356 def offset(self, value: float) -> None: 1357 self.model.storage.set_objective_offset(value) 1358 1359 @property 1360 def model(self) -> "Model": 1361 return self._model 1362 1363 def set_linear_coefficient(self, variable: Variable, coef: float) -> None: 1364 self.model.check_compatible(variable) 1365 self.model.storage.set_linear_objective_coefficient(variable.id, coef) 1366 1367 def get_linear_coefficient(self, variable: Variable) -> float: 1368 self.model.check_compatible(variable) 1369 return self.model.storage.get_linear_objective_coefficient(variable.id) 1370 1371 def linear_terms(self) -> Iterator[LinearTerm]: 1372 """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.""" 1373 yield from self.model.linear_objective_terms() 1374 1375 def add(self, objective: QuadraticTypes) -> None: 1376 """Adds the provided expression `objective` to the objective function. 1377 1378 To ensure the objective remains linear through type checks, use 1379 add_linear(). 1380 1381 Args: 1382 objective: the expression to add to the objective function. 1383 """ 1384 if isinstance(objective, (LinearBase, int, float)): 1385 self.add_linear(objective) 1386 elif isinstance(objective, QuadraticBase): 1387 self.add_quadratic(objective) 1388 else: 1389 raise TypeError( 1390 "unsupported type in objective argument for " 1391 f"Objective.add(): {type(objective).__name__!r}" 1392 ) 1393 1394 def add_linear(self, objective: LinearTypes) -> None: 1395 """Adds the provided linear expression `objective` to the objective function.""" 1396 if not isinstance(objective, (LinearBase, int, float)): 1397 raise TypeError( 1398 "unsupported type in objective argument for " 1399 f"Objective.add_linear(): {type(objective).__name__!r}" 1400 ) 1401 objective_expr = as_flat_linear_expression(objective) 1402 self.offset += objective_expr.offset 1403 for var, coefficient in objective_expr.terms.items(): 1404 self.set_linear_coefficient( 1405 var, self.get_linear_coefficient(var) + coefficient 1406 ) 1407 1408 def add_quadratic(self, objective: QuadraticTypes) -> None: 1409 """Adds the provided quadratic expression `objective` to the objective function.""" 1410 if not isinstance(objective, (QuadraticBase, LinearBase, int, float)): 1411 raise TypeError( 1412 "unsupported type in objective argument for " 1413 f"Objective.add(): {type(objective).__name__!r}" 1414 ) 1415 objective_expr = as_flat_quadratic_expression(objective) 1416 self.offset += objective_expr.offset 1417 for var, coefficient in objective_expr.linear_terms.items(): 1418 self.set_linear_coefficient( 1419 var, self.get_linear_coefficient(var) + coefficient 1420 ) 1421 for key, coefficient in objective_expr.quadratic_terms.items(): 1422 self.set_quadratic_coefficient( 1423 key.first_var, 1424 key.second_var, 1425 self.get_quadratic_coefficient(key.first_var, key.second_var) 1426 + coefficient, 1427 ) 1428 1429 def set_quadratic_coefficient( 1430 self, first_variable: Variable, second_variable: Variable, coef: float 1431 ) -> None: 1432 self.model.check_compatible(first_variable) 1433 self.model.check_compatible(second_variable) 1434 self.model.storage.set_quadratic_objective_coefficient( 1435 first_variable.id, second_variable.id, coef 1436 ) 1437 1438 def get_quadratic_coefficient( 1439 self, first_variable: Variable, second_variable: Variable 1440 ) -> float: 1441 self.model.check_compatible(first_variable) 1442 self.model.check_compatible(second_variable) 1443 return self.model.storage.get_quadratic_objective_coefficient( 1444 first_variable.id, second_variable.id 1445 ) 1446 1447 def quadratic_terms(self) -> Iterator[QuadraticTerm]: 1448 """Yields quadratic terms with nonzero objective coefficient in undefined order.""" 1449 yield from self.model.quadratic_objective_terms() 1450 1451 def as_linear_expression(self) -> LinearExpression: 1452 if any(self.quadratic_terms()): 1453 raise TypeError("Cannot get a quadratic objective as a linear expression") 1454 return as_flat_linear_expression(self.offset + LinearSum(self.linear_terms())) 1455 1456 def as_quadratic_expression(self) -> QuadraticExpression: 1457 return as_flat_quadratic_expression( 1458 self.offset 1459 + LinearSum(self.linear_terms()) 1460 + QuadraticSum(self.quadratic_terms()) 1461 ) 1462 1463 def clear(self) -> None: 1464 """Clears objective coefficients and offset. Does not change direction.""" 1465 self.model.storage.clear_objective()
The objective for an optimization model.
An objective is either of the form:
min o + sum_{i in I} c_i * x_i + sum_{i, j in I, i <= j} q_i,j * x_i * x_j
or max o + sum_{i in I} c_i * x_i + sum_{(i, j) in Q} q_i,j * x_i * x_j where x_i are the decision variables of the problem and where all pairs (i, j) in Q satisfy i <= j. The values of o, c_i and q_i,j should be finite and not NaN.
The objective can be configured as follows:
- offset: a float property, o above. Should be finite and not NaN.
- is_maximize: a bool property, if the objective is to maximize or minimize.
- set_linear_coefficient and get_linear_coefficient control the c_i * x_i terms. The variables must be from the same model as this objective, and the c_i must be finite and not NaN. The coefficient for any variable not set is 0.0, and setting a coefficient to 0.0 removes it from I above.
- set_quadratic_coefficient and get_quadratic_coefficient control the q_i,j * x_i * x_j terms. The variables must be from the same model as this objective, and the q_i,j must be finite and not NaN. The coefficient for any pair of variables not set is 0.0, and setting a coefficient to 0.0 removes the associated (i,j) from Q above.
Every Objective is associated with a Model (defined below). Note that data describing the objective (e.g. offset) is owned by Model.storage, this class is simply a reference to that data. Do not create an Objective directly, use Model.objective to access the objective instead.
The objective will be linear if only linear coefficients are set. This can be useful to avoid solve-time errors with solvers that do not accept quadratic objectives. To facilitate this linear objective guarantee we provide three functions to add to the objective:
- add(), which accepts linear or quadratic expressions,
- add_quadratic(), which also accepts linear or quadratic expressions and can be used to signal a quadratic objective is possible, and
- add_linear(), which only accepts linear expressions and can be used to guarantee the objective remains linear.
1339 def __init__(self, model: "Model") -> None: 1340 """Do no invoke directly, use Model.objective.""" 1341 self._model: "Model" = model
Do no invoke directly, use Model.objective.
1371 def linear_terms(self) -> Iterator[LinearTerm]: 1372 """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.""" 1373 yield from self.model.linear_objective_terms()
Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.
1375 def add(self, objective: QuadraticTypes) -> None: 1376 """Adds the provided expression `objective` to the objective function. 1377 1378 To ensure the objective remains linear through type checks, use 1379 add_linear(). 1380 1381 Args: 1382 objective: the expression to add to the objective function. 1383 """ 1384 if isinstance(objective, (LinearBase, int, float)): 1385 self.add_linear(objective) 1386 elif isinstance(objective, QuadraticBase): 1387 self.add_quadratic(objective) 1388 else: 1389 raise TypeError( 1390 "unsupported type in objective argument for " 1391 f"Objective.add(): {type(objective).__name__!r}" 1392 )
Adds the provided expression objective
to the objective function.
To ensure the objective remains linear through type checks, use add_linear().
Arguments:
- objective: the expression to add to the objective function.
1394 def add_linear(self, objective: LinearTypes) -> None: 1395 """Adds the provided linear expression `objective` to the objective function.""" 1396 if not isinstance(objective, (LinearBase, int, float)): 1397 raise TypeError( 1398 "unsupported type in objective argument for " 1399 f"Objective.add_linear(): {type(objective).__name__!r}" 1400 ) 1401 objective_expr = as_flat_linear_expression(objective) 1402 self.offset += objective_expr.offset 1403 for var, coefficient in objective_expr.terms.items(): 1404 self.set_linear_coefficient( 1405 var, self.get_linear_coefficient(var) + coefficient 1406 )
Adds the provided linear expression objective
to the objective function.
1408 def add_quadratic(self, objective: QuadraticTypes) -> None: 1409 """Adds the provided quadratic expression `objective` to the objective function.""" 1410 if not isinstance(objective, (QuadraticBase, LinearBase, int, float)): 1411 raise TypeError( 1412 "unsupported type in objective argument for " 1413 f"Objective.add(): {type(objective).__name__!r}" 1414 ) 1415 objective_expr = as_flat_quadratic_expression(objective) 1416 self.offset += objective_expr.offset 1417 for var, coefficient in objective_expr.linear_terms.items(): 1418 self.set_linear_coefficient( 1419 var, self.get_linear_coefficient(var) + coefficient 1420 ) 1421 for key, coefficient in objective_expr.quadratic_terms.items(): 1422 self.set_quadratic_coefficient( 1423 key.first_var, 1424 key.second_var, 1425 self.get_quadratic_coefficient(key.first_var, key.second_var) 1426 + coefficient, 1427 )
Adds the provided quadratic expression objective
to the objective function.
1429 def set_quadratic_coefficient( 1430 self, first_variable: Variable, second_variable: Variable, coef: float 1431 ) -> None: 1432 self.model.check_compatible(first_variable) 1433 self.model.check_compatible(second_variable) 1434 self.model.storage.set_quadratic_objective_coefficient( 1435 first_variable.id, second_variable.id, coef 1436 )
1438 def get_quadratic_coefficient( 1439 self, first_variable: Variable, second_variable: Variable 1440 ) -> float: 1441 self.model.check_compatible(first_variable) 1442 self.model.check_compatible(second_variable) 1443 return self.model.storage.get_quadratic_objective_coefficient( 1444 first_variable.id, second_variable.id 1445 )
1447 def quadratic_terms(self) -> Iterator[QuadraticTerm]: 1448 """Yields quadratic terms with nonzero objective coefficient in undefined order.""" 1449 yield from self.model.quadratic_objective_terms()
Yields quadratic terms with nonzero objective coefficient in undefined order.
1468class LinearConstraintMatrixEntry(NamedTuple): 1469 linear_constraint: LinearConstraint 1470 variable: Variable 1471 coefficient: float
LinearConstraintMatrixEntry(linear_constraint, variable, coefficient)
Create new instance of LinearConstraintMatrixEntry(linear_constraint, variable, coefficient)
Inherited Members
- builtins.tuple
- index
- count
1474class UpdateTracker: 1475 """Tracks updates to an optimization model from a ModelStorage. 1476 1477 Do not instantiate directly, instead create through 1478 ModelStorage.add_update_tracker(). 1479 1480 Querying an UpdateTracker after calling Model.remove_update_tracker will 1481 result in a model_storage.UsedUpdateTrackerAfterRemovalError. 1482 1483 Example: 1484 mod = Model() 1485 x = mod.add_variable(0.0, 1.0, True, 'x') 1486 y = mod.add_variable(0.0, 1.0, True, 'y') 1487 tracker = mod.add_update_tracker() 1488 mod.set_variable_ub(x, 3.0) 1489 tracker.export_update() 1490 => "variable_updates: {upper_bounds: {ids: [0], values[3.0] }" 1491 mod.set_variable_ub(y, 2.0) 1492 tracker.export_update() 1493 => "variable_updates: {upper_bounds: {ids: [0, 1], values[3.0, 2.0] }" 1494 tracker.advance_checkpoint() 1495 tracker.export_update() 1496 => None 1497 mod.set_variable_ub(y, 4.0) 1498 tracker.export_update() 1499 => "variable_updates: {upper_bounds: {ids: [1], values[4.0] }" 1500 tracker.advance_checkpoint() 1501 mod.remove_update_tracker(tracker) 1502 """ 1503 1504 def __init__(self, storage_update_tracker: model_storage.StorageUpdateTracker): 1505 """Do not invoke directly, use Model.add_update_tracker() instead.""" 1506 self.storage_update_tracker = storage_update_tracker 1507 1508 def export_update(self) -> Optional[model_update_pb2.ModelUpdateProto]: 1509 """Returns changes to the model since last call to checkpoint/creation.""" 1510 return self.storage_update_tracker.export_update() 1511 1512 def advance_checkpoint(self) -> None: 1513 """Track changes to the model only after this function call.""" 1514 return self.storage_update_tracker.advance_checkpoint()
Tracks updates to an optimization model from a ModelStorage.
Do not instantiate directly, instead create through ModelStorage.add_update_tracker().
Querying an UpdateTracker after calling Model.remove_update_tracker will result in a model_storage.UsedUpdateTrackerAfterRemovalError.
Example:
mod = Model() x = mod.add_variable(0.0, 1.0, True, 'x') y = mod.add_variable(0.0, 1.0, True, 'y') tracker = mod.add_update_tracker() mod.set_variable_ub(x, 3.0) tracker.export_update() => "variable_updates: {upper_bounds: {ids: [0], values[3.0] }" mod.set_variable_ub(y, 2.0) tracker.export_update() => "variable_updates: {upper_bounds: {ids: [0, 1], values[3.0, 2.0] }" tracker.advance_checkpoint() tracker.export_update() => None mod.set_variable_ub(y, 4.0) tracker.export_update() => "variable_updates: {upper_bounds: {ids: [1], values[4.0] }" tracker.advance_checkpoint() mod.remove_update_tracker(tracker)
1504 def __init__(self, storage_update_tracker: model_storage.StorageUpdateTracker): 1505 """Do not invoke directly, use Model.add_update_tracker() instead.""" 1506 self.storage_update_tracker = storage_update_tracker
Do not invoke directly, use Model.add_update_tracker() instead.
1517class Model: 1518 """An optimization model. 1519 1520 The objective function of the model can be linear or quadratic, and some 1521 solvers can only handle linear objective functions. For this reason Model has 1522 three versions of all objective setting functions: 1523 * A generic one (e.g. maximize()), which accepts linear or quadratic 1524 expressions, 1525 * a quadratic version (e.g. maximize_quadratic_objective()), which also 1526 accepts linear or quadratic expressions and can be used to signal a 1527 quadratic objective is possible, and 1528 * a linear version (e.g. maximize_linear_objective()), which only accepts 1529 linear expressions and can be used to avoid solve time errors for solvers 1530 that do not accept quadratic objectives. 1531 1532 Attributes: 1533 name: A description of the problem, can be empty. 1534 objective: A function to maximize or minimize. 1535 storage: Implementation detail, do not access directly. 1536 _variable_ids: Maps variable ids to Variable objects. 1537 _linear_constraint_ids: Maps linear constraint ids to LinearConstraint 1538 objects. 1539 """ 1540 1541 __slots__ = "storage", "_variable_ids", "_linear_constraint_ids", "_objective" 1542 1543 def __init__( 1544 self, 1545 *, 1546 name: str = "", 1547 storage_class: StorageClass = hash_model_storage.HashModelStorage, 1548 ) -> None: 1549 self.storage: Storage = storage_class(name) 1550 # Weak references are used to eliminate reference cycles (so that Model will 1551 # be destroyed when we reach zero references, instead of relying on GC cycle 1552 # detection). Do not access a variable or constraint from these maps 1553 # directly, as they may no longer exist, use _get_or_make_variable() and 1554 # _get_or_make_linear_constraint() defined below instead. 1555 self._variable_ids: weakref.WeakValueDictionary[int, Variable] = ( 1556 weakref.WeakValueDictionary() 1557 ) 1558 self._linear_constraint_ids: weakref.WeakValueDictionary[ 1559 int, LinearConstraint 1560 ] = weakref.WeakValueDictionary() 1561 self._objective: Objective = Objective(self) 1562 1563 @property 1564 def name(self) -> str: 1565 return self.storage.name 1566 1567 @property 1568 def objective(self) -> Objective: 1569 return self._objective 1570 1571 def add_variable( 1572 self, 1573 *, 1574 lb: float = -math.inf, 1575 ub: float = math.inf, 1576 is_integer: bool = False, 1577 name: str = "", 1578 ) -> Variable: 1579 """Adds a decision variable to the optimization model. 1580 1581 Args: 1582 lb: The new variable must take at least this value (a lower bound). 1583 ub: The new variable must be at most this value (an upper bound). 1584 is_integer: Indicates if the variable can only take integer values 1585 (otherwise, the variable can take any continuous value). 1586 name: For debugging purposes only, but nonempty names must be distinct. 1587 1588 Returns: 1589 A reference to the new decision variable. 1590 """ 1591 variable_id = self.storage.add_variable(lb, ub, is_integer, name) 1592 result = Variable(self, variable_id) 1593 self._variable_ids[variable_id] = result 1594 return result 1595 1596 def add_integer_variable( 1597 self, *, lb: float = -math.inf, ub: float = math.inf, name: str = "" 1598 ) -> Variable: 1599 return self.add_variable(lb=lb, ub=ub, is_integer=True, name=name) 1600 1601 def add_binary_variable(self, *, name: str = "") -> Variable: 1602 return self.add_variable(lb=0.0, ub=1.0, is_integer=True, name=name) 1603 1604 def get_variable(self, var_id: int) -> Variable: 1605 """Returns the Variable for the id var_id, or raises KeyError.""" 1606 if not self.storage.variable_exists(var_id): 1607 raise KeyError(f"variable does not exist with id {var_id}") 1608 return self._get_or_make_variable(var_id) 1609 1610 def delete_variable(self, variable: Variable) -> None: 1611 self.check_compatible(variable) 1612 self.storage.delete_variable(variable.id) 1613 del self._variable_ids[variable.id] 1614 1615 def variables(self) -> Iterator[Variable]: 1616 """Yields the variables in the order of creation.""" 1617 for var_id in self.storage.get_variables(): 1618 yield self._get_or_make_variable(var_id) 1619 1620 def maximize(self, objective: QuadraticTypes) -> None: 1621 """Sets the objective to maximize the provided expression `objective`.""" 1622 self.set_objective(objective, is_maximize=True) 1623 1624 def maximize_linear_objective(self, objective: LinearTypes) -> None: 1625 """Sets the objective to maximize the provided linear expression `objective`.""" 1626 self.set_linear_objective(objective, is_maximize=True) 1627 1628 def maximize_quadratic_objective(self, objective: QuadraticTypes) -> None: 1629 """Sets the objective to maximize the provided quadratic expression `objective`.""" 1630 self.set_quadratic_objective(objective, is_maximize=True) 1631 1632 def minimize(self, objective: QuadraticTypes) -> None: 1633 """Sets the objective to minimize the provided expression `objective`.""" 1634 self.set_objective(objective, is_maximize=False) 1635 1636 def minimize_linear_objective(self, objective: LinearTypes) -> None: 1637 """Sets the objective to minimize the provided linear expression `objective`.""" 1638 self.set_linear_objective(objective, is_maximize=False) 1639 1640 def minimize_quadratic_objective(self, objective: QuadraticTypes) -> None: 1641 """Sets the objective to minimize the provided quadratic expression `objective`.""" 1642 self.set_quadratic_objective(objective, is_maximize=False) 1643 1644 def set_objective(self, objective: QuadraticTypes, *, is_maximize: bool) -> None: 1645 """Sets the objective to optimize the provided expression `objective`.""" 1646 if isinstance(objective, (LinearBase, int, float)): 1647 self.set_linear_objective(objective, is_maximize=is_maximize) 1648 elif isinstance(objective, QuadraticBase): 1649 self.set_quadratic_objective(objective, is_maximize=is_maximize) 1650 else: 1651 raise TypeError( 1652 "unsupported type in objective argument for " 1653 f"set_objective: {type(objective).__name__!r}" 1654 ) 1655 1656 def set_linear_objective( 1657 self, objective: LinearTypes, *, is_maximize: bool 1658 ) -> None: 1659 """Sets the objective to optimize the provided linear expression `objective`.""" 1660 if not isinstance(objective, (LinearBase, int, float)): 1661 raise TypeError( 1662 "unsupported type in objective argument for " 1663 f"set_linear_objective: {type(objective).__name__!r}" 1664 ) 1665 self.storage.clear_objective() 1666 self._objective.is_maximize = is_maximize 1667 objective_expr = as_flat_linear_expression(objective) 1668 self._objective.offset = objective_expr.offset 1669 for var, coefficient in objective_expr.terms.items(): 1670 self._objective.set_linear_coefficient(var, coefficient) 1671 1672 def set_quadratic_objective( 1673 self, objective: QuadraticTypes, *, is_maximize: bool 1674 ) -> None: 1675 """Sets the objective to optimize the provided linear expression `objective`.""" 1676 if not isinstance(objective, (QuadraticBase, LinearBase, int, float)): 1677 raise TypeError( 1678 "unsupported type in objective argument for " 1679 f"set_quadratic_objective: {type(objective).__name__!r}" 1680 ) 1681 self.storage.clear_objective() 1682 self._objective.is_maximize = is_maximize 1683 objective_expr = as_flat_quadratic_expression(objective) 1684 self._objective.offset = objective_expr.offset 1685 for var, coefficient in objective_expr.linear_terms.items(): 1686 self._objective.set_linear_coefficient(var, coefficient) 1687 for key, coefficient in objective_expr.quadratic_terms.items(): 1688 self._objective.set_quadratic_coefficient( 1689 key.first_var, key.second_var, coefficient 1690 ) 1691 1692 # TODO(b/227214976): Update the note below and link to pytype bug number. 1693 # Note: bounded_expr's type includes bool only as a workaround to a pytype 1694 # issue. Passing a bool for bounded_expr will raise an error in runtime. 1695 def add_linear_constraint( 1696 self, 1697 bounded_expr: Optional[Union[bool, BoundedLinearTypes]] = None, 1698 *, 1699 lb: Optional[float] = None, 1700 ub: Optional[float] = None, 1701 expr: Optional[LinearTypes] = None, 1702 name: str = "", 1703 ) -> LinearConstraint: 1704 """Adds a linear constraint to the optimization model. 1705 1706 The simplest way to specify the constraint is by passing a one-sided or 1707 two-sided linear inequality as in: 1708 * add_linear_constraint(x + y + 1.0 <= 2.0), 1709 * add_linear_constraint(x + y >= 2.0), or 1710 * add_linear_constraint((1.0 <= x + y) <= 2.0). 1711 1712 Note the extra parenthesis for two-sided linear inequalities, which is 1713 required due to some language limitations (see 1714 https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). 1715 If the parenthesis are omitted, a TypeError will be raised explaining the 1716 issue (if this error was not raised the first inequality would have been 1717 silently ignored because of the noted language limitations). 1718 1719 The second way to specify the constraint is by setting lb, ub, and/o expr as 1720 in: 1721 * add_linear_constraint(expr=x + y + 1.0, ub=2.0), 1722 * add_linear_constraint(expr=x + y, lb=2.0), 1723 * add_linear_constraint(expr=x + y, lb=1.0, ub=2.0), or 1724 * add_linear_constraint(lb=1.0). 1725 Omitting lb is equivalent to setting it to -math.inf and omiting ub is 1726 equivalent to setting it to math.inf. 1727 1728 These two alternatives are exclusive and a combined call like: 1729 * add_linear_constraint(x + y <= 2.0, lb=1.0), or 1730 * add_linear_constraint(x + y <= 2.0, ub=math.inf) 1731 will raise a ValueError. A ValueError is also raised if expr's offset is 1732 infinite. 1733 1734 Args: 1735 bounded_expr: a linear inequality describing the constraint. Cannot be 1736 specified together with lb, ub, or expr. 1737 lb: The constraint's lower bound if bounded_expr is omitted (if both 1738 bounder_expr and lb are omitted, the lower bound is -math.inf). 1739 ub: The constraint's upper bound if bounded_expr is omitted (if both 1740 bounder_expr and ub are omitted, the upper bound is math.inf). 1741 expr: The constraint's linear expression if bounded_expr is omitted. 1742 name: For debugging purposes only, but nonempty names must be distinct. 1743 1744 Returns: 1745 A reference to the new linear constraint. 1746 """ 1747 normalized_inequality = as_normalized_linear_inequality( 1748 bounded_expr, lb=lb, ub=ub, expr=expr 1749 ) 1750 lin_con_id = self.storage.add_linear_constraint( 1751 normalized_inequality.lb, normalized_inequality.ub, name 1752 ) 1753 result = LinearConstraint(self, lin_con_id) 1754 self._linear_constraint_ids[lin_con_id] = result 1755 for var, coefficient in normalized_inequality.coefficients.items(): 1756 result.set_coefficient(var, coefficient) 1757 return result 1758 1759 def get_linear_constraint(self, lin_con_id: int) -> LinearConstraint: 1760 """Returns the LinearConstraint for the id lin_con_id.""" 1761 if not self.storage.linear_constraint_exists(lin_con_id): 1762 raise KeyError(f"linear constraint does not exist with id {lin_con_id}") 1763 return self._get_or_make_linear_constraint(lin_con_id) 1764 1765 def delete_linear_constraint(self, linear_constraint: LinearConstraint) -> None: 1766 self.check_compatible(linear_constraint) 1767 self.storage.delete_linear_constraint(linear_constraint.id) 1768 del self._linear_constraint_ids[linear_constraint.id] 1769 1770 def linear_constraints(self) -> Iterator[LinearConstraint]: 1771 """Yields the linear constraints in the order of creation.""" 1772 for lin_con_id in self.storage.get_linear_constraints(): 1773 yield self._get_or_make_linear_constraint(lin_con_id) 1774 1775 def row_nonzeros(self, linear_constraint: LinearConstraint) -> Iterator[Variable]: 1776 """Yields the variables with nonzero coefficient for this linear constraint.""" 1777 for var_id in self.storage.get_variables_for_linear_constraint( 1778 linear_constraint.id 1779 ): 1780 yield self._get_or_make_variable(var_id) 1781 1782 def column_nonzeros(self, variable: Variable) -> Iterator[LinearConstraint]: 1783 """Yields the linear constraints with nonzero coefficient for this variable.""" 1784 for lin_con_id in self.storage.get_linear_constraints_with_variable( 1785 variable.id 1786 ): 1787 yield self._get_or_make_linear_constraint(lin_con_id) 1788 1789 def linear_objective_terms(self) -> Iterator[LinearTerm]: 1790 """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.""" 1791 for term in self.storage.get_linear_objective_coefficients(): 1792 yield LinearTerm( 1793 variable=self._get_or_make_variable(term.variable_id), 1794 coefficient=term.coefficient, 1795 ) 1796 1797 def quadratic_objective_terms(self) -> Iterator[QuadraticTerm]: 1798 """Yields the quadratic terms with nonzero objective coefficient in undefined order.""" 1799 for term in self.storage.get_quadratic_objective_coefficients(): 1800 var1 = self._get_or_make_variable(term.id_key.id1) 1801 var2 = self._get_or_make_variable(term.id_key.id2) 1802 yield QuadraticTerm( 1803 key=QuadraticTermKey(var1, var2), coefficient=term.coefficient 1804 ) 1805 1806 def linear_constraint_matrix_entries( 1807 self, 1808 ) -> Iterator[LinearConstraintMatrixEntry]: 1809 """Yields the nonzero elements of the linear constraint matrix in undefined order.""" 1810 for entry in self.storage.get_linear_constraint_matrix_entries(): 1811 yield LinearConstraintMatrixEntry( 1812 linear_constraint=self._get_or_make_linear_constraint( 1813 entry.linear_constraint_id 1814 ), 1815 variable=self._get_or_make_variable(entry.variable_id), 1816 coefficient=entry.coefficient, 1817 ) 1818 1819 def export_model(self) -> model_pb2.ModelProto: 1820 return self.storage.export_model() 1821 1822 def add_update_tracker(self) -> UpdateTracker: 1823 """Creates an UpdateTracker registered on this model to view changes.""" 1824 return UpdateTracker(self.storage.add_update_tracker()) 1825 1826 def remove_update_tracker(self, tracker: UpdateTracker): 1827 """Stops tracker from getting updates on changes to this model. 1828 1829 An error will be raised if tracker was not created by this Model or if 1830 tracker has been previously removed. 1831 1832 Using (via checkpoint or update) an UpdateTracker after it has been removed 1833 will result in an error. 1834 1835 Args: 1836 tracker: The UpdateTracker to unregister. 1837 1838 Raises: 1839 KeyError: The tracker was created by another model or was already removed. 1840 """ 1841 self.storage.remove_update_tracker(tracker.storage_update_tracker) 1842 1843 def check_compatible( 1844 self, var_or_constraint: Union[Variable, LinearConstraint] 1845 ) -> None: 1846 """Raises a ValueError if the model of var_or_constraint is not self.""" 1847 if var_or_constraint.model is not self: 1848 raise ValueError( 1849 f"{var_or_constraint} is from model {var_or_constraint.model} and" 1850 f" cannot be used with model {self}" 1851 ) 1852 1853 def _get_or_make_variable(self, variable_id: int) -> Variable: 1854 result = self._variable_ids.get(variable_id) 1855 if result: 1856 return result 1857 result = Variable(self, variable_id) 1858 self._variable_ids[variable_id] = result 1859 return result 1860 1861 def _get_or_make_linear_constraint( 1862 self, linear_constraint_id: int 1863 ) -> LinearConstraint: 1864 result = self._linear_constraint_ids.get(linear_constraint_id) 1865 if result: 1866 return result 1867 result = LinearConstraint(self, linear_constraint_id) 1868 self._linear_constraint_ids[linear_constraint_id] = result 1869 return result
An optimization model.
The objective function of the model can be linear or quadratic, and some solvers can only handle linear objective functions. For this reason Model has three versions of all objective setting functions:
- A generic one (e.g. maximize()), which accepts linear or quadratic expressions,
- a quadratic version (e.g. maximize_quadratic_objective()), which also accepts linear or quadratic expressions and can be used to signal a quadratic objective is possible, and
- a linear version (e.g. maximize_linear_objective()), which only accepts linear expressions and can be used to avoid solve time errors for solvers that do not accept quadratic objectives.
Attributes:
- name: A description of the problem, can be empty.
- objective: A function to maximize or minimize.
- storage: Implementation detail, do not access directly.
- _variable_ids: Maps variable ids to Variable objects.
- _linear_constraint_ids: Maps linear constraint ids to LinearConstraint objects.
1543 def __init__( 1544 self, 1545 *, 1546 name: str = "", 1547 storage_class: StorageClass = hash_model_storage.HashModelStorage, 1548 ) -> None: 1549 self.storage: Storage = storage_class(name) 1550 # Weak references are used to eliminate reference cycles (so that Model will 1551 # be destroyed when we reach zero references, instead of relying on GC cycle 1552 # detection). Do not access a variable or constraint from these maps 1553 # directly, as they may no longer exist, use _get_or_make_variable() and 1554 # _get_or_make_linear_constraint() defined below instead. 1555 self._variable_ids: weakref.WeakValueDictionary[int, Variable] = ( 1556 weakref.WeakValueDictionary() 1557 ) 1558 self._linear_constraint_ids: weakref.WeakValueDictionary[ 1559 int, LinearConstraint 1560 ] = weakref.WeakValueDictionary() 1561 self._objective: Objective = Objective(self)
1571 def add_variable( 1572 self, 1573 *, 1574 lb: float = -math.inf, 1575 ub: float = math.inf, 1576 is_integer: bool = False, 1577 name: str = "", 1578 ) -> Variable: 1579 """Adds a decision variable to the optimization model. 1580 1581 Args: 1582 lb: The new variable must take at least this value (a lower bound). 1583 ub: The new variable must be at most this value (an upper bound). 1584 is_integer: Indicates if the variable can only take integer values 1585 (otherwise, the variable can take any continuous value). 1586 name: For debugging purposes only, but nonempty names must be distinct. 1587 1588 Returns: 1589 A reference to the new decision variable. 1590 """ 1591 variable_id = self.storage.add_variable(lb, ub, is_integer, name) 1592 result = Variable(self, variable_id) 1593 self._variable_ids[variable_id] = result 1594 return result
Adds a decision variable to the optimization model.
Arguments:
- lb: The new variable must take at least this value (a lower bound).
- ub: The new variable must be at most this value (an upper bound).
- is_integer: Indicates if the variable can only take integer values (otherwise, the variable can take any continuous value).
- name: For debugging purposes only, but nonempty names must be distinct.
Returns:
A reference to the new decision variable.
1604 def get_variable(self, var_id: int) -> Variable: 1605 """Returns the Variable for the id var_id, or raises KeyError.""" 1606 if not self.storage.variable_exists(var_id): 1607 raise KeyError(f"variable does not exist with id {var_id}") 1608 return self._get_or_make_variable(var_id)
Returns the Variable for the id var_id, or raises KeyError.
1615 def variables(self) -> Iterator[Variable]: 1616 """Yields the variables in the order of creation.""" 1617 for var_id in self.storage.get_variables(): 1618 yield self._get_or_make_variable(var_id)
Yields the variables in the order of creation.
1620 def maximize(self, objective: QuadraticTypes) -> None: 1621 """Sets the objective to maximize the provided expression `objective`.""" 1622 self.set_objective(objective, is_maximize=True)
Sets the objective to maximize the provided expression objective
.
1624 def maximize_linear_objective(self, objective: LinearTypes) -> None: 1625 """Sets the objective to maximize the provided linear expression `objective`.""" 1626 self.set_linear_objective(objective, is_maximize=True)
Sets the objective to maximize the provided linear expression objective
.
1628 def maximize_quadratic_objective(self, objective: QuadraticTypes) -> None: 1629 """Sets the objective to maximize the provided quadratic expression `objective`.""" 1630 self.set_quadratic_objective(objective, is_maximize=True)
Sets the objective to maximize the provided quadratic expression objective
.
1632 def minimize(self, objective: QuadraticTypes) -> None: 1633 """Sets the objective to minimize the provided expression `objective`.""" 1634 self.set_objective(objective, is_maximize=False)
Sets the objective to minimize the provided expression objective
.
1636 def minimize_linear_objective(self, objective: LinearTypes) -> None: 1637 """Sets the objective to minimize the provided linear expression `objective`.""" 1638 self.set_linear_objective(objective, is_maximize=False)
Sets the objective to minimize the provided linear expression objective
.
1640 def minimize_quadratic_objective(self, objective: QuadraticTypes) -> None: 1641 """Sets the objective to minimize the provided quadratic expression `objective`.""" 1642 self.set_quadratic_objective(objective, is_maximize=False)
Sets the objective to minimize the provided quadratic expression objective
.
1644 def set_objective(self, objective: QuadraticTypes, *, is_maximize: bool) -> None: 1645 """Sets the objective to optimize the provided expression `objective`.""" 1646 if isinstance(objective, (LinearBase, int, float)): 1647 self.set_linear_objective(objective, is_maximize=is_maximize) 1648 elif isinstance(objective, QuadraticBase): 1649 self.set_quadratic_objective(objective, is_maximize=is_maximize) 1650 else: 1651 raise TypeError( 1652 "unsupported type in objective argument for " 1653 f"set_objective: {type(objective).__name__!r}" 1654 )
Sets the objective to optimize the provided expression objective
.
1656 def set_linear_objective( 1657 self, objective: LinearTypes, *, is_maximize: bool 1658 ) -> None: 1659 """Sets the objective to optimize the provided linear expression `objective`.""" 1660 if not isinstance(objective, (LinearBase, int, float)): 1661 raise TypeError( 1662 "unsupported type in objective argument for " 1663 f"set_linear_objective: {type(objective).__name__!r}" 1664 ) 1665 self.storage.clear_objective() 1666 self._objective.is_maximize = is_maximize 1667 objective_expr = as_flat_linear_expression(objective) 1668 self._objective.offset = objective_expr.offset 1669 for var, coefficient in objective_expr.terms.items(): 1670 self._objective.set_linear_coefficient(var, coefficient)
Sets the objective to optimize the provided linear expression objective
.
1672 def set_quadratic_objective( 1673 self, objective: QuadraticTypes, *, is_maximize: bool 1674 ) -> None: 1675 """Sets the objective to optimize the provided linear expression `objective`.""" 1676 if not isinstance(objective, (QuadraticBase, LinearBase, int, float)): 1677 raise TypeError( 1678 "unsupported type in objective argument for " 1679 f"set_quadratic_objective: {type(objective).__name__!r}" 1680 ) 1681 self.storage.clear_objective() 1682 self._objective.is_maximize = is_maximize 1683 objective_expr = as_flat_quadratic_expression(objective) 1684 self._objective.offset = objective_expr.offset 1685 for var, coefficient in objective_expr.linear_terms.items(): 1686 self._objective.set_linear_coefficient(var, coefficient) 1687 for key, coefficient in objective_expr.quadratic_terms.items(): 1688 self._objective.set_quadratic_coefficient( 1689 key.first_var, key.second_var, coefficient 1690 )
Sets the objective to optimize the provided linear expression objective
.
1695 def add_linear_constraint( 1696 self, 1697 bounded_expr: Optional[Union[bool, BoundedLinearTypes]] = None, 1698 *, 1699 lb: Optional[float] = None, 1700 ub: Optional[float] = None, 1701 expr: Optional[LinearTypes] = None, 1702 name: str = "", 1703 ) -> LinearConstraint: 1704 """Adds a linear constraint to the optimization model. 1705 1706 The simplest way to specify the constraint is by passing a one-sided or 1707 two-sided linear inequality as in: 1708 * add_linear_constraint(x + y + 1.0 <= 2.0), 1709 * add_linear_constraint(x + y >= 2.0), or 1710 * add_linear_constraint((1.0 <= x + y) <= 2.0). 1711 1712 Note the extra parenthesis for two-sided linear inequalities, which is 1713 required due to some language limitations (see 1714 https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). 1715 If the parenthesis are omitted, a TypeError will be raised explaining the 1716 issue (if this error was not raised the first inequality would have been 1717 silently ignored because of the noted language limitations). 1718 1719 The second way to specify the constraint is by setting lb, ub, and/o expr as 1720 in: 1721 * add_linear_constraint(expr=x + y + 1.0, ub=2.0), 1722 * add_linear_constraint(expr=x + y, lb=2.0), 1723 * add_linear_constraint(expr=x + y, lb=1.0, ub=2.0), or 1724 * add_linear_constraint(lb=1.0). 1725 Omitting lb is equivalent to setting it to -math.inf and omiting ub is 1726 equivalent to setting it to math.inf. 1727 1728 These two alternatives are exclusive and a combined call like: 1729 * add_linear_constraint(x + y <= 2.0, lb=1.0), or 1730 * add_linear_constraint(x + y <= 2.0, ub=math.inf) 1731 will raise a ValueError. A ValueError is also raised if expr's offset is 1732 infinite. 1733 1734 Args: 1735 bounded_expr: a linear inequality describing the constraint. Cannot be 1736 specified together with lb, ub, or expr. 1737 lb: The constraint's lower bound if bounded_expr is omitted (if both 1738 bounder_expr and lb are omitted, the lower bound is -math.inf). 1739 ub: The constraint's upper bound if bounded_expr is omitted (if both 1740 bounder_expr and ub are omitted, the upper bound is math.inf). 1741 expr: The constraint's linear expression if bounded_expr is omitted. 1742 name: For debugging purposes only, but nonempty names must be distinct. 1743 1744 Returns: 1745 A reference to the new linear constraint. 1746 """ 1747 normalized_inequality = as_normalized_linear_inequality( 1748 bounded_expr, lb=lb, ub=ub, expr=expr 1749 ) 1750 lin_con_id = self.storage.add_linear_constraint( 1751 normalized_inequality.lb, normalized_inequality.ub, name 1752 ) 1753 result = LinearConstraint(self, lin_con_id) 1754 self._linear_constraint_ids[lin_con_id] = result 1755 for var, coefficient in normalized_inequality.coefficients.items(): 1756 result.set_coefficient(var, coefficient) 1757 return result
Adds a linear constraint to the optimization model.
The simplest way to specify the constraint is by passing a one-sided or two-sided linear inequality as in:
- add_linear_constraint(x + y + 1.0 <= 2.0),
- add_linear_constraint(x + y >= 2.0), or
- add_linear_constraint((1.0 <= x + y) <= 2.0).
Note the extra parenthesis for two-sided linear inequalities, which is required due to some language limitations (see https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). If the parenthesis are omitted, a TypeError will be raised explaining the issue (if this error was not raised the first inequality would have been silently ignored because of the noted language limitations).
The second way to specify the constraint is by setting lb, ub, and/o expr as in:
- add_linear_constraint(expr=x + y + 1.0, ub=2.0),
- add_linear_constraint(expr=x + y, lb=2.0),
- add_linear_constraint(expr=x + y, lb=1.0, ub=2.0), or
- add_linear_constraint(lb=1.0). Omitting lb is equivalent to setting it to -math.inf and omiting ub is equivalent to setting it to math.inf.
These two alternatives are exclusive and a combined call like:
- add_linear_constraint(x + y <= 2.0, lb=1.0), or
- add_linear_constraint(x + y <= 2.0, ub=math.inf)
will raise a ValueError. A ValueError is also raised if expr's offset is infinite.
Arguments:
- bounded_expr: a linear inequality describing the constraint. Cannot be specified together with lb, ub, or expr.
- lb: The constraint's lower bound if bounded_expr is omitted (if both bounder_expr and lb are omitted, the lower bound is -math.inf).
- ub: The constraint's upper bound if bounded_expr is omitted (if both bounder_expr and ub are omitted, the upper bound is math.inf).
- expr: The constraint's linear expression if bounded_expr is omitted.
- name: For debugging purposes only, but nonempty names must be distinct.
Returns:
A reference to the new linear constraint.
1759 def get_linear_constraint(self, lin_con_id: int) -> LinearConstraint: 1760 """Returns the LinearConstraint for the id lin_con_id.""" 1761 if not self.storage.linear_constraint_exists(lin_con_id): 1762 raise KeyError(f"linear constraint does not exist with id {lin_con_id}") 1763 return self._get_or_make_linear_constraint(lin_con_id)
Returns the LinearConstraint for the id lin_con_id.
1770 def linear_constraints(self) -> Iterator[LinearConstraint]: 1771 """Yields the linear constraints in the order of creation.""" 1772 for lin_con_id in self.storage.get_linear_constraints(): 1773 yield self._get_or_make_linear_constraint(lin_con_id)
Yields the linear constraints in the order of creation.
1775 def row_nonzeros(self, linear_constraint: LinearConstraint) -> Iterator[Variable]: 1776 """Yields the variables with nonzero coefficient for this linear constraint.""" 1777 for var_id in self.storage.get_variables_for_linear_constraint( 1778 linear_constraint.id 1779 ): 1780 yield self._get_or_make_variable(var_id)
Yields the variables with nonzero coefficient for this linear constraint.
1782 def column_nonzeros(self, variable: Variable) -> Iterator[LinearConstraint]: 1783 """Yields the linear constraints with nonzero coefficient for this variable.""" 1784 for lin_con_id in self.storage.get_linear_constraints_with_variable( 1785 variable.id 1786 ): 1787 yield self._get_or_make_linear_constraint(lin_con_id)
Yields the linear constraints with nonzero coefficient for this variable.
1789 def linear_objective_terms(self) -> Iterator[LinearTerm]: 1790 """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.""" 1791 for term in self.storage.get_linear_objective_coefficients(): 1792 yield LinearTerm( 1793 variable=self._get_or_make_variable(term.variable_id), 1794 coefficient=term.coefficient, 1795 )
Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.
1797 def quadratic_objective_terms(self) -> Iterator[QuadraticTerm]: 1798 """Yields the quadratic terms with nonzero objective coefficient in undefined order.""" 1799 for term in self.storage.get_quadratic_objective_coefficients(): 1800 var1 = self._get_or_make_variable(term.id_key.id1) 1801 var2 = self._get_or_make_variable(term.id_key.id2) 1802 yield QuadraticTerm( 1803 key=QuadraticTermKey(var1, var2), coefficient=term.coefficient 1804 )
Yields the quadratic terms with nonzero objective coefficient in undefined order.
1806 def linear_constraint_matrix_entries( 1807 self, 1808 ) -> Iterator[LinearConstraintMatrixEntry]: 1809 """Yields the nonzero elements of the linear constraint matrix in undefined order.""" 1810 for entry in self.storage.get_linear_constraint_matrix_entries(): 1811 yield LinearConstraintMatrixEntry( 1812 linear_constraint=self._get_or_make_linear_constraint( 1813 entry.linear_constraint_id 1814 ), 1815 variable=self._get_or_make_variable(entry.variable_id), 1816 coefficient=entry.coefficient, 1817 )
Yields the nonzero elements of the linear constraint matrix in undefined order.
1822 def add_update_tracker(self) -> UpdateTracker: 1823 """Creates an UpdateTracker registered on this model to view changes.""" 1824 return UpdateTracker(self.storage.add_update_tracker())
Creates an UpdateTracker registered on this model to view changes.
1826 def remove_update_tracker(self, tracker: UpdateTracker): 1827 """Stops tracker from getting updates on changes to this model. 1828 1829 An error will be raised if tracker was not created by this Model or if 1830 tracker has been previously removed. 1831 1832 Using (via checkpoint or update) an UpdateTracker after it has been removed 1833 will result in an error. 1834 1835 Args: 1836 tracker: The UpdateTracker to unregister. 1837 1838 Raises: 1839 KeyError: The tracker was created by another model or was already removed. 1840 """ 1841 self.storage.remove_update_tracker(tracker.storage_update_tracker)
Stops tracker from getting updates on changes to this model.
An error will be raised if tracker was not created by this Model or if tracker has been previously removed.
Using (via checkpoint or update) an UpdateTracker after it has been removed will result in an error.
Arguments:
- tracker: The UpdateTracker to unregister.
Raises:
- KeyError: The tracker was created by another model or was already removed.
1843 def check_compatible( 1844 self, var_or_constraint: Union[Variable, LinearConstraint] 1845 ) -> None: 1846 """Raises a ValueError if the model of var_or_constraint is not self.""" 1847 if var_or_constraint.model is not self: 1848 raise ValueError( 1849 f"{var_or_constraint} is from model {var_or_constraint.model} and" 1850 f" cannot be used with model {self}" 1851 )
Raises a ValueError if the model of var_or_constraint is not self.
1872class LinearSum(LinearBase): 1873 # TODO(b/216492143): consider what details to move elsewhere and/or replace 1874 # by examples, and do complexity analysis. 1875 """A deferred sum of LinearBase objects. 1876 1877 LinearSum objects are automatically created when two linear objects are added 1878 and, as noted in the documentation for Linear, can reduce the inefficiencies. 1879 In particular, they are created when calling sum(iterable) when iterable is 1880 an Iterable[LinearTypes]. However, using LinearSum(iterable) instead 1881 can result in additional performance improvements: 1882 1883 * sum(iterable): creates a nested set of LinearSum objects (e.g. 1884 `sum([a, b, c])` is `LinearSum(0, LinearSum(a, LinearSum(b, c)))`). 1885 * LinearSum(iterable): creates a single LinearSum that saves a tuple with 1886 all the LinearTypes objects in iterable (e.g. 1887 `LinearSum([a, b, c])` does not create additional objects). 1888 1889 This class is immutable. 1890 """ 1891 1892 __slots__ = "__weakref__", "_elements" 1893 1894 # Potentially unsafe use of Iterable argument is handled by immediate local 1895 # storage as tuple. 1896 def __init__(self, iterable: Iterable[LinearTypes]) -> None: 1897 """Creates a LinearSum object. A copy of iterable is saved as a tuple.""" 1898 1899 self._elements = tuple(iterable) 1900 for item in self._elements: 1901 if not isinstance(item, (LinearBase, int, float)): 1902 raise TypeError( 1903 "unsupported type in iterable argument for " 1904 f"LinearSum: {type(item).__name__!r}" 1905 ) 1906 1907 @property 1908 def elements(self) -> Tuple[LinearTypes, ...]: 1909 return self._elements 1910 1911 def _flatten_once_and_add_to( 1912 self, 1913 scale: float, 1914 processed_elements: _ProcessedElements, 1915 target_stack: _ToProcessElements, 1916 ) -> None: 1917 for term in self._elements: 1918 if isinstance(term, (int, float)): 1919 processed_elements.offset += scale * float(term) 1920 else: 1921 target_stack.append(term, scale) 1922 1923 def __str__(self): 1924 return str(as_flat_linear_expression(self)) 1925 1926 def __repr__(self): 1927 result = "LinearSum((" 1928 result += ", ".join(repr(linear) for linear in self._elements) 1929 result += "))" 1930 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.
1896 def __init__(self, iterable: Iterable[LinearTypes]) -> None: 1897 """Creates a LinearSum object. A copy of iterable is saved as a tuple.""" 1898 1899 self._elements = tuple(iterable) 1900 for item in self._elements: 1901 if not isinstance(item, (LinearBase, int, float)): 1902 raise TypeError( 1903 "unsupported type in iterable argument for " 1904 f"LinearSum: {type(item).__name__!r}" 1905 )
Creates a LinearSum object. A copy of iterable is saved as a tuple.
1933class QuadraticSum(QuadraticBase): 1934 # TODO(b/216492143): consider what details to move elsewhere and/or replace 1935 # by examples, and do complexity analysis. 1936 """A deferred sum of QuadraticTypes objects. 1937 1938 QuadraticSum objects are automatically created when a quadratic object is 1939 added to quadratic or linear objects and, as has performance optimizations 1940 similar to LinearSum. 1941 1942 This class is immutable. 1943 """ 1944 1945 __slots__ = "__weakref__", "_elements" 1946 1947 # Potentially unsafe use of Iterable argument is handled by immediate local 1948 # storage as tuple. 1949 def __init__(self, iterable: Iterable[QuadraticTypes]) -> None: 1950 """Creates a QuadraticSum object. A copy of iterable is saved as a tuple.""" 1951 1952 self._elements = tuple(iterable) 1953 for item in self._elements: 1954 if not isinstance(item, (LinearBase, QuadraticBase, int, float)): 1955 raise TypeError( 1956 "unsupported type in iterable argument for " 1957 f"QuadraticSum: {type(item).__name__!r}" 1958 ) 1959 1960 @property 1961 def elements(self) -> Tuple[QuadraticTypes, ...]: 1962 return self._elements 1963 1964 def _quadratic_flatten_once_and_add_to( 1965 self, 1966 scale: float, 1967 processed_elements: _QuadraticProcessedElements, 1968 target_stack: _QuadraticToProcessElements, 1969 ) -> None: 1970 for term in self._elements: 1971 if isinstance(term, (int, float)): 1972 processed_elements.offset += scale * float(term) 1973 else: 1974 target_stack.append(term, scale) 1975 1976 def __str__(self): 1977 return str(as_flat_quadratic_expression(self)) 1978 1979 def __repr__(self): 1980 result = "QuadraticSum((" 1981 result += ", ".join(repr(element) for element in self._elements) 1982 result += "))" 1983 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.
1949 def __init__(self, iterable: Iterable[QuadraticTypes]) -> None: 1950 """Creates a QuadraticSum object. A copy of iterable is saved as a tuple.""" 1951 1952 self._elements = tuple(iterable) 1953 for item in self._elements: 1954 if not isinstance(item, (LinearBase, QuadraticBase, int, float)): 1955 raise TypeError( 1956 "unsupported type in iterable argument for " 1957 f"QuadraticSum: {type(item).__name__!r}" 1958 )
Creates a QuadraticSum object. A copy of iterable is saved as a tuple.
1986class LinearProduct(LinearBase): 1987 """A deferred multiplication computation for linear expressions. 1988 1989 This class is immutable. 1990 """ 1991 1992 __slots__ = "_scalar", "_linear" 1993 1994 def __init__(self, scalar: float, linear: LinearBase) -> None: 1995 if not isinstance(scalar, (float, int)): 1996 raise TypeError( 1997 "unsupported type for scalar argument in " 1998 f"LinearProduct: {type(scalar).__name__!r}" 1999 ) 2000 if not isinstance(linear, LinearBase): 2001 raise TypeError( 2002 "unsupported type for linear argument in " 2003 f"LinearProduct: {type(linear).__name__!r}" 2004 ) 2005 self._scalar: float = float(scalar) 2006 self._linear: LinearBase = linear 2007 2008 @property 2009 def scalar(self) -> float: 2010 return self._scalar 2011 2012 @property 2013 def linear(self) -> LinearBase: 2014 return self._linear 2015 2016 def _flatten_once_and_add_to( 2017 self, 2018 scale: float, 2019 processed_elements: _ProcessedElements, 2020 target_stack: _ToProcessElements, 2021 ) -> None: 2022 target_stack.append(self._linear, self._scalar * scale) 2023 2024 def __str__(self): 2025 return str(as_flat_linear_expression(self)) 2026 2027 def __repr__(self): 2028 result = f"LinearProduct({self._scalar!r}, " 2029 result += f"{self._linear!r})" 2030 return result
A deferred multiplication computation for linear expressions.
This class is immutable.
1994 def __init__(self, scalar: float, linear: LinearBase) -> None: 1995 if not isinstance(scalar, (float, int)): 1996 raise TypeError( 1997 "unsupported type for scalar argument in " 1998 f"LinearProduct: {type(scalar).__name__!r}" 1999 ) 2000 if not isinstance(linear, LinearBase): 2001 raise TypeError( 2002 "unsupported type for linear argument in " 2003 f"LinearProduct: {type(linear).__name__!r}" 2004 ) 2005 self._scalar: float = float(scalar) 2006 self._linear: LinearBase = linear
2033class QuadraticProduct(QuadraticBase): 2034 """A deferred multiplication computation for quadratic expressions. 2035 2036 This class is immutable. 2037 """ 2038 2039 __slots__ = "_scalar", "_quadratic" 2040 2041 def __init__(self, scalar: float, quadratic: QuadraticBase) -> None: 2042 if not isinstance(scalar, (float, int)): 2043 raise TypeError( 2044 "unsupported type for scalar argument in " 2045 f"QuadraticProduct: {type(scalar).__name__!r}" 2046 ) 2047 if not isinstance(quadratic, QuadraticBase): 2048 raise TypeError( 2049 "unsupported type for linear argument in " 2050 f"QuadraticProduct: {type(quadratic).__name__!r}" 2051 ) 2052 self._scalar: float = float(scalar) 2053 self._quadratic: QuadraticBase = quadratic 2054 2055 @property 2056 def scalar(self) -> float: 2057 return self._scalar 2058 2059 @property 2060 def quadratic(self) -> QuadraticBase: 2061 return self._quadratic 2062 2063 def _quadratic_flatten_once_and_add_to( 2064 self, 2065 scale: float, 2066 processed_elements: _QuadraticProcessedElements, 2067 target_stack: _QuadraticToProcessElements, 2068 ) -> None: 2069 target_stack.append(self._quadratic, self._scalar * scale) 2070 2071 def __str__(self): 2072 return str(as_flat_quadratic_expression(self)) 2073 2074 def __repr__(self): 2075 return f"QuadraticProduct({self._scalar}, {self._quadratic!r})"
A deferred multiplication computation for quadratic expressions.
This class is immutable.
2041 def __init__(self, scalar: float, quadratic: QuadraticBase) -> None: 2042 if not isinstance(scalar, (float, int)): 2043 raise TypeError( 2044 "unsupported type for scalar argument in " 2045 f"QuadraticProduct: {type(scalar).__name__!r}" 2046 ) 2047 if not isinstance(quadratic, QuadraticBase): 2048 raise TypeError( 2049 "unsupported type for linear argument in " 2050 f"QuadraticProduct: {type(quadratic).__name__!r}" 2051 ) 2052 self._scalar: float = float(scalar) 2053 self._quadratic: QuadraticBase = quadratic
2078class LinearLinearProduct(QuadraticBase): 2079 """A deferred multiplication of two linear expressions. 2080 2081 This class is immutable. 2082 """ 2083 2084 __slots__ = "_first_linear", "_second_linear" 2085 2086 def __init__(self, first_linear: LinearBase, second_linear: LinearBase) -> None: 2087 if not isinstance(first_linear, LinearBase): 2088 raise TypeError( 2089 "unsupported type for first_linear argument in " 2090 f"LinearLinearProduct: {type(first_linear).__name__!r}" 2091 ) 2092 if not isinstance(second_linear, LinearBase): 2093 raise TypeError( 2094 "unsupported type for second_linear argument in " 2095 f"LinearLinearProduct: {type(second_linear).__name__!r}" 2096 ) 2097 self._first_linear: LinearBase = first_linear 2098 self._second_linear: LinearBase = second_linear 2099 2100 @property 2101 def first_linear(self) -> LinearBase: 2102 return self._first_linear 2103 2104 @property 2105 def second_linear(self) -> LinearBase: 2106 return self._second_linear 2107 2108 def _quadratic_flatten_once_and_add_to( 2109 self, 2110 scale: float, 2111 processed_elements: _QuadraticProcessedElements, 2112 target_stack: _QuadraticToProcessElements, 2113 ) -> None: 2114 # A recursion is avoided here because as_flat_linear_expression() must never 2115 # call _quadratic_flatten_once_and_add_to(). 2116 first_expression = as_flat_linear_expression(self._first_linear) 2117 second_expression = as_flat_linear_expression(self._second_linear) 2118 processed_elements.offset += ( 2119 first_expression.offset * second_expression.offset * scale 2120 ) 2121 for first_var, first_val in first_expression.terms.items(): 2122 processed_elements.terms[first_var] += ( 2123 second_expression.offset * first_val * scale 2124 ) 2125 for second_var, second_val in second_expression.terms.items(): 2126 processed_elements.terms[second_var] += ( 2127 first_expression.offset * second_val * scale 2128 ) 2129 2130 for first_var, first_val in first_expression.terms.items(): 2131 for second_var, second_val in second_expression.terms.items(): 2132 processed_elements.quadratic_terms[ 2133 QuadraticTermKey(first_var, second_var) 2134 ] += (first_val * second_val * scale) 2135 2136 def __str__(self): 2137 return str(as_flat_quadratic_expression(self)) 2138 2139 def __repr__(self): 2140 result = "LinearLinearProduct(" 2141 result += f"{self._first_linear!r}, " 2142 result += f"{self._second_linear!r})" 2143 return result
A deferred multiplication of two linear expressions.
This class is immutable.
2086 def __init__(self, first_linear: LinearBase, second_linear: LinearBase) -> None: 2087 if not isinstance(first_linear, LinearBase): 2088 raise TypeError( 2089 "unsupported type for first_linear argument in " 2090 f"LinearLinearProduct: {type(first_linear).__name__!r}" 2091 ) 2092 if not isinstance(second_linear, LinearBase): 2093 raise TypeError( 2094 "unsupported type for second_linear argument in " 2095 f"LinearLinearProduct: {type(second_linear).__name__!r}" 2096 ) 2097 self._first_linear: LinearBase = first_linear 2098 self._second_linear: LinearBase = second_linear
2146def as_flat_linear_expression(value: LinearTypes) -> LinearExpression: 2147 """Converts floats, ints and Linear objects to a LinearExpression.""" 2148 if isinstance(value, LinearExpression): 2149 return value 2150 return LinearExpression(value)
Converts floats, ints and Linear objects to a LinearExpression.
2153def as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression: 2154 """Converts floats, ints, LinearBase and QuadraticBase objects to a QuadraticExpression.""" 2155 if isinstance(value, QuadraticExpression): 2156 return value 2157 return QuadraticExpression(value)
Converts floats, ints, LinearBase and QuadraticBase objects to a QuadraticExpression.
2160@dataclasses.dataclass 2161class NormalizedLinearInequality: 2162 """Represents an inequality lb <= expr <= ub where expr's offset is zero. 2163 2164 The inequality is of the form: 2165 lb <= sum_{x in V} coefficients[x] * x <= ub 2166 where V is the set of keys of coefficients. 2167 """ 2168 2169 lb: float 2170 ub: float 2171 coefficients: Mapping[Variable, float] 2172 2173 def __init__(self, *, lb: float, ub: float, expr: LinearTypes) -> None: 2174 """Raises a ValueError if expr's offset is infinite.""" 2175 flat_expr = as_flat_linear_expression(expr) 2176 if math.isinf(flat_expr.offset): 2177 raise ValueError( 2178 "Trying to create a linear constraint whose expression has an" 2179 " infinite offset." 2180 ) 2181 self.lb = lb - flat_expr.offset 2182 self.ub = ub - flat_expr.offset 2183 self.coefficients = flat_expr.terms
Represents an inequality lb <= expr <= ub where expr's offset is zero.
The inequality is of the form:
lb <= sum_{x in V} coefficients[x] * x <= ub
where V is the set of keys of coefficients.
2173 def __init__(self, *, lb: float, ub: float, expr: LinearTypes) -> None: 2174 """Raises a ValueError if expr's offset is infinite.""" 2175 flat_expr = as_flat_linear_expression(expr) 2176 if math.isinf(flat_expr.offset): 2177 raise ValueError( 2178 "Trying to create a linear constraint whose expression has an" 2179 " infinite offset." 2180 ) 2181 self.lb = lb - flat_expr.offset 2182 self.ub = ub - flat_expr.offset 2183 self.coefficients = flat_expr.terms
Raises a ValueError if expr's offset is infinite.
2189def as_normalized_linear_inequality( 2190 bounded_expr: Optional[Union[bool, BoundedLinearTypes]] = None, 2191 *, 2192 lb: Optional[float] = None, 2193 ub: Optional[float] = None, 2194 expr: Optional[LinearTypes] = None, 2195) -> NormalizedLinearInequality: 2196 """Converts a linear constraint to a NormalizedLinearInequality. 2197 2198 The simplest way to specify the constraint is by passing a one-sided or 2199 two-sided linear inequality as in: 2200 * as_normalized_linear_inequality(x + y + 1.0 <= 2.0), 2201 * as_normalized_linear_inequality(x + y >= 2.0), or 2202 * as_normalized_linear_inequality((1.0 <= x + y) <= 2.0). 2203 2204 Note the extra parenthesis for two-sided linear inequalities, which is 2205 required due to some language limitations (see 2206 https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). 2207 If the parenthesis are omitted, a TypeError will be raised explaining the 2208 issue (if this error was not raised the first inequality would have been 2209 silently ignored because of the noted language limitations). 2210 2211 The second way to specify the constraint is by setting lb, ub, and/o expr as 2212 in: 2213 * as_normalized_linear_inequality(expr=x + y + 1.0, ub=2.0), 2214 * as_normalized_linear_inequality(expr=x + y, lb=2.0), 2215 * as_normalized_linear_inequality(expr=x + y, lb=1.0, ub=2.0), or 2216 * as_normalized_linear_inequality(lb=1.0). 2217 Omitting lb is equivalent to setting it to -math.inf and omiting ub is 2218 equivalent to setting it to math.inf. 2219 2220 These two alternatives are exclusive and a combined call like: 2221 * as_normalized_linear_inequality(x + y <= 2.0, lb=1.0), or 2222 * as_normalized_linear_inequality(x + y <= 2.0, ub=math.inf) 2223 will raise a ValueError. A ValueError is also raised if expr's offset is 2224 infinite. 2225 2226 Args: 2227 bounded_expr: a linear inequality describing the constraint. Cannot be 2228 specified together with lb, ub, or expr. 2229 lb: The constraint's lower bound if bounded_expr is omitted (if both 2230 bounder_expr and lb are omitted, the lower bound is -math.inf). 2231 ub: The constraint's upper bound if bounded_expr is omitted (if both 2232 bounder_expr and ub are omitted, the upper bound is math.inf). 2233 expr: The constraint's linear expression if bounded_expr is omitted. 2234 2235 Returns: 2236 A NormalizedLinearInequality representing the linear constraint. 2237 """ 2238 if bounded_expr is None: 2239 if lb is None: 2240 lb = -math.inf 2241 if ub is None: 2242 ub = math.inf 2243 if expr is None: 2244 return NormalizedLinearInequality(lb=lb, ub=ub, expr=0) 2245 if not isinstance(expr, (LinearBase, int, float)): 2246 raise TypeError( 2247 f"unsupported type for expr argument: {type(expr).__name__!r}" 2248 ) 2249 return NormalizedLinearInequality(lb=lb, ub=ub, expr=expr) 2250 2251 if isinstance(bounded_expr, bool): 2252 raise TypeError( 2253 "unsupported type for bounded_expr argument:" 2254 " bool. This error can occur when trying to add != constraints " 2255 "(which are not supported) or inequalities/equalities with constant " 2256 "left-hand-side and right-hand-side (which are redundant or make a " 2257 "model infeasible)." 2258 ) 2259 if not isinstance( 2260 bounded_expr, 2261 ( 2262 LowerBoundedLinearExpression, 2263 UpperBoundedLinearExpression, 2264 BoundedLinearExpression, 2265 VarEqVar, 2266 ), 2267 ): 2268 raise TypeError( 2269 f"unsupported type for bounded_expr: {type(bounded_expr).__name__!r}" 2270 ) 2271 if lb is not None: 2272 raise ValueError("lb cannot be specified together with a linear inequality") 2273 if ub is not None: 2274 raise ValueError("ub cannot be specified together with a linear inequality") 2275 if expr is not None: 2276 raise ValueError("expr cannot be specified together with a linear inequality") 2277 2278 if isinstance(bounded_expr, VarEqVar): 2279 return NormalizedLinearInequality( 2280 lb=0.0, 2281 ub=0.0, 2282 expr=bounded_expr.first_variable - bounded_expr.second_variable, 2283 ) 2284 2285 if isinstance( 2286 bounded_expr, (LowerBoundedLinearExpression, BoundedLinearExpression) 2287 ): 2288 lb = bounded_expr.lower_bound 2289 else: 2290 lb = -math.inf 2291 if isinstance( 2292 bounded_expr, (UpperBoundedLinearExpression, BoundedLinearExpression) 2293 ): 2294 ub = bounded_expr.upper_bound 2295 else: 2296 ub = math.inf 2297 return NormalizedLinearInequality(lb=lb, ub=ub, expr=bounded_expr.expression)
Converts a linear constraint to a NormalizedLinearInequality.
The simplest way to specify the constraint is by passing a one-sided or two-sided linear inequality as in:
- as_normalized_linear_inequality(x + y + 1.0 <= 2.0),
- as_normalized_linear_inequality(x + y >= 2.0), or
- as_normalized_linear_inequality((1.0 <= x + y) <= 2.0).
Note the extra parenthesis for two-sided linear inequalities, which is required due to some language limitations (see https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). If the parenthesis are omitted, a TypeError will be raised explaining the issue (if this error was not raised the first inequality would have been silently ignored because of the noted language limitations).
The second way to specify the constraint is by setting lb, ub, and/o expr as in:
- as_normalized_linear_inequality(expr=x + y + 1.0, ub=2.0),
- as_normalized_linear_inequality(expr=x + y, lb=2.0),
- as_normalized_linear_inequality(expr=x + y, lb=1.0, ub=2.0), or
- as_normalized_linear_inequality(lb=1.0). Omitting lb is equivalent to setting it to -math.inf and omiting ub is equivalent to setting it to math.inf.
These two alternatives are exclusive and a combined call like:
- as_normalized_linear_inequality(x + y <= 2.0, lb=1.0), or
- as_normalized_linear_inequality(x + y <= 2.0, ub=math.inf)
will raise a ValueError. A ValueError is also raised if expr's offset is infinite.
Arguments:
- bounded_expr: a linear inequality describing the constraint. Cannot be specified together with lb, ub, or expr.
- lb: The constraint's lower bound if bounded_expr is omitted (if both bounder_expr and lb are omitted, the lower bound is -math.inf).
- ub: The constraint's upper bound if bounded_expr is omitted (if both bounder_expr and ub are omitted, the upper bound is math.inf).
- expr: The constraint's linear expression if bounded_expr is omitted.
Returns:
A NormalizedLinearInequality representing the linear constraint.