Repository: ioos/compliance-checker
Pull request #849 (2020-08-19 19:36:15)

Issue 839 fix
Addresses issue #839.

Add new geophysical attributes accuracy, precision, resolution
Add check for instrument variables make_model, calibration_date
Additionally add instrument_vocabulary to recommended global attributes.


Patch:
diff --git a/compliance_checker/ioos.py b/compliance_checker/ioos.py
index bff8ab8..ba96849 100644
--- a/compliance_checker/ioos.py
+++ b/compliance_checker/ioos.py
@@ -3,6 +3,8 @@ Check for IOOS-approved attributes
 """
 import re
 
+from numbers import Number
+
 import validators
 
 from cf_units import Unit
@@ -482,6 +484,9 @@ class IOOS1_2Check(IOOSNCCheck):
                     # ( "platform", BaseCheck.HIGH) # checked under check_single_platform()
                     # ( "wmo_platform_code", BaseCheck.HIGH) # only "if applicable", see check_wmo_platform_code()
                     # ( "ancillary_variables", BaseCheck.HIGH) # only "if applicable", see _check_var_gts_ingest()
+                    # ("accuracy", BaseCheck.MEDIUM), see check_accuracy
+                    ("precision", BaseCheck.MEDIUM),
+                    ("resolution", BaseCheck.MEDIUM)
                 ]
             )
         )
@@ -553,6 +558,7 @@ class IOOS1_2Check(IOOSNCCheck):
             # checked in check_creator_and_publisher_type
             #'publisher_type',
             "references",
+            "instrument_vocabulary"
         ]
 
     def setup(self, ds):
@@ -741,6 +747,49 @@ class IOOS1_2Check(IOOSNCCheck):
             ds, get_geophysical_variables(ds), self.geophys_check_var_attrs
         )
 
+    def check_accuracy(self, ds):
+        """
+        Special check for accuracy when in the salinity context.
+        https://github.com/ioos/compliance-checker/issues/839
+
+        Parameters
+        ----------
+        ds: netCDF4.Dataset
+
+        Returns
+        -------
+        list of Results objects
+        """
+
+        results = []
+        msg = (
+                   "Variable '{v}' attribute 'accuracy' should have the "
+                   "same units as '{v}'"
+              )
+        for v in get_geophysical_variables(ds):
+            _v = ds.variables[v]
+            std_name = getattr(_v, "standard_name", None)
+            gts_ingest = getattr(_v, "gts_ingest", None)
+            if (std_name=="sea_water_practical_salinity") and (gts_ingest=="true"):
+                msg = (
+                           "Variable '{v}' should have an 'accuracy' attribute "
+                           "that is numeric and of the same units as '{v}'"
+                      )
+                r = isinstance(getattr(_v, "accuracy", None), Number)
+            else: # only test if exists
+                r = getattr(_v, "accuracy", None) is not None
+
+            results.append(
+                Result(
+                    BaseCheck.MEDIUM,
+                    r,
+                    f"geophysical_variable:accuracy",
+                    [msg.format(v=v)],
+                )
+            )
+
+        return results
+
     def _check_vars_have_attrs(self, ds, vars_to_check, atts_to_check):
         """
         Check that the variables in vars_to_check have the attributes in
@@ -1451,6 +1500,46 @@ class IOOS1_2Check(IOOSNCCheck):
 
         return Result(BaseCheck.HIGH, valid, ctxt, None if valid else [msg])
 
+    def check_instrument_make_model_calib_date(self, ds):
+        """
+        Instrument variables should have attributes make_model and
+        calibration_date. Both should be strings, with calibration_date
+        following ISO-8601 date format.
+
+        https://github.com/ioos/compliance-checker/issues/839
+        """
+
+        results = []
+        ivars = get_instrument_variables(ds)
+        for v in ivars:
+            _v = ds.variables[v]
+
+            # make_model
+            mm = getattr(_v, "make_model", None)
+            valid = (isinstance(mm, str))
+            results.append(
+                Result(
+                    BaseCheck.MEDIUM,
+                    valid,
+                    "instrument_variable:make_model",
+                    None if valid else [f"Attribute {v}:make_model ({mm}) should be a string"]
+                )
+            )
+
+            # calibration_date
+            cd = getattr(_v, "calibration_date", "")
+            # thanks folks https://stackoverflow.com/questions/41129921/validate-an-iso-8601-datetime-string-in-python
+            valid = bool(re.match(r'^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$', cd))
+            results.append(
+                Result(
+                    BaseCheck.MEDIUM,
+                    valid,
+                    "instrument_variable:calibration_date",
+                    None if valid else [f"Attribute {v}:calibration_date ({cd}) should be an ISO-8601 string"]
+                )
+            )
+
+        return results
 
 class IOOSBaseSOSCheck(BaseCheck):
     _cc_spec = "ioos_sos"


Test patch:
diff --git a/compliance_checker/tests/test_ioos_profile.py b/compliance_checker/tests/test_ioos_profile.py
index a6288fc..7e32706 100644
--- a/compliance_checker/tests/test_ioos_profile.py
+++ b/compliance_checker/tests/test_ioos_profile.py
@@ -338,11 +338,46 @@ class TestIOOS1_2(BaseTestCase):
         )
         temp.setncattr("units", "degree_C")
         temp.setncattr("platform", "myPlatform")
+        temp.setncattr("precision", "0.0025")
+        temp.setncattr("resolution", "0.0001")
 
         results = self.ioos.check_geophysical_vars_have_attrs(ds)
         scored, out_of, messages = get_results(results)
         self.assertEqual(scored, out_of)
 
+    def test_check_accuracy(self):
+        ds = MockTimeSeries()  # time, lat, lon, depth
+        temp = ds.createVariable(
+            "temp", np.float64, dimensions=("time",), fill_value=9999999999.0
+        )  # _FillValue
+        temp.setncattr("standard_name", "sea_water_temperature")
+        results = self.ioos.check_accuracy(ds)
+        scored, out_of, messages = get_results(results)
+        self.assertLess(scored, out_of)
+
+        # add non-numeric vals for accuracy
+        # no gts_ingest attr, so only existence tested
+        temp.setncattr("accuracy", "bad")
+        results = self.ioos.check_accuracy(ds)
+        scored, out_of, messages = get_results(results)
+        self.assertEqual(scored, out_of)
+
+        # add gts_ingest, accuracy should be numeric
+        temp.setncattr("gts_ingest", "true")
+        temp.setncattr("standard_name", "sea_water_practical_salinity")
+        temp.setncattr("accuracy", "45")
+        results = self.ioos.check_accuracy(ds)
+        scored, out_of, messages = get_results(results)
+        self.assertLess(scored, out_of)
+
+        # add numeric for accuracy
+        temp.setncattr("gts_ingest", "true")
+        temp.setncattr("standard_name", "sea_water_practical_salinity")
+        temp.setncattr("accuracy", 45)
+        results = self.ioos.check_accuracy(ds)
+        scored, out_of, messages = get_results(results)
+        self.assertEqual(scored, out_of)
+
     def test_check_contributor_role_and_vocabulary(self):
         ds = MockTimeSeries()  # time, lat, lon, depth
 
@@ -1049,3 +1084,39 @@ class TestIOOS1_2(BaseTestCase):
             nc_obj.variables["depth"].units = units
             result = self.ioos.check_vertical_coordinates(nc_obj)[0]
             self.assertEqual(*result.value)
+
+    def test_check_instrument_make_model_calib_date(self):
+        """
+        Per the IOOS-1.2 spec, instrument variables should have
+        make_model and calibration_date attributes.
+        """
+        ds = MockTimeSeries()  # time, lat, lon, depth
+
+        results = self.ioos.check_instrument_make_model_calib_date(ds)
+        scored, out_of, messages = get_results(results)
+        self.assertEqual(scored, out_of)
+
+        # make an instrument variable
+        temp = ds.createVariable("temperature", "d", dimensions=("time",))
+        temp.setncattr("instrument", "inst")
+        inst = ds.createVariable("inst", "d", dimensions=()) # no make_model or calibration_date
+        results = self.ioos.check_instrument_make_model_calib_date(ds)
+        scored, out_of, messages = get_results(results)
+        self.assertLess(scored, out_of)
+
+        # add make_model
+        inst.setncattr("make_model", "yessir")
+        results = self.ioos.check_instrument_make_model_calib_date(ds)
+        scored, out_of, messages = get_results(results)
+        self.assertLess(scored, out_of)
+
+        # add calibration_date
+        inst.setncattr("calibration_date", "2020-08-19") # not ISO, fail
+        results = self.ioos.check_instrument_make_model_calib_date(ds)
+        scored, out_of, messages = get_results(results)
+        self.assertLess(scored, out_of)
+
+        inst.setncattr("calibration_date", "2020-08-19T00:00:00") # ISO, pass
+        results = self.ioos.check_instrument_make_model_calib_date(ds)
+        scored, out_of, messages = get_results(results)
+        self.assertEqual(scored, out_of)