diff --git a/CHANGELOG.md b/CHANGELOG.md index 2820f13b2..29b8597bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ - Refactor general expression conversion into per-class `to_scip_expr` recursion - Made expression fields read-only to prevent accidental runtime modification - Methods taking a list of variables (e.g. `addConsKnapsack()`) no longer convert it to a NumPy array to check its dimension +- Speed up `PowExpr._evaluate` via the C-level API +- `PowExpr._evaluate` now raises `ValueError("math domain error")` for a negative base with a non-integer exponent and `OverflowError` when the result overflows. ### Removed ## 6.3.0 - 2026.10.11 diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b07dc00fd..46414b2ca 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -57,7 +57,9 @@ 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 floor as c_floor from libc.math cimport log as c_log +from libc.math cimport pow as c_pow from libc.math cimport sqrt as c_sqrt from libc.math cimport sin as c_sin from libc.math cimport INFINITY @@ -903,7 +905,17 @@ cdef class PowExpr(GenExpr): return expr cpdef double _evaluate(self, Solution sol) except *: - return (self.children[0])._evaluate(sol) ** self.expo + cdef double base = (self.children[0])._evaluate(sol) + cdef double expo = self.expo + if base == 0.0 and expo < 0: + raise ZeroDivisionError("0.0 cannot be raised to a negative power") + elif base < 0.0 and expo != c_floor(expo): + raise ValueError("math domain error") + + cdef double res = c_pow(base, expo) + if c_fabs(res) == INFINITY and c_fabs(base) != INFINITY and c_fabs(expo) != INFINITY: + raise OverflowError("math range error") + return res cdef PowExpr copy(self, bint copy=True): cdef PowExpr res = PowExpr.__new__(PowExpr) diff --git a/tests/test_expr.py b/tests/test_expr.py index c5beb2ecb..7368dd3ca 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -256,7 +256,6 @@ def test_getVal_with_GenExpr(): # sin and cos reject infinite arguments, like math.sin(inf) with pytest.raises(ValueError, match="math domain error"): m.getVal(sin(math.inf)) - with pytest.raises(ValueError, match="math domain error"): m.getVal(cos(-math.inf)) @@ -265,6 +264,28 @@ def test_getVal_with_GenExpr(): m.getVal(exp(log(-x))) +def test_getVal_pow(): + m = Model() + y = m.addVar(lb=2, ub=2, name="y") + z = m.addVar(lb=0, ub=0, name="z") + n = m.addVar(lb=-2, ub=-2, name="n") + m.optimize() + + # C-level pow with a negative exponent (matches Python: 2.0**-2.0 == 0.25) + assert m.getVal(y ** -2.0) == 0.25 + + # non-integer exponents are undefined for negative bases, like math.pow + with pytest.raises(ValueError, match="math domain error"): + m.getVal(n ** 0.5) + + # zero to a negative power raises, like Python's 0.0**-1.0 + with pytest.raises(ZeroDivisionError): + m.getVal(z ** -1.0) + + with pytest.raises(OverflowError, match="math range error"): + m.getVal(exp(y) ** 10000.0) + + def test_unary_ufunc(model): m, x, y, z = model