From 0988f868b4e75c47374fc7a1555a73d94c78fc03 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 8 Jun 2026 21:50:11 +0800 Subject: [PATCH 1/8] refactor: replace Python math with C-level math functions and refactor unary expressions Remove Python math module and use libc.math for C-level functions (fabs, exp, log, sqrt, sin, cos). Refactor unary expression evaluation by introducing specific subclasses (AbsExpr, ExpExpr, LogExpr, SqrtExpr, SinExpr, CosExpr) with dedicated evaluate methods using C functions, replacing the generic UnaryExpr implementation. --- src/pyscipopt/expr.pxi | 73 +++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 62f0c880d..2ad9d1492 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -42,7 +42,6 @@ # 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 from typing import TYPE_CHECKING, Literal, Union import numpy as np @@ -54,6 +53,12 @@ from cpython.number cimport PyNumber_Check from cpython.object cimport Py_LE, Py_EQ, Py_GE, Py_TYPE from cpython.ref cimport PyObject from cpython.tuple cimport PyTuple_GET_ITEM +from libc.math cimport cos as c_cos +from libc.math cimport exp as c_exp +from libc.math cimport fabs as c_fabs +from libc.math cimport log as c_log +from libc.math cimport sqrt as c_sqrt +from libc.math cimport sin as c_sin cimport numpy as cnp from pyscipopt.scip cimport Variable, Solution @@ -279,22 +284,22 @@ cdef class ExprLike: return self * -1.0 def __abs__(self) -> GenExpr: - return UnaryExpr(Operator.fabs, buildGenExprObj(self)) + return AbsExpr(Operator.fabs, buildGenExprObj(self)) def exp(self) -> GenExpr: - return UnaryExpr(Operator.exp, buildGenExprObj(self)) + return ExpExpr(Operator.exp, buildGenExprObj(self)) def log(self) -> GenExpr: - return UnaryExpr(Operator.log, buildGenExprObj(self)) + return LogExpr(Operator.log, buildGenExprObj(self)) def sqrt(self) -> GenExpr: - return UnaryExpr(Operator.sqrt, buildGenExprObj(self)) + return SqrtExpr(Operator.sqrt, buildGenExprObj(self)) def sin(self) -> GenExpr: - return UnaryExpr(Operator.sin, buildGenExprObj(self)) + return SinExpr(Operator.sin, buildGenExprObj(self)) def cos(self) -> GenExpr: - return UnaryExpr(Operator.cos, buildGenExprObj(self)) + return CosExpr(Operator.cos, buildGenExprObj(self)) ##@details Polynomial expressions of variables with operator overloading. \n @@ -799,24 +804,62 @@ cdef class PowExpr(GenExpr): 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 __abs__(self) -> UnaryExpr: - if self._op == "abs": - return self.copy() - return UnaryExpr(Operator.fabs, self) + def __abs__(self) -> AbsExpr: + return AbsExpr(Operator.fabs, self) - def __repr__(self): + def __repr__(self) -> str: return self._op + "(" + self.children[0].__repr__() + ")" + +cdef class AbsExpr(UnaryExpr): + + def __abs__(self) -> AbsExpr: + return self.copy() + + cpdef double _evaluate(self, Solution sol) except *: + return c_fabs((self.children[0])._evaluate(sol)) + + + +cdef class ExpExpr(UnaryExpr): + + cpdef double _evaluate(self, Solution sol) except *: + return c_exp((self.children[0])._evaluate(sol)) + + + +cdef class LogExpr(UnaryExpr): + + cpdef double _evaluate(self, Solution sol) except *: + return c_log((self.children[0])._evaluate(sol)) + + + +cdef class SqrtExpr(UnaryExpr): + + cpdef double _evaluate(self, Solution sol) except *: + return c_sqrt((self.children[0])._evaluate(sol)) + + + +cdef class SinExpr(UnaryExpr): + + cpdef double _evaluate(self, Solution sol) except *: + return c_sin((self.children[0])._evaluate(sol)) + + + +cdef class CosExpr(UnaryExpr): + cpdef double _evaluate(self, Solution sol) except *: - cdef double res = (self.children[0])._evaluate(sol) - return math.fabs(res) if self._op == "abs" else getattr(math, self._op)(res) + return c_cos((self.children[0])._evaluate(sol)) # class for constant expressions From 3c7a98169ee481977a5ce212157b32f61c057f3c Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 8 Jun 2026 21:50:38 +0800 Subject: [PATCH 2/8] feat(type stubs): add specific unary expression types Update UnaryExpr type stubs to include concrete expression subclasses for more precise type annotations. This change refines the return type of __abs__ and introduces new expression classes. - Change __abs__ return type from GenExpr to AbsExpr - Add AbsExpr, ExpExpr, LogExpr, SqrtExpr, SinExpr, and CosExpr classes - All new classes inherit from UnaryExpr --- src/pyscipopt/scip.pyi | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 86196cfc1..11f43139a 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -2262,7 +2262,25 @@ class Term: class UnaryExpr(GenExpr): def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ... - def __abs__(self) -> GenExpr: ... + def __abs__(self) -> AbsExpr: ... + +class AbsExpr(UnaryExpr): + ... + +class ExpExpr(UnaryExpr): + ... + +class LogExpr(UnaryExpr): + ... + +class SqrtExpr(UnaryExpr): + ... + +class SinExpr(UnaryExpr): + ... + +class CosExpr(UnaryExpr): + ... @disjoint_base class VarExpr(GenExpr): From 166dc2117ed6e3957c93ade1e3a871c9cde882e6 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 8 Jun 2026 21:52:09 +0800 Subject: [PATCH 3/8] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80bea8d07..67e86568c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Changed - Move magic methods (`__radd__`, `__sub__`, `__rsub__`, `__rmul__`, `__richcmp__`, `__neg__`, and `__rtruediv__`) to `ExprLike` base class (#1204) - Speed up `Expr.__add__` and `Expr.__iadd__` via the C-level API +- Replace Python math with C-level math functions and refactor unary expressions. ### Removed ## 6.2.1 - 2026.05.16 From 647f8ac84a8715318dd74482c265cf446feac405 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 8 Jun 2026 22:13:23 +0800 Subject: [PATCH 4/8] style(scip): condense UnaryExpr subclass definitions to single lines --- src/pyscipopt/scip.pyi | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 11f43139a..8dda7cdd6 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -2264,23 +2264,12 @@ class UnaryExpr(GenExpr): def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ... def __abs__(self) -> AbsExpr: ... -class AbsExpr(UnaryExpr): - ... - -class ExpExpr(UnaryExpr): - ... - -class LogExpr(UnaryExpr): - ... - -class SqrtExpr(UnaryExpr): - ... - -class SinExpr(UnaryExpr): - ... - -class CosExpr(UnaryExpr): - ... +class AbsExpr(UnaryExpr): ... +class ExpExpr(UnaryExpr): ... +class LogExpr(UnaryExpr): ... +class SqrtExpr(UnaryExpr): ... +class SinExpr(UnaryExpr): ... +class CosExpr(UnaryExpr): ... @disjoint_base class VarExpr(GenExpr): From fadf4e1dc795c8571b428c180ca1ad17321558d8 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 9 Jun 2026 02:11:36 +0000 Subject: [PATCH 5/8] Remove extra blank line --- src/pyscipopt/expr.pxi | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2ad9d1492..5a2a5740c 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -827,7 +827,6 @@ cdef class AbsExpr(UnaryExpr): return c_fabs((self.children[0])._evaluate(sol)) - cdef class ExpExpr(UnaryExpr): cpdef double _evaluate(self, Solution sol) except *: From c27fe01233d96b48acd31c435f58906b630e6800 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 9 Jun 2026 20:45:53 +0800 Subject: [PATCH 6/8] Remove extra blank lines --- src/pyscipopt/expr.pxi | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 5a2a5740c..eb0317da8 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -833,28 +833,24 @@ cdef class ExpExpr(UnaryExpr): return c_exp((self.children[0])._evaluate(sol)) - cdef class LogExpr(UnaryExpr): cpdef double _evaluate(self, Solution sol) except *: return c_log((self.children[0])._evaluate(sol)) - cdef class SqrtExpr(UnaryExpr): cpdef double _evaluate(self, Solution sol) except *: return c_sqrt((self.children[0])._evaluate(sol)) - cdef class SinExpr(UnaryExpr): cpdef double _evaluate(self, Solution sol) except *: return c_sin((self.children[0])._evaluate(sol)) - cdef class CosExpr(UnaryExpr): cpdef double _evaluate(self, Solution sol) except *: From e5bdabcbe5d5e3b6da04b773d3381546a91dc1cc Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 11 Jun 2026 22:41:06 +0800 Subject: [PATCH 7/8] Simplify via `ExprLike.__abs__` --- src/pyscipopt/expr.pxi | 15 ++++++--------- src/pyscipopt/scip.pyi | 13 ++++++------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index eb0317da8..4e83099b6 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -283,22 +283,22 @@ cdef class ExprLike: def __neg__(self, /) -> Union[Expr, GenExpr]: return self * -1.0 - def __abs__(self) -> GenExpr: + def __abs__(self, /) -> AbsExpr: return AbsExpr(Operator.fabs, buildGenExprObj(self)) - def exp(self) -> GenExpr: + def exp(self, /) -> ExpExpr: return ExpExpr(Operator.exp, buildGenExprObj(self)) - def log(self) -> GenExpr: + def log(self, /) -> LogExpr: return LogExpr(Operator.log, buildGenExprObj(self)) - def sqrt(self) -> GenExpr: + def sqrt(self, /) -> SqrtExpr: return SqrtExpr(Operator.sqrt, buildGenExprObj(self)) - def sin(self) -> GenExpr: + def sin(self, /) -> SinExpr: return SinExpr(Operator.sin, buildGenExprObj(self)) - def cos(self) -> GenExpr: + def cos(self, /) -> CosExpr: return CosExpr(Operator.cos, buildGenExprObj(self)) @@ -811,9 +811,6 @@ cdef class UnaryExpr(GenExpr): self.children.append(expr) self._op = op - def __abs__(self) -> AbsExpr: - return AbsExpr(Operator.fabs, self) - def __repr__(self) -> str: return self._op + "(" + self.children[0].__repr__() + ")" diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 8dda7cdd6..16b41bc2a 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -338,12 +338,12 @@ class ExprLike: def __rmul__(self, other: object, /) -> Incomplete: ... def __rtruediv__(self, other: object, /) -> GenExpr: ... def __neg__(self, /) -> Union[Expr, GenExpr]: ... - def __abs__(self) -> GenExpr: ... - def exp(self) -> GenExpr: ... - def log(self) -> GenExpr: ... - def sqrt(self) -> GenExpr: ... - def sin(self) -> GenExpr: ... - def cos(self) -> GenExpr: ... + def __abs__(self, /) -> AbsExpr: ... + def exp(self, /) -> ExpExpr: ... + def log(self, /) -> LogExpr: ... + def sqrt(self, /) -> SqrtExpr: ... + def sin(self, /) -> SinExpr: ... + def cos(self, /) -> CosExpr: ... @disjoint_base class Expr(ExprLike): @@ -2262,7 +2262,6 @@ class Term: class UnaryExpr(GenExpr): def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ... - def __abs__(self) -> AbsExpr: ... class AbsExpr(UnaryExpr): ... class ExpExpr(UnaryExpr): ... From 8294d8a22cbbe241c9fde3bb14741987e1b018d9 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 11 Jun 2026 22:50:50 +0800 Subject: [PATCH 8/8] test C-level math functions --- tests/test_expr.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index f35096f73..5a3db9cdf 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -212,8 +212,16 @@ def test_getVal_with_GenExpr(): assert m.getVal(y / x) == 2 # test "**(prod(1.0,**(sum(0.0,prod(1.0,x)),-1)),2)" assert m.getVal((1 / x) ** 2) == 1 - # test "sin(sum(0.0,prod(1.0,x)))" + + # test C-level math functions + assert m.getVal(abs(x)) == 1 + assert m.getVal(abs(-x)) == 1 + assert m.getVal(abs(abs(-x))) == 1 + assert round(m.getVal(exp(x)), 6) == round(math.exp(1), 6) + assert round(m.getVal(log(x)), 6) == round(math.log(1), 6) + assert round(m.getVal(sqrt(x)), 6) == round(math.sqrt(1), 6) assert round(m.getVal(sin(x)), 6) == round(math.sin(1), 6) + assert round(m.getVal(cos(x)), 6) == round(math.cos(1), 6) with pytest.raises(TypeError): m.getVal(1)