cf-xarray 0.10.10__tar.gz → 0.10.11__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.10.11}/.github/workflows/ci.yaml +2 -2
  2. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/.github/workflows/pypi.yaml +3 -3
  3. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/.github/workflows/testpypi-release.yaml +2 -2
  4. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/.pre-commit-config.yaml +3 -3
  5. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/PKG-INFO +1 -1
  6. cf_xarray-0.10.11/cf_xarray/_version.py +1 -0
  7. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/accessor.py +118 -11
  8. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/datasets.py +188 -0
  9. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/tests/test_accessor.py +191 -0
  10. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray.egg-info/PKG-INFO +1 -1
  11. cf_xarray-0.10.10/cf_xarray/_version.py +0 -1
  12. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/.binder/environment.yml +0 -0
  13. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/.deepsource.toml +0 -0
  14. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/.github/dependabot.yml +0 -0
  15. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/.github/release.yml +0 -0
  16. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/.github/workflows/parse_logs.py +0 -0
  17. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/.github/workflows/upstream-dev-ci.yaml +0 -0
  18. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/.gitignore +0 -0
  19. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/.readthedocs.yml +0 -0
  20. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/.tributors +0 -0
  21. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/CITATION.cff +0 -0
  22. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/LICENSE +0 -0
  23. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/README.rst +0 -0
  24. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/__init__.py +0 -0
  25. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/coding.py +0 -0
  26. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/criteria.py +0 -0
  27. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/formatting.py +0 -0
  28. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/geometry.py +0 -0
  29. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/groupers.py +0 -0
  30. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/helpers.py +0 -0
  31. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/options.py +0 -0
  32. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/parametric.py +0 -0
  33. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/py.typed +0 -0
  34. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/scripts/make_doc.py +0 -0
  35. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/scripts/print_versions.py +0 -0
  36. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/sgrid.py +0 -0
  37. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/tests/__init__.py +0 -0
  38. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/tests/conftest.py +0 -0
  39. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/tests/test_coding.py +0 -0
  40. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/tests/test_geometry.py +0 -0
  41. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/tests/test_groupers.py +0 -0
  42. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/tests/test_helpers.py +0 -0
  43. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/tests/test_options.py +0 -0
  44. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/tests/test_parametric.py +0 -0
  45. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/tests/test_scripts.py +0 -0
  46. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/tests/test_units.py +0 -0
  47. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/units.py +0 -0
  48. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray/utils.py +0 -0
  49. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray.egg-info/SOURCES.txt +0 -0
  50. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray.egg-info/dependency_links.txt +0 -0
  51. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray.egg-info/requires.txt +0 -0
  52. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/cf_xarray.egg-info/top_level.txt +0 -0
  53. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/codecov.yml +0 -0
  54. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/2D_bounds_averaged.png +0 -0
  55. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/2D_bounds_error.png +0 -0
  56. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/2D_bounds_nonunique.png +0 -0
  57. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/Makefile +0 -0
  58. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/_static/dataset-diagram-logo.tex +0 -0
  59. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/_static/full-logo.png +0 -0
  60. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/_static/logo.png +0 -0
  61. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/_static/logo.svg +0 -0
  62. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/_static/rich-repr-example.png +0 -0
  63. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/_static/style.css +0 -0
  64. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/api.rst +0 -0
  65. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/bounds.md +0 -0
  66. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/cartopy_rotated_pole.png +0 -0
  67. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/coding.md +0 -0
  68. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/conf.py +0 -0
  69. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/contributing.rst +0 -0
  70. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/coord_axes.md +0 -0
  71. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/custom-criteria.md +0 -0
  72. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/dsg.md +0 -0
  73. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/examples/introduction.ipynb +0 -0
  74. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/faq.md +0 -0
  75. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/flags.md +0 -0
  76. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/geometry.md +0 -0
  77. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/grid_mappings.md +0 -0
  78. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/howtouse.md +0 -0
  79. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/index.rst +0 -0
  80. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/make.bat +0 -0
  81. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/parametricz.md +0 -0
  82. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/plotting.md +0 -0
  83. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/provenance.md +0 -0
  84. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/quickstart.md +0 -0
  85. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/roadmap.rst +0 -0
  86. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/selecting.md +0 -0
  87. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/sgrid_ugrid.md +0 -0
  88. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/units.md +0 -0
  89. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/doc/whats-new.rst +0 -0
  90. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/pyproject.toml +0 -0
  91. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/setup.cfg +0 -0
  92. {cf_xarray-0.10.10 → cf_xarray-0.10.11}/uv.lock +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@v5.5.2
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@v5.5.2
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@v6
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@v7
58
58
  with:
59
59
  name: releases
60
60
  path: dist
@@ -91,7 +91,7 @@ jobs:
91
91
  id-token: write
92
92
 
93
93
  steps:
94
- - uses: actions/download-artifact@v6
94
+ - uses: actions/download-artifact@v7
95
95
  with:
96
96
  name: releases
97
97
  path: dist
@@ -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@v6
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@v7
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.14.10'
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cf_xarray
3
- Version: 0.10.10
3
+ Version: 0.10.11
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.10.11"
@@ -589,7 +589,83 @@ def _create_grid_mapping(
589
589
  cf_name = var.attrs.get("grid_mapping_name", var_name)
590
590
 
591
591
  # Create CRS from the grid mapping variable
592
- crs = pyproj.CRS.from_cf(var.attrs)
592
+ if cf_name == "reduced_gaussian":
593
+ # pyproj does not recognize "reduced_gaussian" as a grid mapping name,
594
+ # but the grid uses geographic (lat/lon) coordinates on a sphere or
595
+ # spheroid. Build a geographic CRS from the earth shape parameters.
596
+ crs = pyproj.CRS.from_json_dict(
597
+ {
598
+ "$schema": "https://proj.org/schemas/v0.6/projjson.schema.json",
599
+ "type": "GeographicCRS",
600
+ "name": "Reduced Gaussian Grid",
601
+ "datum": {
602
+ "type": "GeodeticReferenceFrame",
603
+ "name": "Unknown",
604
+ "ellipsoid": {
605
+ "name": "Custom",
606
+ "semi_major_axis": var.attrs.get("semi_major_axis", 6371229.0),
607
+ "semi_minor_axis": var.attrs.get("semi_minor_axis", 6371229.0),
608
+ },
609
+ },
610
+ "coordinate_system": {
611
+ "subtype": "ellipsoidal",
612
+ "axis": [
613
+ {
614
+ "name": "Latitude",
615
+ "abbreviation": "lat",
616
+ "direction": "north",
617
+ "unit": "degree",
618
+ },
619
+ {
620
+ "name": "Longitude",
621
+ "abbreviation": "lon",
622
+ "direction": "east",
623
+ "unit": "degree",
624
+ },
625
+ ],
626
+ },
627
+ }
628
+ )
629
+ elif cf_name == "healpix":
630
+ # pyproj does not recognize "healpix" as a grid mapping name,
631
+ # but the grid uses geographic (lat/lon) coordinates on a sphere.
632
+ # Build a geographic CRS from the earth_radius parameter.
633
+ earth_radius = var.attrs.get("earth_radius", 6371229.0)
634
+ crs = pyproj.CRS.from_json_dict(
635
+ {
636
+ "$schema": "https://proj.org/schemas/v0.6/projjson.schema.json",
637
+ "type": "GeographicCRS",
638
+ "name": "HEALPix Grid",
639
+ "datum": {
640
+ "type": "GeodeticReferenceFrame",
641
+ "name": "Unknown",
642
+ "ellipsoid": {
643
+ "name": "Custom",
644
+ "semi_major_axis": earth_radius,
645
+ "semi_minor_axis": earth_radius,
646
+ },
647
+ },
648
+ "coordinate_system": {
649
+ "subtype": "ellipsoidal",
650
+ "axis": [
651
+ {
652
+ "name": "Latitude",
653
+ "abbreviation": "lat",
654
+ "direction": "north",
655
+ "unit": "degree",
656
+ },
657
+ {
658
+ "name": "Longitude",
659
+ "abbreviation": "lon",
660
+ "direction": "east",
661
+ "unit": "degree",
662
+ },
663
+ ],
664
+ },
665
+ }
666
+ )
667
+ else:
668
+ crs = pyproj.CRS.from_cf(var.attrs)
593
669
 
594
670
  # Get associated coordinate variables, fallback to dimension names
595
671
  coordinates: list[Hashable] = grid_mapping_dict.get(var_name, [])
@@ -600,16 +676,47 @@ def _create_grid_mapping(
600
676
  # The appropriate values of the standard_name depend on the grid mapping and are given in Appendix F, Grid Mappings.
601
677
  # """
602
678
  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))
679
+ if cf_name == "healpix":
680
+ # For HEALPix grids, the primary coordinate is the pixel index.
681
+ coords_found = apply_mapper(
682
+ _get_with_standard_name, ds, "healpix_index", error=False, default=[[]]
683
+ )
684
+ coordinates = list(itertools.chain(coords_found))
685
+ elif cf_name == "reduced_gaussian":
686
+ # For reduced gaussian grids, the primary coordinate is the grid
687
+ # point index. For compressed subsets, also look for the gather
688
+ # variable (with compress attribute).
689
+ idx_coords = apply_mapper(
690
+ _get_with_standard_name,
691
+ ds,
692
+ "reduced_gaussian_index",
693
+ error=False,
694
+ default=[[]],
695
+ )
696
+ coordinates = list(itertools.chain(idx_coords))
697
+ # Also find any compress/gather variable that references
698
+ # the detected index coordinate(s)
699
+ for vname in ds.coords:
700
+ if "compress" in ds[vname].attrs:
701
+ compress_target = ds[vname].attrs["compress"]
702
+ if any(c in compress_target for c in coordinates):
703
+ if vname not in coordinates:
704
+ coordinates.append(vname)
705
+ else:
706
+ if crs.to_cf().get("grid_mapping_name") == "rotated_latitude_longitude":
707
+ xname, yname = "grid_longitude", "grid_latitude"
708
+ elif crs.is_geographic:
709
+ xname, yname = "longitude", "latitude"
710
+ elif crs.is_projected:
711
+ xname, yname = "projection_x_coordinate", "projection_y_coordinate"
712
+
713
+ x = apply_mapper(
714
+ _get_with_standard_name, ds, xname, error=False, default=[[]]
715
+ )
716
+ y = apply_mapper(
717
+ _get_with_standard_name, ds, yname, error=False, default=[[]]
718
+ )
719
+ coordinates = list(itertools.chain(x, y))
613
720
 
614
721
  return GridMapping(name=cf_name, crs=crs, array=da, coordinates=tuple(coordinates))
615
722
 
@@ -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()
@@ -1264,6 +1264,132 @@ def test_grid_mappings_coordinates_attribute():
1264
1264
  )
1265
1265
 
1266
1266
 
1267
+ @requires_pyproj
1268
+ def test_reduced_gaussian_grid_mapping_global():
1269
+ """Test GridMapping integration for a full reduced gaussian grid."""
1270
+ from ..datasets import reduced_gaussian_global_ds as ds
1271
+
1272
+ # Grid mapping discovery
1273
+ assert ds.cf.grid_mapping_names == {"reduced_gaussian": ["reduced_gaussian"]}
1274
+
1275
+ # CF indexing returns the grid mapping variable
1276
+ gm_var = ds.cf["reduced_gaussian"]
1277
+ assert gm_var.attrs["grid_mapping_name"] == "reduced_gaussian"
1278
+ assert gm_var.attrs["grid_subtype"] == "octahedral"
1279
+
1280
+ # Grid mapping propagation to data variables
1281
+ da = ds.cf["air_temperature"]
1282
+ assert "reduced_gaussian" in da.coords
1283
+
1284
+ # grid_mapping_name property
1285
+ assert da.cf.grid_mapping_name == "reduced_gaussian"
1286
+
1287
+ # .cf.grid_mappings property returns valid GridMapping
1288
+ gms = ds.cf.grid_mappings
1289
+ assert len(gms) == 1
1290
+ gm = gms[0]
1291
+ assert gm.name == "reduced_gaussian"
1292
+ assert gm.crs is not None
1293
+ assert gm.crs.is_geographic
1294
+ assert gm.array.name == "reduced_gaussian"
1295
+ assert gm.array.shape == () # scalar variable
1296
+ assert isinstance(gm.coordinates, tuple)
1297
+ # Should detect reduced_gaussian_index via standard_name
1298
+ assert "reduced_gaussian_index" in gm.coordinates
1299
+
1300
+ # DataArray grid_mappings should also work
1301
+ da_gms = da.cf.grid_mappings
1302
+ assert len(da_gms) == 1
1303
+ assert da_gms[0].name == "reduced_gaussian"
1304
+ assert da_gms[0].crs.is_geographic
1305
+
1306
+ # Repr should include reduced_gaussian
1307
+ assert "reduced_gaussian" in ds.cf.__repr__()
1308
+
1309
+
1310
+ @requires_pyproj
1311
+ def test_reduced_gaussian_grid_mapping_land():
1312
+ """Test GridMapping for a land-only subset using CF compress/gather."""
1313
+ from ..datasets import reduced_gaussian_land_ds as ds
1314
+
1315
+ # Grid mapping discovery
1316
+ assert ds.cf.grid_mapping_names == {"reduced_gaussian": ["reduced_gaussian"]}
1317
+
1318
+ # Grid mapping propagation
1319
+ da = ds.cf["air_temperature"]
1320
+ assert "reduced_gaussian" in da.coords
1321
+
1322
+ # .cf.grid_mappings property
1323
+ gms = ds.cf.grid_mappings
1324
+ assert len(gms) == 1
1325
+ gm = gms[0]
1326
+ assert gm.name == "reduced_gaussian"
1327
+ assert gm.crs.is_geographic
1328
+ assert gm.array.attrs["grid_subtype"] == "octahedral"
1329
+
1330
+ # Coordinates should include the compress/gather variable
1331
+ assert "grid_points" in gm.coordinates
1332
+
1333
+ # Verify the dataset structure: data on grid_points, with compress attribute
1334
+ assert "grid_points" in ds.dims
1335
+ assert "compress" in ds.grid_points.attrs
1336
+ assert ds.grid_points.attrs["compress"] == "reduced_gaussian_index"
1337
+
1338
+
1339
+ @requires_pyproj
1340
+ def test_reduced_gaussian_grid_mapping_region():
1341
+ """Test GridMapping for a regional subset using CF compress/gather."""
1342
+ from ..datasets import reduced_gaussian_region_ds as ds
1343
+
1344
+ # Grid mapping discovery
1345
+ assert ds.cf.grid_mapping_names == {"reduced_gaussian": ["reduced_gaussian"]}
1346
+
1347
+ # Grid mapping propagation
1348
+ da = ds.cf["air_temperature"]
1349
+ assert "reduced_gaussian" in da.coords
1350
+
1351
+ # .cf.grid_mappings property
1352
+ gms = ds.cf.grid_mappings
1353
+ assert len(gms) == 1
1354
+ gm = gms[0]
1355
+ assert gm.name == "reduced_gaussian"
1356
+ assert gm.crs.is_geographic
1357
+
1358
+ # Coordinates should include both reduced_gaussian_index and grid_points
1359
+ assert "reduced_gaussian_index" in gm.coordinates
1360
+ assert "grid_points" in gm.coordinates
1361
+
1362
+ # Verify compress structure
1363
+ assert "grid_points" in ds.dims
1364
+ assert ds.grid_points.attrs["compress"] == "reduced_gaussian_index"
1365
+
1366
+ # CRS earth parameters should match the grid mapping variable
1367
+ gm_attrs = ds.reduced_gaussian.attrs
1368
+ # The CRS should be built from semi_major/minor_axis
1369
+ assert gm.crs.ellipsoid is not None
1370
+ assert gm.crs.ellipsoid.semi_major_metre == gm_attrs["semi_major_axis"]
1371
+
1372
+
1373
+ @requires_pyproj
1374
+ def test_reduced_gaussian_crs_properties():
1375
+ """Test that the CRS built for reduced_gaussian has correct properties."""
1376
+ from ..datasets import reduced_gaussian_global_ds as ds
1377
+
1378
+ gm = ds.cf.grid_mappings[0]
1379
+ crs = gm.crs
1380
+
1381
+ # Must be geographic (lat/lon on a sphere)
1382
+ assert crs.is_geographic
1383
+ assert not crs.is_projected
1384
+
1385
+ # Earth shape parameters from the grid mapping variable
1386
+ assert crs.ellipsoid.semi_major_metre == 6371229.0
1387
+ assert crs.ellipsoid.semi_minor_metre == 6371229.0
1388
+
1389
+ # Should not have an EPSG code (custom sphere)
1390
+ assert crs.to_epsg() is None
1391
+
1392
+
1267
1393
  @requires_pyproj
1268
1394
  def test_bad_grid_mapping_attribute():
1269
1395
  ds = rotds.copy(deep=False)
@@ -1285,6 +1411,71 @@ def test_bad_grid_mapping_attribute():
1285
1411
  assert grid_mappings == () # No valid grid mappings since 'foo' doesn't exist
1286
1412
 
1287
1413
 
1414
+ @requires_pyproj
1415
+ def test_healpix_grid_mapping():
1416
+ """Test GridMapping integration for HEALPix grids."""
1417
+ from ..datasets import healpix_ds as ds
1418
+
1419
+ # Grid mapping discovery
1420
+ assert ds.cf.grid_mapping_names == {"healpix": ["healpix"]}
1421
+
1422
+ # Grid mapping propagation to data variables
1423
+ da = ds.cf["air_temperature"]
1424
+ assert "healpix" in da.coords
1425
+
1426
+ # grid_mapping_name property
1427
+ assert da.cf.grid_mapping_name == "healpix"
1428
+
1429
+ # .cf.grid_mappings property on Dataset
1430
+ gms = ds.cf.grid_mappings
1431
+ assert len(gms) == 1
1432
+ gm = gms[0]
1433
+ assert gm.name == "healpix"
1434
+ assert gm.crs is not None
1435
+ assert gm.crs.is_geographic
1436
+ assert gm.array.name == "healpix"
1437
+ assert gm.array.shape == () # scalar variable
1438
+ assert isinstance(gm.coordinates, tuple)
1439
+ # Should detect healpix_index via standard_name
1440
+ assert "healpix_index" in gm.coordinates
1441
+
1442
+ # DataArray grid_mappings should also work
1443
+ da_gms = da.cf.grid_mappings
1444
+ assert len(da_gms) == 1
1445
+ assert da_gms[0].name == "healpix"
1446
+ assert da_gms[0].crs.is_geographic
1447
+
1448
+ # Repr should include healpix
1449
+ assert "healpix" in ds.cf.__repr__()
1450
+
1451
+
1452
+ @requires_pyproj
1453
+ def test_healpix_crs_properties():
1454
+ """Test that the CRS built for HEALPix has correct properties."""
1455
+ from ..datasets import healpix_ds as ds
1456
+
1457
+ gm = ds.cf.grid_mappings[0]
1458
+ crs = gm.crs
1459
+
1460
+ # Must be geographic (lat/lon on a sphere)
1461
+ assert crs.is_geographic
1462
+ assert not crs.is_projected
1463
+
1464
+ # Earth shape parameters from the grid mapping variable
1465
+ assert crs.ellipsoid.semi_major_metre == 6371000
1466
+ assert crs.ellipsoid.semi_minor_metre == 6371000
1467
+
1468
+ # Should not have an EPSG code (custom sphere)
1469
+ assert crs.to_epsg() is None
1470
+
1471
+ # Verify HEALPix-specific attributes are preserved in the grid mapping array
1472
+ gm_attrs = gm.array.attrs
1473
+ assert gm_attrs["grid_mapping_name"] == "healpix"
1474
+ assert gm_attrs["refinement_level"] == 1
1475
+ assert gm_attrs["indexing_scheme"] == "nested"
1476
+ assert gm_attrs["earth_radius"] == 6371000
1477
+
1478
+
1288
1479
  def test_docstring() -> None:
1289
1480
  assert "One of ('X'" in airds.cf.groupby.__doc__
1290
1481
  assert "Time variable accessor e.g. 'T.month'" in airds.cf.groupby.__doc__
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cf_xarray
3
- Version: 0.10.10
3
+ Version: 0.10.11
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
@@ -1 +0,0 @@
1
- __version__ = "0.10.10"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes