Runs the Smooth Particle Ewald Mesh (SPME) algorithm for n-body simulations with periodic boundary conditions

##Original paper describing the SPME method
This has applications primarily in structural biology. For example, molecular dynamics. Compared to other
n-body approximations for long-range forces, this has utility when periodic bounday conditions are used.
If not using these, for example in cosmology simulations, consider Barnes Hut, or Fast Multipole Methods (FMM)
instead.
Support for SIMD (256-bit and 512-bit), and CUDA (via CUDARC) are planned.
Used by the Daedalus protein viewer and molecular dynamics program.
Here's an example of use:
use rayon::prelude::*;
use ewald::{force_coulomb_ewald_real, force_coulomb_ewald_real};
impl System {
fn apply_forces(&self) {
pairs
.par_iter()
.map(|(i_0, i_1)| {
let atom_0 = &self.atoms[i_0];
let atom_1 = &self.atoms[i_1];
let diff = atom_1.pos - atom_0.pos;
let r = diff.magnitude();
let dir = diff / r;
let mut f = Vec3::zero();
f += force_coulomb_short_range(
dir,
r,
atom_0.charge,
atom_1.charge,
lr_switch: (LONG_RANGE_START, LONG_RANGE_CUTOFF),
α: f64,
);
f += force_coulomb_lr_recip(
dir,
r,
atom_0.charge,
atom_1.charge,
lr_switch: (LONG_RANGE_START, LONG_RANGE_CUTOFF),
α: f64,
);
atom_0.force += f;
atom_1.force -= f;
});
}
fn gather_pme_particles_wrapped(&self) -> (Vec<Vec3>, Vec<f64>, Vec<PMEIndex>) {
let n_dyn = self.atoms.len();
let n_wat = self.water.len();
let n_st = self.atoms_static.len();
let mut pos = Vec::with_capacity(n_dyn + 4 * n_wat + n_st);
let mut q = Vec::with_capacity(pos.capacity());
let mut map = Vec::with_capacity(pos.capacity());
for (i, a) in self.atoms.iter().enumerate() {
pos.push(self.cell.wrap(a.posit)); q.push(a.partial_charge); map.push(PMEIndex::Dyn(i));
}
for (i, w) in self.water.iter().enumerate() {
pos.push(self.cell.wrap(w.o.posit));
q.push(w.o.partial_charge);
map.push(PMEIndex::WatO(i));
pos.push(self.cell.wrap(w.m.posit));
q.push(w.m.partial_charge);
map.push(PMEIndex::WatM(i));
pos.push(self.cell.wrap(w.h0.posit));
q.push(w.h0.partial_charge);
map.push(PMEIndex::WatH0(i));
pos.push(self.cell.wrap(w.h1.posit));
q.push(w.h1.partial_charge);
map.push(PMEIndex::WatH1(i));
}
for (i, a) in self.atoms_static.iter().enumerate() {
pos.push(self.cell.wrap(a.posit));
q.push(a.partial_charge);
map.push(PMEIndex::Static(i));
}
#[cfg(debug_assertions)]
{
let qsum: f64 = q.iter().sum();
if qsum.abs() > 1e-6 {
eprintln!("[PME] Warning: net charge = {qsum:.6e} (PME assumes neutral or a uniform background)");
}
}
(pos, q, map)
}
pub fn regen_pme(&mut self) {
let [lx, ly, lz] = self.cell.extent.to_arr();
self.pme_recip = PmeRecip::new((SPME_N, SPME_N, SPME_N), (lx, ly, lz), EWALD_ALPHA);
}
}