Skip to content

Implementation of convolve #2205

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 0 additions & 19 deletions dpnp/dpnp_iface_mathematical.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@
"clip",
"conj",
"conjugate",
"convolve",
"copysign",
"cross",
"cumprod",
Expand Down Expand Up @@ -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`.
Expand Down
157 changes: 146 additions & 11 deletions dpnp/dpnp_iface_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"amax",
"amin",
"average",
"convolve",
"corrcoef",
"correlate",
"cov",
Expand Down Expand Up @@ -357,6 +358,150 @@ 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': 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.

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.
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.
Expand Down Expand Up @@ -714,17 +859,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]
Expand Down
29 changes: 0 additions & 29 deletions dpnp/tests/test_mathematical.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,35 +108,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_equal(dpnp.convolve(d, k)[2:-2], dpnp.full(98, 3.0))

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)
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)
Expand Down
Loading
Loading