Extending Rockable
Rockable is meant to be extended by reading and modifying it, not by scripting it from the outside. The design is deliberate: there is no Lua or Python layer, so a new interaction model is written in C++, compiled in, and becomes a keyword of the conf-file like any other.
Five families of components are pluggable through the same mechanism: force laws, body forces, data extractors, pre-processing commands, and post-processors.
The factory mechanism
Each family has an abstract base class, and the concrete classes register
themselves in a factory keyed by their class name. The registration happens
in Rockable::ExplicitRegistrations:
REGISTRER_BASE_DERIVED(ForceLaw, Avalanche);
The macro stringifies the class name, so the keyword written in the conf-file is
exactly the name of the C++ class. This is why forceLaw Avalanches does not
work while forceLaw Avalanche does: the factory is looked up by that exact
string.
ForceLaw* FL = Factory<ForceLaw>::Instance()->Create(lawName);
When the lookup fails, Rockable warns and falls back to a default rather than stopping, so a misspelt name produces a warning and a silently different simulation. It is worth reading the first lines of the log.
The keyword parser
The conf-file is parsed by a kwParser, a map from a keyword to a lambda that
reads the rest of the entry from the stream:
parser.kwMap["tmax"] = __GET__(conf, tmax);
parser.kwMap["density"] = __DO__(conf) {
size_t grp;
double density;
conf >> grp >> density;
properties.set(idDensity, grp, density);
};
Anything the map does not know produces an unknown token message and is
skipped. Two consequences are worth keeping in mind:
Warning
The map is keyed by string, so two components registering the same keyword silently overwrite each other, the last one registered winning. When adding a component, check that its keyword is not already taken.
Note
Parsing is token-based, not line-based. The arguments of a keyword may be
spread over several lines, which is what makes the long Servo and
packing entries readable.
Adding a force law
Create
src/ForceLaws/ForceLaw_MyLaw.hppand.cpp, deriving fromForceLaw.Implement
init(), which declares the parameters the law reads:void MyLaw::init() { box->idKnContact = box->dataTable.add("knContact"); box->idMuContact = box->dataTable.add("muContact"); }
dataTable.addboth allocates the group-pair table and makes the keyword readable from the conf-file. A parameter that is not declared here cannot be set.Implement
computeInteraction(Interaction& I). It returnstruewhen the interaction is active, that is when it carries a non-zero force, andfalsewhen it should be ignored:bool MyLaw::computeInteraction(Interaction& I) { if (I.dn > 0.0) { // no overlap, no contact I.fn = 0.0; I.ft.reset(); I.mom.reset(); return false; } ... return true; }
Register the class in
Rockable::ExplicitRegistrations, and add the file toCMakeLists.txt.
Important
computeInteraction is called from a parallel loop. It may write to the
interaction it is given, but writing to a body, or to any shared structure,
needs the protection that parallel_mode provides. Look at how the
existing laws accumulate forces before adding your own.
Tip
Start from ForceLaw_Default.cpp and change what you need.
ForceLaw_geoVisc.cpp is a good example of a minimal departure: it is the
default law with a viscous friction instead of an elastic one.
Adding a data extractor
Derive from DataExtractor and implement four methods:
Method |
Role |
|---|---|
|
Read the parameters from |
|
Anything that needs the configuration to be loaded and the driving system to be read. |
|
Computations to perform with a period of |
|
Write one line, with a period of |
void MyExtractor::read(std::istream& is) {
is >> ibody >> filename >> nrec;
if (box->isInteractive() == false) recordFile.open(filename.c_str());
nstep = std::numeric_limits<int>::max();
docString << "ibody = " << ibody;
columnDoc.clear();
columnDoc.push_back("Time");
columnDoc.push_back("what the second column holds");
}
Note
The isInteractive test matters: it prevents the extractor from opening,
and truncating, its output file when the conf-file is merely being looked at
with see or conftovtk.
Filling columnDoc is not optional in practice: it is what
extractedDataDoc.txt is built from, and an output file whose columns are
undocumented is of little use six months later.
Set nstep to a huge value when the extractor has nothing to compute between
two records, as all the current ones do.
Adding a pre-processing command
Derive from PreproCommand and implement two methods:
void myCommand::addCommand() {
box->parser.kwMap["myCommand"] = [this](std::istream& conf) {
conf >> this->someParameter;
exec();
};
}
void myCommand::exec() {
// act on box->Particles, box->Interfaces, ...
}
Then add the name to the commands array at the end of
Rockable::initParser, which is what instantiates the command and calls
addCommand on it.
Warning
The keyword written in addCommand must match the class name used in the
array. This is precisely where a copy/paste error is easy to make and hard to
notice: a wrong keyword there does not fail, it overwrites another command’s
entry and makes both behave unexpectedly.
Note
A pre-processing command runs while the conf-file is being parsed, so it sees only what has been read before it. It must therefore be placed after the data it acts on, which is why they are documented as belonging at the end of the file.
Adding a post-processor
Derive from PostProcessor, and implement read, init, exec and
end. The parameters are read with a local kwParser, which lets a
post-processor have its own small keyword set:
void MyPostProcessor::read(std::istream& is) {
kwParser parser;
parser.kwMap["Volume"] = __GET__(istr, Volume);
parser.parse(is);
}
exec() is called once per processed conf-file, and end() after the last
one, which is where a summary or a distribution is written.
Where things live
Directory |
Contents |
|---|---|
|
|
|
|
|
One file per interaction model. |
|
One file per body force. |
|
One file per on-the-fly measurement. |
|
One file per pre-processing command. |
|
One file per post-processor. |
|
Reusable analyses: clusters, broken sub-clusters, interaction groups, mass range, solid fraction probe. |
|
Dependencies fetched by CMake, |
The library toofus provides most of the utilities used throughout:
vec3r, quat, mat9r, AABB, OBB, kwParser, DataTable,
Factory, Tempo, and the logging.
Testing a change
Build the regression tests with -DROCKABLE_USE_TESTING=ON. The comparison
itself is available from the command line:
rockable input.txt -n conf10 -r reference/conf10
which runs the simulation and then compares the two conf-files line by line from
their Particles section onwards, exiting with an error if they differ.
Tip
A conf-file whose first token is redirection holds a path and a file
name, and loads that file instead. It is how the tests share one sample
between several cases without duplicating it.