Repository: ioos/compliance-checker
Pull request #685 (2019-10-28 14:47:51)

Trac Ticket #143: Computed Standard Name
Issue summary: Some vertical coordinates are functions of other variables,
thereby defining a need to describe the appropriate method to compute their
exact values. The computed_standard_name maps to the parameterized vertical
coordinate.

Enable the checking of the computed_standard_name attribute from Appendix D
in CF-1.7 while maintaining compatiblity with CF-1.6. Add the appropriate values
to the Appendix and corresponding unit tests.


Patch:
diff --git a/compliance_checker/cf/__init__.py b/compliance_checker/cf/__init__.py
index 1641de9..6451dd4 100644
--- a/compliance_checker/cf/__init__.py
+++ b/compliance_checker/cf/__init__.py
@@ -4,11 +4,12 @@ from compliance_checker.cf.cf import (
     util,
 )
 
-from compliance_checker.cf.appendix_d import dimless_vertical_coordinates
+from compliance_checker.cf.appendix_d import dimless_vertical_coordinates_1_6, dimless_vertical_coordinates_1_7
 
 __all__ = [
     'CF1_6Check',
     'CF1_7Check',
-    'dimless_vertical_coordinates',
+    'dimless_vertical_coordinates_1_6',
+    'dimless_vertical_coordinates_1_7',
     'util',
 ]
diff --git a/compliance_checker/cf/appendix_d.py b/compliance_checker/cf/appendix_d.py
index 3c4b9a9..f4b55b4 100644
--- a/compliance_checker/cf/appendix_d.py
+++ b/compliance_checker/cf/appendix_d.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 '''
-Appendix D compliance support for CF 1.6
+Appendix D compliance support for CF 1.6 and CF 1.7
 
 The definitions given here allow an application to compute dimensional
 coordinate values from the dimensionless ones and associated variables. The
@@ -22,35 +22,39 @@ in general forms that may be simplified by omitting certain terms. A term that
 is omitted from the formula_terms attribute should be assumed to be zero.
 '''
 
-# Contains the standard name followed by the set of expected formula terms
-dimless_vertical_coordinates = {
-    "atmosphere_ln_pressure_coordinate": {'p0', 'lev'},
-    "atmosphere_sigma_coordinate": {'sigma', 'ps', 'ptop'},
-    "atmosphere_hybrid_sigma_pressure_coordinate":
-       ({'a', 'b', 'ps'}, {'ap', 'b', 'ps'}),
-    "atmosphere_hybrid_height_coordinate": {'a', 'b', 'orog'},
-    "atmosphere_sleve_coordinate":
-       {'a', 'b1', 'b2', 'ztop', 'zsurf1', 'zsurf2'},
-    "ocean_sigma_coordinate": {'sigma', 'eta', 'depth'},
-    "ocean_s_coordinate": {'s', 'eta', 'depth', 'a', 'b', 'depth_c'},
-    "ocean_sigma_z_coordinate":
-       {'sigma', 'eta', 'depth', 'depth_c', 'nsigma', 'zlev'},
-    "ocean_double_sigma_coordinate":
-       {'sigma', 'depth', 'z1', 'z2', 'a', 'href', 'k_c'},
-    # This comes from CF 1.7 but is used in circulation so we include it
-    # TODO (badams): include these *only* with the CF 1.7 checker once we get
-    # around to writing it
-    "ocean_s_coordinate_g1": {'s', 'C', 'eta', 'depth', 'depth_c'},
-    "ocean_s_coordinate_g2": {'s', 'C', 'eta', 'depth', 'depth_c'}
- }
+# Contains the standard name followed by a 2-tuple:
+# (the set of expected formula terms, set of computed_standard_name(s)). Most
+# vertical coordinates only have one computed_standard_name, but some have
+# multiple acceptable values.
+ocean_computed_standard_names = {
+    'altitude', 'height_above_geopotential_datum',
+    'height_above_reference_ellipsoid', 'height_above_mean_sea_level'
+}
+dimless_vertical_coordinates_1_6 = { # only for CF-1.6
+    "atmosphere_ln_pressure_coordinate"          : ({'p0', 'lev'}, {'air_pressure'}),
+    "atmosphere_sigma_coordinate"                : ({'sigma', 'ps', 'ptop'}, {'air_pressure'}),
+    "atmosphere_hybrid_sigma_pressure_coordinate": (({'a', 'b', 'ps'}, {'ap', 'b', 'ps'}), {'air_pressure'}),
+    "atmosphere_hybrid_height_coordinate"        : ({'a', 'b', 'orog'}, {'altitude', 'height_above_geopotential_datum'}),
+    "atmosphere_sleve_coordinate"                : ({'a', 'b1', 'b2', 'ztop', 'zsurf1', 'zsurf2'}, {'altitude', 'height_above_geopotential_datum'}),
+    "ocean_sigma_coordinate"                     : ({'sigma', 'eta', 'depth'}, ocean_computed_standard_names),
+    "ocean_s_coordinate"                         : ({'s', 'eta', 'depth', 'a', 'b', 'depth_c'}, ocean_computed_standard_names),
+    "ocean_sigma_z_coordinate"                   : ({'sigma', 'eta', 'depth', 'depth_c', 'nsigma', 'zlev'}, ocean_computed_standard_names),
+    "ocean_double_sigma_coordinate"              : ({'sigma', 'depth', 'z1', 'z2', 'a', 'href', 'k_c'}, ocean_computed_standard_names)
+}
 
-def no_missing_terms(formula_name, term_set):
+dimless_vertical_coordinates_1_7 = dimless_vertical_coordinates_1_6.copy() # shallow copy
+dimless_vertical_coordinates_1_7.update({ # extends 1.6
+    "ocean_s_coordinate_g1": ({'s', 'C', 'eta', 'depth', 'depth_c'}, ocean_computed_standard_names),
+    "ocean_s_coordinate_g2": ({'s', 'C', 'eta', 'depth', 'depth_c'}, ocean_computed_standard_names)
+ })
+
+def no_missing_terms(formula_name, term_set, dimless_vertical_coordinates):
     """
     Returns true if the set is not missing terms corresponding to the
     entries in Appendix D, False otherwise.  The set of terms should be exactly
     equal, and not contain more or less terms than expected.
     """
-    reqd_terms = dimless_vertical_coordinates[formula_name]
+    reqd_terms = dimless_vertical_coordinates[formula_name][0]
     def has_all_terms(reqd_termset):
         return len(reqd_termset ^ term_set) == 0
 
diff --git a/compliance_checker/cf/cf.py b/compliance_checker/cf/cf.py
index ae0571a..d8c5907 100644
--- a/compliance_checker/cf/cf.py
+++ b/compliance_checker/cf/cf.py
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals, division, print_function
 from compliance_checker.base import BaseCheck, BaseNCCheck, Result, TestCtx
-from compliance_checker.cf.appendix_d import (dimless_vertical_coordinates,
+from compliance_checker.cf.appendix_d import (dimless_vertical_coordinates_1_6, dimless_vertical_coordinates_1_7,
                                               no_missing_terms)
 from compliance_checker.cf.appendix_e import cell_methods16, cell_methods17
 from compliance_checker.cf.appendix_f import grid_mapping_dict16, grid_mapping_dict17
@@ -166,6 +166,82 @@ class CFBaseCheck(BaseCheck):
             reasoning = ['§2.6.1 Conventions field is not present']
         return Result(BaseCheck.MEDIUM, valid, self.section_titles['2.6'], msgs=reasoning)
 
+    def _check_dimensionless_vertical_coordinates(self, ds, deprecated_units, version_specific_check, version_specific_dimless_vertical_coord_dict):
+        '''
+        Check the validity of dimensionless coordinates under CF
+
+        :param netCDF4.Dataset ds: An open netCDF dataset
+        :param list deprecated_units: list of string names of deprecated units
+        :param function version_specific_check: version-specific implementation to check dimensionless vertical coord
+        :param dict version_specific_dimless_coord_dict: version-specific dict of dimensionless vertical coords and computed standard names
+        :return: List of results
+        '''
+        ret_val = []
+
+        z_variables = cfutil.get_z_variables(ds)
+
+        # call version-specific implementation
+        for name in z_variables:
+            version_specific_check(ds, name, deprecated_units, ret_val, version_specific_dimless_vertical_coord_dict)
+
+        return ret_val
+
+    def _check_formula_terms(self, ds, coord, dimless_coords_dict):
+        '''
+        Checks a dimensionless vertical coordinate contains valid formula_terms
+
+        - formula_terms is a non-empty string
+        - formula_terms matches regdimless_coords_dictx
+        - every variable defined in formula_terms exists
+
+        :param netCDF4.Dataset ds: An open netCDF dataset
+        :rtype: compliance_checker.base.Result
+        '''
+        variable = ds.variables[coord]
+        standard_name = getattr(variable, 'standard_name', None)
+        formula_terms = getattr(variable, 'formula_terms', None)
+        valid_formula_terms = TestCtx(BaseCheck.HIGH, self.section_titles['4.3'])
+
+        valid_formula_terms.assert_true(isinstance(formula_terms, basestring) and formula_terms,
+                                        '§4.3.2: {}\'s formula_terms is a required attribute and must be a non-empty string'
+                                        ''.format(coord))
+        # We can't check any more
+        if not formula_terms:
+            return valid_formula_terms.to_result()
+
+        # check that the formula_terms are well formed and are present
+        # The pattern for formula terms is always component: variable_name
+        # the regex grouping always has component names in even positions and
+        # the corresponding variable name in odd positions.
+        matches = regex.findall(r'([A-Za-z][A-Za-z0-9_]*: )([A-Za-z][A-Za-z0-9_]*)',
+                                variable.formula_terms)
+        terms = set(m[0][:-2] for m in matches)
+        # get the variables named in the formula terms and check if any
+        # are not present in the dataset
+        missing_vars = sorted(set(m[1] for m in matches) - set(ds.variables))
+        missing_fmt = "The following variable(s) referenced in {}:formula_terms are not present in the dataset: {}"
+        valid_formula_terms.assert_true(len(missing_vars) == 0,
+                                    missing_fmt.format(coord, ', '.join(missing_vars)))
+        # try to reconstruct formula_terms by adding space in between the regex
+        # matches.  If it doesn't exactly match the original, the formatting
+        # of the attribute is incorrect
+        reconstructed_formula = ' '.join(m[0] + m[1] for m in matches)
+        valid_formula_terms.assert_true(reconstructed_formula == formula_terms,
+                                        "Attribute formula_terms is not well-formed")
+
+        valid_formula_terms.assert_true(standard_name in
+                                        dimless_coords_dict,
+                                        "unknown standard_name '{}' for dimensionless vertical coordinate {}"
+                                        "".format(standard_name, coord))
+        if standard_name not in dimless_coords_dict:
+            return valid_formula_terms.to_result()
+
+        valid_formula_terms.assert_true(no_missing_terms(standard_name, terms, dimless_coords_dict),
+                                        "{}'s formula_terms are invalid for {}, please see appendix D of CF 1.6"
+                                        "".format(coord, standard_name))
+
+        return valid_formula_terms.to_result()
+
     def _dims_in_order(self, dimension_order):
         '''
         :param list dimension_order: A list of axes
@@ -919,20 +995,6 @@ class CF1_6Check(CFNCCheck):
 
         return valid_fill_range.to_result()
 
-    #def check_conventions_are_cf_16(self, ds):
-    #    '''
-    #    Check the global attribute conventions to contain CF-1.6.
-
-    #    CF §2.6.1 the NUG defined global attribute Conventions to the string
-    #    value "CF-1.6"
-
-    #    :param netCDF4.Dataset ds: An open netCDF dataset
-    #    :rtype: compliance_checker.base.Result
-    #    '''
-
-    #    return self.check_conventions_version(ds) # invoke inherited method
-
-
     def check_convention_globals(self, ds):
         '''
         Check the common global attributes are strings if they exist.
@@ -1808,7 +1870,7 @@ class CF1_6Check(CFNCCheck):
 
         return ret_val
 
-    def check_dimensional_vertical_coordinate(self, ds):
+    def check_dimensional_vertical_coordinate(self, ds, dimless_vertical_coordinates=dimless_vertical_coordinates_1_6):
         '''
         Check units for variables defining vertical position are valid under
         CF.
@@ -1864,7 +1926,36 @@ class CF1_6Check(CFNCCheck):
 
         return ret_val
 
-    def check_dimensionless_vertical_coordinate(self, ds):
+    def _check_dimensionless_vertical_coordinate_1_6(self, ds, vname, deprecated_units, ret_val, dim_vert_coords_dict):
+        """
+        Check that a dimensionless vertical coordinate variable is valid under
+        CF-1.6.
+
+        :param netCDF4.Dataset ds: open netCDF4 dataset
+        :param str name: variable name
+        :param list ret_val: array to append Results to
+        :rtype None
+        """
+        variable = ds.variables[vname]
+        standard_name = getattr(variable, 'standard_name', None)
+        units = getattr(variable, 'units', None)
+        formula_terms = getattr(variable, 'formula_terms', None)
+        # Skip the variable if it's dimensional
+        if (formula_terms is None and
+            standard_name not in dim_vert_coords_dict):
+            return
+
+        is_not_deprecated = TestCtx(BaseCheck.LOW, self.section_titles["4.3"])
+
+        is_not_deprecated.assert_true(units not in deprecated_units,
+                                      "§4.3.2: units are deprecated by CF in variable {}: {}"
+                                      "".format(vname, units))
+
+        # check the vertical coordinates
+        ret_val.append(is_not_deprecated.to_result())
+        ret_val.append(self._check_formula_terms(ds, vname, dim_vert_coords_dict))
+
+    def check_dimensionless_vertical_coordinates(self, ds):
         '''
         Check the validity of dimensionless coordinates under CF
 
@@ -1894,82 +1985,16 @@ class CF1_6Check(CFNCCheck):
             'layer',
             'sigma_level'
         ]
-        for name in z_variables:
-            variable = ds.variables[name]
-            standard_name = getattr(variable, 'standard_name', None)
-            units = getattr(variable, 'units', None)
-            formula_terms = getattr(variable, 'formula_terms', None)
-            # Skip the variable if it's dimensional
-            if (formula_terms is None and
-                standard_name not in dimless_vertical_coordinates):
-                continue
-
-            is_not_deprecated = TestCtx(BaseCheck.LOW, self.section_titles["4.3"])
 
-            is_not_deprecated.assert_true(units not in deprecated_units,
-                                          "§4.3.2: units are deprecated by CF in variable {}: {}"
-                                          "".format(name, units))
-            ret_val.append(is_not_deprecated.to_result())
-            ret_val.append(self._check_formula_terms(ds, name))
+        ret_val.extend(self._check_dimensionless_vertical_coordinates(
+            ds,
+            deprecated_units,
+            self._check_dimensionless_vertical_coordinate_1_6,
+            dimless_vertical_coordinates_1_6)
+        )
 
         return ret_val
 
-    def _check_formula_terms(self, ds, coord):
-        '''
-        Checks a dimensionless vertical coordinate contains valid formula_terms
-
-        - formula_terms is a non-empty string
-        - formula_terms matches regx
-        - every variable defined in formula_terms exists
-
-        :param netCDF4.Dataset ds: An open netCDF dataset
-        :rtype: compliance_checker.base.Result
-        '''
-        variable = ds.variables[coord]
-        standard_name = getattr(variable, 'standard_name', None)
-        formula_terms = getattr(variable, 'formula_terms', None)
-        valid_formula_terms = TestCtx(BaseCheck.HIGH, self.section_titles['4.3'])
-
-        valid_formula_terms.assert_true(isinstance(formula_terms, basestring) and formula_terms,
-                                        '§4.3.2: {}\'s formula_terms is a required attribute and must be a non-empty string'
-                                        ''.format(coord))
-        # We can't check any more
-        if not formula_terms:
-            return valid_formula_terms.to_result()
-
-        # check that the formula_terms are well formed and are present
-        # The pattern for formula terms is always component: variable_name
-        # the regex grouping always has component names in even positions and
-        # the corresponding variable name in odd positions.
-        matches = regex.findall(r'([A-Za-z][A-Za-z0-9_]*: )([A-Za-z][A-Za-z0-9_]*)',
-                                variable.formula_terms)
-        terms = set(m[0][:-2] for m in matches)
-        # get the variables named in the formula terms and check if any
-        # are not present in the dataset
-        missing_vars = sorted(set(m[1] for m in matches) - set(ds.variables))
-        missing_fmt = "The following variable(s) referenced in {}:formula_terms are not present in the dataset: {}"
-        valid_formula_terms.assert_true(len(missing_vars) == 0,
-                                    missing_fmt.format(coord, ', '.join(missing_vars)))
-        # try to reconstruct formula_terms by adding space in between the regex
-        # matches.  If it doesn't exactly match the original, the formatting
-        # of the attribute is incorrect
-        reconstructed_formula = ' '.join(m[0] + m[1] for m in matches)
-        valid_formula_terms.assert_true(reconstructed_formula == formula_terms,
-                                        "Attribute formula_terms is not well-formed")
-
-        valid_formula_terms.assert_true(standard_name in
-                                        dimless_vertical_coordinates,
-                                        "unknown standard_name '{}' for dimensionless vertical coordinate {}"
-                                        "".format(standard_name, coord))
-        if standard_name not in dimless_vertical_coordinates:
-            return valid_formula_terms.to_result()
-
-        valid_formula_terms.assert_true(no_missing_terms(standard_name, terms),
-                                        "{}'s formula_terms are invalid for {}, please see appendix D of CF 1.6"
-                                        "".format(coord, standard_name))
-
-        return valid_formula_terms.to_result()
-
     def check_time_coordinate(self, ds):
         '''
         Check variables defining time are valid under CF
@@ -3755,18 +3780,82 @@ class CF1_7Check(CF1_6Check):
                                                 v_datum_str))
         return len(res_set.fetchall()) > 0
 
-    #def check_conventions_are_cf_1_7(self, ds):
-    #    '''
-    #    Check the global attribute conventions to contain CF-1.7.
+    def _check_dimensionless_vertical_coordinate_1_7(self, ds, vname, deprecated_units, ret_val, dim_vert_coords_dict):
+        """
+        Check that a dimensionless vertical coordinate variable is valid under
+        CF-1.7.
 
-    #    CF §2.6.1 the NUG defined global attribute Conventions to the string
-    #    value "CF-1.7"
+        :param netCDF4.Dataset ds: open netCDF4 dataset
+        :param str name: variable name
+        :param list ret_val: array to append Results to
+        :rtype None
+        """
+        variable = ds.variables[vname]
+        standard_name = getattr(variable, 'standard_name', None)
+        units = getattr(variable, 'units', None)
+        formula_terms = getattr(variable, 'formula_terms', None)
+        # Skip the variable if it's dimensional
+        if (formula_terms is None and
+            standard_name not in dim_vert_coords_dict):
+            return
+
+        # assert that the computed_standard_name is maps to the standard_name correctly
+        correct_computed_std_name_ctx = TestCtx(BaseCheck.MEDIUM, self.section_titles['4.3'])
+        _comp_std_name = dim_vert_coords_dict[standard_name][1]
+        correct_computed_std_name_ctx.assert_true(
+            getattr(variable, 'computed_standard_name', None) in _comp_std_name,
+            '§4.3.3 The standard_name of `{}` must map to the correct computed_standard_name, `{}`'.format(vname, _comp_std_name)
+        )
+        ret_val.append(correct_computed_std_name_ctx.to_result())
+
+    def check_dimensionless_vertical_coordinates(self, ds):
+        '''
+        Check the validity of dimensionless coordinates under CF
 
-    #    :param netCDF4.Dataset ds: An open netCDF dataset
-    #    :rtype: compliance_checker.base.Result
-    #    '''
+        CF §4.3.2 The units attribute is not required for dimensionless
+        coordinates.
 
-    #    return self.check_conventions_version(ds) # invoke inherited method
+        The standard_name attribute associates a coordinate with its definition
+        from Appendix D, Dimensionless Vertical Coordinates. The definition
+        provides a mapping between the dimensionless coordinate values and
+        dimensional values that can positively and uniquely indicate the
+        location of the data.
+
+        A new attribute, formula_terms, is used to associate terms in the
+        definitions with variables in a netCDF file.  To maintain backwards
+        compatibility with COARDS the use of these attributes is not required,
+        but is strongly recommended.
+
+        :param netCDF4.Dataset ds: An open netCDF dataset
+        :rtype: list
+        :return: List of results
+        '''
+        ret_val = []
+
+        z_variables = cfutil.get_z_variables(ds)
+        deprecated_units = [
+            'level',
+            'layer',
+            'sigma_level'
+        ]
+
+        # compose this function to use the results from the CF-1.6 check
+        # and then extend it using a CF-1.7 addition
+        ret_val.extend(self._check_dimensionless_vertical_coordinates(
+            ds,
+            deprecated_units,
+            self._check_dimensionless_vertical_coordinate_1_6,
+            dimless_vertical_coordinates_1_7)
+        ) 
+
+        ret_val.extend(self._check_dimensionless_vertical_coordinates(
+            ds,
+            deprecated_units,
+            self._check_dimensionless_vertical_coordinate_1_7,
+            dimless_vertical_coordinates_1_7)
+        ) 
+
+        return ret_val
      
 
 class CFNCCheck(BaseNCCheck, CFBaseCheck):


Test patch:
diff --git a/compliance_checker/tests/test_cf.py b/compliance_checker/tests/test_cf.py
index 8dc80eb..9a3143f 100644
--- a/compliance_checker/tests/test_cf.py
+++ b/compliance_checker/tests/test_cf.py
@@ -3,7 +3,7 @@
 
 from compliance_checker.suite import CheckSuite
 from compliance_checker.cf import (CF1_6Check, CF1_7Check,
-                                   dimless_vertical_coordinates)
+                                   dimless_vertical_coordinates_1_6, dimless_vertical_coordinates_1_7)
 from compliance_checker.cf.util import (is_vertical_coordinate,
                                         is_time_variable,
                                         units_convertible, units_temporal,
@@ -697,40 +697,40 @@ class TestCF1_6(BaseTestCase):
         # For each of the listed dimensionless vertical coordinates,
         # verify that the formula_terms match the provided set of terms
         self.assertTrue(no_missing_terms('atmosphere_ln_pressure_coordinate',
-                                         {"p0", "lev"}))
+                                         {"p0", "lev"}, dimless_vertical_coordinates_1_6))
         self.assertTrue(no_missing_terms('atmosphere_sigma_coordinate',
-                                         {"sigma", "ps", "ptop"}))
+                                         {"sigma", "ps", "ptop"}, dimless_vertical_coordinates_1_6))
         self.assertTrue(no_missing_terms('atmosphere_hybrid_sigma_pressure_coordinate',
-                                         {'a', 'b', 'ps'}))
+                                         {'a', 'b', 'ps'}, dimless_vertical_coordinates_1_6))
         # test alternative terms for
         # 'atmosphere_hybrid_sigma_pressure_coordinate'
         self.assertTrue(no_missing_terms('atmosphere_hybrid_sigma_pressure_coordinate',
-                                         {'ap', 'b', 'ps'}))
+                                         {'ap', 'b', 'ps'}, dimless_vertical_coordinates_1_6))
         # check that an invalid set of terms fails
         self.assertFalse(no_missing_terms('atmosphere_hybrid_sigma_pressure_coordinate',
-                                          {'a', 'b', 'p'}))
+                                          {'a', 'b', 'p'}, dimless_vertical_coordinates_1_6))
         self.assertTrue(no_missing_terms('atmosphere_hybrid_height_coordinate',
-                                          {"a", "b", "orog"}))
+                                          {"a", "b", "orog"}, dimless_vertical_coordinates_1_6))
         # missing terms should cause failure
         self.assertFalse(no_missing_terms('atmosphere_hybrid_height_coordinate',
-                                          {"a", "b"}))
+                                          {"a", "b"}, dimless_vertical_coordinates_1_6))
         # excess terms should cause failure
         self.assertFalse(no_missing_terms('atmosphere_hybrid_height_coordinate',
-                                         {"a", "b", "c", "orog"}))
+                                         {"a", "b", "c", "orog"}, dimless_vertical_coordinates_1_6))
         self.assertTrue(no_missing_terms('atmosphere_sleve_coordinate',
                                          {"a", "b1", "b2", "ztop", "zsurf1",
-                                          "zsurf2"}))
+                                          "zsurf2"}, dimless_vertical_coordinates_1_6))
         self.assertTrue(no_missing_terms('ocean_sigma_coordinate',
-                                         {"sigma", "eta", "depth"}))
+                                         {"sigma", "eta", "depth"}, dimless_vertical_coordinates_1_6))
         self.assertTrue(no_missing_terms('ocean_s_coordinate',
                                          {"s", "eta", "depth", "a", "b",
-                                          "depth_c"}))
+                                          "depth_c"}, dimless_vertical_coordinates_1_6))
         self.assertTrue(no_missing_terms('ocean_sigma_z_coordinate',
                                          {"sigma", "eta", "depth", "depth_c",
-                                          "nsigma", "zlev"}))
+                                          "nsigma", "zlev"}, dimless_vertical_coordinates_1_6))
         self.assertTrue(no_missing_terms('ocean_double_sigma_coordinate',
                                          {"sigma", "depth", "z1", "z2", "a",
-                                          "href", "k_c"}))
+                                          "href", "k_c"}, dimless_vertical_coordinates_1_6))
 
     def test_dimensionless_vertical(self):
         '''
@@ -738,7 +738,7 @@ class TestCF1_6(BaseTestCase):
         '''
         # Check affirmative compliance
         dataset = self.load_dataset(STATIC_FILES['dimensionless'])
-        results = self.cf.check_dimensionless_vertical_coordinate(dataset)
+        results = self.cf.check_dimensionless_vertical_coordinates(dataset)
         scored, out_of, messages = get_results(results)
 
         # all variables checked (2) pass
@@ -749,7 +749,7 @@ class TestCF1_6(BaseTestCase):
         # Check negative compliance -- 3 out of 4 pass
 
         dataset = self.load_dataset(STATIC_FILES['bad'])
-        results = self.cf.check_dimensionless_vertical_coordinate(dataset)
+        results = self.cf.check_dimensionless_vertical_coordinates(dataset)
         scored, out_of, messages = get_results(results)
         assert len(results) == 4
         assert scored <= out_of
@@ -763,7 +763,7 @@ class TestCF1_6(BaseTestCase):
 
         # create a malformed formula_terms attribute and check that it fails
         # 2/4 still pass
-        results = self.cf.check_dimensionless_vertical_coordinate(dataset)
+        results = self.cf.check_dimensionless_vertical_coordinates(dataset)
         scored, out_of, messages = get_results(results)
 
         assert len(results) == 4
@@ -1439,3 +1439,90 @@ class TestCF1_7(BaseTestCase):
             dataset.setncattr("Conventions", "CF-1.7, ACDD-1.3")
             result = self.cf.check_conventions_version(dataset)
             self.assertTrue(result.value)
+
+    def test_appendix_d(self):
+        '''
+        CF 1.7
+        Appendix D
+
+        As the CF-1.7 dimensionless vertical coordinates dict extends the 1.6 version,
+        this test only examines the extensions made there.
+        '''
+
+        # For each of the listed dimensionless vertical coordinates,
+        # verify that the formula_terms match the provided set of terms
+        self.assertTrue(no_missing_terms('ocean_s_coordinate_g1',
+                                         {'s', 'C', 'eta', 'depth', 'depth_c'}, dimless_vertical_coordinates_1_7))
+        self.assertTrue(no_missing_terms('ocean_s_coordinate_g2',
+                                         {'s', 'C', 'eta', 'depth', 'depth_c'}, dimless_vertical_coordinates_1_7))
+
+    def test_check_dimensionless_vertical_coordinate_1_7(self):
+        '''
+        Unit test for _check_dimensionless_vertical_coordinate_1_7 method.
+        '''
+        deprecated_units = [
+            'level',
+            'layer',
+            'sigma_level'
+        ]
+
+        ret_val = []
+
+        # create mock dataset for test; create three variables, one as dimensionless
+        with MockTimeSeries() as dataset:
+            dataset.createVariable('lev', 'd') # dtype=double, dims=1
+            dataset.variables['lev'].setncattr('standard_name', 'atmosphere_sigma_coordinate')
+            dataset.variables['lev'].setncattr('formula_terms', 'sigma: lev ps: PS ptop: PTOP')
+            
+            dataset.createVariable('PS', 'd', ('time',)) # dtype=double, dims=time
+            dataset.createVariable('PTOP', 'd', ('time',)) # dtype=double, dims=time
+
+
+            # run the check
+            self.cf._check_dimensionless_vertical_coordinate_1_7(
+                dataset, 'lev', deprecated_units, ret_val, dimless_vertical_coordinates_1_7
+            )
+
+            # one should have failed, as no computed_standard_name is assigned
+            score, out_of, messages = get_results(ret_val)
+            assert score == 0
+            assert out_of == 1
+
+            # this time, assign compufted_standard_name
+            ret_val = []
+            dataset.variables['lev'].setncattr('computed_standard_name', 'air_pressure')
+
+            # run the check
+            self.cf._check_dimensionless_vertical_coordinate_1_7(
+                dataset, 'lev', deprecated_units, ret_val, dimless_vertical_coordinates_1_7
+            )
+
+            # computed_standard_name is assigned, should pass
+            score, out_of, messages = get_results(ret_val)
+            assert score == out_of
+        
+    def test_dimensionless_vertical(self):
+        '''
+        Section 4.3.2 check, but for CF-1.7 implementation. With the refactor in
+        place, these are more of integration tests, but kept here for simplicity.
+        '''
+        # Check affirmative compliance
+        dataset = self.load_dataset(STATIC_FILES['dimensionless'])
+        dataset.variables['lev'] = MockVariable(dataset.variables['lev'])
+        dataset.variables['lev'].computed_standard_name = 'air_pressure'
+        results = self.cf.check_dimensionless_vertical_coordinates(dataset)
+        scored, out_of, messages = get_results(results)
+
+        # all variables checked (2) pass
+        assert len(results) == 3
+        assert scored == out_of
+        assert all(r.name == u"§4.3 Vertical Coordinate" for r in results)
+
+        # make one variable's computed_standard_name incorrect, one should fail
+        dataset.variables['lev'].computed_standard_name = 'definitely_not_right'
+        results = self.cf.check_dimensionless_vertical_coordinates(dataset)
+        scored, out_of, messages = get_results(results)
+
+        assert len(results) == 3
+        assert scored < out_of
+        assert all(r.name == u"§4.3 Vertical Coordinate" for r in results)