├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── autopush.yml │ └── ci.yml ├── .gitignore ├── .gitlab-ci.yml ├── .pylintrc-local.yml ├── .test-conda-env-py3.yml ├── CITATION.cff ├── MEMO ├── README.rst ├── asv.conf.json ├── benchmarks ├── __init__.py └── bench_translations.py ├── contrib └── translations │ ├── PDE-reduction and translations.ipynb │ └── PDE-reduction-symbolic.ipynb ├── doc ├── Makefile ├── codegen.rst ├── conf.py ├── eval.rst ├── expansion.rst ├── index.rst ├── interactions.rst ├── kernel.rst ├── misc.rst └── upload-docs.sh ├── examples ├── curve-pot.py ├── curve.py ├── expansion-toys.py ├── fourier.py └── sym-exp-complexity.py ├── notes ├── expansion-notes.pdf ├── expansion-notes.tex ├── latexmkrc └── media │ ├── graf.eps │ ├── graf.ipe │ ├── l-expn.eps │ ├── l-expn.ipe │ ├── local-expansion.ipe │ ├── m-expn.eps │ ├── m-expn.ipe │ ├── m2l-translation.eps │ ├── m2l-translation.ipe │ ├── m2m-l2l-translation.eps │ ├── m2m-l2l-translation.ipe │ ├── m2m-translation.eps │ ├── m2m-translation.ipe │ └── multipole.ipe ├── pyproject.toml ├── requirements.txt ├── sumpy ├── __init__.py ├── array_context.py ├── assignment_collection.py ├── codegen.py ├── cse.py ├── derivative_taker.py ├── distributed.py ├── e2e.py ├── e2p.py ├── expansion │ ├── __init__.py │ ├── diff_op.py │ ├── level_to_order.py │ ├── local.py │ ├── loopy.py │ ├── m2l.py │ └── multipole.py ├── fmm.py ├── kernel.py ├── p2e.py ├── p2p.py ├── point_calculus.py ├── qbx.py ├── symbolic.py ├── tools.py ├── toys.py ├── version.py └── visualization.py └── test ├── test_codegen.py ├── test_cse.py ├── test_distributed.py ├── test_fmm.py ├── test_kernels.py ├── test_matrixgen.py ├── test_misc.py ├── test_qbx.py └── test_tools.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | # https://github.com/editorconfig/editorconfig-vim 3 | # https://github.com/editorconfig/editorconfig-emacs 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.py] 15 | indent_size = 4 16 | 17 | [*.rst] 18 | indent_size = 4 19 | 20 | [*.cpp] 21 | indent_size = 2 22 | 23 | [*.hpp] 24 | indent_size = 2 25 | 26 | # There may be one in doc/ 27 | [Makefile] 28 | indent_style = tab 29 | 30 | # https://github.com/microsoft/vscode/issues/1679 31 | [*.md] 32 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Set update schedule for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | 9 | # vim: sw=4 10 | -------------------------------------------------------------------------------- /.github/workflows/autopush.yml: -------------------------------------------------------------------------------- 1 | name: Gitlab mirror 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | autopush: 9 | name: Automatic push to gitlab.tiker.net 10 | if: startsWith(github.repository, 'inducer/') 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - run: | 15 | curl -L -O https://tiker.net/ci-support-v0 16 | . ./ci-support-v0 17 | mirror_github_to_gitlab 18 | 19 | env: 20 | GITLAB_AUTOPUSH_KEY: ${{ secrets.GITLAB_AUTOPUSH_KEY }} 21 | 22 | # vim: sw=4 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | schedule: 8 | - cron: '17 3 * * 0' 9 | 10 | jobs: 11 | ruff: 12 | name: Ruff 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: astral-sh/setup-uv@v6 17 | - name: "Main Script" 18 | run: | 19 | uv run --only-group lint ruff check 20 | 21 | typos: 22 | name: Typos 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: crate-ci/typos@master 27 | 28 | mypy: 29 | name: Mypy 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - 34 | uses: actions/setup-python@v5 35 | with: 36 | python-version: '3.x' 37 | - name: "Main Script" 38 | run: | 39 | curl -L -O https://tiker.net/ci-support-v0 40 | . ./ci-support-v0 41 | build_py_project_in_conda_env 42 | python -m pip install mypy 43 | mypy $(get_proj_name) 44 | 45 | pylint: 46 | name: Pylint 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | - name: "Main Script" 51 | run: | 52 | USE_CONDA_BUILD=1 53 | EXTRA_INSTALL="pyvisfile scipy matplotlib" 54 | curl -L -O https://tiker.net/ci-support-v0 55 | . ci-support-v0 56 | build_py_project 57 | run_pylint "$(basename $GITHUB_REPOSITORY)" examples/*.py test/*.py 58 | 59 | docs: 60 | name: Documentation 61 | runs-on: ubuntu-latest 62 | steps: 63 | - uses: actions/checkout@v4 64 | - name: "Main Script" 65 | run: | 66 | CONDA_ENVIRONMENT=.test-conda-env-py3.yml 67 | curl -L -O https://tiker.net/ci-support-v0 68 | . ci-support-v0 69 | build_py_project_in_conda_env 70 | build_docs 71 | 72 | pytest: 73 | name: Conda Pytest 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: actions/checkout@v4 77 | - name: "Main Script" 78 | run: | 79 | grep -v symengine .test-conda-env-py3.yml > .test-conda-env.yml 80 | CONDA_ENVIRONMENT=.test-conda-env.yml 81 | curl -L -O https://gitlab.tiker.net/inducer/ci-support/raw/main/build-and-test-py-project-within-miniconda.sh 82 | . ./build-and-test-py-project-within-miniconda.sh 83 | 84 | pytest_symengine: 85 | name: Conda Pytest Symengine 86 | runs-on: ubuntu-latest 87 | steps: 88 | - uses: actions/checkout@v4 89 | - name: "Main Script" 90 | run: | 91 | curl -L -O https://gitlab.tiker.net/inducer/ci-support/raw/main/build-and-test-py-project-within-miniconda.sh 92 | . ./build-and-test-py-project-within-miniconda.sh 93 | 94 | examples: 95 | name: Conda Examples 96 | runs-on: ubuntu-latest 97 | steps: 98 | - uses: actions/checkout@v4 99 | - name: "Main Script" 100 | run: | 101 | grep -v symengine .test-conda-env-py3.yml > .test-conda-env.yml 102 | CONDA_ENVIRONMENT=.test-conda-env.yml 103 | curl -L -O https://tiker.net/ci-support-v0 104 | . ci-support-v0 105 | EXTRA_INSTALL="pyvisfile scipy" 106 | build_py_project_in_conda_env 107 | run_examples 108 | 109 | downstream_tests: 110 | strategy: 111 | matrix: 112 | downstream_project: [pytential] 113 | name: Tests for downstream project ${{ matrix.downstream_project }} 114 | runs-on: ubuntu-latest 115 | steps: 116 | - uses: actions/checkout@v4 117 | - name: "Main Script" 118 | env: 119 | DOWNSTREAM_PROJECT: ${{ matrix.downstream_project }} 120 | run: | 121 | curl -L -O https://tiker.net/ci-support-v0 122 | . ./ci-support-v0 123 | if [[ "$DOWNSTREAM_PROJECT" == "pytential" && "$GITHUB_HEAD_REF" == "e2p" ]]; then 124 | DOWNSTREAM_PROJECT=https://github.com/isuruf/pytential.git@e2p 125 | fi 126 | test_downstream "$DOWNSTREAM_PROJECT" 127 | 128 | # vim: sw=4 129 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .*.sw[po] 3 | .sw[po] 4 | *~ 5 | *.pyc 6 | *.pyo 7 | *.egg-info 8 | MANIFEST 9 | dist 10 | setuptools*egg 11 | setuptools.pth 12 | distribute*egg 13 | distribute*tar.gz 14 | a.out 15 | doc/_build 16 | .cache 17 | .DS_Store 18 | .ipynb_checkpoints 19 | .pytest_cache 20 | 21 | sumpy/_git_rev.py 22 | .asv 23 | 24 | *.vts 25 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # Sumpy thus far is poorly parallelized (no workgroup-level parallelism), and 2 | # the Kepler has a tendency to hang as a result. 3 | # 4 | # Python 3.5 K40: 5 | # script: 6 | # - export PY_EXE=python3.5 7 | # - export PYOPENCL_TEST=nvi:k40 8 | # - export EXTRA_INSTALL="numpy mako" 9 | # - curl -L -O https://gitlab.tiker.net/inducer/ci-support/raw/main/build-and-test-py-project.sh 10 | # - ". ./build-and-test-py-project.sh" 11 | # tags: 12 | # - python3.5 13 | # - nvidia-k40 14 | # except: 15 | # - tags 16 | # artifacts: 17 | # reports: 18 | # junit: test/pytest.xml 19 | 20 | stages: 21 | - test 22 | - deploy 23 | 24 | Pytest POCL: 25 | stage: test 26 | script: 27 | - export PY_EXE=python3 28 | - export PYOPENCL_TEST=portable:pthread 29 | - export EXTRA_INSTALL="pybind11 numpy mako mpi4py" 30 | - curl -L -O https://gitlab.tiker.net/inducer/ci-support/raw/main/build-and-test-py-project.sh 31 | - ". ./build-and-test-py-project.sh" 32 | tags: 33 | - python3 34 | - pocl 35 | except: 36 | - tags 37 | artifacts: 38 | reports: 39 | junit: test/pytest.xml 40 | 41 | Pytest Titan V: 42 | stage: test 43 | script: 44 | - py_version=3 45 | - export PYOPENCL_TEST=nvi:titan 46 | - EXTRA_INSTALL="pybind11 numpy mako mpi4py" 47 | - curl -L -O https://gitlab.tiker.net/inducer/ci-support/raw/main/build-and-test-py-project.sh 48 | - ". ./build-and-test-py-project.sh" 49 | tags: 50 | - python3 51 | - nvidia-titan-v 52 | except: 53 | - tags 54 | allow_failure: True 55 | artifacts: 56 | reports: 57 | junit: test/pytest.xml 58 | 59 | Pytest Conda: 60 | stage: test 61 | script: 62 | # Disable caching to ensure SymEngine code generation is exercised. 63 | - export SUMPY_NO_CACHE=1 64 | - export SUMPY_FORCE_SYMBOLIC_BACKEND=symengine 65 | - export PYOPENCL_TEST=portable:pthread 66 | - curl -L -O https://gitlab.tiker.net/inducer/ci-support/raw/main/build-and-test-py-project-within-miniconda.sh 67 | - ". ./build-and-test-py-project-within-miniconda.sh" 68 | tags: 69 | - large-node 70 | except: 71 | - tags 72 | artifacts: 73 | reports: 74 | junit: test/pytest.xml 75 | 76 | Pytest POCL Titan V: 77 | stage: test 78 | script: 79 | # Disable caching to ensure SymEngine code generation is exercised. 80 | - export SUMPY_NO_CACHE=1 81 | - export SUMPY_FORCE_SYMBOLIC_BACKEND=symengine 82 | - export PYOPENCL_TEST=portable:titan 83 | - curl -L -O https://gitlab.tiker.net/inducer/ci-support/raw/main/build-and-test-py-project-within-miniconda.sh 84 | - ". ./build-and-test-py-project-within-miniconda.sh" 85 | tags: 86 | - nvidia-titan-v 87 | except: 88 | - tags 89 | artifacts: 90 | reports: 91 | junit: test/pytest.xml 92 | 93 | Examples Conda: 94 | stage: test 95 | script: | 96 | grep -v symengine .test-conda-env-py3.yml > .test-conda-env.yml 97 | CONDA_ENVIRONMENT=.test-conda-env.yml 98 | curl -L -O https://tiker.net/ci-support-v0 99 | . ci-support-v0 100 | EXTRA_INSTALL="pyvisfile scipy" 101 | build_py_project_in_conda_env 102 | run_examples 103 | tags: 104 | - large-node 105 | except: 106 | - tags 107 | 108 | Documentation: 109 | stage: deploy 110 | script: | 111 | EXTRA_INSTALL="pybind11 numpy mako" 112 | curl -L -O https://tiker.net/ci-support-v0 113 | . ci-support-v0 114 | build_py_project_in_venv 115 | build_docs 116 | build_asv_html 117 | maybe_upload_docs 118 | tags: 119 | - linux 120 | 121 | Ruff: 122 | stage: test 123 | script: 124 | - pipx install uv 125 | - uv run --only-group lint ruff check 126 | tags: 127 | - docker-runner 128 | except: 129 | - tags 130 | 131 | Pylint: 132 | script: 133 | - EXTRA_INSTALL="pybind11 numpy mako scipy matplotlib pyvisfile mpi4py" 134 | - curl -L -O https://tiker.net/ci-support-v0 135 | - . ci-support-v0 136 | - build_py_project 137 | - run_pylint "$(get_proj_name)" examples/*.py test/*.py 138 | tags: 139 | - python3 140 | except: 141 | - tags 142 | 143 | Mypy: 144 | script: | 145 | curl -L -O https://tiker.net/ci-support-v0 146 | . ./ci-support-v0 147 | build_py_project_in_venv 148 | python -m pip install mypy 149 | mypy $(get_proj_name) 150 | tags: 151 | - python3 152 | except: 153 | - tags 154 | 155 | Downstream: 156 | parallel: 157 | matrix: 158 | - DOWNSTREAM_PROJECT: [pytential] 159 | tags: 160 | - large-node 161 | - "docker-runner" 162 | script: | 163 | curl -L -O https://tiker.net/ci-support-v0 164 | . ./ci-support-v0 165 | test_downstream "$DOWNSTREAM_PROJECT" 166 | 167 | # vim: sw=2 168 | -------------------------------------------------------------------------------- /.pylintrc-local.yml: -------------------------------------------------------------------------------- 1 | - arg: py-version 2 | val: '3.10' 3 | 4 | - arg: extension-pkg-whitelist 5 | val: mayavi 6 | - arg: ignored-modules 7 | val: 8 | - symengine 9 | -------------------------------------------------------------------------------- /.test-conda-env-py3.yml: -------------------------------------------------------------------------------- 1 | name: test-conda-env-py3 2 | channels: 3 | - conda-forge 4 | - nodefaults 5 | 6 | dependencies: 7 | - git 8 | - numpy 9 | - sympy 10 | - pocl 11 | - pocl-cuda 12 | - islpy 13 | - pyopencl 14 | - python=3 15 | - python-symengine 16 | - pyfmmlib 17 | - pyvkfft 18 | - mpi4py 19 | 20 | # This is intended to prevent conda from selecting 'external' (i.e. empty) builds 21 | # of OpenMPI to satisfy the MPI dependency of mpi4py. It did so in May 2024, leading 22 | # to confusing failues saying 23 | # 'libmpi.so.40: cannot open shared object file: No such file or directory'. 24 | # https://github.com/conda-forge/openmpi-feedstock/issues/153 25 | # https://conda-forge.org/docs/user/tipsandtricks/#using-external-message-passing-interface-mpi-libraries 26 | - openmpi>=5=h* 27 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Kloeckner" 5 | given-names: "Andreas" 6 | orcid: "https://orcid.org/0000-0003-1228-519X" 7 | - family-names: Fernando 8 | given-names: Isuru 9 | - family-names: Wala 10 | given-names: Matt 11 | - family-names: Fikl 12 | given-names: Alexandru 13 | - family-names: Beams 14 | given-names: Natalie 15 | - family-names: Gao 16 | given-names: Hao 17 | 18 | title: "sumpy" 19 | version: 2022.1 20 | doi: 10.5281/zenodo.7349787 21 | date-released: 2022-11-23 22 | url: "https://github.com/inducer/sumpy" 23 | license: MIT 24 | -------------------------------------------------------------------------------- /MEMO: -------------------------------------------------------------------------------- 1 | - AoS/SoA flexibility? 2 | 3 | Conventions: 4 | 5 | a = center - src 6 | b = tgt - center 7 | d = tgt - src 8 | 9 | Kernel scaling is done strictly on output to point values 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | sumpy: n-body kernels and translation operators 2 | =============================================== 3 | 4 | .. image:: https://gitlab.tiker.net/inducer/sumpy/badges/main/pipeline.svg 5 | :alt: Gitlab Build Status 6 | :target: https://gitlab.tiker.net/inducer/sumpy/commits/main 7 | .. image:: https://github.com/inducer/sumpy/actions/workflows/ci.yml/badge.svg 8 | :alt: Github Build Status 9 | :target: https://github.com/inducer/sumpy/actions/workflows/ci.yml 10 | .. image:: https://badge.fury.io/py/sumpy.svg 11 | :alt: Python Package Index Release Page 12 | :target: https://pypi.org/project/sumpy/ 13 | .. image:: https://zenodo.org/badge/1856097.svg 14 | :alt: Zenodo DOI for latest release 15 | :target: https://zenodo.org/badge/latestdoi/1856097 16 | 17 | sumpy is mainly a 'scaffolding' package for Fast Multipole and quadrature methods. 18 | If you're building one of those and need code generation for the required multipole 19 | and local expansions, come right on in. Together with ``boxtree``, there is a full, 20 | symbolically kernel-independent FMM implementation here. 21 | 22 | It relies on 23 | 24 | * `boxtree `__ for FMM tree building 25 | * `loopy `__ for fast array operations 26 | * `pytest `__ for automated testing 27 | 28 | and, indirectly, 29 | 30 | * `PyOpenCL `__ as computational infrastructure 31 | 32 | Resources: 33 | 34 | * `documentation `__ 35 | * `source code via git `__ 36 | * `benchmarks `__ 37 | -------------------------------------------------------------------------------- /asv.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | // The version of the config file format. Do not change, unless 3 | // you know what you are doing. 4 | "version": 1, 5 | 6 | // The name of the project being benchmarked 7 | "project": "sumpy", 8 | 9 | // The project's homepage 10 | "project_url": "https://documen.tician.de/sumpy", 11 | 12 | // The URL or local path of the source code repository for the 13 | // project being benchmarked 14 | "repo": ".", 15 | 16 | // The Python project's subdirectory in your repo. If missing or 17 | // the empty string, the project is assumed to be located at the root 18 | // of the repository. 19 | // "repo_subdir": "", 20 | 21 | // List of branches to benchmark. If not provided, defaults to "master" 22 | // (for git) or "default" (for mercurial). 23 | "branches": ["main"], // for git 24 | // "branches": ["default"], // for mercurial 25 | 26 | // The DVCS being used. If not set, it will be automatically 27 | // determined from "repo" by looking at the protocol in the URL 28 | // (if remote), or by looking for special directories, such as 29 | // ".git" (if local). 30 | // "dvcs": "git", 31 | 32 | // The tool to use to create environments. May be "conda", 33 | // "virtualenv" or other value depending on the plugins in use. 34 | // If missing or the empty string, the tool will be automatically 35 | // determined by looking for tools on the PATH environment 36 | // variable. 37 | "environment_type": "conda", 38 | 39 | // timeout in seconds for installing any dependencies in environment 40 | // defaults to 10 min 41 | //"install_timeout": 600, 42 | 43 | // the base URL to show a commit for the project. 44 | "show_commit_url": "https://gitlab.tiker.net/inducer/sumpy/commits/", 45 | 46 | // The Pythons you'd like to test against. If not provided, defaults 47 | // to the current version of Python used to run `asv`. 48 | // "pythons": ["2.7", "3.6"], 49 | 50 | // The list of conda channel names to be searched for benchmark 51 | // dependency packages in the specified order 52 | "conda_channels": ["conda-forge"], 53 | 54 | // The matrix of dependencies to test. Each key is the name of a 55 | // package (in PyPI) and the values are version numbers. An empty 56 | // list or empty string indicates to just test against the default 57 | // (latest) version. null indicates that the package is to not be 58 | // installed. If the package to be tested is only available from 59 | // PyPi, and the 'environment_type' is conda, then you can preface 60 | // the package name by 'pip+', and the package will be installed via 61 | // pip (with all the conda available packages installed first, 62 | // followed by the pip installed packages). 63 | // 64 | // "matrix": { 65 | // "numpy": ["1.6", "1.7"], 66 | // "six": ["", null], // test with and without six installed 67 | // "pip+emcee": [""], // emcee is only available for install with pip. 68 | // }, 69 | "matrix": { 70 | "numpy" : [""], 71 | "sympy" : [""], 72 | "pyopencl" : [""], 73 | "islpy" : [""], 74 | "pocl" : [""], 75 | "pyvkfft": [""], 76 | "pip+git+https://github.com/inducer/pymbolic#egg=pymbolic": [""], 77 | "pip+git+https://gitlab.tiker.net/inducer/boxtree#egg=boxtree": [""], 78 | "pip+git+https://github.com/inducer/loopy#egg=loopy": [""], 79 | "pip" : [""], 80 | }, 81 | 82 | // Combinations of libraries/python versions can be excluded/included 83 | // from the set to test. Each entry is a dictionary containing additional 84 | // key-value pairs to include/exclude. 85 | // 86 | // An exclude entry excludes entries where all values match. The 87 | // values are regexps that should match the whole string. 88 | // 89 | // An include entry adds an environment. Only the packages listed 90 | // are installed. The 'python' key is required. The exclude rules 91 | // do not apply to includes. 92 | // 93 | // In addition to package names, the following keys are available: 94 | // 95 | // - python 96 | // Python version, as in the *pythons* variable above. 97 | // - environment_type 98 | // Environment type, as above. 99 | // - sys_platform 100 | // Platform, as in sys.platform. Possible values for the common 101 | // cases: 'linux2', 'win32', 'cygwin', 'darwin'. 102 | // 103 | // "exclude": [ 104 | // {"python": "3.2", "sys_platform": "win32"}, // skip py3.2 on windows 105 | // {"environment_type": "conda", "six": null}, // don't run without six on conda 106 | // ], 107 | // 108 | // "include": [ 109 | // // additional env for python2.7 110 | // {"python": "2.7", "numpy": "1.8"}, 111 | // // additional env if run on windows+conda 112 | // {"platform": "win32", "environment_type": "conda", "python": "2.7", "libpython": ""}, 113 | // ], 114 | 115 | // The directory (relative to the current directory) that benchmarks are 116 | // stored in. If not provided, defaults to "benchmarks" 117 | // "benchmark_dir": "benchmarks", 118 | 119 | // The directory (relative to the current directory) to cache the Python 120 | // environments in. If not provided, defaults to "env" 121 | "env_dir": ".asv/env", 122 | 123 | // The directory (relative to the current directory) that raw benchmark 124 | // results are stored in. If not provided, defaults to "results". 125 | "results_dir": ".asv/results", 126 | 127 | // The directory (relative to the current directory) that the html tree 128 | // should be written to. If not provided, defaults to "html". 129 | "html_dir": ".asv/html", 130 | 131 | // The number of characters to retain in the commit hashes. 132 | // "hash_length": 8, 133 | 134 | // `asv` will cache wheels of the recent builds in each 135 | // environment, making them faster to install next time. This is 136 | // number of builds to keep, per environment. 137 | // "wheel_cache_size": 0 138 | 139 | // The commits after which the regression search in `asv publish` 140 | // should start looking for regressions. Dictionary whose keys are 141 | // regexps matching to benchmark names, and values corresponding to 142 | // the commit (exclusive) after which to start looking for 143 | // regressions. The default is to start from the first commit 144 | // with results. If the commit is `null`, regression detection is 145 | // skipped for the matching benchmark. 146 | // 147 | // "regressions_first_commits": { 148 | // "some_benchmark": "352cdf", // Consider regressions only after this commit 149 | // "another_benchmark": null, // Skip regression detection altogether 150 | // } 151 | 152 | // The thresholds for relative change in results, after which `asv 153 | // publish` starts reporting regressions. Dictionary of the same 154 | // form as in ``regressions_first_commits``, with values 155 | // indicating the thresholds. If multiple entries match, the 156 | // maximum is taken. If no entry matches, the default is 5%. 157 | // 158 | // "regressions_thresholds": { 159 | // "some_benchmark": 0.01, // Threshold of 1% 160 | // "another_benchmark": 0.5, // Threshold of 50% 161 | // } 162 | } 163 | -------------------------------------------------------------------------------- /benchmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inducer/sumpy/c9720f75b2e34cd68b71b40809c07f85e165f271/benchmarks/__init__.py -------------------------------------------------------------------------------- /benchmarks/bench_translations.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | 5 | import numpy as np 6 | 7 | from pyopencl.tools import ( # noqa 8 | pytest_generate_tests_for_pyopencl as pytest_generate_tests, 9 | ) 10 | 11 | from sumpy.expansion.local import ( 12 | H2DLocalExpansion, 13 | LinearPDEConformingVolumeTaylorLocalExpansion, 14 | VolumeTaylorLocalExpansion, 15 | ) 16 | from sumpy.expansion.multipole import ( 17 | H2DMultipoleExpansion, 18 | LinearPDEConformingVolumeTaylorMultipoleExpansion, 19 | VolumeTaylorMultipoleExpansion, 20 | ) 21 | from sumpy.kernel import HelmholtzKernel, LaplaceKernel 22 | 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | import pymbolic.mapper.flop_counter 27 | 28 | import sumpy.symbolic as sym 29 | from sumpy.assignment_collection import SymbolicAssignmentCollection 30 | from sumpy.codegen import to_loopy_insns 31 | 32 | 33 | class Param: 34 | def __init__(self, dim, order): 35 | self.dim = dim 36 | self.order = order 37 | 38 | def __repr__(self): 39 | return f"{self.dim}D_order_{self.order}" 40 | 41 | 42 | class TranslationBenchmarkSuite: 43 | 44 | params = ( 45 | Param(2, 10), 46 | Param(2, 15), 47 | Param(2, 20), 48 | Param(3, 5), 49 | Param(3, 10), 50 | ) 51 | 52 | param_names = ("order",) 53 | 54 | def setup(self, param): 55 | logging.basicConfig(level=logging.INFO) 56 | np.random.seed(17) # noqa: NPY002 57 | if self.__class__ == TranslationBenchmarkSuite: 58 | raise NotImplementedError 59 | mpole_expn_class = self.mpole_expn_class 60 | if param.order == 3 and H2DMultipoleExpansion == mpole_expn_class: 61 | raise NotImplementedError 62 | 63 | def track_m2l_op_count(self, param): 64 | knl = self.knl(param.dim) 65 | m_expn = self.mpole_expn_class(knl, order=param.order) 66 | l_expn = self.local_expn_class(knl, order=param.order) 67 | 68 | src_coeff_exprs = [ 69 | sym.Symbol(f"src_coeff{i}") 70 | for i in range(len(m_expn))] 71 | dvec = sym.make_sym_vector("d", knl.dim) 72 | src_rscale = sym.Symbol("src_rscale") 73 | tgt_rscale = sym.Symbol("tgt_rscale") 74 | sac = SymbolicAssignmentCollection() 75 | try: 76 | result = l_expn.translate_from(m_expn, src_coeff_exprs, src_rscale, 77 | dvec, tgt_rscale, sac) 78 | except TypeError: 79 | # Support older interface to make it possible to compare 80 | # in CI run 81 | result = l_expn.translate_from(m_expn, src_coeff_exprs, src_rscale, 82 | dvec, tgt_rscale) 83 | for i, expr in enumerate(result): 84 | sac.assign_unique(f"coeff{i}", expr) 85 | sac.run_global_cse() 86 | insns = to_loopy_insns(sac.assignments.items()) 87 | counter = pymbolic.mapper.flop_counter.CSEAwareFlopCounter() 88 | 89 | return sum(counter.rec(insn.expression)+1 for insn in insns) 90 | 91 | track_m2l_op_count.unit = "ops" 92 | track_m2l_op_count.timeout = 300.0 93 | 94 | 95 | class LaplaceVolumeTaylorTranslation(TranslationBenchmarkSuite): 96 | knl = LaplaceKernel 97 | local_expn_class = VolumeTaylorLocalExpansion 98 | mpole_expn_class = VolumeTaylorMultipoleExpansion 99 | params = ( 100 | Param(2, 10), 101 | Param(3, 5), 102 | ) 103 | 104 | 105 | class LaplaceConformingVolumeTaylorTranslation(TranslationBenchmarkSuite): 106 | knl = LaplaceKernel 107 | local_expn_class = LinearPDEConformingVolumeTaylorLocalExpansion 108 | mpole_expn_class = LinearPDEConformingVolumeTaylorMultipoleExpansion 109 | 110 | 111 | class HelmholtzVolumeTaylorTranslation(TranslationBenchmarkSuite): 112 | knl = HelmholtzKernel 113 | local_expn_class = VolumeTaylorLocalExpansion 114 | mpole_expn_class = VolumeTaylorMultipoleExpansion 115 | params = ( 116 | Param(2, 10), 117 | Param(3, 5), 118 | ) 119 | 120 | 121 | class HelmholtzConformingVolumeTaylorTranslation(TranslationBenchmarkSuite): 122 | knl = HelmholtzKernel 123 | local_expn_class = LinearPDEConformingVolumeTaylorLocalExpansion 124 | mpole_expn_class = LinearPDEConformingVolumeTaylorMultipoleExpansion 125 | 126 | 127 | class Helmholtz2DTranslation(TranslationBenchmarkSuite): 128 | knl = HelmholtzKernel 129 | local_expn_class = H2DLocalExpansion 130 | mpole_expn_class = H2DMultipoleExpansion 131 | params = ( 132 | Param(2, 10), 133 | Param(2, 15), 134 | Param(2, 20), 135 | ) 136 | -------------------------------------------------------------------------------- /contrib/translations/PDE-reduction and translations.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from __future__ import annotations\n", 10 | "\n", 11 | "import matplotlib.pyplot as plt\n", 12 | "import numpy as np\n", 13 | "import numpy.linalg as la\n", 14 | "\n", 15 | "import pyopencl as cl\n", 16 | "from pytools import add_tuples\n", 17 | "\n", 18 | "import sumpy.toys as t\n", 19 | "from sumpy.expansion.local import VolumeTaylorLocalExpansion\n", 20 | "from sumpy.expansion.multipole import VolumeTaylorMultipoleExpansion\n", 21 | "from sumpy.kernel import HelmholtzKernel, LaplaceKernel, YukawaKernel # noqa: F401\n", 22 | "\n", 23 | "\n", 24 | "rng = np.random.default_rng(seed=42)\n", 25 | "order = 4\n", 26 | "\n", 27 | "if 0:\n", 28 | " knl = LaplaceKernel(2)\n", 29 | " pde = [(1, (2, 0)), (1, (0, 2))]\n", 30 | " extra_kernel_kwargs = {}\n", 31 | "\n", 32 | "else:\n", 33 | " helm_k = 1.2\n", 34 | " knl = HelmholtzKernel(2)\n", 35 | " extra_kernel_kwargs = {\"k\": helm_k}\n", 36 | "\n", 37 | " pde = [(1, (2, 0)), (1, (0, 2)), (helm_k**2, (0, 0))]\n", 38 | "\n", 39 | "mpole_expn = VolumeTaylorMultipoleExpansion(knl, order)\n", 40 | "local_expn = VolumeTaylorLocalExpansion(knl, order)\n", 41 | "\n", 42 | "cl_ctx = cl.create_some_context(answers=[\"port\"])\n", 43 | "\n", 44 | "tctx = t.ToyContext(\n", 45 | " cl_ctx,\n", 46 | " knl,\n", 47 | " mpole_expn_class=type(mpole_expn),\n", 48 | " local_expn_class=type(local_expn),\n", 49 | " extra_kernel_kwargs=extra_kernel_kwargs,\n", 50 | ")" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "pt_src = t.PointSources(tctx, rng.uniform(-0.5, 0.5, size=(2, 50)), np.ones(50))\n", 60 | "\n", 61 | "mexp = t.multipole_expand(pt_src, [0, 0], order)" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "mexp.coeffs" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "def build_pde_mat(expn, pde):\n", 80 | " coeff_ids = expn.get_coefficient_identifiers()\n", 81 | " id_to_index = expn._storage_loc_dict\n", 82 | "\n", 83 | " # FIXME: specific to scalar PDEs\n", 84 | " pde_mat = np.zeros((len(coeff_ids), len(coeff_ids)))\n", 85 | "\n", 86 | " row = 0\n", 87 | " for base_coeff_id in coeff_ids:\n", 88 | " valid = True\n", 89 | "\n", 90 | " for pde_coeff, coeff_id_offset in pde:\n", 91 | " other_coeff = add_tuples(base_coeff_id, coeff_id_offset)\n", 92 | " if other_coeff not in id_to_index:\n", 93 | " valid = False\n", 94 | " break\n", 95 | "\n", 96 | " pde_mat[row, id_to_index[other_coeff]] = pde_coeff\n", 97 | "\n", 98 | " if valid:\n", 99 | " row += 1\n", 100 | " else:\n", 101 | " pde_mat[row] = 0\n", 102 | "\n", 103 | " return pde_mat[:row]\n", 104 | "\n", 105 | "\n", 106 | "pde_mat = build_pde_mat(mpole_expn, pde)" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "def find_nullspace(mat, tol=1e-10):\n", 116 | " _u, sig, vt = la.svd(pde_mat, full_matrices=True)\n", 117 | " zerosig = np.where(np.abs(sig) < tol)[0]\n", 118 | " if zerosig.size:\n", 119 | " nullsp_start = zerosig[0]\n", 120 | " assert np.array_equal(zerosig, np.arange(nullsp_start, pde_mat.shape[1]))\n", 121 | " else:\n", 122 | " nullsp_start = pde_mat.shape[0]\n", 123 | "\n", 124 | " return vt[nullsp_start:].T\n", 125 | "\n", 126 | "\n", 127 | "nullsp = find_nullspace(pde_mat)" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": null, 133 | "metadata": {}, 134 | "outputs": [], 135 | "source": [ 136 | "la.norm(pde_mat @ nullsp)" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": null, 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "def build_translation_mat(mexp, to_center):\n", 146 | " n = len(mexp.coeffs)\n", 147 | " result = np.zeros((n, n))\n", 148 | "\n", 149 | " for j in range(n):\n", 150 | " unit_coeffs = np.zeros(n)\n", 151 | " unit_coeffs[j] = 1\n", 152 | " unit_mexp = mexp.with_coeffs(unit_coeffs)\n", 153 | "\n", 154 | " result[:, j] = t.multipole_expand(unit_mexp, to_center).coeffs\n", 155 | "\n", 156 | " return result\n", 157 | "\n", 158 | "\n", 159 | "new_center = np.array([0, 0.5])\n", 160 | "tmat = build_translation_mat(mexp, new_center)" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": null, 166 | "metadata": {}, 167 | "outputs": [], 168 | "source": [ 169 | "plt.imshow(tmat)" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": null, 175 | "metadata": {}, 176 | "outputs": [], 177 | "source": [ 178 | "nullsp.shape" 179 | ] 180 | }, 181 | { 182 | "cell_type": "code", 183 | "execution_count": null, 184 | "metadata": {}, 185 | "outputs": [], 186 | "source": [ 187 | "if 0:\n", 188 | " reduction_mat = nullsp.T\n", 189 | " expansion_mat = nullsp\n", 190 | "elif 1:\n", 191 | " chosen_indices_and_coeff_ids = [\n", 192 | " (i, cid)\n", 193 | " for i, cid in enumerate(mpole_expn.get_coefficient_identifiers())\n", 194 | " if cid[0] < 2\n", 195 | " ]\n", 196 | " chosen_indices = [idx for idx, _ in chosen_indices_and_coeff_ids]\n", 197 | "\n", 198 | " expansion_mat = np.zeros((\n", 199 | " len(mpole_expn.get_coefficient_identifiers()),\n", 200 | " len(chosen_indices_and_coeff_ids),\n", 201 | " ))\n", 202 | " for i, (idx, _) in enumerate(chosen_indices_and_coeff_ids):\n", 203 | " expansion_mat[idx, i] = 1\n", 204 | "\n", 205 | " reduction_mat = (nullsp @ la.inv(nullsp[chosen_indices])).T" 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": null, 211 | "metadata": {}, 212 | "outputs": [], 213 | "source": [ 214 | "def plot_coeffs(expn, coeffs, **kwargs):\n", 215 | " x = [cid[0] for cid in expn.get_coefficient_identifiers()]\n", 216 | " y = [cid[1] for cid in expn.get_coefficient_identifiers()]\n", 217 | " plt.scatter(x, y, c=coeffs, **kwargs)\n", 218 | " plt.colorbar()\n", 219 | "\n", 220 | " for cid, coeff in zip(expn.get_coefficient_identifiers(), coeffs, strict=True):\n", 221 | " plt.text(cid[0], cid[1] + 0.2, f\"{coeff:.1f}\")" 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": null, 227 | "metadata": {}, 228 | "outputs": [], 229 | "source": [ 230 | "proj_mexp = mexp.with_coeffs(expansion_mat @ reduction_mat @ mexp.coeffs)\n", 231 | "\n", 232 | "proj_resid = proj_mexp.coeffs - mexp.coeffs\n", 233 | "\n", 234 | "plot_coeffs(mpole_expn, np.log10(1e-15 + np.abs(proj_resid)), vmin=-15, vmax=2)" 235 | ] 236 | }, 237 | { 238 | "cell_type": "code", 239 | "execution_count": null, 240 | "metadata": {}, 241 | "outputs": [], 242 | "source": [ 243 | "print(t.l_inf(proj_mexp - mexp, 1.2, center=[3, 0]))" 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": null, 249 | "metadata": {}, 250 | "outputs": [], 251 | "source": [ 252 | "trans_unproj = t.multipole_expand(mexp, new_center)\n", 253 | "trans_proj = t.multipole_expand(proj_mexp, new_center)\n", 254 | "\n", 255 | "print(t.l_inf(trans_unproj - trans_proj, 1.2, center=[3, 0]))" 256 | ] 257 | }, 258 | { 259 | "cell_type": "code", 260 | "execution_count": null, 261 | "metadata": {}, 262 | "outputs": [], 263 | "source": [ 264 | "print(trans_proj.coeffs - trans_unproj.coeffs)" 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": null, 270 | "metadata": {}, 271 | "outputs": [], 272 | "source": [ 273 | "la.norm(reduction_mat @ (trans_proj.coeffs - trans_unproj.coeffs))" 274 | ] 275 | }, 276 | { 277 | "cell_type": "code", 278 | "execution_count": null, 279 | "metadata": {}, 280 | "outputs": [], 281 | "source": [ 282 | "t.l_inf(trans_unproj - pt_src, 1.2, center=[3, 0])" 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": null, 288 | "metadata": {}, 289 | "outputs": [], 290 | "source": [ 291 | "t.l_inf(mexp - pt_src, 1.2, center=[3, 0])" 292 | ] 293 | } 294 | ], 295 | "metadata": { 296 | "kernelspec": { 297 | "display_name": "Python 3 (ipykernel)", 298 | "language": "python", 299 | "name": "python3" 300 | }, 301 | "language_info": { 302 | "codemirror_mode": { 303 | "name": "ipython", 304 | "version": 3 305 | }, 306 | "file_extension": ".py", 307 | "mimetype": "text/x-python", 308 | "name": "python", 309 | "nbconvert_exporter": "python", 310 | "pygments_lexer": "ipython3", 311 | "version": "3.12.7" 312 | } 313 | }, 314 | "nbformat": 4, 315 | "nbformat_minor": 4 316 | } 317 | -------------------------------------------------------------------------------- /contrib/translations/PDE-reduction-symbolic.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from __future__ import annotations\n", 10 | "\n", 11 | "from sumpy.expansion.local import VolumeTaylorLocalExpansion\n", 12 | "from sumpy.expansion.multipole import (\n", 13 | " LaplaceConformingVolumeTaylorMultipoleExpansion,\n", 14 | " LinearPDEConformingVolumeTaylorMultipoleExpansion,\n", 15 | " VolumeTaylorMultipoleExpansion,\n", 16 | ")\n", 17 | "from sumpy.kernel import HelmholtzKernel, LaplaceKernel, YukawaKernel # noqa: F401\n", 18 | "from sumpy.symbolic import make_sym_vector\n", 19 | "\n", 20 | "\n", 21 | "order = 2\n", 22 | "dim = 2\n", 23 | "\n", 24 | "if 0:\n", 25 | " knl = LaplaceKernel(dim)\n", 26 | " extra_kernel_kwargs = {}\n", 27 | " mpole_expn_reduced_class = LaplaceConformingVolumeTaylorMultipoleExpansion\n", 28 | "\n", 29 | "else:\n", 30 | " helm_k = 1.2\n", 31 | " knl = HelmholtzKernel(dim)\n", 32 | " extra_kernel_kwargs = {\"k\": helm_k}\n", 33 | " mpole_expn_reduced_class = LinearPDEConformingVolumeTaylorMultipoleExpansion\n", 34 | "\n", 35 | "mpole_expn_reduced = mpole_expn_reduced_class(knl, order)\n", 36 | "mpole_expn = VolumeTaylorMultipoleExpansion(knl, order)\n", 37 | "local_expn = VolumeTaylorLocalExpansion(knl, order)" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "reduced_wrangler = mpole_expn_reduced.expansion_terms_wrangler\n", 47 | "full_wrangler = mpole_expn.expansion_terms_wrangler\n", 48 | "\n", 49 | "reduced_derivatives = list(\n", 50 | " make_sym_vector(\"deriv\", len(reduced_wrangler.stored_identifiers))\n", 51 | ")\n", 52 | "full_derivatives = reduced_wrangler.get_full_kernel_derivatives_from_stored(\n", 53 | " reduced_derivatives, 1\n", 54 | ")\n", 55 | "\n", 56 | "print(reduced_derivatives)\n", 57 | "print(full_derivatives)" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "full_coeffs = list(\n", 67 | " make_sym_vector(\"coeff\", len(reduced_wrangler.get_full_coefficient_identifiers()))\n", 68 | ")\n", 69 | "\n", 70 | "reduced_coeffs = reduced_wrangler.get_stored_mpole_coefficients_from_full(\n", 71 | " full_mpole_coefficients=full_coeffs, rscale=1\n", 72 | ")\n", 73 | "\n", 74 | "print(full_coeffs)\n", 75 | "print(reduced_coeffs)" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "dvec = make_sym_vector(\"d\", dim)\n", 85 | "translated_reduce_coeffs = mpole_expn_reduced.translate_from(\n", 86 | " mpole_expn_reduced, reduced_coeffs, 1, dvec, 1\n", 87 | ")\n", 88 | "translated_full_coeffs = mpole_expn.translate_from(mpole_expn, full_coeffs, 1, dvec, 1)\n", 89 | "translated_full_coeffs" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": null, 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "eval_reduced = sum(a * b for a, b in zip(translated_reduce_coeffs, reduced_derivatives,\n", 99 | " strict=True))\n", 100 | "eval_full = sum(a * b for a, b in zip(translated_full_coeffs, full_derivatives,\n", 101 | " strict=True))\n", 102 | "\n", 103 | "(eval_full - eval_reduced).simplify()" 104 | ] 105 | } 106 | ], 107 | "metadata": { 108 | "kernelspec": { 109 | "display_name": "Python 3 (ipykernel)", 110 | "language": "python", 111 | "name": "python3" 112 | }, 113 | "language_info": { 114 | "codemirror_mode": { 115 | "name": "ipython", 116 | "version": 3 117 | }, 118 | "file_extension": ".py", 119 | "mimetype": "text/x-python", 120 | "name": "python", 121 | "nbconvert_exporter": "python", 122 | "pygments_lexer": "ipython3", 123 | "version": "3.12.7" 124 | } 125 | }, 126 | "nbformat": 4, 127 | "nbformat_minor": 4 128 | } 129 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python `which sphinx-build` 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from https://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | .PHONY: clean 52 | clean: 53 | rm -rf $(BUILDDIR)/* 54 | 55 | .PHONY: html 56 | html: 57 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 58 | @echo 59 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 60 | 61 | .PHONY: dirhtml 62 | dirhtml: 63 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 64 | @echo 65 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 66 | 67 | .PHONY: singlehtml 68 | singlehtml: 69 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 70 | @echo 71 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 72 | 73 | .PHONY: pickle 74 | pickle: 75 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 76 | @echo 77 | @echo "Build finished; now you can process the pickle files." 78 | 79 | .PHONY: json 80 | json: 81 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 82 | @echo 83 | @echo "Build finished; now you can process the JSON files." 84 | 85 | .PHONY: htmlhelp 86 | htmlhelp: 87 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 88 | @echo 89 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 90 | ".hhp project file in $(BUILDDIR)/htmlhelp." 91 | 92 | .PHONY: qthelp 93 | qthelp: 94 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 95 | @echo 96 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 97 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 98 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sumpy.qhcp" 99 | @echo "To view the help file:" 100 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sumpy.qhc" 101 | 102 | .PHONY: applehelp 103 | applehelp: 104 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 105 | @echo 106 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 107 | @echo "N.B. You won't be able to view it unless you put it in" \ 108 | "~/Library/Documentation/Help or install it in your application" \ 109 | "bundle." 110 | 111 | .PHONY: devhelp 112 | devhelp: 113 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 114 | @echo 115 | @echo "Build finished." 116 | @echo "To view the help file:" 117 | @echo "# mkdir -p $$HOME/.local/share/devhelp/sumpy" 118 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sumpy" 119 | @echo "# devhelp" 120 | 121 | .PHONY: epub 122 | epub: 123 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 124 | @echo 125 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 126 | 127 | .PHONY: epub3 128 | epub3: 129 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 130 | @echo 131 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 132 | 133 | .PHONY: latex 134 | latex: 135 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 136 | @echo 137 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 138 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 139 | "(use \`make latexpdf' here to do that automatically)." 140 | 141 | .PHONY: latexpdf 142 | latexpdf: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through pdflatex..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: latexpdfja 149 | latexpdfja: 150 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 151 | @echo "Running LaTeX files through platex and dvipdfmx..." 152 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 153 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 154 | 155 | .PHONY: text 156 | text: 157 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 158 | @echo 159 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 160 | 161 | .PHONY: man 162 | man: 163 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 164 | @echo 165 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 166 | 167 | .PHONY: texinfo 168 | texinfo: 169 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 170 | @echo 171 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 172 | @echo "Run \`make' in that directory to run these through makeinfo" \ 173 | "(use \`make info' here to do that automatically)." 174 | 175 | .PHONY: info 176 | info: 177 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 178 | @echo "Running Texinfo files through makeinfo..." 179 | make -C $(BUILDDIR)/texinfo info 180 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 181 | 182 | .PHONY: gettext 183 | gettext: 184 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 185 | @echo 186 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 187 | 188 | .PHONY: changes 189 | changes: 190 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 191 | @echo 192 | @echo "The overview file is in $(BUILDDIR)/changes." 193 | 194 | .PHONY: linkcheck 195 | linkcheck: 196 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 197 | @echo 198 | @echo "Link check complete; look for any errors in the above output " \ 199 | "or in $(BUILDDIR)/linkcheck/output.txt." 200 | 201 | .PHONY: doctest 202 | doctest: 203 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 204 | @echo "Testing of doctests in the sources finished, look at the " \ 205 | "results in $(BUILDDIR)/doctest/output.txt." 206 | 207 | .PHONY: coverage 208 | coverage: 209 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 210 | @echo "Testing of coverage in the sources finished, look at the " \ 211 | "results in $(BUILDDIR)/coverage/python.txt." 212 | 213 | .PHONY: xml 214 | xml: 215 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 216 | @echo 217 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 218 | 219 | .PHONY: pseudoxml 220 | pseudoxml: 221 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 222 | @echo 223 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 224 | -------------------------------------------------------------------------------- /doc/codegen.rst: -------------------------------------------------------------------------------- 1 | Code Generation 2 | =============== 3 | 4 | .. automodule:: sumpy.codegen 5 | .. automodule:: sumpy.assignment_collection 6 | .. automodule:: sumpy.cse 7 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | from importlib import metadata 2 | from urllib.request import urlopen 3 | 4 | 5 | _conf_url = \ 6 | "https://raw.githubusercontent.com/inducer/sphinxconfig/main/sphinxconfig.py" 7 | with urlopen(_conf_url) as _inf: 8 | exec(compile(_inf.read(), _conf_url, "exec"), globals()) 9 | 10 | copyright = "2016-21, sumpy contributors" 11 | release = metadata.version("sumpy") 12 | version = ".".join(release.split(".")[:2]) 13 | 14 | intersphinx_mapping = { 15 | "arraycontext": ("https://documen.tician.de/arraycontext/", None), 16 | "boxtree": ("https://documen.tician.de/boxtree/", None), 17 | "loopy": ("https://documen.tician.de/loopy/", None), 18 | "matplotlib": ("https://matplotlib.org/stable/", None), 19 | "numpy": ("https://numpy.org/doc/stable/", None), 20 | "pymbolic": ("https://documen.tician.de/pymbolic/", None), 21 | "pyopencl": ("https://documen.tician.de/pyopencl/", None), 22 | "pytential": ("https://documen.tician.de/pytential/", None), 23 | "python": ("https://docs.python.org/3/", None), 24 | "pytools": ("https://documen.tician.de/pytools/", None), 25 | "sympy": ("https://docs.sympy.org/latest/", None), 26 | } 27 | 28 | nitpick_ignore_regex = [ 29 | ["py:class", r"symengine\.(.+)"], # :cry: 30 | ] 31 | -------------------------------------------------------------------------------- /doc/eval.rst: -------------------------------------------------------------------------------- 1 | Working with Values of Potentials 2 | ================================= 3 | 4 | Visualization of Potentials 5 | --------------------------- 6 | 7 | .. automodule:: sumpy.visualization 8 | 9 | Differentiation of Potentials 10 | ----------------------------- 11 | 12 | .. automodule:: sumpy.point_calculus 13 | 14 | Support for Numerical Experiments with Expansions ("Expansion toys") 15 | -------------------------------------------------------------------- 16 | 17 | .. automodule:: sumpy.toys 18 | -------------------------------------------------------------------------------- /doc/expansion.rst: -------------------------------------------------------------------------------- 1 | Expansions 2 | ========== 3 | 4 | .. automodule:: sumpy.expansion 5 | 6 | Differential Operators 7 | ---------------------- 8 | 9 | .. automodule:: sumpy.expansion.diff_op 10 | 11 | Local Expansions 12 | ---------------- 13 | 14 | .. automodule:: sumpy.expansion.local 15 | 16 | Multipole Expansions 17 | -------------------- 18 | 19 | .. automodule:: sumpy.expansion.multipole 20 | 21 | Multipole to Local Translations 22 | ------------------------------- 23 | 24 | .. automodule:: sumpy.expansion.m2l 25 | 26 | Estimating Expansion Orders 27 | --------------------------- 28 | 29 | .. automodule:: sumpy.expansion.level_to_order 30 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to sumpy's documentation! 2 | ================================= 3 | 4 | .. automodule:: sumpy 5 | 6 | Sumpy is mainly a 'scaffolding' package for Fast Multipole and quadrature methods. 7 | If you're building one of those and need code generation for the required Multipole 8 | and local expansions, come right on in. Together with boxtree, there is a full, 9 | symbolically kernel-independent FMM implementation here. 10 | 11 | Contents 12 | -------- 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | kernel 18 | expansion 19 | interactions 20 | codegen 21 | eval 22 | misc 23 | 🚀 Github 24 | 💾 Download Releases 25 | 26 | Indices and tables 27 | ================== 28 | 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | 33 | Example 34 | ------- 35 | 36 | .. literalinclude:: ../examples/curve-pot.py 37 | 38 | -------------------------------------------------------------------------------- /doc/interactions.rst: -------------------------------------------------------------------------------- 1 | Interaction routines 2 | ==================== 3 | 4 | .. automodule:: sumpy.p2p 5 | .. automodule:: sumpy.qbx 6 | 7 | Translation routines 8 | ==================== 9 | 10 | .. automodule:: sumpy.p2e 11 | .. automodule:: sumpy.e2e 12 | .. automodule:: sumpy.e2p 13 | 14 | Integration with :mod:`boxtree` 15 | =============================== 16 | 17 | .. automodule:: sumpy.fmm 18 | -------------------------------------------------------------------------------- /doc/kernel.rst: -------------------------------------------------------------------------------- 1 | Kernels 2 | ======= 3 | 4 | .. automodule:: sumpy.kernel 5 | -------------------------------------------------------------------------------- /doc/misc.rst: -------------------------------------------------------------------------------- 1 | Misc Tools 2 | ========== 3 | 4 | .. automodule:: sumpy.derivative_taker 5 | 6 | .. automodule:: sumpy.symbolic 7 | 8 | .. automodule:: sumpy.tools 9 | 10 | .. automodule:: sumpy.array_context 11 | 12 | Installation 13 | ============ 14 | 15 | This command should install :mod:`sumpy`:: 16 | 17 | pip install sumpy 18 | 19 | You may need to run this with :command:`sudo`. 20 | If you don't already have `pip `_, 21 | run this beforehand:: 22 | 23 | curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py 24 | python get-pip.py 25 | 26 | For a more manual installation, download the source, unpack it, 27 | and say:: 28 | 29 | python setup.py install 30 | 31 | In addition, you need to have :mod:`numpy` installed. 32 | 33 | Usage 34 | ===== 35 | 36 | Environment variables 37 | --------------------- 38 | 39 | +-----------------------------------+-----------------------------------------------------+ 40 | | Name | Purpose | 41 | +===================================+=====================================================+ 42 | | `SUMPY_FORCE_SYMBOLIC_BACKEND` | Symbolic backend control, see `Symbolic backends`_ | 43 | +-----------------------------------+-----------------------------------------------------+ 44 | | `SUMPY_NO_CACHE` | If set, disables the on-disk cache | 45 | +-----------------------------------+-----------------------------------------------------+ 46 | | `SUMPY_NO_OPT` | If set, disables performance-oriented :mod:`loopy` | 47 | | | transformations | 48 | +-----------------------------------+-----------------------------------------------------+ 49 | 50 | Symbolic backends 51 | ----------------- 52 | 53 | :mod:`sumpy` supports two symbolic backends: sympy and SymEngine. To use the 54 | SymEngine backend, ensure that the `SymEngine library 55 | `_ and the `SymEngine Python bindings 56 | `_ are installed. 57 | 58 | By default, :mod:`sumpy` prefers using SymEngine but falls back to sympy if it 59 | detects that SymEngine is not installed. To force the use of a particular 60 | backend, set the environment variable `SUMPY_FORCE_SYMBOLIC_BACKEND` to 61 | `symengine` or `sympy`. 62 | 63 | User-visible Changes 64 | ==================== 65 | 66 | Version 2016.1 67 | -------------- 68 | .. note:: 69 | 70 | This version is currently under development. You can get snapshots from 71 | sumpy's `git repository `_ 72 | 73 | * Initial release. 74 | 75 | .. _license: 76 | 77 | License 78 | ======= 79 | 80 | :mod:`sumpy` is licensed to you under the MIT/X Consortium license: 81 | 82 | Copyright (c) 2012-16 Andreas Klöckner 83 | 84 | Permission is hereby granted, free of charge, to any person 85 | obtaining a copy of this software and associated documentation 86 | files (the "Software"), to deal in the Software without 87 | restriction, including without limitation the rights to use, 88 | copy, modify, merge, publish, distribute, sublicense, and/or sell 89 | copies of the Software, and to permit persons to whom the 90 | Software is furnished to do so, subject to the following 91 | conditions: 92 | 93 | The above copyright notice and this permission notice shall be 94 | included in all copies or substantial portions of the Software. 95 | 96 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 97 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 98 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 99 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 100 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 101 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 102 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 103 | OTHER DEALINGS IN THE SOFTWARE. 104 | 105 | Frequently Asked Questions 106 | ========================== 107 | 108 | The FAQ is maintained collaboratively on the 109 | `Wiki FAQ page `_. 110 | 111 | Acknowledgments 112 | =============== 113 | 114 | Work on meshmode was supported in part by 115 | 116 | * the US National Science Foundation under grant numbers DMS-1418961, 117 | DMS-1654756, SHF-1911019, and OAC-1931577. 118 | 119 | AK also gratefully acknowledges a hardware gift from Nvidia Corporation. 120 | 121 | The views and opinions expressed herein do not necessarily reflect those of the 122 | funding agencies. 123 | -------------------------------------------------------------------------------- /doc/upload-docs.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | rsync --verbose --archive --delete _build/html/ doc-upload:doc/sumpy 4 | -------------------------------------------------------------------------------- /examples/curve.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy as sp 3 | import scipy.fftpack 4 | 5 | 6 | class CurveGrid: 7 | def __init__(self, x, y): 8 | self.pos = np.vstack([x, y]).copy() 9 | xp = self.xp = sp.fftpack.diff(x, period=1) 10 | yp = self.yp = sp.fftpack.diff(y, period=1) 11 | xpp = self.xpp = sp.fftpack.diff(xp, period=1) 12 | ypp = self.ypp = sp.fftpack.diff(yp, period=1) 13 | self.mean_curvature = (xp*ypp-yp*xpp)/((xp**2+yp**2)**(3/2)) 14 | 15 | speed = self.speed = np.sqrt(xp**2+yp**2) 16 | self.normal = (np.vstack([yp, -xp])/speed).copy() 17 | 18 | def __len__(self): 19 | return len(self.pos) 20 | 21 | def plot(self): 22 | import matplotlib.pyplot as pt 23 | pt.plot(self.pos[:, 0], self.pos[:, 1]) 24 | -------------------------------------------------------------------------------- /examples/expansion-toys.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import pyopencl as cl 4 | 5 | import sumpy.toys as t 6 | from sumpy.kernel import HelmholtzKernel, LaplaceKernel, YukawaKernel # noqa: F401 7 | from sumpy.visualization import FieldPlotter 8 | 9 | 10 | try: 11 | import matplotlib.pyplot as plt 12 | USE_MATPLOTLIB = True 13 | except ImportError: 14 | USE_MATPLOTLIB = False 15 | 16 | 17 | def main(): 18 | from sumpy.array_context import PyOpenCLArrayContext 19 | ctx = cl.create_some_context() 20 | queue = cl.CommandQueue(ctx) 21 | actx = PyOpenCLArrayContext(queue) 22 | 23 | tctx = t.ToyContext( 24 | actx.context, 25 | # LaplaceKernel(2), 26 | YukawaKernel(2), extra_kernel_kwargs={"lam": 5}, 27 | # HelmholtzKernel(2), extra_kernel_kwargs={"k": 0.3}, 28 | ) 29 | 30 | rng = np.random.default_rng() 31 | pt_src = t.PointSources( 32 | tctx, 33 | rng.uniform(size=(2, 50)) - 0.5, 34 | np.ones(50)) 35 | 36 | fp = FieldPlotter([3, 0], extent=8) 37 | 38 | if USE_MATPLOTLIB: 39 | t.logplot(fp, pt_src, cmap="jet") 40 | plt.colorbar() 41 | plt.show() 42 | 43 | mexp = t.multipole_expand(pt_src, [0, 0], 5) 44 | mexp2 = t.multipole_expand(mexp, [0, 0.25]) # noqa: F841 45 | lexp = t.local_expand(mexp, [3, 0]) 46 | lexp2 = t.local_expand(lexp, [3, 1], 3) 47 | 48 | # diff = mexp - pt_src 49 | # diff = mexp2 - pt_src 50 | diff = lexp2 - pt_src 51 | 52 | print(t.l_inf(diff, 1.2, center=lexp2.center)) 53 | if USE_MATPLOTLIB: 54 | t.logplot(fp, diff, cmap="jet", vmin=-3, vmax=0) 55 | plt.colorbar() 56 | plt.show() 57 | 58 | 59 | if __name__ == "__main__": 60 | main() 61 | -------------------------------------------------------------------------------- /examples/fourier.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def make_fourier_vdm(n, inverse): 5 | i = np.arange(n, dtype=np.float64) 6 | imat = i[:, np.newaxis]*i/n 7 | result = np.exp((2j*np.pi)*imat) 8 | 9 | if inverse: 10 | result = result.T.conj()/n 11 | return result 12 | 13 | 14 | def make_fourier_mode_extender(m, n, dtype): 15 | k = min(m, n) 16 | result = np.zeros((m, n), dtype) 17 | 18 | # https://docs.scipy.org/doc/numpy/reference/routines.fft.html 19 | if k % 2 == 0: # noqa: SIM108 20 | peak_pos_freq = k/2 21 | else: 22 | peak_pos_freq = (k-1)/2 23 | 24 | num_pos_freq = peak_pos_freq + 1 25 | num_neg_freq = k-num_pos_freq 26 | 27 | eye = np.eye(k) 28 | result[:num_pos_freq, :num_pos_freq] = eye[:num_pos_freq, :num_pos_freq] 29 | result[-num_neg_freq:, -num_neg_freq:] = eye[-num_neg_freq:, -num_neg_freq:] 30 | return result 31 | 32 | 33 | def make_fourier_interp_matrix(m, n): 34 | return np.dot( 35 | np.dot( 36 | make_fourier_vdm(m, inverse=False), 37 | make_fourier_mode_extender(m, n, np.float64)), 38 | make_fourier_vdm(n, inverse=True)) 39 | -------------------------------------------------------------------------------- /examples/sym-exp-complexity.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import loopy as lp 4 | import pyopencl as cl 5 | 6 | from sumpy.e2e import E2EFromCSR 7 | from sumpy.expansion.local import ( 8 | LinearPDEConformingVolumeTaylorLocalExpansion, 9 | ) 10 | from sumpy.expansion.multipole import ( 11 | LinearPDEConformingVolumeTaylorMultipoleExpansion, 12 | ) 13 | from sumpy.kernel import HelmholtzKernel, LaplaceKernel 14 | 15 | 16 | try: 17 | import matplotlib.pyplot as plt 18 | USE_MATPLOTLIB = True 19 | except ImportError: 20 | USE_MATPLOTLIB = False 21 | 22 | 23 | def find_flops(): 24 | from sumpy.array_context import PyOpenCLArrayContext 25 | ctx = cl.create_some_context() 26 | queue = cl.CommandQueue(ctx) 27 | actx = PyOpenCLArrayContext(queue) 28 | 29 | if 0: 30 | knl = LaplaceKernel(2) 31 | m_expn_cls = LinearPDEConformingVolumeTaylorMultipoleExpansion 32 | l_expn_cls = LinearPDEConformingVolumeTaylorLocalExpansion 33 | flop_type = np.float64 34 | else: 35 | knl = HelmholtzKernel(2) 36 | m_expn_cls = LinearPDEConformingVolumeTaylorMultipoleExpansion 37 | l_expn_cls = LinearPDEConformingVolumeTaylorLocalExpansion 38 | flop_type = np.complex128 39 | 40 | orders = list(range(1, 11, 1)) 41 | flop_counts = [] 42 | for order in orders: 43 | print(order) 44 | m_expn = m_expn_cls(knl, order) 45 | l_expn = l_expn_cls(knl, order) 46 | m2l = E2EFromCSR(actx.context, m_expn, l_expn) 47 | 48 | loopy_knl = m2l.get_kernel() 49 | loopy_knl = lp.add_and_infer_dtypes( 50 | loopy_knl, 51 | { 52 | "target_boxes,src_box_lists,src_box_starts": np.int32, 53 | "centers,src_expansions": np.float64, 54 | }) 55 | 56 | flops = lp.get_op_map(loopy_knl).filter_by(dtype=[flop_type]).sum() 57 | flop_counts.append( 58 | flops.eval_with_dict( 59 | {"isrc_start": 0, "isrc_stop": 1, "ntgt_boxes": 1})) 60 | 61 | print(orders) 62 | print(flop_counts) 63 | 64 | 65 | def plot_flops(): 66 | if 0: 67 | case = "3D Laplace M2L" 68 | orders = [1, 2, 3, 4, 5, 6, 7, 8, 9] 69 | flops = [62, 300, 914, 2221, 4567, 8405, 14172, 22538, 34113] 70 | filename = "laplace-m2l-complexity-3d.pdf" 71 | 72 | elif 1: 73 | case = "2D Laplace M2L" 74 | orders = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 75 | 18, 19, 20] 76 | flops = [36, 99, 193, 319, 476, 665, 889, 1143, 1429, 1747, 2097, 2479, 2893, 77 | 3339, 3817, 4327, 4869, 5443, 6049, 6687] 78 | filename = "laplace-m2l-complexity-2d.pdf" 79 | elif 0: 80 | case = "2D Helmholtz M2L" 81 | orders = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 82 | flops = [45, 194, 474, 931, 1650, 2632, 3925, 5591, 7706, 10272] 83 | filename = "helmholtz-m2l-complexity-2d.pdf" 84 | else: 85 | raise ValueError() 86 | 87 | if USE_MATPLOTLIB: 88 | plt.rc("font", size=16) 89 | plt.title(case) 90 | plt.ylabel("Flop count") 91 | plt.xlabel("Expansion order") 92 | plt.loglog(orders, flops, "o-") 93 | plt.grid() 94 | plt.tight_layout() 95 | plt.savefig(filename) 96 | 97 | 98 | if __name__ == "__main__": 99 | # find_flops() 100 | plot_flops() 101 | -------------------------------------------------------------------------------- /notes/expansion-notes.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inducer/sumpy/c9720f75b2e34cd68b71b40809c07f85e165f271/notes/expansion-notes.pdf -------------------------------------------------------------------------------- /notes/expansion-notes.tex: -------------------------------------------------------------------------------- 1 | \documentclass[10pt]{article} 2 | 3 | % Packages 4 | \usepackage{caption} 5 | \usepackage{graphicx} 6 | \usepackage[bf,small]{titlesec} 7 | \usepackage{float} 8 | \usepackage{hyperref} 9 | \usepackage{subcaption} 10 | 11 | \restylefloat{figure} 12 | 13 | \begin{document} 14 | 15 | \section*{Graf's addition theorem} 16 | 17 | \begin{figure}[H] 18 | \centering 19 | \includegraphics{media/graf.eps} 20 | \caption{Graf's addition theorem} 21 | \end{figure} 22 | 23 | \emph{Graf's addition theorem} says 24 | % 25 | \[ C_\nu(w) e^{i \nu \chi} = \sum_{k = -\infty}^{\infty} C_{\nu + k}(u) \; 26 | J_k(v) e^{i k \alpha} \] 27 | % 28 | where $C_\nu$ can be a Hankel or Bessel function of index $\nu$. This holds when 29 | $|u| > |v|$. \footnote{See for instance \url{https://dlmf.nist.gov/10.23\#ii}} 30 | 31 | \section*{Expansions} 32 | 33 | In all subsequent formulas, $\theta_{xy}$ refers to the angle of the vector 34 | $x-y$ above the horizontal. 35 | 36 | \begin{figure}[H] 37 | \centering 38 | \begin{subfigure}[b]{0.4\linewidth} 39 | \centering 40 | \includegraphics{media/m-expn.eps} 41 | \caption{Multipole expansion} 42 | \end{subfigure} 43 | % 44 | \hspace{1cm} 45 | % 46 | \begin{subfigure}[b]{0.4\linewidth} 47 | \centering 48 | \includegraphics{media/l-expn.eps} 49 | \caption{Local expansion} 50 | \end{subfigure} 51 | 52 | \caption{Multipole and local expansions} 53 | 54 | \end{figure} 55 | 56 | For a multipole expansion, we wish to evaluate $H_0^1(|t - s|)$ where the target 57 | is farther from the center than the source. The multipole expansion takes the 58 | form 59 | % 60 | \[ H_0^{(1)}(|t - s|) = \sum_{k = -\infty}^{\infty} \underbrace{J_k(|s - c|) 61 | e^{- i k \theta_{sc}}}_{\textrm{coefficients}} H_k^{(1)}(|t - c|) e^{i k 62 | \theta_{tc}}. \] 63 | % 64 | In the local expansion, the target is closer to the center than the source. The 65 | local expansion takes the form 66 | % 67 | \[ H_0^{(1)}(|t - s|) = \sum_{k = -\infty}^{\infty} \underbrace{H_k^{(1)}(|s - 68 | c|) e^{i k \theta_{sc}}}_{\textrm{coefficients}} J_k(|t - c|) e^{- i k 69 | \theta_{tc}}. \] 70 | 71 | \section*{Multipole-to-multipole and local-to-local translations} 72 | 73 | We wish to shift the center of the expansion $c_1$ to a new center $c_2$ 74 | satisfying $|c_1 - c_2| < |s - c_1|$. The goal is to derive a formula for the 75 | new coefficients based on the old coefficients and $c_1 - c_2$. This can be done 76 | with the help of Graf's addition theorem. 77 | 78 | \begin{figure}[H] 79 | \centering 80 | \includegraphics{media/m2m-l2l-translation.eps} 81 | \caption{Multipole-to-multipole and local-to-local translation} 82 | \end{figure} 83 | For shifting the multipole coefficients: 84 | \[ J_k(|s - c_2|) e^{-i k \theta_{sc_2}} = \sum_{l = -\infty}^{\infty} 85 | \underbrace{J_{k + l}(|s - c_1|) e^{-i (k + l) \theta_{sc_1}}}_{\textrm{old 86 | coefficients}} \; J_l(|c_2 - c_1|) e^{i l \theta_{c_2 c_1}}. \] 87 | % 88 | In a similar way, for shifting the local expansion coefficients: 89 | % 90 | \[ H_k^{(1)}(|s - c_2|)e^{ik\theta_{sc_2}} = \sum_{l = -\infty}^{\infty} 91 | \underbrace{H_{k + l}^{(1)}(|s - c_1|) e^{i(k + l) \theta_{sc_1}} }_{\textrm{old 92 | coefficients}} \; J_l(|c_2 - c_1|) e^{-i l \theta_{c_2 c_1}}. \] 93 | 94 | 95 | \section*{Multipole-to-local translation} 96 | 97 | Given a multipole expansion with center $c_1$, we wish to shift to center $c_2$ 98 | where $c_1$ and $c_2$ satisfy $|c_2 - c_1| > |s - c_1|$. Furthermore, the 99 | coefficients at the new center will be coefficients for a local expansion. 100 | 101 | \begin{figure}[H] 102 | \centering 103 | \includegraphics{media/m2l-translation.eps} 104 | \caption{Multipole-to-local translation} 105 | \end{figure} 106 | 107 | The translated coefficients satisfy 108 | % 109 | \[ H_k^{(1)}(|s - c_2|) e^{i k \theta_{sc_2}} = (-1)^k \sum_{l = -\infty}^{\infty} 110 | \underbrace{J_l(|s - c_1|) e^{- i l \theta_{sc_1}}}_{\textrm{old coefficients}} 111 | H_{k + l}^{(1)}(|c_1 - c_2|) e^{i(k + l)\theta_{c_2c_1}}. \] 112 | 113 | \end{document} 114 | -------------------------------------------------------------------------------- /notes/latexmkrc: -------------------------------------------------------------------------------- 1 | # https://tex.stackexchange.com/questions/11710/specify-output-directory-when-using-latexmk 2 | $pdflatex="pdflatex -interaction nonstopmode %O %S"; 3 | $out_dir = 'out'; 4 | $pdf_mode = 1; 5 | $pdf_previewer = 'xdg-open'; 6 | 7 | @default_files = ('expansion-notes'); 8 | 9 | $ENV{TEXINPUTS} .=':media'; 10 | -------------------------------------------------------------------------------- /notes/media/graf.ipe: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0 0 m 9 | -1 0.333 l 10 | -1 -0.333 l 11 | h 12 | 13 | 14 | 15 | 16 | 0 0 m 17 | -1 0.333 l 18 | -1 -0.333 l 19 | h 20 | 21 | 22 | 23 | 24 | 0 0 m 25 | -1 0.333 l 26 | -0.8 0 l 27 | -1 -0.333 l 28 | h 29 | 30 | 31 | 32 | 33 | 0 0 m 34 | -1 0.333 l 35 | -0.8 0 l 36 | -1 -0.333 l 37 | h 38 | 39 | 40 | 41 | 42 | 0.6 0 0 0.6 0 0 e 43 | 0.4 0 0 0.4 0 0 e 44 | 45 | 46 | 47 | 48 | 0.6 0 0 0.6 0 0 e 49 | 50 | 51 | 52 | 53 | 54 | 0.5 0 0 0.5 0 0 e 55 | 56 | 57 | 0.6 0 0 0.6 0 0 e 58 | 0.4 0 0 0.4 0 0 e 59 | 60 | 61 | 62 | 63 | 64 | -0.6 -0.6 m 65 | 0.6 -0.6 l 66 | 0.6 0.6 l 67 | -0.6 0.6 l 68 | h 69 | -0.4 -0.4 m 70 | 0.4 -0.4 l 71 | 0.4 0.4 l 72 | -0.4 0.4 l 73 | h 74 | 75 | 76 | 77 | 78 | -0.6 -0.6 m 79 | 0.6 -0.6 l 80 | 0.6 0.6 l 81 | -0.6 0.6 l 82 | h 83 | 84 | 85 | 86 | 87 | 88 | -0.5 -0.5 m 89 | 0.5 -0.5 l 90 | 0.5 0.5 l 91 | -0.5 0.5 l 92 | h 93 | 94 | 95 | -0.6 -0.6 m 96 | 0.6 -0.6 l 97 | 0.6 0.6 l 98 | -0.6 0.6 l 99 | h 100 | -0.4 -0.4 m 101 | 0.4 -0.4 l 102 | 0.4 0.4 l 103 | -0.4 0.4 l 104 | h 105 | 106 | 107 | 108 | 109 | 110 | 111 | -0.43 -0.57 m 112 | 0.57 0.43 l 113 | 0.43 0.57 l 114 | -0.57 -0.43 l 115 | h 116 | 117 | 118 | -0.43 0.57 m 119 | 0.57 -0.43 l 120 | 0.43 -0.57 l 121 | -0.57 0.43 l 122 | h 123 | 124 | 125 | 126 | 127 | 128 | 0 0 m 129 | -1 0.333 l 130 | -1 -0.333 l 131 | h 132 | 133 | 134 | 135 | 136 | 0 0 m 137 | -1 0.333 l 138 | -0.8 0 l 139 | -1 -0.333 l 140 | h 141 | 142 | 143 | 144 | 145 | 0 0 m 146 | -1 0.333 l 147 | -0.8 0 l 148 | -1 -0.333 l 149 | h 150 | 151 | 152 | 153 | 154 | -1 0.333 m 155 | 0 0 l 156 | -1 -0.333 l 157 | 158 | 159 | 160 | 161 | 0 0 m 162 | -1 0.333 l 163 | -1 -0.333 l 164 | h 165 | -1 0 m 166 | -2 0.333 l 167 | -2 -0.333 l 168 | h 169 | 170 | 171 | 172 | 173 | 0 0 m 174 | -1 0.333 l 175 | -1 -0.333 l 176 | h 177 | -1 0 m 178 | -2 0.333 l 179 | -2 -0.333 l 180 | h 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 64 672 m 255 | 112 704 l 256 | 128 768 l 257 | 64 672 l 258 | 64 672 l 259 | 260 | $v$ 261 | $w$ 262 | $u$ 263 | 264 | 96.564 679.886 m 265 | 29.0081 0 0 29.0081 69.0716 670.631 92.1961 688.145 a 266 | 267 | 268 | 159.939 739.451 m 269 | 20.148 0 0 20.148 176 751.615 167.06 733.559 a 270 | 271 | $\chi$ 272 | $\alpha$ 273 | 274 | 275 | -------------------------------------------------------------------------------- /notes/media/l-expn.ipe: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0 0 m 9 | -1 0.333 l 10 | -1 -0.333 l 11 | h 12 | 13 | 14 | 15 | 16 | 0 0 m 17 | -1 0.333 l 18 | -1 -0.333 l 19 | h 20 | 21 | 22 | 23 | 24 | 0 0 m 25 | -1 0.333 l 26 | -0.8 0 l 27 | -1 -0.333 l 28 | h 29 | 30 | 31 | 32 | 33 | 0 0 m 34 | -1 0.333 l 35 | -0.8 0 l 36 | -1 -0.333 l 37 | h 38 | 39 | 40 | 41 | 42 | 0.6 0 0 0.6 0 0 e 43 | 0.4 0 0 0.4 0 0 e 44 | 45 | 46 | 47 | 48 | 0.6 0 0 0.6 0 0 e 49 | 50 | 51 | 52 | 53 | 54 | 0.5 0 0 0.5 0 0 e 55 | 56 | 57 | 0.6 0 0 0.6 0 0 e 58 | 0.4 0 0 0.4 0 0 e 59 | 60 | 61 | 62 | 63 | 64 | -0.6 -0.6 m 65 | 0.6 -0.6 l 66 | 0.6 0.6 l 67 | -0.6 0.6 l 68 | h 69 | -0.4 -0.4 m 70 | 0.4 -0.4 l 71 | 0.4 0.4 l 72 | -0.4 0.4 l 73 | h 74 | 75 | 76 | 77 | 78 | -0.6 -0.6 m 79 | 0.6 -0.6 l 80 | 0.6 0.6 l 81 | -0.6 0.6 l 82 | h 83 | 84 | 85 | 86 | 87 | 88 | -0.5 -0.5 m 89 | 0.5 -0.5 l 90 | 0.5 0.5 l 91 | -0.5 0.5 l 92 | h 93 | 94 | 95 | -0.6 -0.6 m 96 | 0.6 -0.6 l 97 | 0.6 0.6 l 98 | -0.6 0.6 l 99 | h 100 | -0.4 -0.4 m 101 | 0.4 -0.4 l 102 | 0.4 0.4 l 103 | -0.4 0.4 l 104 | h 105 | 106 | 107 | 108 | 109 | 110 | 111 | -0.43 -0.57 m 112 | 0.57 0.43 l 113 | 0.43 0.57 l 114 | -0.57 -0.43 l 115 | h 116 | 117 | 118 | -0.43 0.57 m 119 | 0.57 -0.43 l 120 | 0.43 -0.57 l 121 | -0.57 0.43 l 122 | h 123 | 124 | 125 | 126 | 127 | 128 | 0 0 m 129 | -1 0.333 l 130 | -1 -0.333 l 131 | h 132 | 133 | 134 | 135 | 136 | 0 0 m 137 | -1 0.333 l 138 | -0.8 0 l 139 | -1 -0.333 l 140 | h 141 | 142 | 143 | 144 | 145 | 0 0 m 146 | -1 0.333 l 147 | -0.8 0 l 148 | -1 -0.333 l 149 | h 150 | 151 | 152 | 153 | 154 | -1 0.333 m 155 | 0 0 l 156 | -1 -0.333 l 157 | 158 | 159 | 160 | 161 | 0 0 m 162 | -1 0.333 l 163 | -1 -0.333 l 164 | h 165 | -1 0 m 166 | -2 0.333 l 167 | -2 -0.333 l 168 | h 169 | 170 | 171 | 172 | 173 | 0 0 m 174 | -1 0.333 l 175 | -1 -0.333 l 176 | h 177 | -1 0 m 178 | -2 0.333 l 179 | -2 -0.333 l 180 | h 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | $t$ 254 | 255 | 64 672 m 256 | 112 704 l 257 | 128 768 l 258 | 64 672 l 259 | 64 672 l 260 | 261 | 262 | 64 672 m 263 | 112 672 l 264 | 112 672 l 265 | 266 | 267 | 85.202 672 m 268 | 21.2069 0 0 21.2069 64 671.543 75.7635 689.189 a 269 | 270 | $\theta_{sc}$ 271 | 272 | 105.29 672 m 273 | 41.2929 0 0 41.2929 64 671.543 98.3578 694.449 a 274 | 275 | $\theta_{tc}$ 276 | $c$ 277 | $s$ 278 | 279 | 280 | -------------------------------------------------------------------------------- /notes/media/local-expansion.ipe: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0 0 m 9 | -1 0.333 l 10 | -1 -0.333 l 11 | h 12 | 13 | 14 | 15 | 16 | 0 0 m 17 | -1 0.333 l 18 | -1 -0.333 l 19 | h 20 | 21 | 22 | 23 | 24 | 0 0 m 25 | -1 0.333 l 26 | -0.8 0 l 27 | -1 -0.333 l 28 | h 29 | 30 | 31 | 32 | 33 | 0 0 m 34 | -1 0.333 l 35 | -0.8 0 l 36 | -1 -0.333 l 37 | h 38 | 39 | 40 | 41 | 42 | 0.6 0 0 0.6 0 0 e 43 | 0.4 0 0 0.4 0 0 e 44 | 45 | 46 | 47 | 48 | 0.6 0 0 0.6 0 0 e 49 | 50 | 51 | 52 | 53 | 54 | 0.5 0 0 0.5 0 0 e 55 | 56 | 57 | 0.6 0 0 0.6 0 0 e 58 | 0.4 0 0 0.4 0 0 e 59 | 60 | 61 | 62 | 63 | 64 | -0.6 -0.6 m 65 | 0.6 -0.6 l 66 | 0.6 0.6 l 67 | -0.6 0.6 l 68 | h 69 | -0.4 -0.4 m 70 | 0.4 -0.4 l 71 | 0.4 0.4 l 72 | -0.4 0.4 l 73 | h 74 | 75 | 76 | 77 | 78 | -0.6 -0.6 m 79 | 0.6 -0.6 l 80 | 0.6 0.6 l 81 | -0.6 0.6 l 82 | h 83 | 84 | 85 | 86 | 87 | 88 | -0.5 -0.5 m 89 | 0.5 -0.5 l 90 | 0.5 0.5 l 91 | -0.5 0.5 l 92 | h 93 | 94 | 95 | -0.6 -0.6 m 96 | 0.6 -0.6 l 97 | 0.6 0.6 l 98 | -0.6 0.6 l 99 | h 100 | -0.4 -0.4 m 101 | 0.4 -0.4 l 102 | 0.4 0.4 l 103 | -0.4 0.4 l 104 | h 105 | 106 | 107 | 108 | 109 | 110 | 111 | -0.43 -0.57 m 112 | 0.57 0.43 l 113 | 0.43 0.57 l 114 | -0.57 -0.43 l 115 | h 116 | 117 | 118 | -0.43 0.57 m 119 | 0.57 -0.43 l 120 | 0.43 -0.57 l 121 | -0.57 0.43 l 122 | h 123 | 124 | 125 | 126 | 127 | 128 | 0 0 m 129 | -1 0.333 l 130 | -1 -0.333 l 131 | h 132 | 133 | 134 | 135 | 136 | 0 0 m 137 | -1 0.333 l 138 | -0.8 0 l 139 | -1 -0.333 l 140 | h 141 | 142 | 143 | 144 | 145 | 0 0 m 146 | -1 0.333 l 147 | -0.8 0 l 148 | -1 -0.333 l 149 | h 150 | 151 | 152 | 153 | 154 | -1 0.333 m 155 | 0 0 l 156 | -1 -0.333 l 157 | 158 | 159 | 160 | 161 | 0 0 m 162 | -1 0.333 l 163 | -1 -0.333 l 164 | h 165 | -1 0 m 166 | -2 0.333 l 167 | -2 -0.333 l 168 | h 169 | 170 | 171 | 172 | 173 | 0 0 m 174 | -1 0.333 l 175 | -1 -0.333 l 176 | h 177 | -1 0 m 178 | -2 0.333 l 179 | -2 -0.333 l 180 | h 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | $t$ 254 | 255 | 64 672 m 256 | 112 704 l 257 | 128 768 l 258 | 64 672 l 259 | 64 672 l 260 | 261 | 262 | 64 672 m 263 | 112 672 l 264 | 112 672 l 265 | 266 | 267 | 85.202 672 m 268 | 21.2069 0 0 21.2069 64 671.543 75.7635 689.189 a 269 | 270 | $\theta_{sc}$ 271 | 272 | 105.29 672 m 273 | 41.2929 0 0 41.2929 64 671.543 98.3578 694.449 a 274 | 275 | $\theta_{tc}$ 276 | $c$ 277 | $s$ 278 | 279 | 280 | -------------------------------------------------------------------------------- /notes/media/m-expn.ipe: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0 0 m 9 | -1 0.333 l 10 | -1 -0.333 l 11 | h 12 | 13 | 14 | 15 | 16 | 0 0 m 17 | -1 0.333 l 18 | -1 -0.333 l 19 | h 20 | 21 | 22 | 23 | 24 | 0 0 m 25 | -1 0.333 l 26 | -0.8 0 l 27 | -1 -0.333 l 28 | h 29 | 30 | 31 | 32 | 33 | 0 0 m 34 | -1 0.333 l 35 | -0.8 0 l 36 | -1 -0.333 l 37 | h 38 | 39 | 40 | 41 | 42 | 0.6 0 0 0.6 0 0 e 43 | 0.4 0 0 0.4 0 0 e 44 | 45 | 46 | 47 | 48 | 0.6 0 0 0.6 0 0 e 49 | 50 | 51 | 52 | 53 | 54 | 0.5 0 0 0.5 0 0 e 55 | 56 | 57 | 0.6 0 0 0.6 0 0 e 58 | 0.4 0 0 0.4 0 0 e 59 | 60 | 61 | 62 | 63 | 64 | -0.6 -0.6 m 65 | 0.6 -0.6 l 66 | 0.6 0.6 l 67 | -0.6 0.6 l 68 | h 69 | -0.4 -0.4 m 70 | 0.4 -0.4 l 71 | 0.4 0.4 l 72 | -0.4 0.4 l 73 | h 74 | 75 | 76 | 77 | 78 | -0.6 -0.6 m 79 | 0.6 -0.6 l 80 | 0.6 0.6 l 81 | -0.6 0.6 l 82 | h 83 | 84 | 85 | 86 | 87 | 88 | -0.5 -0.5 m 89 | 0.5 -0.5 l 90 | 0.5 0.5 l 91 | -0.5 0.5 l 92 | h 93 | 94 | 95 | -0.6 -0.6 m 96 | 0.6 -0.6 l 97 | 0.6 0.6 l 98 | -0.6 0.6 l 99 | h 100 | -0.4 -0.4 m 101 | 0.4 -0.4 l 102 | 0.4 0.4 l 103 | -0.4 0.4 l 104 | h 105 | 106 | 107 | 108 | 109 | 110 | 111 | -0.43 -0.57 m 112 | 0.57 0.43 l 113 | 0.43 0.57 l 114 | -0.57 -0.43 l 115 | h 116 | 117 | 118 | -0.43 0.57 m 119 | 0.57 -0.43 l 120 | 0.43 -0.57 l 121 | -0.57 0.43 l 122 | h 123 | 124 | 125 | 126 | 127 | 128 | 0 0 m 129 | -1 0.333 l 130 | -1 -0.333 l 131 | h 132 | 133 | 134 | 135 | 136 | 0 0 m 137 | -1 0.333 l 138 | -0.8 0 l 139 | -1 -0.333 l 140 | h 141 | 142 | 143 | 144 | 145 | 0 0 m 146 | -1 0.333 l 147 | -0.8 0 l 148 | -1 -0.333 l 149 | h 150 | 151 | 152 | 153 | 154 | -1 0.333 m 155 | 0 0 l 156 | -1 -0.333 l 157 | 158 | 159 | 160 | 161 | 0 0 m 162 | -1 0.333 l 163 | -1 -0.333 l 164 | h 165 | -1 0 m 166 | -2 0.333 l 167 | -2 -0.333 l 168 | h 169 | 170 | 171 | 172 | 173 | 0 0 m 174 | -1 0.333 l 175 | -1 -0.333 l 176 | h 177 | -1 0 m 178 | -2 0.333 l 179 | -2 -0.333 l 180 | h 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | $s$ 254 | 255 | 64 672 m 256 | 112 704 l 257 | 128 768 l 258 | 64 672 l 259 | 64 672 l 260 | 261 | 262 | 64 672 m 263 | 112 672 l 264 | 112 672 l 265 | 266 | 267 | 85.202 672 m 268 | 21.2069 0 0 21.2069 64 671.543 75.7635 689.189 a 269 | 270 | $\theta_{tc}$ 271 | 272 | 105.29 672 m 273 | 41.2929 0 0 41.2929 64 671.543 98.3578 694.449 a 274 | 275 | $\theta_{sc}$ 276 | $c$ 277 | $t$ 278 | 279 | 280 | -------------------------------------------------------------------------------- /notes/media/m2l-translation.ipe: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0 0 m 9 | -1 0.333 l 10 | -1 -0.333 l 11 | h 12 | 13 | 14 | 15 | 16 | 0 0 m 17 | -1 0.333 l 18 | -1 -0.333 l 19 | h 20 | 21 | 22 | 23 | 24 | 0 0 m 25 | -1 0.333 l 26 | -0.8 0 l 27 | -1 -0.333 l 28 | h 29 | 30 | 31 | 32 | 33 | 0 0 m 34 | -1 0.333 l 35 | -0.8 0 l 36 | -1 -0.333 l 37 | h 38 | 39 | 40 | 41 | 42 | 0.6 0 0 0.6 0 0 e 43 | 0.4 0 0 0.4 0 0 e 44 | 45 | 46 | 47 | 48 | 0.6 0 0 0.6 0 0 e 49 | 50 | 51 | 52 | 53 | 54 | 0.5 0 0 0.5 0 0 e 55 | 56 | 57 | 0.6 0 0 0.6 0 0 e 58 | 0.4 0 0 0.4 0 0 e 59 | 60 | 61 | 62 | 63 | 64 | -0.6 -0.6 m 65 | 0.6 -0.6 l 66 | 0.6 0.6 l 67 | -0.6 0.6 l 68 | h 69 | -0.4 -0.4 m 70 | 0.4 -0.4 l 71 | 0.4 0.4 l 72 | -0.4 0.4 l 73 | h 74 | 75 | 76 | 77 | 78 | -0.6 -0.6 m 79 | 0.6 -0.6 l 80 | 0.6 0.6 l 81 | -0.6 0.6 l 82 | h 83 | 84 | 85 | 86 | 87 | 88 | -0.5 -0.5 m 89 | 0.5 -0.5 l 90 | 0.5 0.5 l 91 | -0.5 0.5 l 92 | h 93 | 94 | 95 | -0.6 -0.6 m 96 | 0.6 -0.6 l 97 | 0.6 0.6 l 98 | -0.6 0.6 l 99 | h 100 | -0.4 -0.4 m 101 | 0.4 -0.4 l 102 | 0.4 0.4 l 103 | -0.4 0.4 l 104 | h 105 | 106 | 107 | 108 | 109 | 110 | 111 | -0.43 -0.57 m 112 | 0.57 0.43 l 113 | 0.43 0.57 l 114 | -0.57 -0.43 l 115 | h 116 | 117 | 118 | -0.43 0.57 m 119 | 0.57 -0.43 l 120 | 0.43 -0.57 l 121 | -0.57 0.43 l 122 | h 123 | 124 | 125 | 126 | 127 | 128 | 0 0 m 129 | -1 0.333 l 130 | -1 -0.333 l 131 | h 132 | 133 | 134 | 135 | 136 | 0 0 m 137 | -1 0.333 l 138 | -0.8 0 l 139 | -1 -0.333 l 140 | h 141 | 142 | 143 | 144 | 145 | 0 0 m 146 | -1 0.333 l 147 | -0.8 0 l 148 | -1 -0.333 l 149 | h 150 | 151 | 152 | 153 | 154 | -1 0.333 m 155 | 0 0 l 156 | -1 -0.333 l 157 | 158 | 159 | 160 | 161 | 0 0 m 162 | -1 0.333 l 163 | -1 -0.333 l 164 | h 165 | -1 0 m 166 | -2 0.333 l 167 | -2 -0.333 l 168 | h 169 | 170 | 171 | 172 | 173 | 0 0 m 174 | -1 0.333 l 175 | -1 -0.333 l 176 | h 177 | -1 0 m 178 | -2 0.333 l 179 | -2 -0.333 l 180 | h 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | $s$ 254 | 255 | 64 672 m 256 | 112 704 l 257 | 128 768 l 258 | 64 672 l 259 | 64 672 l 260 | 261 | 262 | 64 672 m 263 | 112 672 l 264 | 112 672 l 265 | 266 | $\theta_{c_2c_1}$ 267 | 268 | 105.29 672 m 269 | 41.2929 0 0 41.2929 64 671.543 98.3578 694.449 a 270 | 271 | $\theta_{sc_1}$ 272 | $c_1$ 273 | $c_2$ 274 | 275 | 117.726 746.478 m 276 | 117.726 746.478 l 277 | 117.726 746.478 l 278 | 279 | 280 | 124.677 754.25 m 281 | 13.703 0 0 13.703 128 767.543 141.697 767.934 a 282 | 283 | $-\theta_{sc_2}$ 284 | 285 | 96 768 m 286 | 160 768 l 287 | 288 | 289 | 113.964 768 m 290 | 14.0436 0 0 14.0436 128 767.543 120.21 755.858 a 291 | 292 | 293 | 80.0015 672 m 294 | 15.3166 0 0 15.3166 64.6848 672 72.8876 684.935 a 295 | 296 | $\theta_{c_2c_1}$ 297 | 298 | 299 | -------------------------------------------------------------------------------- /notes/media/m2m-l2l-translation.ipe: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0 0 m 9 | -1 0.333 l 10 | -1 -0.333 l 11 | h 12 | 13 | 14 | 15 | 16 | 0 0 m 17 | -1 0.333 l 18 | -1 -0.333 l 19 | h 20 | 21 | 22 | 23 | 24 | 0 0 m 25 | -1 0.333 l 26 | -0.8 0 l 27 | -1 -0.333 l 28 | h 29 | 30 | 31 | 32 | 33 | 0 0 m 34 | -1 0.333 l 35 | -0.8 0 l 36 | -1 -0.333 l 37 | h 38 | 39 | 40 | 41 | 42 | 0.6 0 0 0.6 0 0 e 43 | 0.4 0 0 0.4 0 0 e 44 | 45 | 46 | 47 | 48 | 0.6 0 0 0.6 0 0 e 49 | 50 | 51 | 52 | 53 | 54 | 0.5 0 0 0.5 0 0 e 55 | 56 | 57 | 0.6 0 0 0.6 0 0 e 58 | 0.4 0 0 0.4 0 0 e 59 | 60 | 61 | 62 | 63 | 64 | -0.6 -0.6 m 65 | 0.6 -0.6 l 66 | 0.6 0.6 l 67 | -0.6 0.6 l 68 | h 69 | -0.4 -0.4 m 70 | 0.4 -0.4 l 71 | 0.4 0.4 l 72 | -0.4 0.4 l 73 | h 74 | 75 | 76 | 77 | 78 | -0.6 -0.6 m 79 | 0.6 -0.6 l 80 | 0.6 0.6 l 81 | -0.6 0.6 l 82 | h 83 | 84 | 85 | 86 | 87 | 88 | -0.5 -0.5 m 89 | 0.5 -0.5 l 90 | 0.5 0.5 l 91 | -0.5 0.5 l 92 | h 93 | 94 | 95 | -0.6 -0.6 m 96 | 0.6 -0.6 l 97 | 0.6 0.6 l 98 | -0.6 0.6 l 99 | h 100 | -0.4 -0.4 m 101 | 0.4 -0.4 l 102 | 0.4 0.4 l 103 | -0.4 0.4 l 104 | h 105 | 106 | 107 | 108 | 109 | 110 | 111 | -0.43 -0.57 m 112 | 0.57 0.43 l 113 | 0.43 0.57 l 114 | -0.57 -0.43 l 115 | h 116 | 117 | 118 | -0.43 0.57 m 119 | 0.57 -0.43 l 120 | 0.43 -0.57 l 121 | -0.57 0.43 l 122 | h 123 | 124 | 125 | 126 | 127 | 128 | 0 0 m 129 | -1 0.333 l 130 | -1 -0.333 l 131 | h 132 | 133 | 134 | 135 | 136 | 0 0 m 137 | -1 0.333 l 138 | -0.8 0 l 139 | -1 -0.333 l 140 | h 141 | 142 | 143 | 144 | 145 | 0 0 m 146 | -1 0.333 l 147 | -0.8 0 l 148 | -1 -0.333 l 149 | h 150 | 151 | 152 | 153 | 154 | -1 0.333 m 155 | 0 0 l 156 | -1 -0.333 l 157 | 158 | 159 | 160 | 161 | 0 0 m 162 | -1 0.333 l 163 | -1 -0.333 l 164 | h 165 | -1 0 m 166 | -2 0.333 l 167 | -2 -0.333 l 168 | h 169 | 170 | 171 | 172 | 173 | 0 0 m 174 | -1 0.333 l 175 | -1 -0.333 l 176 | h 177 | -1 0 m 178 | -2 0.333 l 179 | -2 -0.333 l 180 | h 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | $c_2$ 254 | 255 | 64 672 m 256 | 112 704 l 257 | 128 768 l 258 | 64 672 l 259 | 64 672 l 260 | 261 | 262 | 64 672 m 263 | 112 672 l 264 | 112 672 l 265 | 266 | 267 | 85.202 672 m 268 | 21.2069 0 0 21.2069 64 671.543 75.7635 689.189 a 269 | 270 | $\theta_{sc_1}$ 271 | 272 | 105.29 672 m 273 | 41.2929 0 0 41.2929 64 671.543 98.3578 694.449 a 274 | 275 | $\theta_{c_2 c_1}$ 276 | $c_1$ 277 | $s$ 278 | 279 | 112 704 m 280 | 160 704 l 281 | 160 704 l 282 | 283 | 284 | 127.256 704 m 285 | 15.1606 0 0 15.1606 112.095 703.925 115.772 718.633 a 286 | 287 | $\theta_{sc_2}$ 288 | 289 | 117.726 746.478 m 290 | 117.726 746.478 l 291 | 117.726 746.478 l 292 | 293 | 294 | 115.861 749.335 m 295 | 21.8841 0 0 21.8841 128 767.543 122.692 746.313 a 296 | 297 | 298 | 86.0448 730.155 m 299 | 116.106 741.087 l 300 | 301 | $\theta_{sc_2} - \theta_{sc_1}$ 302 | 303 | 304 | -------------------------------------------------------------------------------- /notes/media/m2m-translation.ipe: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0 0 m 9 | -1 0.333 l 10 | -1 -0.333 l 11 | h 12 | 13 | 14 | 15 | 16 | 0 0 m 17 | -1 0.333 l 18 | -1 -0.333 l 19 | h 20 | 21 | 22 | 23 | 24 | 0 0 m 25 | -1 0.333 l 26 | -0.8 0 l 27 | -1 -0.333 l 28 | h 29 | 30 | 31 | 32 | 33 | 0 0 m 34 | -1 0.333 l 35 | -0.8 0 l 36 | -1 -0.333 l 37 | h 38 | 39 | 40 | 41 | 42 | 0.6 0 0 0.6 0 0 e 43 | 0.4 0 0 0.4 0 0 e 44 | 45 | 46 | 47 | 48 | 0.6 0 0 0.6 0 0 e 49 | 50 | 51 | 52 | 53 | 54 | 0.5 0 0 0.5 0 0 e 55 | 56 | 57 | 0.6 0 0 0.6 0 0 e 58 | 0.4 0 0 0.4 0 0 e 59 | 60 | 61 | 62 | 63 | 64 | -0.6 -0.6 m 65 | 0.6 -0.6 l 66 | 0.6 0.6 l 67 | -0.6 0.6 l 68 | h 69 | -0.4 -0.4 m 70 | 0.4 -0.4 l 71 | 0.4 0.4 l 72 | -0.4 0.4 l 73 | h 74 | 75 | 76 | 77 | 78 | -0.6 -0.6 m 79 | 0.6 -0.6 l 80 | 0.6 0.6 l 81 | -0.6 0.6 l 82 | h 83 | 84 | 85 | 86 | 87 | 88 | -0.5 -0.5 m 89 | 0.5 -0.5 l 90 | 0.5 0.5 l 91 | -0.5 0.5 l 92 | h 93 | 94 | 95 | -0.6 -0.6 m 96 | 0.6 -0.6 l 97 | 0.6 0.6 l 98 | -0.6 0.6 l 99 | h 100 | -0.4 -0.4 m 101 | 0.4 -0.4 l 102 | 0.4 0.4 l 103 | -0.4 0.4 l 104 | h 105 | 106 | 107 | 108 | 109 | 110 | 111 | -0.43 -0.57 m 112 | 0.57 0.43 l 113 | 0.43 0.57 l 114 | -0.57 -0.43 l 115 | h 116 | 117 | 118 | -0.43 0.57 m 119 | 0.57 -0.43 l 120 | 0.43 -0.57 l 121 | -0.57 0.43 l 122 | h 123 | 124 | 125 | 126 | 127 | 128 | 0 0 m 129 | -1 0.333 l 130 | -1 -0.333 l 131 | h 132 | 133 | 134 | 135 | 136 | 0 0 m 137 | -1 0.333 l 138 | -0.8 0 l 139 | -1 -0.333 l 140 | h 141 | 142 | 143 | 144 | 145 | 0 0 m 146 | -1 0.333 l 147 | -0.8 0 l 148 | -1 -0.333 l 149 | h 150 | 151 | 152 | 153 | 154 | -1 0.333 m 155 | 0 0 l 156 | -1 -0.333 l 157 | 158 | 159 | 160 | 161 | 0 0 m 162 | -1 0.333 l 163 | -1 -0.333 l 164 | h 165 | -1 0 m 166 | -2 0.333 l 167 | -2 -0.333 l 168 | h 169 | 170 | 171 | 172 | 173 | 0 0 m 174 | -1 0.333 l 175 | -1 -0.333 l 176 | h 177 | -1 0 m 178 | -2 0.333 l 179 | -2 -0.333 l 180 | h 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | $c_2$ 254 | 255 | 64 672 m 256 | 112 704 l 257 | 128 768 l 258 | 64 672 l 259 | 64 672 l 260 | 261 | 262 | 64 672 m 263 | 112 672 l 264 | 112 672 l 265 | 266 | 267 | 85.202 672 m 268 | 21.2069 0 0 21.2069 64 671.543 75.7635 689.189 a 269 | 270 | $\theta_{sc_1}$ 271 | 272 | 105.29 672 m 273 | 41.2929 0 0 41.2929 64 671.543 98.3578 694.449 a 274 | 275 | $\theta_{c_2 c_1}$ 276 | $c_1$ 277 | $s$ 278 | 279 | 112 704 m 280 | 160 704 l 281 | 160 704 l 282 | 283 | 284 | 127.256 704 m 285 | 15.1606 0 0 15.1606 112.095 703.925 115.772 718.633 a 286 | 287 | $\theta_{sc_2}$ 288 | 289 | 117.726 746.478 m 290 | 117.726 746.478 l 291 | 117.726 746.478 l 292 | 293 | 294 | 115.861 749.335 m 295 | 21.8841 0 0 21.8841 128 767.543 122.692 746.313 a 296 | 297 | 298 | 86.0448 730.155 m 299 | 116.106 741.087 l 300 | 301 | $\theta_{sc_2} - \theta_{sc_1}$ 302 | 303 | 304 | -------------------------------------------------------------------------------- /notes/media/multipole.ipe: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0 0 m 9 | -1 0.333 l 10 | -1 -0.333 l 11 | h 12 | 13 | 14 | 15 | 16 | 0 0 m 17 | -1 0.333 l 18 | -1 -0.333 l 19 | h 20 | 21 | 22 | 23 | 24 | 0 0 m 25 | -1 0.333 l 26 | -0.8 0 l 27 | -1 -0.333 l 28 | h 29 | 30 | 31 | 32 | 33 | 0 0 m 34 | -1 0.333 l 35 | -0.8 0 l 36 | -1 -0.333 l 37 | h 38 | 39 | 40 | 41 | 42 | 0.6 0 0 0.6 0 0 e 43 | 0.4 0 0 0.4 0 0 e 44 | 45 | 46 | 47 | 48 | 0.6 0 0 0.6 0 0 e 49 | 50 | 51 | 52 | 53 | 54 | 0.5 0 0 0.5 0 0 e 55 | 56 | 57 | 0.6 0 0 0.6 0 0 e 58 | 0.4 0 0 0.4 0 0 e 59 | 60 | 61 | 62 | 63 | 64 | -0.6 -0.6 m 65 | 0.6 -0.6 l 66 | 0.6 0.6 l 67 | -0.6 0.6 l 68 | h 69 | -0.4 -0.4 m 70 | 0.4 -0.4 l 71 | 0.4 0.4 l 72 | -0.4 0.4 l 73 | h 74 | 75 | 76 | 77 | 78 | -0.6 -0.6 m 79 | 0.6 -0.6 l 80 | 0.6 0.6 l 81 | -0.6 0.6 l 82 | h 83 | 84 | 85 | 86 | 87 | 88 | -0.5 -0.5 m 89 | 0.5 -0.5 l 90 | 0.5 0.5 l 91 | -0.5 0.5 l 92 | h 93 | 94 | 95 | -0.6 -0.6 m 96 | 0.6 -0.6 l 97 | 0.6 0.6 l 98 | -0.6 0.6 l 99 | h 100 | -0.4 -0.4 m 101 | 0.4 -0.4 l 102 | 0.4 0.4 l 103 | -0.4 0.4 l 104 | h 105 | 106 | 107 | 108 | 109 | 110 | 111 | -0.43 -0.57 m 112 | 0.57 0.43 l 113 | 0.43 0.57 l 114 | -0.57 -0.43 l 115 | h 116 | 117 | 118 | -0.43 0.57 m 119 | 0.57 -0.43 l 120 | 0.43 -0.57 l 121 | -0.57 0.43 l 122 | h 123 | 124 | 125 | 126 | 127 | 128 | 0 0 m 129 | -1 0.333 l 130 | -1 -0.333 l 131 | h 132 | 133 | 134 | 135 | 136 | 0 0 m 137 | -1 0.333 l 138 | -0.8 0 l 139 | -1 -0.333 l 140 | h 141 | 142 | 143 | 144 | 145 | 0 0 m 146 | -1 0.333 l 147 | -0.8 0 l 148 | -1 -0.333 l 149 | h 150 | 151 | 152 | 153 | 154 | -1 0.333 m 155 | 0 0 l 156 | -1 -0.333 l 157 | 158 | 159 | 160 | 161 | 0 0 m 162 | -1 0.333 l 163 | -1 -0.333 l 164 | h 165 | -1 0 m 166 | -2 0.333 l 167 | -2 -0.333 l 168 | h 169 | 170 | 171 | 172 | 173 | 0 0 m 174 | -1 0.333 l 175 | -1 -0.333 l 176 | h 177 | -1 0 m 178 | -2 0.333 l 179 | -2 -0.333 l 180 | h 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | $s$ 254 | 255 | 64 672 m 256 | 112 704 l 257 | 128 768 l 258 | 64 672 l 259 | 64 672 l 260 | 261 | 262 | 64 672 m 263 | 112 672 l 264 | 112 672 l 265 | 266 | 267 | 85.202 672 m 268 | 21.2069 0 0 21.2069 64 671.543 75.7635 689.189 a 269 | 270 | $\theta_{tc}$ 271 | 272 | 105.29 672 m 273 | 41.2929 0 0 41.2929 64 671.543 98.3578 694.449 a 274 | 275 | $\theta_{sc}$ 276 | $c$ 277 | $t$ 278 | 279 | 280 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "sumpy" 7 | version = "2024.0" 8 | description = "Fast summation in Python" 9 | readme = "README.rst" 10 | license = { text = "MIT" } 11 | authors = [ 12 | { name = "Andreas Kloeckner", email = "inform@tiker.net" }, 13 | ] 14 | requires-python = ">=3.10" 15 | classifiers = [ 16 | "Development Status :: 3 - Alpha", 17 | "Intended Audience :: Developers", 18 | "Intended Audience :: Other Audience", 19 | "Intended Audience :: Science/Research", 20 | "License :: OSI Approved :: MIT License", 21 | "Natural Language :: English", 22 | "Programming Language :: Python", 23 | "Programming Language :: Python :: 3 :: Only", 24 | "Topic :: Scientific/Engineering", 25 | "Topic :: Scientific/Engineering :: Information Analysis", 26 | "Topic :: Scientific/Engineering :: Mathematics", 27 | "Topic :: Scientific/Engineering :: Visualization", 28 | "Topic :: Software Development :: Libraries", 29 | "Topic :: Utilities", 30 | ] 31 | dependencies = [ 32 | "arraycontext>=2021.1", 33 | "boxtree>=2023.1", 34 | "constantdict>=2024.4", 35 | "loopy>=2024.1", 36 | "numpy", 37 | "pyopencl>=2022.1", 38 | "pytools>=2024.1", 39 | "pymbolic>=2024.2", 40 | "sympy>=0.7.2", 41 | ] 42 | 43 | [dependency-groups] 44 | dev = [ 45 | {include-group = "doc"}, 46 | {include-group = "test"}, 47 | {include-group = "lint"}, 48 | ] 49 | lint = [ 50 | "pylint", 51 | # https://github.com/astral-sh/ruff/issues/16943 52 | "ruff!=0.11.1,!=0.11.2", 53 | ] 54 | doc = [ 55 | "furo", 56 | "sphinx-copybutton", 57 | "sphinx>=4", 58 | ] 59 | test = [ 60 | "pytest", 61 | ] 62 | 63 | [project.optional-dependencies] 64 | fmmlib = [ 65 | "pyfmmlib>=2023.1", 66 | ] 67 | symengine = [ 68 | "symengine>=0.9.0", 69 | ] 70 | pyvkfft = [ 71 | "pyvkfft>=2024.1", 72 | ] 73 | 74 | [project.urls] 75 | Documentation = "https://documen.tician.de/sumpy" 76 | Repository = "https://github.com/inducer/sumpy" 77 | 78 | [tool.ruff] 79 | preview = true 80 | 81 | [tool.ruff.lint] 82 | extend-select = [ 83 | "B", # flake8-bugbear 84 | "C", # flake8-comprehensions 85 | "E", # pycodestyle 86 | "F", # pyflakes 87 | "G", # flake8-logging-format 88 | "I", # flake8-isort 89 | "N", # pep8-naming 90 | "NPY", # numpy 91 | "Q", # flake8-quotes 92 | "RUF", # ruff 93 | "SIM", # flake8-simplify 94 | "UP", # pyupgrade 95 | "W", # pycodestyle 96 | ] 97 | extend-ignore = [ 98 | "C90", # McCabe complexity 99 | "E221", # multiple spaces before operator 100 | "E226", # missing whitespace around arithmetic operator 101 | "E402", # module-level import not at top of file 102 | ] 103 | 104 | [tool.ruff.lint.flake8-quotes] 105 | docstring-quotes = "double" 106 | inline-quotes = "double" 107 | multiline-quotes = "double" 108 | 109 | [tool.ruff.lint.isort] 110 | combine-as-imports = true 111 | known-first-party = [ 112 | "arraycontext", 113 | "loopy", 114 | "pymbolic", 115 | "pyopencl", 116 | "pytools", 117 | ] 118 | known-local-folder = [ 119 | "sumpy", 120 | ] 121 | lines-after-imports = 2 122 | required-imports = ["from __future__ import annotations"] 123 | 124 | [tool.ruff.lint.per-file-ignores] 125 | "doc/**/*.py" = ["I002"] 126 | "examples/**/*.py" = ["I002"] 127 | 128 | [tool.typos.default] 129 | extend-ignore-re = [ 130 | "(?Rm)^.*(#|//)\\s*spellchecker:\\s*disable-line$" 131 | ] 132 | 133 | [tool.typos.default.extend-words] 134 | # short for multi-indices 135 | mis = "mis" 136 | # short for n-dimensional 137 | nd = "nd" 138 | # short for Theorem 139 | thm = "thm" 140 | 141 | [tool.typos.files] 142 | extend-exclude = [ 143 | "contrib/*/*.ipynb", 144 | "notes/*/*.eps", 145 | ] 146 | 147 | [tool.mypy] 148 | python_version = "3.10" 149 | warn_unused_ignores = true 150 | 151 | [[tool.mypy.overrides]] 152 | module = [ 153 | "boxtree.*", 154 | "loopy.*", 155 | "matplotlib.*", 156 | "mayavi.*", 157 | "pyfmmlib.*", 158 | "pymbolic.*", 159 | "pyopencl.*", 160 | "pyvisfile.*", 161 | "pyvkfft.*", 162 | "scipy.*", 163 | "symengine.*", 164 | "sympy.*", 165 | ] 166 | ignore_missing_imports = true 167 | 168 | [tool.pytest.ini_options] 169 | markers = [ 170 | "mpi: tests distributed FMM", 171 | ] 172 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | sympy 3 | constantdict 4 | pyvkfft 5 | 6 | # used in mpi-based tests 7 | platformdirs 8 | 9 | git+https://github.com/inducer/pytools.git#egg=pytools 10 | git+https://github.com/inducer/pymbolic.git#egg=pymbolic 11 | git+https://github.com/inducer/islpy.git#egg=islpy 12 | git+https://github.com/inducer/pyopencl.git#egg=pyopencl 13 | git+https://github.com/inducer/boxtree.git#egg=boxtree 14 | git+https://github.com/inducer/loopy.git#egg=loopy 15 | git+https://github.com/inducer/arraycontext.git#egg=arraycontext 16 | git+https://github.com/inducer/pyfmmlib.git#egg=pyfmmlib 17 | -------------------------------------------------------------------------------- /sumpy/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | __copyright__ = "Copyright (C) 2013 Andreas Kloeckner" 5 | 6 | __license__ = """ 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import os 27 | from collections.abc import Hashable 28 | 29 | import loopy as lp 30 | from pytools.persistent_dict import WriteOncePersistentDict 31 | 32 | from sumpy.e2e import ( 33 | E2EFromChildren, 34 | E2EFromCSR, 35 | E2EFromParent, 36 | M2LGenerateTranslationClassesDependentData, 37 | M2LPostprocessLocal, 38 | M2LPreprocessMultipole, 39 | M2LUsingTranslationClassesDependentData, 40 | ) 41 | from sumpy.e2p import E2PFromCSR, E2PFromSingleBox 42 | from sumpy.p2e import P2EFromCSR, P2EFromSingleBox 43 | from sumpy.p2p import P2P, P2PFromCSR 44 | from sumpy.version import VERSION_TEXT 45 | 46 | 47 | __all__ = [ 48 | "P2P", 49 | "E2EFromCSR", 50 | "E2EFromChildren", 51 | "E2EFromParent", 52 | "E2PFromCSR", 53 | "E2PFromSingleBox", 54 | "M2LGenerateTranslationClassesDependentData", 55 | "M2LPostprocessLocal", 56 | "M2LPreprocessMultipole", 57 | "M2LUsingTranslationClassesDependentData", 58 | "P2EFromCSR", 59 | "P2EFromSingleBox", 60 | "P2PFromCSR", 61 | ] 62 | 63 | 64 | code_cache: WriteOncePersistentDict[Hashable, lp.TranslationUnit] = \ 65 | WriteOncePersistentDict("sumpy-code-cache-v6-"+VERSION_TEXT, safe_sync=False) 66 | 67 | 68 | # {{{ optimization control 69 | 70 | OPT_ENABLED = True 71 | 72 | OPT_ENABLED = "SUMPY_NO_OPT" not in os.environ 73 | 74 | 75 | def set_optimization_enabled(flag): 76 | """Set whether the :mod:`loopy` kernels should be optimized.""" 77 | global OPT_ENABLED 78 | OPT_ENABLED = flag 79 | 80 | # }}} 81 | 82 | 83 | # {{{ cache control 84 | 85 | CACHING_ENABLED = True 86 | 87 | CACHING_ENABLED = ( 88 | "SUMPY_NO_CACHE" not in os.environ 89 | and "CG_NO_CACHE" not in os.environ) 90 | 91 | NO_CACHE_KERNELS = tuple(os.environ.get("SUMPY_NO_CACHE_KERNELS", 92 | "").split(",")) 93 | 94 | 95 | def set_caching_enabled(flag, no_cache_kernels=()): 96 | """Set whether :mod:`loopy` is allowed to use disk caching for its various 97 | code generation stages. 98 | """ 99 | global CACHING_ENABLED, NO_CACHE_KERNELS 100 | NO_CACHE_KERNELS = no_cache_kernels 101 | CACHING_ENABLED = flag 102 | 103 | 104 | class CacheMode: 105 | """A context manager for setting whether :mod:`sumpy` is allowed to use 106 | disk caches. 107 | """ 108 | 109 | def __init__(self, new_flag, new_no_cache_kernels=()): 110 | self.new_flag = new_flag 111 | self.new_no_cache_kernels = new_no_cache_kernels 112 | 113 | def __enter__(self): 114 | global CACHING_ENABLED, NO_CACHE_KERNELS 115 | self.previous_flag = CACHING_ENABLED 116 | self.previous_kernels = NO_CACHE_KERNELS 117 | set_caching_enabled(self.new_flag, self.new_no_cache_kernels) 118 | 119 | def __exit__(self, exc_type, exc_val, exc_tb): 120 | set_caching_enabled(self.previous_flag, self.previous_kernels) 121 | del self.previous_flag 122 | del self.previous_kernels 123 | 124 | # }}} 125 | -------------------------------------------------------------------------------- /sumpy/array_context.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | __copyright__ = "Copyright (C) 2022 Alexandru Fikl" 5 | 6 | __license__ = """ 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | from boxtree.array_context import PyOpenCLArrayContext as PyOpenCLArrayContextBase 27 | 28 | from arraycontext.pytest import ( 29 | _PytestPyOpenCLArrayContextFactoryWithClass, 30 | register_pytest_array_context_factory, 31 | ) 32 | 33 | 34 | __doc__ = """ 35 | Array Context 36 | ------------- 37 | 38 | .. autoclass:: PyOpenCLArrayContext 39 | """ 40 | 41 | 42 | # {{{ PyOpenCLArrayContext 43 | 44 | class PyOpenCLArrayContext(PyOpenCLArrayContextBase): 45 | def transform_loopy_program(self, t_unit): 46 | default_ep = t_unit.default_entrypoint 47 | options = default_ep.options 48 | 49 | if not (options.return_dict and options.no_numpy): 50 | raise ValueError("Loopy kernel passed to call_loopy must " 51 | "have return_dict and no_numpy options set. " 52 | "Did you use arraycontext.make_loopy_program " 53 | "to create this kernel?") 54 | 55 | return super().transform_loopy_program(t_unit) 56 | 57 | # }}} 58 | 59 | 60 | # {{{ pytest 61 | 62 | def _acf(): 63 | import pyopencl as cl 64 | ctx = cl.create_some_context() 65 | queue = cl.CommandQueue(ctx) 66 | 67 | return PyOpenCLArrayContext(queue) 68 | 69 | 70 | class PytestPyOpenCLArrayContextFactory( 71 | _PytestPyOpenCLArrayContextFactoryWithClass): 72 | actx_class = PyOpenCLArrayContext 73 | 74 | def __call__(self): 75 | # NOTE: prevent any cache explosions during testing! 76 | from sympy.core.cache import clear_cache 77 | clear_cache() 78 | 79 | return super().__call__() 80 | 81 | 82 | register_pytest_array_context_factory( 83 | "sumpy.pyopencl", 84 | PytestPyOpenCLArrayContextFactory) 85 | 86 | # }}} 87 | -------------------------------------------------------------------------------- /sumpy/assignment_collection.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | __copyright__ = "Copyright (C) 2012 Andreas Kloeckner" 5 | 6 | __license__ = """ 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | 27 | import logging 28 | 29 | import sumpy.symbolic as sym 30 | 31 | 32 | logger = logging.getLogger(__name__) 33 | 34 | __doc__ = """ 35 | 36 | Manipulating batches of assignments 37 | ----------------------------------- 38 | 39 | .. autoclass:: SymbolicAssignmentCollection 40 | 41 | """ 42 | 43 | 44 | class _SymbolGenerator: 45 | 46 | def __init__(self, taken_symbols): 47 | self.taken_symbols = taken_symbols 48 | from collections import defaultdict 49 | self.base_to_count = defaultdict(lambda: 0) 50 | 51 | def _normalize(self, base): 52 | # Strip off any _N suffix, to avoid generating conflicting names. 53 | import re 54 | base = re.split(r"_\d+$", base)[0] 55 | return base if base != "" else "expr" 56 | 57 | def __call__(self, base="expr"): 58 | base = self._normalize(base) 59 | count = self.base_to_count[base] 60 | 61 | def make_id_str(base, count): 62 | return "{base}{suffix}".format( 63 | base=base, 64 | suffix="" if count == 0 else "_" + str(count - 1)) 65 | 66 | id_str = make_id_str(base, count) 67 | while id_str in self.taken_symbols: 68 | count += 1 69 | id_str = make_id_str(base, count) 70 | 71 | self.base_to_count[base] = count + 1 72 | 73 | return sym.Symbol(id_str) 74 | 75 | def __iter__(self): 76 | return self 77 | 78 | def next(self): 79 | return self() 80 | 81 | __next__ = next 82 | 83 | 84 | # {{{ collection of assignments 85 | 86 | class SymbolicAssignmentCollection: 87 | """Represents a collection of assignments:: 88 | 89 | a = 5*x 90 | b = a**2-k 91 | 92 | In the above, *x* and *k* are external variables, and *a* and *b* 93 | are variables managed by this object. 94 | 95 | This is a stateful object, but the only state changes allowed 96 | are additions to *assignments*, and corresponding updates of 97 | its lookup tables. 98 | 99 | Note that user code is *only* allowed to hold on to *names* generated 100 | by this class, but not expressions using names defined in this collection. 101 | """ 102 | 103 | def __init__(self, assignments=None): 104 | """ 105 | :arg assignments: mapping from *var_name* to expression 106 | """ 107 | 108 | if assignments is None: 109 | assignments = {} 110 | 111 | self.assignments = assignments 112 | self.reversed_assignments = {v: k for (k, v) in assignments.items()} 113 | 114 | self.symbol_generator = _SymbolGenerator(self.assignments) 115 | self.all_dependencies_cache = {} 116 | 117 | def __str__(self): 118 | return "\n".join( 119 | f"{name} <- {expr}" 120 | for name, expr in self.assignments.items()) 121 | 122 | def get_all_dependencies(self, var_name): 123 | """Including recursive dependencies.""" 124 | try: 125 | return self.all_dependencies_cache[var_name] 126 | except KeyError: 127 | pass 128 | 129 | if var_name not in self.assignments: 130 | return set() 131 | 132 | result = set() 133 | for dep in self.assignments[var_name].atoms(): 134 | if not isinstance(dep, sym.Symbol): 135 | continue 136 | 137 | dep_name = dep.name 138 | if dep_name in self.assignments: 139 | result.update(self.get_all_dependencies(dep_name)) 140 | else: 141 | result.add(dep) 142 | 143 | self.all_dependencies_cache[var_name] = result 144 | return result 145 | 146 | def add_assignment(self, name, expr, root_name=None, wrt_set=None, 147 | retain_name=True): 148 | assert isinstance(name, str) 149 | assert name not in self.assignments 150 | 151 | if wrt_set is None: 152 | wrt_set = frozenset() 153 | if root_name is None: 154 | root_name = name 155 | 156 | new_expr = sym.sympify(expr) 157 | 158 | if not retain_name and new_expr in self.reversed_assignments: 159 | return self.reversed_assignments[new_expr] 160 | 161 | self.assignments[name] = new_expr 162 | self.reversed_assignments[new_expr] = name 163 | 164 | return name 165 | 166 | def assign_unique(self, name_base, expr): 167 | """Assign *expr* to a new variable whose name is based on *name_base*. 168 | Return the new variable name. 169 | """ 170 | new_name = self.symbol_generator(name_base).name 171 | 172 | return self.add_assignment(new_name, expr) 173 | 174 | def assign_temp(self, name_base, expr): 175 | """If *expr* is mapped to a existing variable, then return the existing 176 | variable or assign *expr* to a new variable whose name is based on 177 | *name_base*. Return the variable name *expr* is mapped to in either case. 178 | """ 179 | new_name = self.symbol_generator(name_base).name 180 | return self.add_assignment(new_name, expr, retain_name=False) 181 | 182 | def run_global_cse(self, extra_exprs=None): 183 | if extra_exprs is None: 184 | extra_exprs = [] 185 | 186 | import time 187 | start_time = time.time() 188 | 189 | logger.info("common subexpression elimination: start") 190 | 191 | assign_names = list(self.assignments.keys()) 192 | assign_exprs = [self.assignments[name] for name in assign_names] 193 | 194 | # Options here: 195 | # - checked_cse: if you mistrust the result of the cse. 196 | # Uses maxima to verify. 197 | # - sym.cse: The sympy thing. 198 | # - sumpy.cse.cse: Based on sympy, designed to go faster. 199 | # from sumpy.symbolic import checked_cse 200 | 201 | from sumpy.cse import cse 202 | new_assignments, new_exprs = cse(assign_exprs + extra_exprs, 203 | symbols=self.symbol_generator) 204 | 205 | new_assign_exprs = new_exprs[:len(assign_exprs)] 206 | new_extra_exprs = new_exprs[len(assign_exprs):] 207 | 208 | for name, new_expr in zip(assign_names, new_assign_exprs, strict=True): 209 | self.assignments[name] = new_expr 210 | 211 | for name, value in new_assignments: 212 | assert isinstance(name, sym.Symbol) 213 | self.add_assignment(name.name, value) 214 | 215 | for name, new_expr in zip(assign_names, new_assign_exprs, strict=True): 216 | # We want the assignment collection to be ordered correctly 217 | # to make it easier for loopy to schedule. 218 | # Deleting the original assignments and adding them again 219 | # makes them occur after the CSE'd expression preserving 220 | # the order of operations. 221 | del self.assignments[name] 222 | self.assignments[name] = new_expr 223 | 224 | logger.info("common subexpression elimination: done after %.2f s", 225 | time.time() - start_time) 226 | return new_extra_exprs 227 | 228 | # }}} 229 | 230 | # vim: fdm=marker 231 | -------------------------------------------------------------------------------- /sumpy/distributed.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | __copyright__ = "Copyright (C) 2022 Hao Gao" 5 | 6 | __license__ = """ 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | from boxtree.distributed.calculation import DistributedExpansionWrangler 27 | 28 | import pyopencl as cl 29 | 30 | from sumpy.fmm import SumpyExpansionWrangler 31 | 32 | 33 | class DistributedSumpyExpansionWrangler( 34 | DistributedExpansionWrangler, SumpyExpansionWrangler): 35 | def __init__( 36 | self, context, comm, tree_indep, local_traversal, global_traversal, 37 | dtype, fmm_level_to_order, communicate_mpoles_via_allreduce=False, 38 | **kwarg): 39 | DistributedExpansionWrangler.__init__( 40 | self, context, comm, global_traversal, True, 41 | communicate_mpoles_via_allreduce=communicate_mpoles_via_allreduce) 42 | SumpyExpansionWrangler.__init__( 43 | self, tree_indep, local_traversal, dtype, fmm_level_to_order, **kwarg) 44 | 45 | def distribute_source_weights(self, src_weight_vecs, src_idx_all_ranks): 46 | src_weight_vecs_host = [src_weight.get() for src_weight in src_weight_vecs] 47 | 48 | local_src_weight_vecs_host = super().distribute_source_weights( 49 | src_weight_vecs_host, src_idx_all_ranks) 50 | 51 | local_src_weight_vecs_device = [ 52 | cl.array.to_device(src_weight.queue, local_src_weight) 53 | for local_src_weight, src_weight in 54 | zip(local_src_weight_vecs_host, src_weight_vecs, strict=True)] 55 | 56 | return local_src_weight_vecs_device 57 | 58 | def gather_potential_results(self, potentials, tgt_idx_all_ranks): 59 | mpi_rank = self.comm.Get_rank() 60 | 61 | potentials_host_vec = [potentials_dev.get() for potentials_dev in potentials] 62 | 63 | gathered_potentials_host_vec = [] 64 | for potentials_host in potentials_host_vec: 65 | gathered_potentials_host_vec.append( 66 | super().gather_potential_results(potentials_host, tgt_idx_all_ranks)) 67 | 68 | if mpi_rank == 0: 69 | from pytools.obj_array import make_obj_array 70 | return make_obj_array([ 71 | cl.array.to_device(potentials_dev.queue, gathered_potentials_host) 72 | for gathered_potentials_host, potentials_dev in 73 | zip(gathered_potentials_host_vec, potentials, strict=True)]) 74 | else: 75 | return None 76 | 77 | def reorder_sources(self, source_array): 78 | if self.comm.Get_rank() == 0: 79 | return source_array.with_queue(source_array.queue)[ 80 | self.global_traversal.tree.user_source_ids] 81 | else: 82 | return source_array 83 | 84 | def reorder_potentials(self, potentials): 85 | if self.comm.Get_rank() == 0: 86 | import numpy as np 87 | 88 | from pytools.obj_array import obj_array_vectorize 89 | assert ( 90 | isinstance(potentials, np.ndarray) 91 | and potentials.dtype.char == "O") 92 | 93 | def reorder(x): 94 | return x[self.global_traversal.tree.sorted_target_ids] 95 | 96 | return obj_array_vectorize(reorder, potentials) 97 | else: 98 | return None 99 | 100 | def communicate_mpoles(self, mpole_exps, return_stats=False): 101 | mpole_exps_host = mpole_exps.get() 102 | stats = super().communicate_mpoles(mpole_exps_host, return_stats) 103 | mpole_exps[:] = mpole_exps_host 104 | return stats 105 | -------------------------------------------------------------------------------- /sumpy/expansion/level_to_order.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | __copyright__ = "Copyright (C) 2016 Matt Wala" 5 | 6 | __license__ = """ 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | __doc__ = """ 27 | .. autoclass:: FMMLibExpansionOrderFinder 28 | .. autoclass:: SimpleExpansionOrderFinder 29 | """ 30 | 31 | import math 32 | 33 | import numpy as np 34 | 35 | 36 | class FMMLibExpansionOrderFinder: 37 | r"""Return expansion orders that meet the tolerance for a given level 38 | using routines wrapped from ``pyfmmlib``. 39 | 40 | .. automethod:: __init__ 41 | .. automethod:: __call__ 42 | """ 43 | 44 | def __init__(self, tol, extra_order=0): 45 | """ 46 | :arg tol: error tolerance 47 | :arg extra_order: order increase to accommodate, say, the taking of 48 | derivatives of the FMM expansions. 49 | """ 50 | self.tol = tol 51 | self.extra_order = extra_order 52 | 53 | def __call__(self, kernel, kernel_args, tree, level): 54 | from pyfmmlib import ( # pylint: disable=no-name-in-module 55 | h2dterms, 56 | h3dterms, 57 | l2dterms, 58 | l3dterms, 59 | ) 60 | 61 | from sumpy.kernel import HelmholtzKernel, LaplaceKernel 62 | 63 | if isinstance(kernel, LaplaceKernel): 64 | if tree.dimensions == 2: 65 | nterms, ier = l2dterms(self.tol) 66 | if ier: 67 | raise RuntimeError(f"l2dterms returned error code '{ier}'") 68 | 69 | elif tree.dimensions == 3: 70 | nterms, ier = l3dterms(self.tol) 71 | if ier: 72 | raise RuntimeError(f"l3dterms returned error code '{ier}'") 73 | 74 | else: 75 | raise ValueError(f"unsupported dimension: {tree.dimensions}") 76 | 77 | elif isinstance(kernel, HelmholtzKernel): 78 | helmholtz_k = dict(kernel_args)[kernel.helmholtz_k_name] 79 | size = tree.root_extent / 2 ** level 80 | 81 | if tree.dimensions == 2: 82 | nterms, ier = h2dterms(size, helmholtz_k, self.tol) 83 | if ier: 84 | raise RuntimeError(f"h2dterms returned error code '{ier}'") 85 | 86 | elif tree.dimensions == 3: 87 | nterms, ier = h3dterms(size, helmholtz_k, self.tol) 88 | if ier: 89 | raise RuntimeError(f"h3dterms returned error code '{ier}'") 90 | 91 | else: 92 | raise ValueError(f"unsupported dimension: {tree.dimensions}") 93 | 94 | else: 95 | raise TypeError(f"unsupported kernel: '{type(kernel).__name__}'") 96 | 97 | return nterms + self.extra_order 98 | 99 | 100 | class SimpleExpansionOrderFinder: 101 | r""" 102 | This models the Laplace truncation error as: 103 | 104 | .. math:: 105 | 106 | C_{\text{lap}} \left(\frac{\sqrt{d}}{3}\right)^{p+1}. 107 | 108 | For the Helmholtz kernel, an additional term is added: 109 | 110 | .. math:: 111 | 112 | C_{\text{helm}} \frac 1{p!} 113 | \left(C_{\text{helmscale}} \cdot \frac{hk}{2\pi}\right)^{p+1}, 114 | 115 | where :math:`d` is the number of dimensions, :math:`p` is the expansion order, 116 | :math:`h` is the box size, and :math:`k` is the wave number. 117 | 118 | .. automethod:: __init__ 119 | .. automethod:: __call__ 120 | """ 121 | 122 | def __init__(self, tol, err_const_laplace=0.01, err_const_helmholtz=100, 123 | scaling_const_helmholtz=4, 124 | extra_order=1): 125 | """ 126 | :arg extra_order: order increase to accommodate, say, the taking of 127 | derivatives of the FMM expansions. 128 | """ 129 | self.tol = tol 130 | 131 | self.err_const_laplace = err_const_laplace 132 | self.err_const_helmholtz = err_const_helmholtz 133 | self.scaling_const_helmholtz = scaling_const_helmholtz 134 | 135 | self.extra_order = extra_order 136 | 137 | def __call__(self, kernel, kernel_args, tree, level): 138 | from sumpy.kernel import HelmholtzKernel, LaplaceKernel 139 | 140 | assert isinstance(kernel, LaplaceKernel | HelmholtzKernel) 141 | 142 | laplace_order = int(np.ceil( 143 | (np.log(self.tol) - np.log(self.err_const_laplace)) 144 | / 145 | np.log( 146 | np.sqrt(tree.dimensions)/3 147 | ) - 1)) 148 | 149 | if isinstance(kernel, HelmholtzKernel): 150 | helmholtz_k = dict(kernel_args)[kernel.helmholtz_k_name] 151 | 152 | box_lengthscale = ( 153 | tree.stick_out_factor 154 | * tree.root_extent / (1 << level)) 155 | 156 | factor = ( 157 | self.scaling_const_helmholtz 158 | * box_lengthscale 159 | * helmholtz_k 160 | / (2*float(np.pi))) 161 | 162 | helm_order = 1 163 | helm_rec_error = self.err_const_helmholtz * factor 164 | while True: 165 | helm_rec_error = helm_rec_error * factor / (helm_order+1) 166 | 167 | if helm_order < 4: 168 | # this may overflow for large orders 169 | helm_error_direct = ( 170 | 1/math.factorial(helm_order+1) 171 | * self.err_const_helmholtz 172 | * factor**(helm_order+1)) 173 | assert (abs(helm_rec_error - helm_error_direct) 174 | < 1e-13 * abs(helm_error_direct)) 175 | 176 | if helm_rec_error * helm_order**(tree.dimensions-1) < self.tol: 177 | break 178 | 179 | helm_order += 1 180 | 181 | if helm_order > 10000: 182 | raise ValueError("unable to find suitable order estimate " 183 | "for Helmholtz expansion") 184 | else: 185 | helm_order = 0 186 | 187 | return max(laplace_order, helm_order) + self.extra_order 188 | 189 | 190 | # vim: fdm=marker 191 | -------------------------------------------------------------------------------- /sumpy/expansion/loopy.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | __copyright__ = "Copyright (C) 2022 Isuru Fernando" 5 | 6 | __license__ = """ 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import logging 27 | from collections.abc import Sequence 28 | 29 | import numpy as np 30 | 31 | import loopy as lp 32 | import pymbolic 33 | 34 | import sumpy.symbolic as sym 35 | from sumpy.assignment_collection import SymbolicAssignmentCollection 36 | from sumpy.expansion import ExpansionBase 37 | from sumpy.kernel import Kernel 38 | from sumpy.tools import gather_loopy_arguments, gather_loopy_source_arguments 39 | 40 | 41 | logger = logging.getLogger(__name__) 42 | 43 | 44 | def make_e2p_loopy_kernel( 45 | expansion: ExpansionBase, kernels: Sequence[Kernel]) -> lp.TranslationUnit: 46 | """ 47 | This is a helper function to create a loopy kernel for multipole/local 48 | evaluation. This function uses symbolic expressions given by the expansion class, 49 | converts them to pymbolic expressions and generates a loopy 50 | kernel. Note that the loopy kernel returned has lots of expressions in it and 51 | takes a long time. Therefore, this function should be used only as a fallback 52 | when there is no "loop-y" kernel to evaluate the expansion. 53 | """ 54 | dim = expansion.dim 55 | 56 | bvec = sym.make_sym_vector("b", dim) 57 | ncoeffs = len(expansion.get_coefficient_identifiers()) 58 | 59 | rscale = sym.Symbol("rscale") 60 | 61 | sac = SymbolicAssignmentCollection() 62 | 63 | domains = [ 64 | "{[idim]: 0<=idim lp.TranslationUnit: 142 | """ 143 | This is a helper function to create a loopy kernel for multipole/local 144 | expression. This function uses symbolic expressions given by the expansion 145 | class, converts them to pymbolic expressions and generates a loopy 146 | kernel. Note that the loopy kernel returned has lots of expressions in it and 147 | takes a long time. Therefore, this function should be used only as a fallback 148 | when there is no "loop-y" kernel to evaluate the expansion. 149 | """ 150 | dim = expansion.dim 151 | 152 | avec = sym.make_sym_vector("a", dim) 153 | ncoeffs = len(expansion.get_coefficient_identifiers()) 154 | 155 | rscale = sym.Symbol("rscale") 156 | 157 | sac = SymbolicAssignmentCollection() 158 | 159 | domains = [ 160 | "{[idim]: 0<=idim tuple[tuple[int, ...], str]: 32 | import re 33 | 34 | m = re.match(r"^([0-9.]+)([a-z0-9]*?)$", VERSION_TEXT) 35 | assert m is not None 36 | 37 | return tuple(int(nr) for nr in m.group(1).split(".")), m.group(2) 38 | 39 | 40 | VERSION_TEXT = metadata.version("sumpy") 41 | VERSION, VERSION_STATUS = _parse_version(VERSION_TEXT) 42 | 43 | _GIT_REVISION = find_module_git_revision(__file__, n_levels_up=1) 44 | KERNEL_VERSION = (*VERSION, _GIT_REVISION, 0) 45 | -------------------------------------------------------------------------------- /sumpy/visualization.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | __copyright__ = "Copyright (C) 2012 Andreas Kloeckner" 5 | 6 | __license__ = """ 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | 27 | __doc__ = """ 28 | .. autofunction:: make_field_plotter_from_bbox 29 | .. autoclass:: FieldPlotter 30 | """ 31 | 32 | import numpy as np 33 | 34 | 35 | def separate_by_real_and_imag(data, real_only): 36 | from pytools.obj_array import obj_array_imag_copy, obj_array_real_copy 37 | 38 | for name, field in data: 39 | try: 40 | # Look inside object arrays to get the entry dtype. 41 | entry_dtype = field[0].dtype 42 | except AttributeError: 43 | entry_dtype = field.dtype 44 | 45 | assert entry_dtype.kind != "O" 46 | 47 | if real_only or entry_dtype.kind != "c": 48 | yield (name, obj_array_real_copy(field)) 49 | else: 50 | yield (f"{name}_r", obj_array_real_copy(field)) 51 | yield (f"{name}_i", obj_array_imag_copy(field)) 52 | 53 | 54 | def make_field_plotter_from_bbox(bbox, h, extend_factor=0): 55 | """ 56 | :arg bbox: a tuple (low, high) of points represented as 1D numpy arrays 57 | indicating the low and high ends of the extent of a bounding box. 58 | :arg h: Either a number or a sequence of numbers indicating the desired 59 | (approximate) grid spacing in all or each of the dimensions. If a 60 | sequence, the length must match the number of dimensions. 61 | :arg extend_factor: A floating point number indicating by what percentage 62 | the plot area should be grown compared to *bbox*. 63 | """ 64 | low, high = bbox 65 | 66 | extent = (high-low) * (1 + extend_factor) 67 | center = 0.5*(high+low) 68 | 69 | dimensions = len(center) 70 | from numbers import Number 71 | if isinstance(h, Number): 72 | h = (h,)*dimensions 73 | else: 74 | if len(h) != dimensions: 75 | raise ValueError("length of 'h' must match number of dimensions") 76 | 77 | from math import ceil 78 | 79 | npoints = tuple(ceil(extent[i] / h[i]) for i in range(dimensions)) 80 | 81 | return FieldPlotter(center, extent, npoints) 82 | 83 | 84 | class FieldPlotter: 85 | """ 86 | .. automethod:: set_matplotlib_limits 87 | .. automethod:: show_scalar_in_matplotlib 88 | .. automethod:: show_scalar_in_mayavi 89 | .. automethod:: write_vtk_file 90 | """ 91 | def __init__(self, center, extent=1, npoints=1000): 92 | center = np.asarray(center) 93 | self.dimensions, = dim, = center.shape 94 | self.a = a = center-extent*0.5 95 | self.b = b = center+extent*0.5 96 | 97 | from numbers import Number 98 | if isinstance(npoints, Number): 99 | npoints = dim*(npoints,) 100 | else: 101 | if len(npoints) != dim: 102 | raise ValueError("length of npoints must match dimension") 103 | 104 | for i in range(dim): 105 | if npoints[i] == 1: 106 | a[i] = center[i] 107 | 108 | mgrid_index = tuple( 109 | slice(a[i], b[i], 1j*npoints[i]) 110 | for i in range(dim)) 111 | 112 | # np.asarray is technically unneeded, used to placate pylint 113 | # https://github.com/pylint-dev/pylint/issues/9989 114 | mgrid = np.asarray(np.mgrid[mgrid_index]) 115 | 116 | # (axis, point x idx, point y idx, ...) 117 | self.nd_points = mgrid 118 | 119 | self.points = self.nd_points.reshape(dim, -1).copy() 120 | 121 | from pytools import product 122 | self.npoints = product(npoints) 123 | 124 | def _get_nontrivial_dims(self): 125 | return np.array(self.nd_points.shape[1:]) != 1 126 | 127 | def _get_squeezed_bounds(self): 128 | nontriv_dims = self._get_nontrivial_dims() 129 | 130 | return self.a[nontriv_dims], self.b[nontriv_dims] 131 | 132 | def show_scalar_in_matplotlib(self, fld, max_val=None, 133 | func_name="imshow", **kwargs): 134 | squeezed_points = self.points.squeeze() 135 | 136 | if len(squeezed_points.shape) != 2: 137 | raise RuntimeError( 138 | "matplotlib plotting requires 2D geometry") 139 | 140 | if len(fld.shape) == 1: 141 | fld = fld.reshape(self.nd_points.shape[1:]) 142 | 143 | squeezed_fld = fld.squeeze() 144 | 145 | if max_val is not None: 146 | squeezed_fld[squeezed_fld > max_val] = max_val 147 | squeezed_fld[squeezed_fld < -max_val] = -max_val 148 | 149 | squeezed_fld = squeezed_fld[..., ::-1] 150 | 151 | a, b = self._get_squeezed_bounds() 152 | 153 | kwargs["extent"] = ( 154 | # (left, right, bottom, top) 155 | a[0], b[0], 156 | a[1], b[1]) 157 | 158 | import matplotlib.pyplot as pt 159 | return getattr(pt, func_name)(squeezed_fld.T, **kwargs) 160 | 161 | def set_matplotlib_limits(self): 162 | import matplotlib.pyplot as pt 163 | 164 | a, b = self._get_squeezed_bounds() 165 | pt.xlim((a[0], b[0])) 166 | pt.ylim((a[1], b[1])) 167 | 168 | def show_vector_in_mayavi(self, fld, do_show=True, **kwargs): 169 | c = self.points 170 | 171 | from mayavi import mlab # pylint: disable=import-error 172 | 173 | mlab.quiver3d(c[0], c[1], c[2], fld[0], fld[1], fld[2], 174 | **kwargs) 175 | 176 | if do_show: 177 | mlab.show() 178 | 179 | def write_vtk_file(self, file_name, data, real_only=False, overwrite=False): 180 | from pyvisfile.vtk import write_structured_grid 181 | write_structured_grid(file_name, self.nd_points, 182 | point_data=list(separate_by_real_and_imag(data, real_only)), 183 | overwrite=overwrite) 184 | 185 | def show_scalar_in_mayavi(self, fld, max_val=None, **kwargs): 186 | if max_val is not None: 187 | fld[fld > max_val] = max_val 188 | fld[fld < -max_val] = -max_val 189 | 190 | if len(fld.shape) == 1: 191 | fld = fld.reshape(self.nd_points.shape[1:]) 192 | 193 | nd_points = self.nd_points.squeeze()[self._get_nontrivial_dims()] 194 | squeezed_fld = fld.squeeze() 195 | 196 | from mayavi import mlab # pylint: disable=import-error 197 | mlab.surf(nd_points[0], nd_points[1], squeezed_fld, **kwargs) 198 | 199 | # vim: foldmethod=marker 200 | -------------------------------------------------------------------------------- /test/test_codegen.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | __copyright__ = "Copyright (C) 2017 Matt Wala" 5 | 6 | __license__ = """ 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import logging 27 | import sys 28 | 29 | import pytest 30 | 31 | 32 | logger = logging.getLogger(__name__) 33 | 34 | 35 | # {{{ test_symbolic_assignment_name_uniqueness 36 | 37 | def test_symbolic_assignment_name_uniqueness(): 38 | # https://gitlab.tiker.net/inducer/sumpy/issues/13 39 | from sumpy.assignment_collection import SymbolicAssignmentCollection 40 | 41 | sac = SymbolicAssignmentCollection({"s_0": 1}) 42 | sac.assign_unique("s_", 1) 43 | sac.assign_unique("s_", 1) 44 | assert len(sac.assignments) == 3 45 | 46 | sac = SymbolicAssignmentCollection() 47 | sac.assign_unique("s_0", 1) 48 | sac.assign_unique("s_", 1) 49 | sac.assign_unique("s_", 1) 50 | 51 | assert len(sac.assignments) == 3 52 | 53 | # }}} 54 | 55 | 56 | # {{{ test_line_taylor_coeff_growth 57 | 58 | def test_line_taylor_coeff_growth(): 59 | # Regression test for LineTaylorLocalExpansion. 60 | # See https://gitlab.tiker.net/inducer/pytential/merge_requests/12 61 | import numpy as np 62 | 63 | from sumpy.expansion.local import LineTaylorLocalExpansion 64 | from sumpy.kernel import LaplaceKernel 65 | from sumpy.symbolic import SympyToPymbolicMapper, make_sym_vector 66 | 67 | order = 10 68 | expn = LineTaylorLocalExpansion(LaplaceKernel(2), order) 69 | avec = make_sym_vector("a", 2) 70 | bvec = make_sym_vector("b", 2) 71 | coeffs = expn.coefficients_from_source(expn.kernel, avec, bvec, rscale=1) 72 | 73 | sym2pymbolic = SympyToPymbolicMapper() 74 | coeffs_pymbolic = [sym2pymbolic(c) for c in coeffs] 75 | 76 | from pymbolic.mapper.flop_counter import FlopCounter 77 | flop_counter = FlopCounter() 78 | counts = [flop_counter(c) for c in coeffs_pymbolic] 79 | 80 | indices = np.arange(1, order + 2) 81 | max_order = 2 82 | assert np.polyfit(np.log(indices), np.log(counts), deg=1)[0] < max_order 83 | 84 | # }}} 85 | 86 | 87 | # You can test individual routines by typing 88 | # $ python test_codegen.py 'test_line_taylor_coeff_growth()' 89 | 90 | if __name__ == "__main__": 91 | if len(sys.argv) > 1: 92 | exec(sys.argv[1]) 93 | else: 94 | pytest.main([__file__]) 95 | 96 | # vim: fdm=marker 97 | -------------------------------------------------------------------------------- /test/test_distributed.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | __copyright__ = "Copyright (C) 2022 Hao Gao" 5 | 6 | __license__ = """ 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import os 27 | from functools import partial 28 | 29 | import numpy as np 30 | import pytest 31 | 32 | import pyopencl as cl 33 | 34 | 35 | # Note: Do not import mpi4py.MPI object at the module level, because OpenMPI does not 36 | # support recursive invocations. 37 | 38 | 39 | def set_cache_dir(mpirank): 40 | """Make each rank use a different cache location to avoid conflict.""" 41 | import platformdirs 42 | cache_dir = platformdirs.user_cache_dir("sumpy", "sumpy") 43 | 44 | # FIXME: should clean up this directory after running the tests 45 | os.environ["XDG_CACHE_HOME"] = os.path.join(cache_dir, str(mpirank)) 46 | 47 | 48 | # {{{ _test_against_single_rank 49 | 50 | def _test_against_single_rank( 51 | dims, nsources, ntargets, dtype, communicate_mpoles_via_allreduce=False): 52 | from mpi4py import MPI 53 | 54 | # Get the current rank 55 | comm = MPI.COMM_WORLD 56 | mpi_rank = comm.Get_rank() 57 | set_cache_dir(mpi_rank) 58 | 59 | # Configure array context 60 | cl_context = cl.create_some_context() 61 | queue = cl.CommandQueue(cl_context) 62 | 63 | def fmm_level_to_order(base_kernel, kernel_arg_set, tree, level): 64 | return max(level, 3) 65 | 66 | from boxtree.traversal import FMMTraversalBuilder 67 | traversal_builder = FMMTraversalBuilder(cl_context, well_sep_is_n_away=2) 68 | 69 | from sumpy.expansion import DefaultExpansionFactory 70 | from sumpy.kernel import LaplaceKernel 71 | kernel = LaplaceKernel(dims) 72 | expansion_factory = DefaultExpansionFactory() 73 | local_expansion_factory = expansion_factory.get_local_expansion_class(kernel) 74 | local_expansion_factory = partial(local_expansion_factory, kernel) 75 | multipole_expansion_factory = \ 76 | expansion_factory.get_multipole_expansion_class(kernel) 77 | multipole_expansion_factory = partial(multipole_expansion_factory, kernel) 78 | 79 | from sumpy.fmm import SumpyTreeIndependentDataForWrangler 80 | tree_indep = SumpyTreeIndependentDataForWrangler( 81 | cl_context, multipole_expansion_factory, local_expansion_factory, [kernel]) 82 | 83 | global_tree_dev = None 84 | sources_weights = cl.array.empty(queue, 0, dtype=dtype) 85 | 86 | if mpi_rank == 0: 87 | # Generate random particles and source weights 88 | from boxtree.tools import make_normal_particle_array as p_normal 89 | sources = p_normal(queue, nsources, dims, dtype, seed=15) 90 | targets = p_normal(queue, ntargets, dims, dtype, seed=18) 91 | 92 | # FIXME: Use arraycontext instead of raw PyOpenCL arrays 93 | from pyopencl.clrandom import PhiloxGenerator 94 | rng = PhiloxGenerator(cl_context, seed=20) 95 | sources_weights = rng.uniform(queue, nsources, dtype=np.float64) 96 | 97 | rng = PhiloxGenerator(cl_context, seed=22) 98 | target_radii = rng.uniform( 99 | queue, ntargets, a=0, b=0.05, dtype=np.float64) 100 | 101 | # Build the tree and interaction lists 102 | from boxtree import TreeBuilder 103 | tb = TreeBuilder(cl_context) 104 | global_tree_dev, _ = tb( 105 | queue, sources, targets=targets, target_radii=target_radii, 106 | stick_out_factor=0.25, max_particles_in_box=30, debug=True) 107 | 108 | global_trav_dev, _ = traversal_builder(queue, global_tree_dev, debug=True) 109 | 110 | from sumpy.fmm import SumpyExpansionWrangler 111 | wrangler = SumpyExpansionWrangler(tree_indep, global_trav_dev, dtype, 112 | fmm_level_to_order) 113 | 114 | # Compute FMM with one MPI rank 115 | from boxtree.fmm import drive_fmm 116 | shmem_potential = drive_fmm(wrangler, [sources_weights]) 117 | 118 | # Compute FMM using the distributed implementation 119 | 120 | def wrangler_factory(local_traversal, global_traversal): 121 | from sumpy.distributed import DistributedSumpyExpansionWrangler 122 | return DistributedSumpyExpansionWrangler( 123 | cl_context, comm, tree_indep, local_traversal, global_traversal, dtype, 124 | fmm_level_to_order, 125 | communicate_mpoles_via_allreduce=communicate_mpoles_via_allreduce) 126 | 127 | from boxtree.distributed import DistributedFMMRunner 128 | distributed_fmm_info = DistributedFMMRunner( 129 | queue, global_tree_dev, traversal_builder, wrangler_factory, comm=comm) 130 | 131 | timing_data = {} 132 | distributed_potential = distributed_fmm_info.drive_dfmm( 133 | [sources_weights], timing_data=timing_data) 134 | assert timing_data 135 | 136 | if mpi_rank == 0: 137 | assert shmem_potential.shape == (1,) 138 | assert distributed_potential.shape == (1,) 139 | 140 | shmem_potential = shmem_potential[0].get() 141 | distributed_potential = distributed_potential[0].get() 142 | 143 | error = (np.linalg.norm(distributed_potential - shmem_potential, ord=np.inf) 144 | / np.linalg.norm(shmem_potential, ord=np.inf)) 145 | print(error) 146 | assert error < 1e-14 147 | 148 | 149 | @pytest.mark.mpi 150 | @pytest.mark.parametrize( 151 | "num_processes, dims, nsources, ntargets, communicate_mpoles_via_allreduce", [ 152 | (4, 3, 10000, 10000, True), 153 | (4, 3, 10000, 10000, False) 154 | ] 155 | ) 156 | def test_against_single_rank( 157 | num_processes, dims, nsources, ntargets, communicate_mpoles_via_allreduce): 158 | pytest.importorskip("mpi4py") 159 | 160 | from boxtree.tools import run_mpi 161 | run_mpi(__file__, num_processes, { 162 | "OMP_NUM_THREADS": 1, 163 | "_SUMPY_TEST_NAME": "against_single_rank", 164 | "_SUMPY_TEST_DIMS": dims, 165 | "_SUMPY_TEST_NSOURCES": nsources, 166 | "_SUMPY_TEST_NTARGETS": ntargets, 167 | "_SUMPY_TEST_MPOLES_ALLREDUCE": communicate_mpoles_via_allreduce 168 | }) 169 | 170 | # }}} 171 | 172 | 173 | if __name__ == "__main__": 174 | if "_SUMPY_TEST_NAME" in os.environ: 175 | name = os.environ["_SUMPY_TEST_NAME"] 176 | if name == "against_single_rank": 177 | # Run "test_against_single_rank" test case 178 | dims = int(os.environ["_SUMPY_TEST_DIMS"]) 179 | nsources = int(os.environ["_SUMPY_TEST_NSOURCES"]) 180 | ntargets = int(os.environ["_SUMPY_TEST_NTARGETS"]) 181 | 182 | communicate_mpoles_via_allreduce = ( 183 | os.environ["_SUMPY_TEST_MPOLES_ALLREDUCE"] == "True") 184 | 185 | _test_against_single_rank( 186 | dims, nsources, ntargets, np.float64, 187 | communicate_mpoles_via_allreduce) 188 | else: 189 | raise ValueError(f"Invalid test name: {name!r}") 190 | else: 191 | # You can test individual routines by typing 192 | # $ python test_distributed.py 193 | # 'test_against_single_rank(4, 3, 10000, 10000, False)' 194 | import sys 195 | exec(sys.argv[1]) 196 | -------------------------------------------------------------------------------- /test/test_matrixgen.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | __copyright__ = "Copyright (C) 2018 Alexandru Fikl" 5 | 6 | __license__ = """ 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import logging 27 | import sys 28 | 29 | import numpy as np 30 | import numpy.linalg as la 31 | import pytest 32 | 33 | from arraycontext import pytest_generate_tests_for_array_contexts 34 | 35 | from sumpy.array_context import PytestPyOpenCLArrayContextFactory, _acf # noqa: F401 36 | 37 | 38 | logger = logging.getLogger(__name__) 39 | 40 | pytest_generate_tests = pytest_generate_tests_for_array_contexts([ 41 | PytestPyOpenCLArrayContextFactory, 42 | ]) 43 | 44 | 45 | def _build_geometry(actx, ntargets, nsources, mode, target_radius=1.0): 46 | # source points 47 | t = np.linspace(0.0, 2.0 * np.pi, nsources, endpoint=False) 48 | sources = np.array([np.cos(t), np.sin(t)]) 49 | 50 | # density 51 | sigma = np.cos(mode * t) 52 | 53 | # target points 54 | t = np.linspace(0.0, 2.0 * np.pi, ntargets, endpoint=False) 55 | targets = target_radius * np.array([np.cos(t), np.sin(t)]) 56 | 57 | # target centers and expansion radii 58 | h = 2.0 * np.pi * target_radius / ntargets 59 | radius = 7.0 * h 60 | centers = (1.0 - radius) * targets 61 | expansion_radii = np.full(ntargets, radius) 62 | 63 | return (actx.from_numpy(targets), 64 | actx.from_numpy(sources), 65 | actx.from_numpy(centers), 66 | actx.from_numpy(expansion_radii), 67 | actx.from_numpy(sigma)) 68 | 69 | 70 | def _build_subset_indices(actx, ntargets, nsources, factor): 71 | tgtindices = np.arange(0, ntargets) 72 | srcindices = np.arange(0, nsources) 73 | 74 | rng = np.random.default_rng() 75 | if abs(factor - 1.0) > 1.0e-14: 76 | tgtindices = rng.choice(tgtindices, 77 | size=int(factor * ntargets), replace=False) 78 | srcindices = rng.choice(srcindices, 79 | size=int(factor * nsources), replace=False) 80 | else: 81 | rng.shuffle(tgtindices) 82 | rng.shuffle(srcindices) 83 | 84 | tgtindices, srcindices = np.meshgrid(tgtindices, srcindices) 85 | return ( 86 | actx.freeze(actx.from_numpy(tgtindices.ravel())), 87 | actx.freeze(actx.from_numpy(srcindices.ravel()))) 88 | 89 | 90 | # {{{ test_qbx_direct 91 | 92 | @pytest.mark.parametrize("factor", [1.0, 0.6]) 93 | @pytest.mark.parametrize("lpot_id", [1, 2]) 94 | def test_qbx_direct(actx_factory, factor, lpot_id, visualize=False): 95 | if visualize: 96 | logging.basicConfig(level=logging.INFO) 97 | 98 | actx = actx_factory() 99 | 100 | ndim = 2 101 | order = 12 102 | mode_nr = 25 103 | 104 | from sumpy.kernel import DirectionalSourceDerivative, LaplaceKernel 105 | if lpot_id == 1: 106 | base_knl = LaplaceKernel(ndim) 107 | knl = base_knl 108 | elif lpot_id == 2: 109 | base_knl = LaplaceKernel(ndim) 110 | knl = DirectionalSourceDerivative(base_knl, dir_vec_name="dsource_vec") 111 | else: 112 | raise ValueError(f"unknown lpot_id: {lpot_id}") 113 | 114 | from sumpy.expansion.local import LineTaylorLocalExpansion 115 | expn = LineTaylorLocalExpansion(knl, order) 116 | 117 | from sumpy.qbx import LayerPotential 118 | lpot = LayerPotential(actx.context, expansion=expn, source_kernels=(knl,), 119 | target_kernels=(base_knl,)) 120 | 121 | from sumpy.qbx import LayerPotentialMatrixGenerator 122 | mat_gen = LayerPotentialMatrixGenerator(actx.context, 123 | expansion=expn, 124 | source_kernels=(knl,), 125 | target_kernels=(base_knl,)) 126 | 127 | from sumpy.qbx import LayerPotentialMatrixSubsetGenerator 128 | blk_gen = LayerPotentialMatrixSubsetGenerator(actx.context, 129 | expansion=expn, 130 | source_kernels=(knl,), 131 | target_kernels=(base_knl,)) 132 | 133 | for n in [200, 300, 400]: 134 | targets, sources, centers, expansion_radii, sigma = \ 135 | _build_geometry(actx, n, n, mode_nr, target_radius=1.2) 136 | 137 | h = 2 * np.pi / n 138 | strengths = (sigma * h,) 139 | tgtindices, srcindices = _build_subset_indices(actx, 140 | ntargets=n, nsources=n, factor=factor) 141 | 142 | extra_kwargs = {} 143 | if lpot_id == 2: 144 | from pytools.obj_array import make_obj_array 145 | extra_kwargs["dsource_vec"] = ( 146 | actx.from_numpy(make_obj_array(np.ones((ndim, n)))) 147 | ) 148 | 149 | _, (result_lpot,) = lpot(actx.queue, 150 | targets=targets, 151 | sources=sources, 152 | centers=centers, 153 | expansion_radii=expansion_radii, 154 | strengths=strengths, **extra_kwargs) 155 | result_lpot = actx.to_numpy(result_lpot) 156 | 157 | _, (mat,) = mat_gen(actx.queue, 158 | targets=targets, 159 | sources=sources, 160 | centers=centers, 161 | expansion_radii=expansion_radii, **extra_kwargs) 162 | mat = actx.to_numpy(mat) 163 | result_mat = mat @ actx.to_numpy(strengths[0]) 164 | 165 | _, (blk,) = blk_gen(actx.queue, 166 | targets=targets, 167 | sources=sources, 168 | centers=centers, 169 | expansion_radii=expansion_radii, 170 | tgtindices=tgtindices, 171 | srcindices=srcindices, **extra_kwargs) 172 | blk = actx.to_numpy(blk) 173 | 174 | tgtindices = actx.to_numpy(tgtindices) 175 | srcindices = actx.to_numpy(srcindices) 176 | 177 | eps = 1.0e-10 * la.norm(result_lpot) 178 | assert la.norm(result_mat - result_lpot) < eps 179 | assert la.norm(blk - mat[tgtindices, srcindices]) < eps 180 | 181 | # }}} 182 | 183 | 184 | # {{{ test_p2p_direct 185 | 186 | @pytest.mark.parametrize("exclude_self", [True, False]) 187 | @pytest.mark.parametrize("factor", [1.0, 0.6]) 188 | @pytest.mark.parametrize("lpot_id", [1, 2]) 189 | def test_p2p_direct(actx_factory, exclude_self, factor, lpot_id, visualize=False): 190 | if visualize: 191 | logging.basicConfig(level=logging.INFO) 192 | 193 | actx = actx_factory() 194 | 195 | ndim = 2 196 | mode_nr = 25 197 | 198 | from sumpy.kernel import DirectionalSourceDerivative, LaplaceKernel 199 | if lpot_id == 1: 200 | lknl = LaplaceKernel(ndim) 201 | elif lpot_id == 2: 202 | lknl = LaplaceKernel(ndim) 203 | lknl = DirectionalSourceDerivative(lknl, dir_vec_name="dsource_vec") 204 | else: 205 | raise ValueError(f"unknown lpot_id: '{lpot_id}'") 206 | 207 | from sumpy.p2p import P2P 208 | lpot = P2P(actx.context, [lknl], exclude_self=exclude_self) 209 | 210 | from sumpy.p2p import P2PMatrixGenerator 211 | mat_gen = P2PMatrixGenerator(actx.context, [lknl], exclude_self=exclude_self) 212 | 213 | from sumpy.p2p import P2PMatrixSubsetGenerator 214 | blk_gen = P2PMatrixSubsetGenerator( 215 | actx.context, [lknl], exclude_self=exclude_self) 216 | 217 | for n in [200, 300, 400]: 218 | targets, sources, _, _, sigma = ( 219 | _build_geometry(actx, n, n, mode_nr, target_radius=1.2)) 220 | 221 | h = 2 * np.pi / n 222 | strengths = (sigma * h,) 223 | tgtindices, srcindices = _build_subset_indices(actx, 224 | ntargets=n, nsources=n, factor=factor) 225 | 226 | extra_kwargs = {} 227 | if exclude_self: 228 | extra_kwargs["target_to_source"] = ( 229 | actx.from_numpy(np.arange(n, dtype=np.int32)) 230 | ) 231 | if lpot_id == 2: 232 | from pytools.obj_array import make_obj_array 233 | extra_kwargs["dsource_vec"] = ( 234 | actx.from_numpy(make_obj_array(np.ones((ndim, n))))) 235 | 236 | _, (result_lpot,) = lpot(actx.queue, 237 | targets=targets, 238 | sources=sources, 239 | strength=strengths, **extra_kwargs) 240 | result_lpot = actx.to_numpy(result_lpot) 241 | 242 | _, (mat,) = mat_gen(actx.queue, 243 | targets=targets, 244 | sources=sources, **extra_kwargs) 245 | mat = actx.to_numpy(mat) 246 | result_mat = mat @ actx.to_numpy(strengths[0]) 247 | 248 | _, (blk,) = blk_gen(actx.queue, 249 | targets=targets, 250 | sources=sources, 251 | tgtindices=tgtindices, 252 | srcindices=srcindices, **extra_kwargs) 253 | blk = actx.to_numpy(blk) 254 | 255 | tgtindices = actx.to_numpy(tgtindices) 256 | srcindices = actx.to_numpy(srcindices) 257 | 258 | eps = 1.0e-10 * la.norm(result_lpot) 259 | assert la.norm(result_mat - result_lpot) < eps 260 | assert la.norm(blk - mat[tgtindices, srcindices]) < eps 261 | 262 | # }}} 263 | 264 | 265 | # You can test individual routines by typing 266 | # $ python test_matrixgen.py 'test_p2p_direct(_acf, True, 1.0, 1, visualize=True)' 267 | 268 | if __name__ == "__main__": 269 | if len(sys.argv) > 1: 270 | exec(sys.argv[1]) 271 | else: 272 | pytest.main([__file__]) 273 | 274 | # vim: fdm=marker 275 | -------------------------------------------------------------------------------- /test/test_qbx.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | __copyright__ = "Copyright (C) 2017 Matt Wala" 5 | 6 | __license__ = """ 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import logging 27 | import sys 28 | 29 | import numpy as np 30 | import pytest 31 | 32 | from arraycontext import pytest_generate_tests_for_array_contexts 33 | 34 | from sumpy.array_context import PytestPyOpenCLArrayContextFactory, _acf # noqa: F401 35 | from sumpy.expansion.local import LineTaylorLocalExpansion, VolumeTaylorLocalExpansion 36 | 37 | 38 | logger = logging.getLogger(__name__) 39 | 40 | pytest_generate_tests = pytest_generate_tests_for_array_contexts([ 41 | PytestPyOpenCLArrayContextFactory, 42 | ]) 43 | 44 | 45 | # {{{ test_direct_qbx_vs_eigval 46 | 47 | @pytest.mark.parametrize("expn_class", [ 48 | LineTaylorLocalExpansion, 49 | VolumeTaylorLocalExpansion, 50 | ]) 51 | def test_direct_qbx_vs_eigval(actx_factory, expn_class, visualize=False): 52 | """This evaluates a single layer potential on a circle using a known 53 | eigenvalue/eigenvector combination. 54 | """ 55 | if visualize: 56 | logging.basicConfig(level=logging.INFO) 57 | 58 | actx = actx_factory() 59 | 60 | from sumpy.kernel import LaplaceKernel 61 | lknl = LaplaceKernel(2) 62 | 63 | order = 12 64 | 65 | from sumpy.qbx import LayerPotential 66 | 67 | lpot = LayerPotential(actx.context, 68 | expansion=expn_class(lknl, order), 69 | target_kernels=(lknl,), 70 | source_kernels=(lknl,)) 71 | 72 | mode_nr = 25 73 | 74 | from pytools.convergence import EOCRecorder 75 | 76 | eocrec = EOCRecorder() 77 | 78 | for n in [200, 300, 400]: 79 | t = np.linspace(0, 2 * np.pi, n, endpoint=False) 80 | unit_circle = np.exp(1j * t) 81 | unit_circle = np.array([unit_circle.real, unit_circle.imag]) 82 | 83 | sigma = np.cos(mode_nr * t) 84 | eigval = 1/(2*mode_nr) 85 | 86 | result_ref = eigval * sigma 87 | 88 | h = 2 * np.pi / n 89 | 90 | targets = actx.from_numpy(unit_circle) 91 | sources = actx.from_numpy(unit_circle) 92 | 93 | radius = 7 * h 94 | centers = actx.from_numpy((1 - radius) * unit_circle) 95 | expansion_radii = actx.from_numpy(radius * np.ones(n)) 96 | strengths = (actx.from_numpy(sigma * h),) 97 | 98 | _evt, (result_qbx,) = lpot( 99 | actx.queue, 100 | targets, sources, centers, strengths, 101 | expansion_radii=expansion_radii) 102 | result_qbx = actx.to_numpy(result_qbx) 103 | 104 | eocrec.add_data_point(h, np.max(np.abs(result_ref - result_qbx))) 105 | 106 | logger.info("eoc:\n%s", eocrec) 107 | 108 | slack = 1.5 109 | assert eocrec.order_estimate() > order - slack 110 | 111 | # }}} 112 | 113 | 114 | # {{{ test_direct_qbx_vs_eigval_with_tgt_deriv 115 | 116 | @pytest.mark.parametrize("expn_class", [ 117 | LineTaylorLocalExpansion, 118 | VolumeTaylorLocalExpansion, 119 | ]) 120 | def test_direct_qbx_vs_eigval_with_tgt_deriv( 121 | actx_factory, expn_class, visualize=False): 122 | """This evaluates a single layer potential on a circle using a known 123 | eigenvalue/eigenvector combination. 124 | """ 125 | if visualize: 126 | logging.basicConfig(level=logging.INFO) 127 | 128 | actx = actx_factory() 129 | 130 | from sumpy.kernel import AxisTargetDerivative, LaplaceKernel 131 | lknl = LaplaceKernel(2) 132 | 133 | order = 8 134 | 135 | from sumpy.qbx import LayerPotential 136 | 137 | lpot_dx = LayerPotential(actx.context, expansion=expn_class(lknl, order), 138 | target_kernels=(AxisTargetDerivative(0, lknl),), source_kernels=(lknl,)) 139 | lpot_dy = LayerPotential(actx.context, expansion=expn_class(lknl, order), 140 | target_kernels=(AxisTargetDerivative(1, lknl),), source_kernels=(lknl,)) 141 | 142 | mode_nr = 15 143 | 144 | from pytools.convergence import EOCRecorder 145 | 146 | eocrec = EOCRecorder() 147 | 148 | for n in [200, 300, 400]: 149 | t = np.linspace(0, 2 * np.pi, n, endpoint=False) 150 | unit_circle = np.exp(1j * t) 151 | unit_circle = np.array([unit_circle.real, unit_circle.imag]) 152 | 153 | sigma = np.cos(mode_nr * t) 154 | # eigval = 1/(2*mode_nr) 155 | eigval = 0.5 156 | 157 | result_ref = eigval * sigma 158 | 159 | h = 2 * np.pi / n 160 | 161 | targets = actx.from_numpy(unit_circle) 162 | sources = actx.from_numpy(unit_circle) 163 | 164 | radius = 7 * h 165 | centers = actx.from_numpy((1 - radius) * unit_circle) 166 | expansion_radii = actx.from_numpy(radius * np.ones(n)) 167 | strengths = (actx.from_numpy(sigma * h),) 168 | 169 | _evt, (result_qbx_dx,) = lpot_dx( 170 | actx.queue, 171 | targets, sources, centers, strengths, 172 | expansion_radii=expansion_radii) 173 | _evt, (result_qbx_dy,) = lpot_dy( 174 | actx.queue, 175 | targets, sources, centers, strengths, 176 | expansion_radii=expansion_radii) 177 | 178 | result_qbx_dx = actx.to_numpy(result_qbx_dx) 179 | result_qbx_dy = actx.to_numpy(result_qbx_dy) 180 | 181 | normals = unit_circle 182 | result_qbx = normals[0] * result_qbx_dx + normals[1] * result_qbx_dy 183 | 184 | eocrec.add_data_point(h, np.max(np.abs(result_ref - result_qbx))) 185 | 186 | if expn_class is not LineTaylorLocalExpansion: 187 | logger.info("eoc:\n%s", eocrec) 188 | 189 | slack = 1.5 190 | assert eocrec.order_estimate() > order - slack 191 | 192 | # }}} 193 | 194 | 195 | # You can test individual routines by typing 196 | # $ python test_qbx.py 'test_direct_qbx_vs_eigval(_acf, LineTaylorLocalExpansion)' 197 | 198 | if __name__ == "__main__": 199 | if len(sys.argv) > 1: 200 | exec(sys.argv[1]) 201 | else: 202 | pytest.main([__file__]) 203 | 204 | # vim: fdm=marker 205 | -------------------------------------------------------------------------------- /test/test_tools.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | __copyright__ = "Copyright (C) 2020 Isuru Fernando" 5 | 6 | __license__ = """ 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | 26 | import logging 27 | import sys 28 | 29 | import numpy as np 30 | import pytest 31 | 32 | from arraycontext import pytest_generate_tests_for_array_contexts 33 | 34 | import sumpy.symbolic as sym 35 | from sumpy.array_context import PytestPyOpenCLArrayContextFactory, _acf # noqa: F401 36 | from sumpy.tools import ( 37 | fft, 38 | fft_toeplitz_upper_triangular, 39 | loopy_fft, 40 | matvec_toeplitz_upper_triangular, 41 | ) 42 | 43 | 44 | logger = logging.getLogger(__name__) 45 | 46 | pytest_generate_tests = pytest_generate_tests_for_array_contexts([ 47 | PytestPyOpenCLArrayContextFactory, 48 | ]) 49 | 50 | 51 | # {{{ test_matvec_fft 52 | 53 | def test_matvec_fft(): 54 | k = 5 55 | 56 | rng = np.random.default_rng(42) 57 | v = rng.random(k) 58 | x = rng.random(k) 59 | 60 | fft = fft_toeplitz_upper_triangular(v, x) 61 | matvec = matvec_toeplitz_upper_triangular(v, x) 62 | 63 | for i in range(k): 64 | assert abs(fft[i] - matvec[i]) < 1e-14 65 | 66 | # }}} 67 | 68 | 69 | # {{{ test_matvec_fft_small_floats 70 | 71 | def test_matvec_fft_small_floats(): 72 | k = 5 73 | v = sym.make_sym_vector("v", k) 74 | x = sym.make_sym_vector("x", k) 75 | 76 | fft = fft_toeplitz_upper_triangular(v, x) 77 | for expr in fft: 78 | for f in expr.atoms(sym.Float): 79 | if f == 0: 80 | continue 81 | assert abs(f) > 1e-10 82 | 83 | # }}} 84 | 85 | 86 | # {{{ test_fft 87 | 88 | @pytest.mark.parametrize("size", [1, 2, 7, 10, 30, 210]) 89 | def test_fft(actx_factory, size): 90 | actx = actx_factory() 91 | 92 | inp = np.arange(size, dtype=np.complex64) 93 | inp_dev = actx.from_numpy(inp) 94 | out = fft(inp) 95 | 96 | fft_func = loopy_fft(inp.shape, inverse=False, complex_dtype=inp.dtype.type) 97 | _evt, (out_dev,) = fft_func(actx.queue, y=inp_dev) 98 | 99 | assert np.allclose(actx.to_numpy(out_dev), out) 100 | 101 | # }}} 102 | 103 | 104 | # You can test individual routines by typing 105 | # $ python test_tools.py 'test_fft(_acf, 30)' 106 | 107 | if __name__ == "__main__": 108 | if len(sys.argv) > 1: 109 | exec(sys.argv[1]) 110 | else: 111 | pytest.main([__file__]) 112 | 113 | # vim: fdm=marker 114 | --------------------------------------------------------------------------------