Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
__pycache__
.pytest_cache
build/
dist/
*.egg-info/
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `compliance_checker/data/ISMIP7_variable_request.csv` (variable metadata) and `compliance_checker/data/experiments_ismip7.csv` (valid experiment year ranges and durations). These files are bundled with the package.

---

Expand Down Expand Up @@ -44,28 +44,33 @@ 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
pip install .
```

Installing the package registers the `ismip7-compliance-checker` command and bundles the data files, so the checker can be run from any directory. For development, install in editable mode with the test extra: `pip install -e ".[test]"`.

Dependencies: Python 3.14, `numpy` 2.4, `pandas` 3.0, `xarray` 2026.4, `cftime` 1.6, `netCDF4` 1.7, `tqdm` 4.67.

---

## 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 compliance_checker`). 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 |
Expand Down
23 changes: 11 additions & 12 deletions compliance_checker.py → compliance_checker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import re
import subprocess
import argparse
from importlib import resources

import numpy as np
import pandas as pd
Expand All @@ -54,10 +55,15 @@
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"


def data_path(filename: str) -> str:
"""Return the filesystem path to a bundled data file in ``compliance_checker.data``."""
return str(resources.files("compliance_checker.data").joinpath(filename))

AIS_GRID_EXTENT = [-3040000, -3040000, 3040000, 3040000]
GrIS_GRID_EXTENT = [-720000, -3450000, 960000, -570000]
AIS_POSSIBLE_RESOLUTION = [2, 4, 8, 16, 32]
Expand Down Expand Up @@ -138,22 +144,19 @@ 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,
):
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)
data_path(EXPERIMENTS_ISMIP7_CSV_FILENAME)
)
ismip_meta, ismip_var, mandatory_variables = _load_criteria(workdir, variable_list)
ismip_meta, ismip_var, mandatory_variables = _load_criteria(variable_list)

summary = _run_compliance_checker(
source_path=source_path,
Expand Down Expand Up @@ -205,8 +208,8 @@ 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):
csv_path = data_path(VARIABLE_REQUEST_CSV)
try:
df = pd.read_csv(csv_path)
except IOError:
Expand Down Expand Up @@ -1511,7 +1514,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()
File renamed without changes.
2 changes: 1 addition & 1 deletion generate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,6 @@ 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 `compliance_checker/data/ISMIP7_variable_request.csv`; grid definitions from the top-level `gdfs/` directory.
- `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.
24 changes: 12 additions & 12 deletions generate/generate_test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
from pathlib import Path
import netCDF4

REPO_ROOT = Path(__file__).resolve().parent.parent
GDF_DIR = REPO_ROOT / 'gdfs'
# The variable-request CSV is bundled with the compliance_checker package data.
CONVENTIONS_DIR = REPO_ROOT / 'compliance_checker' / 'data'


def get_available_grids(conventions_dir):
"""
Expand All @@ -27,8 +32,8 @@ def get_available_grids(conventions_dir):
dict
Dictionary with grid info: {'GrIS': [...], 'AIS': [...]}
"""
# Grid definitions moved to project root `gdfs` directory
gdf_dir = Path(conventions_dir).parent / 'gdfs'
# Grid definitions live in the project root `gdfs` directory
gdf_dir = GDF_DIR
grids = {'GrIS': [], 'AIS': []}

if not gdf_dir.exists():
Expand Down Expand Up @@ -221,11 +226,9 @@ def create_netcdf_file(output_file, grid_name='GrIS_16000m', scenario='ctrl', st
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 = CONVENTIONS_DIR

conventions_dir = Path(conventions_dir)

Expand All @@ -252,7 +255,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 = GDF_DIR / f'gdf_ISMIP7_{grid_type}_{resolution}.txt'
if not gdf_file.exists():
raise FileNotFoundError(f"Grid definition file not found: {gdf_file}")

Expand Down Expand Up @@ -755,9 +758,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 = CONVENTIONS_DIR

# Get available grids
grids = get_available_grids(str(conventions_dir))
Expand Down Expand Up @@ -794,8 +795,7 @@ def create_multiple_files(output_dir=None, n_files=3, conventions_dir=None,
import argparse

# Get available grids
script_dir = Path(__file__).parent.parent
conventions_dir = script_dir / 'conventions'
conventions_dir = CONVENTIONS_DIR
available_grids = get_available_grids(str(conventions_dir))

# Create list of available grid choices, excluding some high-resolution entries
Expand Down
18 changes: 9 additions & 9 deletions isschecker_env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ name: isschecker
channels:
- conda-forge
dependencies:
- python=3.14
- pip=26.0.1
- python
- pip

# Core scientific python
- numpy=2.4.3
- pandas=3.0.2
- pytest=8.4.2
- tqdm=4.67.3
- numpy
- pandas
- pytest
- tqdm

# Spatial packages
- xarray=2026.4.0
- cftime=1.6.5
- netcdf4=1.7.4
- xarray
- cftime
- netcdf4
32 changes: 32 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[build-system]
requires = ["setuptools>=61.0"]
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 = { file = "LICENSE" }
requires-python = ">=3.10"
authors = [{ name = "Heiko Goelzer", email = "heig@norceresearch.no" }]
dependencies = [
"numpy",
"pandas",
"xarray",
"cftime",
"netCDF4",
"tqdm",
]

[project.optional-dependencies]
test = ["pytest"]

[project.scripts]
ismip7-compliance-checker = "compliance_checker:main"

[tool.setuptools]
packages = ["compliance_checker", "compliance_checker.data"]

[tool.setuptools.package-data]
"compliance_checker.data" = ["*.csv"]
3 changes: 0 additions & 3 deletions tests/test_compliance_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,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,
Expand All @@ -48,7 +47,6 @@ def baseline_core_dir(tmp_path_factory):
baseline_summary = compliance_checker.run_checker(
source_path=str(core_dir),
variable_list="ismip7_scalars",
workdir=str(REPO_ROOT),
commit_num="tests",
)
assert baseline_summary["total_errors"] == 0, (
Expand All @@ -69,7 +67,6 @@ def run_checker(case_dir: Path):
return compliance_checker.run_checker(
source_path=str(case_dir),
variable_list="ismip7_scalars",
workdir=str(REPO_ROOT),
commit_num="tests",
)

Expand Down