BUG: Expr.__array_ufunc__ can't handle 0-dim array#1219
Conversation
Adjust ExprLike.__call__ to only treat ndarrays with ndim >= 1 as array operands for MatrixExpr/MatrixGenExpr conversion. Convert np.generic and 0-dim np.ndarray values to native Python scalars via .item() to avoid __array_ufunc__ recursion with MatrixExpr/Expr. Also update the array filtering and in-place comment to reflect the new behavior.
Add test_np_generic_cmp_with_expr to cover issue scipopt#1218. The test verifies that comparisons between numpy generic scalars (np.float64) and expression objects produce the expected ExprCons string representations for both operand orders and for positive/negative values (x <= -value, x <= value, -value <= x, value <= x).
Add an Unreleased changelog entry noting that Expr.__array_ufunc__ couldn't handle 0-dimensional arrays.
There was a problem hiding this comment.
Pull request overview
Fixes NumPy scalar/0-d array interactions with PySCIPOpt expressions by preventing ExprLike.__array_ufunc__ from treating 0-d arrays as matrix inputs, resolving the AttributeError reported in #1218 when comparing/negating np.float64 values against expressions.
Changes:
- Update
ExprLike.__array_ufunc__to only routendim >= 1arrays through matrix-handling logic, and to coerce 0-d arrays (andnp.generic) to native Python scalars via.item(). - Add a regression test covering comparisons between
np.float64(including negated) and expression variables in both operand orders.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
tests/test_expr.py |
Adds regression test for np.float64 comparison behavior with expressions (issue #1218). |
src/pyscipopt/expr.pxi |
Adjusts __array_ufunc__ handling to coerce 0-d arrays to scalars and avoid incorrect matrix-ufunc routing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Hey @Zeroto521 , thank you thank you thank you! Some comments from my Claude, please see if any make sense. 1. Gate the 0-d unwrap on The 1-d+ path returns args = [
a.item()
if isinstance(a, np.generic)
or (isinstance(a, np.ndarray) and a.ndim == 0 and a.dtype.kind in "fiub")
else a
for a in args
]
Only <= is exercised, but the same code path serves >=, ==, and all the arithmetic ufuncs. Worth adding a couple more one-liners — == in particular is interesting because it produces a two-sided ExprCons: assert str(value >= x) == "ExprCons(Expr({Term(x): 1.0}), None, 5.0)"
assert str(value == x) == "ExprCons(Expr({Term(x): 1.0}), 5.0, 5.0)"
assert str(np.int64(5) <= x) == "ExprCons(Expr({Term(x): 1.0}), 5.0, None)"
assert str(np.array(5) <= x) == "ExprCons(Expr({Term(x): 1.0}), 5.0, None)"
The comment mentions np.generic + MatrixExpr, but the bug being fixed is comparison ufuncs hitting .view(MatrixExprCons) on a plain ExprCons, not + recursion. Something like: # Numpy scalars and 0-d arrays must take the scalar dispatch path below,
# not the matrix path — the matrix path calls .view(MatrixExprCons) on
# the result, which fails for scalar comparisons returning ExprCons (#1218).Nitpick: stray space at tests/test_expr.py:339 — str(x <= value ). |
fix #1218
numpy automatically converts
np.genericto a 0-dim array.