This tutorial will help you to set up a simple simulation using the MESA-PD module. The scenario for this tutorial is a particle gas (spheres) confined by solid walls.
For this tutorial only a few waLBerla includes are needed, namely:
The MESA-PD framework is comprised of different core components:
- collision_detection: collision_detection holds algorithms for determining collision information
- data: data holds all datastructures which can be used
- domain: domain contains the coupling to the BlockForest domain partitioning
- kernel: kernel holds all algorithms which act on particles
- mpi: mpi holds all functionality needed for synchronization
- vtk: vtk holds utility functions for writing the simulation to disk
First, the waLBerla environment is initialized, the random number generator is seeded and some simulation parameters are set.
Environment env(argc, argv);
int simulationSteps = 10;
We will use the BlockForest datastructure for the domain partitioning of our simulation. For more information about the general design of the waLBerla framework please refer to Tutorial - Basics 1: waLBerla Data Structures: Blocks and Fields and the documentation of domain_decomposition::BlockStorage.
You can choose the number of blocks you want to have in each direction. In a parallel simulation these blocks get assigned to different processes. You should make sure that you always have at least as many blocks as processes. The number of processes you want your simulation to run with is specified when you start your program with mpiexec.
- Attention
- If you run a simulation with periodic boundaries you need at least three blocks in each direction of periodicity!
Vector3<uint_t>(2,2,2),
Vector3<bool>(false, false, false)
);
if (!forest)
{
return EXIT_SUCCESS;
}
Next, we initialize all data structures that we need for the simulation. The data::ParticleStorage is the data container for all particles. The data::ShapeStorage stores information about the shape of the particles. This information is separated from the particle due to that fact that most of the time there are many similar particles in one simulation. With this approach you do not need to store the information for every particle but only once.
Since all kernels are written against an abstract interface to access the particle properties we also need an intermediate accessor class. This class is in most cases data::ParticleAccessorWithShape.
Finally, to improve the complexity of the collision detection step, we need a data::LinkedCells acceleration data structure.
auto ps = std::make_shared<data::ParticleStorage>(100);
auto ss = std::make_shared<data::ShapeStorage>();
data::ParticleAccessorWithShape accessor(ps, ss);
data::LinkedCells lc(localDomain.getExtended(spacing), spacing );
Now we are almost ready to initialize the particles for our simulation. For this purpose we will write two helper functions to create spheres and walls.
data::ParticleStorage::iterator
createWall( data::ParticleStorage& ps,
data::ShapeStorage& ss,
{
auto pl = ps.create(true);
pl->setPosition ( pos );
pl->setInteractionRadius ( std::numeric_limits<real_t>::infinity() );
pl->setShapeID ( ss.create<data::HalfSpace>( normal ) );
pl->setOwner ( walberla::mpi::MPIManager::instance()->rank() );
pl->setType ( 1 );
return pl;
}
data::ParticleStorage::iterator
createSphere( data::ParticleStorage& ps,
const size_t shapeID)
{
auto sp = ps.create();
sp->setInteractionRadius ( radius );
sp->setShapeID ( shapeID );
sp->setOwner ( walberla::MPIManager::instance()->rank() );
sp->setType ( 0 );
return sp;
}
The position should be self explanatory. The interaction radius is used for a first check if two particles can collide. It is the radius of the bounding volume. The shapeID is the index into the data::ShapeStorage datastructure. The owner is the current owning process of this particle and the type identifies the class this particle belongs to. This type is later on used to determine which material parameters should be used for this particle.
The gas particles are generated with the help of grid generators. These generators are iterators which facilitate the construction of particles on a regular grid. Currently grid_generator::SCIterator (for a simple cubic lattice) as well as grid_generator::HCPIterator (for a hexagonal close packing lattice) are supported.
auto sphereShape = ss->create<data::Sphere>( radius );
ss->shapes[sphereShape]->updateMassAndInertia( density );
uint_t numParticles = uint_c(0);
for (auto blkIt = forest->begin(); blkIt != forest->end(); ++blkIt)
{
IBlock & currentBlock = *blkIt;
for (auto it = grid_generator::SCIterator(currentBlock.getAABB().getIntersection(generationDomain),
Vector3<real_t>(spacing, spacing, spacing) * real_c(0.5),
spacing);
it != grid_generator::SCIterator();
++it)
{
Vec3 rndVel(math::realRandom<real_t>(-vMax, vMax),
math::realRandom<real_t>(-vMax, vMax),
math::realRandom<real_t>(-vMax, vMax));
sp->setLinearVelocity(rndVel);
++numParticles;
}
}
Before we can run the simulation we have to set up all kernels that we will use.
kernel::ExplicitEuler explicitEuler( dt );
kernel::InsertParticleIntoLinkedCells ipilc;
kernel::SpringDashpot dem( 2 );
auto meffSphereSphere =
real_t(0.5) * (real_c(4.0)/real_c(3.0) *
math::pi) * radius * radius * radius * density;
auto meffSphereWall =
real_t(1.0) * (real_c(4.0)/real_c(3.0) *
math::pi) * radius * radius * radius * density;
dem.setParametersFromCOR( 0, 0,
real_t(0.9),
real_t(20) * dt, meffSphereSphere );
dem.setParametersFromCOR( 0, 1,
real_t(0.9),
real_t(20) * dt, meffSphereWall );
collision_detection::AnalyticContactDetection acd;
kernel::DoubleCast double_cast;
mpi::ContactFilter contact_filter;
mpi::ReduceProperty RP;
mpi::SyncNextNeighbors SNN;
In this example we will use the explicit Euler integration method. We further need a kernel which updates our LinkedCell data structure and an interaction kernel. For the collision detection we select the analytic functions available within the framework. Together with all synchronization kernels we can now run the simulation.
- Attention
- Before you start the simulation loop you should synchronize once to make sure everything is in place.
for (int i=0; i < simulationSteps; ++i)
{
if( i % 10 == 0 )
{
}
lc.clear();
ps->forEachParticle(true, kernel::SelectAll(), accessor, ipilc, accessor, lc);
lc.forEachParticlePairHalf(true,
kernel::SelectAll(),
accessor,
[&](const size_t idx1, const size_t idx2, auto& ac)
{
if (double_cast(idx1, idx2, ac, acd, ac ))
{
if (contact_filter(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), domain))
{
dem(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), acd.getContactNormal(), acd.getPenetrationDepth());
}
}
},
accessor );
RP.operator()<ForceTorqueNotification>(*ps);
SNN(*ps, domain);
}
After the simulation is finished we can collect the results. In this case we only calculate the mean velocity of all particles. The particles can be easily accessed via the data::ParticleStorage datastructure.
auto meanVelocity =
real_t(0);
ps->forEachParticle(true,
accessor,
[&meanVelocity](const size_t idx, auto& ac)
{
meanVelocity +=
length(ac.getLinearVelocity(idx));
},
accessor);
meanVelocity /= real_c(numParticles);
Congratulation! You successfully created your first rigid particle simulation.