Skip to content

Commit

Permalink
Merge pull request #71 from paulromano/generic-plugin
Browse files Browse the repository at this point in the history
Generalize the template-based plugin to be used with arbitrary executables
  • Loading branch information
paulromano authored Nov 4, 2022
2 parents 8dbef25 + 14f0424 commit bdea2b3
Show file tree
Hide file tree
Showing 27 changed files with 394 additions and 165 deletions.
1 change: 1 addition & 0 deletions doc/source/dev/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ Developer's Guide
:maxdepth: 2

contributing
plugins
styleguide
100 changes: 100 additions & 0 deletions doc/source/dev/plugins.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
.. devguide_plugins:
Developing Plugins
------------------

As discussed in the user's guide, WATTS has a variety of plugins for different
codes (primarily focused on application codes in nuclear science and
engineering) that are already available. If you wish to use WATTS with a code
that does not already have a plugin available, extending WATTS to handle another
code can be done quite easily.

There are two ways to incorporate new codes into WATTS. First, if the code uses
a text-based input, the :class:`~watts.PluginGeneric` class can be used to
specify the executable, how it should be invoked from a command-line, and a
template for the input file. For example, if you have a code called ``goblin``
that should be executed from a command line as:

.. code-block:: sh
goblin input=filein.gbl
a plugin can be set up as::

goblin = watts.PluginGeneric(
executable='goblin',
execute_command=['{self.executable}', 'input={self.input_name}'],
template_file='filein.gbl'
)

where ``filein.gbl`` is a template input for the ``goblin`` code. Alternatively,
the command-line arguments can be specified as a single string::

goblin = watts.PluginGeneric(
executable='goblin',
execute_command='{self.executable} input={self.input_name}',
template_file='filein.gbl'
)

Executing the code is done by passing the plugin instance a set of parameters::

params = watts.Parameters()
...

result = goblin(params)

The ``result`` object will be a :class:`~watts.Results` instance, which holds
lists of input/output files, the parameters used to generate the results, and a
timestamp.

Plugin Customization
++++++++++++++++++++

If the :class:`~watts.PluginGeneric` class doesn't quite meet your needs, you
will need to write your own plugin class that is a subclass of either
:class:`~watts.Plugin` or :class:`~watts.PluginGeneric`. Subclassing
:class:`~watts.PluginGeneric` is appropriate if the code you're working with
uses text-based input files. If text-based input files are not used, you will
need to subclass the more general :class:`~watts.Plugin` class.

In either case, your plugin class will need to provide one or more of the
following:

- A ``prerun(params)`` method that is responsible for rendering inputs based on
the parameters that were passed and any other tasks that need to be taken care
of prior to execution.
- A ``run(**kwargs)`` method that is responsible for executing the code using
the rendered inputs. Keyword arguments are passed through when calling the
plugin.
- A ``postrun(params, name)`` method that is responsible for collecting a list
of input and output files and returning a :class:`~watts.Results` object.

If you are subclassing :class:`watts.PluginGeneric`, note that default
implementations of ``prerun``, ``run``, and ``postrun`` are already provided.
The executable and command-line arguments can also be customized through the
``executable`` and ``execute_command`` properties. Finally, the unit system to
be used for performing unit conversions is specified as an argument that is
passed through the ``__init__`` methods.


Results Customization
+++++++++++++++++++++

The basic :class:`~watts.Results` class stores a list of input and output files
associated with the execution of a plugin, the :class:`~watts.Parameters` that
were used to generate input files, and a timestamp. When writing your own
plugin, if you don't need to provide further customization of the results, you
should simply subclass :class:`~watts.Results` with a name matching the plugin.
For example, if you have::

class PluginGoblin(PluginGeneric):
...

A minimal corresponding results class would be::

class ResultsGoblin(Results):
"""Results from a Goblin simulation"""

However, you may wish to provide extra methods and properties that allow users
of your plugin to easily interrogate results (for example, pulling out key
numerical results from output files).
1 change: 1 addition & 0 deletions doc/source/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ API Reference
watts.Database
watts.Parameters
watts.Plugin
watts.PluginGeneric
watts.PluginABCE
watts.PluginMCNP
watts.PluginMOOSE
Expand Down
51 changes: 51 additions & 0 deletions doc/source/user/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,54 @@ five MCNP simulations:
0.97663+/-0.00063,
1.24086+/-0.00075,
1.47152+/-0.00081]
Your First ``watts`` Program
++++++++++++++++++++++++++++

To show how the various classes fit together, the example below creates a plugin
for a "code" (in this case, just the Linux ``cat`` command) and executes the
code on a templated input file that is rendered using parameters that are
defined in a :class:`~watts.Parameters` instance. This example assumes we have a
file called triangle.txt containing the following text:

.. code-block:: jinja
width={{ width }}
height={{ height }}
area={{ 0.5 * height * width }}
The ``watts`` script is as follows::

import watts

# Create a plugin for the 'cat' code
plugin = watts.PluginGeneric(
executable='cat',
execute_command=['{self.executable}', '{self.input_name}'],
template_file='triangle.txt',
unit_system='cgs'
)

# Define some parameters that will be used to render the input file
params = watts.Parameters()
params['width'] = watts.Quantity(1.0, 'm')
params['height'] = watts.Quantity(1.0, 'inch')

# Execute the plugin
result = plugin(params)

# Show the resulting input file
print(result.stdout)

Running this example will produce the following output:

.. code-block:: text
width=100.0
height=2.54
area=127.0
When the plugin is executed, the ``cat`` command is called on the rendered input
file, which is just the triangle.txt file where the parameters have been filled
in. Note that the physical quantities were :ref:`converted <units>` to
centimeters since we indicated that this plugin uses CGS units.
2 changes: 1 addition & 1 deletion doc/source/user/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ Specifying an Executable
Each plugin has a default executable name for the underlying code. For example,
the :class:`~watts.PluginMCNP` class uses the executable ``mcnp6`` by default.
You can both view and/or change the executable using the
:class:`~watts.TemplatePlugin.executable` attribute:
:class:`~watts.PluginGeneric.executable` attribute:

.. code-block:: pycon
Expand Down
17 changes: 10 additions & 7 deletions examples/1App_BISON_MetalFuel/watts_exec.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# SPDX-FileCopyrightText: 2022 UChicago Argonne, LLC
# SPDX-License-Identifier: MIT

from math import cos, pi
from pathlib import Path
import os

import watts

params = watts.Parameters()
Expand Down Expand Up @@ -38,12 +39,14 @@
params.show_summary(show_metadata=False, sort_by='key')

# MOOSE Workflow
# set your BISON directorate as BISON_DIR

moose_app_type = "bison"
app_dir = os.environ[moose_app_type.upper() + "_DIR"]
moose_plugin = watts.PluginMOOSE('bison_template', show_stdout=True) # show all the output
moose_plugin.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
# set your BISON directory as BISON_DIR

app_dir = Path(os.environ["BISON_DIR"])
moose_plugin = watts.PluginMOOSE(
'bison_template',
executable=app_dir / 'bison-opt',
show_stdout=True
)
moose_result = moose_plugin(params, mpi_args=['mpiexec', '-n', '2'])
for key in moose_result.csv_data:
print(key, moose_result.csv_data[key])
Expand Down
18 changes: 11 additions & 7 deletions examples/1App_MOOSE-MultiApp_Simple/watts_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
# SPDX-License-Identifier: MIT

"""
This example provides a demonstration on how to use WATTS to perform a simple simulation leveraging MOOSE's MultiApps system.
This example provides a demonstration on how to use WATTS to perform a simple
simulation leveraging MOOSE's MultiApps system.
"""

from math import cos, pi
from pathlib import Path
import os

import watts

params = watts.Parameters()
Expand All @@ -18,12 +20,14 @@
params.show_summary(show_metadata=False, sort_by='key')

# MOOSE Workflow
# set your BISON directorate as BISON_DIR
# set your BISON directory as BISON_DIR

moose_app_type = "bison"
app_dir = os.environ[moose_app_type.upper() + "_DIR"]
moose_plugin = watts.PluginMOOSE('main.tmpl', extra_inputs=['main_in.e', 'sub.i'])
moose_plugin.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
app_dir = Path(os.environ["BISON_DIR"])
moose_plugin = watts.PluginMOOSE(
'main.tmpl',
executable=app_dir / 'bison-opt',
extra_inputs=['main_in.e', 'sub.i']
)
moose_result = moose_plugin(params)
for key in moose_result.csv_data:
print(key, moose_result.csv_data[key])
Expand Down
4 changes: 2 additions & 2 deletions examples/1App_PyARC_UnitCell/pyarc_template
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ geometry{
}
}
calculations{

lumped_element_text_file( lu35 ) = "lumped.son"
mcc3{
xslib = "endf7.0"
egroupname = ANL33
scattering_order = 1
lumped_element_text_file( lu35 ) = "lumped.son"

cell( a ){
associated_sub_assembly = sub_assembly_name
}
Expand Down
10 changes: 5 additions & 5 deletions examples/1App_SAM_VHTR/watts_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

from math import cos, pi
import os
from pathlib import Path

import watts
from astropy.units import Quantity

Expand Down Expand Up @@ -52,12 +54,10 @@
params.show_summary(show_metadata=False, sort_by='key')

# MOOSE Workflow
# set your SAM directorate as SAM_DIR
# set your SAM directory as SAM_DIR

moose_app_type = "SAM"
app_dir = os.environ[moose_app_type.upper() + "_DIR"]
moose_plugin = watts.PluginMOOSE(moose_app_type.lower() + '_template') # show all the output
moose_plugin.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
app_dir = Path(os.environ["SAM_DIR"])
moose_plugin = watts.PluginMOOSE('sam_template', executable=app_dir / 'sam-opt')
moose_result = moose_plugin(params)
for key in moose_result.csv_data:
print(key, moose_result.csv_data[key])
Expand Down
15 changes: 9 additions & 6 deletions examples/Main_SAM-OpenMC_VHTR/watts_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
sensitivity analyses.
"""

from pathlib import Path
from math import cos, pi
import os
import watts
from statistics import mean, stdev
from statistics import mean
from openmc_template import build_openmc_model

params = watts.Parameters()
Expand Down Expand Up @@ -64,11 +65,13 @@ def calc_workflow(X):
print("FuelPin_rad / cool_hole_rad", X[0], X[1])

# MOOSE Workflow
# set your SAM directorate as SAM_DIR
moose_app_type = "SAM"
app_dir = os.environ[moose_app_type.upper() + "_DIR"]
sam_plugin = watts.PluginMOOSE('../1App_SAM_VHTR/sam_template', show_stderr=False) # does not show anything
sam_plugin.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
# set your SAM directory as SAM_DIR
app_dir = Path(os.environ["SAM_DIR"])
sam_plugin = watts.PluginMOOSE(
'../1App_SAM_VHTR/sam_template',
executable=app_dir / 'sam-opt',
show_stderr=False
)
sam_result = sam_plugin(params)
max_Tf = max(sam_result.csv_data[f'max_Tf_{i}'][-1] for i in range(1, 6))
avg_Tf = mean(sam_result.csv_data[f'avg_Tf_{i}'][-1] for i in range(1, 6))
Expand Down
15 changes: 9 additions & 6 deletions examples/MultiApp_SAM-OpenMC_VHTR/watts_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
in the params database.
"""

from pathlib import Path
from math import cos, pi
import os
import watts
Expand Down Expand Up @@ -70,12 +71,14 @@


# MOOSE Workflow
# set your SAM directorate as SAM_DIR

moose_app_type = "SAM"
app_dir = os.environ[moose_app_type.upper() + "_DIR"]
moose_plugin = watts.PluginMOOSE('../1App_SAM_VHTR/sam_template', show_stderr=True) # show only error
moose_plugin.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
# set your SAM directory as SAM_DIR

app_dir = Path(os.environ["SAM_DIR"])
moose_plugin = watts.PluginMOOSE(
'../1App_SAM_VHTR/sam_template',
executable=app_dir / 'sam-opt',
show_stderr=True
)
moose_result = moose_plugin(params)
for key in moose_result.csv_data:
print(key, moose_result.csv_data[key])
Expand Down
Loading

0 comments on commit bdea2b3

Please sign in to comment.