Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 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
d3b0fff
Merge branch 'master' into fix/1061
Zeroto521 Oct 10, 2025
cdfbf82
Merge branch 'master' into fix/1061
Zeroto521 Oct 18, 2025
1601484
keep `_is_number` in expr.pxi
Zeroto521 Oct 18, 2025
a985bda
Update CHANGELOG.md
Zeroto521 Oct 18, 2025
581f63d
Add quotes for annotations
Zeroto521 Oct 18, 2025
e5ba3f3
Merge branch 'fix/1061' of github.com:Zeroto521/PySCIPOpt into fix/1061
Zeroto521 Oct 18, 2025
fcf7921
Merge branch 'master' into fix/1061
Zeroto521 Oct 21, 2025
ad03e9b
Simplify loop via `numpy.ndarray.flat`
Zeroto521 Oct 22, 2025
384c299
Merge branch 'master' into fix/1061
Zeroto521 Oct 22, 2025
cf7ede1
Add shape check for ndarray comparison in _matrixexpr_richcmp
Zeroto521 Oct 22, 2025
47a7cd7
Merge branch 'fix/1061' of https://github.com/Zeroto521/PySCIPOpt int…
Zeroto521 Oct 22, 2025
9d42cac
TST: test MatrixExprCons vs Variable
Zeroto521 Oct 22, 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
### Changed
- Add a PEP 735 dependency group for test dependencies in `pyproject.toml`
- Speed up MatrixVariable.sum(axis=None) via quicksum
- MatrixVariable now supports comparison with Expr
### Removed

## v5.6.0 - 2025.08.26
## 5.6.0 - 2025.08.26
### Added
- More support for AND-Constraints
- Added support for knapsack constraints
Expand Down
42 changes: 22 additions & 20 deletions src/pyscipopt/expr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +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)
Expand All @@ -52,7 +53,8 @@ def _is_number(e):
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):
Expand All @@ -62,7 +64,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
Expand All @@ -71,7 +73,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
Expand All @@ -80,7 +82,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 '=='.")

Expand Down Expand Up @@ -201,7 +203,7 @@ cdef class Expr:
elif isinstance(right, MatrixExpr):
return right + left
else:
raise NotImplementedError
raise TypeError(f"Unsupported type {type(right)}")

return Expr(terms)

Expand All @@ -218,7 +220,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

Expand Down Expand Up @@ -337,26 +339,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)
Expand Down
117 changes: 40 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,34 @@ 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):
res.flat = [_richcmp(i, other, op) for i in self.flat]

elif isinstance(other, np.ndarray):
if self.shape != other.shape:
raise ValueError("Shapes do not match for comparison.")

res.flat = [_richcmp(i, j, op) for i, j in zip(self.flat, other.flat)]

else:
raise TypeError(f"Unsupported type {type(other)}")

return res.view(MatrixExprCons)


class MatrixExpr(np.ndarray):
def sum(self, **kwargs):
"""
Expand All @@ -27,51 +56,15 @@ class MatrixExpr(np.ndarray):
return quicksum(self.flat)
return super().sum(**kwargs)

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)}")

return expr_cons_matrix.view(MatrixExprCons)

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 __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: 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 __add__(self, other):
return super().__add__(other).view(MatrixExpr)

Expand Down Expand Up @@ -110,41 +103,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 '=='.")
42 changes: 42 additions & 0 deletions tests/test_matrix_variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,48 @@ def test_matrix_cons_indicator():
assert m.getVal(z) == 1


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()
x = m.addMatrixVar(3)
var = m.addVar(vtype="B", ub=0)

# test MatrixExprCons vs Variable
with pytest.raises(TypeError):
m.addMatrixCons((x <= 1) >= var)

# test "==" operator
with pytest.raises(NotImplementedError):
m.addMatrixCons((x <= 1) == 1)

# test "<=" and ">=" operator
m.addMatrixCons((x <= 1) >= 1)

m.setObjective(x.sum())
m.optimize()

assert (m.getVal(x) == np.ones(3)).all()


_binop_model = Model()

def var():
Expand Down
Loading