Repository: ioos/compliance-checker
Pull request #1174 (2025-05-08 21:49:45)

Adds CF check for units_metadata attribute against temperature related variables


Patch:
diff --git a/compliance_checker/cf/cf.py b/compliance_checker/cf/cf.py
index cb87ee3..8128bcc 100644
--- a/compliance_checker/cf/cf.py
+++ b/compliance_checker/cf/cf.py
@@ -32,3 +32,4 @@ from compliance_checker.cf.cf_1_7 import CF1_7Check  # noqa: F401
 from compliance_checker.cf.cf_1_8 import CF1_8Check  # noqa: F401
 from compliance_checker.cf.cf_1_9 import CF1_9Check  # noqa: F401
 from compliance_checker.cf.cf_1_10 import CF1_10Check  # noqa: F401
+from compliance_checker.cf.cf_1_11 import CF1_11Check  # noqa: F401
diff --git a/compliance_checker/cf/cf_1_11.py b/compliance_checker/cf/cf_1_11.py
new file mode 100644
index 0000000..360725c
--- /dev/null
+++ b/compliance_checker/cf/cf_1_11.py
@@ -0,0 +1,61 @@
+from functools import lru_cache
+
+from compliance_checker.base import BaseCheck, TestCtx
+from compliance_checker.cf.cf_1_10 import CF1_10Check
+
+
+@lru_cache
+def _temperature_standard_names(standard_name_table):
+    re_ns = {"re": "http://exslt.org/regular-expressions"}
+    return set(
+        standard_name_table._root.xpath(
+            "entry[re:test(canonical_units, " r"'(?:K|degree_C)(?:-?\d+)?')]/@id",
+            namespaces=re_ns,
+        ),
+    )
+
+
+class CF1_11Check(CF1_10Check):
+    _cc_spec_version = "1.11"
+    _cc_url = "http://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html"
+
+    def __init__(self, options=None):
+        super().__init__(options)
+        self.section_titles.update(
+            {
+                "3.1.2": "§3.1.2 Temperature units",
+            },
+        )
+
+    # IMPLEMENTATION CONFORMANCE 3.1 RECOMMENDED
+    def check_temperature_units_metadata(self, ds):
+        """Checks that units_metadata exists for variables with standard name of temperature"""
+        temperature_variables = ds.get_variables_by_attributes(
+            standard_name=lambda s: s in _temperature_standard_names(self._std_names),
+        )
+        if not temperature_variables:
+            return []
+        temperature_units_metadata_ctx = TestCtx(
+            BaseCheck.MEDIUM,
+            self.section_titles["3.1.2"],
+        )
+        for temperature_variable in temperature_variables:
+            temperature_units_metadata_ctx.out_of += 1
+            valid_temperature_units_metadata = [
+                "temperature: difference",
+                "temperature: on_scale",
+                "temperature: unknown",
+            ]
+            if (
+                getattr(temperature_variable, "units_metadata", None)
+                not in valid_temperature_units_metadata
+            ):
+                temperature_units_metadata_ctx.messages.append(
+                    f"Variable {temperature_variable.name} has a temperature related standard_name "
+                    "and it is recommended that the units_metadata attribute is present and has one of the values "
+                    f"{valid_temperature_units_metadata}",
+                )
+            else:
+                temperature_units_metadata_ctx.score += 1
+
+        return [temperature_units_metadata_ctx.to_result()]
diff --git a/pyproject.toml b/pyproject.toml
index 80cac24..3f892da 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -45,6 +45,7 @@ scripts.compliance-checker = "cchecker:main"
 entry-points."compliance_checker.suites"."acdd-1.1" = "compliance_checker.acdd:ACDD1_1Check"
 entry-points."compliance_checker.suites"."acdd-1.3" = "compliance_checker.acdd:ACDD1_3Check"
 entry-points."compliance_checker.suites"."cf-1.10" = "compliance_checker.cf.cf:CF1_10Check"
+entry-points."compliance_checker.suites"."cf-1.11" = "compliance_checker.cf.cf:CF1_11Check"
 entry-points."compliance_checker.suites"."cf-1.6" = "compliance_checker.cf.cf:CF1_6Check"
 entry-points."compliance_checker.suites"."cf-1.7" = "compliance_checker.cf.cf:CF1_7Check"
 entry-points."compliance_checker.suites"."cf-1.8" = "compliance_checker.cf.cf:CF1_8Check"


Test patch:
diff --git a/compliance_checker/tests/test_cf.py b/compliance_checker/tests/test_cf.py
index 0aadfd5..06b3462 100644
--- a/compliance_checker/tests/test_cf.py
+++ b/compliance_checker/tests/test_cf.py
@@ -19,6 +19,7 @@ from compliance_checker.cf.cf import (
     CF1_7Check,
     CF1_8Check,
     CF1_9Check,
+    CF1_11Check,
     dimless_vertical_coordinates_1_6,
     dimless_vertical_coordinates_1_7,
 )
@@ -3355,6 +3356,50 @@ class TestCF1_9(BaseTestCase):
         assert results[0].value[0] == results[0].value[1]
 
 
+class TestCF1_11(BaseTestCase):
+    def setup_method(self):
+        self.cf = CF1_11Check()
+
+    @pytest.fixture
+    def temperature_dataset(self):
+        ds = MockTimeSeries()
+        temperature = ds.createVariable("temperature", "f8", ("time",))
+        temperature.standard_name = "sea_water_temperature"
+        temperature.units = "degC"
+        return ds
+
+    # TEST CONFORMANCE 3.1 RECOMMENDED
+    def test_units_metadata(self, temperature_dataset):
+        results = self.cf.check_temperature_units_metadata(temperature_dataset)
+        expected_msg = (
+            "Variable temperature has a temperature related standard_name "
+            "and it is recommended that the units_metadata attribute is present and has one of the values "
+            "['temperature: difference', 'temperature: on_scale', 'temperature: unknown']"
+        )
+        _, _, messages = get_results(results)
+        assert expected_msg in messages
+        # add units_metadata, should be valid now
+        temperature_dataset.variables["temperature"].units_metadata = (
+            "temperature: on_scale"
+        )
+        results = self.cf.check_temperature_units_metadata(temperature_dataset)
+        scored, out_of, _ = get_results(results)
+        assert scored == out_of
+        temperature_dataset.variables["temperature"].standard_name = (
+            "integral_wrt_depth_of_product_of_potential_temperature_and_sea_water_density"
+        )
+        temperature_dataset.variables["temperature"].units = "lb degree_F ft-2"
+        scored, out_of, _ = get_results(results)
+        assert scored == out_of
+
+        temperature_dataset.variables["temperature"].standard_name = (
+            "square_of_air_temperature"
+        )
+        temperature_dataset.variables["temperature"].units = "degrees_Celsius2"
+        scored, out_of, _ = get_results(results)
+        assert scored == out_of
+
+
 class TestCFUtil(BaseTestCase):
     """
     Class to test the cfutil module.