Writing Methods
[ This page is under development ]
All method classes are subclasses of the Method
class. The virtual
methods that concrete classes override are shown below:
-
virtual void Method::compute(Block *block) = 0
Apply the method to advance a block one timestep
The
Block::compute_done()
method MUST be invoked on allblock
s passed to this member function by the end of the control flow that this function launches.In simple cases, that should be done just before this function returns.
The placement will varies in more complex cases. For example, if this function invokes a single reduction, the call to
Block::compute_done()
should be performed after completing the reduction (e.g. in thecompute_resume
member function)
-
virtual std::string Method::name() = 0
Return the name of this Method.
-
inline virtual double Method::timestep(Block *block)
Compute maximum timestep for this method
The default implementation returns the maximum finite value of
double
-
inline virtual void Method::compute_resume(Block *block, CkReductionMsg *msg)
Resume computation after a reduction
This member function only typically needs to be implemented by Method classes that employ reductions.
Note
This page is very incomplete. Among other things, we have not discussed
pup
routines.
Overview
When writing a Method
class, it’s useful to understand how it is used by Cello/Enzo-E
.
Recall that when you launch Enzo-E
, you specify how many PEs (processing elements) should be used on the command line.
During startup, a list of Method
objects are constructed on each PE, based on the parameter file (this list is managed by the PE’s Problem
instance).
It’s also useful to remember simulation data (e.g. fields and particles) are associated with Block
objects.
Throughout the simulation each PE is responsible for evolving a local set of 1 or more Block
objects (note that load-balancing can theoretically migrate Block
objects).
Before each compute cycle, Cello/Enzo-E
determines the current timestep.
For each local Block
, a PE invokes the timestep
for each of its Method
instances to determine constraints on the next timestep.
Some other considerations (e.g. user-specified scheduling of operations/stopping at certain simulation times) may alter the duration of the timestep.
Finally, a reduction is performed to pool together the constraints from all blocks.
During the compute cycle, each PE executes a control flow similar to the following code snippet. Be mindful, that the following snippet doesn’t actually exist anywhere in the codebase.
void call_compute_on_all_methods(std::vector<Method*> &method_l,
std::vector<Block*> &local_block_l){
std::size_t num_methods = method_l.size();
std::size_t num_local_blocks = local_block_l.size();
for (std::size_t method_ind = 0; method_ind < num_methods; num_methods++){
Method* cur_method = method_l[method_ind];
for (std::size_t j = 0; j < num_local_blocks; j++){
/* do some fancy stuff related to refreshing fields with data from
neighboring blocks */
// call compute on the method
cur_method->compute(local_block_l[j])
}
/* apply a synchronization barrier to make sure that all blocks across
* all processes have finished completing the current Method.
*
* (essentially, wait for block->compute_done() to be called on every
* block...)
*/
}
}
Pitfalls
The previous section should have made it clear that a given Method
instance generally has its timestep
and compute
method invoked on one or more Block
per cycle.
Consequently, problems can arise if you mutate the attributes of a Method
instance based on data from a given Block
instance.
A good rule-of-thumb for new developers is that you should generally avoid mutating attributes Method
object outside of the constructor.
If you need to associate data with a given Block
, you should consider using one of the specialized data interfaces that exist for:
field data (managed by
Field
)particle data (managed by
Particle
)scalar data (managed by
Scalar
)
An advantage of using these interfaces is that the associated data will be appropriately migrated if a Block
migrates between PEs.
In certain cases one might alternatively add an attribute to EnzoBlock
, but that’s generally discouraged if it can be avoided (the Scalar
interface is usually a better choice).
As an aside, there may be times where it makes sense to violate this guideline (e.g. to facillitate optimizations).
Standard properties tracked in base class
All Method
classes provide some standardized properties that are managed through the base class.
In some of the following cases, we will talk about how the parameter gets specified for a hypothetical method called "my_method"
(in this hypothetical scenario, subclass’s implementation of name()
would return "my_method"
).
Courant Number
All Method
classes have an associated courant condition.
For a method named "my_method"
, the courant value is specified via the parameter called Method:my_method:courant
.
This parameter is automatically parsed by machinery in the Cello
layer and the machinery will update the Method
objects with this value right after the constructor is called.
The value of this parameter can be accessed in a Method
subclass with the following function:
-
inline double Method::courant() const
Query the associated courant factor.
This parameter is usually accessed in the subclass’s implementation of timestep()
.
At this time, developers should avoid parsing and tracking the courant value separately within the subclass.
Note
This should not be confused with the Method:courant
parameter.
This parameter specifies a global courant factor that should never be touched by a Method
subclass.
Instead, this parameter is entirely handled by the rest of the Cello
infrastructure.
Scheduling
All Method
classes support the ability to be scheduled.
For a method named "my_method"
, the schedule is specified via a subgroup called schedule
.
The rules for specifying a schedule are fairly standard and are described elsewhere in the documentation.
The initialization and usage of an associated schedule are all handled by external Cello
machinery.
A Method
subclass should never need to interact with it (in fact, interacting with it improperly could cause problems).
Refresh Machinery
Cello
provides some standardized machinery for specifying requirements related to the fields and particles that need to be refreshed.
The refresh operations are automatically handled by the Cello machinery prior to calls to the method
class.
Configuration of this machinery is typically handled in the constructor of a Method
subclass.
[ This section is incomplete ]