1#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct GpuMdAtom {
15 pub pos: [f32; 3],
17 pub vel: [f32; 3],
19 pub force: [f32; 3],
21 pub mass: f32,
23 pub charge: f32,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct GpuMdParams {
30 pub epsilon: f32,
32 pub sigma: f32,
34 pub cutoff: f32,
36 pub box_size: [f32; 3],
38 pub n_atoms: usize,
40}
41
42#[derive(Debug, Clone)]
44pub struct GpuMdBuffer {
45 pub atoms: Vec<GpuMdAtom>,
47 pub step: usize,
49}
50
51impl GpuMdBuffer {
52 pub fn new(n: usize) -> Self {
54 Self {
55 atoms: vec![
56 GpuMdAtom {
57 pos: [0.0; 3],
58 vel: [0.0; 3],
59 force: [0.0; 3],
60 mass: 1.0,
61 charge: 0.0,
62 };
63 n
64 ],
65 step: 0,
66 }
67 }
68
69 pub fn len(&self) -> usize {
71 self.atoms.len()
72 }
73
74 pub fn is_empty(&self) -> bool {
76 self.atoms.is_empty()
77 }
78}
79
80pub fn lj_force_gpu(r: f32, params: &GpuMdParams) -> f32 {
87 if r >= params.cutoff || r < 1e-10 {
88 return 0.0;
89 }
90 let sr = params.sigma / r;
91 let sr6 = sr * sr * sr * sr * sr * sr;
92 let sr12 = sr6 * sr6;
93 4.0 * params.epsilon * (12.0 * sr12 - 6.0 * sr6) / r
95}
96
97pub fn lj_potential_gpu(r: f32, params: &GpuMdParams) -> f32 {
102 if r >= params.cutoff || r < 1e-10 {
103 return 0.0;
104 }
105 let sr = params.sigma / r;
106 let sr6 = sr * sr * sr * sr * sr * sr;
107 let sr12 = sr6 * sr6;
108 4.0 * params.epsilon * (sr12 - sr6)
109}
110
111pub fn pbc_distance_gpu(a: [f32; 3], b: [f32; 3], box_size: [f32; 3]) -> f32 {
117 let mut r2 = 0.0_f32;
118 for k in 0..3 {
119 let mut d = a[k] - b[k];
120 let l = box_size[k];
121 if l > 0.0 {
122 d -= (d / l).round() * l;
123 }
124 r2 += d * d;
125 }
126 r2.sqrt()
127}
128
129fn pbc_displacement_gpu(a: [f32; 3], b: [f32; 3], box_size: [f32; 3]) -> [f32; 3] {
131 let mut disp = [0.0_f32; 3];
132 for k in 0..3 {
133 let mut d = a[k] - b[k];
134 let l = box_size[k];
135 if l > 0.0 {
136 d -= (d / l).round() * l;
137 }
138 disp[k] = d;
139 }
140 disp
141}
142
143pub fn compute_forces_gpu(buf: &mut GpuMdBuffer, params: &GpuMdParams) {
149 let n = buf.atoms.len();
150 for atom in buf.atoms.iter_mut() {
152 atom.force = [0.0; 3];
153 }
154 for i in 0..n {
155 for j in (i + 1)..n {
156 let pi = buf.atoms[i].pos;
157 let pj = buf.atoms[j].pos;
158 let disp = pbc_displacement_gpu(pi, pj, params.box_size);
159 let r2 = disp[0] * disp[0] + disp[1] * disp[1] + disp[2] * disp[2];
160 let r = r2.sqrt();
161 if r < 1e-10 || r >= params.cutoff {
162 continue;
163 }
164 let f_mag = lj_force_gpu(r, params);
165 let scale = -f_mag / r;
168 buf.atoms[i].force[0] += scale * disp[0];
169 buf.atoms[i].force[1] += scale * disp[1];
170 buf.atoms[i].force[2] += scale * disp[2];
171 buf.atoms[j].force[0] -= scale * disp[0];
172 buf.atoms[j].force[1] -= scale * disp[1];
173 buf.atoms[j].force[2] -= scale * disp[2];
174 }
175 }
176}
177
178pub fn verlet_integrate_gpu(buf: &mut GpuMdBuffer, dt: f32) {
186 for atom in buf.atoms.iter_mut() {
187 let inv_mass = if atom.mass > 1e-10 {
188 1.0 / atom.mass
189 } else {
190 0.0
191 };
192 atom.vel[0] += atom.force[0] * inv_mass * dt;
193 atom.vel[1] += atom.force[1] * inv_mass * dt;
194 atom.vel[2] += atom.force[2] * inv_mass * dt;
195 atom.pos[0] += atom.vel[0] * dt;
196 atom.pos[1] += atom.vel[1] * dt;
197 atom.pos[2] += atom.vel[2] * dt;
198 }
199 buf.step += 1;
200}
201
202pub fn kinetic_energy_gpu(buf: &GpuMdBuffer) -> f32 {
206 buf.atoms
207 .iter()
208 .map(|a| 0.5 * a.mass * (a.vel[0] * a.vel[0] + a.vel[1] * a.vel[1] + a.vel[2] * a.vel[2]))
209 .sum()
210}
211
212pub fn potential_energy_gpu(buf: &GpuMdBuffer, params: &GpuMdParams) -> f32 {
214 let n = buf.atoms.len();
215 let mut pe = 0.0_f32;
216 for i in 0..n {
217 for j in (i + 1)..n {
218 let r = pbc_distance_gpu(buf.atoms[i].pos, buf.atoms[j].pos, params.box_size);
219 pe += lj_potential_gpu(r, params);
220 }
221 }
222 pe
223}
224
225pub fn temperature_gpu(buf: &GpuMdBuffer) -> f32 {
230 let n = buf.atoms.len();
231 if n == 0 {
232 return 0.0;
233 }
234 let kb = 8.314e-3_f32; let ke = kinetic_energy_gpu(buf);
236 2.0 * ke / (3.0 * n as f32 * kb)
237}
238
239pub fn rescale_velocities_gpu(buf: &mut GpuMdBuffer, target_temp: f32) {
245 let t_curr = temperature_gpu(buf);
246 if t_curr < 1e-10 || target_temp < 0.0 {
247 return;
248 }
249 let scale = (target_temp / t_curr).sqrt();
250 for atom in buf.atoms.iter_mut() {
251 atom.vel[0] *= scale;
252 atom.vel[1] *= scale;
253 atom.vel[2] *= scale;
254 }
255}
256
257#[cfg(test)]
260mod tests {
261 use super::*;
262
263 fn default_params() -> GpuMdParams {
264 GpuMdParams {
265 epsilon: 1.0,
266 sigma: 1.0,
267 cutoff: 3.5,
268 box_size: [10.0; 3],
269 n_atoms: 4,
270 }
271 }
272
273 fn make_buf_grid(n: usize) -> GpuMdBuffer {
274 let mut buf = GpuMdBuffer::new(n);
275 for i in 0..n {
276 buf.atoms[i].pos = [i as f32 * 1.5, 0.0, 0.0];
277 buf.atoms[i].mass = 1.0;
278 }
279 buf
280 }
281
282 #[test]
283 fn test_gpu_md_atom_fields() {
284 let a = GpuMdAtom {
285 pos: [1.0, 2.0, 3.0],
286 vel: [0.1, 0.2, 0.3],
287 force: [0.0; 3],
288 mass: 12.0,
289 charge: -0.5,
290 };
291 assert_eq!(a.mass, 12.0);
292 assert_eq!(a.charge, -0.5);
293 }
294
295 #[test]
296 fn test_gpu_md_params_fields() {
297 let p = default_params();
298 assert_eq!(p.n_atoms, 4);
299 assert!(p.cutoff > p.sigma);
300 }
301
302 #[test]
303 fn test_gpu_md_buffer_new() {
304 let buf = GpuMdBuffer::new(5);
305 assert_eq!(buf.len(), 5);
306 assert!(!buf.is_empty());
307 assert_eq!(buf.step, 0);
308 }
309
310 #[test]
311 fn test_gpu_md_buffer_empty() {
312 let buf = GpuMdBuffer::new(0);
313 assert!(buf.is_empty());
314 }
315
316 #[test]
317 fn test_lj_potential_minimum() {
318 let params = default_params();
319 let r_min = 2.0_f32.powf(1.0 / 6.0) * params.sigma;
321 let u = lj_potential_gpu(r_min, ¶ms);
322 assert!((u - (-params.epsilon)).abs() < 0.01);
324 }
325
326 #[test]
327 fn test_lj_potential_zero_beyond_cutoff() {
328 let params = default_params();
329 assert_eq!(lj_potential_gpu(params.cutoff + 0.1, ¶ms), 0.0);
330 }
331
332 #[test]
333 fn test_lj_potential_zero_near_zero_r() {
334 let params = default_params();
335 assert_eq!(lj_potential_gpu(0.0, ¶ms), 0.0);
336 }
337
338 #[test]
339 fn test_lj_force_repulsive_close() {
340 let params = default_params();
341 let f = lj_force_gpu(0.8, ¶ms);
343 assert!(f > 0.0);
344 }
345
346 #[test]
347 fn test_lj_force_attractive_far() {
348 let params = default_params();
349 let f = lj_force_gpu(1.2, ¶ms);
351 assert!(f < 0.0);
352 }
353
354 #[test]
355 fn test_lj_force_zero_beyond_cutoff() {
356 let params = default_params();
357 assert_eq!(lj_force_gpu(params.cutoff + 1.0, ¶ms), 0.0);
358 }
359
360 #[test]
361 fn test_pbc_distance_no_wrap() {
362 let params = default_params();
363 let a = [1.0, 0.0, 0.0];
364 let b = [2.0, 0.0, 0.0];
365 let d = pbc_distance_gpu(a, b, params.box_size);
366 assert!((d - 1.0).abs() < 1e-5);
367 }
368
369 #[test]
370 fn test_pbc_distance_wrap() {
371 let box_size = [10.0_f32; 3];
372 let a = [0.5, 0.0, 0.0];
373 let b = [9.5, 0.0, 0.0];
374 let d = pbc_distance_gpu(a, b, box_size);
376 assert!((d - 1.0).abs() < 1e-4);
377 }
378
379 #[test]
380 fn test_pbc_distance_self() {
381 let box_size = [10.0_f32; 3];
382 let a = [3.0, 4.0, 5.0];
383 let d = pbc_distance_gpu(a, a, box_size);
384 assert!(d < 1e-5);
385 }
386
387 #[test]
388 fn test_compute_forces_gpu_newton3() {
389 let mut buf = GpuMdBuffer::new(2);
390 buf.atoms[0].pos = [0.0; 3];
391 buf.atoms[1].pos = [1.2, 0.0, 0.0];
392 buf.atoms[0].mass = 1.0;
393 buf.atoms[1].mass = 1.0;
394 let params = default_params();
395 compute_forces_gpu(&mut buf, ¶ms);
396 assert!((buf.atoms[0].force[0] + buf.atoms[1].force[0]).abs() < 1e-5);
398 }
399
400 #[test]
401 fn test_compute_forces_gpu_zero_beyond_cutoff() {
402 let mut buf = GpuMdBuffer::new(2);
403 buf.atoms[0].pos = [0.0; 3];
404 buf.atoms[1].pos = [5.0, 0.0, 0.0]; let params = default_params();
406 compute_forces_gpu(&mut buf, ¶ms);
407 assert!(buf.atoms[0].force[0].abs() < 1e-8);
408 }
409
410 #[test]
411 fn test_verlet_integrate_gpu_position() {
412 let mut buf = GpuMdBuffer::new(1);
413 buf.atoms[0].vel = [1.0, 0.0, 0.0];
414 buf.atoms[0].force = [0.0; 3];
415 verlet_integrate_gpu(&mut buf, 0.1);
416 assert!((buf.atoms[0].pos[0] - 0.1).abs() < 1e-5);
417 }
418
419 #[test]
420 fn test_verlet_integrate_gpu_step_counter() {
421 let mut buf = GpuMdBuffer::new(1);
422 verlet_integrate_gpu(&mut buf, 0.01);
423 assert_eq!(buf.step, 1);
424 }
425
426 #[test]
427 fn test_kinetic_energy_gpu_zero_vel() {
428 let buf = make_buf_grid(4);
429 let ke = kinetic_energy_gpu(&buf);
430 assert!(ke.abs() < 1e-8);
431 }
432
433 #[test]
434 fn test_kinetic_energy_gpu_nonzero() {
435 let mut buf = GpuMdBuffer::new(2);
436 buf.atoms[0].vel = [1.0, 0.0, 0.0];
437 buf.atoms[0].mass = 2.0;
438 buf.atoms[1].vel = [0.0, 0.0, 0.0];
439 buf.atoms[1].mass = 1.0;
440 let ke = kinetic_energy_gpu(&buf);
441 assert!((ke - 1.0).abs() < 1e-5);
443 }
444
445 #[test]
446 fn test_potential_energy_gpu_empty() {
447 let buf = GpuMdBuffer::new(0);
448 let params = default_params();
449 assert_eq!(potential_energy_gpu(&buf, ¶ms), 0.0);
450 }
451
452 #[test]
453 fn test_potential_energy_gpu_single() {
454 let buf = GpuMdBuffer::new(1);
455 let params = default_params();
456 assert_eq!(potential_energy_gpu(&buf, ¶ms), 0.0);
458 }
459
460 #[test]
461 fn test_temperature_gpu_zero_vel() {
462 let buf = make_buf_grid(4);
463 let t = temperature_gpu(&buf);
464 assert!(t < 1e-6);
465 }
466
467 #[test]
468 fn test_temperature_gpu_empty() {
469 let buf = GpuMdBuffer::new(0);
470 assert_eq!(temperature_gpu(&buf), 0.0);
471 }
472
473 #[test]
474 fn test_temperature_gpu_nonzero() {
475 let mut buf = GpuMdBuffer::new(3);
476 for a in buf.atoms.iter_mut() {
477 a.vel = [1.0, 1.0, 1.0];
478 a.mass = 1.0;
479 }
480 let t = temperature_gpu(&buf);
481 assert!(t > 0.0);
482 }
483
484 #[test]
485 fn test_rescale_velocities_gpu() {
486 let mut buf = GpuMdBuffer::new(4);
487 for a in buf.atoms.iter_mut() {
488 a.vel = [1.0, 0.5, 0.2];
489 a.mass = 1.0;
490 }
491 let target = 300.0;
492 rescale_velocities_gpu(&mut buf, target);
493 let t_after = temperature_gpu(&buf);
494 assert!((t_after - target).abs() < 1.0);
495 }
496
497 #[test]
498 fn test_rescale_velocities_gpu_zero_vel_noop() {
499 let mut buf = GpuMdBuffer::new(2);
500 rescale_velocities_gpu(&mut buf, 300.0);
502 for a in &buf.atoms {
503 assert!(a.vel[0].abs() < 1e-8);
504 }
505 }
506
507 #[test]
508 fn test_buf_clone() {
509 let buf = make_buf_grid(3);
510 let buf2 = buf.clone();
511 assert_eq!(buf2.len(), 3);
512 }
513
514 #[test]
515 fn test_compute_forces_accumulate_many() {
516 let mut buf = make_buf_grid(4);
517 let params = default_params();
518 compute_forces_gpu(&mut buf, ¶ms);
519 let fx_total: f32 = buf.atoms.iter().map(|a| a.force[0]).sum();
521 assert!(fx_total.abs() < 1e-4);
522 }
523
524 #[test]
525 fn test_lj_potential_positive_repulsive() {
526 let params = default_params();
527 let u = lj_potential_gpu(0.5, ¶ms);
529 assert!(u > 0.0);
530 }
531
532 #[test]
533 fn test_verlet_integrate_gpu_velocity_from_force() {
534 let mut buf = GpuMdBuffer::new(1);
535 buf.atoms[0].force = [2.0, 0.0, 0.0];
536 buf.atoms[0].mass = 1.0;
537 verlet_integrate_gpu(&mut buf, 0.5);
538 assert!((buf.atoms[0].vel[0] - 1.0).abs() < 1e-5);
540 }
541
542 #[test]
543 fn test_pbc_distance_3d_wrap() {
544 let box_size = [5.0_f32; 3];
545 let a = [0.1, 0.1, 0.1];
546 let b = [4.9, 4.9, 4.9];
547 let d = pbc_distance_gpu(a, b, box_size);
548 let expected = 0.2 * 3.0_f32.sqrt();
550 assert!((d - expected).abs() < 1e-4);
551 }
552
553 #[test]
554 fn test_total_energy_two_atoms() {
555 let mut buf = GpuMdBuffer::new(2);
556 buf.atoms[0].pos = [0.0; 3];
557 buf.atoms[0].mass = 1.0;
558 buf.atoms[1].pos = [1.1, 0.0, 0.0]; buf.atoms[1].mass = 1.0;
560 let params = default_params();
561 let pe = potential_energy_gpu(&buf, ¶ms);
562 assert!(pe < 0.0);
564 }
565}