1use super::types::{
6 ForceBuffer, HarmonicAngle, HarmonicBond, LjPotential, NeighborList, VirialStressTensorKernel,
7 VirialTensor,
8};
9use crate::compute::ComputeKernel;
10
11#[cfg(test)]
12use super::types::*;
13
14pub fn compute_lj_force(r: f64, lj: &LjPotential) -> (f64, f64) {
23 if r < 1e-30 {
24 return (f64::INFINITY, f64::INFINITY);
25 }
26 let sr = lj.sigma / r;
27 let sr6 = sr.powi(6);
28 let sr12 = sr6 * sr6;
29 let energy = 4.0 * lj.epsilon * (sr12 - sr6);
30 let force_mag = 24.0 * lj.epsilon * (2.0 * sr12 - sr6) / r;
31 (energy, force_mag)
32}
33pub fn compute_lj_shifted_energy(r: f64, lj: &LjPotential, cutoff: f64) -> f64 {
37 if r >= cutoff {
38 return 0.0;
39 }
40 let (e_r, _) = compute_lj_force(r, lj);
41 let (e_c, _) = compute_lj_force(cutoff, lj);
42 e_r - e_c
43}
44pub fn compute_coulomb_force(r: f64, qi: f64, qj: f64, k_e: f64) -> (f64, f64) {
48 if r < 1e-30 {
49 return (f64::INFINITY, f64::INFINITY);
50 }
51 let energy = k_e * qi * qj / r;
52 let force_mag = k_e * qi * qj / (r * r);
53 (energy, force_mag)
54}
55pub fn compute_lj_forces_neighborlist(
57 positions: &[[f64; 3]],
58 lj: &LjPotential,
59 nlist: &NeighborList,
60 buffer: &mut ForceBuffer,
61) {
62 let cutoff2 = nlist.cutoff * nlist.cutoff;
63 buffer.clear();
64 let n = positions.len();
65 for i in 0..n {
66 for &j in &nlist.neighbors[i] {
67 if j <= i {
68 continue;
69 }
70 let dx = [
71 positions[i][0] - positions[j][0],
72 positions[i][1] - positions[j][1],
73 positions[i][2] - positions[j][2],
74 ];
75 let r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
76 if r2 >= cutoff2 || r2 < 1e-30 {
77 continue;
78 }
79 let r2_inv = 1.0 / r2;
80 let sr2 = lj.sigma * lj.sigma * r2_inv;
81 let sr6 = sr2 * sr2 * sr2;
82 let sr12 = sr6 * sr6;
83 let energy = 4.0 * lj.epsilon * (sr12 - sr6);
84 let f_mag = 24.0 * lj.epsilon * (2.0 * sr12 - sr6) * r2_inv;
85 let f_ij = [f_mag * dx[0], f_mag * dx[1], f_mag * dx[2]];
86 buffer.add_pair(i, j, f_ij, energy, dx);
87 }
88 }
89}
90pub fn compute_coulomb_forces_neighborlist(
92 positions: &[[f64; 3]],
93 charges: &[f64],
94 k_e: f64,
95 nlist: &NeighborList,
96 buffer: &mut ForceBuffer,
97) {
98 let cutoff2 = nlist.cutoff * nlist.cutoff;
99 let n = positions.len();
100 for i in 0..n {
101 for &j in &nlist.neighbors[i] {
102 if j <= i {
103 continue;
104 }
105 let dx = [
106 positions[i][0] - positions[j][0],
107 positions[i][1] - positions[j][1],
108 positions[i][2] - positions[j][2],
109 ];
110 let r2 = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
111 if r2 >= cutoff2 || r2 < 1e-30 {
112 continue;
113 }
114 let r = r2.sqrt();
115 let qi = charges[i];
116 let qj = charges[j];
117 let energy = k_e * qi * qj / r;
118 let f_mag = k_e * qi * qj / (r2 * r);
119 let f_ij = [f_mag * dx[0], f_mag * dx[1], f_mag * dx[2]];
120 buffer.add_pair(i, j, f_ij, energy, dx);
121 }
122 }
123}
124pub fn compute_all_lj_forces(
129 positions: &[[f64; 3]],
130 _masses: &[f64],
131 lj: &LjPotential,
132 cutoff: f64,
133) -> Vec<[f64; 3]> {
134 let n = positions.len();
135 let cutoff2 = cutoff * cutoff;
136 let mut forces = vec![[0.0f64; 3]; n];
137 for i in 0..n {
138 for j in (i + 1)..n {
139 let dx = positions[i][0] - positions[j][0];
140 let dy = positions[i][1] - positions[j][1];
141 let dz = positions[i][2] - positions[j][2];
142 let r2 = dx * dx + dy * dy + dz * dz;
143 if r2 >= cutoff2 || r2 < 1e-30 {
144 continue;
145 }
146 let sr = lj.sigma / r2.sqrt();
147 let sr6 = sr.powi(6);
148 let sr12 = sr6 * sr6;
149 let f_mag = 24.0 * lj.epsilon * (2.0 * sr12 - sr6) / r2;
150 forces[i][0] += f_mag * dx;
151 forces[i][1] += f_mag * dy;
152 forces[i][2] += f_mag * dz;
153 forces[j][0] -= f_mag * dx;
154 forces[j][1] -= f_mag * dy;
155 forces[j][2] -= f_mag * dz;
156 }
157 }
158 forces
159}
160pub fn compute_all_coulomb_forces(
164 positions: &[[f64; 3]],
165 charges: &[f64],
166 k_e: f64,
167 cutoff: f64,
168) -> Vec<[f64; 3]> {
169 let n = positions.len();
170 let cutoff2 = cutoff * cutoff;
171 let mut forces = vec![[0.0f64; 3]; n];
172 for i in 0..n {
173 for j in (i + 1)..n {
174 let dx = positions[i][0] - positions[j][0];
175 let dy = positions[i][1] - positions[j][1];
176 let dz = positions[i][2] - positions[j][2];
177 let r2 = dx * dx + dy * dy + dz * dz;
178 if r2 >= cutoff2 || r2 < 1e-30 {
179 continue;
180 }
181 let r = r2.sqrt();
182 let f_mag = k_e * charges[i] * charges[j] / (r2 * r);
183 forces[i][0] += f_mag * dx;
184 forces[i][1] += f_mag * dy;
185 forces[i][2] += f_mag * dz;
186 forces[j][0] -= f_mag * dx;
187 forces[j][1] -= f_mag * dy;
188 forces[j][2] -= f_mag * dz;
189 }
190 }
191 forces
192}
193pub(super) fn erfc_approx(x: f64) -> f64 {
195 if x < 0.0 {
196 return 2.0 - erfc_approx(-x);
197 }
198 let t = 1.0 / (1.0 + 0.3275911 * x);
199 let poly = t
200 * (0.254829592
201 + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
202 poly * (-x * x).exp()
203}
204pub fn ewald_self_energy(charges: &[f64], alpha: f64) -> f64 {
208 let sum_q2: f64 = charges.iter().map(|&q| q * q).sum();
209 -alpha / std::f64::consts::PI.sqrt() * sum_q2
210}
211pub fn pppm_mesh_energy_estimate(charge_mesh: &[f64], nx: usize, ny: usize, nz: usize) -> f64 {
216 if nx == 0 || ny == 0 || nz == 0 {
217 return 0.0;
218 }
219 let q2: f64 = charge_mesh.iter().map(|&q| q * q).sum();
220 q2 / (nx * ny * nz) as f64
221}
222pub fn compute_virial_stress_tensor(
226 positions: &[[f64; 3]],
227 lj: &LjPotential,
228 cutoff: f64,
229) -> VirialTensor {
230 let n = positions.len();
231 let flat_pos: Vec<f64> = positions.iter().flat_map(|p| p.iter().copied()).collect();
232 let params = vec![lj.epsilon, lj.sigma, cutoff];
233 let mut outputs = vec![Vec::new()];
234 VirialStressTensorKernel.execute(&[&flat_pos, ¶ms], &mut outputs, n);
235 if outputs[0].len() < 6 {
236 return VirialTensor::zero();
237 }
238 let mut c = [0.0f64; 6];
239 c.copy_from_slice(&outputs[0][..6]);
240 VirialTensor { components: c }
241}
242pub fn compute_bond_forces(positions: &[[f64; 3]], bonds: &[HarmonicBond]) -> (Vec<[f64; 3]>, f64) {
249 let n = positions.len();
250 let mut forces = vec![[0.0f64; 3]; n];
251 let mut total_energy = 0.0f64;
252 for bond in bonds {
253 let i = bond.atom_i;
254 let j = bond.atom_j;
255 if i >= n || j >= n {
256 continue;
257 }
258 let dx = positions[j][0] - positions[i][0];
259 let dy = positions[j][1] - positions[i][1];
260 let dz = positions[j][2] - positions[i][2];
261 let r = (dx * dx + dy * dy + dz * dz).sqrt();
262 if r < 1e-30 {
263 continue;
264 }
265 let delta = r - bond.r0;
266 let energy = 0.5 * bond.k * delta * delta;
267 total_energy += energy;
268 let mag = bond.k * delta / r;
269 forces[i][0] += mag * dx;
270 forces[i][1] += mag * dy;
271 forces[i][2] += mag * dz;
272 forces[j][0] -= mag * dx;
273 forces[j][1] -= mag * dy;
274 forces[j][2] -= mag * dz;
275 }
276 (forces, total_energy)
277}
278pub fn compute_angle_forces(
287 positions: &[[f64; 3]],
288 angles: &[HarmonicAngle],
289) -> (Vec<[f64; 3]>, f64) {
290 let n = positions.len();
291 let mut forces = vec![[0.0f64; 3]; n];
292 let mut total_energy = 0.0f64;
293 for angle in angles {
294 let i = angle.atom_i;
295 let j = angle.atom_j;
296 let k = angle.atom_k;
297 if i >= n || j >= n || k >= n {
298 continue;
299 }
300 let rji = [
301 positions[i][0] - positions[j][0],
302 positions[i][1] - positions[j][1],
303 positions[i][2] - positions[j][2],
304 ];
305 let rjk = [
306 positions[k][0] - positions[j][0],
307 positions[k][1] - positions[j][1],
308 positions[k][2] - positions[j][2],
309 ];
310 let len_ji = (rji[0] * rji[0] + rji[1] * rji[1] + rji[2] * rji[2]).sqrt();
311 let len_jk = (rjk[0] * rjk[0] + rjk[1] * rjk[1] + rjk[2] * rjk[2]).sqrt();
312 if len_ji < 1e-30 || len_jk < 1e-30 {
313 continue;
314 }
315 let cos_theta = (rji[0] * rjk[0] + rji[1] * rjk[1] + rji[2] * rjk[2]) / (len_ji * len_jk);
316 let cos_theta = cos_theta.clamp(-1.0, 1.0);
317 let theta = cos_theta.acos();
318 let delta = theta - angle.theta0;
319 total_energy += 0.5 * angle.k_theta * delta * delta;
320 let sin_theta = (1.0 - cos_theta * cos_theta).sqrt().max(1e-12);
321 let d_prefactor = -angle.k_theta * delta / sin_theta;
322 for dim in 0..3 {
323 let d_cos_d_ri =
324 rjk[dim] / (len_ji * len_jk) - cos_theta * rji[dim] / (len_ji * len_ji);
325 let d_cos_d_rk =
326 rji[dim] / (len_ji * len_jk) - cos_theta * rjk[dim] / (len_jk * len_jk);
327 let fi = d_prefactor * d_cos_d_ri;
328 let fk = d_prefactor * d_cos_d_rk;
329 forces[i][dim] += fi;
330 forces[k][dim] += fk;
331 forces[j][dim] -= fi + fk;
332 }
333 }
334 (forces, total_energy)
335}
336pub fn kinetic_temperature(velocities: &[[f64; 3]], masses: &[f64], k_boltzmann: f64) -> f64 {
346 let n = velocities.len();
347 if n == 0 || k_boltzmann < 1e-30 {
348 return 0.0;
349 }
350 let ke2: f64 = velocities
351 .iter()
352 .zip(masses.iter())
353 .map(|(v, &m)| m * (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]))
354 .sum();
355 let n_dof = (3 * n) as f64;
356 ke2 / (n_dof * k_boltzmann)
357}
358pub fn temperature_scale(
365 velocities: &mut [[f64; 3]],
366 masses: &[f64],
367 t_target: f64,
368 k_boltzmann: f64,
369) {
370 let t_current = kinetic_temperature(velocities, masses, k_boltzmann);
371 if t_current < 1e-30 || t_target < 0.0 {
372 return;
373 }
374 let scale = (t_target / t_current).sqrt();
375 for v in velocities.iter_mut() {
376 v[0] *= scale;
377 v[1] *= scale;
378 v[2] *= scale;
379 }
380}
381#[cfg(test)]
382mod tests {
383 use super::*;
384 #[test]
385 fn test_md_lj_force_repulsive_at_short_range() {
386 let sigma = 1.0_f64;
387 let epsilon = 1.0_f64;
388 let cutoff = 5.0_f64;
389 let r = 0.8_f64 * sigma;
390 let positions = vec![0.0, 0.0, 0.0, r, 0.0, 0.0];
391 let params = vec![epsilon, sigma, cutoff];
392 let mut outputs = vec![Vec::new(), Vec::new()];
393 LennardJonesKernel.execute(&[&positions, ¶ms], &mut outputs, 2);
394 let fx0 = outputs[0][0];
395 let fx1 = outputs[0][3];
396 assert!(
397 fx0 < 0.0,
398 "at r < r_min, force on atom 0 should be negative (repulsive), got {fx0}"
399 );
400 assert!(
401 fx1 > 0.0,
402 "at r < r_min, force on atom 1 should be positive (repulsive), got {fx1}"
403 );
404 assert!(
405 (fx0 + fx1).abs() < 1e-10,
406 "forces should sum to zero (Newton III), got {fx0} + {fx1} = {}",
407 fx0 + fx1
408 );
409 }
410 #[test]
411 fn lj_kernel_correct_force_known_separation() {
412 let sigma = 1.0;
413 let epsilon = 1.0;
414 let cutoff = 3.0;
415 let positions = vec![0.0, 0.0, 0.0, sigma, 0.0, 0.0];
416 let params = vec![epsilon, sigma, cutoff];
417 let mut outputs = vec![Vec::new(), Vec::new()];
418 LennardJonesKernel.execute(&[&positions, ¶ms], &mut outputs, 2);
419 let fx0 = outputs[0][0];
420 assert!(
421 (fx0 - (-24.0)).abs() < 1e-10,
422 "expected fx0 ~ -24.0, got {fx0}"
423 );
424 let pe = outputs[1][0];
425 assert!(pe.abs() < 1e-10, "expected PE ~ 0, got {pe}");
426 }
427 #[test]
428 fn lj_kernel_force_zero_beyond_cutoff() {
429 let sigma = 1.0;
430 let epsilon = 1.0;
431 let cutoff = 2.5;
432 let positions = vec![0.0, 0.0, 0.0, 3.0, 0.0, 0.0];
433 let params = vec![epsilon, sigma, cutoff];
434 let mut outputs = vec![Vec::new(), Vec::new()];
435 LennardJonesKernel.execute(&[&positions, ¶ms], &mut outputs, 2);
436 for &f in &outputs[0] {
437 assert!(f.abs() < 1e-15, "expected zero force, got {f}");
438 }
439 assert!(outputs[1][0].abs() < 1e-15);
440 }
441 #[test]
442 fn lj_minimum_at_r_min() {
443 let lj = LjPotential::new(1.0, 1.0);
444 let r_min = lj.r_min();
445 let (energy, force_mag) = compute_lj_force(r_min, &lj);
446 assert!(
447 (energy - (-lj.epsilon)).abs() < 1e-10,
448 "energy at r_min should be -epsilon={}, got {energy}",
449 -lj.epsilon
450 );
451 assert!(
452 force_mag.abs() < 1e-10,
453 "force at r_min should be 0, got {force_mag}"
454 );
455 }
456 #[test]
457 fn compute_all_lj_forces_newtons_third_law() {
458 let lj = LjPotential::new(1.0, 1.0);
459 let positions = vec![[0.0, 0.0, 0.0], [1.2, 0.0, 0.0], [0.6, 1.0, 0.0]];
460 let masses = vec![1.0; 3];
461 let forces = compute_all_lj_forces(&positions, &masses, &lj, 5.0);
462 assert_eq!(forces.len(), 3);
463 for k in 0..3 {
464 let total: f64 = forces.iter().map(|f| f[k]).sum();
465 assert!(
466 total.abs() < 1e-10,
467 "total force component {k} should be 0, got {total}"
468 );
469 }
470 }
471 #[test]
472 fn compute_all_lj_forces_repulsive_at_short_range() {
473 let sigma = 1.0;
474 let lj = LjPotential::new(1.0, sigma);
475 let positions = vec![[0.0, 0.0, 0.0], [0.9 * sigma, 0.0, 0.0]];
476 let masses = vec![1.0; 2];
477 let forces = compute_all_lj_forces(&positions, &masses, &lj, 5.0);
478 assert!(
479 forces[0][0] < 0.0,
480 "repulsive: force[0].x should be < 0, got {}",
481 forces[0][0]
482 );
483 assert!(
484 forces[1][0] > 0.0,
485 "repulsive: force[1].x should be > 0, got {}",
486 forces[1][0]
487 );
488 }
489 #[test]
490 fn pair_force_kernel_new() {
491 let lj = LjPotential::new(2.0, 0.5);
492 let kern = PairForceKernel::new(lj, 3.0, true);
493 assert!((kern.lj.epsilon - 2.0).abs() < 1e-15);
494 assert!((kern.lj.sigma - 0.5).abs() < 1e-15);
495 assert!((kern.cutoff - 3.0).abs() < 1e-15);
496 assert!(kern.shift);
497 let (e_at_cut, _f_at_cut) = kern.evaluate(kern.cutoff - 1e-10);
499 assert!(
500 e_at_cut.abs() < 0.1,
501 "energy near cutoff should be small when shifted"
502 );
503 }
504 #[test]
505 fn test_coulomb_potential() {
506 let cp = CoulombPotential::new(1.0);
507 let (e, f) = cp.compute(1.0, 1.0, 1.0);
508 assert!((e - 1.0).abs() < 1e-10);
509 assert!((f - 1.0).abs() < 1e-10);
510 let (e2, f2) = cp.compute(1.0, -1.0, 1.0);
511 assert!((e2 - (-1.0)).abs() < 1e-10);
512 assert!((f2 - (-1.0)).abs() < 1e-10);
513 }
514 #[test]
515 fn test_coulomb_force_function() {
516 let (e, f) = compute_coulomb_force(2.0, 1.0, 1.0, 1.0);
517 assert!((e - 0.5).abs() < 1e-10);
518 assert!((f - 0.25).abs() < 1e-10);
519 }
520 #[test]
521 fn test_coulomb_kernel_newton_iii() {
522 let positions = vec![0.0, 0.0, 0.0, 2.0, 0.0, 0.0];
523 let charges = vec![1.0, -1.0];
524 let params = vec![1.0, 10.0];
525 let mut outputs = vec![Vec::new(), Vec::new()];
526 CoulombKernel.execute(&[&positions, &charges, ¶ms], &mut outputs, 2);
527 for k in 0..3 {
528 let total = outputs[0][k] + outputs[0][3 + k];
529 assert!(
530 total.abs() < 1e-10,
531 "forces should sum to zero in dim {k}, got {total}"
532 );
533 }
534 assert!(
535 outputs[0][0] > 0.0,
536 "particle 0 should be attracted toward +x, got {}",
537 outputs[0][0]
538 );
539 }
540 #[test]
541 fn test_coulomb_kernel_same_charge_repulsive() {
542 let positions = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
543 let charges = vec![1.0, 1.0];
544 let params = vec![1.0, 10.0];
545 let mut outputs = vec![Vec::new(), Vec::new()];
546 CoulombKernel.execute(&[&positions, &charges, ¶ms], &mut outputs, 2);
547 assert!(
548 outputs[0][0] < 0.0,
549 "particle 0 should be repelled in -x, got {}",
550 outputs[0][0]
551 );
552 }
553 #[test]
554 fn test_coulomb_kernel_beyond_cutoff() {
555 let positions = vec![0.0, 0.0, 0.0, 5.0, 0.0, 0.0];
556 let charges = vec![1.0, 1.0];
557 let params = vec![1.0, 3.0];
558 let mut outputs = vec![Vec::new(), Vec::new()];
559 CoulombKernel.execute(&[&positions, &charges, ¶ms], &mut outputs, 2);
560 for &f in &outputs[0] {
561 assert!(
562 f.abs() < 1e-15,
563 "expected zero force beyond cutoff, got {f}"
564 );
565 }
566 }
567 #[test]
568 fn test_compute_all_coulomb_forces_newton_iii() {
569 let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
570 let charges = vec![1.0, -1.0, 0.5];
571 let forces = compute_all_coulomb_forces(&positions, &charges, 1.0, 10.0);
572 for k in 0..3 {
573 let total: f64 = forces.iter().map(|f| f[k]).sum();
574 assert!(
575 total.abs() < 1e-10,
576 "total Coulomb force component {k} should be 0, got {total}"
577 );
578 }
579 }
580 #[test]
581 fn test_lj_shifted_energy() {
582 let lj = LjPotential::new(1.0, 1.0);
583 let cutoff = 2.5;
584 let e_at_cutoff = compute_lj_shifted_energy(cutoff, &lj, cutoff);
585 assert!(
586 e_at_cutoff.abs() < 1e-10,
587 "shifted energy at cutoff should be 0, got {e_at_cutoff}"
588 );
589 let e_beyond = compute_lj_shifted_energy(3.0, &lj, cutoff);
590 assert!(e_beyond.abs() < 1e-15);
591 }
592 #[test]
593 fn test_lj_well_depth() {
594 let lj = LjPotential::new(2.5, 1.0);
595 assert!((lj.well_depth() - (-2.5)).abs() < 1e-15);
596 }
597 #[test]
598 fn test_pair_force_kernel_evaluate() {
599 let lj = LjPotential::new(1.0, 1.0);
600 let kern = PairForceKernel::new(lj, 3.0, false);
601 let (e, f) = kern.evaluate(1.0);
602 assert!(e.abs() < 1e-10);
603 assert!((f - 24.0).abs() < 1e-10);
604 let (e2, f2) = kern.evaluate(5.0);
605 assert!(e2.abs() < 1e-15);
606 assert!(f2.abs() < 1e-15);
607 }
608 #[test]
609 fn test_cutoff_scheme_hard() {
610 let scheme = CutoffScheme::Hard { cutoff: 2.5 };
611 assert!((scheme.cutoff_distance() - 2.5).abs() < 1e-15);
612 assert!((scheme.switch_value(1.0) - 1.0).abs() < 1e-15);
613 assert!((scheme.switch_value(3.0) - 0.0).abs() < 1e-15);
614 }
615 #[test]
616 fn test_cutoff_scheme_switched() {
617 let scheme = CutoffScheme::Switched {
618 r_switch: 2.0,
619 r_cutoff: 3.0,
620 };
621 assert!((scheme.cutoff_distance() - 3.0).abs() < 1e-15);
622 assert!((scheme.switch_value(1.5) - 1.0).abs() < 1e-15);
623 assert!((scheme.switch_value(3.5) - 0.0).abs() < 1e-15);
624 assert!((scheme.switch_value(2.5) - 0.5).abs() < 1e-10);
625 let v1 = scheme.switch_value(2.2);
626 let v2 = scheme.switch_value(2.8);
627 assert!(
628 v1 > v2,
629 "switch should decrease: v(2.2)={}, v(2.8)={}",
630 v1,
631 v2
632 );
633 }
634 #[test]
635 fn test_neighbor_list_brute_force() {
636 let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [5.0, 0.0, 0.0]];
637 let nlist = NeighborList::build_brute_force(&positions, 2.0, 0.5);
638 assert_eq!(nlist.num_particles(), 3);
639 assert!(nlist.neighbors[0].contains(&1));
640 assert!(nlist.neighbors[1].contains(&0));
641 assert!(!nlist.neighbors[0].contains(&2));
642 assert!(!nlist.neighbors[2].contains(&0));
643 }
644 #[test]
645 fn test_neighbor_list_num_pairs() {
646 let positions = vec![[0.0, 0.0, 0.0], [0.5, 0.0, 0.0], [1.0, 0.0, 0.0]];
647 let nlist = NeighborList::build_brute_force(&positions, 2.0, 0.0);
648 assert_eq!(nlist.num_pairs(), 3);
649 }
650 #[test]
651 fn test_neighbor_list_needs_rebuild() {
652 let nlist = NeighborList {
653 neighbors: vec![],
654 cutoff: 2.5,
655 skin: 0.4,
656 };
657 assert!(!nlist.needs_rebuild(0.1));
658 assert!(nlist.needs_rebuild(0.3));
659 }
660 #[test]
661 fn test_force_buffer_basic() {
662 let mut buf = ForceBuffer::new(3);
663 assert_eq!(buf.forces.len(), 3);
664 assert_eq!(buf.total_energy(), 0.0);
665 buf.add_pair(0, 1, [1.0, 0.0, 0.0], 2.0, [1.0, 0.0, 0.0]);
666 assert!((buf.forces[0][0] - 1.0).abs() < 1e-15);
667 assert!((buf.forces[1][0] - (-1.0)).abs() < 1e-15);
668 assert!((buf.energies[0] - 1.0).abs() < 1e-15);
669 assert!((buf.energies[1] - 1.0).abs() < 1e-15);
670 assert!((buf.total_energy() - 2.0).abs() < 1e-15);
671 }
672 #[test]
673 fn test_force_buffer_total_force_zero() {
674 let mut buf = ForceBuffer::new(3);
675 buf.add_pair(0, 1, [3.0, -1.0, 2.0], 1.0, [1.0, 0.0, 0.0]);
676 buf.add_pair(1, 2, [-1.0, 2.0, 0.5], 0.5, [0.0, 1.0, 0.0]);
677 let total = buf.total_force();
678 for (k, &tk) in total.iter().enumerate() {
679 assert!(tk.abs() < 1e-10, "total force[{k}] should be 0, got {}", tk);
680 }
681 }
682 #[test]
683 fn test_force_buffer_clear() {
684 let mut buf = ForceBuffer::new(2);
685 buf.add_pair(0, 1, [1.0, 2.0, 3.0], 5.0, [1.0, 0.0, 0.0]);
686 buf.clear();
687 assert!((buf.total_energy() - 0.0).abs() < 1e-15);
688 for f in &buf.forces {
689 for &c in f {
690 assert!(c.abs() < 1e-15);
691 }
692 }
693 }
694 #[test]
695 fn test_force_buffer_reduce() {
696 let mut main_buf = ForceBuffer::new(2);
697 main_buf.add_pair(0, 1, [1.0, 0.0, 0.0], 2.0, [1.0, 0.0, 0.0]);
698 let mut other = ForceBuffer::new(2);
699 other.add_pair(0, 1, [0.5, 0.0, 0.0], 1.0, [1.0, 0.0, 0.0]);
700 main_buf.reduce_from(&[other]);
701 assert!((main_buf.forces[0][0] - 1.5).abs() < 1e-15);
702 assert!((main_buf.total_energy() - 3.0).abs() < 1e-15);
703 }
704 #[test]
705 fn test_force_buffer_virial() {
706 let mut buf = ForceBuffer::new(2);
707 buf.add_pair(0, 1, [2.0, 0.0, 0.0], 1.0, [3.0, 0.0, 0.0]);
708 assert!((buf.virial[0][0] - 3.0).abs() < 1e-15);
709 assert!((buf.virial[1][0] - 3.0).abs() < 1e-15);
710 assert!((buf.total_virial() - 6.0).abs() < 1e-15);
711 }
712 #[test]
713 fn test_lj_forces_neighborlist() {
714 let lj = LjPotential::new(1.0, 1.0);
715 let positions = vec![[0.0, 0.0, 0.0], [1.2, 0.0, 0.0], [5.0, 0.0, 0.0]];
716 let nlist = NeighborList::build_brute_force(&positions, 2.5, 0.0);
717 let mut buf = ForceBuffer::new(3);
718 compute_lj_forces_neighborlist(&positions, &lj, &nlist, &mut buf);
719 let total = buf.total_force();
720 for (k, &tk) in total.iter().enumerate() {
721 assert!(tk.abs() < 1e-10, "total[{k}] = {}", tk);
722 }
723 for &fk in buf.forces[2].iter() {
724 assert!(fk.abs() < 1e-15);
725 }
726 }
727 #[test]
728 fn test_coulomb_forces_neighborlist() {
729 let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
730 let charges = vec![1.0, -1.0];
731 let nlist = NeighborList::build_brute_force(&positions, 5.0, 0.0);
732 let mut buf = ForceBuffer::new(2);
733 compute_coulomb_forces_neighborlist(&positions, &charges, 1.0, &nlist, &mut buf);
734 assert!(buf.forces[0][0] > 0.0, "should attract toward +x");
735 assert!(buf.forces[1][0] < 0.0, "should attract toward -x");
736 let total = buf.total_force();
737 assert!(total[0].abs() < 1e-10);
738 }
739 #[test]
740 fn test_lj_forces_neighborlist_matches_brute_force() {
741 let lj = LjPotential::new(1.0, 1.0);
742 let positions = vec![[0.0, 0.0, 0.0], [1.1, 0.0, 0.0], [0.5, 1.0, 0.0]];
743 let cutoff = 5.0;
744 let masses = vec![1.0; 3];
745 let forces_bf = compute_all_lj_forces(&positions, &masses, &lj, cutoff);
746 let nlist = NeighborList::build_brute_force(&positions, cutoff, 0.0);
747 let mut buf = ForceBuffer::new(3);
748 compute_lj_forces_neighborlist(&positions, &lj, &nlist, &mut buf);
749 for (i, (buf_row, bf_row)) in buf.forces.iter().zip(forces_bf.iter()).enumerate() {
750 for (k, (&buf_val, &bf_val)) in buf_row.iter().zip(bf_row.iter()).enumerate() {
751 assert!(
752 (buf_val - bf_val).abs() < 1e-10,
753 "mismatch at particle {i}, dim {k}: nlist={}, brute={}",
754 buf_val,
755 bf_val
756 );
757 }
758 }
759 }
760 #[test]
761 fn test_erfc_approx_at_zero() {
762 let result = erfc_approx(0.0);
763 assert!((result - 1.0).abs() < 1e-4, "erfc(0) ~ 1, got {result}");
764 }
765 #[test]
766 fn test_erfc_approx_large_arg() {
767 let result = erfc_approx(5.0);
768 assert!(result < 1e-10, "erfc(5) ~ 0, got {result}");
769 }
770 #[test]
771 fn test_ewald_self_energy() {
772 let charges = vec![1.0, -1.0];
773 let alpha = 0.5;
774 let se = ewald_self_energy(&charges, alpha);
775 let expected = -2.0 * alpha / std::f64::consts::PI.sqrt();
776 assert!((se - expected).abs() < 1e-10);
777 }
778 #[test]
779 fn test_ewald_real_space_kernel_newton_iii() {
780 let pos = vec![0.0, 0.0, 0.0, 2.0, 0.0, 0.0];
781 let charges = vec![1.0, -1.0];
782 let params = vec![0.5, 5.0, 20.0];
783 let mut outputs = vec![Vec::new(), Vec::new()];
784 EwaldRealSpaceKernel.execute(&[&pos, &charges, ¶ms], &mut outputs, 2);
785 assert_eq!(outputs[0].len(), 6);
786 let total_fx = outputs[0][0] + outputs[0][3];
787 assert!(
788 total_fx.abs() < 1e-10,
789 "Ewald Newton III violated: {total_fx}"
790 );
791 }
792 #[test]
793 fn test_ewald_params_accuracy() {
794 let p = EwaldParams::new(0.5, 6.0, 100.0, 20.0);
795 let acc = p.real_space_accuracy();
796 assert!(acc < 0.01, "erfc(3) should be small, got {acc}");
797 }
798 #[test]
799 fn test_pppm_grid_spacing() {
800 let grid = PppmGrid::new(32, 32, 32, 10.0, 2);
801 assert!((grid.dx() - 10.0 / 32.0).abs() < 1e-12);
802 assert_eq!(grid.total_points(), 32768);
803 }
804 #[test]
805 fn test_pppm_charge_assign_single_particle() {
806 let pos = vec![0.5, 0.5, 0.5];
807 let charges = vec![1.0];
808 let grid_params = vec![4.0, 4.0, 4.0, 4.0];
809 let mut outputs = vec![Vec::new()];
810 PppmChargeAssignKernel.execute(&[&pos, &charges, &grid_params], &mut outputs, 1);
811 assert_eq!(outputs[0].len(), 64);
812 let total: f64 = outputs[0].iter().sum();
813 assert!(
814 (total - 1.0).abs() < 1e-10,
815 "total charge on mesh = {total}"
816 );
817 }
818 #[test]
819 fn test_pppm_charge_assign_conservation() {
820 let pos = vec![1.0, 2.0, 3.0, 5.0, 5.0, 5.0];
821 let charges = vec![2.0, -1.5];
822 let grid_params = vec![8.0, 8.0, 8.0, 8.0];
823 let mut outputs = vec![Vec::new()];
824 PppmChargeAssignKernel.execute(&[&pos, &charges, &grid_params], &mut outputs, 2);
825 let total: f64 = outputs[0].iter().sum();
826 assert!(
827 (total - 0.5).abs() < 1e-10,
828 "net charge should be 0.5, got {total}"
829 );
830 }
831 #[test]
832 fn test_pppm_mesh_energy_estimate_positive() {
833 let mesh = vec![1.0, -1.0, 2.0, 0.5];
834 let e = pppm_mesh_energy_estimate(&mesh, 2, 2, 1);
835 assert!(e >= 0.0, "mesh energy should be non-negative");
836 }
837 #[test]
838 fn test_nlist_update_kernel_no_rebuild() {
839 let pos = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
840 let ref_pos = pos.clone();
841 let params = vec![2.5, 0.4];
842 let mut outputs = vec![Vec::new(), Vec::new()];
843 NlistUpdateKernel.execute(&[&pos, &ref_pos, ¶ms], &mut outputs, 2);
844 assert!(
845 (outputs[1][0] - 0.0).abs() < 1e-10,
846 "status should be Valid (0)"
847 );
848 }
849 #[test]
850 fn test_nlist_update_kernel_rebuild() {
851 let pos = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
852 let ref_pos = vec![0.0, 0.0, 0.0, 5.0, 0.0, 0.0];
853 let params = vec![2.5, 0.4];
854 let mut outputs = vec![Vec::new(), Vec::new()];
855 NlistUpdateKernel.execute(&[&pos, &ref_pos, ¶ms], &mut outputs, 2);
856 assert!(
857 (outputs[1][0] - 1.0).abs() < 1e-10,
858 "status should be Rebuilt (1)"
859 );
860 }
861 #[test]
862 fn test_nlist_update_pairs_found() {
863 let pos = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 5.0, 0.0, 0.0];
864 let ref_pos = vec![100.0; 9];
865 let params = vec![2.5, 0.4];
866 let mut outputs = vec![Vec::new(), Vec::new()];
867 NlistUpdateKernel.execute(&[&pos, &ref_pos, ¶ms], &mut outputs, 3);
868 let num_pairs = outputs[1][1] as usize;
869 assert_eq!(num_pairs, 1, "only 1 pair should be found, got {num_pairs}");
870 }
871 #[test]
872 fn test_pair_energy_accumulate_basic() {
873 let sigma = 1.0;
874 let pos = vec![0.0, 0.0, 0.0, sigma, 0.0, 0.0];
875 let pairs = vec![0.0, 1.0];
876 let params = vec![1.0, sigma, 5.0];
877 let mut outputs = vec![Vec::new(), Vec::new()];
878 PairEnergyAccumulateKernel.execute(&[&pos, &pairs, ¶ms], &mut outputs, 2);
879 let total = outputs[1][0];
880 assert!(
881 total.abs() < 1e-10,
882 "energy at r=sigma should be 0, got {total}"
883 );
884 }
885 #[test]
886 fn test_pair_energy_accumulate_split_equally() {
887 let pos = vec![0.0, 0.0, 0.0, 0.9, 0.0, 0.0];
888 let pairs = vec![0.0, 1.0];
889 let params = vec![1.0, 1.0, 5.0];
890 let mut outputs = vec![Vec::new(), Vec::new()];
891 PairEnergyAccumulateKernel.execute(&[&pos, &pairs, ¶ms], &mut outputs, 2);
892 let e0 = outputs[0][0];
893 let e1 = outputs[0][1];
894 assert!((e0 - e1).abs() < 1e-12, "energy should be split equally");
895 assert!(
896 outputs[1][0] > 0.0,
897 "total energy should be positive at r < r_min"
898 );
899 }
900 #[test]
901 fn test_pair_energy_beyond_cutoff_zero() {
902 let pos = vec![0.0, 0.0, 0.0, 10.0, 0.0, 0.0];
903 let pairs = vec![0.0, 1.0];
904 let params = vec![1.0, 1.0, 2.5];
905 let mut outputs = vec![Vec::new(), Vec::new()];
906 PairEnergyAccumulateKernel.execute(&[&pos, &pairs, ¶ms], &mut outputs, 2);
907 assert!(
908 outputs[1][0].abs() < 1e-15,
909 "energy beyond cutoff should be 0"
910 );
911 }
912 #[test]
913 fn test_virial_tensor_trace() {
914 let vt = VirialTensor {
915 components: [1.0, 2.0, 3.0, 0.5, 0.2, 0.1],
916 };
917 assert!((vt.trace() - 6.0).abs() < 1e-12);
918 }
919 #[test]
920 fn test_virial_tensor_pressure() {
921 let vt = VirialTensor {
922 components: [-3.0, -3.0, -3.0, 0.0, 0.0, 0.0],
923 };
924 let p = vt.pressure_contribution(1.0);
925 assert!((p - 3.0).abs() < 1e-12);
926 }
927 #[test]
928 fn test_virial_tensor_add() {
929 let a = VirialTensor {
930 components: [1.0, 2.0, 3.0, 0.0, 0.0, 0.0],
931 };
932 let b = VirialTensor {
933 components: [4.0, 5.0, 6.0, 0.0, 0.0, 0.0],
934 };
935 let c = a.add(&b);
936 assert!((c.components[0] - 5.0).abs() < 1e-12);
937 assert!((c.trace() - 21.0).abs() < 1e-12);
938 }
939 #[test]
940 fn test_compute_virial_stress_tensor_symmetric() {
941 let lj = LjPotential::new(1.0, 1.0);
942 let positions = vec![[0.0, 0.0, 0.0], [1.2, 0.0, 0.0]];
943 let vt = compute_virial_stress_tensor(&positions, &lj, 5.0);
944 assert!(vt.components[1].abs() < 1e-10, "Wyy should be 0");
945 assert!(vt.components[2].abs() < 1e-10, "Wzz should be 0");
946 }
947 #[test]
948 fn test_virial_kernel_newton_iii_check() {
949 let lj = LjPotential::new(1.0, 1.0);
950 let positions = [[0.0, 0.0, 0.0], [1.1, 0.0, 0.0], [0.5, 1.0, 0.0]];
951 let flat_pos: Vec<f64> = positions.iter().flat_map(|p| p.iter().copied()).collect();
952 let params = [lj.epsilon, lj.sigma, 5.0f64];
953 let mut outputs = vec![Vec::new()];
954 VirialStressTensorKernel.execute(&[&flat_pos, ¶ms], &mut outputs, 3);
955 assert_eq!(
956 outputs[0].len(),
957 6,
958 "virial tensor should have 6 components"
959 );
960 }
961 #[test]
962 fn test_bond_force_equilibrium_no_force() {
963 let r0 = 1.5_f64;
964 let positions = vec![[0.0, 0.0, 0.0], [r0, 0.0, 0.0]];
965 let bonds = vec![HarmonicBond::new(0, 1, 100.0, r0)];
966 let (forces, energy) = compute_bond_forces(&positions, &bonds);
967 assert_eq!(forces.len(), 2);
968 for (&f0d, &f1d) in forces[0].iter().zip(forces[1].iter()) {
969 assert!(f0d.abs() < 1e-10, "force at equilibrium should be 0");
970 assert!(f1d.abs() < 1e-10, "force at equilibrium should be 0");
971 }
972 assert!(
973 energy.abs() < 1e-10,
974 "energy at equilibrium should be 0, got {energy}"
975 );
976 }
977 #[test]
978 fn test_bond_force_compressed() {
979 let r0 = 2.0_f64;
980 let r = 1.0_f64;
981 let k = 50.0_f64;
982 let positions = vec![[0.0, 0.0, 0.0], [r, 0.0, 0.0]];
983 let bonds = vec![HarmonicBond::new(0, 1, k, r0)];
984 let (forces, energy) = compute_bond_forces(&positions, &bonds);
985 assert!(forces[0][0] < 0.0, "atom 0 should be pushed away from bond");
986 assert!(forces[1][0] > 0.0, "atom 1 should be pushed away from bond");
987 for (dim, (&f0d, &f1d)) in forces[0].iter().zip(forces[1].iter()).enumerate() {
988 assert!(
989 (f0d + f1d).abs() < 1e-10,
990 "Newton III violated at dim {dim}"
991 );
992 }
993 let expected_e = 0.5 * k * (r - r0).powi(2);
994 assert!(
995 (energy - expected_e).abs() < 1e-10,
996 "energy mismatch: {energy} vs {expected_e}"
997 );
998 }
999 #[test]
1000 fn test_bond_force_kernel_executes() {
1001 let positions_flat = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
1002 let bond_data = vec![0.0, 1.0, 100.0, 1.0];
1003 let mut outputs = vec![Vec::new(), Vec::new()];
1004 BondForceKernel.execute(&[&positions_flat, &bond_data], &mut outputs, 2);
1005 assert_eq!(outputs[0].len(), 6, "forces should have 6 components (3*2)");
1006 assert_eq!(outputs[1].len(), 1, "energies should have 1 component");
1007 for &f in &outputs[0] {
1008 assert!(f.abs() < 1e-10, "force at equilibrium should be 0, got {f}");
1009 }
1010 }
1011 #[test]
1012 fn test_bond_force_kernel_stretched() {
1013 let positions_flat = vec![0.0, 0.0, 0.0, 2.0, 0.0, 0.0];
1014 let bond_data = vec![0.0, 1.0, 10.0, 1.0];
1015 let mut outputs = vec![Vec::new(), Vec::new()];
1016 BondForceKernel.execute(&[&positions_flat, &bond_data], &mut outputs, 2);
1017 let fx0 = outputs[0][0];
1018 let fx1 = outputs[0][3];
1019 assert!(
1020 fx0 > 0.0,
1021 "atom 0 should be pulled toward atom 1 (positive x)"
1022 );
1023 assert!(
1024 fx1 < 0.0,
1025 "atom 1 should be pulled toward atom 0 (negative x)"
1026 );
1027 assert!(
1028 (fx0 + fx1).abs() < 1e-10,
1029 "Newton III: forces should cancel"
1030 );
1031 }
1032 #[test]
1033 fn test_angle_force_at_equilibrium() {
1034 let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1035 let angles = vec![HarmonicAngle::new(0, 1, 2, 50.0, std::f64::consts::PI)];
1036 let (forces, energy) = compute_angle_forces(&positions, &angles);
1037 assert_eq!(forces.len(), 3);
1038 assert!(
1039 energy.abs() < 1e-8,
1040 "energy at equilibrium angle should be ~0, got {energy}"
1041 );
1042 }
1043 #[test]
1044 fn test_angle_force_finite_at_90_degrees() {
1045 let positions = vec![[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1046 let theta0 = std::f64::consts::PI / 2.0;
1047 let angles = vec![HarmonicAngle::new(0, 1, 2, 100.0, theta0)];
1048 let (forces, energy) = compute_angle_forces(&positions, &angles);
1049 assert_eq!(forces.len(), 3);
1050 assert!(energy.is_finite(), "angle energy should be finite");
1051 for f in &forces {
1052 for &c in f {
1053 assert!(c.is_finite(), "angle force component should be finite: {c}");
1054 }
1055 }
1056 assert!(
1057 energy.abs() < 1e-8,
1058 "at equilibrium angle energy should be ~0, got {energy}"
1059 );
1060 }
1061 #[test]
1062 fn test_angle_force_kernel_executes() {
1063 let positions_flat = vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0];
1064 let theta0 = std::f64::consts::PI / 2.0;
1065 let angle_data = vec![0.0, 1.0, 2.0, 100.0, theta0];
1066 let mut outputs = vec![Vec::new(), Vec::new()];
1067 AngleForceKernel.execute(&[&positions_flat, &angle_data], &mut outputs, 3);
1068 assert_eq!(outputs[0].len(), 9, "forces should have 9 components (3*3)");
1069 assert_eq!(outputs[1].len(), 1, "energies should have 1 element");
1070 for &f in &outputs[0] {
1071 assert!(f.is_finite(), "angle force not finite: {f}");
1072 }
1073 }
1074 #[test]
1075 fn test_kinetic_temperature_basic() {
1076 let velocities = vec![[3.0, 0.0, 0.0]];
1077 let masses = vec![1.0];
1078 let kb = 1.0;
1079 let t = kinetic_temperature(&velocities, &masses, kb);
1080 assert!((t - 3.0).abs() < 1e-10, "expected T=3, got {t}");
1081 }
1082 #[test]
1083 fn test_kinetic_temperature_zero_velocity() {
1084 let velocities = vec![[0.0; 3]; 5];
1085 let masses = vec![1.0; 5];
1086 let t = kinetic_temperature(&velocities, &masses, 1.0);
1087 assert!(
1088 t.abs() < 1e-15,
1089 "temperature of zero-velocity system should be 0"
1090 );
1091 }
1092 #[test]
1093 fn test_temperature_scale_reaches_target() {
1094 let mut velocities = vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1095 let masses = vec![1.0, 1.0];
1096 let kb = 1.0;
1097 let t_before = kinetic_temperature(&velocities, &masses, kb);
1098 assert!(t_before > 0.0);
1099 let t_target = t_before * 4.0;
1100 temperature_scale(&mut velocities, &masses, t_target, kb);
1101 let t_after = kinetic_temperature(&velocities, &masses, kb);
1102 assert!(
1103 (t_after - t_target).abs() < 1e-8,
1104 "after scaling: expected T={t_target}, got T={t_after}"
1105 );
1106 }
1107 #[test]
1108 fn test_temperature_scale_kernel_rescales() {
1109 let vel_flat = vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
1110 let masses = vec![1.0, 1.0];
1111 let kb = 1.0;
1112 let t_target = 2.0 / 3.0;
1113 let params = vec![t_target, kb];
1114 let mut outputs = vec![Vec::new(), Vec::new()];
1115 TemperatureScaleKernel.execute(&[&vel_flat, &masses, ¶ms], &mut outputs, 2);
1116 assert_eq!(outputs[0].len(), 6);
1117 assert_eq!(outputs[1].len(), 2);
1118 let t_before = outputs[1][0];
1119 let t_after = outputs[1][1];
1120 assert!(t_before > 0.0, "t_before should be positive");
1121 assert!(
1122 (t_after - t_target).abs() < 1e-8,
1123 "t_after should be target {t_target}, got {t_after}"
1124 );
1125 }
1126 #[test]
1127 fn test_temperature_scale_kernel_outputs_finite() {
1128 let vel_flat = vec![2.0, 1.0, 0.5, 0.3, 0.7, 1.2, 0.1, 0.4, 0.9];
1129 let masses = vec![1.0, 2.0, 0.5];
1130 let params = vec![300.0, 1.0];
1131 let mut outputs = vec![Vec::new(), Vec::new()];
1132 TemperatureScaleKernel.execute(&[&vel_flat, &masses, ¶ms], &mut outputs, 3);
1133 for &v in &outputs[0] {
1134 assert!(v.is_finite(), "scaled velocity not finite: {v}");
1135 }
1136 for &t in &outputs[1] {
1137 assert!(t.is_finite(), "temperature not finite: {t}");
1138 }
1139 }
1140}