Skip to content

Commit 616b405

Browse files
authored
Add CMIP7 QC — per-variable rules for all 293 ESM1-6 variables + batch report integration (#456)
* Add CMIP7 output QC docs and CLI usage - add CMIP7 QC validator module and packaged tas range rules - run QC automatically on CMORised CMIP7 outputs after write/repack - add moppy-qc CLI entrypoint and CLI tests - document notebook and CLI workflows in Sphinx docs * Add per-variable CMIP7 QC rules and batch report integration - Generate explicit QC rules for all 293 ACCESS-ESM1-6 mapped variables Each variable now has units, default min/max, and experiment-specific overrides (historical/piControl/ssp*) derived from mapping definitions. No more falling back to unit envelopes at runtime — everything is explicit. - Remove redundant unit_envelopes section from cmip7_ranges.yml They were only ever used to seed the per-variable generation, so keeping them in the YAML was just noise. - Add QC section to batch report (moppy_batch_report.json) When the batch run finishes, the report now includes a qc block with pass/fail counts and per-file failure details (observed vs allowed range). Can be disabled with MOPPY_SKIP_QC=1 or --skip-qc / skip_qc=True. - Add validate_cmip7_output_detailed() for non-raising validation Returns a ValidationResult dataclass so batch collection can gather failures without stopping on the first bad file. - Update docs to reflect explicit per-variable setup and batch QC * Tune QC limits for tasmin and tasmax tasmax (monthly max temperature): ceiling set 5-10 K above tas since we expect warmer peaks — 340 K historical, 335 K piControl, 345 K ssp* tasmin (monthly min temperature): floor dropped 5 K below tas to allow colder night-time minimums — 175 K floor, 325/320/330 K ceiling per experiment * Fix variable selection when file contains *_bnds variables CMORised files can have lat_bnds/lon_bnds/time_bnds alongside the main data variable. The previous fallback only triggered when there was exactly one data_var, so files like tasmax with 4 vars (tasmax + 3 bounds) raised an error instead of selecting tasmax. Fix: filter out *_bnds variables before falling back to single-variable selection. Still raises if multiple non-bounds variables remain. * Add a blank line before the build_batch_report function for improved readability * Update rangess * pre-commit * Fix test_write_repacks_cmip7_output failing after QC integration That test is about verifying the repack subprocess is called — it was written before QC was wired into the write path. The test fixture dataset doesn't have units on the data variable, so QC was raising. Patch out validate_cmip7_output since QC is already tested separately. * Restore CMIP7 tas piControl QC ceiling The widened tas range made the piControl regression tests pass when they should fail at 326.5 K. Put the narrower tas limits back in place so the existing QC behavior stays intact. * Add comprehensive test coverage for CMIP7 QC validation Added 15 new unit tests: QC Validation Tests (5 new): - test_validate_cmip7_output_requires_variable_id: Validates error handling for missing variable_id - test_validate_cmip7_output_requires_experiment_id: Validates error handling for missing experiment_id - test_validate_cmip7_output_detects_all_missing_values: Validates detection of all-NaN data - test_validate_cmip7_output_detects_infinity_values: Validates detection of infinity values - test_validate_cmip7_output_experiment_pattern_matching: Validates wildcard pattern matching for experiments Batch Report QC Integration Tests (10 new): - test_run_qc_on_output_folder_with_all_passing_files: QC passes with valid files - test_run_qc_on_output_folder_with_failing_files: QC detects invalid files - test_run_qc_on_output_folder_with_mixed_pass_fail: QC tallies mixed results - test_run_qc_on_output_folder_respects_moppy_skip_qc_env_var: MOPPY_SKIP_QC disables QC - test_run_qc_on_output_folder_with_no_nc_files: QC handles empty folders - test_run_qc_on_output_folder_with_nested_files: QC finds files in subdirectories - test_build_batch_report_includes_qc_section_by_default: QC section included by default - test_build_batch_report_omits_qc_section_when_skip_qc_true: skip_qc parameter works - test_write_batch_report_passes_skip_qc_to_build: write_batch_report passes skip_qc - test_run_qc_on_output_folder_includes_detailed_failure_info: Failure details properly included All 39 tests pass. Coverage improved from 52.2% to estimated 80%+ for changed code. * Refactor test cases in batch report and CMIP7 QC to remove unnecessary blank linespre-commit * Enhance time bounds calculation to support single monthly midpoint labels * Add tests for CLI skip QC functionality and enhance validation for CMIP7 output * Refactor QC validation to remove positive sign enforcement and update related tests * Add functions to handle missing-value sentinels and update QC validation * Add unit tests for standardizing missing values in vocabulary processor * Add range validation function and corresponding unit test for tiny negative noise * Update validation function docstrings for clarity on ACCESS mapped variables * Add unit tests for range resolution and experiment selection in CMIP7 QC * Enhance CMORiser and vocabulary processor: add support for ACCESS-ESM1-6 and improve nominal resolution handling * Switch on validation for Ocean variables * Add unit tests for unit equivalence checks and implement _units_match function * Increase salinity range in QC * Enhance DatasetChunker: unify inconsistent chunks before rechunking and add unit test for this behavior * Clean range file * update tests
1 parent 488131e commit 616b405

23 files changed

Lines changed: 4106 additions & 30 deletions

docs/source/getting_started.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,29 @@ After writing the file, we recommend validating it using [PrePARE](https://githu
243243
244244
cmoriser.write()
245245
246+
Running output QC checks
247+
------------------------
248+
249+
ACCESS-MOPPy includes CMIP7 output QC checks (currently including physical
250+
range checks for ``tas``). You can run QC from notebooks or the command line.
251+
252+
Notebook/API usage:
253+
254+
.. code-block:: python
255+
256+
from access_moppy.qc import validate_cmip7_output
257+
258+
output_file = "/path/to/CMIP7/output.nc"
259+
validate_cmip7_output(output_file)
260+
261+
CLI usage:
262+
263+
.. code-block:: bash
264+
265+
moppy-qc /path/to/output.nc
266+
267+
See :doc:`qc_validation` for complete examples and rule configuration details.
268+
246269
CMIP7 Support with Full Branded Names
247270
======================================
248271

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ While retaining the core concepts of "custom" and "cmip" modes, ACCESS-MOPPy uni
5555
esmvaltool_integration
5656
CMORise_ILAMB_workflow
5757
mapping_reference
58+
qc_validation
5859
compliance_testing
5960
testing_cmorisation
6061
----

docs/source/mapping_reference.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ required fields:
129129
- Expected physical units of the CMIP output variable (e.g. ``"W m-2"``, ``"kg m-2 s-1"``).
130130
* - ``positive``
131131
- Yes
132-
- Sign convention: ``"up"``, ``"down"``, or ``null`` if not applicable.
132+
- Sign convention metadata: ``"up"``, ``"down"``, or ``null`` if not applicable.
133133
* - ``model_variables``
134134
- Yes
135135
- List of raw model variable names that must be loaded from the input files.

docs/source/qc_validation.rst

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
CMIP7 QC Validation
2+
===================
3+
4+
This page describes how to run ACCESS-MOPPy output quality-control checks on
5+
CMORised files.
6+
7+
Scope
8+
-----
9+
10+
- QC is run on the *CMORised output file*, not the raw model input.
11+
- For ``source_id=ACCESS-ESM1-6``, QC covers all variables present in
12+
``ACCESS-ESM1-6_mappings.json`` with generic checks (non-missing values,
13+
finite values, and units checks where defined).
14+
- Physical ranges for all 293 ACCESS-ESM1-6 mapped variables are defined
15+
explicitly in the QC configuration with defaults and experiment-specific
16+
overrides (historical, piControl, ssp*).
17+
- Each variable's physical range is derived from its definition (mapping units)
18+
and stored as a per-variable rule entry.
19+
- Rules are loaded from: ``access_moppy/resources/qc/cmip7_ranges.yml``.
20+
21+
Running QC in Notebooks
22+
-----------------------
23+
24+
Use the Python API to validate a CMORised file after ``cmoriser.write()``.
25+
26+
.. code-block:: python
27+
28+
from access_moppy.qc import validate_cmip7_output
29+
30+
# Write CMORised output first
31+
cmoriser.run()
32+
cmoriser.write()
33+
34+
# Validate the written file
35+
output_file = "/path/to/CMIP7/output.nc"
36+
validate_cmip7_output(output_file)
37+
38+
If a check fails, ``validate_cmip7_output`` raises ``ValueError`` with details
39+
about the variable, experiment, observed range, and allowed range.
40+
41+
Running QC from the CLI
42+
-----------------------
43+
44+
ACCESS-MOPPy provides a CLI command:
45+
46+
.. code-block:: bash
47+
48+
moppy-qc /path/to/file1.nc /path/to/file2.nc
49+
50+
Exit status:
51+
52+
- ``0``: all files passed
53+
- ``1``: one or more files failed
54+
55+
Example output:
56+
57+
.. code-block:: text
58+
59+
PASS /path/to/file1.nc
60+
FAIL /path/to/file2.nc: CMIP7 QC failed for tas in experiment piControl using rule piControl: observed range 182.000..329.400 K is outside allowed range 180.000..325.000 K.
61+
62+
Automatic QC during CMORisation
63+
-------------------------------
64+
65+
For CMIP7 runs, ACCESS-MOPPy automatically validates output in the write path
66+
after writing and repacking the file. In other words, when you call
67+
``cmoriser.write()`` for CMIP7 output, QC is already executed.
68+
69+
Batch Report QC Summary
70+
-----------------------
71+
72+
When running a batch CMORisation, the batch report (``moppy_batch_report.json``)
73+
automatically includes a QC section summarizing validation results for all
74+
CMORised output files:
75+
76+
.. code-block:: json
77+
78+
{
79+
"qc": {
80+
"passed": 42,
81+
"failed": 2,
82+
"total": 44,
83+
"failures": [
84+
{
85+
"file": "/output/path/tas.nc",
86+
"variable_id": "tas",
87+
"experiment_id": "piControl",
88+
"error": "Observed range 182.000..329.400 K is outside allowed 180.000..325.000 K.",
89+
"observed_range": [182.0, 329.4],
90+
"allowed_range": [180.0, 325.0],
91+
"units": "K"
92+
}
93+
]
94+
}
95+
}
96+
97+
To disable QC collection during batch report generation, use one of:
98+
99+
.. code-block:: bash
100+
101+
# Environment variable
102+
export MOPPY_SKIP_QC=1
103+
moppy-batch-report --db cmor_tasks.db
104+
105+
# CLI flag
106+
moppy-batch-report --db cmor_tasks.db --skip-qc
107+
108+
Or programmatically:
109+
110+
.. code-block:: python
111+
112+
from access_moppy.batch_report import write_batch_report
113+
write_batch_report(db_path, skip_qc=True)
114+
115+
Extending rules
116+
117+
---------------
118+
119+
To add experiment-specific thresholds for a variable, or to override ranges
120+
for newly added variables, edit:
121+
122+
.. code-block:: text
123+
124+
src/access_moppy/resources/qc/cmip7_ranges.yml
125+
126+
Under the ``variables`` section, each variable has a ``default`` entry and an
127+
optional ``experiments`` map for experiment-specific min/max values. For example:
128+
129+
.. code-block:: yaml
130+
131+
variables:
132+
tas:
133+
units: K
134+
default:
135+
min: 180.0
136+
max: 330.0
137+
experiments:
138+
historical:
139+
min: 180.0
140+
max: 330.0
141+
piControl:
142+
min: 180.0
143+
max: 325.0
144+
145+
Rule structure example:
146+
147+
.. code-block:: yaml
148+
149+
variables:
150+
tas:
151+
units: K
152+
default:
153+
min: 180.0
154+
max: 330.0
155+
experiments:
156+
historical:
157+
min: 180.0
158+
max: 330.0
159+
piControl:
160+
min: 180.0
161+
max: 325.0
162+
ssp*:
163+
min: 180.0
164+
max: 335.0

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ moppy-cmorise = "access_moppy.batch_cmoriser:main"
9191
moppy-dashboard = "access_moppy.dashboard.cmor_dashboard:main"
9292
moppy-tui = "access_moppy.dashboard.cli_dashboard:main"
9393
moppy-batch-report = "access_moppy.batch_report:main"
94+
moppy-qc = "access_moppy.qc.cmip7:main"
9495
moppy-example-config = "access_moppy.examples.show_config:main"
9596
moppy-calc-ab-coeffts = "access_moppy.legacy_utilities.calc_hybrid_height_coeffs:main"
9697
moppy-esmval-prepare = "access_moppy.esmval.cli_commands:main_prepare"

src/access_moppy/base.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import xarray as xr
1515
from cftime import date2num
1616

17+
from access_moppy.qc import validate_cmip7_output
1718
from access_moppy.utilities import (
1819
FrequencyMismatchError,
1920
IncompatibleFrequencyError,
@@ -111,9 +112,23 @@ def rechunk_dataset(self, ds: xr.Dataset) -> xr.Dataset:
111112
Returns:
112113
Rechunked xarray Dataset
113114
"""
114-
if not hasattr(ds, "chunks") or not any(
115-
ds.chunks.values() if ds.chunks else []
116-
):
115+
try:
116+
ds_chunks = ds.chunks
117+
except AttributeError:
118+
logger.debug("Dataset is not chunked, skipping rechunking")
119+
return ds
120+
except ValueError as err:
121+
if "inconsistent chunks along dimension" not in str(err):
122+
raise
123+
124+
logger.debug(
125+
"Dataset has inconsistent chunking across variables; "
126+
"calling unify_chunks() before rechunking"
127+
)
128+
ds = ds.unify_chunks()
129+
ds_chunks = ds.chunks
130+
131+
if not any(ds_chunks.values() if ds_chunks else []):
117132
logger.debug("Dataset is not chunked, skipping rechunking")
118133
return ds
119134

@@ -1253,6 +1268,9 @@ def estimate_data_size(ds):
12531268

12541269
self._repack_cmip7_output(path)
12551270

1271+
if getattr(self.vocab, "mip_era", None) == "CMIP7":
1272+
validate_cmip7_output(path)
1273+
12561274
logger.info("CMORised output written to %s", path)
12571275
logger.debug("Optimized layout: metadata -> data chunks")
12581276
if self.enable_compression:

0 commit comments

Comments
 (0)