diff --git a/docs/user-guide/azimuthal-average.ipynb b/docs/user-guide/azimuthal-average.ipynb new file mode 100644 index 000000000..46f3cc03a --- /dev/null +++ b/docs/user-guide/azimuthal-average.ipynb @@ -0,0 +1,324 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2daec505-9d75-4463-aadc-bc5a7584067f", + "metadata": {}, + "source": [ + "# Azimuthal Averaging\n", + "\n", + "This guide showcases how to apply Azimuthal Averaging on 2D and 3D fields using UXarray." + ] + }, + { + "cell_type": "markdown", + "id": "432f2e27-b76b-4966-b52b-d07a884a12c8", + "metadata": {}, + "source": [ + "## Azimuthal mean basics\n", + "An azimuthal average (or azimuthal mean) is a statistical measure that represents the average of a face-centered variable along rings/bands of constant distance from a specified central point. Azimuthal averaging is useful for describing circular/cylindrical features, where fields that strongly depend on distance from a center.\n", + "\n", + "In UXarray, azimuthal averaging is non-conservative. This means that faces are assigned to radial bins (i.e., distance intervals from the central point) based only on the face center coordinate." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "797748ef-93c4-47f8-a032-bc5dd97fc581", + "metadata": {}, + "outputs": [], + "source": [ + "# The imports below are used to visualize range rings in this notebook and\n", + "# are not needed for routine use of `azimuthal_mean()`.\n", + "import math\n", + "import operator\n", + "from functools import reduce\n", + "\n", + "import cartopy.geodesic as gdyn\n", + "import holoviews as hv\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "import uxarray as ux" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44957942-2cf9-4aba-89a8-fd13b3de6497", + "metadata": {}, + "outputs": [], + "source": [ + "uxds = ux.open_dataset(\n", + " \"../../test/meshfiles/ugrid/outCSne30/outCSne30.ug\",\n", + " \"../../test/meshfiles/ugrid/outCSne30/outCSne30_vortex.nc\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "01d9c2ad-b3e4-4d92-900e-67c7a059e7c8", + "metadata": {}, + "source": [ + "## 1. Azimuthally averaging an idealized 2D field" + ] + }, + { + "cell_type": "markdown", + "id": "c51c7e63-cd71-44e2-bace-cf39d916ed0b", + "metadata": {}, + "source": [ + "### Helper function to draw range rings and visualize azimuthal averaging:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93d06880-cfac-46b9-8d99-5dc7255e9483", + "metadata": {}, + "outputs": [], + "source": [ + "def hv_range_ring(lon, lat, rad_gcd, n_samples=2000, color=\"red\", line_width=1):\n", + " geo = gdyn.Geodesic()\n", + " circ_pts = geo.circle(\n", + " lon=lon, lat=lat, radius=rad_gcd * 111320, n_samples=n_samples\n", + " )\n", + " return hv.Path(circ_pts).opts(color=color, line_width=line_width)" + ] + }, + { + "cell_type": "markdown", + "id": "deb9efe9-0200-4e35-971c-60fea192b215", + "metadata": {}, + "source": [ + "### Step 1.1: Visualize the global field\n", + "The global field is shaded below using the UxDataArray.plot() accessor. To help visualize azimuthal averaging, the chosen central point is marked with an 'x' and rings are drawn at every 10 great-circle degrees from the central point (37°N, 1°E)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5417964c-52aa-4bbe-bd18-2fe4f90fbe58", + "metadata": {}, + "outputs": [], + "source": [ + "# Display the global field\n", + "glob_plt = uxds[\"psi\"].plot(\n", + " cmap=\"inferno\", periodic_elements=\"split\", title=\"Global Field\", dynamic=True\n", + ")\n", + "\n", + "# Mark a center coordinate and draw range rings\n", + "lon, lat = 1, 37\n", + "glob_plt = glob_plt * hv.Points([(lon, lat)]).opts(\n", + " color=\"lime\", marker=\"x\", size=10, line_width=2\n", + ")\n", + "glob_plt = glob_plt * reduce(\n", + " operator.mul,\n", + " [hv_range_ring(lon, lat, rr, color=\"lime\") for rr in np.arange(10, 41, 10)],\n", + ")\n", + "glob_plt" + ] + }, + { + "cell_type": "markdown", + "id": "53a40082-9861-4ecc-920c-15797c0537d7", + "metadata": {}, + "source": [ + "### Step 1.2: Compute the azimuthal mean\n", + "Calling `.azimuthal_mean()` with the arguments below samples every 2° out to 40 great-circle degrees around the central point." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0076c75-cf91-4e0b-89e0-a9376bccf706", + "metadata": {}, + "outputs": [], + "source": [ + "azim_mean_psi, hits = uxds[\"psi\"].azimuthal_mean(\n", + " (lon, lat), 40.0, 2, return_hit_counts=True\n", + ")\n", + "azim_mean_psi" + ] + }, + { + "cell_type": "markdown", + "id": "9ecf6cb2-9575-4e13-8dea-d8ec50149a7a", + "metadata": {}, + "source": [ + "### Step 1.3: Plot the azimuthal mean\n", + "In the plot below, red dots mark where samples were taken at 2° intervals. The green vertical lines correspond to the green rings in the global field plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94484bd9-9d90-43be-827d-da668badb3f4", + "metadata": {}, + "outputs": [], + "source": [ + "plt.plot(azim_mean_psi[\"radius\"], azim_mean_psi)\n", + "plt.scatter(azim_mean_psi[\"radius\"], azim_mean_psi, s=10, color=\"red\")\n", + "plt.xlabel(\"radius [great-circle degrees]\")\n", + "plt.ylabel(\"psi\")\n", + "\n", + "[plt.axvline(rr, color=\"lime\") for rr in np.arange(10, 41, 10)]" + ] + }, + { + "cell_type": "markdown", + "id": "6995cf25-c174-431e-8e80-82a3dba669fd", + "metadata": {}, + "source": [ + "### Step 1.4: Inspect the hit count\n", + "The plot below shows the number of face centers that fall within each distance bin. As one would expect on a near-uniformly spaced mesh, the hit count increases linearly with distance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea9d6baf-a81a-44a0-81a2-1264c5df9fca", + "metadata": {}, + "outputs": [], + "source": [ + "plt.plot(azim_mean_psi[\"radius\"], hits)\n", + "plt.scatter(azim_mean_psi[\"radius\"], hits, s=10, color=\"red\")" + ] + }, + { + "cell_type": "markdown", + "id": "6486781f-8d1e-4a9d-8311-da5f48e7234b", + "metadata": {}, + "source": [ + "## 2: Azimuthally averaging tropical cyclone fields\n", + "A high-resolution (~0.25°) aquaplanet general circulation model permits the development of a handful of strong tropical cyclones (TCs). Because TCs are fairly axisymmetric, azimuthal averaging is useful for transforming their fields into cylindrical coordinates and visualizing features such as their low central pressure and warm core." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad86c507-2b41-4803-9270-588993fea5d1", + "metadata": {}, + "outputs": [], + "source": [ + "clon, clat = 114.54, -17.66\n", + "tcds = ux.open_dataset(\n", + " \"/glade/work/jpan/uxazim_demo/ne120np4_pentagons_100310.nc\",\n", + " \"/glade/work/jpan/uxazim_demo/cam.h1i.plev.0013-01-13-00000.nc\",\n", + ").squeeze()" + ] + }, + { + "cell_type": "markdown", + "id": "9269f53d-92ae-4787-8a61-576a588a7dfd", + "metadata": {}, + "source": [ + "### Step 2.1: Visualize the raw surface pressure field\n", + "Because we are only interested in a single tropical cyclone, we can subset a region of the global field using a bounding circle to speed up plotting." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9514b0bd-c6f6-4774-9f9f-aeac2916f1d9", + "metadata": {}, + "outputs": [], + "source": [ + "# Use a bounding circle to select faces whose centers lie\n", + "# within 5 great-circle degrees of the central point.\n", + "tcps = tcds[\"PS\"].subset.bounding_circle((clon, clat), 5)\n", + "\n", + "# Setting dynamic=True allows for quick, adaptive rendering as you zoom/pan.\n", + "# It also preserves the original mesh faces in the plot.\n", + "ps_plt = tcps.plot(\n", + " cmap=\"inferno\", periodic_elements=\"split\", title=\"Surface pressure\", dynamic=True\n", + ")\n", + "ps_plt" + ] + }, + { + "cell_type": "markdown", + "id": "0c989f9f-109d-45b7-b011-ad46c0fdfb6a", + "metadata": {}, + "source": [ + "### Step 2.2: Compute the azimuthal mean of the 3D fields" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b030fc4b-f2d7-4691-832e-9dc32e7bbd67", + "metadata": {}, + "outputs": [], + "source": [ + "args = ((clon, clat), 3, 0.25)\n", + "azim_mean_T = tcds[\"T\"].azimuthal_mean(*args)\n", + "azim_mean_Z = tcds[\"Z3\"].azimuthal_mean(*args)\n", + "azim_mean_T" + ] + }, + { + "cell_type": "markdown", + "id": "7c4bad1b-ff8d-41bf-96a3-24f8de1735dc", + "metadata": {}, + "source": [ + "### Step 2.3: Plot the TC radial profile\n", + "The contour plot below shows the warm core and low pressure of the TC. After taking the azimuthal average, we subtract the value at the outermost radius to obtain an approximate anomaly relative to the ambient environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be4a5dd8-bc76-41e2-bcc5-748d66867c9e", + "metadata": {}, + "outputs": [], + "source": [ + "# Subtract the value at the outer radius\n", + "Tpert = azim_mean_T - azim_mean_T.isel(radius=-1)\n", + "Zpert = azim_mean_Z - azim_mean_Z.isel(radius=-1)\n", + "\n", + "plt.contour(\n", + " azim_mean_Z[\"radius\"],\n", + " tcds[\"plev\"] / 100,\n", + " Zpert,\n", + " levels=np.arange(-500, 501, 50),\n", + " colors=\"black\",\n", + ")\n", + "plt.contourf(\n", + " azim_mean_T[\"radius\"],\n", + " tcds[\"plev\"] / 100,\n", + " Tpert,\n", + " levels=np.arange(-10, 11, 2),\n", + " cmap=\"bwr\",\n", + " extend=\"both\",\n", + ")\n", + "plt.xlabel(\"distance from center [°]\")\n", + "plt.ylim(1000, 100)\n", + "plt.yscale(\"log\")\n", + "plt.ylabel(\"Pressure [hPa]\")\n", + "plt.colorbar(label=\"T anomaly [K]\")" + ] + } + ], + "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.13.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/userguide.rst b/docs/userguide.rst index 1ffd170eb..c281805b3 100644 --- a/docs/userguide.rst +++ b/docs/userguide.rst @@ -58,6 +58,9 @@ These user guides provide detailed explanations of the core functionality in UXa `Zonal Means `_ Compute the zonal averages across lines of constant latitude +`Azimuthal Mean `_ + Compute the azimuthal average along rings of constant distance from a specified central point + `Remapping `_ Remap (a.k.a Regrid) between unstructured grids @@ -114,6 +117,7 @@ These user guides provide additional details about specific features in UXarray. user-guide/subset.ipynb user-guide/cross-sections.ipynb user-guide/zonal-average.ipynb + user-guide/azimuthal-average.ipynb user-guide/remapping.ipynb user-guide/topological-aggregations.ipynb user-guide/weighted_mean.ipynb