ortools.math_opt.python.bounded_expressions

Bounded (above and below), upper bounded, and lower bounded 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"""Bounded (above and below), upper bounded, and lower bounded expressions."""
 16
 17import math
 18from typing import Any, Generic, NoReturn, Optional, Type, TypeVar
 19
 20_CHAINED_COMPARISON_MESSAGE = (
 21    "If you were trying to create a two-sided or "
 22    "ranged linear inequality of the form `lb <= "
 23    "expr <= ub`, try `(lb <= expr) <= ub` instead"
 24)
 25
 26
 27def _raise_binary_operator_type_error(
 28    operator: str,
 29    lhs: Type[Any],
 30    rhs: Type[Any],
 31    extra_message: Optional[str] = None,
 32) -> NoReturn:
 33    """Raises TypeError on unsupported operators."""
 34    message = (
 35        f"unsupported operand type(s) for {operator}: {lhs.__name__!r} and"
 36        f" {rhs.__name__!r}"
 37    )
 38    if extra_message is not None:
 39        message += "\n" + extra_message
 40    raise TypeError(message)
 41
 42
 43T = TypeVar("T")
 44
 45
 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}"
 92
 93
 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}"
138
139
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}"
class BoundedExpression(typing.Generic[~T]):
47class BoundedExpression(Generic[T]):
48    """An inequality of the form lower_bound <= expression <= upper_bound.
49
50    Where:
51     * expression is a T, typically LinearBase or QuadraticBase.
52     * lower_bound is a float.
53     * upper_bound is a float.
54
55    Note: Because of limitations related to Python's handling of chained
56    comparisons, bounded expressions cannot be directly created usign
57    overloaded comparisons as in `lower_bound <= expression <= upper_bound`.
58    One solution is to wrap one of the inequalities in parenthesis as in
59    `(lower_bound <= expression) <= upper_bound`.
60    """
61
62    __slots__ = "_expression", "_lower_bound", "_upper_bound"
63
64    def __init__(self, lower_bound: float, expression: T, upper_bound: float) -> None:
65        self._expression: T = expression
66        self._lower_bound: float = lower_bound
67        self._upper_bound: float = upper_bound
68
69    @property
70    def expression(self) -> T:
71        return self._expression
72
73    @property
74    def lower_bound(self) -> float:
75        return self._lower_bound
76
77    @property
78    def upper_bound(self) -> float:
79        return self._upper_bound
80
81    def __bool__(self) -> bool:
82        raise TypeError(
83            "__bool__ is unsupported for BoundedExpression"
84            + "\n"
85            + _CHAINED_COMPARISON_MESSAGE
86        )
87
88    def __str__(self):
89        return f"{self._lower_bound} <= {self._expression!s} <= {self._upper_bound}"
90
91    def __repr__(self):
92        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)
64    def __init__(self, lower_bound: float, expression: T, upper_bound: float) -> None:
65        self._expression: T = expression
66        self._lower_bound: float = lower_bound
67        self._upper_bound: float = upper_bound
expression: ~T
69    @property
70    def expression(self) -> T:
71        return self._expression
lower_bound: float
73    @property
74    def lower_bound(self) -> float:
75        return self._lower_bound
upper_bound: float
77    @property
78    def upper_bound(self) -> float:
79        return self._upper_bound
class UpperBoundedExpression(typing.Generic[~T]):
 95class UpperBoundedExpression(Generic[T]):
 96    """An inequality of the form expression <= upper_bound.
 97
 98    Where:
 99     * expression is a T, and
100     * upper_bound is a float
101    """
102
103    __slots__ = "_expression", "_upper_bound"
104
105    def __init__(self, expression: T, upper_bound: float) -> None:
106        """Operator overloading can be used instead: e.g. `x + y <= 2.0`."""
107        self._expression: T = expression
108        self._upper_bound: float = upper_bound
109
110    @property
111    def expression(self) -> T:
112        return self._expression
113
114    @property
115    def lower_bound(self) -> float:
116        return -math.inf
117
118    @property
119    def upper_bound(self) -> float:
120        return self._upper_bound
121
122    def __ge__(self, lhs: float) -> BoundedExpression[T]:
123        if isinstance(lhs, (int, float)):
124            return BoundedExpression[T](lhs, self.expression, self.upper_bound)
125        _raise_binary_operator_type_error(">=", type(self), type(lhs))
126
127    def __bool__(self) -> bool:
128        raise TypeError(
129            "__bool__ is unsupported for UpperBoundedExpression"
130            + "\n"
131            + _CHAINED_COMPARISON_MESSAGE
132        )
133
134    def __str__(self):
135        return f"{self._expression!s} <= {self._upper_bound}"
136
137    def __repr__(self):
138        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)
105    def __init__(self, expression: T, upper_bound: float) -> None:
106        """Operator overloading can be used instead: e.g. `x + y <= 2.0`."""
107        self._expression: T = expression
108        self._upper_bound: float = upper_bound

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

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

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

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