Repository: ioos/compliance-checker

Issue:
§5.6: when checking for required standard names for grid mappings -- error when more than one occurrence
Grid mappings require some variables with specific `standard_name`s to exist (CF §5.6 and Appendix F). Namely, two variables with the `standard_name`s `projection_x_coordinate` and `projection_y_coordinate` have to exists. The IOOS Compliance Checker checks this in the function `check_grid_mapping` close to line 2462:

```
            # Make sure that exactly one variable is defined for each of the required standard_names
            expected_std_names = grid_mapping[2]
            for expected_std_name in expected_std_names:
                found_vars = ds.get_variables_by_attributes(standard_name=expected_std_name)
                valid_grid_mapping.assert_true(len(found_vars) == 1,
                                               "grid mapping {} requires exactly".format(grid_mapping_name)+\
                                               "one variable with standard_name "+\
                                               "{} to be defined".format(expected_std_name))
```

An error is thrown if there is not exactly one occurrence (`assert_true(len(found_vars) == 1,`). **However, the CF Conventions do not explicitly forbid the existence of more than one variable per required attribute. In my opinion an error should be only thrown if the occurrence is zero.**

Below an exemplary NetCDF file header, which uses `projection_x_coordinate` three times. `x` and `y` are the locations of the grid cell centers. `xu` are the locations of the x-boundaries of the grid cells and `vy` are the locations of the y-boundaries of the grid cells.

```
netcdf test_projections {
dimensions:
        time = UNLIMITED ; // (4 currently)
        x = 4 ;
        y = 7 ;
        z = 59 ;
        xu = 5 ;
        yv = 8 ;
        nv = 2 ;
variables:
        float conc(time, z, y, x) ;
            conc:standard_name = "mole_concentration_of_nox_expressed_as_nitrogen_in_air" ;
            conc:units = "mol m-3" ;
            conc:coordinates = "lat lon" ;
            conc:grid_mapping = "lambert_conformal" ;
        float xwind(time, z, y, xu) ;
            xwind:standard_name = "x_wind" ;
            xwind:units = "m s-1" ;
            xwind:coordinates = "latu lonu" ;
            xwind:grid_mapping = "lambert_conformal" ;
        float ywind(time, z, yv, x) ;
            ywind:standard_name = "y_wind" ;
            ywind:units = "m s-1" ;
            ywind:coordinates = "latv lonv" ;
            ywind:grid_mapping = "lambert_conformal" ;
        double x(x) ;
            x:standard_name = "projection_x_coordinate" ;
            x:units = "m" ;
        double y(y) ;
            y:standard_name = "projection_y_coordinate" ;
            y:units = "m" ;
        double xu(xu) ;
            xu:standard_name = "projection_x_coordinate" ;
            xu:units = "m" ;
        double yv(yv) ;
            yv:standard_name = "projection_y_coordinate" ;
            yv:units = "m" ;
        double lon(y, x) ;
                lon:standard_name = "longitude" ;
                lon:units = "degrees_east" ;
        double lat(y, x) ;
                lat:standard_name = "latitude" ;
                lat:units = "degrees_north" ;
        double lonu(y, xu) ;
                lonu:standard_name = "longitude" ;
                lonu:units = "degrees_east" ;
        double latu(y, xu) ;
                latu:standard_name = "latitude" ;
                latu:units = "degrees_north" ;
        double lonv(yv, x) ;
                lonv:standard_name = "longitude" ;
                lonv:units = "degrees_east" ;
        double latv(yv, x) ;
                latv:standard_name = "latitude" ;
                latv:units = "degrees_north" ;
        int lambert_conformal ;
                lambert_conformal:grid_mapping_name = "lambert_conformal_conic" ;
                lambert_conformal:false_easting = 0.f ;
                lambert_conformal:false_easting_unit = "km" ;
                lambert_conformal:false_northing = 0.f ;
                lambert_conformal:false_northing_unit = "km" ;
                lambert_conformal:latitude_of_projection_origin = 53.55f ;
                lambert_conformal:longitude_of_central_meridian = 10.f ;
                lambert_conformal:standard_parallel = 53.f, 54.f ;

// global attributes:
                :Conventions = "CF-1.7" ;
}
```


Discussion:
I'm looking at the code and the standard, and I have to agree that having more than one `projection_x_coordinate` and `projection_y_coordinate` as in the case of this file should be fine.
Then we switch from testing `== 1` to `>= 1`? I will create a pull request when I have a confirmation.

Currently, I don't see any possiblity to check directly to which grid mapping a set of projected x/y coordinates belongs to.

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
@@ -2462,8 +2462,8 @@ def check_grid_mapping(self, ds):
             expected_std_names = grid_mapping[2]
             for expected_std_name in expected_std_names:
                 found_vars = ds.get_variables_by_attributes(standard_name=expected_std_name)
-                valid_grid_mapping.assert_true(len(found_vars) == 1,
-                                               "grid mapping {} requires exactly ".format(grid_mapping_name)+\
+                valid_grid_mapping.assert_true(len(found_vars) >= 1,
+                                               "grid mapping {} requires at least ".format(grid_mapping_name)+\
                                                "one variable with standard_name "+\
                                                "{} to be defined".format(expected_std_name))
 


Test patch: