Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
f87432c
Add tests for expression evaluation in Model
Zeroto521 Jan 7, 2026
8c600a2
Add unified expression evaluation with _evaluate methods
Zeroto521 Jan 7, 2026
85ee97e
Optimize matrix expression evaluation in Solution
Zeroto521 Jan 7, 2026
135b954
Add test for matrix variable evaluation
Zeroto521 Jan 8, 2026
ad7d4c6
Remove matrix expression handling in Solution __getitem__
Zeroto521 Jan 8, 2026
5ff4144
Refactor Term class variable naming and usage
Zeroto521 Jan 8, 2026
95b3a80
Use double precision for expression evaluation
Zeroto521 Jan 8, 2026
7770a06
Add type annotations and improve Term class attributes
Zeroto521 Jan 8, 2026
50e9c6c
Make _evaluate methods public and refactor Expr/Variable
Zeroto521 Jan 8, 2026
239c3a3
Update CHANGELOG.md
Zeroto521 Jan 8, 2026
d2ab8e2
Remove hash method from Variable
Zeroto521 Jan 8, 2026
2b145dd
back to old behavior
Zeroto521 Jan 8, 2026
9afb07f
Fix MatrixExpr _evaluate to return ndarray type
Zeroto521 Jan 8, 2026
66c2f6b
Remove unused _evaluate method from Solution stub
Zeroto521 Jan 8, 2026
4ff2682
Add @disjoint_base decorator to Term and UnaryExpr
Zeroto521 Jan 8, 2026
073ac1c
Add noqa to suppress unused import warning
Zeroto521 Jan 8, 2026
22b9f4f
cache `_evaluate` function for matrix
Zeroto521 Jan 9, 2026
449e2fd
Refactor _evaluate to use np.frompyfunc
Zeroto521 Jan 9, 2026
7cd196a
Simplify _evaluate return in MatrixExpr
Zeroto521 Jan 9, 2026
3ff4658
Merge branch 'master' into issue/1062
Zeroto521 Jan 14, 2026
aee875a
Expand test_evaluate with additional variable and cases
Zeroto521 Jan 19, 2026
6470585
Update expected value in test_evaluate assertion
Zeroto521 Jan 19, 2026
93d8b1e
Optimize Expr._evaluate by iterating dict with PyDict_Next
Zeroto521 Jan 19, 2026
dea099b
Merge branch 'master' into issue/1062
Zeroto521 Jan 19, 2026
c6faf4d
Optimize Term evaluation with early exit on zero
Zeroto521 Jan 19, 2026
2eac7dd
Merge branch 'issue/1062' of https://github.com/Zeroto521/PySCIPOpt i…
Zeroto521 Jan 19, 2026
6eaca91
Fix loop variable usage in Term evaluation
Zeroto521 Jan 19, 2026
2007671
Refactor variable names in Term class evaluation
Zeroto521 Jan 19, 2026
0b41acd
Refactor variable initialization in _evaluate methods
Zeroto521 Jan 19, 2026
75956fd
Add exception specification to _evaluate methods
Zeroto521 Jan 19, 2026
33fea88
Fix _evaluate method calls in expression classes
Zeroto521 Jan 19, 2026
cc140b4
Optimize SumExpr and ProdExpr evaluation loops
Zeroto521 Jan 19, 2026
6b56c88
Fix evaluation of UnaryExpr in expression module
Zeroto521 Jan 19, 2026
cc9f254
Merge branch 'master' into issue/1062
Zeroto521 Jan 21, 2026
9f48a61
Fix error message grammar
Zeroto521 Jan 21, 2026
c91a78b
Replace _evaluate with _vec_evaluate in matrix.pxi
Zeroto521 Jan 21, 2026
baf50b2
Change hashval type to Py_ssize_t in Term class
Zeroto521 Jan 21, 2026
db7ea72
Handle 'abs' operator in UnaryExpr evaluation
Zeroto521 Jan 21, 2026
57f906a
Update getSolVal return type to support NDArray
Zeroto521 Jan 21, 2026
8c45b7d
Expand getVal to support GenExpr in Model
Zeroto521 Jan 21, 2026
03cf742
Fix type cast in VarExpr _evaluate method
Zeroto521 Jan 21, 2026
d3121b1
Remove unused include of matrix.pxi
Zeroto521 Jan 21, 2026
10d88d9
Merge branch 'issue/1062' of https://github.com/Zeroto521/PySCIPOpt i…
Zeroto521 Jan 21, 2026
e441513
Update tests/test_expr.py
Joao-Dionisio Jan 21, 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 @@ -10,6 +10,7 @@
- all fundamental callbacks now raise an error if not implemented
- Fixed the type of MatrixExpr.sum(axis=...) result from MatrixVariable to MatrixExpr.
- Updated IIS result in PyiisfinderExec()
- Model.getVal now supports GenExpr type
- Fixed lotsizing_lazy example
- Fixed incorrect getVal() result when _bestSol.sol was outdated
### Changed
Expand Down
105 changes: 97 additions & 8 deletions src/pyscipopt/expr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,20 @@
# which should, in princple, modify the expr. However, since we do not implement __isub__, __sub__
# 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 pyscipopt.scip cimport Variable, Solution
from cpython.dict cimport PyDict_Next
from cpython.ref cimport PyObject

import numpy as np


if TYPE_CHECKING:
double = float


def _is_number(e):
try:
f = float(e)
Expand Down Expand Up @@ -87,23 +98,25 @@ def _expr_richcmp(self, other, op):
raise NotImplementedError("Can only support constraints with '<=', '>=', or '=='.")


class Term:
cdef class Term:
'''This is a monomial term'''

__slots__ = ('vartuple', 'ptrtuple', 'hashval')
cdef readonly tuple vartuple
cdef readonly tuple ptrtuple
cdef Py_ssize_t hashval

def __init__(self, *vartuple):
def __init__(self, *vartuple: Variable):
self.vartuple = tuple(sorted(vartuple, key=lambda v: v.ptr()))
self.ptrtuple = tuple(v.ptr() for v in self.vartuple)
self.hashval = sum(self.ptrtuple)
self.hashval = <Py_ssize_t>hash(self.ptrtuple)

def __getitem__(self, idx):
return self.vartuple[idx]

def __hash__(self):
def __hash__(self) -> Py_ssize_t:
return self.hashval

def __eq__(self, other):
def __eq__(self, other: Term):
return self.ptrtuple == other.ptrtuple

def __len__(self):
Expand All @@ -116,6 +129,20 @@ class Term:
def __repr__(self):
return 'Term(%s)' % ', '.join([str(v) for v in self.vartuple])

cpdef double _evaluate(self, Solution sol) except *:
cdef double res = 1.0
cdef SCIP* scip_ptr = sol.scip
cdef SCIP_SOL* sol_ptr = sol.sol
cdef int i = 0, n = len(self)
cdef Variable var

for i in range(n):
var = <Variable>self.vartuple[i]
res *= SCIPgetSolVal(scip_ptr, sol_ptr, var.scip_var)
if res == 0: # early stop
return 0.0
return res


CONST = Term()

Expand Down Expand Up @@ -157,7 +184,7 @@ def buildGenExprObj(expr):
##@details Polynomial expressions of variables with operator overloading. \n
#See also the @ref ExprDetails "description" in the expr.pxi.
cdef class Expr:

def __init__(self, terms=None):
'''terms is a dict of variables to coefficients.

Expand Down Expand Up @@ -318,6 +345,20 @@ cdef class Expr:
else:
return max(len(v) for v in self.terms)

cpdef double _evaluate(self, Solution sol) except *:
cdef double res = 0
cdef Py_ssize_t pos = <Py_ssize_t>0
cdef PyObject* key_ptr
cdef PyObject* val_ptr
cdef Term term
cdef double coef

while PyDict_Next(self.terms, &pos, &key_ptr, &val_ptr):
term = <Term>key_ptr
coef = <double>(<object>val_ptr)
res += coef * term._evaluate(sol)
return res


cdef class ExprCons:
'''Constraints with a polynomial expressions and lower/upper bounds.'''
Expand Down Expand Up @@ -427,10 +468,10 @@ Operator = Op()
#
#See also the @ref ExprDetails "description" in the expr.pxi.
cdef class GenExpr:

cdef public _op
cdef public children


def __init__(self): # do we need it
''' '''

Expand Down Expand Up @@ -625,44 +666,88 @@ cdef class SumExpr(GenExpr):
def __repr__(self):
return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")"

cpdef double _evaluate(self, Solution sol) except *:
cdef double res = self.constant
cdef int i = 0, n = len(self.children)
cdef list children = self.children
cdef list coefs = self.coefs
for i in range(n):
res += <double>coefs[i] * (<GenExpr>children[i])._evaluate(sol)
return res


# Prod Expressions
cdef class ProdExpr(GenExpr):

cdef public constant

def __init__(self):
self.constant = 1.0
self.children = []
self._op = Operator.prod

def __repr__(self):
return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")"

cpdef double _evaluate(self, Solution sol) except *:
cdef double res = self.constant
cdef list children = self.children
cdef int i = 0, n = len(children)
for i in range(n):
res *= (<GenExpr>children[i])._evaluate(sol)
if res == 0: # early stop
return 0.0
return res


# Var Expressions
cdef class VarExpr(GenExpr):

cdef public var

def __init__(self, var):
self.children = [var]
self._op = Operator.varidx

def __repr__(self):
return self.children[0].__repr__()

cpdef double _evaluate(self, Solution sol) except *:
return (<Expr>self.children[0])._evaluate(sol)


# Pow Expressions
cdef class PowExpr(GenExpr):

cdef public expo

def __init__(self):
self.expo = 1.0
self.children = []
self._op = Operator.power

def __repr__(self):
return self._op + "(" + self.children[0].__repr__() + "," + str(self.expo) + ")"

cpdef double _evaluate(self, Solution sol) except *:
return (<GenExpr>self.children[0])._evaluate(sol) ** self.expo


# Exp, Log, Sqrt, Sin, Cos Expressions
cdef class UnaryExpr(GenExpr):
def __init__(self, op, expr):
self.children = []
self.children.append(expr)
self._op = op

def __repr__(self):
return self._op + "(" + self.children[0].__repr__() + ")"

cpdef double _evaluate(self, Solution sol) except *:
cdef double res = (<GenExpr>self.children[0])._evaluate(sol)
return math.fabs(res) if self._op == "abs" else getattr(math, self._op)(res)


# class for constant expressions
cdef class Constant(GenExpr):
cdef public number
Expand All @@ -673,6 +758,10 @@ cdef class Constant(GenExpr):
def __repr__(self):
return str(self.number)

cpdef double _evaluate(self, Solution sol) except *:
return self.number


def exp(expr):
"""returns expression with exp-function"""
if isinstance(expr, MatrixExpr):
Expand Down
9 changes: 9 additions & 0 deletions src/pyscipopt/matrix.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""
from typing import Literal, Optional, Tuple, Union
import numpy as np
from numpy.typing import NDArray
try:
# NumPy 2.x location
from numpy.lib.array_utils import normalize_axis_tuple
Expand All @@ -12,6 +13,7 @@ except ImportError:
from numpy.core.numeric import normalize_axis_tuple

cimport numpy as cnp
from pyscipopt.scip cimport Expr, Solution

cnp.import_array()

Expand Down Expand Up @@ -142,6 +144,10 @@ class MatrixExpr(np.ndarray):
return super().__rsub__(other).view(MatrixExpr)


def _evaluate(self, Solution sol) -> NDArray[np.float64]:
return _vec_evaluate(self, sol).view(np.ndarray)


class MatrixGenExpr(MatrixExpr):
pass

Expand All @@ -166,6 +172,9 @@ cdef inline _ensure_array(arg, bool convert_scalar = True):
return np.array(arg, dtype=object) if convert_scalar else arg


_vec_evaluate = np.frompyfunc(lambda expr, sol: expr._evaluate(sol), 2, 1)


def _core_dot(cnp.ndarray a, cnp.ndarray b) -> Union[Expr, np.ndarray]:
"""
Perform matrix multiplication between a N-Demension constant array and a N-Demension
Expand Down
2 changes: 2 additions & 0 deletions src/pyscipopt/scip.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -2110,6 +2110,8 @@ cdef extern from "tpi/tpi.h":
cdef class Expr:
cdef public terms

cpdef double _evaluate(self, Solution sol)

cdef class Event:
cdef SCIP_EVENT* event
# can be used to store problem data
Expand Down
48 changes: 15 additions & 33 deletions src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from typing import Union

import numpy as np
from numpy.typing import NDArray

include "expr.pxi"
include "lp.pxi"
Expand Down Expand Up @@ -316,7 +317,7 @@
if rc == SCIP_OKAY:
pass
elif rc == SCIP_ERROR:
raise Exception('SCIP: unspecified error!')

Check failure on line 320 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 +336,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 339 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 @@ -1099,29 +1100,8 @@
return sol

def __getitem__(self, expr: Union[Expr, MatrixExpr]):
if isinstance(expr, MatrixExpr):
result = np.zeros(expr.shape, dtype=np.float64)
for idx in np.ndindex(expr.shape):
result[idx] = self.__getitem__(expr[idx])
return result

# fast track for Variable
cdef SCIP_Real coeff
cdef _VarArray wrapper
if isinstance(expr, Variable):
wrapper = _VarArray(expr)
self._checkStage("SCIPgetSolVal")
return SCIPgetSolVal(self.scip, self.sol, wrapper.ptr[0])
return sum(self._evaluate(term)*coeff for term, coeff in expr.terms.items() if coeff != 0)

def _evaluate(self, term):
self._checkStage("SCIPgetSolVal")
result = 1
cdef _VarArray wrapper
wrapper = _VarArray(term.vartuple)
for i in range(len(term.vartuple)):
result *= SCIPgetSolVal(self.scip, self.sol, wrapper.ptr[i])
return result
return expr._evaluate(self)

def __setitem__(self, Variable var, value):
PY_SCIP_CALL(SCIPsetSolVal(self.scip, self.sol, var.scip_var, value))
Expand Down Expand Up @@ -10747,7 +10727,11 @@

return self.getSolObjVal(self._bestSol, original)

def getSolVal(self, Solution sol, Expr expr):
def getSolVal(
self,
Solution sol,
expr: Union[Expr, GenExpr],
) -> Union[float, NDArray[np.float64]]:
"""
Retrieve value of given variable or expression in the given solution or in
the LP/pseudo solution if sol == None
Expand All @@ -10767,24 +10751,22 @@
A variable is also an expression.

"""
if not isinstance(expr, (Expr, GenExpr)):
raise TypeError(
"Argument 'expr' has incorrect type (expected 'Expr' or 'GenExpr', "
f"got {type(expr)})"
)
# no need to create a NULL solution wrapper in case we have a variable
cdef _VarArray wrapper
if sol == None and isinstance(expr, Variable):
wrapper = _VarArray(expr)
return SCIPgetSolVal(self._scip, NULL, wrapper.ptr[0])
if sol == None:
sol = Solution.create(self._scip, NULL)
return sol[expr]
return (sol or Solution.create(self._scip, NULL))[expr]

def getVal(self, expr: Union[Expr, MatrixExpr] ):
def getVal(self, expr: Union[Expr, GenExpr, MatrixExpr] ):
"""
Retrieve the value of the given variable or expression in the best known solution.
Can only be called after solving is completed.

Parameters
----------
expr : Expr ot MatrixExpr
polynomial expression to query the value of
expr : Expr, GenExpr or MatrixExpr

Returns
-------
Expand Down
5 changes: 3 additions & 2 deletions src/pyscipopt/scip.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import ClassVar
from typing import TYPE_CHECKING, ClassVar # noqa: F401

import numpy
from _typeshed import Incomplete
Expand Down Expand Up @@ -2062,7 +2062,6 @@ class Solution:
data: Incomplete
def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ...
def _checkStage(self, method: Incomplete) -> Incomplete: ...
def _evaluate(self, term: Incomplete) -> Incomplete: ...
def getOrigin(self) -> Incomplete: ...
def retransform(self) -> Incomplete: ...
def translate(self, target: Incomplete) -> Incomplete: ...
Expand Down Expand Up @@ -2122,6 +2121,7 @@ class SumExpr(GenExpr):
constant: Incomplete
def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ...

@disjoint_base
class Term:
hashval: Incomplete
ptrtuple: Incomplete
Expand All @@ -2138,6 +2138,7 @@ class Term:
def __lt__(self, other: object) -> bool: ...
def __ne__(self, other: object) -> bool: ...

@disjoint_base
class UnaryExpr(GenExpr):
def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ...

Expand Down
Loading
Loading