uxarray 2026.4.1__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.4.1 → uxarray-2026.7.0}/PKG-INFO +4 -4
- {uxarray-2026.4.1 → uxarray-2026.7.0}/pyproject.toml +4 -3
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/__init__.py +2 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/core/api.py +70 -15
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/core/dataarray.py +241 -24
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/core/dataset.py +58 -31
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/core/gradient.py +78 -54
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/core/utils.py +5 -1
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/core/zonal.py +230 -71
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/coordinates.py +84 -1
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/geometry.py +6 -6
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/grid.py +29 -4
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_scrip.py +62 -14
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/utils.py +1 -1
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/plot/accessor.py +42 -7
- uxarray-2026.7.0/uxarray/plot/utils.py +72 -0
- uxarray-2026.7.0/uxarray/remap/__init__.py +13 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/remap/accessor.py +150 -1
- uxarray-2026.7.0/uxarray/remap/apply_weights.py +120 -0
- uxarray-2026.7.0/uxarray/remap/structured.py +216 -0
- uxarray-2026.7.0/uxarray/remap/weights.py +194 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/remap/yac.py +144 -10
- uxarray-2026.7.0/uxarray/tutorial/__init__.py +208 -0
- uxarray-2026.7.0/uxarray/tutorial/registry.py +102 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray.egg-info/PKG-INFO +4 -4
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray.egg-info/SOURCES.txt +7 -0
- {uxarray-2026.4.1 → 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.4.1/uxarray/plot/utils.py +0 -40
- uxarray-2026.4.1/uxarray/remap/__init__.py +0 -7
- {uxarray-2026.4.1 → uxarray-2026.7.0}/.codecov.yml +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/.git_archival.txt +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/.gitattributes +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/CITATION.cff +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/CODE_OF_CONDUCT.md +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/CONTRIBUTING.md +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/INSTALLATION.md +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/LICENSE +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/MANIFEST.in +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/README.md +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/build.sh +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/setup.cfg +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/setup.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/constants.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/conventions/__init__.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/conventions/descriptors.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/conventions/ugrid.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/core/__init__.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/core/accessors.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/core/aggregation.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/cross_sections/__init__.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/cross_sections/dataarray_accessor.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/cross_sections/grid_accessor.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/cross_sections/sample.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/formatting_html.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/__init__.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/arcs.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/area.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/bounds.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/connectivity.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/dual.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/integrate.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/intersections.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/neighbors.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/point_in_face.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/slice.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/utils.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/grid/validation.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/__init__.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_delaunay.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_esmf.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_exodus.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_fesom2.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_geopandas.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_geos.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_healpix.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_icon.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_mpas.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_structured.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_topology.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_ugrid.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_vertices.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/io/_voronoi.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/plot/__init__.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/plot/constants.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/plot/matplotlib.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/remap/bilinear.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/remap/inverse_distance_weighted.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/remap/nearest_neighbor.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/remap/spatial_coords_remap.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/remap/utils.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/subset/__init__.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/subset/dataarray_accessor.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/subset/grid_accessor.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/utils/__init__.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray/utils/computing.py +0 -0
- {uxarray-2026.4.1 → uxarray-2026.7.0}/uxarray.egg-info/dependency_links.txt +0 -0
- {uxarray-2026.4.1 → 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/"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from . import tutorial
|
|
1
2
|
from .constants import INT_DTYPE, INT_FILL_VALUE
|
|
2
3
|
from .core.api import (
|
|
3
4
|
concat,
|
|
@@ -30,6 +31,7 @@ __all__ = (
|
|
|
30
31
|
"open_multigrid",
|
|
31
32
|
"list_grid_names",
|
|
32
33
|
"concat",
|
|
34
|
+
"tutorial",
|
|
33
35
|
"UxDataset",
|
|
34
36
|
"UxDataArray",
|
|
35
37
|
"INT_DTYPE",
|
|
@@ -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(
|
|
@@ -24,6 +24,7 @@ from uxarray.core.utils import _map_dims_to_ugrid
|
|
|
24
24
|
from uxarray.core.zonal import (
|
|
25
25
|
_compute_conservative_zonal_mean_bands,
|
|
26
26
|
_compute_non_conservative_zonal_mean,
|
|
27
|
+
_compute_zonal_anomaly,
|
|
27
28
|
)
|
|
28
29
|
from uxarray.cross_sections import UxDataArrayCrossSectionAccessor
|
|
29
30
|
from uxarray.formatting_html import array_repr
|
|
@@ -163,6 +164,40 @@ class UxDataArray(xr.DataArray):
|
|
|
163
164
|
else:
|
|
164
165
|
return None
|
|
165
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
|
+
|
|
166
201
|
def to_geodataframe(
|
|
167
202
|
self,
|
|
168
203
|
periodic_elements: str | None = "exclude",
|
|
@@ -465,12 +500,7 @@ class UxDataArray(xr.DataArray):
|
|
|
465
500
|
else:
|
|
466
501
|
|
|
467
502
|
def _is_default_extent() -> bool:
|
|
468
|
-
|
|
469
|
-
# when autoscale is still on (no extent has been explicitly set)
|
|
470
|
-
if not ax.get_autoscale_on():
|
|
471
|
-
return False
|
|
472
|
-
xlim, ylim = ax.get_xlim(), ax.get_ylim()
|
|
473
|
-
return np.allclose(xlim, (0.0, 1.0)) and np.allclose(ylim, (0.0, 1.0))
|
|
503
|
+
return ax.get_autoscale_on()
|
|
474
504
|
|
|
475
505
|
if _is_default_extent():
|
|
476
506
|
try:
|
|
@@ -772,6 +802,70 @@ class UxDataArray(xr.DataArray):
|
|
|
772
802
|
"""Alias of zonal_mean; prefer `zonal_mean` for primary API."""
|
|
773
803
|
return self.zonal_mean(lat=lat, conservative=conservative, **kwargs)
|
|
774
804
|
|
|
805
|
+
def zonal_anomaly(self, lat=(-90, 90, 10), conservative: bool = False):
|
|
806
|
+
"""Compute the zonal anomaly: each face value minus the mean of its latitude band.
|
|
807
|
+
|
|
808
|
+
Returns a new ``UxDataArray`` with the same dimensions as the input,
|
|
809
|
+
where each face holds its original value minus the zonal mean of the
|
|
810
|
+
latitude band it belongs to.
|
|
811
|
+
|
|
812
|
+
Parameters
|
|
813
|
+
----------
|
|
814
|
+
lat : tuple or array-like, default=(-90, 90, 10)
|
|
815
|
+
Latitude band specification:
|
|
816
|
+
- tuple (start, end, step): band edges via np.linspace(start, end, n)
|
|
817
|
+
- array-like: explicit band edges in degrees
|
|
818
|
+
conservative : bool, default=False
|
|
819
|
+
If True, uses area-weighted band means and blends across bands for
|
|
820
|
+
faces that straddle a band boundary, reusing the face-band weight
|
|
821
|
+
matrix computed for zonal_mean so no geometry is duplicated.
|
|
822
|
+
If False, assigns each face to a band by its centroid latitude.
|
|
823
|
+
|
|
824
|
+
Returns
|
|
825
|
+
-------
|
|
826
|
+
UxDataArray
|
|
827
|
+
Same dimensions as input with per-face band mean subtracted.
|
|
828
|
+
|
|
829
|
+
Examples
|
|
830
|
+
--------
|
|
831
|
+
>>> uxds["var"].zonal_anomaly()
|
|
832
|
+
>>> uxds["var"].zonal_anomaly(lat=(-60, 60, 5), conservative=True)
|
|
833
|
+
"""
|
|
834
|
+
if not self._face_centered():
|
|
835
|
+
raise ValueError(
|
|
836
|
+
"Zonal anomaly is only supported for face-centered data variables."
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
if isinstance(lat, tuple):
|
|
840
|
+
start, end, step = lat
|
|
841
|
+
if step <= 0:
|
|
842
|
+
raise ValueError("Step size must be positive.")
|
|
843
|
+
num_points = int(round((end - start) / step)) + 1
|
|
844
|
+
edges = np.linspace(start, end, num_points)
|
|
845
|
+
edges = np.clip(edges, -90, 90)
|
|
846
|
+
elif isinstance(lat, (list, np.ndarray)):
|
|
847
|
+
edges = np.asarray(lat, dtype=float)
|
|
848
|
+
else:
|
|
849
|
+
raise ValueError(
|
|
850
|
+
"Invalid value for 'lat'. Must be a tuple (start, end, step) or array-like band edges."
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
if edges.ndim != 1 or edges.size < 2:
|
|
854
|
+
raise ValueError("Band edges must be 1D with at least two values.")
|
|
855
|
+
|
|
856
|
+
res = _compute_zonal_anomaly(self, edges, conservative=conservative)
|
|
857
|
+
|
|
858
|
+
return UxDataArray(
|
|
859
|
+
res,
|
|
860
|
+
dims=self.dims,
|
|
861
|
+
coords=self.coords,
|
|
862
|
+
name=self.name + "_zonal_anomaly"
|
|
863
|
+
if self.name is not None
|
|
864
|
+
else "zonal_anomaly",
|
|
865
|
+
attrs={"zonal_anomaly": True, "conservative": conservative},
|
|
866
|
+
uxgrid=self.uxgrid,
|
|
867
|
+
)
|
|
868
|
+
|
|
775
869
|
def azimuthal_mean(
|
|
776
870
|
self,
|
|
777
871
|
center_coord,
|
|
@@ -1416,22 +1510,40 @@ class UxDataArray(xr.DataArray):
|
|
|
1416
1510
|
"""
|
|
1417
1511
|
return _uxda_grid_aggregate(self, destination, "any", **kwargs)
|
|
1418
1512
|
|
|
1419
|
-
def gradient(self, **kwargs) -> UxDataset:
|
|
1513
|
+
def gradient(self, scale_by_radius: bool = True, **kwargs) -> UxDataset:
|
|
1420
1514
|
"""
|
|
1421
1515
|
Computes the gradient of a data variable.
|
|
1422
1516
|
|
|
1517
|
+
Parameters
|
|
1518
|
+
----------
|
|
1519
|
+
scale_by_radius : bool, default=True
|
|
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.
|
|
1526
|
+
|
|
1423
1527
|
Returns
|
|
1424
1528
|
-------
|
|
1425
1529
|
gradient: UxDataset
|
|
1426
|
-
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``.
|
|
1427
1534
|
|
|
1428
1535
|
Notes
|
|
1429
1536
|
-----
|
|
1430
|
-
The Green-Gauss
|
|
1537
|
+
The Green-Gauss theorem is utilized, where a closed control volume around each cell
|
|
1431
1538
|
is formed connecting centroids of the neighboring cells. The surface integral is
|
|
1432
1539
|
approximated using the trapezoidal rule. The sum of the contributions is then
|
|
1433
1540
|
normalized by the cell volume.
|
|
1434
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
|
+
|
|
1435
1547
|
Example
|
|
1436
1548
|
-------
|
|
1437
1549
|
>>> uxds["var"].gradient()
|
|
@@ -1446,7 +1558,9 @@ class UxDataArray(xr.DataArray):
|
|
|
1446
1558
|
)
|
|
1447
1559
|
|
|
1448
1560
|
# Compute the zonal and meridional gradient components of the stored data variable
|
|
1449
|
-
grad_zonal_da, grad_meridional_da = _compute_gradient(
|
|
1561
|
+
grad_zonal_da, grad_meridional_da = _compute_gradient(
|
|
1562
|
+
self, scale_by_radius=scale_by_radius
|
|
1563
|
+
)
|
|
1450
1564
|
|
|
1451
1565
|
# Create a dataset containing both gradient components
|
|
1452
1566
|
return UxDataset(
|
|
@@ -1459,7 +1573,9 @@ class UxDataArray(xr.DataArray):
|
|
|
1459
1573
|
coords=self.coords,
|
|
1460
1574
|
)
|
|
1461
1575
|
|
|
1462
|
-
def curl(
|
|
1576
|
+
def curl(
|
|
1577
|
+
self, other: "UxDataArray", scale_by_radius: bool = True, **kwargs
|
|
1578
|
+
) -> "UxDataArray":
|
|
1463
1579
|
"""
|
|
1464
1580
|
Computes the curl of a vector field.
|
|
1465
1581
|
|
|
@@ -1469,6 +1585,11 @@ class UxDataArray(xr.DataArray):
|
|
|
1469
1585
|
The second component of the vector field. This UxDataArray should
|
|
1470
1586
|
represent the meridional (v) component, while self represents the
|
|
1471
1587
|
zonal (u) component.
|
|
1588
|
+
scale_by_radius : bool, default=True
|
|
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).
|
|
1472
1593
|
**kwargs : dict
|
|
1473
1594
|
Additional keyword arguments (currently unused, reserved for future extensions).
|
|
1474
1595
|
|
|
@@ -1476,7 +1597,9 @@ class UxDataArray(xr.DataArray):
|
|
|
1476
1597
|
-------
|
|
1477
1598
|
curl : UxDataArray
|
|
1478
1599
|
The curl of the vector field (u, v), computed as:
|
|
1479
|
-
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``.
|
|
1480
1603
|
|
|
1481
1604
|
Notes
|
|
1482
1605
|
-----
|
|
@@ -1519,21 +1642,30 @@ class UxDataArray(xr.DataArray):
|
|
|
1519
1642
|
)
|
|
1520
1643
|
|
|
1521
1644
|
# Compute gradients of both components
|
|
1522
|
-
grad_u_zonal, grad_u_meridional = _compute_gradient(
|
|
1523
|
-
|
|
1645
|
+
grad_u_zonal, grad_u_meridional = _compute_gradient(
|
|
1646
|
+
self, scale_by_radius=scale_by_radius
|
|
1647
|
+
)
|
|
1648
|
+
grad_v_zonal, grad_v_meridional = _compute_gradient(
|
|
1649
|
+
other, scale_by_radius=scale_by_radius
|
|
1650
|
+
)
|
|
1524
1651
|
|
|
1525
1652
|
# Compute curl = ∂v/∂x - ∂u/∂y
|
|
1526
1653
|
curl_values = grad_v_zonal.values - grad_u_meridional.values
|
|
1527
1654
|
|
|
1655
|
+
u_units = self.attrs.get("units", "")
|
|
1656
|
+
has_sphere_radius = "sphere_radius" in self.uxgrid._ds.attrs
|
|
1657
|
+
if scale_by_radius and has_sphere_radius:
|
|
1658
|
+
curl_units = f"({u_units})/m" if u_units else "1/m"
|
|
1659
|
+
else:
|
|
1660
|
+
curl_units = f"({u_units})/rad" if u_units else "1/rad"
|
|
1661
|
+
|
|
1528
1662
|
# Create the result UxDataArray
|
|
1529
1663
|
curl_da = UxDataArray(
|
|
1530
1664
|
curl_values,
|
|
1531
1665
|
dims=self.dims,
|
|
1532
1666
|
attrs={
|
|
1533
1667
|
"long_name": f"Curl of ({self.name}, {other.name})",
|
|
1534
|
-
"units":
|
|
1535
|
-
if "units" not in self.attrs
|
|
1536
|
-
else f"({self.attrs.get('units', '1')})/m",
|
|
1668
|
+
"units": curl_units,
|
|
1537
1669
|
"description": "Curl of vector field computed as ∂v/∂x - ∂u/∂y",
|
|
1538
1670
|
},
|
|
1539
1671
|
uxgrid=self.uxgrid,
|
|
@@ -1542,16 +1674,26 @@ class UxDataArray(xr.DataArray):
|
|
|
1542
1674
|
|
|
1543
1675
|
return curl_da
|
|
1544
1676
|
|
|
1545
|
-
def divergence(
|
|
1677
|
+
def divergence(
|
|
1678
|
+
self, other: "UxDataArray", scale_by_radius: bool = True, **kwargs
|
|
1679
|
+
) -> "UxDataArray":
|
|
1546
1680
|
"""
|
|
1547
1681
|
Computes the divergence of the vector field defined by this UxDataArray and other.
|
|
1548
1682
|
|
|
1549
1683
|
Parameters
|
|
1550
1684
|
----------
|
|
1551
1685
|
other : UxDataArray
|
|
1552
|
-
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).
|
|
1553
1694
|
**kwargs
|
|
1554
|
-
Additional keyword arguments
|
|
1695
|
+
Additional keyword arguments. ``units`` may be passed to override the
|
|
1696
|
+
automatically inferred units.
|
|
1555
1697
|
|
|
1556
1698
|
Returns
|
|
1557
1699
|
-------
|
|
@@ -1565,7 +1707,9 @@ class UxDataArray(xr.DataArray):
|
|
|
1565
1707
|
the divergence is calculated as div(V) = ∂u/∂x + ∂v/∂y.
|
|
1566
1708
|
|
|
1567
1709
|
The implementation uses edge-centered gradients and face-centered divergence calculation
|
|
1568
|
-
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.
|
|
1569
1713
|
|
|
1570
1714
|
Example
|
|
1571
1715
|
-------
|
|
@@ -1595,8 +1739,8 @@ class UxDataArray(xr.DataArray):
|
|
|
1595
1739
|
)
|
|
1596
1740
|
|
|
1597
1741
|
# Compute gradients of both components
|
|
1598
|
-
u_gradient = self.gradient()
|
|
1599
|
-
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)
|
|
1600
1744
|
|
|
1601
1745
|
# For divergence: div(V) = ∂u/∂x + ∂v/∂y
|
|
1602
1746
|
# We use the zonal gradient (∂/∂lon) of u and meridional gradient (∂/∂lat) of v
|
|
@@ -1607,10 +1751,24 @@ class UxDataArray(xr.DataArray):
|
|
|
1607
1751
|
u, v = xr.align(u, v)
|
|
1608
1752
|
divergence = u + v
|
|
1609
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
|
+
|
|
1610
1768
|
divergence.attrs.update(
|
|
1611
1769
|
{
|
|
1612
1770
|
"divergence": True,
|
|
1613
|
-
"units":
|
|
1771
|
+
"units": div_units,
|
|
1614
1772
|
}
|
|
1615
1773
|
)
|
|
1616
1774
|
|
|
@@ -1619,6 +1777,65 @@ class UxDataArray(xr.DataArray):
|
|
|
1619
1777
|
|
|
1620
1778
|
return divergence_da
|
|
1621
1779
|
|
|
1780
|
+
def scalardotgradient(self, v: "UxDataArray", q: "UxDataArray") -> "UxDataArray":
|
|
1781
|
+
"""
|
|
1782
|
+
Compute the dot product between a vector field and the gradient of a scalar field.
|
|
1783
|
+
|
|
1784
|
+
Parameters
|
|
1785
|
+
----------
|
|
1786
|
+
v : UxDataArray
|
|
1787
|
+
The meridional component of the vector field. ``self`` is treated as
|
|
1788
|
+
the zonal component.
|
|
1789
|
+
q : UxDataArray
|
|
1790
|
+
Scalar field whose gradient is dotted with the vector field.
|
|
1791
|
+
|
|
1792
|
+
Returns
|
|
1793
|
+
-------
|
|
1794
|
+
scalar_dot_gradient : UxDataArray
|
|
1795
|
+
Dot product ``self * dq/dx + v * dq/dy``.
|
|
1796
|
+
"""
|
|
1797
|
+
if not isinstance(v, UxDataArray):
|
|
1798
|
+
raise TypeError("v must be a UxDataArray")
|
|
1799
|
+
|
|
1800
|
+
if not isinstance(q, UxDataArray):
|
|
1801
|
+
raise TypeError("q must be a UxDataArray")
|
|
1802
|
+
|
|
1803
|
+
if self.uxgrid != v.uxgrid or self.uxgrid != q.uxgrid:
|
|
1804
|
+
raise ValueError("All UxDataArrays must have the same grid")
|
|
1805
|
+
|
|
1806
|
+
if self.dims != v.dims or self.dims != q.dims:
|
|
1807
|
+
raise ValueError("All UxDataArrays must have the same dimensions")
|
|
1808
|
+
|
|
1809
|
+
if self.ndim > 1:
|
|
1810
|
+
raise ValueError(
|
|
1811
|
+
"Scalar dot gradient currently requires 1D face-centered data. "
|
|
1812
|
+
"Consider selecting a single slice before computing."
|
|
1813
|
+
)
|
|
1814
|
+
|
|
1815
|
+
if not (self._face_centered() and v._face_centered() and q._face_centered()):
|
|
1816
|
+
raise ValueError(
|
|
1817
|
+
"Computing the scalar dot gradient is only supported for face-centered data variables."
|
|
1818
|
+
)
|
|
1819
|
+
|
|
1820
|
+
# Validate coordinate alignment up-front so a misaligned input fails
|
|
1821
|
+
# before the (potentially expensive) gradient call.
|
|
1822
|
+
u_aligned, v_aligned, q_aligned = xr.align(self, v, q, join="exact", copy=False)
|
|
1823
|
+
|
|
1824
|
+
q_gradient = q_aligned.gradient()
|
|
1825
|
+
q_zonal = q_gradient["zonal_gradient"]
|
|
1826
|
+
q_meridional = q_gradient["meridional_gradient"]
|
|
1827
|
+
|
|
1828
|
+
scalar_dot_gradient = (u_aligned * q_zonal) + (v_aligned * q_meridional)
|
|
1829
|
+
scalar_dot_gradient.name = "scalar_dot_gradient"
|
|
1830
|
+
scalar_dot_gradient.attrs.update(
|
|
1831
|
+
{
|
|
1832
|
+
"long_name": "scalar dot gradient",
|
|
1833
|
+
"description": "Dot product u * (dq/dx) + v * (dq/dy).",
|
|
1834
|
+
}
|
|
1835
|
+
)
|
|
1836
|
+
|
|
1837
|
+
return UxDataArray(scalar_dot_gradient, uxgrid=self.uxgrid)
|
|
1838
|
+
|
|
1622
1839
|
def difference(self, destination: str | None = "edge"):
|
|
1623
1840
|
"""Computes the absolute difference of a data variable.
|
|
1624
1841
|
|