Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
bounded_expressions.py
Go to the documentation of this file.
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
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}"
None __init__(self, float lower_bound, T expression, float upper_bound)
NoReturn _raise_binary_operator_type_error(str operator, Type[Any] lhs, Type[Any] rhs, Optional[str] extra_message=None)