climate-ref-ilamb 0.8.1__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/.gitignore +10 -1
  2. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/PKG-INFO +2 -1
  3. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/pyproject.toml +3 -2
  4. climate_ref_ilamb-0.9.0/src/climate_ref_ilamb/__init__.py +98 -0
  5. climate_ref_ilamb-0.9.0/tests/integration/test_diagnostics.py +132 -0
  6. climate_ref_ilamb-0.9.0/tests/unit/test_provider.py +80 -0
  7. climate_ref_ilamb-0.8.1/src/climate_ref_ilamb/__init__.py +0 -47
  8. climate_ref_ilamb-0.8.1/tests/integration/test_diagnostics.py +0 -40
  9. climate_ref_ilamb-0.8.1/tests/unit/test_provider.py +0 -15
  10. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/LICENCE +0 -0
  11. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/NOTICE +0 -0
  12. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/README.md +0 -0
  13. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/src/climate_ref_ilamb/configure/ilamb.yaml +0 -0
  14. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/src/climate_ref_ilamb/configure/iomb.yaml +0 -0
  15. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/src/climate_ref_ilamb/dataset_registry/ilamb.txt +0 -0
  16. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/src/climate_ref_ilamb/dataset_registry/iomb.txt +0 -0
  17. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/src/climate_ref_ilamb/dataset_registry/test.txt +0 -0
  18. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/src/climate_ref_ilamb/datasets.py +0 -0
  19. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/src/climate_ref_ilamb/py.typed +0 -0
  20. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/src/climate_ref_ilamb/standard.py +0 -0
  21. {climate_ref_ilamb-0.8.1 → climate_ref_ilamb-0.9.0}/tests/unit/test_standard_metrics.py +0 -0
@@ -150,7 +150,7 @@ dmypy.json
150
150
 
151
151
  # Generated output
152
152
  out
153
- .ref
153
+ .ref*
154
154
 
155
155
  # Ignore copied LICENCE/NOTICE files
156
156
  packages/*/LICENCE
@@ -158,3 +158,12 @@ packages/*/NOTICE
158
158
 
159
159
  # Local directory for data
160
160
  /data
161
+
162
+ # Generated SDK
163
+ /climate_ref_client
164
+
165
+ # User-specific catalog paths (test data)
166
+ *.paths.yaml
167
+
168
+ # Helm dependencies
169
+ helm/charts/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: climate-ref-ilamb
3
- Version: 0.8.1
3
+ Version: 0.9.0
4
4
  Summary: ILAMB diagnostic provider for the Rapid Evaluation Framework
5
5
  Author-email: Nathan Collier <nathaniel.collier@gmail.com>, Jared Lewis <jared.lewis@climate-resource.com>
6
6
  License-Expression: Apache-2.0
@@ -21,6 +21,7 @@ Requires-Python: >=3.11
21
21
  Requires-Dist: climate-ref-core
22
22
  Requires-Dist: ilamb3>=2025.9.9
23
23
  Requires-Dist: scipy<1.16
24
+ Requires-Dist: xarray<2025.11
24
25
  Description-Content-Type: text/markdown
25
26
 
26
27
  # climate-ref-ilamb
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "climate-ref-ilamb"
3
- version = "0.8.1"
3
+ version = "0.9.0"
4
4
  description = "ILAMB diagnostic provider for the Rapid Evaluation Framework"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -25,7 +25,8 @@ classifiers = [
25
25
  dependencies = [
26
26
  "climate-ref-core",
27
27
  "ilamb3>=2025.9.9",
28
- "scipy<1.16", # https://github.com/statsmodels/statsmodels/issues/9584
28
+ "scipy<1.16", # https://github.com/statsmodels/statsmodels/issues/9584
29
+ "xarray<2025.11", # ilamb3 incompatibility with integrate_space units handling
29
30
  ]
30
31
  [project.entry-points."climate-ref.providers"]
31
32
  ilamb = "climate_ref_ilamb:provider"
@@ -0,0 +1,98 @@
1
+ """
2
+ Diagnostic provider for ILAMB
3
+
4
+ This module provides a diagnostics provider for ILAMB, a tool for evaluating
5
+ climate models against observations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.metadata
11
+ import importlib.resources
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING
14
+
15
+ import pooch
16
+ import yaml
17
+ from loguru import logger
18
+
19
+ from climate_ref_core.dataset_registry import (
20
+ DATASET_URL,
21
+ dataset_registry_manager,
22
+ fetch_all_files,
23
+ validate_registry_cache,
24
+ )
25
+ from climate_ref_core.providers import DiagnosticProvider
26
+ from climate_ref_ilamb.standard import ILAMBStandard
27
+
28
+ if TYPE_CHECKING:
29
+ from climate_ref.config import Config
30
+
31
+ __version__ = importlib.metadata.version("climate-ref-ilamb")
32
+
33
+ # Registry names used by ILAMB
34
+ _REGISTRY_NAMES = ("ilamb-test", "ilamb", "iomb")
35
+
36
+
37
+ class ILAMBProvider(DiagnosticProvider):
38
+ """Provider for ILAMB diagnostics."""
39
+
40
+ def fetch_data(self, config: Config) -> None:
41
+ """Fetch ILAMB reference data from all registries."""
42
+ for name in _REGISTRY_NAMES:
43
+ registry = dataset_registry_manager[name]
44
+ fetch_all_files(registry, name, output_dir=None)
45
+
46
+ def validate_setup(self, config: Config) -> bool:
47
+ """Validate that all ILAMB data is cached with correct checksums."""
48
+ all_errors: list[str] = []
49
+ for name in _REGISTRY_NAMES:
50
+ registry = dataset_registry_manager[name]
51
+ errors = validate_registry_cache(registry, name)
52
+ all_errors.extend(errors)
53
+
54
+ if all_errors:
55
+ for error in all_errors:
56
+ logger.error(f"ILAMB validation failed: {error}")
57
+ logger.error(
58
+ f"Data for {self.slug} is missing or corrupted. "
59
+ f"Please run `ref providers setup --provider {self.slug}` to fetch data."
60
+ )
61
+ return False
62
+ return True
63
+
64
+ def get_data_path(self) -> Path | None:
65
+ """Get the path where ILAMB data is cached."""
66
+ # All ILAMB registries use the same cache
67
+ return Path(pooch.os_cache("climate_ref"))
68
+
69
+
70
+ provider = ILAMBProvider("ILAMB", __version__)
71
+
72
+ # Register some datasets
73
+ dataset_registry_manager.register(
74
+ "ilamb-test",
75
+ base_url=DATASET_URL,
76
+ package="climate_ref_ilamb.dataset_registry",
77
+ resource="test.txt",
78
+ )
79
+ dataset_registry_manager.register(
80
+ "ilamb",
81
+ base_url=DATASET_URL,
82
+ package="climate_ref_ilamb.dataset_registry",
83
+ resource="ilamb.txt",
84
+ )
85
+ dataset_registry_manager.register(
86
+ "iomb",
87
+ base_url=DATASET_URL,
88
+ package="climate_ref_ilamb.dataset_registry",
89
+ resource="iomb.txt",
90
+ )
91
+
92
+ # Dynamically register ILAMB diagnostics
93
+ for yaml_file in importlib.resources.files("climate_ref_ilamb.configure").iterdir():
94
+ with open(str(yaml_file)) as fin:
95
+ metrics = yaml.safe_load(fin)
96
+ registry_filename = metrics.pop("registry")
97
+ for metric, options in metrics.items():
98
+ provider.register(ILAMBStandard(registry_filename, metric, options.pop("sources"), **options))
@@ -0,0 +1,132 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+ from climate_ref_ilamb import provider
5
+
6
+ from climate_ref.testing import TestCaseRunner, validate_result
7
+ from climate_ref_core.diagnostics import Diagnostic
8
+ from climate_ref_core.testing import (
9
+ RegressionValidator,
10
+ TestCasePaths,
11
+ collect_test_case_params,
12
+ load_datasets_from_yaml,
13
+ )
14
+
15
+
16
+ @pytest.fixture(scope="session")
17
+ def provider_test_data_dir() -> Path:
18
+ """Path to the package-local test data directory."""
19
+ return Path(__file__).parent.parent / "test-data"
20
+
21
+
22
+ xfail_diagnostics = [
23
+ "ohc-noaa", # Missing sample data
24
+ ]
25
+ skipped_diagnostics = []
26
+
27
+
28
+ diagnostics = [
29
+ pytest.param(
30
+ diagnostic,
31
+ id=diagnostic.slug,
32
+ marks=[
33
+ *([pytest.mark.xfail(reason="Expected failure")] if diagnostic.slug in xfail_diagnostics else []),
34
+ *([pytest.mark.skip(reason="Problem test")] if diagnostic.slug in skipped_diagnostics else []),
35
+ ],
36
+ )
37
+ for diagnostic in provider.diagnostics()
38
+ ]
39
+
40
+ # Test case params for parameterized test_case tests
41
+ test_case_params = collect_test_case_params(provider)
42
+
43
+
44
+ @pytest.mark.slow
45
+ @pytest.mark.parametrize("diagnostic", diagnostics)
46
+ def test_diagnostics(diagnostic: Diagnostic, diagnostic_validation):
47
+ validator = diagnostic_validation(diagnostic)
48
+
49
+ definition = validator.get_definition()
50
+ validator.execute(definition)
51
+
52
+
53
+ @pytest.mark.parametrize("diagnostic", diagnostics)
54
+ def test_build_results(diagnostic: Diagnostic, diagnostic_validation):
55
+ validator = diagnostic_validation(diagnostic)
56
+
57
+ definition = validator.get_regression_definition()
58
+ validator.validate(definition)
59
+ validator.execution_regression.check(definition.key, definition.output_directory)
60
+
61
+
62
+ @pytest.mark.parametrize("diagnostic,test_case_name", test_case_params)
63
+ def test_validate_test_case_regression(
64
+ diagnostic: Diagnostic,
65
+ test_case_name: str,
66
+ provider_test_data_dir: Path,
67
+ config,
68
+ tmp_path: Path,
69
+ ):
70
+ """
71
+ Validate pre-stored test case regression outputs as CMEC bundles.
72
+
73
+ Each diagnostic/test_case is a separate parameterized test.
74
+ """
75
+ diagnostic.provider.configure(config)
76
+
77
+ paths = TestCasePaths.from_test_data_dir(
78
+ provider_test_data_dir,
79
+ diagnostic.slug,
80
+ test_case_name,
81
+ )
82
+
83
+ if not paths.catalog.exists():
84
+ pytest.skip(f"No catalog file for {diagnostic.slug}/{test_case_name}")
85
+ if not paths.regression.exists():
86
+ pytest.skip(f"No regression data for {diagnostic.slug}/{test_case_name}")
87
+
88
+ validator = RegressionValidator(
89
+ diagnostic=diagnostic,
90
+ test_case_name=test_case_name,
91
+ test_data_dir=provider_test_data_dir,
92
+ )
93
+
94
+ definition = validator.load_regression_definition(tmp_path / diagnostic.slug / test_case_name)
95
+ validator.validate(definition)
96
+
97
+
98
+ @pytest.mark.slow
99
+ @pytest.mark.test_cases
100
+ @pytest.mark.parametrize("diagnostic,test_case_name", test_case_params)
101
+ def test_run_test_cases(
102
+ diagnostic: Diagnostic,
103
+ test_case_name: str,
104
+ provider_test_data_dir: Path,
105
+ config,
106
+ tmp_path: Path,
107
+ ):
108
+ """
109
+ Run diagnostic test cases end-to-end with ESGF data.
110
+
111
+ Requires: `ref test-cases fetch --provider ilamb` to have been run first.
112
+ """
113
+ diagnostic.provider.configure(config)
114
+
115
+ paths = TestCasePaths.from_test_data_dir(
116
+ provider_test_data_dir,
117
+ diagnostic.slug,
118
+ test_case_name,
119
+ )
120
+
121
+ if not paths.catalog.exists():
122
+ pytest.skip(f"No catalog file for {diagnostic.slug}/{test_case_name}")
123
+
124
+ datasets = load_datasets_from_yaml(paths.catalog)
125
+
126
+ runner = TestCaseRunner(config=config, datasets=datasets)
127
+ output_dir = tmp_path / diagnostic.slug / test_case_name
128
+
129
+ result = runner.run(diagnostic, test_case_name, output_dir)
130
+
131
+ assert result.successful, f"Diagnostic {diagnostic.slug} failed"
132
+ validate_result(diagnostic, config, result)
@@ -0,0 +1,80 @@
1
+ import importlib.resources
2
+ from pathlib import Path
3
+
4
+ import pooch
5
+ from climate_ref_ilamb import __version__, provider
6
+
7
+
8
+ def test_provider():
9
+ assert provider.name == "ILAMB"
10
+ assert provider.slug == "ilamb"
11
+ assert provider.version == __version__
12
+
13
+ counts = []
14
+ for f in importlib.resources.files("climate_ref_ilamb.configure").iterdir():
15
+ with open(f) as fin:
16
+ counts.append(fin.read().count("sources"))
17
+ assert len(provider) == sum(counts)
18
+
19
+
20
+ class TestILAMBProviderHooks:
21
+ """Tests for ILAMBProvider lifecycle hooks."""
22
+
23
+ def test_get_data_path(self):
24
+ """Test that get_data_path returns the pooch cache path."""
25
+ data_path = provider.get_data_path()
26
+ assert data_path is not None
27
+ assert isinstance(data_path, Path)
28
+ assert data_path == Path(pooch.os_cache("climate_ref"))
29
+
30
+ def test_fetch_data(self, mocker):
31
+ """Test that fetch_data calls fetch_all_files for all registries."""
32
+ mock_fetch = mocker.patch("climate_ref_ilamb.fetch_all_files")
33
+ mock_config = mocker.Mock()
34
+
35
+ provider.fetch_data(mock_config)
36
+
37
+ # Should be called once for each registry (ilamb-test, ilamb, iomb)
38
+ assert mock_fetch.call_count == 3
39
+ registry_names = [call[0][1] for call in mock_fetch.call_args_list]
40
+ assert "ilamb-test" in registry_names
41
+ assert "ilamb" in registry_names
42
+ assert "iomb" in registry_names
43
+
44
+ def test_validate_setup_all_valid(self, mocker):
45
+ """Test validate_setup returns True when all data is valid."""
46
+ mock_config = mocker.Mock()
47
+ mocker.patch(
48
+ "climate_ref_ilamb.validate_registry_cache",
49
+ return_value=[],
50
+ )
51
+
52
+ result = provider.validate_setup(mock_config)
53
+ assert result is True
54
+
55
+ def test_validate_setup_data_invalid(self, mocker):
56
+ """Test validate_setup returns False when any registry has errors."""
57
+ mock_config = mocker.Mock()
58
+ # Return errors for one registry
59
+ mocker.patch(
60
+ "climate_ref_ilamb.validate_registry_cache",
61
+ side_effect=[[], ["File missing: test.nc"], []],
62
+ )
63
+
64
+ result = provider.validate_setup(mock_config)
65
+ assert result is False
66
+
67
+ def test_validate_setup_multiple_errors(self, mocker):
68
+ """Test validate_setup collects errors from all registries."""
69
+ mock_config = mocker.Mock()
70
+ mocker.patch(
71
+ "climate_ref_ilamb.validate_registry_cache",
72
+ side_effect=[
73
+ ["Error 1"],
74
+ ["Error 2", "Error 3"],
75
+ [],
76
+ ],
77
+ )
78
+
79
+ result = provider.validate_setup(mock_config)
80
+ assert result is False
@@ -1,47 +0,0 @@
1
- """
2
- Diagnostic provider for ILAMB
3
-
4
- This module provides a diagnostics provider for ILAMB, a tool for evaluating
5
- climate models against observations.
6
- """
7
-
8
- import importlib.metadata
9
- import importlib.resources
10
-
11
- import yaml
12
-
13
- from climate_ref_core.dataset_registry import DATASET_URL, dataset_registry_manager
14
- from climate_ref_core.providers import DiagnosticProvider
15
- from climate_ref_ilamb.standard import ILAMBStandard
16
-
17
- __version__ = importlib.metadata.version("climate-ref-ilamb")
18
-
19
- provider = DiagnosticProvider("ILAMB", __version__)
20
-
21
- # Register some datasets
22
- dataset_registry_manager.register(
23
- "ilamb-test",
24
- base_url=DATASET_URL,
25
- package="climate_ref_ilamb.dataset_registry",
26
- resource="test.txt",
27
- )
28
- dataset_registry_manager.register(
29
- "ilamb",
30
- base_url=DATASET_URL,
31
- package="climate_ref_ilamb.dataset_registry",
32
- resource="ilamb.txt",
33
- )
34
- dataset_registry_manager.register(
35
- "iomb",
36
- base_url=DATASET_URL,
37
- package="climate_ref_ilamb.dataset_registry",
38
- resource="iomb.txt",
39
- )
40
-
41
- # Dynamically register ILAMB diagnostics
42
- for yaml_file in importlib.resources.files("climate_ref_ilamb.configure").iterdir():
43
- with open(str(yaml_file)) as fin:
44
- metrics = yaml.safe_load(fin)
45
- registry_filename = metrics.pop("registry")
46
- for metric, options in metrics.items():
47
- provider.register(ILAMBStandard(registry_filename, metric, options.pop("sources"), **options))
@@ -1,40 +0,0 @@
1
- import pytest
2
- from climate_ref_ilamb import provider as ilamb_provider
3
-
4
- from climate_ref_core.diagnostics import Diagnostic
5
-
6
- xfail_diagnostics = [
7
- "ohc-noaa", # Missing sample data
8
- ]
9
- skipped_diagnostics = []
10
-
11
-
12
- diagnostics = [
13
- pytest.param(
14
- diagnostic,
15
- id=diagnostic.slug,
16
- marks=[
17
- *([pytest.mark.xfail(reason="Expected failure")] if diagnostic.slug in xfail_diagnostics else []),
18
- *([pytest.mark.skip(reason="Problem test")] if diagnostic.slug in skipped_diagnostics else []),
19
- ],
20
- )
21
- for diagnostic in ilamb_provider.diagnostics()
22
- ]
23
-
24
-
25
- @pytest.mark.slow
26
- @pytest.mark.parametrize("diagnostic", diagnostics)
27
- def test_diagnostics(diagnostic: Diagnostic, diagnostic_validation):
28
- validator = diagnostic_validation(diagnostic)
29
-
30
- definition = validator.get_definition()
31
- validator.execute(definition)
32
-
33
-
34
- @pytest.mark.parametrize("diagnostic", diagnostics)
35
- def test_build_results(diagnostic: Diagnostic, diagnostic_validation):
36
- validator = diagnostic_validation(diagnostic)
37
-
38
- definition = validator.get_regression_definition()
39
- validator.validate(definition)
40
- validator.execution_regression.check(definition.key, definition.output_directory)
@@ -1,15 +0,0 @@
1
- import importlib.resources
2
-
3
- from climate_ref_ilamb import __version__, provider
4
-
5
-
6
- def test_provider():
7
- assert provider.name == "ILAMB"
8
- assert provider.slug == "ilamb"
9
- assert provider.version == __version__
10
-
11
- counts = []
12
- for f in importlib.resources.files("climate_ref_ilamb.configure").iterdir():
13
- with open(f) as fin:
14
- counts.append(fin.read().count("sources"))
15
- assert len(provider) == sum(counts)