├── mkl_fft ├── tests │ ├── __init__.py │ ├── helper.py │ ├── third_party │ │ └── scipy │ │ │ └── test_multithreading.py │ ├── test_interfaces.py │ ├── test_fftnd.py │ └── test_fft1d.py ├── _version.py ├── interfaces │ ├── __init__.py │ ├── numpy_fft.py │ ├── scipy_fft.py │ ├── _float_utils.py │ ├── _numpy_helper.py │ ├── README.md │ ├── _numpy_fft.py │ └── _scipy_fft.py ├── _init_helper.py ├── __init__.py ├── _mkl_fft.py ├── src │ ├── mklfft.h │ └── multi_iter.h └── _fft_utils.py ├── _vendored ├── __init__.py ├── README.md └── conv_template.py ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── pre-commit.yml │ ├── build_pip.yaml │ ├── build-with-clang.yml │ ├── openssf-scorecard.yml │ ├── conda-package-cf.yml │ └── conda-package.yml ├── conda-recipe-cf ├── bld.bat ├── build.sh └── meta.yaml ├── .git-blame-ignore-revs ├── .gitignore ├── SECURITY.md ├── conda-recipe ├── bld.bat ├── conda_build_config.yaml ├── build.sh └── meta.yaml ├── LICENSE.txt ├── .flake8 ├── .pre-commit-config.yaml ├── pyproject.toml ├── setup.py ├── README.md └── CHANGELOG.md /mkl_fft/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_vendored/__init__.py: -------------------------------------------------------------------------------- 1 | # empty file 2 | -------------------------------------------------------------------------------- /mkl_fft/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.2.0dev0" 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ndgrigorian @antonwolfy @xaleryb @jharlow-intel 2 | -------------------------------------------------------------------------------- /conda-recipe-cf/bld.bat: -------------------------------------------------------------------------------- 1 | set MKLROOT=%PREFIX% 2 | %PYTHON% -m pip install --no-build-isolation --no-deps . 3 | if errorlevel 1 exit 1 4 | -------------------------------------------------------------------------------- /conda-recipe-cf/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | export MKLROOT=$PREFIX 4 | export CFLAGS="-I$PREFIX/include $CFLAGS" 5 | $PYTHON -m pip install --no-build-isolation --no-deps . 6 | -------------------------------------------------------------------------------- /_vendored/README.md: -------------------------------------------------------------------------------- 1 | ## Vendored files 2 | 3 | File `conv_template.py` is copied from NumPy's numpy/distutils folder, since 4 | `numpy.distutils` is absent from the installation layout starting with 5 | Python 3.12 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "saturday" 8 | rebase-strategy: "disabled" 9 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # $ git config blame.ignoreRevsFile .git-blame-ignore-revs 2 | 3 | # Add pre-commit hooks 4 | 2e1b33fcf6b7f0c7c9d7d5d7f55d2d0ba35f393a 5 | 6 | # Add cython-lint to pre-commit config 7 | 5df98187dda56f6d340f0824570fe5080d0b4f05 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # CMake build and local install directory 2 | build/ 3 | mkl_fft.egg-info/ 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | 8 | mkl_fft/_pydfti.c 9 | mkl_fft/_pydfti.cpython*.so 10 | mkl_fft/_pydfti.*-win_amd64.pyd 11 | mkl_fft/src/mklfft.c 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | Intel is committed to rapidly addressing security vulnerabilities affecting our customers and providing clear guidance on the solution, impact, severity and mitigation. 3 | 4 | ## Reporting a Vulnerability 5 | Please report any security vulnerabilities in this project utilizing the guidelines [here](https://www.intel.com/content/www/us/en/security-center/vulnerability-handling-guidelines.html). 6 | -------------------------------------------------------------------------------- /conda-recipe/bld.bat: -------------------------------------------------------------------------------- 1 | set MKLROOT=%PREFIX% 2 | 3 | rem Build wheel package 4 | if NOT "%WHEELS_OUTPUT_FOLDER%"=="" ( 5 | %PYTHON% -m pip wheel --no-build-isolation --no-deps . 6 | if errorlevel 1 exit 1 7 | copy mkl_fft*.whl %WHEELS_OUTPUT_FOLDER% 8 | if errorlevel 1 exit 1 9 | ) ELSE ( 10 | rem Build conda package 11 | %PYTHON% -m pip install --no-build-isolation --no-deps . 12 | if errorlevel 1 exit 1 13 | ) 14 | -------------------------------------------------------------------------------- /conda-recipe/conda_build_config.yaml: -------------------------------------------------------------------------------- 1 | c_compiler: # [linux] 2 | - gcc # [linux] 3 | cxx_compiler: # [linux] 4 | - gxx # [linux] 5 | cxx_compiler_version: # [linux] 6 | - '14' # [linux] 7 | c_stdlib: # [linux] 8 | - sysroot # [linux] 9 | c_stdlib_version: # [linux] 10 | - '2.28' # [linux] 11 | c_stdlib: # [win] 12 | - vs # [win] 13 | cxx_compiler: # [win] 14 | - vs2017 # [win] 15 | c_compiler: # [win] 16 | - vs2017 # [win] 17 | -------------------------------------------------------------------------------- /conda-recipe/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | export MKLROOT=$PREFIX 4 | export CFLAGS="-I$PREFIX/include $CFLAGS" 5 | export LDFLAGS="-Wl,-rpath,\$ORIGIN/../.. -Wl,-rpath,\$ORIGIN/../../.. -L${PREFIX}/lib ${LDFLAGS}" 6 | 7 | read -r GLIBC_MAJOR GLIBC_MINOR <<<"$(conda list '^sysroot_linux-64$' \ 8 | | tail -n 1 | awk '{print $2}' | grep -oP '\d+' | head -n 2 | tr '\n' ' ')" 9 | 10 | # Build wheel package 11 | if [ -n "${WHEELS_OUTPUT_FOLDER}" ]; then 12 | $PYTHON -m pip wheel --no-build-isolation --no-deps . 13 | ${PYTHON} -m wheel tags --remove --platform-tag "manylinux_${GLIBC_MAJOR}_${GLIBC_MINOR}_x86_64" mkl_fft*.whl 14 | cp mkl_fft*.whl "${WHEELS_OUTPUT_FOLDER}" 15 | else 16 | # Build conda package 17 | $PYTHON -m pip install --no-build-isolation --no-deps . 18 | fi 19 | -------------------------------------------------------------------------------- /conda-recipe-cf/meta.yaml: -------------------------------------------------------------------------------- 1 | {% set version = "2.2.0dev0" %} 2 | {% set buildnumber = 0 %} 3 | 4 | package: 5 | name: mkl_fft 6 | version: {{ version }} 7 | 8 | source: 9 | path: ../ 10 | 11 | build: 12 | number: {{ buildnumber }} 13 | ignore_run_exports: 14 | - blas 15 | 16 | requirements: 17 | build: 18 | - {{ compiler('c') }} 19 | host: 20 | - python 21 | - setuptools >=77 22 | - mkl-devel 23 | - cython 24 | - numpy 25 | run: 26 | - python 27 | - mkl-service 28 | - numpy 29 | 30 | test: 31 | commands: 32 | - pytest -v --pyargs mkl_fft 33 | requires: 34 | - pytest 35 | - scipy >=1.10 36 | imports: 37 | - mkl_fft 38 | - mkl_fft.interfaces 39 | - mkl_fft.interfaces.numpy_fft 40 | - mkl_fft.interfaces.scipy_fft 41 | 42 | about: 43 | home: http://github.com/IntelPython/mkl_fft 44 | license: BSD-3-Clause 45 | license_file: LICENSE.txt 46 | summary: NumPy-based implementation of Fast Fourier Transform using Intel® oneAPI Math Kernel Library (OneMKL) 47 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | permissions: read-all 9 | 10 | jobs: 11 | pre-commit: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 30 14 | steps: 15 | - name: Checkout repo 16 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 17 | 18 | - name: Set up python 19 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 20 | with: 21 | python-version: '3.13' 22 | 23 | - name: Set up pip packages 24 | uses: BSFishy/pip-action@8f2d471d809dc20b6ada98c91910b6ae6243f318 # v1 25 | with: 26 | packages: | 27 | codespell 28 | pylint 29 | 30 | - name: Set up clang-format 31 | run: | 32 | sudo apt-get install -y clang-format-14 33 | sudo unlink /usr/bin/clang-format 34 | sudo ln -s /usr/bin/clang-format-14 /usr/bin/clang-format 35 | clang-format --version 36 | 37 | - name: Run pre-commit checks 38 | uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Intel Corporation 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Intel Corporation nor the names of its contributors 12 | may be used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-ignore = 3 | # whitespace before ':' (currently conflicts with black formatting): 4 | E203, 5 | # line too long (in docstrings): 6 | E501, 7 | # ‘from module import *’ used; unable to detect undefined names: 8 | F403, 9 | # doc line too long (105 > 80 characters): 10 | W505, 11 | # missing docstring in public module: 12 | D100, 13 | # missing docstring in public class: 14 | D101, 15 | # missing docstring in public method: 16 | D102, 17 | # missing docstring in public function: 18 | D103, 19 | # missing docstring in public package: 20 | D104, 21 | # missing docstring in magic method: 22 | D105, 23 | # missing docstring in __init__: 24 | D107, 25 | # no blank lines allowed after function docstring: 26 | D202, 27 | # 1 blank line required between summary line and description: 28 | D205, 29 | # first line should end with a period: 30 | D400, 31 | # first line should be in imperative mood: 32 | D401, 33 | # first line should not be the function's "signature": 34 | D402, 35 | # first word of the first line should be properly capitalized 36 | D403, 37 | 38 | per-file-ignores = 39 | mkl_fft/__init__.py: E402, F401 40 | mkl_fft/interfaces/__init__.py: F401 41 | mkl_fft/interfaces/scipy_fft.py: F401 42 | mkl_fft/interfaces/numpy_fft.py: F401 43 | 44 | exclude = _vendored/conv_template.py 45 | 46 | filename = *.py, *.pyx, *.pxi, *.pxd 47 | max_line_length = 80 48 | max-doc-length = 80 49 | show-source = True 50 | 51 | # Print detailed statistic if any issue detected 52 | count = True 53 | statistics = True 54 | -------------------------------------------------------------------------------- /conda-recipe/meta.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: mkl_fft 3 | version: {{ GIT_DESCRIBE_TAG }} 4 | 5 | source: 6 | path: ../ 7 | 8 | build: 9 | number: {{ GIT_DESCRIBE_NUMBER }} 10 | script_env: 11 | - WHEELS_OUTPUT_FOLDER 12 | ignore_run_exports: 13 | - blas 14 | 15 | requirements: 16 | build: 17 | - {{ compiler('c') }} 18 | - {{ stdlib('c') }} 19 | host: 20 | - python 21 | - python-gil # [py>=314] 22 | - pip 23 | - setuptools >=77 24 | - mkl-devel 25 | - cython 26 | - numpy-base 27 | - wheel >=0.41.3 28 | run: 29 | - python 30 | - python-gil # [py>=314] 31 | - mkl-service 32 | - {{ pin_compatible('numpy-base') }} 33 | 34 | test: 35 | commands: 36 | - pytest -v --pyargs mkl_fft 37 | requires: 38 | - pytest 39 | # This is a temporary python restriction 40 | - scipy >=1.10 # [py<314] 41 | imports: 42 | - mkl_fft 43 | - mkl_fft.interfaces 44 | 45 | about: 46 | home: http://github.com/IntelPython/mkl_fft 47 | license: BSD-3-Clause 48 | license_file: LICENSE.txt 49 | summary: NumPy-based implementation of Fast Fourier Transform using Intel® oneAPI Math Kernel Library (oneMKL) 50 | description: | 51 | LEGAL NOTICE: Use of this software package is subject to the 52 | software license agreement (as set forth above, in the license section of 53 | the installed Conda package and/or the README file) and all notices, 54 | disclaimers or license terms for third party or open source software 55 | included in or with the software. 56 |

57 | EULA: BSD-3-Clause 58 |

59 | -------------------------------------------------------------------------------- /mkl_fft/interfaces/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017, Intel Corporation 2 | # 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are met: 5 | # 6 | # * Redistributions of source code must retain the above copyright notice, 7 | # this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of Intel Corporation nor the names of its contributors 12 | # may be used to endorse or promote products derived from this software 13 | # without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from . import numpy_fft 27 | 28 | # find scipy, not scipy.fft, to avoid circular dependency 29 | try: 30 | import scipy 31 | except ImportError: 32 | pass 33 | else: 34 | from . import scipy_fft 35 | -------------------------------------------------------------------------------- /mkl_fft/tests/helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2025, Intel Corporation 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of Intel Corporation nor the names of its contributors 13 | # may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS """AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | 28 | import numpy as np 29 | import pytest 30 | 31 | requires_numpy_2 = pytest.mark.skipif( 32 | np.lib.NumpyVersion(np.__version__) < "2.0.0", 33 | reason="Requires NumPy >= 2.0.0", 34 | ) 35 | -------------------------------------------------------------------------------- /.github/workflows/build_pip.yaml: -------------------------------------------------------------------------------- 1 | name: Editable build using pip and pre-release NumPy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | permissions: read-all 10 | 11 | env: 12 | PACKAGE_NAME: mkl_fft 13 | MODULE_NAME: mkl_fft 14 | TEST_ENV_NAME: test_mkl_fft 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | defaults: 20 | run: 21 | shell: bash -el {0} 22 | 23 | strategy: 24 | matrix: 25 | python: ['3.10', '3.11', '3.12', '3.13'] 26 | use_pre: ["", "--pre"] 27 | 28 | steps: 29 | - name: Install jq 30 | shell: bash -l {0} 31 | run: | 32 | sudo apt-get install jq 33 | 34 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 35 | with: 36 | fetch-depth: 0 37 | 38 | - uses: conda-incubator/setup-miniconda@835234971496cad1653abb28a638a281cf32541f # v3.2.0 39 | with: 40 | use-mamba: true 41 | miniforge-version: latest 42 | channels: conda-forge 43 | conda-remove-defaults: true 44 | activate-environment: test 45 | python-version: ${{ matrix.python }} 46 | 47 | - name: Install MKL 48 | run: | 49 | conda install mkl-devel mkl-service 50 | python -c "import sys; print(sys.executable)" 51 | which python 52 | python -c "import mkl; print(mkl.__file__)" 53 | 54 | - name: Build conda package 55 | run: | 56 | pip install --no-cache-dir cython setuptools 57 | pip install --no-cache-dir numpy ${{ matrix.use_pre }} 58 | echo "CONDA_PREFFIX is '${CONDA_PREFIX}'" 59 | export MKLROOT=${CONDA_PREFIX} 60 | pip install -e .[test] --no-build-isolation --verbose 61 | pip list 62 | python -m pytest -v mkl_fft/tests 63 | -------------------------------------------------------------------------------- /mkl_fft/_init_helper.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Intel Corporation 2 | # 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are met: 5 | # 6 | # * Redistributions of source code must retain the above copyright notice, 7 | # this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of Intel Corporation nor the names of its contributors 12 | # may be used to endorse or promote products derived from this software 13 | # without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | import os 27 | import os.path 28 | import sys 29 | 30 | is_venv_win32 = ( 31 | sys.platform == "win32" 32 | and sys.base_exec_prefix != sys.exec_prefix 33 | and os.path.isfile(os.path.join(sys.exec_prefix, "pyvenv.cfg")) 34 | ) 35 | 36 | if is_venv_win32: 37 | # In Windows venv: add Library/bin to PATH for proper DLL loading 38 | dll_dir = os.path.join(sys.exec_prefix, "Library", "bin") 39 | if os.path.isdir(dll_dir): 40 | os.add_dll_directory(dll_dir) 41 | 42 | del is_venv_win32 43 | -------------------------------------------------------------------------------- /mkl_fft/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017, Intel Corporation 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of Intel Corporation nor the names of its contributors 13 | # may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | from . import _init_helper 28 | from ._mkl_fft import ( 29 | fft, 30 | fft2, 31 | fftn, 32 | ifft, 33 | ifft2, 34 | ifftn, 35 | irfft, 36 | irfft2, 37 | irfftn, 38 | rfft, 39 | rfft2, 40 | rfftn, 41 | ) 42 | from ._version import __version__ 43 | 44 | import mkl_fft.interfaces # isort: skip 45 | 46 | __all__ = [ 47 | "fft", 48 | "ifft", 49 | "fft2", 50 | "ifft2", 51 | "fftn", 52 | "ifftn", 53 | "rfft", 54 | "irfft", 55 | "rfft2", 56 | "irfft2", 57 | "rfftn", 58 | "irfftn", 59 | "interfaces", 60 | ] 61 | 62 | del _init_helper 63 | -------------------------------------------------------------------------------- /mkl_fft/interfaces/numpy_fft.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017, Intel Corporation 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of Intel Corporation nor the names of its contributors 13 | # may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | # pylint: disable=no-name-in-module 28 | from ._numpy_fft import ( 29 | fft, 30 | fft2, 31 | fftn, 32 | hfft, 33 | ifft, 34 | ifft2, 35 | ifftn, 36 | ihfft, 37 | irfft, 38 | irfft2, 39 | irfftn, 40 | rfft, 41 | rfft2, 42 | rfftn, 43 | ) 44 | from ._numpy_helper import fftfreq, fftshift, ifftshift, rfftfreq 45 | 46 | __all__ = [ 47 | "fft", 48 | "ifft", 49 | "fft2", 50 | "ifft2", 51 | "fftn", 52 | "ifftn", 53 | "rfft", 54 | "irfft", 55 | "rfft2", 56 | "irfft2", 57 | "rfftn", 58 | "irfftn", 59 | "hfft", 60 | "ihfft", 61 | "fftshift", 62 | "fftfreq", 63 | "rfftfreq", 64 | "ifftshift", 65 | ] 66 | -------------------------------------------------------------------------------- /.github/workflows/build-with-clang.yml: -------------------------------------------------------------------------------- 1 | name: Build project with IntelLLVM clang compiler 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | permissions: read-all 9 | 10 | jobs: 11 | build-with-clang: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | python: ["3.10", "3.11", "3.12", "3.13"] 17 | numpy_version: ["numpy'>=2'"] 18 | 19 | env: 20 | ONEAPI_ROOT: /opt/intel/oneapi 21 | 22 | defaults: 23 | run: 24 | shell: bash -el {0} 25 | 26 | steps: 27 | - name: Cancel Previous Runs 28 | uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 29 | with: 30 | access_token: ${{ github.token }} 31 | 32 | - name: Add Intel repository 33 | run: | 34 | wget https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS-2023.PUB 35 | sudo apt-key add GPG-PUB-KEY-INTEL-SW-PRODUCTS-2023.PUB 36 | rm GPG-PUB-KEY-INTEL-SW-PRODUCTS-2023.PUB 37 | sudo add-apt-repository "deb https://apt.repos.intel.com/oneapi all main" 38 | sudo apt-get update 39 | 40 | - name: Install Intel OneAPI 41 | run: | 42 | sudo apt-get install intel-oneapi-compiler-dpcpp-cpp 43 | sudo apt-get install intel-oneapi-tbb 44 | sudo apt-get install intel-oneapi-mkl-devel 45 | 46 | - name: Setup Python 47 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 48 | with: 49 | python-version: ${{ matrix.python }} 50 | architecture: x64 51 | 52 | - name: Checkout repo 53 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 54 | with: 55 | fetch-depth: 0 56 | 57 | - name: Install mkl_fft dependencies 58 | run: | 59 | pip install cython setuptools">=77" 60 | pip install ${{ matrix.numpy_version }} 61 | 62 | - name: List oneAPI folder content 63 | run: ls ${{ env.ONEAPI_ROOT }}/compiler 64 | 65 | - name: Build mkl_fft 66 | run: | 67 | source ${{ env.ONEAPI_ROOT }}/setvars.sh 68 | echo $CMPLR_ROOT 69 | export CC=$CMPLR_ROOT/bin/icx 70 | export CFLAGS="${CFLAGS} -fno-fast-math -O2" 71 | pip install -e . --no-build-isolation --no-deps --verbose 72 | 73 | - name: Run mkl_fft tests 74 | run: | 75 | source ${{ env.ONEAPI_ROOT }}/setvars.sh 76 | pip install scipy mkl-service pytest 77 | pytest -s -v --pyargs mkl_fft 78 | -------------------------------------------------------------------------------- /mkl_fft/interfaces/scipy_fft.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017, Intel Corporation 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of Intel Corporation nor the names of its contributors 13 | # may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | # pylint: disable=no-name-in-module 28 | from ._scipy_fft import ( 29 | fft, 30 | fft2, 31 | fftfreq, 32 | fftn, 33 | fftshift, 34 | get_workers, 35 | hfft, 36 | hfft2, 37 | hfftn, 38 | ifft, 39 | ifft2, 40 | ifftn, 41 | ifftshift, 42 | ihfft, 43 | ihfft2, 44 | ihfftn, 45 | irfft, 46 | irfft2, 47 | irfftn, 48 | rfft, 49 | rfft2, 50 | rfftfreq, 51 | rfftn, 52 | set_workers, 53 | ) 54 | 55 | __all__ = [ 56 | "fft", 57 | "ifft", 58 | "fft2", 59 | "ifft2", 60 | "fftn", 61 | "ifftn", 62 | "rfft", 63 | "irfft", 64 | "rfft2", 65 | "irfft2", 66 | "rfftn", 67 | "irfftn", 68 | "hfft", 69 | "ihfft", 70 | "hfft2", 71 | "ihfft2", 72 | "hfftn", 73 | "ihfftn", 74 | "fftshift", 75 | "ifftshift", 76 | "fftfreq", 77 | "rfftfreq", 78 | "get_workers", 79 | "set_workers", 80 | ] 81 | 82 | 83 | __ua_domain__ = "numpy.scipy.fft" 84 | 85 | 86 | def __ua_function__(method, args, kwargs): 87 | """Fetch registered UA function.""" 88 | fn = globals().get(method.__name__, None) 89 | if fn is None: 90 | return NotImplemented 91 | return fn(*args, **kwargs) 92 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-ast 6 | - id: check-builtin-literals 7 | - id: check-case-conflict 8 | - id: check-executables-have-shebangs 9 | - id: check-merge-conflict 10 | - id: check-toml 11 | - id: debug-statements 12 | - id: destroyed-symlinks 13 | - id: end-of-file-fixer 14 | - id: fix-byte-order-marker 15 | - id: mixed-line-ending 16 | - id: trailing-whitespace 17 | 18 | - repo: https://github.com/pre-commit/pygrep-hooks 19 | rev: v1.10.0 20 | hooks: 21 | - id: python-check-blanket-noqa 22 | - id: python-check-blanket-type-ignore 23 | - id: python-check-mock-methods 24 | - id: python-no-eval 25 | - id: python-no-log-warn 26 | - id: python-use-type-annotations 27 | - id: rst-backticks 28 | - id: rst-directive-colons 29 | - id: rst-inline-touching-normal 30 | - id: text-unicode-replacement-char 31 | 32 | - repo: https://github.com/codespell-project/codespell 33 | rev: v2.4.1 34 | hooks: 35 | - id: codespell 36 | args: ["-L", "nd"] # ignore "nd" used for n-dimensional 37 | additional_dependencies: 38 | - tomli 39 | 40 | - repo: https://github.com/psf/black 41 | rev: 25.1.0 42 | hooks: 43 | - id: black 44 | exclude: "_vendored/conv_template.py" 45 | 46 | - repo: https://github.com/pocc/pre-commit-hooks 47 | rev: v1.3.5 48 | hooks: 49 | - id: clang-format 50 | args: ["-i"] 51 | 52 | - repo: https://github.com/MarcoGorelli/cython-lint 53 | rev: v0.16.6 54 | hooks: 55 | - id: cython-lint 56 | - id: double-quote-cython-strings 57 | 58 | - repo: https://github.com/pycqa/flake8 59 | rev: 7.1.2 60 | hooks: 61 | - id: flake8 62 | args: ["--config=.flake8"] 63 | additional_dependencies: 64 | - flake8-docstrings==1.7.0 65 | - flake8-bugbear==24.4.26 66 | 67 | - repo: https://github.com/pycqa/isort 68 | rev: 6.0.1 69 | hooks: 70 | - id: isort 71 | name: isort (python) 72 | - id: isort 73 | name: isort (cython) 74 | types: [cython] 75 | - id: isort 76 | name: isort (pyi) 77 | types: [pyi] 78 | 79 | - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks 80 | rev: v2.14.0 81 | hooks: 82 | - id: pretty-format-toml 83 | args: [--autofix] 84 | 85 | - repo: local 86 | hooks: 87 | - id: pylint 88 | name: pylint 89 | entry: pylint 90 | language: system 91 | types: [python] 92 | require_serial: true 93 | args: 94 | [ 95 | "-rn", # Only display messages 96 | "-sn", # Don't display the score 97 | "--errors-only", 98 | "--disable=import-error", 99 | ] 100 | 101 | - repo: https://github.com/jumanjihouse/pre-commit-hooks 102 | rev: 3.0.0 103 | hooks: 104 | - id: shellcheck 105 | -------------------------------------------------------------------------------- /.github/workflows/openssf-scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '28 2 * * 1' 14 | - cron: '28 2 * * 4' 15 | push: 16 | branches: [ "master" ] 17 | 18 | # Declare default permissions as read only. 19 | permissions: read-all 20 | 21 | jobs: 22 | analysis: 23 | name: Scorecard analysis 24 | runs-on: ubuntu-latest 25 | timeout-minutes: 30 26 | permissions: 27 | # Needed to upload the results to code-scanning dashboard. 28 | security-events: write 29 | # Needed to publish results and get a badge (see publish_results below). 30 | id-token: write 31 | # Uncomment the permissions below if installing in a private repository. 32 | # contents: read 33 | # actions: read 34 | 35 | steps: 36 | - name: "Checkout code" 37 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 38 | with: 39 | persist-credentials: false 40 | 41 | - name: "Run analysis" 42 | uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 43 | with: 44 | results_file: results.sarif 45 | results_format: sarif 46 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 47 | # - you want to enable the Branch-Protection check on a *public* repository, or 48 | # - you are installing Scorecard on a *private* repository 49 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 50 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 51 | 52 | # Public repositories: 53 | # - Publish results to OpenSSF REST API for easy access by consumers 54 | # - Allows the repository to include the Scorecard badge. 55 | # - See https://github.com/ossf/scorecard-action#publishing-results. 56 | # For private repositories: 57 | # - `publish_results` will always be set to `false`, regardless 58 | # of the value entered here. 59 | publish_results: true 60 | 61 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 62 | # format to the repository Actions tab. 63 | - name: "Upload artifact" 64 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 65 | with: 66 | name: SARIF file 67 | path: results.sarif 68 | retention-days: 14 69 | 70 | # Upload the results to GitHub's code scanning dashboard. 71 | - name: "Upload to code-scanning" 72 | uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 73 | with: 74 | sarif_file: results.sarif 75 | -------------------------------------------------------------------------------- /mkl_fft/tests/third_party/scipy/test_multithreading.py: -------------------------------------------------------------------------------- 1 | # This file includes tests from scipy.fft module: 2 | # https://github.com/scipy/scipy/blob/main/scipy/fft/tests/test_multithreading.py.py 3 | 4 | import multiprocessing 5 | 6 | import mkl 7 | import numpy as np 8 | import pytest 9 | from numpy.testing import assert_allclose 10 | 11 | try: 12 | import mkl_fft.interfaces.scipy_fft as fft 13 | except ImportError: 14 | pytest.skip("This test file needs scipy", allow_module_level=True) 15 | 16 | 17 | @pytest.fixture(scope="module") 18 | def x(): 19 | return np.random.randn(512, 128) # Must be large enough to qualify for mt 20 | 21 | 22 | @pytest.mark.parametrize( 23 | "func", 24 | [ 25 | fft.fft, 26 | fft.ifft, 27 | fft.fft2, 28 | fft.ifft2, 29 | fft.fftn, 30 | fft.ifftn, 31 | fft.rfft, 32 | fft.irfft, 33 | fft.rfft2, 34 | fft.irfft2, 35 | fft.rfftn, 36 | fft.irfftn, 37 | fft.hfft, 38 | fft.ihfft, 39 | fft.hfft2, 40 | fft.ihfft2, 41 | fft.hfftn, 42 | fft.ihfftn, 43 | # TODO: fft.dct, fft.idct, fft.dctn, fft.idctn, 44 | # TODO: fft.dst, fft.idst, fft.dstn, fft.idstn, 45 | ], 46 | ) 47 | @pytest.mark.parametrize("workers", [2, -1]) 48 | def test_threaded_same(x, func, workers): 49 | expected = func(x, workers=1) 50 | actual = func(x, workers=workers) 51 | assert_allclose(actual, expected) 52 | 53 | 54 | def _mt_fft(x): 55 | return fft.fft(x, workers=2) 56 | 57 | 58 | # @pytest.mark.slow 59 | def test_mixed_threads_processes(x): 60 | # Test that the fft threadpool is safe to use before & after fork 61 | 62 | expect = fft.fft(x, workers=2) 63 | 64 | with multiprocessing.Pool(2) as p: 65 | res = p.map(_mt_fft, [x for _ in range(4)]) 66 | 67 | for r in res: 68 | assert_allclose(r, expect) 69 | 70 | fft.fft(x, workers=2) 71 | 72 | 73 | def test_invalid_workers(x): 74 | # cpus = os.cpu_count() 75 | threads = mkl.get_max_threads() # pylint: disable=no-member 76 | # cpus and threads are usually the same but in CI, cpus = 4 and threads = 2 77 | # SciPy uses `os.cpu_count()` to get the number of workers, while 78 | # `mkl_fft.interfaces.scipy_fft` uses `mkl.get_max_threads()` 79 | 80 | fft.ifft([1], workers=-threads) 81 | 82 | with pytest.raises(ValueError, match="workers must not be zero"): 83 | fft.fft(x, workers=0) 84 | 85 | with pytest.raises(ValueError, match="workers value out of range"): 86 | fft.ifft(x, workers=-threads - 1) 87 | 88 | 89 | def test_set_get_workers(): 90 | # cpus = os.cpu_count() 91 | threads = mkl.get_max_threads() # pylint: disable=no-member 92 | 93 | # default value is max number of threads unlike stock SciPy 94 | assert fft.get_workers() == threads 95 | with fft.set_workers(4): 96 | assert fft.get_workers() == 4 97 | 98 | with fft.set_workers(-1): 99 | assert fft.get_workers() == threads 100 | 101 | assert fft.get_workers() == 4 102 | 103 | # default value is max number of threads unlike stock SciPy 104 | assert fft.get_workers() == threads 105 | 106 | with fft.set_workers(-threads): 107 | assert fft.get_workers() == 1 108 | 109 | 110 | def test_set_workers_invalid(): 111 | 112 | with pytest.raises(ValueError, match="workers must not be zero"): 113 | with fft.set_workers(0): 114 | pass 115 | 116 | with pytest.raises(ValueError, match="workers value out of range"): 117 | # pylint: disable=no-member 118 | with fft.set_workers(-mkl.get_max_threads() - 1): 119 | pass 120 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Intel Corporation 2 | # 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are met: 5 | # 6 | # * Redistributions of source code must retain the above copyright notice, 7 | # this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of Intel Corporation nor the names of its contributors 12 | # may be used to endorse or promote products derived from this software 13 | # without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | [build-system] 27 | build-backend = "setuptools.build_meta" 28 | requires = ["setuptools>=77", "Cython", "numpy", "mkl-devel"] 29 | 30 | [project] 31 | authors = [ 32 | {name = "Intel Corporation", email = "scripting@intel.com"} 33 | ] 34 | classifiers = [ 35 | "Development Status :: 5 - Production/Stable", 36 | "Intended Audience :: Science/Research", 37 | "Intended Audience :: Developers", 38 | "Programming Language :: C", 39 | "Programming Language :: Python", 40 | "Programming Language :: Python :: 3", 41 | "Programming Language :: Python :: 3.10", 42 | "Programming Language :: Python :: 3.11", 43 | "Programming Language :: Python :: 3.12", 44 | "Programming Language :: Python :: 3.13", 45 | "Programming Language :: Python :: 3.14", 46 | "Programming Language :: Python :: Implementation :: CPython", 47 | "Topic :: Software Development", 48 | "Topic :: Scientific/Engineering", 49 | "Operating System :: Microsoft :: Windows", 50 | "Operating System :: POSIX", 51 | "Operating System :: Unix" 52 | ] 53 | dependencies = ["numpy>=1.26.4", "mkl-service"] 54 | description = "MKL-based FFT transforms for NumPy arrays" 55 | dynamic = ["version"] 56 | keywords = ["DFTI", "FFT", "Fourier", "MKL"] 57 | license = "BSD-3-Clause" 58 | name = "mkl_fft" 59 | readme = {file = "README.md", content-type = "text/markdown"} 60 | requires-python = ">=3.10,<3.15" 61 | 62 | [project.optional-dependencies] 63 | scipy_interface = ["scipy>=1.10"] 64 | test = ["pytest", "scipy>=1.10"] 65 | 66 | [project.urls] 67 | Download = "http://github.com/IntelPython/mkl_fft" 68 | Homepage = "http://github.com/IntelPython/mkl_fft" 69 | 70 | [tool.black] 71 | exclude = "_vendored/conv_template.py" 72 | line-length = 80 73 | 74 | [tool.cython-lint] 75 | ignore = ['E722'] # do not use bare 'except' 76 | max-line-length = 80 77 | 78 | [tool.isort] 79 | ensure_newline_before_comments = true 80 | force_grid_wrap = 0 81 | include_trailing_comma = true 82 | line_length = 80 83 | multi_line_output = 3 84 | skip = ["_vendored/conv_template.py"] 85 | use_parentheses = true 86 | 87 | [tool.setuptools] 88 | include-package-data = true 89 | packages = ["mkl_fft", "mkl_fft.interfaces"] 90 | 91 | [tool.setuptools.dynamic] 92 | version = {attr = "mkl_fft._version.__version__"} 93 | 94 | [tool.setuptools.package-data] 95 | "mkl_fft" = ["tests/**/*.py"] 96 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017, Intel Corporation 2 | # 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are met: 5 | # 6 | # * Redistributions of source code must retain the above copyright notice, 7 | # this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of Intel Corporation nor the names of its contributors 12 | # may be used to endorse or promote products derived from this software 13 | # without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | import os 27 | import sys 28 | from os.path import join 29 | 30 | import Cython.Build 31 | import numpy 32 | from setuptools import Extension, setup 33 | 34 | sys.path.insert(0, os.path.dirname(__file__)) # Ensures local imports work 35 | from _vendored.conv_template import process_file as process_c_file # noqa: E402 36 | 37 | 38 | def extensions(): 39 | mkl_root = os.environ.get("MKLROOT", None) 40 | if mkl_root: 41 | mkl_info = { 42 | "include_dirs": [join(mkl_root, "include")], 43 | "library_dirs": [ 44 | join(mkl_root, "lib"), 45 | join(mkl_root, "lib", "intel64"), 46 | ], 47 | "libraries": ["mkl_rt"], 48 | } 49 | else: 50 | raise ValueError("MKLROOT environment variable not set.") 51 | 52 | mkl_include_dirs = mkl_info.get("include_dirs", []) 53 | mkl_library_dirs = mkl_info.get("library_dirs", []) 54 | mkl_libraries = mkl_info.get("libraries", ["mkl_rt"]) 55 | 56 | mklfft_templ = join("mkl_fft", "src", "mklfft.c.src") 57 | processed_mklfft_fn = join("mkl_fft", "src", "mklfft.c") 58 | src_processed = process_c_file(mklfft_templ) 59 | 60 | with open(processed_mklfft_fn, "w") as fid: 61 | fid.write(src_processed) 62 | 63 | return [ 64 | Extension( 65 | "mkl_fft._pydfti", 66 | sources=[ 67 | join("mkl_fft", "_pydfti.pyx"), 68 | join("mkl_fft", "src", "mklfft.c"), 69 | ], 70 | depends=[ 71 | join("mkl_fft", "src", "mklfft.h"), 72 | join("mkl_fft", "src", "multi_iter.h"), 73 | ], 74 | include_dirs=[join("mkl_fft", "src"), numpy.get_include()] 75 | + mkl_include_dirs, 76 | libraries=mkl_libraries, 77 | library_dirs=mkl_library_dirs, 78 | extra_compile_args=[ 79 | "-DNDEBUG", 80 | # '-ggdb', '-O0', '-Wall', '-Wextra', '-DDEBUG', 81 | ], 82 | define_macros=[ 83 | ("NPY_NO_DEPRECATED_API", None), 84 | ("PY_ARRAY_UNIQUE_SYMBOL", "mkl_fft_ext"), 85 | ], 86 | ) 87 | ] 88 | 89 | 90 | setup( 91 | cmdclass={"build_ext": Cython.Build.build_ext}, 92 | ext_modules=extensions(), 93 | zip_safe=False, 94 | ) 95 | -------------------------------------------------------------------------------- /mkl_fft/interfaces/_float_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017, Intel Corporation 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of Intel Corporation nor the names of its contributors 13 | # may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | import numpy as np 28 | 29 | 30 | def _upcast_float16_array(x): 31 | """ 32 | Used in _scipy_fftpack to upcast float16 to float32, 33 | instead of float64, as mkl_fft would do 34 | """ 35 | if hasattr(x, "dtype"): 36 | xdt = x.dtype 37 | if xdt == np.half: 38 | # no half-precision routines, so convert to single precision 39 | return np.asarray(x, dtype=np.float32) 40 | if xdt == np.longdouble and not xdt == np.float64: 41 | raise ValueError("type %s is not supported" % xdt) 42 | if not isinstance(x, np.ndarray): 43 | _x = np.asarray(x) 44 | xdt = _x.dtype 45 | if xdt == np.half: 46 | # no half-precision routines, so convert to single precision 47 | return np.asarray(_x, dtype=np.float32) 48 | if xdt == np.longdouble and not xdt == np.float64: 49 | raise ValueError("type %s is not supported" % xdt) 50 | return _x 51 | return x 52 | 53 | 54 | def _downcast_float128_array(x): 55 | """ 56 | Used in _numpy_fft to unsafely downcast float128/complex256 to 57 | complex128, instead of raising an error 58 | """ 59 | if hasattr(x, "dtype"): 60 | xdt = x.dtype 61 | if xdt == np.longdouble and not xdt == np.float64: 62 | return np.asarray(x, dtype=np.float64) 63 | elif xdt == np.clongdouble and not xdt == np.complex128: 64 | return np.asarray(x, dtype=np.complex128) 65 | if not isinstance(x, np.ndarray): 66 | _x = np.asarray(x) 67 | xdt = _x.dtype 68 | if xdt == np.longdouble and not xdt == np.float64: 69 | return np.asarray(x, dtype=np.float64) 70 | elif xdt == np.clongdouble and not xdt == np.complex128: 71 | return np.asarray(x, dtype=np.complex128) 72 | return _x 73 | return x 74 | 75 | 76 | def _supported_array_or_not_implemented(x): 77 | """ 78 | Used in _scipy_fft to convert array to float32, float64, complex64, 79 | or complex128 type or raise NotImplementedError 80 | """ 81 | _x = np.asarray(x) 82 | black_list = [np.half] 83 | if hasattr(np, "float128"): 84 | black_list.append(np.float128) 85 | if hasattr(np, "complex256"): 86 | black_list.append(np.complex256) 87 | if _x.dtype in black_list: 88 | raise NotImplementedError 89 | return _x 90 | -------------------------------------------------------------------------------- /mkl_fft/_mkl_fft.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2025, Intel Corporation 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of Intel Corporation nor the names of its contributors 13 | # may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | from ._fft_utils import ( 28 | _c2c_fftnd_impl, 29 | _c2r_fftnd_impl, 30 | _compute_fwd_scale, 31 | _r2c_fftnd_impl, 32 | ) 33 | 34 | # pylint: disable=no-name-in-module 35 | from ._pydfti import _c2c_fft1d_impl, _c2r_fft1d_impl, _r2c_fft1d_impl 36 | 37 | __all__ = [ 38 | "fft", 39 | "ifft", 40 | "fft2", 41 | "ifft2", 42 | "fftn", 43 | "ifftn", 44 | "rfft", 45 | "irfft", 46 | "rfft2", 47 | "irfft2", 48 | "rfftn", 49 | "irfftn", 50 | ] 51 | 52 | 53 | def fft(x, n=None, axis=-1, norm=None, out=None): 54 | fsc = _compute_fwd_scale(norm, n, x.shape[axis]) 55 | return _c2c_fft1d_impl(x, n=n, axis=axis, out=out, direction=+1, fsc=fsc) 56 | 57 | 58 | def ifft(x, n=None, axis=-1, norm=None, out=None): 59 | fsc = _compute_fwd_scale(norm, n, x.shape[axis]) 60 | return _c2c_fft1d_impl(x, n=n, axis=axis, out=out, direction=-1, fsc=fsc) 61 | 62 | 63 | def fft2(x, s=None, axes=(-2, -1), norm=None, out=None): 64 | return fftn(x, s=s, axes=axes, norm=norm, out=out) 65 | 66 | 67 | def ifft2(x, s=None, axes=(-2, -1), norm=None, out=None): 68 | return ifftn(x, s=s, axes=axes, norm=norm, out=out) 69 | 70 | 71 | def fftn(x, s=None, axes=None, norm=None, out=None): 72 | fsc = _compute_fwd_scale(norm, s, x.shape) 73 | return _c2c_fftnd_impl(x, s=s, axes=axes, out=out, direction=+1, fsc=fsc) 74 | 75 | 76 | def ifftn(x, s=None, axes=None, norm=None, out=None): 77 | fsc = _compute_fwd_scale(norm, s, x.shape) 78 | return _c2c_fftnd_impl(x, s=s, axes=axes, out=out, direction=-1, fsc=fsc) 79 | 80 | 81 | def rfft(x, n=None, axis=-1, norm=None, out=None): 82 | fsc = _compute_fwd_scale(norm, n, x.shape[axis]) 83 | return _r2c_fft1d_impl(x, n=n, axis=axis, out=out, fsc=fsc) 84 | 85 | 86 | def irfft(x, n=None, axis=-1, norm=None, out=None): 87 | fsc = _compute_fwd_scale(norm, n, 2 * (x.shape[axis] - 1)) 88 | return _c2r_fft1d_impl(x, n=n, axis=axis, out=out, fsc=fsc) 89 | 90 | 91 | def rfft2(x, s=None, axes=(-2, -1), norm=None, out=None): 92 | return rfftn(x, s=s, axes=axes, norm=norm, out=out) 93 | 94 | 95 | def irfft2(x, s=None, axes=(-2, -1), norm=None, out=None): 96 | return irfftn(x, s=s, axes=axes, norm=norm, out=out) 97 | 98 | 99 | def rfftn(x, s=None, axes=None, norm=None, out=None): 100 | fsc = _compute_fwd_scale(norm, s, x.shape) 101 | return _r2c_fftnd_impl(x, s=s, axes=axes, out=out, fsc=fsc) 102 | 103 | 104 | def irfftn(x, s=None, axes=None, norm=None, out=None): 105 | fsc = _compute_fwd_scale(norm, s, x.shape) 106 | return _c2r_fftnd_impl(x, s=s, axes=axes, out=out, fsc=fsc) 107 | -------------------------------------------------------------------------------- /mkl_fft/interfaces/_numpy_helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017, Intel Corporation 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of Intel Corporation nor the names of its contributors 13 | # may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | """ 28 | FFT helper functions copied from `numpy.fft` (with some modification) to 29 | prevent circular dependencies when patching NumPy. 30 | """ 31 | 32 | import numpy as np 33 | 34 | __all__ = ["fftshift", "ifftshift", "fftfreq", "rfftfreq"] 35 | 36 | 37 | def fftshift(x, axes=None): 38 | """ 39 | Shift the zero-frequency component to the center of the spectrum. 40 | 41 | For full documentation refer to `numpy.fft.fftshift`. 42 | 43 | """ 44 | x = np.asarray(x) 45 | if axes is None: 46 | axes = tuple(range(x.ndim)) 47 | shift = [dim // 2 for dim in x.shape] 48 | elif isinstance(axes, (int, np.integer)): 49 | shift = x.shape[axes] // 2 50 | else: 51 | shift = [x.shape[ax] // 2 for ax in axes] 52 | 53 | return np.roll(x, shift, axes) 54 | 55 | 56 | def ifftshift(x, axes=None): 57 | """ 58 | The inverse of `fftshift`. Although identical for even-length `x`, the 59 | functions differ by one sample for odd-length `x`. 60 | 61 | For full documentation refer to `numpy.fft.ifftshift`. 62 | 63 | """ 64 | x = np.asarray(x) 65 | if axes is None: 66 | axes = tuple(range(x.ndim)) 67 | shift = [-(dim // 2) for dim in x.shape] 68 | elif isinstance(axes, (int, np.integer)): 69 | shift = -(x.shape[axes] // 2) 70 | else: 71 | shift = [-(x.shape[ax] // 2) for ax in axes] 72 | 73 | return np.roll(x, shift, axes) 74 | 75 | 76 | def fftfreq(n, d=1.0, device=None): 77 | """ 78 | Return the Discrete Fourier Transform sample frequencies. 79 | 80 | For full documentation refer to `numpy.fft.fftfreq`. 81 | 82 | """ 83 | if not isinstance(n, (int, np.integer)): 84 | raise ValueError("n should be an integer") 85 | val = 1.0 / (n * d) 86 | # pylint: disable=unexpected-keyword-arg 87 | results = np.empty(n, int, device=device) 88 | # pylint: enable=unexpected-keyword-arg 89 | N = (n - 1) // 2 + 1 90 | # pylint: disable=unexpected-keyword-arg 91 | p1 = np.arange(0, N, dtype=int, device=device) 92 | # pylint: enable=unexpected-keyword-arg 93 | results[:N] = p1 94 | # pylint: disable=unexpected-keyword-arg 95 | p2 = np.arange(-(n // 2), 0, dtype=int, device=device) 96 | # pylint: enable=unexpected-keyword-arg 97 | results[N:] = p2 98 | return results * val 99 | 100 | 101 | def rfftfreq(n, d=1.0, device=None): 102 | """ 103 | Return the Discrete Fourier Transform sample frequencies (for usage with 104 | `rfft`, `irfft`). 105 | 106 | For full documentation refer to `numpy.fft.rfftfreq`. 107 | 108 | """ 109 | if not isinstance(n, (int, np.integer)): 110 | raise ValueError("n should be an integer") 111 | val = 1.0 / (n * d) 112 | N = n // 2 + 1 113 | # pylint: disable=unexpected-keyword-arg 114 | results = np.arange(0, N, dtype=int, device=device) 115 | # pylint: enable=unexpected-keyword-arg 116 | return results * val 117 | -------------------------------------------------------------------------------- /mkl_fft/interfaces/README.md: -------------------------------------------------------------------------------- 1 | # Interfaces 2 | The `mkl_fft` package provides interfaces that serve as drop-in replacements for equivalent functions in NumPy and SciPy. 3 | 4 | --- 5 | 6 | ## NumPy interface - `mkl_fft.interfaces.numpy_fft` 7 | 8 | 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: 9 | 10 | * complex-to-complex FFTs: `fft`, `ifft`, `fft2`, `ifft2`, `fftn`, `ifftn`. 11 | 12 | * real-to-complex and complex-to-real FFTs: `rfft`, `irfft`, `rfft2`, `irfft2`, `rfftn`, `irfftn`. 13 | 14 | * Hermitian FFTs: `hfft`, `ihfft`. 15 | 16 | * Helper routines: `fftfreq`, `rfftfreq`, `fftshift`, `ifftshift`. These routines serve as a fallback to the NumPy implementation and are included for completeness. 17 | 18 | The following example shows how to use this interface for calculating a 1D FFT. 19 | 20 | ```python 21 | import numpy 22 | import mkl_fft.interfaces.numpy_fft as numpy_fft 23 | 24 | a = numpy.random.randn(10) + 1j*numpy.random.randn(10) 25 | 26 | mkl_res = numpy_fft.fft(a) 27 | np_res = numpy.fft.fft(a) 28 | numpy.allclose(mkl_res, np_res) 29 | # True 30 | ``` 31 | 32 | --- 33 | 34 | ## SciPy interface - `mkl_fft.interfaces.scipy_fft` 35 | 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: 36 | 37 | * complex-to-complex FFTs: `fft`, `ifft`, `fft2`, `ifft2`, `fftn`, `ifftn`. 38 | 39 | * real-to-complex and complex-to-real FFTs: `rfft`, `irfft`, `rfft2`, `irfft2`, `rfftn`, `irfftn`. 40 | 41 | * Hermitian FFTs: `hfft`, `ihfft`, `hfft2`, `ihfft2`, `hfftn`, `ihfftn`. 42 | 43 | * 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. 44 | 45 | Note that in computing FFTs, the default value of `workers` parameter is the maximum number of threads available unlike the default behavior of SciPy where only one thread is used. 46 | 47 | The following example shows how to use this interface for calculating a 1D FFT. 48 | 49 | ```python 50 | import numpy, scipy 51 | import mkl_fft.interfaces.scipy_fft as scipy_fft 52 | 53 | a = numpy.random.randn(10) + 1j * numpy.random.randn(10) 54 | 55 | mkl_res = scipy_fft.fft(a) 56 | sp_res = scipy.fft.fft(a) 57 | numpy.allclose(mkl_res, sp_res) 58 | # True 59 | ``` 60 | 61 | --- 62 | 63 | ### Registering `mkl_fft` as the FFT backend for SciPy 64 | 65 | `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. 66 | 67 | ```python 68 | import numpy, scipy, mkl 69 | import mkl_fft.interfaces.scipy_fft as mkl_backend 70 | x = numpy.random.randn(56).reshape(7, 8) 71 | 72 | # turning on verbosity to show `mkl_fft` is used as the SciPy backend 73 | mkl.verbose(1) 74 | # True 75 | 76 | with scipy.fft.set_backend(mkl_backend, only=True): 77 | mkl_res = scipy.fft.fft2(x, workers=4) # Calls `mkl_fft` backend 78 | # 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 79 | # 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 80 | 81 | sp_res = scipy.fft.fft2(x, workers=4) # Calls default SciPy backend 82 | numpy.allclose(mkl_res, sp_res) 83 | # True 84 | ``` 85 | 86 | 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. 87 | 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. 88 | 89 | ```python 90 | import numpy, scipy 91 | import mkl_fft.interfaces.scipy_fft as mkl_backend 92 | import timeit 93 | shape = (4096, 2048) 94 | a = numpy.random.randn(*shape) + 1j*numpy.random.randn(*shape) 95 | b = numpy.random.randn(*shape) + 1j*numpy.random.randn(*shape) 96 | 97 | t1 = timeit.timeit(lambda: scipy.signal.fftconvolve(a, b), number=10) 98 | print(f"Time with scipy.fft default backend: {t1:.1f} seconds") 99 | # Time with scipy.fft default backend: 51.9 seconds 100 | 101 | with scipy.fft.set_backend(mkl_backend, only=True): 102 | t2 = timeit.timeit(lambda: scipy.signal.fftconvolve(a, b), number=10) 103 | 104 | print(f"Time with oneMKL FFT backend installed: {t2:.1f} seconds") 105 | # Time with MKL FFT backend installed: 9.1 seconds 106 | ``` 107 | 108 | In the following example, we use `set_worker` to control the number of threads when `mkl_fft` is being used as a backend for SciPy. 109 | 110 | ```python 111 | import numpy, mkl, scipy 112 | import mkl_fft.interfaces.scipy_fft as mkl_fft 113 | a = numpy.random.randn(128, 64) + 1j*numpy.random.randn(128, 64) 114 | scipy.fft.set_global_backend(mkl_fft) # set mkl_fft as global backend 115 | 116 | mkl.verbose(1) 117 | # True 118 | mkl.get_max_threads() 119 | # 112 120 | y = scipy.signal.fftconvolve(a, a) # Note that Nthr:112 121 | # MKL_VERBOSE FFT(dcbo256x128,input_strides:{0,128,1},output_strides:{0,128,1},bScale:3.05176e-05,tLim:56,unaligned_input,unaligned_output,desc:0x563aefe86180) 165.02us CNR:OFF Dyn:1 FastMM:1 TID:0 NThr:112 122 | 123 | with mkl_fft.set_workers(4): 124 | y = scipy.signal.fftconvolve(a, a) # Note that Nthr:4 125 | # MKL_VERBOSE FFT(dcbo256x128,input_strides:{0,128,1},output_strides:{0,128,1},bScale:3.05176e-05,tLim:4,unaligned_output,desc:0x563aefe86180) 187.37us CNR:OFF Dyn:1 FastMM:1 TID:0 NThr:4 126 | ``` 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Conda package](https://github.com/IntelPython/mkl_fft/actions/workflows/conda-package.yml/badge.svg)](https://github.com/IntelPython/mkl_fft/actions/workflows/conda-package.yml) 2 | [![Editable build using pip and pre-release NumPy](https://github.com/IntelPython/mkl_fft/actions/workflows/build_pip.yaml/badge.svg)](https://github.com/IntelPython/mkl_fft/actions/workflows/build_pip.yaml) 3 | [![Conda package with conda-forge channel only](https://github.com/IntelPython/mkl_fft/actions/workflows/conda-package-cf.yml/badge.svg)](https://github.com/IntelPython/mkl_fft/actions/workflows/conda-package-cf.yml) 4 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/IntelPython/mkl_fft/badge)](https://securityscorecards.dev/viewer/?uri=github.com/IntelPython/mkl_fft) 5 | 6 | ## `mkl_fft` -- a NumPy-based Python interface to Intel® oneAPI Math Kernel Library (oneMKL) Fourier Transform Functions 7 | 8 | # Introduction 9 | `mkl_fft` is part of [Intel® Distribution for Python*](https://www.intel.com/content/www/us/en/developer/tools/oneapi/distribution-for-python.html) optimizations to NumPy. 10 | It offers a thin layered python interface to the [Intel® oneAPI Math Kernel Library (oneMKL) Fourier Transform Functions](https://www.intel.com/content/www/us/en/docs/onemkl/developer-reference-c/2025-2/fourier-transform-functions.html) that allows efficient access to computing a discrete Fourier transform through the fast Fourier transform (FFT) algorithm. As a result, its performance is close to the performance of native C/Intel® oneMKL. The optimizations are provided for real and complex data types in both single and double precisions for in-place and out-of-place modes of operation. For analyzing the performance use [FFT benchmarks](https://github.com/intelpython/fft_benchmark). 11 | 12 | Thanks to Intel® oneMKL’s flexibility in its supports for arbitrarily strided input and output arrays both one-dimensional and multi-dimensional FFTs along distinct axes can be performed directly, without the need to copy the input into a contiguous array first. Furthermore, input strides can be arbitrary, including negative or zero, as long as strides remain an integer multiple of array’s item size, otherwise a copy will be made. 13 | 14 | More details can be found in ["Accelerating Scientific Python with Intel Optimizations"](https://proceedings.scipy.org/articles/shinma-7f4c6e7-00f) from Proceedings of the 16th Python in Science Conference (SciPy 2017). 15 | 16 | --- 17 | # Installation 18 | `mkl_fft` can be installed into conda environment from Intel's channel using: 19 | 20 | ``` 21 | conda install -c https://software.repos.intel.com/python/conda mkl_fft 22 | ``` 23 | 24 | or from conda-forge channel: 25 | 26 | ``` 27 | conda install -c conda-forge mkl_fft 28 | ``` 29 | 30 | To install `mkl_fft` PyPI package please use the following command: 31 | 32 | ``` 33 | python -m pip install --index-url https://software.repos.intel.com/python/pypi --extra-index-url https://pypi.org/simple mkl_fft 34 | ``` 35 | 36 | If command above installs NumPy package from the PyPI, please use following command to install Intel optimized NumPy wheel package from Intel PyPI Cloud: 37 | 38 | ``` 39 | python -m pip install --index-url https://software.repos.intel.com/python/pypi --extra-index-url https://pypi.org/simple mkl_fft numpy== 40 | ``` 41 | 42 | where `` should be the latest version from https://software.repos.intel.com/python/conda/. 43 | 44 | --- 45 | # How to use? 46 | ## `mkl_fft.interfaces` module 47 | The recommended way to use `mkl_fft` package is through `mkl_fft.interfaces` module. These interfaces 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). 48 | 49 | ## `mkl_fft` package 50 | While using the interfaces module is the recommended way to leverage `mk_fft`, one can also use `mkl_fft` directly with the following FFT functions: 51 | 52 | ### complex-to-complex (c2c) transforms: 53 | 54 | `fft(x, n=None, axis=-1, norm=None, out=None)` - 1D FFT, similar to `scipy.fft.fft` 55 | 56 | `fft2(x, s=None, axes=(-2, -1), norm=None, out=None)` - 2D FFT, similar to `scipy.fft.fft2` 57 | 58 | `fftn(x, s=None, axes=None, norm=None, out=None)` - ND FFT, similar to `scipy.fft.fftn` 59 | 60 | and similar inverse FFT (`ifft*`) functions. 61 | 62 | ### real-to-complex (r2c) and complex-to-real (c2r) transforms: 63 | 64 | `rfft(x, n=None, axis=-1, norm=None, out=None)` - r2c 1D FFT, similar to `numpy.fft.rfft` 65 | 66 | `rfft2(x, s=None, axes=(-2, -1), norm=None, out=None)` - r2c 2D FFT, similar to `numpy.fft.rfft2` 67 | 68 | `rfftn(x, s=None, axes=None, norm=None, out=None)` - r2c ND FFT, similar to `numpy.fft.rfftn` 69 | 70 | and similar inverse c2r FFT (`irfft*`) functions. 71 | 72 | The following example shows how to use `mkl_fft` for calculating a 1D FFT. 73 | 74 | ```python 75 | import numpy, mkl_fft 76 | a = numpy.random.randn(10) + 1j*numpy.random.randn(10) 77 | 78 | mkl_res = mkl_fft.fft(a) 79 | np_res = numpy.fft.fft(a) 80 | numpy.allclose(mkl_res, np_res) 81 | # True 82 | ``` 83 | 84 | --- 85 | # Building from source 86 | 87 | To build `mkl_fft` from sources on Linux with Intel® oneMKL: 88 | - create a virtual environment: `python3 -m venv fft_env` 89 | - activate the environment: `source fft_env/bin/activate` 90 | - install a recent version of oneMKL, if necessary 91 | - execute `source /path_to_oneapi/mkl/latest/env/vars.sh` 92 | - `git clone https://github.com/IntelPython/mkl_fft.git mkl_fft` 93 | - `cd mkl_fft` 94 | - `python -m pip install .` 95 | - `pip install scipy` (optional: for using `mkl_fft.interface.scipy_fft` module) 96 | - `cd ..` 97 | - `python -c "import mkl_fft"` 98 | 99 | To build `mkl_fft` from sources on Linux with conda follow these steps: 100 | - `conda create -n fft_env python=3.12 mkl-devel` 101 | - `conda activate fft_env` 102 | - `export MKLROOT=$CONDA_PREFIX` 103 | - `git clone https://github.com/IntelPython/mkl_fft.git mkl_fft` 104 | - `cd mkl_fft` 105 | - `python -m pip install .` 106 | - `conda install scipy` (optional: for using `mkl_fft.interface.scipy_fft` module) 107 | - `cd ..` 108 | - `python -c "import mkl_fft"` 109 | -------------------------------------------------------------------------------- /mkl_fft/src/mklfft.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Intel Corporation 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of Intel Corporation nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | #include "mkl.h" 28 | #include "numpy/arrayobject.h" 29 | 30 | typedef struct DftiCache { 31 | DFTI_DESCRIPTOR_HANDLE hand; 32 | int initialized; 33 | } DftiCache; 34 | 35 | extern int _free_dfti_cache(DftiCache *); 36 | 37 | /* Complex input, in-place */ 38 | extern int cdouble_mkl_fft1d_in(PyArrayObject *, npy_intp, int, double, 39 | DftiCache *); 40 | extern int cfloat_mkl_fft1d_in(PyArrayObject *, npy_intp, int, double, 41 | DftiCache *); 42 | extern int cdouble_mkl_ifft1d_in(PyArrayObject *, npy_intp, int, double, 43 | DftiCache *); 44 | extern int cfloat_mkl_ifft1d_in(PyArrayObject *, npy_intp, int, double, 45 | DftiCache *); 46 | 47 | /* Complex input/output, out-of-place */ 48 | extern int cfloat_cfloat_mkl_fft1d_out(PyArrayObject *, npy_intp, int, 49 | PyArrayObject *, double, DftiCache *); 50 | extern int cdouble_cdouble_mkl_fft1d_out(PyArrayObject *, npy_intp, int, 51 | PyArrayObject *, double, DftiCache *); 52 | extern int cfloat_cfloat_mkl_ifft1d_out(PyArrayObject *, npy_intp, int, 53 | PyArrayObject *, double, DftiCache *); 54 | extern int cdouble_cdouble_mkl_ifft1d_out(PyArrayObject *, npy_intp, int, 55 | PyArrayObject *, double, DftiCache *); 56 | 57 | /* Real input, complex output, out-of-place */ 58 | extern int float_cfloat_mkl_fft1d_out(PyArrayObject *, npy_intp, int, 59 | PyArrayObject *, int, double, 60 | DftiCache *); 61 | extern int double_cdouble_mkl_fft1d_out(PyArrayObject *, npy_intp, int, 62 | PyArrayObject *, int, double, 63 | DftiCache *); 64 | extern int float_cfloat_mkl_ifft1d_out(PyArrayObject *, npy_intp, int, 65 | PyArrayObject *, int, double, 66 | DftiCache *); 67 | extern int double_cdouble_mkl_ifft1d_out(PyArrayObject *, npy_intp, int, 68 | PyArrayObject *, int, double, 69 | DftiCache *); 70 | 71 | /* Real input, real output, in-place */ 72 | extern int float_mkl_rfft_in(PyArrayObject *, npy_intp, int, double, 73 | DftiCache *); 74 | extern int float_mkl_irfft_in(PyArrayObject *, npy_intp, int, double, 75 | DftiCache *); 76 | 77 | extern int double_mkl_rfft_in(PyArrayObject *, npy_intp, int, double, 78 | DftiCache *); 79 | extern int double_mkl_irfft_in(PyArrayObject *, npy_intp, int, double, 80 | DftiCache *); 81 | 82 | /* Real input, real output, out-of-place */ 83 | extern int float_float_mkl_rfft_out(PyArrayObject *, npy_intp, int, 84 | PyArrayObject *, double, DftiCache *); 85 | extern int float_float_mkl_irfft_out(PyArrayObject *, npy_intp, int, 86 | PyArrayObject *, double, DftiCache *); 87 | 88 | extern int double_double_mkl_rfft_out(PyArrayObject *, npy_intp, int, 89 | PyArrayObject *, double, DftiCache *); 90 | extern int double_double_mkl_irfft_out(PyArrayObject *, npy_intp, int, 91 | PyArrayObject *, double, DftiCache *); 92 | 93 | /* Complex input. real output, out-of-place */ 94 | extern int cdouble_double_mkl_irfft_out(PyArrayObject *, npy_intp, int, 95 | PyArrayObject *, double, DftiCache *); 96 | extern int cfloat_float_mkl_irfft_out(PyArrayObject *, npy_intp, int, 97 | PyArrayObject *, double, DftiCache *); 98 | 99 | /* Complex, ND, in-place */ 100 | extern int cdouble_cdouble_mkl_fftnd_in(PyArrayObject *, double); 101 | extern int cdouble_cdouble_mkl_ifftnd_in(PyArrayObject *, double); 102 | extern int cfloat_cfloat_mkl_fftnd_in(PyArrayObject *, double); 103 | extern int cfloat_cfloat_mkl_ifftnd_in(PyArrayObject *, double); 104 | 105 | extern int cdouble_cdouble_mkl_fftnd_out(PyArrayObject *, PyArrayObject *, 106 | double); 107 | extern int cdouble_cdouble_mkl_ifftnd_out(PyArrayObject *, PyArrayObject *, 108 | double); 109 | extern int cfloat_cfloat_mkl_fftnd_out(PyArrayObject *, PyArrayObject *, 110 | double); 111 | extern int cfloat_cfloat_mkl_ifftnd_out(PyArrayObject *, PyArrayObject *, 112 | double); 113 | 114 | extern int float_cfloat_mkl_fftnd_out(PyArrayObject *, PyArrayObject *, double); 115 | extern int float_cfloat_mkl_ifftnd_out(PyArrayObject *, PyArrayObject *, 116 | double); 117 | extern int double_cdouble_mkl_fftnd_out(PyArrayObject *, PyArrayObject *, 118 | double); 119 | extern int double_cdouble_mkl_ifftnd_out(PyArrayObject *, PyArrayObject *, 120 | double); 121 | 122 | extern char *mkl_dfti_error(MKL_LONG); 123 | -------------------------------------------------------------------------------- /mkl_fft/src/multi_iter.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017, Intel Corporation 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of Intel Corporation nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | #ifndef MULTI_ITER_H 29 | #define MULTI_ITER_H 30 | 31 | #include "mkl.h" 32 | #include "numpy/arrayobject.h" 33 | #include 34 | #include 35 | 36 | typedef struct multi_iter_t { 37 | npy_intp *shape; // shape of the tensor 38 | npy_intp *ind; // multi-index 39 | int rank; // tensor rank, length of shape and of ind 40 | char done; // boolean variable: True when end of iterator has been reached 41 | } multi_iter_t; 42 | 43 | typedef struct multi_iter_masked_t { 44 | npy_intp *shape; // shape of the tensor 45 | npy_intp *ind; // multi-index 46 | int *mask; // list of indexes to iterate over 47 | int rank; // tensor rank, length of shape and of ind 48 | int mask_len; 49 | char done; // boolean variable: True when end of iterator has been reached 50 | } multi_iter_masked_t; 51 | 52 | #define MultiIter_Index(mit) ((mit).ind) 53 | #define MultiIter_Shape(mit) ((mit).shape) 54 | #define MultiIter_Done(mit) ((mit).done) 55 | #define MultiIter_Rank(mit) ((mit).rank) 56 | #define MultiIter_Mask(mit) ((mit).mask) 57 | #define MultiIter_MaskLength(mit) ((mit).mask_len) 58 | 59 | #define MultiIter_IndexElem(mit, i) ((mit).ind)[(i)] 60 | #define MultiIter_ShapeElem(mit, i) ((mit).shape)[(i)] 61 | #define MultiIter_MaskElem(mit, i) ((mit).mask)[(i)] 62 | 63 | /* 64 | void multi_iter_new(multi_iter_t*, npy_intp*, int); 65 | void multi_iter_masked_new(multi_iter_masked_t*, npy_intp*, int, int*, int); 66 | 67 | void multi_iter_free(multi_iter_t*); 68 | int multi_iter_next(multi_iter_t*); 69 | 70 | void multi_iter_masked_free(multi_iter_masked_t*); 71 | int multi_iter_masked_next(multi_iter_masked_t*); 72 | */ 73 | 74 | static NPY_INLINE void multi_iter_new(multi_iter_t *mi, npy_intp shape[], 75 | int rank) { 76 | int i; 77 | char d = 0; 78 | 79 | assert(rank > 0); 80 | 81 | MultiIter_Index(*mi) = (npy_intp *)mkl_calloc(rank, sizeof(npy_intp), 64); 82 | MultiIter_Shape(*mi) = (npy_intp *)mkl_malloc(rank * sizeof(npy_intp), 64); 83 | memcpy(MultiIter_Shape(*mi), shape, rank * sizeof(npy_intp)); 84 | MultiIter_Rank(*mi) = rank; 85 | 86 | for (i = 0; i < rank; i++) { 87 | d |= MultiIter_IndexElem(*mi, i) >= MultiIter_ShapeElem(*mi, i); 88 | if (d) 89 | break; 90 | } 91 | 92 | MultiIter_Done(*mi) = d; 93 | 94 | return; 95 | } 96 | 97 | static NPY_INLINE void multi_iter_masked_new(multi_iter_masked_t *mi, 98 | npy_intp shape[], int rank, 99 | int mask[], int mask_len) { 100 | int i; 101 | char d = 0; 102 | 103 | assert(rank > 0); 104 | 105 | MultiIter_Index(*mi) = (npy_intp *)mkl_calloc(rank, sizeof(npy_intp), 64); 106 | MultiIter_Shape(*mi) = (npy_intp *)mkl_malloc(rank * sizeof(npy_intp), 64); 107 | memcpy(MultiIter_Shape(*mi), shape, rank * sizeof(npy_intp)); 108 | MultiIter_Rank(*mi) = rank; 109 | 110 | for (i = 0; i < rank; i++) { 111 | d |= MultiIter_IndexElem(*mi, i) >= MultiIter_ShapeElem(*mi, i); 112 | if (d) 113 | break; 114 | } 115 | 116 | MultiIter_Done(*mi) = d; 117 | 118 | assert(mask_len > 0); 119 | MultiIter_MaskLength(*mi) = mask_len; 120 | MultiIter_Mask(*mi) = (int *)mkl_malloc(mask_len * sizeof(int), 64); 121 | memcpy(MultiIter_Mask(*mi), mask, mask_len * sizeof(int)); 122 | 123 | return; 124 | } 125 | 126 | static NPY_INLINE void multi_iter_masked_free(multi_iter_masked_t *mi) { 127 | if (mi) { 128 | if (MultiIter_Index(*mi)) 129 | mkl_free(MultiIter_Index(*mi)); 130 | 131 | if (MultiIter_Shape(*mi)) 132 | mkl_free(MultiIter_Shape(*mi)); 133 | 134 | if (MultiIter_Mask(*mi)) 135 | mkl_free(MultiIter_Mask(*mi)); 136 | } 137 | 138 | return; 139 | } 140 | 141 | static NPY_INLINE void multi_iter_free(multi_iter_t *mi) { 142 | if (mi) { 143 | if (MultiIter_Index(*mi)) 144 | mkl_free(MultiIter_Index(*mi)); 145 | 146 | if (MultiIter_Shape(*mi)) 147 | mkl_free(MultiIter_Shape(*mi)); 148 | } 149 | 150 | return; 151 | } 152 | 153 | /* Modifies iterator in-place, returns 1 when iterator is empty, 0 otherwise */ 154 | static NPY_INLINE int multi_iter_next(multi_iter_t *mi) { 155 | int j, k; 156 | 157 | if (MultiIter_Done(*mi)) 158 | return 1; 159 | 160 | for (k = MultiIter_Rank(*mi); k > 0; k--) { 161 | j = k - 1; 162 | if (++(MultiIter_IndexElem(*mi, j)) < MultiIter_ShapeElem(*mi, j)) 163 | return 0; 164 | else { 165 | MultiIter_IndexElem(*mi, j) = 0; 166 | if (!j) { 167 | MultiIter_Done(*mi) = 1; 168 | } 169 | } 170 | } 171 | 172 | return 1; 173 | } 174 | 175 | /* Modifies iterator in-place, returns 1 when iterator is empty, 0 otherwise */ 176 | static NPY_INLINE int multi_iter_masked_next(multi_iter_masked_t *mi) { 177 | int j, k; 178 | 179 | if (MultiIter_Done(*mi)) 180 | return 1; 181 | 182 | for (k = MultiIter_MaskLength(*mi); k > 0; k--) { 183 | j = MultiIter_MaskElem(*mi, k - 1); 184 | if (++(MultiIter_IndexElem(*mi, j)) < MultiIter_ShapeElem(*mi, j)) 185 | return 0; 186 | else { 187 | MultiIter_IndexElem(*mi, j) = 0; 188 | if (!k) { 189 | MultiIter_Done(*mi) = 1; 190 | } 191 | } 192 | } 193 | 194 | return 1; 195 | } 196 | 197 | #endif 198 | -------------------------------------------------------------------------------- /mkl_fft/tests/test_interfaces.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017, Intel Corporation 2 | # 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are met: 5 | # 6 | # * Redistributions of source code must retain the above copyright notice, 7 | # this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of Intel Corporation nor the names of its contributors 12 | # may be used to endorse or promote products derived from this software 13 | # without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | import numpy as np 27 | import pytest 28 | from numpy.testing import assert_raises 29 | 30 | import mkl_fft.interfaces as mfi 31 | 32 | try: 33 | scipy_fft = mfi.scipy_fft 34 | except AttributeError: 35 | scipy_fft = None 36 | 37 | interfaces = [] 38 | ids = [] 39 | if scipy_fft is not None: 40 | interfaces.append(scipy_fft) 41 | ids.append("scipy") 42 | interfaces.append(mfi.numpy_fft) 43 | ids.append("numpy") 44 | 45 | 46 | @pytest.mark.parametrize("norm", [None, "forward", "backward", "ortho"]) 47 | @pytest.mark.parametrize( 48 | "dtype", [np.float32, np.float64, np.complex64, np.complex128] 49 | ) 50 | def test_scipy_fft(norm, dtype): 51 | pytest.importorskip("scipy", reason="requires scipy") 52 | x = np.ones(511, dtype=dtype) 53 | w = mfi.scipy_fft.fft(x, norm=norm, workers=None, plan=None) 54 | xx = mfi.scipy_fft.ifft(w, norm=norm, workers=None, plan=None) 55 | tol = 128 * np.finfo(np.dtype(dtype)).eps 56 | np.testing.assert_allclose(x, xx, atol=tol, rtol=tol) 57 | 58 | 59 | @pytest.mark.parametrize("norm", [None, "forward", "backward", "ortho"]) 60 | @pytest.mark.parametrize( 61 | "dtype", [np.float32, np.float64, np.complex64, np.complex128] 62 | ) 63 | def test_numpy_fft(norm, dtype): 64 | x = np.ones(511, dtype=dtype) 65 | w = mfi.numpy_fft.fft(x, norm=norm) 66 | xx = mfi.numpy_fft.ifft(w, norm=norm) 67 | tol = 128 * np.finfo(np.dtype(dtype)).eps 68 | np.testing.assert_allclose(x, xx, atol=tol, rtol=tol) 69 | 70 | 71 | @pytest.mark.parametrize("norm", [None, "forward", "backward", "ortho"]) 72 | @pytest.mark.parametrize("dtype", [np.float32, np.float64]) 73 | def test_scipy_rfft(norm, dtype): 74 | pytest.importorskip("scipy", reason="requires scipy") 75 | x = np.ones(511, dtype=dtype) 76 | w = mfi.scipy_fft.rfft(x, norm=norm, workers=None, plan=None) 77 | xx = mfi.scipy_fft.irfft( 78 | w, n=x.shape[0], norm=norm, workers=None, plan=None 79 | ) 80 | tol = 64 * np.finfo(np.dtype(dtype)).eps 81 | assert np.allclose(x, xx, atol=tol, rtol=tol) 82 | 83 | x = np.ones(510, dtype=dtype) 84 | w = mfi.scipy_fft.rfft(x, norm=norm, workers=None, plan=None) 85 | xx = mfi.scipy_fft.irfft(w, norm=norm, workers=None, plan=None) 86 | tol = 64 * np.finfo(np.dtype(dtype)).eps 87 | assert np.allclose(x, xx, atol=tol, rtol=tol) 88 | 89 | 90 | @pytest.mark.parametrize("norm", [None, "forward", "backward", "ortho"]) 91 | @pytest.mark.parametrize("dtype", [np.float32, np.float64]) 92 | def test_numpy_rfft(norm, dtype): 93 | x = np.ones(511, dtype=dtype) 94 | w = mfi.numpy_fft.rfft(x, norm=norm) 95 | xx = mfi.numpy_fft.irfft(w, n=x.shape[0], norm=norm) 96 | tol = 64 * np.finfo(np.dtype(dtype)).eps 97 | assert np.allclose(x, xx, atol=tol, rtol=tol) 98 | 99 | 100 | @pytest.mark.parametrize("norm", [None, "forward", "backward", "ortho"]) 101 | @pytest.mark.parametrize( 102 | "dtype", [np.float32, np.float64, np.complex64, np.complex128] 103 | ) 104 | def test_scipy_fftn(norm, dtype): 105 | pytest.importorskip("scipy", reason="requires scipy") 106 | x = np.ones((37, 83), dtype=dtype) 107 | w = mfi.scipy_fft.fftn(x, norm=norm, workers=None, plan=None) 108 | xx = mfi.scipy_fft.ifftn(w, norm=norm, workers=None, plan=None) 109 | tol = 64 * np.finfo(np.dtype(dtype)).eps 110 | assert np.allclose(x, xx, atol=tol, rtol=tol) 111 | 112 | 113 | @pytest.mark.parametrize("norm", [None, "forward", "backward", "ortho"]) 114 | @pytest.mark.parametrize( 115 | "dtype", [np.float32, np.float64, np.complex64, np.complex128] 116 | ) 117 | def test_numpy_fftn(norm, dtype): 118 | x = np.ones((37, 83), dtype=dtype) 119 | w = mfi.numpy_fft.fftn(x, norm=norm) 120 | xx = mfi.numpy_fft.ifftn(w, norm=norm) 121 | tol = 64 * np.finfo(np.dtype(dtype)).eps 122 | assert np.allclose(x, xx, atol=tol, rtol=tol) 123 | 124 | 125 | @pytest.mark.parametrize("norm", [None, "forward", "backward", "ortho"]) 126 | @pytest.mark.parametrize("dtype", [np.float32, np.float64]) 127 | def test_scipy_rfftn(norm, dtype): 128 | pytest.importorskip("scipy", reason="requires scipy") 129 | x = np.ones((37, 83), dtype=dtype) 130 | w = mfi.scipy_fft.rfftn(x, norm=norm, workers=None, plan=None) 131 | xx = mfi.scipy_fft.irfftn(w, s=x.shape, norm=norm, workers=None, plan=None) 132 | tol = 64 * np.finfo(np.dtype(dtype)).eps 133 | assert np.allclose(x, xx, atol=tol, rtol=tol) 134 | 135 | x = np.ones((36, 82), dtype=dtype) 136 | w = mfi.scipy_fft.rfftn(x, norm=norm, workers=None, plan=None) 137 | xx = mfi.scipy_fft.irfftn(w, norm=norm, workers=None, plan=None) 138 | tol = 64 * np.finfo(np.dtype(dtype)).eps 139 | assert np.allclose(x, xx, atol=tol, rtol=tol) 140 | 141 | 142 | @pytest.mark.filterwarnings("ignore::DeprecationWarning") 143 | @pytest.mark.parametrize("norm", [None, "forward", "backward", "ortho"]) 144 | @pytest.mark.parametrize("dtype", [np.float32, np.float64]) 145 | def test_numpy_rfftn(norm, dtype): 146 | x = np.ones((37, 83), dtype=dtype) 147 | w = mfi.numpy_fft.rfftn(x, norm=norm) 148 | xx = mfi.numpy_fft.irfftn(w, s=x.shape, norm=norm) 149 | tol = 64 * np.finfo(np.dtype(dtype)).eps 150 | assert np.allclose(x, xx, atol=tol, rtol=tol) 151 | 152 | 153 | def _get_blacklisted_dtypes(): 154 | bl_list = [] 155 | for dt in ["float16", "float128", "complex256"]: 156 | if hasattr(np, dt): 157 | bl_list.append(getattr(np, dt)) 158 | return bl_list 159 | 160 | 161 | @pytest.mark.parametrize("dtype", _get_blacklisted_dtypes()) 162 | def test_scipy_no_support_for(dtype): 163 | pytest.importorskip("scipy", reason="requires scipy") 164 | x = np.ones(16, dtype=dtype) 165 | assert_raises(NotImplementedError, mfi.scipy_fft.ifft, x) 166 | 167 | 168 | def test_scipy_fft_arg_validate(): 169 | pytest.importorskip("scipy", reason="requires scipy") 170 | with pytest.raises(ValueError): 171 | mfi.scipy_fft.fft([1, 2, 3, 4], norm=b"invalid") 172 | 173 | with pytest.raises(NotImplementedError): 174 | mfi.scipy_fft.fft([1, 2, 3, 4], plan="magic") 175 | 176 | 177 | @pytest.mark.parametrize("interface", interfaces, ids=ids) 178 | def test_axes(interface): 179 | x = np.arange(24.0).reshape(2, 3, 4) 180 | res = interface.rfft2(x, axes=(1, 2)) 181 | exp = np.fft.rfft2(x, axes=(1, 2)) 182 | tol = 64 * np.finfo(np.float64).eps 183 | assert np.allclose(res, exp, atol=tol, rtol=tol) 184 | 185 | 186 | @pytest.mark.parametrize("interface", interfaces, ids=ids) 187 | @pytest.mark.parametrize( 188 | "func", ["fftshift", "ifftshift", "fftfreq", "rfftfreq"] 189 | ) 190 | def test_interface_helper_functions(interface, func): 191 | assert hasattr(interface, func) 192 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [dev] - YYYY-MM-DD 8 | 9 | ### Added 10 | * Enabled support of Python 3.14 [gh-234](https://github.com/IntelPython/mkl_fft/pull/234) 11 | 12 | ### Removed 13 | * Dropped support for Python 3.9 [gh-243](https://github.com/IntelPython/mkl_fft/pull/243) 14 | 15 | ## [2.1.1] - 2025-10-09 16 | 17 | ### Fixed 18 | * Fix a circular dependency in Intel NumPy: revert vendoring of `scipy.fft` functions, instead using thin wrappers to call from `scipy` directly [gh-233](https://github.com/IntelPython/mkl_fft/pull/233) 19 | 20 | ## [2.1.0] - 2025-10-06 21 | 22 | ### Added 23 | * Enabled support of Python 3.13 [gh-164](https://github.com/IntelPython/mkl_fft/pull/164) 24 | 25 | ### Changed 26 | * Dropped support for `scipy.fftpack` interface [gh-185](https://github.com/IntelPython/mkl_fft/pull/185) 27 | * Dropped support for `overwrite_x` parameter in `mkl_fft` [gh-185](https://github.com/IntelPython/mkl_fft/pull/185) 28 | * Replaced `fwd_scale` parameter with `norm` in `mkl_fft` [gh-189](https://github.com/IntelPython/mkl_fft/pull/189) 29 | * Conditionally import `scipy_fft` only if `scipy` is installed [gh-195](https://github.com/IntelPython/mkl_fft/pull/195) 30 | * Vendor `fftfreq`, `rfftfreq`, `fftshift`, and `ifftshift` to `scipy_fft` and `numpy_fft` interfaces [gh-226](https://github.com/IntelPython/mkl_fft/pull/226), [gh=229](https://github.com/IntelPython/mkl_fft/pull/229) 31 | 32 | ### Fixed 33 | * Fixed a bug for N-D FFTs when both `s` and `out` are given [gh-185](https://github.com/IntelPython/mkl_fft/pull/185) 34 | 35 | ## [2.0.0] - 2025-06-03 36 | 37 | ### Added 38 | * 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) 39 | * 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) 40 | * Added `fftfreq`, `fftshift`, `ifftshift`, and `rfftfreq` to both NumPy and SciPy interfaces [gh-179](https://github.com/IntelPython/mkl_fft/pull/179) 41 | 42 | ### Changed 43 | * 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) 44 | * 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) 45 | * SciPy interface `mkl_fft.interfaces.scipy_fft` uses the same function from SciPy for handling `s` and `axes` for N-D FFTs [gh-181](https://github.com/IntelPython/mkl_fft/pull/181) 46 | 47 | ### Fixed 48 | * Fixed a bug in `mkl_fft.interfaces.numpy.fftn` when an empty tuple is passed for `axes` [gh-139](https://github.com/IntelPython/mkl_fft/pull/139) 49 | * Fixed a bug for a case when a zero-size array is passed to `mkl_fft.interfaces.numpy.fftn` [gh-139](https://github.com/IntelPython/mkl_fft/pull/139) 50 | * Fixed inconsistency of input and output arrays dtype for `irfft` function [gh-180](https://github.com/IntelPython/mkl_fft/pull/180) 51 | * Fixed issues with `set_workers` function in SciPy interface `mkl_fft.interfaces.scipy_fft` [gh-183](https://github.com/IntelPython/mkl_fft/pull/183) 52 | 53 | ## [1.3.14] - 2025-04-10 54 | 55 | resolves gh-152 by adding an explicit `mkl-service` dependency to `mkl-fft` when building the wheel 56 | to ensure the `mkl` Python module is always available at runtime 57 | 58 | resolves gh-115, gh-116, gh-119, gh-135 59 | 60 | ## [1.3.13] - 2025-03-25 61 | 62 | Supported python versions are 3.9, 3.10, 3.11, 3.12 63 | 64 | migrate from `setup.py` to `pyproject.toml` 65 | 66 | includes support in virtual environment out of the box 67 | 68 | the original `mkl_fft.rfft` and `mkl_fft.irfft` are renamed to `mkl_fft.rfftpack` and `mkl_fft.irfftpack`, 69 | since they replicate the behavior from the deprecated `scipy.fftpack` module. 70 | 71 | `mkl_fft.rfft_numpy`, `mkl_fft.irfft_numpy`, `mkl_fft.rfft2_numpy`, `mkl_fft.irfft2_numpy`, 72 | `mkl_fft.rfftn_numpy`, and `mkl_fft.irfftn_numpy` are renamed to `mkl_fft.rfft`, `mkl_fft.irfft`, 73 | `mkl_fft.rfft2`, `mkl_fft.irfft2`, `mkl_fft.rfftn`, and `mkl_fft.irfftn`, respectively. 74 | (consistent with `numpy.fft` and `scipy.fft` modules) 75 | 76 | file `_scipy_fft_backend.py` is renamed to `_scipy_fft.py` since it replicates `scipy.fft` module 77 | (similar to file `_numpy_fft.py` which replicates `numpy.fft` module) 78 | 79 | ## [1.3.11] 80 | 81 | Bugfix release, resolving gh-109 and updating installation instructions 82 | 83 | ## [1.3.10] 84 | 85 | Bugfix release, resolving leftover uses of NumPy attributes removed in NumPy 2.0 that break 86 | test suite run on Windows. 87 | 88 | ## [1.3.9] 89 | 90 | Updated code and build system to support NumPy 2.0 91 | 92 | ## [1.3.8] 93 | 94 | Added vendored `conv_template.py` from NumPy's distutils submodule to enable building of `mkl_fft` with 95 | NumPy >=1.25 and Python 3.12 96 | 97 | ## [1.3.7] 98 | 99 | Updated build system away from removed in NumPy 1.25 numpy.distutils module. 100 | 101 | Transitioned to Cython 3.0. 102 | 103 | ## [1.3.0] 104 | 105 | Updated numpy interface to support new in NumPy 1.20 supported values of norm keyword, such as "forward" and "backward". 106 | To enable this, `mkl_fft` functions now support `forward_scale` parameter that defaults to 1. 107 | 108 | Fixed issue #48. 109 | 110 | ## [1.2.1] 111 | 112 | Includes bug fix #54 113 | 114 | ## [1.2.0] 115 | 116 | Due to removal of deprecated real-to-real FFT with `DFTI_CONJUGATE_EVEN_STORAGE=DFTI_COMPLEX_REAL` and 117 | `DFTI_PACKED_FORMAT=DFTI_PACK` from Intel(R) Math Kernel Library, reimplemented `mkl_fft.rfft` and 118 | `mkl_fft.irfft` to use real-to-complex functionality with subsequent copying to rearange the transform as expected 119 | of `mkl_fft.rfft`, with the associated performance penalty. The use of the real-to-complex 120 | transform improves multi-core utilization which may offset the performance loss incurred due to copying. 121 | 122 | ## [1.1.0] 123 | 124 | Added `scipy.fft` backend, see #42. Fixed #46. 125 | 126 | ## [1.0.15] 127 | 128 | Changed tests to not compare against numpy fft, as this broke due to renaming of `np.fft.pocketfft` to 129 | `np.fft._pocketfft`. Instead compare against naive realization of 1D FFT as a sum. 130 | 131 | Setup script is now aware of `MKLROOT` environment variable. If unset, NumPy's mkl_info will be queried. 132 | 133 | ## [1.0.14] 134 | 135 | Fixed unreferenced bug in `irfftn_numpy`, and adjusted NumPy interfaces to change to pocketfft in NumPy 1.17 136 | 137 | ## [1.0.13] 138 | 139 | Issue #39 fixed (memory leak with complex FFT on real arrays) 140 | 141 | ## [1.0.12] 142 | 143 | Issue #37 fixed. 144 | 145 | Inhibited vectorization of short loops computing pointer to memory referenced by a multi-iterator by Intel (R) C Compiler, 146 | improving performance of ND `fft` and `ifft` on real input arrays. 147 | 148 | ## [1.0.11] 149 | 150 | Improvement for performance of ND `fft` on real input arrays by inlining multi-iterators. 151 | This particularly benefits performance of mkl_fft built with Intel (R) C Compiler. 152 | 153 | ## [1.0.10] 154 | 155 | Fix for issue #29. 156 | 157 | ## [1.0.7] 158 | 159 | Improved exception message raised if MKL is signalling an error. The message now includes MKL's own description of the exception. 160 | This partially improves #24. 161 | 162 | Improved argument validation for ND transforms aligning with scipy 1.2.0 163 | 164 | ## [1.0.6] 165 | 166 | Fixed issues #21, and addressed NumPy 1.15 deprecation warnings from using lists instead of tuples to specify multiple slices. 167 | 168 | ## [1.0.5] 169 | 170 | Fixed issues #7, #17, #18. 171 | Consolidated version specification into a single file `mkl_fft/_version.py`. 172 | 173 | ## [1.0.4] 174 | 175 | Added CHANGES.rst. Fixed issue #11 by using lock around calls to 1D FFT routines. 176 | 177 | ## [1.0.3] 178 | 179 | This is a bug fix release. 180 | 181 | It fixes issues #9, and #13. 182 | 183 | As part of fixing issue #13, out-of-place 1D FFT calls such as `fft`, `ifft`, `rfft_numpy` 184 | and `irfftn_numpy` will allocate Fortran layout array for the output is the input is a Fotran array. 185 | 186 | ## [1.0.2] 187 | 188 | Minor update of `mkl_fft`, reflecting renaming of `numpy.core.multiarray_tests` module to 189 | `numpy.core._multiarray_tests` as well as fixing #4. 190 | 191 | ## [1.0.1] 192 | 193 | Bug fix release. 194 | 195 | ## [1.0.0] 196 | 197 | Initial release of `mkl_fft`. 198 | -------------------------------------------------------------------------------- /mkl_fft/interfaces/_numpy_fft.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017, Intel Corporation 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of Intel Corporation nor the names of its contributors 13 | # may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | """ 28 | An interface for FFT module of NumPy (`numpy.fft`) that uses OneMKL FFT 29 | in the backend. 30 | """ 31 | 32 | import re 33 | import warnings 34 | 35 | import numpy as np 36 | 37 | import mkl_fft 38 | 39 | from .._fft_utils import _swap_direction 40 | from ._float_utils import _downcast_float128_array 41 | 42 | __all__ = [ 43 | "fft", 44 | "ifft", 45 | "fft2", 46 | "ifft2", 47 | "fftn", 48 | "ifftn", 49 | "rfft", 50 | "irfft", 51 | "rfft2", 52 | "irfft2", 53 | "rfftn", 54 | "irfftn", 55 | "hfft", 56 | "ihfft", 57 | ] 58 | 59 | 60 | # copied with modifications from: 61 | # https://github.com/numpy/numpy/blob/main/numpy/fft/_pocketfft.py 62 | def _cook_nd_args(a, s=None, axes=None, invreal=False): 63 | if s is None: 64 | shapeless = True 65 | if axes is None: 66 | s = list(a.shape) 67 | else: 68 | s = np.take(a.shape, axes) 69 | else: 70 | shapeless = False 71 | s = list(s) 72 | if axes is None: 73 | if not shapeless and np.lib.NumpyVersion(np.__version__) >= "2.0.0": 74 | msg = ( 75 | "`axes` should not be `None` if `s` is not `None` " 76 | "(Deprecated in NumPy 2.0). In a future version of NumPy, " 77 | "this will raise an error and `s[i]` will correspond to " 78 | "the size along the transformed axis specified by " 79 | "`axes[i]`. To retain current behaviour, pass a sequence " 80 | "[0, ..., k-1] to `axes` for an array of dimension k." 81 | ) 82 | warnings.warn(msg, DeprecationWarning, stacklevel=3) 83 | axes = list(range(-len(s), 0)) 84 | if len(s) != len(axes): 85 | raise ValueError("Shape and axes have different lengths.") 86 | if invreal and shapeless: 87 | s[-1] = (a.shape[axes[-1]] - 1) * 2 88 | if None in s and np.lib.NumpyVersion(np.__version__) >= "2.0.0": 89 | msg = ( 90 | "Passing an array containing `None` values to `s` is " 91 | "deprecated in NumPy 2.0 and will raise an error in " 92 | "a future version of NumPy. To use the default behaviour " 93 | "of the corresponding 1-D transform, pass the value matching " 94 | "the default for its `n` parameter. To use the default " 95 | "behaviour for every axis, the `s` argument can be omitted." 96 | ) 97 | warnings.warn(msg, DeprecationWarning, stacklevel=3) 98 | # use the whole input array along axis `i` if `s[i] == -1 or None` 99 | s = [a.shape[_a] if _s in [-1, None] else _s for _s, _a in zip(s, axes)] 100 | 101 | return s, axes 102 | 103 | 104 | def _trycall(func, args, kwrds): 105 | try: 106 | res = func(*args, **kwrds) 107 | except ValueError as ve: 108 | if len(ve.args) == 1: 109 | if re.match("^Dimension n", ve.args[0]): 110 | raise ValueError("Invalid number of FFT data points") 111 | raise ve 112 | return res 113 | 114 | 115 | def fft(a, n=None, axis=-1, norm=None, out=None): 116 | """ 117 | Compute the one-dimensional discrete Fourier Transform. 118 | 119 | For full documentation refer to `numpy.fft.fft`. 120 | 121 | """ 122 | x = _downcast_float128_array(a) 123 | return _trycall( 124 | mkl_fft.fft, (x,), {"n": n, "axis": axis, "norm": norm, "out": out} 125 | ) 126 | 127 | 128 | def ifft(a, n=None, axis=-1, norm=None, out=None): 129 | """ 130 | Compute the one-dimensional inverse discrete Fourier Transform. 131 | 132 | For full documentation refer to `numpy.fft.ifft`. 133 | 134 | """ 135 | x = _downcast_float128_array(a) 136 | return _trycall( 137 | mkl_fft.ifft, (x,), {"n": n, "axis": axis, "norm": norm, "out": out} 138 | ) 139 | 140 | 141 | def fft2(a, s=None, axes=(-2, -1), norm=None, out=None): 142 | """ 143 | Compute the 2-dimensional discrete Fourier Transform 144 | 145 | For full documentation refer to `numpy.fft.fft2`. 146 | 147 | """ 148 | return fftn(a, s=s, axes=axes, norm=norm, out=out) 149 | 150 | 151 | def ifft2(a, s=None, axes=(-2, -1), norm=None, out=None): 152 | """ 153 | Compute the 2-dimensional inverse discrete Fourier Transform. 154 | 155 | For full documentation refer to `numpy.fft.ifft2`. 156 | 157 | """ 158 | return ifftn(a, s=s, axes=axes, norm=norm, out=out) 159 | 160 | 161 | def fftn(a, s=None, axes=None, norm=None, out=None): 162 | """ 163 | Compute the N-dimensional discrete Fourier Transform. 164 | 165 | For full documentation refer to `numpy.fft.fftn`. 166 | 167 | """ 168 | x = _downcast_float128_array(a) 169 | s, axes = _cook_nd_args(x, s, axes) 170 | 171 | return _trycall( 172 | mkl_fft.fftn, (x,), {"s": s, "axes": axes, "norm": norm, "out": out} 173 | ) 174 | 175 | 176 | def ifftn(a, s=None, axes=None, norm=None, out=None): 177 | """ 178 | Compute the N-dimensional inverse discrete Fourier Transform. 179 | 180 | For full documentation refer to `numpy.fft.ifftn`. 181 | 182 | """ 183 | x = _downcast_float128_array(a) 184 | s, axes = _cook_nd_args(x, s, axes) 185 | 186 | return _trycall( 187 | mkl_fft.ifftn, 188 | (x,), 189 | {"s": s, "axes": axes, "norm": norm, "out": out}, 190 | ) 191 | 192 | 193 | def rfft(a, n=None, axis=-1, norm=None, out=None): 194 | """ 195 | Compute the one-dimensional discrete Fourier Transform for real input. 196 | 197 | For full documentation refer to `numpy.fft.rfft`. 198 | 199 | """ 200 | x = _downcast_float128_array(a) 201 | 202 | return _trycall( 203 | mkl_fft.rfft, (x,), {"n": n, "axis": axis, "norm": norm, "out": out} 204 | ) 205 | 206 | 207 | def irfft(a, n=None, axis=-1, norm=None, out=None): 208 | """ 209 | Compute the inverse of `rfft`. 210 | 211 | For full documentation refer to `numpy.fft.irfft`. 212 | 213 | """ 214 | x = _downcast_float128_array(a) 215 | 216 | return _trycall( 217 | mkl_fft.irfft, 218 | (x,), 219 | {"n": n, "axis": axis, "norm": norm, "out": out}, 220 | ) 221 | 222 | 223 | def rfft2(a, s=None, axes=(-2, -1), norm=None, out=None): 224 | """ 225 | Compute the 2-dimensional FFT of a real array. 226 | 227 | For full documentation refer to `numpy.fft.rfft2`. 228 | 229 | """ 230 | return rfftn(a, s=s, axes=axes, norm=norm, out=out) 231 | 232 | 233 | def irfft2(a, s=None, axes=(-2, -1), norm=None, out=None): 234 | """ 235 | Compute the inverse FFT of `rfft2`. 236 | 237 | For full documentation refer to `numpy.fft.irfft2`. 238 | 239 | """ 240 | return irfftn(a, s=s, axes=axes, norm=norm, out=out) 241 | 242 | 243 | def rfftn(a, s=None, axes=None, norm=None, out=None): 244 | """ 245 | Compute the N-dimensional discrete Fourier Transform for real input. 246 | 247 | For full documentation refer to `numpy.fft.rfftn`. 248 | 249 | """ 250 | x = _downcast_float128_array(a) 251 | s, axes = _cook_nd_args(x, s, axes) 252 | 253 | return _trycall( 254 | mkl_fft.rfftn, 255 | (x,), 256 | {"s": s, "axes": axes, "norm": norm, "out": out}, 257 | ) 258 | 259 | 260 | def irfftn(a, s=None, axes=None, norm=None, out=None): 261 | """ 262 | Compute the inverse of `rfftn`. 263 | 264 | For full documentation refer to `numpy.fft.irfftn`. 265 | 266 | """ 267 | 268 | x = _downcast_float128_array(a) 269 | s, axes = _cook_nd_args(x, s, axes, invreal=True) 270 | 271 | return _trycall( 272 | mkl_fft.irfftn, 273 | (x,), 274 | {"s": s, "axes": axes, "norm": norm, "out": out}, 275 | ) 276 | 277 | 278 | def hfft(a, n=None, axis=-1, norm=None, out=None): 279 | """ 280 | Compute the FFT of a signal which has Hermitian symmetry, 281 | i.e., a real spectrum.. 282 | 283 | For full documentation refer to `numpy.fft.hfft`. 284 | 285 | """ 286 | norm = _swap_direction(norm) 287 | x = _downcast_float128_array(a) 288 | return _trycall( 289 | mkl_fft.irfft, 290 | (np.conjugate(x),), 291 | {"n": n, "axis": axis, "norm": norm, "out": out}, 292 | ) 293 | 294 | 295 | def ihfft(a, n=None, axis=-1, norm=None, out=None): 296 | """ 297 | Compute the inverse FFT of a signal which has Hermitian symmetry. 298 | 299 | For full documentation refer to `numpy.fft.ihfft`. 300 | 301 | """ 302 | norm = _swap_direction(norm) 303 | x = _downcast_float128_array(a) 304 | 305 | result = _trycall( 306 | mkl_fft.rfft, (x,), {"n": n, "axis": axis, "norm": norm, "out": out} 307 | ) 308 | 309 | np.conjugate(result, out=result) 310 | return result 311 | -------------------------------------------------------------------------------- /_vendored/conv_template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | takes templated file .xxx.src and produces .xxx file where .xxx is 4 | .i or .c or .h, using the following template rules 5 | 6 | /**begin repeat -- on a line by itself marks the start of a repeated code 7 | segment 8 | /**end repeat**/ -- on a line by itself marks it's end 9 | 10 | After the /**begin repeat and before the */, all the named templates are placed 11 | these should all have the same number of replacements 12 | 13 | Repeat blocks can be nested, with each nested block labeled with its depth, 14 | i.e. 15 | /**begin repeat1 16 | *.... 17 | */ 18 | /**end repeat1**/ 19 | 20 | When using nested loops, you can optionally exclude particular 21 | combinations of the variables using (inside the comment portion of the inner loop): 22 | 23 | :exclude: var1=value1, var2=value2, ... 24 | 25 | This will exclude the pattern where var1 is value1 and var2 is value2 when 26 | the result is being generated. 27 | 28 | 29 | In the main body each replace will use one entry from the list of named replacements 30 | 31 | Note that all #..# forms in a block must have the same number of 32 | comma-separated entries. 33 | 34 | Example: 35 | 36 | An input file containing 37 | 38 | /**begin repeat 39 | * #a = 1,2,3# 40 | * #b = 1,2,3# 41 | */ 42 | 43 | /**begin repeat1 44 | * #c = ted, jim# 45 | */ 46 | @a@, @b@, @c@ 47 | /**end repeat1**/ 48 | 49 | /**end repeat**/ 50 | 51 | produces 52 | 53 | line 1 "template.c.src" 54 | 55 | /* 56 | ********************************************************************* 57 | ** This file was autogenerated from a template DO NOT EDIT!!** 58 | ** Changes should be made to the original source (.src) file ** 59 | ********************************************************************* 60 | */ 61 | 62 | #line 9 63 | 1, 1, ted 64 | 65 | #line 9 66 | 1, 1, jim 67 | 68 | #line 9 69 | 2, 2, ted 70 | 71 | #line 9 72 | 2, 2, jim 73 | 74 | #line 9 75 | 3, 3, ted 76 | 77 | #line 9 78 | 3, 3, jim 79 | 80 | """ 81 | 82 | __all__ = ['process_str', 'process_file'] 83 | 84 | import os 85 | import sys 86 | import re 87 | 88 | # names for replacement that are already global. 89 | global_names = {} 90 | 91 | # header placed at the front of head processed file 92 | header =\ 93 | """ 94 | /* 95 | ***************************************************************************** 96 | ** This file was autogenerated from a template DO NOT EDIT!!!! ** 97 | ** Changes should be made to the original source (.src) file ** 98 | ***************************************************************************** 99 | */ 100 | 101 | """ 102 | # Parse string for repeat loops 103 | def parse_structure(astr, level): 104 | """ 105 | The returned line number is from the beginning of the string, starting 106 | at zero. Returns an empty list if no loops found. 107 | 108 | """ 109 | if level == 0 : 110 | loopbeg = "/**begin repeat" 111 | loopend = "/**end repeat**/" 112 | else : 113 | loopbeg = "/**begin repeat%d" % level 114 | loopend = "/**end repeat%d**/" % level 115 | 116 | ind = 0 117 | line = 0 118 | spanlist = [] 119 | while True: 120 | start = astr.find(loopbeg, ind) 121 | if start == -1: 122 | break 123 | start2 = astr.find("*/", start) 124 | start2 = astr.find("\n", start2) 125 | fini1 = astr.find(loopend, start2) 126 | fini2 = astr.find("\n", fini1) 127 | line += astr.count("\n", ind, start2+1) 128 | spanlist.append((start, start2+1, fini1, fini2+1, line)) 129 | line += astr.count("\n", start2+1, fini2) 130 | ind = fini2 131 | spanlist.sort() 132 | return spanlist 133 | 134 | 135 | def paren_repl(obj): 136 | torep = obj.group(1) 137 | numrep = obj.group(2) 138 | return ','.join([torep]*int(numrep)) 139 | 140 | parenrep = re.compile(r"\(([^)]*)\)\*(\d+)") 141 | plainrep = re.compile(r"([^*]+)\*(\d+)") 142 | def parse_values(astr): 143 | # replaces all occurrences of '(a,b,c)*4' in astr 144 | # with 'a,b,c,a,b,c,a,b,c,a,b,c'. Empty braces generate 145 | # empty values, i.e., ()*4 yields ',,,'. The result is 146 | # split at ',' and a list of values returned. 147 | astr = parenrep.sub(paren_repl, astr) 148 | # replaces occurrences of xxx*3 with xxx, xxx, xxx 149 | astr = ','.join([plainrep.sub(paren_repl, x.strip()) 150 | for x in astr.split(',')]) 151 | return astr.split(',') 152 | 153 | 154 | stripast = re.compile(r"\n\s*\*?") 155 | named_re = re.compile(r"#\s*(\w*)\s*=([^#]*)#") 156 | exclude_vars_re = re.compile(r"(\w*)=(\w*)") 157 | exclude_re = re.compile(":exclude:") 158 | def parse_loop_header(loophead) : 159 | """Find all named replacements in the header 160 | 161 | Returns a list of dictionaries, one for each loop iteration, 162 | where each key is a name to be substituted and the corresponding 163 | value is the replacement string. 164 | 165 | Also return a list of exclusions. The exclusions are dictionaries 166 | of key value pairs. There can be more than one exclusion. 167 | [{'var1':'value1', 'var2', 'value2'[,...]}, ...] 168 | 169 | """ 170 | # Strip out '\n' and leading '*', if any, in continuation lines. 171 | # This should not effect code previous to this change as 172 | # continuation lines were not allowed. 173 | loophead = stripast.sub("", loophead) 174 | # parse out the names and lists of values 175 | names = [] 176 | reps = named_re.findall(loophead) 177 | nsub = None 178 | for rep in reps: 179 | name = rep[0] 180 | vals = parse_values(rep[1]) 181 | size = len(vals) 182 | if nsub is None : 183 | nsub = size 184 | elif nsub != size : 185 | msg = "Mismatch in number of values, %d != %d\n%s = %s" 186 | raise ValueError(msg % (nsub, size, name, vals)) 187 | names.append((name, vals)) 188 | 189 | 190 | # Find any exclude variables 191 | excludes = [] 192 | 193 | for obj in exclude_re.finditer(loophead): 194 | span = obj.span() 195 | # find next newline 196 | endline = loophead.find('\n', span[1]) 197 | substr = loophead[span[1]:endline] 198 | ex_names = exclude_vars_re.findall(substr) 199 | excludes.append(dict(ex_names)) 200 | 201 | # generate list of dictionaries, one for each template iteration 202 | dlist = [] 203 | if nsub is None : 204 | raise ValueError("No substitution variables found") 205 | for i in range(nsub): 206 | tmp = {name: vals[i] for name, vals in names} 207 | dlist.append(tmp) 208 | return dlist 209 | 210 | replace_re = re.compile(r"@(\w+)@") 211 | def parse_string(astr, env, level, line) : 212 | lineno = "#line %d\n" % line 213 | 214 | # local function for string replacement, uses env 215 | def replace(match): 216 | name = match.group(1) 217 | try : 218 | val = env[name] 219 | except KeyError: 220 | msg = 'line %d: no definition of key "%s"'%(line, name) 221 | raise ValueError(msg) from None 222 | return val 223 | 224 | code = [lineno] 225 | struct = parse_structure(astr, level) 226 | if struct : 227 | # recurse over inner loops 228 | oldend = 0 229 | newlevel = level + 1 230 | for sub in struct: 231 | pref = astr[oldend:sub[0]] 232 | head = astr[sub[0]:sub[1]] 233 | text = astr[sub[1]:sub[2]] 234 | oldend = sub[3] 235 | newline = line + sub[4] 236 | code.append(replace_re.sub(replace, pref)) 237 | try : 238 | envlist = parse_loop_header(head) 239 | except ValueError as e: 240 | msg = "line %d: %s" % (newline, e) 241 | raise ValueError(msg) 242 | for newenv in envlist : 243 | newenv.update(env) 244 | newcode = parse_string(text, newenv, newlevel, newline) 245 | code.extend(newcode) 246 | suff = astr[oldend:] 247 | code.append(replace_re.sub(replace, suff)) 248 | else : 249 | # replace keys 250 | code.append(replace_re.sub(replace, astr)) 251 | code.append('\n') 252 | return ''.join(code) 253 | 254 | def process_str(astr): 255 | code = [header] 256 | code.extend(parse_string(astr, global_names, 0, 1)) 257 | return ''.join(code) 258 | 259 | 260 | include_src_re = re.compile(r"(\n|\A)#include\s*['\"]" 261 | r"(?P[\w\d./\\]+[.]src)['\"]", re.I) 262 | 263 | def resolve_includes(source): 264 | d = os.path.dirname(source) 265 | with open(source) as fid: 266 | lines = [] 267 | for line in fid: 268 | m = include_src_re.match(line) 269 | if m: 270 | fn = m.group('name') 271 | if not os.path.isabs(fn): 272 | fn = os.path.join(d, fn) 273 | if os.path.isfile(fn): 274 | lines.extend(resolve_includes(fn)) 275 | else: 276 | lines.append(line) 277 | else: 278 | lines.append(line) 279 | return lines 280 | 281 | def process_file(source): 282 | lines = resolve_includes(source) 283 | sourcefile = os.path.normcase(source).replace("\\", "\\\\") 284 | try: 285 | code = process_str(''.join(lines)) 286 | except ValueError as e: 287 | raise ValueError('In "%s" loop at %s' % (sourcefile, e)) from None 288 | return '#line 1 "%s"\n%s' % (sourcefile, code) 289 | 290 | 291 | def unique_key(adict): 292 | # this obtains a unique key given a dictionary 293 | # currently it works by appending together n of the letters of the 294 | # current keys and increasing n until a unique key is found 295 | # -- not particularly quick 296 | allkeys = list(adict.keys()) 297 | done = False 298 | n = 1 299 | while not done: 300 | newkey = "".join([x[:n] for x in allkeys]) 301 | if newkey in allkeys: 302 | n += 1 303 | else: 304 | done = True 305 | return newkey 306 | 307 | 308 | def main(): 309 | try: 310 | file = sys.argv[1] 311 | except IndexError: 312 | fid = sys.stdin 313 | outfile = sys.stdout 314 | else: 315 | fid = open(file, 'r') 316 | (base, ext) = os.path.splitext(file) 317 | newname = base 318 | outfile = open(newname, 'w') 319 | 320 | allstr = fid.read() 321 | try: 322 | writestr = process_str(allstr) 323 | except ValueError as e: 324 | raise ValueError("In %s loop at %s" % (file, e)) from None 325 | 326 | outfile.write(writestr) 327 | 328 | if __name__ == "__main__": 329 | main() 330 | -------------------------------------------------------------------------------- /mkl_fft/tests/test_fftnd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017, Intel Corporation 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of Intel Corporation nor the names of its contributors 13 | # may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | import numpy as np 28 | import pytest 29 | from numpy import random as rnd 30 | from numpy.testing import TestCase, assert_allclose 31 | 32 | import mkl_fft 33 | 34 | from .helper import requires_numpy_2 35 | 36 | reps_64 = (2**11) * np.finfo(np.float64).eps 37 | reps_32 = (2**11) * np.finfo(np.float32).eps 38 | atol_64 = (2**9) * np.finfo(np.float64).eps 39 | atol_32 = (2**9) * np.finfo(np.float32).eps 40 | 41 | 42 | def _get_rtol_atol(x): 43 | dt = x.dtype 44 | if dt == np.float64 or dt == np.complex128: 45 | return reps_64, atol_64 46 | elif dt == np.float32 or dt == np.complex64: 47 | return reps_32, atol_32 48 | else: 49 | assert ( 50 | dt == np.float64 51 | or dt == np.complex128 52 | or dt == np.float32 53 | or dt == np.complex64 54 | ), "Unexpected dtype {}".format(dt) 55 | return reps_64, atol_64 56 | 57 | 58 | class Test_mklfft_matrix(TestCase): 59 | def setUp(self): 60 | rnd.seed(123456) 61 | self.md = rnd.randn(256, 256) 62 | self.mf = self.md.astype(np.float32) 63 | self.mz = rnd.randn(256, 256 * 2).view(np.complex128) 64 | self.mc = self.mz.astype(np.complex64) 65 | 66 | def test_matrix1(self): 67 | """fftn equals repeated fft""" 68 | for ar in [self.md, self.mz, self.mf, self.mc]: 69 | r_tol, a_tol = _get_rtol_atol(ar) 70 | d = ar.copy() 71 | t1 = mkl_fft.fftn(d) 72 | t2 = mkl_fft.fft(mkl_fft.fft(d, axis=0), axis=1) 73 | t3 = mkl_fft.fft(mkl_fft.fft(d, axis=1), axis=0) 74 | assert_allclose( 75 | t1, 76 | t2, 77 | rtol=r_tol, 78 | atol=a_tol, 79 | err_msg="failed test for dtype {}, max abs diff: {}".format( 80 | d.dtype, np.max(np.abs(t1 - t2)) 81 | ), 82 | ) 83 | assert_allclose( 84 | t1, 85 | t3, 86 | rtol=r_tol, 87 | atol=a_tol, 88 | err_msg="failed test for dtype {}, max abs diff: {}".format( 89 | d.dtype, np.max(np.abs(t1 - t3)) 90 | ), 91 | ) 92 | 93 | def test_matrix2(self): 94 | """ifftn(fftn(x)) is x""" 95 | for ar in [self.md, self.mz, self.mf, self.mc]: 96 | d = ar.copy() 97 | r_tol, a_tol = _get_rtol_atol(d) 98 | t = mkl_fft.ifftn(mkl_fft.fftn(d)) 99 | assert_allclose( 100 | d, 101 | t, 102 | rtol=r_tol, 103 | atol=a_tol, 104 | err_msg="failed test for dtype {}, max abs diff: {}".format( 105 | d.dtype, np.max(np.abs(d - t)) 106 | ), 107 | ) 108 | 109 | def test_matrix3(self): 110 | """fftn(ifftn(x)) is x""" 111 | for ar in [self.md, self.mz, self.mf, self.mc]: 112 | d = ar.copy() 113 | r_tol, a_tol = _get_rtol_atol(d) 114 | t = mkl_fft.fftn(mkl_fft.ifftn(d)) 115 | assert_allclose( 116 | d, 117 | t, 118 | rtol=r_tol, 119 | atol=a_tol, 120 | err_msg="failed test for dtype {}, max abs diff: {}".format( 121 | d.dtype, np.max(np.abs(d - t)) 122 | ), 123 | ) 124 | 125 | def test_matrix4(self): 126 | """fftn of strided array is same as fftn of a contiguous copy""" 127 | for ar in [self.md, self.mz, self.mf, self.mc]: 128 | r_tol, a_tol = _get_rtol_atol(ar) 129 | d_strided = ar[::2, ::2] 130 | d_contig = d_strided.copy() 131 | t_strided = mkl_fft.fftn(d_strided) 132 | t_contig = mkl_fft.fftn(d_contig) 133 | assert_allclose(t_strided, t_contig, rtol=r_tol, atol=a_tol) 134 | 135 | def test_matrix5(self): 136 | """fftn of strided array is same as fftn of a contiguous copy""" 137 | np.random.seed(1234) 138 | x = np.random.randn(6, 11, 12, 13) 139 | y = x[::-2, :, :, ::3] 140 | r_tol, a_tol = _get_rtol_atol(y) 141 | f = mkl_fft.fftn(y, axes=(1, 2)) 142 | for i0 in range(y.shape[0]): 143 | for i3 in range(y.shape[3]): 144 | assert_allclose( 145 | f[i0, :, :, i3], 146 | mkl_fft.fftn(y[i0, :, :, i3]), 147 | rtol=r_tol, 148 | atol=a_tol, 149 | ) 150 | 151 | def test_matrix6(self): 152 | """fftn with tuple, list and ndarray axes and s""" 153 | for ar in [self.md, self.mz, self.mf, self.mc]: 154 | d = ar.copy() 155 | for norm in [None, "forward", "backward", "ortho"]: 156 | for container in [tuple, list, np.array]: 157 | axes = container(range(d.ndim)) 158 | s = container(d.shape) 159 | kwargs = dict(s=s, axes=axes, norm=norm) 160 | r_tol, a_tol = _get_rtol_atol(d) 161 | t = mkl_fft.fftn(mkl_fft.ifftn(d, **kwargs), **kwargs) 162 | assert_allclose( 163 | d, 164 | t, 165 | rtol=r_tol, 166 | atol=a_tol, 167 | err_msg="failed test for dtype {}, max abs diff: {}".format( 168 | d.dtype, np.max(np.abs(d - t)) 169 | ), 170 | ) 171 | 172 | 173 | class Test_Regressions(TestCase): 174 | 175 | def setUp(self): 176 | rnd.seed(123456) 177 | self.ad = rnd.randn(32, 17, 23) 178 | self.af = self.ad.astype(np.float32) 179 | self.az = rnd.randn(32, 17, 23 * 2).view(np.complex128) 180 | self.ac = self.az.astype(np.complex64) 181 | 182 | def test_cf_contig(self): 183 | """fft of F-contiguous array is the same as of C-contiguous with same data""" 184 | for ar in [self.ad, self.af, self.az, self.ac]: 185 | r_tol, a_tol = _get_rtol_atol(ar) 186 | d_ccont = ar.copy() 187 | d_fcont = np.asfortranarray(d_ccont) 188 | for a in range(ar.ndim): 189 | f1 = mkl_fft.fft(d_ccont, axis=a) 190 | f2 = mkl_fft.fft(d_fcont, axis=a) 191 | assert_allclose(f1, f2, rtol=r_tol, atol=a_tol) 192 | 193 | def test_rfftn(self): 194 | axes = [ 195 | (0, 1, 2), 196 | (0, 2, 1), 197 | (1, 0, 2), 198 | (1, 2, 0), 199 | (2, 0, 1), 200 | (2, 1, 0), 201 | ] 202 | for x in [self.ad, self.af]: 203 | for a in axes: 204 | r_tol, a_tol = _get_rtol_atol(x) 205 | rfft_tr = mkl_fft.rfftn(np.transpose(x, a)) 206 | tr_rfft = np.transpose(mkl_fft.rfftn(x, axes=a), a) 207 | assert_allclose(rfft_tr, tr_rfft, rtol=r_tol, atol=a_tol) 208 | 209 | def test_gh64(self): 210 | a = np.arange(12).reshape((3, 4)) 211 | x = a.astype(np.cdouble) 212 | r1 = mkl_fft.fftn(a, s=None, axes=(-2, -1)) 213 | r2 = mkl_fft.fftn(x) 214 | r_tol, a_tol = _get_rtol_atol(x) 215 | assert_allclose(r1, r2, rtol=r_tol, atol=a_tol) 216 | 217 | 218 | def test_gh109(): 219 | b_int = np.array([[5, 7, 6, 5], [4, 6, 4, 8], [9, 3, 7, 5]], dtype=np.int64) 220 | b = np.asarray(b_int, dtype=np.float32) 221 | 222 | r1 = mkl_fft.fftn(b, s=None, axes=(0,), norm="ortho") 223 | r2 = mkl_fft.fftn(b_int, s=None, axes=(0,), norm="ortho") 224 | 225 | rtol, atol = _get_rtol_atol(b) 226 | assert_allclose(r1, r2, rtol=rtol, atol=atol) 227 | 228 | 229 | @pytest.mark.parametrize("dtype", [complex, float]) 230 | @pytest.mark.parametrize("s", [(15, 24, 10), [35, 25, 15], [25, 15, 5]]) 231 | @pytest.mark.parametrize("axes", [(0, 1, 2), (-1, -2, -3), [1, 0, 2]]) 232 | @pytest.mark.parametrize("func", ["fftn", "ifftn", "rfftn", "irfftn"]) 233 | def test_s_axes(dtype, s, axes, func): 234 | shape = (30, 20, 10) 235 | if dtype is complex and func != "rfftn": 236 | x = np.random.random(shape) + 1j * np.random.random(shape) 237 | else: 238 | x = np.random.random(shape) 239 | 240 | r1 = getattr(np.fft, func)(x, s=s, axes=axes) 241 | r2 = getattr(mkl_fft, func)(x, s=s, axes=axes) 242 | 243 | rtol, atol = _get_rtol_atol(x) 244 | assert_allclose(r1, r2, rtol=rtol, atol=atol) 245 | 246 | 247 | @requires_numpy_2 248 | @pytest.mark.parametrize("dtype", [complex, float]) 249 | @pytest.mark.parametrize("s", [(15, 24, 10), [35, 25, 15], [25, 15, 5]]) 250 | @pytest.mark.parametrize("axes", [(0, 1, 2), (-1, -2, -3), [1, 0, 2]]) 251 | @pytest.mark.parametrize("func", ["fftn", "ifftn", "rfftn", "irfftn"]) 252 | def test_s_axes_out(dtype, s, axes, func): 253 | shape = (30, 20, 10) 254 | if dtype is complex and func != "rfftn": 255 | x = np.random.random(shape) + 1j * np.random.random(shape) 256 | else: 257 | x = np.random.random(shape) 258 | 259 | r1 = getattr(np.fft, func)(x, s=s, axes=axes) 260 | out = np.empty_like(r1) 261 | r2 = getattr(mkl_fft, func)(x, s=s, axes=axes, out=out) 262 | assert r2 is out 263 | 264 | rtol, atol = _get_rtol_atol(x) 265 | assert_allclose(r1, r2, rtol=rtol, atol=atol) 266 | 267 | 268 | @pytest.mark.parametrize("dtype", [complex, float]) 269 | @pytest.mark.parametrize("axes", [(2, 0, 2, 0), (0, 1, 1), (2, 0, 1, 3, 2, 1)]) 270 | @pytest.mark.parametrize("func", ["rfftn", "irfftn"]) 271 | def test_repeated_axes(dtype, axes, func): 272 | shape = (2, 3, 4, 5) 273 | if dtype is complex and func != "rfftn": 274 | x = np.random.random(shape) + 1j * np.random.random(shape) 275 | else: 276 | x = np.random.random(shape) 277 | 278 | r1 = getattr(np.fft, func)(x, axes=axes) 279 | r2 = getattr(mkl_fft, func)(x, axes=axes) 280 | 281 | rtol, atol = _get_rtol_atol(x) 282 | assert_allclose(r1, r2, rtol=rtol, atol=atol) 283 | 284 | 285 | @requires_numpy_2 286 | @pytest.mark.parametrize("axes", [None, (0, 1), (0, 2), (1, 2)]) 287 | @pytest.mark.parametrize("func", ["fftn", "ifftn"]) 288 | def test_out_strided(axes, func): 289 | shape = (20, 30, 40) 290 | x = rnd.random(shape) + 1j * rnd.random(shape) 291 | out = np.empty(shape, dtype=x.dtype) 292 | 293 | x = x[::2, ::3, ::4] 294 | out = out[::2, ::3, ::4] 295 | result = getattr(mkl_fft, func)(x, axes=axes, out=out) 296 | expected = getattr(np.fft, func)(x, axes=axes, out=out) 297 | 298 | assert_allclose(result, expected, strict=True) 299 | -------------------------------------------------------------------------------- /.github/workflows/conda-package-cf.yml: -------------------------------------------------------------------------------- 1 | name: Conda package with conda-forge channel only 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | permissions: read-all 10 | 11 | env: 12 | PACKAGE_NAME: mkl_fft 13 | MODULE_NAME: mkl_fft 14 | TEST_ENV_NAME: test_mkl_fft 15 | VER_SCRIPT1: "import json; f = open('ver.json', 'r'); j = json.load(f); f.close(); d = j['mkl_fft'][0];" 16 | VER_SCRIPT2: "print('='.join((d[s] for s in ('version', 'build'))))" 17 | 18 | jobs: 19 | build_linux: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | include: 24 | - python: '3.10' 25 | numpy: '2.2' 26 | - python: '3.11' 27 | numpy: '2.3' 28 | - python: '3.12' 29 | numpy: '2.3' 30 | - python: '3.13' 31 | numpy: '2.3' 32 | steps: 33 | - name: Cancel Previous Runs 34 | uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 35 | with: 36 | access_token: ${{ github.token }} 37 | 38 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 39 | with: 40 | fetch-depth: 0 41 | 42 | - name: Set pkgs_dirs 43 | run: | 44 | echo "pkgs_dirs: [~/.conda/pkgs]" >> ~/.condarc 45 | 46 | - name: Cache conda packages 47 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 48 | env: 49 | CACHE_NUMBER: 0 # Increase to reset cache 50 | with: 51 | path: ~/.conda/pkgs 52 | key: 53 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('**/meta.yaml') }} 54 | restore-keys: | 55 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- 56 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- 57 | 58 | - name: Add conda to system path 59 | run: echo $CONDA/bin >> $GITHUB_PATH 60 | 61 | - name: Install conda-build 62 | run: conda install conda-build 63 | 64 | - name: Build conda package with NumPy 2.x 65 | run: | 66 | CHANNELS="-c conda-forge --override-channels" 67 | VERSIONS="--python ${{ matrix.python }} --numpy ${{ matrix.numpy }}" 68 | TEST="--no-test" 69 | 70 | conda build \ 71 | $TEST \ 72 | $VERSIONS \ 73 | $CHANNELS \ 74 | conda-recipe-cf 75 | 76 | - name: Upload artifact 77 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 78 | with: 79 | name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} 80 | path: /usr/share/miniconda/conda-bld/linux-64/${{ env.PACKAGE_NAME }}-*.conda 81 | 82 | test_linux: 83 | needs: build_linux 84 | runs-on: ${{ matrix.runner }} 85 | 86 | strategy: 87 | matrix: 88 | python_ver: ['3.10', '3.11', '3.12', '3.13'] 89 | numpy: ['numpy">=2"'] 90 | experimental: [false] 91 | runner: [ubuntu-latest] 92 | continue-on-error: ${{ matrix.experimental }} 93 | env: 94 | CHANNELS: -c conda-forge --override-channels 95 | 96 | steps: 97 | - name: Download artifact 98 | uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 99 | with: 100 | name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python_ver }} 101 | 102 | - name: Add conda to system path 103 | run: echo $CONDA/bin >> $GITHUB_PATH 104 | 105 | - name: Install conda-build 106 | run: conda install conda-build 107 | 108 | - name: Create conda channel 109 | run: | 110 | mkdir -p $GITHUB_WORKSPACE/channel/linux-64 111 | mv ${PACKAGE_NAME}-*.conda $GITHUB_WORKSPACE/channel/linux-64 112 | conda index $GITHUB_WORKSPACE/channel 113 | # Test channel 114 | conda search $PACKAGE_NAME -c $GITHUB_WORKSPACE/channel --override-channels 115 | 116 | - name: Collect dependencies 117 | run: | 118 | CHANNELS="-c $GITHUB_WORKSPACE/channel ${{ env.CHANNELS }}" 119 | conda create -n ${{ env.TEST_ENV_NAME }} $PACKAGE_NAME python=${{ matrix.python_ver }} ${{ matrix.numpy }} $CHANNELS --only-deps --dry-run > lockfile 120 | 121 | - name: Display lockfile 122 | run: cat lockfile 123 | 124 | - name: Set pkgs_dirs 125 | run: | 126 | echo "pkgs_dirs: [~/.conda/pkgs]" >> ~/.condarc 127 | 128 | - name: Cache conda packages 129 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 130 | env: 131 | CACHE_NUMBER: 0 # Increase to reset cache 132 | with: 133 | path: ~/.conda/pkgs 134 | key: 135 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_ver }}-${{hashFiles('lockfile') }} 136 | restore-keys: | 137 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_ver }}- 138 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- 139 | 140 | - name: Install mkl_fft 141 | run: | 142 | CHANNELS="-c $GITHUB_WORKSPACE/channel ${{ env.CHANNELS }}" 143 | conda create -n ${{ env.TEST_ENV_NAME }} python=${{ matrix.python_ver }} ${{ matrix.numpy }} $PACKAGE_NAME pytest scipy $CHANNELS 144 | # Test installed packages 145 | conda list -n ${{ env.TEST_ENV_NAME }} 146 | 147 | - name: Run tests 148 | run: | 149 | source $CONDA/etc/profile.d/conda.sh 150 | conda activate ${{ env.TEST_ENV_NAME }} 151 | pytest -v --pyargs $MODULE_NAME 152 | 153 | build_windows: 154 | runs-on: windows-latest 155 | 156 | strategy: 157 | matrix: 158 | include: 159 | - python: '3.10' 160 | numpy: '2.2' 161 | - python: '3.11' 162 | numpy: '2.3' 163 | - python: '3.12' 164 | numpy: '2.3' 165 | - python: '3.13' 166 | numpy: '2.3' 167 | steps: 168 | - name: Cancel Previous Runs 169 | uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 170 | with: 171 | access_token: ${{ github.token }} 172 | 173 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 174 | with: 175 | fetch-depth: 0 176 | 177 | - uses: conda-incubator/setup-miniconda@835234971496cad1653abb28a638a281cf32541f # v3.2.0 178 | with: 179 | miniforge-version: latest 180 | activate-environment: build 181 | python-version: ${{ matrix.python }} 182 | channels: conda-forge 183 | conda-remove-defaults: 'true' 184 | 185 | - name: Install conda-build 186 | run: conda install -n base conda-build 187 | 188 | - name: Cache conda packages 189 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 190 | env: 191 | CACHE_NUMBER: 3 # Increase to reset cache 192 | with: 193 | path: /home/runner/conda_pkgs_dir 194 | key: 195 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('**/meta.yaml') }} 196 | restore-keys: | 197 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- 198 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- 199 | 200 | - name: Setup MSVC 201 | uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 202 | 203 | - name: Build conda package with NumPy 2.x 204 | run: | 205 | conda activate 206 | conda build --no-test --python ${{ matrix.python }} --numpy ${{ matrix.numpy }} -c conda-forge --override-channels conda-recipe-cf 207 | 208 | - name: Store conda paths as envs 209 | shell: bash -l {0} 210 | run: | 211 | echo "CONDA_BLD=$CONDA/conda-bld/win-64/" | tr "\\\\" '/' >> $GITHUB_ENV 212 | 213 | - name: Upload artifact 214 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 215 | with: 216 | name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} 217 | path: ${{ env.CONDA_BLD }}${{ env.PACKAGE_NAME }}-*.conda 218 | 219 | test_windows: 220 | needs: build_windows 221 | runs-on: ${{ matrix.runner }} 222 | defaults: 223 | run: 224 | shell: cmd /C CALL {0} 225 | 226 | strategy: 227 | matrix: 228 | python_ver: ['3.10', '3.11', '3.12', '3.13'] 229 | numpy: ['numpy">=2"'] 230 | experimental: [false] 231 | runner: [windows-latest] 232 | continue-on-error: ${{ matrix.experimental }} 233 | env: 234 | workdir: '${{ github.workspace }}' 235 | CHANNELS: -c conda-forge --override-channels 236 | 237 | steps: 238 | - name: Download artifact 239 | uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 240 | with: 241 | name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python_ver }} 242 | 243 | - uses: conda-incubator/setup-miniconda@835234971496cad1653abb28a638a281cf32541f # v3.2.0 244 | with: 245 | miniforge-version: latest 246 | activate-environment: ${{ env.TEST_ENV_NAME }} 247 | python-version: ${{ matrix.python }} 248 | channels: conda-forge 249 | conda-remove-defaults: 'true' 250 | 251 | - name: Create conda channel with the artifact bit 252 | shell: cmd /C CALL {0} 253 | run: | 254 | echo ${{ env.workdir }} 255 | mkdir ${{ env.workdir }}\channel\ 256 | mkdir ${{ env.workdir }}\channel\win-64 257 | move ${{ env.PACKAGE_NAME }}-*.conda ${{ env.workdir }}\channel\win-64 258 | dir ${{ env.workdir }}\channel\win-64 259 | 260 | - name: Install conda index 261 | shell: cmd /C CALL {0} 262 | run: conda install -n base conda-index 263 | 264 | - name: Index the channel 265 | shell: cmd /C CALL {0} 266 | run: conda index ${{ env.workdir }}\channel 267 | 268 | - name: Dump mkl_fft version info from created channel into ver.json 269 | shell: cmd /C CALL {0} 270 | run: | 271 | conda search ${{ env.PACKAGE_NAME }} -c ${{ env.workdir }}/channel --override-channels --info --json > ${{ env.workdir }}\ver.json 272 | 273 | - name: Output content of produced ver.json 274 | shell: pwsh 275 | run: Get-Content -Path ${{ env.workdir }}\ver.json 276 | 277 | - name: Collect dependencies 278 | shell: cmd /C CALL {0} 279 | run: | 280 | @ECHO ON 281 | IF NOT EXIST ver.json ( 282 | copy /Y ${{ env.workdir }}\ver.json . 283 | ) 284 | SET "SCRIPT=%VER_SCRIPT1% %VER_SCRIPT2%" 285 | FOR /F "tokens=* USEBACKQ" %%F IN (`python -c "%SCRIPT%"`) DO ( 286 | SET PACKAGE_VERSION=%%F 287 | ) 288 | conda install -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% python=${{ matrix.python_ver }} ${{ matrix.numpy }} -c ${{ env.workdir }}/channel ${{ env.CHANNELS }} --only-deps --dry-run > lockfile 289 | 290 | - name: Display lockfile content 291 | shell: pwsh 292 | run: Get-Content -Path .\lockfile 293 | 294 | - name: Cache conda packages 295 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 296 | env: 297 | CACHE_NUMBER: 0 # Increase to reset cache 298 | with: 299 | path: /home/runner/conda_pkgs_dir 300 | key: 301 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_ver }}-${{hashFiles('lockfile') }} 302 | restore-keys: | 303 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python_ver }}- 304 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- 305 | 306 | - name: Install mkl_fft 307 | shell: cmd /C CALL {0} 308 | run: | 309 | @ECHO ON 310 | IF NOT EXIST ver.json ( 311 | copy /Y ${{ env.workdir }}\ver.json . 312 | ) 313 | set "SCRIPT=%VER_SCRIPT1% %VER_SCRIPT2%" 314 | FOR /F "tokens=* USEBACKQ" %%F IN (`python -c "%SCRIPT%"`) DO ( 315 | SET PACKAGE_VERSION=%%F 316 | ) 317 | SET "TEST_DEPENDENCIES=pytest scipy" 318 | conda install -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% %TEST_DEPENDENCIES% python=${{ matrix.python }} ${{ matrix.numpy }} -c ${{ env.workdir }}/channel ${{ env.CHANNELS }} 319 | 320 | - name: Report content of test environment 321 | shell: cmd /C CALL {0} 322 | run: | 323 | echo "Value of CONDA environment variable was: " %CONDA% 324 | echo "Value of CONDA_PREFIX environment variable was: " %CONDA_PREFIX% 325 | conda info && conda list -n ${{ env.TEST_ENV_NAME }} 326 | 327 | - name: Run tests 328 | shell: cmd /C CALL {0} 329 | run: >- 330 | conda activate ${{ env.TEST_ENV_NAME }} && python -m pytest -v -s --pyargs ${{ env.MODULE_NAME }} 331 | -------------------------------------------------------------------------------- /.github/workflows/conda-package.yml: -------------------------------------------------------------------------------- 1 | name: Conda package 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | permissions: read-all 10 | 11 | env: 12 | PACKAGE_NAME: mkl_fft 13 | MODULE_NAME: mkl_fft 14 | TEST_ENV_NAME: test_mkl_fft 15 | VER_SCRIPT1: "import json; f = open('ver.json', 'r'); j = json.load(f); f.close(); d = j['mkl_fft'][0];" 16 | VER_SCRIPT2: "print('='.join((d[s] for s in ('version', 'build'))))" 17 | 18 | jobs: 19 | build_linux: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | python: ['3.10', '3.11', '3.12', '3.13'] 24 | steps: 25 | - name: Cancel Previous Runs 26 | uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 27 | with: 28 | access_token: ${{ github.token }} 29 | 30 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: Set pkgs_dirs 35 | run: | 36 | echo "pkgs_dirs: [~/.conda/pkgs]" >> ~/.condarc 37 | 38 | - name: Cache conda packages 39 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 40 | env: 41 | CACHE_NUMBER: 0 # Increase to reset cache 42 | with: 43 | path: ~/.conda/pkgs 44 | key: 45 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('**/meta.yaml') }} 46 | restore-keys: | 47 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- 48 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- 49 | 50 | - name: Add conda to system path 51 | run: echo $CONDA/bin >> $GITHUB_PATH 52 | 53 | - name: Install conda-build 54 | run: conda install conda-build 55 | 56 | - name: Build conda package 57 | run: | 58 | CHANNELS="-c https://software.repos.intel.com/python/conda -c conda-forge --override-channels" 59 | VERSIONS="--python ${{ matrix.python }}" 60 | TEST="--no-test" 61 | 62 | conda build \ 63 | $TEST \ 64 | $VERSIONS \ 65 | $CHANNELS \ 66 | conda-recipe 67 | 68 | - name: Upload artifact 69 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 70 | with: 71 | name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} 72 | path: /usr/share/miniconda/conda-bld/linux-64/${{ env.PACKAGE_NAME }}-*.conda 73 | 74 | test_linux: 75 | needs: build_linux 76 | runs-on: ${{ matrix.runner }} 77 | 78 | strategy: 79 | matrix: 80 | python: ['3.10', '3.11', '3.12', '3.13'] 81 | experimental: [false] 82 | runner: [ubuntu-latest] 83 | continue-on-error: ${{ matrix.experimental }} 84 | env: 85 | CHANNELS: -c https://software.repos.intel.com/python/conda -c conda-forge --override-channels 86 | 87 | steps: 88 | - name: Download artifact 89 | uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 90 | with: 91 | name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} 92 | 93 | - name: Add conda to system path 94 | run: echo $CONDA/bin >> $GITHUB_PATH 95 | 96 | - name: Install conda-build 97 | run: conda install conda-build 98 | 99 | - name: Create conda channel 100 | run: | 101 | mkdir -p $GITHUB_WORKSPACE/channel/linux-64 102 | mv ${PACKAGE_NAME}-*.conda $GITHUB_WORKSPACE/channel/linux-64 103 | conda index $GITHUB_WORKSPACE/channel 104 | 105 | - name: Test conda channel 106 | run: | 107 | conda search $PACKAGE_NAME -c $GITHUB_WORKSPACE/channel --override-channels --info --json > $GITHUB_WORKSPACE/ver.json 108 | cat $GITHUB_WORKSPACE/ver.json 109 | 110 | - name: Get package version 111 | run: | 112 | export PACKAGE_VERSION=$(python -c "${{ env.VER_SCRIPT1 }} ${{ env.VER_SCRIPT2 }}") 113 | echo PACKAGE_VERSION=${PACKAGE_VERSION} 114 | echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> $GITHUB_ENV 115 | 116 | - name: Collect dependencies 117 | run: | 118 | CHANNELS="-c $GITHUB_WORKSPACE/channel ${{ env.CHANNELS }}" 119 | conda create -n ${{ env.TEST_ENV_NAME }} $PACKAGE_NAME python=${{ matrix.python }} $CHANNELS --only-deps --dry-run > lockfile 120 | 121 | - name: Display lockfile 122 | run: cat lockfile 123 | 124 | - name: Set pkgs_dirs 125 | run: | 126 | echo "pkgs_dirs: [~/.conda/pkgs]" >> ~/.condarc 127 | 128 | - name: Cache conda packages 129 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 130 | env: 131 | CACHE_NUMBER: 0 # Increase to reset cache 132 | with: 133 | path: ~/.conda/pkgs 134 | key: 135 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('lockfile') }} 136 | restore-keys: | 137 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- 138 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- 139 | 140 | - name: Install mkl_fft 141 | run: | 142 | CHANNELS="-c $GITHUB_WORKSPACE/channel ${{ env.CHANNELS }}" 143 | conda create -n ${{ env.TEST_ENV_NAME }} $PACKAGE_NAME=${{ env.PACKAGE_VERSION }} python=${{ matrix.python }} pytest $CHANNELS 144 | conda install -n ${{ env.TEST_ENV_NAME }} "scipy>=1.10" $CHANNELS 145 | # Test installed packages 146 | conda list -n ${{ env.TEST_ENV_NAME }} 147 | 148 | - name: Run tests 149 | run: | 150 | source $CONDA/etc/profile.d/conda.sh 151 | conda activate ${{ env.TEST_ENV_NAME }} 152 | pytest -v --pyargs $MODULE_NAME 153 | 154 | build_windows: 155 | runs-on: windows-latest 156 | 157 | strategy: 158 | matrix: 159 | python: ['3.10', '3.11', '3.12', '3.13'] 160 | steps: 161 | - name: Cancel Previous Runs 162 | uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 163 | with: 164 | access_token: ${{ github.token }} 165 | 166 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 167 | with: 168 | fetch-depth: 0 169 | 170 | - uses: conda-incubator/setup-miniconda@835234971496cad1653abb28a638a281cf32541f # v3.2.0 171 | with: 172 | miniforge-version: latest 173 | activate-environment: build 174 | python-version: ${{ matrix.python }} 175 | channels: conda-forge 176 | conda-remove-defaults: 'true' 177 | 178 | - name: Install conda-build 179 | run: | 180 | conda activate 181 | conda install -n base conda-build 182 | 183 | - name: Cache conda packages 184 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 185 | env: 186 | CACHE_NUMBER: 3 # Increase to reset cache 187 | with: 188 | path: /home/runner/conda_pkgs_dir 189 | key: 190 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('**/meta.yaml') }} 191 | restore-keys: | 192 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- 193 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- 194 | 195 | - name: Setup MSVC 196 | uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 197 | 198 | - name: Build conda package 199 | run: | 200 | conda activate 201 | conda build --no-test --python ${{ matrix.python }} -c https://software.repos.intel.com/python/conda -c conda-forge --override-channels conda-recipe 202 | 203 | - name: Store conda paths as envs 204 | shell: bash -l {0} 205 | run: | 206 | echo "CONDA_BLD=$CONDA/conda-bld/win-64/" | tr "\\\\" '/' >> $GITHUB_ENV 207 | 208 | - name: Upload artifact 209 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 210 | with: 211 | name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} 212 | path: ${{ env.CONDA_BLD }}${{ env.PACKAGE_NAME }}-*.conda 213 | 214 | test_windows: 215 | needs: build_windows 216 | runs-on: ${{ matrix.runner }} 217 | defaults: 218 | run: 219 | shell: cmd /C CALL {0} 220 | 221 | strategy: 222 | matrix: 223 | python: ['3.10', '3.11', '3.12', '3.13'] 224 | experimental: [false] 225 | runner: [windows-latest] 226 | continue-on-error: ${{ matrix.experimental }} 227 | env: 228 | workdir: '${{ github.workspace }}' 229 | CHANNELS: -c https://software.repos.intel.com/python/conda -c conda-forge --override-channels 230 | 231 | steps: 232 | - name: Download artifact 233 | uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 234 | with: 235 | name: ${{ env.PACKAGE_NAME }} ${{ runner.os }} Python ${{ matrix.python }} 236 | 237 | - uses: conda-incubator/setup-miniconda@835234971496cad1653abb28a638a281cf32541f # v3.2.0 238 | with: 239 | miniforge-version: latest 240 | activate-environment: ${{ env.TEST_ENV_NAME }} 241 | python-version: ${{ matrix.python }} 242 | channels: conda-forge 243 | conda-remove-defaults: 'true' 244 | 245 | - name: Create conda channel with the artifact bit 246 | shell: cmd /C CALL {0} 247 | run: | 248 | echo ${{ env.workdir }} 249 | mkdir ${{ env.workdir }}\channel\ 250 | mkdir ${{ env.workdir }}\channel\win-64 251 | move ${{ env.PACKAGE_NAME }}-*.conda ${{ env.workdir }}\channel\win-64 252 | dir ${{ env.workdir }}\channel\win-64 253 | 254 | - name: Install conda index 255 | shell: cmd /C CALL {0} 256 | run: conda install -n base conda-index 257 | 258 | - name: Index the channel 259 | shell: cmd /C CALL {0} 260 | run: conda index ${{ env.workdir }}\channel 261 | 262 | - name: Dump mkl_fft version info from created channel into ver.json 263 | shell: cmd /C CALL {0} 264 | run: | 265 | conda search ${{ env.PACKAGE_NAME }} -c ${{ env.workdir }}/channel --override-channels --info --json > ${{ env.workdir }}\ver.json 266 | 267 | - name: Output content of produced ver.json 268 | shell: pwsh 269 | run: Get-Content -Path ${{ env.workdir }}\ver.json 270 | 271 | - name: Collect dependencies 272 | shell: cmd /C CALL {0} 273 | run: | 274 | @ECHO ON 275 | IF NOT EXIST ver.json ( 276 | copy /Y ${{ env.workdir }}\ver.json . 277 | ) 278 | SET "SCRIPT=%VER_SCRIPT1% %VER_SCRIPT2%" 279 | FOR /F "tokens=* USEBACKQ" %%F IN (`python -c "%SCRIPT%"`) DO ( 280 | SET PACKAGE_VERSION=%%F 281 | ) 282 | conda install -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% python=${{ matrix.python }} -c ${{ env.workdir }}/channel ${{ env.CHANNELS }} --only-deps --dry-run > lockfile 283 | 284 | - name: Display lockfile content 285 | shell: pwsh 286 | run: Get-Content -Path .\lockfile 287 | 288 | - name: Cache conda packages 289 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 290 | env: 291 | CACHE_NUMBER: 0 # Increase to reset cache 292 | with: 293 | path: /home/runner/conda_pkgs_dir 294 | key: 295 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}-${{hashFiles('lockfile') }} 296 | restore-keys: | 297 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-python-${{ matrix.python }}- 298 | ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}- 299 | 300 | - name: Install mkl_fft 301 | shell: cmd /C CALL {0} 302 | run: | 303 | @ECHO ON 304 | IF NOT EXIST ver.json ( 305 | copy /Y ${{ env.workdir }}\ver.json . 306 | ) 307 | set "SCRIPT=%VER_SCRIPT1% %VER_SCRIPT2%" 308 | FOR /F "tokens=* USEBACKQ" %%F IN (`python -c "%SCRIPT%"`) DO ( 309 | SET PACKAGE_VERSION=%%F 310 | ) 311 | SET "TEST_DEPENDENCIES=pytest" 312 | conda install -n ${{ env.TEST_ENV_NAME }} ${{ env.PACKAGE_NAME }}=%PACKAGE_VERSION% %TEST_DEPENDENCIES% python=${{ matrix.python }} -c ${{ env.workdir }}/channel ${{ env.CHANNELS }} 313 | conda install -n ${{ env.TEST_ENV_NAME }} scipy -c ${{ env.workdir }}/channel ${{ env.CHANNELS }} 314 | } 315 | - name: Report content of test environment 316 | shell: cmd /C CALL {0} 317 | run: | 318 | echo "Value of CONDA environment variable was: " %CONDA% 319 | echo "Value of CONDA_PREFIX environment variable was: " %CONDA_PREFIX% 320 | conda info && conda list -n ${{ env.TEST_ENV_NAME }} 321 | 322 | - name: Run tests 323 | shell: cmd /C CALL {0} 324 | run: >- 325 | conda activate ${{ env.TEST_ENV_NAME }} && python -m pytest -v -s --pyargs ${{ env.MODULE_NAME }} 326 | -------------------------------------------------------------------------------- /mkl_fft/tests/test_fft1d.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017, Intel Corporation 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of Intel Corporation nor the names of its contributors 13 | # may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS """AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | import numpy as np 28 | import pytest 29 | from numpy import random as rnd 30 | from numpy.testing import TestCase, assert_, assert_allclose 31 | 32 | import mkl_fft 33 | 34 | from .helper import requires_numpy_2 35 | 36 | 37 | def naive_fft1d(vec): 38 | L = len(vec) 39 | phase = -2j * np.pi * (np.arange(L) / float(L)) 40 | phase = np.arange(L).reshape(-1, 1) * phase 41 | return np.sum(vec * np.exp(phase), axis=1) 42 | 43 | 44 | def _datacopied(arr, original): 45 | """ 46 | Strict check for `arr` not sharing any data with `original`, 47 | under the assumption that arr = asarray(original) 48 | """ 49 | if arr is original: 50 | return False 51 | if not isinstance(original, np.ndarray) and hasattr(original, "__array__"): 52 | return False 53 | return arr.base is None 54 | 55 | 56 | class Test_mklfft_vector(TestCase): 57 | def setUp(self): 58 | rnd.seed(1234567) 59 | self.xd1 = rnd.standard_normal(128) 60 | self.xf1 = self.xd1.astype(np.float32) 61 | self.xz1 = ( 62 | rnd.standard_normal((128, 2)).view(dtype=np.complex128).squeeze() 63 | ) 64 | self.xc1 = self.xz1.astype(np.complex64) 65 | 66 | def test_vector1(self): 67 | """check that mkl_fft gives the same result of numpy.fft""" 68 | f1 = mkl_fft.fft(self.xz1) 69 | f2 = naive_fft1d(self.xz1) 70 | assert_allclose(f1, f2, rtol=1e-7, atol=2e-12) 71 | 72 | f1 = mkl_fft.fft(self.xc1) 73 | f2 = naive_fft1d(self.xc1) 74 | assert_allclose(f1, f2, rtol=2e-6, atol=2e-6) 75 | 76 | def test_vector2(self): 77 | """ifft(fft(x)) is identity""" 78 | f1 = mkl_fft.fft(self.xz1) 79 | f2 = mkl_fft.ifft(f1) 80 | assert_(np.allclose(self.xz1, f2)) 81 | 82 | f1 = mkl_fft.fft(self.xc1) 83 | f2 = mkl_fft.ifft(f1) 84 | assert_(np.allclose(self.xc1, f2)) 85 | 86 | f1 = mkl_fft.fft(self.xd1) 87 | f2 = mkl_fft.ifft(f1) 88 | assert_(np.allclose(self.xd1, f2)) 89 | 90 | f1 = mkl_fft.fft(self.xf1) 91 | f2 = mkl_fft.ifft(f1) 92 | assert_(np.allclose(self.xf1, f2, atol=2.0e-7)) 93 | 94 | def test_vector3(self): 95 | """fft(ifft(x)) is identity""" 96 | f1 = mkl_fft.ifft(self.xz1) 97 | f2 = mkl_fft.fft(f1) 98 | assert_(np.allclose(self.xz1, f2)) 99 | 100 | f1 = mkl_fft.ifft(self.xc1) 101 | f2 = mkl_fft.fft(f1) 102 | assert_(np.allclose(self.xc1, f2)) 103 | 104 | f1 = mkl_fft.ifft(self.xd1) 105 | f2 = mkl_fft.fft(f1) 106 | assert_(np.allclose(self.xd1, f2)) 107 | 108 | f1 = mkl_fft.ifft(self.xf1) 109 | f2 = mkl_fft.fft(f1) 110 | assert_(np.allclose(self.xf1, f2, atol=2.0e-7)) 111 | 112 | def test_vector4(self): 113 | """fft of strided is same as fft of contiguous copy""" 114 | x = self.xz1[::2] 115 | f1 = mkl_fft.fft(x) 116 | f2 = mkl_fft.fft(x.copy()) 117 | assert_(np.allclose(f1, f2)) 118 | 119 | x = self.xz1[::-1] 120 | f1 = mkl_fft.fft(x) 121 | f2 = mkl_fft.fft(x.copy()) 122 | assert_(np.allclose(f1, f2)) 123 | 124 | def test_vector5(self): 125 | """fft in-place is the same as fft out-of-place""" 126 | x = self.xz1.copy()[::-2] 127 | f1 = mkl_fft.fft(x, out=x) 128 | f2 = mkl_fft.fft(self.xz1[::-2]) 129 | assert_(np.allclose(f1, f2)) 130 | 131 | def test_vector6(self): 132 | """fft in place""" 133 | x = self.xz1.copy() 134 | f1 = mkl_fft.fft(x, out=x) 135 | assert_(not _datacopied(f1, x)) # this is in-place 136 | 137 | x = self.xz1.copy() 138 | f1 = mkl_fft.fft(x[::-2], out=x[::-2]) 139 | assert_(not np.allclose(x, self.xz1)) # this is also in-place 140 | assert_(np.allclose(x[-2::-2], self.xz1[-2::-2])) 141 | assert_(np.allclose(x[-1::-2], f1)) 142 | 143 | def test_vector7(self): 144 | """fft of real array is the same as fft of its complex cast""" 145 | x = self.xd1[3:17:2] 146 | f1 = mkl_fft.fft(x) 147 | f2 = mkl_fft.fft(x.astype(np.complex128)) 148 | assert_(np.allclose(f1, f2)) 149 | 150 | def test_vector8(self): 151 | """ifft of real array is the same as fft of its complex cast""" 152 | x = self.xd1[3:17:2] 153 | f1 = mkl_fft.ifft(x) 154 | f2 = mkl_fft.ifft(x.astype(np.complex128)) 155 | assert_(np.allclose(f1, f2)) 156 | 157 | def test_vector9(self): 158 | """works on subtypes of ndarray""" 159 | mask = np.zeros(self.xd1.shape, dtype="int") 160 | mask[1] = 1 161 | mask[-2] = 1 162 | x = np.ma.masked_array(self.xd1, mask=mask) 163 | f1 = mkl_fft.fft(x) 164 | f2 = mkl_fft.fft(self.xd1) 165 | assert_allclose(f1, f2) 166 | 167 | def test_vector10(self): 168 | """check n for real arrays""" 169 | x = self.xd1[:8].copy() 170 | f1 = mkl_fft.fft(x, n=7) 171 | f2 = mkl_fft.fft(self.xd1[:7]) 172 | assert_allclose(f1, f2) 173 | 174 | f1 = mkl_fft.fft(x, n=9) 175 | y = self.xd1[:9].copy() 176 | y[-1] = 0.0 177 | f2 = mkl_fft.fft(y) 178 | assert_allclose(f1, f2) 179 | 180 | def test_vector11(self): 181 | """check n for complex arrays""" 182 | x = self.xz1[:8].copy() 183 | f1 = mkl_fft.fft(x, n=7) 184 | f2 = mkl_fft.fft(self.xz1[:7]) 185 | assert_allclose(f1, f2) 186 | 187 | f1 = mkl_fft.fft(x, n=9) 188 | y = self.xz1[:9].copy() 189 | y[-1] = 0.0 + 0.0j 190 | f2 = mkl_fft.fft(y) 191 | assert_allclose(f1, f2) 192 | 193 | def test_vector12(self): 194 | """check fft of float-valued array""" 195 | x = np.arange(20) 196 | f1 = mkl_fft.fft(x) 197 | f2 = mkl_fft.fft(x.astype(np.float64)) 198 | assert_allclose(f1, f2) 199 | 200 | 201 | class DuckArray(np.ndarray): 202 | pass 203 | 204 | 205 | class Test_mklfft_matrix(TestCase): 206 | def setUp(self): 207 | rnd.seed(1234567) 208 | self.ad2 = rnd.standard_normal((4, 3)) 209 | self.af2 = self.ad2.astype(np.float32) 210 | self.az2 = np.dot( 211 | rnd.standard_normal((17, 15, 2)), 212 | np.array([1.0 + 0.0j, 0.0 + 1.0j], dtype=np.complex128), 213 | ) 214 | self.ac2 = self.az2.astype(np.complex64) 215 | self.mat = self.az2.view(DuckArray) 216 | self.xd1 = rnd.standard_normal(128) 217 | 218 | def test_matrix1(self): 219 | x = self.az2.copy() 220 | f1 = mkl_fft.fft(x) 221 | f2 = np.array([mkl_fft.fft(x[i]) for i in range(x.shape[0])]) 222 | assert_allclose(f1, f2) 223 | 224 | f1 = mkl_fft.fft(x, axis=0) 225 | f2 = np.array([mkl_fft.fft(x[:, i]) for i in range(x.shape[1])]).T 226 | assert_allclose(f1, f2) 227 | 228 | def test_matrix2(self): 229 | f1 = mkl_fft.fft(self.az2) 230 | f2 = mkl_fft.fft(self.mat) 231 | assert_allclose(f1, f2) 232 | 233 | def test_matrix3(self): 234 | x = self.az2.copy() 235 | f1 = mkl_fft.fft(x[::3, ::-1]) 236 | f2 = mkl_fft.fft(x[::3, ::-1].copy()) 237 | assert_allclose(f1, f2) 238 | 239 | def test_matrix4(self): 240 | x = self.az2.copy() 241 | f1 = mkl_fft.fft(x[::3, ::-1]) 242 | f2 = mkl_fft.fft(x[::3, ::-1], out=x[::3, ::-1]) 243 | assert_allclose(f1, f2) 244 | 245 | def test_matrix5(self): 246 | x = self.ad2 247 | f1 = mkl_fft.fft(x) 248 | f2 = mkl_fft.ifft(f1) 249 | assert_allclose(x, f2, atol=1e-10) 250 | 251 | def test_matrix6(self): 252 | x = self.ad2 253 | f1 = mkl_fft.ifft(x) 254 | f2 = mkl_fft.fft(f1) 255 | assert_allclose(x, f2, atol=1e-10) 256 | 257 | def test_matrix7(self): 258 | x = self.ad2.copy() 259 | f1 = mkl_fft.fft(x) 260 | f2 = np.array([mkl_fft.fft(x[i]) for i in range(x.shape[0])]) 261 | assert_allclose(f1, f2) 262 | 263 | f1 = mkl_fft.fft(x, axis=0) 264 | f2 = np.array([mkl_fft.fft(x[:, i]) for i in range(x.shape[1])]).T 265 | assert_allclose(f1, f2) 266 | 267 | def test_matrix8(self): 268 | from numpy.lib.stride_tricks import as_strided 269 | 270 | x = self.xd1[:10].copy() 271 | y = as_strided( 272 | x, 273 | shape=( 274 | 4, 275 | 4, 276 | ), 277 | strides=(2 * x.itemsize, x.itemsize), 278 | ) 279 | f1 = mkl_fft.fft(y) 280 | f2 = mkl_fft.fft(y.copy()) 281 | assert_allclose(f1, f2, atol=1e-15, rtol=1e-7) 282 | 283 | 284 | class Test_mklfft_rank3(TestCase): 285 | def setUp(self): 286 | rnd.seed(1234567) 287 | self.ad3 = rnd.standard_normal((7, 11, 19)) 288 | self.af3 = self.ad3.astype(np.float32) 289 | self.az3 = np.dot( 290 | rnd.standard_normal((17, 13, 15, 2)), 291 | np.array([1.0 + 0.0j, 0.0 + 1.0j], dtype=np.complex128), 292 | ) 293 | self.ac3 = self.az3.astype(np.complex64) 294 | 295 | def test_array1(self): 296 | x = self.az3 297 | for ax in range(x.ndim): 298 | f1 = mkl_fft.fft(x, axis=ax) 299 | f2 = mkl_fft.ifft(f1, axis=ax) 300 | assert_allclose(f2, x, atol=2e-15) 301 | 302 | def test_array2(self): 303 | x = self.ad3 304 | for ax in range(x.ndim): 305 | f1 = mkl_fft.fft(x, axis=ax) 306 | f2 = mkl_fft.ifft(f1, axis=ax) 307 | assert_allclose(f2, x, atol=2e-15) 308 | 309 | def test_array3(self): 310 | x = self.az3 311 | for ax in range(x.ndim): 312 | f1 = mkl_fft.ifft(x, axis=ax) 313 | f2 = mkl_fft.fft(f1, axis=ax) 314 | assert_allclose(f2, x, atol=2e-15) 315 | 316 | def test_array4(self): 317 | x = self.ad3 318 | for ax in range(x.ndim): 319 | f1 = mkl_fft.ifft(x, axis=ax) 320 | f2 = mkl_fft.fft(f1, axis=ax) 321 | assert_allclose(f2, x, atol=2e-15) 322 | 323 | def test_array5(self): 324 | """Inputs with zero strides are handled correctly""" 325 | z = self.az3 326 | z1 = z[np.newaxis] 327 | f1 = mkl_fft.fft(z1, axis=-1) 328 | f2 = mkl_fft.fft(z1.reshape(z1.shape), axis=-1) 329 | assert_allclose(f1, f2, atol=2e-15) 330 | z1 = z[:, np.newaxis] 331 | f1 = mkl_fft.fft(z1, axis=-1) 332 | f2 = mkl_fft.fft(z1.reshape(z1.shape), axis=-1) 333 | assert_allclose(f1, f2, atol=2e-15) 334 | z1 = z[:, :, np.newaxis] 335 | f1 = mkl_fft.fft(z1, axis=-1) 336 | f2 = mkl_fft.fft(z1.reshape(z1.shape), axis=-1) 337 | assert_allclose(f1, f2, atol=2e-15) 338 | z1 = z[:, :, :, np.newaxis] 339 | f1 = mkl_fft.fft(z1, axis=-1) 340 | f2 = mkl_fft.fft(z1.reshape(z1.shape), axis=-1) 341 | assert_allclose(f1, f2, atol=2e-15) 342 | 343 | def test_array6(self): 344 | """Inputs with Fortran layout are handled correctly, issue 29""" 345 | z = self.az3 346 | z = z.astype(z.dtype, order="F") 347 | y1 = mkl_fft.fft(z, axis=0) 348 | y2 = mkl_fft.fft(self.az3, axis=0) 349 | assert_allclose(y1, y2, atol=2e-15) 350 | y1 = mkl_fft.fft(z, axis=-1) 351 | y2 = mkl_fft.fft(self.az3, axis=-1) 352 | assert_allclose(y1, y2, atol=2e-15) 353 | 354 | 355 | @requires_numpy_2 356 | @pytest.mark.parametrize("axis", [0, 1, 2]) 357 | @pytest.mark.parametrize("func", ["fft", "ifft"]) 358 | def test_fft_out_strided(axis, func): 359 | shape = (20, 33, 54) 360 | x = rnd.random(shape) + 1j * rnd.random(shape) 361 | out = np.empty(shape, dtype=x.dtype) 362 | 363 | x = x[::2, ::3, ::4] 364 | out = np.empty(x.shape, dtype=x.dtype) 365 | result = getattr(mkl_fft, func)(x, axis=axis, out=out) 366 | expected = getattr(np.fft, func)(x, axis=axis, out=out) 367 | 368 | assert_allclose(result, expected) 369 | 370 | 371 | @requires_numpy_2 372 | @pytest.mark.parametrize("axis", [0, 1, 2]) 373 | def test_rfft_out_strided(axis): 374 | shape = (20, 33, 54) 375 | x = rnd.random(shape) 376 | if axis == 0: 377 | out_sh = (12, 33, 54) 378 | elif axis == 1: 379 | out_sh = (20, 18, 54) 380 | else: # axis == 2 381 | out_sh = (20, 33, 32) 382 | out = np.empty(out_sh, dtype=np.complex128) 383 | 384 | x = x[::2, ::3, ::4] 385 | out = out[::2, ::3, ::4] 386 | result = mkl_fft.rfft(x, axis=axis, out=out) 387 | expected = np.fft.rfft(x, axis=axis, out=out) 388 | 389 | assert_allclose(result, expected) 390 | 391 | 392 | @requires_numpy_2 393 | @pytest.mark.parametrize("axis", [0, 1, 2]) 394 | def test_irfft_out_strided(axis): 395 | shape = (20, 33, 54) 396 | x = rnd.random(shape) + 1j * rnd.random(shape) 397 | if axis == 0: 398 | out_sh = (36, 33, 54) 399 | elif axis == 1: 400 | out_sh = (20, 60, 54) 401 | else: # axis == 2 402 | out_sh = (20, 33, 104) 403 | out = np.empty(out_sh, dtype=np.float64) 404 | 405 | x = x[::2, ::3, ::4] 406 | out = out[::2, ::3, ::4] 407 | result = mkl_fft.irfft(x, axis=axis, out=out) 408 | expected = np.fft.irfft(x, axis=axis, out=out) 409 | 410 | assert_allclose(result, expected) 411 | 412 | 413 | @requires_numpy_2 414 | @pytest.mark.parametrize("dt", ["i4", "f4", "f8", "c8", "c16"]) 415 | def test_irfft_dtype(dt): 416 | x = np.array(rnd.random((20, 20)), dtype=dt) 417 | result = mkl_fft.irfft(x) 418 | expected = np.fft.irfft(x) 419 | assert_allclose(result, expected, rtol=1e-7, atol=1e-7, strict=True) 420 | -------------------------------------------------------------------------------- /mkl_fft/interfaces/_scipy_fft.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017, Intel Corporation 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of Intel Corporation nor the names of its contributors 13 | # may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | """ 28 | An interface for FFT module of SciPy (`scipy.fft`) that uses OneMKL FFT 29 | in the backend. 30 | """ 31 | 32 | import contextlib 33 | import contextvars 34 | import operator 35 | from numbers import Number 36 | 37 | import mkl 38 | import numpy as np 39 | import scipy 40 | 41 | import mkl_fft 42 | 43 | from .._fft_utils import _swap_direction 44 | from ._float_utils import _supported_array_or_not_implemented 45 | 46 | __all__ = [ 47 | "fft", 48 | "ifft", 49 | "fft2", 50 | "ifft2", 51 | "fftn", 52 | "ifftn", 53 | "rfft", 54 | "irfft", 55 | "rfft2", 56 | "irfft2", 57 | "rfftn", 58 | "irfftn", 59 | "hfft", 60 | "ihfft", 61 | "hfft2", 62 | "ihfft2", 63 | "hfftn", 64 | "ihfftn", 65 | "fftshift", 66 | "ifftshift", 67 | "fftfreq", 68 | "rfftfreq", 69 | "get_workers", 70 | "set_workers", 71 | ] 72 | 73 | 74 | class _workers_data: 75 | def __init__(self, workers=None): 76 | if workers is not None: # workers = 0 should be handled 77 | self.workers_ = _workers_to_num_threads(workers) 78 | else: 79 | # Unlike SciPy, the default value is maximum number of threads 80 | self.workers_ = mkl.get_max_threads() # pylint: disable=no-member 81 | self.workers_ = operator.index(self.workers_) 82 | 83 | @property 84 | def workers(self): 85 | return self.workers_ 86 | 87 | @workers.setter 88 | def workers(self, workers_val): 89 | self.workerks_ = operator.index(workers_val) 90 | 91 | 92 | _workers_global_settings = contextvars.ContextVar( 93 | "scipy_backend_workers", default=_workers_data() 94 | ) 95 | 96 | 97 | def _workers_to_num_threads(w): 98 | """ 99 | Handle conversion of workers to a positive number of threads in the 100 | same way as scipy.fft._pocketfft.helpers._workers. 101 | """ 102 | if w is None: 103 | return _workers_global_settings.get().workers 104 | _w = operator.index(w) 105 | if _w == 0: 106 | raise ValueError("Number of workers must not be zero") 107 | if _w < 0: 108 | # SciPy uses os.cpu_count() 109 | _cpu_count = mkl.get_max_threads() # pylint: disable=no-member 110 | _w += _cpu_count + 1 111 | if _w <= 0: 112 | raise ValueError( 113 | f"workers value out of range; got {w}, must not be less " 114 | f"than {-_cpu_count}" 115 | ) 116 | return _w 117 | 118 | 119 | class _Workers: 120 | def __init__(self, workers): 121 | self.workers = workers 122 | self.n_threads = _workers_to_num_threads(workers) 123 | 124 | def __enter__(self): 125 | try: 126 | # mkl.set_num_threads_local sets the number of threads to the 127 | # given input number, and returns the previous number of threads 128 | # pylint: disable=no-member 129 | self.prev_num_threads = mkl.set_num_threads_local(self.n_threads) 130 | except Exception as e: 131 | raise ValueError( 132 | f"Class argument {self.workers} results in invalid number of " 133 | f"threads {self.n_threads}" 134 | ) from e 135 | return self 136 | 137 | def __exit__(self, *args): 138 | # restore old value 139 | # pylint: disable=no-member 140 | mkl.set_num_threads_local(self.prev_num_threads) 141 | 142 | 143 | def _check_plan(plan): 144 | if plan is not None: 145 | raise NotImplementedError( 146 | f"Passing a precomputed plan with value={plan} is currently not supported" 147 | ) 148 | 149 | 150 | # copied from scipy.fft._pocketfft.helper 151 | # https://github.com/scipy/scipy/blob/main/scipy/fft/_pocketfft/helper.py 152 | def _iterable_of_int(x, name=None): 153 | if isinstance(x, Number): 154 | x = (x,) 155 | 156 | try: 157 | x = [operator.index(a) for a in x] 158 | except TypeError as e: 159 | name = name or "value" 160 | raise ValueError( 161 | f"{name} must be a scalar or iterable of integers" 162 | ) from e 163 | 164 | return x 165 | 166 | 167 | # copied and modified from scipy.fft._pocketfft.helper 168 | # https://github.com/scipy/scipy/blob/main/scipy/fft/_pocketfft/helper.py 169 | def _init_nd_shape_and_axes(x, shape, axes, invreal=False): 170 | noshape = shape is None 171 | noaxes = axes is None 172 | 173 | if not noaxes: 174 | axes = _iterable_of_int(axes, "axes") 175 | axes = [a + x.ndim if a < 0 else a for a in axes] 176 | 177 | if any(a >= x.ndim or a < 0 for a in axes): 178 | raise ValueError("axes exceeds dimensionality of input") 179 | if len(set(axes)) != len(axes): 180 | raise ValueError("all axes must be unique") 181 | 182 | if not noshape: 183 | shape = _iterable_of_int(shape, "shape") 184 | 185 | if axes and len(axes) != len(shape): 186 | raise ValueError( 187 | "when given, axes and shape arguments" 188 | " have to be of the same length" 189 | ) 190 | if noaxes: 191 | if len(shape) > x.ndim: 192 | raise ValueError("shape requires more axes than are present") 193 | axes = range(x.ndim - len(shape), x.ndim) 194 | 195 | shape = [x.shape[a] if s == -1 else s for s, a in zip(shape, axes)] 196 | elif noaxes: 197 | shape = list(x.shape) 198 | axes = range(x.ndim) 199 | else: 200 | shape = [x.shape[a] for a in axes] 201 | 202 | if noshape and invreal: 203 | shape[-1] = (x.shape[axes[-1]] - 1) * 2 204 | 205 | if any(s < 1 for s in shape): 206 | raise ValueError(f"invalid number of data points ({shape}) specified") 207 | 208 | return tuple(shape), list(axes) 209 | 210 | 211 | def _use_input_as_out(x, overwrite_x): 212 | """Check if the input can be used as output.""" 213 | if overwrite_x and np.issubdtype(x.dtype, np.complexfloating): 214 | # pass input as out to overwrite it 215 | return x 216 | return None 217 | 218 | 219 | def _validate_input(x): 220 | try: 221 | x = _supported_array_or_not_implemented(x) 222 | except ValueError: 223 | raise NotImplementedError 224 | 225 | return x 226 | 227 | 228 | def fft( 229 | x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *, plan=None 230 | ): 231 | """ 232 | Compute the 1-D discrete Fourier Transform. 233 | 234 | For full documentation refer to `scipy.fft.fft`. 235 | 236 | """ 237 | _check_plan(plan) 238 | x = _validate_input(x) 239 | out = _use_input_as_out(x, overwrite_x) 240 | 241 | with _Workers(workers): 242 | return mkl_fft.fft(x, n=n, axis=axis, norm=norm, out=out) 243 | 244 | 245 | def ifft( 246 | x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *, plan=None 247 | ): 248 | """ 249 | Compute the 1-D inverse discrete Fourier Transform. 250 | 251 | For full documentation refer to `scipy.fft.ifft`. 252 | 253 | """ 254 | _check_plan(plan) 255 | x = _validate_input(x) 256 | out = _use_input_as_out(x, overwrite_x) 257 | 258 | with _Workers(workers): 259 | return mkl_fft.ifft(x, n=n, axis=axis, norm=norm, out=out) 260 | 261 | 262 | def fft2( 263 | x, 264 | s=None, 265 | axes=(-2, -1), 266 | norm=None, 267 | overwrite_x=False, 268 | workers=None, 269 | *, 270 | plan=None, 271 | ): 272 | """ 273 | Compute the 2-D discrete Fourier Transform. 274 | 275 | For full documentation refer to `scipy.fft.fft2`. 276 | 277 | """ 278 | return fftn( 279 | x, 280 | s=s, 281 | axes=axes, 282 | norm=norm, 283 | overwrite_x=overwrite_x, 284 | workers=workers, 285 | plan=plan, 286 | ) 287 | 288 | 289 | def ifft2( 290 | x, 291 | s=None, 292 | axes=(-2, -1), 293 | norm=None, 294 | overwrite_x=False, 295 | workers=None, 296 | *, 297 | plan=None, 298 | ): 299 | """ 300 | Compute the 2-D inverse discrete Fourier Transform. 301 | 302 | For full documentation refer to `scipy.fft.ifft2`. 303 | 304 | """ 305 | return ifftn( 306 | x, 307 | s=s, 308 | axes=axes, 309 | norm=norm, 310 | overwrite_x=overwrite_x, 311 | workers=workers, 312 | plan=plan, 313 | ) 314 | 315 | 316 | def fftn( 317 | x, 318 | s=None, 319 | axes=None, 320 | norm=None, 321 | overwrite_x=False, 322 | workers=None, 323 | *, 324 | plan=None, 325 | ): 326 | """ 327 | Compute the N-D discrete Fourier Transform. 328 | 329 | For full documentation refer to `scipy.fft.fftn`. 330 | 331 | """ 332 | _check_plan(plan) 333 | x = _validate_input(x) 334 | out = _use_input_as_out(x, overwrite_x) 335 | s, axes = _init_nd_shape_and_axes(x, s, axes) 336 | 337 | with _Workers(workers): 338 | return mkl_fft.fftn(x, s=s, axes=axes, norm=norm, out=out) 339 | 340 | 341 | def ifftn( 342 | x, 343 | s=None, 344 | axes=None, 345 | norm=None, 346 | overwrite_x=False, 347 | workers=None, 348 | *, 349 | plan=None, 350 | ): 351 | """ 352 | Compute the N-D inverse discrete Fourier Transform. 353 | 354 | For full documentation refer to `scipy.fft.ifftn`. 355 | 356 | """ 357 | _check_plan(plan) 358 | x = _validate_input(x) 359 | out = _use_input_as_out(x, overwrite_x) 360 | s, axes = _init_nd_shape_and_axes(x, s, axes) 361 | 362 | with _Workers(workers): 363 | return mkl_fft.ifftn(x, s=s, axes=axes, norm=norm, out=out) 364 | 365 | 366 | def rfft( 367 | x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *, plan=None 368 | ): 369 | """ 370 | Compute the 1-D discrete Fourier Transform for real input.. 371 | 372 | For full documentation refer to `scipy.fft.rfft`. 373 | 374 | """ 375 | _check_plan(plan) 376 | x = _validate_input(x) 377 | 378 | with _Workers(workers): 379 | # Note: overwrite_x is not utilized 380 | return mkl_fft.rfft(x, n=n, axis=axis, norm=norm) 381 | 382 | 383 | def irfft( 384 | x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *, plan=None 385 | ): 386 | """ 387 | Compute the inverse of `rfft`. 388 | 389 | For full documentation refer to `scipy.fft.irfft`. 390 | 391 | """ 392 | _check_plan(plan) 393 | x = _validate_input(x) 394 | 395 | with _Workers(workers): 396 | # Note: overwrite_x is not utilized 397 | return mkl_fft.irfft(x, n=n, axis=axis, norm=norm) 398 | 399 | 400 | def rfft2( 401 | x, 402 | s=None, 403 | axes=(-2, -1), 404 | overwrite_x=False, 405 | norm=None, 406 | workers=None, 407 | *, 408 | plan=None, 409 | ): 410 | """ 411 | Compute the 2-D discrete Fourier Transform for real input. 412 | 413 | For full documentation refer to `scipy.fft.rfft2`. 414 | 415 | """ 416 | return rfftn( 417 | x, 418 | s=s, 419 | axes=axes, 420 | norm=norm, 421 | overwrite_x=overwrite_x, 422 | workers=workers, 423 | plan=plan, 424 | ) 425 | 426 | 427 | def irfft2( 428 | x, 429 | s=None, 430 | axes=(-2, -1), 431 | norm=None, 432 | overwrite_x=False, 433 | workers=None, 434 | *, 435 | plan=None, 436 | ): 437 | """ 438 | Compute the inverse of `rfft2`. 439 | 440 | For full documentation refer to `scipy.fft.irfft2`. 441 | 442 | """ 443 | return irfftn( 444 | x, 445 | s=s, 446 | axes=axes, 447 | norm=norm, 448 | overwrite_x=overwrite_x, 449 | workers=workers, 450 | plan=plan, 451 | ) 452 | 453 | 454 | def rfftn( 455 | x, 456 | s=None, 457 | axes=None, 458 | norm=None, 459 | overwrite_x=False, 460 | workers=None, 461 | *, 462 | plan=None, 463 | ): 464 | """ 465 | Compute the N-D discrete Fourier Transform for real input. 466 | 467 | For full documentation refer to `scipy.fft.rfftn`. 468 | 469 | """ 470 | _check_plan(plan) 471 | x = _validate_input(x) 472 | s, axes = _init_nd_shape_and_axes(x, s, axes) 473 | 474 | with _Workers(workers): 475 | # Note: overwrite_x is not utilized 476 | return mkl_fft.rfftn(x, s, axes, norm=norm) 477 | 478 | 479 | def irfftn( 480 | x, 481 | s=None, 482 | axes=None, 483 | norm=None, 484 | overwrite_x=False, 485 | workers=None, 486 | *, 487 | plan=None, 488 | ): 489 | """ 490 | Compute the inverse of `rfftn`. 491 | 492 | For full documentation refer to `scipy.fft.irfftn`. 493 | 494 | """ 495 | _check_plan(plan) 496 | x = _validate_input(x) 497 | s, axes = _init_nd_shape_and_axes(x, s, axes, invreal=True) 498 | 499 | with _Workers(workers): 500 | # Note: overwrite_x is not utilized 501 | return mkl_fft.irfftn(x, s, axes, norm=norm) 502 | 503 | 504 | def hfft( 505 | x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *, plan=None 506 | ): 507 | """ 508 | Compute the FFT of a signal that has Hermitian symmetry, 509 | i.e., a real spectrum. 510 | 511 | For full documentation refer to `scipy.fft.hfft`. 512 | 513 | """ 514 | _check_plan(plan) 515 | x = _validate_input(x) 516 | norm = _swap_direction(norm) 517 | x = np.array(x, copy=True) 518 | np.conjugate(x, out=x) 519 | 520 | with _Workers(workers): 521 | # Note: overwrite_x is not utilized 522 | return mkl_fft.irfft(x, n=n, axis=axis, norm=norm) 523 | 524 | 525 | def ihfft( 526 | x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *, plan=None 527 | ): 528 | """ 529 | Compute the inverse FFT of a signal that has Hermitian symmetry. 530 | 531 | For full documentation refer to `scipy.fft.ihfft`. 532 | 533 | """ 534 | _check_plan(plan) 535 | x = _validate_input(x) 536 | norm = _swap_direction(norm) 537 | 538 | with _Workers(workers): 539 | # Note: overwrite_x is not utilized 540 | result = mkl_fft.rfft(x, n=n, axis=axis, norm=norm) 541 | 542 | np.conjugate(result, out=result) 543 | return result 544 | 545 | 546 | def hfft2( 547 | x, 548 | s=None, 549 | axes=(-2, -1), 550 | norm=None, 551 | overwrite_x=False, 552 | workers=None, 553 | *, 554 | plan=None, 555 | ): 556 | """ 557 | Compute the 2-D FFT of a Hermitian complex array. 558 | 559 | For full documentation refer to `scipy.fft.hfft2`. 560 | 561 | """ 562 | return hfftn( 563 | x, 564 | s=s, 565 | axes=axes, 566 | norm=norm, 567 | overwrite_x=overwrite_x, 568 | workers=workers, 569 | plan=plan, 570 | ) 571 | 572 | 573 | def ihfft2( 574 | x, 575 | s=None, 576 | axes=(-2, -1), 577 | norm=None, 578 | overwrite_x=False, 579 | workers=None, 580 | *, 581 | plan=None, 582 | ): 583 | """ 584 | Compute the 2-D inverse FFT of a real spectrum. 585 | 586 | For full documentation refer to `scipy.fft.ihfft2`. 587 | 588 | """ 589 | return ihfftn( 590 | x, 591 | s=s, 592 | axes=axes, 593 | norm=norm, 594 | overwrite_x=overwrite_x, 595 | workers=workers, 596 | plan=plan, 597 | ) 598 | 599 | 600 | def hfftn( 601 | x, 602 | s=None, 603 | axes=None, 604 | norm=None, 605 | overwrite_x=False, 606 | workers=None, 607 | *, 608 | plan=None, 609 | ): 610 | """ 611 | Compute the N-D FFT of Hermitian symmetric complex input, 612 | i.e., a signal with a real spectrum. 613 | 614 | For full documentation refer to `scipy.fft.hfftn`. 615 | 616 | """ 617 | _check_plan(plan) 618 | x = _validate_input(x) 619 | norm = _swap_direction(norm) 620 | x = np.array(x, copy=True) 621 | np.conjugate(x, out=x) 622 | s, axes = _init_nd_shape_and_axes(x, s, axes, invreal=True) 623 | 624 | with _Workers(workers): 625 | # Note: overwrite_x is not utilized 626 | return mkl_fft.irfftn(x, s, axes, norm=norm) 627 | 628 | 629 | def ihfftn( 630 | x, 631 | s=None, 632 | axes=None, 633 | norm=None, 634 | overwrite_x=False, 635 | workers=None, 636 | *, 637 | plan=None, 638 | ): 639 | """ 640 | Compute the N-D inverse discrete Fourier Transform for a real spectrum. 641 | 642 | For full documentation refer to `scipy.fft.ihfftn`. 643 | 644 | """ 645 | _check_plan(plan) 646 | x = _validate_input(x) 647 | norm = _swap_direction(norm) 648 | s, axes = _init_nd_shape_and_axes(x, s, axes) 649 | 650 | with _Workers(workers): 651 | # Note: overwrite_x is not utilized 652 | result = mkl_fft.rfftn(x, s, axes, norm=norm) 653 | 654 | np.conjugate(result, out=result) 655 | return result 656 | 657 | 658 | # define thin wrappers for scipy functions to avoid circular dependencies 659 | def fftfreq(n, d=1.0, *, xp=None, device=None): 660 | """ 661 | Return the Discrete Fourier Transform sample frequencies. 662 | 663 | For full documentation refer to `scipy.fft.fftfreq`. 664 | 665 | """ 666 | return scipy.fft.fftfreq(n, d=d, xp=xp, device=device) 667 | 668 | 669 | def rfftfreq(n, d=1.0, *, xp=None, device=None): 670 | """ 671 | Return the Discrete Fourier Transform sample frequencies (for usage with 672 | `rfft`, `irfft`). 673 | 674 | For full documentation refer to `scipy.fft.rfftfreq`. 675 | 676 | """ 677 | return scipy.fft.rfftfreq(n, d=d, xp=xp, device=device) 678 | 679 | 680 | def fftshift(x, axes=None): 681 | """ 682 | Shift the zero-frequency component to the center of the spectrum. 683 | 684 | For full documentation refer to `scipy.fft.fftshift`. 685 | 686 | """ 687 | from scipy.fft import fftshift 688 | 689 | return fftshift(x, axes=axes) 690 | 691 | 692 | def ifftshift(x, axes=None): 693 | """ 694 | The inverse of `fftshift`. Although identical for even-length `x`, the 695 | functions differ by one sample for odd-length `x`. 696 | 697 | For full documentation refer to `scipy.fft.ifftshift`. 698 | 699 | """ 700 | return scipy.fft.ifftshift(x, axes=axes) 701 | 702 | 703 | def get_workers(): 704 | """ 705 | Gets the number of workers used by mkl_fft by default. 706 | 707 | For full documentation refer to `scipy.fft.get_workers`. 708 | 709 | """ 710 | return _workers_global_settings.get().workers 711 | 712 | 713 | @contextlib.contextmanager 714 | def set_workers(workers): 715 | """ 716 | Set the value of workers used by default, returns the previous value. 717 | 718 | For full documentation refer to `scipy.fft.set_workers`. 719 | 720 | """ 721 | nw = operator.index(workers) 722 | token = None 723 | try: 724 | new_wd = _workers_data(nw) 725 | token = _workers_global_settings.set(new_wd) 726 | yield 727 | finally: 728 | if token is not None: 729 | _workers_global_settings.reset(token) 730 | -------------------------------------------------------------------------------- /mkl_fft/_fft_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2025, Intel Corporation 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # * Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of Intel Corporation nor the names of its contributors 13 | # may be used to endorse or promote products derived from this software 14 | # without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | import numpy as np 28 | 29 | # pylint: disable=no-name-in-module 30 | from ._pydfti import ( 31 | _c2c_fft1d_impl, 32 | _c2r_fft1d_impl, 33 | _direct_fftnd, 34 | _r2c_fft1d_impl, 35 | _validate_out_array, 36 | ) 37 | 38 | 39 | def _check_norm(norm): 40 | if norm not in (None, "ortho", "forward", "backward"): 41 | raise ValueError( 42 | f"Invalid norm value {norm} should be None, 'ortho', 'forward', " 43 | "or 'backward'." 44 | ) 45 | 46 | 47 | def _check_shapes_for_direct(xs, shape, axes): 48 | if len(axes) > 7: # Intel MKL supports up to 7D 49 | return False 50 | if not (len(xs) == len(shape)): 51 | # full-dimensional transform 52 | return False 53 | if not (len(set(axes)) == len(axes)): 54 | # repeated axes 55 | return False 56 | for xsi, ai in zip(xs, axes): 57 | try: 58 | sh_ai = shape[ai] 59 | except IndexError: 60 | raise ValueError("Invalid axis (%d) specified" % ai) 61 | 62 | if not (xsi == sh_ai): 63 | return False 64 | return True 65 | 66 | 67 | def _compute_fwd_scale(norm, n, shape): 68 | _check_norm(norm) 69 | if norm in (None, "backward"): 70 | return 1.0 71 | 72 | ss = n if n is not None else shape 73 | nn = np.prod(ss) 74 | fsc = 1 / nn if nn != 0 else 1 75 | if norm == "forward": 76 | return fsc 77 | else: # norm == "ortho" 78 | return np.sqrt(fsc) 79 | 80 | 81 | def _cook_nd_args(a, s=None, axes=None, invreal=False): 82 | if s is None: 83 | shapeless = True 84 | if axes is None: 85 | s = list(a.shape) 86 | else: 87 | try: 88 | s = [a.shape[i] for i in axes] 89 | except IndexError: 90 | # fake s designed to trip the ValueError further down 91 | s = range(len(axes) + 1) 92 | pass 93 | else: 94 | shapeless = False 95 | s = list(s) 96 | if axes is None: 97 | axes = list(range(-len(s), 0)) 98 | if len(s) != len(axes): 99 | raise ValueError("Shape and axes have different lengths.") 100 | if invreal and shapeless: 101 | s[-1] = (a.shape[axes[-1]] - 1) * 2 102 | return s, axes 103 | 104 | 105 | # copied from scipy.fft module 106 | # https://github.com/scipy/scipy/blob/main/scipy/fft/_pocketfft/helper.py 107 | def _datacopied(arr, original): 108 | """ 109 | Strict check for `arr` not sharing any data with `original`, 110 | under the assumption that arr = np.asarray(original). 111 | """ 112 | if arr is original: 113 | return False 114 | if not isinstance(original, np.ndarray) and hasattr(original, "__array__"): 115 | return False 116 | return arr.base is None 117 | 118 | 119 | def _flat_to_multi(ind, shape): 120 | nd = len(shape) 121 | m_ind = [-1] * nd 122 | j = ind 123 | for i in range(nd): 124 | si = shape[nd - 1 - i] 125 | q = j // si 126 | r = j - si * q 127 | m_ind[nd - 1 - i] = r 128 | j = q 129 | return m_ind 130 | 131 | 132 | # copied from scipy.fftpack.helper 133 | def _init_nd_shape_and_axes(x, shape, axes): 134 | """Handle shape and axes arguments for n-dimensional transforms. 135 | Returns the shape and axes in a standard form, taking into account negative 136 | values and checking for various potential errors. 137 | Parameters 138 | ---------- 139 | x : array_like 140 | The input array. 141 | shape : int or array_like of ints or None 142 | The shape of the result. If both `shape` and `axes` (see below) are 143 | None, `shape` is ``x.shape``; if `shape` is None but `axes` is 144 | not None, then `shape` is ``scipy.take(x.shape, axes, axis=0)``. 145 | If `shape` is -1, the size of the corresponding dimension of `x` is 146 | used. 147 | axes : int or array_like of ints or None 148 | Axes along which the calculation is computed. 149 | The default is over all axes. 150 | Negative indices are automatically converted to their positive 151 | counterpart. 152 | Returns 153 | ------- 154 | shape : array 155 | The shape of the result. It is a 1D integer array. 156 | axes : array 157 | The shape of the result. It is a 1D integer array. 158 | """ 159 | x = np.asarray(x) 160 | noshape = shape is None 161 | noaxes = axes is None 162 | 163 | if noaxes: 164 | axes = np.arange(x.ndim, dtype=np.intc) 165 | else: 166 | axes = np.atleast_1d(axes) 167 | 168 | if axes.size == 0: 169 | axes = axes.astype(np.intc) 170 | 171 | if not axes.ndim == 1: 172 | raise ValueError("when given, axes values must be a scalar or vector") 173 | if not np.issubdtype(axes.dtype, np.integer): 174 | raise ValueError("when given, axes values must be integers") 175 | 176 | axes = np.where(axes < 0, axes + x.ndim, axes) 177 | 178 | if axes.size != 0 and (axes.max() >= x.ndim or axes.min() < 0): 179 | raise ValueError("axes exceeds dimensionality of input") 180 | if axes.size != 0 and np.unique(axes).shape != axes.shape: 181 | raise ValueError("all axes must be unique") 182 | 183 | if not noshape: 184 | shape = np.atleast_1d(shape) 185 | elif np.isscalar(x): 186 | shape = np.array([], dtype=np.intc) 187 | elif noaxes: 188 | shape = np.array(x.shape, dtype=np.intc) 189 | else: 190 | shape = np.take(x.shape, axes) 191 | 192 | if shape.size == 0: 193 | shape = shape.astype(np.intc) 194 | 195 | if shape.ndim != 1: 196 | raise ValueError("when given, shape values must be a scalar or vector") 197 | if not np.issubdtype(shape.dtype, np.integer): 198 | raise ValueError("when given, shape values must be integers") 199 | if axes.shape != shape.shape: 200 | raise ValueError( 201 | "when given, axes and shape arguments have to be of the same length" 202 | ) 203 | 204 | shape = np.where(shape == -1, np.array(x.shape)[axes], shape) 205 | if shape.size != 0 and (shape < 1).any(): 206 | raise ValueError(f"invalid number of data points ({shape}) specified") 207 | 208 | return shape, axes 209 | 210 | 211 | def _iter_complementary(x, axes, func, kwargs, result): 212 | if axes is None: 213 | # s and axes are None, direct N-D FFT 214 | return func(x, **kwargs, out=result) 215 | x_shape = x.shape 216 | nd = x.ndim 217 | r = list(range(nd)) 218 | sl = [slice(None, None, None)] * nd 219 | if not np.iterable(axes): 220 | axes = (axes,) 221 | for ai in axes: 222 | r[ai] = None 223 | size = 1 224 | sub_shape = [] 225 | dual_ind = [] 226 | for ri in r: 227 | if ri is not None: 228 | size *= x_shape[ri] 229 | sub_shape.append(x_shape[ri]) 230 | dual_ind.append(ri) 231 | 232 | for ind in range(size): 233 | m_ind = _flat_to_multi(ind, sub_shape) 234 | for k1, k2 in zip(dual_ind, m_ind): 235 | sl[k1] = k2 236 | if np.issubdtype(x.dtype, np.complexfloating): 237 | func(x[tuple(sl)], **kwargs, out=result[tuple(sl)]) 238 | else: 239 | # For c2c FFT, if the input is real, half of the output is the 240 | # complex conjugate of the other half. Instead of upcasting the 241 | # input to complex and performing c2c FFT, we perform rfft and then 242 | # construct the other half using the first half. 243 | # However, when using the `out` keyword here I encountered a result 244 | # in tests/third_party/scipy/test_basic.py::test_fft_with_order 245 | # that was correct but the elements were not necessarily similar to 246 | # NumPy. For example, an element in the first half of mkl_fft output 247 | # array appeared in the second half of the NumPy output array, 248 | # while the equivalent element in the NumPy array was the conjugate 249 | # of the mkl_fft output array. 250 | np.copyto(result[tuple(sl)], func(x[tuple(sl)], **kwargs)) 251 | 252 | return result 253 | 254 | 255 | def _iter_fftnd( 256 | a, 257 | s=None, 258 | axes=None, 259 | out=None, 260 | direction=+1, 261 | scale_function=lambda ind: 1.0, 262 | ): 263 | a = np.asarray(a) 264 | s, axes = _init_nd_shape_and_axes(a, s, axes) 265 | 266 | # Combine the two, but in reverse, to end with the first axis given. 267 | axes_and_s = list(zip(axes, s))[::-1] 268 | # We try to use in-place calculations where possible, which is 269 | # everywhere except when the size changes after the first FFT. 270 | size_changes = [axis for axis, n in axes_and_s[1:] if a.shape[axis] != n] 271 | 272 | # If there are any size changes, we cannot use out 273 | res = None if size_changes else out 274 | for ind, (axis, n) in enumerate(axes_and_s): 275 | if axis in size_changes: 276 | if axis == size_changes[-1]: 277 | # Last size change, so any output should now be OK 278 | # (an error will be raised if not), and if no output is 279 | # required, we want a freshly allocated array of the right size. 280 | res = out 281 | elif res is not None and n < res.shape[axis]: 282 | # For an intermediate step where we return fewer elements, we 283 | # can use a smaller view of the previous array. 284 | res = res[(slice(None),) * axis + (slice(n),)] 285 | else: 286 | # If we need more elements, we cannot use res. 287 | res = None 288 | a = _c2c_fft1d_impl( 289 | a, 290 | n=n, 291 | axis=axis, 292 | direction=direction, 293 | fsc=scale_function(ind), 294 | out=res, 295 | ) 296 | # Default output for next iteration. 297 | res = a 298 | return a 299 | 300 | 301 | def _output_dtype(dt): 302 | if dt == np.float64: 303 | return np.complex128 304 | if dt == np.float32: 305 | return np.complex64 306 | return dt 307 | 308 | 309 | def _pad_array(arr, s, axes): 310 | """Pads array arr with zeros to attain shape s associated with axes""" 311 | arr_shape = arr.shape 312 | no_padding = True 313 | pad_widths = [(0, 0)] * len(arr_shape) 314 | for si, ai in zip(s, axes): 315 | try: 316 | shp_i = arr_shape[ai] 317 | except IndexError: 318 | raise ValueError(f"Invalid axis {ai} specified") 319 | if si > shp_i: 320 | no_padding = False 321 | pad_widths[ai] = (0, si - shp_i) 322 | if no_padding: 323 | return arr 324 | return np.pad(arr, tuple(pad_widths), "constant") 325 | 326 | 327 | def _remove_axis(s, axes, axis_to_remove): 328 | lens = len(s) 329 | axes_normalized = tuple(lens + ai if ai < 0 else ai for ai in axes) 330 | a2r = lens + axis_to_remove if axis_to_remove < 0 else axis_to_remove 331 | 332 | ss = s[:a2r] + s[a2r + 1 :] 333 | pivot = axes_normalized[a2r] 334 | aa = tuple( 335 | ai if ai < pivot else ai - 1 for ai in axes_normalized[:a2r] 336 | ) + tuple(ai if ai < pivot else ai - 1 for ai in axes_normalized[a2r + 1 :]) 337 | return ss, aa 338 | 339 | 340 | def _trim_array(arr, s, axes): 341 | """ 342 | Forms a view into subarray of arr if any element of shape parameter s is 343 | smaller than the corresponding element of the shape of the input array arr, 344 | otherwise returns the input array. 345 | """ 346 | 347 | arr_shape = arr.shape 348 | no_trim = True 349 | ind = [slice(None, None, None)] * len(arr_shape) 350 | for si, ai in zip(s, axes): 351 | try: 352 | shp_i = arr_shape[ai] 353 | except IndexError: 354 | raise ValueError(f"Invalid axis {ai} specified") 355 | if si < shp_i: 356 | no_trim = False 357 | ind[ai] = slice(None, si, None) 358 | if no_trim: 359 | return arr 360 | return arr[tuple(ind)] 361 | 362 | 363 | def _swap_direction(norm): 364 | _check_norm(norm) 365 | _swap_direction_map = { 366 | "backward": "forward", 367 | None: "forward", 368 | "ortho": "ortho", 369 | "forward": "backward", 370 | } 371 | 372 | return _swap_direction_map[norm] 373 | 374 | 375 | def _c2c_fftnd_impl( 376 | x, 377 | s=None, 378 | axes=None, 379 | direction=+1, 380 | fsc=1.0, 381 | out=None, 382 | ): 383 | if direction not in [-1, +1]: 384 | raise ValueError("Direction of FFT should +1 or -1") 385 | 386 | valid_dtypes = [np.complex64, np.complex128, np.float32, np.float64] 387 | # _direct_fftnd requires complex type, and full-dimensional transform 388 | if isinstance(x, np.ndarray) and x.size != 0 and x.ndim > 1: 389 | _direct = s is None and axes is None 390 | if _direct: 391 | _direct = x.ndim <= 7 # Intel MKL only supports FFT up to 7D 392 | if not _direct: 393 | xs, xa = _cook_nd_args(x, s, axes) 394 | if _check_shapes_for_direct(xs, x.shape, xa): 395 | _direct = True 396 | _direct = _direct and x.dtype in valid_dtypes 397 | else: 398 | _direct = False 399 | 400 | if _direct: 401 | return _direct_fftnd( 402 | x, 403 | direction=direction, 404 | fsc=fsc, 405 | out=out, 406 | ) 407 | else: 408 | if s is None and x.dtype in valid_dtypes: 409 | x = np.asarray(x) 410 | if out is None: 411 | res = np.empty_like(x, dtype=_output_dtype(x.dtype)) 412 | else: 413 | _validate_out_array(out, x, _output_dtype(x.dtype)) 414 | res = out 415 | 416 | return _iter_complementary( 417 | x, 418 | axes, 419 | _direct_fftnd, 420 | {"direction": direction, "fsc": fsc}, 421 | res, 422 | ) 423 | else: 424 | # perform N-D FFT as a series of 1D FFTs 425 | return _iter_fftnd( 426 | x, 427 | s=s, 428 | axes=axes, 429 | out=out, 430 | direction=direction, 431 | scale_function=lambda i: fsc if i == 0 else 1.0, 432 | ) 433 | 434 | 435 | def _r2c_fftnd_impl(x, s=None, axes=None, fsc=1.0, out=None): 436 | a = np.asarray(x) 437 | no_trim = (s is None) and (axes is None) 438 | s, axes = _cook_nd_args(a, s, axes) 439 | axes = [ax + a.ndim if ax < 0 else ax for ax in axes] 440 | la = axes[-1] 441 | 442 | # trim array, so that rfft avoids doing unnecessary computations 443 | if not no_trim: 444 | a = _trim_array(a, s, axes) 445 | 446 | # last axis is not included since we calculate r2c FFT separately 447 | # and not in the loop 448 | axes_and_s = list(zip(axes, s))[-2::-1] 449 | size_changes = [axis for axis, n in axes_and_s if a.shape[axis] != n] 450 | res = None if size_changes else out 451 | 452 | # r2c along last axis 453 | a = _r2c_fft1d_impl(a, n=s[-1], axis=la, fsc=fsc, out=res) 454 | res = a 455 | if len(s) > 1: 456 | 457 | len_axes = len(axes) 458 | if len(set(axes)) == len_axes and len_axes == a.ndim and len_axes > 2: 459 | if not no_trim: 460 | ss = list(s) 461 | ss[-1] = a.shape[la] 462 | a = _pad_array(a, tuple(ss), axes) 463 | # a series of ND c2c FFTs along last axis 464 | ss, aa = _remove_axis(s, axes, -1) 465 | ind = [slice(None, None, 1)] * len(s) 466 | for ii in range(a.shape[la]): 467 | ind[la] = ii 468 | tind = tuple(ind) 469 | a_inp = a[tind] 470 | res = out[tind] if out is not None else a_inp 471 | _ = _c2c_fftnd_impl(a_inp, s=ss, axes=aa, direction=1, out=res) 472 | if out is not None: 473 | a = out 474 | else: 475 | # another size_changes check is needed if there are repeated axes 476 | # of last axis, since since FFT changes the shape along last axis 477 | size_changes = [ 478 | axis for axis, n in axes_and_s if a.shape[axis] != n 479 | ] 480 | 481 | # a series of 1D c2c FFTs along all axes except last 482 | for axis, n in axes_and_s: 483 | if axis in size_changes: 484 | if axis == size_changes[-1]: 485 | res = out 486 | elif res is not None and n < res.shape[axis]: 487 | res = res[(slice(None),) * axis + (slice(n),)] 488 | else: 489 | res = None 490 | a = _c2c_fft1d_impl(a, n, axis, out=res) 491 | res = a 492 | return a 493 | 494 | 495 | def _c2r_fftnd_impl(x, s=None, axes=None, fsc=1.0, out=None): 496 | a = np.asarray(x) 497 | no_trim = (s is None) and (axes is None) 498 | s, axes = _cook_nd_args(a, s, axes, invreal=True) 499 | axes = [ax + a.ndim if ax < 0 else ax for ax in axes] 500 | la = axes[-1] 501 | if not no_trim: 502 | a = _trim_array(a, s, axes) 503 | if len(s) > 1: 504 | len_axes = len(axes) 505 | if len(set(axes)) == len_axes and len_axes == a.ndim and len_axes > 2: 506 | if not no_trim: 507 | a = _pad_array(a, s, axes) 508 | # a series of ND c2c FFTs along last axis 509 | # due to need to write into a, we must copy 510 | a = a if _datacopied(a, x) else a.copy() 511 | if not np.issubdtype(a.dtype, np.complexfloating): 512 | # complex output will be copied to input, copy is needed 513 | if a.dtype == np.float32: 514 | a = a.astype(np.complex64) 515 | else: 516 | a = a.astype(np.complex128) 517 | ss, aa = _remove_axis(s, axes, -1) 518 | ind = [slice(None, None, 1)] * len(s) 519 | for ii in range(a.shape[la]): 520 | ind[la] = ii 521 | tind = tuple(ind) 522 | a_inp = a[tind] 523 | # out has real dtype and cannot be used in intermediate steps 524 | # ss and aa are reversed since np.irfftn uses forward order but 525 | # np.ifftn uses reverse order see numpy-gh-28950 526 | _ = _c2c_fftnd_impl( 527 | a_inp, s=ss[::-1], axes=aa[::-1], out=a_inp, direction=-1 528 | ) 529 | else: 530 | # a series of 1D c2c FFTs along all axes except last 531 | # forward order, see numpy-gh-28950 532 | axes_and_s = list(zip(axes, s))[:-1] 533 | size_changes = [ 534 | axis for axis, n in axes_and_s[1:] if a.shape[axis] != n 535 | ] 536 | # out has real dtype cannot be used for intermediate steps 537 | res = None 538 | for axis, n in axes_and_s: 539 | if axis in size_changes: 540 | if res is not None and n < res.shape[axis]: 541 | # pylint: disable=unsubscriptable-object 542 | res = res[(slice(None),) * axis + (slice(n),)] 543 | else: 544 | res = None 545 | a = _c2c_fft1d_impl(a, n, axis, out=res, direction=-1) 546 | res = a 547 | # c2r along last axis 548 | a = _c2r_fft1d_impl(a, n=s[-1], axis=la, fsc=fsc, out=out) 549 | return a 550 | --------------------------------------------------------------------------------