Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d33379c
Replace Variable with Expr in MatrixExpr
Zeroto521 Sep 4, 2025
6c48a73
add test case
Zeroto521 Sep 4, 2025
b578ea5
Replace Variable with Expr in MatrixExprCons
Zeroto521 Sep 4, 2025
e8db5a1
add test case
Zeroto521 Sep 4, 2025
aae9df9
Update CHANGELOG.md
Zeroto521 Sep 4, 2025
d8a9377
Add test for ranged matrix constraint
Zeroto521 Sep 4, 2025
99446bc
Refactor matrix comparison operators using helper
Zeroto521 Sep 4, 2025
a09be1a
Replace TypeError with NotImplementedError in __eq__
Zeroto521 Sep 4, 2025
771437b
Add tests for matrix constraint operators
Zeroto521 Sep 4, 2025
2b9a3c0
Update CHANGELOG.md
Zeroto521 Sep 4, 2025
b7b1321
BUG: fix circular imports
Zeroto521 Sep 4, 2025
987c219
Fix matrix comparison shape handling
Zeroto521 Sep 4, 2025
7a1275d
Fix redundant .all() calls in matrix variable tests
Zeroto521 Sep 4, 2025
f1dc2fa
Fix matrix variable test assertions to use getVal
Zeroto521 Sep 4, 2025
b6dcf42
let MatrixExprCons support <= and >= operator
Zeroto521 Sep 4, 2025
64ae70e
Refactor matrix comparison tests to optimize assertions and remove re…
Zeroto521 Sep 4, 2025
f69ce7e
let MatrixExprCons support <= and >= operator
Zeroto521 Sep 4, 2025
3700261
find what type it is
Zeroto521 Sep 4, 2025
c677b34
align with `__add__`
Zeroto521 Sep 4, 2025
bca7262
test "==" first
Zeroto521 Sep 4, 2025
cb600b2
Revert "let MatrixExprCons support <= and >= operator"
Zeroto521 Sep 4, 2025
a3a6239
Revert "let MatrixExprCons support <= and >= operator"
Zeroto521 Sep 4, 2025
06f8ebc
find what type it is
Zeroto521 Sep 4, 2025
ef5aecf
test expr
Zeroto521 Sep 4, 2025
ceaab05
Change the order
Zeroto521 Sep 4, 2025
3861420
Remove ExprCons
Zeroto521 Sep 4, 2025
a2ae9c9
Ranged ExprCons requires number
Zeroto521 Sep 4, 2025
6afa150
Update CHANGELOG.md
Zeroto521 Sep 4, 2025
88a935f
Lint codes with 4 spaces
Zeroto521 Sep 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased
### Added
### Fixed
### Changed
- MatrixVariable and MatrixExprCons 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
Expand Down
10 changes: 1 addition & 9 deletions src/pyscipopt/expr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,7 @@
# Modifying the expression directly would be a bug, given that the expression might be re-used by the user. </pre>
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

Comment on lines -47 to -55
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove duplicated parts. It also appears in matrix.pxi


def _expr_richcmp(self, other, op):
if op == 1: # <=
if isinstance(other, Expr) or isinstance(other, GenExpr):
Expand Down
116 changes: 39 additions & 77 deletions src/pyscipopt/matrix.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import numpy as np
from typing import Union


def _is_number(e):
try:
f = float(e)
Expand All @@ -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 '=='.")
Comment on lines +21 to +29
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't use expr.pxi/_expr_richcmp. It will cause circular imports


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):
"""
Expand All @@ -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)

Expand Down Expand Up @@ -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!')
Comment on lines -127 to -128
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This checking is duplicated to _expr_richcmp


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 __eq__(self, other):
raise TypeError
raise NotImplementedError("Cannot compare MatrixExprCons with '=='.")
55 changes: 55 additions & 0 deletions tests/test_matrix_variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,3 +392,58 @@ 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()


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()


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()
Loading