From 4b3b3247366830a3914de6dc81bbfb2197904ae2 Mon Sep 17 00:00:00 2001 From: vtavana <120411540+vtavana@users.noreply.github.com> Date: Mon, 12 Aug 2024 12:44:17 -0500 Subject: [PATCH] Implementation of `dpnp.fft.fft2`, `dpnp.fft.ifft2`, `dpnp.fft.fftn`, `dpnp.fft.ifftn` (#1961) * implement fft2, ifft2, fftn, ifftn * unmute a few tests * Revert "unmute a few tests" This reverts commit de6e0e397ff2aec5714594bb345497f9a0d757bf. * update a few tests * improve coverage + update tests * address comments * raise Error for deprecated behaviors in NumPy 2.0 * only support sequence for s and axes * update a test + keep functionin alphabetic order * update when both s and axes are given * revert incorrect change for cupy test --- .github/workflows/conda-package.yml | 1 + dpnp/fft/dpnp_iface_fft.py | 518 ++++++++++++++----- dpnp/fft/dpnp_utils_fft.py | 231 ++++++++- tests/skipped_tests.tbl | 24 - tests/skipped_tests_gpu.tbl | 24 - tests/test_fft.py | 255 ++++++++- tests/test_sycl_queue.py | 19 + tests/test_usm_type.py | 12 + tests/third_party/cupy/fft_tests/test_fft.py | 183 ++++--- 9 files changed, 1006 insertions(+), 261 deletions(-) diff --git a/.github/workflows/conda-package.yml b/.github/workflows/conda-package.yml index 38505c5f5e1..0261a7acea9 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 43b7b107cda..2c06bede5d5 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 @@ -133,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 @@ -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,101 @@ 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 (and hence is incompatible with + passing in all but the trivial `s`). + 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`. - return call_origin(numpy.fft.fft2, x, s, axes, norm) + 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 + + """ + + 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): @@ -303,59 +379,104 @@ 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 (and hence is incompatible with + passing in all but the trivial `s`). + 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.fft2` : The two-dimensional FFT. + :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): @@ -436,7 +557,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 @@ -458,7 +579,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. @@ -518,6 +639,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 @@ -556,7 +695,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 ----- @@ -580,95 +719,200 @@ 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 (and hence is incompatible with + passing in all but the trivial `s`). + 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. + + 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]]) - return call_origin(numpy.fft.ifft2, 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 ifftn(x, s=None, axes=None, norm=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. - Multi-dimensional arrays computed as batch of 1-D arrays. + 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 (and hence is incompatible with + passing in all but the trivial `s`). + 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): @@ -802,7 +1046,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.) @@ -825,7 +1069,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 @@ -847,7 +1091,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 @@ -858,12 +1102,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. @@ -998,7 +1242,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). @@ -1041,8 +1285,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 1be56c3974d..d1e015068c0 100644 --- a/dpnp/fft/dpnp_utils_fft.py +++ b/dpnp/fft/dpnp_utils_fft.py @@ -36,11 +36,16 @@ # pylint: disable=protected-access # 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 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 @@ -54,6 +59,7 @@ __all__ = [ "dpnp_fft", + "dpnp_fftn", ] @@ -159,6 +165,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 @@ -204,6 +241,80 @@ def _copy_array(x, complex_input): return x, copy_flag +def _extract_axes_chunk(a, s, chunk_size=3): + """ + 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 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 : 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) + >>> 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 = (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]]) + + """ + + a_chunks = [] + a_current_chunk = [] + seen_elements = set() + + 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 + 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: + 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 a_current_chunk: + a_chunks.append(a_current_chunk[::-1]) + s_chunks.append(s_current_chunk[::-1]) + + return a_chunks[::-1], s_chunks[::-1] + + def _fft(a, norm, out, forward, in_place, c2c, axes=None): """Calculates FFT of the input array along the specified axes.""" @@ -238,7 +349,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) @@ -293,7 +408,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) @@ -305,16 +420,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 @@ -328,9 +445,33 @@ 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(not isinstance(s_i, int) for s_i in s): + raise_error = True + else: + raise_error = True + + 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""" + _check_norm(norm) a_ndim = a.ndim if a_ndim == 0: raise ValueError("Input array must be at least 1D") @@ -354,7 +495,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: @@ -377,3 +518,71 @@ 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 + + _validate_s_axes(a, s, axes) + s, axes = _cook_nd_args(a, s, axes) + # TODO: False and False are place holder for future development of + # rfft2, irfft2, rfftn, irfftn + _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) + + 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, 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=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=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 + + 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 ec7b54638df..a515d5876b2 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 408374124cf..8b9278e8dbd 100644 --- a/tests/skipped_tests_gpu.tbl +++ b/tests/skipped_tests_gpu.tbl @@ -110,30 +110,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 b952baaf367..3ded3b09102 100644 --- a/tests/test_fft.py +++ b/tests/test_fft.py @@ -304,10 +304,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) @@ -316,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) @@ -355,6 +357,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_complex=True)) + def test_fft2(self, dtype): + x1 = numpy.random.uniform(-10, 10, 24) + a_np = numpy.array(x1, dtype=dtype).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]) @@ -373,6 +435,189 @@ 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), (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) + 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, 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) + 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, 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.fft.ifftn(result, 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) + + @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) + + 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): + 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 = 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) + + # 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 = a.asnumpy() + 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 raise TypeError + # Intel® NumPy raises ValueError + assert_raises( + (TypeError, ValueError), xp.fft.fftn, a, s=(5.0,), axes=(0,) + ) + + # s is not a 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,)) + + # axes should be given if s is not None + # 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 + 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,)) + + class TestFftshift: @pytest.mark.parametrize("func", ["fftshift", "ifftshift"]) @pytest.mark.parametrize("axes", [None, 1, (0, 1)]) @@ -711,7 +956,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/test_sycl_queue.py b/tests/test_sycl_queue.py index 9c97c226c31..902e5c9ba8d 100644 --- a/tests/test_sycl_queue.py +++ b/tests/test_sycl_queue.py @@ -1288,6 +1288,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 d440c4d5573..ca9dda5d198 100644 --- a/tests/test_usm_type.py +++ b/tests/test_usm_type.py @@ -981,6 +981,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..a23d042f9dc 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,180 @@ 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( + [ + # 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, 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, 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 + {"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( + [ + # 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, 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]}, + # {"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, 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 + # {"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