Skip to content

Commit 881d356

Browse files
authored
implement dpnp.cumulative_sum and dpnp.cumulative_prod (#2171)
* implement dpnp.cumulative_sum and dpnp.cumulative_prod * add requires numpy-2.0.0 * address comments * update test_sycl_queue.py to include cumulative_sum and cumulative_prod only if NumPy > = 2.1.0
1 parent afa6980 commit 881d356

File tree

4 files changed

+267
-5
lines changed

4 files changed

+267
-5
lines changed

dpnp/dpnp_iface_mathematical.py

Lines changed: 188 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@
9494
"cross",
9595
"cumprod",
9696
"cumsum",
97+
"cumulative_prod",
98+
"cumulative_sum",
9799
"diff",
98100
"divide",
99101
"ediff1d",
@@ -1062,6 +1064,8 @@ def cumprod(a, axis=None, dtype=None, out=None):
10621064
10631065
See Also
10641066
--------
1067+
:obj:`dpnp.cumulative_prod` : Array API compatible alternative for
1068+
:obj:`dpnp.cumprod`.
10651069
:obj:`dpnp.prod` : Product array elements.
10661070
10671071
Examples
@@ -1143,6 +1147,8 @@ def cumsum(a, axis=None, dtype=None, out=None):
11431147
11441148
See Also
11451149
--------
1150+
:obj:`dpnp.cumulative_sum` : Array API compatible alternative for
1151+
:obj:`dpnp.cumsum`.
11461152
:obj:`dpnp.sum` : Sum array elements.
11471153
:obj:`dpnp.trapezoid` : Integration of array values using composite
11481154
trapezoidal rule.
@@ -1157,8 +1163,8 @@ def cumsum(a, axis=None, dtype=None, out=None):
11571163
[4, 5, 6]])
11581164
>>> np.cumsum(a)
11591165
array([ 1, 3, 6, 10, 15, 21])
1160-
>>> np.cumsum(a, dtype=float) # specifies type of output value(s)
1161-
array([ 1., 3., 6., 10., 15., 21.])
1166+
>>> np.cumsum(a, dtype=np.float32) # specifies type of output value(s)
1167+
array([ 1., 3., 6., 10., 15., 21.], dtype=np.float32)
11621168
11631169
>>> np.cumsum(a, axis=0) # sum over rows for each of the 3 columns
11641170
array([[1, 2, 3],
@@ -1169,8 +1175,8 @@ def cumsum(a, axis=None, dtype=None, out=None):
11691175
11701176
``cumsum(b)[-1]`` may not be equal to ``sum(b)``
11711177
1172-
>>> b = np.array([1, 2e-9, 3e-9] * 10000)
1173-
>>> b.cumsum().dtype == b.sum().dtype == np.float64
1178+
>>> b = np.array([1, 2e-7, 3e-7] * 100000, dtype=np.float32)
1179+
>>> b.cumsum().dtype == b.sum().dtype == np.float32
11741180
True
11751181
>>> b.cumsum()[-1] == b.sum()
11761182
array(False)
@@ -1194,6 +1200,184 @@ def cumsum(a, axis=None, dtype=None, out=None):
11941200
)
11951201

11961202

1203+
def cumulative_prod(
1204+
x, /, *, axis=None, dtype=None, out=None, include_initial=False
1205+
):
1206+
"""
1207+
Return the cumulative product of elements along a given axis.
1208+
1209+
This function is an Array API compatible alternative to :obj:`dpnp.cumprod`.
1210+
1211+
For full documentation refer to :obj:`numpy.cumulative_prod`.
1212+
1213+
Parameters
1214+
----------
1215+
x : {dpnp.ndarray, usm_ndarray}
1216+
Input array.
1217+
axis : {None, int}, optional
1218+
Axis along which the cumulative product is computed. The default value
1219+
is only allowed for one-dimensional arrays. For arrays with more than
1220+
one dimension `axis` is required.
1221+
Default: ``None``.
1222+
dtype : {None, dtype}, optional
1223+
Type of the returned array and of the accumulator in which the elements
1224+
are summed. If `dtype` is not specified, it defaults to the dtype of
1225+
`x`, unless `x` has an integer dtype with a precision less than that of
1226+
the default platform integer. In that case, the default platform
1227+
integer is used.
1228+
Default: ``None``.
1229+
out : {None, dpnp.ndarray, usm_ndarray}, optional
1230+
Alternative output array in which to place the result. It must have the
1231+
same shape and buffer length as the expected output but the type will
1232+
be cast if necessary.
1233+
Default: ``None``.
1234+
include_initial : bool, optional
1235+
Boolean indicating whether to include the initial value (ones) as
1236+
the first value in the output. With ``include_initial=True``
1237+
the shape of the output is different than the shape of the input.
1238+
Default: ``False``.
1239+
1240+
Returns
1241+
-------
1242+
out : dpnp.ndarray
1243+
A new array holding the result is returned unless `out` is specified,
1244+
in which case a reference to `out` is returned. The
1245+
result has the same shape as `x` if ``include_initial=False``.
1246+
1247+
See Also
1248+
--------
1249+
:obj:`dpnp.prod` : Product array elements.
1250+
1251+
Examples
1252+
--------
1253+
>>> import dpnp as np
1254+
>>> a = np.array([1, 2, 3])
1255+
>>> np.cumulative_prod(a) # intermediate results 1, 1*2
1256+
... # total product 1*2*3 = 6
1257+
array([1, 2, 6])
1258+
>>> a = np.array([1, 2, 3, 4, 5, 6])
1259+
>>> np.cumulative_prod(a, dtype=np.float32) # specify type of output
1260+
array([ 1., 2., 6., 24., 120., 720.], dtype=float32)
1261+
1262+
The cumulative product for each column (i.e., over the rows) of `b`:
1263+
1264+
>>> b = np.array([[1, 2, 3], [4, 5, 6]])
1265+
>>> np.cumulative_prod(b, axis=0)
1266+
array([[ 1, 2, 3],
1267+
[ 4, 10, 18]])
1268+
1269+
The cumulative product for each row (i.e. over the columns) of `b`:
1270+
1271+
>>> np.cumulative_prod(b, axis=1)
1272+
array([[ 1, 2, 6],
1273+
[ 4, 20, 120]])
1274+
1275+
"""
1276+
1277+
return dpnp_wrap_reduction_call(
1278+
x,
1279+
out,
1280+
dpt.cumulative_prod,
1281+
_get_reduction_res_dt,
1282+
dpnp.get_usm_ndarray(x),
1283+
axis=axis,
1284+
dtype=dtype,
1285+
include_initial=include_initial,
1286+
)
1287+
1288+
1289+
def cumulative_sum(
1290+
x, /, *, axis=None, dtype=None, out=None, include_initial=False
1291+
):
1292+
"""
1293+
Return the cumulative sum of the elements along a given axis.
1294+
1295+
This function is an Array API compatible alternative to :obj:`dpnp.cumsum`.
1296+
1297+
For full documentation refer to :obj:`numpy.cumulative_sum`.
1298+
1299+
Parameters
1300+
----------
1301+
x : {dpnp.ndarray, usm_ndarray}
1302+
Input array.
1303+
axis : {None, int}, optional
1304+
Axis along which the cumulative sum is computed. The default value
1305+
is only allowed for one-dimensional arrays. For arrays with more than
1306+
one dimension `axis` is required.
1307+
Default: ``None``.
1308+
dtype : {None, dtype}, optional
1309+
Type of the returned array and of the accumulator in which the elements
1310+
are summed. If `dtype` is not specified, it defaults to the dtype of
1311+
`x`, unless `x` has an integer dtype with a precision less than that of
1312+
the default platform integer. In that case, the default platform
1313+
integer is used.
1314+
Default: ``None``.
1315+
out : {None, dpnp.ndarray, usm_ndarray}, optional
1316+
Alternative output array in which to place the result. It must have the
1317+
same shape and buffer length as the expected output but the type will
1318+
be cast if necessary.
1319+
Default: ``None``.
1320+
include_initial : bool, optional
1321+
Boolean indicating whether to include the initial value (ones) as
1322+
the first value in the output. With ``include_initial=True``
1323+
the shape of the output is different than the shape of the input.
1324+
Default: ``False``.
1325+
1326+
Returns
1327+
-------
1328+
out : dpnp.ndarray
1329+
A new array holding the result is returned unless `out` is specified,
1330+
in which case a reference to `out` is returned. The
1331+
result has the same shape as `x` if ``include_initial=False``.
1332+
1333+
See Also
1334+
--------
1335+
:obj:`dpnp.sum` : Sum array elements.
1336+
:obj:`dpnp.trapezoid` : Integration of array values using composite
1337+
trapezoidal rule.
1338+
:obj:`dpnp.diff` : Calculate the n-th discrete difference along given axis.
1339+
1340+
Examples
1341+
--------
1342+
>>> import dpnp as np
1343+
>>> a = np.array([1, 2, 3, 4, 5, 6])
1344+
>>> a
1345+
array([1, 2, 3, 4, 5, 6])
1346+
>>> np.cumulative_sum(a)
1347+
array([ 1, 3, 6, 10, 15, 21])
1348+
>>> np.cumulative_sum(a, dtype=np.float32) # specifies output dtype
1349+
array([ 1., 3., 6., 10., 15., 21.], dtype=np.float32)
1350+
1351+
>>> b = np.array([[1, 2, 3], [4, 5, 6]])
1352+
>>> np.cumulative_sum(b, axis=0) # sum over rows for each of the 3 columns
1353+
array([[1, 2, 3],
1354+
[5, 7, 9]])
1355+
>>> np.cumulative_sum(b, axis=1) # sum over columns for each of the 2 rows
1356+
array([[ 1, 3, 6],
1357+
[ 4, 9, 15]])
1358+
1359+
``cumulative_sum(c)[-1]`` may not be equal to ``sum(c)``
1360+
1361+
>>> c = np.array([1, 2e-7, 3e-7] * 100000, dtype=np.float32)
1362+
>>> np.cumulative_sum(c).dtype == c.sum().dtype == np.float32
1363+
True
1364+
>>> np.cumulative_sum(c)[-1] == c.sum()
1365+
array(False)
1366+
1367+
"""
1368+
1369+
return dpnp_wrap_reduction_call(
1370+
x,
1371+
out,
1372+
dpt.cumulative_sum,
1373+
_get_reduction_res_dt,
1374+
dpnp.get_usm_ndarray(x),
1375+
axis=axis,
1376+
dtype=dtype,
1377+
include_initial=include_initial,
1378+
)
1379+
1380+
11971381
def diff(a, n=1, axis=-1, prepend=None, append=None):
11981382
"""
11991383
Calculate the n-th discrete difference along the given axis.

tests/test_mathematical.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,39 @@ def test_out_dtype(self, arr_dt, out_dt, dtype):
424424
assert_array_equal(expected, result)
425425
assert result is iout
426426

427+
@testing.with_requires("numpy>=2.1.0")
428+
def test_include_initial(self):
429+
a = numpy.arange(8).reshape(2, 2, 2)
430+
ia = dpnp.array(a)
431+
432+
expected = numpy.cumulative_prod(a, axis=1, include_initial=True)
433+
result = dpnp.cumulative_prod(ia, axis=1, include_initial=True)
434+
assert_array_equal(result, expected)
435+
436+
expected = numpy.cumulative_prod(a, axis=0, include_initial=True)
437+
result = dpnp.cumulative_prod(ia, axis=0, include_initial=True)
438+
assert_array_equal(result, expected)
439+
440+
a = numpy.arange(1, 5).reshape(2, 2)
441+
ia = dpnp.array(a)
442+
out = numpy.zeros((3, 2), dtype=numpy.float32)
443+
out_dp = dpnp.array(out)
444+
445+
expected = numpy.cumulative_prod(
446+
a, axis=0, out=out, include_initial=True
447+
)
448+
result = dpnp.cumulative_prod(
449+
ia, axis=0, out=out_dp, include_initial=True
450+
)
451+
assert result is out_dp
452+
assert_array_equal(result, expected)
453+
454+
a = numpy.array([2, 2])
455+
ia = dpnp.array(a)
456+
expected = numpy.cumulative_prod(a, include_initial=True)
457+
result = dpnp.cumulative_prod(ia, include_initial=True)
458+
assert_array_equal(result, expected)
459+
427460

428461
class TestCumSum:
429462
@pytest.mark.parametrize(
@@ -495,6 +528,39 @@ def test_out_dtype(self, arr_dt, out_dt, dtype):
495528
assert_array_equal(expected, result)
496529
assert result is iout
497530

531+
@testing.with_requires("numpy>=2.1.0")
532+
def test_include_initial(self):
533+
a = numpy.arange(8).reshape(2, 2, 2)
534+
ia = dpnp.array(a)
535+
536+
expected = numpy.cumulative_sum(a, axis=1, include_initial=True)
537+
result = dpnp.cumulative_sum(ia, axis=1, include_initial=True)
538+
assert_array_equal(result, expected)
539+
540+
expected = numpy.cumulative_sum(a, axis=0, include_initial=True)
541+
result = dpnp.cumulative_sum(ia, axis=0, include_initial=True)
542+
assert_array_equal(result, expected)
543+
544+
a = numpy.arange(1, 5).reshape(2, 2)
545+
ia = dpnp.array(a)
546+
out = numpy.zeros((3, 2), dtype=numpy.float32)
547+
out_dp = dpnp.array(out)
548+
549+
expected = numpy.cumulative_sum(
550+
a, axis=0, out=out, include_initial=True
551+
)
552+
result = dpnp.cumulative_sum(
553+
ia, axis=0, out=out_dp, include_initial=True
554+
)
555+
assert result is out_dp
556+
assert_array_equal(result, expected)
557+
558+
a = numpy.array([2, 2])
559+
ia = dpnp.array(a)
560+
expected = numpy.cumulative_sum(a, include_initial=True)
561+
result = dpnp.cumulative_sum(ia, include_initial=True)
562+
assert_array_equal(result, expected)
563+
498564

499565
class TestDiff:
500566
@pytest.mark.parametrize("n", list(range(0, 3)))
@@ -1927,7 +1993,7 @@ def test_zero(self, dt):
19271993
expected = numpy.sinc(a)
19281994
assert_dtype_allclose(result, expected)
19291995

1930-
# TODO: add a proper NumPY version once resolved
1996+
# TODO: add a proper NumPy version once resolved
19311997
@testing.with_requires("numpy>=2.0.0")
19321998
def test_zero_fp16(self):
19331999
a = numpy.array([0.0], dtype=numpy.float16)

tests/test_sycl_queue.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,8 @@ def test_meshgrid(device):
464464
pytest.param("count_nonzero", [3, 0, 2, -1.2]),
465465
pytest.param("cumprod", [[1, 2, 3], [4, 5, 6]]),
466466
pytest.param("cumsum", [[1, 2, 3], [4, 5, 6]]),
467+
pytest.param("cumulative_prod", [1, 2, 3, 4, 5, 6]),
468+
pytest.param("cumulative_sum", [1, 2, 3, 4, 5, 6]),
467469
pytest.param("degrees", [numpy.pi, numpy.pi / 2, 0]),
468470
pytest.param("diagonal", [[[1, 2], [3, 4]]]),
469471
pytest.param("diff", [1.0, 2.0, 4.0, 7.0, 0.0]),
@@ -556,6 +558,14 @@ def test_1in_1out(func, data, device):
556558
# `trapezoid` is available from NumPy 2.0
557559
func = "trapz"
558560

561+
if (
562+
func in ["cumulative_prod", "cumulative_sum"]
563+
and numpy.lib.NumpyVersion(numpy.__version__) < "2.1.0"
564+
):
565+
pytest.skip(
566+
"cumulative_prod and cumulative_sum are available from NumPy 2.1"
567+
)
568+
559569
x_orig = dpnp.asnumpy(x)
560570
expected = getattr(numpy, func)(x_orig)
561571
assert_dtype_allclose(result, expected)

tests/test_usm_type.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,8 @@ def test_norm(usm_type, ord, axis):
585585
pytest.param("cumlogsumexp", [1.0, 2.0, 4.0, 7.0]),
586586
pytest.param("cumprod", [[1, 2, 3], [4, 5, 6]]),
587587
pytest.param("cumsum", [[1, 2, 3], [4, 5, 6]]),
588+
pytest.param("cumulative_prod", [1, 2, 3, 4, 5, 6]),
589+
pytest.param("cumulative_sum", [1, 2, 3, 4, 5, 6]),
588590
pytest.param("degrees", [numpy.pi, numpy.pi / 2, 0]),
589591
pytest.param("diagonal", [[[1, 2], [3, 4]]]),
590592
pytest.param("diff", [1.0, 2.0, 4.0, 7.0, 0.0]),

0 commit comments

Comments
 (0)