"""Contains the executor class."""
import logging
import re
from copy import deepcopy
from pathlib import Path
from typing import Optional
from bout_runners.make.make import Make
from bout_runners.parameters.bout_paths import BoutPaths
from bout_runners.parameters.run_parameters import RunParameters
from bout_runners.submitter.abstract_cluster_submitter import AbstractClusterSubmitter
from bout_runners.submitter.abstract_submitter import AbstractSubmitter
from bout_runners.submitter.submitter_factory import get_submitter
[docs]class BoutRunExecutor:
r"""
Executes the command for submitting a bout run.
Attributes
----------
__bout_paths : BoutPaths
Getter variable for project_path
__make : Make
Object for making the project
__run_parameters : RunParameters
Object containing the run parameters
bout_paths : BoutPaths
Object containing the paths
exec_name : str
Name of the executable
restart_from : None or Path
Path to copy restart files from prior to the execution
run_parameters : RunParameters
Object containing the run parameters
submitter : AbstractSubmitter
Object containing the submitter
Methods
-------
get_execute_command()
Return the execute command string
execute()
Execute a BOUT++ run
Examples
--------
The easiest way to use the Executor is to run a script from the root directory of
the project (i.e. where the `Makefile` and `data` directory are normally
situated. The script can simply call
>>> BoutRunExecutor().execute()
and `Executor` takes care of the rest.
A more elaborate example where all the dependency objects are built manually:
Import the dependencies
>>> from pathlib import Path
>>> from bout_runners.executor.bout_paths import BoutPaths
>>> from bout_runners.submitter.local_submitter import LocalSubmitter
Create the `bout_paths` object
>>> project_path = Path().joinpath('path', 'to', 'project')
>>> bout_inp_src_dir = Path().joinpath('path', 'to', 'source', 'BOUT.inp')
>>> bout_inp_dst_dir = Path().joinpath('path', 'to', 'destination', 'BOUT.inp')
>>> bout_paths = BoutPaths(project_path=project_path,
... bout_inp_src_dir=bout_inp_src_dir,
... bout_inp_dst_dir=bout_inp_dst_dir)
Create the executor object
>>> run_parameters = RunParameters({'global': {'nout': 0}})
>>> executor = BoutRunExecutor(
... bout_paths=bout_paths,
... submitter=LocalSubmitter(bout_paths.project_path),
... run_parameters=run_parameters)
Execute the run
>>> executor.execute()
"""
def __init__(
self,
bout_paths: Optional[BoutPaths] = None,
submitter: Optional[AbstractSubmitter] = None,
run_parameters: Optional[RunParameters] = None,
restart_from: Optional[Path] = None,
) -> None:
"""
Set the input parameters.
Parameters
----------
bout_paths : BoutPaths or None
Object containing the paths
If None, default BoutPaths values will be used
submitter : AbstractSubmitter
Object containing the submitter
run_parameters : RunParameters or None
Object containing the run parameters
If None, default parameters will be used
restart_from : Path or None
The path to copy the restart files from
"""
# NOTE: We are not setting the default as a keyword argument
# as this would mess up the paths
# NOTE: We are deepcopying bout_paths as it may be altered by for
# example the self.restart_from setter
logging.info("Start: Making an BoutRunExecutor object")
self.__bout_paths = (
deepcopy(bout_paths) if bout_paths is not None else BoutPaths()
)
self.__run_parameters = (
run_parameters if run_parameters is not None else RunParameters()
)
self.__make = Make(self.__bout_paths.project_path)
self.submitter = submitter if submitter is not None else get_submitter()
if isinstance(self.submitter, AbstractClusterSubmitter):
self.submitter.store_dir = self.__bout_paths.bout_inp_dst_dir
self.__restart_from = None
self.restart_from = restart_from
logging.info("Done: Making an BoutRunExecutor object")
@property
def restart_from(self) -> Optional[Path]:
"""
Set the properties of self.restart from and update bout_inp_dst_dir.
The bout_inp_dst_dir is updated to reflect that this is a restart run.
The new bout_inp_dst_dir will be the same as
bout_run_setup.executor.restart_from with _restart_/d* appended
/d* will be the next digit based on the number of other restart directories
Notes
-----
This will not copy the restart files as the restart files may not be ready.
Copying of files can either be done manually using
bout_runner.utils.file_operations.copy_restart_files or automatically by
using BoutRunner.__inject_copy_restart_files_node which is called from
BoutRunner.__prepare_run
See Also
--------
bout_runners.runner.bout_runner.BoutRunner.__inject_copy_restart_files_node
Search for restart files, make a restart node where needed
"""
return self.__restart_from
@restart_from.setter
def restart_from(self, restart_from: Optional[Path]) -> None:
self.__restart_from = restart_from
if restart_from is not None:
logging.debug(
"Changing bout_paths.bout_inp_dst_dir as restart_from is not None"
)
restart_dir_parent = restart_from.parent
restart_dir_name = restart_from.name
restart_dirs = list(restart_dir_parent.glob(f"{restart_dir_name}*"))
restart_number = 0
restart_numbers = list()
pattern = r"_restart_(\d)+$"
for restart_dir in restart_dirs:
match = re.search(pattern, restart_dir.name)
if match is not None:
# NOTE: The zeroth group is the matching string
restart_numbers.append(int(match.group(1)))
if len(restart_numbers) != 0:
restart_numbers.sort()
restart_number = restart_numbers[-1] + 1
prev_inp_dst_dir = self.bout_paths.bout_inp_dst_dir
stripped_restart_dir_name = re.sub(pattern, "", restart_dir_name)
new_inp_dst_dir = restart_dir_parent.joinpath(
f"{stripped_restart_dir_name}_restart_{restart_number}"
)
self.bout_paths.bout_inp_dst_dir = new_inp_dst_dir
logging.debug(
"bout_run_setup.bout_paths.bout_inp_dst_dir set from %s to %s",
prev_inp_dst_dir,
new_inp_dst_dir,
)
@property
def bout_paths(self) -> BoutPaths:
"""
Get the properties of self.bout_paths.
Returns
-------
self.__bout_paths : BoutPaths
Object containing the paths
Notes
-----
The bout_paths is read only
"""
return self.__bout_paths
@property
def exec_name(self) -> str:
"""
Set the properties of self.bout_paths.
Returns
-------
self.__bout_paths : BoutPaths
Object containing the paths
Notes
-----
The exec_name is read only
"""
return self.__make.exec_name
@property
def run_parameters(self) -> RunParameters:
"""
Get the properties of self.run_parameters.
Returns
-------
self.__run_parameters : RunParameters
Object containing the run parameters
Notes
-----
The run_parameters is read only
"""
return self.__run_parameters
[docs] def get_execute_command(self) -> str:
"""
Return the execute command string.
Returns
-------
command : str
The terminal command for executing the run
"""
mpi_cmd = "mpirun -np"
# NOTE: No spaces if parameters are None
command = (
f"{mpi_cmd} "
f"{self.submitter.processor_split.number_of_processors} "
f"{self.__bout_paths.project_path.joinpath(self.exec_name)} "
f"-d {self.__bout_paths.bout_inp_dst_dir} "
f"{self.__run_parameters.run_parameters_str}"
)
return command
[docs] def execute(self, restart: bool = False) -> None:
"""
Execute a BOUT++ run.
Parameters
----------
restart : bool
If True the 'restart' will be appended to the command string
"""
# Make the project if not already made
self.__make.run_make()
# Submit the command
command = self.get_execute_command()
if restart:
command += " restart"
self.submitter.submit_command(command)