Aboria

PrevUpHomeNext

Examples - Container/Iterator API

Neighbourhood Search
Molecular Dynamics (Linear Spring) (MD)
1955 FPUT Experiment

This example creates $N$ randomly distributed particles in a periodic domain. For each particle, the number of neighbouring particles within a certain radius is counted.

#include <random>

#include "Aboria.h"
using namespace Aboria;

int main() {
    // Create a 2d particle container with one additional variable which
    // will hold the count for each particle
    ABORIA_VARIABLE(count, int, "number_of_neighbours")
    typedef Particles<std::tuple<count>,2> container_type;

    typedef typename container_type::position position;

    // set radius and number of particles
    const double radius = 0.5;
    const int N = 100;

    // create the $N$ particles
    container_type particles(N);

    // create N particles uniformly distributed
    std::uniform_real_distribution<double> uni(0, 1);
    for (int i = 0; i < N; ++i) {
      get<position>(particles)[i] = vdouble2(uni(generator), uni(generator));
    }

    // initiate neighbour search on a periodic 2d domain of side length L
    // set average number of particles per cell to 1
    particles.init_neighbour_search(vdouble2(0, 0), vdouble2(1, 1),
                                    vbool2(true, true));

    // count neighbours around each particle (note: includes self!) and store in
    // `count` variable
    for (int i = 0; i < N; ++i) {
      get<count>(particles)[i] = 0;
      for (auto j = euclidean_search(particles.get_query(),
                                     get<position>(particles)[i], radius);
           j != false; ++j) {
        get<count>(particles)[i]++;
      }
    }
  }

Simple Molecular Dynamics of $N$ particles interacting via a linear spring potential.

This example creates $N$ particles within a two-dimensional square domain, with periodic boundary conditions.

There is a linear spring force $\mathbf{f}_{ij}$ between particles $i$ and $j$ with a rest separation of $r$ (constant for all particles), and a cutoff at $r$. That is, if $\mathbf{r}_i$ is the position of particle $i$ and $\mathbf{dx}_{ij}=\mathbf{r}_j-\mathbf{r}_j$, then

$$ \mathbf{f}_{ij} = \begin{cases} \frac{r-|\mathbf{dx}_{ij}|}{|\mathbf{dx}_{ij}|}\mathbf{dx}_{ij}, & \text{for } |\mathbf{dx}_{ij}|<r \\ 0 & \text{otherwise}. \end{cases} $$

We wish to use a leap frog integrator to evolve positions $\mathbf{r}_i$ using velocities $\mathbf{v}_i$ and accelerations $\mathbf{a}_i = \sum_j \mathbf{f}_{ij}$. This gives the following update equations for each timestep $n$

\begin{align*} \mathbf{v}^{n+1}_i &= \mathbf{v}^n_i + \frac{dt}{m_i} \sum_j \mathbf{f}^n_{ij} \\ \mathbf{r}^{n+1}_i &= \mathbf{r}^n_i + dt, \mathbf{v}^{n+1}_i. \end{align*}

We implement this in Aboria using the code given below. Firstly we create the particle set data structure and add particles, ensuring that we have an initial condition where all the spring forces are $\mathbf{f}_{ij}=0$. Then we start the timestep loop, using our update equations given above.

#include <boost/math/constants/constants.hpp>
#include <math.h>
#include <random>

#include "Aboria.h"
using namespace Aboria;

int main() {
    const double PI = boost::math::constants::pi<double>();

    // Create a 2d particle container with one additional variable
    // "velocity", represented by a 2d double vector
    ABORIA_VARIABLE(velocity, vdouble2, "velocity")
            typedef Particles<std::tuple<velocity>,2> container_type;

    typedef typename container_type::position position;
    container_type particles;

    // set parameters for the MD simulation
    const int timesteps = 3000;
    const int nout = 200;
    const int timesteps_per_out = timesteps / nout;
    const double L = 31.0 / 1000.0;
    const int N = 30;
    const double diameter = 0.0022;
    const double k = 1.0e01;
    const double dens = 1160.0;
    const double mass = PI * std::pow(0.5 * diameter, 2) * dens;
    const double reduced_mass = 0.5 * mass;
    const double dt = (1.0 / 50.0) * PI / std::sqrt(k / reduced_mass);
    const double v0 = L / (timesteps * dt);

    // initiate neighbour search on a periodic 2d domain of side length L
    // set average number of particles per cell to 1
    particles.init_neighbour_search(vdouble2(0, 0), vdouble2(L, L),
                                    vbool2(true, true));

    // create N particles, ensuring that they do not overlap, according
    // to the set diameter. set the initial velocity in a random direction
    std::uniform_real_distribution<double> uni(0, 1);
    for (int i = 0; i < N; ++i) {
      bool free_position = false;

      // create new particle
      typename container_type::value_type p;

      // set a random direction, and initialise velocity
      const double theta = uni(generator) * 2 * PI;
      get<velocity>(p) = v0 * vdouble2(cos(theta), sin(theta));

      // randomly choose positions within the domain until one is
      // found with no other particles within a range equal to diameter
      while (free_position == false) {
        get<position>(p) = vdouble2(uni(generator) * L, uni(generator) * L);
        free_position = true;

        // loop over all neighbouring particles within a euclidean distance
        // of size "diameter"
        for (auto j = euclidean_search(particles.get_query(), get<position>(p),
                                       diameter);
             j != false; ++j) {
          free_position = false;
          break;
        }
      }
      particles.push_back(p);
    }

    // perform MD timestepping
    for (int io = 0; io < nout; ++io) {

      // on every i/o step write particle container to a vtk
      // unstructured grid file
      std::cout << "." << std::flush;
#ifdef HAVE_VTK
      vtkWriteGrid("particles", io, particles.get_grid(true));
#endif
      for (int t = 0; t < timesteps_per_out; t++) {

        // loop through particles and calculate velocity update
        for (auto i : particles) {
          auto sum = vdouble2::Constant(0);

          // use a range search with radius `diameter` to
          // accelerate velocity update
          for (auto j = euclidean_search(particles.get_query(),
                                         get<position>(i), diameter);
               j != false; ++j) {
            const double r = j.dx().norm();
            if (r != 0) {
              sum += -k * (diameter / r - 1.0) * j.dx();
            }
          }
          get<velocity>(i) += dt * sum / mass;
        }

        // now loop through particles and use velocity
        // to update the particle positions
        for (auto i : particles) {
          get<position>(i) += dt * get<velocity>(i);
        }

        // since we have changed the particle positions, we need
        // to update the spatial data structures
        particles.update_positions();
      }
    }
    std::cout << std::endl;
  }

The first-ever numerical experiment was performed in 1955 by Fermi, Pasta, Ulam, and Tsingou. They simulated a lattice of particles, with nearest neighbour particles connected by springs. The motion of the particles over time is chaotic and quasi-periodic.

This experiment was the first to demonstrate the importance of computer simulation in the analysis of non-linear systems, and demonstrates the paradox that many apparently chaotic systems exhibit periodic behaviour.

We reproduce this experiment using Aboria below. Of course, being a 1D lattice code, Aboria is overkill for this type of simulation, but given that the first numerical experiment was a particle simulation I had to include it in the examples.

The equations of motion for each particle in terms of its displacement from a rest configuration are

$$ \frac{d^2 u_j}{dt^2} = \frac{c^2}{h^2} (u_{j+1}+u_{j-1}-2u_{j}) + \alpha [(u_{j+1}-u_{j})^2 - (u_{j}-u_{j-1})^2] $$

If we calculate the energy in the lowest mode

$$ E_1 = 0.5(\dot{A_1}^2 + w_1^2 A_1^2) $$

where $w_1^2 = 4 \sin^2(\pi/2N)$, and $A_1 = \sqrt{2/(N+1)}\sum_{n=1}^N u_n \sin(n\pi/(N+1))$ and set up an initial condition with all the energy in this lowest mode, then we see that while the energy initially diffuses away to the higher modes, after a long enough time the energy returns to the lowest modes until the system is in its original state.

If you compile the code below with Cairo enabled, then it will output svg files showing the image below. The circles are the particles, coloured red by their displacement $u$, and the line shows the amount of energy in the lowest mode versus time.

#include "Aboria.h"
#include <random>
using namespace Aboria;

#include <boost/math/constants/constants.hpp>

int main() {
    const double PI = boost::math::constants::pi<double>();

    // setup types
    ABORIA_VARIABLE(velocity, vdouble1, "velocity")
    ABORIA_VARIABLE(acceleration, vdouble1, "acceleration")
    ABORIA_VARIABLE(acceleration0, vdouble1, "old acceleration")
    typedef Particles<std::tuple<velocity, acceleration, acceleration0>, 1>
        particles_t;
    typedef particles_t::position position;

    // simulation parameters
    const double c = 1.0;
    const double alpha = 0.25;
    const double h = 1.0;
    const double scale = std::pow(c, 2) / std::pow(h, 2);
    const int N = 32;

    const double w1 = 2.0 * std::sin(PI / (2.0 * N));
    const double Tf = 160 * 2 * PI / w1;
    const long timesteps = 1000000;
    const int nout = 1000;
    const int timesteps_per_out = timesteps / nout;
    const double dt = static_cast<double>(Tf) / timesteps;

    // create particles
    particles_t particles(N);

    // set intial variables, position here is simply a displacement
    // for each particle away from its resting position
    for (size_t i = 0; i < N; ++i) {
      get<position>(particles)[i][0] = std::sin((i + 1) * PI / (N + 1));
      get<acceleration>(particles)[i][0] = 0;
      get<velocity>(particles)[i][0] = 0;
    }

    // time loop
    std::vector<double> energy(nout, 0);
    for (size_t out = 0; out < nout; ++out) {
      for (size_t t = 0; t < timesteps_per_out; ++t) {
        // advance position: x1 = x0 + dt*v + 0.5*dt^2*a
        for (size_t i = 0; i < N; ++i) {
          get<position>(particles)[i] +=
              dt * get<velocity>(particles)[i] +
              0.5 * std::pow(dt, 2) * get<acceleration>(particles)[i];
        }

        // calculate acceleration: a1 = f(x1)
        for (size_t i = 0; i < N; ++i) {
          // get displacements, taking into account boundary conditions
          // of x0 = xN = 0
          const double &x = get<position>(particles)[i][0];
          const double xp1 =
              i != N - 1 ? get<position>(particles)[i + 1][0] : 0;
          const double xm1 = i != 0 ? get<position>(particles)[i - 1][0] : 0;

          get<acceleration0>(particles)[i] = get<acceleration>(particles)[i];
          get<acceleration>(particles)[i][0] =
              scale * (xp1 + xm1 - 2 * x) +
              alpha * (std::pow(xp1 - x, 2) - std::pow(x - xm1, 2));
        }

        // advance velocity: v1 = v0 + 0.5*dt*(a0 + a1)
        for (size_t i = 0; i < N; ++i) {
          get<velocity>(particles)[i] += 0.5 * dt *
                                         (get<acceleration0>(particles)[i] +
                                          get<acceleration>(particles)[i]);
        }
      }
      // calculate energy in the lowest mode
      // (w1*A1)^2 = potential energy
      // (Adot1)^2 = kinetic energy
      double A1 = 0;
      double Adot1 = 0;
      for (size_t i = 0; i < N; ++i) {
        const double &u = get<position>(particles)[i][0];
        const double &v = get<velocity>(particles)[i][0];
        A1 += std::sqrt(2.0 / (N + 1)) * u * std::sin((i + 1) * PI / (N + 1));
        Adot1 +=
            std::sqrt(2.0 / (N + 1)) * v * std::sin((i + 1) * PI / (N + 1));
      }
      energy[out] = 0.5 * (std::pow(Adot1, 2) + std::pow(w1 * A1, 2));

      // visualise system if compiled with cairo
#ifdef HAVE_CAIRO
      // create surface
      const int image_size = 512;
      const double ratio = 1.0 / 5.0;
      cairo_surface_t *surface = cairo_svg_surface_create(
          ("fput" + std::to_string(out) + ".svg").c_str(), image_size,
          image_size * ratio);
      cairo_svg_surface_restrict_to_version(surface, CAIRO_SVG_VERSION_1_2);
      cairo_t *cr = cairo_create(surface);

      // set source
      cairo_scale(cr, image_size, image_size);
      cairo_set_source_rgba(cr, 0, 0, 0, 1.0);
      const double lw = 0.01;
      cairo_set_line_width(cr, lw);

      // draw particles
      for (size_t i = 0; i < N; ++i) {
        const double &u = get<position>(particles)[i][0];
        const double pos = 0.1 * u + (i + 1) * dx;
        cairo_set_source_rgba(cr, std::abs(u), 0, 0, 1.0);
        cairo_arc(cr, pos, 0.5 * ratio, lw, 0, 2 * PI);
        cairo_fill(cr);
      }

      // draw energy
      cairo_set_source_rgba(cr, 1, 0, 0, 0.8);
      cairo_move_to(cr, 0, energy[0] * ratio);
      for (size_t i = 1; i < out; ++i) {
        cairo_line_to(cr, static_cast<double>(i) / nout,
                      (1.0 - 10 * energy[i]) * ratio);
      }
      cairo_stroke(cr);

      // clean up
      cairo_destroy(cr);
      cairo_surface_destroy(surface);
#endif // HAVE_CAIRO
    }
  }

PrevUpHomeNext