-
Notifications
You must be signed in to change notification settings - Fork 271
BUG: MatrixExpr can't be compared with Expr #1069
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
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.
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.
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.
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.
The __eq__ method of MatrixExprCons now raises NotImplementedError with a descriptive message instead of TypeError, clarifying that '==' comparison is not supported.
Added tests for '<=', '>=', and '==' operators in matrix constraints. Verified correct exception is raised for unsupported '==' operator.
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.
Replaces usage of undefined 'shape' variable with 'self.shape' when creating the result array in _matrixexpr_richcmp, ensuring correct array dimensions.
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 | ||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rather have this code be here than in matrix.pxi
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 '=='.") |
There was a problem hiding this comment.
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
Removed unnecessary double .all() calls in assertions for matrix variable tests, simplifying the checks for equality with np.ones(3).
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.
if not _is_number(other) or not isinstance(other, MatrixExpr): | ||
raise TypeError('Ranged MatrixExprCons is not well defined!') |
There was a problem hiding this comment.
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
Can't add with ExprCons
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: |
There was a problem hiding this comment.
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
.
PySCIPOpt/src/pyscipopt/expr.pxi
Lines 334 to 356 in c681f94
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
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: |
There was a problem hiding this comment.
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
This is great @Zeroto521, I just hit the same issue so thanks a lot for working on this! |
|
|
||
def _matrixexpr_richcmp(self, other, op): | ||
def _richcmp(self, other, op): | ||
if op == 1: # <= |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are the operations 1,5,2?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Fixes the inability to compare MatrixExpr with Expr by introducing shared comparison logic and adjusting operator overloads, plus tests and changelog updates.
- Add support for MatrixExpr <=, >=, == comparisons against Expr (and arrays) via a central helper.
- Disallow == on ranged Matrix constraints (MatrixExprCons) with a clear error, and add tests.
- Update error types/messages for unsupported comparisons; update CHANGELOG.
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
File | Description |
---|---|
tests/test_matrix_variable.py | Adds tests for MatrixExpr vs Expr comparisons and ranged Matrix constraints behavior. |
src/pyscipopt/matrix.pxi | Refactors comparison handling via _matrixexpr_richcmp; updates MatrixExpr and MatrixExprCons rich comparisons. |
src/pyscipopt/expr.pxi | Harmonizes error types/messages and removes duplicate _is_number; integrates with matrix changes. |
CHANGELOG.md | Notes new capability; adjusts version header format. |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
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) |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forward references in type annotations to Expr and MatrixExprCons can raise NameError at import time because these names are defined later in the include order. Quote the forward-referenced types (and the return type) to avoid runtime evaluation, or remove/relax the annotations. For example: def le(..., other: Union[float, int, 'Expr', np.ndarray, 'MatrixExpr']) -> 'MatrixExprCons'.
Copilot uses AI. Check for mistakes.
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) |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] This change removes support for comparing MatrixExpr with Variable (previously handled via isinstance(other, Variable)). That is a breaking change and will now raise TypeError for x <= var. To preserve backward compatibility while adding Expr support, extend the check to include Variable (and optionally GenExpr), e.g.: if _is_number(other) or isinstance(other, (Expr, Variable, GenExpr)):. Also update the signatures accordingly (using quoted annotations to avoid forward-ref issues).
Copilot uses AI. Check for mistakes.
### Added | ||
### Fixed | ||
### Changed | ||
- MatrixVariable supported to compare with Expr |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Improve grammar: 'MatrixVariable supports comparing with Expr' or 'Add support for comparing MatrixVariable with Expr'.
- MatrixVariable supported to compare with Expr | |
- MatrixVariable now supports comparison with Expr |
Copilot uses AI. Check for mistakes.
### Removed | ||
|
||
## v5.6.0 - 2025.08.26 | ||
## 5.6.0 - 2025.08.26 |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Version header format is inconsistent with prior entries ('## v5.x.x'). For consistency, consider reverting to '## v5.6.0 - 2025.08.26'.
## 5.6.0 - 2025.08.26 | |
## v5.6.0 - 2025.08.26 |
Copilot uses AI. Check for mistakes.
|
||
# test "<=" and ">=" operator | ||
x = m.addMatrixVar(3) | ||
m.addMatrixCons((x <= 1) >= 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this mean? that at least one entry in matrix variable x is at most 1? I find it confusing. If so, why shouldn't the above also work?
to fix #1061