-
Notifications
You must be signed in to change notification settings - Fork 277
Return NotImplemented for Expr and GenExpr operators to simplify
#1182
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
Open
Zeroto521
wants to merge
31
commits into
scipopt:master
Choose a base branch
from
Zeroto521:expr/notimplemented
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+89
−127
Open
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
34d712c
Refactor _expr_richcmp for type safety and clarity
Zeroto521 eff8a9f
Refactor type checks to use NUMBER_TYPES tuple
Zeroto521 a531bb3
Update operator overloads to return NotImplemented for invalid types
Zeroto521 a784346
Update type check in _expr_richcmp function
Zeroto521 3ce74b1
Improve type checking and error handling in expression ops
Zeroto521 7833fb4
Refactor division operator in Expr class
Zeroto521 57b2861
Refactor Expr arithmetic methods to simplify logic
Zeroto521 582283f
Merge branch 'master' into dep/_is_number(self)
Zeroto521 532dee4
Merge branch 'master' into dep/_is_number(self)
Joao-Dionisio 1d0e6f5
Refactor performance tests to use timeit and update assertions
Zeroto521 f67ec3b
use a fixed value for constant
Zeroto521 1515898
Merge branch 'master' into expr/notimplemented
Zeroto521 66fcf0a
Merge branch 'dep/_is_number(self)' into expr/notimplemented
Zeroto521 f2dae45
remove `_is_number`
Zeroto521 9083957
Refactor type checks and arithmetic in Expr and GenExpr
Zeroto521 6fcc519
Merge branch 'master' into expr/notimplemented
Zeroto521 106e2f3
Update changelog for NotImplemented return in Expr classes
Zeroto521 38129ab
Refactor operator type checks for Expr and GenExpr
Zeroto521 736bc0d
Update changelog entry for Expr and GenExpr operators
Zeroto521 59591fc
Fix type checks and error messages in expr and scip modules
Zeroto521 4859daa
Fix type check in _expr_richcmp function
Zeroto521 9c9355f
Ensure exponent base is float in GenExpr
Zeroto521 66b27c5
Ensure float conversion in exponentiation
Zeroto521 3c88e82
Remove _is_number from incomplete stubs
Zeroto521 cad83ff
Fix multiplication with numeric types in Expr
Zeroto521 183b1af
Improve type handling in readStatistics parsing
Zeroto521 0dca5c2
Merge branch 'master' into expr/notimplemented
Zeroto521 4d84056
Return computed res when creating Expr
Zeroto521 0d4a6c2
Merge branch 'master' into expr/notimplemented
Zeroto521 c385d5e
Merge branch 'master' into expr/notimplemented
Zeroto521 2f1d21d
Return NotImplemented for unsupported expr RHS
Zeroto521 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,61 +43,39 @@ | |
| # gets called (I guess) and so a copy is returned. | ||
| # Modifying the expression directly would be a bug, given that the expression might be re-used by the user. </pre> | ||
| import math | ||
| from typing import TYPE_CHECKING | ||
| from typing import TYPE_CHECKING, Union | ||
|
|
||
| import numpy as np | ||
|
|
||
| from cpython.dict cimport PyDict_Next, PyDict_GetItem | ||
| from cpython.object cimport Py_TYPE | ||
| from cpython.object cimport Py_LE, Py_EQ, Py_GE, Py_TYPE | ||
| from cpython.ref cimport PyObject | ||
| from cpython.tuple cimport PyTuple_GET_ITEM | ||
| from pyscipopt.scip cimport Variable, Solution | ||
|
|
||
| import numpy as np | ||
|
|
||
| from pyscipopt.scip cimport Variable, Solution | ||
|
|
||
| if TYPE_CHECKING: | ||
| double = float | ||
|
|
||
|
|
||
| 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 | ||
| def _expr_richcmp(self: Union[Expr, GenExpr], other, int op): | ||
| if not isinstance(other, GENEXPR_OP_TYPES): | ||
| return NotImplemented | ||
|
|
||
|
|
||
| def _expr_richcmp(self, other, op): | ||
| if op == 1: # <= | ||
| if isinstance(other, Expr) or isinstance(other, GenExpr): | ||
| return (self - other) <= 0.0 | ||
| elif _is_number(other): | ||
| if op == Py_LE: | ||
| if isinstance(other, NUMBER_TYPES): | ||
| return ExprCons(self, rhs=float(other)) | ||
| elif isinstance(other, np.ndarray): | ||
| return _expr_richcmp(other, self, 5) | ||
| else: | ||
| raise TypeError(f"Unsupported type {type(other)}") | ||
| elif op == 5: # >= | ||
| if isinstance(other, Expr) or isinstance(other, GenExpr): | ||
| return (self - other) >= 0.0 | ||
| elif _is_number(other): | ||
| return (self - other) <= 0.0 | ||
| elif op == Py_GE: | ||
| if isinstance(other, NUMBER_TYPES): | ||
| return ExprCons(self, lhs=float(other)) | ||
| elif isinstance(other, np.ndarray): | ||
| return _expr_richcmp(other, self, 1) | ||
| else: | ||
| raise TypeError(f"Unsupported type {type(other)}") | ||
| elif op == 2: # == | ||
| if isinstance(other, Expr) or isinstance(other, GenExpr): | ||
| return (self - other) == 0.0 | ||
| elif _is_number(other): | ||
| return (self - other) >= 0.0 | ||
| elif op == Py_EQ: | ||
| if isinstance(other, NUMBER_TYPES): | ||
| return ExprCons(self, lhs=float(other), rhs=float(other)) | ||
| elif isinstance(other, np.ndarray): | ||
| return _expr_richcmp(other, self, 2) | ||
| else: | ||
| raise TypeError(f"Unsupported type {type(other)}") | ||
| else: | ||
| raise NotImplementedError("Can only support constraints with '<=', '>=', or '=='.") | ||
| return (self - other) == 0.0 | ||
| raise NotImplementedError("can only support with '<=', '>=', or '=='") | ||
|
|
||
|
|
||
| cdef class Term: | ||
|
|
@@ -181,9 +159,12 @@ cdef class Term: | |
| CONST = Term() | ||
|
|
||
| # helper function | ||
| def buildGenExprObj(expr): | ||
| def buildGenExprObj(expr: Union[int, float, Expr, GenExpr]) -> GenExpr: | ||
| """helper function to generate an object of type GenExpr""" | ||
| if _is_number(expr): | ||
| if not isinstance(expr, GENEXPR_OP_TYPES): | ||
| raise TypeError(f"Unsupported type {type(expr)}") | ||
|
|
||
| if isinstance(expr, NUMBER_TYPES): | ||
| return Constant(expr) | ||
|
|
||
| elif isinstance(expr, Expr): | ||
|
|
@@ -205,15 +186,7 @@ def buildGenExprObj(expr): | |
| sumexpr += coef * prodexpr | ||
| return sumexpr | ||
|
|
||
| elif isinstance(expr, np.ndarray): | ||
| GenExprs = np.empty(expr.shape, dtype=object) | ||
| for idx in np.ndindex(expr.shape): | ||
| GenExprs[idx] = buildGenExprObj(expr[idx]) | ||
| return GenExprs.view(MatrixExpr) | ||
|
Comment on lines
-208
to
-212
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MatrixExpr is a vector container. It could take care of vector and scalar. |
||
|
|
||
| else: | ||
| assert isinstance(expr, GenExpr) | ||
| return expr | ||
| return expr | ||
|
|
||
| ##@details Polynomial expressions of variables with operator overloading. \n | ||
| #See also the @ref ExprDetails "description" in the expr.pxi. | ||
|
|
@@ -240,6 +213,9 @@ cdef class Expr: | |
| return abs(buildGenExprObj(self)) | ||
|
|
||
| def __add__(self, other): | ||
| if not isinstance(other, EXPR_OP_TYPES): | ||
| return NotImplemented | ||
|
|
||
| left = self | ||
| right = other | ||
| terms = left.terms.copy() | ||
|
|
@@ -248,38 +224,30 @@ cdef class Expr: | |
| # merge the terms by component-wise addition | ||
| for v,c in right.terms.items(): | ||
| terms[v] = terms.get(v, 0.0) + c | ||
| elif _is_number(right): | ||
| elif isinstance(right, NUMBER_TYPES): | ||
| c = float(right) | ||
| terms[CONST] = terms.get(CONST, 0.0) + c | ||
| elif isinstance(right, GenExpr): | ||
| return buildGenExprObj(left) + right | ||
| elif isinstance(right, np.ndarray): | ||
| return right + left | ||
| else: | ||
| raise TypeError(f"Unsupported type {type(right)}") | ||
|
|
||
| return Expr(terms) | ||
|
|
||
| def __iadd__(self, other): | ||
| if not isinstance(other, EXPR_OP_TYPES): | ||
| return NotImplemented | ||
|
|
||
| if isinstance(other, Expr): | ||
| for v,c in other.terms.items(): | ||
| self.terms[v] = self.terms.get(v, 0.0) + c | ||
| elif _is_number(other): | ||
| elif isinstance(other, NUMBER_TYPES): | ||
| c = float(other) | ||
| self.terms[CONST] = self.terms.get(CONST, 0.0) + c | ||
| elif isinstance(other, GenExpr): | ||
| # is no longer in place, might affect performance? | ||
| # can't do `self = buildGenExprObj(self) + other` since I get | ||
| # TypeError: Cannot convert pyscipopt.scip.SumExpr to pyscipopt.scip.Expr | ||
| return buildGenExprObj(self) + other | ||
| else: | ||
| raise TypeError(f"Unsupported type {type(other)}") | ||
|
|
||
| return self | ||
|
|
||
| def __mul__(self, other): | ||
| if isinstance(other, np.ndarray): | ||
| return other * self | ||
| if not isinstance(other, EXPR_OP_TYPES): | ||
| return NotImplemented | ||
|
|
||
| if isinstance(other, NUMBER_TYPES): | ||
| f = float(other) | ||
| return Expr({v: f * c for v, c in self.terms.items()}) | ||
|
|
||
| cdef dict res = {} | ||
| cdef Py_ssize_t pos1 = <Py_ssize_t>0, pos2 = <Py_ssize_t>0 | ||
|
|
@@ -291,36 +259,29 @@ cdef class Expr: | |
| cdef Term child | ||
| cdef double prod_v | ||
|
|
||
| if _is_number(other): | ||
| f = float(other) | ||
| return Expr({v:f*c for v,c in self.terms.items()}) | ||
|
|
||
| elif isinstance(other, Expr): | ||
| while PyDict_Next(self.terms, &pos1, &k1_ptr, &v1_ptr): | ||
| pos2 = <Py_ssize_t>0 | ||
| while PyDict_Next(other.terms, &pos2, &k2_ptr, &v2_ptr): | ||
| child = (<Term>k1_ptr) * (<Term>k2_ptr) | ||
| prod_v = (<double>(<object>v1_ptr)) * (<double>(<object>v2_ptr)) | ||
| if (old_v_ptr := PyDict_GetItem(res, child)) != NULL: | ||
| res[child] = <double>(<object>old_v_ptr) + prod_v | ||
| else: | ||
| res[child] = prod_v | ||
| return Expr(res) | ||
|
|
||
| elif isinstance(other, GenExpr): | ||
| return buildGenExprObj(self) * other | ||
| else: | ||
| raise NotImplementedError | ||
|
|
||
| def __truediv__(self,other): | ||
| if _is_number(other): | ||
| f = 1.0/float(other) | ||
| return f * self | ||
| selfexpr = buildGenExprObj(self) | ||
| return selfexpr.__truediv__(other) | ||
| while PyDict_Next(self.terms, &pos1, &k1_ptr, &v1_ptr): | ||
| pos2 = <Py_ssize_t>0 | ||
| while PyDict_Next(other.terms, &pos2, &k2_ptr, &v2_ptr): | ||
| child = (<Term>k1_ptr) * (<Term>k2_ptr) | ||
| prod_v = (<double>(<object>v1_ptr)) * (<double>(<object>v2_ptr)) | ||
| if (old_v_ptr := PyDict_GetItem(res, child)) != NULL: | ||
| res[child] = <double>(<object>old_v_ptr) + prod_v | ||
| else: | ||
| res[child] = prod_v | ||
| return Expr(res) | ||
|
|
||
| def __truediv__(self, other): | ||
| if not isinstance(other, EXPR_OP_TYPES): | ||
| return NotImplemented | ||
|
|
||
| if isinstance(other, NUMBER_TYPES): | ||
| return 1.0 / other * self | ||
| return buildGenExprObj(self) / other | ||
|
|
||
| def __rtruediv__(self, other): | ||
| ''' other / self ''' | ||
| if not isinstance(other, EXPR_OP_TYPES): | ||
| return NotImplemented | ||
| return buildGenExprObj(other) / self | ||
|
|
||
| def __pow__(self, other, modulo): | ||
|
|
@@ -339,13 +300,11 @@ cdef class Expr: | |
| Implements base**x as scip.exp(x * scip.log(base)). | ||
| Note: base must be positive. | ||
| """ | ||
| if _is_number(other): | ||
| base = float(other) | ||
| if base <= 0.0: | ||
| raise ValueError("Base of a**x must be positive, as expression is reformulated to scip.exp(x * scip.log(a)); got %g" % base) | ||
| return exp(self * log(base)) | ||
| else: | ||
| if not isinstance(other, NUMBER_TYPES): | ||
| raise TypeError(f"Unsupported base type {type(other)} for exponentiation.") | ||
| if other <= 0.0: | ||
| raise ValueError("Base of a**x must be positive, as expression is reformulated to scip.exp(x * scip.log(a)); got %g" % other) | ||
| return exp(self * log(float(other))) | ||
|
|
||
| def __neg__(self): | ||
| return Expr({v:-c for v,c in self.terms.items()}) | ||
|
|
@@ -427,24 +386,21 @@ cdef class ExprCons: | |
|
|
||
| def __richcmp__(self, other, op): | ||
| '''turn it into a constraint''' | ||
| if not isinstance(other, NUMBER_TYPES): | ||
| raise TypeError('Ranged ExprCons is not well defined!') | ||
|
|
||
| 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 NotImplementedError("Ranged ExprCons can only support with '<=' or '>='.") | ||
|
|
@@ -514,8 +470,8 @@ cdef class GenExpr: | |
| return UnaryExpr(Operator.fabs, self) | ||
|
|
||
| def __add__(self, other): | ||
| if isinstance(other, np.ndarray): | ||
| return other + self | ||
| if not isinstance(other, GENEXPR_OP_TYPES): | ||
| return NotImplemented | ||
|
|
||
| left = buildGenExprObj(self) | ||
| right = buildGenExprObj(other) | ||
|
|
@@ -572,8 +528,8 @@ cdef class GenExpr: | |
| # return self | ||
|
|
||
| def __mul__(self, other): | ||
| if isinstance(other, np.ndarray): | ||
| return other * self | ||
| if not isinstance(other, GENEXPR_OP_TYPES): | ||
| return NotImplemented | ||
|
|
||
| left = buildGenExprObj(self) | ||
| right = buildGenExprObj(other) | ||
|
|
@@ -638,16 +594,17 @@ cdef class GenExpr: | |
| Implements base**x as scip.exp(x * scip.log(base)). | ||
| Note: base must be positive. | ||
| """ | ||
| if _is_number(other): | ||
| base = float(other) | ||
| if base <= 0.0: | ||
| raise ValueError("Base of a**x must be positive, as expression is reformulated to scip.exp(x * scip.log(a)); got %g" % base) | ||
| return exp(self * log(base)) | ||
| else: | ||
| if not isinstance(other, NUMBER_TYPES): | ||
| raise TypeError(f"Unsupported base type {type(other)} for exponentiation.") | ||
| if other <= 0.0: | ||
| raise ValueError("Base of a**x must be positive, as expression is reformulated to scip.exp(x * scip.log(a)); got %g" % other) | ||
| return exp(self * log(float(other))) | ||
|
|
||
| #TODO: ipow, idiv, etc | ||
| def __truediv__(self,other): | ||
| if not isinstance(other, GENEXPR_OP_TYPES): | ||
| return NotImplemented | ||
|
|
||
| divisor = buildGenExprObj(other) | ||
| # we can't divide by 0 | ||
| if isinstance(divisor, GenExpr) and divisor.getOp() == Operator.const and divisor.number == 0.0: | ||
|
|
@@ -656,8 +613,9 @@ cdef class GenExpr: | |
|
|
||
| def __rtruediv__(self, other): | ||
| ''' other / self ''' | ||
| otherexpr = buildGenExprObj(other) | ||
| return otherexpr.__truediv__(self) | ||
| if not isinstance(other, GENEXPR_OP_TYPES): | ||
| return NotImplemented | ||
| return buildGenExprObj(other) / self | ||
|
|
||
| def __neg__(self): | ||
| return -1.0 * self | ||
|
|
@@ -905,3 +863,8 @@ def expr_to_array(expr, nodes): | |
| else: # var | ||
| nodes.append( tuple( [op, expr.children] ) ) | ||
| return len(nodes) - 1 | ||
|
|
||
|
|
||
| cdef tuple NUMBER_TYPES = (int, float, np.number) | ||
| cdef tuple EXPR_OP_TYPES = NUMBER_TYPES + (Expr,) | ||
| cdef tuple GENEXPR_OP_TYPES = EXPR_OP_TYPES + (GenExpr,) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
MatrixExprcan handle a vector and a scalar well.