#[cfg(feature = "cuda")]
use cudarc::driver::CudaContext;
use lin_alg::f32::Vec3;
use crate::{
AtomDynamics, MdState, NATIVE_TO_KCAL, Solvent,
barostat::BAR_PER_KCAL_MOL_PER_ANSTROM_CUBED,
solvent::{H_MASS, MASS_WATER_MOL, O_MASS, WaterMol},
thermostat::GAS_CONST_R,
};
fn assert_close(got: f64, expected: f64, rel_tol: f64, label: &str) {
if expected.abs() < 1e-12 {
assert!(got.abs() < 1e-9, "{label}: expected ≈0, got {got:.6e}");
return;
}
let rel = ((got - expected) / expected).abs();
assert!(
rel < rel_tol,
"{label}: got {got:.6e}, expected {expected:.6e}, rel_err={rel:.4} (tol={rel_tol})"
);
}
#[test]
fn test_measure_kinetic_energy_atoms() {
let mut md = MdState::default();
md.atoms = vec![
AtomDynamics {
mass: 12.0,
vel: Vec3::new(3.0, 0.0, 0.0),
..Default::default()
},
AtomDynamics {
mass: 1.0,
vel: Vec3::new(0.0, 2.0, 0.0),
..Default::default()
},
];
let expected = 0.5 * (12.0 * 9.0 + 1.0 * 4.0) * NATIVE_TO_KCAL as f64;
let got = md.measure_kinetic_energy();
assert_close(got, expected, 1e-6, "KE from two atoms");
}
#[test]
fn test_measure_kinetic_energy_excludes_static() {
let mut md = MdState::default();
md.atoms = vec![
AtomDynamics {
mass: 12.0,
vel: Vec3::new(1.0, 0.0, 0.0),
static_: false,
..Default::default()
},
AtomDynamics {
mass: 12.0,
vel: Vec3::new(10.0, 10.0, 10.0),
static_: true,
..Default::default()
},
];
let expected = 0.5 * 12.0 * 1.0 * NATIVE_TO_KCAL as f64;
let got = md.measure_kinetic_energy();
assert_close(got, expected, 1e-6, "KE excludes static atom");
}
#[test]
fn test_measure_kinetic_energy_includes_water() {
let mut md = MdState::default();
let v_o = Vec3::new(2.0, 0.0, 0.0);
let v_h = Vec3::new(0.0, 1.0, 0.0);
md.water = vec![WaterMol {
o: AtomDynamics {
mass: O_MASS,
vel: v_o,
..Default::default()
},
h0: AtomDynamics {
mass: H_MASS,
vel: v_h,
..Default::default()
},
h1: AtomDynamics {
mass: H_MASS,
vel: v_h,
..Default::default()
},
m: AtomDynamics::default(),
}];
let expected = 0.5
* (O_MASS as f64 * 4.0 + H_MASS as f64 * 1.0 + H_MASS as f64 * 1.0)
* NATIVE_TO_KCAL as f64;
let got = md.measure_kinetic_energy();
assert_close(got, expected, 1e-5, "KE from water molecule");
}
#[test]
fn test_measure_kinetic_energy_translational_rigid_translation() {
let mut md = MdState::default();
let v_com = Vec3::new(3.0, 0.0, 0.0);
md.water = vec![WaterMol {
o: AtomDynamics {
mass: O_MASS,
vel: v_com,
..Default::default()
},
h0: AtomDynamics {
mass: H_MASS,
vel: v_com,
..Default::default()
},
h1: AtomDynamics {
mass: H_MASS,
vel: v_com,
..Default::default()
},
m: AtomDynamics::default(),
}];
let expected = 0.5 * MASS_WATER_MOL as f64 * 9.0 * NATIVE_TO_KCAL as f64;
let got = md.measure_kinetic_energy_translational();
assert_close(got, expected, 1e-5, "translational KE — rigid translation");
}
#[test]
fn test_measure_kinetic_energy_translational_excludes_rotation() {
let mut md = MdState::default();
let v_h = Vec3::new(0.0, 1.0, 0.0);
let v_o = Vec3::new(0.0, -2.0 * H_MASS / O_MASS, 0.0);
md.water = vec![WaterMol {
o: AtomDynamics {
mass: O_MASS,
vel: v_o,
..Default::default()
},
h0: AtomDynamics {
mass: H_MASS,
vel: v_h,
..Default::default()
},
h1: AtomDynamics {
mass: H_MASS,
vel: v_h,
..Default::default()
},
m: AtomDynamics::default(),
}];
let ke_full = md.measure_kinetic_energy();
let ke_trans = md.measure_kinetic_energy_translational();
assert!(ke_full > 1e-10, "full KE must be nonzero");
assert!(
ke_trans < 1e-5 * ke_full,
"translational KE should be ~0 for pure internal motion; \
got ke_trans={ke_trans:.4e}, ke_full={ke_full:.4e}"
);
}
#[test]
fn test_measure_temperature_roundtrip_300k() {
let mut md = MdState::default();
let dof = 9;
let t_target = 300.0;
md.kinetic_energy = 0.5 * dof as f64 * GAS_CONST_R * t_target;
md.thermo_dof = dof;
let got = md.measure_temperature();
assert_close(got, t_target, 1e-9, "round-trip temperature at 300 K");
}
#[test]
fn test_pressure_no_forces() {
use bio_files::{AtomGeneric, BondGeneric, BondType};
use na_seq::Element;
use crate::{
ComputationDevice, FfMolType, Integrator, MdConfig, MdOverrides, MolDynamics, SimBoxInit,
params::FfParamSet,
};
let param_set = FfParamSet::new_amber().unwrap();
let dev = ComputationDevice::Cpu;
let v0 = Vec3::new(2.0, 0.0, 0.0);
let v1 = Vec3::new(-1.0, 1.0, 0.0);
let v2 = Vec3::new(0.0, 0.0, 3.0);
let c = 30.0_f32;
let cfg = MdConfig {
integrator: Integrator::VerletVelocity { thermostat: None },
sim_box: SimBoxInit::Fixed((Vec3::new(0., 0., 0.), Vec3::new(60., 60., 60.))),
overrides: MdOverrides {
skip_solvent: true,
thermo_disabled: true,
baro_disabled: true,
bonded_disabled: true,
coulomb_disabled: true,
lj_disabled: true,
long_range_recip_disabled: true,
..Default::default()
},
max_init_relaxation_iters: None,
..Default::default()
};
let mol_a = MolDynamics {
ff_mol_type: FfMolType::SmallOrganic,
atoms: vec![
AtomGeneric {
serial_number: 1,
posit: Vec3::new(c - 2.5, c, c).into(),
force_field_type: Some("ca".to_string()),
element: Element::Carbon,
partial_charge: Some(0.0),
..Default::default()
},
AtomGeneric {
serial_number: 2,
posit: Vec3::new(c + 2.5, c, c).into(),
force_field_type: Some("ca".to_string()),
element: Element::Carbon,
partial_charge: Some(0.0),
..Default::default()
},
],
atom_init_velocities: Some(vec![v0, v1]),
bonds: vec![BondGeneric {
atom_0_sn: 1,
atom_1_sn: 2,
bond_type: BondType::Aromatic,
}],
..Default::default()
};
let mol_b = MolDynamics {
ff_mol_type: FfMolType::SmallOrganic,
atoms: vec![AtomGeneric {
serial_number: 3,
posit: Vec3::new(c, c - 8.0, c).into(),
force_field_type: Some("ca".to_string()),
element: Element::Carbon,
partial_charge: Some(0.0),
..Default::default()
}],
atom_init_velocities: Some(vec![v2]),
..Default::default()
};
let mut md = MdState::new(&dev, &cfg, &[mol_a, mol_b], ¶m_set).unwrap();
md.step(&dev, 0.001, None);
let snap = md.snapshots.last().expect("snapshot must exist");
let ke: f64 = md
.atoms
.iter()
.zip([v0, v1, v2])
.map(|(a, v)| 0.5 * a.mass as f64 * (v.x * v.x + v.y * v.y + v.z * v.z) as f64)
.sum::<f64>()
* NATIVE_TO_KCAL as f64;
let ext = md.cell.extent;
let vol = ext.x as f64 * ext.y as f64 * ext.z as f64;
let expected = 2.0 * ke / (3.0 * vol) * BAR_PER_KCAL_MOL_PER_ANSTROM_CUBED;
assert_close(snap.pressure as f64, expected, 1e-4, "no-force pressure");
}
#[test]
fn test_pressure_lj_virial() {
use bio_files::AtomGeneric;
use na_seq::Element;
use crate::{
ComputationDevice, FfMolType, Integrator, MdConfig, MdOverrides, MolDynamics, SimBoxInit,
forces::force_e_lj, params::FfParamSet,
};
let param_set = FfParamSet::new_amber().unwrap();
let dev = ComputationDevice::Cpu;
let r = 4.0_f32; let c = 30.0_f32;
let cfg = MdConfig {
integrator: Integrator::VerletVelocity { thermostat: None },
sim_box: SimBoxInit::Fixed((Vec3::new(0., 0., 0.), Vec3::new(60., 60., 60.))),
overrides: MdOverrides {
skip_solvent: true,
thermo_disabled: true,
baro_disabled: true,
bonded_disabled: true,
coulomb_disabled: true,
lj_disabled: false, long_range_recip_disabled: true,
..Default::default()
},
max_init_relaxation_iters: None,
..Default::default()
};
let mol_a = MolDynamics {
ff_mol_type: FfMolType::SmallOrganic,
atoms: vec![AtomGeneric {
serial_number: 1,
posit: Vec3::new(c - r / 2., c, c).into(),
force_field_type: Some("ca".to_string()),
element: Element::Carbon,
partial_charge: Some(0.0),
..Default::default()
}],
..Default::default()
};
let mol_b = MolDynamics {
ff_mol_type: FfMolType::SmallOrganic,
atoms: vec![AtomGeneric {
serial_number: 2,
posit: Vec3::new(c + r / 2., c, c).into(),
force_field_type: Some("ca".to_string()),
element: Element::Carbon,
partial_charge: Some(0.0),
..Default::default()
}],
..Default::default()
};
let mut md = MdState::new(&dev, &cfg, &[mol_a, mol_b], ¶m_set).unwrap();
md.step(&dev, 0.001, None);
let snap = md.snapshots.last().expect("snapshot must exist");
let a0 = &md.atoms[0];
let a1 = &md.atoms[1];
let sigma = 0.5 * (a0.lj_sigma + a1.lj_sigma);
let eps = (a0.lj_eps * a1.lj_eps).sqrt();
let dir = Vec3::new(1.0, 0.0, 0.0); let (f_on_tgt, _energy) = force_e_lj(dir, 1.0 / r, sigma, eps);
let virial_expected = (r * f_on_tgt.x) as f64;
let ext = md.cell.extent;
let vol = ext.x as f64 * ext.y as f64 * ext.z as f64;
let p_expected = virial_expected / (3.0 * vol) * BAR_PER_KCAL_MOL_PER_ANSTROM_CUBED;
println!(
"sigma={sigma:.4} eps={eps:.4} r={r} F_x={:.6} virial={virial_expected:.6} \
P_expected={p_expected:.4} bar P_snapshot={:.4} bar",
f_on_tgt.x, snap.pressure
);
assert_close(
snap.pressure as f64,
p_expected,
0.01, "LJ virial pressure",
);
}
#[test]
#[ignore]
fn generate_water_30a_template() {
use std::path::PathBuf;
use crate::{
ComputationDevice, MdConfig, MdOverrides, MdState, SimBoxInit, integrate::Integrator,
params::FfParamSet, solvent::init::WaterInitTemplate,
};
let param_set = FfParamSet::new_amber().unwrap();
let dev = ComputationDevice::Cpu;
let cfg = MdConfig {
integrator: Integrator::LangevinMiddle { gamma: 100.0 },
sim_box: SimBoxInit::Fixed((Vec3::new(0., 0., 0.), Vec3::new(30., 30., 30.))),
temp_target: 310.,
skip_water_pbc_filter: true,
overrides: MdOverrides {
baro_disabled: true,
..Default::default()
},
..Default::default()
};
let mut md = MdState::new(&dev, &cfg, &[], ¶m_set).unwrap();
println!("Phase 1: resolving close contacts (10 000 steps, gamma=100)...");
for step in 0..10_000_usize {
md.step(&dev, 0.002, None);
if step % 2_000 == 1_999 {
let t = md.measure_temperature();
let v = md.barostat.virial.to_kcal_mol();
println!(
" phase1 step {}: T={t:.1} K W_lr={:.1} kcal/mol",
step + 1,
v.nonbonded_long_range
);
}
}
println!("Phase 2: Debye relaxation (15 000 steps, gamma=10)...");
md.cfg.integrator = Integrator::LangevinMiddle { gamma: 10.0 };
for step in 0..15_000_usize {
md.step(&dev, 0.002, None);
if step % 3_000 == 2_999 {
let t = md.measure_temperature();
let v = md.barostat.virial.to_kcal_mol();
println!(
" phase2 step {}: T={t:.1} K W_lr={:.1} kcal/mol",
step + 1,
v.nonbonded_long_range
);
}
}
let out_path = PathBuf::from("src/param_data/water_30A.water_init_template");
let bounds = (md.cell.bounds_low, md.cell.bounds_high);
WaterInitTemplate::create_and_save(&md.water, bounds, &out_path).unwrap();
println!(
"Saved 30 Å water template ({} molecules) to {:?}",
md.water.len(),
out_path
);
}
#[test]
fn test_pressure_water_sim_1bar() {
use crate::{
ComputationDevice, MdConfig, MdOverrides, MdState, SimBoxInit, integrate::Integrator,
params::FfParamSet,
};
let template_path = "src/param_data/water_30A.water_init_template";
if !std::path::Path::new(template_path).exists() {
println!(
"Skipping test_pressure_water_sim_1bar: template not found at {template_path}.\n\
Run `cargo test generate_water_30a_template -- --ignored --nocapture` to generate it."
);
return;
}
let param_set = FfParamSet::new_amber().unwrap();
#[cfg(feature = "cuda")]
let dev = {
let stream = {
let ctx = CudaContext::new(0).unwrap();
ctx.default_stream()
};
ComputationDevice::Gpu(stream)
};
let dev = ComputationDevice::Cpu;
let cfg_auto_water_count = MdConfig {
integrator: Integrator::VerletVelocity { thermostat: None },
sim_box: SimBoxInit::Fixed((Vec3::new(0., 0., 0.), Vec3::new(30., 30., 30.))),
temp_target: 310.,
overrides: MdOverrides {
baro_disabled: true, ..Default::default()
},
max_init_relaxation_iters: None,
water_template_path: Some(template_path.to_string()),
..Default::default()
};
let cfg_fixed_water_count = MdConfig {
solvent: Solvent::WaterOpcSpecifyMolCount(898), ..cfg_auto_water_count.clone()
};
let pressure_expected = 1.; let n_steps = 100;
for (i, cfg) in [cfg_auto_water_count, cfg_fixed_water_count]
.iter()
.enumerate()
{
let mut md = MdState::new(&dev, &cfg, &[], ¶m_set).unwrap();
let num_water_mols = md.water.len();
if i == 0 {
println!("Simulating with automatic water count. {num_water_mols} mols");
} else {
println!("Simulating with fixed water count. {num_water_mols} mols");
}
for step in 0..n_steps {
md.step(&dev, 0.002, None);
if step == 0 {
let v = md.barostat.virial.to_kcal_mol();
let ke_trans = md.measure_kinetic_energy_translational();
let cell_vol = {
let e = md.cell.extent;
e.x as f64 * e.y as f64 * e.z as f64
};
println!(
" step 1 diag: KE_trans={ke_trans:.1} kcal/mol W_bonded={:.1} \
W_sr={:.1} W_lr={:.1} W_constr={:.3} V={cell_vol:.0} ų",
v.bonded, v.nonbonded_short_range, v.nonbonded_long_range, v.constraints
);
}
}
let avg_pressure: f64 =
md.snapshots.iter().map(|s| s.pressure as f64).sum::<f64>() / md.snapshots.len() as f64;
println!("avg pressure over {n_steps} steps: {avg_pressure:.1} bar");
assert!(
(avg_pressure - pressure_expected).abs() < 500.0,
"average pressure {avg_pressure:.1} bar is outside the expected range"
);
}
}