Table of contents


Back to the main page Back to the documentation 1. Overview: MPIParticles 2. Requirements on particle class 3. Creating MPIParticles 4. Saving and loading of particles 5. Accessing the particles

Overview: MPIParticles


MPIParticles is a general class for keeping a set of particles that are shared among tasks and to communicate these particles if they move outside of the domain given to each task. The particle class its templated on has to provide methods for appending and assigning the particle data from the communication buffers we use.

Figure 1: Here we show the full domain. The red region is the domain the current task is responsible for. Particles moving outside the boundaries will be communicated to the respective task once we call communicate_particles().

Requirements on particle class


MPIParticles can be used with any particle class (for more info about particle classes within the library see Global). The minimal requirement is that it has the two methods

// Here FloatingPointType can be any floating point type (float, double, long double)
FloatingPointType *get_pos();

// The dimension of the position
int get_ndim();

If you have dynamical allocated memory inside the class then you must also provide the methods

// How many bytes to communicate
int get_particle_byte_size(){ 

// Append the particle  data to a buffer
void append_to_buffer(char *data);

 // Assign the particle data from a buffer
void assign_from_buffer(char *data);

You can of course also provide these methods if you want even if its not required (which might be useful in case your layout is such that we have a lot of padding between elements). Here is an example of how to make these methods yourself (note that in this example we don't need to provide these methods as we have no dynamically allocated memory inside the class so the fiducial methods we use if these methods are not provided will work):

template<int NDIM>
struct Particle{
  double x[NDIM];
  double v[NDIM];

  // We must have a way to get the position on the particle
  double* get_pos(){ 
    return x; 
  }
  
  // The dimension we are in
  int get_ndim(){
    return NDIM;   
  }
  
  // The velocity
  double* get_vel(){ 
    return v; 
  }
  
  // We need the size of the particle 
  int get_particle_byte_size(){ 
    return sizeof(x) + sizeof(v);
  }

  // Append the particle to a buffer
  void append_to_buffer(char *data){ 
    int bytes = sizeof(x);
    std::memcpy(data, x, bytes); 
    data += bytes;
  
    bytes = sizeof(v);
    std::memcpy(data, v, bytes); 
   }

  // Assign the particle from a buffer
  void assign_from_buffer(char *data){ 
    int bytes = sizeof(x);
    std::memcpy(x, data, bytes); 
    data += bytes;

    bytes = sizeof(v);
    std::memcpy(v, data, bytes); 
  }
};

Creating MPIParticles


We offer methods to create MPIParticles from an exisiting set of particles or to make a regular grid of particles. For the former we can either copy over the particles, provided a selection function that tells us what particles to keep or just simply move the storage of existing particles straight into the class. See the doxygen documentation, the examples or the source for more info.

Here we set up the class and make a regular particle distribution out of it:

FML::PARTICLE::MPIPartice< Particle<3> > part;

// Lets fill it with some particles. Make a regular distribution
const int Npart_1D = 6;           // Make Npart_1D^NDIM particles
const double buffer_factor = 2.0; // How many more particles we allocate for
part.create_particle_grid(
   Npart_1D, 
   buffer_factor, 
   FML::xmin_domain, 
   FML::xmax_domain);

// Move some particles out of bounds
auto& p = part.get_particles();
int i = 0;
while(i < 5){
  p[i++].get_pos()[0] = (std::rand() % 1000)/1000.0;
}

// Communicate the particles so they end up in the right task
p.communicate_particles();

Creating from an exisiting set of particles:

// If we have a set of particles
Particle *p;
int NumPart;

// ... make the particles

//... then we can make MPIParticles out of them by running create
FML::PARTICLE::MPIParticles< Particle > part;
const bool all_tasks_have_the_same_particles = false; // If all tasks reads the same file for example
auto nallocate_local = 10000;                         // Maximum number of particles we think we need on each task (exits if we ever go over)
part.create(
    p 
    NumPart, 
    nallocate_local, 
    FML::xmin_domain, 
    FML::xmax_domain, 
    all_tasks_have_the_same_particles);

Saving and loading of particles


// Save the particles on each task to output.X with X = ThisTask
part.dump_to_file("output");

// Read particles from file output.X with X = ThisTask
part.load_from_file("output");

Accessing the particles


Different ways of accessing the particles:


// Iterate over all the active particles (not buffer)
for(auto pit = part.begin(); pit != part.end(); ++pit){
  // Reference to the current particle
  auto & p = *pit;
  auto pos = p.get_pos();
  ...
}

// Iterate over all the active particles (not buffer)
for(auto & p : part){
  auto pos = p.get_pos();
  ...
}

// Explicit for-loop over the active particles
for(size_t i = 0; i < part.get_npart(); i++){
  auto & p = part[i]; 
  auto pos = p.get_pos();
  ...
}

// Get a reference to the raw storage (vector<T> &)
// Note that this contains all particles. 
// Active partices at the front and buffer particles at the back
auto part_vector = part.get_particles();

// Get a pointer to the first element in the raw storage
// The i'th particle is accessed through p[i]
auto part_ptr = part.get_particles_ptr();