diff --git a/docs/user-guide/area_calc.ipynb b/docs/user-guide/area_calc.ipynb index d3925cb9c..49e4d1ace 100644 --- a/docs/user-guide/area_calc.ipynb +++ b/docs/user-guide/area_calc.ipynb @@ -2,24 +2,15 @@ "cells": [ { "cell_type": "markdown", - "source": [ - "# Face Area Calculations" - ], "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ - "import uxarray as ux\n", - "import numpy as np" - ], - "metadata": { - "collapsed": false - } + "# Face Area Calculations" + ] }, { "cell_type": "markdown", @@ -34,6 +25,21 @@ "5. Calculate Area from Multiple Faces in Spherical Coordinates" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "import uxarray as ux\n", + "import numpy as np" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -45,7 +51,10 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -68,7 +77,10 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -129,7 +141,10 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -199,7 +214,10 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -221,7 +239,10 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -239,7 +260,10 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -266,7 +290,10 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -317,7 +344,10 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -330,7 +360,10 @@ "cell_type": "code", "execution_count": null, "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -355,7 +388,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/user-guide/data-structures.ipynb b/docs/user-guide/data-structures.ipynb index 94ab758a8..adb4e0447 100644 --- a/docs/user-guide/data-structures.ipynb +++ b/docs/user-guide/data-structures.ipynb @@ -12,6 +12,22 @@ "# Data Structures\n" ] }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "The core functionality of UXarray revolves around three data structures, which are used for interacting with unstructured grids and the data variables that reside on them.\n", + "\n", + "1. **[`uxarray.Grid`](https://uxarray.readthedocs.io/en/latest/user_api/generated/uxarray.UxDataArray.html)**: Stores the grid representation (i.e. coordinates, connectivity information, etc.)\n", + "2. **[`uxarray.UxDataset`](https://uxarray.readthedocs.io/en/latest/user_api/generated/uxarray.UxDataset.html)**: One or more data variable that resided on a grid.\n", + "3. **[`uxarray.UxDataArray`](https://uxarray.readthedocs.io/en/latest/user_api/generated/uxarray.UxDataArray.html)**: A single data variable that resides on a grid \n" + ] + }, { "cell_type": "code", "execution_count": 1, @@ -36,22 +52,6 @@ "import xarray as xr" ] }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "The core functionality of UXarray revolves around three data structures, which are used for interacting with unstructured grids and the data variables that reside on them.\n", - "\n", - "1. **[`uxarray.Grid`](https://uxarray.readthedocs.io/en/latest/user_api/generated/uxarray.UxDataArray.html)**: Stores the grid representation (i.e. coordinates, connectivity information, etc.)\n", - "2. **[`uxarray.UxDataset`](https://uxarray.readthedocs.io/en/latest/user_api/generated/uxarray.UxDataset.html)**: One or more data variable that resided on a grid.\n", - "3. **[`uxarray.UxDataArray`](https://uxarray.readthedocs.io/en/latest/user_api/generated/uxarray.UxDataArray.html)**: A single data variable that resides on a grid \n" - ] - }, { "cell_type": "markdown", "metadata": { @@ -5419,7 +5419,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.5" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/user-guide/remapping.ipynb b/docs/user-guide/remapping.ipynb new file mode 100644 index 000000000..76e16a802 --- /dev/null +++ b/docs/user-guide/remapping.ipynb @@ -0,0 +1,620 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d9d3f5a8-6d3c-4a7e-9150-a2915f3e0ceb", + "metadata": {}, + "source": [ + "# Remapping" + ] + }, + { + "cell_type": "markdown", + "id": "1b0c0018-3c30-4909-9057-3fc59abe96fd", + "metadata": {}, + "source": [ + "Remapping, or commonly referred to as Regridding, is the process of taking data that resides on one grid and mapping it to another. Details on various remapping methods can be found [here](https://climatedataguide.ucar.edu/climate-tools/regridding-overview). This user guide section will cover the two native remapping methods that are supported by UXarray:\n", + "\n", + "* Nearest Neighbor\n", + "* Inverse Distance Weighted" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7449507f-3d79-4e86-a775-3b9137153adc", + "metadata": {}, + "outputs": [], + "source": [ + "import uxarray as ux\n", + "import geoviews.feature as gf\n", + "import cartopy.crs as ccrs\n", + "import holoviews as hv\n", + "import os\n", + "import urllib.request\n", + "from pathlib import Path\n", + "import warnings\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "hv.extension(\"bokeh\")\n", + "\n", + "features = gf.coastline(projection=ccrs.PlateCarree(), scale=\"50m\")\n", + "\n", + "plot_kwargs = {\n", + " \"backend\": \"matplotlib\",\n", + " \"aspect\": 2,\n", + " \"fig_size\": 400,\n", + " \"pixel_ratio\": 5.0,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "b0a6ff3e-df92-4118-80dc-829f38083630", + "metadata": {}, + "source": [ + "### Simple Remapping Example" + ] + }, + { + "cell_type": "markdown", + "id": "1711d9d7-7ba7-4d62-bcd4-6592d6d465aa", + "metadata": {}, + "source": [ + "Provided below is an example of remapping using a simple grid. A has data, B does not. We will remap to B from A." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a2dddad7-872a-4b4f-a2b0-7de698ef6a38", + "metadata": {}, + "outputs": [], + "source": [ + "grid_path = \"../../test/meshfiles/ugrid/quad-hexagon/grid.nc\"\n", + "data_path = \"../../test/meshfiles/ugrid/quad-hexagon/data.nc\"\n", + "destination_grid = \"../../test/meshfiles/ugrid/quad-hexagon/triangulated-grid.nc\"\n", + "grid = ux.open_grid(destination_grid)\n", + "uxds = ux.open_dataset(grid_path, data_path)\n", + "(\n", + " uxds[\"t2m\"].plot(fig_size=150, colorbar=False, cmap=\"inferno\", backend=\"matplotlib\")\n", + " + grid.plot(fig_size=150, colorbar=False, cmap=\"inferno\", backend=\"matplotlib\")\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c5ea957-be2e-4d43-9141-d506c46a19f8", + "metadata": {}, + "outputs": [], + "source": [ + "remapped_grid = uxds[\"t2m\"].remap.inverse_distance_weighted(\n", + " grid, k=3, remap_to=\"face centers\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9c187cd8-e689-4f1a-942e-2055051f8265", + "metadata": {}, + "outputs": [], + "source": [ + "remapped_grid.plot(fig_size=150, colorbar=False, cmap=\"inferno\", backend=\"matplotlib\")" + ] + }, + { + "cell_type": "markdown", + "id": "927d259c-6b84-4f0a-ab25-d38c76b4626a", + "metadata": {}, + "source": [ + "### Data" + ] + }, + { + "cell_type": "markdown", + "id": "9ee52811-dbe5-4f32-be86-151e94737b75", + "metadata": {}, + "source": [ + "In this notebook, we are using two datasets with different resolutions (480km and 120km) from the MPAS Ocean Model. We will be remapping the bottomDepth variable, which measures the ocean depth." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d73a380-349d-473d-8e57-10c52102adca", + "metadata": {}, + "outputs": [], + "source": [ + "data_var = \"bottomDepth\"\n", + "\n", + "grid_filename_480 = \"oQU480.grid.nc\"\n", + "data_filename_480 = \"oQU480.data.nc\"\n", + "\n", + "grid_filename_120 = \"oQU120.grid.nc\"\n", + "data_filename_120 = \"oQU120.data.nc\"\n", + "\n", + "filenames = [grid_filename_480, data_filename_480, grid_filename_120, data_filename_120]\n", + "\n", + "for filename in filenames:\n", + " if not os.path.isfile(filename):\n", + " # downloads the files from Cookbook repo, if they haven't been downloaded locally yet\n", + " url = f\"https://github.com/ProjectPythia/unstructured-grid-viz-cookbook/raw/main/meshfiles/{filename}\"\n", + " _, headers = urllib.request.urlretrieve(url, filename=filename)\n", + "\n", + "\n", + "file_path_dict = {\n", + " \"480km\": [grid_filename_480, data_filename_480],\n", + " \"120km\": [grid_filename_120, data_filename_120],\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe358933-bc7c-422c-88af-a697ffb4e45a", + "metadata": {}, + "outputs": [], + "source": [ + "uxds_480 = ux.open_dataset(*file_path_dict[\"480km\"])\n", + "uxds_120 = ux.open_dataset(*file_path_dict[\"120km\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da0b1ff8-da1a-4c6c-9031-749b34bfad7a", + "metadata": {}, + "outputs": [], + "source": [ + "(\n", + " uxds_480[\"bottomDepth\"].plot(title=\"Bottom Depth (480km)\", backend=\"matplotlib\")\n", + " * features\n", + " + uxds_120[\"bottomDepth\"].plot(title=\"Bottom Depth (120km)\", backend=\"matplotlib\")\n", + " * features\n", + ").opts(fig_size=300).cols(1)" + ] + }, + { + "cell_type": "markdown", + "id": "0d2345bc-ce03-48b5-b08c-6e9c679f3bc1", + "metadata": {}, + "source": [ + "We can view the supported remapping methods by accessing the `.remap` attribute that is part of a `UxDataArray`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2453895d-b41d-47fe-bc2b-42358a9acbe5", + "metadata": {}, + "outputs": [], + "source": [ + "uxds_120[\"bottomDepth\"].remap" + ] + }, + { + "cell_type": "markdown", + "id": "0e7f1fc3-090f-4aa4-86b3-de240dc68909", + "metadata": {}, + "source": [ + "### Nearest Neighbor" + ] + }, + { + "cell_type": "markdown", + "id": "969b42fd-bcd8-4781-b1b3-5dfddc42153f", + "metadata": {}, + "source": [ + "Nearest Neighbor Remapping is a point-based method that uses the nearest neighbor from the source grid when assigning data to the destination grid. It is a distance-based approach that uses `kd_tree` or `ball_tree` to determine the distance between points. We can use the `UxDataArray.remap.nearest_neighbor()` method, which takes in the following parameters:" + ] + }, + { + "cell_type": "markdown", + "id": "7a942103-7188-4352-b203-fa58e5cc6fe9", + "metadata": {}, + "source": [ + "* `destination_grid` is the grid object that is being remapped to.\n", + "* `destination_obj` is being deprecated and soon will no longer be used, it allows remapping to data arrays, grids, and datasets.\n", + "* `remap_to` specifies the location of the remapping, either to `nodes`, `face centers`, or `edge centers`.\n", + "* `coord_type` refers to what coordinate system to use, either `spherical` or `cartesian`." + ] + }, + { + "cell_type": "markdown", + "id": "4ac66105-7d40-49f1-abe6-1b65038cb02f", + "metadata": {}, + "source": [ + "#### Upsampling" + ] + }, + { + "cell_type": "markdown", + "id": "e58c5079-d714-4b4a-bb8f-a3c4d4e26fce", + "metadata": {}, + "source": [ + "We can remap from the 480km grid to the 120km one, which would perform an upsampling operation. Our `destination_obj` will be our 120km mesh, and we will make sure to specify to do the remap to `face centers`. View the plots below to see a comparison of the original 120km mesh compared to the remapped one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4550735-053a-4542-b259-fb7d8c2e6fae", + "metadata": {}, + "outputs": [], + "source": [ + "upsampling = uxds_480[\"bottomDepth\"].remap.nearest_neighbor(\n", + " uxds_120.uxgrid, remap_to=\"face centers\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf2cf918-62f8-4aa4-9fa1-122bc06862ce", + "metadata": {}, + "outputs": [], + "source": [ + "(\n", + " uxds_480[\"bottomDepth\"].plot(title=\"Bottom Depth (480km)\", **plot_kwargs) * features\n", + " + upsampling.plot(title=\"Remapped Bottom Depth (480km to 120km)\", **plot_kwargs)\n", + " * features\n", + " + uxds_480[\"bottomDepth\"].plot(\n", + " title=\"Zoomed (480km)\", **plot_kwargs, xlim=(-10, 10), ylim=(-5, 5)\n", + " )\n", + " * features\n", + " + upsampling.plot(\n", + " title=\"Zoomed Remap (480km to 120km)\",\n", + " **plot_kwargs,\n", + " xlim=(-10, 10),\n", + " ylim=(-5, 5),\n", + " )\n", + " * features\n", + ").opts(fig_size=300).cols(1)" + ] + }, + { + "cell_type": "markdown", + "id": "7902f3df-eeb4-4b7d-9c77-0ec50a30ec6b", + "metadata": {}, + "source": [ + "As we can see, the nearest neighbor remap has successfully upsampled, going from a lower resolution to a higher resolution. Of course, since all the data is coming from the lower-resolution grid, it should be noted that no new information is being added, we are simply remapping data from a lower-resolution grid to a higher one. This can be observed in the result since multiple data points from the source grid are repeated in the destination grid." + ] + }, + { + "cell_type": "markdown", + "id": "48f45a45-d369-48cc-a6d8-31c2cedc4ef8", + "metadata": {}, + "source": [ + "#### Downsampling" + ] + }, + { + "cell_type": "markdown", + "id": "52e2b97c-b8f5-42c6-a000-bc929b77bbc4", + "metadata": {}, + "source": [ + "Now we can do the reverse, which is downsampling. We we go from a higher resolution (120km) to a lower one (480km). Once again comparision plots are found below, with a zoomed in version provided." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40094ba0-0dad-48d7-af70-040f088d7be5", + "metadata": {}, + "outputs": [], + "source": [ + "downsampling = uxds_120[\"bottomDepth\"].remap.nearest_neighbor(\n", + " uxds_480.uxgrid, remap_to=\"face centers\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65d3f7db-9820-4c46-8d78-571a8ea48ef1", + "metadata": {}, + "outputs": [], + "source": [ + "(\n", + " uxds_120[\"bottomDepth\"].plot(title=\"Bottom Depth (120km)\", **plot_kwargs) * features\n", + " + downsampling.plot(title=\"Remapped Bottom Depth (120km to 480km)\", **plot_kwargs)\n", + " * features\n", + " + uxds_120[\"bottomDepth\"].plot(\n", + " title=\"Zoomed (120km)\", **plot_kwargs, xlim=(-10, 10), ylim=(-5, 5)\n", + " )\n", + " * features\n", + " + downsampling.plot(\n", + " title=\"Zoomed Remap (120km to 480km)\",\n", + " **plot_kwargs,\n", + " xlim=(-10, 10),\n", + " ylim=(-5, 5),\n", + " )\n", + " * features\n", + ").opts(fig_size=300).cols(1)" + ] + }, + { + "cell_type": "markdown", + "id": "63d2a6cd-6b9a-4ee8-8716-b2059b8a008a", + "metadata": {}, + "source": [ + "As you can see, the two datasets here don't look as similar as with the upsampling one. This is an important note, when you are downsampling, there will always be a loss of information. This is because there are many more data points in the source grid than in the destination grid." + ] + }, + { + "cell_type": "markdown", + "id": "549c51a5-861d-40ff-af04-99a9118173dc", + "metadata": {}, + "source": [ + "### Inverse Distance Weighted" + ] + }, + { + "cell_type": "markdown", + "id": "33318f23-276d-46ed-aa47-3e7ffaf466fd", + "metadata": {}, + "source": [ + "Inverse distance weighted remapping assigns a value based on the weighted average of a specified number of nearby points. This gives a more smooth remapping than the nearest neighbor and helps decrease the chance of outliers. Unlike the nearest neighbor remapping, it constructs new values based on the values around it. " + ] + }, + { + "cell_type": "markdown", + "id": "484806dd-e0f3-46cd-93fd-2c944b702eac", + "metadata": {}, + "source": [ + "For inverse distance weighted remapping the parameters are the same as nearest neighbor with the addition of two extra parameters we can use to change the remapping parameters.\n", + "\n", + "* `power` controls how local or global the remapping is, the larger this value the less influence points that are further away have.\n", + "* `k` is the number of neighbors to use in the weighted average." + ] + }, + { + "cell_type": "markdown", + "id": "e1ed1194-5fd4-4d44-afa2-32a6064de5ae", + "metadata": {}, + "source": [ + "Using the same examples as before, we can see the differences between nearest neighbor and inverse distance weighted remapping." + ] + }, + { + "cell_type": "markdown", + "id": "c381355d-503c-4b3e-be8c-386e90868f58", + "metadata": {}, + "source": [ + "#### Upsampling" + ] + }, + { + "cell_type": "markdown", + "id": "33238daf-202c-4d84-a5c5-06e5a0b3291a", + "metadata": {}, + "source": [ + "We will upsample from the 480km grid to 120km grid again. This time we will see that the results are a lot smoother, and that the 120km looks a lot more like it's original dataset compared to when the nearest neighbor remap was used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "400398d5-5cc0-4790-9a95-cb0f88cc1ca8", + "metadata": {}, + "outputs": [], + "source": [ + "upsampling_idw = uxds_480[\"bottomDepth\"].remap.inverse_distance_weighted(\n", + " uxds_120.uxgrid, remap_to=\"face centers\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8dd1d8e3-54e2-4710-b132-f69f0ba950fd", + "metadata": {}, + "outputs": [], + "source": [ + "(\n", + " uxds_480[\"bottomDepth\"].plot(title=\"Bottom Depth (480km)\", **plot_kwargs) * features\n", + " + upsampling_idw.plot(title=\"Remapped Bottom Depth (480km to 120km)\", **plot_kwargs)\n", + " * features\n", + " + uxds_480[\"bottomDepth\"].plot(\n", + " title=\"Zoomed (480km)\", **plot_kwargs, xlim=(-10, 10), ylim=(-5, 5)\n", + " )\n", + " * features\n", + " + upsampling_idw.plot(\n", + " title=\"Zoomed Remap (480km to 120km)\",\n", + " **plot_kwargs,\n", + " xlim=(-10, 10),\n", + " ylim=(-5, 5),\n", + " )\n", + " * features\n", + ").opts(fig_size=300).cols(1)" + ] + }, + { + "cell_type": "markdown", + "id": "c0b125ed-04b8-40c5-86df-b6597bc3ef27", + "metadata": {}, + "source": [ + "#### Downsampling" + ] + }, + { + "cell_type": "markdown", + "id": "4fc05ed6-5a45-41f1-818f-27829022b6ec", + "metadata": {}, + "source": [ + "Here we can see an example of downsampling with inverse distance weighted. It has less of a noticable difference compared to the nearest neighbor, however this may be due to the `k` number and the default `power` value used in the calculation. Depending on your graph and the scale difference, you may need to use different `k` and `power` values to achieve a smoother result. That will be discussed in the next secition." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7d21c42d-d368-4a34-8dcb-6643f0a0a7d1", + "metadata": {}, + "outputs": [], + "source": [ + "downsampling_idw = uxds_120[\"bottomDepth\"].remap.inverse_distance_weighted(\n", + " uxds_480.uxgrid, remap_to=\"face centers\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "935a7a74-7d88-49ae-bc20-7b18d76795c9", + "metadata": {}, + "outputs": [], + "source": [ + "(\n", + " uxds_120[\"bottomDepth\"].plot(title=\"Bottom Depth (120km)\", **plot_kwargs) * features\n", + " + downsampling_idw.plot(\n", + " title=\"Remapped Bottom Depth (120km to 480km)\", **plot_kwargs\n", + " )\n", + " * features\n", + " + uxds_120[\"bottomDepth\"].plot(\n", + " title=\"Zoomed (120km)\", **plot_kwargs, xlim=(-10, 10), ylim=(-5, 5)\n", + " )\n", + " * features\n", + " + downsampling_idw.plot(\n", + " title=\"Zoomed Remap (120km to 480km)\",\n", + " **plot_kwargs,\n", + " xlim=(-10, 10),\n", + " ylim=(-5, 5),\n", + " )\n", + " * features\n", + ").opts(fig_size=300).cols(1)" + ] + }, + { + "cell_type": "markdown", + "id": "1de0ccc5-c761-4fcb-9396-7e91d2a8ffa5", + "metadata": {}, + "source": [ + "#### `k` and `power` Parameter Comparisons" + ] + }, + { + "cell_type": "markdown", + "id": "1befced5-3bf7-4be8-b6fc-9cfd4eabb84d", + "metadata": {}, + "source": [ + "The higher the `k` value, the more neighbors used in the weights calculation. However, along simply changing the `k` value may not have a large impact on the remapping, and you might also need to change the `power` value, because the `power` value changes how much influence a point has as it gets further away from the source point. Let's use do two remaps, a high and low `power` / `k` value and see a side by side comparsion with downsampling." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e31c8ec-75a0-4898-96e7-35a4b4853ad0", + "metadata": {}, + "outputs": [], + "source": [ + "downsampling_idw_low = uxds_120[\"bottomDepth\"].remap.inverse_distance_weighted(\n", + " uxds_480.uxgrid, remap_to=\"face centers\", power=1, k=2\n", + ")\n", + "downsampling_idw_high = uxds_120[\"bottomDepth\"].remap.inverse_distance_weighted(\n", + " uxds_480.uxgrid, remap_to=\"face centers\", power=5, k=128\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88756342-64e0-42f4-96d9-b9822e002bd7", + "metadata": {}, + "outputs": [], + "source": [ + "(\n", + " downsampling_idw_low.plot(\n", + " title=\"Zoomed 480km (power=1, k=2)\", **plot_kwargs, xlim=(-10, 10), ylim=(-5, 5)\n", + " )\n", + " * features\n", + " + downsampling_idw_high.plot(\n", + " title=\"Zoomed 480km (power=5, k=128)\",\n", + " **plot_kwargs,\n", + " xlim=(-10, 10),\n", + " ylim=(-5, 5),\n", + " )\n", + " * features\n", + ").opts(fig_size=300).cols(1)" + ] + }, + { + "cell_type": "markdown", + "id": "5c476348-312e-4c91-91d0-3b13c7b3c972", + "metadata": {}, + "source": [ + "Now we can do the same thing with an upsampling example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "439ac480-c4f3-4080-a18c-8f670367c194", + "metadata": {}, + "outputs": [], + "source": [ + "upsampling_idw_low = uxds_480[\"bottomDepth\"].remap.inverse_distance_weighted(\n", + " uxds_120.uxgrid, remap_to=\"face centers\", power=1, k=2\n", + ")\n", + "upsampling_idw_high = uxds_480[\"bottomDepth\"].remap.inverse_distance_weighted(\n", + " uxds_120.uxgrid, remap_to=\"face centers\", power=5, k=128\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0dc0497d-13bd-4b5b-9792-ad6ce26aded5", + "metadata": {}, + "outputs": [], + "source": [ + "(\n", + " upsampling_idw_low.plot(\n", + " title=\"Zoomed 120km (power=1, k=2)\", **plot_kwargs, xlim=(-10, 10), ylim=(-5, 5)\n", + " )\n", + " * features\n", + " + upsampling_idw_high.plot(\n", + " title=\"Zoomed 120km (power=5, k=128)\",\n", + " **plot_kwargs,\n", + " xlim=(-10, 10),\n", + " ylim=(-5, 5),\n", + " )\n", + " * features\n", + ").opts(fig_size=300).cols(1)" + ] + }, + { + "cell_type": "markdown", + "id": "6b3b52bb-4d21-48a1-ae65-91387f97636b", + "metadata": {}, + "source": [ + "When we alter the `k` and `power` values during downsampling, we observe only minor effects. This is somewhat expected because downsampling involves transitioning from a grid with numerous faces to one where several faces are consolidated into one. Consequently, regardless of how many neighboring faces we consider, the impact remains limited, especially given the weighted calculation. \n", + "\n", + "Conversely, during upsampling, the effects are much more pronounced. This is logical, considering that upsampling involves transitioning from a grid with fewer faces, where each face represents a larger area, to one where more faces are present, representing a smaller area per face. Therefore, adjusting from 2 neighbors to 128 leads to substantial changes because the additional faces encompase a much larger area, and by effect much more drastic changes in values." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/user-guide/topological-aggregations.ipynb b/docs/user-guide/topological-aggregations.ipynb index c19a9a3d8..f2149cd80 100644 --- a/docs/user-guide/topological-aggregations.ipynb +++ b/docs/user-guide/topological-aggregations.ipynb @@ -4,39 +4,51 @@ "cell_type": "markdown", "id": "8c7ebaa6a858cbbb", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "# Topological Aggregations" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "93d83a02a42e21c1", + "cell_type": "markdown", + "id": "5121ff038b3e683e", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, - "outputs": [], "source": [ - "import uxarray as ux" + "Data variables are typically mapped to either the nodes, edges, or faces of an unstructured grid. The data on each of these elements can be manipulated and aggregated to perform various operations, such as mean, min, max and many others. This section will introduce the concept of Topological Aggregations and how to perform them using UXarray.\n" ] }, { - "cell_type": "markdown", - "id": "5121ff038b3e683e", + "cell_type": "code", + "execution_count": null, + "id": "93d83a02a42e21c1", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, + "outputs": [], "source": [ - "Data variables are typically mapped to either the nodes, edges, or faces of an unstructured grid. The data on each of these elements can be manipulated and aggregated to perform various operations, such as mean, min, max and many others. This section will introduce the concept of Topological Aggregations and how to perform them using UXarray.\n" + "import uxarray as ux" ] }, { "cell_type": "markdown", "id": "775f787cfb55ef91", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "## What are Aggregations?\n", @@ -53,7 +65,10 @@ "cell_type": "markdown", "id": "99c4450a10bfa9e9", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "## What are Topological Aggregations? \n", @@ -91,7 +106,10 @@ "cell_type": "markdown", "id": "db3ce31e96ce3719", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "## Data\n", @@ -108,7 +126,10 @@ "execution_count": null, "id": "a0c9a0f19efd0633", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -129,7 +150,10 @@ "cell_type": "markdown", "id": "ce3bcc602c52502c", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "We can visualize the data on each element by using different markers:" @@ -140,7 +164,10 @@ "execution_count": null, "id": "4439706a33f61576", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -164,7 +191,10 @@ "cell_type": "markdown", "id": "584207e271b49e06", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "## Node Aggregations\n", @@ -176,7 +206,10 @@ "cell_type": "markdown", "id": "62136ed1b4e4d52e", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "### Node to Face\n", @@ -191,7 +224,10 @@ "execution_count": null, "id": "6cdf74072b2c2b43", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -203,7 +239,10 @@ "execution_count": null, "id": "fdea0dda96ebe09d", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -222,7 +261,10 @@ "cell_type": "markdown", "id": "8968f43278f270ec", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [] }, @@ -230,7 +272,10 @@ "cell_type": "markdown", "id": "e8f959f893e838ff", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "One use case for aggregating node-centered data to each face is that it allows for the result to be plotted as Polygons." @@ -241,7 +286,10 @@ "execution_count": null, "id": "e7c7780435202338", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -258,7 +306,10 @@ "cell_type": "markdown", "id": "71b10f1a1f0a0939", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "### Node to Edge\n", @@ -272,7 +323,10 @@ "cell_type": "markdown", "id": "19b624f42cbdc16b", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "For a node-centered data variable, we can set `destination=\"edge\"` to specify that the aggregation should be performed on the nodes that saddle each edge, with the result stored on each edge." @@ -283,7 +337,10 @@ "execution_count": null, "id": "db5c4cce296023a", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -295,7 +352,10 @@ "execution_count": null, "id": "4a3c7b486e5d78d", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "outputs": [], "source": [ @@ -314,7 +374,10 @@ "cell_type": "markdown", "id": "448ac6705a18f85b", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "## Edge Aggregations\n", @@ -331,7 +394,10 @@ "cell_type": "markdown", "id": "357fe2f645bf3d4e", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "### Edge to Node\n", @@ -345,7 +411,10 @@ "cell_type": "markdown", "id": "86846522863860f5", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "### Edge to Face\n", @@ -359,7 +428,10 @@ "cell_type": "markdown", "id": "7dd482e719e7d775", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "## Face Aggregations\n", @@ -375,7 +447,10 @@ "cell_type": "markdown", "id": "29ebe5d21bbcc46b", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "### Face to Node\n", @@ -389,7 +464,10 @@ "cell_type": "markdown", "id": "1609e8bef449a334", "metadata": { - "collapsed": false + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, "source": [ "### Face to Edge\n", @@ -402,21 +480,21 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/userguide.rst b/docs/userguide.rst index 78c2df9a6..1e204f384 100644 --- a/docs/userguide.rst +++ b/docs/userguide.rst @@ -17,3 +17,4 @@ common tasks that you can accomplish with UXarray. user-guide/data-structures.ipynb user-guide/topological-aggregations.ipynb user-guide/area_calc.ipynb + user-guide/remapping.ipynb diff --git a/test/meshfiles/ugrid/quad-hexagon/triangulated-grid.nc b/test/meshfiles/ugrid/quad-hexagon/triangulated-grid.nc new file mode 100644 index 000000000..be609d8f2 Binary files /dev/null and b/test/meshfiles/ugrid/quad-hexagon/triangulated-grid.nc differ diff --git a/uxarray/remap/dataarray_accessor.py b/uxarray/remap/dataarray_accessor.py index 31784a0a9..4a5f21dfe 100644 --- a/uxarray/remap/dataarray_accessor.py +++ b/uxarray/remap/dataarray_accessor.py @@ -33,7 +33,7 @@ def nearest_neighbor( self, destination_grid: Optional[Grid] = None, destination_obj: Optional[Grid, UxDataArray, UxDataset] = None, - remap_to: str = "nodes", + remap_to: str = "face centers", coord_type: str = "spherical", ): """Nearest Neighbor Remapping between a source (``UxDataArray``) and @@ -75,7 +75,7 @@ def inverse_distance_weighted( self, destination_grid: Optional[Grid] = None, destination_obj: Optional[Grid, UxDataArray, UxDataset] = None, - remap_to: str = "nodes", + remap_to: str = "face centers", coord_type: str = "spherical", power=2, k=8, @@ -109,7 +109,7 @@ def inverse_distance_weighted( if destination_grid is not None: return _inverse_distance_weighted_remap_uxda( - self.uxda, destination_grid, remap_to, coord_type + self.uxda, destination_grid, remap_to, coord_type, power, k ) elif destination_obj is not None: warn( @@ -117,5 +117,5 @@ def inverse_distance_weighted( DeprecationWarning, ) return _inverse_distance_weighted_remap_uxda( - self.uxda, destination_obj, remap_to, coord_type + self.uxda, destination_obj, remap_to, coord_type, power, k ) diff --git a/uxarray/remap/dataset_accessor.py b/uxarray/remap/dataset_accessor.py index f41eeebbf..d5a9edf3f 100644 --- a/uxarray/remap/dataset_accessor.py +++ b/uxarray/remap/dataset_accessor.py @@ -33,7 +33,7 @@ def nearest_neighbor( self, destination_grid: Optional[Grid] = None, destination_obj: Optional[Grid, UxDataArray, UxDataset] = None, - remap_to: str = "nodes", + remap_to: str = "face centers", coord_type: str = "spherical", ): """Nearest Neighbor Remapping between a source (``UxDataset``) and @@ -76,7 +76,7 @@ def inverse_distance_weighted( self, destination_grid: Optional[Grid] = None, destination_obj: Optional[Grid, UxDataArray, UxDataset] = None, - remap_to: str = "nodes", + remap_to: str = "face centers", coord_type: str = "spherical", power=2, k=8, @@ -111,7 +111,7 @@ def inverse_distance_weighted( if destination_grid is not None: return _inverse_distance_weighted_remap_uxds( - self.uxds, destination_grid, remap_to, coord_type + self.uxds, destination_grid, remap_to, coord_type, power, k ) elif destination_obj is not None: warn( @@ -119,5 +119,5 @@ def inverse_distance_weighted( DeprecationWarning, ) return _inverse_distance_weighted_remap_uxds( - self.uxds, destination_obj, remap_to, coord_type + self.uxds, destination_obj, remap_to, coord_type, power, k ) diff --git a/uxarray/remap/inverse_distance_weighted.py b/uxarray/remap/inverse_distance_weighted.py index fad3b4fd2..d70fc9631 100644 --- a/uxarray/remap/inverse_distance_weighted.py +++ b/uxarray/remap/inverse_distance_weighted.py @@ -17,7 +17,7 @@ def _inverse_distance_weighted_remap( source_grid, destination_grid, source_data, - remap_to="nodes", + remap_to="face centers", coord_type="spherical", power=2, k=8, @@ -129,7 +129,11 @@ def _inverse_distance_weighted_remap( f"but received: {remap_to}" ) - _source_tree = source_grid.get_kd_tree(coordinates=source_data_mapping) + _source_tree = source_grid.get_ball_tree( + coordinates=source_data_mapping, + coordinate_system="cartesian", + distance_metric="minkowski", + ) dest_coords = np.vstack([x, y, z]).T @@ -156,7 +160,7 @@ def _inverse_distance_weighted_remap( def _inverse_distance_weighted_remap_uxda( source_uxda: UxDataArray, destination_obj: Union[Grid, UxDataArray, UxDataset], - remap_to: str = "nodes", + remap_to: str = "face centers", coord_type: str = "spherical", power=2, k=8, @@ -249,7 +253,7 @@ def _inverse_distance_weighted_remap_uxda( def _inverse_distance_weighted_remap_uxds( source_uxds: UxDataset, destination_obj: Union[Grid, UxDataArray, UxDataset], - remap_to: str = "nodes", + remap_to: str = "face centers", coord_type: str = "spherical", power=2, k=8, diff --git a/uxarray/remap/nearest_neighbor.py b/uxarray/remap/nearest_neighbor.py index e4c8177a6..e7c261507 100644 --- a/uxarray/remap/nearest_neighbor.py +++ b/uxarray/remap/nearest_neighbor.py @@ -16,7 +16,7 @@ def _nearest_neighbor( source_grid: Grid, destination_grid: Grid, source_data: np.ndarray, - remap_to: str = "nodes", + remap_to: str = "face centers", coord_type: str = "spherical", ) -> np.ndarray: """Nearest Neighbor Remapping between two grids, mapping data that resides @@ -118,7 +118,11 @@ def _nearest_neighbor( ) # specify whether to query on the corner nodes or face centers based on source grid - _source_tree = source_grid.get_kd_tree(coordinates=source_data_mapping) + _source_tree = source_grid.get_ball_tree( + coordinates=source_data_mapping, + coordinate_system="cartesian", + distance_metric="minkowski", + ) # prepare coordinates for query cartesian = np.vstack([cart_x, cart_y, cart_z]).T @@ -147,7 +151,7 @@ def _nearest_neighbor( def _nearest_neighbor_uxda( source_uxda: UxDataArray, destination_obj: Union[Grid, UxDataArray, UxDataset], - remap_to: str = "nodes", + remap_to: str = "face centers", coord_type: str = "spherical", ): """Nearest Neighbor Remapping implementation for ``UxDataArray``. @@ -217,7 +221,7 @@ def _nearest_neighbor_uxda( def _nearest_neighbor_uxds( source_uxds: UxDataset, destination_obj: Union[Grid, UxDataArray, UxDataset], - remap_to: str = "nodes", + remap_to: str = "face centers", coord_type: str = "spherical", ): """Nearest Neighbor Remapping implementation for ``UxDataset``.