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.