From d33379cbb15adfebecfef07330a8bdb6b3408887 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 16:54:29 +0800 Subject: [PATCH 01/29] Replace Variable with Expr in MatrixExpr Updated type hints and isinstance checks in MatrixExpr comparison methods to use Expr instead of Variable. This change improves compatibility with broader expression types in matrix operations. --- src/pyscipopt/matrix.pxi | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 0548fbd10..69d98c1c5 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -24,10 +24,10 @@ 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: + def __le__(self, other: Union[float, int, Expr, np.ndarray, 'MatrixExpr']) -> np.ndarray: expr_cons_matrix = np.empty(self.shape, dtype=object) - if _is_number(other) or isinstance(other, Variable): + if _is_number(other) or isinstance(other, Expr): for idx in np.ndindex(self.shape): expr_cons_matrix[idx] = self[idx] <= other @@ -39,10 +39,10 @@ class MatrixExpr(np.ndarray): return expr_cons_matrix.view(MatrixExprCons) - def __ge__(self, other: Union[float, int, Variable, np.ndarray, 'MatrixExpr']) -> np.ndarray: + def __ge__(self, other: Union[float, int, Expr, np.ndarray, 'MatrixExpr']) -> np.ndarray: expr_cons_matrix = np.empty(self.shape, dtype=object) - if _is_number(other) or isinstance(other, Variable): + if _is_number(other) or isinstance(other, Expr): for idx in np.ndindex(self.shape): expr_cons_matrix[idx] = self[idx] >= other @@ -54,10 +54,10 @@ class MatrixExpr(np.ndarray): return expr_cons_matrix.view(MatrixExprCons) - def __eq__(self, other: Union[float, int, Variable, np.ndarray, 'MatrixExpr']) -> np.ndarray: + def __eq__(self, other: Union[float, int, Expr, np.ndarray, 'MatrixExpr']) -> np.ndarray: expr_cons_matrix = np.empty(self.shape, dtype=object) - if _is_number(other) or isinstance(other, Variable): + if _is_number(other) or isinstance(other, Expr): for idx in np.ndindex(self.shape): expr_cons_matrix[idx] = self[idx] == other From 6c48a733cd2c04dd652bf197fa43278b3c6ca24b Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 17:02:49 +0800 Subject: [PATCH 02/29] add test case --- tests/test_matrix_variable.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index 0308bb694..59ba15ab6 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -392,3 +392,22 @@ 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()) + + assert (x == np.ones(3)).all().all() + assert (y == np.ones(3)).all().all() From b578ea59bb44d11943cba5b6c57eaa009c45e9ee Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 17:06:06 +0800 Subject: [PATCH 03/29] Replace Variable with Expr in MatrixExprCons Updated type hints and isinstance checks in MatrixExprCons.__le__ and __ge__ methods to use Expr instead of Variable. This change improves consistency with the expected types for matrix expression constraints. --- src/pyscipopt/matrix.pxi | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 69d98c1c5..227b4a66b 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -104,13 +104,13 @@ class MatrixGenExpr(MatrixExpr): class MatrixExprCons(np.ndarray): - def __le__(self, other: Union[float, int, Variable, MatrixExpr]) -> np.ndarray: + def __le__(self, other: Union[float, int, Expr, 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): + if _is_number(other) or isinstance(other, Expr): for idx in np.ndindex(self.shape): expr_cons_matrix[idx] = self[idx] <= other @@ -122,13 +122,13 @@ class MatrixExprCons(np.ndarray): return expr_cons_matrix.view(MatrixExprCons) - def __ge__(self, other: Union[float, int, Variable, MatrixExpr]) -> np.ndarray: + def __ge__(self, other: Union[float, int, Expr, 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): + if _is_number(other) or isinstance(other, Expr): for idx in np.ndindex(self.shape): expr_cons_matrix[idx] = self[idx] >= other From e8db5a1dfc3e34a7c83db2a6857dcbd377894a96 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 17:06:14 +0800 Subject: [PATCH 04/29] add test case --- tests/test_matrix_variable.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index 59ba15ab6..d0ee54efc 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -411,3 +411,22 @@ def test_matrix_compare_with_expr(): assert (x == np.ones(3)).all().all() assert (y == np.ones(3)).all().all() + + +def test_matrix_cons_compare_with_expr(): + m = Model() + var = m.addVar(vtype="B", ub=0) + + # test "<=" and ">=" operator + x = m.addMatrixVar(3) + m.addMatrixCons(x + 1 <= var + 2) + m.addMatrixCons(x + 1 >= var + 2) + + # test "==" operator + y = m.addMatrixVar(3) + m.addMatrixCons(y + 1 == var + 2) + + m.setObjective(x.sum() + y.sum()) + + assert (x == np.ones(3)).all().all() + assert (y == np.ones(3)).all().all() From aae9df9b9fa72eade91f256d4470c67264474b81 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 17:12:20 +0800 Subject: [PATCH 05/29] Update CHANGELOG.md --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) 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 From d8a9377fea8ddb06c39aa1f7c859bd57dac38db9 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 17:58:02 +0800 Subject: [PATCH 06/29] Add test for ranged matrix constraint Introduces test_ranged_matrix_cons to verify correct behavior when adding a ranged matrix constraint to the model. Ensures that the matrix variable x is set to ones as expected. --- tests/test_matrix_variable.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index d0ee54efc..2631a03c2 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -430,3 +430,15 @@ def test_matrix_cons_compare_with_expr(): assert (x == np.ones(3)).all().all() assert (y == np.ones(3)).all().all() + + +def test_ranged_matrix_cons(): + m = Model() + var = m.addVar(vtype="B", ub=0) + + x = m.addMatrixVar(3) + m.addMatrixCons(var + 1 <= (x <= 1)) + + m.setObjective(x.sum()) + + assert (x == np.ones(3)).all().all() From 99446bcd89b171f80000cad9ccc0f7fc6fc420d0 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 18:01:09 +0800 Subject: [PATCH 07/29] Refactor matrix comparison operators using helper Introduced a shared _matrixexpr_richcmp helper to handle rich comparison logic for MatrixExpr and MatrixExprCons, reducing code duplication and improving maintainability. Updated __le__, __ge__, and __eq__ methods to use this helper, and removed redundant code. --- src/pyscipopt/matrix.pxi | 112 ++++++++++----------------------------- 1 file changed, 28 insertions(+), 84 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 227b4a66b..6a87e412d 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -6,14 +6,24 @@ import numpy as np from typing import Union -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 +include "expr.pxi" + + +def _matrixexpr_richcmp(self, other, op): + res = np.empty(shape, dtype=object) + if _is_number(other) or isinstance(other, Expr): + for idx in np.ndindex(self.shape): + res[idx] = _expr_richcmp(self[idx], other, op) + + elif isinstance(other, np.ndarray): + for idx in np.ndindex(self.shape): + res[idx] = _expr_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 +34,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, Expr, np.ndarray, 'MatrixExpr']) -> np.ndarray: - - expr_cons_matrix = np.empty(self.shape, dtype=object) - if _is_number(other) or isinstance(other, Expr): - 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 __le__(self, other: Union[float, int, Expr, np.ndarray, 'MatrixExpr']) -> MatrixExprCons: + return _matrixexpr_richcmp(self, other, 1) - def __ge__(self, other: Union[float, int, Expr, np.ndarray, 'MatrixExpr']) -> np.ndarray: - - expr_cons_matrix = np.empty(self.shape, dtype=object) - if _is_number(other) or isinstance(other, Expr): - 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 __ge__(self, other: Union[float, int, Expr, np.ndarray, 'MatrixExpr']) -> MatrixExprCons: + return _matrixexpr_richcmp(self, other, 5) - return expr_cons_matrix.view(MatrixExprCons) + def __eq__(self, other: Union[float, int, Expr, np.ndarray, 'MatrixExpr']) -> MatrixExprCons: + return _matrixexpr_richcmp(self, other, 2) - def __eq__(self, other: Union[float, int, Expr, np.ndarray, 'MatrixExpr']) -> np.ndarray: - - expr_cons_matrix = np.empty(self.shape, dtype=object) - if _is_number(other) or isinstance(other, Expr): - 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 +78,11 @@ class MatrixGenExpr(MatrixExpr): class MatrixExprCons(np.ndarray): - def __le__(self, other: Union[float, int, Expr, 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, Expr): - 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, Expr, 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, Expr): - 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 __eq__(self, other): raise TypeError From a09be1a1ede3ae67cbf7784eff1cfae71837ab50 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 18:04:12 +0800 Subject: [PATCH 08/29] Replace TypeError with NotImplementedError in __eq__ The __eq__ method of MatrixExprCons now raises NotImplementedError with a descriptive message instead of TypeError, clarifying that '==' comparison is not supported. --- src/pyscipopt/matrix.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 6a87e412d..089a28844 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -85,4 +85,4 @@ class MatrixExprCons(np.ndarray): return _matrixexpr_richcmp(self, other, 5) def __eq__(self, other): - raise TypeError + raise NotImplementedError("Cannot compare MatrixExprCons with '=='.") From 771437b87b10d11cb3f0a654b3805bf6fe9d4802 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 18:08:52 +0800 Subject: [PATCH 09/29] Add tests for matrix constraint operators Added tests for '<=', '>=', and '==' operators in matrix constraints. Verified correct exception is raised for unsupported '==' operator. --- tests/test_matrix_variable.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index 2631a03c2..d5f1681e6 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -436,9 +436,14 @@ def test_ranged_matrix_cons(): m = Model() var = m.addVar(vtype="B", ub=0) + # test "<=" and ">=" operator x = m.addMatrixVar(3) m.addMatrixCons(var + 1 <= (x <= 1)) + # test "==" operator + with pytest.raises(NotImplementedError): + m.addMatrixCons(0 == (m.addMatrixVar(3) <= 1)) + m.setObjective(x.sum()) assert (x == np.ones(3)).all().all() From 2b9a3c06bc187c1f2e7bb571d8be764180c95092 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 18:10:16 +0800 Subject: [PATCH 10/29] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbb52a7fe..b0f9186fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added ### Fixed ### Changed -- MatrixVariable supported to compare with Expr +- MatrixVariable and MatrixExprCons supported to compare with Expr ### Removed ## 5.6.0 - 2025.08.26 From b7b1321bd1ec4aca6945bbddbcd865149add0c62 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 18:20:54 +0800 Subject: [PATCH 11/29] BUG: fix circular imports Relocated the _is_number utility from expr.pxi to matrix.pxi for better modularity. Updated _matrixexpr_richcmp to use a local _richcmp helper for comparison operations. --- src/pyscipopt/expr.pxi | 10 +--------- src/pyscipopt/matrix.pxi | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2fc56f5cb..252feda04 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): diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 089a28844..db6608144 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -6,18 +6,36 @@ import numpy as np from typing import Union -include "expr.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 _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(shape, dtype=object) if _is_number(other) or isinstance(other, Expr): for idx in np.ndindex(self.shape): - res[idx] = _expr_richcmp(self[idx], other, op) + res[idx] = _richcmp(self[idx], other, op) elif isinstance(other, np.ndarray): for idx in np.ndindex(self.shape): - res[idx] = _expr_richcmp(self[idx], other[idx], op) + res[idx] = _richcmp(self[idx], other[idx], op) else: raise TypeError(f"Unsupported type {type(other)}") From 987c219c74a47b7b15b2dd48f54884d594ea74c0 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 18:25:20 +0800 Subject: [PATCH 12/29] Fix matrix comparison shape handling Replaces usage of undefined 'shape' variable with 'self.shape' when creating the result array in _matrixexpr_richcmp, ensuring correct array dimensions. --- src/pyscipopt/matrix.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index db6608144..4197a5893 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -28,7 +28,7 @@ def _matrixexpr_richcmp(self, other, op): else: raise NotImplementedError("Can only support constraints with '<=', '>=', or '=='.") - res = np.empty(shape, dtype=object) + 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) From 7a1275dc5d07583f43a70cb48a169e7c0b282002 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 18:33:22 +0800 Subject: [PATCH 13/29] Fix redundant .all() calls in matrix variable tests Removed unnecessary double .all() calls in assertions for matrix variable tests, simplifying the checks for equality with np.ones(3). --- tests/test_matrix_variable.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index d5f1681e6..df1d544f2 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -428,8 +428,8 @@ def test_matrix_cons_compare_with_expr(): m.setObjective(x.sum() + y.sum()) - assert (x == np.ones(3)).all().all() - assert (y == np.ones(3)).all().all() + assert (x == np.ones(3)).all() + assert (y == np.ones(3)).all() def test_ranged_matrix_cons(): @@ -446,4 +446,4 @@ def test_ranged_matrix_cons(): m.setObjective(x.sum()) - assert (x == np.ones(3)).all().all() + assert (x == np.ones(3)).all() From f1dc2fac463a99b145cae2efc657cc2d1db72799 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 18:40:35 +0800 Subject: [PATCH 14/29] Fix matrix variable test assertions to use getVal Updated assertions in test_matrix_variable.py to use m.getVal(x) and m.getVal(y) instead of direct variable comparison. This ensures the tests check the evaluated values from the model rather than the symbolic variables. --- tests/test_matrix_variable.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index df1d544f2..f79f82c70 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -428,8 +428,8 @@ def test_matrix_cons_compare_with_expr(): m.setObjective(x.sum() + y.sum()) - assert (x == np.ones(3)).all() - assert (y == np.ones(3)).all() + assert (m.getVal(x) == np.ones(3)).all() + assert (m.getVal(y) == np.ones(3)).all() def test_ranged_matrix_cons(): @@ -446,4 +446,4 @@ def test_ranged_matrix_cons(): m.setObjective(x.sum()) - assert (x == np.ones(3)).all() + assert (m.getVal(x) == np.ones(3)).all() From b6dcf428290604efff3f971d9a9fda5bf7771c94 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 18:44:04 +0800 Subject: [PATCH 15/29] let MatrixExprCons support <= and >= operator --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 252feda04..f471204af 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -51,7 +51,7 @@ def _expr_richcmp(self, other, op): return (self - other) <= 0.0 elif _is_number(other): return ExprCons(self, rhs=float(other)) - elif isinstance(other, MatrixExpr): + elif isinstance(other, (MatrixExpr, MatrixExprCons)): return _expr_richcmp(other, self, 5) else: raise NotImplementedError @@ -60,7 +60,7 @@ def _expr_richcmp(self, other, op): return (self - other) >= 0.0 elif _is_number(other): return ExprCons(self, lhs=float(other)) - elif isinstance(other, MatrixExpr): + elif isinstance(other, (MatrixExpr, MatrixExprCons)): return _expr_richcmp(other, self, 1) else: raise NotImplementedError From 64ae70e513a937313c732f10c7c63c55eb5aa9c8 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 19:56:35 +0800 Subject: [PATCH 16/29] Refactor matrix comparison tests to optimize assertions and remove redundant checks --- tests/test_matrix_variable.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index f79f82c70..3d3595c98 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -408,25 +408,7 @@ def test_matrix_compare_with_expr(): m.addMatrixCons(y == var + 1) m.setObjective(x.sum() + y.sum()) - - assert (x == np.ones(3)).all().all() - assert (y == np.ones(3)).all().all() - - -def test_matrix_cons_compare_with_expr(): - m = Model() - var = m.addVar(vtype="B", ub=0) - - # test "<=" and ">=" operator - x = m.addMatrixVar(3) - m.addMatrixCons(x + 1 <= var + 2) - m.addMatrixCons(x + 1 >= var + 2) - - # test "==" operator - y = m.addMatrixVar(3) - m.addMatrixCons(y + 1 == var + 2) - - m.setObjective(x.sum() + y.sum()) + m.optimize() assert (m.getVal(x) == np.ones(3)).all() assert (m.getVal(y) == np.ones(3)).all() @@ -445,5 +427,6 @@ def test_ranged_matrix_cons(): m.addMatrixCons(0 == (m.addMatrixVar(3) <= 1)) m.setObjective(x.sum()) + m.optimize() assert (m.getVal(x) == np.ones(3)).all() From f69ce7e4efecc2fe8d38ef6ff93259e1d52ed21e Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 20:03:41 +0800 Subject: [PATCH 17/29] let MatrixExprCons support <= and >= operator --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f471204af..56add5f7f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -190,7 +190,7 @@ cdef class Expr: terms[CONST] = terms.get(CONST, 0.0) + c elif isinstance(right, GenExpr): return buildGenExprObj(left) + right - elif isinstance(right, MatrixExpr): + elif isinstance(right, (MatrixExpr, MatrixExprCons)): return right + left else: raise NotImplementedError From 3700261075e50bf499e5b8d884982419d8f724b0 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 20:27:12 +0800 Subject: [PATCH 18/29] find what type it is --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 56add5f7f..047f6f014 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -193,7 +193,7 @@ cdef class Expr: elif isinstance(right, (MatrixExpr, MatrixExprCons)): return right + left else: - raise NotImplementedError + raise TypeError(f"Unsupported type {type(right)}") return Expr(terms) From c677b34031094a2c4246739c44a154abe78c14f8 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 20:35:04 +0800 Subject: [PATCH 19/29] align with `__add__` --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 047f6f014..cca3f770b 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -210,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 From bca7262e119372bf88810c019b35e43fcf4e482b Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 20:39:57 +0800 Subject: [PATCH 20/29] test "==" first --- tests/test_matrix_variable.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index 3d3595c98..ac8e90d13 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -418,14 +418,14 @@ def test_ranged_matrix_cons(): m = Model() var = m.addVar(vtype="B", ub=0) - # test "<=" and ">=" operator - x = m.addMatrixVar(3) - m.addMatrixCons(var + 1 <= (x <= 1)) - # test "==" operator with pytest.raises(NotImplementedError): m.addMatrixCons(0 == (m.addMatrixVar(3) <= 1)) + # test "<=" and ">=" operator + x = m.addMatrixVar(3) + m.addMatrixCons(var + 1 <= (x <= 1)) + m.setObjective(x.sum()) m.optimize() From cb600b26da05e286732777423afb783cc7e8cafe Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 20:41:13 +0800 Subject: [PATCH 21/29] Revert "let MatrixExprCons support <= and >= operator" This reverts commit f69ce7e4efecc2fe8d38ef6ff93259e1d52ed21e. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index cca3f770b..f9d9435e4 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -190,7 +190,7 @@ cdef class Expr: terms[CONST] = terms.get(CONST, 0.0) + c elif isinstance(right, GenExpr): return buildGenExprObj(left) + right - elif isinstance(right, (MatrixExpr, MatrixExprCons)): + elif isinstance(right, MatrixExpr): return right + left else: raise TypeError(f"Unsupported type {type(right)}") From a3a62394965b84981bc3cb91be103d6a38e4686e Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 20:41:19 +0800 Subject: [PATCH 22/29] Revert "let MatrixExprCons support <= and >= operator" This reverts commit b6dcf428290604efff3f971d9a9fda5bf7771c94. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f9d9435e4..d79a7ad7f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -51,7 +51,7 @@ def _expr_richcmp(self, other, op): return (self - other) <= 0.0 elif _is_number(other): return ExprCons(self, rhs=float(other)) - elif isinstance(other, (MatrixExpr, MatrixExprCons)): + elif isinstance(other, MatrixExpr): return _expr_richcmp(other, self, 5) else: raise NotImplementedError @@ -60,7 +60,7 @@ def _expr_richcmp(self, other, op): return (self - other) >= 0.0 elif _is_number(other): return ExprCons(self, lhs=float(other)) - elif isinstance(other, (MatrixExpr, MatrixExprCons)): + elif isinstance(other, MatrixExpr): return _expr_richcmp(other, self, 1) else: raise NotImplementedError From 06f8ebc79412d077887af7ae77cc6c06da6b23b1 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 20:56:32 +0800 Subject: [PATCH 23/29] find what type it is --- src/pyscipopt/expr.pxi | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index d79a7ad7f..94a4a0a1e 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -54,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 @@ -63,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 @@ -72,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 '=='.") @@ -181,7 +181,7 @@ cdef class Expr: left,right = right,left terms = left.terms.copy() - if isinstance(right, Expr): + if isinstance(right, (Expr, ExprCons)): # merge the terms by component-wise addition for v,c in right.terms.items(): terms[v] = terms.get(v, 0.0) + c From ef5aecf76ee9f6cdd1b4b7e18e0c29773b667e31 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 21:33:16 +0800 Subject: [PATCH 24/29] test expr --- tests/test_matrix_variable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index ac8e90d13..2e52e717b 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -414,13 +414,13 @@ def test_matrix_compare_with_expr(): assert (m.getVal(y) == np.ones(3)).all() -def test_ranged_matrix_cons(): +def test_ranged_matrix_cons_with_expr(): m = Model() var = m.addVar(vtype="B", ub=0) # test "==" operator with pytest.raises(NotImplementedError): - m.addMatrixCons(0 == (m.addMatrixVar(3) <= 1)) + m.addMatrixCons(var + 1 == (m.addMatrixVar(3) <= 1)) # test "<=" and ">=" operator x = m.addMatrixVar(3) From ceaab054d2bb597deab50b8466eebcecea10ff2d Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 21:36:48 +0800 Subject: [PATCH 25/29] Change the order --- tests/test_matrix_variable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index 2e52e717b..b7404b05b 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -420,11 +420,11 @@ def test_ranged_matrix_cons_with_expr(): # test "==" operator with pytest.raises(NotImplementedError): - m.addMatrixCons(var + 1 == (m.addMatrixVar(3) <= 1)) + m.addMatrixCons((m.addMatrixVar(3) <= 1) == var + 1) # test "<=" and ">=" operator x = m.addMatrixVar(3) - m.addMatrixCons(var + 1 <= (x <= 1)) + m.addMatrixCons((x <= 1) >= var + 1) m.setObjective(x.sum()) m.optimize() From 386142042b03ea411a6ab969c17c386b86b9c4bd Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 21:41:04 +0800 Subject: [PATCH 26/29] Remove ExprCons Can't add with ExprCons --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 94a4a0a1e..31ff9c8c8 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -181,7 +181,7 @@ cdef class Expr: left,right = right,left terms = left.terms.copy() - if isinstance(right, (Expr, ExprCons)): + if isinstance(right, Expr): # merge the terms by component-wise addition for v,c in right.terms.items(): terms[v] = terms.get(v, 0.0) + c From a2ae9c9676ec5aea3f9a0f3f8894f75789815368 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 21:56:37 +0800 Subject: [PATCH 27/29] Ranged ExprCons requires number --- src/pyscipopt/matrix.pxi | 4 ++-- tests/test_matrix_variable.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 4197a5893..bafdd9e1e 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -96,10 +96,10 @@ class MatrixGenExpr(MatrixExpr): class MatrixExprCons(np.ndarray): - def __le__(self, other: Union[float, int, Expr, np.ndarray, MatrixExpr]) -> MatrixExprCons: + def __le__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons: return _matrixexpr_richcmp(self, other, 1) - def __ge__(self, other: Union[float, int, Expr, np.ndarray, MatrixExpr]) -> MatrixExprCons: + def __ge__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons: return _matrixexpr_richcmp(self, other, 5) def __eq__(self, other): diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index b7404b05b..e3086198c 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -424,7 +424,7 @@ def test_ranged_matrix_cons_with_expr(): # test "<=" and ">=" operator x = m.addMatrixVar(3) - m.addMatrixCons((x <= 1) >= var + 1) + m.addMatrixCons((x <= 1) >= 1) m.setObjective(x.sum()) m.optimize() From 6afa150c0a3fd7b43e2a8a5f6afa2a7ace7fbe59 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 21:56:43 +0800 Subject: [PATCH 28/29] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0f9186fb..cbb52a7fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added ### Fixed ### Changed -- MatrixVariable and MatrixExprCons supported to compare with Expr +- MatrixVariable supported to compare with Expr ### Removed ## 5.6.0 - 2025.08.26 From 88a935f1d347bfe9cc78d31d36ae96be8cd5249d Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Sep 2025 21:59:24 +0800 Subject: [PATCH 29/29] Lint codes with 4 spaces --- src/pyscipopt/expr.pxi | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 31ff9c8c8..eaec9fa7c 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -326,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)