diff --git a/CHANGELOG.md b/CHANGELOG.md index b875887ae..2a53c7fb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Replaced `format()` with f-strings in examples - 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 ### Removed ## 6.3.0 - 2026.10.11 diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index ec70f160d..ef65a34ec 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -58,6 +58,7 @@ 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 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 @@ -839,7 +840,11 @@ cdef class PowExpr(GenExpr): return f"**({self.children[0]},{self.expo})" 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") + return c_pow(base, expo) 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..a0fff1cfb 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -257,6 +257,23 @@ def test_getVal_with_GenExpr(): with pytest.raises(ValueError, match="math domain error"): m.getVal(sin(math.inf)) + +def test_getVal_pow_negative_exponent(): + m = Model() + x = m.addVar(lb=1, ub=1, name="x") + y = m.addVar(lb=2, ub=2, name="y") + z = m.addVar(lb=0, ub=0, name="z") + m.optimize() + + # C-level pow with a negative exponent (matches Python: 2.0**-2.0 == 0.25) + assert 2.0 ** -2.0 == 0.25 + assert m.getVal(x ** -2.0) == 1 + + # zero to a negative power raises, like Python's 0.0**-1.0; + # z / x would short-circuit to 0 in ProdExpr before evaluating x**-1, so divide by z + with pytest.raises(ZeroDivisionError): + m.getVal(y / z) + with pytest.raises(ValueError, match="math domain error"): m.getVal(cos(-math.inf))