From f7ed376d7109f3a136f0f0fdcf5514fb3a5b156c Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Fri, 14 Jun 2024 10:25:43 -0500 Subject: [PATCH 01/11] implement fft2, ifft2, fftn, ifftn --- .github/workflows/conda-package.yml | 1 + dpnp/fft/dpnp_iface_fft.py | 487 ++++++++++++++----- dpnp/fft/dpnp_utils_fft.py | 189 ++++++- tests/skipped_tests.tbl | 24 - tests/skipped_tests_gpu.tbl | 24 - tests/test_fft.py | 190 ++++++++ tests/test_sycl_queue.py | 19 + tests/test_usm_type.py | 12 + tests/third_party/cupy/fft_tests/test_fft.py | 177 ++++--- 9 files changed, 889 insertions(+), 234 deletions(-) diff --git a/.github/workflows/conda-package.yml b/.github/workflows/conda-package.yml index 9d9fec52cf8..963ff970b82 100644 --- a/.github/workflows/conda-package.yml +++ b/.github/workflows/conda-package.yml @@ -52,6 +52,7 @@ env: test_umath.py test_usm_type.py third_party/cupy/core_tests + third_party/cupy/fft_tests third_party/cupy/creation_tests third_party/cupy/indexing_tests/test_indexing.py third_party/cupy/lib_tests diff --git a/dpnp/fft/dpnp_iface_fft.py b/dpnp/fft/dpnp_iface_fft.py index 08f5245ae02..54498df7a09 100644 --- a/dpnp/fft/dpnp_iface_fft.py +++ b/dpnp/fft/dpnp_iface_fft.py @@ -50,6 +50,7 @@ from .dpnp_utils_fft import ( dpnp_fft, + dpnp_fftn, ) __all__ = [ @@ -96,6 +97,9 @@ def fft(a, n=None, axis=-1, norm=None, out=None): """ Compute the one-dimensional discrete Fourier Transform. + This function computes the one-dimensional `n`-point discrete Fourier + Transform (DFT) with the efficient Fast Fourier Transform (FFT) algorithm. + For full documentation refer to :obj:`numpy.fft.fft`. Parameters @@ -144,6 +148,9 @@ def fft(a, n=None, axis=-1, norm=None, out=None): calculated terms. The symmetry is highest when `n` is a power of 2, and the transform is therefore most efficient for these sizes. + The DFT is defined, with the conventions used in this implementation, + in the documentation for the :obj:`dpnp.fft` module. + Examples -------- >>> import dpnp as np @@ -162,32 +169,100 @@ def fft(a, n=None, axis=-1, norm=None, out=None): ) -def fft2(x, s=None, axes=(-2, -1), norm=None): +def fft2(a, s=None, axes=(-2, -1), norm=None, out=None): """ Compute the 2-dimensional discrete Fourier Transform. - Multi-dimensional arrays computed as batch of 1-D arrays. + This function computes the `N`-dimensional discrete Fourier Transform over + any axes in an `M`-dimensional array by means of the Fast Fourier + Transform (FFT). By default, the transform is computed over the last two + axes of the input array, i.e., a 2-dimensional FFT. For full documentation refer to :obj:`numpy.fft.fft2`. - Limitations - ----------- - Parameter `x` is supported either as :class:`dpnp.ndarray`. - Parameter `norm` is unsupported. - Only `dpnp.float64`, `dpnp.float32`, `dpnp.int64`, `dpnp.int32`, - `dpnp.complex128` data types are supported. - Otherwise the function will be executed sequentially on CPU. + Parameters + ---------- + a : {dpnp.ndarray, usm_ndarray} + Input array, can be complex. + s : {None, sequence of ints}, optional + Shape (length of each transformed axis) of the output + (``s[0]`` refers to axis 0, ``s[1]`` to axis 1, etc.). + This corresponds to `n` for ``fft(x, n)``. + Along each axis, if the given shape is smaller than that of the input, + the input is cropped. If it is larger, the input is padded with zeros. + If it is ``-1``, the whole input is used (no padding/trimming). + If `s` is not given, the shape of the input along the axes specified + by `axes` is used. If `s` is not ``None``, `axes` must not be ``None`` + either. Default: ``None``. + axes : {None, sequence of ints}, optional + Axes over which to compute the FFT. If not given, the last two axes are + used. A repeated index in `axes` means the transform over that axis is + performed multiple times. If `s` is specified, the corresponding `axes` + to be transformed must be explicitly specified too. A one-element + sequence means that a one-dimensional FFT is performed. An empty + sequence means that no FFT is performed. + Default: ``(-2, -1)``. + norm : {None, "backward", "ortho", "forward"}, optional + Normalization mode (see :obj:`dpnp.fft`). + Indicates which direction of the forward/backward pair of transforms + is scaled and with what normalization factor. ``None`` is an alias of + the default option ``"backward"``. + Default: ``"backward"``. + out : {None, dpnp.ndarray or usm_ndarray of complex dtype}, optional + If provided, the result will be placed in this array. It should be + of the appropriate shape and dtype. + Default: ``None``. - """ + Returns + ------- + out : dpnp.ndarray of complex dtype + The truncated or zero-padded input, transformed along the axes + indicated by `axes`, or the last two axes if `axes` is not given. - x_desc = dpnp.get_dpnp_descriptor(x, copy_when_nondefault_queue=False) - if x_desc: - if norm is not None: - pass - else: - return fftn(x, s, axes, norm) + See Also + -------- + :obj:`dpnp.fft` : Overall view of discrete Fourier transforms, with + definitions and conventions used. + :obj:`dpnp.fft.ifft2` : The inverse two-dimensional FFT. + :obj:`dpnp.fft.fft` : The one-dimensional FFT. + :obj:`dpnp.fft.fftn` : The `n`-dimensional FFT. + :obj:`dpnp.fft.fftshift` : Shifts zero-frequency terms to the center of + the array. For two-dimensional input, swaps first and third quadrants, + and second and fourth quadrants. + + Notes + ----- + :obj:`dpnp.fft.fft2` is just :obj:`dpnp.fft.fftn` with a different + default for `axes`. + + The output, analogously to :obj:`dpnp.fft.fft`, contains the term for zero + frequency in the low-order corner of the transformed axes, the positive + frequency terms in the first half of these axes, the term for the Nyquist + frequency in the middle of the axes and the negative frequency terms in + the second half of the axes, in order of decreasingly negative frequency. + + See :obj:`dpnp.fft` for details, definitions and conventions used. + + Examples + -------- + >>> import dpnp as np + >>> a = np.mgrid[:5, :5][0] + >>> np.fft.fft2(a) + array([[ 50. +0.j , 0. +0.j , 0. +0.j , + 0. +0.j , 0. +0.j ], + [-12.5+17.20477401j, 0. +0.j , 0. +0.j , + 0. +0.j , 0. +0.j ], + [-12.5 +4.0614962j , 0. +0.j , 0. +0.j , + 0. +0.j , 0. +0.j ], + [-12.5 -4.0614962j , 0. +0.j , 0. +0.j , + 0. +0.j , 0. +0.j ], + [-12.5-17.20477401j, 0. +0.j , 0. +0.j , + 0. +0.j , 0. +0.j ]]) # may vary + + """ - return call_origin(numpy.fft.fft2, x, s, axes, norm) + dpnp.check_supported_arrays_type(a) + return dpnp_fftn(a, forward=True, s=s, axes=axes, norm=norm, out=out) def fftfreq(n, d=1.0, device=None, usm_type=None, sycl_queue=None): @@ -300,59 +375,102 @@ def fftfreq(n, d=1.0, device=None, usm_type=None, sycl_queue=None): return results * val -def fftn(x, s=None, axes=None, norm=None): +def fftn(a, s=None, axes=None, norm=None, out=None): """ - Compute the N-dimensional FFT. + Compute the `N`-dimensional discrete Fourier Transform. - Multi-dimensional arrays computed as batch of 1-D arrays. + This function computes the `N`-dimensional discrete Fourier Transform over + any number of axes in an `M`-dimensional array by means of the + Fast Fourier Transform (FFT). For full documentation refer to :obj:`numpy.fft.fftn`. - Limitations - ----------- - Parameter `x` is supported either as :class:`dpnp.ndarray`. - Parameter `norm` is unsupported. - Only `dpnp.float64`, `dpnp.float32`, `dpnp.int64`, `dpnp.int32`, - `dpnp.complex128` data types are supported. - Otherwise the function will be executed sequentially on CPU. + Parameters + ---------- + a : {dpnp.ndarray, usm_ndarray} + Input array, can be complex. + s : {None, sequence of ints}, optional + Shape (length of each transformed axis) of the output + (``s[0]`` refers to axis 0, ``s[1]`` to axis 1, etc.). + This corresponds to `n` for ``fft(x, n)``. + Along each axis, if the given shape is smaller than that of the input, + the input is cropped. If it is larger, the input is padded with zeros. + If it is ``-1``, the whole input is used (no padding/trimming). + If `s` is not given, the shape of the input along the axes specified + by `axes` is used. If `s` is not ``None``, `axes` must not be ``None`` + either. Default: ``None``. + axes : {None, sequence of ints}, optional + Axes over which to compute the FFT. If not given, the last ``len(s)`` + axes are used, or all axes if `s` is also not specified. + Repeated indices in `axes` means that the transform over that axis is + performed multiple times. If `s` is specified, the corresponding `axes` + to be transformed must be explicitly specified too. A one-element + sequence means that a one-dimensional FFT is performed. An empty + sequence means that no FFT is performed. + Default: ``None``. + norm : {None, "backward", "ortho", "forward"}, optional + Normalization mode (see :obj:`dpnp.fft`). + Indicates which direction of the forward/backward pair of transforms + is scaled and with what normalization factor. ``None`` is an alias of + the default option ``"backward"``. + Default: ``"backward"``. + out : {None, dpnp.ndarray or usm_ndarray of complex dtype}, optional + If provided, the result will be placed in this array. It should be + of the appropriate shape and dtype. + Default: ``None``. - """ + Returns + ------- + out : dpnp.ndarray of complex dtype + The truncated or zero-padded input, transformed along the axes + indicated by `axes`, or by a combination of `s` and `a`, + as explained in the parameters section above. - x_desc = dpnp.get_dpnp_descriptor(x, copy_when_nondefault_queue=False) - if x_desc: - if s is None: - boundaries = tuple(x_desc.shape[i] for i in range(x_desc.ndim)) - else: - boundaries = s + See Also + -------- + :obj:`dpnp.fft` : Overall view of discrete Fourier transforms, with + definitions and conventions used. + :obj:`dpnp.fft.ifftn` : The inverse `n`-dimensional FFT. + :obj:`dpnp.fft.fft` : The one-dimensional FFT. + :obj:`dpnp.fft.rfftn` : The `n`-dimensional FFT of real input. + :obj:`dpnp.fft.fftshift` : Shifts zero-frequency terms to the center of + the array. - if axes is None: - axes_param = list(range(x_desc.ndim)) - else: - axes_param = axes + Notes + ----- + The output, analogously to :obj:`dpnp.fft.fft`, contains the term for zero + frequency in the low-order corner of the transformed axes, the positive + frequency terms in the first half of these axes, the term for the Nyquist + frequency in the middle of the axes and the negative frequency terms in + the second half of the axes, in order of decreasingly negative frequency. - if norm is not None: - pass - else: - x_iter = x - iteration_list = list(range(len(axes_param))) - iteration_list.reverse() # inplace operation - for it in iteration_list: - param_axis = axes_param[it] - try: - param_n = boundaries[param_axis] - except IndexError: - checker_throw_axis_error( - "fft.fftn", - "is out of bounds", - param_axis, - f"< {len(boundaries)}", - ) + See :obj:`dpnp.fft` for details, definitions and conventions used. - x_iter = fft(x_iter, n=param_n, axis=param_axis, norm=norm) + Examples + -------- + >>> import dpnp as np + >>> a = np.mgrid[:3, :3, :3][0] + >>> np.fft.fftn(a, axes=(1, 2)) + array([[[ 0.+0.j, 0.+0.j, 0.+0.j], # may vary + [ 0.+0.j, 0.+0.j, 0.+0.j], + [ 0.+0.j, 0.+0.j, 0.+0.j]], + [[ 9.+0.j, 0.+0.j, 0.+0.j], + [ 0.+0.j, 0.+0.j, 0.+0.j], + [ 0.+0.j, 0.+0.j, 0.+0.j]], + [[18.+0.j, 0.+0.j, 0.+0.j], + [ 0.+0.j, 0.+0.j, 0.+0.j], + [ 0.+0.j, 0.+0.j, 0.+0.j]]]) + + >>> np.fft.fftn(a, (2, 2), axes=(0, 1)) + array([[[ 2.+0.j, 2.+0.j, 2.+0.j], # may vary + [ 0.+0.j, 0.+0.j, 0.+0.j]], + [[-2.+0.j, -2.+0.j, -2.+0.j], + [ 0.+0.j, 0.+0.j, 0.+0.j]]]) - return x_iter + """ - return call_origin(numpy.fft.fftn, x, s, axes, norm) + dpnp.check_supported_arrays_type(a) + return dpnp_fftn(a, forward=True, s=s, axes=axes, norm=norm, out=out) def fftshift(x, axes=None): @@ -515,6 +633,24 @@ def ifft(a, n=None, axis=-1, norm=None, out=None): """ Compute the one-dimensional inverse discrete Fourier Transform. + This function computes the inverse of the one-dimensional `n`-point + discrete Fourier transform computed by :obj:`dpnp.fft.fft`. In other words, + ``ifft(fft(a)) == a`` to within numerical accuracy. + For a general description of the algorithm and definitions, + see :obj:`dpnp.fft`. + + The input should be ordered in the same way as is returned by + :obj:`dpnp.fft.fft`, i.e., + + * ``a[0]`` should contain the zero frequency term, + * ``a[1:n//2]`` should contain the positive-frequency terms, + * ``a[n//2 + 1:]`` should contain the negative-frequency terms, in + increasing order starting from the most negative frequency. + + For an even number of input points, ``A[n//2]`` represents the sum of + the values at the positive and negative Nyquist frequencies, as the two + are aliased together. + For full documentation refer to :obj:`numpy.fft.ifft`. Parameters @@ -577,95 +713,198 @@ def ifft(a, n=None, axis=-1, norm=None, out=None): ) -def ifft2(x, s=None, axes=(-2, -1), norm=None): +def ifft2(a, s=None, axes=(-2, -1), norm=None, out=None): """ Compute the 2-dimensional inverse discrete Fourier Transform. - Multi-dimensional arrays computed as batch of 1-D arrays. + This function computes the inverse of the 2-dimensional discrete Fourier + Transform over any number of axes in an `M`-dimensional array by means of + the Fast Fourier Transform (FFT). In other words, ``ifft2(fft2(a)) == a`` + to within numerical accuracy. By default, the inverse transform is + computed over the last two axes of the input array. + + The input, analogously to :obj:`dpnp.fft.ifft`, should be ordered in the + same way as is returned by :obj:`dpnp.fft.fft2`, i.e. it should have the + term for zero frequency in the low-order corner of the two axes, the + positive frequency terms in the first half of these axes, the term for the + Nyquist frequency in the middle of the axes and the negative frequency + terms in the second half of both axes, in order of decreasingly negative + frequency. For full documentation refer to :obj:`numpy.fft.ifft2`. - Limitations - ----------- - Parameter `x` is supported either as :class:`dpnp.ndarray`. - Parameter `norm` is unsupported. - Only `dpnp.float64`, `dpnp.float32`, `dpnp.int64`, `dpnp.int32`, - `dpnp.complex128` data types are supported. - Otherwise the function will be executed sequentially on CPU. + Parameters + ---------- + a : {dpnp.ndarray, usm_ndarray} + Input array, can be complex. + s : {None, sequence of ints}, optional + Shape (length of each transformed axis) of the output + (``s[0]`` refers to axis 0, ``s[1]`` to axis 1, etc.). + This corresponds to `n` for ``ifft(x, n)``. + Along each axis, if the given shape is smaller than that of the input, + the input is cropped. If it is larger, the input is padded with zeros. + If it is ``-1``, the whole input is used (no padding/trimming). + If `s` is not given, the shape of the input along the axes specified + by `axes` is used. See notes for issue on :obj:`dpnp.fft.ifft` + zero padding. If `s` is not ``None``, `axes` must not be ``None`` + either. Default: ``None``. + axes : {None, sequence of ints}, optional + Axes over which to compute the inverse FFT. If not given, the last two + axes are used. A repeated index in `axes` means the transform over that + axis is performed multiple times. If `s` is specified, the + corresponding `axes` to be transformed must be explicitly specified + too. A one-element sequence means that a one-dimensional FFT is + performed. An empty sequence means that no FFT is performed. + Default: ``(-2, -1)``. + norm : {None, "backward", "ortho", "forward"}, optional + Normalization mode (see :obj:`dpnp.fft`). + Indicates which direction of the forward/backward pair of transforms + is scaled and with what normalization factor. ``None`` is an alias of + the default option ``"backward"``. + Default: ``"backward"``. + out : {None, dpnp.ndarray or usm_ndarray of complex dtype}, optional + If provided, the result will be placed in this array. It should be + of the appropriate shape and dtype. + Default: ``None``. - """ + Returns + ------- + out : dpnp.ndarray of complex dtype + The truncated or zero-padded input, transformed along the axes + indicated by `axes`, or the last two axes if `axes` is not given. - x_desc = dpnp.get_dpnp_descriptor(x, copy_when_nondefault_queue=False) - if x_desc: - if norm is not None: - pass - else: - return ifftn(x, s, axes, norm) + See Also + -------- + :obj:`dpnp.fft` : Overall view of discrete Fourier transforms, with + definitions and conventions used. + :obj:`dpnp.fft.fft2` : The forward two-dimensional FFT, of which + :obj:`dpnp.fft.ifft2` is the inverse. + :obj:`dpnp.fft.ifftn` : The inverse of `n`-dimensional FFT. + :obj:`dpnp.fft.fft` : The one-dimensional FFT. + :obj:`dpnp.fft.ifft` : The one-dimensional inverse FFT. - return call_origin(numpy.fft.ifft2, x, s, axes, norm) + Notes + ----- + :obj:`dpnp.fft.ifft2` is just :obj:`dpnp.fft.ifftn` with a different + default for `axes`. See :obj:`dpnp.fft` for details, definitions and + conventions used. + Zero-padding, analogously with :obj:`dpnp.fft.ifft`, is performed by + appending zeros to the input along the specified dimension. Although this + is the common approach, it might lead to surprising results. If another + form of zero padding is desired, it must be performed before + :obj:`dpnp.fft.ifft2` is called. + + Examples + -------- + >>> import dpnp as np + >>> a = 4 * np.eye(4) + >>> np.fft.ifft2(a) + array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], # may vary + [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j], + [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j], + [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j]]) -def ifftn(x, s=None, axes=None, norm=None): """ - Compute the N-dimensional inverse discrete Fourier Transform. - Multi-dimensional arrays computed as batch of 1-D arrays. + dpnp.check_supported_arrays_type(a) + return dpnp_fftn(a, forward=False, s=s, axes=axes, norm=norm, out=out) + + +def ifftn(a, s=None, axes=None, norm=None, out=None): + """ + Compute the `N`-dimensional inverse discrete Fourier Transform. + + This function computes the inverse of the `N`-dimensional discrete + Fourier Transform over any number of axes in an `M`-dimensional array by + means of the Fast Fourier Transform (FFT). In other words, + ``ifftn(fftn(a)) == a`` to within numerical accuracy. For a description + of the definitions and conventions used, see :obj:`dpnp.fft`. + + The input, analogously to :obj:`dpnp.fft.ifft`, should be ordered in the + same way as is returned by :obj:`dpnp.fft.fftn`, i.e. it should have the + term for zero frequency in all axes in the low-order corner, the positive + frequency terms in the first half of all axes, the term for the Nyquist + frequency in the middle of all axes and the negative frequency terms in + the second half of all axes, in order of decreasingly negative frequency. For full documentation refer to :obj:`numpy.fft.ifftn`. - Limitations - ----------- - Parameter `x` is supported either as :class:`dpnp.ndarray`. - Parameter `norm` is unsupported. - Only `dpnp.float64`, `dpnp.float32`, `dpnp.int64`, `dpnp.int32`, - `dpnp.complex128` data types are supported. - Otherwise the function will be executed sequentially on CPU. + Parameters + ---------- + a : {dpnp.ndarray, usm_ndarray} + Input array, can be complex. + s : {None, sequence of ints}, optional + Shape (length of each transformed axis) of the output + (``s[0]`` refers to axis 0, ``s[1]`` to axis 1, etc.). + This corresponds to `n` for ``ifft(x, n)``. + Along each axis, if the given shape is smaller than that of the input, + the input is cropped. If it is larger, the input is padded with zeros. + If it is ``-1``, the whole input is used (no padding/trimming). + if `s` is not given, the shape of the input along the axes specified + by `axes` is used. If `s` is not ``None``, `axes` must not be ``None`` + either. Default: ``None``. + axes : {None, sequence of ints}, optional + Axes over which to compute the inverse FFT. If not given, the last + ``len(s)`` axes are used, or all axes if `s` is also not specified. + Repeated indices in `axes` means that the transform over that axis is + performed multiple times. If `s` is specified, the corresponding `axes` + to be transformed must be explicitly specified too. A one-element + sequence means that a one-dimensional FFT is performed. An empty + sequence means that no FFT is performed. + Default: ``None``. + norm : {None, "backward", "ortho", "forward"}, optional + Normalization mode (see :obj:`dpnp.fft`). + Indicates which direction of the forward/backward pair of transforms + is scaled and with what normalization factor. ``None`` is an alias of + the default option ``"backward"``. + Default: ``"backward"``. + out : {None, dpnp.ndarray or usm_ndarray of complex dtype}, optional + If provided, the result will be placed in this array. It should be + of the appropriate shape and dtype. + Default: ``None``. - """ + Returns + ------- + out : dpnp.ndarray of complex dtype + The truncated or zero-padded input, transformed along the axes + indicated by `axes`, or by a combination of `s` and `a`, + as explained in the parameters section above. - x_desc = dpnp.get_dpnp_descriptor(x, copy_when_nondefault_queue=False) - # TODO: enable implementation - # pylint: disable=condition-evals-to-constant - if x_desc and 0: - if s is None: - boundaries = tuple(x_desc.shape[i] for i in range(x_desc.ndim)) - else: - boundaries = s + See Also + -------- + :obj:`dpnp.fft` : Overall view of discrete Fourier transforms, with + definitions and conventions used. + :obj:`dpnp.fft.fftn` : The `n`-dimensional FFT. + :obj:`dpnp.fft.ifft` : The one-dimensional inverse FFT. + :obj:`dpnp.fft.ifft2` : The two-dimensional inverse FFT. + :obj:`dpnp.fft.ifftshift` : Undoes :obj:`dpnp.fft.fftshift`, shifts + zero-frequency terms to the center of the array. - if axes is None: - axes_param = list(range(x_desc.ndim)) - else: - axes_param = axes + Notes + ----- + See :obj:`dpnp.fft` for details, definitions and conventions used. - if norm is not None: - pass - else: - x_iter = x - iteration_list = list(range(len(axes_param))) - iteration_list.reverse() # inplace operation - for it in iteration_list: - param_axis = axes_param[it] - try: - param_n = boundaries[param_axis] - except IndexError: - checker_throw_axis_error( - "fft.ifftn", - "is out of bounds", - param_axis, - f"< {len(boundaries)}", - ) + Zero-padding, analogously with :obj:`dpnp.fft.ifft`, is performed by + appending zeros to the input along the specified dimension. Although this + is the common approach, it might lead to surprising results. If another + form of zero padding is desired, it must be performed before + :obj:`dpnp.fft.ifftn` is called. - x_iter_desc = dpnp.get_dpnp_descriptor(x_iter) - x_iter = ifft( - x_iter_desc.get_pyobj(), - n=param_n, - axis=param_axis, - norm=norm, - ) + Examples + -------- + >>> import dpnp as np + >>> a = np.eye(4) + >>> np.fft.ifftn(np.fft.fftn(a, axes=(0,)), axes=(1,)) + array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], # may vary + [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], + [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j], + [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j]]) - return x_iter + """ - return call_origin(numpy.fft.ifftn, x, s, axes, norm) + dpnp.check_supported_arrays_type(a) + return dpnp_fftn(a, forward=False, s=s, axes=axes, norm=norm, out=out) def ifftshift(x, axes=None): diff --git a/dpnp/fft/dpnp_utils_fft.py b/dpnp/fft/dpnp_utils_fft.py index 736e800e559..efc2457fc3b 100644 --- a/dpnp/fft/dpnp_utils_fft.py +++ b/dpnp/fft/dpnp_utils_fft.py @@ -41,7 +41,10 @@ import dpctl.tensor._tensor_impl as ti import dpctl.utils as dpu import numpy -from dpctl.tensor._numpy_helper import normalize_axis_index +from dpctl.tensor._numpy_helper import ( + normalize_axis_index, + normalize_axis_tuple, +) from dpctl.utils import ExecutionPlacementError import dpnp @@ -66,6 +69,63 @@ def _check_norm(norm): ) +def _cook_nd_args(a, s=None, axes=None, c2r=False): + if axes is not None: + # validate axes is a sequence and + # each axis is an integer within the range + normalize_axis_tuple(list(set(axes)), a.ndim, "axes") + + if s is None: + shapeless = True + if axes is None: + s = list(a.shape) + else: + s = numpy.take(a.shape, axes) + else: + shapeless = False + try: + iter(s) + except Exception as e: + raise TypeError( + "`s` must be `None` or a sequence of integers." + ) from e + + for s_i in s: + if s_i is not None and not isinstance(s_i, int): + raise TypeError("`s` must be `None` or a sequence of integers.") + + for s_i in s: + if s_i is not None and s_i < 1 and s_i != -1: + raise ValueError( + f"Invalid number of FFT data points ({s_i}) specified." + ) + + if axes is None: + # TODO: uncomment the checkpoint + # both `s` and `axes` being `None` is currently deprecated + # and will raise an error in future versions of NumPy + # if not shapeless: + # raise ValueError( + # "`axes` should not be `None` if `s` is not `None`." + # ) + axes = list(range(-len(s), 0)) + if len(s) != len(axes): + raise ValueError("Shape and axes have different lengths.") + + # TODO: remove this for loop + # support of `i`` being `None`` is deprecated and will raise + # a TypeError in future versions of NumPy + for i, s_i in enumerate(s): + s = list(s) + s[i] = a.shape[axes[i]] if s_i is None else s_i + + if c2r and shapeless: + s[-1] = (a.shape[axes[-1]] - 1) * 2 + # use the whole input array along axis `i` if `s[i] == -1` + s = [a.shape[_a] if _s == -1 else _s for _s, _a in zip(s, axes)] + return s, axes + + def _commit_descriptor(a, in_place, c2c, a_strides, index, axes): """Commit the FFT descriptor for the input array.""" @@ -205,6 +265,63 @@ def _copy_array(x, complex_input): return x, copy_flag +def _extract_axes_chunk(a, chunk_size=3): + """ + Classify input into a list of list with each list containing + only unique values and its length is at most `chunk_size`. + + Parameters + ---------- + a : list, tuple + Input. + chunk_size : int + Maximum number of elements in each chunk. + + Return + ------ + out : list of lists + List of lists with each list containing only unique values + and its length is at most `chunk_size`. + The final list is returned in reverse order. + + Examples + -------- + >>> axes = (0, 1, 2, 3, 4) + >>> _extract_axes_chunk(axes, chunk_size=3) + [[2, 3, 4], [0, 1]] + + >>> axes = (0, 1, 2, 3, 4, 4) + >>> _extract_axes_chunk(axes, chunk_size=3) + [[4], [2, 3, 4], [0, 1]] + + """ + + chunks = [] + current_chunk = [] + seen_elements = set() + + for elem in a: + if elem in seen_elements: + # If element is already seen, start a new chunk + chunks.append(current_chunk) + current_chunk = [elem] + seen_elements = {elem} + else: + current_chunk.append(elem) + seen_elements.add(elem) + + if len(current_chunk) == chunk_size: + chunks.append(current_chunk) + current_chunk = [] + seen_elements = set() + + # Add the last chunk if it's not empty + if current_chunk: + chunks.append(current_chunk) + + return chunks[::-1] + + def _fft(a, norm, out, forward, in_place, c2c, axes=None): """Calculates FFT of the input array along the specified axes.""" @@ -239,7 +356,11 @@ def _fft(a, norm, out, forward, in_place, c2c, axes=None): def _scale_result(res, a_shape, norm, forward, index): """Scale the result of the FFT according to `norm`.""" - scale = numpy.prod(a_shape[index:], dtype=res.real.dtype) + if res.dtype in [dpnp.float32, dpnp.complex64]: + dtype = dpnp.float32 + else: + dtype = dpnp.float64 + scale = numpy.prod(a_shape[index:], dtype=dtype) norm_factor = 1 if norm == "ortho": norm_factor = numpy.sqrt(scale) @@ -332,6 +453,7 @@ def _validate_out_keyword(a, out, axis, c2r, r2c): def dpnp_fft(a, forward, real, n=None, axis=-1, norm=None, out=None): """Calculates 1-D FFT of the input array along axis""" + _check_norm(norm) a_ndim = a.ndim if a_ndim == 0: raise ValueError("Input array must be at least 1D") @@ -378,3 +500,66 @@ def dpnp_fft(a, forward, real, n=None, axis=-1, norm=None, out=None): c2c=c2c, axes=axis, ) + + +def dpnp_fftn(a, forward, s=None, axes=None, norm=None, out=None): + """Calculates N-D FFT of the input array along axes""" + + _check_norm(norm) + if isinstance(axes, (list, tuple)) and len(axes) == 0: + return a + + if a.ndim == 0: + if axes is not None: + raise IndexError( + "Input array is 0-dimensional while axis is not `None`." + ) + + return a + + s, axes = _cook_nd_args(a, s, axes) + a = _truncate_or_pad(a, s, axes) + # TODO: None, False, False are place holder for future development of + # rfft2, irfft2, rfftn, irfftn + _validate_out_keyword(a, out, None, False, False) + # TODO: True is place holder for future development of + # rfft2, irfft2, rfftn, irfftn + a, in_place = _copy_array(a, True) + + if a.size == 0: + return dpnp.get_result_array(a, out=out, casting="same_kind") + + len_axes = len(axes) + # OneMKL supports up to 3-dimensional FFT on GPU + # repeated axis in OneMKL FFT is not allowed + if len_axes > 3 or len(set(axes)) < len_axes: + axes_chunk = _extract_axes_chunk(axes, chunk_size=3) + for chunk in axes_chunk: + a = _fft( + a, + norm=norm, + out=out, + forward=forward, + in_place=in_place, + # TODO: c2c=True is place holder for future development of + # rfft2, irfft2, rfftn, irfftn + c2c=True, + axes=chunk, + ) + return a + + if a.ndim == len_axes: + # non-batch FFT + axes = None + + return _fft( + a, + norm=norm, + out=out, + forward=forward, + in_place=in_place, + # TODO: c2c=True is place holder for future development of + # rfft2, irfft2, rfftn, irfftn + c2c=True, + axes=axes, + ) diff --git a/tests/skipped_tests.tbl b/tests/skipped_tests.tbl index 836eb3e11a4..b38e4c5b438 100644 --- a/tests/skipped_tests.tbl +++ b/tests/skipped_tests.tbl @@ -9,30 +9,6 @@ tests/test_random.py::TestPermutationsTestShuffle::test_shuffle1[lambda x: (dpnp tests/test_random.py::TestPermutationsTestShuffle::test_shuffle1[lambda x: dpnp.asarray([(i, i) for i in x], [("a", object), ("b", dpnp.int32)])]] tests/test_random.py::TestPermutationsTestShuffle::test_shuffle1[lambda x: dpnp.asarray(x).astype(dpnp.int8)] -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_1_{axes=None, norm=None, s=(1, None), shape=(3, 4)}::test_fft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_7_{axes=(), norm=None, s=None, shape=(3, 4)}::test_fft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_7_{axes=(), norm=None, s=None, shape=(3, 4)}::test_ifft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_9_{axes=None, norm=None, s=(1, 4, None), shape=(2, 3, 4)}::test_fft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_15_{axes=(), norm=None, s=None, shape=(2, 3, 4)}::test_fft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_15_{axes=(), norm=None, s=None, shape=(2, 3, 4)}::test_ifft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_16_{axes=(0, 1, 2), norm='ortho', s=(2, 3), shape=(2, 3, 4)}::test_fft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_16_{axes=(0, 1, 2), norm='ortho', s=(2, 3), shape=(2, 3, 4)}::test_ifft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_18_{axes=None, norm=None, s=None, shape=(0, 5)}::test_fft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_19_{axes=None, norm=None, s=None, shape=(2, 0, 5)}::test_fft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_20_{axes=None, norm=None, s=None, shape=(0, 0, 5)}::test_fft2 - -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_1_{axes=None, norm=None, s=(1, None), shape=(3, 4)}::test_fftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_7_{axes=(), norm=None, s=None, shape=(3, 4)}::test_fftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_7_{axes=(), norm=None, s=None, shape=(3, 4)}::test_ifftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_17_{axes=(), norm='ortho', s=None, shape=(2, 3, 4)}::test_fftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_17_{axes=(), norm='ortho', s=None, shape=(2, 3, 4)}::test_ifftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_18_{axes=(0, 1, 2), norm='ortho', s=(2, 3), shape=(2, 3, 4)}::test_fftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_18_{axes=(0, 1, 2), norm='ortho', s=(2, 3), shape=(2, 3, 4)}::test_ifftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_10_{axes=None, norm=None, s=(1, 4, None), shape=(2, 3, 4)}::test_fftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_21_{axes=None, norm=None, s=None, shape=(0, 5)}::test_fftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_22_{axes=None, norm=None, s=None, shape=(2, 0, 5)}::test_fftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_23_{axes=None, norm=None, s=None, shape=(0, 0, 5)}::test_fftn - tests/third_party/intel/test_zero_copy_test1.py::test_dpnp_interaction_with_dpctl_memory tests/test_umath.py::test_umaths[('divmod', 'ii')] diff --git a/tests/skipped_tests_gpu.tbl b/tests/skipped_tests_gpu.tbl index 8a5f1d7aba6..0d2954db2bd 100644 --- a/tests/skipped_tests_gpu.tbl +++ b/tests/skipped_tests_gpu.tbl @@ -111,30 +111,6 @@ tests/third_party/cupy/core_tests/test_ndarray_reduction.py::TestCubReduction_pa tests/third_party/cupy/core_tests/test_ndarray_reduction.py::TestCubReduction_param_7_{order='F', shape=(10, 20, 30, 40)}::test_cub_max tests/third_party/cupy/core_tests/test_ndarray_reduction.py::TestCubReduction_param_7_{order='F', shape=(10, 20, 30, 40)}::test_cub_min -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_1_{axes=None, norm=None, s=(1, None), shape=(3, 4)}::test_fft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_7_{axes=(), norm=None, s=None, shape=(3, 4)}::test_fft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_7_{axes=(), norm=None, s=None, shape=(3, 4)}::test_ifft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_9_{axes=None, norm=None, s=(1, 4, None), shape=(2, 3, 4)}::test_fft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_15_{axes=(), norm=None, s=None, shape=(2, 3, 4)}::test_fft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_15_{axes=(), norm=None, s=None, shape=(2, 3, 4)}::test_ifft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_16_{axes=(0, 1, 2), norm='ortho', s=(2, 3), shape=(2, 3, 4)}::test_fft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_16_{axes=(0, 1, 2), norm='ortho', s=(2, 3), shape=(2, 3, 4)}::test_ifft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_18_{axes=None, norm=None, s=None, shape=(0, 5)}::test_fft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_19_{axes=None, norm=None, s=None, shape=(2, 0, 5)}::test_fft2 -tests/third_party/cupy/fft_tests/test_fft.py::TestFft2_param_20_{axes=None, norm=None, s=None, shape=(0, 0, 5)}::test_fft2 - -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_1_{axes=None, norm=None, s=(1, None), shape=(3, 4)}::test_fftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_7_{axes=(), norm=None, s=None, shape=(3, 4)}::test_fftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_7_{axes=(), norm=None, s=None, shape=(3, 4)}::test_ifftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_17_{axes=(), norm='ortho', s=None, shape=(2, 3, 4)}::test_fftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_17_{axes=(), norm='ortho', s=None, shape=(2, 3, 4)}::test_ifftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_18_{axes=(0, 1, 2), norm='ortho', s=(2, 3), shape=(2, 3, 4)}::test_fftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_18_{axes=(0, 1, 2), norm='ortho', s=(2, 3), shape=(2, 3, 4)}::test_ifftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_10_{axes=None, norm=None, s=(1, 4, None), shape=(2, 3, 4)}::test_fftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_21_{axes=None, norm=None, s=None, shape=(0, 5)}::test_fftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_22_{axes=None, norm=None, s=None, shape=(2, 0, 5)}::test_fftn -tests/third_party/cupy/fft_tests/test_fft.py::TestFftn_param_23_{axes=None, norm=None, s=None, shape=(0, 0, 5)}::test_fftn - tests/third_party/cupy/indexing_tests/test_generate.py::TestAxisConcatenator::test_AxisConcatenator_init1 tests/third_party/cupy/indexing_tests/test_generate.py::TestAxisConcatenator::test_len tests/third_party/cupy/indexing_tests/test_generate.py::TestC_::test_c_1 diff --git a/tests/test_fft.py b/tests/test_fft.py index 5d142f7e31a..06f5e0222cd 100644 --- a/tests/test_fft.py +++ b/tests/test_fft.py @@ -292,10 +292,12 @@ def test_fft_empty_array(self): a_np = numpy.empty((10, 0, 4), dtype=numpy.complex64) a = dpnp.array(a_np) + # returns empty array, a.size=0 result = dpnp.fft.fft(a, axis=0) expected = numpy.fft.fft(a_np, axis=0) assert_dtype_allclose(result, expected, check_only_type_kind=True) + # calculates FFT, a.size become non-zero because of n=2 result = dpnp.fft.fft(a, axis=1, n=2) expected = numpy.fft.fft(a_np, axis=1, n=2) assert_dtype_allclose(result, expected, check_only_type_kind=True) @@ -343,6 +345,66 @@ def test_fft_validate_out(self): assert_raises(TypeError, dpnp.fft.fft, a, out=out) +class TestFft2: + def setup_method(self): + numpy.random.seed(42) + + @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) + def test_fft2(self, dtype): + x1 = numpy.random.uniform(-10, 10, 24) + a_np = numpy.array(x1).reshape(2, 3, 4) + a = dpnp.asarray(a_np) + + result = dpnp.fft.fft2(a) + expected = numpy.fft.fft2(a_np) + assert_dtype_allclose(result, expected, check_only_type_kind=True) + + iresult = dpnp.fft.ifft2(result) + iexpected = numpy.fft.ifft2(expected) + assert_dtype_allclose(iresult, iexpected, check_only_type_kind=True) + + @pytest.mark.parametrize("dtype", get_complex_dtypes()) + @pytest.mark.parametrize("axes", [(0, 1), (1, 2), (0, 2), (2, 1), (2, 0)]) + @pytest.mark.parametrize("norm", ["forward", "backward", "ortho"]) + @pytest.mark.parametrize("order", ["C", "F"]) + def test_fft2_complex(self, dtype, axes, norm, order): + x1 = numpy.random.uniform(-10, 10, 24) + x2 = numpy.random.uniform(-10, 10, 24) + a_np = numpy.array(x1 + 1j * x2, dtype=dtype).reshape( + 2, 3, 4, order=order + ) + a = dpnp.asarray(a_np) + + result = dpnp.fft.fft2(a, axes=axes, norm=norm) + expected = numpy.fft.fft2(a_np, axes=axes, norm=norm) + assert_dtype_allclose(result, expected, check_only_type_kind=True) + + iresult = dpnp.fft.ifft2(result, axes=axes, norm=norm) + iexpected = numpy.fft.ifft2(expected, axes=axes, norm=norm) + assert_dtype_allclose(iresult, iexpected, check_only_type_kind=True) + + @pytest.mark.parametrize("s", [None, (3, 3), (10, 10), (3, 10)]) + def test_fft2_s(self, s): + x1 = numpy.random.uniform(-10, 10, 48) + x2 = numpy.random.uniform(-10, 10, 48) + a_np = numpy.array(x1 + 1j * x2, dtype=numpy.complex64).reshape(6, 8) + a = dpnp.asarray(a_np) + + result = dpnp.fft.fft2(a, s=s) + expected = numpy.fft.fft2(a_np, s=s) + assert_dtype_allclose(result, expected, check_only_type_kind=True) + + iresult = dpnp.fft.ifft2(result, s=s) + iexpected = numpy.fft.ifft2(expected, s=s) + assert_dtype_allclose(iresult, iexpected, check_only_type_kind=True) + + @pytest.mark.parametrize("xp", [numpy, dpnp]) + def test_fft_error(self, xp): + # 0-D input + a = xp.ones(()) + assert_raises(IndexError, xp.fft.fft2, a) + + class TestFftfreq: @pytest.mark.parametrize("func", ["fftfreq", "rfftfreq"]) @pytest.mark.parametrize("n", [10, 20]) @@ -361,6 +423,134 @@ def test_error(self, func): assert_raises(ValueError, getattr(dpnp.fft, func), 10, (2,)) +class TestFftn: + def setup_method(self): + numpy.random.seed(42) + + @pytest.mark.parametrize("dtype", get_complex_dtypes()) + @pytest.mark.parametrize( + "axes", [None, (0, 1, 2), (-1, -4, -2), (-2, -4, -1, -3)] + ) + @pytest.mark.parametrize("norm", ["forward", "backward", "ortho"]) + @pytest.mark.parametrize("order", ["C", "F"]) + def test_fftn(self, dtype, axes, norm, order): + x1 = numpy.random.uniform(-10, 10, 120) + x2 = numpy.random.uniform(-10, 10, 120) + a_np = numpy.array(x1 + 1j * x2, dtype=dtype).reshape( + 2, 3, 4, 5, order=order + ) + a = dpnp.asarray(a_np) + + result = dpnp.fft.fftn(a, axes=axes, norm=norm) + expected = numpy.fft.fftn(a_np, axes=axes, norm=norm) + assert_dtype_allclose(result, expected, check_only_type_kind=True) + + iresult = dpnp.fft.ifftn(result, axes=axes, norm=norm) + iexpected = numpy.fft.ifftn(expected, axes=axes, norm=norm) + assert_dtype_allclose(iresult, iexpected, check_only_type_kind=True) + + @pytest.mark.parametrize("axes", [(2, 0, 2, 0), (0, 1, 1)]) + def test_fftn_repeated_axes(self, axes): + x1 = numpy.random.uniform(-10, 10, 120) + x2 = numpy.random.uniform(-10, 10, 120) + a_np = numpy.array(x1 + 1j * x2, dtype=numpy.complex64).reshape( + 2, 3, 4, 5 + ) + a = dpnp.asarray(a_np) + + result = dpnp.fft.fftn(a, axes=axes) + # Intel® NumPy ignores repeated axes, handle it one by one + expected = a_np + for ii in axes: + expected = numpy.fft.fft(expected, axis=ii) + assert_dtype_allclose(result, expected, check_only_type_kind=True) + + iresult = dpnp.fft.ifftn(result, axes=axes) + iexpected = expected + for ii in axes: + iexpected = numpy.fft.ifft(iexpected, axis=ii) + assert_dtype_allclose(iresult, iexpected, check_only_type_kind=True) + + @pytest.mark.parametrize("axes", [(2, 3, 3, 2), (0, 0, 3, 3)]) + @pytest.mark.parametrize("s", [(5, 4, 3, 3), (7, 8, 10, 7)]) + def test_fftn_repeated_axes_with_s(self, axes, s): + x1 = numpy.random.uniform(-10, 10, 120) + x2 = numpy.random.uniform(-10, 10, 120) + a_np = numpy.array(x1 + 1j * x2, dtype=numpy.complex64).reshape( + 2, 3, 4, 5 + ) + a = dpnp.asarray(a_np) + + result = dpnp.fft.fftn(a, axes=axes) + # Intel® NumPy ignores repeated axes, handle it one by one + expected = a_np + for ii in axes: + expected = numpy.fft.fft(expected, axis=ii) + assert_dtype_allclose(result, expected, check_only_type_kind=True) + + iresult = dpnp.fft.ifftn(result, axes=axes) + iexpected = expected + for ii in axes: + iexpected = numpy.fft.ifft(iexpected, axis=ii) + assert_dtype_allclose(iresult, iexpected, check_only_type_kind=True) + + def test_fftn_empty_array(self): + a_np = numpy.empty((10, 0, 4), dtype=numpy.complex64) + a = dpnp.array(a_np) + + result = dpnp.fft.fftn(a, axes=(0, 2)) + expected = numpy.fft.fftn(a_np, axes=(0, 2)) + assert_dtype_allclose(result, expected, check_only_type_kind=True) + + result = dpnp.fft.fftn(a, axes=(0, 1, 2), s=(5, 2, 4)) + expected = numpy.fft.fftn(a_np, axes=(0, 1, 2), s=(5, 2, 4)) + assert_dtype_allclose(result, expected, check_only_type_kind=True) + + @pytest.mark.parametrize("dtype", get_all_dtypes()) + def test_fftn_0D(self, dtype): + # 0-D input + a = dpnp.array(3, dtype=dtype) + a_np = a.asnumpy() + result = dpnp.fft.fftn(a) + expected = numpy.fft.fftn(a_np, axes=()) + assert_dtype_allclose(result, expected) + + @pytest.mark.parametrize("dtype", get_all_dtypes()) + def test_fftn_empty_axes(self, dtype): + a = dpnp.ones((2, 3, 4), dtype=dtype) + a_np = a.asnumpy() + + result = dpnp.fft.fftn(a, axes=()) + expected = numpy.fft.fftn(a_np, axes=()) + assert_dtype_allclose(result, expected) + + @pytest.mark.parametrize("xp", [numpy, dpnp]) + def test_fft_error(self, xp): + # s is not int + a = xp.ones((4, 3)) + # dpnp and stock NumPy return TypeError + # Intel® NumPy returns ValueError + assert_raises( + (TypeError, ValueError), xp.fft.fftn, a, s=(5.0,), axes=(0,) + ) + + # s is not sequence + assert_raises(TypeError, xp.fft.fftn, a, s=5, axes=(0,)) + + # Invalid number of FFT point, invalid s value + assert_raises(ValueError, xp.fft.fftn, a, s=(-5,), axes=(0,)) + + # TODO: should be added in future versions + # axes should be given if s is not None + # dpnp and stock NumPy will returns TypeError in future versions + # Intel® NumPy returns TypeError for a different reason: + # when given, axes and shape arguments have to be of the same length + # assert_raises(ValueError, xp.fft.fftn, a, s=(5,)) + + # axes and s should have the same length + assert_raises(ValueError, xp.fft.fftn, a, s=(5, 5), axes=(0,)) + + class TestFftshift: @pytest.mark.parametrize("func", ["fftshift", "ifftshift"]) @pytest.mark.parametrize("axes", [None, 1, (0, 1)]) diff --git a/tests/test_sycl_queue.py b/tests/test_sycl_queue.py index 57e2e6dc4c9..5a6aac06746 100644 --- a/tests/test_sycl_queue.py +++ b/tests/test_sycl_queue.py @@ -1245,6 +1245,25 @@ def test_fft(func, device): assert_sycl_queue_equal(result_queue, expected_queue) +@pytest.mark.parametrize("func", ["fftn", "ifftn"]) +@pytest.mark.parametrize( + "device", + valid_devices, + ids=[device.filter_string for device in valid_devices], +) +def test_fftn(func, device): + data = numpy.arange(24, dtype=numpy.complex128).reshape(2, 3, 4) + dpnp_data = dpnp.array(data, device=device) + + expected = getattr(numpy.fft, func)(data) + result = getattr(dpnp.fft, func)(dpnp_data) + assert_dtype_allclose(result, expected) + + expected_queue = dpnp_data.get_array().sycl_queue + result_queue = result.get_array().sycl_queue + assert_sycl_queue_equal(result_queue, expected_queue) + + @pytest.mark.parametrize("func", ["fftfreq", "rfftfreq"]) @pytest.mark.parametrize( "device", diff --git a/tests/test_usm_type.py b/tests/test_usm_type.py index b81b9f25edd..e5f4b74ef7a 100644 --- a/tests/test_usm_type.py +++ b/tests/test_usm_type.py @@ -951,6 +951,18 @@ def test_fft(func, usm_type): assert result.usm_type == usm_type +@pytest.mark.parametrize("func", ["fftn", "ifftn"]) +@pytest.mark.parametrize("usm_type", list_of_usm_types, ids=list_of_usm_types) +def test_fftn(func, usm_type): + dpnp_data = dp.arange(24, usm_type=usm_type, dtype=dp.complex64).reshape( + 2, 3, 4 + ) + result = getattr(dp.fft, func)(dpnp_data) + + assert dpnp_data.usm_type == usm_type + assert result.usm_type == usm_type + + @pytest.mark.parametrize("func", ["fftfreq", "rfftfreq"]) @pytest.mark.parametrize("usm_type", list_of_usm_types + [None]) def test_fftfreq(func, usm_type): diff --git a/tests/third_party/cupy/fft_tests/test_fft.py b/tests/third_party/cupy/fft_tests/test_fft.py index 4a25f738dcc..59f37b57bd0 100644 --- a/tests/third_party/cupy/fft_tests/test_fft.py +++ b/tests/third_party/cupy/fft_tests/test_fft.py @@ -1,5 +1,4 @@ import functools -import unittest import numpy as np import pytest @@ -116,116 +115,174 @@ def test_ifft(self, xp, dtype): return out +@pytest.mark.usefixtures("skip_forward_backward") @testing.parameterize( - {"shape": (3, 4), "s": None, "axes": None, "norm": None}, - {"shape": (3, 4), "s": (1, None), "axes": None, "norm": None}, - {"shape": (3, 4), "s": (1, 5), "axes": None, "norm": None}, - {"shape": (3, 4), "s": None, "axes": (-2, -1), "norm": None}, - {"shape": (3, 4), "s": None, "axes": (-1, -2), "norm": None}, - {"shape": (3, 4), "s": None, "axes": (0,), "norm": None}, - {"shape": (3, 4), "s": None, "axes": None, "norm": "ortho"}, - {"shape": (3, 4), "s": None, "axes": (), "norm": None}, - {"shape": (2, 3, 4), "s": None, "axes": None, "norm": None}, - {"shape": (2, 3, 4), "s": (1, 4, None), "axes": None, "norm": None}, - {"shape": (2, 3, 4), "s": (1, 4, 10), "axes": None, "norm": None}, - {"shape": (2, 3, 4), "s": None, "axes": (-3, -2, -1), "norm": None}, - {"shape": (2, 3, 4), "s": None, "axes": (-1, -2, -3), "norm": None}, - {"shape": (2, 3, 4), "s": None, "axes": (0, 1), "norm": None}, - {"shape": (2, 3, 4), "s": None, "axes": None, "norm": "ortho"}, - {"shape": (2, 3, 4), "s": None, "axes": (), "norm": None}, - {"shape": (2, 3, 4), "s": (2, 3), "axes": (0, 1, 2), "norm": "ortho"}, - {"shape": (2, 3, 4, 5), "s": None, "axes": None, "norm": None}, - {"shape": (0, 5), "s": None, "axes": None, "norm": None}, - {"shape": (2, 0, 5), "s": None, "axes": None, "norm": None}, - {"shape": (0, 0, 5), "s": None, "axes": None, "norm": None}, - {"shape": (3, 4), "s": (0, 5), "axes": None, "norm": None}, - {"shape": (3, 4), "s": (1, 0), "axes": None, "norm": None}, + *( + testing.product_dict( + [ + {"shape": (3, 4), "s": None, "axes": None}, + {"shape": (3, 4), "s": (1, None), "axes": None}, + {"shape": (3, 4), "s": (1, 5), "axes": None}, + {"shape": (3, 4), "s": None, "axes": (-2, -1)}, + {"shape": (3, 4), "s": None, "axes": (-1, -2)}, + # {"shape": (3, 4), "s": None, "axes": (0,)}, # mkl_fft gh-109 + {"shape": (3, 4), "s": None, "axes": None}, + # {"shape": (3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 + {"shape": (2, 3, 4), "s": None, "axes": None}, + {"shape": (2, 3, 4), "s": (1, 4, None), "axes": None}, + {"shape": (2, 3, 4), "s": (1, 4, 10), "axes": None}, + {"shape": (2, 3, 4), "s": None, "axes": (-3, -2, -1)}, + {"shape": (2, 3, 4), "s": None, "axes": (-1, -2, -3)}, + # {"shape": (2, 3, 4), "s": None, "axes": (0, 1)}, # mkl_fft gh-109 + {"shape": (2, 3, 4), "s": None, "axes": None}, + # {"shape": (2, 3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 + # {"shape": (2, 3, 4), "s": (2, 3), "axes": (0, 1, 2)}, # mkl_fft gh-109 + {"shape": (2, 3, 4, 5), "s": None, "axes": None}, + # {"shape": (0, 5), "s": None, "axes": None}, # mkl_fft gh-110 + # {"shape": (2, 0, 5), "s": None, "axes": None}, # mkl_fft gh-110 + # {"shape": (0, 0, 5), "s": None, "axes": None}, # mkl_fft gh-110 + {"shape": (3, 4), "s": (0, 5), "axes": None}, + {"shape": (3, 4), "s": (1, 0), "axes": None}, + ], + testing.product( + {"norm": [None, "backward", "ortho", "forward", ""]} + ), + ) + ) ) -@pytest.mark.usefixtures("allow_fall_back_on_numpy") -class TestFft2(unittest.TestCase): +class TestFft2: + @testing.for_orders("CF") @testing.for_all_dtypes() @testing.numpy_cupy_allclose( rtol=1e-4, atol=1e-7, accept_error=ValueError, contiguous_check=False, - type_check=False, + type_check=has_support_aspect64(), ) - def test_fft2(self, xp, dtype): + def test_fft2(self, xp, dtype, order): a = testing.shaped_random(self.shape, xp, dtype) + if order == "F": + a = xp.asfortranarray(a) out = xp.fft.fft2(a, s=self.s, axes=self.axes, norm=self.norm) + if self.axes is not None and not self.axes: + assert out is a + return out + + if xp is np and dtype in [np.float16, np.float32, np.complex64]: + out = out.astype(np.complex64) + return out + @testing.for_orders("CF") @testing.for_all_dtypes() @testing.numpy_cupy_allclose( rtol=1e-4, atol=1e-7, accept_error=ValueError, contiguous_check=False, - type_check=False, + type_check=has_support_aspect64(), ) - def test_ifft2(self, xp, dtype): + def test_ifft2(self, xp, dtype, order): a = testing.shaped_random(self.shape, xp, dtype) + if order == "F": + a = xp.asfortranarray(a) out = xp.fft.ifft2(a, s=self.s, axes=self.axes, norm=self.norm) + if self.axes is not None and not self.axes: + assert out is a + return out + + if xp is np and dtype in [np.float16, np.float32, np.complex64]: + out = out.astype(np.complex64) + return out +@pytest.mark.usefixtures("skip_forward_backward") @testing.parameterize( - {"shape": (3, 4), "s": None, "axes": None, "norm": None}, - {"shape": (3, 4), "s": (1, None), "axes": None, "norm": None}, - {"shape": (3, 4), "s": (1, 5), "axes": None, "norm": None}, - {"shape": (3, 4), "s": None, "axes": (-2, -1), "norm": None}, - {"shape": (3, 4), "s": None, "axes": (-1, -2), "norm": None}, - {"shape": (3, 4), "s": None, "axes": [-1, -2], "norm": None}, - {"shape": (3, 4), "s": None, "axes": (0,), "norm": None}, - {"shape": (3, 4), "s": None, "axes": (), "norm": None}, - {"shape": (3, 4), "s": None, "axes": None, "norm": "ortho"}, - {"shape": (2, 3, 4), "s": None, "axes": None, "norm": None}, - {"shape": (2, 3, 4), "s": (1, 4, None), "axes": None, "norm": None}, - {"shape": (2, 3, 4), "s": (1, 4, 10), "axes": None, "norm": None}, - {"shape": (2, 3, 4), "s": None, "axes": (-3, -2, -1), "norm": None}, - {"shape": (2, 3, 4), "s": None, "axes": (-1, -2, -3), "norm": None}, - {"shape": (2, 3, 4), "s": None, "axes": (-1, -3), "norm": None}, - {"shape": (2, 3, 4), "s": None, "axes": (0, 1), "norm": None}, - {"shape": (2, 3, 4), "s": None, "axes": None, "norm": "ortho"}, - {"shape": (2, 3, 4), "s": None, "axes": (), "norm": "ortho"}, - {"shape": (2, 3, 4), "s": (2, 3), "axes": (0, 1, 2), "norm": "ortho"}, - {"shape": (2, 3, 4), "s": (4, 3, 2), "axes": (2, 0, 1), "norm": "ortho"}, - {"shape": (2, 3, 4, 5), "s": None, "axes": None, "norm": None}, - {"shape": (0, 5), "s": None, "axes": None, "norm": None}, - {"shape": (2, 0, 5), "s": None, "axes": None, "norm": None}, - {"shape": (0, 0, 5), "s": None, "axes": None, "norm": None}, + *( + testing.product_dict( + [ + {"shape": (3, 4), "s": None, "axes": None}, + {"shape": (3, 4), "s": (1, None), "axes": None}, + {"shape": (3, 4), "s": (1, 5), "axes": None}, + {"shape": (3, 4), "s": None, "axes": (-2, -1)}, + {"shape": (3, 4), "s": None, "axes": (-1, -2)}, + {"shape": (3, 4), "s": None, "axes": [-1, -2]}, + # {"shape": (3, 4), "s": None, "axes": (0,)}, # mkl_fft gh-109 + # {"shape": (3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 + {"shape": (3, 4), "s": None, "axes": None}, + {"shape": (2, 3, 4), "s": None, "axes": None}, + {"shape": (2, 3, 4), "s": (1, 4, None), "axes": None}, + {"shape": (2, 3, 4), "s": (1, 4, 10), "axes": None}, + {"shape": (2, 3, 4), "s": None, "axes": (-3, -2, -1)}, + {"shape": (2, 3, 4), "s": None, "axes": (-1, -2, -3)}, + # {"shape": (2, 3, 4), "s": None, "axes": (-1, -3)}, # mkl_fft gh-109 + # {"shape": (2, 3, 4), "s": None, "axes": (0, 1)}, # mkl_fft gh-109 + {"shape": (2, 3, 4), "s": None, "axes": None}, + # {"shape": (2, 3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 + # {"shape": (2, 3, 4), "s": (2, 3), "axes": (0, 1, 2)}, # mkl_fft gh-109 + {"shape": (2, 3, 4), "s": (4, 3, 2), "axes": (2, 0, 1)}, + {"shape": (2, 3, 4, 5), "s": None, "axes": None}, + # {"shape": (0, 5), "s": None, "axes": None}, # mkl_fft gh-110 + # {"shape": (2, 0, 5), "s": None, "axes": None}, # mkl_fft gh-110 + # {"shape": (0, 0, 5), "s": None, "axes": None}, # mkl_fft gh-110 + ], + testing.product( + {"norm": [None, "backward", "ortho", "forward", ""]} + ), + ) + ) ) -@pytest.mark.usefixtures("allow_fall_back_on_numpy") -class TestFftn(unittest.TestCase): +class TestFftn: + @testing.for_orders("CF") @testing.for_all_dtypes() @testing.numpy_cupy_allclose( rtol=1e-4, atol=1e-7, accept_error=ValueError, contiguous_check=False, - type_check=False, + type_check=has_support_aspect64(), ) - def test_fftn(self, xp, dtype): + def test_fftn(self, xp, dtype, order): a = testing.shaped_random(self.shape, xp, dtype) + if order == "F": + a = xp.asfortranarray(a) out = xp.fft.fftn(a, s=self.s, axes=self.axes, norm=self.norm) + if self.axes is not None and not self.axes: + assert out is a + return out + + if xp is np and dtype in [np.float16, np.float32, np.complex64]: + out = out.astype(np.complex64) + return out + @testing.for_orders("CF") @testing.for_all_dtypes() @testing.numpy_cupy_allclose( rtol=1e-4, atol=1e-7, accept_error=ValueError, contiguous_check=False, - type_check=False, + type_check=has_support_aspect64(), ) - def test_ifftn(self, xp, dtype): + def test_ifftn(self, xp, dtype, order): a = testing.shaped_random(self.shape, xp, dtype) + if order == "F": + a = xp.asfortranarray(a) out = xp.fft.ifftn(a, s=self.s, axes=self.axes, norm=self.norm) + if self.axes is not None and not self.axes: + assert out is a + return out + + if xp is np and dtype in [np.float16, np.float32, np.complex64]: + out = out.astype(np.complex64) + return out From de6e0e397ff2aec5714594bb345497f9a0d757bf Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Thu, 1 Aug 2024 10:06:12 -0500 Subject: [PATCH 02/11] unmute a few tests --- tests/third_party/cupy/fft_tests/test_fft.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/third_party/cupy/fft_tests/test_fft.py b/tests/third_party/cupy/fft_tests/test_fft.py index 59f37b57bd0..a6a528f817d 100644 --- a/tests/third_party/cupy/fft_tests/test_fft.py +++ b/tests/third_party/cupy/fft_tests/test_fft.py @@ -125,7 +125,7 @@ def test_ifft(self, xp, dtype): {"shape": (3, 4), "s": (1, 5), "axes": None}, {"shape": (3, 4), "s": None, "axes": (-2, -1)}, {"shape": (3, 4), "s": None, "axes": (-1, -2)}, - # {"shape": (3, 4), "s": None, "axes": (0,)}, # mkl_fft gh-109 + {"shape": (3, 4), "s": None, "axes": (0,)}, {"shape": (3, 4), "s": None, "axes": None}, # {"shape": (3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 {"shape": (2, 3, 4), "s": None, "axes": None}, @@ -133,10 +133,10 @@ def test_ifft(self, xp, dtype): {"shape": (2, 3, 4), "s": (1, 4, 10), "axes": None}, {"shape": (2, 3, 4), "s": None, "axes": (-3, -2, -1)}, {"shape": (2, 3, 4), "s": None, "axes": (-1, -2, -3)}, - # {"shape": (2, 3, 4), "s": None, "axes": (0, 1)}, # mkl_fft gh-109 + {"shape": (2, 3, 4), "s": None, "axes": (0, 1)}, {"shape": (2, 3, 4), "s": None, "axes": None}, # {"shape": (2, 3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 - # {"shape": (2, 3, 4), "s": (2, 3), "axes": (0, 1, 2)}, # mkl_fft gh-109 + {"shape": (2, 3, 4), "s": (2, 3), "axes": (0, 1, 2)}, {"shape": (2, 3, 4, 5), "s": None, "axes": None}, # {"shape": (0, 5), "s": None, "axes": None}, # mkl_fft gh-110 # {"shape": (2, 0, 5), "s": None, "axes": None}, # mkl_fft gh-110 @@ -211,7 +211,7 @@ def test_ifft2(self, xp, dtype, order): {"shape": (3, 4), "s": None, "axes": (-2, -1)}, {"shape": (3, 4), "s": None, "axes": (-1, -2)}, {"shape": (3, 4), "s": None, "axes": [-1, -2]}, - # {"shape": (3, 4), "s": None, "axes": (0,)}, # mkl_fft gh-109 + {"shape": (3, 4), "s": None, "axes": (0,)}, # {"shape": (3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 {"shape": (3, 4), "s": None, "axes": None}, {"shape": (2, 3, 4), "s": None, "axes": None}, @@ -219,11 +219,11 @@ def test_ifft2(self, xp, dtype, order): {"shape": (2, 3, 4), "s": (1, 4, 10), "axes": None}, {"shape": (2, 3, 4), "s": None, "axes": (-3, -2, -1)}, {"shape": (2, 3, 4), "s": None, "axes": (-1, -2, -3)}, - # {"shape": (2, 3, 4), "s": None, "axes": (-1, -3)}, # mkl_fft gh-109 - # {"shape": (2, 3, 4), "s": None, "axes": (0, 1)}, # mkl_fft gh-109 + {"shape": (2, 3, 4), "s": None, "axes": (-1, -3)}, + {"shape": (2, 3, 4), "s": None, "axes": (0, 1)}, {"shape": (2, 3, 4), "s": None, "axes": None}, # {"shape": (2, 3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 - # {"shape": (2, 3, 4), "s": (2, 3), "axes": (0, 1, 2)}, # mkl_fft gh-109 + {"shape": (2, 3, 4), "s": (2, 3), "axes": (0, 1, 2)}, {"shape": (2, 3, 4), "s": (4, 3, 2), "axes": (2, 0, 1)}, {"shape": (2, 3, 4, 5), "s": None, "axes": None}, # {"shape": (0, 5), "s": None, "axes": None}, # mkl_fft gh-110 From 74fa1e088657b6b2c228aed0287a0514492a7e0d Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Fri, 2 Aug 2024 12:24:31 -0500 Subject: [PATCH 03/11] Revert "unmute a few tests" This reverts commit de6e0e397ff2aec5714594bb345497f9a0d757bf. --- tests/third_party/cupy/fft_tests/test_fft.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/third_party/cupy/fft_tests/test_fft.py b/tests/third_party/cupy/fft_tests/test_fft.py index a6a528f817d..59f37b57bd0 100644 --- a/tests/third_party/cupy/fft_tests/test_fft.py +++ b/tests/third_party/cupy/fft_tests/test_fft.py @@ -125,7 +125,7 @@ def test_ifft(self, xp, dtype): {"shape": (3, 4), "s": (1, 5), "axes": None}, {"shape": (3, 4), "s": None, "axes": (-2, -1)}, {"shape": (3, 4), "s": None, "axes": (-1, -2)}, - {"shape": (3, 4), "s": None, "axes": (0,)}, + # {"shape": (3, 4), "s": None, "axes": (0,)}, # mkl_fft gh-109 {"shape": (3, 4), "s": None, "axes": None}, # {"shape": (3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 {"shape": (2, 3, 4), "s": None, "axes": None}, @@ -133,10 +133,10 @@ def test_ifft(self, xp, dtype): {"shape": (2, 3, 4), "s": (1, 4, 10), "axes": None}, {"shape": (2, 3, 4), "s": None, "axes": (-3, -2, -1)}, {"shape": (2, 3, 4), "s": None, "axes": (-1, -2, -3)}, - {"shape": (2, 3, 4), "s": None, "axes": (0, 1)}, + # {"shape": (2, 3, 4), "s": None, "axes": (0, 1)}, # mkl_fft gh-109 {"shape": (2, 3, 4), "s": None, "axes": None}, # {"shape": (2, 3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 - {"shape": (2, 3, 4), "s": (2, 3), "axes": (0, 1, 2)}, + # {"shape": (2, 3, 4), "s": (2, 3), "axes": (0, 1, 2)}, # mkl_fft gh-109 {"shape": (2, 3, 4, 5), "s": None, "axes": None}, # {"shape": (0, 5), "s": None, "axes": None}, # mkl_fft gh-110 # {"shape": (2, 0, 5), "s": None, "axes": None}, # mkl_fft gh-110 @@ -211,7 +211,7 @@ def test_ifft2(self, xp, dtype, order): {"shape": (3, 4), "s": None, "axes": (-2, -1)}, {"shape": (3, 4), "s": None, "axes": (-1, -2)}, {"shape": (3, 4), "s": None, "axes": [-1, -2]}, - {"shape": (3, 4), "s": None, "axes": (0,)}, + # {"shape": (3, 4), "s": None, "axes": (0,)}, # mkl_fft gh-109 # {"shape": (3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 {"shape": (3, 4), "s": None, "axes": None}, {"shape": (2, 3, 4), "s": None, "axes": None}, @@ -219,11 +219,11 @@ def test_ifft2(self, xp, dtype, order): {"shape": (2, 3, 4), "s": (1, 4, 10), "axes": None}, {"shape": (2, 3, 4), "s": None, "axes": (-3, -2, -1)}, {"shape": (2, 3, 4), "s": None, "axes": (-1, -2, -3)}, - {"shape": (2, 3, 4), "s": None, "axes": (-1, -3)}, - {"shape": (2, 3, 4), "s": None, "axes": (0, 1)}, + # {"shape": (2, 3, 4), "s": None, "axes": (-1, -3)}, # mkl_fft gh-109 + # {"shape": (2, 3, 4), "s": None, "axes": (0, 1)}, # mkl_fft gh-109 {"shape": (2, 3, 4), "s": None, "axes": None}, # {"shape": (2, 3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 - {"shape": (2, 3, 4), "s": (2, 3), "axes": (0, 1, 2)}, + # {"shape": (2, 3, 4), "s": (2, 3), "axes": (0, 1, 2)}, # mkl_fft gh-109 {"shape": (2, 3, 4), "s": (4, 3, 2), "axes": (2, 0, 1)}, {"shape": (2, 3, 4, 5), "s": None, "axes": None}, # {"shape": (0, 5), "s": None, "axes": None}, # mkl_fft gh-110 From 9f30fb1ad167d3d2be722e834417b7fef590adf4 Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Fri, 2 Aug 2024 15:07:47 -0500 Subject: [PATCH 04/11] update a few tests --- tests/test_fft.py | 27 +++++++++++++++----- tests/third_party/cupy/fft_tests/test_fft.py | 8 +++--- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/test_fft.py b/tests/test_fft.py index 06f5e0222cd..254bbabb9e4 100644 --- a/tests/test_fft.py +++ b/tests/test_fft.py @@ -508,20 +508,35 @@ def test_fftn_empty_array(self): @pytest.mark.parametrize("dtype", get_all_dtypes()) def test_fftn_0D(self, dtype): - # 0-D input - a = dpnp.array(3, dtype=dtype) - a_np = a.asnumpy() + a = dpnp.array(3, dtype=dtype) # 0-D input + + # axes is None + # For 0-D array, stock Numpy and dpnp return input array + # while Intel® NumPy return a complex zero result = dpnp.fft.fftn(a) - expected = numpy.fft.fftn(a_np, axes=()) + expected = a.asnumpy() + assert_dtype_allclose(result, expected) + + # axes=() + # For 0-D array with axes=(), stock Numpy and dpnp return input array + # Intel® NumPy does not support empty axes and raises an Error + result = dpnp.fft.fftn(a, axes=()) + expected = a.asnumpy() assert_dtype_allclose(result, expected) + # axes=(0,) + # For 0-D array with non-empty axes, stock Numpy and dpnp raise + # IndexError, while Intel® NumPy raises ZeroDivisionError + assert_raises(IndexError, dpnp.fft.fftn, a, axes=(0,)) + @pytest.mark.parametrize("dtype", get_all_dtypes()) def test_fftn_empty_axes(self, dtype): a = dpnp.ones((2, 3, 4), dtype=dtype) - a_np = a.asnumpy() + # For axes=(), stock Numpy and dpnp return input array + # Intel® NumPy does not support empty axes and raises an Error result = dpnp.fft.fftn(a, axes=()) - expected = numpy.fft.fftn(a_np, axes=()) + expected = a.asnumpy() assert_dtype_allclose(result, expected) @pytest.mark.parametrize("xp", [numpy, dpnp]) diff --git a/tests/third_party/cupy/fft_tests/test_fft.py b/tests/third_party/cupy/fft_tests/test_fft.py index 59f37b57bd0..f0bc9d13ae9 100644 --- a/tests/third_party/cupy/fft_tests/test_fft.py +++ b/tests/third_party/cupy/fft_tests/test_fft.py @@ -121,7 +121,7 @@ def test_ifft(self, xp, dtype): testing.product_dict( [ {"shape": (3, 4), "s": None, "axes": None}, - {"shape": (3, 4), "s": (1, None), "axes": None}, + # {"shape": (3, 4), "s": (1, None), "axes": None}, # s is not int {"shape": (3, 4), "s": (1, 5), "axes": None}, {"shape": (3, 4), "s": None, "axes": (-2, -1)}, {"shape": (3, 4), "s": None, "axes": (-1, -2)}, @@ -129,7 +129,7 @@ def test_ifft(self, xp, dtype): {"shape": (3, 4), "s": None, "axes": None}, # {"shape": (3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 {"shape": (2, 3, 4), "s": None, "axes": None}, - {"shape": (2, 3, 4), "s": (1, 4, None), "axes": None}, + # {"shape": (2, 3, 4), "s": (1, 4, None), "axes": None}, # s is not int {"shape": (2, 3, 4), "s": (1, 4, 10), "axes": None}, {"shape": (2, 3, 4), "s": None, "axes": (-3, -2, -1)}, {"shape": (2, 3, 4), "s": None, "axes": (-1, -2, -3)}, @@ -206,7 +206,7 @@ def test_ifft2(self, xp, dtype, order): testing.product_dict( [ {"shape": (3, 4), "s": None, "axes": None}, - {"shape": (3, 4), "s": (1, None), "axes": None}, + # {"shape": (3, 4), "s": (1, None), "axes": None}, # s is not int {"shape": (3, 4), "s": (1, 5), "axes": None}, {"shape": (3, 4), "s": None, "axes": (-2, -1)}, {"shape": (3, 4), "s": None, "axes": (-1, -2)}, @@ -215,7 +215,7 @@ def test_ifft2(self, xp, dtype, order): # {"shape": (3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 {"shape": (3, 4), "s": None, "axes": None}, {"shape": (2, 3, 4), "s": None, "axes": None}, - {"shape": (2, 3, 4), "s": (1, 4, None), "axes": None}, + # {"shape": (2, 3, 4), "s": (1, 4, None), "axes": None}, # s is not int {"shape": (2, 3, 4), "s": (1, 4, 10), "axes": None}, {"shape": (2, 3, 4), "s": None, "axes": (-3, -2, -1)}, {"shape": (2, 3, 4), "s": None, "axes": (-1, -2, -3)}, From dfdab4e5472f2995e33ca35ca08662b3b4822d08 Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Fri, 2 Aug 2024 19:17:01 -0500 Subject: [PATCH 05/11] improve coverage + update tests --- dpnp/fft/dpnp_utils_fft.py | 3 +++ tests/test_fft.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/dpnp/fft/dpnp_utils_fft.py b/dpnp/fft/dpnp_utils_fft.py index efc2457fc3b..bfc045e4a3c 100644 --- a/dpnp/fft/dpnp_utils_fft.py +++ b/dpnp/fft/dpnp_utils_fft.py @@ -69,6 +69,7 @@ def _check_norm(norm): ) +# TODO: c2r keyword is place holder for irfftn def _cook_nd_args(a, s=None, axes=None, c2r=False): if axes is not None: # validate axes is a sequence and @@ -316,7 +317,9 @@ def _extract_axes_chunk(a, chunk_size=3): seen_elements = set() # Add the last chunk if it's not empty + print("SAEED", current_chunk) if current_chunk: + print("VAHID") chunks.append(current_chunk) return chunks[::-1] diff --git a/tests/test_fft.py b/tests/test_fft.py index 254bbabb9e4..80d4979ddc8 100644 --- a/tests/test_fft.py +++ b/tests/test_fft.py @@ -449,7 +449,9 @@ def test_fftn(self, dtype, axes, norm, order): iexpected = numpy.fft.ifftn(expected, axes=axes, norm=norm) assert_dtype_allclose(iresult, iexpected, check_only_type_kind=True) - @pytest.mark.parametrize("axes", [(2, 0, 2, 0), (0, 1, 1)]) + @pytest.mark.parametrize( + "axes", [(2, 0, 2, 0), (0, 1, 1), (2, 0, 1, 3, 2, 1)] + ) def test_fftn_repeated_axes(self, axes): x1 = numpy.random.uniform(-10, 10, 120) x2 = numpy.random.uniform(-10, 10, 120) From b86940d821d356379b642658115008d2a5f192b4 Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Tue, 6 Aug 2024 13:07:30 -0500 Subject: [PATCH 06/11] address comments --- dpnp/fft/dpnp_iface_fft.py | 73 ++++++++++++++++++++------------------ dpnp/fft/dpnp_utils_fft.py | 44 +++++++++++++---------- tests/test_fft.py | 13 ++++++- 3 files changed, 77 insertions(+), 53 deletions(-) diff --git a/dpnp/fft/dpnp_iface_fft.py b/dpnp/fft/dpnp_iface_fft.py index 54498df7a09..aa73a4d0112 100644 --- a/dpnp/fft/dpnp_iface_fft.py +++ b/dpnp/fft/dpnp_iface_fft.py @@ -97,7 +97,7 @@ def fft(a, n=None, axis=-1, norm=None, out=None): """ Compute the one-dimensional discrete Fourier Transform. - This function computes the one-dimensional `n`-point discrete Fourier + This function computes the one-dimensional *n*-point discrete Fourier Transform (DFT) with the efficient Fast Fourier Transform (FFT) algorithm. For full documentation refer to :obj:`numpy.fft.fft`. @@ -137,8 +137,8 @@ def fft(a, n=None, axis=-1, norm=None, out=None): :obj:`dpnp.fft` : For definition of the DFT and conventions used. :obj:`dpnp.fft.ifft` : The inverse of :obj:`dpnp.fft.fft`. :obj:`dpnp.fft.fft2` : The two-dimensional FFT. - :obj:`dpnp.fft.fftn` : The `n`-dimensional FFT. - :obj:`dpnp.fft.rfftn` : The `n`-dimensional FFT of real input. + :obj:`dpnp.fft.fftn` : The *N*-dimensional FFT. + :obj:`dpnp.fft.rfftn` : The *N*-dimensional FFT of real input. :obj:`dpnp.fft.fftfreq` : Frequency bins for given FFT parameters. Notes @@ -173,8 +173,8 @@ def fft2(a, s=None, axes=(-2, -1), norm=None, out=None): """ Compute the 2-dimensional discrete Fourier Transform. - This function computes the `N`-dimensional discrete Fourier Transform over - any axes in an `M`-dimensional array by means of the Fast Fourier + This function computes the *N*-dimensional discrete Fourier Transform over + any axes in an *M*-dimensional array by means of the Fast Fourier Transform (FFT). By default, the transform is computed over the last two axes of the input array, i.e., a 2-dimensional FFT. @@ -210,7 +210,8 @@ def fft2(a, s=None, axes=(-2, -1), norm=None, out=None): Default: ``"backward"``. out : {None, dpnp.ndarray or usm_ndarray of complex dtype}, optional If provided, the result will be placed in this array. It should be - of the appropriate shape and dtype. + of the appropriate shape and dtype (and hence is incompatible with + passing in all but the trivial `s`). Default: ``None``. Returns @@ -225,7 +226,7 @@ def fft2(a, s=None, axes=(-2, -1), norm=None, out=None): definitions and conventions used. :obj:`dpnp.fft.ifft2` : The inverse two-dimensional FFT. :obj:`dpnp.fft.fft` : The one-dimensional FFT. - :obj:`dpnp.fft.fftn` : The `n`-dimensional FFT. + :obj:`dpnp.fft.fftn` : The *N*-dimensional FFT. :obj:`dpnp.fft.fftshift` : Shifts zero-frequency terms to the center of the array. For two-dimensional input, swaps first and third quadrants, and second and fourth quadrants. @@ -377,10 +378,10 @@ def fftfreq(n, d=1.0, device=None, usm_type=None, sycl_queue=None): def fftn(a, s=None, axes=None, norm=None, out=None): """ - Compute the `N`-dimensional discrete Fourier Transform. + Compute the *N*-dimensional discrete Fourier Transform. - This function computes the `N`-dimensional discrete Fourier Transform over - any number of axes in an `M`-dimensional array by means of the + This function computes the *N*-dimensional discrete Fourier Transform over + any number of axes in an *M*-dimensional array by means of the Fast Fourier Transform (FFT). For full documentation refer to :obj:`numpy.fft.fftn`. @@ -416,7 +417,8 @@ def fftn(a, s=None, axes=None, norm=None, out=None): Default: ``"backward"``. out : {None, dpnp.ndarray or usm_ndarray of complex dtype}, optional If provided, the result will be placed in this array. It should be - of the appropriate shape and dtype. + of the appropriate shape and dtype (and hence is incompatible with + passing in all but the trivial `s`). Default: ``None``. Returns @@ -430,9 +432,10 @@ def fftn(a, s=None, axes=None, norm=None, out=None): -------- :obj:`dpnp.fft` : Overall view of discrete Fourier transforms, with definitions and conventions used. - :obj:`dpnp.fft.ifftn` : The inverse `n`-dimensional FFT. + :obj:`dpnp.fft.ifftn` : The inverse *N*-dimensional FFT. :obj:`dpnp.fft.fft` : The one-dimensional FFT. - :obj:`dpnp.fft.rfftn` : The `n`-dimensional FFT of real input. + :obj:`dpnp.fft.rfftn` : The *N*-dimensional FFT of real input. + :obj:`dpnp.fft.fft2` : The two-dimensional FFT. :obj:`dpnp.fft.fftshift` : Shifts zero-frequency terms to the center of the array. @@ -551,7 +554,7 @@ def hfft(a, n=None, axis=-1, norm=None, out=None): For `n` output points, ``n//2+1`` input points are necessary. If the input is longer than this, it is cropped. If it is shorter than this, it is padded with zeros. If `n` is not given, it is taken to be - ``2*(m-1)`` where ``m`` is the length of the input along the axis + ``2*(m-1)`` where `m` is the length of the input along the axis specified by `axis`. Default: ``None``. axis : int, optional Axis over which to compute the FFT. If not given, the last axis is @@ -573,7 +576,7 @@ def hfft(a, n=None, axis=-1, norm=None, out=None): The truncated or zero-padded input, transformed along the axis indicated by `axis`, or the last one if `axis` is not specified. The length of the transformed axis is `n`, or, if `n` is not given, - ``2*(m-1)`` where ``m`` is the length of the transformed axis of the + ``2*(m-1)`` where `m` is the length of the transformed axis of the input. To get an odd number of output points, `n` must be specified, for instance as ``2*m - 1`` in the typical case. @@ -633,7 +636,7 @@ def ifft(a, n=None, axis=-1, norm=None, out=None): """ Compute the one-dimensional inverse discrete Fourier Transform. - This function computes the inverse of the one-dimensional `n`-point + This function computes the inverse of the one-dimensional *n*-point discrete Fourier transform computed by :obj:`dpnp.fft.fft`. In other words, ``ifft(fft(a)) == a`` to within numerical accuracy. For a general description of the algorithm and definitions, @@ -689,7 +692,7 @@ def ifft(a, n=None, axis=-1, norm=None, out=None): :obj:`dpnp.fft.fft` : The one-dimensional (forward) FFT, of which :obj:`dpnp.fft.ifft` is the inverse. :obj:`dpnp.fft.ifft2` : The two-dimensional inverse FFT. - :obj:`dpnp.fft.ifftn` : The `n`-dimensional inverse FFT. + :obj:`dpnp.fft.ifftn` : The *N*-dimensional inverse FFT. Notes ----- @@ -718,7 +721,7 @@ def ifft2(a, s=None, axes=(-2, -1), norm=None, out=None): Compute the 2-dimensional inverse discrete Fourier Transform. This function computes the inverse of the 2-dimensional discrete Fourier - Transform over any number of axes in an `M`-dimensional array by means of + Transform over any number of axes in an *M*-dimensional array by means of the Fast Fourier Transform (FFT). In other words, ``ifft2(fft2(a)) == a`` to within numerical accuracy. By default, the inverse transform is computed over the last two axes of the input array. @@ -746,7 +749,7 @@ def ifft2(a, s=None, axes=(-2, -1), norm=None, out=None): If it is ``-1``, the whole input is used (no padding/trimming). If `s` is not given, the shape of the input along the axes specified by `axes` is used. See notes for issue on :obj:`dpnp.fft.ifft` - zero padding. If `s` is not ``None``, `axes` must not be ``None`` + zero padding. If `s` is not ``None``, `axes` must not be ``None`` either. Default: ``None``. axes : {None, sequence of ints}, optional Axes over which to compute the inverse FFT. If not given, the last two @@ -764,7 +767,8 @@ def ifft2(a, s=None, axes=(-2, -1), norm=None, out=None): Default: ``"backward"``. out : {None, dpnp.ndarray or usm_ndarray of complex dtype}, optional If provided, the result will be placed in this array. It should be - of the appropriate shape and dtype. + of the appropriate shape and dtype (and hence is incompatible with + passing in all but the trivial `s`). Default: ``None``. Returns @@ -779,7 +783,7 @@ def ifft2(a, s=None, axes=(-2, -1), norm=None, out=None): definitions and conventions used. :obj:`dpnp.fft.fft2` : The forward two-dimensional FFT, of which :obj:`dpnp.fft.ifft2` is the inverse. - :obj:`dpnp.fft.ifftn` : The inverse of `n`-dimensional FFT. + :obj:`dpnp.fft.ifftn` : The inverse of *N*-dimensional FFT. :obj:`dpnp.fft.fft` : The one-dimensional FFT. :obj:`dpnp.fft.ifft` : The one-dimensional inverse FFT. @@ -813,10 +817,10 @@ def ifft2(a, s=None, axes=(-2, -1), norm=None, out=None): def ifftn(a, s=None, axes=None, norm=None, out=None): """ - Compute the `N`-dimensional inverse discrete Fourier Transform. + Compute the *N*-dimensional inverse discrete Fourier Transform. - This function computes the inverse of the `N`-dimensional discrete - Fourier Transform over any number of axes in an `M`-dimensional array by + This function computes the inverse of the *N*-dimensional discrete + Fourier Transform over any number of axes in an *M*-dimensional array by means of the Fast Fourier Transform (FFT). In other words, ``ifftn(fftn(a)) == a`` to within numerical accuracy. For a description of the definitions and conventions used, see :obj:`dpnp.fft`. @@ -861,7 +865,8 @@ def ifftn(a, s=None, axes=None, norm=None, out=None): Default: ``"backward"``. out : {None, dpnp.ndarray or usm_ndarray of complex dtype}, optional If provided, the result will be placed in this array. It should be - of the appropriate shape and dtype. + of the appropriate shape and dtype (and hence is incompatible with + passing in all but the trivial `s`). Default: ``None``. Returns @@ -875,7 +880,7 @@ def ifftn(a, s=None, axes=None, norm=None, out=None): -------- :obj:`dpnp.fft` : Overall view of discrete Fourier transforms, with definitions and conventions used. - :obj:`dpnp.fft.fftn` : The `n`-dimensional FFT. + :obj:`dpnp.fft.fftn` : The *N*-dimensional FFT. :obj:`dpnp.fft.ifft` : The one-dimensional inverse FFT. :obj:`dpnp.fft.ifft2` : The two-dimensional inverse FFT. :obj:`dpnp.fft.ifftshift` : Undoes :obj:`dpnp.fft.fftshift`, shifts @@ -1038,7 +1043,7 @@ def irfft(a, n=None, axis=-1, norm=None, out=None): """ Computes the inverse of :obj:`dpnp.fft.rfft`. - This function computes the inverse of the one-dimensional `n`-point + This function computes the inverse of the one-dimensional *n*-point discrete Fourier Transform of real input computed by :obj:`dpnp.fft.rfft`. In other words, ``irfft(rfft(a), len(a)) == a`` to within numerical accuracy. (See Notes below for why ``len(a)`` is necessary here.) @@ -1061,7 +1066,7 @@ def irfft(a, n=None, axis=-1, norm=None, out=None): For `n` output points, ``n//2+1`` input points are necessary. If the input is longer than this, it is cropped. If it is shorter than this, it is padded with zeros. If `n` is not given, it is taken to be - ``2*(m-1)`` where ``m`` is the length of the input along the axis + ``2*(m-1)`` where `m` is the length of the input along the axis specified by `axis`. Default: ``None``. axis : int, optional Axis over which to compute the FFT. If not given, the last axis is @@ -1083,7 +1088,7 @@ def irfft(a, n=None, axis=-1, norm=None, out=None): The truncated or zero-padded input, transformed along the axis indicated by `axis`, or the last one if `axis` is not specified. The length of the transformed axis is `n`, or, if `n` is not given, - ``2*(m-1)`` where ``m`` is the length of the transformed axis of the + ``2*(m-1)`` where `m` is the length of the transformed axis of the input. To get an odd number of output points, `n` must be specified. See Also @@ -1094,12 +1099,12 @@ def irfft(a, n=None, axis=-1, norm=None, out=None): :obj:`dpnp.fft.fft` : The one-dimensional FFT of general (complex) input. :obj:`dpnp.fft.irfft2` :The inverse of the two-dimensional FFT of real input. - :obj:`dpnp.fft.irfftn` : The inverse of the `n`-dimensional FFT of + :obj:`dpnp.fft.irfftn` : The inverse of the *N*-dimensional FFT of real input. Notes ----- - Returns the real valued `n`-point inverse discrete Fourier transform + Returns the real valued *n*-point inverse discrete Fourier transform of `a`, where `a` contains the non-negative frequency terms of a Hermitian-symmetric sequence. `n` is the length of the result, not the input. @@ -1234,7 +1239,7 @@ def rfft(a, n=None, axis=-1, norm=None, out=None): """ Compute the one-dimensional discrete Fourier Transform for real input. - This function computes the one-dimensional `n`-point discrete Fourier + This function computes the one-dimensional *n*-point discrete Fourier Transform (DFT) of a real-valued array by means of an efficient algorithm called the Fast Fourier Transform (FFT). @@ -1277,8 +1282,8 @@ def rfft(a, n=None, axis=-1, norm=None, out=None): :obj:`dpnp.fft` : For definition of the DFT and conventions used. :obj:`dpnp.fft.irfft` : The inverse of :obj:`dpnp.fft.rfft`. :obj:`dpnp.fft.fft` : The one-dimensional FFT of general (complex) input. - :obj:`dpnp.fft.fftn` : The `n`-dimensional FFT. - :obj:`dpnp.fft.rfftn` : The `n`-dimensional FFT of real input. + :obj:`dpnp.fft.fftn` : The *N*-dimensional FFT. + :obj:`dpnp.fft.rfftn` : The *N*-dimensional FFT of real input. Notes ----- diff --git a/dpnp/fft/dpnp_utils_fft.py b/dpnp/fft/dpnp_utils_fft.py index bfc045e4a3c..36b2aa99522 100644 --- a/dpnp/fft/dpnp_utils_fft.py +++ b/dpnp/fft/dpnp_utils_fft.py @@ -37,6 +37,8 @@ # pylint: disable=c-extension-no-member # pylint: disable=no-name-in-module +from collections.abc import Sequence + import dpctl import dpctl.tensor._tensor_impl as ti import dpctl.utils as dpu @@ -58,6 +60,7 @@ __all__ = [ "dpnp_fft", + "dpnp_fftn", ] @@ -71,11 +74,6 @@ def _check_norm(norm): # TODO: c2r keyword is place holder for irfftn def _cook_nd_args(a, s=None, axes=None, c2r=False): - if axes is not None: - # validate axes is a sequence and - # each axis is an integer within the range - normalize_axis_tuple(list(set(axes)), a.ndim, "axes") - if s is None: shapeless = True if axes is None: @@ -84,16 +82,6 @@ def _cook_nd_args(a, s=None, axes=None, c2r=False): s = numpy.take(a.shape, axes) else: shapeless = False - try: - iter(s) - except Exception as e: - raise TypeError( - "`s` must be `None` or a sequence of integers." - ) from e - - for s_i in s: - if s_i is not None and not isinstance(s_i, int): - raise TypeError("`s` must be `None` or a sequence of integers.") for s_i in s: if s_i is not None and s_i < 1 and s_i != -1: @@ -113,11 +101,11 @@ def _cook_nd_args(a, s=None, axes=None, c2r=False): if len(s) != len(axes): raise ValueError("Shape and axes have different lengths.") + s = list(s) # TODO: remove this for loop # support of `i`` being `None`` is deprecated and will raise # a TypeError in future versions of NumPy for i, s_i in enumerate(s): - s = list(s) s[i] = a.shape[axes[i]] if s_i is None else s_i if c2r and shapeless: @@ -317,9 +305,7 @@ def _extract_axes_chunk(a, chunk_size=3): seen_elements = set() # Add the last chunk if it's not empty - print("SAEED", current_chunk) if current_chunk: - print("VAHID") chunks.append(current_chunk) return chunks[::-1] @@ -453,6 +439,27 @@ def _validate_out_keyword(a, out, axis, c2r, r2c): raise TypeError("output array should have complex data type.") +def _validate_s_axes(a, s, axes): + if axes is not None: + # validate axes is a sequence and + # each axis is an integer within the range + normalize_axis_tuple(list(set(axes)), a.ndim, "axes") + + if s is not None: + raise_error = False + if isinstance(s, Sequence): + if any(s_i is not None and not isinstance(s_i, int) for s_i in s): + raise_error = True + elif dpnp.is_supported_array_type(s): + if s.ndim != 1 or not dpnp.issubdtype(s, dpnp.integer): + raise_error = True + else: + raise_error = True + + if raise_error: + raise TypeError("`s` must be `None` or a sequence of integers.") + + def dpnp_fft(a, forward, real, n=None, axis=-1, norm=None, out=None): """Calculates 1-D FFT of the input array along axis""" @@ -520,6 +527,7 @@ def dpnp_fftn(a, forward, s=None, axes=None, norm=None, out=None): return a + _validate_s_axes(a, s, axes) s, axes = _cook_nd_args(a, s, axes) a = _truncate_or_pad(a, s, axes) # TODO: None, False, False are place holder for future development of diff --git a/tests/test_fft.py b/tests/test_fft.py index 80d4979ddc8..3abbfc45c55 100644 --- a/tests/test_fft.py +++ b/tests/test_fft.py @@ -551,9 +551,20 @@ def test_fft_error(self, xp): (TypeError, ValueError), xp.fft.fftn, a, s=(5.0,), axes=(0,) ) - # s is not sequence + # s is not a sequence assert_raises(TypeError, xp.fft.fftn, a, s=5, axes=(0,)) + # s is not a sequence of ints + # dpnp and stock NumPy return TypeError + # Intel® NumPy returns ValueError + assert_raises( + (TypeError, ValueError), + xp.fft.fftn, + a, + s=xp.array([5.0]), + axes=(0,), + ) + # Invalid number of FFT point, invalid s value assert_raises(ValueError, xp.fft.fftn, a, s=(-5,), axes=(0,)) From aa384d160621dd43d2e16015058ee3f91480004b Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Wed, 7 Aug 2024 09:46:37 -0500 Subject: [PATCH 07/11] raise Error for deprecated behaviors in NumPy 2.0 --- dpnp/fft/dpnp_utils_fft.py | 21 +++++---------- tests/test_fft.py | 27 ++++++++++---------- tests/third_party/cupy/fft_tests/test_fft.py | 22 ++++++++++------ 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/dpnp/fft/dpnp_utils_fft.py b/dpnp/fft/dpnp_utils_fft.py index 36b2aa99522..abb554f9c12 100644 --- a/dpnp/fft/dpnp_utils_fft.py +++ b/dpnp/fft/dpnp_utils_fft.py @@ -90,24 +90,12 @@ def _cook_nd_args(a, s=None, axes=None, c2r=False): ) if axes is None: - # TODO: uncomment the checkpoint - # both `s` and `axes` being `None` is currently deprecated - # and will raise an error in future versions of NumPy - # if not shapeless: - # raise ValueError( - # "`axes` should not be `None` if `s` is not `None`." - # ) axes = list(range(-len(s), 0)) + if len(s) != len(axes): raise ValueError("Shape and axes have different lengths.") s = list(s) - # TODO: remove this for loop - # support of `i`` being `None`` is deprecated and will raise - # a TypeError in future versions of NumPy - for i, s_i in enumerate(s): - s[i] = a.shape[axes[i]] if s_i is None else s_i - if c2r and shapeless: s[-1] = (a.shape[axes[-1]] - 1) * 2 # use the whole input array along axis `i` if `s[i] == -1` @@ -448,7 +436,7 @@ def _validate_s_axes(a, s, axes): if s is not None: raise_error = False if isinstance(s, Sequence): - if any(s_i is not None and not isinstance(s_i, int) for s_i in s): + if any(not isinstance(s_i, int) for s_i in s): raise_error = True elif dpnp.is_supported_array_type(s): if s.ndim != 1 or not dpnp.issubdtype(s, dpnp.integer): @@ -459,6 +447,11 @@ def _validate_s_axes(a, s, axes): if raise_error: raise TypeError("`s` must be `None` or a sequence of integers.") + if axes is None: + raise ValueError( + "`axes` should not be `None` if `s` is not `None`." + ) + def dpnp_fft(a, forward, real, n=None, axis=-1, norm=None, out=None): """Calculates 1-D FFT of the input array along axis""" diff --git a/tests/test_fft.py b/tests/test_fft.py index 8b7c16bde5a..57b63e85991 100644 --- a/tests/test_fft.py +++ b/tests/test_fft.py @@ -318,15 +318,15 @@ def test_fft_empty_array(self): def test_fft_error(self, xp): # 0-D input a = xp.array(3) - # dpnp and Intel® NumPy return ValueError - # stock NumPy returns IndexError + # dpnp and Intel® NumPy raise ValueError + # stock NumPy raises IndexError assert_raises((ValueError, IndexError), xp.fft.fft, a) # n is not int a = xp.ones((4, 3)) if xp == dpnp: - # dpnp and stock NumPy return TypeError - # Intel® NumPy returns SystemError for Python 3.10 and 3.11 + # dpnp and stock NumPy raise TypeError + # Intel® NumPy raises SystemError for Python 3.10 and 3.11 # and no error for Python 3.9 assert_raises(TypeError, xp.fft.fft, a, n=5.0) @@ -557,8 +557,8 @@ def test_fftn_empty_axes(self, dtype): def test_fft_error(self, xp): # s is not int a = xp.ones((4, 3)) - # dpnp and stock NumPy return TypeError - # Intel® NumPy returns ValueError + # dpnp and stock NumPy raise TypeError + # Intel® NumPy raises ValueError assert_raises( (TypeError, ValueError), xp.fft.fftn, a, s=(5.0,), axes=(0,) ) @@ -567,8 +567,8 @@ def test_fft_error(self, xp): assert_raises(TypeError, xp.fft.fftn, a, s=5, axes=(0,)) # s is not a sequence of ints - # dpnp and stock NumPy return TypeError - # Intel® NumPy returns ValueError + # dpnp and stock NumPy raise TypeError + # Intel® NumPy raises ValueError assert_raises( (TypeError, ValueError), xp.fft.fftn, @@ -580,12 +580,13 @@ def test_fft_error(self, xp): # Invalid number of FFT point, invalid s value assert_raises(ValueError, xp.fft.fftn, a, s=(-5,), axes=(0,)) - # TODO: should be added in future versions # axes should be given if s is not None - # dpnp and stock NumPy will returns TypeError in future versions - # Intel® NumPy returns TypeError for a different reason: + # dpnp raises ValueError + # stock NumPy will raise an Error in future versions + # Intel® NumPy raises TypeError for a different reason: # when given, axes and shape arguments have to be of the same length - # assert_raises(ValueError, xp.fft.fftn, a, s=(5,)) + if xp == dpnp: + assert_raises(ValueError, xp.fft.fftn, a, s=(5,)) # axes and s should have the same length assert_raises(ValueError, xp.fft.fftn, a, s=(5, 5), axes=(0,)) @@ -929,7 +930,7 @@ def test_fft_error(self, xp): # invalid dtype of input array for r2c FFT if xp == dpnp: # stock NumPy-1.26 ignores imaginary part - # Intel® NumPy, dpnp, stock NumPy-2.0 return TypeError + # Intel® NumPy, dpnp, stock NumPy-2.0 raise TypeError assert_raises(TypeError, xp.fft.rfft, a) def test_fft_validate_out(self): diff --git a/tests/third_party/cupy/fft_tests/test_fft.py b/tests/third_party/cupy/fft_tests/test_fft.py index f0bc9d13ae9..a23d042f9dc 100644 --- a/tests/third_party/cupy/fft_tests/test_fft.py +++ b/tests/third_party/cupy/fft_tests/test_fft.py @@ -120,17 +120,20 @@ def test_ifft(self, xp, dtype): *( testing.product_dict( [ + # some of the following cases are modified, since in NumPy 2.0.0 + # `s` must contain only integer `s`, not None values, and + # If `s` is not None, `axes`` must not be None either. {"shape": (3, 4), "s": None, "axes": None}, - # {"shape": (3, 4), "s": (1, None), "axes": None}, # s is not int - {"shape": (3, 4), "s": (1, 5), "axes": None}, + {"shape": (3, 4), "s": (1, 4), "axes": (0, 1)}, + {"shape": (3, 4), "s": (1, 5), "axes": (0, 1)}, {"shape": (3, 4), "s": None, "axes": (-2, -1)}, {"shape": (3, 4), "s": None, "axes": (-1, -2)}, # {"shape": (3, 4), "s": None, "axes": (0,)}, # mkl_fft gh-109 {"shape": (3, 4), "s": None, "axes": None}, # {"shape": (3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 {"shape": (2, 3, 4), "s": None, "axes": None}, - # {"shape": (2, 3, 4), "s": (1, 4, None), "axes": None}, # s is not int - {"shape": (2, 3, 4), "s": (1, 4, 10), "axes": None}, + {"shape": (2, 3, 4), "s": (1, 4, 4), "axes": (0, 1, 2)}, + {"shape": (2, 3, 4), "s": (1, 4, 10), "axes": (0, 1, 2)}, {"shape": (2, 3, 4), "s": None, "axes": (-3, -2, -1)}, {"shape": (2, 3, 4), "s": None, "axes": (-1, -2, -3)}, # {"shape": (2, 3, 4), "s": None, "axes": (0, 1)}, # mkl_fft gh-109 @@ -205,9 +208,12 @@ def test_ifft2(self, xp, dtype, order): *( testing.product_dict( [ + # some of the following cases are modified, since in NumPy 2.0.0 + # `s` must contain only integer `s`, not None values, and + # If `s` is not None, `axes`` must not be None either. {"shape": (3, 4), "s": None, "axes": None}, - # {"shape": (3, 4), "s": (1, None), "axes": None}, # s is not int - {"shape": (3, 4), "s": (1, 5), "axes": None}, + {"shape": (3, 4), "s": (1, 4), "axes": (0, 1)}, + {"shape": (3, 4), "s": (1, 5), "axes": (0, 1)}, {"shape": (3, 4), "s": None, "axes": (-2, -1)}, {"shape": (3, 4), "s": None, "axes": (-1, -2)}, {"shape": (3, 4), "s": None, "axes": [-1, -2]}, @@ -215,8 +221,8 @@ def test_ifft2(self, xp, dtype, order): # {"shape": (3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 {"shape": (3, 4), "s": None, "axes": None}, {"shape": (2, 3, 4), "s": None, "axes": None}, - # {"shape": (2, 3, 4), "s": (1, 4, None), "axes": None}, # s is not int - {"shape": (2, 3, 4), "s": (1, 4, 10), "axes": None}, + {"shape": (2, 3, 4), "s": (1, 4, 4), "axes": (0, 1, 2)}, + {"shape": (2, 3, 4), "s": (1, 4, 10), "axes": (0, 1, 2)}, {"shape": (2, 3, 4), "s": None, "axes": (-3, -2, -1)}, {"shape": (2, 3, 4), "s": None, "axes": (-1, -2, -3)}, # {"shape": (2, 3, 4), "s": None, "axes": (-1, -3)}, # mkl_fft gh-109 From fc769918c305c32ed0fa931c7bd1f55fb66b34c3 Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Wed, 7 Aug 2024 11:10:39 -0500 Subject: [PATCH 08/11] only support sequence for s and axes --- dpnp/fft/dpnp_utils_fft.py | 3 --- tests/test_fft.py | 11 ----------- 2 files changed, 14 deletions(-) diff --git a/dpnp/fft/dpnp_utils_fft.py b/dpnp/fft/dpnp_utils_fft.py index abb554f9c12..c8b3e6353ee 100644 --- a/dpnp/fft/dpnp_utils_fft.py +++ b/dpnp/fft/dpnp_utils_fft.py @@ -438,9 +438,6 @@ def _validate_s_axes(a, s, axes): if isinstance(s, Sequence): if any(not isinstance(s_i, int) for s_i in s): raise_error = True - elif dpnp.is_supported_array_type(s): - if s.ndim != 1 or not dpnp.issubdtype(s, dpnp.integer): - raise_error = True else: raise_error = True diff --git a/tests/test_fft.py b/tests/test_fft.py index 57b63e85991..09feb1ccf3b 100644 --- a/tests/test_fft.py +++ b/tests/test_fft.py @@ -566,17 +566,6 @@ def test_fft_error(self, xp): # s is not a sequence assert_raises(TypeError, xp.fft.fftn, a, s=5, axes=(0,)) - # s is not a sequence of ints - # dpnp and stock NumPy raise TypeError - # Intel® NumPy raises ValueError - assert_raises( - (TypeError, ValueError), - xp.fft.fftn, - a, - s=xp.array([5.0]), - axes=(0,), - ) - # Invalid number of FFT point, invalid s value assert_raises(ValueError, xp.fft.fftn, a, s=(-5,), axes=(0,)) From 2be4c03d2f3b70315d8900b8cde5ee7f512187af Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Thu, 8 Aug 2024 19:03:58 -0500 Subject: [PATCH 09/11] update a test + keep functionin alphabetic order --- dpnp/fft/dpnp_utils_fft.py | 62 +++++++++++++++++++------------------- tests/test_fft.py | 4 +-- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/dpnp/fft/dpnp_utils_fft.py b/dpnp/fft/dpnp_utils_fft.py index c8b3e6353ee..264fc15de7e 100644 --- a/dpnp/fft/dpnp_utils_fft.py +++ b/dpnp/fft/dpnp_utils_fft.py @@ -72,37 +72,6 @@ def _check_norm(norm): ) -# TODO: c2r keyword is place holder for irfftn -def _cook_nd_args(a, s=None, axes=None, c2r=False): - if s is None: - shapeless = True - if axes is None: - s = list(a.shape) - else: - s = numpy.take(a.shape, axes) - else: - shapeless = False - - for s_i in s: - if s_i is not None and s_i < 1 and s_i != -1: - raise ValueError( - f"Invalid number of FFT data points ({s_i}) specified." - ) - - if axes is None: - axes = list(range(-len(s), 0)) - - if len(s) != len(axes): - raise ValueError("Shape and axes have different lengths.") - - s = list(s) - if c2r and shapeless: - s[-1] = (a.shape[axes[-1]] - 1) * 2 - # use the whole input array along axis `i` if `s[i] == -1` - s = [a.shape[_a] if _s == -1 else _s for _s, _a in zip(s, axes)] - return s, axes - - def _commit_descriptor(a, in_place, c2c, a_strides, index, axes): """Commit the FFT descriptor for the input array.""" @@ -197,6 +166,37 @@ def _compute_result(dsc, a, out, forward, c2c, a_strides): return result +# TODO: c2r keyword is place holder for irfftn +def _cook_nd_args(a, s=None, axes=None, c2r=False): + if s is None: + shapeless = True + if axes is None: + s = list(a.shape) + else: + s = numpy.take(a.shape, axes) + else: + shapeless = False + + for s_i in s: + if s_i is not None and s_i < 1 and s_i != -1: + raise ValueError( + f"Invalid number of FFT data points ({s_i}) specified." + ) + + if axes is None: + axes = list(range(-len(s), 0)) + + if len(s) != len(axes): + raise ValueError("Shape and axes have different lengths.") + + s = list(s) + if c2r and shapeless: + s[-1] = (a.shape[axes[-1]] - 1) * 2 + # use the whole input array along axis `i` if `s[i] == -1` + s = [a.shape[_a] if _s == -1 else _s for _s, _a in zip(s, axes)] + return s, axes + + def _copy_array(x, complex_input): """ Creating a C-contiguous copy of input array if input array has a negative diff --git a/tests/test_fft.py b/tests/test_fft.py index 09feb1ccf3b..11e0b30afa9 100644 --- a/tests/test_fft.py +++ b/tests/test_fft.py @@ -361,10 +361,10 @@ class TestFft2: def setup_method(self): numpy.random.seed(42) - @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) + @pytest.mark.parametrize("dtype", get_all_dtypes(no_complex=True)) def test_fft2(self, dtype): x1 = numpy.random.uniform(-10, 10, 24) - a_np = numpy.array(x1).reshape(2, 3, 4) + a_np = numpy.array(x1, dtype=dtype).reshape(2, 3, 4) a = dpnp.asarray(a_np) result = dpnp.fft.fft2(a) From 6d0fd935f64906fd59fc5d37794e872debcb0e11 Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Fri, 9 Aug 2024 09:00:03 -0500 Subject: [PATCH 10/11] update when both s and axes are given --- dpnp/fft/dpnp_utils_fft.py | 121 +++++++++++-------- tests/test_fft.py | 51 ++++++-- tests/third_party/cupy/fft_tests/test_fft.py | 2 +- 3 files changed, 117 insertions(+), 57 deletions(-) diff --git a/dpnp/fft/dpnp_utils_fft.py b/dpnp/fft/dpnp_utils_fft.py index 264fc15de7e..8703e70b419 100644 --- a/dpnp/fft/dpnp_utils_fft.py +++ b/dpnp/fft/dpnp_utils_fft.py @@ -242,61 +242,78 @@ def _copy_array(x, complex_input): return x, copy_flag -def _extract_axes_chunk(a, chunk_size=3): +def _extract_axes_chunk(a, s, chunk_size=3): """ - Classify input into a list of list with each list containing - only unique values and its length is at most `chunk_size`. + Classify the first input into a list of lists with each list containing + only unique values in reverse order and its length is at most `chunk_size`. + The second input is also classified into a list of lists with each list + containing the corresponding values of the first input. Parameters ---------- - a : list, tuple - Input. + a : list or tuple of ints + The first input. + s : list or tuple of ints + The second input. chunk_size : int Maximum number of elements in each chunk. Return ------ - out : list of lists - List of lists with each list containing only unique values - and its length is at most `chunk_size`. - The final list is returned in reverse order. + out : a tuple of two lists + The first element of output is a list of lists with each list + containing only unique values in revere order and its length is + at most `chunk_size`. + The second element of output is a list of lists with each list + containing the corresponding values of the first input. Examples -------- >>> axes = (0, 1, 2, 3, 4) - >>> _extract_axes_chunk(axes, chunk_size=3) - [[2, 3, 4], [0, 1]] + >>> shape = (7, 8, 10, 9, 5) + >>> _extract_axes_chunk(axes, shape, chunk_size=3) + ([[4, 3], [2, 1, 0]], [[5, 9], [10, 8, 7]]) - >>> axes = (0, 1, 2, 3, 4, 4) - >>> _extract_axes_chunk(axes, chunk_size=3) - [[4], [2, 3, 4], [0, 1]] + >>> axes = (1, 0, 3, 2, 4, 4) + >>> shape = (7, 8, 10, 5, 7, 6) + >>> _extract_axes_chunk(axes, shape, chunk_size=3) + ([[4], [4, 2], [3, 0, 1]], [[6], [7, 5], [10, 8, 7]]) """ - chunks = [] - current_chunk = [] + a_chunks = [] + a_current_chunk = [] seen_elements = set() - for elem in a: - if elem in seen_elements: + s_chunks = [] + s_current_chunk = [] + + for a_elem, s_elem in zip(a, s): + if a_elem in seen_elements: # If element is already seen, start a new chunk - chunks.append(current_chunk) - current_chunk = [elem] - seen_elements = {elem} + a_chunks.append(a_current_chunk[::-1]) + s_chunks.append(s_current_chunk[::-1]) + a_current_chunk = [a_elem] + s_current_chunk = [s_elem] + seen_elements = {a_elem} else: - current_chunk.append(elem) - seen_elements.add(elem) - - if len(current_chunk) == chunk_size: - chunks.append(current_chunk) - current_chunk = [] + a_current_chunk.append(a_elem) + s_current_chunk.append(s_elem) + seen_elements.add(a_elem) + + if len(a_current_chunk) == chunk_size: + a_chunks.append(a_current_chunk[::-1]) + s_chunks.append(s_current_chunk[::-1]) + a_current_chunk = [] + s_current_chunk = [] seen_elements = set() # Add the last chunk if it's not empty - if current_chunk: - chunks.append(current_chunk) + if a_current_chunk: + a_chunks.append(a_current_chunk[::-1]) + s_chunks.append(s_current_chunk[::-1]) - return chunks[::-1] + return a_chunks[::-1], s_chunks[::-1] def _fft(a, norm, out, forward, in_place, c2c, axes=None): @@ -392,7 +409,7 @@ def _truncate_or_pad(a, shape, axes): return a -def _validate_out_keyword(a, out, axis, c2r, r2c): +def _validate_out_keyword(a, out, s, axes, c2r, r2c): """Validate out keyword argument.""" if out is not None: dpnp.check_supported_arrays_type(out) @@ -404,16 +421,18 @@ def _validate_out_keyword(a, out, axis, c2r, r2c): "Input and output allocation queues are not compatible" ) - # validate out shape - expected_shape = a.shape + # validate out shape against the final shape, + # intermediate shapes may vary + expected_shape = list(a.shape) + for s_i, axis in zip(s[::-1], axes[::-1]): + expected_shape[axis] = s_i if r2c: - expected_shape = list(a.shape) - expected_shape[axis] = a.shape[axis] // 2 + 1 - expected_shape = tuple(expected_shape) - if out.shape != expected_shape: + expected_shape[axes[-1]] = expected_shape[axes[-1]] // 2 + 1 + + if out.shape != tuple(expected_shape): raise ValueError( "output array has incorrect shape, expected " - f"{expected_shape}, got {out.shape}." + f"{tuple(expected_shape)}, got {out.shape}." ) # validate out data type @@ -477,7 +496,7 @@ def dpnp_fft(a, forward, real, n=None, axis=-1, norm=None, out=None): _check_norm(norm) a = _truncate_or_pad(a, n, axis) - _validate_out_keyword(a, out, axis, c2r, r2c) + _validate_out_keyword(a, out, (n,), (axis,), c2r, r2c) # if input array is copied, in-place FFT can be used a, in_place = _copy_array(a, c2c or c2r) if not in_place and out is not None: @@ -519,36 +538,40 @@ def dpnp_fftn(a, forward, s=None, axes=None, norm=None, out=None): _validate_s_axes(a, s, axes) s, axes = _cook_nd_args(a, s, axes) - a = _truncate_or_pad(a, s, axes) - # TODO: None, False, False are place holder for future development of + # TODO: False and False are place holder for future development of # rfft2, irfft2, rfftn, irfftn - _validate_out_keyword(a, out, None, False, False) + _validate_out_keyword(a, out, s, axes, False, False) # TODO: True is place holder for future development of # rfft2, irfft2, rfftn, irfftn a, in_place = _copy_array(a, True) - if a.size == 0: - return dpnp.get_result_array(a, out=out, casting="same_kind") - len_axes = len(axes) # OneMKL supports up to 3-dimensional FFT on GPU # repeated axis in OneMKL FFT is not allowed if len_axes > 3 or len(set(axes)) < len_axes: - axes_chunk = _extract_axes_chunk(axes, chunk_size=3) - for chunk in axes_chunk: + axes_chunk, shape_chunk = _extract_axes_chunk(axes, s, chunk_size=3) + for s_chunk, a_chunk in zip(shape_chunk, axes_chunk): + a = _truncate_or_pad(a, shape=s_chunk, axes=a_chunk) + if out is not None and out.shape == a.shape: + tmp_out = out + else: + tmp_out = None a = _fft( a, norm=norm, - out=out, + out=tmp_out, forward=forward, in_place=in_place, # TODO: c2c=True is place holder for future development of # rfft2, irfft2, rfftn, irfftn c2c=True, - axes=chunk, + axes=a_chunk, ) return a + a = _truncate_or_pad(a, s, axes) + if a.size == 0: + return dpnp.get_result_array(a, out=out, casting="same_kind") if a.ndim == len_axes: # non-batch FFT axes = None diff --git a/tests/test_fft.py b/tests/test_fft.py index 11e0b30afa9..3ded3b09102 100644 --- a/tests/test_fft.py +++ b/tests/test_fft.py @@ -486,7 +486,7 @@ def test_fftn_repeated_axes(self, axes): assert_dtype_allclose(iresult, iexpected, check_only_type_kind=True) @pytest.mark.parametrize("axes", [(2, 3, 3, 2), (0, 0, 3, 3)]) - @pytest.mark.parametrize("s", [(5, 4, 3, 3), (7, 8, 10, 7)]) + @pytest.mark.parametrize("s", [(5, 4, 3, 3), (7, 8, 10, 9)]) def test_fftn_repeated_axes_with_s(self, axes, s): x1 = numpy.random.uniform(-10, 10, 120) x2 = numpy.random.uniform(-10, 10, 120) @@ -495,19 +495,56 @@ def test_fftn_repeated_axes_with_s(self, axes, s): ) a = dpnp.asarray(a_np) - result = dpnp.fft.fftn(a, axes=axes) + result = dpnp.fft.fftn(a, s=s, axes=axes) # Intel® NumPy ignores repeated axes, handle it one by one expected = a_np - for ii in axes: - expected = numpy.fft.fft(expected, axis=ii) + for jj, ii in zip(s[::-1], axes[::-1]): + expected = numpy.fft.fft(expected, n=jj, axis=ii) assert_dtype_allclose(result, expected, check_only_type_kind=True) - iresult = dpnp.fft.ifftn(result, axes=axes) + iresult = dpnp.fft.ifftn(result, s=s, axes=axes) iexpected = expected - for ii in axes: - iexpected = numpy.fft.ifft(iexpected, axis=ii) + for jj, ii in zip(s[::-1], axes[::-1]): + iexpected = numpy.fft.ifft(iexpected, n=jj, axis=ii) assert_dtype_allclose(iresult, iexpected, check_only_type_kind=True) + @pytest.mark.parametrize("axes", [(0, 1, 2, 3), (1, 2, 1, 2), (2, 2, 2, 3)]) + @pytest.mark.parametrize("s", [(2, 3, 4, 5), (5, 4, 7, 8), (2, 5, 1, 2)]) + def test_fftn_out(self, axes, s): + x1 = numpy.random.uniform(-10, 10, 120) + x2 = numpy.random.uniform(-10, 10, 120) + a_np = numpy.array(x1 + 1j * x2, dtype=numpy.complex64).reshape( + 2, 3, 4, 5 + ) + a = dpnp.asarray(a_np) + + out_shape = list(a.shape) + for s_i, axis in zip(s[::-1], axes[::-1]): + out_shape[axis] = s_i + result = dpnp.empty(out_shape, dtype=a.dtype) + dpnp.fft.fftn(a, out=result, s=s, axes=axes) + # Intel® NumPy ignores repeated axes, handle it one by one + expected = a_np + for jj, ii in zip(s[::-1], axes[::-1]): + expected = numpy.fft.fft(expected, n=jj, axis=ii) + assert_dtype_allclose(result, expected, check_only_type_kind=True) + + iresult = dpnp.empty(out_shape, dtype=a.dtype) + dpnp.fft.ifftn(result, out=iresult, s=s, axes=axes) + iexpected = expected + for jj, ii in zip(s[::-1], axes[::-1]): + iexpected = numpy.fft.ifft(iexpected, n=jj, axis=ii) + assert_dtype_allclose(iresult, iexpected, check_only_type_kind=True) + + def test_negative_s(self): + # stock NumPy 2.0, if s is -1, the whole input is used (no padding/trimming). + a_np = numpy.empty((3, 4, 5), dtype=numpy.complex64) + a = dpnp.array(a_np) + + result = dpnp.fft.fftn(a, s=(-1, -1), axes=(0, 2)) + expected = numpy.fft.fftn(a_np, s=(3, 5), axes=(0, 2)) + assert_dtype_allclose(result, expected, check_only_type_kind=True) + def test_fftn_empty_array(self): a_np = numpy.empty((10, 0, 4), dtype=numpy.complex64) a = dpnp.array(a_np) diff --git a/tests/third_party/cupy/fft_tests/test_fft.py b/tests/third_party/cupy/fft_tests/test_fft.py index a23d042f9dc..16b3d802601 100644 --- a/tests/third_party/cupy/fft_tests/test_fft.py +++ b/tests/third_party/cupy/fft_tests/test_fft.py @@ -216,7 +216,7 @@ def test_ifft2(self, xp, dtype, order): {"shape": (3, 4), "s": (1, 5), "axes": (0, 1)}, {"shape": (3, 4), "s": None, "axes": (-2, -1)}, {"shape": (3, 4), "s": None, "axes": (-1, -2)}, - {"shape": (3, 4), "s": None, "axes": [-1, -2]}, + {"shape": (3, 4), "s": None, "axes": (-1, -2)}, # {"shape": (3, 4), "s": None, "axes": (0,)}, # mkl_fft gh-109 # {"shape": (3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 {"shape": (3, 4), "s": None, "axes": None}, From ce49a429bb489f6c1f8035bdf2f80ba25ba5be0d Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Mon, 12 Aug 2024 11:05:34 -0500 Subject: [PATCH 11/11] revert incorrect change for cupy test --- tests/third_party/cupy/fft_tests/test_fft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/third_party/cupy/fft_tests/test_fft.py b/tests/third_party/cupy/fft_tests/test_fft.py index 16b3d802601..a23d042f9dc 100644 --- a/tests/third_party/cupy/fft_tests/test_fft.py +++ b/tests/third_party/cupy/fft_tests/test_fft.py @@ -216,7 +216,7 @@ def test_ifft2(self, xp, dtype, order): {"shape": (3, 4), "s": (1, 5), "axes": (0, 1)}, {"shape": (3, 4), "s": None, "axes": (-2, -1)}, {"shape": (3, 4), "s": None, "axes": (-1, -2)}, - {"shape": (3, 4), "s": None, "axes": (-1, -2)}, + {"shape": (3, 4), "s": None, "axes": [-1, -2]}, # {"shape": (3, 4), "s": None, "axes": (0,)}, # mkl_fft gh-109 # {"shape": (3, 4), "s": None, "axes": ()}, # mkl_fft gh-108 {"shape": (3, 4), "s": None, "axes": None},