ortools.math_opt.python.bounded_expressions

Bounded (above and below), upper bounded, and lower bounded expressions.

  1# Copyright 2010-2025 Google LLC
  2# Licensed under the Apache License, Version 2.0 (the "License");
  3# you may not use this file except in compliance with the License.
  4# You may obtain a copy of the License at
  5#
  6#     http://www.apache.org/licenses/LICENSE-2.0
  7#
  8# Unless required by applicable law or agreed to in writing, software
  9# distributed under the License is distributed on an "AS IS" BASIS,
 10# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11# See the License for the specific language governing permissions and
 12# limitations under the License.
 13
 14"""Bounded (above and below), upper bounded, and lower bounded expressions."""
 15
 16import math
 17from typing import Any, Generic, NoReturn, Optional, Type, TypeVar
 18
 19_CHAINED_COMPARISON_MESSAGE = (
 20    "If you were trying to create a two-sided or "
 21    "ranged linear inequality of the form `lb <= "
 22    "expr <= ub`, try `(lb <= expr) <= ub` instead"
 23)
 24
 25
 26def _raise_binary_operator_type_error(
 27    operator: str,
 28    lhs: Type[Any],
 29    rhs: Type[Any],
 30    extra_message: Optional[str] = None,
 31) -> NoReturn:
 32    """Raises TypeError on unsupported operators."""
 33    message = (
 34        f"unsupported operand type(s) for {operator}: {lhs.__name__!r} and"
 35        f" {rhs.__name__!r}"
 36    )
 37    if extra_message is not None:
 38        message += "\n" + extra_message
 39    raise TypeError(message)
 40
 41
 42T = TypeVar("T")
 43
 44
 45class BoundedExpression(Generic[T]):
 46    """An inequality of the form lower_bound <= expression <= upper_bound.
 47
 48    Where:
 49     * expression is a T, typically LinearBase or QuadraticBase.
 50     * lower_bound is a float.
 51     * upper_bound is a float.
 52
 53    Note: Because of limitations related to Python's handling of chained
 54    comparisons, bounded expressions cannot be directly created usign
 55    overloaded comparisons as in `lower_bound <= expression <= upper_bound`.
 56    One solution is to wrap one of the inequalities in parenthesis as in
 57    `(lower_bound <= expression) <= upper_bound`.
 58    """
 59
 60    __slots__ = "_expression", "_lower_bound", "_upper_bound"
 61
 62    def __init__(self, lower_bound: float, expression: T, upper_bound: float) -> None:
 63        self._expression: T = expression
 64        self._lower_bound: float = lower_bound
 65        self._upper_bound: float = upper_bound
 66
 67    @property
 68    def expression(self) -> T:
 69        return self._expression
 70
 71    @property
 72    def lower_bound(self) -> float:
 73        return self._lower_bound
 74
 75    @property
 76    def upper_bound(self) -> float:
 77        return self._upper_bound
 78
 79    def __bool__(self) -> bool:
 80        raise TypeError(
 81            "__bool__ is unsupported for BoundedExpression"
 82            + "\n"
 83            + _CHAINED_COMPARISON_MESSAGE
 84        )
 85
 86    def __str__(self):
 87        return f"{self._lower_bound} <= {self._expression!s} <= {self._upper_bound}"
 88
 89    def __repr__(self):
 90        return f"{self._lower_bound} <= {self._expression!r} <= {self._upper_bound}"
 91
 92
 93class UpperBoundedExpression(Generic[T]):
 94    """An inequality of the form expression <= upper_bound.
 95
 96    Where:
 97     * expression is a T, and
 98     * upper_bound is a float
 99    """
100
101    __slots__ = "_expression", "_upper_bound"
102
103    def __init__(self, expression: T, upper_bound: float) -> None:
104        """Operator overloading can be used instead: e.g. `x + y <= 2.0`."""
105        self._expression: T = expression
106        self._upper_bound: float = upper_bound
107
108    @property
109    def expression(self) -> T:
110        return self._expression
111
112    @property
113    def lower_bound(self) -> float:
114        return -math.inf
115
116    @property
117    def upper_bound(self) -> float:
118        return self._upper_bound
119
120    def __ge__(self, lhs: float) -> BoundedExpression[T]:
121        if isinstance(lhs, (int, float)):
122            return BoundedExpression[T](lhs, self.expression, self.upper_bound)
123        _raise_binary_operator_type_error(">=", type(self), type(lhs))
124
125    def __bool__(self) -> bool:
126        raise TypeError(
127            "__bool__ is unsupported for UpperBoundedExpression"
128            + "\n"
129            + _CHAINED_COMPARISON_MESSAGE
130        )
131
132    def __str__(self):
133        return f"{self._expression!s} <= {self._upper_bound}"
134
135    def __repr__(self):
136        return f"{self._expression!r} <= {self._upper_bound}"
137
138
139class LowerBoundedExpression(Generic[T]):
140    """An inequality of the form expression >= lower_bound.
141
142    Where:
143     * expression is a linear expression, and
144     * lower_bound is a float
145    """
146
147    __slots__ = "_expression", "_lower_bound"
148
149    def __init__(self, expression: T, lower_bound: float) -> None:
150        """Operator overloading can be used instead: e.g. `x + y >= 2.0`."""
151        self._expression: T = expression
152        self._lower_bound: float = lower_bound
153
154    @property
155    def expression(self) -> T:
156        return self._expression
157
158    @property
159    def lower_bound(self) -> float:
160        return self._lower_bound
161
162    @property
163    def upper_bound(self) -> float:
164        return math.inf
165
166    def __le__(self, rhs: float) -> BoundedExpression[T]:
167        if isinstance(rhs, (int, float)):
168            return BoundedExpression[T](self.lower_bound, self.expression, rhs)
169        _raise_binary_operator_type_error("<=", type(self), type(rhs))
170
171    def __bool__(self) -> bool:
172        raise TypeError(
173            "__bool__ is unsupported for LowerBoundedExpression"
174            + "\n"
175            + _CHAINED_COMPARISON_MESSAGE
176        )
177
178    def __str__(self):
179        return f"{self._expression!s} >= {self._lower_bound}"
180
181    def __repr__(self):
182        return f"{self._expression!r} >= {self._lower_bound}"
class BoundedExpression(typing.Generic[~T]):
46class BoundedExpression(Generic[T]):
47    """An inequality of the form lower_bound <= expression <= upper_bound.
48
49    Where:
50     * expression is a T, typically LinearBase or QuadraticBase.
51     * lower_bound is a float.
52     * upper_bound is a float.
53
54    Note: Because of limitations related to Python's handling of chained
55    comparisons, bounded expressions cannot be directly created usign
56    overloaded comparisons as in `lower_bound <= expression <= upper_bound`.
57    One solution is to wrap one of the inequalities in parenthesis as in
58    `(lower_bound <= expression) <= upper_bound`.
59    """
60
61    __slots__ = "_expression", "_lower_bound", "_upper_bound"
62
63    def __init__(self, lower_bound: float, expression: T, upper_bound: float) -> None:
64        self._expression: T = expression
65        self._lower_bound: float = lower_bound
66        self._upper_bound: float = upper_bound
67
68    @property
69    def expression(self) -> T:
70        return self._expression
71
72    @property
73    def lower_bound(self) -> float:
74        return self._lower_bound
75
76    @property
77    def upper_bound(self) -> float:
78        return self._upper_bound
79
80    def __bool__(self) -> bool:
81        raise TypeError(
82            "__bool__ is unsupported for BoundedExpression"
83            + "\n"
84            + _CHAINED_COMPARISON_MESSAGE
85        )
86
87    def __str__(self):
88        return f"{self._lower_bound} <= {self._expression!s} <= {self._upper_bound}"
89
90    def __repr__(self):
91        return f"{self._lower_bound} <= {self._expression!r} <= {self._upper_bound}"

An inequality of the form lower_bound <= expression <= upper_bound.

Where:
  • expression is a T, typically LinearBase or QuadraticBase.
  • lower_bound is a float.
  • upper_bound is a float.

Note: Because of limitations related to Python's handling of chained comparisons, bounded expressions cannot be directly created usign overloaded comparisons as in lower_bound <= expression <= upper_bound. One solution is to wrap one of the inequalities in parenthesis as in (lower_bound <= expression) <= upper_bound.

BoundedExpression(lower_bound: float, expression: ~T, upper_bound: float)
63    def __init__(self, lower_bound: float, expression: T, upper_bound: float) -> None:
64        self._expression: T = expression
65        self._lower_bound: float = lower_bound
66        self._upper_bound: float = upper_bound
expression: ~T
68    @property
69    def expression(self) -> T:
70        return self._expression
lower_bound: float
72    @property
73    def lower_bound(self) -> float:
74        return self._lower_bound
upper_bound: float
76    @property
77    def upper_bound(self) -> float:
78        return self._upper_bound
class UpperBoundedExpression(typing.Generic[~T]):
 94class UpperBoundedExpression(Generic[T]):
 95    """An inequality of the form expression <= upper_bound.
 96
 97    Where:
 98     * expression is a T, and
 99     * upper_bound is a float
100    """
101
102    __slots__ = "_expression", "_upper_bound"
103
104    def __init__(self, expression: T, upper_bound: float) -> None:
105        """Operator overloading can be used instead: e.g. `x + y <= 2.0`."""
106        self._expression: T = expression
107        self._upper_bound: float = upper_bound
108
109    @property
110    def expression(self) -> T:
111        return self._expression
112
113    @property
114    def lower_bound(self) -> float:
115        return -math.inf
116
117    @property
118    def upper_bound(self) -> float:
119        return self._upper_bound
120
121    def __ge__(self, lhs: float) -> BoundedExpression[T]:
122        if isinstance(lhs, (int, float)):
123            return BoundedExpression[T](lhs, self.expression, self.upper_bound)
124        _raise_binary_operator_type_error(">=", type(self), type(lhs))
125
126    def __bool__(self) -> bool:
127        raise TypeError(
128            "__bool__ is unsupported for UpperBoundedExpression"
129            + "\n"
130            + _CHAINED_COMPARISON_MESSAGE
131        )
132
133    def __str__(self):
134        return f"{self._expression!s} <= {self._upper_bound}"
135
136    def __repr__(self):
137        return f"{self._expression!r} <= {self._upper_bound}"

An inequality of the form expression <= upper_bound.

Where:
  • expression is a T, and
  • upper_bound is a float
UpperBoundedExpression(expression: ~T, upper_bound: float)
104    def __init__(self, expression: T, upper_bound: float) -> None:
105        """Operator overloading can be used instead: e.g. `x + y <= 2.0`."""
106        self._expression: T = expression
107        self._upper_bound: float = upper_bound

Operator overloading can be used instead: e.g. x + y <= 2.0.

expression: ~T
109    @property
110    def expression(self) -> T:
111        return self._expression
lower_bound: float
113    @property
114    def lower_bound(self) -> float:
115        return -math.inf
upper_bound: float
117    @property
118    def upper_bound(self) -> float:
119        return self._upper_bound
class LowerBoundedExpression(typing.Generic[~T]):
140class LowerBoundedExpression(Generic[T]):
141    """An inequality of the form expression >= lower_bound.
142
143    Where:
144     * expression is a linear expression, and
145     * lower_bound is a float
146    """
147
148    __slots__ = "_expression", "_lower_bound"
149
150    def __init__(self, expression: T, lower_bound: float) -> None:
151        """Operator overloading can be used instead: e.g. `x + y >= 2.0`."""
152        self._expression: T = expression
153        self._lower_bound: float = lower_bound
154
155    @property
156    def expression(self) -> T:
157        return self._expression
158
159    @property
160    def lower_bound(self) -> float:
161        return self._lower_bound
162
163    @property
164    def upper_bound(self) -> float:
165        return math.inf
166
167    def __le__(self, rhs: float) -> BoundedExpression[T]:
168        if isinstance(rhs, (int, float)):
169            return BoundedExpression[T](self.lower_bound, self.expression, rhs)
170        _raise_binary_operator_type_error("<=", type(self), type(rhs))
171
172    def __bool__(self) -> bool:
173        raise TypeError(
174            "__bool__ is unsupported for LowerBoundedExpression"
175            + "\n"
176            + _CHAINED_COMPARISON_MESSAGE
177        )
178
179    def __str__(self):
180        return f"{self._expression!s} >= {self._lower_bound}"
181
182    def __repr__(self):
183        return f"{self._expression!r} >= {self._lower_bound}"

An inequality of the form expression >= lower_bound.

Where:
  • expression is a linear expression, and
  • lower_bound is a float
LowerBoundedExpression(expression: ~T, lower_bound: float)
150    def __init__(self, expression: T, lower_bound: float) -> None:
151        """Operator overloading can be used instead: e.g. `x + y >= 2.0`."""
152        self._expression: T = expression
153        self._lower_bound: float = lower_bound

Operator overloading can be used instead: e.g. x + y >= 2.0.

expression: ~T
155    @property
156    def expression(self) -> T:
157        return self._expression
lower_bound: float
159    @property
160    def lower_bound(self) -> float:
161        return self._lower_bound
upper_bound: float
163    @property
164    def upper_bound(self) -> float:
165        return math.inf