From d5118a7e7c59908e0bacc5df659edf98b63a895c Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 6 Apr 2026 19:14:37 +0800 Subject: [PATCH 1/9] Support more ufuncs Handle numpy.ndarray arguments in ExprLike.__call__ by converting numeric ndarrays to MatrixGenExpr via a new _to_matrix helper and applying the ufunc. Non-numeric dtypes return NotImplemented. Also map common ufuncs (add, subtract, multiply, divide/true_divide, power, negative, comparisons, equal, absolute) to corresponding Expr/operator semantics to enable elementwise and operator-based behavior. --- src/pyscipopt/expr.pxi | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b26b87997..96ff59e20 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -246,7 +246,30 @@ cdef class ExprLike: ) if method == "__call__": - if ufunc is np.absolute: + if arrays := [a for a in args if type(a) is np.ndarray]: + if any(a.dtype.kind not in "fiub" for a in arrays): + return NotImplemented + return ufunc(*[_to_matrix(a) for a in args], **kwargs) + + if ufunc is np.add: + return args[0] + args[1] + elif ufunc is np.subtract: + return args[0] - args[1] + elif ufunc is np.multiply: + return args[0] * args[1] + elif ufunc in {np.divide, np.true_divide}: + return args[0] / args[1] + elif ufunc is np.power: + return args[0] ** args[1] + elif ufunc is np.negative: + return -args[0] + elif ufunc is np.less_equal: + return args[0] <= args[1] + elif ufunc is np.greater_equal: + return args[0] >= args[1] + elif ufunc is np.equal: + return args[0] == args[1] + elif ufunc is np.absolute: return args[0].__abs__() elif ufunc is np.exp: return args[0].exp() @@ -1031,6 +1054,11 @@ cdef inline object _wrap_ufunc(object x, object ufunc): return res.view(MatrixGenExpr) if isinstance(res, np.ndarray) else res return ufunc(_to_const(x)) +cdef inline object _to_matrix(arg): + if type(arg) is np.ndarray: + return arg.view(MatrixGenExpr) + return np.array(arg, dtype=object).view(MatrixGenExpr) + def expr_to_nodes(expr): '''transforms tree to an array of nodes. each node is an operator and the position of the From 3a557ab69a8fa61d58225f8fd44efdb8845c3ca9 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 6 Apr 2026 19:17:59 +0800 Subject: [PATCH 2/9] Use MatrixExpr for Expr/ndarray in _to_matrix Annotate _to_matrix arg as object and change its behavior to return MatrixExpr for numpy ndarray inputs and for values that are Expr instances; otherwise return MatrixGenExpr. This ensures Expr-backed arrays get a MatrixExpr view while keeping generic inputs as MatrixGenExpr. --- src/pyscipopt/expr.pxi | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 96ff59e20..ab01cbf5b 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1054,10 +1054,11 @@ cdef inline object _wrap_ufunc(object x, object ufunc): return res.view(MatrixGenExpr) if isinstance(res, np.ndarray) else res return ufunc(_to_const(x)) -cdef inline object _to_matrix(arg): +cdef inline object _to_matrix(object arg): if type(arg) is np.ndarray: - return arg.view(MatrixGenExpr) - return np.array(arg, dtype=object).view(MatrixGenExpr) + return arg.view(MatrixExpr) + matrix = MatrixExpr if isinstance(arg, Expr) else MatrixGenExpr + return np.array(arg, dtype=object).view(matrix) def expr_to_nodes(expr): From fcdc6043d846922ab9d4b298436a72245d627b42 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 6 Apr 2026 19:33:34 +0800 Subject: [PATCH 3/9] Rename test_unary to test_unary_ufunc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the test function in tests/test_expr.py from test_unary to test_unary_ufunc to make its purpose clearer — it specifically targets unary ufunc behavior. No functional changes to the test logic. --- tests/test_expr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index 8f5802d63..e23fa2d46 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -222,7 +222,7 @@ def test_getVal_with_GenExpr(): m.getVal(1 / z) -def test_unary(model): +def test_unary_ufunc(model): m, x, y, z = model res = "abs(sum(0.0,prod(1.0,x)))" From deee28a2246792fe2bc88632712f860781c94c24 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 6 Apr 2026 19:33:45 +0800 Subject: [PATCH 4/9] Add tests for binary numpy ufuncs Add test_binary_ufunc to tests/test_expr.py. The new tests exercise np.add, np.subtract, np.multiply, np.divide, np.negative and np.power against Expr objects and numpy arrays, including scalar/array operands and swapped operand orders, asserting expected string representations to catch regressions in binary ufunc handling. --- tests/test_expr.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/test_expr.py b/tests/test_expr.py index e23fa2d46..d9d4324fe 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -277,6 +277,45 @@ def test_unary_ufunc(model): np.sin(x, out=np.array([0])) +def test_binary_ufunc(model): + m, x, y, z = model + + # test np.add + assert str(np.add(x, 1)) == "Expr({Term(x): 1.0, Term(): 1.0})" + assert str(np.add(1, x)) == "Expr({Term(x): 1.0, Term(): 1.0})" + a = np.array([1]) + assert str(np.add(x, a)) == "[Expr({Term(x): 1.0, Term(): 1.0})]" + assert str(np.add(a, x)) == "[Expr({Term(x): 1.0, Term(): 1.0})]" + + # test np.subtract + assert str(np.subtract(x, 1)) == "Expr({Term(x): 1.0, Term(): -1.0})" + assert str(np.subtract(1, x)) == "Expr({Term(x): -1.0, Term(): 1.0})" + assert str(np.subtract(x, a)) == "[Expr({Term(x): 1.0, Term(): -1.0})]" + assert str(np.subtract(a, x)) == "[Expr({Term(x): -1.0, Term(): 1.0})]" + + # test np.multiply + a = np.array([2]) + assert str(np.multiply(x, 2)) == "Expr({Term(x): 2.0})" + assert str(np.multiply(2, x)) == "Expr({Term(x): 2.0})" + assert str(np.multiply(x, a)) == "[Expr({Term(x): 2.0})]" + assert str(np.multiply(a, x)) == "[Expr({Term(x): 2.0})]" + + # test np.divide + assert str(np.divide(x, 2)) == "Expr({Term(x): 0.5})" + assert str(np.divide(2, x)) == "prod(2.0,**(sum(0.0,prod(1.0,x)),-1))" + assert str(np.divide(x, a)) == "[Expr({Term(x): 0.5})]" + assert str(np.divide(a, x)) == "[prod(2.0,**(sum(0.0,prod(1.0,x)),-1))]" + + # test np.negative + assert str(np.negative(x)) == "Expr({Term(x): -1.0})" + + # test np.power + assert str(np.power(x, 2)) == "Expr({Term(x, x): 1.0})" + assert str(np.power(2, x)) == "exp(prod(1.0,sum(0.0,prod(1.0,x)),log(2.0)))" + assert str(np.power(x, a)) == "[Expr({Term(x, x): 1.0})]" + assert str(np.power(a, x)) == "[exp(prod(1.0,sum(0.0,prod(1.0,x)),log(2.0)))]" + + def test_mul(): m = Model() x = m.addVar(name="x") From 554f534064d6e919c04ed2d9c215e9da4c7348d0 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 6 Apr 2026 19:36:46 +0800 Subject: [PATCH 5/9] Document ndarray conversion before ufunc Add a clarifying comment in src/pyscipopt/expr.pxi explaining that when a numeric numpy ndarray is present among arguments, all arguments are converted to MatrixExpr or MatrixGenExpr before applying the ufunc. This improves code readability for future maintainers and documents the intended behavior in the ExprLike ufunc handling. --- src/pyscipopt/expr.pxi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index ab01cbf5b..7b50822ef 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -249,6 +249,8 @@ cdef class ExprLike: if arrays := [a for a in args if type(a) is np.ndarray]: 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(*[_to_matrix(a) for a in args], **kwargs) if ufunc is np.add: From 8a3ba126395fe750a6700ee8e91b492b4cb0fd9a Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 6 Apr 2026 19:38:38 +0800 Subject: [PATCH 6/9] Move np.negative test into unary ufunc tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relocate the np.negative assertion from the binary ufunc test block into the unary_ufunc test to group unary function checks together and eliminate the duplicate assertion. No functional changes—just test reorganization for clarity. --- tests/test_expr.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index d9d4324fe..4cc2759d6 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -276,6 +276,9 @@ def test_unary_ufunc(model): # forbid modifying Variable/Expr/GenExpr in-place via out parameter np.sin(x, out=np.array([0])) + # test np.negative + assert str(np.negative(x)) == "Expr({Term(x): -1.0})" + def test_binary_ufunc(model): m, x, y, z = model @@ -306,9 +309,6 @@ def test_binary_ufunc(model): assert str(np.divide(x, a)) == "[Expr({Term(x): 0.5})]" assert str(np.divide(a, x)) == "[prod(2.0,**(sum(0.0,prod(1.0,x)),-1))]" - # test np.negative - assert str(np.negative(x)) == "Expr({Term(x): -1.0})" - # test np.power assert str(np.power(x, 2)) == "Expr({Term(x, x): 1.0})" assert str(np.power(2, x)) == "exp(prod(1.0,sum(0.0,prod(1.0,x)),log(2.0)))" From cdf3d129b361a9f953ed12539814a68790908e6a Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 6 Apr 2026 19:38:52 +0800 Subject: [PATCH 7/9] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9e1944cf..774edfb63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ## Unreleased ### Added -- `Expr` and `GenExpr` support NumPy unary functions (`np.sin`, `np.cos`, `np.sqrt`, `np.exp`, `np.log`, `np.absolute`) +- `Expr` and `GenExpr` support NumPy unary functions (`np.sin`, `np.cos`, `np.sqrt`, `np.exp`, `np.log`, `np.absolute`, `np.negative`) +- `Expr` and `GenExpr` support NumPy binary functions (`np.add`, `np.subtract`, `np.multiply`, `np.divide`, `np.true_divide`, `np.power`) - Added `getBase()` and `setBase()` methods to `LP` class for getting/setting basis status - Added `getMemUsed()`, `getMemTotal()`, and `getMemExternEstim()` methods ### Fixed From 08c5088a8b8aad91f4073f23db81aefb95a16954 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 6 Apr 2026 19:52:47 +0800 Subject: [PATCH 8/9] Update CHANGELOG.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 774edfb63..88d06ba64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased ### Added - `Expr` and `GenExpr` support NumPy unary functions (`np.sin`, `np.cos`, `np.sqrt`, `np.exp`, `np.log`, `np.absolute`, `np.negative`) -- `Expr` and `GenExpr` support NumPy binary functions (`np.add`, `np.subtract`, `np.multiply`, `np.divide`, `np.true_divide`, `np.power`) +- `Expr` and `GenExpr` support NumPy binary functions (`np.add`, `np.subtract`, `np.multiply`, `np.divide`, `np.true_divide`, `np.power`, `np.less_equal`, `np.greater_equal`, `np.equal`) - Added `getBase()` and `setBase()` methods to `LP` class for getting/setting basis status - Added `getMemUsed()`, `getMemTotal()`, and `getMemExternEstim()` methods ### Fixed From ab6805bf272ae7b0a41bdb0c49bdc06276563123 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 6 Apr 2026 20:28:14 +0800 Subject: [PATCH 9/9] Rename _to_matrix to _ensure_matrix Rename helper function _to_matrix to _ensure_matrix and update its call site in ExprLike ufunc handling. The change preserves behavior (numpy arrays are viewed as MatrixExpr/MatrixGenExpr and other Expr instances map to MatrixExpr/MatrixGenExpr) while clarifying the function's intention. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 7b50822ef..570706fa6 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -251,7 +251,7 @@ cdef class ExprLike: 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(*[_to_matrix(a) for a in args], **kwargs) + return ufunc(*[_ensure_matrix(a) for a in args], **kwargs) if ufunc is np.add: return args[0] + args[1] @@ -1056,7 +1056,7 @@ cdef inline object _wrap_ufunc(object x, object ufunc): return res.view(MatrixGenExpr) if isinstance(res, np.ndarray) else res return ufunc(_to_const(x)) -cdef inline object _to_matrix(object arg): +cdef inline object _ensure_matrix(object arg): if type(arg) is np.ndarray: return arg.view(MatrixExpr) matrix = MatrixExpr if isinstance(arg, Expr) else MatrixGenExpr