#!/usr/bin/env python3 # Copyright 2010-2025 Google LLC # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Bounded (above and below), upper bounded, and lower bounded expressions.""" import math from typing import Any, Generic, NoReturn, Optional, Type, TypeVar _CHAINED_COMPARISON_MESSAGE = ( "If you were trying to create a two-sided or " "ranged linear inequality of the form `lb <= " "expr <= ub`, try `(lb <= expr) <= ub` instead" ) def _raise_binary_operator_type_error( operator: str, lhs: Type[Any], rhs: Type[Any], extra_message: Optional[str] = None, ) -> NoReturn: """Raises TypeError on unsupported operators.""" message = ( f"unsupported operand type(s) for {operator}: {lhs.__name__!r} and" f" {rhs.__name__!r}" ) if extra_message is not None: message += "\n" + extra_message raise TypeError(message) T = TypeVar("T") class BoundedExpression(Generic[T]): """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`. """ __slots__ = "_expression", "_lower_bound", "_upper_bound" def __init__(self, lower_bound: float, expression: T, upper_bound: float) -> None: self._expression: T = expression self._lower_bound: float = lower_bound self._upper_bound: float = upper_bound @property def expression(self) -> T: return self._expression @property def lower_bound(self) -> float: return self._lower_bound @property def upper_bound(self) -> float: return self._upper_bound def __bool__(self) -> bool: raise TypeError( "__bool__ is unsupported for BoundedExpression" + "\n" + _CHAINED_COMPARISON_MESSAGE ) def __str__(self): return f"{self._lower_bound} <= {self._expression!s} <= {self._upper_bound}" def __repr__(self): return f"{self._lower_bound} <= {self._expression!r} <= {self._upper_bound}" class UpperBoundedExpression(Generic[T]): """An inequality of the form expression <= upper_bound. Where: * expression is a T, and * upper_bound is a float """ __slots__ = "_expression", "_upper_bound" def __init__(self, expression: T, upper_bound: float) -> None: """Operator overloading can be used instead: e.g. `x + y <= 2.0`.""" self._expression: T = expression self._upper_bound: float = upper_bound @property def expression(self) -> T: return self._expression @property def lower_bound(self) -> float: return -math.inf @property def upper_bound(self) -> float: return self._upper_bound def __ge__(self, lhs: float) -> BoundedExpression[T]: if isinstance(lhs, (int, float)): return BoundedExpression[T](lhs, self.expression, self.upper_bound) _raise_binary_operator_type_error(">=", type(self), type(lhs)) def __bool__(self) -> bool: raise TypeError( "__bool__ is unsupported for UpperBoundedExpression" + "\n" + _CHAINED_COMPARISON_MESSAGE ) def __str__(self): return f"{self._expression!s} <= {self._upper_bound}" def __repr__(self): return f"{self._expression!r} <= {self._upper_bound}" class LowerBoundedExpression(Generic[T]): """An inequality of the form expression >= lower_bound. Where: * expression is a linear expression, and * lower_bound is a float """ __slots__ = "_expression", "_lower_bound" def __init__(self, expression: T, lower_bound: float) -> None: """Operator overloading can be used instead: e.g. `x + y >= 2.0`.""" self._expression: T = expression self._lower_bound: float = lower_bound @property def expression(self) -> T: return self._expression @property def lower_bound(self) -> float: return self._lower_bound @property def upper_bound(self) -> float: return math.inf def __le__(self, rhs: float) -> BoundedExpression[T]: if isinstance(rhs, (int, float)): return BoundedExpression[T](self.lower_bound, self.expression, rhs) _raise_binary_operator_type_error("<=", type(self), type(rhs)) def __bool__(self) -> bool: raise TypeError( "__bool__ is unsupported for LowerBoundedExpression" + "\n" + _CHAINED_COMPARISON_MESSAGE ) def __str__(self): return f"{self._expression!s} >= {self._lower_bound}" def __repr__(self): return f"{self._expression!r} >= {self._lower_bound}"