Skip to content

Commit ec0568f

Browse files
Zeroto521CopilotJoao-Dionisio
authored
Feature: Expr and GenExpr support NumPy binary functions like np.add (#1203)
* 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. * 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. * Rename test_unary to test_unary_ufunc 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. * 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. * 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. * Move np.negative test into unary ufunc tests 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. * Update CHANGELOG.md * Update CHANGELOG.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * 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. * Add tests for comparison ufuncs in expressions Extend tests/test_expr.py to cover np.less_equal, np.equal and np.greater_equal in test_binary_ufunc. Adds assertions verifying the string representation of ExprCons for both operand orders to ensure comparison ufuncs produce the expected expression forms. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com>
1 parent 89f092f commit ec0568f

3 files changed

Lines changed: 86 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
## Unreleased
44
### Added
5-
- `Expr` and `GenExpr` support NumPy unary functions (`np.sin`, `np.cos`, `np.sqrt`, `np.exp`, `np.log`, `np.absolute`)
5+
- `Expr` and `GenExpr` support NumPy unary functions (`np.sin`, `np.cos`, `np.sqrt`, `np.exp`, `np.log`, `np.absolute`, `np.negative`)
6+
- `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`)
67
- Added `getBase()` and `setBase()` methods to `LP` class for getting/setting basis status
78
- Added `getMemUsed()`, `getMemTotal()`, and `getMemExternEstim()` methods
89
- Added `isReoptEnabled()` and raising error if not enabled upon calling `reoptSolve()`

src/pyscipopt/expr.pxi

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,32 @@ cdef class ExprLike:
246246
)
247247

248248
if method == "__call__":
249-
if ufunc is np.absolute:
249+
if arrays := [a for a in args if type(a) is np.ndarray]:
250+
if any(a.dtype.kind not in "fiub" for a in arrays):
251+
return NotImplemented
252+
# If the np.ndarray is of numeric type, all arguments are converted to
253+
# MatrixExpr or MatrixGenExpr and then the ufunc is applied.
254+
return ufunc(*[_ensure_matrix(a) for a in args], **kwargs)
255+
256+
if ufunc is np.add:
257+
return args[0] + args[1]
258+
elif ufunc is np.subtract:
259+
return args[0] - args[1]
260+
elif ufunc is np.multiply:
261+
return args[0] * args[1]
262+
elif ufunc in {np.divide, np.true_divide}:
263+
return args[0] / args[1]
264+
elif ufunc is np.power:
265+
return args[0] ** args[1]
266+
elif ufunc is np.negative:
267+
return -args[0]
268+
elif ufunc is np.less_equal:
269+
return args[0] <= args[1]
270+
elif ufunc is np.greater_equal:
271+
return args[0] >= args[1]
272+
elif ufunc is np.equal:
273+
return args[0] == args[1]
274+
elif ufunc is np.absolute:
250275
return args[0].__abs__()
251276
elif ufunc is np.exp:
252277
return args[0].exp()
@@ -1031,6 +1056,12 @@ cdef inline object _wrap_ufunc(object x, object ufunc):
10311056
return res.view(MatrixGenExpr) if isinstance(res, np.ndarray) else res
10321057
return ufunc(_to_const(x))
10331058

1059+
cdef inline object _ensure_matrix(object arg):
1060+
if type(arg) is np.ndarray:
1061+
return arg.view(MatrixExpr)
1062+
matrix = MatrixExpr if isinstance(arg, Expr) else MatrixGenExpr
1063+
return np.array(arg, dtype=object).view(matrix)
1064+
10341065

10351066
def expr_to_nodes(expr):
10361067
'''transforms tree to an array of nodes. each node is an operator and the position of the

tests/test_expr.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def test_getVal_with_GenExpr():
222222
m.getVal(1 / z)
223223

224224

225-
def test_unary(model):
225+
def test_unary_ufunc(model):
226226
m, x, y, z = model
227227

228228
res = "abs(sum(0.0,prod(1.0,x)))"
@@ -276,6 +276,57 @@ def test_unary(model):
276276
# forbid modifying Variable/Expr/GenExpr in-place via out parameter
277277
np.sin(x, out=np.array([0]))
278278

279+
# test np.negative
280+
assert str(np.negative(x)) == "Expr({Term(x): -1.0})"
281+
282+
283+
def test_binary_ufunc(model):
284+
m, x, y, z = model
285+
286+
# test np.add
287+
assert str(np.add(x, 1)) == "Expr({Term(x): 1.0, Term(): 1.0})"
288+
assert str(np.add(1, x)) == "Expr({Term(x): 1.0, Term(): 1.0})"
289+
a = np.array([1])
290+
assert str(np.add(x, a)) == "[Expr({Term(x): 1.0, Term(): 1.0})]"
291+
assert str(np.add(a, x)) == "[Expr({Term(x): 1.0, Term(): 1.0})]"
292+
293+
# test np.subtract
294+
assert str(np.subtract(x, 1)) == "Expr({Term(x): 1.0, Term(): -1.0})"
295+
assert str(np.subtract(1, x)) == "Expr({Term(x): -1.0, Term(): 1.0})"
296+
assert str(np.subtract(x, a)) == "[Expr({Term(x): 1.0, Term(): -1.0})]"
297+
assert str(np.subtract(a, x)) == "[Expr({Term(x): -1.0, Term(): 1.0})]"
298+
299+
# test np.multiply
300+
a = np.array([2])
301+
assert str(np.multiply(x, 2)) == "Expr({Term(x): 2.0})"
302+
assert str(np.multiply(2, x)) == "Expr({Term(x): 2.0})"
303+
assert str(np.multiply(x, a)) == "[Expr({Term(x): 2.0})]"
304+
assert str(np.multiply(a, x)) == "[Expr({Term(x): 2.0})]"
305+
306+
# test np.divide
307+
assert str(np.divide(x, 2)) == "Expr({Term(x): 0.5})"
308+
assert str(np.divide(2, x)) == "prod(2.0,**(sum(0.0,prod(1.0,x)),-1))"
309+
assert str(np.divide(x, a)) == "[Expr({Term(x): 0.5})]"
310+
assert str(np.divide(a, x)) == "[prod(2.0,**(sum(0.0,prod(1.0,x)),-1))]"
311+
312+
# test np.power
313+
assert str(np.power(x, 2)) == "Expr({Term(x, x): 1.0})"
314+
assert str(np.power(2, x)) == "exp(prod(1.0,sum(0.0,prod(1.0,x)),log(2.0)))"
315+
assert str(np.power(x, a)) == "[Expr({Term(x, x): 1.0})]"
316+
assert str(np.power(a, x)) == "[exp(prod(1.0,sum(0.0,prod(1.0,x)),log(2.0)))]"
317+
318+
# test np.less_equal
319+
assert str(np.less_equal(x, a)) == "[ExprCons(Expr({Term(x): 1.0}), None, 2.0)]"
320+
assert str(np.less_equal(a, x)) == "[ExprCons(Expr({Term(x): 1.0}), 2.0, None)]"
321+
322+
# test np.equal
323+
assert str(np.equal(x, a)) == "[ExprCons(Expr({Term(x): 1.0}), 2.0, 2.0)]"
324+
assert str(np.equal(a, x)) == "[ExprCons(Expr({Term(x): 1.0}), 2.0, 2.0)]"
325+
326+
# test np.greater_equal
327+
assert str(np.greater_equal(x, a)) == "[ExprCons(Expr({Term(x): 1.0}), 2.0, None)]"
328+
assert str(np.greater_equal(a, x)) == "[ExprCons(Expr({Term(x): 1.0}), None, 2.0)]"
329+
279330

280331
def test_mul():
281332
m = Model()

0 commit comments

Comments
 (0)