cf-xarray 0.10.10__tar.gz → 0.11.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.
Files changed (92) hide show
  1. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/.github/workflows/ci.yaml +2 -2
  2. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/.github/workflows/pypi.yaml +5 -5
  3. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/.github/workflows/testpypi-release.yaml +2 -2
  4. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/.pre-commit-config.yaml +4 -4
  5. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/PKG-INFO +1 -1
  6. cf_xarray-0.11.0/cf_xarray/_version.py +1 -0
  7. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/accessor.py +123 -21
  8. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/datasets.py +188 -0
  9. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/helpers.py +16 -17
  10. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/tests/test_accessor.py +241 -4
  11. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray.egg-info/PKG-INFO +1 -1
  12. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/index.rst +1 -1
  13. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/uv.lock +265 -235
  14. cf_xarray-0.10.10/cf_xarray/_version.py +0 -1
  15. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/.binder/environment.yml +0 -0
  16. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/.deepsource.toml +0 -0
  17. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/.github/dependabot.yml +0 -0
  18. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/.github/release.yml +0 -0
  19. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/.github/workflows/parse_logs.py +0 -0
  20. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/.github/workflows/upstream-dev-ci.yaml +0 -0
  21. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/.gitignore +0 -0
  22. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/.readthedocs.yml +0 -0
  23. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/.tributors +0 -0
  24. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/CITATION.cff +0 -0
  25. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/LICENSE +0 -0
  26. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/README.rst +0 -0
  27. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/__init__.py +0 -0
  28. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/coding.py +0 -0
  29. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/criteria.py +0 -0
  30. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/formatting.py +0 -0
  31. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/geometry.py +0 -0
  32. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/groupers.py +0 -0
  33. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/options.py +0 -0
  34. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/parametric.py +0 -0
  35. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/py.typed +0 -0
  36. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/scripts/make_doc.py +0 -0
  37. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/scripts/print_versions.py +0 -0
  38. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/sgrid.py +0 -0
  39. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/tests/__init__.py +0 -0
  40. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/tests/conftest.py +0 -0
  41. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/tests/test_coding.py +0 -0
  42. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/tests/test_geometry.py +0 -0
  43. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/tests/test_groupers.py +0 -0
  44. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/tests/test_helpers.py +0 -0
  45. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/tests/test_options.py +0 -0
  46. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/tests/test_parametric.py +0 -0
  47. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/tests/test_scripts.py +0 -0
  48. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/tests/test_units.py +0 -0
  49. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/units.py +0 -0
  50. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray/utils.py +0 -0
  51. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray.egg-info/SOURCES.txt +0 -0
  52. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray.egg-info/dependency_links.txt +0 -0
  53. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray.egg-info/requires.txt +0 -0
  54. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/cf_xarray.egg-info/top_level.txt +0 -0
  55. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/codecov.yml +0 -0
  56. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/2D_bounds_averaged.png +0 -0
  57. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/2D_bounds_error.png +0 -0
  58. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/2D_bounds_nonunique.png +0 -0
  59. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/Makefile +0 -0
  60. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/_static/dataset-diagram-logo.tex +0 -0
  61. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/_static/full-logo.png +0 -0
  62. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/_static/logo.png +0 -0
  63. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/_static/logo.svg +0 -0
  64. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/_static/rich-repr-example.png +0 -0
  65. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/_static/style.css +0 -0
  66. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/api.rst +0 -0
  67. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/bounds.md +0 -0
  68. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/cartopy_rotated_pole.png +0 -0
  69. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/coding.md +0 -0
  70. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/conf.py +0 -0
  71. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/contributing.rst +0 -0
  72. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/coord_axes.md +0 -0
  73. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/custom-criteria.md +0 -0
  74. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/dsg.md +0 -0
  75. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/examples/introduction.ipynb +0 -0
  76. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/faq.md +0 -0
  77. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/flags.md +0 -0
  78. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/geometry.md +0 -0
  79. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/grid_mappings.md +0 -0
  80. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/howtouse.md +0 -0
  81. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/make.bat +0 -0
  82. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/parametricz.md +0 -0
  83. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/plotting.md +0 -0
  84. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/provenance.md +0 -0
  85. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/quickstart.md +0 -0
  86. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/roadmap.rst +0 -0
  87. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/selecting.md +0 -0
  88. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/sgrid_ugrid.md +0 -0
  89. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/units.md +0 -0
  90. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/doc/whats-new.rst +0 -0
  91. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/pyproject.toml +0 -0
  92. {cf_xarray-0.10.10 → cf_xarray-0.11.0}/setup.cfg +0 -0
@@ -58,7 +58,7 @@ jobs:
58
58
  pytest -n auto --cov=./ --cov-report=xml
59
59
 
60
60
  - name: Upload code coverage to Codecov
61
- uses: codecov/codecov-action@v5.5.1
61
+ uses: codecov/codecov-action@v6.0.0
62
62
  with:
63
63
  file: ./coverage.xml
64
64
  flags: unittests
@@ -98,7 +98,7 @@ jobs:
98
98
  python -m mypy --install-types --non-interactive --cobertura-xml-report mypy_report cf_xarray/
99
99
 
100
100
  - name: Upload mypy coverage to Codecov
101
- uses: codecov/codecov-action@v5.5.1
101
+ uses: codecov/codecov-action@v6.0.0
102
102
  with:
103
103
  file: mypy_report/cobertura.xml
104
104
  flags: mypy
@@ -41,7 +41,7 @@ jobs:
41
41
  else
42
42
  echo "✅ Looks good"
43
43
  fi
44
- - uses: actions/upload-artifact@v5
44
+ - uses: actions/upload-artifact@v7
45
45
  with:
46
46
  name: releases
47
47
  path: dist
@@ -54,7 +54,7 @@ jobs:
54
54
  name: Install Python
55
55
  with:
56
56
  python-version: "3.11"
57
- - uses: actions/download-artifact@v6
57
+ - uses: actions/download-artifact@v8
58
58
  with:
59
59
  name: releases
60
60
  path: dist
@@ -72,7 +72,7 @@ jobs:
72
72
 
73
73
  - name: Publish package to TestPyPI
74
74
  if: github.event_name == 'push'
75
- uses: pypa/gh-action-pypi-publish@v1.13.0
75
+ uses: pypa/gh-action-pypi-publish@v1.14.0
76
76
  with:
77
77
  password: ${{ secrets.TESTPYPI_TOKEN }}
78
78
  repository_url: https://test.pypi.org/legacy/
@@ -91,11 +91,11 @@ jobs:
91
91
  id-token: write
92
92
 
93
93
  steps:
94
- - uses: actions/download-artifact@v6
94
+ - uses: actions/download-artifact@v8
95
95
  with:
96
96
  name: releases
97
97
  path: dist
98
98
  - name: Publish package to PyPI
99
- uses: pypa/gh-action-pypi-publish@v1.13.0
99
+ uses: pypa/gh-action-pypi-publish@v1.14.0
100
100
  with:
101
101
  verbose: true
@@ -53,7 +53,7 @@ jobs:
53
53
  echo "✅ Looks good"
54
54
  fi
55
55
 
56
- - uses: actions/upload-artifact@v5
56
+ - uses: actions/upload-artifact@v7
57
57
  with:
58
58
  name: releases
59
59
  path: dist
@@ -66,7 +66,7 @@ jobs:
66
66
  name: Install Python
67
67
  with:
68
68
  python-version: "3.11"
69
- - uses: actions/download-artifact@v6
69
+ - uses: actions/download-artifact@v8
70
70
  with:
71
71
  name: releases
72
72
  path: dist
@@ -3,14 +3,14 @@ ci:
3
3
 
4
4
  repos:
5
5
  - repo: https://github.com/asottile/pyupgrade
6
- rev: v3.20.0
6
+ rev: v3.21.2
7
7
  hooks:
8
8
  - id: pyupgrade
9
9
  args: ["--py310-plus"]
10
10
 
11
11
  - repo: https://github.com/astral-sh/ruff-pre-commit
12
12
  # Ruff version.
13
- rev: 'v0.13.3'
13
+ rev: 'v0.15.9'
14
14
  hooks:
15
15
  - id: ruff
16
16
  args: ["--fix", "--show-fixes"]
@@ -24,7 +24,7 @@ repos:
24
24
  args: ['--config', 'pyproject.toml']
25
25
 
26
26
  - repo: https://github.com/executablebooks/mdformat
27
- rev: 0.7.22
27
+ rev: 1.0.0
28
28
  hooks:
29
29
  - id: mdformat
30
30
  additional_dependencies:
@@ -55,7 +55,7 @@ repos:
55
55
  - id: validate-cff
56
56
 
57
57
  - repo: https://github.com/abravalheri/validate-pyproject
58
- rev: v0.24.1
58
+ rev: v0.25
59
59
  hooks:
60
60
  - id: validate-pyproject
61
61
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cf_xarray
3
- Version: 0.10.10
3
+ Version: 0.11.0
4
4
  Summary: A convenience wrapper for using CF attributes on xarray objects
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -0,0 +1 @@
1
+ __version__ = "0.11.0"
@@ -48,6 +48,11 @@ except ImportError:
48
48
  Weighted,
49
49
  )
50
50
 
51
+ try:
52
+ from regex import match as regex_match
53
+ except ImportError:
54
+ from re import match as regex_match # type: ignore[no-redef]
55
+
51
56
 
52
57
  from . import parametric, sgrid
53
58
  from .criteria import (
@@ -305,12 +310,6 @@ def _get_custom_criteria(
305
310
  List[str]
306
311
  Variable name(s) in parent xarray object that matches axis, coordinate, or custom ``key``
307
312
  """
308
-
309
- try:
310
- from regex import match as regex_match
311
- except ImportError:
312
- from re import match as regex_match # type: ignore[no-redef]
313
-
314
313
  if criteria is None:
315
314
  if not OPTIONS["custom_criteria"]:
316
315
  return []
@@ -589,7 +588,83 @@ def _create_grid_mapping(
589
588
  cf_name = var.attrs.get("grid_mapping_name", var_name)
590
589
 
591
590
  # Create CRS from the grid mapping variable
592
- crs = pyproj.CRS.from_cf(var.attrs)
591
+ if cf_name == "reduced_gaussian":
592
+ # pyproj does not recognize "reduced_gaussian" as a grid mapping name,
593
+ # but the grid uses geographic (lat/lon) coordinates on a sphere or
594
+ # spheroid. Build a geographic CRS from the earth shape parameters.
595
+ crs = pyproj.CRS.from_json_dict(
596
+ {
597
+ "$schema": "https://proj.org/schemas/v0.6/projjson.schema.json",
598
+ "type": "GeographicCRS",
599
+ "name": "Reduced Gaussian Grid",
600
+ "datum": {
601
+ "type": "GeodeticReferenceFrame",
602
+ "name": "Unknown",
603
+ "ellipsoid": {
604
+ "name": "Custom",
605
+ "semi_major_axis": var.attrs.get("semi_major_axis", 6371229.0),
606
+ "semi_minor_axis": var.attrs.get("semi_minor_axis", 6371229.0),
607
+ },
608
+ },
609
+ "coordinate_system": {
610
+ "subtype": "ellipsoidal",
611
+ "axis": [
612
+ {
613
+ "name": "Latitude",
614
+ "abbreviation": "lat",
615
+ "direction": "north",
616
+ "unit": "degree",
617
+ },
618
+ {
619
+ "name": "Longitude",
620
+ "abbreviation": "lon",
621
+ "direction": "east",
622
+ "unit": "degree",
623
+ },
624
+ ],
625
+ },
626
+ }
627
+ )
628
+ elif cf_name == "healpix":
629
+ # pyproj does not recognize "healpix" as a grid mapping name,
630
+ # but the grid uses geographic (lat/lon) coordinates on a sphere.
631
+ # Build a geographic CRS from the earth_radius parameter.
632
+ earth_radius = var.attrs.get("earth_radius", 6371229.0)
633
+ crs = pyproj.CRS.from_json_dict(
634
+ {
635
+ "$schema": "https://proj.org/schemas/v0.6/projjson.schema.json",
636
+ "type": "GeographicCRS",
637
+ "name": "HEALPix Grid",
638
+ "datum": {
639
+ "type": "GeodeticReferenceFrame",
640
+ "name": "Unknown",
641
+ "ellipsoid": {
642
+ "name": "Custom",
643
+ "semi_major_axis": earth_radius,
644
+ "semi_minor_axis": earth_radius,
645
+ },
646
+ },
647
+ "coordinate_system": {
648
+ "subtype": "ellipsoidal",
649
+ "axis": [
650
+ {
651
+ "name": "Latitude",
652
+ "abbreviation": "lat",
653
+ "direction": "north",
654
+ "unit": "degree",
655
+ },
656
+ {
657
+ "name": "Longitude",
658
+ "abbreviation": "lon",
659
+ "direction": "east",
660
+ "unit": "degree",
661
+ },
662
+ ],
663
+ },
664
+ }
665
+ )
666
+ else:
667
+ crs = pyproj.CRS.from_cf(var.attrs)
593
668
 
594
669
  # Get associated coordinate variables, fallback to dimension names
595
670
  coordinates: list[Hashable] = grid_mapping_dict.get(var_name, [])
@@ -600,16 +675,47 @@ def _create_grid_mapping(
600
675
  # The appropriate values of the standard_name depend on the grid mapping and are given in Appendix F, Grid Mappings.
601
676
  # """
602
677
  if not coordinates and len(grid_mapping_dict) == 1:
603
- if crs.to_cf().get("grid_mapping_name") == "rotated_latitude_longitude":
604
- xname, yname = "grid_longitude", "grid_latitude"
605
- elif crs.is_geographic:
606
- xname, yname = "longitude", "latitude"
607
- elif crs.is_projected:
608
- xname, yname = "projection_x_coordinate", "projection_y_coordinate"
609
-
610
- x = apply_mapper(_get_with_standard_name, ds, xname, error=False, default=[[]])
611
- y = apply_mapper(_get_with_standard_name, ds, yname, error=False, default=[[]])
612
- coordinates = list(itertools.chain(x, y))
678
+ if cf_name == "healpix":
679
+ # For HEALPix grids, the primary coordinate is the pixel index.
680
+ coords_found = apply_mapper(
681
+ _get_with_standard_name, ds, "healpix_index", error=False, default=[[]]
682
+ )
683
+ coordinates = list(itertools.chain(coords_found))
684
+ elif cf_name == "reduced_gaussian":
685
+ # For reduced gaussian grids, the primary coordinate is the grid
686
+ # point index. For compressed subsets, also look for the gather
687
+ # variable (with compress attribute).
688
+ idx_coords = apply_mapper(
689
+ _get_with_standard_name,
690
+ ds,
691
+ "reduced_gaussian_index",
692
+ error=False,
693
+ default=[[]],
694
+ )
695
+ coordinates = list(itertools.chain(idx_coords))
696
+ # Also find any compress/gather variable that references
697
+ # the detected index coordinate(s)
698
+ for vname in ds.coords:
699
+ if "compress" in ds[vname].attrs:
700
+ compress_target = ds[vname].attrs["compress"]
701
+ if any(c in compress_target for c in coordinates):
702
+ if vname not in coordinates:
703
+ coordinates.append(vname)
704
+ else:
705
+ if crs.to_cf().get("grid_mapping_name") == "rotated_latitude_longitude":
706
+ xname, yname = "grid_longitude", "grid_latitude"
707
+ elif crs.is_geographic:
708
+ xname, yname = "longitude", "latitude"
709
+ elif crs.is_projected:
710
+ xname, yname = "projection_x_coordinate", "projection_y_coordinate"
711
+
712
+ x = apply_mapper(
713
+ _get_with_standard_name, ds, xname, error=False, default=[[]]
714
+ )
715
+ y = apply_mapper(
716
+ _get_with_standard_name, ds, yname, error=False, default=[[]]
717
+ )
718
+ coordinates = list(itertools.chain(x, y))
613
719
 
614
720
  return GridMapping(name=cf_name, crs=crs, array=da, coordinates=tuple(coordinates))
615
721
 
@@ -1707,10 +1813,6 @@ class CFAccessor:
1707
1813
  for k, v in value.items()
1708
1814
  )
1709
1815
  )
1710
-
1711
- elif value is Ellipsis:
1712
- pass
1713
-
1714
1816
  else:
1715
1817
  # things like sum which have dim
1716
1818
  newvalue = [
@@ -784,6 +784,52 @@ try:
784
784
  np.zeros((10, 20)),
785
785
  {"standard_name": "projected_y_coordinate"},
786
786
  )
787
+ # HEALPix dataset: refinement_level=1, nside=2, 12*nside^2 = 48 pixels, nested ordering
788
+ healpix_ds = xr.Dataset(
789
+ {
790
+ "tas": xr.DataArray(
791
+ np.random.default_rng(42).standard_normal((2, 48)).astype(np.float32),
792
+ dims=["time", "healpix_index"],
793
+ attrs={
794
+ "standard_name": "air_temperature",
795
+ "units": "K",
796
+ "coordinates": "height",
797
+ "grid_mapping": "healpix",
798
+ "cell_methods": "time: mean area: mean",
799
+ },
800
+ ),
801
+ "healpix": xr.DataArray(
802
+ np.int32(0),
803
+ attrs={
804
+ "grid_mapping_name": "healpix",
805
+ "earth_radius": 6371000,
806
+ "indexing_scheme": "nested",
807
+ "refinement_level": 1,
808
+ },
809
+ ),
810
+ },
811
+ coords={
812
+ "time": xr.DataArray(
813
+ [0.0, 1.0],
814
+ dims=["time"],
815
+ attrs={
816
+ "standard_name": "time",
817
+ "calendar": "proleptic_gregorian",
818
+ "units": "days since 2025-06-01",
819
+ },
820
+ ),
821
+ "healpix_index": xr.DataArray(
822
+ np.arange(48),
823
+ dims=["healpix_index"],
824
+ attrs={"standard_name": "healpix_index"},
825
+ ),
826
+ "height": xr.DataArray(
827
+ np.float32(2.0),
828
+ attrs={"standard_name": "height", "units": "m"},
829
+ ),
830
+ },
831
+ )
832
+
787
833
  except ImportError:
788
834
  pass
789
835
 
@@ -815,3 +861,145 @@ def encoded_point_dataset():
815
861
  {"geometry": "geometry_container"},
816
862
  )
817
863
  return ds
864
+
865
+
866
+ # --- Reduced Gaussian Grid test fixtures ---
867
+ # A tiny O2 octahedral reduced gaussian grid with 4 latitudes and 40 total points.
868
+
869
+
870
+ def _create_reduced_gaussian_global():
871
+ """Create a small O2 reduced gaussian grid dataset (full global)."""
872
+ lat = np.array([59.44, 19.47, -19.47, -59.44])
873
+ pl = np.array([8, 12, 12, 8], dtype=np.int32)
874
+ total = int(np.sum(pl)) # 40
875
+
876
+ rng = np.random.default_rng(42)
877
+ temp_data = rng.standard_normal((1, 1, total)).astype(np.float32)
878
+
879
+ ds = xr.Dataset(
880
+ {
881
+ "air_temperature": xr.DataArray(
882
+ temp_data,
883
+ dims=["time", "height", "reduced_gaussian_index"],
884
+ attrs={
885
+ "grid_mapping": "reduced_gaussian",
886
+ "coordinates": "reduced_gaussian_index",
887
+ "standard_name": "air_temperature",
888
+ "units": "K",
889
+ },
890
+ ),
891
+ "reduced_gaussian": xr.DataArray(
892
+ np.int32(0),
893
+ attrs={
894
+ "grid_mapping_name": "reduced_gaussian",
895
+ "grid_subtype": "octahedral",
896
+ "points_per_latitude": "pl",
897
+ "latitude_dimension": "lat",
898
+ "semi_major_axis": 6371229.0,
899
+ "semi_minor_axis": 6371229.0,
900
+ },
901
+ ),
902
+ },
903
+ coords={
904
+ "lat": xr.DataArray(
905
+ lat,
906
+ dims=["lat"],
907
+ attrs={"standard_name": "latitude", "units": "degrees_north"},
908
+ ),
909
+ "pl": xr.DataArray(
910
+ pl,
911
+ dims=["lat"],
912
+ attrs={"long_name": "number of points per latitude"},
913
+ ),
914
+ "reduced_gaussian_index": xr.DataArray(
915
+ np.arange(total, dtype=np.int32),
916
+ dims=["reduced_gaussian_index"],
917
+ attrs={"standard_name": "reduced_gaussian_index"},
918
+ ),
919
+ "time": xr.DataArray(
920
+ [0.0], dims=["time"], attrs={"units": "hours since 2024-01-01"}
921
+ ),
922
+ "height": xr.DataArray([2.0], dims=["height"], attrs={"units": "m"}),
923
+ },
924
+ )
925
+ return ds
926
+
927
+
928
+ def _create_reduced_gaussian_land():
929
+ """Create a small O2 reduced gaussian grid with compression by gathering (land subset)."""
930
+ global_ds = _create_reduced_gaussian_global()
931
+ land_indices = np.array([2, 5, 10, 15, 20, 25, 30, 35], dtype=np.int32)
932
+
933
+ rng = np.random.default_rng(43)
934
+ temp_data = rng.standard_normal((1, 1, len(land_indices))).astype(np.float32)
935
+
936
+ ds = xr.Dataset(
937
+ {
938
+ "air_temperature": xr.DataArray(
939
+ temp_data,
940
+ dims=["time", "height", "grid_points"],
941
+ attrs={
942
+ "grid_mapping": "reduced_gaussian",
943
+ "coordinates": "time height reduced_gaussian_index",
944
+ },
945
+ ),
946
+ "reduced_gaussian": global_ds["reduced_gaussian"],
947
+ },
948
+ coords={
949
+ "lat": global_ds["lat"],
950
+ "pl": global_ds["pl"],
951
+ "reduced_gaussian_index": global_ds["reduced_gaussian_index"],
952
+ "grid_points": xr.DataArray(
953
+ land_indices,
954
+ dims=["grid_points"],
955
+ attrs={"compress": "reduced_gaussian_index"},
956
+ ),
957
+ "time": global_ds["time"],
958
+ "height": global_ds["height"],
959
+ },
960
+ )
961
+ return ds
962
+
963
+
964
+ def _create_reduced_gaussian_region():
965
+ """Create a small O2 reduced gaussian grid with compression (regional subset)."""
966
+ global_ds = _create_reduced_gaussian_global()
967
+ region_indices = np.array([8, 9, 10, 11, 12, 13, 14], dtype=np.int32)
968
+
969
+ rng = np.random.default_rng(44)
970
+ temp_data = rng.standard_normal((1, 1, len(region_indices))).astype(np.float32)
971
+
972
+ ds = xr.Dataset(
973
+ {
974
+ "air_temperature": xr.DataArray(
975
+ temp_data,
976
+ dims=["time", "height", "grid_points"],
977
+ attrs={
978
+ "grid_mapping": "reduced_gaussian",
979
+ "coordinates": "time height reduced_gaussian_index",
980
+ },
981
+ ),
982
+ "reduced_gaussian": global_ds["reduced_gaussian"],
983
+ },
984
+ coords={
985
+ "lat": global_ds["lat"],
986
+ "pl": global_ds["pl"],
987
+ "reduced_gaussian_index": global_ds["reduced_gaussian_index"],
988
+ "grid_points": xr.DataArray(
989
+ region_indices,
990
+ dims=["grid_points"],
991
+ attrs={
992
+ "standard_name": "reduced_gaussian_index",
993
+ "compress": "reduced_gaussian_index",
994
+ },
995
+ ),
996
+ "time": global_ds["time"],
997
+ "height": global_ds["height"],
998
+ },
999
+ )
1000
+ return ds
1001
+
1002
+
1003
+ reduced_gaussian_global_ds = _create_reduced_gaussian_global()
1004
+ reduced_gaussian_land_ds = _create_reduced_gaussian_land()
1005
+ reduced_gaussian_region_ds = _create_reduced_gaussian_region()
@@ -17,24 +17,23 @@ def _guess_bounds_1d(da, dim):
17
17
  """
18
18
  if dim not in da.dims:
19
19
  (dim,) = da.cf.axes[dim]
20
- ADDED_INDEX = False
21
- if dim not in da.coords:
22
- # For proper alignment in the lines below, we need an index on dim.
23
- da = da.assign_coords({dim: da[dim]})
24
- ADDED_INDEX = True
25
-
26
- diff = da.diff(dim)
27
- lower = da - diff / 2
28
- upper = da + diff / 2
29
- bounds = xr.concat([lower, upper], dim="bounds")
30
-
31
- first = (bounds.isel({dim: 0}) - diff.isel({dim: 0})).assign_coords(
32
- {dim: da[dim][0]}
20
+
21
+ bound_position = 0.5
22
+
23
+ diff = da.diff(dim).pad({dim: (1, 1)}, mode="edge")
24
+ lower = da.copy(
25
+ deep=False,
26
+ data=da.data - bound_position * diff.isel({dim: slice(0, -1)}).data,
27
+ )
28
+ upper = da.copy(
29
+ deep=False,
30
+ data=da.data + bound_position * diff.isel({dim: slice(1, None)}).data,
31
+ )
32
+ return (
33
+ xr.concat([lower, upper], dim="bounds")
34
+ .transpose(..., "bounds")
35
+ .drop_attrs(deep=False)
33
36
  )
34
- result = xr.concat([first, bounds], dim=dim).transpose(..., "bounds")
35
- if ADDED_INDEX:
36
- result = result.drop_vars(dim)
37
- return result.drop_attrs(deep=False)
38
37
 
39
38
 
40
39
  def _guess_bounds_2d(da, dims):