cf-xarray 0.10.9__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.9 → cf_xarray-0.10.11}/.github/workflows/ci.yaml +8 -8
  2. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/.github/workflows/pypi.yaml +6 -6
  3. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/.github/workflows/testpypi-release.yaml +5 -5
  4. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/.github/workflows/upstream-dev-ci.yaml +3 -3
  5. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/.pre-commit-config.yaml +4 -4
  6. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/PKG-INFO +1 -1
  7. cf_xarray-0.10.11/cf_xarray/_version.py +1 -0
  8. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/accessor.py +118 -12
  9. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/datasets.py +188 -0
  10. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/geometry.py +5 -1
  11. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/helpers.py +7 -1
  12. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/tests/test_accessor.py +194 -0
  13. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/units.py +1 -1
  14. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray.egg-info/PKG-INFO +1 -1
  15. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/pyproject.toml +0 -1
  16. cf_xarray-0.10.9/cf_xarray/_version.py +0 -1
  17. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/.binder/environment.yml +0 -0
  18. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/.deepsource.toml +0 -0
  19. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/.github/dependabot.yml +0 -0
  20. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/.github/release.yml +0 -0
  21. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/.github/workflows/parse_logs.py +0 -0
  22. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/.gitignore +0 -0
  23. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/.readthedocs.yml +0 -0
  24. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/.tributors +0 -0
  25. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/CITATION.cff +0 -0
  26. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/LICENSE +0 -0
  27. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/README.rst +0 -0
  28. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/__init__.py +0 -0
  29. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/coding.py +0 -0
  30. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/criteria.py +0 -0
  31. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/formatting.py +0 -0
  32. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/groupers.py +0 -0
  33. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/options.py +0 -0
  34. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/parametric.py +0 -0
  35. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/py.typed +0 -0
  36. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/scripts/make_doc.py +0 -0
  37. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/scripts/print_versions.py +0 -0
  38. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/sgrid.py +0 -0
  39. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/tests/__init__.py +0 -0
  40. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/tests/conftest.py +0 -0
  41. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/tests/test_coding.py +0 -0
  42. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/tests/test_geometry.py +0 -0
  43. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/tests/test_groupers.py +0 -0
  44. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/tests/test_helpers.py +0 -0
  45. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/tests/test_options.py +0 -0
  46. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/tests/test_parametric.py +0 -0
  47. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/tests/test_scripts.py +0 -0
  48. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/tests/test_units.py +0 -0
  49. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray/utils.py +0 -0
  50. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray.egg-info/SOURCES.txt +0 -0
  51. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray.egg-info/dependency_links.txt +0 -0
  52. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray.egg-info/requires.txt +0 -0
  53. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/cf_xarray.egg-info/top_level.txt +0 -0
  54. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/codecov.yml +0 -0
  55. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/2D_bounds_averaged.png +0 -0
  56. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/2D_bounds_error.png +0 -0
  57. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/2D_bounds_nonunique.png +0 -0
  58. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/Makefile +0 -0
  59. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/_static/dataset-diagram-logo.tex +0 -0
  60. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/_static/full-logo.png +0 -0
  61. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/_static/logo.png +0 -0
  62. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/_static/logo.svg +0 -0
  63. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/_static/rich-repr-example.png +0 -0
  64. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/_static/style.css +0 -0
  65. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/api.rst +0 -0
  66. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/bounds.md +0 -0
  67. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/cartopy_rotated_pole.png +0 -0
  68. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/coding.md +0 -0
  69. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/conf.py +0 -0
  70. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/contributing.rst +0 -0
  71. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/coord_axes.md +0 -0
  72. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/custom-criteria.md +0 -0
  73. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/dsg.md +0 -0
  74. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/examples/introduction.ipynb +0 -0
  75. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/faq.md +0 -0
  76. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/flags.md +0 -0
  77. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/geometry.md +0 -0
  78. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/grid_mappings.md +0 -0
  79. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/howtouse.md +0 -0
  80. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/index.rst +0 -0
  81. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/make.bat +0 -0
  82. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/parametricz.md +0 -0
  83. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/plotting.md +0 -0
  84. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/provenance.md +0 -0
  85. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/quickstart.md +0 -0
  86. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/roadmap.rst +0 -0
  87. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/selecting.md +0 -0
  88. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/sgrid_ugrid.md +0 -0
  89. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/units.md +0 -0
  90. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/doc/whats-new.rst +0 -0
  91. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/setup.cfg +0 -0
  92. {cf_xarray-0.10.9 → cf_xarray-0.10.11}/uv.lock +0 -0
@@ -34,17 +34,17 @@ jobs:
34
34
  python-version: "3.13"
35
35
  os: ubuntu-latest
36
36
  steps:
37
- - uses: actions/checkout@v5
37
+ - uses: actions/checkout@v6
38
38
  with:
39
39
  fetch-depth: 0 # Fetch all history for all branches and tags.
40
40
 
41
41
  - name: Set up Python
42
- uses: actions/setup-python@v5
42
+ uses: actions/setup-python@v6
43
43
  with:
44
44
  python-version: ${{ matrix.python-version }}
45
45
 
46
46
  - name: Install uv
47
- uses: astral-sh/setup-uv@v4
47
+ uses: astral-sh/setup-uv@v7
48
48
  with:
49
49
  enable-cache: true
50
50
 
@@ -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.0
61
+ uses: codecov/codecov-action@v5.5.2
62
62
  with:
63
63
  file: ./coverage.xml
64
64
  flags: unittests
@@ -73,17 +73,17 @@ jobs:
73
73
  matrix:
74
74
  python-version: ["3.11", "3.13"]
75
75
  steps:
76
- - uses: actions/checkout@v5
76
+ - uses: actions/checkout@v6
77
77
  with:
78
78
  fetch-depth: 0 # Fetch all history for all branches and tags.
79
79
 
80
80
  - name: Set up Python
81
- uses: actions/setup-python@v5
81
+ uses: actions/setup-python@v6
82
82
  with:
83
83
  python-version: ${{ matrix.python-version }}
84
84
 
85
85
  - name: Install uv
86
- uses: astral-sh/setup-uv@v4
86
+ uses: astral-sh/setup-uv@v7
87
87
  with:
88
88
  enable-cache: true
89
89
 
@@ -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.0
101
+ uses: codecov/codecov-action@v5.5.2
102
102
  with:
103
103
  file: mypy_report/cobertura.xml
104
104
  flags: mypy
@@ -12,10 +12,10 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
  if: github.repository == 'xarray-contrib/cf-xarray'
14
14
  steps:
15
- - uses: actions/checkout@v5
15
+ - uses: actions/checkout@v6
16
16
  with:
17
17
  fetch-depth: 0
18
- - uses: actions/setup-python@v5
18
+ - uses: actions/setup-python@v6
19
19
  name: Install Python
20
20
  with:
21
21
  python-version: "3.11"
@@ -41,7 +41,7 @@ jobs:
41
41
  else
42
42
  echo "✅ Looks good"
43
43
  fi
44
- - uses: actions/upload-artifact@v4
44
+ - uses: actions/upload-artifact@v6
45
45
  with:
46
46
  name: releases
47
47
  path: dist
@@ -50,11 +50,11 @@ jobs:
50
50
  needs: build-artifacts
51
51
  runs-on: ubuntu-latest
52
52
  steps:
53
- - uses: actions/setup-python@v5
53
+ - uses: actions/setup-python@v6
54
54
  name: Install Python
55
55
  with:
56
56
  python-version: "3.11"
57
- - uses: actions/download-artifact@v5
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@v5
94
+ - uses: actions/download-artifact@v7
95
95
  with:
96
96
  name: releases
97
97
  path: dist
@@ -17,11 +17,11 @@ jobs:
17
17
  if: ${{ contains( github.event.pull_request.labels.*.name, 'test-build') && github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }}
18
18
  runs-on: ubuntu-latest
19
19
  steps:
20
- - uses: actions/checkout@v5
20
+ - uses: actions/checkout@v6
21
21
  with:
22
22
  fetch-depth: 0
23
23
 
24
- - uses: actions/setup-python@v5
24
+ - uses: actions/setup-python@v6
25
25
  name: Install Python
26
26
  with:
27
27
  python-version: "3.11"
@@ -53,7 +53,7 @@ jobs:
53
53
  echo "✅ Looks good"
54
54
  fi
55
55
 
56
- - uses: actions/upload-artifact@v4
56
+ - uses: actions/upload-artifact@v6
57
57
  with:
58
58
  name: releases
59
59
  path: dist
@@ -62,11 +62,11 @@ jobs:
62
62
  needs: build-artifacts
63
63
  runs-on: ubuntu-latest
64
64
  steps:
65
- - uses: actions/setup-python@v5
65
+ - uses: actions/setup-python@v6
66
66
  name: Install Python
67
67
  with:
68
68
  python-version: "3.11"
69
- - uses: actions/download-artifact@v5
69
+ - uses: actions/download-artifact@v7
70
70
  with:
71
71
  name: releases
72
72
  path: dist
@@ -28,17 +28,17 @@ jobs:
28
28
  matrix:
29
29
  python-version: ["3.13"]
30
30
  steps:
31
- - uses: actions/checkout@v5
31
+ - uses: actions/checkout@v6
32
32
  with:
33
33
  fetch-depth: 0 # Fetch all history for all branches and tags.
34
34
 
35
35
  - name: Set up Python
36
- uses: actions/setup-python@v5
36
+ uses: actions/setup-python@v6
37
37
  with:
38
38
  python-version: ${{ matrix.python-version }}
39
39
 
40
40
  - name: Install uv
41
- uses: astral-sh/setup-uv@v4
41
+ uses: astral-sh/setup-uv@v7
42
42
 
43
43
  - name: Install dependencies
44
44
  run: |
@@ -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.12.2'
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:
@@ -41,7 +41,7 @@ repos:
41
41
  additional_dependencies: [mdformat==0.7.17]
42
42
 
43
43
  - repo: https://github.com/pre-commit/pre-commit-hooks
44
- rev: v5.0.0
44
+ rev: v6.0.0
45
45
  hooks:
46
46
  - id: trailing-whitespace
47
47
  - id: end-of-file-fixer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cf_xarray
3
- Version: 0.10.9
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
 
@@ -796,7 +903,6 @@ def _guess_bounds(da, dim=None, out_dim="bounds"):
796
903
  f"If dim is None, variable {da.name} must be 1D or 2D. Received {da.ndim}D variable instead."
797
904
  )
798
905
  dim = da.dims
799
-
800
906
  if not isinstance(dim, str):
801
907
  if len(dim) > 2:
802
908
  raise NotImplementedError(
@@ -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()
@@ -586,7 +586,11 @@ def points_to_cf(
586
586
  coord = None
587
587
  pts_ = pts
588
588
 
589
- x, y, node_count, crdX, crdY = [], [], [], [], []
589
+ x: list[np.ndarray] = []
590
+ y: list[np.ndarray] = []
591
+ node_count: list[int] = []
592
+ crdX: list[float] = []
593
+ crdY: list[float] = []
590
594
  for pt in pts_:
591
595
  if isinstance(pt, MultiPoint):
592
596
  xy = np.concatenate([p.coords for p in pt.geoms])
@@ -34,7 +34,7 @@ def _guess_bounds_1d(da, dim):
34
34
  result = xr.concat([first, bounds], dim=dim).transpose(..., "bounds")
35
35
  if ADDED_INDEX:
36
36
  result = result.drop_vars(dim)
37
- return result
37
+ return result.drop_attrs(deep=False)
38
38
 
39
39
 
40
40
  def _guess_bounds_2d(da, dims):
@@ -338,6 +338,12 @@ def _get_ordered_vertices(
338
338
  elif order == "descending":
339
339
  endpoints = np.maximum(bounds[..., :, 0], bounds[..., :, 1])
340
340
  last_endpoint = np.minimum(bounds[..., -1, 0], bounds[..., -1, 1])
341
+ else:
342
+ raise NotImplementedError(
343
+ f"Cannot determine vertices for non-monotonic bounds with {order} core "
344
+ "dimension orders. Try normalizing the coordinates to a monotonic "
345
+ "convention and try again."
346
+ )
341
347
 
342
348
  vertices = np.concatenate(
343
349
  [endpoints, np.expand_dims(last_endpoint, axis=-1)], axis=-1
@@ -832,6 +832,9 @@ def test_add_bounds(dims):
832
832
  assert_allclose(
833
833
  added[name].reset_coords(drop=True), expected[dim].transpose(..., "bounds")
834
834
  )
835
+ if dim == "lat":
836
+ # The CF axes shouldn't have changed
837
+ assert added.cf.axes["Y"] == ["lat"]
835
838
 
836
839
  _check_unchanged(original, ds)
837
840
 
@@ -1261,6 +1264,132 @@ def test_grid_mappings_coordinates_attribute():
1261
1264
  )
1262
1265
 
1263
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
+
1264
1393
  @requires_pyproj
1265
1394
  def test_bad_grid_mapping_attribute():
1266
1395
  ds = rotds.copy(deep=False)
@@ -1282,6 +1411,71 @@ def test_bad_grid_mapping_attribute():
1282
1411
  assert grid_mappings == () # No valid grid mappings since 'foo' doesn't exist
1283
1412
 
1284
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
+
1285
1479
  def test_docstring() -> None:
1286
1480
  assert "One of ('X'" in airds.cf.groupby.__doc__
1287
1481
  assert "Time variable accessor e.g. 'T.month'" in airds.cf.groupby.__doc__
@@ -60,7 +60,7 @@ def short_formatter(unit, registry, **options):
60
60
  # Reused with modification from MetPy under the terms of the BSD 3-Clause License.
61
61
  # Copyright (c) 2015,2017,2019 MetPy Developers.
62
62
  # Create registry, with preprocessors for UDUNITS-style powers (m2 s-2) and percent signs
63
- units = pint.UnitRegistry(
63
+ units: pint.UnitRegistry = pint.UnitRegistry(
64
64
  autoconvert_offset_to_baseunit=True,
65
65
  preprocessors=[
66
66
  functools.partial(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cf_xarray
3
- Version: 0.10.9
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
@@ -107,7 +107,6 @@ docstring-code-format = true
107
107
 
108
108
 
109
109
  [tool.pytest]
110
- python_files = "test_*.py"
111
110
  testpaths = ["cf_xarray/tests"]
112
111
 
113
112
  [tool.rstcheck]
@@ -1 +0,0 @@
1
- __version__ = "0.10.9"
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
File without changes
File without changes