Skip to content

Conversation

Zeroto521
Copy link
Contributor

to fix #1061

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.
Comment on lines -47 to -55
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

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

Copy link
Member

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

Comment on lines +21 to +29
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 '=='.")
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

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.
Comment on lines -127 to -131
if not _is_number(other) or not isinstance(other, MatrixExpr):
raise TypeError('Ranged MatrixExprCons is not well defined!')
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[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

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

@jonathanberthias
Copy link
Contributor

This is great @Zeroto521, I just hit the same issue so thanks a lot for working on this!
Just a quick question, would any more changes be needed to make comparison with GenExprs work?

@Zeroto521
Copy link
Contributor Author

Zeroto521 commented Sep 17, 2025

This is great @Zeroto521, I just hit the same issue so thanks a lot for working on this! Just a quick question, would any more changes be needed to make comparison with GenExprs work?

Expr and GenExpr are two different classes. The relationships between Variable, Term, Expr, and GenExpr are kind of complex. I want to merge Expr and GenExpr and simplify the inheritance.
This pr only helps for Expr now.

@Joao-Dionisio Joao-Dionisio requested a review from Copilot October 17, 2025 09:03

def _matrixexpr_richcmp(self, other, op):
def _richcmp(self, other, op):
if op == 1: # <=
Copy link
Member

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?

Copy link
Contributor

@Copilot Copilot AI left a 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.

Comment on lines +55 to +62
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)
Copy link

Copilot AI Oct 17, 2025

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.

Comment on lines +31 to +43
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)
Copy link

Copilot AI Oct 17, 2025

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
Copy link

Copilot AI Oct 17, 2025

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'.

Suggested change
- 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
Copy link

Copilot AI Oct 17, 2025

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'.

Suggested change
## 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)
Copy link
Member

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: matrix variable can't be compared with expr directly

3 participants