├── version.txt
├── requirements.txt
├── requirements-dev.txt
├── notebooks
├── md00324.mat
├── interpolate_sphere.ipynb
├── interpolate_missing.ipynb
└── interpolate_franke.ipynb
├── CONTRIBUTORS.rst
├── setup.cfg
├── MANIFEST.in
├── environment.yml
├── spatial_interpolators
├── version.py
├── __init__.py
├── PvQv_C.pyx
├── inpaint.py
├── sph_bilinear.py
├── barnes_objective.py
├── legendre.py
├── shepard_interpolant.py
├── compact_radial_basis.py
├── radial_basis.py
├── sph_spline.py
├── biharmonic_spline.py
├── sph_radial_basis.py
└── spatial.py
├── doc
├── environment.yml
├── source
│ ├── _static
│ │ └── style.css
│ ├── api_reference
│ │ ├── sph_bilinear.rst
│ │ ├── radial_basis.rst
│ │ ├── sph_spline.rst
│ │ ├── barnes_objective.rst
│ │ ├── shepard_interpolant.rst
│ │ ├── inpaint.rst
│ │ ├── sph_radial_basis.rst
│ │ ├── biharmonic_spline.rst
│ │ ├── compact_radial_basis.rst
│ │ ├── legendre.rst
│ │ └── spatial.rst
│ ├── _templates
│ │ └── layout.html
│ ├── index.rst
│ ├── getting_started
│ │ ├── Install.rst
│ │ ├── Citations.rst
│ │ └── Contributing.rst
│ ├── user_guide
│ │ └── Examples.rst
│ └── conf.py
├── Makefile
└── make.bat
├── readthedocs.yml
├── .github
└── workflows
│ ├── python-publish.yml
│ └── python-request.yml
├── LICENSE
├── .gitignore
├── setup.py
└── README.rst
/version.txt:
--------------------------------------------------------------------------------
1 | 1.0.0.4
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | numpy
2 | scipy
3 | cython
4 | matplotlib
5 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | flake8
2 | pytest>=4.6
3 | pytest-cov
4 | numpy
5 |
6 |
--------------------------------------------------------------------------------
/notebooks/md00324.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsutterley/spatial-interpolators/HEAD/notebooks/md00324.mat
--------------------------------------------------------------------------------
/CONTRIBUTORS.rst:
--------------------------------------------------------------------------------
1 | - `Tyler Sutterley (University of Washington) `_
2 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [setuptools:build-system]
2 | requires = ["setuptools>=18.0", "wheel", "cython"]
3 | build-backend = "setuptools.build_meta"
4 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | prune .github*
2 | exclude *.yml
3 | include README.rst
4 | include LICENSE
5 | include requirements.txt
6 | include setup.cfg
7 | include setup.py
8 | include version.txt
9 |
--------------------------------------------------------------------------------
/environment.yml:
--------------------------------------------------------------------------------
1 | name: spatial-interpolators
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - python>=3.6
6 | - notebook
7 | - numpy
8 | - scipy
9 | - cython
10 | - matplotlib
11 |
--------------------------------------------------------------------------------
/spatial_interpolators/version.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | u"""
3 | version.py (01/2022)
4 | Gets version number of a package
5 | """
6 | from pkg_resources import get_distribution
7 |
8 | # get version
9 | version = get_distribution("spatial_interpolators").version
10 | # append "v" before the version
11 | full_version = "v{0}".format(version)
12 |
--------------------------------------------------------------------------------
/doc/environment.yml:
--------------------------------------------------------------------------------
1 | name: spatial-docs
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - cython
6 | - docutils<0.18
7 | - fontconfig
8 | - freetype
9 | - graphviz
10 | - matplotlib
11 | - notebook
12 | - numpy
13 | - numpydoc
14 | - pip
15 | - python>=3.6
16 | - scipy
17 | - sphinx
18 | - sphinx_rtd_theme
19 | - pip:
20 | - sphinx-argparse>=0.4
21 | - ..
22 |
--------------------------------------------------------------------------------
/doc/source/_static/style.css:
--------------------------------------------------------------------------------
1 | /* fix for position of equation numbers
2 | * https://github.com/rtfd/sphinx_rtd_theme/issues/301
3 | */
4 | .eqno {
5 | margin-left: 5px;
6 | float: right;
7 | }
8 | .math .headerlink {
9 | display: none;
10 | visibility: hidden;
11 | }
12 | .math:hover .headerlink {
13 | display: inline-block;
14 | visibility: visible;
15 | margin-right: -0.7em;
16 | }
17 |
--------------------------------------------------------------------------------
/doc/source/api_reference/sph_bilinear.rst:
--------------------------------------------------------------------------------
1 | ============
2 | sph_bilinear
3 | ============
4 |
5 | - Interpolates data over a sphere using bilinear functions
6 |
7 | Calling Sequence
8 | ################
9 |
10 | .. code-block:: python
11 |
12 | import spatial_interpolators as spi
13 | zi = spi.sph_bilinear(x,y,z,xi,yi)
14 |
15 | `Source code`__
16 |
17 | .. __: https://github.com/tsutterley/spatial-interpolators/blob/main/spatial_interpolators/sph_bilinear.py
18 |
19 | .. autofunction:: spatial_interpolators.sph_bilinear
20 |
--------------------------------------------------------------------------------
/doc/source/api_reference/radial_basis.rst:
--------------------------------------------------------------------------------
1 | ============
2 | radial_basis
3 | ============
4 |
5 | - Interpolates data using radial basis functions
6 |
7 | Calling Sequence
8 | ################
9 |
10 | .. code-block:: python
11 |
12 | import spatial_interpolators as spi
13 | ZI = spi.radial_basis(xs, ys, zs, XI, YI, method='inverse')
14 |
15 | `Source code`__
16 |
17 | .. __: https://github.com/tsutterley/spatial-interpolators/blob/main/spatial_interpolators/radial_basis.py
18 |
19 | .. autofunction:: spatial_interpolators.radial_basis
20 |
--------------------------------------------------------------------------------
/doc/source/api_reference/sph_spline.rst:
--------------------------------------------------------------------------------
1 | ==========
2 | sph_spline
3 | ==========
4 |
5 | - Interpolates data over a sphere using spherical surface splines in tension
6 |
7 | Calling Sequence
8 | ################
9 |
10 | .. code-block:: python
11 |
12 | import spatial_interpolators as spi
13 | output = spi.sph_spline(lon, lat, data, longitude, latitude, tension=0.5)
14 |
15 | `Source code`__
16 |
17 | .. __: https://github.com/tsutterley/spatial-interpolators/blob/main/spatial_interpolators/sph_spline.py
18 |
19 | .. autofunction:: spatial_interpolators.sph_spline
20 |
--------------------------------------------------------------------------------
/doc/source/api_reference/barnes_objective.rst:
--------------------------------------------------------------------------------
1 | ================
2 | barnes_objective
3 | ================
4 |
5 | - Optimally interpolates data using Barnes objective analysis using a successive corrections scheme
6 |
7 | Calling Sequence
8 | ################
9 |
10 | .. code-block:: python
11 |
12 | import spatial_interpolators as spi
13 | ZI = spi.barnes_objective(xs, ys, zs, XI, YI, XR, YR)
14 |
15 | `Source code`__
16 |
17 | .. __: https://github.com/tsutterley/spatial-interpolators/blob/main/spatial_interpolators/barnes_objective.py
18 |
19 | .. autofunction:: spatial_interpolators.barnes_objective
20 |
--------------------------------------------------------------------------------
/doc/source/api_reference/shepard_interpolant.rst:
--------------------------------------------------------------------------------
1 | ===================
2 | shepard_interpolant
3 | ===================
4 |
5 | - Interpolates data by evaluating Shepard Interpolants based on inverse distances
6 |
7 | Calling Sequence
8 | ################
9 |
10 | .. code-block:: python
11 |
12 | import spatial_interpolators as spi
13 | ZI = spi.shepard_interpolant(xs, ys, zs, XI, YI, power=2.0)
14 |
15 | `Source code`__
16 |
17 | .. __: https://github.com/tsutterley/spatial-interpolators/blob/main/spatial_interpolators/shepard_interpolant.py
18 |
19 | .. autofunction:: spatial_interpolators.shepard_interpolant
20 |
--------------------------------------------------------------------------------
/doc/source/api_reference/inpaint.rst:
--------------------------------------------------------------------------------
1 | =======
2 | inpaint
3 | =======
4 |
5 | - Inpaint over missing data in a two-dimensional array using a penalized least square method based on discrete cosine transforms
6 |
7 | Calling Sequence
8 | ################
9 |
10 | .. code-block:: python
11 |
12 | import spatial_interpolators as spi
13 | output = spi.inpaint(xs, ys, zs)
14 |
15 | `Source code`__
16 |
17 | .. __: https://github.com/tsutterley/spatial-interpolators/blob/main/spatial_interpolators/inpaint.py
18 |
19 | .. autofunction:: spatial_interpolators.inpaint
20 |
21 | .. autofunction:: spatial_interpolators.inpaint.nearest_neighbors
22 |
--------------------------------------------------------------------------------
/doc/source/api_reference/sph_radial_basis.rst:
--------------------------------------------------------------------------------
1 | ================
2 | sph_radial_basis
3 | ================
4 |
5 | - Interpolates data over a sphere using radial basis functions
6 | - QR factorization option to eliminate ill-conditioning
7 |
8 | Calling Sequence
9 | ################
10 |
11 | .. code-block:: python
12 |
13 | import spatial_interpolators as spi
14 | output = spi.sph_radial_basis(lon, lat, data, longitude, latitude, method='inverse')
15 |
16 | `Source code`__
17 |
18 | .. __: https://github.com/tsutterley/spatial-interpolators/blob/main/spatial_interpolators/sph_radial_basis.py
19 |
20 | .. autofunction:: spatial_interpolators.sph_radial_basis
21 |
--------------------------------------------------------------------------------
/doc/source/api_reference/biharmonic_spline.rst:
--------------------------------------------------------------------------------
1 | =================
2 | biharmonic_spline
3 | =================
4 |
5 | - Interpolates data using 2-dimensional biharmonic splines
6 | - Can use surface splines in tension
7 | - Can use regularized surface splines
8 |
9 | Calling Sequence
10 | ################
11 |
12 | .. code-block:: python
13 |
14 | import spatial_interpolators as spi
15 | ZI = spi.biharmonic_spline(xs, ys, zs, XI, YI, tension=0.5)
16 |
17 | `Source code`__
18 |
19 | .. __: https://github.com/tsutterley/spatial-interpolators/blob/main/spatial_interpolators/biharmonic_spline.py
20 |
21 | .. autofunction:: spatial_interpolators.biharmonic_spline
22 |
--------------------------------------------------------------------------------
/doc/source/api_reference/compact_radial_basis.rst:
--------------------------------------------------------------------------------
1 | ====================
2 | compact_radial_basis
3 | ====================
4 |
5 | - Interpolates data using compactly supported radial basis functions of minimal degree and sparse matrix algebra
6 |
7 | Calling Sequence
8 | ################
9 |
10 | .. code-block:: python
11 |
12 | import spatial_interpolators as spi
13 | ZI = spi.compact_radial_basis(xs, ys, zs, XI, YI, dimension, order, method='wendland')
14 |
15 | `Source code`__
16 |
17 | .. __: https://github.com/tsutterley/spatial-interpolators/blob/main/spatial_interpolators/compact_radial_basis.py
18 |
19 | .. autofunction:: spatial_interpolators.compact_radial_basis
20 |
--------------------------------------------------------------------------------
/readthedocs.yml:
--------------------------------------------------------------------------------
1 | # readthedocs.yml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | # Required
6 | version: 2
7 |
8 | # Configuration for the documentation build process
9 | build:
10 | os: "ubuntu-22.04"
11 | tools:
12 | python: "mambaforge-22.9"
13 |
14 | # Build documentation in the docs/ directory with Sphinx
15 | sphinx:
16 | configuration: doc/source/conf.py
17 |
18 | # Optionally build your docs in additional formats such as PDF and ePub
19 | formats: []
20 |
21 | # Optionally set the version of conda and environment required to build your docs
22 | conda:
23 | environment: doc/environment.yml
24 |
--------------------------------------------------------------------------------
/doc/source/api_reference/legendre.rst:
--------------------------------------------------------------------------------
1 | ========
2 | legendre
3 | ========
4 |
5 | - Computes associated Legendre functions of degree ``l`` evaluated for elements ``x``
6 | - ``l`` must be a scalar integer and ``x`` must contain real values ranging -1 <= ``x`` <= 1
7 | - Unnormalized associated Legendre function values will overflow for ``l`` > 150
8 |
9 | Calling Sequence
10 | ################
11 |
12 | .. code-block:: python
13 |
14 | from spatial_interpolators.legendre import legendre
15 | Pl = legendre(l, x)
16 |
17 | `Source code`__
18 |
19 | .. __: https://github.com/tsutterley/spatial-interpolators/blob/main/spatial_interpolators/legendre.py
20 |
21 | .. autofunction:: spatial_interpolators.legendre
22 |
23 |
--------------------------------------------------------------------------------
/doc/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/spatial_interpolators/__init__.py:
--------------------------------------------------------------------------------
1 | from spatial_interpolators.barnes_objective import barnes_objective
2 | from spatial_interpolators.biharmonic_spline import biharmonic_spline
3 | from spatial_interpolators.sph_spline import sph_spline
4 | from spatial_interpolators.legendre import legendre
5 | from spatial_interpolators.radial_basis import radial_basis
6 | from spatial_interpolators.compact_radial_basis import compact_radial_basis
7 | from spatial_interpolators.sph_radial_basis import sph_radial_basis
8 | from spatial_interpolators.shepard_interpolant import shepard_interpolant
9 | from spatial_interpolators.sph_bilinear import sph_bilinear
10 | from spatial_interpolators.inpaint import inpaint
11 | import spatial_interpolators.spatial
12 |
--------------------------------------------------------------------------------
/doc/source/_templates/layout.html:
--------------------------------------------------------------------------------
1 | {# Import the theme's layout. #}
2 | {% extends "!layout.html" %}
3 |
4 | {% block htmltitle %}
5 | {% if title == '' or title == 'Home' %}
6 |
{{ docstitle|e }}
7 | {% else %}
8 | {{ title|striptags|e }}{{ titlesuffix }}
9 | {% endif %}
10 | {% endblock %}
11 |
12 | {% block menu %}
13 | {{ super() }}
14 |
15 | {% if menu_links %}
16 |
17 | External links
18 |
19 |
20 | {% for text, link in menu_links %}
21 | - {{ text }}
22 | {% endfor %}
23 |
24 | {% endif %}
25 | {% endblock %}
26 |
--------------------------------------------------------------------------------
/doc/source/api_reference/spatial.rst:
--------------------------------------------------------------------------------
1 | =======
2 | spatial
3 | =======
4 |
5 | Utilities for operating on spatial data
6 |
7 | `Source code`__
8 |
9 | .. __: https://github.com/tsutterley/spatial-interpolators/blob/main/spatial_interpolators/spatial.py
10 |
11 | General Methods
12 | ===============
13 |
14 | .. autofunction:: spatial_interpolators.spatial.data_type
15 |
16 | .. autofunction:: spatial_interpolators.spatial.convert_ellipsoid
17 |
18 | .. autofunction:: spatial_interpolators.spatial.compute_delta_h
19 |
20 | .. autofunction:: spatial_interpolators.spatial.wrap_longitudes
21 |
22 | .. autofunction:: spatial_interpolators.spatial.to_cartesian
23 |
24 | .. autofunction:: spatial_interpolators.spatial.to_sphere
25 |
26 | .. autofunction:: spatial_interpolators.spatial.to_geodetic
27 |
28 | .. autofunction:: spatial_interpolators.spatial.scale_areas
29 |
--------------------------------------------------------------------------------
/doc/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/doc/source/index.rst:
--------------------------------------------------------------------------------
1 | spatial-interpolators
2 | =====================
3 |
4 | Python-based software for spatially interpolating data over Cartesian
5 | and spherical grids
6 |
7 | .. toctree::
8 | :maxdepth: 2
9 | :caption: Getting Started
10 |
11 | getting_started/Install.rst
12 | getting_started/Contributing.rst
13 | getting_started/Citations.rst
14 |
15 | .. toctree::
16 | :maxdepth: 1
17 | :hidden:
18 | :caption: User Guide
19 |
20 | user_guide/Examples.rst
21 |
22 | .. toctree::
23 | :maxdepth: 1
24 | :hidden:
25 | :caption: API Reference
26 |
27 | api_reference/barnes_objective.rst
28 | api_reference/biharmonic_spline.rst
29 | api_reference/compact_radial_basis.rst
30 | api_reference/inpaint.rst
31 | api_reference/legendre.rst
32 | api_reference/radial_basis.rst
33 | api_reference/spatial.rst
34 | api_reference/shepard_interpolant.rst
35 | api_reference/sph_bilinear.rst
36 | api_reference/sph_radial_basis.rst
37 | api_reference/sph_spline.rst
38 |
39 |
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflows will upload a Python Package using Twine when a release is created
2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3 |
4 | name: Upload Python Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | deploy:
12 |
13 | runs-on: ubuntu-20.04
14 |
15 | steps:
16 | - uses: actions/checkout@v4
17 | - name: Set up Python
18 | uses: actions/setup-python@v4
19 | with:
20 | python-version: '3.x'
21 | - name: Install dependencies
22 | run: |
23 | sudo apt-get update
24 | sudo apt-get install gcc
25 | python -m pip install --upgrade pip
26 | pip install setuptools build twine cython
27 | - name: Build and publish
28 | env:
29 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
30 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
31 | run: |
32 | python3 -m build
33 | twine upload dist/*.tar.gz
34 |
--------------------------------------------------------------------------------
/doc/source/getting_started/Install.rst:
--------------------------------------------------------------------------------
1 | ============
2 | Installation
3 | ============
4 |
5 | ``spatial-interpolators`` is available for download from the `GitHub repository `_,
6 | and the `Python Package Index (pypi) `_,
7 | The contents of the repository can be downloaded as a `zipped file `_ or cloned.
8 |
9 | To use this repository, please fork into your own account and then clone onto your system:
10 |
11 | .. code-block:: bash
12 |
13 | git clone https://github.com/tsutterley/spatial-interpolators.git
14 |
15 | Can then install using ``setuptools``:
16 |
17 | .. code-block:: bash
18 |
19 | python3 setup.py install
20 |
21 | or ``pip``
22 |
23 | .. code-block:: bash
24 |
25 | python3 -m pip install --user .
26 |
27 | Alternatively can install the ``spatial_interpolators`` utilities directly from GitHub with ``pip``:
28 |
29 | .. code-block:: bash
30 |
31 | python3 -m pip install --user git+https://github.com/tsutterley/spatial-interpolators.git
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Tyler C Sutterley
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled source #
2 | ###################
3 | *.com
4 | *.class
5 | *.dll
6 | *.exe
7 | *.o
8 | *.so
9 | *.pyc
10 | *.m~
11 | *.m~~
12 | PvQv_C.c
13 |
14 | # Packages #
15 | ############
16 | *.7z
17 | *.dmg
18 | *.gz
19 | *.iso
20 | *.jar
21 | *.rar
22 | *.tar
23 | *.zip
24 |
25 | # Logs and databases #
26 | ######################
27 | *.log
28 | *.sql
29 | *.sqlite
30 | *.gmtcommands4
31 | *.gmtdefaults4
32 | .RData
33 | .Rhistory
34 | octave-workspace
35 | __pycache__
36 | build/
37 | dist/
38 | develop-eggs/
39 | run/
40 | wheels/
41 | .eggs/
42 | *.egg-info/
43 | .installed.cfg
44 | *.egg
45 | .pytest_cache
46 | pythonenv*/
47 |
48 | # OS generated files #
49 | ######################
50 | .DS_Store
51 | .DS_Store?
52 | ._*
53 | .Spotlight-V100
54 | .Trashes
55 | ehthumbs.db
56 | Thumbs.db
57 |
58 | # LaTeX and Vim #
59 | ######################
60 | *.aux
61 | *.bbl
62 | *.blg
63 | *.dvi
64 | *.fdb_latexmk
65 | *.glg
66 | *.glo
67 | *.gls
68 | *.idx
69 | *.ilg
70 | *.ind
71 | *.ist
72 | *.lof
73 | *.lot
74 | *.nav
75 | *.nlo
76 | *.out
77 | *.pdfsync
78 | *.ps
79 | *.eps
80 | *.snm
81 | *.synctex.gz
82 | *.toc
83 | *.vrb
84 | *.maf
85 | *.mtc
86 | *.mtc0
87 | *.sw*
88 | *.hidden
89 | None*.png
90 |
91 | # Jupyter Checkpoints #
92 | #######################
93 | .ipynb_checkpoints
94 | *-checkpoint.ipynb
95 | Untitled.ipynb
96 |
--------------------------------------------------------------------------------
/.github/workflows/python-request.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: Python on pull request
5 |
6 | on:
7 | pull_request:
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ${{ matrix.os }}
13 | strategy:
14 | matrix:
15 | os: [ubuntu-latest, macos-latest]
16 | python-version: [3.11]
17 | env:
18 | OS: ${{ matrix.os }}
19 | PYTHON: ${{ matrix.python-version }}
20 | defaults:
21 | run:
22 | shell: bash -l {0}
23 |
24 | steps:
25 | - uses: actions/checkout@v4
26 | - name: Set up mamba ${{ matrix.python-version }}
27 | uses: mamba-org/setup-micromamba@v1
28 | with:
29 | micromamba-version: 'latest'
30 | environment-file: environment.yml
31 | init-shell: bash
32 | environment-name: spatial-interpolators
33 | cache-environment: true
34 | post-cleanup: 'all'
35 | create-args: >-
36 | python=${{ matrix.python-version }}
37 | flake8
38 | pytest
39 | pytest-cov
40 | - name: Lint with flake8
41 | run: |
42 | # stop the build if there are Python syntax errors or undefined names
43 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
44 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
45 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
46 |
--------------------------------------------------------------------------------
/doc/source/getting_started/Citations.rst:
--------------------------------------------------------------------------------
1 | ====================
2 | Citation Information
3 | ====================
4 |
5 | References
6 | ##########
7 |
8 | This work was initially supported by an appointment to the NASA Postdoctoral
9 | Program (NPP) at NASA Goddard Space Flight Center (GSFC), administered by
10 | Universities Space Research Association (USRA) under contract with NASA.
11 |
12 |
13 | Contributors
14 | ############
15 |
16 | .. include:: ../../../CONTRIBUTORS.rst
17 |
18 | Development
19 | ###########
20 |
21 | ``spatial-interpolators`` is an open source project.
22 | We welcome any help in maintaining and developing the software and documentation.
23 | Anyone at any career stage and with any level of coding experience can contribute.
24 | Please see the `Contribution Guidelines <./Contributing.html>`_ for more information.
25 |
26 | Problem Reports
27 | ###############
28 |
29 | If you have found a problem in ``spatial-interpolators``, or you would like to suggest an improvement or modification,
30 | please submit a `GitHub issue `_ and we will get back to you.
31 |
32 | Dependencies
33 | ############
34 |
35 | This software is also dependent on other commonly used Python packages:
36 |
37 | - `cython: C-extensions for Python `_
38 | - `matplotlib: Python 2D plotting library `_
39 | - `numpy: Scientific Computing Tools For Python `_
40 | - `scipy: Scientific Tools for Python `_
41 |
42 | Disclaimer
43 | ##########
44 |
45 | This package includes software developed at NASA Goddard Space Flight Center (GSFC) and the University
46 | of Washington Applied Physics Laboratory (UW-APL).
47 | It is not sponsored or maintained by the Universities Space Research Association (USRA), or NASA.
48 |
--------------------------------------------------------------------------------
/doc/source/user_guide/Examples.rst:
--------------------------------------------------------------------------------
1 | .. _examples:
2 |
3 | ========
4 | Examples
5 | ========
6 |
7 | Jupyter Notebooks providing demonstrations of ``spatial-interpolators`` functionality:
8 |
9 | - Interpolate Franke |github cartesian| |nbviewer cartesian|
10 | Visualizing the different Cartesian interpolators
11 | - Interpolate Sphere |github sphere| |nbviewer sphere|
12 | Visualizing the different spherical interpolators
13 | - Interpolate Missing |github missing| |nbviewer missing|
14 | Visualizing inpainting algorithms for missing values in a grid
15 |
16 |
17 | .. |github cartesian| image:: https://img.shields.io/badge/GitHub-view-6f42c1?style=flat&logo=Github
18 | :target: https://github.com/tsutterley/spatial-interpolators/blob/main/notebooks/interpolate_franke.ipynb
19 |
20 | .. |nbviewer cartesian| image:: https://raw.githubusercontent.com/jupyter/design/master/logos/Badges/nbviewer_badge.svg
21 | :target: https://nbviewer.jupyter.org/github/tsutterley/spatial-interpolators/blob/main/notebooks/interpolate_franke.ipynb
22 |
23 | .. |github sphere| image:: https://img.shields.io/badge/GitHub-view-6f42c1?style=flat&logo=Github
24 | :target: https://github.com/tsutterley/spatial-interpolators/blob/main/notebooks/interpolate_sphere.ipynb
25 |
26 | .. |nbviewer sphere| image:: https://raw.githubusercontent.com/jupyter/design/master/logos/Badges/nbviewer_badge.svg
27 | :target: https://nbviewer.jupyter.org/github/tsutterley/spatial-interpolators/blob/main/notebooks/interpolate_sphere.ipynb
28 |
29 | .. |github missing| image:: https://img.shields.io/badge/GitHub-view-6f42c1?style=flat&logo=Github
30 | :target: https://github.com/tsutterley/spatial-interpolators/blob/main/notebooks/interpolate_missing.ipynb
31 |
32 | .. |nbviewer missing| image:: https://raw.githubusercontent.com/jupyter/design/master/logos/Badges/nbviewer_badge.svg
33 | :target: https://nbviewer.jupyter.org/github/tsutterley/spatial-interpolators/blob/main/notebooks/interpolate_missing.ipynb
34 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import logging
3 | from setuptools import setup, Extension, find_packages
4 |
5 | logging.basicConfig(stream=sys.stderr, level=logging.INFO)
6 | log = logging.getLogger()
7 |
8 | # package description and keywords
9 | description = 'Spatial interpolation tools for Python'
10 | keywords = 'spatial interpolation, regridding, regridding over a sphere'
11 | # get long_description from README.rst
12 | with open("README.rst", mode="r", encoding="utf8") as fh:
13 | long_description = fh.read()
14 | long_description_content_type = "text/x-rst"
15 |
16 | # get install requirements
17 | with open('requirements.txt', mode="r", encoding="utf8") as fh:
18 | install_requires = [line.split().pop(0) for line in fh.read().splitlines()]
19 |
20 | # get version
21 | with open('version.txt', mode="r", encoding="utf8") as fh:
22 | version = fh.read()
23 |
24 | # Setuptools 18.0 properly handles Cython extensions.
25 | setup_requires=[
26 | 'setuptools>=18.0',
27 | 'cython',
28 | ]
29 | # cythonize extensions
30 | ext_modules=[
31 | Extension('spatial_interpolators.PvQv_C',
32 | sources=['spatial_interpolators/PvQv_C.pyx'])
33 | ]
34 | setup(
35 | name='spatial-interpolators',
36 | version=version,
37 | description=description,
38 | long_description=long_description,
39 | long_description_content_type=long_description_content_type,
40 | url='https://github.com/tsutterley/spatial-interpolators',
41 | author='Tyler Sutterley',
42 | author_email='tsutterl@uw.edu',
43 | license='MIT',
44 | classifiers=[
45 | 'Development Status :: 3 - Alpha',
46 | 'Intended Audience :: Science/Research',
47 | 'Topic :: Scientific/Engineering :: Physics',
48 | 'License :: OSI Approved :: MIT License',
49 | 'Programming Language :: Python :: 3',
50 | 'Programming Language :: Python :: 3.6',
51 | 'Programming Language :: Python :: 3.7',
52 | 'Programming Language :: Python :: 3.8',
53 | ],
54 | keywords=keywords,
55 | packages=find_packages(),
56 | install_requires=install_requires,
57 | setup_requires=setup_requires,
58 | ext_modules=ext_modules,
59 | )
60 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | =====================
2 | spatial-interpolators
3 | =====================
4 |
5 | |Language|
6 | |License|
7 | |PyPI Version|
8 | |Documentation Status|
9 | |zenodo|
10 |
11 | .. |Language| image:: https://img.shields.io/pypi/pyversions/spatial-interpolators?color=green
12 | :target: https://www.python.org/
13 |
14 | .. |License| image:: https://img.shields.io/github/license/tsutterley/spatial-interpolators
15 | :target: https://github.com/tsutterley/spatial-interpolators/blob/main/LICENSE
16 |
17 | .. |PyPI Version| image:: https://img.shields.io/pypi/v/spatial-interpolators.svg
18 | :target: https://pypi.python.org/pypi/spatial-interpolators/
19 |
20 | .. |Documentation Status| image:: https://readthedocs.org/projects/spatial-interpolators/badge/?version=latest
21 | :target: https://spatial-interpolators.readthedocs.io/en/latest/?badge=latest
22 |
23 | .. |zenodo| image:: https://zenodo.org/badge/140747492.svg
24 | :target: https://zenodo.org/badge/latestdoi/140747492
25 |
26 | Functions to spatially interpolate data over Cartesian and spherical grids
27 |
28 | Dependencies
29 | ############
30 |
31 | - `cython: C-extensions for Python `_
32 | - `matplotlib: Python 2D plotting library `_
33 | - `numpy: Scientific Computing Tools For Python `_
34 | - `scipy: Scientific Tools for Python `_
35 |
36 | Download
37 | ########
38 |
39 | | The program homepage is:
40 | | https://github.com/tsutterley/spatial-interpolators
41 | | A zip archive of the latest version is available directly at:
42 | | https://github.com/tsutterley/spatial-interpolators/archive/main.zip
43 |
44 | Disclaimer
45 | ##########
46 |
47 | This project contains work and contributions from the `scientific community <./CONTRIBUTORS.rst>`_.
48 | It includes software developed at NASA Goddard Space Flight Center (GSFC) and the
49 | University of Washington Applied Physics Laboratory (UW-APL).
50 | This software not sponsored or maintained by the Universities Space Research Association (USRA), or NASA.
51 | It is provided here for your convenience but *with no guarantees whatsoever*.
52 |
53 | License
54 | #######
55 |
56 | The content of this project is licensed under the
57 | `Creative Commons Attribution 4.0 Attribution license `_
58 | and the source code is licensed under the `MIT license `_.
59 |
--------------------------------------------------------------------------------
/spatial_interpolators/PvQv_C.pyx:
--------------------------------------------------------------------------------
1 | # Calculate generalized Legendre functions of arbitrary degree v
2 | # Based on recipe in "An Atlas of Functions" by Spanier and Oldham, 1987 (589)
3 | # Pv is the Legendre function of the first kind
4 | # Qv is the Legendre function of the second kind
5 | from libc.math cimport sin, cos, pow, floor, sqrt, INFINITY, M_PI
6 | cdef extern from "complex.h":
7 | double cabs(double complex)
8 | double creal(double complex)
9 | double complex cpow(double complex, double complex)
10 | double complex csqrt(double complex)
11 | double complex csin(double complex)
12 | double complex ccos(double complex)
13 |
14 | def PvQv_C(double x, double complex v):
15 | cdef unsigned iter
16 | cdef double a, k
17 | cdef double complex P, Q, R, K, s, c, w, X, g, u, f, t
18 | iter = 0
19 | if (x == -1):
20 | P = -INFINITY
21 | Q = -INFINITY
22 | elif (x == +1):
23 | P = 1.0
24 | Q = INFINITY
25 | else:
26 | # set a and R to 1
27 | a = 1.0
28 | R = 1.0
29 | K = 4.0*csqrt(cabs(v - cpow(v,2)))
30 | if ((cabs(1.0 + v) + floor(1.0 + creal(v))) == 0):
31 | a = 1.0e99
32 | v = -1.0 - v
33 | # s and c = sin and cos of (pi*v/2.0)
34 | s = csin(0.5*M_PI*v)
35 | c = ccos(0.5*M_PI*v)
36 | w = cpow(0.5 + v, 2)
37 | # if v is less than or equal to six (repeat until greater than six)
38 | while (creal(v) <= 6.0):
39 | v += 2.0
40 | R = R*(v - 1.0)/v
41 | # calculate X and g and update R
42 | X = 1.0 / (4.0 + 4.0*v)
43 | g = 1.0 + 5*X*(1.0 - 3.0*X*(0.35 + 6.1*X))
44 | R = R*(1.0 - X*(1.0 - g*X/2))/csqrt(8.0*X)
45 | # set g and u to 2.0*x
46 | g = 2.0*x
47 | u = 2.0*x
48 | # set f and t to 1
49 | f = 1.0
50 | t = 1.0
51 | # set k to 1/2
52 | k = 0.5
53 | # calculate new X
54 | X = 1.0 + (1e8/(1.0 - pow(x,2)))
55 | # update t
56 | t = t*pow(x,2) * (pow(k,2) - w)/(pow(k + 1.0,2) - 0.25)
57 | # add 1 to k
58 | k += 1.0
59 | # add t to f
60 | f += t
61 | # update u
62 | u = u*pow(x,2) * (pow(k,2) - w)/(pow(k + 1.0,2) - 0.25)
63 | # add 1 to k
64 | k += 1.0
65 | # add u to g
66 | g += u
67 | # if k is less than K and |Xt| is greater than |f|
68 | # repeat previous set of operations until valid
69 | while ((k < creal(K)) | (cabs(X*t) > cabs(f))):
70 | iter += 1
71 | t = t*pow(x,2) * (pow(k,2) - w)/(pow(k + 1.0,2) - 0.25)
72 | k += 1.0
73 | f += t
74 | u = u*pow(x,2) * (pow(k,2) - w)/(pow(k + 1.0,2) - 0.25)
75 | k += 1.0
76 | g += u
77 | # update f and g
78 | f = f + (pow(x,2)*t/(1.0 - pow(x,2)))
79 | g = g + (pow(x,2)*u/(1.0 - pow(x,2)))
80 | # calculate generalized Legendre functions
81 | P = ((s*g*R) + (c*f/R))/sqrt(M_PI)
82 | Q = a*sqrt(M_PI)*((c*g*R) - (s*f/R))/2.0
83 | # return P, Q and number of iterations
84 | return (P, Q, iter)
85 |
--------------------------------------------------------------------------------
/doc/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | # import os
14 | # import sys
15 | import datetime
16 | # sys.path.insert(0, os.path.abspath('.'))
17 | from pkg_resources import get_distribution
18 |
19 |
20 | # -- Project information -----------------------------------------------------
21 |
22 | project = 'spatial-interpolators'
23 | year = datetime.date.today().year
24 | copyright = f"2018\u2013{year}, Tyler C. Sutterley"
25 | author = 'Tyler C. Sutterley'
26 |
27 | # The full version, including alpha/beta/rc tags
28 | # get semantic version from setuptools-scm
29 | version = get_distribution("spatial-interpolators").version
30 | # append "v" before the version
31 | release = "v{0}".format(version)
32 |
33 | # -- General configuration ---------------------------------------------------
34 |
35 | # Add any Sphinx extension module names here, as strings. They can be
36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
37 | # ones.
38 | extensions = [
39 | "sphinx.ext.autodoc",
40 | "numpydoc",
41 | "sphinx.ext.graphviz",
42 | "sphinx.ext.viewcode",
43 | "sphinxarg.ext"
44 | ]
45 |
46 | # Add any paths that contain templates here, relative to this directory.
47 | templates_path = ['_templates']
48 |
49 | # List of patterns, relative to source directory, that match files and
50 | # directories to ignore when looking for source files.
51 | # This pattern also affects html_static_path and html_extra_path.
52 | exclude_patterns = ['**.ipynb_checkpoints']
53 |
54 | # location of master document (by default sphinx looks for contents.rst)
55 | master_doc = 'index'
56 |
57 | # -- Configuration options ---------------------------------------------------
58 | autosummary_generate = True
59 | autodoc_member_order = 'bysource'
60 | numpydoc_show_class_members = False
61 | pygments_style = 'native'
62 |
63 | # -- Options for HTML output -------------------------------------------------
64 |
65 | # html_title = "spatial-interpolators"
66 | html_short_title = "spatial-interpolators"
67 | html_show_sourcelink = False
68 | html_show_sphinx = True
69 | html_show_copyright = True
70 |
71 | # The theme to use for HTML and HTML Help pages. See the documentation for
72 | # a list of builtin themes.
73 | #
74 | html_theme = 'sphinx_rtd_theme'
75 | html_theme_options = {}
76 |
77 | # Add any paths that contain custom static files (such as style sheets) here,
78 | # relative to this directory. They are copied after the builtin static files,
79 | # so a file named "default.css" will overwrite the builtin "default.css".
80 | html_static_path = ['_static']
81 | repository_url = "https://github.com/tsutterley/spatial-interpolators"
82 | html_context = {
83 | "menu_links": [
84 | (
85 | ' Source Code',
86 | repository_url,
87 | ),
88 | (
89 | ' License',
90 | f"{repository_url}/blob/main/LICENSE",
91 | ),
92 | ],
93 | }
94 |
95 | # Load the custom CSS files (needs sphinx >= 1.6 for this to work)
96 | def setup(app):
97 | """Load the custom CSS file"""
98 | app.add_css_file("style.css")
99 |
--------------------------------------------------------------------------------
/spatial_interpolators/inpaint.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | u"""
3 | inpaint.py
4 | Written by Tyler Sutterley (05/2022)
5 | Inpaint over missing data in a two-dimensional array using a
6 | penalized least square method based on discrete cosine
7 | transforms
8 |
9 | INPUTS:
10 | xs: input x-coordinates
11 | ys: input y-coordinates
12 | zs: input data
13 |
14 | OPTIONS:
15 | n: Number of iterations
16 | Use 0 for nearest neighbors interpolation
17 | s0: Smoothing
18 | z0: Initial guess for input data
19 | power: power for lambda function
20 | epsilon: relaxation factor
21 |
22 | REFERENCES:
23 | D. Garcia, Robust smoothing of gridded data in one and higher
24 | dimensions with missing values. Computational Statistics &
25 | Data Analysis, 54(4), 1167--1178 (2010)
26 | https://doi.org/10.1016/j.csda.2009.09.020
27 |
28 | G. Wang, D. Garcia, Y. Liu, R. de Jeu, and A. J. Dolman,
29 | A three-dimensional gap filling method for large geophysical
30 | datasets: Application to global satellite soil moisture
31 | observations, Environmental Modelling & Software,
32 | 30, 139--142 (2012)
33 | https://doi.org/10.1016/j.envsoft.2011.10.015
34 |
35 | UPDATE HISTORY:
36 | Written 06/2022
37 | """
38 | import numpy as np
39 | import scipy.fftpack
40 | import scipy.spatial
41 |
42 | def inpaint(xs, ys, zs, n=100, s0=3, z0=None, power=2, epsilon=2):
43 | """
44 | Inpaint over missing data in a two-dimensional array using a
45 | penalized least square method based on discrete cosine transforms
46 | [Garcia2010]_ [Wang2012]_
47 |
48 | Parameters
49 | ----------
50 | xs: float
51 | input x-coordinates
52 | ys: float
53 | input y-coordinates
54 | zs: float
55 | input data
56 | n: int, default 100
57 | Number of iterations
58 | Use 0 for nearest neighbors interpolation
59 | s0: int, default 3
60 | Smoothing
61 | z0: float or NoneType, default None
62 | Initial guess for input data
63 | power: int, default 2
64 | power for lambda function
65 | epsilon: int, default 2
66 | relaxation factor
67 |
68 | References
69 | ----------
70 | .. [Garcia2010] D. Garcia, Robust smoothing of gridded data
71 | in one and higher dimensions with missing values.
72 | Computational Statistics & Data Analysis, 54(4),
73 | 1167--1178 (2010). `doi: 10.1016/j.csda.2009.09.020
74 | `_
75 | .. [Wang2012] G. Wang, D. Garcia, Y. Liu, R. de Jeu, and A. J. Dolman,
76 | A three-dimensional gap filling method for large geophysical
77 | datasets: Application to global satellite soil moisture
78 | observations, Environmental Modelling & Software, 30,
79 | 139--142 (2012). `doi: 10.1016/j.envsoft.2011.10.015
80 | `_
81 | """
82 |
83 | # find masked values
84 | if isinstance(zs, np.ma.MaskedArray):
85 | W = np.logical_not(zs.mask)
86 | else:
87 | W = np.isfinite(zs)
88 | # no valid values can be found
89 | if not np.any(W):
90 | raise ValueError('No valid values found')
91 |
92 | # dimensions of input grid
93 | ny, nx = np.shape(zs)
94 | # calculate lambda function
95 | L = np.zeros((ny, nx))
96 | L += np.broadcast_to(np.cos(np.pi*np.arange(ny)/ny)[:, None], (ny, nx))
97 | L += np.broadcast_to(np.cos(np.pi*np.arange(nx)/nx)[None, :], (ny, nx))
98 | LAMBDA = np.power(2.0*(2.0 - L), power)
99 |
100 | # calculate initial values using nearest neighbors
101 | if z0 is None:
102 | z0 = nearest_neighbors(xs, ys, zs, W)
103 |
104 | # copy data to new array with 0 values for mask
105 | ZI = np.zeros((ny, nx))
106 | ZI[W] = np.copy(z0[W])
107 |
108 | # smoothness parameters
109 | s = np.logspace(s0, -6, n)
110 | for i in range(n):
111 | # calculate discrete cosine transform
112 | GAMMA = 1.0/(1.0 + s[i]*LAMBDA)
113 | discos = GAMMA*scipy.fftpack.dctn(W*(ZI - z0) + z0, norm='ortho')
114 | # update interpolated grid
115 | z0 = epsilon*scipy.fftpack.idctn(discos, norm='ortho') + \
116 | (1.0 - epsilon)*z0
117 |
118 | # reset original values
119 | z0[W] = np.copy(zs[W])
120 | # return the inpainted grid
121 | return z0
122 |
123 | # PURPOSE: use nearest neighbors to form an initial guess
124 | def nearest_neighbors(xs, ys, zs, W):
125 | """
126 | Calculate nearest neighbors to form an initial for
127 | missing values
128 |
129 | Parameters
130 | ----------
131 | xs: float
132 | input x-coordinates
133 | ys: float
134 | input y-coordinates
135 | zs: float
136 | input data
137 | W: bool
138 | mask with valid points
139 | """
140 | # computation of distance Matrix
141 | # use scipy spatial KDTree routines
142 | xgrid, ygrid = np.meshgrid(xs, ys)
143 | tree = scipy.spatial.cKDTree(np.c_[xgrid[W], ygrid[W]])
144 | # find nearest neighbors
145 | masked = np.logical_not(W)
146 | _, ii = tree.query(np.c_[xgrid[masked], ygrid[masked]], k=1)
147 | # copy valid original values
148 | z0 = np.zeros_like(zs)
149 | z0[W] = np.copy(zs[W])
150 | # copy nearest neighbors
151 | z0[masked] = zs[W][ii]
152 | # return initial guess
153 | return z0
154 |
--------------------------------------------------------------------------------
/spatial_interpolators/sph_bilinear.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | u"""
3 | sph_bilinear.py
4 | Written by Tyler Sutterley (05/2022)
5 |
6 | Interpolates data over a sphere using bilinear functions
7 |
8 | CALLING SEQUENCE:
9 | zi = sph_bilinear(x, y, z, xi, yi)
10 |
11 | INPUTS:
12 | x: input longitude
13 | y: input latitude
14 | z: input data (matrix)
15 | xi: output longitude
16 | yi: output latitude
17 |
18 | OUTPUTS:
19 | zi: interpolated data
20 |
21 | OPTIONS:
22 | flattened: input xi, yi are flattened arrays (nlon must equal nlat)
23 | fill_value: value to use if xi and yi are out of range
24 |
25 | PYTHON DEPENDENCIES:
26 | numpy: Scientific Computing Tools For Python
27 | https://numpy.org
28 |
29 | UPDATE HISTORY:
30 | Updated 05/2022: updated docstrings to numpy documentation format
31 | Updated 01/2022: added function docstrings
32 | Updated 09/2017: use minimum distances with FLATTENED method
33 | if indices are out of range: replace with FILL_VALUE
34 | Updated 03/2016: added FLATTENED option for regional grids to global grids
35 | Updated 11/2015: made easier to read with data and weight values
36 | Written 07/2013
37 | """
38 | import numpy as np
39 |
40 | def sph_bilinear(x, y, z, xi, yi, flattened=False, fill_value=-9999.0):
41 | """
42 | Spherical interpolation routine for gridded data using
43 | bilinear interpolation
44 |
45 | Parameters
46 | ----------
47 | x: float
48 | input longitude
49 | y: float
50 | input latitude
51 | z: float
52 | input data
53 | xi: float
54 | output longitude
55 | yi: float
56 | output latitude
57 | flattened: bool, default False
58 | input xi, yi are flattened arrays
59 | fill_value: float, default -9999.0
60 | value to use if xi and yi are out of range
61 |
62 | Returns
63 | -------
64 | zi: float
65 | interpolated data
66 | """
67 |
68 | # Converting input data into geodetic coordinates in radians
69 | phi = x*np.pi/180.0
70 | th = (90.0 - y)*np.pi/180.0
71 | # grid steps for lon and lat
72 | dlon = np.abs(x[1]-x[0])
73 | dlat = np.abs(y[1]-y[0])
74 | # grid steps in radians
75 | dphi = dlon*np.pi/180.0
76 | dth = dlat*np.pi/180.0
77 | # input data shape
78 | nx = len(x)
79 | ny = len(y)
80 | # Converting output data into geodetic coordinates in radians
81 | xphi = xi*np.pi/180.0
82 | xth = (90.0 - yi)*np.pi/180.0
83 | # check if using flattened array or two-dimensional lat/lon
84 | if flattened:
85 | # output array
86 | ndat = len(xi)
87 | zi = np.zeros((ndat))
88 | for i in range(0, ndat):
89 | # calculating the indices for the original grid
90 | dx = (x - np.floor(xi[i]/dlon)*dlon)**2
91 | dy = (y - np.floor(yi[i]/dlat)*dlat)**2
92 | iph = np.argmin(dx)
93 | ith = np.argmin(dy)
94 | # data is within range of values
95 | if ((iph+1) < nx) & ((ith+1) < ny):
96 | # corner data values for i,j
97 | Ia = z[iph, ith] # (0,0)
98 | Ib = z[iph+1, ith] # (1,0)
99 | Ic = z[iph, ith+1] # (0,1)
100 | Id = z[iph+1, ith+1] # (1,1)
101 | # corner weight values for i,j
102 | Wa = (xphi[i]-phi[iph])*(xth[i]-th[ith])
103 | Wb = (phi[iph+1]-xphi[i])*(xth[i]-th[ith])
104 | Wc = (xphi[i]-phi[iph])*(th[ith+1]-xth[i])
105 | Wd = (phi[iph+1]-xphi[i])*(th[ith+1]-xth[i])
106 | # divisor weight value
107 | W = (phi[iph+1]-phi[iph])*(th[ith+1]-th[ith])
108 | # calculate interpolated value for i
109 | zi[i] = (Ia*Wa + Ib*Wb + Ic*Wc + Id*Wd)/W
110 | else:
111 | # replace with fill value
112 | zi[i] = fill_value
113 | else:
114 | # output grid
115 | nphi = len(xi)
116 | nth = len(yi)
117 | zi = np.zeros((nphi, nth))
118 | for i in range(0, nphi):
119 | for j in range(0, nth):
120 | # calculating the indices for the original grid
121 | iph = np.floor(xphi[i]/dphi)
122 | jth = np.floor(xth[j]/dth)
123 | # data is within range of values
124 | if ((iph+1) < nx) & ((jth+1) < ny):
125 | # corner data values for i,j
126 | Ia = z[iph, jth] # (0,0)
127 | Ib = z[iph+1, jth] # (1,0)
128 | Ic = z[iph, jth+1] # (0,1)
129 | Id = z[iph+1, jth+1] # (1,1)
130 | # corner weight values for i,j
131 | Wa = (xphi[i]-phi[iph])*(xth[j]-th[jth])
132 | Wb = (phi[iph+1]-xphi[i])*(xth[j]-th[jth])
133 | Wc = (xphi[i]-phi[iph])*(th[jth+1]-xth[j])
134 | Wd = (phi[iph+1]-xphi[i])*(th[jth+1]-xth[j])
135 | # divisor weight value
136 | W = (phi[iph+1]-phi[iph])*(th[jth+1]-th[jth])
137 | # calculate interpolated value for i,j
138 | zi[i, j] = (Ia*Wa + Ib*Wb + Ic*Wc + Id*Wd)/W
139 | else:
140 | # replace with fill value
141 | zi[i, j] = fill_value
142 |
143 | # return the interpolated data
144 | return zi
145 |
--------------------------------------------------------------------------------
/doc/source/getting_started/Contributing.rst:
--------------------------------------------------------------------------------
1 | =======================
2 | Contribution Guidelines
3 | =======================
4 |
5 | We welcome and invite contributions from anyone at any career stage and with any amount of coding experience towards the development of ``spatial-interpolators``.
6 | We appreciate any and all contributions made to the project.
7 | You will be recognized for your work by being listed as one of the `project contributors <./Citations.html#contributors>`_.
8 |
9 | Ways to Contribute
10 | ------------------
11 |
12 | 1) Fixing typographical or coding errors
13 | 2) Submitting bug reports or feature requests through the use of `GitHub issues `_
14 | 3) Improving documentation and testing
15 | 4) Sharing use cases and examples (such as `Jupyter Notebooks <../user_guide/Examples.html>`_)
16 | 5) Providing code for everyone to use
17 |
18 | Requesting a Feature
19 | --------------------
20 | Check the `project issues tab `_ to see if the feature has already been suggested.
21 | If not, please submit a new issue describing your requested feature or enhancement .
22 | Please give your feature request both a clear title and description.
23 | Let us know if this is something you would like to contribute to ``spatial-interpolators`` in your description as well.
24 |
25 | Reporting a Bug
26 | ---------------
27 | Check the `project issues tab `_ to see if the problem has already been reported.
28 | If not, *please* submit a new issue so that we are made aware of the problem.
29 | Please provide as much detail as possible when writing the description of your bug report.
30 | Providing information and examples will help us resolve issues faster.
31 |
32 | Contributing Code or Examples
33 | -----------------------------
34 | We follow a standard Forking Workflow for code changes and additions.
35 | Submitted code goes through the pull request process for `continuous integration (CI) testing <./Contributing.html#continuous-integration>`_ and comments.
36 |
37 | General Guidelines
38 | ^^^^^^^^^^^^^^^^^^
39 |
40 | - Make each pull request as small and simple as possible
41 | - `Commit messages should be clear and describe the changes <./Contributing.html#semantic-commit-messages>`_
42 | - Larger changes should be broken down into their basic components and integrated separately
43 | - Bug fixes should be their own pull requests with an associated `GitHub issue `_
44 | - Write a descriptive pull request message with a clear title
45 | - Be patient as reviews of pull requests take time
46 |
47 | Steps to Contribute
48 | ^^^^^^^^^^^^^^^^^^^
49 |
50 | 1) Fork the repository to your personal GitHub account by clicking the "Fork" button on the project `main page `_. This creates your own server-side copy of the repository.
51 | 2) Either by cloning to your local system or working in `GitHub Codespaces `_, create a work environment to make your changes.
52 | 3) Add your fork as the ``origin`` remote and the original project repository as the ``upstream`` remote. While this step isn't a necessary, it allows you to keep your fork up to date in the future.
53 | 4) Create a new branch to do your work.
54 | 5) Make your changes on the new branch and add yourself to the list of project `contributors `_.
55 | 6) Push your work to GitHub under your fork of the project.
56 | 7) Submit a `Pull Request `_ from your forked branch to the project repository.
57 |
58 | Adding Examples
59 | ^^^^^^^^^^^^^^^
60 | Examples may be in the form of executable scripts or interactive `Jupyter Notebooks <../user_guide/Examples.html>`_.
61 | Fully working (but unrendered) examples should be submitted with the same steps as above.
62 |
63 | Continuous Integration
64 | ^^^^^^^^^^^^^^^^^^^^^^
65 | We use `GitHub Actions `_ continuous integration (CI) services to build and test the project on Linux (Ubuntu) and Mac Operating Systems.
66 | The configuration files for this service are in `.github/workflows `_.
67 | The workflows rely on the `requirements.txt `_ and `requirements-dev.txt `_ files to install the required dependencies.
68 |
69 | The GitHub Actions jobs include:
70 |
71 | * Running `flake8 `_ to check the code for style and compilation errors
72 |
73 | Semantic Commit Messages
74 | ^^^^^^^^^^^^^^^^^^^^^^^^
75 |
76 | Please follow the `Conventional Commits `_ specification for your commit messages to help organize the pull requests:
77 |
78 | .. code-block:: bash
79 |
80 | :
81 |
82 | [optional message body]
83 |
84 | where ```` is one of the following:
85 |
86 | - ``feat``: adding new features or programs
87 | - ``fix``: fixing bugs or problems
88 | - ``docs``: changing the documentation
89 | - ``style``: changing the line order or adding comments
90 | - ``refactor``: changing the names of variables or programs
91 | - ``ci``: changing the `continuous integration <./Contributing.html#continuous-integration>`_ configuration files or scripts
92 | - ``test``: adding or updating `continuous integration tests <./Contributing.html#continuous-integration>`_
93 |
--------------------------------------------------------------------------------
/spatial_interpolators/barnes_objective.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | u"""
3 | barnes_objective.py
4 | Written by Tyler Sutterley (05/2022)
5 |
6 | Barnes objective analysis for the optimal interpolation
7 | of an input grid using a successive corrections scheme
8 |
9 | CALLING SEQUENCE:
10 | ZI = barnes_objective(xs, ys, zs, XI, YI, XR, YR)
11 | ZI = barnes_objective(xs, ys, zs, XI, YI, XR, YR, RUNS=3)
12 |
13 | INPUTS:
14 | xs: input X data
15 | ys: input Y data
16 | zs: input data
17 | XI: grid X for output ZI
18 | YI: grid Y for output ZI
19 | XR: x component of Barnes smoothing length scale
20 | Remains fixed throughout the iterations
21 | YR: y component of Barnes smoothing length scale
22 | Remains fixed throughout the iterations
23 |
24 | OUTPUTS:
25 | ZI: interpolated grid
26 |
27 | OPTIONS:
28 | runs: number of iterations
29 |
30 | PYTHON DEPENDENCIES:
31 | numpy: Scientific Computing Tools For Python
32 | https://numpy.org
33 |
34 | REFERENCES:
35 | S. L. Barnes, Applications of the Barnes objective analysis scheme.
36 | Part I: effects of undersampling, wave position, and station
37 | randomness. J. of Atmos. and Oceanic Tech., 11, 1433-1448. (1994)
38 | S. L. Barnes, Applications of the Barnes objective analysis scheme.
39 | Part II: Improving derivative estimates. J. of Atmos. and
40 | Oceanic Tech., 11, 1449-1458. (1994_
41 | S. L. Barnes, Applications of the Barnes objective analysis scheme.
42 | Part III: Tuning for minimum error. J. of Atmos. and Oceanic
43 | Tech., 11, 1459-1479. (1994)
44 | R. Daley, Atmospheric data analysis, Cambridge Press, New York.
45 | Section 3.6. (1991)
46 |
47 | UPDATE HISTORY:
48 | Updated 05/2022: updated docstrings to numpy documentation format
49 | Updated 01/2022: added function docstrings
50 | Written 08/2016
51 | """
52 | import numpy as np
53 |
54 | def barnes_objective(xs, ys, zs, XI, YI, XR, YR, runs=3):
55 | """
56 | Barnes objective analysis for the optimal interpolation
57 | of an input grid using a successive corrections scheme
58 |
59 | Parameters
60 | ----------
61 | xs: float
62 | input x-coordinates
63 | ys: float
64 | input y-coordinates
65 | zs: float
66 | input data
67 | XI: float
68 | output x-coordinates for data grid
69 | YI: float
70 | output y-coordinates for data grid
71 | XR: float
72 | x-component of Barnes smoothing length scale
73 | YR: float
74 | y-component of Barnes smoothing length scale
75 | runs: int, default 3
76 | number of iterations
77 |
78 | Returns
79 | -------
80 | ZI: float
81 | interpolated data grid
82 |
83 | References
84 | ----------
85 | .. [Barnes1994a] S. L. Barnes,
86 | "Applications of the Barnes objective analysis scheme.
87 | Part I: Effects of undersampling, wave position, and
88 | station randomness," *Journal of Atmospheric and Oceanic
89 | Technology*, 11(6), 1433--1448, (1994).
90 | .. [Barnes1994b] S. L. Barnes,
91 | "Applications of the Barnes objective analysis scheme.
92 | Part II: Improving derivative estimates,"
93 | *Journal of Atmospheric and Oceanic Technology*,
94 | 11(6), 1449--1458, (1994).
95 | .. [Barnes1994c] S. L. Barnes,
96 | "Applications of the Barnes objective analysis scheme.
97 | Part III: Tuning for minimum error,"
98 | *Journal of Atmospheric and Oceanic Technology*,
99 | 11(6), 1459--1479, (1994).
100 | .. [Daley1991] R. Daley, *Atmospheric data analysis*,
101 | Cambridge Press, New York. (1991).
102 | """
103 | # remove singleton dimensions
104 | xs = np.squeeze(xs)
105 | ys = np.squeeze(ys)
106 | zs = np.squeeze(zs)
107 | XI = np.squeeze(XI)
108 | YI = np.squeeze(YI)
109 | # size of new matrix
110 | if (np.ndim(XI) == 1):
111 | nx = len(XI)
112 | else:
113 | nx, ny = np.shape(XI)
114 |
115 | # Check to make sure sizes of input arguments are correct and consistent
116 | if (len(zs) != len(xs)) | (len(zs) != len(ys)):
117 | raise Exception('Length of X, Y, and Z must be equal')
118 | if (np.shape(XI) != np.shape(YI)):
119 | raise Exception('Size of XI and YI must be equal')
120 |
121 | # square of Barnes smoothing lengths scale
122 | xr2 = XR**2
123 | yr2 = YR**2
124 | # allocate for output zp array
125 | zp = np.zeros_like(XI.flatten())
126 | # first analysis
127 | for i, XY in enumerate(zip(XI.flatten(), YI.flatten())):
128 | dx = np.abs(xs - XY[0])
129 | dy = np.abs(ys - XY[1])
130 | # calculate weights
131 | w = np.exp(-dx**2/xr2 - dy**2/yr2)
132 | zp[i] = np.sum(zs*w)/sum(w)
133 |
134 | # allocate for even and odd zp arrays if iterating
135 | if (runs > 0):
136 | zp_even = np.zeros_like(zs)
137 | zp_odd = np.zeros_like(zs)
138 |
139 | # for each run
140 | for n in range(runs):
141 | # calculate even and odd zp arrays
142 | for j, xy in enumerate(zip(xs, ys)):
143 | dx = np.abs(xs - xy[0])
144 | dy = np.abs(ys - xy[1])
145 | # calculate weights
146 | w = np.exp(-dx**2/xr2 - dy**2/yr2)
147 | # differing weights for even and odd arrays
148 | if ((n % 2) == 0):
149 | zp_even[j] = zp_odd[j] + np.sum((zs - zp_odd)*w)/np.sum(w)
150 | else:
151 | zp_odd[j] = zp_even[j] + np.sum((zs - zp_even)*w)/np.sum(w)
152 | # calculate zp for run n
153 | for i, XY in enumerate(zip(XI.flatten(), YI.flatten())):
154 | dx = np.abs(xs - XY[0])
155 | dy = np.abs(ys - XY[1])
156 | w = np.exp(-dx**2/xr2 - dy**2/yr2)
157 | # differing weights for even and odd arrays
158 | if ((n % 2) == 0):
159 | zp[i] = zp[i] + np.sum((zs - zp_even)*w)/np.sum(w)
160 | else:
161 | zp[i] = zp[i] + np.sum((zs - zp_odd)*w)/np.sum(w)
162 |
163 | # reshape to original dimensions
164 | if (np.ndim(XI) != 1):
165 | ZI = zp.reshape(nx, ny)
166 | else:
167 | ZI = zp.copy()
168 |
169 | # return output matrix/array
170 | return ZI
171 |
--------------------------------------------------------------------------------
/spatial_interpolators/legendre.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | u"""
3 | legendre.py
4 | Written by Tyler Sutterley (05/2022)
5 | Computes associated Legendre functions of degree l evaluated for elements x
6 | l must be a scalar integer and x must contain real values ranging -1 <= x <= 1
7 | Parallels the MATLAB legendre function
8 |
9 | Based on Fortran program by Robert L. Parker, Scripps Institution of
10 | Oceanography, Institute for Geophysics and Planetary Physics, UCSD. 1993
11 |
12 | INPUTS:
13 | l: degree of Legrendre polynomials
14 | x: elements ranging from -1 to 1
15 | typically cos(theta), where theta is the colatitude in radians
16 |
17 | OUTPUT:
18 | Pl: legendre polynomials of degree l for orders 0 to l
19 |
20 | OPTIONS:
21 | NORMALIZE: output Fully Normalized Associated Legendre Functions
22 |
23 | PYTHON DEPENDENCIES:
24 | numpy: Scientific Computing Tools For Python (https://numpy.org)
25 | scipy: Scientific Tools for Python (https://docs.scipy.org/doc/)
26 |
27 | REFERENCES:
28 | M. Abramowitz and I.A. Stegun, "Handbook of Mathematical Functions",
29 | Dover Publications, 1965, Ch. 8.
30 | J. A. Jacobs, "Geomagnetism", Academic Press, 1987, Ch.4.
31 |
32 | UPDATE HISTORY:
33 | Updated 05/2022: updated docstrings to numpy documentation format
34 | Updated 11/2021: modify normalization to prevent high degree overflows
35 | Updated 05/2021: define int/float precision to prevent deprecation warning
36 | Updated 02/2021: modify case with underflow
37 | Updated 09/2020: verify dimensions of x variable
38 | Updated 07/2020: added function docstrings
39 | Updated 05/2020: added normalization option for output polynomials
40 | Updated 03/2019: calculate twocot separately to avoid divide warning
41 | Written 08/2016
42 | """
43 | import numpy as np
44 |
45 | def legendre(l, x, NORMALIZE=False):
46 | """
47 | Computes associated Legendre functions of degree ``l``
48 | following [Abramowitz1965]_ and [Jacobs1987]_
49 |
50 | Parameters
51 | ----------
52 | l: int
53 | degree of Legrendre polynomials
54 | x: float
55 | elements ranging from -1 to 1
56 |
57 | Typically ``cos(theta)``, where ``theta`` is the colatitude in radians
58 | NORMALIZE: bool, default False
59 | Fully-normalize the Legendre Functions
60 |
61 | Returns
62 | -------
63 | Pl: legendre polynomials of degree ``l``
64 |
65 | References
66 | ----------
67 | .. [Abramowitz1965] M. Abramowitz and I. A. Stegun,
68 | *Handbook of Mathematical Functions*, 1046 pp., (1965).
69 |
70 | .. [Jacobs1987] J. A. Jacobs, *Geomagnetism*,
71 | Volume 1, 1st Edition, 832 pp., (1987).
72 | """
73 | # verify integer
74 | l = np.int64(l)
75 | # verify dimensions
76 | x = np.atleast_1d(x).flatten()
77 | # size of the x array
78 | nx = len(x)
79 |
80 | # for the l = 0 case
81 | if (l == 0):
82 | Pl = np.ones((1, nx), dtype=np.float64)
83 | return Pl
84 |
85 | # for all other degrees greater than 0
86 | rootl = np.sqrt(np.arange(0, 2*l+1)) # +1 to include 2*l
87 | # s is sine of colatitude (cosine of latitude) so that 0 <= s <= 1
88 | s = np.sqrt(1.0 - x**2) # for x=cos(th): s=sin(th)
89 | P = np.zeros((l+3, nx), dtype=np.float64)
90 |
91 | # Find values of x,s for which there will be underflow
92 | sn = (-s)**l
93 | tol = np.sqrt(np.finfo(np.float64).tiny)
94 | count = np.count_nonzero((s > 0) & (np.abs(sn) <= tol))
95 | if (count > 0):
96 | ind, = np.nonzero((s > 0) & (np.abs(sn) <= tol))
97 | # Approximate solution of x*ln(x) = Pl
98 | v = 9.2 - np.log(tol)/(l*s[ind])
99 | w = 1.0/np.log(v)
100 | m1 = 1 + l*s[ind]*v*w*(1.0058 + w*(3.819 - w*12.173))
101 | m1 = np.where(l < np.floor(m1), l, np.floor(m1)).astype(np.int64)
102 | # Column-by-column recursion
103 | for k, mm1 in enumerate(m1):
104 | col = ind[k]
105 | # Calculate twocot for underflow case
106 | twocot = -2.0*x[col]/s[col]
107 | P[mm1-1:l+1, col] = 0.0
108 | # Start recursion with proper sign
109 | tstart = np.finfo(np.float64).eps
110 | P[mm1-1, col] = np.sign(np.fmod(mm1, 2)-0.5)*tstart
111 | if (x[col] < 0):
112 | P[mm1-1, col] = np.sign(np.fmod(l+1, 2)-0.5)*tstart
113 | # Recur from m1 to m = 0, accumulating normalizing factor.
114 | sumsq = tol.copy()
115 | for m in range(mm1-2, -1, -1):
116 | P[m, col] = ((m+1)*twocot*P[m+1, col] -
117 | rootl[l+m+2]*rootl[l-m-1]*P[m+2, col]) / \
118 | (rootl[l+m+1]*rootl[l-m])
119 | sumsq += P[m, col]**2
120 | # calculate scale
121 | scale = 1.0/np.sqrt(2.0*sumsq - P[0, col]**2)
122 | P[0:mm1+1, col] = scale*P[0:mm1+1, col]
123 |
124 | # Find the values of x,s for which there is no underflow, and (x != +/-1)
125 | count = np.count_nonzero((x != 1) & (np.abs(sn) >= tol))
126 | if (count > 0):
127 | nind, = np.nonzero((x != 1) & (np.abs(sn) >= tol))
128 | # Calculate twocot for normal case
129 | twocot = -2.0*x[nind]/s[nind]
130 | # Produce normalization constant for the m = l function
131 | d = np.arange(2, 2*l+2, 2)
132 | c = np.prod(1.0 - 1.0/d)
133 | # Use sn = (-s)**l (written above) to write the m = l function
134 | P[l, nind] = np.sqrt(c)*sn[nind]
135 | P[l-1, nind] = P[l, nind]*twocot*l/rootl[-1]
136 |
137 | # Recur downwards to m = 0
138 | for m in range(l-2, -1, -1):
139 | P[m, nind] = (P[m+1, nind]*twocot*(m+1) -
140 | P[m+2, nind]*rootl[l+m+2]*rootl[l-m-1]) / \
141 | (rootl[l+m+1]*rootl[l-m])
142 |
143 | # calculate Pl from P
144 | Pl = np.copy(P[0:l+1, :])
145 |
146 | # Polar argument (x == +/-1)
147 | count = np.count_nonzero(s == 0)
148 | if (count > 0):
149 | s0, = np.nonzero(s == 0)
150 | Pl[0, s0] = x[s0]**l
151 |
152 | # calculate Fully Normalized Associated Legendre functions
153 | if NORMALIZE:
154 | norm = np.zeros((l+1))
155 | norm[0] = np.sqrt(2.0*l+1)
156 | m = np.arange(1, l+1)
157 | norm[1:] = (-1)**m*np.sqrt(2.0*(2.0*l+1.0))
158 | Pl *= np.kron(np.ones((1, nx)), norm[:, np.newaxis])
159 | else:
160 | # Calculate the unnormalized Legendre functions by multiplying each row
161 | # by: sqrt((l+m)!/(l-m)!) == sqrt(prod(n-m+1:n+m))
162 | # following Abramowitz and Stegun
163 | for m in range(1, l):
164 | Pl[m, :] *= np.prod(rootl[l-m+1:l+m+1])
165 | # sectoral case (l = m) should be done separately to handle 0!
166 | Pl[l, :] *= np.prod(rootl[1:])
167 |
168 | return Pl
169 |
--------------------------------------------------------------------------------
/spatial_interpolators/shepard_interpolant.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | u"""
3 | shepard_interpolant.py
4 | Written by Tyler Sutterley (05/2022)
5 |
6 | Evaluates Shepard interpolants to 2D data based on inverse distances
7 | Resultant output will not be as accurate as radial basis functions
8 |
9 | CALLING SEQUENCE:
10 | ZI = shepard_interpolant(xs, ys, zs, XI, YI)
11 | ZI = shepard_interpolant(xs, ys, zs, XI, YI, modified=True,
12 | D=25e3, L=500e3)
13 |
14 | INPUTS:
15 | xs: input X data
16 | ys: input Y data
17 | zs: input data
18 | XI: grid X for output ZI
19 | YI: grid Y for output ZI
20 |
21 | OUTPUTS:
22 | ZI: interpolated grid
23 |
24 | OPTIONS:
25 | power: Power used in the inverse distance weighting (positive real number)
26 | eps: minimum distance value for valid points (default 1e-7)
27 | modified: use declustering modified Shepard's interpolants
28 | D: declustering distance
29 | L: maximum distance to be included in weights
30 |
31 | PYTHON DEPENDENCIES:
32 | numpy: Scientific Computing Tools For Python
33 | https://numpy.org
34 |
35 | REFERENCES:
36 | D. Shepard, A two-dimensional interpolation function for irregularly
37 | spaced data, ACM68: Proceedings of the 1968 23rd ACM National
38 | Conference
39 | Schnell et al., Skill in forecasting extreme ozone pollution episodes with
40 | a global atmospheric chemistry model. Atmos. Chem. Phys., 14,
41 | 7721-7739, doi:10.5194/acp-14-7721-2014, 2014
42 |
43 | UPDATE HISTORY:
44 | Updated 05/2022: updated docstrings to numpy documentation format
45 | Updated 01/2022: added function docstrings
46 | Updated 09/2016: added modified Shepard's interpolants for declustering
47 | following Schnell et al (2014)
48 | Written 08/2016
49 | """
50 | import numpy as np
51 |
52 | def shepard_interpolant(xs, ys, zs, XI, YI, power=0.0, eps=1e-7,
53 | modified=False, D=25e3, L=500e3):
54 | """
55 | Evaluates Shepard interpolants to 2D data based on
56 | inverse distance weighting
57 |
58 | Parameters
59 | ----------
60 | xs: float
61 | input x-coordinates
62 | ys: float
63 | input y-coordinates
64 | zs: float
65 | input data
66 | XI: float
67 | output x-coordinates for data grid
68 | YI: float
69 | output y-coordinates for data grid
70 | power: float, default 0.0
71 | Power used in the inverse distance weighting
72 | eps: float, default 1e-7
73 | minimum distance value for valid points
74 | modified: boo, default False
75 | use declustering modified Shepard's interpolants [Schnell2014]_
76 | D: float, default 25e3
77 | declustering distance
78 | L: float, default 500e3
79 | maximum distance to be included in weights
80 |
81 | Returns
82 | -------
83 | ZI: float
84 | interpolated data grid
85 |
86 | References
87 | ----------
88 | .. [Schnell2014] J. Schnell, C. D. Holmes, A. Jangam, and M. J. Prather,
89 | "Skill in forecasting extreme ozone pollution episodes with a global
90 | atmospheric chemistry model," *Atmospheric Physics and chemistry*,
91 | 14(15), 7721--7739, (2014). `doi: 10.5194/acp-14-7721-2014
92 | `_
93 | .. [Shepard1968] D. Shepard, "A two-dimensional interpolation function
94 | for irregularly spaced data," *ACM68: Proceedings of the 1968 23rd
95 | ACM National Conference*, 517--524, (1968).
96 | `doi: 10.1145/800186.810616 `_
97 | """
98 |
99 | # remove singleton dimensions
100 | xs = np.squeeze(xs)
101 | ys = np.squeeze(ys)
102 | zs = np.squeeze(zs)
103 | XI = np.squeeze(XI)
104 | YI = np.squeeze(YI)
105 | # number of data points
106 | npts = len(zs)
107 | # size of new matrix
108 | if (np.ndim(XI) == 1):
109 | ni = len(XI)
110 | else:
111 | nx, ny = np.shape(XI)
112 | ni = XI.size
113 |
114 | # Check to make sure sizes of input arguments are correct and consistent
115 | if (len(zs) != len(xs)) | (len(zs) != len(ys)):
116 | raise Exception('Length of input arrays must be equal')
117 | if (np.shape(XI) != np.shape(YI)):
118 | raise Exception('Size of output arrays must be equal')
119 | if (power < 0):
120 | raise ValueError('Power parameter must be positive')
121 |
122 | # Modified Shepard interpolants for declustering data
123 | if modified:
124 | # calculate number of points within distance D of each point
125 | M = np.zeros((npts))
126 | for i, XY in enumerate(zip(xs, ys)):
127 | # compute radial distance between data point and data coordinates
128 | Rd = np.sqrt((XY[0] - xs)**2 + (XY[1] - ys)**2)
129 | M[i] = np.count_nonzero(Rd <= D)
130 |
131 | # for each interpolated value
132 | ZI = np.zeros((ni))
133 | for i, XY in enumerate(zip(XI.flatten(), YI.flatten())):
134 | # compute the radial distance between point i and data coordinates
135 | Re = np.sqrt((XY[0] - xs)**2 + (XY[1] - ys)**2)
136 | # Modified Shepard interpolants for declustering data
137 | if modified:
138 | # calculate weights
139 | w = np.zeros((npts))
140 | # find indices of cases
141 | ind_D = np.nonzero(Re < D)
142 | ind_M = np.nonzero((Re >= D) & (Re < L))
143 | ind_L = np.nonzero((Re >= L))
144 | # declustering of close points (weighted equally)
145 | w[ind_D] = D**(-power)/M[ind_D]
146 | # inverse distance weighting of mid-range points with scaling
147 | power_inverse_distance = Re[ind_M]**(-power)
148 | w[ind_M] = power_inverse_distance/M[ind_M]
149 | # no weight of distant points
150 | w[ind_L] = 0.0
151 | # calculate sum of all weights
152 | s = np.sum(w)
153 | # Find 2D interpolated surface
154 | ZI[i] = np.dot(w/s, zs) if (s > 0.0) else np.nan
155 | elif (Re < eps).any():
156 | # if a data coordinate is within the EPS cutoff
157 | min_indice, = np.nonzero(Re < eps)
158 | ZI[i] = zs[min_indice]
159 | else:
160 | # compute the weights based on POWER
161 | if (power == 0.0):
162 | # weights if POWER is 0
163 | w = np.ones((npts))/npts
164 | else:
165 | # normalized weights if POWER > 0 (typically between 1 and 3)
166 | # in the inverse distance weighting
167 | power_inverse_distance = Re**(-power)
168 | s = np.sum(power_inverse_distance)
169 | w = power_inverse_distance/s
170 | # Find 2D interpolated surface
171 | ZI[i] = np.dot(w, zs)
172 |
173 | # reshape to original dimensions
174 | if (np.ndim(XI) != 1):
175 | ZI = ZI.reshape(nx, ny)
176 | # return output matrix/array
177 | return ZI
178 |
--------------------------------------------------------------------------------
/notebooks/interpolate_sphere.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "## Test spherical spatial interpolators using Franke 3D evaluation function \n",
8 | "### Test functions\n",
9 | "- http://www.sfu.ca/~ssurjano/franke2d.html \n",
10 | "- http://www.sciencedirect.com/science/article/pii/S037704270100485X \n",
11 | "\n",
12 | "### QR Factorization (Fornberg)\n",
13 | "- http://epubs.siam.org/doi/abs/10.1137/060671991 \n",
14 | "- http://epubs.siam.org/doi/abs/10.1137/09076756X \n",
15 | "\n",
16 | "### Initial nodes\n",
17 | "- http://web.maths.unsw.edu.au/~rsw/Sphere/ \n",
18 | "- http://math.boisestate.edu/~wright/montestigliano/nodes.zip "
19 | ]
20 | },
21 | {
22 | "cell_type": "code",
23 | "execution_count": null,
24 | "metadata": {},
25 | "outputs": [],
26 | "source": [
27 | "import scipy.io\n",
28 | "import numpy as np\n",
29 | "import spatial_interpolators as spi\n",
30 | "import matplotlib.pyplot as plt"
31 | ]
32 | },
33 | {
34 | "cell_type": "markdown",
35 | "metadata": {},
36 | "source": [
37 | "### Franke's 3D evaluation function"
38 | ]
39 | },
40 | {
41 | "cell_type": "code",
42 | "execution_count": null,
43 | "metadata": {},
44 | "outputs": [],
45 | "source": [
46 | "def franke(x,y,z):\n",
47 | "\tF1 = 0.75*np.exp(-((9.*x-2.)**2 + (9.*y-2.)**2 + (9.0*z-2.)**2)/4.)\n",
48 | "\tF2 = 0.75*np.exp(-((9.*x+1.)**2/49. + (9.*y+1.)/10. + (9.0*z+1.)/10.))\n",
49 | "\tF3 = 0.5*np.exp(-((9.*x-7.)**2 + (9.*y-3.)**2 + (9.*z-5)**2)/4.)\n",
50 | "\tF4 = 0.2*np.exp(-((9.*x-4.)**2 + (9.*y-7.)**2 + (9.*z-5.)**2))\n",
51 | "\tF = F1 + F2 + F3 - F4\n",
52 | "\treturn F"
53 | ]
54 | },
55 | {
56 | "cell_type": "markdown",
57 | "metadata": {},
58 | "source": [
59 | "### Calculate Franke's evaluation function at nodal points"
60 | ]
61 | },
62 | {
63 | "cell_type": "code",
64 | "execution_count": null,
65 | "metadata": {},
66 | "outputs": [],
67 | "source": [
68 | "# using max_determinant nodal points from\n",
69 | "# http://math.boisestate.edu/~wright/montestigliano/nodes.zip\n",
70 | "N = 324\n",
71 | "xd = scipy.io.loadmat('md{0:05d}.mat'.format(N))\n",
72 | "x,y,z = xd['x'][:,0],xd['x'][:,1],xd['x'][:,2]\n",
73 | "# compute functional values at nodes\n",
74 | "f = franke(x,y,z)"
75 | ]
76 | },
77 | {
78 | "cell_type": "markdown",
79 | "metadata": {},
80 | "source": [
81 | "### Calculate Franke's evaluation function at grid points"
82 | ]
83 | },
84 | {
85 | "cell_type": "code",
86 | "execution_count": null,
87 | "metadata": {},
88 | "outputs": [],
89 | "source": [
90 | "# convert node coordinates to lat/lon\n",
91 | "lon,lat,_ = spi.spatial.to_sphere(x,y,z)\n",
92 | "\n",
93 | "# calculate output points (standard lat/lon grid)\n",
94 | "dlon = 5.0\n",
95 | "dlat = 5.0\n",
96 | "gridlon = np.arange(0,360+dlon,dlon)\n",
97 | "gridlat = np.arange(90,-90-dlat,-dlat)\n",
98 | "LON,LAT = np.meshgrid(gridlon,gridlat,indexing='ij')\n",
99 | "x,y,z = spi.spatial.to_cartesian(LON,LAT,a_axis=1.0,flat=0.0)\n",
100 | "# calculate functional values at output points\n",
101 | "FI = franke(x, y, z)"
102 | ]
103 | },
104 | {
105 | "cell_type": "markdown",
106 | "metadata": {},
107 | "source": [
108 | "### Interpolate to spherical grid"
109 | ]
110 | },
111 | {
112 | "cell_type": "code",
113 | "execution_count": null,
114 | "metadata": {},
115 | "outputs": [],
116 | "source": [
117 | "# interpolate with radial basis functions\n",
118 | "m = 'gaussian'\n",
119 | "RBF = spi.sph_radial_basis(lon,lat,f,LON,LAT,method=m,smooth=0.0001,\n",
120 | "\tepsilon=1.0)\n",
121 | "QR = spi.sph_radial_basis(lon,lat,f,LON,LAT,method=m,epsilon=0.9,QR=True)\n",
122 | "# interpolate with spherical splines\n",
123 | "t = 2.\n",
124 | "SPL = spi.sph_spline(lon,lat,f,LON,LAT,tension=t)"
125 | ]
126 | },
127 | {
128 | "cell_type": "markdown",
129 | "metadata": {},
130 | "source": [
131 | "### Create output plot showing interpolation and points"
132 | ]
133 | },
134 | {
135 | "cell_type": "code",
136 | "execution_count": null,
137 | "metadata": {},
138 | "outputs": [],
139 | "source": [
140 | "# plot interpolation and real-interpolated\n",
141 | "fig, ((ax1,ax2),(ax3,ax4)) = plt.subplots(num=1, ncols=2, nrows=2,\n",
142 | "\tsharex=True, sharey=True, figsize=(9,4.9))\n",
143 | "cmap = plt.cm.get_cmap('Spectral_r').copy()\n",
144 | "cmap.set_bad('w',0.)\n",
145 | "\n",
146 | "ax1.scatter(lon, lat, c=f, vmin=f.min(), vmax=f.max(),\n",
147 | "\tcmap=cmap, edgecolors='w')\n",
148 | "ax1.imshow(FI.transpose(),extent=(0,360,-90,90),\n",
149 | "\tvmin=f.min(), vmax=f.max(), cmap=cmap)\n",
150 | "ax2.imshow(RBF.transpose(),extent=(0,360,-90,90),\n",
151 | "\tvmin=f.min(), vmax=f.max(), cmap=cmap)\n",
152 | "ax2.scatter(lon, lat, c=f, vmin=f.min(), vmax=f.max(),\n",
153 | "\tcmap=cmap, edgecolors='none')\n",
154 | "ax3.imshow(QR.transpose(),extent=(0,360,-90,90),\n",
155 | "\tvmin=f.min(), vmax=f.max(), cmap=cmap)\n",
156 | "ax3.scatter(lon, lat, c=f, vmin=f.min(), vmax=f.max(),\n",
157 | "\tcmap=cmap, edgecolors='none')\n",
158 | "ax4.imshow(SPL.transpose(),extent=(0,360,-90,90),\n",
159 | "\tvmin=f.min(), vmax=f.max(), cmap=cmap)\n",
160 | "ax4.scatter(lon, lat, c=f, vmin=f.min(), vmax=f.max(),\n",
161 | "\tcmap=cmap, edgecolors='none')\n",
162 | "\n",
163 | "# for each axis\n",
164 | "for ax in [ax1,ax2,ax3,ax4]:\n",
165 | " # no ticks on the x and y axes\n",
166 | "\tax.get_xaxis().set_ticks([])\n",
167 | "\tax.get_yaxis().set_ticks([])\n",
168 | "\t# set x and y limits (global)\n",
169 | "\tax1.set_xlim(0,360)\n",
170 | "\tax1.set_ylim(-90,90)\n",
171 | "\n",
172 | "# set titles\n",
173 | "ax1.set_title('Franke Evaluation Function')\n",
174 | "ax2.set_title('RBF {0}'.format(m.capitalize()))\n",
175 | "ax3.set_title('RBF {0} with QR Factorization'.format(m.capitalize()))\n",
176 | "ax4.set_title('Spline with Tension {0:0.0f}'.format(t*100))\n",
177 | "# subplot adjustments\n",
178 | "fig.subplots_adjust(left=0.02,right=0.98,bottom=0.01,top=0.95,\n",
179 | "\twspace=0.02,hspace=0.11)\n",
180 | "plt.show()"
181 | ]
182 | },
183 | {
184 | "cell_type": "code",
185 | "execution_count": null,
186 | "metadata": {},
187 | "outputs": [],
188 | "source": []
189 | }
190 | ],
191 | "metadata": {
192 | "kernelspec": {
193 | "display_name": "Python 3.10.6 64-bit",
194 | "language": "python",
195 | "name": "python3"
196 | },
197 | "language_info": {
198 | "codemirror_mode": {
199 | "name": "ipython",
200 | "version": 3
201 | },
202 | "file_extension": ".py",
203 | "mimetype": "text/x-python",
204 | "name": "python",
205 | "nbconvert_exporter": "python",
206 | "pygments_lexer": "ipython3",
207 | "version": "3.10.6"
208 | },
209 | "vscode": {
210 | "interpreter": {
211 | "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6"
212 | }
213 | }
214 | },
215 | "nbformat": 4,
216 | "nbformat_minor": 2
217 | }
218 |
--------------------------------------------------------------------------------
/notebooks/interpolate_missing.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "## Test gridded spatial interpolators using Franke bivariate test function \n",
8 | "http://www.sfu.ca/~ssurjano/franke2d.html "
9 | ]
10 | },
11 | {
12 | "cell_type": "code",
13 | "execution_count": null,
14 | "metadata": {},
15 | "outputs": [],
16 | "source": [
17 | "import numpy as np\n",
18 | "import scipy.interpolate\n",
19 | "import spatial_interpolators as spi\n",
20 | "import matplotlib.pyplot as plt"
21 | ]
22 | },
23 | {
24 | "cell_type": "markdown",
25 | "metadata": {},
26 | "source": [
27 | "### Franke's bivariate test function"
28 | ]
29 | },
30 | {
31 | "cell_type": "code",
32 | "execution_count": null,
33 | "metadata": {},
34 | "outputs": [],
35 | "source": [
36 | "def franke(x,y):\n",
37 | "\tF1 = 0.75*np.exp(-((9.0*x-2.0)**2 + (9.0*y-2.0)**2)/4.0)\n",
38 | "\tF2 = 0.75*np.exp(-((9.0*x+1.0)**2/49.0-(9.0*y+1.0)/10.0))\n",
39 | "\tF3 = 0.5*np.exp(-((9.0*x-7.0)**2 + (9.0*y-3.0)**2)/4.0)\n",
40 | "\tF4 = 0.2*np.exp(-((9.0*x-4.0)**2 + (9.0*y-7.0)**2))\n",
41 | "\tF = F1 + F2 + F3 - F4\n",
42 | "\treturn F"
43 | ]
44 | },
45 | {
46 | "cell_type": "markdown",
47 | "metadata": {},
48 | "source": [
49 | "### Calculate Franke's evaluation function"
50 | ]
51 | },
52 | {
53 | "cell_type": "code",
54 | "execution_count": null,
55 | "metadata": {},
56 | "outputs": [],
57 | "source": [
58 | "# calculate output points\n",
59 | "nx = 250\n",
60 | "ny = 250\n",
61 | "xpts = np.arange(nx)/np.float64(nx)\n",
62 | "ypts = np.arange(ny)/np.float64(ny)\n",
63 | "XI,YI = np.meshgrid(xpts,ypts)\n",
64 | "# calculate real values at grid points\n",
65 | "ZI = np.ma.zeros((ny,nx))\n",
66 | "ZI.mask = np.zeros((ny,nx),dtype=bool)\n",
67 | "ZI.data[:] = franke(XI,YI)\n",
68 | "# create random points to be removed from the grid\n",
69 | "indx = np.random.randint(0, high=nx, size=32150)\n",
70 | "indy = np.random.randint(0, high=ny, size=32150)\n",
71 | "ZI.mask[indy,indx] = True"
72 | ]
73 | },
74 | {
75 | "cell_type": "markdown",
76 | "metadata": {},
77 | "source": [
78 | "### Plot Original Franke Function with Missing Values"
79 | ]
80 | },
81 | {
82 | "cell_type": "code",
83 | "execution_count": null,
84 | "metadata": {},
85 | "outputs": [],
86 | "source": [
87 | "# plot data and interpolated data\n",
88 | "f1, ax1 = plt.subplots(num=1, figsize=(6,6))\n",
89 | "extents=(0,1,1,0)\n",
90 | "\n",
91 | "# create color map with invalid points\n",
92 | "cmap = plt.cm.get_cmap('Spectral_r').copy()\n",
93 | "cmap.set_bad('w',0.)\n",
94 | "# plot read data with missing values\n",
95 | "ax1.imshow(ZI, interpolation='nearest', extent=extents, cmap=cmap,\n",
96 | "\tvmin=ZI.min(), vmax=ZI.max())\n",
97 | "# no ticks on the x and y axes\n",
98 | "ax1.get_xaxis().set_ticks([]); ax1.get_yaxis().set_ticks([])\n",
99 | "# set x and y limits\n",
100 | "ax1.set_xlim(0, 1)\n",
101 | "ax1.set_ylim(0, 1)\n",
102 | "# add titles\n",
103 | "ax1.set_title('Franke Function')\n",
104 | "# subplot adjustments\n",
105 | "f1.subplots_adjust(left=0.02,right=0.98,bottom=0.02,top=0.95,\n",
106 | "\twspace=0.02,hspace=0.1)\n",
107 | "plt.show()"
108 | ]
109 | },
110 | {
111 | "cell_type": "markdown",
112 | "metadata": {},
113 | "source": [
114 | "### Interpolate missing values using inpainting algorithms"
115 | ]
116 | },
117 | {
118 | "cell_type": "code",
119 | "execution_count": null,
120 | "metadata": {},
121 | "outputs": [],
122 | "source": [
123 | "interp = {}\n",
124 | "interp['nearest'] = spi.inpaint(xpts, ypts, ZI, n=0)\n",
125 | "interp['inpainted'] = spi.inpaint(xpts, ypts, ZI, n=100)"
126 | ]
127 | },
128 | {
129 | "cell_type": "markdown",
130 | "metadata": {},
131 | "source": [
132 | "### Plot Interpolated Grids"
133 | ]
134 | },
135 | {
136 | "cell_type": "code",
137 | "execution_count": null,
138 | "metadata": {},
139 | "outputs": [],
140 | "source": [
141 | "# plot data and interpolated data\n",
142 | "f2,ax2 = plt.subplots(num=2, ncols=2, sharex=True, sharey=True, figsize=(12,6))\n",
143 | "# plot interpolated data with filled values\n",
144 | "for i,key in enumerate(interp.keys()):\n",
145 | "\tax2[i].imshow(interp[key],\n",
146 | " interpolation='nearest',\n",
147 | "\t\textent=extents, cmap=cmap,\n",
148 | "\t\tvmin=ZI.min(), vmax=ZI.max())\n",
149 | "\t# no ticks on the x and y axes\n",
150 | "\tax2[i].get_xaxis().set_ticks([])\n",
151 | "\tax2[i].get_yaxis().set_ticks([])\n",
152 | "\t# set x and y limits\n",
153 | "\tax2[i].set_xlim(0, 1)\n",
154 | "\tax2[i].set_ylim(0, 1)\n",
155 | "# add titles\n",
156 | "ax2[0].set_title('Nearest Neighbors')\n",
157 | "ax2[1].set_title('DCT Inpainted')\n",
158 | "# subplot adjustments\n",
159 | "f2.subplots_adjust(left=0.02,right=0.98,bottom=0.02,top=0.95,\n",
160 | "\twspace=0.02,hspace=0.1)\n",
161 | "plt.show()"
162 | ]
163 | },
164 | {
165 | "cell_type": "code",
166 | "execution_count": null,
167 | "metadata": {},
168 | "outputs": [],
169 | "source": [
170 | "# calculate real values at grid points\n",
171 | "ZAll = franke(XI,YI)\n",
172 | "difference = {}\n",
173 | "for key,val in interp.items():\n",
174 | "\tdifference[key] = np.sqrt((ZAll - val)**2)\n",
175 | "\n",
176 | "# plot data and interpolated data\n",
177 | "f2,ax2 = plt.subplots(num=2, ncols=2, sharex=True, sharey=True, figsize=(12,6))\n",
178 | "\n",
179 | "# create color map with invalid points\n",
180 | "dmap = plt.cm.get_cmap('viridis').copy()\n",
181 | "dmap.set_bad('w',0.)\n",
182 | "# maximum value in differences\n",
183 | "vmax = np.max([np.max(val) for key,val in difference.items()])\n",
184 | "# inverse indices\n",
185 | "invy,invx = np.nonzero(np.logical_not(ZI.mask))\n",
186 | "ninv = np.count_nonzero(np.logical_not(ZI.mask))\n",
187 | "\n",
188 | "# plot differences\n",
189 | "for i,key in enumerate(difference.keys()):\n",
190 | "\tRMS = np.sqrt(np.sum(difference[key][indy,indx]**2)/ninv)\n",
191 | "\tprint('{0} RMS: {1:0.6f}'.format(key,RMS))\n",
192 | "\tax2[i].imshow(difference[key],\n",
193 | "\t\tinterpolation='nearest',\n",
194 | "\t\textent=extents, cmap=dmap,\n",
195 | "\t\tvmin=0, vmax=vmax)\n",
196 | "\t# no ticks on the x and y axes\n",
197 | "\tax2[i].get_xaxis().set_ticks([])\n",
198 | "\tax2[i].get_yaxis().set_ticks([])\n",
199 | "\t# set x and y limits\n",
200 | "\tax2[i].set_xlim(0, 1)\n",
201 | "\tax2[i].set_ylim(0, 1)\n",
202 | "\t# add titles\n",
203 | "\tax2[0].set_title('Nearest Neighbors')\n",
204 | "\tax2[1].set_title('DCT Inpainted')\n",
205 | "\n",
206 | "# subplot adjustments\n",
207 | "f2.subplots_adjust(left=0.02,right=0.98,bottom=0.02,top=0.95,\n",
208 | "wspace=0.02,hspace=0.1)\n",
209 | "plt.show()"
210 | ]
211 | }
212 | ],
213 | "metadata": {
214 | "kernelspec": {
215 | "display_name": "Python 3.10.6 64-bit",
216 | "language": "python",
217 | "name": "python3"
218 | },
219 | "language_info": {
220 | "codemirror_mode": {
221 | "name": "ipython",
222 | "version": 3
223 | },
224 | "file_extension": ".py",
225 | "mimetype": "text/x-python",
226 | "name": "python",
227 | "nbconvert_exporter": "python",
228 | "pygments_lexer": "ipython3",
229 | "version": "3.10.6"
230 | },
231 | "vscode": {
232 | "interpreter": {
233 | "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6"
234 | }
235 | }
236 | },
237 | "nbformat": 4,
238 | "nbformat_minor": 4
239 | }
240 |
--------------------------------------------------------------------------------
/spatial_interpolators/compact_radial_basis.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | u"""
3 | compact_radial_basis.py
4 | Written by Tyler Sutterley (05/2022)
5 |
6 | Interpolates data using compactly supported radial basis functions
7 | of minimal degree (Wendland functions) and sparse matrix algebra
8 |
9 | Wendland functions have the form
10 | p(r) if 0 <= r <= 1
11 | 0 if r > 1
12 | where p represents a univariate polynomial
13 |
14 | CALLING SEQUENCE:
15 | ZI = compact_radial_basis(xs, ys, zs, XI, YI, dimension, order,
16 | smooth=smooth, radius=radius, method='wendland')
17 |
18 | INPUTS:
19 | xs: scaled input X data
20 | ys: scaled input Y data
21 | zs: input data
22 | XI: scaled grid X for output ZI
23 | YI: scaled grid Y for output ZI
24 | dimension: spatial dimension of Wendland function (d)
25 | order: smoothness order of Wendland function (k)
26 |
27 | OUTPUTS:
28 | ZI: interpolated data grid
29 |
30 | OPTIONS:
31 | smooth: smoothing weights
32 | radius: scaling factor for the basis function (the radius of the
33 | support of the function)
34 | method: compactly supported radial basis function
35 | buhmann (not yet implemented)
36 | wendland (default)
37 | wu (not yet implemented)
38 |
39 | PYTHON DEPENDENCIES:
40 | numpy: Scientific Computing Tools For Python
41 | https://numpy.org
42 | scipy: Scientific Tools for Python
43 | https://docs.scipy.org/doc/
44 |
45 | REFERENCES:
46 | Holger Wendland, "Piecewise polynomial, positive definite and compactly
47 | supported radial functions of minimal degree." Advances in
48 | Computational Mathematics, 1995.
49 | Holger Wendland, "Scattered Data Approximation", Cambridge Monographs on
50 | Applied and Computational Mathematics, 2005.
51 | Martin Buhmann, "Radial Basis Functions", Cambridge Monographs on
52 | Applied and Computational Mathematics, 2003.
53 |
54 | UPDATE HISTORY:
55 | Updated 05/2022: updated docstrings to numpy documentation format
56 | Updated 01/2022: added function docstrings
57 | Updated 02/2019: compatibility updates for python3
58 | Updated 09/2017: using rcond=-1 in numpy least-squares algorithms
59 | Updated 08/2016: using format text within ValueError, edit constant vector
60 | removed 3 dimensional option of radial basis (spherical)
61 | changed hierarchical_radial_basis to compact_radial_basis using
62 | compactly-supported radial basis functions and sparse matrices
63 | added low-order polynomial option (previously used default constant)
64 | Updated 01/2016: new hierarchical_radial_basis function
65 | that first reduces to points within distance. added cutoff option
66 | Updated 10/2014: added third dimension (spherical)
67 | Written 08/2014
68 | """
69 | from __future__ import print_function, division
70 | import numpy as np
71 | import scipy.sparse
72 | import scipy.sparse.linalg
73 | import scipy.spatial
74 |
75 | def compact_radial_basis(xs, ys, zs, XI, YI, dimension, order, smooth=0.,
76 | radius=None, method='wendland'):
77 | """
78 | Interpolates a sparse grid using compactly supported radial basis
79 | functions of minimal degree and sparse matrix algebra
80 |
81 | Parameters
82 | ----------
83 | xs: float
84 | scaled input x-coordinates
85 | ys: float
86 | scaled input y-coordinates
87 | zs: float
88 | input data
89 | XI: float
90 | scaled output x-coordinates for data grid
91 | YI: float
92 | scaled output y-coordinates for data grid
93 | dimension: int
94 | spatial dimension of Wendland function (d)
95 | order: int
96 | smoothness order of Wendland function (k)
97 | smooth: float, default 0.0
98 | smoothing weights
99 | radius: float or NoneType, default None
100 | scaling factor for the basis function
101 | method: str, default `wendland`
102 | compactly supported radial basis function
103 |
104 | * ``'wendland'``
105 |
106 | Returns
107 | -------
108 | ZI: float
109 | interpolated data grid
110 |
111 | References
112 | ----------
113 | .. [Buhmann2003] M. Buhmann, "Radial Basis Functions",
114 | *Cambridge Monographs on Applied and Computational
115 | Mathematics*, (2003).
116 | .. [Wendland1995] H. Wendland, "Piecewise polynomial,
117 | positive definite and compactly supported radial
118 | functions of minimal degree," *Advances in
119 | Computational Mathematics*, 4, 389--396, (1995).
120 | `doi: 10.1007/BF02123482 `_
121 | .. [Wendland2005] H. Wendland, "Scattered Data Approximation",
122 | *Cambridge Monographs on Applied and Computational Mathematics*,
123 | (2005).
124 | """
125 | # remove singleton dimensions
126 | xs = np.squeeze(xs)
127 | ys = np.squeeze(ys)
128 | zs = np.squeeze(zs)
129 | XI = np.squeeze(XI)
130 | YI = np.squeeze(YI)
131 | # size of new matrix
132 | if (np.ndim(XI) == 1):
133 | nx = len(XI)
134 | else:
135 | nx, ny = np.shape(XI)
136 |
137 | # Check to make sure sizes of input arguments are correct and consistent
138 | if (len(zs) != len(xs)) | (len(zs) != len(ys)):
139 | raise Exception('Length of input arrays must be equal')
140 | if (np.shape(XI) != np.shape(YI)):
141 | raise Exception('Shape of output arrays must be equal')
142 |
143 | # create python dictionary of compact radial basis function formulas
144 | radial_basis_functions = {}
145 | # radial_basis_functions['buhmann'] = buhmann
146 | radial_basis_functions['wendland'] = wendland
147 | # radial_basis_functions['wu'] = wu
148 | # check if formula name is listed
149 | if method in radial_basis_functions.keys():
150 | cRBF = radial_basis_functions[method]
151 | else:
152 | raise ValueError(f"Method {method} not implemented")
153 |
154 | # construct kd-tree for Data points
155 | kdtree = scipy.spatial.cKDTree(np.c_[xs, ys])
156 | if radius is None:
157 | # quick nearest-neighbor lookup to calculate mean radius
158 | ds, _ = kdtree.query(np.c_[xs, ys], k=2)
159 | radius = 2.0*np.mean(ds[:, 1])
160 |
161 | # Creation of data-data distance sparse matrix in COOrdinate format
162 | Rd = kdtree.sparse_distance_matrix(kdtree, radius,
163 | output_type='coo_matrix')
164 | # calculate ratio between data-data distance and radius
165 | # replace cases where the data-data distance is greater than the radius
166 | r0 = np.where(Rd.data < radius, Rd.data/radius, radius/radius)
167 | # calculation of model PHI
168 | PHI = cRBF(r0, dimension, order)
169 | # construct sparse radial matrix
170 | PHI = scipy.sparse.coo_matrix((PHI, (Rd.row, Rd.col)), shape=Rd.shape)
171 | # Augmentation of the PHI Matrix with a smoothing factor
172 | if (smooth != 0):
173 | # calculate eigenvalues of distance matrix
174 | eig = scipy.sparse.linalg.eigsh(Rd, k=1, which="LA", maxiter=1000,
175 | return_eigenvectors=False)[0]
176 | PHI += scipy.sparse.identity(len(xs), format='coo') * smooth * eig
177 |
178 | # Computation of the Weights
179 | w = scipy.sparse.linalg.spsolve(PHI, zs)
180 |
181 | # construct kd-tree for Mesh points
182 | # Data to Mesh Points
183 | mkdtree = scipy.spatial.cKDTree(np.c_[XI.flatten(), YI.flatten()])
184 | # Creation of data-mesh distance sparse matrix in COOrdinate format
185 | Re = kdtree.sparse_distance_matrix(mkdtree, radius,
186 | output_type='coo_matrix')
187 | # calculate ratio between data-mesh distance and radius
188 | # replace cases where the data-mesh distance is greater than the radius
189 | R0 = np.where(Re.data < radius, Re.data/radius, radius/radius)
190 | # calculation of the Evaluation Matrix
191 | E = cRBF(R0, dimension, order)
192 | # construct sparse radial matrix
193 | E = scipy.sparse.coo_matrix((E, (Re.row, Re.col)), shape=Re.shape)
194 |
195 | # calculate output interpolated array (or matrix)
196 | if (np.ndim(XI) == 1):
197 | ZI = E.transpose().dot(w[:, np.newaxis])
198 | else:
199 | ZI = np.zeros((nx, ny))
200 | ZI[:, :] = E.transpose().dot(w[:, np.newaxis]).reshape(nx, ny)
201 | # return the interpolated array (or matrix)
202 | return ZI
203 |
204 | # define compactly supported radial basis function formulas
205 | def wendland(r, d, k):
206 | # Wendland functions of dimension d and order k
207 | # can replace with recursive method of Wendland for generalized case
208 | L = (d//2) + k + 1
209 | if (k == 0):
210 | f = (1. - r)**L
211 | elif (k == 1):
212 | f = (1. - r)**(L + 1)*((L + 1.)*r + 1.)
213 | elif (k == 2):
214 | f = (1. - r)**(L + 2)*((L**2 + 4.*L + 3.)*r**2 + (3.*L + 6.)*r + 3.)
215 | elif (k == 3):
216 | f = (1. - r)**(L + 3)*((L**3 + 9.*L**2 + 23.*L + 15.)*r**3 +
217 | (6.*L**2 + 36.*L + 45.)*r**2 + (15.*L + 45.)*r + 15.)
218 | return f
219 |
--------------------------------------------------------------------------------
/notebooks/interpolate_franke.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "## Test cartesian spatial interpolators using Franke bivariate test function \n",
8 | "http://www.sfu.ca/~ssurjano/franke2d.html "
9 | ]
10 | },
11 | {
12 | "cell_type": "code",
13 | "execution_count": null,
14 | "metadata": {},
15 | "outputs": [],
16 | "source": [
17 | "import numpy as np\n",
18 | "import scipy.interpolate\n",
19 | "import spatial_interpolators as spi\n",
20 | "import matplotlib.pyplot as plt"
21 | ]
22 | },
23 | {
24 | "cell_type": "markdown",
25 | "metadata": {},
26 | "source": [
27 | "### Franke's bivariate test function"
28 | ]
29 | },
30 | {
31 | "cell_type": "code",
32 | "execution_count": null,
33 | "metadata": {},
34 | "outputs": [],
35 | "source": [
36 | "def franke(x,y):\n",
37 | "\tF1 = 0.75*np.exp(-((9.0*x-2.0)**2 + (9.0*y-2.0)**2)/4.0)\n",
38 | "\tF2 = 0.75*np.exp(-((9.0*x+1.0)**2/49.0-(9.0*y+1.0)/10.0))\n",
39 | "\tF3 = 0.5*np.exp(-((9.0*x-7.0)**2 + (9.0*y-3.0)**2)/4.0)\n",
40 | "\tF4 = 0.2*np.exp(-((9.0*x-4.0)**2 + (9.0*y-7.0)**2))\n",
41 | "\tF = F1 + F2 + F3 - F4\n",
42 | "\treturn F"
43 | ]
44 | },
45 | {
46 | "cell_type": "markdown",
47 | "metadata": {},
48 | "source": [
49 | "### Calculate Franke's evaluation function"
50 | ]
51 | },
52 | {
53 | "cell_type": "code",
54 | "execution_count": null,
55 | "metadata": {},
56 | "outputs": [],
57 | "source": [
58 | "# calculate Franke's evaluation function at random points\n",
59 | "X = np.random.rand(500)\n",
60 | "Y = np.random.rand(500)\n",
61 | "Z = franke(X, Y)\n",
62 | "# calculate output points\n",
63 | "nx = 250\n",
64 | "ny = 250\n",
65 | "xpts = np.arange(nx)/np.float64(nx)\n",
66 | "ypts = np.arange(ny)/np.float64(ny)\n",
67 | "XI,YI = np.meshgrid(xpts,ypts)\n",
68 | "# calculate real values at grid points\n",
69 | "ZI = franke(XI,YI)"
70 | ]
71 | },
72 | {
73 | "cell_type": "markdown",
74 | "metadata": {},
75 | "source": [
76 | "### Interpolate to grid"
77 | ]
78 | },
79 | {
80 | "cell_type": "code",
81 | "execution_count": null,
82 | "metadata": {},
83 | "outputs": [],
84 | "source": [
85 | "# interpolate with radial basis functions\n",
86 | "radial = spi.radial_basis(X, Y, Z, XI, YI, polynomial=0,\n",
87 | "\tsmooth=0.001, epsilon=10.0, method='inverse')\n",
88 | "wendland = spi.compact_radial_basis(X, Y, Z, XI, YI,\n",
89 | "\t3, 3, radius=0.45, smooth=0.01)\n",
90 | "\n",
91 | "# interpolate with biharmonic spline functions\n",
92 | "spline = {}\n",
93 | "# spline['0'] = spi.biharmonic_spline(X, Y, Z, XI, YI, tension=0)\n",
94 | "spline['10'] = spi.biharmonic_spline(X, Y, Z, XI, YI, tension=0.1)\n",
95 | "# spline['50'] = spi.biharmonic_spline(X, Y, Z, XI, YI, tension=0.5)\n",
96 | "# spline['90'] = spi.biharmonic_spline(X, Y, Z, XI, YI, tension=0.9)\n",
97 | "# spline['R10'] = spi.biharmonic_spline(X, Y, Z, XI, YI,\n",
98 | "# \ttension=0.1, regular=True)\n",
99 | "spline['R50'] = spi.biharmonic_spline(X, Y, Z, XI, YI,\n",
100 | "\ttension=0.5, regular=True)\n",
101 | "\n",
102 | "# interpolate with Shepard Interpolant function\n",
103 | "shepard = {}\n",
104 | "# shepard['0'] = spi.shepard_interpolant(X, Y, Z, XI, YI, power=0.,\n",
105 | "# \tmodified=True, D=0.1, L=0.5)\n",
106 | "# shepard['1'] = spi.shepard_interpolant(X, Y, Z, XI, YI, power=1.,\n",
107 | "# \tmodified=True, D=0.1, L=0.5)\n",
108 | "shepard['2'] = spi.shepard_interpolant(X, Y, Z, XI, YI, power=2.,\n",
109 | "\tmodified=True, D=0.1, L=0.5)\n",
110 | "# shepard['3'] = spi.shepard_interpolant(X, Y, Z, XI, YI, power=3.,\n",
111 | "# \tmodified=True, D=0.1, L=0.5)\n",
112 | "# shepard['5'] = spi.shepard_interpolant(X, Y, Z, XI, YI, power=5.,\n",
113 | "# \tmodified=True, D=0.1, L=0.5)\n",
114 | "# shepard['10'] = spi.shepard_interpolant(X, Y, Z, XI, YI, power=10.,\n",
115 | "# \tmodified=True, D=0.1, L=0.5)\n",
116 | "# shepard['16'] = spi.shepard_interpolant(X, Y, Z, XI, YI, power=16.,\n",
117 | "# \tmodified=True, D=0.1, L=0.5)\n",
118 | "\n",
119 | "# Interpolating with barnes objective with different lengths scales\n",
120 | "barnes = {}\n",
121 | "# barnes['5'] = spi.barnes_objective(X, Y, Z, XI, YI, 0.05, 0.05)\n",
122 | "barnes['10'] = spi.barnes_objective(X, Y, Z, XI, YI, .10, 0.10)\n",
123 | "\n",
124 | "# Interpolating with griddata (linear, nearest, cubic)\n",
125 | "# interpolation points\n",
126 | "interp_points = list(zip(XI.flatten(), YI.flatten()))\n",
127 | "# linear_output = scipy.interpolate.griddata(list(zip(X, Y)), Z,\n",
128 | "# \tinterp_points, method='linear')\n",
129 | "cubic_output = scipy.interpolate.griddata(list(zip(X, Y)), Z,\n",
130 | "\tinterp_points, method='cubic')\n",
131 | "# interpolated data grid\n",
132 | "# linear = linear_output.reshape(ny,nx)\n",
133 | "cubic = cubic_output.reshape(ny,nx)"
134 | ]
135 | },
136 | {
137 | "cell_type": "markdown",
138 | "metadata": {},
139 | "source": [
140 | "### Create output plot showing interpolation and points"
141 | ]
142 | },
143 | {
144 | "cell_type": "code",
145 | "execution_count": null,
146 | "metadata": {},
147 | "outputs": [],
148 | "source": [
149 | "# plot data and interpolated data\n",
150 | "fig, ((ax1,ax2,ax3,ax4),(ax5,ax6,ax7,ax8)) = plt.subplots(num=1,\n",
151 | "\tncols=4, nrows=2, sharex=True, sharey=True, figsize=(12,6.5))\n",
152 | "extents=(0,1,1,0)\n",
153 | "\n",
154 | "# create color map with invalid points\n",
155 | "cmap = plt.cm.get_cmap('Spectral_r').copy()\n",
156 | "cmap.set_bad('w',0.)\n",
157 | "\n",
158 | "# plot real data\n",
159 | "ax1.imshow(ZI, interpolation='nearest', extent=extents, cmap=cmap,\n",
160 | "\tvmin=ZI.min(), vmax=ZI.max())\n",
161 | "ax1.scatter(X,Y,c=Z, s=10, cmap=cmap, zorder=2,\n",
162 | "\tvmin=ZI.min(), vmax=ZI.max(), edgecolors='w')\n",
163 | "\n",
164 | "# plot radial basis interpolated data\n",
165 | "ax2.imshow(radial, interpolation='nearest', extent=extents, cmap=cmap,\n",
166 | "\tvmin=ZI.min(), vmax=ZI.max())\n",
167 | "# plot real data\n",
168 | "ax2.scatter(X,Y,c=Z, s=10, cmap=cmap, zorder=2,\n",
169 | "\tvmin=ZI.min(), vmax=ZI.max(), edgecolors='none')\n",
170 | "\n",
171 | "# plot compact radial basis interpolated data\n",
172 | "ax3.imshow(wendland, interpolation='nearest', extent=extents, cmap=cmap,\n",
173 | "\tvmin=ZI.min(), vmax=ZI.max())\n",
174 | "# plot real data\n",
175 | "ax3.scatter(X,Y,c=Z, s=10, cmap=cmap, zorder=2,\n",
176 | "\tvmin=ZI.min(), vmax=ZI.max(), edgecolors='none')\n",
177 | "\n",
178 | "# plot Barnes objective interpolant data\n",
179 | "SMOOTH = '10'\n",
180 | "ax4.imshow(barnes[SMOOTH], interpolation='nearest', extent=extents,\n",
181 | "\tcmap=cmap, vmin=ZI.min(), vmax=ZI.max())\n",
182 | "# plot real data\n",
183 | "ax4.scatter(X,Y,c=Z, s=10, cmap=cmap, zorder=2,\n",
184 | "\tvmin=ZI.min(), vmax=ZI.max(), edgecolors='none')\n",
185 | "\n",
186 | "# plot biharmonic spline interpolated data\n",
187 | "tension = '10'\n",
188 | "ax5.imshow(spline[tension], interpolation='nearest', extent=extents,\n",
189 | "\tcmap=cmap, vmin=ZI.min(), vmax=ZI.max())\n",
190 | "# plot real data\n",
191 | "ax5.scatter(X,Y,c=Z, s=10, cmap=cmap, zorder=2,\n",
192 | "\tvmin=ZI.min(), vmax=ZI.max(), edgecolors='none')\n",
193 | "\n",
194 | "# plot regularized biharmonic spline interpolated data\n",
195 | "Rtension = 'R50'\n",
196 | "ax6.imshow(spline[Rtension], interpolation='nearest', extent=extents,\n",
197 | "\tcmap=cmap, vmin=ZI.min(), vmax=ZI.max())\n",
198 | "# plot real data\n",
199 | "ax6.scatter(X,Y,c=Z, s=10, cmap=cmap, zorder=2,\n",
200 | "\tvmin=ZI.min(), vmax=ZI.max(), edgecolors='none')\n",
201 | "\n",
202 | "# plot Shepard interpolant data\n",
203 | "power = '2'\n",
204 | "ax7.imshow(shepard[power], interpolation='nearest', extent=extents,\n",
205 | "\tcmap=cmap, vmin=ZI.min(), vmax=ZI.max())\n",
206 | "# plot real data\n",
207 | "ax7.scatter(X,Y,c=Z, s=10, cmap=cmap, zorder=2,\n",
208 | "\tvmin=ZI.min(), vmax=ZI.max(), edgecolors='none')\n",
209 | "\n",
210 | "# plot interpolated data from griddata\n",
211 | "ax8.imshow(cubic, interpolation='nearest', extent=extents, cmap=cmap,\n",
212 | "\tvmin=ZI.min(), vmax=ZI.max())\n",
213 | "# plot real data\n",
214 | "ax8.scatter(X,Y,c=Z, s=10, cmap=cmap, zorder=2,\n",
215 | "\tvmin=ZI.min(), vmax=ZI.max(), edgecolors='none')\n",
216 | "\n",
217 | "# no ticks on the x and y axes\n",
218 | "ax1.get_xaxis().set_ticks([]); ax1.get_yaxis().set_ticks([])\n",
219 | "ax2.get_xaxis().set_ticks([]); ax2.get_yaxis().set_ticks([])\n",
220 | "ax3.get_xaxis().set_ticks([]); ax3.get_yaxis().set_ticks([])\n",
221 | "ax4.get_xaxis().set_ticks([]); ax4.get_yaxis().set_ticks([])\n",
222 | "ax5.get_xaxis().set_ticks([]); ax5.get_yaxis().set_ticks([])\n",
223 | "ax6.get_xaxis().set_ticks([]); ax6.get_yaxis().set_ticks([])\n",
224 | "ax7.get_xaxis().set_ticks([]); ax7.get_yaxis().set_ticks([])\n",
225 | "ax8.get_xaxis().set_ticks([]); ax8.get_yaxis().set_ticks([])\n",
226 | "# set x and y limits\n",
227 | "ax1.set_xlim(0, 1)\n",
228 | "ax1.set_ylim(0, 1)\n",
229 | "# add titles\n",
230 | "ax1.set_title('True Franke Function')\n",
231 | "ax2.set_title('RBF Inverse Multiquadric')\n",
232 | "ax3.set_title('Compact RBF Wendland')\n",
233 | "ax4.set_title('Barnes Objective (L{0})'.format(SMOOTH))\n",
234 | "ax5.set_title('Biharmonic Spline (T{0})'.format(tension))\n",
235 | "ax6.set_title('Regular Biharmonic Spline ({0})'.format(Rtension))\n",
236 | "ax7.set_title('Shepard Interpolant (P{0})'.format(power))\n",
237 | "ax8.set_title('{0} with SciPy'.format('Cubic'))\n",
238 | "# subplot adjustments\n",
239 | "fig.subplots_adjust(left=0.02,right=0.98,bottom=0.02,top=0.95,\n",
240 | "\twspace=0.02,hspace=0.1)\n",
241 | "plt.show()"
242 | ]
243 | },
244 | {
245 | "cell_type": "code",
246 | "execution_count": null,
247 | "metadata": {},
248 | "outputs": [],
249 | "source": []
250 | }
251 | ],
252 | "metadata": {
253 | "kernelspec": {
254 | "display_name": "Python 3",
255 | "language": "python",
256 | "name": "python3"
257 | },
258 | "language_info": {
259 | "codemirror_mode": {
260 | "name": "ipython",
261 | "version": 3
262 | },
263 | "file_extension": ".py",
264 | "mimetype": "text/x-python",
265 | "name": "python",
266 | "nbconvert_exporter": "python",
267 | "pygments_lexer": "ipython3",
268 | "version": "3.10.12"
269 | }
270 | },
271 | "nbformat": 4,
272 | "nbformat_minor": 4
273 | }
274 |
--------------------------------------------------------------------------------
/spatial_interpolators/radial_basis.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | u"""
3 | radial_basis.py
4 | Written by Tyler Sutterley (05/2022)
5 |
6 | Interpolates data using radial basis functions
7 |
8 | CALLING SEQUENCE:
9 | ZI = radial_basis(xs, ys, zs, XI, YI, polynomial=0,
10 | smooth=smooth, epsilon=epsilon, method='inverse')
11 |
12 | INPUTS:
13 | xs: scaled input X data
14 | ys: scaled input Y data
15 | zs: input data
16 | XI: scaled grid X for output ZI
17 | YI: scaled grid Y for output ZI
18 |
19 | OUTPUTS:
20 | ZI: interpolated data grid
21 |
22 | OPTIONS:
23 | smooth: smoothing weights
24 | metric: distance metric to use (default euclidean)
25 | epsilon: adjustable constant for distance functions
26 | default is mean Euclidean distance
27 | polynomial: polynomial order if augmenting radial basis functions
28 | default None: no polynomials
29 | method: radial basis function
30 | multiquadric
31 | inverse_multiquadric or inverse (default)
32 | inverse_quadratic
33 | gaussian
34 | linear (first-order polyharmonic spline)
35 | cubic (third-order polyharmonic spline)
36 | quintic (fifth-order polyharmonic spline)
37 | thin_plate: thin-plate spline
38 |
39 | PYTHON DEPENDENCIES:
40 | numpy: Scientific Computing Tools For Python
41 | https://numpy.org
42 | scipy: Scientific Tools for Python
43 | https://docs.scipy.org/doc/
44 |
45 | REFERENCES:
46 | R. L. Hardy, Multiquadric equations of topography and other irregular
47 | surfaces, J. Geophys. Res., 76(8), 1905-1915, 1971.
48 | M. Buhmann, "Radial Basis Functions", Cambridge Monographs on Applied and
49 | Computational Mathematics, 2003.
50 |
51 | UPDATE HISTORY:
52 | Updated 05/2022: updated docstrings to numpy documentation format
53 | Updated 01/2022: added function docstrings
54 | Updated 07/2021: using scipy spatial distance routines
55 | Updated 09/2017: using rcond=-1 in numpy least-squares algorithms
56 | Updated 01/2017: epsilon in polyharmonic splines (linear, cubic, quintic)
57 | Updated 08/2016: using format text within ValueError, edit constant vector
58 | added low-order polynomial option (previously used default constant)
59 | Updated 01/2016: new hierarchical_radial_basis function
60 | that first reduces to points within distance. added cutoff option
61 | Updated 10/2014: added third dimension (spherical)
62 | Written 08/2014
63 | """
64 | from __future__ import print_function, division
65 | import numpy as np
66 | import scipy.spatial
67 |
68 | def radial_basis(xs, ys, zs, XI, YI, smooth=0.0, metric='euclidean',
69 | epsilon=None, method='inverse', polynomial=None):
70 | """
71 | Interpolates data using radial basis functions
72 |
73 | Parameters
74 | ----------
75 | xs: float
76 | scaled input x-coordinates
77 | ys: float
78 | scaled input y-coordinates
79 | zs: float
80 | input data
81 | XI: float
82 | scaled output x-coordinates for data grid
83 | YI: float
84 | scaled output y-coordinates for data grid
85 | smooth: float, default 0.0
86 | smoothing weights
87 | metric: str, default 'euclidean'
88 | distance metric to use
89 | epsilon: float or NoneType, default None
90 | adjustable constant for distance functions
91 | method: str, default 'inverse'
92 | radial basis function
93 |
94 | * ``'multiquadric'``
95 | * ``'inverse_multiquadric'`` or ``'inverse'``
96 | * ``'inverse_quadratic'``
97 | * ``'gaussian'``
98 | * ``'linear'``
99 | * ``'cubic'``
100 | * ``'quintic'``
101 | * ``'thin_plate'``
102 | polynomial: int or NoneType, default None
103 | polynomial order if augmenting radial basis functions
104 |
105 | Returns
106 | -------
107 | ZI: interpolated data grid
108 |
109 | References
110 | ----------
111 | .. [Hardy1971] R. L. Hardy,
112 | "Multiquadric equations of topography and other irregular surfaces,"
113 | *Journal of Geophysical Research*, 76(8), 1905-1915, (1971).
114 | `doi: 10.1029/JB076i008p01905
115 | `_
116 | .. [Buhmann2003] M. Buhmann, "Radial Basis Functions",
117 | *Cambridge Monographs on Applied and Computational Mathematics*,
118 | (2003).
119 | """
120 |
121 | # remove singleton dimensions
122 | xs = np.squeeze(xs)
123 | ys = np.squeeze(ys)
124 | zs = np.squeeze(zs)
125 | XI = np.squeeze(XI)
126 | YI = np.squeeze(YI)
127 | # size of new matrix
128 | if (np.ndim(XI) == 1):
129 | nx = len(XI)
130 | else:
131 | nx, ny = np.shape(XI)
132 |
133 | # Check to make sure sizes of input arguments are correct and consistent
134 | if (len(zs) != len(xs)) | (len(zs) != len(ys)):
135 | raise Exception('Length of input arrays must be equal')
136 | if (np.shape(XI) != np.shape(YI)):
137 | raise Exception('Size of output arrays must be equal')
138 |
139 | # create python dictionary of radial basis function formulas
140 | radial_basis_functions = {}
141 | radial_basis_functions['multiquadric'] = multiquadric
142 | radial_basis_functions['inverse_multiquadric'] = inverse_multiquadric
143 | radial_basis_functions['inverse'] = inverse_multiquadric
144 | radial_basis_functions['inverse_quadratic'] = inverse_quadratic
145 | radial_basis_functions['gaussian'] = gaussian
146 | radial_basis_functions['linear'] = poly_spline1
147 | radial_basis_functions['cubic'] = poly_spline3
148 | radial_basis_functions['quintic'] = poly_spline5
149 | radial_basis_functions['thin_plate'] = thin_plate
150 | # check if formula name is listed
151 | if method in radial_basis_functions.keys():
152 | RBF = radial_basis_functions[method]
153 | else:
154 | raise ValueError(f"Method {method} not implemented")
155 |
156 | # Creation of data distance matrix
157 | # Data to Data
158 | if (metric == 'brute'):
159 | # use linear algebra to compute euclidean distances
160 | Rd = distance_matrix(
161 | np.array([xs, ys]),
162 | np.array([xs, ys])
163 | )
164 | else:
165 | # use scipy spatial distance routines
166 | Rd = scipy.spatial.distance.cdist(
167 | np.array([xs, ys]).T,
168 | np.array([xs, ys]).T,
169 | metric=metric)
170 | # shape of distance matrix
171 | N, M = np.shape(Rd)
172 |
173 | # if epsilon is not specified
174 | if epsilon is None:
175 | # calculate norm with mean euclidean distance
176 | uix, uiy = np.nonzero(np.tri(N, M=M, k=-1))
177 | epsilon = np.mean(Rd[uix, uiy])
178 |
179 | # possible augmentation of the PHI Matrix with polynomial Vectors
180 | if polynomial is None:
181 | # calculate radial basis function for data-to-data with smoothing
182 | PHI = RBF(epsilon, Rd) + np.eye(N, M=M)*smooth
183 | DMAT = zs.copy()
184 | else:
185 | # number of polynomial coefficients
186 | nt = (polynomial**2 + 3*polynomial)//2 + 1
187 | # calculate radial basis function for data-to-data with smoothing
188 | PHI = np.zeros((N+nt, M+nt))
189 | PHI[:N, :M] = RBF(epsilon, Rd) + np.eye(N, M=M)*smooth
190 | # augmentation of PHI matrix with polynomials
191 | POLY = polynomial_matrix(xs, ys, polynomial)
192 | DMAT = np.concatenate(([zs, np.zeros((nt))]), axis=0)
193 | # augment PHI matrix
194 | for t in range(nt):
195 | PHI[:N, M+t] = POLY[:, t]
196 | PHI[N+t, :M] = POLY[:, t]
197 |
198 | # Computation of the Weights
199 | w = np.linalg.lstsq(PHI, DMAT[:, np.newaxis], rcond=-1)[0]
200 |
201 | # Computation of distance Matrix
202 | # Computation of distance Matrix (data to mesh points)
203 | if (metric == 'brute'):
204 | # use linear algebra to compute euclidean distances
205 | Re = distance_matrix(
206 | np.array([XI.flatten(), YI.flatten()]),
207 | np.array([xs, ys])
208 | )
209 | else:
210 | # use scipy spatial distance routines
211 | Re = scipy.spatial.distance.cdist(
212 | np.array([XI.flatten(), YI.flatten()]).T,
213 | np.array([xs, ys]).T,
214 | metric=metric)
215 | # calculate radial basis function for data-to-mesh matrix
216 | E = RBF(epsilon, Re)
217 |
218 | # possible augmentation of the Evaluation Matrix with polynomial vectors
219 | if polynomial is not None:
220 | P = polynomial_matrix(XI.flatten(), YI.flatten(), polynomial)
221 | E = np.concatenate(([E, P]), axis=1)
222 | # calculate output interpolated array (or matrix)
223 | if (np.ndim(XI) == 1):
224 | ZI = np.squeeze(np.dot(E, w))
225 | else:
226 | ZI = np.zeros((nx, ny))
227 | ZI[:, :] = np.dot(E, w).reshape(nx, ny)
228 | # return the interpolated array (or matrix)
229 | return ZI
230 |
231 | # define radial basis function formulas
232 | def multiquadric(epsilon, r):
233 | # multiquadratic
234 | f = np.sqrt((epsilon*r)**2 + 1.0)
235 | return f
236 |
237 | def inverse_multiquadric(epsilon, r):
238 | # inverse multiquadratic
239 | f = 1.0/np.sqrt((epsilon*r)**2 + 1.0)
240 | return f
241 |
242 | def inverse_quadratic(epsilon, r):
243 | # inverse quadratic
244 | f = 1.0/(1.0+(epsilon*r)**2)
245 | return f
246 |
247 | def gaussian(epsilon, r):
248 | # gaussian
249 | f = np.exp(-(epsilon*r)**2)
250 | return f
251 |
252 | def poly_spline1(epsilon, r):
253 | # First-order polyharmonic spline
254 | f = (epsilon*r)
255 | return f
256 |
257 | def poly_spline3(epsilon, r):
258 | # Third-order polyharmonic spline
259 | f = (epsilon*r)**3
260 | return f
261 |
262 | def poly_spline5(epsilon, r):
263 | # Fifth-order polyharmonic spline
264 | f = (epsilon*r)**5
265 | return f
266 |
267 | def thin_plate(epsilon, r):
268 | # thin plate spline
269 | f = r**2 * np.log(r)
270 | # the spline is zero at zero
271 | f[r == 0] = 0.0
272 | return f
273 |
274 | # calculate Euclidean distances between points as matrices
275 | def distance_matrix(x, cntrs):
276 | s, M = np.shape(x)
277 | s, N = np.shape(cntrs)
278 | D = np.zeros((M, N))
279 | for d in range(s):
280 | ii, = np.dot(d, np.ones((1, N))).astype(np.int64)
281 | jj, = np.dot(d, np.ones((1, M))).astype(np.int64)
282 | dx = x[ii, :].T - cntrs[jj, :]
283 | D += dx**2
284 | D = np.sqrt(D)
285 | return D
286 |
287 | # calculate polynomial matrix to augment radial basis functions
288 | def polynomial_matrix(x, y, order):
289 | c = 0
290 | M = len(x)
291 | N = (order**2 + 3*order)//2 + 1
292 | POLY = np.zeros((M, N))
293 | for ii in range(order + 1):
294 | for jj in range(ii + 1):
295 | POLY[:, c] = (x**jj)*(y**(ii-jj))
296 | c += 1
297 | return POLY
298 |
--------------------------------------------------------------------------------
/spatial_interpolators/sph_spline.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | u"""
3 | sph_spline.py
4 | Written by Tyler Sutterley (05/2022)
5 |
6 | Interpolates a sparse grid over a sphere using spherical surface splines in
7 | tension following Wessel and Becker (2008)
8 | Adapted from P. Wessel, SOEST, U of Hawaii, April 2008
9 | Uses Generalized Legendre Function algorithm from Spanier and Oldman
10 | "An Atlas of Functions", 1987
11 |
12 | CALLING SEQUENCE:
13 | output = sph_spline(lon, lat, data, longitude, latitude, tension=0)
14 |
15 | INPUTS:
16 | lon: input longitude
17 | lat: input latitude
18 | data: input data
19 | longitude: output longitude
20 | latitude: output latitude
21 |
22 | OUTPUTS:
23 | output: interpolated data
24 |
25 | OPTIONS:
26 | tension: tension to use in interpolation (greater than 0)
27 |
28 | PYTHON DEPENDENCIES:
29 | numpy: Scientific Computing Tools For Python
30 | https://numpy.org
31 | scipy: Scientific Tools for Python
32 | https://docs.scipy.org/doc/
33 | cython: C-extensions for Python
34 | http://cython.org/
35 |
36 | REFERENCES:
37 | Wessel, P. and J. M. Becker, 2008, Interpolation using a generalized
38 | Green's function for a spherical surface spline in tension,
39 | Geophysical Journal International, doi:10.1111/j.1365-246X.2008.03829.x
40 |
41 | UPDATE HISTORY:
42 | Updated 05/2022: updated docstrings to numpy documentation format
43 | Updated 01/2022: added function docstrings
44 | Updated 09/2017: using rcond=-1 in numpy least-squares algorithms
45 | Updated 08/2016: using cythonized version of generalized Legendre function
46 | treat case for no tension but x is equal to 1 within machine precision
47 | Written 08/2016
48 | """
49 | import numpy as np
50 | import scipy.special
51 | from spatial_interpolators.PvQv_C import PvQv_C
52 |
53 | def sph_spline(lon, lat, data, longitude, latitude, tension=0.):
54 | """
55 | Interpolates a sparse grid over a sphere using spherical
56 | surface splines in tension
57 |
58 | Parameters
59 | ----------
60 | lon: float
61 | input longitude
62 | lat: float
63 | input latitude
64 | data: float
65 | input data
66 | longitude: float
67 | output longitude
68 | latitude: float
69 | output latitude
70 | tension: float, default 0.0
71 | tension to use in interpolation
72 |
73 | Returns
74 | -------
75 | output: float
76 | interpolated data grid
77 |
78 | References
79 | ----------
80 | .. [Wessel2008] P. Wessel, and J. M. Becker,
81 | "Interpolation using a generalized Green's function for a
82 | spherical surface spline in tension,"
83 | *Geophysical Journal International*, 174(1), 21--28, (2008).
84 | `doi: 10.1111/j.1365-246X.2008.03829.x
85 | `_
86 | """
87 |
88 | # remove singleton dimensions
89 | lon = np.squeeze(lon)
90 | lat = np.squeeze(lat)
91 | data = np.squeeze(data)
92 | longitude = np.squeeze(longitude)
93 | latitude = np.squeeze(latitude)
94 | # size of new matrix
95 | if (np.ndim(longitude) > 1):
96 | nlon, nlat = np.shape(longitude)
97 |
98 | # Check to make sure sizes of input arguments are correct and consistent
99 | if (len(data) != len(lon)) | (len(data) != len(lat)):
100 | raise Exception('Length of input arrays must be equal')
101 | if (np.shape(longitude) != np.shape(latitude)):
102 | raise Exception('Size of output Longitude and Latitude must be equal')
103 | if (tension < 0):
104 | raise ValueError('tension must be greater than 0')
105 |
106 | # convert input lat and lon into cartesian X,Y,Z over unit sphere
107 | phi = np.pi*lon/180.0
108 | th = np.pi*(90.0 - lat)/180.0
109 | xs = np.sin(th)*np.cos(phi)
110 | ys = np.sin(th)*np.sin(phi)
111 | zs = np.cos(th)
112 | # convert output longitude and latitude into cartesian X,Y,Z
113 | PHI = np.pi*longitude.flatten()/180.0
114 | THETA = np.pi*(90.0 - latitude.flatten())/180.0
115 | XI = np.sin(THETA)*np.cos(PHI)
116 | YI = np.sin(THETA)*np.sin(PHI)
117 | ZI = np.cos(THETA)
118 | sz = len(longitude.flatten())
119 |
120 | # Find and remove mean from data
121 | data_mean = data.mean()
122 | data_range = data.max() - data.min()
123 | # Normalize data
124 | data_norm = (data - data_mean) / data_range
125 |
126 | # compute linear system
127 | N = len(data)
128 | GG = np.zeros((N, N))
129 | for i in range(N):
130 | Rd = np.dot(np.c_[xs, ys, zs], np.array([xs[i], ys[i], zs[i]]))
131 | # remove singleton dimensions and calculate spherical surface splines
132 | GG[i, :] = SSST(Rd, P=tension)
133 |
134 | # Compute model m for normalized data
135 | m = np.linalg.lstsq(GG, data_norm, rcond=-1)[0]
136 |
137 | # calculate output interpolated array (or matrix)
138 | output = np.zeros((sz))
139 | for j in range(sz):
140 | Re = np.dot(np.c_[xs, ys, zs], np.array([XI[j], YI[j], ZI[j]]))
141 | # remove singleton dimensions and calculate spherical surface splines
142 | gg = SSST(Re, P=tension)
143 | output[j] = data_mean + data_range*np.dot(gg, m)
144 |
145 | # reshape output to original dimensions and return
146 | if (np.ndim(longitude) > 1):
147 | output = output.reshape(nlon, nlat)
148 |
149 | return output
150 |
151 | # SSST: Spherical Surface Spline in Tension
152 | # Returns the Green's function for a spherical surface spline in tension,
153 | # following Wessel and Becker [2008].
154 | # If p == 0 or not given then use minimum curvature solution with dilogarithm
155 | def SSST(x, P=0):
156 | """
157 | Calculates the Green's function for a
158 | spherical surface spline in tension
159 |
160 | Parameters
161 | ----------
162 | x: float
163 | distance between points
164 | P: float or int, default 0
165 | Tension parameter
166 |
167 | Returns
168 | -------
169 | y: float
170 | Green's function
171 | """
172 | # floating point machine precision
173 | eps = np.finfo(np.float64).eps
174 | if (P == 0):
175 | # use dilogarithm (Spence's function) if using splines without tension
176 | y = np.zeros_like(x)
177 | if np.any(np.abs(x) < (1.0 - eps)):
178 | k, = np.nonzero(np.abs(x) < (1.0 - eps))
179 | y[k] = scipy.special.spence(0.5 - 0.5*x[k])
180 | # Deal with special cases x == +/- 1
181 | if np.any(((x + eps) >= 1.0) | ((x - eps) <= -1.0)):
182 | k, = np.nonzero(((x + eps) >= 1.0) | ((x - eps) <= -1.0))
183 | y[k] = scipy.special.spence(0.5 - 0.5*np.sign(x[k]))
184 | else:
185 | # if in tension
186 | # calculate tension parameter
187 | v = (-1.0 + np.lib.scimath.sqrt(1.0 - 4.0*P**2))/2.0
188 | # Initialize output array
189 | y = np.zeros_like(x, dtype=v.dtype)
190 | A = np.pi/np.sin(v*np.pi)
191 | # Where Pv solution works
192 | if np.any(np.abs(x) < (1.0 - eps)):
193 | k, = np.nonzero(np.abs(x) < (1.0 - eps))
194 | y[k] = A*Pv(-x[k], v) - np.log(1.0 - x[k])
195 | # Approximations where x is close to -1 or 1 using values from
196 | # "An Atlas of Functions" by Spanier and Oldham, 1987 (590)
197 | # Deal with special case x == -1
198 | if np.any((x - eps) <= -1.0):
199 | k, = np.nonzero((x - eps) <= -1.0)
200 | y[k] = A - np.log(2.0)
201 | # Deal with special case x == +1
202 | if np.any((x + eps) >= 1.0):
203 | k, = np.nonzero((x + eps) >= 1.0)
204 | y[k] = np.pi*(1.0/np.tan(v*np.pi)) + 2.0*(np.euler_gamma +
205 | scipy.special.psi(1.0+v)) - np.log(2.0)
206 | # use only the real part (remove insignificant imaginary noise)
207 | y = np.real(y)
208 | # return the Green's function
209 | return y
210 |
211 | # Calculate Legendre function of the first kind for arbitrary degree v
212 | def Pv(x, v, method=PvQv_C):
213 | """Calculate Legendre function of the first kind
214 | """
215 | P = np.zeros_like(x, dtype=v.dtype)
216 | for i, val in enumerate(x):
217 | if (val == -1):
218 | p = np.inf
219 | else:
220 | # use compiled Cython version of PvQv as default
221 | p, q, k = method(val, v)
222 | P[i] = p
223 | return P
224 |
225 | # Calculate generalized Legendre functions of arbitrary degree v
226 | # Based on recipe in "An Atlas of Functions" by Spanier and Oldham, 1987 (589)
227 | # Pv is the Legendre function of the first kind
228 | # Qv is the Legendre function of the second kind
229 | def PvQv(x, v):
230 | """Calculate Generalized Legendre function of arbitrary degree
231 | """
232 | iter = 0
233 | if (x == -1):
234 | P = -np.inf
235 | Q = -np.inf
236 | elif (x == +1):
237 | P = 1.0
238 | Q = np.inf
239 | else:
240 | # set a and R to 1
241 | a = 1.0
242 | R = 1.0
243 | K = 4.0*np.sqrt(np.abs(v - v**2))
244 | if ((np.abs(1 + v) + np.floor(1 + v.real)) == 0):
245 | a = 1.0e99
246 | v = -1.0 - v
247 | # s and c = sin and cos of (pi*v/2.0)
248 | s = np.sin(0.5*np.pi*v)
249 | c = np.cos(0.5*np.pi*v)
250 | w = (0.5 + v)**2
251 | # if v is less than or equal to six (repeat until greater than six)
252 | while (v.real <= 6.0):
253 | v += 2
254 | R = R*(v - 1.0)/v
255 | # calculate X and g and update R
256 | X = 1.0 / (4.0 + 4.0*v)
257 | g = 1.0 + 5*X*(1.0 - 3.0*X*(0.35 + 6.1*X))
258 | R = R*(1.0 - X*(1.0 - g*X/2))/np.sqrt(8.0*X)
259 | # set g and u to 2.0*x
260 | g = 2.0*x
261 | u = 2.0*x
262 | # set f and t to 1
263 | f = 1.0
264 | t = 1.0
265 | # set k to 1/2
266 | k = 0.5
267 | # calculate new X
268 | X = 1.0 + (1e8/(1.0 - x**2))
269 | # update t
270 | t = t*x**2 * (k**2.0 - w)/((k + 1.0)**2 - 0.25)
271 | # add 1 to k
272 | k += 1.0
273 | # add t to f
274 | f += t
275 | # update u
276 | u = u*x**2 * (k**2 - w)/((k + 1)**2 - 0.25)
277 | # add 1 to k
278 | k += 1.0
279 | # add u to g
280 | g += u
281 | # if k is less than K and |Xt| is greater than |f|
282 | # repeat previous set of operations until valid
283 | while ((k < K) | (np.abs(X*t) > np.abs(f))):
284 | iter += 1
285 | t = t*x**2 * (k**2.0 - w) / ((k + 1.0)**2 - 0.25)
286 | k += 1.0
287 | f += t
288 | u = u*x**2 * (k**2.0 - w) / ((k + 1.0)**2 - 0.25)
289 | k += 1.0
290 | g += u
291 | # update f and g
292 | f += (x**2*t/(1.0 - x**2))
293 | g += (x**2*u/(1.0 - x**2))
294 | # calculate generalized Legendre functions
295 | P = ((s*g*R) + (c*f/R))/np.sqrt(np.pi)
296 | Q = a*np.sqrt(np.pi)*((c*g*R) - (s*f/R))/2.0
297 | # return P, Q and number of iterations
298 | return (P, Q, iter)
299 |
--------------------------------------------------------------------------------
/spatial_interpolators/biharmonic_spline.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | u"""
3 | biharmonic_spline.py
4 | Written by Tyler Sutterley (05/2022)
5 |
6 | Interpolates data using 2D biharmonic splines (Sandwell, 1987)
7 | With or without tension parameters (Wessel and Bercovici, 1998)
8 | or using the regularized function of Mitasova and Mitas 1993
9 |
10 | CALLING SEQUENCE:
11 | ZI = biharmonic_spline(xs, ys, zs, XI, YI)
12 |
13 | INPUTS:
14 | xs: input X data
15 | ys: input Y data
16 | zs: input data
17 | XI: grid X for output ZI
18 | YI: grid Y for output ZI
19 |
20 | OUTPUTS:
21 | ZI: interpolated grid
22 |
23 | OPTIONS:
24 | metric: distance metric to use (default euclidean)
25 | tension: tension to use in interpolation (between 0 and 1)
26 | regular: use regularized function of Mitasova and Mitas
27 | eps: minimum distance value for valid points (default 1e-7)
28 | scale: scale factor for normalized lengths (default 2e-2)
29 |
30 | PYTHON DEPENDENCIES:
31 | numpy: Scientific Computing Tools For Python
32 | https://numpy.org
33 | scipy: Scientific Tools for Python
34 | https://docs.scipy.org/doc/
35 |
36 | REFERENCES:
37 | Sandwell, Biharmonic spline interpolation of GEOS-3 and SEASAT
38 | altimeter data, Geophysical Research Letters, 14(2), (1987)
39 | Wessel and Bercovici, Interpolation with Splines in Tension: A
40 | Green's Function Approach, Mathematical Geology, 30(1), (1998)
41 | Mitasova and Mitas, Mathematical Geology, 25(6), (1993)
42 |
43 | UPDATE HISTORY:
44 | Updated 05/2022: updated docstrings to numpy documentation format
45 | Updated 01/2022: added function docstrings
46 | update regularized spline function to use arrays
47 | Updated 07/2021: using scipy spatial distance routines
48 | Updated 09/2017: use rcond=-1 in numpy least-squares algorithms
49 | Updated 08/2016: detrend input data and retrend output data. calculate c
50 | added regularized function of Mitasova and Mitas
51 | Updated 06/2016: added TENSION parameter (Wessel and Bercovici, 1998)
52 | Written 06/2016
53 | """
54 | import numpy as np
55 | import scipy.spatial
56 |
57 | def biharmonic_spline(xs, ys, zs, XI, YI, metric='euclidean',
58 | tension=0, regular=False, eps=1e-7, scale=0.02):
59 | r"""
60 | Interpolates a sparse grid using 2D biharmonic splines
61 | with or without tension parameters or regularized functions
62 |
63 | Parameters
64 | ----------
65 | xs: float
66 | input x-coordinates
67 | ys: float
68 | input y-coordinates
69 | zs: float
70 | input data
71 | XI: float
72 | output x-coordinates for data grid
73 | YI: float
74 | output y-coordinates for data grid
75 | metric: str, default 'euclidean'
76 | distance metric to use
77 | tension: float, default 0
78 | tension to use in interpolation
79 | value must be between 0 and 1
80 | regular: bool, default False
81 | Use regularized function of Mitasova and Mitas
82 | eps: float, default 1e-7
83 | minimum distance value for valid points
84 | scale: float, default 2e-2
85 | scale factor for normalized lengths
86 |
87 | Returns
88 | -------
89 | ZI: float
90 | interpolated data grid
91 |
92 | References
93 | ----------
94 | .. [Sandwell1987] D. T. Sandwell, "Biharmonic spline
95 | interpolation of GEOS-3 and SEASAT altimeter data",
96 | *Geophysical Research Letters*, 14(2), 139--142 (1987).
97 | `doi: 10.1029/GL014i002p00139
98 | `_
99 | .. [Wessel1998] P. Wessel and D. Bercovici, "Interpolation
100 | with Splines in Tension: A Green's Function Approach",
101 | *Mathematical Geology*, 30(1), 77--93 (1998).
102 | `doi: 10.1023/A:1021713421882
103 | `_
104 | .. [Mitasova1993] H. Mit\ |aacute|\ |scaron|\ ov\ |aacute| and
105 | L. Mit\ |aacute|\ |scaron|\, "Interpolation by regularized
106 | spline with tension: I. Theory and implementation",
107 | *Mathematical Geology*, 25(6), 641--655, (1993).
108 | `doi: 10.1007/BF00893171 `_
109 |
110 | .. |aacute| unicode:: U+00E1 .. LATIN SMALL LETTER A WITH ACUTE
111 | .. |scaron| unicode:: U+0161 .. LATIN SMALL LETTER S WITH CARON
112 | """
113 | # remove singleton dimensions
114 | xs = np.squeeze(xs)
115 | ys = np.squeeze(ys)
116 | zs = np.squeeze(zs)
117 | XI = np.squeeze(XI)
118 | YI = np.squeeze(YI)
119 | # size of new matrix
120 | if (np.ndim(XI) == 1):
121 | nx = len(XI)
122 | else:
123 | nx, ny = np.shape(XI)
124 |
125 | # Check to make sure sizes of input arguments are correct and consistent
126 | if (len(zs) != len(xs)) | (len(zs) != len(ys)):
127 | raise Exception('Length of input arrays must be equal')
128 | if (np.shape(XI) != np.shape(YI)):
129 | raise Exception('Size of output arrays must be equal')
130 | if (tension < 0) or (tension >= 1):
131 | raise ValueError('tension must be greater than 0 and less than 1')
132 |
133 | # Compute GG matrix for GG*m = d inversion problem
134 | npts = len(zs)
135 | GG = np.zeros((npts, npts))
136 | # Computation of distance Matrix (data to data)
137 | if (metric == 'brute'):
138 | # use linear algebra to compute euclidean distances
139 | Rd = distance_matrix(
140 | np.array([xs, ys]),
141 | np.array([xs, ys])
142 | )
143 | else:
144 | # use scipy spatial distance routines
145 | Rd = scipy.spatial.distance.cdist(
146 | np.array([xs, ys]).T,
147 | np.array([xs, ys]).T,
148 | metric=metric)
149 | # Calculate length scale for regularized case (Mitasova and Mitas)
150 | length_scale = np.sqrt((XI.max() - XI.min())**2 + (YI.max() - YI.min())**2)
151 | # calculate Green's function for valid points (with or without tension)
152 | ii, jj = np.nonzero(Rd >= eps)
153 | if (tension == 0):
154 | GG[ii, jj] = (Rd[ii, jj]**2) * (np.log(Rd[ii, jj]) - 1.0)
155 | elif regular:
156 | GG[ii, jj] = regular_spline2D(Rd[ii, jj], tension, scale*length_scale)
157 | else:
158 | GG[ii, jj] = green_spline2D(Rd[ii, jj], tension)
159 | # detrend dataset
160 | z0, r0, p = detrend2D(xs, ys, zs)
161 | # Compute model m for detrended data
162 | m = np.linalg.lstsq(GG, z0, rcond=-1)[0]
163 |
164 | # Computation of distance Matrix (data to mesh points)
165 | if (metric == 'brute'):
166 | # use linear algebra to compute euclidean distances
167 | Re = distance_matrix(
168 | np.array([XI.flatten(), YI.flatten()]),
169 | np.array([xs, ys])
170 | )
171 | else:
172 | # use scipy spatial distance routines
173 | Re = scipy.spatial.distance.cdist(
174 | np.array([XI.flatten(), YI.flatten()]).T,
175 | np.array([xs, ys]).T,
176 | metric=metric)
177 | gg = np.zeros_like(Re)
178 | # calculate Green's function for valid points (with or without tension)
179 | ii, jj = np.nonzero(Re >= eps)
180 | if (tension == 0):
181 | gg[ii, jj] = (Re[ii, jj]**2) * (np.log(Re[ii, jj]) - 1.0)
182 | elif regular:
183 | gg[ii, jj] = regular_spline2D(Re[ii, jj], tension, scale*length_scale)
184 | else:
185 | gg[ii, jj] = green_spline2D(Re[ii, jj], tension)
186 |
187 | # Find 2D interpolated surface through irregular/regular X, Y grid points
188 | if (np.ndim(XI) == 1):
189 | ZI = np.squeeze(np.dot(gg, m))
190 | else:
191 | ZI = np.zeros((nx, ny))
192 | ZI[:, :] = np.dot(gg, m).reshape(nx, ny)
193 | # return output matrix after retrending
194 | return (ZI + r0[2]) + (XI-r0[0])*p[0] + (YI-r0[1])*p[1]
195 |
196 | # Removing mean and slope in 2-D dataset
197 | # http://www.soest.hawaii.edu/wessel/tspline/
198 | def detrend2D(xi, yi, zi):
199 | # Find mean values
200 | r0 = np.zeros((3))
201 | r0[0] = xi.mean()
202 | r0[1] = yi.mean()
203 | r0[2] = zi.mean()
204 | # Extract mean values from X, Y and Z
205 | x0 = xi - r0[0]
206 | y0 = yi - r0[1]
207 | z0 = zi - r0[2]
208 | # Find slope parameters
209 | p = np.linalg.lstsq(np.transpose([x0, y0]), z0, rcond=-1)[0]
210 | # Extract slope from data
211 | z0 = z0 - x0*p[0] - y0*p[1]
212 | # return the detrended value, the mean values, and the slope parameters
213 | return (z0, r0, p)
214 |
215 | # calculate Euclidean distances between points as matrices
216 | def distance_matrix(x, cntrs):
217 | s, M = np.shape(x)
218 | s, N = np.shape(cntrs)
219 | D = np.zeros((M, N))
220 | for d in range(s):
221 | ii, = np.dot(d, np.ones((1, N))).astype(np.int64)
222 | jj, = np.dot(d, np.ones((1, M))).astype(np.int64)
223 | dx = x[ii, :].transpose() - cntrs[jj, :]
224 | D += dx**2
225 | D = np.sqrt(D)
226 | return D
227 |
228 | # Green function for 2-D spline in tension (Wessel et al, 1998)
229 | # http://www.soest.hawaii.edu/wessel/tspline/
230 | def green_spline2D(x, t):
231 | # in tension: G(u) = G(u) - log(u)
232 | # where u = c * x and c = sqrt (t/(1-t))
233 | c = np.sqrt(t/(1.0 - t))
234 | # allocate for output Green's function
235 | G = np.zeros_like(x)
236 | # inverse of tension parameter
237 | inv_c = 1.0/c
238 | # log(2) - 0.5772156
239 | g0 = np.log(2) - np.euler_gamma
240 | # find points below (or equal to) 2 times inverse tension parameter
241 | ii, = np.nonzero(x <= (2.0*inv_c))
242 | u = c*x[ii]
243 | y = (0.5*u)**2
244 | z = (u/3.75)**2
245 | # Green's function for points ii (less than or equal to 2.0*c)
246 | # from modified Bessel function of order zero
247 | G[ii] = (-np.log(0.5*u) *
248 | (z * (3.5156229 + z *
249 | (3.0899424 + z *
250 | (1.2067492 + z *
251 | (0.2659732 + z *
252 | (0.360768e-1 + z*0.45813e-2))))))) + \
253 | (y *
254 | (0.42278420 + y *
255 | (0.23069756 + y *
256 | (0.3488590e-1 + y *
257 | (0.262698e-2 + y *
258 | (0.10750e-3 + y * 0.74e-5))))))
259 | # find points above 2 times inverse tension parameter
260 | ii, = np.nonzero(x > 2.0*inv_c)
261 | y = 2.0*inv_c/x[ii]
262 | u = c*x[ii]
263 | # Green's function for points ii (greater than 2.0*c)
264 | G[ii] = (np.exp(-u)/np.sqrt(u)) * \
265 | (1.25331414 + y *
266 | (-0.7832358e-1 + y *
267 | (0.2189568e-1 + y *
268 | (-0.1062446e-1 + y *
269 | (0.587872e-2 + y *
270 | (-0.251540e-2 + y * 0.53208e-3)))))) + \
271 | np.log(u) - g0
272 | return G
273 |
274 | # Regularized spline in tension (Mitasova and Mitas, 1993)
275 | def regular_spline2D(r, t, l):
276 | # calculate tension parameter
277 | p = np.sqrt(t/(1.0 - t))/l
278 | z = (0.5 * p * r)**2
279 | # allocate for output Green's function
280 | G = np.zeros_like(r)
281 | # Green's function for points A (less than or equal to 1)
282 | A, = np.nonzero(z <= 1.0)
283 | Pa = [0.0, 0.99999193, -0.24991055, 0.05519968, -0.00976004, 0.00107857]
284 | G[A] = polynomial_sum(Pa, z[A])
285 | # Green's function for points B (greater than 1)
286 | B, = np.nonzero(z > 1.0)
287 | Pn = [0.2677737343, 8.6347608925, 18.0590169730, 8.5733287401, 1]
288 | Pd = [3.9584869228, 21.0996530827, 25.6329561486, 9.5733223454, 1]
289 | En = polynomial_sum(Pn, z[B])
290 | Ed = polynomial_sum(Pd, z[B])
291 | G[B] = np.log(z[B]) + np.euler_gamma + (En/Ed)/(z[B]*np.exp(z[B]))
292 | return G
293 |
294 | # calculate the sum of a polynomial function of a variable
295 | def polynomial_sum(x1, x2):
296 | """
297 | Calculates the sum of a polynomial function of a variable
298 |
299 | Arguments
300 | ---------
301 | x1: leading coefficient of polynomials of increasing order
302 | x2: coefficients to be raised by polynomials
303 | """
304 | # convert variable to array if importing a single value
305 | x2 = np.atleast_1d(x2)
306 | return np.sum([c * (x2 ** i) for i,c in enumerate(x1)], axis=0)
307 |
--------------------------------------------------------------------------------
/spatial_interpolators/sph_radial_basis.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | u"""
3 | sph_radial_basis.py
4 | Written by Tyler Sutterley (05/2022)
5 |
6 | Interpolates data over a sphere using radial basis functions
7 | with QR factorization option to eliminate ill-conditioning
8 |
9 | CALLING SEQUENCE:
10 | output = sph_radial_basis(lon, lat, data, longitude, latitude,
11 | smooth=smooth, epsilon=epsilon, method='inverse')
12 |
13 | INPUTS:
14 | lon: input longitude
15 | lat: input latitude
16 | data: input data
17 | longitude: output longitude
18 | latitude: output latitude
19 |
20 | OUTPUTS:
21 | output: interpolated data
22 |
23 | OPTIONS:
24 | smooth: smoothing weights
25 | epsilon: adjustable constant for distance functions
26 | default is the mean Euclidean distance
27 | method: radial basis function (** has option for QR factorization method)
28 | multiquadric**
29 | inverse_multiquadric** or inverse** (default)
30 | inverse_quadratic**
31 | gaussian**
32 | linear
33 | cubic
34 | quintic
35 | thin_plate: thin-plate spline
36 | QR: use QR factorization algorithm of Fornberg (2007)
37 | norm: distance function for radial basis functions (if not using QR)
38 | euclidean: Euclidean Distance with distance_matrix (default)
39 | GCD: Great-Circle Distance using n-vectors with angle_matrix
40 |
41 | PYTHON DEPENDENCIES:
42 | numpy: Scientific Computing Tools For Python
43 | https://numpy.org
44 | scipy: Scientific Tools for Python
45 | https://docs.scipy.org/doc/
46 |
47 | REFERENCES:
48 | B Fornberg and C Piret, "A stable algorithm for flat radial basis functions
49 | on a sphere." SIAM J. Sci. Comput. 30(1), 60-80 (2007)
50 | B Fornberg, E Larsson, and N Flyer, "Stable Computations with Gaussian
51 | Radial Basis Functions." SIAM J. Sci. Comput. 33(2), 869-892 (2011)
52 |
53 | UPDATE HISTORY:
54 | Updated 05/2022: updated docstrings to numpy documentation format
55 | Updated 01/2022: added function docstrings
56 | Updated 02/2019: compatibility updates for python3
57 | Updated 09/2017: using rcond=-1 in numpy least-squares algorithms
58 | Updated 08/2016: finished QR factorization method, added norm option
59 | Forked 08/2016 from radial_basis.py for use over a sphere
60 | Updated 08/2016: using format text within ValueError, edit constant vector
61 | removed 3 dimensional option of radial basis (spherical)
62 | changed hierarchical_radial_basis to compact_radial_basis using
63 | compactly-supported radial basis functions and sparse matrices
64 | added low-order polynomial option (previously used default constant)
65 | Updated 01/2016: new hierarchical_radial_basis function
66 | that first reduces to points within distance. added cutoff option
67 | Updated 10/2014: added third dimension (spherical)
68 | Written 08/2014
69 | """
70 | from __future__ import print_function, division
71 | import numpy as np
72 | import scipy.special as spc
73 | from spatial_interpolators.legendre import legendre
74 |
75 | def sph_radial_basis(lon, lat, data, longitude, latitude, smooth=0.,
76 | epsilon=None, method='inverse', QR=False, norm='euclidean'):
77 | """
78 | Interpolates a sparse grid over a sphere using radial basis
79 | functions with QR factorization option
80 |
81 | Parameters
82 | ----------
83 | lon: float
84 | input longitude
85 | lat: float
86 | input latitude
87 | data: float
88 | input data
89 | longitude: float
90 | output longitude
91 | latitude: float
92 | output latitude
93 | smooth: float, default 0.0
94 | smoothing weights
95 | epsilon: float or NoneType, default None
96 | adjustable constant for distance functions
97 | method: str, default 'inverse'
98 | compactly supported radial basis function
99 |
100 | * ``'multiquadric'`` [#f1]_
101 | * ``'inverse_multiquadric'`` [#f1]_ or ``'inverse'`` [#f1]_
102 | * ``'inverse_quadratic'`` [#f1]_
103 | * ``'gaussian'`` [#f1]_
104 | * ``'linear'``
105 | * ``'cubic'``
106 | * ``'quintic'``
107 | * ``'thin_plate'``
108 |
109 | QR: bool, default False
110 | use QR factorization algorithm of [Fornsberg2007]_
111 | norm: str, default 'euclidean'
112 | Distance function for radial basis functions
113 |
114 | * ``'euclidean'``: Euclidean Distance with distance_matrix
115 | * ``'GCD'``: Great-Circle Distance using n-vectors with angle_matrix
116 |
117 | Returns
118 | -------
119 | output: float
120 | interpolated data grid
121 |
122 | References
123 | ----------
124 | .. [Fornsberg2007] B. Fornberg and C. Piret,
125 | "A stable algorithm for flat radial basis functions on a sphere,"
126 | *SIAM Journal on Scientific Computing*, 30(1), 60--80, (2007).
127 | `doi: 10.1137/060671991 `_
128 | .. [Fornsberg2011] B. Fornberg, E. Larsson, and N. Flyer,
129 | "Stable Computations with Gaussian Radial Basis Functions,"
130 | *SIAM Journal on Scientific Computing*, 33(2), 869--892, (2011).
131 | `doi: 10.1137/09076756X `_
132 | .. [#f1] has option for QR factorization method
133 | """
134 |
135 | # remove singleton dimensions
136 | lon = np.squeeze(lon)
137 | lat = np.squeeze(lat)
138 | data = np.squeeze(data)
139 | longitude = np.squeeze(longitude)
140 | latitude = np.squeeze(latitude)
141 | # size of new matrix
142 | if (np.ndim(longitude) > 1):
143 | nlon, nlat = np.shape(longitude)
144 | sz = np.int64(nlon*nlat)
145 | else:
146 | sz = len(longitude)
147 |
148 | # Check to make sure sizes of input arguments are correct and consistent
149 | if (len(data) != len(lon)) | (len(data) != len(lat)):
150 | raise Exception('Length of input arrays must be equal')
151 | if (np.shape(longitude) != np.shape(latitude)):
152 | raise Exception('Size of output arrays must be equal')
153 |
154 | # create python dictionary of radial basis function formulas
155 | radial_basis_functions = {}
156 | radial_basis_functions['multiquadric'] = multiquadric
157 | radial_basis_functions['inverse_multiquadric'] = inverse_multiquadric
158 | radial_basis_functions['inverse'] = inverse_multiquadric
159 | radial_basis_functions['inverse_quadratic'] = inverse_quadratic
160 | radial_basis_functions['gaussian'] = gaussian
161 | radial_basis_functions['linear'] = linear
162 | radial_basis_functions['cubic'] = cubic
163 | radial_basis_functions['quintic'] = quintic
164 | radial_basis_functions['thin_plate'] = thin_plate
165 | # create python dictionary of radial basis function expansions
166 | radial_expansions = {}
167 | radial_expansions['multiquadric'] = multiquadratic_expansion
168 | radial_expansions['inverse_multiquadric'] = inverse_multiquadric_expansion
169 | radial_expansions['inverse'] = inverse_multiquadric_expansion
170 | radial_expansions['inverse_quadratic'] = inverse_quadratic_expansion
171 | radial_expansions['gaussian'] = gaussian_expansion
172 | # check if formula name is listed
173 | if method in radial_basis_functions.keys():
174 | RBF = radial_basis_functions[method]
175 | else:
176 | raise ValueError(f"Method {method} not implemented")
177 | # check if formula name is valid for QR factorization method
178 | if QR and (method in radial_expansions.keys()):
179 | expansion = radial_expansions[method]
180 | elif QR and (method not in radial_expansions.keys()):
181 | raise ValueError(f"{method} expansion not available with QR")
182 | # create python dictionary of distance functions (if not using QR)
183 | norm_functions = {}
184 | norm_functions['euclidean'] = distance_matrix
185 | norm_functions['GCD'] = angle_matrix
186 | if norm in norm_functions:
187 | norm_matrix = norm_functions[norm]
188 | else:
189 | raise ValueError(f"Distance Function {norm} not implemented")
190 |
191 | # convert input lat and lon into cartesian X,Y,Z over unit sphere
192 | phi = np.pi*lon/180.0
193 | th = np.pi*(90.0 - lat)/180.0
194 | xs = np.sin(th)*np.cos(phi)
195 | ys = np.sin(th)*np.sin(phi)
196 | zs = np.cos(th)
197 | # convert output longitude and latitude into cartesian X,Y,Z
198 | PHI = np.pi*longitude.flatten()/180.0
199 | THETA = np.pi*(90.0 - latitude.flatten())/180.0
200 | XI = np.sin(THETA)*np.cos(PHI)
201 | YI = np.sin(THETA)*np.sin(PHI)
202 | ZI = np.cos(THETA)
203 |
204 | # Creation of data distance matrix (Euclidean or Great-Circle Distance)
205 | # Data to Data
206 | Rd = norm_matrix(np.array([xs, ys, zs]), np.array([xs, ys, zs]))
207 | N, M = np.shape(Rd)
208 | # if epsilon is not specified
209 | if epsilon is None:
210 | # calculate norm with mean distance
211 | uix, uiy = np.nonzero(np.tri(N, M=M, k=-1))
212 | epsilon = np.mean(Rd[uix, uiy])
213 |
214 | # QR factorization algorithm of Fornberg (2007)
215 | if QR:
216 | # calculate radial basis functions using spherical harmonics
217 | R, w = RBF_QR(th, phi, epsilon, data, expansion)
218 | n_harm = np.sqrt(np.shape(R)[0]).astype(np.int64)
219 | # counter variable for filling spherical harmonic matrix
220 | index = 0
221 | # evaluation matrix E
222 | E = np.zeros((sz, np.int64(n_harm**2)))
223 | for l in range(0, n_harm):
224 | # Each loop adds a block of columns of degree l to E
225 | E[:, index:2*l+index+1] = spherical_harmonic_matrix(l, THETA, PHI)
226 | index += 2*l + 1
227 | # calculate output interpolated array (or matrix)
228 | output = np.dot(E, np.dot(R, w))
229 | else:
230 | # Calculation of the PHI Matrix with smoothing
231 | PHI = np.zeros((N+1, M+1))
232 | PHI[:N, :M] = RBF(epsilon, Rd) + np.eye(N, M=M)*smooth
233 | # Augmentation of the PHI Matrix with a Constant Vector
234 | PHI[:N, M] = np.ones((N))
235 | PHI[N, :M] = np.ones((M))
236 |
237 | # Computation of the Weights
238 | DMAT = np.concatenate(([data, [0]]), axis=0)
239 | w = np.linalg.lstsq(PHI, DMAT[:, np.newaxis], rcond=-1)[0]
240 |
241 | # Computation of distance Matrix (Euclidean or Great-Circle Distance)
242 | # Data to Mesh Points
243 | Re = norm_matrix(np.array([XI, YI, ZI]), np.array([xs, ys, zs]))
244 | # calculate radial basis function for data-to-mesh matrix
245 | E = RBF(epsilon, Re)
246 |
247 | # Augmentation of the Evaluation Matrix with a Constant Vector
248 | P = np.ones((sz, 1))
249 | E = np.concatenate(([E, P]), axis=1)
250 | # calculate output interpolated array (or matrix)
251 | output = np.dot(E, w)
252 |
253 | # reshape output to original dimensions and return
254 | if (np.ndim(longitude) == 1):
255 | return np.squeeze(output)
256 | else:
257 | return output.reshape(nlon, nlat)
258 |
259 | # define radial basis function formulas
260 | def multiquadric(epsilon, r):
261 | # multiquadratic
262 | f = np.sqrt((epsilon*r)**2 + 1.0)
263 | return f
264 |
265 | def multiquadratic_expansion(epsilon, mu):
266 | c = -2.0*np.pi*(2.0*epsilon**2 + 1.0 +
267 | (mu + 1.0/2.0)*np.sqrt(1.0 + 4.0*epsilon**2)) / \
268 | (mu + 1.0/2.0)/(mu + 3.0/2.0)/(mu - 1.0/2.0) * \
269 | (2.0/(1.0 + np.sqrt(4.0*epsilon**2+1.0)))**(2.0*mu+1.0)
270 | return c
271 |
272 | def inverse_multiquadric(epsilon, r):
273 | # inverse multiquadratic
274 | f = 1.0/np.sqrt((epsilon*r)**2 + 1.0)
275 | return f
276 |
277 | def inverse_multiquadric_expansion(epsilon, mu):
278 | c = 4.0*np.pi/(mu + 1.0/2.0) * \
279 | (2.0/(1.0 + np.sqrt(4.0*epsilon**2 + 1.0)))**(2*mu + 1.0)
280 | return c
281 |
282 | def inverse_quadratic(epsilon, r):
283 | # inverse quadratic
284 | f = 1.0/(1.0+(epsilon*r)**2)
285 | return f
286 |
287 | def inverse_quadratic_expansion(epsilon, mu):
288 | c = 4.0*np.pi**(3.0/2.0)*spc.factorial(mu) / \
289 | spc.gamma(mu + 3.0/2.0)/(1.0 + 4.0*epsilon**2)**(mu+1) * \
290 | spc.hyp2f1(mu+1, mu+1, 2.0*mu+2, 4.0*epsilon**2/(1.0 + 4.0*epsilon**2))
291 | return c
292 |
293 | def gaussian(epsilon, r):
294 | # gaussian
295 | f = np.exp(-(epsilon*r)**2)
296 | return f
297 |
298 | def gaussian_expansion(epsilon, mu):
299 | c = 4.0*np.pi**(3.0/2.0)*np.exp(-2.0*epsilon**2) * \
300 | spc.iv(mu + 1.0/2.0, 2.0*epsilon**2)/epsilon**(2.0*mu + 1.0)
301 | return c
302 |
303 | def linear(epsilon, r):
304 | # linear polynomial
305 | return r
306 |
307 | def cubic(epsilon, r):
308 | # cubic polynomial
309 | f = r**3
310 | return f
311 |
312 | def quintic(epsilon, r):
313 | # quintic polynomial
314 | f = r**5
315 | return f
316 |
317 | def thin_plate(epsilon, r):
318 | # thin plate spline
319 | f = r**2 * np.log(r)
320 | # the spline is zero at zero
321 | f[r == 0] = 0.0
322 | return f
323 |
324 | # calculate great-circle distance between between n-vectors
325 | def angle_matrix(x, cntrs):
326 | s, M = np.shape(x)
327 | s, N = np.shape(cntrs)
328 | A = np.zeros((M, N))
329 | A[:, :] = np.arccos(np.dot(x.transpose(), cntrs))
330 | A[np.isnan(A)] = 0.0
331 | return A
332 |
333 | # calculate Euclidean distances between points (default norm)
334 | def distance_matrix(x, cntrs):
335 | s, M = np.shape(x)
336 | s, N = np.shape(cntrs)
337 | # decompose Euclidean distance: (x-y)^2 = x^2 - 2xy + y^2
338 | dx2 = np.kron(np.ones((1, N)), np.sum(x * x, axis=0)[:, np.newaxis])
339 | dxy = 2.0*np.dot(x.transpose(), cntrs)
340 | dy2 = np.kron(np.ones((M, 1)), np.sum(cntrs * cntrs, axis=0))
341 | D = np.sqrt(dx2 - dxy + dy2)
342 | return D
343 |
344 | # calculate spherical harmonics of degree l evaluated at (theta,phi)
345 | def spherical_harmonic_matrix(l, theta, phi):
346 | # calculate legendre polynomials
347 | nth = len(theta)
348 | Pl = legendre(l, np.cos(theta)).transpose()
349 | # calculate degree dependent factors C and F
350 | m = np.arange(0, l+1) # spherical harmonic orders up to degree l
351 | C = np.sqrt((2.0*l + 1.0)/(4.0*np.pi))
352 | F = np.sqrt(spc.factorial(1 + l - m - 1)/spc.factorial(1 + l + m - 1))
353 | F = np.kron(np.ones((nth, 1)), F[np.newaxis, :])
354 | # calculate Euler's of spherical harmonic order multiplied by azimuth phi
355 | mphi = np.exp(1j*np.dot(np.squeeze(phi)[:, np.newaxis], m[np.newaxis, :]))
356 | # calculate spherical harmonics
357 | Ylms = F*Pl[:, 0:l+1]*mphi
358 | # multiply by C and convert to reduced matrix (theta,Slm:Clm)
359 | SPH = C*np.concatenate((np.imag(Ylms[:, :0:-1]), np.real(Ylms)), axis=1)
360 | return SPH
361 |
362 | # RBF interpolant with shape parameter epsilon through the node points
363 | # (theta,phi) with function values f from Fornberg
364 | # Outputs beta: the expansion coefficients of the interpolant with respect to
365 | # the RBF_QR basis.
366 | def RBF_QR(theta, phi, epsilon, data, RBF):
367 | n = len(phi)
368 | Y1 = np.zeros((n, n))
369 | B1 = np.zeros((n, n))
370 | # counter variable for filling spherical harmonic matrix
371 | index = 0
372 | # difference adding the next spherical harmonic degree
373 | d = 0.0
374 | # degree of the n_th spherical harmonic
375 | l = 0
376 | l_n = np.ceil(np.sqrt(n))-1
377 | # floating point machine precision
378 | eps = np.finfo(np.float64).eps
379 | while (d < -np.log10(eps)):
380 | # create new variables for Y and B which will resize if (l > (l_n -1))
381 | lmax = np.max([l_n, l])
382 | Y = np.zeros((n, int((lmax+1)**2)))
383 | Y[:, :index] = Y1[:, :index]
384 | B = np.zeros((n, int((lmax+1)**2)))
385 | B[:, :index] = B1[:, :index]
386 | # Each loop adds a block of columns of SPH of degree l to Y and to B.
387 | # Compute the spherical harmonics matrix
388 | Y[:, index:2*l+index+1] = spherical_harmonic_matrix(l, theta, phi)
389 | # Compute the expansion coefficients matrix
390 | B[:, index:2*l+index+1] = Y[:, index:2*l+index+1]*RBF(epsilon, l)
391 | B[:, index+l] = B[:, index+l]/2.0
392 | # Truncation criterion
393 | if (l > (l_n - 1)):
394 | dN1 = np.linalg.norm(B[:, int(l_n**2):int((l_n+1)**2)], ord=np.inf)
395 | dN2 = np.linalg.norm(B[:, int((l+1)**2)-1], ord=np.inf)
396 | d = np.log10(dN1/dN2*epsilon**(2*(l_n-l)))
397 | # copy B to B1 and Y to Y1
398 | B1 = B.copy()
399 | Y1 = Y.copy()
400 | # Calculate column index of next block
401 | index += 2*l + 1
402 | l += 1
403 | # QR-factorization to find the RBF_QR basis
404 | Q, R = np.linalg.qr(B)
405 | # Introduce the powers of epsilon
406 | X1 = np.kron(np.ones((n, 1)), np.ceil(np.sqrt(np.arange(n, l**2))))
407 | X2 = np.kron(np.ones((1, l**2-n)),
408 | (np.ceil(np.sqrt(np.arange(1, n+1)))-1)[:, np.newaxis])
409 | E = epsilon**(2.0*(X1 - X2))
410 | # Solve the interpolation linear system
411 | R_beta = E*np.linalg.lstsq(R[:n, :n], R[:n, n:], rcond=-1)[0]
412 | R_new = np.concatenate((np.eye(n), R_beta.T), axis=0)
413 | w = np.linalg.lstsq(np.dot(Y, R_new), data, rcond=-1)[0]
414 | return (R_new, w)
415 |
--------------------------------------------------------------------------------
/spatial_interpolators/spatial.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | u"""
3 | spatial.py
4 | Written by Tyler Sutterley (04/2023)
5 |
6 | Utilities for operating on spatial data
7 |
8 | PYTHON DEPENDENCIES:
9 | numpy: Scientific Computing Tools For Python
10 | https://numpy.org
11 | https://numpy.org/doc/stable/user/numpy-for-matlab-users.html
12 |
13 | UPDATE HISTORY:
14 | Updated 04/2023: copy inputs in cartesian to not modify original arrays
15 | added iterative methods for converting from cartesian to geodetic
16 | Updated 03/2023: add basic variable typing to function inputs
17 | Updated 04/2022: docstrings in numpy documentation format
18 | Updated 01/2022: use iteration breaks in convert ellipsoid function
19 | Updated 10/2021: add pole case in stereographic area scale calculation
20 | Updated 09/2021: can calculate height differences between ellipsoids
21 | Updated 07/2021: added function for determining input variable type
22 | Updated 03/2021: added polar stereographic area scale calculation
23 | add routines for converting to and from cartesian coordinates
24 | replaced numpy bool/int to prevent deprecation warnings
25 | Updated 12/2020: added module for converting ellipsoids
26 | Written 11/2020
27 | """
28 | from __future__ import annotations
29 |
30 | import numpy as np
31 |
32 | def data_type(x: np.ndarray, y: np.ndarray, t: np.ndarray) -> str:
33 | """
34 | Determines input data type based on variable dimensions
35 |
36 | Parameters
37 | ----------
38 | x: np.ndarray
39 | x-dimension coordinates
40 | y: np.ndarray
41 | y-dimension coordinates
42 | t: np.ndarray
43 | time-dimension coordinates
44 |
45 | Returns
46 | -------
47 | string denoting input data type
48 |
49 | - ``'time series'``
50 | - ``'drift'``
51 | - ``'grid'``
52 | """
53 | xsize = np.size(x)
54 | ysize = np.size(y)
55 | tsize = np.size(t)
56 | if (xsize == 1) and (ysize == 1) and (tsize >= 1):
57 | return 'time series'
58 | elif (xsize == ysize) & (xsize == tsize):
59 | return 'drift'
60 | elif (np.ndim(x) > 1) & (xsize == ysize):
61 | return 'grid'
62 | elif (xsize != ysize):
63 | return 'grid'
64 | else:
65 | raise ValueError('Unknown data type')
66 |
67 | def convert_ellipsoid(
68 | phi1: np.ndarray,
69 | h1: np.ndarray,
70 | a1: float,
71 | f1: float,
72 | a2: float,
73 | f2: float,
74 | eps: float = 1e-12,
75 | itmax: int = 10
76 | ):
77 | """
78 | Convert latitudes and heights to a different ellipsoid using Newton-Raphson
79 |
80 | Parameters
81 | ----------
82 | phi1: np.ndarray
83 | latitude of input ellipsoid in degrees
84 | h1: np.ndarray
85 | height above input ellipsoid in meters
86 | a1: float
87 | semi-major axis of input ellipsoid
88 | f1: float
89 | flattening of input ellipsoid
90 | a2: float
91 | semi-major axis of output ellipsoid
92 | f2: float
93 | flattening of output ellipsoid
94 | eps: float, default 1e-12
95 | tolerance to prevent division by small numbers and
96 | to determine convergence
97 | itmax: int, default 10
98 | maximum number of iterations to use in Newton-Raphson
99 |
100 | Returns
101 | -------
102 | phi2: np.ndarray
103 | latitude of output ellipsoid in degrees
104 | h2: np.ndarray
105 | height above output ellipsoid in meters
106 |
107 | References
108 | ----------
109 | .. [1] J. Meeus, *Astronomical Algorithms*, 2nd edition, 477 pp., (1998).
110 | """
111 | if (len(phi1) != len(h1)):
112 | raise ValueError('phi and h have incompatible dimensions')
113 | # semiminor axis of input and output ellipsoid
114 | b1 = (1.0 - f1)*a1
115 | b2 = (1.0 - f2)*a2
116 | # initialize output arrays
117 | npts = len(phi1)
118 | phi2 = np.zeros((npts))
119 | h2 = np.zeros((npts))
120 | # for each point
121 | for N in range(npts):
122 | # force phi1 into range -90 <= phi1 <= 90
123 | if (np.abs(phi1[N]) > 90.0):
124 | phi1[N] = np.sign(phi1[N])*90.0
125 | # handle special case near the equator
126 | # phi2 = phi1 (latitudes congruent)
127 | # h2 = h1 + a1 - a2
128 | if (np.abs(phi1[N]) < eps):
129 | phi2[N] = np.copy(phi1[N])
130 | h2[N] = h1[N] + a1 - a2
131 | # handle special case near the poles
132 | # phi2 = phi1 (latitudes congruent)
133 | # h2 = h1 + b1 - b2
134 | elif ((90.0 - np.abs(phi1[N])) < eps):
135 | phi2[N] = np.copy(phi1[N])
136 | h2[N] = h1[N] + b1 - b2
137 | # handle case if latitude is within 45 degrees of equator
138 | elif (np.abs(phi1[N]) <= 45):
139 | # convert phi1 to radians
140 | phi1r = phi1[N] * np.pi/180.0
141 | sinphi1 = np.sin(phi1r)
142 | cosphi1 = np.cos(phi1r)
143 | # prevent division by very small numbers
144 | cosphi1 = np.copy(eps) if (cosphi1 < eps) else cosphi1
145 | # calculate tangent
146 | tanphi1 = sinphi1 / cosphi1
147 | u1 = np.arctan(b1 / a1 * tanphi1)
148 | hpr1sin = b1 * np.sin(u1) + h1[N] * sinphi1
149 | hpr1cos = a1 * np.cos(u1) + h1[N] * cosphi1
150 | # set initial value for u2
151 | u2 = np.copy(u1)
152 | # setup constants
153 | k0 = b2 * b2 - a2 * a2
154 | k1 = a2 * hpr1cos
155 | k2 = b2 * hpr1sin
156 | # perform newton-raphson iteration to solve for u2
157 | # cos(u2) will not be close to zero since abs(phi1) <= 45
158 | for i in range(0, itmax+1):
159 | cosu2 = np.cos(u2)
160 | fu2 = k0 * np.sin(u2) + k1 * np.tan(u2) - k2
161 | fu2p = k0 * cosu2 + k1 / (cosu2 * cosu2)
162 | if (np.abs(fu2p) < eps):
163 | break
164 | else:
165 | delta = fu2 / fu2p
166 | u2 -= delta
167 | if (np.abs(delta) < eps):
168 | break
169 | # convert latitude to degrees and verify values between +/- 90
170 | phi2r = np.arctan(a2 / b2 * np.tan(u2))
171 | phi2[N] = phi2r*180.0/np.pi
172 | if (np.abs(phi2[N]) > 90.0):
173 | phi2[N] = np.sign(phi2[N])*90.0
174 | # calculate height
175 | h2[N] = (hpr1cos - a2 * np.cos(u2)) / np.cos(phi2r)
176 | # handle final case where latitudes are between 45 degrees and pole
177 | else:
178 | # convert phi1 to radians
179 | phi1r = phi1[N] * np.pi/180.0
180 | sinphi1 = np.sin(phi1r)
181 | cosphi1 = np.cos(phi1r)
182 | # prevent division by very small numbers
183 | cosphi1 = np.copy(eps) if (cosphi1 < eps) else cosphi1
184 | # calculate tangent
185 | tanphi1 = sinphi1 / cosphi1
186 | u1 = np.arctan(b1 / a1 * tanphi1)
187 | hpr1sin = b1 * np.sin(u1) + h1[N] * sinphi1
188 | hpr1cos = a1 * np.cos(u1) + h1[N] * cosphi1
189 | # set initial value for u2
190 | u2 = np.copy(u1)
191 | # setup constants
192 | k0 = a2 * a2 - b2 * b2
193 | k1 = b2 * hpr1sin
194 | k2 = a2 * hpr1cos
195 | # perform newton-raphson iteration to solve for u2
196 | # sin(u2) will not be close to zero since abs(phi1) > 45
197 | for i in range(0, itmax+1):
198 | sinu2 = np.sin(u2)
199 | fu2 = k0 * np.cos(u2) + k1 / np.tan(u2) - k2
200 | fu2p = -1 * (k0 * sinu2 + k1 / (sinu2 * sinu2))
201 | if (np.abs(fu2p) < eps):
202 | break
203 | else:
204 | delta = fu2 / fu2p
205 | u2 -= delta
206 | if (np.abs(delta) < eps):
207 | break
208 | # convert latitude to degrees and verify values between +/- 90
209 | phi2r = np.arctan(a2 / b2 * np.tan(u2))
210 | phi2[N] = phi2r*180.0/np.pi
211 | if (np.abs(phi2[N]) > 90.0):
212 | phi2[N] = np.sign(phi2[N])*90.0
213 | # calculate height
214 | h2[N] = (hpr1sin - b2 * np.sin(u2)) / np.sin(phi2r)
215 |
216 | # return the latitude and height
217 | return (phi2, h2)
218 |
219 | def compute_delta_h(
220 | a1: float,
221 | f1: float,
222 | a2: float,
223 | f2: float,
224 | lat: np.ndarray
225 | ):
226 | """
227 | Compute difference in elevation for two ellipsoids at a given
228 | latitude using a simplified empirical equation
229 |
230 | Parameters
231 | ----------
232 | a1: float
233 | semi-major axis of input ellipsoid
234 | f1: float
235 | flattening of input ellipsoid
236 | a2: float
237 | semi-major axis of output ellipsoid
238 | f2: float
239 | flattening of output ellipsoid
240 | lat: np.ndarray
241 | latitudes (degrees north)
242 |
243 | Returns
244 | -------
245 | delta_h: np.ndarray
246 | difference in elevation for two ellipsoids
247 |
248 | References
249 | ----------
250 | .. [1] J Meeus, *Astronomical Algorithms*, pp. 77--82, (1991).
251 | """
252 | # force phi into range -90 <= phi <= 90
253 | gt90, = np.nonzero((lat < -90.0) | (lat > 90.0))
254 | lat[gt90] = np.sign(lat[gt90])*90.0
255 | # semiminor axis of input and output ellipsoid
256 | b1 = (1.0 - f1)*a1
257 | b2 = (1.0 - f2)*a2
258 | # compute delta_a and delta_b coefficients
259 | delta_a = a2 - a1
260 | delta_b = b2 - b1
261 | # compute differences between ellipsoids
262 | # delta_h = -(delta_a * cos(phi)^2 + delta_b * sin(phi)^2)
263 | phi = lat * np.pi/180.0
264 | delta_h = -(delta_a*np.cos(phi)**2 + delta_b*np.sin(phi)**2)
265 | return delta_h
266 |
267 | def wrap_longitudes(lon: float | np.ndarray):
268 | """
269 | Wraps longitudes to range from -180 to +180
270 |
271 | Parameters
272 | ----------
273 | lon: float or np.ndarray
274 | longitude (degrees east)
275 | """
276 | phi = np.arctan2(np.sin(lon*np.pi/180.0), np.cos(lon*np.pi/180.0))
277 | # convert phi from radians to degrees
278 | return phi*180.0/np.pi
279 |
280 | def to_cartesian(
281 | lon: np.ndarray,
282 | lat: np.ndarray,
283 | h: float | np.ndarray = 0.0,
284 | a_axis: float = 6378137.0,
285 | flat: float = 1.0/298.257223563
286 | ):
287 | """
288 | Converts geodetic coordinates to Cartesian coordinates
289 |
290 | Parameters
291 | ----------
292 | lon: np.ndarray
293 | longitude (degrees east)
294 | lat: np.ndarray
295 | latitude (degrees north)
296 | h: float or np.ndarray, default 0.0
297 | height above ellipsoid (or sphere)
298 | a_axis: float, default 6378137.0
299 | semimajor axis of the ellipsoid
300 |
301 | for spherical coordinates set to radius of the Earth
302 | flat: float, default 1.0/298.257223563
303 | ellipsoidal flattening
304 |
305 | for spherical coordinates set to 0
306 | """
307 | # verify axes and copy to not modify inputs
308 | lon = np.atleast_1d(np.copy(lon))
309 | lat = np.atleast_1d(np.copy(lat))
310 | # fix coordinates to be 0:360
311 | lon[lon < 0] += 360.0
312 | # Linear eccentricity and first numerical eccentricity
313 | lin_ecc = np.sqrt((2.0*flat - flat**2)*a_axis**2)
314 | ecc1 = lin_ecc/a_axis
315 | # convert from geodetic latitude to geocentric latitude
316 | dtr = np.pi/180.0
317 | # geodetic latitude in radians
318 | latitude_geodetic_rad = lat*dtr
319 | # prime vertical radius of curvature
320 | N = a_axis/np.sqrt(1.0 - ecc1**2.0*np.sin(latitude_geodetic_rad)**2.0)
321 | # calculate X, Y and Z from geodetic latitude and longitude
322 | X = (N + h) * np.cos(latitude_geodetic_rad) * np.cos(lon*dtr)
323 | Y = (N + h) * np.cos(latitude_geodetic_rad) * np.sin(lon*dtr)
324 | Z = (N * (1.0 - ecc1**2.0) + h) * np.sin(latitude_geodetic_rad)
325 | # return the cartesian coordinates
326 | return (X, Y, Z)
327 |
328 | def to_sphere(x: np.ndarray, y: np.ndarray, z: np.ndarray):
329 | """
330 | Convert from cartesian coordinates to spherical coordinates
331 |
332 | Parameters
333 | ----------
334 | x, np.ndarray
335 | cartesian x-coordinates
336 | y, np.ndarray
337 | cartesian y-coordinates
338 | z, np.ndarray
339 | cartesian z-coordinates
340 | """
341 | # verify axes and copy to not modify inputs
342 | x = np.atleast_1d(np.copy(x))
343 | y = np.atleast_1d(np.copy(y))
344 | z = np.atleast_1d(np.copy(z))
345 | # calculate radius
346 | rad = np.sqrt(x**2.0 + y**2.0 + z**2.0)
347 | # calculate angular coordinates
348 | # phi: azimuthal angle
349 | phi = np.arctan2(y, x)
350 | # th: polar angle
351 | th = np.arccos(z/rad)
352 | # convert to degrees and fix to 0:360
353 | lon = 180.0*phi/np.pi
354 | if np.any(lon < 0):
355 | lt0 = np.nonzero(lon < 0)
356 | lon[lt0] += 360.0
357 | # convert to degrees and fix to -90:90
358 | lat = 90.0 - (180.0*th/np.pi)
359 | np.clip(lat, -90, 90, out=lat)
360 | # return latitude, longitude and radius
361 | return (lon, lat, rad)
362 |
363 | def to_geodetic(
364 | x: np.ndarray,
365 | y: np.ndarray,
366 | z: np.ndarray,
367 | a_axis: float = 6378137.0,
368 | flat: float = 1.0/298.257223563,
369 | method: str = 'bowring',
370 | eps: float = np.finfo(np.float64).eps,
371 | iterations: int = 10
372 | ):
373 | """
374 | Convert from cartesian coordinates to geodetic coordinates
375 | using either iterative or closed-form methods
376 |
377 | Parameters
378 | ----------
379 | x, float
380 | cartesian x-coordinates
381 | y, float
382 | cartesian y-coordinates
383 | z, float
384 | cartesian z-coordinates
385 | a_axis: float, default 6378137.0
386 | semimajor axis of the ellipsoid
387 | flat: float, default 1.0/298.257223563
388 | ellipsoidal flattening
389 | method: str, default 'bowring'
390 | method to use for conversion
391 |
392 | - ``'moritz'``: iterative solution
393 | - ``'bowring'``: iterative solution
394 | - ``'zhu'``: closed-form solution
395 | eps: float, default np.finfo(np.float64).eps
396 | tolerance for iterative methods
397 | iterations: int, default 10
398 | maximum number of iterations
399 | """
400 | # verify axes and copy to not modify inputs
401 | x = np.atleast_1d(np.copy(x))
402 | y = np.atleast_1d(np.copy(y))
403 | z = np.atleast_1d(np.copy(z))
404 | # calculate the geodetic coordinates using the specified method
405 | if (method.lower() == 'moritz'):
406 | return _moritz_iterative(x, y, z, a_axis=a_axis, flat=flat,
407 | eps=eps, iterations=iterations)
408 | elif (method.lower() == 'bowring'):
409 | return _bowring_iterative(x, y, z, a_axis=a_axis, flat=flat,
410 | eps=eps, iterations=iterations)
411 | elif (method.lower() == 'zhu'):
412 | return _zhu_closed_form(x, y, z, a_axis=a_axis, flat=flat)
413 | else:
414 | raise ValueError(f'Unknown conversion method: {method}')
415 |
416 | def _moritz_iterative(
417 | x: np.ndarray,
418 | y: np.ndarray,
419 | z: np.ndarray,
420 | a_axis: float = 6378137.0,
421 | flat: float = 1.0/298.257223563,
422 | eps: float = np.finfo(np.float64).eps,
423 | iterations: int = 10
424 | ):
425 | """
426 | Convert from cartesian coordinates to geodetic coordinates
427 | using the iterative solution of [1]_
428 |
429 | Parameters
430 | ----------
431 | x, float
432 | cartesian x-coordinates
433 | y, float
434 | cartesian y-coordinates
435 | z, float
436 | cartesian z-coordinates
437 | a_axis: float, default 6378137.0
438 | semimajor axis of the ellipsoid
439 | flat: float, default 1.0/298.257223563
440 | ellipsoidal flattening
441 | eps: float, default np.finfo(np.float64).eps
442 | tolerance for iterative method
443 | iterations: int, default 10
444 | maximum number of iterations
445 |
446 | References
447 | ----------
448 | .. [1] B. Hofmann-Wellenhof and H. Moritz,
449 | *Physical Geodesy*, 2nd Edition, 403 pp., (2006).
450 | `doi: 10.1007/978-3-211-33545-1
451 | `_
452 | """
453 | # Linear eccentricity and first numerical eccentricity
454 | lin_ecc = np.sqrt((2.0*flat - flat**2)*a_axis**2)
455 | ecc1 = lin_ecc/a_axis
456 | # degrees to radians
457 | dtr = np.pi/180.0
458 | # calculate longitude
459 | lon = np.arctan2(y, x)/dtr
460 | # set initial estimate of height to 0
461 | h = np.zeros_like(lon)
462 | h0 = np.inf*np.ones_like(lon)
463 | # calculate radius of parallel
464 | p = np.sqrt(x**2 + y**2)
465 | # initial estimated value for phi using h=0
466 | phi = np.arctan(z/(p*(1.0 - ecc1**2)))
467 | # iterate to tolerance or to maximum number of iterations
468 | i = 0
469 | while np.any(np.abs(h - h0) > eps) and (i <= iterations):
470 | # copy previous iteration of height
471 | h0 = np.copy(h)
472 | # calculate radius of curvature
473 | N = a_axis/np.sqrt(1.0 - ecc1**2 * np.sin(phi)**2)
474 | # estimate new value of height
475 | h = p/np.cos(phi) - N
476 | # estimate new value for latitude using heights
477 | phi = np.arctan(z/(p*(1.0 - ecc1**2*N/(N + h))))
478 | # add to iterator
479 | i += 1
480 | # return latitude, longitude and height
481 | return (lon, phi/dtr, h)
482 |
483 | def _bowring_iterative(
484 | x: np.ndarray,
485 | y: np.ndarray,
486 | z: np.ndarray,
487 | a_axis: float = 6378137.0,
488 | flat: float = 1.0/298.257223563,
489 | eps: float = np.finfo(np.float64).eps,
490 | iterations: int = 10
491 | ):
492 | """
493 | Convert from cartesian coordinates to geodetic coordinates
494 | using the iterative solution of [1]_ [2]_
495 |
496 | Parameters
497 | ----------
498 | x, float
499 | cartesian x-coordinates
500 | y, float
501 | cartesian y-coordinates
502 | z, float
503 | cartesian z-coordinates
504 | a_axis: float, default 6378137.0
505 | semimajor axis of the ellipsoid
506 | flat: float, default 1.0/298.257223563
507 | ellipsoidal flattening
508 | eps: float, default np.finfo(np.float64).eps
509 | tolerance for iterative method
510 | iterations: int, default 10
511 | maximum number of iterations
512 |
513 | References
514 | ----------
515 | .. [1] B. R. Bowring, "Transformation from spatial
516 | to geodetic coordinates," *Survey Review*, 23(181),
517 | 323--327, (1976). `doi: 10.1179/sre.1976.23.181.323
518 | `_
519 | .. [2] B. R. Bowring, "The Accuracy Of Geodetic
520 | Latitude and Height Equations," *Survey Review*, 28(218),
521 | 202--206, (1985). `doi: 10.1179/sre.1985.28.218.202
522 | `_
523 | """
524 | # semiminor axis of the WGS84 ellipsoid [m]
525 | b_axis = (1.0 - flat)*a_axis
526 | # Linear eccentricity
527 | lin_ecc = np.sqrt((2.0*flat - flat**2)*a_axis**2)
528 | # square of first and second numerical eccentricity
529 | e12 = lin_ecc**2/a_axis**2
530 | e22 = lin_ecc**2/b_axis**2
531 | # degrees to radians
532 | dtr = np.pi/180.0
533 | # calculate longitude
534 | lon = np.arctan2(y, x)/dtr
535 | # calculate radius of parallel
536 | p = np.sqrt(x**2 + y**2)
537 | # initial estimated value for reduced parametric latitude
538 | u = np.arctan(a_axis*z/(b_axis*p))
539 | # initial estimated value for latitude
540 | phi = np.arctan((z + e22*b_axis*np.sin(u)**3) /
541 | (p - e12*a_axis*np.cos(u)**3))
542 | phi0 = np.inf*np.ones_like(lon)
543 | # iterate to tolerance or to maximum number of iterations
544 | i = 0
545 | while np.any(np.abs(phi - phi0) > eps) and (i <= iterations):
546 | # copy previous iteration of phi
547 | phi0 = np.copy(phi)
548 | # calculate reduced parametric latitude
549 | u = np.arctan(b_axis*np.tan(phi)/a_axis)
550 | # estimate new value of latitude
551 | phi = np.arctan((z + e22*b_axis*np.sin(u)**3) /
552 | (p - e12*a_axis*np.cos(u)**3))
553 | # add to iterator
554 | i += 1
555 | # calculate final radius of curvature
556 | N = a_axis/np.sqrt(1.0 - e12 * np.sin(phi)**2)
557 | # estimate final height (Bowring, 1985)
558 | h = p*np.cos(phi) + z*np.sin(phi) - a_axis**2/N
559 | # return latitude, longitude and height
560 | return (lon, phi/dtr, h)
561 |
562 | def _zhu_closed_form(
563 | x: np.ndarray,
564 | y: np.ndarray,
565 | z: np.ndarray,
566 | a_axis: float = 6378137.0,
567 | flat: float = 1.0/298.257223563,
568 | ):
569 | """
570 | Convert from cartesian coordinates to geodetic coordinates
571 | using the closed-form solution of [1]_
572 |
573 | Parameters
574 | ----------
575 | x, float
576 | cartesian x-coordinates
577 | y, float
578 | cartesian y-coordinates
579 | z, float
580 | cartesian z-coordinates
581 | a_axis: float, default 6378137.0
582 | semimajor axis of the ellipsoid
583 | flat: float, default 1.0/298.257223563
584 | ellipsoidal flattening
585 |
586 | References
587 | ----------
588 | .. [1] J. Zhu, "Exact conversion of Earth-centered,
589 | Earth-fixed coordinates to geodetic coordinates,"
590 | *Journal of Guidance, Control, and Dynamics*,
591 | 16(2), 389--391, (1993). `doi: 10.2514/3.21016
592 | `_
593 | """
594 | # semiminor axis of the WGS84 ellipsoid [m]
595 | b_axis = (1.0 - flat)*a_axis
596 | # Linear eccentricity
597 | lin_ecc = np.sqrt((2.0*flat - flat**2)*a_axis**2)
598 | # square of first numerical eccentricity
599 | e12 = lin_ecc**2/a_axis**2
600 | # degrees to radians
601 | dtr = np.pi/180.0
602 | # calculate longitude
603 | lon = np.arctan2(y, x)/dtr
604 | # calculate radius of parallel
605 | w = np.sqrt(x**2 + y**2)
606 | # allocate for output latitude and height
607 | lat = np.zeros_like(lon)
608 | h = np.zeros_like(lon)
609 | if np.any(w == 0):
610 | # special case where w == 0 (exact polar solution)
611 | ind, = np.nonzero(w == 0)
612 | h[ind] = np.sign(z[ind])*z[ind] - b_axis
613 | lat[ind] = 90.0*np.sign(z[ind])
614 | else:
615 | # all other cases
616 | ind, = np.nonzero(w != 0)
617 | l = e12/2.0
618 | m = (w[ind]/a_axis)**2.0
619 | n = ((1.0 - e12)*z[ind]/b_axis)**2.0
620 | i = -(2.0*l**2 + m + n)/2.0
621 | k = (l**2.0 - m - n)*l**2.0
622 | q = (1.0/216.0)*(m + n - 4.0*l**2)**3.0 + m*n*l**2.0
623 | D = np.sqrt((2.0*q - m*n*l**2)*m*n*l**2)
624 | B = i/3.0 - (q + D)**(1.0/3.0) - (q - D)**(1.0/3.0)
625 | t = np.sqrt(np.sqrt(B**2-k) - (B + i)/2.0) - \
626 | np.sign(m - n)*np.sqrt((B - i)/2.0)
627 | wi = w/(t + l)
628 | zi = (1.0 - e12)*z[ind]/(t - l)
629 | # calculate latitude and height
630 | lat[ind] = np.arctan2(zi, ((1.0 - e12)*wi))/dtr
631 | h[ind] = np.sign(t-1.0+l)*np.sqrt((w-wi)**2.0 + (z[ind]-zi)**2.0)
632 | # return latitude, longitude and height
633 | return (lon, lat, h)
634 |
635 | def scale_areas(
636 | lat: np.ndarray,
637 | flat: float = 1.0/298.257223563,
638 | ref: float = 70.0
639 | ):
640 | """
641 | Calculates area scaling factors for a polar stereographic projection
642 | including special case of at the exact pole [1]_ [2]_
643 |
644 | Parameters
645 | ----------
646 | lat: np.ndarray
647 | latitude (degrees north)
648 | flat: float, default 1.0/298.257223563
649 | ellipsoidal flattening
650 | ref: float, default 70.0
651 | reference latitude (true scale latitude)
652 |
653 | Returns
654 | -------
655 | scale: np.ndarray
656 | area scaling factors at input latitudes
657 |
658 | References
659 | ----------
660 | .. [1] J. P. Snyder, *Map Projections used by the U.S. Geological Survey*,
661 | Geological Survey Bulletin 1532, U.S. Government Printing Office, (1982).
662 | .. [2] JPL Technical Memorandum 3349-85-101
663 | """
664 | # convert latitude from degrees to positive radians
665 | theta = np.abs(lat)*np.pi/180.0
666 | # convert reference latitude from degrees to positive radians
667 | theta_ref = np.abs(ref)*np.pi/180.0
668 | # square of the eccentricity of the ellipsoid
669 | # ecc2 = (1-b**2/a**2) = 2.0*flat - flat^2
670 | ecc2 = 2.0*flat - flat**2
671 | # eccentricity of the ellipsoid
672 | ecc = np.sqrt(ecc2)
673 | # calculate ratio at input latitudes
674 | m = np.cos(theta)/np.sqrt(1.0 - ecc2*np.sin(theta)**2)
675 | t = np.tan(np.pi/4.0 - theta/2.0)/((1.0 - ecc*np.sin(theta)) / \
676 | (1.0 + ecc*np.sin(theta)))**(ecc/2.0)
677 | # calculate ratio at reference latitude
678 | mref = np.cos(theta_ref)/np.sqrt(1.0 - ecc2*np.sin(theta_ref)**2)
679 | tref = np.tan(np.pi/4.0 - theta_ref/2.0)/((1.0 - ecc*np.sin(theta_ref)) / \
680 | (1.0 + ecc*np.sin(theta_ref)))**(ecc/2.0)
681 | # distance scaling
682 | k = (mref/m)*(t/tref)
683 | kp = 0.5*mref*np.sqrt(((1.0+ecc)**(1.0+ecc))*((1.0-ecc)**(1.0-ecc)))/tref
684 | # area scaling
685 | scale = np.where(np.isclose(theta, np.pi/2.0), 1.0/(kp**2), 1.0/(k**2))
686 | return scale
687 |
--------------------------------------------------------------------------------