Skip to content

Commit 0f598c7

Browse files
authored
Merge pull request #154 from ACCESS-NRI/148-compare-multi-experiments-repro-command
Add multi experiment repro test command: `compare-exp-tests` Given a list of experiment directories with the `--dirs` flag, generates tests that compares the checksums of the first output between each pair of experiments.
2 parents a00eef2 + 62cedf8 commit 0f598c7

29 files changed

Lines changed: 504 additions & 24 deletions

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ The checksum pytests are used for reproducibility CI checks in this repository.
66

77
Code from these pytests is adapted from COSIMAS's ACCESS-OM2's [bit reproducibility tests](https://github.com/COSIMA/access-om2/blob/master/test/test_bit_reproducibility.py).
88

9-
## Pytests
9+
## model-config-tests Pytests
1010

1111
### How to run pytests manually on NCI
1212

@@ -127,6 +127,21 @@ It is helpful if we work from the same definitions of what 'reproducibility' mea
127127
128128
Code block names above indicate tests that are currently available using the CI/CD system, see [pytest markers](#pytest_markers)
129129
130+
## Check for pairwise reproducibility between multiple experiments
131+
132+
To compare output/checksums between pairs of
133+
experiment directories, use the `compare-exp-tests` command. For example
134+
135+
```
136+
compare-exp-tests --dirs "path/to/exp1 path/to/exp2 path/to/exp3"
137+
```
138+
139+
The `--dirs` option specifies a space separated list of experiment directories to compare. These can be relative or absolute paths and should point to payu control directories. The above example generates three pairwise tests: "exp1 vs exp2", "exp1 vs exp3", and "exp2 vs exp3".
140+
141+
To enable more detailed output during the test runs, add the `-vvv` flag to the command.
142+
143+
Currently these tests only compare the first model run output directory.
144+
130145
## CI/CD
131146
132147
The `.github` directory contains many different workflows and actions. This section describes how they are used.

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ dependencies = [
2121
"pytest >=8.0.1",
2222
"ruamel.yaml >=0.18.5",
2323
"jsonschema >=4.21.1",
24-
"payu >=1.1.3"
24+
"payu >=1.1.3",
25+
"pytest-sugar"
2526
]
2627

2728
[project.optional-dependencies]
@@ -30,7 +31,8 @@ test = [
3031
]
3132

3233
[project.scripts]
33-
model-config-tests = "model_config_tests.__main__:main"
34+
model-config-tests = "model_config_tests.cmds.config_tests_cmd:main"
35+
compare-exp-tests = "model_config_tests.cmds.compare_exp_tests_cmd:main"
3436

3537
[project.urls]
3638
Homepage = "https://github.com/ACCESS-NRI/model-config-tests/"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Tests for comparing multiple experiments results"""
2+
3+
import sys
4+
from pathlib import Path
5+
6+
# Running pytests using --pyargs does not run pytest_addoption in conftest.py
7+
# Using workaround as described here:
8+
# https://stackoverflow.com/questions/41270604/using-command-line-parameters-with-pytest-pyargs
9+
HERE = Path(__file__)
10+
COMPARE_EXP_TESTS_DIR = "compare_exp_tests"
11+
12+
13+
def main():
14+
import pytest
15+
16+
test_path = str(HERE.parent.parent / COMPARE_EXP_TESTS_DIR)
17+
18+
# Specify --rootdir to shorten relative paths in displayed test output
19+
errcode = pytest.main([test_path] + sys.argv[1:] + [f"--rootdir={test_path}"])
20+
21+
sys.exit(errcode)
22+
23+
24+
if __name__ == "__main__":
25+
main()

src/model_config_tests/__main__.py renamed to src/model_config_tests/cmds/config_tests_cmd.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
import os
1+
"""Tests for a singular model configuration"""
2+
23
import sys
4+
from pathlib import Path
35

46
# Running pytests using --pyargs does not run pytest_addoption in conftest.py
57
# Using workaround as described here:
68
# https://stackoverflow.com/questions/41270604/using-command-line-parameters-with-pytest-pyargs
7-
HERE = os.path.dirname(__file__)
9+
HERE = Path(__file__)
10+
CONFIG_TESTS_DIR = "config_tests"
811

912

1013
def main():
1114
import pytest
1215

13-
errcode = pytest.main([HERE] + sys.argv[1:])
16+
test_path = str(HERE.parent.parent / CONFIG_TESTS_DIR)
17+
18+
errcode = pytest.main([test_path] + sys.argv[1:])
1419
sys.exit(errcode)
1520

1621

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright 2024 ACCESS-NRI and contributors. See the top-level COPYRIGHT file for details.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from itertools import combinations
4+
from pathlib import Path
5+
6+
7+
# Set up command line options and default for directory paths
8+
def pytest_addoption(parser):
9+
"""Attaches custom command line arguments"""
10+
parser.addoption(
11+
"--dirs",
12+
action="store",
13+
help="Specify a space separated list of experiment control directories to compare",
14+
)
15+
16+
17+
def pytest_generate_tests(metafunc):
18+
"""Set up dynamic parametrisation for testing pairwise comparisons
19+
of input directories"""
20+
if (
21+
"experiment_1" in metafunc.fixturenames
22+
and "experiment_2" in metafunc.fixturenames
23+
):
24+
# Generate pairs of experiments from command input
25+
input_dirs = metafunc.config.getoption("dirs")
26+
dir_pairs = get_experiment_pairs(input_dirs)
27+
28+
# Generate some readable IDs for the pairs
29+
ids = [f"{exp1.name} vs {exp2.name}" for exp1, exp2 in dir_pairs]
30+
metafunc.parametrize("experiment_1,experiment_2", dir_pairs, ids=ids)
31+
32+
33+
def get_experiment_pairs(dirs):
34+
"""Generate experiment directory pairs
35+
36+
Parameters
37+
----------
38+
dirs : str
39+
Space separated list of directories to compare
40+
41+
Returns
42+
-------
43+
list[tuple[Path, Path]]
44+
List of pairs of directories to compare
45+
"""
46+
if dirs is None:
47+
raise ValueError(
48+
"No directories specified, use --dirs to specify a space separated list"
49+
)
50+
51+
dirs = dirs.split()
52+
53+
paths = set()
54+
for dir in dirs:
55+
# Check if the path exists and is a directory
56+
path = Path(dir)
57+
if not path.exists():
58+
raise ValueError(f"Directory {dir} does not exist")
59+
if not path.is_dir():
60+
raise ValueError(f"Path {dir} is not a directory")
61+
62+
# Resolve to absolute path
63+
path = path.resolve()
64+
paths.add(path)
65+
66+
if len(paths) < 2:
67+
raise ValueError("Need at least two directories with --dirs to compare")
68+
69+
paths = sorted(list(paths))
70+
dir_pairs = list(combinations(paths, 2))
71+
return dir_pairs
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright 2024 ACCESS-NRI and contributors. See the top-level COPYRIGHT file for details.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from pathlib import Path
4+
5+
from model_config_tests.exp_test_helper import ExpTestHelper
6+
7+
8+
def get_lab_path(experiment: Path) -> Path:
9+
"""
10+
Derive the lab path from the experiment configuration directory
11+
archive symlink
12+
"""
13+
archive_symlink = experiment / "archive"
14+
if not archive_symlink.is_symlink():
15+
raise ValueError(f"Archive symlink does not exist in {experiment}.")
16+
17+
# experiment/archive symlink points to lab_path/archive/exp_name
18+
return archive_symlink.resolve().parent.parent
19+
20+
21+
def test_pairwise_repro(experiment_1: Path, experiment_2: Path):
22+
"""
23+
Compare combinations of experiments to check for reproducibility.
24+
This is parametrised in conftest with pytest_generate_tests to
25+
dynamically generate pairs of experiments to compare.
26+
"""
27+
28+
lab_path1 = get_lab_path(experiment_1)
29+
exp1 = ExpTestHelper(
30+
control_path=experiment_1, lab_path=lab_path1, disable_payu_run=True
31+
)
32+
33+
lab_path2 = get_lab_path(experiment_2)
34+
exp2 = ExpTestHelper(
35+
control_path=experiment_2, lab_path=lab_path2, disable_payu_run=True
36+
)
37+
38+
# Compare the two experiments - compares checksums from output000
39+
exp1_checksums = exp1.model.extract_full_checksums()
40+
exp2_checksums = exp2.model.extract_full_checksums()
41+
assert (
42+
exp1_checksums == exp2_checksums
43+
), f"Checksums do not match for {experiment_1.name} and {experiment_2.name} experiments"
File renamed without changes.

src/model_config_tests/qa/test_access_esm1p5_config.py renamed to src/model_config_tests/config_tests/qa/test_access_esm1p5_config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
import f90nml
1111
import pytest
1212

13-
from model_config_tests.qa.test_config import check_manifest_exes_in_spack_location
13+
from model_config_tests.config_tests.qa.test_config import (
14+
check_manifest_exes_in_spack_location,
15+
)
1416
from model_config_tests.util import get_git_branch_name
1517

1618
# Name of module on NCI

src/model_config_tests/qa/test_access_esm1p6_config.py renamed to src/model_config_tests/config_tests/qa/test_access_esm1p6_config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
import f90nml
1111
import pytest
1212

13-
from model_config_tests.qa.test_config import check_manifest_exes_in_spack_location
13+
from model_config_tests.config_tests.qa.test_config import (
14+
check_manifest_exes_in_spack_location,
15+
)
1416
from model_config_tests.util import get_git_branch_name
1517

1618
# Name of module on NCI

src/model_config_tests/qa/test_access_om2_config.py renamed to src/model_config_tests/config_tests/qa/test_access_om2_config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
import f90nml
1010
import pytest
1111

12-
from model_config_tests.qa.test_config import check_manifest_exes_in_spack_location
12+
from model_config_tests.config_tests.qa.test_config import (
13+
check_manifest_exes_in_spack_location,
14+
)
1315
from model_config_tests.util import get_git_branch_name
1416

1517
# Mutually exclusive topic keywords

0 commit comments

Comments
 (0)