Executor
The Executor controls the "execution" behavior of the simulation. They direct the flow of solves, stepping, output recording, etc. Executors are similar to the original Executioner system in that they have one primary virtual function that each executor implements that defines its behavior. However, executors differ in that they are designed to be composed into arbitrary tree structures both in-code and by users via input files - similar to how the MeshGenerator system works. The Executor system is currently highly experimental and has not yet stabilized and been fully implemented. For this reason, MOOSE will only run in this mode if given the --executor
flag on the command line. The system name may change, input syntax may change, etc. YOU WERE WARNED!
Using Executors from input files
When using executors, you must remember to pass in the --executor
flag on the cli when you run your application binary. This allows the Executioner block to be omitted and causes MOOSE to ignore it if present. Support for a new [Executor]
block has been added to input files. Users will populate this block with equivalent content that normally was present in the [Executioner]
block. A hypothetical example is this:
[Executor]
[solve]
type = FooSolver
max_its = 42
...
[]
[refine]
type = Refine
inner = solve
[]
[init]
type = Init
inner = refine
petsc_options = ...
[]
[]
Which might perform the equivalent of something like the current Steady
executioner. In this example, "init" becomes the primary executor which does some things to set up and tear down the simulation. Inside its setup and tear-down, it executes an inner executor that has been set to "refine". The "refine" executor does some mesh refinement things and then executes an inner executor - which here has been set to "solve". This allows refinement to be wrapped/inserted into arbitrary areas of the simulation execution process.
The new system, is also capable of providing alias-like executors that generate equivalent executor trees programmatically in order to replicate the current Steady
or Transient
behavior like users currently expect now:
[Executor]
[steady]
type = FauxSteady
solve_type = 'PJFNK'
petsc_options_iname = '-pc_type -pc_hypre_type'
petsc_options_value = 'hypre boomeramg'
...
[]
[]
This executor would simply generate the init+solve+refine trio of executors programmatically - hiding the executor structure from the user. By default the last executor listed in the Executor
block becomes the master/primary executor. MOOSE only directly executes this executor; all other executors are executed if/when execution reaches them within the executor tree starting from the master executor.
By default, an executor has automatically generated execute-on flags created for it. These flags are executed right before and right after the executor executes and are named exec_[obj-name]_begin
and exec_[obj-name]_end
respectively where [obj-name]
is the name given to an object by its block header in an input file - e.g. [foo] type = FooExecutor []
has an object name of foo
. Other objects (e.g. user objects, materials, etc.) can be assigned to execute at these execute-on flags/times within the input file. This behavior is NOT fully implemented and will almost certainly not work right - so you should definitely not try to use it (yet). The names of these flags can also be modified from within the input file via an executor's begin_exec_flag
and end_exec_flag
input parameters.
Writing Custom Executors
Executors have one primary function - virtual Result run()
that must be implemented. If an executor has any internal executors, it will call these executors' Result exec()
functions - NOT their run functions.
All executors' "exec" and "run" functions return a Result
value containing information about how execution turned out within the executor tree. Each executor is responsible for recording how convergence/success occurs within it. This should generally be accomplished using the Result::pass(msg)
and Result::fail(msg)
functions on a result object created and initialized by calling the newResult()
member function:
Result
FooExecutor::run()
{
Result & r = newResult(); // MUST catch this return value by reference
...
bool success = ... // do some solve stuff
if (!success)
r.fail("the foo didn't work right with the bar");
else
{
// by default, a result is considered successful/converged - so we only need
// to call fail on failure - and calling pass on success is optional.
r.pass("runnin' like a well oiled machine");
}
...
return r;
}
Some executors will have internal/sub executors that they need to execute. They are both responsible for initiating this execution as well as recording the result value generated by these executors using the Result::record
function:
InputParameters
Steady2::validParams()
{
InputParameters params = Executor::validParams();
// create input parameters for our sub/internal solve executors
params.addRequiredParam<std::string>("solve1", "the first solve");
params.addRequiredParam<std::string>("solve2", "the second solve");
return params;
}
FooExecutor::FooExecutor(InputParameters & params)
: _inner_solve1(&_fe_problem.getExecutor(getParam<std::string>("solve1"))), // retrieve inner executor objects
_inner_solve2(&_fe_problem.getExecutor(getParam<std::string>("solve2")))
{
}
Result
FooExecutor::run()
{
Result & r = newResult();
...
// When we record an inner/sub executor's result, we give it a label - which
// helps identify its placement/role within the executor hierarchy.
r.record("solve1", _inner_solve1->exec());
r.record("solve2", _inner_solve2->exec());
...
return r;
}
Result values provide a convenience bool convergedAll()
function for recursively determining if any single executor result within the currently executed portion of the tree has failed to converge. When checking for convergence within an executor, this is usually the mechanism that should be used:
Result
FooExecutor::run()
{
Result & r = newResult();
...
// When we record an inner/sub executor's result, we give it a label - which
// helps identify its placement/role within the executor hierarchy.
r.record("solve1", _inner_solve1->exec());
r.record("solve2", _inner_solve2->exec());
// something inside _inner_solve1 or _inner_solve2 may have failed to converge
if (!r.convergedAll())
{
r.fail("foo iterations didn't work right"); // maybe add additional error msg context
return r; // maybe you want to bail early
}
r.record("solve3", _inner_solve3->exec());
...
return r;
}