Simulating molecular dynamics efficiently

Okay, here is a way to compute the forces much faster: We create a CompiledFunction (called getForces). It eats a list of points in the plane and spits out the net force onto the first point of the list; here the second to last points are supposed to be those points that are so close to the first one that they exert a force onto it.

size = 50.;(*size of the box*)
box = {{-size, size}, {-size, size}};
r0 = 1.;(*diameter of one particle*)

Quiet[Block[{r, x1, x2, y1, y2, xx, yy, force, potential},
   xx = {x1, x2};
   yy = {y1, y2};
   potential = r \[Function] 1/4 ((r0/r)^2 - (r0/r));
   force = -D[potential[Sqrt[Dot[xx - yy, xx - yy]]], {xx, 1}];
   With[{
     f1code = N@force[[1]], f2code = N@force[[2]], slope = 100.,
     a1 = N@box[[1, 1]], b1 = N@box[[1, 2]], a2 = N@box[[2, 1]], 
     b2 = N@box[[2, 2]]
     },
    getForces = Compile[{{X, _Real, 2}},
      Block[{x1, x2, y1, y2, f1, f2},
       x1 = Compile`GetElement[X, 1, 1];
       x2 = Compile`GetElement[X, 1, 2];
       f1 = slope (Ramp[a1 - x1] - Ramp[x1 - b1]);
       f2 = slope (Ramp[a2 - x2] - Ramp[x2 - b2]);
       Do[
        y1 = Compile`GetElement[X, i, 1];
        y2 = Compile`GetElement[X, i, 2];
        f1 += f1code;
        f2 += f2code;
        , {i, 2, Length[X]}
        ];
       {f1, f2}
       ],
      CompilationTarget -> "C",
      RuntimeAttributes -> {Listable},
      Parallelization -> True
      ]
    ]]];

The following is a (not so pure) function that computes the $n \times 2$ matrix xnew consisting of the new point positions; it uses the matrix x of positions at a given time instance and the matrix xold which represents the positions at the preceeding time instance. xold is handled as side effect which makes it not too nicely, but this way, we can use it in NestList later.

step = x \[Function] (
    xnew =  2. x - xold +  h^2 (1./m) getForces[Nearest[x, x, {\[Infinity], epsilon}]];
    xold = x;
    xnew
    );

Setting up the remaining parameters and the initial conditions...

SeedRandom[1234];
n = 200;(*number of particules*)
h = 0.01;(*time step*)
epsilon = 10.;(*radius of influence*)
(*initial conditions*)
x0 =   N@Table[{2 Mod[i, size] - size, Floor[i/50] - size}, {i, n}];
v0 = 0.1 size RandomReal[{-1, 1}, {n, 2}];
x1 = x0 + h v0;(*Verlet integration scheme*)
m = ConstantArray[1., n];(*particle masses*)

... and now, we let it run:

timesteps = 10000;
xold = x0;
x = x1;
data = Join[{x0}, NestList[step, x1, timesteps]]; // AbsoluteTiming

{4.58476, Null}

Aha, roughly 2000 iterations per second. That's at least not so much worse than the JavaScript implementation...

And here a visualization:

frames = Map[X \[Function] Graphics[{PointSize[1/size ],
      Point[X]}, PlotRange -> box, PlotRangePadding -> 0.1 size
     ], data];
Export["a.gif", frames[[1 ;; -1 ;; 20]]]

enter image description here

Note that I use a different potential (less singular) in order to get it running. Of course, you can play with the parameters...

Further improvements

In the meantime, I did some hand tuning of the compiled code. The new positions get now computed completely within the CompiledFunction. That's the first new thing. The second is that for each point also all current positions, the preceeding position of this point, and an index list ilist with the indices for the relevant points are handed over. The first index in ilist markes the point for which we want to compute the new position; the other entries mark the points in the region of interaction. This way, we can recycle the index lists ilists that Nearest will produce every now and then in the main loop (see below).

size = 50.;(*size of the box*)
box = {{-size, size}, {-size, size}};
r0 = 1.;(*diameter of one particle*)
Quiet[
 Block[{x1, x2, y1, y2, xx, yy, force, potential, r, r2},
  xx = {x1, x2};
  yy = {y1, y2};
  potential = r \[Function] 4 ((r0/r)^12 - (r0/r)^6);
  force = Simplify[
    -D[potential[Sqrt[Dot[xx - yy, xx - yy]]], {xx, 1}] 
    /. (x1 - y1)^2 + (x2 - y2)^2 -> r2
    ] /. Sqrt[r2] -> r;

  With[{
    f1code = N@force[[1]], f2code = N@force[[2]], slope = 100.,
    a1 = N@box[[1, 1]], b1 = N@box[[1, 2]], a2 = N@box[[2, 1]], 
    b2 = N@box[[2, 2]]
    },
   getStep = 
    Compile[{{X, _Real, 2}, {Xold, _Real, 1}, {ilist, _Integer, 
       1}, {factor, _Real}},
     Block[{x1, x2, y1, y2, f1, f2, r, r2, j, i},
      j = Compile`GetElement[ilist, 1];
      x1 = Compile`GetElement[X, j, 1];
      x2 = Compile`GetElement[X, j, 2];
      f1 = slope (Ramp[a1 - x1] - Ramp[x1 - b1]);
      f2 = slope (Ramp[a2 - x2] - Ramp[x2 - b2]);
      Do[
       i = Compile`GetElement[ilist, k];
       y1 = Compile`GetElement[X, i, 1];
       y2 = Compile`GetElement[X, i, 2];
       r2 = (x1 - y1)^2 + (x2 - y2)^2;
       r = Sqrt[r2];
       f1 += f1code;
       f2 += f2code;
       , {k, 2, Length[ilist]}
       ];
      {
       2. x1 - Compile`GetElement[Xold, 1] + factor f1,
       2. x2 - Compile`GetElement[Xold, 2] + factor f2
       }
      ],
     CompilationTarget -> "C",
     RuntimeAttributes -> {Listable},
     Parallelization -> True,
     RuntimeOptions -> "Speed"
     ]
   ]

  ]];

Preparing some constants...

SeedRandom[1234];
n = 1000;(*number of particules*)
h = 0.005;(*time step*)

epsilon = 3.;(*radius of influence*)
(*initial conditions*)

x0 = Developer`ToPackedArray[
   N@Table[{2 Mod[i, size] - size, Floor[i/50] - size}, {i, n}]];
v0 = 0.1 size RandomReal[{-1, 1}, {n, 2}];
x1 = x0 + h v0;

m = ConstantArray[1., n];(*particle masses*)
factors = (h^2./m);
timesteps = 10000;
skip = 10; (*Nearest gets called only every 10th time iteration*)

Main loop

We write directly into a preallocated array (which makes no difference since NestList is cleverly implemented). The major change is that we request only the vertex indices from Nearest (with x -> Automatic). In contrast to the coordinates, these won't change significantly for several iterations. So we have to call Nearest less than once per time iteration. Developer`ToPackedArray seems to be needed because the (listable) getStep is applied to ragged lists (which cannot be packed) and so the output is not packed.

data = ConstantArray[0., {timesteps + 1, n, 2}];
data[[1]] = x0;
data[[2]] = x1;
xold = x0;
x = x1;
ilists = Nearest[x -> Automatic, x, {\[Infinity], epsilon}];
Do[
   If[Mod[iter, skip] == 0,
    ilists = Nearest[x -> Automatic, x, {\[Infinity], epsilon}];
    ];
   data[[iter]] = xnew = Developer`ToPackedArray[getStep[x, xold, ilists, factors]];
   xold = x;
   x = xnew;
   ,
   {iter, 3, timesteps + 1}]; // AbsoluteTiming

{4.37231, Null}

The timing is much better than 2000 steps per second now.

I have also prepared a somewhat nicer visualization:

velocities = Sqrt[(Join[{v0}, Differences[data]/h]^2).ConstantArray[1., {2}]];
colorcoords = Rescale[Clip[velocities, {-100, 100}]];
frames = Table[
   Graphics[{
     PointSize[0.5 r0/size],
     Transpose[{ColorData["TemperatureMap"] /@ colorcoords[[i]], 
       Point /@ data[[i]]}],
     }, PlotRange -> box, PlotRangePadding -> 0.1 size, 
    Background -> Black
    ],
   {i, 1, Length[data], 100}];

Manipulate[frames[[i]],{i, 1, Length[frames], 1}]

enter image description here


To expand on @HenrikSchumacher's comment, compare:

r1 = Table[
    Nearest[Drop[x[1],{i}], x[1][[i]], {100,10}],
    {i,200}
];//AbsoluteTiming

nf = Nearest[x[1]]; //AbsoluteTiming
r2 = nf[x[1], {100, 10}][[All, 2;;]]; //AbsoluteTiming

r1 === r2

{0.004298, Null}

{0.000029, Null}

{0.000463, Null}

True

Addendum

Also, note that you can have the NearestFunction return distances as well:

r1 = Table[
    Norm[#-x[1][[i]]]& /@ Nearest[Drop[x[1],{i}], x[1][[i]], {100,10}],
    {i,200}
]; //AbsoluteTiming

nf = Nearest[x[1]->"Distance"];//AbsoluteTiming
r2 = nf[x[1], {100, 10}][[All, 2;;]]; //AbsoluteTiming

r1 === r2

{0.021323, Null}

{0.000049, Null}

{0.000326, Null}

True