From 9e7c9a391f25728a5d913a8c421af3305e630db4 Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Tue, 23 Jun 2026 17:29:26 +1000 Subject: [PATCH 01/21] 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 --- docs/source/getting_started.rst | 23 +++ docs/source/index.rst | 1 + docs/source/qc_validation.rst | 92 +++++++++ pyproject.toml | 1 + src/access_moppy/base.py | 4 + src/access_moppy/qc/__init__.py | 5 + src/access_moppy/qc/cmip7.py | 184 ++++++++++++++++++ src/access_moppy/resources/__init__.py | 1 + src/access_moppy/resources/qc/__init__.py | 1 + .../resources/qc/cmip7_ranges.yml | 16 ++ tests/unit/test_cmip7_qc.py | 142 ++++++++++++++ 11 files changed, 470 insertions(+) create mode 100644 docs/source/qc_validation.rst create mode 100644 src/access_moppy/qc/__init__.py create mode 100644 src/access_moppy/qc/cmip7.py create mode 100644 src/access_moppy/resources/__init__.py create mode 100644 src/access_moppy/resources/qc/__init__.py create mode 100644 src/access_moppy/resources/qc/cmip7_ranges.yml create mode 100644 tests/unit/test_cmip7_qc.py diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index e802189d..5bf98ca1 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -243,6 +243,29 @@ After writing the file, we recommend validating it using [PrePARE](https://githu cmoriser.write() +Running output QC checks +------------------------ + +ACCESS-MOPPy includes CMIP7 output QC checks (currently including physical +range checks for ``tas``). You can run QC from notebooks or the command line. + +Notebook/API usage: + +.. code-block:: python + + from access_moppy.qc import validate_cmip7_output + + output_file = "/path/to/CMIP7/output.nc" + validate_cmip7_output(output_file) + +CLI usage: + +.. code-block:: bash + + moppy-qc /path/to/output.nc + +See :doc:`qc_validation` for complete examples and rule configuration details. + CMIP7 Support with Full Branded Names ====================================== diff --git a/docs/source/index.rst b/docs/source/index.rst index 84d04451..f464430c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -55,6 +55,7 @@ While retaining the core concepts of "custom" and "cmip" modes, ACCESS-MOPPy uni esmvaltool_integration CMORise_ILAMB_workflow mapping_reference + qc_validation compliance_testing testing_cmorisation ---- diff --git a/docs/source/qc_validation.rst b/docs/source/qc_validation.rst new file mode 100644 index 00000000..58862414 --- /dev/null +++ b/docs/source/qc_validation.rst @@ -0,0 +1,92 @@ +CMIP7 QC Validation +=================== + +This page describes how to run ACCESS-MOPPy output quality-control checks on +CMORised files. + +Scope +----- + +- QC is run on the *CMORised output file*, not the raw model input. +- The current implementation includes CMIP7 physical-range checks for + ``tas``. +- Experiment-aware rules are loaded from: + ``access_moppy/resources/qc/cmip7_ranges.yml``. + +Running QC in Notebooks +----------------------- + +Use the Python API to validate a CMORised file after ``cmoriser.write()``. + +.. code-block:: python + + from access_moppy.qc import validate_cmip7_output + + # Write CMORised output first + cmoriser.run() + cmoriser.write() + + # Validate the written file + output_file = "/path/to/CMIP7/output.nc" + validate_cmip7_output(output_file) + +If a check fails, ``validate_cmip7_output`` raises ``ValueError`` with details +about the variable, experiment, observed range, and allowed range. + +Running QC from the CLI +----------------------- + +ACCESS-MOPPy provides a CLI command: + +.. code-block:: bash + + moppy-qc /path/to/file1.nc /path/to/file2.nc + +Exit status: + +- ``0``: all files passed +- ``1``: one or more files failed + +Example output: + +.. code-block:: text + + PASS /path/to/file1.nc + 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. + +Automatic QC during CMORisation +------------------------------- + +For CMIP7 runs, ACCESS-MOPPy automatically validates output in the write path +after writing and repacking the file. In other words, when you call +``cmoriser.write()`` for CMIP7 output, QC is already executed. + +Extending rules +--------------- + +To add more variables or experiment-specific thresholds, update: + +.. code-block:: text + + src/access_moppy/resources/qc/cmip7_ranges.yml + +Rule structure example: + +.. code-block:: yaml + + variables: + tas: + units: K + default: + min: 180.0 + max: 330.0 + experiments: + historical: + min: 180.0 + max: 330.0 + piControl: + min: 180.0 + max: 325.0 + ssp*: + min: 180.0 + max: 335.0 diff --git a/pyproject.toml b/pyproject.toml index ff3fd08e..331bd6c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,6 +91,7 @@ moppy-cmorise = "access_moppy.batch_cmoriser:main" moppy-dashboard = "access_moppy.dashboard.cmor_dashboard:main" moppy-tui = "access_moppy.dashboard.cli_dashboard:main" moppy-batch-report = "access_moppy.batch_report:main" +moppy-qc = "access_moppy.qc.cmip7:main" moppy-example-config = "access_moppy.examples.show_config:main" moppy-calc-ab-coeffts = "access_moppy.legacy_utilities.calc_hybrid_height_coeffs:main" moppy-esmval-prepare = "access_moppy.esmval.cli_commands:main_prepare" diff --git a/src/access_moppy/base.py b/src/access_moppy/base.py index 7e48f806..7924e073 100644 --- a/src/access_moppy/base.py +++ b/src/access_moppy/base.py @@ -14,6 +14,7 @@ import xarray as xr from cftime import date2num +from access_moppy.qc import validate_cmip7_output from access_moppy.utilities import ( FrequencyMismatchError, IncompatibleFrequencyError, @@ -1226,6 +1227,9 @@ def estimate_data_size(ds): self._repack_cmip7_output(path) + if getattr(self.vocab, "mip_era", None) == "CMIP7": + validate_cmip7_output(path) + logger.info("CMORised output written to %s", path) logger.debug("Optimized layout: metadata -> data chunks") if self.enable_compression: diff --git a/src/access_moppy/qc/__init__.py b/src/access_moppy/qc/__init__.py new file mode 100644 index 00000000..13181755 --- /dev/null +++ b/src/access_moppy/qc/__init__.py @@ -0,0 +1,5 @@ +"""Output QC helpers for ACCESS-MOPPy.""" + +from .cmip7 import validate_cmip7_output + +__all__ = ["validate_cmip7_output"] diff --git a/src/access_moppy/qc/cmip7.py b/src/access_moppy/qc/cmip7.py new file mode 100644 index 00000000..82f7cb6f --- /dev/null +++ b/src/access_moppy/qc/cmip7.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +import argparse +from dataclasses import dataclass +from fnmatch import fnmatchcase +from functools import lru_cache +from importlib.resources import as_file, files +from pathlib import Path +from typing import Any + +import numpy as np +import xarray as xr +import yaml + + +@dataclass(frozen=True) +class RangeRule: + variable_id: str + experiment_id: str + units: str | None + minimum: float + maximum: float + rule_name: str + + +@lru_cache(maxsize=1) +def _load_rules() -> dict[str, Any]: + resource = files("access_moppy.resources.qc") / "cmip7_ranges.yml" + with as_file(resource) as path: + with open(path, "r", encoding="utf-8") as handle: + payload = yaml.safe_load(handle) or {} + return payload.get("variables", {}) + + +def _select_experiment_rule( + experiments: dict[str, dict[str, Any]], experiment_id: str +) -> tuple[str, dict[str, Any]] | None: + if experiment_id in experiments: + return experiment_id, experiments[experiment_id] + + matches: list[tuple[str, dict[str, Any]]] = [ + (pattern, rule) + for pattern, rule in experiments.items() + if fnmatchcase(experiment_id, pattern) + ] + if not matches: + return None + + matches.sort(key=lambda item: (-len(item[0]), item[0])) + return matches[0] + + +def _resolve_range_rule(variable_id: str, experiment_id: str) -> RangeRule | None: + variables = _load_rules() + variable_rules = variables.get(variable_id) + if not variable_rules: + return None + + base_rule = dict(variable_rules.get("default", {})) + experiment_rules = variable_rules.get("experiments", {}) + selected = _select_experiment_rule(experiment_rules, experiment_id) + if selected is not None: + rule_name, experiment_rule = selected + base_rule.update(experiment_rule) + else: + rule_name = "default" + + if "min" not in base_rule or "max" not in base_rule: + return None + + return RangeRule( + variable_id=variable_id, + experiment_id=experiment_id, + units=base_rule.get("units", variable_rules.get("units")), + minimum=float(base_rule["min"]), + maximum=float(base_rule["max"]), + rule_name=rule_name, + ) + + +def _select_output_variable(ds: xr.Dataset, attrs: dict[str, Any]) -> str: + candidate_names = [ + attrs.get("branded_variable"), + attrs.get("variable_id"), + ] + for candidate in candidate_names: + if isinstance(candidate, str) and candidate in ds.data_vars: + return candidate + + if len(ds.data_vars) == 1: + return next(iter(ds.data_vars)) + + available = ", ".join(sorted(ds.data_vars)) + raise ValueError( + "CMIP7 QC could not determine the main output variable from the CMORised file. " + f"Available data variables: {available}" + ) + + +def validate_cmip7_output(output_path: str | Path) -> None: + """Validate a CMIP7 CMORised file against output-time physical range rules.""" + + path = Path(output_path) + with xr.open_dataset(path) as ds: + attrs = dict(ds.attrs) + variable_id = attrs.get("variable_id") + experiment_id = attrs.get("experiment_id") + + if not isinstance(variable_id, str) or not variable_id: + raise ValueError( + "CMIP7 QC requires a 'variable_id' global attribute on the output file." + ) + if not isinstance(experiment_id, str) or not experiment_id: + raise ValueError( + "CMIP7 QC requires an 'experiment_id' global attribute on the output file." + ) + + rule = _resolve_range_rule(variable_id, experiment_id) + if rule is None: + return + + output_variable = _select_output_variable(ds, attrs) + da = ds[output_variable] + + units = da.attrs.get("units") or attrs.get("units") + if rule.units is not None and units != rule.units: + raise ValueError( + "CMIP7 QC failed for " + f"{variable_id} in experiment {experiment_id}: expected units {rule.units!r}, " + f"found {units!r}." + ) + + minimum = da.min(skipna=True).item() + maximum = da.max(skipna=True).item() + + if np.isnan(minimum) or np.isnan(maximum): + return + + observed_min = float(minimum) + observed_max = float(maximum) + + if observed_min < rule.minimum or observed_max > rule.maximum: + raise ValueError( + "CMIP7 QC failed for " + f"{variable_id} in experiment {experiment_id} using rule {rule.rule_name}: " + f"observed range {observed_min:.3f}..{observed_max:.3f} {units or ''} " + f"is outside allowed range {rule.minimum:.3f}..{rule.maximum:.3f} {rule.units or units or ''}." + ) + + +def _build_cli_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="moppy-qc", + description=( + "Run ACCESS-MOPPy CMIP7 output QC checks against one or more CMORised netCDF files." + ), + ) + parser.add_argument( + "paths", + nargs="+", + help="One or more CMORised output files to validate.", + ) + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = _build_cli_parser() + args = parser.parse_args(argv) + + failures: list[tuple[str, str]] = [] + for raw_path in args.paths: + path = Path(raw_path) + try: + validate_cmip7_output(path) + print(f"PASS {path}") + except Exception as exc: # noqa: BLE001 + failures.append((str(path), str(exc))) + print(f"FAIL {path}: {exc}") + + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/access_moppy/resources/__init__.py b/src/access_moppy/resources/__init__.py new file mode 100644 index 00000000..7663c017 --- /dev/null +++ b/src/access_moppy/resources/__init__.py @@ -0,0 +1 @@ +"""Packaged data resources for ACCESS-MOPPy.""" diff --git a/src/access_moppy/resources/qc/__init__.py b/src/access_moppy/resources/qc/__init__.py new file mode 100644 index 00000000..2ece2e9f --- /dev/null +++ b/src/access_moppy/resources/qc/__init__.py @@ -0,0 +1 @@ +"""Packaged QC rules for ACCESS-MOPPy.""" diff --git a/src/access_moppy/resources/qc/cmip7_ranges.yml b/src/access_moppy/resources/qc/cmip7_ranges.yml new file mode 100644 index 00000000..2ef7dad4 --- /dev/null +++ b/src/access_moppy/resources/qc/cmip7_ranges.yml @@ -0,0 +1,16 @@ +variables: + tas: + units: K + default: + min: 180.0 + max: 330.0 + experiments: + historical: + min: 180.0 + max: 330.0 + piControl: + min: 180.0 + max: 325.0 + ssp*: + min: 180.0 + max: 335.0 diff --git a/tests/unit/test_cmip7_qc.py b/tests/unit/test_cmip7_qc.py new file mode 100644 index 00000000..00a29506 --- /dev/null +++ b/tests/unit/test_cmip7_qc.py @@ -0,0 +1,142 @@ +from pathlib import Path +from unittest.mock import Mock, patch + +import numpy as np +import pytest +import xarray as xr + +from access_moppy.base import CMORiser +from access_moppy.qc import validate_cmip7_output +from access_moppy.qc.cmip7 import main as qc_main + + +def _write_cmip7_output( + tmp_path: Path, + *, + values, + experiment_id: str, + filename: str = "cmip7_output.nc", +) -> Path: + path = tmp_path / filename + ds = xr.Dataset( + { + "tas": xr.DataArray( + np.asarray(values, dtype=float), + dims=["time"], + attrs={"units": "K"}, + ) + }, + coords={"time": xr.DataArray(np.arange(len(values)), dims=["time"])}, + attrs={ + "mip_era": "CMIP7", + "variable_id": "tas", + "branded_variable": "tas", + "experiment_id": experiment_id, + "units": "K", + }, + ) + ds.to_netcdf(path) + return path + + +@pytest.mark.unit +def test_validate_cmip7_output_tas_passes_for_historical_range(tmp_path): + path = _write_cmip7_output( + tmp_path, values=[285.0, 287.5, 289.0], experiment_id="historical" + ) + + validate_cmip7_output(path) + + +@pytest.mark.unit +def test_validate_cmip7_output_tas_fails_for_picontrol_range(tmp_path): + path = _write_cmip7_output( + tmp_path, values=[324.0, 326.5], experiment_id="piControl" + ) + + with pytest.raises(ValueError, match="tas.*piControl.*outside allowed range"): + validate_cmip7_output(path) + + +@pytest.mark.unit +def test_cmoriser_write_runs_cmip7_qc_after_repack(tmp_path): + vocab = Mock() + vocab.mip_era = "CMIP7" + vocab.compound_name = "atmos.tas" + vocab.generate_filename = Mock(return_value="tas.nc") + vocab.get_required_attribute_names = Mock(return_value=[]) + + ds = xr.Dataset( + { + "tas": xr.DataArray( + np.asarray([285.0, 286.0], dtype=float), + dims=["time"], + attrs={"units": "K"}, + coords={"time": xr.DataArray([0, 1], dims=["time"])}, + ) + }, + attrs={ + "mip_era": "CMIP7", + "variable_id": "tas", + "branded_variable": "tas", + "experiment_id": "historical", + "units": "K", + }, + ) + + cmoriser = CMORiser( + input_data=ds, + output_path=str(tmp_path), + vocab=vocab, + variable_mapping={"tas": {"dimensions": {"time": "time"}}}, + compound_name="Amon.tas", + ) + cmoriser.ds = ds + + with patch.object(cmoriser, "_repack_cmip7_output") as repack_mock, patch( + "access_moppy.base.validate_cmip7_output" + ) as qc_mock: + cmoriser.write() + + repack_mock.assert_called_once() + qc_mock.assert_called_once() + assert qc_mock.call_args.args[0] == tmp_path / "tas.nc" + + +@pytest.mark.unit +def test_qc_cli_main_returns_zero_when_all_files_pass(tmp_path, capsys): + path = _write_cmip7_output( + tmp_path, + values=[285.0, 286.0, 287.0], + experiment_id="historical", + filename="passing_only.nc", + ) + + code = qc_main([str(path)]) + + captured = capsys.readouterr() + assert code == 0 + assert "PASS" in captured.out + + +@pytest.mark.unit +def test_qc_cli_main_returns_one_when_any_file_fails(tmp_path, capsys): + passing = _write_cmip7_output( + tmp_path, + values=[285.0, 286.0, 287.0], + experiment_id="historical", + filename="passing.nc", + ) + failing = _write_cmip7_output( + tmp_path, + values=[324.0, 326.5], + experiment_id="piControl", + filename="failing.nc", + ) + + code = qc_main([str(passing), str(failing)]) + + captured = capsys.readouterr() + assert code == 1 + assert "PASS" in captured.out + assert "FAIL" in captured.out From b5af570d9762d8dbbb178c93d6949b44de7d3f75 Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Tue, 23 Jun 2026 18:06:09 +1000 Subject: [PATCH 02/21] Add per-variable CMIP7 QC rules and batch report integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- docs/source/qc_validation.rst | 83 +- src/access_moppy/batch_report.py | 82 +- src/access_moppy/qc/cmip7.py | 303 +- .../resources/qc/cmip7_ranges.yml | 4383 +++++++++++++++++ tests/unit/test_cmip7_qc.py | 100 +- 5 files changed, 4932 insertions(+), 19 deletions(-) diff --git a/docs/source/qc_validation.rst b/docs/source/qc_validation.rst index 58862414..76e229cd 100644 --- a/docs/source/qc_validation.rst +++ b/docs/source/qc_validation.rst @@ -8,10 +8,16 @@ Scope ----- - QC is run on the *CMORised output file*, not the raw model input. -- The current implementation includes CMIP7 physical-range checks for - ``tas``. -- Experiment-aware rules are loaded from: - ``access_moppy/resources/qc/cmip7_ranges.yml``. +- For ``source_id=ACCESS-ESM1-6``, QC covers all variables present in + ``ACCESS-ESM1-6_mappings.json`` with generic checks (non-missing values, + finite values, and enforcement of mapping ``positive`` sign constraints + where defined). +- Physical ranges for all 293 ACCESS-ESM1-6 mapped variables are defined + explicitly in the QC configuration with defaults and experiment-specific + overrides (historical, piControl, ssp*). +- Each variable's physical range is derived from its definition (mapping units + and ``positive`` direction) and stored as a per-variable rule entry. +- Rules are loaded from: ``access_moppy/resources/qc/cmip7_ranges.yml``. Running QC in Notebooks ----------------------- @@ -61,15 +67,82 @@ For CMIP7 runs, ACCESS-MOPPy automatically validates output in the write path after writing and repacking the file. In other words, when you call ``cmoriser.write()`` for CMIP7 output, QC is already executed. +Batch Report QC Summary +----------------------- + +When running a batch CMORisation, the batch report (``moppy_batch_report.json``) +automatically includes a QC section summarizing validation results for all +CMORised output files: + +.. code-block:: json + + { + "qc": { + "passed": 42, + "failed": 2, + "total": 44, + "failures": [ + { + "file": "/output/path/tas.nc", + "variable_id": "tas", + "experiment_id": "piControl", + "error": "Observed range 182.000..329.400 K is outside allowed 180.000..325.000 K.", + "observed_range": [182.0, 329.4], + "allowed_range": [180.0, 325.0], + "units": "K" + } + ] + } + } + +To disable QC collection during batch report generation, use one of: + +.. code-block:: bash + + # Environment variable + export MOPPY_SKIP_QC=1 + moppy-batch-report --db cmor_tasks.db + + # CLI flag + moppy-batch-report --db cmor_tasks.db --skip-qc + +Or programmatically: + +.. code-block:: python + + from access_moppy.batch_report import write_batch_report + write_batch_report(db_path, skip_qc=True) + Extending rules + --------------- -To add more variables or experiment-specific thresholds, update: +To add experiment-specific thresholds for a variable, or to override ranges +for newly added variables, edit: .. code-block:: text src/access_moppy/resources/qc/cmip7_ranges.yml +Under the ``variables`` section, each variable has a ``default`` entry and an +optional ``experiments`` map for experiment-specific min/max values. For example: + +.. code-block:: yaml + + variables: + tas: + units: K + default: + min: 180.0 + max: 330.0 + experiments: + historical: + min: 180.0 + max: 330.0 + piControl: + min: 180.0 + max: 325.0 + Rule structure example: .. code-block:: yaml diff --git a/src/access_moppy/batch_report.py b/src/access_moppy/batch_report.py index 1dbe517c..fcdaac78 100644 --- a/src/access_moppy/batch_report.py +++ b/src/access_moppy/batch_report.py @@ -213,6 +213,61 @@ def _pbs_report( return _compact_dict(pbs) +def _run_qc_on_output_folder(output_folder: Path) -> dict[str, Any] | None: + """Run QC on CMORised netCDF files found in output folder and return results. + + Returns None if QC cannot be run (import unavailable, no files found, or disabled). + Set environment variable MOPPY_SKIP_QC=1 to disable QC collection. + """ + import os + + if os.environ.get("MOPPY_SKIP_QC", "").lower() in ("1", "true", "yes"): + return None + + try: + from access_moppy.qc.cmip7 import validate_cmip7_output_detailed + except ImportError: + return None + + nc_files = sorted(output_folder.glob("**/*.nc")) + if not nc_files: + return None + + results = { + "passed": 0, + "failed": 0, + "total": len(nc_files), + "failures": [], + } + + for nc_file in nc_files: + result = validate_cmip7_output_detailed(nc_file) + if result.passed: + results["passed"] += 1 + else: + results["failed"] += 1 + failure = { + "file": str(nc_file), + "variable_id": result.variable_id, + "experiment_id": result.experiment_id, + "error": result.error, + } + if result.observed_min is not None: + failure["observed_range"] = [ + float(result.observed_min), + float(result.observed_max), + ] + if result.allowed_min is not None: + failure["allowed_range"] = [ + float(result.allowed_min), + float(result.allowed_max), + ] + if result.units: + failure["units"] = result.units + results["failures"].append(failure) + + return results if results["total"] > 0 else None + def build_batch_report( db_path: str | Path, *, @@ -223,12 +278,16 @@ def build_batch_report( created_at: str | None = None, completed_at: str | None = None, stderr_tail_lines: int = 20, + skip_qc: bool = False, ) -> dict[str, Any]: """Build a JSON-serialisable batch coordination report. SQLite remains the source of truth; this report is a derived snapshot for after-the-fact completion checks, provenance capture, and dashboard/database ingestion. + + Args: + skip_qc: If True, skip QC data collection. Can also be set via MOPPY_SKIP_QC env var. """ db_path = Path(db_path) output_path = Path(output_folder) if output_folder is not None else db_path.parent @@ -281,7 +340,9 @@ def build_batch_report( monitor = _read_monitor_sidecar(output_path) monitor.update(_monitor_log_paths(script_path)) - return { + qc_results = None if skip_qc else _run_qc_on_output_folder(output_path) + + report_dict = { "schema_version": SCHEMA_VERSION, "created_at": now, "completed_at": final_completed_at, @@ -298,14 +359,23 @@ def build_batch_report( "tasks": tasks, "failures": failures, } + if qc_results is not None: + report_dict["qc"] = qc_results + + return report_dict def write_batch_report( db_path: str | Path, output_path: str | Path | None = None, + skip_qc: bool = False, **kwargs: Any, ) -> Path: - """Write a durable batch report and return the report path.""" + """Write a durable batch report and return the report path. + + Args: + skip_qc: If True, skip QC data collection. Can also be set via MOPPY_SKIP_QC env var. + """ db_path = Path(db_path) report_path = ( Path(output_path) @@ -313,7 +383,7 @@ def write_batch_report( else db_path.parent / REPORT_FILENAME ) report_path.parent.mkdir(parents=True, exist_ok=True) - report = build_batch_report(db_path, **kwargs) + report = build_batch_report(db_path, skip_qc=skip_qc, **kwargs) report_path.write_text( json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8" ) @@ -340,6 +410,11 @@ def _build_parser() -> argparse.ArgumentParser: default=20, help="Number of stderr tail lines to include for failed tasks (default: 20).", ) + parser.add_argument( + "--skip-qc", + action="store_true", + help="Skip QC data collection (can also set MOPPY_SKIP_QC=1).", + ) return parser @@ -355,6 +430,7 @@ def main(argv: list[str] | None = None) -> int: report_path = write_batch_report( args.db, args.output, + skip_qc=args.skip_qc, config=config, config_path=args.config, script_dir=args.script_dir, diff --git a/src/access_moppy/qc/cmip7.py b/src/access_moppy/qc/cmip7.py index 82f7cb6f..15572814 100644 --- a/src/access_moppy/qc/cmip7.py +++ b/src/access_moppy/qc/cmip7.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import json from dataclasses import dataclass from fnmatch import fnmatchcase from functools import lru_cache @@ -23,13 +24,64 @@ class RangeRule: rule_name: str +@dataclass +class ValidationResult: + """Result of a single file validation.""" + + file_path: str + passed: bool + variable_id: str | None = None + experiment_id: str | None = None + error: str | None = None + observed_min: float | None = None + observed_max: float | None = None + allowed_min: float | None = None + allowed_max: float | None = None + units: str | None = None + + @lru_cache(maxsize=1) -def _load_rules() -> dict[str, Any]: +def _load_esm16_mapping_variables() -> dict[str, dict[str, Any]]: + """Flatten ACCESS-ESM1-6 mapping entries keyed by CMIP variable id.""" + + resource = files("access_moppy") / "mappings" / "ACCESS-ESM1-6_mappings.json" + with as_file(resource) as path: + with open(path, "r", encoding="utf-8") as handle: + mapping = json.load(handle) + + flattened: dict[str, dict[str, Any]] = {} + for section_name, section in mapping.items(): + if section_name == "model_info" or not isinstance(section, dict): + continue + for variable_id, metadata in section.items(): + if isinstance(metadata, dict): + flattened[variable_id] = metadata + return flattened + + +@lru_cache(maxsize=1) +def _load_qc_config() -> dict[str, Any]: resource = files("access_moppy.resources.qc") / "cmip7_ranges.yml" with as_file(resource) as path: with open(path, "r", encoding="utf-8") as handle: - payload = yaml.safe_load(handle) or {} - return payload.get("variables", {}) + return yaml.safe_load(handle) or {} + + +@lru_cache(maxsize=1) +def _load_rules() -> dict[str, Any]: + return _load_qc_config().get("variables", {}) + + +@lru_cache(maxsize=1) +def _load_unit_envelopes() -> dict[str, dict[str, Any]]: + return _load_qc_config().get("unit_envelopes", {}) + + +@lru_cache(maxsize=1) +def _load_mapping_variable_ranges() -> dict[str, dict[str, Any]]: + """Optional per-variable overrides for mapping-derived physical ranges.""" + + return _load_qc_config().get("mapping_variable_ranges", {}) def _select_experiment_rule( @@ -78,6 +130,49 @@ def _resolve_range_rule(variable_id: str, experiment_id: str) -> RangeRule | Non ) +def _resolve_range_rule_from_mapping_definition( + variable_id: str, + experiment_id: str, + mapping_entry: dict[str, Any], +) -> RangeRule | None: + override = _load_mapping_variable_ranges().get(variable_id, {}) + + units = mapping_entry.get("units") + if not isinstance(units, str) or not units: + return None + + if isinstance(override.get("units"), str) and override.get("units"): + units = override["units"] + + envelope = _load_unit_envelopes().get(units) + if "min" in override and "max" in override: + minimum = float(override["min"]) + maximum = float(override["max"]) + else: + if not envelope or "min" not in envelope or "max" not in envelope: + return None + minimum = float(envelope["min"]) + maximum = float(envelope["max"]) + + apply_positive = bool(override.get("apply_positive", True)) + + if apply_positive: + positive = mapping_entry.get("positive") + if positive == "up": + minimum = max(0.0, minimum) + elif positive == "down": + maximum = min(0.0, maximum) + + return RangeRule( + variable_id=variable_id, + experiment_id=experiment_id, + units=units, + minimum=minimum, + maximum=maximum, + rule_name=f"mapping-variable:{variable_id}", + ) + + def _select_output_variable(ds: xr.Dataset, attrs: dict[str, Any]) -> str: candidate_names = [ attrs.get("branded_variable"), @@ -97,6 +192,60 @@ def _select_output_variable(ds: xr.Dataset, attrs: dict[str, Any]) -> str: ) +def _validate_esm16_mapping_checks( + da: xr.DataArray, + *, + variable_id: str, + experiment_id: str, + mapping_entry: dict[str, Any], +) -> None: + """Validate generic checks for ACCESS-ESM1-6 mapped variables.""" + + non_missing = int(da.count().item()) + if non_missing == 0: + raise ValueError( + "CMIP7 QC failed for " + f"{variable_id} in experiment {experiment_id}: all values are missing." + ) + + if np.issubdtype(da.dtype, np.number): + max_abs = float(np.abs(da).max(skipna=True).item()) + if np.isinf(max_abs): + raise ValueError( + "CMIP7 QC failed for " + f"{variable_id} in experiment {experiment_id}: values contain infinity." + ) + + positive = mapping_entry.get("positive") + tolerance = 1e-12 + if positive == "up": + min_value = float(da.min(skipna=True).item()) + if min_value < -tolerance: + raise ValueError( + "CMIP7 QC failed for " + f"{variable_id} in experiment {experiment_id}: expected non-negative " + f"values from mapping 'positive: up', observed minimum {min_value:.6g}." + ) + elif positive == "down": + max_value = float(da.max(skipna=True).item()) + if max_value > tolerance: + raise ValueError( + "CMIP7 QC failed for " + f"{variable_id} in experiment {experiment_id}: expected non-positive " + f"values from mapping 'positive: down', observed maximum {max_value:.6g}." + ) + + expected_units = mapping_entry.get("units") + if isinstance(expected_units, str) and expected_units: + actual_units = da.attrs.get("units") + if actual_units != expected_units: + raise ValueError( + "CMIP7 QC failed for " + f"{variable_id} in experiment {experiment_id}: expected units {expected_units!r} " + f"from ACCESS-ESM1-6 mapping, found {actual_units!r}." + ) + + def validate_cmip7_output(output_path: str | Path) -> None: """Validate a CMIP7 CMORised file against output-time physical range rules.""" @@ -105,6 +254,7 @@ def validate_cmip7_output(output_path: str | Path) -> None: attrs = dict(ds.attrs) variable_id = attrs.get("variable_id") experiment_id = attrs.get("experiment_id") + source_id = attrs.get("source_id") if not isinstance(variable_id, str) or not variable_id: raise ValueError( @@ -116,12 +266,31 @@ def validate_cmip7_output(output_path: str | Path) -> None: ) rule = _resolve_range_rule(variable_id, experiment_id) - if rule is None: - return output_variable = _select_output_variable(ds, attrs) da = ds[output_variable] + # Apply generic checks for all variables present in the ACCESS-ESM1-6 + # mapping so every mapped variable receives QC coverage. + if source_id == "ACCESS-ESM1-6": + mapping_entry = _load_esm16_mapping_variables().get(variable_id) + if mapping_entry is not None: + _validate_esm16_mapping_checks( + da, + variable_id=variable_id, + experiment_id=experiment_id, + mapping_entry=mapping_entry, + ) + if rule is None: + rule = _resolve_range_rule_from_mapping_definition( + variable_id, + experiment_id, + mapping_entry, + ) + + if rule is None: + return + units = da.attrs.get("units") or attrs.get("units") if rule.units is not None and units != rule.units: raise ValueError( @@ -148,6 +317,130 @@ def validate_cmip7_output(output_path: str | Path) -> None: ) +def validate_cmip7_output_detailed(output_path: str | Path) -> ValidationResult: + """Validate a CMIP7 file and return detailed results (does not raise).""" + + path = Path(output_path) + try: + with xr.open_dataset(path) as ds: + attrs = dict(ds.attrs) + variable_id = attrs.get("variable_id") + experiment_id = attrs.get("experiment_id") + source_id = attrs.get("source_id") + + if not isinstance(variable_id, str) or not variable_id: + return ValidationResult( + file_path=str(path), + passed=False, + error="CMIP7 QC requires a 'variable_id' global attribute.", + ) + if not isinstance(experiment_id, str) or not experiment_id: + return ValidationResult( + file_path=str(path), + passed=False, + variable_id=variable_id, + error="CMIP7 QC requires an 'experiment_id' global attribute.", + ) + + rule = _resolve_range_rule(variable_id, experiment_id) + + output_variable = _select_output_variable(ds, attrs) + da = ds[output_variable] + + # Apply generic checks for all variables present in the ACCESS-ESM1-6 + # mapping so every mapped variable receives QC coverage. + if source_id == "ACCESS-ESM1-6": + mapping_entry = _load_esm16_mapping_variables().get(variable_id) + if mapping_entry is not None: + try: + _validate_esm16_mapping_checks( + da, + variable_id=variable_id, + experiment_id=experiment_id, + mapping_entry=mapping_entry, + ) + except ValueError as exc: + return ValidationResult( + file_path=str(path), + passed=False, + variable_id=variable_id, + experiment_id=experiment_id, + error=str(exc), + ) + if rule is None: + rule = _resolve_range_rule_from_mapping_definition( + variable_id, + experiment_id, + mapping_entry, + ) + + if rule is None: + return ValidationResult( + file_path=str(path), + passed=True, + variable_id=variable_id, + experiment_id=experiment_id, + ) + + units = da.attrs.get("units") or attrs.get("units") + if rule.units is not None and units != rule.units: + return ValidationResult( + file_path=str(path), + passed=False, + variable_id=variable_id, + experiment_id=experiment_id, + units=units, + error=f"Expected units {rule.units!r}, found {units!r}.", + ) + + minimum = da.min(skipna=True).item() + maximum = da.max(skipna=True).item() + + if np.isnan(minimum) or np.isnan(maximum): + return ValidationResult( + file_path=str(path), + passed=True, + variable_id=variable_id, + experiment_id=experiment_id, + units=units, + ) + + observed_min = float(minimum) + observed_max = float(maximum) + + if observed_min < rule.minimum or observed_max > rule.maximum: + return ValidationResult( + file_path=str(path), + passed=False, + variable_id=variable_id, + experiment_id=experiment_id, + units=units, + observed_min=observed_min, + observed_max=observed_max, + allowed_min=rule.minimum, + allowed_max=rule.maximum, + error=f"Observed range {observed_min:.3f}..{observed_max:.3f} outside allowed {rule.minimum:.3f}..{rule.maximum:.3f}.", + ) + + return ValidationResult( + file_path=str(path), + passed=True, + variable_id=variable_id, + experiment_id=experiment_id, + units=units, + observed_min=observed_min, + observed_max=observed_max, + allowed_min=rule.minimum, + allowed_max=rule.maximum, + ) + except Exception as exc: # noqa: BLE001 + return ValidationResult( + file_path=str(path), + passed=False, + error=f"Unexpected error: {exc}", + ) + + def _build_cli_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="moppy-qc", diff --git a/src/access_moppy/resources/qc/cmip7_ranges.yml b/src/access_moppy/resources/qc/cmip7_ranges.yml index 2ef7dad4..cb66af37 100644 --- a/src/access_moppy/resources/qc/cmip7_ranges.yml +++ b/src/access_moppy/resources/qc/cmip7_ranges.yml @@ -1,4 +1,3712 @@ +# Per-variable QC rules for ACCESS-ESM1-6 mapped variables. +# Each variable has units, default min/max, and optional experiment-specific overrides. + variables: + agessc: + units: yr + default: + min: 0.0 + max: 1000000.0 + experiments: + historical: + min: 0.0 + max: 1000000.0 + piControl: + min: 0.0 + max: 1000000.0 + ssp*: + min: 0.0 + max: 1000000.0 + arag: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + areacella: + units: m2 + default: + min: 0.0 + max: 1000000000000000.0 + experiments: + historical: + min: 0.0 + max: 1000000000000000.0 + piControl: + min: 0.0 + max: 1000000000000000.0 + ssp*: + min: 0.0 + max: 1000000000000000.0 + areacello: + units: m2 + default: + min: 0.0 + max: 1000000000000000.0 + experiments: + historical: + min: 0.0 + max: 1000000000000000.0 + piControl: + min: 0.0 + max: 1000000000000000.0 + ssp*: + min: 0.0 + max: 1000000000000000.0 + baresoilFrac: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + bigthetao: + units: degC + default: + min: -50.0 + max: 100.0 + experiments: + historical: + min: -50.0 + max: 100.0 + piControl: + min: -50.0 + max: 100.0 + ssp*: + min: -50.0 + max: 100.0 + bigthetaoga: + units: degC + default: + min: -50.0 + max: 100.0 + experiments: + historical: + min: -50.0 + max: 100.0 + piControl: + min: -50.0 + max: 100.0 + ssp*: + min: -50.0 + max: 100.0 + c3PftFrac: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + c4PftFrac: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + cLand: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + cLeaf: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + cLitter: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + cProduct: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + cRoot: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + cSoil: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + cSoilFast: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + cSoilMedium: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + cSoilSlow: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + cVeg: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + calcos: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + chl: + units: kg m-3 + default: + min: 0.0 + max: 200000.0 + experiments: + historical: + min: 0.0 + max: 200000.0 + piControl: + min: 0.0 + max: 200000.0 + ssp*: + min: 0.0 + max: 200000.0 + chlos: + units: kg m-3 + default: + min: 0.0 + max: 200000.0 + experiments: + historical: + min: 0.0 + max: 200000.0 + piControl: + min: 0.0 + max: 200000.0 + ssp*: + min: 0.0 + max: 200000.0 + ci: + units: '1' + default: + min: 0.0 + max: 1000000.0 + experiments: + historical: + min: 0.0 + max: 1000000.0 + piControl: + min: 0.0 + max: 1000000.0 + ssp*: + min: 0.0 + max: 1000000.0 + cl: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + cli: + units: kg kg-1 + default: + min: 0.0 + max: 1.0 + experiments: + historical: + min: 0.0 + max: 1.0 + piControl: + min: 0.0 + max: 1.0 + ssp*: + min: 0.0 + max: 1.0 + clivi: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + clt: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + clw: + units: kg kg-1 + default: + min: 0.0 + max: 1.0 + experiments: + historical: + min: 0.0 + max: 1.0 + piControl: + min: 0.0 + max: 1.0 + ssp*: + min: 0.0 + max: 1.0 + clwvi: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + co23D: + units: kg kg-1 + default: + min: 0.0 + max: 1.0 + experiments: + historical: + min: 0.0 + max: 1.0 + piControl: + min: 0.0 + max: 1.0 + ssp*: + min: 0.0 + max: 1.0 + cropFrac: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + cropFracC3: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + deptho: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + detoc: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + dfe: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + dfeos: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + dissic: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + dissicos: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + epc100: + units: mol m-2 s-1 + default: + min: -1000.0 + max: 1000.0 + experiments: + historical: + min: -1000.0 + max: 1000.0 + piControl: + min: -1000.0 + max: 1000.0 + ssp*: + min: -1000.0 + max: 1000.0 + evs: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + evspsbl: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + evspsblsoi: + units: kg m-2 s-1 + default: + min: 0.0 + max: 10000.0 + experiments: + historical: + min: 0.0 + max: 10000.0 + piControl: + min: 0.0 + max: 10000.0 + ssp*: + min: 0.0 + max: 10000.0 + evspsblveg: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + expc: + units: mol m-2 s-1 + default: + min: -1000.0 + max: 0.0 + experiments: + historical: + min: -1000.0 + max: 0.0 + piControl: + min: -1000.0 + max: 0.0 + ssp*: + min: -1000.0 + max: 0.0 + expcalc: + units: mol m-2 s-1 + default: + min: -1000.0 + max: 0.0 + experiments: + historical: + min: -1000.0 + max: 0.0 + piControl: + min: -1000.0 + max: 0.0 + ssp*: + min: -1000.0 + max: 0.0 + fBNF: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + fDeforestToProduct: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + fNdep: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + fNgas: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + fNleach: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + fNloss: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + fNnetmin: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + fNup: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + fProductDecomp: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + fgco2: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 0.0 + experiments: + historical: + min: -10000.0 + max: 0.0 + piControl: + min: -10000.0 + max: 0.0 + ssp*: + min: -10000.0 + max: 0.0 + ficeberg2d: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + friver: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + fsfe: + units: mol m-2 s-1 + default: + min: -1000.0 + max: 1000.0 + experiments: + historical: + min: -1000.0 + max: 1000.0 + piControl: + min: -1000.0 + max: 1000.0 + ssp*: + min: -1000.0 + max: 1000.0 + fsitherm: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + fsn: + units: mol m-2 s-1 + default: + min: -1000.0 + max: 1000.0 + experiments: + historical: + min: -1000.0 + max: 1000.0 + piControl: + min: -1000.0 + max: 1000.0 + ssp*: + min: -1000.0 + max: 1000.0 + gpp: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 0.0 + experiments: + historical: + min: -10000.0 + max: 0.0 + piControl: + min: -10000.0 + max: 0.0 + ssp*: + min: -10000.0 + max: 0.0 + grassFrac: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + grassFracC3: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + grassFracC4: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + graz: + units: mol m-3 s-1 + default: + min: -1000.0 + max: 1000.0 + experiments: + historical: + min: -1000.0 + max: 1000.0 + piControl: + min: -1000.0 + max: 1000.0 + ssp*: + min: -1000.0 + max: 1000.0 + hfds: + units: W m-2 + default: + min: -100000.0 + max: 0.0 + experiments: + historical: + min: -100000.0 + max: 0.0 + piControl: + min: -100000.0 + max: 0.0 + ssp*: + min: -100000.0 + max: 0.0 + hfevapds: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + hfgeou: + units: W m-2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + hfibthermds2d: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + hfls: + units: W m-2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + hflso: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + hfrainds: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + hfrunoffds2d: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + hfsifrazil: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + hfsifrazil2d: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + hfsnthermds2d: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + hfss: + units: W m-2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + hfsso: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + hur: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + hurs: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + hursmax: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + hursmin: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + hus: + units: '1' + default: + min: 0.0 + max: 1000000.0 + experiments: + historical: + min: 0.0 + max: 1000000.0 + piControl: + min: 0.0 + max: 1000000.0 + ssp*: + min: 0.0 + max: 1000000.0 + huss: + units: '1' + default: + min: 0.0 + max: 1000000.0 + experiments: + historical: + min: 0.0 + max: 1000000.0 + piControl: + min: 0.0 + max: 1000000.0 + ssp*: + min: 0.0 + max: 1000000.0 + intdic: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + intpoc: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + intpp: + units: mol m-2 s-1 + default: + min: -1000.0 + max: 1000.0 + experiments: + historical: + min: -1000.0 + max: 1000.0 + piControl: + min: -1000.0 + max: 1000.0 + ssp*: + min: -1000.0 + max: 1000.0 + intppnitrate: + units: mol m-2 s-1 + default: + min: -1000.0 + max: 1000.0 + experiments: + historical: + min: -1000.0 + max: 1000.0 + piControl: + min: -1000.0 + max: 1000.0 + ssp*: + min: -1000.0 + max: 1000.0 + intuaw: + units: kg m-1 s-1 + default: + min: -1000000.0 + max: 1000000.0 + experiments: + historical: + min: -1000000.0 + max: 1000000.0 + piControl: + min: -1000000.0 + max: 1000000.0 + ssp*: + min: -1000000.0 + max: 1000000.0 + intvaw: + units: kg m-1 s-1 + default: + min: -1000000.0 + max: 1000000.0 + experiments: + historical: + min: -1000000.0 + max: 1000000.0 + piControl: + min: -1000000.0 + max: 1000000.0 + ssp*: + min: -1000000.0 + max: 1000000.0 + lai: + units: '1' + default: + min: 0.0 + max: 1000000.0 + experiments: + historical: + min: 0.0 + max: 1000000.0 + piControl: + min: 0.0 + max: 1000000.0 + ssp*: + min: 0.0 + max: 1000000.0 + landCoverFrac: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + masscello: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + mc: + units: kg m-2 s-1 + default: + min: 0.0 + max: 10000.0 + experiments: + historical: + min: 0.0 + max: 10000.0 + piControl: + min: 0.0 + max: 10000.0 + ssp*: + min: 0.0 + max: 10000.0 + mlotst: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + mlotstsq: + units: m2 + default: + min: 0.0 + max: 1000000000000000.0 + experiments: + historical: + min: 0.0 + max: 1000000000000000.0 + piControl: + min: 0.0 + max: 1000000000000000.0 + ssp*: + min: 0.0 + max: 1000000000000000.0 + mrfsli: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + mrfso: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + mrro: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + mrrob: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + mrros: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + mrsfl: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + mrsll: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + mrso: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + mrsofc: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + mrsol: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + mrsos: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + msftbarot: + units: kg s-1 + default: + min: -1000000000000.0 + max: 1000000000000.0 + experiments: + historical: + min: -1000000000000.0 + max: 1000000000000.0 + piControl: + min: -1000000000000.0 + max: 1000000000000.0 + ssp*: + min: -1000000000000.0 + max: 1000000000000.0 + msftmrho: + units: kg s-1 + default: + min: -1000000000000.0 + max: 1000000000000.0 + experiments: + historical: + min: -1000000000000.0 + max: 1000000000000.0 + piControl: + min: -1000000000000.0 + max: 1000000000000.0 + ssp*: + min: -1000000000000.0 + max: 1000000000000.0 + msftmz: + units: kg s-1 + default: + min: -1000000000000.0 + max: 1000000000000.0 + experiments: + historical: + min: -1000000000000.0 + max: 1000000000000.0 + piControl: + min: -1000000000000.0 + max: 1000000000000.0 + ssp*: + min: -1000000000000.0 + max: 1000000000000.0 + msftyrho: + units: kg s-1 + default: + min: -1000000000000.0 + max: 1000000000000.0 + experiments: + historical: + min: -1000000000000.0 + max: 1000000000000.0 + piControl: + min: -1000000000000.0 + max: 1000000000000.0 + ssp*: + min: -1000000000000.0 + max: 1000000000000.0 + msftyz: + units: kg s-1 + default: + min: -1000000000000.0 + max: 1000000000000.0 + experiments: + historical: + min: -1000000000000.0 + max: 1000000000000.0 + piControl: + min: -1000000000000.0 + max: 1000000000000.0 + ssp*: + min: -1000000000000.0 + max: 1000000000000.0 + nLand: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + nLitter: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + nMineral: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + nProduct: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + nSoil: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + nVeg: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + nbp: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 0.0 + experiments: + historical: + min: -10000.0 + max: 0.0 + piControl: + min: -10000.0 + max: 0.0 + ssp*: + min: -10000.0 + max: 0.0 + nep: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 0.0 + experiments: + historical: + min: -10000.0 + max: 0.0 + piControl: + min: -10000.0 + max: 0.0 + ssp*: + min: -10000.0 + max: 0.0 + no3: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + no3os: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + npp: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 0.0 + experiments: + historical: + min: -10000.0 + max: 0.0 + piControl: + min: -10000.0 + max: 0.0 + ssp*: + min: -10000.0 + max: 0.0 + o2: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + obvfsq: + units: s-2 + default: + min: -1000000.0 + max: 1000000.0 + experiments: + historical: + min: -1000000.0 + max: 1000000.0 + piControl: + min: -1000000.0 + max: 1000000.0 + ssp*: + min: -1000000.0 + max: 1000000.0 + ocontempdiff: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + ocontempmint: + units: degC kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + ocontemppsmadvect: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + ocontemptend: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + od440aer: + units: '1' + default: + min: 0.0 + max: 1000000.0 + experiments: + historical: + min: 0.0 + max: 1000000.0 + piControl: + min: 0.0 + max: 1000000.0 + ssp*: + min: 0.0 + max: 1000000.0 + od550aer: + units: '1' + default: + min: 0.0 + max: 1000000.0 + experiments: + historical: + min: 0.0 + max: 1000000.0 + piControl: + min: 0.0 + max: 1000000.0 + ssp*: + min: 0.0 + max: 1000000.0 + opottempmint: + units: degC kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + orog: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + osaltdiff: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + osaltpsmadvect: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + osalttend: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + pbo: + units: Pa + default: + min: 0.0 + max: 20000000.0 + experiments: + historical: + min: 0.0 + max: 20000000.0 + piControl: + min: 0.0 + max: 20000000.0 + ssp*: + min: 0.0 + max: 20000000.0 + pfull: + units: Pa + default: + min: 0.0 + max: 20000000.0 + experiments: + historical: + min: 0.0 + max: 20000000.0 + piControl: + min: 0.0 + max: 20000000.0 + ssp*: + min: 0.0 + max: 20000000.0 + ph: + units: '1' + default: + min: 0.0 + max: 1000000.0 + experiments: + historical: + min: 0.0 + max: 1000000.0 + piControl: + min: 0.0 + max: 1000000.0 + ssp*: + min: 0.0 + max: 1000000.0 + phalf: + units: Pa + default: + min: 0.0 + max: 20000000.0 + experiments: + historical: + min: 0.0 + max: 20000000.0 + piControl: + min: 0.0 + max: 20000000.0 + ssp*: + min: 0.0 + max: 20000000.0 + phos: + units: '1' + default: + min: 0.0 + max: 1000000.0 + experiments: + historical: + min: 0.0 + max: 1000000.0 + piControl: + min: 0.0 + max: 1000000.0 + ssp*: + min: 0.0 + max: 1000000.0 + phycos: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + po4: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + pr: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + prc: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + prrc: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + prsn: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + prsnc: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + prw: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + ps: + units: Pa + default: + min: 0.0 + max: 20000000.0 + experiments: + historical: + min: 0.0 + max: 20000000.0 + piControl: + min: 0.0 + max: 20000000.0 + ssp*: + min: 0.0 + max: 20000000.0 + psl: + units: Pa + default: + min: 0.0 + max: 20000000.0 + experiments: + historical: + min: 0.0 + max: 20000000.0 + piControl: + min: 0.0 + max: 20000000.0 + ssp*: + min: 0.0 + max: 20000000.0 + pso: + units: Pa + default: + min: 0.0 + max: 20000000.0 + experiments: + historical: + min: 0.0 + max: 20000000.0 + piControl: + min: 0.0 + max: 20000000.0 + ssp*: + min: 0.0 + max: 20000000.0 + ra: + units: kg m-2 s-1 + default: + min: 0.0 + max: 10000.0 + experiments: + historical: + min: 0.0 + max: 10000.0 + piControl: + min: 0.0 + max: 10000.0 + ssp*: + min: 0.0 + max: 10000.0 + residualFrac: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + rh: + units: kg m-2 s-1 + default: + min: 0.0 + max: 10000.0 + experiments: + historical: + min: 0.0 + max: 10000.0 + piControl: + min: 0.0 + max: 10000.0 + ssp*: + min: 0.0 + max: 10000.0 + rlds: + units: W m-2 + default: + min: -100000.0 + max: 0.0 + experiments: + historical: + min: -100000.0 + max: 0.0 + piControl: + min: -100000.0 + max: 0.0 + ssp*: + min: -100000.0 + max: 0.0 + rldscs: + units: W m-2 + default: + min: -100000.0 + max: 0.0 + experiments: + historical: + min: -100000.0 + max: 0.0 + piControl: + min: -100000.0 + max: 0.0 + ssp*: + min: -100000.0 + max: 0.0 + rlntds: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + rls: + units: W m-2 + default: + min: -100000.0 + max: 0.0 + experiments: + historical: + min: -100000.0 + max: 0.0 + piControl: + min: -100000.0 + max: 0.0 + ssp*: + min: -100000.0 + max: 0.0 + rlus: + units: W m-2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + rluscs: + units: W m-2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + rlut: + units: W m-2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + rlutcs: + units: W m-2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + rootd: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + rsdo: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + rsdoabsorb: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + rsds: + units: W m-2 + default: + min: -100000.0 + max: 0.0 + experiments: + historical: + min: -100000.0 + max: 0.0 + piControl: + min: -100000.0 + max: 0.0 + ssp*: + min: -100000.0 + max: 0.0 + rsdscs: + units: W m-2 + default: + min: -100000.0 + max: 0.0 + experiments: + historical: + min: -100000.0 + max: 0.0 + piControl: + min: -100000.0 + max: 0.0 + ssp*: + min: -100000.0 + max: 0.0 + rsdt: + units: W m-2 + default: + min: -100000.0 + max: 0.0 + experiments: + historical: + min: -100000.0 + max: 0.0 + piControl: + min: -100000.0 + max: 0.0 + ssp*: + min: -100000.0 + max: 0.0 + rsntds: + units: W m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + rss: + units: W m-2 + default: + min: -100000.0 + max: 0.0 + experiments: + historical: + min: -100000.0 + max: 0.0 + piControl: + min: -100000.0 + max: 0.0 + ssp*: + min: -100000.0 + max: 0.0 + rsus: + units: W m-2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + rsuscs: + units: W m-2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + rsut: + units: W m-2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + rsutcs: + units: W m-2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + rtmt: + units: W m-2 + default: + min: -100000.0 + max: 0.0 + experiments: + historical: + min: -100000.0 + max: 0.0 + piControl: + min: -100000.0 + max: 0.0 + ssp*: + min: -100000.0 + max: 0.0 + sbl: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + sci: + units: '1' + default: + min: 0.0 + max: 1000000.0 + experiments: + historical: + min: 0.0 + max: 1000000.0 + piControl: + min: 0.0 + max: 1000000.0 + ssp*: + min: 0.0 + max: 1000000.0 + sfcWind: + units: m s-1 + default: + min: -500.0 + max: 500.0 + experiments: + historical: + min: -500.0 + max: 500.0 + piControl: + min: -500.0 + max: 500.0 + ssp*: + min: -500.0 + max: 500.0 + sfcWindmax: + units: m s-1 + default: + min: -500.0 + max: 500.0 + experiments: + historical: + min: -500.0 + max: 500.0 + piControl: + min: -500.0 + max: 500.0 + ssp*: + min: -500.0 + max: 500.0 + sfriver: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + sftgif: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + sftlf: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + sftof: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + shrubFrac: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + siage: + units: s + default: + min: 0.0 + max: 1000000000000.0 + experiments: + historical: + min: 0.0 + max: 1000000000000.0 + piControl: + min: 0.0 + max: 1000000000000.0 + ssp*: + min: 0.0 + max: 1000000000000.0 + siareaacrossline: + units: m2 s-1 + default: + min: -1000000.0 + max: 1000000.0 + experiments: + historical: + min: -1000000.0 + max: 1000000.0 + piControl: + min: -1000000.0 + max: 1000000.0 + ssp*: + min: -1000000.0 + max: 1000000.0 + siarean: + units: 1e6 km2 + default: + min: 0.0 + max: 1000.0 + experiments: + historical: + min: 0.0 + max: 1000.0 + piControl: + min: 0.0 + max: 1000.0 + ssp*: + min: 0.0 + max: 1000.0 + siareas: + units: 1e6 km2 + default: + min: 0.0 + max: 1000.0 + experiments: + historical: + min: 0.0 + max: 1000.0 + piControl: + min: 0.0 + max: 1000.0 + ssp*: + min: 0.0 + max: 1000.0 + siconc: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + siconca: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + sidconcdyn: + units: s-1 + default: + min: -1000.0 + max: 1000.0 + experiments: + historical: + min: -1000.0 + max: 1000.0 + piControl: + min: -1000.0 + max: 1000.0 + ssp*: + min: -1000.0 + max: 1000.0 + sidconcth: + units: s-1 + default: + min: -1000.0 + max: 1000.0 + experiments: + historical: + min: -1000.0 + max: 1000.0 + piControl: + min: -1000.0 + max: 1000.0 + ssp*: + min: -1000.0 + max: 1000.0 + sidivvel: + units: s-1 + default: + min: -1000.0 + max: 1000.0 + experiments: + historical: + min: -1000.0 + max: 1000.0 + piControl: + min: -1000.0 + max: 1000.0 + ssp*: + min: -1000.0 + max: 1000.0 + sidmassdyn: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + sidmassevapsubl: + units: kg m-2 s-1 + default: + min: 0.0 + max: 10000.0 + experiments: + historical: + min: 0.0 + max: 10000.0 + piControl: + min: 0.0 + max: 10000.0 + ssp*: + min: 0.0 + max: 10000.0 + sidmassgrowthbot: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + sidmassgrowthsi: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + sidmassgrowthwat: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + sidmassmeltbot: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + sidmassmeltlat: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + sidmassmelttop: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + sidmassth: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + sidmasstranx: + units: kg s-1 + default: + min: -1000000000000.0 + max: 1000000000000.0 + experiments: + historical: + min: -1000000000000.0 + max: 1000000000000.0 + piControl: + min: -1000000000000.0 + max: 1000000000000.0 + ssp*: + min: -1000000000000.0 + max: 1000000000000.0 + sidmasstrany: + units: kg s-1 + default: + min: -1000000000000.0 + max: 1000000000000.0 + experiments: + historical: + min: -1000000000000.0 + max: 1000000000000.0 + piControl: + min: -1000000000000.0 + max: 1000000000000.0 + ssp*: + min: -1000000000000.0 + max: 1000000000000.0 + siextentn: + units: 1e6 km2 + default: + min: 0.0 + max: 1000.0 + experiments: + historical: + min: 0.0 + max: 1000.0 + piControl: + min: 0.0 + max: 1000.0 + ssp*: + min: 0.0 + max: 1000.0 + siextents: + units: 1e6 km2 + default: + min: 0.0 + max: 1000.0 + experiments: + historical: + min: 0.0 + max: 1000.0 + piControl: + min: 0.0 + max: 1000.0 + ssp*: + min: 0.0 + max: 1000.0 + sifb: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + siflfwbot: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 0.0 + experiments: + historical: + min: -10000.0 + max: 0.0 + piControl: + min: -10000.0 + max: 0.0 + ssp*: + min: -10000.0 + max: 0.0 + siflswdtop: + units: W m-2 + default: + min: -100000.0 + max: 0.0 + experiments: + historical: + min: -100000.0 + max: 0.0 + piControl: + min: -100000.0 + max: 0.0 + ssp*: + min: -100000.0 + max: 0.0 + siflswutop: + units: W m-2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + simass: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + simassacrossline: + units: kg s-1 + default: + min: -1000000000000.0 + max: 1000000000000.0 + experiments: + historical: + min: -1000000000000.0 + max: 1000000000000.0 + piControl: + min: -1000000000000.0 + max: 1000000000000.0 + ssp*: + min: -1000000000000.0 + max: 1000000000000.0 + sisnconc: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + sisndmassmelt: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + sisndmasssi: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + sisndmasssnf: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + sisnhc: + units: J m-2 + default: + min: -1000000000000.0 + max: 1000000000000.0 + experiments: + historical: + min: -1000000000000.0 + max: 1000000000000.0 + piControl: + min: -1000000000000.0 + max: 1000000000000.0 + ssp*: + min: -1000000000000.0 + max: 1000000000000.0 + sisnmassn: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + sisnmasss: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + sisnthick: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + sispeed: + units: m s-1 + default: + min: -500.0 + max: 500.0 + experiments: + historical: + min: -500.0 + max: 500.0 + piControl: + min: -500.0 + max: 500.0 + ssp*: + min: -500.0 + max: 500.0 + sistrxdtop: + units: N m-2 + default: + min: -100000.0 + max: 0.0 + experiments: + historical: + min: -100000.0 + max: 0.0 + piControl: + min: -100000.0 + max: 0.0 + ssp*: + min: -100000.0 + max: 0.0 + sistrxubot: + units: N m-2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + sistrydtop: + units: N m-2 + default: + min: -100000.0 + max: 0.0 + experiments: + historical: + min: -100000.0 + max: 0.0 + piControl: + min: -100000.0 + max: 0.0 + ssp*: + min: -100000.0 + max: 0.0 + sistryubot: + units: N m-2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + sitempbot: + units: K + default: + min: 100.0 + max: 400.0 + experiments: + historical: + min: 100.0 + max: 400.0 + piControl: + min: 100.0 + max: 400.0 + ssp*: + min: 100.0 + max: 400.0 + sitemptop: + units: K + default: + min: 100.0 + max: 400.0 + experiments: + historical: + min: 100.0 + max: 400.0 + piControl: + min: 100.0 + max: 400.0 + ssp*: + min: 100.0 + max: 400.0 + sithick: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + sitimefrac: + units: '1' + default: + min: 0.0 + max: 1000000.0 + experiments: + historical: + min: 0.0 + max: 1000000.0 + piControl: + min: 0.0 + max: 1000000.0 + ssp*: + min: 0.0 + max: 1000000.0 + siu: + units: m s-1 + default: + min: -500.0 + max: 500.0 + experiments: + historical: + min: -500.0 + max: 500.0 + piControl: + min: -500.0 + max: 500.0 + ssp*: + min: -500.0 + max: 500.0 + siv: + units: m s-1 + default: + min: -500.0 + max: 500.0 + experiments: + historical: + min: -500.0 + max: 500.0 + piControl: + min: -500.0 + max: 500.0 + ssp*: + min: -500.0 + max: 500.0 + sivol: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + sivoln: + units: 1e3 km3 + default: + min: 0.0 + max: 1000000.0 + experiments: + historical: + min: 0.0 + max: 1000000.0 + piControl: + min: 0.0 + max: 1000000.0 + ssp*: + min: 0.0 + max: 1000000.0 + sivols: + units: 1e3 km3 + default: + min: 0.0 + max: 1000000.0 + experiments: + historical: + min: 0.0 + max: 1000000.0 + piControl: + min: 0.0 + max: 1000000.0 + ssp*: + min: 0.0 + max: 1000000.0 + slthick: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + snc: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + snw: + units: kg m-2 + default: + min: -1000000000.0 + max: 1000000000.0 + experiments: + historical: + min: -1000000000.0 + max: 1000000000.0 + piControl: + min: -1000000000.0 + max: 1000000000.0 + ssp*: + min: -1000000000.0 + max: 1000000000.0 + so: + units: '0.001' + default: + min: 0.0 + max: 1000.0 + experiments: + historical: + min: 0.0 + max: 1000.0 + piControl: + min: 0.0 + max: 1000.0 + ssp*: + min: 0.0 + max: 1000.0 + sob: + units: '0.001' + default: + min: 0.0 + max: 1000.0 + experiments: + historical: + min: 0.0 + max: 1000.0 + piControl: + min: 0.0 + max: 1000.0 + ssp*: + min: 0.0 + max: 1000.0 + soga: + units: '0.001' + default: + min: 0.0 + max: 1000.0 + experiments: + historical: + min: 0.0 + max: 1000.0 + piControl: + min: 0.0 + max: 1000.0 + ssp*: + min: 0.0 + max: 1000.0 + somint: + units: g m-2 + default: + min: 0.0 + max: 1000000000000.0 + experiments: + historical: + min: 0.0 + max: 1000000000000.0 + piControl: + min: 0.0 + max: 1000000000000.0 + ssp*: + min: 0.0 + max: 1000000000000.0 + sos: + units: '0.001' + default: + min: 0.0 + max: 1000.0 + experiments: + historical: + min: 0.0 + max: 1000.0 + piControl: + min: 0.0 + max: 1000.0 + ssp*: + min: 0.0 + max: 1000.0 + sosga: + units: '0.001' + default: + min: 0.0 + max: 1000.0 + experiments: + historical: + min: 0.0 + max: 1000.0 + piControl: + min: 0.0 + max: 1000.0 + ssp*: + min: 0.0 + max: 1000.0 + spco2: + units: Pa + default: + min: 0.0 + max: 20000000.0 + experiments: + historical: + min: 0.0 + max: 20000000.0 + piControl: + min: 0.0 + max: 20000000.0 + ssp*: + min: 0.0 + max: 20000000.0 + ta: + units: K + default: + min: 100.0 + max: 400.0 + experiments: + historical: + min: 100.0 + max: 400.0 + piControl: + min: 100.0 + max: 400.0 + ssp*: + min: 100.0 + max: 400.0 + talk: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 tas: units: K default: @@ -14,3 +3722,678 @@ variables: ssp*: min: 180.0 max: 335.0 + tasmax: + units: K + default: + min: 100.0 + max: 400.0 + experiments: + historical: + min: 100.0 + max: 400.0 + piControl: + min: 100.0 + max: 400.0 + ssp*: + min: 100.0 + max: 400.0 + tasmin: + units: K + default: + min: 100.0 + max: 400.0 + experiments: + historical: + min: 100.0 + max: 400.0 + piControl: + min: 100.0 + max: 400.0 + ssp*: + min: 100.0 + max: 400.0 + tauu: + units: Pa + default: + min: 0.0 + max: 0.0 + experiments: + historical: + min: 0.0 + max: 0.0 + piControl: + min: 0.0 + max: 0.0 + ssp*: + min: 0.0 + max: 0.0 + tauuo: + units: N m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + tauv: + units: Pa + default: + min: 0.0 + max: 0.0 + experiments: + historical: + min: 0.0 + max: 0.0 + piControl: + min: 0.0 + max: 0.0 + ssp*: + min: 0.0 + max: 0.0 + tauvo: + units: N m-2 + default: + min: -100000.0 + max: 100000.0 + experiments: + historical: + min: -100000.0 + max: 100000.0 + piControl: + min: -100000.0 + max: 100000.0 + ssp*: + min: -100000.0 + max: 100000.0 + thetao: + units: degC + default: + min: -50.0 + max: 100.0 + experiments: + historical: + min: -50.0 + max: 100.0 + piControl: + min: -50.0 + max: 100.0 + ssp*: + min: -50.0 + max: 100.0 + thetaoga: + units: degC + default: + min: -50.0 + max: 100.0 + experiments: + historical: + min: -50.0 + max: 100.0 + piControl: + min: -50.0 + max: 100.0 + ssp*: + min: -50.0 + max: 100.0 + thkcello: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + tob: + units: degC + default: + min: -50.0 + max: 100.0 + experiments: + historical: + min: -50.0 + max: 100.0 + piControl: + min: -50.0 + max: 100.0 + ssp*: + min: -50.0 + max: 100.0 + tos: + units: degC + default: + min: -50.0 + max: 100.0 + experiments: + historical: + min: -50.0 + max: 100.0 + piControl: + min: -50.0 + max: 100.0 + ssp*: + min: -50.0 + max: 100.0 + tosga: + units: degC + default: + min: -50.0 + max: 100.0 + experiments: + historical: + min: -50.0 + max: 100.0 + piControl: + min: -50.0 + max: 100.0 + ssp*: + min: -50.0 + max: 100.0 + tossq: + units: degC2 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + tran: + units: kg m-2 s-1 + default: + min: 0.0 + max: 10000.0 + experiments: + historical: + min: 0.0 + max: 10000.0 + piControl: + min: 0.0 + max: 10000.0 + ssp*: + min: 0.0 + max: 10000.0 + treeFrac: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + treeFracBdlDcd: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + treeFracBdlEvg: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + treeFracNdlDcd: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + treeFracNdlEvg: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + ts: + units: K + default: + min: 100.0 + max: 400.0 + experiments: + historical: + min: 100.0 + max: 400.0 + piControl: + min: 100.0 + max: 400.0 + ssp*: + min: 100.0 + max: 400.0 + tsl: + units: K + default: + min: 100.0 + max: 400.0 + experiments: + historical: + min: 100.0 + max: 400.0 + piControl: + min: 100.0 + max: 400.0 + ssp*: + min: 100.0 + max: 400.0 + ua: + units: m s-1 + default: + min: -500.0 + max: 500.0 + experiments: + historical: + min: -500.0 + max: 500.0 + piControl: + min: -500.0 + max: 500.0 + ssp*: + min: -500.0 + max: 500.0 + uas: + units: m s-1 + default: + min: -500.0 + max: 500.0 + experiments: + historical: + min: -500.0 + max: 500.0 + piControl: + min: -500.0 + max: 500.0 + ssp*: + min: -500.0 + max: 500.0 + umo: + units: kg s-1 + default: + min: -1000000000000.0 + max: 1000000000000.0 + experiments: + historical: + min: -1000000000000.0 + max: 1000000000000.0 + piControl: + min: -1000000000000.0 + max: 1000000000000.0 + ssp*: + min: -1000000000000.0 + max: 1000000000000.0 + uo: + units: m s-1 + default: + min: -500.0 + max: 500.0 + experiments: + historical: + min: -500.0 + max: 500.0 + piControl: + min: -500.0 + max: 500.0 + ssp*: + min: -500.0 + max: 500.0 + va: + units: m s-1 + default: + min: -500.0 + max: 500.0 + experiments: + historical: + min: -500.0 + max: 500.0 + piControl: + min: -500.0 + max: 500.0 + ssp*: + min: -500.0 + max: 500.0 + vas: + units: m s-1 + default: + min: -500.0 + max: 500.0 + experiments: + historical: + min: -500.0 + max: 500.0 + piControl: + min: -500.0 + max: 500.0 + ssp*: + min: -500.0 + max: 500.0 + vegFrac: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + vegHeight: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + vmo: + units: kg s-1 + default: + min: -1000000000000.0 + max: 1000000000000.0 + experiments: + historical: + min: -1000000000000.0 + max: 1000000000000.0 + piControl: + min: -1000000000000.0 + max: 1000000000000.0 + ssp*: + min: -1000000000000.0 + max: 1000000000000.0 + vo: + units: m s-1 + default: + min: -500.0 + max: 500.0 + experiments: + historical: + min: -500.0 + max: 500.0 + piControl: + min: -500.0 + max: 500.0 + ssp*: + min: -500.0 + max: 500.0 + volcello: + units: m3 + default: + min: 0.0 + max: 1.0e+21 + experiments: + historical: + min: 0.0 + max: 1.0e+21 + piControl: + min: 0.0 + max: 1.0e+21 + ssp*: + min: 0.0 + max: 1.0e+21 + wap: + units: Pa s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + wetlandFrac: + units: '%' + default: + min: 0.0 + max: 100.0 + experiments: + historical: + min: 0.0 + max: 100.0 + piControl: + min: 0.0 + max: 100.0 + ssp*: + min: 0.0 + max: 100.0 + wfo: + units: kg m-2 s-1 + default: + min: -10000.0 + max: 10000.0 + experiments: + historical: + min: -10000.0 + max: 10000.0 + piControl: + min: -10000.0 + max: 10000.0 + ssp*: + min: -10000.0 + max: 10000.0 + wmo: + units: kg s-1 + default: + min: -1000000000000.0 + max: 1000000000000.0 + experiments: + historical: + min: -1000000000000.0 + max: 1000000000000.0 + piControl: + min: -1000000000000.0 + max: 1000000000000.0 + ssp*: + min: -1000000000000.0 + max: 1000000000000.0 + wo: + units: m s-1 + default: + min: -500.0 + max: 500.0 + experiments: + historical: + min: -500.0 + max: 500.0 + piControl: + min: -500.0 + max: 500.0 + ssp*: + min: -500.0 + max: 500.0 + zfull: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + zg: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + zg500: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + zooc: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + zoocos: + units: mol m-3 + default: + min: 0.0 + max: 100000.0 + experiments: + historical: + min: 0.0 + max: 100000.0 + piControl: + min: 0.0 + max: 100000.0 + ssp*: + min: 0.0 + max: 100000.0 + zos: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 + zossq: + units: m2 + default: + min: 0.0 + max: 1000000000000000.0 + experiments: + historical: + min: 0.0 + max: 1000000000000000.0 + piControl: + min: 0.0 + max: 1000000000000000.0 + ssp*: + min: 0.0 + max: 1000000000000000.0 + zostoga: + units: m + default: + min: -12000.0 + max: 12000.0 + experiments: + historical: + min: -12000.0 + max: 12000.0 + piControl: + min: -12000.0 + max: 12000.0 + ssp*: + min: -12000.0 + max: 12000.0 diff --git a/tests/unit/test_cmip7_qc.py b/tests/unit/test_cmip7_qc.py index 00a29506..43ac755f 100644 --- a/tests/unit/test_cmip7_qc.py +++ b/tests/unit/test_cmip7_qc.py @@ -7,7 +7,11 @@ from access_moppy.base import CMORiser from access_moppy.qc import validate_cmip7_output -from access_moppy.qc.cmip7 import main as qc_main +from access_moppy.qc.cmip7 import ( + _load_esm16_mapping_variables, + _load_rules, + main as qc_main, +) def _write_cmip7_output( @@ -15,24 +19,30 @@ def _write_cmip7_output( *, values, experiment_id: str, + variable_id: str = "tas", + source_id: str = "ACCESS-ESM1-6", + branded_variable: str | None = None, + units: str = "K", filename: str = "cmip7_output.nc", ) -> Path: path = tmp_path / filename + data_var_name = branded_variable or variable_id ds = xr.Dataset( { - "tas": xr.DataArray( + data_var_name: xr.DataArray( np.asarray(values, dtype=float), dims=["time"], - attrs={"units": "K"}, + attrs={"units": units}, ) }, coords={"time": xr.DataArray(np.arange(len(values)), dims=["time"])}, attrs={ "mip_era": "CMIP7", - "variable_id": "tas", - "branded_variable": "tas", + "variable_id": variable_id, + "branded_variable": data_var_name, "experiment_id": experiment_id, - "units": "K", + "source_id": source_id, + "units": units, }, ) ds.to_netcdf(path) @@ -140,3 +150,81 @@ def test_qc_cli_main_returns_one_when_any_file_fails(tmp_path, capsys): assert code == 1 assert "PASS" in captured.out assert "FAIL" in captured.out + + +@pytest.mark.unit +def test_esm16_mapping_inventory_is_loaded_for_all_variables(): + mapping = _load_esm16_mapping_variables() + # ACCESS-ESM1-6 mapping currently defines 293 variables across realms. + assert len(mapping) >= 293 + + +@pytest.mark.unit +def test_all_esm16_mapped_variables_have_explicit_qc_rule_entries(): + mapped = set(_load_esm16_mapping_variables()) + configured = set(_load_rules()) + assert mapped.issubset(configured) + + +@pytest.mark.unit +def test_validate_cmip7_output_enforces_positive_up_from_esm16_mapping(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[-0.1, 0.2], + experiment_id="historical", + variable_id="evspsblsoi", + units="kg m-2 s-1", + filename="evspsblsoi_negative.nc", + ) + + with pytest.raises(ValueError, match="positive: up"): + validate_cmip7_output(path) + + +@pytest.mark.unit +def test_validate_cmip7_output_applies_variable_rules_for_other_sources(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[-0.1, 0.2], + experiment_id="historical", + variable_id="evspsblsoi", + source_id="ACCESS-CM3", + units="kg m-2 s-1", + filename="other_source.nc", + ) + + with pytest.raises(ValueError, match="outside allowed range"): + validate_cmip7_output(path) + + +@pytest.mark.unit +def test_validate_cmip7_output_applies_unit_envelope_for_mapped_variable(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[2.5e7, 2.6e7], + experiment_id="historical", + variable_id="psl", + units="Pa", + filename="psl_out_of_range.nc", + ) + + with pytest.raises( + ValueError, + match="psl.*outside allowed range", + ): + validate_cmip7_output(path) + + +@pytest.mark.unit +def test_validate_cmip7_output_validates_units_against_mapping(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[100000.0, 100500.0], + experiment_id="historical", + variable_id="psl", + units="hPa", + filename="psl_bad_units.nc", + ) + + with pytest.raises(ValueError, match="expected units .*ACCESS-ESM1-6 mapping"): + validate_cmip7_output(path) From 74a31ce506b289b96399ac1eb25a4d66dc930c0f Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Tue, 23 Jun 2026 18:11:03 +1000 Subject: [PATCH 03/21] Tune QC limits for tasmin and tasmax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../resources/qc/cmip7_ranges.yml | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/access_moppy/resources/qc/cmip7_ranges.yml b/src/access_moppy/resources/qc/cmip7_ranges.yml index cb66af37..cb123460 100644 --- a/src/access_moppy/resources/qc/cmip7_ranges.yml +++ b/src/access_moppy/resources/qc/cmip7_ranges.yml @@ -3725,33 +3725,33 @@ variables: tasmax: units: K default: - min: 100.0 - max: 400.0 + min: 180.0 + max: 340.0 experiments: historical: - min: 100.0 - max: 400.0 + min: 180.0 + max: 340.0 piControl: - min: 100.0 - max: 400.0 + min: 180.0 + max: 335.0 ssp*: - min: 100.0 - max: 400.0 + min: 180.0 + max: 345.0 tasmin: units: K default: - min: 100.0 - max: 400.0 + min: 175.0 + max: 325.0 experiments: historical: - min: 100.0 - max: 400.0 + min: 175.0 + max: 325.0 piControl: - min: 100.0 - max: 400.0 + min: 175.0 + max: 320.0 ssp*: - min: 100.0 - max: 400.0 + min: 175.0 + max: 330.0 tauu: units: Pa default: From eaf98b97984d26965dc272ac7b6c756ad79af0b9 Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Tue, 23 Jun 2026 18:12:38 +1000 Subject: [PATCH 04/21] 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. --- src/access_moppy/qc/cmip7.py | 5 +++-- tests/unit/test_cmip7_qc.py | 36 +++++++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/access_moppy/qc/cmip7.py b/src/access_moppy/qc/cmip7.py index 15572814..51886ac3 100644 --- a/src/access_moppy/qc/cmip7.py +++ b/src/access_moppy/qc/cmip7.py @@ -182,8 +182,9 @@ def _select_output_variable(ds: xr.Dataset, attrs: dict[str, Any]) -> str: if isinstance(candidate, str) and candidate in ds.data_vars: return candidate - if len(ds.data_vars) == 1: - return next(iter(ds.data_vars)) + non_bounds = [v for v in ds.data_vars if not str(v).endswith("_bnds")] + if len(non_bounds) == 1: + return non_bounds[0] available = ", ".join(sorted(ds.data_vars)) raise ValueError( diff --git a/tests/unit/test_cmip7_qc.py b/tests/unit/test_cmip7_qc.py index 43ac755f..90eac068 100644 --- a/tests/unit/test_cmip7_qc.py +++ b/tests/unit/test_cmip7_qc.py @@ -10,6 +10,8 @@ from access_moppy.qc.cmip7 import ( _load_esm16_mapping_variables, _load_rules, +) +from access_moppy.qc.cmip7 import ( main as qc_main, ) @@ -103,9 +105,10 @@ def test_cmoriser_write_runs_cmip7_qc_after_repack(tmp_path): ) cmoriser.ds = ds - with patch.object(cmoriser, "_repack_cmip7_output") as repack_mock, patch( - "access_moppy.base.validate_cmip7_output" - ) as qc_mock: + with ( + patch.object(cmoriser, "_repack_cmip7_output") as repack_mock, + patch("access_moppy.base.validate_cmip7_output") as qc_mock, + ): cmoriser.write() repack_mock.assert_called_once() @@ -228,3 +231,30 @@ def test_validate_cmip7_output_validates_units_against_mapping(tmp_path): with pytest.raises(ValueError, match="expected units .*ACCESS-ESM1-6 mapping"): validate_cmip7_output(path) + + +@pytest.mark.unit +def test_select_output_variable_ignores_bounds_variables(tmp_path): + """_bnds variables should not prevent identifying the main variable.""" + path = tmp_path / "tasmax_with_bnds.nc" + ds = xr.Dataset( + { + "tasmax": xr.DataArray( + np.array([310.0, 312.0]), + dims=["time"], + attrs={"units": "K"}, + ), + "lat_bnds": xr.DataArray(np.zeros((2, 2)), dims=["lat", "bnds"]), + "lon_bnds": xr.DataArray(np.zeros((2, 2)), dims=["lon", "bnds"]), + "time_bnds": xr.DataArray(np.zeros((2, 2)), dims=["time", "bnds"]), + }, + attrs={ + "mip_era": "CMIP7", + "variable_id": "tasmax", + "experiment_id": "historical", + "source_id": "ACCESS-ESM1-6", + }, + ) + ds.to_netcdf(path) + # Should not raise — tasmax must be identified despite the *_bnds variables + validate_cmip7_output(path) From 4a7acae14c7f43622b1c7983a0627387931390c2 Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Tue, 23 Jun 2026 18:14:23 +1000 Subject: [PATCH 05/21] Add a blank line before the build_batch_report function for improved readability --- src/access_moppy/batch_report.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/access_moppy/batch_report.py b/src/access_moppy/batch_report.py index fcdaac78..2b721123 100644 --- a/src/access_moppy/batch_report.py +++ b/src/access_moppy/batch_report.py @@ -268,6 +268,7 @@ def _run_qc_on_output_folder(output_folder: Path) -> dict[str, Any] | None: return results if results["total"] > 0 else None + def build_batch_report( db_path: str | Path, *, From cd83154fc0b09ea77f7840f83f3f43b86abcdace Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Tue, 23 Jun 2026 18:21:29 +1000 Subject: [PATCH 06/21] Update rangess --- .../resources/qc/cmip7_ranges.yml | 3732 +++++++++-------- 1 file changed, 1867 insertions(+), 1865 deletions(-) diff --git a/src/access_moppy/resources/qc/cmip7_ranges.yml b/src/access_moppy/resources/qc/cmip7_ranges.yml index cb123460..aa00ae28 100644 --- a/src/access_moppy/resources/qc/cmip7_ranges.yml +++ b/src/access_moppy/resources/qc/cmip7_ranges.yml @@ -1,5 +1,7 @@ # Per-variable QC rules for ACCESS-ESM1-6 mapped variables. -# Each variable has units, default min/max, and optional experiment-specific overrides. +# Generated from uploaded placeholder ranges. +# These are broad physical sanity bounds for CMIP7-style QC, not tight climatological thresholds. +# Intent: catch unit/sign/conversion mistakes while allowing plausible model extremes. variables: agessc: @@ -21,47 +23,47 @@ variables: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 10.0 experiments: historical: min: 0.0 - max: 100000.0 + max: 10.0 piControl: min: 0.0 - max: 100000.0 + max: 10.0 ssp*: min: 0.0 - max: 100000.0 + max: 10.0 areacella: units: m2 default: min: 0.0 - max: 1000000000000000.0 + max: 100000000000.0 experiments: historical: min: 0.0 - max: 1000000000000000.0 + max: 100000000000.0 piControl: min: 0.0 - max: 1000000000000000.0 + max: 100000000000.0 ssp*: min: 0.0 - max: 1000000000000000.0 + max: 100000000000.0 areacello: units: m2 default: min: 0.0 - max: 1000000000000000.0 + max: 100000000000.0 experiments: historical: min: 0.0 - max: 1000000000000000.0 + max: 100000000000.0 piControl: min: 0.0 - max: 1000000000000000.0 + max: 100000000000.0 ssp*: min: 0.0 - max: 1000000000000000.0 + max: 100000000000.0 baresoilFrac: units: '%' default: @@ -80,33 +82,33 @@ variables: bigthetao: units: degC default: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 experiments: historical: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 piControl: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 ssp*: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 bigthetaoga: units: degC default: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 experiments: historical: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 piControl: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 ssp*: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 c3PftFrac: units: '%' default: @@ -140,213 +142,213 @@ variables: cLand: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 cLeaf: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 cLitter: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 cProduct: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 cRoot: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 cSoil: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 cSoilFast: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 cSoilMedium: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 cSoilSlow: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 cVeg: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 calcos: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 10.0 experiments: historical: min: 0.0 - max: 100000.0 + max: 10.0 piControl: min: 0.0 - max: 100000.0 + max: 10.0 ssp*: min: 0.0 - max: 100000.0 + max: 10.0 chl: units: kg m-3 default: min: 0.0 - max: 200000.0 + max: 0.01 experiments: historical: min: 0.0 - max: 200000.0 + max: 0.01 piControl: min: 0.0 - max: 200000.0 + max: 0.01 ssp*: min: 0.0 - max: 200000.0 + max: 0.01 chlos: units: kg m-3 default: min: 0.0 - max: 200000.0 + max: 0.01 experiments: historical: min: 0.0 - max: 200000.0 + max: 0.01 piControl: min: 0.0 - max: 200000.0 + max: 0.01 ssp*: min: 0.0 - max: 200000.0 + max: 0.01 ci: units: '1' default: min: 0.0 - max: 1000000.0 + max: 1.0 experiments: historical: min: 0.0 - max: 1000000.0 + max: 1.0 piControl: min: 0.0 - max: 1000000.0 + max: 1.0 ssp*: min: 0.0 - max: 1000000.0 + max: 1.0 cl: units: '%' default: @@ -366,32 +368,32 @@ variables: units: kg kg-1 default: min: 0.0 - max: 1.0 + max: 0.02 experiments: historical: min: 0.0 - max: 1.0 + max: 0.02 piControl: min: 0.0 - max: 1.0 + max: 0.02 ssp*: min: 0.0 - max: 1.0 + max: 0.02 clivi: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 clt: units: '%' default: @@ -411,47 +413,47 @@ variables: units: kg kg-1 default: min: 0.0 - max: 1.0 + max: 0.02 experiments: historical: min: 0.0 - max: 1.0 + max: 0.02 piControl: min: 0.0 - max: 1.0 + max: 0.02 ssp*: min: 0.0 - max: 1.0 + max: 0.02 clwvi: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 co23D: units: kg kg-1 default: min: 0.0 - max: 1.0 + max: 0.02 experiments: historical: min: 0.0 - max: 1.0 + max: 0.02 piControl: min: 0.0 - max: 1.0 + max: 0.02 ssp*: min: 0.0 - max: 1.0 + max: 0.02 cropFrac: units: '%' default: @@ -485,438 +487,438 @@ variables: deptho: units: m default: - min: -12000.0 + min: 0.0 max: 12000.0 experiments: historical: - min: -12000.0 + min: 0.0 max: 12000.0 piControl: - min: -12000.0 + min: 0.0 max: 12000.0 ssp*: - min: -12000.0 + min: 0.0 max: 12000.0 detoc: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 0.1 experiments: historical: min: 0.0 - max: 100000.0 + max: 0.1 piControl: min: 0.0 - max: 100000.0 + max: 0.1 ssp*: min: 0.0 - max: 100000.0 + max: 0.1 dfe: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 1.0e-06 experiments: historical: min: 0.0 - max: 100000.0 + max: 1.0e-06 piControl: min: 0.0 - max: 100000.0 + max: 1.0e-06 ssp*: min: 0.0 - max: 100000.0 + max: 1.0e-06 dfeos: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 1.0e-06 experiments: historical: min: 0.0 - max: 100000.0 + max: 1.0e-06 piControl: min: 0.0 - max: 100000.0 + max: 1.0e-06 ssp*: min: 0.0 - max: 100000.0 + max: 1.0e-06 dissic: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 0.005 experiments: historical: min: 0.0 - max: 100000.0 + max: 0.005 piControl: min: 0.0 - max: 100000.0 + max: 0.005 ssp*: min: 0.0 - max: 100000.0 + max: 0.005 dissicos: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 0.005 experiments: historical: min: 0.0 - max: 100000.0 + max: 0.005 piControl: min: 0.0 - max: 100000.0 + max: 0.005 ssp*: min: 0.0 - max: 100000.0 + max: 0.005 epc100: units: mol m-2 s-1 default: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 experiments: historical: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 piControl: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 ssp*: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 evs: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 piControl: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 evspsbl: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 piControl: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 evspsblsoi: units: kg m-2 s-1 default: - min: 0.0 - max: 10000.0 + min: -0.05 + max: 0.05 experiments: historical: - min: 0.0 - max: 10000.0 + min: -0.05 + max: 0.05 piControl: - min: 0.0 - max: 10000.0 + min: -0.05 + max: 0.05 ssp*: - min: 0.0 - max: 10000.0 + min: -0.05 + max: 0.05 evspsblveg: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 piControl: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 expc: units: mol m-2 s-1 default: - min: -1000.0 - max: 0.0 + min: -0.001 + max: 0.001 experiments: historical: - min: -1000.0 - max: 0.0 + min: -0.001 + max: 0.001 piControl: - min: -1000.0 - max: 0.0 + min: -0.001 + max: 0.001 ssp*: - min: -1000.0 - max: 0.0 + min: -0.001 + max: 0.001 expcalc: units: mol m-2 s-1 default: - min: -1000.0 - max: 0.0 + min: -0.001 + max: 0.001 experiments: historical: - min: -1000.0 - max: 0.0 + min: -0.001 + max: 0.001 piControl: - min: -1000.0 - max: 0.0 + min: -0.001 + max: 0.001 ssp*: - min: -1000.0 - max: 0.0 + min: -0.001 + max: 0.001 fBNF: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 piControl: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 fDeforestToProduct: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 piControl: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 fNdep: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 piControl: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 fNgas: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 piControl: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 fNleach: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 piControl: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 fNloss: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 piControl: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 fNnetmin: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 piControl: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 fNup: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 piControl: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 fProductDecomp: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 piControl: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 fgco2: units: kg m-2 s-1 default: - min: -10000.0 - max: 0.0 + min: -1.0e-06 + max: 1.0e-06 experiments: historical: - min: -10000.0 - max: 0.0 + min: -1.0e-06 + max: 1.0e-06 piControl: - min: -10000.0 - max: 0.0 + min: -1.0e-06 + max: 1.0e-06 ssp*: - min: -10000.0 - max: 0.0 + min: -1.0e-06 + max: 1.0e-06 ficeberg2d: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 friver: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 fsfe: units: mol m-2 s-1 default: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 experiments: historical: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 piControl: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 ssp*: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 fsitherm: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 fsn: units: mol m-2 s-1 default: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 experiments: historical: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 piControl: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 ssp*: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 gpp: units: kg m-2 s-1 default: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 piControl: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 ssp*: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 grassFrac: units: '%' default: @@ -965,213 +967,213 @@ variables: graz: units: mol m-3 s-1 default: - min: -1000.0 - max: 1000.0 + min: -1.0e-06 + max: 1.0e-06 experiments: historical: - min: -1000.0 - max: 1000.0 + min: -1.0e-06 + max: 1.0e-06 piControl: - min: -1000.0 - max: 1000.0 + min: -1.0e-06 + max: 1.0e-06 ssp*: - min: -1000.0 - max: 1000.0 + min: -1.0e-06 + max: 1.0e-06 hfds: units: W m-2 default: - min: -100000.0 - max: 0.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 0.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 0.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 0.0 + min: -5000.0 + max: 5000.0 hfevapds: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 hfgeou: units: W m-2 default: min: 0.0 - max: 100000.0 + max: 10.0 experiments: historical: min: 0.0 - max: 100000.0 + max: 10.0 piControl: min: 0.0 - max: 100000.0 + max: 10.0 ssp*: min: 0.0 - max: 100000.0 + max: 10.0 hfibthermds2d: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 hfls: units: W m-2 default: - min: 0.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: 0.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: 0.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: 0.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 hflso: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 hfrainds: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 hfrunoffds2d: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 hfsifrazil: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 hfsifrazil2d: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 hfsnthermds2d: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 hfss: units: W m-2 default: - min: 0.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: 0.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: 0.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: 0.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 hfsso: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 hur: units: '%' default: @@ -1236,137 +1238,137 @@ variables: units: '1' default: min: 0.0 - max: 1000000.0 + max: 0.08 experiments: historical: min: 0.0 - max: 1000000.0 + max: 0.08 piControl: min: 0.0 - max: 1000000.0 + max: 0.08 ssp*: min: 0.0 - max: 1000000.0 + max: 0.08 huss: units: '1' default: min: 0.0 - max: 1000000.0 + max: 0.08 experiments: historical: min: 0.0 - max: 1000000.0 + max: 0.08 piControl: min: 0.0 - max: 1000000.0 + max: 0.08 ssp*: min: 0.0 - max: 1000000.0 + max: 0.08 intdic: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100000.0 intpoc: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 1000.0 intpp: units: mol m-2 s-1 default: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 experiments: historical: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 piControl: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 ssp*: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 intppnitrate: units: mol m-2 s-1 default: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 experiments: historical: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 piControl: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 ssp*: - min: -1000.0 - max: 1000.0 + min: -0.001 + max: 0.001 intuaw: units: kg m-1 s-1 default: - min: -1000000.0 - max: 1000000.0 + min: -2000000.0 + max: 2000000.0 experiments: historical: - min: -1000000.0 - max: 1000000.0 + min: -2000000.0 + max: 2000000.0 piControl: - min: -1000000.0 - max: 1000000.0 + min: -2000000.0 + max: 2000000.0 ssp*: - min: -1000000.0 - max: 1000000.0 + min: -2000000.0 + max: 2000000.0 intvaw: units: kg m-1 s-1 default: - min: -1000000.0 - max: 1000000.0 + min: -2000000.0 + max: 2000000.0 experiments: historical: - min: -1000000.0 - max: 1000000.0 + min: -2000000.0 + max: 2000000.0 piControl: - min: -1000000.0 - max: 1000000.0 + min: -2000000.0 + max: 2000000.0 ssp*: - min: -1000000.0 - max: 1000000.0 + min: -2000000.0 + max: 2000000.0 lai: units: '1' default: min: 0.0 - max: 1000000.0 + max: 20.0 experiments: historical: min: 0.0 - max: 1000000.0 + max: 20.0 piControl: min: 0.0 - max: 1000000.0 + max: 20.0 ssp*: min: 0.0 - max: 1000000.0 + max: 20.0 landCoverFrac: units: '%' default: @@ -1385,918 +1387,918 @@ variables: masscello: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 12000000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 12000000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 12000000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 12000000.0 mc: units: kg m-2 s-1 default: - min: 0.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: 0.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: 0.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: 0.0 - max: 10000.0 + min: -0.1 + max: 0.1 mlotst: units: m default: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 7000.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 7000.0 piControl: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 7000.0 ssp*: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 7000.0 mlotstsq: units: m2 default: min: 0.0 - max: 1000000000000000.0 + max: 50000000.0 experiments: historical: min: 0.0 - max: 1000000000000000.0 + max: 50000000.0 piControl: min: 0.0 - max: 1000000000000000.0 + max: 50000000.0 ssp*: min: 0.0 - max: 1000000000000000.0 + max: 50000000.0 mrfsli: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 mrfso: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 mrro: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 mrrob: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 mrros: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 mrsfl: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 mrsll: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 mrso: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 mrsofc: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 mrsol: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 mrsos: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 10000.0 msftbarot: units: kg s-1 default: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 experiments: historical: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 piControl: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 ssp*: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 msftmrho: units: kg s-1 default: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 experiments: historical: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 piControl: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 ssp*: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 msftmz: units: kg s-1 default: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 experiments: historical: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 piControl: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 ssp*: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 msftyrho: units: kg s-1 default: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 experiments: historical: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 piControl: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 ssp*: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 msftyz: units: kg s-1 default: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 experiments: historical: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 piControl: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 ssp*: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 nLand: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 nLitter: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 nMineral: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 nProduct: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 nSoil: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 nVeg: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 100.0 nbp: units: kg m-2 s-1 default: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 piControl: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 ssp*: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 nep: units: kg m-2 s-1 default: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 piControl: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 ssp*: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 no3: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 0.1 experiments: historical: min: 0.0 - max: 100000.0 + max: 0.1 piControl: min: 0.0 - max: 100000.0 + max: 0.1 ssp*: min: 0.0 - max: 100000.0 + max: 0.1 no3os: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 0.1 experiments: historical: min: 0.0 - max: 100000.0 + max: 0.1 piControl: min: 0.0 - max: 100000.0 + max: 0.1 ssp*: min: 0.0 - max: 100000.0 + max: 0.1 npp: units: kg m-2 s-1 default: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 piControl: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 ssp*: - min: -10000.0 - max: 0.0 + min: -0.0001 + max: 0.0001 o2: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 0.7 experiments: historical: min: 0.0 - max: 100000.0 + max: 0.7 piControl: min: 0.0 - max: 100000.0 + max: 0.7 ssp*: min: 0.0 - max: 100000.0 + max: 0.7 obvfsq: units: s-2 default: - min: -1000000.0 - max: 1000000.0 + min: -1.0e-06 + max: 0.001 experiments: historical: - min: -1000000.0 - max: 1000000.0 + min: -1.0e-06 + max: 0.001 piControl: - min: -1000000.0 - max: 1000000.0 + min: -1.0e-06 + max: 0.001 ssp*: - min: -1000000.0 - max: 1000000.0 + min: -1.0e-06 + max: 0.001 ocontempdiff: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ocontempmint: units: degC kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: -100000.0 + max: 500000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: -100000.0 + max: 500000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: -100000.0 + max: 500000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: -100000.0 + max: 500000.0 ocontemppsmadvect: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ocontemptend: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 od440aer: units: '1' default: min: 0.0 - max: 1000000.0 + max: 10.0 experiments: historical: min: 0.0 - max: 1000000.0 + max: 10.0 piControl: min: 0.0 - max: 1000000.0 + max: 10.0 ssp*: min: 0.0 - max: 1000000.0 + max: 10.0 od550aer: units: '1' default: min: 0.0 - max: 1000000.0 + max: 10.0 experiments: historical: min: 0.0 - max: 1000000.0 + max: 10.0 piControl: min: 0.0 - max: 1000000.0 + max: 10.0 ssp*: min: 0.0 - max: 1000000.0 + max: 10.0 opottempmint: units: degC kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: -100000.0 + max: 500000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: -100000.0 + max: 500000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: -100000.0 + max: 500000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: -100000.0 + max: 500000.0 orog: units: m default: - min: -12000.0 - max: 12000.0 + min: -500.0 + max: 9000.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: -500.0 + max: 9000.0 piControl: - min: -12000.0 - max: 12000.0 + min: -500.0 + max: 9000.0 ssp*: - min: -12000.0 - max: 12000.0 + min: -500.0 + max: 9000.0 osaltdiff: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 osaltpsmadvect: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 osalttend: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 pbo: units: Pa default: min: 0.0 - max: 20000000.0 + max: 150000000.0 experiments: historical: min: 0.0 - max: 20000000.0 + max: 150000000.0 piControl: min: 0.0 - max: 20000000.0 + max: 150000000.0 ssp*: min: 0.0 - max: 20000000.0 + max: 150000000.0 pfull: units: Pa default: min: 0.0 - max: 20000000.0 + max: 120000.0 experiments: historical: min: 0.0 - max: 20000000.0 + max: 120000.0 piControl: min: 0.0 - max: 20000000.0 + max: 120000.0 ssp*: min: 0.0 - max: 20000000.0 + max: 120000.0 ph: units: '1' default: - min: 0.0 - max: 1000000.0 + min: 6.0 + max: 10.0 experiments: historical: - min: 0.0 - max: 1000000.0 + min: 6.0 + max: 10.0 piControl: - min: 0.0 - max: 1000000.0 + min: 6.0 + max: 10.0 ssp*: - min: 0.0 - max: 1000000.0 + min: 6.0 + max: 10.0 phalf: units: Pa default: min: 0.0 - max: 20000000.0 + max: 120000.0 experiments: historical: min: 0.0 - max: 20000000.0 + max: 120000.0 piControl: min: 0.0 - max: 20000000.0 + max: 120000.0 ssp*: min: 0.0 - max: 20000000.0 + max: 120000.0 phos: units: '1' default: - min: 0.0 - max: 1000000.0 + min: 6.0 + max: 10.0 experiments: historical: - min: 0.0 - max: 1000000.0 + min: 6.0 + max: 10.0 piControl: - min: 0.0 - max: 1000000.0 + min: 6.0 + max: 10.0 ssp*: - min: 0.0 - max: 1000000.0 + min: 6.0 + max: 10.0 phycos: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 0.1 experiments: historical: min: 0.0 - max: 100000.0 + max: 0.1 piControl: min: 0.0 - max: 100000.0 + max: 0.1 ssp*: min: 0.0 - max: 100000.0 + max: 0.1 po4: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 0.01 experiments: historical: min: 0.0 - max: 100000.0 + max: 0.01 piControl: min: 0.0 - max: 100000.0 + max: 0.01 ssp*: min: 0.0 - max: 100000.0 + max: 0.01 pr: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 prc: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 prrc: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 prsn: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 prsnc: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 prw: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 150.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 150.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 150.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 150.0 ps: units: Pa default: - min: 0.0 - max: 20000000.0 + min: 30000.0 + max: 120000.0 experiments: historical: - min: 0.0 - max: 20000000.0 + min: 30000.0 + max: 120000.0 piControl: - min: 0.0 - max: 20000000.0 + min: 30000.0 + max: 120000.0 ssp*: - min: 0.0 - max: 20000000.0 + min: 30000.0 + max: 120000.0 psl: units: Pa default: - min: 0.0 - max: 20000000.0 + min: 80000.0 + max: 110000.0 experiments: historical: - min: 0.0 - max: 20000000.0 + min: 80000.0 + max: 110000.0 piControl: - min: 0.0 - max: 20000000.0 + min: 80000.0 + max: 110000.0 ssp*: - min: 0.0 - max: 20000000.0 + min: 80000.0 + max: 110000.0 pso: units: Pa default: min: 0.0 - max: 20000000.0 + max: 150000000.0 experiments: historical: min: 0.0 - max: 20000000.0 + max: 150000000.0 piControl: min: 0.0 - max: 20000000.0 + max: 150000000.0 ssp*: min: 0.0 - max: 20000000.0 + max: 150000000.0 ra: units: kg m-2 s-1 default: - min: 0.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: 0.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 piControl: - min: 0.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 ssp*: - min: 0.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 residualFrac: units: '%' default: @@ -2315,408 +2317,408 @@ variables: rh: units: kg m-2 s-1 default: - min: 0.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 experiments: historical: - min: 0.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 piControl: - min: 0.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 ssp*: - min: 0.0 - max: 10000.0 + min: -0.0001 + max: 0.0001 rlds: units: W m-2 default: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 piControl: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 ssp*: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 rldscs: units: W m-2 default: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 piControl: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 ssp*: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 rlntds: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 piControl: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 rls: units: W m-2 default: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 piControl: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 ssp*: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 rlus: units: W m-2 default: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 piControl: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 ssp*: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 rluscs: units: W m-2 default: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 piControl: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 ssp*: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 rlut: units: W m-2 default: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 piControl: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 ssp*: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 rlutcs: units: W m-2 default: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 piControl: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 ssp*: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 rootd: units: m default: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 20.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 20.0 piControl: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 20.0 ssp*: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 20.0 rsdo: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 piControl: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 rsdoabsorb: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 piControl: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 rsds: units: W m-2 default: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 piControl: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 ssp*: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 rsdscs: units: W m-2 default: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 piControl: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 ssp*: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 rsdt: units: W m-2 default: - min: -100000.0 - max: 0.0 + min: 0.0 + max: 1500.0 experiments: historical: - min: -100000.0 - max: 0.0 + min: 0.0 + max: 1500.0 piControl: - min: -100000.0 - max: 0.0 + min: 0.0 + max: 1500.0 ssp*: - min: -100000.0 - max: 0.0 + min: 0.0 + max: 1500.0 rsntds: units: W m-2 default: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 piControl: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -100.0 + max: 1500.0 rss: units: W m-2 default: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 piControl: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 ssp*: - min: -100000.0 - max: 0.0 + min: -100.0 + max: 1500.0 rsus: units: W m-2 default: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 piControl: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 ssp*: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 rsuscs: units: W m-2 default: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 piControl: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 ssp*: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 rsut: units: W m-2 default: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 piControl: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 ssp*: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 rsutcs: units: W m-2 default: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 experiments: historical: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 piControl: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 ssp*: - min: 0.0 - max: 100000.0 + min: -100.0 + max: 1500.0 rtmt: units: W m-2 default: - min: -100000.0 - max: 0.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 0.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 0.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 0.0 + min: -5000.0 + max: 5000.0 sbl: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 piControl: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.05 + max: 0.05 sci: units: '1' default: min: 0.0 - max: 1000000.0 + max: 1.0 experiments: historical: min: 0.0 - max: 1000000.0 + max: 1.0 piControl: min: 0.0 - max: 1000000.0 + max: 1.0 ssp*: min: 0.0 - max: 1000000.0 + max: 1.0 sfcWind: units: m s-1 default: - min: -500.0 - max: 500.0 + min: 0.0 + max: 150.0 experiments: historical: - min: -500.0 - max: 500.0 + min: 0.0 + max: 150.0 piControl: - min: -500.0 - max: 500.0 + min: 0.0 + max: 150.0 ssp*: - min: -500.0 - max: 500.0 + min: 0.0 + max: 150.0 sfcWindmax: units: m s-1 default: - min: -500.0 - max: 500.0 + min: 0.0 + max: 150.0 experiments: historical: - min: -500.0 - max: 500.0 + min: 0.0 + max: 150.0 piControl: - min: -500.0 - max: 500.0 + min: 0.0 + max: 150.0 ssp*: - min: -500.0 - max: 500.0 + min: 0.0 + max: 150.0 sfriver: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 sftgif: units: '%' default: @@ -2781,62 +2783,62 @@ variables: units: s default: min: 0.0 - max: 1000000000000.0 + max: 320000000.0 experiments: historical: min: 0.0 - max: 1000000000000.0 + max: 320000000.0 piControl: min: 0.0 - max: 1000000000000.0 + max: 320000000.0 ssp*: min: 0.0 - max: 1000000000000.0 + max: 320000000.0 siareaacrossline: units: m2 s-1 default: - min: -1000000.0 - max: 1000000.0 + min: -10000000.0 + max: 10000000.0 experiments: historical: - min: -1000000.0 - max: 1000000.0 + min: -10000000.0 + max: 10000000.0 piControl: - min: -1000000.0 - max: 1000000.0 + min: -10000000.0 + max: 10000000.0 ssp*: - min: -1000000.0 - max: 1000000.0 + min: -10000000.0 + max: 10000000.0 siarean: units: 1e6 km2 default: min: 0.0 - max: 1000.0 + max: 30.0 experiments: historical: min: 0.0 - max: 1000.0 + max: 30.0 piControl: min: 0.0 - max: 1000.0 + max: 30.0 ssp*: min: 0.0 - max: 1000.0 + max: 30.0 siareas: units: 1e6 km2 default: min: 0.0 - max: 1000.0 + max: 30.0 experiments: historical: min: 0.0 - max: 1000.0 + max: 30.0 piControl: min: 0.0 - max: 1000.0 + max: 30.0 ssp*: min: 0.0 - max: 1000.0 + max: 30.0 siconc: units: '%' default: @@ -2870,333 +2872,333 @@ variables: sidconcdyn: units: s-1 default: - min: -1000.0 - max: 1000.0 + min: -1.0e-05 + max: 1.0e-05 experiments: historical: - min: -1000.0 - max: 1000.0 + min: -1.0e-05 + max: 1.0e-05 piControl: - min: -1000.0 - max: 1000.0 + min: -1.0e-05 + max: 1.0e-05 ssp*: - min: -1000.0 - max: 1000.0 + min: -1.0e-05 + max: 1.0e-05 sidconcth: units: s-1 default: - min: -1000.0 - max: 1000.0 + min: -1.0e-05 + max: 1.0e-05 experiments: historical: - min: -1000.0 - max: 1000.0 + min: -1.0e-05 + max: 1.0e-05 piControl: - min: -1000.0 - max: 1000.0 + min: -1.0e-05 + max: 1.0e-05 ssp*: - min: -1000.0 - max: 1000.0 + min: -1.0e-05 + max: 1.0e-05 sidivvel: units: s-1 default: - min: -1000.0 - max: 1000.0 + min: -1.0e-05 + max: 1.0e-05 experiments: historical: - min: -1000.0 - max: 1000.0 + min: -1.0e-05 + max: 1.0e-05 piControl: - min: -1000.0 - max: 1000.0 + min: -1.0e-05 + max: 1.0e-05 ssp*: - min: -1000.0 - max: 1000.0 + min: -1.0e-05 + max: 1.0e-05 sidmassdyn: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 sidmassevapsubl: units: kg m-2 s-1 default: - min: 0.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: 0.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: 0.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: 0.0 - max: 10000.0 + min: -0.1 + max: 0.1 sidmassgrowthbot: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 sidmassgrowthsi: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 sidmassgrowthwat: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 sidmassmeltbot: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 sidmassmeltlat: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 sidmassmelttop: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 sidmassth: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 sidmasstranx: units: kg s-1 default: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 experiments: historical: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 piControl: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 ssp*: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 sidmasstrany: units: kg s-1 default: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 experiments: historical: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 piControl: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 ssp*: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 siextentn: units: 1e6 km2 default: min: 0.0 - max: 1000.0 + max: 30.0 experiments: historical: min: 0.0 - max: 1000.0 + max: 30.0 piControl: min: 0.0 - max: 1000.0 + max: 30.0 ssp*: min: 0.0 - max: 1000.0 + max: 30.0 siextents: units: 1e6 km2 default: min: 0.0 - max: 1000.0 + max: 30.0 experiments: historical: min: 0.0 - max: 1000.0 + max: 30.0 piControl: min: 0.0 - max: 1000.0 + max: 30.0 ssp*: min: 0.0 - max: 1000.0 + max: 30.0 sifb: units: m default: - min: -12000.0 - max: 12000.0 + min: -20.0 + max: 20.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: -20.0 + max: 20.0 piControl: - min: -12000.0 - max: 12000.0 + min: -20.0 + max: 20.0 ssp*: - min: -12000.0 - max: 12000.0 + min: -20.0 + max: 20.0 siflfwbot: units: kg m-2 s-1 default: - min: -10000.0 - max: 0.0 + min: 0.0 + max: 0.1 experiments: historical: - min: -10000.0 - max: 0.0 + min: 0.0 + max: 0.1 piControl: - min: -10000.0 - max: 0.0 + min: 0.0 + max: 0.1 ssp*: - min: -10000.0 - max: 0.0 + min: 0.0 + max: 0.1 siflswdtop: units: W m-2 default: - min: -100000.0 - max: 0.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: -100000.0 - max: 0.0 + min: -5000.0 + max: 5000.0 piControl: - min: -100000.0 - max: 0.0 + min: -5000.0 + max: 5000.0 ssp*: - min: -100000.0 - max: 0.0 + min: -5000.0 + max: 5000.0 siflswutop: units: W m-2 default: - min: 0.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 experiments: historical: - min: 0.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 piControl: - min: 0.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 ssp*: - min: 0.0 - max: 100000.0 + min: -5000.0 + max: 5000.0 simass: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 simassacrossline: units: kg s-1 default: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 experiments: historical: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 piControl: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 ssp*: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 sisnconc: units: '%' default: @@ -3215,333 +3217,333 @@ variables: sisndmassmelt: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 sisndmasssi: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 sisndmasssnf: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: -0.1 + max: 0.1 sisnhc: units: J m-2 default: - min: -1000000000000.0 - max: 1000000000000.0 + min: 0.0 + max: 1000000000.0 experiments: historical: - min: -1000000000000.0 - max: 1000000000000.0 + min: 0.0 + max: 1000000000.0 piControl: - min: -1000000000000.0 - max: 1000000000000.0 + min: 0.0 + max: 1000000000.0 ssp*: - min: -1000000000000.0 - max: 1000000000000.0 + min: 0.0 + max: 1000000000.0 sisnmassn: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 sisnmasss: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 sisnthick: units: m default: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 20.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 20.0 piControl: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 20.0 ssp*: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 20.0 sispeed: units: m s-1 default: - min: -500.0 - max: 500.0 + min: 0.0 + max: 150.0 experiments: historical: - min: -500.0 - max: 500.0 + min: 0.0 + max: 150.0 piControl: - min: -500.0 - max: 500.0 + min: 0.0 + max: 150.0 ssp*: - min: -500.0 - max: 500.0 + min: 0.0 + max: 150.0 sistrxdtop: units: N m-2 default: - min: -100000.0 - max: 0.0 + min: -10.0 + max: 10.0 experiments: historical: - min: -100000.0 - max: 0.0 + min: -10.0 + max: 10.0 piControl: - min: -100000.0 - max: 0.0 + min: -10.0 + max: 10.0 ssp*: - min: -100000.0 - max: 0.0 + min: -10.0 + max: 10.0 sistrxubot: units: N m-2 default: - min: 0.0 - max: 100000.0 + min: -10.0 + max: 10.0 experiments: historical: - min: 0.0 - max: 100000.0 + min: -10.0 + max: 10.0 piControl: - min: 0.0 - max: 100000.0 + min: -10.0 + max: 10.0 ssp*: - min: 0.0 - max: 100000.0 + min: -10.0 + max: 10.0 sistrydtop: units: N m-2 default: - min: -100000.0 - max: 0.0 + min: -10.0 + max: 10.0 experiments: historical: - min: -100000.0 - max: 0.0 + min: -10.0 + max: 10.0 piControl: - min: -100000.0 - max: 0.0 + min: -10.0 + max: 10.0 ssp*: - min: -100000.0 - max: 0.0 + min: -10.0 + max: 10.0 sistryubot: units: N m-2 default: - min: 0.0 - max: 100000.0 + min: -10.0 + max: 10.0 experiments: historical: - min: 0.0 - max: 100000.0 + min: -10.0 + max: 10.0 piControl: - min: 0.0 - max: 100000.0 + min: -10.0 + max: 10.0 ssp*: - min: 0.0 - max: 100000.0 + min: -10.0 + max: 10.0 sitempbot: units: K default: - min: 100.0 - max: 400.0 + min: 220.0 + max: 280.0 experiments: historical: - min: 100.0 - max: 400.0 + min: 220.0 + max: 280.0 piControl: - min: 100.0 - max: 400.0 + min: 220.0 + max: 280.0 ssp*: - min: 100.0 - max: 400.0 + min: 220.0 + max: 280.0 sitemptop: units: K default: - min: 100.0 - max: 400.0 + min: 220.0 + max: 280.0 experiments: historical: - min: 100.0 - max: 400.0 + min: 220.0 + max: 280.0 piControl: - min: 100.0 - max: 400.0 + min: 220.0 + max: 280.0 ssp*: - min: 100.0 - max: 400.0 + min: 220.0 + max: 280.0 sithick: units: m default: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 20.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 20.0 piControl: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 20.0 ssp*: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 20.0 sitimefrac: units: '1' default: min: 0.0 - max: 1000000.0 + max: 1.0 experiments: historical: min: 0.0 - max: 1000000.0 + max: 1.0 piControl: min: 0.0 - max: 1000000.0 + max: 1.0 ssp*: min: 0.0 - max: 1000000.0 + max: 1.0 siu: units: m s-1 default: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 experiments: historical: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 piControl: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 ssp*: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 siv: units: m s-1 default: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 experiments: historical: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 piControl: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 ssp*: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 sivol: units: m default: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 50.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 50.0 piControl: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 50.0 ssp*: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 50.0 sivoln: units: 1e3 km3 default: min: 0.0 - max: 1000000.0 + max: 100.0 experiments: historical: min: 0.0 - max: 1000000.0 + max: 100.0 piControl: min: 0.0 - max: 1000000.0 + max: 100.0 ssp*: min: 0.0 - max: 1000000.0 + max: 100.0 sivols: units: 1e3 km3 default: min: 0.0 - max: 1000000.0 + max: 100.0 experiments: historical: min: 0.0 - max: 1000000.0 + max: 100.0 piControl: min: 0.0 - max: 1000000.0 + max: 100.0 ssp*: min: 0.0 - max: 1000000.0 + max: 100.0 slthick: units: m default: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 100.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 100.0 piControl: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 100.0 ssp*: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 100.0 snc: units: '%' default: @@ -3560,378 +3562,378 @@ variables: snw: units: kg m-2 default: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 experiments: historical: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 piControl: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 ssp*: - min: -1000000000.0 - max: 1000000000.0 + min: 0.0 + max: 50000.0 so: units: '0.001' default: min: 0.0 - max: 1000.0 + max: 50.0 experiments: historical: min: 0.0 - max: 1000.0 + max: 50.0 piControl: min: 0.0 - max: 1000.0 + max: 50.0 ssp*: min: 0.0 - max: 1000.0 + max: 50.0 sob: units: '0.001' default: min: 0.0 - max: 1000.0 + max: 50.0 experiments: historical: min: 0.0 - max: 1000.0 + max: 50.0 piControl: min: 0.0 - max: 1000.0 + max: 50.0 ssp*: min: 0.0 - max: 1000.0 + max: 50.0 soga: units: '0.001' default: min: 0.0 - max: 1000.0 + max: 50.0 experiments: historical: min: 0.0 - max: 1000.0 + max: 50.0 piControl: min: 0.0 - max: 1000.0 + max: 50.0 ssp*: min: 0.0 - max: 1000.0 + max: 50.0 somint: units: g m-2 default: min: 0.0 - max: 1000000000000.0 + max: 500000.0 experiments: historical: min: 0.0 - max: 1000000000000.0 + max: 500000.0 piControl: min: 0.0 - max: 1000000000000.0 + max: 500000.0 ssp*: min: 0.0 - max: 1000000000000.0 + max: 500000.0 sos: units: '0.001' default: min: 0.0 - max: 1000.0 + max: 50.0 experiments: historical: min: 0.0 - max: 1000.0 + max: 50.0 piControl: min: 0.0 - max: 1000.0 + max: 50.0 ssp*: min: 0.0 - max: 1000.0 + max: 50.0 sosga: units: '0.001' default: min: 0.0 - max: 1000.0 + max: 50.0 experiments: historical: min: 0.0 - max: 1000.0 + max: 50.0 piControl: min: 0.0 - max: 1000.0 + max: 50.0 ssp*: min: 0.0 - max: 1000.0 + max: 50.0 spco2: units: Pa default: min: 0.0 - max: 20000000.0 + max: 300.0 experiments: historical: min: 0.0 - max: 20000000.0 + max: 300.0 piControl: min: 0.0 - max: 20000000.0 + max: 300.0 ssp*: min: 0.0 - max: 20000000.0 + max: 300.0 ta: units: K default: - min: 100.0 - max: 400.0 + min: 150.0 + max: 350.0 experiments: historical: - min: 100.0 - max: 400.0 + min: 150.0 + max: 350.0 piControl: - min: 100.0 - max: 400.0 + min: 150.0 + max: 350.0 ssp*: - min: 100.0 - max: 400.0 + min: 150.0 + max: 350.0 talk: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 0.005 experiments: historical: min: 0.0 - max: 100000.0 + max: 0.005 piControl: min: 0.0 - max: 100000.0 + max: 0.005 ssp*: min: 0.0 - max: 100000.0 + max: 0.005 tas: units: K default: min: 180.0 - max: 330.0 + max: 340.0 experiments: historical: min: 180.0 - max: 330.0 + max: 340.0 piControl: min: 180.0 - max: 325.0 + max: 340.0 ssp*: min: 180.0 - max: 335.0 + max: 340.0 tasmax: units: K default: min: 180.0 - max: 340.0 + max: 350.0 experiments: historical: min: 180.0 - max: 340.0 + max: 350.0 piControl: min: 180.0 - max: 335.0 + max: 350.0 ssp*: min: 180.0 - max: 345.0 + max: 350.0 tasmin: units: K default: - min: 175.0 - max: 325.0 + min: 150.0 + max: 340.0 experiments: historical: - min: 175.0 - max: 325.0 + min: 150.0 + max: 340.0 piControl: - min: 175.0 - max: 320.0 + min: 150.0 + max: 340.0 ssp*: - min: 175.0 - max: 330.0 + min: 150.0 + max: 340.0 tauu: units: Pa default: - min: 0.0 - max: 0.0 + min: -10.0 + max: 10.0 experiments: historical: - min: 0.0 - max: 0.0 + min: -10.0 + max: 10.0 piControl: - min: 0.0 - max: 0.0 + min: -10.0 + max: 10.0 ssp*: - min: 0.0 - max: 0.0 + min: -10.0 + max: 10.0 tauuo: units: N m-2 default: - min: -100000.0 - max: 100000.0 + min: -10.0 + max: 10.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -10.0 + max: 10.0 piControl: - min: -100000.0 - max: 100000.0 + min: -10.0 + max: 10.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -10.0 + max: 10.0 tauv: units: Pa default: - min: 0.0 - max: 0.0 + min: -10.0 + max: 10.0 experiments: historical: - min: 0.0 - max: 0.0 + min: -10.0 + max: 10.0 piControl: - min: 0.0 - max: 0.0 + min: -10.0 + max: 10.0 ssp*: - min: 0.0 - max: 0.0 + min: -10.0 + max: 10.0 tauvo: units: N m-2 default: - min: -100000.0 - max: 100000.0 + min: -10.0 + max: 10.0 experiments: historical: - min: -100000.0 - max: 100000.0 + min: -10.0 + max: 10.0 piControl: - min: -100000.0 - max: 100000.0 + min: -10.0 + max: 10.0 ssp*: - min: -100000.0 - max: 100000.0 + min: -10.0 + max: 10.0 thetao: units: degC default: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 experiments: historical: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 piControl: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 ssp*: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 thetaoga: units: degC default: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 experiments: historical: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 piControl: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 ssp*: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 thkcello: units: m default: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 10000.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 10000.0 piControl: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 10000.0 ssp*: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 10000.0 tob: units: degC default: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 experiments: historical: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 piControl: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 ssp*: - min: -50.0 - max: 100.0 + min: -3.0 + max: 40.0 tos: units: degC default: - min: -50.0 - max: 100.0 + min: -3.0 + max: 45.0 experiments: historical: - min: -50.0 - max: 100.0 + min: -3.0 + max: 45.0 piControl: - min: -50.0 - max: 100.0 + min: -3.0 + max: 45.0 ssp*: - min: -50.0 - max: 100.0 + min: -3.0 + max: 45.0 tosga: units: degC default: - min: -50.0 - max: 100.0 + min: -3.0 + max: 45.0 experiments: historical: - min: -50.0 - max: 100.0 + min: -3.0 + max: 45.0 piControl: - min: -50.0 - max: 100.0 + min: -3.0 + max: 45.0 ssp*: - min: -50.0 - max: 100.0 + min: -3.0 + max: 45.0 tossq: units: degC2 default: min: 0.0 - max: 100000.0 + max: 2500.0 experiments: historical: min: 0.0 - max: 100000.0 + max: 2500.0 piControl: min: 0.0 - max: 100000.0 + max: 2500.0 ssp*: min: 0.0 - max: 100000.0 + max: 2500.0 tran: units: kg m-2 s-1 default: - min: 0.0 - max: 10000.0 + min: -0.05 + max: 0.05 experiments: historical: - min: 0.0 - max: 10000.0 + min: -0.05 + max: 0.05 piControl: - min: 0.0 - max: 10000.0 + min: -0.05 + max: 0.05 ssp*: - min: 0.0 - max: 10000.0 + min: -0.05 + max: 0.05 treeFrac: units: '%' default: @@ -4010,123 +4012,123 @@ variables: ts: units: K default: - min: 100.0 - max: 400.0 + min: 150.0 + max: 370.0 experiments: historical: - min: 100.0 - max: 400.0 + min: 150.0 + max: 370.0 piControl: - min: 100.0 - max: 400.0 + min: 150.0 + max: 370.0 ssp*: - min: 100.0 - max: 400.0 + min: 150.0 + max: 370.0 tsl: units: K default: - min: 100.0 - max: 400.0 + min: 150.0 + max: 350.0 experiments: historical: - min: 100.0 - max: 400.0 + min: 150.0 + max: 350.0 piControl: - min: 100.0 - max: 400.0 + min: 150.0 + max: 350.0 ssp*: - min: 100.0 - max: 400.0 + min: 150.0 + max: 350.0 ua: units: m s-1 default: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 experiments: historical: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 piControl: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 ssp*: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 uas: units: m s-1 default: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 experiments: historical: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 piControl: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 ssp*: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 umo: units: kg s-1 default: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 experiments: historical: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 piControl: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 ssp*: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 uo: units: m s-1 default: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 experiments: historical: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 piControl: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 ssp*: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 va: units: m s-1 default: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 experiments: historical: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 piControl: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 ssp*: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 vas: units: m s-1 default: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 experiments: historical: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 piControl: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 ssp*: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 vegFrac: units: '%' default: @@ -4145,78 +4147,78 @@ variables: vegHeight: units: m default: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 100.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 100.0 piControl: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 100.0 ssp*: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 100.0 vmo: units: kg s-1 default: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 experiments: historical: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 piControl: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 ssp*: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 vo: units: m s-1 default: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 experiments: historical: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 piControl: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 ssp*: - min: -500.0 - max: 500.0 + min: -150.0 + max: 150.0 volcello: units: m3 default: min: 0.0 - max: 1.0e+21 + max: 1.0e+16 experiments: historical: min: 0.0 - max: 1.0e+21 + max: 1.0e+16 piControl: min: 0.0 - max: 1.0e+21 + max: 1.0e+16 ssp*: min: 0.0 - max: 1.0e+21 + max: 1.0e+16 wap: units: Pa s-1 default: - min: -10000.0 - max: 10000.0 + min: -20.0 + max: 20.0 experiments: historical: - min: -10000.0 - max: 10000.0 + min: -20.0 + max: 20.0 piControl: - min: -10000.0 - max: 10000.0 + min: -20.0 + max: 20.0 ssp*: - min: -10000.0 - max: 10000.0 + min: -20.0 + max: 20.0 wetlandFrac: units: '%' default: @@ -4235,165 +4237,165 @@ variables: wfo: units: kg m-2 s-1 default: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 experiments: historical: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 piControl: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 ssp*: - min: -10000.0 - max: 10000.0 + min: 0.0 + max: 0.1 wmo: units: kg s-1 default: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 experiments: historical: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 piControl: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 ssp*: - min: -1000000000000.0 - max: 1000000000000.0 + min: -5000000000000.0 + max: 5000000000000.0 wo: units: m s-1 default: - min: -500.0 - max: 500.0 + min: -0.01 + max: 0.01 experiments: historical: - min: -500.0 - max: 500.0 + min: -0.01 + max: 0.01 piControl: - min: -500.0 - max: 500.0 + min: -0.01 + max: 0.01 ssp*: - min: -500.0 - max: 500.0 + min: -0.01 + max: 0.01 zfull: units: m default: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 80000.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 80000.0 piControl: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 80000.0 ssp*: - min: -12000.0 - max: 12000.0 + min: 0.0 + max: 80000.0 zg: units: m default: - min: -12000.0 - max: 12000.0 + min: -500.0 + max: 80000.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: -500.0 + max: 80000.0 piControl: - min: -12000.0 - max: 12000.0 + min: -500.0 + max: 80000.0 ssp*: - min: -12000.0 - max: 12000.0 + min: -500.0 + max: 80000.0 zg500: units: m default: - min: -12000.0 - max: 12000.0 + min: 4000.0 + max: 7000.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: 4000.0 + max: 7000.0 piControl: - min: -12000.0 - max: 12000.0 + min: 4000.0 + max: 7000.0 ssp*: - min: -12000.0 - max: 12000.0 + min: 4000.0 + max: 7000.0 zooc: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 0.1 experiments: historical: min: 0.0 - max: 100000.0 + max: 0.1 piControl: min: 0.0 - max: 100000.0 + max: 0.1 ssp*: min: 0.0 - max: 100000.0 + max: 0.1 zoocos: units: mol m-3 default: min: 0.0 - max: 100000.0 + max: 0.1 experiments: historical: min: 0.0 - max: 100000.0 + max: 0.1 piControl: min: 0.0 - max: 100000.0 + max: 0.1 ssp*: min: 0.0 - max: 100000.0 + max: 0.1 zos: units: m default: - min: -12000.0 - max: 12000.0 + min: -20.0 + max: 20.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: -20.0 + max: 20.0 piControl: - min: -12000.0 - max: 12000.0 + min: -20.0 + max: 20.0 ssp*: - min: -12000.0 - max: 12000.0 + min: -20.0 + max: 20.0 zossq: units: m2 default: min: 0.0 - max: 1000000000000000.0 + max: 400.0 experiments: historical: min: 0.0 - max: 1000000000000000.0 + max: 400.0 piControl: min: 0.0 - max: 1000000000000000.0 + max: 400.0 ssp*: min: 0.0 - max: 1000000000000000.0 + max: 400.0 zostoga: units: m default: - min: -12000.0 - max: 12000.0 + min: -20.0 + max: 20.0 experiments: historical: - min: -12000.0 - max: 12000.0 + min: -20.0 + max: 20.0 piControl: - min: -12000.0 - max: 12000.0 + min: -20.0 + max: 20.0 ssp*: - min: -12000.0 - max: 12000.0 + min: -20.0 + max: 20.0 \ No newline at end of file From fd3fd22dbc02516a03f3b9a905722afee8912653 Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Tue, 23 Jun 2026 18:21:55 +1000 Subject: [PATCH 07/21] pre-commit --- src/access_moppy/resources/qc/cmip7_ranges.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/access_moppy/resources/qc/cmip7_ranges.yml b/src/access_moppy/resources/qc/cmip7_ranges.yml index aa00ae28..3c4cb433 100644 --- a/src/access_moppy/resources/qc/cmip7_ranges.yml +++ b/src/access_moppy/resources/qc/cmip7_ranges.yml @@ -4398,4 +4398,4 @@ variables: max: 20.0 ssp*: min: -20.0 - max: 20.0 \ No newline at end of file + max: 20.0 From c754da939408174d8e802fc73d5d556d633b2281 Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Tue, 23 Jun 2026 18:23:24 +1000 Subject: [PATCH 08/21] Fix test_write_repacks_cmip7_output failing after QC integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tests/unit/test_base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_base.py b/tests/unit/test_base.py index 7c0ca085..1d462b6c 100644 --- a/tests/unit/test_base.py +++ b/tests/unit/test_base.py @@ -1159,6 +1159,7 @@ def test_write_repacks_cmip7_output(self, cmoriser_with_dataset, temp_dir): with ( patch("psutil.virtual_memory") as mock_mem, patch("access_moppy.base.subprocess.run") as mock_run, + patch("access_moppy.base.validate_cmip7_output"), ): mock_mem.return_value = MagicMock( total=32 * 1024**3, From 674f4483c54de33e66e84c2a7c90f6a8d52c590a Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Tue, 23 Jun 2026 18:26:26 +1000 Subject: [PATCH 09/21] 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. --- src/access_moppy/resources/qc/cmip7_ranges.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/access_moppy/resources/qc/cmip7_ranges.yml b/src/access_moppy/resources/qc/cmip7_ranges.yml index 3c4cb433..dcd66e04 100644 --- a/src/access_moppy/resources/qc/cmip7_ranges.yml +++ b/src/access_moppy/resources/qc/cmip7_ranges.yml @@ -3713,17 +3713,17 @@ variables: units: K default: min: 180.0 - max: 340.0 + max: 330.0 experiments: historical: min: 180.0 - max: 340.0 + max: 330.0 piControl: min: 180.0 - max: 340.0 + max: 325.0 ssp*: min: 180.0 - max: 340.0 + max: 335.0 tasmax: units: K default: From c4f1bea95aa81806ceed3379d9b345d606e21d71 Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Tue, 23 Jun 2026 19:03:43 +1000 Subject: [PATCH 10/21] 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. --- tests/unit/test_batch_report.py | 292 ++++++++++++++++++++++++++++++++ tests/unit/test_cmip7_qc.py | 101 +++++++++++ 2 files changed, 393 insertions(+) diff --git a/tests/unit/test_batch_report.py b/tests/unit/test_batch_report.py index b25629aa..b6c99240 100644 --- a/tests/unit/test_batch_report.py +++ b/tests/unit/test_batch_report.py @@ -6,8 +6,11 @@ import sqlite3 from datetime import datetime, timedelta from pathlib import Path +from unittest.mock import patch +import numpy as np import pytest +import xarray as xr from access_moppy import batch_report from access_moppy.batch_cmoriser import SIDECAR_FILENAME, finalize_monitor @@ -354,3 +357,292 @@ def test_finalize_monitor_writes_report_before_removing_sidecar(tmp_path: Path) assert report["success"] is True assert report["monitor"]["pbs_job_id"] == "monitor.123" assert report["script_dir"] == str(script_dir) + + +def _write_qc_test_file( + path: Path, + variable_id: str, + experiment_id: str, + values: list[float], + units: str = "K", +) -> None: + """Write a test NetCDF file for QC testing.""" + ds = xr.Dataset( + { + variable_id: xr.DataArray( + np.asarray(values, dtype=float), + dims=["time"], + attrs={"units": units}, + ) + }, + attrs={ + "mip_era": "CMIP7", + "variable_id": variable_id, + "experiment_id": experiment_id, + "source_id": "ACCESS-ESM1-6", + }, + ) + ds.to_netcdf(path) + ds.close() + + +@pytest.mark.unit +def test_run_qc_on_output_folder_with_all_passing_files(tmp_path: Path) -> None: + """QC reports success when all files pass validation.""" + output_folder = tmp_path / "output" + output_folder.mkdir() + + # Create a single passing file to avoid xarray caching issues + _write_qc_test_file( + output_folder / "tas_pass.nc", + "tas", + "historical", + [285.0, 287.5, 289.0], + ) + + results = batch_report._run_qc_on_output_folder(output_folder) + + assert results is not None + assert results["total"] == 1 + assert results["passed"] == 1 + assert results["failed"] == 0 + assert results["failures"] == [] + + +@pytest.mark.unit +def test_run_qc_on_output_folder_with_failing_files(tmp_path: Path) -> None: + """QC reports failures when files fail validation.""" + output_folder = tmp_path / "output" + output_folder.mkdir() + + # Create a failing file (tas out of range for piControl) + _write_qc_test_file( + output_folder / "tas_fail.nc", + "tas", + "piControl", + [326.5], # Above 325K limit for piControl + ) + + results = batch_report._run_qc_on_output_folder(output_folder) + + assert results is not None + assert results["passed"] == 0 + assert results["failed"] == 1 + assert results["total"] == 1 + assert len(results["failures"]) == 1 + assert results["failures"][0]["variable_id"] == "tas" + assert results["failures"][0]["experiment_id"] == "piControl" + assert "error" in results["failures"][0] + + +@pytest.mark.unit +def test_run_qc_on_output_folder_with_mixed_pass_fail(tmp_path: Path) -> None: + """QC correctly tallies both passing and failing files.""" + output_folder = tmp_path / "output" + output_folder.mkdir() + + # Create passing file + _write_qc_test_file( + output_folder / "tas_pass.nc", + "tas", + "historical", + [285.0], + ) + + # Create failing file + _write_qc_test_file( + output_folder / "tas_fail.nc", + "tas", + "piControl", + [326.5], + ) + + results = batch_report._run_qc_on_output_folder(output_folder) + + assert results is not None + assert results["passed"] == 1 + assert results["failed"] == 1 + assert results["total"] == 2 + assert len(results["failures"]) == 1 + + +@pytest.mark.unit +def test_run_qc_on_output_folder_respects_moppy_skip_qc_env_var( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """QC returns None when MOPPY_SKIP_QC=1 environment variable is set.""" + output_folder = tmp_path / "output" + output_folder.mkdir() + + _write_qc_test_file( + output_folder / "tas.nc", + "tas", + "historical", + [285.0], + ) + + monkeypatch.setenv("MOPPY_SKIP_QC", "1") + results = batch_report._run_qc_on_output_folder(output_folder) + + assert results is None + + +@pytest.mark.unit +def test_run_qc_on_output_folder_with_no_nc_files(tmp_path: Path) -> None: + """QC returns None when no .nc files are found.""" + output_folder = tmp_path / "output" + output_folder.mkdir() + + results = batch_report._run_qc_on_output_folder(output_folder) + + assert results is None + + +@pytest.mark.unit +def test_run_qc_on_output_folder_with_nested_files(tmp_path: Path) -> None: + """QC finds and validates files in nested subdirectories.""" + output_folder = tmp_path / "output" + nested = output_folder / "deep" / "nested" / "path" + nested.mkdir(parents=True) + + _write_qc_test_file( + nested / "tas.nc", + "tas", + "historical", + [285.0], + ) + + results = batch_report._run_qc_on_output_folder(output_folder) + + assert results is not None + assert results["total"] == 1 + assert results["passed"] == 1 + + +@pytest.mark.unit +def test_build_batch_report_includes_qc_section_by_default(tmp_path: Path) -> None: + """build_batch_report includes QC results by default.""" + db_path = tmp_path / "cmor_tasks.db" + output_folder = tmp_path / "output" + output_folder.mkdir() + + # Create a valid test file + _write_qc_test_file( + output_folder / "tas.nc", + "tas", + "historical", + [285.0], + ) + + with TaskTracker(db_path) as tracker: + tracker.add_task("Amon.tas", "historical") + tracker.mark_completed("Amon.tas", "historical") + + report = batch_report.build_batch_report( + db_path, + output_folder=output_folder, + skip_qc=False, + ) + + assert "qc" in report + assert report["qc"]["passed"] == 1 + assert report["qc"]["failed"] == 0 + assert report["qc"]["total"] == 1 + + +@pytest.mark.unit +def test_build_batch_report_omits_qc_section_when_skip_qc_true(tmp_path: Path) -> None: + """build_batch_report omits QC results when skip_qc=True.""" + db_path = tmp_path / "cmor_tasks.db" + output_folder = tmp_path / "output" + output_folder.mkdir() + + _write_qc_test_file( + output_folder / "tas.nc", + "tas", + "historical", + [285.0], + ) + + with TaskTracker(db_path) as tracker: + tracker.add_task("Amon.tas", "historical") + tracker.mark_completed("Amon.tas", "historical") + + report = batch_report.build_batch_report( + db_path, + output_folder=output_folder, + skip_qc=True, + ) + + assert "qc" not in report + + +@pytest.mark.unit +def test_write_batch_report_passes_skip_qc_to_build( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """write_batch_report correctly passes skip_qc parameter to build_batch_report.""" + db_path = tmp_path / "cmor_tasks.db" + output_folder = tmp_path / "output" + output_folder.mkdir() + + _write_qc_test_file( + output_folder / "tas.nc", + "tas", + "historical", + [285.0], + ) + + with TaskTracker(db_path) as tracker: + tracker.add_task("Amon.tas", "historical") + tracker.mark_completed("Amon.tas", "historical") + + # Test with skip_qc=False (should include QC) + batch_report.write_batch_report( + db_path, + output_folder=output_folder, + skip_qc=False, + ) + + report = json.loads((db_path.parent / "moppy_batch_report.json").read_text()) + assert "qc" in report + + # Clean up report + (db_path.parent / "moppy_batch_report.json").unlink() + + # Test with skip_qc=True (should omit QC) + batch_report.write_batch_report( + db_path, + output_folder=output_folder, + skip_qc=True, + ) + + report = json.loads((db_path.parent / "moppy_batch_report.json").read_text()) + assert "qc" not in report + + +@pytest.mark.unit +def test_run_qc_on_output_folder_includes_detailed_failure_info(tmp_path: Path) -> None: + """QC failure details include ranges and units where available.""" + output_folder = tmp_path / "output" + output_folder.mkdir() + + _write_qc_test_file( + output_folder / "tas_fail.nc", + "tas", + "piControl", + [326.5], + ) + + results = batch_report._run_qc_on_output_folder(output_folder) + + assert results["failed"] == 1 + failure = results["failures"][0] + assert "observed_range" in failure + assert "allowed_range" in failure + assert "units" in failure + assert failure["units"] == "K" + assert failure["observed_range"][0] == 326.5 + assert failure["observed_range"][1] == 326.5 + assert failure["allowed_range"][0] == 180.0 + assert failure["allowed_range"][1] == 325.0 diff --git a/tests/unit/test_cmip7_qc.py b/tests/unit/test_cmip7_qc.py index 90eac068..7892538b 100644 --- a/tests/unit/test_cmip7_qc.py +++ b/tests/unit/test_cmip7_qc.py @@ -258,3 +258,104 @@ def test_select_output_variable_ignores_bounds_variables(tmp_path): ds.to_netcdf(path) # Should not raise — tasmax must be identified despite the *_bnds variables validate_cmip7_output(path) + + +@pytest.mark.unit +def test_validate_cmip7_output_requires_variable_id(tmp_path): + """Validation fails if variable_id attribute is missing.""" + path = tmp_path / "no_var_id.nc" + ds = xr.Dataset( + { + "tas": xr.DataArray( + np.array([285.0, 286.0]), + dims=["time"], + attrs={"units": "K"}, + ) + }, + attrs={ + "mip_era": "CMIP7", + "experiment_id": "historical", + "source_id": "ACCESS-ESM1-6", + }, + ) + ds.to_netcdf(path) + + with pytest.raises(ValueError, match="variable_id"): + validate_cmip7_output(path) + + +@pytest.mark.unit +def test_validate_cmip7_output_requires_experiment_id(tmp_path): + """Validation fails if experiment_id attribute is missing.""" + path = tmp_path / "no_exp_id.nc" + ds = xr.Dataset( + { + "tas": xr.DataArray( + np.array([285.0, 286.0]), + dims=["time"], + attrs={"units": "K"}, + ) + }, + attrs={ + "mip_era": "CMIP7", + "variable_id": "tas", + "source_id": "ACCESS-ESM1-6", + }, + ) + ds.to_netcdf(path) + + with pytest.raises(ValueError, match="experiment_id"): + validate_cmip7_output(path) + + +@pytest.mark.unit +def test_validate_cmip7_output_detects_all_missing_values(tmp_path): + """Validation fails when all data is missing/NaN.""" + path = _write_cmip7_output( + tmp_path, + values=[np.nan, np.nan], + experiment_id="historical", + filename="all_nan.nc", + ) + + with pytest.raises(ValueError, match="missing"): + validate_cmip7_output(path) + + +@pytest.mark.unit +def test_validate_cmip7_output_detects_infinity_values(tmp_path): + """Validation fails when data contains infinity.""" + path = _write_cmip7_output( + tmp_path, + values=[285.0, np.inf], + experiment_id="historical", + filename="with_inf.nc", + ) + + with pytest.raises(ValueError, match="infinity"): + validate_cmip7_output(path) + + +@pytest.mark.unit +def test_validate_cmip7_output_experiment_pattern_matching(tmp_path): + """Experiment matching falls back to wildcard patterns - ssp370 uses ssp* rules.""" + # ssp370 should match ssp* pattern and pass with values in [180-335] range + path = _write_cmip7_output( + tmp_path, + values=[330.0], + experiment_id="ssp370", + filename="ssp370.nc", + ) + + # ssp370 with 330K is within the ssp* range (180-335), so should pass + validate_cmip7_output(path) + + # But 335.5K should fail + path_fail = _write_cmip7_output( + tmp_path, + values=[335.5], + experiment_id="ssp370", + filename="ssp370_fail.nc", + ) + with pytest.raises(ValueError, match="outside allowed range"): + validate_cmip7_output(path_fail) From 8c7325b7a08019f25077894dfd6b57aa531e0cb3 Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Tue, 23 Jun 2026 19:07:41 +1000 Subject: [PATCH 11/21] Refactor test cases in batch report and CMIP7 QC to remove unnecessary blank linespre-commit --- tests/unit/test_batch_report.py | 73 ++++++++++++++++----------------- tests/unit/test_cmip7_qc.py | 8 ++-- 2 files changed, 40 insertions(+), 41 deletions(-) diff --git a/tests/unit/test_batch_report.py b/tests/unit/test_batch_report.py index b6c99240..ea85fd9e 100644 --- a/tests/unit/test_batch_report.py +++ b/tests/unit/test_batch_report.py @@ -6,7 +6,6 @@ import sqlite3 from datetime import datetime, timedelta from pathlib import Path -from unittest.mock import patch import numpy as np import pytest @@ -391,7 +390,7 @@ def test_run_qc_on_output_folder_with_all_passing_files(tmp_path: Path) -> None: """QC reports success when all files pass validation.""" output_folder = tmp_path / "output" output_folder.mkdir() - + # Create a single passing file to avoid xarray caching issues _write_qc_test_file( output_folder / "tas_pass.nc", @@ -399,9 +398,9 @@ def test_run_qc_on_output_folder_with_all_passing_files(tmp_path: Path) -> None: "historical", [285.0, 287.5, 289.0], ) - + results = batch_report._run_qc_on_output_folder(output_folder) - + assert results is not None assert results["total"] == 1 assert results["passed"] == 1 @@ -414,7 +413,7 @@ def test_run_qc_on_output_folder_with_failing_files(tmp_path: Path) -> None: """QC reports failures when files fail validation.""" output_folder = tmp_path / "output" output_folder.mkdir() - + # Create a failing file (tas out of range for piControl) _write_qc_test_file( output_folder / "tas_fail.nc", @@ -422,9 +421,9 @@ def test_run_qc_on_output_folder_with_failing_files(tmp_path: Path) -> None: "piControl", [326.5], # Above 325K limit for piControl ) - + results = batch_report._run_qc_on_output_folder(output_folder) - + assert results is not None assert results["passed"] == 0 assert results["failed"] == 1 @@ -440,7 +439,7 @@ def test_run_qc_on_output_folder_with_mixed_pass_fail(tmp_path: Path) -> None: """QC correctly tallies both passing and failing files.""" output_folder = tmp_path / "output" output_folder.mkdir() - + # Create passing file _write_qc_test_file( output_folder / "tas_pass.nc", @@ -448,7 +447,7 @@ def test_run_qc_on_output_folder_with_mixed_pass_fail(tmp_path: Path) -> None: "historical", [285.0], ) - + # Create failing file _write_qc_test_file( output_folder / "tas_fail.nc", @@ -456,9 +455,9 @@ def test_run_qc_on_output_folder_with_mixed_pass_fail(tmp_path: Path) -> None: "piControl", [326.5], ) - + results = batch_report._run_qc_on_output_folder(output_folder) - + assert results is not None assert results["passed"] == 1 assert results["failed"] == 1 @@ -473,17 +472,17 @@ def test_run_qc_on_output_folder_respects_moppy_skip_qc_env_var( """QC returns None when MOPPY_SKIP_QC=1 environment variable is set.""" output_folder = tmp_path / "output" output_folder.mkdir() - + _write_qc_test_file( output_folder / "tas.nc", "tas", "historical", [285.0], ) - + monkeypatch.setenv("MOPPY_SKIP_QC", "1") results = batch_report._run_qc_on_output_folder(output_folder) - + assert results is None @@ -492,9 +491,9 @@ def test_run_qc_on_output_folder_with_no_nc_files(tmp_path: Path) -> None: """QC returns None when no .nc files are found.""" output_folder = tmp_path / "output" output_folder.mkdir() - + results = batch_report._run_qc_on_output_folder(output_folder) - + assert results is None @@ -504,16 +503,16 @@ def test_run_qc_on_output_folder_with_nested_files(tmp_path: Path) -> None: output_folder = tmp_path / "output" nested = output_folder / "deep" / "nested" / "path" nested.mkdir(parents=True) - + _write_qc_test_file( nested / "tas.nc", "tas", "historical", [285.0], ) - + results = batch_report._run_qc_on_output_folder(output_folder) - + assert results is not None assert results["total"] == 1 assert results["passed"] == 1 @@ -525,7 +524,7 @@ def test_build_batch_report_includes_qc_section_by_default(tmp_path: Path) -> No db_path = tmp_path / "cmor_tasks.db" output_folder = tmp_path / "output" output_folder.mkdir() - + # Create a valid test file _write_qc_test_file( output_folder / "tas.nc", @@ -533,17 +532,17 @@ def test_build_batch_report_includes_qc_section_by_default(tmp_path: Path) -> No "historical", [285.0], ) - + with TaskTracker(db_path) as tracker: tracker.add_task("Amon.tas", "historical") tracker.mark_completed("Amon.tas", "historical") - + report = batch_report.build_batch_report( db_path, output_folder=output_folder, skip_qc=False, ) - + assert "qc" in report assert report["qc"]["passed"] == 1 assert report["qc"]["failed"] == 0 @@ -556,24 +555,24 @@ def test_build_batch_report_omits_qc_section_when_skip_qc_true(tmp_path: Path) - db_path = tmp_path / "cmor_tasks.db" output_folder = tmp_path / "output" output_folder.mkdir() - + _write_qc_test_file( output_folder / "tas.nc", "tas", "historical", [285.0], ) - + with TaskTracker(db_path) as tracker: tracker.add_task("Amon.tas", "historical") tracker.mark_completed("Amon.tas", "historical") - + report = batch_report.build_batch_report( db_path, output_folder=output_folder, skip_qc=True, ) - + assert "qc" not in report @@ -585,38 +584,38 @@ def test_write_batch_report_passes_skip_qc_to_build( db_path = tmp_path / "cmor_tasks.db" output_folder = tmp_path / "output" output_folder.mkdir() - + _write_qc_test_file( output_folder / "tas.nc", "tas", "historical", [285.0], ) - + with TaskTracker(db_path) as tracker: tracker.add_task("Amon.tas", "historical") tracker.mark_completed("Amon.tas", "historical") - + # Test with skip_qc=False (should include QC) batch_report.write_batch_report( db_path, output_folder=output_folder, skip_qc=False, ) - + report = json.loads((db_path.parent / "moppy_batch_report.json").read_text()) assert "qc" in report - + # Clean up report (db_path.parent / "moppy_batch_report.json").unlink() - + # Test with skip_qc=True (should omit QC) batch_report.write_batch_report( db_path, output_folder=output_folder, skip_qc=True, ) - + report = json.loads((db_path.parent / "moppy_batch_report.json").read_text()) assert "qc" not in report @@ -626,16 +625,16 @@ def test_run_qc_on_output_folder_includes_detailed_failure_info(tmp_path: Path) """QC failure details include ranges and units where available.""" output_folder = tmp_path / "output" output_folder.mkdir() - + _write_qc_test_file( output_folder / "tas_fail.nc", "tas", "piControl", [326.5], ) - + results = batch_report._run_qc_on_output_folder(output_folder) - + assert results["failed"] == 1 failure = results["failures"][0] assert "observed_range" in failure diff --git a/tests/unit/test_cmip7_qc.py b/tests/unit/test_cmip7_qc.py index 7892538b..e7e77341 100644 --- a/tests/unit/test_cmip7_qc.py +++ b/tests/unit/test_cmip7_qc.py @@ -279,7 +279,7 @@ def test_validate_cmip7_output_requires_variable_id(tmp_path): }, ) ds.to_netcdf(path) - + with pytest.raises(ValueError, match="variable_id"): validate_cmip7_output(path) @@ -303,7 +303,7 @@ def test_validate_cmip7_output_requires_experiment_id(tmp_path): }, ) ds.to_netcdf(path) - + with pytest.raises(ValueError, match="experiment_id"): validate_cmip7_output(path) @@ -346,10 +346,10 @@ def test_validate_cmip7_output_experiment_pattern_matching(tmp_path): experiment_id="ssp370", filename="ssp370.nc", ) - + # ssp370 with 330K is within the ssp* range (180-335), so should pass validate_cmip7_output(path) - + # But 335.5K should fail path_fail = _write_cmip7_output( tmp_path, From d890345cc35913629b63868f8f6fc3e01a84728e Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Wed, 24 Jun 2026 12:02:21 +1000 Subject: [PATCH 12/21] Enhance time bounds calculation to support single monthly midpoint labels --- src/access_moppy/utilities.py | 53 ++++++++++++++++++++++++++++++++--- tests/unit/test_utilities.py | 10 +++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/access_moppy/utilities.py b/src/access_moppy/utilities.py index 3207f5b7..35dde5f7 100644 --- a/src/access_moppy/utilities.py +++ b/src/access_moppy/utilities.py @@ -2205,8 +2205,8 @@ def calculate_time_bounds( time = ds[time_coord] n_times = len(time) - if n_times < 2: - raise ValueError("Need at least 2 time points to infer time bounds") + if n_times < 1: + raise ValueError(f"Time coordinate '{time_coord}' is empty") # Compute only the 1-D time coordinate. Using .compute().values (rather than # plain .values) ensures that only the time coordinate's dask graph is @@ -2236,8 +2236,14 @@ def calculate_time_bounds( ) is_cftime = True - # Try to infer frequency - freq = _infer_frequency(time_values) + # Try to infer frequency. Single-point monthly outputs can still be bounded + # exactly when the label is already the midpoint of its monthly cell. + if n_times == 1: + freq = _infer_single_time_frequency(time_values[0], calendar, is_cftime) + if freq is None: + raise ValueError("Need at least 2 time points to infer time bounds") + else: + freq = _infer_frequency(time_values) # Initialize bounds array time_bnds = np.empty((n_times, 2), dtype=object if is_cftime else time_values.dtype) @@ -2320,6 +2326,45 @@ def _infer_frequency(time_values) -> Optional[str]: return "irregular" +def _infer_single_time_frequency( + time_value, calendar: str, is_cftime: bool +) -> Optional[str]: + """Infer frequency from a single timestamp when its position is unambiguous.""" + if _is_monthly_midpoint(time_value, calendar, is_cftime): + return "monthly" + return None + + +def _is_monthly_midpoint(time_value, calendar: str, is_cftime: bool) -> bool: + """Return True when *time_value* is exactly the midpoint of its month.""" + if is_cftime: + actual_calendar = ( + time_value.calendar if hasattr(time_value, "calendar") else calendar + ) + start = cftime.datetime( + time_value.year, + time_value.month, + 1, + calendar=actual_calendar, + ) + if time_value.month == 12: + end = cftime.datetime(time_value.year + 1, 1, 1, calendar=actual_calendar) + else: + end = cftime.datetime( + time_value.year, + time_value.month + 1, + 1, + calendar=actual_calendar, + ) + return time_value == start + (end - start) / 2 + + ts = pd.Timestamp(time_value) + start = pd.Timestamp(year=ts.year, month=ts.month, day=1) + end = start + pd.offsets.MonthBegin(1) + midpoint = start + (end - start) / 2 + return ts == midpoint + + def _calculate_monthly_bounds(time_values, calendar: str, is_cftime: bool): """Calculate bounds for monthly data.""" n_times = len(time_values) diff --git a/tests/unit/test_utilities.py b/tests/unit/test_utilities.py index 97cc2369..5fe1f8ff 100644 --- a/tests/unit/test_utilities.py +++ b/tests/unit/test_utilities.py @@ -46,6 +46,16 @@ def test_insufficient_time_points(self): with pytest.raises(ValueError, match="Need at least 2 time points"): calculate_time_bounds(ds) + def test_single_monthly_midpoint_is_supported(self): + """Single monthly midpoint labels should yield exact monthly bounds.""" + ds = xr.Dataset(coords={"time": [np.datetime64("2000-01-16T12:00:00")]}) + + time_bnds = calculate_time_bounds(ds) + + assert time_bnds.shape == (1, 2) + assert time_bnds[0, 0] == np.datetime64("2000-01-01T00:00:00") + assert time_bnds[0, 1] == np.datetime64("2000-02-01T00:00:00") + class TestCalculateTimeBoundsMonthly: """Test monthly frequency time bounds calculation.""" From 0e0b803c80ba81365e17cf97a17de4775b6e79b3 Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Wed, 24 Jun 2026 12:14:06 +1000 Subject: [PATCH 13/21] Add tests for CLI skip QC functionality and enhance validation for CMIP7 output --- tests/unit/test_batch_report.py | 29 ++++++++ tests/unit/test_cmip7_qc.py | 124 ++++++++++++++++++++++++++++++++ tests/unit/test_utilities.py | 25 +++++++ 3 files changed, 178 insertions(+) diff --git a/tests/unit/test_batch_report.py b/tests/unit/test_batch_report.py index ea85fd9e..86b753b1 100644 --- a/tests/unit/test_batch_report.py +++ b/tests/unit/test_batch_report.py @@ -302,6 +302,35 @@ def test_cli_loads_config_file(tmp_path: Path) -> None: assert json.loads(output.read_text())["experiment_id"] == "historical" +def test_cli_skip_qc_omits_qc_section(tmp_path: Path) -> None: + """The report CLI forwards --skip-qc into report generation.""" + db_path = tmp_path / "cmor_tasks.db" + _write_qc_test_file( + tmp_path / "tas.nc", + "tas", + "historical", + [285.0], + ) + + with TaskTracker(db_path) as tracker: + tracker.add_task("Amon.tas", "historical") + tracker.mark_completed("Amon.tas", "historical") + + output = tmp_path / "report.json" + rc = batch_report.main( + [ + "--db", + str(db_path), + "--output", + str(output), + "--skip-qc", + ] + ) + + assert rc == 0 + assert "qc" not in json.loads(output.read_text()) + + def test_build_batch_report_handles_old_tracker_schema(tmp_path: Path) -> None: """Reports can still be generated from pre-PBS-metadata tracker DBs.""" db_path = tmp_path / "old.db" diff --git a/tests/unit/test_cmip7_qc.py b/tests/unit/test_cmip7_qc.py index e7e77341..e5289380 100644 --- a/tests/unit/test_cmip7_qc.py +++ b/tests/unit/test_cmip7_qc.py @@ -10,6 +10,7 @@ from access_moppy.qc.cmip7 import ( _load_esm16_mapping_variables, _load_rules, + validate_cmip7_output_detailed, ) from access_moppy.qc.cmip7 import ( main as qc_main, @@ -184,6 +185,21 @@ def test_validate_cmip7_output_enforces_positive_up_from_esm16_mapping(tmp_path) validate_cmip7_output(path) +@pytest.mark.unit +def test_validate_cmip7_output_enforces_positive_down_from_esm16_mapping(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[1.0, 2.0], + experiment_id="historical", + variable_id="rldscs", + units="W m-2", + filename="rldscs_positive.nc", + ) + + with pytest.raises(ValueError, match="positive: down"): + validate_cmip7_output(path) + + @pytest.mark.unit def test_validate_cmip7_output_applies_variable_rules_for_other_sources(tmp_path): path = _write_cmip7_output( @@ -359,3 +375,111 @@ def test_validate_cmip7_output_experiment_pattern_matching(tmp_path): ) with pytest.raises(ValueError, match="outside allowed range"): validate_cmip7_output(path_fail) + + +@pytest.mark.unit +def test_validate_cmip7_output_detailed_passes_without_rule_for_unconfigured_variable( + tmp_path, +): + path = _write_cmip7_output( + tmp_path, + values=[1.0, 2.0], + experiment_id="historical", + variable_id="customvar", + source_id="ACCESS-CM3", + units="1", + filename="customvar.nc", + ) + + result = validate_cmip7_output_detailed(path) + + assert result.passed is True + assert result.variable_id == "customvar" + assert result.experiment_id == "historical" + assert result.error is None + + +@pytest.mark.unit +def test_validate_cmip7_output_detailed_reports_mapping_check_failure(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[-0.1, 0.2], + experiment_id="historical", + variable_id="evspsblsoi", + units="kg m-2 s-1", + filename="evspsblsoi_detailed.nc", + ) + + result = validate_cmip7_output_detailed(path) + + assert result.passed is False + assert result.variable_id == "evspsblsoi" + assert result.experiment_id == "historical" + assert "positive: up" in result.error + + +@pytest.mark.unit +def test_validate_cmip7_output_detailed_reports_units_mismatch(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[285.0, 286.0], + experiment_id="historical", + variable_id="tas", + source_id="ACCESS-CM3", + units="degC", + filename="tas_bad_units_detailed.nc", + ) + + result = validate_cmip7_output_detailed(path) + + assert result.passed is False + assert result.variable_id == "tas" + assert result.experiment_id == "historical" + assert result.units == "degC" + assert "Expected units" in result.error + + +@pytest.mark.unit +def test_validate_cmip7_output_detailed_returns_range_metadata_on_success(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[285.0, 287.0], + experiment_id="historical", + filename="tas_detailed_success.nc", + ) + + result = validate_cmip7_output_detailed(path) + + assert result.passed is True + assert result.units == "K" + assert result.observed_min == 285.0 + assert result.observed_max == 287.0 + assert result.allowed_min is not None + assert result.allowed_max is not None + + +@pytest.mark.unit +def test_validate_cmip7_output_detailed_reports_unexpected_selection_error(tmp_path): + path = tmp_path / "ambiguous.nc" + ds = xr.Dataset( + { + "tas": xr.DataArray(np.array([285.0]), dims=["time"], attrs={"units": "K"}), + "pr": xr.DataArray( + np.array([1.0]), + dims=["time"], + attrs={"units": "kg m-2 s-1"}, + ), + }, + attrs={ + "mip_era": "CMIP7", + "variable_id": "unknown_var", + "experiment_id": "historical", + "source_id": "ACCESS-CM3", + }, + ) + ds.to_netcdf(path) + + result = validate_cmip7_output_detailed(path) + + assert result.passed is False + assert result.error.startswith("Unexpected error: CMIP7 QC could not determine") diff --git a/tests/unit/test_utilities.py b/tests/unit/test_utilities.py index 5fe1f8ff..3afe7148 100644 --- a/tests/unit/test_utilities.py +++ b/tests/unit/test_utilities.py @@ -46,6 +46,13 @@ def test_insufficient_time_points(self): with pytest.raises(ValueError, match="Need at least 2 time points"): calculate_time_bounds(ds) + def test_empty_time_coordinate_raises_specific_error(self): + """Empty time coordinates should fail before frequency inference.""" + ds = xr.Dataset(coords={"time": np.array([], dtype="datetime64[ns]")}) + + with pytest.raises(ValueError, match="Time coordinate 'time' is empty"): + calculate_time_bounds(ds) + def test_single_monthly_midpoint_is_supported(self): """Single monthly midpoint labels should yield exact monthly bounds.""" ds = xr.Dataset(coords={"time": [np.datetime64("2000-01-16T12:00:00")]}) @@ -56,6 +63,24 @@ def test_single_monthly_midpoint_is_supported(self): assert time_bnds[0, 0] == np.datetime64("2000-01-01T00:00:00") assert time_bnds[0, 1] == np.datetime64("2000-02-01T00:00:00") + def test_single_non_midpoint_still_raises(self): + """Single timestamps that are not monthly midpoints remain ambiguous.""" + ds = xr.Dataset(coords={"time": [np.datetime64("2000-01-15T00:00:00")]}) + + with pytest.raises(ValueError, match="Need at least 2 time points"): + calculate_time_bounds(ds) + + def test_single_monthly_midpoint_cftime_is_supported(self): + """Single monthly midpoint support also applies to CFTime coordinates.""" + midpoint = cftime.DatetimeNoLeap(2000, 1, 16, 12) + ds = xr.Dataset(coords={"time": [midpoint]}) + + time_bnds = calculate_time_bounds(ds) + + assert time_bnds.shape == (1, 2) + assert time_bnds.values[0, 0] == cftime.DatetimeNoLeap(2000, 1, 1, 0) + assert time_bnds.values[0, 1] == cftime.DatetimeNoLeap(2000, 2, 1, 0) + class TestCalculateTimeBoundsMonthly: """Test monthly frequency time bounds calculation.""" From 747760af31604f5a361646243d85b66ba91d9812 Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Wed, 24 Jun 2026 12:20:06 +1000 Subject: [PATCH 14/21] Refactor QC validation to remove positive sign enforcement and update related tests --- docs/source/mapping_reference.rst | 2 +- docs/source/qc_validation.rst | 7 +++---- src/access_moppy/qc/cmip7.py | 28 ---------------------------- tests/unit/test_cmip7_qc.py | 24 ++++++++++++------------ 4 files changed, 16 insertions(+), 45 deletions(-) diff --git a/docs/source/mapping_reference.rst b/docs/source/mapping_reference.rst index 81f6e8f0..90217816 100644 --- a/docs/source/mapping_reference.rst +++ b/docs/source/mapping_reference.rst @@ -129,7 +129,7 @@ required fields: - Expected physical units of the CMIP output variable (e.g. ``"W m-2"``, ``"kg m-2 s-1"``). * - ``positive`` - Yes - - Sign convention: ``"up"``, ``"down"``, or ``null`` if not applicable. + - Sign convention metadata: ``"up"``, ``"down"``, or ``null`` if not applicable. * - ``model_variables`` - Yes - List of raw model variable names that must be loaded from the input files. diff --git a/docs/source/qc_validation.rst b/docs/source/qc_validation.rst index 76e229cd..97162134 100644 --- a/docs/source/qc_validation.rst +++ b/docs/source/qc_validation.rst @@ -10,13 +10,12 @@ Scope - QC is run on the *CMORised output file*, not the raw model input. - For ``source_id=ACCESS-ESM1-6``, QC covers all variables present in ``ACCESS-ESM1-6_mappings.json`` with generic checks (non-missing values, - finite values, and enforcement of mapping ``positive`` sign constraints - where defined). + finite values, and units checks where defined). - Physical ranges for all 293 ACCESS-ESM1-6 mapped variables are defined explicitly in the QC configuration with defaults and experiment-specific overrides (historical, piControl, ssp*). -- Each variable's physical range is derived from its definition (mapping units - and ``positive`` direction) and stored as a per-variable rule entry. +- Each variable's physical range is derived from its definition (mapping units) + and stored as a per-variable rule entry. - Rules are loaded from: ``access_moppy/resources/qc/cmip7_ranges.yml``. Running QC in Notebooks diff --git a/src/access_moppy/qc/cmip7.py b/src/access_moppy/qc/cmip7.py index 51886ac3..301b60af 100644 --- a/src/access_moppy/qc/cmip7.py +++ b/src/access_moppy/qc/cmip7.py @@ -154,15 +154,6 @@ def _resolve_range_rule_from_mapping_definition( minimum = float(envelope["min"]) maximum = float(envelope["max"]) - apply_positive = bool(override.get("apply_positive", True)) - - if apply_positive: - positive = mapping_entry.get("positive") - if positive == "up": - minimum = max(0.0, minimum) - elif positive == "down": - maximum = min(0.0, maximum) - return RangeRule( variable_id=variable_id, experiment_id=experiment_id, @@ -217,25 +208,6 @@ def _validate_esm16_mapping_checks( f"{variable_id} in experiment {experiment_id}: values contain infinity." ) - positive = mapping_entry.get("positive") - tolerance = 1e-12 - if positive == "up": - min_value = float(da.min(skipna=True).item()) - if min_value < -tolerance: - raise ValueError( - "CMIP7 QC failed for " - f"{variable_id} in experiment {experiment_id}: expected non-negative " - f"values from mapping 'positive: up', observed minimum {min_value:.6g}." - ) - elif positive == "down": - max_value = float(da.max(skipna=True).item()) - if max_value > tolerance: - raise ValueError( - "CMIP7 QC failed for " - f"{variable_id} in experiment {experiment_id}: expected non-positive " - f"values from mapping 'positive: down', observed maximum {max_value:.6g}." - ) - expected_units = mapping_entry.get("units") if isinstance(expected_units, str) and expected_units: actual_units = da.attrs.get("units") diff --git a/tests/unit/test_cmip7_qc.py b/tests/unit/test_cmip7_qc.py index e5289380..65875245 100644 --- a/tests/unit/test_cmip7_qc.py +++ b/tests/unit/test_cmip7_qc.py @@ -171,22 +171,21 @@ def test_all_esm16_mapped_variables_have_explicit_qc_rule_entries(): @pytest.mark.unit -def test_validate_cmip7_output_enforces_positive_up_from_esm16_mapping(tmp_path): +def test_validate_cmip7_output_allows_positive_up_mapping_signs(tmp_path): path = _write_cmip7_output( tmp_path, - values=[-0.1, 0.2], + values=[-0.01, 0.02], experiment_id="historical", variable_id="evspsblsoi", units="kg m-2 s-1", - filename="evspsblsoi_negative.nc", + filename="evspsblsoi_cross_zero.nc", ) - with pytest.raises(ValueError, match="positive: up"): - validate_cmip7_output(path) + validate_cmip7_output(path) @pytest.mark.unit -def test_validate_cmip7_output_enforces_positive_down_from_esm16_mapping(tmp_path): +def test_validate_cmip7_output_allows_positive_down_mapping_signs(tmp_path): path = _write_cmip7_output( tmp_path, values=[1.0, 2.0], @@ -196,8 +195,7 @@ def test_validate_cmip7_output_enforces_positive_down_from_esm16_mapping(tmp_pat filename="rldscs_positive.nc", ) - with pytest.raises(ValueError, match="positive: down"): - validate_cmip7_output(path) + validate_cmip7_output(path) @pytest.mark.unit @@ -400,10 +398,10 @@ def test_validate_cmip7_output_detailed_passes_without_rule_for_unconfigured_var @pytest.mark.unit -def test_validate_cmip7_output_detailed_reports_mapping_check_failure(tmp_path): +def test_validate_cmip7_output_detailed_ignores_positive_sign_metadata(tmp_path): path = _write_cmip7_output( tmp_path, - values=[-0.1, 0.2], + values=[-0.01, 0.02], experiment_id="historical", variable_id="evspsblsoi", units="kg m-2 s-1", @@ -412,10 +410,12 @@ def test_validate_cmip7_output_detailed_reports_mapping_check_failure(tmp_path): result = validate_cmip7_output_detailed(path) - assert result.passed is False + assert result.passed is True assert result.variable_id == "evspsblsoi" assert result.experiment_id == "historical" - assert "positive: up" in result.error + assert result.error is None + assert result.observed_min == -0.01 + assert result.observed_max == 0.02 @pytest.mark.unit From 1f484118533ab9c81f954c4592f513eaeff8af12 Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Wed, 24 Jun 2026 13:41:31 +1000 Subject: [PATCH 15/21] Add functions to handle missing-value sentinels and update QC validation --- src/access_moppy/qc/cmip7.py | 50 ++++++++++++++++++++++- src/access_moppy/vocabulary_processors.py | 33 +++++++++++++-- tests/unit/test_cmip7_qc.py | 33 +++++++++++++++ tests/unit/test_vocabulary_processors.py | 30 ++++++++++++++ 4 files changed, 140 insertions(+), 6 deletions(-) diff --git a/src/access_moppy/qc/cmip7.py b/src/access_moppy/qc/cmip7.py index 301b60af..4e06fc76 100644 --- a/src/access_moppy/qc/cmip7.py +++ b/src/access_moppy/qc/cmip7.py @@ -40,6 +40,52 @@ class ValidationResult: units: str | None = None +def _iter_missing_sentinels(da: xr.DataArray) -> list[float]: + """Collect numeric missing-value sentinels from attrs/encoding.""" + + sentinels: list[float] = [] + for container in (da.attrs, da.encoding): + for key in ("missing_value", "_FillValue"): + value = container.get(key) + if value is None: + continue + values = np.asarray(value).ravel() + for item in values: + try: + candidate = float(item) + except (TypeError, ValueError): + continue + if np.isfinite(candidate): + sentinels.append(candidate) + return sentinels + + +def _mask_missing_sentinels_for_qc(da: xr.DataArray) -> xr.DataArray: + """Mask numeric missing-value sentinels prior to QC reductions. + + Some files encode missing values as float32 and write to float64 arrays, + so values can appear as 1.0000000200408773e+20 while metadata carries 1e20. + Use tolerance-aware matching so these sentinels are not interpreted as data. + """ + + sentinels = _iter_missing_sentinels(da) + if not sentinels: + return da + + mask = xr.zeros_like(da, dtype=bool) + for sentinel in sentinels: + atol = np.finfo(np.float32).eps * max(abs(sentinel), 1.0) + mask = mask | xr.apply_ufunc( + np.isclose, + da, + sentinel, + kwargs={"rtol": 0.0, "atol": atol, "equal_nan": False}, + dask="allowed", + ) + + return da.where(~mask) + + @lru_cache(maxsize=1) def _load_esm16_mapping_variables() -> dict[str, dict[str, Any]]: """Flatten ACCESS-ESM1-6 mapping entries keyed by CMIP variable id.""" @@ -241,7 +287,7 @@ def validate_cmip7_output(output_path: str | Path) -> None: rule = _resolve_range_rule(variable_id, experiment_id) output_variable = _select_output_variable(ds, attrs) - da = ds[output_variable] + da = _mask_missing_sentinels_for_qc(ds[output_variable]) # Apply generic checks for all variables present in the ACCESS-ESM1-6 # mapping so every mapped variable receives QC coverage. @@ -318,7 +364,7 @@ def validate_cmip7_output_detailed(output_path: str | Path) -> ValidationResult: rule = _resolve_range_rule(variable_id, experiment_id) output_variable = _select_output_variable(ds, attrs) - da = ds[output_variable] + da = _mask_missing_sentinels_for_qc(ds[output_variable]) # Apply generic checks for all variables present in the ACCESS-ESM1-6 # mapping so every mapped variable receives QC coverage. diff --git a/src/access_moppy/vocabulary_processors.py b/src/access_moppy/vocabulary_processors.py index 97301069..dd9a77ad 100644 --- a/src/access_moppy/vocabulary_processors.py +++ b/src/access_moppy/vocabulary_processors.py @@ -42,6 +42,23 @@ _CMOR_CVS_CACHE: Optional[Dict[str, Any]] = None +def _cast_missing_value_to_data_dtype(value: Any, data_array: xr.DataArray) -> Any: + """Cast a missing-value marker to the data array dtype when possible. + + Keeping ``missing_value`` and ``_FillValue`` aligned with the variable dtype + avoids float64-vs-float32 sentinel drift (e.g., 1e20 vs 1.00000002e20). + """ + + try: + dtype = data_array.dtype + if np.issubdtype(dtype, np.floating) or np.issubdtype(dtype, np.integer): + return np.asarray(value, dtype=dtype)[()] + except (TypeError, ValueError): + pass + + return value + + def _load_cmor_cvs() -> Dict[str, Any]: """Load and cache the cmor-cvs.json CMIP7 controlled vocabulary. @@ -677,8 +694,12 @@ def standardize_missing_values(self, data_array, convert_existing: bool = True): xarray.DataArray: Data array with standardized missing values """ # Get the correct CMIP6 missing value - cmip_missing_value = self.get_cmip_missing_value() - cmip_fill_value = self.get_cmip_fill_value() + cmip_missing_value = _cast_missing_value_to_data_dtype( + self.get_cmip_missing_value(), data_array + ) + cmip_fill_value = _cast_missing_value_to_data_dtype( + self.get_cmip_fill_value(), data_array + ) # Create a shallow copy to avoid modifying the original (preserves dask arrays) result = data_array.copy(deep=False) @@ -2067,8 +2088,12 @@ def standardize_missing_values(self, data_array, convert_existing: bool = True): xarray.DataArray: Data array with standardized missing values """ # Get the correct CMIP7 missing value - cmip_missing_value = self.get_cmip_missing_value() - cmip_fill_value = self.get_cmip_fill_value() + cmip_missing_value = _cast_missing_value_to_data_dtype( + self.get_cmip_missing_value(), data_array + ) + cmip_fill_value = _cast_missing_value_to_data_dtype( + self.get_cmip_fill_value(), data_array + ) # Create a shallow copy to avoid modifying the original (preserves dask arrays) result = data_array.copy(deep=False) diff --git a/tests/unit/test_cmip7_qc.py b/tests/unit/test_cmip7_qc.py index 65875245..a8fc35a4 100644 --- a/tests/unit/test_cmip7_qc.py +++ b/tests/unit/test_cmip7_qc.py @@ -350,6 +350,39 @@ def test_validate_cmip7_output_detects_infinity_values(tmp_path): validate_cmip7_output(path) +@pytest.mark.unit +def test_validate_cmip7_output_masks_rounded_fill_values_in_range_checks(tmp_path): + """Rounded float32 fill values (e.g. 1.00000002e20) are ignored in QC.""" + path = tmp_path / "hur_with_rounded_fill.nc" + rounded_fill = float(np.float32(1e20)) + + ds = xr.Dataset( + { + "hur": xr.DataArray( + np.array([50.0, rounded_fill], dtype=np.float64), + dims=["time"], + attrs={"units": "%", "_FillValue": 1e20, "missing_value": 1e20}, + ) + }, + attrs={ + "mip_era": "CMIP7", + "variable_id": "hur", + "branded_variable": "hur", + "experiment_id": "historical", + "source_id": "ACCESS-ESM1-6", + "units": "%", + }, + ) + ds.to_netcdf(path) + + validate_cmip7_output(path) + + result = validate_cmip7_output_detailed(path) + assert result.passed is True + assert result.observed_min == pytest.approx(50.0) + assert result.observed_max == pytest.approx(50.0) + + @pytest.mark.unit def test_validate_cmip7_output_experiment_pattern_matching(tmp_path): """Experiment matching falls back to wildcard patterns - ssp370 uses ssp* rules.""" diff --git a/tests/unit/test_vocabulary_processors.py b/tests/unit/test_vocabulary_processors.py index 3a883522..cdd5db3c 100644 --- a/tests/unit/test_vocabulary_processors.py +++ b/tests/unit/test_vocabulary_processors.py @@ -156,6 +156,20 @@ def test_normalize_dataset_missing_values_static_method(): assert "missing_value" not in result["b"].attrs +@pytest.mark.unit +def test_standardize_missing_values_casts_markers_to_data_dtype(vocabulary_instance): + da = xr.DataArray( + np.array([1.0, np.nan, 3.0], dtype=np.float32), + dims=["x"], + attrs={"units": "K"}, + ) + + result = vocabulary_instance.standardize_missing_values(da, convert_existing=False) + + assert np.asarray(result.attrs["missing_value"]).dtype == np.float32 + assert np.asarray(result.attrs["_FillValue"]).dtype == np.float32 + + @pytest.mark.unit def test_get_external_variables_cell_measures_and_heuristics(vocabulary_instance): vocabulary_instance.variable = { @@ -630,6 +644,22 @@ def test_cmip7_generate_filename_cftime_time_branch(cmip7_vocab_instance): assert "202002" in filename +@pytest.mark.unit +def test_cmip7_standardize_missing_values_casts_markers_to_data_dtype( + cmip7_vocab_instance, +): + da = xr.DataArray( + np.array([1.0, np.nan, 3.0], dtype=np.float32), + dims=["x"], + attrs={"units": "K"}, + ) + + result = cmip7_vocab_instance.standardize_missing_values(da, convert_existing=False) + + assert np.asarray(result.attrs["missing_value"]).dtype == np.float32 + assert np.asarray(result.attrs["_FillValue"]).dtype == np.float32 + + @pytest.mark.unit def test_cmip7_generate_filename_datetime64_time_branch(cmip7_vocab_instance): """CMIP7: numpy datetime64 time – uses pd.Timestamp branch.""" From f2d1a2cab23aa3dde846c973a2bbb42521af34ea Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Wed, 24 Jun 2026 15:52:46 +1000 Subject: [PATCH 16/21] Add unit tests for standardizing missing values in vocabulary processor --- tests/unit/test_cmip7_qc.py | 232 +++++++++++++++++++++++ tests/unit/test_vocabulary_processors.py | 48 +++++ 2 files changed, 280 insertions(+) diff --git a/tests/unit/test_cmip7_qc.py b/tests/unit/test_cmip7_qc.py index a8fc35a4..cc7fe297 100644 --- a/tests/unit/test_cmip7_qc.py +++ b/tests/unit/test_cmip7_qc.py @@ -8,8 +8,10 @@ from access_moppy.base import CMORiser from access_moppy.qc import validate_cmip7_output from access_moppy.qc.cmip7 import ( + _iter_missing_sentinels, _load_esm16_mapping_variables, _load_rules, + _mask_missing_sentinels_for_qc, validate_cmip7_output_detailed, ) from access_moppy.qc.cmip7 import ( @@ -52,6 +54,101 @@ def _write_cmip7_output( return path +@pytest.mark.unit +def test_iter_missing_sentinels_from_attrs(): + """Test that sentinels are collected from DataArray attrs.""" + da = xr.DataArray( + np.array([1.0, 2.0, 3.0]), + attrs={"missing_value": 1e20, "_FillValue": 9.96921e36}, + ) + + sentinels = _iter_missing_sentinels(da) + + assert 1e20 in sentinels + assert 9.96921e36 in sentinels + assert len(sentinels) == 2 + + +@pytest.mark.unit +def test_iter_missing_sentinels_from_encoding(): + """Test that sentinels are collected from DataArray encoding.""" + da = xr.DataArray( + np.array([1.0, 2.0, 3.0]), + attrs={}, + ) + da.encoding["_FillValue"] = 1e20 + + sentinels = _iter_missing_sentinels(da) + + assert 1e20 in sentinels + + +@pytest.mark.unit +def test_iter_missing_sentinels_ignores_non_finite_values(): + """Test that non-finite sentinels (inf, nan) are ignored.""" + da = xr.DataArray( + np.array([1.0, 2.0, 3.0]), + attrs={"missing_value": np.inf, "_FillValue": np.nan}, + ) + + sentinels = _iter_missing_sentinels(da) + + assert len(sentinels) == 0 + + +@pytest.mark.unit +def test_iter_missing_sentinels_converts_non_numeric_to_float(): + """Test that non-numeric metadata values are safely ignored.""" + da = xr.DataArray( + np.array([1.0, 2.0, 3.0]), + attrs={"missing_value": "not_a_number"}, + ) + + sentinels = _iter_missing_sentinels(da) + + assert len(sentinels) == 0 + + +@pytest.mark.unit +def test_iter_missing_sentinels_with_array_values(): + """Test that array values in metadata are flattened and processed.""" + da = xr.DataArray( + np.array([1.0, 2.0, 3.0]), + attrs={"missing_value": np.array([1e20, 2e20])}, + ) + + sentinels = _iter_missing_sentinels(da) + + assert 1e20 in sentinels + assert 2e20 in sentinels + + +@pytest.mark.unit +def test_mask_missing_sentinels_for_qc_returns_unmodified_when_no_sentinels(): + """Early return when no sentinels found.""" + da = xr.DataArray(np.array([1.0, 2.0, 3.0])) + + result = _mask_missing_sentinels_for_qc(da) + + assert result is da # Should return the same object + + +@pytest.mark.unit +def test_mask_missing_sentinels_for_qc_masks_with_tolerance(): + """Sentinels are masked using tolerance-aware matching.""" + da = xr.DataArray( + np.array([280.0, 1.00000002e20, 285.0], dtype=np.float64), + attrs={"_FillValue": 1e20}, + ) + + result = _mask_missing_sentinels_for_qc(da) + + # Masked value should become NaN + assert np.isnan(result.values[1]) + assert result.values[0] == 280.0 + assert result.values[2] == 285.0 + + @pytest.mark.unit def test_validate_cmip7_output_tas_passes_for_historical_range(tmp_path): path = _write_cmip7_output( @@ -383,6 +480,141 @@ def test_validate_cmip7_output_masks_rounded_fill_values_in_range_checks(tmp_pat assert result.observed_max == pytest.approx(50.0) +@pytest.mark.unit +def test_validate_cmip7_output_masks_multiple_sentinels_in_range_checks(tmp_path): + """Multiple sentinels (missing_value and _FillValue) are both masked.""" + path = tmp_path / "tas_with_multiple_sentinels.nc" + + ds = xr.Dataset( + { + "tas": xr.DataArray( + np.array([280.0, 1e20, 285.0], dtype=np.float64), + dims=["time"], + attrs={ + "units": "K", + "missing_value": 1e20, + }, + ) + }, + attrs={ + "mip_era": "CMIP7", + "variable_id": "tas", + "branded_variable": "tas", + "experiment_id": "historical", + "source_id": "ACCESS-ESM1-6", + "units": "K", + }, + ) + ds.to_netcdf(path) + + validate_cmip7_output(path) + + result = validate_cmip7_output_detailed(path) + assert result.passed is True + assert result.observed_min == pytest.approx(280.0) + assert result.observed_max == pytest.approx(285.0) + + +@pytest.mark.unit +def test_validate_cmip7_output_masks_no_sentinels_early_return(tmp_path): + """When no sentinels are present, masking is skipped.""" + path = tmp_path / "tas_no_sentinels.nc" + + ds = xr.Dataset( + { + "tas": xr.DataArray( + np.array([280.0, 285.0, 290.0], dtype=np.float64), + dims=["time"], + attrs={"units": "K"}, + ) + }, + attrs={ + "mip_era": "CMIP7", + "variable_id": "tas", + "branded_variable": "tas", + "experiment_id": "historical", + "source_id": "ACCESS-ESM1-6", + "units": "K", + }, + ) + ds.to_netcdf(path) + + validate_cmip7_output(path) + + result = validate_cmip7_output_detailed(path) + assert result.passed is True + assert result.observed_min == pytest.approx(280.0) + assert result.observed_max == pytest.approx(290.0) + + +@pytest.mark.unit +def test_validate_cmip7_output_masks_encoding_sentinels(tmp_path): + """Sentinels from encoding (not just attrs) are detected and masked.""" + path = tmp_path / "tas_encoding_sentinels.nc" + + ds = xr.Dataset( + { + "tas": xr.DataArray( + np.array([280.0, 1e20], dtype=np.float64), + dims=["time"], + attrs={"units": "K"}, + ) + }, + attrs={ + "mip_era": "CMIP7", + "variable_id": "tas", + "branded_variable": "tas", + "experiment_id": "historical", + "source_id": "ACCESS-ESM1-6", + "units": "K", + }, + ) + # Add sentinel to encoding instead of attrs + ds["tas"].encoding["_FillValue"] = 1e20 + ds.to_netcdf(path) + + validate_cmip7_output(path) + + result = validate_cmip7_output_detailed(path) + assert result.passed is True + assert result.observed_min == pytest.approx(280.0) + + +@pytest.mark.unit +def test_validate_cmip7_output_masks_non_numeric_metadata(tmp_path): + """Non-numeric metadata values in attrs/encoding are safely ignored.""" + path = tmp_path / "tas_non_numeric_metadata.nc" + + ds = xr.Dataset( + { + "tas": xr.DataArray( + np.array([280.0, 285.0], dtype=np.float64), + dims=["time"], + attrs={ + "units": "K", + "missing_value": "invalid_string", + }, + ) + }, + attrs={ + "mip_era": "CMIP7", + "variable_id": "tas", + "branded_variable": "tas", + "experiment_id": "historical", + "source_id": "ACCESS-ESM1-6", + "units": "K", + }, + ) + ds.to_netcdf(path) + + validate_cmip7_output(path) + + result = validate_cmip7_output_detailed(path) + assert result.passed is True + assert result.observed_min == pytest.approx(280.0) + assert result.observed_max == pytest.approx(285.0) + + @pytest.mark.unit def test_validate_cmip7_output_experiment_pattern_matching(tmp_path): """Experiment matching falls back to wildcard patterns - ssp370 uses ssp* rules.""" diff --git a/tests/unit/test_vocabulary_processors.py b/tests/unit/test_vocabulary_processors.py index cdd5db3c..c98ebaaa 100644 --- a/tests/unit/test_vocabulary_processors.py +++ b/tests/unit/test_vocabulary_processors.py @@ -170,6 +170,54 @@ def test_standardize_missing_values_casts_markers_to_data_dtype(vocabulary_insta assert np.asarray(result.attrs["_FillValue"]).dtype == np.float32 +@pytest.mark.unit +def test_standardize_missing_values_casts_markers_integer_dtype(vocabulary_instance): + """Test casting to float64 (standard data type for climate variables).""" + da = xr.DataArray( + np.array([1.0, 2.0, 3.0], dtype=np.float64), + dims=["x"], + attrs={"units": "K"}, + ) + + result = vocabulary_instance.standardize_missing_values(da, convert_existing=False) + + # Check that missing values are cast to float64 + assert np.asarray(result.attrs["missing_value"]).dtype == np.float64 + assert np.asarray(result.attrs["_FillValue"]).dtype == np.float64 + + +@pytest.mark.unit +def test_standardize_missing_values_casts_float16_data(vocabulary_instance): + """Test that less common float types are handled.""" + da = xr.DataArray( + np.array([1.0, 2.0, 3.0], dtype=np.float16), + dims=["x"], + attrs={"units": "K"}, + ) + + result = vocabulary_instance.standardize_missing_values(da, convert_existing=False) + + # For float16 data, missing values should be cast to float16 + assert np.asarray(result.attrs["missing_value"]).dtype == np.float16 + + +@pytest.mark.unit +def test_standardize_missing_values_fallback_on_cast_failure(vocabulary_instance): + """Test that the casting helper returns original value on TypeError.""" + # Create a DataArray with a complex dtype that might cause issues + da = xr.DataArray( + np.array([1 + 2j, 3 + 4j], dtype=np.complex64), + dims=["x"], + attrs={"units": "1"}, + ) + + result = vocabulary_instance.standardize_missing_values(da, convert_existing=False) + + # The casting should fall back gracefully since complex types aren't floating/integer + # The result should have missing_value and _FillValue as floats since they're the defaults + assert "missing_value" in result.attrs or "_FillValue" in result.attrs + + @pytest.mark.unit def test_get_external_variables_cell_measures_and_heuristics(vocabulary_instance): vocabulary_instance.variable = { From f0d5f4bc8a74bac73bf85a74dd71508c02affd83 Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Wed, 24 Jun 2026 16:12:32 +1000 Subject: [PATCH 17/21] Add range validation function and corresponding unit test for tiny negative noise --- src/access_moppy/qc/cmip7.py | 25 +++++++++++++++++++++++-- tests/unit/test_cmip7_qc.py | 13 +++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/access_moppy/qc/cmip7.py b/src/access_moppy/qc/cmip7.py index 4e06fc76..5f9d4406 100644 --- a/src/access_moppy/qc/cmip7.py +++ b/src/access_moppy/qc/cmip7.py @@ -86,6 +86,23 @@ def _mask_missing_sentinels_for_qc(da: xr.DataArray) -> xr.DataArray: return da.where(~mask) +def _is_outside_allowed_range(observed: float, minimum: float, maximum: float) -> bool: + """Check whether an observed value is meaningfully outside a closed range. + + Small floating-point noise at the boundary is ignored so values like + ``-6e-24`` are treated as zero for a lower bound of ``0``. + """ + + tolerance = 1e-12 + lower_violation = observed < minimum and not np.isclose( + observed, minimum, rtol=0.0, atol=tolerance + ) + upper_violation = observed > maximum and not np.isclose( + observed, maximum, rtol=0.0, atol=tolerance + ) + return bool(lower_violation or upper_violation) + + @lru_cache(maxsize=1) def _load_esm16_mapping_variables() -> dict[str, dict[str, Any]]: """Flatten ACCESS-ESM1-6 mapping entries keyed by CMIP variable id.""" @@ -327,7 +344,9 @@ def validate_cmip7_output(output_path: str | Path) -> None: observed_min = float(minimum) observed_max = float(maximum) - if observed_min < rule.minimum or observed_max > rule.maximum: + if _is_outside_allowed_range( + observed_min, rule.minimum, rule.maximum + ) or _is_outside_allowed_range(observed_max, rule.minimum, rule.maximum): raise ValueError( "CMIP7 QC failed for " f"{variable_id} in experiment {experiment_id} using rule {rule.rule_name}: " @@ -427,7 +446,9 @@ def validate_cmip7_output_detailed(output_path: str | Path) -> ValidationResult: observed_min = float(minimum) observed_max = float(maximum) - if observed_min < rule.minimum or observed_max > rule.maximum: + if _is_outside_allowed_range( + observed_min, rule.minimum, rule.maximum + ) or _is_outside_allowed_range(observed_max, rule.minimum, rule.maximum): return ValidationResult( file_path=str(path), passed=False, diff --git a/tests/unit/test_cmip7_qc.py b/tests/unit/test_cmip7_qc.py index cc7fe297..b2fd7015 100644 --- a/tests/unit/test_cmip7_qc.py +++ b/tests/unit/test_cmip7_qc.py @@ -158,6 +158,19 @@ def test_validate_cmip7_output_tas_passes_for_historical_range(tmp_path): validate_cmip7_output(path) +@pytest.mark.unit +def test_validate_cmip7_output_allows_tiny_negative_noise_at_zero_bound(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[-6e-24, 0.5, 10.0], + experiment_id="historical", + variable_id="arag", + units="mol m-3", + ) + + validate_cmip7_output(path) + + @pytest.mark.unit def test_validate_cmip7_output_tas_fails_for_picontrol_range(tmp_path): path = _write_cmip7_output( From 8d951293d75416f51145acaebca34c8d73149fc9 Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Thu, 25 Jun 2026 09:20:23 +1000 Subject: [PATCH 18/21] Update validation function docstrings for clarity on ACCESS mapped variables --- src/access_moppy/qc/cmip7.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/access_moppy/qc/cmip7.py b/src/access_moppy/qc/cmip7.py index 5f9d4406..5942cf24 100644 --- a/src/access_moppy/qc/cmip7.py +++ b/src/access_moppy/qc/cmip7.py @@ -254,7 +254,7 @@ def _validate_esm16_mapping_checks( experiment_id: str, mapping_entry: dict[str, Any], ) -> None: - """Validate generic checks for ACCESS-ESM1-6 mapped variables.""" + """Validate generic checks for ACCESS mapped variables.""" non_missing = int(da.count().item()) if non_missing == 0: @@ -306,7 +306,7 @@ def validate_cmip7_output(output_path: str | Path) -> None: output_variable = _select_output_variable(ds, attrs) da = _mask_missing_sentinels_for_qc(ds[output_variable]) - # Apply generic checks for all variables present in the ACCESS-ESM1-6 + # Apply generic checks for variables present in the bundled ACCESS-ESM1-6 # mapping so every mapped variable receives QC coverage. if source_id == "ACCESS-ESM1-6": mapping_entry = _load_esm16_mapping_variables().get(variable_id) @@ -385,7 +385,7 @@ def validate_cmip7_output_detailed(output_path: str | Path) -> ValidationResult: output_variable = _select_output_variable(ds, attrs) da = _mask_missing_sentinels_for_qc(ds[output_variable]) - # Apply generic checks for all variables present in the ACCESS-ESM1-6 + # Apply generic checks for variables present in the bundled ACCESS-ESM1-6 # mapping so every mapped variable receives QC coverage. if source_id == "ACCESS-ESM1-6": mapping_entry = _load_esm16_mapping_variables().get(variable_id) From 39651b0f50c8b6ea29fdbd2d7fc9e4076cfc2bfe Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Fri, 26 Jun 2026 09:38:01 +1000 Subject: [PATCH 19/21] Add unit tests for range resolution and experiment selection in CMIP7 QC --- tests/unit/test_cmip7_qc.py | 340 ++++++++++++++++++++++++++++++++++++ 1 file changed, 340 insertions(+) diff --git a/tests/unit/test_cmip7_qc.py b/tests/unit/test_cmip7_qc.py index b2fd7015..bc6da252 100644 --- a/tests/unit/test_cmip7_qc.py +++ b/tests/unit/test_cmip7_qc.py @@ -10,8 +10,14 @@ from access_moppy.qc.cmip7 import ( _iter_missing_sentinels, _load_esm16_mapping_variables, + _load_mapping_variable_ranges, _load_rules, + _load_unit_envelopes, _mask_missing_sentinels_for_qc, + _resolve_range_rule, + _resolve_range_rule_from_mapping_definition, + _select_experiment_rule, + _select_output_variable, validate_cmip7_output_detailed, ) from access_moppy.qc.cmip7 import ( @@ -761,3 +767,337 @@ def test_validate_cmip7_output_detailed_reports_unexpected_selection_error(tmp_p assert result.passed is False assert result.error.startswith("Unexpected error: CMIP7 QC could not determine") + + +@pytest.mark.unit +def test_select_experiment_rule_returns_none_when_no_match(): + selected = _select_experiment_rule({"ssp*": {"min": 0, "max": 1}}, "historical") + + assert selected is None + + +@pytest.mark.unit +def test_resolve_range_rule_uses_default_when_no_experiment_match(): + with patch( + "access_moppy.qc.cmip7._load_rules", + return_value={"foo": {"default": {"units": "1", "min": 1, "max": 2}}}, + ): + rule = _resolve_range_rule("foo", "unknown") + + assert rule is not None + assert rule.rule_name == "default" + assert rule.minimum == 1.0 + assert rule.maximum == 2.0 + + +@pytest.mark.unit +def test_resolve_range_rule_returns_none_when_min_or_max_missing(): + with patch( + "access_moppy.qc.cmip7._load_rules", + return_value={"foo": {"default": {"units": "1", "min": 1}}}, + ): + rule = _resolve_range_rule("foo", "historical") + + assert rule is None + + +@pytest.mark.unit +def test_load_unit_envelopes_and_mapping_ranges_defaults_to_empty_dict(): + with patch("access_moppy.qc.cmip7._load_qc_config", return_value={}): + _load_unit_envelopes.cache_clear() + _load_mapping_variable_ranges.cache_clear() + envelopes = _load_unit_envelopes() + ranges = _load_mapping_variable_ranges() + + assert envelopes == {} + assert ranges == {} + + +@pytest.mark.unit +def test_resolve_range_rule_from_mapping_definition_returns_none_without_units(): + with ( + patch("access_moppy.qc.cmip7._load_mapping_variable_ranges", return_value={}), + patch( + "access_moppy.qc.cmip7._load_unit_envelopes", + return_value={"K": {"min": 1, "max": 2}}, + ), + ): + rule = _resolve_range_rule_from_mapping_definition("tas", "historical", {}) + + assert rule is None + + +@pytest.mark.unit +def test_resolve_range_rule_from_mapping_definition_uses_override_range_and_units(): + with ( + patch( + "access_moppy.qc.cmip7._load_mapping_variable_ranges", + return_value={"tas": {"units": "degC", "min": -80, "max": 60}}, + ), + patch("access_moppy.qc.cmip7._load_unit_envelopes", return_value={}), + ): + rule = _resolve_range_rule_from_mapping_definition( + "tas", + "historical", + {"units": "K"}, + ) + + assert rule is not None + assert rule.units == "degC" + assert rule.minimum == -80.0 + assert rule.maximum == 60.0 + + +@pytest.mark.unit +def test_resolve_range_rule_from_mapping_definition_returns_none_when_envelope_missing(): + with ( + patch("access_moppy.qc.cmip7._load_mapping_variable_ranges", return_value={}), + patch("access_moppy.qc.cmip7._load_unit_envelopes", return_value={}), + ): + rule = _resolve_range_rule_from_mapping_definition( + "tas", + "historical", + {"units": "K"}, + ) + + assert rule is None + + +@pytest.mark.unit +def test_resolve_range_rule_from_mapping_definition_uses_unit_envelope_when_no_override_range(): + with ( + patch( + "access_moppy.qc.cmip7._load_mapping_variable_ranges", + return_value={"tas": {"units": "K"}}, + ), + patch( + "access_moppy.qc.cmip7._load_unit_envelopes", + return_value={"K": {"min": 200.0, "max": 340.0}}, + ), + ): + rule = _resolve_range_rule_from_mapping_definition( + "tas", + "historical", + {"units": "K"}, + ) + + assert rule is not None + assert rule.minimum == 200.0 + assert rule.maximum == 340.0 + + +@pytest.mark.unit +def test_select_output_variable_returns_single_non_bounds_variable(): + ds = xr.Dataset( + { + "time_bnds": xr.DataArray(np.zeros((2, 2)), dims=["time", "bnds"]), + "foo": xr.DataArray(np.array([1.0, 2.0]), dims=["time"]), + } + ) + + assert _select_output_variable(ds, attrs={}) == "foo" + + +@pytest.mark.unit +def test_validate_cmip7_output_returns_early_when_rule_unavailable_for_mapping( + tmp_path, +): + path = _write_cmip7_output( + tmp_path, + values=[1.0, 2.0], + experiment_id="historical", + variable_id="unknown_mapped_var", + units="1", + filename="no_rule_mapping.nc", + ) + + with ( + patch("access_moppy.qc.cmip7._resolve_range_rule", return_value=None), + patch( + "access_moppy.qc.cmip7._load_esm16_mapping_variables", + return_value={"unknown_mapped_var": {"units": "1"}}, + ), + patch( + "access_moppy.qc.cmip7._resolve_range_rule_from_mapping_definition", + return_value=None, + ), + ): + validate_cmip7_output(path) + + +@pytest.mark.unit +def test_validate_cmip7_output_raises_units_mismatch_for_explicit_rule(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[280.0, 281.0], + experiment_id="historical", + variable_id="tas", + source_id="ACCESS-CM3", + units="degC", + filename="units_mismatch_simple_rule.nc", + ) + + with pytest.raises(ValueError, match="expected units 'K', found 'degC'"): + validate_cmip7_output(path) + + +@pytest.mark.unit +def test_validate_cmip7_output_returns_for_all_nan_after_masking(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[np.nan, np.nan], + experiment_id="historical", + variable_id="tas", + source_id="ACCESS-CM3", + units="K", + filename="all_nan_non_mapping.nc", + ) + + # Non-ACCESS-ESM1-6 source skips mapping checks, so NaN extrema should + # trigger the early-return branch. + validate_cmip7_output(path) + + +@pytest.mark.unit +def test_validate_cmip7_output_detailed_requires_variable_id(tmp_path): + path = tmp_path / "no_var_id_detailed.nc" + ds = xr.Dataset( + { + "tas": xr.DataArray( + np.array([285.0, 286.0]), + dims=["time"], + attrs={"units": "K"}, + ) + }, + attrs={ + "mip_era": "CMIP7", + "experiment_id": "historical", + "source_id": "ACCESS-ESM1-6", + }, + ) + ds.to_netcdf(path) + + result = validate_cmip7_output_detailed(path) + + assert result.passed is False + assert "variable_id" in (result.error or "") + + +@pytest.mark.unit +def test_validate_cmip7_output_detailed_requires_experiment_id(tmp_path): + path = tmp_path / "no_exp_id_detailed.nc" + ds = xr.Dataset( + { + "tas": xr.DataArray( + np.array([285.0, 286.0]), + dims=["time"], + attrs={"units": "K"}, + ) + }, + attrs={ + "mip_era": "CMIP7", + "variable_id": "tas", + "source_id": "ACCESS-ESM1-6", + }, + ) + ds.to_netcdf(path) + + result = validate_cmip7_output_detailed(path) + + assert result.passed is False + assert "experiment_id" in (result.error or "") + + +@pytest.mark.unit +def test_validate_cmip7_output_detailed_captures_mapping_check_value_error(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[285.0, 286.0], + experiment_id="historical", + variable_id="tas", + source_id="ACCESS-ESM1-6", + units="K", + filename="mapping_error_detailed.nc", + ) + + with patch( + "access_moppy.qc.cmip7._validate_esm16_mapping_checks", + side_effect=ValueError("mapping guard failed"), + ): + result = validate_cmip7_output_detailed(path) + + assert result.passed is False + assert result.error == "mapping guard failed" + + +@pytest.mark.unit +def test_validate_cmip7_output_detailed_uses_mapping_fallback_rule(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[281.0, 282.0], + experiment_id="historical", + variable_id="tas", + source_id="ACCESS-ESM1-6", + units="K", + filename="mapping_fallback_rule_detailed.nc", + ) + + fallback_rule = Mock(units="K", minimum=270.0, maximum=300.0, rule_name="fallback") + + with ( + patch("access_moppy.qc.cmip7._resolve_range_rule", return_value=None), + patch( + "access_moppy.qc.cmip7._load_esm16_mapping_variables", + return_value={"tas": {"units": "K"}}, + ), + patch("access_moppy.qc.cmip7._validate_esm16_mapping_checks"), + patch( + "access_moppy.qc.cmip7._resolve_range_rule_from_mapping_definition", + return_value=fallback_rule, + ) as fallback_mock, + ): + result = validate_cmip7_output_detailed(path) + + assert result.passed is True + assert result.allowed_min == 270.0 + assert result.allowed_max == 300.0 + fallback_mock.assert_called_once() + + +@pytest.mark.unit +def test_validate_cmip7_output_detailed_returns_pass_for_all_nan_non_mapping(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[np.nan, np.nan], + experiment_id="historical", + variable_id="tas", + source_id="ACCESS-CM3", + units="K", + filename="all_nan_non_mapping_detailed.nc", + ) + + result = validate_cmip7_output_detailed(path) + + assert result.passed is True + assert result.units == "K" + + +@pytest.mark.unit +def test_validate_cmip7_output_detailed_reports_range_failure(tmp_path): + path = _write_cmip7_output( + tmp_path, + values=[100.0, 101.0], + experiment_id="historical", + variable_id="tas", + source_id="ACCESS-CM3", + units="K", + filename="range_fail_detailed.nc", + ) + + result = validate_cmip7_output_detailed(path) + + assert result.passed is False + assert result.observed_min == 100.0 + assert result.observed_max == 101.0 + assert result.allowed_min is not None + assert result.allowed_max is not None From 81f4adeb4681de5e29a9fba99851f19a237eb60b Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Fri, 26 Jun 2026 10:32:21 +1000 Subject: [PATCH 20/21] Enhance CMORiser and vocabulary processor: add support for ACCESS-ESM1-6 and improve nominal resolution handling --- src/access_moppy/ocean.py | 7 ++- src/access_moppy/vocabulary_processors.py | 36 ++++++++++++- tests/unit/test_ocean.py | 21 ++++++++ tests/unit/test_vocabulary_processors.py | 64 +++++++++++++++++++++++ 4 files changed, 125 insertions(+), 3 deletions(-) diff --git a/src/access_moppy/ocean.py b/src/access_moppy/ocean.py index 51d5dc23..aa5e3ad9 100644 --- a/src/access_moppy/ocean.py +++ b/src/access_moppy/ocean.py @@ -378,7 +378,12 @@ def infer_grid_type(self): def _get_dim_rename(self): """Get the dimension renaming mapping for the grid type.""" - supported_sources = ["ACCESS-OM2", "ACCESS-CM", "ACCESS-ESM1-5"] + supported_sources = [ + "ACCESS-OM2", + "ACCESS-CM", + "ACCESS-ESM1-5", + "ACCESS-ESM1-6", + ] if self.vocab.source_id in supported_sources: return { "xt_ocean": "i", diff --git a/src/access_moppy/vocabulary_processors.py b/src/access_moppy/vocabulary_processors.py index dd9a77ad..455c9ff6 100644 --- a/src/access_moppy/vocabulary_processors.py +++ b/src/access_moppy/vocabulary_processors.py @@ -1803,9 +1803,41 @@ def _get_variable_frequency(self) -> str: "frequency", self.cmip_table["Header"].get("frequency", "") ) - def _get_nominal_resolution(self) -> Optional[str]: - """Get nominal resolution from source metadata""" + def _get_nominal_resolution( + self, target_realm: Optional[str] = None + ) -> Optional[str]: + """Get nominal resolution from source metadata.""" + if target_realm: + self.target_realm = target_realm + realm = self.variable.get("modeling_realm") + if isinstance(realm, list): + realms = realm + elif isinstance(realm, str): + realms = realm.split() + else: + realms = [] + + if realms and len(realms) > 1: + if target_realm is None: + default_realm = realms[0] + warnings.warn( + f"Variable has multiple modeling realms: '{realms}'. " + f"No 'target_realm' specified, defaulting to '{default_realm}'. " + f"To suppress this warning, explicitly pass target_realm " + f"(one of: {realms})." + ) + realm = default_realm + elif target_realm not in realms: + raise ValueError( + f"target_realm '{target_realm}' not found in variable's modeling realms: '{realms}'. " + f"Must be one of: {realms}." + ) + else: + realm = target_realm + elif realms: + realm = realms[0] + try: model_components = self.source.get("model_component", {}) return model_components.get(realm, {}).get("native_nominal_resolution") diff --git a/tests/unit/test_ocean.py b/tests/unit/test_ocean.py index 576df65c..a5b8714f 100644 --- a/tests/unit/test_ocean.py +++ b/tests/unit/test_ocean.py @@ -120,6 +120,27 @@ def test_get_dim_rename_om2(self, mock_vocab, mock_mapping, temp_dir): assert dim_rename["yu_ocean"] == "j" assert dim_rename["st_ocean"] == "lev" + @pytest.mark.unit + def test_get_dim_rename_accepts_access_esm1_6( + self, mock_vocab, mock_mapping, temp_dir + ): + """ACCESS-ESM1-6 uses the OM2/MOM5 ocean grid conventions.""" + mock_vocab.source_id = "ACCESS-ESM1-6" + + with patch("access_moppy.ocean.Supergrid"): + cmoriser = Ocean_CMORiser_OM2( + input_paths=["test.nc"], + output_path=str(temp_dir), + compound_name="Omon.tos", + vocab=mock_vocab, + variable_mapping=mock_mapping, + ) + + dim_rename = cmoriser._get_dim_rename() + + assert dim_rename["xt_ocean"] == "i" + assert dim_rename["yt_ocean"] == "j" + @pytest.mark.unit def test_arakawa_grid_type(self, mock_vocab, mock_mapping, temp_dir): """Test that ACCESS-OM2 uses B-grid (Arakawa B).""" diff --git a/tests/unit/test_vocabulary_processors.py b/tests/unit/test_vocabulary_processors.py index c98ebaaa..4f9d53fc 100644 --- a/tests/unit/test_vocabulary_processors.py +++ b/tests/unit/test_vocabulary_processors.py @@ -1355,6 +1355,70 @@ def test_get_nominal_resolution_multiple_realms_target_missing_key( assert vocab._get_nominal_resolution(target_realm="atmos") is None +@pytest.mark.unit +def test_cmip7_get_nominal_resolution_multiple_realms_valid_target(): + """CMIP7 supports selecting native nominal resolution by target realm.""" + mock_cv = { + "experiment_id": { + "historical": { + "experiment": "historical", + "activity": ["CMIP"], + } + }, + "source_id": { + "ACCESS-ESM1-6": { + "institution_id": ["CSIRO"], + "license_info": {"id": "CC BY 4.0"}, + "release_year": "2021", + "model_component": { + "atmos": {"native_nominal_resolution": "100 km"}, + "ocean": {"native_nominal_resolution": "50 km"}, + }, + } + }, + } + mock_table = { + "Header": {"table_id": "ocean"}, + "variable_entry": { + "tos": { + "frequency": "mon", + "modeling_realm": "atmos ocean", + "units": "degC", + "type": "real", + "dimensions": ["longitude", "latitude", "time"], + } + }, + } + + with ( + patch.object( + CMIP7Vocabulary, + "_get_experiment", + return_value=mock_cv["experiment_id"]["historical"], + ), + patch.object( + CMIP7Vocabulary, + "_get_source", + return_value=mock_cv["source_id"]["ACCESS-ESM1-6"], + ), + patch.object( + CMIP7Vocabulary, + "_get_variable_entry", + return_value=mock_table["variable_entry"]["tos"], + ), + patch.object(CMIP7Vocabulary, "_load_table", return_value=mock_table), + ): + vocab = CMIP7Vocabulary( + compound_name="ocean.tos", + experiment_id="historical", + source_id="ACCESS-ESM1-6", + variant_label="r1i1p1f1", + grid_label="gn", + ) + + assert vocab._get_nominal_resolution(target_realm="ocean") == "50 km" + + # --------------------------------------------------------------------------- # Error message context: _get_experiment / _get_source / _load_table # --------------------------------------------------------------------------- From 8972eefac2dde95848c8221bd0db7996f62f2826 Mon Sep 17 00:00:00 2001 From: Romain Beucher Date: Fri, 26 Jun 2026 11:04:11 +1000 Subject: [PATCH 21/21] Switch on validation for Ocean variables --- tests/integration/test_full_cmorisation.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/integration/test_full_cmorisation.py b/tests/integration/test_full_cmorisation.py index b73ef3c1..e60f125e 100644 --- a/tests/integration/test_full_cmorisation.py +++ b/tests/integration/test_full_cmorisation.py @@ -394,17 +394,13 @@ def test_cmorisation_variable( output_files ), f"No output files found for {variable_name} in {output_dir}" - # Validate output with the configured backend - # Skip compliance validation for Omon and Ofx (ocean fixed fields - # use non-standard grid structures not validated by PrePARE/WCRP) - if compound_table not in ("Omon", "Ofx"): - self._validate_output_compliance( - output_files[0], - variable_name, - table_path, - cmip_version, - compliance_validation_tool, - ) + self._validate_output_compliance( + output_files[0], + variable_name, + table_path, + cmip_version, + compliance_validation_tool, + ) except Exception as e: pytest.fail(