diff --git a/.github/fluidfft-site.cfg b/.github/fluidfft-site-mpi.cfg similarity index 100% rename from .github/fluidfft-site.cfg rename to .github/fluidfft-site-mpi.cfg diff --git a/.github/fluidfft-site-macos.cfg b/.github/fluidfft-site-seq.cfg similarity index 100% rename from .github/fluidfft-site-macos.cfg rename to .github/fluidfft-site-seq.cfg diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 831f97d..f93f045 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -12,9 +12,10 @@ jobs: python-version: [3.9, "3.10", "3.11"] steps: + - name: apt install run: | - sudo apt install -y libfftw3-dev libfftw3-mpi-dev \ + sudo apt-get install -y make libfftw3-dev libfftw3-mpi-dev \ libhdf5-openmpi-dev openmpi-bin libopenmpi-dev \ libopenblas-dev @@ -37,8 +38,10 @@ jobs: - name: Run tests run: | - cp .github/fluidfft-site.cfg $HOME/.fluidfft-site.cfg + cp .github/fluidfft-site-seq.cfg $HOME/.fluidfft-site.cfg nox --session "tests(with_cov=True, with_mpi=False)" + make cleanall + cp .github/fluidfft-site-mpi.cfg $HOME/.fluidfft-site.cfg nox --session "tests(with_cov=True, with_mpi=True)" coverage xml diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index a77bbf4..f1141cf 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -24,8 +24,9 @@ jobs: use-mamba: true - name: Install run: | - cp .github/fluidfft-site-macos.cfg $HOME/.fluidfft-site.cfg + cp .github/fluidfft-site-seq.cfg $HOME/.fluidfft-site.cfg pip install -e . -v --no-build-isolation --no-deps + pip install plugins/fluidfft-pyfftw - name: Tests run: | - pytest -v + pytest -v tests plugins/fluidfft-pyfftw diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 94ccf59..cb53822 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -31,6 +31,7 @@ jobs: run: | cp .github/pythranrc-windows $HOME/.pythranrc pip install -e . -v --no-build-isolation --no-deps + pip install plugins/fluidfft-pyfftw - name: Tests run: | - pytest -v + pytest -v tests plugins/fluidfft-pyfftw diff --git a/.readthedocs.yml b/.readthedocs.yml index a5a6165..5e0a42b 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -15,6 +15,7 @@ build: post_install: - pdm use -f $READTHEDOCS_VIRTUALENV_PATH - pdm sync -G doc --no-self + - pdm run pip install plugins/fluidfft-pyfftw - FLUIDFFT_TRANSONIC_BACKEND="python" pip install . -v --no-deps sphinx: diff --git a/Makefile b/Makefile index cd38264..6ce92b1 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,11 @@ -.PHONY: clean cleanall cleanmako cleancython develop build_ext_inplace list-sessions requirements +.PHONY: clean cleanall cleanmako cleancython develop build_ext_inplace list-sessions tests develop: sync pdm run pip install -e . --no-deps --no-build-isolation -v sync: pdm sync --clean --no-self + pdm run pip install -e plugins/fluidfft-pyfftw clean: rm -rf build @@ -27,20 +28,20 @@ black: pdm run black tests: - pytest -s src + pytest -s tests tests_mpi: - mpirun -np 2 pytest -s src + mpirun -np 2 pytest -s tests tests_mpi4: - mpirun -np 4 pytest -s src + mpirun -np 4 pytest -s tests _tests_coverage: mkdir -p .coverage - coverage run -p -m pytest -s src - TRANSONIC_NO_REPLACE=1 coverage run -p -m pytest -s src + coverage run -p -m pytest -s tests + TRANSONIC_NO_REPLACE=1 coverage run -p -m pytest -s tests # Using TRANSONIC_NO_REPLACE with mpirun in docker can block the tests - mpirun -np 2 --oversubscribe coverage run -p -m unittest discover src + mpirun -np 2 --oversubscribe coverage run -p -m unittest discover tests _report_coverage: coverage combine diff --git a/doc/ipynb/tuto_fft2d_seq.md b/doc/ipynb/tuto_fft2d_seq.md index 4719f6e..c24959f 100644 --- a/doc/ipynb/tuto_fft2d_seq.md +++ b/doc/ipynb/tuto_fft2d_seq.md @@ -21,12 +21,11 @@ If you really want performance, first benchmark the different methods for an arr ```{code-cell} ipython3 import numpy as np -from fluidfft.fft2d import methods_seq -from fluidfft import import_fft_class +from fluidfft import get_methods, import_fft_class ``` ```{code-cell} ipython3 -print(methods_seq) +print(get_methods(ndim=2, sequential=True)) ``` We import a class and instantiate it: diff --git a/doc/ipynb/tuto_fft3d_seq.md b/doc/ipynb/tuto_fft3d_seq.md index 645e6e9..869b5f6 100644 --- a/doc/ipynb/tuto_fft3d_seq.md +++ b/doc/ipynb/tuto_fft3d_seq.md @@ -17,12 +17,11 @@ In this tutorial, we present how to use fluidfft to perform 3D fft in sequential ```{code-cell} ipython3 import numpy as np -from fluidfft.fft3d import methods_seq -from fluidfft import import_fft_class +from fluidfft import get_methods, import_fft_class ``` ```{code-cell} ipython3 -print(methods_seq) +print(get_methods(ndim=3, sequential=True)) ``` We import a class and instantiate it: diff --git a/noxfile.py b/noxfile.py index b973ea6..294dc7e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -52,6 +52,8 @@ def tests(session, with_mpi, with_cov): session.install("-e", ".", "--no-deps", "-v", silent=False) session.run("ls", "src/fluidfft/fft3d", silent=False, external=True) + session.install("-e", "plugins/fluidfft-pyfftw") + def run_command(command, **kwargs): session.run(*command.split(), **kwargs) @@ -59,7 +61,7 @@ def run_command(command, **kwargs): cov_path = Path.cwd() / ".coverage" cov_path.mkdir(exist_ok=True) - command = "pytest -v -s" + command = "pytest -v -s tests" if with_cov: command += ( " --cov --cov-config=setup.cfg --no-cov-on-fail --cov-report=term-missing" @@ -70,12 +72,10 @@ def run_command(command, **kwargs): if with_mpi: if with_cov: - command = ( - "mpirun -np 2 --oversubscribe coverage run -p -m pytest --exitfirst src" - ) + command = "mpirun -np 2 --oversubscribe coverage run -p -m pytest -v -s --exitfirst tests" else: - command = "mpirun -np 2 --oversubscribe pytest src" + command = "mpirun -np 2 --oversubscribe pytest -v -s tests" # Using TRANSONIC_NO_REPLACE with mpirun in docker can block the tests run_command(command, external=True) @@ -92,6 +92,7 @@ def doc(session): session.install( "-e", ".", "--no-deps", env={"FLUIDFFT_TRANSONIC_BACKEND": "python"} ) + session.install("-e", "plugins/fluidfft-pyfftw") session.chdir("doc") session.run("make", "cleanall", external=True) diff --git a/pdm.lock b/pdm.lock index 8f5e671..f6ea9b5 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "build", "dev", "doc", "lint", "mpi", "pyfftw", "test"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:3f0c968b3d15ae6ab4202c70cbd3809ab59408b75b3f02fe70ccc684fef9b99f" +content_hash = "sha256:b0cd5d3097b0ce5d9dd24d5e6d0a49614375083d8e5bca3d75198ef401714893" [[package]] name = "alabaster" @@ -216,7 +216,7 @@ files = [ [[package]] name = "black" -version = "23.12.1" +version = "24.1.0" requires_python = ">=3.8" summary = "The uncompromising code formatter." groups = ["lint"] @@ -230,24 +230,24 @@ dependencies = [ "typing-extensions>=4.0.1; python_version < \"3.11\"", ] files = [ - {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, - {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, - {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, - {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, - {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, - {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, - {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, - {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, - {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, - {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, - {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, - {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, - {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, - {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, - {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, - {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, - {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, - {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, + {file = "black-24.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94d5280d020dadfafc75d7cae899609ed38653d3f5e82e7ce58f75e76387ed3d"}, + {file = "black-24.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aaf9aa85aaaa466bf969e7dd259547f4481b712fe7ee14befeecc152c403ee05"}, + {file = "black-24.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec489cae76eac3f7573629955573c3a0e913641cafb9e3bfc87d8ce155ebdb29"}, + {file = "black-24.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5a0100b4bdb3744dd68412c3789f472d822dc058bb3857743342f8d7f93a5a7"}, + {file = "black-24.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6cc5a6ba3e671cfea95a40030b16a98ee7dc2e22b6427a6f3389567ecf1b5262"}, + {file = "black-24.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0e367759062dcabcd9a426d12450c6d61faf1704a352a49055a04c9f9ce8f5a"}, + {file = "black-24.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be305563ff4a2dea813f699daaffac60b977935f3264f66922b1936a5e492ee4"}, + {file = "black-24.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a8977774929b5db90442729f131221e58cc5d8208023c6af9110f26f75b6b20"}, + {file = "black-24.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d74d4d0da276fbe3b95aa1f404182562c28a04402e4ece60cf373d0b902f33a0"}, + {file = "black-24.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39addf23f7070dbc0b5518cdb2018468ac249d7412a669b50ccca18427dba1f3"}, + {file = "black-24.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:827a7c0da520dd2f8e6d7d3595f4591aa62ccccce95b16c0e94bb4066374c4c2"}, + {file = "black-24.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:0cd59d01bf3306ff7e3076dd7f4435fcd2fafe5506a6111cae1138fc7de52382"}, + {file = "black-24.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a15670c650668399c4b5eae32e222728185961d6ef6b568f62c1681d57b381ba"}, + {file = "black-24.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1e0fa70b8464055069864a4733901b31cbdbe1273f63a24d2fa9d726723d45ac"}, + {file = "black-24.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa8d9aaa22d846f8c0f7f07391148e5e346562e9b215794f9101a8339d8b6d8"}, + {file = "black-24.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:f0dfbfbacfbf9cd1fac7a5ddd3e72510ffa93e841a69fcf4a6358feab1685382"}, + {file = "black-24.1.0-py3-none-any.whl", hash = "sha256:5134a6f6b683aa0a5592e3fd61dd3519d8acd953d93e2b8b76f9981245b65594"}, + {file = "black-24.1.0.tar.gz", hash = "sha256:30fbf768cd4f4576598b1db0202413fafea9a227ef808d1a12230c643cefe9fc"}, ] [[package]] @@ -978,7 +978,7 @@ name = "importlib-metadata" version = "7.0.1" requires_python = ">=3.8" summary = "Read metadata from Python packages" -groups = ["doc"] +groups = ["default", "doc"] dependencies = [ "zipp>=0.5", ] diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 0000000..9038375 --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,21 @@ +# Fluidfft plugins + +Directory containing the plugins, i.e. Python packages declaring the +`fluidfft.plugins` entry point. + +We should have + +- [x] fluidfft-mpi4pyfft (cannot be tested because mpi4py-fft installation fails) +- [ ] fluidfft-fftw +- [ ] fluidfft-mpi_with_fftw +- [ ] fluidfft-fftwmpi +- [ ] fluidfft-p3dfft +- [ ] fluidfft-pfft +- [ ] fluidfft-pyvkfft (https://pyvkfft.readthedocs.io) + +Currently, we have only one tested plugin (fluidfft-pyfftw), which was written to +design and test the plugin machinery. However, I (PA) think that this (pure Python) +code will have to go back in fluidfft. Pyfftw can just be an optional dependency +for fluidfft. + +TODO: When we have other plugins, move back the code using pyfftw inside fluidfft. diff --git a/plugins/fluidfft-mpi4pyfft/LICENSE b/plugins/fluidfft-mpi4pyfft/LICENSE new file mode 100644 index 0000000..4687304 --- /dev/null +++ b/plugins/fluidfft-mpi4pyfft/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 Pierre Augier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/fluidfft-mpi4pyfft/pyproject.toml b/plugins/fluidfft-mpi4pyfft/pyproject.toml new file mode 100644 index 0000000..fffce73 --- /dev/null +++ b/plugins/fluidfft-mpi4pyfft/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "fluidfft_mpi4pyfft" +version = "0.0.1" +description = "Fluidfft plugin using mpi4pyfft" +authors = [{name = "Pierre Augier", email = "pierre.augier@univ-grenoble-alpes.fr"}] +license = {file = "LICENSE"} +classifiers = ["License :: OSI Approved :: MIT License"] +dependencies = ["fluidfft", "mpi4py-fft"] + +[project.urls] +Home = "https://fluidfft.readthedocs.io" + +[project.entry-points."fluidfft.plugins"] + +"fft3d.mpi_with_mpi4pyfft" = "fluidfft_mpi4pyfft.mpi_with_mpi4pyfft" +"fft3d.mpi_with_mpi4pyfft_slab" = "fluidfft_mpi4pyfft.fft3d.mpi_with_mpi4pyfft_slab" diff --git a/plugins/fluidfft-mpi4pyfft/src/fluidfft_mpi4pyfft/__init__.py b/plugins/fluidfft-mpi4pyfft/src/fluidfft_mpi4pyfft/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fluidfft/fft3d/mpi_with_mpi4pyfft.py b/plugins/fluidfft-mpi4pyfft/src/fluidfft_mpi4pyfft/mpi_with_mpi4pyfft.py similarity index 99% rename from src/fluidfft/fft3d/mpi_with_mpi4pyfft.py rename to plugins/fluidfft-mpi4pyfft/src/fluidfft_mpi4pyfft/mpi_with_mpi4pyfft.py index 38a4145..e8924ad 100644 --- a/src/fluidfft/fft3d/mpi_with_mpi4pyfft.py +++ b/plugins/fluidfft-mpi4pyfft/src/fluidfft_mpi4pyfft/mpi_with_mpi4pyfft.py @@ -3,7 +3,7 @@ from mpi4py import MPI from mpi4py_fft import PFFT, newDistArray -from .base import BaseFFTMPI +from fluidfft.fft3d.base import BaseFFTMPI class FFT3DMPIWithMPI4PYFFT(BaseFFTMPI): diff --git a/src/fluidfft/fft3d/mpi_with_mpi4pyfft_slab.py b/plugins/fluidfft-mpi4pyfft/src/fluidfft_mpi4pyfft/mpi_with_mpi4pyfft_slab.py similarity index 100% rename from src/fluidfft/fft3d/mpi_with_mpi4pyfft_slab.py rename to plugins/fluidfft-mpi4pyfft/src/fluidfft_mpi4pyfft/mpi_with_mpi4pyfft_slab.py diff --git a/plugins/fluidfft-mpi4pyfft/tests/test_with_mpi4pyfft.py b/plugins/fluidfft-mpi4pyfft/tests/test_with_mpi4pyfft.py new file mode 100644 index 0000000..cbba9a3 --- /dev/null +++ b/plugins/fluidfft-mpi4pyfft/tests/test_with_mpi4pyfft.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from fluidfft.fft3d.testing import complete_test_class_3d + + +class Tests(TestCase): + pass + + +methods = ["fft3d.mpi_with_mpi4pyfft", "fft3d.mpi_with_mpi4pyfft_slab"] +for method in methods: + complete_test_class_3d(method, Tests) diff --git a/plugins/fluidfft-pyfftw/LICENSE b/plugins/fluidfft-pyfftw/LICENSE new file mode 100644 index 0000000..4687304 --- /dev/null +++ b/plugins/fluidfft-pyfftw/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 Pierre Augier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/plugins/fluidfft-pyfftw/pyproject.toml b/plugins/fluidfft-pyfftw/pyproject.toml new file mode 100644 index 0000000..9e48698 --- /dev/null +++ b/plugins/fluidfft-pyfftw/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "fluidfft_pyfftw" +version = "0.0.1" +description = "Fluidfft plugin using pyfftw" +authors = [{name = "Pierre Augier", email = "pierre.augier@univ-grenoble-alpes.fr"}] +license = {file = "LICENSE"} +classifiers = ["License :: OSI Approved :: MIT License"] +dependencies = ["fluidfft", "pyfftw"] + +[project.urls] +Home = "https://foss.heptapod.net/fluiddyn/fluidfft" + +[project.entry-points."fluidfft.plugins"] + +"fft2d.with_pyfftw" = "fluidfft_pyfftw.fft2d" +"fft3d.with_pyfftw" = "fluidfft_pyfftw.fft3d" diff --git a/plugins/fluidfft-pyfftw/src/fluidfft_pyfftw/__init__.py b/plugins/fluidfft-pyfftw/src/fluidfft_pyfftw/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fluidfft/fft2d/with_pyfftw.py b/plugins/fluidfft-pyfftw/src/fluidfft_pyfftw/fft2d.py similarity index 87% rename from src/fluidfft/fft2d/with_pyfftw.py rename to plugins/fluidfft-pyfftw/src/fluidfft_pyfftw/fft2d.py index 8d1b735..1d5376c 100644 --- a/src/fluidfft/fft2d/with_pyfftw.py +++ b/plugins/fluidfft-pyfftw/src/fluidfft_pyfftw/fft2d.py @@ -1,5 +1,5 @@ -"""Class using pyfftw (:mod:`fluidfft.fft2d.with_pyfftw`) -========================================================= +"""Class using pyfftw +===================== .. autoclass:: FFT2DWithPYFFTW :members: diff --git a/src/fluidfft/fft3d/with_pyfftw.py b/plugins/fluidfft-pyfftw/src/fluidfft_pyfftw/fft3d.py similarity index 88% rename from src/fluidfft/fft3d/with_pyfftw.py rename to plugins/fluidfft-pyfftw/src/fluidfft_pyfftw/fft3d.py index 4c37857..f32fed8 100644 --- a/src/fluidfft/fft3d/with_pyfftw.py +++ b/plugins/fluidfft-pyfftw/src/fluidfft_pyfftw/fft3d.py @@ -1,5 +1,5 @@ -"""Class using pyfftw (:mod:`fluidfft.fft3d.with_pyfftw`) -========================================================= +"""Class using pyfftw +===================== .. autoclass:: FFT3DWithPYFFTW :members: diff --git a/plugins/fluidfft-pyfftw/tests/test_with_pyfftw.py b/plugins/fluidfft-pyfftw/tests/test_with_pyfftw.py new file mode 100644 index 0000000..9157695 --- /dev/null +++ b/plugins/fluidfft-pyfftw/tests/test_with_pyfftw.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from fluidfft.fft3d.testing import complete_test_class_3d +from fluidfft.fft2d.testing import complete_test_class_2d + + +class Tests(TestCase): + pass + + +complete_test_class_2d("fft2d.with_pyfftw", Tests) +complete_test_class_3d("fft3d.with_pyfftw", Tests) diff --git a/pyproject.toml b/pyproject.toml index b747aca..aebf0ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ authors = [ dependencies = [ "fluiddyn >= 0.2.3", "transonic >= 0.4", + "importlib_metadata; python_version < '3.10'", ] requires-python = ">= 3.9" readme = "README.md" @@ -52,6 +53,36 @@ mpi = [ fluidfft-bench = "fluidfft.bench:run" fluidfft-bench-analysis = "fluidfft.bench_analysis:run" +[project.entry-points."fluidfft.plugins"] +# TODO: uncomment when code using pyfftw is back in fluidfft +# "fft2d.with_pyfftw" = "fluidfft.fft2d.with_pyfftw" +# "fft3d.with_pyfftw" = "fluidfft.fft3d.with_pyfftw" +"fft2d.with_dask" = "fluidfft.fft2d.with_dask" + +# should be in fluidfft-fftw +"fft2d.with_fftw1d" = "fluidfft.fft2d.with_fftw1d" +"fft2d.with_fftw2d" = "fluidfft.fft2d.with_fftw2d" +"fft3d.with_fftw3d" = "fluidfft.fft3d.with_fftw3d" + +# should be in fluidfft-cuda +# (or better HIP?, see https://foss.heptapod.net/fluiddyn/fluidfft/-/merge_requests/46) +"fft2d.with_cufft" = "fluidfft.fft2d.with_cufft" +"fft3d.with_cufft" = "fluidfft.fft3d.with_cufft" + +# should be in fluidfft-mpi_with_fftw +"fft2d.mpi_with_fftw1d" = "fluidfft.fft2d.mpi_with_fftw1d" +"fft3d.mpi_with_fftw1d" = "fluidfft.fft3d.mpi_with_fftw1d" + +# should be in fluidfft-fftwmpi +"fft2d.mpi_with_fftwmpi2d" = "fluidfft.fft2d.mpi_with_fftwmpi2d" +"fft3d.mpi_with_fftwmpi3d" = "fluidfft.fft3d.mpi_with_fftwmpi3d" + +# should be in fluidfft-p3dfft +"fft3d.mpi_with_p3dfft" = "fluidfft.fft3d.mpi_with_p3dfft" + +# should be in fluidfft-pfft +"fft3d.mpi_with_pfft" = "fluidfft.fft3d.mpi_with_pfft" + [tool.pdm] distribution = true package-dir = "src" @@ -59,6 +90,10 @@ package-dir = "src" [tool.pdm.dev-dependencies] build = ["setuptools", "transonic", "pythran", "wheel", "jinja2", "cython"] +# plugins = [ +# "-e fluidfft-mpi4pyfft @ file:///${PROJECT_ROOT}/plugins/fluidfft-mpi4pyfft", +# ] + test = [ "pytest", "coverage", @@ -82,7 +117,7 @@ doc = [ lint = ["black", "pylint"] [tool.pdm.scripts] -black = 'black -l 82 src doc src_cy --exclude "/(__pythran__|__python__|__numba__|doc/_build|\.ipynb_checkpoints/*)/"' -lint = {shell="pylint -rn --rcfile=pylintrc --jobs=$(nproc) src --exit-zero"} -black_check = 'black --check -l 82 src doc src_cy --exclude "/(__pythran__|__python__|__numba__|doc/_build|\.ipynb_checkpoints/*)/"' +black = 'black -l 82 src doc src_cy tests --exclude "/(__pythran__|__python__|__numba__|doc/_build|\.ipynb_checkpoints/*)/"' +black_check = 'black --check -l 82 src doc src_cy tests --exclude "/(__pythran__|__python__|__numba__|doc/_build|\.ipynb_checkpoints/*)/"' +lint = {shell="pylint -rn --rcfile=pylintrc --jobs=$(nproc) src doc tests plugins --exit-zero"} validate_code = {composite = ["black_check", "lint"]} diff --git a/src/fluidfft/__init__.py b/src/fluidfft/__init__.py index 4f5b39a..447d4b1 100644 --- a/src/fluidfft/__init__.py +++ b/src/fluidfft/__init__.py @@ -32,9 +32,14 @@ import os import subprocess import sys +import logging -from fluiddyn.util import mpi +if sys.version_info < (3, 10): + from importlib_metadata import entry_points, EntryPoint +else: + from importlib.metadata import entry_points, EntryPoint +from fluiddyn.util import mpi from fluidfft._version import __version__ @@ -76,16 +81,135 @@ def byte_align(values, *args): __all__ = [ + "__citation__", "__version__", - "import_fft_class", + "byte_align", "create_fft_object", "empty_aligned", - "byte_align", - "__citation__", + "get_module_fullname_from_method", + "get_plugins", + "get_methods", + "import_fft_class", ] -def import_fft_class(method, raise_import_error=True): +_plugins = None + + +def get_plugins(reload=False, ndim=None, sequential=None): + """Discover the fluidfft plugins installed""" + global _plugins + if _plugins is None or reload: + _plugins = entry_points(group="fluidfft.plugins") + + if not _plugins: + raise RuntimeError("No Fluidfft plugins were found.") + + if ndim is None and sequential is None: + return _plugins + + if ndim is None: + index = 6 + prefix = "" + elif ndim in (2, 3): + index = 0 + prefix = f"fft{ndim}d." + else: + raise ValueError(f"Unsupported value for {ndim = }") + + if sequential is not None and not sequential: + prefix += "mpi_" + elif sequential: + prefix += "with_" + + return tuple( + plugin for plugin in _plugins if plugin.name[index:].startswith(prefix) + ) + + +def get_methods(ndim=None, sequential=None): + """Get available methods""" + plugins = get_plugins(ndim=ndim, sequential=sequential) + return set(plug.name for plug in plugins) + + +def get_module_fullname_from_method(method): + """Get the module name from a method string + + Parameters + ---------- + + method : str + Name of module or string characterizing a method. + + """ + plugins = get_plugins() + selected_plugins = plugins.select(name=method) + if len(selected_plugins) == 0: + raise ValueError( + f"Cannot find a fluidfft plugin for {method = }. {plugins}" + ) + elif len(selected_plugins) > 1: + logging.warning( + f"{len(selected_plugins)} plugins were found for {method = }" + ) + + return selected_plugins[method].value + + +def _normalize_method_name(method): + """Normalize a method name""" + if method == "sequential": + method = "fft2d.with_fftw2d" + elif method.startswith("fluidfft:"): + method = method.removeprefix("fluidfft:") + return method + + +def _check_failure(method): + """Check if a tiny fft maker can be created""" + + if not any(method.endswith(postfix) for postfix in ("pfft", "p3dfft")): + return False + + # for few methods, try before real import because importing can lead to + # a fatal error (Illegal instruction) + if mpi.rank == 0: + if mpi.nb_proc > 1: + # We need to filter out the MPI environment variables. + # Fragile because it is specific to MPI implementations + env = { + key: value + for key, value in os.environ.items() + if not ("MPI" in key or key.startswith("PMI_")) + } + else: + env = os.environ + try: + # TODO: capture stdout and stderr and include last line in case of failure + subprocess.check_call( + [ + sys.executable, + "-c", + f"from fluidfft import create_fft_object as c; c('{method}', 2, 2, 2, check=False)", + ], + env=env, + shell=False, + ) + failure = False + except subprocess.CalledProcessError: + failure = True + + else: + failure = None + + if mpi.nb_proc > 1: + failure = mpi.comm.bcast(failure, root=0) + + return failure + + +def import_fft_class(method, raise_import_error=True, check=True): """Import a fft class. Parameters @@ -107,67 +231,44 @@ def import_fft_class(method, raise_import_error=True): The corresponding FFT class. """ - if method == "sequential": - method = "fft2d.with_fftw2d" - - if method.startswith("fft2d.") or method.startswith("fft3d."): - method = "fluidfft." + method - - if not method.startswith("fluidfft."): - raise ValueError( - "not method.startswith('fluidfft.')\nmethod = {}".format(method) - ) - - if any(method.endswith(postfix) for postfix in ("pfft", "p3dfft")): - # for few methods, try before real import because importing can lead to - # a fatal error (Illegal instruction) - if mpi.rank == 0: - if mpi.nb_proc > 1: - # We need to filter out the MPI environment variables. - # Fragile because it is specific to MPI implementations - env = { - key: value - for key, value in os.environ.items() - if not ("MPI" in key or key.startswith("PMI_")) - } - else: - env = os.environ - try: - subprocess.check_call( - [sys.executable, "-c", "import " + method], - env=env, - shell=False, - ) - failure = False - except subprocess.CalledProcessError: - failure = True - else: - failure = None - - if mpi.nb_proc > 1: - failure = mpi.comm.bcast(failure, root=0) + if isinstance(method, EntryPoint): + module_fullname = method.value + method = method.name + else: + method = _normalize_method_name(method) + module_fullname = get_module_fullname_from_method(method) + if check: + failure = _check_failure(method) if failure: if not raise_import_error: - mpi.printby0("ImportError:", method) + mpi.printby0("ImportError during check:", module_fullname) return None else: - raise ImportError(method) + raise ImportError(module_fullname) try: - mod = importlib.import_module(method) + mod = importlib.import_module(module_fullname) except ImportError: if raise_import_error: raise - mpi.printby0("ImportError:", method) + mpi.printby0("ImportError:", module_fullname) return None return mod.FFTclass -def create_fft_object(method, n0, n1, n2=None): +def _get_classes(ndim, sequential): + plugins = get_plugins(ndim=ndim, sequential=sequential) + return { + plugin.name: import_fft_class(plugin, raise_import_error=False) + for plugin in plugins + } + + +def create_fft_object(method, n0, n1, n2=None, check=True): """Helper for creating fft objects. Parameters @@ -189,15 +290,15 @@ def create_fft_object(method, n0, n1, n2=None): """ - cls = import_fft_class(method) + cls = import_fft_class(method, check=check) str_module = cls.__module__ - if n2 is None and str_module.startswith("fluidfft.fft3d."): + if n2 is None and "fft3d" in str_module: raise ValueError("Arguments incompatible") - elif n2 is not None and str_module.startswith("fluidfft.fft2d."): - raise ValueError("Arguments incompatible") + if n2 is not None and "fft2d" in str_module: + n2 = None if n2 is None: return cls(n0, n1) diff --git a/src/fluidfft/base.py b/src/fluidfft/base.py index 9f5cf58..02f56a4 100644 --- a/src/fluidfft/base.py +++ b/src/fluidfft/base.py @@ -7,6 +7,7 @@ :undoc-members: """ + import numpy as np import fluidfft diff --git a/src/fluidfft/bench_analysis.py b/src/fluidfft/bench_analysis.py index 3c0c026..0514df7 100644 --- a/src/fluidfft/bench_analysis.py +++ b/src/fluidfft/bench_analysis.py @@ -2,6 +2,7 @@ ============================================================ """ + from glob import glob import json from copy import copy diff --git a/src/fluidfft/fft2d/__init__.py b/src/fluidfft/fft2d/__init__.py index 77f5bcd..f269cb0 100644 --- a/src/fluidfft/fft2d/__init__.py +++ b/src/fluidfft/fft2d/__init__.py @@ -32,12 +32,12 @@ class :class:`fluidfft.fft2d.operators.OperatorsPseudoSpectral2D` defined in """ -from .. import import_fft_class +import sys + +from .. import _get_classes __all__ = [ "FFT2dFakeForDoc", - "methods_seq", - "methods_mpi", "get_classes_seq", "get_classes_mpi", ] @@ -47,24 +47,18 @@ class :class:`fluidfft.fft2d.operators.OperatorsPseudoSpectral2D` defined in except ImportError: pass -methods_seq = ["fftw1d", "fftw2d", "cufft", "pyfftw"] # "dask"] -methods_seq = ["fft2d.with_" + method for method in methods_seq] - -methods_mpi = ["fftwmpi2d", "fftw1d"] -methods_mpi = ["fft2d.mpi_with_" + method for method in methods_mpi] - def get_classes_seq(): """Return all sequential 2d classes.""" - return { - method: import_fft_class(method, raise_import_error=False) - for method in methods_seq - } + return _get_classes(2, sequential=True) def get_classes_mpi(): """Return all parallel 2d classes.""" - return { - method: import_fft_class(method, raise_import_error=False) - for method in methods_mpi - } + return _get_classes(2, sequential=False) + + +if any("pytest" in part for part in sys.argv): + import pytest + + pytest.register_assert_rewrite("fluidfft.fft2d.testing") diff --git a/src/fluidfft/fft2d/test_2d.py b/src/fluidfft/fft2d/testing.py similarity index 75% rename from src/fluidfft/fft2d/test_2d.py rename to src/fluidfft/fft2d/testing.py index 762ff36..10c0f79 100644 --- a/src/fluidfft/fft2d/test_2d.py +++ b/src/fluidfft/fft2d/testing.py @@ -1,41 +1,20 @@ -import unittest from math import pi -import traceback import numpy as np from fluiddyn.util import mpi -from fluidfft.fft2d import get_classes_seq, get_classes_mpi +from fluidfft import import_fft_class from fluidfft.fft2d.operators import OperatorsPseudoSpectral2D -try: - import fluidfft.fft2d.with_fftw2d -except ImportError: - # If this one does not work it is a bad sign so we want to know what happened. - traceback.print_exc() - - -n = 24 rank = mpi.rank nb_proc = mpi.nb_proc -classes_seq = get_classes_seq() -classes_seq = {name: cls for name, cls in classes_seq.items() if cls is not None} - -if not classes_seq: - raise ImportError("Not sequential 2d classes working!") - -if nb_proc > 1: - classes_mpi = get_classes_mpi() - classes_mpi = { - name: cls for name, cls in classes_mpi.items() if cls is not None - } - def make_test_function(cls): def test(self): + n = 24 o = cls(n, 2 * n) o.run_tests() a = np.random.rand(*o.get_shapeX_loc()) @@ -166,42 +145,15 @@ def test(self, n0=n0, n1=n1): return tests -class Tests2D(unittest.TestCase): - pass +def complete_test_class_2d(method, test_class, cls=None): + if cls is None: + cls = import_fft_class(method) + short_name = method.split(".")[1] -def complete_class(name, cls, Tests2D=Tests2D): - if cls is not None: - setattr(Tests2D, "test_{}".format(name), make_test_function(cls)) + setattr(test_class, f"test_{short_name}", make_test_function(cls)) - tests = make_testop_functions(name, cls) + tests = make_testop_functions(method, cls) for key, test in tests.items(): - setattr(Tests2D, "test_operator2d_{}_{}".format(name, key), test) - - -if rank == 0: - if nb_proc == 1 and len(classes_seq) == 0: - raise Exception( - "ImportError for all sequential classes. Nothing is working!" - ) - - for name, cls in classes_seq.items(): - complete_class(name, cls) - - -if nb_proc > 1: - if len(classes_mpi) == 0: - raise Exception( - "ImportError for all mpi classes. Nothing is working in mpi!" - ) - - for name, cls in classes_mpi.items(): - complete_class(name, cls) - - -complete_class("None", None) - - -if __name__ == "__main__": - unittest.main() + setattr(test_class, f"test_operator2d_{short_name}_{key}", test) diff --git a/src/fluidfft/fft2d/with_dask.py b/src/fluidfft/fft2d/with_dask.py index 7e09e69..65e2ed0 100644 --- a/src/fluidfft/fft2d/with_dask.py +++ b/src/fluidfft/fft2d/with_dask.py @@ -7,6 +7,7 @@ TODO: Find a mechanism for setting chunksize """ + import warnings from typing import Union diff --git a/src/fluidfft/fft3d/__init__.py b/src/fluidfft/fft3d/__init__.py index 6980a0e..fa61786 100644 --- a/src/fluidfft/fft3d/__init__.py +++ b/src/fluidfft/fft3d/__init__.py @@ -35,12 +35,12 @@ class :class:`fluidfft.fft3d.operators.OperatorsPseudoSpectral3D` defined in """ -from .. import import_fft_class +import sys + +from .. import _get_classes __all__ = [ "FFT3dFakeForDoc", - "methods_seq", - "methods_mpi", "get_classes_seq", "get_classes_mpi", ] @@ -50,31 +50,18 @@ class :class:`fluidfft.fft3d.operators.OperatorsPseudoSpectral3D` defined in except ImportError: pass -methods_seq = ["fftw3d", "pyfftw"] -methods_seq = ["fft3d.with_" + method for method in methods_seq] - -methods_mpi = [ - "fftw1d", - "fftwmpi3d", - "p3dfft", - "pfft", - "mpi4pyfft", - "mpi4pyfft_slab", -] -methods_mpi = ["fft3d.mpi_with_" + method for method in methods_mpi] - def get_classes_seq(): """Return all sequential 3d classes.""" - return { - method: import_fft_class(method, raise_import_error=False) - for method in methods_seq - } + return _get_classes(3, sequential=True) def get_classes_mpi(): """Return all parallel 3d classes.""" - return { - method: import_fft_class(method, raise_import_error=False) - for method in methods_mpi - } + return _get_classes(3, sequential=False) + + +if any("pytest" in part for part in sys.argv): + import pytest + + pytest.register_assert_rewrite("fluidfft.fft3d.testing") diff --git a/src/fluidfft/fft3d/test_3d.py b/src/fluidfft/fft3d/testing.py similarity index 76% rename from src/fluidfft/fft3d/test_3d.py rename to src/fluidfft/fft3d/testing.py index 7df7f9b..f0e050f 100644 --- a/src/fluidfft/fft3d/test_3d.py +++ b/src/fluidfft/fft3d/testing.py @@ -1,40 +1,24 @@ -import unittest -import traceback - import numpy as np -from fluiddyn.util import mpi - -from fluidfft.fft3d import get_classes_seq, get_classes_mpi -from fluidfft.fft3d.operators import OperatorsPseudoSpectral3D, vector_product - - -try: - import fluidfft.fft3d.with_fftw3d -except ImportError: - # If this one does not work it is a bad sign so we want to know what appends. - traceback.print_exc() - +from fluidfft import import_fft_class +from .operators import OperatorsPseudoSpectral3D, vector_product -n = 8 +from fluiddyn.util import mpi rank = mpi.rank nb_proc = mpi.nb_proc -classes_seq = get_classes_seq() -classes_seq = {name: cls for name, cls in classes_seq.items() if cls is not None} - -if not classes_seq: - raise ImportError("Not sequential 2d classes working!") -if nb_proc > 1: - classes_mpi = get_classes_mpi() - classes_mpi = { - name: cls for name, cls in classes_mpi.items() if cls is not None - } +def complete_test_class_3d(method, test_class, cls=None): + short_name = method.split(".")[1] + if cls is None: + cls = import_fft_class(method) + tests = make_testop_functions(cls) + for key, test in tests.items(): + setattr(test_class, f"test_operator3d_{short_name}_{key}", test) -def make_testop_functions(name, cls): +def make_testop_functions(cls): tests = {} shapes = {"even": (4, 8, 12)} if nb_proc == 1: @@ -165,40 +149,3 @@ def test(self, n0=n0, n1=n1, n2=n2): tests[key] = test return tests - - -class Tests3D(unittest.TestCase): - pass - - -def complete_class(name, cls): - tests = make_testop_functions(name, cls) - - for key, test in tests.items(): - setattr(Tests3D, "test_operator3d_{}_{}".format(name, key), test) - - -if nb_proc == 1: - if nb_proc == 1 and len(classes_seq) == 0: - raise Exception( - "ImportError for all sequential classes. Nothing is working!" - ) - - for name, cls in classes_seq.items(): - complete_class(name, cls) - - complete_class("None", None) - - -if nb_proc > 1: - if len(classes_mpi) == 0: - raise Exception( - "ImportError for all mpi classes. Nothing is working in mpi!" - ) - - for name, cls in classes_mpi.items(): - complete_class(name, cls) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/fluidfft/util.py b/src/fluidfft/util.py index 8cf9297..4159b77 100644 --- a/src/fluidfft/util.py +++ b/src/fluidfft/util.py @@ -2,6 +2,7 @@ ========================= """ + # import inspect as _inspect # we need this try because this file is executed during the build when we don't diff --git a/tests/test_2d.py b/tests/test_2d.py new file mode 100644 index 0000000..10ff8e3 --- /dev/null +++ b/tests/test_2d.py @@ -0,0 +1,75 @@ +import unittest +import traceback + +from fluiddyn.util import mpi + +from fluidfft import import_fft_class +from fluidfft.fft2d import get_classes_seq, get_classes_mpi +from fluidfft.fft2d.testing import complete_test_class_2d + +try: + import fluidfft.fft2d.with_fftw2d +except ImportError: + # If this one does not work it is a bad sign so we want to know what happened. + traceback.print_exc() + + +def test_get_classes(): + get_classes_seq() + get_classes_mpi() + + +rank = mpi.rank +nb_proc = mpi.nb_proc + + +methods_seq = ["fftw1d", "fftw2d", "cufft", "pyfftw"] +methods_seq = ["fft2d.with_" + method for method in methods_seq] +classes_seq = { + method: import_fft_class(method, raise_import_error=False) + for method in methods_seq +} +classes_seq = { + method: cls for method, cls in classes_seq.items() if cls is not None +} + +if not classes_seq: + raise ImportError("Not sequential 2d classes working!") + +if nb_proc > 1: + methods_mpi = ["fftwmpi2d", "fftw1d"] + methods_mpi = ["fft2d.mpi_with_" + method for method in methods_mpi] + classes_mpi = { + method: import_fft_class(method, raise_import_error=False) + for method in methods_mpi + } + classes_mpi = { + method: cls for method, cls in classes_mpi.items() if cls is not None + } + + +class Tests2D(unittest.TestCase): + pass + + +if rank == 0: + if nb_proc == 1 and len(classes_seq) == 0: + raise RuntimeError( + "ImportError for all sequential classes. Nothing is working!" + ) + + for method, cls in classes_seq.items(): + complete_test_class_2d(method, Tests2D, cls=cls) + + +if nb_proc > 1: + if len(classes_mpi) == 0: + raise RuntimeError( + "ImportError for all mpi classes. Nothing is working in mpi!" + ) + + for method, cls in classes_mpi.items(): + complete_test_class_2d(method, Tests2D, cls=cls) + +# TODO: understand what was done here before! +# complete_test_class_2d("None", Tests2D, cls=False) diff --git a/tests/test_3d.py b/tests/test_3d.py new file mode 100644 index 0000000..fc9c81a --- /dev/null +++ b/tests/test_3d.py @@ -0,0 +1,72 @@ +import unittest +import traceback + +from fluiddyn.util import mpi + +from fluidfft import import_fft_class +from fluidfft.fft3d import get_classes_seq, get_classes_mpi +from fluidfft.fft3d.testing import complete_test_class_3d + +try: + import fluidfft.fft3d.with_fftw3d +except ImportError: + # If this one does not work it is a bad sign so we want to know what appends. + traceback.print_exc() + + +def test_get_classes(): + get_classes_seq() + get_classes_mpi() + + +methods_seq = ["fftw3d", "pyfftw"] +methods_seq = ["fft3d.with_" + method for method in methods_seq] +classes_seq = { + method: import_fft_class(method, raise_import_error=False) + for method in methods_seq +} +classes_seq = { + method: cls for method, cls in classes_seq.items() if cls is not None +} +if not classes_seq: + raise ImportError("Not sequential 2d classes working!") + +methods_mpi = ["fftw1d", "fftwmpi3d", "p3dfft", "pfft"] +methods_mpi = ["fft3d.mpi_with_" + method for method in methods_mpi] + +nb_proc = mpi.nb_proc +if nb_proc > 1: + classes_mpi = { + method: import_fft_class(method, raise_import_error=False) + for method in methods_mpi + } + classes_mpi = { + method: cls for method, cls in classes_mpi.items() if cls is not None + } + + +class Tests3D(unittest.TestCase): + pass + + +if nb_proc == 1: + if len(classes_seq) == 0: + raise RuntimeError( + "ImportError for all sequential classes. Nothing is working!" + ) + + for method, cls in classes_seq.items(): + complete_test_class_3d(method, Tests3D, cls=cls) + + # TODO: understand what was done here before! + # complete_class("None", None) + + +if nb_proc > 1: + if len(classes_mpi) == 0: + raise RuntimeError( + "ImportError for all mpi classes. Nothing is working in mpi!" + ) + + for method, cls in classes_mpi.items(): + complete_test_class_3d(method, Tests3D, cls=cls) diff --git a/src/fluidfft/test_bench.py b/tests/test_bench.py similarity index 93% rename from src/fluidfft/test_bench.py rename to tests/test_bench.py index a828374..2febbb9 100644 --- a/src/fluidfft/test_bench.py +++ b/tests/test_bench.py @@ -8,7 +8,7 @@ try: import pandas - from . import bench_analysis + from fluidfft import bench_analysis use_pandas = True except ImportError: @@ -18,7 +18,7 @@ from fluiddyn.util import mpi -from .bench import bench_all, run +from fluidfft.bench import bench_all, run path_tmp = "/tmp/fluidfft_test_bench" + getpass.getuser() diff --git a/src/fluidfft/test_init.py b/tests/test_init.py similarity index 96% rename from src/fluidfft/test_init.py rename to tests/test_init.py index ee5c567..50874f7 100644 --- a/src/fluidfft/test_init.py +++ b/tests/test_init.py @@ -18,7 +18,7 @@ from fluiddyn.util.mpi import rank -from . import create_fft_object +from fluidfft import create_fft_object class TestsCreateFFTObject(unittest.TestCase): diff --git a/tests/test_plugins.py b/tests/test_plugins.py new file mode 100644 index 0000000..a2603a7 --- /dev/null +++ b/tests/test_plugins.py @@ -0,0 +1,49 @@ +from fluidfft import get_plugins, get_methods + + +methodss = { + (2, True): set( + [ + "fft2d.with_fftw1d", + "fft2d.with_fftw2d", + "fft2d.with_cufft", + "fft2d.with_pyfftw", + "fft2d.with_dask", + ] + ), + (2, False): set( + [ + "fft2d.mpi_with_fftw1d", + "fft2d.mpi_with_fftwmpi2d", + ] + ), + (3, True): set( + [ + "fft3d.with_fftw3d", + "fft3d.with_pyfftw", + "fft3d.with_cufft", + ] + ), + (3, False): set( + [ + "fft3d.mpi_with_fftw1d", + "fft3d.mpi_with_fftwmpi3d", + "fft3d.mpi_with_p3dfft", + "fft3d.mpi_with_pfft", + ] + ), +} + + +def test_plugins(): + plugins = get_plugins() + assert plugins + + for ndim in (2, 3): + assert get_methods(ndim=ndim) == methodss[(ndim, True)].union( + methodss[(ndim, False)] + ) + for sequential in (True, False): + assert methodss[(ndim, sequential)] == get_methods( + ndim=ndim, sequential=sequential + )