uxarray 2026.6.0__tar.gz → 2026.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {uxarray-2026.6.0 → uxarray-2026.7.0}/PKG-INFO +4 -4
- {uxarray-2026.6.0 → uxarray-2026.7.0}/pyproject.toml +4 -3
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/core/api.py +70 -15
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/core/dataarray.py +91 -13
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/core/dataset.py +33 -27
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/core/gradient.py +52 -53
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/core/utils.py +5 -1
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/grid.py +26 -4
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_scrip.py +62 -14
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/utils.py +1 -1
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/plot/accessor.py +36 -3
- uxarray-2026.7.0/uxarray/plot/utils.py +72 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/remap/accessor.py +7 -3
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/remap/yac.py +28 -3
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray.egg-info/PKG-INFO +4 -4
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray.egg-info/SOURCES.txt +2 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray.egg-info/requires.txt +2 -3
- uxarray-2026.7.0/uxarray.egg-info/scm_file_list.json +385 -0
- uxarray-2026.7.0/uxarray.egg-info/scm_version.json +8 -0
- uxarray-2026.6.0/uxarray/plot/utils.py +0 -40
- {uxarray-2026.6.0 → uxarray-2026.7.0}/.codecov.yml +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/.git_archival.txt +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/.gitattributes +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/CITATION.cff +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/CODE_OF_CONDUCT.md +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/CONTRIBUTING.md +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/INSTALLATION.md +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/LICENSE +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/MANIFEST.in +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/README.md +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/build.sh +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/setup.cfg +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/setup.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/__init__.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/constants.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/conventions/__init__.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/conventions/descriptors.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/conventions/ugrid.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/core/__init__.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/core/accessors.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/core/aggregation.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/core/zonal.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/cross_sections/__init__.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/cross_sections/dataarray_accessor.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/cross_sections/grid_accessor.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/cross_sections/sample.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/formatting_html.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/__init__.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/arcs.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/area.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/bounds.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/connectivity.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/coordinates.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/dual.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/geometry.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/integrate.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/intersections.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/neighbors.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/point_in_face.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/slice.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/utils.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/grid/validation.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/__init__.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_delaunay.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_esmf.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_exodus.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_fesom2.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_geopandas.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_geos.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_healpix.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_icon.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_mpas.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_structured.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_topology.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_ugrid.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_vertices.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/io/_voronoi.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/plot/__init__.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/plot/constants.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/plot/matplotlib.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/remap/__init__.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/remap/apply_weights.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/remap/bilinear.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/remap/inverse_distance_weighted.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/remap/nearest_neighbor.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/remap/spatial_coords_remap.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/remap/structured.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/remap/utils.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/remap/weights.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/subset/__init__.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/subset/dataarray_accessor.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/subset/grid_accessor.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/tutorial/__init__.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/tutorial/registry.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/utils/__init__.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray/utils/computing.py +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray.egg-info/dependency_links.txt +0 -0
- {uxarray-2026.6.0 → uxarray-2026.7.0}/uxarray.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uxarray
|
|
3
|
-
Version: 2026.
|
|
3
|
+
Version: 2026.7.0
|
|
4
4
|
Summary: Xarray extension for unstructured climate and global weather data analysis and visualization.
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -217,6 +217,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
217
217
|
Classifier: Programming Language :: Python :: 3.11
|
|
218
218
|
Classifier: Programming Language :: Python :: 3.12
|
|
219
219
|
Classifier: Programming Language :: Python :: 3.13
|
|
220
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
220
221
|
Requires-Python: >=3.10
|
|
221
222
|
Description-Content-Type: text/markdown
|
|
222
223
|
License-File: LICENSE
|
|
@@ -226,10 +227,10 @@ Requires-Dist: dask[dataframe]
|
|
|
226
227
|
Requires-Dist: datashader
|
|
227
228
|
Requires-Dist: geoviews
|
|
228
229
|
Requires-Dist: holoviews
|
|
229
|
-
Requires-Dist: matplotlib
|
|
230
|
+
Requires-Dist: matplotlib<3.11
|
|
230
231
|
Requires-Dist: matplotlib-inline
|
|
231
232
|
Requires-Dist: netcdf4
|
|
232
|
-
Requires-Dist: numba
|
|
233
|
+
Requires-Dist: numba>=0.63
|
|
233
234
|
Requires-Dist: numpy
|
|
234
235
|
Requires-Dist: pandas
|
|
235
236
|
Requires-Dist: pyarrow
|
|
@@ -247,7 +248,6 @@ Requires-Dist: pyproj
|
|
|
247
248
|
Provides-Extra: complete
|
|
248
249
|
Requires-Dist: uxarray[dev]; extra == "complete"
|
|
249
250
|
Provides-Extra: dev
|
|
250
|
-
Requires-Dist: pathlib; extra == "dev"
|
|
251
251
|
Requires-Dist: pre_commit; extra == "dev"
|
|
252
252
|
Requires-Dist: pytest; extra == "dev"
|
|
253
253
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -9,6 +9,7 @@ classifiers=[
|
|
|
9
9
|
"Programming Language :: Python :: 3.11",
|
|
10
10
|
"Programming Language :: Python :: 3.12",
|
|
11
11
|
"Programming Language :: Python :: 3.13",
|
|
12
|
+
"Programming Language :: Python :: 3.14",
|
|
12
13
|
]
|
|
13
14
|
|
|
14
15
|
dynamic = ["version"]
|
|
@@ -27,10 +28,10 @@ dependencies = [
|
|
|
27
28
|
"datashader",
|
|
28
29
|
"geoviews",
|
|
29
30
|
"holoviews",
|
|
30
|
-
"matplotlib",
|
|
31
|
+
"matplotlib<3.11", # matplotlib 3.11.0 breaks some plots but <3.11 is fine; see issue #1542.
|
|
31
32
|
"matplotlib-inline",
|
|
32
33
|
"netcdf4",
|
|
33
|
-
"numba",
|
|
34
|
+
"numba>=0.63", # 0.63 is the first release supporting Python 3.14 (3.10 <= py < 3.15). See #1561.
|
|
34
35
|
"numpy",
|
|
35
36
|
"pandas",
|
|
36
37
|
"pyarrow",
|
|
@@ -50,7 +51,7 @@ dependencies = [
|
|
|
50
51
|
|
|
51
52
|
[project.optional-dependencies]
|
|
52
53
|
complete = ["uxarray[dev]"]
|
|
53
|
-
dev = ['
|
|
54
|
+
dev = ['pre_commit', 'pytest', 'pytest-cov', 'ruff', 'asv']
|
|
54
55
|
|
|
55
56
|
[project.urls]
|
|
56
57
|
Documentation = "https://uxarray.readthedocs.io/"
|
|
@@ -353,7 +353,7 @@ def list_grid_names(
|
|
|
353
353
|
|
|
354
354
|
def open_dataset(
|
|
355
355
|
grid_filename_or_obj: str | os.PathLike[Any] | dict | Dataset,
|
|
356
|
-
filename_or_obj: str | os.PathLike[Any],
|
|
356
|
+
filename_or_obj: str | os.PathLike[Any] | Dataset | None = None,
|
|
357
357
|
chunks=None,
|
|
358
358
|
chunk_grid: bool = True,
|
|
359
359
|
use_dual: bool | None = False,
|
|
@@ -364,14 +364,22 @@ def open_dataset(
|
|
|
364
364
|
|
|
365
365
|
Parameters
|
|
366
366
|
----------
|
|
367
|
-
grid_filename_or_obj : str | os.PathLike[Any] | dict | xr.
|
|
368
|
-
Strings and Path objects are interpreted as a path to a grid
|
|
369
|
-
each member variable is in the UGRID conventions and will be used to
|
|
370
|
-
containing UGRID variables can be used to create a
|
|
371
|
-
|
|
367
|
+
grid_filename_or_obj : str | os.PathLike[Any] | dict | xr.Dataset
|
|
368
|
+
Grid information for the ``UxDataset``. Strings and Path objects are interpreted as a path to a grid
|
|
369
|
+
file. Xarray Datasets assume that each member variable is in the UGRID conventions and will be used to
|
|
370
|
+
create a ``ux.Grid``. Similarly, a dictionary containing UGRID variables can be used to create a
|
|
371
|
+
``ux.Grid``. A path to a directory containing grid files (e.g. a FESOM2 ASCII grid) is also accepted,
|
|
372
|
+
but only when ``filename_or_obj`` is provided; directory input is not supported in the single-argument
|
|
373
|
+
form below.
|
|
374
|
+
filename_or_obj : str | os.PathLike[Any] | xr.Dataset, optional
|
|
372
375
|
String or Path object as a path to a netCDF file or an OpenDAP URL that
|
|
373
|
-
stores the actual data set
|
|
374
|
-
``xarray.open_dataset``.
|
|
376
|
+
stores the actual data set, or an already-open ``xarray.Dataset``. It
|
|
377
|
+
is the same ``filename_or_obj`` in ``xarray.open_dataset``. If omitted,
|
|
378
|
+
``grid_filename_or_obj`` is also used as the data source, allowing a
|
|
379
|
+
combined grid-and-data file or ``xarray.Dataset`` to be opened with a
|
|
380
|
+
single argument. In this single-argument form the file is used to build
|
|
381
|
+
the grid and also treated as data; all variables it contains, including
|
|
382
|
+
grid variables, will be included as data.
|
|
375
383
|
chunks : int, dict, 'auto' or None, default: None
|
|
376
384
|
If provided, used to load the grid into dask arrays.
|
|
377
385
|
|
|
@@ -400,29 +408,76 @@ def open_dataset(
|
|
|
400
408
|
uxds : uxarray.UxDataset
|
|
401
409
|
Dataset with linked `uxgrid` property of type `Grid`.
|
|
402
410
|
|
|
411
|
+
Notes
|
|
412
|
+
-----
|
|
413
|
+
The grid need not be a complete or fully UGRID-compliant mesh, nor represent
|
|
414
|
+
a spherical/Earth-science domain. A partial grid can be opened; operations
|
|
415
|
+
requiring information absent from the file will raise when used, but
|
|
416
|
+
unrelated functionality remains available.
|
|
417
|
+
|
|
403
418
|
Examples
|
|
404
419
|
--------
|
|
405
420
|
Open a dataset with a grid file and data file
|
|
406
421
|
|
|
407
422
|
>>> import uxarray as ux
|
|
408
423
|
>>> ux_ds = ux.open_dataset("grid_file.nc", "data_file.nc")
|
|
424
|
+
|
|
425
|
+
Open a dataset stored in a single combined grid-and-data file
|
|
426
|
+
|
|
427
|
+
>>> ux_ds = ux.open_dataset("combined_file.nc")
|
|
409
428
|
"""
|
|
429
|
+
import xarray as xr
|
|
430
|
+
|
|
410
431
|
if grid_kwargs is None:
|
|
411
432
|
grid_kwargs = {}
|
|
412
433
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
434
|
+
if filename_or_obj is None:
|
|
435
|
+
if isinstance(grid_filename_or_obj, (str, os.PathLike)):
|
|
436
|
+
if os.path.isdir(grid_filename_or_obj):
|
|
437
|
+
raise ValueError(
|
|
438
|
+
"ux.open_dataset() with a single directory argument is not supported. "
|
|
439
|
+
"Supply a path to a grid file instead. Directory-based grids (e.g. a "
|
|
440
|
+
"FESOM2 ASCII grid) are only recognized when a separate data file is "
|
|
441
|
+
"also provided, i.e. ux.open_dataset(grid_directory, data_file)."
|
|
442
|
+
)
|
|
417
443
|
|
|
418
|
-
|
|
419
|
-
|
|
444
|
+
ds = _open_dataset_with_fallback(
|
|
445
|
+
grid_filename_or_obj,
|
|
446
|
+
chunks=match_chunks_to_ugrid(grid_filename_or_obj, chunks),
|
|
447
|
+
**kwargs,
|
|
448
|
+
)
|
|
449
|
+
elif isinstance(grid_filename_or_obj, xr.Dataset):
|
|
450
|
+
ds = grid_filename_or_obj
|
|
451
|
+
else:
|
|
452
|
+
raise ValueError(
|
|
453
|
+
"If filename_or_obj is omitted, grid_filename_or_obj must be a file path or xarray.Dataset."
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
uxgrid, _ = _get_grid(ds, chunks, chunk_grid, use_dual, grid_kwargs, **kwargs)
|
|
457
|
+
filename_or_obj = grid_filename_or_obj
|
|
458
|
+
else:
|
|
459
|
+
# Construct a Grid, validate parameters, and correct chunks
|
|
460
|
+
uxgrid, corrected_chunks = _get_grid(
|
|
461
|
+
grid_filename_or_obj, chunks, chunk_grid, use_dual, grid_kwargs, **kwargs
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# Load the data as a Xarray Dataset
|
|
465
|
+
if isinstance(filename_or_obj, xr.Dataset):
|
|
466
|
+
ds = filename_or_obj
|
|
467
|
+
else:
|
|
468
|
+
ds = _open_dataset_with_fallback(
|
|
469
|
+
filename_or_obj, chunks=corrected_chunks, **kwargs
|
|
470
|
+
)
|
|
420
471
|
|
|
421
472
|
# Map original dimensions to the UGRID conventions
|
|
422
473
|
ds = _map_dims_to_ugrid(ds, uxgrid._source_dims_dict, uxgrid)
|
|
423
474
|
|
|
424
475
|
# Create a UXarray Dataset by linking the Xarray Dataset with a UXarray Grid
|
|
425
|
-
|
|
476
|
+
source_datasets = (
|
|
477
|
+
None if isinstance(filename_or_obj, xr.Dataset) else str(filename_or_obj)
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
return UxDataset(ds, uxgrid=uxgrid, source_datasets=source_datasets)
|
|
426
481
|
|
|
427
482
|
|
|
428
483
|
def open_mfdataset(
|
|
@@ -164,6 +164,40 @@ class UxDataArray(xr.DataArray):
|
|
|
164
164
|
else:
|
|
165
165
|
return None
|
|
166
166
|
|
|
167
|
+
@property
|
|
168
|
+
def data_location(self):
|
|
169
|
+
"""Returns where on the grid the data variable is stored.
|
|
170
|
+
|
|
171
|
+
The location is inferred from the grid dimension present in the data
|
|
172
|
+
variable, using UGRID-style names:
|
|
173
|
+
|
|
174
|
+
- ``"face_centered"`` if the data contains the ``n_face`` dimension
|
|
175
|
+
- ``"node_centered"`` if the data contains the ``n_node`` dimension
|
|
176
|
+
- ``"edge_centered"`` if the data contains the ``n_edge`` dimension
|
|
177
|
+
- ``None`` if the data is not mapped to the grid
|
|
178
|
+
|
|
179
|
+
Notes
|
|
180
|
+
-----
|
|
181
|
+
Additional locations described in the UGRID/spectral-element ecosystem
|
|
182
|
+
(e.g. ``"face_average"``, ``"edge_orthogonal"``, ``"edge_parallel"``,
|
|
183
|
+
``"cgll"``, ``"dgll"``) cannot be inferred from a data variable's
|
|
184
|
+
dimensions alone and are not currently distinguished here.
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
str or None
|
|
189
|
+
One of ``"face_centered"``, ``"node_centered"``,
|
|
190
|
+
``"edge_centered"``, or ``None``.
|
|
191
|
+
"""
|
|
192
|
+
if self._face_centered():
|
|
193
|
+
return "face_centered"
|
|
194
|
+
elif self._node_centered():
|
|
195
|
+
return "node_centered"
|
|
196
|
+
elif self._edge_centered():
|
|
197
|
+
return "edge_centered"
|
|
198
|
+
else:
|
|
199
|
+
return None
|
|
200
|
+
|
|
167
201
|
def to_geodataframe(
|
|
168
202
|
self,
|
|
169
203
|
periodic_elements: str | None = "exclude",
|
|
@@ -1483,20 +1517,33 @@ class UxDataArray(xr.DataArray):
|
|
|
1483
1517
|
Parameters
|
|
1484
1518
|
----------
|
|
1485
1519
|
scale_by_radius : bool, default=True
|
|
1486
|
-
Divide unit-sphere derivatives by ``uxgrid.sphere_radius
|
|
1520
|
+
Divide unit-sphere derivatives by ``uxgrid.sphere_radius`` so the
|
|
1521
|
+
result carries physical, per-meter units (``[data units]/m``). When
|
|
1522
|
+
``False`` the result is left on the unit sphere with per-radian units
|
|
1523
|
+
(``[data units]/rad``). If ``True`` but the grid has no
|
|
1524
|
+
``sphere_radius`` attribute, the result falls back to unit-sphere
|
|
1525
|
+
output and a ``UserWarning`` is emitted.
|
|
1487
1526
|
|
|
1488
1527
|
Returns
|
|
1489
1528
|
-------
|
|
1490
1529
|
gradient: UxDataset
|
|
1491
|
-
Dataset containing the zonal and
|
|
1530
|
+
Dataset containing the zonal and meridional components of the gradient.
|
|
1531
|
+
With the default ``scale_by_radius=True`` the components are in
|
|
1532
|
+
``[data units]/m``; with ``scale_by_radius=False`` they are in
|
|
1533
|
+
``[data units]/rad``.
|
|
1492
1534
|
|
|
1493
1535
|
Notes
|
|
1494
1536
|
-----
|
|
1495
|
-
The Green-Gauss
|
|
1537
|
+
The Green-Gauss theorem is utilized, where a closed control volume around each cell
|
|
1496
1538
|
is formed connecting centroids of the neighboring cells. The surface integral is
|
|
1497
1539
|
approximated using the trapezoidal rule. The sum of the contributions is then
|
|
1498
1540
|
normalized by the cell volume.
|
|
1499
1541
|
|
|
1542
|
+
By default the raw unit-sphere (per-radian) gradient is divided by
|
|
1543
|
+
``uxgrid.sphere_radius`` to yield physical per-meter values. For Earth
|
|
1544
|
+
(radius ~6.37e6 m) this scales magnitudes down by ~1/6.37e6 relative to
|
|
1545
|
+
the unit-sphere result.
|
|
1546
|
+
|
|
1500
1547
|
Example
|
|
1501
1548
|
-------
|
|
1502
1549
|
>>> uxds["var"].gradient()
|
|
@@ -1539,7 +1586,10 @@ class UxDataArray(xr.DataArray):
|
|
|
1539
1586
|
represent the meridional (v) component, while self represents the
|
|
1540
1587
|
zonal (u) component.
|
|
1541
1588
|
scale_by_radius : bool, default=True
|
|
1542
|
-
Divide unit-sphere derivatives by ``uxgrid.sphere_radius
|
|
1589
|
+
Divide unit-sphere derivatives by ``uxgrid.sphere_radius`` so the
|
|
1590
|
+
result carries physical, per-meter units (e.g. ``1/s`` for a velocity
|
|
1591
|
+
field). When ``False`` the result is left on the unit sphere
|
|
1592
|
+
(per radian).
|
|
1543
1593
|
**kwargs : dict
|
|
1544
1594
|
Additional keyword arguments (currently unused, reserved for future extensions).
|
|
1545
1595
|
|
|
@@ -1547,7 +1597,9 @@ class UxDataArray(xr.DataArray):
|
|
|
1547
1597
|
-------
|
|
1548
1598
|
curl : UxDataArray
|
|
1549
1599
|
The curl of the vector field (u, v), computed as:
|
|
1550
|
-
curl = ∂v/∂x - ∂u/∂y
|
|
1600
|
+
curl = ∂v/∂x - ∂u/∂y. With the default ``scale_by_radius=True`` the
|
|
1601
|
+
result is in ``([u units])/m`` (``1/s`` for velocity); with
|
|
1602
|
+
``scale_by_radius=False`` it is in ``([u units])/rad``.
|
|
1551
1603
|
|
|
1552
1604
|
Notes
|
|
1553
1605
|
-----
|
|
@@ -1603,7 +1655,7 @@ class UxDataArray(xr.DataArray):
|
|
|
1603
1655
|
u_units = self.attrs.get("units", "")
|
|
1604
1656
|
has_sphere_radius = "sphere_radius" in self.uxgrid._ds.attrs
|
|
1605
1657
|
if scale_by_radius and has_sphere_radius:
|
|
1606
|
-
curl_units = f"({u_units})/m" if u_units else "1/
|
|
1658
|
+
curl_units = f"({u_units})/m" if u_units else "1/m"
|
|
1607
1659
|
else:
|
|
1608
1660
|
curl_units = f"({u_units})/rad" if u_units else "1/rad"
|
|
1609
1661
|
|
|
@@ -1622,16 +1674,26 @@ class UxDataArray(xr.DataArray):
|
|
|
1622
1674
|
|
|
1623
1675
|
return curl_da
|
|
1624
1676
|
|
|
1625
|
-
def divergence(
|
|
1677
|
+
def divergence(
|
|
1678
|
+
self, other: "UxDataArray", scale_by_radius: bool = True, **kwargs
|
|
1679
|
+
) -> "UxDataArray":
|
|
1626
1680
|
"""
|
|
1627
1681
|
Computes the divergence of the vector field defined by this UxDataArray and other.
|
|
1628
1682
|
|
|
1629
1683
|
Parameters
|
|
1630
1684
|
----------
|
|
1631
1685
|
other : UxDataArray
|
|
1632
|
-
The second component of the vector field
|
|
1686
|
+
The second (meridional, v) component of the vector field; ``self`` is
|
|
1687
|
+
the first (zonal, u) component.
|
|
1688
|
+
scale_by_radius : bool, default=True
|
|
1689
|
+
Divide unit-sphere derivatives by ``uxgrid.sphere_radius``. When
|
|
1690
|
+
``True`` (and the grid has a ``sphere_radius`` attribute) the result
|
|
1691
|
+
carries physical, per-meter units (e.g. ``1/s`` for a velocity
|
|
1692
|
+
field); when ``False`` the result is left on the unit sphere
|
|
1693
|
+
(per radian).
|
|
1633
1694
|
**kwargs
|
|
1634
|
-
Additional keyword arguments
|
|
1695
|
+
Additional keyword arguments. ``units`` may be passed to override the
|
|
1696
|
+
automatically inferred units.
|
|
1635
1697
|
|
|
1636
1698
|
Returns
|
|
1637
1699
|
-------
|
|
@@ -1645,7 +1707,9 @@ class UxDataArray(xr.DataArray):
|
|
|
1645
1707
|
the divergence is calculated as div(V) = ∂u/∂x + ∂v/∂y.
|
|
1646
1708
|
|
|
1647
1709
|
The implementation uses edge-centered gradients and face-centered divergence calculation
|
|
1648
|
-
following the discrete divergence theorem.
|
|
1710
|
+
following the discrete divergence theorem. By default the underlying
|
|
1711
|
+
gradients are divided by ``uxgrid.sphere_radius``; pass
|
|
1712
|
+
``scale_by_radius=False`` for per-radian (unit-sphere) output.
|
|
1649
1713
|
|
|
1650
1714
|
Example
|
|
1651
1715
|
-------
|
|
@@ -1675,8 +1739,8 @@ class UxDataArray(xr.DataArray):
|
|
|
1675
1739
|
)
|
|
1676
1740
|
|
|
1677
1741
|
# Compute gradients of both components
|
|
1678
|
-
u_gradient = self.gradient()
|
|
1679
|
-
v_gradient = other.gradient()
|
|
1742
|
+
u_gradient = self.gradient(scale_by_radius=scale_by_radius)
|
|
1743
|
+
v_gradient = other.gradient(scale_by_radius=scale_by_radius)
|
|
1680
1744
|
|
|
1681
1745
|
# For divergence: div(V) = ∂u/∂x + ∂v/∂y
|
|
1682
1746
|
# We use the zonal gradient (∂/∂lon) of u and meridional gradient (∂/∂lat) of v
|
|
@@ -1687,10 +1751,24 @@ class UxDataArray(xr.DataArray):
|
|
|
1687
1751
|
u, v = xr.align(u, v)
|
|
1688
1752
|
divergence = u + v
|
|
1689
1753
|
divergence.name = "divergence"
|
|
1754
|
+
|
|
1755
|
+
# Infer units consistently with gradient()/curl(): a divergence is a
|
|
1756
|
+
# spatial derivative of the input field, so it carries an extra 1/length
|
|
1757
|
+
# factor (per meter when scaled by radius, otherwise per radian).
|
|
1758
|
+
if "units" in kwargs:
|
|
1759
|
+
div_units = kwargs["units"]
|
|
1760
|
+
else:
|
|
1761
|
+
u_units = self.attrs.get("units", "")
|
|
1762
|
+
has_sphere_radius = "sphere_radius" in self.uxgrid._ds.attrs
|
|
1763
|
+
if scale_by_radius and has_sphere_radius:
|
|
1764
|
+
div_units = f"({u_units})/m" if u_units else "1/m"
|
|
1765
|
+
else:
|
|
1766
|
+
div_units = f"({u_units})/rad" if u_units else "1/rad"
|
|
1767
|
+
|
|
1690
1768
|
divergence.attrs.update(
|
|
1691
1769
|
{
|
|
1692
1770
|
"divergence": True,
|
|
1693
|
-
"units":
|
|
1771
|
+
"units": div_units,
|
|
1694
1772
|
}
|
|
1695
1773
|
)
|
|
1696
1774
|
|
|
@@ -570,7 +570,13 @@ class UxDataset(xr.Dataset):
|
|
|
570
570
|
buf.write("\n".join(lines))
|
|
571
571
|
|
|
572
572
|
def integrate(self, quadrature_rule="triangular", order=4):
|
|
573
|
-
"""Integrates over all the faces of the
|
|
573
|
+
"""Integrates over all the faces of the given mesh, for every data
|
|
574
|
+
variable in the dataset.
|
|
575
|
+
|
|
576
|
+
Each data variable is integrated independently using
|
|
577
|
+
:meth:`UxDataArray.integrate`, and the results are collected into a new
|
|
578
|
+
:class:`UxDataset`. Variables whose final dimension does not map to the
|
|
579
|
+
grid (i.e. that are not face/node/edge centered) are skipped.
|
|
574
580
|
|
|
575
581
|
Parameters
|
|
576
582
|
----------
|
|
@@ -581,45 +587,45 @@ class UxDataset(xr.Dataset):
|
|
|
581
587
|
|
|
582
588
|
Returns
|
|
583
589
|
-------
|
|
584
|
-
|
|
590
|
+
uxds : UxDataset
|
|
591
|
+
Dataset containing the integrated value of each (grid-mapped) data
|
|
592
|
+
variable.
|
|
585
593
|
|
|
586
594
|
Examples
|
|
587
595
|
--------
|
|
588
|
-
Open a UXarray dataset
|
|
596
|
+
Open a UXarray dataset and integrate every data variable
|
|
589
597
|
|
|
590
598
|
>>> import uxarray as ux
|
|
591
599
|
>>> uxds = ux.open_dataset("grid.ug", "centroid_pressure_data_ug")
|
|
592
|
-
|
|
593
|
-
# Compute the integral
|
|
594
600
|
>>> integral = uxds.integrate()
|
|
595
|
-
"""
|
|
596
601
|
|
|
597
|
-
|
|
598
|
-
warn(
|
|
599
|
-
"This method currently only works when there is a single DataArray in this Dataset. For integration of a "
|
|
600
|
-
"single data variable, use the UxDataArray.integrate() method instead. This function will be deprecated and "
|
|
601
|
-
"replaced with one that can perform a Dataset-wide integration in a future release.",
|
|
602
|
-
DeprecationWarning,
|
|
603
|
-
)
|
|
602
|
+
Access the integral of a single variable
|
|
604
603
|
|
|
605
|
-
integral
|
|
604
|
+
>>> integral["psi"]
|
|
605
|
+
"""
|
|
606
|
+
integrated_vars = {}
|
|
607
|
+
skipped = []
|
|
606
608
|
|
|
607
|
-
|
|
609
|
+
for name, uxda in self.items():
|
|
610
|
+
try:
|
|
611
|
+
integrated_vars[name] = uxda.integrate(
|
|
612
|
+
quadrature_rule=quadrature_rule, order=order
|
|
613
|
+
)
|
|
614
|
+
except ValueError:
|
|
615
|
+
# Variable is not mapped to the grid (or its location is not
|
|
616
|
+
# yet supported for integration); skip it rather than failing
|
|
617
|
+
# the whole dataset-wide integration.
|
|
618
|
+
skipped.append(name)
|
|
608
619
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
"WARNING: The dataset has more than one variable, using the first variable for integration"
|
|
620
|
+
if skipped:
|
|
621
|
+
warn(
|
|
622
|
+
"The following variables were skipped during integration because "
|
|
623
|
+
"their final dimension does not map to the grid (n_face, n_node, "
|
|
624
|
+
f"or n_edge) or is not yet supported: {skipped}.",
|
|
625
|
+
UserWarning,
|
|
616
626
|
)
|
|
617
627
|
|
|
618
|
-
|
|
619
|
-
face_vals = self[var_key].to_numpy()
|
|
620
|
-
integral = np.dot(face_areas, face_vals)
|
|
621
|
-
|
|
622
|
-
return integral
|
|
628
|
+
return UxDataset(integrated_vars, uxgrid=self.uxgrid)
|
|
623
629
|
|
|
624
630
|
def to_array(
|
|
625
631
|
self,
|
|
@@ -295,59 +295,58 @@ def _compute_gradients_on_faces(
|
|
|
295
295
|
# Parallel across faces
|
|
296
296
|
for face_idx in prange(n_face):
|
|
297
297
|
gradient = np.zeros(3)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
else:
|
|
298
|
+
has_contribution = False
|
|
299
|
+
|
|
300
|
+
for node_idx in face_node_connectivity[face_idx]: # take each node on that face
|
|
301
|
+
if node_idx != INT_FILL_VALUE:
|
|
302
|
+
for edge_idx in node_edge_connectivity[
|
|
303
|
+
node_idx
|
|
304
|
+
]: # grab each edge connected to that node
|
|
305
|
+
if edge_idx != INT_FILL_VALUE:
|
|
306
|
+
# Skip edges that lack a second face neighbor
|
|
307
|
+
# instead of NaN-ing the entire face. Fixes
|
|
308
|
+
# grids where edge_face_connectivity has
|
|
309
|
+
# spurious INT_FILL_VALUE entries (e.g. SCRIP-
|
|
310
|
+
# derived SE grids like ne120np4). See #1452.
|
|
311
|
+
if INT_FILL_VALUE in edge_face_connectivity[edge_idx]:
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
if (
|
|
315
|
+
face_idx not in edge_face_connectivity[edge_idx]
|
|
316
|
+
): # check if edge connected to original face
|
|
317
|
+
face1_idx = edge_face_connectivity[edge_idx][0]
|
|
318
|
+
face2_idx = edge_face_connectivity[edge_idx][1]
|
|
319
|
+
|
|
320
|
+
face1_coords = face_coords[face1_idx]
|
|
321
|
+
face2_coords = face_coords[face2_idx]
|
|
322
|
+
|
|
323
|
+
# compute normal pointing outwards from face
|
|
324
|
+
cross = np.cross(face1_coords, face2_coords)
|
|
325
|
+
norm = np.linalg.norm(cross)
|
|
326
|
+
if np.dot(cross, face1_coords - face_coords[face_idx]) > 0:
|
|
327
|
+
normal = cross / norm
|
|
328
|
+
else:
|
|
329
|
+
normal = -cross / norm
|
|
330
|
+
|
|
331
|
+
# compute arc length between the two faces
|
|
332
|
+
arc_length = _compute_arc_length(
|
|
333
|
+
face_lat[face1_idx],
|
|
334
|
+
face_lat[face2_idx],
|
|
335
|
+
face_lon[face1_idx],
|
|
336
|
+
face_lon[face2_idx],
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# compute trapezoidal rule
|
|
340
|
+
trapz = (data[face1_idx] + data[face2_idx]) / 2
|
|
341
|
+
|
|
342
|
+
# add to the gradient (subtract correction term)
|
|
343
|
+
gradient = (
|
|
344
|
+
gradient
|
|
345
|
+
+ (trapz - data[face_idx]) * arc_length * normal
|
|
346
|
+
)
|
|
347
|
+
has_contribution = True
|
|
348
|
+
|
|
349
|
+
if not has_contribution:
|
|
351
350
|
gradient = np.full(3, np.nan)
|
|
352
351
|
|
|
353
352
|
node_neighbors = face_node_connectivity[face_idx]
|
|
@@ -104,7 +104,11 @@ def match_chunks_to_ugrid(grid_filename_or_obj, chunks):
|
|
|
104
104
|
# No need to rename
|
|
105
105
|
return chunks
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
if isinstance(grid_filename_or_obj, xr.Dataset):
|
|
108
|
+
ds = grid_filename_or_obj
|
|
109
|
+
else:
|
|
110
|
+
ds = _open_dataset_with_fallback(grid_filename_or_obj, chunks=chunks)
|
|
111
|
+
|
|
108
112
|
grid_spec, _, _ = _parse_grid_type(ds)
|
|
109
113
|
|
|
110
114
|
source_dims_dict = _get_source_dims_dict(ds, grid_spec)
|
|
@@ -302,7 +302,7 @@ class Grid:
|
|
|
302
302
|
# custom source grid spec is provided
|
|
303
303
|
source_grid_spec = kwargs.get("source_grid_spec", None)
|
|
304
304
|
grid_ds = dataset
|
|
305
|
-
source_dims_dict = {}
|
|
305
|
+
source_dims_dict = kwargs.get("source_dims_dict") or {}
|
|
306
306
|
else:
|
|
307
307
|
try:
|
|
308
308
|
if os.path.isdir(dataset):
|
|
@@ -528,9 +528,18 @@ class Grid:
|
|
|
528
528
|
if ds is not None:
|
|
529
529
|
return cls.from_dataset(ds)
|
|
530
530
|
if lon is not None and lat is not None:
|
|
531
|
-
|
|
531
|
+
# Capture the source dimension names so data variables mapped over
|
|
532
|
+
# (lon, lat) can later be flattened onto ``n_face`` by
|
|
533
|
+
# ``UxDataset.from_xarray`` (see GH #1410). ``xr.DataArray`` inputs
|
|
534
|
+
# carry their dim names; plain arrays fall back to "lon"/"lat".
|
|
535
|
+
lon_name = lon.dims[0] if isinstance(lon, xr.DataArray) else "lon"
|
|
536
|
+
lat_name = lat.dims[0] if isinstance(lat, xr.DataArray) else "lat"
|
|
537
|
+
|
|
538
|
+
grid_ds = _read_structured_grid(np.asarray(lon), np.asarray(lat), tol)
|
|
532
539
|
return cls.from_dataset(
|
|
533
|
-
grid_ds,
|
|
540
|
+
grid_ds,
|
|
541
|
+
source_dims_dict={"n_face": (lon_name, lat_name)},
|
|
542
|
+
source_grid_spec="Structured",
|
|
534
543
|
)
|
|
535
544
|
else:
|
|
536
545
|
raise ValueError(
|
|
@@ -1951,8 +1960,21 @@ class Grid:
|
|
|
1951
1960
|
-------
|
|
1952
1961
|
Sum of area of all the faces in the mesh : float
|
|
1953
1962
|
"""
|
|
1963
|
+
# Default parameters match the cached ``face_areas`` property, which also
|
|
1964
|
+
# preserves the equal-area values used for HEALPix grids; reuse it to avoid
|
|
1965
|
+
# recomputation. Any non-default quadrature settings require a fresh
|
|
1966
|
+
# geometric computation so the requested rule/order actually take effect.
|
|
1967
|
+
if (
|
|
1968
|
+
quadrature_rule == "triangular"
|
|
1969
|
+
and order == 4
|
|
1970
|
+
and not latitude_adjusted_area
|
|
1971
|
+
):
|
|
1972
|
+
return np.sum(self.face_areas.values)
|
|
1954
1973
|
|
|
1955
|
-
|
|
1974
|
+
face_areas, _ = self._compute_face_areas(
|
|
1975
|
+
quadrature_rule, order, latitude_adjusted_area
|
|
1976
|
+
)
|
|
1977
|
+
return np.sum(face_areas)
|
|
1956
1978
|
|
|
1957
1979
|
def compute_face_areas(
|
|
1958
1980
|
self,
|