Skip to content

Commit 1c52d67

Browse files
Rebase on master
1 parent 2956a29 commit 1c52d67

File tree

6 files changed

+294
-59
lines changed

6 files changed

+294
-59
lines changed

dpnp/dpnp_iface_mathematical.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@
9090
"clip",
9191
"conj",
9292
"conjugate",
93-
"convolve",
9493
"copysign",
9594
"cross",
9695
"cumprod",
@@ -791,24 +790,6 @@ def clip(a, /, min=None, max=None, *, out=None, order="K", **kwargs):
791790

792791
conj = conjugate
793792

794-
795-
def convolve(a, v, mode="full"):
796-
"""
797-
Returns the discrete, linear convolution of two one-dimensional sequences.
798-
799-
For full documentation refer to :obj:`numpy.convolve`.
800-
801-
Examples
802-
--------
803-
>>> ca = dpnp.convolve([1, 2, 3], [0, 1, 0.5])
804-
>>> print(ca)
805-
[0. , 1. , 2.5, 4. , 1.5]
806-
807-
"""
808-
809-
return call_origin(numpy.convolve, a=a, v=v, mode=mode)
810-
811-
812793
_COPYSIGN_DOCSTRING = """
813794
Composes a floating-point value with the magnitude of `x1_i` and the sign of
814795
`x2_i` for each element of input arrays `x1` and `x2`.

dpnp/dpnp_iface_statistics.py

Lines changed: 134 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"amax",
6363
"amin",
6464
"average",
65+
"convolve",
6566
"corrcoef",
6667
"correlate",
6768
"cov",
@@ -357,6 +358,138 @@ def average(a, axis=None, weights=None, returned=False, *, keepdims=False):
357358
return avg
358359

359360

361+
def _convolve_impl(a, v, mode, method, rdtype):
362+
l_pad, r_pad = _get_padding(a.size, v.size, mode)
363+
364+
if method == "auto":
365+
method = _choose_conv_method(a, v, rdtype)
366+
367+
if method == "direct":
368+
r = _run_native_sliding_dot_product1d(a, v[::-1], l_pad, r_pad, rdtype)
369+
elif method == "fft":
370+
r = _convolve_fft(a, v, l_pad, r_pad, rdtype)
371+
else:
372+
raise ValueError(
373+
f"Unknown method: {method}. Supported methods: auto, direct, fft"
374+
)
375+
376+
return r
377+
378+
379+
def convolve(a, v, mode="full", method="auto"):
380+
r"""
381+
Returns the discrete, linear convolution of two one-dimensional sequences.
382+
The convolution operator is often seen in signal processing, where it
383+
models the effect of a linear time-invariant system on a signal [1]_. In
384+
probability theory, the sum of two independent random variables is
385+
distributed according to the convolution of their individual
386+
distributions.
387+
If `v` is longer than `a`, the arrays are swapped before computation.
388+
For full documentation refer to :obj:`numpy.convolve`.
389+
Parameters
390+
----------
391+
a : {dpnp.ndarray, usm_ndarray}
392+
First 1-D array.
393+
v : {dpnp.ndarray, usm_ndarray}
394+
Second 1-D array. The length of `v` must be less than or equal to
395+
the length of `a`.
396+
mode : {'full', 'valid', 'same'}, optional
397+
'full':
398+
By default, mode is 'full'. This returns the convolution
399+
at each point of overlap, with an output shape of (N+M-1,). At
400+
the end-points of the convolution, the signals do not overlap
401+
completely, and boundary effects may be seen.
402+
'same':
403+
Mode 'same' returns output of length ``max(M, N)``. Boundary
404+
effects are still visible.
405+
'valid':
406+
Mode 'valid' returns output of length
407+
``max(M, N) - min(M, N) + 1``. The convolution product is only given
408+
for points where the signals overlap completely. Values outside
409+
the signal boundary have no effect.
410+
method : {'auto', 'direct', 'fft'}, optional
411+
'direct':
412+
The convolution is determined directly from sums.
413+
'fft':
414+
The Fourier Transform is used to perform the calculations.
415+
This method is faster for long sequences but can have accuracy issues.
416+
'auto':
417+
Automatically chooses direct or Fourier method based on
418+
an estimate of which is faster.
419+
Note: Use of the FFT convolution on input containing NAN or INF
420+
will lead to the entire output being NAN or INF.
421+
Use method='direct' when your input contains NAN or INF values.
422+
Default: ``'auto'``.
423+
Returns
424+
-------
425+
out : ndarray
426+
Discrete, linear convolution of `a` and `v`.
427+
See Also
428+
--------
429+
:obj:`dpnp.correlate` : Cross-correlation of two 1-dimensional sequences.
430+
Notes
431+
-----
432+
The discrete convolution operation is defined as
433+
.. math:: (a * v)_n = \\sum_{m = -\\infty}^{\\infty} a_m v_{n - m}
434+
It can be shown that a convolution :math:`x(t) * y(t)` in time/space
435+
is equivalent to the multiplication :math:`X(f) Y(f)` in the Fourier
436+
domain, after appropriate padding (padding is necessary to prevent
437+
circular convolution). Since multiplication is more efficient (faster)
438+
than convolution, the function implements two approaches - direct and fft
439+
which are regulated by the keyword `method`.
440+
References
441+
----------
442+
.. [1] Uncyclopedia, "Convolution",
443+
https://en.wikipedia.org/wiki/Convolution
444+
Examples
445+
--------
446+
Note how the convolution operator flips the second array
447+
before "sliding" the two across one another:
448+
>>> import dpnp as np
449+
>>> a = np.array([1, 2, 3], dtype=np.float32)
450+
>>> v = np.array([0, 1, 0.5], dtype=np.float32)
451+
>>> np.convolve(a, v)
452+
array([0. , 1. , 2.5, 4. , 1.5], dtype=float32)
453+
Only return the middle values of the convolution.
454+
Contains boundary effects, where zeros are taken
455+
into account:
456+
>>> np.convolve(a, v, 'same')
457+
array([1. , 2.5, 4. ], dtype=float32)
458+
The two arrays are of the same length, so there
459+
is only one position where they completely overlap:
460+
>>> np.convolve(a, v, 'valid')
461+
array([2.5], dtype=float32)
462+
"""
463+
464+
dpnp.check_supported_arrays_type(a, v)
465+
466+
if a.size == 0 or v.size == 0:
467+
raise ValueError(
468+
f"Array arguments cannot be empty. "
469+
f"Received sizes: a.size={a.size}, v.size={v.size}"
470+
)
471+
if a.ndim > 1 or v.ndim > 1:
472+
raise ValueError(
473+
f"Only 1-dimensional arrays are supported. "
474+
f"Received shapes: a.shape={a.shape}, v.shape={v.shape}"
475+
)
476+
477+
if a.ndim == 0:
478+
a = dpnp.reshape(a, (1,))
479+
if v.ndim == 0:
480+
v = dpnp.reshape(v, (1,))
481+
482+
device = a.sycl_device
483+
rdtype = result_type_for_device([a.dtype, v.dtype], device)
484+
485+
if v.size > a.size:
486+
a, v = v, a
487+
488+
r = _convolve_impl(a, v, mode, method, rdtype)
489+
490+
return dpnp.asarray(r, dtype=rdtype, order="C")
491+
492+
360493
def corrcoef(x, y=None, rowvar=True, *, dtype=None):
361494
"""
362495
Return Pearson product-moment correlation coefficients.
@@ -714,17 +847,7 @@ def correlate(a, v, mode="valid", method="auto"):
714847
revert = True
715848
a, v = v, a
716849

717-
l_pad, r_pad = _get_padding(a.size, v.size, mode)
718-
719-
if method == "auto":
720-
method = _choose_conv_method(a, v, rdtype)
721-
722-
if method == "direct":
723-
r = _run_native_sliding_dot_product1d(a, v, l_pad, r_pad, rdtype)
724-
elif method == "fft":
725-
r = _convolve_fft(a, v[::-1], l_pad, r_pad, rdtype)
726-
else: # pragma: no cover
727-
raise ValueError(f"Unknown method: {method}")
850+
r = _convolve_impl(a, v[::-1], mode, method, rdtype)
728851

729852
if revert:
730853
r = r[::-1]

dpnp/tests/test_mathematical.py

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -110,35 +110,6 @@ def test_conj_out(self, dtype):
110110
assert_dtype_allclose(result, expected)
111111

112112

113-
@pytest.mark.usefixtures("allow_fall_back_on_numpy")
114-
class TestConvolve:
115-
def test_object(self):
116-
d = [1.0] * 100
117-
k = [1.0] * 3
118-
assert_array_almost_equal(dpnp.convolve(d, k)[2:-2], dpnp.full(98, 3))
119-
120-
def test_no_overwrite(self):
121-
d = dpnp.ones(100)
122-
k = dpnp.ones(3)
123-
dpnp.convolve(d, k)
124-
assert_array_equal(d, dpnp.ones(100))
125-
assert_array_equal(k, dpnp.ones(3))
126-
127-
def test_mode(self):
128-
d = dpnp.ones(100)
129-
k = dpnp.ones(3)
130-
default_mode = dpnp.convolve(d, k, mode="full")
131-
full_mode = dpnp.convolve(d, k, mode="full")
132-
assert_array_equal(full_mode, default_mode)
133-
# integer mode
134-
with assert_raises(ValueError):
135-
dpnp.convolve(d, k, mode=-1)
136-
assert_array_equal(dpnp.convolve(d, k, mode=2), full_mode)
137-
# illegal arguments
138-
with assert_raises(TypeError):
139-
dpnp.convolve(d, k, mode=None)
140-
141-
142113
class TestClip:
143114
@pytest.mark.parametrize(
144115
"dtype", get_all_dtypes(no_bool=True, no_none=True, no_complex=True)

dpnp/tests/test_statistics.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,164 @@ def test_avg_error(self):
127127
dpnp.average(a, axis=0, weights=w)
128128

129129

130+
class TestConvolve:
131+
def setup_method(self):
132+
numpy.random.seed(0)
133+
134+
@pytest.mark.parametrize(
135+
"a, v", [([1], [1, 2, 3]), ([1, 2, 3], [1]), ([1, 2, 3], [1, 2])]
136+
)
137+
@pytest.mark.parametrize("mode", [None, "full", "valid", "same"])
138+
@pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True))
139+
@pytest.mark.parametrize("method", [None, "auto", "direct", "fft"])
140+
def test_convolve(self, a, v, mode, dtype, method):
141+
an = numpy.array(a, dtype=dtype)
142+
vn = numpy.array(v, dtype=dtype)
143+
ad = dpnp.array(an)
144+
vd = dpnp.array(vn)
145+
146+
dpnp_kwargs = {}
147+
numpy_kwargs = {}
148+
if mode is not None:
149+
dpnp_kwargs["mode"] = mode
150+
numpy_kwargs["mode"] = mode
151+
if method is not None:
152+
dpnp_kwargs["method"] = method
153+
154+
expected = numpy.convolve(an, vn, **numpy_kwargs)
155+
result = dpnp.convolve(ad, vd, **dpnp_kwargs)
156+
157+
assert_dtype_allclose(result, expected)
158+
159+
@pytest.mark.parametrize("a_size", [1, 100, 10000])
160+
@pytest.mark.parametrize("v_size", [1, 100, 10000])
161+
@pytest.mark.parametrize("mode", ["full", "valid", "same"])
162+
@pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True))
163+
@pytest.mark.parametrize("method", ["auto", "direct", "fft"])
164+
def test_convolve_random(self, a_size, v_size, mode, dtype, method):
165+
if dtype == dpnp.bool:
166+
an = numpy.random.rand(a_size) > 0.9
167+
vn = numpy.random.rand(v_size) > 0.9
168+
else:
169+
an = (100 * numpy.random.rand(a_size)).astype(dtype)
170+
vn = (100 * numpy.random.rand(v_size)).astype(dtype)
171+
172+
if dpnp.issubdtype(dtype, dpnp.complexfloating):
173+
an = an + 1j * (100 * numpy.random.rand(a_size)).astype(dtype)
174+
vn = vn + 1j * (100 * numpy.random.rand(v_size)).astype(dtype)
175+
176+
ad = dpnp.array(an)
177+
vd = dpnp.array(vn)
178+
179+
dpnp_kwargs = {}
180+
numpy_kwargs = {}
181+
if mode is not None:
182+
dpnp_kwargs["mode"] = mode
183+
numpy_kwargs["mode"] = mode
184+
if method is not None:
185+
dpnp_kwargs["method"] = method
186+
187+
result = dpnp.convolve(ad, vd, **dpnp_kwargs)
188+
expected = numpy.convolve(an, vn, **numpy_kwargs)
189+
190+
rdtype = result.dtype
191+
if dpnp.issubdtype(rdtype, dpnp.integer):
192+
rdtype = dpnp.default_float_type(ad.device)
193+
194+
if method != "fft" and (
195+
dpnp.issubdtype(dtype, dpnp.integer) or dtype == dpnp.bool
196+
):
197+
# For 'direct' and 'auto' methods, we expect exact results for integer types
198+
assert_array_equal(result, expected)
199+
else:
200+
result = result.astype(rdtype)
201+
if method == "direct":
202+
expected = numpy.convolve(an, vn, **numpy_kwargs)
203+
# For 'direct' method we can use standard validation
204+
assert_dtype_allclose(result, expected, factor=30)
205+
else:
206+
rtol = 1e-3
207+
atol = 1e-10
208+
209+
if rdtype == dpnp.float64 or rdtype == dpnp.complex128:
210+
rtol = 1e-6
211+
atol = 1e-12
212+
elif rdtype == dpnp.bool:
213+
result = result.astype(dpnp.int32)
214+
rdtype = result.dtype
215+
216+
expected = expected.astype(rdtype)
217+
218+
diff = numpy.abs(result.asnumpy() - expected)
219+
invalid = diff > atol + rtol * numpy.abs(expected)
220+
221+
# When using the 'fft' method, we might encounter outliers.
222+
# This usually happens when the resulting array contains values close to zero.
223+
# For these outliers, the relative error can be significant.
224+
# We can tolerate a few such outliers.
225+
max_outliers = 8 if expected.size > 1 else 0
226+
if invalid.sum() > max_outliers:
227+
assert_dtype_allclose(result, expected, factor=1000)
228+
229+
def test_convolve_mode_error(self):
230+
a = dpnp.arange(5)
231+
v = dpnp.arange(3)
232+
233+
# invalid mode
234+
with pytest.raises(ValueError):
235+
dpnp.convolve(a, v, mode="unknown")
236+
237+
@pytest.mark.parametrize("a, v", [([], [1]), ([1], []), ([], [])])
238+
def test_convolve_empty(self, a, v):
239+
a = dpnp.asarray(a)
240+
v = dpnp.asarray(v)
241+
242+
with pytest.raises(ValueError):
243+
dpnp.convolve(a, v)
244+
245+
@pytest.mark.parametrize(
246+
"a, v",
247+
[
248+
([[1, 2], [2, 3]], [1]),
249+
([1], [[1, 2], [2, 3]]),
250+
([[1, 2], [2, 3]], [[1, 2], [2, 3]]),
251+
],
252+
)
253+
def test_convolve_shape_error(self, a, v):
254+
a = dpnp.asarray(a)
255+
v = dpnp.asarray(v)
256+
257+
with pytest.raises(ValueError):
258+
dpnp.convolve(a, v)
259+
260+
@pytest.mark.parametrize("size", [2, 10**1, 10**2, 10**3, 10**4, 10**5])
261+
def test_convolve_different_sizes(self, size):
262+
a = numpy.random.rand(size).astype(numpy.float32)
263+
v = numpy.random.rand(size // 2).astype(numpy.float32)
264+
265+
ad = dpnp.array(a)
266+
vd = dpnp.array(v)
267+
268+
expected = numpy.convolve(a, v)
269+
result = dpnp.convolve(ad, vd, method="direct")
270+
271+
assert_dtype_allclose(result, expected, factor=20)
272+
273+
def test_convolve_another_sycl_queue(self):
274+
a = dpnp.arange(5, sycl_queue=dpctl.SyclQueue())
275+
v = dpnp.arange(3, sycl_queue=dpctl.SyclQueue())
276+
277+
with pytest.raises(ValueError):
278+
dpnp.convolve(a, v)
279+
280+
def test_convolve_unkown_method(self):
281+
a = dpnp.arange(5)
282+
v = dpnp.arange(3)
283+
284+
with pytest.raises(ValueError):
285+
dpnp.convolve(a, v, method="unknown")
286+
287+
130288
class TestCorrcoef:
131289
@pytest.mark.usefixtures(
132290
"suppress_divide_invalid_numpy_warnings",

0 commit comments

Comments
 (0)