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)
StorageClass = typing.Type[~ModelStorageImpl]
LinearTypes = typing.Union[int, float, ForwardRef('LinearBase')]
QuadraticTypes = typing.Union[int, float, ForwardRef('LinearBase'), ForwardRef('QuadraticBase')]
LinearTypesExceptVariable = typing.Union[float, int, ForwardRef('LinearTerm'), ForwardRef('LinearExpression'), ForwardRef('LinearSum'), ForwardRef('LinearProduct')]
class UpperBoundedLinearExpression:
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
UpperBoundedLinearExpression( expression: LinearBase, upper_bound: 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.

expression: LinearBase
133    @property
134    def expression(self) -> "LinearBase":
135        return self._expression
upper_bound: float
137    @property
138    def upper_bound(self) -> float:
139        return self._upper_bound
class LowerBoundedLinearExpression:
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
LowerBoundedLinearExpression( expression: LinearBase, lower_bound: 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.

expression: LinearBase
175    @property
176    def expression(self) -> "LinearBase":
177        return self._expression
lower_bound: float
179    @property
180    def lower_bound(self) -> float:
181        return self._lower_bound
class BoundedLinearExpression:
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.

BoundedLinearExpression( lower_bound: float, expression: LinearBase, upper_bound: float)
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
expression: LinearBase
226    @property
227    def expression(self) -> "LinearBase":
228        return self._expression
lower_bound: float
230    @property
231    def lower_bound(self) -> float:
232        return self._lower_bound
upper_bound: float
234    @property
235    def upper_bound(self) -> float:
236        return self._upper_bound
class VarEqVar:
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:

  1. 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.

  2. when the user want to use the equality to create a constraint of equality between two variables.

VarEqVar( first_variable: Variable, second_variable: Variable)
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
first_variable: Variable
276    @property
277    def first_variable(self) -> "Variable":
278        return self._first_variable
second_variable: Variable
280    @property
281    def second_variable(self) -> "Variable":
282        return self._second_variable
BoundedLinearTypesList = (<class 'LowerBoundedLinearExpression'>, <class 'UpperBoundedLinearExpression'>, <class 'BoundedLinearExpression'>, <class 'VarEqVar'>)
class QuadraticTermKey:
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.

QuadraticTermKey( a: Variable, b: Variable)
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.

first_var: Variable
319    @property
320    def first_var(self) -> "Variable":
321        return self._first_var
second_var: Variable
323    @property
324    def second_var(self) -> "Variable":
325        return self._second_var
class LinearBase:
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.

class QuadraticBase:
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.

class Variable(LinearBase):
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.

Variable(model: Model, vid: int)
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().

lower_bound: float
716    @property
717    def lower_bound(self) -> float:
718        return self.model.storage.get_variable_lb(self._id)
upper_bound: float
724    @property
725    def upper_bound(self) -> float:
726        return self.model.storage.get_variable_ub(self._id)
integer: bool
732    @property
733    def integer(self) -> bool:
734        return self.model.storage.get_variable_is_integer(self._id)
name: str
740    @property
741    def name(self) -> str:
742        return self.model.storage.get_variable_name(self._id)
id: int
744    @property
745    def id(self) -> int:
746        return self._id
model: Model
748    @property
749    def model(self) -> "Model":
750        return self._model
class LinearTerm(LinearBase):
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.

LinearTerm(variable: Variable, coefficient: float)
837    def __init__(self, variable: Variable, coefficient: float) -> None:
838        self._variable: Variable = variable
839        self._coefficient: float = coefficient
variable: Variable
841    @property
842    def variable(self) -> Variable:
843        return self._variable
coefficient: float
845    @property
846    def coefficient(self) -> float:
847        return self._coefficient
class QuadraticTerm(QuadraticBase):
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.

QuadraticTerm( key: QuadraticTermKey, coefficient: float)
910    def __init__(self, key: QuadraticTermKey, coefficient: float) -> None:
911        self._key: QuadraticTermKey = key
912        self._coefficient: float = coefficient
key: QuadraticTermKey
914    @property
915    def key(self) -> QuadraticTermKey:
916        return self._key
coefficient: float
918    @property
919    def coefficient(self) -> float:
920        return self._coefficient
class LinearExpression(LinearBase):
 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.

LinearExpression( other: Union[int, float, LinearBase] = 0)
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
terms: Mapping[Variable, float]
982    @property
983    def terms(self) -> Mapping[Variable, float]:
984        return self._terms
offset: float
986    @property
987    def offset(self) -> float:
988        return self._offset
def evaluate( self, variable_values: Mapping[Variable, float]) -> float:
 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.

class QuadraticExpression(QuadraticBase):
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.

QuadraticExpression( other: Union[int, float, LinearBase, QuadraticBase])
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
linear_terms: Mapping[Variable, float]
1094    @property
1095    def linear_terms(self) -> Mapping[Variable, float]:
1096        return self._linear_terms
quadratic_terms: Mapping[QuadraticTermKey, float]
1098    @property
1099    def quadratic_terms(self) -> Mapping[QuadraticTermKey, float]:
1100        return self._quadratic_terms
offset: float
1102    @property
1103    def offset(self) -> float:
1104        return self._offset
def evaluate( self, variable_values: Mapping[Variable, float]) -> float:
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.

class LinearConstraint:
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.

LinearConstraint(model: Model, cid: int)
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().

lower_bound: float
1233    @property
1234    def lower_bound(self) -> float:
1235        return self.model.storage.get_linear_constraint_lb(self._id)
upper_bound: float
1241    @property
1242    def upper_bound(self) -> float:
1243        return self.model.storage.get_linear_constraint_ub(self._id)
name: str
1249    @property
1250    def name(self) -> str:
1251        return self.model.storage.get_linear_constraint_name(self._id)
id: int
1253    @property
1254    def id(self) -> int:
1255        return self._id
model: Model
1257    @property
1258    def model(self) -> "Model":
1259        return self._model
def set_coefficient( self, variable: Variable, coefficient: float) -> None:
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        )
def get_coefficient(self, variable: Variable) -> float:
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        )
def terms(self) -> Iterator[LinearTerm]:
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.

def as_bounded_linear_expression(self) -> BoundedLinearExpression:
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.

class Objective:
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.
Objective(model: Model)
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.

is_maximize: bool
1343    @property
1344    def is_maximize(self) -> bool:
1345        return self.model.storage.get_is_maximize()
offset: float
1351    @property
1352    def offset(self) -> float:
1353        return self.model.storage.get_objective_offset()
model: Model
1359    @property
1360    def model(self) -> "Model":
1361        return self._model
def set_linear_coefficient( self, variable: Variable, coef: float) -> None:
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)
def get_linear_coefficient(self, variable: Variable) -> float:
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)
def linear_terms(self) -> Iterator[LinearTerm]:
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.

def add( self, objective: Union[int, float, LinearBase, QuadraticBase]) -> None:
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.
def add_linear( self, objective: Union[int, float, LinearBase]) -> None:
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.

def add_quadratic( self, objective: Union[int, float, LinearBase, QuadraticBase]) -> None:
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.

def set_quadratic_coefficient( self, first_variable: Variable, second_variable: Variable, coef: float) -> None:
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        )
def get_quadratic_coefficient( self, first_variable: Variable, second_variable: Variable) -> float:
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        )
def quadratic_terms(self) -> Iterator[QuadraticTerm]:
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.

def as_linear_expression(self) -> LinearExpression:
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()))
def as_quadratic_expression(self) -> QuadraticExpression:
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        )
def clear(self) -> None:
1463    def clear(self) -> None:
1464        """Clears objective coefficients and offset. Does not change direction."""
1465        self.model.storage.clear_objective()

Clears objective coefficients and offset. Does not change direction.

class LinearConstraintMatrixEntry(typing.NamedTuple):
1468class LinearConstraintMatrixEntry(NamedTuple):
1469    linear_constraint: LinearConstraint
1470    variable: Variable
1471    coefficient: float

LinearConstraintMatrixEntry(linear_constraint, variable, coefficient)

LinearConstraintMatrixEntry( linear_constraint: LinearConstraint, variable: Variable, coefficient: float)

Create new instance of LinearConstraintMatrixEntry(linear_constraint, variable, coefficient)

linear_constraint: LinearConstraint

Alias for field number 0

variable: Variable

Alias for field number 1

coefficient: float

Alias for field number 2

Inherited Members
builtins.tuple
index
count
class UpdateTracker:
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)

UpdateTracker( storage_update_tracker: ortools.math_opt.python.model_storage.StorageUpdateTracker)
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.

storage_update_tracker
def export_update(self) -> Optional[ortools.math_opt.model_update_pb2.ModelUpdateProto]:
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()

Returns changes to the model since last call to checkpoint/creation.

def advance_checkpoint(self) -> None:
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()

Track changes to the model only after this function call.

class Model:
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.
Model( *, name: str = '', storage_class: Type[~ModelStorageImpl] = <class 'ortools.math_opt.python.hash_model_storage.HashModelStorage'>)
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)
name: str
1563    @property
1564    def name(self) -> str:
1565        return self.storage.name
objective: Objective
1567    @property
1568    def objective(self) -> Objective:
1569        return self._objective
def add_variable( self, *, lb: float = -inf, ub: float = inf, is_integer: bool = False, name: str = '') -> Variable:
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.

def add_integer_variable( self, *, lb: float = -inf, ub: float = inf, name: str = '') -> Variable:
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)
def add_binary_variable(self, *, name: str = '') -> Variable:
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)
def get_variable(self, var_id: int) -> 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.

def delete_variable(self, variable: Variable) -> None:
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]
def variables(self) -> Iterator[Variable]:
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.

def maximize( self, objective: Union[int, float, LinearBase, QuadraticBase]) -> None:
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.

def maximize_linear_objective( self, objective: Union[int, float, LinearBase]) -> None:
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.

def maximize_quadratic_objective( self, objective: Union[int, float, LinearBase, QuadraticBase]) -> None:
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.

def minimize( self, objective: Union[int, float, LinearBase, QuadraticBase]) -> None:
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.

def minimize_linear_objective( self, objective: Union[int, float, LinearBase]) -> None:
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.

def minimize_quadratic_objective( self, objective: Union[int, float, LinearBase, QuadraticBase]) -> None:
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.

def set_objective( self, objective: Union[int, float, LinearBase, QuadraticBase], *, is_maximize: bool) -> None:
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.

def set_linear_objective( self, objective: Union[int, float, LinearBase], *, is_maximize: bool) -> None:
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.

def set_quadratic_objective( self, objective: Union[int, float, LinearBase, QuadraticBase], *, is_maximize: bool) -> None:
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.

def add_linear_constraint( self, bounded_expr: Union[bool, LowerBoundedLinearExpression, UpperBoundedLinearExpression, BoundedLinearExpression, VarEqVar, NoneType] = None, *, lb: Optional[float] = None, ub: Optional[float] = None, expr: Union[int, float, LinearBase, NoneType] = None, name: str = '') -> LinearConstraint:
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.

def get_linear_constraint(self, lin_con_id: int) -> LinearConstraint:
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.

def delete_linear_constraint( self, linear_constraint: LinearConstraint) -> None:
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]
def linear_constraints(self) -> Iterator[LinearConstraint]:
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.

def row_nonzeros( self, linear_constraint: LinearConstraint) -> Iterator[Variable]:
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.

def column_nonzeros( self, variable: Variable) -> Iterator[LinearConstraint]:
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.

def linear_objective_terms(self) -> Iterator[LinearTerm]:
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.

def quadratic_objective_terms(self) -> Iterator[QuadraticTerm]:
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.

def linear_constraint_matrix_entries( self) -> Iterator[LinearConstraintMatrixEntry]:
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.

def export_model(self) -> ortools.math_opt.model_pb2.ModelProto:
1819    def export_model(self) -> model_pb2.ModelProto:
1820        return self.storage.export_model()
def add_update_tracker(self) -> UpdateTracker:
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.

def remove_update_tracker(self, tracker: UpdateTracker):
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.
def check_compatible( self, var_or_constraint: Union[Variable, LinearConstraint]) -> None:
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.

class LinearSum(LinearBase):
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]) is LinearSum(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.

LinearSum( iterable: Iterable[Union[int, float, LinearBase]])
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.

elements: Tuple[Union[int, float, LinearBase], ...]
1907    @property
1908    def elements(self) -> Tuple[LinearTypes, ...]:
1909        return self._elements
class QuadraticSum(QuadraticBase):
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.

QuadraticSum( iterable: Iterable[Union[int, float, LinearBase, QuadraticBase]])
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.

elements: Tuple[Union[int, float, LinearBase, QuadraticBase], ...]
1960    @property
1961    def elements(self) -> Tuple[QuadraticTypes, ...]:
1962        return self._elements
class LinearProduct(LinearBase):
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.

LinearProduct(scalar: float, linear: LinearBase)
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
scalar: float
2008    @property
2009    def scalar(self) -> float:
2010        return self._scalar
linear: LinearBase
2012    @property
2013    def linear(self) -> LinearBase:
2014        return self._linear
class QuadraticProduct(QuadraticBase):
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.

QuadraticProduct( scalar: float, quadratic: QuadraticBase)
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
scalar: float
2055    @property
2056    def scalar(self) -> float:
2057        return self._scalar
quadratic: QuadraticBase
2059    @property
2060    def quadratic(self) -> QuadraticBase:
2061        return self._quadratic
class LinearLinearProduct(QuadraticBase):
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.

LinearLinearProduct( first_linear: LinearBase, second_linear: LinearBase)
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
first_linear: LinearBase
2100    @property
2101    def first_linear(self) -> LinearBase:
2102        return self._first_linear
second_linear: LinearBase
2104    @property
2105    def second_linear(self) -> LinearBase:
2106        return self._second_linear
def as_flat_linear_expression( value: Union[int, float, LinearBase]) -> LinearExpression:
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.

def as_flat_quadratic_expression( value: Union[int, float, LinearBase, QuadraticBase]) -> QuadraticExpression:
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.

@dataclasses.dataclass
class NormalizedLinearInequality:
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.

NormalizedLinearInequality( *, lb: float, ub: float, expr: Union[int, float, LinearBase])
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.

lb: float
ub: float
coefficients: Mapping[Variable, float]
def as_normalized_linear_inequality( bounded_expr: Union[bool, LowerBoundedLinearExpression, UpperBoundedLinearExpression, BoundedLinearExpression, VarEqVar, NoneType] = None, *, lb: Optional[float] = None, ub: Optional[float] = None, expr: Union[int, float, LinearBase, NoneType] = None) -> NormalizedLinearInequality:
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.