Skip to content

Commit 7c49e72

Browse files
committed
Follow CF Conventions when finding latitude / longitude coordinate variables
1 parent 3ffa3f2 commit 7c49e72

File tree

4 files changed

+150
-8
lines changed

4 files changed

+150
-8
lines changed

docs/releases/development.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Next release (in development)
1111
* Added :func:`emsarray.utils.timed_func` for easily logging some performance metrics (:pr:`79`).
1212
* Add :attr:`.Convention.bounds` and :attr:`.Convention.geometry` attributes (:pr:`83`).
1313
* Fix a number of numpy warnings about unsafe casts (:pr:`85`).
14+
* Follow CF Conventions properly when finding latitude / longitude coordinate variables (:issue:`84`, :pr:`86`)

src/emsarray/conventions/grid.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ class CFGridKind(str, enum.Enum):
3535
CFGridIndex = Tuple[int, int]
3636

3737

38+
CF_LATITUDE_UNITS = {
39+
'degrees_north', 'degree_north', 'degree_N', 'degrees_N',
40+
'degreeN', 'degreesN'
41+
}
42+
43+
CF_LONGITUDE_UNITS = {
44+
'degrees_east', 'degree_east', 'degree_E', 'degrees_E',
45+
'degreeE', 'degreesE'
46+
}
47+
48+
3849
class CFGridTopology(abc.ABC):
3950
"""
4051
A topology helper that keeps track of the latitude and longitude coordinates
@@ -69,14 +80,18 @@ def latitude_name(self) -> Hashable:
6980
``standard_name = "latitude"`` or
7081
``units = "degree_north"``
7182
attribute.
83+
84+
See also
85+
--------
86+
https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#latitude-coordinate
7287
"""
7388
try:
7489
return next(
7590
name
7691
for name, variable in self.dataset.variables.items()
77-
if variable.attrs.get('standard_name') == 'latitude'
78-
or variable.attrs.get('coordinate_type') == 'latitude'
79-
or variable.attrs.get('units') == 'degree_north'
92+
if variable.attrs.get('units') in CF_LATITUDE_UNITS
93+
or variable.attrs.get('standard_name') == 'latitude'
94+
or variable.attrs.get('axis') == 'Y'
8095
)
8196
except StopIteration:
8297
raise ValueError("Could not find latitude coordinate")
@@ -89,13 +104,17 @@ def longitude_name(self) -> Hashable:
89104
``standard_name = "longitude"`` or
90105
``units = "degree_east"``
91106
attribute.
107+
108+
See also
109+
--------
110+
https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#longitude-coordinate
92111
"""
93112
try:
94113
return next(
95114
name for name, variable in self.dataset.variables.items()
96-
if variable.attrs.get('standard_name') == 'longitude'
97-
or variable.attrs.get('coordinate_type') == 'longitude'
98-
or variable.attrs.get('units') == 'degree_east'
115+
if variable.attrs.get('units') in CF_LONGITUDE_UNITS
116+
or variable.attrs.get('standard_name') == 'longitude'
117+
or variable.attrs.get('axis') == 'X'
99118
)
100119
except StopIteration:
101120
raise ValueError("Could not find longitude coordinate")

tests/conventions/test_cfgrid1d.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
from shapely.testing import assert_geometries_equal
1414

1515
from emsarray.conventions import get_dataset_convention
16-
from emsarray.conventions.grid import CFGrid1D, CFGridKind, CFGridTopology
16+
from emsarray.conventions.grid import (
17+
CFGrid1D, CFGrid1DTopology, CFGridKind, CFGridTopology
18+
)
1719
from emsarray.operations import geometry
1820
from tests.utils import assert_property_not_cached, box, mask_from_strings
1921

@@ -172,6 +174,66 @@ def test_make_dataset():
172174
assert_allclose(lons.values, np.array([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]))
173175

174176

177+
@pytest.mark.parametrize(
178+
['name', 'attrs'],
179+
[
180+
('lat', {'units': 'degrees_north'}),
181+
('l1', {'units': 'degree_north'}),
182+
('latitude', {'units': 'degree_N'}),
183+
('y', {'units': 'degrees_N'}),
184+
('Latitude', {'units': 'degreeN'}),
185+
('lats', {'units': 'degreesN'}),
186+
('latitude', {'standard_name': 'latitude'}),
187+
('y', {'axis': 'Y'}),
188+
],
189+
)
190+
def test_latitude_detection(name: str, attrs: dict):
191+
dataset = xr.Dataset({
192+
name: xr.DataArray([0, 1, 2], dims=[name], attrs=attrs),
193+
'dummy': xr.DataArray([3, 4, 5], dims=['other']),
194+
})
195+
topology = CFGrid1DTopology(dataset)
196+
assert topology.latitude_name == name
197+
198+
199+
@pytest.mark.parametrize(
200+
['name', 'attrs'],
201+
[
202+
('lon', {'units': 'degrees_east'}),
203+
('l2', {'units': 'degree_east'}),
204+
('longitude', {'units': 'degree_E'}),
205+
('x', {'units': 'degrees_E'}),
206+
('Longitude', {'units': 'degreeE'}),
207+
('lons', {'units': 'degreesE'}),
208+
('longitude', {'standard_name': 'longitude'}),
209+
('x', {'axis': 'X'}),
210+
],
211+
)
212+
def test_longitude_detection(name: str, attrs: dict):
213+
dataset = xr.Dataset({
214+
name: xr.DataArray([0, 1, 2], dims=[name], attrs=attrs),
215+
'dummy': xr.DataArray([3, 4, 5], dims=['other']),
216+
})
217+
topology = CFGrid1DTopology(dataset)
218+
assert topology.longitude_name == name
219+
220+
221+
def test_manual_coordinate_names():
222+
dataset = xr.Dataset({
223+
'x': xr.DataArray([0, 1, 2], dims=['x']),
224+
'y': xr.DataArray([0, 1, 2], dims=['y']),
225+
})
226+
topology = CFGrid1DTopology(dataset)
227+
with pytest.raises(ValueError):
228+
topology.latitude_name
229+
230+
topology = CFGrid1DTopology(dataset, latitude='y', longitude='x')
231+
assert topology.latitude_name == 'y'
232+
assert topology.longitude_name == 'x'
233+
xr.testing.assert_equal(topology.latitude, dataset['y'])
234+
xr.testing.assert_equal(topology.longitude, dataset['x'])
235+
236+
175237
def test_varnames():
176238
dataset = make_dataset(width=11, height=7, depth=5)
177239
assert dataset.ems.get_depth_name() == 'depth'

tests/conventions/test_cfgrid2d.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from shapely.testing import assert_geometries_equal
2323

2424
from emsarray.conventions import get_dataset_convention
25-
from emsarray.conventions.grid import CFGridKind
25+
from emsarray.conventions.grid import CFGrid2DTopology, CFGridKind
2626
from emsarray.conventions.shoc import ShocSimple
2727
from emsarray.operations import geometry
2828
from tests.utils import (
@@ -167,6 +167,66 @@ def test_varnames():
167167
assert dataset.ems.get_time_name() == 'time'
168168

169169

170+
@pytest.mark.parametrize(
171+
['name', 'attrs'],
172+
[
173+
('lat', {'units': 'degrees_north'}),
174+
('l1', {'units': 'degree_north'}),
175+
('latitude', {'units': 'degree_N'}),
176+
('y', {'units': 'degrees_N'}),
177+
('Latitude', {'units': 'degreeN'}),
178+
('lats', {'units': 'degreesN'}),
179+
('latitude', {'standard_name': 'latitude'}),
180+
('y', {'axis': 'Y'}),
181+
],
182+
)
183+
def test_latitude_detection(name: str, attrs: dict):
184+
dataset = xr.Dataset({
185+
name: xr.DataArray([[0, 1], [2, 3]], dims=['j', 'i'], attrs=attrs),
186+
'dummy': xr.DataArray([3, 4, 5], dims=['other']),
187+
})
188+
topology = CFGrid2DTopology(dataset)
189+
assert topology.latitude_name == name
190+
191+
192+
@pytest.mark.parametrize(
193+
['name', 'attrs'],
194+
[
195+
('lon', {'units': 'degrees_east'}),
196+
('l2', {'units': 'degree_east'}),
197+
('longitude', {'units': 'degree_E'}),
198+
('x', {'units': 'degrees_E'}),
199+
('Longitude', {'units': 'degreeE'}),
200+
('lons', {'units': 'degreesE'}),
201+
('longitude', {'standard_name': 'longitude'}),
202+
('x', {'axis': 'X'}),
203+
],
204+
)
205+
def test_longitude_detection(name: str, attrs: dict):
206+
dataset = xr.Dataset({
207+
name: xr.DataArray([[0, 1], [2, 3]], dims=['j', 'i'], attrs=attrs),
208+
'dummy': xr.DataArray([3, 4, 5], dims=['other']),
209+
})
210+
topology = CFGrid2DTopology(dataset)
211+
assert topology.longitude_name == name
212+
213+
214+
def test_manual_coordinate_names():
215+
dataset = xr.Dataset({
216+
'n': xr.DataArray([[0, 1], [2, 3]], dims=['j', 'i']),
217+
'e': xr.DataArray([[4, 5], [6, 7]], dims=['j', 'i']),
218+
})
219+
topology = CFGrid2DTopology(dataset)
220+
with pytest.raises(ValueError):
221+
topology.latitude_name
222+
223+
topology = CFGrid2DTopology(dataset, latitude='n', longitude='e')
224+
assert topology.latitude_name == 'n'
225+
assert topology.longitude_name == 'e'
226+
xr.testing.assert_equal(topology.latitude, dataset['n'])
227+
xr.testing.assert_equal(topology.longitude, dataset['e'])
228+
229+
170230
def test_polygons_no_bounds():
171231
dataset = make_dataset(
172232
j_size=10, i_size=20,

0 commit comments

Comments
 (0)