"""Contains the class dealing with the default parameters."""
import ast
import configparser
import logging
import re
from pathlib import Path
from typing import Dict, Optional, Union
from bout_runners.parameters.bout_paths import BoutPaths
from bout_runners.parameters.run_parameters import RunParameters
from bout_runners.runner.bout_run_executor import BoutRunExecutor
from bout_runners.submitter.local_submitter import LocalSubmitter
[docs]class DefaultParameters:
"""
Class which deals with the default parameters.
The default parameters are those set internally in BOUT++, or in the specified
BOUT.inp file
Attributes
----------
self.__bout_paths : BoutPaths
Object for the BOUT++ paths
self.__settings_path : None or Path
Path to the BOUT.settings file
Methods
-------
run_parameters_run()
Execute a run to obtain the default parameters.
get_default_parameters()
Return the default parameters from the settings file.
Examples
--------
The easiest way to use DefaultParameters 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
>>> DefaultParameters().get_default_parameters()
{'global': {'append': False, 'async_send': False, ...}}
A more elaborate example where all the dependency objects are
built manually:
Import dependencies
>>> from pathlib import Path
>>> from bout_runners.executor.bout_paths import BoutPaths
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)
Get the default parameters
>>> default_parameter = DefaultParameters(bout_paths=bout_paths)
>>> default_parameter.get_default_parameters()
{'global': {'append': False, 'async_send': False, ...}}
"""
def __init__(
self,
bout_paths: Optional[BoutPaths] = None,
settings_path: Optional[Path] = None,
) -> None:
"""
Set the member data.
If the settings_path is None, the constructor will call run_parameters_run to
create a settings_path
Warnings
--------
There can be a potential mismatch between a user provided `settings_path` and
the actual default values. This can occur if the user has updated `BOUT.inp`
without updating the `BOUT.settings` file.
It is therefore recommended to set `settings_path` to None unless the user is
sure the `BOUT.settings` file pointed to by `settings_path` is up to date
Parameters
----------
bout_paths : BoutPaths or None
Object containing the paths of the project
Will only be used in the `run_parameters_run` call if the `settings_path`
is not valid
settings_path : None or Path
Path to the up-to-date `settings_path`
Will invoke `run_parameters_run` if set to None
"""
logging.info("Start: Making a DefaultParameters object")
self.__bout_paths = bout_paths
self.__settings_path = Path() if settings_path is None else Path(settings_path)
if not self.__settings_path.is_file():
logging.info(
"Running parameter run as the parameters of the project are unknown"
)
self.run_parameters_run(self.__bout_paths)
logging.info("Done: Making a DefaultParameters object")
[docs] def run_parameters_run(self, bout_paths: Optional[BoutPaths]) -> None:
"""
Execute a run to obtain the default parameters.
A settings run executes the executable of the project with nout = 0 in order
to capture all parameters used in the project
Parameters
----------
bout_paths : BoutPaths
Object containing the paths of the project
"""
if bout_paths is None:
bout_paths = BoutPaths(bout_inp_dst_dir="settings_run")
else:
# NOTE: We are creating a new BoutPaths object instead of using the
# property setter as the object is mutable
project_path = bout_paths.bout_inp_src_dir.parent
bout_paths = BoutPaths(
project_path=project_path,
bout_inp_src_dir=bout_paths.bout_inp_src_dir,
bout_inp_dst_dir=project_path.joinpath("settings_run"),
)
executor = self.get_test_executor(bout_paths)
executor.execute()
executor.submitter.wait_until_completed()
self.__settings_path = bout_paths.bout_inp_dst_dir.joinpath("BOUT.settings")
[docs] @staticmethod
def get_test_executor(bout_paths: BoutPaths) -> BoutRunExecutor:
"""
Return the executor used for test (i.e. where nout=0).
Parameters
----------
bout_paths : BoutPaths
Object containing the BOUT++ paths
Returns
-------
executor : BoutRunExecutor
Executor instantiated with the test set up
"""
run_parameters = RunParameters({"global": {"nout": 0}})
executor = BoutRunExecutor(
bout_paths=bout_paths,
submitter=LocalSubmitter(bout_paths.project_path),
run_parameters=run_parameters,
)
return executor
[docs] def get_default_parameters(
self,
) -> Dict[str, Dict[str, Union[str, int, float, bool]]]:
"""
Return the default parameters from the settings file.
Returns
-------
default_parameters_dict : dict
Dictionary containing the parameters given in BOUT.settings
On the form
>>> {'section': {'parameter': 'value'}}
Notes
-----
1. The section-less part of BOUT.settings will be renamed `global`
2. In the `global` section, the keys `d` and the directory to the BOUT.inp
file will be removed
3. If the section `all` is present in BOUT.settings, the section will be
renamed `all_boundaries` as `all` is a protected SQL keyword
4. The section `run` will be dropped due to bout_runners own `run` table
5. The string values will be stored using lowercase
"""
# The settings file lacks a header for the global parameter
# Therefore, we add add the header [global]
with self.__settings_path.open("r") as settings_file:
settings_memory = f"[global]\n{settings_file.read()}"
config = configparser.ConfigParser()
config.read_string(settings_memory)
default_parameters_dict: Dict[str, Dict[str, Union[str, int, float]]] = dict()
for section in config.sections():
default_parameters_dict[section] = dict()
for key, val in config[section].items():
# Strip comments
capture_all_but_comment = "^([^#]*)"
matches = re.findall(capture_all_but_comment, val, re.M)
# Exclude comment line
if len(matches) == 0:
continue
# Capitalize in case of boolean
stripped_val = matches[0].capitalize()
# If type is not found, type is str
try:
val = ast.literal_eval(stripped_val)
except (SyntaxError, ValueError):
# Strip the whitespaces and cast to lowercase
val = stripped_val.strip().lower()
default_parameters_dict[section][key] = val
# NOTE: Bug in .settings: -d path is captured with # not in use
bout_inp_dir = self.__settings_path.parent
default_parameters_dict["global"].pop("d", None)
default_parameters_dict["global"].pop(str(bout_inp_dir).lower(), None)
if "all" in default_parameters_dict.keys():
default_parameters_dict["all_boundaries"] = default_parameters_dict.pop(
"all"
)
# Drop run as bout_runners will make its own table with that
# name
default_parameters_dict.pop("run", None)
return default_parameters_dict