Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 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
48 changes: 20 additions & 28 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 All @@ -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
Expand All @@ -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
Expand All @@ -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 '=='.")

Expand Down Expand Up @@ -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)

Expand All @@ -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

Expand Down Expand Up @@ -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:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Use 4 spaces as the indent

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
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, np.ndarray]) -> MatrixExprCons:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ranged ExprCons can only support numbers.
So MatrixExprCons.__ge__ can only receive a number type other.

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 _is_number(other):
raise TypeError('Ranged ExprCons is not well defined!')
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 _is_number(other):
raise TypeError('Ranged ExprCons is not well defined!')
return ExprCons(self.expr, lhs=float(other), rhs=self._rhs)
else:
raise TypeError

A toy demo to show that

from pyscipopt import Model

m = Model()
x = m.addVar(vtype="B", ub=0)
y = m.addVar(vtype="B", ub=0)
# (x <= 1) >= y  # left is (x <= 1) (ExprCons), right is y (Variable)
# Traceback (most recent call last):
#   line 6, in <module>
#     (x <= 1) >= y
#   File "src/pyscipopt/expr.pxi", line 352, in pyscipopt.scip.ExprCons.__richcmp__
# TypeError: Ranged ExprCons is not well defined!
y <= (x <= 1)  # left is y (Variable), right is (x <= 1) (ExprCons)
# Traceback (most recent call last):
#   line 12, in <module>
#     y <= (x <= 1)
#   File "src/pyscipopt/expr.pxi", line 287, in pyscipopt.scip.Expr.__richcmp__
#   File "src/pyscipopt/expr.pxi", line 65, in pyscipopt.scip._expr_richcmp
# NotImplementedError

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 '=='.")
38 changes: 38 additions & 0 deletions tests/test_matrix_variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading