├── 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 | 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 | --------------------------------------------------------------------------------