diff --git a/_toc.yml b/_toc.yml index 47e8d35c..33d72793 100644 --- a/_toc.yml +++ b/_toc.yml @@ -60,6 +60,9 @@ parts: sections: - file: advanced/backends/1.Backend_without_Lazy_Loading.ipynb - file: advanced/backends/2.Backend_with_Lazy_Loading.ipynb + - file: advanced/accessors/accessors.md + sections: + - file: advanced/accessors/01_accessor_examples.ipynb - caption: Workshops chapters: diff --git a/advanced/accessors/01_accessor_examples.ipynb b/advanced/accessors/01_accessor_examples.ipynb new file mode 100644 index 00000000..ab96e8bd --- /dev/null +++ b/advanced/accessors/01_accessor_examples.ipynb @@ -0,0 +1,429 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Creating custom accessors" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Introduction\n", + "\n", + "An accessor is a way of attaching a custom function to xarray objects so that it can be called as if it were a method while retaining a clear separation between the \"core\" xarray API and custom API. It enables you to easily *extend* (which is why you'll sometimes see it referred to as an extension) and customize xarray's functionality while limiting naming conflicts and minimizing the chances of your code breaking with xarray upgrades.\n", + "\n", + "If you've used [rioxarray](https://corteva.github.io/rioxarray/stable/) (e.g. `da.rio.crs`) or [hvplot](https://hvplot.holoviz.org/) (e.g. `ds.hvplot()`), you may have already used an xarray accessor without knowing it!\n", + "\n", + "The [Xarray documentation](https://docs.xarray.dev/en/stable/internals/extending-xarray.html) has some more technical details, and this tutorial provides example custom accessors and their uses." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Why create a custom accessor\n", + "\n", + "- You can easily create a custom suite of tools that work on Xarray objects\n", + "- It keeps your workflows cleaner and simpler\n", + "- Your project-specific code is easy to share\n", + "- It's easy to implement: you don't need to integrate any code into Xarray\n", + "- It makes it easier to perform checks and write code documentation because you only have to create them once!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Easy steps to create your own accessor\n", + "\n", + "1. Create your custom class, including the mandatory `__init__` method\n", + "2. Add the `xr.register_dataarray_accessor()` or `xr.register_dataset_accessor()` \n", + "3. Use your custom functions " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 1: accessing scipy functionality" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For example, imagine you're a statistician who regularly uses a special `skewness` function which acts on dataarrays but is only of interest to people in your specific field.\n", + "\n", + "You can create a method which applies this skewness function to an xarray object and then register the method under a custom `stats` accessor like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import xarray as xr\n", + "from scipy.stats import skew\n", + "\n", + "xr.set_options(display_expand_attrs=False, display_expand_coords=False)\n", + "\n", + "\n", + "@xr.register_dataarray_accessor(\"stats\")\n", + "class StatsAccessor:\n", + " def __init__(self, da):\n", + " self._da = da\n", + "\n", + " def skewness(self, dim):\n", + " return self._da.reduce(func=skew, dim=dim)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can conveniently access this functionality via the `stats` accessor" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ds = xr.tutorial.load_dataset(\"air_temperature\")\n", + "ds[\"skewair\"] = ds['air'].stats.skewness(dim=\"time\")\n", + "ds" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notice how the presence of `.stats` clearly differentiates our new \"accessor method\" from core xarray methods." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 2: creating your own workflows\n", + "\n", + "Perhaps you find yourself running similar code for multiple xarray objects or across related projects. By packing your code into an extension, it makes it easy to repeat the same operation while reducing the likelihood of [human introduced] errors.\n", + "\n", + "Here we wrap the reorganization of InSAR ice velocity data illustrated in [this tutorial](https://tutorial.xarray.dev/data_cleaning/ice_velocity.html?highlight=ice_velocity) into a custom Xarray extension that makes it easy to re-apply each time you begin working with a new InSAR velocity dataset. Please see the linked tutorial for details on the data, applications, and each step in this process." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import os\n", + "import pandas as pd\n", + "import xarray as xr\n", + "\n", + "\n", + "@xr.register_dataset_accessor(\"insar_vel\")\n", + "class InsarReorg:\n", + " \"\"\"\n", + " An extension for an XArray dataset that will prepare InSAR data for analysis.\n", + "\n", + " Re-organize the data from its native structure to have x and y velocity and error along a time dimension.\n", + " \"\"\"\n", + "\n", + " # ----------------------------------------------------------------------\n", + " # Constructors\n", + "\n", + " def __init__(self, xrds):\n", + " self._xrds = xrds\n", + "\n", + " # ----------------------------------------------------------------------\n", + " # Methods\n", + "\n", + " @staticmethod\n", + " def _validate(self, req_dim=None, req_vars=None):\n", + " '''\n", + " Make sure the xarray dataset has the correct dimensions and variables.\n", + "\n", + " Running this function will check that my dataset has all the needed dimensions and variables\n", + " for a given function, saving time and headache later if they were missing and the computation fails\n", + " partway through.\n", + "\n", + " Parameters\n", + " ----------\n", + " req_dim : list of str\n", + " List of all required dimension names\n", + " req_vars : list of str\n", + " List of all required variable names\n", + " '''\n", + "\n", + " if req_dim is not None:\n", + " if all([dim not in list(self._xrds.dims) for dim in req_dim]):\n", + " raise AttributeError(\"Required dimensions are missing\")\n", + " if req_vars is not None:\n", + " if all([var not in self._xrds.variables for var in req_vars.keys()]):\n", + " raise AttributeError(\"Required variables are missing\")\n", + " # print(\"successfully validated your dataset\")\n", + "\n", + " # ----------------------------------------------------------------------\n", + " # Functions\n", + "\n", + " def change_vars_to_coords(\n", + " self,\n", + " req_dim=['ny', 'nx'],\n", + " req_vars={'xaxis': ['nx'], 'yaxis': ['ny']},\n", + " ):\n", + " \"\"\"\n", + " Turn the xaxis and y axis variables into coordinates.\n", + "\n", + " Parameters\n", + " ----------\n", + " req_dim : list of str\n", + " List of all required dimension names.\n", + " req_vars : list of str\n", + " List of all required variable names\n", + " \"\"\"\n", + "\n", + " self._validate(self, req_dim, req_vars)\n", + "\n", + " self._xrds = self._xrds.swap_dims({'ny': 'yaxis', 'nx': 'xaxis'})\n", + " self._xrds = self._xrds.rename({'xaxis': 'x', 'yaxis': 'y'})\n", + "\n", + " return self._xrds\n", + "\n", + " def reorg_dataset(self):\n", + " \"\"\"\n", + " Reorganize the data by time for each of the desired end variables (here vx, vy, err)\n", + "\n", + " \"\"\"\n", + "\n", + " reorged = []\n", + " for reorg_var in ['vx', 'vy', 'err']:\n", + " ds = self.reorg_var_time(reorg_var)\n", + " reorged.append(ds)\n", + "\n", + " reorged_ds = xr.merge(reorged)\n", + "\n", + " return reorged_ds\n", + "\n", + " def reorg_var_time(self, reorg_var):\n", + " \"\"\"\n", + " Repeat the process for a given variable.\n", + "\n", + " Figure out which of the original variables are time steps for this variable and turn each one into a dataarray.\n", + " Add a time dimension and update the variable name for each dataarray.\n", + " Combine the modified data arrays back into a single dataset.\n", + " \"\"\"\n", + "\n", + " # create storage list for reorganizing\n", + " var_ls = list(self._xrds)\n", + " to_reorg = [var for var in var_ls if reorg_var in var]\n", + "\n", + " # list the arrays from the original dataset that correspond to the variable\n", + " das_to_reorg = [self._xrds[var] for var in to_reorg]\n", + "\n", + " # add the time dimension\n", + " das_to_reorg = [das_to_reorg[var].expand_dims('time') for var in range(len(das_to_reorg))]\n", + "\n", + " # update variable name to remove time\n", + " das_to_reorg = [das_to_reorg[var].rename(reorg_var) for var in range(len(das_to_reorg))]\n", + "\n", + " ds = xr.concat(das_to_reorg, dim='time')\n", + "\n", + " return ds" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ds = xr.tutorial.open_dataset('ASE_ice_velocity.nc')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ds = ds.insar_vel.change_vars_to_coords()\n", + "ds" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ds = ds.insar_vel.reorg_dataset()\n", + "ds" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 3: creating your own workflows with locally stored corrections\n", + "\n", + "Consider someone who frequently converts their elevations to be relative to the geoid (rather than the ellipsoid) using a custom, local conversion (otherwise, we'd recommend using an established conversion library like [pyproj](https://pypi.org/project/pyproj/) to switch between datums).\n", + "\n", + "An accessor provides an elegant way to build (once) and apply (as often as needed!) this custom conversion on top of the existing xarray ecosystem without the need to copy-paste the code into the start of each project. By standardizing our approach and adding a few sanity checks within the accessor, we also eliminate the risk of accidentally applying the correction multiple times." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import rasterio\n", + "import xarray as xr\n", + "\n", + "\n", + "@xr.register_dataset_accessor(\"geoidxr\")\n", + "class GeoidXR:\n", + " \"\"\"\n", + " An extension for an XArray dataset that will calculate geoidal elevations from a local source file.\n", + " \"\"\"\n", + "\n", + " # ----------------------------------------------------------------------\n", + " # Constructors\n", + "\n", + " def __init__(\n", + " self,\n", + " xrds,\n", + " ):\n", + " self._xrds = xrds\n", + " # Running this function on init will check that my dataset has all the needed dimensions and variables\n", + " # as specific to my workflow, saving time and headache later if they were missing and the computation fails\n", + " # partway through.\n", + " self._validate(\n", + " self, req_dim=['x', 'y', 'dtime'], req_vars={'elevation': ['x', 'y', 'dtime']}\n", + " )\n", + "\n", + " # ----------------------------------------------------------------------\n", + " # Methods\n", + "\n", + " @staticmethod\n", + " def _validate(self, req_dim=None, req_vars=None):\n", + " '''\n", + " Make sure the xarray dataset has the correct dimensions and variables\n", + "\n", + " Parameters\n", + " ----------\n", + " req_dim : list of str\n", + " List of all required dimension names\n", + " req_vars : list of str\n", + " List of all required variable names\n", + " '''\n", + "\n", + " if req_dim is not None:\n", + " if all([dim not in list(self._xrds.dims) for dim in req_dim]):\n", + " raise AttributeError(\"Required dimensions are missing\")\n", + " if req_vars is not None:\n", + " if all([var not in self._xrds.variables for var in req_vars.keys()]):\n", + " raise AttributeError(\"Required variables are missing\")\n", + "\n", + " # Notice that 'geoid' has been added to the req_vars list\n", + " def to_geoid(\n", + " self,\n", + " req_dim=['dtime', 'x', 'y'],\n", + " req_vars={'elevation': ['x', 'y', 'dtime', 'geoid']},\n", + " source=None,\n", + " ):\n", + " \"\"\"\n", + " Get geoid layer from your local file, which is provided to the function as \"source\",\n", + " and apply the offset to all elevation values.\n", + " Adds 'geoid_offset' keyword to \"offsets\" attribute so you know the geoid offset was applied.\n", + "\n", + " Parameters\n", + " ----------\n", + " req_dim : list of str\n", + " List of all required dimension names.\n", + " req_vars : list of str\n", + " List of all required variable names\n", + " source : str\n", + " Full path to your source file containing geoid offsets\n", + " \"\"\"\n", + "\n", + " # check to make sure you haven't already run this function (and are thus applying the offset twice)\n", + " try:\n", + " values = self._xrds.attrs['offset_names']\n", + " assert 'geoid_offset' not in values, \"You've already applied the geoid offset!\"\n", + " values = list([values]) + ['geoid_offset']\n", + " except KeyError:\n", + " values = ['geoid_offset']\n", + "\n", + " self._validate(self, req_dim, req_vars)\n", + "\n", + " # read in your geoid values\n", + " # WARNING: this implementation assumes your geoid values are in the same CRS and grid as the data you are applying\n", + " # them to. If not, you will need to reproject and/or resample them to match the data to which you are applying them.\n", + " # That step is not included here to emphasize the accessor aspect of the workflow.\n", + " with rasterio.open(source) as src:\n", + " geoid = src['geoid_varname']\n", + "\n", + " # As noted above, this step will fail or produce unreliable results if your data is not properly gridded\n", + " self._xrds['elevation'] = self._xrds.elevation - geoid\n", + "\n", + " self._xrds.attrs['offset_names'] = values\n", + "\n", + " return self._xrds" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, each time we want to convert our ellipsoid data to the geoid, we only have to run one line of code, and it will also perform a multitude of checks for us to make sure we're performing exactly the operation we expect. Imagine the possibilities (and decrease in frustration)!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "raises-exception" + ] + }, + "outputs": [], + "source": [ + "ds = ds.geoidxr.to_geoid(source='/Path/to/Custom/source/file.nc')\n", + "ds" + ] + } + ], + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + }, + "vscode": { + "interpreter": { + "hash": "eeef546aa85c5aee566c457bd2890cafb9e11a3b514b94bbf230bf44d1caf251" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/advanced/accessors/accessors.md b/advanced/accessors/accessors.md new file mode 100644 index 00000000..f49df5d2 --- /dev/null +++ b/advanced/accessors/accessors.md @@ -0,0 +1,3 @@ +```{tableofcontents} + +``` diff --git a/data_cleaning/ice_velocity.ipynb b/data_cleaning/ice_velocity.ipynb index 27327c15..5172cb96 100644 --- a/data_cleaning/ice_velocity.ipynb +++ b/data_cleaning/ice_velocity.ipynb @@ -1,6 +1,7 @@ { "cells": [ { + "attachments": {}, "cell_type": "markdown", "id": "3c29d537", "metadata": {}, @@ -13,7 +14,11 @@ "\n", "Downloaded data is `.hdr` and `.dat` files for each year, and a `.nc` for all of the years together. \n", "\n", - "The `.nc` object is a dataset with dimensions x,y and data vars for each year. So for each year there are `vx`,`vy`,`err` vars. We'd like to re-organize this so that there are 3 variables (`vx`, `vy` and `err`) that exist along a time dimension." + "The `.nc` object is a dataset with dimensions x,y and data vars for each year. So for each year there are `vx`,`vy`,`err` vars. We'd like to re-organize this so that there are 3 variables (`vx`, `vy` and `err`) that exist along a time dimension.\n", + "\n", + "```{note}\n", + "These steps were turned into a accessor/extension example, which can be viewed [here](/advanced/accessors/01_accessor_examples).\n", + "```" ] }, { diff --git a/intermediate/xarray_ecosystem.ipynb b/intermediate/xarray_ecosystem.ipynb index a08f1cd8..a587aa9a 100644 --- a/intermediate/xarray_ecosystem.ipynb +++ b/intermediate/xarray_ecosystem.ipynb @@ -1,18 +1,19 @@ { "cells": [ { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", - "# A Tour of the Xarray Ecosystem\n", + "# A Tour of Xarray Customizations\n", "\n", "Xarray is easily extensible.\n", - "This means it is easy to add onto to build custom packages that tackle particular computational problems.\n", + "This means it is easy to add on to to build custom packages that tackle particular computational problems or supply domain specific functionality.\n", "\n", "These packages can plug in to xarray in various different ways. They may build directly on top of xarray, or they may take advantage of some of xarray's dedicated interfacing features:\n", - "- Accessors\n", + "- Accessors (extensions)\n", "- Backend (filetype) entrypoint\n", "- Metadata attributes\n", "- Duck-array wrapping interface\n", @@ -20,6 +21,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -31,6 +33,14 @@ "- [pint-xarray](https://pint-xarray.readthedocs.io/en/latest/), for unit-aware computations using pint." ] }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Specific examples for implementing your own xarray customizations using these interfacing features are available in the ADVANCED section of this book." + ] + }, { "cell_type": "code", "execution_count": null, @@ -49,6 +59,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "tags": [] @@ -58,9 +69,7 @@ "\n", "The accessor-style syntax is used heavily by the other libraries we are about to cover.\n", "\n", - "For users accessors just allow us to have a method-like syntax on xarray objects, for example `da.hvplot()`, `da.pint.quantify()`, or `da.cf.describe()`.\n", - "\n", - "For developers who are interested in defining their own accessors, see the section on accessors at the bottom of this page." + "For users, accessors just allow us to have a familiar dot (method-like) syntax on xarray objects, for example `da.hvplot()`, `da.pint.quantify()`, or `da.cf.describe()`.\n" ] }, { @@ -71,10 +80,11 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "The [HoloViews library](https://holoviews.org/) makes great use of accessors to allow seamless plotting of xarray data using a completely different plotting backend." + "The [HoloViews library](https://holoviews.org/) makes great use of accessors to allow seamless plotting of xarray data using a completely different plotting backend (by default, xarray uses [matplotlib](https://matplotlib.org/))." ] }, { @@ -119,10 +129,11 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "For some more examples of how powerful holoviews is [see here](https://tutorial.xarray.dev/intermediate/hvplot.html)." + "For some more examples of how powerful HoloViews is [see here](https://tutorial.xarray.dev/intermediate/hvplot.html)." ] }, { @@ -153,10 +164,11 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "You can explicitly use rioxarray's 'rasterio' engine to load myriad geospatial raster formats, below is a [Cloud-Optimized Geotiff](https://www.cogeo.org) from an AWS [public dataset](https://registry.opendata.aws/sentinel-1-rtc-indigo/) of synthetic aperture radar data over Washington, State, USA. `overview_level=4` is an argument specific to the `rasterio` engine that allows opening a pre-computed lower resolution \"overview\" of the data." + "You can explicitly use rioxarray's 'rasterio' engine to load myriad geospatial raster formats. Below is a [Cloud-Optimized Geotiff](https://www.cogeo.org) from an AWS [public dataset](https://registry.opendata.aws/sentinel-1-rtc-indigo/) of synthetic aperture radar data over Washington, State, USA. `overview_level=4` is an argument specific to the `rasterio` engine that allows opening a pre-computed lower resolution \"overview\" of the data." ] }, { @@ -171,10 +183,11 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "The `spatial_ref` coordinate is added by rioxarray to store standardized geospatial Coordinate Reference System (CRS) information, we can access that information and additional methods via the `.rio` accessor:" + "The `spatial_ref` coordinate is added by rioxarray to store standardized geospatial Coordinate Reference System (CRS) information. We can access that information and additional methods via the `.rio` accessor:" ] }, { @@ -187,10 +200,11 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "EPSG refers to 'European Petroleum Survey Group' a database of the many CRS definitions for our Planet used over the years! EPSG=32610 is the [\"UTM 10N\" CRS](https://epsg.io/32610), with coordinate units in meters. Let's say you want longitude,latitude coordinate points in degrees instead. You'd have to *reproject* this data:" + "EPSG refers to 'European Petroleum Survey Group', a database of the many CRS definitions for our Planet used over the years! EPSG=32610 is the [\"UTM 10N\" CRS](https://epsg.io/32610), with coordinate units in meters. Let's say you want longitude,latitude coordinate points in degrees instead. You'd have to *reproject* this data:" ] }, { @@ -204,10 +218,11 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "Note that that the size of the data has changed as well as the coordinate values. This is typical of reprojection, your data must be resampled and often interpolated to match the new CRS grid! A quick plot will compare the results of our reprojected data:" + "Note that that the size of the data has changed as well as the coordinate values. This is typical of reprojection, as your data must be resampled and often interpolated to match the new CRS grid! A quick plot will compare the results of our reprojected data:" ] }, { @@ -631,70 +646,6 @@ "Take a look at the [pint-xarray documentation](https://pint-xarray.readthedocs.io/en/latest/) or the [pint documentation](https://pint.readthedocs.io/en/stable/) if you get stuck." ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Accessors part 2 - defining custom accessors" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "An accessor is a way of attaching a custom function to xarray types so that it can be called as if it were a method, but while retaining a clear separation between \"core\" xarray API and custom API." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For example, imagine you're a statistician who regularly uses a special `skewness` function which acts on dataarrays but is only of interest to people in your specific field.\n", - "\n", - "You can create a method which applies this skewness function to an xarray objects, and then register the method under a custom `stats` accessor like this" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from scipy.stats import skew\n", - "\n", - "\n", - "@xr.register_dataarray_accessor(\"stats\")\n", - "class StatsAccessor:\n", - " def __init__(self, da):\n", - " self._da = da\n", - "\n", - " def skewness(self, dim):\n", - " return self._da.reduce(func=skew, dim=dim)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we can conveniently access this functionality via the `stats` accessor" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ds['air'].stats.skewness(dim=\"time\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Notice how the presence of `.stats` clearly differentiates our new \"accessor method\" from core xarray methods." - ] - }, { "cell_type": "markdown", "metadata": {},