Skip to content

Commit 659c1d0

Browse files
committed
Add payu test suite for spatial configuration
The spatial test suite runs CABLE with CRUJRA forcing at ACCESS resolution (see [here][cable_example] for more details) over all science configurations and model versions. Spatial tests use the [payu framework][payu]. The payu framework was chosen so that we: - Encourage uptake of payu amongst users of CABLE - Have the foundations in place for running coupled models (atmosphere + land) with payu - Can easily test longer running simulations (payu makes it easy to run a model multiple times and have state persist in the model via restart files) The run directory structure is organised as follows: runs/ ├── spatial │ └── tasks │ ├── <spatial-task-name> (a payu control / experiment directory) │ └── ... ├── payu-laboratory │ └── ... └── fluxsite └── ... Note we have a separate payu-laboratory directory. This is so we keep all CABLE outputs produced by benchcab under the bench_example work directory. Add the ability to build the CABLE executable with MPI at runtime so that we run the spatial configurations with MPI. Add the --mpi flag to benchcab build command so that the user can run the MPI build step independently. Add utility functions for git API requests and manipulating namelist files. Add subcommands to run each step of the spatial workflow in isolation. Add payu key in the benchcab config file so that users can easily configure payu experiments. Fixes #5 [payu]: https://github.com/payu-org/payu [cable_example]: https://github.com/CABLE-LSM/cable_example
1 parent b5fe7fd commit 659c1d0

20 files changed

Lines changed: 987 additions & 275 deletions

.conda/benchcab-dev.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ dependencies:
1010
- pytest-cov
1111
- pyyaml
1212
- flatdict
13+
- gitpython

benchcab/benchcab.py

Lines changed: 81 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@
1111
from benchcab import internal
1212
from benchcab.internal import get_met_forcing_file_names
1313
from benchcab.config import read_config
14-
from benchcab.workdir import setup_fluxsite_directory_tree, setup_src_dir
15-
from benchcab.repository import CableRepository
16-
from benchcab.fluxsite import (
17-
get_fluxsite_tasks,
18-
get_fluxsite_comparisons,
19-
run_tasks,
20-
run_tasks_in_parallel,
21-
Task,
14+
from benchcab.workdir import (
15+
setup_src_dir,
16+
setup_fluxsite_directory_tree,
17+
setup_spatial_directory_tree,
2218
)
19+
from benchcab.repository import CableRepository
20+
from benchcab import fluxsite
21+
from benchcab import spatial
2322
from benchcab.comparison import run_comparisons, run_comparisons_in_parallel
2423
from benchcab.cli import generate_parser
2524
from benchcab.environment_modules import EnvironmentModules, EnvironmentModulesInterface
@@ -48,7 +47,17 @@ def __init__(
4847
CableRepository(**config, repo_id=id)
4948
for id, config in enumerate(self.config["realisations"])
5049
]
51-
self.tasks: list[Task] = [] # initialise fluxsite tasks lazily
50+
self.science_configurations = self.config.get(
51+
"science_configurations", internal.DEFAULT_SCIENCE_CONFIGURATIONS
52+
)
53+
self.fluxsite_tasks: list[
54+
fluxsite.FluxsiteTask
55+
] = [] # initialise fluxsite tasks lazily
56+
self.spatial_tasks = spatial.get_spatial_tasks(
57+
repos=self.repos,
58+
met_forcings=internal.SPATIAL_DEFAULT_FORCINGS,
59+
science_configurations=self.science_configurations,
60+
)
5261
self.benchcab_exe_path = benchcab_exe_path
5362

5463
if validate_env:
@@ -103,18 +112,16 @@ def _validate_environment(self, project: str, modules: list):
103112
)
104113
sys.exit(1)
105114

106-
def _initialise_tasks(self) -> list[Task]:
115+
def _initialise_tasks(self) -> list[fluxsite.FluxsiteTask]:
107116
"""A helper method that initialises and returns the `tasks` attribute."""
108-
self.tasks = get_fluxsite_tasks(
117+
self.fluxsite_tasks = fluxsite.get_fluxsite_tasks(
109118
repos=self.repos,
110-
science_configurations=self.config.get(
111-
"science_configurations", internal.DEFAULT_SCIENCE_CONFIGURATIONS
112-
),
119+
science_configurations=self.science_configurations,
113120
fluxsite_forcing_file_names=get_met_forcing_file_names(
114121
self.config["experiment"]
115122
),
116123
)
117-
return self.tasks
124+
return self.fluxsite_tasks
118125

119126
def fluxsite_submit_job(self) -> None:
120127
"""Submits the PBS job script step in the fluxsite test workflow."""
@@ -189,7 +196,7 @@ def checkout(self):
189196

190197
print("")
191198

192-
def build(self):
199+
def build(self, mpi=False):
193200
"""Endpoint for `benchcab build`."""
194201
for repo in self.repos:
195202
if repo.build_script:
@@ -201,30 +208,38 @@ def build(self):
201208
modules=self.config["modules"], verbose=self.args.verbose
202209
)
203210
else:
204-
build_mode = "with MPI" if internal.MPI else "serially"
211+
build_mode = "with MPI" if mpi else "serially"
205212
print(f"Compiling CABLE {build_mode} for realisation {repo.name}...")
206-
repo.pre_build(verbose=self.args.verbose)
213+
repo.pre_build(mpi=mpi, verbose=self.args.verbose)
207214
repo.run_build(
208-
modules=self.config["modules"], verbose=self.args.verbose
215+
modules=self.config["modules"], mpi=mpi, verbose=self.args.verbose
209216
)
210-
repo.post_build(verbose=self.args.verbose)
217+
repo.post_build(mpi=mpi, verbose=self.args.verbose)
211218
print(f"Successfully compiled CABLE for realisation {repo.name}")
212219
print("")
213220

214221
def fluxsite_setup_work_directory(self):
215222
"""Endpoint for `benchcab fluxsite-setup-work-dir`."""
216-
tasks = self.tasks if self.tasks else self._initialise_tasks()
223+
224+
if not self.fluxsite_tasks:
225+
self._initialise_tasks()
226+
217227
print("Setting up run directory tree for fluxsite tests...")
218-
setup_fluxsite_directory_tree(fluxsite_tasks=tasks, verbose=self.args.verbose)
228+
setup_fluxsite_directory_tree(
229+
fluxsite_tasks=self.fluxsite_tasks, verbose=self.args.verbose
230+
)
219231
print("Setting up tasks...")
220-
for task in tasks:
232+
for task in self.fluxsite_tasks:
221233
task.setup_task(verbose=self.args.verbose)
222234
print("Successfully setup fluxsite tasks")
223235
print("")
224236

225237
def fluxsite_run_tasks(self):
226238
"""Endpoint for `benchcab fluxsite-run-tasks`."""
227-
tasks = self.tasks if self.tasks else self._initialise_tasks()
239+
240+
if not self.fluxsite_tasks:
241+
self._initialise_tasks()
242+
228243
print("Running fluxsite tasks...")
229244
try:
230245
multiprocess = self.config["fluxsite"]["multiprocess"]
@@ -234,9 +249,11 @@ def fluxsite_run_tasks(self):
234249
ncpus = self.config.get("pbs", {}).get(
235250
"ncpus", internal.FLUXSITE_DEFAULT_PBS["ncpus"]
236251
)
237-
run_tasks_in_parallel(tasks, n_processes=ncpus, verbose=self.args.verbose)
252+
fluxsite.run_tasks_in_parallel(
253+
self.fluxsite_tasks, n_processes=ncpus, verbose=self.args.verbose
254+
)
238255
else:
239-
run_tasks(tasks, verbose=self.args.verbose)
256+
fluxsite.run_tasks(self.fluxsite_tasks, verbose=self.args.verbose)
240257
print("Successfully ran fluxsite tasks")
241258
print("")
242259

@@ -248,8 +265,10 @@ def fluxsite_bitwise_cmp(self):
248265
"nccmp/1.8.5.0"
249266
) # use `nccmp -df` for bitwise comparisons
250267

251-
tasks = self.tasks if self.tasks else self._initialise_tasks()
252-
comparisons = get_fluxsite_comparisons(tasks)
268+
if not self.fluxsite_tasks:
269+
self._initialise_tasks()
270+
271+
comparisons = fluxsite.get_fluxsite_comparisons(self.fluxsite_tasks)
253272

254273
print("Running comparison tasks...")
255274
try:
@@ -280,13 +299,38 @@ def fluxsite(self):
280299
else:
281300
self.fluxsite_submit_job()
282301

302+
def spatial_setup_work_directory(self):
303+
"""Endpoint for `benchcab spatial-setup-work-dir`."""
304+
print("Setting up run directory tree for spatial tests...")
305+
setup_spatial_directory_tree()
306+
print("Setting up tasks...")
307+
for task in self.spatial_tasks:
308+
task.setup_task(verbose=self.args.verbose)
309+
print("Successfully setup spatial tasks")
310+
print("")
311+
312+
def spatial_run_tasks(self):
313+
"""Endpoint for `benchcab spatial-run-tasks`."""
314+
print("Running spatial tasks...")
315+
spatial.run_tasks(tasks=self.spatial_tasks, verbose=self.args.verbose)
316+
print("")
317+
283318
def spatial(self):
284319
"""Endpoint for `benchcab spatial`."""
320+
self.checkout()
321+
self.build(mpi=True)
322+
self.spatial_setup_work_directory()
323+
self.spatial_run_tasks()
285324

286325
def run(self):
287326
"""Endpoint for `benchcab run`."""
288-
self.fluxsite()
289-
self.spatial()
327+
self.checkout()
328+
self.build()
329+
self.build(mpi=True)
330+
self.fluxsite_setup_work_directory()
331+
self.spatial_setup_work_directory()
332+
self.fluxsite_submit_job()
333+
self.spatial_run_tasks()
290334

291335
def main(self):
292336
"""Main function for `benchcab`."""
@@ -298,7 +342,7 @@ def main(self):
298342
self.checkout()
299343

300344
if self.args.subcommand == "build":
301-
self.build()
345+
self.build(mpi=self.args.mpi)
302346

303347
if self.args.subcommand == "fluxsite":
304348
self.fluxsite()
@@ -318,6 +362,12 @@ def main(self):
318362
if self.args.subcommand == "spatial":
319363
self.spatial()
320364

365+
if self.args.subcommand == "spatial-setup-work-dir":
366+
self.spatial_setup_work_directory()
367+
368+
if self.args.subcommand == "spatial-run-tasks":
369+
self.spatial_run_tasks()
370+
321371

322372
def main():
323373
"""Main program entry point for `benchcab`.

benchcab/cli.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ def generate_parser() -> argparse.ArgumentParser:
3232
action="store_true",
3333
)
3434

35-
# parent parser that contains arguments common to all run specific subcommands
36-
args_run_subcommand = argparse.ArgumentParser(add_help=False)
37-
args_run_subcommand.add_argument(
35+
# parent parser that contains the argument for --no-submit
36+
args_no_submit = argparse.ArgumentParser(add_help=False)
37+
args_no_submit.add_argument(
3838
"--no-submit",
3939
action="store_true",
4040
help="Force benchcab to execute tasks on the current compute node.",
@@ -74,7 +74,6 @@ def generate_parser() -> argparse.ArgumentParser:
7474
parents=[
7575
args_help,
7676
args_subcommand,
77-
args_run_subcommand,
7877
args_composite_subcommand,
7978
],
8079
help="Run all test suites for CABLE.",
@@ -89,7 +88,7 @@ def generate_parser() -> argparse.ArgumentParser:
8988
parents=[
9089
args_help,
9190
args_subcommand,
92-
args_run_subcommand,
91+
args_no_submit,
9392
args_composite_subcommand,
9493
],
9594
help="Run the fluxsite test suite for CABLE.",
@@ -110,14 +109,19 @@ def generate_parser() -> argparse.ArgumentParser:
110109
)
111110

112111
# subcommand: 'benchcab build'
113-
subparsers.add_parser(
112+
build_parser = subparsers.add_parser(
114113
"build",
115114
parents=[args_help, args_subcommand],
116115
help="Run the build step in the benchmarking workflow.",
117116
description="""Build the CABLE offline executable for each repository specified in the
118117
config file.""",
119118
add_help=False,
120119
)
120+
build_parser.add_argument(
121+
"--mpi",
122+
action="store_true",
123+
help="Enable MPI build.",
124+
)
121125

122126
# subcommand: 'benchcab fluxsite-setup-work-dir'
123127
subparsers.add_parser(
@@ -143,9 +147,9 @@ def generate_parser() -> argparse.ArgumentParser:
143147
"fluxsite-run-tasks",
144148
parents=[args_help, args_subcommand],
145149
help="Run the fluxsite tasks of the main fluxsite command.",
146-
description="""Runs the fluxsite tasks for the fluxsite test suite. Note, this command should
147-
ideally be run inside a PBS job. This command is invoked by the PBS job script generated by
148-
`benchcab run`.""",
150+
description="""Runs the fluxsite tasks for the fluxsite test suite.
151+
Note, this command should ideally be run inside a PBS job. This command
152+
is invoked by the PBS job script generated by `benchcab run`.""",
149153
add_help=False,
150154
)
151155

@@ -165,10 +169,29 @@ def generate_parser() -> argparse.ArgumentParser:
165169
# subcommand: 'benchcab spatial'
166170
subparsers.add_parser(
167171
"spatial",
168-
parents=[args_help, args_subcommand],
172+
parents=[args_help, args_subcommand, args_composite_subcommand],
169173
help="Run the spatial tests only.",
170174
description="""Runs the default spatial test suite for CABLE.""",
171175
add_help=False,
172176
)
173177

178+
# subcommand: 'benchcab spatial-setup-work-dir'
179+
subparsers.add_parser(
180+
"spatial-setup-work-dir",
181+
parents=[args_help, args_subcommand],
182+
help="Run the work directory setup step of the spatial command.",
183+
description="""Generates the spatial run directory tree in the current working
184+
directory so that spatial tasks can be run.""",
185+
add_help=False,
186+
)
187+
188+
# subcommand 'benchcab spatial-run-tasks'
189+
subparsers.add_parser(
190+
"spatial-run-tasks",
191+
parents=[args_help, args_subcommand],
192+
help="Run the spatial tasks of the main spatial command.",
193+
description="Runs the spatial tasks for the spatial test suite.",
194+
add_help=False,
195+
)
196+
174197
return main_parser

benchcab/config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,21 @@ def check_config(config: dict):
7979
):
8080
raise TypeError("The 'multiprocessing' key must be a boolean.")
8181

82+
# the "spatial" key is optional
83+
if "spatial" in config:
84+
if not isinstance(config["spatial"], dict):
85+
raise TypeError("The 'spatial' key must be a dictionary.")
86+
# the "payu" key is optional
87+
if "payu" in config["spatial"]:
88+
if not isinstance(config["spatial"]["payu"], dict):
89+
raise TypeError("The 'payu' key must be a dictionary.")
90+
# the "met_forcings" key is optional
91+
if "met_forcings" in config["spatial"]:
92+
if not isinstance(config["spatial"]["met_forcings"], list) or any(
93+
not isinstance(val, str) for val in config["spatial"]["met_forcings"]
94+
):
95+
raise TypeError("The 'met_forcings' key must be a list of strings.")
96+
8297
valid_experiments = (
8398
list(internal.MEORG_EXPERIMENTS) + internal.MEORG_EXPERIMENTS["five-site-test"]
8499
)

0 commit comments

Comments
 (0)