This commit is contained in:
2024-11-29 18:15:30 +00:00
parent 40aade2d8e
commit bc9415586e
5298 changed files with 1938676 additions and 80 deletions

View File

@ -0,0 +1,114 @@
"""
==============================================
Discrete Fourier transforms (:mod:`scipy.fft`)
==============================================
.. currentmodule:: scipy.fft
Fast Fourier Transforms (FFTs)
==============================
.. autosummary::
:toctree: generated/
fft - Fast (discrete) Fourier Transform (FFT)
ifft - Inverse FFT
fft2 - 2-D FFT
ifft2 - 2-D inverse FFT
fftn - N-D FFT
ifftn - N-D inverse FFT
rfft - FFT of strictly real-valued sequence
irfft - Inverse of rfft
rfft2 - 2-D FFT of real sequence
irfft2 - Inverse of rfft2
rfftn - N-D FFT of real sequence
irfftn - Inverse of rfftn
hfft - FFT of a Hermitian sequence (real spectrum)
ihfft - Inverse of hfft
hfft2 - 2-D FFT of a Hermitian sequence
ihfft2 - Inverse of hfft2
hfftn - N-D FFT of a Hermitian sequence
ihfftn - Inverse of hfftn
Discrete Sin and Cosine Transforms (DST and DCT)
================================================
.. autosummary::
:toctree: generated/
dct - Discrete cosine transform
idct - Inverse discrete cosine transform
dctn - N-D Discrete cosine transform
idctn - N-D Inverse discrete cosine transform
dst - Discrete sine transform
idst - Inverse discrete sine transform
dstn - N-D Discrete sine transform
idstn - N-D Inverse discrete sine transform
Fast Hankel Transforms
======================
.. autosummary::
:toctree: generated/
fht - Fast Hankel transform
ifht - Inverse of fht
Helper functions
================
.. autosummary::
:toctree: generated/
fftshift - Shift the zero-frequency component to the center of the spectrum
ifftshift - The inverse of `fftshift`
fftfreq - Return the Discrete Fourier Transform sample frequencies
rfftfreq - DFT sample frequencies (for usage with rfft, irfft)
fhtoffset - Compute an optimal offset for the Fast Hankel Transform
next_fast_len - Find the optimal length to zero-pad an FFT for speed
prev_fast_len - Find the maximum slice length that results in a fast FFT
set_workers - Context manager to set default number of workers
get_workers - Get the current default number of workers
Backend control
===============
.. autosummary::
:toctree: generated/
set_backend - Context manager to set the backend within a fixed scope
skip_backend - Context manager to skip a backend within a fixed scope
set_global_backend - Sets the global fft backend
register_backend - Register a backend for permanent use
"""
from ._basic import (
fft, ifft, fft2, ifft2, fftn, ifftn,
rfft, irfft, rfft2, irfft2, rfftn, irfftn,
hfft, ihfft, hfft2, ihfft2, hfftn, ihfftn)
from ._realtransforms import dct, idct, dst, idst, dctn, idctn, dstn, idstn
from ._fftlog import fht, ifht, fhtoffset
from ._helper import (
next_fast_len, prev_fast_len, fftfreq,
rfftfreq, fftshift, ifftshift)
from ._backend import (set_backend, skip_backend, set_global_backend,
register_backend)
from ._pocketfft.helper import set_workers, get_workers
__all__ = [
'fft', 'ifft', 'fft2', 'ifft2', 'fftn', 'ifftn',
'rfft', 'irfft', 'rfft2', 'irfft2', 'rfftn', 'irfftn',
'hfft', 'ihfft', 'hfft2', 'ihfft2', 'hfftn', 'ihfftn',
'fftfreq', 'rfftfreq', 'fftshift', 'ifftshift',
'next_fast_len', 'prev_fast_len',
'dct', 'idct', 'dst', 'idst', 'dctn', 'idctn', 'dstn', 'idstn',
'fht', 'ifht',
'fhtoffset',
'set_backend', 'skip_backend', 'set_global_backend', 'register_backend',
'get_workers', 'set_workers']
from scipy._lib._testutils import PytestTester
test = PytestTester(__name__)
del PytestTester

View File

@ -0,0 +1,196 @@
import scipy._lib.uarray as ua
from . import _basic_backend
from . import _realtransforms_backend
from . import _fftlog_backend
class _ScipyBackend:
"""The default backend for fft calculations
Notes
-----
We use the domain ``numpy.scipy`` rather than ``scipy`` because ``uarray``
treats the domain as a hierarchy. This means the user can install a single
backend for ``numpy`` and have it implement ``numpy.scipy.fft`` as well.
"""
__ua_domain__ = "numpy.scipy.fft"
@staticmethod
def __ua_function__(method, args, kwargs):
fn = getattr(_basic_backend, method.__name__, None)
if fn is None:
fn = getattr(_realtransforms_backend, method.__name__, None)
if fn is None:
fn = getattr(_fftlog_backend, method.__name__, None)
if fn is None:
return NotImplemented
return fn(*args, **kwargs)
_named_backends = {
'scipy': _ScipyBackend,
}
def _backend_from_arg(backend):
"""Maps strings to known backends and validates the backend"""
if isinstance(backend, str):
try:
backend = _named_backends[backend]
except KeyError as e:
raise ValueError(f'Unknown backend {backend}') from e
if backend.__ua_domain__ != 'numpy.scipy.fft':
raise ValueError('Backend does not implement "numpy.scipy.fft"')
return backend
def set_global_backend(backend, coerce=False, only=False, try_last=False):
"""Sets the global fft backend
This utility method replaces the default backend for permanent use. It
will be tried in the list of backends automatically, unless the
``only`` flag is set on a backend. This will be the first tried
backend outside the :obj:`set_backend` context manager.
Parameters
----------
backend : {object, 'scipy'}
The backend to use.
Can either be a ``str`` containing the name of a known backend
{'scipy'} or an object that implements the uarray protocol.
coerce : bool
Whether to coerce input types when trying this backend.
only : bool
If ``True``, no more backends will be tried if this fails.
Implied by ``coerce=True``.
try_last : bool
If ``True``, the global backend is tried after registered backends.
Raises
------
ValueError: If the backend does not implement ``numpy.scipy.fft``.
Notes
-----
This will overwrite the previously set global backend, which, by default, is
the SciPy implementation.
Examples
--------
We can set the global fft backend:
>>> from scipy.fft import fft, set_global_backend
>>> set_global_backend("scipy") # Sets global backend (default is "scipy").
>>> fft([1]) # Calls the global backend
array([1.+0.j])
"""
backend = _backend_from_arg(backend)
ua.set_global_backend(backend, coerce=coerce, only=only, try_last=try_last)
def register_backend(backend):
"""
Register a backend for permanent use.
Registered backends have the lowest priority and will be tried after the
global backend.
Parameters
----------
backend : {object, 'scipy'}
The backend to use.
Can either be a ``str`` containing the name of a known backend
{'scipy'} or an object that implements the uarray protocol.
Raises
------
ValueError: If the backend does not implement ``numpy.scipy.fft``.
Examples
--------
We can register a new fft backend:
>>> from scipy.fft import fft, register_backend, set_global_backend
>>> class NoopBackend: # Define an invalid Backend
... __ua_domain__ = "numpy.scipy.fft"
... def __ua_function__(self, func, args, kwargs):
... return NotImplemented
>>> set_global_backend(NoopBackend()) # Set the invalid backend as global
>>> register_backend("scipy") # Register a new backend
# The registered backend is called because
# the global backend returns `NotImplemented`
>>> fft([1])
array([1.+0.j])
>>> set_global_backend("scipy") # Restore global backend to default
"""
backend = _backend_from_arg(backend)
ua.register_backend(backend)
def set_backend(backend, coerce=False, only=False):
"""Context manager to set the backend within a fixed scope.
Upon entering the ``with`` statement, the given backend will be added to
the list of available backends with the highest priority. Upon exit, the
backend is reset to the state before entering the scope.
Parameters
----------
backend : {object, 'scipy'}
The backend to use.
Can either be a ``str`` containing the name of a known backend
{'scipy'} or an object that implements the uarray protocol.
coerce : bool, optional
Whether to allow expensive conversions for the ``x`` parameter. e.g.,
copying a NumPy array to the GPU for a CuPy backend. Implies ``only``.
only : bool, optional
If only is ``True`` and this backend returns ``NotImplemented``, then a
BackendNotImplemented error will be raised immediately. Ignoring any
lower priority backends.
Examples
--------
>>> import scipy.fft as fft
>>> with fft.set_backend('scipy', only=True):
... fft.fft([1]) # Always calls the scipy implementation
array([1.+0.j])
"""
backend = _backend_from_arg(backend)
return ua.set_backend(backend, coerce=coerce, only=only)
def skip_backend(backend):
"""Context manager to skip a backend within a fixed scope.
Within the context of a ``with`` statement, the given backend will not be
called. This covers backends registered both locally and globally. Upon
exit, the backend will again be considered.
Parameters
----------
backend : {object, 'scipy'}
The backend to skip.
Can either be a ``str`` containing the name of a known backend
{'scipy'} or an object that implements the uarray protocol.
Examples
--------
>>> import scipy.fft as fft
>>> fft.fft([1]) # Calls default SciPy backend
array([1.+0.j])
>>> with fft.skip_backend('scipy'): # We explicitly skip the SciPy backend
... fft.fft([1]) # leaving no implementation available
Traceback (most recent call last):
...
BackendNotImplementedError: No selected backends had an implementation ...
"""
backend = _backend_from_arg(backend)
return ua.skip_backend(backend)
set_global_backend('scipy', try_last=True)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,180 @@
from scipy._lib._array_api import (
array_namespace, is_numpy, xp_unsupported_param_msg, is_complex
)
from . import _pocketfft
import numpy as np
def _validate_fft_args(workers, plan, norm):
if workers is not None:
raise ValueError(xp_unsupported_param_msg("workers"))
if plan is not None:
raise ValueError(xp_unsupported_param_msg("plan"))
if norm is None:
norm = 'backward'
return norm
# pocketfft is used whenever SCIPY_ARRAY_API is not set,
# or x is a NumPy array or array-like.
# When SCIPY_ARRAY_API is set, we try to use xp.fft for CuPy arrays,
# PyTorch arrays and other array API standard supporting objects.
# If xp.fft does not exist, we attempt to convert to np and back to use pocketfft.
def _execute_1D(func_str, pocketfft_func, x, n, axis, norm, overwrite_x, workers, plan):
xp = array_namespace(x)
if is_numpy(xp):
x = np.asarray(x)
return pocketfft_func(x, n=n, axis=axis, norm=norm,
overwrite_x=overwrite_x, workers=workers, plan=plan)
norm = _validate_fft_args(workers, plan, norm)
if hasattr(xp, 'fft'):
xp_func = getattr(xp.fft, func_str)
return xp_func(x, n=n, axis=axis, norm=norm)
x = np.asarray(x)
y = pocketfft_func(x, n=n, axis=axis, norm=norm)
return xp.asarray(y)
def _execute_nD(func_str, pocketfft_func, x, s, axes, norm, overwrite_x, workers, plan):
xp = array_namespace(x)
if is_numpy(xp):
x = np.asarray(x)
return pocketfft_func(x, s=s, axes=axes, norm=norm,
overwrite_x=overwrite_x, workers=workers, plan=plan)
norm = _validate_fft_args(workers, plan, norm)
if hasattr(xp, 'fft'):
xp_func = getattr(xp.fft, func_str)
return xp_func(x, s=s, axes=axes, norm=norm)
x = np.asarray(x)
y = pocketfft_func(x, s=s, axes=axes, norm=norm)
return xp.asarray(y)
def fft(x, n=None, axis=-1, norm=None,
overwrite_x=False, workers=None, *, plan=None):
return _execute_1D('fft', _pocketfft.fft, x, n=n, axis=axis, norm=norm,
overwrite_x=overwrite_x, workers=workers, plan=plan)
def ifft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *,
plan=None):
return _execute_1D('ifft', _pocketfft.ifft, x, n=n, axis=axis, norm=norm,
overwrite_x=overwrite_x, workers=workers, plan=plan)
def rfft(x, n=None, axis=-1, norm=None,
overwrite_x=False, workers=None, *, plan=None):
return _execute_1D('rfft', _pocketfft.rfft, x, n=n, axis=axis, norm=norm,
overwrite_x=overwrite_x, workers=workers, plan=plan)
def irfft(x, n=None, axis=-1, norm=None,
overwrite_x=False, workers=None, *, plan=None):
return _execute_1D('irfft', _pocketfft.irfft, x, n=n, axis=axis, norm=norm,
overwrite_x=overwrite_x, workers=workers, plan=plan)
def hfft(x, n=None, axis=-1, norm=None,
overwrite_x=False, workers=None, *, plan=None):
return _execute_1D('hfft', _pocketfft.hfft, x, n=n, axis=axis, norm=norm,
overwrite_x=overwrite_x, workers=workers, plan=plan)
def ihfft(x, n=None, axis=-1, norm=None,
overwrite_x=False, workers=None, *, plan=None):
return _execute_1D('ihfft', _pocketfft.ihfft, x, n=n, axis=axis, norm=norm,
overwrite_x=overwrite_x, workers=workers, plan=plan)
def fftn(x, s=None, axes=None, norm=None,
overwrite_x=False, workers=None, *, plan=None):
return _execute_nD('fftn', _pocketfft.fftn, x, s=s, axes=axes, norm=norm,
overwrite_x=overwrite_x, workers=workers, plan=plan)
def ifftn(x, s=None, axes=None, norm=None,
overwrite_x=False, workers=None, *, plan=None):
return _execute_nD('ifftn', _pocketfft.ifftn, x, s=s, axes=axes, norm=norm,
overwrite_x=overwrite_x, workers=workers, plan=plan)
def fft2(x, s=None, axes=(-2, -1), norm=None,
overwrite_x=False, workers=None, *, plan=None):
return fftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
def ifft2(x, s=None, axes=(-2, -1), norm=None,
overwrite_x=False, workers=None, *, plan=None):
return ifftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
def rfftn(x, s=None, axes=None, norm=None,
overwrite_x=False, workers=None, *, plan=None):
return _execute_nD('rfftn', _pocketfft.rfftn, x, s=s, axes=axes, norm=norm,
overwrite_x=overwrite_x, workers=workers, plan=plan)
def rfft2(x, s=None, axes=(-2, -1), norm=None,
overwrite_x=False, workers=None, *, plan=None):
return rfftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
def irfftn(x, s=None, axes=None, norm=None,
overwrite_x=False, workers=None, *, plan=None):
return _execute_nD('irfftn', _pocketfft.irfftn, x, s=s, axes=axes, norm=norm,
overwrite_x=overwrite_x, workers=workers, plan=plan)
def irfft2(x, s=None, axes=(-2, -1), norm=None,
overwrite_x=False, workers=None, *, plan=None):
return irfftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
def _swap_direction(norm):
if norm in (None, 'backward'):
norm = 'forward'
elif norm == 'forward':
norm = 'backward'
elif norm != 'ortho':
raise ValueError('Invalid norm value %s; should be "backward", '
'"ortho", or "forward".' % norm)
return norm
def hfftn(x, s=None, axes=None, norm=None,
overwrite_x=False, workers=None, *, plan=None):
xp = array_namespace(x)
if is_numpy(xp):
x = np.asarray(x)
return _pocketfft.hfftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
if is_complex(x, xp):
x = xp.conj(x)
return irfftn(x, s, axes, _swap_direction(norm),
overwrite_x, workers, plan=plan)
def hfft2(x, s=None, axes=(-2, -1), norm=None,
overwrite_x=False, workers=None, *, plan=None):
return hfftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
def ihfftn(x, s=None, axes=None, norm=None,
overwrite_x=False, workers=None, *, plan=None):
xp = array_namespace(x)
if is_numpy(xp):
x = np.asarray(x)
return _pocketfft.ihfftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
return xp.conj(rfftn(x, s, axes, _swap_direction(norm),
overwrite_x, workers, plan=plan))
def ihfft2(x, s=None, axes=(-2, -1), norm=None,
overwrite_x=False, workers=None, *, plan=None):
return ihfftn(x, s, axes, norm, overwrite_x, workers, plan=plan)

View File

@ -0,0 +1,22 @@
import numpy as np
class NumPyBackend:
"""Backend that uses numpy.fft"""
__ua_domain__ = "numpy.scipy.fft"
@staticmethod
def __ua_function__(method, args, kwargs):
kwargs.pop("overwrite_x", None)
fn = getattr(np.fft, method.__name__, None)
return (NotImplemented if fn is None
else fn(*args, **kwargs))
class EchoBackend:
"""Backend that just prints the __ua_function__ arguments"""
__ua_domain__ = "numpy.scipy.fft"
@staticmethod
def __ua_function__(method, args, kwargs):
print(method, args, kwargs, sep='\n')

View File

@ -0,0 +1,223 @@
"""Fast Hankel transforms using the FFTLog algorithm.
The implementation closely follows the Fortran code of Hamilton (2000).
added: 14/11/2020 Nicolas Tessore <n.tessore@ucl.ac.uk>
"""
from ._basic import _dispatch
from scipy._lib.uarray import Dispatchable
from ._fftlog_backend import fhtoffset
import numpy as np
__all__ = ['fht', 'ifht', 'fhtoffset']
@_dispatch
def fht(a, dln, mu, offset=0.0, bias=0.0):
r'''Compute the fast Hankel transform.
Computes the discrete Hankel transform of a logarithmically spaced periodic
sequence using the FFTLog algorithm [1]_, [2]_.
Parameters
----------
a : array_like (..., n)
Real periodic input array, uniformly logarithmically spaced. For
multidimensional input, the transform is performed over the last axis.
dln : float
Uniform logarithmic spacing of the input array.
mu : float
Order of the Hankel transform, any positive or negative real number.
offset : float, optional
Offset of the uniform logarithmic spacing of the output array.
bias : float, optional
Exponent of power law bias, any positive or negative real number.
Returns
-------
A : array_like (..., n)
The transformed output array, which is real, periodic, uniformly
logarithmically spaced, and of the same shape as the input array.
See Also
--------
ifht : The inverse of `fht`.
fhtoffset : Return an optimal offset for `fht`.
Notes
-----
This function computes a discrete version of the Hankel transform
.. math::
A(k) = \int_{0}^{\infty} \! a(r) \, J_\mu(kr) \, k \, dr \;,
where :math:`J_\mu` is the Bessel function of order :math:`\mu`. The index
:math:`\mu` may be any real number, positive or negative. Note that the
numerical Hankel transform uses an integrand of :math:`k \, dr`, while the
mathematical Hankel transform is commonly defined using :math:`r \, dr`.
The input array `a` is a periodic sequence of length :math:`n`, uniformly
logarithmically spaced with spacing `dln`,
.. math::
a_j = a(r_j) \;, \quad
r_j = r_c \exp[(j-j_c) \, \mathtt{dln}]
centred about the point :math:`r_c`. Note that the central index
:math:`j_c = (n-1)/2` is half-integral if :math:`n` is even, so that
:math:`r_c` falls between two input elements. Similarly, the output
array `A` is a periodic sequence of length :math:`n`, also uniformly
logarithmically spaced with spacing `dln`
.. math::
A_j = A(k_j) \;, \quad
k_j = k_c \exp[(j-j_c) \, \mathtt{dln}]
centred about the point :math:`k_c`.
The centre points :math:`r_c` and :math:`k_c` of the periodic intervals may
be chosen arbitrarily, but it would be usual to choose the product
:math:`k_c r_c = k_j r_{n-1-j} = k_{n-1-j} r_j` to be unity. This can be
changed using the `offset` parameter, which controls the logarithmic offset
:math:`\log(k_c) = \mathtt{offset} - \log(r_c)` of the output array.
Choosing an optimal value for `offset` may reduce ringing of the discrete
Hankel transform.
If the `bias` parameter is nonzero, this function computes a discrete
version of the biased Hankel transform
.. math::
A(k) = \int_{0}^{\infty} \! a_q(r) \, (kr)^q \, J_\mu(kr) \, k \, dr
where :math:`q` is the value of `bias`, and a power law bias
:math:`a_q(r) = a(r) \, (kr)^{-q}` is applied to the input sequence.
Biasing the transform can help approximate the continuous transform of
:math:`a(r)` if there is a value :math:`q` such that :math:`a_q(r)` is
close to a periodic sequence, in which case the resulting :math:`A(k)` will
be close to the continuous transform.
References
----------
.. [1] Talman J. D., 1978, J. Comp. Phys., 29, 35
.. [2] Hamilton A. J. S., 2000, MNRAS, 312, 257 (astro-ph/9905191)
Examples
--------
This example is the adapted version of ``fftlogtest.f`` which is provided
in [2]_. It evaluates the integral
.. math::
\int^\infty_0 r^{\mu+1} \exp(-r^2/2) J_\mu(k, r) k dr
= k^{\mu+1} \exp(-k^2/2) .
>>> import numpy as np
>>> from scipy import fft
>>> import matplotlib.pyplot as plt
Parameters for the transform.
>>> mu = 0.0 # Order mu of Bessel function
>>> r = np.logspace(-7, 1, 128) # Input evaluation points
>>> dln = np.log(r[1]/r[0]) # Step size
>>> offset = fft.fhtoffset(dln, initial=-6*np.log(10), mu=mu)
>>> k = np.exp(offset)/r[::-1] # Output evaluation points
Define the analytical function.
>>> def f(x, mu):
... """Analytical function: x^(mu+1) exp(-x^2/2)."""
... return x**(mu + 1)*np.exp(-x**2/2)
Evaluate the function at ``r`` and compute the corresponding values at
``k`` using FFTLog.
>>> a_r = f(r, mu)
>>> fht = fft.fht(a_r, dln, mu=mu, offset=offset)
For this example we can actually compute the analytical response (which in
this case is the same as the input function) for comparison and compute the
relative error.
>>> a_k = f(k, mu)
>>> rel_err = abs((fht-a_k)/a_k)
Plot the result.
>>> figargs = {'sharex': True, 'sharey': True, 'constrained_layout': True}
>>> fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4), **figargs)
>>> ax1.set_title(r'$r^{\mu+1}\ \exp(-r^2/2)$')
>>> ax1.loglog(r, a_r, 'k', lw=2)
>>> ax1.set_xlabel('r')
>>> ax2.set_title(r'$k^{\mu+1} \exp(-k^2/2)$')
>>> ax2.loglog(k, a_k, 'k', lw=2, label='Analytical')
>>> ax2.loglog(k, fht, 'C3--', lw=2, label='FFTLog')
>>> ax2.set_xlabel('k')
>>> ax2.legend(loc=3, framealpha=1)
>>> ax2.set_ylim([1e-10, 1e1])
>>> ax2b = ax2.twinx()
>>> ax2b.loglog(k, rel_err, 'C0', label='Rel. Error (-)')
>>> ax2b.set_ylabel('Rel. Error (-)', color='C0')
>>> ax2b.tick_params(axis='y', labelcolor='C0')
>>> ax2b.legend(loc=4, framealpha=1)
>>> ax2b.set_ylim([1e-9, 1e-3])
>>> plt.show()
'''
return (Dispatchable(a, np.ndarray),)
@_dispatch
def ifht(A, dln, mu, offset=0.0, bias=0.0):
r"""Compute the inverse fast Hankel transform.
Computes the discrete inverse Hankel transform of a logarithmically spaced
periodic sequence. This is the inverse operation to `fht`.
Parameters
----------
A : array_like (..., n)
Real periodic input array, uniformly logarithmically spaced. For
multidimensional input, the transform is performed over the last axis.
dln : float
Uniform logarithmic spacing of the input array.
mu : float
Order of the Hankel transform, any positive or negative real number.
offset : float, optional
Offset of the uniform logarithmic spacing of the output array.
bias : float, optional
Exponent of power law bias, any positive or negative real number.
Returns
-------
a : array_like (..., n)
The transformed output array, which is real, periodic, uniformly
logarithmically spaced, and of the same shape as the input array.
See Also
--------
fht : Definition of the fast Hankel transform.
fhtoffset : Return an optimal offset for `ifht`.
Notes
-----
This function computes a discrete version of the Hankel transform
.. math::
a(r) = \int_{0}^{\infty} \! A(k) \, J_\mu(kr) \, r \, dk \;,
where :math:`J_\mu` is the Bessel function of order :math:`\mu`. The index
:math:`\mu` may be any real number, positive or negative. Note that the
numerical inverse Hankel transform uses an integrand of :math:`r \, dk`, while the
mathematical inverse Hankel transform is commonly defined using :math:`k \, dk`.
See `fht` for further details.
"""
return (Dispatchable(A, np.ndarray),)

View File

@ -0,0 +1,199 @@
import numpy as np
from warnings import warn
from ._basic import rfft, irfft
from ..special import loggamma, poch
from scipy._lib._array_api import array_namespace, copy
__all__ = ['fht', 'ifht', 'fhtoffset']
# constants
LN_2 = np.log(2)
def fht(a, dln, mu, offset=0.0, bias=0.0):
xp = array_namespace(a)
a = xp.asarray(a)
# size of transform
n = a.shape[-1]
# bias input array
if bias != 0:
# a_q(r) = a(r) (r/r_c)^{-q}
j_c = (n-1)/2
j = xp.arange(n, dtype=xp.float64)
a = a * xp.exp(-bias*(j - j_c)*dln)
# compute FHT coefficients
u = xp.asarray(fhtcoeff(n, dln, mu, offset=offset, bias=bias))
# transform
A = _fhtq(a, u, xp=xp)
# bias output array
if bias != 0:
# A(k) = A_q(k) (k/k_c)^{-q} (k_c r_c)^{-q}
A *= xp.exp(-bias*((j - j_c)*dln + offset))
return A
def ifht(A, dln, mu, offset=0.0, bias=0.0):
xp = array_namespace(A)
A = xp.asarray(A)
# size of transform
n = A.shape[-1]
# bias input array
if bias != 0:
# A_q(k) = A(k) (k/k_c)^{q} (k_c r_c)^{q}
j_c = (n-1)/2
j = xp.arange(n, dtype=xp.float64)
A = A * xp.exp(bias*((j - j_c)*dln + offset))
# compute FHT coefficients
u = xp.asarray(fhtcoeff(n, dln, mu, offset=offset, bias=bias, inverse=True))
# transform
a = _fhtq(A, u, inverse=True, xp=xp)
# bias output array
if bias != 0:
# a(r) = a_q(r) (r/r_c)^{q}
a /= xp.exp(-bias*(j - j_c)*dln)
return a
def fhtcoeff(n, dln, mu, offset=0.0, bias=0.0, inverse=False):
"""Compute the coefficient array for a fast Hankel transform."""
lnkr, q = offset, bias
# Hankel transform coefficients
# u_m = (kr)^{-i 2m pi/(n dlnr)} U_mu(q + i 2m pi/(n dlnr))
# with U_mu(x) = 2^x Gamma((mu+1+x)/2)/Gamma((mu+1-x)/2)
xp = (mu+1+q)/2
xm = (mu+1-q)/2
y = np.linspace(0, np.pi*(n//2)/(n*dln), n//2+1)
u = np.empty(n//2+1, dtype=complex)
v = np.empty(n//2+1, dtype=complex)
u.imag[:] = y
u.real[:] = xm
loggamma(u, out=v)
u.real[:] = xp
loggamma(u, out=u)
y *= 2*(LN_2 - lnkr)
u.real -= v.real
u.real += LN_2*q
u.imag += v.imag
u.imag += y
np.exp(u, out=u)
# fix last coefficient to be real
u.imag[-1] = 0
# deal with special cases
if not np.isfinite(u[0]):
# write u_0 = 2^q Gamma(xp)/Gamma(xm) = 2^q poch(xm, xp-xm)
# poch() handles special cases for negative integers correctly
u[0] = 2**q * poch(xm, xp-xm)
# the coefficient may be inf or 0, meaning the transform or the
# inverse transform, respectively, is singular
# check for singular transform or singular inverse transform
if np.isinf(u[0]) and not inverse:
warn('singular transform; consider changing the bias', stacklevel=3)
# fix coefficient to obtain (potentially correct) transform anyway
u = copy(u)
u[0] = 0
elif u[0] == 0 and inverse:
warn('singular inverse transform; consider changing the bias', stacklevel=3)
# fix coefficient to obtain (potentially correct) inverse anyway
u = copy(u)
u[0] = np.inf
return u
def fhtoffset(dln, mu, initial=0.0, bias=0.0):
"""Return optimal offset for a fast Hankel transform.
Returns an offset close to `initial` that fulfils the low-ringing
condition of [1]_ for the fast Hankel transform `fht` with logarithmic
spacing `dln`, order `mu` and bias `bias`.
Parameters
----------
dln : float
Uniform logarithmic spacing of the transform.
mu : float
Order of the Hankel transform, any positive or negative real number.
initial : float, optional
Initial value for the offset. Returns the closest value that fulfils
the low-ringing condition.
bias : float, optional
Exponent of power law bias, any positive or negative real number.
Returns
-------
offset : float
Optimal offset of the uniform logarithmic spacing of the transform that
fulfils a low-ringing condition.
Examples
--------
>>> from scipy.fft import fhtoffset
>>> dln = 0.1
>>> mu = 2.0
>>> initial = 0.5
>>> bias = 0.0
>>> offset = fhtoffset(dln, mu, initial, bias)
>>> offset
0.5454581477676637
See Also
--------
fht : Definition of the fast Hankel transform.
References
----------
.. [1] Hamilton A. J. S., 2000, MNRAS, 312, 257 (astro-ph/9905191)
"""
lnkr, q = initial, bias
xp = (mu+1+q)/2
xm = (mu+1-q)/2
y = np.pi/(2*dln)
zp = loggamma(xp + 1j*y)
zm = loggamma(xm + 1j*y)
arg = (LN_2 - lnkr)/dln + (zp.imag + zm.imag)/np.pi
return lnkr + (arg - np.round(arg))*dln
def _fhtq(a, u, inverse=False, *, xp=None):
"""Compute the biased fast Hankel transform.
This is the basic FFTLog routine.
"""
if xp is None:
xp = np
# size of transform
n = a.shape[-1]
# biased fast Hankel transform via real FFT
A = rfft(a, axis=-1)
if not inverse:
# forward transform
A *= u
else:
# backward transform
A /= xp.conj(u)
A = irfft(A, n, axis=-1)
A = xp.flip(A, axis=-1)
return A

View File

@ -0,0 +1,379 @@
from functools import update_wrapper, lru_cache
import inspect
from ._pocketfft import helper as _helper
import numpy as np
from scipy._lib._array_api import array_namespace
def next_fast_len(target, real=False):
"""Find the next fast size of input data to ``fft``, for zero-padding, etc.
SciPy's FFT algorithms gain their speed by a recursive divide and conquer
strategy. This relies on efficient functions for small prime factors of the
input length. Thus, the transforms are fastest when using composites of the
prime factors handled by the fft implementation. If there are efficient
functions for all radices <= `n`, then the result will be a number `x`
>= ``target`` with only prime factors < `n`. (Also known as `n`-smooth
numbers)
Parameters
----------
target : int
Length to start searching from. Must be a positive integer.
real : bool, optional
True if the FFT involves real input or output (e.g., `rfft` or `hfft`
but not `fft`). Defaults to False.
Returns
-------
out : int
The smallest fast length greater than or equal to ``target``.
Notes
-----
The result of this function may change in future as performance
considerations change, for example, if new prime factors are added.
Calling `fft` or `ifft` with real input data performs an ``'R2C'``
transform internally.
Examples
--------
On a particular machine, an FFT of prime length takes 11.4 ms:
>>> from scipy import fft
>>> import numpy as np
>>> rng = np.random.default_rng()
>>> min_len = 93059 # prime length is worst case for speed
>>> a = rng.standard_normal(min_len)
>>> b = fft.fft(a)
Zero-padding to the next regular length reduces computation time to
1.6 ms, a speedup of 7.3 times:
>>> fft.next_fast_len(min_len, real=True)
93312
>>> b = fft.fft(a, 93312)
Rounding up to the next power of 2 is not optimal, taking 3.0 ms to
compute; 1.9 times longer than the size given by ``next_fast_len``:
>>> b = fft.fft(a, 131072)
"""
pass
# Directly wrap the c-function good_size but take the docstring etc., from the
# next_fast_len function above
_sig = inspect.signature(next_fast_len)
next_fast_len = update_wrapper(lru_cache(_helper.good_size), next_fast_len)
next_fast_len.__wrapped__ = _helper.good_size
next_fast_len.__signature__ = _sig
def prev_fast_len(target, real=False):
"""Find the previous fast size of input data to ``fft``.
Useful for discarding a minimal number of samples before FFT.
SciPy's FFT algorithms gain their speed by a recursive divide and conquer
strategy. This relies on efficient functions for small prime factors of the
input length. Thus, the transforms are fastest when using composites of the
prime factors handled by the fft implementation. If there are efficient
functions for all radices <= `n`, then the result will be a number `x`
<= ``target`` with only prime factors <= `n`. (Also known as `n`-smooth
numbers)
Parameters
----------
target : int
Maximum length to search until. Must be a positive integer.
real : bool, optional
True if the FFT involves real input or output (e.g., `rfft` or `hfft`
but not `fft`). Defaults to False.
Returns
-------
out : int
The largest fast length less than or equal to ``target``.
Notes
-----
The result of this function may change in future as performance
considerations change, for example, if new prime factors are added.
Calling `fft` or `ifft` with real input data performs an ``'R2C'``
transform internally.
In the current implementation, prev_fast_len assumes radices of
2,3,5,7,11 for complex FFT and 2,3,5 for real FFT.
Examples
--------
On a particular machine, an FFT of prime length takes 16.2 ms:
>>> from scipy import fft
>>> import numpy as np
>>> rng = np.random.default_rng()
>>> max_len = 93059 # prime length is worst case for speed
>>> a = rng.standard_normal(max_len)
>>> b = fft.fft(a)
Performing FFT on the maximum fast length less than max_len
reduces the computation time to 1.5 ms, a speedup of 10.5 times:
>>> fft.prev_fast_len(max_len, real=True)
92160
>>> c = fft.fft(a[:92160]) # discard last 899 samples
"""
pass
# Directly wrap the c-function prev_good_size but take the docstring etc.,
# from the prev_fast_len function above
_sig_prev_fast_len = inspect.signature(prev_fast_len)
prev_fast_len = update_wrapper(lru_cache()(_helper.prev_good_size), prev_fast_len)
prev_fast_len.__wrapped__ = _helper.prev_good_size
prev_fast_len.__signature__ = _sig_prev_fast_len
def _init_nd_shape_and_axes(x, shape, axes):
"""Handle shape and axes arguments for N-D transforms.
Returns the shape and axes in a standard form, taking into account negative
values and checking for various potential errors.
Parameters
----------
x : array_like
The input array.
shape : int or array_like of ints or None
The shape of the result. If both `shape` and `axes` (see below) are
None, `shape` is ``x.shape``; if `shape` is None but `axes` is
not None, then `shape` is ``numpy.take(x.shape, axes, axis=0)``.
If `shape` is -1, the size of the corresponding dimension of `x` is
used.
axes : int or array_like of ints or None
Axes along which the calculation is computed.
The default is over all axes.
Negative indices are automatically converted to their positive
counterparts.
Returns
-------
shape : tuple
The shape of the result as a tuple of integers.
axes : list
Axes along which the calculation is computed, as a list of integers.
"""
x = np.asarray(x)
return _helper._init_nd_shape_and_axes(x, shape, axes)
def fftfreq(n, d=1.0, *, xp=None, device=None):
"""Return the Discrete Fourier Transform sample frequencies.
The returned float array `f` contains the frequency bin centers in cycles
per unit of the sample spacing (with zero at the start). For instance, if
the sample spacing is in seconds, then the frequency unit is cycles/second.
Given a window length `n` and a sample spacing `d`::
f = [0, 1, ..., n/2-1, -n/2, ..., -1] / (d*n) if n is even
f = [0, 1, ..., (n-1)/2, -(n-1)/2, ..., -1] / (d*n) if n is odd
Parameters
----------
n : int
Window length.
d : scalar, optional
Sample spacing (inverse of the sampling rate). Defaults to 1.
xp : array_namespace, optional
The namespace for the return array. Default is None, where NumPy is used.
device : device, optional
The device for the return array.
Only valid when `xp.fft.fftfreq` implements the device parameter.
Returns
-------
f : ndarray
Array of length `n` containing the sample frequencies.
Examples
--------
>>> import numpy as np
>>> import scipy.fft
>>> signal = np.array([-2, 8, 6, 4, 1, 0, 3, 5], dtype=float)
>>> fourier = scipy.fft.fft(signal)
>>> n = signal.size
>>> timestep = 0.1
>>> freq = scipy.fft.fftfreq(n, d=timestep)
>>> freq
array([ 0. , 1.25, 2.5 , ..., -3.75, -2.5 , -1.25])
"""
xp = np if xp is None else xp
# numpy does not yet support the `device` keyword
# `xp.__name__ != 'numpy'` should be removed when numpy is compatible
if hasattr(xp, 'fft') and xp.__name__ != 'numpy':
return xp.fft.fftfreq(n, d=d, device=device)
if device is not None:
raise ValueError('device parameter is not supported for input array type')
return np.fft.fftfreq(n, d=d)
def rfftfreq(n, d=1.0, *, xp=None, device=None):
"""Return the Discrete Fourier Transform sample frequencies
(for usage with rfft, irfft).
The returned float array `f` contains the frequency bin centers in cycles
per unit of the sample spacing (with zero at the start). For instance, if
the sample spacing is in seconds, then the frequency unit is cycles/second.
Given a window length `n` and a sample spacing `d`::
f = [0, 1, ..., n/2-1, n/2] / (d*n) if n is even
f = [0, 1, ..., (n-1)/2-1, (n-1)/2] / (d*n) if n is odd
Unlike `fftfreq` (but like `scipy.fftpack.rfftfreq`)
the Nyquist frequency component is considered to be positive.
Parameters
----------
n : int
Window length.
d : scalar, optional
Sample spacing (inverse of the sampling rate). Defaults to 1.
xp : array_namespace, optional
The namespace for the return array. Default is None, where NumPy is used.
device : device, optional
The device for the return array.
Only valid when `xp.fft.rfftfreq` implements the device parameter.
Returns
-------
f : ndarray
Array of length ``n//2 + 1`` containing the sample frequencies.
Examples
--------
>>> import numpy as np
>>> import scipy.fft
>>> signal = np.array([-2, 8, 6, 4, 1, 0, 3, 5, -3, 4], dtype=float)
>>> fourier = scipy.fft.rfft(signal)
>>> n = signal.size
>>> sample_rate = 100
>>> freq = scipy.fft.fftfreq(n, d=1./sample_rate)
>>> freq
array([ 0., 10., 20., ..., -30., -20., -10.])
>>> freq = scipy.fft.rfftfreq(n, d=1./sample_rate)
>>> freq
array([ 0., 10., 20., 30., 40., 50.])
"""
xp = np if xp is None else xp
# numpy does not yet support the `device` keyword
# `xp.__name__ != 'numpy'` should be removed when numpy is compatible
if hasattr(xp, 'fft') and xp.__name__ != 'numpy':
return xp.fft.rfftfreq(n, d=d, device=device)
if device is not None:
raise ValueError('device parameter is not supported for input array type')
return np.fft.rfftfreq(n, d=d)
def fftshift(x, axes=None):
"""Shift the zero-frequency component to the center of the spectrum.
This function swaps half-spaces for all axes listed (defaults to all).
Note that ``y[0]`` is the Nyquist component only if ``len(x)`` is even.
Parameters
----------
x : array_like
Input array.
axes : int or shape tuple, optional
Axes over which to shift. Default is None, which shifts all axes.
Returns
-------
y : ndarray
The shifted array.
See Also
--------
ifftshift : The inverse of `fftshift`.
Examples
--------
>>> import numpy as np
>>> freqs = np.fft.fftfreq(10, 0.1)
>>> freqs
array([ 0., 1., 2., ..., -3., -2., -1.])
>>> np.fft.fftshift(freqs)
array([-5., -4., -3., -2., -1., 0., 1., 2., 3., 4.])
Shift the zero-frequency component only along the second axis:
>>> freqs = np.fft.fftfreq(9, d=1./9).reshape(3, 3)
>>> freqs
array([[ 0., 1., 2.],
[ 3., 4., -4.],
[-3., -2., -1.]])
>>> np.fft.fftshift(freqs, axes=(1,))
array([[ 2., 0., 1.],
[-4., 3., 4.],
[-1., -3., -2.]])
"""
xp = array_namespace(x)
if hasattr(xp, 'fft'):
return xp.fft.fftshift(x, axes=axes)
x = np.asarray(x)
y = np.fft.fftshift(x, axes=axes)
return xp.asarray(y)
def ifftshift(x, axes=None):
"""The inverse of `fftshift`. Although identical for even-length `x`, the
functions differ by one sample for odd-length `x`.
Parameters
----------
x : array_like
Input array.
axes : int or shape tuple, optional
Axes over which to calculate. Defaults to None, which shifts all axes.
Returns
-------
y : ndarray
The shifted array.
See Also
--------
fftshift : Shift zero-frequency component to the center of the spectrum.
Examples
--------
>>> import numpy as np
>>> freqs = np.fft.fftfreq(9, d=1./9).reshape(3, 3)
>>> freqs
array([[ 0., 1., 2.],
[ 3., 4., -4.],
[-3., -2., -1.]])
>>> np.fft.ifftshift(np.fft.fftshift(freqs))
array([[ 0., 1., 2.],
[ 3., 4., -4.],
[-3., -2., -1.]])
"""
xp = array_namespace(x)
if hasattr(xp, 'fft'):
return xp.fft.ifftshift(x, axes=axes)
x = np.asarray(x)
y = np.fft.ifftshift(x, axes=axes)
return xp.asarray(y)

View File

@ -0,0 +1,25 @@
Copyright (C) 2010-2019 Max-Planck-Society
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,9 @@
""" FFT backend using pypocketfft """
from .basic import *
from .realtransforms import *
from .helper import *
from scipy._lib._testutils import PytestTester
test = PytestTester(__name__)
del PytestTester

View File

@ -0,0 +1,251 @@
"""
Discrete Fourier Transforms - basic.py
"""
import numpy as np
import functools
from . import pypocketfft as pfft
from .helper import (_asfarray, _init_nd_shape_and_axes, _datacopied,
_fix_shape, _fix_shape_1d, _normalization,
_workers)
def c2c(forward, x, n=None, axis=-1, norm=None, overwrite_x=False,
workers=None, *, plan=None):
""" Return discrete Fourier transform of real or complex sequence. """
if plan is not None:
raise NotImplementedError('Passing a precomputed plan is not yet '
'supported by scipy.fft functions')
tmp = _asfarray(x)
overwrite_x = overwrite_x or _datacopied(tmp, x)
norm = _normalization(norm, forward)
workers = _workers(workers)
if n is not None:
tmp, copied = _fix_shape_1d(tmp, n, axis)
overwrite_x = overwrite_x or copied
elif tmp.shape[axis] < 1:
message = f"invalid number of data points ({tmp.shape[axis]}) specified"
raise ValueError(message)
out = (tmp if overwrite_x and tmp.dtype.kind == 'c' else None)
return pfft.c2c(tmp, (axis,), forward, norm, out, workers)
fft = functools.partial(c2c, True)
fft.__name__ = 'fft'
ifft = functools.partial(c2c, False)
ifft.__name__ = 'ifft'
def r2c(forward, x, n=None, axis=-1, norm=None, overwrite_x=False,
workers=None, *, plan=None):
"""
Discrete Fourier transform of a real sequence.
"""
if plan is not None:
raise NotImplementedError('Passing a precomputed plan is not yet '
'supported by scipy.fft functions')
tmp = _asfarray(x)
norm = _normalization(norm, forward)
workers = _workers(workers)
if not np.isrealobj(tmp):
raise TypeError("x must be a real sequence")
if n is not None:
tmp, _ = _fix_shape_1d(tmp, n, axis)
elif tmp.shape[axis] < 1:
raise ValueError(f"invalid number of data points ({tmp.shape[axis]}) specified")
# Note: overwrite_x is not utilised
return pfft.r2c(tmp, (axis,), forward, norm, None, workers)
rfft = functools.partial(r2c, True)
rfft.__name__ = 'rfft'
ihfft = functools.partial(r2c, False)
ihfft.__name__ = 'ihfft'
def c2r(forward, x, n=None, axis=-1, norm=None, overwrite_x=False,
workers=None, *, plan=None):
"""
Return inverse discrete Fourier transform of real sequence x.
"""
if plan is not None:
raise NotImplementedError('Passing a precomputed plan is not yet '
'supported by scipy.fft functions')
tmp = _asfarray(x)
norm = _normalization(norm, forward)
workers = _workers(workers)
# TODO: Optimize for hermitian and real?
if np.isrealobj(tmp):
tmp = tmp + 0.j
# Last axis utilizes hermitian symmetry
if n is None:
n = (tmp.shape[axis] - 1) * 2
if n < 1:
raise ValueError(f"Invalid number of data points ({n}) specified")
else:
tmp, _ = _fix_shape_1d(tmp, (n//2) + 1, axis)
# Note: overwrite_x is not utilized
return pfft.c2r(tmp, (axis,), n, forward, norm, None, workers)
hfft = functools.partial(c2r, True)
hfft.__name__ = 'hfft'
irfft = functools.partial(c2r, False)
irfft.__name__ = 'irfft'
def hfft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None,
*, plan=None):
"""
2-D discrete Fourier transform of a Hermitian sequence
"""
if plan is not None:
raise NotImplementedError('Passing a precomputed plan is not yet '
'supported by scipy.fft functions')
return hfftn(x, s, axes, norm, overwrite_x, workers)
def ihfft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None,
*, plan=None):
"""
2-D discrete inverse Fourier transform of a Hermitian sequence
"""
if plan is not None:
raise NotImplementedError('Passing a precomputed plan is not yet '
'supported by scipy.fft functions')
return ihfftn(x, s, axes, norm, overwrite_x, workers)
def c2cn(forward, x, s=None, axes=None, norm=None, overwrite_x=False,
workers=None, *, plan=None):
"""
Return multidimensional discrete Fourier transform.
"""
if plan is not None:
raise NotImplementedError('Passing a precomputed plan is not yet '
'supported by scipy.fft functions')
tmp = _asfarray(x)
shape, axes = _init_nd_shape_and_axes(tmp, s, axes)
overwrite_x = overwrite_x or _datacopied(tmp, x)
workers = _workers(workers)
if len(axes) == 0:
return x
tmp, copied = _fix_shape(tmp, shape, axes)
overwrite_x = overwrite_x or copied
norm = _normalization(norm, forward)
out = (tmp if overwrite_x and tmp.dtype.kind == 'c' else None)
return pfft.c2c(tmp, axes, forward, norm, out, workers)
fftn = functools.partial(c2cn, True)
fftn.__name__ = 'fftn'
ifftn = functools.partial(c2cn, False)
ifftn.__name__ = 'ifftn'
def r2cn(forward, x, s=None, axes=None, norm=None, overwrite_x=False,
workers=None, *, plan=None):
"""Return multidimensional discrete Fourier transform of real input"""
if plan is not None:
raise NotImplementedError('Passing a precomputed plan is not yet '
'supported by scipy.fft functions')
tmp = _asfarray(x)
if not np.isrealobj(tmp):
raise TypeError("x must be a real sequence")
shape, axes = _init_nd_shape_and_axes(tmp, s, axes)
tmp, _ = _fix_shape(tmp, shape, axes)
norm = _normalization(norm, forward)
workers = _workers(workers)
if len(axes) == 0:
raise ValueError("at least 1 axis must be transformed")
# Note: overwrite_x is not utilized
return pfft.r2c(tmp, axes, forward, norm, None, workers)
rfftn = functools.partial(r2cn, True)
rfftn.__name__ = 'rfftn'
ihfftn = functools.partial(r2cn, False)
ihfftn.__name__ = 'ihfftn'
def c2rn(forward, x, s=None, axes=None, norm=None, overwrite_x=False,
workers=None, *, plan=None):
"""Multidimensional inverse discrete fourier transform with real output"""
if plan is not None:
raise NotImplementedError('Passing a precomputed plan is not yet '
'supported by scipy.fft functions')
tmp = _asfarray(x)
# TODO: Optimize for hermitian and real?
if np.isrealobj(tmp):
tmp = tmp + 0.j
noshape = s is None
shape, axes = _init_nd_shape_and_axes(tmp, s, axes)
if len(axes) == 0:
raise ValueError("at least 1 axis must be transformed")
shape = list(shape)
if noshape:
shape[-1] = (x.shape[axes[-1]] - 1) * 2
norm = _normalization(norm, forward)
workers = _workers(workers)
# Last axis utilizes hermitian symmetry
lastsize = shape[-1]
shape[-1] = (shape[-1] // 2) + 1
tmp, _ = tuple(_fix_shape(tmp, shape, axes))
# Note: overwrite_x is not utilized
return pfft.c2r(tmp, axes, lastsize, forward, norm, None, workers)
hfftn = functools.partial(c2rn, True)
hfftn.__name__ = 'hfftn'
irfftn = functools.partial(c2rn, False)
irfftn.__name__ = 'irfftn'
def r2r_fftpack(forward, x, n=None, axis=-1, norm=None, overwrite_x=False):
"""FFT of a real sequence, returning fftpack half complex format"""
tmp = _asfarray(x)
overwrite_x = overwrite_x or _datacopied(tmp, x)
norm = _normalization(norm, forward)
workers = _workers(None)
if tmp.dtype.kind == 'c':
raise TypeError('x must be a real sequence')
if n is not None:
tmp, copied = _fix_shape_1d(tmp, n, axis)
overwrite_x = overwrite_x or copied
elif tmp.shape[axis] < 1:
raise ValueError(f"invalid number of data points ({tmp.shape[axis]}) specified")
out = (tmp if overwrite_x else None)
return pfft.r2r_fftpack(tmp, (axis,), forward, forward, norm, out, workers)
rfft_fftpack = functools.partial(r2r_fftpack, True)
rfft_fftpack.__name__ = 'rfft_fftpack'
irfft_fftpack = functools.partial(r2r_fftpack, False)
irfft_fftpack.__name__ = 'irfft_fftpack'

View File

@ -0,0 +1,221 @@
from numbers import Number
import operator
import os
import threading
import contextlib
import numpy as np
from scipy._lib._util import copy_if_needed
# good_size is exposed (and used) from this import
from .pypocketfft import good_size, prev_good_size
__all__ = ['good_size', 'prev_good_size', 'set_workers', 'get_workers']
_config = threading.local()
_cpu_count = os.cpu_count()
def _iterable_of_int(x, name=None):
"""Convert ``x`` to an iterable sequence of int
Parameters
----------
x : value, or sequence of values, convertible to int
name : str, optional
Name of the argument being converted, only used in the error message
Returns
-------
y : ``List[int]``
"""
if isinstance(x, Number):
x = (x,)
try:
x = [operator.index(a) for a in x]
except TypeError as e:
name = name or "value"
raise ValueError(f"{name} must be a scalar or iterable of integers") from e
return x
def _init_nd_shape_and_axes(x, shape, axes):
"""Handles shape and axes arguments for nd transforms"""
noshape = shape is None
noaxes = axes is None
if not noaxes:
axes = _iterable_of_int(axes, 'axes')
axes = [a + x.ndim if a < 0 else a for a in axes]
if any(a >= x.ndim or a < 0 for a in axes):
raise ValueError("axes exceeds dimensionality of input")
if len(set(axes)) != len(axes):
raise ValueError("all axes must be unique")
if not noshape:
shape = _iterable_of_int(shape, 'shape')
if axes and len(axes) != len(shape):
raise ValueError("when given, axes and shape arguments"
" have to be of the same length")
if noaxes:
if len(shape) > x.ndim:
raise ValueError("shape requires more axes than are present")
axes = range(x.ndim - len(shape), x.ndim)
shape = [x.shape[a] if s == -1 else s for s, a in zip(shape, axes)]
elif noaxes:
shape = list(x.shape)
axes = range(x.ndim)
else:
shape = [x.shape[a] for a in axes]
if any(s < 1 for s in shape):
raise ValueError(
f"invalid number of data points ({shape}) specified")
return tuple(shape), list(axes)
def _asfarray(x):
"""
Convert to array with floating or complex dtype.
float16 values are also promoted to float32.
"""
if not hasattr(x, "dtype"):
x = np.asarray(x)
if x.dtype == np.float16:
return np.asarray(x, np.float32)
elif x.dtype.kind not in 'fc':
return np.asarray(x, np.float64)
# Require native byte order
dtype = x.dtype.newbyteorder('=')
# Always align input
copy = True if not x.flags['ALIGNED'] else copy_if_needed
return np.array(x, dtype=dtype, copy=copy)
def _datacopied(arr, original):
"""
Strict check for `arr` not sharing any data with `original`,
under the assumption that arr = asarray(original)
"""
if arr is original:
return False
if not isinstance(original, np.ndarray) and hasattr(original, '__array__'):
return False
return arr.base is None
def _fix_shape(x, shape, axes):
"""Internal auxiliary function for _raw_fft, _raw_fftnd."""
must_copy = False
# Build an nd slice with the dimensions to be read from x
index = [slice(None)]*x.ndim
for n, ax in zip(shape, axes):
if x.shape[ax] >= n:
index[ax] = slice(0, n)
else:
index[ax] = slice(0, x.shape[ax])
must_copy = True
index = tuple(index)
if not must_copy:
return x[index], False
s = list(x.shape)
for n, axis in zip(shape, axes):
s[axis] = n
z = np.zeros(s, x.dtype)
z[index] = x[index]
return z, True
def _fix_shape_1d(x, n, axis):
if n < 1:
raise ValueError(
f"invalid number of data points ({n}) specified")
return _fix_shape(x, (n,), (axis,))
_NORM_MAP = {None: 0, 'backward': 0, 'ortho': 1, 'forward': 2}
def _normalization(norm, forward):
"""Returns the pypocketfft normalization mode from the norm argument"""
try:
inorm = _NORM_MAP[norm]
return inorm if forward else (2 - inorm)
except KeyError:
raise ValueError(
f'Invalid norm value {norm!r}, should '
'be "backward", "ortho" or "forward"') from None
def _workers(workers):
if workers is None:
return getattr(_config, 'default_workers', 1)
if workers < 0:
if workers >= -_cpu_count:
workers += 1 + _cpu_count
else:
raise ValueError(f"workers value out of range; got {workers}, must not be"
f" less than {-_cpu_count}")
elif workers == 0:
raise ValueError("workers must not be zero")
return workers
@contextlib.contextmanager
def set_workers(workers):
"""Context manager for the default number of workers used in `scipy.fft`
Parameters
----------
workers : int
The default number of workers to use
Examples
--------
>>> import numpy as np
>>> from scipy import fft, signal
>>> rng = np.random.default_rng()
>>> x = rng.standard_normal((128, 64))
>>> with fft.set_workers(4):
... y = signal.fftconvolve(x, x)
"""
old_workers = get_workers()
_config.default_workers = _workers(operator.index(workers))
try:
yield
finally:
_config.default_workers = old_workers
def get_workers():
"""Returns the default number of workers within the current context
Examples
--------
>>> from scipy import fft
>>> fft.get_workers()
1
>>> with fft.set_workers(4):
... fft.get_workers()
4
"""
return getattr(_config, 'default_workers', 1)

View File

@ -0,0 +1,109 @@
import numpy as np
from . import pypocketfft as pfft
from .helper import (_asfarray, _init_nd_shape_and_axes, _datacopied,
_fix_shape, _fix_shape_1d, _normalization, _workers)
import functools
def _r2r(forward, transform, x, type=2, n=None, axis=-1, norm=None,
overwrite_x=False, workers=None, orthogonalize=None):
"""Forward or backward 1-D DCT/DST
Parameters
----------
forward : bool
Transform direction (determines type and normalisation)
transform : {pypocketfft.dct, pypocketfft.dst}
The transform to perform
"""
tmp = _asfarray(x)
overwrite_x = overwrite_x or _datacopied(tmp, x)
norm = _normalization(norm, forward)
workers = _workers(workers)
if not forward:
if type == 2:
type = 3
elif type == 3:
type = 2
if n is not None:
tmp, copied = _fix_shape_1d(tmp, n, axis)
overwrite_x = overwrite_x or copied
elif tmp.shape[axis] < 1:
raise ValueError(f"invalid number of data points ({tmp.shape[axis]}) specified")
out = (tmp if overwrite_x else None)
# For complex input, transform real and imaginary components separably
if np.iscomplexobj(x):
out = np.empty_like(tmp) if out is None else out
transform(tmp.real, type, (axis,), norm, out.real, workers)
transform(tmp.imag, type, (axis,), norm, out.imag, workers)
return out
return transform(tmp, type, (axis,), norm, out, workers, orthogonalize)
dct = functools.partial(_r2r, True, pfft.dct)
dct.__name__ = 'dct'
idct = functools.partial(_r2r, False, pfft.dct)
idct.__name__ = 'idct'
dst = functools.partial(_r2r, True, pfft.dst)
dst.__name__ = 'dst'
idst = functools.partial(_r2r, False, pfft.dst)
idst.__name__ = 'idst'
def _r2rn(forward, transform, x, type=2, s=None, axes=None, norm=None,
overwrite_x=False, workers=None, orthogonalize=None):
"""Forward or backward nd DCT/DST
Parameters
----------
forward : bool
Transform direction (determines type and normalisation)
transform : {pypocketfft.dct, pypocketfft.dst}
The transform to perform
"""
tmp = _asfarray(x)
shape, axes = _init_nd_shape_and_axes(tmp, s, axes)
overwrite_x = overwrite_x or _datacopied(tmp, x)
if len(axes) == 0:
return x
tmp, copied = _fix_shape(tmp, shape, axes)
overwrite_x = overwrite_x or copied
if not forward:
if type == 2:
type = 3
elif type == 3:
type = 2
norm = _normalization(norm, forward)
workers = _workers(workers)
out = (tmp if overwrite_x else None)
# For complex input, transform real and imaginary components separably
if np.iscomplexobj(x):
out = np.empty_like(tmp) if out is None else out
transform(tmp.real, type, axes, norm, out.real, workers)
transform(tmp.imag, type, axes, norm, out.imag, workers)
return out
return transform(tmp, type, axes, norm, out, workers, orthogonalize)
dctn = functools.partial(_r2rn, True, pfft.dct)
dctn.__name__ = 'dctn'
idctn = functools.partial(_r2rn, False, pfft.dct)
idctn.__name__ = 'idctn'
dstn = functools.partial(_r2rn, True, pfft.dst)
dstn.__name__ = 'dstn'
idstn = functools.partial(_r2rn, False, pfft.dst)
idstn.__name__ = 'idstn'

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,494 @@
from os.path import join, dirname
from typing import Callable, Union
import numpy as np
from numpy.testing import (
assert_array_almost_equal, assert_equal, assert_allclose)
import pytest
from pytest import raises as assert_raises
from scipy.fft._pocketfft.realtransforms import (
dct, idct, dst, idst, dctn, idctn, dstn, idstn)
fftpack_test_dir = join(dirname(__file__), '..', '..', '..', 'fftpack', 'tests')
MDATA_COUNT = 8
FFTWDATA_COUNT = 14
def is_longdouble_binary_compatible():
try:
one = np.frombuffer(
b'\x00\x00\x00\x00\x00\x00\x00\x80\xff\x3f\x00\x00\x00\x00\x00\x00',
dtype='<f16')
return one == np.longdouble(1.)
except TypeError:
return False
@pytest.fixture(scope="module")
def reference_data():
# Matlab reference data
MDATA = np.load(join(fftpack_test_dir, 'test.npz'))
X = [MDATA['x%d' % i] for i in range(MDATA_COUNT)]
Y = [MDATA['y%d' % i] for i in range(MDATA_COUNT)]
# FFTW reference data: the data are organized as follows:
# * SIZES is an array containing all available sizes
# * for every type (1, 2, 3, 4) and every size, the array dct_type_size
# contains the output of the DCT applied to the input np.linspace(0, size-1,
# size)
FFTWDATA_DOUBLE = np.load(join(fftpack_test_dir, 'fftw_double_ref.npz'))
FFTWDATA_SINGLE = np.load(join(fftpack_test_dir, 'fftw_single_ref.npz'))
FFTWDATA_SIZES = FFTWDATA_DOUBLE['sizes']
assert len(FFTWDATA_SIZES) == FFTWDATA_COUNT
if is_longdouble_binary_compatible():
FFTWDATA_LONGDOUBLE = np.load(
join(fftpack_test_dir, 'fftw_longdouble_ref.npz'))
else:
FFTWDATA_LONGDOUBLE = {k: v.astype(np.longdouble)
for k,v in FFTWDATA_DOUBLE.items()}
ref = {
'FFTWDATA_LONGDOUBLE': FFTWDATA_LONGDOUBLE,
'FFTWDATA_DOUBLE': FFTWDATA_DOUBLE,
'FFTWDATA_SINGLE': FFTWDATA_SINGLE,
'FFTWDATA_SIZES': FFTWDATA_SIZES,
'X': X,
'Y': Y
}
yield ref
if is_longdouble_binary_compatible():
FFTWDATA_LONGDOUBLE.close()
FFTWDATA_SINGLE.close()
FFTWDATA_DOUBLE.close()
MDATA.close()
@pytest.fixture(params=range(FFTWDATA_COUNT))
def fftwdata_size(request, reference_data):
return reference_data['FFTWDATA_SIZES'][request.param]
@pytest.fixture(params=range(MDATA_COUNT))
def mdata_x(request, reference_data):
return reference_data['X'][request.param]
@pytest.fixture(params=range(MDATA_COUNT))
def mdata_xy(request, reference_data):
y = reference_data['Y'][request.param]
x = reference_data['X'][request.param]
return x, y
def fftw_dct_ref(type, size, dt, reference_data):
x = np.linspace(0, size-1, size).astype(dt)
dt = np.result_type(np.float32, dt)
if dt == np.float64:
data = reference_data['FFTWDATA_DOUBLE']
elif dt == np.float32:
data = reference_data['FFTWDATA_SINGLE']
elif dt == np.longdouble:
data = reference_data['FFTWDATA_LONGDOUBLE']
else:
raise ValueError()
y = (data['dct_%d_%d' % (type, size)]).astype(dt)
return x, y, dt
def fftw_dst_ref(type, size, dt, reference_data):
x = np.linspace(0, size-1, size).astype(dt)
dt = np.result_type(np.float32, dt)
if dt == np.float64:
data = reference_data['FFTWDATA_DOUBLE']
elif dt == np.float32:
data = reference_data['FFTWDATA_SINGLE']
elif dt == np.longdouble:
data = reference_data['FFTWDATA_LONGDOUBLE']
else:
raise ValueError()
y = (data['dst_%d_%d' % (type, size)]).astype(dt)
return x, y, dt
def ref_2d(func, x, **kwargs):
"""Calculate 2-D reference data from a 1d transform"""
x = np.array(x, copy=True)
for row in range(x.shape[0]):
x[row, :] = func(x[row, :], **kwargs)
for col in range(x.shape[1]):
x[:, col] = func(x[:, col], **kwargs)
return x
def naive_dct1(x, norm=None):
"""Calculate textbook definition version of DCT-I."""
x = np.array(x, copy=True)
N = len(x)
M = N-1
y = np.zeros(N)
m0, m = 1, 2
if norm == 'ortho':
m0 = np.sqrt(1.0/M)
m = np.sqrt(2.0/M)
for k in range(N):
for n in range(1, N-1):
y[k] += m*x[n]*np.cos(np.pi*n*k/M)
y[k] += m0 * x[0]
y[k] += m0 * x[N-1] * (1 if k % 2 == 0 else -1)
if norm == 'ortho':
y[0] *= 1/np.sqrt(2)
y[N-1] *= 1/np.sqrt(2)
return y
def naive_dst1(x, norm=None):
"""Calculate textbook definition version of DST-I."""
x = np.array(x, copy=True)
N = len(x)
M = N+1
y = np.zeros(N)
for k in range(N):
for n in range(N):
y[k] += 2*x[n]*np.sin(np.pi*(n+1.0)*(k+1.0)/M)
if norm == 'ortho':
y *= np.sqrt(0.5/M)
return y
def naive_dct4(x, norm=None):
"""Calculate textbook definition version of DCT-IV."""
x = np.array(x, copy=True)
N = len(x)
y = np.zeros(N)
for k in range(N):
for n in range(N):
y[k] += x[n]*np.cos(np.pi*(n+0.5)*(k+0.5)/(N))
if norm == 'ortho':
y *= np.sqrt(2.0/N)
else:
y *= 2
return y
def naive_dst4(x, norm=None):
"""Calculate textbook definition version of DST-IV."""
x = np.array(x, copy=True)
N = len(x)
y = np.zeros(N)
for k in range(N):
for n in range(N):
y[k] += x[n]*np.sin(np.pi*(n+0.5)*(k+0.5)/(N))
if norm == 'ortho':
y *= np.sqrt(2.0/N)
else:
y *= 2
return y
@pytest.mark.parametrize('dtype', [np.complex64, np.complex128, np.clongdouble])
@pytest.mark.parametrize('transform', [dct, dst, idct, idst])
def test_complex(transform, dtype):
y = transform(1j*np.arange(5, dtype=dtype))
x = 1j*transform(np.arange(5))
assert_array_almost_equal(x, y)
DecMapType = dict[
tuple[Callable[..., np.ndarray], Union[type[np.floating], type[int]], int],
int,
]
# map (transform, dtype, type) -> decimal
dec_map: DecMapType = {
# DCT
(dct, np.float64, 1): 13,
(dct, np.float32, 1): 6,
(dct, np.float64, 2): 14,
(dct, np.float32, 2): 5,
(dct, np.float64, 3): 14,
(dct, np.float32, 3): 5,
(dct, np.float64, 4): 13,
(dct, np.float32, 4): 6,
# IDCT
(idct, np.float64, 1): 14,
(idct, np.float32, 1): 6,
(idct, np.float64, 2): 14,
(idct, np.float32, 2): 5,
(idct, np.float64, 3): 14,
(idct, np.float32, 3): 5,
(idct, np.float64, 4): 14,
(idct, np.float32, 4): 6,
# DST
(dst, np.float64, 1): 13,
(dst, np.float32, 1): 6,
(dst, np.float64, 2): 14,
(dst, np.float32, 2): 6,
(dst, np.float64, 3): 14,
(dst, np.float32, 3): 7,
(dst, np.float64, 4): 13,
(dst, np.float32, 4): 5,
# IDST
(idst, np.float64, 1): 14,
(idst, np.float32, 1): 6,
(idst, np.float64, 2): 14,
(idst, np.float32, 2): 6,
(idst, np.float64, 3): 14,
(idst, np.float32, 3): 6,
(idst, np.float64, 4): 14,
(idst, np.float32, 4): 6,
}
for k,v in dec_map.copy().items():
if k[1] == np.float64:
dec_map[(k[0], np.longdouble, k[2])] = v
elif k[1] == np.float32:
dec_map[(k[0], int, k[2])] = v
@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
@pytest.mark.parametrize('type', [1, 2, 3, 4])
class TestDCT:
def test_definition(self, rdt, type, fftwdata_size, reference_data):
x, yr, dt = fftw_dct_ref(type, fftwdata_size, rdt, reference_data)
y = dct(x, type=type)
assert_equal(y.dtype, dt)
dec = dec_map[(dct, rdt, type)]
assert_allclose(y, yr, rtol=0., atol=np.max(yr)*10**(-dec))
@pytest.mark.parametrize('size', [7, 8, 9, 16, 32, 64])
def test_axis(self, rdt, type, size):
nt = 2
dec = dec_map[(dct, rdt, type)]
x = np.random.randn(nt, size)
y = dct(x, type=type)
for j in range(nt):
assert_array_almost_equal(y[j], dct(x[j], type=type),
decimal=dec)
x = x.T
y = dct(x, axis=0, type=type)
for j in range(nt):
assert_array_almost_equal(y[:,j], dct(x[:,j], type=type),
decimal=dec)
@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
def test_dct1_definition_ortho(rdt, mdata_x):
# Test orthornomal mode.
dec = dec_map[(dct, rdt, 1)]
x = np.array(mdata_x, dtype=rdt)
dt = np.result_type(np.float32, rdt)
y = dct(x, norm='ortho', type=1)
y2 = naive_dct1(x, norm='ortho')
assert_equal(y.dtype, dt)
assert_allclose(y, y2, rtol=0., atol=np.max(y2)*10**(-dec))
@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
def test_dct2_definition_matlab(mdata_xy, rdt):
# Test correspondence with matlab (orthornomal mode).
dt = np.result_type(np.float32, rdt)
x = np.array(mdata_xy[0], dtype=dt)
yr = mdata_xy[1]
y = dct(x, norm="ortho", type=2)
dec = dec_map[(dct, rdt, 2)]
assert_equal(y.dtype, dt)
assert_array_almost_equal(y, yr, decimal=dec)
@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
def test_dct3_definition_ortho(mdata_x, rdt):
# Test orthornomal mode.
x = np.array(mdata_x, dtype=rdt)
dt = np.result_type(np.float32, rdt)
y = dct(x, norm='ortho', type=2)
xi = dct(y, norm="ortho", type=3)
dec = dec_map[(dct, rdt, 3)]
assert_equal(xi.dtype, dt)
assert_array_almost_equal(xi, x, decimal=dec)
@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
def test_dct4_definition_ortho(mdata_x, rdt):
# Test orthornomal mode.
x = np.array(mdata_x, dtype=rdt)
dt = np.result_type(np.float32, rdt)
y = dct(x, norm='ortho', type=4)
y2 = naive_dct4(x, norm='ortho')
dec = dec_map[(dct, rdt, 4)]
assert_equal(y.dtype, dt)
assert_allclose(y, y2, rtol=0., atol=np.max(y2)*10**(-dec))
@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
@pytest.mark.parametrize('type', [1, 2, 3, 4])
def test_idct_definition(fftwdata_size, rdt, type, reference_data):
xr, yr, dt = fftw_dct_ref(type, fftwdata_size, rdt, reference_data)
x = idct(yr, type=type)
dec = dec_map[(idct, rdt, type)]
assert_equal(x.dtype, dt)
assert_allclose(x, xr, rtol=0., atol=np.max(xr)*10**(-dec))
@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
@pytest.mark.parametrize('type', [1, 2, 3, 4])
def test_definition(fftwdata_size, rdt, type, reference_data):
xr, yr, dt = fftw_dst_ref(type, fftwdata_size, rdt, reference_data)
y = dst(xr, type=type)
dec = dec_map[(dst, rdt, type)]
assert_equal(y.dtype, dt)
assert_allclose(y, yr, rtol=0., atol=np.max(yr)*10**(-dec))
@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
def test_dst1_definition_ortho(rdt, mdata_x):
# Test orthornomal mode.
dec = dec_map[(dst, rdt, 1)]
x = np.array(mdata_x, dtype=rdt)
dt = np.result_type(np.float32, rdt)
y = dst(x, norm='ortho', type=1)
y2 = naive_dst1(x, norm='ortho')
assert_equal(y.dtype, dt)
assert_allclose(y, y2, rtol=0., atol=np.max(y2)*10**(-dec))
@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
def test_dst4_definition_ortho(rdt, mdata_x):
# Test orthornomal mode.
dec = dec_map[(dst, rdt, 4)]
x = np.array(mdata_x, dtype=rdt)
dt = np.result_type(np.float32, rdt)
y = dst(x, norm='ortho', type=4)
y2 = naive_dst4(x, norm='ortho')
assert_equal(y.dtype, dt)
assert_array_almost_equal(y, y2, decimal=dec)
@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
@pytest.mark.parametrize('type', [1, 2, 3, 4])
def test_idst_definition(fftwdata_size, rdt, type, reference_data):
xr, yr, dt = fftw_dst_ref(type, fftwdata_size, rdt, reference_data)
x = idst(yr, type=type)
dec = dec_map[(idst, rdt, type)]
assert_equal(x.dtype, dt)
assert_allclose(x, xr, rtol=0., atol=np.max(xr)*10**(-dec))
@pytest.mark.parametrize('routine', [dct, dst, idct, idst])
@pytest.mark.parametrize('dtype', [np.float32, np.float64, np.longdouble])
@pytest.mark.parametrize('shape, axis', [
((16,), -1), ((16, 2), 0), ((2, 16), 1)
])
@pytest.mark.parametrize('type', [1, 2, 3, 4])
@pytest.mark.parametrize('overwrite_x', [True, False])
@pytest.mark.parametrize('norm', [None, 'ortho'])
def test_overwrite(routine, dtype, shape, axis, type, norm, overwrite_x):
# Check input overwrite behavior
np.random.seed(1234)
if np.issubdtype(dtype, np.complexfloating):
x = np.random.randn(*shape) + 1j*np.random.randn(*shape)
else:
x = np.random.randn(*shape)
x = x.astype(dtype)
x2 = x.copy()
routine(x2, type, None, axis, norm, overwrite_x=overwrite_x)
sig = "{}({}{!r}, {!r}, axis={!r}, overwrite_x={!r})".format(
routine.__name__, x.dtype, x.shape, None, axis, overwrite_x)
if not overwrite_x:
assert_equal(x2, x, err_msg="spurious overwrite in %s" % sig)
class Test_DCTN_IDCTN:
dec = 14
dct_type = [1, 2, 3, 4]
norms = [None, 'backward', 'ortho', 'forward']
rstate = np.random.RandomState(1234)
shape = (32, 16)
data = rstate.randn(*shape)
@pytest.mark.parametrize('fforward,finverse', [(dctn, idctn),
(dstn, idstn)])
@pytest.mark.parametrize('axes', [None,
1, (1,), [1],
0, (0,), [0],
(0, 1), [0, 1],
(-2, -1), [-2, -1]])
@pytest.mark.parametrize('dct_type', dct_type)
@pytest.mark.parametrize('norm', ['ortho'])
def test_axes_round_trip(self, fforward, finverse, axes, dct_type, norm):
tmp = fforward(self.data, type=dct_type, axes=axes, norm=norm)
tmp = finverse(tmp, type=dct_type, axes=axes, norm=norm)
assert_array_almost_equal(self.data, tmp, decimal=12)
@pytest.mark.parametrize('funcn,func', [(dctn, dct), (dstn, dst)])
@pytest.mark.parametrize('dct_type', dct_type)
@pytest.mark.parametrize('norm', norms)
def test_dctn_vs_2d_reference(self, funcn, func, dct_type, norm):
y1 = funcn(self.data, type=dct_type, axes=None, norm=norm)
y2 = ref_2d(func, self.data, type=dct_type, norm=norm)
assert_array_almost_equal(y1, y2, decimal=11)
@pytest.mark.parametrize('funcn,func', [(idctn, idct), (idstn, idst)])
@pytest.mark.parametrize('dct_type', dct_type)
@pytest.mark.parametrize('norm', norms)
def test_idctn_vs_2d_reference(self, funcn, func, dct_type, norm):
fdata = dctn(self.data, type=dct_type, norm=norm)
y1 = funcn(fdata, type=dct_type, norm=norm)
y2 = ref_2d(func, fdata, type=dct_type, norm=norm)
assert_array_almost_equal(y1, y2, decimal=11)
@pytest.mark.parametrize('fforward,finverse', [(dctn, idctn),
(dstn, idstn)])
def test_axes_and_shape(self, fforward, finverse):
with assert_raises(ValueError,
match="when given, axes and shape arguments"
" have to be of the same length"):
fforward(self.data, s=self.data.shape[0], axes=(0, 1))
with assert_raises(ValueError,
match="when given, axes and shape arguments"
" have to be of the same length"):
fforward(self.data, s=self.data.shape, axes=0)
@pytest.mark.parametrize('fforward', [dctn, dstn])
def test_shape(self, fforward):
tmp = fforward(self.data, s=(128, 128), axes=None)
assert_equal(tmp.shape, (128, 128))
@pytest.mark.parametrize('fforward,finverse', [(dctn, idctn),
(dstn, idstn)])
@pytest.mark.parametrize('axes', [1, (1,), [1],
0, (0,), [0]])
def test_shape_is_none_with_axes(self, fforward, finverse, axes):
tmp = fforward(self.data, s=None, axes=axes, norm='ortho')
tmp = finverse(tmp, s=None, axes=axes, norm='ortho')
assert_array_almost_equal(self.data, tmp, decimal=self.dec)
@pytest.mark.parametrize('func', [dct, dctn, idct, idctn,
dst, dstn, idst, idstn])
def test_swapped_byte_order(func):
rng = np.random.RandomState(1234)
x = rng.rand(10)
swapped_dt = x.dtype.newbyteorder('S')
assert_allclose(func(x.astype(swapped_dt)), func(x))

View File

@ -0,0 +1,693 @@
from ._basic import _dispatch
from scipy._lib.uarray import Dispatchable
import numpy as np
__all__ = ['dct', 'idct', 'dst', 'idst', 'dctn', 'idctn', 'dstn', 'idstn']
@_dispatch
def dctn(x, type=2, s=None, axes=None, norm=None, overwrite_x=False,
workers=None, *, orthogonalize=None):
"""
Return multidimensional Discrete Cosine Transform along the specified axes.
Parameters
----------
x : array_like
The input array.
type : {1, 2, 3, 4}, optional
Type of the DCT (see Notes). Default type is 2.
s : int or array_like of ints or None, optional
The shape of the result. If both `s` and `axes` (see below) are None,
`s` is ``x.shape``; if `s` is None but `axes` is not None, then `s` is
``numpy.take(x.shape, axes, axis=0)``.
If ``s[i] > x.shape[i]``, the ith dimension of the input is padded with zeros.
If ``s[i] < x.shape[i]``, the ith dimension of the input is truncated to length
``s[i]``.
If any element of `s` is -1, the size of the corresponding dimension of
`x` is used.
axes : int or array_like of ints or None, optional
Axes over which the DCT is computed. If not given, the last ``len(s)``
axes are used, or all axes if `s` is also not specified.
norm : {"backward", "ortho", "forward"}, optional
Normalization mode (see Notes). Default is "backward".
overwrite_x : bool, optional
If True, the contents of `x` can be destroyed; the default is False.
workers : int, optional
Maximum number of workers to use for parallel computation. If negative,
the value wraps around from ``os.cpu_count()``.
See :func:`~scipy.fft.fft` for more details.
orthogonalize : bool, optional
Whether to use the orthogonalized DCT variant (see Notes).
Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
.. versionadded:: 1.8.0
Returns
-------
y : ndarray of real
The transformed input array.
See Also
--------
idctn : Inverse multidimensional DCT
Notes
-----
For full details of the DCT types and normalization modes, as well as
references, see `dct`.
Examples
--------
>>> import numpy as np
>>> from scipy.fft import dctn, idctn
>>> rng = np.random.default_rng()
>>> y = rng.standard_normal((16, 16))
>>> np.allclose(y, idctn(dctn(y)))
True
"""
return (Dispatchable(x, np.ndarray),)
@_dispatch
def idctn(x, type=2, s=None, axes=None, norm=None, overwrite_x=False,
workers=None, orthogonalize=None):
"""
Return multidimensional Inverse Discrete Cosine Transform along the specified axes.
Parameters
----------
x : array_like
The input array.
type : {1, 2, 3, 4}, optional
Type of the DCT (see Notes). Default type is 2.
s : int or array_like of ints or None, optional
The shape of the result. If both `s` and `axes` (see below) are
None, `s` is ``x.shape``; if `s` is None but `axes` is
not None, then `s` is ``numpy.take(x.shape, axes, axis=0)``.
If ``s[i] > x.shape[i]``, the ith dimension of the input is padded with zeros.
If ``s[i] < x.shape[i]``, the ith dimension of the input is truncated to length
``s[i]``.
If any element of `s` is -1, the size of the corresponding dimension of
`x` is used.
axes : int or array_like of ints or None, optional
Axes over which the IDCT is computed. If not given, the last ``len(s)``
axes are used, or all axes if `s` is also not specified.
norm : {"backward", "ortho", "forward"}, optional
Normalization mode (see Notes). Default is "backward".
overwrite_x : bool, optional
If True, the contents of `x` can be destroyed; the default is False.
workers : int, optional
Maximum number of workers to use for parallel computation. If negative,
the value wraps around from ``os.cpu_count()``.
See :func:`~scipy.fft.fft` for more details.
orthogonalize : bool, optional
Whether to use the orthogonalized IDCT variant (see Notes).
Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
.. versionadded:: 1.8.0
Returns
-------
y : ndarray of real
The transformed input array.
See Also
--------
dctn : multidimensional DCT
Notes
-----
For full details of the IDCT types and normalization modes, as well as
references, see `idct`.
Examples
--------
>>> import numpy as np
>>> from scipy.fft import dctn, idctn
>>> rng = np.random.default_rng()
>>> y = rng.standard_normal((16, 16))
>>> np.allclose(y, idctn(dctn(y)))
True
"""
return (Dispatchable(x, np.ndarray),)
@_dispatch
def dstn(x, type=2, s=None, axes=None, norm=None, overwrite_x=False,
workers=None, orthogonalize=None):
"""
Return multidimensional Discrete Sine Transform along the specified axes.
Parameters
----------
x : array_like
The input array.
type : {1, 2, 3, 4}, optional
Type of the DST (see Notes). Default type is 2.
s : int or array_like of ints or None, optional
The shape of the result. If both `s` and `axes` (see below) are None,
`s` is ``x.shape``; if `s` is None but `axes` is not None, then `s` is
``numpy.take(x.shape, axes, axis=0)``.
If ``s[i] > x.shape[i]``, the ith dimension of the input is padded with zeros.
If ``s[i] < x.shape[i]``, the ith dimension of the input is truncated to length
``s[i]``.
If any element of `shape` is -1, the size of the corresponding dimension
of `x` is used.
axes : int or array_like of ints or None, optional
Axes over which the DST is computed. If not given, the last ``len(s)``
axes are used, or all axes if `s` is also not specified.
norm : {"backward", "ortho", "forward"}, optional
Normalization mode (see Notes). Default is "backward".
overwrite_x : bool, optional
If True, the contents of `x` can be destroyed; the default is False.
workers : int, optional
Maximum number of workers to use for parallel computation. If negative,
the value wraps around from ``os.cpu_count()``.
See :func:`~scipy.fft.fft` for more details.
orthogonalize : bool, optional
Whether to use the orthogonalized DST variant (see Notes).
Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
.. versionadded:: 1.8.0
Returns
-------
y : ndarray of real
The transformed input array.
See Also
--------
idstn : Inverse multidimensional DST
Notes
-----
For full details of the DST types and normalization modes, as well as
references, see `dst`.
Examples
--------
>>> import numpy as np
>>> from scipy.fft import dstn, idstn
>>> rng = np.random.default_rng()
>>> y = rng.standard_normal((16, 16))
>>> np.allclose(y, idstn(dstn(y)))
True
"""
return (Dispatchable(x, np.ndarray),)
@_dispatch
def idstn(x, type=2, s=None, axes=None, norm=None, overwrite_x=False,
workers=None, orthogonalize=None):
"""
Return multidimensional Inverse Discrete Sine Transform along the specified axes.
Parameters
----------
x : array_like
The input array.
type : {1, 2, 3, 4}, optional
Type of the DST (see Notes). Default type is 2.
s : int or array_like of ints or None, optional
The shape of the result. If both `s` and `axes` (see below) are None,
`s` is ``x.shape``; if `s` is None but `axes` is not None, then `s` is
``numpy.take(x.shape, axes, axis=0)``.
If ``s[i] > x.shape[i]``, the ith dimension of the input is padded with zeros.
If ``s[i] < x.shape[i]``, the ith dimension of the input is truncated to length
``s[i]``.
If any element of `s` is -1, the size of the corresponding dimension of
`x` is used.
axes : int or array_like of ints or None, optional
Axes over which the IDST is computed. If not given, the last ``len(s)``
axes are used, or all axes if `s` is also not specified.
norm : {"backward", "ortho", "forward"}, optional
Normalization mode (see Notes). Default is "backward".
overwrite_x : bool, optional
If True, the contents of `x` can be destroyed; the default is False.
workers : int, optional
Maximum number of workers to use for parallel computation. If negative,
the value wraps around from ``os.cpu_count()``.
See :func:`~scipy.fft.fft` for more details.
orthogonalize : bool, optional
Whether to use the orthogonalized IDST variant (see Notes).
Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
.. versionadded:: 1.8.0
Returns
-------
y : ndarray of real
The transformed input array.
See Also
--------
dstn : multidimensional DST
Notes
-----
For full details of the IDST types and normalization modes, as well as
references, see `idst`.
Examples
--------
>>> import numpy as np
>>> from scipy.fft import dstn, idstn
>>> rng = np.random.default_rng()
>>> y = rng.standard_normal((16, 16))
>>> np.allclose(y, idstn(dstn(y)))
True
"""
return (Dispatchable(x, np.ndarray),)
@_dispatch
def dct(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False, workers=None,
orthogonalize=None):
r"""Return the Discrete Cosine Transform of arbitrary type sequence x.
Parameters
----------
x : array_like
The input array.
type : {1, 2, 3, 4}, optional
Type of the DCT (see Notes). Default type is 2.
n : int, optional
Length of the transform. If ``n < x.shape[axis]``, `x` is
truncated. If ``n > x.shape[axis]``, `x` is zero-padded. The
default results in ``n = x.shape[axis]``.
axis : int, optional
Axis along which the dct is computed; the default is over the
last axis (i.e., ``axis=-1``).
norm : {"backward", "ortho", "forward"}, optional
Normalization mode (see Notes). Default is "backward".
overwrite_x : bool, optional
If True, the contents of `x` can be destroyed; the default is False.
workers : int, optional
Maximum number of workers to use for parallel computation. If negative,
the value wraps around from ``os.cpu_count()``.
See :func:`~scipy.fft.fft` for more details.
orthogonalize : bool, optional
Whether to use the orthogonalized DCT variant (see Notes).
Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
.. versionadded:: 1.8.0
Returns
-------
y : ndarray of real
The transformed input array.
See Also
--------
idct : Inverse DCT
Notes
-----
For a single dimension array ``x``, ``dct(x, norm='ortho')`` is equal to
MATLAB ``dct(x)``.
.. warning:: For ``type in {1, 2, 3}``, ``norm="ortho"`` breaks the direct
correspondence with the direct Fourier transform. To recover
it you must specify ``orthogonalize=False``.
For ``norm="ortho"`` both the `dct` and `idct` are scaled by the same
overall factor in both directions. By default, the transform is also
orthogonalized which for types 1, 2 and 3 means the transform definition is
modified to give orthogonality of the DCT matrix (see below).
For ``norm="backward"``, there is no scaling on `dct` and the `idct` is
scaled by ``1/N`` where ``N`` is the "logical" size of the DCT. For
``norm="forward"`` the ``1/N`` normalization is applied to the forward
`dct` instead and the `idct` is unnormalized.
There are, theoretically, 8 types of the DCT, only the first 4 types are
implemented in SciPy.'The' DCT generally refers to DCT type 2, and 'the'
Inverse DCT generally refers to DCT type 3.
**Type I**
There are several definitions of the DCT-I; we use the following
(for ``norm="backward"``)
.. math::
y_k = x_0 + (-1)^k x_{N-1} + 2 \sum_{n=1}^{N-2} x_n \cos\left(
\frac{\pi k n}{N-1} \right)
If ``orthogonalize=True``, ``x[0]`` and ``x[N-1]`` are multiplied by a
scaling factor of :math:`\sqrt{2}`, and ``y[0]`` and ``y[N-1]`` are divided
by :math:`\sqrt{2}`. When combined with ``norm="ortho"``, this makes the
corresponding matrix of coefficients orthonormal (``O @ O.T = np.eye(N)``).
.. note::
The DCT-I is only supported for input size > 1.
**Type II**
There are several definitions of the DCT-II; we use the following
(for ``norm="backward"``)
.. math::
y_k = 2 \sum_{n=0}^{N-1} x_n \cos\left(\frac{\pi k(2n+1)}{2N} \right)
If ``orthogonalize=True``, ``y[0]`` is divided by :math:`\sqrt{2}` which,
when combined with ``norm="ortho"``, makes the corresponding matrix of
coefficients orthonormal (``O @ O.T = np.eye(N)``).
**Type III**
There are several definitions, we use the following (for
``norm="backward"``)
.. math::
y_k = x_0 + 2 \sum_{n=1}^{N-1} x_n \cos\left(\frac{\pi(2k+1)n}{2N}\right)
If ``orthogonalize=True``, ``x[0]`` terms are multiplied by
:math:`\sqrt{2}` which, when combined with ``norm="ortho"``, makes the
corresponding matrix of coefficients orthonormal (``O @ O.T = np.eye(N)``).
The (unnormalized) DCT-III is the inverse of the (unnormalized) DCT-II, up
to a factor `2N`. The orthonormalized DCT-III is exactly the inverse of
the orthonormalized DCT-II.
**Type IV**
There are several definitions of the DCT-IV; we use the following
(for ``norm="backward"``)
.. math::
y_k = 2 \sum_{n=0}^{N-1} x_n \cos\left(\frac{\pi(2k+1)(2n+1)}{4N} \right)
``orthogonalize`` has no effect here, as the DCT-IV matrix is already
orthogonal up to a scale factor of ``2N``.
References
----------
.. [1] 'A Fast Cosine Transform in One and Two Dimensions', by J.
Makhoul, `IEEE Transactions on acoustics, speech and signal
processing` vol. 28(1), pp. 27-34,
:doi:`10.1109/TASSP.1980.1163351` (1980).
.. [2] Wikipedia, "Discrete cosine transform",
https://en.wikipedia.org/wiki/Discrete_cosine_transform
Examples
--------
The Type 1 DCT is equivalent to the FFT (though faster) for real,
even-symmetrical inputs. The output is also real and even-symmetrical.
Half of the FFT input is used to generate half of the FFT output:
>>> from scipy.fft import fft, dct
>>> import numpy as np
>>> fft(np.array([4., 3., 5., 10., 5., 3.])).real
array([ 30., -8., 6., -2., 6., -8.])
>>> dct(np.array([4., 3., 5., 10.]), 1)
array([ 30., -8., 6., -2.])
"""
return (Dispatchable(x, np.ndarray),)
@_dispatch
def idct(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False,
workers=None, orthogonalize=None):
"""
Return the Inverse Discrete Cosine Transform of an arbitrary type sequence.
Parameters
----------
x : array_like
The input array.
type : {1, 2, 3, 4}, optional
Type of the DCT (see Notes). Default type is 2.
n : int, optional
Length of the transform. If ``n < x.shape[axis]``, `x` is
truncated. If ``n > x.shape[axis]``, `x` is zero-padded. The
default results in ``n = x.shape[axis]``.
axis : int, optional
Axis along which the idct is computed; the default is over the
last axis (i.e., ``axis=-1``).
norm : {"backward", "ortho", "forward"}, optional
Normalization mode (see Notes). Default is "backward".
overwrite_x : bool, optional
If True, the contents of `x` can be destroyed; the default is False.
workers : int, optional
Maximum number of workers to use for parallel computation. If negative,
the value wraps around from ``os.cpu_count()``.
See :func:`~scipy.fft.fft` for more details.
orthogonalize : bool, optional
Whether to use the orthogonalized IDCT variant (see Notes).
Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
.. versionadded:: 1.8.0
Returns
-------
idct : ndarray of real
The transformed input array.
See Also
--------
dct : Forward DCT
Notes
-----
For a single dimension array `x`, ``idct(x, norm='ortho')`` is equal to
MATLAB ``idct(x)``.
.. warning:: For ``type in {1, 2, 3}``, ``norm="ortho"`` breaks the direct
correspondence with the inverse direct Fourier transform. To
recover it you must specify ``orthogonalize=False``.
For ``norm="ortho"`` both the `dct` and `idct` are scaled by the same
overall factor in both directions. By default, the transform is also
orthogonalized which for types 1, 2 and 3 means the transform definition is
modified to give orthogonality of the IDCT matrix (see `dct` for the full
definitions).
'The' IDCT is the IDCT-II, which is the same as the normalized DCT-III.
The IDCT is equivalent to a normal DCT except for the normalization and
type. DCT type 1 and 4 are their own inverse and DCTs 2 and 3 are each
other's inverses.
Examples
--------
The Type 1 DCT is equivalent to the DFT for real, even-symmetrical
inputs. The output is also real and even-symmetrical. Half of the IFFT
input is used to generate half of the IFFT output:
>>> from scipy.fft import ifft, idct
>>> import numpy as np
>>> ifft(np.array([ 30., -8., 6., -2., 6., -8.])).real
array([ 4., 3., 5., 10., 5., 3.])
>>> idct(np.array([ 30., -8., 6., -2.]), 1)
array([ 4., 3., 5., 10.])
"""
return (Dispatchable(x, np.ndarray),)
@_dispatch
def dst(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False, workers=None,
orthogonalize=None):
r"""
Return the Discrete Sine Transform of arbitrary type sequence x.
Parameters
----------
x : array_like
The input array.
type : {1, 2, 3, 4}, optional
Type of the DST (see Notes). Default type is 2.
n : int, optional
Length of the transform. If ``n < x.shape[axis]``, `x` is
truncated. If ``n > x.shape[axis]``, `x` is zero-padded. The
default results in ``n = x.shape[axis]``.
axis : int, optional
Axis along which the dst is computed; the default is over the
last axis (i.e., ``axis=-1``).
norm : {"backward", "ortho", "forward"}, optional
Normalization mode (see Notes). Default is "backward".
overwrite_x : bool, optional
If True, the contents of `x` can be destroyed; the default is False.
workers : int, optional
Maximum number of workers to use for parallel computation. If negative,
the value wraps around from ``os.cpu_count()``.
See :func:`~scipy.fft.fft` for more details.
orthogonalize : bool, optional
Whether to use the orthogonalized DST variant (see Notes).
Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
.. versionadded:: 1.8.0
Returns
-------
dst : ndarray of reals
The transformed input array.
See Also
--------
idst : Inverse DST
Notes
-----
.. warning:: For ``type in {2, 3}``, ``norm="ortho"`` breaks the direct
correspondence with the direct Fourier transform. To recover
it you must specify ``orthogonalize=False``.
For ``norm="ortho"`` both the `dst` and `idst` are scaled by the same
overall factor in both directions. By default, the transform is also
orthogonalized which for types 2 and 3 means the transform definition is
modified to give orthogonality of the DST matrix (see below).
For ``norm="backward"``, there is no scaling on the `dst` and the `idst` is
scaled by ``1/N`` where ``N`` is the "logical" size of the DST.
There are, theoretically, 8 types of the DST for different combinations of
even/odd boundary conditions and boundary off sets [1]_, only the first
4 types are implemented in SciPy.
**Type I**
There are several definitions of the DST-I; we use the following for
``norm="backward"``. DST-I assumes the input is odd around :math:`n=-1` and
:math:`n=N`.
.. math::
y_k = 2 \sum_{n=0}^{N-1} x_n \sin\left(\frac{\pi(k+1)(n+1)}{N+1}\right)
Note that the DST-I is only supported for input size > 1.
The (unnormalized) DST-I is its own inverse, up to a factor :math:`2(N+1)`.
The orthonormalized DST-I is exactly its own inverse.
``orthogonalize`` has no effect here, as the DST-I matrix is already
orthogonal up to a scale factor of ``2N``.
**Type II**
There are several definitions of the DST-II; we use the following for
``norm="backward"``. DST-II assumes the input is odd around :math:`n=-1/2` and
:math:`n=N-1/2`; the output is odd around :math:`k=-1` and even around :math:`k=N-1`
.. math::
y_k = 2 \sum_{n=0}^{N-1} x_n \sin\left(\frac{\pi(k+1)(2n+1)}{2N}\right)
If ``orthogonalize=True``, ``y[-1]`` is divided :math:`\sqrt{2}` which, when
combined with ``norm="ortho"``, makes the corresponding matrix of
coefficients orthonormal (``O @ O.T = np.eye(N)``).
**Type III**
There are several definitions of the DST-III, we use the following (for
``norm="backward"``). DST-III assumes the input is odd around :math:`n=-1` and
even around :math:`n=N-1`
.. math::
y_k = (-1)^k x_{N-1} + 2 \sum_{n=0}^{N-2} x_n \sin\left(
\frac{\pi(2k+1)(n+1)}{2N}\right)
If ``orthogonalize=True``, ``x[-1]`` is multiplied by :math:`\sqrt{2}`
which, when combined with ``norm="ortho"``, makes the corresponding matrix
of coefficients orthonormal (``O @ O.T = np.eye(N)``).
The (unnormalized) DST-III is the inverse of the (unnormalized) DST-II, up
to a factor :math:`2N`. The orthonormalized DST-III is exactly the inverse of the
orthonormalized DST-II.
**Type IV**
There are several definitions of the DST-IV, we use the following (for
``norm="backward"``). DST-IV assumes the input is odd around :math:`n=-0.5` and
even around :math:`n=N-0.5`
.. math::
y_k = 2 \sum_{n=0}^{N-1} x_n \sin\left(\frac{\pi(2k+1)(2n+1)}{4N}\right)
``orthogonalize`` has no effect here, as the DST-IV matrix is already
orthogonal up to a scale factor of ``2N``.
The (unnormalized) DST-IV is its own inverse, up to a factor :math:`2N`. The
orthonormalized DST-IV is exactly its own inverse.
References
----------
.. [1] Wikipedia, "Discrete sine transform",
https://en.wikipedia.org/wiki/Discrete_sine_transform
"""
return (Dispatchable(x, np.ndarray),)
@_dispatch
def idst(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False,
workers=None, orthogonalize=None):
"""
Return the Inverse Discrete Sine Transform of an arbitrary type sequence.
Parameters
----------
x : array_like
The input array.
type : {1, 2, 3, 4}, optional
Type of the DST (see Notes). Default type is 2.
n : int, optional
Length of the transform. If ``n < x.shape[axis]``, `x` is
truncated. If ``n > x.shape[axis]``, `x` is zero-padded. The
default results in ``n = x.shape[axis]``.
axis : int, optional
Axis along which the idst is computed; the default is over the
last axis (i.e., ``axis=-1``).
norm : {"backward", "ortho", "forward"}, optional
Normalization mode (see Notes). Default is "backward".
overwrite_x : bool, optional
If True, the contents of `x` can be destroyed; the default is False.
workers : int, optional
Maximum number of workers to use for parallel computation. If negative,
the value wraps around from ``os.cpu_count()``.
See :func:`~scipy.fft.fft` for more details.
orthogonalize : bool, optional
Whether to use the orthogonalized IDST variant (see Notes).
Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
.. versionadded:: 1.8.0
Returns
-------
idst : ndarray of real
The transformed input array.
See Also
--------
dst : Forward DST
Notes
-----
.. warning:: For ``type in {2, 3}``, ``norm="ortho"`` breaks the direct
correspondence with the inverse direct Fourier transform.
For ``norm="ortho"`` both the `dst` and `idst` are scaled by the same
overall factor in both directions. By default, the transform is also
orthogonalized which for types 2 and 3 means the transform definition is
modified to give orthogonality of the DST matrix (see `dst` for the full
definitions).
'The' IDST is the IDST-II, which is the same as the normalized DST-III.
The IDST is equivalent to a normal DST except for the normalization and
type. DST type 1 and 4 are their own inverse and DSTs 2 and 3 are each
other's inverses.
"""
return (Dispatchable(x, np.ndarray),)

View File

@ -0,0 +1,63 @@
from scipy._lib._array_api import array_namespace
import numpy as np
from . import _pocketfft
__all__ = ['dct', 'idct', 'dst', 'idst', 'dctn', 'idctn', 'dstn', 'idstn']
def _execute(pocketfft_func, x, type, s, axes, norm,
overwrite_x, workers, orthogonalize):
xp = array_namespace(x)
x = np.asarray(x)
y = pocketfft_func(x, type, s, axes, norm,
overwrite_x=overwrite_x, workers=workers,
orthogonalize=orthogonalize)
return xp.asarray(y)
def dctn(x, type=2, s=None, axes=None, norm=None,
overwrite_x=False, workers=None, *, orthogonalize=None):
return _execute(_pocketfft.dctn, x, type, s, axes, norm,
overwrite_x, workers, orthogonalize)
def idctn(x, type=2, s=None, axes=None, norm=None,
overwrite_x=False, workers=None, *, orthogonalize=None):
return _execute(_pocketfft.idctn, x, type, s, axes, norm,
overwrite_x, workers, orthogonalize)
def dstn(x, type=2, s=None, axes=None, norm=None,
overwrite_x=False, workers=None, orthogonalize=None):
return _execute(_pocketfft.dstn, x, type, s, axes, norm,
overwrite_x, workers, orthogonalize)
def idstn(x, type=2, s=None, axes=None, norm=None,
overwrite_x=False, workers=None, *, orthogonalize=None):
return _execute(_pocketfft.idstn, x, type, s, axes, norm,
overwrite_x, workers, orthogonalize)
def dct(x, type=2, n=None, axis=-1, norm=None,
overwrite_x=False, workers=None, orthogonalize=None):
return _execute(_pocketfft.dct, x, type, n, axis, norm,
overwrite_x, workers, orthogonalize)
def idct(x, type=2, n=None, axis=-1, norm=None,
overwrite_x=False, workers=None, orthogonalize=None):
return _execute(_pocketfft.idct, x, type, n, axis, norm,
overwrite_x, workers, orthogonalize)
def dst(x, type=2, n=None, axis=-1, norm=None,
overwrite_x=False, workers=None, orthogonalize=None):
return _execute(_pocketfft.dst, x, type, n, axis, norm,
overwrite_x, workers, orthogonalize)
def idst(x, type=2, n=None, axis=-1, norm=None,
overwrite_x=False, workers=None, orthogonalize=None):
return _execute(_pocketfft.idst, x, type, n, axis, norm,
overwrite_x, workers, orthogonalize)

View File

@ -0,0 +1,92 @@
import numpy as np
import scipy.fft
class _MockFunction:
def __init__(self, return_value = None):
self.number_calls = 0
self.return_value = return_value
self.last_args = ([], {})
def __call__(self, *args, **kwargs):
self.number_calls += 1
self.last_args = (args, kwargs)
return self.return_value
fft = _MockFunction(np.random.random(10))
fft2 = _MockFunction(np.random.random(10))
fftn = _MockFunction(np.random.random(10))
ifft = _MockFunction(np.random.random(10))
ifft2 = _MockFunction(np.random.random(10))
ifftn = _MockFunction(np.random.random(10))
rfft = _MockFunction(np.random.random(10))
rfft2 = _MockFunction(np.random.random(10))
rfftn = _MockFunction(np.random.random(10))
irfft = _MockFunction(np.random.random(10))
irfft2 = _MockFunction(np.random.random(10))
irfftn = _MockFunction(np.random.random(10))
hfft = _MockFunction(np.random.random(10))
hfft2 = _MockFunction(np.random.random(10))
hfftn = _MockFunction(np.random.random(10))
ihfft = _MockFunction(np.random.random(10))
ihfft2 = _MockFunction(np.random.random(10))
ihfftn = _MockFunction(np.random.random(10))
dct = _MockFunction(np.random.random(10))
idct = _MockFunction(np.random.random(10))
dctn = _MockFunction(np.random.random(10))
idctn = _MockFunction(np.random.random(10))
dst = _MockFunction(np.random.random(10))
idst = _MockFunction(np.random.random(10))
dstn = _MockFunction(np.random.random(10))
idstn = _MockFunction(np.random.random(10))
fht = _MockFunction(np.random.random(10))
ifht = _MockFunction(np.random.random(10))
__ua_domain__ = "numpy.scipy.fft"
_implements = {
scipy.fft.fft: fft,
scipy.fft.fft2: fft2,
scipy.fft.fftn: fftn,
scipy.fft.ifft: ifft,
scipy.fft.ifft2: ifft2,
scipy.fft.ifftn: ifftn,
scipy.fft.rfft: rfft,
scipy.fft.rfft2: rfft2,
scipy.fft.rfftn: rfftn,
scipy.fft.irfft: irfft,
scipy.fft.irfft2: irfft2,
scipy.fft.irfftn: irfftn,
scipy.fft.hfft: hfft,
scipy.fft.hfft2: hfft2,
scipy.fft.hfftn: hfftn,
scipy.fft.ihfft: ihfft,
scipy.fft.ihfft2: ihfft2,
scipy.fft.ihfftn: ihfftn,
scipy.fft.dct: dct,
scipy.fft.idct: idct,
scipy.fft.dctn: dctn,
scipy.fft.idctn: idctn,
scipy.fft.dst: dst,
scipy.fft.idst: idst,
scipy.fft.dstn: dstn,
scipy.fft.idstn: idstn,
scipy.fft.fht: fht,
scipy.fft.ifht: ifht
}
def __ua_function__(method, args, kwargs):
fn = _implements.get(method)
return (fn(*args, **kwargs) if fn is not None
else NotImplemented)

View File

@ -0,0 +1,98 @@
from functools import partial
import numpy as np
import scipy.fft
from scipy.fft import _fftlog, _pocketfft, set_backend
from scipy.fft.tests import mock_backend
from numpy.testing import assert_allclose, assert_equal
import pytest
fnames = ('fft', 'fft2', 'fftn',
'ifft', 'ifft2', 'ifftn',
'rfft', 'rfft2', 'rfftn',
'irfft', 'irfft2', 'irfftn',
'dct', 'idct', 'dctn', 'idctn',
'dst', 'idst', 'dstn', 'idstn',
'fht', 'ifht')
np_funcs = (np.fft.fft, np.fft.fft2, np.fft.fftn,
np.fft.ifft, np.fft.ifft2, np.fft.ifftn,
np.fft.rfft, np.fft.rfft2, np.fft.rfftn,
np.fft.irfft, np.fft.irfft2, np.fft.irfftn,
np.fft.hfft, _pocketfft.hfft2, _pocketfft.hfftn, # np has no hfftn
np.fft.ihfft, _pocketfft.ihfft2, _pocketfft.ihfftn,
_pocketfft.dct, _pocketfft.idct, _pocketfft.dctn, _pocketfft.idctn,
_pocketfft.dst, _pocketfft.idst, _pocketfft.dstn, _pocketfft.idstn,
# must provide required kwargs for fht, ifht
partial(_fftlog.fht, dln=2, mu=0.5),
partial(_fftlog.ifht, dln=2, mu=0.5))
funcs = (scipy.fft.fft, scipy.fft.fft2, scipy.fft.fftn,
scipy.fft.ifft, scipy.fft.ifft2, scipy.fft.ifftn,
scipy.fft.rfft, scipy.fft.rfft2, scipy.fft.rfftn,
scipy.fft.irfft, scipy.fft.irfft2, scipy.fft.irfftn,
scipy.fft.hfft, scipy.fft.hfft2, scipy.fft.hfftn,
scipy.fft.ihfft, scipy.fft.ihfft2, scipy.fft.ihfftn,
scipy.fft.dct, scipy.fft.idct, scipy.fft.dctn, scipy.fft.idctn,
scipy.fft.dst, scipy.fft.idst, scipy.fft.dstn, scipy.fft.idstn,
# must provide required kwargs for fht, ifht
partial(scipy.fft.fht, dln=2, mu=0.5),
partial(scipy.fft.ifht, dln=2, mu=0.5))
mocks = (mock_backend.fft, mock_backend.fft2, mock_backend.fftn,
mock_backend.ifft, mock_backend.ifft2, mock_backend.ifftn,
mock_backend.rfft, mock_backend.rfft2, mock_backend.rfftn,
mock_backend.irfft, mock_backend.irfft2, mock_backend.irfftn,
mock_backend.hfft, mock_backend.hfft2, mock_backend.hfftn,
mock_backend.ihfft, mock_backend.ihfft2, mock_backend.ihfftn,
mock_backend.dct, mock_backend.idct,
mock_backend.dctn, mock_backend.idctn,
mock_backend.dst, mock_backend.idst,
mock_backend.dstn, mock_backend.idstn,
mock_backend.fht, mock_backend.ifht)
@pytest.mark.parametrize("func, np_func, mock", zip(funcs, np_funcs, mocks))
def test_backend_call(func, np_func, mock):
x = np.arange(20).reshape((10,2))
answer = np_func(x.astype(np.float64))
assert_allclose(func(x), answer, atol=1e-10)
with set_backend(mock_backend, only=True):
mock.number_calls = 0
y = func(x)
assert_equal(y, mock.return_value)
assert_equal(mock.number_calls, 1)
assert_allclose(func(x), answer, atol=1e-10)
plan_funcs = (scipy.fft.fft, scipy.fft.fft2, scipy.fft.fftn,
scipy.fft.ifft, scipy.fft.ifft2, scipy.fft.ifftn,
scipy.fft.rfft, scipy.fft.rfft2, scipy.fft.rfftn,
scipy.fft.irfft, scipy.fft.irfft2, scipy.fft.irfftn,
scipy.fft.hfft, scipy.fft.hfft2, scipy.fft.hfftn,
scipy.fft.ihfft, scipy.fft.ihfft2, scipy.fft.ihfftn)
plan_mocks = (mock_backend.fft, mock_backend.fft2, mock_backend.fftn,
mock_backend.ifft, mock_backend.ifft2, mock_backend.ifftn,
mock_backend.rfft, mock_backend.rfft2, mock_backend.rfftn,
mock_backend.irfft, mock_backend.irfft2, mock_backend.irfftn,
mock_backend.hfft, mock_backend.hfft2, mock_backend.hfftn,
mock_backend.ihfft, mock_backend.ihfft2, mock_backend.ihfftn)
@pytest.mark.parametrize("func, mock", zip(plan_funcs, plan_mocks))
def test_backend_plan(func, mock):
x = np.arange(20).reshape((10, 2))
with pytest.raises(NotImplementedError, match='precomputed plan'):
func(x, plan='foo')
with set_backend(mock_backend, only=True):
mock.number_calls = 0
y = func(x, plan='foo')
assert_equal(y, mock.return_value)
assert_equal(mock.number_calls, 1)
assert_equal(mock.last_args[1]['plan'], 'foo')

View File

@ -0,0 +1,491 @@
import queue
import threading
import multiprocessing
import numpy as np
import pytest
from numpy.random import random
from numpy.testing import assert_array_almost_equal, assert_allclose
from pytest import raises as assert_raises
import scipy.fft as fft
from scipy.conftest import array_api_compatible
from scipy._lib._array_api import (
array_namespace, size, xp_assert_close, xp_assert_equal
)
pytestmark = [array_api_compatible, pytest.mark.usefixtures("skip_xp_backends")]
skip_xp_backends = pytest.mark.skip_xp_backends
# Expected input dtypes. Note that `scipy.fft` is more flexible for numpy,
# but for C2C transforms like `fft.fft`, the array API standard only mandates
# that complex dtypes should work, float32/float64 aren't guaranteed to.
def get_expected_input_dtype(func, xp):
if func in [fft.fft, fft.fftn, fft.fft2,
fft.ifft, fft.ifftn, fft.ifft2,
fft.hfft, fft.hfftn, fft.hfft2,
fft.irfft, fft.irfftn, fft.irfft2]:
dtype = xp.complex128
elif func in [fft.rfft, fft.rfftn, fft.rfft2,
fft.ihfft, fft.ihfftn, fft.ihfft2]:
dtype = xp.float64
else:
raise ValueError(f'Unknown FFT function: {func}')
return dtype
def fft1(x):
L = len(x)
phase = -2j*np.pi*(np.arange(L)/float(L))
phase = np.arange(L).reshape(-1, 1) * phase
return np.sum(x*np.exp(phase), axis=1)
class TestFFT:
def test_identity(self, xp):
maxlen = 512
x = xp.asarray(random(maxlen) + 1j*random(maxlen))
xr = xp.asarray(random(maxlen))
# Check some powers of 2 and some primes
for i in [1, 2, 16, 128, 512, 53, 149, 281, 397]:
xp_assert_close(fft.ifft(fft.fft(x[0:i])), x[0:i])
xp_assert_close(fft.irfft(fft.rfft(xr[0:i]), i), xr[0:i])
@skip_xp_backends(np_only=True, reasons=['significant overhead for some backends'])
def test_identity_extensive(self, xp):
maxlen = 512
x = xp.asarray(random(maxlen) + 1j*random(maxlen))
xr = xp.asarray(random(maxlen))
for i in range(1, maxlen):
xp_assert_close(fft.ifft(fft.fft(x[0:i])), x[0:i])
xp_assert_close(fft.irfft(fft.rfft(xr[0:i]), i), xr[0:i])
def test_fft(self, xp):
x = random(30) + 1j*random(30)
expect = xp.asarray(fft1(x))
x = xp.asarray(x)
xp_assert_close(fft.fft(x), expect)
xp_assert_close(fft.fft(x, norm="backward"), expect)
xp_assert_close(fft.fft(x, norm="ortho"),
expect / xp.sqrt(xp.asarray(30, dtype=xp.float64)),)
xp_assert_close(fft.fft(x, norm="forward"), expect / 30)
@skip_xp_backends(np_only=True, reasons=['some backends allow `n=0`'])
def test_fft_n(self, xp):
x = xp.asarray([1, 2, 3], dtype=xp.complex128)
assert_raises(ValueError, fft.fft, x, 0)
def test_ifft(self, xp):
x = xp.asarray(random(30) + 1j*random(30))
xp_assert_close(fft.ifft(fft.fft(x)), x)
for norm in ["backward", "ortho", "forward"]:
xp_assert_close(fft.ifft(fft.fft(x, norm=norm), norm=norm), x)
def test_fft2(self, xp):
x = xp.asarray(random((30, 20)) + 1j*random((30, 20)))
expect = fft.fft(fft.fft(x, axis=1), axis=0)
xp_assert_close(fft.fft2(x), expect)
xp_assert_close(fft.fft2(x, norm="backward"), expect)
xp_assert_close(fft.fft2(x, norm="ortho"),
expect / xp.sqrt(xp.asarray(30 * 20, dtype=xp.float64)))
xp_assert_close(fft.fft2(x, norm="forward"), expect / (30 * 20))
def test_ifft2(self, xp):
x = xp.asarray(random((30, 20)) + 1j*random((30, 20)))
expect = fft.ifft(fft.ifft(x, axis=1), axis=0)
xp_assert_close(fft.ifft2(x), expect)
xp_assert_close(fft.ifft2(x, norm="backward"), expect)
xp_assert_close(fft.ifft2(x, norm="ortho"),
expect * xp.sqrt(xp.asarray(30 * 20, dtype=xp.float64)))
xp_assert_close(fft.ifft2(x, norm="forward"), expect * (30 * 20))
def test_fftn(self, xp):
x = xp.asarray(random((30, 20, 10)) + 1j*random((30, 20, 10)))
expect = fft.fft(fft.fft(fft.fft(x, axis=2), axis=1), axis=0)
xp_assert_close(fft.fftn(x), expect)
xp_assert_close(fft.fftn(x, norm="backward"), expect)
xp_assert_close(fft.fftn(x, norm="ortho"),
expect / xp.sqrt(xp.asarray(30 * 20 * 10, dtype=xp.float64)))
xp_assert_close(fft.fftn(x, norm="forward"), expect / (30 * 20 * 10))
def test_ifftn(self, xp):
x = xp.asarray(random((30, 20, 10)) + 1j*random((30, 20, 10)))
expect = fft.ifft(fft.ifft(fft.ifft(x, axis=2), axis=1), axis=0)
xp_assert_close(fft.ifftn(x), expect, rtol=1e-7)
xp_assert_close(fft.ifftn(x, norm="backward"), expect, rtol=1e-7)
xp_assert_close(
fft.ifftn(x, norm="ortho"),
fft.ifftn(x) * xp.sqrt(xp.asarray(30 * 20 * 10, dtype=xp.float64))
)
xp_assert_close(fft.ifftn(x, norm="forward"),
expect * (30 * 20 * 10),
rtol=1e-7)
def test_rfft(self, xp):
x = xp.asarray(random(29), dtype=xp.float64)
for n in [size(x), 2*size(x)]:
for norm in [None, "backward", "ortho", "forward"]:
xp_assert_close(fft.rfft(x, n=n, norm=norm),
fft.fft(xp.asarray(x, dtype=xp.complex128),
n=n, norm=norm)[:(n//2 + 1)])
xp_assert_close(
fft.rfft(x, n=n, norm="ortho"),
fft.rfft(x, n=n) / xp.sqrt(xp.asarray(n, dtype=xp.float64))
)
def test_irfft(self, xp):
x = xp.asarray(random(30))
xp_assert_close(fft.irfft(fft.rfft(x)), x)
for norm in ["backward", "ortho", "forward"]:
xp_assert_close(fft.irfft(fft.rfft(x, norm=norm), norm=norm), x)
def test_rfft2(self, xp):
x = xp.asarray(random((30, 20)), dtype=xp.float64)
expect = fft.fft2(xp.asarray(x, dtype=xp.complex128))[:, :11]
xp_assert_close(fft.rfft2(x), expect)
xp_assert_close(fft.rfft2(x, norm="backward"), expect)
xp_assert_close(fft.rfft2(x, norm="ortho"),
expect / xp.sqrt(xp.asarray(30 * 20, dtype=xp.float64)))
xp_assert_close(fft.rfft2(x, norm="forward"), expect / (30 * 20))
def test_irfft2(self, xp):
x = xp.asarray(random((30, 20)))
xp_assert_close(fft.irfft2(fft.rfft2(x)), x)
for norm in ["backward", "ortho", "forward"]:
xp_assert_close(fft.irfft2(fft.rfft2(x, norm=norm), norm=norm), x)
def test_rfftn(self, xp):
x = xp.asarray(random((30, 20, 10)), dtype=xp.float64)
expect = fft.fftn(xp.asarray(x, dtype=xp.complex128))[:, :, :6]
xp_assert_close(fft.rfftn(x), expect)
xp_assert_close(fft.rfftn(x, norm="backward"), expect)
xp_assert_close(fft.rfftn(x, norm="ortho"),
expect / xp.sqrt(xp.asarray(30 * 20 * 10, dtype=xp.float64)))
xp_assert_close(fft.rfftn(x, norm="forward"), expect / (30 * 20 * 10))
def test_irfftn(self, xp):
x = xp.asarray(random((30, 20, 10)))
xp_assert_close(fft.irfftn(fft.rfftn(x)), x)
for norm in ["backward", "ortho", "forward"]:
xp_assert_close(fft.irfftn(fft.rfftn(x, norm=norm), norm=norm), x)
def test_hfft(self, xp):
x = random(14) + 1j*random(14)
x_herm = np.concatenate((random(1), x, random(1)))
x = np.concatenate((x_herm, x[::-1].conj()))
x = xp.asarray(x)
x_herm = xp.asarray(x_herm)
expect = xp.real(fft.fft(x))
xp_assert_close(fft.hfft(x_herm), expect)
xp_assert_close(fft.hfft(x_herm, norm="backward"), expect)
xp_assert_close(fft.hfft(x_herm, norm="ortho"),
expect / xp.sqrt(xp.asarray(30, dtype=xp.float64)))
xp_assert_close(fft.hfft(x_herm, norm="forward"), expect / 30)
def test_ihfft(self, xp):
x = random(14) + 1j*random(14)
x_herm = np.concatenate((random(1), x, random(1)))
x = np.concatenate((x_herm, x[::-1].conj()))
x = xp.asarray(x)
x_herm = xp.asarray(x_herm)
xp_assert_close(fft.ihfft(fft.hfft(x_herm)), x_herm)
for norm in ["backward", "ortho", "forward"]:
xp_assert_close(fft.ihfft(fft.hfft(x_herm, norm=norm), norm=norm), x_herm)
def test_hfft2(self, xp):
x = xp.asarray(random((30, 20)))
xp_assert_close(fft.hfft2(fft.ihfft2(x)), x)
for norm in ["backward", "ortho", "forward"]:
xp_assert_close(fft.hfft2(fft.ihfft2(x, norm=norm), norm=norm), x)
def test_ihfft2(self, xp):
x = xp.asarray(random((30, 20)), dtype=xp.float64)
expect = fft.ifft2(xp.asarray(x, dtype=xp.complex128))[:, :11]
xp_assert_close(fft.ihfft2(x), expect)
xp_assert_close(fft.ihfft2(x, norm="backward"), expect)
xp_assert_close(
fft.ihfft2(x, norm="ortho"),
expect * xp.sqrt(xp.asarray(30 * 20, dtype=xp.float64))
)
xp_assert_close(fft.ihfft2(x, norm="forward"), expect * (30 * 20))
def test_hfftn(self, xp):
x = xp.asarray(random((30, 20, 10)))
xp_assert_close(fft.hfftn(fft.ihfftn(x)), x)
for norm in ["backward", "ortho", "forward"]:
xp_assert_close(fft.hfftn(fft.ihfftn(x, norm=norm), norm=norm), x)
def test_ihfftn(self, xp):
x = xp.asarray(random((30, 20, 10)), dtype=xp.float64)
expect = fft.ifftn(xp.asarray(x, dtype=xp.complex128))[:, :, :6]
xp_assert_close(expect, fft.ihfftn(x))
xp_assert_close(expect, fft.ihfftn(x, norm="backward"))
xp_assert_close(
fft.ihfftn(x, norm="ortho"),
expect * xp.sqrt(xp.asarray(30 * 20 * 10, dtype=xp.float64))
)
xp_assert_close(fft.ihfftn(x, norm="forward"), expect * (30 * 20 * 10))
def _check_axes(self, op, xp):
dtype = get_expected_input_dtype(op, xp)
x = xp.asarray(random((30, 20, 10)), dtype=dtype)
axes = [(0, 1, 2), (0, 2, 1), (1, 0, 2), (1, 2, 0), (2, 0, 1), (2, 1, 0)]
xp_test = array_namespace(x)
for a in axes:
op_tr = op(xp_test.permute_dims(x, axes=a))
tr_op = xp_test.permute_dims(op(x, axes=a), axes=a)
xp_assert_close(op_tr, tr_op)
@pytest.mark.parametrize("op", [fft.fftn, fft.ifftn, fft.rfftn, fft.irfftn])
def test_axes_standard(self, op, xp):
self._check_axes(op, xp)
@pytest.mark.parametrize("op", [fft.hfftn, fft.ihfftn])
def test_axes_non_standard(self, op, xp):
self._check_axes(op, xp)
@pytest.mark.parametrize("op", [fft.fftn, fft.ifftn,
fft.rfftn, fft.irfftn])
def test_axes_subset_with_shape_standard(self, op, xp):
dtype = get_expected_input_dtype(op, xp)
x = xp.asarray(random((16, 8, 4)), dtype=dtype)
axes = [(0, 1, 2), (0, 2, 1), (1, 2, 0)]
xp_test = array_namespace(x)
for a in axes:
# different shape on the first two axes
shape = tuple([2*x.shape[ax] if ax in a[:2] else x.shape[ax]
for ax in range(x.ndim)])
# transform only the first two axes
op_tr = op(xp_test.permute_dims(x, axes=a),
s=shape[:2], axes=(0, 1))
tr_op = xp_test.permute_dims(op(x, s=shape[:2], axes=a[:2]),
axes=a)
xp_assert_close(op_tr, tr_op)
@pytest.mark.parametrize("op", [fft.fft2, fft.ifft2,
fft.rfft2, fft.irfft2,
fft.hfft2, fft.ihfft2,
fft.hfftn, fft.ihfftn])
def test_axes_subset_with_shape_non_standard(self, op, xp):
dtype = get_expected_input_dtype(op, xp)
x = xp.asarray(random((16, 8, 4)), dtype=dtype)
axes = [(0, 1, 2), (0, 2, 1), (1, 2, 0)]
xp_test = array_namespace(x)
for a in axes:
# different shape on the first two axes
shape = tuple([2*x.shape[ax] if ax in a[:2] else x.shape[ax]
for ax in range(x.ndim)])
# transform only the first two axes
op_tr = op(xp_test.permute_dims(x, axes=a), s=shape[:2], axes=(0, 1))
tr_op = xp_test.permute_dims(op(x, s=shape[:2], axes=a[:2]), axes=a)
xp_assert_close(op_tr, tr_op)
def test_all_1d_norm_preserving(self, xp):
# verify that round-trip transforms are norm-preserving
x = xp.asarray(random(30), dtype=xp.float64)
xp_test = array_namespace(x)
x_norm = xp_test.linalg.vector_norm(x)
n = size(x) * 2
func_pairs = [(fft.rfft, fft.irfft),
# hfft: order so the first function takes x.size samples
# (necessary for comparison to x_norm above)
(fft.ihfft, fft.hfft),
# functions that expect complex dtypes at the end
(fft.fft, fft.ifft),
]
for forw, back in func_pairs:
if forw == fft.fft:
x = xp.asarray(x, dtype=xp.complex128)
x_norm = xp_test.linalg.vector_norm(x)
for n in [size(x), 2*size(x)]:
for norm in ['backward', 'ortho', 'forward']:
tmp = forw(x, n=n, norm=norm)
tmp = back(tmp, n=n, norm=norm)
xp_assert_close(xp_test.linalg.vector_norm(tmp), x_norm)
@skip_xp_backends(np_only=True)
@pytest.mark.parametrize("dtype", [np.float16, np.longdouble])
def test_dtypes_nonstandard(self, dtype):
x = random(30).astype(dtype)
out_dtypes = {np.float16: np.complex64, np.longdouble: np.clongdouble}
x_complex = x.astype(out_dtypes[dtype])
res_fft = fft.ifft(fft.fft(x))
res_rfft = fft.irfft(fft.rfft(x))
res_hfft = fft.hfft(fft.ihfft(x), x.shape[0])
# Check both numerical results and exact dtype matches
assert_array_almost_equal(res_fft, x_complex)
assert_array_almost_equal(res_rfft, x)
assert_array_almost_equal(res_hfft, x)
assert res_fft.dtype == x_complex.dtype
assert res_rfft.dtype == np.result_type(np.float32, x.dtype)
assert res_hfft.dtype == np.result_type(np.float32, x.dtype)
@pytest.mark.parametrize("dtype", ["float32", "float64"])
def test_dtypes_real(self, dtype, xp):
x = xp.asarray(random(30), dtype=getattr(xp, dtype))
res_rfft = fft.irfft(fft.rfft(x))
res_hfft = fft.hfft(fft.ihfft(x), x.shape[0])
# Check both numerical results and exact dtype matches
xp_assert_close(res_rfft, x)
xp_assert_close(res_hfft, x)
@pytest.mark.parametrize("dtype", ["complex64", "complex128"])
def test_dtypes_complex(self, dtype, xp):
x = xp.asarray(random(30), dtype=getattr(xp, dtype))
res_fft = fft.ifft(fft.fft(x))
# Check both numerical results and exact dtype matches
xp_assert_close(res_fft, x)
@skip_xp_backends(np_only=True,
reasons=['array-likes only supported for NumPy backend'])
@pytest.mark.parametrize("op", [fft.fft, fft.ifft,
fft.fft2, fft.ifft2,
fft.fftn, fft.ifftn,
fft.rfft, fft.irfft,
fft.rfft2, fft.irfft2,
fft.rfftn, fft.irfftn,
fft.hfft, fft.ihfft,
fft.hfft2, fft.ihfft2,
fft.hfftn, fft.ihfftn,])
def test_array_like(self, xp, op):
x = [[[1.0, 1.0], [1.0, 1.0]],
[[1.0, 1.0], [1.0, 1.0]],
[[1.0, 1.0], [1.0, 1.0]]]
xp_assert_close(op(x), op(xp.asarray(x)))
@skip_xp_backends(np_only=True)
@pytest.mark.parametrize(
"dtype",
[np.float32, np.float64, np.longdouble,
np.complex64, np.complex128, np.clongdouble])
@pytest.mark.parametrize("order", ["F", 'non-contiguous'])
@pytest.mark.parametrize(
"fft",
[fft.fft, fft.fft2, fft.fftn,
fft.ifft, fft.ifft2, fft.ifftn])
def test_fft_with_order(dtype, order, fft):
# Check that FFT/IFFT produces identical results for C, Fortran and
# non contiguous arrays
rng = np.random.RandomState(42)
X = rng.rand(8, 7, 13).astype(dtype, copy=False)
if order == 'F':
Y = np.asfortranarray(X)
else:
# Make a non contiguous array
Y = X[::-1]
X = np.ascontiguousarray(X[::-1])
if fft.__name__.endswith('fft'):
for axis in range(3):
X_res = fft(X, axis=axis)
Y_res = fft(Y, axis=axis)
assert_array_almost_equal(X_res, Y_res)
elif fft.__name__.endswith(('fft2', 'fftn')):
axes = [(0, 1), (1, 2), (0, 2)]
if fft.__name__.endswith('fftn'):
axes.extend([(0,), (1,), (2,), None])
for ax in axes:
X_res = fft(X, axes=ax)
Y_res = fft(Y, axes=ax)
assert_array_almost_equal(X_res, Y_res)
else:
raise ValueError
@skip_xp_backends(cpu_only=True)
class TestFFTThreadSafe:
threads = 16
input_shape = (800, 200)
def _test_mtsame(self, func, *args, xp=None):
def worker(args, q):
q.put(func(*args))
q = queue.Queue()
expected = func(*args)
# Spin off a bunch of threads to call the same function simultaneously
t = [threading.Thread(target=worker, args=(args, q))
for i in range(self.threads)]
[x.start() for x in t]
[x.join() for x in t]
# Make sure all threads returned the correct value
for i in range(self.threads):
xp_assert_equal(
q.get(timeout=5), expected,
err_msg='Function returned wrong value in multithreaded context'
)
def test_fft(self, xp):
a = xp.ones(self.input_shape, dtype=xp.complex128)
self._test_mtsame(fft.fft, a, xp=xp)
def test_ifft(self, xp):
a = xp.full(self.input_shape, 1+0j)
self._test_mtsame(fft.ifft, a, xp=xp)
def test_rfft(self, xp):
a = xp.ones(self.input_shape)
self._test_mtsame(fft.rfft, a, xp=xp)
def test_irfft(self, xp):
a = xp.full(self.input_shape, 1+0j)
self._test_mtsame(fft.irfft, a, xp=xp)
def test_hfft(self, xp):
a = xp.ones(self.input_shape, dtype=xp.complex64)
self._test_mtsame(fft.hfft, a, xp=xp)
def test_ihfft(self, xp):
a = xp.ones(self.input_shape)
self._test_mtsame(fft.ihfft, a, xp=xp)
@skip_xp_backends(np_only=True)
@pytest.mark.parametrize("func", [fft.fft, fft.ifft, fft.rfft, fft.irfft])
def test_multiprocess(func):
# Test that fft still works after fork (gh-10422)
with multiprocessing.Pool(2) as p:
res = p.map(func, [np.ones(100) for _ in range(4)])
expect = func(np.ones(100))
for x in res:
assert_allclose(x, expect)
class TestIRFFTN:
def test_not_last_axis_success(self, xp):
ar, ai = np.random.random((2, 16, 8, 32))
a = ar + 1j*ai
a = xp.asarray(a)
axes = (-2,)
# Should not raise error
fft.irfftn(a, axes=axes)
@pytest.mark.parametrize("func", [fft.fft, fft.ifft, fft.rfft, fft.irfft,
fft.fftn, fft.ifftn,
fft.rfftn, fft.irfftn, fft.hfft, fft.ihfft])
def test_non_standard_params(func, xp):
if func in [fft.rfft, fft.rfftn, fft.ihfft]:
dtype = xp.float64
else:
dtype = xp.complex128
if xp.__name__ != 'numpy':
x = xp.asarray([1, 2, 3], dtype=dtype)
# func(x) should not raise an exception
func(x)
assert_raises(ValueError, func, x, workers=2)
# `plan` param is not tested since SciPy does not use it currently
# but should be tested if it comes into use

View File

@ -0,0 +1,179 @@
import warnings
import numpy as np
import pytest
from scipy.fft._fftlog import fht, ifht, fhtoffset
from scipy.special import poch
from scipy.conftest import array_api_compatible
from scipy._lib._array_api import xp_assert_close
pytestmark = [array_api_compatible, pytest.mark.usefixtures("skip_xp_backends"),]
skip_xp_backends = pytest.mark.skip_xp_backends
def test_fht_agrees_with_fftlog(xp):
# check that fht numerically agrees with the output from Fortran FFTLog,
# the results were generated with the provided `fftlogtest` program,
# after fixing how the k array is generated (divide range by n-1, not n)
# test function, analytical Hankel transform is of the same form
def f(r, mu):
return r**(mu+1)*np.exp(-r**2/2)
r = np.logspace(-4, 4, 16)
dln = np.log(r[1]/r[0])
mu = 0.3
offset = 0.0
bias = 0.0
a = xp.asarray(f(r, mu))
# test 1: compute as given
ours = fht(a, dln, mu, offset=offset, bias=bias)
theirs = [-0.1159922613593045E-02, +0.1625822618458832E-02,
-0.1949518286432330E-02, +0.3789220182554077E-02,
+0.5093959119952945E-03, +0.2785387803618774E-01,
+0.9944952700848897E-01, +0.4599202164586588E+00,
+0.3157462160881342E+00, -0.8201236844404755E-03,
-0.7834031308271878E-03, +0.3931444945110708E-03,
-0.2697710625194777E-03, +0.3568398050238820E-03,
-0.5554454827797206E-03, +0.8286331026468585E-03]
theirs = xp.asarray(theirs, dtype=xp.float64)
xp_assert_close(ours, theirs)
# test 2: change to optimal offset
offset = fhtoffset(dln, mu, bias=bias)
ours = fht(a, dln, mu, offset=offset, bias=bias)
theirs = [+0.4353768523152057E-04, -0.9197045663594285E-05,
+0.3150140927838524E-03, +0.9149121960963704E-03,
+0.5808089753959363E-02, +0.2548065256377240E-01,
+0.1339477692089897E+00, +0.4821530509479356E+00,
+0.2659899781579785E+00, -0.1116475278448113E-01,
+0.1791441617592385E-02, -0.4181810476548056E-03,
+0.1314963536765343E-03, -0.5422057743066297E-04,
+0.3208681804170443E-04, -0.2696849476008234E-04]
theirs = xp.asarray(theirs, dtype=xp.float64)
xp_assert_close(ours, theirs)
# test 3: positive bias
bias = 0.8
offset = fhtoffset(dln, mu, bias=bias)
ours = fht(a, dln, mu, offset=offset, bias=bias)
theirs = [-7.3436673558316850E+00, +0.1710271207817100E+00,
+0.1065374386206564E+00, -0.5121739602708132E-01,
+0.2636649319269470E-01, +0.1697209218849693E-01,
+0.1250215614723183E+00, +0.4739583261486729E+00,
+0.2841149874912028E+00, -0.8312764741645729E-02,
+0.1024233505508988E-02, -0.1644902767389120E-03,
+0.3305775476926270E-04, -0.7786993194882709E-05,
+0.1962258449520547E-05, -0.8977895734909250E-06]
theirs = xp.asarray(theirs, dtype=xp.float64)
xp_assert_close(ours, theirs)
# test 4: negative bias
bias = -0.8
offset = fhtoffset(dln, mu, bias=bias)
ours = fht(a, dln, mu, offset=offset, bias=bias)
theirs = [+0.8985777068568745E-05, +0.4074898209936099E-04,
+0.2123969254700955E-03, +0.1009558244834628E-02,
+0.5131386375222176E-02, +0.2461678673516286E-01,
+0.1235812845384476E+00, +0.4719570096404403E+00,
+0.2893487490631317E+00, -0.1686570611318716E-01,
+0.2231398155172505E-01, -0.1480742256379873E-01,
+0.1692387813500801E+00, +0.3097490354365797E+00,
+2.7593607182401860E+00, 10.5251075070045800E+00]
theirs = xp.asarray(theirs, dtype=xp.float64)
xp_assert_close(ours, theirs)
@pytest.mark.parametrize('optimal', [True, False])
@pytest.mark.parametrize('offset', [0.0, 1.0, -1.0])
@pytest.mark.parametrize('bias', [0, 0.1, -0.1])
@pytest.mark.parametrize('n', [64, 63])
def test_fht_identity(n, bias, offset, optimal, xp):
rng = np.random.RandomState(3491349965)
a = xp.asarray(rng.standard_normal(n))
dln = rng.uniform(-1, 1)
mu = rng.uniform(-2, 2)
if optimal:
offset = fhtoffset(dln, mu, initial=offset, bias=bias)
A = fht(a, dln, mu, offset=offset, bias=bias)
a_ = ifht(A, dln, mu, offset=offset, bias=bias)
xp_assert_close(a_, a, rtol=1.5e-7)
def test_fht_special_cases(xp):
rng = np.random.RandomState(3491349965)
a = xp.asarray(rng.standard_normal(64))
dln = rng.uniform(-1, 1)
# let x = (mu+1+q)/2, y = (mu+1-q)/2, M = {0, -1, -2, ...}
# case 1: x in M, y in M => well-defined transform
mu, bias = -4.0, 1.0
with warnings.catch_warnings(record=True) as record:
fht(a, dln, mu, bias=bias)
assert not record, 'fht warned about a well-defined transform'
# case 2: x not in M, y in M => well-defined transform
mu, bias = -2.5, 0.5
with warnings.catch_warnings(record=True) as record:
fht(a, dln, mu, bias=bias)
assert not record, 'fht warned about a well-defined transform'
# case 3: x in M, y not in M => singular transform
mu, bias = -3.5, 0.5
with pytest.warns(Warning) as record:
fht(a, dln, mu, bias=bias)
assert record, 'fht did not warn about a singular transform'
# case 4: x not in M, y in M => singular inverse transform
mu, bias = -2.5, 0.5
with pytest.warns(Warning) as record:
ifht(a, dln, mu, bias=bias)
assert record, 'ifht did not warn about a singular transform'
@pytest.mark.parametrize('n', [64, 63])
def test_fht_exact(n, xp):
rng = np.random.RandomState(3491349965)
# for a(r) a power law r^\gamma, the fast Hankel transform produces the
# exact continuous Hankel transform if biased with q = \gamma
mu = rng.uniform(0, 3)
# convergence of HT: -1-mu < gamma < 1/2
gamma = rng.uniform(-1-mu, 1/2)
r = np.logspace(-2, 2, n)
a = xp.asarray(r**gamma)
dln = np.log(r[1]/r[0])
offset = fhtoffset(dln, mu, initial=0.0, bias=gamma)
A = fht(a, dln, mu, offset=offset, bias=gamma)
k = np.exp(offset)/r[::-1]
# analytical result
At = xp.asarray((2/k)**gamma * poch((mu+1-gamma)/2, gamma))
xp_assert_close(A, At)
@skip_xp_backends(np_only=True,
reasons=['array-likes only supported for NumPy backend'])
@pytest.mark.parametrize("op", [fht, ifht])
def test_array_like(xp, op):
x = [[[1.0, 1.0], [1.0, 1.0]],
[[1.0, 1.0], [1.0, 1.0]],
[[1.0, 1.0], [1.0, 1.0]]]
xp_assert_close(op(x, 1.0, 2.0), op(xp.asarray(x), 1.0, 2.0))

View File

@ -0,0 +1,570 @@
"""Includes test functions for fftpack.helper module
Copied from fftpack.helper by Pearu Peterson, October 2005
Modified for Array API, 2023
"""
from scipy.fft._helper import next_fast_len, prev_fast_len, _init_nd_shape_and_axes
from numpy.testing import assert_equal
from pytest import raises as assert_raises
import pytest
import numpy as np
import sys
from scipy.conftest import array_api_compatible
from scipy._lib._array_api import (
xp_assert_close, get_xp_devices, device, array_namespace
)
from scipy import fft
pytestmark = [array_api_compatible, pytest.mark.usefixtures("skip_xp_backends")]
skip_xp_backends = pytest.mark.skip_xp_backends
_5_smooth_numbers = [
2, 3, 4, 5, 6, 8, 9, 10,
2 * 3 * 5,
2**3 * 3**5,
2**3 * 3**3 * 5**2,
]
def test_next_fast_len():
for n in _5_smooth_numbers:
assert_equal(next_fast_len(n), n)
def _assert_n_smooth(x, n):
x_orig = x
if n < 2:
assert False
while True:
q, r = divmod(x, 2)
if r != 0:
break
x = q
for d in range(3, n+1, 2):
while True:
q, r = divmod(x, d)
if r != 0:
break
x = q
assert x == 1, \
f'x={x_orig} is not {n}-smooth, remainder={x}'
@skip_xp_backends(np_only=True)
class TestNextFastLen:
def test_next_fast_len(self):
np.random.seed(1234)
def nums():
yield from range(1, 1000)
yield 2**5 * 3**5 * 4**5 + 1
for n in nums():
m = next_fast_len(n)
_assert_n_smooth(m, 11)
assert m == next_fast_len(n, False)
m = next_fast_len(n, True)
_assert_n_smooth(m, 5)
def test_np_integers(self):
ITYPES = [np.int16, np.int32, np.int64, np.uint16, np.uint32, np.uint64]
for ityp in ITYPES:
x = ityp(12345)
testN = next_fast_len(x)
assert_equal(testN, next_fast_len(int(x)))
def testnext_fast_len_small(self):
hams = {
1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 8, 8: 8, 14: 15, 15: 15,
16: 16, 17: 18, 1021: 1024, 1536: 1536, 51200000: 51200000
}
for x, y in hams.items():
assert_equal(next_fast_len(x, True), y)
@pytest.mark.xfail(sys.maxsize < 2**32,
reason="Hamming Numbers too large for 32-bit",
raises=ValueError, strict=True)
def testnext_fast_len_big(self):
hams = {
510183360: 510183360, 510183360 + 1: 512000000,
511000000: 512000000,
854296875: 854296875, 854296875 + 1: 859963392,
196608000000: 196608000000, 196608000000 + 1: 196830000000,
8789062500000: 8789062500000, 8789062500000 + 1: 8796093022208,
206391214080000: 206391214080000,
206391214080000 + 1: 206624260800000,
470184984576000: 470184984576000,
470184984576000 + 1: 470715894135000,
7222041363087360: 7222041363087360,
7222041363087360 + 1: 7230196133913600,
# power of 5 5**23
11920928955078125: 11920928955078125,
11920928955078125 - 1: 11920928955078125,
# power of 3 3**34
16677181699666569: 16677181699666569,
16677181699666569 - 1: 16677181699666569,
# power of 2 2**54
18014398509481984: 18014398509481984,
18014398509481984 - 1: 18014398509481984,
# above this, int(ceil(n)) == int(ceil(n+1))
19200000000000000: 19200000000000000,
19200000000000000 + 1: 19221679687500000,
288230376151711744: 288230376151711744,
288230376151711744 + 1: 288325195312500000,
288325195312500000 - 1: 288325195312500000,
288325195312500000: 288325195312500000,
288325195312500000 + 1: 288555831593533440,
}
for x, y in hams.items():
assert_equal(next_fast_len(x, True), y)
def test_keyword_args(self):
assert next_fast_len(11, real=True) == 12
assert next_fast_len(target=7, real=False) == 7
@skip_xp_backends(np_only=True)
class TestPrevFastLen:
def test_prev_fast_len(self):
np.random.seed(1234)
def nums():
yield from range(1, 1000)
yield 2**5 * 3**5 * 4**5 + 1
for n in nums():
m = prev_fast_len(n)
_assert_n_smooth(m, 11)
assert m == prev_fast_len(n, False)
m = prev_fast_len(n, True)
_assert_n_smooth(m, 5)
def test_np_integers(self):
ITYPES = [np.int16, np.int32, np.int64, np.uint16, np.uint32,
np.uint64]
for ityp in ITYPES:
x = ityp(12345)
testN = prev_fast_len(x)
assert_equal(testN, prev_fast_len(int(x)))
testN = prev_fast_len(x, real=True)
assert_equal(testN, prev_fast_len(int(x), real=True))
def testprev_fast_len_small(self):
hams = {
1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 6, 8: 8, 14: 12, 15: 15,
16: 16, 17: 16, 1021: 1000, 1536: 1536, 51200000: 51200000
}
for x, y in hams.items():
assert_equal(prev_fast_len(x, True), y)
hams = {
1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10,
11: 11, 12: 12, 13: 12, 14: 14, 15: 15, 16: 16, 17: 16, 18: 18,
19: 18, 20: 20, 21: 21, 22: 22, 120: 120, 121: 121, 122: 121,
1021: 1008, 1536: 1536, 51200000: 51200000
}
for x, y in hams.items():
assert_equal(prev_fast_len(x, False), y)
@pytest.mark.xfail(sys.maxsize < 2**32,
reason="Hamming Numbers too large for 32-bit",
raises=ValueError, strict=True)
def testprev_fast_len_big(self):
hams = {
# 2**6 * 3**13 * 5**1
510183360: 510183360,
510183360 + 1: 510183360,
510183360 - 1: 509607936, # 2**21 * 3**5
# 2**6 * 5**6 * 7**1 * 73**1
511000000: 510183360,
511000000 + 1: 510183360,
511000000 - 1: 510183360, # 2**6 * 3**13 * 5**1
# 3**7 * 5**8
854296875: 854296875,
854296875 + 1: 854296875,
854296875 - 1: 850305600, # 2**6 * 3**12 * 5**2
# 2**22 * 3**1 * 5**6
196608000000: 196608000000,
196608000000 + 1: 196608000000,
196608000000 - 1: 195910410240, # 2**13 * 3**14 * 5**1
# 2**5 * 3**2 * 5**15
8789062500000: 8789062500000,
8789062500000 + 1: 8789062500000,
8789062500000 - 1: 8748000000000, # 2**11 * 3**7 * 5**9
# 2**24 * 3**9 * 5**4
206391214080000: 206391214080000,
206391214080000 + 1: 206391214080000,
206391214080000 - 1: 206158430208000, # 2**39 * 3**1 * 5**3
# 2**18 * 3**15 * 5**3
470184984576000: 470184984576000,
470184984576000 + 1: 470184984576000,
470184984576000 - 1: 469654673817600, # 2**33 * 3**7 **5**2
# 2**25 * 3**16 * 5**1
7222041363087360: 7222041363087360,
7222041363087360 + 1: 7222041363087360,
7222041363087360 - 1: 7213895789838336, # 2**40 * 3**8
# power of 5 5**23
11920928955078125: 11920928955078125,
11920928955078125 + 1: 11920928955078125,
11920928955078125 - 1: 11901557422080000, # 2**14 * 3**19 * 5**4
# power of 3 3**34
16677181699666569: 16677181699666569,
16677181699666569 + 1: 16677181699666569,
16677181699666569 - 1: 16607531250000000, # 2**7 * 3**12 * 5**12
# power of 2 2**54
18014398509481984: 18014398509481984,
18014398509481984 + 1: 18014398509481984,
18014398509481984 - 1: 18000000000000000, # 2**16 * 3**2 * 5**15
# 2**20 * 3**1 * 5**14
19200000000000000: 19200000000000000,
19200000000000000 + 1: 19200000000000000,
19200000000000000 - 1: 19131876000000000, # 2**11 * 3**14 * 5**9
# 2**58
288230376151711744: 288230376151711744,
288230376151711744 + 1: 288230376151711744,
288230376151711744 - 1: 288000000000000000, # 2**20 * 3**2 * 5**15
# 2**5 * 3**10 * 5**16
288325195312500000: 288325195312500000,
288325195312500000 + 1: 288325195312500000,
288325195312500000 - 1: 288230376151711744, # 2**58
}
for x, y in hams.items():
assert_equal(prev_fast_len(x, True), y)
def test_keyword_args(self):
assert prev_fast_len(11, real=True) == 10
assert prev_fast_len(target=7, real=False) == 7
@skip_xp_backends(cpu_only=True)
class Test_init_nd_shape_and_axes:
def test_py_0d_defaults(self, xp):
x = xp.asarray(4)
shape = None
axes = None
shape_expected = ()
axes_expected = []
shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
assert shape_res == shape_expected
assert axes_res == axes_expected
def test_xp_0d_defaults(self, xp):
x = xp.asarray(7.)
shape = None
axes = None
shape_expected = ()
axes_expected = []
shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
assert shape_res == shape_expected
assert axes_res == axes_expected
def test_py_1d_defaults(self, xp):
x = xp.asarray([1, 2, 3])
shape = None
axes = None
shape_expected = (3,)
axes_expected = [0]
shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
assert shape_res == shape_expected
assert axes_res == axes_expected
def test_xp_1d_defaults(self, xp):
x = xp.arange(0, 1, .1)
shape = None
axes = None
shape_expected = (10,)
axes_expected = [0]
shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
assert shape_res == shape_expected
assert axes_res == axes_expected
def test_py_2d_defaults(self, xp):
x = xp.asarray([[1, 2, 3, 4],
[5, 6, 7, 8]])
shape = None
axes = None
shape_expected = (2, 4)
axes_expected = [0, 1]
shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
assert shape_res == shape_expected
assert axes_res == axes_expected
def test_xp_2d_defaults(self, xp):
x = xp.arange(0, 1, .1)
x = xp.reshape(x, (5, 2))
shape = None
axes = None
shape_expected = (5, 2)
axes_expected = [0, 1]
shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
assert shape_res == shape_expected
assert axes_res == axes_expected
def test_xp_5d_defaults(self, xp):
x = xp.zeros([6, 2, 5, 3, 4])
shape = None
axes = None
shape_expected = (6, 2, 5, 3, 4)
axes_expected = [0, 1, 2, 3, 4]
shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
assert shape_res == shape_expected
assert axes_res == axes_expected
def test_xp_5d_set_shape(self, xp):
x = xp.zeros([6, 2, 5, 3, 4])
shape = [10, -1, -1, 1, 4]
axes = None
shape_expected = (10, 2, 5, 1, 4)
axes_expected = [0, 1, 2, 3, 4]
shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
assert shape_res == shape_expected
assert axes_res == axes_expected
def test_xp_5d_set_axes(self, xp):
x = xp.zeros([6, 2, 5, 3, 4])
shape = None
axes = [4, 1, 2]
shape_expected = (4, 2, 5)
axes_expected = [4, 1, 2]
shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
assert shape_res == shape_expected
assert axes_res == axes_expected
def test_xp_5d_set_shape_axes(self, xp):
x = xp.zeros([6, 2, 5, 3, 4])
shape = [10, -1, 2]
axes = [1, 0, 3]
shape_expected = (10, 6, 2)
axes_expected = [1, 0, 3]
shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
assert shape_res == shape_expected
assert axes_res == axes_expected
def test_shape_axes_subset(self, xp):
x = xp.zeros((2, 3, 4, 5))
shape, axes = _init_nd_shape_and_axes(x, shape=(5, 5, 5), axes=None)
assert shape == (5, 5, 5)
assert axes == [1, 2, 3]
def test_errors(self, xp):
x = xp.zeros(1)
with assert_raises(ValueError, match="axes must be a scalar or "
"iterable of integers"):
_init_nd_shape_and_axes(x, shape=None, axes=[[1, 2], [3, 4]])
with assert_raises(ValueError, match="axes must be a scalar or "
"iterable of integers"):
_init_nd_shape_and_axes(x, shape=None, axes=[1., 2., 3., 4.])
with assert_raises(ValueError,
match="axes exceeds dimensionality of input"):
_init_nd_shape_and_axes(x, shape=None, axes=[1])
with assert_raises(ValueError,
match="axes exceeds dimensionality of input"):
_init_nd_shape_and_axes(x, shape=None, axes=[-2])
with assert_raises(ValueError,
match="all axes must be unique"):
_init_nd_shape_and_axes(x, shape=None, axes=[0, 0])
with assert_raises(ValueError, match="shape must be a scalar or "
"iterable of integers"):
_init_nd_shape_and_axes(x, shape=[[1, 2], [3, 4]], axes=None)
with assert_raises(ValueError, match="shape must be a scalar or "
"iterable of integers"):
_init_nd_shape_and_axes(x, shape=[1., 2., 3., 4.], axes=None)
with assert_raises(ValueError,
match="when given, axes and shape arguments"
" have to be of the same length"):
_init_nd_shape_and_axes(xp.zeros([1, 1, 1, 1]),
shape=[1, 2, 3], axes=[1])
with assert_raises(ValueError,
match="invalid number of data points"
r" \(\[0\]\) specified"):
_init_nd_shape_and_axes(x, shape=[0], axes=None)
with assert_raises(ValueError,
match="invalid number of data points"
r" \(\[-2\]\) specified"):
_init_nd_shape_and_axes(x, shape=-2, axes=None)
class TestFFTShift:
def test_definition(self, xp):
x = xp.asarray([0., 1, 2, 3, 4, -4, -3, -2, -1])
y = xp.asarray([-4., -3, -2, -1, 0, 1, 2, 3, 4])
xp_assert_close(fft.fftshift(x), y)
xp_assert_close(fft.ifftshift(y), x)
x = xp.asarray([0., 1, 2, 3, 4, -5, -4, -3, -2, -1])
y = xp.asarray([-5., -4, -3, -2, -1, 0, 1, 2, 3, 4])
xp_assert_close(fft.fftshift(x), y)
xp_assert_close(fft.ifftshift(y), x)
def test_inverse(self, xp):
for n in [1, 4, 9, 100, 211]:
x = xp.asarray(np.random.random((n,)))
xp_assert_close(fft.ifftshift(fft.fftshift(x)), x)
def test_axes_keyword(self, xp):
freqs = xp.asarray([[0., 1, 2], [3, 4, -4], [-3, -2, -1]])
shifted = xp.asarray([[-1., -3, -2], [2, 0, 1], [-4, 3, 4]])
xp_assert_close(fft.fftshift(freqs, axes=(0, 1)), shifted)
xp_assert_close(fft.fftshift(freqs, axes=0), fft.fftshift(freqs, axes=(0,)))
xp_assert_close(fft.ifftshift(shifted, axes=(0, 1)), freqs)
xp_assert_close(fft.ifftshift(shifted, axes=0),
fft.ifftshift(shifted, axes=(0,)))
xp_assert_close(fft.fftshift(freqs), shifted)
xp_assert_close(fft.ifftshift(shifted), freqs)
def test_uneven_dims(self, xp):
""" Test 2D input, which has uneven dimension sizes """
freqs = xp.asarray([
[0, 1],
[2, 3],
[4, 5]
], dtype=xp.float64)
# shift in dimension 0
shift_dim0 = xp.asarray([
[4, 5],
[0, 1],
[2, 3]
], dtype=xp.float64)
xp_assert_close(fft.fftshift(freqs, axes=0), shift_dim0)
xp_assert_close(fft.ifftshift(shift_dim0, axes=0), freqs)
xp_assert_close(fft.fftshift(freqs, axes=(0,)), shift_dim0)
xp_assert_close(fft.ifftshift(shift_dim0, axes=[0]), freqs)
# shift in dimension 1
shift_dim1 = xp.asarray([
[1, 0],
[3, 2],
[5, 4]
], dtype=xp.float64)
xp_assert_close(fft.fftshift(freqs, axes=1), shift_dim1)
xp_assert_close(fft.ifftshift(shift_dim1, axes=1), freqs)
# shift in both dimensions
shift_dim_both = xp.asarray([
[5, 4],
[1, 0],
[3, 2]
], dtype=xp.float64)
xp_assert_close(fft.fftshift(freqs, axes=(0, 1)), shift_dim_both)
xp_assert_close(fft.ifftshift(shift_dim_both, axes=(0, 1)), freqs)
xp_assert_close(fft.fftshift(freqs, axes=[0, 1]), shift_dim_both)
xp_assert_close(fft.ifftshift(shift_dim_both, axes=[0, 1]), freqs)
# axes=None (default) shift in all dimensions
xp_assert_close(fft.fftshift(freqs, axes=None), shift_dim_both)
xp_assert_close(fft.ifftshift(shift_dim_both, axes=None), freqs)
xp_assert_close(fft.fftshift(freqs), shift_dim_both)
xp_assert_close(fft.ifftshift(shift_dim_both), freqs)
@skip_xp_backends("cupy", "jax.numpy",
reasons=["CuPy has not implemented the `device` param",
"JAX has not implemented the `device` param"])
class TestFFTFreq:
def test_definition(self, xp):
x = xp.asarray([0, 1, 2, 3, 4, -4, -3, -2, -1], dtype=xp.float64)
x2 = xp.asarray([0, 1, 2, 3, 4, -5, -4, -3, -2, -1], dtype=xp.float64)
# default dtype varies across backends
y = 9 * fft.fftfreq(9, xp=xp)
xp_assert_close(y, x, check_dtype=False, check_namespace=True)
y = 9 * xp.pi * fft.fftfreq(9, xp.pi, xp=xp)
xp_assert_close(y, x, check_dtype=False)
y = 10 * fft.fftfreq(10, xp=xp)
xp_assert_close(y, x2, check_dtype=False)
y = 10 * xp.pi * fft.fftfreq(10, xp.pi, xp=xp)
xp_assert_close(y, x2, check_dtype=False)
def test_device(self, xp):
xp_test = array_namespace(xp.empty(0))
devices = get_xp_devices(xp)
for d in devices:
y = fft.fftfreq(9, xp=xp, device=d)
x = xp_test.empty(0, device=d)
assert device(y) == device(x)
@skip_xp_backends("cupy", "jax.numpy",
reasons=["CuPy has not implemented the `device` param",
"JAX has not implemented the `device` param"])
class TestRFFTFreq:
def test_definition(self, xp):
x = xp.asarray([0, 1, 2, 3, 4], dtype=xp.float64)
x2 = xp.asarray([0, 1, 2, 3, 4, 5], dtype=xp.float64)
# default dtype varies across backends
y = 9 * fft.rfftfreq(9, xp=xp)
xp_assert_close(y, x, check_dtype=False, check_namespace=True)
y = 9 * xp.pi * fft.rfftfreq(9, xp.pi, xp=xp)
xp_assert_close(y, x, check_dtype=False)
y = 10 * fft.rfftfreq(10, xp=xp)
xp_assert_close(y, x2, check_dtype=False)
y = 10 * xp.pi * fft.rfftfreq(10, xp.pi, xp=xp)
xp_assert_close(y, x2, check_dtype=False)
def test_device(self, xp):
xp_test = array_namespace(xp.empty(0))
devices = get_xp_devices(xp)
for d in devices:
y = fft.rfftfreq(9, xp=xp, device=d)
x = xp_test.empty(0, device=d)
assert device(y) == device(x)

View File

@ -0,0 +1,84 @@
from scipy import fft
import numpy as np
import pytest
from numpy.testing import assert_allclose
import multiprocessing
import os
@pytest.fixture(scope='module')
def x():
return np.random.randn(512, 128) # Must be large enough to qualify for mt
@pytest.mark.parametrize("func", [
fft.fft, fft.ifft, fft.fft2, fft.ifft2, fft.fftn, fft.ifftn,
fft.rfft, fft.irfft, fft.rfft2, fft.irfft2, fft.rfftn, fft.irfftn,
fft.hfft, fft.ihfft, fft.hfft2, fft.ihfft2, fft.hfftn, fft.ihfftn,
fft.dct, fft.idct, fft.dctn, fft.idctn,
fft.dst, fft.idst, fft.dstn, fft.idstn,
])
@pytest.mark.parametrize("workers", [2, -1])
def test_threaded_same(x, func, workers):
expected = func(x, workers=1)
actual = func(x, workers=workers)
assert_allclose(actual, expected)
def _mt_fft(x):
return fft.fft(x, workers=2)
@pytest.mark.slow
def test_mixed_threads_processes(x):
# Test that the fft threadpool is safe to use before & after fork
expect = fft.fft(x, workers=2)
with multiprocessing.Pool(2) as p:
res = p.map(_mt_fft, [x for _ in range(4)])
for r in res:
assert_allclose(r, expect)
fft.fft(x, workers=2)
def test_invalid_workers(x):
cpus = os.cpu_count()
fft.ifft([1], workers=-cpus)
with pytest.raises(ValueError, match='workers must not be zero'):
fft.fft(x, workers=0)
with pytest.raises(ValueError, match='workers value out of range'):
fft.ifft(x, workers=-cpus-1)
def test_set_get_workers():
cpus = os.cpu_count()
assert fft.get_workers() == 1
with fft.set_workers(4):
assert fft.get_workers() == 4
with fft.set_workers(-1):
assert fft.get_workers() == cpus
assert fft.get_workers() == 4
assert fft.get_workers() == 1
with fft.set_workers(-cpus):
assert fft.get_workers() == 1
def test_set_workers_invalid():
with pytest.raises(ValueError, match='workers must not be zero'):
with fft.set_workers(0):
pass
with pytest.raises(ValueError, match='workers value out of range'):
with fft.set_workers(-os.cpu_count()-1):
pass

View File

@ -0,0 +1,249 @@
import numpy as np
from numpy.testing import assert_allclose, assert_array_equal
import pytest
import math
from scipy.fft import dct, idct, dctn, idctn, dst, idst, dstn, idstn
import scipy.fft as fft
from scipy import fftpack
from scipy.conftest import array_api_compatible
from scipy._lib._array_api import copy, xp_assert_close
pytestmark = [array_api_compatible, pytest.mark.usefixtures("skip_xp_backends")]
skip_xp_backends = pytest.mark.skip_xp_backends
SQRT_2 = math.sqrt(2)
# scipy.fft wraps the fftpack versions but with normalized inverse transforms.
# So, the forward transforms and definitions are already thoroughly tested in
# fftpack/test_real_transforms.py
@skip_xp_backends(cpu_only=True)
@pytest.mark.parametrize("forward, backward", [(dct, idct), (dst, idst)])
@pytest.mark.parametrize("type", [1, 2, 3, 4])
@pytest.mark.parametrize("n", [2, 3, 4, 5, 10, 16])
@pytest.mark.parametrize("axis", [0, 1])
@pytest.mark.parametrize("norm", [None, 'backward', 'ortho', 'forward'])
@pytest.mark.parametrize("orthogonalize", [False, True])
def test_identity_1d(forward, backward, type, n, axis, norm, orthogonalize, xp):
# Test the identity f^-1(f(x)) == x
x = xp.asarray(np.random.rand(n, n))
y = forward(x, type, axis=axis, norm=norm, orthogonalize=orthogonalize)
z = backward(y, type, axis=axis, norm=norm, orthogonalize=orthogonalize)
xp_assert_close(z, x)
pad = [(0, 0)] * 2
pad[axis] = (0, 4)
y2 = xp.asarray(np.pad(np.asarray(y), pad, mode='edge'))
z2 = backward(y2, type, n, axis, norm, orthogonalize=orthogonalize)
xp_assert_close(z2, x)
@skip_xp_backends(np_only=True,
reasons=['`overwrite_x` only supported for NumPy backend.'])
@pytest.mark.parametrize("forward, backward", [(dct, idct), (dst, idst)])
@pytest.mark.parametrize("type", [1, 2, 3, 4])
@pytest.mark.parametrize("dtype", [np.float16, np.float32, np.float64,
np.complex64, np.complex128])
@pytest.mark.parametrize("axis", [0, 1])
@pytest.mark.parametrize("norm", [None, 'backward', 'ortho', 'forward'])
@pytest.mark.parametrize("overwrite_x", [True, False])
def test_identity_1d_overwrite(forward, backward, type, dtype, axis, norm,
overwrite_x):
# Test the identity f^-1(f(x)) == x
x = np.random.rand(7, 8).astype(dtype)
x_orig = x.copy()
y = forward(x, type, axis=axis, norm=norm, overwrite_x=overwrite_x)
y_orig = y.copy()
z = backward(y, type, axis=axis, norm=norm, overwrite_x=overwrite_x)
if not overwrite_x:
assert_allclose(z, x, rtol=1e-6, atol=1e-6)
assert_array_equal(x, x_orig)
assert_array_equal(y, y_orig)
else:
assert_allclose(z, x_orig, rtol=1e-6, atol=1e-6)
@skip_xp_backends(cpu_only=True)
@pytest.mark.parametrize("forward, backward", [(dctn, idctn), (dstn, idstn)])
@pytest.mark.parametrize("type", [1, 2, 3, 4])
@pytest.mark.parametrize("shape, axes",
[
((4, 4), 0),
((4, 4), 1),
((4, 4), None),
((4, 4), (0, 1)),
((10, 12), None),
((10, 12), (0, 1)),
((4, 5, 6), None),
((4, 5, 6), 1),
((4, 5, 6), (0, 2)),
])
@pytest.mark.parametrize("norm", [None, 'backward', 'ortho', 'forward'])
@pytest.mark.parametrize("orthogonalize", [False, True])
def test_identity_nd(forward, backward, type, shape, axes, norm,
orthogonalize, xp):
# Test the identity f^-1(f(x)) == x
x = xp.asarray(np.random.random(shape))
if axes is not None:
shape = np.take(shape, axes)
y = forward(x, type, axes=axes, norm=norm, orthogonalize=orthogonalize)
z = backward(y, type, axes=axes, norm=norm, orthogonalize=orthogonalize)
xp_assert_close(z, x)
if axes is None:
pad = [(0, 4)] * x.ndim
elif isinstance(axes, int):
pad = [(0, 0)] * x.ndim
pad[axes] = (0, 4)
else:
pad = [(0, 0)] * x.ndim
for a in axes:
pad[a] = (0, 4)
# TODO write an array-agnostic pad
y2 = xp.asarray(np.pad(np.asarray(y), pad, mode='edge'))
z2 = backward(y2, type, shape, axes, norm, orthogonalize=orthogonalize)
xp_assert_close(z2, x)
@skip_xp_backends(np_only=True,
reasons=['`overwrite_x` only supported for NumPy backend.'])
@pytest.mark.parametrize("forward, backward", [(dctn, idctn), (dstn, idstn)])
@pytest.mark.parametrize("type", [1, 2, 3, 4])
@pytest.mark.parametrize("shape, axes",
[
((4, 5), 0),
((4, 5), 1),
((4, 5), None),
])
@pytest.mark.parametrize("dtype", [np.float16, np.float32, np.float64,
np.complex64, np.complex128])
@pytest.mark.parametrize("norm", [None, 'backward', 'ortho', 'forward'])
@pytest.mark.parametrize("overwrite_x", [False, True])
def test_identity_nd_overwrite(forward, backward, type, shape, axes, dtype,
norm, overwrite_x):
# Test the identity f^-1(f(x)) == x
x = np.random.random(shape).astype(dtype)
x_orig = x.copy()
if axes is not None:
shape = np.take(shape, axes)
y = forward(x, type, axes=axes, norm=norm)
y_orig = y.copy()
z = backward(y, type, axes=axes, norm=norm)
if overwrite_x:
assert_allclose(z, x_orig, rtol=1e-6, atol=1e-6)
else:
assert_allclose(z, x, rtol=1e-6, atol=1e-6)
assert_array_equal(x, x_orig)
assert_array_equal(y, y_orig)
@skip_xp_backends(cpu_only=True)
@pytest.mark.parametrize("func", ['dct', 'dst', 'dctn', 'dstn'])
@pytest.mark.parametrize("type", [1, 2, 3, 4])
@pytest.mark.parametrize("norm", [None, 'backward', 'ortho', 'forward'])
def test_fftpack_equivalience(func, type, norm, xp):
x = np.random.rand(8, 16)
fftpack_res = xp.asarray(getattr(fftpack, func)(x, type, norm=norm))
x = xp.asarray(x)
fft_res = getattr(fft, func)(x, type, norm=norm)
xp_assert_close(fft_res, fftpack_res)
@skip_xp_backends(cpu_only=True)
@pytest.mark.parametrize("func", [dct, dst, dctn, dstn])
@pytest.mark.parametrize("type", [1, 2, 3, 4])
def test_orthogonalize_default(func, type, xp):
# Test orthogonalize is the default when norm="ortho", but not otherwise
x = xp.asarray(np.random.rand(100))
for norm, ortho in [
("forward", False),
("backward", False),
("ortho", True),
]:
a = func(x, type=type, norm=norm, orthogonalize=ortho)
b = func(x, type=type, norm=norm)
xp_assert_close(a, b)
@skip_xp_backends(cpu_only=True)
@pytest.mark.parametrize("norm", ["backward", "ortho", "forward"])
@pytest.mark.parametrize("func, type", [
(dct, 4), (dst, 1), (dst, 4)])
def test_orthogonalize_noop(func, type, norm, xp):
# Transforms where orthogonalize is a no-op
x = xp.asarray(np.random.rand(100))
y1 = func(x, type=type, norm=norm, orthogonalize=True)
y2 = func(x, type=type, norm=norm, orthogonalize=False)
xp_assert_close(y1, y2)
@skip_xp_backends('jax.numpy',
reasons=['jax arrays do not support item assignment'],
cpu_only=True)
@pytest.mark.parametrize("norm", ["backward", "ortho", "forward"])
def test_orthogonalize_dct1(norm, xp):
x = xp.asarray(np.random.rand(100))
x2 = copy(x, xp=xp)
x2[0] *= SQRT_2
x2[-1] *= SQRT_2
y1 = dct(x, type=1, norm=norm, orthogonalize=True)
y2 = dct(x2, type=1, norm=norm, orthogonalize=False)
y2[0] /= SQRT_2
y2[-1] /= SQRT_2
xp_assert_close(y1, y2)
@skip_xp_backends('jax.numpy',
reasons=['jax arrays do not support item assignment'],
cpu_only=True)
@pytest.mark.parametrize("norm", ["backward", "ortho", "forward"])
@pytest.mark.parametrize("func", [dct, dst])
def test_orthogonalize_dcst2(func, norm, xp):
x = xp.asarray(np.random.rand(100))
y1 = func(x, type=2, norm=norm, orthogonalize=True)
y2 = func(x, type=2, norm=norm, orthogonalize=False)
y2[0 if func == dct else -1] /= SQRT_2
xp_assert_close(y1, y2)
@skip_xp_backends('jax.numpy',
reasons=['jax arrays do not support item assignment'],
cpu_only=True)
@pytest.mark.parametrize("norm", ["backward", "ortho", "forward"])
@pytest.mark.parametrize("func", [dct, dst])
def test_orthogonalize_dcst3(func, norm, xp):
x = xp.asarray(np.random.rand(100))
x2 = copy(x, xp=xp)
x2[0 if func == dct else -1] *= SQRT_2
y1 = func(x, type=3, norm=norm, orthogonalize=True)
y2 = func(x2, type=3, norm=norm, orthogonalize=False)
xp_assert_close(y1, y2)
@skip_xp_backends(np_only=True,
reasons=['array-likes only supported for NumPy backend'])
@pytest.mark.parametrize("func", [dct, idct, dctn, idctn, dst, idst, dstn, idstn])
def test_array_like(xp, func):
x = [[[1.0, 1.0], [1.0, 1.0]],
[[1.0, 1.0], [1.0, 1.0]],
[[1.0, 1.0], [1.0, 1.0]]]
xp_assert_close(func(x), func(xp.asarray(x)))