If you have two points, and periodic boundary conditions, and you want to know the closest distance vector between them, that’s easy to write in python:
import numpy as np
[...]
dr = np.remainder(r1 - r2 + L/2., L) - L/2.
If you have a set of, say, 10 points, and you want to know the distance between all 55 pairs of them, that’s still not too complicated:
def dists(rs, L):
"""
Returns the difference vectors between all pairs of points in rs, given a periodic box of size L.
Parameters
----------
rs : (N,d) array_like
Input vectors
L : (d,) array_like, or scalar
Size of the box
Returns
-------
out : (N,N,d) ndarray
Where out[i,j,:] is the difference vector between rs[i,:] and rs[j,:]
"""
diffs = np.array([np.subtract.outer(rd, rd) for rd in rs.T]).T
return np.remainder(diffs + L/2., L) - L/2.
Note that the above function is really only two lines, yet it has some nice properties:
d
We can do something similar in Matlab:
function dr = dists2(rs, L)
% Returns the difference vectors between all pairs of points in rs,
% given a periodic box of size L.
%
% rs : (N,d), Input vectors
% L : scalar, Size of the box
%
% Returns
% -------
% out : (N,N,d),
% Where out(i,j,:) is the difference vector between rs(i,:) and rs(j,:)
[N,d] = size(rs);
out = zeros(N,N,d);
for i = 1:d
x = rs(:,i);
out(:,:,i) = bsxfun(@minus, x, x');
end
dr = mod(out + L/2, L) - L/2;
end
The matlab code has about 8 lines to Python’s 2, and also can’t handle non-square boxes, but it is also using vectorized operations and any number of dimensions.
Out of curiosity, I benchmarked these two functions:
Input size (rs ) |
Matlab | Python |
---|---|---|
100 x 3 |
0.7 ms | 2.6 ms |
200 x 3 |
2.5 ms | 8.3 ms |
500 x 3 |
18.2 ms | 60.8 ms |
1000 x 3 |
83.3 ms | 288.0 ms |
Matlab clearly has the upper hand, by a factor of ~3.
We can also do a nice broadcasting method:
def dists3(rs, L):
diffs = rs[..., np.newaxis] - rs.T[np.newaxis, ...]
diffs = np.rollaxis(diffs, -1, -2)
return np.remainder(diffs + L/2., L) - L/2.
Note that we had to add in a call to np.rollaxis
here, which converts diffs
from \(N \times d \times N\) to \(N \times N \times d\), and allows us to broadcast
over the adding L
in the case where L
is not a scalar.
We can also extend this to a pairwise function for comparing two different sets of particles:
def pair_dists(rs1, rs2, L):
N,d = np.shape(rs)
diffs = rs1[..., np.newaxis] - rs2.T[np.newaxis, ...]
diffs = np.rollaxis(diffs, -1, -2)
return np.remainder(diffs + L/2., L) - L/2.
And if our two pairs of particles may have had a center-of-mass motion that we want to ignore, we can calculate
\[d^{2} = \sum_i \left(\vec{r}_{i} \ominus_{\vec{L}} \vec{s}_{i} - \vec{\delta}\right)^2\]where \(\ominus_{\vec{L}}\) means “shortest distance given periodic boundary conditions in a box of shape \(\vec{L}\),” and \(\vec{\delta}\) is the center-of-mass motion. It turns out that this is equal to
\[d^2 = \frac{1}{N}\sum_{\left\langle i,j\right\rangle }\left(\vec{r}_{ij} \ominus_{\vec{L}} \vec{s}_{ij}\right)^2\]The reason this math works (and the \(\vec \delta\) drops out!) is talked about a bit more in this post. The Python function for this is as follows:
def pair_dist(rs1, rs2, L):
"""
Returns the total distance between pairs of points in rs1 and rs2, given a
periodic box of size L. Ignores overall translation.
"""
dists1 = (rs1[..., np.newaxis] - rs1.T[np.newaxis, ...])
dists2 = (rs2[..., np.newaxis] - rs2.T[np.newaxis, ...])
rij_minus_sij = np.rollaxis(dists1 - dists2, -1, -2)
rij_minus_sij = ((rij_minus_sij + L/2.) % L) - L/2.
dist_sqs = np.triu(np.sum(rij_minus_sij**2, axis=-1))
return np.sqrt(np.sum(dist_sqs) / len(rs1))