Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Added pre-commit hook for automatic stub regeneration (see .pre-commit-config.yaml)
- Wrapped isObjIntegral() and test
- Added structured_optimization_trace recipe for structured optimization progress tracking
- Expr and GenExpr support numpy unary func (`np.sin`, `np.cos`, `np.sqrt`, `np.exp`, `np.log`, `np.absolute`)
### Fixed
- getBestSol() now returns None for infeasible problems instead of a Solution with NULL pointer
- all fundamental callbacks now raise an error if not implemented
Expand Down
118 changes: 56 additions & 62 deletions src/pyscipopt/expr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@
# 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 pyscipopt.scip cimport Variable, Solution
from cpython.dict cimport PyDict_Next
from cpython.ref cimport PyObject

import numpy as np
from pyscipopt.scip cimport Variable, Solution


if TYPE_CHECKING:
Expand Down Expand Up @@ -146,6 +146,7 @@ cdef class Term:

CONST = Term()


# helper function
def buildGenExprObj(expr):
"""helper function to generate an object of type GenExpr"""
Expand Down Expand Up @@ -181,10 +182,45 @@ def buildGenExprObj(expr):
assert isinstance(expr, GenExpr)
return expr


cdef class ExprLike:
Comment thread
Joao-Dionisio marked this conversation as resolved.

def __array_ufunc__(
self,
ufunc: np.ufunc,
method: Literal["__call__", "reduce", "reduceat", "accumulate", "outer", "at"],
*args,
**kwargs,
):
if method == "__call__":
if ufunc in UNARY_MAPPER:
return getattr(args[0], UNARY_MAPPER[ufunc])()

return NotImplemented
Comment thread
Zeroto521 marked this conversation as resolved.

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

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

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

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

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

def cos(self):
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 @@ -202,9 +238,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 @@ -463,17 +496,13 @@ 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 @@ -758,55 +787,20 @@ 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))
exp = np.exp
log = np.log
sqrt = np.sqrt
sin = np.sin
cos = np.cos
Comment thread
Zeroto521 marked this conversation as resolved.
Outdated
UNARY_MAPPER = {
np.absolute: "__abs__",
np.exp: "exp",
np.log: "log",
np.sqrt: "sqrt",
np.sin: "sin",
np.cos: "cos",
}

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 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))
Comment thread
Joao-Dionisio marked this conversation as resolved.

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 @@ -2108,7 +2108,10 @@ cdef extern from "scip/scip_var.h":
cdef extern from "tpi/tpi.h":
int SCIPtpiGetNumThreads()

cdef class Expr:
cdef class ExprLike:
Comment thread
Zeroto521 marked this conversation as resolved.
pass

cdef class Expr(ExprLike):
cdef public terms

cpdef double _evaluate(self, Solution sol)
Expand Down
50 changes: 32 additions & 18 deletions src/pyscipopt/scip.pyi
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from typing import ClassVar
from typing import ClassVar, Dict

import numpy
import numpy as np
from _typeshed import Incomplete
from typing_extensions import disjoint_base

CONST: Term
UNARY_MAPPER: Dict[np.ufunc, str]
EventNames: dict
MAJOR: int
MINOR: int
Operator: Op
PATCH: int
Operator: Op
PY_SCIP_CALL: Incomplete
StageNames: dict
TYPE_CHECKING: bool
Expand All @@ -20,18 +21,18 @@ _core_sum: Incomplete
_expr_richcmp: Incomplete
_is_number: Incomplete
buildGenExprObj: Incomplete
cos: Incomplete
exp: Incomplete
log: Incomplete
sin: Incomplete
cos: Incomplete
sqrt: Incomplete
Comment thread
Joao-Dionisio marked this conversation as resolved.
Outdated
expr_to_array: Incomplete
expr_to_nodes: Incomplete
is_memory_freed: Incomplete
log: Incomplete
print_memory_in_use: Incomplete
quickprod: Incomplete
quicksum: Incomplete
readStatistics: Incomplete
sin: Incomplete
sqrt: Incomplete
str_conversion: Incomplete
value_to_array: Incomplete

Expand Down Expand Up @@ -325,13 +326,27 @@ class Eventhdlr:
def eventinit(self) -> Incomplete: ...
def eventinitsol(self) -> Incomplete: ...

class ExprLike:
def __array_ufunc__(
self,
ufunc: np.ufunc,
method: str,
*args: Incomplete,
**kwargs: Incomplete,
) -> Incomplete: ...
def __abs__(self) -> Incomplete: ...
def exp(self) -> Incomplete: ...
def log(self) -> Incomplete: ...
def sqrt(self) -> Incomplete: ...
def sin(self) -> Incomplete: ...
def cos(self) -> Incomplete: ...

@disjoint_base
class Expr:
class Expr(ExprLike):
terms: Incomplete
def __init__(self, terms: Incomplete = ...) -> None: ...
def degree(self) -> Incomplete: ...
def normalize(self) -> Incomplete: ...
def __abs__(self) -> Incomplete: ...
def __add__(self, other: Incomplete) -> Incomplete: ...
def __eq__(self, other: object) -> bool: ...
def __ge__(self, other: object) -> bool: ...
Expand Down Expand Up @@ -371,13 +386,12 @@ class ExprCons:
def __ne__(self, other: object) -> bool: ...

@disjoint_base
class GenExpr:
class GenExpr(ExprLike):
_op: Incomplete
children: Incomplete
def __init__(self) -> None: ...
def degree(self) -> Incomplete: ...
def getOp(self) -> Incomplete: ...
def __abs__(self) -> Incomplete: ...
def __add__(self, other: Incomplete) -> Incomplete: ...
def __eq__(self, other: object) -> bool: ...
def __ge__(self, other: object) -> bool: ...
Expand Down Expand Up @@ -496,7 +510,7 @@ class LP:
def solve(self, dual: Incomplete = ...) -> Incomplete: ...
def writeLP(self, filename: Incomplete) -> Incomplete: ...

class MatrixConstraint(numpy.ndarray):
class MatrixConstraint(np.ndarray):
def getConshdlrName(self) -> Incomplete: ...
def isActive(self) -> Incomplete: ...
def isChecked(self) -> Incomplete: ...
Expand All @@ -512,21 +526,21 @@ class MatrixConstraint(numpy.ndarray):
def isSeparated(self) -> Incomplete: ...
def isStickingAtNode(self) -> Incomplete: ...

class MatrixExpr(numpy.ndarray):
class MatrixExpr(np.ndarray):
def _evaluate(self, sol: Incomplete) -> Incomplete: ...
def __array_ufunc__(
self,
ufunc: Incomplete,
method: Incomplete,
ufunc: np.ufunc,
method: str,
*args: Incomplete,
**kwargs: Incomplete,
) -> Incomplete: ...

class MatrixExprCons(numpy.ndarray):
class MatrixExprCons(np.ndarray):
def __array_ufunc__(
self,
ufunc: Incomplete,
method: Incomplete,
ufunc: np.ufunc,
method: str,
*args: Incomplete,
**kwargs: Incomplete,
) -> Incomplete: ...
Expand Down
34 changes: 27 additions & 7 deletions tests/test_expr.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import math

import numpy as np
import pytest

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


@pytest.fixture(scope="module")
Expand Down Expand Up @@ -118,10 +118,12 @@ def test_genexpr_op_genexpr(model):
assert isinstance(1/x + genexpr, GenExpr)
assert isinstance(1/x**1.5 - genexpr, GenExpr)
assert isinstance(y/x - exp(genexpr), GenExpr)
# sqrt(2) is not a constant expression and
Comment thread
Zeroto521 marked this conversation as resolved.
# we can only power to constant expressions!
with pytest.raises(NotImplementedError):
genexpr **= sqrt(2)

genexpr **= sqrt(2)
assert isinstance(genexpr, GenExpr)

with pytest.raises(TypeError):
genexpr **= sqrt("2")

def test_degree(model):
m, x, y, z = model
Expand Down Expand Up @@ -218,3 +220,21 @@ def test_getVal_with_GenExpr():

with pytest.raises(ZeroDivisionError):
m.getVal(1 / z)


def test_unary(model):
m, x, y, z = model

assert str(abs(x)) == "abs(sum(0.0,prod(1.0,x)))"
assert str(np.absolute(x)) == "abs(sum(0.0,prod(1.0,x)))"
assert str(sin([x, y])) == "[sin(sum(0.0,prod(1.0,x))) sin(sum(0.0,prod(1.0,y)))]"
assert (
str(np.sin([x, y])) == "[sin(sum(0.0,prod(1.0,x))) sin(sum(0.0,prod(1.0,y)))]"
)
assert (
str(sqrt([x, y])) == "[sqrt(sum(0.0,prod(1.0,x))) sqrt(sum(0.0,prod(1.0,y)))]"
)
assert (
str(np.sqrt([x, y]))
== "[sqrt(sum(0.0,prod(1.0,x))) sqrt(sum(0.0,prod(1.0,y)))]"
)
Loading