use std::{fmt::Write as _, io, path::Path};
use lin_alg::f32::Vec3;
use crate::gromacs::{
CUSTOM_SOLVENT_GRO, GMX_BUILTIN_TIP4P_WATER, MoleculeInput,
gro::{AtomGro, Gro},
run_gmx,
};
pub(in crate::gromacs) const CUSTOM_REGIONS_SOLVENT_GRO: &str = "water_custom_regions.gro";
const CUSTOM_REGIONS_FULL_BOX_GRO: &str = "water_custom_regions_full_box.gro";
const REGION_BOUNDS_TOLERANCE_NM: f64 = 1.0e-5;
#[derive(Clone, Debug, Default)]
pub enum Solvent {
#[default]
WaterOpc,
WaterOpcCustomRegions(Vec<(Vec3, Vec3)>), Custom(CustomSolventTemplate),
}
impl Solvent {
pub(in crate::gromacs) fn gro_filename(&self) -> &'static str {
match self {
Self::WaterOpc => GMX_BUILTIN_TIP4P_WATER,
Self::WaterOpcCustomRegions(_) => GMX_BUILTIN_TIP4P_WATER,
Self::Custom(_) => CUSTOM_SOLVENT_GRO,
}
}
pub(in crate::gromacs) fn prepare_gro(
&self,
dir: &Path,
box_nm: Option<(f64, f64, f64)>,
) -> io::Result<Option<&'static str>> {
match self {
Self::WaterOpcCustomRegions(regions) => {
make_water_opc_custom_regions_gro(dir, box_nm, regions)
}
_ => Ok(Some(self.gro_filename())),
}
}
}
fn make_water_opc_custom_regions_gro(
dir: &Path,
box_nm: Option<(f64, f64, f64)>,
regions: &[(Vec3, Vec3)],
) -> io::Result<Option<&'static str>> {
if regions.is_empty() {
return Ok(None);
}
let box_vec = validate_regions(box_nm, regions)?;
let x = box_vec.x.to_string();
let y = box_vec.y.to_string();
let z = box_vec.z.to_string();
run_gmx(
dir,
&[
"solvate",
"-cs",
GMX_BUILTIN_TIP4P_WATER,
"-box",
&x,
&y,
&z,
"-o",
CUSTOM_REGIONS_FULL_BOX_GRO,
],
)?;
let full_box = Gro::load(&dir.join(CUSTOM_REGIONS_FULL_BOX_GRO))?;
let regional_box = retain_waters_in_regions(full_box, regions);
regional_box.save(&dir.join(CUSTOM_REGIONS_SOLVENT_GRO))?;
Ok(Some(CUSTOM_REGIONS_SOLVENT_GRO))
}
fn validate_regions(
box_nm: Option<(f64, f64, f64)>,
regions: &[(Vec3, Vec3)],
) -> io::Result<lin_alg::f64::Vec3> {
let Some((x, y, z)) = box_nm else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"WaterOpcCustomRegions requires box_nm.",
));
};
let box_vec = lin_alg::f64::Vec3::new(x, y, z);
if !is_positive_finite(box_vec) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Simulation box must have positive finite dimensions; got {box_vec:?}."),
));
}
for (i, &(low, high)) in regions.iter().enumerate() {
if !is_finite(low)
|| !is_finite(high)
|| (low.x as f64) < -REGION_BOUNDS_TOLERANCE_NM
|| (low.y as f64) < -REGION_BOUNDS_TOLERANCE_NM
|| (low.z as f64) < -REGION_BOUNDS_TOLERANCE_NM
|| high.x <= low.x
|| high.y <= low.y
|| high.z <= low.z
|| (high.x as f64) > box_vec.x + REGION_BOUNDS_TOLERANCE_NM
|| (high.y as f64) > box_vec.y + REGION_BOUNDS_TOLERANCE_NM
|| (high.z as f64) > box_vec.z + REGION_BOUNDS_TOLERANCE_NM
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"Water OPC custom region {i} must have positive finite dimensions and fit \
inside box {box_vec:?}; got low={low:?}, high={high:?}."
),
));
}
}
Ok(box_vec)
}
fn retain_waters_in_regions(full_box: Gro, regions: &[(Vec3, Vec3)]) -> Gro {
let mut atoms = Vec::new();
let mut start = 0;
while start < full_box.atoms.len() {
let first = &full_box.atoms[start];
let mut end = start + 1;
while end < full_box.atoms.len()
&& full_box.atoms[end].mol_id == first.mol_id
&& full_box.atoms[end].mol_name == first.mol_name
{
end += 1;
}
let water = &full_box.atoms[start..end];
if regions
.iter()
.any(|&(low, high)| water.iter().all(|atom| region_contains(low, high, atom)))
{
atoms.extend_from_slice(water);
}
start = end;
}
Gro {
atoms,
head_text: "OPC water custom regions generated by Bio Files".to_owned(),
box_vec: full_box.box_vec,
}
}
fn is_positive_finite(v: lin_alg::f64::Vec3) -> bool {
v.x.is_finite() && v.y.is_finite() && v.z.is_finite() && v.x > 0.0 && v.y > 0.0 && v.z > 0.0
}
fn is_finite(v: Vec3) -> bool {
v.x.is_finite() && v.y.is_finite() && v.z.is_finite()
}
fn region_contains(low: Vec3, high: Vec3, atom: &AtomGro) -> bool {
atom.posit.x >= low.x as f64
&& atom.posit.y >= low.y as f64
&& atom.posit.z >= low.z as f64
&& atom.posit.x <= high.x as f64
&& atom.posit.y <= high.y as f64
&& atom.posit.z <= high.z as f64
}
#[derive(Clone, Debug, PartialEq)]
pub struct WaterInitTemplate {
pub o_posits: Vec<Vec3>,
pub h0_posits: Vec<Vec3>,
pub h1_posits: Vec<Vec3>,
pub o_velocities: Vec<Vec3>,
pub h0_velocities: Vec<Vec3>,
pub h1_velocities: Vec<Vec3>,
pub bounds: (Vec3, Vec3),
}
impl WaterInitTemplate {
pub fn to_gro(&self) -> String {
const VS_A: f32 = 0.147803;
let n_mols = self.o_posits.len();
let n_atoms = n_mols * 4;
let mut s = String::from("OPC water template — generated by Bio Files\n");
let _ = writeln!(s, "{}", n_atoms);
let mut atom_serial = 1usize;
for i in 0..n_mols {
let res = (i + 1) % 100_000;
let ow = self.o_posits[i];
let hw1 = self.h0_posits[i];
let hw2 = self.h1_posits[i];
let mw = ow + (hw1 - ow) * VS_A + (hw2 - ow) * VS_A;
let v_ow = self.o_velocities[i];
let v_hw1 = self.h0_velocities[i];
let v_hw2 = self.h1_velocities[i];
for (name, pos, vel) in [
("OW", ow, v_ow),
("HW1", hw1, v_hw1),
("HW2", hw2, v_hw2),
("MW", mw, Vec3::new_zero()), ] {
let _ = writeln!(
s,
"{:>5}{:<5}{:>5}{:>5}{:>8.3}{:>8.3}{:>8.3}{:>8.4}{:>8.4}{:>8.4}",
res,
"SOL",
name,
atom_serial % 100_000,
pos.x / 10.0, pos.y / 10.0,
pos.z / 10.0,
vel.x / 10.0, vel.y / 10.0,
vel.z / 10.0,
);
atom_serial += 1;
}
}
let lo = self.bounds.0;
let hi = self.bounds.1;
let _ = writeln!(
s,
"{:>10.5}{:>10.5}{:>10.5}",
(hi.x - lo.x).abs() / 10.0,
(hi.y - lo.y).abs() / 10.0,
(hi.z - lo.z).abs() / 10.0,
);
s
}
}
#[derive(Clone, Debug)]
pub struct CustomSolventTemplate {
pub gro_text: String,
pub topology_molecules: Vec<MoleculeInput>,
pub include_opc_water: bool,
}
impl CustomSolventTemplate {
pub fn from_water(template: WaterInitTemplate) -> Self {
Self {
gro_text: template.to_gro(),
topology_molecules: Vec::new(),
include_opc_water: false,
}
}
}