Skip to content

Commit e491f57

Browse files
authored
Fast Constant Latitude Cross Sections (#989)
* initial work on fast cross-sections * add API * add section for cross sections * parallel numba * update notebook * add docstrings * add benchmark * update benchmark * update quad hex grid * add tests * update tests * update quad hex grid * update name of intersection func * update value error * update user guide * add docstring for uxda * update docstring of intersections * fix benchmark * add tests for north and south pole * Update intersections.py * update user guide * Delete docs/user-guide/grid.nc * use accessor * update notebook * fix docs
1 parent d6e26e9 commit e491f57

File tree

12 files changed

+667
-19
lines changed

12 files changed

+667
-19
lines changed

benchmarks/mpas_ocean.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import uxarray as ux
66

7+
import numpy as np
8+
79
current_path = Path(os.path.dirname(os.path.realpath(__file__)))
810

911
data_var = 'bottomDepth'
@@ -164,3 +166,11 @@ def teardown(self, resolution):
164166
def time_check_norm(self, resolution):
165167
from uxarray.grid.validation import _check_normalization
166168
_check_normalization(self.uxgrid)
169+
170+
171+
class CrossSections(DatasetBenchmark):
172+
param_names = DatasetBenchmark.param_names + ['n_lat']
173+
params = DatasetBenchmark.params + [[1, 2, 4, 8]]
174+
def time_constant_lat_fast(self, resolution, n_lat):
175+
for lat in np.linspace(-89, 89, n_lat):
176+
self.uxds.uxgrid.constant_latitude_cross_section(lat, method='fast')

docs/api.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,30 @@ UxDataArray
291291
UxDataArray.subset.bounding_circle
292292

293293

294+
Cross Sections
295+
--------------
296+
297+
298+
Grid
299+
~~~~
300+
301+
.. autosummary::
302+
:toctree: generated/
303+
:template: autosummary/accessor_method.rst
304+
305+
Grid.cross_section
306+
Grid.cross_section.constant_latitude
307+
308+
UxDataArray
309+
~~~~~~~~~~~
310+
311+
.. autosummary::
312+
:toctree: generated/
313+
:template: autosummary/accessor_method.rst
314+
315+
UxDataArray.cross_section
316+
UxDataArray.cross_section.constant_latitude
317+
294318
Remapping
295319
---------
296320

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "4a432a8bf95d9cdb",
6+
"metadata": {},
7+
"source": [
8+
"# Cross-Sections\n",
9+
"\n",
10+
"This section demonstrates how to extract cross-sections from an unstructured grid using UXarray, which allows the analysis and visualization across slices of grids.\n"
11+
]
12+
},
13+
{
14+
"cell_type": "code",
15+
"execution_count": null,
16+
"id": "b35ba4a2c30750e4",
17+
"metadata": {
18+
"ExecuteTime": {
19+
"end_time": "2024-10-09T17:50:50.244285Z",
20+
"start_time": "2024-10-09T17:50:50.239653Z"
21+
}
22+
},
23+
"outputs": [],
24+
"source": [
25+
"import uxarray as ux\n",
26+
"import geoviews.feature as gf\n",
27+
"\n",
28+
"import cartopy.crs as ccrs\n",
29+
"import geoviews as gv\n",
30+
"\n",
31+
"projection = ccrs.Robinson()"
32+
]
33+
},
34+
{
35+
"cell_type": "markdown",
36+
"id": "395a3db7-495c-4cff-b733-06bbe522a604",
37+
"metadata": {},
38+
"source": [
39+
"## Data"
40+
]
41+
},
42+
{
43+
"cell_type": "code",
44+
"execution_count": null,
45+
"id": "b4160275c09fe6b0",
46+
"metadata": {
47+
"ExecuteTime": {
48+
"end_time": "2024-10-09T17:50:51.217211Z",
49+
"start_time": "2024-10-09T17:50:50.540946Z"
50+
}
51+
},
52+
"outputs": [],
53+
"source": [
54+
"base_path = \"../../test/meshfiles/ugrid/outCSne30/\"\n",
55+
"grid_path = base_path + \"outCSne30.ug\"\n",
56+
"data_path = base_path + \"outCSne30_vortex.nc\"\n",
57+
"\n",
58+
"uxds = ux.open_dataset(grid_path, data_path)\n",
59+
"uxds[\"psi\"].plot(\n",
60+
" cmap=\"inferno\",\n",
61+
" periodic_elements=\"split\",\n",
62+
" projection=projection,\n",
63+
" title=\"Global Plot\",\n",
64+
")"
65+
]
66+
},
67+
{
68+
"cell_type": "markdown",
69+
"id": "a7a40958-0a4d-47e4-9e38-31925261a892",
70+
"metadata": {},
71+
"source": [
72+
"## Constant Latitude\n",
73+
"\n",
74+
"Cross-sections along constant latitude lines can be obtained using the ``.cross_section.constant_latitude`` method, available for both ``ux.Grid`` and ``ux.DataArray`` objects. This functionality allows users to extract and analyze slices of data at specified latitudes, providing insights into variations along horizontal sections of the grid.\n"
75+
]
76+
},
77+
{
78+
"cell_type": "markdown",
79+
"id": "2fbe9f6e5bb59a17",
80+
"metadata": {},
81+
"source": [
82+
"For example, we can obtain a cross-section at 30 degrees latitude by doing the following:"
83+
]
84+
},
85+
{
86+
"cell_type": "code",
87+
"execution_count": null,
88+
"id": "3775daa1-2f1d-4738-bab5-2b69ebd689d9",
89+
"metadata": {
90+
"ExecuteTime": {
91+
"end_time": "2024-10-09T17:50:53.093314Z",
92+
"start_time": "2024-10-09T17:50:53.077719Z"
93+
}
94+
},
95+
"outputs": [],
96+
"source": [
97+
"lat = 30\n",
98+
"\n",
99+
"uxda_constant_lat = uxds[\"psi\"].cross_section.constant_latitude(lat)"
100+
]
101+
},
102+
{
103+
"cell_type": "markdown",
104+
"id": "dcec0b96b92e7f4",
105+
"metadata": {},
106+
"source": [
107+
"Since the result is a new ``UxDataArray``, we can directly plot the result to see the cross-section."
108+
]
109+
},
110+
{
111+
"cell_type": "code",
112+
"execution_count": null,
113+
"id": "484b77a6-86da-4395-9e63-f5ac56e37deb",
114+
"metadata": {},
115+
"outputs": [],
116+
"source": [
117+
"(\n",
118+
" uxda_constant_lat.plot(\n",
119+
" rasterize=False,\n",
120+
" backend=\"bokeh\",\n",
121+
" cmap=\"inferno\",\n",
122+
" projection=projection,\n",
123+
" global_extent=True,\n",
124+
" coastline=True,\n",
125+
" title=f\"Cross Section at {lat} degrees latitude\",\n",
126+
" )\n",
127+
" * gf.grid(projection=projection)\n",
128+
")"
129+
]
130+
},
131+
{
132+
"cell_type": "markdown",
133+
"id": "c7cca7de4722c121",
134+
"metadata": {},
135+
"source": [
136+
"You can also perform operations on the cross-section, such as taking the mean."
137+
]
138+
},
139+
{
140+
"cell_type": "code",
141+
"execution_count": null,
142+
"id": "1cbee722-34a4-4e67-8e22-f393d7d36c99",
143+
"metadata": {},
144+
"outputs": [],
145+
"source": [
146+
"print(f\"Global Mean: {uxds['psi'].data.mean()}\")\n",
147+
"print(f\"Mean at {lat} degrees lat: {uxda_constant_lat.data.mean()}\")"
148+
]
149+
},
150+
{
151+
"cell_type": "markdown",
152+
"id": "c4a7ee25-0b60-470f-bab7-92ff70563076",
153+
"metadata": {},
154+
"source": [
155+
"## Constant Longitude"
156+
]
157+
},
158+
{
159+
"cell_type": "markdown",
160+
"id": "9fcc8ec5-c6a8-4bde-a33d-7f37f9116ee2",
161+
"metadata": {},
162+
"source": [
163+
"```{warning}\n",
164+
"Constant longitude cross sections are not yet supported.\n",
165+
"```"
166+
]
167+
},
168+
{
169+
"cell_type": "markdown",
170+
"id": "54d9eff1-67f1-4691-a3b0-1ee0c874c98f",
171+
"metadata": {},
172+
"source": [
173+
"## Arbitrary Great Circle Arc (GCA)"
174+
]
175+
},
176+
{
177+
"cell_type": "markdown",
178+
"id": "ea94ff9f-fe86-470d-813b-45f32a633ffc",
179+
"metadata": {},
180+
"source": [
181+
"```{warning}\n",
182+
"Arbitrary great circle arc cross sections are not yet supported.\n",
183+
"```"
184+
]
185+
}
186+
],
187+
"metadata": {
188+
"kernelspec": {
189+
"display_name": "Python 3 (ipykernel)",
190+
"language": "python",
191+
"name": "python3"
192+
},
193+
"language_info": {
194+
"codemirror_mode": {
195+
"name": "ipython",
196+
"version": 3
197+
},
198+
"file_extension": ".py",
199+
"mimetype": "text/x-python",
200+
"name": "python",
201+
"nbconvert_exporter": "python",
202+
"pygments_lexer": "ipython3",
203+
"version": "3.11.8"
204+
}
205+
},
206+
"nbformat": 4,
207+
"nbformat_minor": 5
208+
}

docs/userguide.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ These user guides provide detailed explanations of the core functionality in UXa
4343
`Subsetting <user-guide/subset.ipynb>`_
4444
Select specific regions of a grid
4545

46+
`Cross-Sections <user-guide/cross-sections.ipynb>`_
47+
Select cross-sections of a grid
48+
4649
`Remapping <user-guide/remapping.ipynb>`_
4750
Remap (a.k.a Regrid) between unstructured grids
4851

@@ -82,6 +85,7 @@ These user guides provide additional detail about specific features in UXarray.
8285
user-guide/mpl.ipynb
8386
user-guide/advanced-plotting.ipynb
8487
user-guide/subset.ipynb
88+
user-guide/cross-sections.ipynb
8589
user-guide/remapping.ipynb
8690
user-guide/topological-aggregations.ipynb
8791
user-guide/calculus.ipynb
-457 Bytes
Binary file not shown.

test/test_cross_sections.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import uxarray as ux
2+
import pytest
3+
from pathlib import Path
4+
import os
5+
6+
import numpy.testing as nt
7+
8+
# Define the current path and file paths for grid and data
9+
current_path = Path(os.path.dirname(os.path.realpath(__file__)))
10+
quad_hex_grid_path = current_path / 'meshfiles' / "ugrid" / "quad-hexagon" / 'grid.nc'
11+
quad_hex_data_path = current_path / 'meshfiles' / "ugrid" / "quad-hexagon" / 'data.nc'
12+
13+
cube_sphere_grid = current_path / "meshfiles" / "geos-cs" / "c12" / "test-c12.native.nc4"
14+
15+
16+
17+
class TestQuadHex:
18+
"""The quad hexagon grid contains four faces.
19+
20+
Top Left Face: Index 1
21+
22+
Top Right Face: Index 2
23+
24+
Bottom Left Face: Index 0
25+
26+
Bottom Right Face: Index 3
27+
28+
The top two faces intersect a constant latitude of 0.1
29+
30+
The bottom two faces intersect a constant latitude of -0.1
31+
32+
All four faces intersect a constant latitude of 0.0
33+
"""
34+
35+
def test_constant_lat_cross_section_grid(self):
36+
uxgrid = ux.open_grid(quad_hex_grid_path)
37+
38+
grid_top_two = uxgrid.cross_section.constant_latitude(lat=0.1)
39+
40+
assert grid_top_two.n_face == 2
41+
42+
grid_bottom_two = uxgrid.cross_section.constant_latitude(lat=-0.1)
43+
44+
assert grid_bottom_two.n_face == 2
45+
46+
grid_all_four = uxgrid.cross_section.constant_latitude(lat=0.0)
47+
48+
assert grid_all_four.n_face == 4
49+
50+
with pytest.raises(ValueError):
51+
# no intersections found at this line
52+
uxgrid.cross_section.constant_latitude(lat=10.0)
53+
54+
55+
def test_constant_lat_cross_section_uxds(self):
56+
uxds = ux.open_dataset(quad_hex_grid_path, quad_hex_data_path)
57+
58+
da_top_two = uxds['t2m'].cross_section.constant_latitude(lat=0.1)
59+
60+
nt.assert_array_equal(da_top_two.data, uxds['t2m'].isel(n_face=[1, 2]).data)
61+
62+
da_bottom_two = uxds['t2m'].cross_section.constant_latitude(lat=-0.1)
63+
64+
nt.assert_array_equal(da_bottom_two.data, uxds['t2m'].isel(n_face=[0, 3]).data)
65+
66+
da_all_four = uxds['t2m'].cross_section.constant_latitude(lat=0.0)
67+
68+
nt.assert_array_equal(da_all_four.data , uxds['t2m'].data)
69+
70+
with pytest.raises(ValueError):
71+
# no intersections found at this line
72+
uxds['t2m'].cross_section.constant_latitude(lat=10.0)
73+
74+
75+
class TestGeosCubeSphere:
76+
def test_north_pole(self):
77+
uxgrid = ux.open_grid(cube_sphere_grid)
78+
79+
lats = [89.85, 89.9, 89.95, 89.99]
80+
81+
for lat in lats:
82+
cross_grid = uxgrid.cross_section.constant_latitude(lat=lat)
83+
# Cube sphere grid should have 4 faces centered around the pole
84+
assert cross_grid.n_face == 4
85+
86+
def test_south_pole(self):
87+
uxgrid = ux.open_grid(cube_sphere_grid)
88+
89+
lats = [-89.85, -89.9, -89.95, -89.99]
90+
91+
for lat in lats:
92+
cross_grid = uxgrid.cross_section.constant_latitude(lat=lat)
93+
# Cube sphere grid should have 4 faces centered around the pole
94+
assert cross_grid.n_face == 4

uxarray/core/dataarray.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from uxarray.plot.accessor import UxDataArrayPlotAccessor
3434
from uxarray.subset import DataArraySubsetAccessor
3535
from uxarray.remap import UxDataArrayRemapAccessor
36+
from uxarray.cross_sections import UxDataArrayCrossSectionAccessor
3637
from uxarray.core.aggregation import _uxda_grid_aggregate
3738

3839
import warnings
@@ -85,6 +86,7 @@ def __init__(self, *args, uxgrid: Grid = None, **kwargs):
8586
plot = UncachedAccessor(UxDataArrayPlotAccessor)
8687
subset = UncachedAccessor(DataArraySubsetAccessor)
8788
remap = UncachedAccessor(UxDataArrayRemapAccessor)
89+
cross_section = UncachedAccessor(UxDataArrayCrossSectionAccessor)
8890

8991
def _repr_html_(self) -> str:
9092
if OPTIONS["display_style"] == "text":

uxarray/cross_sections/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from .dataarray_accessor import UxDataArrayCrossSectionAccessor
2+
from .grid_accessor import GridCrossSectionAccessor
3+
4+
__all__ = (
5+
"GridCrossSectionAccessor",
6+
"UxDataArrayCrossSectionAccessor",
7+
)

0 commit comments

Comments
 (0)