Skip to content

Commit e4f0ee4

Browse files
authored
Merge pull request #25 from bcdev/forman-x-lat_lon_rules
Lat/lon rules
2 parents b0b61db + ddb1d5c commit e4f0ee4

File tree

9 files changed

+439
-103
lines changed

9 files changed

+439
-103
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
- Added more rules
66
- core rule "flags"
7+
- core rule "lon-coordinate"
8+
- core rule "lat-coordinate"
79
- core rule "time-coordinate" (#15)
810
- xcube rule "time-naming" (#15)
911

docs/rule-ref.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ Grid mappings, if any, shall have valid grid mapping coordinate variables.
3030

3131
Contained in: `all`-:material-lightning-bolt: `recommended`-:material-lightning-bolt:
3232

33+
### :material-bug: `lat-coordinate`
34+
35+
Latitude coordinate should have standard units and standard names.
36+
[More information.](https://cfconventions.org/cf-conventions/cf-conventions.html#latitude-coordinate)
37+
38+
Contained in: `all`-:material-lightning-bolt:
39+
40+
### :material-bug: `lon-coordinate`
41+
42+
Longitude coordinate should have standard units and standard names.
43+
[More information.](https://cfconventions.org/cf-conventions/cf-conventions.html#longitude-coordinate)
44+
45+
Contained in: `all`-:material-lightning-bolt:
46+
3347
### :material-lightbulb: `no-empty-attrs`
3448

3549
Every dataset element should have metadata that describes it.

notebooks/mkdataset.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,18 @@ def make_dataset() -> xr.Dataset:
1313
attrs=dict(title="SST-Climatology Subset"),
1414
coords={
1515
"x": xr.DataArray(
16-
np.linspace(-180, 180, nx), dims="x", attrs={"units": "degrees"}
16+
np.linspace(-180, 180, nx), dims="x", attrs={
17+
"standard_name": "longitude",
18+
"long_name": "longitude",
19+
"units": "degrees_east"
20+
}
1721
),
1822
"y": xr.DataArray(
19-
np.linspace(-90, 90, ny), dims="y", attrs={"units": "degrees"}
23+
np.linspace(-90, 90, ny), dims="y", attrs={
24+
"standard_name": "latitude",
25+
"long_name": "latitude",
26+
"units": "degrees_north"
27+
}
2028
),
2129
"time": xr.DataArray(
2230
[365 * i for i in range(nt)],
@@ -55,6 +63,10 @@ def make_dataset() -> xr.Dataset:
5563
def make_dataset_with_issues() -> xr.Dataset:
5664
"""Create a dataset that produces issues with xrlint core rules."""
5765
invalid_ds = make_dataset()
66+
invalid_ds.x.attrs["units"] = "degrees"
67+
invalid_ds.x.attrs["axis"] = "x"
68+
del invalid_ds.y.attrs["standard_name"]
69+
invalid_ds.y.attrs["axis"] = "y"
5870
invalid_ds.time.attrs["units"] = "days since 2020-01-01 ß0:000:00"
5971
invalid_ds.attrs = {}
6072
invalid_ds.sst.attrs["units"] = 1

notebooks/xrlint-linter.ipynb

Lines changed: 87 additions & 77 deletions
Large diffs are not rendered by default.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import numpy as np
2+
import xarray as xr
3+
4+
from xrlint.plugins.core.rules.lat_lon_coordinate import LatCoordinate
5+
from xrlint.plugins.core.rules.lat_lon_coordinate import LonCoordinate
6+
from xrlint.testing import RuleTest
7+
from xrlint.testing import RuleTester
8+
9+
valid_dataset_0 = xr.Dataset()
10+
valid_dataset_1 = xr.Dataset(
11+
coords={
12+
"lat": xr.DataArray(
13+
np.array([3, 4, 5]),
14+
dims="lat",
15+
attrs={
16+
"units": "degrees_north",
17+
"standard_name": "latitude",
18+
"long_name": "latitude",
19+
},
20+
),
21+
"lon": xr.DataArray(
22+
np.array([-2, -1, 0, 1]),
23+
dims="lon",
24+
attrs={
25+
"units": "degrees_east",
26+
"standard_name": "longitude",
27+
"long_name": "longitude",
28+
},
29+
),
30+
},
31+
data_vars={
32+
"mask": xr.DataArray(
33+
[[10, 20, 30, 40], [30, 40, 50, 60], [50, 60, 70, 80]], dims=("lat", "lon")
34+
)
35+
},
36+
)
37+
38+
# Valid, because the coord names doesn't matter as long their metadata is ok
39+
valid_dataset_2 = valid_dataset_1.rename_vars({"lon": "x", "lat": "y"})
40+
41+
# Valid, because the coord units have aliases
42+
valid_dataset_3 = valid_dataset_1.copy()
43+
valid_dataset_3.lat.attrs["units"] = "degreeN"
44+
valid_dataset_3.lon.attrs["units"] = "degreeE"
45+
46+
# Valid, because the coord units have aliases
47+
valid_dataset_4 = valid_dataset_1.copy()
48+
del valid_dataset_4.lat.attrs["standard_name"]
49+
del valid_dataset_4.lon.attrs["standard_name"]
50+
valid_dataset_4.lat.attrs["axis"] = "Y"
51+
valid_dataset_4.lon.attrs["axis"] = "X"
52+
53+
invalid_lat_dataset_0 = valid_dataset_1.copy()
54+
del invalid_lat_dataset_0.lat.attrs["standard_name"]
55+
56+
invalid_lon_dataset_0 = valid_dataset_1.copy()
57+
del invalid_lon_dataset_0.lon.attrs["units"]
58+
59+
invalid_lat_dataset_1 = valid_dataset_1.copy()
60+
invalid_lat_dataset_1.lat.attrs["units"] = "deg"
61+
62+
invalid_lon_dataset_1 = valid_dataset_1.copy()
63+
invalid_lon_dataset_1.lon.attrs["long_name"] = "poo"
64+
65+
invalid_lat_dataset_2 = valid_dataset_1.copy()
66+
del invalid_lat_dataset_2.lat.attrs["standard_name"]
67+
invalid_lat_dataset_2.lat.attrs["axis"] = "y" # should be "Y"
68+
69+
invalid_lon_dataset_2 = valid_dataset_1.copy()
70+
del invalid_lon_dataset_2.lon.attrs["standard_name"]
71+
invalid_lon_dataset_2.lon.attrs["axis"] = "x" # should be "X"
72+
73+
LatCoordsTest = RuleTester.define_test(
74+
"lat-coordinate",
75+
LatCoordinate,
76+
valid=[
77+
RuleTest(dataset=valid_dataset_0),
78+
RuleTest(dataset=valid_dataset_1),
79+
RuleTest(dataset=valid_dataset_2),
80+
RuleTest(dataset=valid_dataset_3),
81+
RuleTest(dataset=valid_dataset_4),
82+
RuleTest(dataset=invalid_lon_dataset_0),
83+
RuleTest(dataset=invalid_lon_dataset_1),
84+
RuleTest(dataset=invalid_lon_dataset_2),
85+
],
86+
invalid=[
87+
RuleTest(dataset=invalid_lat_dataset_0),
88+
RuleTest(dataset=invalid_lat_dataset_1),
89+
RuleTest(dataset=invalid_lat_dataset_2),
90+
],
91+
)
92+
93+
LonCoordsTest = RuleTester.define_test(
94+
"lon-coordinate",
95+
LonCoordinate,
96+
valid=[
97+
RuleTest(dataset=valid_dataset_0),
98+
RuleTest(dataset=valid_dataset_1),
99+
RuleTest(dataset=valid_dataset_2),
100+
RuleTest(dataset=valid_dataset_3),
101+
RuleTest(dataset=valid_dataset_4),
102+
RuleTest(dataset=invalid_lat_dataset_0),
103+
RuleTest(dataset=invalid_lat_dataset_1),
104+
RuleTest(dataset=invalid_lat_dataset_2),
105+
],
106+
invalid=[
107+
RuleTest(dataset=invalid_lon_dataset_0),
108+
RuleTest(dataset=invalid_lon_dataset_1),
109+
RuleTest(dataset=invalid_lon_dataset_2),
110+
],
111+
)

tests/plugins/core/test_plugin.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,38 @@
44

55

66
class ExportPluginTest(TestCase):
7-
def test_configs_complete(self):
8-
_plugin = export_plugin()
9-
self.assertEqual(
10-
{
11-
"all",
12-
"recommended",
13-
},
14-
set(_plugin.configs.keys()),
15-
)
16-
177
def test_rules_complete(self):
18-
_plugin = export_plugin()
8+
plugin = export_plugin()
199
self.assertEqual(
2010
{
2111
"coords-for-dims",
2212
"dataset-title-attr",
23-
"grid-mappings",
2413
"flags",
14+
"grid-mappings",
15+
"lat-coordinate",
16+
"lon-coordinate",
2517
"no-empty-attrs",
2618
"time-coordinate",
2719
"var-units-attr",
2820
},
29-
set(_plugin.rules.keys()),
21+
set(plugin.rules.keys()),
22+
)
23+
24+
def test_configs_complete(self):
25+
plugin = export_plugin()
26+
self.assertEqual(
27+
{
28+
"all",
29+
"recommended",
30+
},
31+
set(plugin.configs.keys()),
32+
)
33+
all_rule_names = set(plugin.rules.keys())
34+
self.assertEqual(
35+
all_rule_names,
36+
set(plugin.configs["all"].rules.keys()),
37+
)
38+
self.assertEqual(
39+
all_rule_names,
40+
set(plugin.configs["recommended"].rules.keys()),
3041
)

tests/plugins/xcube/test_plugin.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,9 @@
44

55

66
class ExportPluginTest(TestCase):
7-
def test_configs_complete(self):
8-
_plugin = export_plugin()
9-
self.assertEqual(
10-
{
11-
"all",
12-
"recommended",
13-
},
14-
set(_plugin.configs.keys()),
15-
)
167

178
def test_rules_complete(self):
18-
_plugin = export_plugin()
9+
plugin = export_plugin()
1910
self.assertEqual(
2011
{
2112
"any-spatial-data-var",
@@ -27,5 +18,24 @@ def test_rules_complete(self):
2718
"single-grid-mapping",
2819
"time-naming",
2920
},
30-
set(_plugin.rules.keys()),
21+
set(plugin.rules.keys()),
22+
)
23+
24+
def test_configs_complete(self):
25+
plugin = export_plugin()
26+
self.assertEqual(
27+
{
28+
"all",
29+
"recommended",
30+
},
31+
set(plugin.configs.keys()),
32+
)
33+
all_rule_names = set(f"xcube/{k}" for k in plugin.rules.keys())
34+
self.assertEqual(
35+
all_rule_names,
36+
set(plugin.configs["all"].rules.keys()),
37+
)
38+
self.assertEqual(
39+
all_rule_names,
40+
set(plugin.configs["recommended"].rules.keys()),
3141
)

xrlint/plugins/core/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ def export_plugin() -> Plugin:
1414
"rules": {
1515
"coords-for-dims": "error",
1616
"dataset-title-attr": "warn",
17+
"flags": "error",
1718
"grid-mappings": "error",
19+
"lat-coordinate": "error",
20+
"lon-coordinate": "error",
1821
"no-empty-attrs": "warn",
1922
"time-coordinate": "error",
2023
"var-units-attr": "warn",

0 commit comments

Comments
 (0)