├── 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 |
11 | {% for text, link in menu_links %}
12 | - {{ text }}
13 | {% endfor %}
14 |
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 |
--------------------------------------------------------------------------------