diff --git a/CHANGELOG.md b/CHANGELOG.md index addf21992..cbb52a7fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased ### Added +### Fixed +### Changed +- MatrixVariable supported to compare with Expr +### Removed + +## 5.6.0 - 2025.08.26 +### Added - More support for AND-Constraints - Added support for knapsack constraints - Added isPositive(), isNegative(), isFeasLE(), isFeasLT(), isFeasGE(), isFeasGT(), isHugeValue(), and tests diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2fc56f5cb..eaec9fa7c 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -44,15 +44,7 @@ # Modifying the expression directly would be a bug, given that the expression might be re-used by the user. include "matrix.pxi" -def _is_number(e): - try: - f = float(e) - return True - except ValueError: # for malformed strings - return False - except TypeError: # for other types (Variable, Expr) - return False - + def _expr_richcmp(self, other, op): if op == 1: # <= if isinstance(other, Expr) or isinstance(other, GenExpr): @@ -62,7 +54,7 @@ def _expr_richcmp(self, other, op): elif isinstance(other, MatrixExpr): return _expr_richcmp(other, self, 5) else: - raise NotImplementedError + raise TypeError(f"Unsupported type {type(other)}") elif op == 5: # >= if isinstance(other, Expr) or isinstance(other, GenExpr): return (self - other) >= 0.0 @@ -71,7 +63,7 @@ def _expr_richcmp(self, other, op): elif isinstance(other, MatrixExpr): return _expr_richcmp(other, self, 1) else: - raise NotImplementedError + raise TypeError(f"Unsupported type {type(other)}") elif op == 2: # == if isinstance(other, Expr) or isinstance(other, GenExpr): return (self - other) == 0.0 @@ -80,7 +72,7 @@ def _expr_richcmp(self, other, op): elif isinstance(other, MatrixExpr): return _expr_richcmp(other, self, 2) else: - raise NotImplementedError + raise TypeError(f"Unsupported type {type(other)}") else: raise NotImplementedError("Can only support constraints with '<=', '>=', or '=='.") @@ -201,7 +193,7 @@ cdef class Expr: elif isinstance(right, MatrixExpr): return right + left else: - raise NotImplementedError + raise TypeError(f"Unsupported type {type(right)}") return Expr(terms) @@ -218,7 +210,7 @@ cdef class Expr: # TypeError: Cannot convert pyscipopt.scip.SumExpr to pyscipopt.scip.Expr return buildGenExprObj(self) + other else: - raise NotImplementedError + raise TypeError(f"Unsupported type {type(other)}") return self @@ -334,26 +326,26 @@ cdef class ExprCons: def __richcmp__(self, other, op): '''turn it into a constraint''' if op == 1: # <= - if not self._rhs is None: - raise TypeError('ExprCons already has upper bound') - assert not self._lhs is None + if not self._rhs is None: + raise TypeError('ExprCons already has upper bound') + assert not self._lhs is None - if not _is_number(other): - raise TypeError('Ranged ExprCons is not well defined!') + if not _is_number(other): + raise TypeError('Ranged ExprCons is not well defined!') - return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) + return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) elif op == 5: # >= - if not self._lhs is None: - raise TypeError('ExprCons already has lower bound') - assert self._lhs is None - assert not self._rhs is None + if not self._lhs is None: + raise TypeError('ExprCons already has lower bound') + assert self._lhs is None + assert not self._rhs is None - if not _is_number(other): - raise TypeError('Ranged ExprCons is not well defined!') + if not _is_number(other): + raise TypeError('Ranged ExprCons is not well defined!') - return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) + return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) else: - raise TypeError + raise NotImplementedError("Ranged ExprCons can only support with '<=' or '>='.") def __repr__(self): return 'ExprCons(%s, %s, %s)' % (self.expr, self._lhs, self._rhs) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 0548fbd10..bafdd9e1e 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -6,6 +6,7 @@ import numpy as np from typing import Union + def _is_number(e): try: f = float(e) @@ -15,6 +16,33 @@ def _is_number(e): except TypeError: # for other types (Variable, Expr) return False + +def _matrixexpr_richcmp(self, other, op): + def _richcmp(self, other, op): + if op == 1: # <= + return self.__le__(other) + elif op == 5: # >= + return self.__ge__(other) + elif op == 2: # == + return self.__eq__(other) + else: + raise NotImplementedError("Can only support constraints with '<=', '>=', or '=='.") + + res = np.empty(self.shape, dtype=object) + if _is_number(other) or isinstance(other, Expr): + for idx in np.ndindex(self.shape): + res[idx] = _richcmp(self[idx], other, op) + + elif isinstance(other, np.ndarray): + for idx in np.ndindex(self.shape): + res[idx] = _richcmp(self[idx], other[idx], op) + + else: + raise TypeError(f"Unsupported type {type(other)}") + + return res.view(MatrixExprCons) + + class MatrixExpr(np.ndarray): def sum(self, **kwargs): """ @@ -24,51 +52,15 @@ class MatrixExpr(np.ndarray): res = super().sum(**kwargs) return res if res.size > 1 else res.item() - def __le__(self, other: Union[float, int, Variable, np.ndarray, 'MatrixExpr']) -> np.ndarray: - - expr_cons_matrix = np.empty(self.shape, dtype=object) - if _is_number(other) or isinstance(other, Variable): - for idx in np.ndindex(self.shape): - expr_cons_matrix[idx] = self[idx] <= other - - elif isinstance(other, np.ndarray): - for idx in np.ndindex(self.shape): - expr_cons_matrix[idx] = self[idx] <= other[idx] - else: - raise TypeError(f"Unsupported type {type(other)}") + def __le__(self, other: Union[float, int, Expr, np.ndarray, 'MatrixExpr']) -> MatrixExprCons: + return _matrixexpr_richcmp(self, other, 1) - return expr_cons_matrix.view(MatrixExprCons) + def __ge__(self, other: Union[float, int, Expr, np.ndarray, 'MatrixExpr']) -> MatrixExprCons: + return _matrixexpr_richcmp(self, other, 5) - def __ge__(self, other: Union[float, int, Variable, np.ndarray, 'MatrixExpr']) -> np.ndarray: - - expr_cons_matrix = np.empty(self.shape, dtype=object) - if _is_number(other) or isinstance(other, Variable): - for idx in np.ndindex(self.shape): - expr_cons_matrix[idx] = self[idx] >= other - - elif isinstance(other, np.ndarray): - for idx in np.ndindex(self.shape): - expr_cons_matrix[idx] = self[idx] >= other[idx] - else: - raise TypeError(f"Unsupported type {type(other)}") + def __eq__(self, other: Union[float, int, Expr, np.ndarray, 'MatrixExpr']) -> MatrixExprCons: + return _matrixexpr_richcmp(self, other, 2) - return expr_cons_matrix.view(MatrixExprCons) - - def __eq__(self, other: Union[float, int, Variable, np.ndarray, 'MatrixExpr']) -> np.ndarray: - - expr_cons_matrix = np.empty(self.shape, dtype=object) - if _is_number(other) or isinstance(other, Variable): - for idx in np.ndindex(self.shape): - expr_cons_matrix[idx] = self[idx] == other - - elif isinstance(other, np.ndarray): - for idx in np.ndindex(self.shape): - expr_cons_matrix[idx] = self[idx] == other[idx] - else: - raise TypeError(f"Unsupported type {type(other)}") - - return expr_cons_matrix.view(MatrixExprCons) - def __add__(self, other): return super().__add__(other).view(MatrixExpr) @@ -104,41 +96,11 @@ class MatrixGenExpr(MatrixExpr): class MatrixExprCons(np.ndarray): - def __le__(self, other: Union[float, int, Variable, MatrixExpr]) -> np.ndarray: - - if not _is_number(other) or not isinstance(other, MatrixExpr): - raise TypeError('Ranged MatrixExprCons is not well defined!') - - expr_cons_matrix = np.empty(self.shape, dtype=object) - if _is_number(other) or isinstance(other, Variable): - for idx in np.ndindex(self.shape): - expr_cons_matrix[idx] = self[idx] <= other - - elif isinstance(other, np.ndarray): - for idx in np.ndindex(self.shape): - expr_cons_matrix[idx] = self[idx] <= other[idx] - else: - raise TypeError(f"Unsupported type {type(other)}") - - return expr_cons_matrix.view(MatrixExprCons) - - def __ge__(self, other: Union[float, int, Variable, MatrixExpr]) -> np.ndarray: - - if not _is_number(other) or not isinstance(other, MatrixExpr): - raise TypeError('Ranged MatrixExprCons is not well defined!') - - expr_cons_matrix = np.empty(self.shape, dtype=object) - if _is_number(other) or isinstance(other, Variable): - for idx in np.ndindex(self.shape): - expr_cons_matrix[idx] = self[idx] >= other - - elif isinstance(other, np.ndarray): - for idx in np.ndindex(self.shape): - expr_cons_matrix[idx] = self[idx] >= other[idx] - else: - raise TypeError(f"Unsupported type {type(other)}") + def __le__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons: + return _matrixexpr_richcmp(self, other, 1) - return expr_cons_matrix.view(MatrixExprCons) + def __ge__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons: + return _matrixexpr_richcmp(self, other, 5) def __eq__(self, other): - raise TypeError + raise NotImplementedError("Cannot compare MatrixExprCons with '=='.") diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index 0308bb694..e3086198c 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -392,3 +392,41 @@ def test_matrix_cons_indicator(): assert m.getVal(is_equal).sum() == 2 assert (m.getVal(x) == m.getVal(y)).all().all() assert (m.getVal(x) == np.array([[5, 5, 5], [5, 5, 5]])).all().all() + + +def test_matrix_compare_with_expr(): + m = Model() + var = m.addVar(vtype="B", ub=0) + + # test "<=" and ">=" operator + x = m.addMatrixVar(3) + m.addMatrixCons(x <= var + 1) + m.addMatrixCons(x >= var + 1) + + # test "==" operator + y = m.addMatrixVar(3) + m.addMatrixCons(y == var + 1) + + m.setObjective(x.sum() + y.sum()) + m.optimize() + + assert (m.getVal(x) == np.ones(3)).all() + assert (m.getVal(y) == np.ones(3)).all() + + +def test_ranged_matrix_cons_with_expr(): + m = Model() + var = m.addVar(vtype="B", ub=0) + + # test "==" operator + with pytest.raises(NotImplementedError): + m.addMatrixCons((m.addMatrixVar(3) <= 1) == var + 1) + + # test "<=" and ">=" operator + x = m.addMatrixVar(3) + m.addMatrixCons((x <= 1) >= 1) + + m.setObjective(x.sum()) + m.optimize() + + assert (m.getVal(x) == np.ones(3)).all()