Detailed Usage#
WATTS consists of a set of Python classes that can manage simulation workflows for one or multiple codes. It provides the following capabilities:
An isolated execution environment when running a code;
The ability to use placeholder values in input files that are filled in programmatically;
Seamless unit conversions when working with multiple codes;
A managed database that simulation inputs and outputs are automatically saved to; and
Python classes that provide extra post-processing and analysis capabilities for each code.
Parameters#
The parameters that are used to “fill in” input files with placeholders are
managed by the Parameters
class. This class mostly behaves like
a Python dictionary but has a few extra capabilities. Setting parameters can be
done as follows:
params = watts.Parameters()
params['temperature'] = 550.0
params['option'] = True
params['values'] = [10.0, 20.0, 0.05]
Like a Python dictionary, key/value pairs can also be set when instantiating the object:
params = watts.Parameters(
temperature=550.0,
option=True,
values=[10.0, 20.0, 0.05]
)
Parameters can be saved to a pickle file:
params.save('parameters.pkl')
and later re-created using the from_pickle()
method:
loaded_params = watts.Parameters.from_pickle('parameters.pkl')
By themselves, Parameters
are not very useful, but when
combined with plugin classes, they become building blocks for sophisticated
workflows.
Units#
To handle codes that use different unit systems, WATTS relies on the
Quantity
class from astropy.units
to perform unit
conversion on parameters to ensure that the correct units are used for each
code. For instance, MOOSE-based codes use the SI units while OpenMC uses the CGS
units. With the built-in unit-conversion capability, a parameter needs only to
be set once in any unit system and WATTS can automatically convert it to the
correct unit for different codes. To use the unit-conversion capability,
parameters need to be set using the Quantity
class as
follows:
from astropy.units import Quantity
params['radius'] = Quantity(9.9, "mm")
params['inlet_temperature'] = Quantity(600, "Celsius")
params['c_p'] = Quantity(4.9184126, "BTU/(kg*K)")
with the format of Quantity(value, unit)
.
Plugins#
Using a particular code within WATTS requires a “plugin” that controls input file generation, execution, and post-processing. To see a full list of available plugins, refer to Available Plugins. Below, the general functionality of the plugin classes is discussed and applies to nearly all classes.
Execution#
Running a code via watts
is as simple as creating an instance of a plugin
class and then calling that instance as though it were a function. Here, we will
show an example using the PluginMCNP
class that demonstrates a
simulation using MCNP. Let’s say we have the following input file for MCNP that
we want to run:
Bare sphere of plutonium
1 1 0.04 -1 imp:n=1
2 0 1 imp:n=0
1 so 6.5
m1 94239.70c 0.04
kcode 10000 1.0 50 150
ksrc 0 0 0
If the filename of the input file is sphere_model
, we start by creating a
watts.PluginMCNP
object:
plugin_mcnp = watts.PluginMCNP("sphere_model")
Calling the plugin class then executes the code:
result = plugin_mcnp()
When you call a plugin, it will return an instance of a subclass of
Results
(see Results for further details).
Note that when a plugin is called, a temporary directory with all necessary
files is created and used while the underlying code is running. Once the call is
complete, the input and output files are moved to the database and the temporary directory is removed. To retain the
temporary directory for debugging purposes, the cleanup
argument can be
used:
result = plugin_mcnp(cleanup=False)
Templated Inputs#
For any code that use text-based input files, watts
relies on the Jinja templating engine for handling templated
variables and expressions. The templated input file looks like a normal input
file where some values have been replaced with variables, which are denoted
by {{
and }}
pairs and get replaced with actual values when the template
is rendered. For example, the example MCNP input file above could be templated
as follows:
Bare sphere of plutonium
1 1 0.04 -1 imp:n=1
2 0 1 imp:n=0
1 so {{ radius }}
m1 94239.70c 0.04
kcode 10000 1.0 50 150
ksrc 0 0 0
The input file now contains a placeholder, {{ radius }}
, that will be filled
in at the time the plugin is called. Before creating and calling our plugin, we
need to specify the parameter using the Parameters
class:
params = watts.Parameters()
params['radius'] = 6.0
As before, we create an instance of PluginMCNP
but instead of
calling it with no arguments, we pass it the Parameters
instance:
plugin_mcnp = watts.PluginMCNP("sphere_model")
result = plugin_mcnp(params)
While this example solely demonstrates a simple variable substitution, Jinja has sophisticated capabilities for using logical control structures, filters, calling Python methods, and extensible templates; for advanced usage, please read through the Jinja template designer documentation.
Specifying an Executable#
Each plugin has a default executable name for the underlying code. For example,
the PluginMCNP
class uses the executable mcnp6
by default.
You can explicitly specify the path to an executable at the time a plugin is
created:
mcnp = watts.PluginMCNP(template, executable='mcnp5')
The executable
argument can be given as an absolute path, in which case it
will be used as is. Alternatively, when the executable
argument is given as
a relative path, WATTS will look for an environment variable indicating the
directory where the executable can be found and prepend that to the executable
if it exists. For example, the PluginMCNP
class will look for a
MCNP_DIR
environment variable. If no environment variable is found,
the directory containing the executable must be present on your PATH
environment variable.
You can also view and change the executable using the
executable
attribute:
>>> plugin_mcnp.executable
PosixPath('mcnp6')
>>> plugin_mcnp.executable = 'mcnp5'
Specifying Input Files#
The above example with MCNP uses only a single input file. However, some codes
require more than one input file, some of which may be templated. When you’re
creating a plugin, the extra_inputs
argument allows you to specify a list of
files that will be copied to the isolated executable environment:
arc = watts.PluginPyARC('pyarc_template', extra_inputs=['lumped_test5.son'])
If you have extra input files that also contain template variables that need to
get rendered, use the extra_template_inputs
argument instead:
arc = watts.PluginPyARC('pyarc_template', extra_template_inputs=['extra_template'])
Configuring the Execution Command#
Each plugin has a predefined execute command that will be run when the plugin is
called. For example, for the PluginSerpent
class, the execute
command can be determined as:
>>> plugin_serpent = watts.PluginSerpent('input')
>>> plugin_serpent.execute_command
['sss2', 'serpent_input']
If you want to specify extra command-line arguments, this can be done with the
extra_args
argument, which accepts a list of strings, at the time you are
calling the plugin. If we wanted to run Serpent with 24 threads, for example,
this could be accomplished as:
result = plugin_serpent(params, extra_args=['-omp', '24'])
The extra_args
are added after the main execute command. If you need to
prepend arguments (most commonly to run a code using MPI), you can specify the
mpi_args
argument at the time you call the plugin:
result = plugin_serpent(params, mpi_args=['mpiexec', '-n', '8'])
Standard Output#
When you call a plugin, by default you will not see output from the code being
run under the hood. The output is redirected to a file which is available to you
afterward via the stdout
attribute. If you do want to see
output from the execution of a code as it’s running, you can use the
show_stdout
argument at the time you are creating the plugin:
plugin_sas = watts.PluginSAS('sas_input', show_stdout=True)
There’s also a show_stderr
argument that modifies behavior for anything
written to standard error.
Results#
When you call a Plugin()
instance, an instance of the
Results
class specific to the plugin will be returned that
contains information about the results. Every Results
object
contains a list of input and output files that were generated:
>>> results = plugin_openmc(params)
>>> results.inputs
[PosixPath('geometry.xml'),
PosixPath('settings.xml'),
PosicPath('materials.xmll')]
>>> results.outputs
[PosixPath('OpenMC_log.txt'),
PosixPath('statepoint.250.h5')]
Results
objects also contain a copy of the
Parameters
that were used at the time the plugin was called:
>>> results.parameters
{'radius': 10.0}
Each plugin actually returns a subclass of Results
that extends
the basic functionality by adding methods/attributes that incorporate
post-processing logic. For example, the ResultsOpenMC
class
provides a keff
attribute that provides the
k-effective value at the end of the simulation:
>>> results.keff
1.0026170700986219+/-0.003342785895893627
For MOOSE, the ResultsMOOSE
class provides a
csv_data
attribute that gathers the results from
every CSV files generated by MOOSE applications (such as SAM or BISON):
moose_result = moose_plugin(params)
for key in moose_result.csv_data:
print(key, moose_result.csv_data[key])
For PyARC, the ResultsPyARC
class
provides a results_data
attribute that gathers the
results stored in PyARC.user_object:
pyarc_result = pyarc_plugin(params)
for key in pyarc_result.results_data:
print(key, pyarc_result.results_data[key])
Database#
When you call a Plugin
instance, the Results
object and all accompanying files are automatically added to a database on disk
for later retrieval. Interacting with this database can be done either via the
Database
class or through the watts
command-line tool.
The Database class#
The Database
class provides a list-like object that contains all
previously generated Results
objects:
>>> db = watts.Database()
>>> db
[<ResultsOpenMC: 2022-01-01 12:05:02.130384>,
<ResultsOpenMC: 2022-01-01 12:11:38.037813>,
<ResultsMOOSE: 2022-01-02 08:45:12.846409>]
>>> db[1]
<ResultsOpenMC: 2022-01-01 12:11:38.037813>
By default, the database will be created in a user-specific data directory (on
Linux machines, this is normally within ~/.local/share
). However, the
location of the database can be specified:
db = watts.Database('/opt/watts_db/')
Creating a database this way doesn’t change the default path used when running
plugins. If you want to change the default database path used in plugins, the
set_default_path()
classmethod should be used:
>>> watts.Database.set_default_path('/opt/watts_db')
>>> db = watts.Database()
>>> db.path
PosixPath('/opt/watts_db')
To remove a result from the database, you can call the
remove()
method, passing a watts.Results
object:
>>> db = watts.Database()
>>> db
[<ResultsOpenMC: 2022-01-01 12:05:02.130384>,
<ResultsOpenMC: 2022-01-01 12:11:38.037813>,
<ResultsMOOSE: 2022-01-02 08:45:12.846409>]
>>> moose_result = db[-1]
>>> db.remove(moose_result)
>>> db
[<ResultsOpenMC: 2022-01-01 12:05:02.130384>,
<ResultsOpenMC: 2022-01-01 12:11:38.037813>]
Note that removing a database result will delete the data directory associated
with the result but will not affect the input files stored in their original
location on your system. To clear all results from the database, simply use the
clear()
method:
>>> db.clear()
>>> db
[]
As with the remove()
method, clearing the database will
delete all the corresponding results on disk, including copies of the input and
output files from the workflow stored in the data directory. Original input
files stored outside the database directory will be unaffected.
Directory names#
Within the database, each result is stored in a uniquely named directory. By
default, the directory name is generated using Python’s uuid
module.
However, you can manually specify the directory name when a plugin is executed
by passing the output_dir
argument:
>>> result = plugin(params, output_dir='iteration_5')
>>> result.base_path
PosixPath('/home/username/.local/share/watts/iteration_5')
Note that if you try to use the same output_dir
twice, an exception will be
raised.
Command-line Tool#
The watts
command-line tool provides an easy way to inspect results stored
in the database. This tool has three subcommands:
- results
Displays a list of all results in the database
- dir
Provides the directory for a specific result (referenced by index)
- stdout
Shows the standard output from a specific result (referenced by index)
- rm
Remove a specific result (referenced by index)
The results
subcommand will produce a table such as the following:
$ watts results
+-------+--------+--------+--------+----------------------------+
| Index | Job ID | Plugin | Name | Time |
+-------+--------+--------+--------+----------------------------+
| 0 | 0 | MCNP | | 2022-06-01 13:21:44.713942 |
| 1 | 1 | MCNP | | 2022-06-01 13:23:12.410774 |
| 2 | 2 | MCNP | r=2.0 | 2022-06-02 07:46:05.463723 |
| 3 | 2 | MCNP | r=4.0 | 2022-06-02 07:46:10.996932 |
| 4 | 2 | MCNP | r=6.0 | 2022-06-02 07:46:17.487411 |
| 5 | 2 | MCNP | r=8.0 | 2022-06-02 07:46:24.964455 |
| 6 | 2 | MCNP | r=10.0 | 2022-06-02 07:46:33.426781 |
+-------+--------+--------+--------+----------------------------+
For each result, you’re given an index (used in other subcommands), a job ID,
the plugin name, the name
that was used when calling the plugin, and a
timestamp for when the plugin was called. The job ID is the same for each plugin
execution from a single Python invocation. There are several optional flags that
can be used to narrow down the list of results. For example, to only display
results that have job ID 2:
$ watts results --job-id 2
+-------+--------+--------+--------+----------------------------+
| Index | Job ID | Plugin | Name | Time |
+-------+--------+--------+--------+----------------------------+
| 2 | 2 | MCNP | r=2.0 | 2022-06-02 07:46:05.463723 |
| 3 | 2 | MCNP | r=4.0 | 2022-06-02 07:46:10.996932 |
| 4 | 2 | MCNP | r=6.0 | 2022-06-02 07:46:17.487411 |
| 5 | 2 | MCNP | r=8.0 | 2022-06-02 07:46:24.964455 |
| 6 | 2 | MCNP | r=10.0 | 2022-06-02 07:46:33.426781 |
+-------+--------+--------+--------+----------------------------+
The index of a result can be used to get more information. For example, to determine the directory where input/output files are stored for the result with index 2, you can run:
$ watts dir 2
/home/username/.local/share/watts/3c5674ae37094d74af7a7fc5562555a3
Similarly, a result can be removed by referencing its index:
$ watts rm 5
As with the watts.Database.remove()
method, the watts rm
subcommand
will delete the data directory associated with the result.