Skip to content

Commit effd703

Browse files
committed
spatial wip
1 parent 77e6d8f commit effd703

11 files changed

Lines changed: 294 additions & 107 deletions

File tree

.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: 74 additions & 27 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,15 @@ 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, science_configurations=self.science_configurations
58+
)
5259
self.benchcab_exe_path = benchcab_exe_path
5360

5461
if validate_env:
@@ -103,18 +110,16 @@ def _validate_environment(self, project: str, modules: list):
103110
)
104111
sys.exit(1)
105112

106-
def _initialise_tasks(self) -> list[Task]:
113+
def _initialise_tasks(self) -> list[fluxsite.FluxsiteTask]:
107114
"""A helper method that initialises and returns the `tasks` attribute."""
108-
self.tasks = get_fluxsite_tasks(
115+
self.fluxsite_tasks = fluxsite.get_fluxsite_tasks(
109116
repos=self.repos,
110-
science_configurations=self.config.get(
111-
"science_configurations", internal.DEFAULT_SCIENCE_CONFIGURATIONS
112-
),
117+
science_configurations=self.science_configurations,
113118
fluxsite_forcing_file_names=get_met_forcing_file_names(
114119
self.config["experiment"]
115120
),
116121
)
117-
return self.tasks
122+
return self.fluxsite_tasks
118123

119124
def fluxsite_submit_job(self) -> None:
120125
"""Submits the PBS job script step in the fluxsite test workflow."""
@@ -189,27 +194,40 @@ def checkout(self):
189194

190195
print("")
191196

192-
def build(self):
197+
def build(self, mpi=None, clean=False):
193198
"""Endpoint for `benchcab build`."""
194199
for repo in self.repos:
195-
repo.build(modules=self.config["modules"], verbose=self.args.verbose)
200+
repo.build(
201+
modules=self.config["modules"],
202+
verbose=self.args.verbose,
203+
mpi=mpi if mpi else self.args.mpi,
204+
clean=clean,
205+
)
196206
print(f"Successfully compiled CABLE for realisation {repo.name}")
197207
print("")
198208

199209
def fluxsite_setup_work_directory(self):
200210
"""Endpoint for `benchcab fluxsite-setup-work-dir`."""
201-
tasks = self.tasks if self.tasks else self._initialise_tasks()
211+
212+
if not self.fluxsite_tasks:
213+
self._initialise_tasks()
214+
202215
print("Setting up run directory tree for fluxsite tests...")
203-
setup_fluxsite_directory_tree(fluxsite_tasks=tasks, verbose=self.args.verbose)
216+
setup_fluxsite_directory_tree(
217+
fluxsite_tasks=self.fluxsite_tasks, verbose=self.args.verbose
218+
)
204219
print("Setting up tasks...")
205-
for task in tasks:
220+
for task in self.fluxsite_tasks:
206221
task.setup_task(verbose=self.args.verbose)
207222
print("Successfully setup fluxsite tasks")
208223
print("")
209224

210225
def fluxsite_run_tasks(self):
211226
"""Endpoint for `benchcab fluxsite-run-tasks`."""
212-
tasks = self.tasks if self.tasks else self._initialise_tasks()
227+
228+
if not self.fluxsite_tasks:
229+
self._initialise_tasks()
230+
213231
print("Running fluxsite tasks...")
214232
try:
215233
multiprocess = self.config["fluxsite"]["multiprocess"]
@@ -219,9 +237,11 @@ def fluxsite_run_tasks(self):
219237
ncpus = self.config.get("pbs", {}).get(
220238
"ncpus", internal.FLUXSITE_DEFAULT_PBS["ncpus"]
221239
)
222-
run_tasks_in_parallel(tasks, n_processes=ncpus, verbose=self.args.verbose)
240+
fluxsite.run_tasks_in_parallel(
241+
self.fluxsite_tasks, n_processes=ncpus, verbose=self.args.verbose
242+
)
223243
else:
224-
run_tasks(tasks, verbose=self.args.verbose)
244+
fluxsite.run_tasks(self.fluxsite_tasks, verbose=self.args.verbose)
225245
print("Successfully ran fluxsite tasks")
226246
print("")
227247

@@ -233,8 +253,10 @@ def fluxsite_bitwise_cmp(self):
233253
"nccmp/1.8.5.0"
234254
) # use `nccmp -df` for bitwise comparisons
235255

236-
tasks = self.tasks if self.tasks else self._initialise_tasks()
237-
comparisons = get_fluxsite_comparisons(tasks)
256+
if not self.fluxsite_tasks:
257+
self._initialise_tasks()
258+
259+
comparisons = fluxsite.get_fluxsite_comparisons(self.fluxsite_tasks)
238260

239261
print("Running comparison tasks...")
240262
try:
@@ -265,13 +287,38 @@ def fluxsite(self):
265287
else:
266288
self.fluxsite_submit_job()
267289

290+
def spatial_setup_work_directory(self):
291+
"""Endpoint for `benchcab spatial-setup-work-dir`."""
292+
print("Setting up run directory tree for spatial tests...")
293+
setup_spatial_directory_tree()
294+
print("Setting up tasks...")
295+
for task in self.spatial_tasks:
296+
task.setup_task(verbose=self.args.verbose)
297+
print("Successfully setup spatial tasks")
298+
print("")
299+
300+
def spatial_run_tasks(self):
301+
"""Endpoint for `benchcab spatial-run-tasks`."""
302+
print("Running spatial tasks...")
303+
spatial.run_tasks(tasks=self.spatial_tasks, verbose=self.args.verbose)
304+
print("")
305+
268306
def spatial(self):
269307
"""Endpoint for `benchcab spatial`."""
308+
self.checkout()
309+
self.build(mpi=True)
310+
self.spatial_setup_work_directory()
311+
self.spatial_run_tasks()
270312

271313
def run(self):
272314
"""Endpoint for `benchcab run`."""
273-
self.fluxsite()
274-
self.spatial()
315+
self.checkout()
316+
self.build()
317+
self.build(mpi=True, clean=True)
318+
self.fluxsite_setup_work_directory()
319+
self.spatial_setup_work_directory()
320+
self.fluxsite_submit_job()
321+
self.spatial_run_tasks()
275322

276323
def main(self):
277324
"""Main function for `benchcab`."""

benchcab/cli.py

Lines changed: 10 additions & 6 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(

benchcab/fluxsite.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class CableError(Exception):
6666
"""Custom exception class for CABLE errors."""
6767

6868

69-
class Task:
69+
class FluxsiteTask:
7070
"""A class used to represent a single fluxsite task."""
7171

7272
root_dir: Path = internal.CWD
@@ -312,10 +312,10 @@ def get_fluxsite_tasks(
312312
repos: list[CableRepository],
313313
science_configurations: list[dict],
314314
fluxsite_forcing_file_names: list[str],
315-
) -> list[Task]:
315+
) -> list[FluxsiteTask]:
316316
"""Returns a list of fluxsite tasks to run."""
317317
tasks = [
318-
Task(
318+
FluxsiteTask(
319319
repo=repo,
320320
met_forcing_file=file_name,
321321
sci_conf_id=sci_conf_id,
@@ -328,14 +328,16 @@ def get_fluxsite_tasks(
328328
return tasks
329329

330330

331-
def run_tasks(tasks: list[Task], verbose=False):
331+
def run_tasks(tasks: list[FluxsiteTask], verbose=False):
332332
"""Runs tasks in `tasks` serially."""
333333
for task in tasks:
334334
task.run(verbose=verbose)
335335

336336

337337
def run_tasks_in_parallel(
338-
tasks: list[Task], n_processes=internal.FLUXSITE_DEFAULT_PBS["ncpus"], verbose=False
338+
tasks: list[FluxsiteTask],
339+
n_processes=internal.FLUXSITE_DEFAULT_PBS["ncpus"],
340+
verbose=False,
339341
):
340342
"""Runs tasks in `tasks` in parallel across multiple processes."""
341343

@@ -364,7 +366,7 @@ def worker_run(task_queue: multiprocessing.Queue, verbose=False):
364366

365367

366368
def get_fluxsite_comparisons(
367-
tasks: list[Task], root_dir=internal.CWD
369+
tasks: list[FluxsiteTask], root_dir=internal.CWD
368370
) -> list[ComparisonTask]:
369371
"""Returns a list of `ComparisonTask` objects to run comparisons with.
370372

benchcab/internal.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
"walltime": "6:00:00",
1818
"storage": [],
1919
}
20-
MPI = False
2120
FLUXSITE_DEFAULT_MULTIPROCESS = True
2221

2322
# DIRECTORY PATHS/STRUCTURE:
@@ -76,14 +75,38 @@
7675
# Relative path to directory that stores bitwise comparison results
7776
FLUXSITE_BITWISE_CMP_DIR = FLUXSITE_ANALYSIS_DIR / "bitwise-comparisons"
7877

78+
# Relative path to root directory for CABLE spatial runs
79+
SPATIAL_RUN_DIR = RUN_DIR / "spatial"
80+
81+
# Relative path to tasks directory (contains payu control directories configured
82+
# for each spatial task)
83+
SPATIAL_TASKS_DIR = SPATIAL_RUN_DIR / "tasks"
84+
85+
# A custom payu laboratory directory for payu runs
86+
PAYU_LABORATORY_DIR = RUN_DIR / "payu-laboratory"
87+
88+
# URL to a payu experiment template for offline spatial runs:
89+
EXPERIMENT_TEMPLATE_SPATIAL = "https://github.com/CABLE-LSM/cable_example.git"
90+
91+
# TODO(Sean) add more info here e.g. site forcing data, what is PLUMBER2?
92+
# Observations from where? What has been done to the data? Is it available?:
7993
# Path to met files:
8094
MET_DIR = Path("/g/data/ks32/CLEX_Data/PLUMBER2/v1-0/Met/")
8195

96+
# TODO(Sean) add more info here
97+
SPATIAL_MET_FORCING_CRUJRA = {
98+
"name": "CRUJRA-ACCESS",
99+
"path": "/g/data/tm70/ccc561/CABLE/CABLE-as-ACCESS_Yingping/cruncep10",
100+
}
101+
82102
# CABLE SVN root url:
83103
CABLE_SVN_ROOT = "https://trac.nci.org.au/svn/cable"
84104

85105
# CABLE executable file name:
86-
CABLE_EXE = "cable-mpi" if MPI else "cable"
106+
CABLE_EXE = "cable"
107+
108+
# CABLE MPI executable file name:
109+
CABLE_MPI_EXE = "cable-mpi"
87110

88111
# CABLE namelist file name:
89112
CABLE_NML = "cable.nml"

benchcab/repository.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def svn_info_show_item(self, item: str) -> str:
7272
)
7373
return proc.stdout.strip()
7474

75-
def build(self, modules: list[str], verbose=False) -> None:
75+
def build(self, modules: list[str], mpi=False, clean=False, verbose=False) -> None:
7676
"""Build CABLE using the default build script or a custom build script."""
7777

7878
if self.build_script:
@@ -82,7 +82,7 @@ def build(self, modules: list[str], verbose=False) -> None:
8282
)
8383
else:
8484
print(
85-
f"Compiling CABLE {'with MPI' if internal.MPI else 'serially'} for "
85+
f"Compiling CABLE {'with MPI' if mpi else 'serially'} for "
8686
f"realisation {self.name}..."
8787
)
8888

@@ -100,6 +100,16 @@ def build(self, modules: list[str], verbose=False) -> None:
100100
"'build_script' option in config.yaml?",
101101
)
102102

103+
if clean:
104+
# TODO(Sean) ideally we should not have to do this. We do this
105+
# because the build script is designed to build the serial and MPI
106+
# executable using the same .tmp directory. This forces us to delete
107+
# the .tmp directory everytime we switch from building serial to
108+
# building with MPI and visa versa. A simple solution to this would
109+
# be to have a separate temporary directory for serial and MPI
110+
# builds.
111+
shutil.rmtree(build_script_path.parent / ".tmp")
112+
103113
tmp_script_path = build_script_path.parent / "tmp-build.sh"
104114

105115
if verbose:
@@ -118,7 +128,7 @@ def build(self, modules: list[str], verbose=False) -> None:
118128
remove_module_lines(tmp_script_path)
119129

120130
args: list[str] = []
121-
if internal.MPI and self.build_script is None:
131+
if mpi and self.build_script is None:
122132
args.append("mpi")
123133

124134
with chdir(build_script_path.parent), self.modules_handler.load(

0 commit comments

Comments
 (0)