#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GpuMdAtom {
pub pos: [f32; 3],
pub vel: [f32; 3],
pub force: [f32; 3],
pub mass: f32,
pub charge: f32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GpuMdParams {
pub epsilon: f32,
pub sigma: f32,
pub cutoff: f32,
pub box_size: [f32; 3],
pub n_atoms: usize,
}
#[derive(Debug, Clone)]
pub struct GpuMdBuffer {
pub atoms: Vec<GpuMdAtom>,
pub step: usize,
}
impl GpuMdBuffer {
pub fn new(n: usize) -> Self {
Self {
atoms: vec![
GpuMdAtom {
pos: [0.0; 3],
vel: [0.0; 3],
force: [0.0; 3],
mass: 1.0,
charge: 0.0,
};
n
],
step: 0,
}
}
pub fn len(&self) -> usize {
self.atoms.len()
}
pub fn is_empty(&self) -> bool {
self.atoms.is_empty()
}
}
pub fn lj_force_gpu(r: f32, params: &GpuMdParams) -> f32 {
if r >= params.cutoff || r < 1e-10 {
return 0.0;
}
let sr = params.sigma / r;
let sr6 = sr * sr * sr * sr * sr * sr;
let sr12 = sr6 * sr6;
4.0 * params.epsilon * (12.0 * sr12 - 6.0 * sr6) / r
}
pub fn lj_potential_gpu(r: f32, params: &GpuMdParams) -> f32 {
if r >= params.cutoff || r < 1e-10 {
return 0.0;
}
let sr = params.sigma / r;
let sr6 = sr * sr * sr * sr * sr * sr;
let sr12 = sr6 * sr6;
4.0 * params.epsilon * (sr12 - sr6)
}
pub fn pbc_distance_gpu(a: [f32; 3], b: [f32; 3], box_size: [f32; 3]) -> f32 {
let mut r2 = 0.0_f32;
for k in 0..3 {
let mut d = a[k] - b[k];
let l = box_size[k];
if l > 0.0 {
d -= (d / l).round() * l;
}
r2 += d * d;
}
r2.sqrt()
}
fn pbc_displacement_gpu(a: [f32; 3], b: [f32; 3], box_size: [f32; 3]) -> [f32; 3] {
let mut disp = [0.0_f32; 3];
for k in 0..3 {
let mut d = a[k] - b[k];
let l = box_size[k];
if l > 0.0 {
d -= (d / l).round() * l;
}
disp[k] = d;
}
disp
}
pub fn compute_forces_gpu(buf: &mut GpuMdBuffer, params: &GpuMdParams) {
let n = buf.atoms.len();
for atom in buf.atoms.iter_mut() {
atom.force = [0.0; 3];
}
for i in 0..n {
for j in (i + 1)..n {
let pi = buf.atoms[i].pos;
let pj = buf.atoms[j].pos;
let disp = pbc_displacement_gpu(pi, pj, params.box_size);
let r2 = disp[0] * disp[0] + disp[1] * disp[1] + disp[2] * disp[2];
let r = r2.sqrt();
if r < 1e-10 || r >= params.cutoff {
continue;
}
let f_mag = lj_force_gpu(r, params);
let scale = -f_mag / r;
buf.atoms[i].force[0] += scale * disp[0];
buf.atoms[i].force[1] += scale * disp[1];
buf.atoms[i].force[2] += scale * disp[2];
buf.atoms[j].force[0] -= scale * disp[0];
buf.atoms[j].force[1] -= scale * disp[1];
buf.atoms[j].force[2] -= scale * disp[2];
}
}
}
pub fn verlet_integrate_gpu(buf: &mut GpuMdBuffer, dt: f32) {
for atom in buf.atoms.iter_mut() {
let inv_mass = if atom.mass > 1e-10 {
1.0 / atom.mass
} else {
0.0
};
atom.vel[0] += atom.force[0] * inv_mass * dt;
atom.vel[1] += atom.force[1] * inv_mass * dt;
atom.vel[2] += atom.force[2] * inv_mass * dt;
atom.pos[0] += atom.vel[0] * dt;
atom.pos[1] += atom.vel[1] * dt;
atom.pos[2] += atom.vel[2] * dt;
}
buf.step += 1;
}
pub fn kinetic_energy_gpu(buf: &GpuMdBuffer) -> f32 {
buf.atoms
.iter()
.map(|a| 0.5 * a.mass * (a.vel[0] * a.vel[0] + a.vel[1] * a.vel[1] + a.vel[2] * a.vel[2]))
.sum()
}
pub fn potential_energy_gpu(buf: &GpuMdBuffer, params: &GpuMdParams) -> f32 {
let n = buf.atoms.len();
let mut pe = 0.0_f32;
for i in 0..n {
for j in (i + 1)..n {
let r = pbc_distance_gpu(buf.atoms[i].pos, buf.atoms[j].pos, params.box_size);
pe += lj_potential_gpu(r, params);
}
}
pe
}
pub fn temperature_gpu(buf: &GpuMdBuffer) -> f32 {
let n = buf.atoms.len();
if n == 0 {
return 0.0;
}
let kb = 8.314e-3_f32; let ke = kinetic_energy_gpu(buf);
2.0 * ke / (3.0 * n as f32 * kb)
}
pub fn rescale_velocities_gpu(buf: &mut GpuMdBuffer, target_temp: f32) {
let t_curr = temperature_gpu(buf);
if t_curr < 1e-10 || target_temp < 0.0 {
return;
}
let scale = (target_temp / t_curr).sqrt();
for atom in buf.atoms.iter_mut() {
atom.vel[0] *= scale;
atom.vel[1] *= scale;
atom.vel[2] *= scale;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_params() -> GpuMdParams {
GpuMdParams {
epsilon: 1.0,
sigma: 1.0,
cutoff: 3.5,
box_size: [10.0; 3],
n_atoms: 4,
}
}
fn make_buf_grid(n: usize) -> GpuMdBuffer {
let mut buf = GpuMdBuffer::new(n);
for i in 0..n {
buf.atoms[i].pos = [i as f32 * 1.5, 0.0, 0.0];
buf.atoms[i].mass = 1.0;
}
buf
}
#[test]
fn test_gpu_md_atom_fields() {
let a = GpuMdAtom {
pos: [1.0, 2.0, 3.0],
vel: [0.1, 0.2, 0.3],
force: [0.0; 3],
mass: 12.0,
charge: -0.5,
};
assert_eq!(a.mass, 12.0);
assert_eq!(a.charge, -0.5);
}
#[test]
fn test_gpu_md_params_fields() {
let p = default_params();
assert_eq!(p.n_atoms, 4);
assert!(p.cutoff > p.sigma);
}
#[test]
fn test_gpu_md_buffer_new() {
let buf = GpuMdBuffer::new(5);
assert_eq!(buf.len(), 5);
assert!(!buf.is_empty());
assert_eq!(buf.step, 0);
}
#[test]
fn test_gpu_md_buffer_empty() {
let buf = GpuMdBuffer::new(0);
assert!(buf.is_empty());
}
#[test]
fn test_lj_potential_minimum() {
let params = default_params();
let r_min = 2.0_f32.powf(1.0 / 6.0) * params.sigma;
let u = lj_potential_gpu(r_min, ¶ms);
assert!((u - (-params.epsilon)).abs() < 0.01);
}
#[test]
fn test_lj_potential_zero_beyond_cutoff() {
let params = default_params();
assert_eq!(lj_potential_gpu(params.cutoff + 0.1, ¶ms), 0.0);
}
#[test]
fn test_lj_potential_zero_near_zero_r() {
let params = default_params();
assert_eq!(lj_potential_gpu(0.0, ¶ms), 0.0);
}
#[test]
fn test_lj_force_repulsive_close() {
let params = default_params();
let f = lj_force_gpu(0.8, ¶ms);
assert!(f > 0.0);
}
#[test]
fn test_lj_force_attractive_far() {
let params = default_params();
let f = lj_force_gpu(1.2, ¶ms);
assert!(f < 0.0);
}
#[test]
fn test_lj_force_zero_beyond_cutoff() {
let params = default_params();
assert_eq!(lj_force_gpu(params.cutoff + 1.0, ¶ms), 0.0);
}
#[test]
fn test_pbc_distance_no_wrap() {
let params = default_params();
let a = [1.0, 0.0, 0.0];
let b = [2.0, 0.0, 0.0];
let d = pbc_distance_gpu(a, b, params.box_size);
assert!((d - 1.0).abs() < 1e-5);
}
#[test]
fn test_pbc_distance_wrap() {
let box_size = [10.0_f32; 3];
let a = [0.5, 0.0, 0.0];
let b = [9.5, 0.0, 0.0];
let d = pbc_distance_gpu(a, b, box_size);
assert!((d - 1.0).abs() < 1e-4);
}
#[test]
fn test_pbc_distance_self() {
let box_size = [10.0_f32; 3];
let a = [3.0, 4.0, 5.0];
let d = pbc_distance_gpu(a, a, box_size);
assert!(d < 1e-5);
}
#[test]
fn test_compute_forces_gpu_newton3() {
let mut buf = GpuMdBuffer::new(2);
buf.atoms[0].pos = [0.0; 3];
buf.atoms[1].pos = [1.2, 0.0, 0.0];
buf.atoms[0].mass = 1.0;
buf.atoms[1].mass = 1.0;
let params = default_params();
compute_forces_gpu(&mut buf, ¶ms);
assert!((buf.atoms[0].force[0] + buf.atoms[1].force[0]).abs() < 1e-5);
}
#[test]
fn test_compute_forces_gpu_zero_beyond_cutoff() {
let mut buf = GpuMdBuffer::new(2);
buf.atoms[0].pos = [0.0; 3];
buf.atoms[1].pos = [5.0, 0.0, 0.0]; let params = default_params();
compute_forces_gpu(&mut buf, ¶ms);
assert!(buf.atoms[0].force[0].abs() < 1e-8);
}
#[test]
fn test_verlet_integrate_gpu_position() {
let mut buf = GpuMdBuffer::new(1);
buf.atoms[0].vel = [1.0, 0.0, 0.0];
buf.atoms[0].force = [0.0; 3];
verlet_integrate_gpu(&mut buf, 0.1);
assert!((buf.atoms[0].pos[0] - 0.1).abs() < 1e-5);
}
#[test]
fn test_verlet_integrate_gpu_step_counter() {
let mut buf = GpuMdBuffer::new(1);
verlet_integrate_gpu(&mut buf, 0.01);
assert_eq!(buf.step, 1);
}
#[test]
fn test_kinetic_energy_gpu_zero_vel() {
let buf = make_buf_grid(4);
let ke = kinetic_energy_gpu(&buf);
assert!(ke.abs() < 1e-8);
}
#[test]
fn test_kinetic_energy_gpu_nonzero() {
let mut buf = GpuMdBuffer::new(2);
buf.atoms[0].vel = [1.0, 0.0, 0.0];
buf.atoms[0].mass = 2.0;
buf.atoms[1].vel = [0.0, 0.0, 0.0];
buf.atoms[1].mass = 1.0;
let ke = kinetic_energy_gpu(&buf);
assert!((ke - 1.0).abs() < 1e-5);
}
#[test]
fn test_potential_energy_gpu_empty() {
let buf = GpuMdBuffer::new(0);
let params = default_params();
assert_eq!(potential_energy_gpu(&buf, ¶ms), 0.0);
}
#[test]
fn test_potential_energy_gpu_single() {
let buf = GpuMdBuffer::new(1);
let params = default_params();
assert_eq!(potential_energy_gpu(&buf, ¶ms), 0.0);
}
#[test]
fn test_temperature_gpu_zero_vel() {
let buf = make_buf_grid(4);
let t = temperature_gpu(&buf);
assert!(t < 1e-6);
}
#[test]
fn test_temperature_gpu_empty() {
let buf = GpuMdBuffer::new(0);
assert_eq!(temperature_gpu(&buf), 0.0);
}
#[test]
fn test_temperature_gpu_nonzero() {
let mut buf = GpuMdBuffer::new(3);
for a in buf.atoms.iter_mut() {
a.vel = [1.0, 1.0, 1.0];
a.mass = 1.0;
}
let t = temperature_gpu(&buf);
assert!(t > 0.0);
}
#[test]
fn test_rescale_velocities_gpu() {
let mut buf = GpuMdBuffer::new(4);
for a in buf.atoms.iter_mut() {
a.vel = [1.0, 0.5, 0.2];
a.mass = 1.0;
}
let target = 300.0;
rescale_velocities_gpu(&mut buf, target);
let t_after = temperature_gpu(&buf);
assert!((t_after - target).abs() < 1.0);
}
#[test]
fn test_rescale_velocities_gpu_zero_vel_noop() {
let mut buf = GpuMdBuffer::new(2);
rescale_velocities_gpu(&mut buf, 300.0);
for a in &buf.atoms {
assert!(a.vel[0].abs() < 1e-8);
}
}
#[test]
fn test_buf_clone() {
let buf = make_buf_grid(3);
let buf2 = buf.clone();
assert_eq!(buf2.len(), 3);
}
#[test]
fn test_compute_forces_accumulate_many() {
let mut buf = make_buf_grid(4);
let params = default_params();
compute_forces_gpu(&mut buf, ¶ms);
let fx_total: f32 = buf.atoms.iter().map(|a| a.force[0]).sum();
assert!(fx_total.abs() < 1e-4);
}
#[test]
fn test_lj_potential_positive_repulsive() {
let params = default_params();
let u = lj_potential_gpu(0.5, ¶ms);
assert!(u > 0.0);
}
#[test]
fn test_verlet_integrate_gpu_velocity_from_force() {
let mut buf = GpuMdBuffer::new(1);
buf.atoms[0].force = [2.0, 0.0, 0.0];
buf.atoms[0].mass = 1.0;
verlet_integrate_gpu(&mut buf, 0.5);
assert!((buf.atoms[0].vel[0] - 1.0).abs() < 1e-5);
}
#[test]
fn test_pbc_distance_3d_wrap() {
let box_size = [5.0_f32; 3];
let a = [0.1, 0.1, 0.1];
let b = [4.9, 4.9, 4.9];
let d = pbc_distance_gpu(a, b, box_size);
let expected = 0.2 * 3.0_f32.sqrt();
assert!((d - expected).abs() < 1e-4);
}
#[test]
fn test_total_energy_two_atoms() {
let mut buf = GpuMdBuffer::new(2);
buf.atoms[0].pos = [0.0; 3];
buf.atoms[0].mass = 1.0;
buf.atoms[1].pos = [1.1, 0.0, 0.0]; buf.atoms[1].mass = 1.0;
let params = default_params();
let pe = potential_energy_gpu(&buf, ¶ms);
assert!(pe < 0.0);
}
}