Repository: ioos/compliance-checker
Pull request #1161 (2025-04-24 16:37:11)

Handle geometry cases for grid_mapping and coordinates


Patch:
diff --git a/compliance_checker/cf/cf_1_8.py b/compliance_checker/cf/cf_1_8.py
index 4938343..f3a2b88 100644
--- a/compliance_checker/cf/cf_1_8.py
+++ b/compliance_checker/cf/cf_1_8.py
@@ -13,6 +13,7 @@ What's new in CF-1.8
 import itertools
 import re
 import warnings
+from collections import defaultdict
 
 import numpy as np
 import requests
@@ -119,19 +120,75 @@ class CF1_8Check(CF1_7Check):
             geometry=lambda g: g is not None,
         )
         results = []
-        unique_geometry_var_names = {var.geometry for var in vars_with_geometry}
+        unique_geometry_var_names = defaultdict(list)
+        for var in vars_with_geometry:
+            unique_geometry_var_names[var.geometry].append(var)
+
         if unique_geometry_var_names:
             geom_valid = TestCtx(BaseCheck.MEDIUM, self.section_titles["7.5"])
             geom_valid.out_of += 1
         for geometry_var_name in unique_geometry_var_names:
             if geometry_var_name not in ds.variables:
                 geom_valid.messages.append(
-                    "Cannot find geometry variable " f"named {geometry_var_name}",
+                    f"Cannot find geometry variable named {geometry_var_name}",
                 )
                 results.append(geom_valid.to_result())
                 continue
-            else:
-                geometry_var = ds.variables[geometry_var_name]
+            geometry_var = ds.variables[geometry_var_name]
+            # IMPLEMENTATION CONFORMANCE 7.5 REQUIRED 10/20
+            # The grid_mapping and coordinates attributes can be carried by the
+            # geometry container variable provided they are also carried by the
+            # data variables associated with the container.
+            if hasattr(geometry_var, "coordinates"):
+                coord_err_template = (
+                    "Geometry variable {geometry_var_name} has "
+                    "attribute coordinates which is "
+                    "either not present or is not a subset "
+                    "of the coordinates attribute of the "
+                    "referring parent variable {parent_var_name}"
+                )
+                # allow for geometry coordinates attribute to be a subset of
+                # the parent variable's coordinates
+                geom_var_coords = set(re.split(r"\s+", geometry_var.coordinates))
+                for parent_var in vars_with_geometry:
+                    geom_valid.out_of += 1
+                    if not hasattr(parent_var, "coordinates"):
+                        parent_var_coords = set()
+                    else:
+                        parent_var_coords = set(
+                            re.split(r"\s+", parent_var.coordinates),
+                        )
+                    if not geom_var_coords.issubset(parent_var_coords):
+                        err_msg = coord_err_template.format(
+                            geometry_var_name=geometry_var_name,
+                            parent_var_name=parent_var.name,
+                        )
+                        geom_valid.messages.append(err_msg)
+                    else:
+                        geom_valid.score += 1
+            # TODO: should grid_mapping checks not be strict equality?
+            if hasattr(geometry_var, "grid_mapping"):
+                for parent_var in vars_with_geometry:
+                    geom_valid.out_of += 1
+                    grid_mapping_err_template = (
+                        "Geometry variable {geometry_var_name} has "
+                        "attribute grid_mapping which is "
+                        "either not present or does not have the "
+                        "same value as the referring parent variable "
+                        "{parent_var_name}"
+                    )
+                    if geometry_var.grid_mapping != getattr(
+                        parent_var,
+                        "grid_mapping",
+                        None,
+                    ):
+                        err_msg = grid_mapping_err_template.format(
+                            geometry_var_name=geometry_var_name,
+                            parent_var_name=parent_var.name,
+                        )
+                        geom_valid.messages.append(err_msg)
+                    else:
+                        geom_valid.score += 1
 
             geometry_type = geometry_var.geometry_type
             try:


Test patch:
diff --git a/compliance_checker/tests/test_cf.py b/compliance_checker/tests/test_cf.py
index f3ebe5b..82dc324 100644
--- a/compliance_checker/tests/test_cf.py
+++ b/compliance_checker/tests/test_cf.py
@@ -2811,7 +2811,15 @@ class TestCF1_8(BaseTestCase):
 
     def test_point_geometry_simple(self):
         dataset = MockTimeSeries()
-        fake_data = dataset.createVariable("someData", "f8", ("time",))
+        dataset.createDimension("instance", 1)
+        fake_data = dataset.createVariable(
+            "someData",
+            "f8",
+            (
+                "instance",
+                "time",
+            ),
+        )
         fake_data.geometry = "geometry"
         x = dataset.createVariable("x", "f8", ())
         y = dataset.createVariable("y", "f8", ())
@@ -2822,10 +2830,87 @@ class TestCF1_8(BaseTestCase):
         y[:] = 1
         self.cf.check_geometry(dataset)
 
+    @pytest.fixture
+    def geometry_ds(self):
+        dataset = MockTimeSeries()
+        dataset.createDimension("point_count", 3)
+        dataset.createDimension("instance", 1)
+        fake_data = dataset.createVariable("someData", "f8", ("instance", "time"))
+        fake_data.geometry = "geometry"
+        x = dataset.createVariable("x", "f8", ("point_count",))
+        y = dataset.createVariable("y", "f8", ("point_count",))
+        geom_var = dataset.createVariable("geometry", "i4", ())
+        geom_var.geometry_type = "point"
+        geom_var.node_coordinates = "x y"
+        x[:] = np.array([10, 20, 30])
+        y[:] = np.array([30, 35, 21])
+        return dataset
+
+    # TEST CONFORMANCE 7.5 REQUIRED 10/20
+    @pytest.mark.parametrize(
+        "geom_grid_mapping,geom_coordinates",
+        [(None, None), ("datum", None), (None, "x y"), ("datum", "x y")],
+    )
+    def test_coordinate_grid_mapping_geometry_ds_pass(
+        self,
+        geometry_ds,
+        geom_grid_mapping,
+        geom_coordinates,
+    ):
+        geometry_ds.variables["someData"].grid_mapping = "datum"
+        geom_gm_none = geom_grid_mapping is None
+        if not geom_gm_none:
+            geometry_ds.variables["geometry"].grid_mapping = geom_grid_mapping
+
+        geometry_ds.variables["someData"].coordinates = "x y"
+        geom_coords_none = geom_coordinates is None
+        if not geom_coords_none:
+            geometry_ds.variables["geometry"].coordinates = geom_coordinates
+        msg_template = (
+            "Geometry variable geometry has "
+            "attribute {} the referring parent "
+            "variable someData"
+        )
+        coord_subst = (
+            "coordinates which is either not present or is not a "
+            "subset of the coordinates attribute of"
+        )
+        gm_subst = (
+            "grid_mapping which is either not present or does not have "
+            "the same value as"
+        )
+        results = self.cf.check_geometry(geometry_ds)
+        assert msg_template.format(gm_subst) not in results[0].msgs
+        assert msg_template.format(coord_subst) not in results[0].msgs
+        # test subset of coordinates
+        geometry_ds.variables["someData"].coordinates = "x y z"
+        results = self.cf.check_geometry(geometry_ds)
+        assert msg_template.format(coord_subst) not in results[0].msgs
+
+        # try with possibly invalid data
+        # mismatch between parent variable coordinates/grid_mapping
+        # and geometry variable
+        if not geom_gm_none:
+            geometry_ds.variables["someData"].grid_mapping = "other_datum"
+            results = self.cf.check_geometry(geometry_ds)
+            assert msg_template.format(gm_subst) in results[0].msgs
+        if not geom_coords_none:
+            geometry_ds.variables["someData"].coordinates = "u v"
+            results = self.cf.check_geometry(geometry_ds)
+            assert msg_template.format(coord_subst) in results[0].msgs
+            geometry_ds.variables["geometry"].coordinates = "x y z"
+            results = self.cf.check_geometry(geometry_ds)
+            assert msg_template.format(coord_subst) in results[0].msgs
+
     def test_point_geometry_multiple(self):
         dataset = MockTimeSeries()
         dataset.createDimension("point_count", 3)
-        fake_data = dataset.createVariable("someData", "f8", ("time",))
+        dataset.createDimension("instance", 1)
+        fake_data = dataset.createVariable(
+            "someData",
+            "f8",
+            ("instance", "time"),
+        )
         fake_data.geometry = "geometry"
         x = dataset.createVariable("x", "f8", ("point_count",))
         y = dataset.createVariable("y", "f8", ("point_count",))