From 09fc1fe076aa3e34104f9f4e40eff69ad73f464d Mon Sep 17 00:00:00 2001 From: Emma Marshall <55526386+e-marshall@users.noreply.github.com> Date: Mon, 19 Aug 2024 09:56:21 -0600 Subject: [PATCH 1/2] custom index nb + img --- .../indexing/build_custom_index_1d.ipynb | 4756 +++++++++++++++++ intermediate/indexing/img2.png | Bin 0 -> 82118 bytes 2 files changed, 4756 insertions(+) create mode 100644 intermediate/indexing/build_custom_index_1d.ipynb create mode 100644 intermediate/indexing/img2.png diff --git a/intermediate/indexing/build_custom_index_1d.ipynb b/intermediate/indexing/build_custom_index_1d.ipynb new file mode 100644 index 00000000..59de152f --- /dev/null +++ b/intermediate/indexing/build_custom_index_1d.ipynb @@ -0,0 +1,4756 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8a8e5623-0622-4ec8-92e8-31d1d2e61d0e", + "metadata": {}, + "source": [ + "# Creating a custom Xarray index \n", + "\n", + "This tutorial demonstrates [Xarray's](https://xarray.dev/) (relatively) new Flexible Index feature which allows users to modify traditional Xarray indexes to add custom functionality. \n", + "\n", + "Indexes are an important element of any Xarray object. They facilitate the label-based indexing that makes Xarray a great tool for n-dimensional array data. Most Xarray objects have `Pandas.Indexes`, which fit a wide range of use cases. However, these indexes also have limitations: \n", + "- All coordinate labels must be explicitly loaded in memory, \n", + "- It can be difficult to fit irregularly-sampled data within the `Pandas.Index` structure, \n", + "- There is no built-in support for dimensions that require additional metadata. \n", + "\n", + "Xarray's custom (wc: flexible?) index feature allows users to define their own Indexes and add them to Xarray objects. A few examples of situations where this is useful are: \n", + "- Periodic index, for datasets with periodic dimensions (such as longitude). \n", + "- Unit-aware index (see the [Pint] project) \n", + "- An index for coordinates described by a function rather than an array \n", + "- An index that can handle a 2D rotation \n", + "(add links to those that have references/examples out there) \n", + "\n", + "## Overview\n", + "We will focus on the following example: \n", + "- We have a 1-dimensional `Xarray.Dataset` indexed in a given coordinate system. However, we want to frequently query the dataset from a different coordinate reference system. \n", + "- Information describing the transformation between the two coordinate systems is stored as an attribute of the Xarray object. In this example, we use a simple, multiplicative transform.\n", + "- We want to define a custom index that will handle the coordinate transformation. This is a simplified analog of a common scenario: a geospatial dataset is in a given coordinate reference system and you would like to query it in another coordinate system. (maybe take out last sentence)\n", + "- link to existing documentation\n", + "\n", + "We start by defining a very simple index and then increase the complexity by adding more functionality.\n", + "\n", + "![coord transform](img2.png)\n", + "\n", + "## Learning goals\n", + "This notebook shows how to build a custom Xarray index and assign it to an Xarray object using [`xr.set_xindex()`](https://docs.xarray.dev/en/stable/generated/xarray.Dataset.set_xindex.html). After working through this tutorial, users should expect to understand:\n", + "- How to define a custom Xarray index \n", + "- How to add a custom index index to an existing Xarray object using `xr.set_xindex()`\n", + "- The different components of an Xarray index and their function.\n", + "- How to implement methods to Xarray indexes such as `.sel()` and methods that handle alignment.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "09c0cdb8-8570-4a29-b013-bbd8a6a451e8", + "metadata": {}, + "outputs": [], + "source": [ + "import xarray as xr \n", + "import numpy as np\n", + "from collections.abc import Sequence\n", + "from copy import deepcopy\n", + "\n", + "\n", + "from xarray import Index\n", + "from xarray.core.indexes import PandasIndex\n", + "from xarray.core.indexing import merge_sel_results\n", + "from xarray.core.indexes import Index, PandasIndex, get_indexer_nd\n", + "from xarray.core.indexing import merge_sel_results" + ] + }, + { + "cell_type": "markdown", + "id": "6105c786", + "metadata": {}, + "source": [ + "### Define sample data\n", + "First, we define a sample dataset to work with. The functions below define parameters that are used to generate an Xarray dataset with a data variable that exists at arbitrary coordinates along an `x` dimension. It also has a scalar variable, `spatial_ref`, where metadata describing the coordinate transform is stored as an attribute. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "fb2f688b", + "metadata": {}, + "outputs": [], + "source": [ + "def make_kwargs(factor, range_ls, data_len):\n", + " \"\"\"\n", + " Create keyword arguments for a function.\n", + "\n", + " Parameters\n", + " ----------\n", + " factor : int or float\n", + " Multiplicative factor for coordinate transform\n", + " range_ls : list\n", + " Range describing the x-coordinate\n", + " data_len : int\n", + " Length of dataset along x-dim\n", + "\n", + " Returns\n", + " -------\n", + " dict\n", + "\n", + " \"\"\"\n", + " da_kwargs = {\n", + " 'factor': factor,\n", + " 'range' : range_ls,\n", + " 'idx_name':'x',\n", + " 'real_name':'lon',\n", + " 'data_len': data_len\n", + " }\n", + " return da_kwargs\n", + "\n", + "def create_sample_data(kwargs: dict) -> xr.Dataset:\n", + " \"\"\"\n", + " Function to create sample data.\n", + "\n", + " Parameters\n", + " ----------\n", + " kwargs : dict\n", + " A dictionary generated from make_kwargs() containing the following key-value pairs:\n", + " - 'factor' (float): A multiplicative factor.\n", + " - 'range' (tuple): A tuple specifying the range of the x-coordinate.\n", + " - 'idx_name' (str): The name of the coordinate reference system A.\n", + " - 'real_name' (str): The name of the coordinate reference system B.\n", + "\n", + " Returns\n", + " -------\n", + " xr.Dataset\n", + " An Xarray dataset containing the sample data.\n", + "\n", + " Notes\n", + " -----\n", + " This function creates an Xarray dataset with random data. The dimensions and coordinates of the dataset are specified by the input arguments.\n", + "\n", + " Example\n", + " -------\n", + " >>> kwargs = {\n", + " ... 'factor': 2.0,\n", + " ... 'range': (0, 10, 1),\n", + " ... 'idx_name': 'coord_A',\n", + " ... 'real_name': 'coord_B'\n", + " ... }\n", + " >>> dataset = create_sample_data(kwargs)\n", + " \"\"\"\n", + " attrs = {\n", + " 'factor': kwargs['factor'],\n", + " 'range': kwargs['range'],\n", + " 'idx_name': kwargs['idx_name'],\n", + " 'real_name': kwargs['real_name']\n", + " }\n", + "\n", + " da = xr.DataArray(\n", + " data=np.random.rand(kwargs['data_len']),\n", + " dims=(kwargs['idx_name']),\n", + " coords={\n", + " 'x': np.arange(kwargs['range'][0], kwargs['range'][1], kwargs['range'][2])\n", + " }\n", + " )\n", + "\n", + " ds = xr.Dataset({'var1': da})\n", + "\n", + " spatial_ref = xr.DataArray()\n", + " spatial_ref.attrs = attrs\n", + "\n", + " ds['spatial_ref'] = spatial_ref\n", + " ds = ds.set_coords('spatial_ref')\n", + "\n", + " #ds = ds.expand_dims({'y': 1})\n", + "\n", + " return ds" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3873f02b", + "metadata": {}, + "outputs": [], + "source": [ + "#create sample data \n", + "sample_ds1 = create_sample_data(make_kwargs(2,[0,10,1],10))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5fa03c98", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 168B\n",
+       "Dimensions:      (x: 10)\n",
+       "Coordinates:\n",
+       "  * x            (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n",
+       "    spatial_ref  float64 8B nan\n",
+       "Data variables:\n",
+       "    var1         (x) float64 80B 0.6362 0.6464 0.3027 ... 0.00198 0.2211 0.5605
" + ], + "text/plain": [ + " Size: 168B\n", + "Dimensions: (x: 10)\n", + "Coordinates:\n", + " * x (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n", + " spatial_ref float64 8B nan\n", + "Data variables:\n", + " var1 (x) float64 80B 0.6362 0.6464 0.3027 ... 0.00198 0.2211 0.5605" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sample_ds1" + ] + }, + { + "cell_type": "markdown", + "id": "7a32f356", + "metadata": {}, + "source": [ + "## Defining a custom index\n", + "\n", + "### First, how will it be used?\n", + "Before we get into defining the custom index, it's helpful to see how it will be used. We have the object `sample_ds1`, which has a `PandasIndex`. \n", + "\n", + "Note, `PandasIndex` is a Xarray wrapper for `Pandas.Index` object <- maybe more detail than necessary" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d22821de", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "pandas.core.indexes.base.Index" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(sample_ds1.indexes['x'])" + ] + }, + { + "cell_type": "markdown", + "id": "eddcbb2a", + "metadata": {}, + "source": [ + "We want to replace the `PandasIndex` with our `CustomIndex`. To do this, we'll drop the `x` index from that dataset: \n", + "`sample_ds1 = sample_ds1.drop_indexes('x')`\n", + "\n", + "Once we define the new index, we'll attach it to the Xarray objects using the `xr.set_xindex()` method. This takes the coordinates from the Xarray object used to build the index, and the index class. It will look like this:\n", + "\n", + "`s1 = sample_ds1.set_xindex(['x','spatial_ref'], ToyIndex_scalar)`\n", + "\n", + "Now, let's define the custom index class." + ] + }, + { + "cell_type": "markdown", + "id": "976b9a31", + "metadata": {}, + "source": [ + "## The smallest `CustomIndex` \n", + "\n", + "This is an index that contains only the required component of an Xarray index, the `from_variables()` method. It can be successfully added to `ds` but it can't do much beyond that, and it doesn't contain any information about the transform between coordinate reference systems that we're interested in. Still, it's helpful to understand because `from_variables()` is how information gets from `ds` to our new index. `from_variables()` receives information about `ds` from `xr.set_xindex()` and uses it to construct an instance of `CustomIndex`. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "3fe488e8-3f13-42a8-a74e-a8a2bb6f15f9", + "metadata": {}, + "outputs": [], + "source": [ + "class CustomIndex_tiny(xr.Index): #customindex inherits xarray Index\n", + " def __init__(self, x_indexes, variables=None): \n", + " \n", + " self.indexes = variables\n", + " self._xindexes = x_indexes \n", + "\n", + " self.spatial_ref = variables['spatial_ref']\n", + " \n", + " @classmethod \n", + " def from_variables(cls,variables, **kwargs):\n", + " '''this method creates a CustomIndex obj from a variables object.\n", + " variables is a dict created from ds1, keys are variable names,\n", + " values are associated xr.variables. created like this:\n", + " coord_vars = {name:ds._variables[name] for name in coord_names}\n", + " coord_names is passed to set_xindex\n", + " '''\n", + " # this index class expects to work with datasets with certain properties\n", + " # it must have exactly 2 variables: x and spatial_ref \n", + " assert len(variables) == 2\n", + " assert 'x' in variables\n", + " assert 'spatial_ref' in variables \n", + " \n", + " #separate dimensional, scalar variables into own dicts\n", + " dim_variables = {}\n", + " scalar_vars = {}\n", + " for k,i in variables.items():\n", + " if variables[k].ndim ==1:\n", + " dim_variables[k] = variables[k]\n", + " if variables[k].ndim ==0:\n", + " scalar_vars[k] = variables[k]\n", + " \n", + " options = {'dim':'x',\n", + " 'name':'x'}\n", + " \n", + " #make dict of PandasIndexes for dim. variable\n", + " x_indexes = {\n", + " k: PandasIndex.from_variables({k: v}, options = options) \n", + " for k,v in dim_variables.items()\n", + " }\n", + " #add scalar var to dict\n", + " x_indexes['spatial_ref'] = variables['spatial_ref']\n", + " \n", + " return cls(x_indexes, variables) #return an instance of CustomIndex class\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "904247b1", + "metadata": {}, + "outputs": [], + "source": [ + "sample_ds1 = sample_ds1.drop_indexes('x')" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "9f3ce806", + "metadata": {}, + "outputs": [], + "source": [ + "ds1 = sample_ds1.set_xindex(['x','spatial_ref'], CustomIndex_tiny)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c00eefb4-c0b7-4c78-9f80-8d068ba245f4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 168B\n",
+       "Dimensions:      (x: 10)\n",
+       "Coordinates:\n",
+       "  * x            (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n",
+       "  * spatial_ref  float64 8B nan\n",
+       "Data variables:\n",
+       "    var1         (x) float64 80B 0.6362 0.6464 0.3027 ... 0.00198 0.2211 0.5605\n",
+       "Indexes:\n",
+       "  ┌ x            CustomIndex_tiny\n",
+       "  └ spatial_ref
" + ], + "text/plain": [ + " Size: 168B\n", + "Dimensions: (x: 10)\n", + "Coordinates:\n", + " * x (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n", + " * spatial_ref float64 8B nan\n", + "Data variables:\n", + " var1 (x) float64 80B 0.6362 0.6464 0.3027 ... 0.00198 0.2211 0.5605\n", + "Indexes:\n", + " ┌ x CustomIndex_tiny\n", + " └ spatial_ref" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds1" + ] + }, + { + "cell_type": "markdown", + "id": "71bfea5e", + "metadata": {}, + "source": [ + "As mentioned above, `ds1` now has the CustomIndex, but it can't do much." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "5281809e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exception reporting mode: Minimal\n" + ] + }, + { + "ename": "NotImplementedError", + "evalue": "<__main__.CustomIndex_tiny object at 0x7f7c7c220ad0> doesn't support label-based selection", + "output_type": "error", + "traceback": [ + "\u001b[0;31mNotImplementedError\u001b[0m\u001b[0;31m:\u001b[0m <__main__.CustomIndex_tiny object at 0x7f7c7c220ad0> doesn't support label-based selection\n" + ] + } + ], + "source": [ + "%xmode Minimal\n", + "\n", + "ds1.sel(x=4)" + ] + }, + { + "cell_type": "markdown", + "id": "425b91fe", + "metadata": {}, + "source": [ + "### More detail on `from_variables()`\n", + "> - During `xr.set_xindex()`, a dict object called `variables` is created. For every coordinate in `ds`, `variables` has a key-value pair like follows: `name: ds._variables[name]`. \n", + "> - `variables` is passed to `from_variables()` and used to create another dict. The values in this dictionary hold a `PandasIndex` for each dimensional coordinate, and an `xr.Variable` for each scalar coordinate. \n", + "> - It's important to note that `from_variables()` is a **class method** (Add link). This means that it acts as a constructor, returning an instance of the `CustomIndex` class. \n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "b082ec1e", + "metadata": {}, + "source": [ + "## Adding a coordinate transform and `.sel()` to `CustomIndex`\n", + "This section adds three new methods:\n", + "1. `create_variables()`: Returns a coordinate variable created from the new index.\n", + "2. `transform()`: Handles the coordinate transform between CRS A and CRS B. <- NOTE: remove this from class and pass to set_xindex?\n", + "3. `sel()`: Select points from `ds1` using `transform()`. This allows user to pass labels in coordinate reference system B, and `.sel()` will return appropriate elements from ds1." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "e581fe41", + "metadata": {}, + "outputs": [], + "source": [ + "#create new sample data\n", + "sample_ds1 = create_sample_data(make_kwargs(2,[0,10,1],10))\n", + "\n", + "#create a copy used for testing later\n", + "orig_ds1 = sample_ds1.copy()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "d905c300", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 168B\n",
+       "Dimensions:      (x: 10)\n",
+       "Coordinates:\n",
+       "  * x            (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n",
+       "    spatial_ref  float64 8B nan\n",
+       "Data variables:\n",
+       "    var1         (x) float64 80B 0.9233 0.5917 0.4112 ... 0.291 0.2584 0.1781
" + ], + "text/plain": [ + " Size: 168B\n", + "Dimensions: (x: 10)\n", + "Coordinates:\n", + " * x (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n", + " spatial_ref float64 8B nan\n", + "Data variables:\n", + " var1 (x) float64 80B 0.9233 0.5917 0.4112 ... 0.291 0.2584 0.1781" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sample_ds1" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "c380e9a9", + "metadata": {}, + "outputs": [], + "source": [ + "class CustomIndex_sel(xr.Index): #customindex inherits xarray Index\n", + " def __init__(self, x_indexes, variables=None): \n", + " \n", + " self.indexes = variables\n", + " self._xindexes = x_indexes \n", + "\n", + " self.spatial_ref = variables['spatial_ref']\n", + " \n", + " @classmethod \n", + " def from_variables(cls,variables, **kwargs):\n", + " '''this method creates a CustomIndex obj from a variables object.\n", + " variables is a dict created from ds1, keys are variable names,\n", + " values are associated xr.variables. created like this:\n", + " coord_vars = {name:ds._variables[name] for name in coord_names}\n", + " coord_names is passed to set_xindex\n", + " '''\n", + " # this index class expects to work with datasets with certain properties\n", + " # it must have exactly 2 variables: x and spatial_ref \n", + " assert len(variables) == 2\n", + " assert 'x' in variables\n", + " assert 'spatial_ref' in variables \n", + " \n", + " dim_variables = {}\n", + " scalar_vars = {}\n", + " for k,i in variables.items():\n", + " if variables[k].ndim ==1:\n", + " dim_variables[k] = variables[k]\n", + " if variables[k].ndim ==0:\n", + " scalar_vars[k] = variables[k]\n", + " \n", + " options = {'dim':'x',\n", + " 'name':'x'}\n", + " \n", + " x_indexes = {\n", + " k: PandasIndex.from_variables({k: v}, options = options) \n", + " for k,v in dim_variables.items()\n", + " }\n", + " \n", + " x_indexes['spatial_ref'] = variables['spatial_ref']\n", + " \n", + " return cls(x_indexes, variables) #return an instance of CustomIndex class\n", + " \n", + " def create_variables(self, variables=None):\n", + " '''\n", + " Creates coord variable from index.\n", + "\n", + " Parameters:\n", + " -----------\n", + " variables : dict, optional\n", + " A dictionary of variables.\n", + "\n", + " Returns:\n", + " --------\n", + " dict\n", + " A dictionary containing the created variables.\n", + "\n", + " Notes:\n", + " ------\n", + " This method iterates over the `_xindexes` values and creates coord variables from the indexes.\n", + " It skips the spatial reference variable and updates the `idx_variables` dictionary with the created variables.\n", + " Finally, it adds the `spatial_ref` variable from the `variables` dictionary to the `idx_variables` dictionary.\n", + "\n", + " Example:\n", + " --------\n", + " >>> variables = {'spatial_ref': 123}\n", + " >>> result = create_variables(variables)\n", + " >>> print(result)\n", + " {'var1': ..., 'var2': ..., 'spatial_ref': 123}\n", + " '''\n", + " idx_variables = {}\n", + "\n", + " for index in self._xindexes.values():\n", + " if type(index) == xr.core.variable.Variable:\n", + " pass\n", + " else:\n", + " x = index.create_variables(variables)\n", + " idx_variables.update(x)\n", + "\n", + " idx_variables['spatial_ref'] = variables['spatial_ref']\n", + " return idx_variables\n", + "\n", + " idx_variables = {}\n", + " \n", + "\n", + " for index in self._xindexes.values():\n", + " #want to skip spatial ref\n", + " if type(index) == xr.core.variable.Variable:\n", + " pass\n", + " else:\n", + "\n", + " x = index.create_variables(variables)\n", + " idx_variables.update(x)\n", + " \n", + " idx_variables['spatial_ref'] = variables['spatial_ref'] \n", + " return idx_variables\n", + "\n", + " def transform(self, value):\n", + " \"\"\"\n", + " Transform the given value based on the spatial reference attributes. Currently, this only handles a very simple transform.\n", + " NOTE: this could be removed from the index class and passed to set_xindex()? \n", + "\n", + " Parameters:\n", + " -----------\n", + " value : int, float, slice, or list\n", + " The value to be transformed.\n", + "\n", + " Returns:\n", + " --------\n", + " transformed_labels : dict\n", + " A dictionary containing the transformed labels.\n", + "\n", + " Notes:\n", + " ------\n", + " - If `value` is a slice, it will be transformed based on the factor and index name attributes.\n", + " - If `value` is a single value or a list of values, each value will be transformed based on the factor attribute.\n", + "\n", + " Examples:\n", + " ---------\n", + " >>> spatial_ref = SpatialReference(factor=2, idx_name='index')\n", + " >>> transformed_labels = spatial_ref.transform(10)\n", + " >>> print(transformed_labels)\n", + " {'index': 5}\n", + "\n", + " >>> transformed_labels = spatial_ref.transform([10, 20, 30])\n", + " >>> print(transformed_labels)\n", + " {'index': [5, 10, 15]}\n", + "\n", + " >>> transformed_labels = spatial_ref.transform(slice(10, 20, 2))\n", + " >>> print(transformed_labels)\n", + " {'index': slice(5, 10, 2)}\n", + " \"\"\"\n", + " #extract attrs\n", + " fac = self.spatial_ref.attrs['factor']\n", + " key = self.spatial_ref.attrs['idx_name']\n", + "\n", + " #handle slice\n", + " if isinstance(value, slice):\n", + " \n", + " start, stop, step = value.start, value.stop, value.step\n", + " new_start, new_stop, new_step = start / fac, stop/fac, step\n", + " new_val = slice(new_start, new_stop, new_step)\n", + " transformed_labels = {key: new_val}\n", + " return transformed_labels\n", + " \n", + " #single or list of values\n", + " else:\n", + " \n", + " vals_to_transform = [] \n", + "\n", + " if not isinstance(value, Sequence):\n", + " value = [value]\n", + "\n", + " for k in range(len(value)):\n", + "\n", + " val = value[k]\n", + " vals_to_transform.append(val)\n", + "\n", + " #logic for parsing attrs\n", + " transformed_x = [int(v / fac) for v in vals_to_transform]\n", + "\n", + " transformed_labels = {key:transformed_x}\n", + " return transformed_labels\n", + "\n", + " def sel(self, labels):\n", + " \"\"\"\n", + " Selects data from the index based on the provided labels.\n", + "\n", + " Parameters:\n", + " -----------\n", + " labels : dict\n", + " A dictionary containing the labels for each dimension.\n", + "\n", + " Returns:\n", + " --------\n", + " matches : PandasIndex\n", + " A PandasIndex object containing the selected data.\n", + "\n", + " Raises:\n", + " -------\n", + " AssertionError:\n", + " If the type of `labels` is not a dictionary.\n", + "\n", + " Notes:\n", + " ------\n", + " - The `labels` dictionary should have keys corresponding to the dimensions of the index.\n", + " - The values of the `labels` dictionary should be the labels to select from each dimension.\n", + " - The method uses the `transform` method to convert the labels to coordinate CRS.\n", + " - The selection is performed on the index created in the `.sel()` method.\n", + "\n", + " Example:\n", + " --------\n", + " >>> labels = {'x': 10}\n", + " >>> matches = obj.sel(labels)\n", + " >>> print(matches)\n", + " PandasIndex([10], dtype='int64', name='x')\n", + " \"\"\"\n", + " \n", + " assert type(labels) == dict\n", + "\n", + " #user passes to sel\n", + " label = next(iter(labels.values()))\n", + "\n", + " #materialize coord array to idx off of\n", + " params = self.spatial_ref.attrs['range']\n", + " full_arr = np.arange(params[0], params[1], params[2])\n", + " toy_index = PandasIndex(full_arr, dim='x')\n", + "\n", + " #transform user labesl to coord crs\n", + " idx = self.transform(label)\n", + "\n", + " #sel on index created in .sel()\n", + " matches = toy_index.sel(idx)\n", + "\n", + " return matches " + ] + }, + { + "cell_type": "markdown", + "id": "8b79dee8", + "metadata": {}, + "source": [ + "Drop index:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "8d7f2ccd", + "metadata": {}, + "outputs": [], + "source": [ + "sample_ds1 = sample_ds1.drop_indexes('x')" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "ddcf8c79", + "metadata": {}, + "outputs": [], + "source": [ + "ds1 = sample_ds1.set_xindex(['x','spatial_ref'], CustomIndex_sel)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "2c1290b1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 168B\n",
+       "Dimensions:      (x: 10)\n",
+       "Coordinates:\n",
+       "  * x            (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n",
+       "  * spatial_ref  float64 8B nan\n",
+       "Data variables:\n",
+       "    var1         (x) float64 80B 0.7656 0.8868 0.01945 ... 0.3991 0.5366\n",
+       "Indexes:\n",
+       "  ┌ x            CustomIndex_sel\n",
+       "  └ spatial_ref
" + ], + "text/plain": [ + " Size: 168B\n", + "Dimensions: (x: 10)\n", + "Coordinates:\n", + " * x (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n", + " * spatial_ref float64 8B nan\n", + "Data variables:\n", + " var1 (x) float64 80B 0.7656 0.8868 0.01945 ... 0.3991 0.5366\n", + "Indexes:\n", + " ┌ x CustomIndex_sel\n", + " └ spatial_ref" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds1" + ] + }, + { + "cell_type": "markdown", + "id": "cdc8af27", + "metadata": {}, + "source": [ + "Let's see if this works! Remember our coordinate transform (add desc. or illustration)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "a439ced4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 24B\n",
+       "Dimensions:      (x: 1)\n",
+       "Coordinates:\n",
+       "    x            (x) int64 8B 7\n",
+       "  * spatial_ref  float64 8B nan\n",
+       "Data variables:\n",
+       "    var1         (x) float64 8B 0.002764\n",
+       "Indexes:\n",
+       "    spatial_ref  CustomIndex_sel
" + ], + "text/plain": [ + " Size: 24B\n", + "Dimensions: (x: 1)\n", + "Coordinates:\n", + " x (x) int64 8B 7\n", + " * spatial_ref float64 8B nan\n", + "Data variables:\n", + " var1 (x) float64 8B 0.002764\n", + "Indexes:\n", + " spatial_ref CustomIndex_sel" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds1.sel(x=14)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "04a7c21d", + "metadata": {}, + "outputs": [], + "source": [ + "assert ds1.sel(x=14) == orig_ds1.sel(x=7)" + ] + }, + { + "cell_type": "markdown", + "id": "3cd5a17b", + "metadata": {}, + "source": [ + "`.sel()` can also handle passing lists and slices" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "855d6bc7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 56B\n",
+       "Dimensions:      (x: 3)\n",
+       "Coordinates:\n",
+       "    x            (x) int64 24B 4 5 7\n",
+       "  * spatial_ref  float64 8B nan\n",
+       "Data variables:\n",
+       "    var1         (x) float64 24B 0.1692 0.1182 0.002764\n",
+       "Indexes:\n",
+       "    spatial_ref  CustomIndex_sel
" + ], + "text/plain": [ + " Size: 56B\n", + "Dimensions: (x: 3)\n", + "Coordinates:\n", + " x (x) int64 24B 4 5 7\n", + " * spatial_ref float64 8B nan\n", + "Data variables:\n", + " var1 (x) float64 24B 0.1692 0.1182 0.002764\n", + "Indexes:\n", + " spatial_ref CustomIndex_sel" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds1.sel(x=[8,10,14])" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "3940d82e", + "metadata": {}, + "outputs": [], + "source": [ + "# dim order switches? so need to specify data to assert\n", + "assert np.array_equal(ds1.sel(x=[8,10,14])['var1'].data, orig_ds1.sel(x=[4,5,7])['var1'].data)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "990dd904", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 136B\n",
+       "Dimensions:      (x: 8)\n",
+       "Coordinates:\n",
+       "    x            (x) int64 64B 2 3 4 5 6 7 8 9\n",
+       "  * spatial_ref  float64 8B nan\n",
+       "Data variables:\n",
+       "    var1         (x) float64 64B 0.01945 0.9708 0.1692 ... 0.3991 0.5366\n",
+       "Indexes:\n",
+       "    spatial_ref  CustomIndex_sel
" + ], + "text/plain": [ + " Size: 136B\n", + "Dimensions: (x: 8)\n", + "Coordinates:\n", + " x (x) int64 64B 2 3 4 5 6 7 8 9\n", + " * spatial_ref float64 8B nan\n", + "Data variables:\n", + " var1 (x) float64 64B 0.01945 0.9708 0.1692 ... 0.3991 0.5366\n", + "Indexes:\n", + " spatial_ref CustomIndex_sel" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds1.sel(x=slice(4,18))" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "6453b620", + "metadata": {}, + "outputs": [], + "source": [ + "assert np.array_equal(ds1.sel(x=slice(4,18))['var1'].data, orig_ds1.sel(x=slice(2,9))['var1'].data)" + ] + }, + { + "cell_type": "markdown", + "id": "f38a8a7b-045b-444e-9e30-950c77d64b3f", + "metadata": {}, + "source": [ + "## Adding align\n", + "\n", + "NOTE: add illustration? \n", + "\n", + "Alignment is an important capability of Xarray indexes. It relies on three methods: `equals()`, `join()` and `reindex_like()`. \n", + "- `equals()`: Checks if the index is equal to the other index passed in the signatures are equal.\n", + "- `join()`: Joins the two indexes.\n", + "- `reindex_like()`: Reindexes the current index to match the result of the join.\n", + "Let's add them to the index :" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "b60dd186", + "metadata": {}, + "outputs": [], + "source": [ + "class CustomIndex(xr.Index): #customindex inherits xarray Index\n", + " def __init__(self, x_indexes, variables=None): \n", + " \n", + " self.indexes = variables\n", + " self._xindexes = x_indexes \n", + " if variables is not None:\n", + "\n", + " self.spatial_ref = variables['spatial_ref']\n", + " else:\n", + " self.spatial_ref = None\n", + " @classmethod \n", + " def from_variables(cls,variables, **kwargs):\n", + " '''this method creates a CustomIndex obj from a variables object.\n", + " variables is a dict created from ds1, keys are variable names,\n", + " values are associated xr.variables. created like this:\n", + " coord_vars = {name:ds._variables[name] for name in coord_names}\n", + " coord_names is passed to set_xindex\n", + " '''\n", + " # this index class expects to work with datasets with certain properties\n", + " # must have exactly 2 variables: x and spatial_ref \n", + " assert len(variables) == 2\n", + " assert 'x' in variables\n", + " assert 'spatial_ref' in variables \n", + " \n", + " dim_variables = {}\n", + " scalar_vars = {}\n", + " for k,i in variables.items():\n", + " if variables[k].ndim ==1:\n", + " dim_variables[k] = variables[k]\n", + " if variables[k].ndim ==0:\n", + " scalar_vars[k] = variables[k]\n", + " \n", + " options = {'dim':'x',\n", + " 'name':'x'}\n", + " \n", + " x_indexes = {\n", + " k: PandasIndex.from_variables({k: v}, options = options) \n", + " for k,v in dim_variables.items()\n", + " }\n", + " \n", + " x_indexes['spatial_ref'] = variables['spatial_ref']\n", + " \n", + " return cls(x_indexes, variables)\n", + " \n", + " def create_variables(self, variables=None):\n", + " '''creates coord variable from index'''\n", + " if not variables:\n", + " variables = self.joined_var\n", + "\n", + " idx_variables = {}\n", + " \n", + "\n", + " for index in self._xindexes.values():\n", + " #want to skip spatial ref\n", + " if type(index) == xr.core.variable.Variable:\n", + " pass\n", + " else:\n", + "\n", + " x = index.create_variables(variables)\n", + " idx_variables.update(x)\n", + " \n", + " idx_variables['spatial_ref'] = variables['spatial_ref'] \n", + " return idx_variables\n", + "\n", + " def transform(self, value):\n", + " \n", + " #extract attrs\n", + " fac = self.spatial_ref.attrs['factor']\n", + " key = self.spatial_ref.attrs['idx_name']\n", + "\n", + " #handle slice\n", + " if isinstance(value, slice):\n", + " \n", + " start, stop, step = value.start, value.stop, value.step\n", + " new_start, new_stop, new_step = start / fac, stop/fac, step\n", + " new_val = slice(new_start, new_stop, new_step)\n", + " transformed_labels = {key: new_val}\n", + " return transformed_labels\n", + " \n", + " #single or list of values\n", + " else:\n", + " \n", + " vals_to_transform = [] \n", + "\n", + " if not isinstance(value, Sequence):\n", + " value = [value]\n", + "\n", + " for k in range(len(value)):\n", + "\n", + " val = value[k]\n", + " vals_to_transform.append(val)\n", + "\n", + " #logic for parsing attrs, todo: switch to actual transform\n", + " transformed_x = [int(v / fac) for v in vals_to_transform]\n", + "\n", + " transformed_labels = {key:transformed_x}\n", + " return transformed_labels\n", + "\n", + " def sel(self, labels):\n", + " \n", + " assert type(labels) == dict\n", + "\n", + " #user passes to sel\n", + " label = next(iter(labels.values()))\n", + "\n", + " #materialize coord array to idx off of\n", + " params = self.spatial_ref.attrs['range']\n", + " full_arr = np.arange(params[0], params[1], params[2])\n", + " toy_index = PandasIndex(full_arr, dim='x')\n", + "\n", + " #transform user labesl to coord crs\n", + " idx = self.transform(label)\n", + "\n", + " #sel on index created in .sel()\n", + " matches = toy_index.sel(idx)\n", + "\n", + " return matches \n", + " \n", + "\n", + " def equals(self, other):\n", + " \"\"\"\n", + " Check if the current instance is equal to another instance.\n", + " Parameters\n", + " ----------\n", + " other : object\n", + " The other instance to compare with.\n", + "\n", + " Returns\n", + " -------\n", + " bool\n", + " True if the current instance is equal to the other instance, False otherwise.\n", + " \"\"\"\n", + " \n", + " result = self._xindexes['x'].equals(other._xindexes['x']) and self._xindexes['spatial_ref'].equals(other._xindexes['spatial_ref'])\n", + " \n", + " return result\n", + "\n", + " def join(self, other, how='inner'):\n", + " \"\"\"\n", + " Join the current index with another index.\n", + "\n", + " Parameters:\n", + " -----------\n", + " other : PandasIndex\n", + " The index to join with.\n", + " how : str, optional\n", + " The type of join to perform. Default is 'inner'.\n", + "\n", + " Returns:\n", + " --------\n", + " new_obj : PandasIndex\n", + " A new PandasIndex object representing the joined index.\n", + "\n", + " Notes:\n", + " ------\n", + " This method joins the current index with another index based on a common dimension.\n", + "\n", + " The current index and the other index are first converted into PandasIndex objects.\n", + "\n", + " The spatial reference information of the joined index is updated based on the start, stop, and step values of the joined index.\n", + "\n", + " The joined index is then converted back into a PandasIndex object and returned as a new PandasIndex object.\n", + " \"\"\"\n", + " #make self index obj\n", + " params_self = self.spatial_ref.attrs['range']\n", + " full_arr_self = np.arange(params_self[0], params_self[1], params_self[2])\n", + " toy_index_self = PandasIndex(full_arr_self, dim='x')\n", + " \n", + "\n", + " #make other index obj\n", + " other_start = other._xindexes['x'].index.array[0]\n", + " other_stop = other._xindexes['x'].index.array[-1]\n", + " other_step = np.abs(int((other_start-other_stop) / (len(other._xindexes['x'].index.array)-1)))\n", + " \n", + " \n", + " params_other = other.spatial_ref.attrs['range']\n", + " full_arr_other = np.arange(other_start, other_stop, other_step) #prev elements of params_other\n", + " toy_index_other = PandasIndex(full_arr_other, dim='x')\n", + " \n", + " self._indexes = {'x': toy_index_self}\n", + " other._indexes = {'x':toy_index_other}\n", + " \n", + " \n", + " new_indexes = {'x':toy_index_self.join(toy_index_other, how=how)}\n", + " \n", + " #need to return an index obj, but don't want to have to pass variables\n", + " # so need to add all of the things that index needs to new_indexes before passign it to return?\n", + " \n", + " #this will need to be generalized / tested more\n", + " new_indexes['spatial_ref'] = deepcopy(self.spatial_ref) \n", + " start = int(new_indexes['x'].index.array[0])\n", + " stop = int(new_indexes['x'].index.array[-1])\n", + " step = int((stop-start) / (len(new_indexes['x'].index.array) -1))\n", + " \n", + " new_indexes['spatial_ref'].attrs['range'] = [start, stop, step]\n", + " \n", + " idx_var = xr.IndexVariable(dims=new_indexes['x'].index.name,\n", + " data = new_indexes['x'].index.array)\n", + " attr_var = new_indexes['spatial_ref']\n", + " \n", + " idx_dict = {'x':idx_var, \n", + " 'spatial_ref':attr_var}\n", + " \n", + " new_obj = type(self)(new_indexes)\n", + " new_obj.joined_var = idx_dict\n", + " return new_obj\n", + " \n", + "\n", + " def reindex_like(self, other, method=None, tolerance=None):\n", + " \"\"\"\n", + " Reindexes the current object to match the index of another object.\n", + "\n", + " Parameters:\n", + " -----------\n", + " other : object\n", + " The object whose index will be used for reindexing.\n", + " method : str, optional\n", + " The method to use for reindexing. Default is None.\n", + " tolerance : float, optional\n", + " The tolerance value to use for reindexing. Default is None.\n", + "\n", + " Returns:\n", + " --------\n", + " dict\n", + " A dictionary containing the reindexed values.\n", + "\n", + " Raises:\n", + " -------\n", + " None\n", + "\n", + " Notes:\n", + " ------\n", + " This method reindexes the current object to match the index of the `other` object.\n", + " It uses the `method` and `tolerance` parameters to determine the reindexing behavior.\n", + " The reindexed values are returned as a dictionary.\n", + " \"\"\"\n", + " \n", + " params_self = self.spatial_ref.attrs['range']\n", + " full_arr_self = np.arange(params_self[0], params_self[1], params_self[2])\n", + " toy_index_self = PandasIndex(full_arr_self, dim='x')\n", + " \n", + " toy_index_other = other._xindexes['x']\n", + " \n", + " d = {'x': toy_index_self.index.get_indexer(other._xindexes['x'].index, method, tolerance)}\n", + " \n", + " return d\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "33592c6e", + "metadata": {}, + "outputs": [], + "source": [ + "#create new sample data\n", + "sample_ds1 = create_sample_data(make_kwargs(2,[0,10,1],10))\n", + "sample_ds2 = create_sample_data(make_kwargs(5,[5,15,1],10))\n", + "\n", + "\n", + "#create a copy used for testing later\n", + "orig_ds1 = sample_ds1.copy()\n", + "orig_ds2 = sample_ds2.copy()" + ] + }, + { + "cell_type": "markdown", + "id": "1f45f21d-d7c9-4001-95cd-5a178977c00a", + "metadata": {}, + "source": [ + "*** reindex_like needs to return an object like variables to pass to create vars (?)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "fec339d7", + "metadata": {}, + "outputs": [], + "source": [ + "sample_ds1 = sample_ds1.drop_indexes('x')\n", + "sample_ds2 = sample_ds2.drop_indexes('x')" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "d1b68363", + "metadata": {}, + "outputs": [], + "source": [ + "ds1 = sample_ds1.set_xindex(['x','spatial_ref'], CustomIndex)\n", + "ds2 = sample_ds2.set_xindex(['x','spatial_ref'], CustomIndex)\n" + ] + }, + { + "cell_type": "markdown", + "id": "a9780e70-26f8-48b5-917a-10521c2563ef", + "metadata": {}, + "source": [ + "## Align" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "929056d2", + "metadata": {}, + "outputs": [], + "source": [ + "#create sample data -- we define 2 for alignment\n", + "sample_ds1 = create_sample_data(make_kwargs(2,[0,10,1],10))\n", + "sample_ds2 = create_sample_data(make_kwargs(5,[8,18,1], 10))\n", + "\n", + "#create copies used for testing later\n", + "orig_ds1 = sample_ds1.copy()\n", + "orig_ds2 = sample_ds2.copy()" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "18cecad6", + "metadata": {}, + "outputs": [], + "source": [ + "sample_ds1 = sample_ds1.drop_indexes('x')\n", + "sample_ds2 = sample_ds2.drop_indexes('x')" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "21f9bae9", + "metadata": {}, + "outputs": [], + "source": [ + "ds1 = sample_ds1.set_xindex(['x','spatial_ref'], CustomIndex)\n", + "ds2 = sample_ds2.set_xindex(['x','spatial_ref'], CustomIndex)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "9cd9ba30-bcd8-4228-9ab5-d4bf983a173c", + "metadata": {}, + "outputs": [], + "source": [ + "inner_align, _ = xr.align(ds1, ds2, join='inner')" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "123b9079-ed3f-47ec-96f5-6eb07cec2f06", + "metadata": {}, + "outputs": [], + "source": [ + "outer_align, _ = xr.align(ds1, ds2, join='outer')" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "81649250-0f02-4194-ac8b-347badf1a42c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 280B\n",
+       "Dimensions:      (x: 17)\n",
+       "Coordinates:\n",
+       "  * x            (x) int64 136B 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16\n",
+       "  * spatial_ref  float64 8B nan\n",
+       "Data variables:\n",
+       "    var1         (x) float64 136B 0.06861 0.6461 0.5169 0.1095 ... nan nan nan\n",
+       "Indexes:\n",
+       "    x            CustomIndex\n",
+       "    spatial_ref  CustomIndex
" + ], + "text/plain": [ + " Size: 280B\n", + "Dimensions: (x: 17)\n", + "Coordinates:\n", + " * x (x) int64 136B 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16\n", + " * spatial_ref float64 8B nan\n", + "Data variables:\n", + " var1 (x) float64 136B 0.06861 0.6461 0.5169 0.1095 ... nan nan nan\n", + "Indexes:\n", + " x CustomIndex\n", + " spatial_ref CustomIndex" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "outer_align" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "ef183d67-3715-442a-8ac6-18327352e5f5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 40B\n",
+       "Dimensions:      (x: 2)\n",
+       "Coordinates:\n",
+       "  * x            (x) int64 16B 8 9\n",
+       "  * spatial_ref  float64 8B nan\n",
+       "Data variables:\n",
+       "    var1         (x) float64 16B 0.1917 0.01797\n",
+       "Indexes:\n",
+       "    x            CustomIndex\n",
+       "    spatial_ref  CustomIndex
" + ], + "text/plain": [ + " Size: 40B\n", + "Dimensions: (x: 2)\n", + "Coordinates:\n", + " * x (x) int64 16B 8 9\n", + " * spatial_ref float64 8B nan\n", + "Data variables:\n", + " var1 (x) float64 16B 0.1917 0.01797\n", + "Indexes:\n", + " x CustomIndex\n", + " spatial_ref CustomIndex" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "inner_align" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "d539bffe-af6f-4d62-afcc-2ba35a7b3da2", + "metadata": {}, + "outputs": [], + "source": [ + "# reindex_like not implemented for PandasIndx\n", + "# but that defaults to inner, and these are successsfuly producing left and right so shouldn't be it\n", + "#left_align,_ = xr.align(ds1, ds2, join='left')\n", + "#right_align,_ = xr.align(ds1, ds2, join='right')\n", + "\n", + "#don't remember what above was about , is reindex like not implemented for left, right joins something like that ?" + ] + }, + { + "cell_type": "markdown", + "id": "27dc5208-44a0-415e-ab55-8082e5af5276", + "metadata": {}, + "source": [ + "## Wrap up / summary\n", + "To do" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4d62979-c452-4cdd-836d-0781cd9cd475", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:miniconda-arraylake]", + "language": "python", + "name": "conda-env-miniconda-arraylake-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.11.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/intermediate/indexing/img2.png b/intermediate/indexing/img2.png new file mode 100644 index 0000000000000000000000000000000000000000..6282ff3cd3690d35c03c9d4f139330404092f567 GIT binary patch literal 82118 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�s^ByjRz+IDk?AkY-wpJ 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

%5dhfaxxCB5S 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 Date: Mon, 19 Aug 2024 22:01:27 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../indexing/build_custom_index_1d.ipynb | 4134 ++--------------- 1 file changed, 275 insertions(+), 3859 deletions(-) diff --git a/intermediate/indexing/build_custom_index_1d.ipynb b/intermediate/indexing/build_custom_index_1d.ipynb index 59de152f..fa6f50cd 100644 --- a/intermediate/indexing/build_custom_index_1d.ipynb +++ b/intermediate/indexing/build_custom_index_1d.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "8a8e5623-0622-4ec8-92e8-31d1d2e61d0e", + "id": "0", "metadata": {}, "source": [ "# Creating a custom Xarray index \n", @@ -43,12 +43,12 @@ }, { "cell_type": "code", - "execution_count": 1, - "id": "09c0cdb8-8570-4a29-b013-bbd8a6a451e8", + "execution_count": null, + "id": "1", "metadata": {}, "outputs": [], "source": [ - "import xarray as xr \n", + "import xarray as xr\n", "import numpy as np\n", "from collections.abc import Sequence\n", "from copy import deepcopy\n", @@ -63,7 +63,7 @@ }, { "cell_type": "markdown", - "id": "6105c786", + "id": "2", "metadata": {}, "source": [ "### Define sample data\n", @@ -72,8 +72,8 @@ }, { "cell_type": "code", - "execution_count": 2, - "id": "fb2f688b", + "execution_count": null, + "id": "3", "metadata": {}, "outputs": [], "source": [ @@ -94,16 +94,17 @@ " -------\n", " dict\n", "\n", - " \"\"\"\n", + " \"\"\"\n", " da_kwargs = {\n", " 'factor': factor,\n", - " 'range' : range_ls,\n", - " 'idx_name':'x',\n", - " 'real_name':'lon',\n", - " 'data_len': data_len\n", + " 'range': range_ls,\n", + " 'idx_name': 'x',\n", + " 'real_name': 'lon',\n", + " 'data_len': data_len,\n", " }\n", " return da_kwargs\n", "\n", + "\n", "def create_sample_data(kwargs: dict) -> xr.Dataset:\n", " \"\"\"\n", " Function to create sample data.\n", @@ -140,15 +141,13 @@ " 'factor': kwargs['factor'],\n", " 'range': kwargs['range'],\n", " 'idx_name': kwargs['idx_name'],\n", - " 'real_name': kwargs['real_name']\n", + " 'real_name': kwargs['real_name'],\n", " }\n", "\n", " da = xr.DataArray(\n", " data=np.random.rand(kwargs['data_len']),\n", " dims=(kwargs['idx_name']),\n", - " coords={\n", - " 'x': np.arange(kwargs['range'][0], kwargs['range'][1], kwargs['range'][2])\n", - " }\n", + " coords={'x': np.arange(kwargs['range'][0], kwargs['range'][1], kwargs['range'][2])},\n", " )\n", "\n", " ds = xr.Dataset({'var1': da})\n", @@ -159,425 +158,35 @@ " ds['spatial_ref'] = spatial_ref\n", " ds = ds.set_coords('spatial_ref')\n", "\n", - " #ds = ds.expand_dims({'y': 1})\n", + " # ds = ds.expand_dims({'y': 1})\n", "\n", " return ds" ] }, { "cell_type": "code", - "execution_count": 3, - "id": "3873f02b", + "execution_count": null, + "id": "4", "metadata": {}, "outputs": [], "source": [ - "#create sample data \n", - "sample_ds1 = create_sample_data(make_kwargs(2,[0,10,1],10))" + "# create sample data\n", + "sample_ds1 = create_sample_data(make_kwargs(2, [0, 10, 1], 10))" ] }, { "cell_type": "code", - "execution_count": 4, - "id": "5fa03c98", + "execution_count": null, + "id": "5", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "

\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset> Size: 168B\n",
-       "Dimensions:      (x: 10)\n",
-       "Coordinates:\n",
-       "  * x            (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n",
-       "    spatial_ref  float64 8B nan\n",
-       "Data variables:\n",
-       "    var1         (x) float64 80B 0.6362 0.6464 0.3027 ... 0.00198 0.2211 0.5605
" - ], - "text/plain": [ - " Size: 168B\n", - "Dimensions: (x: 10)\n", - "Coordinates:\n", - " * x (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n", - " spatial_ref float64 8B nan\n", - "Data variables:\n", - " var1 (x) float64 80B 0.6362 0.6464 0.3027 ... 0.00198 0.2211 0.5605" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "sample_ds1" ] }, { "cell_type": "markdown", - "id": "7a32f356", + "id": "6", "metadata": {}, "source": [ "## Defining a custom index\n", @@ -590,28 +199,17 @@ }, { "cell_type": "code", - "execution_count": 5, - "id": "d22821de", + "execution_count": null, + "id": "7", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pandas.core.indexes.base.Index" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "type(sample_ds1.indexes['x'])" ] }, { "cell_type": "markdown", - "id": "eddcbb2a", + "id": "8", "metadata": {}, "source": [ "We want to replace the `PandasIndex` with our `CustomIndex`. To do this, we'll drop the `x` index from that dataset: \n", @@ -626,7 +224,7 @@ }, { "cell_type": "markdown", - "id": "976b9a31", + "id": "9", "metadata": {}, "source": [ "## The smallest `CustomIndex` \n", @@ -636,21 +234,21 @@ }, { "cell_type": "code", - "execution_count": 6, - "id": "3fe488e8-3f13-42a8-a74e-a8a2bb6f15f9", + "execution_count": null, + "id": "10", "metadata": {}, "outputs": [], "source": [ - "class CustomIndex_tiny(xr.Index): #customindex inherits xarray Index\n", - " def __init__(self, x_indexes, variables=None): \n", - " \n", + "class CustomIndex_tiny(xr.Index): # customindex inherits xarray Index\n", + " def __init__(self, x_indexes, variables=None):\n", + "\n", " self.indexes = variables\n", - " self._xindexes = x_indexes \n", + " self._xindexes = x_indexes\n", "\n", " self.spatial_ref = variables['spatial_ref']\n", - " \n", - " @classmethod \n", - " def from_variables(cls,variables, **kwargs):\n", + "\n", + " @classmethod\n", + " def from_variables(cls, variables, **kwargs):\n", " '''this method creates a CustomIndex obj from a variables object.\n", " variables is a dict created from ds1, keys are variable names,\n", " values are associated xr.variables. created like this:\n", @@ -658,38 +256,36 @@ " coord_names is passed to set_xindex\n", " '''\n", " # this index class expects to work with datasets with certain properties\n", - " # it must have exactly 2 variables: x and spatial_ref \n", + " # it must have exactly 2 variables: x and spatial_ref\n", " assert len(variables) == 2\n", " assert 'x' in variables\n", - " assert 'spatial_ref' in variables \n", - " \n", - " #separate dimensional, scalar variables into own dicts\n", + " assert 'spatial_ref' in variables\n", + "\n", + " # separate dimensional, scalar variables into own dicts\n", " dim_variables = {}\n", " scalar_vars = {}\n", - " for k,i in variables.items():\n", - " if variables[k].ndim ==1:\n", + " for k, i in variables.items():\n", + " if variables[k].ndim == 1:\n", " dim_variables[k] = variables[k]\n", - " if variables[k].ndim ==0:\n", + " if variables[k].ndim == 0:\n", " scalar_vars[k] = variables[k]\n", - " \n", - " options = {'dim':'x',\n", - " 'name':'x'}\n", - " \n", - " #make dict of PandasIndexes for dim. variable\n", + "\n", + " options = {'dim': 'x', 'name': 'x'}\n", + "\n", + " # make dict of PandasIndexes for dim. variable\n", " x_indexes = {\n", - " k: PandasIndex.from_variables({k: v}, options = options) \n", - " for k,v in dim_variables.items()\n", + " k: PandasIndex.from_variables({k: v}, options=options) for k, v in dim_variables.items()\n", " }\n", - " #add scalar var to dict\n", + " # add scalar var to dict\n", " x_indexes['spatial_ref'] = variables['spatial_ref']\n", - " \n", - " return cls(x_indexes, variables) #return an instance of CustomIndex class\n" + "\n", + " return cls(x_indexes, variables) # return an instance of CustomIndex class" ] }, { "cell_type": "code", - "execution_count": 7, - "id": "904247b1", + "execution_count": null, + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -698,423 +294,27 @@ }, { "cell_type": "code", - "execution_count": 8, - "id": "9f3ce806", + "execution_count": null, + "id": "12", "metadata": {}, "outputs": [], "source": [ - "ds1 = sample_ds1.set_xindex(['x','spatial_ref'], CustomIndex_tiny)" + "ds1 = sample_ds1.set_xindex(['x', 'spatial_ref'], CustomIndex_tiny)" ] }, { "cell_type": "code", - "execution_count": 9, - "id": "c00eefb4-c0b7-4c78-9f80-8d068ba245f4", + "execution_count": null, + "id": "13", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset> Size: 168B\n",
-       "Dimensions:      (x: 10)\n",
-       "Coordinates:\n",
-       "  * x            (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n",
-       "  * spatial_ref  float64 8B nan\n",
-       "Data variables:\n",
-       "    var1         (x) float64 80B 0.6362 0.6464 0.3027 ... 0.00198 0.2211 0.5605\n",
-       "Indexes:\n",
-       "  ┌ x            CustomIndex_tiny\n",
-       "  └ spatial_ref
" - ], - "text/plain": [ - " Size: 168B\n", - "Dimensions: (x: 10)\n", - "Coordinates:\n", - " * x (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n", - " * spatial_ref float64 8B nan\n", - "Data variables:\n", - " var1 (x) float64 80B 0.6362 0.6464 0.3027 ... 0.00198 0.2211 0.5605\n", - "Indexes:\n", - " ┌ x CustomIndex_tiny\n", - " └ spatial_ref" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ds1" ] }, { "cell_type": "markdown", - "id": "71bfea5e", + "id": "14", "metadata": {}, "source": [ "As mentioned above, `ds1` now has the CustomIndex, but it can't do much." @@ -1122,26 +322,10 @@ }, { "cell_type": "code", - "execution_count": 10, - "id": "5281809e", + "execution_count": null, + "id": "15", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Exception reporting mode: Minimal\n" - ] - }, - { - "ename": "NotImplementedError", - "evalue": "<__main__.CustomIndex_tiny object at 0x7f7c7c220ad0> doesn't support label-based selection", - "output_type": "error", - "traceback": [ - "\u001b[0;31mNotImplementedError\u001b[0m\u001b[0;31m:\u001b[0m <__main__.CustomIndex_tiny object at 0x7f7c7c220ad0> doesn't support label-based selection\n" - ] - } - ], + "outputs": [], "source": [ "%xmode Minimal\n", "\n", @@ -1150,7 +334,7 @@ }, { "cell_type": "markdown", - "id": "425b91fe", + "id": "16", "metadata": {}, "source": [ "### More detail on `from_variables()`\n", @@ -1163,7 +347,7 @@ }, { "cell_type": "markdown", - "id": "b082ec1e", + "id": "17", "metadata": {}, "source": [ "## Adding a coordinate transform and `.sel()` to `CustomIndex`\n", @@ -1175,435 +359,45 @@ }, { "cell_type": "code", - "execution_count": 11, - "id": "e581fe41", + "execution_count": null, + "id": "18", "metadata": {}, "outputs": [], "source": [ - "#create new sample data\n", - "sample_ds1 = create_sample_data(make_kwargs(2,[0,10,1],10))\n", + "# create new sample data\n", + "sample_ds1 = create_sample_data(make_kwargs(2, [0, 10, 1], 10))\n", "\n", - "#create a copy used for testing later\n", + "# create a copy used for testing later\n", "orig_ds1 = sample_ds1.copy()" ] }, { "cell_type": "code", - "execution_count": 12, - "id": "d905c300", + "execution_count": null, + "id": "19", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset> Size: 168B\n",
-       "Dimensions:      (x: 10)\n",
-       "Coordinates:\n",
-       "  * x            (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n",
-       "    spatial_ref  float64 8B nan\n",
-       "Data variables:\n",
-       "    var1         (x) float64 80B 0.9233 0.5917 0.4112 ... 0.291 0.2584 0.1781
" - ], - "text/plain": [ - " Size: 168B\n", - "Dimensions: (x: 10)\n", - "Coordinates:\n", - " * x (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n", - " spatial_ref float64 8B nan\n", - "Data variables:\n", - " var1 (x) float64 80B 0.9233 0.5917 0.4112 ... 0.291 0.2584 0.1781" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "sample_ds1" ] }, { "cell_type": "code", - "execution_count": 13, - "id": "c380e9a9", + "execution_count": null, + "id": "20", "metadata": {}, "outputs": [], "source": [ - "class CustomIndex_sel(xr.Index): #customindex inherits xarray Index\n", - " def __init__(self, x_indexes, variables=None): \n", - " \n", + "class CustomIndex_sel(xr.Index): # customindex inherits xarray Index\n", + " def __init__(self, x_indexes, variables=None):\n", + "\n", " self.indexes = variables\n", - " self._xindexes = x_indexes \n", + " self._xindexes = x_indexes\n", "\n", " self.spatial_ref = variables['spatial_ref']\n", - " \n", - " @classmethod \n", - " def from_variables(cls,variables, **kwargs):\n", + "\n", + " @classmethod\n", + " def from_variables(cls, variables, **kwargs):\n", " '''this method creates a CustomIndex obj from a variables object.\n", " variables is a dict created from ds1, keys are variable names,\n", " values are associated xr.variables. created like this:\n", @@ -1611,31 +405,29 @@ " coord_names is passed to set_xindex\n", " '''\n", " # this index class expects to work with datasets with certain properties\n", - " # it must have exactly 2 variables: x and spatial_ref \n", + " # it must have exactly 2 variables: x and spatial_ref\n", " assert len(variables) == 2\n", " assert 'x' in variables\n", - " assert 'spatial_ref' in variables \n", - " \n", + " assert 'spatial_ref' in variables\n", + "\n", " dim_variables = {}\n", " scalar_vars = {}\n", - " for k,i in variables.items():\n", - " if variables[k].ndim ==1:\n", + " for k, i in variables.items():\n", + " if variables[k].ndim == 1:\n", " dim_variables[k] = variables[k]\n", - " if variables[k].ndim ==0:\n", + " if variables[k].ndim == 0:\n", " scalar_vars[k] = variables[k]\n", - " \n", - " options = {'dim':'x',\n", - " 'name':'x'}\n", - " \n", + "\n", + " options = {'dim': 'x', 'name': 'x'}\n", + "\n", " x_indexes = {\n", - " k: PandasIndex.from_variables({k: v}, options = options) \n", - " for k,v in dim_variables.items()\n", + " k: PandasIndex.from_variables({k: v}, options=options) for k, v in dim_variables.items()\n", " }\n", - " \n", + "\n", " x_indexes['spatial_ref'] = variables['spatial_ref']\n", - " \n", - " return cls(x_indexes, variables) #return an instance of CustomIndex class\n", - " \n", + "\n", + " return cls(x_indexes, variables) # return an instance of CustomIndex class\n", + "\n", " def create_variables(self, variables=None):\n", " '''\n", " Creates coord variable from index.\n", @@ -1676,24 +468,23 @@ " return idx_variables\n", "\n", " idx_variables = {}\n", - " \n", "\n", " for index in self._xindexes.values():\n", - " #want to skip spatial ref\n", + " # want to skip spatial ref\n", " if type(index) == xr.core.variable.Variable:\n", " pass\n", " else:\n", "\n", " x = index.create_variables(variables)\n", " idx_variables.update(x)\n", - " \n", - " idx_variables['spatial_ref'] = variables['spatial_ref'] \n", + "\n", + " idx_variables['spatial_ref'] = variables['spatial_ref']\n", " return idx_variables\n", "\n", " def transform(self, value):\n", " \"\"\"\n", " Transform the given value based on the spatial reference attributes. Currently, this only handles a very simple transform.\n", - " NOTE: this could be removed from the index class and passed to set_xindex()? \n", + " NOTE: this could be removed from the index class and passed to set_xindex()?\n", "\n", " Parameters:\n", " -----------\n", @@ -1725,23 +516,23 @@ " >>> print(transformed_labels)\n", " {'index': slice(5, 10, 2)}\n", " \"\"\"\n", - " #extract attrs\n", + " # extract attrs\n", " fac = self.spatial_ref.attrs['factor']\n", " key = self.spatial_ref.attrs['idx_name']\n", "\n", - " #handle slice\n", + " # handle slice\n", " if isinstance(value, slice):\n", - " \n", + "\n", " start, stop, step = value.start, value.stop, value.step\n", - " new_start, new_stop, new_step = start / fac, stop/fac, step\n", + " new_start, new_stop, new_step = start / fac, stop / fac, step\n", " new_val = slice(new_start, new_stop, new_step)\n", " transformed_labels = {key: new_val}\n", " return transformed_labels\n", - " \n", - " #single or list of values\n", + "\n", + " # single or list of values\n", " else:\n", - " \n", - " vals_to_transform = [] \n", + "\n", + " vals_to_transform = []\n", "\n", " if not isinstance(value, Sequence):\n", " value = [value]\n", @@ -1751,10 +542,10 @@ " val = value[k]\n", " vals_to_transform.append(val)\n", "\n", - " #logic for parsing attrs\n", + " # logic for parsing attrs\n", " transformed_x = [int(v / fac) for v in vals_to_transform]\n", "\n", - " transformed_labels = {key:transformed_x}\n", + " transformed_labels = {key: transformed_x}\n", " return transformed_labels\n", "\n", " def sel(self, labels):\n", @@ -1790,29 +581,29 @@ " >>> print(matches)\n", " PandasIndex([10], dtype='int64', name='x')\n", " \"\"\"\n", - " \n", + "\n", " assert type(labels) == dict\n", "\n", - " #user passes to sel\n", + " # user passes to sel\n", " label = next(iter(labels.values()))\n", "\n", - " #materialize coord array to idx off of\n", + " # materialize coord array to idx off of\n", " params = self.spatial_ref.attrs['range']\n", " full_arr = np.arange(params[0], params[1], params[2])\n", " toy_index = PandasIndex(full_arr, dim='x')\n", "\n", - " #transform user labesl to coord crs\n", + " # transform user labesl to coord crs\n", " idx = self.transform(label)\n", "\n", - " #sel on index created in .sel()\n", + " # sel on index created in .sel()\n", " matches = toy_index.sel(idx)\n", "\n", - " return matches " + " return matches" ] }, { "cell_type": "markdown", - "id": "8b79dee8", + "id": "21", "metadata": {}, "source": [ "Drop index:" @@ -1820,8 +611,8 @@ }, { "cell_type": "code", - "execution_count": 15, - "id": "8d7f2ccd", + "execution_count": null, + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -1830,423 +621,27 @@ }, { "cell_type": "code", - "execution_count": 16, - "id": "ddcf8c79", + "execution_count": null, + "id": "23", "metadata": {}, "outputs": [], "source": [ - "ds1 = sample_ds1.set_xindex(['x','spatial_ref'], CustomIndex_sel)" + "ds1 = sample_ds1.set_xindex(['x', 'spatial_ref'], CustomIndex_sel)" ] }, { "cell_type": "code", - "execution_count": 17, - "id": "2c1290b1", + "execution_count": null, + "id": "24", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset> Size: 168B\n",
-       "Dimensions:      (x: 10)\n",
-       "Coordinates:\n",
-       "  * x            (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n",
-       "  * spatial_ref  float64 8B nan\n",
-       "Data variables:\n",
-       "    var1         (x) float64 80B 0.7656 0.8868 0.01945 ... 0.3991 0.5366\n",
-       "Indexes:\n",
-       "  ┌ x            CustomIndex_sel\n",
-       "  └ spatial_ref
" - ], - "text/plain": [ - " Size: 168B\n", - "Dimensions: (x: 10)\n", - "Coordinates:\n", - " * x (x) int64 80B 0 1 2 3 4 5 6 7 8 9\n", - " * spatial_ref float64 8B nan\n", - "Data variables:\n", - " var1 (x) float64 80B 0.7656 0.8868 0.01945 ... 0.3991 0.5366\n", - "Indexes:\n", - " ┌ x CustomIndex_sel\n", - " └ spatial_ref" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ds1" ] }, { "cell_type": "markdown", - "id": "cdc8af27", + "id": "25", "metadata": {}, "source": [ "Let's see if this works! Remember our coordinate transform (add desc. or illustration)" @@ -2254,411 +649,18 @@ }, { "cell_type": "code", - "execution_count": 18, - "id": "a439ced4", + "execution_count": null, + "id": "26", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset> Size: 24B\n",
-       "Dimensions:      (x: 1)\n",
-       "Coordinates:\n",
-       "    x            (x) int64 8B 7\n",
-       "  * spatial_ref  float64 8B nan\n",
-       "Data variables:\n",
-       "    var1         (x) float64 8B 0.002764\n",
-       "Indexes:\n",
-       "    spatial_ref  CustomIndex_sel
" - ], - "text/plain": [ - " Size: 24B\n", - "Dimensions: (x: 1)\n", - "Coordinates:\n", - " x (x) int64 8B 7\n", - " * spatial_ref float64 8B nan\n", - "Data variables:\n", - " var1 (x) float64 8B 0.002764\n", - "Indexes:\n", - " spatial_ref CustomIndex_sel" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ds1.sel(x=14)" ] }, { "cell_type": "code", - "execution_count": 19, - "id": "04a7c21d", + "execution_count": null, + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -2667,7 +669,7 @@ }, { "cell_type": "markdown", - "id": "3cd5a17b", + "id": "28", "metadata": {}, "source": [ "`.sel()` can also handle passing lists and slices" @@ -2675,835 +677,50 @@ }, { "cell_type": "code", - "execution_count": 20, - "id": "855d6bc7", + "execution_count": null, + "id": "29", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset> Size: 56B\n",
-       "Dimensions:      (x: 3)\n",
-       "Coordinates:\n",
-       "    x            (x) int64 24B 4 5 7\n",
-       "  * spatial_ref  float64 8B nan\n",
-       "Data variables:\n",
-       "    var1         (x) float64 24B 0.1692 0.1182 0.002764\n",
-       "Indexes:\n",
-       "    spatial_ref  CustomIndex_sel
" - ], - "text/plain": [ - " Size: 56B\n", - "Dimensions: (x: 3)\n", - "Coordinates:\n", - " x (x) int64 24B 4 5 7\n", - " * spatial_ref float64 8B nan\n", - "Data variables:\n", - " var1 (x) float64 24B 0.1692 0.1182 0.002764\n", - "Indexes:\n", - " spatial_ref CustomIndex_sel" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "ds1.sel(x=[8,10,14])" + "ds1.sel(x=[8, 10, 14])" ] }, { "cell_type": "code", - "execution_count": 21, - "id": "3940d82e", + "execution_count": null, + "id": "30", "metadata": {}, "outputs": [], "source": [ "# dim order switches? so need to specify data to assert\n", - "assert np.array_equal(ds1.sel(x=[8,10,14])['var1'].data, orig_ds1.sel(x=[4,5,7])['var1'].data)" + "assert np.array_equal(ds1.sel(x=[8, 10, 14])['var1'].data, orig_ds1.sel(x=[4, 5, 7])['var1'].data)" ] }, { "cell_type": "code", - "execution_count": 22, - "id": "990dd904", + "execution_count": null, + "id": "31", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset> Size: 136B\n",
-       "Dimensions:      (x: 8)\n",
-       "Coordinates:\n",
-       "    x            (x) int64 64B 2 3 4 5 6 7 8 9\n",
-       "  * spatial_ref  float64 8B nan\n",
-       "Data variables:\n",
-       "    var1         (x) float64 64B 0.01945 0.9708 0.1692 ... 0.3991 0.5366\n",
-       "Indexes:\n",
-       "    spatial_ref  CustomIndex_sel
" - ], - "text/plain": [ - " Size: 136B\n", - "Dimensions: (x: 8)\n", - "Coordinates:\n", - " x (x) int64 64B 2 3 4 5 6 7 8 9\n", - " * spatial_ref float64 8B nan\n", - "Data variables:\n", - " var1 (x) float64 64B 0.01945 0.9708 0.1692 ... 0.3991 0.5366\n", - "Indexes:\n", - " spatial_ref CustomIndex_sel" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "ds1.sel(x=slice(4,18))" + "ds1.sel(x=slice(4, 18))" ] }, { "cell_type": "code", - "execution_count": 23, - "id": "6453b620", + "execution_count": null, + "id": "32", "metadata": {}, "outputs": [], "source": [ - "assert np.array_equal(ds1.sel(x=slice(4,18))['var1'].data, orig_ds1.sel(x=slice(2,9))['var1'].data)" + "assert np.array_equal(\n", + " ds1.sel(x=slice(4, 18))['var1'].data, orig_ds1.sel(x=slice(2, 9))['var1'].data\n", + ")" ] }, { "cell_type": "markdown", - "id": "f38a8a7b-045b-444e-9e30-950c77d64b3f", + "id": "33", "metadata": {}, "source": [ "## Adding align\n", @@ -3519,23 +736,24 @@ }, { "cell_type": "code", - "execution_count": 18, - "id": "b60dd186", + "execution_count": null, + "id": "34", "metadata": {}, "outputs": [], "source": [ - "class CustomIndex(xr.Index): #customindex inherits xarray Index\n", - " def __init__(self, x_indexes, variables=None): \n", - " \n", + "class CustomIndex(xr.Index): # customindex inherits xarray Index\n", + " def __init__(self, x_indexes, variables=None):\n", + "\n", " self.indexes = variables\n", - " self._xindexes = x_indexes \n", + " self._xindexes = x_indexes\n", " if variables is not None:\n", "\n", " self.spatial_ref = variables['spatial_ref']\n", " else:\n", " self.spatial_ref = None\n", - " @classmethod \n", - " def from_variables(cls,variables, **kwargs):\n", + "\n", + " @classmethod\n", + " def from_variables(cls, variables, **kwargs):\n", " '''this method creates a CustomIndex obj from a variables object.\n", " variables is a dict created from ds1, keys are variable names,\n", " values are associated xr.variables. created like this:\n", @@ -3543,70 +761,67 @@ " coord_names is passed to set_xindex\n", " '''\n", " # this index class expects to work with datasets with certain properties\n", - " # must have exactly 2 variables: x and spatial_ref \n", + " # must have exactly 2 variables: x and spatial_ref\n", " assert len(variables) == 2\n", " assert 'x' in variables\n", - " assert 'spatial_ref' in variables \n", - " \n", + " assert 'spatial_ref' in variables\n", + "\n", " dim_variables = {}\n", " scalar_vars = {}\n", - " for k,i in variables.items():\n", - " if variables[k].ndim ==1:\n", + " for k, i in variables.items():\n", + " if variables[k].ndim == 1:\n", " dim_variables[k] = variables[k]\n", - " if variables[k].ndim ==0:\n", + " if variables[k].ndim == 0:\n", " scalar_vars[k] = variables[k]\n", - " \n", - " options = {'dim':'x',\n", - " 'name':'x'}\n", - " \n", + "\n", + " options = {'dim': 'x', 'name': 'x'}\n", + "\n", " x_indexes = {\n", - " k: PandasIndex.from_variables({k: v}, options = options) \n", - " for k,v in dim_variables.items()\n", + " k: PandasIndex.from_variables({k: v}, options=options) for k, v in dim_variables.items()\n", " }\n", - " \n", + "\n", " x_indexes['spatial_ref'] = variables['spatial_ref']\n", - " \n", + "\n", " return cls(x_indexes, variables)\n", - " \n", + "\n", " def create_variables(self, variables=None):\n", " '''creates coord variable from index'''\n", " if not variables:\n", " variables = self.joined_var\n", "\n", " idx_variables = {}\n", - " \n", "\n", " for index in self._xindexes.values():\n", - " #want to skip spatial ref\n", + " # want to skip spatial ref\n", " if type(index) == xr.core.variable.Variable:\n", " pass\n", " else:\n", "\n", " x = index.create_variables(variables)\n", " idx_variables.update(x)\n", - " \n", - " idx_variables['spatial_ref'] = variables['spatial_ref'] \n", + "\n", + " idx_variables['spatial_ref'] = variables['spatial_ref']\n", " return idx_variables\n", "\n", " def transform(self, value):\n", - " \n", - " #extract attrs\n", + "\n", + " # extract attrs\n", " fac = self.spatial_ref.attrs['factor']\n", " key = self.spatial_ref.attrs['idx_name']\n", "\n", - " #handle slice\n", + " # handle slice\n", " if isinstance(value, slice):\n", - " \n", + "\n", " start, stop, step = value.start, value.stop, value.step\n", - " new_start, new_stop, new_step = start / fac, stop/fac, step\n", + " new_start, new_stop, new_step = start / fac, stop / fac, step\n", " new_val = slice(new_start, new_stop, new_step)\n", " transformed_labels = {key: new_val}\n", " return transformed_labels\n", - " \n", - " #single or list of values\n", + "\n", + " # single or list of values\n", " else:\n", - " \n", - " vals_to_transform = [] \n", + "\n", + " vals_to_transform = []\n", "\n", " if not isinstance(value, Sequence):\n", " value = [value]\n", @@ -3616,32 +831,31 @@ " val = value[k]\n", " vals_to_transform.append(val)\n", "\n", - " #logic for parsing attrs, todo: switch to actual transform\n", + " # logic for parsing attrs, todo: switch to actual transform\n", " transformed_x = [int(v / fac) for v in vals_to_transform]\n", "\n", - " transformed_labels = {key:transformed_x}\n", + " transformed_labels = {key: transformed_x}\n", " return transformed_labels\n", "\n", " def sel(self, labels):\n", - " \n", + "\n", " assert type(labels) == dict\n", "\n", - " #user passes to sel\n", + " # user passes to sel\n", " label = next(iter(labels.values()))\n", "\n", - " #materialize coord array to idx off of\n", + " # materialize coord array to idx off of\n", " params = self.spatial_ref.attrs['range']\n", " full_arr = np.arange(params[0], params[1], params[2])\n", " toy_index = PandasIndex(full_arr, dim='x')\n", "\n", - " #transform user labesl to coord crs\n", + " # transform user labesl to coord crs\n", " idx = self.transform(label)\n", "\n", - " #sel on index created in .sel()\n", + " # sel on index created in .sel()\n", " matches = toy_index.sel(idx)\n", "\n", - " return matches \n", - " \n", + " return matches\n", "\n", " def equals(self, other):\n", " \"\"\"\n", @@ -3655,10 +869,12 @@ " -------\n", " bool\n", " True if the current instance is equal to the other instance, False otherwise.\n", - " \"\"\"\n", - " \n", - " result = self._xindexes['x'].equals(other._xindexes['x']) and self._xindexes['spatial_ref'].equals(other._xindexes['spatial_ref'])\n", - " \n", + " \"\"\"\n", + "\n", + " result = self._xindexes['x'].equals(other._xindexes['x']) and self._xindexes[\n", + " 'spatial_ref'\n", + " ].equals(other._xindexes['spatial_ref'])\n", + "\n", " return result\n", "\n", " def join(self, other, how='inner'):\n", @@ -3687,50 +903,50 @@ "\n", " The joined index is then converted back into a PandasIndex object and returned as a new PandasIndex object.\n", " \"\"\"\n", - " #make self index obj\n", + " # make self index obj\n", " params_self = self.spatial_ref.attrs['range']\n", " full_arr_self = np.arange(params_self[0], params_self[1], params_self[2])\n", " toy_index_self = PandasIndex(full_arr_self, dim='x')\n", - " \n", "\n", - " #make other index obj\n", + " # make other index obj\n", " other_start = other._xindexes['x'].index.array[0]\n", " other_stop = other._xindexes['x'].index.array[-1]\n", - " other_step = np.abs(int((other_start-other_stop) / (len(other._xindexes['x'].index.array)-1)))\n", - " \n", - " \n", + " other_step = np.abs(\n", + " int((other_start - other_stop) / (len(other._xindexes['x'].index.array) - 1))\n", + " )\n", + "\n", " params_other = other.spatial_ref.attrs['range']\n", - " full_arr_other = np.arange(other_start, other_stop, other_step) #prev elements of params_other\n", + " full_arr_other = np.arange(\n", + " other_start, other_stop, other_step\n", + " ) # prev elements of params_other\n", " toy_index_other = PandasIndex(full_arr_other, dim='x')\n", - " \n", + "\n", " self._indexes = {'x': toy_index_self}\n", - " other._indexes = {'x':toy_index_other}\n", - " \n", - " \n", - " new_indexes = {'x':toy_index_self.join(toy_index_other, how=how)}\n", - " \n", - " #need to return an index obj, but don't want to have to pass variables\n", + " other._indexes = {'x': toy_index_other}\n", + "\n", + " new_indexes = {'x': toy_index_self.join(toy_index_other, how=how)}\n", + "\n", + " # need to return an index obj, but don't want to have to pass variables\n", " # so need to add all of the things that index needs to new_indexes before passign it to return?\n", - " \n", - " #this will need to be generalized / tested more\n", - " new_indexes['spatial_ref'] = deepcopy(self.spatial_ref) \n", + "\n", + " # this will need to be generalized / tested more\n", + " new_indexes['spatial_ref'] = deepcopy(self.spatial_ref)\n", " start = int(new_indexes['x'].index.array[0])\n", " stop = int(new_indexes['x'].index.array[-1])\n", - " step = int((stop-start) / (len(new_indexes['x'].index.array) -1))\n", - " \n", + " step = int((stop - start) / (len(new_indexes['x'].index.array) - 1))\n", + "\n", " new_indexes['spatial_ref'].attrs['range'] = [start, stop, step]\n", - " \n", - " idx_var = xr.IndexVariable(dims=new_indexes['x'].index.name,\n", - " data = new_indexes['x'].index.array)\n", + "\n", + " idx_var = xr.IndexVariable(\n", + " dims=new_indexes['x'].index.name, data=new_indexes['x'].index.array\n", + " )\n", " attr_var = new_indexes['spatial_ref']\n", - " \n", - " idx_dict = {'x':idx_var, \n", - " 'spatial_ref':attr_var}\n", - " \n", + "\n", + " idx_dict = {'x': idx_var, 'spatial_ref': attr_var}\n", + "\n", " new_obj = type(self)(new_indexes)\n", " new_obj.joined_var = idx_dict\n", " return new_obj\n", - " \n", "\n", " def reindex_like(self, other, method=None, tolerance=None):\n", " \"\"\"\n", @@ -3760,39 +976,38 @@ " It uses the `method` and `tolerance` parameters to determine the reindexing behavior.\n", " The reindexed values are returned as a dictionary.\n", " \"\"\"\n", - " \n", + "\n", " params_self = self.spatial_ref.attrs['range']\n", " full_arr_self = np.arange(params_self[0], params_self[1], params_self[2])\n", " toy_index_self = PandasIndex(full_arr_self, dim='x')\n", - " \n", + "\n", " toy_index_other = other._xindexes['x']\n", - " \n", + "\n", " d = {'x': toy_index_self.index.get_indexer(other._xindexes['x'].index, method, tolerance)}\n", - " \n", - " return d\n", - " " + "\n", + " return d" ] }, { "cell_type": "code", - "execution_count": 19, - "id": "33592c6e", + "execution_count": null, + "id": "35", "metadata": {}, "outputs": [], "source": [ - "#create new sample data\n", - "sample_ds1 = create_sample_data(make_kwargs(2,[0,10,1],10))\n", - "sample_ds2 = create_sample_data(make_kwargs(5,[5,15,1],10))\n", + "# create new sample data\n", + "sample_ds1 = create_sample_data(make_kwargs(2, [0, 10, 1], 10))\n", + "sample_ds2 = create_sample_data(make_kwargs(5, [5, 15, 1], 10))\n", "\n", "\n", - "#create a copy used for testing later\n", + "# create a copy used for testing later\n", "orig_ds1 = sample_ds1.copy()\n", "orig_ds2 = sample_ds2.copy()" ] }, { "cell_type": "markdown", - "id": "1f45f21d-d7c9-4001-95cd-5a178977c00a", + "id": "36", "metadata": {}, "source": [ "*** reindex_like needs to return an object like variables to pass to create vars (?)" @@ -3800,8 +1015,8 @@ }, { "cell_type": "code", - "execution_count": 20, - "id": "fec339d7", + "execution_count": null, + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -3811,18 +1026,18 @@ }, { "cell_type": "code", - "execution_count": 21, - "id": "d1b68363", + "execution_count": null, + "id": "38", "metadata": {}, "outputs": [], "source": [ - "ds1 = sample_ds1.set_xindex(['x','spatial_ref'], CustomIndex)\n", - "ds2 = sample_ds2.set_xindex(['x','spatial_ref'], CustomIndex)\n" + "ds1 = sample_ds1.set_xindex(['x', 'spatial_ref'], CustomIndex)\n", + "ds2 = sample_ds2.set_xindex(['x', 'spatial_ref'], CustomIndex)" ] }, { "cell_type": "markdown", - "id": "a9780e70-26f8-48b5-917a-10521c2563ef", + "id": "39", "metadata": {}, "source": [ "## Align" @@ -3830,24 +1045,24 @@ }, { "cell_type": "code", - "execution_count": 22, - "id": "929056d2", + "execution_count": null, + "id": "40", "metadata": {}, "outputs": [], "source": [ - "#create sample data -- we define 2 for alignment\n", - "sample_ds1 = create_sample_data(make_kwargs(2,[0,10,1],10))\n", - "sample_ds2 = create_sample_data(make_kwargs(5,[8,18,1], 10))\n", + "# create sample data -- we define 2 for alignment\n", + "sample_ds1 = create_sample_data(make_kwargs(2, [0, 10, 1], 10))\n", + "sample_ds2 = create_sample_data(make_kwargs(5, [8, 18, 1], 10))\n", "\n", - "#create copies used for testing later\n", + "# create copies used for testing later\n", "orig_ds1 = sample_ds1.copy()\n", "orig_ds2 = sample_ds2.copy()" ] }, { "cell_type": "code", - "execution_count": 23, - "id": "18cecad6", + "execution_count": null, + "id": "41", "metadata": {}, "outputs": [], "source": [ @@ -3857,19 +1072,19 @@ }, { "cell_type": "code", - "execution_count": 24, - "id": "21f9bae9", + "execution_count": null, + "id": "42", "metadata": {}, "outputs": [], "source": [ - "ds1 = sample_ds1.set_xindex(['x','spatial_ref'], CustomIndex)\n", - "ds2 = sample_ds2.set_xindex(['x','spatial_ref'], CustomIndex)\n" + "ds1 = sample_ds1.set_xindex(['x', 'spatial_ref'], CustomIndex)\n", + "ds2 = sample_ds2.set_xindex(['x', 'spatial_ref'], CustomIndex)" ] }, { "cell_type": "code", - "execution_count": 25, - "id": "9cd9ba30-bcd8-4228-9ab5-d4bf983a173c", + "execution_count": null, + "id": "43", "metadata": {}, "outputs": [], "source": [ @@ -3878,8 +1093,8 @@ }, { "cell_type": "code", - "execution_count": 26, - "id": "123b9079-ed3f-47ec-96f5-6eb07cec2f06", + "execution_count": null, + "id": "44", "metadata": {}, "outputs": [], "source": [ @@ -3888,835 +1103,42 @@ }, { "cell_type": "code", - "execution_count": 27, - "id": "81649250-0f02-4194-ac8b-347badf1a42c", + "execution_count": null, + "id": "45", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset> Size: 280B\n",
-       "Dimensions:      (x: 17)\n",
-       "Coordinates:\n",
-       "  * x            (x) int64 136B 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16\n",
-       "  * spatial_ref  float64 8B nan\n",
-       "Data variables:\n",
-       "    var1         (x) float64 136B 0.06861 0.6461 0.5169 0.1095 ... nan nan nan\n",
-       "Indexes:\n",
-       "    x            CustomIndex\n",
-       "    spatial_ref  CustomIndex
" - ], - "text/plain": [ - " Size: 280B\n", - "Dimensions: (x: 17)\n", - "Coordinates:\n", - " * x (x) int64 136B 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16\n", - " * spatial_ref float64 8B nan\n", - "Data variables:\n", - " var1 (x) float64 136B 0.06861 0.6461 0.5169 0.1095 ... nan nan nan\n", - "Indexes:\n", - " x CustomIndex\n", - " spatial_ref CustomIndex" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "outer_align" ] }, { "cell_type": "code", - "execution_count": 28, - "id": "ef183d67-3715-442a-8ac6-18327352e5f5", + "execution_count": null, + "id": "46", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset> Size: 40B\n",
-       "Dimensions:      (x: 2)\n",
-       "Coordinates:\n",
-       "  * x            (x) int64 16B 8 9\n",
-       "  * spatial_ref  float64 8B nan\n",
-       "Data variables:\n",
-       "    var1         (x) float64 16B 0.1917 0.01797\n",
-       "Indexes:\n",
-       "    x            CustomIndex\n",
-       "    spatial_ref  CustomIndex
" - ], - "text/plain": [ - " Size: 40B\n", - "Dimensions: (x: 2)\n", - "Coordinates:\n", - " * x (x) int64 16B 8 9\n", - " * spatial_ref float64 8B nan\n", - "Data variables:\n", - " var1 (x) float64 16B 0.1917 0.01797\n", - "Indexes:\n", - " x CustomIndex\n", - " spatial_ref CustomIndex" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "inner_align" ] }, { "cell_type": "code", - "execution_count": 29, - "id": "d539bffe-af6f-4d62-afcc-2ba35a7b3da2", + "execution_count": null, + "id": "47", "metadata": {}, "outputs": [], "source": [ "# reindex_like not implemented for PandasIndx\n", "# but that defaults to inner, and these are successsfuly producing left and right so shouldn't be it\n", - "#left_align,_ = xr.align(ds1, ds2, join='left')\n", - "#right_align,_ = xr.align(ds1, ds2, join='right')\n", + "# left_align,_ = xr.align(ds1, ds2, join='left')\n", + "# right_align,_ = xr.align(ds1, ds2, join='right')\n", "\n", - "#don't remember what above was about , is reindex like not implemented for left, right joins something like that ?" + "# don't remember what above was about , is reindex like not implemented for left, right joins something like that ?" ] }, { "cell_type": "markdown", - "id": "27dc5208-44a0-415e-ab55-8082e5af5276", + "id": "48", "metadata": {}, "source": [ "## Wrap up / summary\n", @@ -4726,18 +1148,13 @@ { "cell_type": "code", "execution_count": null, - "id": "e4d62979-c452-4cdd-836d-0781cd9cd475", + "id": "49", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { - "kernelspec": { - "display_name": "Python [conda env:miniconda-arraylake]", - "language": "python", - "name": "conda-env-miniconda-arraylake-py" - }, "language_info": { "codemirror_mode": { "name": "ipython", @@ -4747,8 +1164,7 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.8" + "pygments_lexer": "ipython3" } }, "nbformat": 4,