Skip to content
Open
Show file tree
Hide file tree
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 Jan 22, 2026
eff8a9f
Refactor type checks to use NUMBER_TYPES tuple
Zeroto521 Jan 22, 2026
a531bb3
Update operator overloads to return NotImplemented for invalid types
Zeroto521 Jan 22, 2026
a784346
Update type check in _expr_richcmp function
Zeroto521 Jan 22, 2026
3ce74b1
Improve type checking and error handling in expression ops
Zeroto521 Jan 22, 2026
7833fb4
Refactor division operator in Expr class
Zeroto521 Jan 22, 2026
57b2861
Refactor Expr arithmetic methods to simplify logic
Zeroto521 Jan 22, 2026
582283f
Merge branch 'master' into dep/_is_number(self)
Zeroto521 Jan 22, 2026
532dee4
Merge branch 'master' into dep/_is_number(self)
Joao-Dionisio Jan 22, 2026
1d0e6f5
Refactor performance tests to use timeit and update assertions
Zeroto521 Jan 23, 2026
f67ec3b
use a fixed value for constant
Zeroto521 Jan 23, 2026
1515898
Merge branch 'master' into expr/notimplemented
Zeroto521 Jan 23, 2026
66fcf0a
Merge branch 'dep/_is_number(self)' into expr/notimplemented
Zeroto521 Jan 25, 2026
f2dae45
remove `_is_number`
Zeroto521 Jan 25, 2026
9083957
Refactor type checks and arithmetic in Expr and GenExpr
Zeroto521 Jan 25, 2026
6fcc519
Merge branch 'master' into expr/notimplemented
Zeroto521 Jan 31, 2026
106e2f3
Update changelog for NotImplemented return in Expr classes
Zeroto521 Jan 31, 2026
38129ab
Refactor operator type checks for Expr and GenExpr
Zeroto521 Jan 31, 2026
736bc0d
Update changelog entry for Expr and GenExpr operators
Zeroto521 Jan 31, 2026
59591fc
Fix type checks and error messages in expr and scip modules
Zeroto521 Jan 31, 2026
4859daa
Fix type check in _expr_richcmp function
Zeroto521 Jan 31, 2026
9c9355f
Ensure exponent base is float in GenExpr
Zeroto521 Jan 31, 2026
66b27c5
Ensure float conversion in exponentiation
Zeroto521 Jan 31, 2026
3c88e82
Remove _is_number from incomplete stubs
Zeroto521 Jan 31, 2026
cad83ff
Fix multiplication with numeric types in Expr
Zeroto521 Feb 1, 2026
183b1af
Improve type handling in readStatistics parsing
Zeroto521 Feb 2, 2026
0dca5c2
Merge branch 'master' into expr/notimplemented
Zeroto521 Feb 2, 2026
4d84056
Return computed res when creating Expr
Zeroto521 Feb 2, 2026
0d4a6c2
Merge branch 'master' into expr/notimplemented
Zeroto521 Feb 5, 2026
c385d5e
Merge branch 'master' into expr/notimplemented
Zeroto521 Feb 6, 2026
2f1d21d
Return NotImplemented for unsupported expr RHS
Zeroto521 Feb 6, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
205 changes: 84 additions & 121 deletions src/pyscipopt/expr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines -95 to -96
Copy link
Contributor Author

@Zeroto521 Zeroto521 Jan 31, 2026

Choose a reason for hiding this comment

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

MatrixExpr can handle a vector and a scalar well.

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

Choose a reason for hiding this comment

The 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.
Expr and GenExpr should only care about scalar types.


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.
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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):
Expand All @@ -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()})
Expand Down Expand Up @@ -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 '>='.")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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,)
9 changes: 4 additions & 5 deletions src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@
if rc == SCIP_OKAY:
pass
elif rc == SCIP_ERROR:
raise Exception('SCIP: unspecified error!')

Check failure on line 319 in src/pyscipopt/scip.pxi

View workflow job for this annotation

GitHub Actions / test-coverage (3.11)

SCIP: unspecified error!
elif rc == SCIP_NOMEMORY:
raise MemoryError('SCIP: insufficient memory error!')
elif rc == SCIP_READERROR:
Expand All @@ -335,7 +335,7 @@
raise Exception('SCIP: method cannot be called at this time'
+ ' in solution process!')
elif rc == SCIP_INVALIDDATA:
raise Exception('SCIP: error in input data!')

Check failure on line 338 in src/pyscipopt/scip.pxi

View workflow job for this annotation

GitHub Actions / test-coverage (3.11)

SCIP: error in input data!
elif rc == SCIP_INVALIDRESULT:
raise Exception('SCIP: method returned an invalid result code!')
elif rc == SCIP_PLUGINNOTFOUND:
Expand Down Expand Up @@ -3992,7 +3992,6 @@

# 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:
Expand Down Expand Up @@ -12711,14 +12710,14 @@
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",
Expand Down
1 change: 0 additions & 1 deletion src/pyscipopt/scip.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down