In order for simulation codes to be flexible, they should allow the end
user to switch different physical effects on or off. They may also allow
the user to switch between different algorithms. In addition the user
may want to include additional calculations on demand, such as the time
averaging of fields. These calculations should only be performed when
needed and it should be possible to configure these choices through the
configuration file without having to re-compile the code. The
hierarchical block structure in Schnek allows the developer to create
modules of simulation code which can be included in the configuration
file. This allows for highly versatile codes. In Schnek, each type of
simulation block is represented by a class that descends from the
Block
class. The Block
class was already introduced in the
first example on setup
files
as the class that represents the complete simulation. However a
simulation code can have a large number of different block types and
these can be arranged in a hierarchical manner. To start with a simple
example, let’s assume we want to create a simulation with a number of
individual particles. Each particle can have its own mass, position and
velocity. In Schnek these particles could be represented by the
following code.
class Particle : public Block {
private:
Array<double,3> pos;
Array<double,3> velocity;
double mass;
protected:
void initParameters(BlockParameters ¶meters) {
parameters.addArrayParameter("pos", pos);
parameters.addArrayParameter("velocity", pos);
parameters.addParameter("mass", &mass);
}
public:
void advance(const Array<double,3> &force, double dt) {
pos = pos+velocity*dt;
velocity = velocity + force*dt/mass;
}
void display() {
std::cout << "Mass: " << mass << std::endl;
std::cout << "Position: (" << pos[0] << ", " << pos[1] << ", " << pos[1] << ")" << std::endl;
std::cout << "Velocity: (" << velocity[0] << ", " << velocity[1] << ", " << velocity[1] << ")" << std::endl;
}
};
The Particle
class is very much like the Block
classes we have
seen in the previous
chapter.
The private members pos
, velocity
, and mass
are registered
with the setup file parser in the initParameters()
function. Two
additional functions, advance()
and display()
have been defined.
The first function will advance the particle position and velocity based
on an external force. The second function will print out the values to
the console. Very similar to the Particle
class, we define a class
that will act as the root class for the simulation.
class NBody : public Block {
private:
double dt;
double totalTime;
protected:
void initParameters(BlockParameters ¶meters) {
parameters.addParameter("dt", &dt);
parameters.addParameter("totalTime", &totalTime);
}
public:
void runSimulation();
};
Again, this class looks very much like the block classes we have seen
before. We have added an additional function runSimulation()
that
will run the simulation. We will implement this function later. The
logical arrangement of these two classes is that an object of type
NBody
, as the main simulation class, should contain an arbitrary
number of Particle
objects. We can tell the setup file parser this
relationship in a few simple commands. In the main()
function simply
include the following lines
BlockClasses blocks;
blocks.registerBlock("NBody").setClass<NBody>();
blocks.registerBlock("Particle").setClass<Particle>();
blocks("NBody").addChildren("Particle");
These lines will register two block types with the BlockClasses
object. As introduced in the previous chapter, the string argument of
the registerBlock()
function determines the name given to the class
in the setup file. The last line of the fragment above specifies the
relationship between the two classes. Here "Particle"
is specified
as the child class of "NBody"
. The rest of the main function follows
the well known pattern.
std::ifstream in("example_blocks_hierarchy01.setup");
Parser P("nbody_simulation", "NBody", blocks);
pBlock application = P.parse(in);
NBody *mysim = dynamic_cast<NBody*>(application.get());
mysim->evaluateParameters();
mysim->runSimulation();
We set the NBody
class as the main class of the simulation and then
parse the configuration file through an input file stream. Because we
know that the pBlock
pointer really points to an NBody
object we
can perform a cast. Then we first need to evaluate the parameters before
we finally run the simulation. After this we can write the setup file.
In the following example we include two particles.
dt = 0.01;
totalTime = 1;
Particle A {
posx = 0;
posy = 0;
posz = 0;
velocityx = 0;
velocityy = 0;
velocityz = 0;
mass = 10;
}
Particle B {
posx = 1;
posy = 0;
posz = 0;
velocityx = 0;
velocityy = 10;
velocityz = 0;
mass = 1;
}
When the code reads this file it will first create an NBody
object
and set the dt
and totalTime
members. After this, two
Particle
objects are created, each with its own values for pos
,
velocity
, and mass
. These two objects are added as children to
the NBody
. Inside the Particle
class you can get the surrounding
NBody
object through the getParent()
function. This function
returns a pBlock
which is a boost shared pointer to the parent
block. Inside the NBody
class you can get a collection of
Particle
objects through the getChildren()
function. This will
return a list of pBlock
pointers. You should use the BlockList
type to store this list. This allows us to write the code for the
runSimulation()
function.
void NBody::runSimulation() {
BlockList children = getChildren();
Array<double,3> force(0.0,0.0,0.0);
for (double time=0.0; time<=totalTime; time+=dt) {
BOOST_FOREACH(pBlock block, children) {
boost::shared_ptr<Particle> particle = boost::dynamic_pointer_cast<Particle>(block);
particle->advance(force, dt);
}
}
BOOST_FOREACH(pBlock block, children) {
std::cout << "Child: " << block->getName() << std::endl;
boost::shared_ptr<Particle> particle = boost::dynamic_pointer_cast<Particle>(block);
particle->display();
}
}
This function iterates through the time steps and for each time step it
loops over all children, advancing their positions. After the simulation
has finished another loop prints out the positions of all the particles.
Note the use of the BOOST_FOREACH
macro here. If you are compiling
using the C++11 standard you can use a ranged based for loop
for (pBlock block : children)
instead. Please not however, that
Schnek has not yet been tested under the C++11 standard. We are using a
dynamic cast to cast the pBlock
pointer to a shared pointer of
Particle
type. We can do this because we know that all children of
NBody
must be of type Particle
. This is OK for the example
presented here. In general we are allowed to declare different classes
as child classes of a block. In this case we would need to check for
each child what class it is and perform the required operation only on
those objects that match a given class. In the next section we will see
how this problem is solved by Schnek. The code for this example can be
downloaded
here.
The setup file can be found
here.