Skip to content

Commit 9482458

Browse files
authored
Merge pull request #136 from CABLE-LSM/5-spatial-testing
Add payu test suite for spatial configuration
2 parents 3783439 + ce3608e commit 9482458

25 files changed

Lines changed: 1065 additions & 327 deletions

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@
77

88
- checks out the model versions specified by the user
99
- builds the required executables
10-
- runs each model version across N standard science configurations
10+
- runs each model version across N standard science configurations for a variety of meteorological forcings
1111
- performs bitwise comparison checks on model outputs across model versions
1212

1313
The user can then pipe the model outputs into a benchmark analysis via [modelevaluation.org][meorg] to assess model performance.
1414

1515
The full documentation is available at [benchcab.readthedocs.io][docs].
1616

17+
## Supported configurations
18+
19+
`benchcab` currently tests the following model configurations for CABLE:
20+
21+
- **Flux site simulations (offline)** - running CABLE forced with observed eddy covariance data at a single site
22+
- **Global/regional simulations (offline)** - running CABLE forced with meteorological fields over a region (global or regional)
23+
1724
## License
1825

1926
`benchcab` is distributed under [an Apache License v2.0][apache-license].

benchcab/benchcab.py

Lines changed: 77 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,21 @@
1010
from subprocess import CalledProcessError
1111
from typing import Optional
1212

13-
from benchcab import internal
13+
from benchcab import fluxsite, internal, spatial
1414
from benchcab.comparison import run_comparisons, run_comparisons_in_parallel
1515
from benchcab.config import read_config
1616
from benchcab.environment_modules import EnvironmentModules, EnvironmentModulesInterface
17-
from benchcab.fluxsite import (
18-
Task,
19-
get_fluxsite_comparisons,
20-
get_fluxsite_tasks,
21-
run_tasks,
22-
run_tasks_in_parallel,
23-
)
2417
from benchcab.internal import get_met_forcing_file_names
2518
from benchcab.model import Model
2619
from benchcab.utils import get_logger
2720
from benchcab.utils.fs import mkdir, next_path
2821
from benchcab.utils.pbs import render_job_script
2922
from benchcab.utils.repo import create_repo
3023
from benchcab.utils.subprocess import SubprocessWrapper, SubprocessWrapperInterface
31-
from benchcab.workdir import setup_fluxsite_directory_tree
24+
from benchcab.workdir import (
25+
setup_fluxsite_directory_tree,
26+
setup_spatial_directory_tree,
27+
)
3228

3329

3430
class Benchcab:
@@ -57,7 +53,8 @@ def __init__(
5753

5854
self._config: Optional[dict] = None
5955
self._models: list[Model] = []
60-
self.tasks: list[Task] = [] # initialise fluxsite tasks lazily
56+
self._fluxsite_tasks: list[fluxsite.FluxsiteTask] = []
57+
self._spatial_tasks: list[spatial.SpatialTask] = []
6158

6259
# Get the logger object
6360
self.logger = get_logger()
@@ -148,16 +145,26 @@ def _get_models(self, config: dict) -> list[Model]:
148145
self._models.append(Model(repo=repo, model_id=id, **sub_config))
149146
return self._models
150147

151-
def _initialise_tasks(self, config: dict) -> list[Task]:
152-
"""A helper method that initialises and returns the `tasks` attribute."""
153-
self.tasks = get_fluxsite_tasks(
154-
models=self._get_models(config),
155-
science_configurations=config["science_configurations"],
156-
fluxsite_forcing_file_names=get_met_forcing_file_names(
157-
config["fluxsite"]["experiment"]
158-
),
159-
)
160-
return self.tasks
148+
def _get_fluxsite_tasks(self, config: dict) -> list[fluxsite.FluxsiteTask]:
149+
if not self._fluxsite_tasks:
150+
self._fluxsite_tasks = fluxsite.get_fluxsite_tasks(
151+
models=self._get_models(config),
152+
science_configurations=config["science_configurations"],
153+
fluxsite_forcing_file_names=get_met_forcing_file_names(
154+
config["fluxsite"]["experiment"]
155+
),
156+
)
157+
return self._fluxsite_tasks
158+
159+
def _get_spatial_tasks(self, config) -> list[spatial.SpatialTask]:
160+
if not self._spatial_tasks:
161+
self._spatial_tasks = spatial.get_spatial_tasks(
162+
models=self._get_models(config),
163+
met_forcings=config["spatial"]["met_forcings"],
164+
science_configurations=config["science_configurations"],
165+
payu_args=config["spatial"]["payu"]["args"],
166+
)
167+
return self._spatial_tasks
161168

162169
def validate_config(self, config_path: str):
163170
"""Endpoint for `benchcab validate_config`."""
@@ -226,7 +233,7 @@ def checkout(self, config_path: str):
226233
with rev_number_log_path.open("w", encoding="utf-8") as file:
227234
file.write(rev_number_log)
228235

229-
def build(self, config_path: str):
236+
def build(self, config_path: str, mpi=False):
230237
"""Endpoint for `benchcab build`."""
231238
config = self._get_config(config_path)
232239
self._validate_environment(project=config["project"], modules=config["modules"])
@@ -239,40 +246,39 @@ def build(self, config_path: str):
239246
repo.custom_build(modules=config["modules"])
240247

241248
else:
242-
build_mode = "with MPI" if internal.MPI else "serially"
249+
build_mode = "with MPI" if mpi else "serially"
243250
self.logger.info(
244251
f"Compiling CABLE {build_mode} for realisation {repo.name}..."
245252
)
246-
repo.pre_build()
247-
repo.run_build(modules=config["modules"])
248-
repo.post_build()
253+
repo.pre_build(mpi=mpi)
254+
repo.run_build(modules=config["modules"], mpi=mpi)
255+
repo.post_build(mpi=mpi)
249256
self.logger.info(f"Successfully compiled CABLE for realisation {repo.name}")
250257

251258
def fluxsite_setup_work_directory(self, config_path: str):
252259
"""Endpoint for `benchcab fluxsite-setup-work-dir`."""
253260
config = self._get_config(config_path)
254261
self._validate_environment(project=config["project"], modules=config["modules"])
255262

256-
tasks = self.tasks if self.tasks else self._initialise_tasks(config)
257263
self.logger.info("Setting up run directory tree for fluxsite tests...")
258264
setup_fluxsite_directory_tree()
259265
self.logger.info("Setting up tasks...")
260-
for task in tasks:
266+
for task in self._get_fluxsite_tasks(config):
261267
task.setup_task()
262268
self.logger.info("Successfully setup fluxsite tasks")
263269

264270
def fluxsite_run_tasks(self, config_path: str):
265271
"""Endpoint for `benchcab fluxsite-run-tasks`."""
266272
config = self._get_config(config_path)
267273
self._validate_environment(project=config["project"], modules=config["modules"])
274+
tasks = self._get_fluxsite_tasks(config)
268275

269-
tasks = self.tasks if self.tasks else self._initialise_tasks(config)
270276
self.logger.info("Running fluxsite tasks...")
271277
if config["fluxsite"]["multiprocess"]:
272278
ncpus = config["fluxsite"]["pbs"]["ncpus"]
273-
run_tasks_in_parallel(tasks, n_processes=ncpus)
279+
fluxsite.run_tasks_in_parallel(tasks, n_processes=ncpus)
274280
else:
275-
run_tasks(tasks)
281+
fluxsite.run_tasks(tasks)
276282
self.logger.info("Successfully ran fluxsite tasks")
277283

278284
def fluxsite_bitwise_cmp(self, config_path: str):
@@ -285,8 +291,9 @@ def fluxsite_bitwise_cmp(self, config_path: str):
285291
"nccmp/1.8.5.0"
286292
) # use `nccmp -df` for bitwise comparisons
287293

288-
tasks = self.tasks if self.tasks else self._initialise_tasks(config)
289-
comparisons = get_fluxsite_comparisons(tasks)
294+
comparisons = fluxsite.get_fluxsite_comparisons(
295+
self._get_fluxsite_tasks(config)
296+
)
290297

291298
self.logger.info("Running comparison tasks...")
292299
if config["fluxsite"]["multiprocess"]:
@@ -308,10 +315,44 @@ def fluxsite(self, config_path: str, no_submit: bool, skip: list[str]):
308315
else:
309316
self.fluxsite_submit_job(config_path, skip)
310317

311-
def spatial(self, config_path: str):
318+
def spatial_setup_work_directory(self, config_path: str):
319+
"""Endpoint for `benchcab spatial-setup-work-dir`."""
320+
config = self._get_config(config_path)
321+
self._validate_environment(project=config["project"], modules=config["modules"])
322+
323+
self.logger.info("Setting up run directory tree for spatial tests...")
324+
setup_spatial_directory_tree()
325+
self.logger.info("Setting up tasks...")
326+
try:
327+
payu_config = config["spatial"]["payu"]["config"]
328+
except KeyError:
329+
payu_config = None
330+
for task in self._get_spatial_tasks(config):
331+
task.setup_task(payu_config=payu_config)
332+
self.logger.info("Successfully setup spatial tasks")
333+
334+
def spatial_run_tasks(self, config_path: str):
335+
"""Endpoint for `benchcab spatial-run-tasks`."""
336+
config = self._get_config(config_path)
337+
self._validate_environment(project=config["project"], modules=config["modules"])
338+
339+
self.logger.info("Running spatial tasks...")
340+
spatial.run_tasks(tasks=self._get_spatial_tasks(config))
341+
self.logger.info("Successfully dispatched payu jobs")
342+
343+
def spatial(self, config_path: str, skip: list):
312344
"""Endpoint for `benchcab spatial`."""
345+
self.checkout(config_path)
346+
self.build(config_path, mpi=True)
347+
self.spatial_setup_work_directory(config_path)
348+
self.spatial_run_tasks(config_path)
313349

314-
def run(self, config_path: str, no_submit: bool, skip: list[str]):
350+
def run(self, config_path: str, skip: list[str]):
315351
"""Endpoint for `benchcab run`."""
316-
self.fluxsite(config_path, no_submit, skip)
317-
self.spatial(config_path)
352+
self.checkout(config_path)
353+
self.build(config_path)
354+
self.build(config_path, mpi=True)
355+
self.fluxsite_setup_work_directory(config_path)
356+
self.spatial_setup_work_directory(config_path)
357+
self.fluxsite_submit_job(config_path, skip)
358+
self.spatial_run_tasks(config_path)

benchcab/cli.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ def generate_parser(app: Benchcab) -> argparse.ArgumentParser:
3838
action="store_true",
3939
)
4040

41-
# parent parser that contains arguments common to all run specific subcommands
42-
args_run_subcommand = argparse.ArgumentParser(add_help=False)
43-
args_run_subcommand.add_argument(
41+
# parent parser that contains the argument for --no-submit
42+
args_no_submit = argparse.ArgumentParser(add_help=False)
43+
args_no_submit.add_argument(
4444
"--no-submit",
4545
action="store_true",
4646
help="Force benchcab to execute tasks on the current compute node.",
@@ -80,7 +80,6 @@ def generate_parser(app: Benchcab) -> argparse.ArgumentParser:
8080
parents=[
8181
args_help,
8282
args_subcommand,
83-
args_run_subcommand,
8483
args_composite_subcommand,
8584
],
8685
help="Run all test suites for CABLE.",
@@ -109,7 +108,7 @@ def generate_parser(app: Benchcab) -> argparse.ArgumentParser:
109108
parents=[
110109
args_help,
111110
args_subcommand,
112-
args_run_subcommand,
111+
args_no_submit,
113112
args_composite_subcommand,
114113
],
115114
help="Run the fluxsite test suite for CABLE.",
@@ -140,6 +139,11 @@ def generate_parser(app: Benchcab) -> argparse.ArgumentParser:
140139
config file.""",
141140
add_help=False,
142141
)
142+
parser_build.add_argument(
143+
"--mpi",
144+
action="store_true",
145+
help="Enable MPI build.",
146+
)
143147
parser_build.set_defaults(func=app.build)
144148

145149
# subcommand: 'benchcab fluxsite-setup-work-dir'
@@ -168,9 +172,9 @@ def generate_parser(app: Benchcab) -> argparse.ArgumentParser:
168172
"fluxsite-run-tasks",
169173
parents=[args_help, args_subcommand],
170174
help="Run the fluxsite tasks of the main fluxsite command.",
171-
description="""Runs the fluxsite tasks for the fluxsite test suite. Note, this command should
172-
ideally be run inside a PBS job. This command is invoked by the PBS job script generated by
173-
`benchcab run`.""",
175+
description="""Runs the fluxsite tasks for the fluxsite test suite.
176+
Note, this command should ideally be run inside a PBS job. This command
177+
is invoked by the PBS job script generated by `benchcab run`.""",
174178
add_help=False,
175179
)
176180
parser_fluxsite_run_tasks.set_defaults(func=app.fluxsite_run_tasks)
@@ -192,11 +196,32 @@ def generate_parser(app: Benchcab) -> argparse.ArgumentParser:
192196
# subcommand: 'benchcab spatial'
193197
parser_spatial = subparsers.add_parser(
194198
"spatial",
195-
parents=[args_help, args_subcommand],
199+
parents=[args_help, args_subcommand, args_composite_subcommand],
196200
help="Run the spatial tests only.",
197201
description="""Runs the default spatial test suite for CABLE.""",
198202
add_help=False,
199203
)
200204
parser_spatial.set_defaults(func=app.spatial)
201205

206+
# subcommand: 'benchcab spatial-setup-work-dir'
207+
parser_spatial_setup_work_dir = subparsers.add_parser(
208+
"spatial-setup-work-dir",
209+
parents=[args_help, args_subcommand],
210+
help="Run the work directory setup step of the spatial command.",
211+
description="""Generates the spatial run directory tree in the current working
212+
directory so that spatial tasks can be run.""",
213+
add_help=False,
214+
)
215+
parser_spatial_setup_work_dir.set_defaults(func=app.spatial_setup_work_directory)
216+
217+
# subcommand 'benchcab spatial-run-tasks'
218+
parser_spatial_run_tasks = subparsers.add_parser(
219+
"spatial-run-tasks",
220+
parents=[args_help, args_subcommand],
221+
help="Run the spatial tasks of the main spatial command.",
222+
description="Runs the spatial tasks for the spatial test suite.",
223+
add_help=False,
224+
)
225+
parser_spatial_run_tasks.set_defaults(func=app.spatial_run_tasks)
226+
202227
return main_parser

benchcab/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@ def read_optional_key(config: dict):
9696
"science_configurations", internal.DEFAULT_SCIENCE_CONFIGURATIONS
9797
)
9898

99+
# Default values for spatial
100+
config["spatial"] = config.get("spatial", {})
101+
102+
config["spatial"]["met_forcings"] = config["spatial"].get(
103+
"met_forcings", internal.SPATIAL_DEFAULT_MET_FORCINGS
104+
)
105+
106+
config["spatial"]["payu"] = config["spatial"].get("payu", {})
107+
config["spatial"]["payu"]["config"] = config["spatial"]["payu"].get("config", {})
108+
config["spatial"]["payu"]["args"] = config["spatial"]["payu"].get("args")
109+
110+
# Default values for fluxsite
99111
config["fluxsite"] = config.get("fluxsite", {})
100112

101113
config["fluxsite"]["multiprocess"] = config["fluxsite"].get(

benchcab/data/config-schema.yml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,27 @@ fluxsite:
9797
schema:
9898
type: "string"
9999
required: false
100-
100+
101+
spatial:
102+
type: "dict"
103+
required: false
104+
schema:
105+
met_forcings:
106+
type: "dict"
107+
required: false
108+
minlength: 1
109+
keysrules:
110+
type: "string"
111+
valuesrules:
112+
type: "string"
113+
payu:
114+
type: "dict"
115+
required: false
116+
schema:
117+
config:
118+
type: "dict"
119+
required: false
120+
args:
121+
nullable: true
122+
type: "string"
123+
required: false

benchcab/data/test/config-optional.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ fluxsite:
1111
storage:
1212
- scratch/$PROJECT
1313

14+
spatial:
15+
met_forcings:
16+
crujra_access: https://github.com/CABLE-LSM/cable_example.git
17+
payu:
18+
config:
19+
walltime: "1:00:00"
20+
args: -n 2
21+
1422
science_configurations:
1523
- cable:
1624
cable_user:

benchcab/environment_modules.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
try:
1515
from python import module
1616
except ImportError:
17-
print(
17+
get_logger().error(
1818
"Environment modules error: unable to import "
1919
"initialization script for python."
2020
)

0 commit comments

Comments
 (0)