Repository: ioos/compliance-checker
Pull request #775 (2020-03-19 15:41:58)

Add check to examine flag_values and flag_meanings
All QARTOD variables should have flag_meanings and flag_values
attributes. Add checks to look for their existence and test they
meet the requirements put forth in the CF specifications.

New output looks like so (for the [org_cormp_sun2.nc](http://testing.erddap.axds.co/erddap/tabledap/org_cormp_sun2.nc) dataset):

```
qartod_variables flags
* air_temperature_qc_agg's flag_values must be an array of values not <class 'str'>
* air_pressure_qc_agg's flag_values must be an array of values not <class 'str'>
* relative_humidity_qc_agg's flag_values must be an array of values not <class 'str'>
* sea_water_velocity_to_direction_qc_agg's flag_values must be an array of values not <class 'str'>
* sea_water_speed_qc_agg's flag_values must be an array of values not <class 'str'>
* sea_water_practical_salinity_qc_agg's flag_values must be an array of values not <class 'str'>
* sea_water_temperature_qc_agg's flag_values must be an array of values not <class 'str'>
* sea_surface_dominant_wave_period_qc_agg's flag_values must be an array of values not <class 'str'>
* sea_surface_wave_significant_height_qc_agg's flag_values must be an array of values not <class 'str'>
* wind_speed_of_gust_qc_agg's flag_values must be an array of values not <class 'str'>
* wind_speed_qc_agg's flag_values must be an array of values not <class 'str'>
* wind_from_direction_qc_agg's flag_values must be an array of values not <class 'str'>
```

Addresses #764 

mwengren: I just took care of it.  You can link to this anchor tag: https://ioos.github.io/ioos-metadata/ioos-metadata-profile-v1-2.html#quality-controlqartod

Patch:
diff --git a/compliance_checker/cf/cf.py b/compliance_checker/cf/cf.py
index 053fe70..0e8cf94 100644
--- a/compliance_checker/cf/cf.py
+++ b/compliance_checker/cf/cf.py
@@ -1955,7 +1955,7 @@ class CF1_6Check(CFNCCheck):
         '''
         variable = ds.variables[name]
 
-        flag_values = variable.flag_values
+        flag_values = getattr(variable, "flag_values", None)
         flag_meanings = getattr(variable, 'flag_meanings', None)
         valid_values = TestCtx(BaseCheck.HIGH, self.section_titles['3.5'])
 
diff --git a/compliance_checker/ioos.py b/compliance_checker/ioos.py
index 16c310b..984c390 100644
--- a/compliance_checker/ioos.py
+++ b/compliance_checker/ioos.py
@@ -1081,6 +1081,49 @@ class IOOS1_2Check(IOOSNCCheck):
 
         return results
 
+    def check_qartod_variables_flags(self, ds):
+        """
+        https://ioos.github.io/ioos-metadata/ioos-metadata-profile-v1-2.html#quality-controlqartod
+
+        Check that all QARTOD variables have flag_meanings and flag_values attributes.
+        Use delegation to methods in the CF module.
+
+        Parameters
+        ----------
+        ds (netCDF4.Dataset): open dataset
+
+        Returns
+        -------
+        list of Result objects
+        """
+
+        results = []
+        # get qartod variables
+        for v in ds.get_variables_by_attributes(standard_name=lambda x: x in self._qartod_std_names):
+
+            missing_msg = "flag_{} not present on {}"
+
+            # check if each has flag_values, flag_meanings
+            # need isinstance() as can't compare truth value of array
+            if getattr(v, "flag_values", None) is None:
+                results.append(Result(BaseCheck.MEDIUM, False, "qartod_variables flags", missing_msg.format("values", v.name)))
+
+            else: # if exist, test
+                results.append(self.cf1_7._check_flag_values(ds, v.name))
+
+            if getattr(v, "flag_meanings", None) is None:
+                results.append(Result(BaseCheck.MEDIUM, False, "qartod_variables flags", missing_msg.format("meanings", v.name)))
+
+            else: # if exist, test
+                results.append(self.cf1_7._check_flag_meanings(ds, v.name))
+
+        # Ensure message name is "qartod_variables flags"
+        # NOTE this is a bit of a hack to shove into CF results
+        for r in results:
+            r.name = "qartod_variables flags"
+
+        return results
+
     def check_qartod_variables_references(self, ds):
         """
         For any variables that are deemed QARTOD variables, check that they


Test patch:
diff --git a/compliance_checker/tests/test_ioos_profile.py b/compliance_checker/tests/test_ioos_profile.py
index fd53ef3..0eaf053 100644
--- a/compliance_checker/tests/test_ioos_profile.py
+++ b/compliance_checker/tests/test_ioos_profile.py
@@ -724,6 +724,44 @@ class TestIOOS1_2(BaseTestCase):
         ds.setncattr("platform_vocabulary", "bad")
         self.assertFalse(self.ioos.check_platform_vocabulary(ds).value)
 
+    def test_check_qartod_variables_flags(self):
+        ds = MockTimeSeries() # time, lat, lon, depth
+
+        # no QARTOD variables
+        results = self.ioos.check_qartod_variables_flags(ds)
+        scored, out_of, messages = get_results(results)
+        self.assertEqual(scored, out_of)
+
+        # QARTOD variable without flag_values, flag_meanings (fail)
+        qr = ds.createVariable("depth_qc", np.byte)
+        qr.setncattr("standard_name", "spike_test_quality_flag")
+        results = self.ioos.check_qartod_variables_flags(ds)
+        self.assertTrue(not any(r.value for r in results)) # all False
+
+        # QARTOD variable with flag meanings, without flag_meanings
+        qr.setncattr("flag_values", np.array([0, 1, 2], dtype=np.byte))
+        results = self.ioos.check_qartod_variables_flags(ds)
+        self.assertEqual(results[0].value[0], results[0].value[1]) # should pass
+        self.assertFalse(results[1].value) # still fail
+
+        # QARTOD variable with flag meanings, flag_values
+        qr.setncattr("flag_meanings", "x y z") # alphanumeric, space-separated
+        results = self.ioos.check_qartod_variables_flags(ds)
+        self.assertEqual(results[0].value[0], results[0].value[1]) # pass
+        self.assertEqual(results[1].value[0], results[1].value[1]) # pass
+
+        # flag_values array not equal to length of flag_meanings
+        qr.setncattr("flag_values", np.array([0, 1], dtype=np.byte))
+        results = self.ioos.check_qartod_variables_flags(ds)
+        self.assertLess(results[0].value[0], results[0].value[1])  # should fail
+        self.assertEqual(results[1].value[0], results[1].value[1]) # pass
+
+        # flag_values right length, wrong type
+        qr.setncattr("flag_values", np.array([0, 1, 2], dtype=np.float64))
+        results = self.ioos.check_qartod_variables_flags(ds)
+        self.assertLess(results[0].value[0], results[0].value[1])  # should fail
+        self.assertEqual(results[1].value[0], results[1].value[1]) # pass
+
     def test_check_qartod_variables_references(self):
         ds = MockTimeSeries() # time, lat, lon, depth