Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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
Expand Down
14 changes: 13 additions & 1 deletion src/pyscipopt/expr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -903,7 +905,17 @@ cdef class PowExpr(GenExpr):
return expr

cpdef double _evaluate(self, Solution sol) except *:
return (<GenExpr>self.children[0])._evaluate(sol) ** self.expo
cdef double base = (<GenExpr>self.children[0])._evaluate(sol)
cdef double expo = <double>self.expo
if base == 0.0 and expo < 0:
Comment thread
Zeroto521 marked this conversation as resolved.
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)
Expand Down
23 changes: 22 additions & 1 deletion tests/test_expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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

Expand Down
Loading