Repository: ioos/compliance-checker

Issue:
Add check for duplicate axis values
No geophysical variable can have two coordinates (coordinate variables and auxiliary coordinate variables) that share the same axis value.



Discussion:

Resolving patch:
diff --git a/compliance_checker/cf/cf.py b/compliance_checker/cf/cf.py
--- a/compliance_checker/cf/cf.py
+++ b/compliance_checker/cf/cf.py
@@ -1932,6 +1932,48 @@ def _get_coord_vars(self, ds):
                 coord_vars.append(name)
         return coord_vars
 
+    def check_duplicate_axis(self, ds):
+        '''
+        Chapter 5 paragraph 6
+
+        If an axis attribute is attached to an auxiliary coordinate variable,
+        it can be used by applications in the same way the `axis` attribute
+        attached to a coordinate variable is used. However, it is not
+        permissible for a [geophysical variable] to have both a coordinate
+        variable and an auxiliary coordinate variable, or more than one of
+        either type of variable, having an `axis` attribute with any given
+        value e.g. there must be no more than one axis attribute for X for any
+        [geophysical variable].
+
+        :param netCDF4.Dataset ds: An open netCDF dataset
+        :rtype: Result
+        :return: List of results
+        '''
+
+        ret_val = []
+        geophysical_variables = self._find_geophysical_vars(ds)
+        for name in geophysical_variables:
+            no_duplicates = TestCtx(BaseCheck.HIGH, '§5.0 Variable {} does not contain duplicate coordinates'.format(name))
+            axis_map = cfutil.get_axis_map(ds, name)
+            axes = []
+            # For every coordinate associated with this variable, keep track of
+            # which coordinates define an axis and assert that there are no
+            # duplicate axis attributes defined in the set of associated
+            # coordinates.
+            for axis, coordinates in axis_map.items():
+                for coordinate in coordinates:
+                    axis = getattr(ds.variables[coordinate], 'axis', None)
+
+                    no_duplicates.assert_true(axis is None or axis not in axes,
+                                              "duplicate axis {} defined by {}".format(axis, coordinate))
+
+                    if axis and axis not in axes:
+                        axes.append(axis)
+
+            ret_val.append(no_duplicates.to_result())
+
+        return ret_val
+
     def check_two_dimensional(self, ds):
         """
         5.2 The latitude and longitude coordinates of a horizontal grid that was


Test patch:
diff --git a/compliance_checker/tests/data/duplicate_axis.cdl b/compliance_checker/tests/data/duplicate_axis.cdl
new file mode 100644
--- /dev/null
+++ b/compliance_checker/tests/data/duplicate_axis.cdl
@@ -0,0 +1,46 @@
+netcdf duplicate_axis {
+dimensions:
+    time = 2;
+    eta_rho = 4 ;
+    eta_u = 4 ;
+    eta_v = 4 ;
+    s_rho = 1 ;
+    xi_rho = 4 ;
+    xi_u = 4 ;
+    xi_v = 4 ;
+variables:
+    double time(time);
+        time:standard_name = "time";
+        time:units = "seconds since 1970-01-01";
+    double lat_u(eta_u, xi_u) ;
+        lat_u:long_name = "latitude of U-points" ;
+        lat_u:units = "degree_north" ;
+        lat_u:standard_name = "latitude" ;
+        lat_u:field = "lat_u, scalar" ;
+        lat_u:axis = "Y";
+    double lon_u(eta_u, xi_u) ;
+        lon_u:long_name = "longitude of U-points" ;
+        lon_u:units = "degree_east" ;
+        lon_u:standard_name = "longitude" ;
+        lon_u:field = "lon_u, scalar" ;
+        lon_u:axis = "X";
+    double lat_rho(eta_rho, xi_rho) ;
+        lat_rho:long_name = "latitude of RHO-points" ;
+        lat_rho:units = "degree_north" ;
+        lat_rho:standard_name = "latitude" ;
+        lat_rho:field = "lat_rho, scalar" ;
+        lat_rho:axis = "Y";
+    double lon_rho(eta_rho, xi_rho) ;
+        lon_rho:long_name = "longitude of RHO-points" ;
+        lon_rho:units = "degree_east" ;
+        lon_rho:standard_name = "longitude" ;
+        lon_rho:field = "lon_rho, scalar" ;
+        lon_rho:axis = "X";
+    float temp(time, s_rho, eta_rho, xi_rho) ;
+        temp:_FillValue = 1.e+37f ;
+        temp:long_name = "potential temperature" ;
+        temp:units = "deg_C" ;
+        temp:coordinates = "lon_u lon_rho lat_rho time" ;
+        temp:field = "temperature, scalar, series" ;
+
+}
diff --git a/compliance_checker/tests/resources.py b/compliance_checker/tests/resources.py
--- a/compliance_checker/tests/resources.py
+++ b/compliance_checker/tests/resources.py
@@ -74,4 +74,5 @@ def generate_dataset(cdl_path, nc_path):
     'chap2'                                : get_filename('tests/data/chap2.cdl'),
     'vertical_coords'                      : get_filename('tests/data/vertical_coords.cdl'),
     'reduced_horizontal_grid'              : get_filename('tests/data/reduced_horizontal_grid.cdl'),
+    'duplicate_axis'                       : get_filename('tests/data/duplicate_axis.cdl'),
 }
diff --git a/compliance_checker/tests/test_cf.py b/compliance_checker/tests/test_cf.py
--- a/compliance_checker/tests/test_cf.py
+++ b/compliance_checker/tests/test_cf.py
@@ -938,6 +938,16 @@ def test_check_units(self):
         assert out_of == 20
         assert messages == []
 
+    def test_check_duplicates(self):
+        '''
+        Test to verify that the check identifies duplicate axes
+        '''
+        dataset = self.load_dataset(STATIC_FILES['duplicate_axis'])
+        results = self.cf.check_duplicate_axis(dataset)
+        result_dict = {result.name: result for result in results}
+        result = result_dict[u'§5.0 Variable temp does not contain duplicate coordinates']
+        assert result.msgs[0] == 'duplicate axis X defined by lon_rho'
+
     def test_64bit(self):
         dataset = self.load_dataset(STATIC_FILES['ints64'])
         suite = CheckSuite()