Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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 @@ -9,6 +9,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 supported GenExpr type
- Fixed lotsizing_lazy example
### Changed
- changed default value of enablepricing flag to True
Expand Down
82 changes: 75 additions & 7 deletions src/pyscipopt/expr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,16 @@
# 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
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 could let IDE know what Variable and Solution are.


include "matrix.pxi"

if TYPE_CHECKING:
double = float
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For IDE



def _is_number(e):
try:
Expand Down Expand Up @@ -87,23 +95,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 int 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 = hash(self.ptrtuple)

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

def __hash__(self):
return self.hashval

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

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

cpdef double _evaluate(self, Solution sol):
cdef double res = 1
cdef Variable i
for i in self.vartuple:
res *= SCIPgetSolVal(sol.scip, sol.sol, i.scip_var)
return res


CONST = Term()

Expand Down Expand Up @@ -157,7 +174,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 +335,14 @@ cdef class Expr:
else:
return max(len(v) for v in self.terms)

cpdef double _evaluate(self, Solution sol):
cdef double res = 0
cdef double coef
cdef Term term
for term, coef in self.terms.items():
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 +452,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 +650,83 @@ 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):
cdef double res = self.constant
cdef GenExpr child
cdef double coef
for child, coef in zip(self.children, self.coefs):
res += coef * child._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):
cdef double res = self.constant
cdef GenExpr child
for child in self.children:
res *= child._evaluate(sol)
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):
return 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):
return 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):
return getattr(math, self._op)(self.children[0]._evaluate(sol))


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

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


def exp(expr):
"""returns expression with exp-function"""
if isinstance(expr, MatrixExpr):
Expand Down
11 changes: 11 additions & 0 deletions src/pyscipopt/matrix.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@

from typing import 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
except ImportError:
# Fallback for NumPy 1.x
from numpy.core.numeric import normalize_axis_tuple

from pyscipopt.scip cimport Expr, Solution


def _is_number(e):
try:
Expand Down Expand Up @@ -49,6 +52,9 @@ def _matrixexpr_richcmp(self, other, op):
return res.view(MatrixExprCons)


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


class MatrixExpr(np.ndarray):

def sum(
Expand Down Expand Up @@ -148,9 +154,14 @@ class MatrixExpr(np.ndarray):
def __matmul__(self, other):
return super().__matmul__(other).view(MatrixExpr)

def _evaluate(self, Solution sol) -> NDArray[np.float64]:
Copy link
Contributor Author

@Zeroto521 Zeroto521 Jan 8, 2026

Choose a reason for hiding this comment

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

np.ndarray is not a cdef class, so it can't convert MatrixExpr to a cdef class.
And in MatrixExpr, _evaluate is a pure Python function.

return _evaluate(self, sol).view(np.ndarray)


class MatrixGenExpr(MatrixExpr):
pass


class MatrixExprCons(np.ndarray):

def __le__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons:
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
38 changes: 8 additions & 30 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 @@ -1099,29 +1099,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 +10726,7 @@

return self.getSolObjVal(self._bestSol, original)

def getSolVal(self, Solution sol, Expr expr):
def getSolVal(self, Solution sol, expr: Union[Expr, GenExpr]) -> float:
"""
Retrieve value of given variable or expression in the given solution or in
the LP/pseudo solution if sol == None
Expand All @@ -10767,14 +10746,13 @@
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] ):
"""
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 @@ -2065,7 +2065,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 @@ -2125,6 +2124,7 @@ class SumExpr(GenExpr):
constant: Incomplete
def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ...

@disjoint_base
class Term:
hashval: Incomplete
ptrtuple: Incomplete
Expand All @@ -2141,6 +2141,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
23 changes: 22 additions & 1 deletion tests/test_expr.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import math

import pytest

from pyscipopt import Model, sqrt, log, exp, sin, cos
from pyscipopt.scip import Expr, GenExpr, ExprCons, Term, quicksum
from pyscipopt.scip import Expr, GenExpr, ExprCons, Term


@pytest.fixture(scope="module")
def model():
Expand Down Expand Up @@ -188,3 +191,21 @@ def test_rpow_constant_base(model):

with pytest.raises(ValueError):
c = (-2)**x


def test_evaluate():
m = Model()
x = m.addVar(lb=1, ub=1)
m.optimize()

# test "Expr({Term(x1): 1.0, Term(): 1.0})"
assert m.getVal(x + 1) == 2
# test "prod(1.0,sum(0.0,prod(1.0,x1)),**(sum(0.0,prod(1.0,x1)),-1))"
assert m.getVal(x / x) == 1
# test "**(prod(1.0,**(sum(0.0,prod(1.0,x1)),-1)),2)"
assert m.getVal((1 / x) ** 2) == 1
# test "sin(sum(0.0,prod(1.0,x1)))"
assert round(m.getVal(sin(x)), 6) == round(math.sin(1), 6)

with pytest.raises(TypeError):
m.getVal(1)
9 changes: 9 additions & 0 deletions tests/test_matrix_variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,3 +604,12 @@ def test_broadcast():
m.optimize()

assert (m.getVal(x) == np.zeros((2, 3))).all()


def test_evaluate():
m = Model()
x = m.addMatrixVar((1, 1), lb=1, ub=1)
m.optimize()

assert type(m.getVal(x)) is np.ndarray
assert m.getVal(x).sum() == 1
Loading