diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index a9b831f..7158315 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -6,8 +6,21 @@ on: jobs: tests: - name: Run Pytest - runs-on: ubuntu-latest + name: ${{ matrix.os }} / ${{ matrix.versions }} versions + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + versions: [latest, floor] + include: + # the ranges in isschecker_env.yml, solved fresh + - versions: latest + environment-file: isschecker_env.yml + # every floor in isschecker_env.yml pinned exactly + - versions: floor + environment-file: ci/isschecker_env_floor.yml steps: - name: Check out repository @@ -16,13 +29,38 @@ jobs: - name: Set up micromamba environment uses: mamba-org/setup-micromamba@v2 with: - environment-file: isschecker_env.yml + environment-file: ${{ matrix.environment-file }} environment-name: isschecker - cache-environment: true + # Caching a solved environment would hide the day a new release + # breaks the open end of a range, so only the floor -- which is + # pinned, and so cannot drift -- is cached. + cache-environment: ${{ matrix.versions == 'floor' }} cache-downloads: true init-shell: >- bash + - name: Install isschecker + shell: micromamba-shell {0} + # The same flags the README gives modelers, so CI exercises the + # documented install rather than a different one. + run: python -m pip install --no-deps --no-build-isolation . + + - name: Report the installed versions + shell: micromamba-shell {0} + run: micromamba list + + - name: Check the entry points + shell: micromamba-shell {0} + working-directory: ${{ runner.temp }} + run: | + ismip7-compliance-checker --version + python -m isschecker --version + ismip7-generate-test-files --list-grids + - name: Run test suite shell: micromamba-shell {0} - run: pytest -v tests/test_compliance_checker.py \ No newline at end of file + # Run from outside the checkout so the tests import the installed + # package: a data file or entry point that did not make it into the + # wheel then fails here instead of passing against the source tree. + working-directory: ${{ runner.temp }} + run: pytest -v "$GITHUB_WORKSPACE/tests/test_compliance_checker.py" diff --git a/.gitignore b/.gitignore index 375de91..2c62f44 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ __pycache__ .pytest_cache +build/ +dist/ +*.egg-info/ +*.whl +.venv/ diff --git a/README.md b/README.md index f8aa5f2..bca08fe 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Checks ISMIP7 NetCDF simulation datasets for compliance with the [ISMIP7 data re 4. **Time** — time dimension is present, unlimited, and monotonically increasing; annual cadence for `x,y,t` and `t` variables; sparse snapshot timestamps for `x,y,z,t` variables (see [Time encoding](#time-encoding)); experiment start/end dates and duration match `experiments_ismip7.csv` for `x,y,t` and `t` variables. 5. **Attributes** — required global and coordinate attributes are present and have correct values; `standard_name` matches data request; `_FillValue` equals the NetCDF4 default for the variable's dtype; variable and time are float32; `scale_factor` and `add_offset` are not allowed. -Compliance criteria are defined in `conventions/ISMIP7_variable_request.csv` (variable metadata) and `experiments_ismip7.csv` (valid experiment year ranges and durations). +Compliance criteria are defined in `isschecker/data/ISMIP7_variable_request.csv` (variable metadata) and `isschecker/data/experiments_ismip7.csv` (valid experiment year ranges and durations). Together with the grid definitions in `isschecker/data/gdfs/`, these files are bundled with the package. --- @@ -44,34 +44,66 @@ Reference lookup tables are available in the companion repository [`ismip7-time- ## Setup +Create the conda environment and install the package: + ```bash conda env create -f isschecker_env.yml conda activate isschecker +python -m pip install --no-deps --no-build-isolation . +``` + +Installing the package registers the `ismip7-compliance-checker` command and bundles the data files, so the checker can be run from any directory. + +**Use those pip flags.** All dependencies come from conda-forge, and a plain `pip install .` can silently replace them with PyPI wheels — `netCDF4` in particular bundles its own copy of the netCDF C library — which is exactly how two people end up with different results from the same files. `--no-deps` keeps pip from resolving anything, and `--no-build-isolation` builds with the environment's `setuptools` instead of downloading one from PyPI. Add `--no-index` if you want any accidental network fetch to fail loudly rather than succeed quietly. + +For development, add `-e` for an editable install: + +```bash +python -m pip install --no-deps --no-build-isolation -e . ``` -Dependencies: Python 3.14, `numpy` 2.4, `pandas` 3.0, `xarray` 2026.4, `cftime` 1.6, `netCDF4` 1.7, `tqdm` 4.67. +(`pytest` comes from the conda environment, so the `[test]` extra is not needed.) An editable install is worth having while developing, because the tests import the installed package: after a non-editable install, edits to the source tree do not affect a test run until you reinstall. + +If a rebuild ever behaves as though it were still running older code, delete the `build/` directory: `setuptools` reuses its contents, so files that have since been renamed or removed can otherwise end up back in the installed package. + +### Dependencies + +Versions are constrained in `isschecker_env.yml`; the same constraints appear in `pyproject.toml`. The suite is tested at both ends of every range, so results should agree across machines and operating systems within these bounds. + +| Package | Constraint | Why bounded | +|---|---|---| +| `python` | `>=3.11,<3.15` | `str \| None` annotations need ≥3.10; 3.10 is EOL in Oct 2026 | +| `numpy` | `>=2.1,<3` | what recent `pandas`/`xarray` are built against | +| `pandas` | `>=2.2,<4` | reads the criteria CSVs; 3.0 changed the default string dtype | +| `xarray` | `>=2025.1.2,<2027` | `xarray.coders.CFDatetimeCoder` (public API in 2025.1.1) and non-nanosecond datetime decoding, both used by the time checks | +| `cftime` | `>=1.6.4,<2` | date arithmetic in the start/end/duration checks | +| `netCDF4` | `>=1.7,<2` | `_FillValue` checks compare against `netCDF4.default_fillvals` | +| `tqdm` | `>=4.66` | progress bar only; never affects the log | + +If you report a problem with the checker, please include the output of `conda list` for your `isschecker` environment. --- ## Running the checker -The script must be run from the repository root. It writes `compliance_checker_log.txt` into the `--source-path` directory. +Once installed, run the checker from any directory with the `ismip7-compliance-checker` command (equivalently, `python -m isschecker`). It writes `compliance_checker_log.txt` into the `--source-path` directory. ```bash # Check x,y,t (3D spatial) variables -python compliance_checker.py --source-path ./Models/GrIS/ISMIP7/SYNTH1/CORE/C001 --variable-list ismip7_xyt +ismip7-compliance-checker --source-path ./Models/GrIS/ISMIP7/SYNTH1/CORE/C001 --variable-list ismip7_xyt # Check scalar (time-only) variables -python compliance_checker.py --source-path ./Models/AIS/ISMIP7/SYNTH1/CORE/C001 --variable-list ismip7_scalars +ismip7-compliance-checker --source-path ./Models/AIS/ISMIP7/SYNTH1/CORE/C001 --variable-list ismip7_scalars # Check both -python compliance_checker.py --source-path ./Models/GrIS/ISMIP7/SYNTH1/CORE/C001 --variable-list ismip7 +ismip7-compliance-checker --source-path ./Models/GrIS/ISMIP7/SYNTH1/CORE/C001 --variable-list ismip7 ``` | Option | Default | Description | |--------|---------|-------------| | `--source-path` | `./Models/GrIS/ISMIP7/SYNTH1/CORE/C001` | Set-counter subdirectory containing `.nc` files to check | | `--variable-list` | `ismip7_scalars` | `ismip7_xyt`, `ismip7_scalars`, or `ismip7` (both) | +| `--version` | — | Print the installed version and exit; quote it when reporting a problem | `experiments_ismip7.csv` defines the allowed nominal year ranges and durations for each experiment. The checker derives the expected FL and ST timestamps from these year values at runtime (see [Time encoding](#time-encoding)). @@ -79,26 +111,26 @@ python compliance_checker.py --source-path ./Models/GrIS/ISMIP7/SYNTH1/CORE/C001 ## Generating synthetic test files -`generate/generate_test_files.py` creates ISMIP7-style NetCDF test files with synthetic data. See [generate/README.md](generate/README.md) for full options and examples. +The `ismip7-generate-test-files` command creates ISMIP7-style NetCDF test files with synthetic data, under `Models/` in the current directory. See [docs/generating_test_files.md](docs/generating_test_files.md) for full options and examples. ```bash conda activate isschecker # Generate 286-year GrIS ctrl xyt variables -python generate/generate_test_files.py --grid GrIS_16000m --scenario ctrl --xyt --nyears 286 --start-year 2015 +ismip7-generate-test-files --grid GrIS_16000m --scenario ctrl --xyt --nyears 286 --start-year 2015 # Generate 286-year AIS ctrl scalar variables -python generate/generate_test_files.py --grid AIS_16000m --scenario ctrl --scalars --nyears 286 --start-year 2015 +ismip7-generate-test-files --grid AIS_16000m --scenario ctrl --scalars --nyears 286 --start-year 2015 # List available grids -python generate/generate_test_files.py --list-grids +ismip7-generate-test-files --list-grids ``` --- ## Running Tests -The regression suite uses `pytest` and creates temporary synthetic datasets, then mutates them to verify expected checker failures for naming, missing variables, time-axis problems, and missing attributes. +The regression suite uses `pytest` and creates temporary synthetic datasets, then mutates them to verify expected checker failures for naming, missing variables, time-axis problems, and missing attributes. It imports `isschecker`, so install the package first (see [Setup](#setup)); the tests then exercise what is actually installed and can be run from any directory. ```bash pytest -v tests/test_compliance_checker.py @@ -109,3 +141,13 @@ If you want to retain the files generated during testing you can use: pytest -v tests/test_compliance_checker.py --basetemp=/tmp/pytest_tmp ``` The files will then be left in `/tmp/pytest_tmp`. Otherwise, they are cleaned up once tests pass. + +### The reference log + +`tests/test_golden_log.py` runs the checker over a fixed, seeded dataset and compares the resulting log line by line against `tests/reference/compliance_checker_log.txt`, with the version, date, and source path masked out. It is what turns "results should agree across machines" into something CI can check: any difference in log text fails the test, whether it comes from a change of ours or from a dependency release inside the supported version ranges. + +When a change to the checker is *meant* to change the log, read the diff the failure prints, then regenerate the reference and commit it alongside the change: + +```bash +ISSCHECKER_UPDATE_GOLDEN_LOG=1 pytest tests/test_golden_log.py +``` diff --git a/ci/isschecker_env_floor.yml b/ci/isschecker_env_floor.yml new file mode 100644 index 0000000..d160d47 --- /dev/null +++ b/ci/isschecker_env_floor.yml @@ -0,0 +1,26 @@ +# The oldest dependency versions the checker supports: every floor in +# isschecker_env.yml pinned exactly. CI runs the test suite against this +# environment as well as the latest solve, so both ends of every range in +# isschecker_env.yml are actually verified rather than assumed. +# +# Keep this file in step with isschecker_env.yml: when a floor moves there, move +# it here too. +name: isschecker +channels: + - conda-forge + - nodefaults + +dependencies: + - python =3.11 + - pip + - setuptools >=77 + + - numpy =2.1 + - pandas =2.2 + - tqdm =4.66 + + - xarray =2025.1.2 + - cftime =1.6.4 + - netcdf4 =1.7 + + - pytest =8 diff --git a/generate/README.md b/docs/generating_test_files.md similarity index 66% rename from generate/README.md rename to docs/generating_test_files.md index ea7b15b..3b5679d 100644 --- a/generate/README.md +++ b/docs/generating_test_files.md @@ -1,14 +1,14 @@ # ISMIP7 NetCDF generator -`generate/generate_test_files.py` creates ISMIP7-style NetCDF test files with synthetic data, one file per variable, following the naming convention and grid definitions used by the compliance checker. +`ismip7-generate-test-files` creates ISMIP7-style NetCDF test files with synthetic data, one file per variable, following the naming convention and grid definitions used by the compliance checker. It is part of the `isschecker` package (`isschecker.generate`), so it is installed along with the checker and can be run from any directory. -Files are written to `Models/{GrIS|AIS}/ISMIP7/SYNTH1/CORE/{set_counter}/` (default `C001`). +Files are written to `Models/{GrIS|AIS}/ISMIP7/SYNTH1/CORE/{set_counter}/` (default `C001`) beneath the current working directory. ## Usage ```bash conda activate isschecker -python generate/generate_test_files.py [OPTIONS] +ismip7-generate-test-files [OPTIONS] ``` ## Key options @@ -26,38 +26,39 @@ python generate/generate_test_files.py [OPTIONS] | `--esm-id` | `CESM2-WACCM` | ESM id (written into filenames) | | `--forcing-member-id` | `f001` | Forcing ensemble member id (written into filenames) | | `--set-counter` | `C001` | Set counter id (written into filenames) | +| `--seed` | `0` | Seed for the synthetic data; the same seed always produces the same values | | `--list-grids` | — | List all available grids and exit | ## Examples ```bash # List available grids -python generate/generate_test_files.py --list-grids +ismip7-generate-test-files --list-grids # Generate 286-year GrIS ctrl files (x,y,t variables) -python generate/generate_test_files.py --grid GrIS_16000m --scenario ctrl \ +ismip7-generate-test-files --grid GrIS_16000m --scenario ctrl \ --xyt --nyears 286 --start-year 2015 # Generate 286-year AIS ssp370 files -python generate/generate_test_files.py --grid AIS_08000m --scenario ssp370 \ +ismip7-generate-test-files --grid AIS_08000m --scenario ssp370 \ --xyt --nyears 286 --start-year 2015 # Generate scalar-only variables -python generate/generate_test_files.py --grid GrIS_16000m --scenario ctrl \ +ismip7-generate-test-files --grid GrIS_16000m --scenario ctrl \ --scalars --nyears 286 --start-year 2015 # Generate both 3D and scalar variables, including non-mandatory ones -python generate/generate_test_files.py --grid GrIS_16000m --scenario ctrl \ +ismip7-generate-test-files --grid GrIS_16000m --scenario ctrl \ --xyt --scalars --include-non-mandatory --nyears 286 --start-year 2015 # Generate testdata for ismip7-scalar-processing -python generate/generate_test_files.py --grid AIS_16000m --scenario historical \ +ismip7-generate-test-files --grid AIS_16000m --scenario historical \ --set-counter C001 --xyt --include-non-mandatory --nyears 1 --start-year 2014 -python generate/generate_test_files.py --grid AIS_16000m --scenario ssp585 \ +ismip7-generate-test-files --grid AIS_16000m --scenario ssp585 \ --set-counter C007 --xyt --include-non-mandatory --nyears 286 --start-year 2015 -python generate/generate_test_files.py --grid GrIS_16000m --scenario historical \ +ismip7-generate-test-files --grid GrIS_16000m --scenario historical \ --set-counter C001 --xyt --include-non-mandatory --nyears 55 --start-year 1960 -python generate/generate_test_files.py --grid GrIS_16000m --scenario ctrl \ +ismip7-generate-test-files --grid GrIS_16000m --scenario ctrl \ --set-counter C009 --xyt --include-non-mandatory --nyears 286 --start-year 2015 ``` @@ -76,6 +77,7 @@ python generate/generate_test_files.py --grid GrIS_16000m --scenario ctrl \ ## Notes -- Variable metadata is read from `conventions/ISMIP7_variable_request.csv`; grid definitions from the top-level `gdfs/` directory. +- Variable metadata is read from `ISMIP7_variable_request.csv` and grid definitions from `gdfs/`, both bundled as package data in `isschecker/data/`. Use `--conventions-dir` to point at a different directory (it must contain the CSV and a `gdfs/` subdirectory). - `group`, `model`, `contact_name`, and `contact_email` are hardcoded in `create_netcdf_file()` to synthetic defaults — edit there to customise. - Generated files are synthetic and intended for testing the compliance checker, not for scientific use. +- Data values are drawn from a seeded generator, so a given `--seed` reproduces the same files. With `--multiple`, each file uses `seed + i` so the files differ from one another while the run as a whole stays reproducible. diff --git a/isschecker/__init__.py b/isschecker/__init__.py new file mode 100644 index 0000000..5088a5e --- /dev/null +++ b/isschecker/__init__.py @@ -0,0 +1,5 @@ +"""ISMIP7 compliance checker for ice sheet model simulation NetCDF datasets.""" + +from isschecker.checker import __version__, main, run_checker + +__all__ = ["__version__", "main", "run_checker"] diff --git a/isschecker/__main__.py b/isschecker/__main__.py new file mode 100644 index 0000000..10b1b5b --- /dev/null +++ b/isschecker/__main__.py @@ -0,0 +1,6 @@ +"""Entry point for ``python -m isschecker``.""" + +from isschecker import main + +if __name__ == "__main__": + main() diff --git a/compliance_checker.py b/isschecker/checker.py old mode 100755 new mode 100644 similarity index 95% rename from compliance_checker.py rename to isschecker/checker.py index 44a149c..ece0a73 --- a/compliance_checker.py +++ b/isschecker/checker.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # # ISMIP7 Compliance Checker — check summary # @@ -42,6 +41,7 @@ import re import subprocess import argparse +from importlib import metadata, resources import numpy as np import pandas as pd @@ -50,14 +50,30 @@ from tqdm import tqdm +try: + __version__ = metadata.version("isschecker") +except metadata.PackageNotFoundError: + # Running from a source tree that has not been installed. + __version__ = "unknown" + DEFAULT_SOURCE_PATH = "./Models/GrIS/ISMIP7/SYNTH1/CORE/C001" DEFAULT_VARIABLE_LIST = "ismip7_scalars" VARIABLE_LIST_CHOICES = ("ismip7_scalars", "ismip7_xyt", "ismip7") -VARIABLE_REQUEST_CSV = os.path.join("conventions", "ISMIP7_variable_request.csv") +VARIABLE_REQUEST_CSV = "ISMIP7_variable_request.csv" EXPERIMENTS_ISMIP7_CSV_FILENAME = "experiments_ismip7.csv" + +DATA_PACKAGE = f"{__package__}.data" + + +def _read_data_csv(filename: str, **kwargs) -> pd.DataFrame: + """Read a CSV bundled as package data in :data:`DATA_PACKAGE`.""" + resource = resources.files(DATA_PACKAGE).joinpath(filename) + with resources.as_file(resource) as path: + return pd.read_csv(path, **kwargs) + AIS_GRID_EXTENT = [-3040000, -3040000, 3040000, 3040000] GrIS_GRID_EXTENT = [-720000, -3450000, 960000, -570000] AIS_POSSIBLE_RESOLUTION = [2, 4, 8, 16, 32] @@ -138,26 +154,21 @@ def main() -> None: run_checker( source_path=source_path, variable_list=variable_list, - workdir=os.getcwd(), ) def run_checker( source_path: str, variable_list: str = DEFAULT_VARIABLE_LIST, - workdir: str | None = None, - commit_num: str | None = None, + version: str | None = None, ): - workdir = os.path.abspath(workdir or os.getcwd()) - commit_num = _get_commit_number() if commit_num is None else commit_num - experiments_ismip7 = _load_experiments_csv( - os.path.join(workdir, EXPERIMENTS_ISMIP7_CSV_FILENAME) - ) - ismip_meta, ismip_var, mandatory_variables = _load_criteria(workdir, variable_list) + version = _describe_version() if version is None else version + experiments_ismip7 = _load_experiments_csv() + ismip_meta, ismip_var, mandatory_variables = _load_criteria(variable_list) summary = _run_compliance_checker( source_path=source_path, - commit_num=commit_num, + version=version, ismip_meta=ismip_meta, ismip_var=ismip_var, mandatory_variables=mandatory_variables, @@ -176,21 +187,50 @@ def run_checker( return summary -def _get_commit_number() -> str: +def _describe_version() -> str: + """Describe which checker produced a log. + + The installed version, plus the git commit when the package is being run + from a checkout (a source or editable install). + """ + commit = _git_commit() + return __version__ if commit is None else f"{__version__} (git {commit})" + + +def _git_commit() -> str | None: + """Return the short git commit of the checkout this module lives in. + + Deliberately keyed to the location of this file rather than the working + directory: the log records which checker ran, not which repository the user + happened to be standing in. Returns None for a non-editable install, where + there is no checkout to describe. + """ + package_dir = os.path.dirname(os.path.abspath(__file__)) try: - bash_command = "git log --pretty=format:'%h' -n 1" - process = subprocess.Popen(bash_command.split(), stdout=subprocess.PIPE) - commit_num, _error = process.communicate() - return commit_num.decode("UTF-8") - except Exception: - print("Could not retrieve git commit number. Is there a .git directory here?") - return "No commit number identified." + process = subprocess.run( + ["git", "-C", package_dir, "log", "--pretty=format:%h", "-n", "1"], + capture_output=True, + text=True, + check=False, + ) + except OSError: + # git is not installed + return None + if process.returncode != 0: + return None + return process.stdout.strip() or None def _parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Check simulation NetCDF datasets for ISMIP compliance." ) + parser.add_argument( + "--version", + action="version", + version=f"%(prog)s {__version__}", + help="Show the installed isschecker version and exit.", + ) parser.add_argument( "--source-path", default=DEFAULT_SOURCE_PATH, @@ -205,14 +245,14 @@ def _parse_args() -> argparse.Namespace: return parser.parse_args() -def _load_criteria(workdir: str, variable_list: str): - csv_path = os.path.join(workdir, VARIABLE_REQUEST_CSV) +def _load_criteria(variable_list: str): try: - df = pd.read_csv(csv_path) - except IOError: + df = _read_data_csv(VARIABLE_REQUEST_CSV) + except (IOError, ModuleNotFoundError): print( - "ERROR: Unable to open the variable request file. Is the path correct? " - + csv_path + "ERROR: Unable to open the variable request file " + + VARIABLE_REQUEST_CSV + + f" bundled in {DATA_PACKAGE}. Is the package installed correctly?" ) raise @@ -248,9 +288,9 @@ def _load_criteria(workdir: str, variable_list: str): return ismip_meta, ismip_var, ismip_mandatory_var -def _load_experiments_csv(file_path: str): +def _load_experiments_csv(filename: str = EXPERIMENTS_ISMIP7_CSV_FILENAME): experiments = [] - frame = pd.read_csv(file_path, delimiter=";") + frame = _read_data_csv(filename, delimiter=";") for _, row in frame.iterrows(): experiments.append( { @@ -277,7 +317,7 @@ def _nominal_to_timestamp(year: int, var_type: str) -> datetime.datetime: def _run_compliance_checker( source_path: str, - commit_num: str, + version: str, ismip_meta, ismip_var, mandatory_variables, @@ -293,7 +333,7 @@ def _run_compliance_checker( print("-> Checking " + source_path) print() today = datetime.date.today() - _write_log_header(f, commit_num, source_path, today, criteria_file) + _write_log_header(f, version, source_path, today, criteria_file) experiment_groups = _group_files_by_experiment(source_path) if not experiment_groups: @@ -1425,7 +1465,7 @@ def _strictly_increasing(values) -> bool: def _write_log_header( - log_file, commit_num: str, source_path: str, today: datetime.date, criteria_file: str, + log_file, version: str, source_path: str, today: datetime.date, criteria_file: str, ) -> None: log_file.write( "************************************************************************************\n" @@ -1436,7 +1476,7 @@ def _write_log_header( log_file.write( "************************************************************************************\n" ) - log_file.write(f"Commit Number: {commit_num} \n") + log_file.write(f"isschecker version: {version} \n") log_file.write("verification criteria: " + criteria_file + "\n") log_file.write("date: " + today.strftime("%Y/%m/%d") + "\n") log_file.write("source: https://github.com/ismip/ISM_SimulationChecker \n") @@ -1511,7 +1551,3 @@ def _insert_synthesis( with open(os.path.join(source_path, "compliance_checker_log.txt"), "w") as f: f.writelines(contents) - - -if __name__ == "__main__": - main() diff --git a/conventions/ISMIP7_variable_request.csv b/isschecker/data/ISMIP7_variable_request.csv similarity index 100% rename from conventions/ISMIP7_variable_request.csv rename to isschecker/data/ISMIP7_variable_request.csv diff --git a/isschecker/data/__init__.py b/isschecker/data/__init__.py new file mode 100644 index 0000000..18645a0 --- /dev/null +++ b/isschecker/data/__init__.py @@ -0,0 +1,6 @@ +"""Compliance criteria shipped with the checker. + +This module exists so that the directory is a regular package rather than a +namespace package: ``importlib.resources.files()`` then resolves it to a single +concrete directory instead of a ``MultiplexedPath``. +""" diff --git a/experiments_ismip7.csv b/isschecker/data/experiments_ismip7.csv similarity index 100% rename from experiments_ismip7.csv rename to isschecker/data/experiments_ismip7.csv diff --git a/gdfs/gdf_ISMIP7_AIS_02000m.txt b/isschecker/data/gdfs/gdf_ISMIP7_AIS_02000m.txt similarity index 100% rename from gdfs/gdf_ISMIP7_AIS_02000m.txt rename to isschecker/data/gdfs/gdf_ISMIP7_AIS_02000m.txt diff --git a/gdfs/gdf_ISMIP7_AIS_04000m.txt b/isschecker/data/gdfs/gdf_ISMIP7_AIS_04000m.txt similarity index 100% rename from gdfs/gdf_ISMIP7_AIS_04000m.txt rename to isschecker/data/gdfs/gdf_ISMIP7_AIS_04000m.txt diff --git a/gdfs/gdf_ISMIP7_AIS_08000m.txt b/isschecker/data/gdfs/gdf_ISMIP7_AIS_08000m.txt similarity index 100% rename from gdfs/gdf_ISMIP7_AIS_08000m.txt rename to isschecker/data/gdfs/gdf_ISMIP7_AIS_08000m.txt diff --git a/gdfs/gdf_ISMIP7_AIS_16000m.txt b/isschecker/data/gdfs/gdf_ISMIP7_AIS_16000m.txt similarity index 100% rename from gdfs/gdf_ISMIP7_AIS_16000m.txt rename to isschecker/data/gdfs/gdf_ISMIP7_AIS_16000m.txt diff --git a/gdfs/gdf_ISMIP7_AIS_32000m.txt b/isschecker/data/gdfs/gdf_ISMIP7_AIS_32000m.txt similarity index 100% rename from gdfs/gdf_ISMIP7_AIS_32000m.txt rename to isschecker/data/gdfs/gdf_ISMIP7_AIS_32000m.txt diff --git a/gdfs/gdf_ISMIP7_GrIS_01000m.txt b/isschecker/data/gdfs/gdf_ISMIP7_GrIS_01000m.txt similarity index 100% rename from gdfs/gdf_ISMIP7_GrIS_01000m.txt rename to isschecker/data/gdfs/gdf_ISMIP7_GrIS_01000m.txt diff --git a/gdfs/gdf_ISMIP7_GrIS_02000m.txt b/isschecker/data/gdfs/gdf_ISMIP7_GrIS_02000m.txt similarity index 100% rename from gdfs/gdf_ISMIP7_GrIS_02000m.txt rename to isschecker/data/gdfs/gdf_ISMIP7_GrIS_02000m.txt diff --git a/gdfs/gdf_ISMIP7_GrIS_04000m.txt b/isschecker/data/gdfs/gdf_ISMIP7_GrIS_04000m.txt similarity index 100% rename from gdfs/gdf_ISMIP7_GrIS_04000m.txt rename to isschecker/data/gdfs/gdf_ISMIP7_GrIS_04000m.txt diff --git a/gdfs/gdf_ISMIP7_GrIS_05000m.txt b/isschecker/data/gdfs/gdf_ISMIP7_GrIS_05000m.txt similarity index 100% rename from gdfs/gdf_ISMIP7_GrIS_05000m.txt rename to isschecker/data/gdfs/gdf_ISMIP7_GrIS_05000m.txt diff --git a/gdfs/gdf_ISMIP7_GrIS_08000m.txt b/isschecker/data/gdfs/gdf_ISMIP7_GrIS_08000m.txt similarity index 100% rename from gdfs/gdf_ISMIP7_GrIS_08000m.txt rename to isschecker/data/gdfs/gdf_ISMIP7_GrIS_08000m.txt diff --git a/gdfs/gdf_ISMIP7_GrIS_16000m.txt b/isschecker/data/gdfs/gdf_ISMIP7_GrIS_16000m.txt similarity index 100% rename from gdfs/gdf_ISMIP7_GrIS_16000m.txt rename to isschecker/data/gdfs/gdf_ISMIP7_GrIS_16000m.txt diff --git a/generate/generate_test_files.py b/isschecker/generate.py similarity index 92% rename from generate/generate_test_files.py rename to isschecker/generate.py index f05f1f8..058a360 100644 --- a/generate/generate_test_files.py +++ b/isschecker/generate.py @@ -1,34 +1,54 @@ -#!/usr/bin/env python3 """ NetCDF file generator for ISMIP7 ice sheet simulation data. -This script generates NetCDF files with variables and metadata following -ISMIP7 conventions as defined in the criteria CSV files and grid definitions. +Generates NetCDF files with variables and metadata following ISMIP7 +conventions as defined in the criteria CSV files and grid definitions, both of +which ship as package data alongside the checker. """ +import argparse import re +from importlib import resources import numpy as np import xarray as xr from datetime import datetime from pathlib import Path import netCDF4 +DATA_PACKAGE = f'{__package__}.data' -def get_available_grids(conventions_dir): +# Synthetic data is drawn from a seeded generator so that a given seed always +# produces the same files: reproducible test data, and no test that can drift +# across a numerical bound from one run to the next. +DEFAULT_SEED = 0 + + +def data_dir() -> Path: + """Return the bundled directory of criteria CSVs and `gdfs` grid definitions. + + The package is always installed as a directory rather than a zip, so the + resource is a real path on disk. + """ + return Path(str(resources.files(DATA_PACKAGE))) + + +def get_available_grids(conventions_dir=None): """ - Get available grid definitions from gdfs directory. + Get available grid definitions from the `gdfs` directory. Parameters ---------- - conventions_dir : str - Path to conventions directory + conventions_dir : str, optional + Path to conventions directory (default: the bundled package data) Returns ------- dict Dictionary with grid info: {'GrIS': [...], 'AIS': [...]} """ - # Grid definitions moved to project root `gdfs` directory - gdf_dir = Path(conventions_dir).parent / 'gdfs' + if conventions_dir is None: + conventions_dir = data_dir() + gdf_dir = Path(conventions_dir) / 'gdfs' + grids = {'GrIS': [], 'AIS': []} if not gdf_dir.exists(): @@ -159,7 +179,8 @@ def read_variable_criteria(csv_file, include_non_mandatory=False): return variables -def generate_synthetic_data(shape, min_val, max_val, dtype=np.float32, eps=1e-6): +def generate_synthetic_data(shape, min_val, max_val, dtype=np.float32, eps=1e-6, + rng=None): """ Generate synthetic data within specified range. @@ -176,21 +197,27 @@ def generate_synthetic_data(shape, min_val, max_val, dtype=np.float32, eps=1e-6) eps : float, optional A small fraction by which `min_val` and `max_val` move toward one another to avoid synthetic data that violates numerical checks because of roundoff. + rng : np.random.Generator, optional + Random number generator to draw from (default: a fresh generator seeded + with `DEFAULT_SEED`) Returns ------- np.ndarray Random data within the specified range """ + if rng is None: + rng = np.random.default_rng(DEFAULT_SEED) delta = eps * (max_val - min_val) - data = np.random.uniform(min_val + delta, max_val - delta, shape).astype(dtype) + data = rng.uniform(min_val + delta, max_val - delta, shape).astype(dtype) return data def create_netcdf_file(output_file, grid_name='GrIS_16000m', scenario='ctrl', start_year=2015, nyears=5, conventions_dir=None, include_non_mandatory=False, include_scalars=False, include_xyt=False, output_root=None, nz=5, - ism_member_id='m001', esm_id='CESM2-WACCM', forcing_member_id='f001', set_counter='C001'): + ism_member_id='m001', esm_id='CESM2-WACCM', forcing_member_id='f001', set_counter='C001', + seed=DEFAULT_SEED): """ Create NetCDF files with ISMIP7 variables (one file per variable). @@ -214,18 +241,20 @@ def create_netcdf_file(output_file, grid_name='GrIS_16000m', scenario='ctrl', st Whether to include scalar time-series variables include_xyt : bool Whether to include non-scalar x,y,t variables (3D). Set to False to skip x,y,t variables. + seed : int, optional + Seed for the synthetic data; the same seed always produces the same values. """ + rng = np.random.default_rng(seed) + group='ISMIP7' model='SYNTH1' contact_names='Your Name' contact_emails='your@email.org' - # Determine conventions directory + # Determine conventions directory (bundled package data by default) if conventions_dir is None: - # Try to find conventions directory relative to this script - script_dir = Path(__file__).parent.parent - conventions_dir = script_dir / 'conventions' + conventions_dir = data_dir() conventions_dir = Path(conventions_dir) @@ -243,7 +272,7 @@ def create_netcdf_file(output_file, grid_name='GrIS_16000m', scenario='ctrl', st domain_crs = 'EPSG:3031' # Determine output directory - models_dir = Path(output_root) if output_root is not None else Path(__file__).parent.parent / 'Models' + models_dir = Path(output_root) if output_root is not None else Path.cwd() / 'Models' if grid_type == 'AIS': output_dir = models_dir / 'AIS' / group / model / 'CORE' / set_counter else: # GrIS @@ -252,7 +281,7 @@ def create_netcdf_file(output_file, grid_name='GrIS_16000m', scenario='ctrl', st output_dir.mkdir(parents=True, exist_ok=True) # Load grid definition - gdf_file = conventions_dir.parent / 'gdfs' / f'gdf_ISMIP7_{grid_type}_{resolution}.txt' + gdf_file = conventions_dir / 'gdfs' / f'gdf_ISMIP7_{grid_type}_{resolution}.txt' if not gdf_file.exists(): raise FileNotFoundError(f"Grid definition file not found: {gdf_file}") @@ -376,7 +405,7 @@ def create_netcdf_file(output_file, grid_name='GrIS_16000m', scenario='ctrl', st if min_val >= max_val: max_val = min_val + 1.0 - data = generate_synthetic_data((nyears, ny, nx), min_val, max_val) + data = generate_synthetic_data((nyears, ny, nx), min_val, max_val, rng=rng) # Create data array with metadata data_vars = { @@ -491,7 +520,7 @@ def create_netcdf_file(output_file, grid_name='GrIS_16000m', scenario='ctrl', st if min_val >= max_val: max_val = min_val + 1.0 - data = generate_synthetic_data((n_snapshots, nz, ny, nx), min_val, max_val) + data = generate_synthetic_data((n_snapshots, nz, ny, nx), min_val, max_val, rng=rng) data_vars = { var_name: ( @@ -560,7 +589,7 @@ def create_netcdf_file(output_file, grid_name='GrIS_16000m', scenario='ctrl', st if min_val >= max_val: max_val = min_val + 1.0 - data = generate_synthetic_data((ny, nx), min_val, max_val) + data = generate_synthetic_data((ny, nx), min_val, max_val, rng=rng) data_vars = { var_name: ( @@ -661,7 +690,7 @@ def create_netcdf_file(output_file, grid_name='GrIS_16000m', scenario='ctrl', st max_val = min_val + 1.0 # Generate synthetic 1D data - data = generate_synthetic_data((nyears,), min_val, max_val).astype(np.float32) # Wide range for scalar data + data = generate_synthetic_data((nyears,), min_val, max_val, rng=rng).astype(np.float32) # Wide range for scalar data # Create data array with metadata data_vars = { var_name: ( @@ -740,7 +769,8 @@ def create_netcdf_file(output_file, grid_name='GrIS_16000m', scenario='ctrl', st def create_multiple_files(output_dir=None, n_files=3, conventions_dir=None, start_year=2015, contact_names='Your Name', contact_emails='your@email.org', - include_xyt=True, include_non_mandatory=False): + include_xyt=True, include_non_mandatory=False, + seed=DEFAULT_SEED): """ Create multiple NetCDF files for testing (one file per variable per grid). @@ -755,9 +785,7 @@ def create_multiple_files(output_dir=None, n_files=3, conventions_dir=None, """ if conventions_dir is None: - # Try to find conventions directory relative to this script - script_dir = Path(__file__).parent.parent - conventions_dir = script_dir / 'conventions' + conventions_dir = data_dir() # Get available grids grids = get_available_grids(str(conventions_dir)) @@ -784,19 +812,18 @@ def create_multiple_files(output_dir=None, n_files=3, conventions_dir=None, include_scalars=(i == 0), include_xyt=include_xyt, include_non_mandatory=include_non_mandatory, + seed=seed + i, ) total_files += len(created_files) print(f"Total files created: {total_files}") -if __name__ == '__main__': - import argparse - +def main(): + """Command-line entry point for `ismip7-generate-test-files`.""" # Get available grids - script_dir = Path(__file__).parent.parent - conventions_dir = script_dir / 'conventions' - available_grids = get_available_grids(str(conventions_dir)) + conventions_directory = data_dir() + available_grids = get_available_grids(conventions_directory) # Create list of available grid choices, excluding some high-resolution entries grid_choices = [] @@ -854,8 +881,8 @@ def create_multiple_files(output_dir=None, n_files=3, conventions_dir=None, ) parser.add_argument( '--conventions-dir', - default=str(conventions_dir), - help=f'Path to conventions directory (default: {conventions_dir})' + default=str(conventions_directory), + help=f'Path to conventions directory (default: {conventions_directory})' ) parser.add_argument( '--ism-member-id', @@ -877,6 +904,13 @@ def create_multiple_files(output_dir=None, n_files=3, conventions_dir=None, default='C001', help='Set counter id (default: C001)' ) + parser.add_argument( + '--seed', + type=int, + default=DEFAULT_SEED, + help=f'Seed for the synthetic data (default: {DEFAULT_SEED}); the same ' + f'seed always produces the same values' + ) parser.add_argument( '--list-grids', action='store_true', @@ -894,13 +928,14 @@ def create_multiple_files(output_dir=None, n_files=3, conventions_dir=None, for resolution in available_grids[grid_type]: print(f" - {grid_type}_{resolution}") print() - exit(0) + return if args.multiple: create_multiple_files(conventions_dir=args.conventions_dir, start_year=args.start_year, include_xyt=args.xyt, - include_non_mandatory=args.include_non_mandatory) + include_non_mandatory=args.include_non_mandatory, + seed=args.seed) else: create_netcdf_file( None, # Use default output path @@ -916,4 +951,9 @@ def create_multiple_files(output_dir=None, n_files=3, conventions_dir=None, esm_id=args.esm_id, forcing_member_id=args.forcing_member_id, set_counter=args.set_counter, + seed=args.seed, ) + + +if __name__ == '__main__': + main() diff --git a/isschecker_env.yml b/isschecker_env.yml index 2823a79..6946902 100644 --- a/isschecker_env.yml +++ b/isschecker_env.yml @@ -1,17 +1,29 @@ name: isschecker channels: - conda-forge + # Never let a user's `defaults` channel leak into the solve: it would give + # different builds on different machines. + - nodefaults + dependencies: - - python=3.14 - - pip=26.0.1 +# Interpreter and build tools + - python >=3.11,<3.15 + - pip >=24 + # setuptools is required by `pip install --no-build-isolation` (conda-forge + # python no longer vendors it); >=77 for PEP 639 license metadata + - setuptools >=77 # Core scientific python - - numpy=2.4.3 - - pandas=3.0.2 - - pytest=8.4.2 - - tqdm=4.67.3 + - numpy >=2.1,<3 + - pandas >=2.2,<4 + - tqdm >=4.66 # Spatial packages - - xarray=2026.4.0 - - cftime=1.6.5 - - netcdf4=1.7.4 + # xarray >=2025.1.2 provides `xarray.coders.CFDatetimeCoder` (public API since + # 2025.1.1) and the non-nanosecond datetime decoding the checker relies on + - xarray >=2025.1.2,<2027 + - cftime >=1.6.4,<2 + - netcdf4 >=1.7,<2 + +# Testing + - pytest >=8 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0f1fae1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,47 @@ +[build-system] +requires = ["setuptools>=77"] +build-backend = "setuptools.build_meta" + +[project] +name = "isschecker" +version = "0.1.0" +description = "ISMIP7 compliance checker for ice sheet model simulation NetCDF datasets" +readme = "README.md" +license = "MIT" +license-files = ["LICENSE"] +requires-python = ">=3.11,<3.15" +authors = [{ name = "Heiko Goelzer", email = "heig@norceresearch.no" }] +classifiers = [ + "Intended Audience :: Science/Research", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering :: Atmospheric Science", +] +# These must stay in sync with isschecker_env.yml. The supported install path +# is `pip install --no-deps --no-build-isolation .` into the conda environment, +# so pip never resolves these; they are here so the metadata is honest about +# what the checker needs. +dependencies = [ + "numpy>=2.1,<3", + "pandas>=2.2,<4", + "xarray>=2025.1.2,<2027", + "cftime>=1.6.4,<2", + "netCDF4>=1.7,<2", + "tqdm>=4.66", +] + +[project.optional-dependencies] +test = ["pytest>=8"] + +[project.urls] +Homepage = "https://github.com/ismip/ISM_SimulationChecker" +Issues = "https://github.com/ismip/ISM_SimulationChecker/issues" + +[project.scripts] +ismip7-compliance-checker = "isschecker:main" +ismip7-generate-test-files = "isschecker.generate:main" + +[tool.setuptools] +packages = ["isschecker", "isschecker.data"] + +[tool.setuptools.package-data] +"isschecker.data" = ["*.csv", "gdfs/*.txt"] diff --git a/tests/reference/compliance_checker_log.txt b/tests/reference/compliance_checker_log.txt new file mode 100644 index 0000000..b320069 --- /dev/null +++ b/tests/reference/compliance_checker_log.txt @@ -0,0 +1,894 @@ +************************************************************************************ +************* Ice Sheet Model Simulations - Compliance Checker ************* +************************************************************************************ +isschecker version: +verification criteria: ISMIP7_variable_request.csv +date: +source: https://github.com/ismip/ISM_SimulationChecker + +------------------------------------------------------------------------------------ +Verified directory: +------------------------------------------------------------------------------------ +1 experiments checked. +27 files checked. + +0 error(s) detected. + - Mandatory variables: 0 error(s) + - Naming Tests : 0 error(s) + - Numerical Tests : 0 error(s) + - Spatial Tests : 0 error(s) + - Time Tests : 0 error(s) + - Attribute Tests : 0 error(s) + +0 warning(s) detected. + + +==================================================================================== +================ DETAILED RESULTS ================ +==================================================================================== +Hint: Use Cltr+F to look for specific problems. + + + ********************************************************** + ** Experiment: historical + ********************************************************** + + Mandatory variables Test: historical : all mandatory variables exist. + +Experiment: historical - File: acabf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: acabf + +NUMERICAL Tests + - The unit is correct: kg m-2 s-1 + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2013-07-01 (nominal year 2013). + - Last time stamp correctly set to 2014-07-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - acabf - File:acabf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: base_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: base + +NUMERICAL Tests + - The unit is correct: m + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2014-01-01 (nominal year 2013). + - Last time stamp correctly set to 2015-01-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - base - File:base_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: dlithkdt_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: dlithkdt + +NUMERICAL Tests + - The unit is correct: m s-1 + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2013-07-01 (nominal year 2013). + - Last time stamp correctly set to 2014-07-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - dlithkdt - File:dlithkdt_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: iareafl_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: iareafl + +NUMERICAL Tests + - The unit is correct: m^2 +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2014-01-01 (nominal year 2013). + - Last time stamp correctly set to 2015-01-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - iareafl - File:iareafl_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: iareagr_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: iareagr + +NUMERICAL Tests + - The unit is correct: m^2 +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2014-01-01 (nominal year 2013). + - Last time stamp correctly set to 2015-01-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - iareagr - File:iareagr_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: libmassbffl_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: libmassbffl + +NUMERICAL Tests + - The unit is correct: kg m-2 s-1 + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2013-07-01 (nominal year 2013). + - Last time stamp correctly set to 2014-07-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - libmassbffl - File:libmassbffl_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: libmassbfgr_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: libmassbfgr + +NUMERICAL Tests + - The unit is correct: kg m-2 s-1 + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2013-07-01 (nominal year 2013). + - Last time stamp correctly set to 2014-07-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - libmassbfgr - File:libmassbfgr_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: licalvf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: licalvf + +NUMERICAL Tests + - The unit is correct: kg m-2 s-1 + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2013-07-01 (nominal year 2013). + - Last time stamp correctly set to 2014-07-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - licalvf - File:licalvf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: lifmassbf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: lifmassbf + +NUMERICAL Tests + - The unit is correct: kg m-2 s-1 + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2013-07-01 (nominal year 2013). + - Last time stamp correctly set to 2014-07-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - lifmassbf - File:lifmassbf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: ligroundf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: ligroundf + +NUMERICAL Tests + - The unit is correct: kg m-2 s-1 + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2013-07-01 (nominal year 2013). + - Last time stamp correctly set to 2014-07-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - ligroundf - File:ligroundf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: lim_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: lim + +NUMERICAL Tests + - The unit is correct: kg +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2014-01-01 (nominal year 2013). + - Last time stamp correctly set to 2015-01-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - lim - File:lim_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: limnsw_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: limnsw + +NUMERICAL Tests + - The unit is correct: kg +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2014-01-01 (nominal year 2013). + - Last time stamp correctly set to 2015-01-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - limnsw - File:limnsw_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: lithk_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: lithk + +NUMERICAL Tests + - The unit is correct: m + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2014-01-01 (nominal year 2013). + - Last time stamp correctly set to 2015-01-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - lithk - File:lithk_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: orog_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: orog + +NUMERICAL Tests + - The unit is correct: m + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2014-01-01 (nominal year 2013). + - Last time stamp correctly set to 2015-01-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - orog - File:orog_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: sftflf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: sftflf + +NUMERICAL Tests + - The unit is correct: 1 + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2014-01-01 (nominal year 2013). + - Last time stamp correctly set to 2015-01-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - sftflf - File:sftflf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: sftgif_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: sftgif + +NUMERICAL Tests + - The unit is correct: 1 + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2014-01-01 (nominal year 2013). + - Last time stamp correctly set to 2015-01-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - sftgif - File:sftgif_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: sftgrf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: sftgrf + +NUMERICAL Tests + - The unit is correct: 1 + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2014-01-01 (nominal year 2013). + - Last time stamp correctly set to 2015-01-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - sftgrf - File:sftgrf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: strbasemag_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: strbasemag + +NUMERICAL Tests + - The unit is correct: Pa + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2014-01-01 (nominal year 2013). + - Last time stamp correctly set to 2015-01-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - strbasemag - File:strbasemag_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: tendacabf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: tendacabf + +NUMERICAL Tests + - The unit is correct: kg s-1 +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2013-07-01 (nominal year 2013). + - Last time stamp correctly set to 2014-07-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - tendacabf - File:tendacabf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: tendlibmassbffl_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: tendlibmassbffl + +NUMERICAL Tests + - The unit is correct: kg s-1 +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2013-07-01 (nominal year 2013). + - Last time stamp correctly set to 2014-07-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - tendlibmassbffl - File:tendlibmassbffl_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: tendlibmassbfgr_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: tendlibmassbfgr + +NUMERICAL Tests + - The unit is correct: kg s-1 +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2013-07-01 (nominal year 2013). + - Last time stamp correctly set to 2014-07-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - tendlibmassbfgr - File:tendlibmassbfgr_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: tendlicalvf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: tendlicalvf + +NUMERICAL Tests + - The unit is correct: kg s-1 +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2013-07-01 (nominal year 2013). + - Last time stamp correctly set to 2014-07-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - tendlicalvf - File:tendlicalvf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: tendlifmassbf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: tendlifmassbf + +NUMERICAL Tests + - The unit is correct: kg s-1 +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2013-07-01 (nominal year 2013). + - Last time stamp correctly set to 2014-07-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - tendlifmassbf - File:tendlifmassbf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: tendligroundf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: tendligroundf + +NUMERICAL Tests + - The unit is correct: kg s-1 +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2013-07-01 (nominal year 2013). + - Last time stamp correctly set to 2014-07-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - tendligroundf - File:tendligroundf_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: topg_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: topg + +NUMERICAL Tests + - The unit is correct: m + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2014-01-01 (nominal year 2013). + - Last time stamp correctly set to 2015-01-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - topg - File:topg_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: xvelmean_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: xvelmean + +NUMERICAL Tests + - The unit is correct: m s-1 + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2014-01-01 (nominal year 2013). + - Last time stamp correctly set to 2015-01-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - xvelmean - File:xvelmean_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- + +Experiment: historical - File: yvelmean_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc + +NAMING Tests + - Filename year range 2013-2014 matches time axis: OK +** Tested Variable: yvelmean + +NUMERICAL Tests + - The unit is correct: m s-1 + - The minimum value successfully verified. + - The maximum value successfully verified. +SPATIAL Tests + - Grid: Lowest left corner is well defined. + - Grid: Upper right corner is well defined. + - The grid resolution (16 km) was successfully verified. +TIME Tests + - Time is a record (unlimited) dimension: OK + - Time step: 365 days + - Experiment lasts 2 years (>= 1 year required): OK + - First time stamp correctly set to 2014-01-01 (nominal year 2013). + - Last time stamp correctly set to 2015-01-01 (nominal year 2014). +ATTRIBUTE Tests + - Global attributes: OK + - Coordinate attributes: OK + - Variable attributes: OK + - Fill value attributes: OK + - Dtype attributes: OK + - Packing attributes: OK + +---------------------------------------------------------- +historical - yvelmean - File:yvelmean_GrIS_ISMIP7_SYNTH1_m001_CESM2-WACCM_f001_historical_C001_2013-2014.nc +No errors. Good job ! +No warnings. +---------------------------------------------------------- diff --git a/tests/test_compliance_checker.py b/tests/test_compliance_checker.py index 886af24..2283a91 100644 --- a/tests/test_compliance_checker.py +++ b/tests/test_compliance_checker.py @@ -1,31 +1,14 @@ -import importlib.util import shutil -import sys from datetime import datetime from pathlib import Path import netCDF4 import pytest - -REPO_ROOT = Path(__file__).resolve().parents[1] -if str(REPO_ROOT) not in sys.path: - sys.path.insert(0, str(REPO_ROOT)) - -compliance_checker = importlib.import_module("compliance_checker") - - -def _load_generator_module(): - generator_path = REPO_ROOT / "generate" / "generate_test_files.py" - spec = importlib.util.spec_from_file_location("generate_test_files", generator_path) - assert spec is not None - module = importlib.util.module_from_spec(spec) - assert spec.loader is not None - spec.loader.exec_module(module) - return module - - -generate_test_files = _load_generator_module() +# The installed package is what is tested: run `pip install --no-deps +# --no-build-isolation -e .` (or without -e) before pytest. +from isschecker import checker +from isschecker import generate as generate_test_files @pytest.fixture(scope="session") @@ -37,7 +20,6 @@ def baseline_core_dir(tmp_path_factory): scenario="historical", start_year=2013, nyears=2, - conventions_dir=REPO_ROOT / "conventions", include_scalars=True, include_xyt=False, output_root=baseline_root, @@ -45,11 +27,10 @@ def baseline_core_dir(tmp_path_factory): assert created_files, "Synthetic baseline generation did not create any files." core_dir = baseline_root / "GrIS" / "ISMIP7" / "SYNTH1" / "CORE" / "C001" - baseline_summary = compliance_checker.run_checker( + baseline_summary = checker.run_checker( source_path=str(core_dir), variable_list="ismip7_scalars", - workdir=str(REPO_ROOT), - commit_num="tests", + version="tests", ) assert baseline_summary["total_errors"] == 0, ( "Synthetic baseline files should pass the checker, but found " @@ -66,11 +47,10 @@ def case_dir(tmp_path, baseline_core_dir): def run_checker(case_dir: Path): - return compliance_checker.run_checker( + return checker.run_checker( source_path=str(case_dir), variable_list="ismip7_scalars", - workdir=str(REPO_ROOT), - commit_num="tests", + version="tests", ) @@ -124,7 +104,7 @@ def test_checker_reports_missing_mandatory_variable(case_dir): def test_checker_reports_invalid_esm_in_filename(case_dir): rename_file_part( first_dataset(case_dir), - compliance_checker.ISMIP7_FILENAME_ESM_IDX, + checker.ISMIP7_FILENAME_ESM_IDX, "NOT-A-CMIP-MODEL", ) @@ -138,7 +118,7 @@ def test_checker_reports_invalid_esm_in_filename(case_dir): def test_checker_reports_invalid_year_range_format(case_dir): rename_file_part( first_dataset(case_dir), - compliance_checker.ISMIP7_FILENAME_YEAR_RANGE_IDX, + checker.ISMIP7_FILENAME_YEAR_RANGE_IDX, "2013to2014.nc", ) @@ -160,7 +140,7 @@ def test_checker_reports_historical_time_range_violation(case_dir): ) rename_file_part( target_file, - compliance_checker.ISMIP7_FILENAME_YEAR_RANGE_IDX, + checker.ISMIP7_FILENAME_YEAR_RANGE_IDX, "2015-2016.nc", ) diff --git a/tests/test_golden_log.py b/tests/test_golden_log.py new file mode 100644 index 0000000..031c31d --- /dev/null +++ b/tests/test_golden_log.py @@ -0,0 +1,110 @@ +"""Compare the checker's log against a stored reference. + +The log file is what the checker actually delivers, so this is the test that +makes "everyone gets the same answer from the same files" checkable rather than +merely intended: it fails on any change in log text, whether that change comes +from us or from a dependency release inside the supported version ranges. In +CI it runs against both the floor and the latest environment, so a difference +between them shows up as a failure here. + +It relies on the generator being seeded, so the input data is fixed too. + +To adopt a change in the log as the new expected output, inspect the diff the +failure prints, then regenerate the reference: + + ISSCHECKER_UPDATE_GOLDEN_LOG=1 pytest tests/test_golden_log.py + +and commit the updated reference alongside the change that caused it. +""" + +import difflib +import os +import re +from pathlib import Path + +import pytest + +from isschecker import checker +from isschecker import generate as generate_test_files + +REFERENCE_LOG = Path(__file__).parent / "reference" / "compliance_checker_log.txt" + +# Fixed so that the log is reproducible. +GRID_NAME = "GrIS_16000m" +SCENARIO = "historical" +START_YEAR = 2013 +NYEARS = 2 +SEED = 0 +VARIABLE_LIST = "ismip7" +VERSION = "tests" + +# Which checker ran, when it ran, and where the data sat: all expected to +# differ between runs and machines, none of them a regression. +VOLATILE_LINES = ( + (re.compile(r"^isschecker version: .*$", re.MULTILINE), "isschecker version: "), + (re.compile(r"^date: .*$", re.MULTILINE), "date: "), +) + + +def _mask_volatile(text: str, source_path: str) -> str: + """Replace the parts of a log that are expected to vary between runs. + + The reference is stored in this masked form, so `` and + `` appear in it literally and masking it again is a no-op. + """ + for pattern, replacement in VOLATILE_LINES: + text = pattern.sub(replacement, text) + return text.replace(source_path, "") + + +@pytest.fixture(scope="module") +def generated_log(tmp_path_factory) -> str: + """Return the log of a checker run over a fixed synthetic dataset, masked.""" + output_root = tmp_path_factory.mktemp("golden_log_data") + created_files = generate_test_files.create_netcdf_file( + None, + grid_name=GRID_NAME, + scenario=SCENARIO, + start_year=START_YEAR, + nyears=NYEARS, + include_scalars=True, + include_xyt=True, + output_root=output_root, + seed=SEED, + ) + assert created_files, "Synthetic generation did not create any files." + + core_dir = output_root / "GrIS" / "ISMIP7" / "SYNTH1" / "CORE" / "C001" + summary = checker.run_checker( + source_path=str(core_dir), + variable_list=VARIABLE_LIST, + version=VERSION, + ) + return _mask_volatile(summary["log_text"], str(core_dir)) + + +def test_log_matches_reference(generated_log): + if os.environ.get("ISSCHECKER_UPDATE_GOLDEN_LOG"): + REFERENCE_LOG.parent.mkdir(parents=True, exist_ok=True) + REFERENCE_LOG.write_text(generated_log) + pytest.skip(f"Wrote a new reference log to {REFERENCE_LOG}") + + assert REFERENCE_LOG.exists(), ( + f"No reference log at {REFERENCE_LOG}. Create one with " + "ISSCHECKER_UPDATE_GOLDEN_LOG=1 pytest tests/test_golden_log.py" + ) + + expected = REFERENCE_LOG.read_text().splitlines(keepends=True) + actual = generated_log.splitlines(keepends=True) + + if actual != expected: + diff = "".join( + difflib.unified_diff( + expected, actual, fromfile="reference log", tofile="this run" + ) + ) + pytest.fail( + "The checker's log differs from the stored reference. If the change " + "is intended, regenerate the reference with " + "ISSCHECKER_UPDATE_GOLDEN_LOG=1.\n\n" + diff + )