diff --git a/CHANGELOG.md b/CHANGELOG.md index ebb5d30e..6ad2a04a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Added Hermitian FFT functions to SciPy interface `mkl_fft.interfaces.scipy_fft`: `hfft`, `ihfft`, `hfftn`, `ihfftn`, `hfft2`, and `ihfft2` [gh-161](https://github.com/IntelPython/mkl_fft/pull/161) * Added support for `out` kwarg to all FFT functions in `mkl_fft` and `mkl_fft.interfaces.numpy_fft` [gh-157](https://github.com/IntelPython/mkl_fft/pull/157) +* Added `fftfreq`, `fftshift`, `ifftshift`, and `rfftfreq` to both NumPy and SciPy interfaces [gh-179](https://github.com/IntelPython/mkl_fft/pull/179) ### Changed -* NumPy interface `mkl_fft.interfaces.numpy_fft` is aligned with numpy-2.* [gh-139](https://github.com/IntelPython/mkl_fft/pull/139), [gh-157](https://github.com/IntelPython/mkl_fft/pull/157) +* NumPy interface `mkl_fft.interfaces.numpy_fft` is aligned with numpy-2.x.x [gh-139](https://github.com/IntelPython/mkl_fft/pull/139), [gh-157](https://github.com/IntelPython/mkl_fft/pull/157) +* To set `mkl_fft` as the backend for SciPy is only possible through `mkl_fft.interfaces.scipy_fft` [gh-179](https://github.com/IntelPython/mkl_fft/pull/179) ## [1.3.14] (04/10/2025) @@ -86,24 +88,6 @@ transform improves multi-core utilization which may offset the performance loss Added `scipy.fft` backend, see #42. Fixed #46. - -```python ->>> import numpy as np, mkl_fft, mkl_fft._scipy_fft as mkl_be, scipy, scipy.fft, mkl - ->>> mkl.verbose(1) -# True - ->>> x = np.random.randn(8*7).reshape((7, 8)) ->>> with scipy.fft.set_backend(mkl_be, only=True): ->>> ff = scipy.fft.fft2(x, workers=4) ->>> ff2 = scipy.fft.fft2(x) -# MKL_VERBOSE Intel(R) MKL 2020.0 Product build 20191102 for Intel(R) 64 architecture Intel(R) Advanced Vector Extensions 2 (Intel(R) AVX2) enabled processors, Lnx 2.40GHz intel_thread -# MKL_VERBOSE FFT(drfo7:8:8x8:1:1,bScale:0.0178571,tLim:1,desc:0x5629ad31b800) 24.85ms CNR:OFF Dyn:1 FastMM:1 TID:0 NThr:16,FFT:4 - ->>> np.allclose(ff, ff2) -# True -``` - ## [1.0.15] Changed tests to not compare against numpy fft, as this broke due to renaming of `np.fft.pocketfft` to diff --git a/README.md b/README.md index c7356abf..6b51563e 100644 --- a/README.md +++ b/README.md @@ -43,34 +43,45 @@ This eliminates the need to copy input array contiguously into an intermediate b `mkl_fft` directly supports N-dimensional Fourier transforms. -More details can be found in SciPy 2017 conference proceedings: - https://github.com/scipy-conference/scipy_proceedings/tree/2017/papers/oleksandr_pavlyk +More details can be found in [SciPy 2017 conference proceedings](https://github.com/scipy-conference/scipy_proceedings/tree/2017/papers/oleksandr_pavlyk). --- -`mkl_fft` implements the following functions: +The `mkl_fft` package offers interfaces that act as drop-in replacements for equivalent functions in NumPy and SciPy. Learn more about these interfaces [here](https://github.com/IntelPython/mkl_fft/blob/master/mkl_fft/interfaces/README.md). + +While using these interfaces is the easiest way to leverage `mk_fft`, one can also use `mkl_fft` directly with the following FFT functions: ### complex-to-complex (c2c) transforms: -`fft(x, n=None, axis=-1, overwrite_x=False, fwd_scale=1.0, out=out)` - 1D FFT, similar to `scipy.fft.fft` +`fft(x, n=None, axis=-1, overwrite_x=False, fwd_scale=1.0, out=None)` - 1D FFT, similar to `scipy.fft.fft` -`fft2(x, s=None, axes=(-2, -1), overwrite_x=False, fwd_scale=1.0, out=out)` - 2D FFT, similar to `scipy.fft.fft2` +`fft2(x, s=None, axes=(-2, -1), overwrite_x=False, fwd_scale=1.0, out=None)` - 2D FFT, similar to `scipy.fft.fft2` -`fftn(x, s=None, axes=None, overwrite_x=False, fwd_scale=1.0, out=out)` - ND FFT, similar to `scipy.fft.fftn` +`fftn(x, s=None, axes=None, overwrite_x=False, fwd_scale=1.0, out=None)` - ND FFT, similar to `scipy.fft.fftn` and similar inverse FFT (`ifft*`) functions. ### real-to-complex (r2c) and complex-to-real (c2r) transforms: -`rfft(x, n=None, axis=-1, fwd_scale=1.0, out=out)` - r2c 1D FFT, similar to `numpy.fft.rfft` +`rfft(x, n=None, axis=-1, fwd_scale=1.0, out=None)` - r2c 1D FFT, similar to `numpy.fft.rfft` -`rfft2(x, s=None, axes=(-2, -1), fwd_scale=1.0, out=out)` - r2c 2D FFT, similar to `numpy.fft.rfft2` +`rfft2(x, s=None, axes=(-2, -1), fwd_scale=1.0, out=None)` - r2c 2D FFT, similar to `numpy.fft.rfft2` -`rfftn(x, s=None, axes=None, fwd_scale=1.0, out=out)` - r2c ND FFT, similar to `numpy.fft.rfftn` +`rfftn(x, s=None, axes=None, fwd_scale=1.0, out=None)` - r2c ND FFT, similar to `numpy.fft.rfftn` and similar inverse c2r FFT (`irfft*`) functions. -The package also provides `mkl_fft.interfaces.numpy_fft` and `mkl_fft.interfaces.scipy_fft` interfaces which provide drop-in replacements for equivalent functions in NumPy and SciPy, respectively. +The following example shows how to use `mkl_fft` for calculating a 1D FFT. + +```python +import numpy, mkl_fft +a = numpy.random.randn(10) + 1j*numpy.random.randn(10) + +mkl_res = mkl_fft.fft(a) +np_res = numpy.fft.fft(a) +numpy.allclose(mkl_res, np_res) +# True +``` --- diff --git a/mkl_fft/interfaces/README.md b/mkl_fft/interfaces/README.md new file mode 100644 index 00000000..d4cae26f --- /dev/null +++ b/mkl_fft/interfaces/README.md @@ -0,0 +1,104 @@ +# Interfaces +The `mkl_fft` package provides interfaces that serve as drop-in replacements for equivalent functions in NumPy and SciPy. + +--- + +## NumPy interface - `mkl_fft.interfaces.numpy_fft` + +This interface is a drop-in replacement for the [`numpy.fft`](https://numpy.org/devdocs/reference/routines.fft.html) module and includes **all** the functions available there: + +* complex-to-complex FFTs: `fft`, `ifft`, `fft2`, `ifft2`, `fftn`, `ifftn`. + +* real-to-complex and complex-to-real FFTs: `rfft`, `irfft`, `rfft2`, `irfft2`, `rfftn`, `irfftn`. + +* Hermitian FFTs: `hfft`, `ihfft`. + +* Helper routines: `fftfreq`, `rfftfreq`, `fftshift`, `ifftshift`. These routines serve as a fallback to the NumPy implementation and are included for completeness. + +The following example shows how to use this interface for calculating a 1D FFT. + +```python +import numpy +import mkl_fft.interfaces.numpy_fft as numpy_fft + +a = numpy.random.randn(10) + 1j*numpy.random.randn(10) + +mkl_res = numpy_fft.fft(a) +np_res = numpy.fft.fft(a) +numpy.allclose(mkl_res, np_res) +# True +``` + +--- + +## SciPy interface - `mkl_fft.interfaces.scipy_fft` +This interface is a drop-in replacement for the [`scipy.fft`](https://scipy.github.io/devdocs/reference/fft.html) module and includes **subset** of the functions available there: + +* complex-to-complex FFTs: `fft`, `ifft`, `fft2`, `ifft2`, `fftn`, `ifftn`. + +* real-to-complex and complex-to-real FFTs: `rfft`, `irfft`, `rfft2`, `irfft2`, `rfftn`, `irfftn`. + +* Hermitian FFTs: `hfft`, `ihfft`, `hfft2`, `ihfft2`, `hfftn`, `ihfftn`. + +* Helper functions: `fftshift`, `ifftshift`, `fftfreq`, `rfftfreq`, `set_workers`, `get_workers`. All of these functions, except for `set_workers` and `get_workers`, serve as a fallback to the SciPy implementation and are included for completeness. + +The following example shows how to use this interface for calculating a 1D FFT. + +```python +import numpy, scipy +import mkl_fft.interfaces.scipy_fft as scipy_fft + +a = numpy.random.randn(10) + 1j * numpy.random.randn(10) + +mkl_res = scipy_fft.fft(a) +sp_res = scipy.fft.fft(a) +numpy.allclose(mkl_res, sp_res) +# True +``` + +--- + +### Registering `mkl_fft` as the FFT backend for SciPy + +`mkl_fft.interfaces.scipy_fft` can be registered as a backend for SciPy. To learn more about how to control the backend [see the SciPy documentation](https://docs.scipy.org/doc/scipy/reference/fft.html#backend-control). The following example shows how to set `mkl_fft` as the FFT backend for SciPy using a context manager. + +```python +import numpy, scipy, mkl +import mkl_fft.interfaces.scipy_fft as mkl_backend +x = numpy.random.randn(56).reshape(7, 8) + +# turning on verbosity to show `mkl_fft` is used as the SciPy backend +mkl.verbose(1) +# True + +with scipy.fft.set_backend(mkl_backend, only=True): + mkl_res = scipy.fft.fft2(x, workers=4) # Calls `mkl_fft` backend +# MKL_VERBOSE oneMKL 2024.0 Update 2 Patch 2 Product build 20240823 for Intel(R) 64 architecture Intel(R) Advanced Vector Extensions 512 (Intel(R) AVX-512) with support for INT8, BF16, FP16 (limited) instructions, and Intel(R) Advanced Matrix Extensions (Intel(R) AMX) with INT8 and BF16, Lnx 2.00GHz intel_thread +# MKL_VERBOSE FFT(drfo7:8:8x8:1:1,input_strides:{0,8,1},output_strides:{0,8,1},bScale:0.0178571,tLim:1,unaligned_output,desc:0x557affb60d40) 36.11us CNR:OFF Dyn:1 FastMM:1 TID:0 NThr:4 + +sp_res = scipy.fft.fft2(x, workers=4) # Calls default SciPy backend +numpy.allclose(mkl_res, sp_res) +# True +``` + +The previous example was only for illustration purposes. In practice, there is no added benefit to defining a backend and calculating FFT, since this functionality is already accessible through the scipy interface, as shown earlier. +To demonstrate the advantages of using `mkl_fft` as a backend, the following example compares the timing of `scipy.signal.fftconvolve` using the default SciPy backend versus the `mkl_fft` backend on an Intel® Xeon® CPU. + +```python +import numpy, scipy +import mkl_fft.interfaces.scipy_fft as mkl_backend +import timeit +shape = (4096, 2048) +a = numpy.random.randn(*shape) + 1j*numpy.random.randn(*shape) +b = numpy.random.randn(*shape) + 1j*numpy.random.randn(*shape) + +t1 = timeit.timeit(lambda: scipy.signal.fftconvolve(a, b), number=10) +print(f"Time with scipy.fft default backend: {t1:.1f} seconds") +# Time with scipy.fft default backend: 51.9 seconds + +with scipy.fft.set_backend(mkl_backend, only=True): + t2 = timeit.timeit(lambda: scipy.signal.fftconvolve(a, b), number=10) + +print(f"Time with OneMKL FFT backend installed: {t2:.1f} seconds") +# Time with MKL FFT backend installed: 9.1 seconds +``` diff --git a/mkl_fft/interfaces/_scipy_fft.py b/mkl_fft/interfaces/_scipy_fft.py index e310da1d..d1f8facb 100644 --- a/mkl_fft/interfaces/_scipy_fft.py +++ b/mkl_fft/interfaces/_scipy_fft.py @@ -63,47 +63,9 @@ "ihfftn", "get_workers", "set_workers", - "DftiBackend", ] -__doc__ = """ -This module implements interfaces mimicking `scipy.fft` module. - -It also provides DftiBackend class which can be used to set mkl_fft to be used -via `scipy.fft` namespace. - -:Example: - import scipy.fft - import mkl_fft.interfaces._scipy_fft as mkl_be - # Set mkl_fft to be used as backend of SciPy's FFT functions. - scipy.fft.set_global_backend(mkl_be) -""" - - -__ua_domain__ = "numpy.scipy.fft" - - -def __ua_function__(method, args, kwargs): - """Fetch registered UA function.""" - fn = globals().get(method.__name__, None) - if fn is None: - return NotImplemented - return fn(*args, **kwargs) - - -class DftiBackend: - __ua_domain__ = "numpy.scipy.fft" - - @staticmethod - def __ua_function__(method, args, kwargs): - """Fetch registered UA function.""" - fn = globals().get(method.__name__, None) - if fn is None: - return NotImplemented - return fn(*args, **kwargs) - - class _cpu_max_threads_count: def __init__(self): self.cpu_count = None diff --git a/mkl_fft/interfaces/numpy_fft.py b/mkl_fft/interfaces/numpy_fft.py index e30aa786..6ab7e795 100644 --- a/mkl_fft/interfaces/numpy_fft.py +++ b/mkl_fft/interfaces/numpy_fft.py @@ -24,4 +24,44 @@ # 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. -from ._numpy_fft import * +# Added for completing the namespaces +from numpy.fft import fftfreq, fftshift, ifftshift, rfftfreq + +# pylint: disable=no-name-in-module +from ._numpy_fft import ( + fft, + fft2, + fftn, + hfft, + ifft, + ifft2, + ifftn, + ihfft, + irfft, + irfft2, + irfftn, + rfft, + rfft2, + rfftn, +) + +__all__ = [ + "fft", + "ifft", + "fft2", + "ifft2", + "fftn", + "ifftn", + "rfft", + "irfft", + "rfft2", + "irfft2", + "rfftn", + "irfftn", + "hfft", + "ihfft", + "fftshift", + "ifftshift", + "fftfreq", + "rfftfreq", +] diff --git a/mkl_fft/interfaces/scipy_fft.py b/mkl_fft/interfaces/scipy_fft.py index 0b77e4a0..c3ae4717 100644 --- a/mkl_fft/interfaces/scipy_fft.py +++ b/mkl_fft/interfaces/scipy_fft.py @@ -24,4 +24,68 @@ # 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. -from ._scipy_fft import * + +# Added for completing the namespaces +from scipy.fft import fftfreq, fftshift, ifftshift, rfftfreq + +# pylint: disable=no-name-in-module +from ._scipy_fft import ( + fft, + fft2, + fftn, + get_workers, + hfft, + hfft2, + hfftn, + ifft, + ifft2, + ifftn, + ihfft, + ihfft2, + ihfftn, + irfft, + irfft2, + irfftn, + rfft, + rfft2, + rfftn, + set_workers, +) + +__all__ = [ + "fft", + "ifft", + "fft2", + "ifft2", + "fftn", + "ifftn", + "rfft", + "irfft", + "rfft2", + "irfft2", + "rfftn", + "irfftn", + "hfft", + "ihfft", + "hfft2", + "ihfft2", + "hfftn", + "ihfftn", + "fftshift", + "ifftshift", + "fftfreq", + "rfftfreq", + "get_workers", + "set_workers", +] + + +__ua_domain__ = "numpy.scipy.fft" + + +def __ua_function__(method, args, kwargs): + """Fetch registered UA function.""" + fn = globals().get(method.__name__, None) + if fn is None: + return NotImplemented + return fn(*args, **kwargs) diff --git a/mkl_fft/tests/test_interfaces.py b/mkl_fft/tests/test_interfaces.py index c91affae..0a093690 100644 --- a/mkl_fft/tests/test_interfaces.py +++ b/mkl_fft/tests/test_interfaces.py @@ -156,9 +156,7 @@ def test_scipy_fft_arg_validate(): @pytest.mark.parametrize( - "func", - [mfi.scipy_fft.rfft2, mfi.numpy_fft.rfft2], - ids=["scipy", "numpy"], + "func", [mfi.scipy_fft.rfft2, mfi.numpy_fft.rfft2], ids=["scipy", "numpy"] ) def test_axes(func): x = np.arange(24.0).reshape(2, 3, 4) @@ -166,3 +164,13 @@ def test_axes(func): exp = np.fft.rfft2(x, axes=(1, 2)) tol = 64 * np.finfo(np.float64).eps assert np.allclose(res, exp, atol=tol, rtol=tol) + + +@pytest.mark.parametrize( + "interface", [mfi.scipy_fft, mfi.numpy_fft], ids=["scipy", "numpy"] +) +@pytest.mark.parametrize( + "func", ["fftshift", "ifftshift", "fftfreq", "rfftfreq"] +) +def test_interface_helper_functions(interface, func): + assert hasattr(interface, func)