Repository: ioos/compliance-checker
Pull request #649 (2019-04-18 19:02:12)

Handle vertical min/max, including zero length arrays
Adds unit tests for geospatial vertical min/max.  Also handles case
where all Z axis array is empty or all values are masked by raising a
failure case, as the min/max cannot be established from an empty input
array.


Patch:
diff --git a/compliance_checker/acdd.py b/compliance_checker/acdd.py
index ae2ac29..6175d00 100644
--- a/compliance_checker/acdd.py
+++ b/compliance_checker/acdd.py
@@ -400,30 +400,48 @@ class ACDDBaseCheck(BaseCheck):
         :param netCDF4.Dataset ds: An open netCDF dataset
         :param str z_variable: Name of the variable representing the Z-Axis
         '''
-        vert_min = float(ds.geospatial_vertical_min)
-        vert_max = float(ds.geospatial_vertical_max)
         msgs = []
         total = 2
+        try:
+            vert_min = float(ds.geospatial_vertical_min)
+        except ValueError:
+            msgs.append('geospatial_vertical_min cannot be cast to float')
+
+        try:
+            vert_max = float(ds.geospatial_vertical_max)
+        except ValueError:
+            msgs.append('geospatial_vertical_max cannot be cast to float')
+        if len(msgs) > 0:
+            return Result(BaseCheck.MEDIUM, (0, total),
+                          'geospatial_vertical_extents_match', msgs)
 
         zvalue = ds.variables[z_variable][:]
         # If the array has fill values, which is allowed in the case of point
         # features
         if hasattr(zvalue, 'mask'):
             zvalue = zvalue[~zvalue.mask]
-        zmin = zvalue.min()
-        zmax = zvalue.max()
-        if not np.isclose(vert_min, zmin):
-            msgs.append("geospatial_vertical_min != min(%s) values, %s != %s" % (
-                z_variable,
-                vert_min,
-                zmin
-            ))
-        if not np.isclose(vert_max, zmax):
-            msgs.append("geospatial_vertical_max != max(%s) values, %s != %s" % (
-                z_variable,
-                vert_min,
-                zmax
-            ))
+
+        if zvalue.size == 0:
+            msgs.append("Cannot compare geospatial vertical extents "
+                        "against min/max of data, as non-masked data "
+                        "length is zero")
+            return Result(BaseCheck.MEDIUM, (0, total),
+                          'geospatial_vertical_extents_match', msgs)
+        else:
+            zmin = zvalue.min()
+            zmax = zvalue.max()
+            if not np.isclose(vert_min, zmin):
+                msgs.append("geospatial_vertical_min != min(%s) values, %s != %s" % (
+                    z_variable,
+                    vert_min,
+                    zmin
+                ))
+            if not np.isclose(vert_max, zmax):
+                msgs.append("geospatial_vertical_max != max(%s) values, %s != %s" % (
+                    z_variable,
+                    vert_min,
+                    zmax
+                ))
 
         return Result(BaseCheck.MEDIUM,
                       (total - len(msgs), total),


Test patch:
diff --git a/compliance_checker/tests/helpers.py b/compliance_checker/tests/helpers.py
index 4a77cfd..b103bea 100644
--- a/compliance_checker/tests/helpers.py
+++ b/compliance_checker/tests/helpers.py
@@ -29,9 +29,14 @@ class MockTimeSeries(MockNetCDF):
 
         # give some applicable units
         self.variables["time"].units  = "seconds since 2019-04-11T00:00:00"
+        self.variables["time"].axis  = "T"
         self.variables["lat"].units   = "degree_north"
+        self.variables["lat"].axis  = "Y"
         self.variables["lon"].units   = "degree_east"
+        self.variables["lon"].axis  = "X"
         self.variables["depth"].units = "meters"
+        self.variables["depth"].axis  = "Z"
+        self.variables["depth"].positive = "down"
 
 
 class MockVariable(object):
diff --git a/compliance_checker/tests/test_acdd.py b/compliance_checker/tests/test_acdd.py
index bdde501..1cf0977 100644
--- a/compliance_checker/tests/test_acdd.py
+++ b/compliance_checker/tests/test_acdd.py
@@ -456,21 +456,21 @@ class TestACDD1_3(BaseTestCase):
         # give float geospatial_lat_min/max, test
         ds.setncattr("geospatial_lat_min", -135.)
         ds.setncattr("geospatial_lat_max", -130.)
-        
+
         result = self.acdd.check_lat_extents(ds)
         self.assert_result_is_good(result)
 
         # give string (in number-form), test
         ds.setncattr("geospatial_lat_min", "-135.")
         ds.setncattr("geospatial_lat_max", "-130.")
-        
+
         result = self.acdd.check_lat_extents(ds)
         self.assert_result_is_good(result)
 
         # give garbage string -- expect failure
         ds.setncattr("geospatial_lat_min", "bad")
         ds.setncattr("geospatial_lat_max", "val")
-        
+
         result = self.acdd.check_lat_extents(ds)
         self.assert_result_is_bad(result)
 
@@ -479,7 +479,7 @@ class TestACDD1_3(BaseTestCase):
 
         # create dataset using MockDataset, give it lat/lon dimensions
         ds = MockTimeSeries()
-        ds.variables["lon"][:] = np.linspace(65., 67., num=500) 
+        ds.variables["lon"][:] = np.linspace(65., 67., num=500)
 
         # test no values, expect failure
         #result = self.acdd.check_lon_extents(ds)
@@ -495,21 +495,61 @@ class TestACDD1_3(BaseTestCase):
         # give float geospatial_lon_min/max, test
         ds.setncattr("geospatial_lon_min", 65.)
         ds.setncattr("geospatial_lon_max", 67.)
-        
+
         result = self.acdd.check_lon_extents(ds)
         self.assert_result_is_good(result)
 
         # give string (in number-form), test
         ds.setncattr("geospatial_lon_min", "65.")
         ds.setncattr("geospatial_lon_max", "67.")
-        
+
         result = self.acdd.check_lon_extents(ds)
         self.assert_result_is_good(result)
 
         # give garbage string -- expect failure
         ds.setncattr("geospatial_lon_min", "bad")
         ds.setncattr("geospatial_lon_max", "val")
-        
+
         result = self.acdd.check_lon_extents(ds)
         self.assert_result_is_bad(result)
 
+    def test_check_geospatial_vertical_max(self):
+        ds = MockTimeSeries()
+        ds.variables["depth"][:] = np.linspace(0., 30., num=500)
+
+        # give integer geospatial_vertical_max/min, test
+        ds.setncattr("geospatial_vertical_min", 0)
+        ds.setncattr("geospatial_vertical_max", 30)
+
+        result = self.acdd.check_vertical_extents(ds)
+        self.assert_result_is_good(result)
+
+        # give float geospatial_vertical_min/max, test
+        ds.setncattr("geospatial_vertical_min", 0.)
+        ds.setncattr("geospatial_vertical_max", 30.)
+
+        result = self.acdd.check_vertical_extents(ds)
+        self.assert_result_is_good(result)
+
+        # give string (in number-form), test
+        ds.setncattr("geospatial_vertical_min", "0.")
+        ds.setncattr("geospatial_vertical_max", "30.")
+
+        result = self.acdd.check_vertical_extents(ds)
+        self.assert_result_is_good(result)
+
+        # give garbage string -- expect failure
+        ds.setncattr("geospatial_vertical_min", "bad")
+        ds.setncattr("geospatial_vertical_max", "val")
+
+        result = self.acdd.check_vertical_extents(ds)
+        self.assert_result_is_bad(result)
+
+        # all masked values mean that there are no valid array elements to get
+        # the min/max of
+        ds.setncattr("geospatial_vertical_min", 0.)
+        ds.setncattr("geospatial_vertical_max", 30.)
+        ds.variables["depth"][:] = np.ma.masked_all(ds.variables["depth"]
+                                                    .shape)
+        result = self.acdd.check_vertical_extents(ds)
+        self.assert_result_is_bad(result)