├── tests ├── __init__.py └── test_pyfftlog.py ├── examples ├── README.rst ├── contrib │ ├── README.rst │ └── sinetransform.py ├── geophysical_em.py └── fftlogtest.py ├── .gitattributes ├── docs ├── changelog.rst ├── code.rst ├── index.rst ├── _templates │ └── layout.html ├── Makefile ├── _static │ └── style.css ├── references.rst └── conf.py ├── .git_archival.txt ├── MANIFEST.in ├── .gitignore ├── .readthedocs.yml ├── pyfftlog ├── __init__.py └── pyfftlog.py ├── CHANGELOG.rst ├── Makefile ├── pyproject.toml ├── README.rst ├── .github └── workflows │ └── pytest.yml └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/README.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ******** 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .git_archival.txt export-subst 2 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /examples/contrib/README.rst: -------------------------------------------------------------------------------- 1 | Examples contributed by users 2 | ============================= 3 | -------------------------------------------------------------------------------- /docs/code.rst: -------------------------------------------------------------------------------- 1 | Manual and API 2 | ############## 3 | 4 | .. automodule:: pyfftlog.pyfftlog 5 | :members: 6 | -------------------------------------------------------------------------------- /.git_archival.txt: -------------------------------------------------------------------------------- 1 | node: 0791d44fc4be487f054c6ce5fc4e768fa33a2405 2 | node-date: 2025-12-12T11:20:16+01:00 3 | describe-name: v0.2.1-2-g0791d44 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | prune docs 2 | prune tests 3 | prune examples 4 | prune .github 5 | exclude MANIFEST.in 6 | exclude CHANGELOG.rst 7 | exclude Makefile 8 | exclude .gitignore 9 | exclude .readthedocs.yml 10 | exclude .git_archival.txt 11 | exclude .gitattributes 12 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. _pyfftlog-manual: 2 | 3 | ######## 4 | pyfftlog 5 | ######## 6 | 7 | Version: |version| ~ Date: |today| 8 | 9 | .. include:: ../README.rst 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :hidden: 14 | :caption: User Manual 15 | 16 | code 17 | examples/index 18 | references 19 | changelog 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Directories 2 | __pycache__/ 3 | 4 | # Sphinx 5 | docs/_build/ 6 | docs/examples/ 7 | docs/sg_execution_times.rst 8 | 9 | # Pytest and coverage related 10 | htmlcov 11 | .coverage 12 | .coverage.* 13 | .pytest_cache/ 14 | 15 | # setuptools_scm 16 | pyfftlog/version.py 17 | 18 | # Build related 19 | .eggs/ 20 | build/ 21 | dist/ 22 | pyfftlog.egg-info/ 23 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block menu %} 4 | {{ super() }} 5 | 6 | {% if menu_links %} 7 |

8 | {{ menu_links_name }} 9 |

10 | 15 | {% endif %} 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Required 2 | version: 2 3 | 4 | build: 5 | os: "ubuntu-22.04" 6 | tools: 7 | python: "3.11" 8 | 9 | # Build documentation in the docs/ directory with Sphinx 10 | sphinx: 11 | configuration: docs/conf.py 12 | 13 | # Optionally build your docs in additional formats such as PDF and ePub 14 | formats: [] 15 | 16 | # Optionally set the version of Python and requirements required to build your 17 | # docs 18 | python: 19 | install: 20 | - method: pip 21 | path: . 22 | extra_requirements: 23 | - docs 24 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Command line options. 2 | SPHINXOPTS = 3 | SPHINXBUILD = sphinx-build 4 | SPHINXPROJ = empymod 5 | SOURCEDIR = . 6 | BUILDDIR = _build 7 | 8 | # Will also be triggered if "make" is provided without argument. 9 | help: 10 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 11 | 12 | .PHONY: help Makefile 13 | 14 | # Catch-all target: route all unknown targets to Sphinx using the new 15 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 16 | %: Makefile 17 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 18 | -------------------------------------------------------------------------------- /docs/_static/style.css: -------------------------------------------------------------------------------- 1 | /* Customized styles */ 2 | 3 | /* Sidebar header background */ 4 | .wy-side-nav-search, .wy-nav-top { 5 | background: #996666; 6 | } 7 | 8 | /* Sidebar header text */ 9 | .wy-side-nav-search > div.version { 10 | color: #ffffff; 11 | } 12 | 13 | /* Navigation headings */ 14 | .wy-menu .caption-text { 15 | color: #cc3333; 16 | font-weight: bold; 17 | 18 | } 19 | 20 | /* To fix equation numbering in rtd theme. */ 21 | span.eqno { 22 | margin-left: 5px; 23 | float: right; 24 | } 25 | span.eqno a { 26 | display: none; 27 | } 28 | 29 | /* Don't let captions be italic in Sphinx-Gallery */ 30 | .rst-content div.figure p.caption { 31 | font-style: normal; 32 | } 33 | -------------------------------------------------------------------------------- /docs/references.rst: -------------------------------------------------------------------------------- 1 | References 2 | ########## 3 | 4 | .. _references: 5 | 6 | .. [Hami00] Hamilton, A. J. S., 2000, Uncorrelated modes of the non-linear 7 | power spectrum: Monthly Notices of the Royal Astronomical Society, 312, 8 | pages 257--284; DOI: `10.1046/j.1365-8711.2000.03071.x 9 | `_; Website of FFTLog: 10 | `jila.colorado.edu/~ajsh/FFTLog `_. 11 | .. [Talm78] Talman, J. D., 1978, Numerical Fourier and Bessel transforms in 12 | logarithmic variables: Journal of Computational Physics, 29, pages 35--48; 13 | DOI: `10.1016/0021-9991(78)90107-9 14 | `_. 15 | -------------------------------------------------------------------------------- /pyfftlog/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pyfftlog.pyfftlog import fhti, fftl, fht, fhtq, krgood 3 | 4 | __all__ = ['fhti', 'fftl', 'fht', 'fhtq', 'krgood'] 5 | 6 | # Version 7 | try: 8 | # - Released versions just tags: 1.10.0 9 | # - GitHub commits add .dev#+hash: 1.10.1.dev3+g973038c 10 | # - Uncommitted changes add timestamp: 1.10.1.dev3+g973038c.d20191022 11 | from .version import version as __version__ 12 | except ImportError: 13 | # If it was not installed, then we don't know the version. We could throw a 14 | # warning here, but this case *should* be rare. pyfftlog should be 15 | # installed properly! 16 | __version__ = 'unknown-'+datetime.today().strftime('%Y%m%d') 17 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ######### 3 | 4 | 5 | v0.2.1 : emsig 6 | -------------- 7 | 8 | **2024-10-18** 9 | 10 | Maintenance 11 | 12 | - Move from ``prisae`` to ``emsig``. 13 | - Changed from ``setup.py`` to ``pyproject.toml``. 14 | - Ready for numpy v2. 15 | - Updated all CI. 16 | - Change from ``master`` to ``main``. 17 | - Add Makefile. 18 | - Add monthly tests. 19 | 20 | 21 | v0.2.0 : First packaged release 22 | ------------------------------- 23 | 24 | **2020-05-16** 25 | 26 | First packaged release on PyPi and conda-forge. This includes: 27 | 28 | - Re-structuring the repo. 29 | - Add a proper documentation, https://pyfftlog.readthedocs.io, convert 30 | plain-text math into LaTeX, and add a reference section. 31 | - Add example notebook as sphinx-gallery to docs. 32 | - Add tests and CI on Travis, https://travis-ci.org/github/prisae/pyfftlog. 33 | - Link to Zenodo, https://zenodo.org/record/3830366. 34 | - PEP8 checking and coveralls (https://coveralls.io/github/prisae/pyfftlog). 35 | - Add the relevant badges to README. 36 | 37 | 38 | v0.1.1 : Bugfix uneven values 39 | ----------------------------- 40 | 41 | **2019-08-16** 42 | 43 | - Small bugfix for uneven values. 44 | 45 | 46 | v0.1.0 : Initial upload to GitHub 47 | --------------------------------- 48 | 49 | **2016-12-09** 50 | 51 | - Initially working version uploaded to GitHub. 52 | -------------------------------------------------------------------------------- /tests/test_pyfftlog.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.testing import assert_allclose 3 | 4 | from pyfftlog import fhti, fht 5 | 6 | 7 | def test_fftlog(): 8 | # This is the example provided in `fftlogtest.f` of the original code. It 9 | # is the same test that is shown as an example in the gallery. 10 | # See the example for more details. 11 | 12 | # Parameters. 13 | logrmin = -4 14 | logrmax = 4 15 | n = 64 16 | mu = 0 17 | q = 0 18 | kr = 1 19 | kropt = 1 20 | tdir = 1 21 | logrc = (logrmin + logrmax)/2 22 | nc = (n + 1)/2.0 23 | dlogr = (logrmax - logrmin)/n 24 | dlnr = dlogr*np.log(10.0) 25 | 26 | # Calculate input function: $r^{\mu+1}\exp\left(-\frac{r^2}{2}\right)$. 27 | r = 10**(logrc + (np.arange(1, n+1) - nc)*dlogr) 28 | ar = r**(mu + 1)*np.exp(-r**2/2.0) 29 | 30 | # Initialize FFTLog transform - note fhti resets `kr` 31 | kr, xsave = fhti(n, mu, dlnr, q, kr, kropt) 32 | assert_allclose(kr, 0.953538967579) 33 | logkc = np.log10(kr) - logrc 34 | # rk = 10**(logrc - logkc) 35 | 36 | # Transform 37 | # ak = fftl(ar.copy(), xsave, rk, tdir) 38 | ak = fht(ar.copy(), xsave, tdir) 39 | 40 | # Calculate Output function: $k^{\mu+1}\exp\left(-\frac{k^2}{2}\right)$ 41 | k = 10**(logkc + (np.arange(1, n+1) - nc)*dlogr) 42 | theo = k**(mu + 1)*np.exp(-k**2/2.0) 43 | 44 | # Check values 45 | assert_allclose(theo[theo > 1e-3], ak[theo > 1e-3], rtol=1e-8, atol=5e-5) 46 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "Commands:" 3 | @echo "" 4 | @echo " install install in editable mode" 5 | @echo " dev-install install in editable mode with dev requirements" 6 | @echo " pytest run the test suite and report coverage" 7 | @echo " flake8 style check with flake8" 8 | @echo " html build docs (update existing)" 9 | @echo " html-clean build docs (new, removing any existing)" 10 | @echo " preview renders docs in Browser" 11 | @echo " linkcheck check all links in docs" 12 | @echo " clean clean up all generated files" 13 | @echo "" 14 | 15 | install: 16 | python -m pip install -e . 17 | 18 | dev-install: 19 | python -m pip install -e .[all] 20 | 21 | .ONESHELL: 22 | pytest: 23 | rm -rf .coverage htmlcov/ .pytest_cache/ 24 | pytest --cov=pyfftlog --mpl 25 | coverage html 26 | 27 | flake8: 28 | flake8 docs/conf.py pyfftlog/ tests/ examples/ 29 | 30 | html: 31 | cd docs && make html 32 | 33 | html-clean: 34 | cd docs && rm -rf examples/* _build/ && make html 35 | 36 | preview: 37 | xdg-open docs/_build/html/index.html 38 | 39 | linkcheck: 40 | cd docs && make linkcheck 41 | 42 | clean: 43 | python -m pip uninstall pyfftlog -y 44 | rm -rf build/ dist/ .eggs/ pyfftlog.egg-info/ pyfftlog/version.py # build 45 | rm -rf */__pycache__/ */*/__pycache__/ # python cache 46 | rm -rf .coverage htmlcov/ .pytest_cache/ # tests and coverage 47 | rm -rf docs/_build/ docs/examples/* # docs 48 | rm docs/sg_execution_times.rst 49 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=64", "setuptools_scm>=8"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pyfftlog" 7 | description = "Logarithmic Fast Fourier Transform" 8 | readme = "README.rst" 9 | requires-python = ">=3.10" 10 | authors = [ 11 | {name = "The emsig community", email = "info@emsig.xyz"}, 12 | ] 13 | dependencies = [ 14 | "numpy", 15 | "scipy>=1.10", 16 | ] 17 | classifiers = [ 18 | "Development Status :: 5 - Production/Stable", 19 | "License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", 20 | "Programming Language :: Python", 21 | ] 22 | dynamic = ["version"] 23 | 24 | [project.license] 25 | file = "LICENSE" 26 | 27 | [project.urls] 28 | Homepage = "https://pyfftlog.rtfd.io" 29 | Documentation = "https://pyfftlog.rtfd.io" 30 | Repository = "https://github.com/emsig/pyfftlog" 31 | 32 | [project.optional-dependencies] 33 | docs = [ 34 | "ipympl", 35 | "sphinx", 36 | "empymod", 37 | "numpydoc", 38 | "ipykernel", 39 | "matplotlib", 40 | "pickleshare", 41 | "sphinx_gallery", 42 | "sphinx-rtd-theme", 43 | ] 44 | tests = [ 45 | "flake8", 46 | "pytest", 47 | "coveralls", 48 | "pytest_cov", 49 | "flake8-pyproject", 50 | ] 51 | all = [ 52 | "pyfftlog[docs]", 53 | "pyfftlog[tests]", 54 | ] 55 | build = [ 56 | "setuptools_scm>=8", 57 | "setuptools>=64", 58 | ] 59 | 60 | [tool.setuptools.packages.find] 61 | include = ["pyfftlog*"] 62 | 63 | [tool.setuptools_scm] 64 | version_file = "pyfftlog/version.py" 65 | 66 | [tool.coverage.run] 67 | relative_files = true 68 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | `pyfftlog` - A python version of FFTLog 2 | ======================================= 3 | 4 | This is a python version of the logarithmic FFT code *FFTLog* as presented in 5 | Appendix B of `Hamilton (2000) 6 | `_ and published at 7 | `jila.colorado.edu/~ajsh/FFTLog `_. 8 | 9 | A simple `f2py`-wrapper (`fftlog`) can be found on `github.com/emsig/fftlog 10 | `_. Tests have shown that `fftlog` is a bit 11 | faster than `pyfftlog`, but `pyfftlog` is easier to implement, as you only need 12 | `NumPy` and `SciPy`, without the need to compile anything. 13 | 14 | I hope that `FFTLog` will make it into `SciPy` in the future, which will make 15 | this project redundant. (If you have the bandwidth and are willing to chip in 16 | have a look at `SciPy PR #7310 `_.) 17 | 18 | Be aware that `pyfftlog` has not been tested extensively. It works fine for the 19 | test from the original code, and my use case, which is `pyfftlog.fftl` with 20 | `mu=0.5` (sine-transform), `q=0` (unbiased), `k=1`, `kropt=1`, and `tdir=1` 21 | (forward). Please let me know if you encounter any issues. 22 | 23 | - **Documentation**: https://pyfftlog.readthedocs.io 24 | - **Source Code**: https://github.com/emsig/pyfftlog 25 | 26 | 27 | Description of FFTLog from the FFTLog-Website 28 | --------------------------------------------- 29 | 30 | FFTLog is a set of fortran subroutines that compute the fast Fourier or Hankel 31 | (= Fourier-Bessel) transform of a periodic sequence of logarithmically spaced 32 | points. 33 | 34 | FFTLog can be regarded as a natural analogue to the standard Fast Fourier 35 | Transform (FFT), in the sense that, just as the normal FFT gives the exact (to 36 | machine precision) Fourier transform of a linearly spaced periodic sequence, so 37 | also FFTLog gives the exact Fourier or Hankel transform, of arbitrary order m, 38 | of a logarithmically spaced periodic sequence. 39 | 40 | FFTLog shares with the normal FFT the problems of ringing (response to sudden 41 | steps) and aliasing (periodic folding of frequencies), but under appropriate 42 | circumstances FFTLog may approximate the results of a continuous Fourier or 43 | Hankel transform. 44 | 45 | The FFTLog algorithm was originally proposed by `Talman (1978) 46 | `_. 47 | 48 | *For the full documentation, see* `jila.colorado.edu/~ajsh/FFTLog 49 | `_. 50 | 51 | 52 | Installation 53 | ------------ 54 | 55 | You can install pyfftlog either via **conda**: 56 | 57 | .. code-block:: console 58 | 59 | conda install -c conda-forge pyfftlog 60 | 61 | or via **pip**: 62 | 63 | .. code-block:: console 64 | 65 | pip install pyfftlog 66 | 67 | 68 | License, Citation, and Credits 69 | ------------------------------ 70 | 71 | Released to the public domain under the `CC0 1.0 License 72 | `_. 73 | 74 | All releases have a Zenodo-DOI, which can be found on `10.5281/zenodo.3830364 75 | `_. 76 | 77 | Be kind and give credits by citing `Hamilton (2000) 78 | `_. See the 79 | `references-section 80 | `_ in the manual for 81 | full references. 82 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import time 2 | import warnings 3 | from pyfftlog import __version__ 4 | 5 | # ==== 1. Extensions ==== 6 | 7 | # Load extensions 8 | extensions = [ 9 | 'sphinx.ext.autodoc', 10 | 'sphinx.ext.mathjax', 11 | 'sphinx.ext.viewcode', 12 | 'sphinx.ext.todo', 13 | 'sphinx.ext.intersphinx', 14 | 'numpydoc', 15 | 'sphinx_gallery.gen_gallery', 16 | ] 17 | 18 | # Numpydoc settings 19 | numpydoc_show_class_members = False 20 | # numfig = True 21 | # numfig_format = {'figure': 'Figure %s:'} 22 | 23 | # Todo settings 24 | todo_include_todos = True 25 | 26 | # Sphinx gallery configuration 27 | sphinx_gallery_conf = { 28 | 'examples_dirs': [ 29 | '../examples', 30 | ], 31 | 'gallery_dirs': [ 32 | 'examples', 33 | ], 34 | 'capture_repr': ('_repr_html_', '__repr__'), 35 | # Patter to search for example files 36 | "filename_pattern": r"\.py", 37 | # Sort gallery example by file name instead of number of lines (default) 38 | "within_subsection_order": "FileNameSortKey", 39 | # Remove the settings (e.g., sphinx_gallery_thumbnail_number) 40 | 'remove_config_comments': True, 41 | # Show memory 42 | 'show_memory': True, 43 | # Custom first notebook cell 44 | 'first_notebook_cell': '%matplotlib notebook', 45 | } 46 | 47 | # https://github.com/sphinx-gallery/sphinx-gallery/pull/521/files 48 | # Remove matplotlib agg warnings from generated doc when using plt.show 49 | warnings.filterwarnings("ignore", category=UserWarning, 50 | message='Matplotlib is currently using agg, which is a' 51 | ' non-GUI backend, so cannot show the figure.') 52 | 53 | # Intersphinx configuration 54 | intersphinx_mapping = { 55 | "numpy": ("https://docs.scipy.org/doc/numpy", None), 56 | "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), 57 | } 58 | 59 | # ==== 2. General Settings ==== 60 | description = 'Logarithmic Fast Fourier Transform.' 61 | 62 | # The name of the Pygments (syntax highlighting) style to use. 63 | pygments_style = 'friendly' 64 | 65 | # The templates path. 66 | templates_path = ['_templates'] 67 | 68 | # The suffix(es) of source filenames. 69 | source_suffix = '.rst' 70 | 71 | # The master toctree document. 72 | master_doc = 'index' 73 | 74 | # General information about the project. 75 | project = 'pyfftlog' 76 | copyright = u'2016-{}, Dieter Werthmüller.'.format(time.strftime("%Y")) 77 | author = 'Dieter Werthmüller' 78 | 79 | # |version| and |today| tags (|release|-tag is not used). 80 | version = __version__ 81 | release = __version__ 82 | today_fmt = '%d %B %Y' 83 | 84 | # List of patterns to ignore, relative to source directory. 85 | exclude_patterns = ['_build', '../tests'] 86 | 87 | # ==== 3. HTML settings ==== 88 | html_theme = 'sphinx_rtd_theme' 89 | html_theme_options = { 90 | 'logo_only': True, 91 | 'prev_next_buttons_location': 'both', 92 | } 93 | html_static_path = ['_static'] 94 | html_sidebars = { 95 | '**': [ 96 | 'about.html', 97 | 'navigation.html', 98 | 'searchbox.html', 99 | ] 100 | } 101 | 102 | html_context = { 103 | 'menu_links_name': 'Links', 104 | 'menu_links': [ 105 | (' Source Code', 106 | 'https://github.com/emsig/pyfftlog'), 107 | ], 108 | } 109 | 110 | htmlhelp_basename = 'pyfftlogdoc' 111 | html_css_files = [ 112 | "style.css", 113 | ] 114 | 115 | # ==== 4. Other Document Type Settings ==== 116 | # Options for LaTeX output 117 | latex_elements = { 118 | 'papersize': 'a4paper', 119 | 'pointsize': '10pt', 120 | } 121 | latex_documents = [ 122 | (master_doc, 'pyfftlog.tex', 'pyfftlog Documentation', 123 | 'Dieter Werthmüller', 'manual'), 124 | ] 125 | 126 | # Options for manual page output 127 | man_pages = [ 128 | (master_doc, 'pyfftlog', 'pyfftlog Documentation', 129 | [author], 1) 130 | ] 131 | 132 | # Options for Texinfo output 133 | texinfo_documents = [ 134 | (master_doc, 'pyfftlog', 'pyfftlog Documentation', 135 | author, 'pyfftlog', description, 136 | 'Logarithmic Fast Fourier Transform'), 137 | ] 138 | -------------------------------------------------------------------------------- /examples/contrib/sinetransform.py: -------------------------------------------------------------------------------- 1 | r""" 2 | Sine Transform 3 | ============== 4 | 5 | Contributed by `@ShazAlvi `_. 6 | 7 | This is a simple test program to illustrate how the sine (or cosine as it works 8 | basically the same way ) Fourier transform works using `FFTLog`. The test 9 | provides as input as sine function and performs the sine Fourier transform. The 10 | input function is then recovered by performing an inverse Fourier transform. 11 | The inverse is performed using the following integral, 12 | 13 | .. math:: 14 | :label: sinetest 15 | 16 | F(t) = \sqrt{\frac{\pi}{2}}\int^\infty_0 A(f)\ \sin(ft) \ \text{d}f \ . 17 | 18 | """ 19 | import pyfftlog 20 | import numpy as np 21 | import scipy as sp 22 | import matplotlib.pyplot as plt 23 | 24 | ############################################################################### 25 | # Define the parameters you wish to use 26 | # ------------------------------------- 27 | # The presets are the *Reasonable choices of parameters* from `fftlogtest.f`. 28 | 29 | # Range of periodic interval 30 | logtmin = -3 31 | # logtmax = 0.798 #2pi 32 | # 5(2pi) #Longer range in r gives you a better reconstruction. 10\pi will give 33 | # you a better reconstruction than 2\pi. 34 | logtmax = 1.497 35 | # Number of points (Max 4096) 36 | # 1000 points give you a fairly smooth distribution of af in frequency, f. 37 | # However you can get a good, working fit for 300 points as well. 38 | n = 1000 39 | 40 | # Order mu of Bessel function 41 | mu = 0.5 # Choose -0.5 for cosine fourier transform 42 | 43 | # Bias exponent: q = 0 is unbiased 44 | # The unbiased transforms give better results as far as I checked. 45 | q = 0 46 | # Sensible approximate choice of f_c t_c 47 | # The output and the reconstruction is sensitive to the choice of this value 48 | # This value is found by trial and error. In this example, the input function 49 | # is a simple sine function which is not smooth in frequency space (as it 50 | # only has one frequency) because of this reason a better value of this 51 | # quantity is not found by the function fhti. For functions smooth 52 | # in both time and frequency domain, the fhti should return the best 53 | # value of the f_c t_c. 54 | 55 | ft = 0.016 56 | 57 | # Tell fhti to change ft to low-ringing value 58 | # WARNING: kropt = 3 will fail, as interaction is not supported 59 | ftopt = 1 60 | 61 | # Forward transform (changed from dir to tdir, as dir is a python fct) 62 | tdir = 1 63 | 64 | ############################################################################### 65 | # Computation related to the logarithmic spacing 66 | # ---------------------------------------------- 67 | 68 | # Central point log10(t_c) of periodic interval 69 | logtc = (logtmin + logtmax)/2 70 | 71 | print(f"Central point of periodic interval at log10(t_c) = {logtc}") 72 | 73 | # Central index (1/2 integral if n is even) 74 | nc = (n + 1)/2.0 75 | 76 | # Log-spacing of points 77 | dlogt = (logtmax - logtmin)/n 78 | 79 | dlnr = dlogt*np.log(10.0) 80 | 81 | 82 | ############################################################################### 83 | # Compute input function: :math:`\sin(t)` 84 | # --------------------------------------- 85 | 86 | t = 10**(logtc + (np.arange(1, n+1) - nc)*dlogt) 87 | a_t = np.sin(t) 88 | 89 | ############################################################################### 90 | # Initialize FFTLog transform - note `fhti` resets `ft` 91 | # ----------------------------------------------------- 92 | 93 | ft, xsave = pyfftlog.fhti(n, mu, dlnr, q, ft, ftopt) 94 | 95 | ############################################################################### 96 | # Call `pyfftlog.fhtl` 97 | # -------------------- 98 | 99 | logfc = np.log10(ft) - logtc 100 | 101 | # Fourier sine Transform 102 | a_f = pyfftlog.fftl(a_t.copy(), xsave, np.sqrt(2/np.pi), tdir) 103 | # Notice that np.sqrt(2/np.pi) is the normalization factor for the transform 104 | # Reconstruct the input function by taking the inverse fourier transform as 105 | # given in the description 106 | f = 10**(logfc + (np.arange(1, n+1) - nc)*dlogt) 107 | # Array to store the reconstructed function for each value of t 108 | Recon_Fun = np.zeros((len(t))) 109 | for i in range(len(t)): 110 | Recon_Fun[i] = (np.sqrt(2/np.pi)**-1) * \ 111 | sp.integrate.trapezoid(f, a_f*np.sin(t[i]*f)) 112 | 113 | # Plotting the input function and the reconstructed input function and also 114 | # the distribution of the a(f) vs f. 115 | plt.figure() 116 | 117 | ax1 = plt.subplot(121) 118 | plt.title(r'Frequency domain') 119 | plt.xlabel('f') 120 | plt.ylabel(r'$a_f(f)$') 121 | plt.semilogx(f, a_f, 'k') 122 | 123 | ax2 = plt.subplot(122) 124 | plt.title('Time domain') 125 | plt.xlabel("t") 126 | plt.ylabel("sin(t)") 127 | plt.semilogx(t, a_t, lw=2, label=r'$\sin(t)$') 128 | plt.semilogx(t, -Recon_Fun, '--', label='Reconstructed') 129 | plt.legend() 130 | 131 | plt.tight_layout() 132 | plt.show() 133 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: pytest 2 | 3 | # Only build PRs, the main branch, and releases. 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | release: 10 | types: 11 | - published 12 | schedule: 13 | - cron: "14 14 20 * *" 14 | 15 | # Cancel any previous run of the test job. 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | test: 22 | 23 | name: ${{ matrix.os }} py${{ matrix.python-version }} 24 | runs-on: ${{ matrix.os }}-latest 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | os: [ubuntu, ] # macos, windows] # Only Linux currently. 29 | python-version: ["3.11", "3.12", "3.13", "3.14"] 30 | 31 | env: 32 | # Used for coveralls flag 33 | OS: ${{ matrix.os }} 34 | PYTHON: ${{ matrix.python-version }} 35 | 36 | steps: 37 | 38 | # Checks-out your repository under $GITHUB_WORKSPACE 39 | - name: Checkout 40 | uses: actions/checkout@v6.0.1 41 | with: 42 | # Need to fetch more than the last commit so that setuptools_scm can 43 | # create the correct version string. If the number of commits since 44 | # the last release is greater than this, the version still be wrong. 45 | # Increase if necessary. 46 | fetch-depth: 100 47 | # The GitHub token is preserved by default but this job doesn't need 48 | # to be able to push to GitHub. 49 | persist-credentials: false 50 | 51 | # Need the tags so that setuptools_scm can form a valid version number 52 | - name: Fetch git tags 53 | run: git fetch origin 'refs/tags/*:refs/tags/*' 54 | 55 | - name: Setup Python 56 | uses: actions/setup-python@v6.1.0 57 | with: 58 | python-version: ${{ matrix.python-version }} 59 | 60 | - name: Install dependencies 61 | run: | 62 | python -m pip install --upgrade pip 63 | python -m pip install .[all] 64 | 65 | - name: Flake8 66 | run: flake8 docs/conf.py pyfftlog/ tests/ examples/ 67 | 68 | - name: Test with pytest 69 | run: pytest --cov=pyfftlog 70 | 71 | - name: Coveralls 72 | # [pin @develop@20240509] 73 | uses: AndreMiras/coveralls-python-action@65c1672f0b8a201702d86c81b79187df74072505 74 | with: 75 | parallel: true 76 | flag-name: ${{ matrix.python-version }} (${{ matrix.os }}) 77 | 78 | 79 | coveralls_finish: 80 | needs: test 81 | runs-on: ubuntu-latest 82 | steps: 83 | - name: Coveralls Finished 84 | # [pin @develop@20240509] 85 | uses: AndreMiras/coveralls-python-action@65c1672f0b8a201702d86c81b79187df74072505 86 | with: 87 | parallel-finished: true 88 | 89 | deploy: 90 | needs: test 91 | name: Deploy to PyPI 92 | runs-on: ubuntu-latest 93 | # Only from the origin repository, not forks; only main and tags. 94 | if: github.repository_owner == 'emsig' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) 95 | 96 | steps: 97 | # Checks-out your repository under $GITHUB_WORKSPACE 98 | - name: Checkout 99 | uses: actions/checkout@v6.0.1 100 | with: 101 | # Need to fetch more than the last commit so that setuptools_scm can 102 | # create the correct version string. If the number of commits since 103 | # the last release is greater than this, the version will still be 104 | # wrong. Increase if necessary. 105 | fetch-depth: 100 106 | # The GitHub token is preserved by default but this job doesn't need 107 | # to be able to push to GitHub. 108 | persist-credentials: false 109 | 110 | # Need the tags so that setuptools_scm can form a valid version number 111 | - name: Fetch git tags 112 | run: git fetch origin 'refs/tags/*:refs/tags/*' 113 | 114 | - name: Setup Python 115 | uses: actions/setup-python@v6.1.0 116 | with: 117 | python-version: "3.13" 118 | 119 | - name: Install dependencies 120 | run: | 121 | python -m pip install --upgrade pip 122 | python -m pip install build setuptools-scm 123 | 124 | - name: Build source and wheel distributions 125 | if: github.ref == 'refs/heads/main' 126 | run: | 127 | # Change setuptools-scm local_scheme to "no-local-version" so the 128 | # local part of the version isn't included, making the version string 129 | # compatible with Test PyPI. 130 | sed --in-place 's/version_file/local_scheme = "no-local-version"\nversion_file/g' pyproject.toml 131 | 132 | - name: Build source and wheel distributions 133 | run: | 134 | # Build source and wheel packages 135 | python -m build 136 | echo "" 137 | echo "Generated files:" 138 | ls -lh dist/ 139 | 140 | - name: Publish to Test PyPI 141 | if: success() 142 | uses: pypa/gh-action-pypi-publish@v1.13.0 143 | with: 144 | user: __token__ 145 | password: ${{ secrets.TEST_PYPI_PASSWORD }} 146 | repository_url: https://test.pypi.org/legacy/ 147 | # Allow existing releases on test PyPI without errors. 148 | # NOT TO BE USED in PyPI! 149 | skip_existing: true 150 | 151 | - name: Publish to PyPI 152 | # Only for releases 153 | if: success() && github.event_name == 'release' 154 | uses: pypa/gh-action-pypi-publish@v1.13.0 155 | with: 156 | user: __token__ 157 | password: ${{ secrets.PYPI_PASSWORD }} 158 | -------------------------------------------------------------------------------- /examples/geophysical_em.py: -------------------------------------------------------------------------------- 1 | r""" 2 | Geophysical Electromagnetic modelling 3 | ===================================== 4 | 5 | In this example we use `pyfftlog` to obtain time-domain EM data from 6 | frequency-domain data and vice versa. We do this by using analytical 7 | halfspace solution in both domains, and comparing the transformed responses to 8 | the true result. The analytical halfspace solutions are computed using 9 | `empymod` (see https://empymod.github.io). 10 | """ 11 | import empymod 12 | import pyfftlog 13 | import numpy as np 14 | import matplotlib.pyplot as plt 15 | from scipy.interpolate import InterpolatedUnivariateSpline as iuSpline 16 | 17 | 18 | ############################################################################### 19 | # Model and Survey parameters 20 | # --------------------------- 21 | 22 | # Impulse response (in the time domain) 23 | signal = 0 24 | 25 | # x-directed electric source and receiver point-dipoles 26 | ab = 11 27 | 28 | # We use the same range of times (s) and frequencies (Hz) 29 | ftpts = np.logspace(-4, 4, 301) 30 | 31 | # Source and receiver 32 | src = [0, 0, 100] # At the origin, 100 m below surface 33 | rec = [6000, 0, 200] # At an inline offset of 6 km, 200 m below surface 34 | 35 | # Resistivity 36 | depth = [0] # Interface at z = 0, default for empymod.analytical 37 | res = [2e14, 1] # Horizontal resistivity [air, subsurface] 38 | aniso = [1, 2] # Anisotropy [air, subsurface] 39 | 40 | # Collect parameters 41 | analytical = { 42 | 'src': src, 43 | 'rec': rec, 44 | 'res': res[1], 45 | 'aniso': aniso[1], 46 | 'solution': 'dhs', # Diffusive half-space solution 47 | 'verb': 2, 48 | 'ab': ab, 49 | } 50 | 51 | dipole = { 52 | 'src': src, 53 | 'rec': rec, 54 | 'depth': depth, 55 | 'res': res, 56 | 'aniso': aniso, 57 | 'ht': 'dlf', 58 | 'verb': 2, 59 | 'ab': ab, 60 | } 61 | 62 | 63 | ############################################################################### 64 | # Analytical solutions 65 | # -------------------- 66 | 67 | # Frequency Domain 68 | f_ana = empymod.analytical(**analytical, freqtime=ftpts) 69 | 70 | # Time Domain 71 | t_ana = empymod.analytical(**analytical, freqtime=ftpts, signal=signal) 72 | 73 | 74 | ############################################################################### 75 | # FFTLog 76 | # ------ 77 | 78 | # FFTLog parameters 79 | pts_per_dec = 5 # Increase if not precise enough 80 | add_dec = [-2, 2] # e.g. [-2, 2] to add 2 decades on each side 81 | q = 0 # -1 - +1; can improve results 82 | 83 | # Compute minimum and maximum required inputs 84 | rmin = np.log10(1/ftpts.max()) + add_dec[0] 85 | rmax = np.log10(1/ftpts.min()) + add_dec[1] 86 | n = int(rmax - rmin)*pts_per_dec 87 | 88 | # Pre-allocate output 89 | f_resp = np.zeros(ftpts.shape, dtype=complex) 90 | 91 | # Loop over Sine, Cosine transform. 92 | for mu in [0.5, -0.5]: 93 | 94 | # Central point log10(r_c) of periodic interval 95 | logrc = (rmin + rmax)/2 96 | 97 | # Central index (1/2 integral if n is even) 98 | nc = (n + 1)/2. 99 | 100 | # Log spacing of points 101 | dlogr = (rmax - rmin)/n 102 | dlnr = dlogr*np.log(10.) 103 | 104 | # Compute required input x-values 105 | pts_req = 10**(logrc + (np.arange(1, n+1) - nc)*dlogr)/2/np.pi 106 | 107 | # Initialize FFTLog 108 | kr, xsave = pyfftlog.fhti(n, mu, dlnr, q, kr=1, kropt=1) 109 | 110 | # Compute pts_out with adjusted kr 111 | logkc = np.log10(kr) - logrc 112 | pts_out = 10**(logkc + (np.arange(1, n+1) - nc)*dlogr) 113 | 114 | # rk = r_c/k_r; adjust for Fourier transform scaling 115 | rk = 10**(logrc - logkc)*np.pi/2 116 | 117 | # Compute required times/frequencies with the analytical solution 118 | t2f_t_resp = empymod.analytical(**analytical, freqtime=pts_req, 119 | signal=signal) 120 | f2t_f_resp = empymod.analytical(**analytical, freqtime=pts_req) 121 | 122 | # Carry out FFTLog 123 | t2f_f_coarse = pyfftlog.fftl(t2f_t_resp, xsave.copy(), rk, 1) 124 | if mu > 0: 125 | f2t_t_coarse = pyfftlog.fftl(f2t_f_resp.imag, xsave.copy(), rk, 1) 126 | else: 127 | f2t_t_coarse = pyfftlog.fftl(f2t_f_resp.real, xsave.copy(), rk, 1) 128 | 129 | # Interpolate for required frequencies/times 130 | t2f_f_spline = iuSpline(np.log(pts_out), t2f_f_coarse) 131 | f2t_t_spline = iuSpline(np.log(pts_out), f2t_t_coarse) 132 | 133 | if mu > 0: 134 | f_resp += -1j*t2f_f_spline(np.log(ftpts))/np.pi/2 135 | t_resp_sin = -f2t_t_spline(np.log(ftpts))/np.pi*2 136 | else: 137 | f_resp += t2f_f_spline(np.log(ftpts))/np.pi/2 138 | t_resp_cos = f2t_t_spline(np.log(ftpts))/np.pi*2 139 | 140 | 141 | ############################################################################### 142 | # Comparison 143 | # ---------- 144 | 145 | fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(9, 4)) 146 | 147 | # TIME DOMAIN 148 | ax0.set_title(r'Frequency domain') 149 | ax0.set_xlabel('Frequency (Hz)') 150 | ax0.set_ylabel('Amplitude (V/m)') 151 | ax0.semilogx(ftpts, f_ana.real, 'k-', label='Analytical') 152 | ax0.semilogx(ftpts, f_ana.imag, 'k-') 153 | ax0.semilogx(ftpts, f_resp.real, 'C3--', label=r'FFTLog, $\mu=-0.5$') 154 | ax0.semilogx(ftpts, f_resp.imag, 'C2--', label=r'FFTLog, $\mu=+0.5$') 155 | ax0.legend(loc='best') 156 | ax0.grid(which='both', c='.95') 157 | 158 | # TIME DOMAIN 159 | ax1.set_title(r'Time domain') 160 | ax1.set_xlabel('Time (s)') 161 | ax1.set_ylabel('Amplitude (V/m)') 162 | ax1.semilogx(ftpts, t_ana, 'k', label='Analytical') 163 | ax1.semilogx(ftpts, t_resp_cos, 'C3--', label=r'FFTLog, $\mu=-0.5$') 164 | ax1.semilogx(ftpts, t_resp_sin, 'C2-.', label=r'FFTLog, $\mu=+0.5$') 165 | ax1.legend(loc='best') 166 | ax1.yaxis.set_label_position("right") 167 | ax1.yaxis.tick_right() 168 | ax1.grid(which='both', c='.95') 169 | 170 | fig.tight_layout() 171 | fig.show() 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /examples/fftlogtest.py: -------------------------------------------------------------------------------- 1 | r""" 2 | FFTLog-Test 3 | =========== 4 | 5 | This example is a translation of `fftlogtest.f` from the Fortran package 6 | `FFTLog`, which was presented in Appendix B of [Hami00]_ and published at 7 | https://jila.colorado.edu/~ajsh/FFTLog. It serves as an example for the python 8 | package `pyfftlog` (which is a Python version of `FFTLog`), in the same manner 9 | as the original file `fftlogtest.f` serves as an example for Fortran package 10 | `FFTLog`. 11 | 12 | **What follows is the original documentation from the file `fftlogtest.f`:** 13 | 14 | This is fftlogtest.f 15 | -------------------- 16 | 17 | This is a simple test program to illustrate how `FFTLog` works. The test 18 | transform is: 19 | 20 | .. math:: 21 | :label: hamtest1 22 | 23 | \int^\infty_0 r^{\mu+1} \exp\left(-\frac{r^2}{2} \right)\ 24 | J_\mu(k, r)\ k\ {\rm d}r = k^{\mu+1} \exp\left(-\frac{k^2}{2} 25 | \right) 26 | 27 | 28 | Disclaimer 29 | ''''''''''' 30 | 31 | `FFTLog` does NOT claim to provide the most accurate possible solution of the 32 | continuous transform (which is the stated aim of some other codes). Rather, 33 | `FFTLog` claims to solve the exact discrete transform of a 34 | logarithmically-spaced periodic sequence. If the periodic interval is wide 35 | enough, the resolution high enough, and the function well enough behaved 36 | outside the periodic interval, then `FFTLog` may yield a satisfactory 37 | approximation to the continuous transform. 38 | 39 | Observe: 40 | 41 | 1. How the result improves as the periodic interval is enlarged. With the 42 | normal FFT, one is not used to ranges orders of magnitude wide, but this is 43 | how `FFTLog` prefers it. 44 | 2. How the result improves as the resolution is increased. Because the function 45 | is rather smooth, modest resolution actually works quite well here. 46 | 3. That the central part of the transform is more reliable than the outer 47 | parts. Experience suggests that a good general strategy is to double the 48 | periodic interval over which the input function is defined, and then to 49 | discard the outer half of the transform. 50 | 4. That the best bias exponent seems to be :math:`q = 0`. 51 | 5. That for the critical index :math:`\mu = -1`, the result seems to be offset 52 | by a constant from the 'correct' answer. 53 | 6. That the result grows progressively worse as mu decreases below -1. 54 | 55 | The analytic integral above fails for :math:`\mu \le -1`, but `FFTLog` still 56 | returns answers. Namely, `FFTLog` returns the analytic continuation of the 57 | discrete transform. Because of ambiguity in the path of integration around 58 | poles, this analytic continuation is liable to differ, for :math:`\mu \le -1`, 59 | by a constant from the 'correct' continuation given by the above equation. 60 | 61 | `FFTLog` begins to have serious difficulties with aliasing as :math:`\mu` 62 | decreases below :math:`-1`, because then :math:`r^{\mu+1} \exp(-r^2/2)` is far 63 | from resembling a periodic function. You might have thought that it would help 64 | to introduce a bias exponent :math:`q = \mu`, or perhaps :math:`q = \mu+1`, or 65 | more, to make the function :math:`a(r) = A(r) r^{-q}` input to `fhtq` more 66 | nearly periodic. In practice a nonzero :math:`q` makes things worse. 67 | 68 | A symmetry argument lends support to the notion that the best exponent here 69 | should be :math:`q = 0,` as empirically appears to be true. The symmetry 70 | argument is that the function :math:`r^{\mu+1} \exp(-r^2/2)` happens to be the 71 | same as its transform :math:`k^{\mu+1} \exp(-k^2/2)`. If the best bias exponent 72 | were q in the forward transform, then the best exponent would be :math:`-q` 73 | that in the backward transform; but the two transforms happen to be the same in 74 | this case, suggesting :math:`q = -q`, hence :math:`q = 0`. 75 | 76 | This example illustrates that you cannot always tell just by looking at a 77 | function what the best bias exponent :math:`q` should be. You also have to look 78 | at its transform. The best exponent :math:`q` is, in a sense, the one that 79 | makes both the function and its transform look most nearly periodic. 80 | 81 | """ 82 | import pyfftlog 83 | import numpy as np 84 | import matplotlib.pyplot as plt 85 | 86 | 87 | ############################################################################### 88 | # Define the parameters you wish to use 89 | # ------------------------------------- 90 | # 91 | # The presets are the *Reasonable choices of parameters* from `fftlogtest.f`. 92 | 93 | # Range of periodic interval 94 | logrmin = -4 95 | logrmax = 4 96 | 97 | # Number of points (Max 4096) 98 | n = 64 99 | 100 | # Order mu of Bessel function 101 | mu = 0 102 | 103 | # Bias exponent: q = 0 is unbiased 104 | q = 0 105 | 106 | # Sensible approximate choice of k_c r_c 107 | kr = 1 108 | 109 | # Tell fhti to change kr to low-ringing value 110 | # WARNING: kropt = 3 will fail, as interaction is not supported 111 | kropt = 1 112 | 113 | # Forward transform (changed from dir to tdir, as dir is a python fct) 114 | tdir = 1 115 | 116 | 117 | ############################################################################### 118 | # Computation related to the logarithmic spacing 119 | # ---------------------------------------------- 120 | 121 | # Central point log10(r_c) of periodic interval 122 | logrc = (logrmin + logrmax)/2 123 | 124 | print(f"Central point of periodic interval at log10(r_c) = {logrc}") 125 | 126 | # Central index (1/2 integral if n is even) 127 | nc = (n + 1)/2.0 128 | 129 | # Log-spacing of points 130 | dlogr = (logrmax - logrmin)/n 131 | dlnr = dlogr*np.log(10.0) 132 | 133 | 134 | ############################################################################### 135 | # Compute input function: :math:`r^{\mu+1}\exp\left(-\frac{r^2}{2}\right)` 136 | # ------------------------------------------------------------------------ 137 | 138 | r = 10**(logrc + (np.arange(1, n+1) - nc)*dlogr) 139 | ar = r**(mu + 1)*np.exp(-r**2/2.0) 140 | 141 | 142 | ############################################################################### 143 | # Initialize FFTLog transform - note fhti resets `kr` 144 | # --------------------------------------------------- 145 | 146 | kr, xsave = pyfftlog.fhti(n, mu, dlnr, q, kr, kropt) 147 | print(f"pyfftlog.fhti: new kr = {kr}") 148 | 149 | ############################################################################### 150 | # Call `pyfftlog.fht` (or `pyfftlog.fhtl`) 151 | # ---------------------------------------- 152 | 153 | logkc = np.log10(kr) - logrc 154 | print(f"Central point in k-space at log10(k_c) = {logkc}") 155 | 156 | # rk = r_c/k_c 157 | rk = 10**(logrc - logkc) 158 | 159 | # Transform 160 | # ak = pyfftlog.fftl(ar.copy(), xsave, rk, tdir) 161 | ak = pyfftlog.fht(ar.copy(), xsave, tdir) 162 | 163 | ############################################################################### 164 | # Compute Output function: :math:`k^{\mu+1}\exp\left(-\frac{k^2}{2}\right)` 165 | # ------------------------------------------------------------------------- 166 | 167 | k = 10**(logkc + (np.arange(1, n+1) - nc)*dlogr) 168 | theo = k**(mu + 1)*np.exp(-k**2/2.0) 169 | 170 | ############################################################################### 171 | # Plot result 172 | # ----------- 173 | 174 | plt.figure() 175 | 176 | # Input 177 | ax1 = plt.subplot(121) 178 | plt.title(r'$r^{\mu+1}\ \exp(-r^2/2)$') 179 | plt.xlabel('r') 180 | 181 | plt.loglog(r, ar, 'k', lw=2) 182 | 183 | plt.grid(axis='y', c='0.9') 184 | 185 | # Transformed result 186 | ax2 = plt.subplot(122, sharey=ax1) 187 | plt.title(r'$k^{\mu+1} \exp(-k^2/2)$') 188 | plt.xlabel('k') 189 | 190 | plt.loglog(k, theo, 'k', lw=2, label='Theoretical') 191 | plt.loglog(k, ak, 'r--', lw=2, label='FFTLog') 192 | 193 | plt.legend() 194 | plt.ylim([1e-8, 1e1]) 195 | 196 | ax2.yaxis.tick_right() 197 | ax2.yaxis.set_label_position("right") 198 | plt.grid(axis='y', c='0.9') 199 | 200 | # Switch off spines 201 | ax1.spines['top'].set_visible(False) 202 | ax1.spines['right'].set_visible(False) 203 | ax2.spines['top'].set_visible(False) 204 | ax2.spines['left'].set_visible(False) 205 | plt.tight_layout(rect=[0, 0, 1, .9]) 206 | 207 | # Main title 208 | plt.suptitle(r"$\int_0^\infty r^{\mu+1}\ \exp(-r^2/2)\ J_\mu(k,r)\ " + 209 | r"k\ {\rm d}r = k^{\mu+1} \exp(-k^2/2)$", y=0.98) 210 | 211 | plt.show() 212 | 213 | ############################################################################### 214 | # Print values 215 | # ------------ 216 | 217 | print(' k a(k) k^(mu+1) exp(-k^2/2)') 218 | print('----------------------------------------------------------------') 219 | for i in range(n): 220 | print(f"{k[i]:18.6e} {ak[i]:18.6e} {theo[i]:18.6e}") 221 | -------------------------------------------------------------------------------- /pyfftlog/pyfftlog.py: -------------------------------------------------------------------------------- 1 | r""" 2 | 3 | `pyfftlog` -- Python version of FFTLog 4 | ====================================== 5 | 6 | This is a Python version of the FFTLog Fortran code by Andrew Hamilton, 7 | [Hami00]_. 8 | 9 | The function :obj:`scipy.special.loggamma` replaces the file `cdgamma.f` in 10 | the original code, and the functions :func:`scipy.fftpack.rfft` and 11 | :func:`scipy.fftpack.irfft` replace the files `drffti.f`, `drfftf.f`, and 12 | `drfftb.f` in the original code. 13 | 14 | The original documentation has just been adjusted where necessary, and put into 15 | a more pythonic format (e.g. using `Parameters` and `Returns` in the 16 | documentation'). 17 | 18 | **What follows is the original documentation from the file `fftlog.f`:** 19 | 20 | THE FFTLog CODE 21 | --------------- 22 | 23 | FFTLog computes the discrete Fast Fourier Transform or Fast Hankel Transform 24 | (of arbitrary real index) of a periodic logarithmic sequence. 25 | 26 | - Version of 13 Mar 2000. 27 | - For more information about FFTLog, see 28 | https://jila.colorado.edu/~ajsh/FFTLog. 29 | - Andrew J S Hamilton March 1999. 30 | - Refs: [Talm78]_. 31 | 32 | FFTLog computes a discrete version of the Hankel Transform (= Fourier-Bessel 33 | Transform) with a power law bias :math:`(k r)^q` 34 | 35 | .. math:: 36 | :label: ham1 37 | 38 | \tilde{a}(k) = \int^\infty_0 a(r) (k r)^q J_{\mu} (k r) k \,dr \, , 39 | 40 | .. math:: 41 | :label: ham2 42 | 43 | a(r) = \int^\infty_0 \tilde{a}(k) (k r)^{-q} J_{\mu} (k r) r \,dk \, , 44 | 45 | where :math:`J_{\mu}` is the Bessel function of order :math:`\mu`. The index 46 | :math:`\mu` may be any real number, positive or negative. 47 | 48 | The input array :math:`a_j` is a periodic sequence of length :math:`n`, 49 | uniformly logarithmically spaced with spacing :math:`dlnr` 50 | 51 | .. math:: 52 | :label: ham3 53 | 54 | a_j = a(r_j) \quad \text{at} \quad r_j = r_c \exp[(j-j_c) dlnr] 55 | 56 | centred about the point :math:`r_c`. The central index :math:`j_c = (n+1)/2` is 57 | 1/2 integral if :math:`n` is even. Similarly, the output array 58 | :math:`\tilde{a}_j` is a periodic sequence of length :math:`n`, also uniformly 59 | logarithmically spaced with spacing :math:`dlnr` 60 | 61 | .. math:: 62 | :label: ham4 63 | 64 | \tilde{a}_j = \tilde{a}(k_j) \quad \text{at} \quad 65 | k_j = k_c \exp[(j-j_c) dlnr] 66 | 67 | centred about the point :math:`k_c`. 68 | 69 | The centre points :math:`r_c` and :math:`k_c` of the periodic intervals may be 70 | chosen arbitrarily; but it would be normal to choose the product 71 | 72 | .. math:: 73 | :label: ham5 74 | 75 | kr = k_c r_c = k_j r_{(n+1-j)} = k_{(n+1-j)} r_j 76 | 77 | to be about 1 (or 2, or pi, to taste). 78 | 79 | The FFTLog algorithm is (see [Hami00]_): 80 | 81 | 1. FFT the input array :math:`a_j` to obtain the Fourier coefficients 82 | :math:`c_m` ; 83 | 2. Multiply :math:`c_m` by 84 | :math:`u_m = (kr)^{- i 2 m \pi/(n dlnr)} U_{\mu}[q + i 2 m \pi/(n dlnr)]` 85 | where :math:`U_{\mu}(x) = 2^x \Gamma[(\mu+1+x)/2] / \Gamma[(\mu+1-x)/2]` to 86 | obtain :math:`c_m u_m`; 87 | 3. FFT :math:`c_m u_m` back to obtain the discrete Hankel transform 88 | :math:`\tilde{a}_j`. 89 | 90 | The Fourier sine and cosine transforms 91 | `````````````````````````````````````` 92 | 93 | .. math:: 94 | :label: ham6 95 | 96 | \tilde{A}(k) = \sqrt{2/\pi} \int^\infty_0 A(r) \sin(k r) \,dr \, , 97 | 98 | .. math:: 99 | :label: ham7 100 | 101 | \tilde{A}(k) = \sqrt{2/\pi} \int^\infty_0 A(r) \cos(k r) \,dr \, , 102 | 103 | may be regarded as special cases of the Hankel transform with :math:`\mu = 1/2` 104 | and :math:`-1/2` since 105 | 106 | .. math:: 107 | :label: ham8 108 | 109 | \sqrt{2/\pi} \sin(x) = \sqrt(x) J_{1/2} (x) \, , 110 | 111 | .. math:: 112 | :label: ham9 113 | 114 | \sqrt{2/\pi} \cos(x) = \sqrt(x) J_{-1/2} (x) \, . 115 | 116 | 117 | The Fourier transforms may be done by making the substitutions 118 | 119 | .. math:: 120 | :label: ham10 121 | 122 | A(r) = a(r) r^{q-1/2} \quad \text{and} \quad 123 | \tilde{A}(k) = \tilde{a}(k) k^{-q-1/2} 124 | 125 | and Hankel transforming :math:`a(r)` with a power law bias :math:`(k r)^q` 126 | 127 | .. math:: 128 | :label: ham11 129 | 130 | \tilde{a}(k) = \int^\infty_0 a(r) (k r)^q J_{\pm 1/2} (k r) k \,dr \, . 131 | 132 | Different choices of power law bias :math:`q` lead to different discrete 133 | Fourier transforms of :math:`A(r)`, because the assumption of periodicity of 134 | :math:`a(r) = A(r) r^{-q+(1/2)}` is different for different :math:`q`. 135 | 136 | If :math:`A(r)` is a power law, :math:`A(r)` proportional to 137 | :math:`r^{q-(1/2)}`, then applying a bias :math:`q` yields a discrete Fourier 138 | transform :math:`\tilde{A}(k)` that is exactly equal to the continuous Fourier 139 | transform, because then :math:`a(r)` is a constant, which is a periodic 140 | function. 141 | 142 | The Hankel transform 143 | ```````````````````` 144 | 145 | .. math:: 146 | :label: ham12 147 | 148 | \tilde{A}(k) = \int^\infty_0 A(r) J_{\mu} (k r) k \,dr 149 | 150 | may be done by making the substitutions 151 | 152 | .. math:: 153 | :label: ham13 154 | 155 | A(r) = a(r) r^q \quad \text{and} \quad \tilde{A}(k) = \tilde{a}(k) k^{-q} 156 | 157 | and Hankel transforming :math:`a(r)` with a power law bias :math:`(k r)^q` 158 | 159 | .. math:: 160 | :label: ham14 161 | 162 | \tilde{a}(k) = \int^\infty_0 a(r) (k r)^q J_{\mu} (k r) k \,dr \, . 163 | 164 | Different choices of power law bias :math:`q` lead to different discrete Hankel 165 | transforms of :math:`A(r)`, because the assumption of periodicity of 166 | :math:`a(r) = A(r) r^{-q}` is different for different :math:`q`. 167 | 168 | If :math:`A(r)` is a power law, :math:`A(r)` proportional to :math:`r^q`, then 169 | applying a bias :math:`q` yields a discrete Hankel transform 170 | :math:`\tilde{A}(k)` that is exactly equal to the continuous Hankel transform, 171 | because then :math:`a(r)` is a constant, which is a periodic function. 172 | 173 | There are five routines: 174 | ```````````````````````` 175 | Comments in the subroutines contain further details. 176 | 177 | 1. **subroutine `fhti(n,mu,q,dlnr,kr,kropt,wsave,ok)`** 178 | is an initialization routine. 179 | 180 | 2. **subroutine `fftl(n,a,rk,dir,wsave)`** 181 | computes the discrete Fourier sine or cosine transform of a logarithmically 182 | spaced periodic sequence. This is a driver routine that calls `fhtq`. 183 | 184 | 3. **subroutine `fht(n,a,dir,wsave)`** 185 | computes the discrete Hankel transform of a logarithmically spaced periodic 186 | sequence. This is a driver routine that calls `fhtq`. 187 | 188 | 4. **subroutine `fhtq(n,a,dir,wsave)`** 189 | computes the biased discrete Hankel transform of a logarithmically spaced 190 | periodic sequence. **This is the basic FFTLog routine.** 191 | 192 | 5. **real*8 function `krgood(mu,q,dlnr,kr)`** 193 | takes an input `kr` and returns the nearest low-ringing `kr`. This is an 194 | optional routine called by `fhti`. 195 | 196 | **END of the original documentation from the file `fftlog.f`** 197 | 198 | """ 199 | import numpy as np 200 | from scipy.special import loggamma 201 | from scipy.fftpack import rfft, irfft 202 | 203 | __all__ = ['fhti', 'fftl', 'fht', 'fhtq', 'krgood'] 204 | 205 | 206 | def fhti(n, mu, dlnr, q=0, kr=1, kropt=0): 207 | r"""Initialize the working array xsave used by fftl, fht, and fhtq. 208 | 209 | fhti initializes the working array xsave used by fftl, fht, and fhtq. fhti 210 | need be called once, whereafter fftl, fht, or fhtq may be called many 211 | times, as long as n, mu, q, dlnr, and kr remain unchanged. fhti should be 212 | called each time n, mu, q, dlnr, or kr is changed. The work array xsave 213 | should not be changed between calls to fftl, fht, or fhtq. 214 | 215 | Parameters 216 | ---------- 217 | n : int 218 | Number of points in the array to be transformed; n may be any positive 219 | integer, but the FFT routines run fastest if n is a product of small 220 | primes 2, 3, 5. 221 | 222 | mu : float 223 | Index of J_mu in Hankel transform; mu may be any real number, positive 224 | or negative. 225 | 226 | dlnr : float 227 | Separation between natural log of points; dlnr may be positive or 228 | negative. 229 | 230 | q : float, optional 231 | Exponent of power law bias; q may be any real number, positive or 232 | negative. If in doubt, use q = 0, for which case the Hankel transform 233 | is orthogonal, i.e. self-inverse, provided also that, for n even, kr is 234 | low-ringing. Non-zero q may yield better approximations to the 235 | continuous Hankel transform for some functions. 236 | Defaults to 0 (unbiased). 237 | 238 | kr : float, optional 239 | k_c r_c where c is central point of array 240 | = k_j r_(n+1-j) = k_(n+1-j) r_j . 241 | Normally one would choose kr to be about 1 (default) (or 2, or pi, to 242 | taste). 243 | 244 | kropt : int, optional; {0, 1, 2, 3} 245 | - 0 to use input kr as is (default); 246 | - 1 to change kr to nearest low-ringing kr, quietly; 247 | - 2 to change kr to nearest low-ringing kr, verbosely; 248 | - 3 for option to change kr interactively. 249 | 250 | Returns 251 | ------- 252 | kr : float, optional 253 | kr, adjusted depending on kropt. 254 | 255 | xsave : array 256 | Working array used by fftl, fht, and fhtq. Dimension: 257 | - for q = 0 (unbiased transform): n+3 258 | - for q != 0 (biased transform): 1.5*n+4 259 | If odd, last element is not needed. 260 | 261 | """ 262 | 263 | # adjust kr 264 | if kropt == 0: # keep kr as is 265 | pass 266 | elif kropt == 1: # change kr to low-ringing kr quietly 267 | kr = krgood(mu, q, dlnr, kr) 268 | elif kropt == 2: # change kr to low-ringing kr verbosely 269 | d = krgood(mu, q, dlnr, kr) 270 | if abs(kr/d - 1) >= 1e-15: 271 | kr = d 272 | print(" kr changed to ", kr) 273 | else: # option to change kr to low-ringing kr interactively 274 | d = krgood(mu, q, dlnr, kr) 275 | if abs(kr/d-1.0) >= 1e-15: 276 | print(" change kr = ", kr) 277 | print(" to low-ringing kr = ", d) 278 | go = input("? [CR, y=yes, n=no, x=exit]: ") 279 | if go.lower() in ['', 'y']: 280 | kr = d 281 | print(" kr changed to ", kr) 282 | elif go.lower() == 'n': 283 | print(" kr left unchanged at ", kr) 284 | else: 285 | print("exit") 286 | return False 287 | 288 | # return if n is <= 0 289 | if n <= 0: 290 | return kr 291 | 292 | # The normal FFT is not initialized here as in the original FFTLog code, as 293 | # the `scipy.fftpack`-FFT routines `rfft` and `irfft` do that internally. 294 | # Therefore xsave in `pyfftlog` is 2*n+15 elements shorter, and named 295 | # xsave to not confuse it with xsave from the FFT. 296 | 297 | if q == 0: # unbiased case (q = 0) 298 | ln2kr = np.log(2.0/kr) 299 | xp = (mu + 1)/2.0 300 | d = np.pi/(n*dlnr) 301 | 302 | m = np.arange(1, (n+1)/2) 303 | y = m*d # y = m*pi/(n*dlnr) 304 | zp = loggamma(xp + 1j*y) 305 | arg = 2.0*(ln2kr*y + zp.imag) # Argument of kr^(-2 i y) U_mu(2 i y) 306 | 307 | # Arange xsave: [q, dlnr, kr, cos, sin] 308 | xsave = np.empty(2*arg.size+3) 309 | xsave[0] = q 310 | xsave[1] = dlnr 311 | xsave[2] = kr 312 | xsave[3::2] = np.cos(arg) 313 | xsave[4::2] = np.sin(arg) 314 | 315 | # Altogether 3 + 2*(n/2) elements used for q = 0, which is n+3 for even 316 | # n, n+2 for odd n. 317 | 318 | else: # biased case (q != 0) 319 | ln2 = np.log(2.0) 320 | ln2kr = np.log(2.0/kr) 321 | xp = (mu + 1 + q)/2.0 322 | xm = (mu + 1 - q)/2.0 323 | 324 | # first element of rest of xsave 325 | y = 0 326 | 327 | # case where xp or xm is a negative integer 328 | xpnegi = np.round(xp) == xp and xp <= 0 329 | xmnegi = np.round(xm) == xm and xm <= 0 330 | if xpnegi or xmnegi: 331 | 332 | # case where xp and xm are both negative integers 333 | # U_mu(q) = 2^q Gamma[xp]/Gamma[xm] is finite in this case 334 | if xpnegi and xmnegi: 335 | # Amplitude and Argument of U_mu(q) 336 | amp = np.exp(ln2*q) 337 | if xp > xm: 338 | m = np.arange(1, np.round(xp - xm)+1) 339 | amp *= xm + m - 1 340 | elif xp < xm: 341 | m = np.arange(1, np.round(xm - xp)+1) 342 | amp /= xp + m - 1 343 | arg = np.round(xp + xm)*np.pi 344 | 345 | else: # one of xp or xm is a negative integer 346 | # Transformation is singular if xp is -ve integer, and inverse 347 | # transformation is singular if xm is -ve integer, but 348 | # transformation may be well-defined if sum_j a_j = 0, as may 349 | # well occur in physical cases. Policy is to drop the 350 | # potentially infinite constant in the transform. 351 | 352 | if xpnegi: 353 | print('fhti: (mu+1+q)/2 =', np.round(xp), 'is -ve integer', 354 | ', yields singular transform:\ntransform will omit', 355 | 'additive constant that is generically infinite,', 356 | '\nbut that may be finite or zero if the sum of the', 357 | 'elements of the input array a_j is zero.') 358 | else: 359 | print('fhti: (mu+1-q)/2 =', np.round(xm), 'is -ve integer', 360 | ', yields singular inverse transform:\n inverse', 361 | 'transform will omit additive constant that is', 362 | 'generically infinite,\nbut that may be finite or', 363 | 'zero if the sum of the elements of the input array', 364 | 'a_j is zero.') 365 | amp = 0 366 | arg = 0 367 | 368 | else: # neither xp nor xm is a negative integer 369 | zp = loggamma(xp + 1j*y) 370 | zm = loggamma(xm + 1j*y) 371 | 372 | # Amplitude and Argument of U_mu(q) 373 | amp = np.exp(ln2*q + zp.real - zm.real) 374 | # note +Im(zm) to get conjugate value below real axis 375 | arg = zp.imag + zm.imag 376 | 377 | # first element: cos(arg) = ±1, sin(arg) = 0 378 | xsave1 = amp*np.cos(arg) 379 | 380 | # remaining elements of xsave 381 | d = np.pi/(n*dlnr) 382 | m = np.arange(1, (n+1)/2) 383 | y = m*d # y = m pi/(n dlnr) 384 | zp = loggamma(xp + 1j*y) 385 | zm = loggamma(xm + 1j*y) 386 | # Amplitude and Argument of kr^(-2 i y) U_mu(q + 2 i y) 387 | amp = np.exp(ln2*q + zp.real - zm.real) 388 | arg = 2*ln2kr*y + zp.imag + zm.imag 389 | 390 | # Arrange xsave: [q, dlnr, kr, xsave1, cos, sin] 391 | xsave = np.empty(3*arg.size+4) 392 | xsave[0] = q 393 | xsave[1] = dlnr 394 | xsave[2] = kr 395 | xsave[3] = xsave1 396 | xsave[4::3] = amp 397 | xsave[5::3] = np.cos(arg) 398 | xsave[6::3] = np.sin(arg) 399 | 400 | # Altogether 3 + 3*(n/2)+1 elements used for q != 0, which is (3*n)/2+4 401 | # for even n, (3*n)/2+3 for odd n. For even n, the very last element 402 | # of xsave [i.e. xsave(3*m+1)=sin(arg) for m=n/2] is not used within 403 | # FFTLog; if a low-ringing kr is used, this element should be zero. 404 | # The last element is computed in case somebody wants it. 405 | 406 | return kr, xsave 407 | 408 | 409 | def fftl(a, xsave, rk=1, tdir=1): 410 | r"""Logarithmic fast Fourier transform FFTLog. 411 | 412 | This is a driver routine that calls :func:`fhtq`. 413 | 414 | `fftl` computes a discrete version of the Fourier sine (if mu = 1/2) or 415 | cosine (if mu = -1/2) transform 416 | 417 | .. math:: 418 | 419 | \tilde{A}(k) = \sqrt{2/\pi} \int^\infty_0 A(r) \sin(k r) \,dr \, , 420 | 421 | .. math:: 422 | 423 | \tilde{A}(k) = \sqrt{2/\pi} \int^\infty_0 A(r) \cos(k r) \,dr \, , 424 | 425 | by making the substitutions 426 | 427 | .. math:: 428 | 429 | A(r) = a(r) r^{q-1/2} \quad \text{and} \quad 430 | \tilde{A}(k) = \tilde{a}(k) k^{-q-1/2} 431 | 432 | and applying a biased Hankel transform to :math:`a(r)`. 433 | 434 | The steps are: 435 | 1. :math:`a(r) = A(r) r^[-dir (q-0.5)]` 436 | 2. call `fhtq` to transform :math:`a(r) \rightarrow \tilde{a}(k)` 437 | 3. :math:`\tilde{A}(k) = \tilde{a}(k) k^[-dir (q+0.5)]` 438 | 439 | `fhti` must be called before the first call to `fftl`, with `mu=1/2` for a 440 | sine transform, or `mu=-1/2` for a cosine transform. 441 | 442 | A call to `fftl` with `dir=1` followed by a call to `fftl` with `dir=-1` 443 | (and rk unchanged), or vice versa, leaves the array a unchanged. 444 | 445 | Parameters 446 | ---------- 447 | a : array 448 | Array A(r) to transform: a(j) is A(r_j) at r_j = r_c exp[(j-jc) dlnr], 449 | where jc = (n+1)/2 = central index of array. 450 | 451 | xsave : array 452 | Working array set up by fhti. 453 | 454 | rk : float, optional 455 | r_c/k_c = r_j/k_j (a constant, the same constant for any j); rk is not 456 | (necessarily) the same quantity as kr. rk is used only to multiply the 457 | output array by sqrt(rk)^dir, so if you want to do the normalization 458 | later, or you don't care about the normalization, you can set rk = 1. 459 | Defaults to 1. 460 | 461 | tdir : int, optional; {1, -1} 462 | - 1 for forward transform (default), 463 | - -1 for backward transform. 464 | 465 | A backward transform (dir = -1) is the same as a forward transform with 466 | q -> -q and rk -> 1/rk, for any kr if n is odd, for low-ringing kr if n 467 | is even. 468 | 469 | Returns 470 | ------- 471 | a : array 472 | Transformed array Ã(k): a(j) is Ã(k_j) at k_j = k_c exp[(j-jc) dlnr]. 473 | 474 | """ 475 | fct = a.copy() 476 | q = xsave[0] 477 | dlnr = xsave[1] 478 | kr = xsave[2] 479 | 480 | # centre point of array 481 | jc = np.array((fct.size + 1)/2.0) 482 | j = np.arange(fct.size)+1 483 | 484 | # a(r) = A(r) (r/rc)^[-dir*(q-.5)] 485 | fct *= np.exp(-tdir*(q - 0.5)*(j - jc)*dlnr) 486 | 487 | # transform a(r) -> ã(k) 488 | fct = fhtq(fct, xsave, tdir) 489 | 490 | # Ã(k) = ã(k) k^[-dir*(q+.5)] rc^[-dir*(q-.5)] 491 | # = ã(k) (k/kc)^[-dir*(q+.5)] (kc rc)^(-dir*q) (rc/kc)^(dir*.5) 492 | lnkr = np.log(kr) 493 | lnrk = np.log(rk) 494 | fct *= np.exp(-tdir*((q + 0.5)*(j - jc)*dlnr + q*lnkr - lnrk/2.0)) 495 | 496 | return fct 497 | 498 | 499 | def fht(a, xsave, tdir=1): 500 | r"""Fast Hankel transform FHT. 501 | 502 | This is a driver routine that calls :func:`fhtq`. 503 | 504 | `fht` computes a discrete version of the Hankel transform 505 | 506 | .. math:: 507 | 508 | \tilde{A}(k) = \int^\infty_0 A(r) J_{\mu} (k r) k \,dr \, 509 | 510 | by making the substitutions 511 | 512 | .. math:: 513 | 514 | A(r) = a(r) r^q \quad \text{and} \quad 515 | \tilde{A}(k) = \tilde{a}(k) k^{-q} 516 | 517 | and applying a biased Hankel transform to :math:`a(r)`. 518 | 519 | The steps are: 520 | 1. :math:`a(r) = A(r) r^{-dir q}` 521 | 2. call `fhtq` to transform :math:`a(r) \rightarrow \tilde{a}(k)` 522 | 3. :math:`\tilde{A}(k) = \tilde{a}(k) k^{-dir q}` 523 | 524 | `fhti` must be called before the first call to `fht`. 525 | 526 | A call to `fht` with `dir=1` followed by a call to `fht` with `dir=-1`, or 527 | vice versa, leaves the array a unchanged. 528 | 529 | 530 | Parameters 531 | ---------- 532 | a : array 533 | Array A(r) to transform: a(j) is A(r_j) at r_j = r_c exp[(j-jc) dlnr], 534 | where jc = (n+1)/2 = central index of array. 535 | 536 | xsave : array 537 | Working array set up by fhti. 538 | 539 | tdir : int, optional; {1, -1} 540 | - 1 for forward transform (default), 541 | - -1 for backward transform. 542 | 543 | A backward transform (dir = -1) is the same as a forward transform with 544 | q -> -q, for any kr if n is odd, for low-ringing kr if n is even. 545 | 546 | Returns 547 | ------- 548 | a : array 549 | Transformed array Ã(k): a(j) is Ã(k_j) at k_j = k_c exp[(j-jc) dlnr]. 550 | 551 | """ 552 | fct = a.copy() 553 | q = xsave[0] 554 | dlnr = xsave[1] 555 | kr = xsave[2] 556 | 557 | # a(r) = A(r) (r/rc)^(-dir*q) 558 | if q != 0: 559 | # centre point of array 560 | jc = np.array((fct.size + 1)/2.0) 561 | j = np.arange(fct.size)+1 562 | fct *= np.exp(-tdir*q*(j - jc)*dlnr) 563 | 564 | # transform a(r) -> ã(k) 565 | fct = fhtq(fct, xsave, tdir) 566 | 567 | # Ã(k) = ã(k) (k rc)^(-dir*q) 568 | # = ã(k) (k/kc)^(-dir*q) (kc rc)^(-dir*q) 569 | if q != 0: 570 | lnkr = np.log(kr) 571 | fct *= np.exp(-tdir*q*((j - jc)*dlnr + lnkr)) 572 | 573 | return fct 574 | 575 | 576 | def fhtq(a, xsave, tdir=1): 577 | r"""Kernel routine of FFTLog. 578 | 579 | This is the basic FFTLog routine. 580 | 581 | `fhtq` computes a discrete version of the biased Hankel transform 582 | 583 | .. math:: 584 | 585 | \tilde{a}(k) = \int^\infty_0 a(r) (k r)^q J_{\mu} (k r) k \,dr \, . 586 | 587 | `fhti` must be called before the first call to `fhtq`. 588 | 589 | A call to `fhtq` with `dir=1` followed by a call to `fhtq` with `dir=-1`, 590 | or vice versa, leaves the array a unchanged. 591 | 592 | Parameters 593 | ---------- 594 | a : array 595 | Periodic array a(r) to transform: a(j) is a(r_j) at r_j = r_c 596 | exp[(j-jc) dlnr] where jc = (n+1)/2 = central index of array. 597 | 598 | xsave : array 599 | Working array set up by fhti. 600 | 601 | tdir : int, optional; {1, -1} 602 | - 1 for forward transform (default), 603 | - -1 for backward transform. 604 | 605 | A backward transform (dir = -1) is the same as a forward transform with 606 | q -> -q, for any kr if n is odd, for low-ringing kr if n is even. 607 | 608 | Returns 609 | ------- 610 | a : array 611 | Transformed periodic array ã(k): a(j) is ã(k_j) at k_j = k_c exp[(j-jc) 612 | dlnr]. 613 | 614 | """ 615 | fct = a.copy() 616 | q = xsave[0] 617 | n = fct.size 618 | 619 | # normal FFT 620 | fct = rfft(fct) 621 | 622 | m = np.arange(1, n//2, dtype=int) # index variable 623 | if q == 0: # unbiased (q = 0) transform 624 | # multiply by (kr)^[- i 2 m pi/(n dlnr)] U_mu[i 2 m pi/(n dlnr)] 625 | ar = fct[2*m-1] 626 | ai = fct[2*m] 627 | fct[2*m-1] = ar*xsave[2*m+1] - ai*xsave[2*m+2] 628 | fct[2*m] = ar*xsave[2*m+2] + ai*xsave[2*m+1] 629 | # problem(2*m)atical last element, for even n 630 | if np.mod(n, 2) == 0: 631 | ar = xsave[-2] 632 | if (tdir == 1): # forward transform: multiply by real part 633 | # Why? See 634 | # https://jila.colorado.edu/~ajsh/FFTLog/index.html#ure 635 | fct[-1] *= ar 636 | elif (tdir == -1): # backward transform: divide by real part 637 | # Real part ar can be zero for maximally bad choice of kr. 638 | # This is unlikely to happen by chance, but if it does, policy 639 | # is to let it happen. For low-ringing kr, imaginary part ai 640 | # is zero by construction, and real part ar is guaranteed 641 | # nonzero. 642 | fct[-1] /= ar 643 | 644 | else: # biased (q != 0) transform 645 | # multiply by (kr)^[- i 2 m pi/(n dlnr)] U_mu[q + i 2 m pi/(n dlnr)] 646 | # phase 647 | ar = fct[2*m-1] 648 | ai = fct[2*m] 649 | fct[2*m-1] = ar*xsave[3*m+2] - ai*xsave[3*m+3] 650 | fct[2*m] = ar*xsave[3*m+3] + ai*xsave[3*m+2] 651 | 652 | if tdir == 1: # forward transform: multiply by amplitude 653 | fct[0] *= xsave[3] 654 | fct[2*m-1] *= xsave[3*m+1] 655 | fct[2*m] *= xsave[3*m+1] 656 | 657 | elif tdir == -1: # backward transform: divide by amplitude 658 | # amplitude of m=0 element 659 | ar = xsave[3] 660 | if ar == 0: 661 | # Amplitude of m=0 element can be zero for some mu, q 662 | # combinations (singular inverse); policy is to drop 663 | # potentially infinite constant. 664 | fct[0] = 0 665 | else: 666 | fct[0] /= ar 667 | 668 | # remaining amplitudes should never be zero 669 | fct[2*m-1] /= xsave[3*m+1] 670 | fct[2*m] /= xsave[3*m+1] 671 | 672 | # problematical last element, for even n 673 | if np.mod(n, 2) == 0: 674 | m = int(n/2) 675 | ar = xsave[3*m+2]*xsave[3*m+1] 676 | if tdir == 1: # forward transform: multiply by real part 677 | fct[-1] *= ar 678 | elif (tdir == -1): # backward transform: divide by real part 679 | # Real part ar can be zero for maximally bad choice of kr. 680 | # This is unlikely to happen by chance, but if it does, policy 681 | # is to let it happen. For low-ringing kr, imaginary part ai 682 | # is zero by construction, and real part ar is guaranteed 683 | # nonzero. 684 | fct[-1] /= ar 685 | 686 | # normal FFT back 687 | fct = irfft(fct) 688 | 689 | # reverse the array and at the same time undo the FFTs' multiplication by n 690 | # => Just reverse the array, the rest is already done in drfft. 691 | fct = fct[::-1] 692 | 693 | return fct 694 | 695 | 696 | def krgood(mu, q, dlnr, kr): 697 | r"""Return optimal kr. 698 | 699 | Use of this routine is optional. 700 | 701 | Choosing kr so that 702 | 703 | .. math:: 704 | 705 | (k r)^{- i pi/dlnr} U_{\mu}(q + i pi/dlnr) 706 | 707 | is real may reduce ringing of the discrete Hankel transform, because it 708 | makes the transition of this function across the period boundary smoother. 709 | 710 | Parameters 711 | ---------- 712 | mu : float 713 | index of J_mu in Hankel transform; mu may be any real number, positive 714 | or negative. 715 | 716 | q : float 717 | exponent of power law bias; q may be any real number, positive or 718 | negative. If in doubt, use q = 0, for which case the Hankel transform 719 | is orthogonal, i.e. self-inverse, provided also that, for n even, kr is 720 | low-ringing. Non-zero q may yield better approximations to the 721 | continuous Hankel transform for some functions. 722 | 723 | dlnr : float 724 | separation between natural log of points; dlnr may be positive or 725 | negative. 726 | 727 | kr : float, optional 728 | k_c r_c where c is central point of array 729 | = k_j r_(n+1-j) = k_(n+1-j) r_j . 730 | Normally one would choose kr to be about 1 (default) (or 2, or pi, to 731 | taste). 732 | 733 | Returns 734 | ------- 735 | krgood : float 736 | low-ringing value of kr nearest to input kr. ln(krgood) is always 737 | within dlnr/2 of ln(kr). 738 | 739 | """ 740 | if dlnr == 0: 741 | return kr 742 | 743 | xp = (mu + 1.0 + q)/2.0 744 | xm = (mu + 1.0 - q)/2.0 745 | y = 1j*np.pi/(2.0*dlnr) 746 | zp = loggamma(xp + y) 747 | zm = loggamma(xm + y) 748 | 749 | # low-ringing condition is that following should be integral 750 | arg = np.log(2.0/kr)/dlnr + (zp.imag + zm.imag)/np.pi 751 | 752 | # return low-ringing kr 753 | return kr*np.exp((arg - np.round(arg))*dlnr) 754 | --------------------------------------------------------------------------------