From e80d30ebe19de9669f63b2029c42a7c221e92104 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 12 May 2026 23:05:06 +0800 Subject: [PATCH 1/7] Handle 0-dim numpy arrays in expr ufuncs 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. --- src/pyscipopt/expr.pxi | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index cdbed25cd..03a376b96 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -203,16 +203,24 @@ cdef class ExprLike: ) if method == "__call__": - if arrays := [a for a in args if isinstance(a, np.ndarray)]: + if arrays := [a for a in args if isinstance(a, np.ndarray) and a.ndim >= 1]: if any(a.dtype.kind not in "fiub" for a in arrays): return NotImplemented # If the np.ndarray is of numeric type, all arguments are converted to # MatrixExpr or MatrixGenExpr and then the ufunc is applied. return ufunc(*[_ensure_matrix(a) for a in args], **kwargs) - # Convert `np.generic` to native Python types to stop __array_ufunc__ - # recursion from `np.generic + MatrixExpr`. - args = [a.item() if isinstance(a, np.generic) else a for a in args] + # Convert `np.generic` and 0-dim `np.ndarray` to native Python types to stop + # __array_ufunc__ recursion from `np.generic + MatrixExpr/Expr`. + args = [ + a.item() + if ( + isinstance(a, np.generic) + or (isinstance(a, np.ndarray) and a.ndim == 0) + ) + else a + for a in args + ] if ufunc is np.add: return args[0] + args[1] From 4e3f0a9b47228e941fc0c3e09c89d20ba5fddb1a Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 12 May 2026 23:12:46 +0800 Subject: [PATCH 2/7] Add test for np.float64 comparisons with Expr Add test_np_generic_cmp_with_expr to cover issue #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). --- tests/test_expr.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_expr.py b/tests/test_expr.py index 8b8e0ae08..5013eb43b 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -328,6 +328,18 @@ def test_binary_ufunc(model): assert str(np.greater_equal(a, x)) == "[ExprCons(Expr({Term(x): 1.0}), None, 2.0)]" +def test_np_generic_cmp_with_expr(): + # test #1218 + m = Model() + x = m.addVar(name="x") + value = np.float64(5.0) + + assert str(x <= -value) == "ExprCons(Expr({Term(x): 1.0}), None, -5.0)" + assert str(x <= value ) == "ExprCons(Expr({Term(x): 1.0}), None, 5.0)" + assert str(-value <= x) == "ExprCons(Expr({Term(x): 1.0}), -5.0, None)" + assert str(value <= x) == "ExprCons(Expr({Term(x): 1.0}), 5.0, None)" + + def test_mul(): m = Model() x = m.addVar(name="x") From 833b4d4062606ffa848b743240eb96397d6f6c19 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 12 May 2026 23:21:25 +0800 Subject: [PATCH 3/7] Changelog: note Expr __array_ufunc__ 0-dim fix Add an Unreleased changelog entry noting that Expr.__array_ufunc__ couldn't handle 0-dimensional arrays. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 928aff515..3152a4c57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Added ### Fixed +- `Expr.__array_ufunc__` can't handle 0-dim array ### Changed ### Removed From 8d80bd7c4a3c821fb5c1f58b40cccaaccfa692d9 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 13 May 2026 21:41:23 +0800 Subject: [PATCH 4/7] Add more numpy comparison tests for Expr Expand tests in tests/test_expr.py to cover additional comparisons between numpy scalars/arrays and Expr objects. Added assertions for <=, >= and == involving np.float64 and np.int64, and a test for a 0-dim numpy array to ensure ExprCons string representations are correct. --- tests/test_expr.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index 5013eb43b..76d763d31 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -328,16 +328,28 @@ def test_binary_ufunc(model): assert str(np.greater_equal(a, x)) == "[ExprCons(Expr({Term(x): 1.0}), None, 2.0)]" -def test_np_generic_cmp_with_expr(): +def test_np_generic_vs_expr(): # test #1218 m = Model() x = m.addVar(name="x") value = np.float64(5.0) + # test <= assert str(x <= -value) == "ExprCons(Expr({Term(x): 1.0}), None, -5.0)" - assert str(x <= value ) == "ExprCons(Expr({Term(x): 1.0}), None, 5.0)" + assert str(x <= value) == "ExprCons(Expr({Term(x): 1.0}), None, 5.0)" assert str(-value <= x) == "ExprCons(Expr({Term(x): 1.0}), -5.0, None)" assert str(value <= x) == "ExprCons(Expr({Term(x): 1.0}), 5.0, None)" + assert str(np.int64(5) <= x) == "ExprCons(Expr({Term(x): 1.0}), 5.0, None)" + + # test >= + assert str(value >= x) == "ExprCons(Expr({Term(x): 1.0}), None, 5.0)" + assert str(-value >= x) == "ExprCons(Expr({Term(x): 1.0}), None, -5.0)" + + # test == + assert str(value == x) == "ExprCons(Expr({Term(x): 1.0}), 5.0, 5.0)" + + # test 0-ndim array + assert str(np.array(5) <= x) == "ExprCons(Expr({Term(x): 1.0}), 5.0, None)" def test_mul(): From 3c4b524bee622ec5877d2faac0e88801453c9136 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 13 May 2026 21:51:47 +0800 Subject: [PATCH 5/7] Clarify numpy comparison comments in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update comments in tests/test_expr.py to explicitly note that comparisons involve numpy scalar (np.generic) and 0-dim arrays versus Variable instances. No test logic was changed—only comment text to improve clarity. --- tests/test_expr.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index 76d763d31..64a2cebcf 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -334,21 +334,21 @@ def test_np_generic_vs_expr(): x = m.addVar(name="x") value = np.float64(5.0) - # test <= + # test <=, np.generic vs Variable assert str(x <= -value) == "ExprCons(Expr({Term(x): 1.0}), None, -5.0)" assert str(x <= value) == "ExprCons(Expr({Term(x): 1.0}), None, 5.0)" assert str(-value <= x) == "ExprCons(Expr({Term(x): 1.0}), -5.0, None)" assert str(value <= x) == "ExprCons(Expr({Term(x): 1.0}), 5.0, None)" assert str(np.int64(5) <= x) == "ExprCons(Expr({Term(x): 1.0}), 5.0, None)" - # test >= + # test >=, np.generic vs Variable assert str(value >= x) == "ExprCons(Expr({Term(x): 1.0}), None, 5.0)" assert str(-value >= x) == "ExprCons(Expr({Term(x): 1.0}), None, -5.0)" - # test == + # test ==, np.generic vs Variable assert str(value == x) == "ExprCons(Expr({Term(x): 1.0}), 5.0, 5.0)" - # test 0-ndim array + # test <=, 0-ndim int array vs Variable assert str(np.array(5) <= x) == "ExprCons(Expr({Term(x): 1.0}), 5.0, None)" From 7dfc027693dbcfada5fc91dc18d8afd315e303b6 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 13 May 2026 21:52:08 +0800 Subject: [PATCH 6/7] Test <= with 0-dim Variable array raises TypeError Add a unit test in tests/test_expr.py asserting that comparing 1 <= np.array(x) (a 0-dim Variable array) raises TypeError. This complements the existing 0-dim int array vs Variable test and helps catch regressions in comparison handling. --- tests/test_expr.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_expr.py b/tests/test_expr.py index 64a2cebcf..7154ba083 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -351,6 +351,10 @@ def test_np_generic_vs_expr(): # test <=, 0-ndim int array vs Variable assert str(np.array(5) <= x) == "ExprCons(Expr({Term(x): 1.0}), 5.0, None)" + # test <=, 0-ndim Variable array vs Variable + with pytest.raises(TypeError): + 1 <= np.array(x) + def test_mul(): m = Model() From e4c6749507c7620fc27385540365ed2862447d0c Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 13 May 2026 21:54:15 +0800 Subject: [PATCH 7/7] Clarify comment on numpy scalar/0-dim handling Update comment in src/pyscipopt/expr.pxi to explicitly mention that both np.generic and 0-dim np.ndarray are converted to native Python types to avoid __array_ufunc__ recursion with MatrixExpr/Expr. Documentation-only change; no functional edits. --- src/pyscipopt/expr.pxi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 03a376b96..c625e279c 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -211,7 +211,8 @@ cdef class ExprLike: return ufunc(*[_ensure_matrix(a) for a in args], **kwargs) # Convert `np.generic` and 0-dim `np.ndarray` to native Python types to stop - # __array_ufunc__ recursion from `np.generic + MatrixExpr/Expr`. + # __array_ufunc__ recursion from `np.generic + MatrixExpr/Expr` or + # `0-dim np.ndarray + MatrixExpr/Expr`. args = [ a.item() if (