ortools.math_opt.python.variables

Define Variables and Linear Expressions.

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

The result of the equality comparison between two Variable.

We use an object here to delay the evaluation of equality so that we can use the operator== in two use-cases:

  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)
103    def __init__(
104        self,
105        first_variable: "Variable",
106        second_variable: "Variable",
107    ) -> None:
108        self._first_variable: "Variable" = first_variable
109        self._second_variable: "Variable" = second_variable
first_variable: Variable
111    @property
112    def first_variable(self) -> "Variable":
113        return self._first_variable
second_variable: Variable
115    @property
116    def second_variable(self) -> "Variable":
117        return self._second_variable
LowerBoundedQuadraticExpression = ortools.math_opt.python.bounded_expressions.LowerBoundedExpression[ForwardRef('QuadraticBase')]
UpperBoundedQuadraticExpression = ortools.math_opt.python.bounded_expressions.UpperBoundedExpression[ForwardRef('QuadraticBase')]
BoundedQuadraticExpression = ortools.math_opt.python.bounded_expressions.BoundedExpression[ForwardRef('QuadraticBase')]
class QuadraticTermKey:
157class QuadraticTermKey:
158    """An id-ordered pair of variables used as a key for quadratic terms."""
159
160    __slots__ = "_first_var", "_second_var"
161
162    def __init__(self, a: "Variable", b: "Variable"):
163        """Variables a and b will be ordered internally by their ids."""
164        self._first_var: "Variable" = a
165        self._second_var: "Variable" = b
166        if self._first_var.id > self._second_var.id:
167            self._first_var, self._second_var = self._second_var, self._first_var
168
169    @property
170    def first_var(self) -> "Variable":
171        return self._first_var
172
173    @property
174    def second_var(self) -> "Variable":
175        return self._second_var
176
177    def __eq__(self, other: "QuadraticTermKey") -> bool:
178        return bool(
179            self._first_var == other._first_var
180            and self._second_var == other._second_var
181        )
182
183    def __hash__(self) -> int:
184        return hash((self._first_var, self._second_var))
185
186    def __str__(self):
187        return f"{self._first_var!s} * {self._second_var!s}"
188
189    def __repr__(self):
190        return f"QuadraticTermKey({self._first_var!r}, {self._second_var!r})"

An id-ordered pair of variables used as a key for quadratic terms.

QuadraticTermKey( a: Variable, b: Variable)
162    def __init__(self, a: "Variable", b: "Variable"):
163        """Variables a and b will be ordered internally by their ids."""
164        self._first_var: "Variable" = a
165        self._second_var: "Variable" = b
166        if self._first_var.id > self._second_var.id:
167            self._first_var, self._second_var = self._second_var, self._first_var

Variables a and b will be ordered internally by their ids.

first_var: Variable
169    @property
170    def first_var(self) -> "Variable":
171        return self._first_var
second_var: Variable
173    @property
174    def second_var(self) -> "Variable":
175        return self._second_var
class LinearBase:
248class LinearBase(metaclass=abc.ABCMeta):
249    """Interface for types that can build linear expressions with +, -, * and /.
250
251    Classes derived from LinearBase (plus float and int scalars) are used to
252    build expression trees describing a linear expression. Operations nodes of the
253    expression tree include:
254
255      * LinearSum: describes a deferred sum of LinearTypes objects.
256      * LinearProduct: describes a deferred product of a scalar and a
257        LinearTypes object.
258
259    Leaf nodes of the expression tree include:
260
261      * float and int scalars.
262      * Variable: a single variable.
263      * LinearTerm: the product of a scalar and a Variable object.
264      * LinearExpression: the sum of a scalar and LinearTerm objects.
265
266    LinearBase objects/expression-trees can be used directly to create
267    constraints or objective functions. However, to facilitate their inspection,
268    any LinearTypes object can be flattened to a LinearExpression
269    through:
270
271     as_flat_linear_expression(value: LinearTypes) -> LinearExpression:
272
273    In addition, all LinearBase objects are immutable.
274
275    Performance notes:
276
277    Using an expression tree representation instead of an eager construction of
278    LinearExpression objects reduces known inefficiencies associated with the
279    use of operator overloading to construct linear expressions. In particular, we
280    expect the runtime of as_flat_linear_expression() to be linear in the size of
281    the expression tree. Additional performance can gained by using LinearSum(c)
282    instead of sum(c) for a container c, as the latter creates len(c) LinearSum
283    objects.
284    """
285
286    __slots__ = ()
287
288    # TODO(b/216492143): explore requirements for this function so calculation of
289    # coefficients and offsets follow expected associativity rules (so approximate
290    # float calculations are as expected).
291    # TODO(b/216492143): add more details of what subclasses need to do in
292    # developers guide.
293    @abc.abstractmethod
294    def _flatten_once_and_add_to(
295        self,
296        scale: float,
297        processed_elements: _ProcessedElements,
298        target_stack: _ToProcessElements,
299    ) -> None:
300        """Flatten one level of tree if needed and add to targets.
301
302        Classes derived from LinearBase only need to implement this function
303        to enable transformation to LinearExpression through
304        as_flat_linear_expression().
305
306        Args:
307          scale: multiply elements by this number when processing or adding to
308            stack.
309          processed_elements: where to add LinearTerms and scalars that can be
310            processed immediately.
311          target_stack: where to add LinearBase elements that are not scalars or
312            LinearTerms (i.e. elements that need further flattening).
313            Implementations should append() to this stack to avoid being recursive.
314        """
315
316    def __eq__(
317        self, rhs: LinearTypes
318    ) -> BoundedLinearExpression:  # overriding-return-type-checks
319        # Note: when rhs is a QuadraticBase, this will cause rhs.__eq__(self) to be
320        # invoked, which is defined.
321        if isinstance(rhs, QuadraticBase):
322            return NotImplemented
323        if isinstance(rhs, (int, float)):
324            return BoundedLinearExpression(rhs, self, rhs)
325        if not isinstance(rhs, LinearBase):
326            _raise_binary_operator_type_error("==", type(self), type(rhs))
327        return BoundedLinearExpression(0.0, self - rhs, 0.0)
328
329    def __ne__(self, rhs: LinearTypes) -> NoReturn:  # overriding-return-type-checks
330        _raise_ne_not_supported()
331
332    @typing.overload
333    def __le__(self, rhs: float) -> "UpperBoundedLinearExpression": ...
334
335    @typing.overload
336    def __le__(self, rhs: "LinearBase") -> "BoundedLinearExpression": ...
337
338    @typing.overload
339    def __le__(self, rhs: "BoundedLinearExpression") -> NoReturn: ...
340
341    def __le__(self, rhs):
342        # Note: when rhs is a QuadraticBase, this will cause rhs.__ge__(self) to be
343        # invoked, which is defined.
344        if isinstance(rhs, QuadraticBase):
345            return NotImplemented
346        if isinstance(rhs, (int, float)):
347            return UpperBoundedLinearExpression(self, rhs)
348        if isinstance(rhs, LinearBase):
349            return BoundedLinearExpression(-math.inf, self - rhs, 0.0)
350        if isinstance(rhs, bounded_expressions.BoundedExpression):
351            _raise_binary_operator_type_error(
352                "<=", type(self), type(rhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE
353            )
354        _raise_binary_operator_type_error("<=", type(self), type(rhs))
355
356    @typing.overload
357    def __ge__(self, lhs: float) -> "LowerBoundedLinearExpression": ...
358
359    @typing.overload
360    def __ge__(self, lhs: "LinearBase") -> "BoundedLinearExpression": ...
361
362    @typing.overload
363    def __ge__(self, lhs: "BoundedLinearExpression") -> NoReturn: ...
364
365    def __ge__(self, lhs):
366        # Note: when lhs is a QuadraticBase, this will cause lhs.__le__(self) to be
367        # invoked, which is defined.
368        if isinstance(lhs, QuadraticBase):
369            return NotImplemented
370        if isinstance(lhs, (int, float)):
371            return LowerBoundedLinearExpression(self, lhs)
372        if isinstance(lhs, LinearBase):
373            return BoundedLinearExpression(0.0, self - lhs, math.inf)
374        if isinstance(lhs, bounded_expressions.BoundedExpression):
375            _raise_binary_operator_type_error(
376                ">=", type(self), type(lhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE
377            )
378        _raise_binary_operator_type_error(">=", type(self), type(lhs))
379
380    def __add__(self, expr: LinearTypes) -> "LinearSum":
381        if not isinstance(expr, (int, float, LinearBase)):
382            return NotImplemented
383        return LinearSum((self, expr))
384
385    def __radd__(self, expr: LinearTypes) -> "LinearSum":
386        if not isinstance(expr, (int, float, LinearBase)):
387            return NotImplemented
388        return LinearSum((expr, self))
389
390    def __sub__(self, expr: LinearTypes) -> "LinearSum":
391        if not isinstance(expr, (int, float, LinearBase)):
392            return NotImplemented
393        return LinearSum((self, -expr))
394
395    def __rsub__(self, expr: LinearTypes) -> "LinearSum":
396        if not isinstance(expr, (int, float, LinearBase, QuadraticBase)):
397            return NotImplemented
398        return LinearSum((expr, -self))
399
400    @typing.overload
401    def __mul__(self, other: float) -> "LinearProduct": ...
402
403    @typing.overload
404    def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ...
405
406    def __mul__(self, other):
407        if not isinstance(other, (int, float, LinearBase)):
408            return NotImplemented
409        if isinstance(other, LinearBase):
410            return LinearLinearProduct(self, other)
411        return LinearProduct(other, self)
412
413    def __rmul__(self, constant: float) -> "LinearProduct":
414        if not isinstance(constant, (int, float)):
415            return NotImplemented
416        return LinearProduct(constant, self)
417
418    # TODO(b/216492143): explore numerical consequences of 1.0 / constant below.
419    def __truediv__(self, constant: float) -> "LinearProduct":
420        if not isinstance(constant, (int, float)):
421            return NotImplemented
422        return LinearProduct(1.0 / constant, self)
423
424    def __neg__(self) -> "LinearProduct":
425        return LinearProduct(-1.0, self)

Interface for types that can build linear expressions with +, -, * and /.

Classes derived from LinearBase (plus float and int scalars) are used to build expression trees describing a linear expression. Operations nodes of the expression tree include:

  • LinearSum: describes a deferred sum of LinearTypes objects.
  • LinearProduct: describes a deferred product of a scalar and a LinearTypes object.
Leaf nodes of the expression tree include:
  • float and int scalars.
  • Variable: a single variable.
  • LinearTerm: the product of a scalar and a Variable object.
  • LinearExpression: the sum of a scalar and LinearTerm objects.

LinearBase objects/expression-trees can be used directly to create constraints or objective functions. However, to facilitate their inspection, any LinearTypes object can be flattened to a LinearExpression through:

as_flat_linear_expression(value: LinearTypes) -> LinearExpression:

In addition, all LinearBase objects are immutable.

Performance notes:

Using an expression tree representation instead of an eager construction of LinearExpression objects reduces known inefficiencies associated with the use of operator overloading to construct linear expressions. In particular, we expect the runtime of as_flat_linear_expression() to be linear in the size of the expression tree. Additional performance can gained by using LinearSum(c) instead of sum(c) for a container c, as the latter creates len(c) LinearSum objects.

class QuadraticBase:
428class QuadraticBase(metaclass=abc.ABCMeta):
429    """Interface for types that can build quadratic expressions with +, -, * and /.
430
431    Classes derived from QuadraticBase and LinearBase (plus float and int scalars)
432    are used to build expression trees describing a quadratic expression.
433    Operations nodes of the expression tree include:
434
435        * QuadraticSum: describes a deferred sum of QuadraticTypes objects.
436        * QuadraticProduct: describes a deferred product of a scalar and a
437          QuadraticTypes object.
438        * LinearLinearProduct: describes a deferred product of two LinearTypes
439          objects.
440
441      Leaf nodes of the expression tree include:
442
443        * float and int scalars.
444        * Variable: a single variable.
445        * LinearTerm: the product of a scalar and a Variable object.
446        * LinearExpression: the sum of a scalar and LinearTerm objects.
447        * QuadraticTerm: the product of a scalar and two Variable objects.
448        * QuadraticExpression: the sum of a scalar, LinearTerm objects and
449          QuadraticTerm objects.
450
451    QuadraticBase objects/expression-trees can be used directly to create
452    objective functions. However, to facilitate their inspection, any
453    QuadraticTypes object can be flattened to a QuadraticExpression
454    through:
455
456     as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression:
457
458    In addition, all QuadraticBase objects are immutable.
459
460    Performance notes:
461
462    Using an expression tree representation instead of an eager construction of
463    QuadraticExpression objects reduces known inefficiencies associated with the
464    use of operator overloading to construct quadratic expressions. In particular,
465    we expect the runtime of as_flat_quadratic_expression() to be linear in the
466    size of the expression tree. Additional performance can gained by using
467    QuadraticSum(c) instead of sum(c) for a container c, as the latter creates
468    len(c) QuadraticSum objects.
469    """
470
471    __slots__ = ()
472
473    # TODO(b/216492143): explore requirements for this function so calculation of
474    # coefficients and offsets follow expected associativity rules (so approximate
475    # float calculations are as expected).
476    # TODO(b/216492143): add more details of what subclasses need to do in
477    # developers guide.
478    @abc.abstractmethod
479    def _quadratic_flatten_once_and_add_to(
480        self,
481        scale: float,
482        processed_elements: _QuadraticProcessedElements,
483        target_stack: _QuadraticToProcessElements,
484    ) -> None:
485        """Flatten one level of tree if needed and add to targets.
486
487        Classes derived from QuadraticBase only need to implement this function
488        to enable transformation to QuadraticExpression through
489        as_flat_quadratic_expression().
490
491        Args:
492          scale: multiply elements by this number when processing or adding to
493            stack.
494          processed_elements: where to add linear terms, quadratic terms and scalars
495            that can be processed immediately.
496          target_stack: where to add LinearBase and QuadraticBase elements that are
497            not scalars or linear terms or quadratic terms (i.e. elements that need
498            further flattening). Implementations should append() to this stack to
499            avoid being recursive.
500        """
501
502    def __eq__(
503        self, rhs: QuadraticTypes
504    ) -> BoundedQuadraticExpression:  # overriding-return-type-checks
505        if isinstance(rhs, (int, float)):
506            return BoundedQuadraticExpression(rhs, self, rhs)
507        if not isinstance(rhs, (LinearBase, QuadraticBase)):
508            _raise_binary_operator_type_error("==", type(self), type(rhs))
509        return BoundedQuadraticExpression(0.0, self - rhs, 0.0)
510
511    def __ne__(self, rhs: QuadraticTypes) -> NoReturn:  # overriding-return-type-checks
512        _raise_ne_not_supported()
513
514    @typing.overload
515    def __le__(self, rhs: float) -> UpperBoundedQuadraticExpression: ...
516
517    @typing.overload
518    def __le__(
519        self, rhs: Union[LinearBase, "QuadraticBase"]
520    ) -> BoundedQuadraticExpression: ...
521
522    @typing.overload
523    def __le__(self, rhs: BoundedQuadraticExpression) -> NoReturn: ...
524
525    def __le__(self, rhs):
526        if isinstance(rhs, (int, float)):
527            return UpperBoundedQuadraticExpression(self, rhs)
528        if isinstance(rhs, (LinearBase, QuadraticBase)):
529            return BoundedQuadraticExpression(-math.inf, self - rhs, 0.0)
530        if isinstance(rhs, bounded_expressions.BoundedExpression):
531            _raise_binary_operator_type_error(
532                "<=", type(self), type(rhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE
533            )
534        _raise_binary_operator_type_error("<=", type(self), type(rhs))
535
536    @typing.overload
537    def __ge__(self, lhs: float) -> LowerBoundedQuadraticExpression: ...
538
539    @typing.overload
540    def __ge__(
541        self, lhs: Union[LinearBase, "QuadraticBase"]
542    ) -> BoundedQuadraticExpression: ...
543
544    @typing.overload
545    def __ge__(self, lhs: BoundedQuadraticExpression) -> NoReturn: ...
546
547    def __ge__(self, lhs):
548        if isinstance(lhs, (int, float)):
549            return LowerBoundedQuadraticExpression(self, lhs)
550        if isinstance(lhs, (LinearBase, QuadraticBase)):
551            return BoundedQuadraticExpression(0.0, self - lhs, math.inf)
552        if isinstance(lhs, bounded_expressions.BoundedExpression):
553            _raise_binary_operator_type_error(
554                ">=", type(self), type(lhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE
555            )
556        _raise_binary_operator_type_error(">=", type(self), type(lhs))
557
558    def __add__(self, expr: QuadraticTypes) -> "QuadraticSum":
559        if not isinstance(expr, (int, float, LinearBase, QuadraticBase)):
560            return NotImplemented
561        return QuadraticSum((self, expr))
562
563    def __radd__(self, expr: QuadraticTypes) -> "QuadraticSum":
564        if not isinstance(expr, (int, float, LinearBase, QuadraticBase)):
565            return NotImplemented
566        return QuadraticSum((expr, self))
567
568    def __sub__(self, expr: QuadraticTypes) -> "QuadraticSum":
569        if not isinstance(expr, (int, float, LinearBase, QuadraticBase)):
570            return NotImplemented
571        return QuadraticSum((self, -expr))
572
573    def __rsub__(self, expr: QuadraticTypes) -> "QuadraticSum":
574        if not isinstance(expr, (int, float, LinearBase, QuadraticBase)):
575            return NotImplemented
576        return QuadraticSum((expr, -self))
577
578    def __mul__(self, other: float) -> "QuadraticProduct":
579        if not isinstance(other, (int, float)):
580            return NotImplemented
581        return QuadraticProduct(other, self)
582
583    def __rmul__(self, other: float) -> "QuadraticProduct":
584        if not isinstance(other, (int, float)):
585            return NotImplemented
586        return QuadraticProduct(other, self)
587
588    # TODO(b/216492143): explore numerical consequences of 1.0 / constant below.
589    def __truediv__(self, constant: float) -> "QuadraticProduct":
590        if not isinstance(constant, (int, float)):
591            return NotImplemented
592        return QuadraticProduct(1.0 / constant, self)
593
594    def __neg__(self) -> "QuadraticProduct":
595        return QuadraticProduct(-1.0, self)

Interface for types that can build quadratic expressions with +, -, * and /.

Classes derived from QuadraticBase and LinearBase (plus float and int scalars) are used to build expression trees describing a quadratic expression.

Operations nodes of the expression tree include:
  • QuadraticSum: describes a deferred sum of QuadraticTypes objects.
    • QuadraticProduct: describes a deferred product of a scalar and a QuadraticTypes object.
    • LinearLinearProduct: describes a deferred product of two LinearTypes objects.

Leaf nodes of the expression tree include:

  • float and int scalars.
  • Variable: a single variable.
  • LinearTerm: the product of a scalar and a Variable object.
  • LinearExpression: the sum of a scalar and LinearTerm objects.
  • QuadraticTerm: the product of a scalar and two Variable objects.
  • QuadraticExpression: the sum of a scalar, LinearTerm objects and QuadraticTerm objects.

QuadraticBase objects/expression-trees can be used directly to create objective functions. However, to facilitate their inspection, any QuadraticTypes object can be flattened to a QuadraticExpression through:

as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression:

In addition, all QuadraticBase objects are immutable.

Performance notes:

Using an expression tree representation instead of an eager construction of QuadraticExpression objects reduces known inefficiencies associated with the use of operator overloading to construct quadratic expressions. In particular, we expect the runtime of as_flat_quadratic_expression() to be linear in the size of the expression tree. Additional performance can gained by using QuadraticSum(c) instead of sum(c) for a container c, as the latter creates len(c) QuadraticSum objects.

598class Variable(LinearBase, from_model.FromModel):
599    """A decision variable for an optimization model.
600
601    A decision variable takes a value from a domain, either the real numbers or
602    the integers, and restricted to be in some interval [lb, ub] (where lb and ub
603    can be infinite). The case of lb == ub is allowed, this means the variable
604    must take a single value. The case of lb > ub is also allowed, this implies
605    that the problem is infeasible.
606
607    A Variable is configured as follows:
608      * lower_bound: a float property, lb above. Should not be NaN nor +inf.
609      * upper_bound: a float property, ub above. Should not be NaN nor -inf.
610      * integer: a bool property, if the domain is integer or continuous.
611
612    The name is optional, read only, and used only for debugging. Non-empty names
613    should be distinct.
614
615    Every Variable is associated with a Model (defined below). Note that data
616    describing the variable (e.g. lower_bound) is owned by Model.storage, this
617    class is simply a reference to that data. Do not create a Variable directly,
618    use Model.add_variable() instead.
619    """
620
621    __slots__ = "_elemental", "_id"
622
623    def __init__(self, elem: elemental.Elemental, vid: int) -> None:
624        """Internal only, prefer Model functions (add_variable() and get_variable())."""
625        if not isinstance(vid, int):
626            raise TypeError(f"vid type should be int, was:{type(vid)}")
627        self._elemental: elemental.Elemental = elem
628        self._id: int = vid
629
630    @property
631    def lower_bound(self) -> float:
632        return self._elemental.get_attr(
633            enums.DoubleAttr1.VARIABLE_LOWER_BOUND, (self._id,)
634        )
635
636    @lower_bound.setter
637    def lower_bound(self, value: float) -> None:
638        self._elemental.set_attr(
639            enums.DoubleAttr1.VARIABLE_LOWER_BOUND,
640            (self._id,),
641            value,
642        )
643
644    @property
645    def upper_bound(self) -> float:
646        return self._elemental.get_attr(
647            enums.DoubleAttr1.VARIABLE_UPPER_BOUND, (self._id,)
648        )
649
650    @upper_bound.setter
651    def upper_bound(self, value: float) -> None:
652        self._elemental.set_attr(
653            enums.DoubleAttr1.VARIABLE_UPPER_BOUND,
654            (self._id,),
655            value,
656        )
657
658    @property
659    def integer(self) -> bool:
660        return self._elemental.get_attr(enums.BoolAttr1.VARIABLE_INTEGER, (self._id,))
661
662    @integer.setter
663    def integer(self, value: bool) -> None:
664        self._elemental.set_attr(enums.BoolAttr1.VARIABLE_INTEGER, (self._id,), value)
665
666    @property
667    def name(self) -> str:
668        return self._elemental.get_element_name(enums.ElementType.VARIABLE, self._id)
669
670    @property
671    def id(self) -> int:
672        return self._id
673
674    @property
675    def elemental(self) -> elemental.Elemental:
676        """Internal use only."""
677        return self._elemental
678
679    def __str__(self):
680        """Returns the name, or a string containing the id if the name is empty."""
681        return self.name if self.name else f"variable_{self.id}"
682
683    def __repr__(self):
684        return f"<Variable id: {self.id}, name: {self.name!r}>"
685
686    @typing.overload
687    def __eq__(self, rhs: "Variable") -> "VarEqVar": ...
688
689    @typing.overload
690    def __eq__(self, rhs: LinearTypesExceptVariable) -> "BoundedLinearExpression": ...
691
692    def __eq__(self, rhs):
693        if isinstance(rhs, Variable):
694            return VarEqVar(self, rhs)
695        return super().__eq__(rhs)
696
697    @typing.overload
698    def __ne__(self, rhs: "Variable") -> bool: ...
699
700    @typing.overload
701    def __ne__(self, rhs: LinearTypesExceptVariable) -> NoReturn: ...
702
703    def __ne__(self, rhs):
704        if isinstance(rhs, Variable):
705            return not self == rhs
706        _raise_ne_not_supported()
707
708    def __hash__(self) -> int:
709        return hash(self._id)
710
711    @typing.overload
712    def __mul__(self, other: float) -> "LinearTerm": ...
713
714    @typing.overload
715    def __mul__(self, other: Union["Variable", "LinearTerm"]) -> "QuadraticTerm": ...
716
717    @typing.overload
718    def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ...
719
720    def __mul__(self, other):
721        if not isinstance(other, (int, float, LinearBase)):
722            return NotImplemented
723        if isinstance(other, Variable):
724            return QuadraticTerm(QuadraticTermKey(self, other), 1.0)
725        if isinstance(other, LinearTerm):
726            return QuadraticTerm(
727                QuadraticTermKey(self, other.variable), other.coefficient
728            )
729        if isinstance(other, LinearBase):
730            return LinearLinearProduct(self, other)  # pytype: disable=bad-return-type
731        return LinearTerm(self, other)
732
733    def __rmul__(self, constant: float) -> "LinearTerm":
734        if not isinstance(constant, (int, float)):
735            return NotImplemented
736        return LinearTerm(self, constant)
737
738    # TODO(b/216492143): explore numerical consequences of 1.0 / constant below.
739    def __truediv__(self, constant: float) -> "LinearTerm":
740        if not isinstance(constant, (int, float)):
741            return NotImplemented
742        return LinearTerm(self, 1.0 / constant)
743
744    def __neg__(self) -> "LinearTerm":
745        return LinearTerm(self, -1.0)
746
747    def _flatten_once_and_add_to(
748        self,
749        scale: float,
750        processed_elements: _ProcessedElements,
751        target_stack: _ToProcessElements,
752    ) -> None:
753        processed_elements.terms[self] += scale

A decision variable for an optimization model.

A decision variable takes a value from a domain, either the real numbers or the integers, and restricted to be in some interval [lb, ub] (where lb and ub can be infinite). The case of lb == ub is allowed, this means the variable must take a single value. The case of lb > ub is also allowed, this implies that the problem is infeasible.

A Variable is configured as follows:
  • lower_bound: a float property, lb above. Should not be NaN nor +inf.
  • upper_bound: a float property, ub above. Should not be NaN nor -inf.
  • integer: a bool property, if the domain is integer or continuous.

The name is optional, read only, and used only for debugging. Non-empty names should be distinct.

Every Variable is associated with a Model (defined below). Note that data describing the variable (e.g. lower_bound) is owned by Model.storage, this class is simply a reference to that data. Do not create a Variable directly, use Model.add_variable() instead.

Variable()
623    def __init__(self, elem: elemental.Elemental, vid: int) -> None:
624        """Internal only, prefer Model functions (add_variable() and get_variable())."""
625        if not isinstance(vid, int):
626            raise TypeError(f"vid type should be int, was:{type(vid)}")
627        self._elemental: elemental.Elemental = elem
628        self._id: int = vid

Internal only, prefer Model functions (add_variable() and get_variable()).

lower_bound: float
630    @property
631    def lower_bound(self) -> float:
632        return self._elemental.get_attr(
633            enums.DoubleAttr1.VARIABLE_LOWER_BOUND, (self._id,)
634        )
upper_bound: float
644    @property
645    def upper_bound(self) -> float:
646        return self._elemental.get_attr(
647            enums.DoubleAttr1.VARIABLE_UPPER_BOUND, (self._id,)
648        )
integer: bool
658    @property
659    def integer(self) -> bool:
660        return self._elemental.get_attr(enums.BoolAttr1.VARIABLE_INTEGER, (self._id,))
name: str
666    @property
667    def name(self) -> str:
668        return self._elemental.get_element_name(enums.ElementType.VARIABLE, self._id)
id: int
670    @property
671    def id(self) -> int:
672        return self._id
elemental
674    @property
675    def elemental(self) -> elemental.Elemental:
676        """Internal use only."""
677        return self._elemental

Internal use only.

class LinearTerm(LinearBase):
756class LinearTerm(LinearBase):
757    """The product of a scalar and a variable.
758
759    This class is immutable.
760    """
761
762    __slots__ = "_variable", "_coefficient"
763
764    def __init__(self, variable: Variable, coefficient: float) -> None:
765        self._variable: Variable = variable
766        self._coefficient: float = coefficient
767
768    @property
769    def variable(self) -> Variable:
770        return self._variable
771
772    @property
773    def coefficient(self) -> float:
774        return self._coefficient
775
776    def _flatten_once_and_add_to(
777        self,
778        scale: float,
779        processed_elements: _ProcessedElements,
780        target_stack: _ToProcessElements,
781    ) -> None:
782        processed_elements.terms[self._variable] += self._coefficient * scale
783
784    @typing.overload
785    def __mul__(self, other: float) -> "LinearTerm": ...
786
787    @typing.overload
788    def __mul__(self, other: Union["Variable", "LinearTerm"]) -> "QuadraticTerm": ...
789
790    @typing.overload
791    def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ...
792
793    def __mul__(self, other):
794        if not isinstance(other, (int, float, LinearBase)):
795            return NotImplemented
796        if isinstance(other, Variable):
797            return QuadraticTerm(
798                QuadraticTermKey(self._variable, other), self._coefficient
799            )
800        if isinstance(other, LinearTerm):
801            return QuadraticTerm(
802                QuadraticTermKey(self.variable, other.variable),
803                self._coefficient * other.coefficient,
804            )
805        if isinstance(other, LinearBase):
806            return LinearLinearProduct(self, other)  # pytype: disable=bad-return-type
807        return LinearTerm(self._variable, self._coefficient * other)
808
809    def __rmul__(self, constant: float) -> "LinearTerm":
810        if not isinstance(constant, (int, float)):
811            return NotImplemented
812        return LinearTerm(self._variable, self._coefficient * constant)
813
814    def __truediv__(self, constant: float) -> "LinearTerm":
815        if not isinstance(constant, (int, float)):
816            return NotImplemented
817        return LinearTerm(self._variable, self._coefficient / constant)
818
819    def __neg__(self) -> "LinearTerm":
820        return LinearTerm(self._variable, -self._coefficient)
821
822    def __str__(self):
823        return f"{self._coefficient} * {self._variable}"
824
825    def __repr__(self):
826        return f"LinearTerm({self._variable!r}, {self._coefficient!r})"

The product of a scalar and a variable.

This class is immutable.

LinearTerm( variable: Variable, coefficient: float)
764    def __init__(self, variable: Variable, coefficient: float) -> None:
765        self._variable: Variable = variable
766        self._coefficient: float = coefficient
variable: Variable
768    @property
769    def variable(self) -> Variable:
770        return self._variable
coefficient: float
772    @property
773    def coefficient(self) -> float:
774        return self._coefficient
class QuadraticTerm(QuadraticBase):
829class QuadraticTerm(QuadraticBase):
830    """The product of a scalar and two variables.
831
832    This class is immutable.
833    """
834
835    __slots__ = "_key", "_coefficient"
836
837    def __init__(self, key: QuadraticTermKey, coefficient: float) -> None:
838        self._key: QuadraticTermKey = key
839        self._coefficient: float = coefficient
840
841    @property
842    def key(self) -> QuadraticTermKey:
843        return self._key
844
845    @property
846    def coefficient(self) -> float:
847        return self._coefficient
848
849    def _quadratic_flatten_once_and_add_to(
850        self,
851        scale: float,
852        processed_elements: _QuadraticProcessedElements,
853        target_stack: _ToProcessElements,
854    ) -> None:
855        processed_elements.quadratic_terms[self._key] += self._coefficient * scale
856
857    def __mul__(self, constant: float) -> "QuadraticTerm":
858        if not isinstance(constant, (int, float)):
859            return NotImplemented
860        return QuadraticTerm(self._key, self._coefficient * constant)
861
862    def __rmul__(self, constant: float) -> "QuadraticTerm":
863        if not isinstance(constant, (int, float)):
864            return NotImplemented
865        return QuadraticTerm(self._key, self._coefficient * constant)
866
867    def __truediv__(self, constant: float) -> "QuadraticTerm":
868        if not isinstance(constant, (int, float)):
869            return NotImplemented
870        return QuadraticTerm(self._key, self._coefficient / constant)
871
872    def __neg__(self) -> "QuadraticTerm":
873        return QuadraticTerm(self._key, -self._coefficient)
874
875    def __str__(self):
876        return f"{self._coefficient} * {self._key!s}"
877
878    def __repr__(self):
879        return f"QuadraticTerm({self._key!r}, {self._coefficient})"

The product of a scalar and two variables.

This class is immutable.

QuadraticTerm( key: QuadraticTermKey, coefficient: float)
837    def __init__(self, key: QuadraticTermKey, coefficient: float) -> None:
838        self._key: QuadraticTermKey = key
839        self._coefficient: float = coefficient
key: QuadraticTermKey
841    @property
842    def key(self) -> QuadraticTermKey:
843        return self._key
coefficient: float
845    @property
846    def coefficient(self) -> float:
847        return self._coefficient
class LinearExpression(LinearBase):
882class LinearExpression(LinearBase):
883    """For variables x, an expression: b + sum_{i in I} a_i * x_i.
884
885    This class is immutable.
886    """
887
888    __slots__ = "__weakref__", "_terms", "_offset"
889
890    # TODO(b/216492143): consider initializing from a dictionary.
891    def __init__(self, /, other: LinearTypes = 0) -> None:
892        self._offset: float = 0.0
893        if isinstance(other, (int, float)):
894            self._offset = float(other)
895            self._terms: Mapping[Variable, float] = immutabledict.immutabledict()
896            return
897
898        to_process: _LinearToProcessElements = _LinearToProcessElements(other, 1.0)
899        processed_elements = _ProcessedElements()
900        while to_process:
901            linear, coef = to_process.pop()
902            linear._flatten_once_and_add_to(coef, processed_elements, to_process)
903        # TODO(b/216492143): explore avoiding this copy.
904        self._terms: Mapping[Variable, float] = immutabledict.immutabledict(
905            processed_elements.terms
906        )
907        self._offset = processed_elements.offset
908
909    @property
910    def terms(self) -> Mapping[Variable, float]:
911        return self._terms
912
913    @property
914    def offset(self) -> float:
915        return self._offset
916
917    def evaluate(self, variable_values: Mapping[Variable, float]) -> float:
918        """Returns the value of this expression for given variable values.
919
920        E.g. if this is 3 * x + 4 and variable_values = {x: 2.0}, then
921        evaluate(variable_values) equals 10.0.
922
923        See also mathopt.evaluate_expression(), which works on any type in
924        QuadraticTypes.
925
926        Args:
927          variable_values: Must contain a value for every variable in expression.
928
929        Returns:
930          The value of this expression when replacing variables by their value.
931        """
932        result = self._offset
933        for var, coef in sorted(
934            self._terms.items(), key=lambda var_coef_pair: var_coef_pair[0].id
935        ):
936            result += coef * variable_values[var]
937        return result
938
939    def _flatten_once_and_add_to(
940        self,
941        scale: float,
942        processed_elements: _ProcessedElements,
943        target_stack: _ToProcessElements,
944    ) -> None:
945        for var, val in self._terms.items():
946            processed_elements.terms[var] += val * scale
947        processed_elements.offset += scale * self.offset
948
949    # TODO(b/216492143): change __str__ to match C++ implementation in
950    # cl/421649402.
951    def __str__(self):
952        """Returns the name, or a string containing the id if the name is empty."""
953        result = str(self.offset)
954        sorted_keys = sorted(self._terms.keys(), key=str)
955        for var in sorted_keys:
956            # TODO(b/216492143): consider how to better deal with `NaN` and try to
957            # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in
958            # linear_expression_test.py.
959            coefficient = self._terms[var]
960            if coefficient == 0.0:
961                continue
962            if coefficient > 0:
963                result += " + "
964            else:
965                result += " - "
966            result += str(abs(coefficient)) + " * " + str(var)
967        return result
968
969    def __repr__(self):
970        result = f"LinearExpression({self.offset}, " + "{"
971        result += ", ".join(
972            f"{var!r}: {coefficient}" for var, coefficient in self._terms.items()
973        )
974        result += "})"
975        return result

For variables x, an expression: b + sum_{i in I} a_i * x_i.

This class is immutable.

LinearExpression( other: int | float | LinearBase = 0)
891    def __init__(self, /, other: LinearTypes = 0) -> None:
892        self._offset: float = 0.0
893        if isinstance(other, (int, float)):
894            self._offset = float(other)
895            self._terms: Mapping[Variable, float] = immutabledict.immutabledict()
896            return
897
898        to_process: _LinearToProcessElements = _LinearToProcessElements(other, 1.0)
899        processed_elements = _ProcessedElements()
900        while to_process:
901            linear, coef = to_process.pop()
902            linear._flatten_once_and_add_to(coef, processed_elements, to_process)
903        # TODO(b/216492143): explore avoiding this copy.
904        self._terms: Mapping[Variable, float] = immutabledict.immutabledict(
905            processed_elements.terms
906        )
907        self._offset = processed_elements.offset
terms: Mapping[Variable, float]
909    @property
910    def terms(self) -> Mapping[Variable, float]:
911        return self._terms
offset: float
913    @property
914    def offset(self) -> float:
915        return self._offset
def evaluate( self, variable_values: Mapping[Variable, float]) -> float:
917    def evaluate(self, variable_values: Mapping[Variable, float]) -> float:
918        """Returns the value of this expression for given variable values.
919
920        E.g. if this is 3 * x + 4 and variable_values = {x: 2.0}, then
921        evaluate(variable_values) equals 10.0.
922
923        See also mathopt.evaluate_expression(), which works on any type in
924        QuadraticTypes.
925
926        Args:
927          variable_values: Must contain a value for every variable in expression.
928
929        Returns:
930          The value of this expression when replacing variables by their value.
931        """
932        result = self._offset
933        for var, coef in sorted(
934            self._terms.items(), key=lambda var_coef_pair: var_coef_pair[0].id
935        ):
936            result += coef * variable_values[var]
937        return result

Returns the value of this expression for given variable values.

E.g. if this is 3 * x + 4 and variable_values = {x: 2.0}, then evaluate(variable_values) equals 10.0.

See also mathopt.evaluate_expression(), which works on any type in QuadraticTypes.

Arguments:
  • variable_values: Must contain a value for every variable in expression.
Returns:

The value of this expression when replacing variables by their value.

class QuadraticExpression(QuadraticBase):
 978class QuadraticExpression(QuadraticBase):
 979    """For variables x, an expression: b + sum_{i in I} a_i * x_i + sum_{i,j in I, i<=j} a_i,j * x_i * x_j.
 980
 981    This class is immutable.
 982    """
 983
 984    __slots__ = "__weakref__", "_linear_terms", "_quadratic_terms", "_offset"
 985
 986    # TODO(b/216492143): consider initializing from a dictionary.
 987    def __init__(self, other: QuadraticTypes) -> None:
 988        self._offset: float = 0.0
 989        if isinstance(other, (int, float)):
 990            self._offset = float(other)
 991            self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict()
 992            self._quadratic_terms: Mapping[QuadraticTermKey, float] = (
 993                immutabledict.immutabledict()
 994            )
 995            return
 996
 997        to_process: _QuadraticToProcessElements = _QuadraticToProcessElements(
 998            other, 1.0
 999        )
1000        processed_elements = _QuadraticProcessedElements()
1001        while to_process:
1002            linear_or_quadratic, coef = to_process.pop()
1003            if isinstance(linear_or_quadratic, LinearBase):
1004                linear_or_quadratic._flatten_once_and_add_to(
1005                    coef, processed_elements, to_process
1006                )
1007            else:
1008                linear_or_quadratic._quadratic_flatten_once_and_add_to(
1009                    coef, processed_elements, to_process
1010                )
1011        # TODO(b/216492143): explore avoiding this copy.
1012        self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict(
1013            processed_elements.terms
1014        )
1015        self._quadratic_terms: Mapping[QuadraticTermKey, float] = (
1016            immutabledict.immutabledict(processed_elements.quadratic_terms)
1017        )
1018        self._offset = processed_elements.offset
1019
1020    @property
1021    def linear_terms(self) -> Mapping[Variable, float]:
1022        return self._linear_terms
1023
1024    @property
1025    def quadratic_terms(self) -> Mapping[QuadraticTermKey, float]:
1026        return self._quadratic_terms
1027
1028    @property
1029    def offset(self) -> float:
1030        return self._offset
1031
1032    def evaluate(self, variable_values: Mapping[Variable, float]) -> float:
1033        """Returns the value of this expression for given variable values.
1034
1035        E.g. if this is 3 * x * x + 4 and variable_values = {x: 2.0}, then
1036        evaluate(variable_values) equals 16.0.
1037
1038        See also mathopt.evaluate_expression(), which works on any type in
1039        QuadraticTypes.
1040
1041        Args:
1042          variable_values: Must contain a value for every variable in expression.
1043
1044        Returns:
1045          The value of this expression when replacing variables by their value.
1046        """
1047        result = self._offset
1048        for var, coef in sorted(
1049            self._linear_terms.items(),
1050            key=lambda var_coef_pair: var_coef_pair[0].id,
1051        ):
1052            result += coef * variable_values[var]
1053        for key, coef in sorted(
1054            self._quadratic_terms.items(),
1055            key=lambda quad_coef_pair: (
1056                quad_coef_pair[0].first_var.id,
1057                quad_coef_pair[0].second_var.id,
1058            ),
1059        ):
1060            result += (
1061                coef * variable_values[key.first_var] * variable_values[key.second_var]
1062            )
1063        return result
1064
1065    def _quadratic_flatten_once_and_add_to(
1066        self,
1067        scale: float,
1068        processed_elements: _QuadraticProcessedElements,
1069        target_stack: _QuadraticToProcessElements,
1070    ) -> None:
1071        for var, val in self._linear_terms.items():
1072            processed_elements.terms[var] += val * scale
1073        for key, val in self._quadratic_terms.items():
1074            processed_elements.quadratic_terms[key] += val * scale
1075        processed_elements.offset += scale * self.offset
1076
1077    # TODO(b/216492143): change __str__ to match C++ implementation in
1078    # cl/421649402.
1079    def __str__(self):
1080        result = str(self.offset)
1081        sorted_linear_keys = sorted(self._linear_terms.keys(), key=str)
1082        for var in sorted_linear_keys:
1083            # TODO(b/216492143): consider how to better deal with `NaN` and try to
1084            # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in
1085            # linear_expression_test.py.
1086            coefficient = self._linear_terms[var]
1087            if coefficient == 0.0:
1088                continue
1089            if coefficient > 0:
1090                result += " + "
1091            else:
1092                result += " - "
1093            result += str(abs(coefficient)) + " * " + str(var)
1094        sorted_quadratic_keys = sorted(self._quadratic_terms.keys(), key=str)
1095        for key in sorted_quadratic_keys:
1096            # TODO(b/216492143): consider how to better deal with `NaN` and try to
1097            # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in
1098            # linear_expression_test.py.
1099            coefficient = self._quadratic_terms[key]
1100            if coefficient == 0.0:
1101                continue
1102            if coefficient > 0:
1103                result += " + "
1104            else:
1105                result += " - "
1106            result += str(abs(coefficient)) + " * " + str(key)
1107        return result
1108
1109    def __repr__(self):
1110        result = f"QuadraticExpression({self.offset}, " + "{"
1111        result += ", ".join(
1112            f"{var!r}: {coefficient}" for var, coefficient in self._linear_terms.items()
1113        )
1114        result += "}, {"
1115        result += ", ".join(
1116            f"{key!r}: {coefficient}"
1117            for key, coefficient in self._quadratic_terms.items()
1118        )
1119        result += "})"
1120        return result

For variables x, an expression: b + sum_{i in I} a_i * x_i + sum_{i,j in I, i<=j} a_i,j * x_i * x_j.

This class is immutable.

QuadraticExpression( other: int | float | LinearBase | QuadraticBase)
 987    def __init__(self, other: QuadraticTypes) -> None:
 988        self._offset: float = 0.0
 989        if isinstance(other, (int, float)):
 990            self._offset = float(other)
 991            self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict()
 992            self._quadratic_terms: Mapping[QuadraticTermKey, float] = (
 993                immutabledict.immutabledict()
 994            )
 995            return
 996
 997        to_process: _QuadraticToProcessElements = _QuadraticToProcessElements(
 998            other, 1.0
 999        )
1000        processed_elements = _QuadraticProcessedElements()
1001        while to_process:
1002            linear_or_quadratic, coef = to_process.pop()
1003            if isinstance(linear_or_quadratic, LinearBase):
1004                linear_or_quadratic._flatten_once_and_add_to(
1005                    coef, processed_elements, to_process
1006                )
1007            else:
1008                linear_or_quadratic._quadratic_flatten_once_and_add_to(
1009                    coef, processed_elements, to_process
1010                )
1011        # TODO(b/216492143): explore avoiding this copy.
1012        self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict(
1013            processed_elements.terms
1014        )
1015        self._quadratic_terms: Mapping[QuadraticTermKey, float] = (
1016            immutabledict.immutabledict(processed_elements.quadratic_terms)
1017        )
1018        self._offset = processed_elements.offset
linear_terms: Mapping[Variable, float]
1020    @property
1021    def linear_terms(self) -> Mapping[Variable, float]:
1022        return self._linear_terms
quadratic_terms: Mapping[QuadraticTermKey, float]
1024    @property
1025    def quadratic_terms(self) -> Mapping[QuadraticTermKey, float]:
1026        return self._quadratic_terms
offset: float
1028    @property
1029    def offset(self) -> float:
1030        return self._offset
def evaluate( self, variable_values: Mapping[Variable, float]) -> float:
1032    def evaluate(self, variable_values: Mapping[Variable, float]) -> float:
1033        """Returns the value of this expression for given variable values.
1034
1035        E.g. if this is 3 * x * x + 4 and variable_values = {x: 2.0}, then
1036        evaluate(variable_values) equals 16.0.
1037
1038        See also mathopt.evaluate_expression(), which works on any type in
1039        QuadraticTypes.
1040
1041        Args:
1042          variable_values: Must contain a value for every variable in expression.
1043
1044        Returns:
1045          The value of this expression when replacing variables by their value.
1046        """
1047        result = self._offset
1048        for var, coef in sorted(
1049            self._linear_terms.items(),
1050            key=lambda var_coef_pair: var_coef_pair[0].id,
1051        ):
1052            result += coef * variable_values[var]
1053        for key, coef in sorted(
1054            self._quadratic_terms.items(),
1055            key=lambda quad_coef_pair: (
1056                quad_coef_pair[0].first_var.id,
1057                quad_coef_pair[0].second_var.id,
1058            ),
1059        ):
1060            result += (
1061                coef * variable_values[key.first_var] * variable_values[key.second_var]
1062            )
1063        return result

Returns the value of this expression for given variable values.

E.g. if this is 3 * x * x + 4 and variable_values = {x: 2.0}, then evaluate(variable_values) equals 16.0.

See also mathopt.evaluate_expression(), which works on any type in QuadraticTypes.

Arguments:
  • variable_values: Must contain a value for every variable in expression.
Returns:

The value of this expression when replacing variables by their value.

class LinearSum(LinearBase):
1123class LinearSum(LinearBase):
1124    # TODO(b/216492143): consider what details to move elsewhere and/or replace
1125    # by examples, and do complexity analysis.
1126    """A deferred sum of LinearBase objects.
1127
1128    LinearSum objects are automatically created when two linear objects are added
1129    and, as noted in the documentation for Linear, can reduce the inefficiencies.
1130    In particular, they are created when calling sum(iterable) when iterable is
1131    an Iterable[LinearTypes]. However, using LinearSum(iterable) instead
1132    can result in additional performance improvements:
1133
1134      * sum(iterable): creates a nested set of LinearSum objects (e.g.
1135        `sum([a, b, c])` is `LinearSum(0, LinearSum(a, LinearSum(b, c)))`).
1136      * LinearSum(iterable): creates a single LinearSum that saves a tuple with
1137        all the LinearTypes objects in iterable (e.g.
1138        `LinearSum([a, b, c])` does not create additional objects).
1139
1140    This class is immutable.
1141    """
1142
1143    __slots__ = "__weakref__", "_elements"
1144
1145    # Potentially unsafe use of Iterable argument is handled by immediate local
1146    # storage as tuple.
1147    def __init__(self, iterable: Iterable[LinearTypes]) -> None:
1148        """Creates a LinearSum object. A copy of iterable is saved as a tuple."""
1149
1150        self._elements = tuple(iterable)
1151        for item in self._elements:
1152            if not isinstance(item, (LinearBase, int, float)):
1153                raise TypeError(
1154                    "unsupported type in iterable argument for "
1155                    f"LinearSum: {type(item).__name__!r}"
1156                )
1157
1158    @property
1159    def elements(self) -> Tuple[LinearTypes, ...]:
1160        return self._elements
1161
1162    def _flatten_once_and_add_to(
1163        self,
1164        scale: float,
1165        processed_elements: _ProcessedElements,
1166        target_stack: _ToProcessElements,
1167    ) -> None:
1168        for term in self._elements:
1169            if isinstance(term, (int, float)):
1170                processed_elements.offset += scale * float(term)
1171            else:
1172                target_stack.append(term, scale)
1173
1174    def __str__(self):
1175        return str(as_flat_linear_expression(self))
1176
1177    def __repr__(self):
1178        result = "LinearSum(("
1179        result += ", ".join(repr(linear) for linear in self._elements)
1180        result += "))"
1181        return result

A deferred sum of LinearBase objects.

LinearSum objects are automatically created when two linear objects are added and, as noted in the documentation for Linear, can reduce the inefficiencies. In particular, they are created when calling sum(iterable) when iterable is an Iterable[LinearTypes]. However, using LinearSum(iterable) instead can result in additional performance improvements:

  • sum(iterable): creates a nested set of LinearSum objects (e.g. sum([a, b, c]) 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[int | float | LinearBase])
1147    def __init__(self, iterable: Iterable[LinearTypes]) -> None:
1148        """Creates a LinearSum object. A copy of iterable is saved as a tuple."""
1149
1150        self._elements = tuple(iterable)
1151        for item in self._elements:
1152            if not isinstance(item, (LinearBase, int, float)):
1153                raise TypeError(
1154                    "unsupported type in iterable argument for "
1155                    f"LinearSum: {type(item).__name__!r}"
1156                )

Creates a LinearSum object. A copy of iterable is saved as a tuple.

elements: Tuple[int | float | LinearBase, ...]
1158    @property
1159    def elements(self) -> Tuple[LinearTypes, ...]:
1160        return self._elements
class QuadraticSum(QuadraticBase):
1184class QuadraticSum(QuadraticBase):
1185    # TODO(b/216492143): consider what details to move elsewhere and/or replace
1186    # by examples, and do complexity analysis.
1187    """A deferred sum of QuadraticTypes objects.
1188
1189    QuadraticSum objects are automatically created when a quadratic object is
1190    added to quadratic or linear objects and, as has performance optimizations
1191    similar to LinearSum.
1192
1193    This class is immutable.
1194    """
1195
1196    __slots__ = "__weakref__", "_elements"
1197
1198    # Potentially unsafe use of Iterable argument is handled by immediate local
1199    # storage as tuple.
1200    def __init__(self, iterable: Iterable[QuadraticTypes]) -> None:
1201        """Creates a QuadraticSum object. A copy of iterable is saved as a tuple."""
1202
1203        self._elements = tuple(iterable)
1204        for item in self._elements:
1205            if not isinstance(item, (LinearBase, QuadraticBase, int, float)):
1206                raise TypeError(
1207                    "unsupported type in iterable argument for "
1208                    f"QuadraticSum: {type(item).__name__!r}"
1209                )
1210
1211    @property
1212    def elements(self) -> Tuple[QuadraticTypes, ...]:
1213        return self._elements
1214
1215    def _quadratic_flatten_once_and_add_to(
1216        self,
1217        scale: float,
1218        processed_elements: _QuadraticProcessedElements,
1219        target_stack: _QuadraticToProcessElements,
1220    ) -> None:
1221        for term in self._elements:
1222            if isinstance(term, (int, float)):
1223                processed_elements.offset += scale * float(term)
1224            else:
1225                target_stack.append(term, scale)
1226
1227    def __str__(self):
1228        return str(as_flat_quadratic_expression(self))
1229
1230    def __repr__(self):
1231        result = "QuadraticSum(("
1232        result += ", ".join(repr(element) for element in self._elements)
1233        result += "))"
1234        return result

A deferred sum of QuadraticTypes objects.

QuadraticSum objects are automatically created when a quadratic object is added to quadratic or linear objects and, as has performance optimizations similar to LinearSum.

This class is immutable.

QuadraticSum( iterable: Iterable[int | float | LinearBase | QuadraticBase])
1200    def __init__(self, iterable: Iterable[QuadraticTypes]) -> None:
1201        """Creates a QuadraticSum object. A copy of iterable is saved as a tuple."""
1202
1203        self._elements = tuple(iterable)
1204        for item in self._elements:
1205            if not isinstance(item, (LinearBase, QuadraticBase, int, float)):
1206                raise TypeError(
1207                    "unsupported type in iterable argument for "
1208                    f"QuadraticSum: {type(item).__name__!r}"
1209                )

Creates a QuadraticSum object. A copy of iterable is saved as a tuple.

elements: Tuple[int | float | LinearBase | QuadraticBase, ...]
1211    @property
1212    def elements(self) -> Tuple[QuadraticTypes, ...]:
1213        return self._elements
class LinearProduct(LinearBase):
1237class LinearProduct(LinearBase):
1238    """A deferred multiplication computation for linear expressions.
1239
1240    This class is immutable.
1241    """
1242
1243    __slots__ = "_scalar", "_linear"
1244
1245    def __init__(self, scalar: float, linear: LinearBase) -> None:
1246        if not isinstance(scalar, (float, int)):
1247            raise TypeError(
1248                "unsupported type for scalar argument in "
1249                f"LinearProduct: {type(scalar).__name__!r}"
1250            )
1251        if not isinstance(linear, LinearBase):
1252            raise TypeError(
1253                "unsupported type for linear argument in "
1254                f"LinearProduct: {type(linear).__name__!r}"
1255            )
1256        self._scalar: float = float(scalar)
1257        self._linear: LinearBase = linear
1258
1259    @property
1260    def scalar(self) -> float:
1261        return self._scalar
1262
1263    @property
1264    def linear(self) -> LinearBase:
1265        return self._linear
1266
1267    def _flatten_once_and_add_to(
1268        self,
1269        scale: float,
1270        processed_elements: _ProcessedElements,
1271        target_stack: _ToProcessElements,
1272    ) -> None:
1273        target_stack.append(self._linear, self._scalar * scale)
1274
1275    def __str__(self):
1276        return str(as_flat_linear_expression(self))
1277
1278    def __repr__(self):
1279        result = f"LinearProduct({self._scalar!r}, "
1280        result += f"{self._linear!r})"
1281        return result

A deferred multiplication computation for linear expressions.

This class is immutable.

LinearProduct(scalar: float, linear: LinearBase)
1245    def __init__(self, scalar: float, linear: LinearBase) -> None:
1246        if not isinstance(scalar, (float, int)):
1247            raise TypeError(
1248                "unsupported type for scalar argument in "
1249                f"LinearProduct: {type(scalar).__name__!r}"
1250            )
1251        if not isinstance(linear, LinearBase):
1252            raise TypeError(
1253                "unsupported type for linear argument in "
1254                f"LinearProduct: {type(linear).__name__!r}"
1255            )
1256        self._scalar: float = float(scalar)
1257        self._linear: LinearBase = linear
scalar: float
1259    @property
1260    def scalar(self) -> float:
1261        return self._scalar
linear: LinearBase
1263    @property
1264    def linear(self) -> LinearBase:
1265        return self._linear
class QuadraticProduct(QuadraticBase):
1284class QuadraticProduct(QuadraticBase):
1285    """A deferred multiplication computation for quadratic expressions.
1286
1287    This class is immutable.
1288    """
1289
1290    __slots__ = "_scalar", "_quadratic"
1291
1292    def __init__(self, scalar: float, quadratic: QuadraticBase) -> None:
1293        if not isinstance(scalar, (float, int)):
1294            raise TypeError(
1295                "unsupported type for scalar argument in "
1296                f"QuadraticProduct: {type(scalar).__name__!r}"
1297            )
1298        if not isinstance(quadratic, QuadraticBase):
1299            raise TypeError(
1300                "unsupported type for linear argument in "
1301                f"QuadraticProduct: {type(quadratic).__name__!r}"
1302            )
1303        self._scalar: float = float(scalar)
1304        self._quadratic: QuadraticBase = quadratic
1305
1306    @property
1307    def scalar(self) -> float:
1308        return self._scalar
1309
1310    @property
1311    def quadratic(self) -> QuadraticBase:
1312        return self._quadratic
1313
1314    def _quadratic_flatten_once_and_add_to(
1315        self,
1316        scale: float,
1317        processed_elements: _QuadraticProcessedElements,
1318        target_stack: _QuadraticToProcessElements,
1319    ) -> None:
1320        target_stack.append(self._quadratic, self._scalar * scale)
1321
1322    def __str__(self):
1323        return str(as_flat_quadratic_expression(self))
1324
1325    def __repr__(self):
1326        return f"QuadraticProduct({self._scalar}, {self._quadratic!r})"

A deferred multiplication computation for quadratic expressions.

This class is immutable.

QuadraticProduct( scalar: float, quadratic: QuadraticBase)
1292    def __init__(self, scalar: float, quadratic: QuadraticBase) -> None:
1293        if not isinstance(scalar, (float, int)):
1294            raise TypeError(
1295                "unsupported type for scalar argument in "
1296                f"QuadraticProduct: {type(scalar).__name__!r}"
1297            )
1298        if not isinstance(quadratic, QuadraticBase):
1299            raise TypeError(
1300                "unsupported type for linear argument in "
1301                f"QuadraticProduct: {type(quadratic).__name__!r}"
1302            )
1303        self._scalar: float = float(scalar)
1304        self._quadratic: QuadraticBase = quadratic
scalar: float
1306    @property
1307    def scalar(self) -> float:
1308        return self._scalar
quadratic: QuadraticBase
1310    @property
1311    def quadratic(self) -> QuadraticBase:
1312        return self._quadratic
class LinearLinearProduct(QuadraticBase):
1329class LinearLinearProduct(QuadraticBase):
1330    """A deferred multiplication of two linear expressions.
1331
1332    This class is immutable.
1333    """
1334
1335    __slots__ = "_first_linear", "_second_linear"
1336
1337    def __init__(self, first_linear: LinearBase, second_linear: LinearBase) -> None:
1338        if not isinstance(first_linear, LinearBase):
1339            raise TypeError(
1340                "unsupported type for first_linear argument in "
1341                f"LinearLinearProduct: {type(first_linear).__name__!r}"
1342            )
1343        if not isinstance(second_linear, LinearBase):
1344            raise TypeError(
1345                "unsupported type for second_linear argument in "
1346                f"LinearLinearProduct: {type(second_linear).__name__!r}"
1347            )
1348        self._first_linear: LinearBase = first_linear
1349        self._second_linear: LinearBase = second_linear
1350
1351    @property
1352    def first_linear(self) -> LinearBase:
1353        return self._first_linear
1354
1355    @property
1356    def second_linear(self) -> LinearBase:
1357        return self._second_linear
1358
1359    def _quadratic_flatten_once_and_add_to(
1360        self,
1361        scale: float,
1362        processed_elements: _QuadraticProcessedElements,
1363        target_stack: _QuadraticToProcessElements,
1364    ) -> None:
1365        # A recursion is avoided here because as_flat_linear_expression() must never
1366        # call _quadratic_flatten_once_and_add_to().
1367        first_expression = as_flat_linear_expression(self._first_linear)
1368        second_expression = as_flat_linear_expression(self._second_linear)
1369        processed_elements.offset += (
1370            first_expression.offset * second_expression.offset * scale
1371        )
1372        for first_var, first_val in first_expression.terms.items():
1373            processed_elements.terms[first_var] += (
1374                second_expression.offset * first_val * scale
1375            )
1376        for second_var, second_val in second_expression.terms.items():
1377            processed_elements.terms[second_var] += (
1378                first_expression.offset * second_val * scale
1379            )
1380
1381        for first_var, first_val in first_expression.terms.items():
1382            for second_var, second_val in second_expression.terms.items():
1383                processed_elements.quadratic_terms[
1384                    QuadraticTermKey(first_var, second_var)
1385                ] += (first_val * second_val * scale)
1386
1387    def __str__(self):
1388        return str(as_flat_quadratic_expression(self))
1389
1390    def __repr__(self):
1391        result = "LinearLinearProduct("
1392        result += f"{self._first_linear!r}, "
1393        result += f"{self._second_linear!r})"
1394        return result

A deferred multiplication of two linear expressions.

This class is immutable.

LinearLinearProduct( first_linear: LinearBase, second_linear: LinearBase)
1337    def __init__(self, first_linear: LinearBase, second_linear: LinearBase) -> None:
1338        if not isinstance(first_linear, LinearBase):
1339            raise TypeError(
1340                "unsupported type for first_linear argument in "
1341                f"LinearLinearProduct: {type(first_linear).__name__!r}"
1342            )
1343        if not isinstance(second_linear, LinearBase):
1344            raise TypeError(
1345                "unsupported type for second_linear argument in "
1346                f"LinearLinearProduct: {type(second_linear).__name__!r}"
1347            )
1348        self._first_linear: LinearBase = first_linear
1349        self._second_linear: LinearBase = second_linear
first_linear: LinearBase
1351    @property
1352    def first_linear(self) -> LinearBase:
1353        return self._first_linear
second_linear: LinearBase
1355    @property
1356    def second_linear(self) -> LinearBase:
1357        return self._second_linear
def as_flat_linear_expression( value: int | float | LinearBase) -> LinearExpression:
1397def as_flat_linear_expression(value: LinearTypes) -> LinearExpression:
1398    """Converts floats, ints and Linear objects to a LinearExpression."""
1399    if isinstance(value, LinearExpression):
1400        return value
1401    return LinearExpression(value)

Converts floats, ints and Linear objects to a LinearExpression.

def as_flat_quadratic_expression( value: int | float | LinearBase | QuadraticBase) -> QuadraticExpression:
1404def as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression:
1405    """Converts floats, ints, LinearBase and QuadraticBase objects to a QuadraticExpression."""
1406    if isinstance(value, QuadraticExpression):
1407        return value
1408    return QuadraticExpression(value)

Converts floats, ints, LinearBase and QuadraticBase objects to a QuadraticExpression.