Repository: ioos/compliance-checker
Pull request #797 (2020-04-15 17:53:54)

Add vertical coordinate variable checks for IOOS 1.2
Checks vertical variables for IOOS 1.2 profile.  Checks any variable
with a "Z" axis that the following criteria are met:

- positive attribute is either "up" or "down"

- units attribute is one of "meter", "inch", "foot", "yard",
  "US_survey_foot", or "fathom", or equivalent abbreviations or plural
  forms.  Note that unlike CF checks, other length units recognized by
  UDUNITS are not accepted.


Patch:
diff --git a/compliance_checker/ioos.py b/compliance_checker/ioos.py
index 6959ad3..19e9b06 100644
--- a/compliance_checker/ioos.py
+++ b/compliance_checker/ioos.py
@@ -6,6 +6,15 @@ import re
 import validators
 
 from lxml.etree import XPath
+from cf_units import Unit
+from compliance_checker.acdd import ACDD1_3Check
+from compliance_checker.cfutil import (get_geophysical_variables,
+                                       get_instrument_variables,
+                                       get_coordinate_variables,
+                                       get_z_variables)
+from compliance_checker import base
+from compliance_checker.base import BaseCheck, BaseNCCheck, Result, TestCtx
+from compliance_checker.cf import util as cf_util # not to be confused with cfutil.py
 from owslib.namespaces import Namespaces
 
 from compliance_checker import base
@@ -1053,6 +1062,55 @@ class IOOS1_2Check(IOOSNCCheck):
 
         return isinstance(val, str) and val.lower() in {"true", "false"}
 
+    def check_vertical_coordinates(self, ds):
+        '''
+        Check that vertical units (corresponding to axis "Z") are a unit
+        equivalent to one of "meter", "inch", "foot", "yard", "US_survey_foot",
+        "mile", or "fathom".  Check that the vertical coordinate variable
+        "positive" attribute is either "up" or "down".  Note that unlike the CF
+        version of this check, pressure units are not accepted and length units
+        are constrained to the aforementioned set instead of accepting any valid
+        UDUNITS length unit.
+
+        :param netCDF4.Dataset ds: An open netCDF dataset
+        :rtype: list
+        :return: List of results
+        '''
+        ret_val = []
+        for name in get_z_variables(ds):
+            variable = ds.variables[name]
+            units_str = getattr(variable, 'units', None)
+            positive = getattr(variable, 'positive', None)
+            expected_unit_strs = ('meter', 'inch', 'foot', 'yard',
+                                  'US_survey_foot', 'mile', 'fathom')
+
+            unit_def_set = {Unit(unit_str).definition for unit_str
+                               in expected_unit_strs}
+
+            try:
+                units = Unit(units_str)
+                pass_stat = units.definition in unit_def_set
+            # unknown unit not convertible to UDUNITS
+            except ValueError:
+                pass_stat = False
+
+            valid_vertical_coord = TestCtx(BaseCheck.HIGH,
+                                           "Vertical coordinates")
+            units_set_msg = ("{}'s units attribute {} is not equivalent to one "
+                             "of {}".format(name, units_str,
+                                            expected_unit_strs))
+            valid_vertical_coord.assert_true(pass_stat, units_set_msg)
+
+            pos_msg = ("{}: vertical coordinates must include a positive "
+                       "attribute that is either 'up' or 'down'".format(name))
+            valid_vertical_coord.assert_true(positive in ('up', 'down'),
+                                             pos_msg)
+
+
+            ret_val.append(valid_vertical_coord.to_result())
+
+        return ret_val
+
     def check_gts_ingest_global(self, ds):
         """
         Check if a dataset has the global attribute "gts_ingest" and that


Test patch:
diff --git a/compliance_checker/tests/test_ioos_profile.py b/compliance_checker/tests/test_ioos_profile.py
index a7d46f1..29a629d 100644
--- a/compliance_checker/tests/test_ioos_profile.py
+++ b/compliance_checker/tests/test_ioos_profile.py
@@ -1002,3 +1002,36 @@ class TestIOOS1_2(BaseTestCase):
         self.assertFalse(self.ioos.check_ioos_ingest(ds).value)
         ds.setncattr("ioos_ingest", 0)
         self.assertFalse(self.ioos.check_ioos_ingest(ds).value)
+
+    def test_vertical_dimension(self):
+        # MockTimeSeries has a depth variable, with axis of 'Z', units of 'm',
+        # and positive = 'down'
+        nc_obj = MockTimeSeries()
+        result = self.ioos.check_vertical_coordinates(nc_obj)[0]
+        self.assertEqual(*result.value)
+        nc_obj.variables['depth'].positive = 'upwards'
+        result = self.ioos.check_vertical_coordinates(nc_obj)[0]
+        self.assertNotEqual(*result.value)
+        nc_obj.variables['depth'].positive = 'up'
+        result = self.ioos.check_vertical_coordinates(nc_obj)[0]
+        self.assertEqual(*result.value)
+        # test units
+        nc_obj.variables['depth'].units = 'furlong'
+        result = self.ioos.check_vertical_coordinates(nc_obj)[0]
+        expected_msg = ("depth's units attribute furlong is not equivalent to "
+                        "one of ('meter', 'inch', 'foot', 'yard', "
+                        "'US_survey_foot', 'mile', 'fathom')")
+        self.assertEqual(result.msgs[0], expected_msg)
+        self.assertNotEqual(*result.value)
+        accepted_units = ("meter", "meters", "inch", "foot", "yard", "mile",
+                          "miles", "US_survey_foot", "US_survey_feet", "fathom",
+                          "fathoms", "international_inch",
+                          "international_inches", "international_foot",
+                          "international_feet", "international_yard",
+                          "international_yards", "international_mile",
+                          "international_miles", "inches", "in", "feet", "ft",
+                          "yd", "mi")
+        for units in accepted_units:
+            nc_obj.variables['depth'].units = units
+            result = self.ioos.check_vertical_coordinates(nc_obj)[0]
+            self.assertEqual(*result.value)