diff --git a/CHANGES.md b/CHANGES.md index 5e76412..2cf7b1a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ - Added more rules - core rule "flags" + - core rule "lon-coordinate" + - core rule "lat-coordinate" - core rule "time-coordinate" (#15) - xcube rule "time-naming" (#15) diff --git a/docs/rule-ref.md b/docs/rule-ref.md index 2a8c46f..6db23a8 100644 --- a/docs/rule-ref.md +++ b/docs/rule-ref.md @@ -30,6 +30,20 @@ Grid mappings, if any, shall have valid grid mapping coordinate variables. Contained in: `all`-:material-lightning-bolt: `recommended`-:material-lightning-bolt: +### :material-bug: `lat-coordinate` + +Latitude coordinate should have standard units and standard names. +[More information.](https://cfconventions.org/cf-conventions/cf-conventions.html#latitude-coordinate) + +Contained in: `all`-:material-lightning-bolt: + +### :material-bug: `lon-coordinate` + +Longitude coordinate should have standard units and standard names. +[More information.](https://cfconventions.org/cf-conventions/cf-conventions.html#longitude-coordinate) + +Contained in: `all`-:material-lightning-bolt: + ### :material-lightbulb: `no-empty-attrs` Every dataset element should have metadata that describes it. diff --git a/notebooks/mkdataset.py b/notebooks/mkdataset.py index bb64a9b..39b9a04 100644 --- a/notebooks/mkdataset.py +++ b/notebooks/mkdataset.py @@ -13,10 +13,18 @@ def make_dataset() -> xr.Dataset: attrs=dict(title="SST-Climatology Subset"), coords={ "x": xr.DataArray( - np.linspace(-180, 180, nx), dims="x", attrs={"units": "degrees"} + np.linspace(-180, 180, nx), dims="x", attrs={ + "standard_name": "longitude", + "long_name": "longitude", + "units": "degrees_east" + } ), "y": xr.DataArray( - np.linspace(-90, 90, ny), dims="y", attrs={"units": "degrees"} + np.linspace(-90, 90, ny), dims="y", attrs={ + "standard_name": "latitude", + "long_name": "latitude", + "units": "degrees_north" + } ), "time": xr.DataArray( [365 * i for i in range(nt)], @@ -55,6 +63,10 @@ def make_dataset() -> xr.Dataset: def make_dataset_with_issues() -> xr.Dataset: """Create a dataset that produces issues with xrlint core rules.""" invalid_ds = make_dataset() + invalid_ds.x.attrs["units"] = "degrees" + invalid_ds.x.attrs["axis"] = "x" + del invalid_ds.y.attrs["standard_name"] + invalid_ds.y.attrs["axis"] = "y" invalid_ds.time.attrs["units"] = "days since 2020-01-01 ß0:000:00" invalid_ds.attrs = {} invalid_ds.sst.attrs["units"] = 1 diff --git a/notebooks/xrlint-linter.ipynb b/notebooks/xrlint-linter.ipynb index b8474e7..0c1e987 100644 --- a/notebooks/xrlint-linter.ipynb +++ b/notebooks/xrlint-linter.ipynb @@ -437,38 +437,38 @@ " * time (time) int64 32B 0 365 730 1095\n", " spatial_ref int64 8B 0\n", "Data variables:\n", - " sst (time, y, x) float64 192B 0.3002 0.935 ... 0.2138 0.04308\n", - " sst_anomaly (time, y, x) float64 192B 0.1556 0.3582 ... 0.4877 0.04322\n", + " sst (time, y, x) float64 192B 0.357 0.4776 0.8956 ... 0.2892 0.1911\n", + " sst_anomaly (time, y, x) float64 192B 0.1693 0.4643 ... 0.6467 0.9646\n", "Attributes:\n", - " title: SST-Climatology Subset" + " title: SST-Climatology Subset" ], "text/plain": [ " Size: 464B\n", @@ -479,8 +479,8 @@ " * time (time) int64 32B 0 365 730 1095\n", " spatial_ref int64 8B 0\n", "Data variables:\n", - " sst (time, y, x) float64 192B 0.3002 0.935 ... 0.2138 0.04308\n", - " sst_anomaly (time, y, x) float64 192B 0.1556 0.3582 ... 0.4877 0.04322\n", + " sst (time, y, x) float64 192B 0.357 0.4776 0.8956 ... 0.2892 0.1911\n", + " sst_anomaly (time, y, x) float64 192B 0.1693 0.4643 ... 0.6467 0.9646\n", "Attributes:\n", " title: SST-Climatology Subset" ] @@ -516,7 +516,7 @@ "

<dataset> - ok

\n" ], "text/plain": [ - "Result(config=Config(name=None, files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins={'__core__': Plugin(meta=PluginMeta(name='__core__', version='0.3.0.dev0', ref='xrlint.plugins.core:export_plugin'), rules={'coords-for-dims': Rule(meta=RuleMeta(name='coords-for-dims', version='1.0.0', description='Dimensions of data variables should have corresponding coordinates.', schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'dataset-title-attr': Rule(meta=RuleMeta(name='dataset-title-attr', version='1.0.0', description='Datasets should be given a non-empty title.', schema=None, ref=None, docs_url=None, type='suggestion'), op_class=), 'grid-mappings': Rule(meta=RuleMeta(name='grid-mappings', version='1.0.0', description='Grid mappings, if any, shall have valid grid mapping coordinate variables.', schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'no-empty-attrs': Rule(meta=RuleMeta(name='no-empty-attrs', version='1.0.0', description='Every dataset element should have metadata that describes it.', schema=None, ref=None, docs_url=None, type='suggestion'), op_class=), 'time-coords': Rule(meta=RuleMeta(name='time-coords', version='1.0.0', description=\"Time coordinates (standard_name='time') should have unambiguous time encoding.\", schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'var-units-attr': Rule(meta=RuleMeta(name='var-units-attr', version='1.0.0', description=\"Every variable should have a valid 'units' attribute.\", schema=None, ref=None, docs_url=None, type='suggestion'), op_class=)}, processors={}, configs={'recommended': Config(name='recommended', files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins=None, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=1, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'time-coords': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=1, args=(), kwargs={})}, settings=None), 'all': Config(name='all', files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins=None, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=2, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=2, args=(), kwargs={}), 'time-coords': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=2, args=(), kwargs={})}, settings=None)})}, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=1, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'time-coords': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=1, args=(), kwargs={})}, settings=None), file_path='', messages=[], fixable_error_count=0, fixable_warning_count=0, error_count=0, fatal_error_count=0, warning_count=0)" + "Result(config=Config(name=None, files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins={'__core__': Plugin(meta=PluginMeta(name='__core__', version='0.3.0.dev0', ref='xrlint.plugins.core:export_plugin'), rules={'coords-for-dims': Rule(meta=RuleMeta(name='coords-for-dims', version='1.0.0', description='Dimensions of data variables should have corresponding coordinates.', schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'dataset-title-attr': Rule(meta=RuleMeta(name='dataset-title-attr', version='1.0.0', description='Datasets should be given a non-empty title.', schema=None, ref=None, docs_url=None, type='suggestion'), op_class=), 'grid-mappings': Rule(meta=RuleMeta(name='grid-mappings', version='1.0.0', description='Grid mappings, if any, shall have valid grid mapping coordinate variables.', schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'lat-coordinate': Rule(meta=RuleMeta(name='lat-coordinate', version='1.0.0', description='Latitude coordinate should have standard units and standard names.', schema=None, ref=None, docs_url='https://cfconventions.org/cf-conventions/cf-conventions.html#latitude-coordinate', type='problem'), op_class=), 'lon-coordinate': Rule(meta=RuleMeta(name='lon-coordinate', version='1.0.0', description='Longitude coordinate should have standard units and standard names.', schema=None, ref=None, docs_url='https://cfconventions.org/cf-conventions/cf-conventions.html#longitude-coordinate', type='problem'), op_class=), 'no-empty-attrs': Rule(meta=RuleMeta(name='no-empty-attrs', version='1.0.0', description='Every dataset element should have metadata that describes it.', schema=None, ref=None, docs_url=None, type='suggestion'), op_class=), 'time-coordinate': Rule(meta=RuleMeta(name='time-coordinate', version='1.0.0', description=\"Time coordinate (standard_name='time') should have unambiguous time units encoding.\", schema=None, ref=None, docs_url='https://cfconventions.org/cf-conventions/cf-conventions.html#time-coordinate', type='problem'), op_class=), 'var-units-attr': Rule(meta=RuleMeta(name='var-units-attr', version='1.0.0', description=\"Every variable should have a valid 'units' attribute.\", schema=None, ref=None, docs_url=None, type='suggestion'), op_class=)}, processors={}, configs={'recommended': Config(name='recommended', files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins=None, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=1, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'lat-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'lon-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'time-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=1, args=(), kwargs={})}, settings=None), 'all': Config(name='all', files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins=None, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=2, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'lat-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'lon-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=2, args=(), kwargs={}), 'time-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=2, args=(), kwargs={})}, settings=None)})}, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=1, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'lat-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'lon-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'time-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=1, args=(), kwargs={})}, settings=None), file_path='', messages=[], fixable_error_count=0, fixable_warning_count=0, error_count=0, fatal_error_count=0, warning_count=0)" ] }, "execution_count": 5, @@ -914,38 +914,38 @@ " * time (time) int64 32B 0 365 730 1095\n", " spatial_ref int64 8B 0\n", "Data variables:\n", - " sst (time, y, x) float64 192B 0.8253 0.09788 ... 0.9227 0.03201\n", - " sst_anomaly (time, y, x) float64 192B 0.1763 0.1965 ... 0.8407 0.6847\n", - " sst_avg (x, y) float64 48B 0.2573 0.7063 0.8764 0.2189 0.7286 0.889" + " sst (time, y, x) float64 192B 0.9673 0.8684 ... 0.6397 0.2261\n", + " sst_anomaly (time, y, x) float64 192B 0.5013 0.1469 0.03853 ... 0.51 0.9694\n", + " sst_avg (x, y) float64 48B 0.5494 0.02223 0.5723 0.4118 0.1869 0.872" ], "text/plain": [ " Size: 512B\n", @@ -956,9 +956,9 @@ " * time (time) int64 32B 0 365 730 1095\n", " spatial_ref int64 8B 0\n", "Data variables:\n", - " sst (time, y, x) float64 192B 0.8253 0.09788 ... 0.9227 0.03201\n", - " sst_anomaly (time, y, x) float64 192B 0.1763 0.1965 ... 0.8407 0.6847\n", - " sst_avg (x, y) float64 48B 0.2573 0.7063 0.8764 0.2189 0.7286 0.889" + " sst (time, y, x) float64 192B 0.9673 0.8684 ... 0.6397 0.2261\n", + " sst_anomaly (time, y, x) float64 192B 0.5013 0.1469 0.03853 ... 0.51 0.9694\n", + " sst_avg (x, y) float64 48B 0.5494 0.02223 0.5723 0.4118 0.1869 0.872" ] }, "execution_count": 6, @@ -985,14 +985,18 @@ "\n", "\n", "\n", + "\n", + "\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "
dataset warn Missing 'title' attribute in dataset. dataset-title-attr
dataset.coords['y'] errorAttribute 'standard_name' should be 'latitude', was None. lat-coordinate
dataset.coords['y'] errorAttribute 'axis' should be 'Y', was 'y'. lat-coordinate
dataset.coords['x'] errorAttribute 'units' should be 'degrees_east', was 'degrees'. lon-coordinate
dataset.coords['x'] errorAttribute 'axis' should be 'X', was 'x'. lon-coordinate
dataset.attrs warn Missing metadata, attributes are empty. no-empty-attrs
dataset.coords['time'] errorMissing timezone in 'units' attribute: days since 2020-01-01 ß0:000:00time-coords
dataset.coords['time'] errorMissing timezone in 'units' attribute: days since 2020-01-01 ß0:000:00time-coordinate
dataset.data_vars['sst']warn Invalid 'units' attribute in variable 'sst'. var-units-attr

4 problems (one error and 3 warnings)

\n" + "

8 problems (5 errors and 3 warnings)

\n" ], "text/plain": [ - "Result(config=Config(name=None, files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins={'__core__': Plugin(meta=PluginMeta(name='__core__', version='0.3.0.dev0', ref='xrlint.plugins.core:export_plugin'), rules={'coords-for-dims': Rule(meta=RuleMeta(name='coords-for-dims', version='1.0.0', description='Dimensions of data variables should have corresponding coordinates.', schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'dataset-title-attr': Rule(meta=RuleMeta(name='dataset-title-attr', version='1.0.0', description='Datasets should be given a non-empty title.', schema=None, ref=None, docs_url=None, type='suggestion'), op_class=), 'grid-mappings': Rule(meta=RuleMeta(name='grid-mappings', version='1.0.0', description='Grid mappings, if any, shall have valid grid mapping coordinate variables.', schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'no-empty-attrs': Rule(meta=RuleMeta(name='no-empty-attrs', version='1.0.0', description='Every dataset element should have metadata that describes it.', schema=None, ref=None, docs_url=None, type='suggestion'), op_class=), 'time-coords': Rule(meta=RuleMeta(name='time-coords', version='1.0.0', description=\"Time coordinates (standard_name='time') should have unambiguous time encoding.\", schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'var-units-attr': Rule(meta=RuleMeta(name='var-units-attr', version='1.0.0', description=\"Every variable should have a valid 'units' attribute.\", schema=None, ref=None, docs_url=None, type='suggestion'), op_class=)}, processors={}, configs={'recommended': Config(name='recommended', files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins=None, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=1, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'time-coords': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=1, args=(), kwargs={})}, settings=None), 'all': Config(name='all', files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins=None, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=2, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=2, args=(), kwargs={}), 'time-coords': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=2, args=(), kwargs={})}, settings=None)})}, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=1, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'time-coords': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=1, args=(), kwargs={})}, settings=None), file_path='', messages=[Message(message=\"Missing 'title' attribute in dataset.\", node_path='dataset', rule_id='dataset-title-attr', severity=1, fatal=None, fix=None, suggestions=None), Message(message='Missing metadata, attributes are empty.', node_path='dataset.attrs', rule_id='no-empty-attrs', severity=1, fatal=None, fix=None, suggestions=[Suggestion(desc='Make sure to add appropriate metadata attributes to dataset elements.', data=None, fix=None)]), Message(message=\"Missing timezone in 'units' attribute: days since 2020-01-01 ß0:000:00\", node_path=\"dataset.coords['time']\", rule_id='time-coords', severity=2, fatal=None, fix=None, suggestions=[Suggestion(desc=\"Specify 'units' attribute using format ' since