diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e63edb73..5fdad2201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added ### Fixed ### Changed +- Return NotImplemented for `Expr` and `GenExpr` operators, if they can't handle input types in the calculation ### Removed ## 6.1.0 - 2026.01.31 diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f782a46da..e447211d5 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -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. 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) - - 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 = 0, pos2 = 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 = 0 - while PyDict_Next(other.terms, &pos2, &k2_ptr, &v2_ptr): - child = (k1_ptr) * (k2_ptr) - prod_v = ((v1_ptr)) * ((v2_ptr)) - if (old_v_ptr := PyDict_GetItem(res, child)) != NULL: - res[child] = (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 = 0 + while PyDict_Next(other.terms, &pos2, &k2_ptr, &v2_ptr): + child = (k1_ptr) * (k2_ptr) + prod_v = ((v1_ptr)) * ((v2_ptr)) + if (old_v_ptr := PyDict_GetItem(res, child)) != NULL: + res[child] = (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,14 +386,14 @@ 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: @@ -442,9 +401,6 @@ cdef class ExprCons: 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,) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 9c21942bd..8e649a5a4 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3992,7 +3992,6 @@ cdef class Model: # turn the constant value into an Expr instance for further processing if not isinstance(expr, Expr): - assert(_is_number(expr)), "given coefficients are neither Expr or number but %s" % expr.__class__.__name__ expr = Expr() + expr if expr.degree() > 1: @@ -12711,14 +12710,14 @@ def readStatistics(filename): if stat_name == "Gap": relevant_value = relevant_value[:-1] # removing % - if _is_number(relevant_value): + try: result[stat_name] = float(relevant_value) + except: + result[stat_name] = relevant_value + else: if stat_name == "Solutions found" and result[stat_name] == 0: break - else: # it's a string - result[stat_name] = relevant_value - # changing keys to pythonic variable names treated_keys = {"status": "status", "Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", "copying":"copying_time", "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index e8bbc46d5..9590a5f29 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -18,7 +18,6 @@ _core_dot: Incomplete _core_dot_2d: Incomplete _core_sum: Incomplete _expr_richcmp: Incomplete -_is_number: Incomplete buildGenExprObj: Incomplete cos: Incomplete exp: Incomplete