Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add cf.grid_mapping_names #391

Merged
merged 60 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
5b0af86
added grid_mapping attribute
larsbuntemeyer Jan 22, 2023
015f19e
added get_grid_mapping_name function
larsbuntemeyer Jan 22, 2023
9192470
updated rotds with grid_mapping
larsbuntemeyer Jan 23, 2023
3f72bdc
renamed dummy variable
larsbuntemeyer Jan 23, 2023
9ae838a
updated grid mapping scalar
larsbuntemeyer Jan 23, 2023
252f8bf
update repr tests and rotds dims
larsbuntemeyer Jan 23, 2023
19224af
api docs update
larsbuntemeyer Jan 23, 2023
400e3dd
updated examples in grid_mappings docstring
larsbuntemeyer Jan 23, 2023
ab08961
added references
larsbuntemeyer Jan 23, 2023
824b929
remove zero dim from rotds
larsbuntemeyer Jan 23, 2023
f165325
format fix
larsbuntemeyer Jan 23, 2023
ea53b0b
added basic grid mappings section in docs
larsbuntemeyer Jan 23, 2023
b9cd3fe
update section
larsbuntemeyer Jan 23, 2023
2f09483
update docs section
larsbuntemeyer Jan 23, 2023
82d7b7c
added grid_mappgin to associated variables
larsbuntemeyer Jan 24, 2023
b0f796f
added grid_mapping accessor tests
larsbuntemeyer Jan 24, 2023
88d8c95
updated docs section
larsbuntemeyer Jan 24, 2023
6f03e4b
syntax fix
larsbuntemeyer Jan 24, 2023
37f27b8
eval-rst
larsbuntemeyer Jan 24, 2023
2615635
typo
larsbuntemeyer Jan 24, 2023
e2b18a6
fix eval-rst
larsbuntemeyer Jan 24, 2023
576f31c
polish
larsbuntemeyer Jan 24, 2023
a960700
Update cf_xarray/accessor.py
larsbuntemeyer Jan 24, 2023
8df4f0d
Update cf_xarray/accessor.py
larsbuntemeyer Jan 24, 2023
e789cce
Update cf_xarray/accessor.py
larsbuntemeyer Jan 24, 2023
e4b60bd
Update cf_xarray/accessor.py
larsbuntemeyer Jan 24, 2023
1f5fda7
Update doc/grid_mappings.md
larsbuntemeyer Jan 25, 2023
c13e7ba
updated see also sections
larsbuntemeyer Jan 25, 2023
0811b29
added warning tests
larsbuntemeyer Jan 25, 2023
dccb7f4
extract warning tests
larsbuntemeyer Jan 25, 2023
182a54c
remove skip_grid_mappings
larsbuntemeyer Jan 27, 2023
6d982ce
added dataarray grid_mapping accessor
larsbuntemeyer Jan 27, 2023
903bb9e
added dataarray grid_mapping_name
larsbuntemeyer Feb 3, 2023
aeb20be
docstrings
larsbuntemeyer Feb 3, 2023
d17d7f7
docs updates
larsbuntemeyer Feb 3, 2023
4df3c04
merge upstream/main
larsbuntemeyer Feb 3, 2023
2d0d353
fix
larsbuntemeyer Feb 3, 2023
ea45eda
skip grid_mapping coords repr
larsbuntemeyer Feb 3, 2023
38e61c7
cf.grid_mappings switchted logic
larsbuntemeyer Feb 3, 2023
0cddee4
update grid_mapping accessor logic
larsbuntemeyer Feb 3, 2023
79ebe4f
test updates
larsbuntemeyer Feb 3, 2023
bacd42a
Merge remote-tracking branch 'upstream/main' into grid_mapping
larsbuntemeyer Feb 6, 2023
bf532e5
revert changes
larsbuntemeyer Feb 6, 2023
423b9f6
fixed grid_mapping key critera
larsbuntemeyer Feb 6, 2023
49ea7b2
adapted rotds dim names
larsbuntemeyer Feb 6, 2023
ead0928
small cleanup
dcherian Feb 6, 2023
daed53e
Add "grid_mapping" as special name
dcherian Feb 6, 2023
17e0724
Swtich to .cf.grid_mapping_names
dcherian Feb 6, 2023
9f61859
Avoid DataArray.cf.grid_mapping
dcherian Feb 6, 2023
8dc1293
Avoid misleading n/a grid mapping in DataArray repr
dcherian Feb 6, 2023
4fdb192
Fix repr
dcherian Feb 6, 2023
147544b
Merge branch 'main' into grid_mapping
dcherian Feb 6, 2023
d487330
Fix typing
dcherian Feb 6, 2023
301809f
Update docs.
dcherian Feb 6, 2023
fcddfac
fix cartopy cell
dcherian Feb 6, 2023
19945f5
fix typing
dcherian Feb 6, 2023
1807e51
Add to quickstart too
dcherian Feb 6, 2023
06f6692
docs udpates
larsbuntemeyer Feb 8, 2023
998e895
Merge branch 'main' into grid_mapping
dcherian Feb 8, 2023
21e9188
Add whats-new note.
dcherian Feb 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 165 additions & 2 deletions cf_xarray/accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@
from xarray.core.rolling import Coarsen, Rolling
from xarray.core.weighted import Weighted

from .criteria import cf_role_criteria, coordinate_criteria, regex
from .criteria import (
cf_role_criteria,
coordinate_criteria,
grid_mapping_var_criteria,
regex,
)
from .helpers import _guess_bounds_1d, _guess_bounds_2d, bounds_to_vertices
from .options import OPTIONS
from .utils import (
Expand Down Expand Up @@ -369,6 +374,41 @@ def _get_bounds(obj: DataArray | Dataset, key: Hashable) -> list[Hashable]:
return list(results)


def _get_grid_mapping_name(obj: DataArray | Dataset, key: str) -> list[str]:
"""
Translate from grid mapping name attribute to appropriate variable name.
This function interprets the ``grid_mapping`` attribute on DataArrays.

Parameters
----------
obj : DataArray, Dataset
DataArray belonging to the coordinate to be checked
key : str
key to check for.

Returns
-------
List[str], Variable name(s) in parent xarray object that matches grid_mapping_name `key`
"""

if isinstance(obj, DataArray):
obj = obj._to_temp_dataset()

results = set()
for var in obj.variables:
da = obj[var]
attrs_or_encoding = ChainMap(da.attrs, da.encoding)
if "grid_mapping" in attrs_or_encoding:
grid_mapping_var_name = attrs_or_encoding["grid_mapping"]
if grid_mapping_var_name not in obj.variables:
raise ValueError(
f"{var} defines non-existing grid_mapping variable {grid_mapping_var_name}."
)
if key == obj[grid_mapping_var_name].attrs["grid_mapping_name"]:
results.update([grid_mapping_var_name])
return list(results)


def _get_with_standard_name(
obj: DataArray | Dataset, name: Hashable | Iterable[Hashable]
) -> list[Hashable]:
Expand All @@ -395,8 +435,10 @@ def _get_all(obj: DataArray | Dataset, key: Hashable) -> list[Hashable]:
all_mappers: tuple[Mapper] = (
_get_custom_criteria,
functools.partial(_get_custom_criteria, criteria=cf_role_criteria), # type: ignore
functools.partial(_get_custom_criteria, criteria=grid_mapping_var_criteria),
_get_axis_coord,
_get_measure,
_get_grid_mapping_name,
_get_with_standard_name,
)
results = apply_mapper(all_mappers, obj, key, error=False, default=None)
Expand Down Expand Up @@ -706,6 +748,15 @@ def check_results(names, key):
measures = []
warnings.warn("Ignoring bad cell_measures attribute.", UserWarning)

if isinstance(obj, Dataset):
grid_mapping_names = list(accessor.grid_mapping_names)
else:
try:
grid_mapping_names = [accessor.grid_mapping_name]
except ValueError:
grid_mapping_names = []
grid_mapping_names.append("grid_mapping")

custom_criteria = ChainMap(*OPTIONS["custom_criteria"])

varnames: list[Hashable] = []
Expand All @@ -724,6 +775,12 @@ def check_results(names, key):
successful[k] = bool(measure)
if measure:
varnames.extend(measure)
elif "grid_mapping_names" not in skip and k in grid_mapping_names:
grid_mapping = _get_all(obj, k)
check_results(grid_mapping, k)
successful[k] = bool(grid_mapping)
if grid_mapping:
varnames.extend(grid_mapping)
elif k in custom_criteria or k in cf_role_criteria:
names = _get_all(obj, k)
check_results(names, k)
Expand Down Expand Up @@ -1415,13 +1472,15 @@ def make_text_section(subtitle, attr, valid_values=None, default_keys=None):
text += make_text_section("Standard Names", "standard_names", coords)
text += make_text_section("Bounds", "bounds", coords)
if isinstance(self._obj, Dataset):
text += make_text_section("Grid Mappings", "grid_mapping_names", coords)
data_vars = self._obj.data_vars
text += "\nData Variables:"
text += make_text_section(
"Cell Measures", "cell_measures", data_vars, _CELL_MEASURES
)
text += make_text_section("Standard Names", "standard_names", data_vars)
text += make_text_section("Bounds", "bounds", data_vars)
text += make_text_section("Grid Mappings", "grid_mapping_names", data_vars)

return text

Expand All @@ -1442,6 +1501,14 @@ def keys(self) -> set[Hashable]:
varnames.extend(list(self.cell_measures))
varnames.extend(list(self.standard_names))
varnames.extend(list(self.cf_roles))
if isinstance(self._obj, xr.Dataset):
varnames.extend(list(self.grid_mapping_names))
else:
try:
gmname = self.grid_mapping_name
varnames.extend(list(gmname))
except ValueError:
pass

return set(varnames)

Expand Down Expand Up @@ -1604,6 +1671,7 @@ def get_associated_variable_names(
2. "bounds"
3. "cell_measures"
4. "coordinates"
5. "grid_mapping"
to a list of variable names referred to in the appropriate attribute

Parameters
Expand All @@ -1618,7 +1686,13 @@ def get_associated_variable_names(
names : dict
Dictionary with keys "ancillary_variables", "cell_measures", "coordinates", "bounds".
"""
keys = ["ancillary_variables", "cell_measures", "coordinates", "bounds"]
keys = [
"ancillary_variables",
"cell_measures",
"coordinates",
"bounds",
"grid_mapping",
]
coords: dict[str, list[Hashable]] = {k: [] for k in keys}
attrs_or_encoding = ChainMap(self._obj[name].attrs, self._obj[name].encoding)
larsbuntemeyer marked this conversation as resolved.
Show resolved Hide resolved

Expand Down Expand Up @@ -1660,6 +1734,9 @@ def get_associated_variable_names(
if dbounds:
coords["bounds"].append(dbounds)

if "grid_mapping" in attrs_or_encoding:
coords["grid_mapping"] = [attrs_or_encoding["grid_mapping"]]

allvars = itertools.chain(*coords.values())
missing = set(allvars) - set(self._maybe_to_dataset()._variables)
if missing:
Expand Down Expand Up @@ -2048,6 +2125,8 @@ def __getitem__(self, key: Hashable | Iterable[Hashable]) -> DataArray | Dataset
- cell measures: "area", "volume", or other names present in the \
``cell_measures`` attribute
- standard names: names present in ``standard_name`` attribute
- cf roles: 'timeseries_id', 'profile_id', 'trajectory_id', 'mesh_topology', 'grid_topology'
- grid mappings: 'grid_mapping' or a grid_mapping_name like 'rotated_latitude_longitude'

Returns
-------
Expand Down Expand Up @@ -2372,6 +2451,51 @@ def bounds_to_vertices(
)
return obj

@property
def grid_mapping_names(self) -> dict[str, list[str]]:
"""
Property that returns a dictionary mapping the CF grid mapping name
to the variable name containing the grid mapping attributes.

Returns
-------
dict
Dictionary mapping the CF grid mapping name to the grid mapping variable name.

See Also
--------
DataArray.cf.grid_mapping

References
----------
Please refer to the CF conventions document : https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#grid-mappings-and-projections

For a list of valid grid_mapping names, refer to: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#appendix-grid-mappings

Examples
--------
>>> from cf_xarray.datasets import rotds
>>> rotds.cf.grid_mapping_names
{'rotated_latitude_longitude': ['rotated_pole']}
"""

obj = self._obj
keys = set(obj.variables)

vardict = {
key: obj.variables[key].attrs["grid_mapping_name"]
for key in keys
if "grid_mapping_name" in obj.variables[key].attrs
}

results = {}
for k, v in vardict.items():
if v not in results:
results[v] = [k]
else:
results[v].append(k)
return results

def decode_vertical_coords(self, *, outnames=None, prefix=None):
"""
Decode parameterized vertical coordinates in place.
Expand Down Expand Up @@ -2547,6 +2671,43 @@ def formula_terms(self) -> dict[str, str]:
terms[key] = value
return terms

@property
def grid_mapping_name(self) -> str:
"""
Get CF grid mapping name associated with this variable.

Parameters
----------
key : str
Name of variable whose grid_mapping name is desired.

Returns
-------
str
CF Name of the associated grid mapping.

See Also
--------
Dataset.cf.grid_mapping_names

Examples
--------
>>> from cf_xarray.datasets import rotds
>>> rotds.cf["temp"].cf.grid_mapping_name
'rotated_latitude_longitude'

"""

da = self._obj

attrs_or_encoding = ChainMap(da.attrs, da.encoding)
grid_mapping = attrs_or_encoding.get("grid_mapping", None)
if not grid_mapping:
raise ValueError("No 'grid_mapping' attribute present.")

grid_mapping_var = da[grid_mapping]
return grid_mapping_var.attrs["grid_mapping_name"]

def __getitem__(self, key: Hashable | Iterable[Hashable]) -> DataArray:
"""
Index into a DataArray making use of CF attributes.
Expand All @@ -2561,6 +2722,8 @@ def __getitem__(self, key: Hashable | Iterable[Hashable]) -> DataArray:
``cell_measures`` attribute
- standard names: names present in ``standard_name`` attribute of \
coordinate variables
- cf roles: 'timeseries_id', 'profile_id', 'trajectory_id', 'mesh_topology', 'grid_topology'
- grid mappings: 'grid_mapping' or a grid_mapping_name like 'rotated_latitude_longitude'

Returns
-------
Expand Down
12 changes: 10 additions & 2 deletions cf_xarray/criteria.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
Copyright (c) 2017 MetPy Developers.
"""

try:
import regex as re
except ImportError:
import re # type: ignore

import re
from typing import Mapping, MutableMapping, Tuple
from typing import Any, Mapping, MutableMapping, Tuple

cf_role_criteria: Mapping[str, Mapping[str, str]] = {
k: {"cf_role": k}
Expand All @@ -22,6 +25,11 @@
)
}

# A grid mapping varibale is anything with a grid_mapping_name attribute
grid_mapping_var_criteria: Mapping[str, Mapping[str, Any]] = {
"grid_mapping": {"grid_mapping_name": re.compile(".")}
}

coordinate_criteria: MutableMapping[str, MutableMapping[str, Tuple]] = {
"latitude": {
"standard_name": ("latitude",),
Expand Down
63 changes: 57 additions & 6 deletions cf_xarray/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ def _create_mollw_ds():
def _create_inexact_bounds():
# Dataset that creates rotated pole curvilinear coordinates with CF bounds in
# counterclockwise order that have precision issues.
# dataset created using: https://gist.github.com/larsbuntemeyer/105d83c1eb39b1462150d3fabca0b66b
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice. In a future PR, it would be nice to add this as a "demo" notebook which creates a datasets, and illustrates creation of bounds/ extracting grid_mapping etc.

rlon = np.array([17.935, 18.045, 18.155])
rlat = np.array([21.615, 21.725, 21.835])

lon = np.array(
[
[64.21746363939087, 64.42305921561967, 64.62774455060337],
Expand Down Expand Up @@ -296,23 +300,70 @@ def _create_inexact_bounds():

rotated = xr.Dataset(
coords=dict(
rlon=xr.DataArray(
rlon,
dims="rlon",
attrs={
"units": "degrees",
"axis": "X",
"standard_name": "grid_longitude",
},
),
rlat=xr.DataArray(
rlat,
dims="rlat",
attrs={
"units": "degrees",
"axis": "Y",
"standard_name": "grid_latitude",
},
),
lon=xr.DataArray(
lon,
dims=("x", "y"),
attrs={"units": "degrees_east", "bounds": "lon_bounds"},
dims=("rlon", "rlat"),
attrs={
"units": "degrees_east",
"bounds": "lon_bounds",
"standard_name": "longitude",
},
),
lat=xr.DataArray(
lat,
dims=("x", "y"),
attrs={"units": "degrees_north", "bounds": "lat_bounds"},
dims=("rlon", "rlat"),
attrs={
"units": "degrees_north",
"bounds": "lat_bounds",
"standard_name": "latitude",
},
),
),
data_vars=dict(
lon_bounds=xr.DataArray(
lon_bounds, dims=("bounds", "x", "y"), attrs={"units": "degrees_east"}
lon_bounds,
dims=("bounds", "rlon", "rlat"),
attrs={"units": "degrees_east"},
),
lat_bounds=xr.DataArray(
lat_bounds, dims=("bounds", "x", "y"), attrs={"units": "degrees_north"}
lat_bounds,
dims=("bounds", "rlon", "rlat"),
attrs={"units": "degrees_north"},
),
rotated_pole=xr.DataArray(
np.zeros((), dtype=np.int32),
dims=None,
attrs={
"grid_mapping_name": "rotated_latitude_longitude",
"grid_north_pole_latitude": 39.25,
"grid_north_pole_longitude": -162.0,
},
),
temp=xr.DataArray(
np.random.rand(3, 3),
dims=("rlat", "rlon"),
attrs={
"standard_name": "air_temperature",
"grid_mapping": "rotated_pole",
},
),
),
)
Expand Down
Loading