From f87432c517f9cd9fbb660c64cf87be75566b7cf9 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 7 Jan 2026 22:41:19 +0800 Subject: [PATCH 01/19] Add tests for expression evaluation in Model Introduces the test_evaluate function to verify correct evaluation of various expressions using Model.getVal, including arithmetic and trigonometric operations, and checks for TypeError on invalid input. --- tests/test_expr.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index ce79b7cc5..e6b11cdaf 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -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(): @@ -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) From 8c600a291e058d7f833cd043d9252042fdebfb5a Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 7 Jan 2026 22:43:52 +0800 Subject: [PATCH 02/19] Add unified expression evaluation with _evaluate methods Introduced _evaluate methods to Term, Expr, and all GenExpr subclasses for consistent evaluation of expressions and variables. Refactored Solution and Model to use these methods, simplifying and unifying value retrieval for variables and expressions. Cleaned up class definitions and improved hashing and equality for Term and Variable. --- src/pyscipopt/expr.pxi | 92 +++++++++++++++++++++++++++++++++++------- src/pyscipopt/scip.pxd | 10 ----- src/pyscipopt/scip.pxi | 52 ++++++++++-------------- 3 files changed, 98 insertions(+), 56 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f0c406fcb..ba2e0c586 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -42,6 +42,8 @@ # 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. +import math + include "matrix.pxi" @@ -87,34 +89,41 @@ 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 vars + cdef _hash - def __init__(self, *vartuple): - 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) + def __init__(self, *vars): + self.vars = tuple(sorted(vars, key=hash)) + self._hash = hash(self.vars) def __getitem__(self, idx): - return self.vartuple[idx] + return self.vars[idx] def __hash__(self): - return self.hashval + return self._hash - def __eq__(self, other): - return self.ptrtuple == other.ptrtuple + def __eq__(self, other: Term): + return isinstance(other, Term) and self._hash == other._hash def __len__(self): - return len(self.vartuple) + return len(self.vars) def __add__(self, other): - both = self.vartuple + other.vartuple + both = self.vars + other.vars return Term(*both) def __repr__(self): - return 'Term(%s)' % ', '.join([str(v) for v in self.vartuple]) + return 'Term(%s)' % ', '.join([str(v) for v in self.vars]) + + cpdef float _evaluate(self, Solution sol): + cdef float res = 1 + cdef Variable i + for i in self.vars: + res *= SCIPgetSolVal(sol.scip, sol.sol, i.scip_var) + return res CONST = Term() @@ -157,7 +166,9 @@ 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: - + + cdef public terms + def __init__(self, terms=None): '''terms is a dict of variables to coefficients. @@ -318,6 +329,14 @@ cdef class Expr: else: return max(len(v) for v in self.terms) + cpdef float _evaluate(self, Solution sol): + cdef float res = 0 + cdef float 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.''' @@ -427,10 +446,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 ''' ''' @@ -625,44 +644,83 @@ cdef class SumExpr(GenExpr): def __repr__(self): return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")" + cpdef float _evaluate(self, Solution sol): + cdef float res = self.constant + cdef GenExpr child + cdef float 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 float _evaluate(self, Solution sol): + cdef float 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 float _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 float _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 float _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 @@ -673,6 +731,10 @@ cdef class Constant(GenExpr): def __repr__(self): return str(self.number) + cpdef float _evaluate(self, Solution sol): + return self.number + + def exp(expr): """returns expression with exp-function""" if isinstance(expr, MatrixExpr): diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index b9bffc1d6..f31050d82 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2107,9 +2107,6 @@ cdef extern from "scip/scip_var.h": cdef extern from "tpi/tpi.h": int SCIPtpiGetNumThreads() -cdef class Expr: - cdef public terms - cdef class Event: cdef SCIP_EVENT* event # can be used to store problem data @@ -2186,13 +2183,6 @@ cdef class Node: @staticmethod cdef create(SCIP_NODE* scipnode) -cdef class Variable(Expr): - cdef SCIP_VAR* scip_var - # can be used to store problem data - cdef public object data - - @staticmethod - cdef create(SCIP_VAR* scipvar) cdef class Constraint: cdef SCIP_CONS* scip_cons diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index da23028f9..bbf04d3a1 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1099,29 +1099,14 @@ cdef class Solution: return sol def __getitem__(self, expr: Union[Expr, MatrixExpr]): - if isinstance(expr, MatrixExpr): - result = np.zeros(expr.shape, dtype=np.float64) + self._checkStage("SCIPgetSolVal") + if isinstance(expr, (MatrixExpr, MatrixGenExpr)): + res = np.zeros(expr.shape, dtype=np.float64) for idx in np.ndindex(expr.shape): - result[idx] = self.__getitem__(expr[idx]) - return result + res[idx] = expr[idx]._evaluate(self) + return res - # 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)) @@ -1540,6 +1525,10 @@ cdef class Node: cdef class Variable(Expr): """Is a linear expression and has SCIP_VAR*""" + cdef SCIP_VAR* scip_var + # can be used to store problem data + cdef public object data + @staticmethod cdef create(SCIP_VAR* scipvar): """ @@ -1568,10 +1557,12 @@ cdef class Variable(Expr): cname = bytes( SCIPvarGetName(self.scip_var) ) return cname.decode('utf-8') - def ptr(self): - """ """ + def __hash__(self): return (self.scip_var) + def ptr(self): + return hash(self) + def __repr__(self): return self.name @@ -10747,7 +10738,7 @@ cdef class Model: 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 @@ -10767,14 +10758,13 @@ cdef class Model: 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] ): """ From 85ee97e4d7919e7d2a00f4ffbdccf53385676094 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 8 Jan 2026 00:00:30 +0800 Subject: [PATCH 03/19] Optimize matrix expression evaluation in Solution Replaced explicit loop with flat iterator for evaluating matrix expressions in Solution.getVal, improving performance and code clarity. --- src/pyscipopt/scip.pxi | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index bbf04d3a1..1ec85ff96 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1102,10 +1102,8 @@ cdef class Solution: self._checkStage("SCIPgetSolVal") if isinstance(expr, (MatrixExpr, MatrixGenExpr)): res = np.zeros(expr.shape, dtype=np.float64) - for idx in np.ndindex(expr.shape): - res[idx] = expr[idx]._evaluate(self) + res.flat[:] = [i._evaluate(self) for i in expr.flat] return res - return expr._evaluate(self) def __setitem__(self, Variable var, value): From 135b95414befc1ebf4ea30329916ef205d38a557 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 8 Jan 2026 19:36:23 +0800 Subject: [PATCH 04/19] Add test for matrix variable evaluation Introduces a new test 'test_evaluate' to verify correct evaluation of matrix variable division and summation in the model. --- tests/test_matrix_variable.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index e4758f077..ef6c50d40 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -604,3 +604,11 @@ 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 m.getVal(x).sum() == 1 From ad7d4c696c9c79c5675c6341136ec7e9bc6dc371 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 8 Jan 2026 19:37:20 +0800 Subject: [PATCH 05/19] Remove matrix expression handling in Solution __getitem__ Eliminated special handling for MatrixExpr and MatrixGenExpr in Solution.__getitem__, simplifying the method to only evaluate single expressions. This change streamlines the code and removes unnecessary numpy array construction. --- src/pyscipopt/matrix.pxi | 6 ++++++ src/pyscipopt/scip.pxi | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 1a6a09cf3..2835e1a6e 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -5,6 +5,7 @@ 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 @@ -148,9 +149,14 @@ class MatrixExpr(np.ndarray): def __matmul__(self, other): return super().__matmul__(other).view(MatrixExpr) + def _evaluate(self, Solution sol) -> NDArray[np.float64]: + return np.vectorize(lambda e: e._evaluate(sol))(self) + + class MatrixGenExpr(MatrixExpr): pass + class MatrixExprCons(np.ndarray): def __le__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons: diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 1ec85ff96..932fd394f 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1100,10 +1100,6 @@ cdef class Solution: def __getitem__(self, expr: Union[Expr, MatrixExpr]): self._checkStage("SCIPgetSolVal") - if isinstance(expr, (MatrixExpr, MatrixGenExpr)): - res = np.zeros(expr.shape, dtype=np.float64) - res.flat[:] = [i._evaluate(self) for i in expr.flat] - return res return expr._evaluate(self) def __setitem__(self, Variable var, value): From 5ff4144025ba661c4837c1d9776bb1be58878298 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 8 Jan 2026 19:49:12 +0800 Subject: [PATCH 06/19] Refactor Term class variable naming and usage Renamed 'vars' to 'vartuple' and '_hash' to 'hashval' in the Term class for clarity. Updated all references and methods to use the new attribute names, improving code readability and consistency. --- src/pyscipopt/expr.pxi | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index ba2e0c586..353061612 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -92,36 +92,35 @@ def _expr_richcmp(self, other, op): cdef class Term: '''This is a monomial term''' - cdef readonly vars - cdef _hash + cdef readonly vartuple + cdef hashval def __init__(self, *vars): - self.vars = tuple(sorted(vars, key=hash)) - self._hash = hash(self.vars) + self.vartuple = tuple(sorted(vars, key=hash)) + self.hashval = hash(self.vartuple) def __getitem__(self, idx): - return self.vars[idx] + return self.vartuple[idx] def __hash__(self): - return self._hash + return self.hashval def __eq__(self, other: Term): - return isinstance(other, Term) and self._hash == other._hash + return isinstance(other, Term) and hash(self) == hash(other) def __len__(self): - return len(self.vars) + return len(self.vartuple) - def __add__(self, other): - both = self.vars + other.vars - return Term(*both) + def __add__(self, Term other): + return Term(*self.vartuple, *other.vartuple) def __repr__(self): - return 'Term(%s)' % ', '.join([str(v) for v in self.vars]) + return 'Term(%s)' % ', '.join([str(v) for v in self.vartuple]) cpdef float _evaluate(self, Solution sol): cdef float res = 1 cdef Variable i - for i in self.vars: + for i in self.vartuple: res *= SCIPgetSolVal(sol.scip, sol.sol, i.scip_var) return res From 95b3a808ccd8372fc2843ac57e6aef5be10e8cb0 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 8 Jan 2026 19:57:36 +0800 Subject: [PATCH 07/19] Use double precision for expression evaluation Changed internal evaluation methods in expression classes from float to double for improved numerical precision. Updated type declarations and imports accordingly. --- src/pyscipopt/expr.pxi | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 353061612..2db835123 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -44,6 +44,8 @@ # Modifying the expression directly would be a bug, given that the expression might be re-used by the user. import math +from libc.float cimport double + include "matrix.pxi" @@ -117,8 +119,8 @@ cdef class Term: def __repr__(self): return 'Term(%s)' % ', '.join([str(v) for v in self.vartuple]) - cpdef float _evaluate(self, Solution sol): - cdef float res = 1 + cdef 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) @@ -328,9 +330,9 @@ cdef class Expr: else: return max(len(v) for v in self.terms) - cpdef float _evaluate(self, Solution sol): - cdef float res = 0 - cdef float coef + cdef 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) @@ -643,10 +645,10 @@ cdef class SumExpr(GenExpr): def __repr__(self): return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")" - cpdef float _evaluate(self, Solution sol): - cdef float res = self.constant + cdef double _evaluate(self, Solution sol): + cdef double res = self.constant cdef GenExpr child - cdef float coef + cdef double coef for child, coef in zip(self.children, self.coefs): res += coef * child._evaluate(sol) return res @@ -665,8 +667,8 @@ cdef class ProdExpr(GenExpr): def __repr__(self): return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")" - cpdef float _evaluate(self, Solution sol): - cdef float res = self.constant + cdef double _evaluate(self, Solution sol): + cdef double res = self.constant cdef GenExpr child for child in self.children: res *= child._evaluate(sol) @@ -685,7 +687,7 @@ cdef class VarExpr(GenExpr): def __repr__(self): return self.children[0].__repr__() - cpdef float _evaluate(self, Solution sol): + cdef double _evaluate(self, Solution sol): return self.children[0]._evaluate(sol) @@ -702,7 +704,7 @@ cdef class PowExpr(GenExpr): def __repr__(self): return self._op + "(" + self.children[0].__repr__() + "," + str(self.expo) + ")" - cpdef float _evaluate(self, Solution sol): + cdef double _evaluate(self, Solution sol): return self.children[0]._evaluate(sol) ** self.expo @@ -716,7 +718,7 @@ cdef class UnaryExpr(GenExpr): def __repr__(self): return self._op + "(" + self.children[0].__repr__() + ")" - cpdef float _evaluate(self, Solution sol): + cdef double _evaluate(self, Solution sol): return getattr(math, self._op)(self.children[0]._evaluate(sol)) @@ -730,7 +732,7 @@ cdef class Constant(GenExpr): def __repr__(self): return str(self.number) - cpdef float _evaluate(self, Solution sol): + cdef double _evaluate(self, Solution sol): return self.number From 7770a06a28cb3fef3c9ce04fd271dbe933d79369 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 8 Jan 2026 20:05:13 +0800 Subject: [PATCH 08/19] Add type annotations and improve Term class attributes Added type annotations to the Term constructor and specified types for class attributes. This improves code clarity and type safety. --- src/pyscipopt/expr.pxi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2db835123..b0e5b0afd 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -94,10 +94,10 @@ def _expr_richcmp(self, other, op): cdef class Term: '''This is a monomial term''' - cdef readonly vartuple - cdef hashval + cdef readonly tuple vartuple + cdef int hashval - def __init__(self, *vars): + def __init__(self, *vars: Variable): self.vartuple = tuple(sorted(vars, key=hash)) self.hashval = hash(self.vartuple) From 50e9c6c8d5678907606e86441458b791822b911a Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 8 Jan 2026 20:26:47 +0800 Subject: [PATCH 09/19] Make _evaluate methods public and refactor Expr/Variable Changed all _evaluate methods in expression classes from cdef to cpdef to make them accessible from Python. Moved the definition of terms and _evaluate to the Expr class in scip.pxd, and refactored Variable to inherit from Expr in scip.pxd, consolidating class member definitions. These changes improve the interface and maintainability of expression evaluation. --- src/pyscipopt/expr.pxi | 24 +++++++++++++----------- src/pyscipopt/scip.pxd | 12 ++++++++++++ src/pyscipopt/scip.pxi | 4 ---- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b0e5b0afd..9513a8f88 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -43,11 +43,15 @@ # 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 libc.float cimport double +from pyscipopt.scip cimport Variable, Solution include "matrix.pxi" +if TYPE_CHECKING: + double = float + def _is_number(e): try: @@ -119,7 +123,7 @@ cdef class Term: def __repr__(self): return 'Term(%s)' % ', '.join([str(v) for v in self.vartuple]) - cdef double _evaluate(self, Solution sol): + cpdef double _evaluate(self, Solution sol): cdef double res = 1 cdef Variable i for i in self.vartuple: @@ -168,8 +172,6 @@ def buildGenExprObj(expr): #See also the @ref ExprDetails "description" in the expr.pxi. cdef class Expr: - cdef public terms - def __init__(self, terms=None): '''terms is a dict of variables to coefficients. @@ -330,7 +332,7 @@ cdef class Expr: else: return max(len(v) for v in self.terms) - cdef double _evaluate(self, Solution sol): + cpdef double _evaluate(self, Solution sol): cdef double res = 0 cdef double coef cdef Term term @@ -645,7 +647,7 @@ cdef class SumExpr(GenExpr): def __repr__(self): return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")" - cdef double _evaluate(self, Solution sol): + cpdef double _evaluate(self, Solution sol): cdef double res = self.constant cdef GenExpr child cdef double coef @@ -667,7 +669,7 @@ cdef class ProdExpr(GenExpr): def __repr__(self): return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")" - cdef double _evaluate(self, Solution sol): + cpdef double _evaluate(self, Solution sol): cdef double res = self.constant cdef GenExpr child for child in self.children: @@ -687,7 +689,7 @@ cdef class VarExpr(GenExpr): def __repr__(self): return self.children[0].__repr__() - cdef double _evaluate(self, Solution sol): + cpdef double _evaluate(self, Solution sol): return self.children[0]._evaluate(sol) @@ -704,7 +706,7 @@ cdef class PowExpr(GenExpr): def __repr__(self): return self._op + "(" + self.children[0].__repr__() + "," + str(self.expo) + ")" - cdef double _evaluate(self, Solution sol): + cpdef double _evaluate(self, Solution sol): return self.children[0]._evaluate(sol) ** self.expo @@ -718,7 +720,7 @@ cdef class UnaryExpr(GenExpr): def __repr__(self): return self._op + "(" + self.children[0].__repr__() + ")" - cdef double _evaluate(self, Solution sol): + cpdef double _evaluate(self, Solution sol): return getattr(math, self._op)(self.children[0]._evaluate(sol)) @@ -732,7 +734,7 @@ cdef class Constant(GenExpr): def __repr__(self): return str(self.number) - cdef double _evaluate(self, Solution sol): + cpdef double _evaluate(self, Solution sol): return self.number diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index f31050d82..4743a50a8 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2107,6 +2107,11 @@ cdef extern from "scip/scip_var.h": cdef extern from "tpi/tpi.h": int SCIPtpiGetNumThreads() +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 @@ -2183,6 +2188,13 @@ cdef class Node: @staticmethod cdef create(SCIP_NODE* scipnode) +cdef class Variable(Expr): + cdef SCIP_VAR* scip_var + # can be used to store problem data + cdef public object data + + @staticmethod + cdef create(SCIP_VAR* scipvar) cdef class Constraint: cdef SCIP_CONS* scip_cons diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 932fd394f..324887462 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1519,10 +1519,6 @@ cdef class Node: cdef class Variable(Expr): """Is a linear expression and has SCIP_VAR*""" - cdef SCIP_VAR* scip_var - # can be used to store problem data - cdef public object data - @staticmethod cdef create(SCIP_VAR* scipvar): """ From 239c3a346169e1002f72f1d818eb0781365188cc Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 8 Jan 2026 20:29:54 +0800 Subject: [PATCH 10/19] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cec5a1cdc..aa56d65b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ### Changed - changed default value of enablepricing flag to True ### Removed From d2ab8e2faf1a4c2e11a4c107d20adde0999cf512 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 8 Jan 2026 20:46:52 +0800 Subject: [PATCH 11/19] Remove hash method from Variable --- src/pyscipopt/expr.pxi | 11 ++++++----- src/pyscipopt/scip.pxi | 6 ++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 9513a8f88..62747f4b4 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -101,9 +101,9 @@ cdef class Term: cdef readonly tuple vartuple cdef int hashval - def __init__(self, *vars: Variable): - self.vartuple = tuple(sorted(vars, key=hash)) - self.hashval = hash(self.vartuple) + def __init__(self, *vartuple: Variable): + self.vartuple = tuple(sorted(vartuple, key=lambda v: v.ptr())) + self.hashval = hash((v.ptr() for v in self.vartuple)) def __getitem__(self, idx): return self.vartuple[idx] @@ -117,8 +117,9 @@ cdef class Term: def __len__(self): return len(self.vartuple) - def __add__(self, Term other): - return Term(*self.vartuple, *other.vartuple) + def __add__(self, other): + both = self.vartuple + other.vartuple + return Term(*both) def __repr__(self): return 'Term(%s)' % ', '.join([str(v) for v in self.vartuple]) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 324887462..0ab226915 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1547,11 +1547,9 @@ cdef class Variable(Expr): cname = bytes( SCIPvarGetName(self.scip_var) ) return cname.decode('utf-8') - def __hash__(self): - return (self.scip_var) - def ptr(self): - return hash(self) + """ """ + return (self.scip_var) def __repr__(self): return self.name From 2b145dd51ecc3ff60a92a075e30e6cbc42a0b45b Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 8 Jan 2026 21:01:31 +0800 Subject: [PATCH 12/19] back to old behavior --- src/pyscipopt/expr.pxi | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 62747f4b4..84a3d6d99 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -99,11 +99,13 @@ cdef class Term: '''This is a monomial term''' cdef readonly tuple vartuple + cdef readonly tuple ptrtuple cdef int hashval def __init__(self, *vartuple: Variable): self.vartuple = tuple(sorted(vartuple, key=lambda v: v.ptr())) - self.hashval = hash((v.ptr() for v in self.vartuple)) + self.ptrtuple = tuple(v.ptr() for v in self.vartuple) + self.hashval = hash(self.ptrtuple) def __getitem__(self, idx): return self.vartuple[idx] @@ -112,7 +114,7 @@ cdef class Term: return self.hashval def __eq__(self, other: Term): - return isinstance(other, Term) and hash(self) == hash(other) + return self.ptrtuple == other.ptrtuple def __len__(self): return len(self.vartuple) From 9afb07fa8f0414fb90f55b3cb194961373f7c0e5 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 8 Jan 2026 21:11:12 +0800 Subject: [PATCH 13/19] Fix MatrixExpr _evaluate to return ndarray type Ensures that MatrixExpr._evaluate returns a numpy ndarray when appropriate. Adds a test to verify the return type of getVal for matrix variables. --- src/pyscipopt/matrix.pxi | 3 ++- tests/test_matrix_variable.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 2835e1a6e..31350db9b 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -150,7 +150,8 @@ class MatrixExpr(np.ndarray): return super().__matmul__(other).view(MatrixExpr) def _evaluate(self, Solution sol) -> NDArray[np.float64]: - return np.vectorize(lambda e: e._evaluate(sol))(self) + res = np.vectorize(lambda e: e._evaluate(sol))(self) + return res.view(np.ndarray) if isinstance(res, np.ndarray) else res class MatrixGenExpr(MatrixExpr): diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index ef6c50d40..c232abcb6 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -611,4 +611,5 @@ def test_evaluate(): 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 From 66c2f6b18f65cf873502c332b3b0c7a4ba1332b7 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 8 Jan 2026 21:16:07 +0800 Subject: [PATCH 14/19] Remove unused _evaluate method from Solution stub Deleted the _evaluate method from the Solution class in the scip.pyi stub file, as it is no longer needed. Also added TYPE_CHECKING to the typing imports. --- src/pyscipopt/scip.pyi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 831dd02ed..2ccee2e5f 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import ClassVar +from typing import TYPE_CHECKING, ClassVar import numpy from _typeshed import Incomplete @@ -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: ... From 4ff26822f949d351c2ead6f6a527744328c2417e Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 8 Jan 2026 21:16:47 +0800 Subject: [PATCH 15/19] Add @disjoint_base decorator to Term and UnaryExpr Applied the @disjoint_base decorator to the Term and UnaryExpr classes in scip.pyi to clarify their base class relationships. This may improve type checking or class hierarchy handling. --- src/pyscipopt/scip.pyi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 2ccee2e5f..ba29eb418 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -2124,6 +2124,7 @@ class SumExpr(GenExpr): constant: Incomplete def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ... +@disjoint_base class Term: hashval: Incomplete ptrtuple: Incomplete @@ -2140,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: ... From 073ac1c7823d176dd759e9049e8fbc25a9625da4 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 8 Jan 2026 21:19:26 +0800 Subject: [PATCH 16/19] Add noqa to suppress unused import warning Appended '# noqa: F401' to the 'ClassVar' import to suppress linter warnings about unused imports in scip.pyi. --- src/pyscipopt/scip.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index ba29eb418..f373e6ee0 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import TYPE_CHECKING, ClassVar +from typing import TYPE_CHECKING, ClassVar # noqa: F401 import numpy from _typeshed import Incomplete From 22b9f4f86c80e98bbee5154912b6bd128fffad88 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 9 Jan 2026 17:32:05 +0800 Subject: [PATCH 17/19] cache `_evaluate` function for matrix --- src/pyscipopt/matrix.pxi | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 31350db9b..80d402fbe 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -13,6 +13,8 @@ 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: @@ -50,6 +52,11 @@ def _matrixexpr_richcmp(self, other, op): return res.view(MatrixExprCons) +@np.vectorize +def _evaluate(expr: Union[Expr, GenExpr], Solution sol): + return expr._evaluate(sol) + + class MatrixExpr(np.ndarray): def sum( @@ -150,7 +157,7 @@ class MatrixExpr(np.ndarray): return super().__matmul__(other).view(MatrixExpr) def _evaluate(self, Solution sol) -> NDArray[np.float64]: - res = np.vectorize(lambda e: e._evaluate(sol))(self) + res = _evaluate(self, sol) return res.view(np.ndarray) if isinstance(res, np.ndarray) else res From 449e2fd76462932b9d2fc81708a8431279cea996 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 9 Jan 2026 19:34:50 +0800 Subject: [PATCH 18/19] Refactor _evaluate to use np.frompyfunc Replaces the @np.vectorize-decorated _evaluate function with an np.frompyfunc-based implementation for evaluating expressions with solutions. This change may improve compatibility and performance when applying _evaluate to arrays. --- src/pyscipopt/matrix.pxi | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 80d402fbe..b908ecf96 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -52,9 +52,7 @@ def _matrixexpr_richcmp(self, other, op): return res.view(MatrixExprCons) -@np.vectorize -def _evaluate(expr: Union[Expr, GenExpr], Solution sol): - return expr._evaluate(sol) +_evaluate = np.frompyfunc(lambda expr, sol: expr._evaluate(sol), 2, 1) class MatrixExpr(np.ndarray): From 7cd196aa955a0e6b8ea013ea480157fe1c0e138f Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 9 Jan 2026 19:43:35 +0800 Subject: [PATCH 19/19] Simplify _evaluate return in MatrixExpr Refactored the _evaluate method in MatrixExpr to always return the result as a NumPy ndarray, removing the conditional type check. --- src/pyscipopt/matrix.pxi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index b908ecf96..febdeaf84 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -155,8 +155,7 @@ class MatrixExpr(np.ndarray): return super().__matmul__(other).view(MatrixExpr) def _evaluate(self, Solution sol) -> NDArray[np.float64]: - res = _evaluate(self, sol) - return res.view(np.ndarray) if isinstance(res, np.ndarray) else res + return _evaluate(self, sol).view(np.ndarray) class MatrixGenExpr(MatrixExpr):