Skip to content

Commit f2ba953

Browse files
committed
EAMxx: improve test based on feedback
1 parent f3946a7 commit f2ba953

File tree

5 files changed

+110
-95
lines changed

5 files changed

+110
-95
lines changed

components/eamxx/cime_config/SystemTests/rcs.py

Lines changed: 48 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@
1515
- rcs_stats.py: functions to conduct statistical testing
1616
"""
1717

18-
import os
1918
import glob
2019
import logging
2120
import sys
21+
from pathlib import Path
2222

2323
import CIME.test_status
2424
import CIME.utils
2525
from CIME.status import append_testlog
2626
from CIME.SystemTests.system_tests_common import SystemTestsCommon
27-
from CIME.case.case_setup import case_setup
27+
from CIME.utils import expect
2828

2929
logger = logging.getLogger(__name__)
3030

@@ -56,37 +56,33 @@ def setup_phase(
5656
disable_git=disable_git,
5757
)
5858
self._case.flush()
59-
# and again...?
60-
case_setup(self._case, test_mode=False, reset=True)
6159

6260
# get run directory
6361
run_dir = self._case.get_value("RUNDIR")
6462
# get n_inst
6563
n_inst = int(self._case.get_value("NINST_ATM"))
6664
# return early if n_inst <= 1
6765
# we really don't want people to run this test with n_inst=1
68-
if n_inst <= 1:
69-
msg = (
70-
f"NINST_ATM = {n_inst}. This test requires NINST_ATM > 1. "
71-
"Consider setting NINST_ATM > 1 in your env_run.xml "
72-
"or use _C# specifier in test name for a multi-driver "
73-
"multi-instance setup (producing # pelayout copies), "
74-
"or _N# for a single-driver multi-instance setup "
75-
"(dividing specified pelayout among # instances)."
76-
)
77-
raise ValueError(msg)
66+
expect(
67+
n_inst > 1,
68+
f"NINST_ATM = {n_inst}. This test requires NINST_ATM > 1. "
69+
"Consider setting NINST_ATM > 1 in your env_run.xml "
70+
"or use _C# specifier in test name for a multi-driver "
71+
"multi-instance setup (producing # pelayout copies), "
72+
"or _N# for a single-driver multi-instance setup "
73+
"(dividing specified pelayout among # instances)."
74+
)
7875

7976
# get rcs_perts functions
8077
# but first add the directory to sys.path if not already there
81-
rcs_perts_path = os.path.join(
82-
os.path.dirname(__file__), 'rcs_perts.py'
78+
rcs_perts_path = Path(__file__).parent / 'rcs_perts.py'
79+
expect(
80+
rcs_perts_path.exists(),
81+
f"Cannot find rcs_perts.py at {rcs_perts_path}"
8382
)
84-
if not os.path.exists(rcs_perts_path):
85-
raise ImportError(
86-
f"Cannot find rcs_perts.py at {rcs_perts_path}"
87-
)
88-
if os.path.dirname(__file__) not in sys.path:
89-
sys.path.insert(0, os.path.dirname(__file__))
83+
script_dir = str(Path(__file__).parent)
84+
if script_dir not in sys.path:
85+
sys.path.insert(0, script_dir)
9086
# pylint: disable=import-outside-toplevel
9187
from rcs_perts import duplicate_yaml_file, update_yaml_file
9288

@@ -98,11 +94,14 @@ def setup_phase(
9894
for i in range(1, n_inst + 1):
9995
yaml_file = f"{run_dir}/data/scream_input.yaml_{i:04d}"
10096
out_file = f"{run_dir}/data/monthly_average.yaml_{i:04d}"
101-
if not os.path.isfile(yaml_file):
102-
raise FileNotFoundError(
103-
f"File {yaml_file} does not exist.")
104-
if not os.path.isfile(out_file):
105-
raise FileNotFoundError(f"File {out_file} does not exist.")
97+
expect(
98+
Path(yaml_file).is_file(),
99+
f"File {yaml_file} does not exist."
100+
)
101+
expect(
102+
Path(out_file).is_file(),
103+
f"File {out_file} does not exist."
104+
)
106105
update_yaml_file(yaml_file, i, "pert")
107106
update_yaml_file(out_file, i, "out")
108107

@@ -113,32 +112,29 @@ def _generate_baseline(self):
113112

114113
with CIME.utils.SharedArea():
115114
# get the baseline and run directories
116-
base_gen_dir = os.path.join(
117-
self._case.get_value("BASELINE_ROOT"),
118-
self._case.get_value("BASEGEN_CASE"),
119-
)
120-
run_dir = self._case.get_value("RUNDIR")
115+
baseline_root = Path(self._case.get_value("BASELINE_ROOT"))
116+
basegen_case = self._case.get_value("BASEGEN_CASE")
117+
base_gen_dir = baseline_root / basegen_case
118+
run_dir = Path(self._case.get_value("RUNDIR"))
121119

122120
# Get all files that match the ensemble pattern
123-
hists = glob.glob(
124-
os.path.join(run_dir, self.ENSEMBLE_FILE_PATTERN)
125-
)
126-
hist_files = [os.path.basename(h) for h in hists]
121+
hists = glob.glob(str(run_dir / self.ENSEMBLE_FILE_PATTERN))
122+
hist_files = [Path(h).name for h in hists]
127123

128124
for hist in hist_files:
129-
src = os.path.join(run_dir, hist)
130-
tgt = os.path.join(base_gen_dir, hist)
125+
src = run_dir / hist
126+
tgt = base_gen_dir / hist
131127
# remove baselines if they exist
132128
# this is safe because cime forces users to use -o
133-
if os.path.exists(tgt):
134-
os.remove(tgt)
129+
if tgt.exists():
130+
tgt.unlink()
135131

136132
# log and copy
137133
logger.info(
138134
"Copying ... \n \t %s \n ... to ... \n \t %s \n\n",
139135
src, tgt
140136
)
141-
CIME.utils.safe_copy(src, tgt, preserve_meta=False)
137+
CIME.utils.safe_copy(str(src), str(tgt), preserve_meta=False)
142138

143139
def _compare_baseline(self):
144140
"""compare phase implementation"""
@@ -159,31 +155,29 @@ def _compare_baseline(self):
159155

160156
# get the run and baseline directories
161157
run_dir = self._case.get_value("RUNDIR")
162-
base_dir = os.path.join(
163-
self._case.get_value("BASELINE_ROOT"),
164-
self._case.get_value("BASECMP_CASE"),
165-
)
158+
baseline_root = Path(self._case.get_value("BASELINE_ROOT"))
159+
basecmp_case = self._case.get_value("BASECMP_CASE")
160+
base_dir = baseline_root / basecmp_case
166161

167162
# launch the statistics tests
168163
# first, import rcs_stats funcs from the other file
169-
rcs_stats_path = os.path.join(
170-
os.path.dirname(__file__), 'rcs_stats.py'
164+
rcs_stats_path = Path(__file__).parent / 'rcs_stats.py'
165+
expect(
166+
rcs_stats_path.exists(),
167+
f"Cannot find rcs_stats.py at {rcs_stats_path}"
171168
)
172-
if not os.path.exists(rcs_stats_path):
173-
raise ImportError(
174-
f"Cannot find rcs_stats.py at {rcs_stats_path}"
175-
)
176169
# Add the directory to sys.path if not already there
177-
if os.path.dirname(__file__) not in sys.path:
178-
sys.path.insert(0, os.path.dirname(__file__))
170+
script_dir = str(Path(__file__).parent)
171+
if script_dir not in sys.path:
172+
sys.path.insert(0, script_dir)
179173
# note be extra safe and import whole file
180174
# because we want to avoid import errors of needed pkgs
181175
# pylint: disable=import-outside-toplevel
182176
import rcs_stats as rcss
183177
# now, launch
184178
comments, new_ts = rcss.run_stats_comparison(
185179
run_dir,
186-
base_dir,
180+
str(base_dir),
187181
analysis_type="spatiotemporal",
188182
test_type="ks",
189183
alpha=0.01,

components/eamxx/cime_config/SystemTests/rcs_perts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
Perturbation functions for EST system test.
2+
Perturbation functions for RCS system test.
33
"""
44

55
import os

components/eamxx/cime_config/SystemTests/rcs_stats.py

Lines changed: 58 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,50 +1142,59 @@ def _prepare_variable_data(var):
11421142
return var
11431143

11441144

1145-
if __name__ == "__main__":
1145+
###############################################################################
1146+
def parse_command_line(args, description):
1147+
###############################################################################
11461148
import argparse
1149+
from pathlib import Path
11471150

11481151
parser = argparse.ArgumentParser(
1149-
description="Statistical comparison of two ensemble simulations.",
1152+
usage="""\n{0} run_dir base_dir [options]
1153+
OR
1154+
{0} --help
1155+
1156+
\033[1mEXAMPLES:\033[0m
1157+
\033[1;32m# Default: KS test with Bonferroni correction\033[0m
1158+
> {0} /path/to/run /path/to/baseline
1159+
1160+
\033[1;32m# Anderson-Darling with temporal analysis\033[0m
1161+
> {0} /path/to/run /path/to/baseline --test_type ad \\
1162+
--analysis_type temporal
1163+
1164+
\033[1;32m# Custom significance level with FDR correction\033[0m
1165+
> {0} /path/to/run /path/to/baseline --test_type ks \\
1166+
--alpha 0.001 --correction_method fdr
1167+
1168+
\033[1;32m# No multiple testing correction\033[0m
1169+
> {0} /path/to/run /path/to/baseline \\
1170+
--correction_method none --magnitude_threshold 0.01
1171+
1172+
\033[1;32m# Custom file patterns\033[0m
1173+
> {0} /path/to/run /path/to/baseline \\
1174+
--run_file_pattern "*.eam_????.h0.*.nc" \\
1175+
--base_file_pattern "*.scream_????.h.AVERAGE.*.nc"
1176+
""".format(Path(args[0]).name),
1177+
description=description,
11501178
formatter_class=argparse.RawDescriptionHelpFormatter,
11511179
epilog="""
1152-
Available Statistical Tests:
1180+
\033[1mAVAILABLE STATISTICAL TESTS:\033[0m
11531181
1154-
DISTRIBUTION TESTS (compare entire distributions):
1182+
\033[1mDISTRIBUTION TESTS\033[0m (compare entire distributions):
11551183
ks Kolmogorov-Smirnov (recommended default)
11561184
ad Anderson-Darling (high sensitivity, especially tails)
11571185
cvm Cramér-von Mises (moderate-high sensitivity)
11581186
epps Epps-Singleton (location + scale)
11591187
energy Energy distance (powerful, any difference)
11601188
1161-
LOCATION TESTS (compare means/medians):
1189+
\033[1mLOCATION TESTS\033[0m (compare means/medians):
11621190
mw Mann-Whitney U (non-parametric median test)
11631191
ttest Welch's t-test (parametric mean test)
11641192
brunner Brunner-Munzel (robust alternative to t-test)
11651193
1166-
SCALE TESTS (compare variances/spread):
1194+
\033[1mSCALE TESTS\033[0m (compare variances/spread):
11671195
levene Levene's test (variance equality)
11681196
ansari Ansari-Bradley (non-parametric scale)
11691197
mood Mood's test (non-parametric dispersion)
1170-
1171-
Examples:
1172-
# Default: KS test with spatiotemporal analysis and Bonferroni correction
1173-
%(prog)s /path/to/run /path/to/baseline
1174-
1175-
# Anderson-Darling with temporal analysis
1176-
%(prog)s /path/to/run /path/to/baseline --test_type ad --analysis_type temporal
1177-
1178-
# Custom significance level with FDR correction
1179-
%(prog)s /path/to/run /path/to/baseline --test_type ks --alpha 0.001 --correction_method fdr
1180-
1181-
# No multiple testing correction with magnitude threshold
1182-
%(prog)s /path/to/run /path/to/baseline \\
1183-
--correction_method none --magnitude_threshold 0.01
1184-
1185-
# Custom file patterns for different output formats
1186-
%(prog)s /path/to/run /path/to/baseline \\
1187-
--run_file_pattern "*.eam_????.h0.*.nc" \\
1188-
--base_file_pattern "*.scream_????.h.AVERAGE.*.nc"
11891198
""",
11901199
)
11911200

@@ -1268,20 +1277,26 @@ def _prepare_variable_data(var):
12681277
"number, default: *.scream_????.h.AVERAGE.*.nc)",
12691278
)
12701279

1271-
args = parser.parse_args()
1280+
return parser.parse_args(args[1:])
1281+
1282+
1283+
###############################################################################
1284+
def _main_func(description):
1285+
###############################################################################
1286+
cli_args = parse_command_line(sys.argv, description)
12721287

12731288
cli_comments, cli_status = run_stats_comparison(
1274-
args.run_dir,
1275-
args.base_dir,
1276-
analysis_type=args.analysis_type,
1277-
test_type=args.test_type,
1278-
alpha=args.alpha,
1279-
critical_fraction=args.critical_fraction,
1280-
correction_method=args.correction_method,
1281-
max_failed_vars=args.max_failed_vars,
1282-
magnitude_threshold=args.magnitude_threshold,
1283-
run_file_pattern=args.run_file_pattern,
1284-
base_file_pattern=args.base_file_pattern,
1289+
cli_args.run_dir,
1290+
cli_args.base_dir,
1291+
analysis_type=cli_args.analysis_type,
1292+
test_type=cli_args.test_type,
1293+
alpha=cli_args.alpha,
1294+
critical_fraction=cli_args.critical_fraction,
1295+
correction_method=cli_args.correction_method,
1296+
max_failed_vars=cli_args.max_failed_vars,
1297+
magnitude_threshold=cli_args.magnitude_threshold,
1298+
run_file_pattern=cli_args.run_file_pattern,
1299+
base_file_pattern=cli_args.base_file_pattern,
12851300
)
12861301

12871302
print("\n")
@@ -1295,3 +1310,9 @@ def _prepare_variable_data(var):
12951310
print("=" * 70)
12961311
print("\n")
12971312
print(cli_comments)
1313+
1314+
1315+
###############################################################################
1316+
1317+
if (__name__ == "__main__"):
1318+
_main_func(__doc__)

components/eamxx/cime_config/config_tests.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ This defines any EAMxx specific CIME tests
1313
<CONTINUE_RUN>FALSE</CONTINUE_RUN>
1414
<STOP_OPTION>nmonths</STOP_OPTION>
1515
<STOP_N>12</STOP_N>
16-
<REST_OPTION>$STOP_OPTION</REST_OPTION>
16+
<REST_OPTION>never</REST_OPTION>
1717
<REST_N>$STOP_N</REST_N>
18-
<HIST_OPTION>$STOP_OPTION</HIST_OPTION>
18+
<HIST_OPTION>never</HIST_OPTION>
1919
<HIST_N>$STOP_N</HIST_N>
2020
<RESUBMIT>0</RESUBMIT>
2121
</test>

components/eamxx/docs/user/multi-instance-rcs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ These tests focus on differences in variability:
122122
- Best for: Non-parametric scale comparison
123123
- Assumption: Samples differ primarily in scale, not location
124124

125-
###### `mood` - Mood's Test
125+
##### `mood` - Mood's Test
126126

127127
- Sensitivity: Moderate
128128
- Best for: Non-parametric dispersion comparison

0 commit comments

Comments
 (0)