#![allow(clippy::excessive_precision)]
use std::{fs, io, path::Path, time::Instant};
use bincode::{Decode, Encode};
use bio_files::{gromacs, gromacs::gro::Gro};
use lin_alg::f32::{Quaternion, Vec3};
use crate::{
AtomDynamics, ComputationDevice, MdState, Solvent,
barostat::SimBox,
partial_charge_inference::{files::load_from_bytes_bincode, save},
sa_surface,
solvent::WaterMolOpc,
};
const WATER_DENSITY: f32 = 0.997;
const MASS_WATER: f32 = 18.015_28;
const N_A: f32 = 6.022_140_76e23;
const WATER_MOLS_PER_VOL: f32 = WATER_DENSITY * N_A / (MASS_WATER * 1.0e24);
const MIN_NONWATER_DIST: f32 = 1.7;
const MIN_NONWATER_DIST_SQ: f32 = MIN_NONWATER_DIST * MIN_NONWATER_DIST;
const MIN_WATER_O_O_DIST: f32 = 1.7;
pub(in crate::solvent) const MIN_WATER_O_O_DIST_SQ: f32 = MIN_WATER_O_O_DIST * MIN_WATER_O_O_DIST;
const PBC_MIN_WATER_O_O_DIST: f32 = 2.8;
const PBC_MIN_WATER_O_O_DIST_SQ: f32 = PBC_MIN_WATER_O_O_DIST * PBC_MIN_WATER_O_O_DIST;
const NUM_EQUILIBRATION_STEPS_WATER: usize = 200;
const NUM_EQUILIBRATION_STEPS_OTHER_SOLVENT: usize = 600;
const DT_EQUILIBRATION: f32 = 0.0005;
pub const WATER_TEMPLATE_60A: &[u8] =
include_bytes!("../../param_data/water_60A.water_init_template");
pub const WATER_TEMPLATE_TIP4: &str = include_str!("../../param_data/tip4p.gro");
pub const OCTANOL_WATER_TEMPLATE: &str =
include_str!("../../param_data/octanol_water_saturated.gro");
#[derive(Clone, Debug, PartialEq, Default, Decode, Encode)]
pub enum SolventTemplateType {
Water60A,
#[default]
Tip4Gromacs,
OctanolWithWater,
Custom(WaterInitTemplate),
}
impl SolventTemplateType {
pub fn get_template(&self) -> io::Result<WaterInitTemplate> {
match self {
Self::Water60A => load_from_bytes_bincode(WATER_TEMPLATE_60A),
Self::Tip4Gromacs => WaterInitTemplate::from_gro(WATER_TEMPLATE_TIP4),
Self::OctanolWithWater => Ok(Default::default()),
Self::Custom(t) => Ok(t.clone()),
}
}
}
#[derive(Clone, Debug, PartialEq, Default, Encode, Decode)]
pub struct WaterInitTemplate {
o_posits: Vec<Vec3>,
h0_posits: Vec<Vec3>,
h1_posits: Vec<Vec3>,
o_velocities: Vec<Vec3>,
h0_velocities: Vec<Vec3>,
h1_velocities: Vec<Vec3>,
cell: SimBox,
}
impl WaterInitTemplate {
pub fn load(path: &Path) -> io::Result<Self> {
let bytes = fs::read(path)?;
load_from_bytes_bincode(&bytes)
}
pub fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
load_from_bytes_bincode(bytes)
}
pub fn from_parts(
o_posits: Vec<Vec3>,
h0_posits: Vec<Vec3>,
h1_posits: Vec<Vec3>,
o_velocities: Vec<Vec3>,
h0_velocities: Vec<Vec3>,
h1_velocities: Vec<Vec3>,
cell: SimBox,
) -> io::Result<Self> {
let n = o_posits.len();
let lengths = [
h0_posits.len(),
h1_posits.len(),
o_velocities.len(),
h0_velocities.len(),
h1_velocities.len(),
];
if lengths.iter().any(|len| *len != n) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"WaterInitTemplate component lengths must match.",
));
}
Ok(Self {
o_posits,
h0_posits,
h1_posits,
o_velocities,
h0_velocities,
h1_velocities,
cell,
})
}
pub fn len(&self) -> usize {
self.o_posits.len()
}
pub fn is_empty(&self) -> bool {
self.o_posits.is_empty()
}
pub fn from_gro(gro_text: &str) -> io::Result<Self> {
const NM_TO_ANGSTROM: f32 = 10.;
let gro = Gro::new(gro_text)?;
let mut o_posits = Vec::new();
let mut h0_posits = Vec::new();
let mut h1_posits = Vec::new();
let mut o_velocities = Vec::new();
let mut h0_velocities = Vec::new();
let mut h1_velocities = Vec::new();
for atom in gro.atoms {
match atom.atom_type.as_ref() {
"OW" => {
let p: Vec3 = atom.posit.into();
o_posits.push(p * NM_TO_ANGSTROM);
let Some(vel) = &atom.velocity else {
return Err(io::Error::other("Missing velocity on tip4 water template"));
};
let v: Vec3 = (*vel).into();
o_velocities.push(v * NM_TO_ANGSTROM.into());
}
"HW1" => {
let p: Vec3 = atom.posit.into();
h0_posits.push(p * NM_TO_ANGSTROM);
let Some(vel) = &atom.velocity else {
return Err(io::Error::other("Missing velocity on tip4 water template"));
};
let v: Vec3 = (*vel).into();
h0_velocities.push(v * NM_TO_ANGSTROM);
}
"HW2" => {
let p: Vec3 = atom.posit.into();
h1_posits.push(p * NM_TO_ANGSTROM);
let Some(vel) = &atom.velocity else {
return Err(io::Error::other("Missing velocity on tip4 water template"));
};
let v: Vec3 = (*vel).into();
h1_velocities.push(v * NM_TO_ANGSTROM);
}
_ => (),
}
}
Ok(Self {
o_posits,
h0_posits,
h1_posits,
o_velocities,
h0_velocities,
h1_velocities,
cell: SimBox::new(
(-gro.box_vec * NM_TO_ANGSTROM as f64 / 2.).into(),
(gro.box_vec * NM_TO_ANGSTROM as f64 / 2.).into(),
), })
}
pub fn create_and_save(water: &[WaterMolOpc], cell: SimBox, path: &Path) -> io::Result<()> {
let n = water.len();
let mut o_posits = Vec::with_capacity(n);
let mut h0_posits = Vec::with_capacity(n);
let mut h1_posits = Vec::with_capacity(n);
let mut o_velocities = Vec::with_capacity(n);
let mut h0_velocities = Vec::with_capacity(n);
let mut h1_velocities = Vec::with_capacity(n);
let water = {
let ctr = cell.center();
let mut w = water.to_vec();
w.sort_by(|a, b| {
let da = (a.o.posit - ctr).magnitude_squared();
let db = (b.o.posit - ctr).magnitude_squared();
da.total_cmp(&db)
});
w
};
for mol in water {
o_posits.push(mol.o.posit);
h0_posits.push(mol.h0.posit);
h1_posits.push(mol.h1.posit);
o_velocities.push(mol.o.vel);
h0_velocities.push(mol.h0.vel);
h1_velocities.push(mol.h1.vel);
}
let result = Self {
o_posits,
h0_posits,
h1_posits,
o_velocities,
h0_velocities,
h1_velocities,
cell,
};
save(path, &result)
}
pub fn from_water_mols(water: &[WaterMolOpc], cell: SimBox) -> io::Result<Self> {
let n = water.len();
let mut o_posits = Vec::with_capacity(n);
let mut h0_posits = Vec::with_capacity(n);
let mut h1_posits = Vec::with_capacity(n);
let mut o_velocities = Vec::with_capacity(n);
let mut h0_velocities = Vec::with_capacity(n);
let mut h1_velocities = Vec::with_capacity(n);
for mol in water {
o_posits.push(mol.o.posit);
h0_posits.push(mol.h0.posit);
h1_posits.push(mol.h1.posit);
o_velocities.push(mol.o.vel);
h0_velocities.push(mol.h0.vel);
h1_velocities.push(mol.h1.vel);
}
Self::from_parts(
o_posits,
h0_posits,
h1_posits,
o_velocities,
h0_velocities,
h1_velocities,
cell,
)
}
pub fn to_gromacs(&self) -> gromacs::solvate::WaterInitTemplate {
gromacs::solvate::WaterInitTemplate {
o_posits: self.o_posits.clone(),
h0_posits: self.h0_posits.clone(),
h1_posits: self.h1_posits.clone(),
o_velocities: self.o_velocities.clone(),
h0_velocities: self.h0_velocities.clone(),
h1_velocities: self.h1_velocities.clone(),
bounds: (self.cell.bounds_low, self.cell.bounds_high),
}
}
}
pub(in crate::solvent) fn n_water_mols(cell: &SimBox, solute_atoms: &[AtomDynamics]) -> usize {
let cell_volume = cell.volume();
let mol_volume = sa_surface::vol_take_up_by_atoms(solute_atoms);
let free_vol = cell_volume - mol_volume;
let dims = format!(
"{}:.2 x {:.2} x {:.2}",
(cell.bounds_high.x - cell.bounds_low.x).abs(),
(cell.bounds_high.y - cell.bounds_low.y).abs(),
(cell.bounds_high.z - cell.bounds_low.z).abs()
);
println!(
"Solvent-free vol: {:.2} Cell vol: {:.2} (ų / 1,000). Dims: {dims} Å",
free_vol / 1_000.,
cell_volume / 1_000.
);
(WATER_MOLS_PER_VOL * free_vol).round() as usize
}
pub fn water_mols_from_template(
cell: &SimBox,
solute: &[AtomDynamics],
specify_num_water: Option<usize>,
template_type: &SolventTemplateType,
skip_pbc_filter: bool,
) -> Vec<WaterMolOpc> {
match water_mols_from_template_in_region(
cell,
cell,
solute,
specify_num_water,
template_type,
skip_pbc_filter,
) {
Ok(water) => water,
Err(e) => {
eprintln!("\nError initializing water: {e}");
Vec::new()
}
}
}
fn validate_positive_cell(label: &str, cell: &SimBox) -> io::Result<()> {
if cell.extent.x.is_finite()
&& cell.extent.y.is_finite()
&& cell.extent.z.is_finite()
&& cell.extent.x > 0.0
&& cell.extent.y > 0.0
&& cell.extent.z > 0.0
{
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"{label} must have positive finite dimensions; got low={:?}, high={:?}.",
cell.bounds_low, cell.bounds_high
),
))
}
}
pub fn water_mols_from_template_in_region(
cell: &SimBox,
region: &SimBox,
solute: &[AtomDynamics],
specify_num_water: Option<usize>,
template_type: &SolventTemplateType,
skip_pbc_filter: bool,
) -> io::Result<Vec<WaterMolOpc>> {
water_mols_from_template_in_region_avoiding(
cell,
region,
solute,
&[],
specify_num_water,
template_type,
skip_pbc_filter,
)
}
#[derive(Clone, Copy)]
struct WaterTemplateCandidate {
tile: (i32, i32, i32),
o_posit: Vec3,
h0_posit: Vec3,
h1_posit: Vec3,
o_velocity: Vec3,
h0_velocity: Vec3,
h1_velocity: Vec3,
}
fn candidate_fits_region(
candidate: &WaterTemplateCandidate,
cell: &SimBox,
region: &SimBox,
) -> bool {
[candidate.o_posit, candidate.h0_posit, candidate.h1_posit]
.into_iter()
.all(|posit| region.contains(posit) && cell.contains(posit))
}
fn spatially_interleave_candidates(
candidates: Vec<WaterTemplateCandidate>,
region: &SimBox,
desired_count: usize,
) -> Vec<WaterTemplateCandidate> {
let bins_per_axis = (desired_count as f32).cbrt().ceil() as usize;
let mut bins = vec![Vec::new(); bins_per_axis.pow(3)];
let bin_coord = |value: f32, low: f32, extent: f32| {
(((value - low) / extent * bins_per_axis as f32).floor() as usize).min(bins_per_axis - 1)
};
for candidate in candidates {
let x = bin_coord(candidate.o_posit.x, region.bounds_low.x, region.extent.x);
let y = bin_coord(candidate.o_posit.y, region.bounds_low.y, region.extent.y);
let z = bin_coord(candidate.o_posit.z, region.bounds_low.z, region.extent.z);
bins[x + bins_per_axis * (y + bins_per_axis * z)].push(candidate);
}
let max_bin_len = bins.iter().map(Vec::len).max().unwrap_or(0);
let mut bin_order: Vec<_> = (0..bins.len()).collect();
bin_order.sort_unstable_by_key(|index| (*index as u32).wrapping_mul(0x9E37_79B9));
let mut result = Vec::new();
for round in 0..max_bin_len {
for &bin_i in &bin_order {
let bin = &bins[bin_i];
if let Some(candidate) = bin.get(round) {
result.push(*candidate);
}
}
}
result
}
pub(crate) fn water_mols_from_template_in_region_avoiding(
cell: &SimBox,
region: &SimBox,
solute: &[AtomDynamics],
prior_water: &[WaterMolOpc],
specify_num_water: Option<usize>,
template_type: &SolventTemplateType,
skip_pbc_filter: bool,
) -> io::Result<Vec<WaterMolOpc>> {
validate_positive_cell("Simulation cell", cell)?;
validate_positive_cell("Water placement region", region)?;
if !cell.contains(region.bounds_low) || !cell.contains(region.bounds_high) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Water placement region must be fully inside the simulation cell.",
));
}
println!("Initializing solvent molecules...");
let start = Instant::now();
let template = template_type.get_template()?;
validate_positive_cell("Water template cell", &template.cell)?;
let solute_for_count: Vec<_> = if cell == region {
Vec::new()
} else {
solute
.iter()
.filter(|atom| region.contains(atom.posit))
.cloned()
.collect()
};
let n_mols = specify_num_water.unwrap_or_else(|| {
if cell == region {
n_water_mols(region, solute)
} else {
n_water_mols(region, &solute_for_count)
}
});
let mut result = Vec::with_capacity(n_mols);
if n_mols == 0 {
println!("Complete in {} ms.", start.elapsed().as_millis());
return Ok(result);
}
let solute_posits: Vec<_> = solute.iter().map(|a| a.posit).collect();
let template_size = template.cell.extent;
let template_ctr = template.cell.center();
let region_ctr = region.center();
let base_offset = region_ctr - template_ctr;
let region_size = region.extent;
let half_x = (region_size.x / (2.0 * template_size.x)).ceil() as i32 + 1;
let half_y = (region_size.y / (2.0 * template_size.y)).ceil() as i32 + 1;
let half_z = (region_size.z / (2.0 * template_size.z)).ceil() as i32 + 1;
let mut loops_used = 0;
let mut placed_tiles: Vec<(i32, i32, i32)> = Vec::with_capacity(n_mols);
let make_candidate =
|tile: (i32, i32, i32), tile_offset: Vec3, i: usize| WaterTemplateCandidate {
tile,
o_posit: template.o_posits[i] + tile_offset,
h0_posit: template.h0_posits[i] + tile_offset,
h1_posit: template.h1_posits[i] + tile_offset,
o_velocity: template.o_velocities[i],
h0_velocity: template.h0_velocities[i],
h1_velocity: template.h1_velocities[i],
};
let mut place_candidate = |candidate: WaterTemplateCandidate| {
if !candidate_fits_region(&candidate, cell, region) {
return false;
}
for &atom_p in &solute_posits {
if cell
.min_image(atom_p - candidate.o_posit)
.magnitude_squared()
< MIN_NONWATER_DIST_SQ
{
return false;
}
}
for w in prior_water {
let diff = cell.min_image(w.o.posit - candidate.o_posit);
if diff.magnitude_squared() < PBC_MIN_WATER_O_O_DIST_SQ {
return false;
}
}
for (j, w) in result.iter().enumerate() {
let diff = w.o.posit - candidate.o_posit;
let direct_sq = diff.magnitude_squared();
if direct_sq < MIN_WATER_O_O_DIST_SQ {
return false;
}
let min_image_sq = cell.min_image(diff).magnitude_squared();
if min_image_sq < MIN_WATER_O_O_DIST_SQ {
return false;
}
if !skip_pbc_filter && placed_tiles[j] == candidate.tile {
if min_image_sq < PBC_MIN_WATER_O_O_DIST_SQ && min_image_sq < direct_sq {
return false;
}
}
}
let mut mol = WaterMolOpc::new(
Vec3::new_zero(),
Vec3::new_zero(),
Quaternion::new_identity(),
);
mol.o.posit = candidate.o_posit;
mol.h0.posit = candidate.h0_posit;
mol.h1.posit = candidate.h1_posit;
mol.o.vel = candidate.o_velocity;
mol.h0.vel = candidate.h0_velocity;
mol.h1.vel = candidate.h1_velocity;
mol.update_virtual_site();
result.push(mol);
placed_tiles.push(candidate.tile);
result.len() == n_mols
};
if specify_num_water.is_some() {
let mut candidates = Vec::new();
for ix in -half_x..=half_x {
for iy in -half_y..=half_y {
for iz in -half_z..=half_z {
let tile = (ix, iy, iz);
let tile_offset = base_offset
+ Vec3::new(
ix as f32 * template_size.x,
iy as f32 * template_size.y,
iz as f32 * template_size.z,
);
for i in 0..template.o_posits.len() {
loops_used += 1;
let candidate = make_candidate(tile, tile_offset, i);
if candidate_fits_region(&candidate, cell, region) {
candidates.push(candidate);
}
}
}
}
}
for candidate in spatially_interleave_candidates(candidates, region, n_mols) {
if place_candidate(candidate) {
break;
}
}
} else {
'tiles: for ix in -half_x..=half_x {
for iy in -half_y..=half_y {
for iz in -half_z..=half_z {
let tile = (ix, iy, iz);
let tile_offset = base_offset
+ Vec3::new(
ix as f32 * template_size.x,
iy as f32 * template_size.y,
iz as f32 * template_size.z,
);
for i in 0..template.o_posits.len() {
loops_used += 1;
if place_candidate(make_candidate(tile, tile_offset, i)) {
break 'tiles;
}
}
}
}
}
}
let elapsed = start.elapsed().as_millis();
println!(
"Added {} / {n_mols} solvent mols in {elapsed} ms. Used {loops_used} loops",
result.len()
);
Ok(result)
}
impl MdState {
fn mark_solute_static_for_init_relaxation(&mut self) -> Vec<bool> {
let mut static_state = Vec::with_capacity(self.atoms.len());
for (i, atom) in self.atoms.iter_mut().enumerate() {
static_state.push(atom.static_);
if i < self.solute_atom_count {
atom.static_ = true;
}
}
static_state
}
fn restore_static_state(&mut self, static_state: &[bool]) {
for (atom, &was_static) in self.atoms.iter_mut().zip(static_state.iter()) {
atom.static_ = was_static;
}
}
pub fn md_on_solute_only(&mut self, dev: &ComputationDevice) {
println!("Initializing solvent structure prior to production MD...");
let start = Instant::now();
self.solvent_only_sim_at_init = true;
let thermo_dof_prev = self.thermo_dof;
let static_state = self.mark_solute_static_for_init_relaxation();
self.thermo_dof = self.dof_for_thermo();
let steps = match self.cfg.solvent {
Solvent::None => 0,
Solvent::WaterOpc
| Solvent::WaterOpcSpecifyMolCount(_)
| Solvent::WaterOpcCustomRegions(_) => NUM_EQUILIBRATION_STEPS_WATER,
Solvent::OctanolWithWater | Solvent::Custom(_) => NUM_EQUILIBRATION_STEPS_OTHER_SOLVENT,
};
for _ in 0..steps {
self.step(dev, DT_EQUILIBRATION, None);
}
self.restore_static_state(&static_state);
self.solvent_only_sim_at_init = false;
self.thermo_dof = thermo_dof_prev;
self.step_count = 0;
let elapsed = start.elapsed().as_millis();
println!("Solvent initialization MD complete in {elapsed} ms");
}
}