├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.rst ├── binder └── environment.yml ├── ci ├── environment-py35.yml ├── environment-py36.yml └── environment-py37.yml ├── doc ├── Makefile ├── conf.py ├── index.rst ├── installation.rst ├── internal_api.rst ├── limitations.rst ├── make.bat ├── notebooks │ ├── Backend.ipynb │ ├── Compare_algorithms.ipynb │ ├── Curvilinear_grid.ipynb │ ├── Dask.ipynb │ ├── Dataset.ipynb │ ├── Pure_numpy.ipynb │ ├── Rectilinear_grid.ipynb │ ├── Reuse_regridder.ipynb │ └── Using_LocStream.ipynb ├── other_tools.rst ├── requirements.txt ├── user_api.rst └── why.rst ├── readthedocs.yml ├── setup.py └── xesmf ├── __init__.py ├── backend.py ├── data.py ├── frontend.py ├── smm.py ├── tests ├── __init__.py ├── test_backend.py ├── test_frontend.py └── test_util.py └── util.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = xesmf/tests/* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # Sphinx documentation 27 | docs/_build/ 28 | doc/_build/ 29 | 30 | # notebook 31 | .ipynb_checkpoints 32 | 33 | # OS-generated 34 | .DS_Store 35 | 36 | # NetCDF files 37 | *.nc 38 | *.nc4 39 | 40 | # ESMPy log files 41 | PET0.ESMF_LogFile 42 | 43 | # Unit test / coverage reports 44 | .cache 45 | .coverage 46 | coverage.xml 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # largely taken from https://github.com/xgcm/xgcm/blob/master/.travis.yml 2 | language: python 3 | sudo: false # use container based build 4 | notifications: 5 | email: false 6 | 7 | matrix: 8 | include: 9 | - python: "3.7" 10 | env: CONDA_ENV=py37 11 | - python: "3.6" 12 | env: CONDA_ENV=py36 13 | - python: "3.5" 14 | env: CONDA_ENV=py35 15 | 16 | before_install: 17 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; 18 | - bash miniconda.sh -b -p $HOME/miniconda 19 | - export PATH="$HOME/miniconda/bin:$PATH" 20 | - hash -r 21 | - conda config --set always_yes yes --set changeps1 no 22 | - conda update -q conda 23 | - conda info -a 24 | 25 | install: 26 | - conda env create --file ci/environment-$CONDA_ENV.yml 27 | - source activate test_env 28 | - pip install -e . 29 | 30 | script: 31 | - pytest -v xesmf --cov=xesmf --cov-config .coveragerc --cov-report term-missing 32 | 33 | after_success: 34 | - codecov 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jiawei Zhuang 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 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | xESMF: Universal Regridder for Geospatial Data 2 | ============================================== 3 | 4 | |Binder| |pypi| |Build Status| |codecov| |docs| |license| |DOI| 5 | 6 | xESMF is a Python package for 7 | `regridding `_. 8 | It is 9 | 10 | - **Powerful**: It uses ESMF_/ESMPy_ as backend and can regrid between **general curvilinear grids** 11 | with all `ESMF regridding algorithms `_, 12 | such as **bilinear**, **conservative** and **nearest neighbour**. 13 | - **Easy-to-use**: It abstracts away ESMF's complicated infrastructure 14 | and provides a simple, high-level API, compatible with xarray_ as well as basic numpy arrays. 15 | - **Fast**: It is faster than ESMPy's original Fortran regridding engine in serial case, and also supports dask_ for `out-of-core, parallel computation `_. 16 | 17 | Please see `online documentation `_, or `play with example notebooks on Binder `_. 18 | 19 | For new users, I also recommend reading `How to ask for help `_ and `How to support xESMF `_. 20 | 21 | .. _ESMF: https://www.earthsystemcog.org/projects/esmf/ 22 | .. _ESMPy: https://www.earthsystemcog.org/projects/esmpy/ 23 | .. _xarray: http://xarray.pydata.org 24 | .. _dask: https://dask.org/ 25 | 26 | .. |pypi| image:: https://badge.fury.io/py/xesmf.svg 27 | :target: https://badge.fury.io/py/xesmf 28 | :alt: pypi package 29 | 30 | .. |Build Status| image:: https://api.travis-ci.org/JiaweiZhuang/xESMF.svg 31 | :target: https://travis-ci.org/JiaweiZhuang/xESMF 32 | :alt: travis-ci build status 33 | 34 | .. |codecov| image:: https://codecov.io/gh/JiaweiZhuang/xESMF/branch/master/graph/badge.svg 35 | :target: https://codecov.io/gh/JiaweiZhuang/xESMF 36 | :alt: code coverage 37 | 38 | .. |docs| image:: https://readthedocs.org/projects/xesmf/badge/?version=latest 39 | :target: http://xesmf.readthedocs.io/en/latest/?badge=latest 40 | :alt: documentation status 41 | 42 | .. |license| image:: https://img.shields.io/badge/License-MIT-blue.svg 43 | :target: https://github.com/JiaweiZhuang/xESMF/blob/master/LICENSE 44 | :alt: license 45 | 46 | .. |DOI| image:: https://zenodo.org/badge/101709596.svg 47 | :target: https://zenodo.org/badge/latestdoi/101709596 48 | :alt: DOI 49 | 50 | .. |Binder| image:: https://mybinder.org/badge_logo.svg 51 | :target: https://mybinder.org/v2/gh/JiaweiZhuang/xESMF/master?filepath=doc%2Fnotebooks 52 | :alt: binder 53 | -------------------------------------------------------------------------------- /binder/environment.yml: -------------------------------------------------------------------------------- 1 | channels: 2 | - conda-forge 3 | dependencies: 4 | - python=3.7 5 | - esmpy==7.1.0r 6 | - xarray 7 | - dask 8 | - numpy 9 | - scipy 10 | - matplotlib 11 | - cartopy 12 | - pip: 13 | - xesmf==0.2.2 14 | -------------------------------------------------------------------------------- /ci/environment-py35.yml: -------------------------------------------------------------------------------- 1 | name: test_env 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.5 6 | - esmpy 7 | - xarray 8 | - dask 9 | - numpy 10 | - scipy 11 | - pytest 12 | - pip: 13 | - pytest-cov 14 | - codecov 15 | -------------------------------------------------------------------------------- /ci/environment-py36.yml: -------------------------------------------------------------------------------- 1 | name: test_env 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.6 6 | - esmpy 7 | - xarray 8 | - dask 9 | - numpy 10 | - scipy 11 | - pytest 12 | - pip: 13 | - pytest-cov 14 | - codecov 15 | -------------------------------------------------------------------------------- /ci/environment-py37.yml: -------------------------------------------------------------------------------- 1 | name: test_env 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.7 6 | - esmpy 7 | - xarray 8 | - dask 9 | - numpy 10 | - scipy 11 | - pytest 12 | - pip: 13 | - pytest-cov 14 | - codecov 15 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = xESMF 8 | SOURCEDIR = . 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) -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # xESMF documentation build configuration file, created by 5 | # sphinx-quickstart on Sun Dec 17 19:49:04 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('..')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ['sphinx.ext.autodoc', 'numpydoc', 'sphinx.ext.autosummary', 35 | 'sphinx.ext.mathjax', 36 | 'nbsphinx', 'IPython.sphinxext.ipython_console_highlighting'] 37 | # IPython.sphinxext.ipython_console_highlighting is required for anaconda. 38 | # See the issue: https://github.com/spatialaudio/nbsphinx/issues/24 39 | 40 | # https://stackoverflow.com/questions/12206334/sphinx-autosummary-toctree-contains-reference-to-nonexisting-document-warnings 41 | numpydoc_show_class_members = False 42 | 43 | # NOT to sort autodoc functions in alphabetical order 44 | autodoc_member_order = 'bysource' 45 | 46 | # To avoid installing xESMF and all its dependencies when building doc 47 | # https://stackoverflow.com/a/15912502/8729698 48 | autodoc_mock_imports = ['numpy', 'xarray', 'scipy', 'ESMF'] 49 | 50 | # avoid automatic execution for notebooks 51 | nbsphinx_execute = 'never' 52 | 53 | # Add any paths that contain templates here, relative to this directory. 54 | templates_path = ['_templates'] 55 | 56 | # The suffix(es) of source filenames. 57 | # You can specify multiple suffix as a list of string: 58 | # 59 | # source_suffix = ['.rst', '.md'] 60 | source_suffix = '.rst' 61 | 62 | # The master toctree document. 63 | master_doc = 'index' 64 | 65 | # General information about the project. 66 | project = 'xESMF' 67 | copyright = '2017, Jiawei Zhuang' 68 | author = 'Jiawei Zhuang' 69 | 70 | # The version info for the project you're documenting, acts as replacement for 71 | # |version| and |release|, also used in various other places throughout the 72 | # built documents. 73 | # 74 | # The short X.Y version. 75 | version = '0.3.0' 76 | # The full version, including alpha/beta/rc tags. 77 | release = '0.3.0' 78 | 79 | # The language for content autogenerated by Sphinx. Refer to documentation 80 | # for a list of supported languages. 81 | # 82 | # This is also used if you do content translation via gettext catalogs. 83 | # Usually you set "language" from the command line for these cases. 84 | language = None 85 | 86 | # List of patterns, relative to source directory, that match files and 87 | # directories to ignore when looking for source files. 88 | # This patterns also effect to html_static_path and html_extra_path 89 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '**.ipynb_checkpoints'] 90 | 91 | # The name of the Pygments (syntax highlighting) style to use. 92 | pygments_style = 'sphinx' 93 | 94 | # If true, `todo` and `todoList` produce output, else they produce nothing. 95 | todo_include_todos = False 96 | 97 | 98 | # -- Options for HTML output ---------------------------------------------- 99 | 100 | # The theme to use for HTML and HTML Help pages. See the documentation for 101 | # a list of builtin themes. 102 | # 103 | html_theme = 'sphinx_rtd_theme' 104 | 105 | # Theme options are theme-specific and customize the look and feel of a theme 106 | # further. For a list of options available for each theme, see the 107 | # documentation. 108 | # 109 | # html_theme_options = {} 110 | 111 | # Add any paths that contain custom static files (such as style sheets) here, 112 | # relative to this directory. They are copied after the builtin static files, 113 | # so a file named "default.css" will overwrite the builtin "default.css". 114 | html_static_path = ['_static'] 115 | 116 | # Custom sidebar templates, must be a dictionary that maps document names 117 | # to template names. 118 | # 119 | # This is required for the alabaster theme 120 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 121 | html_sidebars = { 122 | '**': [ 123 | 'about.html', 124 | 'navigation.html', 125 | 'relations.html', # needs 'show_related': True theme option to display 126 | 'searchbox.html', 127 | 'donate.html', 128 | ] 129 | } 130 | 131 | 132 | # -- Options for HTMLHelp output ------------------------------------------ 133 | 134 | # Output file base name for HTML help builder. 135 | htmlhelp_basename = 'xESMFdoc' 136 | 137 | 138 | # -- Options for LaTeX output --------------------------------------------- 139 | 140 | latex_elements = { 141 | # The paper size ('letterpaper' or 'a4paper'). 142 | # 143 | # 'papersize': 'letterpaper', 144 | 145 | # The font size ('10pt', '11pt' or '12pt'). 146 | # 147 | # 'pointsize': '10pt', 148 | 149 | # Additional stuff for the LaTeX preamble. 150 | # 151 | # 'preamble': '', 152 | 153 | # Latex figure (float) alignment 154 | # 155 | # 'figure_align': 'htbp', 156 | } 157 | 158 | # Grouping the document tree into LaTeX files. List of tuples 159 | # (source start file, target name, title, 160 | # author, documentclass [howto, manual, or own class]). 161 | latex_documents = [ 162 | (master_doc, 'xESMF.tex', 'xESMF Documentation', 163 | 'Jiawei Zhuang', 'manual'), 164 | ] 165 | 166 | 167 | # -- Options for manual page output --------------------------------------- 168 | 169 | # One entry per manual page. List of tuples 170 | # (source start file, name, description, authors, manual section). 171 | man_pages = [ 172 | (master_doc, 'xesmf', 'xESMF Documentation', 173 | [author], 1) 174 | ] 175 | 176 | 177 | # -- Options for Texinfo output ------------------------------------------- 178 | 179 | # Grouping the document tree into Texinfo files. List of tuples 180 | # (source start file, target name, title, author, 181 | # dir menu entry, description, category) 182 | texinfo_documents = [ 183 | (master_doc, 'xESMF', 'xESMF Documentation', 184 | author, 'xESMF', 'One line description of project.', 185 | 'Miscellaneous'), 186 | ] 187 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | xESMF: Universal Regridder for Geospatial Data 2 | ============================================== 3 | 4 | xESMF is a Python package for 5 | `regridding `_. 6 | It is 7 | 8 | - **Powerful**: It uses ESMF_/ESMPy_ as backend and can regrid between **general curvilinear grids** 9 | with all `ESMF regridding algorithms `_, 10 | such as **bilinear**, **conservative** and **nearest neighbour**. 11 | - **Easy-to-use**: It abstracts away ESMF's complicated infrastructure 12 | and provides a simple, high-level API, compatible with xarray_ as well as basic numpy arrays. 13 | - **Fast**: It is :doc:`faster than <./notebook/Backend>` ESMPy's original Fortran regridding engine in serial case, and also supports dask_ for `out-of-core, parallel computation `_. 14 | 15 | 16 | .. _ESMF: https://www.earthsystemcog.org/projects/esmf/ 17 | .. _ESMPy: https://www.earthsystemcog.org/projects/esmpy/ 18 | .. _xarray: http://xarray.pydata.org 19 | .. _dask: https://dask.org/ 20 | 21 | 22 | Contents 23 | -------- 24 | 25 | .. toctree:: 26 | :maxdepth: 1 27 | :caption: Overview 28 | 29 | why 30 | other_tools 31 | limitations 32 | 33 | .. toctree:: 34 | :maxdepth: 1 35 | :caption: Beginner tutorials 36 | 37 | installation 38 | notebooks/Rectilinear_grid 39 | notebooks/Curvilinear_grid 40 | notebooks/Pure_numpy 41 | notebooks/Dataset 42 | 43 | .. toctree:: 44 | :maxdepth: 1 45 | :caption: Intermediate tutorials 46 | 47 | notebooks/Dask 48 | notebooks/Compare_algorithms 49 | notebooks/Reuse_regridder 50 | notebooks/Using_LocStream 51 | 52 | .. toctree:: 53 | :maxdepth: 1 54 | :caption: Technical notes 55 | 56 | notebooks/Backend 57 | 58 | .. toctree:: 59 | :maxdepth: 1 60 | :caption: API 61 | 62 | user_api 63 | internal_api 64 | 65 | 66 | How to ask for help 67 | ------------------- 68 | 69 | The `GitHub issue tracker `_ is the primary place for bug reports. If you hit any issues, I recommend the following steps: 70 | 71 | - First, `search for existing issues `_. Other people are likely to hit the same problem and probably have already found the solution. 72 | 73 | - For a new bug, please `craft a minimal bug report `_ with reproducible code. Use synthetic data or `upload `_ a small sample of input data (~1 MB) so I can quickly reproducible your error. 74 | 75 | - For platform-dependent problems (such as kernel dying and installation error), please also show how to reproduce your system environment, otherwise I have no way to diagnose the issue. The best approach is probably finding an `official Docker image `_ that is closest to your OS (such as `Ubuntu `_ or `CentOS `_), and build your Python environment starting with such image, to see whether the error still exists. Alternatively you can select from public cloud images, such as `Amazon Machine Images `_ or `Google Cloud Images `_. If the error only happens on your institution's HPC cluster, please contact the system administrator for help. 76 | 77 | For general "how-to" questions that are not bugs, you can also post on `StackOverflow `_ (ref: `xarray questions `_) and send me the link. For small questions also feel free to @ me `on Twitter `_. 78 | 79 | 80 | **The "Don'ts"**: 81 | 82 | - Do not describe your problem in a private email, as this would require me to reply similar emails many times. `Keep all discussions in public places `_ like GitHub or StackOverflow. 83 | - Do not only show the error/problem without providing the steps to reproduce it. 84 | - Do not take screenshots of your code, as they are not copy-pastable. 85 | 86 | 87 | How to support xESMF 88 | -------------------- 89 | 90 | xESMF is so far my personal unfunded project; most development happens during my (very limited) free time at graduate school. Your support in any form will be appreciated. 91 | 92 | The easy ways (takes several seconds): 93 | 94 | - `Give a star `_ to its `GitHub repository `_. 95 | - Share it via social media like Twitter; introduce it to your friends/advisors/students. 96 | 97 | More advanced ways: 98 | 99 | - Cite xESMF in your scientific publications. Currently the best way is to cite the DOI: https://doi.org/10.5281/zenodo.1134365. 100 | - If you'd like to contribute code, see this `preliminary contributor guide `_. Also see `Contributing to xarray `_ for more backgrounds. 101 | -------------------------------------------------------------------------------- /doc/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation-label: 2 | 3 | Installation 4 | ============ 5 | 6 | Try on Binder without local installation 7 | ---------------------------------------- 8 | 9 | The `Binder project `_ provides pre-configured environment in the cloud. You just need a web browser to access it. Please follow the Binder link on `xESMF's GitHub page `_. 10 | 11 | Install on local machine with Conda 12 | ----------------------------------- 13 | 14 | xESMF requires Python>=3.5. The major dependencies are xarray and ESMPy. The best way to install them is using Conda_. 15 | 16 | First, `install miniconda `_. Then, I recommend creating a new, clean environment: 17 | 18 | .. code-block:: bash 19 | 20 | $ conda create -n xesmf_env python=3.7 21 | $ conda activate xesmf_env 22 | 23 | Getting xESMF is as simple as: 24 | 25 | .. code-block:: bash 26 | 27 | $ conda install -c conda-forge xesmf 28 | 29 | .. warning:: 30 | 31 | One some platforms you might get :code:`ImportError: Regrid(filename) requires PIO and does not work if ESMF has not been built with MPI support`. (see `this comment `_). A quick workaround is to constrain ESMPy version :code:`conda install -c conda-forge xesmf esmpy=7.1.0`. 32 | 33 | I also highly recommend those extra packages for full functionality: 34 | 35 | .. code-block:: bash 36 | 37 | # to support all features in xESMF 38 | $ conda install -c conda-forge dask netCDF4 39 | 40 | # optional dependencies for executing all notebook examples 41 | $ conda install -c conda-forge matplotlib cartopy jupyterlab 42 | 43 | Alternatively, you can first install dependencies, and then use ``pip`` to install xESMF: 44 | 45 | .. code-block:: bash 46 | 47 | $ conda install -c conda-forge esmpy xarray scipy dask netCDF4 48 | $ pip install xesmf 49 | 50 | Testing your installation 51 | ------------------------- 52 | 53 | xESMF itself is a lightweight package, but its dependency ESMPy is a quite heavy and sometimes might be installed incorrectly. To validate & debug your installation, you can use pytest to run the test suites: 54 | 55 | .. code-block:: bash 56 | 57 | $ pip install pytest 58 | $ pytest -v --pyargs xesmf # should all pass 59 | 60 | A common cause of error (especially for HPC cluster users) is that pre-installed modules like NetCDF, MPI, and ESMF are incompatible with the conda-installed equivalents. Make sure you have a clean environment when running ``conda install`` (do not ``module load`` other libraries). See `this issue `_ for more discussions. 61 | 62 | Notes for Windows users 63 | ----------------------- 64 | 65 | The ESMPy conda package is currently only available for Linux and Mac OSX. 66 | Windows users can try the 67 | `Linux subsystem `_ 68 | or `docker-miniconda `_ . 69 | 70 | Installing scientific software on Windows can often be a pain, and 71 | `Docker `_ is a pretty good workaround. 72 | It takes some time to learn but worths the effort. 73 | Check out this `tutorial on using Docker with Anaconda 74 | `_. 76 | 77 | This problem is being investigated. 78 | See `this issue `_. 79 | 80 | Install development version from GitHub repo 81 | -------------------------------------------- 82 | 83 | To get the latest version that is not uploaded to PyPI_ yet:: 84 | 85 | $ pip install --upgrade git+https://github.com/JiaweiZhuang/xESMF.git 86 | 87 | Developers can track source code change:: 88 | 89 | $ git clone https://github.com/JiaweiZhuang/xESMF.git 90 | $ cd xESMF 91 | $ pip install -e . 92 | 93 | .. _xarray: http://xarray.pydata.org 94 | .. _ESMPy: https://www.earthsystemcog.org/projects/esmpy/ 95 | .. _Conda: https://docs.conda.io/ 96 | .. _PyPI: https://pypi.python.org/pypi 97 | .. _NESII: https://www.esrl.noaa.gov/gsd/nesii/ 98 | -------------------------------------------------------------------------------- /doc/internal_api.rst: -------------------------------------------------------------------------------- 1 | Internal API 2 | ############ 3 | 4 | frontend 5 | ======== 6 | 7 | .. automodule:: xesmf.frontend 8 | :members: 9 | 10 | backend 11 | ======= 12 | 13 | .. automodule:: xesmf.backend 14 | :members: 15 | 16 | smm 17 | === 18 | 19 | .. automodule:: xesmf.smm 20 | :members: 21 | -------------------------------------------------------------------------------- /doc/limitations.rst: -------------------------------------------------------------------------------- 1 | Current limitations 2 | =================== 3 | 4 | .. _irregular_meshes-label: 5 | 6 | Irregular meshes 7 | ---------------- 8 | 9 | xESMF only supports quadrilateral grids and doesn't support weirder grids 10 | like triangular or hexagonal meshes. 11 | 12 | ESMPy is actually able to deal with general irregular meshes 13 | (`example `_), 15 | but designing an elegant front-end for that is very challenging. 16 | Plain 2D arrays cannot describe irregular meshes. 17 | There needs to be additional information for connectivitity, as suggested by 18 | `UGRID Conventions `_. 19 | 20 | xarray's data model, although powerful, can only describe quadrilateral grids 21 | (including multi-tile quadrilateral grids like the cubed-sphere). 22 | If there is an elegant data model in Python for irregular meshes, 23 | interfacing that with ESMPy should not be super difficult. 24 | 25 | Vector regridding 26 | ----------------- 27 | 28 | Like almost all regridding packages, xESMF assumes scalar fields. 29 | The most common way to remap winds is to rotate/re-decompose the 30 | wind components (U and V) to the new direction, 31 | and then regrid each component individually using a scalar regridding function. 32 | 33 | Exact conservation of vector properities (like divergence and vorticity) 34 | is beyond the scope of almost all regridding packages. 35 | Using bilinear algorithm on each component should lead to OK results in most cases. 36 | -------------------------------------------------------------------------------- /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=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=xESMF 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /doc/notebooks/Backend.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# xESMF backend usage and benchmark" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "xESMF isn't just a wrapper of ESMPy. It only uses ESMPy to generate regridding weights, but has its own Scipy-based method for applying weights (see [more about regridding weights](./Reuse_regridder.ipynb#Why-applying-regridding-is-so-fast?)). \n", 15 | "\n", 16 | "We switch to the Scipy method because its serial performance is much higher than ESMPy's own engine and can also reuse weights ([issue#2](https://github.com/JiaweiZhuang/xESMF/issues/2)). ESMPy's native method is available in the backend, mainly for benchmarking Scipy results in unit tests.\n", 17 | "\n", 18 | "Here we show how to use xESMF backend and compare the performance of two methods. Note that the backend is still pretty easy to use compared to the original ESMPy -- it just doesn't have a fancy API and cannot deal with xarray metadata." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 1, 24 | "metadata": { 25 | "collapsed": true 26 | }, 27 | "outputs": [], 28 | "source": [ 29 | "import os\n", 30 | "import numpy as np\n", 31 | "import xesmf as xe\n", 32 | "\n", 33 | "# backend functions\n", 34 | "from xesmf.backend import (esmf_grid, esmf_regrid_build, \n", 35 | " esmf_regrid_apply, esmf_regrid_finalize)\n", 36 | "from xesmf.smm import read_weights, apply_weights" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "## Prepare data" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "We use the same data as in the [reusing regridder example](./Reuse_regridder.ipynb), but convert xarray DataSet to pure numpy arrays to work with the backend." 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 2, 56 | "metadata": { 57 | "collapsed": true 58 | }, 59 | "outputs": [], 60 | "source": [ 61 | "ds_in = xe.util.grid_2d(-120, 120, 0.4, # longitude range and resolution\n", 62 | " -60, 60, 0.3) # latitude range and resolution\n", 63 | "ds_out = xe.util.grid_2d(-120, 120, 0.6,\n", 64 | " -60, 60, 0.4)\n", 65 | "ds_in.coords['time'] = np.arange(1, 11)\n", 66 | "ds_in.coords['lev'] = np.arange(1, 51)\n", 67 | "ds_in['data2D'] = xe.data.wave_smooth(ds_in['lon'], ds_in['lat'])\n", 68 | "ds_in['data4D'] = ds_in['time'] * ds_in['lev'] * ds_in['data2D']" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": 3, 74 | "metadata": {}, 75 | "outputs": [ 76 | { 77 | "data": { 78 | "text/plain": [ 79 | "(10, 50, 400, 600)" 80 | ] 81 | }, 82 | "execution_count": 3, 83 | "metadata": {}, 84 | "output_type": "execute_result" 85 | } 86 | ], 87 | "source": [ 88 | "# backend only accepts pure numpy array\n", 89 | "lon_in = ds_in['lon'].values\n", 90 | "lat_in = ds_in['lat'].values\n", 91 | "\n", 92 | "lon_out = ds_out['lon'].values\n", 93 | "lat_out = ds_out['lat'].values\n", 94 | "\n", 95 | "data_in = ds_in['data4D'].values\n", 96 | "data_in.shape" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "## Make ESMF Grid objects" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": 4, 109 | "metadata": { 110 | "collapsed": true 111 | }, 112 | "outputs": [], 113 | "source": [ 114 | "grid_in = esmf_grid(lon_in.T, lat_in.T)\n", 115 | "grid_out = esmf_grid(lon_out.T, lat_out.T)" 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "metadata": {}, 121 | "source": [ 122 | "This is a native ESMPy Grid object:" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": 5, 128 | "metadata": {}, 129 | "outputs": [ 130 | { 131 | "data": { 132 | "text/plain": [ 133 | "ESMF.api.grid.Grid" 134 | ] 135 | }, 136 | "execution_count": 5, 137 | "metadata": {}, 138 | "output_type": "execute_result" 139 | } 140 | ], 141 | "source": [ 142 | "type(grid_in)" 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "metadata": {}, 148 | "source": [ 149 | "We pass the transpose (`lon.T`) because ESMPy prefer Fortran-ordering to C-ordering (see this [issue](https://github.com/nawendt/esmpy-tutorial/issues/4))." 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": 6, 155 | "metadata": {}, 156 | "outputs": [ 157 | { 158 | "data": { 159 | "text/plain": [ 160 | " C_CONTIGUOUS : True\n", 161 | " F_CONTIGUOUS : False\n", 162 | " OWNDATA : False\n", 163 | " WRITEABLE : True\n", 164 | " ALIGNED : True\n", 165 | " UPDATEIFCOPY : False" 166 | ] 167 | }, 168 | "execution_count": 6, 169 | "metadata": {}, 170 | "output_type": "execute_result" 171 | } 172 | ], 173 | "source": [ 174 | "lon_in.flags # numpy arrays are mostly C-ordered" 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": 7, 180 | "metadata": {}, 181 | "outputs": [ 182 | { 183 | "data": { 184 | "text/plain": [ 185 | " C_CONTIGUOUS : False\n", 186 | " F_CONTIGUOUS : True\n", 187 | " OWNDATA : False\n", 188 | " WRITEABLE : True\n", 189 | " ALIGNED : True\n", 190 | " UPDATEIFCOPY : False" 191 | ] 192 | }, 193 | "execution_count": 7, 194 | "metadata": {}, 195 | "output_type": "execute_result" 196 | } 197 | ], 198 | "source": [ 199 | "lon_in.T.flags # a memory view on its tranpose would be Fortran-ordered" 200 | ] 201 | }, 202 | { 203 | "cell_type": "markdown", 204 | "metadata": {}, 205 | "source": [ 206 | "## Compute weights" 207 | ] 208 | }, 209 | { 210 | "cell_type": "code", 211 | "execution_count": 8, 212 | "metadata": { 213 | "collapsed": true 214 | }, 215 | "outputs": [], 216 | "source": [ 217 | "filename = 'test_weights.nc' # weight filename\n", 218 | "if os.path.exists(filename):\n", 219 | " os.remove(filename) # ESMPy will crash if the file exists" 220 | ] 221 | }, 222 | { 223 | "cell_type": "markdown", 224 | "metadata": {}, 225 | "source": [ 226 | "Computing weights takes ~7s, as in the [reusing regridder example](./Reuse_regridder.ipynb#Build-Regridder). " 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": 9, 232 | "metadata": {}, 233 | "outputs": [ 234 | { 235 | "name": "stdout", 236 | "output_type": "stream", 237 | "text": [ 238 | "CPU times: user 7.06 s, sys: 382 ms, total: 7.44 s\n", 239 | "Wall time: 7.57 s\n" 240 | ] 241 | } 242 | ], 243 | "source": [ 244 | "%%time\n", 245 | "regrid = esmf_regrid_build(grid_in, grid_out, 'bilinear', \n", 246 | " extra_dims=[50, 10], # reversed to Fortran-ordering\n", 247 | " filename=filename)" 248 | ] 249 | }, 250 | { 251 | "cell_type": "markdown", 252 | "metadata": {}, 253 | "source": [ 254 | "It returns a native ESMPy Regrid object:" 255 | ] 256 | }, 257 | { 258 | "cell_type": "code", 259 | "execution_count": 10, 260 | "metadata": {}, 261 | "outputs": [ 262 | { 263 | "data": { 264 | "text/plain": [ 265 | "ESMF.api.regrid.Regrid" 266 | ] 267 | }, 268 | "execution_count": 10, 269 | "metadata": {}, 270 | "output_type": "execute_result" 271 | } 272 | ], 273 | "source": [ 274 | "type(regrid)" 275 | ] 276 | }, 277 | { 278 | "cell_type": "markdown", 279 | "metadata": {}, 280 | "source": [ 281 | "It also writes weights to disk so we can then read them back for Scipy." 282 | ] 283 | }, 284 | { 285 | "cell_type": "code", 286 | "execution_count": 11, 287 | "metadata": {}, 288 | "outputs": [ 289 | { 290 | "name": "stdout", 291 | "output_type": "stream", 292 | "text": [ 293 | "netcdf test_weights {\n", 294 | "dimensions:\n", 295 | "\tn_s = 480000 ;\n", 296 | "variables:\n", 297 | "\tdouble S(n_s) ;\n", 298 | "\tint col(n_s) ;\n", 299 | "\tint row(n_s) ;\n", 300 | "}\n" 301 | ] 302 | } 303 | ], 304 | "source": [ 305 | "%%bash\n", 306 | "ncdump -h test_weights.nc" 307 | ] 308 | }, 309 | { 310 | "cell_type": "markdown", 311 | "metadata": {}, 312 | "source": [ 313 | "## Apply weights using ESMPy backend" 314 | ] 315 | }, 316 | { 317 | "cell_type": "markdown", 318 | "metadata": {}, 319 | "source": [ 320 | "It takes ~3s with ESMPy's native method." 321 | ] 322 | }, 323 | { 324 | "cell_type": "code", 325 | "execution_count": 12, 326 | "metadata": {}, 327 | "outputs": [ 328 | { 329 | "name": "stdout", 330 | "output_type": "stream", 331 | "text": [ 332 | "CPU times: user 2.35 s, sys: 662 ms, total: 3.01 s\n", 333 | "Wall time: 3.09 s\n" 334 | ] 335 | } 336 | ], 337 | "source": [ 338 | "%%time\n", 339 | "data_out_esmpy = esmf_regrid_apply(regrid, data_in.T).T" 340 | ] 341 | }, 342 | { 343 | "cell_type": "markdown", 344 | "metadata": {}, 345 | "source": [ 346 | "The first `.T` converts C-ordering to F-ordering for ESMPy, and the second `.T` converts the result back to C-ordering. It just gets a memory view and thus incurs almost no overhead." 347 | ] 348 | }, 349 | { 350 | "cell_type": "code", 351 | "execution_count": 13, 352 | "metadata": {}, 353 | "outputs": [ 354 | { 355 | "data": { 356 | "text/plain": [ 357 | " C_CONTIGUOUS : True\n", 358 | " F_CONTIGUOUS : False\n", 359 | " OWNDATA : False\n", 360 | " WRITEABLE : True\n", 361 | " ALIGNED : True\n", 362 | " UPDATEIFCOPY : False" 363 | ] 364 | }, 365 | "execution_count": 13, 366 | "metadata": {}, 367 | "output_type": "execute_result" 368 | } 369 | ], 370 | "source": [ 371 | "data_out_esmpy.flags" 372 | ] 373 | }, 374 | { 375 | "cell_type": "code", 376 | "execution_count": 14, 377 | "metadata": {}, 378 | "outputs": [ 379 | { 380 | "data": { 381 | "text/plain": [ 382 | "(10, 50, 300, 400)" 383 | ] 384 | }, 385 | "execution_count": 14, 386 | "metadata": {}, 387 | "output_type": "execute_result" 388 | } 389 | ], 390 | "source": [ 391 | "data_out_esmpy.shape # broadcasted over extra dimensions" 392 | ] 393 | }, 394 | { 395 | "cell_type": "markdown", 396 | "metadata": {}, 397 | "source": [ 398 | "## Apply weights using Scipy backend" 399 | ] 400 | }, 401 | { 402 | "cell_type": "markdown", 403 | "metadata": {}, 404 | "source": [ 405 | "Read weights back for Scipy. `read_weights` needs to know the shape of the sparse matrix, i.e. how many points in input and output grids." 406 | ] 407 | }, 408 | { 409 | "cell_type": "code", 410 | "execution_count": 15, 411 | "metadata": {}, 412 | "outputs": [ 413 | { 414 | "data": { 415 | "text/plain": [ 416 | "<120000x240000 sparse matrix of type ''\n", 417 | "\twith 480000 stored elements in COOrdinate format>" 418 | ] 419 | }, 420 | "execution_count": 15, 421 | "metadata": {}, 422 | "output_type": "execute_result" 423 | } 424 | ], 425 | "source": [ 426 | "weights = read_weights(filename, lon_in.size, lon_out.size)\n", 427 | "weights" 428 | ] 429 | }, 430 | { 431 | "cell_type": "markdown", 432 | "metadata": {}, 433 | "source": [ 434 | "`apply_weights` needs to know shape of the output grid." 435 | ] 436 | }, 437 | { 438 | "cell_type": "code", 439 | "execution_count": 16, 440 | "metadata": {}, 441 | "outputs": [ 442 | { 443 | "data": { 444 | "text/plain": [ 445 | "(300, 400)" 446 | ] 447 | }, 448 | "execution_count": 16, 449 | "metadata": {}, 450 | "output_type": "execute_result" 451 | } 452 | ], 453 | "source": [ 454 | "lon_out.shape" 455 | ] 456 | }, 457 | { 458 | "cell_type": "code", 459 | "execution_count": 17, 460 | "metadata": {}, 461 | "outputs": [ 462 | { 463 | "name": "stdout", 464 | "output_type": "stream", 465 | "text": [ 466 | "CPU times: user 443 ms, sys: 165 ms, total: 609 ms\n", 467 | "Wall time: 620 ms\n" 468 | ] 469 | } 470 | ], 471 | "source": [ 472 | "%%time\n", 473 | "data_out_scipy = apply_weights(weights, data_in, lon_in.shape, lon_out.shape)" 474 | ] 475 | }, 476 | { 477 | "cell_type": "markdown", 478 | "metadata": {}, 479 | "source": [ 480 | "It is several times faster than ESMPy's native method. The conclusion seems to be pretty robust across different platforms (feel free to verify on your own), so we choose Scipy as the default backend. \n", 481 | "\n", 482 | "A likely explanation for this performance discrepancy is, the original ESMF is optimized for large processor counts (~1000 CPUs) at the expense of serial performance (ESMF team, personal communication)." 483 | ] 484 | }, 485 | { 486 | "cell_type": "code", 487 | "execution_count": 18, 488 | "metadata": {}, 489 | "outputs": [ 490 | { 491 | "data": { 492 | "text/plain": [ 493 | "(10, 50, 300, 400)" 494 | ] 495 | }, 496 | "execution_count": 18, 497 | "metadata": {}, 498 | "output_type": "execute_result" 499 | } 500 | ], 501 | "source": [ 502 | "data_out_scipy.shape # broadcasted over extra dimensions" 503 | ] 504 | }, 505 | { 506 | "cell_type": "code", 507 | "execution_count": 19, 508 | "metadata": { 509 | "collapsed": true 510 | }, 511 | "outputs": [], 512 | "source": [ 513 | "np.testing.assert_equal(data_out_scipy, data_out_esmpy) # exactly the same" 514 | ] 515 | }, 516 | { 517 | "cell_type": "code", 518 | "execution_count": 20, 519 | "metadata": { 520 | "collapsed": true 521 | }, 522 | "outputs": [], 523 | "source": [ 524 | "os.remove(filename) # clean-up" 525 | ] 526 | } 527 | ], 528 | "metadata": { 529 | "kernelspec": { 530 | "display_name": "Python 3", 531 | "language": "python", 532 | "name": "python3" 533 | }, 534 | "language_info": { 535 | "codemirror_mode": { 536 | "name": "ipython", 537 | "version": 3 538 | }, 539 | "file_extension": ".py", 540 | "mimetype": "text/x-python", 541 | "name": "python", 542 | "nbconvert_exporter": "python", 543 | "pygments_lexer": "ipython3", 544 | "version": "3.6.2" 545 | }, 546 | "toc": { 547 | "nav_menu": {}, 548 | "number_sections": true, 549 | "sideBar": true, 550 | "skip_h1_title": false, 551 | "toc_cell": false, 552 | "toc_position": {}, 553 | "toc_section_display": "block", 554 | "toc_window_display": false 555 | } 556 | }, 557 | "nbformat": 4, 558 | "nbformat_minor": 2 559 | } 560 | -------------------------------------------------------------------------------- /doc/notebooks/Dask.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Lazy evaluation on Dask arrays" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "If you are unfamiliar with Dask, read [Parallel computing with Dask](http://xarray.pydata.org/en/stable/dask.html) in Xarray documentation first. The current version only supports dask arrays on a single machine. Support of [Dask.distributed](https://distributed.dask.org) is in roadmap.\n", 15 | "\n", 16 | "xESMF's Dask support is mostly for [lazy evaluation](https://en.wikipedia.org/wiki/Lazy_evaluation) and [out-of-core computing](https://en.wikipedia.org/wiki/External_memory_algorithm), to allow processing large volumes of data with limited memory. You might also get moderate speed-up on a multi-core machine by [choosing proper chunk sizes](http://xarray.pydata.org/en/stable/dask.html#chunking-and-performance), but that generally won't help your entire pipeline too much, because the read-regrid-write pipeline is severely I/O limited (see [this issue](https://github.com/pangeo-data/pangeo/issues/334) for more discussions). On a single machine, the disk bandwidth is typically limited to ~500 MB/s, and you cannot process data faster than such rate. If you need much faster data processing rate, you should resort to parallel file systems on HPC clusters or distributed storage on public cloud platforms. Please refer to the [Pangeo project](http://pangeo.io/) for more information." 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 1, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "%matplotlib inline\n", 26 | "import matplotlib.pyplot as plt\n", 27 | "import numpy as np\n", 28 | "import dask.array as da # need to have dask.array installed, although not directly using it here.\n", 29 | "import xarray as xr\n", 30 | "import xesmf as xe" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "metadata": {}, 36 | "source": [ 37 | "## A simple example" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "### Prepare input data" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": 2, 50 | "metadata": {}, 51 | "outputs": [ 52 | { 53 | "data": { 54 | "text/plain": [ 55 | "\n", 56 | "Dimensions: (lat: 25, lon: 53, time: 2920)\n", 57 | "Coordinates:\n", 58 | " * lat (lat) float32 75.0 72.5 70.0 67.5 65.0 ... 25.0 22.5 20.0 17.5 15.0\n", 59 | " * lon (lon) float32 200.0 202.5 205.0 207.5 ... 322.5 325.0 327.5 330.0\n", 60 | " * time (time) datetime64[ns] 2013-01-01 ... 2014-12-31T18:00:00\n", 61 | "Data variables:\n", 62 | " air (time, lat, lon) float32 dask.array\n", 63 | "Attributes:\n", 64 | " Conventions: COARDS\n", 65 | " title: 4x daily NMC reanalysis (1948)\n", 66 | " description: Data is from NMC initialized reanalysis\\n(4x/day). These a...\n", 67 | " platform: Model\n", 68 | " references: http://www.esrl.noaa.gov/psd/data/gridded/data.ncep.reanaly..." 69 | ] 70 | }, 71 | "execution_count": 2, 72 | "metadata": {}, 73 | "output_type": "execute_result" 74 | } 75 | ], 76 | "source": [ 77 | "ds = xr.tutorial.open_dataset('air_temperature', chunks={'time': 500})\n", 78 | "ds" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 3, 84 | "metadata": {}, 85 | "outputs": [ 86 | { 87 | "data": { 88 | "text/plain": [ 89 | "Frozen(SortedKeysDict({'time': (500, 500, 500, 500, 500, 420), 'lat': (25,), 'lon': (53,)}))" 90 | ] 91 | }, 92 | "execution_count": 3, 93 | "metadata": {}, 94 | "output_type": "execute_result" 95 | } 96 | ], 97 | "source": [ 98 | "ds.chunks" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": 4, 104 | "metadata": {}, 105 | "outputs": [ 106 | { 107 | "data": { 108 | "text/html": [ 109 | "\n", 110 | "\n", 111 | "\n", 121 | "\n", 173 | "\n", 174 | "
\n", 112 | "\n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | "
Array Chunk
Bytes 15.48 MB 2.65 MB
Shape (2920, 25, 53) (500, 25, 53)
Count 7 Tasks 6 Chunks
Type float32 numpy.ndarray
\n", 120 | "
\n", 122 | "\n", 123 | "\n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | "\n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | "\n", 137 | " \n", 138 | " \n", 139 | "\n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | "\n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | "\n", 153 | " \n", 154 | " \n", 155 | "\n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | "\n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | "\n", 164 | " \n", 165 | " \n", 166 | "\n", 167 | " \n", 168 | " 53\n", 169 | " 25\n", 170 | " 2920\n", 171 | "\n", 172 | "
" 175 | ], 176 | "text/plain": [ 177 | "dask.array" 178 | ] 179 | }, 180 | "execution_count": 4, 181 | "metadata": {}, 182 | "output_type": "execute_result" 183 | } 184 | ], 185 | "source": [ 186 | "ds['air'].data" 187 | ] 188 | }, 189 | { 190 | "cell_type": "markdown", 191 | "metadata": {}, 192 | "source": [ 193 | "### Build regridder" 194 | ] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "execution_count": 5, 199 | "metadata": {}, 200 | "outputs": [ 201 | { 202 | "name": "stdout", 203 | "output_type": "stream", 204 | "text": [ 205 | "Create weight file: bilinear_25x53_59x87.nc\n", 206 | "Remove file bilinear_25x53_59x87.nc\n" 207 | ] 208 | }, 209 | { 210 | "data": { 211 | "text/plain": [ 212 | "xESMF Regridder \n", 213 | "Regridding algorithm: bilinear \n", 214 | "Weight filename: bilinear_25x53_59x87.nc \n", 215 | "Reuse pre-computed weights? False \n", 216 | "Input grid shape: (25, 53) \n", 217 | "Output grid shape: (59, 87) \n", 218 | "Output grid dimension name: ('lat', 'lon') \n", 219 | "Periodic in longitude? False" 220 | ] 221 | }, 222 | "execution_count": 5, 223 | "metadata": {}, 224 | "output_type": "execute_result" 225 | } 226 | ], 227 | "source": [ 228 | "ds_out = xr.Dataset({'lat': (['lat'], np.arange(16, 75, 1.0)),\n", 229 | " 'lon': (['lon'], np.arange(200, 330, 1.5)),\n", 230 | " }\n", 231 | " )\n", 232 | "\n", 233 | "regridder = xe.Regridder(ds, ds_out, 'bilinear')\n", 234 | "regridder.clean_weight_file()\n", 235 | "regridder" 236 | ] 237 | }, 238 | { 239 | "cell_type": "markdown", 240 | "metadata": {}, 241 | "source": [ 242 | "### Apply to xarray Dataset/DataArray" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": 6, 248 | "metadata": {}, 249 | "outputs": [ 250 | { 251 | "name": "stdout", 252 | "output_type": "stream", 253 | "text": [ 254 | "using dimensions ('lat', 'lon') from data variable air as the horizontal dimensions for this dataset.\n", 255 | "CPU times: user 17 ms, sys: 4.58 ms, total: 21.6 ms\n", 256 | "Wall time: 18.8 ms\n" 257 | ] 258 | }, 259 | { 260 | "data": { 261 | "text/plain": [ 262 | "\n", 263 | "Dimensions: (lat: 59, lon: 87, time: 2920)\n", 264 | "Coordinates:\n", 265 | " * time (time) datetime64[ns] 2013-01-01 ... 2014-12-31T18:00:00\n", 266 | " * lon (lon) float64 200.0 201.5 203.0 204.5 ... 324.5 326.0 327.5 329.0\n", 267 | " * lat (lat) float64 16.0 17.0 18.0 19.0 20.0 ... 70.0 71.0 72.0 73.0 74.0\n", 268 | "Data variables:\n", 269 | " air (time, lat, lon) float64 dask.array\n", 270 | "Attributes:\n", 271 | " regrid_method: bilinear" 272 | ] 273 | }, 274 | "execution_count": 6, 275 | "metadata": {}, 276 | "output_type": "execute_result" 277 | } 278 | ], 279 | "source": [ 280 | "# only build the dask graph; actual computation happens later when calling compute()\n", 281 | "%time ds_out = regridder(ds)\n", 282 | "ds_out" 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": 7, 288 | "metadata": {}, 289 | "outputs": [ 290 | { 291 | "data": { 292 | "text/html": [ 293 | "\n", 294 | "\n", 295 | "\n", 305 | "\n", 357 | "\n", 358 | "
\n", 296 | "\n", 297 | " \n", 298 | " \n", 299 | " \n", 300 | " \n", 301 | " \n", 302 | " \n", 303 | "
Array Chunk
Bytes 119.91 MB 20.53 MB
Shape (2920, 59, 87) (500, 59, 87)
Count 13 Tasks 6 Chunks
Type float64 numpy.ndarray
\n", 304 | "
\n", 306 | "\n", 307 | "\n", 308 | " \n", 309 | " \n", 310 | " \n", 311 | "\n", 312 | " \n", 313 | " \n", 314 | " \n", 315 | " \n", 316 | " \n", 317 | " \n", 318 | " \n", 319 | " \n", 320 | "\n", 321 | " \n", 322 | " \n", 323 | "\n", 324 | " \n", 325 | " \n", 326 | " \n", 327 | " \n", 328 | " \n", 329 | " \n", 330 | " \n", 331 | " \n", 332 | "\n", 333 | " \n", 334 | " \n", 335 | " \n", 336 | "\n", 337 | " \n", 338 | " \n", 339 | "\n", 340 | " \n", 341 | " \n", 342 | " \n", 343 | "\n", 344 | " \n", 345 | " \n", 346 | " \n", 347 | "\n", 348 | " \n", 349 | " \n", 350 | "\n", 351 | " \n", 352 | " 87\n", 353 | " 59\n", 354 | " 2920\n", 355 | "\n", 356 | "
" 359 | ], 360 | "text/plain": [ 361 | "dask.array" 362 | ] 363 | }, 364 | "execution_count": 7, 365 | "metadata": {}, 366 | "output_type": "execute_result" 367 | } 368 | ], 369 | "source": [ 370 | "ds_out['air'].data # chunks are preserved" 371 | ] 372 | }, 373 | { 374 | "cell_type": "code", 375 | "execution_count": 8, 376 | "metadata": {}, 377 | "outputs": [ 378 | { 379 | "name": "stdout", 380 | "output_type": "stream", 381 | "text": [ 382 | "CPU times: user 310 ms, sys: 619 ms, total: 929 ms\n", 383 | "Wall time: 389 ms\n" 384 | ] 385 | } 386 | ], 387 | "source": [ 388 | "%time result = ds_out['air'].compute() # actually applies regridding" 389 | ] 390 | }, 391 | { 392 | "cell_type": "code", 393 | "execution_count": 9, 394 | "metadata": {}, 395 | "outputs": [ 396 | { 397 | "data": { 398 | "text/plain": [ 399 | "(numpy.ndarray, (2920, 59, 87))" 400 | ] 401 | }, 402 | "execution_count": 9, 403 | "metadata": {}, 404 | "output_type": "execute_result" 405 | } 406 | ], 407 | "source": [ 408 | "type(result.data), result.data.shape" 409 | ] 410 | }, 411 | { 412 | "cell_type": "markdown", 413 | "metadata": {}, 414 | "source": [ 415 | "## Invalid chunk sizes to avoid" 416 | ] 417 | }, 418 | { 419 | "cell_type": "markdown", 420 | "metadata": {}, 421 | "source": [ 422 | "Dask support relies on `xarray.apply_ufunc`, which requires only chunking over extra/broadcasting dimensions (like `time` and `lev`), not horizontal/core dimensions (`lat`, `lon`, or `x`, `y`)." 423 | ] 424 | }, 425 | { 426 | "cell_type": "code", 427 | "execution_count": 10, 428 | "metadata": {}, 429 | "outputs": [ 430 | { 431 | "data": { 432 | "text/plain": [ 433 | "\n", 434 | "Dimensions: (lat: 25, lon: 53, time: 2920)\n", 435 | "Coordinates:\n", 436 | " * lat (lat) float32 75.0 72.5 70.0 67.5 65.0 ... 25.0 22.5 20.0 17.5 15.0\n", 437 | " * lon (lon) float32 200.0 202.5 205.0 207.5 ... 322.5 325.0 327.5 330.0\n", 438 | " * time (time) datetime64[ns] 2013-01-01 ... 2014-12-31T18:00:00\n", 439 | "Data variables:\n", 440 | " air (time, lat, lon) float32 dask.array\n", 441 | "Attributes:\n", 442 | " Conventions: COARDS\n", 443 | " title: 4x daily NMC reanalysis (1948)\n", 444 | " description: Data is from NMC initialized reanalysis\\n(4x/day). These a...\n", 445 | " platform: Model\n", 446 | " references: http://www.esrl.noaa.gov/psd/data/gridded/data.ncep.reanaly..." 447 | ] 448 | }, 449 | "execution_count": 10, 450 | "metadata": {}, 451 | "output_type": "execute_result" 452 | } 453 | ], 454 | "source": [ 455 | "# xESMF doesn't like chunking over horizontal dimensions\n", 456 | "ds_bad = ds.chunk({'lat': 10, 'lon': 10, 'time': None})\n", 457 | "ds_bad" 458 | ] 459 | }, 460 | { 461 | "cell_type": "code", 462 | "execution_count": 11, 463 | "metadata": {}, 464 | "outputs": [], 465 | "source": [ 466 | "# regridder(ds_bad) # uncomment this line to see the error message" 467 | ] 468 | }, 469 | { 470 | "cell_type": "code", 471 | "execution_count": 12, 472 | "metadata": {}, 473 | "outputs": [ 474 | { 475 | "name": "stdout", 476 | "output_type": "stream", 477 | "text": [ 478 | "using dimensions ('lat', 'lon') from data variable air as the horizontal dimensions for this dataset.\n" 479 | ] 480 | }, 481 | { 482 | "data": { 483 | "text/plain": [ 484 | "\n", 485 | "Dimensions: (lat: 59, lon: 87, time: 2920)\n", 486 | "Coordinates:\n", 487 | " * time (time) datetime64[ns] 2013-01-01 ... 2014-12-31T18:00:00\n", 488 | " * lon (lon) float64 200.0 201.5 203.0 204.5 ... 324.5 326.0 327.5 329.0\n", 489 | " * lat (lat) float64 16.0 17.0 18.0 19.0 20.0 ... 70.0 71.0 72.0 73.0 74.0\n", 490 | "Data variables:\n", 491 | " air (time, lat, lon) float64 296.1 296.4 296.6 ... 240.9 241.0 241.5\n", 492 | "Attributes:\n", 493 | " regrid_method: bilinear" 494 | ] 495 | }, 496 | "execution_count": 12, 497 | "metadata": {}, 498 | "output_type": "execute_result" 499 | } 500 | ], 501 | "source": [ 502 | "# besides rechunking data properly, another simple fix is to convert to numpy array without chunking\n", 503 | "regridder(ds_bad.load())" 504 | ] 505 | } 506 | ], 507 | "metadata": { 508 | "kernelspec": { 509 | "display_name": "Python 3", 510 | "language": "python", 511 | "name": "python3" 512 | }, 513 | "language_info": { 514 | "codemirror_mode": { 515 | "name": "ipython", 516 | "version": 3 517 | }, 518 | "file_extension": ".py", 519 | "mimetype": "text/x-python", 520 | "name": "python", 521 | "nbconvert_exporter": "python", 522 | "pygments_lexer": "ipython3", 523 | "version": "3.7.3" 524 | } 525 | }, 526 | "nbformat": 4, 527 | "nbformat_minor": 4 528 | } 529 | -------------------------------------------------------------------------------- /doc/notebooks/Dataset.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Regrid xarray Dataset with multiple variables" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "%matplotlib inline\n", 17 | "import matplotlib.pyplot as plt\n", 18 | "import numpy as np\n", 19 | "import xarray as xr\n", 20 | "import xesmf as xe" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "Starting v0.2.0, xESMF is able to take `xarray.Dataset` as input data, and automatically loop over all variables." 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "## A simple example" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "### Prepare input data" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 2, 47 | "metadata": {}, 48 | "outputs": [ 49 | { 50 | "data": { 51 | "text/plain": [ 52 | "\n", 53 | "Dimensions: (lat: 25, lon: 53, time: 2920)\n", 54 | "Coordinates:\n", 55 | " * lat (lat) float32 75.0 72.5 70.0 67.5 65.0 ... 25.0 22.5 20.0 17.5 15.0\n", 56 | " * lon (lon) float32 200.0 202.5 205.0 207.5 ... 322.5 325.0 327.5 330.0\n", 57 | " * time (time) datetime64[ns] 2013-01-01 ... 2014-12-31T18:00:00\n", 58 | "Data variables:\n", 59 | " air (time, lat, lon) float32 ...\n", 60 | "Attributes:\n", 61 | " Conventions: COARDS\n", 62 | " title: 4x daily NMC reanalysis (1948)\n", 63 | " description: Data is from NMC initialized reanalysis\\n(4x/day). These a...\n", 64 | " platform: Model\n", 65 | " references: http://www.esrl.noaa.gov/psd/data/gridded/data.ncep.reanaly..." 66 | ] 67 | }, 68 | "execution_count": 2, 69 | "metadata": {}, 70 | "output_type": "execute_result" 71 | } 72 | ], 73 | "source": [ 74 | "ds = xr.tutorial.open_dataset('air_temperature')\n", 75 | "ds # air temperature in Kelvin" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 3, 81 | "metadata": {}, 82 | "outputs": [ 83 | { 84 | "data": { 85 | "text/plain": [ 86 | "\n", 87 | "Dimensions: (lat: 25, lon: 53, time: 2920)\n", 88 | "Coordinates:\n", 89 | " * lat (lat) float32 75.0 72.5 70.0 67.5 65.0 ... 25.0 22.5 20.0 17.5 15.0\n", 90 | " * lon (lon) float32 200.0 202.5 205.0 207.5 ... 322.5 325.0 327.5 330.0\n", 91 | " * time (time) datetime64[ns] 2013-01-01 ... 2014-12-31T18:00:00\n", 92 | "Data variables:\n", 93 | " air (time, lat, lon) float32 241.2 242.5 243.5 ... 296.49 296.19 295.69\n", 94 | " celsius (time, lat, lon) float32 -31.949997 -30.649994 ... 22.540009\n", 95 | " slice (lat, lon) float32 241.2 242.5 243.5 244.0 ... 296.9 296.79 296.6\n", 96 | "Attributes:\n", 97 | " Conventions: COARDS\n", 98 | " title: 4x daily NMC reanalysis (1948)\n", 99 | " description: Data is from NMC initialized reanalysis\\n(4x/day). These a...\n", 100 | " platform: Model\n", 101 | " references: http://www.esrl.noaa.gov/psd/data/gridded/data.ncep.reanaly..." 102 | ] 103 | }, 104 | "execution_count": 3, 105 | "metadata": {}, 106 | "output_type": "execute_result" 107 | } 108 | ], 109 | "source": [ 110 | "# input dataset can contain variables of different shapes (e.g. 2D, 3D, 4D), as long as horizontal shapes are the same. \n", 111 | "ds['celsius'] = ds['air'] - 273.15 # Kelvin -> celsius\n", 112 | "ds['slice'] = ds['air'].isel(time=0)\n", 113 | "ds" 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "metadata": {}, 119 | "source": [ 120 | "### Build regridder" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": 4, 126 | "metadata": {}, 127 | "outputs": [ 128 | { 129 | "name": "stdout", 130 | "output_type": "stream", 131 | "text": [ 132 | "Create weight file: bilinear_25x53_59x87.nc\n", 133 | "Remove file bilinear_25x53_59x87.nc\n" 134 | ] 135 | }, 136 | { 137 | "data": { 138 | "text/plain": [ 139 | "xESMF Regridder \n", 140 | "Regridding algorithm: bilinear \n", 141 | "Weight filename: bilinear_25x53_59x87.nc \n", 142 | "Reuse pre-computed weights? False \n", 143 | "Input grid shape: (25, 53) \n", 144 | "Output grid shape: (59, 87) \n", 145 | "Output grid dimension name: ('lat', 'lon') \n", 146 | "Periodic in longitude? False" 147 | ] 148 | }, 149 | "execution_count": 4, 150 | "metadata": {}, 151 | "output_type": "execute_result" 152 | } 153 | ], 154 | "source": [ 155 | "ds_out = xr.Dataset({'lat': (['lat'], np.arange(16, 75, 1.0)),\n", 156 | " 'lon': (['lon'], np.arange(200, 330, 1.5)),\n", 157 | " }\n", 158 | " )\n", 159 | "\n", 160 | "regridder = xe.Regridder(ds, ds_out, 'bilinear')\n", 161 | "regridder.clean_weight_file()\n", 162 | "regridder" 163 | ] 164 | }, 165 | { 166 | "cell_type": "markdown", 167 | "metadata": {}, 168 | "source": [ 169 | "### Apply to data" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 5, 175 | "metadata": {}, 176 | "outputs": [ 177 | { 178 | "name": "stdout", 179 | "output_type": "stream", 180 | "text": [ 181 | "using dimensions ('lat', 'lon') from data variable air as the horizontal dimensions for this dataset.\n" 182 | ] 183 | }, 184 | { 185 | "data": { 186 | "text/plain": [ 187 | "\n", 188 | "Dimensions: (lat: 59, lon: 87, time: 2920)\n", 189 | "Coordinates:\n", 190 | " * time (time) datetime64[ns] 2013-01-01 ... 2014-12-31T18:00:00\n", 191 | " * lon (lon) float64 200.0 201.5 203.0 204.5 ... 324.5 326.0 327.5 329.0\n", 192 | " * lat (lat) float64 16.0 17.0 18.0 19.0 20.0 ... 70.0 71.0 72.0 73.0 74.0\n", 193 | "Data variables:\n", 194 | " air (time, lat, lon) float64 296.1 296.4 296.6 ... 240.9 241.0 241.5\n", 195 | " celsius (time, lat, lon) float64 22.98 23.24 23.49 ... -32.24 -32.14 -31.7\n", 196 | " slice (lat, lon) float64 296.1 296.4 296.6 296.9 ... 233.8 235.4 237.5\n", 197 | "Attributes:\n", 198 | " regrid_method: bilinear" 199 | ] 200 | }, 201 | "execution_count": 5, 202 | "metadata": {}, 203 | "output_type": "execute_result" 204 | } 205 | ], 206 | "source": [ 207 | "# the entire dataset can be processed at once\n", 208 | "ds_out = regridder(ds)\n", 209 | "ds_out" 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": 6, 215 | "metadata": {}, 216 | "outputs": [ 217 | { 218 | "name": "stdout", 219 | "output_type": "stream", 220 | "text": [ 221 | "air True\n", 222 | "celsius True\n", 223 | "slice True\n" 224 | ] 225 | } 226 | ], 227 | "source": [ 228 | "# verify that the result is the same as regridding each variable one-by-one\n", 229 | "for k in ds.data_vars:\n", 230 | " print(k, ds_out[k].equals(regridder(ds[k])))" 231 | ] 232 | }, 233 | { 234 | "cell_type": "markdown", 235 | "metadata": {}, 236 | "source": [ 237 | "## Invalid dimension orderings to avoid" 238 | ] 239 | }, 240 | { 241 | "cell_type": "markdown", 242 | "metadata": {}, 243 | "source": [ 244 | "xESMF assumes the horizontal dimensions are the last/rightmost dimensions, which matches the convention of most NetCDF data." 245 | ] 246 | }, 247 | { 248 | "cell_type": "code", 249 | "execution_count": 7, 250 | "metadata": {}, 251 | "outputs": [ 252 | { 253 | "data": { 254 | "text/plain": [ 255 | "\n", 256 | "Dimensions: (lat: 25, lon: 53, time: 2920)\n", 257 | "Coordinates:\n", 258 | " * lat (lat) float32 75.0 72.5 70.0 67.5 65.0 ... 25.0 22.5 20.0 17.5 15.0\n", 259 | " * lon (lon) float32 200.0 202.5 205.0 207.5 ... 322.5 325.0 327.5 330.0\n", 260 | " * time (time) datetime64[ns] 2013-01-01 ... 2014-12-31T18:00:00\n", 261 | "Data variables:\n", 262 | " air (lon, lat, time) float32 241.2 242.09999 ... 295.19 295.69\n", 263 | " celsius (time, lat, lon) float32 -31.949997 -30.649994 ... 22.540009\n", 264 | " slice (lat, lon) float32 241.2 242.5 243.5 244.0 ... 296.9 296.79 296.6\n", 265 | "Attributes:\n", 266 | " Conventions: COARDS\n", 267 | " title: 4x daily NMC reanalysis (1948)\n", 268 | " description: Data is from NMC initialized reanalysis\\n(4x/day). These a...\n", 269 | " platform: Model\n", 270 | " references: http://www.esrl.noaa.gov/psd/data/gridded/data.ncep.reanaly..." 271 | ] 272 | }, 273 | "execution_count": 7, 274 | "metadata": {}, 275 | "output_type": "execute_result" 276 | } 277 | ], 278 | "source": [ 279 | "# xESMF doesn't like horizontal dimensions to be the first/leftmost dimensions\n", 280 | "ds_bad = ds.copy()\n", 281 | "ds_bad['air'] = ds_bad['air'].transpose()\n", 282 | "ds_bad" 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": 8, 288 | "metadata": {}, 289 | "outputs": [], 290 | "source": [ 291 | "# regridder(ds_bad) # comment this line to see the error message" 292 | ] 293 | }, 294 | { 295 | "cell_type": "code", 296 | "execution_count": 9, 297 | "metadata": {}, 298 | "outputs": [ 299 | { 300 | "name": "stdout", 301 | "output_type": "stream", 302 | "text": [ 303 | "using dimensions ('lat', 'lon') from data variable celsius as the horizontal dimensions for this dataset.\n" 304 | ] 305 | }, 306 | { 307 | "data": { 308 | "text/plain": [ 309 | "\n", 310 | "Dimensions: (lat: 59, lon: 87, time: 2920)\n", 311 | "Coordinates:\n", 312 | " * time (time) datetime64[ns] 2013-01-01 ... 2014-12-31T18:00:00\n", 313 | " * lon (lon) float64 200.0 201.5 203.0 204.5 ... 324.5 326.0 327.5 329.0\n", 314 | " * lat (lat) float64 16.0 17.0 18.0 19.0 20.0 ... 70.0 71.0 72.0 73.0 74.0\n", 315 | "Data variables:\n", 316 | " celsius (time, lat, lon) float64 22.98 23.24 23.49 ... -32.24 -32.14 -31.7\n", 317 | " slice (lat, lon) float64 296.1 296.4 296.6 296.9 ... 233.8 235.4 237.5\n", 318 | "Attributes:\n", 319 | " regrid_method: bilinear" 320 | ] 321 | }, 322 | "execution_count": 9, 323 | "metadata": {}, 324 | "output_type": "execute_result" 325 | } 326 | ], 327 | "source": [ 328 | "# besides ordering dimensions properly, another simple fix is to drop bad variables\n", 329 | "regridder(ds_bad.drop('air')) " 330 | ] 331 | } 332 | ], 333 | "metadata": { 334 | "kernelspec": { 335 | "display_name": "Python 3", 336 | "language": "python", 337 | "name": "python3" 338 | }, 339 | "language_info": { 340 | "codemirror_mode": { 341 | "name": "ipython", 342 | "version": 3 343 | }, 344 | "file_extension": ".py", 345 | "mimetype": "text/x-python", 346 | "name": "python", 347 | "nbconvert_exporter": "python", 348 | "pygments_lexer": "ipython3", 349 | "version": "3.7.3" 350 | } 351 | }, 352 | "nbformat": 4, 353 | "nbformat_minor": 4 354 | } 355 | -------------------------------------------------------------------------------- /doc/notebooks/Pure_numpy.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Use pure numpy array" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Despite the \"x\" in its name (indicating xarray-compatible), xESMF can also work with basic numpy arrays. You don't have to use xarray data structure if you don't need to track metadata. As long as you have numpy arrays describing the input data and input/output coordinate values, you can perform regridding.\n", 15 | "\n", 16 | "Code in this section is adapted from [an xarray example](http://xarray.pydata.org/en/stable/plotting.html#multidimensional-coordinates)." 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 1, 22 | "metadata": { 23 | "collapsed": true 24 | }, 25 | "outputs": [], 26 | "source": [ 27 | "# not importing xarray here!\n", 28 | "%matplotlib inline\n", 29 | "import matplotlib.pyplot as plt\n", 30 | "import numpy as np\n", 31 | "import xesmf as xe" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "## Rectilinear grid" 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "metadata": {}, 44 | "source": [ 45 | "### Input data" 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "metadata": {}, 51 | "source": [ 52 | "Just make some fake data." 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 2, 58 | "metadata": {}, 59 | "outputs": [ 60 | { 61 | "data": { 62 | "text/plain": [ 63 | "" 64 | ] 65 | }, 66 | "execution_count": 2, 67 | "metadata": {}, 68 | "output_type": "execute_result" 69 | }, 70 | { 71 | "data": { 72 | "image/png": "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\nv1MWAAAAAElFTkSuQmCC\n", 73 | "text/plain": [ 74 | "" 75 | ] 76 | }, 77 | "metadata": {}, 78 | "output_type": "display_data" 79 | } 80 | ], 81 | "source": [ 82 | "data = np.arange(20).reshape(4, 5)\n", 83 | "plt.pcolormesh(data)" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "metadata": {}, 89 | "source": [ 90 | "### Define grids" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "In previous examples we use xarray `DataSet` as input/output grids. But you can also use a simple dictionary:" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 3, 103 | "metadata": { 104 | "collapsed": true 105 | }, 106 | "outputs": [], 107 | "source": [ 108 | "grid_in = {'lon': np.linspace(0, 40, 5),\n", 109 | " 'lat': np.linspace(0, 20, 4)\n", 110 | " }\n", 111 | "\n", 112 | "# output grid has a larger coverage and finer resolution\n", 113 | "grid_out = {'lon': np.linspace(-20, 60, 51),\n", 114 | " 'lat': np.linspace(-10, 30, 41)\n", 115 | " }" 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "metadata": {}, 121 | "source": [ 122 | "### Perform regridding" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": 4, 128 | "metadata": {}, 129 | "outputs": [ 130 | { 131 | "name": "stdout", 132 | "output_type": "stream", 133 | "text": [ 134 | "Create weight file: bilinear_4x5_41x51.nc\n", 135 | "Remove file bilinear_4x5_41x51.nc\n" 136 | ] 137 | }, 138 | { 139 | "data": { 140 | "text/plain": [ 141 | "xESMF Regridder \n", 142 | "Regridding algorithm: bilinear \n", 143 | "Weight filename: bilinear_4x5_41x51.nc \n", 144 | "Reuse pre-computed weights? False \n", 145 | "Input grid shape: (4, 5) \n", 146 | "Output grid shape: (41, 51) \n", 147 | "Output grid dimension name: ('lat', 'lon') \n", 148 | "Periodic in longitude? False" 149 | ] 150 | }, 151 | "execution_count": 4, 152 | "metadata": {}, 153 | "output_type": "execute_result" 154 | } 155 | ], 156 | "source": [ 157 | "regridder = xe.Regridder(grid_in, grid_out, 'bilinear')\n", 158 | "regridder.clean_weight_file()\n", 159 | "regridder " 160 | ] 161 | }, 162 | { 163 | "cell_type": "markdown", 164 | "metadata": {}, 165 | "source": [ 166 | "The `regridder` here has no difference from the ones made from xarray `DataSet`. You can use it to regrid `DataArray` or just a basic `numpy.ndarray`:" 167 | ] 168 | }, 169 | { 170 | "cell_type": "code", 171 | "execution_count": 5, 172 | "metadata": {}, 173 | "outputs": [ 174 | { 175 | "data": { 176 | "text/plain": [ 177 | "(41, 51)" 178 | ] 179 | }, 180 | "execution_count": 5, 181 | "metadata": {}, 182 | "output_type": "execute_result" 183 | } 184 | ], 185 | "source": [ 186 | "data_out = regridder(data) # regrid a basic numpy array\n", 187 | "data_out.shape" 188 | ] 189 | }, 190 | { 191 | "cell_type": "markdown", 192 | "metadata": {}, 193 | "source": [ 194 | "### Check results" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 6, 200 | "metadata": {}, 201 | "outputs": [ 202 | { 203 | "data": { 204 | "text/plain": [ 205 | "" 206 | ] 207 | }, 208 | "execution_count": 6, 209 | "metadata": {}, 210 | "output_type": "execute_result" 211 | }, 212 | { 213 | "data": { 214 | "image/png": "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\n", 215 | "text/plain": [ 216 | "" 217 | ] 218 | }, 219 | "metadata": {}, 220 | "output_type": "display_data" 221 | } 222 | ], 223 | "source": [ 224 | "plt.pcolormesh(data_out)" 225 | ] 226 | }, 227 | { 228 | "cell_type": "markdown", 229 | "metadata": {}, 230 | "source": [ 231 | "## Curvilinear grid" 232 | ] 233 | }, 234 | { 235 | "cell_type": "markdown", 236 | "metadata": {}, 237 | "source": [ 238 | "### Grids" 239 | ] 240 | }, 241 | { 242 | "cell_type": "markdown", 243 | "metadata": {}, 244 | "source": [ 245 | "We use the previous input data, but now assume it is on a curvilinear grid described by 2D arrays. We also computed the cell corners, for two purposes:\n", 246 | "\n", 247 | "- Visualization with `plt.pcolormesh` (using cell centers will miss one row&column)\n", 248 | "- Conservative regridding with xESMF (corner information is required for conservative method)" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": 7, 254 | "metadata": { 255 | "collapsed": true 256 | }, 257 | "outputs": [], 258 | "source": [ 259 | "# cell centers\n", 260 | "lon, lat = np.meshgrid(np.linspace(-20, 20, 5), np.linspace(0, 30, 4))\n", 261 | "lon += lat/3\n", 262 | "lat += lon/3\n", 263 | "\n", 264 | "# cell corners\n", 265 | "lon_b, lat_b = np.meshgrid(np.linspace(-25, 25, 6), np.linspace(-5, 35, 5))\n", 266 | "lon_b += lat_b/3\n", 267 | "lat_b += lon_b/3" 268 | ] 269 | }, 270 | { 271 | "cell_type": "code", 272 | "execution_count": 8, 273 | "metadata": {}, 274 | "outputs": [ 275 | { 276 | "data": { 277 | "text/plain": [ 278 | "" 279 | ] 280 | }, 281 | "execution_count": 8, 282 | "metadata": {}, 283 | "output_type": "execute_result" 284 | }, 285 | { 286 | "data": { 287 | "image/png": "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\n", 288 | "text/plain": [ 289 | "" 290 | ] 291 | }, 292 | "metadata": {}, 293 | "output_type": "display_data" 294 | } 295 | ], 296 | "source": [ 297 | "plt.pcolormesh(lon_b, lat_b, data)\n", 298 | "plt.scatter(lon, lat) # show cell center\n", 299 | "plt.xlabel('lon')\n", 300 | "plt.ylabel('lat')" 301 | ] 302 | }, 303 | { 304 | "cell_type": "markdown", 305 | "metadata": {}, 306 | "source": [ 307 | "For the output grid, just use a simple rectilinear one:" 308 | ] 309 | }, 310 | { 311 | "cell_type": "code", 312 | "execution_count": 9, 313 | "metadata": { 314 | "collapsed": true 315 | }, 316 | "outputs": [], 317 | "source": [ 318 | "lon_out_b = np.linspace(-30, 40, 36) # bounds \n", 319 | "lon_out = 0.5*(lon_out_b[1:]+lon_out_b[:-1]) # centers\n", 320 | "\n", 321 | "lat_out_b = np.linspace(-20, 50, 36)\n", 322 | "lat_out = 0.5*(lat_out_b[1:]+lat_out_b[:-1])" 323 | ] 324 | }, 325 | { 326 | "cell_type": "markdown", 327 | "metadata": {}, 328 | "source": [ 329 | "To use conservative algorithm, both input and output grids should contain 4 variables: `lon`, `lat`, `lon_b`, `lon_b`." 330 | ] 331 | }, 332 | { 333 | "cell_type": "code", 334 | "execution_count": 10, 335 | "metadata": { 336 | "collapsed": true 337 | }, 338 | "outputs": [], 339 | "source": [ 340 | "grid_in = {'lon': lon, 'lat': lat,\n", 341 | " 'lon_b': lon_b, 'lat_b': lat_b}\n", 342 | "\n", 343 | "grid_out = {'lon': lon_out, 'lat': lat_out,\n", 344 | " 'lon_b': lon_out_b, 'lat_b': lat_out_b}" 345 | ] 346 | }, 347 | { 348 | "cell_type": "markdown", 349 | "metadata": {}, 350 | "source": [ 351 | "### Regridding" 352 | ] 353 | }, 354 | { 355 | "cell_type": "code", 356 | "execution_count": 11, 357 | "metadata": {}, 358 | "outputs": [ 359 | { 360 | "name": "stdout", 361 | "output_type": "stream", 362 | "text": [ 363 | "Create weight file: conservative_4x5_35x35.nc\n", 364 | "Remove file conservative_4x5_35x35.nc\n" 365 | ] 366 | }, 367 | { 368 | "data": { 369 | "text/plain": [ 370 | "xESMF Regridder \n", 371 | "Regridding algorithm: conservative \n", 372 | "Weight filename: conservative_4x5_35x35.nc \n", 373 | "Reuse pre-computed weights? False \n", 374 | "Input grid shape: (4, 5) \n", 375 | "Output grid shape: (35, 35) \n", 376 | "Output grid dimension name: ('lat', 'lon') \n", 377 | "Periodic in longitude? False" 378 | ] 379 | }, 380 | "execution_count": 11, 381 | "metadata": {}, 382 | "output_type": "execute_result" 383 | } 384 | ], 385 | "source": [ 386 | "regridder = xe.Regridder(grid_in, grid_out, 'conservative')\n", 387 | "regridder.clean_weight_file()\n", 388 | "regridder " 389 | ] 390 | }, 391 | { 392 | "cell_type": "code", 393 | "execution_count": 12, 394 | "metadata": {}, 395 | "outputs": [ 396 | { 397 | "data": { 398 | "text/plain": [ 399 | "(35, 35)" 400 | ] 401 | }, 402 | "execution_count": 12, 403 | "metadata": {}, 404 | "output_type": "execute_result" 405 | } 406 | ], 407 | "source": [ 408 | "data_out = regridder(data)\n", 409 | "data_out.shape" 410 | ] 411 | }, 412 | { 413 | "cell_type": "markdown", 414 | "metadata": {}, 415 | "source": [ 416 | "### Results" 417 | ] 418 | }, 419 | { 420 | "cell_type": "code", 421 | "execution_count": 13, 422 | "metadata": {}, 423 | "outputs": [ 424 | { 425 | "data": { 426 | "text/plain": [ 427 | "" 428 | ] 429 | }, 430 | "execution_count": 13, 431 | "metadata": {}, 432 | "output_type": "execute_result" 433 | }, 434 | { 435 | "data": { 436 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAY0AAAEKCAYAAADuEgmxAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAF8BJREFUeJzt3X2M3fV15/HPZ549NhNjQoyxYU0qk9Z5IopD6RK12QKp\nIQ+m6YYSbVtXQbLaplK6qpQ1RWpV7UZNFalqpW21tdIolkqS0rQUl02TGBfKpgkBUgi1ebIToBhs\nz/gJ2zyMmZmzf9wf4mYY+3dsz9zv747fL8mae3+/M985M772ub97z5yvI0IAAGT0lE4AANA9KBoA\ngDSKBgAgjaIBAEijaAAA0igaAIC0vpJf3PbTko5KmpQ0ERFrbC+R9DeSVkp6WtINEXGoVI4AgNc1\n4Urjv0TEZRGxprq/UdK2iFglaVt1HwDQAE0oGtOtk7S5ur1Z0vUFcwEAtHHJ3wi3/ZSkF9R6eeov\nI2KT7cMRsbg6b0mHXrs/7XM3SNogSb3qfe+wRjqYOQB0v6M6tD8izj+Vzyn6noak90fEc7bfImmr\n7cfbT0ZE2J6xqkXEJkmbJGnES+KnfdXcZwsA88hd8bVnTvVzir48FRHPVR9HJd0u6XJJ+2wvk6Tq\n42i5DAEA7YoVDdsLbZ/z2m1JH5S0XdIWSeursPWS7iiTIQBgupIvTy2VdHvrbQv1SfpyRHzD9gOS\nbrN9k6RnJN1QMEcAQJtiRSMifiTp3TMcPyCJNygAoIGa2HILAGio0t1TADCv9V28IhW3+5curo1Z\n/q39qbUmdzyZijsdXGkAANIoGgCANIoGACCNogEASKNoAADS6J4CcFbpO++82pj/+OTbUmtd9rFH\na2O+cPE/pNZ68tWJ2pjP/MUHUmvNJa40AABpFA0AQBpFAwCQRtEAAKRRNAAAaXRPASgmO5fp2Y/X\nz2VadHVuv7a/fceXamOW956TWiunPxV14/d/vTZmxfj2M8zlzHGlAQBIo2gAANIoGgCANIoGACCN\nogEASKNoAADSaLkFICk3yE+S9v1S/TC/RR9/PrXWn1365VTckKdqY/qdWmqW22lnz4JvjpROIYUr\nDQBAWvGiYbvX9kO276zuL7G91fbO6uO5pXMEALQULxqSPi3psbb7GyVti4hVkrZV9wEADVC0aNhe\nIelDkr7QdnidpM3V7c2Sru90XgCAmZW+0vhTSZ+R1P4u19KI2FPd3itp6UyfaHuD7QdtP/iqxuc4\nTQCAVLB7yvaHJY1GxPdtf2CmmIgI23GCc5skbZKkES+ZMQboZn3LLqiNee6Gt6bW6v/g/tqYWy79\nemqtdw5uqY3pVef/SQ67t+NfM2M8Xk3FLf2HnbUx9RvCzr2SLbdXSvqo7eskDUkasf3XkvbZXhYR\ne2wvk5QbXQkAmHPFXp6KiJsjYkVErJR0o6R/johfkbRF0voqbL2kOwqlCACYpvR7GjP5nKRrbO+U\ndHV1HwDQAI34jfCIuEfSPdXtA5KuKpkPAGBmTbzSAAA0VCOuNICm61v6ltqYsQ/9RGqtwx98KRX3\nR++9vTZmcc9dqbUyhpzr8inRGZUx7Ny2qp22ce8VqbiJ0bE5zmR2cKUBAEijaAAA0igaAIA0igYA\nII2iAQBIo3sKXad38eJU3Nh/XV0bc2zt0dRav/FT366NGe7Znlpr5UCuS6anw11KC3uOd/TrnYrM\ns9vBZPfUsalXamO+N74wtdZ3X1xVG3PP5stTa71F30nFlcaVBgAgjaIBAEijaAAA0igaAIA0igYA\nII2iAQBIo+UWZyQzyE+Sxq7LDfMbu7J+Q8tfe993U2stG6jflnQ2Dffk9qrvdCtt1kJ3fjPRFyO3\nRevjx5fWxtz6wrLUWtuPXlgbMxWz93z6wm/lWqybsJVrBlcaAIA0igYAII2iAQBIo2gAANIoGgCA\nNLqnzkLHr31fKu6ZG+q7fD7yzkdSa72zN9fx1OP6r7ls4HBqrU5bmOyeKqHXU7Uxx5PPIe95sb4T\n7r4jb02t9dSR81Jx5wzU/2yH+3Lb1c6mR/bWd2yteDw3yLJbcKUBAEgrVjRsD9m+3/YPbO+w/YfV\n8SW2t9reWX08t1SOAIAfV/JKY1zSz0fEuyVdJmmt7SskbZS0LSJWSdpW3QcANECxohEtx6q7/dWf\nkLRO0ubq+GZJ1xdIDwAwg6Lvadjutf2wpFFJWyPie5KWRsSeKmSvpBnnB9jeYPtB2w++qua+AQkA\n80nR7qmImJR0me3Fkm63/Y5p58OeuZ0mIjZJ2iRJI17SzGE+Bbi3fpbPyMZnU2vdMLL3TNM5Zef0\n1m/F2VSz3T11dGqoNub+RCeTJD314ptrY0Zfzm1xOjmLc5myhnqbOZnJD46UTqHjGtE9FRGHJd0t\naa2kfbaXSVL1cbRkbgCA15Xsnjq/usKQ7QWSrpH0uKQtktZXYesl3VEmQwDAdCVfnlomabPtXrWK\n120Rcaft70q6zfZNkp6RdEPBHAEAbYoVjYh4RNJ7Zjh+QNJVnc8IAFCnEe9pAAC6A7On5pnJK99V\nG/NTIw91IJPTM9xzvHQKp+3eoz+Zinvw0MWpuN2HF9fGRLJvcGRBfWfXQF/nO5T6eyZTcZmZZCVc\ncP/Z1+7PlQYAII2iAQBIo2gAANIoGgCANIoGACCN7ql55rmfW1Ab894O5HG6Bnvqd187Nlk/k0mS\nHjpyUW3Mk4fOT6119KX6r9nXm+sEmk12Lq5EbhklZkq9Mln/396TY7nHxcXfebQ2pn7PxO7ClQYA\nII2iAQBIo2gAANIoGgCANIoGACCNogEASKPldp5ZeMX+jn69lycHUnFPv7gkFbft+UtrYw4eXJRa\nS7M4425wuL4VuIT+vu4e+Jf16NjS2pgjY7ntavsO1f+3t/jJXC/z1Msvp+LmE640AABpFA0AQBpF\nAwCQRtEAAKRRNAAAaXRPdYm+lf8pFfeflz5dG3P//txauw/Ubzc6cSg3PFCDuS4f93V4vFty4F9P\nTzPHzg3M4iDCV17tT8XtT3SvxYHB1Fo9x3N/AU78+HPZ55z7+EuzuNr8wpUGACCtWNGwfZHtu20/\nanuH7U9Xx5fY3mp7Z/Xx3FI5AgB+XMkrjQlJvxsRqyVdIelTtldL2ihpW0SskrStug8AaIBiRSMi\n9kTEv1W3j0p6TNJySeskba7CNku6vkyGAIDpGvGehu2Vkt4j6XuSlkbEnurUXkn18wMAAB1RvHvK\n9iJJfyfpdyLiiNv2r4yIsGcemmN7g6QNkjSk4U6kWtRLb8/Vzn984IL6oBKNQL3NnH3U0+lurVMw\nOVX/nC7TySRJGqvvchs4lOtkGkz8yKaSrUyTQwUeF1H/ffb94IeppZq5ie7cKnqlYbtfrYJxa0T8\nfXV4n+1l1fllkkZn+tyI2BQRayJiTb9y7X0AgDNTsnvKkv5K0mMR8Sdtp7ZIWl/dXi/pjk7nBgCY\nWcmXp66U9KuS/t32w9Wx35P0OUm32b5J0jOSbiiUHwBgmmJFIyK+rRP/Pu5VncwFAJDTiO4pAEB3\nKN49hZyDlyb/qqY63M+R7Io6QRNccb3J7qmJyd7amOP7F6TWGhirX0uSBo7Ux/QkR3912lSD/2cZ\n3lsfM3n06Nwn0qW40gAApFE0AABpFA0AQBpFAwCQlioatv84cwwAML9lexyukfQ/ph27doZjOA09\nC+q7bo6tbOaMpOhLdk/N5teczF0g9xxMDEB6Kdd+NHiwPma2xyhNDszuerMlMbpJkWsQK2LxronS\nKXS1kxYN278p6bckvdX2I22nzpH0r3OZGACgeequNL4s6Z8k/ZF+fDOkoxGReO4FAJhPTlo0IuIF\nSS9I+oQk2X6LpCFJi2wvioj/mPsUAQBNkX0j/CO2d0p6StK/SHparSsQAMBZJNty+7/U2sf7yYi4\nRK2BgvfNWVYAgEbKFo1XI+KApB7bPRFxt6Q1c5gXAKCBsi23h6ttWe+VdKvtUUkvzl1aZ5epyy6t\njcm2tnaak720PaO53RWHRusXHEq2YGTaPidyMwaLiIYO/UvllRxQ6YncA2joQH3MgrHc1xz+zs7a\nmLNxG9es7JXGOkkvS/rvkr4h6YeSPjJXSQEAmin1XCYi2q8qNs9RLgCAhqv75b6jkma65rOkiIiR\nOckKANBIdb+ncU6nEgEANB9TbgEAaQ3tzzi7HHz7wkRUrp/Dk/XdKIMHcs8VBhMdK73jucl0PZPZ\n7q/Z6xKbHJzNMYmzJ/2T6PBTOidbhgYTO6FmO9yGR19NxXmq/qfWM577BiYPHkrFYWZcaQAA0ooW\nDdtftD1qe3vbsSW2t9reWX08t2SOAIDXlb7S+JKktdOObZS0LSJWSdqmH5+uCwAoqGjRiIh7JU1/\n9XOdXv9dkM2Sru9oUgCAEyp9pTGTpRGxp7q9V9LSkskAAF7X6O6piAh75iE2tjdI2iBJQxruaF6z\nbfxN9TGLd+S6lBYcqN8W1pO5rWOnBuq7jyZyu6UWMdXQR3d2plSmm2lof26thfvqtzgdGh1PrTWx\nKLGNboHGtf4DuXF4zJU6M0280thne5kkVR9HZwqKiE0RsSYi1vQrNwwPAHBmmlg0tkhaX91eL+mO\ngrkAANqUbrn9iqTvSnqb7d22b5L0OUnXVDsFXl3dBwA0QNFXfSPiEyc4dVVHEwEApDTx5SkAQEM1\ntL9kfug777xU3MgzuW6mTmtq99FUb7I1ZxY7eDKdTAv25/4eB4/k4gYO1HczeWr2HjvRm3wO2cyR\nXvIYM6U6gSsNAEAaRQMAkEbRAACkUTQAAGkUDQBAGkUDAJDW0KbK+SEuPL90CmekqS232ZbP4X31\n7agLnz+eWmtgX2KP0+QgyMk3JQdsdvgp3VR/c59DZrZ7nRhL7E+MM9bcRwkAoHEoGgCANIoGACCN\nogEASKNoAADSmtofMy+8smxR6RRmNJXYrTMrM8hPkob35wKHdh+rjel5KbctqaK+42Y2xWDyn1ND\nn6pFge6pTFeUJA3ufqE2ZiKaOfhzvmnowxcA0EQUDQBAGkUDAJBG0QAApFE0AABpdE+dpp7h+vlB\nx0c6/+PtmazvRhk8nOtkGhh7qf7r7d2fWksTyTarnsRgqTeN5NbqsOhr8D8nz94erQOjicfF6MHU\nWpOjY6m4icnk4wdzjisNAEAaRQMAkNbYomF7re0nbO+yvbF0PgCAhhYN272S/lzStZJWS/qE7dVl\nswIANLJoSLpc0q6I+FFEHJf0VUnrCucEAGe9prZ7LJf0bNv93ZJ+uj3A9gZJGyRpSMmd0GZRz/Jl\n9TGJTiZJGhqrn6XU/2xuV7LJ3c/XxvQku4/sAs8p+mdxMNZsSnQfRX/v7H7JxOOn58jLucUO189u\n0sFDqaWmEp1MTIGav5p6pVErIjZFxJqIWNOvwdLpAMBZoalF4zlJF7XdX1EdAwAU1NSi8YCkVbYv\nsT0g6UZJWwrnBABnvUa+pxERE7Z/W9I3JfVK+mJE7CicFgCc9RpZNCQpIr4u6eul8wAAvK6xRaPp\nYrR+5tLwlqdzayW6USZSK0nure/gKdIVldXf0IdkYiZW7wv1M5kkSQcPp8ImEt1MU+xWhw5r8P8e\nAICmoWgAANIoGgCANIoGACCNogEASKNoAADSGtrf2HyTLyQGwBXggYHSKZyZzMDCyA2C1Muv1C91\n9FhqqcljL9YH0f6KswBXGgCANIoGACCNogEASKNoAADSKBoAgDS6p+aZTndPRbZjaDIZt2+sfqlM\nJ5NENxMwB7jSAACkUTQAAGkUDQBAGkUDAJBG0QAApNE91S2yW7T21sfF8eOppWJ8vDZmKrkWgPmB\nKw0AQBpFAwCQVqRo2P647R22p2yvmXbuZtu7bD9h+xdK5AcAmFmp9zS2S/qYpL9sP2h7taQbJb1d\n0oWS7rJ9aURMdj5FAMB0Ra40IuKxiHhihlPrJH01IsYj4ilJuyRd3tnsAAAn0rTuqeWS7mu7v7s6\n9ga2N0jaIElDGp77zEpLzlGaPHhojhMBcDabs6Jh+y5JF8xw6paIuONM14+ITZI2SdKIlyT3/wQA\nnIk5KxoRcfVpfNpzki5qu7+iOgYAaICmtdxukXSj7UHbl0haJen+wjkBACqlWm5/0fZuST8j6f/a\n/qYkRcQOSbdJelTSNyR9is4pAGiOIm+ER8Ttkm4/wbnPSvpsZzMCAGQ07eUpAECDUTQAAGkUDQBA\nGkUDAJBG0QAApFE0AABpFA0AQBpFAwCQRtEAAKRRNAAAaRQNAEAaRQMAkEbRAACkUTQAAGkUDQBA\nGkUDAJBG0QAApFE0AABpFA0AQBpFAwCQRtEAAKRRNAAAaUWKhu3P237c9iO2b7e9uO3czbZ32X7C\n9i+UyA8AMLNSVxpbJb0jIt4l6UlJN0uS7dWSbpT0dklrJf2F7d5COQIApilSNCLiWxExUd29T9KK\n6vY6SV+NiPGIeErSLkmXl8gRAPBGfaUTkPRJSX9T3V6uVhF5ze7q2BvY3iBpQ3V3/K742vY5y3Du\nvVnS/tJJnAHyL6ub8+/m3KXuz/9tp/oJc1Y0bN8l6YIZTt0SEXdUMbdImpB066muHxGbJG2q1nkw\nItacQbpFkX9Z5F9ON+cuzY/8T/Vz5qxoRMTVJztv+9clfVjSVRER1eHnJF3UFraiOgYAaIBS3VNr\nJX1G0kcj4qW2U1sk3Wh70PYlklZJur9EjgCANyr1nsb/ljQoaattSbovIn4jInbYvk3So2q9bPWp\niJhMrLdp7lLtCPIvi/zL6ebcpbMwf7/+yhAAACfHb4QDANIoGgCAtK4uGrb/ZzWK5GHb37J9Ydu5\nxo8j6fZxKrY/bnuH7Snba6ad64b811b57bK9sXQ+dWx/0fao7e1tx5bY3mp7Z/Xx3JI5nozti2zf\nbfvR6nHz6ep4V3wPtods32/7B1X+f1gd74r8Jcl2r+2HbN9Z3T/l3Lu6aEj6fES8KyIuk3SnpN+X\numocSbePU9ku6WOS7m0/2A35V/n8uaRrJa2W9Ikq7yb7klo/z3YbJW2LiFWStlX3m2pC0u9GxGpJ\nV0j6VPUz75bvYVzSz0fEuyVdJmmt7SvUPflL0qclPdZ2/5Rz7+qiERFH2u4ulPTau/pdMY6k28ep\nRMRjEfHEDKe6If/LJe2KiB9FxHFJX1Ur78aKiHslHZx2eJ2kzdXtzZKu72hSpyAi9kTEv1W3j6r1\nn9dydcn3EC3Hqrv91Z9Ql+Rve4WkD0n6QtvhU869q4uGJNn+rO1nJf03VVcaaj0Qn20LO+E4kgb5\npKR/qm53Y/7tuiH/bsgxY2lE7Klu75W0tGQyWbZXSnqPpO+pi76H6uWdhyWNStoaEd2U/5+q9ftx\nU23HTjn3xhcN23fZ3j7Dn3WSFBG3RMRFao0i+e2y2b5RXf5VzGmPU5lrmfzRDNVkhcb30NteJOnv\nJP3OtFcLGv89RMRk9XL4CkmX237HtPONzN/2hyWNRsT3TxSTzb0JAwtPqm4cSZtbJX1d0h+oQeNI\nun2cyin8/Ns1Jv+T6IYcM/bZXhYRe2wvU+sZcGPZ7lerYNwaEX9fHe6q70GSIuKw7bvVeo+pG/K/\nUtJHbV8naUjSiO2/1mnk3vgrjZOxvart7jpJj1e3u2IcyTwep9IN+T8gaZXtS2wPqPXG/ZbCOZ2O\nLZLWV7fXS7qjYC4nZduS/krSYxHxJ22nuuJ7sH3+ax2OthdIukat/3Man39E3BwRKyJipVqP9X+O\niF/R6eQeEV37R61nLNslPSLpHyUtbzt3i6QfSnpC0rWlcz1B/rvUel394erP/+my/H9RrfcCxiXt\nk/TNLsv/OrW61n6o1vTl4jnV5PsVSXskvVr93G+SdJ5aXS87Jd0laUnpPE+S//vVevnjkbbH/HXd\n8j1Iepekh6r8t0v6/ep4V+Tf9n18QNKdp5s7Y0QAAGld/fIUAKCzKBoAgDSKBgAgjaIBAEijaAAA\n0igawCywfaw+Cuh+FA0AQBpFA5hFbvl8NZ/r323/cnX8A7bvsf01t/ZQubX6DWmgqzR+9hTQZT6m\n1l4L75b0ZkkP2H5tv5H3qLXHyPOS/lWteUDfLpEkcLq40gBm1/slfSVa01D3SfoXSe+rzt0fEbsj\nYkqtERorC+UInDaKBtA54223J8WVProQRQOYXf9P0i9Xm/WcL+ln1bwJv8Bp45kOMLtul/Qzkn6g\n1kTXz0TEXts/WTYtYHYw5RYAkMbLUwCANIoGACCNogEASKNoAADSKBoAgDSKBgAgjaIBAEj7/87a\nXQUTvDc6AAAAAElFTkSuQmCC\n", 437 | "text/plain": [ 438 | "" 439 | ] 440 | }, 441 | "metadata": {}, 442 | "output_type": "display_data" 443 | } 444 | ], 445 | "source": [ 446 | "plt.pcolormesh(lon_out_b, lat_out_b, data_out)\n", 447 | "plt.xlabel('lon')\n", 448 | "plt.ylabel('lat')" 449 | ] 450 | }, 451 | { 452 | "cell_type": "markdown", 453 | "metadata": {}, 454 | "source": [ 455 | "## All possible combinations" 456 | ] 457 | }, 458 | { 459 | "cell_type": "markdown", 460 | "metadata": {}, 461 | "source": [ 462 | "All $2 \\times 2\\times 2 = 8$ combinations would work:\n", 463 | "\n", 464 | "- Input grid: `xarray.DataSet` or `dict`\n", 465 | "- Output grid: `xarray.DataSet` or `dict`\n", 466 | "- Input data: `xarray.DataArray` or `numpy.ndarray`\n", 467 | "\n", 468 | "The output data type will be the same as input data." 469 | ] 470 | } 471 | ], 472 | "metadata": { 473 | "kernelspec": { 474 | "display_name": "Python 3", 475 | "language": "python", 476 | "name": "python3" 477 | }, 478 | "language_info": { 479 | "codemirror_mode": { 480 | "name": "ipython", 481 | "version": 3 482 | }, 483 | "file_extension": ".py", 484 | "mimetype": "text/x-python", 485 | "name": "python", 486 | "nbconvert_exporter": "python", 487 | "pygments_lexer": "ipython3", 488 | "version": "3.6.2" 489 | }, 490 | "toc": { 491 | "nav_menu": {}, 492 | "number_sections": true, 493 | "sideBar": false, 494 | "skip_h1_title": false, 495 | "toc_cell": false, 496 | "toc_position": { 497 | "height": "200px", 498 | "left": "2px", 499 | "right": "20px", 500 | "top": "101px", 501 | "width": "212px" 502 | }, 503 | "toc_section_display": "block", 504 | "toc_window_display": false 505 | } 506 | }, 507 | "nbformat": 4, 508 | "nbformat_minor": 2 509 | } 510 | -------------------------------------------------------------------------------- /doc/other_tools.rst: -------------------------------------------------------------------------------- 1 | .. _other_tools-label: 2 | 3 | Other geospatial regridding tools 4 | ================================= 5 | 6 | Here is a brief overview of other regridding tools that the author is aware of 7 | (for geospatial data on the sphere, excluding traditional image resizing functions). 8 | They are all great tools and have helped the author a lot in both scientific research 9 | and xESMF development. Check them out if xESMF cannot suit your needs. 10 | 11 | - `ESMF `_ (*Fortran package*) 12 | 13 | Although its name "Earth System Modeling Framework" doesn't indicate a regridding 14 | functionality, it actually contains a very powerful regridding engine. 15 | It is widely used in Earth System Models (ESMs), serving as both the software infrastructure 16 | and the regridder for transforming data between the atmosphere, ocean, and land components. 17 | It can deal with general irregular meshes, in either 2D or 3D. 18 | 19 | ESMF is a huge beast, containing 20 | `one million lines of source code `_. 21 | Even just compiling it requires some effort. 22 | It is more for building ESMs than for data analysis. 23 | 24 | - `ESMPy `_ (*Python interface to ESMF*) 25 | 26 | ESMPy provides a much simpler way to use ESMF's regridding functionality. 27 | The greatest thing is, it is pre-compiled as a 28 | `conda package `_, 29 | so you can install it with one-click and don't have to go through 30 | the daunting compiling process on your own. 31 | 32 | However, ESMPy is a complicated Python API that controls a huge Fortran beast 33 | hidden underneath. It is not as intuitive as native Python packages, and even 34 | a simple regridding task requires more than 10 lines of arcane code. That's why 35 | I made xESMF. If you want to involve in xESMF development you need to know ESMPy. 36 | Check out this nice 37 | `tutorial `_ 38 | before going to the 39 | `official doc `_. 40 | 41 | - `TempestRemap `_ 42 | (*C++ package*) 43 | 44 | A pretty modern and powerful package, 45 | supporting arbitrary-order conservative remapping. 46 | It can also generate cubed-sphere grids on the fly 47 | and can be modified to support many cubed-sphere grid variations 48 | (`example `_, only if you can read C++). 49 | 50 | - `SCRIP `_ (*Fortran package*) 51 | 52 | An old pacakge, once popular but **no longer maintained** (long live SCRIP). 53 | You should not use it now, but should know that it exists. 54 | Newer regridding packages often follow its standards -- 55 | you will see "SCRIP format" here and there, for example in ESMF or TempestRemap. 56 | 57 | - `Regridder in NCL `_ 58 | (*NCAR Command Language*) 59 | 60 | Has bilinear and conservative algorithms for rectilinear grids, 61 | and also supports some specialized curvilinear grids. 62 | There is also an `ESMF wrapper `_ 63 | that works for more grid types. 64 | 65 | - `Regridder in NCO `_ 66 | (*command line tool*) 67 | 68 | - `Regridder in Iris `_ 69 | (*Python package*) 70 | 71 | - `Regridder in UV-CDAT `_ 72 | (*Python package*) 73 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | numpydoc 2 | ipython 3 | nbsphinx 4 | -------------------------------------------------------------------------------- /doc/user_api.rst: -------------------------------------------------------------------------------- 1 | User API 2 | ######## 3 | 4 | Regridder 5 | ========= 6 | 7 | .. autoclass:: xesmf.frontend.Regridder 8 | :members: 9 | :special-members: __init__, __call__ 10 | 11 | util 12 | ==== 13 | 14 | .. automodule:: xesmf.util 15 | :members: 16 | 17 | data 18 | ==== 19 | 20 | .. automodule:: xesmf.data 21 | :members: 22 | -------------------------------------------------------------------------------- /doc/why.rst: -------------------------------------------------------------------------------- 1 | Why inventing a new regridding package 2 | ====================================== 3 | 4 | For scientific correctness 5 | -------------------------- 6 | 7 | Traditional interpolation routines, such as 8 | `interp2d in Scipy `_ 9 | and 10 | `interp2 in MATLAB `_, 11 | assume flat 2D planes and do not consider the spherical geometry of the earth. 12 | They are great for image processing, but will produce incorrect/distorted results for geospatial data. 13 | 14 | Also, traditional interpolation algorithms are typically based on piecewise polynomials ("splines"). 15 | While being highly accurate in terms of error convergence, they often lack desired physical properties such as 16 | conservation (total mass should be conserved) and monotonicity (air density cannot go negative). 17 | 18 | For emerging new grid types 19 | --------------------------- 20 | 21 | Non-orthogonal grids are becoming popular in numerical models 22 | (`Staniforth and Thuburn 2012 `_), 23 | but traditional tools often assume standard lat-lon grids. 24 | 25 | xESMF can regrid between general curvilinear (i.e. quadrilateral or "logically rectilinear") grids, like 26 | 27 | - The `Cubed-Sphere `_ grid 28 | in `GFDL-FV3 `_ 29 | - The `Latitude-Longitude-Cap grid `_ 30 | in `MITgcm `_ 31 | - The `Lambert Conformal grid `_ 32 | in WRF 33 | 34 | However, xESMF does not yet support non-quadrilateral grids, 35 | like the hexagonal grid in `MPAS `_. 36 | See :ref:`irregular_meshes-label` for more information. 37 | 38 | For usability and simplicity 39 | ---------------------------- 40 | 41 | :ref:`Current geospatial regridding tools ` tend to have non-trivial learning curves. 42 | xESMF tries to be simple and intuitive. 43 | Instead of inventing a new data structure, it relies on well-estabilished standards 44 | (numpy and xarray), so users don't need to learn a bunch of new syntaxes or even a new software stack. 45 | 46 | xESMF can track metadata in ``xarray.DataArray`` / ``xarray.Dataset``, and 47 | also work with basic ``numpy.ndarray``. 48 | This means any Python users can use it easily, even if being unfamiliar with xarray. 49 | 50 | The choice of Python and Anaconda also makes xESMF :ref:`extremely easy to install `. 51 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | build: 2 | image: latest 3 | 4 | python: 5 | version: 3.6 6 | setup_py_install: true 7 | 8 | requirements_file: doc/requirements.txt 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import os 3 | 4 | VERSION = '0.3.0' 5 | DISTNAME = 'xesmf' 6 | DESCRIPTION = "Universal Regridder for Geospatial Data" 7 | AUTHOR = 'Jiawei Zhuang' 8 | AUTHOR_EMAIL = 'jiaweizhuang@g.harvard.edu' 9 | URL = 'https://github.com/JiaweiZhuang/xESMF' 10 | LICENSE = 'MIT' 11 | PYTHON_REQUIRES = '>=3.5' 12 | 13 | # https://github.com/rtfd/readthedocs.org/issues/5512#issuecomment-475024373 14 | on_rtd = os.environ.get('READTHEDOCS') == 'True' 15 | if on_rtd: 16 | INSTALL_REQUIRES = [] 17 | else: 18 | INSTALL_REQUIRES = ['esmpy', 'xarray', 'numpy', 'scipy'] 19 | 20 | CLASSIFIERS = [ 21 | 'Development Status :: 4 - Beta', 22 | 'License :: OSI Approved :: MIT License', 23 | 'Operating System :: OS Independent', 24 | 'Intended Audience :: Science/Research', 25 | 'Programming Language :: Python', 26 | 'Programming Language :: Python :: 3', 27 | 'Programming Language :: Python :: 3.5', 28 | 'Programming Language :: Python :: 3.6', 29 | 'Programming Language :: Python :: 3.7', 30 | 'Topic :: Scientific/Engineering', 31 | ] 32 | 33 | 34 | def readme(): 35 | with open('README.rst') as f: 36 | return f.read() 37 | 38 | 39 | setup(name=DISTNAME, 40 | version=VERSION, 41 | license=LICENSE, 42 | author=AUTHOR, 43 | author_email=AUTHOR_EMAIL, 44 | classifiers=CLASSIFIERS, 45 | description=DESCRIPTION, 46 | long_description=readme(), 47 | python_requires=PYTHON_REQUIRES, 48 | install_requires=INSTALL_REQUIRES, 49 | url=URL, 50 | packages=find_packages()) 51 | -------------------------------------------------------------------------------- /xesmf/__init__.py: -------------------------------------------------------------------------------- 1 | __version__='0.3.0' 2 | from . import util 3 | from . import data 4 | from . frontend import Regridder 5 | -------------------------------------------------------------------------------- /xesmf/backend.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Backend for xESMF. This module wraps ESMPy's complicated API and can create 3 | ESMF Grid and Regrid objects only using basic numpy arrays. 4 | 5 | General idea: 6 | 7 | 1) Only use pure numpy array in this low-level backend. xarray should only be 8 | used in higher-level APIs which interface with this low-level backend. 9 | 10 | 2) Use simple, procedural programming here. Because ESMPy Classes are 11 | complicated enough, building new Classes will make debugging very difficult. 12 | 13 | 3) Add some basic error checking in this wrapper level. 14 | ESMPy is hard to debug because the program often dies in the Fortran level. 15 | So it would be helpful to catch some common mistakes in Python level. 16 | ''' 17 | 18 | import numpy as np 19 | import ESMF 20 | import warnings 21 | import os 22 | 23 | 24 | def warn_f_contiguous(a): 25 | ''' 26 | Give a warning if input array if not Fortran-ordered. 27 | 28 | ESMPy expects Fortran-ordered array. Passing C-ordered array will slow down 29 | performance due to memory rearrangement. 30 | 31 | Parameters 32 | ---------- 33 | a : numpy array 34 | ''' 35 | if not a.flags['F_CONTIGUOUS']: 36 | warnings.warn("Input array is not F_CONTIGUOUS. " 37 | "Will affect performance.") 38 | 39 | 40 | def warn_lat_range(lat): 41 | ''' 42 | Give a warning if latitude is outside of [-90, 90] 43 | 44 | Longitute, on the other hand, can be in any range, 45 | since the it the transform is done in (x, y, z) space. 46 | 47 | Parameters 48 | ---------- 49 | lat : numpy array 50 | ''' 51 | if (lat.max() > 90.0) or (lat.min() < -90.0): 52 | warnings.warn("Latitude is outside of [-90, 90]") 53 | 54 | 55 | def esmf_grid(lon, lat, periodic=False): 56 | ''' 57 | Create an ESMF.Grid object, for contrusting ESMF.Field and ESMF.Regrid 58 | 59 | Parameters 60 | ---------- 61 | lon, lat : 2D numpy array 62 | Longitute/Latitude of cell centers. 63 | 64 | Recommend Fortran-ordering to match ESMPy internal. 65 | 66 | Shape should be ``(Nlon, Nlat)`` for rectilinear grid, 67 | or ``(Nx, Ny)`` for general quadrilateral grid. 68 | 69 | periodic : bool, optional 70 | Periodic in longitude? Default to False. 71 | Only useful for source grid. 72 | 73 | Returns 74 | ------- 75 | grid : ESMF.Grid object 76 | ''' 77 | 78 | # ESMPy expects Fortran-ordered array. 79 | # Passing C-ordered array will slow down performance. 80 | for a in [lon, lat]: 81 | warn_f_contiguous(a) 82 | 83 | warn_lat_range(lat) 84 | 85 | # ESMF.Grid can actually take 3D array (lon, lat, radius), 86 | # but regridding only works for 2D array 87 | assert lon.ndim == 2, "Input grid must be 2D array" 88 | assert lon.shape == lat.shape, "lon and lat must have same shape" 89 | 90 | staggerloc = ESMF.StaggerLoc.CENTER # actually just integer 0 91 | 92 | if periodic: 93 | num_peri_dims = 1 94 | else: 95 | num_peri_dims = None 96 | 97 | # ESMPy documentation claims that if staggerloc and coord_sys are None, 98 | # they will be set to default values (CENTER and SPH_DEG). 99 | # However, they actually need to be set explicitly, 100 | # otherwise grid._coord_sys and grid._staggerloc will still be None. 101 | grid = ESMF.Grid(np.array(lon.shape), staggerloc=staggerloc, 102 | coord_sys=ESMF.CoordSys.SPH_DEG, 103 | num_peri_dims=num_peri_dims) 104 | 105 | # The grid object points to the underlying Fortran arrays in ESMF. 106 | # To modify lat/lon coordinates, need to get pointers to them 107 | lon_pointer = grid.get_coords(coord_dim=0, staggerloc=staggerloc) 108 | lat_pointer = grid.get_coords(coord_dim=1, staggerloc=staggerloc) 109 | 110 | # Use [...] to avoid overwritting the object. Only change array values. 111 | lon_pointer[...] = lon 112 | lat_pointer[...] = lat 113 | 114 | return grid 115 | 116 | 117 | def esmf_locstream(lon, lat): 118 | ''' 119 | Create an ESMF.LocStream object, for contrusting ESMF.Field and ESMF.Regrid 120 | 121 | Parameters 122 | ---------- 123 | lon, lat : 1D numpy array 124 | Longitute/Latitude of cell centers. 125 | 126 | Returns 127 | ------- 128 | locstream : ESMF.LocStream object 129 | ''' 130 | 131 | if len(lon.shape) > 1: 132 | raise ValueError("lon can only be 1d") 133 | if len(lat.shape) > 1: 134 | raise ValueError("lat can only be 1d") 135 | 136 | assert lon.shape == lat.shape 137 | 138 | location_count = len(lon) 139 | 140 | locstream = ESMF.LocStream(location_count, 141 | coord_sys=ESMF.CoordSys.SPH_DEG) 142 | 143 | locstream["ESMF:Lon"] = lon.astype(np.dtype('f8')) 144 | locstream["ESMF:Lat"] = lat.astype(np.dtype('f8')) 145 | 146 | return locstream 147 | 148 | 149 | def add_corner(grid, lon_b, lat_b): 150 | ''' 151 | Add corner information to ESMF.Grid for conservative regridding. 152 | 153 | Not needed for other methods like bilinear or nearest neighbour. 154 | 155 | Parameters 156 | ---------- 157 | grid : ESMF.Grid object 158 | Generated by ``esmf_grid()``. Will be modified in-place. 159 | 160 | lon_b, lat_b : 2D numpy array 161 | Longitute/Latitude of cell corner 162 | Recommend Fortran-ordering to match ESMPy internal. 163 | Shape should be ``(Nlon+1, Nlat+1)``, or ``(Nx+1, Ny+1)`` 164 | ''' 165 | 166 | # codes here are almost the same as esmf_grid(), 167 | # except for the "staggerloc" keyword 168 | staggerloc = ESMF.StaggerLoc.CORNER # actually just integer 3 169 | 170 | for a in [lon_b, lat_b]: 171 | warn_f_contiguous(a) 172 | 173 | warn_lat_range(lat_b) 174 | 175 | assert lon_b.ndim == 2, "Input grid must be 2D array" 176 | assert lon_b.shape == lat_b.shape, "lon_b and lat_b must have same shape" 177 | assert np.array_equal(lon_b.shape, grid.max_index+1), ( 178 | "lon_b should be size (Nx+1, Ny+1)") 179 | assert (grid.num_peri_dims == 0) and (grid.periodic_dim is None), ( 180 | "Cannot add corner for periodic grid") 181 | 182 | grid.add_coords(staggerloc=staggerloc) 183 | 184 | lon_b_pointer = grid.get_coords(coord_dim=0, staggerloc=staggerloc) 185 | lat_b_pointer = grid.get_coords(coord_dim=1, staggerloc=staggerloc) 186 | 187 | lon_b_pointer[...] = lon_b 188 | lat_b_pointer[...] = lat_b 189 | 190 | 191 | def esmf_regrid_build(sourcegrid, destgrid, method, 192 | filename=None, extra_dims=None, ignore_degenerate=None): 193 | ''' 194 | Create an ESMF.Regrid object, containing regridding weights. 195 | 196 | Parameters 197 | ---------- 198 | sourcegrid, destgrid : ESMF.Grid object 199 | Source and destination grids. 200 | 201 | Should create them by ``esmf_grid()`` 202 | (with optionally ``add_corner()``), 203 | instead of ESMPy's original API. 204 | 205 | method : str 206 | Regridding method. Options are 207 | 208 | - 'bilinear' 209 | - 'conservative', **need grid corner information** 210 | - 'patch' 211 | - 'nearest_s2d' 212 | - 'nearest_d2s' 213 | 214 | filename : str, optional 215 | Offline weight file. **Require ESMPy 7.1.0.dev38 or newer.** 216 | With the weights available, we can use Scipy's sparse matrix 217 | mulplication to apply weights, which is faster and more Pythonic 218 | than ESMPy's online regridding. 219 | 220 | extra_dims : a list of integers, optional 221 | Extra dimensions (e.g. time or levels) in the data field 222 | 223 | This does NOT affect offline weight file, only affects online regrid. 224 | 225 | Extra dimensions will be stacked to the fastest-changing dimensions, 226 | i.e. following Fortran-like instead of C-like conventions. 227 | For example, if extra_dims=[Nlev, Ntime], then the data field dimension 228 | will be [Nlon, Nlat, Nlev, Ntime] 229 | 230 | ignore_degenerate : bool, optional 231 | If False (default), raise error if grids contain degenerated cells 232 | (i.e. triangles or lines, instead of quadrilaterals) 233 | 234 | Returns 235 | ------- 236 | grid : ESMF.Grid object 237 | 238 | ''' 239 | 240 | # use shorter, clearer names for options in ESMF.RegridMethod 241 | method_dict = {'bilinear': ESMF.RegridMethod.BILINEAR, 242 | 'conservative': ESMF.RegridMethod.CONSERVE, 243 | 'patch': ESMF.RegridMethod.PATCH, 244 | 'nearest_s2d': ESMF.RegridMethod.NEAREST_STOD, 245 | 'nearest_d2s': ESMF.RegridMethod.NEAREST_DTOS 246 | } 247 | try: 248 | esmf_regrid_method = method_dict[method] 249 | except: 250 | raise ValueError('method should be chosen from ' 251 | '{}'.format(list(method_dict.keys()))) 252 | 253 | # conservative regridding needs cell corner information 254 | if method == 'conservative': 255 | if not sourcegrid.has_corners: 256 | raise ValueError('source grid has no corner information. ' 257 | 'cannot use conservative regridding.') 258 | if not destgrid.has_corners: 259 | raise ValueError('destination grid has no corner information. ' 260 | 'cannot use conservative regridding.') 261 | 262 | # ESMF.Regrid requires Field (Grid+data) as input, not just Grid. 263 | # Extra dimensions are specified when constructing the Field objects, 264 | # not when constructing the Regrid object later on. 265 | sourcefield = ESMF.Field(sourcegrid, ndbounds=extra_dims) 266 | destfield = ESMF.Field(destgrid, ndbounds=extra_dims) 267 | 268 | # ESMPy will throw an incomprehensive error if the weight file 269 | # already exists. Better to catch it here! 270 | if filename is not None: 271 | assert not os.path.exists(filename), ( 272 | 'Weight file already exists! Please remove it or use a new name.') 273 | 274 | # Calculate regridding weights. 275 | # Must set unmapped_action to IGNORE, otherwise the function will fail, 276 | # if the destination grid is larger than the source grid. 277 | regrid = ESMF.Regrid(sourcefield, destfield, filename=filename, 278 | regrid_method=esmf_regrid_method, 279 | unmapped_action=ESMF.UnmappedAction.IGNORE, 280 | ignore_degenerate=ignore_degenerate) 281 | 282 | return regrid 283 | 284 | 285 | def esmf_regrid_apply(regrid, indata): 286 | ''' 287 | Apply existing regridding weights to the data field, 288 | using ESMPy's built-in functionality. 289 | 290 | xESMF use Scipy to apply weights instead of this. 291 | This is only for benchmarking Scipy's result and performance. 292 | 293 | Parameters 294 | ---------- 295 | regrid : ESMF.Regrid object 296 | Contains the mapping from the source grid to the destination grid. 297 | 298 | Users should create them by esmf_regrid_build(), 299 | instead of ESMPy's original API. 300 | 301 | indata : numpy array of shape ``(Nlon, Nlat, N1, N2, ...)`` 302 | Extra dimensions ``(N1, N2, ...)`` are specified in 303 | ``esmf_regrid_build()``. 304 | 305 | Recommend Fortran-ordering to match ESMPy internal. 306 | 307 | Returns 308 | ------- 309 | outdata : numpy array of shape ``(Nlon_out, Nlat_out, N1, N2, ...)`` 310 | 311 | ''' 312 | 313 | # Passing C-ordered input data will be terribly slow, 314 | # since indata is often quite large and re-ordering memory is expensive. 315 | warn_f_contiguous(indata) 316 | 317 | # Get the pointers to source and destination fields. 318 | # Because the regrid object points to its underlying field&grid, 319 | # we can just pass regrid from ESMF_regrid_build() to ESMF_regrid_apply(), 320 | # without having to pass all the field&grid objects. 321 | sourcefield = regrid.srcfield 322 | destfield = regrid.dstfield 323 | 324 | # pass numpy array to the underlying Fortran array 325 | sourcefield.data[...] = indata 326 | 327 | # apply regridding weights 328 | destfield = regrid(sourcefield, destfield) 329 | 330 | return destfield.data 331 | 332 | 333 | def esmf_regrid_finalize(regrid): 334 | ''' 335 | Free the underlying Fortran array to avoid memory leak. 336 | 337 | After calling ``destroy()`` on regrid or its fields, we cannot use the 338 | regrid method anymore, but the input and output data still exist. 339 | 340 | Parameters 341 | ---------- 342 | regrid : ESMF.Regrid object 343 | 344 | ''' 345 | 346 | regrid.destroy() 347 | regrid.srcfield.destroy() 348 | regrid.dstfield.destroy() 349 | regrid.srcfield.grid.destroy() 350 | regrid.dstfield.grid.destroy() 351 | 352 | # double check 353 | assert regrid.finalized 354 | assert regrid.srcfield.finalized 355 | assert regrid.dstfield.finalized 356 | assert regrid.srcfield.grid.finalized 357 | assert regrid.dstfield.grid.finalized 358 | -------------------------------------------------------------------------------- /xesmf/data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Standard test data for regridding benchmark. 3 | """ 4 | 5 | import numpy as np 6 | 7 | 8 | def wave_smooth(lon, lat): 9 | ''' 10 | Spherical harmonic with low frequency. 11 | 12 | Parameters 13 | ---------- 14 | lon, lat : 2D numpy array or xarray DataArray 15 | Longitute/Latitude of cell centers 16 | 17 | Returns 18 | ------- 19 | f : 2D numpy array or xarray DataArray depending on input 20 | 2D wave field 21 | 22 | Notes 23 | ------- 24 | Equation from [1]_ [2]_: 25 | 26 | .. math:: Y_2^2 = 2 + \cos^2(\\theta) \cos(2 \phi) 27 | 28 | References 29 | ---------- 30 | .. [1] Jones, P. W. (1999). First-and second-order conservative remapping 31 | schemes for grids in spherical coordinates. Monthly Weather Review, 32 | 127(9), 2204-2210. 33 | 34 | .. [2] Ullrich, P. A., Lauritzen, P. H., & Jablonowski, C. (2009). 35 | Geometrically exact conservative remapping (GECoRe): regular 36 | latitude–longitude and cubed-sphere grids. Monthly Weather Review, 37 | 137(6), 1721-1741. 38 | ''' 39 | # degree to radius, make a copy 40 | lat = lat/180.0*np.pi 41 | lon = lon/180.0*np.pi 42 | 43 | f = 2 + np.cos(lat)**2 * np.cos(2*lon) 44 | return f 45 | -------------------------------------------------------------------------------- /xesmf/frontend.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Frontend for xESMF, exposed to users. 3 | ''' 4 | 5 | import numpy as np 6 | import xarray as xr 7 | import os 8 | import warnings 9 | 10 | from . backend import (esmf_grid, esmf_locstream, add_corner, 11 | esmf_regrid_build, esmf_regrid_finalize) 12 | 13 | from . smm import read_weights, apply_weights 14 | 15 | try: 16 | import dask.array as da 17 | dask_array_type = (da.Array,) # for isinstance checks 18 | except ImportError: 19 | dask_array_type = () 20 | 21 | def as_2d_mesh(lon, lat): 22 | 23 | if (lon.ndim, lat.ndim) == (2, 2): 24 | assert lon.shape == lat.shape, 'lon and lat should have same shape' 25 | elif (lon.ndim, lat.ndim) == (1, 1): 26 | lon, lat = np.meshgrid(lon, lat) 27 | else: 28 | raise ValueError('lon and lat should be both 1D or 2D') 29 | 30 | return lon, lat 31 | 32 | 33 | def ds_to_ESMFgrid(ds, need_bounds=False, periodic=None, append=None): 34 | ''' 35 | Convert xarray DataSet or dictionary to ESMF.Grid object. 36 | 37 | Parameters 38 | ---------- 39 | ds : xarray DataSet or dictionary 40 | Contains variables ``lon``, ``lat``, 41 | and optionally ``lon_b``, ``lat_b`` if need_bounds=True. 42 | 43 | Shape should be ``(n_lat, n_lon)`` or ``(n_y, n_x)``, 44 | as normal C or Python ordering. Will be then tranposed to F-ordered. 45 | 46 | need_bounds : bool, optional 47 | Need cell boundary values? 48 | 49 | periodic : bool, optional 50 | Periodic in longitude? 51 | 52 | Returns 53 | ------- 54 | grid : ESMF.Grid object 55 | 56 | ''' 57 | 58 | # use np.asarray(dr) instead of dr.values, so it also works for dictionary 59 | lon = np.asarray(ds['lon']) 60 | lat = np.asarray(ds['lat']) 61 | lon, lat = as_2d_mesh(lon, lat) 62 | 63 | # tranpose the arrays so they become Fortran-ordered 64 | grid = esmf_grid(lon.T, lat.T, periodic=periodic) 65 | 66 | if need_bounds: 67 | lon_b = np.asarray(ds['lon_b']) 68 | lat_b = np.asarray(ds['lat_b']) 69 | lon_b, lat_b = as_2d_mesh(lon_b, lat_b) 70 | add_corner(grid, lon_b.T, lat_b.T) 71 | 72 | return grid, lon.shape 73 | 74 | 75 | def ds_to_ESMFlocstream(ds): 76 | ''' 77 | Convert xarray DataSet or dictionary to ESMF.LocStream object. 78 | 79 | Parameters 80 | ---------- 81 | ds : xarray DataSet or dictionary 82 | Contains variables ``lon``, ``lat``. 83 | 84 | Returns 85 | ------- 86 | locstream : ESMF.LocStream object 87 | 88 | ''' 89 | 90 | lon = np.asarray(ds['lon']) 91 | lat = np.asarray(ds['lat']) 92 | 93 | if len(lon.shape) > 1: 94 | raise ValueError("lon can only be 1d") 95 | if len(lat.shape) > 1: 96 | raise ValueError("lat can only be 1d") 97 | 98 | assert lon.shape == lat.shape 99 | 100 | locstream = esmf_locstream(lon, lat) 101 | 102 | return locstream, (1,) + lon.shape 103 | 104 | 105 | class Regridder(object): 106 | def __init__(self, ds_in, ds_out, method, periodic=False, 107 | filename=None, reuse_weights=False, ignore_degenerate=None, 108 | locstream_in=False, locstream_out=False): 109 | """ 110 | Make xESMF regridder 111 | 112 | Parameters 113 | ---------- 114 | ds_in, ds_out : xarray DataSet, or dictionary 115 | Contain input and output grid coordinates. Look for variables 116 | ``lon``, ``lat``, and optionally ``lon_b``, ``lat_b`` for 117 | conservative method. 118 | 119 | Shape can be 1D (n_lon,) and (n_lat,) for rectilinear grids, 120 | or 2D (n_y, n_x) for general curvilinear grids. 121 | Shape of bounds should be (n+1,) or (n_y+1, n_x+1). 122 | 123 | method : str 124 | Regridding method. Options are 125 | 126 | - 'bilinear' 127 | - 'conservative', **need grid corner information** 128 | - 'patch' 129 | - 'nearest_s2d' 130 | - 'nearest_d2s' 131 | 132 | periodic : bool, optional 133 | Periodic in longitude? Default to False. 134 | Only useful for global grids with non-conservative regridding. 135 | Will be forced to False for conservative regridding. 136 | 137 | filename : str, optional 138 | Name for the weight file. The default naming scheme is:: 139 | 140 | {method}_{Ny_in}x{Nx_in}_{Ny_out}x{Nx_out}.nc 141 | 142 | e.g. bilinear_400x600_300x400.nc 143 | 144 | reuse_weights : bool, optional 145 | Whether to read existing weight file to save computing time. 146 | False by default (i.e. re-compute, not reuse). 147 | 148 | ignore_degenerate : bool, optional 149 | If False (default), raise error if grids contain degenerated cells 150 | (i.e. triangles or lines, instead of quadrilaterals) 151 | 152 | locstream_in: bool, optional 153 | input is a LocStream (list of locations) 154 | 155 | locstream_out: bool, optional 156 | output is a LocStream (list of locations) 157 | 158 | Returns 159 | ------- 160 | regridder : xESMF regridder object 161 | 162 | """ 163 | 164 | # record basic switches 165 | if method == 'conservative': 166 | self.need_bounds = True 167 | periodic = False # bound shape will not be N+1 for periodic grid 168 | else: 169 | self.need_bounds = False 170 | 171 | self.method = method 172 | self.periodic = periodic 173 | self.reuse_weights = reuse_weights 174 | self.ignore_degenerate = ignore_degenerate 175 | self.locstream_in = locstream_in 176 | self.locstream_out = locstream_out 177 | 178 | methods_avail_ls_in = ['nearest_s2d', 'nearest_d2s'] 179 | methods_avail_ls_out = ['bilinear', 'patch'] + methods_avail_ls_in 180 | 181 | if locstream_in and self.method not in methods_avail_ls_in: 182 | raise ValueError(f'locstream input is only available for method in {methods_avail_ls_in}') 183 | if locstream_out and self.method not in methods_avail_ls_out: 184 | raise ValueError(f'locstream output is only available for method in {methods_avail_ls_out}') 185 | 186 | # construct ESMF grid, with some shape checking 187 | if locstream_in: 188 | self._grid_in, shape_in = ds_to_ESMFlocstream(ds_in) 189 | else: 190 | self._grid_in, shape_in = ds_to_ESMFgrid(ds_in, 191 | need_bounds=self.need_bounds, 192 | periodic=periodic 193 | ) 194 | if locstream_out: 195 | self._grid_out, shape_out = ds_to_ESMFlocstream(ds_out) 196 | else: 197 | self._grid_out, shape_out = ds_to_ESMFgrid(ds_out, 198 | need_bounds=self.need_bounds 199 | ) 200 | 201 | # record output grid and metadata 202 | self._lon_out = np.asarray(ds_out['lon']) 203 | self._lat_out = np.asarray(ds_out['lat']) 204 | 205 | if self._lon_out.ndim == 2: 206 | try: 207 | self.lon_dim = self.lat_dim = ds_out['lon'].dims 208 | except: 209 | self.lon_dim = self.lat_dim = ('y', 'x') 210 | 211 | self.out_horiz_dims = self.lon_dim 212 | 213 | elif self._lon_out.ndim == 1: 214 | try: 215 | self.lon_dim, = ds_out['lon'].dims 216 | self.lat_dim, = ds_out['lat'].dims 217 | except: 218 | self.lon_dim = 'lon' 219 | self.lat_dim = 'lat' 220 | 221 | self.out_horiz_dims = (self.lat_dim, self.lon_dim) 222 | 223 | # record grid shape information 224 | self.shape_in = shape_in 225 | self.shape_out = shape_out 226 | self.n_in = shape_in[0] * shape_in[1] 227 | self.n_out = shape_out[0] * shape_out[1] 228 | 229 | if filename is None: 230 | self.filename = self._get_default_filename() 231 | else: 232 | self.filename = filename 233 | 234 | # get weight matrix 235 | self._write_weight_file() 236 | self.weights = read_weights(self.filename, self.n_in, self.n_out) 237 | 238 | @property 239 | def A(self): 240 | message = ( 241 | "regridder.A is deprecated and will be removed in future versions. " 242 | "Use regridder.weights instead." 243 | ) 244 | 245 | warnings.warn(message, DeprecationWarning) 246 | # DeprecationWarning seems to be ignored by certain Python environments 247 | # Also print to make sure users notice this. 248 | print(message) 249 | return self.weights 250 | 251 | def _get_default_filename(self): 252 | # e.g. bilinear_400x600_300x400.nc 253 | filename = ('{0}_{1}x{2}_{3}x{4}'.format(self.method, 254 | self.shape_in[0], self.shape_in[1], 255 | self.shape_out[0], self.shape_out[1],) 256 | ) 257 | if self.periodic: 258 | filename += '_peri.nc' 259 | else: 260 | filename += '.nc' 261 | 262 | return filename 263 | 264 | def _write_weight_file(self): 265 | 266 | if os.path.exists(self.filename): 267 | if self.reuse_weights: 268 | print('Reuse existing file: {}'.format(self.filename)) 269 | return # do not compute it again, just read it 270 | else: 271 | print('Overwrite existing file: {} \n'.format(self.filename), 272 | 'You can set reuse_weights=True to save computing time.') 273 | os.remove(self.filename) 274 | else: 275 | print('Create weight file: {}'.format(self.filename)) 276 | 277 | regrid = esmf_regrid_build(self._grid_in, self._grid_out, self.method, 278 | filename=self.filename, 279 | ignore_degenerate=self.ignore_degenerate) 280 | esmf_regrid_finalize(regrid) # only need weights, not regrid object 281 | 282 | def clean_weight_file(self): 283 | """ 284 | Remove the offline weight file on disk. 285 | 286 | To save the time on re-computing weights, you can just keep the file, 287 | and set "reuse_weights=True" when initializing the regridder next time. 288 | """ 289 | if os.path.exists(self.filename): 290 | print("Remove file {}".format(self.filename)) 291 | os.remove(self.filename) 292 | else: 293 | print("File {} is already removed.".format(self.filename)) 294 | 295 | def __repr__(self): 296 | info = ('xESMF Regridder \n' 297 | 'Regridding algorithm: {} \n' 298 | 'Weight filename: {} \n' 299 | 'Reuse pre-computed weights? {} \n' 300 | 'Input grid shape: {} \n' 301 | 'Output grid shape: {} \n' 302 | 'Output grid dimension name: {} \n' 303 | 'Periodic in longitude? {}' 304 | .format(self.method, 305 | self.filename, 306 | self.reuse_weights, 307 | self.shape_in, 308 | self.shape_out, 309 | self.out_horiz_dims, 310 | self.periodic) 311 | ) 312 | 313 | return info 314 | 315 | def __call__(self, indata, keep_attrs=False): 316 | """ 317 | Apply regridding to input data. 318 | 319 | Parameters 320 | ---------- 321 | indata : numpy array, dask array, xarray DataArray or Dataset. 322 | The rightmost two dimensions must be the same as ``ds_in``. 323 | Can have arbitrary additional dimensions. 324 | 325 | Examples of valid shapes 326 | 327 | - (n_lat, n_lon), if ``ds_in`` has shape (n_lat, n_lon) 328 | - (n_time, n_lev, n_y, n_x), if ``ds_in`` has shape (Ny, n_x) 329 | 330 | Transpose your input data if the horizontal dimensions are not 331 | the rightmost two dimensions. 332 | 333 | keep_attrs : bool, optional 334 | Keep attributes for xarray DataArrays or Datasets. 335 | Defaults to False. 336 | 337 | Returns 338 | ------- 339 | outdata : Data type is the same as input data type. 340 | On the same horizontal grid as ``ds_out``, 341 | with extra dims in ``dr_in``. 342 | 343 | Assuming ``ds_out`` has the shape of (n_y_out, n_x_out), 344 | examples of returning shapes are 345 | 346 | - (n_y_out, n_x_out), if ``dr_in`` is 2D 347 | - (n_time, n_lev, n_y_out, n_x_out), if ``dr_in`` has shape 348 | (n_time, n_lev, n_y, n_x) 349 | 350 | """ 351 | 352 | if isinstance(indata, np.ndarray): 353 | return self.regrid_numpy(indata) 354 | elif isinstance(indata, dask_array_type): 355 | return self.regrid_dask(indata) 356 | elif isinstance(indata, xr.DataArray): 357 | return self.regrid_dataarray(indata, keep_attrs=keep_attrs) 358 | elif isinstance(indata, xr.Dataset): 359 | return self.regrid_dataset(indata, keep_attrs=keep_attrs) 360 | else: 361 | raise TypeError( 362 | "input must be numpy array, dask array, " 363 | "xarray DataArray or Dataset!") 364 | 365 | def regrid_numpy(self, indata): 366 | """See __call__().""" 367 | 368 | if self.locstream_in: 369 | indata = np.expand_dims(indata, axis=-2) 370 | 371 | outdata = apply_weights(self.weights, indata, 372 | self.shape_in, self.shape_out) 373 | return outdata 374 | 375 | def regrid_dask(self, indata): 376 | """See __call__().""" 377 | 378 | extra_chunk_shape = indata.chunksize[0:-2] 379 | 380 | output_chunk_shape = extra_chunk_shape + self.shape_out 381 | 382 | outdata = da.map_blocks( 383 | self.regrid_numpy, 384 | indata, 385 | dtype=float, 386 | chunks=output_chunk_shape 387 | ) 388 | 389 | return outdata 390 | 391 | def regrid_dataarray(self, dr_in, keep_attrs=False): 392 | """See __call__().""" 393 | 394 | # example: ('lat', 'lon') or ('y', 'x') 395 | if self.locstream_in: 396 | input_horiz_dims = dr_in.dims[-1:] 397 | else: 398 | input_horiz_dims = dr_in.dims[-2:] 399 | 400 | # apply_ufunc needs a different name for output_core_dims 401 | # example: ('lat', 'lon') -> ('lat_new', 'lon_new') 402 | # https://github.com/pydata/xarray/issues/1931#issuecomment-367417542 403 | if self.locstream_out: 404 | temp_horiz_dims = ['dummy', 'locations'] 405 | else: 406 | temp_horiz_dims = [s + '_new' for s in input_horiz_dims] 407 | 408 | if self.locstream_in and not self.locstream_out: 409 | temp_horiz_dims = ['dummy_new'] + temp_horiz_dims 410 | 411 | 412 | dr_out = xr.apply_ufunc( 413 | self.regrid_numpy, dr_in, 414 | input_core_dims=[input_horiz_dims], 415 | output_core_dims=[temp_horiz_dims], 416 | dask='parallelized', 417 | output_dtypes=[float], 418 | output_sizes={temp_horiz_dims[0]: self.shape_out[0], 419 | temp_horiz_dims[1]: self.shape_out[1] 420 | }, 421 | keep_attrs=keep_attrs 422 | ) 423 | 424 | if not self.locstream_out: 425 | # rename dimension name to match output grid 426 | dr_out = dr_out.rename( 427 | {temp_horiz_dims[0]: self.out_horiz_dims[0], 428 | temp_horiz_dims[1]: self.out_horiz_dims[1] 429 | } 430 | ) 431 | 432 | # append output horizontal coordinate values 433 | # extra coordinates are automatically tracked by apply_ufunc 434 | if self.locstream_out: 435 | dr_out.coords['lon'] = xr.DataArray(self._lon_out, dims=('locations',)) 436 | dr_out.coords['lat'] = xr.DataArray(self._lat_out, dims=('locations',)) 437 | else: 438 | dr_out.coords['lon'] = xr.DataArray(self._lon_out, dims=self.lon_dim) 439 | dr_out.coords['lat'] = xr.DataArray(self._lat_out, dims=self.lat_dim) 440 | 441 | dr_out.attrs['regrid_method'] = self.method 442 | 443 | if self.locstream_out: 444 | dr_out = dr_out.squeeze(dim='dummy') 445 | 446 | return dr_out 447 | 448 | def regrid_dataset(self, ds_in, keep_attrs=False): 449 | """See __call__().""" 450 | 451 | # most logic is the same as regrid_dataarray() 452 | # the major caution is that some data variables might not contain 453 | # the correct horizontal dimension names. 454 | 455 | # get the first data variable to infer input_core_dims 456 | name, dr_in = next(iter(ds_in.items())) 457 | 458 | if self.locstream_in: 459 | input_horiz_dims = dr_in.dims[-1:] 460 | else: 461 | input_horiz_dims = dr_in.dims[-2:] 462 | 463 | if self.locstream_out: 464 | temp_horiz_dims = ['dummy', 'locations'] 465 | else: 466 | temp_horiz_dims = [s + '_new' for s in input_horiz_dims] 467 | 468 | if self.locstream_in and not self.locstream_out: 469 | temp_horiz_dims = ['dummy_new'] + temp_horiz_dims 470 | 471 | # help user debugging invalid horizontal dimensions 472 | print('using dimensions {} from data variable {} ' 473 | 'as the horizontal dimensions for this dataset.' 474 | .format(input_horiz_dims, name) 475 | ) 476 | 477 | ds_out = xr.apply_ufunc( 478 | self.regrid_numpy, ds_in, 479 | input_core_dims=[input_horiz_dims], 480 | output_core_dims=[temp_horiz_dims], 481 | dask='parallelized', 482 | output_dtypes=[float], 483 | output_sizes={temp_horiz_dims[0]: self.shape_out[0], 484 | temp_horiz_dims[1]: self.shape_out[1] 485 | }, 486 | keep_attrs=keep_attrs 487 | ) 488 | 489 | if not self.locstream_out: 490 | # rename dimension name to match output grid 491 | ds_out = ds_out.rename( 492 | {temp_horiz_dims[0]: self.out_horiz_dims[0], 493 | temp_horiz_dims[1]: self.out_horiz_dims[1] 494 | } 495 | ) 496 | 497 | # append output horizontal coordinate values 498 | # extra coordinates are automatically tracked by apply_ufunc 499 | if self.locstream_out: 500 | ds_out.coords['lon'] = xr.DataArray(self._lon_out, dims=('locations',)) 501 | ds_out.coords['lat'] = xr.DataArray(self._lat_out, dims=('locations',)) 502 | else: 503 | ds_out.coords['lon'] = xr.DataArray(self._lon_out, dims=self.lon_dim) 504 | ds_out.coords['lat'] = xr.DataArray(self._lat_out, dims=self.lat_dim) 505 | 506 | ds_out.attrs['regrid_method'] = self.method 507 | 508 | if self.locstream_out: 509 | ds_out = ds_out.squeeze(dim='dummy') 510 | 511 | return ds_out 512 | -------------------------------------------------------------------------------- /xesmf/smm.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sparse matrix multiplication (SMM) using scipy.sparse library. 3 | """ 4 | 5 | import xarray as xr 6 | import scipy.sparse as sps 7 | import warnings 8 | 9 | 10 | def read_weights(filename, n_in, n_out): 11 | ''' 12 | Read regridding weights into a scipy sparse COO matrix. 13 | 14 | Parameters 15 | ---------- 16 | filename : str 17 | Offline weight file generated by ESMPy. 18 | 19 | N_in, N_out : integers 20 | ``(N_out, N_in)`` will be the shape of the returning sparse matrix. 21 | They are the total number of grid boxes in input and output grids:: 22 | 23 | N_in = Nx_in * Ny_in 24 | N_out = Nx_out * Ny_out 25 | 26 | We need them because the shape cannot always be infered from the 27 | largest column and row indices, due to unmapped grid boxes. 28 | 29 | Returns 30 | ------- 31 | A : scipy sparse COO matrix. 32 | 33 | ''' 34 | ds_w = xr.open_dataset(filename) 35 | 36 | col = ds_w['col'].values - 1 # Python starts with 0 37 | row = ds_w['row'].values - 1 38 | S = ds_w['S'].values 39 | 40 | weights = sps.coo_matrix((S, (row, col)), shape=[n_out, n_in]) 41 | return weights 42 | 43 | 44 | def apply_weights(weights, indata, shape_in, shape_out): 45 | ''' 46 | Apply regridding weights to data. 47 | 48 | Parameters 49 | ---------- 50 | A : scipy sparse COO matrix 51 | 52 | indata : numpy array of shape ``(..., n_lat, n_lon)`` or ``(..., n_y, n_x)``. 53 | Should be C-ordered. Will be then tranposed to F-ordered. 54 | 55 | shape_in, shape_out : tuple of two integers 56 | Input/output data shape for unflatten operation. 57 | For rectilinear grid, it is just ``(n_lat, n_lon)``. 58 | 59 | Returns 60 | ------- 61 | outdata : numpy array of shape ``(..., shape_out[0], shape_out[1])``. 62 | Extra dimensions are the same as `indata`. 63 | If input data is C-ordered, output will also be C-ordered. 64 | ''' 65 | 66 | # COO matrix is fast with F-ordered array but slow with C-array, so we 67 | # take in a C-ordered and then transpose) 68 | # (CSR or CRS matrix is fast with C-ordered array but slow with F-array) 69 | if not indata.flags['C_CONTIGUOUS']: 70 | warnings.warn("Input array is not C_CONTIGUOUS. " 71 | "Will affect performance.") 72 | 73 | # get input shape information 74 | shape_horiz = indata.shape[-2:] 75 | extra_shape = indata.shape[0:-2] 76 | 77 | assert shape_horiz == shape_in, ( 78 | 'The horizontal shape of input data is {}, different from that of' 79 | 'the regridder {}!'.format(shape_horiz, shape_in) 80 | ) 81 | 82 | assert shape_in[0] * shape_in[1] == weights.shape[1], ( 83 | "ny_in * nx_in should equal to weights.shape[1]") 84 | 85 | assert shape_out[0] * shape_out[1] == weights.shape[0], ( 86 | "ny_out * nx_out should equal to weights.shape[0]") 87 | 88 | # use flattened array for dot operation 89 | indata_flat = indata.reshape(-1, shape_in[0]*shape_in[1]) 90 | outdata_flat = weights.dot(indata_flat.T).T 91 | 92 | # unflattened output array 93 | outdata = outdata_flat.reshape( 94 | [*extra_shape, shape_out[0], shape_out[1]]) 95 | return outdata 96 | -------------------------------------------------------------------------------- /xesmf/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiaweiZhuang/xESMF/2d2e365065711337286404e36fcb23c0def9eba8/xesmf/tests/__init__.py -------------------------------------------------------------------------------- /xesmf/tests/test_backend.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import ESMF 4 | import xesmf as xe 5 | from xesmf.backend import (warn_f_contiguous, warn_lat_range, 6 | esmf_grid, add_corner, esmf_locstream, 7 | esmf_regrid_build, esmf_regrid_apply, 8 | esmf_regrid_finalize) 9 | from xesmf.smm import read_weights, apply_weights 10 | 11 | from numpy.testing import assert_equal, assert_almost_equal 12 | import pytest 13 | 14 | # We use pure numpy arrays to test backend 15 | # xarray DataSet is only used at the very beginning as a quick way to make data 16 | coord_names = ['lon', 'lat', 'lon_b', 'lat_b'] 17 | 18 | ds_in = xe.util.grid_global(20, 12) 19 | lon_in, lat_in, lon_b_in, lat_b_in = [ds_in[name].values 20 | for name in coord_names] 21 | 22 | ds_out = xe.util.grid_global(15, 9) 23 | lon_out, lat_out, lon_b_out, lat_b_out = [ds_out[name].values 24 | for name in coord_names] 25 | 26 | # shortcut to test a single grid 27 | lon, lat, lon_b, lat_b = [lon_in, lat_in, lon_b_in, lat_b_in] 28 | 29 | # input test data 30 | ds_in['data'] = xe.data.wave_smooth(ds_in['lon'], ds_in['lat']) 31 | data_in = ds_in['data'].values 32 | 33 | # reference output data, calculated analytically 34 | ds_out['data_ref'] = xe.data.wave_smooth(ds_out['lon'], ds_out['lat']) 35 | data_ref = ds_out['data_ref'].values 36 | 37 | # 4D data to test broadcasting, increasing linearly with time and lev 38 | ds_in.coords['time'] = np.arange(1, 11) 39 | ds_in.coords['lev'] = np.arange(1, 51) 40 | ds_in['data4D'] = ds_in['time'] * ds_in['lev'] * ds_in['data'] 41 | data4D_in = ds_in['data4D'].values 42 | 43 | 44 | def test_warn_f_on_array(): 45 | a = np.zeros([2, 2], order='C') 46 | with pytest.warns(UserWarning): 47 | warn_f_contiguous(a) 48 | 49 | 50 | def test_warn_f_on_grid(): 51 | # should throw a warning if not passing transpose 52 | with pytest.warns(UserWarning): 53 | esmf_grid(lon, lat) 54 | 55 | 56 | def test_warn_lat_range(): 57 | # latitude goes to -100 (invalid value) 58 | ds_temp = xe.util.grid_2d(-180, 180, 10, -100, 90, 5) 59 | with pytest.warns(UserWarning): 60 | warn_lat_range(ds_temp['lat'].values) 61 | with pytest.warns(UserWarning): 62 | warn_lat_range(ds_temp['lat_b'].values) 63 | 64 | 65 | def test_esmf_grid_with_corner(): 66 | 67 | # only center coordinate, no corners 68 | # remember to pass transpose (F-ordered) to backend 69 | grid = esmf_grid(lon.T, lat.T) 70 | 71 | # make sure coordinate values agree 72 | assert_equal(grid.coords[0][0], lon.T) 73 | assert_equal(grid.coords[0][1], lat.T) 74 | 75 | # make sure meta data agree 76 | assert not grid.has_corners # no corner yet! 77 | assert grid.staggerloc == [True, False, False, False] 78 | assert grid.coord_sys is ESMF.CoordSys.SPH_DEG 79 | assert grid.rank == 2 80 | assert_equal(grid.size[0], lon.T.shape) 81 | assert_equal(grid.upper_bounds[0], lon.T.shape) 82 | assert_equal(grid.lower_bounds[0], np.array([0, 0])) 83 | 84 | # now add corner information 85 | add_corner(grid, lon_b.T, lat_b.T) 86 | 87 | # coordinate values 88 | assert_equal(grid.coords[3][0], lon_b.T) 89 | assert_equal(grid.coords[3][1], lat_b.T) 90 | 91 | # metadata 92 | assert grid.has_corners # should have corner now 93 | assert grid.staggerloc == [True, False, False, True] 94 | assert_equal(grid.size[3], lon_b.T.shape) 95 | assert_equal(grid.upper_bounds[3], lon_b.T.shape) 96 | assert_equal(grid.lower_bounds[3], np.array([0, 0])) 97 | 98 | 99 | def test_esmf_build_bilinear(): 100 | 101 | grid_in = esmf_grid(lon_in.T, lat_in.T) 102 | grid_out = esmf_grid(lon_out.T, lat_out.T) 103 | 104 | regrid = esmf_regrid_build(grid_in, grid_out, 'bilinear') 105 | assert regrid.unmapped_action is ESMF.UnmappedAction.IGNORE 106 | assert regrid.regrid_method is ESMF.RegridMethod.BILINEAR 107 | 108 | # they should share the same memory 109 | regrid.srcfield.grid is grid_in 110 | regrid.dstfield.grid is grid_out 111 | 112 | esmf_regrid_finalize(regrid) 113 | 114 | 115 | def test_regrid(): 116 | 117 | # use conservative regridding as an example, 118 | # since it is the most well-tested studied one in papers 119 | 120 | # TODO: possible to break this long test into smaller tests? 121 | # not easy due to strong dependencies. 122 | 123 | grid_in = esmf_grid(lon_in.T, lat_in.T) 124 | grid_out = esmf_grid(lon_out.T, lat_out.T) 125 | 126 | # no corner info yet, should not be able to use conservative 127 | with pytest.raises(ValueError): 128 | esmf_regrid_build(grid_in, grid_out, 'conservative') 129 | 130 | # now add corners 131 | add_corner(grid_in, lon_b_in.T, lat_b_in.T) 132 | add_corner(grid_out, lon_b_out.T, lat_b_out.T) 133 | 134 | # also write to file for scipy regridding 135 | filename = 'test_weights.nc' 136 | if os.path.exists(filename): 137 | os.remove(filename) 138 | regrid = esmf_regrid_build(grid_in, grid_out, 'conservative', 139 | filename=filename) 140 | assert regrid.regrid_method is ESMF.RegridMethod.CONSERVE 141 | 142 | # apply regridding using ESMPy's native method 143 | data_out_esmpy = esmf_regrid_apply(regrid, data_in.T).T 144 | 145 | rel_err = (data_out_esmpy - data_ref)/data_ref # relative error 146 | assert np.max(np.abs(rel_err)) < 0.05 147 | 148 | # apply regridding using scipy 149 | weights = read_weights(filename, lon_in.size, lon_out.size) 150 | shape_in = lon_in.shape 151 | shape_out = lon_out.shape 152 | data_out_scipy = apply_weights(weights, data_in, shape_in, shape_out) 153 | 154 | # must be exactly the same as esmpy's result! 155 | # TODO: this fails once but I cannot replicate it. 156 | # Maybe assert_equal is too strict for scipy vs esmpy comparision 157 | assert_equal(data_out_scipy, data_out_esmpy) 158 | 159 | # finally, test broadcasting with scipy 160 | # TODO: need to test broadcasting with ESMPy backend? 161 | # We only use Scipy in frontend, and ESMPy is just for backend benchmark 162 | # However, it is useful to compare performance and show scipy is 3x faster 163 | data4D_out = apply_weights(weights, data4D_in, shape_in, shape_out) 164 | 165 | # data over broadcasting dimensions should agree 166 | assert_almost_equal(data4D_in.mean(axis=(2, 3)), 167 | data4D_out.mean(axis=(2, 3)), 168 | decimal=10) 169 | 170 | # clean-up 171 | esmf_regrid_finalize(regrid) 172 | os.remove(filename) 173 | 174 | 175 | def test_regrid_periodic_wrong(): 176 | 177 | # not using periodic grid 178 | grid_in = esmf_grid(lon_in.T, lat_in.T) 179 | grid_out = esmf_grid(lon_out.T, lat_out.T) 180 | 181 | assert grid_in.num_peri_dims == 0 182 | assert grid_in.periodic_dim is None 183 | 184 | regrid = esmf_regrid_build(grid_in, grid_out, 'bilinear') 185 | data_out_esmpy = esmf_regrid_apply(regrid, data_in.T).T 186 | 187 | rel_err = (data_out_esmpy - data_ref)/data_ref # relative error 188 | assert np.max(np.abs(rel_err)) == 1.0 # some data will be missing 189 | 190 | # clean-up 191 | esmf_regrid_finalize(regrid) 192 | 193 | 194 | def test_regrid_periodic_correct(): 195 | 196 | # only need to specific periodic for input grid 197 | grid_in = esmf_grid(lon_in.T, lat_in.T, periodic=True) 198 | grid_out = esmf_grid(lon_out.T, lat_out.T) 199 | 200 | assert grid_in.num_peri_dims == 1 201 | assert grid_in.periodic_dim == 0 # the first axis, longitude 202 | 203 | regrid = esmf_regrid_build(grid_in, grid_out, 'bilinear') 204 | data_out_esmpy = esmf_regrid_apply(regrid, data_in.T).T 205 | 206 | rel_err = (data_out_esmpy - data_ref)/data_ref # relative error 207 | assert np.max(np.abs(rel_err)) < 0.065 208 | # clean-up 209 | esmf_regrid_finalize(regrid) 210 | 211 | 212 | def test_esmf_locstream(): 213 | lon = np.arange(5) 214 | lat = np.arange(5) 215 | 216 | ls = esmf_locstream(lon, lat) 217 | assert isinstance(ls, ESMF.LocStream) 218 | 219 | lon2d, lat2d = np.meshgrid(lon, lat) 220 | with pytest.raises(ValueError): 221 | ls = esmf_locstream(lon2d, lat2d) 222 | with pytest.raises(ValueError): 223 | ls = esmf_locstream(lon, lat2d) 224 | with pytest.raises(ValueError): 225 | ls = esmf_locstream(lon2d, lat) 226 | -------------------------------------------------------------------------------- /xesmf/tests/test_frontend.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import xarray as xr 4 | import xesmf as xe 5 | from xesmf.frontend import as_2d_mesh 6 | 7 | from numpy.testing import assert_equal, assert_almost_equal 8 | import pytest 9 | 10 | # same test data as test_backend.py, but here we can use xarray DataSet 11 | ds_in = xe.util.grid_global(20, 12) 12 | ds_out = xe.util.grid_global(15, 9) 13 | 14 | horiz_shape_in = ds_in['lon'].shape 15 | horiz_shape_out = ds_out['lon'].shape 16 | 17 | ds_in['data'] = xe.data.wave_smooth(ds_in['lon'], ds_in['lat']) 18 | ds_out['data_ref'] = xe.data.wave_smooth(ds_out['lon'], ds_out['lat']) 19 | 20 | # 4D data to test broadcasting, increasing linearly with time and lev 21 | ds_in.coords['time'] = np.arange(7) + 1 22 | ds_in.coords['lev'] = np.arange(11) + 1 23 | ds_in['data4D'] = ds_in['time'] * ds_in['lev'] * ds_in['data'] 24 | ds_out['data4D_ref'] = ds_in['time'] * ds_in['lev'] * ds_out['data_ref'] 25 | 26 | # use non-divisible chunk size to catch edge cases 27 | ds_in_chunked = ds_in.chunk({'time': 3, 'lev': 2}) 28 | 29 | ds_locs = xr.Dataset() 30 | ds_locs['lat'] = xr.DataArray(data=[-20, -10, 0, 10], dims=('locations',)) 31 | ds_locs['lon'] = xr.DataArray(data=[0, 5, 10, 15], dims=('locations',)) 32 | 33 | def test_as_2d_mesh(): 34 | # 2D grid should not change 35 | lon2d = ds_in['lon'].values 36 | lat2d = ds_in['lat'].values 37 | assert_equal((lon2d, lat2d), as_2d_mesh(lon2d, lat2d)) 38 | 39 | # 1D grid should become 2D 40 | lon1d = lon2d[0, :] 41 | lat1d = lat2d[:, 0] 42 | assert_equal((lon2d, lat2d), as_2d_mesh(lon1d, lat1d)) 43 | 44 | # mix of 1D and 2D should fail 45 | with pytest.raises(ValueError): 46 | as_2d_mesh(lon1d, lat2d) 47 | 48 | 49 | # 'patch' is too slow to test 50 | methods_list = ['bilinear', 'conservative', 'nearest_s2d', 'nearest_d2s'] 51 | 52 | @pytest.mark.parametrize("locstream_in,locstream_out,method", [ 53 | (False, False, 'conservative'), 54 | (False, False, 'bilinear'), 55 | (False, True, 'bilinear'), 56 | (False, False, 'nearest_s2d'), 57 | (False, True, 'nearest_s2d'), 58 | (True, False, 'nearest_s2d'), 59 | (True, True, 'nearest_s2d'), 60 | (False, False, 'nearest_d2s'), 61 | (False, True, 'nearest_d2s'), 62 | (True, False, 'nearest_d2s'), 63 | (True, True, 'nearest_d2s') 64 | ]) 65 | def test_build_regridder(method, locstream_in, locstream_out): 66 | din = ds_locs if locstream_in else ds_in 67 | dout = ds_locs if locstream_out else ds_out 68 | 69 | regridder = xe.Regridder(din, dout, method, 70 | locstream_in=locstream_in, 71 | locstream_out=locstream_out) 72 | 73 | # check screen output 74 | assert repr(regridder) == str(regridder) 75 | assert 'xESMF Regridder' in str(regridder) 76 | assert method in str(regridder) 77 | 78 | regridder.clean_weight_file() 79 | 80 | 81 | def test_existing_weights(): 82 | # the first run 83 | method = 'bilinear' 84 | regridder = xe.Regridder(ds_in, ds_out, method) 85 | 86 | # make sure we can reuse weights 87 | assert os.path.exists(regridder.filename) 88 | regridder_reuse = xe.Regridder(ds_in, ds_out, method, 89 | reuse_weights=True) 90 | assert regridder_reuse.A.shape == regridder.A.shape 91 | 92 | # or can also overwrite it 93 | xe.Regridder(ds_in, ds_out, method) 94 | 95 | # clean-up 96 | regridder.clean_weight_file() 97 | assert not os.path.exists(regridder.filename) 98 | 99 | 100 | def test_conservative_without_bounds(): 101 | with pytest.raises(KeyError): 102 | xe.Regridder(ds_in.drop_vars('lon_b'), ds_out, 'conservative') 103 | 104 | 105 | def test_build_regridder_from_dict(): 106 | lon_in = ds_in['lon'].values 107 | lat_in = ds_in['lat'].values 108 | lon_out = ds_out['lon'].values 109 | lat_out = ds_out['lat'].values 110 | regridder = xe.Regridder({'lon': lon_in, 'lat': lat_in}, 111 | {'lon': lon_out, 'lat': lat_out}, 112 | 'bilinear') 113 | regridder.clean_weight_file() 114 | 115 | 116 | def test_regrid_periodic_wrong(): 117 | # not using periodic option 118 | regridder = xe.Regridder(ds_in, ds_out, 'bilinear') 119 | 120 | dr_out = regridder(ds_in['data']) # xarray DataArray 121 | 122 | # compare with analytical solution 123 | rel_err = (ds_out['data_ref'] - dr_out)/ds_out['data_ref'] 124 | assert np.max(np.abs(rel_err)) == 1.0 # some data will be missing 125 | 126 | # clean-up 127 | regridder.clean_weight_file() 128 | 129 | 130 | def test_regrid_periodic_correct(): 131 | regridder = xe.Regridder(ds_in, ds_out, 'bilinear', periodic=True) 132 | 133 | dr_out = regridder(ds_in['data']) 134 | 135 | # compare with analytical solution 136 | rel_err = (ds_out['data_ref'] - dr_out)/ds_out['data_ref'] 137 | assert np.max(np.abs(rel_err)) < 0.065 138 | 139 | # clean-up 140 | regridder.clean_weight_file() 141 | 142 | 143 | def ds_2d_to_1d(ds): 144 | ds_temp = ds.reset_coords() 145 | ds_1d = xr.merge([ds_temp['lon'][0, :], ds_temp['lat'][:, 0]]) 146 | ds_1d.coords['lon'] = ds_1d['lon'] 147 | ds_1d.coords['lat'] = ds_1d['lat'] 148 | return ds_1d 149 | 150 | 151 | def test_regrid_with_1d_grid(): 152 | ds_in_1d = ds_2d_to_1d(ds_in) 153 | ds_out_1d = ds_2d_to_1d(ds_out) 154 | 155 | regridder = xe.Regridder(ds_in_1d, ds_out_1d, 'bilinear', periodic=True) 156 | 157 | dr_out = regridder(ds_in['data']) 158 | 159 | # compare with analytical solution 160 | rel_err = (ds_out['data_ref'] - dr_out)/ds_out['data_ref'] 161 | assert np.max(np.abs(rel_err)) < 0.065 162 | 163 | # metadata should be 1D 164 | assert_equal(dr_out['lon'].values, ds_out_1d['lon'].values) 165 | assert_equal(dr_out['lat'].values, ds_out_1d['lat'].values) 166 | 167 | # clean-up 168 | regridder.clean_weight_file() 169 | 170 | 171 | # TODO: consolidate (regrid method, input data types) combination 172 | # using pytest fixtures and parameterization 173 | 174 | def test_regrid_dataarray(): 175 | # xarray.DataArray containing in-memory numpy array 176 | 177 | regridder = xe.Regridder(ds_in, ds_out, 'conservative') 178 | 179 | outdata = regridder(ds_in['data'].values) # pure numpy array 180 | dr_out = regridder(ds_in['data']) # xarray DataArray 181 | 182 | # DataArray and numpy array should lead to the same result 183 | assert_equal(outdata, dr_out.values) 184 | 185 | # compare with analytical solution 186 | rel_err = (ds_out['data_ref'] - dr_out)/ds_out['data_ref'] 187 | assert np.max(np.abs(rel_err)) < 0.05 188 | 189 | # check metadata 190 | assert_equal(dr_out['lat'].values, ds_out['lat'].values) 191 | assert_equal(dr_out['lon'].values, ds_out['lon'].values) 192 | 193 | # test broadcasting 194 | dr_out_4D = regridder(ds_in['data4D']) 195 | 196 | # data over broadcasting dimensions should agree 197 | assert_almost_equal(ds_in['data4D'].values.mean(axis=(2, 3)), 198 | dr_out_4D.values.mean(axis=(2, 3)), 199 | decimal=10) 200 | 201 | # check metadata 202 | xr.testing.assert_identical(dr_out_4D['time'], ds_in['time']) 203 | xr.testing.assert_identical(dr_out_4D['lev'], ds_in['lev']) 204 | 205 | # clean-up 206 | regridder.clean_weight_file() 207 | 208 | 209 | def test_regrid_dataarray_to_locstream(): 210 | # xarray.DataArray containing in-memory numpy array 211 | 212 | regridder = xe.Regridder(ds_in, ds_locs, 'bilinear', locstream_out=True) 213 | 214 | outdata = regridder(ds_in['data'].values) # pure numpy array 215 | dr_out = regridder(ds_in['data']) # xarray DataArray 216 | 217 | # DataArray and numpy array should lead to the same result 218 | assert_equal(outdata.squeeze(), dr_out.values) 219 | 220 | # clean-up 221 | regridder.clean_weight_file() 222 | 223 | with pytest.raises(ValueError): 224 | regridder = xe.Regridder(ds_in, ds_locs, 'conservative', locstream_out=True) 225 | 226 | 227 | def test_regrid_dataarray_from_locstream(): 228 | # xarray.DataArray containing in-memory numpy array 229 | 230 | regridder = xe.Regridder(ds_locs, ds_in, 'nearest_s2d', locstream_in=True) 231 | 232 | outdata = regridder(ds_locs['lat'].values) # pure numpy array 233 | dr_out = regridder(ds_locs['lat']) # xarray DataArray 234 | 235 | # DataArray and numpy array should lead to the same result 236 | assert_equal(outdata, dr_out.values) 237 | 238 | # clean-up 239 | regridder.clean_weight_file() 240 | 241 | with pytest.raises(ValueError): 242 | regridder = xe.Regridder(ds_locs, ds_in, 'bilinear', locstream_in=True) 243 | with pytest.raises(ValueError): 244 | regridder = xe.Regridder(ds_locs, ds_in, 'patch', locstream_in=True) 245 | with pytest.raises(ValueError): 246 | regridder = xe.Regridder(ds_locs, ds_in, 'conservative', locstream_in=True) 247 | 248 | 249 | def test_regrid_dask(): 250 | # chunked dask array (no xarray metadata) 251 | 252 | regridder = xe.Regridder(ds_in, ds_out, 'conservative') 253 | 254 | indata = ds_in_chunked['data4D'].data 255 | outdata = regridder(indata) 256 | 257 | # lazy dask arrays have incorrect shape attribute due to last chunk 258 | assert outdata.compute().shape == indata.shape[:-2] + horiz_shape_out 259 | assert outdata.chunksize == indata.chunksize[:-2] + horiz_shape_out 260 | 261 | outdata_ref = ds_out['data4D_ref'].values 262 | rel_err = (outdata.compute() - outdata_ref) / outdata_ref 263 | assert np.max(np.abs(rel_err)) < 0.05 264 | 265 | # clean-up 266 | regridder.clean_weight_file() 267 | 268 | 269 | def test_regrid_dask_to_locstream(): 270 | # chunked dask array (no xarray metadata) 271 | 272 | regridder = xe.Regridder(ds_in, ds_locs, 'bilinear', locstream_out=True) 273 | 274 | indata = ds_in_chunked['data4D'].data 275 | outdata = regridder(indata) 276 | 277 | # clean-up 278 | regridder.clean_weight_file() 279 | 280 | 281 | def test_regrid_dask_from_locstream(): 282 | # chunked dask array (no xarray metadata) 283 | 284 | regridder = xe.Regridder(ds_locs, ds_in, 'nearest_s2d', locstream_in=True) 285 | 286 | outdata = regridder(ds_locs['lat'].data) 287 | 288 | # clean-up 289 | regridder.clean_weight_file() 290 | 291 | 292 | def test_regrid_dataarray_dask(): 293 | # xarray.DataArray containing chunked dask array 294 | 295 | regridder = xe.Regridder(ds_in, ds_out, 'conservative') 296 | 297 | dr_in = ds_in_chunked['data4D'] 298 | dr_out = regridder(dr_in) 299 | 300 | assert dr_out.data.shape == dr_in.data.shape[:-2] + horiz_shape_out 301 | assert dr_out.data.chunksize == dr_in.data.chunksize[:-2] + horiz_shape_out 302 | 303 | # data over broadcasting dimensions should agree 304 | assert_almost_equal(dr_in.values.mean(axis=(2, 3)), 305 | dr_out.values.mean(axis=(2, 3)), 306 | decimal=10) 307 | 308 | # check metadata 309 | xr.testing.assert_identical(dr_out['time'], dr_in['time']) 310 | xr.testing.assert_identical(dr_out['lev'], dr_in['lev']) 311 | assert_equal(dr_out['lat'].values, ds_out['lat'].values) 312 | assert_equal(dr_out['lon'].values, ds_out['lon'].values) 313 | 314 | # clean-up 315 | regridder.clean_weight_file() 316 | 317 | 318 | def test_regrid_dataarray_dask_to_locstream(): 319 | # xarray.DataArray containing chunked dask array 320 | 321 | regridder = xe.Regridder(ds_in, ds_locs, 'bilinear', locstream_out=True) 322 | 323 | dr_in = ds_in_chunked['data4D'] 324 | dr_out = regridder(dr_in) 325 | 326 | # clean-up 327 | regridder.clean_weight_file() 328 | 329 | 330 | def test_regrid_dataarray_dask_from_locstream(): 331 | # xarray.DataArray containing chunked dask array 332 | 333 | regridder = xe.Regridder(ds_locs, ds_in, 'nearest_s2d', locstream_in=True) 334 | 335 | outdata = regridder(ds_locs['lat']) 336 | 337 | # clean-up 338 | regridder.clean_weight_file() 339 | 340 | 341 | def test_regrid_dataset(): 342 | # xarray.Dataset containing in-memory numpy array 343 | 344 | regridder = xe.Regridder(ds_in, ds_out, 'conservative') 345 | 346 | # `ds_out` already refers to output grid object 347 | # TODO: use more consistent variable namings across tests 348 | ds_result = regridder(ds_in) 349 | 350 | # output should contain all data variables 351 | assert set(ds_result.data_vars.keys()) == set(ds_in.data_vars.keys()) 352 | 353 | # compare with analytical solution 354 | rel_err = (ds_out['data_ref'] - ds_result['data'])/ds_out['data_ref'] 355 | assert np.max(np.abs(rel_err)) < 0.05 356 | 357 | # data over broadcasting dimensions should agree 358 | assert_almost_equal(ds_in['data4D'].values.mean(axis=(2, 3)), 359 | ds_result['data4D'].values.mean(axis=(2, 3)), 360 | decimal=10) 361 | 362 | # check metadata 363 | xr.testing.assert_identical(ds_result['time'], ds_in['time']) 364 | xr.testing.assert_identical(ds_result['lev'], ds_in['lev']) 365 | assert_equal(ds_result['lat'].values, ds_out['lat'].values) 366 | assert_equal(ds_result['lon'].values, ds_out['lon'].values) 367 | 368 | # clean-up 369 | regridder.clean_weight_file() 370 | 371 | 372 | def test_regrid_dataset_to_locstream(): 373 | # xarray.Dataset containing in-memory numpy array 374 | 375 | regridder = xe.Regridder(ds_in, ds_locs, 'bilinear', locstream_out=True) 376 | ds_result = regridder(ds_in) 377 | # clean-up 378 | regridder.clean_weight_file() 379 | 380 | 381 | def test_regrid_dataset_from_locstream(): 382 | # xarray.Dataset containing in-memory numpy array 383 | 384 | regridder = xe.Regridder(ds_locs, ds_in, 'nearest_s2d', locstream_in=True) 385 | outdata = regridder(ds_locs) 386 | # clean-up 387 | regridder.clean_weight_file() 388 | 389 | 390 | def test_ds_to_ESMFlocstream(): 391 | import ESMF 392 | from xesmf.frontend import ds_to_ESMFlocstream 393 | 394 | locstream, shape = ds_to_ESMFlocstream(ds_locs) 395 | assert isinstance(locstream, ESMF.LocStream) 396 | assert shape == (1,4,) 397 | with pytest.raises(ValueError): 398 | locstream, shape = ds_to_ESMFlocstream(ds_in) 399 | ds_bogus = ds_in.copy() 400 | ds_bogus['lon'] = ds_locs['lon'] 401 | with pytest.raises(ValueError): 402 | locstream, shape = ds_to_ESMFlocstream(ds_bogus) 403 | -------------------------------------------------------------------------------- /xesmf/tests/test_util.py: -------------------------------------------------------------------------------- 1 | import xesmf as xe 2 | import pytest 3 | 4 | 5 | def test_grid_global(): 6 | ds = xe.util.grid_global(1.5, 1.5) 7 | refshape = (120, 240) 8 | refshape_b = (121, 241) 9 | 10 | assert ds['lon'].values.shape == refshape 11 | assert ds['lat'].values.shape == refshape 12 | assert ds['lon_b'].values.shape == refshape_b 13 | assert ds['lat_b'].values.shape == refshape_b 14 | 15 | 16 | def test_grid_global_bad_resolution(): 17 | with pytest.warns(UserWarning): 18 | xe.util.grid_global(1.5, 1.23) 19 | 20 | with pytest.warns(UserWarning): 21 | xe.util.grid_global(1.23, 1.5) 22 | -------------------------------------------------------------------------------- /xesmf/util.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import xarray as xr 3 | import warnings 4 | 5 | 6 | def _grid_1d(start_b, end_b, step): 7 | ''' 8 | 1D grid centers and bounds 9 | 10 | Parameters 11 | ---------- 12 | start_b, end_b : float 13 | start/end position. Bounds, not centers. 14 | 15 | step: float 16 | step size, i.e. grid resolution 17 | 18 | Returns 19 | ------- 20 | centers : 1D numpy array 21 | 22 | bounds : 1D numpy array, with one more element than centers 23 | 24 | ''' 25 | bounds = np.arange(start_b, end_b+step, step) 26 | centers = (bounds[:-1] + bounds[1:])/2 27 | 28 | return centers, bounds 29 | 30 | 31 | def grid_2d(lon0_b, lon1_b, d_lon, 32 | lat0_b, lat1_b, d_lat): 33 | ''' 34 | 2D rectilinear grid centers and bounds 35 | 36 | Parameters 37 | ---------- 38 | lon0_b, lon1_b : float 39 | Longitude bounds 40 | 41 | d_lon : float 42 | Longitude step size, i.e. grid resolution 43 | 44 | lat0_b, lat1_b : float 45 | Latitude bounds 46 | 47 | d_lat : float 48 | Latitude step size, i.e. grid resolution 49 | 50 | Returns 51 | ------- 52 | ds : xarray DataSet with coordinate values 53 | 54 | ''' 55 | 56 | lon_1d, lon_b_1d = _grid_1d(lon0_b, lon1_b, d_lon) 57 | lat_1d, lat_b_1d = _grid_1d(lat0_b, lat1_b, d_lat) 58 | 59 | lon, lat = np.meshgrid(lon_1d, lat_1d) 60 | lon_b, lat_b = np.meshgrid(lon_b_1d, lat_b_1d) 61 | 62 | ds = xr.Dataset(coords={'lon': (['y', 'x'], lon), 63 | 'lat': (['y', 'x'], lat), 64 | 'lon_b': (['y_b', 'x_b'], lon_b), 65 | 'lat_b': (['y_b', 'x_b'], lat_b) 66 | } 67 | ) 68 | 69 | return ds 70 | 71 | 72 | def grid_global(d_lon, d_lat): 73 | ''' 74 | Global 2D rectilinear grid centers and bounds 75 | 76 | Parameters 77 | ---------- 78 | d_lon : float 79 | Longitude step size, i.e. grid resolution 80 | 81 | d_lat : float 82 | Latitude step size, i.e. grid resolution 83 | 84 | Returns 85 | ------- 86 | ds : xarray DataSet with coordinate values 87 | 88 | ''' 89 | 90 | if not np.isclose(360/d_lon, 360//d_lon): 91 | warnings.warn('360 cannot be divided by d_lon = {}, ' 92 | 'might not cover the globe uniformally'.format(d_lon)) 93 | 94 | if not np.isclose(180/d_lat, 180//d_lat): 95 | warnings.warn('180 cannot be divided by d_lat = {}, ' 96 | 'might not cover the globe uniformally'.format(d_lat)) 97 | 98 | return grid_2d(-180, 180, d_lon, -90, 90, d_lat) 99 | --------------------------------------------------------------------------------