Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
452ca36
Add numpy ufunc support for expression classes
Zeroto521 Jan 21, 2026
4d911c1
Introduce ExprLike base class for expressions
Zeroto521 Jan 21, 2026
b119224
Fix unary ufunc mapping in ExprLike class
Zeroto521 Jan 21, 2026
4a65d98
Add tests for unary operations and numpy compatibility
Zeroto521 Jan 21, 2026
2d8945f
Merge branch 'master' into expr/unary
Zeroto521 Jan 22, 2026
01658f4
Update CHANGELOG.md
Zeroto521 Jan 22, 2026
f0aacb6
Add colon to ExprLike class definition
Zeroto521 Jan 22, 2026
7e78cac
Fix test expectations for variable names in unary ops
Zeroto521 Jan 22, 2026
196f43a
Fix expected output format in test_unary
Zeroto521 Jan 22, 2026
6dd2c93
Fix expected output for sin function in test_unary
Zeroto521 Jan 22, 2026
ee4c6f6
Merge branch 'master' into expr/unary
Zeroto521 Jan 22, 2026
e76b0d6
Add _evaluate method to Constant class
Zeroto521 Jan 23, 2026
06b4df4
Update genexpr power tests for sqrt handling
Zeroto521 Jan 23, 2026
a76ef44
Update test_unary to use two variables instead of three
Zeroto521 Jan 23, 2026
5611d52
Refactor expression classes with ExprLike base
Zeroto521 Jan 23, 2026
3402b32
Merge branch 'master' into expr/unary
Zeroto521 Jan 23, 2026
925cb43
Add __array_ufunc__ to ExprLike type stub
Zeroto521 Jan 23, 2026
857c969
Merge branch 'expr/unary' of https://github.com/Zeroto521/PySCIPOpt i…
Zeroto521 Jan 23, 2026
c94177c
Update numpy import and type annotations in stubs
Zeroto521 Jan 23, 2026
588cba4
Update __array_ufunc__ type hints to use np.ufunc and str
Zeroto521 Jan 23, 2026
cd642f9
Reorder and add math functions in scip.pyi
Zeroto521 Jan 23, 2026
f8e6132
Reorder Operator and PATCH declarations in scip.pyi
Zeroto521 Jan 23, 2026
940fd93
Remove @disjoint_base decorator from ExprLike
Zeroto521 Jan 23, 2026
900fdc3
Fix UNARY_MAPPER to use local math function references
Zeroto521 Jan 23, 2026
c85be94
Update typing for UNARY_MAPPER in scip.pyi
Zeroto521 Jan 23, 2026
4f6058a
Remove unused UNARY_MAPPER from type stub
Zeroto521 Jan 23, 2026
a1501e6
Add return type annotations to ExprLike methods
Zeroto521 Jan 24, 2026
259f4ce
Merge branch 'master' into expr/unary
Zeroto521 Jan 29, 2026
cfa8d5a
Merge branch 'master' into expr/unary
Zeroto521 Jan 30, 2026
0517639
Merge branch 'master' into expr/unary
Zeroto521 Feb 2, 2026
c82d570
Merge branch 'master' into expr/unary
Zeroto521 Feb 3, 2026
9c78ad6
Format: add blank line in GenExpr class
Zeroto521 Feb 3, 2026
55c1343
Refactor unary tests and add more unary ops
Zeroto521 Feb 3, 2026
b4d45b1
Add array assertions for unary ops
Zeroto521 Feb 3, 2026
3688217
Add test for invalid unary arcsin operation
Zeroto521 Feb 3, 2026
2579e87
Merge branch 'master' into expr/unary
Zeroto521 Feb 4, 2026
8c4b75f
Merge branch 'master' into expr/unary
Zeroto521 Feb 5, 2026
bc873b8
Update CHANGELOG: numpy unary function notes
Zeroto521 Feb 5, 2026
4621945
Merge branch 'master' into expr/unary
Zeroto521 Mar 14, 2026
ad7370a
pyscipopt.sqrt(2) will return a Constant class
Zeroto521 Mar 14, 2026
590859d
Expect NotImplementedError for genexpr ** sqrt(2)
Zeroto521 Mar 14, 2026
cce6c92
Dispatch numpy ufuncs for expr functions
Zeroto521 Mar 14, 2026
766759b
Update expr tests for MatrixExpr and repr
Zeroto521 Mar 14, 2026
69f470a
Fix ufunc dispatch for scalars
Zeroto521 Mar 14, 2026
d388a77
Rename _dispatch_ufunc to _wrap_ufunc
Zeroto521 Mar 14, 2026
c4dfdb3
Use MatrixGenExpr for ufunc results
Zeroto521 Mar 14, 2026
243231a
Potential fix for pull request finding
Zeroto521 Mar 14, 2026
9333fdb
Update CHANGELOG.md
Zeroto521 Mar 14, 2026
92851c8
Reject 'out' kwarg in __array_ufunc__
Zeroto521 Mar 14, 2026
3c4c30f
Merge branch 'master' into expr/unary
Joao-Dionisio Mar 30, 2026
2043a0d
Test: forbid np.sin out= on Expr/Variable
Zeroto521 Mar 31, 2026
43746e2
Use MatrixGenExpr view for GenExpr arrays
Zeroto521 Mar 31, 2026
3c88ce7
Add docstring to _wrap_ufunc
Zeroto521 Mar 31, 2026
2e1ba2e
Use Constant.log().exp() for pow with float base
Zeroto521 Mar 31, 2026
0823af8
Merge branch 'master' into expr/unary
Joao-Dionisio Apr 3, 2026
013d734
Fix merge conflict artifacts in stubs and test imports
Joao-Dionisio Apr 3, 2026
73d5a6c
Update src/pyscipopt/scip.pyi
Joao-Dionisio Apr 3, 2026
5dc71ae
Clarify _wrap_ufunc docstring returns
Zeroto521 Apr 5, 2026
53d0946
Replace ufunc lambdas with documented functions
Zeroto521 Apr 5, 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

## Unreleased
### Added
- `Expr` and `GenExpr` support NumPy unary functions (`np.sin`, `np.cos`, `np.sqrt`, `np.exp`, `np.log`, `np.absolute`)
- Added `getBase()` and `setBase()` methods to `LP` class for getting/setting basis status
- Added `getMemUsed()`, `getMemTotal()`, and `getMemExternEstim()` methods
### Fixed
- Removed `Py_INCREF`/`Py_DECREF` on `Model` in `catchEvent`/`dropEvent` that caused memory leak for imbalanced usage
- Used `getIndex()` instead of `ptr()` for sorting nonlinear expression terms to avoid nondeterministic behavior
- Fixed stubtest failures with mypy 1.20 by marking dunder method parameters as positional-only
- Return `MatrixGenExpr` in `buildGenExprObj` instead of `MatrixExpr`
### Changed
- Speed up `constant * Expr` via C-level API
- Speed up `Term.__eq__` via the C-level API
Expand Down
280 changes: 215 additions & 65 deletions src/pyscipopt/expr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,16 @@
# 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, Literal

import numpy as np

from cpython.dict cimport PyDict_Next, PyDict_GetItem
from cpython.object cimport Py_TYPE
from cpython.ref cimport PyObject
from cpython.tuple cimport PyTuple_GET_ITEM
from pyscipopt.scip cimport Variable, Solution

import numpy as np


if TYPE_CHECKING:
double = float
Expand Down Expand Up @@ -194,6 +194,7 @@ cdef class Term:

CONST = Term()


# helper function
def buildGenExprObj(expr):
"""helper function to generate an object of type GenExpr"""
Expand Down Expand Up @@ -223,16 +224,66 @@ def buildGenExprObj(expr):
GenExprs = np.empty(expr.shape, dtype=object)
for idx in np.ndindex(expr.shape):
GenExprs[idx] = buildGenExprObj(expr[idx])
return GenExprs.view(MatrixExpr)
return GenExprs.view(MatrixGenExpr)

else:
assert isinstance(expr, GenExpr)
return expr


cdef class ExprLike:

def __array_ufunc__(
self,
ufunc: np.ufunc,
method: Literal["__call__", "reduce", "reduceat", "accumulate", "outer", "at"],
*args,
**kwargs,
):
if kwargs.get("out", None) is not None:
raise TypeError(
f"{self.__class__.__name__} doesn't support the 'out' parameter in __array_ufunc__"
)

if method == "__call__":
if ufunc is np.absolute:
return args[0].__abs__()
elif ufunc is np.exp:
return args[0].exp()
elif ufunc is np.log:
return args[0].log()
elif ufunc is np.sqrt:
return args[0].sqrt()
elif ufunc is np.sin:
return args[0].sin()
elif ufunc is np.cos:
return args[0].cos()

return NotImplemented

def __abs__(self) -> GenExpr:
return UnaryExpr(Operator.fabs, buildGenExprObj(self))

def exp(self) -> GenExpr:
return UnaryExpr(Operator.exp, buildGenExprObj(self))

def log(self) -> GenExpr:
return UnaryExpr(Operator.log, buildGenExprObj(self))

def sqrt(self) -> GenExpr:
return UnaryExpr(Operator.sqrt, buildGenExprObj(self))

def sin(self) -> GenExpr:
return UnaryExpr(Operator.sin, buildGenExprObj(self))

def cos(self) -> GenExpr:
return UnaryExpr(Operator.cos, buildGenExprObj(self))


##@details Polynomial expressions of variables with operator overloading. \n
#See also the @ref ExprDetails "description" in the expr.pxi.
cdef class Expr:

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

Expand All @@ -250,9 +301,6 @@ cdef class Expr:
def __iter__(self):
return iter(self.terms)

def __abs__(self):
return abs(buildGenExprObj(self))

def __add__(self, other):
left = self
right = other
Expand Down Expand Up @@ -356,10 +404,10 @@ cdef class Expr:
Note: base must be positive.
"""
if _is_number(other):
base = float(other)
base = <double>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))
return (self * Constant(base).log()).exp()
else:
raise TypeError(f"Unsupported base type {type(other)} for exponentiation.")

Expand Down Expand Up @@ -518,17 +566,14 @@ Operator = Op()
# so expr[x] will generate an error instead of returning the coefficient of x </pre>
#
#See also the @ref ExprDetails "description" in the expr.pxi.
cdef class GenExpr:
cdef class GenExpr(ExprLike):

cdef public _op
cdef public children

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

def __abs__(self):
return UnaryExpr(Operator.fabs, self)

def __add__(self, other):
if isinstance(other, np.ndarray):
return other + self
Expand Down Expand Up @@ -655,10 +700,10 @@ cdef class GenExpr:
Note: base must be positive.
"""
if _is_number(other):
base = float(other)
base = <double>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))
return (self * Constant(base).log()).exp()
else:
raise TypeError(f"Unsupported base type {type(other)} for exponentiation.")

Expand Down Expand Up @@ -832,55 +877,160 @@ cdef class Constant(GenExpr):
return self.number


def exp(expr):
"""returns expression with exp-function"""
if isinstance(expr, MatrixExpr):
unary_exprs = np.empty(shape=expr.shape, dtype=object)
for idx in np.ndindex(expr.shape):
unary_exprs[idx] = UnaryExpr(Operator.exp, buildGenExprObj(expr[idx]))
return unary_exprs.view(MatrixGenExpr)
else:
return UnaryExpr(Operator.exp, buildGenExprObj(expr))

def log(expr):
"""returns expression with log-function"""
if isinstance(expr, MatrixExpr):
unary_exprs = np.empty(shape=expr.shape, dtype=object)
for idx in np.ndindex(expr.shape):
unary_exprs[idx] = UnaryExpr(Operator.log, buildGenExprObj(expr[idx]))
return unary_exprs.view(MatrixGenExpr)
else:
return UnaryExpr(Operator.log, buildGenExprObj(expr))
def exp(x):
"""
returns expression with exp-function

Parameters
----------
x : Expr, GenExpr, number, np.ndarray, list, or tuple
- If x is a scalar expression or number, apply the exp function directly to it.
And if it's a number, convert it to a Constant expression first.
- If x is a vector (np.ndarray, list, or tuple), apply the exp function
element-wise using np.frompyfunc to convert each element to a Constant if it's
a number, and then apply the exp function.

Returns
-------
GenExpr or MatrixGenExpr
- If x is a scalar expression or number, returns the result of applying the exp
function to it.
- If x is a vector, returns an np.ndarray of the same shape with the exp
function applied element-wise.
"""
return _wrap_ufunc(x, np.exp)


def log(x):
"""
returns expression with log-function

Parameters
----------
x : Expr, GenExpr, number, np.ndarray, list, or tuple
- If x is a scalar expression or number, apply the log function directly to it.
And if it's a number, convert it to a Constant expression first.
- If x is a vector (np.ndarray, list, or tuple), apply the log function
element-wise using np.frompyfunc to convert each element to a Constant if it's
a number, and then apply the log function.

Returns
-------
GenExpr or MatrixGenExpr
- If x is a scalar expression or number, returns the result of applying the log
function to it.
- If x is a vector, returns an np.ndarray of the same shape with the log
function applied element-wise.
"""
return _wrap_ufunc(x, np.log)


def sqrt(x):
"""
returns expression with sqrt-function

Parameters
----------
x : Expr, GenExpr, number, np.ndarray, list, or tuple
- If x is a scalar expression or number, apply the sqrt function directly to it.
And if it's a number, convert it to a Constant expression first.
- If x is a vector (np.ndarray, list, or tuple), apply the sqrt function
element-wise using np.frompyfunc to convert each element to a Constant if it's
a number, and then apply the sqrt function.

Returns
-------
GenExpr or MatrixGenExpr
- If x is a scalar expression or number, returns the result of applying the sqrt
function to it.
- If x is a vector, returns an np.ndarray of the same shape with the sqrt
function applied element-wise.
"""
return _wrap_ufunc(x, np.sqrt)


def sin(x):
"""
returns expression with sin-function

Parameters
----------
x : Expr, GenExpr, number, np.ndarray, list, or tuple
- If x is a scalar expression or number, apply the sin function directly to it.
And if it's a number, convert it to a Constant expression first.
- If x is a vector (np.ndarray, list, or tuple), apply the sin function
element-wise using np.frompyfunc to convert each element to a Constant if it's
a number, and then apply the sin function.

Returns
-------
GenExpr or MatrixGenExpr
- If x is a scalar expression or number, returns the result of applying the sin
function to it.
- If x is a vector, returns an np.ndarray of the same shape with the sin
function applied element-wise.
"""
return _wrap_ufunc(x, np.sin)


def cos(x):
"""
returns expression with cos-function

Parameters
----------
x : Expr, GenExpr, number, np.ndarray, list, or tuple
- If x is a scalar expression or number, apply the cos function directly to it.
And if it's a number, convert it to a Constant expression first.
- If x is a vector (np.ndarray, list, or tuple), apply the cos function
element-wise using np.frompyfunc to convert each element to a Constant if it's
a number, and then apply the cos function.

Returns
-------
GenExpr or MatrixGenExpr
- If x is a scalar expression or number, returns the result of applying the cos
function to it.
- If x is a vector, returns an np.ndarray of the same shape with the cos
function applied element-wise.
"""
return _wrap_ufunc(x, np.cos)


cdef inline object _to_const(object x):
return Constant(<double>x) if _is_number(x) else x

cdef object _vec_to_const = np.frompyfunc(_to_const, 1, 1)

cdef inline object _wrap_ufunc(object x, object ufunc):
"""
Apply a universal function (ufunc) to an expression or a collection of expressions.

Parameters
----------
x : Expr, GenExpr, number, np.ndarray, list, or tuple
- If x is a scalar expression or number, apply the ufunc directly to it. And if
it's a number, convert it to a Constant expression first.
- If x is a vector (np.ndarray, list, or tuple), apply the ufunc element-wise
using np.frompyfunc to convert each element to a Constant if it's a number,
and then apply the ufunc.

ufunc : np.ufunc
The universal function to be applied to x.

Returns
-------
GenExpr or MatrixGenExpr
- If x is a scalar expression or number, returns the result of applying the
ufunc to it.
- If x is a vector, returns an np.ndarray of the same shape with the ufunc
applied element-wise.
"""
if isinstance(x, (np.ndarray, list, tuple)):
res = ufunc(_vec_to_const(x))
return res.view(MatrixGenExpr) if isinstance(res, np.ndarray) else res
return ufunc(_to_const(x))

def sqrt(expr):
"""returns expression with sqrt-function"""
if isinstance(expr, MatrixExpr):
unary_exprs = np.empty(shape=expr.shape, dtype=object)
for idx in np.ndindex(expr.shape):
unary_exprs[idx] = UnaryExpr(Operator.sqrt, buildGenExprObj(expr[idx]))
return unary_exprs.view(MatrixGenExpr)
else:
return UnaryExpr(Operator.sqrt, buildGenExprObj(expr))

def sin(expr):
"""returns expression with sin-function"""
if isinstance(expr, MatrixExpr):
unary_exprs = np.empty(shape=expr.shape, dtype=object)
for idx in np.ndindex(expr.shape):
unary_exprs[idx] = UnaryExpr(Operator.sin, buildGenExprObj(expr[idx]))
return unary_exprs.view(MatrixGenExpr)
else:
return UnaryExpr(Operator.sin, buildGenExprObj(expr))

def cos(expr):
"""returns expression with cos-function"""
if isinstance(expr, MatrixExpr):
unary_exprs = np.empty(shape=expr.shape, dtype=object)
for idx in np.ndindex(expr.shape):
unary_exprs[idx] = UnaryExpr(Operator.cos, buildGenExprObj(expr[idx]))
return unary_exprs.view(MatrixGenExpr)
else:
return UnaryExpr(Operator.cos, buildGenExprObj(expr))

def expr_to_nodes(expr):
'''transforms tree to an array of nodes. each node is an operator and the position of the
Expand Down
5 changes: 4 additions & 1 deletion src/pyscipopt/scip.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -2128,7 +2128,10 @@ cdef extern from "scip/scip_var.h":
cdef extern from "tpi/tpi.h":
int SCIPtpiGetNumThreads()

cdef class Expr:
cdef class ExprLike:
pass

cdef class Expr(ExprLike):
cdef public terms

cpdef double _evaluate(self, Solution sol)
Expand Down
Loading
Loading