Skip to content

Commit

Permalink
Move plasmod solver to new interface (#1054)
Browse files Browse the repository at this point in the history
* Refactor test helper into new module

* Use ParameterFrame not Dict in Task constructor

* Start on Plasmod interface refactor - Setup

* Add plasmod Run task

The run_subprocess function has been moved to a separate utility.
This allows us to mock our own `run_subprocess` function in tests, and
not worry about if we want to change the implementation of how we run
our shell commands. E.g., if we wanted to start using os.system rather
than subprocess.Popen, for whatever reason.

* Refactor plasmod solver interface's Teardown

* Fix bug in run_subprocess + add test reminder

* Fix param mappings in refactored Plasmod interface

* Fix dataclass attribute/method ordering

Apparently dataclass attributes need to be declared first for the
corresponding kwarg to be added to the constructor.

Declare the methods after the attributes to fix this.

* Add missing Plasmod Teardown tests

* Remove extraneous dict init in plasmod.params

* Add refactored plasmod Solver class

* Add end-to-end test for plasmod solver

* Add 'cwol' to PlasmodOutput dataclass

* Add test for error on bad Plasmod result flag

* Add test for writing PlasmodInput params

* Convert PlasmodInput models to enums on init

* Create new PlasmodInputs on Solver.update_inputs

This allows for the post-init processing to do its magic.

* Fix typing in plasmod MODEL_MAP

* Replace old plasmod Solver with refactored

* Add warning if plasmod output param has no value

Adds back change made in
#1009
after refactor.

* Split up large files in plasmod.solver

* Allow using enum name to specify plasmod models

* Warn when using unrecognised inputs in plasmod

* Add get_profile back into plasmod.Solver

This was temporarily removed as part of a refactor.

* Add get_profiles method back into plasmod.Solver

This had been temporarily removed during a refactor.

* Add 'scalar_outputs' method to plasmod.Solver

This replaces the previous 'get_raw_variables' function. Using a
structure as an output means that users know exactly what attributes
are allowed. IDEs can then make suggestions about attributes, and also
spot typos that wouldn't be spotted using strings.

* Allow param edits between calls of plasmod solver

* Put modify_mappings back into plasmod solver

* Flesh out plasmod Task constructor docstrings

* Update plasmod example to use new solver interface

* Add CodesSolver base class

This is intended to replace the FileProgramInterface class, as part of
the refactor of the solvers interface.

* Move  plasmod.Solver functionality to base class

* Extract plasmod.Solver general setup to base class

* Re-add 'jiter' as plasmod output parameter

This had been lost in a refactor.

* Separate profiles and scalars in plasmod outputs

* Move plasmod exit code check to Teardown task

This makes more sense than throwing when reading the outputs.

* Undo change in plasmod example's transport model

This was an accidental change made during a refactor.

* Change to a better method in CodesTeardown

* Use plasmod.Profiles enums in Sovler.get_profile

This is in-line with the effort towards:
"All text based choices should be enumified"
#1044

* Remove resolved TODO

* Write tests to resolve TODO

* Resolve flake8 warnings

* Resolve ambiguity in plasmod solver Setup var name

This attempts to resolve the ambiguity around the 'input_file' variable
name.

* Update plasmod solver directory structure

This is more consistent with the structure in PROCESS's solver

* Fix plasmod example's get_profile usage

* Fix sphinx build

* Fix file path in flake8 ignores

I'd forgotten to update the file path after moving the file.

* Fix import in tests

* Simplify import in plasmod example

* Add missing type hint in CodesTask._run_subprocess

* Add Optional to typing

* Add better typing + docstrings to udpate_inputs
  • Loading branch information
hsaunders1904 committed Jun 15, 2022
1 parent dbf6aa7 commit 4fa1f41
Show file tree
Hide file tree
Showing 26 changed files with 2,142 additions and 1,067 deletions.
6 changes: 6 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ per-file-ignores =
bluemira/codes/__init__.py:
# FreeCAD message removal function above imports
E402,
bluemira/codes/plasmod/api/_inputs.py:
# We don't control the names of plasmod inputs
N815,
bluemira/codes/plasmod/api/_outputs.py:
# We don't control the names of plasmod outputs
N815,
bluemira/geometry/parameterisations.py:
# Lambdas are probably cleaner here
E731,
Expand Down
28 changes: 18 additions & 10 deletions bluemira/base/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@

import abc
import enum
from typing import Any, Callable, Dict, Optional, Type
from typing import Any, Callable, Optional, Type

from bluemira.base.parameter import ParameterFrame


class Task(abc.ABC):
Expand All @@ -58,11 +60,11 @@ class Task(abc.ABC):
"run modes" can also be defined.
"""

def __init__(self, params: Dict[str, Any]) -> None:
self._params = params
def __init__(self, params: ParameterFrame) -> None:
self.params = params

@abc.abstractmethod
def run(self, *args, **kwargs):
def run(self):
"""Run the task."""
pass

Expand All @@ -75,13 +77,18 @@ class NoOpTask(Task):
teardown stages.
"""

def run(*_, **__) -> None:
def run(self) -> None:
"""Do nothing."""
return


class RunMode(enum.Enum):
"""Base enum class for defining run modes within a solver."""
"""
Base enum class for defining run modes within a solver.
Note that no two enumeration's names should be case-insensitively
equal.
"""

def to_string(self) -> str:
"""
Expand All @@ -106,11 +113,12 @@ class SolverABC(abc.ABC):
arbitrary run modes.
"""

def __init__(self, params: Dict[str, Any]):
def __init__(self, params: ParameterFrame):
super().__init__()
self._setup = self.setup_cls(params)
self._run = self.run_cls(params)
self._teardown = self.teardown_cls(params)
self.params = params
self._setup = self.setup_cls(self.params)
self._run = self.run_cls(self.params)
self._teardown = self.teardown_cls(self.params)

@abc.abstractproperty
def setup_cls(self) -> Type[Task]:
Expand Down
254 changes: 254 additions & 0 deletions bluemira/codes/interface_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
# bluemira is an integrated inter-disciplinary design tool for future fusion
# reactors. It incorporates several modules, some of which rely on other
# codes, to carry out a range of typical conceptual fusion reactor design
# activities.
#
# Copyright (C) 2021 M. Coleman, J. Cook, F. Franza, I.A. Maione, S. McIntosh, J. Morris,
# D. Short
#
# bluemira is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# bluemira is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with bluemira; if not, see <https://www.gnu.org/licenses/>.
"""Base classes for solvers using external codes."""

import abc
from typing import Any, Callable, Dict, List, Optional, Union

from bluemira.base.constants import raw_uc
from bluemira.base.look_and_feel import bluemira_warn
from bluemira.base.parameter import ParameterFrame
from bluemira.base.solver import SolverABC, Task
from bluemira.codes.error import CodesError
from bluemira.codes.utilities import get_recv_mapping, get_send_mapping, run_subprocess


class CodesTask(Task):
"""
Base class for a task used by a solver for an external code.
"""

def __init__(self, params: ParameterFrame, codes_name: str) -> None:
super().__init__(params)
self._name = codes_name

def _run_subprocess(self, command: List[str], **kwargs):
"""
Run a subprocess command and raise a CodesError if it returns a
non-zero exit code.
"""
return_code = run_subprocess(command, **kwargs)
if return_code != 0:
raise CodesError(
f"'{self._name}' subprocess task exited with non-zero error code "
f"'{return_code}'."
)


class CodesSetup(CodesTask):
"""
Base class for setup tasks of a solver for an external code.
"""

def _get_new_inputs(
self, remapper: Optional[Union[Callable, Dict[str, str]]] = None
) -> Dict[str, float]:
"""
Retrieve inputs values to the external code from this tasks'
ParameterFrame.
Convert the inputs' units to those used by the external code.
Parameters
----------
remapper: Optional[Union[Callable, Dict[str, str]]]
A function or dictionary for remapping variable names.
Useful for renaming old variables
Returns
-------
_inputs: Dict[str, float]
Keys are external code parameter names, values are the input
values for those parameters.
"""
_inputs = {}

if not (callable(remapper) or isinstance(remapper, (type(None), Dict))):
raise TypeError("remapper is not callable or a dictionary")
if isinstance(remapper, Dict):
orig_remap = remapper.copy()

def remapper(x):
return orig_remap[x]

elif remapper is None:

def remapper(x):
return x

send_mappings = get_send_mapping(self.params, self._name)
for external_key, bm_key in send_mappings.items():
external_key = remapper(external_key)
if isinstance(external_key, list):
for key in external_key:
_inputs[key] = self._convert_units(self.params.get_param(bm_key))
continue

_inputs[external_key] = self._convert_units(self.params.get_param(bm_key))

return _inputs

def _convert_units(self, param):
code_unit = param.mapping[self._name].unit
if code_unit is not None:
return raw_uc(param.value, param.unit, code_unit)
else:
return param.value


class CodesTeardown(CodesTask):
"""
Base class for teardown tasks of a solver for an external code.
Parameters
----------
params: ParameterFrame
The parameters for this task.
codes_name: str
The name of the external code the task is associated with.
"""

def _update_params_with_outputs(self, outputs: Dict[str, Any]):
"""
Update this task's parameters with the external code's outputs.
This performs implicitly performs any unit conversions.
Raises
------
CodesError
If any outputs do not have a mapping to bluemira, or the
mapping points to a parameter name that does not exist in
this object's ParameterFrame.
"""
mapped_outputs = self._map_external_outputs_to_bluemira_params(outputs)
self.params.update_kw_parameters(mapped_outputs, source=self._name)

def _map_external_outputs_to_bluemira_params(
self, external_outputs: Dict[str, Any]
) -> Dict[str, Dict[str, Any]]:
"""
Loop through external outputs, find the corresponding bluemira
parameter name, and map it to the output's value and unit.
Parameters
----------
external_outputs: Dict[str, Any]
An output produced by an external code. The keys are the
outputs' names (not the bluemira version of the name), the
values are the output's value (in the external code's unit).
Returns
-------
mapped_outputs: Dict[str, Dict[str, Any]]
The keys are bluemira parameter names and the values are a
dict of form '{"value": Any, "unit": str}', where the value
is the external code's output value, and the unit is the
external code's unit.
"""
mapped_outputs = {}
recv_mappings = get_recv_mapping(self.params, self._name)
for external_key, bluemira_key in recv_mappings.items():
output_value = self._get_output_or_raise(external_outputs, external_key)
if output_value is None:
continue
param_mapping = self._get_parameter_mapping_or_raise(bluemira_key)
if param_mapping.unit is not None:
mapped_outputs[bluemira_key] = {
"value": output_value,
"unit": param_mapping.unit,
}
return mapped_outputs

def _get_output_or_raise(
self, external_outputs: Dict[str, Any], parameter_name: str
):
try:
output_value = external_outputs[parameter_name]
except KeyError as key_error:
raise CodesError(
f"No output value from code '{self._name}' found for parameter "
f"'{parameter_name}'."
) from key_error
if output_value is None:
bluemira_warn(
f"No value for output parameter '{parameter_name}' from code "
f"'{self._name}'."
)
return output_value

def _get_parameter_mapping_or_raise(self, bluemira_param_name: str):
try:
return self.params.get_param(bluemira_param_name).mapping[self._name]
except AttributeError as attr_error:
raise CodesError(
f"No mapping defined between parameter '{bluemira_param_name}' and "
f"code '{self._name}'."
) from attr_error


class CodesSolver(SolverABC):
"""
Base class for solvers running an external code.
"""

@abc.abstractproperty
def name(self):
"""
The name of the solver.
In the base class, this is used to find mappings and specialise
error messages for the concrete solver.
"""
pass

def modify_mappings(self, send_recv: Dict[str, Dict[str, bool]]):
"""
Modify the send/receive truth values of a parameter.
If a parameter's 'send' is set to False, its value will not be
passed to the external code (a default will be used). Likewise,
if a parameter's 'recv' is False, its value will not be updated
from the external code's outputs.
Parameters
----------
mappings: dict
A dictionary where keys are variables to change the mappings
of, and values specify 'send', and or, 'recv' booleans.
E.g.,
.. code-block:: python
{
"var1": {"send": False, "recv": True},
"var2": {"recv": False}
}
"""
for key, val in send_recv.items():
try:
p_map = getattr(self.params, key).mapping[self.name]
except (AttributeError, KeyError):
bluemira_warn(f"No mapping known for {key} in {self.name}")
else:
for sr_key, sr_val in val.items():
setattr(p_map, sr_key, sr_val)
Loading

0 comments on commit 4fa1f41

Please sign in to comment.