diff --git a/.readthedocs.yml b/.readthedocs.yml index 169181c..781c913 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -6,15 +6,8 @@ build: tools: python: 'mambaforge-4.10' -# Build documentation in the docs/ directory with Sphinx -sphinx: - configuration: docs/conf.py - # Optionally set the version of Python and requirements required to build your docs conda: environment: ci/doc.yml -python: - install: - - method: pip - path: . +formats: [] diff --git a/README.md b/README.md index 1d34be1..e51cfe5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # cupy-xarray +> [!IMPORTANT] > ⚠️ This project is looking for maintainers and contributors. Come help out! -[![GitHub Workflow CI Status](https://img.shields.io/github/workflow/status/xarray-contrib/cupy-xarray/CI?logo=github&style=flat)](https://github.com/xarray-contrib/cupy-xarray/actions) +![GitHub Workflow CI Status](https://img.shields.io/github/actions/workflow/status/xarray-contrib/cupy-xarray/pypi-release.yaml?style=flat) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/xarray-contrib/cupy-xarray/main.svg)](https://results.pre-commit.ci/latest/github/xarray-contrib/cupy-xarray/main) -[![Documentation Status](https://readthedocs.org/projects/cupy-xarray/badge/?version=latest)](https://cupy-xarray.readthedocs.io/en/latest/?badge=latest) +[![Documentation Status](https://readthedocs.org/projects/cupy-xarray/badge/?version=latest)](https://cupy-xarray.readthedocs.io) [![PyPI](https://img.shields.io/pypi/v/cupy-xarray.svg?style=flat)](https://pypi.org/project/cupy-xarray/) [![Conda-forge](https://img.shields.io/conda/vn/conda-forge/cupy-xarray.svg?style=flat)](https://anaconda.org/conda-forge/cupy-xarray) diff --git a/ci/doc.yml b/ci/doc.yml index 8e0d7d5..45d6b3c 100644 --- a/ci/doc.yml +++ b/ci/doc.yml @@ -5,9 +5,10 @@ dependencies: - pip - python=3.10 - sphinx + - sphinx-design - sphinx-copybutton - - numpydoc - sphinx-autosummary-accessors + - numpydoc - ipython - ipykernel - ipywidgets diff --git a/docs/conf.py b/docs/conf.py index 12d16d6..dfbc36e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,7 +10,7 @@ import sphinx_autosummary_accessors project = "cupy-xarray" -copyright = "2022, cupy-xarray developers" +copyright = "2023, cupy-xarray developers" author = "cupy-xarray developers" release = "v0.1" @@ -20,15 +20,18 @@ extensions = [ # "sphinx.ext.autodoc", "sphinx.ext.viewcode", - # "sphinx.ext.autosummary", + "sphinx.ext.autosummary", "sphinx.ext.doctest", "sphinx.ext.intersphinx", "sphinx.ext.extlinks", "numpydoc", # "sphinx_autosummary_accessors", "IPython.sphinxext.ipython_directive", + "sphinx.ext.napoleon", "myst_nb", + # "nbsphinx", "sphinx_copybutton", + "sphinx_design", ] @@ -56,3 +59,68 @@ "cupy": ("https://docs.cupy.dev/en/latest", None), "xarray": ("http://docs.xarray.dev/en/latest/", None), } + +autosummary_generate = True +autodoc_typehints = "none" + +# Napoleon configurations +napoleon_google_docstring = False +napoleon_numpy_docstring = True +napoleon_use_param = False +napoleon_use_rtype = False +napoleon_preprocess_types = True +napoleon_type_aliases = { + # general terms + "sequence": ":term:`sequence`", + "iterable": ":term:`iterable`", + "callable": ":py:func:`callable`", + "dict_like": ":term:`dict-like `", + "dict-like": ":term:`dict-like `", + "path-like": ":term:`path-like `", + "mapping": ":term:`mapping`", + "file-like": ":term:`file-like `", + # special terms + # "same type as caller": "*same type as caller*", # does not work, yet + # "same type as values": "*same type as values*", # does not work, yet + # stdlib type aliases + "MutableMapping": "~collections.abc.MutableMapping", + "sys.stdout": ":obj:`sys.stdout`", + "timedelta": "~datetime.timedelta", + "string": ":class:`string `", + # numpy terms + "array_like": ":term:`array_like`", + "array-like": ":term:`array-like `", + "scalar": ":term:`scalar`", + "array": ":term:`array`", + "hashable": ":term:`hashable `", + # matplotlib terms + "color-like": ":py:func:`color-like `", + "matplotlib colormap name": ":doc:`matplotlib colormap name `", + "matplotlib axes object": ":py:class:`matplotlib axes object `", + "colormap": ":py:class:`colormap `", + # objects without namespace: xarray + "DataArray": "~xarray.DataArray", + "Dataset": "~xarray.Dataset", + "Variable": "~xarray.Variable", + "DatasetGroupBy": "~xarray.core.groupby.DatasetGroupBy", + "DataArrayGroupBy": "~xarray.core.groupby.DataArrayGroupBy", + # objects without namespace: numpy + "ndarray": "~numpy.ndarray", + "DaskArray": "~dask.array.Array", + "MaskedArray": "~numpy.ma.MaskedArray", + "dtype": "~numpy.dtype", + "ComplexWarning": "~numpy.ComplexWarning", + # objects without namespace: pandas + "Index": "~pandas.Index", + "MultiIndex": "~pandas.MultiIndex", + "CategoricalIndex": "~pandas.CategoricalIndex", + "TimedeltaIndex": "~pandas.TimedeltaIndex", + "DatetimeIndex": "~pandas.DatetimeIndex", + "Series": "~pandas.Series", + "DataFrame": "~pandas.DataFrame", + "Categorical": "~pandas.Categorical", + "Path": "~~pathlib.Path", + # objects with abbreviated namespace (from pandas) + "pd.Index": "~pandas.Index", + "pd.NaT": "~pandas.NaT", +} diff --git a/docs/index.md b/docs/index.md index 597dd16..3bbd9a0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,10 +1,86 @@ -# Welcome to cupy-xarray's documentation! +# CuPy-Xarray: Xarray on GPUs! + +![GitHub Workflow CI Status](https://img.shields.io/github/actions/workflow/status/xarray-contrib/cupy-xarray/pypi-release.yaml?style=flat-square) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/xarray-contrib/cupy-xarray/main.svg?style=flat-square)](https://results.pre-commit.ci/latest/github/xarray-contrib/cupy-xarray/main) +[![Documentation Status](https://readthedocs.org/projects/cupy-xarray/badge/?version=latest&style=flat-square)](https://cupy-xarray.readthedocs.io) +[![license](https://img.shields.io/github/license/xarray-contrib/cupy-xarray.svg?style=flat-square)](https://github.com/xarray-contrib/cupy-xarray) + +[![PyPI](https://img.shields.io/pypi/v/cupy-xarray.svg?style=flat-square)](https://pypi.org/project/cupy-xarray/) +[![Conda-forge](https://img.shields.io/conda/vn/conda-forge/cupy-xarray.svg?style=flat-square)](https://anaconda.org/conda-forge/cupy-xarray) + +[![NASA-80NSSC22K0345](https://img.shields.io/badge/NASA-80NSSC22K0345-blue?style=flat-square)](https://science.nasa.gov/open-science-overview) + +## Overview + +CuPy-Xarray is a Python library that leverages [CuPy](https://cupy.dev/), a GPU array library, and [Xarray](https://docs.xarray.dev/en/stable/), a library for multi-dimensional labeled array computations, to enable fast and efficient data processing on GPUs. By combining the capabilities of CuPy and Xarray, CuPy-Xarray provides a convenient interface for performing accelerated computations and analysis on large multidimensional datasets. + +## Installation + +CuPy-Xarray can be installed using `pip` or `conda`: + +From Conda Forge: + +```bash + +conda install cupy-xarray -c conda-forge +``` + +From PyPI: + +```bash +pip install cupy-xarray +``` + +The latest version from Github: + +```bash +pip install git+https://github.com/xarray-contrib/cupy-xarray.git +``` + +## Acknowledgements + +Large parts of this documentations comes from [SciPy 2023 Xarray on GPUs tutorial](https://negin513.github.io/cupy-xarray-tutorials/README.html) and [this NCAR tutorial to GPUs](https://github.com/NCAR/GPU_workshop/tree/workshop/13_CuPyAndLegate). ## Contents ```{eval-rst} + +**User Guide**: + +.. toctree:: + :maxdepth: 1 + :caption: User Guide + + source/cupy-basics + source/introduction + source/basic-computations + source/high-level-api + source/apply-ufunc + source/real-example-1 + + +**Tutorials & Presentations**: + +.. toctree:: + :maxdepth: 1 + :caption: Tutorials & Presentations + + source/tutorials-and-presentations + +**Contributing**: + +.. toctree:: + :maxdepth: 1 + :caption: Contributing + + source/contributing + + +**API Reference**: + .. toctree:: :maxdepth: 1 + :caption: API Reference - quickstart + api ``` diff --git a/docs/quickstart.ipynb b/docs/quickstart.ipynb deleted file mode 100644 index 02e9d18..0000000 --- a/docs/quickstart.ipynb +++ /dev/null @@ -1,3354 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "e6597bb3-15b6-4639-82ed-841386591567", - "metadata": {}, - "source": [ - "# Quickstart\n", - "\n", - "**Acknowledgments:** This notebook adapts the content in [this NCAR tutorial](https://github.com/NCAR/GPU_workshop/blob/workshop/12_CuPyAndLegate/12_CuPyAndLegate.ipynb) to Xarray, and uses it to illustrate `cupy-xarray` and working with cupy arrays and Xarray objects in general." - ] - }, - { - "cell_type": "markdown", - "id": "71235ea5-c0fb-4afb-a30e-72275ad0b7f0", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "860567c3-18b2-444c-93a9-96ce620ea08f", - "metadata": {}, - "outputs": [], - "source": [ - "import cupy as cp\n", - "import cupy_xarray # Adds .cupy to Xarray objects\n", - "import numpy as np\n", - "import xarray as xr" - ] - }, - { - "cell_type": "markdown", - "id": "be9ca22a-8e41-46b1-a43d-6cf93e4fd677", - "metadata": {}, - "source": [ - "## Creating Arrays\n", - "\n", - "First we create arrays on the CPU and GPU" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "8aba4551-3b83-4f2b-9fc6-541b0ae071e6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "On the CPU: [0. 0.5 1. 1.5 2. ]\n", - "On the GPU: [2. 2.5 3. 3.5 4. ]\n" - ] - } - ], - "source": [ - "# NumPy data (host / cpu)\n", - "x_cpu = np.linspace(0, 2, 5)\n", - "print(\"On the CPU: \", x_cpu)\n", - "\n", - "# CuPy data\n", - "x_gpu = cp.linspace(2, 4, 5)\n", - "print(\"On the GPU: \", x_gpu)" - ] - }, - { - "cell_type": "markdown", - "id": "3976d9b8-d028-4d87-b5c7-70943d807e85", - "metadata": {}, - "source": [ - "And now wrap those in a Xarray DataArray" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "0bacb2e0-3367-4efe-98af-2176937f84ab", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray (x: 5)>\n",
-       "array([2. , 2.5, 3. , 3.5, 4. ])\n",
-       "Dimensions without coordinates: x
" - ], - "text/plain": [ - "\n", - "array([2. , 2.5, 3. , 3.5, 4. ])\n", - "Dimensions without coordinates: x" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "da_gpu = xr.DataArray(x_gpu, dims=\"x\")\n", - "da_gpu" - ] - }, - { - "cell_type": "markdown", - "id": "0e536b2b-e540-4a68-b7c8-627fd015832c", - "metadata": {}, - "source": [ - "That was easy! Xarray seamlessly wraps numpy array-like objects that [support specific protocols](https://docs.xarray.dev/en/stable/internals/duck-arrays-integration.html)." - ] - }, - { - "cell_type": "markdown", - "id": "6fb23b66-4cd9-4e1f-82ca-77b6235dc1d0", - "metadata": {}, - "source": [ - "For array-specific functionality Xarray recommends adding new packages that provide [\"accessors\"](https://docs.xarray.dev/en/stable/internals/extending-xarray.html) on Xarray objects. \n", - "\n", - "For example, the [pint-xarray](https://pint-xarray.readthedocs.io/en/latest/) package that wraps unit-aware pint arrays and provides a `.pint` for unit-specific functionality.\n", - "\n", - "In this tutorial, we demonstrate `cupy-xarray` which provides a `cupy` accessor that in turn provides access to cupy-specific functionality." - ] - }, - { - "cell_type": "markdown", - "id": "8fed5c08-a729-4d55-a167-4fc557c67d74", - "metadata": {}, - "source": [ - "## Checking for cupy arrays\n", - "\n", - "Unfortunately the text representation of CuPy arrays isn't [very informative](https://github.com/cupy/cupy/issues/6926) so it isn't obvious that this DataArray wraps a CuPy array on the GPU." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "c582c151-9399-4dd0-a067-2125589bb605", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray (x: 5)>\n",
-       "array([2. , 2.5, 3. , 3.5, 4. ])\n",
-       "Dimensions without coordinates: x
" - ], - "text/plain": [ - "\n", - "array([2. , 2.5, 3. , 3.5, 4. ])\n", - "Dimensions without coordinates: x" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "da_gpu" - ] - }, - { - "cell_type": "markdown", - "id": "f089cf99-9d60-49da-a74b-bee9be52fd27", - "metadata": {}, - "source": [ - "Instead we'll use the `is_cupy` property provided by the `cupy` accessor" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "951776ff-dad4-45ba-8adf-a2a83c68de85", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "da_gpu.cupy.is_cupy" - ] - }, - { - "cell_type": "markdown", - "id": "78044088-9853-4fbc-ae23-05b8853254d8", - "metadata": {}, - "source": [ - "## Accessing the underlying array\n", - "\n", - "Use the `DataArray.data` property to access the underlying CuPy Array" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "8b419a4c-59de-45bd-84f7-b4c4e9268dd2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([2. , 2.5, 3. , 3.5, 4. ])" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "da_gpu.data" - ] - }, - { - "cell_type": "markdown", - "id": "0beda1aa-2989-49a1-948d-ee42f96cffb3", - "metadata": {}, - "source": [ - "This means we now have access to CuPy-specific properties" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "63522d11-fe3b-44dd-b51c-6ba4ba0c98cb", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "da_gpu.data.device" - ] - }, - { - "cell_type": "markdown", - "id": "10fa58ed-0911-4dea-80fb-d4061b95e2d3", - "metadata": {}, - "source": [ - "## Moving data between CPU and GPU (or host and device)\n", - "\n", - "Xarray provides [DataArray.as_numpy](https://docs.xarray.dev/en/stable/generated/xarray.Dataset.as_numpy.html#xarray.Dataset.as_numpy) to convert all kinds of arrays to numpy arrays" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "11b7f877-2087-4d6e-a134-e0fcd084abd7", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray (x: 5)>\n",
-       "array([2. , 2.5, 3. , 3.5, 4. ])\n",
-       "Dimensions without coordinates: x
" - ], - "text/plain": [ - "\n", - "array([2. , 2.5, 3. , 3.5, 4. ])\n", - "Dimensions without coordinates: x" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Move data to host\n", - "da_cpu = da_gpu.as_numpy()\n", - "da_cpu" - ] - }, - { - "cell_type": "markdown", - "id": "86ac2adf-da1b-4bf6-82fb-3472292f2c12", - "metadata": {}, - "source": [ - "Let's make sure this isn't a cupy array anymore" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "1669e378-1757-4051-9073-d78c5b0b9ff1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "False" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "da_cpu.cupy.is_cupy" - ] - }, - { - "cell_type": "markdown", - "id": "1c504ad1-7527-4757-8042-bc1641d55506", - "metadata": {}, - "source": [ - "To convert a numpy array to a CuPy array (move data to GPU) use `cupy.as_cupy()`" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "8b23a051-d936-49e8-b351-aae75e7a88bf", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray (x: 5)>\n",
-       "array([2. , 2.5, 3. , 3.5, 4. ])\n",
-       "Dimensions without coordinates: x
" - ], - "text/plain": [ - "\n", - "array([2. , 2.5, 3. , 3.5, 4. ])\n", - "Dimensions without coordinates: x" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Move data to GPU\n", - "da_cpu.cupy.as_cupy()" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "3f1e19f6-7084-4e03-90e1-48e418c6d6a0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "da_cpu.as_cupy().cupy.is_cupy" - ] - }, - { - "cell_type": "markdown", - "id": "b46e6a52-5de1-4aac-94d0-627ef22f47de", - "metadata": {}, - "source": [ - "## Most Xarray operations preserve array type\n" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "91b51f09-524f-4797-b7f2-0dafe6185622", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "expanded = da_gpu.expand_dims(y=3)\n", - "expanded.cupy.is_cupy" - ] - }, - { - "cell_type": "markdown", - "id": "f0a735f9-cca7-4cbd-b635-ad57811468a1", - "metadata": {}, - "source": [ - "### Alignment\n", - "\n", - "Alignment is a fundamental Xarray operation. It preserves array types" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "0ce4dd47-267a-469a-b539-8aa557254a29", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[True, True]" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "aligned = xr.align(da_gpu, expanded)\n", - "[a.cupy.is_cupy for a in aligned]" - ] - }, - { - "cell_type": "markdown", - "id": "fad69278-82da-40b1-aba2-2b2baa31d859", - "metadata": {}, - "source": [ - "### Broadcasting" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "9cc6e7a8-9c1d-4429-a962-8827c78cc363", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[True, True]" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "da_gpu2 = da_gpu.rename({\"x\": \"y\"})\n", - "broadcasted = xr.broadcast(da_gpu, da_gpu2)\n", - "[a.cupy.is_cupy for a in broadcasted]" - ] - }, - { - "cell_type": "markdown", - "id": "b6829643-c7ea-462d-ac7a-38daa4ab0f24", - "metadata": {}, - "source": [ - "### Basic Arithmetic" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "a4ad6ac6-c0d1-4e82-a2c9-22d0f1adb2b8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "is_gpu: True\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray (x: 5)>\n",
-       "array([3. , 3.5, 4. , 4.5, 5. ])\n",
-       "Dimensions without coordinates: x
" - ], - "text/plain": [ - "\n", - "array([3. , 3.5, 4. , 4.5, 5. ])\n", - "Dimensions without coordinates: x" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# works on both CPU and GPU\n", - "print(\"is_gpu: \", (da_gpu + 1).cupy.is_cupy)\n", - "da_gpu + 1" - ] - }, - { - "cell_type": "markdown", - "id": "0cc3b197-5732-489c-829f-43a168ad15a5", - "metadata": {}, - "source": [ - "### Numpy universal functions" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "9c571c2f-5d61-443e-897c-9b7a098bb18f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "np.min(da_gpu.mean()).cupy.is_cupy" - ] - }, - { - "cell_type": "markdown", - "id": "1c075cc9-e2c3-464f-a504-32d9d3a879d5", - "metadata": {}, - "source": [ - "We can use `np.round` which dispatches" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "bd0713dc-d86d-4afb-bd10-563a9eb60883", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "np.round(da_gpu.mean(), 2).cupy.is_cupy" - ] - }, - { - "cell_type": "markdown", - "id": "19899ada-0871-4cfb-baa4-feb46c9a6981", - "metadata": {}, - "source": [ - "## High-level Xarray functions" - ] - }, - { - "cell_type": "markdown", - "id": "42394855-a20f-4ec4-b816-3e1cb1a5be54", - "metadata": { - "tags": [] - }, - "source": [ - "### Groupby works\n", - "\n", - "Though this is a slow for loop over groups. We could add an explicit parallel algorithm to [flox](https://github.com/xarray-contrib/flox)" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "fe994a8a-1c45-4c14-add5-6ed38fe95585", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray (x: 5)>\n",
-       "array([2. , 2.5, 3. , 3.5, 4. ])\n",
-       "Dimensions without coordinates: x
" - ], - "text/plain": [ - "\n", - "array([2. , 2.5, 3. , 3.5, 4. ])\n", - "Dimensions without coordinates: x" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "da_gpu.groupby(\"x\").mean(...)" - ] - }, - { - "cell_type": "markdown", - "id": "141771cc-0594-411f-9fe2-8a18f289ef32", - "metadata": {}, - "source": [ - "Since groupby works; groupby_bins and resample *should* also work" - ] - }, - { - "cell_type": "markdown", - "id": "7c7286e4-b651-4778-bbb8-c3f8cc24c78a", - "metadata": { - "tags": [] - }, - "source": [ - "### Rolling windows do not work\n", - "\n", - "cupy needs to add support for [sliding_window_view](https://numpy.org/devdocs/reference/generated/numpy.lib.stride_tricks.sliding_window_view.html)" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "fd5da1e2-26ad-40b3-9fac-2559d33c345a", - "metadata": { - "tags": [ - "raises-exception" - ] - }, - "outputs": [ - { - "ename": "TypeError", - "evalue": "no implementation found for 'numpy.lib.stride_tricks.sliding_window_view' on types that implement __array_function__: []", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", - "Input \u001b[0;32mIn [19]\u001b[0m, in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mda_gpu\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrolling\u001b[49m\u001b[43m(\u001b[49m\u001b[43mx\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m3\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmean\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39mcupy\u001b[38;5;241m.\u001b[39mis_cupy\n", - "File \u001b[0;32m~/miniconda3/envs/gpu/lib/python3.10/site-packages/xarray/core/rolling.py:155\u001b[0m, in \u001b[0;36mRolling._reduce_method..method\u001b[0;34m(self, keep_attrs, **kwargs)\u001b[0m\n\u001b[1;32m 151\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mmethod\u001b[39m(\u001b[38;5;28mself\u001b[39m, keep_attrs\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m 153\u001b[0m keep_attrs \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_get_keep_attrs(keep_attrs)\n\u001b[0;32m--> 155\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_numpy_or_bottleneck_reduce\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 156\u001b[0m \u001b[43m \u001b[49m\u001b[43marray_agg_func\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 157\u001b[0m \u001b[43m \u001b[49m\u001b[43mbottleneck_move_func\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 158\u001b[0m \u001b[43m \u001b[49m\u001b[43mrolling_agg_func\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 159\u001b[0m \u001b[43m \u001b[49m\u001b[43mkeep_attrs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mkeep_attrs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 160\u001b[0m \u001b[43m \u001b[49m\u001b[43mfillna\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfillna\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 161\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 162\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/miniconda3/envs/gpu/lib/python3.10/site-packages/xarray/core/rolling.py:580\u001b[0m, in \u001b[0;36mDataArrayRolling._numpy_or_bottleneck_reduce\u001b[0;34m(self, array_agg_func, bottleneck_move_func, rolling_agg_func, keep_attrs, fillna, **kwargs)\u001b[0m\n\u001b[1;32m 576\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_bottleneck_reduce(\n\u001b[1;32m 577\u001b[0m bottleneck_move_func, keep_attrs\u001b[38;5;241m=\u001b[39mkeep_attrs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs\n\u001b[1;32m 578\u001b[0m )\n\u001b[1;32m 579\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m rolling_agg_func:\n\u001b[0;32m--> 580\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mrolling_agg_func\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mkeep_attrs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_get_keep_attrs\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkeep_attrs\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 581\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m fillna \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 582\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m fillna \u001b[38;5;129;01mis\u001b[39;00m dtypes\u001b[38;5;241m.\u001b[39mINF:\n", - "File \u001b[0;32m~/miniconda3/envs/gpu/lib/python3.10/site-packages/xarray/core/rolling.py:169\u001b[0m, in \u001b[0;36mRolling._mean\u001b[0;34m(self, keep_attrs, **kwargs)\u001b[0m\n\u001b[1;32m 168\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_mean\u001b[39m(\u001b[38;5;28mself\u001b[39m, keep_attrs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[0;32m--> 169\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msum\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkeep_attrs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;241m/\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcount(keep_attrs\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m)\n\u001b[1;32m 170\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m keep_attrs:\n\u001b[1;32m 171\u001b[0m result\u001b[38;5;241m.\u001b[39mattrs \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mobj\u001b[38;5;241m.\u001b[39mattrs\n", - "File \u001b[0;32m~/miniconda3/envs/gpu/lib/python3.10/site-packages/xarray/core/rolling.py:155\u001b[0m, in \u001b[0;36mRolling._reduce_method..method\u001b[0;34m(self, keep_attrs, **kwargs)\u001b[0m\n\u001b[1;32m 151\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mmethod\u001b[39m(\u001b[38;5;28mself\u001b[39m, keep_attrs\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m 153\u001b[0m keep_attrs \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_get_keep_attrs(keep_attrs)\n\u001b[0;32m--> 155\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_numpy_or_bottleneck_reduce\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 156\u001b[0m \u001b[43m \u001b[49m\u001b[43marray_agg_func\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 157\u001b[0m \u001b[43m \u001b[49m\u001b[43mbottleneck_move_func\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 158\u001b[0m \u001b[43m \u001b[49m\u001b[43mrolling_agg_func\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 159\u001b[0m \u001b[43m \u001b[49m\u001b[43mkeep_attrs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mkeep_attrs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 160\u001b[0m \u001b[43m \u001b[49m\u001b[43mfillna\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfillna\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 161\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 162\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/miniconda3/envs/gpu/lib/python3.10/site-packages/xarray/core/rolling.py:589\u001b[0m, in \u001b[0;36mDataArrayRolling._numpy_or_bottleneck_reduce\u001b[0;34m(self, array_agg_func, bottleneck_move_func, rolling_agg_func, keep_attrs, fillna, **kwargs)\u001b[0m\n\u001b[1;32m 586\u001b[0m kwargs\u001b[38;5;241m.\u001b[39msetdefault(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mskipna\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;28;01mFalse\u001b[39;00m)\n\u001b[1;32m 587\u001b[0m kwargs\u001b[38;5;241m.\u001b[39msetdefault(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfillna\u001b[39m\u001b[38;5;124m\"\u001b[39m, fillna)\n\u001b[0;32m--> 589\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mreduce\u001b[49m\u001b[43m(\u001b[49m\u001b[43marray_agg_func\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mkeep_attrs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mkeep_attrs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m~/miniconda3/envs/gpu/lib/python3.10/site-packages/xarray/core/rolling.py:472\u001b[0m, in \u001b[0;36mDataArrayRolling.reduce\u001b[0;34m(self, func, keep_attrs, **kwargs)\u001b[0m\n\u001b[1;32m 470\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 471\u001b[0m obj \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mobj\n\u001b[0;32m--> 472\u001b[0m windows \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_construct\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 473\u001b[0m \u001b[43m \u001b[49m\u001b[43mobj\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrolling_dim\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mkeep_attrs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mkeep_attrs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfill_value\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfillna\u001b[49m\n\u001b[1;32m 474\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 476\u001b[0m result \u001b[38;5;241m=\u001b[39m windows\u001b[38;5;241m.\u001b[39mreduce(\n\u001b[1;32m 477\u001b[0m func, dim\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mlist\u001b[39m(rolling_dim\u001b[38;5;241m.\u001b[39mvalues()), keep_attrs\u001b[38;5;241m=\u001b[39mkeep_attrs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs\n\u001b[1;32m 478\u001b[0m )\n\u001b[1;32m 480\u001b[0m \u001b[38;5;66;03m# Find valid windows based on count.\u001b[39;00m\n", - "File \u001b[0;32m~/miniconda3/envs/gpu/lib/python3.10/site-packages/xarray/core/rolling.py:389\u001b[0m, in \u001b[0;36mDataArrayRolling._construct\u001b[0;34m(self, obj, window_dim, stride, fill_value, keep_attrs, **window_dim_kwargs)\u001b[0m\n\u001b[1;32m 384\u001b[0m window_dims \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_mapping_to_list(\n\u001b[1;32m 385\u001b[0m window_dim, allow_default\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m, allow_allsame\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m \u001b[38;5;66;03m# type: ignore[arg-type] # https://github.com/python/mypy/issues/12506\u001b[39;00m\n\u001b[1;32m 386\u001b[0m )\n\u001b[1;32m 387\u001b[0m strides \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_mapping_to_list(stride, default\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1\u001b[39m)\n\u001b[0;32m--> 389\u001b[0m window \u001b[38;5;241m=\u001b[39m \u001b[43mobj\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvariable\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrolling_window\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 390\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdim\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mwindow\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mwindow_dims\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcenter\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfill_value\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfill_value\u001b[49m\n\u001b[1;32m 391\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 393\u001b[0m attrs \u001b[38;5;241m=\u001b[39m obj\u001b[38;5;241m.\u001b[39mattrs \u001b[38;5;28;01mif\u001b[39;00m keep_attrs \u001b[38;5;28;01melse\u001b[39;00m {}\n\u001b[1;32m 395\u001b[0m result \u001b[38;5;241m=\u001b[39m DataArray(\n\u001b[1;32m 396\u001b[0m window,\n\u001b[1;32m 397\u001b[0m dims\u001b[38;5;241m=\u001b[39mobj\u001b[38;5;241m.\u001b[39mdims \u001b[38;5;241m+\u001b[39m \u001b[38;5;28mtuple\u001b[39m(window_dims),\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 400\u001b[0m name\u001b[38;5;241m=\u001b[39mobj\u001b[38;5;241m.\u001b[39mname,\n\u001b[1;32m 401\u001b[0m )\n", - "File \u001b[0;32m~/miniconda3/envs/gpu/lib/python3.10/site-packages/xarray/core/variable.py:2319\u001b[0m, in \u001b[0;36mVariable.rolling_window\u001b[0;34m(self, dim, window, window_dim, center, fill_value)\u001b[0m\n\u001b[1;32m 2315\u001b[0m axis \u001b[38;5;241m=\u001b[39m [\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mget_axis_num(d) \u001b[38;5;28;01mfor\u001b[39;00m d \u001b[38;5;129;01min\u001b[39;00m dim]\n\u001b[1;32m 2316\u001b[0m new_dims \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdims \u001b[38;5;241m+\u001b[39m \u001b[38;5;28mtuple\u001b[39m(window_dim)\n\u001b[1;32m 2317\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m Variable(\n\u001b[1;32m 2318\u001b[0m new_dims,\n\u001b[0;32m-> 2319\u001b[0m \u001b[43mduck_array_ops\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msliding_window_view\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2320\u001b[0m \u001b[43m \u001b[49m\u001b[43mpadded\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mwindow_shape\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mwindow\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43maxis\u001b[49m\n\u001b[1;32m 2321\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m,\n\u001b[1;32m 2322\u001b[0m )\n", - "File \u001b[0;32m~/miniconda3/envs/gpu/lib/python3.10/site-packages/xarray/core/duck_array_ops.py:640\u001b[0m, in \u001b[0;36msliding_window_view\u001b[0;34m(array, window_shape, axis)\u001b[0m\n\u001b[1;32m 638\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m dask_array_compat\u001b[38;5;241m.\u001b[39msliding_window_view(array, window_shape, axis)\n\u001b[1;32m 639\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 640\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mnpcompat\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msliding_window_view\u001b[49m\u001b[43m(\u001b[49m\u001b[43marray\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mwindow_shape\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxis\u001b[49m\u001b[43m)\u001b[49m\n", - "File \u001b[0;32m<__array_function__ internals>:180\u001b[0m, in \u001b[0;36msliding_window_view\u001b[0;34m(*args, **kwargs)\u001b[0m\n", - "\u001b[0;31mTypeError\u001b[0m: no implementation found for 'numpy.lib.stride_tricks.sliding_window_view' on types that implement __array_function__: []" - ] - } - ], - "source": [ - "da_gpu.rolling(x=3).mean().cupy.is_cupy" - ] - }, - { - "cell_type": "markdown", - "id": "5296f2b6-d8c7-49c1-8d39-c7231f7ad70b", - "metadata": { - "tags": [] - }, - "source": [ - "### Weighted operations work" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "7a8ab1a5-48c0-448f-a062-dd27b3956b3f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "weights = xr.DataArray(cp.asarray([0, 0.5, 1, 0.5, 0]), dims=\"y\")\n", - "da_gpu.weighted(weights).sum().cupy.is_cupy" - ] - }, - { - "cell_type": "markdown", - "id": "5aaa21ee-781a-4f76-9a8c-61291f3ecf57", - "metadata": {}, - "source": [ - "## Plotting works\n", - "\n", - "Automatically moves data to the CPU before passing on to matplotlib" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "153b4e92-55a8-4a0a-9ed3-eb825483936c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAvQAAAILCAYAAACHGAjaAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAABYlAAAWJQFJUiTwAABe5UlEQVR4nO3dd3hU55n38e8jISE6pttgm96RHfeSuMe9UbybfdN34zTvOs7GMbgQ47gB6T2b5myS3ThZcO+9O3HiJjqmY0wxYDpC7Xn/mGGQCAKVEUcjfT/Xpeuge8555p7jY/jp0SkhxogkSZKk3JSXdAOSJEmSGs5AL0mSJOUwA70kSZKUwwz0kiRJUg4z0EuSJEk5zEAvSZIk5TADvSRJkpTDDPSSJElSDjPQS5IkSTnMQC9JkiTlMAO9JEmSlMMM9JIkSVIOa5N0A81dCGEp0BlYlnArkiRJatn6A1tijAPqs5GB/sA6t2vXrtuIESO6Jd2IJEmSWq558+axc+fOem9noD+wZSNGjOj2+uuvJ92HJEmSWrBjjz2WN954Y1l9t/McekmSJCmHGeglSZKkHGaglyRJknKYgV6SJEnKYQZ6SZIkKYcZ6CVJkqQcZqCXJEmScpiBXpIkScphBnpJkiQphxnoJUmSpBxmoJckSZJymIFekiRJymFNEuhDCJ8MIcT01+fquW2/EMJvQgjvhRB2hRCWhRC+H0I4ZD/bnBJCeCSEsDGEsCOEUBJCuCaEkN/4TyNJkiQ1X1kP9CGEw4EfAdsasO0g4HXgs8BrwPeAJcBXgFdDCN33sc1lwAvAacC9wE+AwvS2dzfsU0iSJEm5IauBPoQQgLuADcDPGzDET4FewNUxxstjjJNijGeRCufDgNv3er/OwC+BSuCMGOO/xRi/DhwNvApMCCF8rKGfR5IkSWrusj1DfzVwFqkZ9u312TCEMBA4F1hGapa9upvT430yhNChWn0C0BO4O8b4993FGGMpcFP62y/Vpw9JkiS1XltLy3lk1uqk26iXNtkaKIQwApgK/CDG+EII4ax6DrF7/SdijFXVX4gxbg0hvEwq8J8EPL3XNo/tY7wXgB3AKSGEtjHGXQfo//VaXhpel+YlSZKU256dv44b7p3Fmi2lzPzSKRxzRK2XcDYrWZmhDyG0AX4PrABuaOAww9LLhbW8/k56ObQu28QYK4ClpH5oGdjAniRJktTCbdxexjV3v8lnf/s3Vm8uJUaYOKOEsoqqA2/cDGRrhv4bwIeAD8cYdzZwjC7p5eZaXt9d79rIbfYpxnjsvurpmftjDrS9JEmSckuMkYdKVjPlgTls2F6WqXfvUMjVZw+hID8k2F3dNTrQhxBOIDUr/50Y46uNb6n2t0ovYxNvI0mSpBZu7ZZSbrx3Nk/NW1ujfvnRh/GNS0bRrUNhQp3VX6MCfbVTbRYCkxvZy+7Z9C61vN55r/Uauo0kSZJaqRgjf/rbSm5/ZB5bSysy9UO7FHH72NGcNbx3gt01TGNn6Duy55z20tRdK//BL0MIvyR1sew1+xlrQXo5tJbXh6SX1c+XXwAcl96mxkWt6R82BgAVpO5lL0mSpFZsxYYdTLqnhFcWb6hR/8RJRzDx/OF0KipIqLPGaWyg3wX8upbXjiF1Xv1LpIL3gU7HeTa9PDeEkFf9TjchhE7AqcBO4C/VtnkG+DhwPvDHvcY7DWgPvHCgO9xIkiSp5aqsitz18lK+/cQCSsv3XOjav3t7po4v5qSB//Ds0pzSqECfvgD2c/t6LYQwhVSg/+8Y46+q1QuAQUB5jHFxtbEWhxCeIHVryqtIPW12t1uADsB/xRir399+BjAN+FgI4Ue770UfQigCbkuv87PGfEZJkiTlrgVrtnLdzBLeXrkpU8sLcOVpA/nqOUMpKshPrrksydp96OuhLzAPWA703+u1LwOvAD8MIZydXu9E4ExSp9rcWH3lGOOWEMKVpIL9cyGEu4GNwKWkbmk5A/hTk30SSZIkNUtlFVX89LlF/OTZRZRX7rk/yvA+nZg+oZjifl2Tay7Lkgj0tUrP0h8HfJPUaTQXAquBHwK3xBg37mOb+0IIp5MK++OBImAR8J/AD2OM3uFGkiSpFXl75Saum1HCgrVbM7XC/Dz+46zBfOH0QRS2ycqjmJqNJgv0McYpwJR91Jex53aS+9puJfDZer7Xy6TCvyRJklqpnWWVfPfJBfz6paVUVZvS/dARXZk+vpghvTsl11wTalYz9JIkSVJDvLJ4PdffM4vlG3Zkau0K8vn6ecP49Cn9yc/LjYdENYSBXpIkSTlrS2k5dz4ynz++tqJG/cODe3DnuDEc3q19Qp0dPAZ6SZIk5aSn5q7lxvtmsXbLnjuUdypqw+SLRnLFcf2o5RlJLY6BXpIkSTllw7Zd3PLgXB54+70a9XNH9ubWy0fTu3NRQp0lw0AvSZKknBBj5IG332PKA3P4YEd5pt6jYyHfvGw0F4zu02pm5asz0EuSJKnZW715JzfdO5un56+rUR93TF8mXzSSQzoUJtRZ8gz0kiRJaraqqiJ//NsK7nxkPtt2VWTqfbu24/axozljWK8Eu2seDPSSJElqlpau386kmSX8dWnNZ4t+6uQjue784XRsa5QFA70kSZKamYrKKn7z8lK+88RCdlVUZeoDe3Rg6vhiThjQLcHumh8DvSRJkpqNeau3MHFmCSXvbs7U8vMCnz9tIF85ewhFBfkJdtc8GeglSZKUuF0Vlfzk2cX89NlFVFTFTH3koZ2ZPqGY0X27JNhd82aglyRJUqLeWPEBE2eU8M66bZlaYZs8vnL2ED5/2kAK8vMS7K75M9BLkiQpETvKKvjOEwv5zctLiXsm5Tn2yEOYNr6Ywb06JtdcDjHQS5Ik6aB7edF6Jt1TwsqNOzO19oX5TDx/OJ886Ujy8lrfA6IaykAvSZKkg2bzznLueHgef/r7yhr1jwzpwR1jx3B4t/YJdZa7DPSSJEk6KJ6Ys4ab7pvNuq27MrUu7QqYfPFIxh/TlxCclW8IA70kSZKa1PtbdzHlwTk8XLK6Rv3CMX2YcukoenUqSqizlsFAL0mSpCYRY+S+t1Zxy4Nz2bSjPFPv0bEtt10+ivNHH5pgdy2HgV6SJElZt2rTTm68dxbPLXi/Rv2KY/tx00Uj6dK+IKHOWh4DvSRJkrKmqiryP39dztRH57O9rDJT79u1HXeOG8NpQ3sm2F3LZKCXJElSVix5fxuTZs7itWUbM7UQ4NMn9+fr5w2jQ1ujZ1Nwr0qSJKlRKiqr+OWLS/neUwspq6jK1Af17MC08cUc179bgt21fAZ6SZIkNdjc97Zw3cy3mb1qS6bWJi/wxdMH8e9nDaaoID/B7loHA70kSZLqrbS8kh8/s4ifP7+YiqqYqY/u25lp44sZdViXBLtrXQz0kiRJqpfXl2/kuhklLH5/e6ZW2CaPr54zlCs/MoA2+XkJdtf6GOglSZJUJ9t3VfCtxxfw368uI+6ZlOeE/t2YOn4MA3t2TK65VsxAL0mSpAN6YeH7XH/PLFZt2pmpdSjMZ9IFw/n4iUeSlxcS7K51M9BLkiSpVpt3lHPrw3OZ8fq7NepnDOvJ7WPH0Ldru4Q6024GekmSJO3TY7NXM/n+Oby/dVem1rV9ATdfMpLLj+5LCM7KNwcGekmSJNWwbmspN98/h0dnr6lRv6j4UG65dBQ9OrZNqDPti4FekiRJAMQYmfnGKm59aC6bd5Zn6r06teXWy0dz3qg+CXan2hjoJUmSxMqNO7jh3lm8+M76GvV/Pu5wbrhoBF3aFSTUmQ7EQC9JktSKVVVFfvfqMqY/voAdZZWZ+uHd2jF1XDGnDu6RYHeqCwO9JElSK7Vo3TYmzSzh78s/yNRCgH89dQBfO3co7QuNirkgK/+VQgjTgOOAoUAPYCewHLgP+HGMcUMdxvgMcNcBVquKMeZX26Y/sHQ/6/8pxvixA723JElSa1JeWcUvXljCD556h7LKqkx9SK+OTJtQzDFHHJJgd6qvbP3Y9VXgDeBJYB3QATgJmAJ8PoRwUoxx5QHGeAu4pZbXPgKcBTxay+tvk/rhYW+zD/CekiRJrcrsVZu5bkYJc1dvydTa5AW+fOZgrjpzEG3b5O9nazVH2Qr0nWOMpXsXQwi3AzcA1wNf3t8AMca3SIX6fxBCeDX9x1/UsvlbMcYpdexVkiSp1Sktr+QHT7/DL15YQmVVzNSL+3Vh2vhiRhzaOcHu1BhZCfT7CvNpfyYV6Ic0dOwQwmhSs/2rgIcbOo4kSVJr9bdlG5k4o4Ql67dnam3b5PG1c4fyr6cOoE1+XoLdqbGa+kqHS9LLkkaM8YX08tcxxspa1jkshPAFoDuwAXg1xtiY95QkScp523ZVMP2x+fzu1eU16icO6Ma08cX079Ehoc6UTVkN9CGEa4GOQBdSF8l+mFSYn9rA8doBnwCqgF/tZ9WPpr+qb/sc8OkY44o6vtfrtbw0vC7bS5IkNSfPLVjHjffOZtWmnZlax7ZtuP7C4fzL8UeQlxcS7E7ZlO0Z+muB3tW+fwz4TIzx/QaO909AV+DhWi6q3QHcSuqC2CXpWjGpi3HPBJ4OIRwdY9y+j20lSZJanA+2l3Hrw3O5541VNepnDe/F7WNHc2iXdgl1pqaS1UAfY+wDEELoDZxCamb+zRDCxTHGNxow5OfTy/+q5f3WAd/Yq/xCCOFc4CXgROBzwA/q0Pux+6qnZ+6PqWvDkiRJSYgx8ujsNXzj/tms31aWqXfrUMjNl4zk0qMOIwRn5VuiJjmHPsa4Frg3hPAGsBD4HTC6PmOEEEaS+qHgXeCRer5/RQjhV6QC/WnUIdBLkiTlqnVbSpl8/2wen7O2Rv3Sow7j5ktG0r1j24Q608HQpBfFxhiXhxDmAkeHEHrEGNfXY/O6XAy7P7tP8/FqD0mS1CLFGPm/v7/LrQ/PZWtpRabep3MRt10+mnNG9t7P1mopDsbzfA9LL+scykMIRcAnSV0M++sGvu9J6eWS/a4lSZKUg1Zu3MH198zipUU150v/5YQjuP7C4XQuKkioMx1sjQ70IYThwKYY45q96nmkLljtBbwSY/wgXS8ABgHlMcbFtQx7BXAI8ND+njAbQjgReDPGWLZX/SxST68F+EP9P5UkSVLzVFkV+e9XlvGtxxews3zPfOmR3dtz57gxnDKoR4LdKQnZmKE/H/hWCOEFYDGp+8D3Bk4HBgJrgCurrd8XmAcsB/rXMubui2FrezLsbtOAUelbVL6brhUDZ6X/PDnG+EpdP4gkSVJz9s7arUycWcIbKzZlankBPveRgXz1nKG0K8xPrjklJhuB/ilSwftU4ChSt5ncTupi2N8DP4wxbqzrYCGEEaTuX1+Xi2F/D4wFjgcuAAqAtaSeUPvjGOOL9fkgkiRJzVFZRRX/9fxifvTMIsoqqzL1Yb07MW1CMUcf3jW55pS4Rgf6GONs4Kp6rL8MqPWeSTHGeft7fa91f03Dz7GXJElq9kre3cR1M0qYv2ZrplaQH/j3M4fwpTMGUdgmL8Hu1BwcjItiJUmSVE+l5ZV878mF/PLFJVTFPfWjDu/K9PHFDOvTKbnm1KwY6CVJkpqZvyzZwKSZJSzbsCNTKyrI49pzh/HZUweQn+cDorSHgV6SJKmZ2FpaztRH5/M/f11Ro37KoO7cOW4MR3b38Tr6RwZ6SZKkZuDZ+eu44d5ZrN5cmql1atuGGy8awT8ffzghOCuvfTPQS5IkJWjj9jK++eAc7nvrvRr1c0b05rbLR9OnS1FCnSlXGOglSZISEGPkwZLVTHlgDhu373lGZvcOhUy5dBQXFx/qrLzqxEAvSZJ0kK3ZXMpN983mqXlra9THfqgvky8eSbcOhQl1plxkoJckSTpIYozc/beV3PHwPLbuqsjUD+1SxO1jR3PW8N4JdqdcZaCXJEk6CJZv2M6kmbN4dcmGGvVPnHQEE88fTqeigoQ6U64z0EuSJDWhyqrIXS8v5dtPLKC0vCpTH9CjA1PHjeHEgd0T7E4tgYFekiSpiSxYs5XrZpbw9spNmVpegCtPG8hXzxlKUUF+cs2pxTDQS5IkZVlZRRU/fW4RP3l2EeWVMVMf3qcT0ycUU9yva3LNqcUx0EuSJGXRWys3MXFGCQvWbs3UCvPz+I+zBvPFMwZRkJ+XYHdqiQz0kiRJWbCzrJLvPrmAX7+0lKo9k/J86IiuTB9fzJDenZJrTi2agV6SJKmRXlm8nkkzZ7Fi445MrV1BPtedP4xPndyf/DwfEKWmY6CXJElqoC2l5dz5yHz++NqKGvUPD+7BnePGcHi39gl1ptbEQC9JktQAT81dy433zWLtll2ZWueiNtx08UiuOLYfITgrr4PDQC9JklQPG7btYsqDc3nw7fdq1M8b1ZtbLxtNr85FCXWm1spAL0mSVAcxRh54+z2mPDCHD3aUZ+o9OhbyzctGc8HoPs7KKxEGekmSpAN4b9NObrpvNs/MX1ejPv6Yftx00QgO6VCYUGeSgV6SJKlWVVWRP/5tBXc+Mp9tuyoy9b5d23HHuDGcPrRngt1JKQZ6SZKkfVi6fjuTZpbw16Uba9Q/ffKRfP384XRsa4xS8+CRKEmSVE1FZRW/fmkp331yIbsqqjL1gT07MG18Mcf375Zgd9I/MtBLkiSlzVu9hYkzSyh5d3Omlp8X+MJpA7n67CEUFeQn2J20bwZ6SZLU6u2qqOQnzyzip88tpqIqZuojD+3M9AnFjO7bJcHupP0z0EuSpFbtjRUfMHFGCe+s25apFbbJ4ytnD+Hzpw2kID8vwe6kAzPQS5KkVmlHWQXffnwhd72ylLhnUp7jjjyEqeOLGdyrY3LNSfVgoJckSa3Oy4vWM+meElZu3JmptS/MZ+L5w/nkSUeSl+cDopQ7DPSSJKnV2LyznDsensef/r6yRv20oT25Y+xo+h3SPqHOpIYz0EuSpFbh8TlrmHzfbNZt3ZWpdWlXwOSLRzL+mL6E4Ky8cpOBXpIktWjvb93FlAfm8PCs1TXqF47pw5RLR9GrU1FCnUnZYaCXJEktUoyRe99cxTcfmsumHeWZes9Obbn1slGcP/rQBLuTssdAL0mSWpxVm3Zy472zeG7B+zXqVxzbj5suGkmX9gUJdSZln4FekiS1GFVVkf/563KmPjqf7WWVmXq/Q9px57gxfGRIzwS7k5qGgV6SJLUIi9/fxvUzZ/Haso2ZWgjw6ZP78/XzhtGhrbFHLVNWjuwQwjTgOGAo0APYCSwH7gN+HGPcUMdxlgFH1vLy2hhjn1q2OwW4CTgJKAIWAb8BfhRjrNzXNpIkqWWoqKziFy8u4ftPvUNZRVWmPqhnB6ZPKObYI7sl2J3U9LL1o+pXgTeAJ4F1QAdS4XoK8PkQwkkxxpW1b17DZuD7+6hv20eNEMJlwEygFPgTsBG4BPgecCpwRV0/hCRJyi1z3tvMxJklzF61JVNrkxf44umD+PezBlNUkJ9gd9LBka1A3znGWLp3MYRwO3ADcD3w5TqOtSnGOKUuK4YQOgO/BCqBM2KMf0/XJwPPABNCCB+LMd5dx/eWJEk5oLS8kh898w4/f34JlVUxUx/dtzPTxhcz6rAuCXYnHVx52RhkX2E+7c/p5ZBsvM8+TAB6AnfvDvPV+rkp/e2Xmui9JUlSAl5fvpGLfvgiP3l2cSbMt22Tx6QLhnPfl081zKvVaeqrQy5JL0vqsU3bEMIngCOA7eltX6jlXPiz0svH9vHaC8AO4JQQQtsY4659rJMRQni9lpeG161tSZLUlLbvquBbjy/gv19dRtwzKc8J/bsxdfwYBvbsmFxzUoKyGuhDCNcCHYEupC6S/TCpQD61HsP0AX6/V21pCOGzMcbn96oPSy8X7j1IjLEihLAUGAUMBObVowdJktSMvLDwfa6/ZxarNu3M1DoU5jPpwhF8/IQjyMsLCXYnJSvbM/TXAr2rff8Y8JkY4/u1rL+3u4AXgTnAVlJB/N+BzwOPhhBOjjG+XW393b9T21zLeLvrXQ/0xjHGY/dVT8/cH3PAziVJUtZt2lHGbQ/PY8br79aonzGsJ7ePHUPfru0S6kxqPrIa6HffVjKE0Bs4hdTM/JshhItjjG/UYftb9irNBr4YQtgGfI3UXXPG1qOl3T+ux/2uJUmSmp1HZ61m8v1zWL9tz1mzXdsXcPMlI7n86L6E4Ky8BE10Dn2McS1wbwjhDVKnw/wOGN2IIX9OKtCftld99wx8bVe/dN5rPUmS1Myt21rKzffP4dHZa2rULy4+lCmXjqJHx7YJdSY1T016UWyMcXkIYS5wdAihR4xxfQOHWpdedtirvoA9D7SqcVFrCKENMACoAJY08H0lSdJBEmNkxuvvctvD89i8szxT79WpLbddPppzR+3z+ZJSq3cwnoF8WHrZmCe2npxe7h3MnwE+DpwP/HGv104D2pO6Q85+73AjSZKStXLjDm64dxYvvlNz7u9jxx/O9ReOoEu7goQ6k5q/Rgf6EMJwUg+DWrNXPQ+4FegFvBJj/CBdLwAGAeUxxsXV1h8FrI4xbtxrnCOBH6e//cNebz8DmAZ8LITwo2oPlioCbkuv87PGfkZJktQ0qqoiv3t1GdMfX8COsj1zf4d3a8fUccWcOrhHgt1JuSEbM/TnA98KIbwALAY2kLrTzemk7lKzBriy2vp9Sd1CcjnQv1r9CmBSCOFZYCmpu9wMAi4CioBHgG9Xf+MY45YQwpWkgv1zIYS7gY3ApaRuaTkD+FMWPqMkScqyReu2MnHmLF5f/kGmFgL866kD+Nq5Q2lfeDBOJJByXzb+T3kK+AVwKnAUqVtEbid1MezvgR/uPetei2dJhfAPkTrFpgOwCXgpPc7vY4z/cLeaGON9IYTTgRuB8aTC/yLgP9Pv7R1uJElqRsorq/jFC0v4wVPvUFZZlakP6dWRaROKOeaIQxLsTso9jQ70McbZwFX1WH8Ze24nWb3+PLD3g6PqOubLwIUN2VaSJB08s1dt5roZJcxdvSVTa5MXuOrMwXz5zEG0bZOfYHdSbvJ3WZIkqcmVllfyg6ff4RcvLKGyas8vz4v7dWH6hGKG9+m8n60l7Y+BXpIkNanXlm5k0swSlqzfnqm1bZPHtecO47On9qdNfl6C3Um5z0AvSZKaxLZdFUx7dD6//8vyGvUTB3Rj2vhi+vfY+/EykhrCQC9JkrLu2QXruPGeWby3uTRT69i2DTdcOIKPHX84eXn/cDmdpAYy0EuSpKz5YHsZtz40l3veXFWjfvbwXtw2djSHdmmXUGdSy2WglyRJjRZj5JFZa7j5gdms31aWqXfrUMjNl4zk0qMOIwRn5aWmYKCXJEmNsnZLKZPvm80Tc9fWqF929GF84+KRdO/YNqHOpNbBQC9Jkhokxsif/76S2x6ex9bSiky9T+cibrt8NOeM7J1gd1LrYaCXJEn1tmLDDq6/t4SXF22oUf9/Jx7BpAuG07moIKHOpNbHQC9Jkuqssiry21eW8e3HF7CzvDJTP7J7e+4cN4ZTBvVIsDupdTLQS5KkOnln7Vaum1nCmys2ZWp5AT73kYF89ZyhtCvMT645qRUz0EuSpP0qq6ji588v5sfPLKKssipTH96nE9PGF3PU4V2Ta06SgV6SJNWu5N1NXDejhPlrtmZqBfmBfz9zCF86YxCFbfIS7E4SGOglSdI+7Cyr5PtPLeSXLy6hKu6pH314V6ZPKGZo707JNSepBgO9JEmq4S9LNjBpZgnLNuzI1IoK8rj23GF89tQB5Of5gCipOTHQS5IkALaWljP10fn8z19X1KifMqg7U8cVc0T39gl1Jml/DPSSJIln5q/lxntns3pzaabWqagNN100gn867nBCcFZeaq4M9JIktWIbtu3imw/N5f633qtR/+jI3tx2+Wh6dy5KqDNJdWWglySpFYox8mDJaqY8MIeN28sy9e4dCrnlslFcNOZQZ+WlHGGglySplVmzuZSb7pvFU/PW1aiP/VBfvnHxSA7pUJhQZ5IawkAvSVIrEWPk7r+t5I6H57F1V0WmfmiXIu4YO4Yzh/dKsDtJDWWglySpFVi+YTuTZs7i1SUbatQ/edKRXHf+MDoVFSTUmaTGMtBLktSCVVZF7np5Kd9+YgGl5VWZ+oAeHZg6bgwnDuyeYHeSssFAL0lSC7VgzVaum1nC2ys3ZWr5eYErPzKQa84ZQlFBfnLNScoaA70kSS1MWUUVP3l2ET99bhHllTFTH3FoZ6aPL2ZMvy4Jdicp2wz0kiS1IG+t3MR1M95m4dptmVphfh5Xnz2YL5w+iIL8vAS7k9QUDPSSJLUAO8sq+c4TC/jNy0up2jMpzzFHdGX6hGIG9+qUXHOSmpSBXpKkHPfK4vVMmjmLFRt3ZGrtC/O57rxhfPLk/uTn+YAoqSUz0EuSlKO2lJZz5yPz+ONrK2vUPzKkB3eMHcPh3don1Jmkg8lAL0lSDnpy7lpuum8Wa7fsytQ6F7Vh8sUjmXBsP0JwVl5qLQz0kiTlkPXbdjHlgTk8VLK6Rv28Ub259bLR9OpclFBnkpJioJckKQfEGLn/rfe45cE5fLCjPFPv0bEtt142igvGHJpgd5KSZKCXJKmZe2/TTm66bzbPzF9Xoz7+mH5MvngEXdsXJtSZpObAQC9JUjNVVRX539dWMPXR+WzbVZGp9+3ajjvGjeH0oT0T7E5Sc5GVQB9CmAYcBwwFegA7geXAfcCPY4wb6jBGd2AscBEwBugLlAGzgLuAu2KMVXtt0x9Yup9h/xRj/Fg9P44kSYlbun47E2eW8NrSjZlaCPCpk47k6+cPp2Nb5+QkpWTrb4OvAm8ATwLrgA7AScAU4PMhhJNijCtr3xyAK4CfAauBZ4EVQG9gHPAr4IIQwhUxxriPbd8m9cPD3mbX+5NIkpSgisoqfvXSUr735EJ2VeyZxxrYswPTxhdzfP9uCXYnqTnKVqDvHGMs3bsYQrgduAG4HvjyAcZYCFwKPFx9Jj6EcAPwGjCeVLifuY9t34oxTmlY65IkNQ9z39vCxJklzFq1OVPLzwt84bSBXH32EIoK8hPsTlJzlZVAv68wn/ZnUoF+SB3GeKaW+poQws+B24Ez2HeglyQpZ+2qqOTHzyziZ88tpqJqzy+iRx7amekTihndt0uC3Ulq7pr6BLxL0suSRo6z+/5cFbW8flgI4QtAd2AD8GqMsbHvKUlSk3t9+QdMnFnConXbMrXCNnlcc84QrvzIQAry8xLsTlIuyGqgDyFcC3QEupC6SPbDpML81EaM2Qb4VPrbx2pZ7aPpr+rbPQd8Osa4oo7v83otLw2vy/aSJNXHjrIKvvX4An77yjKqXx12fP9DmDq+mEE9OybXnKScku0Z+mtJXci622PAZ2KM7zdizKnAaOCRGOPje722A7iV1AWxS9K1YlIX454JPB1CODrGuL0R7y9JUla99M56Jt1Twrsf7MzUOhTmM/GC4XzixCPJywsJdicp12Q10McY+wCEEHoDp5AK42+GEC6OMb5R3/FCCFcDXwPmA5/cx/utA76xV/mFEMK5wEvAicDngB/Uofdja+nhdeCY+nUuSdI/2ryjnNsfmcuf//5ujfppQ3tyx9jR9DukfUKdScplTXIOfYxxLXBvCOENUnev+R2pWfY6CyFcRSqIzwXOjjFuPMAm1d+/IoTwK1KB/jTqEOglSWpKj81ew+T7Z/P+1l2ZWpd2BXzj4pGMO6YvITgrL6lhmvSi2Bjj8hDCXODoEEKPGOP6umwXQrgG+B6p+8ifnZ6Jr6/dp/l0aMC2kiRlxftbdzHlgTk8PGt1jfpFYw5lyqWj6NmpbUKdSWopDsZj5g5LLyvrsnIIYSKpU3XeAj5a1x8C9uGk9HLJfteSJKkJxBi5541VfPOhuWzeWZ6p9+zUllsvG835o/sk2J2klqTRgT6EMBzYFGNcs1c9j9QFq72AV2KMH6TrBcAgoDzGuHivbSYD3wReB8490Gk2IYQTgTdjjGV71c8i9fRagD809LNJktQQqzbt5IZ7ZvH8wpr3hPin4/px44Uj6dK+IKHOJLVE2ZihPx/4VgjhBWAxqfvA9wZOBwYCa4Arq63fF5gHLAf67y6GED5NKsxXAi8CV+/jfMJlMcbfVvt+GjAqfYvK3VcYFQNnpf88Ocb4SqM+nSRJdVRVFfnDX5cz7dH5bC/b84vpfoe0Y+q4Yj48pEeC3UlqqbIR6J8CfgGcChwFdAW2k7oY9vfAD+t4QeuA9DIfuKaWdZ4Hflvt+98DY4HjgQuAAmAtqSfU/jjG+GLdP4YkSQ23+P1tTJpZwt+WfZCphQCfOaU/1547jA5tD8ZZrpJao0b/7RJjnA1cVY/1lwH/MPUeY5xC6v7x9XnvXwO/rs82kiRlU3llFb98cQnff+odyiqqMvXBvToybXwxxx55SILdSWoNnC6QJKmBZq/azMSZJcx5b0um1iYv8OUzBnHVWYNp2yY/we4ktRYGekmS6qm0vJIfPfMOP39+CZVVMVMf07cL08YXM/Kwzgl2J6m1MdBLklQPf1+2ketmlrDk/e2ZWts2efznR4fybx8eQJv8vAS7k9QaGeglSaqDbbsq+NZj8/ndX5YT90zKc8KAbkwdN4aBPTsm15ykVs1AL0nSATy/8H1uuGcWqzbtzNQ6tm3DpAuG8/9OOIK8vH+414MkHTQGekmSarFpRxm3PjSPmW+8W6N+xrCe3DF2DId1bZdQZ5K0h4FekqR9eHTWaibfP4f123Zlaoe0L+DmS0Zx2dGHsY+HH0pSIgz0kiRVs25LKd+4fw6PzVlTo37JUYdx8yUj6dGxbUKdSdK+GeglSQJijMx4/V1ufWguW0orMvXendty2+Vj+OjI3gl2J0m1M9BLklq9lRt3cMO9s3jxnfU16v9ywuFMumAEXdoVJNSZJB2YgV6S1GpVVkV+9+oyvvX4AnaUVWbqR3Rrz9RxYzhlcI8Eu5OkujHQS5JapUXrtjJx5ixeX/5BppYX4F9PHcB/njuU9oX+EykpN/i3lSSpVSmvrOK/nl/MD59eRFllVaY+tHdHpo0v5kNHHJJgd5JUfwZ6SVKrMevdzVw3s4R5q7dkagX5gS+fMZirzhxMYZu8BLuTpIYx0EuSWrzS8kq+/9Q7/PLFJVRWxUz9qH5dmDahmOF9OifYnSQ1joFektSi/XXJBibdM4ul67dnakUFeXzto8P41w8PID/PB0RJym0GeklSi7S1tJzpjy3g939ZXqN+0sBuTB1XTP8eHRLqTJKyy0AvSWpxnl2wjhvvmcV7m0sztU5t23DDRSP45+MOJ89ZeUktiIFektRifLC9jFsfmss9b66qUT9nRC9uu3wMfboUJdSZJDUdA70kKefFGHl41mpuvn8OG7aXZerdOhQy5dJRXFJ8KCE4Ky+pZTLQS5Jy2totpdx032yenLu2Rv2yow/j5ktG0a1DYUKdSdLBYaCXJOWkGCN//vtKbnt4HltLKzL1Pp2LuH3saM4e0TvB7iTp4DHQS5JyzooNO5h0TwmvLN5Qo/7xE49g4gXD6VxUkFBnknTwGeglSTmjsiry21eW8e3HF7CzvDJT79+9PVPHF3PSwO4JdidJyTDQS5JywsK1W7luRglvrdyUqeUFuPIjA7nmnKG0K8xPrjlJSpCBXpLUrJVVVPGz5xbz42ffobwyZurD+3Ri+oRiivt1Ta45SWoGDPSSpGbr7ZWbmDizhPlrtmZqBfmB/zhrCF88fRCFbfIS7E6SmgcDvSSp2dlZVsn3nlrIr15cQtWeSXmOPrwr0ycUM7R3p+Sak6RmxkAvSWpWXl28gevvKWHZhh2ZWruCfK49bxifOaU/+Xk+IEqSqjPQS5KahS2l5Ux9dD7/+9cVNeqnDu7OnWOLOaJ7+4Q6k6TmzUAvSUrc0/PWcuO9s1mzpTRT61TUhskXjeSK4/oRgrPyklQbA70kKTEbtu3ilgfn8sDb79Wof3Rkb267fDS9Oxcl1Jkk5Q4DvSTpoIsx8sDb73HLg3PZuL0sU+/RsZBbLh3NhWP6OCsvSXVkoJckHVSrN+/kpntn8/T8dTXq4z7Ul8kXj+SQDoUJdSZJuSkrN/ANIUwLITwdQlgZQtgZQtgYQngzhHBzCKFez+EOIfQLIfwmhPBeCGFXCGFZCOH7IYRD9rPNKSGER9LvuyOEUBJCuCaE4GMDJamZqKqK/O9fV3Dud1+oEeYP61LEXZ89nu/+89GGeUlqgGzN0H8VeAN4ElgHdABOAqYAnw8hnBRjXHmgQUIIg4BXgF7A/cB84ATgK8D5IYRTY4wb9trmMmAmUAr8CdgIXAJ8DzgVuCILn0+S1AjL1m9n0j0l/GXJxhr1T518JNedP5yObf2FsSQ1VLb+Bu0cYyzduxhCuB24Abge+HIdxvkpqTB/dYzxR9XG+S6pHxpuB75Yrd4Z+CVQCZwRY/x7uj4ZeAaYEEL4WIzx7oZ+MElSw1VUVnHXy8v4zpMLKC2vytQH9OjAtPHFnDCgW4LdSVLLkJVTbvYV5tP+nF4OOdAYIYSBwLnAMuAne718M7Ad+GQIoUO1+gSgJ3D37jBfrZ+b0t9+6UDvLUnKvvlrtjD+Z69w+yPzMmE+Py/wpTMG8ehXPmKYl6QsaerfcV6SXpbUYd2z0ssnYoxV1V+IMW4NIbxMKvCfBDy91zaP7WO8F4AdwCkhhLYxxl316lyS1CC7Kir5ybOL+emzi6ioipn6iEM7M318MWP6dUmwO0lqebIa6EMI1wIdgS7AccCHSYX5qXXYfFh6ubCW198hFeiHsifQ17pNjLEihLAUGAUMBOYdoPfXa3lp+P62kyTt8eaKD5g4s4SFa7dlaoX5eXzlnCF8/rSBFORn5RfDkqRqsj1Dfy3Qu9r3jwGfiTG+X4dtd0/ZbK7l9d31ro3cRpKUZTvKKvjOEwv5zctLiXsm5Tn2yEOYNr6Ywb06JtecJLVwWQ30McY+ACGE3sAppGbm3wwhXBxjfKORw+9+wkjc71oN3CbGeOw+B0jN3B9Tj/eUpFbllUXrmXTPLFZs3JGptS/M5+vnDeNTJ/cnP88HRElSU2qSc+hjjGuBe0MIb5A6HeZ3wOgDbLZ7Nr22kys777VeQ7eRJGXB5p3l3PnIPO7+W827En9kSA/uGDuGw7u1T6gzSWpdmvSi2Bjj8hDCXODoEEKPGOP6/ay+IL0cWsvru++UU/18+QWkztUfCtQ4Bz6E0AYYAFQAS+rbuySpdk/MWcNN981m3dY99xvoXNSGyRePZMKx/QjBWXlJOlgOxpM8DksvKw+w3rPp5bkhhLzqd7oJIXQi9ZConcBfqm3zDPBx4Hzgj3uNdxrQHnjBO9xIUnas37aLKQ/M4aGS1TXq54/qwzcvH0WvTkUJdSZJrVejbzcQQhgeQuizj3pe+sFSvYBXYowfpOsF6W0GVV8/xrgYeALoD1y113C3kHr67O9ijNur1WcA64GPhRCOq/beRcBt6W9/1pjPJ0mCGCP3vvku53z3+RphvkfHtvzs48fw808ea5iXpIRkY4b+fOBbIYQXgMXABlJ3ujmd1O0i1wBXVlu/L6lbSC4nFd6r+zLwCvDDEMLZ6fVOBM4kdarNjdVXjjFuCSFcSSrYPxdCuBvYCFxK6paWM4A/ZeEzSlKr9d6mndx47yyeXVDzhmUTju3HTReNoGv7woQ6kyRBdgL9U8AvSJ0ScxSpW0RuJxXAfw/8MMa4sS4DxRgXp2fav0nqB4ULgdXAD4Fb9jVOjPG+EMLppML+eKAIWAT8Z/q963NXHElSWlVV5H9eW8G0R+ezbVdFpt63azvuHDeG04b2TLA7SdJujQ70McbZ/OMpMvtbfxl7bie5r9dXAp+tZw8vkwr/kqQsWPL+NibNnMVry/bMo4QAnz65P18/bxgd2h6MS7AkSXXh38iSpIyKyip+9dJSvvfkQnZVZO5NwMCeHZg+vpjj+ndLsDtJ0r4Y6CVJAMx9bwvXzXyb2au2ZGr5eYEvnj6Q/zhrCEUF+Ql2J0mqjYFeklq5XRWV/PiZRfzsucVUVO257GjUYZ2ZPqGYUYfV9uw+SVJzYKCXpFbs9eUfMHFmCYvWbcvUCtvk8dVzhnLlRwbQJr/RdzeWJDUxA70ktULbd1Xw7ScW8NtXllH9XmDH9z+EqeOLGdSzY3LNSZLqxUAvSa3Mi++8z/X3zOLdD3Zmah0K85l0wXA+fuKR5OXVeiMySVIzZKCXpFZi845ybnt4Lv/3+rs16qcP7cntY0fT75D2CXUmSWoMA70ktQKPzV7D5Ptn8/7WXZla1/YFfOPikYz9UF9CcFZeknKVgV6SWrB1W0uZ8sAcHpm1pkb9ouJDmXLJKHp2aptQZ5KkbDHQS1ILFGNk5huruPWhuWzeWZ6p9+zUltsuH815o/ok2J0kKZsM9JLUwrz7wQ5uuHc2Lyx8v0b9n487nBsuHEGX9gUJdSZJagoGeklqIaqqIr//y3KmPTafHWWVmXq/Q9oxdVwxHx7SI8HuJElNxUAvSS3A4ve3MXFGCX9f/kGmFgJ89pQBXHveUNoX+te9JLVU/g0vSTmsvLKKX7ywhB88/Q5lFVWZ+uBeHZk2vphjjzwkwe4kSQeDgV6SctTsVZuZOLOEOe9tydTa5AW+fMYgrjprMG3b5CfYnSTpYDHQS1KOKS2v5IdPv8N/vbCEyqqYqY/p24XpE4oZcWjnBLuTJB1sBnpJyiF/W7aRiTNKWLJ+e6bWtk0e//nRofzbhwfQJj8vwe4kSUkw0EtSDti2q4Lpj83nd68ur1E/YUA3po0vZkCPDgl1JklKmoFekpq55xe+zw33zGLVpp2ZWse2bZh0wXD+3wlHkJcXEuxOkpQ0A70kNVObdpTxzYfmcs8bq2rUzxzWk9vHjuGwru0S6kyS1JwY6CWpGXpk1mq+cf9s1m8ry9QOaV/AlEtHcelRhxGCs/KSpBQDvSQ1I+u2lDL5/tk8PmdtjfolRx3GlEtG0r1j24Q6kyQ1VwZ6SWoGYoz83+vvcttDc9lSWpGp9+7cltsuH8NHR/ZOsDtJUnNmoJekhK3cuIPr75nFS4vW16j/ywmHc/2FI+hcVJBQZ5KkXGCgl6SEVFZFfvfqMqY/toCd5ZWZ+hHd2jN13BhOGdwjwe4kSbnCQC9JCVi0bivXzSjhjRWbMrW8AP966gC+du4w2hXmJ9ecJCmnGOgl6SAqr6zi588t5kfPLKKssipTH9q7I9PGF/OhIw5JsDtJUi4y0EvSQTLr3c18fcbbzF+zNVMryA9cdeZgvnzGYArb5CXYnSQpVxnoJamJlZZX8r2nFvLLF5ZQFffUjzq8K9PHFzOsT6fkmpMk5TwDvSQ1ob8u2cCke2axdP32TK2oII9rzx3GZ08dQH6eD4iSJDWOgV6SmsDW0nKmPTafP/xlRY36yQO7M3X8GI7s3iGhziRJLY2BXpKy7Nn567jx3lm8t7k0U+vUtg03XDSCjx1/OCE4Ky9Jyh4DvSRlycbtZdz60FzufXNVjfo5I3px2+Vj6NOlKKHOJEktmYFekhopxshDJauZ8sAcNmwvy9S7dyhkyqWjuLj4UGflJUlNxkAvSY2wdkspN947m6fmra1Rv/zow/jGJaPo1qEwoc4kSa1FowN9CKE7MBa4CBgD9AXKgFnAXcBdMcaq2kfIjPOZ9Pr7UxVjzDw+MYTQH1i6n/X/FGP82IHeW5LqK8bIn/62ktsfmcfW0opM/dAuRdw+djRnDe+dYHeSpNYkGzP0VwA/A1YDzwIrgN7AOOBXwAUhhCtijLH2IQB4C7illtc+ApwFPFrL628D9+2jPvsA7ylJ9bZiww4m3VPCK4s31Kh//MQjmHTBcDoVFSTUmSSpNcpGoF8IXAo8XH0mPoRwA/AaMJ5UuJ+5v0FijG+RCvX/IITwavqPv6hl87dijFPq07Qk1VdlVeSul5fy7ScWUFq+5xeP/bu3Z+r4Yk4a2D3B7iRJrVWjA32M8Zla6mtCCD8HbgfO4ACBvjYhhNHAScAq4OEGtilJjbJgzVaum1nC2ys3ZWp5Aa78yECuOWco7Qrza99YkqQm1NQXxZanlxX7XWv/vpBe/jrGWFnLOoeFEL4AdAc2AK/GGEsa8Z6SBEBZRRU/fW4RP3l2EeWVe84cHN6nE9MnFFPcr2tyzUmSRBMG+hBCG+BT6W8fa+AY7YBPAFWkzsevzUfTX9W3fQ74dIxxxT63+Mf3er2Wl4bXZXtJLc/bKzdx3YwSFqzdmqkV5ufxH2cN5gunD6KwTV6C3UmSlNKUM/RTgdHAIzHGxxs4xj8BXUmdn79yH6/vAG4ldUHsknStGJgCnAk8HUI4Osa4vYHvL6kV2llWyXefXMCvX1pKVbXL+T90RFemjy9mSO9OyTUnSdJemiTQhxCuBr4GzAc+2YihPp9e/te+XowxrgO+sVf5hRDCucBLwInA54AfHOiNYozH7quenrk/pq4NS8ptry7ewKR7Sli+YUem1q4gn6+fN4xPn9Kf/DwfECVJal6yHuhDCFeRCtBzgbNjjBsbOM5I4BTgXeCR+mwbY6wIIfyKVKA/jToEekmt25bScu58ZD5/fK3mWXqnDu7OnWOLOaJ7+4Q6kyRp/7Ia6EMI1wDfI3X/97PTM+gNVZeLYffn/fSyQyN6kNQKPDV3LTfeN4u1W3Zlap2K2jD5opFccVw/QnBWXpLUfGUt0IcQJpI6b/4t4KMxxvWNGKuI1Kk6VcCvGzjMSenlkv2uJanV2rBtF7c8OJcH3n6vRv3ckb259fLR9O5clFBnkiTVXVYCfQhhMvBN4HXg3P2dZhNCKAAGAeUxxsW1rHYFcAjwUC0Xw+4e60TgzRhj2V71s4Cvpr/9Q50/iKRWIcbIA2+/x5QH5vDBjvJMvUfHQm65dDQXjunjrLwkKWc0OtCHED5NKsxXAi8CV+/jH8JlMcbfpv/cF5gHLAf61zLs7otha3sy7G7TgFHpW1S+m64VA2el/zw5xvjKAT+EpFZj9ead3HTvbJ6eX/OMwHHH9GXyRSM5pENhQp1JktQw2ZihH5Be5gPX1LLO88Bv6zJYCGEE8GHqdjHs74GxwPHABUABsBb4M/DjGOOLdXlPSS1fVVXkj39bwZ2PzGfbrj3PujusSxG3jxvDmcN6JdidJEkN1+hAH2OcQuq+73VdfxlQ6++yY4zz9vf6Xuv+moafYy+plVi2fjuT7inhL0tqng34qZOP5Lrzh9OxbVM/NFuSpKbjv2KSWqyKyip+8/JSvvPEQnZVVGXqA3t0YOr4Yk4Y0C3B7iRJyg4DvaQWad7qLUycWULJu5sztfy8wOdPG8hXzh5CUUF+gt1JkpQ9BnpJLcquikp+8uxifvrsIiqqYqY+8tDOTJ9QzOi+XRLsTpKk7DPQS2ox3ljxARNnlPDOum2ZWmF+Hl85ZwifP20gBfl5CXYnSVLTMNBLynk7yir4zhML+c3LS4l7JuU59shDmDa+mMG9OibXnCRJTcxALymnvbxoPZPuKWHlxp2ZWvvCfK47bxifOrk/eXk+IEqS1LIZ6CXlpM07y7nj4Xn86e81Hyb9kSE9uGPsGA7v1j6hziRJOrgM9JJyzhNz1nDTfbNZt3VXptalXQGTLx7J+GP6so+nVUuS1GIZ6CXljPe37mLKg3N4uGR1jfoFo/twy2Wj6NWpKKHOJElKjoFeUrMXY+S+t1Zxy4Nz2bSjPFPv0bEtt142igvGHJpgd5IkJctAL6lZW7VpJzfeO4vnFrxfoz7h2H7cdNEIurYvTKgzSZKaBwO9pGapqiryP39dztRH57O9rDJT79u1HXeOG8NpQ3sm2J0kSc2HgV5Ss7Pk/W1MmjmL15ZtzNRCgE+f3J+vnzeMDm39q0uSpN38V1FSs1FRWcUvX1zK955aSFlFVaY+qGcHpo0v5rj+3RLsTpKk5slAL6lZmPveFq6b+TazV23J1PLzAl86fRD/ftZgigryE+xOkqTmy0AvKVGl5ZX8+JlF/Pz5xVRUxUx91GGdmT6hmFGHdUmwO0mSmj8DvaTEvL58I9fNKGHx+9sztcI2eXz1nKFc+ZEBtMnPS7A7SZJyg4Fe0kG3fVcF33p8Af/96jLinkl5ju9/CFPHFzOoZ8fkmpMkKccY6CUdVC8sfJ/r75nFqk07M7UOhflMumA4Hz/xSPLyQoLdSZKUewz0kg6KzTvKufXhucx4/d0a9dOH9uSOcWPo27VdQp1JkpTbDPSSmtxjs1cz+f45vL91V6bWtX0B37h4JGM/1JcQnJWXJKmhDPSSmsy6raXcfP8cHp29pkb9ouJDmXLJKHp2aptQZ5IktRwGeklZF2Nk5huruPWhuWzeWZ6p9+zUltsuH815o/ok2J0kSS2LgV5SVq3cuIMb7p3Fi++sr1H/5+MO54YLR9ClfUFCnUmS1DIZ6CVlRVVV5HevLmP64wvYUVaZqR/erR13ji3mw0N6JNidJEktl4FeUqMtWreNSTNL+PvyDzK1EOCzpwzg2vOG0r7Qv2okSWoq/isrqcHKK6v4xQtL+MFT71BWWZWpD+nVkWkTijnmiEMS7E6SpNbBQC+pQWav2sx1M0qYu3pLptYmL/DlMwZx1VmDadsmP8HuJElqPQz0kuqltLySHzz9Dr94YQmVVTFTH9O3C9MnFDPi0M4JdidJUutjoJdUZ39btpGJM0pYsn57pta2TR7/+dGh/NuHB9AmPy/B7iRJap0M9JIOaNuuCqY/Np/fvbq8Rv3EAd2YOr6YAT06JNSZJEky0Evar+cWrOPGe2ezatPOTK1j2zZcf+Fw/uX4I8jLCwl2J0mSDPSS9umD7WXc+vBc7nljVY36WcN7cfvY0RzapV1CnUmSpOoM9JJqiDHyyKw13PzAbNZvK8vUD2lfwJRLR3HpUYcRgrPykiQ1F40O9CGE7sBY4CJgDNAXKANmAXcBd8UYq2ofocZYy4Aja3l5bYyxTy3bnQLcBJwEFAGLgN8AP4oxVu5rG0n/aN2WUm66bzZPzF1bo37JUYcx5ZKRdO/YNqHOJElSbbIxQ38F8DNgNfAssALoDYwDfgVcEEK4IsYYax+ihs3A9/dR37avlUMIlwEzgVLgT8BG4BLge8Cp6f4k7UeMkf/7+7vc+vBctpZWZOq9O7fltsvH8NGRvRPsTpIk7U82Av1C4FLg4eoz8SGEG4DXgPGkwv3MOo63KcY4pS4rhhA6A78EKoEzYox/T9cnA88AE0IIH4sx3l3H95ZanZUbd3D9PbN4adH6GvV/OeEIrr9wOJ2LChLqTJIk1UWjbxodY3wmxvjg3qfVxBjXAD9Pf3tGY9+nFhOAnsDdu8N8+r1LSZ2CA/ClJnpvKadVVkV+89JSzv3eCzXC/JHd2/O/V57InePGGOYlScoBTX1RbHl6WbHftWpqG0L4BHAEsB0oAV6o5Vz4s9LLx/bx2gvADuCUEELbGOOuevQgtWjvrN3KxJklvLFiU6aWF+DfPjyA//zoMNoV5ifXnCRJqpcmC/QhhDbAp9Lf7itw16YP8Pu9aktDCJ+NMT6/V31Yerlw70FijBUhhKXAKGAgMO8A/b5ey0vDD9yylBvKKqr4r+cX86NnFlFWueeXasN6d2LahGKOPrxrcs1JkqQGacoZ+qnAaOCRGOPjddzmLuBFYA6wlVQQ/3fg88CjIYSTY4xvV1u/S3q5uZbxdte71qNvqUUqeXcT180oYf6arZlaQX7gqjMH8+UzBlPYptFn4EmSpAQ0SaAPIVwNfA2YD3yyrtvFGG/ZqzQb+GIIYVt6vCmkbpFZ51Z2D12H9z52nwOkZu6Pqcd7Ss1KaXkl33tyIb98cQlV1f5POOrwrkwfX8ywPp2Sa06SJDVa1gN9COEq4AfAXODsGOPGLAz7c1KB/rS96rtn4Luwb533Wk9qVf6yZAOTZpawbMOOTK2oII9rzx3GZ08dQH6eD4iSJCnXZTXQhxCuIXX/99mkwvy6LA29e5wOe9UXAMcBQ4Ea58Cnz+EfQOqC3CVZ6kPKCVtLy5n66Hz+568ratRPHtidqePHcGT3vf9XkiRJuSprgT6EMJHUefNvAR+NMa7f/xb1cnJ6uXcwfwb4OHA+8Me9XjsNaE/qDjne4UatxrPz13HDvbNYvbk0U+vUtg03XjSCfz7+cEJwVl6SpJYkK4E+/SCnb5KaJT93f6fZhBAKgEFAeYxxcbX6KGD13tuGEI4Efpz+9g97DTcDmAZ8LITwo2oPlioCbkuv87MGfzAph2zcXsY3H5zDfW+9V6N+zohe3Hb5GPp0KUqoM0mS1JQaHehDCJ8mFeYrSd2h5up9zAAuizH+Nv3nvqRuIbkc6F9tnSuASSGEZ4GlpO5yMwi4CCgCHgG+XX3QGOOWEMKVpIL9cyGEu4GNpJ5cOyxd/1NjP6PUnMUYebBkNVMemMPG7WWZevcOhUy5dBQXFx/qrLwkSS1YNmboB6SX+cA1tazzPPDbA4zzLKkQ/iFSp9h0ADYBL5G6L/3vY4z/cLeaGON9IYTTgRuB8aTC/yLgP4Ef7msbqaVYs7mUm+6bzVPz1taoX370YXzjklF061CYUGeSJOlgaXSgjzFOIXU7ybquv4w9t5OsXn+eVPBvSA8vAxc2ZFspF8UYuftvK7nj4Xls3bXnQcyHdini9rGjOWt47wS7kyRJB1NTPlhKUhNYvmE7k2bO4tUlG2rUP3HSEUw8fzidigoS6kySJCXBQC/liMqqyF0vL+XbTyygtLwqU+/fvT1Txxdz0sDuCXYnSZKSYqCXcsCCNVu5bmYJb6/clKnlBbjytIF89ZyhFBXkJ9ecJElKlIFeasbKKqr46XOL+Mmziyiv3HN99/A+nZg+oZjifl2Ta06SJDULBnqpmXpr5SYmzihhwdqtmVphfh7/cdZgvnD6IArb5CXYnSRJai4M9FIzs7Osku8+uYBfv7SUqmo3Xf3QEV2ZPr6YIb07JdecJElqdgz0UjPyyuL1TJo5ixUbd2Rq7Qry+fp5w/j0Kf3Jz/MBUZIkqSYDvdQMbCkt585H5vPH11bUqH94cA/uHDeGw7u1T6gzSZLU3BnopYQ9NXctN943i7VbdmVqnYraMPmikVxxXD9CcFZekiTVzkAvJWTDtl1MeXAuD779Xo36uSN7c+vlo+nduSihziRJUi4x0EsHWYyRB95+jykPzOGDHeWZeo+Ohdxy6WguHNPHWXlJklRnBnrpIHpv005uum82z8xfV6M+7pi+TL5oJId0KEyoM0mSlKsM9NJBUFUV+ePfVnDnI/PZtqsiU+/btR23jx3NGcN6JdidJEnKZQZ6qYktXb+dSTNL+OvSjTXqnzr5SK47fzgd2/q/oSRJajiThNREKiqr+PVLS/nukwvZVVGVqQ/s0YGp44s5YUC3BLuTJEkthYFeagLzVm9h4swSSt7dnKnl5wU+f9pAvnL2EIoK8hPsTpIktSQGeimLdlVU8pNnFvHT5xZTURUz9ZGHdmb6hGJG9+2SYHeSJKklMtBLWfLGig+YOKOEd9Zty9QK2+TxlbOH8PnTBlKQn5dgd5IkqaUy0EuNtKOsgm8/vpC7XllK3DMpz7FHHsK08cUM7tUxueYkSVKLZ6CXGuGld9Zz/b0lrNy4M1NrX5jPxPOH88mTjiQvzwdESZKkpmWglxpg885ybn94Ln/++7s16h8Z0oM7xo7h8G7tE+pMkiS1NgZ6qZ4en7OGyffNZt3WXZlal3YFTL54JOOP6UsIzspLkqSDx0Av1dH7W3cx5YE5PDxrdY36BaP7cMtlo+jVqSihziRJUmtmoJcOIMbIvW+u4psPzWXTjvJMvUfHttx62SguGHNogt1JkqTWzkAv7ceqTTu58d5ZPLfg/Rr1K47tx00XjaRL+4KEOpMkSUox0Ev7UFUV+Z+/Lmfqo/PZXlaZqfft2o47x43htKE9E+xOkiRpDwO9tJfF729j0swS/rbsg0wtBPj0yf35+nnD6NDW/20kSVLzYTKR0ioqq/jFi0v4/lPvUFZRlakP6tmBaeOLOa5/twS7kyRJ2jcDvQTMeW8zE2eWMHvVlkwtPy/wpdMH8e9nDaaoID/B7iRJkmpnoFerVlpeyY+eeYefP7+EyqqYqY/u25lp44sZdViXBLuTJEk6MAO9Wq3Xl2/kuhklLH5/e6ZW2CaPr54zlCs/MoA2+XkJdidJklQ3Bnq1Ott3VfCtxxfw368uI+6ZlOeE/t2YOn4MA3t2TK45SZKkejLQq1V5YeH7XH/PLFZt2pmpdSjMZ9IFw/n4iUeSlxcS7E6SJKn+DPRqFTbtKOO2h+cx4/V3a9RPH9qTO8aNoW/Xdgl1JkmS1DgGerV4j85azeT757B+265MrWv7Ar5x8UjGfqgvITgrL0mSclejA30IoTswFrgIGAP0BcqAWcBdwF0xxqraR2j4OCGE/sDS/Qz7pxjjx+r5kdRCrNtays33z+HR2Wtq1C8qPpQpl4yiZ6e2CXUmSZKUPdmYob8C+BmwGngWWAH0BsYBvwIuCCFcEWP1yw+zPs7bwH37qM+u96dRzosxMuP1d7nt4Xls3lmeqffq1JZbLx/NeaP6JNidJElSdmUj0C8ELgUerj6DHkK4AXgNGE8qlM9swnHeijFOacRnUAuxcuMObrh3Fi++s75G/Z+PO5wbLhpBl3YFCXUmSZLUNBod6GOMz9RSXxNC+DlwO3AGBwj02RpHrVNVVeR3ry5j+uML2FFWmakf3q0dU8cVc+rgHgl2J0mS1HSa+qLY3ec7VDTxOIeFEL4AdAc2AK/GGEvq8wYhhNdreWl4fcbRwbdo3VYmzpzF68s/yNRCgM+eMoBrzxtK+0Kv/ZYkSS1XkyWdEEIb4FPpbx9r4nE+mv6qvt1zwKdjjCsa+t5q3sorq/jFC0v4wVPvUFa553rpIb06Mm1CMccccUiC3UmSJB0cTTl1ORUYDTwSY3y8icbZAdxK6oLYJelaMTAFOBN4OoRwdIxx+4HeJMZ47L7q6Zn7YxrUuZrM7FWbuW5GCXNXb8nU2uQFvnzmYK46cxBt2+Qn2J0kSdLB0ySBPoRwNfA1YD7wyaYaJ8a4DvjGXuUXQgjnAi8BJwKfA37Q0B7UvJSWV/KDp9/hFy8sobJqzw2Pivt1Ydr4YkYc2jnB7iRJkg6+rAf6EMJVpAL0XODsGOPGgz1OjLEihPArUoH+NAz0LcJrSzcyaWYJS9bv+YVL2zZ5fO3cofzrqQNok5+XYHeSJEnJyGqgDyFcA3yP1P3fz07PoCc1zvvpZYeG9KDmY9uuCqY9Op/f/2V5jfqJA7oxbXwx/Xv4n1iSJLVeWQv0IYSJpM53fwv4aIxx/f63aNpxgJPSyyX7XUvN2rML1nHjPbN4b3NpptaxbRuuv3A4/3L8EeTlhQS7kyRJSl5WAn0IYTLwTeB14Nz9nR4TQigABgHlMcbFDR0nvf6JwJsxxrK96mcBX01/+4d6fhw1Ax9sL+PWh+Zyz5uratTPGt6L28eO5tAu7RLqTJIkqXlpdKAPIXyaVAivBF4Erg7hH2ZNl8UYf5v+c19gHrAc6N+IcQCmAaPSt6h8N10rBs5K/3lyjPGVhn0yJSHGyCOz1nDzA7NZv23Pz2ndOhRy8yUjufSow9jHcSFJktRqZWOGfkB6mQ9cU8s6zwO/bYJxfg+MBY4HLgAKgLXAn4EfxxhfPMB7qhlZu6WUyffN5om5a2vULz3qMG6+ZCTdO7ZNqDNJkqTmq9GBPsY4hdR93+u6/jLgH6ZY6ztOeptfA7+uzzZqfmKM/PnvK7nt4XlsLd3zMOA+nYu47fLRnDOyd4LdSZIkNW9N+WAp6YBWbNjB9feW8PKiDTXq/+/EI5h0wXA6FxUk1JkkSVJuMNArEZVVkd++soxvP76AneWVmfqR3dtz57gxnDKoR4LdSZIk5Q4DvQ66d9Zu5bqZJby5YlOmlhfgcx8ZyFfPGUq7wvzkmpMkScoxBnodNGUVVfz8+cX8+JlFlFVWZerDendi2oRijj68a3LNSZIk5SgDvQ6Kknc3cd2MEuav2ZqpFeQH/v3MIXzpjEEUtslLsDtJkqTcZaBXk9pZVsn3n1rIL19cQlXcUz/q8K5MH1/MsD6dkmtOkiSpBTDQq8n8ZckGJs0sYdmGHZlaUUEe1547jM+eOoD8PB8QJUmS1FgGemXd1tJypj46n//564oa9VMGdWfquGKO6N4+oc4kSZJaHgO9suqZ+Wu58d7ZrN5cmql1atuGGy8awT8ffzghOCsvSZKUTQZ6ZcWGbbv45kNzuf+t92rUzxnRm9suH02fLkUJdSZJktSyGejVKDFGHixZzZQH5rBxe1mm3r1DIVMuHcXFxYc6Ky9JktSEDPRqsDWbS7npvlk8NW9djfrYD/Vl8sUj6dahMKHOJEmSWg8Dveotxsjdf1vJHQ/PY+uuikz90C5F3DF2DGcO75Vgd5IkSa2LgV71snzDdibNnMWrSzbUqH/ipCOYeP5wOhUVJNSZJElS62SgV51UVkXuenkp335iAaXlVZn6gB4dmDpuDCcO7J5gd5IkSa2XgV4HtGDNVq6bWcLbKzdlankBrjxtIF89ZyhFBfnJNSdJktTKGehVq7KKKn7y7CJ++twiyitjpj68TyemTyimuF/X5JqTJEkSYKBXLd5auYnrZrzNwrXbMrXC/DyuPnswXzh9EAX5eQl2J0mSpN0M9KphZ1kl33liAb95eSlVeyblOeaIrkyfUMzgXp2Sa06SJEn/wECvjFcWr2fSzFms2LgjU2tXkM915w/jUyf3Jz/PB0RJkiQ1NwZ6saW0nDsfmccfX1tZo/7hwT24c9wYDu/WPqHOJEmSdCAG+lbuyblruem+WazdsitT61zUhpsuHskVx/YjBGflJUmSmjMDfSu1ftsupjwwh4dKVteonzeqN7deNppenYsS6kySJEn1YaBvZWKM3P/We9zy4Bw+2FGeqffo2JZbLxvFBWMOTbA7SZIk1ZeBvhV5b9NObrpvNs/MX1ejPv6Yfky+eARd2xcm1JkkSZIaykDfClRVRf73tRVMfXQ+23ZVZOp9u7bjjnFjOH1ozwS7kyRJUmMY6Fu4peu3M2lmCX9dujFTCwE+ddKRfP384XRs6yEgSZKUy0xzLVRFZRW/fmkp331yIbsqqjL1gT07MG18Mcf375Zgd5IkScoWA30LNPe9LUycWcKsVZsztfy8wBdOG8jVZw+hqCA/we4kSZKUTQb6FmRXRSU/fmYRP3tuMRVVMVMfeWhnpk8oZnTfLgl2J0mSpKZgoG8hXl/+ARNnlrBo3bZMrbBNHl85ewifP20gBfl5CXYnSZKkpmKgz3E7yir41uML+O0ry4h7JuU57shDmDq+mMG9OibXnCRJkpqcgT6HvfTOeibdU8K7H+zM1DoU5jPxguF84sQjycsLCXYnSZKkg8FAn4M27yjn9kfm8ue/v1ujftrQntwxdjT9DmmfUGeSJEk62Bp9YnUIoXsI4XMhhHtDCItCCDtDCJtDCC+FEP4thFCv9wgh9Ash/CaE8F4IYVcIYVkI4fshhEP2s80pIYRHQggbQwg7QgglIYRrQggt7nYuj81ewznfe75GmO/SroDvXHEU//3Z4w3zkiRJrUw2ZuivAH4GrAaeBVYAvYFxwK+AC0IIV8RY/QzvfQshDAJeAXoB9wPzgROArwDnhxBOjTFu2Guby4CZQCnwJ2AjcAnwPeDUdH857/2tu5jywBwenrW6Rv3CMX245dLR9OzUNqHOJEmSlKRsBPqFwKXAwzHGzBOMQgg3AK8B40mF+5l1GOunpML81THGH1Ub67vAV4HbgS9Wq3cGfglUAmfEGP+erk8GngEmhBA+FmO8u1GfMEExRu55YxXffGgum3eWZ+o9O7Xl1stGcf7oQxPsTpIkSUlr9Ck3McZnYowPVg/z6foa4Ofpb8840DghhIHAucAy4Cd7vXwzsB34ZAihQ7X6BKAncPfuMJ9+71LgpvS3X6rzh2lmVm3ayWfu+htf+7+3a4T5fzquH0999XTDvCRJkpr8otjdKbSiDuuelV4+sY8fDraGEF4mFfhPAp7ea5vH9jHeC8AO4JQQQtsY4656dZ6gqqrIH/66nGmPzmd7WWWm3u+QdkwdV8yHh/RIsDtJkiQ1J00W6EMIbYBPpb/dV+De27D0cmEtr79DKtAPZU+gr3WbGGNFCGEpMAoYCMw7QL+v1/LS8P1t1xS+//Q7/PDpdzLfhwCfOaU/1547jA5tvTGRJEmS9mjKx4dOBUYDj8QYH6/D+l3Sy821vL673rWR2zR7nzjpCLq0KwBgcK+OzPjiKdx8ySjDvCRJkv5BkyTEEMLVwNdI3aXmk9kaNr084N1yGrJNjPHYfQ6Qmrk/ph7v2Wi9OhUx5dKRLHl/O/9+1mDatmlxd9+UJElSlmQ90IcQrgJ+AMwFzo4xbqzjprtn07vU8nrnvdZr6DY5YeyH+iXdgiRJknJAVk+5CSFcA/wYmA2cmb7TTV0tSC+H1vL6kPSy+vnytW6TPod/AKkLcpfUow9JkiQpZ2Qt0IcQJpJ6mNNbpML8unoO8Wx6ee7eT5cNIXQi9ZConcBfqr30THp5/j7GOw1oD7ySS3e4kSRJkuojK4E+/SCnqcDrpE6zWb+fdQtCCMPTT4XNiDEuBp4A+gNX7bXZLUAH4Hcxxu3V6jOA9cDHQgjHVXuPIuC29Lc/a9CHkiRJknJAo8+hDyF8Gvgmqae1vghcHULYe7VlMcbfpv/cl9QtJJeTCu/VfRl4BfhhCOHs9HonAmeSOtXmxuorxxi3hBCuJBXsnwsh3A1sJPXk2mHp+p8a+xklSZKk5iobF8UOSC/zgWtqWed54LcHGijGuDg90/5NUqfRXAisBn4I3LKvC2xjjPeFEE4nFfbHA0XAIuA/gR/GGOtzVxxJkiQppzQ60McYpwBT6rH+MvbcTnJfr68EPlvPHl4mFf4lSZKkVqUpHywlSZIkqYkZ6CVJkqQcZqCXJEmScpiBXpIkScphBnpJkiQphxnoJUmSpBxmoJckSZJymIFekiRJymEGekmSJCmHGeglSZKkHBZijEn30KyFEDa0a9eu24gRI5JuRZIkSS3YvHnz2Llz58YYY/f6bGegP4AQwlKgM7DsIL/18PRy/kF+31zl/qo/91n9uL/qx/1VP+6v+nF/1Y/7q36S3F/9gS0xxgH12chA30yFEF4HiDEem3QvucD9VX/us/pxf9WP+6t+3F/14/6qH/dX/eTi/vIcekmSJCmHGeglSZKkHGaglyRJknKYgV6SJEnKYQZ6SZIkKYd5lxtJkiQphzlDL0mSJOUwA70kSZKUwwz0kiRJUg4z0EuSJEk5zEAvSZIk5TADvSRJkpTDDPSSJElSDjPQH0QhhH4hhN+EEN4LIewKISwLIXw/hHBIEuM0d9n4nOltYi1fa5qy/4MphDAhhPCjEMKLIYQt6c/3hwaO1eKPr2ztr9ZwfIUQuocQPhdCuDeEsCiEsDOEsDmE8FII4d9CCPX6d6SlH1/Z3F+t4fjaLYQwLYTwdAhhZXqfbQwhvBlCuDmE0L2eY7XoYwyyt79a0zFWXQjhk9U+5+fquW2zPL58sNRBEkIYBLwC9ALuB+YDJwBnAguAU2OMGw7WOM1dFvfXMqAr8P19vLwtxvjt7HScrBDCW8BRwDbgXWA48D8xxk/Uc5zWcny9RXb21zJa+PEVQvgi8DNgNfAssALoDYwDugAzgStiHf4xaQ3HV5b31zJa+PG1WwihDHgDmAusAzoAJwHHAe8BJ8UYV9ZhnBZ/jEFW99cyWskxtlsI4XBgFpAPdASujDH+qo7bNt/jK8bo10H4Ah4HIvAfe9W/m67//GCO09y/sri/lgHLkv48B2F/nQkMAQJwRnof/SGp/d7cv7K4v1r88QWcBVwC5O1V70MqrEZgfB3HavHHV5b3V4s/vqp91qJa6ren99lP6zhOiz/Gsry/Ws0xlv68AXgKWAx8K72vPleP7Zvt8ZX4zm0NX8DA9H/opfv4S74TqVnC7UCHgzFOc//K5udsbX9ZpT9zgwJqazm+srW/0tu2uuNrr89/Q3rf/agO67bK46uh+yu9fqs+vtL74Kj0PnuyDut6jNVjf6XXb1XHGPAVoAo4DZhSn0Df3I8vz6E/OM5KL5+IMVZVfyHGuBV4GWhP6tdlB2Oc5i7bn7NtCOETIYQbQghfCSGcGULIz2K/LUVrOb6yrTUfX+XpZUUd1vX4qt/+2q01H1+Q+m0HQEkd1vUYq9/+2q1VHGMhhBHAVOAHMcYXGjBEsz6+2iTxpq3QsPRyYS2vvwOcCwwFnj4I4zR32f6cfYDf71VbGkL4bIzx+Ya12CK1luMr21rl8RVCaAN8Kv3tY3XYpFUfXw3YX7u1quMrhHAtqfOau5A6H/zDpMLp1Dps3uqOsUbur91a/DGW/v/v96ROe7uhgcM06+PLGfqDo0t6ubmW13fXux6kcZq7bH7Ou4CzSf2F1QEYA/wX0B94NIRwVIO7bHlay/GVTa35+JoKjAYeiTE+Xof1W/vxVd/9Ba3z+LoWuBm4hlQ4fQw4N8b4fh22bY3HWGP2F7SeY+wbwIeAz8QYdzZwjGZ9fBnom4eQXjb2lkPZGqe5q/PnjDHeEmN8Jsa4Nsa4I8Y4O8b4RVIXsLQjdQ6d6qa1HF911lqPrxDC1cDXSN3h4ZPZGja9bHHHV0P3V2s8vmKMfWKMgVTAHEfqvOU3QwjHZGH4FneMNXZ/tYZjLIRwAqlZ+e/EGF9tyrdKLxM5vgz0B8fun9q61PJ6573Wa+pxmruD8Tl/nl6e1ogxWprWcnwdDC32+AohXAX8gNTt8s6MMW6s46at8vhqxP7anxZ7fO2WDpj3kjqFoTvwuzps1iqPMWjw/tqfFnGMVTvVZiEwuZHDNevjy0B/cCxIL4fW8vqQ9LK287KyPU5zdzA+57r0skMjxmhpWsvxdTC0yOMrhHAN8GNgNqlwWp8Hz7S646uR+2t/WuTxtS8xxuWkfhgaFULocYDVW90xtrd67q/9aSnHWEdSx8MIoLT6g7NInaoE8Mt07fsHGKtZH19eFHtwPJtenhtCyKt+dXQIoRNwKrAT+MtBGqe5Oxif8+T0ckkjxmhpWsvxdTC0uOMrhDCR1HngbwEfjTGur+cQrer4ysL+2p8Wd3wdwGHpZeUB1mtVx9h+1HV/7U9LOcZ2Ab+u5bVjSJ1X/xKpsH6g03Ga9fHlDP1BEGNcDDxB6iKTq/Z6+RZSPwH/Lsa4HSCEUBBCGJ5+IlmDx8lV2dpfIYRRIYRue48fQjiS1KwZwB+y3H6z19qPr/ry+IIQwmRS4fR14Oz9hVOPr+zsr1Z2fA0PIfTZRz0vhHA7qadyvhJj/CBdb9XHWLb2V2s4xmKMO2OMn9vXF/BAerX/Ttf+BLl7fIUYW8y1Ic3aPh4XPA84kdQTKxcCp8T044JDCP1JPbhgeYyxf0PHyWXZ2F8hhCnAJFI/VS8FtgKDgIuAIuARYGyMsexgfKamFEK4HLg8/W0f4DxSMysvpmvrY4zXptftj8fX5TRyf7WW4yuE8Gngt6Rm+37Evs8PXRZj/G16/f604uMrW/urtRxfkDk16VvAC6Se4LkB6A2cTuoizzWkfjCam16/P637GLuGLOyv1nSM7Uv6898MXBlj/FW1en9y8fiKzeDJXa3lCzic1C2iVgNlwHJSF0t122u9/qSukl7WmHFy/aux+4vUX25/JHVniU2kHuryPvAkqftBh6Q/Yxb31ZT0Pqjta1m1dVv98ZWN/dVajq867KsIPOfxld391VqOr/RnHQ38hNTpSetJPXhrM/C39P7038gm2F+t6RirZT/u/n/1c3vVc/L4coZekiRJymGeQy9JkiTlMAO9JEmSlMMM9JIkSVIOM9BLkiRJOcxAL0mSJOUwA70kSZKUwwz0kiRJUg4z0EuSJEk5zEAvSZIk5TADvSRJkpTDDPSSJElSDjPQS5IkSTnMQC9JkiTlMAO9JEmSlMMM9JIkSVIOM9BLkhothHBfCCGGEP5jH6/dmn7tV0n0JkktXYgxJt2DJCnHhRC6AW8CvYGTY4xvputnA08A84HjY4w7kutSklomA70kKStCCKcAzwNLgWOA9sDbQBdSYX5Ogu1JUovlKTeSpKyIMb4CTAaGAP8F/AHoA1xtmJekpuMMvSQpa0IIAXgUOC9d+mOM8f8l2JIktXjO0EuSsiamZonurVb6fkKtSFKr4Qy9JClrQghDgDeAclLnzs8BTogxlibamCS1YM7QS5KyIoTQFvgT0AH4GHAnMAZn6SWpSRnoJUnZ8m3gQ8D0GOMTwM3Ay8AXQgj/lGhnktSCecqNJKnRQgiXkzp3/q/Ah2OMFen64cBbQBvgQzHGJUn1KEktlYFektQoIYQjSIX2PFKhfeler18G3Af8jVTYLzvYPUpSS2aglyRJknKY59BLkiRJOcxAL0mSJOUwA70kSZKUwwz0kiRJUg4z0EuSJEk5zEAvSZIk5TADvSRJkpTDDPSSJElSDjPQS5IkSTnMQC9JkiTlMAO9JEmSlMMM9JIkSVIOM9BLkiRJOcxAL0mSJOUwA70kSZKUwwz0kiRJUg4z0EuSJEk57P8Dm/W+2Pz4J6wAAAAASUVORK5CYII=\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "image/png": { - "height": 261, - "width": 378 - }, - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "da_gpu.plot()" - ] - }, - { - "cell_type": "markdown", - "id": "cf09471d-6df5-4bd4-b78d-3ab5ffefc371", - "metadata": {}, - "source": [ - "## Apply custom kernels with apply_ufunc\n", - "\n", - "(This kernel was copied from [this NCAR tutorial](https://github.com/NCAR/GPU_workshop/blob/workshop/12_CuPyAndLegate/12_CuPyAndLegate.ipynb))" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "50389d84-49e6-44a9-9bd8-679457752280", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([[ 0., 2., 4.],\n", - " [ 0., 4., 10.]], dtype=float32)" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "x = cp.arange(6, dtype=\"f\").reshape(2, 3)\n", - "y = cp.arange(3, dtype=\"f\")\n", - "\n", - "kernel = cp.ElementwiseKernel(\n", - " \"float32 x, float32 y\",\n", - " \"float32 z\",\n", - " \"\"\"\n", - " if (x - 2 > y) {\n", - " z = x * y;\n", - " } else {\n", - " z = x + y;\n", - " }\n", - " \"\"\",\n", - " \"my_kernel\",\n", - ")\n", - "\n", - "kernel(x, y)" - ] - }, - { - "cell_type": "markdown", - "id": "da96291a-28b3-468d-abdb-98392c050e00", - "metadata": {}, - "source": [ - "We can apply these and other custom kernels using `xarray.apply_ufunc`" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "4fdc7331-0e1a-415c-a2c0-ca172e9e2573", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "is_gpu: True\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray (a: 2, b: 3)>\n",
-       "array([[ 0.,  2.,  4.],\n",
-       "       [ 0.,  4., 10.]], dtype=float32)\n",
-       "Dimensions without coordinates: a, b
" - ], - "text/plain": [ - "\n", - "array([[ 0., 2., 4.],\n", - " [ 0., 4., 10.]], dtype=float32)\n", - "Dimensions without coordinates: a, b" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "xda = xr.DataArray(x, dims=(\"a\", \"b\"))\n", - "yda = xr.DataArray(y, dims=(\"b\"))\n", - "result = xr.apply_ufunc(\n", - " kernel,\n", - " xda,\n", - " yda,\n", - ")\n", - "print(\"is_gpu:\", result.cupy.is_cupy)\n", - "result" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:miniconda3-gpu]", - "language": "python", - "name": "conda-env-miniconda3-gpu-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.5" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/source/apply-ufunc.ipynb b/docs/source/apply-ufunc.ipynb new file mode 100644 index 0000000..1bb5d46 --- /dev/null +++ b/docs/source/apply-ufunc.ipynb @@ -0,0 +1,578 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "93434031-d7fe-4322-a9cf-41e5b8be622d", + "metadata": {}, + "source": [ + "# Custom Kernels with `apply_ufunc`" + ] + }, + { + "cell_type": "markdown", + "id": "4c0164c9-f970-4206-964a-d1d8080e2b0a", + "metadata": {}, + "source": [ + "## Overview\n", + "### In this tutorial, you learn:\n", + "\n", + "* What `apply_ufunc` is and its importance in the xarray Python library.\n", + "* The basic usage of `apply_ufunc` to apply your function to a DataArray.\n", + "* Applying custom kernels to DataArray with CuPy\n", + "\n", + "## Prerequisites\n", + "\n", + "| Concepts | Importance | Notes |\n", + "| --- | --- | --- |\n", + "| [Basics of Cupy](Notebook0_Introduction) | Necessary | |\n", + "| [Familiarity with Xarray](https://foundations.projectpythia.org/core/xarray.html) | Necessary | |\n", + "\n", + "- **Time to learn**: 20 minutes\n", + "\n", + "\n", + "\n", + "## What is `apply_ufunc`? \n", + "\n", + "`apply_ufunc` is a powerful function provided by the xarray library, which is commonly used for data manipulation in the Python programming language. This function allows users to apply universal functions (ufuncs) on xarray data structures, including DataArray, Dataset, or Variable objects. With `apply_ufunc`, users can apply arbitrary functions that are compatible with raw arrays (e.g. NumPy or CuPy), and the function will take care of aligning the input data, looping over dimensions, and maintaining metadata.\n", + "\n", + "```{seealso}\n", + "See the [Xarray tutorial material on apply_ufunc](https://tutorial.xarray.dev/advanced/apply_ufunc/simple_numpy_apply_ufunc.html) for more.\n", + "```\n", + "\n", + "\n", + "Simple functions that act independently on each value should work without any additional arguments.\n", + "\n", + "### Simple Example \n", + "\n", + "In the example below, we calculate the saturation vapor pressure by using `apply_ufunc()`.\n", + "\n", + "But first, let's create some sample data to work with.\n", + "\n", + "We'll use a 3-dimensional dataset (time, latitude, longitude) with random values:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "ed7a50f8-a9f1-4a32-b864-389a6be2f4fa", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "import cupy as cp\n", + "import cupy_xarray # Adds .cupy to Xarray objects\n", + "import numpy as np\n", + "import pandas as pd\n", + "import xarray as xr" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "5f6f83a3-36d6-493d-bfda-49f5d766ef14", + "metadata": {}, + "outputs": [], + "source": [ + "np.random.seed(0)\n", + "\n", + "# Create the time range.\n", + "date = pd.date_range(\"2010-01-01\", \"2020-12-31\", freq=\"M\")\n", + "\n", + "# Create the latitude range.\n", + "lat = np.arange(-90, 90, 1)\n", + "\n", + "# Create the longitude range.\n", + "lon = np.arange(-180, 180, 1)\n", + "\n", + "# Create random data\n", + "data_np = np.random.rand(len(date), len(lat), len(lon))\n", + "data_cp = cp.array(data_np)\n", + "\n", + "# -- Create DataArray with Numpy data\n", + "data_xr_np = xr.DataArray(\n", + " data_np,\n", + " dims=[\"time\", \"lat\", \"lon\"],\n", + " coords=[date, lat, lon],\n", + ")\n", + "\n", + "# -- Create DataArray with CuPy data\n", + "data_xr_cp = xr.DataArray(\n", + " data_cp,\n", + " dims=[\"time\", \"lat\", \"lon\"],\n", + " coords=[date, lat, lon],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "94eb3898-97a9-419d-a158-158e92ca7c61", + "metadata": {}, + "source": [ + "Now, let's define our function that calculate the saturation vapor pressure using Clausius-Clapeyron equation:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "b11788f6-af4f-4cc9-8bd8-662d57fbf2a2", + "metadata": {}, + "outputs": [], + "source": [ + "def sat_p(t):\n", + " # return saturation vapor pressure\n", + " # using Clausius-Clapeyron equation\n", + " return 0.611 * np.exp(17.67 * (t - 273.15) * ((t - 29.65) ** (-1)))" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "e1c0b119-23f9-4397-957a-ace2a607ab6f", + "metadata": {}, + "outputs": [], + "source": [ + "start_time_np = time.time()\n", + "\n", + "es_np = xr.apply_ufunc(sat_p, data_xr_np)\n", + "\n", + "end_time_np = time.time()\n", + "time_np = end_time_np - start_time_np" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "53419533-005b-4ceb-9ae5-2deaa316c52a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "numpy.ndarray" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(es_np.data)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "973e4b7e-1b2f-4fbb-a4da-a7a47b1be327", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "GroupBy with Xarray DataArrays using CuPy provides a 0.22 x speedup over NumPy.\n", + "\n" + ] + } + ], + "source": [ + "start_time_cp = time.time()\n", + "\n", + "es_cp = xr.apply_ufunc(sat_p, data_xr_cp)\n", + "\n", + "end_time_cp = time.time()\n", + "time_cp = end_time_cp - start_time_cp\n", + "\n", + "print(\n", + " \"apply_ufunc with Xarray DataArrays using CuPy provides a\",\n", + " round(time_np / time_cp, 2),\n", + " \"x speedup over NumPy.\\n\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d8c387d9-8c95-4559-a1a7-76c02219f1a9", + "metadata": {}, + "source": [ + "Now, what is the output type? Does `apply_ufunc` preserve the underlying data type?" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "a74d9141-c219-4341-93c3-bd4b1abbd80c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "cupy.ndarray" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(es_cp.data)" + ] + }, + { + "cell_type": "markdown", + "id": "5ca7bf10-7919-41ad-acd8-5bc617b627ba", + "metadata": {}, + "source": [ + "```{important}\n", + "`apply_ufunc` preserves the underlying data type.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "6a1bdc2b-1d49-4204-8668-1cff5c521379", + "metadata": {}, + "source": [ + "In the timing test, you might notice not much speed-up when using CuPy. But let's run this cell another time: " + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "665125da-3082-4f5d-994c-41d2eac8cca6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "GroupBy with Xarray DataArrays using CuPy provides a 97.87 x speedup over NumPy.\n", + "\n" + ] + } + ], + "source": [ + "start_time_cp = time.time()\n", + "\n", + "es_cp = xr.apply_ufunc(sat_p, data_xr_cp)\n", + "\n", + "end_time_cp = time.time()\n", + "time_cp = end_time_cp - start_time_cp\n", + "\n", + "print(\n", + " \"apply_ufunc with Xarray DataArrays using CuPy provides a\",\n", + " round(time_np / time_cp, 2),\n", + " \"x speedup over NumPy.\\n\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f9ac9b58-8d04-444c-9f25-b4b098a48848", + "metadata": {}, + "source": [ + "Now, we can see much more speed-up using CuPy as explained in the first lesson: \n", + "\n", + "> When running these functions for the first time, you may experience a brief pause. This occurs as CuPy compiles the CUDA functions for the first time and cached them on disk for future use." + ] + }, + { + "cell_type": "markdown", + "id": "bc3c4f8c-2077-4cdd-b86d-3a6643e38a4d", + "metadata": {}, + "source": [ + "## Elementwise Kernel with CuPy\n", + "\n", + "Elementwise Kernels in CuPy allow for operations to be performed on an element-by-element basis on CuPy arrays.\n", + "\n", + "To create an elementwise kernel in CuPy, you need to use `cupy.ElementwiseKernel` class. This class defines a CUDA kernel which can be invoked by the `__call__` method of the instance. \n", + "\n", + "This elementwise kernel takes three arguments: \n", + "* a string defining the input type(s), \n", + "* a string defining the output type(s), \n", + "* and a string representing the operation to be performed, written in C syntax." + ] + }, + { + "cell_type": "markdown", + "id": "0fdf747a-cfda-4836-b69b-9abafb3842a1", + "metadata": {}, + "source": [ + "In this example, we want to calculate Relative Humidity using Revised Magnus coefficients by Alduchov and Eskridge." + ] + }, + { + "cell_type": "markdown", + "id": "cc0b24b1-3143-4fcc-81e5-d119af100bd0", + "metadata": {}, + "source": [ + "Revised Magnus Coefficients by Alduchov and Eskridge:\n", + "$$\n", + "RH = \\left(\\frac{{6.112 \\cdot \\exp\\left(\\frac{{17.67 \\cdot (T_d - 273.15)}}{{T_d - 29.65}}\\right)}}{{6.112 \\cdot \\exp\\left(\\frac{{17.67 \\cdot (T - 273.15)}}{{T - 29.65}}\\right)}}\\right) \\times 100 \\%\n", + "$$\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "a5727614-4289-45e0-b5fb-42997de34d61", + "metadata": {}, + "outputs": [], + "source": [ + "def calculate_relative_humidity(temp, dew_point):\n", + " \"\"\"\n", + " Calculate Relative Humidity using Revised Magnus coefficients by Alduchov and Eskridge.\n", + "\n", + " Args:\n", + " temp (float): Temperature in Celsius.\n", + " dew_point (float): Dew Point Temperature in Celsius.\n", + "\n", + " Returns:\n", + " float: Relative Humidity in percentage.\n", + " \"\"\"\n", + " temp += 273.15 # Convert temperature to Kelvin\n", + " dew_point += 273.15 # Convert dew point temperature to Kelvin\n", + "\n", + " es_temp = 6.112 * np.exp(\n", + " (17.67 * (dew_point - 273.15)) / (dew_point - 29.65)\n", + " ) # Saturation vapor pressure at dew point\n", + " es_dew = 6.112 * np.exp(\n", + " (17.67 * (temp - 273.15)) / (temp - 29.65)\n", + " ) # Saturation vapor pressure at temperature\n", + "\n", + " relative_humidity = (es_dew / es_temp) * 100.0 # Calculate relative humidity in percentage\n", + " return relative_humidity" + ] + }, + { + "cell_type": "markdown", + "id": "26d10b0a-1462-4fd9-9e53-5da2f36cd952", + "metadata": {}, + "source": [ + "But for `Elementwise` kernels we need to write it in C syntax:" + ] + }, + { + "cell_type": "markdown", + "id": "62a5996b-1a3d-443b-aa73-2594c52f96ba", + "metadata": {}, + "source": [ + "1. Set the list of input and output arguments and their data types: \n", + "\n", + " * input arguments : `float32 temp`, `float32 d_temp`\n", + " * output arguments : `float32 rh`\n", + "\n", + "2. Write the code body: \n", + "``` C\n", + " temp += 273.15;\n", + " dew_point += 273.15;\n", + "\n", + " // Calculate saturation vapor pressure at dew point\n", + " float es_temp = 6.112 * exp((17.67 * (dew_point - 273.15)) / (dew_point - 29.65));\n", + "\n", + " // Calculate saturation vapor pressure at temperature\n", + " float es_dew = 6.112 * exp((17.67 * (temp - 273.15)) / (temp - 29.65));\n", + "\n", + " // Calculate relative humidity in percentage\n", + " float relative_humidity = (es_dew / es_temp) * 100.0;\n", + " \n", + "```\n", + "\n", + "3. Define the element-wise class and set the kernel name: \n", + "\n", + "```\n", + " compute_call = cp.ElementwiseKernel(input_list, output_list, code_body, 'RH')\n", + "\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "id": "8289b506-5905-4b8d-b099-7d45dd819584", + "metadata": {}, + "source": [ + "Now let's test to see how this works in a real example: " + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "id": "24f7fdbe-4c71-40b3-84af-564fd987f96b", + "metadata": {}, + "outputs": [], + "source": [ + "# Create random data\n", + "data_cp = 20 * (cp.random.rand(len(date), len(lat), len(lon)))\n", + "\n", + "\n", + "# -- Create Temp DataArray with CuPy data\n", + "temp = xr.DataArray(\n", + " data_cp,\n", + " dims=[\"time\", \"lat\", \"lon\"],\n", + " coords=[date, lat, lon],\n", + ")\n", + "\n", + "\n", + "offset = 20 * cp.random.rand(len(date), len(lat), len(lon))\n", + "\n", + "# -- Create Wet Bulb Temp DataArray with CuPy data\n", + "\n", + "temp_wet = xr.DataArray(\n", + " data_cp - offset,\n", + " dims=[\"time\", \"lat\", \"lon\"],\n", + " coords=[date, lat, lon],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 89, + "id": "1fe6bf87-8ea2-4df6-8033-1c6a8debfd85", + "metadata": {}, + "outputs": [], + "source": [ + "input_list = \"float64 temp, float64 dew_temp\"\n", + "output_list = \"float64 rh\"\n", + "\n", + "code_body = \"\"\"\n", + "\n", + " // Calculate saturation vapor pressure at dew point\n", + " float es_temp = 6.112 * exp((17.67 * (dew_temp)) / (dew_temp - 29.65));\n", + "\n", + " // Calculate saturation vapor pressure at temperature\n", + " float es_dew = 6.112 * exp((17.67 * (temp)) / (temp - 29.65));\n", + "\n", + " // Calculate relative humidity in percentage\n", + " rh = (es_dew / es_temp) * 100.0;\n", + " \"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 90, + "id": "6b5cc07c-0de3-4f87-874b-6c2fb82e6c46", + "metadata": {}, + "outputs": [], + "source": [ + "## -- define the elementwise kernel:\n", + "compute_call = cp.ElementwiseKernel(input_list, output_list, code_body, \"RH\")\n", + "\n", + "kernel = compute_call(data_cp, data_cp - offset)" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "id": "3e794157-cd72-459d-a9a7-8818c9ed6b5b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.24 ms, sys: 0 ns, total: 1.24 ms\n", + "Wall time: 1.24 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "result = xr.apply_ufunc(\n", + " compute_call,\n", + " temp,\n", + " temp_wet,\n", + ")\n", + "##result" + ] + }, + { + "cell_type": "markdown", + "id": "e1332e6e-ab34-4859-8b4b-9265eda4572d", + "metadata": {}, + "source": [ + "How much this computation took if we wanted to use pure Python?" + ] + }, + { + "cell_type": "code", + "execution_count": 100, + "id": "0030fec5-41fa-41bb-983b-b30ccc2ff734", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 3.97 ms, sys: 0 ns, total: 3.97 ms\n", + "Wall time: 3.98 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "relative_humidity = calculate_relative_humidity(temp, temp_wet)" + ] + }, + { + "cell_type": "markdown", + "id": "2c6da1f0-48f2-473b-a09f-51869611c679", + "metadata": {}, + "source": [ + "We can see using the custom kernel method, we removed the pure Python overhead in between calculations, by creating a custom \"elementwise\" kernel that will run the entire computations on the GPU device. " + ] + }, + { + "cell_type": "markdown", + "id": "95dccf03-bb64-43ef-883e-9b84081c4514", + "metadata": {}, + "source": [ + "Congratulations! You have now uncovered how to use `apply_ufunc` with custom CUDA kernels. \n", + "\n", + "## Summary\n", + "\n", + "In this notebook, we have learned about:\n", + "\n", + "* What `apply_ufunc` is and its importance in the xarray Python library.\n", + "* The basic usage of `apply_ufunc` to apply your function to a DataArray.\n", + "* Applying custom kernels to DataArray with CuPy\n", + "\n", + "```{seealso}\n", + "[Xarray apply_ufunc](https://docs.xarray.dev/en/stable/generated/xarray.apply_ufunc.html)\n", + "[CuPy User Guide](https://docs.cupy.dev/en/stable/user_guide/index.html) \n", + "[Xarray User Guide](https://docs.xarray.dev/en/stable/user-guide/index.html) \n", + "[Cupy-Xarray Github](https://github.com/xarray-contrib/cupy-xarray.git)\n", + "```" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/basic-computations.ipynb b/docs/source/basic-computations.ipynb new file mode 100644 index 0000000..c6b2545 --- /dev/null +++ b/docs/source/basic-computations.ipynb @@ -0,0 +1,3698 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0d6fecdf-48c0-4745-b802-2117fb3137cf", + "metadata": {}, + "source": [ + "# Basic Computations" + ] + }, + { + "cell_type": "markdown", + "id": "15a05d43-0bf5-48d3-9c88-6074eed82a04", + "metadata": {}, + "source": [ + "## Overview\n", + "### In this tutorial, you learn:\n", + "\n", + "* Applying basic arithmetic and NumPy functions to xarray DataArrays with CuPy.\n", + "* Perform operations across multiple datasets\n", + "* Understand two important concepts: broadcasting and alignment.\n", + "* Performance of Xarray using Cupy vs. Numpy on different array sizes. \n", + "\n", + "## Prerequisites\n", + "\n", + "| Concepts | Importance | Notes |\n", + "| --- | --- | --- |\n", + "| [Familiarity with NumPy](https://foundations.projectpythia.org/core/numpy.html) | Necessary | |\n", + "| [Basics of Cupy](Notebook0_Introduction) | Necessary | |\n", + "| [Familiarity with Xarray](https://foundations.projectpythia.org/core/xarray.html) | Necessary | |\n", + "\n", + "- **Time to learn**: 40 minutes\n", + "\n", + "\n", + "## Introduction " + ] + }, + { + "cell_type": "markdown", + "id": "77343efb-de6d-423c-b1cd-934c5d6d68e1", + "metadata": {}, + "source": [ + "First, let's import our packages\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "55c72b7d-8899-4e2f-9432-e9cf1531cbdf", + "metadata": {}, + "outputs": [], + "source": [ + "## Import NumPy and CuPy\n", + "import cupy as cp\n", + "import numpy as np\n", + "import xarray as xr\n", + "import cupy_xarray # Adds .cupy to Xarray objects" + ] + }, + { + "cell_type": "markdown", + "id": "4ed42841-264a-4eb6-9f82-be9a463be816", + "metadata": {}, + "source": [ + "### Creating Xarray DataArray with CuPy" + ] + }, + { + "cell_type": "markdown", + "id": "573bb115-0e77-4f86-be9f-9ee6ac1c6f9b", + "metadata": {}, + "source": [ + "In the previous tutorial, we learned how to create a DataArray that wraps a CuPy array:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4b91fc01-9e99-4b01-b700-9b4802b7ef14", + "metadata": {}, + "outputs": [], + "source": [ + "arr_gpu = cp.random.rand(10, 10)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "431edeb0-661f-4929-a83d-e39a6e753a60", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray (x: 10, y: 10)>\n",
+       "array([[0.64440645, 0.62072123, 0.75168547, 0.41128605, 0.88459028,\n",
+       "        0.47016308, 0.86304331, 0.92990986, 0.0041129 , 0.4666957 ],\n",
+       "       [0.56647797, 0.11373418, 0.62628122, 0.78959584, 0.36494045,\n",
+       "        0.13310425, 0.73672578, 0.86921365, 0.05596426, 0.55426342],\n",
+       "       [0.4720759 , 0.6481852 , 0.46598961, 0.93751977, 0.97099829,\n",
+       "        0.94932666, 0.54603983, 0.29783205, 0.36190421, 0.44288443],\n",
+       "       [0.62394009, 0.14474529, 0.36714822, 0.30050983, 0.44310121,\n",
+       "        0.45300226, 0.84836414, 0.41480516, 0.15972742, 0.30865762],\n",
+       "       [0.17974085, 0.43178982, 0.68688623, 0.2870211 , 0.94622374,\n",
+       "        0.05305575, 0.10551911, 0.50202377, 0.32414185, 0.52343633],\n",
+       "       [0.57433335, 0.55480641, 0.65053659, 0.84821379, 0.86448478,\n",
+       "        0.4614566 , 0.41249327, 0.04641715, 0.9086778 , 0.55099052],\n",
+       "       [0.99359918, 0.19577754, 0.42470934, 0.20198499, 0.49022272,\n",
+       "        0.56950438, 0.55683842, 0.81856686, 0.97131091, 0.73117734],\n",
+       "       [0.05195378, 0.09355582, 0.23061675, 0.48168679, 0.20765511,\n",
+       "        0.44548051, 0.54251798, 0.63568233, 0.61946882, 0.48324004],\n",
+       "       [0.89803925, 0.89935711, 0.57733868, 0.21010146, 0.15491007,\n",
+       "        0.27044434, 0.14652858, 0.35991027, 0.87969536, 0.57918609],\n",
+       "       [0.31083571, 0.29447116, 0.06544057, 0.46585981, 0.0189647 ,\n",
+       "        0.08291839, 0.16705158, 0.53118993, 0.99264236, 0.75636455]])\n",
+       "Dimensions without coordinates: x, y
" + ], + "text/plain": [ + "\n", + "array([[0.64440645, 0.62072123, 0.75168547, 0.41128605, 0.88459028,\n", + " 0.47016308, 0.86304331, 0.92990986, 0.0041129 , 0.4666957 ],\n", + " [0.56647797, 0.11373418, 0.62628122, 0.78959584, 0.36494045,\n", + " 0.13310425, 0.73672578, 0.86921365, 0.05596426, 0.55426342],\n", + " [0.4720759 , 0.6481852 , 0.46598961, 0.93751977, 0.97099829,\n", + " 0.94932666, 0.54603983, 0.29783205, 0.36190421, 0.44288443],\n", + " [0.62394009, 0.14474529, 0.36714822, 0.30050983, 0.44310121,\n", + " 0.45300226, 0.84836414, 0.41480516, 0.15972742, 0.30865762],\n", + " [0.17974085, 0.43178982, 0.68688623, 0.2870211 , 0.94622374,\n", + " 0.05305575, 0.10551911, 0.50202377, 0.32414185, 0.52343633],\n", + " [0.57433335, 0.55480641, 0.65053659, 0.84821379, 0.86448478,\n", + " 0.4614566 , 0.41249327, 0.04641715, 0.9086778 , 0.55099052],\n", + " [0.99359918, 0.19577754, 0.42470934, 0.20198499, 0.49022272,\n", + " 0.56950438, 0.55683842, 0.81856686, 0.97131091, 0.73117734],\n", + " [0.05195378, 0.09355582, 0.23061675, 0.48168679, 0.20765511,\n", + " 0.44548051, 0.54251798, 0.63568233, 0.61946882, 0.48324004],\n", + " [0.89803925, 0.89935711, 0.57733868, 0.21010146, 0.15491007,\n", + " 0.27044434, 0.14652858, 0.35991027, 0.87969536, 0.57918609],\n", + " [0.31083571, 0.29447116, 0.06544057, 0.46585981, 0.0189647 ,\n", + " 0.08291839, 0.16705158, 0.53118993, 0.99264236, 0.75636455]])\n", + "Dimensions without coordinates: x, y" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "da_cp = xr.DataArray(arr_gpu, dims=[\"x\", \"y\"])\n", + "\n", + "da_cp" + ] + }, + { + "cell_type": "markdown", + "id": "62af1f7c-0ac2-4bad-ab92-8f1bcfbaffe3", + "metadata": {}, + "source": [ + "## Basic Operations with Xarray and CuPy" + ] + }, + { + "cell_type": "markdown", + "id": "ff85bc62-de58-4859-80d2-433fc6af4dcf", + "metadata": {}, + "source": [ + "### Basic Arithmetic" + ] + }, + { + "cell_type": "markdown", + "id": "5e700f37-3962-4aeb-86ae-7465e0549e1a", + "metadata": {}, + "source": [ + "Xarray data arrays and datasets are compatible with arithmetic operators and numpy array functions, making it easy to work with arithmetic operators.\n" + ] + }, + { + "cell_type": "markdown", + "id": "d8ffedc1-51e3-4a9f-8925-b98650133a44", + "metadata": {}, + "source": [ + "Once we have created a DataArray using CuPy, we can perform various operations on it using the familiar Xarray syntax. For example:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "2adc12d1-d213-454d-892a-b26e6e10b581", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "result_cp = da_cp * 2 + 200\n", + "print(type(result_cp.data))" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "61cf4ce6-1f2d-40bb-8933-aebc5b850f38", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "da_np = da_cp.as_numpy()\n", + "result_np = da_np * 2 + 200\n", + "print(type(result_np.data))" + ] + }, + { + "cell_type": "markdown", + "id": "4a23ebf6-ee17-48de-8474-83dcfe863b18", + "metadata": {}, + "source": [ + "### Reductions\n", + "\n", + "We can use similar statistical functions as the NumPy equivalants here. For a complete list of statistical functions, please visit [the API reference](https://docs.cupy.dev/en/v8.6.0/reference/statistics.html)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "d812cb7b-8a9e-40d3-b2eb-9ed051554db9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.24 ms, sys: 98 µs, total: 1.34 ms\n", + "Wall time: 1.35 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "# calculate the mean along the x dimension\n", + "mean_cp = da_cp.mean(dim=\"x\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "ea6636c6-d666-4d26-aa38-cea7fbf1a090", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "print(type(mean_cp.data))" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "340de30b-dac8-424f-a1db-9ee0a80bce18", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 191 µs, sys: 75 µs, total: 266 µs\n", + "Wall time: 270 µs\n" + ] + } + ], + "source": [ + "%%time\n", + "# calculate the mean along the x dimension\n", + "mean_np = da_np.mean(dim=\"x\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "c65514eb-b61e-4b1c-93c8-9a6d30536177", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "print(type(mean_np.data))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "35bc6495-54bf-40a8-9417-5852f0f8731b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 2.22 ms, sys: 909 µs, total: 3.12 ms\n", + "Wall time: 3.15 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "# calculate the standard deviation along the x and y dimensions\n", + "std_cp = da_cp.std(dim=[\"x\", \"y\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "c4c05ec3-68f1-4106-8ee3-587014eaf40e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "print(type(std_cp.data))" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "25e128ad-0b55-4528-a273-a80ecb1f5bd4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 215 µs, sys: 85 µs, total: 300 µs\n", + "Wall time: 304 µs\n" + ] + } + ], + "source": [ + "%%time\n", + "# calculate the standard deviation along the x and y dimensions\n", + "std_np = da_np.std(dim=[\"x\", \"y\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "c9b131b8-2aa9-4f54-bcea-19ee587a28d3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "print(type(std_cp.data))" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "c1c09656-6238-4c56-a649-56cca44370b0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 3.85 ms, sys: 942 µs, total: 4.79 ms\n", + "Wall time: 4.83 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "cupy.ndarray" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "# calculate the median along the x dimension\n", + "med_cp = da_cp.median(dim=[\"x\"])\n", + "type(med_cp.data)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "4e769ea7-afb0-43f0-9507-4d142d4a9987", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 113 µs, sys: 44 µs, total: 157 µs\n", + "Wall time: 160 µs\n" + ] + }, + { + "data": { + "text/plain": [ + "numpy.ndarray" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "# calculate the median along the x dimension\n", + "med_np = da_np.median(dim=[\"x\"])\n", + "type(med_np.data)" + ] + }, + { + "cell_type": "markdown", + "id": "30425020-0828-4f7a-b9bb-7ade36ceae20", + "metadata": {}, + "source": [ + "Similarly we use statical functions to find order statistics:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "0ee3c022-1a9c-4464-b728-be9f60f8bbfa", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 2.24 ms, sys: 94 µs, total: 2.33 ms\n", + "Wall time: 2.33 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "cupy.ndarray" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "# calculate the minimum along all dimensions\n", + "min_cp = da_cp.min()\n", + "type(min_cp.data)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "0df6c10f-335d-45d0-b0d8-40142b519bd9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 104 µs, sys: 0 ns, total: 104 µs\n", + "Wall time: 106 µs\n" + ] + }, + { + "data": { + "text/plain": [ + "numpy.ndarray" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "# calculate the minimum along all dimensions\n", + "min_np = da_np.min()\n", + "type(min_np.data)" + ] + }, + { + "cell_type": "markdown", + "id": "629a7339-b7ad-4636-a5f3-93e0772829f0", + "metadata": {}, + "source": [ + "```{note}\n", + "All Xarray operations *should* preserve array type. If they don't, please open an [issue](https://github.com/pydata/xarray/issues/new/choose).\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "7bab9311-8a4f-4ce5-bfdf-086aa09d5d11", + "metadata": {}, + "source": [ + "### Universal Functions (`ufunc`)\n", + "\n", + "Universal functions (or `ufunc` for short) are functions that operate element-wise on ndarrays, meaning they can perform computations on each element of an array without the need for explicit looping. \n", + "\n", + "These functions are designed to handle vectorized operations, which can significantly improve the performance and readability of your code.\n", + "\n", + "NumPy's universal functions offer a wide range of mathematical operations, including trigonometric functions (sin, cos, tan), exponential functions (exp, log), comparison operations (greater than, less than), and many others. We can apply these functions to the Xarray DataArray that wraps CuPy arrays:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "1304c015-e152-418a-b403-b489e8e2492c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.14 ms, sys: 54 µs, total: 1.19 ms\n", + "Wall time: 1.2 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "cupy.ndarray" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "# calculate the element-wise trigonometric sine\n", + "sin_cp = np.sin(da_cp)\n", + "type(sin_cp.data)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "f594a824-5459-4f24-9f9b-d5dfffd769c2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 846 µs, sys: 938 µs, total: 1.78 ms\n", + "Wall time: 1.79 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "cupy.ndarray" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "round_cp = np.round(da_cp.mean(), 2)\n", + "type(round_cp.data)" + ] + }, + { + "cell_type": "markdown", + "id": "a44f6d72-0e7c-4899-81eb-64187123cfa0", + "metadata": {}, + "source": [ + "## Computing with Multiple Objects\n", + "\n", + "### Alignment \n", + "\n", + "Alignment in xarray refers to the process of automatically aligning multiple DataArrays or Datasets based on their coordinates. Alignment ensures that the data along these coordinates is properly aligned before performing operations or calculations. This alignment is crucial because it enables xarray to handle operations on arrays with different sizes, shapes, and dimensions.\n", + "\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "69929337-6ed0-4312-b461-88f4006cf4b9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray (space: 3, time: 4)>\n",
+       "array([[ 0,  1,  2,  3],\n",
+       "       [ 4,  5,  6,  7],\n",
+       "       [ 8,  9, 10, 11]])\n",
+       "Coordinates:\n",
+       "  * space    (space) <U1 'a' 'b' 'c'\n",
+       "  * time     (time) int64 0 1 2 3
" + ], + "text/plain": [ + "\n", + "array([[ 0, 1, 2, 3],\n", + " [ 4, 5, 6, 7],\n", + " [ 8, 9, 10, 11]])\n", + "Coordinates:\n", + " * space (space) \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray (space: 2, time: 7)>\n",
+       "array([[ 0,  1,  2,  3,  4,  5,  6],\n",
+       "       [ 7,  8,  9, 10, 11, 12, 13]])\n",
+       "Coordinates:\n",
+       "  * space    (space) <U1 'b' 'd'\n",
+       "  * time     (time) int64 -2 -1 0 1 2 3 4
" + ], + "text/plain": [ + "\n", + "array([[ 0, 1, 2, 3, 4, 5, 6],\n", + " [ 7, 8, 9, 10, 11, 12, 13]])\n", + "Coordinates:\n", + " * space (space) \n", + "\n", + "Xarray broadcasting work similarly with CuPy and it preserves the data type. Here's an example to illustrate this:" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "2f395864-7ef6-42a2-812e-703ca82eefd4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray (space: 3)>\n",
+       "array([0, 1, 2])\n",
+       "Coordinates:\n",
+       "  * space    (space) <U1 'a' 'b' 'c'
" + ], + "text/plain": [ + "\n", + "array([0, 1, 2])\n", + "Coordinates:\n", + " * space (space) \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray (time: 4)>\n",
+       "array([0, 1, 2, 3])\n",
+       "Coordinates:\n",
+       "  * time     (time) int64 0 1 2 3
" + ], + "text/plain": [ + "\n", + "array([0, 1, 2, 3])\n", + "Coordinates:\n", + " * time (time) int64 0 1 2 3" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gpu_arr2 = xr.DataArray(\n", + " cp.arange(4),\n", + " dims=\"time\",\n", + " coords={\"time\": [0, 1, 2, 3]},\n", + ")\n", + "\n", + "gpu_arr2" + ] + }, + { + "cell_type": "markdown", + "id": "e8e01e23-d697-4485-b466-4288dc1429dd", + "metadata": {}, + "source": [ + "We can explicitly broadcast any number of arrays against each other using `xr.broadcast`:" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "dd3ce7b7-7d16-4ec3-83a3-751b25e62e2e", + "metadata": {}, + "outputs": [], + "source": [ + "arr1_broadcasted, arr2_broadcasted = xr.broadcast(gpu_arr1, gpu_arr2)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "19d43635-4b1d-49c3-a94e-f4470360e518", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray (space: 3, time: 4)>\n",
+       "array([[0, 0, 0, 0],\n",
+       "       [1, 1, 1, 1],\n",
+       "       [2, 2, 2, 2]])\n",
+       "Coordinates:\n",
+       "  * space    (space) <U1 'a' 'b' 'c'\n",
+       "  * time     (time) int64 0 1 2 3
" + ], + "text/plain": [ + "\n", + "array([[0, 0, 0, 0],\n", + " [1, 1, 1, 1],\n", + " [2, 2, 2, 2]])\n", + "Coordinates:\n", + " * space (space) \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray (space: 3, time: 4)>\n",
+       "array([[0, 1, 2, 3],\n",
+       "       [0, 1, 2, 3],\n",
+       "       [0, 1, 2, 3]])\n",
+       "Coordinates:\n",
+       "  * time     (time) int64 0 1 2 3\n",
+       "  * space    (space) <U1 'a' 'b' 'c'
" + ], + "text/plain": [ + "\n", + "array([[0, 1, 2, 3],\n", + " [0, 1, 2, 3],\n", + " [0, 1, 2, 3]])\n", + "Coordinates:\n", + " * time (time) int64 0 1 2 3\n", + " * space (space) " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "# Creating figure with two subplots\n", + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 5))\n", + "\n", + "# Plot 1 : CuPy time and NumPy time\n", + "ax1.plot(sizes, cp_times, marker=\"o\", label=\"CuPy Time\")\n", + "ax1.plot(sizes, np_times, marker=\"o\", label=\"NumPy Time\")\n", + "ax1.set_xlabel(\"Array Size\")\n", + "ax1.set_ylabel(\"Time\")\n", + "ax1.set_xticks(sizes)\n", + "ax1.legend()\n", + "\n", + "# Plot 2 : Speedup\n", + "ax2.plot(sizes, speedups, marker=\"o\")\n", + "ax2.set_xlabel(\"Array Size\")\n", + "ax2.set_ylabel(\"Speedup (CuPy time / NumPy time)\")\n", + "ax2.set_xticks(sizes)\n", + "fig.suptitle(\"Relative Humidity Calculation\")\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "ae89bf5f-10b1-435d-b9f6-487def091dbd", + "metadata": {}, + "source": [ + "The plots above clearly illustrate that as the size of the data increases, the performance improvement offered by CuPy becomes increasingly significant." + ] + }, + { + "cell_type": "markdown", + "id": "add5a5c1-8a18-4717-8775-9b75edb8347a", + "metadata": {}, + "source": [ + "Congratulations! You have now uncovered the basic operations and capabilities of CuPy. \n", + "\n", + "## Summary\n", + "\n", + "In this notebook, we have learned about:\n", + " \n", + "* Applying basic arithmetic and NumPy functions to xarray DataArrays with CuPy.\n", + "* Perform operations across multiple datasets\n", + "* Understand two important concepts: broadcasting and alignment.\n", + "* Performance of Cupy vs. Numpy on different array sizes. \n", + "\n", + "```{seealso}\n", + "\n", + "[CuPy User Guide](https://docs.cupy.dev/en/stable/user_guide/index.html) \n", + "[Xarray User Guide](https://docs.xarray.dev/en/stable/user-guide/index.html) \n", + "[Cupy-Xarray Github](https://github.com/xarray-contrib/cupy-xarray.git) \n", + "```" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst new file mode 100644 index 0000000..4eafd43 --- /dev/null +++ b/docs/source/contributing.rst @@ -0,0 +1,184 @@ +.. _contributing: + +****************** +Contributing guide +****************** + +.. note:: + + Large parts of this document came from the `Xarray Contributing + Guide `_ and `xbatcher Contributing Guide `_ + , which is based on the `Pandas Contributing Guide + `_. + +Bug reports and feature requests +================================ + +To report bugs or request new features, head over to the `cupy-xarray repository +`_. + +Contributing code +================== + +`GitHub has instructions `__ for +installing git, setting up your SSH key, and configuring git. All these steps +need to be completed for you to work between your local repository and GitHub. + +.. _contributing.forking: + +Forking +------- + +You will need your own fork to work on the code. Go to the `cupy-xarray project +page `_ and hit the ``Fork`` button. +You will need to clone your fork to your machine:: + + git clone git@github.com:yourusername/cupy-xarray.git + cd cupy-xarray + git remote add upstream git@github.com:xarray-contrib/cupy-xarray.git + +This creates the directory ``cupy-xarray`` and connects your repository to +the upstream (main project) *cupy-xarray* repository. + +.. _contributing.dev_env: + +Creating a development environment +---------------------------------- + +To test out code changes, you'll need to build *cupy-xarray* from source, which +requires a Python environment. If you're making documentation changes, you can +skip to :ref:`contributing.documentation` but you won't be able to build the +documentation locally before pushing your changes. + +.. _contributiong.dev_python: + +Creating a Python Environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before starting any development, you'll need to create an isolated cupy-xarray +development environment: + +- Install either `Anaconda `_ or `miniconda + `_ +- Make sure your conda is up to date (``conda update conda``) +- Make sure that you have :ref:`cloned the repository ` +- ``cd`` to the *cupy-xarray* source directory + +First we'll create and activate the build environment: + +.. code-block:: sh + + conda env create --file ci/requirements/environment.yml + conda activate cupy-xarray-tests + +At this point you should be able to import *cupy-xarray* from your locally +built version. + +This will create the new environment, and not touch any of your existing environments, +nor any existing Python installation. + +To view your environments:: + + conda info --envs + +To return to your base environment:: + + conda deactivate + +See the full conda docs `here `__. + +Setting up pre-commit +~~~~~~~~~~~~~~~~~~~~~ + +We use `pre-commit `_ to manage code linting and style. +To set up pre-commit after activating your conda environment, run: + +.. code-block:: sh + + pre-commit install + +Creating a branch +----------------- + +You want your ``main`` branch to reflect only production-ready code, so create a +feature branch before making your changes. For example:: + + git branch shiny-new-feature + git checkout shiny-new-feature + +The above can be simplified to:: + + git checkout -b shiny-new-feature + +This changes your working directory to the shiny-new-feature branch. Keep any +changes in this branch specific to one bug or feature so it is clear +what the branch brings to *cupy-xarray*. You can have many "shiny-new-features" +and switch in between them using the ``git checkout`` command. + +To update this branch, you need to retrieve the changes from the ``main`` branch:: + + git fetch upstream + git merge upstream/main + +This will combine your commits with the latest *cupy-xarray* git ``main``. If this +leads to merge conflicts, you must resolve these before submitting your pull +request. If you have uncommitted changes, you will need to ``git stash`` them +prior to updating. This will effectively store your changes, which can be +reapplied after updating. + +Running the test suite +---------------------- + +*cupy-xarray* uses the `pytest `_ +framework for testing. You can run the test suite using:: + + pytest cupy-xarray + +Contributing documentation +========================== + +We greatly appreciate documentation improvements. The docs are built from the docstrings +in the code and the docs in the ``doc`` directory. + +To build the documentation, you will need to requirements listed in ``ci/doc.yml``. +You can create an environment for building the documentation using:: + + conda env create --file ci/doc.yml + conda activate cupy-xarray-docs + +You can then build the documentation using:: + + cd docs + make html + +Contributing changes +==================== + +Once you've made changes, you can see them by typing:: + + git status + +If you have created a new file, it is not being tracked by git. Add it by typing:: + + git add path/to/file-to-be-added.py + +The following defines how a commit message should be structured: + + * A subject line with `< 72` chars. + * One blank line. + * Optionally, a commit message body. + +Now you can commit your changes in your local repository:: + + git commit -m + +When you want your changes to appear publicly on your GitHub page, push your +commits to a branch off your fork:: + + git push origin shiny-new-feature + +Here ``origin`` is the default name given to your remote repository on GitHub. You can see the remote repositories:: + + git remote -v + +If you navigate to your branch on GitHub, you should see a banner to submit a pull request to the *cupy-xarray* repository. diff --git a/docs/source/cupy-basics.ipynb b/docs/source/cupy-basics.ipynb new file mode 100644 index 0000000..1680a1c --- /dev/null +++ b/docs/source/cupy-basics.ipynb @@ -0,0 +1,750 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0d6fecdf-48c0-4745-b802-2117fb3137cf", + "metadata": {}, + "source": [ + "# Basics of CuPy" + ] + }, + { + "cell_type": "markdown", + "id": "15a05d43-0bf5-48d3-9c88-6074eed82a04", + "metadata": {}, + "source": [ + "## Overview\n", + "### In this tutorial, you learn:\n", + "\n", + "* Basics of Cupy and GPU computing\n", + "* Data Transfer Between Host and Device\n", + "* Compare speeds to NumPy\n", + "\n", + "## Prerequisites\n", + "\n", + "| Concepts | Importance | Notes |\n", + "| --- | --- | --- |\n", + "| [Familiarity with NumPy](https://foundations.projectpythia.org/core/numpy.html) | Necessary | |\n", + "\n", + "- **Time to learn**: 30 minutes\n", + "\n", + "## Introduction to CuPy\n", + "CuPy is an open-source GPU-accelerated array library for Python that is compatible with NumPy/SciPy. \n", + "\n", + "\n", + "\n", + "CuPy uses NVIDIA CUDA to run operations on the GPU, which can provide significant performance improvements for numerical computations compared to running on the CPU, especially at larger data sizes. CuPy provides a NumPy-like interface for array manipulation and supports a wide range of mathematical operations, making it a powerful tool for scientific computing on GPUs.\n", + "\n", + "
\n", + " In simple terms, CuPy can be described as the GPU equivalent of NumPy.\n", + "
\n", + "\n", + "CuPy is a library that has similar capabilities as NumPy, but with important distinctions that make it ideal for GPU computing. CuPy provides:\n", + "\n", + "* An object similar to NumPy's multidimensional array, except that it resides in the memory of the GPU, allowing for faster computations involving large data sets.\n", + "\n", + "* A system for applying \"universal functions\" (`ufuncs`) that adhere to broadcasting rules. This system leverages the parallel computing power of GPUs for better performance.\n", + "\n", + "* CuPy provides an extensive collection of CUDA-ready array functions. CUDA is NVIDIA's parallel computing platform and API model, which allows software developers to use a CUDA-enabled GPU for general purpose processing. CuPy's extensive set of pre-implemented mathematical functions can be used on arrays right off the bat, taking full advantage of GPU acceleration.\n", + "\n", + "For more information about CuPy, please visit:\n", + "\n", + "[CuPy Homepage](https://docs.cupy.dev/en/stable/index.html#)\n", + "\n", + "[CuPy Github](https://github.com/cupy/cupy)\n", + "\n", + "In this tutorial, we will explore the distinctive features of CuPy and show their differences from NumPy. Let's get started!" + ] + }, + { + "cell_type": "markdown", + "id": "77343efb-de6d-423c-b1cd-934c5d6d68e1", + "metadata": {}, + "source": [ + "## Getting Started with CuPy" + ] + }, + { + "cell_type": "markdown", + "id": "1c0a8fe5-0923-464e-8ea0-77e8d46b7977", + "metadata": {}, + "source": [ + "Once CuPy is installed, we can import it in the same way as NumPy:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "55c72b7d-8899-4e2f-9432-e9cf1531cbdf", + "metadata": {}, + "outputs": [], + "source": [ + "## Import NumPy and CuPy\n", + "import cupy as cp\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "62af1f7c-0ac2-4bad-ab92-8f1bcfbaffe3", + "metadata": {}, + "source": [ + "### Arrays in CuPy vs. NumPy\n", + "\n", + "CuPy arrays can be declared using the `cupy.ndarray` class, much like NumPy arrays using `numpy.ndarrays`. However, it is important to note that while NumPy arrays are generated on the CPU (referred to as the \"host\"), CuPy arrays are generated on the GPU (known as the \"device\").\n", + "\n", + "CuPy arrays look just like NumPy arrays:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c98d68a4-3b43-4a7d-91e2-53afdb121273", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "On the CPU: [1 2 3 4 5]\n", + "\n" + ] + } + ], + "source": [ + "# create a 1D array with 5 elements on CPU\n", + "arr_cpu = np.array([1, 2, 3, 4, 5])\n", + "print(\"On the CPU: \", arr_cpu)\n", + "print(type(arr_cpu))" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "7f09bd38-67fd-465f-a3f7-547b2b989b62", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "On the GPU: [1 2 3 4 5]\n", + "\n" + ] + } + ], + "source": [ + "# create a 1D array with 5 elements on GPU\n", + "arr_gpu = cp.array([1, 2, 3, 4, 5])\n", + "print(\"On the GPU: \", arr_gpu)\n", + "print(type(arr_gpu))" + ] + }, + { + "cell_type": "markdown", + "id": "e4d08c51-65a1-471f-841d-418ad0df592c", + "metadata": {}, + "source": [ + " You can also create multi-dimensional arrays:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "693b52b5-0b94-464d-b3bd-1c4a53b4f17d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "On the CPU: [[0. 0. 0. 0.]\n", + " [0. 0. 0. 0.]\n", + " [0. 0. 0. 0.]]\n", + "\n" + ] + } + ], + "source": [ + "# create a 2D array of zeros with 3 rows and 4 columns\n", + "arr_cpu = np.zeros((3, 4))\n", + "print(\"On the CPU: \", arr_cpu)\n", + "print(type(arr_cpu))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9845d93b-0d04-450b-ae68-47fc911f339d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "On the GPU: [[0. 0. 0. 0.]\n", + " [0. 0. 0. 0.]\n", + " [0. 0. 0. 0.]]\n", + "\n" + ] + } + ], + "source": [ + "arr_gpu = cp.zeros((3, 4))\n", + "print(\"On the GPU: \", arr_gpu)\n", + "print(type(arr_gpu))" + ] + }, + { + "cell_type": "markdown", + "id": "266ab29b-d11f-419d-b52b-9b6be5638945", + "metadata": {}, + "source": [ + "As we can see in the above examples, CuPy arrays look just like NumPy arrays, except that Cupy arrays are stored on GPUs vs. Numpy arrays are stored on CPUs." + ] + }, + { + "cell_type": "markdown", + "id": "5398e305-063a-4b15-b259-5eddf29c8cf9", + "metadata": {}, + "source": [ + "### Basic Operations \n", + "CuPy provides equivalents for many common NumPy functions, although not all. Most of CuPy's functions have the same function call as their NumPy counterparts. See the reference for the supported subset of NumPy API.\n", + "| | |\n", + "| :--- | :--- |\n", + "| **NumPy** | **CuPy** |\n", + "| numpy.identity | cupy.identity |\n", + "| numpy.matmul | cupy.matmul |\n", + "| numpy.nan_to_num | cupy.nan_to_num |\n", + "| numpy.zeros | cupy.zeros |\n", + "| numpy.ones | cupy.ones |\n", + "| numpy.shape | cupy.shape |\n", + "| numpy.reshape | cupy.reshape |\n", + "| numpy.tensordot | cupy.tensordot |\n", + "| numpy.transpose | cupy.transpose |\n", + "| numpy.fft.fft | cupy.fft.fft |\n", + "\n", + "Cupy also provides equivalant functions for some SciPy functions, but its implementation is not as extensive as NumPy's.\n", + "\n", + "See [here](https://docs.cupy.dev/en/stable/reference/comparison.html) for a full list of CuPy's Numpy and Scipy equivalent functions.\n", + "\n", + "\n", + "[CuPy API Reference](https://docs.cupy.dev/en/stable/reference/index.html)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0850adf9-0c24-4687-b8de-1b7da734347e", + "metadata": {}, + "outputs": [], + "source": [ + "# NumPy: Create an array\n", + "numpy_a = np.array([1, 2, 3, 4, 5])\n", + "\n", + "# CuPy: Create an array\n", + "cupy_a = cp.array([1, 2, 3, 4, 5])" + ] + }, + { + "cell_type": "markdown", + "id": "45fe9f2a-e00f-4cfa-b0a5-eb5a5682e743", + "metadata": {}, + "source": [ + "Basic arithmetic operations is exactly identical between numpy and cupy. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f6e7880b-2238-4c3b-a431-157e6c5389dc", + "metadata": {}, + "outputs": [], + "source": [ + "# Basic arithmetic operations\n", + "numpy_b = numpy_a + 2\n", + "cupy_b = cupy_a + 2\n", + "\n", + "numpy_c = numpy_a * 2\n", + "cupy_c = cupy_a * 2\n", + "\n", + "numpy_d = numpy_a.dot(numpy_a)\n", + "cupy_d = cupy_a.dot(cupy_a)\n", + "\n", + "# Reshaping arrays\n", + "numpy_e = numpy_a.reshape(5, 1)\n", + "cupy_e = cupy_a.reshape(5, 1)\n", + "\n", + "# Transposing arrays\n", + "numpy_f = numpy_e.T\n", + "cupy_f = cupy_e.T\n", + "\n", + "# Complex example: element-wise exponential and sum\n", + "numpy_g = np.exp(numpy_a) / np.sum(np.exp(numpy_a))\n", + "cupy_g = cp.exp(cupy_a) / cp.sum(cp.exp(cupy_a))" + ] + }, + { + "cell_type": "markdown", + "id": "9f25ee88-1adf-45fd-8b24-04e30fe4488f", + "metadata": {}, + "source": [ + "### Data Transfer\n", + "\n", + "#### Data Transfer to a Device\n", + "`cupy.asarray()` can be used to move a numpy array to a device (GPU)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "20bd69c4-5a8b-4147-9169-efc65a49b5e4", + "metadata": {}, + "outputs": [], + "source": [ + "# Move data to GPU\n", + "arr_gpu = cp.asarray(arr_cpu)" + ] + }, + { + "cell_type": "markdown", + "id": "39ccf012-f467-49f4-99cd-0eea489e21a0", + "metadata": {}, + "source": [ + "#### Move array from GPU to the CPU\n", + "\n", + "Moving a device array to the host (i.e. CPU) can be done by `cupy.asnumpy()` as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2e557105-755f-48ec-977e-bedee81b99c9", + "metadata": {}, + "outputs": [], + "source": [ + "# Move data back to host\n", + "arr_cpu = cp.asnumpy(arr_gpu)" + ] + }, + { + "cell_type": "markdown", + "id": "30386bbc-26b0-4afd-904b-30bb34d80d6a", + "metadata": {}, + "source": [ + "We can also use `cupy.ndarray.get()`:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "bc09a284-dbd1-4f30-b262-0761b2832bfa", + "metadata": {}, + "outputs": [], + "source": [ + "arr_cpu = arr_gpu.get()" + ] + }, + { + "cell_type": "markdown", + "id": "46dfb920-eb81-4cd1-b407-00099b76f633", + "metadata": { + "tags": [] + }, + "source": [ + "### Device Information \n", + "CuPy introduces the concept of a *current* device, which represents the default GPU device for array allocation, manipulation, calculations, and other operations. \n", + "\n", + "`cupy.ndarray.device` attribute can be used to determine the device allocated to a CUPY array: " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "114120c2-99c1-4f0f-9ad8-40486dfff4e5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cupy_g.device" + ] + }, + { + "cell_type": "markdown", + "id": "e6310585-8dbe-4cc4-a235-6694db49d44a", + "metadata": {}, + "source": [ + "To obtain the total number of accessible devices, you can utilize the getDeviceCount function." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "e808fa97-7360-4f4a-b239-12d6a3cacbaf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cp.cuda.runtime.getDeviceCount()" + ] + }, + { + "cell_type": "markdown", + "id": "983b94ba-8127-461c-89cb-651fe123ccbb", + "metadata": {}, + "source": [ + "The default behavior runs code on Device 0, but we can transfer arrays other devices with CuPy using `cp.cuda.Device()`. This capability becomes significantly important when your code is designed to harness the power of multiple GPUs.\n", + "\n", + "If you want to change to a different GPU device, you can do so by utilizing the \"device\" context manager. For example the following create an array on the GPU 2. \n", + "\n", + "``` python \n", + "with cp.cuda.Device(2):\n", + " x_on_gpu2 = cp.array([1, 2, 3, 4, 5])\n", + "```\n", + "\n", + "There is no need for explicit device switching when only one device is available." + ] + }, + { + "cell_type": "markdown", + "id": "747151e6-dc5f-4444-a906-528d1066a1dd", + "metadata": {}, + "source": [ + "## CuPy vs NumPy: Speed Comparison\n", + "\n", + "Now that we are familar with CuPy, let's explore the performance improvements that CuPy can provide in comparison to NumPy for different data sizes. \n", + "\n", + "First, we are looking at matrix multiplication for array size of 3000x3000." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "1545d1e5-3ae8-422b-95b5-cd88e7eb64e7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NumPy time: 0.7095739841461182 seconds\n", + "CuPy time: 0.6216685771942139 seconds\n", + "CuPy provides a 1.14 x speedup over NumPy.\n" + ] + } + ], + "source": [ + "import time\n", + "\n", + "# create two 3000x3000 matrices\n", + "n = 3000\n", + "\n", + "a_np = np.random.rand(n, n)\n", + "b_np = np.random.rand(n, n)\n", + "\n", + "a_cp = cp.asarray(a_np)\n", + "b_cp = cp.asarray(b_np)\n", + "\n", + "# perform matrix multiplication with NumPy and time it\n", + "start_time = time.time()\n", + "c_np = np.matmul(a_np, b_np)\n", + "end_time = time.time()\n", + "\n", + "numpy_time = end_time - start_time\n", + "print(\"NumPy time:\", numpy_time, \"seconds\")\n", + "\n", + "# perform matrix multiplication with CuPy and time it\n", + "start_time = time.time()\n", + "c_cp = cp.matmul(a_cp, b_cp)\n", + "cp.cuda.Stream.null.synchronize() # wait for GPU computation to finish\n", + "end_time = time.time()\n", + "\n", + "cupy_time = end_time - start_time\n", + "\n", + "print(\"CuPy time:\", cupy_time, \"seconds\")\n", + "print(\"CuPy provides a\", round(numpy_time / cupy_time, 2), \"x speedup over NumPy.\")" + ] + }, + { + "cell_type": "markdown", + "id": "a1cb881d-9ef1-4fbb-8044-d5bd4c8cb8b6", + "metadata": {}, + "source": [ + "Now, let's run the same CuPy operation again:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "a83b1fd5-7896-49ed-9e64-74bcd1417c2c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CuPy time: 0.01408529281616211 seconds\n", + "CuPy provides a 50.38 x speedup over NumPy.\n" + ] + } + ], + "source": [ + "# perform matrix multiplication with CuPy and time it\n", + "start_time = time.time()\n", + "c_cp = cp.matmul(a_cp, b_cp)\n", + "cp.cuda.Stream.null.synchronize() # wait for GPU computation to finish\n", + "end_time = time.time()\n", + "\n", + "cupy_time = end_time - start_time\n", + "\n", + "print(\"CuPy time:\", cupy_time, \"seconds\")\n", + "print(\"CuPy provides a\", round(numpy_time / cupy_time, 2), \"x speedup over NumPy.\")" + ] + }, + { + "cell_type": "markdown", + "id": "ca229603-89a0-49ca-8920-b40d29a2b703", + "metadata": {}, + "source": [ + "### What happened? Why CuPy is faster the second time?\n", + "When running these functions for the first time, you may experience a brief pause. This occurs as CuPy compiles the CUDA functions for the first time and cached them on disk for future use.\n" + ] + }, + { + "cell_type": "markdown", + "id": "662bb0c3-4051-4125-b801-173b8b3c30b5", + "metadata": {}, + "source": [ + "Now, let's make the same comparison with different array sizes." + ] + }, + { + "cell_type": "markdown", + "id": "29b798e6-4c8b-44c6-86df-b92abdb0a683", + "metadata": {}, + "source": [ + "We can use the following function to find the size of a variable on memory. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "e4fdad11-9e3b-4f65-9dce-9ffbd33dc419", + "metadata": {}, + "outputs": [], + "source": [ + "# Define function to display variable size in MB\n", + "import sys\n", + "\n", + "\n", + "def var_size(in_var):\n", + " result = sys.getsizeof(in_var) / 1e6\n", + " print(f\"Size of variable: {result:.2f} MB\")" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "bba9681e-ca7b-486c-92c8-1a79434ba0da", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "n = 100\n", + "Size of variable: 0.08 MB\n", + "CuPy provides a 6.45 x speedup over NumPy.\n", + "\n", + "n = 200\n", + "Size of variable: 0.32 MB\n", + "CuPy provides a 1.28 x speedup over NumPy.\n", + "\n", + "n = 500\n", + "Size of variable: 2.00 MB\n", + "CuPy provides a 9.83 x speedup over NumPy.\n", + "\n", + "n = 1000\n", + "Size of variable: 8.00 MB\n", + "CuPy provides a 41.17 x speedup over NumPy.\n", + "\n", + "n = 2000\n", + "Size of variable: 32.00 MB\n", + "CuPy provides a 72.55 x speedup over NumPy.\n", + "\n", + "n = 5000\n", + "Size of variable: 200.00 MB\n", + "CuPy provides a 77.9 x speedup over NumPy.\n", + "\n", + "n = 10000\n", + "Size of variable: 800.00 MB\n", + "CuPy provides a 80.68 x speedup over NumPy.\n", + "\n" + ] + } + ], + "source": [ + "speed_ups = []\n", + "arr_sizes = []\n", + "sizes = [100, 200, 500, 1000, 2000, 5000, 10000]\n", + "for n in sizes:\n", + " print(\"n =\", n)\n", + "\n", + " # create two nxn matrices\n", + " a_np = np.random.rand(n, n)\n", + " b_np = np.random.rand(n, n)\n", + "\n", + " a_cp = cp.asarray(a_np)\n", + " b_cp = cp.asarray(b_np)\n", + "\n", + " arr_size = a_cp.nbytes / 1e6\n", + " print(f\"Size of variable: {arr_size:.2f} MB\")\n", + "\n", + " # perform matrix multiplication with NumPy and time it\n", + " start_time = time.time()\n", + " c_np = np.matmul(a_np, b_np)\n", + " end_time = time.time()\n", + " numpy_time = end_time - start_time\n", + "\n", + " # perform matrix multiplication with CuPy and time it\n", + " start_time = time.time()\n", + " c_cp = cp.matmul(a_cp, b_cp)\n", + " cp.cuda.Stream.null.synchronize() # wait for GPU computation to finish\n", + " end_time = time.time()\n", + " cupy_time = end_time - start_time\n", + "\n", + " speed_up = round(numpy_time / cupy_time, 2)\n", + "\n", + " speed_ups.append(speed_up)\n", + " arr_sizes.append(arr_size)\n", + " # print the speedup\n", + " print(\"CuPy provides a\", speed_up, \"x speedup over NumPy.\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "fc3dd3c8-032f-437b-a6d0-7dae7e55b73c", + "metadata": {}, + "source": [ + "We can also create a plot of data size vs. speed-ups:" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "fd5dbadf-8286-464d-880c-c25297ed310d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "plt.figure(figsize=(5, 5))\n", + "plt.plot(sizes, speed_ups, marker=\"o\")\n", + "plt.xlabel(\"Matrix size\")\n", + "plt.ylabel(\"Speedup (CuPy time / NumPy time)\")\n", + "# plt.xticks(sizes)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "93da5acf-ba39-4617-8246-ac7f7b3fd8df", + "metadata": {}, + "source": [ + "```{note}\n", + "As we can see above, GPUs computations can be slower than CPUs!\n", + "```\n", + "\n", + "There are several reasons for this: \n", + " \n", + "* The size of our arrays: The GPU's performance relies on parallelism, processing thousands of values simultaneously. To fully leverage the GPU's capabilities, we require a significantly larger array. As we see in the above example, for bigger matrix size we see more speed-ups. \n", + "\n", + "* The simplicity of our calculation: Transferring a calculation to the GPU involves considerable overhead compared to executing a function on the CPU. If our calculation lacks a sufficient number of mathematical operations (known as \"arithmetic intensity\"), the GPU will spend most of its time waiting for data movement.\n", + "\n", + "* Data copying to and from the GPU impacts performance: While including copy time can be realistic for a single function, there are instances where we need to execute multiple GPU operations sequentially. In such cases, it is advantageous to transfer data to the GPU and keep it there until all processing is complete." + ] + }, + { + "cell_type": "markdown", + "id": "1fc57cc2-d237-49e9-bf4f-ef74511673d7", + "metadata": {}, + "source": [ + "Congratulations! You have now uncovered the capabilities of CuPy. It's time to unleash its power and accelerate your own code by replacing NumPy with CuPy wherever applicable and appropriate. In the next chapters we will delve into Cupy Xarray capabilities. \n", + "\n", + "## Summary\n", + "\n", + "In this notebook, we have learned about:\n", + "\n", + "* Cupy Basics\n", + "* Data Transfer between Device and Host\n", + "* Performance of Cupy vs. Numpy on different array sizes. \n", + "\n", + "```{seealso}\n", + "\n", + "[CuPy Homepage](https://cupy.dev/) \n", + "[CuPy Github](https://github.com/cupy/cupy) \n", + "[CuPy User Guide](https://docs.cupy.dev/en/stable/user_guide/index.html)\n", + "```" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:gpu-xdev]", + "language": "python", + "name": "conda-env-gpu-xdev-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.15" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/high-level-api.ipynb b/docs/source/high-level-api.ipynb new file mode 100644 index 0000000..b329be5 --- /dev/null +++ b/docs/source/high-level-api.ipynb @@ -0,0 +1,629 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "93434031-d7fe-4322-a9cf-41e5b8be622d", + "metadata": {}, + "source": [ + "# High-level Computation" + ] + }, + { + "cell_type": "markdown", + "id": "4c0164c9-f970-4206-964a-d1d8080e2b0a", + "metadata": {}, + "source": [ + "## Overview\n", + "### In this tutorial, you learn:\n", + "\n", + "* High level Xarray computations using CuPy arrays. \n", + "* Applying custom kernels to DataArray with CuPy and NumPy\n", + "\n", + "## Prerequisites\n", + "\n", + "| Concepts | Importance | Notes |\n", + "| --- | --- | --- |\n", + "| [Familiarity with NumPy](https://foundations.projectpythia.org/core/numpy.html) | Necessary | |\n", + "| [Basics of Cupy](Notebook0_Introduction) | Necessary | |\n", + "| [Familiarity with Xarray](https://foundations.projectpythia.org/core/xarray.html) | Necessary | |\n", + "\n", + "- **Time to learn**: 40 minutes\n", + "\n", + "\n", + "\n", + "## Introduction \n", + "\n", + "In the previous tutorial, we introduced the powerful combination of Xarray and CuPy for handling multi-dimensional datasets and leveraging GPU acceleration to significantly improve performance. \n", + "\n", + "In this tutorial, we are going to explore high-level Xarray functions such as `groupby`, `rolling mean`, and `weighted mean`, and compared their execution times with traditional NumPy-based implementations.\n", + "\n", + "## High-level Xarray Functions: CuPy vs. NumPy\n", + "\n", + "In this tutorial, we'll explore the performance differences between high-level Xarray functions using CuPy and NumPy. CuPy is a GPU-based NumPy-compatible library, while NumPy is the well-known CPU-based library for numerical computations. We'll focus on three high-level functions: groupby, rolling mean, and weighted mean. We'll also compare the time it takes to execute each function using both CuPy and NumPy.\n", + "Let's create some sample data to work with.\n", + "\n", + "We'll use a 3-dimensional dataset (time, latitude, longitude) with random values:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e21d74da-e620-4737-91a4-0180f3703c75", + "metadata": {}, + "outputs": [], + "source": [ + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "377ac7ca-6c09-486f-95cc-8a2d599df800", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import xarray as xr" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "447116b1-bc54-4d0a-af46-5908a7f95e93", + "metadata": {}, + "outputs": [], + "source": [ + "import cupy as cp\n", + "import cupy_xarray # Adds .cupy to Xarray objects" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5f6f83a3-36d6-493d-bfda-49f5d766ef14", + "metadata": {}, + "outputs": [], + "source": [ + "np.random.seed(0)\n", + "\n", + "# Create the time range.\n", + "date = pd.date_range(\"2010-01-01\", \"2020-12-31\", freq=\"M\")\n", + "\n", + "# Create the latitude range.\n", + "lat = np.arange(-90, 90, 1)\n", + "\n", + "# Create the longitude range.\n", + "lon = np.arange(-180, 180, 1)\n", + "\n", + "# Create random data\n", + "data_np = np.random.rand(len(date), len(lat), len(lon))\n", + "data_cp = cp.array(data_np)\n", + "\n", + "# -- Create DataArray with Numpy data\n", + "data_xr_np = xr.DataArray(\n", + " data_np,\n", + " dims=[\"time\", \"lat\", \"lon\"],\n", + " coords=[date, lat, lon],\n", + ")\n", + "\n", + "# -- Create DataArray with CuPy data\n", + "data_xr_cp = xr.DataArray(\n", + " data_cp,\n", + " dims=[\"time\", \"lat\", \"lon\"],\n", + " coords=[date, lat, lon],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "6ea80193-172e-45c9-af18-1ec288c4bbd8", + "metadata": {}, + "source": [ + "### Groupby\n", + "The `groupby` function is used to group data based on one or more dimensions. Here, we'll group our data by the season in the `time` dimension using both CuPy and NumPy:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "e1c0b119-23f9-4397-957a-ace2a607ab6f", + "metadata": {}, + "outputs": [], + "source": [ + "start_time_np = time.time()\n", + "\n", + "grouped_data_np = data_xr_np.groupby(\"time.season\")\n", + "mean_np = grouped_data_np.mean()\n", + "\n", + "end_time_np = time.time()\n", + "time_np = end_time_np - start_time_np" + ] + }, + { + "cell_type": "markdown", + "id": "fd95f64d-87cc-4ae5-95f5-2170db1eb547", + "metadata": {}, + "source": [ + "The data type of data in grouped_data_np is `numpy.ndarray`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "53419533-005b-4ceb-9ae5-2deaa316c52a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[type(arr.data) for group, arr in grouped_data_np]" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "973e4b7e-1b2f-4fbb-a4da-a7a47b1be327", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "GroupBy with Xarray DataArrays using CuPy provides a 79.83 x speedup over NumPy.\n", + "\n" + ] + } + ], + "source": [ + "start_time_cp = time.time()\n", + "\n", + "grouped_data_cp = data_xr_cp.groupby(\"time.season\")\n", + "mean_cp = grouped_data_cp.mean()\n", + "\n", + "end_time_cp = time.time()\n", + "time_cp = end_time_cp - start_time_cp\n", + "\n", + "print(\n", + " \"GroupBy with Xarray DataArrays using CuPy provides a\",\n", + " round(time_np / time_cp, 2),\n", + " \"x speedup over NumPy.\\n\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d28001bb-f360-4b50-84c6-aec922807a24", + "metadata": {}, + "source": [ + "What about the CuPy arrays? Does it preserve the array type?" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "eadab73b-769a-4a43-800e-dde016e6834c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[cupy.ndarray, cupy.ndarray, cupy.ndarray, cupy.ndarray]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[type(arr.data) for group, arr in grouped_data_cp]" + ] + }, + { + "cell_type": "markdown", + "id": "bc3c4f8c-2077-4cdd-b86d-3a6643e38a4d", + "metadata": {}, + "source": [ + "### What about different sizes of arrays? How does the performance compare then?" + ] + }, + { + "cell_type": "markdown", + "id": "0e13ecad-4b00-4766-afc3-91a9525eed7b", + "metadata": {}, + "source": [ + "The example above showed a 1 degree DataArray. What if we increase the data size to 0.5 degree?" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "24f7fdbe-4c71-40b3-84af-564fd987f96b", + "metadata": {}, + "outputs": [], + "source": [ + "# Create the latitude range.\n", + "lat = np.arange(-90, 90, 0.5)\n", + "\n", + "# Create the longitude range.\n", + "lon = np.arange(-180, 180, 0.5)\n", + "\n", + "# Create random data\n", + "data_np = np.random.rand(len(date), len(lat), len(lon))\n", + "data_cp = cp.array(data_np)\n", + "\n", + "# -- Create DataArray with Numpy data\n", + "data_xr_np = xr.DataArray(\n", + " data_np,\n", + " dims=[\"time\", \"lat\", \"lon\"],\n", + " coords=[date, lat, lon],\n", + ")\n", + "\n", + "# -- Create DataArray with CuPy data\n", + "data_xr_cp = xr.DataArray(\n", + " data_cp,\n", + " dims=[\"time\", \"lat\", \"lon\"],\n", + " coords=[date, lat, lon],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "0403170b-f1b2-4883-8945-af16e5571496", + "metadata": {}, + "outputs": [], + "source": [ + "start_time_np = time.time()\n", + "\n", + "grouped_data_np = data_xr_np.groupby(\"time.season\").mean()\n", + "mean_np = grouped_data_np.mean()\n", + "\n", + "end_time_np = time.time()\n", + "time_np = end_time_np - start_time_np" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "d6adf806-83ff-4b1f-954c-2a7d4ac9e0c9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "GroupBy with Xarray DataArrays using CuPy provides a 89.87 x speedup over NumPy.\n", + "\n" + ] + } + ], + "source": [ + "start_time_cp = time.time()\n", + "\n", + "grouped_data_cp = data_xr_cp.groupby(\"time.season\").mean()\n", + "mean_cp = grouped_data_cp.mean()\n", + "\n", + "end_time_cp = time.time()\n", + "time_cp = end_time_cp - start_time_cp\n", + "\n", + "print(\n", + " \"GroupBy with Xarray DataArrays using CuPy provides a\",\n", + " round(time_np / time_cp, 2),\n", + " \"x speedup over NumPy.\\n\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "5bc0b970-7893-4a9c-b06a-941c134b8c2d", + "metadata": {}, + "source": [ + "```{attention}\n", + "Is this consistent with what you have learned in the previous modules? What if we test a very low resolution dataset?\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "d3d10f6d-74a3-494d-a564-a780415373b2", + "metadata": {}, + "source": [ + "### Rolling Mean\n", + "\n", + "The `rolling()` method is available in DataArray objects, providing support for rolling window aggregation. This feature allows for the computation of aggregated values over a sliding window of data points within the array.\n", + "\n", + "A rolling window refers to a fixed-size window that moves sequentially across the data, calculating aggregated statistics or applying functions to the values within each window. This capability is particularly useful for analyzing time series or spatial data, where examining data within a specific window can reveal patterns, trends, or relationships.\n", + "\n", + "The rolling mean is a widely used technique for smoothing data over a specified window. \n", + "\n", + "In the example below, we calculate the rolling mean along the `'time'` dimension with a window size of 10:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "d30fc60b-d947-43ec-8699-852c1aca6b59", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "xr.set_options(use_bottleneck=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "a0202ef1-e8a4-4676-9b20-3e91a175b78f", + "metadata": {}, + "outputs": [], + "source": [ + "start_time_np = time.time()\n", + "\n", + "rolling_mean_np = data_xr_np.rolling(time=10).mean()\n", + "\n", + "end_time_np = time.time()\n", + "time_np = end_time_np - start_time_np" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "2f496410-b347-4f20-a1de-44e39129177d", + "metadata": {}, + "outputs": [], + "source": [ + "start_time_cp = time.time()\n", + "\n", + "rolling_mean_cp = data_xr_cp.rolling(time=10).mean()\n", + "\n", + "end_time_cp = time.time()\n", + "time_cp = end_time_cp - start_time_cp" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "1211e89c-7d17-462f-99ac-18a71245adf8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Rolling mean with Xarray DataArrays using CuPy provides a 30.22 x speedup over NumPy.\n", + "\n" + ] + } + ], + "source": [ + "print(\n", + " \"Rolling mean with Xarray DataArrays using CuPy provides a\",\n", + " round(time_np / time_cp, 2),\n", + " \"x speedup over NumPy.\\n\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "c75ff2c6-1f65-4545-bf76-274ff1ec3939", + "metadata": {}, + "source": [ + "### Weighted Array Reductions\n", + "\n", + "Weighted array reductions in Xarray empower users with the ability to perform aggregations on multidimensional arrays while considering the weights assigned to each element. They currently support weighted `sum`, `mean`, `std`, `var` and `quantile`. By default, aggregation results in Xarray's rolling window operations are assigned the coordinate at the end of each window. However, it is possible to center the results by specifying `center=True` when creating the Rolling object. \n", + "\n", + "For example, the weighted mean is another way to smooth data, taking into account the varying importance of each data point. \n", + "\n", + "Here, we'll use a uniform weight along the `time` dimension:\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "1a580f4f-769c-46fe-84e7-1a8372362ad5", + "metadata": {}, + "outputs": [], + "source": [ + "start_time_np = time.time()\n", + "\n", + "weights_np = xr.DataArray(np.ones_like(data_np), dims=[\"time\", \"lat\", \"lon\"])\n", + "weighted_mean_np = data_xr_np.weighted(weights_np).mean(dim=\"time\")\n", + "\n", + "end_time_np = time.time()\n", + "time_np = end_time_np - start_time_np" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "bcce9e70-67c9-4228-ac18-311fa3a555f3", + "metadata": {}, + "outputs": [], + "source": [ + "start_time_cp = time.time()\n", + "\n", + "weights_cp = xr.DataArray(cp.ones_like(data_cp), dims=[\"time\", \"lat\", \"lon\"])\n", + "weighted_mean_cp = data_xr_cp.weighted(weights_cp).mean(dim=\"time\")\n", + "\n", + "end_time_cp = time.time()\n", + "time_cp = end_time_cp - start_time_cp" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "c48a20d7-5d83-433e-8d8f-86aba0c5cf4b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Weighted mean with Xarray DataArrays using CuPy provides a 13.32 x speedup over NumPy.\n", + "\n" + ] + } + ], + "source": [ + "print(\n", + " \"Weighted mean with Xarray DataArrays using CuPy provides a\",\n", + " round(time_np / time_cp, 2),\n", + " \"x speedup over NumPy.\\n\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "16cd108a-0bea-42a3-aa59-41acd505f5f6", + "metadata": {}, + "source": [ + "Similarly we can calculate weighted sum or weighted quantile, etc. To learn more about weighted array reduction, please see [the user guide](https://docs.xarray.dev/en/stable/user-guide/computation.html#weighted-array-reductions)." + ] + }, + { + "cell_type": "markdown", + "id": "b5ce7661-da08-4f51-8f3c-a67cc8dacd83", + "metadata": {}, + "source": [ + "### Coarsen large arrays\n", + "\n", + "In Xarray, the `coarsen` operation is a powerful tool for downsampling or reducing the size of large arrays. When dealing with large datasets, coarsening allows for efficient summarization of data by aggregating multiple values into a single value within a defined coarsening window. This process is particularly useful when working with high-resolution or fine-grained data, as it enables the transformation of large arrays into smaller ones while preserving the overall structure and key characteristics of the data. \n", + "\n", + "In order to take a block mean for every 3 days along time dimension and every 2 points along lat and lon, we can use the following: " + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "f9198d05-ece4-4383-8b7e-2598efba49af", + "metadata": {}, + "outputs": [], + "source": [ + "start_time_np = time.time()\n", + "\n", + "coarsen_np = data_xr_np.coarsen(time=3, lat=2, lon=2).mean()\n", + "\n", + "end_time_np = time.time()\n", + "time_np = end_time_np - start_time_np" + ] + }, + { + "cell_type": "markdown", + "id": "7f6cc471-8e3e-4471-8ffc-2f90de161055", + "metadata": {}, + "source": [ + "`coarsen` also works in similar fashion when using CuPy:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "67bdfb24-90ac-4502-a57e-c3406381988b", + "metadata": {}, + "outputs": [], + "source": [ + "start_time_cp = time.time()\n", + "\n", + "coarsen_cp = data_xr_cp.coarsen(time=3, lat=2, lon=2).mean()\n", + "\n", + "end_time_cp = time.time()\n", + "time_cp = end_time_cp - start_time_cp" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "9a41faf8-74f4-4a33-bf7d-c77797e8e784", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Coarsen with Xarray DataArrays using CuPy provides a 443.79 x speedup over NumPy.\n", + "\n" + ] + } + ], + "source": [ + "print(\n", + " \"Coarsen with Xarray DataArrays using CuPy provides a\",\n", + " round(time_np / time_cp, 2),\n", + " \"x speedup over NumPy.\\n\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "95dccf03-bb64-43ef-883e-9b84081c4514", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "## Summary\n", + "\n", + "In this notebook, we have learned about:\n", + " \n", + "* High level Xarray computations using CuPy arrays. \n", + "* Applying custom kernels to DataArray with CuPy and NumPy\n", + "\n", + "```{seealso}\n", + "[CuPy User Guide](https://docs.cupy.dev/en/stable/user_guide/index.html) \n", + "[Xarray User Guide](https://docs.xarray.dev/en/stable/user-guide/index.html) \n", + "[Cupy-Xarray Github](https://github.com/xarray-contrib/cupy-xarray.git) \n", + "```" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/introduction.ipynb b/docs/source/introduction.ipynb new file mode 100644 index 0000000..8a68f8b --- /dev/null +++ b/docs/source/introduction.ipynb @@ -0,0 +1,1963 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0d6fecdf-48c0-4745-b802-2117fb3137cf", + "metadata": {}, + "source": [ + "# Introduction" + ] + }, + { + "cell_type": "markdown", + "id": "15a05d43-0bf5-48d3-9c88-6074eed82a04", + "metadata": {}, + "source": [ + "## Overview\n", + "### In this tutorial, you learn:\n", + "\n", + "* Basics of Cupy-Xarray\n", + "* Creating and handling Xarray DataArrays on GPUs\n", + "* Data Transfer Between Host and Device\n", + "\n", + "## Prerequisites\n", + "\n", + "| Concepts | Importance | Notes |\n", + "| --- | --- | --- |\n", + "| [Familiarity with NumPy](https://foundations.projectpythia.org/core/numpy.html) | Necessary | |\n", + "| [Basics of Cupy](Notebook0_Introduction) | Necessary | |\n", + "| [Familiarity with Xarray](https://foundations.projectpythia.org/core/xarray.html) | Necessary | |\n", + "\n", + "- **Time to learn**: 10 minutes\n", + "\n", + "\n", + "## Introduction " + ] + }, + { + "cell_type": "markdown", + "id": "8cfcfa1f-f74a-405b-957d-3c925aca7eb4", + "metadata": {}, + "source": [ + "Xarray is a powerful library for working with labeled multi-dimensional arrays in Python. It provides a convenient and intuitive way to manipulate large and complex datasets, and is built on top of NumPy. CuPy, on the other hand, is a library that allows for GPU-accelerated computing with Python and is compatible with NumPy.\n", + "\n", + "When used together, Xarray and CuPy can provide an easy way to take advantage of GPU acceleration for scientific computing tasks.\n", + "\n", + "Xarray can wrap custom duck array objects (i.e. NumPy-like arrays) that follow specific protocols. \n", + "\n", + "CuPy-Xarray provides an interface for using CuPy in Xarray, providing [accessors](https://docs.xarray.dev/en/stable/internals/extending-xarray.html) on the Xarray objects. \n", + "\n", + "This tutorial showcases the use of `cupy-xarray`, which offers a `cupy` accessor that allows access to cupy-specific features." + ] + }, + { + "cell_type": "markdown", + "id": "77343efb-de6d-423c-b1cd-934c5d6d68e1", + "metadata": {}, + "source": [ + "First, let's import our packages\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "55c72b7d-8899-4e2f-9432-e9cf1531cbdf", + "metadata": {}, + "outputs": [], + "source": [ + "## Import NumPy and CuPy\n", + "import cupy as cp\n", + "import numpy as np\n", + "import xarray as xr\n", + "import cupy_xarray # Adds .cupy to Xarray objects" + ] + }, + { + "cell_type": "markdown", + "id": "4ed42841-264a-4eb6-9f82-be9a463be816", + "metadata": {}, + "source": [ + "### Creating Xarray DataArray with CuPy" + ] + }, + { + "cell_type": "markdown", + "id": "573bb115-0e77-4f86-be9f-9ee6ac1c6f9b", + "metadata": {}, + "source": [ + "In the previous tutorial, we learned how to create a NumPy and CuPy array:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4b91fc01-9e99-4b01-b700-9b4802b7ef14", + "metadata": {}, + "outputs": [], + "source": [ + "arr_cpu = np.random.rand(10, 10, 10)\n", + "arr_gpu = cp.random.rand(10, 10, 10)" + ] + }, + { + "cell_type": "markdown", + "id": "2136402a-0224-4a2d-828f-99d571c4524b", + "metadata": {}, + "source": [ + "We can create the Xarray DataArray using the CuPy array or NumPy array as the data source in a similar fashion:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "67e76867-373b-4011-b7c6-cb9b737e54c7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray (x: 10, y: 10, time: 10)>\n",
+       "array([[[9.86345808e-01, 4.17660665e-01, 1.88327552e-01, 8.90280942e-01,\n",
+       "         2.21689274e-01, 3.17943609e-01, 5.00880587e-01, 7.53337533e-01,\n",
+       "         3.59031996e-01, 1.91030893e-01],\n",
+       "        [1.33144043e-02, 5.02596284e-01, 5.42913172e-01, 5.23846968e-01,\n",
+       "         4.05313585e-01, 9.46118837e-01, 1.06548298e-01, 3.47524404e-01,\n",
+       "         1.52843324e-01, 8.48253778e-01],\n",
+       "        [4.33649929e-01, 6.23728979e-01, 6.47445402e-01, 9.03623126e-01,\n",
+       "         9.05012105e-01, 4.06989322e-03, 4.19896664e-01, 8.60406673e-02,\n",
+       "         7.41788571e-01, 6.22665340e-01],\n",
+       "        [1.74267952e-01, 6.14897148e-01, 5.01242328e-01, 6.66759345e-01,\n",
+       "         8.44182632e-01, 3.19620076e-01, 7.90701915e-01, 2.43897985e-01,\n",
+       "         8.25956047e-01, 6.06534832e-01],\n",
+       "        [5.28111326e-01, 7.42343565e-01, 8.05094324e-02, 8.84691476e-01,\n",
+       "         1.69856723e-02, 3.32512453e-01, 6.67738160e-01, 7.06905069e-01,\n",
+       "         5.16369945e-01, 2.71965903e-01],\n",
+       "        [2.81638568e-01, 2.89389278e-01, 8.19006807e-01, 3.53878654e-01,\n",
+       "         9.21084664e-02, 5.69411698e-01, 8.56797393e-01, 3.24107223e-01,\n",
+       "         8.15087813e-01, 4.70350855e-01],\n",
+       "        [5.70124339e-01, 7.92088214e-01, 9.33540441e-01, 9.88027072e-01,\n",
+       "         9.05585677e-01, 5.28417548e-01, 4.40169554e-01, 1.40924601e-01,\n",
+       "...\n",
+       "         5.29797743e-01, 9.87589722e-01, 9.18635655e-01, 8.68580278e-01,\n",
+       "         4.71548324e-01, 3.64257635e-01],\n",
+       "        [6.42229124e-01, 2.33643023e-02, 5.85033551e-01, 8.80436137e-02,\n",
+       "         7.07996956e-01, 4.40586920e-01, 3.10391741e-01, 1.22763638e-02,\n",
+       "         8.02412664e-01, 4.33761051e-01],\n",
+       "        [1.24780820e-01, 3.53875474e-01, 8.36031716e-01, 2.84138174e-02,\n",
+       "         3.57476794e-01, 2.44890794e-02, 1.47504786e-02, 3.19465404e-01,\n",
+       "         2.91984584e-01, 3.39490525e-01],\n",
+       "        [2.04021642e-01, 4.71267303e-01, 9.03187717e-02, 3.83928128e-01,\n",
+       "         5.96265409e-01, 3.17287239e-01, 3.22413673e-01, 8.38235070e-01,\n",
+       "         9.58316554e-01, 9.73589612e-01],\n",
+       "        [6.13802208e-01, 8.70356525e-01, 5.17350919e-01, 7.72374828e-03,\n",
+       "         5.35340510e-01, 8.89268388e-01, 6.93943330e-01, 6.29953006e-01,\n",
+       "         1.70230716e-01, 7.16573680e-01],\n",
+       "        [8.44214598e-01, 3.35186917e-01, 8.78891352e-01, 1.98027834e-01,\n",
+       "         6.36005433e-01, 1.21753118e-01, 6.48103717e-01, 8.68341345e-01,\n",
+       "         7.81023406e-01, 4.45064620e-01],\n",
+       "        [3.85731750e-01, 8.02230895e-01, 6.41415045e-01, 7.60886886e-01,\n",
+       "         2.00746550e-01, 3.76787007e-01, 6.68073723e-01, 7.87222270e-01,\n",
+       "         6.75273015e-01, 8.63705777e-01]]])\n",
+       "Dimensions without coordinates: x, y, time
" + ], + "text/plain": [ + "\n", + "array([[[9.86345808e-01, 4.17660665e-01, 1.88327552e-01, 8.90280942e-01,\n", + " 2.21689274e-01, 3.17943609e-01, 5.00880587e-01, 7.53337533e-01,\n", + " 3.59031996e-01, 1.91030893e-01],\n", + " [1.33144043e-02, 5.02596284e-01, 5.42913172e-01, 5.23846968e-01,\n", + " 4.05313585e-01, 9.46118837e-01, 1.06548298e-01, 3.47524404e-01,\n", + " 1.52843324e-01, 8.48253778e-01],\n", + " [4.33649929e-01, 6.23728979e-01, 6.47445402e-01, 9.03623126e-01,\n", + " 9.05012105e-01, 4.06989322e-03, 4.19896664e-01, 8.60406673e-02,\n", + " 7.41788571e-01, 6.22665340e-01],\n", + " [1.74267952e-01, 6.14897148e-01, 5.01242328e-01, 6.66759345e-01,\n", + " 8.44182632e-01, 3.19620076e-01, 7.90701915e-01, 2.43897985e-01,\n", + " 8.25956047e-01, 6.06534832e-01],\n", + " [5.28111326e-01, 7.42343565e-01, 8.05094324e-02, 8.84691476e-01,\n", + " 1.69856723e-02, 3.32512453e-01, 6.67738160e-01, 7.06905069e-01,\n", + " 5.16369945e-01, 2.71965903e-01],\n", + " [2.81638568e-01, 2.89389278e-01, 8.19006807e-01, 3.53878654e-01,\n", + " 9.21084664e-02, 5.69411698e-01, 8.56797393e-01, 3.24107223e-01,\n", + " 8.15087813e-01, 4.70350855e-01],\n", + " [5.70124339e-01, 7.92088214e-01, 9.33540441e-01, 9.88027072e-01,\n", + " 9.05585677e-01, 5.28417548e-01, 4.40169554e-01, 1.40924601e-01,\n", + "...\n", + " 5.29797743e-01, 9.87589722e-01, 9.18635655e-01, 8.68580278e-01,\n", + " 4.71548324e-01, 3.64257635e-01],\n", + " [6.42229124e-01, 2.33643023e-02, 5.85033551e-01, 8.80436137e-02,\n", + " 7.07996956e-01, 4.40586920e-01, 3.10391741e-01, 1.22763638e-02,\n", + " 8.02412664e-01, 4.33761051e-01],\n", + " [1.24780820e-01, 3.53875474e-01, 8.36031716e-01, 2.84138174e-02,\n", + " 3.57476794e-01, 2.44890794e-02, 1.47504786e-02, 3.19465404e-01,\n", + " 2.91984584e-01, 3.39490525e-01],\n", + " [2.04021642e-01, 4.71267303e-01, 9.03187717e-02, 3.83928128e-01,\n", + " 5.96265409e-01, 3.17287239e-01, 3.22413673e-01, 8.38235070e-01,\n", + " 9.58316554e-01, 9.73589612e-01],\n", + " [6.13802208e-01, 8.70356525e-01, 5.17350919e-01, 7.72374828e-03,\n", + " 5.35340510e-01, 8.89268388e-01, 6.93943330e-01, 6.29953006e-01,\n", + " 1.70230716e-01, 7.16573680e-01],\n", + " [8.44214598e-01, 3.35186917e-01, 8.78891352e-01, 1.98027834e-01,\n", + " 6.36005433e-01, 1.21753118e-01, 6.48103717e-01, 8.68341345e-01,\n", + " 7.81023406e-01, 4.45064620e-01],\n", + " [3.85731750e-01, 8.02230895e-01, 6.41415045e-01, 7.60886886e-01,\n", + " 2.00746550e-01, 3.76787007e-01, 6.68073723e-01, 7.87222270e-01,\n", + " 6.75273015e-01, 8.63705777e-01]]])\n", + "Dimensions without coordinates: x, y, time" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# create a DataArray using CuPy array with three dimensions and 10 elements along each dimension\n", + "da_np = xr.DataArray(arr_cpu, dims=[\"x\", \"y\", \"time\"])\n", + "\n", + "da_np" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "431edeb0-661f-4929-a83d-e39a6e753a60", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray (x: 10, y: 10, time: 10)>\n",
+       "array([[[2.88196731e-01, 3.71102840e-01, 8.22413516e-01, 7.61603373e-01,\n",
+       "         2.14247694e-01, 6.08972260e-01, 6.35605124e-01, 4.51735394e-02,\n",
+       "         3.56580833e-01, 3.33245593e-01],\n",
+       "        [9.56686233e-02, 3.09634487e-01, 5.72034429e-01, 8.64203361e-01,\n",
+       "         5.44551902e-01, 4.54445926e-01, 1.21606888e-01, 2.30160410e-01,\n",
+       "         6.14639953e-01, 7.73246535e-01],\n",
+       "        [8.03011705e-01, 2.69969912e-01, 2.03781951e-01, 6.64806547e-01,\n",
+       "         4.93709552e-01, 2.60248353e-01, 6.82195033e-01, 6.75837492e-01,\n",
+       "         5.07293067e-01, 6.45924343e-01],\n",
+       "        [1.03968071e-01, 1.31787260e-01, 2.31666523e-02, 2.90727455e-01,\n",
+       "         6.22514068e-02, 9.54996781e-01, 1.38868633e-01, 3.18043546e-01,\n",
+       "         9.94141764e-01, 6.52825114e-01],\n",
+       "        [6.72144360e-01, 9.25109790e-01, 9.24907616e-01, 9.97835547e-01,\n",
+       "         1.30089788e-01, 3.28381980e-01, 9.47761645e-01, 2.15451004e-01,\n",
+       "         1.55072912e-01, 2.84564825e-01],\n",
+       "        [5.32157180e-01, 4.05812774e-01, 6.65152077e-01, 1.62793186e-01,\n",
+       "         8.38375837e-01, 4.38498164e-01, 3.93970103e-01, 3.25181026e-01,\n",
+       "         8.43314943e-01, 6.37218468e-01],\n",
+       "        [9.47935236e-01, 1.39071514e-01, 3.34994498e-01, 7.42907508e-01,\n",
+       "         1.13865457e-01, 3.69531071e-01, 6.58907523e-01, 4.10997683e-01,\n",
+       "...\n",
+       "         5.01101857e-01, 6.76530919e-01, 6.01550513e-01, 1.91761020e-01,\n",
+       "         2.01591335e-01, 3.73443454e-01],\n",
+       "        [8.72935075e-01, 9.28175014e-01, 7.03819938e-01, 4.25757273e-01,\n",
+       "         6.80355431e-01, 1.22351044e-01, 8.22086635e-03, 9.23118431e-01,\n",
+       "         8.00040998e-02, 3.51963004e-01],\n",
+       "        [5.30917733e-01, 1.73025731e-03, 5.46551386e-01, 3.41904305e-01,\n",
+       "         6.11276326e-01, 7.83903426e-01, 7.67650251e-01, 9.27383669e-02,\n",
+       "         5.99146336e-01, 1.44674661e-02],\n",
+       "        [9.32478257e-02, 6.51279678e-01, 3.40032365e-01, 6.66761485e-02,\n",
+       "         3.88243075e-01, 3.06181721e-02, 5.58666002e-01, 3.10356676e-01,\n",
+       "         6.46523629e-01, 1.19013418e-01],\n",
+       "        [1.81940990e-01, 3.89650142e-01, 9.98204973e-01, 4.39178186e-02,\n",
+       "         6.88137446e-02, 7.61541679e-02, 6.26075251e-01, 9.14708720e-01,\n",
+       "         4.45414011e-01, 5.16678456e-01],\n",
+       "        [8.51618677e-01, 6.81900815e-01, 6.66821786e-01, 8.75685884e-01,\n",
+       "         2.90499242e-01, 3.25977864e-01, 3.67627054e-01, 3.93770674e-01,\n",
+       "         7.40898577e-01, 3.50451112e-02],\n",
+       "        [7.06374026e-01, 7.19519511e-01, 1.79160522e-01, 8.81425785e-01,\n",
+       "         3.51431945e-01, 4.11507382e-01, 6.86088790e-01, 3.04671156e-01,\n",
+       "         5.70729870e-01, 7.76584760e-01]]])\n",
+       "Dimensions without coordinates: x, y, time
" + ], + "text/plain": [ + "\n", + "array([[[2.88196731e-01, 3.71102840e-01, 8.22413516e-01, 7.61603373e-01,\n", + " 2.14247694e-01, 6.08972260e-01, 6.35605124e-01, 4.51735394e-02,\n", + " 3.56580833e-01, 3.33245593e-01],\n", + " [9.56686233e-02, 3.09634487e-01, 5.72034429e-01, 8.64203361e-01,\n", + " 5.44551902e-01, 4.54445926e-01, 1.21606888e-01, 2.30160410e-01,\n", + " 6.14639953e-01, 7.73246535e-01],\n", + " [8.03011705e-01, 2.69969912e-01, 2.03781951e-01, 6.64806547e-01,\n", + " 4.93709552e-01, 2.60248353e-01, 6.82195033e-01, 6.75837492e-01,\n", + " 5.07293067e-01, 6.45924343e-01],\n", + " [1.03968071e-01, 1.31787260e-01, 2.31666523e-02, 2.90727455e-01,\n", + " 6.22514068e-02, 9.54996781e-01, 1.38868633e-01, 3.18043546e-01,\n", + " 9.94141764e-01, 6.52825114e-01],\n", + " [6.72144360e-01, 9.25109790e-01, 9.24907616e-01, 9.97835547e-01,\n", + " 1.30089788e-01, 3.28381980e-01, 9.47761645e-01, 2.15451004e-01,\n", + " 1.55072912e-01, 2.84564825e-01],\n", + " [5.32157180e-01, 4.05812774e-01, 6.65152077e-01, 1.62793186e-01,\n", + " 8.38375837e-01, 4.38498164e-01, 3.93970103e-01, 3.25181026e-01,\n", + " 8.43314943e-01, 6.37218468e-01],\n", + " [9.47935236e-01, 1.39071514e-01, 3.34994498e-01, 7.42907508e-01,\n", + " 1.13865457e-01, 3.69531071e-01, 6.58907523e-01, 4.10997683e-01,\n", + "...\n", + " 5.01101857e-01, 6.76530919e-01, 6.01550513e-01, 1.91761020e-01,\n", + " 2.01591335e-01, 3.73443454e-01],\n", + " [8.72935075e-01, 9.28175014e-01, 7.03819938e-01, 4.25757273e-01,\n", + " 6.80355431e-01, 1.22351044e-01, 8.22086635e-03, 9.23118431e-01,\n", + " 8.00040998e-02, 3.51963004e-01],\n", + " [5.30917733e-01, 1.73025731e-03, 5.46551386e-01, 3.41904305e-01,\n", + " 6.11276326e-01, 7.83903426e-01, 7.67650251e-01, 9.27383669e-02,\n", + " 5.99146336e-01, 1.44674661e-02],\n", + " [9.32478257e-02, 6.51279678e-01, 3.40032365e-01, 6.66761485e-02,\n", + " 3.88243075e-01, 3.06181721e-02, 5.58666002e-01, 3.10356676e-01,\n", + " 6.46523629e-01, 1.19013418e-01],\n", + " [1.81940990e-01, 3.89650142e-01, 9.98204973e-01, 4.39178186e-02,\n", + " 6.88137446e-02, 7.61541679e-02, 6.26075251e-01, 9.14708720e-01,\n", + " 4.45414011e-01, 5.16678456e-01],\n", + " [8.51618677e-01, 6.81900815e-01, 6.66821786e-01, 8.75685884e-01,\n", + " 2.90499242e-01, 3.25977864e-01, 3.67627054e-01, 3.93770674e-01,\n", + " 7.40898577e-01, 3.50451112e-02],\n", + " [7.06374026e-01, 7.19519511e-01, 1.79160522e-01, 8.81425785e-01,\n", + " 3.51431945e-01, 4.11507382e-01, 6.86088790e-01, 3.04671156e-01,\n", + " 5.70729870e-01, 7.76584760e-01]]])\n", + "Dimensions without coordinates: x, y, time" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# create a DataArray using NumPy array with three dimensions and 10 elements along each dimension\n", + "da_cp = xr.DataArray(arr_gpu, dims=[\"x\", \"y\", \"time\"])\n", + "\n", + "da_cp" + ] + }, + { + "cell_type": "markdown", + "id": "aa9a4672-f11a-460b-b5d8-18eb071ef932", + "metadata": {}, + "source": [ + "But how are these two DataArrays different from each other? How do we know which array is on CPU vs. GPU?" + ] + }, + { + "cell_type": "markdown", + "id": "8eb97bee-48da-494e-b24d-46ad9f9658c9", + "metadata": {}, + "source": [ + "### Checking for CuPy Arrays\n", + "\n", + "The `cupy` accessor provides the `is_cupy` method to check if these arrays are on the host or device. For example:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f0b43b25-b0d9-4809-9b11-729077b81d4e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "da_np.cupy.is_cupy" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "883128df-fdca-4603-9797-52e6d53ced7f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "da_cp.cupy.is_cupy" + ] + }, + { + "cell_type": "markdown", + "id": "fa0f35af-52a6-439c-ad8e-b95d4d82c085", + "metadata": {}, + "source": [ + "### Accessing Device Information of the DataArray" + ] + }, + { + "cell_type": "markdown", + "id": "a3190b98-85d2-431f-90bc-2fb6775d7998", + "metadata": {}, + "source": [ + "To access the underlying CuPy array, use the `data` property of the DataArray. It returns the CuPy array:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "ea95ef0d-d7b2-4ee0-9ebf-761461b3e044", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "cupy.ndarray" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cupy_array = da_cp.data\n", + "type(cupy_array)" + ] + }, + { + "cell_type": "markdown", + "id": "1e0bf47d-491e-448c-8662-6e7d24149016", + "metadata": {}, + "source": [ + "In the previous tutorial, we learned about CuPy's introduction of the notion of a current device. We also learned that to identify the device assigned to a CuPy array, the `cupy.ndarray.device` attribute can be used. Similar concept can be applied to a DataArray:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b70eb4b1-42ce-499a-bb15-1831351edd8b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "da_cp.data.device" + ] + }, + { + "cell_type": "markdown", + "id": "dc466f07-b0ef-4f02-bca6-9f25fc810b0e", + "metadata": {}, + "source": [ + "### Data Transfer\n", + "\n", + "#### Transferring DataArrays to another Device\n", + "\n", + "In the previous lesson we learned that by default, code execution is carried out on Device 0. However, with CuPy, we have the ability to transfer arrays to other devices using cp.cuda.Device(). This feature becomes particularly valuable when your code is designed to leverage the capabilities of multiple GPUs. Similar concept applies to DataArrays that include Cupy Arrays:\n", + "\n", + "``` python \n", + "with cp.cuda.Device(1):\n", + " x_on_gpu1 = cp.array([5, 7, 8, 5, 5])\n", + " da_cp1 = xr.DataArray(x_on_gpu1, dims=['time'])\n", + "\n", + "da_cp1.data.device # 1\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "367f9364-f31e-44b9-ae59-784d9361d509", + "metadata": {}, + "source": [ + "#### Transferring Data between Host and Device\n", + "Xarray provides `DataArray.as_numpy` to convert all kinds of arrays to NumPy arrays.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "4cf26810-e889-47e9-8750-860841f5876c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray (x: 10, y: 10, time: 10)>\n",
+       "array([[[2.88196731e-01, 3.71102840e-01, 8.22413516e-01, 7.61603373e-01,\n",
+       "         2.14247694e-01, 6.08972260e-01, 6.35605124e-01, 4.51735394e-02,\n",
+       "         3.56580833e-01, 3.33245593e-01],\n",
+       "        [9.56686233e-02, 3.09634487e-01, 5.72034429e-01, 8.64203361e-01,\n",
+       "         5.44551902e-01, 4.54445926e-01, 1.21606888e-01, 2.30160410e-01,\n",
+       "         6.14639953e-01, 7.73246535e-01],\n",
+       "        [8.03011705e-01, 2.69969912e-01, 2.03781951e-01, 6.64806547e-01,\n",
+       "         4.93709552e-01, 2.60248353e-01, 6.82195033e-01, 6.75837492e-01,\n",
+       "         5.07293067e-01, 6.45924343e-01],\n",
+       "        [1.03968071e-01, 1.31787260e-01, 2.31666523e-02, 2.90727455e-01,\n",
+       "         6.22514068e-02, 9.54996781e-01, 1.38868633e-01, 3.18043546e-01,\n",
+       "         9.94141764e-01, 6.52825114e-01],\n",
+       "        [6.72144360e-01, 9.25109790e-01, 9.24907616e-01, 9.97835547e-01,\n",
+       "         1.30089788e-01, 3.28381980e-01, 9.47761645e-01, 2.15451004e-01,\n",
+       "         1.55072912e-01, 2.84564825e-01],\n",
+       "        [5.32157180e-01, 4.05812774e-01, 6.65152077e-01, 1.62793186e-01,\n",
+       "         8.38375837e-01, 4.38498164e-01, 3.93970103e-01, 3.25181026e-01,\n",
+       "         8.43314943e-01, 6.37218468e-01],\n",
+       "        [9.47935236e-01, 1.39071514e-01, 3.34994498e-01, 7.42907508e-01,\n",
+       "         1.13865457e-01, 3.69531071e-01, 6.58907523e-01, 4.10997683e-01,\n",
+       "...\n",
+       "         5.01101857e-01, 6.76530919e-01, 6.01550513e-01, 1.91761020e-01,\n",
+       "         2.01591335e-01, 3.73443454e-01],\n",
+       "        [8.72935075e-01, 9.28175014e-01, 7.03819938e-01, 4.25757273e-01,\n",
+       "         6.80355431e-01, 1.22351044e-01, 8.22086635e-03, 9.23118431e-01,\n",
+       "         8.00040998e-02, 3.51963004e-01],\n",
+       "        [5.30917733e-01, 1.73025731e-03, 5.46551386e-01, 3.41904305e-01,\n",
+       "         6.11276326e-01, 7.83903426e-01, 7.67650251e-01, 9.27383669e-02,\n",
+       "         5.99146336e-01, 1.44674661e-02],\n",
+       "        [9.32478257e-02, 6.51279678e-01, 3.40032365e-01, 6.66761485e-02,\n",
+       "         3.88243075e-01, 3.06181721e-02, 5.58666002e-01, 3.10356676e-01,\n",
+       "         6.46523629e-01, 1.19013418e-01],\n",
+       "        [1.81940990e-01, 3.89650142e-01, 9.98204973e-01, 4.39178186e-02,\n",
+       "         6.88137446e-02, 7.61541679e-02, 6.26075251e-01, 9.14708720e-01,\n",
+       "         4.45414011e-01, 5.16678456e-01],\n",
+       "        [8.51618677e-01, 6.81900815e-01, 6.66821786e-01, 8.75685884e-01,\n",
+       "         2.90499242e-01, 3.25977864e-01, 3.67627054e-01, 3.93770674e-01,\n",
+       "         7.40898577e-01, 3.50451112e-02],\n",
+       "        [7.06374026e-01, 7.19519511e-01, 1.79160522e-01, 8.81425785e-01,\n",
+       "         3.51431945e-01, 4.11507382e-01, 6.86088790e-01, 3.04671156e-01,\n",
+       "         5.70729870e-01, 7.76584760e-01]]])\n",
+       "Dimensions without coordinates: x, y, time
" + ], + "text/plain": [ + "\n", + "array([[[2.88196731e-01, 3.71102840e-01, 8.22413516e-01, 7.61603373e-01,\n", + " 2.14247694e-01, 6.08972260e-01, 6.35605124e-01, 4.51735394e-02,\n", + " 3.56580833e-01, 3.33245593e-01],\n", + " [9.56686233e-02, 3.09634487e-01, 5.72034429e-01, 8.64203361e-01,\n", + " 5.44551902e-01, 4.54445926e-01, 1.21606888e-01, 2.30160410e-01,\n", + " 6.14639953e-01, 7.73246535e-01],\n", + " [8.03011705e-01, 2.69969912e-01, 2.03781951e-01, 6.64806547e-01,\n", + " 4.93709552e-01, 2.60248353e-01, 6.82195033e-01, 6.75837492e-01,\n", + " 5.07293067e-01, 6.45924343e-01],\n", + " [1.03968071e-01, 1.31787260e-01, 2.31666523e-02, 2.90727455e-01,\n", + " 6.22514068e-02, 9.54996781e-01, 1.38868633e-01, 3.18043546e-01,\n", + " 9.94141764e-01, 6.52825114e-01],\n", + " [6.72144360e-01, 9.25109790e-01, 9.24907616e-01, 9.97835547e-01,\n", + " 1.30089788e-01, 3.28381980e-01, 9.47761645e-01, 2.15451004e-01,\n", + " 1.55072912e-01, 2.84564825e-01],\n", + " [5.32157180e-01, 4.05812774e-01, 6.65152077e-01, 1.62793186e-01,\n", + " 8.38375837e-01, 4.38498164e-01, 3.93970103e-01, 3.25181026e-01,\n", + " 8.43314943e-01, 6.37218468e-01],\n", + " [9.47935236e-01, 1.39071514e-01, 3.34994498e-01, 7.42907508e-01,\n", + " 1.13865457e-01, 3.69531071e-01, 6.58907523e-01, 4.10997683e-01,\n", + "...\n", + " 5.01101857e-01, 6.76530919e-01, 6.01550513e-01, 1.91761020e-01,\n", + " 2.01591335e-01, 3.73443454e-01],\n", + " [8.72935075e-01, 9.28175014e-01, 7.03819938e-01, 4.25757273e-01,\n", + " 6.80355431e-01, 1.22351044e-01, 8.22086635e-03, 9.23118431e-01,\n", + " 8.00040998e-02, 3.51963004e-01],\n", + " [5.30917733e-01, 1.73025731e-03, 5.46551386e-01, 3.41904305e-01,\n", + " 6.11276326e-01, 7.83903426e-01, 7.67650251e-01, 9.27383669e-02,\n", + " 5.99146336e-01, 1.44674661e-02],\n", + " [9.32478257e-02, 6.51279678e-01, 3.40032365e-01, 6.66761485e-02,\n", + " 3.88243075e-01, 3.06181721e-02, 5.58666002e-01, 3.10356676e-01,\n", + " 6.46523629e-01, 1.19013418e-01],\n", + " [1.81940990e-01, 3.89650142e-01, 9.98204973e-01, 4.39178186e-02,\n", + " 6.88137446e-02, 7.61541679e-02, 6.26075251e-01, 9.14708720e-01,\n", + " 4.45414011e-01, 5.16678456e-01],\n", + " [8.51618677e-01, 6.81900815e-01, 6.66821786e-01, 8.75685884e-01,\n", + " 2.90499242e-01, 3.25977864e-01, 3.67627054e-01, 3.93770674e-01,\n", + " 7.40898577e-01, 3.50451112e-02],\n", + " [7.06374026e-01, 7.19519511e-01, 1.79160522e-01, 8.81425785e-01,\n", + " 3.51431945e-01, 4.11507382e-01, 6.86088790e-01, 3.04671156e-01,\n", + " 5.70729870e-01, 7.76584760e-01]]])\n", + "Dimensions without coordinates: x, y, time" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Move data to host\n", + "da_np = da_cp.as_numpy()\n", + "da_np" + ] + }, + { + "cell_type": "markdown", + "id": "fd1f4133-310b-4780-a71e-0599518eefeb", + "metadata": {}, + "source": [ + "Let’s confirm this isn’t a CuPy array anymore:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "b071bc93-e5dc-4185-b905-1842da06a45f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "da_np.cupy.is_cupy" + ] + }, + { + "cell_type": "markdown", + "id": "305c68f4-bdf8-4be4-be95-3a8bf005d122", + "metadata": {}, + "source": [ + "We also can convert an Xarray DataArray that include NumPy array to a CuPy array (move data to Device) use `cupy.as_cupy()`:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "7dccf500-b6ac-498b-a869-2db52b90a083", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Move data to GPU\n", + "da_cp = da_np.cupy.as_cupy()\n", + "da_cp.as_cupy().cupy.is_cupy" + ] + }, + { + "cell_type": "markdown", + "id": "0888781c-d9dd-4d1e-9014-72beb3bfcc56", + "metadata": {}, + "source": [ + "### Plotting\n", + "\n", + "Plotting DataArrays with underlying data as CuPy arrays work in the same way as DataArrays with Numpy Arrays; however, data is first transferred to CPU before being plotted. " + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "95552a84-ca06-4483-9db8-31459c10edd3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(array([111., 93., 96., 112., 88., 93., 115., 88., 98., 106.]),\n", + " array([5.01237631e-05, 9.98656087e-02, 1.99681094e-01, 2.99496578e-01,\n", + " 3.99312063e-01, 4.99127548e-01, 5.98943033e-01, 6.98758518e-01,\n", + " 7.98574003e-01, 8.98389488e-01, 9.98204973e-01]),\n", + " )" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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qq6uDttfXV2t3NzcnhgJAAAY0mO/4pk3b56uueYajR49Wueff76WLl2qDz/8ULNnz+6pkQAAgBE9FihXXXWVPvnkE917773avXu3RowYoeeff15Dhw7tqZEkffHrpHvuuafLr5QQXaxz92Gtuwfr3D1Y5+7T02vtcd/ksz4AAADdiO/iAQAA5hAoAADAHAIFAACYQ6AAAABzCBQAAGDOcRkoixcvVlZWlvr376+cnBytW7fua4+vqalRTk6O+vfvr+HDh+uRRx7ppknjWyTr/PTTT2vChAn6zne+o9TUVJ1//vl66aWXunHa+BXpz3On1157TQkJCTrrrLNiO2AvEulat7e3q7i4WEOHDpXX69V3v/td/f73v++maeNXpOv8xBNPaNSoUUpOTlZGRoauu+46ffLJJ900bXxau3atJk2apEAgII/Ho2eeeeaoj+n210J3nFm5cqVLTEx0y5Ytc9u2bXNz5851AwYMcDt37jzi8Tt27HDJyclu7ty5btu2bW7ZsmUuMTHRPfXUU908eXyJdJ3nzp3r7r//fvfGG2+47du3u6KiIpeYmOg2bdrUzZPHl0jXudNnn33mhg8f7vLz892oUaO6Z9g4dyxrPXnyZDdmzBhXXV3t6uvr3b/+9S/32muvdePU8SfSdV63bp3r06eP+93vfud27Njh1q1b58444wx3xRVXdPPk8eX55593xcXF7s9//rOT5FavXv21x/fEa+FxFyjnnXeemz17dti+U0891RUWFh7x+Pnz57tTTz01bN+sWbPc97///ZjN2BtEus5Hcvrpp7sFCxZEe7Re5VjX+aqrrnJ33nmnu+eeewiUbyjStX7hhRdcWlqa++STT7pjvF4j0nV+4IEH3PDhw8P2PfTQQ27IkCExm7G3+SaB0hOvhcfVr3g6OjpUV1en/Pz8sP35+fmqra094mNef/31Lsdfcskl2rhxow4cOBCzWePZsazzlx0+fFitra0aOHBgLEbsFY51nZcvX673339f99xzT6xH7DWOZa2fffZZjR49WgsXLtRJJ52kU045Rbfddpva2tq6Y+S4dCzrnJubq127dun555+Xc07//e9/9dRTT2nixIndMfJxoydeC3vsT933hD179ujQoUNdvjHZ5/N1+WblTo2NjUc8/uDBg9qzZ48yMjJiNm+8OpZ1/rIHH3xQ+/bt09SpU2MxYq9wLOv87rvvqrCwUOvWrVNCwnH1X/9v5VjWeseOHVq/fr369++v1atXa8+ePZozZ44+/fRT3ofyFY5lnXNzc/XEE0/oqquu0ueff66DBw9q8uTJWrRoUXeMfNzoidfC4+oKSiePxxN22znXZd/Rjj/SfoSLdJ07PfnkkyopKdGqVas0ePDgWI3Xa3zTdT506JCmTZumBQsW6JRTTumu8XqVSH6mDx8+LI/HoyeeeELnnXeeLrvsMpWXl+uxxx7jKspRRLLO27Zt080336y7775bdXV1evHFF1VfX88Xz8ZAd78WHlf/F2rQoEHq27dvlxJvamrqUoad/H7/EY9PSEhQenp6zGaNZ8eyzp1WrVqlmTNn6k9/+pPGjx8fyzHjXqTr3Nraqo0bN2rz5s268cYbJX3xIuqcU0JCgtasWaMf/vCH3TJ7vDmWn+mMjAyddNJJSktLC+077bTT5JzTrl27lJ2dHdOZ49GxrHNZWZnGjh2r22+/XZJ05plnasCAAbrwwgv1q1/9iqvcUdITr4XH1RWUfv36KScnR9XV1WH7q6urlZube8THnH/++V2OX7NmjUaPHq3ExMSYzRrPjmWdpS+unFx77bWqqqri98ffQKTrnJqaqjfffFNbtmwJbbNnz9b3vvc9bdmyRWPGjOmu0ePOsfxMjx07Vh9//LH27t0b2rd9+3b16dNHQ4YMiem88epY1nn//v3q0yf8paxv376S/u//4ePb65HXwpi9/daozo+wPfroo27btm2uoKDADRgwwH3wwQfOOecKCwvdNddcEzq+86NVt9xyi9u2bZt79NFH+ZjxNxDpOldVVbmEhAT38MMPu927d4e2zz77rKeeQlyIdJ2/jE/xfHORrnVra6sbMmSIu/LKK93WrVtdTU2Ny87Odtdff31PPYW4EOk6L1++3CUkJLjFixe7999/361fv96NHj3anXfeeT31FOJCa2ur27x5s9u8ebOT5MrLy93mzZtDH+e28Fp43AWKc849/PDDbujQoa5fv37unHPOcTU1NaH7ZsyY4caNGxd2/KuvvurOPvts169fPzds2DC3ZMmSbp44PkWyzuPGjXOSumwzZszo/sHjTKQ/z/8fgRKZSNf67bffduPHj3dJSUluyJAhbt68eW7//v3dPHX8iXSdH3roIXf66ae7pKQkl5GR4aZPn+527drVzVPHl1deeeVr/zfXwmuhxzmugQEAAFuOq/egAACA+ECgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgzv8Aj/AJlq+rLaIAAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "da_cp.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "4cb39238-124e-4282-a7f9-314667cc87d2", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "In this notebook, we have learned about:\n", + "\n", + "* CuPy-Xarray Basics\n", + "* Data Transfer between Device to Host \n", + "\n", + "```{seealso}\n", + "[CuPy User Guide](https://docs.cupy.dev/en/stable/user_guide/index.html) \n", + "[Xarray User Guide](https://docs.xarray.dev/en/stable/user-guide/index.html) \n", + "[Cupy-Xarray Github](https://github.com/xarray-contrib/cupy-xarray.git)\n", + "```" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/real-example-1.ipynb b/docs/source/real-example-1.ipynb new file mode 100644 index 0000000..2c074c7 --- /dev/null +++ b/docs/source/real-example-1.ipynb @@ -0,0 +1,348 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "93434031-d7fe-4322-a9cf-41e5b8be622d", + "metadata": {}, + "source": [ + "# A real world example" + ] + }, + { + "cell_type": "markdown", + "id": "4c0164c9-f970-4206-964a-d1d8080e2b0a", + "metadata": {}, + "source": [ + "## Introduction \n", + "\n", + "In the previous tutorial, we introduced the powerful combination of Xarray and CuPy for handling multi-dimensional datasets and leveraging GPU acceleration to significantly improve performance. We explored high-level Xarray functions such as groupby, rolling mean, and weighted mean, and compared their execution times with traditional NumPy-based implementations. In this tutorial, we will dive deeper into the subject with a hands-on approach, utilizing a real-world dataset. This will enable us to better understand the practical applications of Xarray and CuPy and how they can be efficiently utilized for real-life data analysis tasks." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "377ac7ca-6c09-486f-95cc-8a2d599df800", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import xarray as xr" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "447116b1-bc54-4d0a-af46-5908a7f95e93", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import cupy as cp\n", + "import cupy_xarray # Adds .cupy to Xarray objects" + ] + }, + { + "cell_type": "markdown", + "id": "6d6011e6-4f66-4512-960c-b4683deef17b", + "metadata": {}, + "source": [ + "#### Reading data" + ] + }, + { + "cell_type": "markdown", + "id": "1421d75b-b42d-4abb-9a7b-75f85431f1b4", + "metadata": {}, + "source": [ + "Here we read in a small portion of the data available from the [NEX-GDDP-CMIP6 dataset](https://registry.opendata.aws/nex-gddp-cmip6/) available through the registry of open data on AWS." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2def3d71-3d0e-4d59-9898-5dec3d43200f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import s3fs\n", + "\n", + "fs = s3fs.S3FileSystem(anon=True, default_fill_cache=False)\n", + "\n", + "scenario = \"ssp245\"\n", + "var = \"tasmax\"\n", + "years = list(range(2020, 2022))\n", + "\n", + "file_objs = [\n", + " fs.open(\n", + " f\"nex-gddp-cmip6/NEX-GDDP-CMIP6/ACCESS-CM2/{scenario}/r1i1p1f1/{var}/{var}_day_ACCESS-CM2_{scenario}_r1i1p1f1_gn_{year}.nc\"\n", + " )\n", + " for year in years\n", + "]\n", + "da = xr.open_mfdataset(file_objs, engine=\"h5netcdf\")[var].load()" + ] + }, + { + "cell_type": "markdown", + "id": "d3d10f6d-74a3-494d-a564-a780415373b2", + "metadata": {}, + "source": [ + "We can convert the underlying numpy array to cupy array using `cupy.as_cupy()`. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7c4a7a06-452c-452a-8bae-2137dc522336", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "da = da.as_cupy()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f7271377-6eb0-435c-9d55-0f6d954e1af4", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Check if data is cupy Array\n", + "da.cupy.is_cupy" + ] + }, + { + "cell_type": "markdown", + "id": "5e5cc29d-7df7-4f33-9ce0-0dd185d94a48", + "metadata": {}, + "source": [ + "As a first step, let's calculate the mean global temperature " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "6f908d95-21fc-439c-9786-099407bcedd7", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 389 ms, sys: 1.77 ms, total: 391 ms\n", + "Wall time: 391 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "# Calculate the mean global temperature\n", + "da_mean = da.mean(dim=[\"lat\", \"lon\"]).compute()\n", + "da_mean.cupy.is_cupy" + ] + }, + { + "cell_type": "markdown", + "id": "5594b382-f3f2-4e0e-ac6c-271894d7aa21", + "metadata": {}, + "source": [ + "### Groupby\n", + "The groupby function is used to group data based on one or more dimensions. Here, we'll group our data by the 'time' dimension using CuPy:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "d53ba777-d34e-4d26-b193-23af17a27593", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 44.2 ms, sys: 0 ns, total: 44.2 ms\n", + "Wall time: 47 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "climo_da = da.groupby(\"time.month\").mean(\"time\").compute()\n", + "climo_da.cupy.is_cupy" + ] + }, + { + "cell_type": "markdown", + "id": "26dd9613-a4bd-417f-98b1-b525b9deadf3", + "metadata": {}, + "source": [ + "## Advanced workflows and automatic parallelization using `apply_ufunc`\n", + "\n", + "`xr.apply_ufunc()` can automate embarrassingly parallel “map” type operations where a function written for processing NumPy arrays, but we want to apply it on our Xarray DataArray.\n", + "\n", + "`xr.apply_ufunc()` give users capability to run custom-written functions such as parameter calculations in a parallel way. \n", + "\n", + "In the example below, we calculate the saturation vapor pressure by using apply_ufunc() to apply this function to our Dask Array chunk by chunk." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "1339782d-a9df-484f-9ae9-3fb35f909833", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# return saturation vapor pressure\n", + "# using Clausius-Clapeyron equation\n", + "def sat_p(t):\n", + " return 0.611 * cp.exp(17.67 * (t - 273.15) * ((t - 29.65) ** (-1)))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "966928d2-7159-4aa2-b7c2-7646110ace45", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n", + "CPU times: user 102 ms, sys: 11.2 ms, total: 113 ms\n", + "Wall time: 119 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "es = xr.apply_ufunc(sat_p, da, output_dtypes=[float]).rename(\"saturation_vapor_pressure\")\n", + "print(es.cupy.is_cupy)" + ] + }, + { + "cell_type": "markdown", + "id": "635b7f63-7c44-4440-a9f0-9dd682afb994", + "metadata": {}, + "source": [ + "### Add Plotting" + ] + }, + { + "cell_type": "markdown", + "id": "b26f1c51-f404-41e6-a465-5dfb6e36b530", + "metadata": {}, + "source": [ + "We can plot the result, which will involve the data *automatically* being transferred to the host" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "b461d342-28af-481f-9850-0527b501a271", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjYAAAHFCAYAAADhWLMfAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy88F64QAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOx9eZwUxfn+U9U9114cghwR8SIeoImCtwlEBXZjPHOqUaNiYjASwDtqwANQjIBRo0Gj4u03h8b8zAJGkUTBiFeimESjqGgkaESuPWa6q35/1NHVPd1z7eyyu/Tz+cxnZnq6q6t7uqufet/nfV/COeeIESNGjBgxYsToBaDbugMxYsSIESNGjBjVQkxsYsSIESNGjBi9BjGxiREjRowYMWL0GsTEJkaMGDFixIjRaxATmxgxYsSIESNGr0FMbGLEiBEjRowYvQYxsYkRI0aMGDFi9BrExCZGjBgxYsSI0WsQE5sYMWLEiBEjRq9BTGxiVAUrVqzAzJkz8dlnn+X9Nm7cOIwbN67L+9QVePPNN3HhhRdi9OjR6Nu3L/r374/DDz8cv/nNb0LXX79+Pb73ve9hwIABqKmpwaGHHoqnnnrKt86mTZswa9YsjBs3DoMHD0ZdXR323XdfXH/99Whra8trM5fL4aqrrsIuu+yCVCqFvfbaCzfffHNZx1FKvwDg//2//4fTTz8d++67LxKJBAghZe1H4eabb8Zee+2FVCqFXXfdFVdddRVyuZxvnQ8++ABTp07F2LFj0bdvXxBCcM899/jWmTlzJgghRV/q+lu9ejUmT56MQw89FLW1tSCE4Jlnnsnr30cffYQrrrgChx56KAYMGICGhgaMHj0aCxcuhOu6JR/nO++8g5NOOgl9+/ZFXV0dxo8fj5dffjl03Ycffhhf/OIXkU6nMXToUEydOhVbtmzplvuKEaNbg8eIUQXccMMNHABfs2ZN3m+rV6/mq1ev7vpOdQFuvvlmvtdee/FZs2bxpUuX8j/+8Y/8jDPO4AD4VVdd5Vu3ra2Njxo1iu+00078/vvv50uXLuXHH388t22bP/PMM3q91157jQ8YMIBPmzaN//73v+dPPfUUnzlzJk+n0/yoo47ijDFfu5MmTeKpVIrPnTuXL1u2jF966aWcEMJnzZpV0jGU2i/OOT/rrLP4iBEj+Le+9S0+evRoXskQcu2113JCCL/sssv4smXL+Ny5c3kymeTnnHOOb71ly5bxAQMG8KOPPpqffPLJHAC/++67feusXbuWr1y5Ur9+97vfcQD8/PPP9y1X198999zDhwwZwr/61a/yY489lgPgy5Yty+vjH/7wBz5s2DB++eWX8yeeeIIvXbqUT5s2jVNK+ZlnnlnSca5fv54PHTqUjxw5kv/2t7/lTzzxBD/iiCN4fX09/+c//+lb9/777+cA+KRJk/jTTz/Nb7/9dt6nTx8+fvz4brevGDG6O2JiE6MqKERsejM+/vjjPKLBOefHHHMMr6mp4W1tbXrZrbfeygHwFStW6GW5XI7vs88+/KCDDtLLtmzZwrds2ZLXpjrHf/nLX/Sy119/nRNC+OzZs33rnnPOOTyTyfD//e9/RY+h1H5xzrnruvrzeeedVzax+eSTT3g6nebf//73fctnzZrFCSE+Amzua9WqVaHEJog1a9ZwAPyGG24I/d1s89e//nUksfn00095NpvNW66O+f333y/YD845v+iii3gikeDvvvuuXrZx40Y+YMAA/q1vfUsvcxyHDxkyhE+YMMG3/QMPPMAB8D/+8Y/dal8xYnR3xK6oGB3GzJkzcdFFFwEAdt11V23+Vyb+oCvq3XffBSEEN9xwA66//nrssssuyGQyGDduHN58803kcjlceumlGDp0KPr06YMTTzwR69evz9vvI488ol0KdXV1mDhxIl555ZWuOGSNAQMGhLpjDjroILS0tODTTz/Vyx599FHsueeeOPTQQ/Uy27bx3e9+Fy+88AI+/PBDAEBtbS1qa2tD2wSAtWvX6mWPPfYYOOc488wzfeueeeaZaG1txeLFi4seQ6n9AgBKOzZkLF68GG1tbaH95Zzjscceq9q+wlBqm/369UMikchbrv6DDz74oGgbjz76KI488kgMHz5cL2toaMBJJ52EP/zhD3AcBwDw/PPP46OPPso7J9/85jdRV1eHRx99tFvtK0aM7o6Y2MToMCZNmoTzzz8fAPC73/0OK1euxMqVK3HAAQcU3O7WW2/Fc889h1tvvRV33nkn/vnPf+LYY4/F2WefjY8//hh33XUX5s6diz/96U+YNGmSb9vZs2fj5JNPxj777IP/+7//w3333YfNmzfjS1/6Et54442ifXYcp6QX57yic7Js2TIMHDgQO+64o172+uuvY7/99stbVy1bvXp1wTaffvppAMDIkSN9bQ4cOBCDBw8ObfP1118v2teO9qscqP7su+++vuVDhgzBgAEDSurvtsTTTz8N27bx+c9/3rfc1PEAQGtrK95+++3I89ra2op33nkHgHdOgusmEgnstddeeeekK/cVI0ZPhL2tOxCj52OnnXbCzjvvDADYf//9scsuu5S0Xd++ffHYY4/pWfQnn3yCqVOnYq+99sLvf/97vd4///lPLFiwAJs2bUJDQwPWrl2LGTNm4Ec/+hF+/vOf6/XGjx+PESNG4KqrrsIjjzwSud93330Xu+66a0l9XLZsWdnC5zvvvBPPPPMMbrrpJliWpZf/73//Q//+/fPWV8v+97//Rbb597//HXPnzsWJJ57oeyhFtVlbW4tkMlmwzWr0q1z873//QyqVCrVI9e/fv6r7qjaWLl2K++67Dz/+8Y+xww47+H6zLMv3X2/YsAGc85LOq3qPWvfdd9/dZvuKEaMnIiY2MbYZvvrVr/pcA3vvvTcA4JhjjvGtp5a///77GDVqFJYsWQLHcXD66adrEzsApNNpjB07FsuWLSu436FDh2LVqlUl9XHPPfcsaT2F5uZmnHfeefjGN76hrVgmCkURRf327rvv4mtf+xqGDRuGO++8s6I2Oed50Ty2beetV06/CsH8XwDxMFbtVHtfXYGXX34Z3/rWt3DIIYdgzpw5eb8Hj1ehnGONWje4vCv3FSNGT0RMbGJsMwRnjclksuByFer83//+FwBw4IEHhrZbTEeRTCbxxS9+saQ+mjPjYliyZAlOOukkjB8/Hg888EDeQ2KHHXYItUgoHU7YLPq9997DV77yFdi2jaeeeipvnR122AGvvvpq3nZbt25FNpvV6y9atChPV6HcbJX0qxiC+pS7774b3/ve97DDDjugra0NLS0tqKmpydvf6NGjy95XZ+OVV17R1sA//vGPSKVSRbfp168fCCElnVdl/fnf//6HQYMG5a1b7Px35b5ixOgJiIlNjB6HAQMGAAB+85vf+MSSpaIzXFFLlizBCSecgLFjx+K3v/2tJmMm9t13X7z22mt5y9WyUaNG+Za/9957GDduHDjneOaZZ7DTTjuFtvnwww9j3bp1Pp1NsM1jjz020kpVbr9KQXBf6nwrbc1rr72Ggw8+WP++bt06fPLJJxXtqzPxyiuv4Oijj8bw4cOxdOlS9OnTp6TtMpkM9thjj8jzmslksNtuuwHwn5N99tlHr+c4Dv75z3/i5JNP7jb7ihGjJyAmNjGqAjWLbW1t7fR9TZw4EbZt4+2338bXv/71srevtitq6dKlOOGEE3DEEUfgsccei5zRn3jiiZg8eTL++te/6oe64zi4//77cfDBB2Po0KF63ffffx/jxo2D67p45plnIgnc8ccfjyuuuAKLFi3CJZdcopffc889yGQyaGxsBCBm6kFdSCX9KhVjxowJXd7Y2Ih0Oo177rnHR2zuueceEEJwwgknlL2vzsKrr76Ko48+GjvttBOefPJJ9OvXr6ztTzzxRCxYsABr167FsGHDAACbN2/G7373Oxx33HHaFXjwwQdjyJAhuOeee/Dtb39bb/+b3/wGW7ZswUknndSt9hUjRndHTGxiVAVqJnjTTTfhjDPOQCKRwJ577on6+vqq72uXXXbB1VdfjcsvvxzvvPMOGhsb0a9fP/z3v//FCy+8gNraWlx11VWR2yeTycgHb7l49tlnccIJJ2Dw4MH4yU9+kucW2meffdDQ0AAAOOuss3Drrbfim9/8Jq677jrsuOOO+MUvfoF//etf+NOf/qS3Wb9+Pb7yla/go48+wq9+9SusX7/eF+6+0047aevNyJEjcfbZZ2PGjBmwLAsHHnggli5dioULF+Laa68tybVQar8AYUVSpPDtt98GAJ1leZdddil6Xvv3748rrrgCV155Jfr3748JEyZg1apVmDlzJiZNmuSzIphtq6ieF198EXV1dQCAb3zjG0WPLYiWlhb88Y9/BCBCnwFg+fLl+OSTT1BbW4umpiYAwL/+9S8cffTRAIBZs2bhrbfewltvvaXb2X333TFw4ED93bZtjB071pet+cILL8R9992HY445BldffTVSqRSuu+46tLW1YebMmXo9y7Iwd+5cnHbaafjBD36Ak08+GW+99RYuvvhijB8/XpPTbbGvGDF6JLZdCp0YvQ2XXXYZHzp0KKeU+hKfjR07lo8dO1avF5VEbdmyZRwA//Wvf+1bfvfdd3MAfNWqVb7ljz32GP/KV77CGxoaeCqV4sOHD+ff+MY3+J/+9KdOOb4wzJgxgwOIfAWTv61bt46ffvrpvH///jydTvNDDjmEP/nkk7511HmIes2YMcO3fjab5TNmzOA777wzTyaT/POf/zz/+c9/XtZxlNIvzr3/Iux1xhlnlLy/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hwIQRRHPPPMMO+64IwCHHHIIF110ES+++CKLFi0CXEJxoVBgyZIlG912oVCgUCgMer69vX1GXrAAbW1t+bFNQ/Jjm57kxzZ9Ge7xjfckuFgsznhjZCyZFjk2bW1tfOITn+Ccc85h++23Z8cdd+Syyy4D4H3vex8Ahx9+OHvuuScf/OAHueyyy3j11VdZtmwZJ5xwwoy+8HJycnJycnJqTAvDBuCyyy7D8zw++MEPUi6XOfjgg/nNb37D3LlzAVBK8bOf/YxPfepTvPnNb6ZUKvF//+//5fLLL5/kPc/JycnJycmZKKaNYeP7PpdffvkmDZUddtiBn/70p1v0OYVCgXPOOWfI8NR0Jz+26Ul+bNOT/NimLzP9+GY6ws4WxZ6cnJycnJycGc+0EOjLycnJycnJyRkOuWGTk5OTk5OTM2PIDZucnJycnJycGUNu2OTk5OTk5OTMGHLDpo5rr72WnXfemWKxyJIlS/jd73432bs0Ys4991yEEA3LwoULs9ettZx77rlsu+22lEol3va2tzU0E51K/Pa3v+U973kP2267LUIIfvzjHze8PpxjqVarnHzyycyfP5/m5mb+1//6X/z973+fwKMYms0d2/HHHz/od3zjG9/YsM5UPbZLLrmEgw46iNbWVrbZZhuOOeYYnnjiiYZ1putvN5xjm66/3Ve/+lX23XffTJTukEMO4dZbb81en66/GWz+2Kbrb5YzNLlhk/CDH/yA0047jc997nOsWrWKt7zlLRx11FFZy4bpxF577cWLL76YLQ899FD22qWXXsry5cu5+uqruffee1m4cCHvete7srYVU4m+vj72228/rr766iFfH86xnHbaafzoRz/i+9//PnfffTe9vb28+93vRms9UYcxJJs7NoAjjzyy4Xf8+c8be8VM1WO76667OPHEE7nnnnu4/fbbieOYww8/nL6+vmyd6frbDefYYHr+dttttx1f/OIXue+++7jvvvt4xzvewXvf+97MeJmuvxls/thgev5mORvB5lhrrX3DG95gP/GJTzQ8t/vuu9vPfOYzk7RHo+Occ86x++2335CvGWPswoUL7Re/+MXsuUqlYtvb2+111103QXs4OgD7ox/9KPt/OMeyYcMG6/u+/f73v5+t8/zzz1sppb3tttsmbN83x8Bjs9ba4447zr73ve/d6Humy7FZa+3atWstYO+66y5r7cz67QYem7Uz67ebO3euveGGG2bUb5aSHpu1M+s3y7E299gAYRiyYsUKDj/88IbnDz/8cP7whz9M0l6Nnr/85S9su+227Lzzzrz//e/nqaeeAuDpp59mzZo1DcdZKBQ47LDDpt1xDudYVqxYQRRFDetsu+227L333tPieO+880622WYbdt11V0444QTWrl2bvTadjq2rqwuAefPmATPrtxt4bCnT/bfTWvP973+fvr4+DjnkkBn1mw08tpTp/pvl1Jg2ysPjySuvvILWelAX8AULFrBmzZpJ2qvRcfDBB/Ptb3+bXXfdlZdeeokLL7yQN73pTTzyyCPZsQx1nM8+++xk7O6oGc6xrFmzhiAIsrYb9etM9d/1qKOO4n3vex877rgjTz/9NF/4whd4xzvewYoVKygUCtPm2Ky1LF26lEMPPZS9994bmDm/3VDHBtP7t3vooYc45JBDqFQqtLS08KMf/Yg999wzu3lP599sY8cG0/s3yxlMbtjUIYRo+N9aO+i5qc5RRx2VPd5nn3045JBDeO1rX8u3vvWtLBluJhxnymiOZToc77/+679mj/fee28OPPBAdtxxR372s59x7LHHbvR9U+3YTjrpJB588EHuvvvuQa9N999uY8c2nX+73Xbbjfvvv58NGzZw8803c9xxx3HXXXdlr0/n32xjx7bnnntO698sZzB5KAqYP38+SqlBlvfatWsHzVCmG83Nzeyzzz785S9/yaqjZsJxDudYFi5cSBiGrF+/fqPrTBcWLVrEjjvuyF/+8hdgehzbySefzE9+8hPuuOMOtttuu+z5mfDbbezYhmI6/XZBEPC6172OAw88kEsuuYT99tuPr3zlKzPiN9vYsQ3FdPrNcgaTGza4E37JkiXcfvvtDc/ffvvtvOlNb5qkvRobqtUqjz32GIsWLWLnnXdm4cKFDccZhiF33XXXtDvO4RzLkiVL8H2/YZ0XX3yRhx9+eNod77p161i9ejWLFi0CpvaxWWs56aSTuOWWW/jNb37Dzjvv3PD6dP7tNndsQzGdfruBWGupVqvT+jfbGOmxDcV0/s1yyKuiUr7//e9b3/ftN77xDfvoo4/a0047zTY3N9tnnnlmsndtRJx++un2zjvvtE899ZS955577Lvf/W7b2tqaHccXv/hF297ebm+55Rb70EMP2f/zf/6PXbRoke3u7p7kPR9MT0+PXbVqlV21apUF7PLly+2qVavss88+a60d3rF84hOfsNttt5391a9+ZVeuXGnf8Y532P3228/GcTxZh2Wt3fSx9fT02NNPP93+4Q9/sE8//bS944477CGHHGJf85rXTItj++QnP2nb29vtnXfeaV988cVs6e/vz9aZrr/d5o5tOv92Z511lv3tb39rn376afvggw/az372s1ZKaX/5y19aa6fvb2btpo9tOv9mOUOTGzZ1XHPNNXbHHXe0QRDYAw44oKGEc7rwr//6r3bRokXW93277bbb2mOPPdY+8sgj2ev/v717j2nqfOMA/i0D2tJyqdDVDioEdILOu3HDC0IGokYHblNxKiUyDDqtYJzM6BTBC6KgzmwOiKuXaSLZYJlzcjFeCIhMiEacDOYmYBa8M5hjaEuf3x9m58cREJw6fvb3fBIT3/O+fS/ngD6c9+Eci8VC69ato759+5JUKqWAgACqrKzsxRl37eTJkwSgwx+9Xk9EPVvLX3/9RUuWLKE+ffqQXC6nadOmUX19fS+sRuxxa2tpaaFJkyaRWq0mOzs76tevH+n1+g7z/l9dW2frAkBGo1Fo86Jeu+7W9iJfuwULFgj//qnVanrzzTeFoIboxb1mRI9f24t8zVjnJERE/979IcYYY4yx54dzbBhjjDFmNTiwYYwxxpjV4MCGMcYYY1aDAxvGGGOMWQ0ObBhjjDFmNTiwYYwxxpjV4MCGMcYYY1aDAxtmdQIDAxEXF2dV40ZFRSE8PPyp+vDy8oJEIoFEIsHvv//eZbu9e/fCxcXlqcZiXYuKihKuwzfffNPb02HM6nBgw9gzkpOTg+TkZKHs5eWFHTt29N6EOpGUlISGhgY4Ozv39lSs3qlTpzoNInfu3ImGhobemRRj/wdse3sCjFmLPn369PYUuuXo6Ci8qbm3mUwm2NnZ9fY0/nXOzs4cWDL2HPEdG2b1GhsbERkZCZVKBQcHB0yZMgU///yzUP/31kt+fj78/PygVCoxefJk0U/VZrMZBoMBLi4ucHV1RUJCAvR6vWh7qP1WVGBgIOrq6hAfHy9sOwBAYmIihg8fLprfjh074OXlJZTb2tqwfPlyYayVK1fi0TefEBFSU1Ph7e0NuVyOYcOG4auvvvpH52fv3r3o168fHBwcMGPGDNy5c6dDmyNHjmDUqFGQyWTw9vbG+vXrYTabhfqffvoJ48ePh0wmw6BBg3D8+HHRVkttbS0kEgmys7MRGBgImUyGL7/8EgBgNBrh5+cHmUwGX19ffPbZZ6Kxf/vtN8yePRsqlQqurq4ICwtDbW2tUH/q1CmMGTMGCoUCLi4uGDduHOrq6nq09u7WlZ6ejiFDhkChUECn02Hx4sW4d++eUF9XV4fp06dDpVJBoVBg8ODB+P7771FbW4ugoCAAgEqlgkQiQVRUVI/mxBh7OhzYMKsXFRWF8vJyfPvttygtLQURYerUqTCZTEKblpYWbNu2DQcOHEBRURHq6+uxYsUKoX7Lli04ePAgjEYjSkpK0Nzc/Nj8iJycHHh4eAhbP0+y9ZCWloYvvvgCe/bsQXFxMe7evYvc3FxRmzVr1sBoNGL37t348ccfER8fj3nz5uH06dM9PzEAysrKsGDBAixevBgXLlxAUFAQNmzYIGqTn5+PefPmwWAw4PLly8jIyMDevXuxceNGAIDFYkF4eDgcHBxQVlaGzMxMrF69utPxEhISYDAYUFVVhdDQUGRlZWH16tXYuHEjqqqqsGnTJnz88cfYt28fgIfXJSgoCEqlEkVFRSguLhYCzwcPHsBsNiM8PBwTJ07ExYsXUVpaioULFwqB5ON0ty4AsLGxwSeffIJLly5h3759OHHiBFauXCnUf/DBB7h//z6KiopQWVmJLVu2QKlUQqfT4euvvwYAVFdXo6GhATt37nyia8MY+4d69RWcjD0HEydOpGXLlhERUU1NDQGgkpISof727dskl8spOzubiIiMRiMBoCtXrghtPv30U9JoNEJZo9HQ1q1bhbLZbKZ+/fpRWFhYp+MSEXl6etL27dtFc1u3bh0NGzZMdGz79u3k6ekplLVaLaWkpAhlk8lEHh4ewlj37t0jmUxGZ86cEfUTHR1Nc+bM6fK8dDafOXPm0OTJk0XHZs+eTc7OzkJ5woQJtGnTJlGbAwcOkFarJSKiY8eOka2tLTU0NAj1hYWFBIByc3OJiOjq1asEgHbs2CHqR6fT0aFDh0THkpOTyd/fn4iI9uzZQwMHDiSLxSLU379/n+RyOeXn59OdO3cIAJ06darLdXelu3V1Jjs7m1xdXYXykCFDKDExsdO2f7/BvbGxsdP69ueHMfbscI4Ns2pVVVWwtbXF66+/LhxzdXXFwIEDUVVVJRxzcHCAj4+PUNZqtbh58yYAoKmpCTdu3MCYMWOE+pdeegmjRo2CxWJ5pvNtampCQ0MD/P39hWO2trYYPXq0sB11+fJltLa2IiQkRPTZBw8eYMSIEU80XlVVFWbMmCE65u/vj7y8PKFcUVGBc+fOie5ktLW1obW1FS0tLaiuroZOpxPl7rQ/V+2NHj1a+PutW7dw7do1REdHIyYmRjhuNpuFHJSKigpcuXIFjo6Oon5aW1vxyy+/YNKkSYiKikJoaChCQkIQHByMWbNmQavVdrv27tbl4OCAkydPYtOmTbh8+TKam5thNpvR2tqKP//8EwqFAgaDAYsWLUJBQQGCg4PxzjvvYOjQod2OzRh7fjiwYVaNHslNaX+8/XbFo0msEomkw2cf3d7oqu/HsbGx6fC59ltiPfF3MHX06FG4u7uL6qRS6RP11ZM1WCwWrF+/Hm+//XaHOplM1uFcPo5CoRD1CwBZWVmiwBN4GDj+3WbUqFE4ePBgh77UajWAhzk6BoMBeXl5OHz4MNasWYPCwkK88cYbT7Wuuro6TJ06FbGxsUhOTkafPn1QXFyM6Oho4Zq9//77CA0NxdGjR1FQUIDNmzcjLS0NS5cu7dH5YIw9exzYMKs2aNAgmM1mlJWVYezYsQCAO3fuoKamBn5+fj3qw9nZGRqNBj/88AMmTJgA4OFP9ufPn++QCNyevb092traRMfUajWuX78uCgYuXLggGkur1eLs2bMICAgA8PAORkVFBUaOHCmsSSqVor6+HhMnTuzRGroyaNAgnD17VnTs0fLIkSNRXV2N/v37d9qHr68v6uvrcePGDWg0GgDAuXPnuh1bo9HA3d0dv/76K+bOndtpm5EjR+Lw4cN4+eWX4eTk1GVfI0aMwIgRI7Bq1Sr4+/vj0KFD3QY23a2rvLwcZrMZaWlpsLF5mI6YnZ3doZ1Op0NsbCxiY2OxatUqZGVlYenSpbC3tweADl8DjLHniwMbZtUGDBiAsLAwxMTEICMjA46Ojvjoo4/g7u6OsLCwHvezdOlSbN68Gf3794evry927dqFxsbGx96p8PLyQlFRESIiIiCVSuHm5obAwEDcunULqampePfdd5GXl4djx46J/tNetmwZUlJSMGDAAPj5+SE9PV30LBRHR0esWLEC8fHxsFgsGD9+PJqbm3HmzBkolUro9foer8tgMGDs2LFITU1FeHg4CgoKRNtQALB27VpMmzYNOp0OM2fOhI2NDS5evIjKykps2LABISEh8PHxgV6vR2pqKv744w8hebi7OzmJiYkwGAxwcnLClClTcP/+fZSXl6OxsRHLly/H3LlzsXXrVoSFhSEpKQkeHh6or69HTk4OPvzwQ5hMJmRmZuKtt97CK6+8gurqatTU1CAyMrLbtXe3Lh8fH5jNZuzatQvTp09HSUkJPv/8c1EfcXFxmDJlCl599VU0NjbixIkTQsDs6ekJiUSC7777DlOnToVcLodSqezxtWGM/UO9lt3D2HPyaBLv3bt3af78+eTs7ExyuZxCQ0OppqZGqDcajaJkWSKi3Nxcav/tYTKZaMmSJeTk5EQqlYoSEhJo5syZFBER0eW4paWlNHToUJJKpaK+du/eTTqdjhQKBUVGRtLGjRtFycMmk4mWLVtGTk5O5OLiQsuXL6fIyEhRorLFYqGdO3fSwIEDyc7OjtRqNYWGhtLp06e7PC+dJQ8TPUzQ9fDwILlcTtOnT6dt27Z1OB95eXk0duxYksvl5OTkRGPGjKHMzEyhvqqqisaNG0f29vbk6+tLR44cIQCUl5dHRP9NHj5//nyH8Q8ePEjDhw8ne3t7UqlUFBAQQDk5OUJ9Q0MDRUZGkpubG0mlUvL29qaYmBhqamqi69evU3h4OGm1WrK3tydPT09au3YttbW1dXkenmRd6enppNVqha+b/fv3ixKClyxZQj4+PiSVSkmtVtP8+fPp9u3bwueTkpKob9++JJFISK/Xi8YGJw8z9lxIiP5BogBj/+csFgv8/Pwwa9Ys0dOG/5d5eXkhLi7uX3ndRElJCcaPH48rV66IkrLZf0kkEuTm5j71qzIYY2L8HBvGeqCurg5ZWVmoqalBZWUlFi1ahKtXr+K9997r7ak9kYSEBCiVSjQ1NT3TfnNzc1FYWIja2locP34cCxcuxLhx4zio6URsbCxvSTH2HPEdG8Z64Nq1a4iIiMClS5dARHjttdeQkpIiJPi+COrq6oTf5vH29hYSYp+F/fv3Izk5GdeuXYObmxuCg4ORlpYGV1fXZzbGkxo8eHCXTyDOyMjoMmH5ebt58yaam5sBPHysQPvfFGOMPT0ObBhjVql9IPcojUbT4dk4jDHrwIENY4wxxqwG59gwxhhjzGpwYMMYY4wxq8GBDWOMMcasBgc2jDHGGLMaHNgwxhhjzGpwYMMYY4wxq8GBDWOMMcasBgc2jDHGGLMa/wGqeQj5/KjzZwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "es.isel(time=0).plot()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/tutorials-and-presentations.rst b/docs/source/tutorials-and-presentations.rst new file mode 100644 index 0000000..ab98d05 --- /dev/null +++ b/docs/source/tutorials-and-presentations.rst @@ -0,0 +1,40 @@ +.. _tutorials-and-presentations: + +Tutorials and Presentations +=========================== + +Tutorials +--------- + +.. toctree:: + :maxdepth: 1 + + Analyzing NASA Earth Exchange Global Daily Downscaled Projections (NEX-GDDP-CMIP6) Data + +.. grid:: 1 2 2 2 + :gutter: 2 + + + .. grid-item-card:: + :text-align: center + :link: real-example-1.html + + Demo using NASA Earth Exchange Global Daily Downscaled Projections (NEX-GDDP-CMIP6) Data + + +++ + CuPy-Xarray Demo + +Presentations +------------- + +.. card:: Xarray on GPUs! + + SciPy 2023 + ^^^ + + + | `Xarray on GPUs! `_ + | DOI: `10.5281/zenodo.8247471 `_ + + +++ + Negin Sobhani, Deepak Cherian, Max Jones