Skip to content

Commit b631f8e

Browse files
authored
Add 'bounds inherit' checks from Appendix A (ioos#1217)
1 parent cbb40ed commit b631f8e

File tree

3 files changed

+170
-0
lines changed

3 files changed

+170
-0
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
appendix_a = {
2+
"actual_range": {"Type": "N", "Use": ["C", "D", "BO"]},
3+
"add_offset": {"Type": "N", "Use": ["C", "D", "BO"]},
4+
"ancillary_variables": {"Type": "S", "Use": ["D"]},
5+
"axis": {"Type": "S", "Use": ["C", "BI"]},
6+
"bounds": {"Type": "S", "Use": ["C"]},
7+
"calendar": {"Type": "S", "Use": ["C", "BI"]},
8+
"cell_measures": {"Type": "S", "Use": ["D", "Do"]},
9+
"cell_methods": {"Type": "S", "Use": ["D"]},
10+
"cf_role": {"Type": "S", "Use": ["C", "BI"]},
11+
"climatology": {"Type": "S", "Use": ["C"]},
12+
"comment": {"Type": "S", "Use": ["G", "C", "D"]},
13+
"compress": {"Type": "S", "Use": ["C"]},
14+
"computed_standard_name": {"Type": "S", "Use": ["C", "BI"]},
15+
"Conventions": {"Type": "S", "Use": ["G"]},
16+
"coordinate_interpolation": {"Type": "S", "Use": ["D", "Do"]},
17+
"coordinates": {"Type": "S", "Use": ["D", "M", "Do"]},
18+
"dimensions": {"Type": "S", "Use": ["Do"]},
19+
"external_variables": {"Type": "S", "Use": ["G"]},
20+
"_FillValue": {"Type": "D", "Use": ["C", "D", "BO"]},
21+
"featureType": {"Type": "S", "Use": ["G"]},
22+
"flag_masks": {"Type": "D", "Use": ["D"]},
23+
"flag_meanings": {"Type": "S", "Use": ["D"]},
24+
"flag_values": {"Type": "D", "Use": ["D"]},
25+
"formula_terms": {"Type": "S", "Use": ["C", "BO"]},
26+
"geometry": {"Type": "S", "Use": ["C", "D", "Do"]},
27+
"geometry_type": {"Type": "S", "Use": ["M"]},
28+
"grid_mapping": {"Type": "S", "Use": ["D", "M", "Do"]},
29+
"history": {"Type": "S", "Use": ["G", "Gr"]},
30+
"instance_dimension": {"Type": "S", "Use": []},
31+
"institution": {"Type": "S", "Use": ["G", "D"]},
32+
"interior_ring": {"Type": "S", "Use": ["M"]},
33+
"leap_month": {"Type": "N", "Use": ["C", "BI"]},
34+
"leap_year": {"Type": "N", "Use": ["C", "BI"]},
35+
"location": {"Type": "S", "Use": ["D", "Do"]},
36+
"location_index_set": {"Type": "S", "Use": ["D", "Do"]},
37+
"long_name": {"Type": "S", "Use": ["C", "D", "Do", "BI"]},
38+
"mesh": {"Type": "S", "Use": ["D", "Do"]},
39+
"missing_value": {"Type": "D", "Use": ["C", "D", "BO"]},
40+
"month_lengths": {"Type": "N", "Use": ["C", "BI"]},
41+
"node_coordinates": {"Type": "S", "Use": ["M"]},
42+
"node_count": {"Type": "S", "Use": ["M"]},
43+
"nodes": {"Type": "S", "Use": ["C"]},
44+
"part_node_count": {"Type": "S", "Use": ["M"]},
45+
"positive": {"Type": "S", "Use": ["C", "BI"]},
46+
"references": {"Type": "S", "Use": ["G", "D"]},
47+
"sample_dimension": {"Type": "S", "Use": []},
48+
"scale_factor": {"Type": "N", "Use": ["C", "D", "BO"]},
49+
"source": {"Type": "S", "Use": ["G", "D"]},
50+
"standard_error_multiplier": {"Type": "N", "Use": ["D"]},
51+
"standard_name": {"Type": "S", "Use": ["C", "D", "BI"]},
52+
"title": {"Type": "S", "Use": ["G", "Gr"]},
53+
"units": {"Type": "S", "Use": ["C", "D", "BI"]},
54+
"units_metadata": {"Type": "S", "Use": ["C", "D", "BI"]},
55+
"valid_max": {"Type": "N", "Use": ["C", "D", "BO"]},
56+
"valid_min": {"Type": "N", "Use": ["C", "D", "BO"]},
57+
"valid_range": {"Type": "N", "Use": ["C", "D", "BO"]},
58+
}

compliance_checker/cf/cf_1_11.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from functools import lru_cache
22

33
from compliance_checker.base import BaseCheck, TestCtx
4+
from compliance_checker.cf.appendix_a import appendix_a
45
from compliance_checker.cf.cf_1_10 import CF1_10Check
6+
from compliance_checker.cf.util import VariableReferenceError, reference_attr_variables
57

68

79
@lru_cache
@@ -106,6 +108,86 @@ def check_time_units_metadata(self, ds):
106108

107109
return [time_units_metadata_ctx.to_result()]
108110

111+
def check_bounds_inherit_attributes(self, ds):
112+
"""
113+
A boundary variable inherits the values of some attributes from its parent coordinate variable.
114+
If a coordinate variable has any of the attributes marked "BI" (for "inherit") in the "Use" column of <<attribute-appendix>>, they are assumed to apply to its bounds variable as well.
115+
It is recommended that BI attributes not be included on a boundary variable.
116+
If a BI attribute is included, it must also be present in the parent variable, and it must exactly match the parent attribute's data type and value.
117+
A boundary variable can only have inheritable attributes if they are also present on its parent coordinate variable.
118+
A bounds variable may have any of the attributes marked "BO" for ("own") in the "Use" column of <<attribute-appendix>>.
119+
These attributes take precedence over any corresponding attributes of the parent variable.
120+
In these cases, the parent variable's attribute does not apply to the bounds variable, regardless of whether the latter has its own attribute.
121+
"""
122+
results = []
123+
124+
appendix_a_bi_attrs = {
125+
attribute_name
126+
for attribute_name, data_dict in appendix_a.items()
127+
if "BI" in data_dict["Use"]
128+
}
129+
for parent_variable in ds.get_variables_by_attributes(
130+
bounds=lambda b: b is not None,
131+
):
132+
parent_bi_attrs = set(parent_variable.ncattrs()) & appendix_a_bi_attrs
133+
bounds_variable = reference_attr_variables(ds, parent_variable.bounds)[0]
134+
# nonexistent bounds variable, skip
135+
if isinstance(bounds_variable, VariableReferenceError):
136+
continue
137+
138+
bounds_bi_ctx = self.get_test_ctx(
139+
BaseCheck.MEDIUM,
140+
self.section_titles["7.1"],
141+
)
142+
bounds_ncattr_set = set(bounds_variable.ncattrs())
143+
# IMPLEMENTATION CONFORMANCE 7.3 REQUIRED 4
144+
# A boundary variable can only have inheritable attributes, i.e. any of those marked "BI" in the "Use" column of Appendix A, if they are also present on its parent coordinate variable.
145+
bounds_bi_attrs = bounds_ncattr_set & appendix_a_bi_attrs
146+
# failure case 1, BI attr is only in bounds variable
147+
bounds_bi_only_attrs = bounds_bi_attrs - parent_bi_attrs
148+
# If a boundary variable has an inheritable attribute then its data type and its value must be exactly the same as the parent variable’s attribute.
149+
bounds_bi_ctx.out_of += 1
150+
if bounds_bi_only_attrs:
151+
bounds_bi_ctx.messages.append(
152+
f"Bounds variable {bounds_variable.name} has the following attributes which must appear on the parent variable {parent_variable.name}: "
153+
f"{sorted(bounds_bi_only_attrs)}",
154+
)
155+
else:
156+
bounds_bi_ctx.score += 1
157+
# failure case 2, BI attrs are in both bounds and parent variable
158+
both_bi_attr = bounds_bi_attrs & parent_bi_attrs
159+
no_match_attrs, match_attrs = [], []
160+
for bi_attr in both_bi_attr:
161+
bounds_bi_ctx.out_of += 1
162+
parent_attr_val = getattr(parent_variable, bi_attr)
163+
bounds_attr_val = getattr(bounds_variable, bi_attr)
164+
# IMPLEMENTATION CONFORMANCE 7.3 REQUIRED 5
165+
# If a boundary variable has an inheritable attribute then its data type and its value must be exactly the same as the parent variable’s attribute.
166+
if (
167+
type(parent_attr_val) is not type(bounds_attr_val)
168+
or parent_attr_val != bounds_attr_val
169+
):
170+
no_match_attrs.append(bi_attr)
171+
else:
172+
match_attrs.append(bi_attr)
173+
174+
pass_bi_both = True
175+
if no_match_attrs:
176+
pass_bi_both = False
177+
bounds_bi_ctx.messages.append(
178+
f"Bounds variable {bounds_variable.name} and parent variable {parent_variable.name} have the following non matching boundary related attributes: {sorted(no_match_attrs)}",
179+
)
180+
181+
if match_attrs:
182+
pass_bi_both = False
183+
bounds_bi_ctx.messages.append(
184+
f"Bounds variable {bounds_variable.name} and parent variable {parent_variable.name} have the following matching attributes {sorted(match_attrs)}. It is recommended that only the parent variable of the bounds variable contains these attributes",
185+
)
186+
bounds_bi_ctx.score += pass_bi_both
187+
188+
results.append(bounds_bi_ctx.to_result())
189+
return results
190+
109191
def check_single_cf_role(self, ds):
110192
test_ctx = self.get_test_ctx(
111193
BaseCheck.HIGH,

compliance_checker/tests/test_cf.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3700,6 +3700,36 @@ def test_time_units_metadata(self):
37003700
scored, out_of, _ = get_results(results)
37013701
assert scored == 1 and out_of == 2
37023702

3703+
@pytest.fixture
3704+
def bounds_ds(self):
3705+
ds = MockTimeSeries()
3706+
ds.createDimension("bounds", 2)
3707+
ds.createVariable("time_bounds", "f8", ("time", "bounds"))
3708+
ds.variables["time"].bounds = "time_bounds"
3709+
return ds
3710+
3711+
def test_bounds_inherit_attributes(self, bounds_ds):
3712+
# TEST CONFORMANCE 7.3 REQUIRED 4, 5
3713+
results = self.cf.check_bounds_inherit_attributes(bounds_ds)
3714+
scored, out_of, _ = get_results(results)
3715+
assert scored == out_of
3716+
time_bounds = bounds_ds.variables["time_bounds"]
3717+
# expected failure, not in parent variable
3718+
time_bounds.cf_role = "timeseries_id"
3719+
# expected failure, differing value of BI attribute (no seconds)
3720+
time_bounds.calendar = "gregorian"
3721+
# repeated BI attributes variable aren't recommended
3722+
time_bounds.axis = "T"
3723+
results = self.cf.check_bounds_inherit_attributes(bounds_ds)
3724+
scored, out_of, messages = get_results(results)
3725+
assert scored < out_of
3726+
expected_msgs = {
3727+
"Bounds variable time_bounds has the following attributes which must appear on the parent variable time: ['cf_role']",
3728+
"Bounds variable time_bounds and parent variable time have the following non matching boundary related attributes: ['calendar']",
3729+
"Bounds variable time_bounds and parent variable time have the following matching attributes ['axis']. It is recommended that only the parent variable of the bounds variable contains these attributes",
3730+
}
3731+
assert expected_msgs == set(messages)
3732+
37033733
def test_single_cf_role(self):
37043734
ds = MockTimeSeries()
37053735
ds.createDimension("ts_no")

0 commit comments

Comments
 (0)