From 1c52d671df935aa2f88243dcc54b141fe03958c7 Mon Sep 17 00:00:00 2001 From: Alexander Kalistratov Date: Fri, 14 Mar 2025 16:48:32 +0100 Subject: [PATCH 01/13] Rebase on master --- dpnp/dpnp_iface_mathematical.py | 19 ---- dpnp/dpnp_iface_statistics.py | 145 ++++++++++++++++++++++++++--- dpnp/tests/test_mathematical.py | 29 ------ dpnp/tests/test_statistics.py | 158 ++++++++++++++++++++++++++++++++ dpnp/tests/test_sycl_queue.py | 1 + dpnp/tests/test_usm_type.py | 1 + 6 files changed, 294 insertions(+), 59 deletions(-) diff --git a/dpnp/dpnp_iface_mathematical.py b/dpnp/dpnp_iface_mathematical.py index 73b881f8125f..aedc2be6f711 100644 --- a/dpnp/dpnp_iface_mathematical.py +++ b/dpnp/dpnp_iface_mathematical.py @@ -90,7 +90,6 @@ "clip", "conj", "conjugate", - "convolve", "copysign", "cross", "cumprod", @@ -791,24 +790,6 @@ def clip(a, /, min=None, max=None, *, out=None, order="K", **kwargs): conj = conjugate - -def convolve(a, v, mode="full"): - """ - Returns the discrete, linear convolution of two one-dimensional sequences. - - For full documentation refer to :obj:`numpy.convolve`. - - Examples - -------- - >>> ca = dpnp.convolve([1, 2, 3], [0, 1, 0.5]) - >>> print(ca) - [0. , 1. , 2.5, 4. , 1.5] - - """ - - return call_origin(numpy.convolve, a=a, v=v, mode=mode) - - _COPYSIGN_DOCSTRING = """ Composes a floating-point value with the magnitude of `x1_i` and the sign of `x2_i` for each element of input arrays `x1` and `x2`. diff --git a/dpnp/dpnp_iface_statistics.py b/dpnp/dpnp_iface_statistics.py index 78e4b03fd009..0a44eeddaf08 100644 --- a/dpnp/dpnp_iface_statistics.py +++ b/dpnp/dpnp_iface_statistics.py @@ -62,6 +62,7 @@ "amax", "amin", "average", + "convolve", "corrcoef", "correlate", "cov", @@ -357,6 +358,138 @@ def average(a, axis=None, weights=None, returned=False, *, keepdims=False): return avg +def _convolve_impl(a, v, mode, method, rdtype): + l_pad, r_pad = _get_padding(a.size, v.size, mode) + + if method == "auto": + method = _choose_conv_method(a, v, rdtype) + + if method == "direct": + r = _run_native_sliding_dot_product1d(a, v[::-1], l_pad, r_pad, rdtype) + elif method == "fft": + r = _convolve_fft(a, v, l_pad, r_pad, rdtype) + else: + raise ValueError( + f"Unknown method: {method}. Supported methods: auto, direct, fft" + ) + + return r + + +def convolve(a, v, mode="full", method="auto"): + r""" + Returns the discrete, linear convolution of two one-dimensional sequences. + The convolution operator is often seen in signal processing, where it + models the effect of a linear time-invariant system on a signal [1]_. In + probability theory, the sum of two independent random variables is + distributed according to the convolution of their individual + distributions. + If `v` is longer than `a`, the arrays are swapped before computation. + For full documentation refer to :obj:`numpy.convolve`. + Parameters + ---------- + a : {dpnp.ndarray, usm_ndarray} + First 1-D array. + v : {dpnp.ndarray, usm_ndarray} + Second 1-D array. The length of `v` must be less than or equal to + the length of `a`. + mode : {'full', 'valid', 'same'}, optional + 'full': + By default, mode is 'full'. This returns the convolution + at each point of overlap, with an output shape of (N+M-1,). At + the end-points of the convolution, the signals do not overlap + completely, and boundary effects may be seen. + 'same': + Mode 'same' returns output of length ``max(M, N)``. Boundary + effects are still visible. + 'valid': + Mode 'valid' returns output of length + ``max(M, N) - min(M, N) + 1``. The convolution product is only given + for points where the signals overlap completely. Values outside + the signal boundary have no effect. + method : {'auto', 'direct', 'fft'}, optional + 'direct': + The convolution is determined directly from sums. + 'fft': + The Fourier Transform is used to perform the calculations. + This method is faster for long sequences but can have accuracy issues. + 'auto': + Automatically chooses direct or Fourier method based on + an estimate of which is faster. + Note: Use of the FFT convolution on input containing NAN or INF + will lead to the entire output being NAN or INF. + Use method='direct' when your input contains NAN or INF values. + Default: ``'auto'``. + Returns + ------- + out : ndarray + Discrete, linear convolution of `a` and `v`. + See Also + -------- + :obj:`dpnp.correlate` : Cross-correlation of two 1-dimensional sequences. + Notes + ----- + The discrete convolution operation is defined as + .. math:: (a * v)_n = \\sum_{m = -\\infty}^{\\infty} a_m v_{n - m} + It can be shown that a convolution :math:`x(t) * y(t)` in time/space + is equivalent to the multiplication :math:`X(f) Y(f)` in the Fourier + domain, after appropriate padding (padding is necessary to prevent + circular convolution). Since multiplication is more efficient (faster) + than convolution, the function implements two approaches - direct and fft + which are regulated by the keyword `method`. + References + ---------- + .. [1] Wikipedia, "Convolution", + https://en.wikipedia.org/wiki/Convolution + Examples + -------- + Note how the convolution operator flips the second array + before "sliding" the two across one another: + >>> import dpnp as np + >>> a = np.array([1, 2, 3], dtype=np.float32) + >>> v = np.array([0, 1, 0.5], dtype=np.float32) + >>> np.convolve(a, v) + array([0. , 1. , 2.5, 4. , 1.5], dtype=float32) + Only return the middle values of the convolution. + Contains boundary effects, where zeros are taken + into account: + >>> np.convolve(a, v, 'same') + array([1. , 2.5, 4. ], dtype=float32) + The two arrays are of the same length, so there + is only one position where they completely overlap: + >>> np.convolve(a, v, 'valid') + array([2.5], dtype=float32) + """ + + dpnp.check_supported_arrays_type(a, v) + + if a.size == 0 or v.size == 0: + raise ValueError( + f"Array arguments cannot be empty. " + f"Received sizes: a.size={a.size}, v.size={v.size}" + ) + if a.ndim > 1 or v.ndim > 1: + raise ValueError( + f"Only 1-dimensional arrays are supported. " + f"Received shapes: a.shape={a.shape}, v.shape={v.shape}" + ) + + if a.ndim == 0: + a = dpnp.reshape(a, (1,)) + if v.ndim == 0: + v = dpnp.reshape(v, (1,)) + + device = a.sycl_device + rdtype = result_type_for_device([a.dtype, v.dtype], device) + + if v.size > a.size: + a, v = v, a + + r = _convolve_impl(a, v, mode, method, rdtype) + + return dpnp.asarray(r, dtype=rdtype, order="C") + + def corrcoef(x, y=None, rowvar=True, *, dtype=None): """ Return Pearson product-moment correlation coefficients. @@ -714,17 +847,7 @@ def correlate(a, v, mode="valid", method="auto"): revert = True a, v = v, a - l_pad, r_pad = _get_padding(a.size, v.size, mode) - - if method == "auto": - method = _choose_conv_method(a, v, rdtype) - - if method == "direct": - r = _run_native_sliding_dot_product1d(a, v, l_pad, r_pad, rdtype) - elif method == "fft": - r = _convolve_fft(a, v[::-1], l_pad, r_pad, rdtype) - else: # pragma: no cover - raise ValueError(f"Unknown method: {method}") + r = _convolve_impl(a, v[::-1], mode, method, rdtype) if revert: r = r[::-1] diff --git a/dpnp/tests/test_mathematical.py b/dpnp/tests/test_mathematical.py index d53be2fb4665..32298b13de94 100644 --- a/dpnp/tests/test_mathematical.py +++ b/dpnp/tests/test_mathematical.py @@ -110,35 +110,6 @@ def test_conj_out(self, dtype): assert_dtype_allclose(result, expected) -@pytest.mark.usefixtures("allow_fall_back_on_numpy") -class TestConvolve: - def test_object(self): - d = [1.0] * 100 - k = [1.0] * 3 - assert_array_almost_equal(dpnp.convolve(d, k)[2:-2], dpnp.full(98, 3)) - - def test_no_overwrite(self): - d = dpnp.ones(100) - k = dpnp.ones(3) - dpnp.convolve(d, k) - assert_array_equal(d, dpnp.ones(100)) - assert_array_equal(k, dpnp.ones(3)) - - def test_mode(self): - d = dpnp.ones(100) - k = dpnp.ones(3) - default_mode = dpnp.convolve(d, k, mode="full") - full_mode = dpnp.convolve(d, k, mode="full") - assert_array_equal(full_mode, default_mode) - # integer mode - with assert_raises(ValueError): - dpnp.convolve(d, k, mode=-1) - assert_array_equal(dpnp.convolve(d, k, mode=2), full_mode) - # illegal arguments - with assert_raises(TypeError): - dpnp.convolve(d, k, mode=None) - - class TestClip: @pytest.mark.parametrize( "dtype", get_all_dtypes(no_bool=True, no_none=True, no_complex=True) diff --git a/dpnp/tests/test_statistics.py b/dpnp/tests/test_statistics.py index e9d303ce6b90..bba04ec40823 100644 --- a/dpnp/tests/test_statistics.py +++ b/dpnp/tests/test_statistics.py @@ -127,6 +127,164 @@ def test_avg_error(self): dpnp.average(a, axis=0, weights=w) +class TestConvolve: + def setup_method(self): + numpy.random.seed(0) + + @pytest.mark.parametrize( + "a, v", [([1], [1, 2, 3]), ([1, 2, 3], [1]), ([1, 2, 3], [1, 2])] + ) + @pytest.mark.parametrize("mode", [None, "full", "valid", "same"]) + @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) + @pytest.mark.parametrize("method", [None, "auto", "direct", "fft"]) + def test_convolve(self, a, v, mode, dtype, method): + an = numpy.array(a, dtype=dtype) + vn = numpy.array(v, dtype=dtype) + ad = dpnp.array(an) + vd = dpnp.array(vn) + + dpnp_kwargs = {} + numpy_kwargs = {} + if mode is not None: + dpnp_kwargs["mode"] = mode + numpy_kwargs["mode"] = mode + if method is not None: + dpnp_kwargs["method"] = method + + expected = numpy.convolve(an, vn, **numpy_kwargs) + result = dpnp.convolve(ad, vd, **dpnp_kwargs) + + assert_dtype_allclose(result, expected) + + @pytest.mark.parametrize("a_size", [1, 100, 10000]) + @pytest.mark.parametrize("v_size", [1, 100, 10000]) + @pytest.mark.parametrize("mode", ["full", "valid", "same"]) + @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) + @pytest.mark.parametrize("method", ["auto", "direct", "fft"]) + def test_convolve_random(self, a_size, v_size, mode, dtype, method): + if dtype == dpnp.bool: + an = numpy.random.rand(a_size) > 0.9 + vn = numpy.random.rand(v_size) > 0.9 + else: + an = (100 * numpy.random.rand(a_size)).astype(dtype) + vn = (100 * numpy.random.rand(v_size)).astype(dtype) + + if dpnp.issubdtype(dtype, dpnp.complexfloating): + an = an + 1j * (100 * numpy.random.rand(a_size)).astype(dtype) + vn = vn + 1j * (100 * numpy.random.rand(v_size)).astype(dtype) + + ad = dpnp.array(an) + vd = dpnp.array(vn) + + dpnp_kwargs = {} + numpy_kwargs = {} + if mode is not None: + dpnp_kwargs["mode"] = mode + numpy_kwargs["mode"] = mode + if method is not None: + dpnp_kwargs["method"] = method + + result = dpnp.convolve(ad, vd, **dpnp_kwargs) + expected = numpy.convolve(an, vn, **numpy_kwargs) + + rdtype = result.dtype + if dpnp.issubdtype(rdtype, dpnp.integer): + rdtype = dpnp.default_float_type(ad.device) + + if method != "fft" and ( + dpnp.issubdtype(dtype, dpnp.integer) or dtype == dpnp.bool + ): + # For 'direct' and 'auto' methods, we expect exact results for integer types + assert_array_equal(result, expected) + else: + result = result.astype(rdtype) + if method == "direct": + expected = numpy.convolve(an, vn, **numpy_kwargs) + # For 'direct' method we can use standard validation + assert_dtype_allclose(result, expected, factor=30) + else: + rtol = 1e-3 + atol = 1e-10 + + if rdtype == dpnp.float64 or rdtype == dpnp.complex128: + rtol = 1e-6 + atol = 1e-12 + elif rdtype == dpnp.bool: + result = result.astype(dpnp.int32) + rdtype = result.dtype + + expected = expected.astype(rdtype) + + diff = numpy.abs(result.asnumpy() - expected) + invalid = diff > atol + rtol * numpy.abs(expected) + + # When using the 'fft' method, we might encounter outliers. + # This usually happens when the resulting array contains values close to zero. + # For these outliers, the relative error can be significant. + # We can tolerate a few such outliers. + max_outliers = 8 if expected.size > 1 else 0 + if invalid.sum() > max_outliers: + assert_dtype_allclose(result, expected, factor=1000) + + def test_convolve_mode_error(self): + a = dpnp.arange(5) + v = dpnp.arange(3) + + # invalid mode + with pytest.raises(ValueError): + dpnp.convolve(a, v, mode="unknown") + + @pytest.mark.parametrize("a, v", [([], [1]), ([1], []), ([], [])]) + def test_convolve_empty(self, a, v): + a = dpnp.asarray(a) + v = dpnp.asarray(v) + + with pytest.raises(ValueError): + dpnp.convolve(a, v) + + @pytest.mark.parametrize( + "a, v", + [ + ([[1, 2], [2, 3]], [1]), + ([1], [[1, 2], [2, 3]]), + ([[1, 2], [2, 3]], [[1, 2], [2, 3]]), + ], + ) + def test_convolve_shape_error(self, a, v): + a = dpnp.asarray(a) + v = dpnp.asarray(v) + + with pytest.raises(ValueError): + dpnp.convolve(a, v) + + @pytest.mark.parametrize("size", [2, 10**1, 10**2, 10**3, 10**4, 10**5]) + def test_convolve_different_sizes(self, size): + a = numpy.random.rand(size).astype(numpy.float32) + v = numpy.random.rand(size // 2).astype(numpy.float32) + + ad = dpnp.array(a) + vd = dpnp.array(v) + + expected = numpy.convolve(a, v) + result = dpnp.convolve(ad, vd, method="direct") + + assert_dtype_allclose(result, expected, factor=20) + + def test_convolve_another_sycl_queue(self): + a = dpnp.arange(5, sycl_queue=dpctl.SyclQueue()) + v = dpnp.arange(3, sycl_queue=dpctl.SyclQueue()) + + with pytest.raises(ValueError): + dpnp.convolve(a, v) + + def test_convolve_unkown_method(self): + a = dpnp.arange(5) + v = dpnp.arange(3) + + with pytest.raises(ValueError): + dpnp.convolve(a, v, method="unknown") + + class TestCorrcoef: @pytest.mark.usefixtures( "suppress_divide_invalid_numpy_warnings", diff --git a/dpnp/tests/test_sycl_queue.py b/dpnp/tests/test_sycl_queue.py index 133832fa2078..33b8c263baa5 100644 --- a/dpnp/tests/test_sycl_queue.py +++ b/dpnp/tests/test_sycl_queue.py @@ -365,6 +365,7 @@ def test_1in_1out(func, data, device): pytest.param("arctan2", [-1, +1, +1, -1], [-1, -1, +1, +1]), pytest.param("compress", [0, 1, 1, 0], [0, 1, 2, 3]), pytest.param("copysign", [0.0, 1.0, 2.0], [-1.0, 0.0, 1.0]), + pytest.param("convolve", [1, 2, 3], [4, 5, 6]), pytest.param( "corrcoef", [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]], diff --git a/dpnp/tests/test_usm_type.py b/dpnp/tests/test_usm_type.py index eabd07daf840..100d9ee5c9f0 100644 --- a/dpnp/tests/test_usm_type.py +++ b/dpnp/tests/test_usm_type.py @@ -651,6 +651,7 @@ def test_1in_1out(func, data, usm_type): pytest.param("copysign", [0.0, 1.0, 2.0], [-1.0, 0.0, 1.0]), pytest.param("cross", [1.0, 2.0, 3.0], [4.0, 5.0, 6.0]), pytest.param("digitize", [0.2, 6.4, 3.0], [0.0, 1.0, 2.5, 4.0]), + pytest.param("convolve", [1, 2, 3], [0, 1, 0.5]), pytest.param( "corrcoef", [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]], From ebaf2aab7e6341c179481fe6ed643685642a15d0 Mon Sep 17 00:00:00 2001 From: Alexander Kalistratov Date: Tue, 18 Mar 2025 16:04:35 +0100 Subject: [PATCH 02/13] Fix tolerance --- dpnp/tests/test_statistics.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dpnp/tests/test_statistics.py b/dpnp/tests/test_statistics.py index bba04ec40823..39e797d1dc9a 100644 --- a/dpnp/tests/test_statistics.py +++ b/dpnp/tests/test_statistics.py @@ -199,9 +199,14 @@ def test_convolve_random(self, a_size, v_size, mode, dtype, method): else: result = result.astype(rdtype) if method == "direct": - expected = numpy.convolve(an, vn, **numpy_kwargs) # For 'direct' method we can use standard validation - assert_dtype_allclose(result, expected, factor=30) + # acceptable error depends on the kernel size + # while error grows linearly with the kernel size, + # this empirically found formula provides a good balance + # the resulting factor is 40 for kernel size = 1, + # 400 for kernel size = 100 and 4000 for kernel size = 10000 + factor = int(40 * (min(a_size, v_size) ** 0.5)) + assert_dtype_allclose(result, expected, factor=factor) else: rtol = 1e-3 atol = 1e-10 From 726bc66384243f36e00ab460c7997a4e317435bb Mon Sep 17 00:00:00 2001 From: Alexander Kalistratov Date: Thu, 20 Mar 2025 03:17:44 +0100 Subject: [PATCH 03/13] Skip [u]int8/16 tests for convolve due to overflow --- dpnp/tests/test_statistics.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dpnp/tests/test_statistics.py b/dpnp/tests/test_statistics.py index 39e797d1dc9a..083b8f0f0f90 100644 --- a/dpnp/tests/test_statistics.py +++ b/dpnp/tests/test_statistics.py @@ -162,6 +162,8 @@ def test_convolve(self, a, v, mode, dtype, method): @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) @pytest.mark.parametrize("method", ["auto", "direct", "fft"]) def test_convolve_random(self, a_size, v_size, mode, dtype, method): + if dtype in [numpy.int8, numpy.uint8, numpy.int16, numpy.uint16]: + pytest.skip("avoid overflow.") if dtype == dpnp.bool: an = numpy.random.rand(a_size) > 0.9 vn = numpy.random.rand(v_size) > 0.9 @@ -385,7 +387,7 @@ def test_correlate(self, a, v, mode, dtype, method): @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) @pytest.mark.parametrize("method", ["auto", "direct", "fft"]) def test_correlate_random(self, a_size, v_size, mode, dtype, method): - if dtype in [numpy.int8, numpy.uint8]: + if dtype in [numpy.int8, numpy.uint8, numpy.int16, numpy.uint16]: pytest.skip("avoid overflow.") an = generate_random_numpy_array( a_size, dtype, low=-3, high=3, probability=0.9 From 163243d1ca3e409e67817147d158d60ea0d8d096 Mon Sep 17 00:00:00 2001 From: Alexander Kalistratov Date: Thu, 20 Mar 2025 13:32:56 +0100 Subject: [PATCH 04/13] Fix docs --- dpnp/dpnp_iface_statistics.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/dpnp/dpnp_iface_statistics.py b/dpnp/dpnp_iface_statistics.py index 628c966c5342..56e6a7ca91a7 100644 --- a/dpnp/dpnp_iface_statistics.py +++ b/dpnp/dpnp_iface_statistics.py @@ -384,8 +384,11 @@ def convolve(a, v, mode="full", method="auto"): probability theory, the sum of two independent random variables is distributed according to the convolution of their individual distributions. + If `v` is longer than `a`, the arrays are swapped before computation. + For full documentation refer to :obj:`numpy.convolve`. + Parameters ---------- a : {dpnp.ndarray, usm_ndarray} @@ -407,6 +410,8 @@ def convolve(a, v, mode="full", method="auto"): ``max(M, N) - min(M, N) + 1``. The convolution product is only given for points where the signals overlap completely. Values outside the signal boundary have no effect. + + Default: ``'full'``. method : {'auto', 'direct', 'fft'}, optional 'direct': The convolution is determined directly from sums. @@ -416,14 +421,18 @@ def convolve(a, v, mode="full", method="auto"): 'auto': Automatically chooses direct or Fourier method based on an estimate of which is faster. + Note: Use of the FFT convolution on input containing NAN or INF will lead to the entire output being NAN or INF. Use method='direct' when your input contains NAN or INF values. + Default: ``'auto'``. + Returns ------- out : ndarray Discrete, linear convolution of `a` and `v`. + See Also -------- :obj:`dpnp.correlate` : Cross-correlation of two 1-dimensional sequences. @@ -437,26 +446,33 @@ def convolve(a, v, mode="full", method="auto"): circular convolution). Since multiplication is more efficient (faster) than convolution, the function implements two approaches - direct and fft which are regulated by the keyword `method`. + References ---------- .. [1] Wikipedia, "Convolution", https://en.wikipedia.org/wiki/Convolution + Examples -------- Note how the convolution operator flips the second array before "sliding" the two across one another: + >>> import dpnp as np >>> a = np.array([1, 2, 3], dtype=np.float32) >>> v = np.array([0, 1, 0.5], dtype=np.float32) >>> np.convolve(a, v) + array([0. , 1. , 2.5, 4. , 1.5], dtype=float32) Only return the middle values of the convolution. Contains boundary effects, where zeros are taken into account: + >>> np.convolve(a, v, 'same') + array([1. , 2.5, 4. ], dtype=float32) The two arrays are of the same length, so there is only one position where they completely overlap: + >>> np.convolve(a, v, 'valid') array([2.5], dtype=float32) """ From 5e5f85214b6e3f0dce182e22e872a4f89050f4da Mon Sep 17 00:00:00 2001 From: Alexander Kalistratov Date: Thu, 20 Mar 2025 13:38:14 +0100 Subject: [PATCH 05/13] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8edbfe38d09..e1260e3e5b76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added implementation of `dpnp.hanning` [#2358](https://github.com/IntelPython/dpnp/pull/2358) * Added implementation of `dpnp.blackman` [#2363](https://github.com/IntelPython/dpnp/pull/2363) * Added implementation of `dpnp.bartlett` [#2366](https://github.com/IntelPython/dpnp/pull/2366) +* Added implementation of `dpnp.convolve` [#2205](https://github.com/IntelPython/dpnp/pull/2205) ### Changed From 2de077ff4d408c2c03b19545c77230489da842b9 Mon Sep 17 00:00:00 2001 From: Alexander Kalistratov Date: Thu, 20 Mar 2025 14:12:42 +0100 Subject: [PATCH 06/13] Fix 'mode' and 'method' options list for convolve in docs --- dpnp/dpnp_iface_statistics.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/dpnp/dpnp_iface_statistics.py b/dpnp/dpnp_iface_statistics.py index 56e6a7ca91a7..ba3f1706c8ca 100644 --- a/dpnp/dpnp_iface_statistics.py +++ b/dpnp/dpnp_iface_statistics.py @@ -397,29 +397,23 @@ def convolve(a, v, mode="full", method="auto"): Second 1-D array. The length of `v` must be less than or equal to the length of `a`. mode : {'full', 'valid', 'same'}, optional - 'full': - By default, mode is 'full'. This returns the convolution + - 'full': By default, mode is 'full'. This returns the convolution at each point of overlap, with an output shape of (N+M-1,). At the end-points of the convolution, the signals do not overlap completely, and boundary effects may be seen. - 'same': - Mode 'same' returns output of length ``max(M, N)``. Boundary + - 'same': Mode 'same' returns output of length ``max(M, N)``. Boundary effects are still visible. - 'valid': - Mode 'valid' returns output of length + - 'valid': Mode 'valid' returns output of length ``max(M, N) - min(M, N) + 1``. The convolution product is only given for points where the signals overlap completely. Values outside the signal boundary have no effect. Default: ``'full'``. method : {'auto', 'direct', 'fft'}, optional - 'direct': - The convolution is determined directly from sums. - 'fft': - The Fourier Transform is used to perform the calculations. + - 'direct': The convolution is determined directly from sums. + - 'fft': The Fourier Transform is used to perform the calculations. This method is faster for long sequences but can have accuracy issues. - 'auto': - Automatically chooses direct or Fourier method based on + - 'auto': Automatically chooses direct or Fourier method based on an estimate of which is faster. Note: Use of the FFT convolution on input containing NAN or INF From 37e7148ab535d030c7726a7a3ada4ab30507862d Mon Sep 17 00:00:00 2001 From: Alexander Kalistratov Date: Tue, 25 Mar 2025 16:42:02 +0100 Subject: [PATCH 07/13] Apply suggestions from code review Co-authored-by: Vahid Tavanashad <120411540+vtavana@users.noreply.github.com> --- dpnp/dpnp_iface_statistics.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dpnp/dpnp_iface_statistics.py b/dpnp/dpnp_iface_statistics.py index ba3f1706c8ca..ac3419bedd4b 100644 --- a/dpnp/dpnp_iface_statistics.py +++ b/dpnp/dpnp_iface_statistics.py @@ -397,7 +397,7 @@ def convolve(a, v, mode="full", method="auto"): Second 1-D array. The length of `v` must be less than or equal to the length of `a`. mode : {'full', 'valid', 'same'}, optional - - 'full': By default, mode is 'full'. This returns the convolution + - 'full': This returns the convolution at each point of overlap, with an output shape of (N+M-1,). At the end-points of the convolution, the signals do not overlap completely, and boundary effects may be seen. @@ -433,7 +433,9 @@ def convolve(a, v, mode="full", method="auto"): Notes ----- The discrete convolution operation is defined as - .. math:: (a * v)_n = \\sum_{m = -\\infty}^{\\infty} a_m v_{n - m} + + .. math:: (a * v)_n = \sum_{m = -\infty}^{\infty} a_m v_{n - m} + It can be shown that a convolution :math:`x(t) * y(t)` in time/space is equivalent to the multiplication :math:`X(f) Y(f)` in the Fourier domain, after appropriate padding (padding is necessary to prevent @@ -455,15 +457,15 @@ def convolve(a, v, mode="full", method="auto"): >>> a = np.array([1, 2, 3], dtype=np.float32) >>> v = np.array([0, 1, 0.5], dtype=np.float32) >>> np.convolve(a, v) - array([0. , 1. , 2.5, 4. , 1.5], dtype=float32) + Only return the middle values of the convolution. Contains boundary effects, where zeros are taken into account: >>> np.convolve(a, v, 'same') - array([1. , 2.5, 4. ], dtype=float32) + The two arrays are of the same length, so there is only one position where they completely overlap: From 21197ee7d6c1a3f11f4e29516832c1b887f384c9 Mon Sep 17 00:00:00 2001 From: Alexander Kalistratov Date: Tue, 25 Mar 2025 17:32:09 +0100 Subject: [PATCH 08/13] Tests for scalar, refactoring and comments --- dpnp/dpnp_iface_statistics.py | 8 ++-- dpnp/tests/test_statistics.py | 90 ++++++++++++++++++++++------------- 2 files changed, 60 insertions(+), 38 deletions(-) diff --git a/dpnp/dpnp_iface_statistics.py b/dpnp/dpnp_iface_statistics.py index ac3419bedd4b..cfb0727bb653 100644 --- a/dpnp/dpnp_iface_statistics.py +++ b/dpnp/dpnp_iface_statistics.py @@ -433,9 +433,9 @@ def convolve(a, v, mode="full", method="auto"): Notes ----- The discrete convolution operation is defined as - + .. math:: (a * v)_n = \sum_{m = -\infty}^{\infty} a_m v_{n - m} - + It can be shown that a convolution :math:`x(t) * y(t)` in time/space is equivalent to the multiplication :math:`X(f) Y(f)` in the Fourier domain, after appropriate padding (padding is necessary to prevent @@ -458,14 +458,14 @@ def convolve(a, v, mode="full", method="auto"): >>> v = np.array([0, 1, 0.5], dtype=np.float32) >>> np.convolve(a, v) array([0. , 1. , 2.5, 4. , 1.5], dtype=float32) - + Only return the middle values of the convolution. Contains boundary effects, where zeros are taken into account: >>> np.convolve(a, v, 'same') array([1. , 2.5, 4. ], dtype=float32) - + The two arrays are of the same length, so there is only one position where they completely overlap: diff --git a/dpnp/tests/test_statistics.py b/dpnp/tests/test_statistics.py index 083b8f0f0f90..bab791dca91f 100644 --- a/dpnp/tests/test_statistics.py +++ b/dpnp/tests/test_statistics.py @@ -128,6 +128,17 @@ def test_avg_error(self): class TestConvolve: + @staticmethod + def _get_kwargs(mode=None, method=None): + dpnp_kwargs = {} + numpy_kwargs = {} + if mode is not None: + dpnp_kwargs["mode"] = mode + numpy_kwargs["mode"] = mode + if method is not None: + dpnp_kwargs["method"] = method + return dpnp_kwargs, numpy_kwargs + def setup_method(self): numpy.random.seed(0) @@ -143,13 +154,7 @@ def test_convolve(self, a, v, mode, dtype, method): ad = dpnp.array(an) vd = dpnp.array(vn) - dpnp_kwargs = {} - numpy_kwargs = {} - if mode is not None: - dpnp_kwargs["mode"] = mode - numpy_kwargs["mode"] = mode - if method is not None: - dpnp_kwargs["method"] = method + dpnp_kwargs, numpy_kwargs = self._get_kwargs(mode, method) expected = numpy.convolve(an, vn, **numpy_kwargs) result = dpnp.convolve(ad, vd, **dpnp_kwargs) @@ -164,42 +169,27 @@ def test_convolve(self, a, v, mode, dtype, method): def test_convolve_random(self, a_size, v_size, mode, dtype, method): if dtype in [numpy.int8, numpy.uint8, numpy.int16, numpy.uint16]: pytest.skip("avoid overflow.") - if dtype == dpnp.bool: - an = numpy.random.rand(a_size) > 0.9 - vn = numpy.random.rand(v_size) > 0.9 - else: - an = (100 * numpy.random.rand(a_size)).astype(dtype) - vn = (100 * numpy.random.rand(v_size)).astype(dtype) - - if dpnp.issubdtype(dtype, dpnp.complexfloating): - an = an + 1j * (100 * numpy.random.rand(a_size)).astype(dtype) - vn = vn + 1j * (100 * numpy.random.rand(v_size)).astype(dtype) + an = generate_random_numpy_array( + a_size, dtype, low=-3, high=3, probability=0.9 + ) + vn = generate_random_numpy_array( + v_size, dtype, low=-3, high=3, probability=0.9 + ) ad = dpnp.array(an) vd = dpnp.array(vn) - dpnp_kwargs = {} - numpy_kwargs = {} - if mode is not None: - dpnp_kwargs["mode"] = mode - numpy_kwargs["mode"] = mode - if method is not None: - dpnp_kwargs["method"] = method + dpnp_kwargs, numpy_kwargs = self._get_kwargs(mode, method) result = dpnp.convolve(ad, vd, **dpnp_kwargs) expected = numpy.convolve(an, vn, **numpy_kwargs) - rdtype = result.dtype - if dpnp.issubdtype(rdtype, dpnp.integer): - rdtype = dpnp.default_float_type(ad.device) - if method != "fft" and ( dpnp.issubdtype(dtype, dpnp.integer) or dtype == dpnp.bool ): # For 'direct' and 'auto' methods, we expect exact results for integer types assert_array_equal(result, expected) else: - result = result.astype(rdtype) if method == "direct": # For 'direct' method we can use standard validation # acceptable error depends on the kernel size @@ -210,6 +200,16 @@ def test_convolve_random(self, a_size, v_size, mode, dtype, method): factor = int(40 * (min(a_size, v_size) ** 0.5)) assert_dtype_allclose(result, expected, factor=factor) else: + rdtype = result.dtype + if dpnp.issubdtype(rdtype, dpnp.integer): + # 'fft' do its calculations in float + # and 'auto' could use fft + # also assert_dtype_allclose for integer types is + # always check for exact match + rdtype = dpnp.default_float_type(ad.device) + + result = result.astype(rdtype) + rtol = 1e-3 atol = 1e-10 @@ -231,6 +231,8 @@ def test_convolve_random(self, a_size, v_size, mode, dtype, method): # We can tolerate a few such outliers. max_outliers = 8 if expected.size > 1 else 0 if invalid.sum() > max_outliers: + # we already failed check, + # call assert_dtype_allclose just to report error nicely assert_dtype_allclose(result, expected, factor=1000) def test_convolve_mode_error(self): @@ -249,6 +251,19 @@ def test_convolve_empty(self, a, v): with pytest.raises(ValueError): dpnp.convolve(a, v) + @pytest.mark.parametrize("a, v", [([1], 2), (3, [4]), (5, 6)]) + def test_convolve_scalar(self, a, v): + an = numpy.asarray(a, dtype=numpy.float32) + vn = numpy.asarray(v, dtype=numpy.float32) + + ad = dpnp.asarray(a, dtype=numpy.float32) + vd = dpnp.asarray(v, dtype=numpy.float32) + + expected = numpy.convolve(an, vn) + result = dpnp.convolve(ad, vd) + + assert_dtype_allclose(result, expected) + @pytest.mark.parametrize( "a, v", [ @@ -404,17 +419,12 @@ def test_correlate_random(self, a_size, v_size, mode, dtype, method): result = dpnp.correlate(ad, vd, **dpnp_kwargs) expected = numpy.correlate(an, vn, **numpy_kwargs) - rdtype = result.dtype - if dpnp.issubdtype(rdtype, dpnp.integer): - rdtype = dpnp.default_float_type(ad.device) - if method != "fft" and ( dpnp.issubdtype(dtype, dpnp.integer) or dtype == dpnp.bool ): # For 'direct' and 'auto' methods, we expect exact results for integer types assert_array_equal(result, expected) else: - result = result.astype(rdtype) if method == "direct": expected = numpy.correlate(an, vn, **numpy_kwargs) # For 'direct' method we can use standard validation @@ -426,6 +436,16 @@ def test_correlate_random(self, a_size, v_size, mode, dtype, method): factor = int(40 * (min(a_size, v_size) ** 0.5)) assert_dtype_allclose(result, expected, factor=factor) else: + rdtype = result.dtype + if dpnp.issubdtype(rdtype, dpnp.integer): + # 'fft' do its calculations in float + # and 'auto' could use fft + # also assert_dtype_allclose for integer types is + # always check for exact match + rdtype = dpnp.default_float_type(ad.device) + + result = result.astype(rdtype) + rtol = 1e-3 atol = 1e-3 @@ -447,6 +467,8 @@ def test_correlate_random(self, a_size, v_size, mode, dtype, method): # We can tolerate a few such outliers. max_outliers = 10 if expected.size > 1 else 0 if invalid.sum() > max_outliers: + # we already failed check, + # call assert_dtype_allclose just to report error nicely assert_dtype_allclose(result, expected, factor=1000) def test_correlate_mode_error(self): From b6b336dea4b78df7355fb87c95c3db8160bea517 Mon Sep 17 00:00:00 2001 From: Alexander Kalistratov Date: Thu, 27 Mar 2025 04:37:05 +0100 Subject: [PATCH 09/13] Review comments & cupy tests --- dpnp/tests/helper.py | 13 ++++++ dpnp/tests/test_statistics.py | 45 ++++++++++--------- .../third_party/cupy/math_tests/test_misc.py | 13 +++--- 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/dpnp/tests/helper.py b/dpnp/tests/helper.py index eb42d76b4d87..c5ec53bd4123 100644 --- a/dpnp/tests/helper.py +++ b/dpnp/tests/helper.py @@ -185,6 +185,19 @@ def generate_random_numpy_array( return a +def factor_to_tol(dtype, factor): + """ + Calculate the tolerance for comparing floating point and complex arrays. + The tolerance is based on the maximum resolution of the input dtype multiplied by the factor. + """ + + tol = 0 + if numpy.issubdtype(dtype, numpy.inexact): + tol = numpy.finfo(dtype).resolution + + return factor * tol + + def get_abs_array(data, dtype=None): if numpy.issubdtype(dtype, numpy.unsignedinteger): data = numpy.abs(data) diff --git a/dpnp/tests/test_statistics.py b/dpnp/tests/test_statistics.py index bab791dca91f..c5cbba2c6374 100644 --- a/dpnp/tests/test_statistics.py +++ b/dpnp/tests/test_statistics.py @@ -13,6 +13,7 @@ from .helper import ( assert_dtype_allclose, + factor_to_tol, generate_random_numpy_array, get_all_dtypes, get_complex_dtypes, @@ -210,30 +211,30 @@ def test_convolve_random(self, a_size, v_size, mode, dtype, method): result = result.astype(rdtype) - rtol = 1e-3 - atol = 1e-10 - - if rdtype == dpnp.float64 or rdtype == dpnp.complex128: - rtol = 1e-6 - atol = 1e-12 - elif rdtype == dpnp.bool: + if rdtype == dpnp.bool: result = result.astype(dpnp.int32) rdtype = result.dtype expected = expected.astype(rdtype) - diff = numpy.abs(result.asnumpy() - expected) - invalid = diff > atol + rtol * numpy.abs(expected) + factor = 1000 + rtol = atol = factor_to_tol(rdtype, factor) + invalid = numpy.logical_not( + numpy.isclose( + result.asnumpy(), expected, rtol=rtol, atol=atol + ) + ) # When using the 'fft' method, we might encounter outliers. # This usually happens when the resulting array contains values close to zero. # For these outliers, the relative error can be significant. # We can tolerate a few such outliers. - max_outliers = 8 if expected.size > 1 else 0 + # max_outliers = 10 if expected.size > 1 else 0 + max_outliers = 10 if invalid.sum() > max_outliers: # we already failed check, # call assert_dtype_allclose just to report error nicely - assert_dtype_allclose(result, expected, factor=1000) + assert_dtype_allclose(result, expected, factor=factor) def test_convolve_mode_error(self): a = dpnp.arange(5) @@ -446,30 +447,30 @@ def test_correlate_random(self, a_size, v_size, mode, dtype, method): result = result.astype(rdtype) - rtol = 1e-3 - atol = 1e-3 - - if rdtype == dpnp.float64 or rdtype == dpnp.complex128: - rtol = 1e-6 - atol = 1e-6 - elif rdtype == dpnp.bool: + if rdtype == dpnp.bool: result = result.astype(dpnp.int32) rdtype = result.dtype expected = expected.astype(rdtype) - diff = numpy.abs(result.asnumpy() - expected) - invalid = diff > atol + rtol * numpy.abs(expected) + factor = 1000 + rtol = atol = factor_to_tol(rdtype, factor) + invalid = numpy.logical_not( + numpy.isclose( + result.asnumpy(), expected, rtol=rtol, atol=atol + ) + ) # When using the 'fft' method, we might encounter outliers. # This usually happens when the resulting array contains values close to zero. # For these outliers, the relative error can be significant. # We can tolerate a few such outliers. - max_outliers = 10 if expected.size > 1 else 0 + # max_outliers = 10 if expected.size > 1 else 0 + max_outliers = 10 if invalid.sum() > max_outliers: # we already failed check, # call assert_dtype_allclose just to report error nicely - assert_dtype_allclose(result, expected, factor=1000) + assert_dtype_allclose(result, expected, factor=factor) def test_correlate_mode_error(self): a = dpnp.arange(5) diff --git a/dpnp/tests/third_party/cupy/math_tests/test_misc.py b/dpnp/tests/third_party/cupy/math_tests/test_misc.py index 3fd562798c33..c76faad1d8b9 100644 --- a/dpnp/tests/third_party/cupy/math_tests/test_misc.py +++ b/dpnp/tests/third_party/cupy/math_tests/test_misc.py @@ -531,7 +531,7 @@ def test_heaviside_nan_inf(self, xp, dtype_1, dtype_2): } ) ) -@pytest.mark.skip("convolve() is not implemented yet") +# @pytest.mark.skip("convolve() is not implemented yet") class TestConvolveShapeCombination: @testing.for_all_dtypes(no_float16=True) @@ -542,40 +542,39 @@ def test_convolve(self, xp, dtype): return xp.convolve(a, b, mode=self.mode) -@pytest.mark.skip("convolve() is not implemented yet") +# @pytest.mark.skip("convolve() is not implemented yet") @pytest.mark.parametrize("mode", ["valid", "same", "full"]) class TestConvolve: @testing.for_all_dtypes(no_float16=True) - @testing.numpy_cupy_allclose(rtol=1e-6) + @testing.numpy_cupy_allclose(rtol=1e-6, type_check=has_support_aspect64()) def test_convolve_non_contiguous(self, xp, dtype, mode): a = testing.shaped_arange((300,), xp, dtype) b = testing.shaped_arange((100,), xp, dtype) return xp.convolve(a[::200], b[10::70], mode=mode) @testing.for_all_dtypes(no_float16=True) - @testing.numpy_cupy_allclose(rtol=5e-4) + @testing.numpy_cupy_allclose(rtol=5e-4, type_check=has_support_aspect64()) def test_convolve_large_non_contiguous(self, xp, dtype, mode): a = testing.shaped_arange((10000,), xp, dtype) b = testing.shaped_arange((100,), xp, dtype) return xp.convolve(a[200::], b[10::70], mode=mode) @testing.for_all_dtypes_combination(names=["dtype1", "dtype2"]) - @testing.numpy_cupy_allclose(rtol=1e-2) + @testing.numpy_cupy_allclose(rtol=1e-2, type_check=has_support_aspect64()) def test_convolve_diff_types(self, xp, dtype1, dtype2, mode): a = testing.shaped_random((200,), xp, dtype1) b = testing.shaped_random((100,), xp, dtype2) return xp.convolve(a, b, mode=mode) -@pytest.mark.skip("convolve() is not implemented yet") @testing.parameterize(*testing.product({"mode": ["valid", "same", "full"]})) class TestConvolveInvalid: @testing.for_all_dtypes() def test_convolve_empty(self, dtype): for xp in (numpy, cupy): - a = xp.zeros((0,), dtype) + a = xp.zeros((0,), dtype=dtype) with pytest.raises(ValueError): xp.convolve(a, a, mode=self.mode) From d4608f5063aab9841e8aa12d0990e2cfc460f69f Mon Sep 17 00:00:00 2001 From: Alexander Kalistratov Date: Thu, 27 Mar 2025 04:45:20 +0100 Subject: [PATCH 10/13] Revert some changes --- dpnp/tests/test_statistics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dpnp/tests/test_statistics.py b/dpnp/tests/test_statistics.py index c5cbba2c6374..c812ed33c97d 100644 --- a/dpnp/tests/test_statistics.py +++ b/dpnp/tests/test_statistics.py @@ -230,7 +230,7 @@ def test_convolve_random(self, a_size, v_size, mode, dtype, method): # For these outliers, the relative error can be significant. # We can tolerate a few such outliers. # max_outliers = 10 if expected.size > 1 else 0 - max_outliers = 10 + max_outliers = 10 if expected.size > 1 else 0 if invalid.sum() > max_outliers: # we already failed check, # call assert_dtype_allclose just to report error nicely @@ -466,7 +466,7 @@ def test_correlate_random(self, a_size, v_size, mode, dtype, method): # For these outliers, the relative error can be significant. # We can tolerate a few such outliers. # max_outliers = 10 if expected.size > 1 else 0 - max_outliers = 10 + max_outliers = 10 if expected.size > 1 else 0 if invalid.sum() > max_outliers: # we already failed check, # call assert_dtype_allclose just to report error nicely From f0d5cfbcbfdde90e10d1e88c7ab46732f7c6a0b0 Mon Sep 17 00:00:00 2001 From: Alexander Kalistratov Date: Fri, 28 Mar 2025 17:16:44 +0100 Subject: [PATCH 11/13] Apply suggestions from code review Co-authored-by: Anton <100830759+antonwolfy@users.noreply.github.com> --- dpnp/dpnp_iface_statistics.py | 6 ++++-- dpnp/tests/test_statistics.py | 4 ++-- dpnp/tests/third_party/cupy/math_tests/test_misc.py | 2 -- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dpnp/dpnp_iface_statistics.py b/dpnp/dpnp_iface_statistics.py index 424c68d05d25..2b6420dde686 100644 --- a/dpnp/dpnp_iface_statistics.py +++ b/dpnp/dpnp_iface_statistics.py @@ -418,18 +418,19 @@ def convolve(a, v, mode="full", method="auto"): Note: Use of the FFT convolution on input containing NAN or INF will lead to the entire output being NAN or INF. - Use method='direct' when your input contains NAN or INF values. + Use ``method='direct'`` when your input contains NAN or INF values. Default: ``'auto'``. Returns ------- - out : ndarray + out : dpnp.ndarray Discrete, linear convolution of `a` and `v`. See Also -------- :obj:`dpnp.correlate` : Cross-correlation of two 1-dimensional sequences. + Notes ----- The discrete convolution operation is defined as @@ -471,6 +472,7 @@ def convolve(a, v, mode="full", method="auto"): >>> np.convolve(a, v, 'valid') array([2.5], dtype=float32) + """ dpnp.check_supported_arrays_type(a, v) diff --git a/dpnp/tests/test_statistics.py b/dpnp/tests/test_statistics.py index c812ed33c97d..7df647755742 100644 --- a/dpnp/tests/test_statistics.py +++ b/dpnp/tests/test_statistics.py @@ -282,8 +282,8 @@ def test_convolve_shape_error(self, a, v): @pytest.mark.parametrize("size", [2, 10**1, 10**2, 10**3, 10**4, 10**5]) def test_convolve_different_sizes(self, size): - a = numpy.random.rand(size).astype(numpy.float32) - v = numpy.random.rand(size // 2).astype(numpy.float32) + a = generate_random_numpy_array(size, dtype=numpy.float32, low=0, high=1) + v = generate_random_numpy_array(size // 2, dtype=numpy.float32, low=0, high=1) ad = dpnp.array(a) vd = dpnp.array(v) diff --git a/dpnp/tests/third_party/cupy/math_tests/test_misc.py b/dpnp/tests/third_party/cupy/math_tests/test_misc.py index c76faad1d8b9..cd73261e7a9b 100644 --- a/dpnp/tests/third_party/cupy/math_tests/test_misc.py +++ b/dpnp/tests/third_party/cupy/math_tests/test_misc.py @@ -531,7 +531,6 @@ def test_heaviside_nan_inf(self, xp, dtype_1, dtype_2): } ) ) -# @pytest.mark.skip("convolve() is not implemented yet") class TestConvolveShapeCombination: @testing.for_all_dtypes(no_float16=True) @@ -542,7 +541,6 @@ def test_convolve(self, xp, dtype): return xp.convolve(a, b, mode=self.mode) -# @pytest.mark.skip("convolve() is not implemented yet") @pytest.mark.parametrize("mode", ["valid", "same", "full"]) class TestConvolve: From 58e859ace11c17206c517b1dbe299e8ba93003d5 Mon Sep 17 00:00:00 2001 From: Alexander Kalistratov Date: Fri, 28 Mar 2025 17:37:39 +0100 Subject: [PATCH 12/13] Review comments --- dpnp/dpnp_iface_manipulation.py | 3 ++ dpnp/dpnp_iface_statistics.py | 12 ++------ dpnp/tests/test_statistics.py | 30 ++++++++++--------- .../third_party/cupy/math_tests/test_misc.py | 4 +-- 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index f156ca6b7969..c97fcc236218 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -958,6 +958,9 @@ def atleast_1d(*arys): dpnp.check_supported_arrays_type(*arys) for ary in arys: if ary.ndim == 0: + # Scalars cannot be empty + # Scalars always have a size of 1, so + # reshape(1) is guaranteed to succeed result = ary.reshape(1) else: result = ary diff --git a/dpnp/dpnp_iface_statistics.py b/dpnp/dpnp_iface_statistics.py index 2b6420dde686..2a7fd228df97 100644 --- a/dpnp/dpnp_iface_statistics.py +++ b/dpnp/dpnp_iface_statistics.py @@ -392,10 +392,9 @@ def convolve(a, v, mode="full", method="auto"): Parameters ---------- a : {dpnp.ndarray, usm_ndarray} - First 1-D array. + First input array. v : {dpnp.ndarray, usm_ndarray} - Second 1-D array. The length of `v` must be less than or equal to - the length of `a`. + Second input array. mode : {'full', 'valid', 'same'}, optional - 'full': This returns the convolution at each point of overlap, with an output shape of (N+M-1,). At @@ -475,7 +474,7 @@ def convolve(a, v, mode="full", method="auto"): """ - dpnp.check_supported_arrays_type(a, v) + a, v = dpnp.atleast_1d(a, v) if a.size == 0 or v.size == 0: raise ValueError( @@ -488,11 +487,6 @@ def convolve(a, v, mode="full", method="auto"): f"Received shapes: a.shape={a.shape}, v.shape={v.shape}" ) - if a.ndim == 0: - a = dpnp.reshape(a, (1,)) - if v.ndim == 0: - v = dpnp.reshape(v, (1,)) - device = a.sycl_device rdtype = result_type_for_device([a.dtype, v.dtype], device) diff --git a/dpnp/tests/test_statistics.py b/dpnp/tests/test_statistics.py index 7df647755742..efcc0ae20437 100644 --- a/dpnp/tests/test_statistics.py +++ b/dpnp/tests/test_statistics.py @@ -140,9 +140,6 @@ def _get_kwargs(mode=None, method=None): dpnp_kwargs["method"] = method return dpnp_kwargs, numpy_kwargs - def setup_method(self): - numpy.random.seed(0) - @pytest.mark.parametrize( "a, v", [([1], [1, 2, 3]), ([1, 2, 3], [1]), ([1, 2, 3], [1, 2])] ) @@ -171,10 +168,10 @@ def test_convolve_random(self, a_size, v_size, mode, dtype, method): if dtype in [numpy.int8, numpy.uint8, numpy.int16, numpy.uint16]: pytest.skip("avoid overflow.") an = generate_random_numpy_array( - a_size, dtype, low=-3, high=3, probability=0.9 + a_size, dtype, low=-3, high=3, probability=0.9, seed_value=0 ) vn = generate_random_numpy_array( - v_size, dtype, low=-3, high=3, probability=0.9 + v_size, dtype, low=-3, high=3, probability=0.9, seed_value=1 ) ad = dpnp.array(an) @@ -282,8 +279,12 @@ def test_convolve_shape_error(self, a, v): @pytest.mark.parametrize("size", [2, 10**1, 10**2, 10**3, 10**4, 10**5]) def test_convolve_different_sizes(self, size): - a = generate_random_numpy_array(size, dtype=numpy.float32, low=0, high=1) - v = generate_random_numpy_array(size // 2, dtype=numpy.float32, low=0, high=1) + a = generate_random_numpy_array( + size, dtype=numpy.float32, low=0, high=1, seed_value=0 + ) + v = generate_random_numpy_array( + size // 2, dtype=numpy.float32, low=0, high=1, seed_value=1 + ) ad = dpnp.array(a) vd = dpnp.array(v) @@ -375,9 +376,6 @@ def _get_kwargs(mode=None, method=None): dpnp_kwargs["method"] = method return dpnp_kwargs, numpy_kwargs - def setup_method(self): - numpy.random.seed(0) - @pytest.mark.parametrize( "a, v", [([1], [1, 2, 3]), ([1, 2, 3], [1]), ([1, 2, 3], [1, 2])] ) @@ -406,10 +404,10 @@ def test_correlate_random(self, a_size, v_size, mode, dtype, method): if dtype in [numpy.int8, numpy.uint8, numpy.int16, numpy.uint16]: pytest.skip("avoid overflow.") an = generate_random_numpy_array( - a_size, dtype, low=-3, high=3, probability=0.9 + a_size, dtype, low=-3, high=3, probability=0.9, seed_value=0 ) vn = generate_random_numpy_array( - v_size, dtype, low=-3, high=3, probability=0.9 + v_size, dtype, low=-3, high=3, probability=0.9, seed_value=1 ) ad = dpnp.array(an) @@ -505,8 +503,12 @@ def test_correlate_shape_error(self, a, v): @pytest.mark.parametrize("size", [2, 10**1, 10**2, 10**3, 10**4, 10**5]) def test_correlate_different_sizes(self, size): - a = numpy.random.rand(size).astype(numpy.float32) - v = numpy.random.rand(size // 2).astype(numpy.float32) + a = generate_random_numpy_array( + size, dtype=numpy.float32, low=0, high=1, seed_value=0 + ) + v = generate_random_numpy_array( + size // 2, dtype=numpy.float32, low=0, high=1, seed_value=1 + ) ad = dpnp.array(a) vd = dpnp.array(v) diff --git a/dpnp/tests/third_party/cupy/math_tests/test_misc.py b/dpnp/tests/third_party/cupy/math_tests/test_misc.py index cd73261e7a9b..7746c56d3253 100644 --- a/dpnp/tests/third_party/cupy/math_tests/test_misc.py +++ b/dpnp/tests/third_party/cupy/math_tests/test_misc.py @@ -545,14 +545,14 @@ def test_convolve(self, xp, dtype): class TestConvolve: @testing.for_all_dtypes(no_float16=True) - @testing.numpy_cupy_allclose(rtol=1e-6, type_check=has_support_aspect64()) + @testing.numpy_cupy_allclose(rtol=1e-6) def test_convolve_non_contiguous(self, xp, dtype, mode): a = testing.shaped_arange((300,), xp, dtype) b = testing.shaped_arange((100,), xp, dtype) return xp.convolve(a[::200], b[10::70], mode=mode) @testing.for_all_dtypes(no_float16=True) - @testing.numpy_cupy_allclose(rtol=5e-4, type_check=has_support_aspect64()) + @testing.numpy_cupy_allclose(rtol=5e-4) def test_convolve_large_non_contiguous(self, xp, dtype, mode): a = testing.shaped_arange((10000,), xp, dtype) b = testing.shaped_arange((100,), xp, dtype) From 4570ed17d0e9870806eb0db2370a8c14cfd54df3 Mon Sep 17 00:00:00 2001 From: Alexander Kalistratov Date: Tue, 1 Apr 2025 13:19:50 +0200 Subject: [PATCH 13/13] Review comments --- dpnp/dpnp_iface_manipulation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index c97fcc236218..28ba1a29d50b 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -958,8 +958,8 @@ def atleast_1d(*arys): dpnp.check_supported_arrays_type(*arys) for ary in arys: if ary.ndim == 0: - # Scalars cannot be empty - # Scalars always have a size of 1, so + # 0-d arrays cannot be empty + # 0-d arrays always have a size of 1, so # reshape(1) is guaranteed to succeed result = ary.reshape(1) else: