From c96ab76b913a4338a4855ec1382011c3f6d991de Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Tue, 15 Aug 2023 03:47:55 -0600 Subject: [PATCH 01/25] adding an inital notebook --- docs/Notebook0_Introduction.ipynb | 765 ++++++++++++++++++++++++++++++ 1 file changed, 765 insertions(+) create mode 100644 docs/Notebook0_Introduction.ipynb diff --git a/docs/Notebook0_Introduction.ipynb b/docs/Notebook0_Introduction.ipynb new file mode 100644 index 0000000..7c17b46 --- /dev/null +++ b/docs/Notebook0_Introduction.ipynb @@ -0,0 +1,765 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0d6fecdf-48c0-4745-b802-2117fb3137cf", + "metadata": {}, + "source": [ + "# Basics of CuPy" + ] + }, + { + "cell_type": "markdown", + "id": "33f29f17-cfd2-478a-b913-6b126394df3a", + "metadata": {}, + "source": [ + "**Negin Sobhani**, **Deepak Cherian**, and **Max Jones**\n", + "\n", + "negins@ucar.edu, dcherian@ucar.edu, max@carbonplan.org\n", + "\n", + "------------" + ] + }, + { + "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", + "A full list of CuPy's Numpy and Scipy equivalent functions are provided on the link below:\n", + "\n", + "[Complete Comparison of NumPy and SciPy to CuPy functions](https://docs.cupy.dev/en/stable/reference/comparison.html)\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", + "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. 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", + "## Additional Resources\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)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b5c6373c-79cd-4829-b8f4-1c11eb112e98", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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 +} From f5258699365af2c562b22d94e6f9f1df80812ac3 Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Tue, 15 Aug 2023 20:15:06 -0600 Subject: [PATCH 02/25] initial commit of restructure --- docs/{ => source}/Notebook0_Introduction.ipynb | 0 docs/{ => source}/api.rst | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename docs/{ => source}/Notebook0_Introduction.ipynb (100%) rename docs/{ => source}/api.rst (100%) diff --git a/docs/Notebook0_Introduction.ipynb b/docs/source/Notebook0_Introduction.ipynb similarity index 100% rename from docs/Notebook0_Introduction.ipynb rename to docs/source/Notebook0_Introduction.ipynb diff --git a/docs/api.rst b/docs/source/api.rst similarity index 100% rename from docs/api.rst rename to docs/source/api.rst From ed5a2bf2b78d2aec012646c3085325195442d3f3 Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Tue, 15 Aug 2023 20:15:31 -0600 Subject: [PATCH 03/25] updating index --- docs/index.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index 597dd16..fe67598 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,10 +1,81 @@ -# 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)](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) + +[![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) + +[![NASA-80NSSC22K0345](https://img.shields.io/badge/NASA-80NSSC22K0345-blue)](https://science.nasa.gov/open-science-overview) + + + +## Overview + +CuPy-Xarray is a Python library that leverages CuPy, a GPU array library, and Xarray, 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 anaconda: +```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). The original notebook also adapts from the content in [this NCAR tutorial to GPUs](https://github.com/NCAR/GPU_workshop/tree/workshop/13_CuPyAndLegate), and uses it to illustrate cupy-xarray and working with cupy arrays and Xarray objects in general. ## Contents ```{eval-rst} + +**User Guide**: + .. toctree:: :maxdepth: 1 + :caption: User Guide - quickstart -``` + source/Notebook0_Introduction + source/Notebook1_Xarray_Cupy + source/Notebook2_Xarray_Cupy_BasicOperations + source/Notebook3_Xarray_Cupy_HighLevel + source/Notebook4_Xarray_Cupy_ApplyUfunc + +**Demo**: + +.. toctree:: + :maxdepth: 1 + :caption: Demo + + source/Notebook5_Xarray_Cupy_Example + +**Contributing**: + +.. toctree:: + :maxdepth: 1 + :caption: Contributing + + source/contributing + +**API Reference**: + +.. toctree:: + + :maxdepth: 1 + :caption: API Reference + source/api + +``` \ No newline at end of file From dd891f33bbd77843276b02f381ca6dc0382e883d Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Tue, 15 Aug 2023 20:15:50 -0600 Subject: [PATCH 04/25] moving the files under source for build --- docs/{ => source}/index.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{ => source}/index.md (100%) diff --git a/docs/index.md b/docs/source/index.md similarity index 100% rename from docs/index.md rename to docs/source/index.md From f503167fa6424ba9e81a0ff27dd148c76a2dce56 Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Tue, 15 Aug 2023 20:16:47 -0600 Subject: [PATCH 05/25] moving index to top level --- docs/{source => }/index.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{source => }/index.md (100%) diff --git a/docs/source/index.md b/docs/index.md similarity index 100% rename from docs/source/index.md rename to docs/index.md From e97204d45245d03de151c4edab87dee929eead8c Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Thu, 17 Aug 2023 02:55:52 -0600 Subject: [PATCH 06/25] update badges --- docs/index.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/index.md b/docs/index.md index fe67598..74035e9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,13 +1,15 @@ # 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)](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) +[![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/en/latest/?badge=latest) +[![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)](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) -[![NASA-80NSSC22K0345](https://img.shields.io/badge/NASA-80NSSC22K0345-blue)](https://science.nasa.gov/open-science-overview) +[![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) @@ -70,12 +72,13 @@ pip install git+https://github.com/xarray-contrib/cupy-xarray.git source/contributing + **API Reference**: .. toctree:: - :maxdepth: 1 :caption: API Reference - source/api + + ``` \ No newline at end of file From 3599651730de126ee6e7d65e9d04a25fd47704f5 Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Thu, 17 Aug 2023 02:56:50 -0600 Subject: [PATCH 07/25] adding notebook content --- docs/source/Notebook1_Xarray_Cupy.ipynb | 1982 ++++++++ ...otebook2_Xarray_Cupy_BasicOperations.ipynb | 4193 +++++++++++++++++ .../Notebook3_Xarray_Cupy_HighLevel.ipynb | 714 +++ .../Notebook4_Xarray_Cupy_ApplyUfunc.ipynb | 610 +++ .../Notebook5_Xarray_Cupy_Example.ipynb | 368 ++ 5 files changed, 7867 insertions(+) create mode 100644 docs/source/Notebook1_Xarray_Cupy.ipynb create mode 100644 docs/source/Notebook2_Xarray_Cupy_BasicOperations.ipynb create mode 100644 docs/source/Notebook3_Xarray_Cupy_HighLevel.ipynb create mode 100644 docs/source/Notebook4_Xarray_Cupy_ApplyUfunc.ipynb create mode 100644 docs/source/Notebook5_Xarray_Cupy_Example.ipynb diff --git a/docs/source/Notebook1_Xarray_Cupy.ipynb b/docs/source/Notebook1_Xarray_Cupy.ipynb new file mode 100644 index 0000000..bae06c3 --- /dev/null +++ b/docs/source/Notebook1_Xarray_Cupy.ipynb @@ -0,0 +1,1982 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0d6fecdf-48c0-4745-b802-2117fb3137cf", + "metadata": {}, + "source": [ + "# Introduction to CuPy-Xarray" + ] + }, + { + "cell_type": "markdown", + "id": "33f29f17-cfd2-478a-b913-6b126394df3a", + "metadata": {}, + "source": [ + "**Negin Sobhani**, **Deepak Cherian**, and **Max Jones** \n", + "negins@ucar.edu, dcherian@ucar.edu, and max@carbonplan.org\n", + "\n", + "------------" + ] + }, + { + "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\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": "iVBORw0KGgoAAAANSUhEUgAAAigAAAGgCAYAAACABpytAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy88F64QAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAffElEQVR4nO3de3BU9d3H8c9CwpIwSZRQdrMSIXRivYCoQalBS6wQRxHqUIsWatFiBwYvRFRMJl6CU5OKNU0lggNjkVYjTK1YW2/EVgMYWyFAq+CISsSgpBk0bgLEhMvv+cMn+zxrUFzcTb4b3q+ZM9M9e/bw3d+k3XdPdrMe55wTAACAIX16egAAAIAvI1AAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5EQfK2rVrNWnSJAUCAXk8Hj3zzDOh+w4cOKA77rhDI0eO1IABAxQIBPTzn/9cH3/8cdg52tvbddNNN2nQoEEaMGCAJk+erF27dn3rJwMAAHqHhEgfsG/fPo0aNUrXXXedfvzjH4fdt3//fm3atEl33XWXRo0apebmZhUUFGjy5MnauHFj6LiCggL99a9/1cqVK5Wenq5bb71Vl19+uerq6tS3b9+jznD48GF9/PHHSklJkcfjifQpAACAHuCcU2trqwKBgPr0Oco1EvctSHKrV6/+2mPeeOMNJ8nt3LnTOefcZ5995hITE93KlStDx3z00UeuT58+7sUXX/xG/25DQ4OTxMbGxsbGxhaHW0NDw1Ff6yO+ghKpYDAoj8ejE044QZJUV1enAwcOKD8/P3RMIBDQiBEjVFtbq0suuaTLOdrb29Xe3h667f73C5gbGhqUmpoa2ycAAACioqWlRZmZmUpJSTnqsTENlM8//1yFhYWaNm1aKCQaGxvVr18/nXjiiWHH+nw+NTY2HvE8ZWVlWrBgQZf9qampBAoAAHHmm7w9I2af4jlw4ICuvvpqHT58WIsXLz7q8c65rxy4qKhIwWAwtDU0NER7XAAAYEhMAuXAgQOaOnWq6uvrVV1dHXaVw+/3q6OjQ83NzWGPaWpqks/nO+L5vF5v6GoJV00AAOj9oh4onXHy7rvv6uWXX1Z6enrY/Tk5OUpMTFR1dXVo3+7du/XWW28pNzc32uMAAIA4FPF7UPbu3av33nsvdLu+vl5btmzRwIEDFQgEdOWVV2rTpk3629/+pkOHDoXeVzJw4ED169dPaWlpmjlzpm699Valp6dr4MCBuu222zRy5EiNHz8+es8MAADELY/r/EjMN/Tqq6/qoosu6rJ/xowZKikpUVZW1hEf98orrygvL0/SF2+evf3221VVVaW2tjZdfPHFWrx4sTIzM7/RDC0tLUpLS1MwGOTXPQAAxIlIXr8jDhQLCBQAAOJPJK/ffBcPAAAwh0ABAADmECgAAMAcAgUAAJhDoAAAAHMIFAAAYA6BAgAAzCFQAACAORH/qXsAQFfDCp/r6RGOyQe/ntjTIwBHxBUUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAc/hDbUAE4vGPcfGHuADEI66gAAAAcwgUAABgDoECAADM4T0oR8D7DAAA6FlcQQEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMSejpAQAA6O2GFT7X0yNE7INfT+zRf58rKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMiThQ1q5dq0mTJikQCMjj8eiZZ54Ju985p5KSEgUCASUlJSkvL09bt24NO6a9vV033XSTBg0apAEDBmjy5MnatWvXt3oiAACg94g4UPbt26dRo0apsrLyiPcvXLhQ5eXlqqys1IYNG+T3+zVhwgS1traGjikoKNDq1au1cuVKrV+/Xnv37tXll1+uQ4cOHfszAQAAvUZCpA+49NJLdemllx7xPuecKioqVFxcrClTpkiSVqxYIZ/Pp6qqKs2aNUvBYFCPPvqo/vjHP2r8+PGSpMcff1yZmZl6+eWXdckll3yLpwMAAHqDqL4Hpb6+Xo2NjcrPzw/t83q9GjdunGprayVJdXV1OnDgQNgxgUBAI0aMCB3zZe3t7WppaQnbAABA7xXVQGlsbJQk+Xy+sP0+ny90X2Njo/r166cTTzzxK4/5srKyMqWlpYW2zMzMaI4NAACMicmneDweT9ht51yXfV/2dccUFRUpGAyGtoaGhqjNCgAA7IlqoPj9fknqciWkqakpdFXF7/ero6NDzc3NX3nMl3m9XqWmpoZtAACg94pqoGRlZcnv96u6ujq0r6OjQzU1NcrNzZUk5eTkKDExMeyY3bt366233godAwAAjm8Rf4pn7969eu+990K36+vrtWXLFg0cOFAnn3yyCgoKVFpaquzsbGVnZ6u0tFTJycmaNm2aJCktLU0zZ87UrbfeqvT0dA0cOFC33XabRo4cGfpUDwAAOL5FHCgbN27URRddFLo9b948SdKMGTP02GOPaf78+Wpra9OcOXPU3NysMWPGaM2aNUpJSQk95re//a0SEhI0depUtbW16eKLL9Zjjz2mvn37RuEpAQCAeBdxoOTl5ck595X3ezwelZSUqKSk5CuP6d+/vxYtWqRFixZF+s8DAI5zwwqf6+kR0A34Lh4AAGAOgQIAAMwhUAAAgDkECgAAMIdAAQAA5kT8KR4gWngnPgDgq3AFBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmMNfku0l+Kus6E34eQbAFRQAAGAOgQIAAMwhUAAAgDkECgAAMIdAAQAA5hAoAADAHAIFAACYQ6AAAABzCBQAAGAOgQIAAMwhUAAAgDkECgAAMIdAAQAA5hAoAADAnISeHgBAbA0rfK6nR4Bh/HzAKq6gAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMiXqgHDx4UHfeeaeysrKUlJSk4cOH695779Xhw4dDxzjnVFJSokAgoKSkJOXl5Wnr1q3RHgUAAMSpqAfK/fffr0ceeUSVlZV6++23tXDhQj3wwANatGhR6JiFCxeqvLxclZWV2rBhg/x+vyZMmKDW1tZojwMAAOJQ1APl9ddf149+9CNNnDhRw4YN05VXXqn8/Hxt3LhR0hdXTyoqKlRcXKwpU6ZoxIgRWrFihfbv36+qqqpojwMAAOJQ1APlggsu0N///ndt375dkvTvf/9b69ev12WXXSZJqq+vV2Njo/Lz80OP8Xq9GjdunGpra494zvb2drW0tIRtAACg90qI9gnvuOMOBYNBnXrqqerbt68OHTqk++67Tz/96U8lSY2NjZIkn88X9jifz6edO3ce8ZxlZWVasGBBtEcFAABGRf0KyqpVq/T444+rqqpKmzZt0ooVK/Sb3/xGK1asCDvO4/GE3XbOddnXqaioSMFgMLQ1NDREe2wAAGBI1K+g3H777SosLNTVV18tSRo5cqR27typsrIyzZgxQ36/X9IXV1IyMjJCj2tqaupyVaWT1+uV1+uN9qgAAMCoqF9B2b9/v/r0CT9t3759Qx8zzsrKkt/vV3V1dej+jo4O1dTUKDc3N9rjAACAOBT1KyiTJk3Sfffdp5NPPllnnHGGNm/erPLycv3iF7+Q9MWvdgoKClRaWqrs7GxlZ2ertLRUycnJmjZtWrTHAQAAcSjqgbJo0SLdddddmjNnjpqamhQIBDRr1izdfffdoWPmz5+vtrY2zZkzR83NzRozZozWrFmjlJSUaI8DAADikMc553p6iEi1tLQoLS1NwWBQqampUT//sMLnon5OAADiyQe/nhj1c0by+s138QAAAHMIFAAAYA6BAgAAzCFQAACAOQQKAAAwh0ABAADmECgAAMAcAgUAAJhDoAAAAHMIFAAAYA6BAgAAzCFQAACAOQQKAAAwh0ABAADmECgAAMAcAgUAAJhDoAAAAHMIFAAAYA6BAgAAzCFQAACAOQQKAAAwh0ABAADmECgAAMAcAgUAAJhDoAAAAHMIFAAAYA6BAgAAzCFQAACAOQQKAAAwh0ABAADmECgAAMAcAgUAAJhDoAAAAHMIFAAAYA6BAgAAzCFQAACAOQQKAAAwh0ABAADmECgAAMAcAgUAAJhDoAAAAHMIFAAAYA6BAgAAzCFQAACAOQQKAAAwh0ABAADmECgAAMAcAgUAAJhDoAAAAHMIFAAAYA6BAgAAzIlJoHz00Uf62c9+pvT0dCUnJ+uss85SXV1d6H7nnEpKShQIBJSUlKS8vDxt3bo1FqMAAIA4FPVAaW5u1tixY5WYmKgXXnhB27Zt04MPPqgTTjghdMzChQtVXl6uyspKbdiwQX6/XxMmTFBra2u0xwEAAHEoIdonvP/++5WZmanly5eH9g0bNiz0n51zqqioUHFxsaZMmSJJWrFihXw+n6qqqjRr1qxojwQAAOJM1K+gPPvssxo9erR+8pOfaPDgwTr77LO1bNmy0P319fVqbGxUfn5+aJ/X69W4ceNUW1t7xHO2t7erpaUlbAMAAL1X1ANlx44dWrJkibKzs/XSSy9p9uzZuvnmm/WHP/xBktTY2ChJ8vl8YY/z+Xyh+76srKxMaWlpoS0zMzPaYwMAAEOiHiiHDx/WOeeco9LSUp199tmaNWuWfvnLX2rJkiVhx3k8nrDbzrku+zoVFRUpGAyGtoaGhmiPDQAADIl6oGRkZOj0008P23faaafpww8/lCT5/X5J6nK1pKmpqctVlU5er1epqalhGwAA6L2iHihjx47VO++8E7Zv+/btGjp0qCQpKytLfr9f1dXVofs7OjpUU1Oj3NzcaI8DAADiUNQ/xXPLLbcoNzdXpaWlmjp1qt544w0tXbpUS5culfTFr3YKCgpUWlqq7OxsZWdnq7S0VMnJyZo2bVq0xwEAAHEo6oFy7rnnavXq1SoqKtK9996rrKwsVVRUaPr06aFj5s+fr7a2Ns2ZM0fNzc0aM2aM1qxZo5SUlGiPAwAA4pDHOed6eohItbS0KC0tTcFgMCbvRxlW+FzUzwkAQDz54NcTo37OSF6/+S4eAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMiXmglJWVyePxqKCgILTPOaeSkhIFAgElJSUpLy9PW7dujfUoAAAgTsQ0UDZs2KClS5fqzDPPDNu/cOFClZeXq7KyUhs2bJDf79eECRPU2toay3EAAECciFmg7N27V9OnT9eyZct04oknhvY751RRUaHi4mJNmTJFI0aM0IoVK7R//35VVVXFahwAABBHYhYoN9xwgyZOnKjx48eH7a+vr1djY6Py8/ND+7xer8aNG6fa2tojnqu9vV0tLS1hGwAA6L0SYnHSlStXatOmTdqwYUOX+xobGyVJPp8vbL/P59POnTuPeL6ysjItWLAg+oMCAACTon4FpaGhQXPnztXjjz+u/v37f+VxHo8n7LZzrsu+TkVFRQoGg6GtoaEhqjMDAABbon4Fpa6uTk1NTcrJyQntO3TokNauXavKykq98847kr64kpKRkRE6pqmpqctVlU5er1derzfaowIAAKOifgXl4osv1ptvvqktW7aEttGjR2v69OnasmWLhg8fLr/fr+rq6tBjOjo6VFNTo9zc3GiPAwAA4lDUr6CkpKRoxIgRYfsGDBig9PT00P6CggKVlpYqOztb2dnZKi0tVXJysqZNmxbtcQAAQByKyZtkj2b+/Plqa2vTnDlz1NzcrDFjxmjNmjVKSUnpiXEAAIAxHuec6+khItXS0qK0tDQFg0GlpqZG/fzDCp+L+jkBAIgnH/x6YtTPGcnrN9/FAwAAzCFQAACAOQQKAAAwh0ABAADmECgAAMAcAgUAAJhDoAAAAHMIFAAAYA6BAgAAzCFQAACAOQQKAAAwh0ABAADmECgAAMAcAgUAAJhDoAAAAHMIFAAAYA6BAgAAzCFQAACAOQQKAAAwh0ABAADmECgAAMAcAgUAAJhDoAAAAHMIFAAAYA6BAgAAzCFQAACAOQQKAAAwh0ABAADmECgAAMAcAgUAAJhDoAAAAHMIFAAAYA6BAgAAzCFQAACAOQQKAAAwh0ABAADmECgAAMAcAgUAAJhDoAAAAHMIFAAAYA6BAgAAzCFQAACAOQQKAAAwh0ABAADmECgAAMAcAgUAAJhDoAAAAHMIFAAAYA6BAgAAzCFQAACAOVEPlLKyMp177rlKSUnR4MGDdcUVV+idd94JO8Y5p5KSEgUCASUlJSkvL09bt26N9igAACBORT1QampqdMMNN+if//ynqqurdfDgQeXn52vfvn2hYxYuXKjy8nJVVlZqw4YN8vv9mjBhglpbW6M9DgAAiEMJ0T7hiy++GHZ7+fLlGjx4sOrq6vSDH/xAzjlVVFSouLhYU6ZMkSStWLFCPp9PVVVVmjVrVrRHAgAAcSbm70EJBoOSpIEDB0qS6uvr1djYqPz8/NAxXq9X48aNU21t7RHP0d7erpaWlrANAAD0XjENFOec5s2bpwsuuEAjRoyQJDU2NkqSfD5f2LE+ny9035eVlZUpLS0ttGVmZsZybAAA0MNiGig33nij/vOf/+jJJ5/scp/H4wm77Zzrsq9TUVGRgsFgaGtoaIjJvAAAwIaovwel00033aRnn31Wa9eu1ZAhQ0L7/X6/pC+upGRkZIT2NzU1dbmq0snr9crr9cZqVAAAYEzUr6A453TjjTfq6aef1j/+8Q9lZWWF3Z+VlSW/36/q6urQvo6ODtXU1Cg3Nzfa4wAAgDgU9SsoN9xwg6qqqvSXv/xFKSkpofeVpKWlKSkpSR6PRwUFBSotLVV2drays7NVWlqq5ORkTZs2LdrjAACAOBT1QFmyZIkkKS8vL2z/8uXLde2110qS5s+fr7a2Ns2ZM0fNzc0aM2aM1qxZo5SUlGiPAwAA4lDUA8U5d9RjPB6PSkpKVFJSEu1/HgAA9AJ8Fw8AADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOYQKAAAwBwCBQAAmEOgAAAAcwgUAABgDoECAADMIVAAAIA5BAoAADCHQAEAAOb0aKAsXrxYWVlZ6t+/v3JycrRu3bqeHAcAABjRY4GyatUqFRQUqLi4WJs3b9aFF16oSy+9VB9++GFPjQQAAIzosUApLy/XzJkzdf311+u0005TRUWFMjMztWTJkp4aCQAAGJHQE/9oR0eH6urqVFhYGLY/Pz9ftbW1XY5vb29Xe3t76HYwGJQktbS0xGS+w+37Y3JeAADiRSxeYzvP6Zw76rE9Eih79uzRoUOH5PP5wvb7fD41NjZ2Ob6srEwLFizosj8zMzNmMwIAcDxLq4jduVtbW5WWlva1x/RIoHTyeDxht51zXfZJUlFRkebNmxe6ffjwYX366adKT08/4vHfRktLizIzM9XQ0KDU1NSonhv/h3XuPqx192Cduwfr3H1isdbOObW2tioQCBz12B4JlEGDBqlv375drpY0NTV1uaoiSV6vV16vN2zfCSecEMsRlZqayg9/N2Cduw9r3T1Y5+7BOnefaK/10a6cdOqRN8n269dPOTk5qq6uDttfXV2t3NzcnhgJAAAY0mO/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", + "## Additional Resources\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) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69d70f03-2a51-4619-9f7b-21056ad0d9c3", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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/Notebook2_Xarray_Cupy_BasicOperations.ipynb b/docs/source/Notebook2_Xarray_Cupy_BasicOperations.ipynb new file mode 100644 index 0000000..0c334e1 --- /dev/null +++ b/docs/source/Notebook2_Xarray_Cupy_BasicOperations.ipynb @@ -0,0 +1,4193 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0d6fecdf-48c0-4745-b802-2117fb3137cf", + "metadata": {}, + "source": [ + "# Basic Computations in CuPy-Xarray" + ] + }, + { + "cell_type": "markdown", + "id": "33f29f17-cfd2-478a-b913-6b126394df3a", + "metadata": {}, + "source": [ + "**Negin Sobhani**, **Deepak Cherian**, and **Max Jones** \n", + "negins@ucar.edu, dcherian@ucar.edu, and max@carbonplan.org\n", + "\n", + "------------" + ] + }, + { + "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": "8cfcfa1f-f74a-405b-957d-3c925aca7eb4", + "metadata": {}, + "source": [ + "In this notebook, we will explore the procedure of conducting scientific computations using Xarray objects that wrap Cupy Arrays. " + ] + }, + { + "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": "code", + "execution_count": 4, + "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)>\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": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Move data to host\n", + "da_np = da_cp.as_numpy()\n", + "da_np\n" + ] + }, + { + "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": 5, + "id": "b071bc93-e5dc-4185-b905-1842da06a45f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "da_np.cupy.is_cupy" + ] + }, + { + "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": [ + "result_np = da_np*2 + 200\n", + "print (type(result_np.data))" + ] + }, + { + "cell_type": "markdown", + "id": "4a23ebf6-ee17-48de-8474-83dcfe863b18", + "metadata": {}, + "source": [ + "### Statistics\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": [ + "
\n", + " NOTE: Most Xarray operations preserve array type. \n", + "
\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)\n" + ] + }, + { + "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) NOTE: \n", + "\n", + "If you encounter additional NaN values or missing data points after performing computations in xarray, it indicates that the coordinates of your xarray were not precisely aligned.\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "ac17a9bd-c963-42f2-8447-b298f8ef8cc9", + "metadata": {}, + "source": [ + "## Comparing Performance: CuPy with Xarray vs NumPy with Xarray\n", + "\n", + "To compare the performance of using CuPy with Xarray to using NumPy with Xarray, let's perform a test with both libraries. In the test below we calculate relative humidity from temperature and dew point temperature:" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "053d2780-72e3-417c-9382-333640055c8e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "n = 100\n", + "Xarray DataArrays using CuPy provides a 0.11 x speedup over NumPy.\n", + "\n", + "n = 1000\n", + "Xarray DataArrays using CuPy provides a 2.31 x speedup over NumPy.\n", + "\n", + "n = 5000\n", + "Xarray DataArrays using CuPy provides a 95.22 x speedup over NumPy.\n", + "\n", + "n = 10000\n", + "Xarray DataArrays using CuPy provides a 115.31 x speedup over NumPy.\n", + "\n" + ] + } + ], + "source": [ + "import time\n", + "sizes = [100, 1000, 5000, 10000]\n", + "np_times= []\n", + "cp_times= []\n", + "\n", + "speedups = []\n", + "\n", + "for n in sizes:\n", + " print(\"n =\", n)\n", + " temp_data = np.random.rand(n, n)\n", + " \n", + " temp_cpu = xr.DataArray(temp_data, dims=['lat', 'lon'])\n", + " temp_gpu = xr.DataArray(cp.asarray(temp_data), dims=['lat', 'lon'])\n", + " \n", + " dew_data = np.random.rand(n, n)\n", + " dew_cpu = xr.DataArray(dew_data, dims=['lat', 'lon'])\n", + " dew_gpu = xr.DataArray(cp.asarray(dew_data), dims=['lat', 'lon'])\n", + " \n", + " # Calculate the relative humidity using the Hydrometeorological equations\n", + " a = 17.27\n", + " b = 237.7\n", + " \n", + " start_time = time.time()\n", + " gamma_cpu = (a * dew_cpu) / (b + dew_cpu) + np.log(temp_cpu / dew_cpu)\n", + " rh_cpu = np.exp(gamma_cpu) * 100\n", + " end_time = time.time()\n", + " numpy_time = end_time - start_time\n", + " np_times.append(numpy_time)\n", + "\n", + " start_time = time.time()\n", + " gamma_gpu = (a * dew_gpu) / (b + dew_gpu) + np.log(temp_gpu / dew_gpu)\n", + " rh_gpu = np.exp(gamma_gpu) * 100\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", + " cp_times.append(cupy_time)\n", + " \n", + " speed_up = round(numpy_time / cupy_time,2)\n", + " speedups.append(speed_up)\n", + " print(\"Xarray DataArrays using CuPy provides a\", speed_up, \"x speedup over NumPy.\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "f524c2a8-64b1-4de1-bf30-b23ac18832a5", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA1QAAAHyCAYAAADsu4+HAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/P9b71AAAACXBIWXMAAA9hAAAPYQGoP6dpAACc90lEQVR4nOzdd3gUVd/G8e+mkoQ0ID0hBKT33gNIR2wIqKiIIuIjFkSk2EBUEFTgtVEsIGJ7EB8LHZVepHdBwBBaQqhJIH133j8WVmOoIcmk3J/r2ouZM7O7d0Ky2d+eM+dYDMMwEBERERERkRvmZHYAERERERGRokoFlYiIiIiISC6poBIREREREcklFVQiIiIiIiK5pIJKREREREQkl1RQiYiIiIiI5JIKKhERERERkVxSQSUiIiIiIpJLKqhERERERERySQWViBR7M2fOxGKxOG4uLi6EhIRw3333sX///lw95vLly7FYLCxfvvyG77tnzx5Gjx7NoUOHchzr168fFSpUyFWmmzF69GgsFgunTp267PFatWrRtm3bgg110Y18ry/3/bNYLIwePdqxf7Xvf15YtWoVvXv3JiwsDDc3N3x9fWnRogVTpkzhwoULN/x4+f0zUdx+lkVECpoKKhEpMWbMmMG6dev45ZdfeOqpp/jpp59o1aoVZ8+eLdAce/bs4bXXXrvsm9BXXnmF//3vfwWap7Br0KAB69ato0GDBrm6/7p163jssccc+1f7/t+sUaNGER0dzbFjx3j99ddZunQp33zzDe3bt2f06NG8/PLLef6cZtLPsogIuJgdQESkoNSqVYtGjRoB0LZtW6xWK6NGjeKHH37gkUceMTmdXaVKlcyOUOj4+PjQrFmzXN//Zu57I+bMmcOYMWPo378/H3/8MRaLxXGsa9euDBs2jHXr1hVIlsJAP8siUlKoh0pESqxLxdWJEyeytW/atIk77riDMmXKUKpUKerXr89///vfaz7epk2buO+++6hQoQIeHh5UqFCB+++/n9jYWMc5M2fOpFevXgC0a9fOMQxx5syZQM5hUvXr16d169Y5nstqtRIWFkaPHj0cbRkZGbzxxhtUq1YNd3d3AgICeOSRRzh58uR1f0+u15WGiR06dCjb1wP2r6l06dLs3buXzp074+XlRUhICG+99RYA69evp1WrVnh5eVGlShU+//zz63qumTNn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DL169WLLli188MEHVKlShY4dO171PiEhIdSuXZslS5aQkpJyzeuofvjhBy5cuMD3339PZGSko33btm3XzBcQEIDVaiU+Pp6QkJArnufu7p7j/wfIUbj8W379LJctW5asrCxOnjyZragyDIP4+HgaN26c68cWETGThvyJSIk1YcIE/P39efXVV7HZbFStWpXKlSuzfft2GjVqdNnblQoTi8WCYRjZLqoH+OSTT7BardnaLp1zvZ+0Ozs789BDD/HDDz+watUqNm3axKOPPprtnO7du3P69GmsVutlc1+pEMytS8PfduzYka39n7PM5ZeqVasSEhLC119/jWEYjvbY2FjWrl17zftf6/t/9913U758eZ5//nl++eUXnnzyycvO2vdvr7zyCmfPnuWZZ57JluuS8+fPs2TJEgDH4/3z58UwDD7++ONrPk/Xrl0BmDJlylXPq1ChQo7/n99++43z589f9X759bPcvn17AGbPnp2tfe7cuVy4cMFxXESkqFEPlYiUWP7+/owcOZJhw4bx1Vdf8eCDDzJt2jS6du1K586d6devH2FhYZw5c4Y//viDLVu2OK6t+TcfHx+io6N5++23KVeuHBUqVGDFihV8+umn+Pn5ZTu3Vq1aAEyfPh1vb29KlSpFVFTUZYc4XfLoo48yfvx4+vTpg4eHB/fee2+24/fddx9ffvkl3bp149lnn6VJkya4urpy9OhRli1bxp133sndd999c9+wfwgODqZDhw6MGzcOf39/IiMj+fXXX/n+++/z7DmuxMnJiddff53HHnuMu+++mwEDBnDu3DlGjx59XcPgrvX9d3Z2ZtCgQQwfPhwvL69rTjF+Sa9evXjllVd4/fXX2bt3L/3796dSpUqkpKTw+++/M23aNO699146depEx44dcXNz4/7772fYsGGkpaUxZcqU61pkunXr1jz00EO88cYbnDhxgu7du+Pu7s7WrVvx9PTk6aefBuChhx7ilVde4dVXX6VNmzbs2bOHDz74AF9f36s+fn79LHfs2JHOnTszfPhwkpKSaNmypWOWv/r16/PQQw9d1/dZRKTQMXNGDBGRgnBplr+NGzfmOJaammqUL1/eqFy5spGVlWUYhmFs377d6N27txEYGGi4uroawcHBxq233mpMnTrVcb/LzYx29OhR45577jH8/f0Nb29vo0uXLsauXbtyzKpmGIYxefJkIyoqynB2ds42K96/Z0b7pxYtWhiA8cADD1z2eGZmpvHOO+8YdevWNUqVKmWULl3aqFatmjFw4EBj//79V/0eXZrl7+TJk5c9XrNmzWyz/BmGYcTFxRk9e/Y0ypQpY/j6+hoPPvigsWnTpsvO8ufl5ZXjMdu0aWPUrFkzR3tkZKRx2223OfavNAvdJ598YlSuXNlwc3MzqlSpYnz22WeX/f5xmdnurvT9v+TQoUMGYDzxxBOX/X5czYoVK4yePXsaISEhhqurq+Hj42M0b97cePvtt42kpCTHeT///LPj/yosLMx44YUXjIULF+b4Wi/3NVmtVmPSpElGrVq1DDc3N8PX19do3ry58fPPPzvOSU9PN4YNG2ZEREQYHh4eRps2bYxt27Zd1yx/+fWznJqaagwfPtyIjIw0XF1djZCQEOM///mPcfbs2Wzn/ftn4JI2bdrk+DkUETGbxTAuMy5BRESkBHv//fd55pln2LVrFzVr1jQ7joiIFGIqqERERC7aunUrMTExDBw4kJYtW/LDDz+YHUlERAo5FVQiIiIXVahQgfj4eFq3bs0XX3yRr1OTi4hI8aCCSkREREREJJc0bbqIiIiIiEguqaASERERERHJJRVUIiIiIiIiuaSCSkREREREJJdUUImIiIiIiOSSCioREREREZFcUkElIiIiIiKSSyqoREREREREckkFlYiIiIiISC6poBIREREREcklFVQiIiIiIiK5pIJKREREREQkl1RQiYiIiIiI5JIKKhERERERkVxSQSUiIiIiIpJLKqhERERERERySQWViIiIiIhILqmgEhERERERySUVVCIiIiIiIrmkgkpERERERCSXVFCJiIiIiIjkkgoqERERERGRXFJBJSIiIiIikksqqERERERERHJJBZWIiIiIiEguqaASERERERHJJRVUIiIiIiIiuaSCSkREREREJJdUUImIiIiIiOSSCioREREREZFcUkElIiIiIiKSSyqoREREREREckkFlYiIiIiISC65mB2goNlsNo4fP463tzcWi8XsOCIiJYphGCQnJxMaGoqTk/mf6a1cuZK3336bzZs3ExcXx//+9z/uuusuADIzM3n55ZdZsGABf/31F76+vnTo0IG33nqL0NBQx2Okp6czdOhQvv76a1JTU2nfvj0fffQR4eHh151Df5tERMyRF3+X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\n", + "text/plain": [ + "
" + ] + }, + "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", + "## Additional Resources\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) " + ] + } + ], + "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/Notebook3_Xarray_Cupy_HighLevel.ipynb b/docs/source/Notebook3_Xarray_Cupy_HighLevel.ipynb new file mode 100644 index 0000000..8397134 --- /dev/null +++ b/docs/source/Notebook3_Xarray_Cupy_HighLevel.ipynb @@ -0,0 +1,714 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "93434031-d7fe-4322-a9cf-41e5b8be622d", + "metadata": {}, + "source": [ + "# High-level Xarray Functions: CuPy vs. NumPy" + ] + }, + { + "cell_type": "markdown", + "id": "e86bf0cb-ec9d-4745-80db-58e12166b272", + "metadata": {}, + "source": [ + "**Negin Sobhani**, **Deepak Cherian**, and **Max Jones** \n", + "negins@ucar.edu, dcherian@ucar.edu, and max@carbonplan.org\n", + "\n", + "------------" + ] + }, + { + "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 xarray as xr\n", + "import pandas as pd" + ] + }, + { + "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(data_np,\n", + " dims=['time', 'lat', 'lon'],\n", + " coords=[date, lat, lon],)\n", + "\n", + "# -- Create DataArray with CuPy data\n", + "data_xr_cp = xr.DataArray(data_cp,\n", + " dims=['time', 'lat', 'lon'],\n", + " coords=[date, lat, lon],)" + ] + }, + { + "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]\n" + ] + }, + { + "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(\"GroupBy with Xarray DataArrays using CuPy provides a\", round(time_np / time_cp,2), \"x speedup over NumPy.\\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]\n" + ] + }, + { + "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(data_np,\n", + " dims=['time', 'lat', 'lon'],\n", + " coords=[date, lat, lon],)\n", + "\n", + "# -- Create DataArray with CuPy data\n", + "data_xr_cp = xr.DataArray(data_cp,\n", + " dims=['time', 'lat', 'lon'],\n", + " coords=[date, lat, lon],)" + ] + }, + { + "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(\"GroupBy with Xarray DataArrays using CuPy provides a\", round(time_np / time_cp,2), \"x speedup over NumPy.\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "5bc0b970-7893-4a9c-b06a-941c134b8c2d", + "metadata": {}, + "source": [ + "Question: Is this consistent with what you have learned in the previous modules? What if we test a very low resolution dataset?" + ] + }, + { + "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\n" + ] + }, + { + "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(\"Rolling mean with Xarray DataArrays using CuPy provides a\", round(time_np / time_cp,2), \"x speedup over NumPy.\\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(\"Weighted mean with Xarray DataArrays using CuPy provides a\", round(time_np / time_cp,2), \"x speedup over NumPy.\\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(\"Coarsen with Xarray DataArrays using CuPy provides a\", round(time_np / time_cp,2), \"x speedup over NumPy.\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "02aede02-a9aa-499a-9531-04092902ac10", + "metadata": {}, + "source": [ + "## Apply custom kernels with `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 numpy arrays, and the function will take care of aligning the input data, looping over dimensions, and maintaining metadata. Since `apply_ufunc` operates on lower-level NumPy objects, it skips the overhead of using Xarray objects making it a good choice for performance-critical functions.\n", + "\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", + "In the example below, we calculate the saturation vapor pressure by using `apply_ufunc()`:" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "679d95f9-920e-4b15-b365-212d6c86b75e", + "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": 54, + "id": "cc9b79ca-03e2-447c-beb3-a84ab55c1415", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 237 ms, sys: 153 ms, total: 390 ms\n", + "Wall time: 409 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "es=xr.apply_ufunc(sat_p,data_xr_np)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "73bc5f6f-9ef1-4f6c-a74e-9e9dd95cf71e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 414 µs, sys: 187 µs, total: 601 µs\n", + "Wall time: 606 µs\n" + ] + } + ], + "source": [ + "%%time\n", + "es_cp=xr.apply_ufunc(sat_p,data_xr_cp)" + ] + }, + { + "cell_type": "markdown", + "id": "609a8590-ce22-40e4-b801-c795bc0f525b", + "metadata": {}, + "source": [ + "`apply_ufunc` also preserve the underlying data type." + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "107a7471-99bb-44ad-aacb-57f984a29725", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "es_cp.cupy.is_cupy" + ] + }, + { + "cell_type": "markdown", + "id": "95dccf03-bb64-43ef-883e-9b84081c4514", + "metadata": {}, + "source": [ + "Congratulations! You have now uncovered the high level operations and capabilities of Xarray using CuPy. \n", + "\n", + "## 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", + "## Additional Resources\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) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4afffcc-0850-479c-b35d-b5c821610405", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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/Notebook4_Xarray_Cupy_ApplyUfunc.ipynb b/docs/source/Notebook4_Xarray_Cupy_ApplyUfunc.ipynb new file mode 100644 index 0000000..c000b0b --- /dev/null +++ b/docs/source/Notebook4_Xarray_Cupy_ApplyUfunc.ipynb @@ -0,0 +1,610 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "93434031-d7fe-4322-a9cf-41e5b8be622d", + "metadata": {}, + "source": [ + "# Apply Custom Kernels with `xarray.apply_ufunc`" + ] + }, + { + "cell_type": "markdown", + "id": "e86bf0cb-ec9d-4745-80db-58e12166b272", + "metadata": {}, + "source": [ + "**Negin Sobhani**, **Deepak Cherian**, and **Max Jones** \n", + "negins@ucar.edu, dcherian@ucar.edu, and max@carbonplan.org\n", + "\n", + "------------" + ] + }, + { + "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 numpy arrays, and the function will take care of aligning the input data, looping over dimensions, and maintaining metadata. Since `apply_ufunc` operates on lower-level NumPy objects, it skips the overhead of using Xarray objects making it a good choice for performance-critical functions.\n", + "\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", + "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": 10, + "id": "e21d74da-e620-4737-91a4-0180f3703c75", + "metadata": {}, + "outputs": [], + "source": [ + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "377ac7ca-6c09-486f-95cc-8a2d599df800", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np \n", + "import xarray as xr\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "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": 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(data_np,\n", + " dims=['time', 'lat', 'lon'],\n", + " coords=[date, lat, lon],)\n", + "\n", + "# -- Create DataArray with CuPy data\n", + "data_xr_cp = xr.DataArray(data_cp,\n", + " dims=['time', 'lat', 'lon'],\n", + " coords=[date, lat, lon],)" + ] + }, + { + "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(\"GroupBy with Xarray DataArrays using CuPy provides a\", round(time_np / time_cp,2), \"x speedup over NumPy.\\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": [ + "
\n", + " apply_ufunc preserve 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(\"GroupBy with Xarray DataArrays using CuPy provides a\", round(time_np / time_cp,2), \"x speedup over NumPy.\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "f9ac9b58-8d04-444c-9f25-b4b098a48848", + "metadata": {}, + "source": [ + "Now, we can see much more speed-up using CuPy. The reason also explained in the first lesson. \n", + "\n", + "
NOTE: \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.\n", + "
" + ] + }, + { + "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": "markdown", + "id": "3b637831-0c01-44ff-92f1-c4e470d26667", + "metadata": {}, + "source": [ + "Here is the python code for calculating the relative humidity from temperature and dew point temperature:" + ] + }, + { + "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((17.67 * (dew_point - 273.15)) / (dew_point - 29.65)) # Saturation vapor pressure at dew point\n", + " es_dew = 6.112 * np.exp((17.67 * (temp - 273.15)) / (temp - 29.65)) # 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": [ + "**step 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", + "**step 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", + "**step 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": "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": 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(data_cp,\n", + " dims=['time', 'lat', 'lon'],\n", + " coords=[date, lat, lon],)\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(data_cp-offset,\n", + " dims=['time', 'lat', 'lon'],\n", + " coords=[date, lat, lon],)" + ] + }, + { + "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", + "## Additional Resources\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) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00d5dc5b-feb3-4628-9167-9b3ba8f2f647", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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/Notebook5_Xarray_Cupy_Example.ipynb b/docs/source/Notebook5_Xarray_Cupy_Example.ipynb new file mode 100644 index 0000000..df5583e --- /dev/null +++ b/docs/source/Notebook5_Xarray_Cupy_Example.ipynb @@ -0,0 +1,368 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "93434031-d7fe-4322-a9cf-41e5b8be622d", + "metadata": {}, + "source": [ + "# Xarray and CuPy (Real Examples)\n" + ] + }, + { + "cell_type": "markdown", + "id": "e86bf0cb-ec9d-4745-80db-58e12166b272", + "metadata": {}, + "source": [ + "Negin Sobhani, Deepak Cherian, and Max Jones \n", + "negins@ucar.edu, dcherian@ucar.edu, and max@carbonplan.org\n", + "------------" + ] + }, + { + "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 available through the registry of open data on AWS - https://registry.opendata.aws/nex-gddp-cmip6/." + ] + }, + { + "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. See the Xarray tutorial material on apply_ufunc for more.\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(\n", + " \"saturation_vapor_pressure\"\n", + ")\n", + "print(es.cupy.is_cupy)" + ] + }, + { + "cell_type": "markdown", + "id": "635b7f63-7c44-4440-a9f0-9dd682afb994", + "metadata": {}, + "source": [ + "### Add Plotting (Transfer to CPUs)" + ] + }, + { + "cell_type": "markdown", + "id": "b26f1c51-f404-41e6-a465-5dfb6e36b530", + "metadata": {}, + "source": [ + "We can plot the result, which will involve the data 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/6PXbRoke3u7p7k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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "es.isel(time=0).plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "589ba03c-4867-40a9-8b6c-c74750f757b4", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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 +} From f153451d28c6617e32b719ef2b4fcb28fdc08231 Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Thu, 17 Aug 2023 02:57:35 -0600 Subject: [PATCH 08/25] restructure --- docs/{source => }/api.rst | 0 docs/quickstart.ipynb | 3354 ------------------------------------- 2 files changed, 3354 deletions(-) rename docs/{source => }/api.rst (100%) delete mode 100644 docs/quickstart.ipynb diff --git a/docs/source/api.rst b/docs/api.rst similarity index 100% rename from docs/source/api.rst rename to docs/api.rst 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 -} From ce7a418d90c52d2e013bf990b16bc91b883352b8 Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Thu, 17 Aug 2023 03:59:36 -0600 Subject: [PATCH 09/25] adding contributing --- docs/source/contributing.rst | 229 +++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 docs/source/contributing.rst diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst new file mode 100644 index 0000000..49d29c0 --- /dev/null +++ b/docs/source/contributing.rst @@ -0,0 +1,229 @@ +.. _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 + + + +Running the performance test suite +---------------------------------- + +*cupy-xarray* is starting a suite of benchmarking tests using +`asv `__ to enable easy monitoring of +the performance of critical operations. These benchmarks are all found in the +``asv_bench`` directory. + +To use all features of asv, you will need either ``conda`` or ``virtualenv``. +For more details please check the `asv installation webpage +`_. + +To install asv:: + + pip install git+https://github.com/airspeed-velocity/asv + +If you need to run a benchmark, change your directory to ``asv_bench/`` and run:: + + asv continuous -f 1.1 main + +You can replace ``my-branch`` with the name of the branch you are working on. +The output will include "BENCHMARKS NOT SIGNIFICANTLY CHANGED" if the +benchmarks did not change by more than 10%. + +The command uses ``conda`` by default for creating the benchmark +environments. If you want to use virtualenv instead, write:: + + asv continuous -f 1.1 -E virtualenv main + +The ``-E virtualenv`` option should be added to all ``asv`` commands +that run benchmarks. The default value is defined in ``asv.conf.json``. + +If you want to only run a specific group of tests from a file, you can do it +using ``.`` as a separator. For example:: + + asv continuous -f 1.1 main HEAD -b benchmarks.Generator.time_batch_preload + +will only run the ``Generator.time_batch_preload`` benchmark defined in +``benchmarks.py``. + +Information on how to write a benchmark and how to use asv can be found in the +`asv documentation `_. + +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/requirements/doc.yml``. +You can create an environment for building the documentation using:: + + conda env create --file ci/requirements/docs.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. From 7f2914dff56eda38dedd7139526e34fc918c2d66 Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Thu, 17 Aug 2023 04:04:20 -0600 Subject: [PATCH 10/25] adding conf.py --- .readthedocs.yml | 9 +------ docs/conf.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 9 deletions(-) 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/docs/conf.py b/docs/conf.py index 12d16d6..b35572d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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", +} From 277218e715fb58526f740049d13a6b718b52cf99 Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Thu, 17 Aug 2023 04:05:51 -0600 Subject: [PATCH 11/25] updating the organization --- docs/index.md | 24 +++++++------ docs/source/tutorials-and-presentations.rst | 38 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 11 deletions(-) create mode 100644 docs/source/tutorials-and-presentations.rst diff --git a/docs/index.md b/docs/index.md index 74035e9..2da43d9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,14 +5,11 @@ [![Documentation Status](https://readthedocs.org/projects/cupy-xarray/badge/?version=latest&style=flat-square)](https://cupy-xarray.readthedocs.io/en/latest/?badge=latest) [![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, a GPU array library, and Xarray, 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. @@ -22,23 +19,27 @@ CuPy-Xarray is a Python library that leverages CuPy, a GPU array library, and Xa CuPy-Xarray can be installed using `pip` or `conda`: From anaconda: + ```bash conda install cupy-xarray -c conda-forge ``` From PyPI: + ```bash pip install cupy-xarray ``` The latest version from Github: -```bash + +```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). The original notebook also adapts from the content in [this NCAR tutorial to GPUs](https://github.com/NCAR/GPU_workshop/tree/workshop/13_CuPyAndLegate), and uses it to illustrate cupy-xarray and working with cupy arrays and Xarray objects in general. + +Large parts of this documentations comes from [SciPy 2023 Xarray on GPUs tutorial](https://negin513.github.io/cupy-xarray-tutorials/README.html). The original notebook also adapts from the content in [this NCAR tutorial to GPUs](https://github.com/NCAR/GPU_workshop/tree/workshop/13_CuPyAndLegate), and uses it to illustrate cupy-xarray and working with cupy arrays and Xarray objects in general. ## Contents @@ -55,14 +56,15 @@ pip install git+https://github.com/xarray-contrib/cupy-xarray.git source/Notebook2_Xarray_Cupy_BasicOperations source/Notebook3_Xarray_Cupy_HighLevel source/Notebook4_Xarray_Cupy_ApplyUfunc - -**Demo**: + + +** Tutorials & Presentations**: .. toctree:: :maxdepth: 1 - :caption: Demo + :caption: Tutorials & Presentations - source/Notebook5_Xarray_Cupy_Example + source/tutorials-and-presentations **Contributing**: @@ -79,6 +81,6 @@ pip install git+https://github.com/xarray-contrib/cupy-xarray.git :maxdepth: 1 :caption: API Reference - -``` \ No newline at end of file + +``` diff --git a/docs/source/tutorials-and-presentations.rst b/docs/source/tutorials-and-presentations.rst new file mode 100644 index 0000000..d0eb1d3 --- /dev/null +++ b/docs/source/tutorials-and-presentations.rst @@ -0,0 +1,38 @@ +.. _tutorials-and-presentations: + +Tutorials and Presentations +=========================== + +Tutorials +--------- + +.. toctree:: + :maxdepth: 1 + + CuPy-Xarray Demo using 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: source/Notebook5_Xarray_Cupy_Example.html + + +++ + CuPy-Xarray Demo + +Presentations +------------- + +.. card:: Xarray on GPUs! + + SciPy 2023 + ^^^ + + + | `Xarray on GPUs! `_ + | DOI: `10.5281/zenodo.8247471 `_ + + +++ + Negin Sobhani, Deepak Cherian, Max Jones From 6dd783fe7bbd141f08106afc94d66eaa68d67922 Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Thu, 17 Aug 2023 04:07:51 -0600 Subject: [PATCH 12/25] updating index.md --- docs/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 2da43d9..1c7f1e3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -58,7 +58,7 @@ Large parts of this documentations comes from [SciPy 2023 Xarray on GPUs tutoria source/Notebook4_Xarray_Cupy_ApplyUfunc -** Tutorials & Presentations**: +**Tutorials & Presentations**: .. toctree:: :maxdepth: 1 @@ -81,6 +81,8 @@ Large parts of this documentations comes from [SciPy 2023 Xarray on GPUs tutoria :maxdepth: 1 :caption: API Reference + api + ``` From 286cdd38bb52ac6e446ba06979896f642cb97f3d Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Thu, 17 Aug 2023 04:11:44 -0600 Subject: [PATCH 13/25] quick fix --- docs/source/Notebook5_Xarray_Cupy_Example.ipynb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/source/Notebook5_Xarray_Cupy_Example.ipynb b/docs/source/Notebook5_Xarray_Cupy_Example.ipynb index df5583e..c1dd4e3 100644 --- a/docs/source/Notebook5_Xarray_Cupy_Example.ipynb +++ b/docs/source/Notebook5_Xarray_Cupy_Example.ipynb @@ -14,7 +14,7 @@ "metadata": {}, "source": [ "Negin Sobhani, Deepak Cherian, and Max Jones \n", - "negins@ucar.edu, dcherian@ucar.edu, and max@carbonplan.org\n", + "\n", "------------" ] }, @@ -273,9 +273,7 @@ ], "source": [ "%%time\n", - "es = xr.apply_ufunc(sat_p, da, output_dtypes=[float]).rename(\n", - " \"saturation_vapor_pressure\"\n", - ")\n", + "es = xr.apply_ufunc(sat_p, da, output_dtypes=[float]).rename(\"saturation_vapor_pressure\")\n", "print(es.cupy.is_cupy)" ] }, From a887a3ca20c3491af5760b508c9041ba9c584402 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 17 Aug 2023 10:36:04 +0000 Subject: [PATCH 14/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source/Notebook0_Introduction.ipynb | 34 +++--- ...otebook2_Xarray_Cupy_BasicOperations.ipynb | 93 +++++++-------- .../Notebook3_Xarray_Cupy_HighLevel.ipynb | 106 +++++++++++------- .../Notebook4_Xarray_Cupy_ApplyUfunc.ipynb | 96 +++++++++------- 4 files changed, 190 insertions(+), 139 deletions(-) diff --git a/docs/source/Notebook0_Introduction.ipynb b/docs/source/Notebook0_Introduction.ipynb index 7c17b46..4e03a5d 100644 --- a/docs/source/Notebook0_Introduction.ipynb +++ b/docs/source/Notebook0_Introduction.ipynb @@ -127,7 +127,7 @@ "# 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))" + "print(type(arr_cpu))" ] }, { @@ -149,7 +149,7 @@ "# 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))" + "print(type(arr_gpu))" ] }, { @@ -181,7 +181,7 @@ "# 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))" + "print(type(arr_cpu))" ] }, { @@ -204,7 +204,7 @@ "source": [ "arr_gpu = cp.zeros((3, 4))\n", "print(\"On the GPU: \", arr_gpu)\n", - "print (type(arr_gpu))" + "print(type(arr_gpu))" ] }, { @@ -568,6 +568,8 @@ "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\")" @@ -617,20 +619,20 @@ "source": [ "speed_ups = []\n", "arr_sizes = []\n", - "sizes = [100,200,500,1000, 2000, 5000, 10000]\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", + "\n", " a_cp = cp.asarray(a_np)\n", " b_cp = cp.asarray(b_np)\n", - " \n", - " arr_size = a_cp.nbytes/ 1e6\n", + "\n", + " arr_size = a_cp.nbytes / 1e6\n", " print(f\"Size of variable: {arr_size:.2f} MB\")\n", - " \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", @@ -644,8 +646,8 @@ " end_time = time.time()\n", " cupy_time = end_time - start_time\n", "\n", - " speed_up = round(numpy_time / cupy_time,2)\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", @@ -680,11 +682,11 @@ "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.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()" ] }, diff --git a/docs/source/Notebook2_Xarray_Cupy_BasicOperations.ipynb b/docs/source/Notebook2_Xarray_Cupy_BasicOperations.ipynb index 0c334e1..3008466 100644 --- a/docs/source/Notebook2_Xarray_Cupy_BasicOperations.ipynb +++ b/docs/source/Notebook2_Xarray_Cupy_BasicOperations.ipynb @@ -74,7 +74,7 @@ "import cupy as cp\n", "import numpy as np\n", "import xarray as xr\n", - "import cupy_xarray # Adds .cupy to Xarray objects" + "import cupy_xarray # Adds .cupy to Xarray objects" ] }, { @@ -548,7 +548,7 @@ } ], "source": [ - "da_cp = xr.DataArray(arr_gpu, dims=['x', 'y'])\n", + "da_cp = xr.DataArray(arr_gpu, dims=[\"x\", \"y\"])\n", "\n", "da_cp" ] @@ -1000,7 +1000,7 @@ "source": [ "# Move data to host\n", "da_np = da_cp.as_numpy()\n", - "da_np\n" + "da_np" ] }, { @@ -1079,8 +1079,8 @@ } ], "source": [ - "result_cp = da_cp*2 + 200\n", - "print (type(result_cp.data))" + "result_cp = da_cp * 2 + 200\n", + "print(type(result_cp.data))" ] }, { @@ -1098,8 +1098,8 @@ } ], "source": [ - "result_np = da_np*2 + 200\n", - "print (type(result_np.data))" + "result_np = da_np * 2 + 200\n", + "print(type(result_np.data))" ] }, { @@ -1130,7 +1130,7 @@ "source": [ "%%time\n", "# calculate the mean along the x dimension\n", - "mean_cp = da_cp.mean(dim='x')" + "mean_cp = da_cp.mean(dim=\"x\")" ] }, { @@ -1148,7 +1148,7 @@ } ], "source": [ - "print (type(mean_cp.data))" + "print(type(mean_cp.data))" ] }, { @@ -1169,7 +1169,7 @@ "source": [ "%%time\n", "# calculate the mean along the x dimension\n", - "mean_np = da_np.mean(dim='x')" + "mean_np = da_np.mean(dim=\"x\")" ] }, { @@ -1187,7 +1187,7 @@ } ], "source": [ - "print (type(mean_np.data))" + "print(type(mean_np.data))" ] }, { @@ -1206,9 +1206,9 @@ } ], "source": [ - "%%time \n", + "%%time\n", "# calculate the standard deviation along the x and y dimensions\n", - "std_cp = da_cp.std(dim=['x', 'y'])" + "std_cp = da_cp.std(dim=[\"x\", \"y\"])" ] }, { @@ -1226,7 +1226,7 @@ } ], "source": [ - "print (type(std_cp.data))" + "print(type(std_cp.data))" ] }, { @@ -1245,9 +1245,9 @@ } ], "source": [ - "%%time \n", + "%%time\n", "# calculate the standard deviation along the x and y dimensions\n", - "std_np = da_np.std(dim=['x', 'y'])" + "std_np = da_np.std(dim=[\"x\", \"y\"])" ] }, { @@ -1265,7 +1265,7 @@ } ], "source": [ - "print (type(std_cp.data))" + "print(type(std_cp.data))" ] }, { @@ -1294,9 +1294,9 @@ } ], "source": [ - "%%time \n", + "%%time\n", "# calculate the median along the x dimension\n", - "med_cp = da_cp.median(dim=['x'])\n", + "med_cp = da_cp.median(dim=[\"x\"])\n", "type(med_cp.data)" ] }, @@ -1326,9 +1326,9 @@ } ], "source": [ - "%%time \n", + "%%time\n", "# calculate the median along the x dimension\n", - "med_np = da_np.median(dim=['x'])\n", + "med_np = da_np.median(dim=[\"x\"])\n", "type(med_np.data)" ] }, @@ -1488,7 +1488,7 @@ "source": [ "%%time\n", "round_cp = np.round(da_cp.mean(), 2)\n", - "type (round_cp.data)" + "type(round_cp.data)" ] }, { @@ -3191,7 +3191,7 @@ "metadata": {}, "outputs": [], "source": [ - "arr1_broadcasted, arr2_broadcasted = xr.broadcast(gpu_arr1, gpu_arr2)\n" + "arr1_broadcasted, arr2_broadcasted = xr.broadcast(gpu_arr1, gpu_arr2)" ] }, { @@ -4048,27 +4048,28 @@ ], "source": [ "import time\n", + "\n", "sizes = [100, 1000, 5000, 10000]\n", - "np_times= []\n", - "cp_times= []\n", + "np_times = []\n", + "cp_times = []\n", "\n", "speedups = []\n", "\n", "for n in sizes:\n", " print(\"n =\", n)\n", " temp_data = np.random.rand(n, n)\n", - " \n", - " temp_cpu = xr.DataArray(temp_data, dims=['lat', 'lon'])\n", - " temp_gpu = xr.DataArray(cp.asarray(temp_data), dims=['lat', 'lon'])\n", - " \n", + "\n", + " temp_cpu = xr.DataArray(temp_data, dims=[\"lat\", \"lon\"])\n", + " temp_gpu = xr.DataArray(cp.asarray(temp_data), dims=[\"lat\", \"lon\"])\n", + "\n", " dew_data = np.random.rand(n, n)\n", - " dew_cpu = xr.DataArray(dew_data, dims=['lat', 'lon'])\n", - " dew_gpu = xr.DataArray(cp.asarray(dew_data), dims=['lat', 'lon'])\n", - " \n", + " dew_cpu = xr.DataArray(dew_data, dims=[\"lat\", \"lon\"])\n", + " dew_gpu = xr.DataArray(cp.asarray(dew_data), dims=[\"lat\", \"lon\"])\n", + "\n", " # Calculate the relative humidity using the Hydrometeorological equations\n", " a = 17.27\n", " b = 237.7\n", - " \n", + "\n", " start_time = time.time()\n", " gamma_cpu = (a * dew_cpu) / (b + dew_cpu) + np.log(temp_cpu / dew_cpu)\n", " rh_cpu = np.exp(gamma_cpu) * 100\n", @@ -4083,8 +4084,8 @@ " end_time = time.time()\n", " cupy_time = end_time - start_time\n", " cp_times.append(cupy_time)\n", - " \n", - " speed_up = round(numpy_time / cupy_time,2)\n", + "\n", + " speed_up = round(numpy_time / cupy_time, 2)\n", " speedups.append(speed_up)\n", " print(\"Xarray DataArrays using CuPy provides a\", speed_up, \"x speedup over NumPy.\\n\")" ] @@ -4110,22 +4111,22 @@ "import matplotlib.pyplot as plt\n", "\n", "# Creating figure with two subplots\n", - "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10,5))\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.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", + "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()" ] diff --git a/docs/source/Notebook3_Xarray_Cupy_HighLevel.ipynb b/docs/source/Notebook3_Xarray_Cupy_HighLevel.ipynb index 8397134..651896a 100644 --- a/docs/source/Notebook3_Xarray_Cupy_HighLevel.ipynb +++ b/docs/source/Notebook3_Xarray_Cupy_HighLevel.ipynb @@ -73,7 +73,7 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np \n", + "import numpy as np\n", "import xarray as xr\n", "import pandas as pd" ] @@ -99,12 +99,12 @@ "np.random.seed(0)\n", "\n", "# Create the time range.\n", - "date = pd.date_range('2010-01-01', '2020-12-31', freq='M')\n", + "date = pd.date_range(\"2010-01-01\", \"2020-12-31\", freq=\"M\")\n", "\n", - "# Create the latitude range. \n", + "# Create the latitude range.\n", "lat = np.arange(-90, 90, 1)\n", "\n", - "# Create the longitude range. \n", + "# Create the longitude range.\n", "lon = np.arange(-180, 180, 1)\n", "\n", "# Create random data\n", @@ -112,14 +112,18 @@ "data_cp = cp.array(data_np)\n", "\n", "# -- Create DataArray with Numpy data\n", - "data_xr_np = xr.DataArray(data_np,\n", - " dims=['time', 'lat', 'lon'],\n", - " coords=[date, lat, lon],)\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(data_cp,\n", - " dims=['time', 'lat', 'lon'],\n", - " coords=[date, lat, lon],)" + "data_xr_cp = xr.DataArray(\n", + " data_cp,\n", + " dims=[\"time\", \"lat\", \"lon\"],\n", + " coords=[date, lat, lon],\n", + ")" ] }, { @@ -140,7 +144,7 @@ "source": [ "start_time_np = time.time()\n", "\n", - "grouped_data_np = data_xr_np.groupby('time.season')\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", @@ -173,7 +177,7 @@ } ], "source": [ - "[type(arr.data) for group, arr in grouped_data_np]\n" + "[type(arr.data) for group, arr in grouped_data_np]" ] }, { @@ -194,13 +198,17 @@ "source": [ "start_time_cp = time.time()\n", "\n", - "grouped_data_cp = data_xr_cp.groupby('time.season')\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(\"GroupBy with Xarray DataArrays using CuPy provides a\", round(time_np / time_cp,2), \"x speedup over NumPy.\\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", + ")" ] }, { @@ -229,7 +237,7 @@ } ], "source": [ - "[type(arr.data) for group, arr in grouped_data_cp]\n" + "[type(arr.data) for group, arr in grouped_data_cp]" ] }, { @@ -255,10 +263,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Create the latitude range. \n", + "# Create the latitude range.\n", "lat = np.arange(-90, 90, 0.5)\n", "\n", - "# Create the longitude range. \n", + "# Create the longitude range.\n", "lon = np.arange(-180, 180, 0.5)\n", "\n", "# Create random data\n", @@ -266,14 +274,18 @@ "data_cp = cp.array(data_np)\n", "\n", "# -- Create DataArray with Numpy data\n", - "data_xr_np = xr.DataArray(data_np,\n", - " dims=['time', 'lat', 'lon'],\n", - " coords=[date, lat, lon],)\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(data_cp,\n", - " dims=['time', 'lat', 'lon'],\n", - " coords=[date, lat, lon],)" + "data_xr_cp = xr.DataArray(\n", + " data_cp,\n", + " dims=[\"time\", \"lat\", \"lon\"],\n", + " coords=[date, lat, lon],\n", + ")" ] }, { @@ -285,7 +297,7 @@ "source": [ "start_time_np = time.time()\n", "\n", - "grouped_data_np = data_xr_np.groupby('time.season').mean()\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", @@ -310,13 +322,17 @@ "source": [ "start_time_cp = time.time()\n", "\n", - "grouped_data_cp = data_xr_cp.groupby('time.season').mean()\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(\"GroupBy with Xarray DataArrays using CuPy provides a\", round(time_np / time_cp,2), \"x speedup over NumPy.\\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", + ")" ] }, { @@ -376,7 +392,7 @@ "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\n" + "time_np = end_time_np - start_time_np" ] }, { @@ -410,7 +426,11 @@ } ], "source": [ - "print(\"Rolling mean with Xarray DataArrays using CuPy provides a\", round(time_np / time_cp,2), \"x speedup over NumPy.\\n\")" + "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", + ")" ] }, { @@ -437,8 +457,8 @@ "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", + "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" @@ -453,8 +473,8 @@ "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", + "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" @@ -476,7 +496,11 @@ } ], "source": [ - "print(\"Weighted mean with Xarray DataArrays using CuPy provides a\", round(time_np / time_cp,2), \"x speedup over NumPy.\\n\")" + "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", + ")" ] }, { @@ -508,7 +532,7 @@ "source": [ "start_time_np = time.time()\n", "\n", - "coarsen_np = data_xr_np.coarsen(time=3, lat=2,lon=2).mean()\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" @@ -531,7 +555,7 @@ "source": [ "start_time_cp = time.time()\n", "\n", - "coarsen_cp = data_xr_cp.coarsen(time=3, lat=2,lon=2).mean()\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" @@ -553,7 +577,11 @@ } ], "source": [ - "print(\"Coarsen with Xarray DataArrays using CuPy provides a\", round(time_np / time_cp,2), \"x speedup over NumPy.\\n\")" + "print(\n", + " \"Coarsen with Xarray DataArrays using CuPy provides a\",\n", + " round(time_np / time_cp, 2),\n", + " \"x speedup over NumPy.\\n\",\n", + ")" ] }, { @@ -581,7 +609,7 @@ "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)))" + " return 0.611 * np.exp(17.67 * (t - 273.15) * ((t - 29.65) ** (-1)))" ] }, { @@ -601,7 +629,7 @@ ], "source": [ "%%time\n", - "es=xr.apply_ufunc(sat_p,data_xr_np)\n" + "es = xr.apply_ufunc(sat_p, data_xr_np)" ] }, { @@ -621,7 +649,7 @@ ], "source": [ "%%time\n", - "es_cp=xr.apply_ufunc(sat_p,data_xr_cp)" + "es_cp = xr.apply_ufunc(sat_p, data_xr_cp)" ] }, { diff --git a/docs/source/Notebook4_Xarray_Cupy_ApplyUfunc.ipynb b/docs/source/Notebook4_Xarray_Cupy_ApplyUfunc.ipynb index c000b0b..fe78799 100644 --- a/docs/source/Notebook4_Xarray_Cupy_ApplyUfunc.ipynb +++ b/docs/source/Notebook4_Xarray_Cupy_ApplyUfunc.ipynb @@ -77,7 +77,7 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np \n", + "import numpy as np\n", "import xarray as xr\n", "import pandas as pd" ] @@ -103,12 +103,12 @@ "np.random.seed(0)\n", "\n", "# Create the time range.\n", - "date = pd.date_range('2010-01-01', '2020-12-31', freq='M')\n", + "date = pd.date_range(\"2010-01-01\", \"2020-12-31\", freq=\"M\")\n", "\n", - "# Create the latitude range. \n", + "# Create the latitude range.\n", "lat = np.arange(-90, 90, 1)\n", "\n", - "# Create the longitude range. \n", + "# Create the longitude range.\n", "lon = np.arange(-180, 180, 1)\n", "\n", "# Create random data\n", @@ -116,14 +116,18 @@ "data_cp = cp.array(data_np)\n", "\n", "# -- Create DataArray with Numpy data\n", - "data_xr_np = xr.DataArray(data_np,\n", - " dims=['time', 'lat', 'lon'],\n", - " coords=[date, lat, lon],)\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(data_cp,\n", - " dims=['time', 'lat', 'lon'],\n", - " coords=[date, lat, lon],)" + "data_xr_cp = xr.DataArray(\n", + " data_cp,\n", + " dims=[\"time\", \"lat\", \"lon\"],\n", + " coords=[date, lat, lon],\n", + ")" ] }, { @@ -144,7 +148,7 @@ "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)))" + " return 0.611 * np.exp(17.67 * (t - 273.15) * ((t - 29.65) ** (-1)))" ] }, { @@ -156,7 +160,7 @@ "source": [ "start_time_np = time.time()\n", "\n", - "es_np=xr.apply_ufunc(sat_p,data_xr_np)\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" @@ -201,12 +205,16 @@ "source": [ "start_time_cp = time.time()\n", "\n", - "es_cp=xr.apply_ufunc(sat_p,data_xr_cp)\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(\"GroupBy with Xarray DataArrays using CuPy provides a\", round(time_np / time_cp,2), \"x speedup over NumPy.\\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", + ")" ] }, { @@ -274,12 +282,16 @@ "source": [ "start_time_cp = time.time()\n", "\n", - "es_cp=xr.apply_ufunc(sat_p,data_xr_cp)\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(\"GroupBy with Xarray DataArrays using CuPy provides a\", round(time_np / time_cp,2), \"x speedup over NumPy.\\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", + ")" ] }, { @@ -349,19 +361,23 @@ "def calculate_relative_humidity(temp, dew_point):\n", " \"\"\"\n", " Calculate Relative Humidity using Revised Magnus coefficients by Alduchov and Eskridge.\n", - " \n", + "\n", " Args:\n", " temp (float): Temperature in Celsius.\n", " dew_point (float): Dew Point Temperature in Celsius.\n", - " \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((17.67 * (dew_point - 273.15)) / (dew_point - 29.65)) # Saturation vapor pressure at dew point\n", - " es_dew = 6.112 * np.exp((17.67 * (temp - 273.15)) / (temp - 29.65)) # Saturation vapor pressure at temperature\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" @@ -433,22 +449,26 @@ "outputs": [], "source": [ "# Create random data\n", - "data_cp = 20*(cp.random.rand(len(date), len(lat), len(lon)))\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(data_cp,\n", - " dims=['time', 'lat', 'lon'],\n", - " coords=[date, lat, lon],)\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", + "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(data_cp-offset,\n", - " dims=['time', 'lat', 'lon'],\n", - " coords=[date, lat, lon],)" + "temp_wet = xr.DataArray(\n", + " data_cp - offset,\n", + " dims=[\"time\", \"lat\", \"lon\"],\n", + " coords=[date, lat, lon],\n", + ")" ] }, { @@ -458,10 +478,10 @@ "metadata": {}, "outputs": [], "source": [ - "input_list = 'float64 temp, float64 dew_temp'\n", - "output_list = 'float64 rh'\n", + "input_list = \"float64 temp, float64 dew_temp\"\n", + "output_list = \"float64 rh\"\n", "\n", - "code_body = '''\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", @@ -471,7 +491,7 @@ "\n", " // Calculate relative humidity in percentage\n", " rh = (es_dew / es_temp) * 100.0;\n", - " '''" + " \"\"\"" ] }, { @@ -481,10 +501,10 @@ "metadata": {}, "outputs": [], "source": [ - "## -- define the elementwise kernel: \n", - "compute_call = cp.ElementwiseKernel(input_list, output_list, code_body, 'RH')\n", + "## -- 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)" + "kernel = compute_call(data_cp, data_cp - offset)" ] }, { @@ -505,9 +525,9 @@ "source": [ "%%time\n", "result = xr.apply_ufunc(\n", - " compute_call,\n", - " temp,\n", - " temp_wet,\n", + " compute_call,\n", + " temp,\n", + " temp_wet,\n", ")\n", "##result" ] From af3fd7063434f913aa42a3c67150e3d9cf53517a Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Thu, 17 Aug 2023 04:38:39 -0600 Subject: [PATCH 15/25] update doc.yml --- ci/doc.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From c8dd73556d49030b25cc9022679aef059994cf59 Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Thu, 17 Aug 2023 04:59:18 -0600 Subject: [PATCH 16/25] updates to readme badges --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1d34be1..43d7f60 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ > ⚠️ 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) From d359a2ad93a6599f426aa5b7138c6a5c9b7984d8 Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Thu, 17 Aug 2023 04:59:59 -0600 Subject: [PATCH 17/25] badges updates --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 1c7f1e3..11239d3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,7 +2,7 @@ ![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/en/latest/?badge=latest) +[![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/) From d114e1e8d86b83f244c9ab2986f9d38c3336cb98 Mon Sep 17 00:00:00 2001 From: Deepak Cherian Date: Fri, 27 Oct 2023 14:25:03 -0600 Subject: [PATCH 18/25] Update docs/index.md Co-authored-by: Jacob Tomlinson --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 11239d3..1a1e748 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,7 +18,7 @@ CuPy-Xarray is a Python library that leverages CuPy, a GPU array library, and Xa CuPy-Xarray can be installed using `pip` or `conda`: -From anaconda: +From Conda Forge: ```bash From bffcb6b2754508762a096baa23d710ed8dd3f143 Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Fri, 27 Oct 2023 14:41:46 -0600 Subject: [PATCH 19/25] fix index --- README.md | 1 + docs/source/contributing.rst | 45 ------------------------------------ 2 files changed, 1 insertion(+), 45 deletions(-) diff --git a/README.md b/README.md index 43d7f60..e51cfe5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # cupy-xarray +> [!IMPORTANT] > ⚠️ This project is looking for maintainers and contributors. Come help out! ![GitHub Workflow CI Status](https://img.shields.io/github/actions/workflow/status/xarray-contrib/cupy-xarray/pypi-release.yaml?style=flat) diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index 49d29c0..61888ce 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -134,51 +134,6 @@ framework for testing. You can run the test suite using:: pytest cupy-xarray - - -Running the performance test suite ----------------------------------- - -*cupy-xarray* is starting a suite of benchmarking tests using -`asv `__ to enable easy monitoring of -the performance of critical operations. These benchmarks are all found in the -``asv_bench`` directory. - -To use all features of asv, you will need either ``conda`` or ``virtualenv``. -For more details please check the `asv installation webpage -`_. - -To install asv:: - - pip install git+https://github.com/airspeed-velocity/asv - -If you need to run a benchmark, change your directory to ``asv_bench/`` and run:: - - asv continuous -f 1.1 main - -You can replace ``my-branch`` with the name of the branch you are working on. -The output will include "BENCHMARKS NOT SIGNIFICANTLY CHANGED" if the -benchmarks did not change by more than 10%. - -The command uses ``conda`` by default for creating the benchmark -environments. If you want to use virtualenv instead, write:: - - asv continuous -f 1.1 -E virtualenv main - -The ``-E virtualenv`` option should be added to all ``asv`` commands -that run benchmarks. The default value is defined in ``asv.conf.json``. - -If you want to only run a specific group of tests from a file, you can do it -using ``.`` as a separator. For example:: - - asv continuous -f 1.1 main HEAD -b benchmarks.Generator.time_batch_preload - -will only run the ``Generator.time_batch_preload`` benchmark defined in -``benchmarks.py``. - -Information on how to write a benchmark and how to use asv can be found in the -`asv documentation `_. - Contributing documentation ========================== From 0547364f210bfd15e9e8e129837194c20596519f Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Fri, 27 Oct 2023 14:42:34 -0600 Subject: [PATCH 20/25] update index to cover the comments --- docs/index.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 1a1e748..e58b5b6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,7 @@ ## Overview -CuPy-Xarray is a Python library that leverages CuPy, a GPU array library, and Xarray, 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. +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 @@ -39,7 +39,9 @@ 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). The original notebook also adapts from the content in [this NCAR tutorial to GPUs](https://github.com/NCAR/GPU_workshop/tree/workshop/13_CuPyAndLegate), and uses it to illustrate cupy-xarray and working with cupy arrays and Xarray objects in general. +Huge acknowledgements to @dcherian, @andersy005, @jacobtomlinson , ... for their contributions and work on this project. + +Documentation: Large parts of this documentations comes from [SciPy 2023 Xarray on GPUs tutorial](https://negin513.github.io/cupy-xarray-tutorials/README.html). The original notebook also adapts from the content in [this NCAR tutorial to GPUs](https://github.com/NCAR/GPU_workshop/tree/workshop/13_CuPyAndLegate), and uses it to illustrate cupy-xarray and working with cupy arrays and Xarray objects in general. ## Contents From 4d36043919c455fc5f3d4681d6da37b96c9ab66e Mon Sep 17 00:00:00 2001 From: dcherian Date: Fri, 27 Oct 2023 14:56:01 -0600 Subject: [PATCH 21/25] Edits. Co-authored-by: Max Jones <14077947+maxrjones@users.noreply.github.com> --- ...upy_ApplyUfunc.ipynb => apply-ufunc.ipynb} | 108 +--- ...rations.ipynb => basic-computations.ipynb} | 518 +----------------- ...0_Introduction.ipynb => cupy-basics.ipynb} | 35 +- ...y_HighLevel.ipynb => high-level-api.ipynb} | 141 +---- ...1_Xarray_Cupy.ipynb => introduction.ipynb} | 33 +- ...upy_Example.ipynb => real-example-1.ipynb} | 28 +- 6 files changed, 74 insertions(+), 789 deletions(-) rename docs/source/{Notebook4_Xarray_Cupy_ApplyUfunc.ipynb => apply-ufunc.ipynb} (83%) rename docs/source/{Notebook2_Xarray_Cupy_BasicOperations.ipynb => basic-computations.ipynb} (90%) rename docs/source/{Notebook0_Introduction.ipynb => cupy-basics.ipynb} (98%) rename docs/source/{Notebook3_Xarray_Cupy_HighLevel.ipynb => high-level-api.ipynb} (82%) rename docs/source/{Notebook1_Xarray_Cupy.ipynb => introduction.ipynb} (99%) rename docs/source/{Notebook5_Xarray_Cupy_Example.ipynb => real-example-1.ipynb} (99%) diff --git a/docs/source/Notebook4_Xarray_Cupy_ApplyUfunc.ipynb b/docs/source/apply-ufunc.ipynb similarity index 83% rename from docs/source/Notebook4_Xarray_Cupy_ApplyUfunc.ipynb rename to docs/source/apply-ufunc.ipynb index fe78799..1bb5d46 100644 --- a/docs/source/Notebook4_Xarray_Cupy_ApplyUfunc.ipynb +++ b/docs/source/apply-ufunc.ipynb @@ -5,18 +5,7 @@ "id": "93434031-d7fe-4322-a9cf-41e5b8be622d", "metadata": {}, "source": [ - "# Apply Custom Kernels with `xarray.apply_ufunc`" - ] - }, - { - "cell_type": "markdown", - "id": "e86bf0cb-ec9d-4745-80db-58e12166b272", - "metadata": {}, - "source": [ - "**Negin Sobhani**, **Deepak Cherian**, and **Max Jones** \n", - "negins@ucar.edu, dcherian@ucar.edu, and max@carbonplan.org\n", - "\n", - "------------" + "# Custom Kernels with `apply_ufunc`" ] }, { @@ -44,9 +33,11 @@ "\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 numpy arrays, and the function will take care of aligning the input data, looping over dimensions, and maintaining metadata. Since `apply_ufunc` operates on lower-level NumPy objects, it skips the overhead of using Xarray objects making it a good choice for performance-critical functions.\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", @@ -60,37 +51,20 @@ "We'll use a 3-dimensional dataset (time, latitude, longitude) with random values:" ] }, - { - "cell_type": "code", - "execution_count": 10, - "id": "e21d74da-e620-4737-91a4-0180f3703c75", - "metadata": {}, - "outputs": [], - "source": [ - "import time" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "377ac7ca-6c09-486f-95cc-8a2d599df800", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import xarray as xr\n", - "import pandas as pd" - ] - }, { "cell_type": "code", "execution_count": 12, - "id": "447116b1-bc54-4d0a-af46-5908a7f95e93", + "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" + "import cupy_xarray # Adds .cupy to Xarray objects\n", + "import numpy as np\n", + "import pandas as pd\n", + "import xarray as xr" ] }, { @@ -211,7 +185,7 @@ "time_cp = end_time_cp - start_time_cp\n", "\n", "print(\n", - " \"GroupBy with Xarray DataArrays using CuPy provides a\",\n", + " \"apply_ufunc with Xarray DataArrays using CuPy provides a\",\n", " round(time_np / time_cp, 2),\n", " \"x speedup over NumPy.\\n\",\n", ")" @@ -251,9 +225,9 @@ "id": "5ca7bf10-7919-41ad-acd8-5bc617b627ba", "metadata": {}, "source": [ - "
\n", - " apply_ufunc preserve the underlying data type.\n", - "
" + "```{important}\n", + "`apply_ufunc` preserves the underlying data type.\n", + "```" ] }, { @@ -288,7 +262,7 @@ "time_cp = end_time_cp - start_time_cp\n", "\n", "print(\n", - " \"GroupBy with Xarray DataArrays using CuPy provides a\",\n", + " \"apply_ufunc with Xarray DataArrays using CuPy provides a\",\n", " round(time_np / time_cp, 2),\n", " \"x speedup over NumPy.\\n\",\n", ")" @@ -299,12 +273,9 @@ "id": "f9ac9b58-8d04-444c-9f25-b4b098a48848", "metadata": {}, "source": [ - "Now, we can see much more speed-up using CuPy. The reason also explained in the first lesson. \n", - "\n", - "
NOTE: \n", + "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.\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." ] }, { @@ -343,14 +314,6 @@ "$$\n" ] }, - { - "cell_type": "markdown", - "id": "3b637831-0c01-44ff-92f1-c4e470d26667", - "metadata": {}, - "source": [ - "Here is the python code for calculating the relative humidity from temperature and dew point temperature:" - ] - }, { "cell_type": "code", "execution_count": 20, @@ -396,12 +359,12 @@ "id": "62a5996b-1a3d-443b-aa73-2594c52f96ba", "metadata": {}, "source": [ - "**step 1:** Set the list of input and output arguments and their data types: \n", + "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", + " * input arguments : `float32 temp`, `float32 d_temp`\n", + " * output arguments : `float32 rh`\n", "\n", - "**step 2:** Write the code body: \n", + "2. Write the code body: \n", "``` C\n", " temp += 273.15;\n", " dew_point += 273.15;\n", @@ -417,7 +380,7 @@ " \n", "```\n", "\n", - "**step 3:** Define the element-wise class and set the kernel name: \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", @@ -433,14 +396,6 @@ "Now let's test to see how this works in a real example: " ] }, - { - "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": 87, @@ -579,24 +534,17 @@ "\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", + "* 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", - "## Additional Resources\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) " + "[Cupy-Xarray Github](https://github.com/xarray-contrib/cupy-xarray.git)\n", + "```" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "00d5dc5b-feb3-4628-9167-9b3ba8f2f647", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/source/Notebook2_Xarray_Cupy_BasicOperations.ipynb b/docs/source/basic-computations.ipynb similarity index 90% rename from docs/source/Notebook2_Xarray_Cupy_BasicOperations.ipynb rename to docs/source/basic-computations.ipynb index 3008466..c6b2545 100644 --- a/docs/source/Notebook2_Xarray_Cupy_BasicOperations.ipynb +++ b/docs/source/basic-computations.ipynb @@ -5,18 +5,7 @@ "id": "0d6fecdf-48c0-4745-b802-2117fb3137cf", "metadata": {}, "source": [ - "# Basic Computations in CuPy-Xarray" - ] - }, - { - "cell_type": "markdown", - "id": "33f29f17-cfd2-478a-b913-6b126394df3a", - "metadata": {}, - "source": [ - "**Negin Sobhani**, **Deepak Cherian**, and **Max Jones** \n", - "negins@ucar.edu, dcherian@ucar.edu, and max@carbonplan.org\n", - "\n", - "------------" + "# Basic Computations" ] }, { @@ -46,14 +35,6 @@ "## Introduction " ] }, - { - "cell_type": "markdown", - "id": "8cfcfa1f-f74a-405b-957d-3c925aca7eb4", - "metadata": {}, - "source": [ - "In this notebook, we will explore the procedure of conducting scientific computations using Xarray objects that wrap Cupy Arrays. " - ] - }, { "cell_type": "markdown", "id": "77343efb-de6d-423c-b1cd-934c5d6d68e1", @@ -553,485 +534,6 @@ "da_cp" ] }, - { - "cell_type": "code", - "execution_count": 4, - "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)>\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": 4, - "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": 5, - "id": "b071bc93-e5dc-4185-b905-1842da06a45f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "False" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "da_np.cupy.is_cupy" - ] - }, { "cell_type": "markdown", "id": "62af1f7c-0ac2-4bad-ab92-8f1bcfbaffe3", @@ -1098,6 +600,7 @@ } ], "source": [ + "da_np = da_cp.as_numpy()\n", "result_np = da_np * 2 + 200\n", "print(type(result_np.data))" ] @@ -1107,7 +610,7 @@ "id": "4a23ebf6-ee17-48de-8474-83dcfe863b18", "metadata": {}, "source": [ - "### Statistics\n", + "### 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)." ] @@ -1409,9 +912,9 @@ "id": "629a7339-b7ad-4636-a5f3-93e0772829f0", "metadata": {}, "source": [ - "
\n", - " NOTE: Most Xarray operations preserve array type. \n", - "
\n" + "```{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", + "```" ] }, { @@ -4003,9 +3506,9 @@ "source": [ "Broadcasting in xarray simplifies the process of working with arrays of different shapes, as it automatically handles the alignment and expansion required for performing computations. It reduces the need for manual reshaping or padding of arrays, making the code more concise and readable. When doing certain computations on two arrays (additions), Xarray automatically broadcast the arrays to match the dimension shapes. To learn more about `xarray.align` , you can check [the user guide](https://docs.xarray.dev/en/stable/generated/xarray.broadcast.html).\n", "\n", - "
NOTE: \n", - "\n", + "```{note}\n", "If you encounter additional NaN values or missing data points after performing computations in xarray, it indicates that the coordinates of your xarray were not precisely aligned.\n", + "```\n", "\n", "\n", "\n" @@ -4155,11 +3658,12 @@ "* Understand two important concepts: broadcasting and alignment.\n", "* Performance of Cupy vs. Numpy on different array sizes. \n", "\n", - "## Additional Resources\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) " + "[Cupy-Xarray Github](https://github.com/xarray-contrib/cupy-xarray.git) \n", + "```" ] } ], diff --git a/docs/source/Notebook0_Introduction.ipynb b/docs/source/cupy-basics.ipynb similarity index 98% rename from docs/source/Notebook0_Introduction.ipynb rename to docs/source/cupy-basics.ipynb index 4e03a5d..1680a1c 100644 --- a/docs/source/Notebook0_Introduction.ipynb +++ b/docs/source/cupy-basics.ipynb @@ -8,18 +8,6 @@ "# Basics of CuPy" ] }, - { - "cell_type": "markdown", - "id": "33f29f17-cfd2-478a-b913-6b126394df3a", - "metadata": {}, - "source": [ - "**Negin Sobhani**, **Deepak Cherian**, and **Max Jones**\n", - "\n", - "negins@ucar.edu, dcherian@ucar.edu, max@carbonplan.org\n", - "\n", - "------------" - ] - }, { "cell_type": "markdown", "id": "15a05d43-0bf5-48d3-9c88-6074eed82a04", @@ -238,9 +226,8 @@ "\n", "Cupy also provides equivalant functions for some SciPy functions, but its implementation is not as extensive as NumPy's.\n", "\n", - "A full list of CuPy's Numpy and Scipy equivalent functions are provided on the link below:\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", - "[Complete Comparison of NumPy and SciPy to CuPy functions](https://docs.cupy.dev/en/stable/reference/comparison.html)\n", "\n", "[CuPy API Reference](https://docs.cupy.dev/en/stable/reference/index.html)" ] @@ -695,8 +682,11 @@ "id": "93da5acf-ba39-4617-8246-ac7f7b3fd8df", "metadata": {}, "source": [ - "
NOTE: \n", - "As we can see above, GPUs computations can be slower than CPUs. There are several reasons for this: \n", + "```{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", @@ -720,20 +710,13 @@ "* Data Transfer between Device and Host\n", "* Performance of Cupy vs. Numpy on different array sizes. \n", "\n", - "## Additional Resources\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)" + "[CuPy User Guide](https://docs.cupy.dev/en/stable/user_guide/index.html)\n", + "```" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b5c6373c-79cd-4829-b8f4-1c11eb112e98", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/source/Notebook3_Xarray_Cupy_HighLevel.ipynb b/docs/source/high-level-api.ipynb similarity index 82% rename from docs/source/Notebook3_Xarray_Cupy_HighLevel.ipynb rename to docs/source/high-level-api.ipynb index 651896a..b329be5 100644 --- a/docs/source/Notebook3_Xarray_Cupy_HighLevel.ipynb +++ b/docs/source/high-level-api.ipynb @@ -5,18 +5,7 @@ "id": "93434031-d7fe-4322-a9cf-41e5b8be622d", "metadata": {}, "source": [ - "# High-level Xarray Functions: CuPy vs. NumPy" - ] - }, - { - "cell_type": "markdown", - "id": "e86bf0cb-ec9d-4745-80db-58e12166b272", - "metadata": {}, - "source": [ - "**Negin Sobhani**, **Deepak Cherian**, and **Max Jones** \n", - "negins@ucar.edu, dcherian@ucar.edu, and max@carbonplan.org\n", - "\n", - "------------" + "# High-level Computation" ] }, { @@ -74,8 +63,8 @@ "outputs": [], "source": [ "import numpy as np\n", - "import xarray as xr\n", - "import pandas as pd" + "import pandas as pd\n", + "import xarray as xr" ] }, { @@ -340,7 +329,9 @@ "id": "5bc0b970-7893-4a9c-b06a-941c134b8c2d", "metadata": {}, "source": [ - "Question: Is this consistent with what you have learned in the previous modules? What if we test a very low resolution dataset?" + "```{attention}\n", + "Is this consistent with what you have learned in the previous modules? What if we test a very low resolution dataset?\n", + "```" ] }, { @@ -356,7 +347,7 @@ "\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:" + "In the example below, we calculate the rolling mean along the `'time'` dimension with a window size of 10:" ] }, { @@ -584,110 +575,14 @@ ")" ] }, - { - "cell_type": "markdown", - "id": "02aede02-a9aa-499a-9531-04092902ac10", - "metadata": {}, - "source": [ - "## Apply custom kernels with `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 numpy arrays, and the function will take care of aligning the input data, looping over dimensions, and maintaining metadata. Since `apply_ufunc` operates on lower-level NumPy objects, it skips the overhead of using Xarray objects making it a good choice for performance-critical functions.\n", - "\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", - "In the example below, we calculate the saturation vapor pressure by using `apply_ufunc()`:" - ] - }, - { - "cell_type": "code", - "execution_count": 53, - "id": "679d95f9-920e-4b15-b365-212d6c86b75e", - "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": 54, - "id": "cc9b79ca-03e2-447c-beb3-a84ab55c1415", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 237 ms, sys: 153 ms, total: 390 ms\n", - "Wall time: 409 ms\n" - ] - } - ], - "source": [ - "%%time\n", - "es = xr.apply_ufunc(sat_p, data_xr_np)" - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "id": "73bc5f6f-9ef1-4f6c-a74e-9e9dd95cf71e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 414 µs, sys: 187 µs, total: 601 µs\n", - "Wall time: 606 µs\n" - ] - } - ], - "source": [ - "%%time\n", - "es_cp = xr.apply_ufunc(sat_p, data_xr_cp)" - ] - }, - { - "cell_type": "markdown", - "id": "609a8590-ce22-40e4-b801-c795bc0f525b", - "metadata": {}, - "source": [ - "`apply_ufunc` also preserve the underlying data type." - ] - }, - { - "cell_type": "code", - "execution_count": 58, - "id": "107a7471-99bb-44ad-aacb-57f984a29725", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 58, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "es_cp.cupy.is_cupy" - ] - }, { "cell_type": "markdown", "id": "95dccf03-bb64-43ef-883e-9b84081c4514", - "metadata": {}, + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, "source": [ - "Congratulations! You have now uncovered the high level operations and capabilities of Xarray using CuPy. \n", - "\n", "## Summary\n", "\n", "In this notebook, we have learned about:\n", @@ -695,20 +590,12 @@ "* High level Xarray computations using CuPy arrays. \n", "* Applying custom kernels to DataArray with CuPy and NumPy\n", "\n", - "## Additional Resources\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) " + "[Cupy-Xarray Github](https://github.com/xarray-contrib/cupy-xarray.git) \n", + "```" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d4afffcc-0850-479c-b35d-b5c821610405", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/source/Notebook1_Xarray_Cupy.ipynb b/docs/source/introduction.ipynb similarity index 99% rename from docs/source/Notebook1_Xarray_Cupy.ipynb rename to docs/source/introduction.ipynb index bae06c3..8a68f8b 100644 --- a/docs/source/Notebook1_Xarray_Cupy.ipynb +++ b/docs/source/introduction.ipynb @@ -5,18 +5,7 @@ "id": "0d6fecdf-48c0-4745-b802-2117fb3137cf", "metadata": {}, "source": [ - "# Introduction to CuPy-Xarray" - ] - }, - { - "cell_type": "markdown", - "id": "33f29f17-cfd2-478a-b913-6b126394df3a", - "metadata": {}, - "source": [ - "**Negin Sobhani**, **Deepak Cherian**, and **Max Jones** \n", - "negins@ucar.edu, dcherian@ucar.edu, and max@carbonplan.org\n", - "\n", - "------------" + "# Introduction" ] }, { @@ -1292,7 +1281,7 @@ " 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\n", + "da_cp1.data.device # 1\n", "```" ] }, @@ -1302,7 +1291,7 @@ "metadata": {}, "source": [ "#### Transferring Data between Host and Device\n", - "Xarray provides DataArray.as_numpy to convert all kinds of arrays to NumPy arrays.\n" + "Xarray provides `DataArray.as_numpy` to convert all kinds of arrays to NumPy arrays.\n" ] }, { @@ -1935,20 +1924,12 @@ "* CuPy-Xarray Basics\n", "* Data Transfer between Device to Host \n", "\n", - "## Additional Resources\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) " + "[Cupy-Xarray Github](https://github.com/xarray-contrib/cupy-xarray.git)\n", + "```" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "69d70f03-2a51-4619-9f7b-21056ad0d9c3", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -1967,7 +1948,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.10.13" }, "widgets": { "application/vnd.jupyter.widget-state+json": { diff --git a/docs/source/Notebook5_Xarray_Cupy_Example.ipynb b/docs/source/real-example-1.ipynb similarity index 99% rename from docs/source/Notebook5_Xarray_Cupy_Example.ipynb rename to docs/source/real-example-1.ipynb index c1dd4e3..2c074c7 100644 --- a/docs/source/Notebook5_Xarray_Cupy_Example.ipynb +++ b/docs/source/real-example-1.ipynb @@ -5,17 +5,7 @@ "id": "93434031-d7fe-4322-a9cf-41e5b8be622d", "metadata": {}, "source": [ - "# Xarray and CuPy (Real Examples)\n" - ] - }, - { - "cell_type": "markdown", - "id": "e86bf0cb-ec9d-4745-80db-58e12166b272", - "metadata": {}, - "source": [ - "Negin Sobhani, Deepak Cherian, and Max Jones \n", - "\n", - "------------" + "# A real world example" ] }, { @@ -67,7 +57,7 @@ "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 available through the registry of open data on AWS - https://registry.opendata.aws/nex-gddp-cmip6/." + "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." ] }, { @@ -233,7 +223,7 @@ "\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. See the Xarray tutorial material on apply_ufunc for more.\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." ] @@ -282,7 +272,7 @@ "id": "635b7f63-7c44-4440-a9f0-9dd682afb994", "metadata": {}, "source": [ - "### Add Plotting (Transfer to CPUs)" + "### Add Plotting" ] }, { @@ -290,7 +280,7 @@ "id": "b26f1c51-f404-41e6-a465-5dfb6e36b530", "metadata": {}, "source": [ - "We can plot the result, which will involve the data being transferred to the host" + "We can plot the result, which will involve the data *automatically* being transferred to the host" ] }, { @@ -325,14 +315,6 @@ "source": [ "es.isel(time=0).plot()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "589ba03c-4867-40a9-8b6c-c74750f757b4", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From e2ea02b8e6e5b7686aa40718e26e65b8c185b3aa Mon Sep 17 00:00:00 2001 From: dcherian Date: Fri, 27 Oct 2023 15:09:12 -0600 Subject: [PATCH 22/25] more edits. --- docs/conf.py | 2 +- docs/index.md | 18 +++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b35572d..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" diff --git a/docs/index.md b/docs/index.md index e58b5b6..3bbd9a0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,9 +39,7 @@ pip install git+https://github.com/xarray-contrib/cupy-xarray.git ## Acknowledgements -Huge acknowledgements to @dcherian, @andersy005, @jacobtomlinson , ... for their contributions and work on this project. - -Documentation: Large parts of this documentations comes from [SciPy 2023 Xarray on GPUs tutorial](https://negin513.github.io/cupy-xarray-tutorials/README.html). The original notebook also adapts from the content in [this NCAR tutorial to GPUs](https://github.com/NCAR/GPU_workshop/tree/workshop/13_CuPyAndLegate), and uses it to illustrate cupy-xarray and working with cupy arrays and Xarray objects in general. +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 @@ -53,11 +51,12 @@ Documentation: Large parts of this documentations comes from [SciPy 2023 Xarray :maxdepth: 1 :caption: User Guide - source/Notebook0_Introduction - source/Notebook1_Xarray_Cupy - source/Notebook2_Xarray_Cupy_BasicOperations - source/Notebook3_Xarray_Cupy_HighLevel - source/Notebook4_Xarray_Cupy_ApplyUfunc + source/cupy-basics + source/introduction + source/basic-computations + source/high-level-api + source/apply-ufunc + source/real-example-1 **Tutorials & Presentations**: @@ -84,7 +83,4 @@ Documentation: Large parts of this documentations comes from [SciPy 2023 Xarray :caption: API Reference api - - - ``` From 1c6c18d24cd53e68660fc48187672b4d68c0fc1a Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Fri, 27 Oct 2023 15:10:53 -0600 Subject: [PATCH 23/25] update the dos locations --- docs/source/contributing.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index 61888ce..4eafd43 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -140,10 +140,10 @@ 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/requirements/doc.yml``. +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/requirements/docs.yml + conda env create --file ci/doc.yml conda activate cupy-xarray-docs You can then build the documentation using:: From 9455046fcf24aca0b42a89ed6edcf8df90ec89a3 Mon Sep 17 00:00:00 2001 From: Negin Sobhani Date: Fri, 27 Oct 2023 15:11:10 -0600 Subject: [PATCH 24/25] fix the bad link in tutorials --- docs/source/tutorials-and-presentations.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/source/tutorials-and-presentations.rst b/docs/source/tutorials-and-presentations.rst index d0eb1d3..a687f48 100644 --- a/docs/source/tutorials-and-presentations.rst +++ b/docs/source/tutorials-and-presentations.rst @@ -9,7 +9,8 @@ Tutorials .. toctree:: :maxdepth: 1 - CuPy-Xarray Demo using NASA Earth Exchange Global Daily Downscaled Projections (NEX-GDDP-CMIP6) Data + demo + .. grid:: 1 2 2 2 :gutter: 2 @@ -17,7 +18,9 @@ Tutorials .. grid-item-card:: :text-align: center - :link: source/Notebook5_Xarray_Cupy_Example.html + :link: Notebook5_Xarray_Cupy_Example.html + + CuPy-Xarray Demo using NASA Earth Exchange Global Daily Downscaled Projections (NEX-GDDP-CMIP6) Data +++ CuPy-Xarray Demo From 11c6e0a0e72ce9d73e08e8dc99098434ed26721c Mon Sep 17 00:00:00 2001 From: dcherian Date: Fri, 27 Oct 2023 15:20:23 -0600 Subject: [PATCH 25/25] Small fix --- docs/source/tutorials-and-presentations.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/source/tutorials-and-presentations.rst b/docs/source/tutorials-and-presentations.rst index a687f48..ab98d05 100644 --- a/docs/source/tutorials-and-presentations.rst +++ b/docs/source/tutorials-and-presentations.rst @@ -9,8 +9,7 @@ Tutorials .. toctree:: :maxdepth: 1 - demo - + Analyzing NASA Earth Exchange Global Daily Downscaled Projections (NEX-GDDP-CMIP6) Data .. grid:: 1 2 2 2 :gutter: 2 @@ -18,9 +17,9 @@ Tutorials .. grid-item-card:: :text-align: center - :link: Notebook5_Xarray_Cupy_Example.html + :link: real-example-1.html - CuPy-Xarray Demo using NASA Earth Exchange Global Daily Downscaled Projections (NEX-GDDP-CMIP6) Data + Demo using NASA Earth Exchange Global Daily Downscaled Projections (NEX-GDDP-CMIP6) Data +++ CuPy-Xarray Demo