pub mod gro;
pub mod mdp;
pub mod output;
pub mod solvate;
pub mod topology;
use std::{
fmt::Write as _,
fs,
fs::File,
io::{self, ErrorKind, Write},
path::Path,
process::{Command, Stdio},
};
pub use mdp::{MdpParams, OutputControl};
pub use output::{GromacsFrame, GromacsOutput, OutputEnergy};
use solvate::WaterModel;
pub use topology::MoleculeTopology;
use crate::{
AtomGeneric, BondGeneric, FrameSlice, gromacs::output::read_trr, md_params::ForceFieldParams,
};
const TEMP_DIR: &str = "gromacs_out";
pub(in crate::gromacs) const GRO_NAME: &str = "conf.gro";
pub(in crate::gromacs) const TOP_NAME: &str = "topo.top";
pub(in crate::gromacs) const MDP_NAME: &str = "md.mdp";
pub(in crate::gromacs) const SOLVENT_GRO_NAME: &str = "opc.gro";
pub(in crate::gromacs) const SOLVATED_NAME: &str = "solvated.gro";
pub(in crate::gromacs) const IONIZED_NAME: &str = "ionized.gro";
pub(in crate::gromacs) const TRR_NAME: &str = "traj.trr";
pub(in crate::gromacs) const XTC_NAME: &str = "traj.xtc";
pub(in crate::gromacs) const GRO_OUT_NAME: &str = "confout.gro";
pub(in crate::gromacs) const ENERGY_OUT_NAME: &str = "energy.edr";
const LOG_NAME: &str = "md.log";
#[derive(Clone, Debug)]
pub struct MoleculeInput {
pub name: String,
pub atoms: Vec<AtomGeneric>,
pub bonds: Vec<BondGeneric>,
pub ff_params: Option<ForceFieldParams>,
pub count: usize,
}
#[derive(Clone, Debug)]
pub struct GromacsInput {
pub mdp: MdpParams,
pub molecules: Vec<MoleculeInput>,
pub box_nm: Option<(f64, f64, f64)>,
pub ff_global: Option<ForceFieldParams>,
pub water_model: Option<WaterModel>,
pub minimize_energy: bool,
}
impl Default for GromacsInput {
fn default() -> Self {
Self {
mdp: MdpParams::default(),
molecules: Vec::new(),
box_nm: None,
ff_global: None,
water_model: None,
minimize_energy: true,
}
}
}
impl GromacsInput {
pub fn make_mdp(&self) -> String {
self.mdp.to_mdp_str()
}
pub fn make_gro(&self) -> String {
let total_atoms: usize = self.molecules.iter().map(|m| m.atoms.len() * m.count).sum();
let (shift_x, shift_y, shift_z) = if let Some((bx, by, bz)) = self.box_nm {
let (mut sx, mut sy, mut sz) = (0.0f64, 0.0f64, 0.0f64);
let mut n = 0usize;
for mol in &self.molecules {
for _ in 0..mol.count {
for atom in &mol.atoms {
sx += atom.posit.x;
sy += atom.posit.y;
sz += atom.posit.z;
n += 1;
}
}
}
if n > 0 {
let cx = sx / n as f64 / 10.0;
let cy = sy / n as f64 / 10.0;
let cz = sz / n as f64 / 10.0;
(bx / 2.0 - cx, by / 2.0 - cy, bz / 2.0 - cz)
} else {
(0.0, 0.0, 0.0)
}
} else {
(0.0, 0.0, 0.0)
};
let mut s = String::from("Generated by Bio Files\n");
let _ = writeln!(s, "{}", total_atoms);
let mut atom_serial = 1;
let mut res_serial = 1;
for mol in &self.molecules {
for _copy in 0..mol.count {
for atom in &mol.atoms {
let atom_name = if atom.hetero {
atom.force_field_type
.clone()
.or_else(|| atom.type_in_res.as_ref().map(|t| t.to_string()))
.or_else(|| atom.type_in_res_general.clone())
.unwrap_or_else(|| {
format!("{}{}", atom.element.to_letter(), atom_serial)
})
} else {
atom.type_in_res
.as_ref()
.map(|t| t.to_string())
.or_else(|| atom.force_field_type.clone())
.or_else(|| atom.type_in_res_general.clone())
.unwrap_or_else(|| {
format!("{}{}", atom.element.to_letter(), atom_serial)
})
};
let x_nm = atom.posit.x / 10.0 + shift_x;
let y_nm = atom.posit.y / 10.0 + shift_y;
let z_nm = atom.posit.z / 10.0 + shift_z;
let _ = writeln!(
s,
"{:>5}{:<5}{:>5}{:>5}{:>8.3}{:>8.3}{:>8.3}",
res_serial,
&mol.name[..mol.name.len().min(5)],
&atom_name[..atom_name.len().min(5)],
atom_serial % 100_000,
x_nm,
y_nm,
z_nm,
);
atom_serial += 1;
}
res_serial += 1;
}
}
let (bx, by, bz) = self.box_nm.unwrap_or((0.0, 0.0, 0.0));
let _ = writeln!(s, "{:>10.5}{:>10.5}{:>10.5}", bx, by, bz);
s
}
pub fn make_top(&self) -> io::Result<String> {
let mol_tops: Vec<MoleculeTopology<'_>> = self
.molecules
.iter()
.map(|m| MoleculeTopology {
name: &m.name,
atoms: &m.atoms,
bonds: &m.bonds,
ff_mol: m.ff_params.as_ref(),
count: m.count,
})
.collect();
topology::make_top(
&mol_tops,
self.ff_global.as_ref(),
self.water_model.as_ref(),
)
}
pub fn solute_atom_count(&self) -> usize {
self.molecules.iter().map(|m| m.atoms.len() * m.count).sum()
}
pub fn save(&self, dir: &Path) -> io::Result<()> {
fs::create_dir_all(dir)?;
save_txt_to_file(dir.join(GRO_NAME), &self.make_gro())?;
save_txt_to_file(dir.join(TOP_NAME), &self.make_top()?)?;
save_txt_to_file(dir.join(MDP_NAME), &self.make_mdp())?;
if let Some(WaterModel::Opc(ref template)) = self.water_model {
save_txt_to_file(dir.join(SOLVENT_GRO_NAME), &template.to_gro())?;
}
Ok(())
}
pub fn run(&self) -> io::Result<GromacsOutput> {
let dir = Path::new(TEMP_DIR);
self.save(dir)?;
let solute_atom_count = self.solute_atom_count();
let structure_gro = if let Some(ref wm) = self.water_model {
run_gmx(
dir,
&[
"solvate",
"-cp",
GRO_NAME,
"-cs",
wm.gro_filename(),
"-o",
SOLVATED_NAME,
"-p",
TOP_NAME,
],
)?;
SOLVATED_NAME
} else {
GRO_NAME
};
let net_q: f32 = self
.molecules
.iter()
.map(|m| {
m.atoms
.iter()
.map(|a| a.partial_charge.unwrap_or(0.0))
.sum::<f32>()
* m.count as f32
})
.sum();
let structure_gro = if self.water_model.is_some() && net_q.abs() >= 0.5 {
save_txt_to_file(dir.join("ions.mdp"), em_mdp_str())?;
run_gmx(
dir,
&[
"grompp",
"-f",
"ions.mdp",
"-c",
structure_gro,
"-p",
TOP_NAME,
"-o",
"ions.tpr",
"-maxwarn",
"5",
],
)?;
run_gmx_stdin(
dir,
&[
"genion",
"-s",
"ions.tpr",
"-o",
IONIZED_NAME,
"-p",
TOP_NAME,
"-pname",
"NA",
"-nname",
"CL",
"-neutral",
],
b"SOL\n",
)?;
IONIZED_NAME
} else {
structure_gro
};
let md_input_gro = if self.minimize_energy {
save_txt_to_file(dir.join("em.mdp"), em_mdp_str())?;
run_gmx(
dir,
&[
"grompp",
"-f",
"em.mdp",
"-c",
structure_gro,
"-p",
TOP_NAME,
"-o",
"em.tpr",
"-maxwarn",
"5",
],
)?;
run_gmx(
dir,
&[
"mdrun",
"-s",
"em.tpr",
"-c",
"em.gro",
"-e",
ENERGY_OUT_NAME,
"-g",
"em.log",
],
)?;
"em.gro"
} else {
structure_gro
};
run_gmx(
dir,
&[
"grompp",
"-f",
MDP_NAME,
"-c",
md_input_gro,
"-p",
TOP_NAME,
"-o",
"topol.tpr",
"-maxwarn",
"5",
],
)?;
run_gmx(
dir,
&[
"mdrun",
"-s",
"topol.tpr",
"-o",
TRR_NAME,
"-x",
XTC_NAME,
"-c",
GRO_OUT_NAME,
"-e",
ENERGY_OUT_NAME,
"-g",
LOG_NAME,
],
)?;
let log_text = read_text(dir.join("md.log")).unwrap_or_default();
let trr_frames = read_trr(
&dir.join(TRR_NAME),
FrameSlice::Time {
start: None,
end: None,
},
)?;
let energies = OutputEnergy::from_edr(&dir.join(ENERGY_OUT_NAME)).unwrap_or_default();
let result = GromacsOutput::new(log_text, trr_frames, energies, solute_atom_count)?;
Ok(result)
}
}
fn em_mdp_str() -> &'static str {
"; Energy minimization — generated by Bio Files\n\
integrator = steep\n\
nsteps = 5000\n\
emtol = 1000.0\n\
emstep = 0.01\n\
\n\
cutoff-scheme = Verlet\n\
coulombtype = PME\n\
fourierspacing = 0.16\n\
rcoulomb = 1.0\n\
vdw-type = Cut-off\n\
rvdw = 1.0\n\
\n\
pbc = xyz\n\
constraints = none\n"
}
fn save_txt_to_file(path: impl AsRef<Path>, text: &str) -> io::Result<()> {
let mut f = File::create(path)?;
write!(f, "{text}")
}
fn read_text(path: impl AsRef<Path>) -> io::Result<String> {
fs::read_to_string(path)
}
pub fn run_gmx(dir: &Path, args: &[&str]) -> io::Result<()> {
let mut cmd = Command::new("gmx");
cmd.current_dir(dir).args(args);
let out = match cmd.output() {
Ok(o) => o,
Err(e) if e.kind() == ErrorKind::NotFound => {
return Err(io::Error::new(
ErrorKind::NotFound,
"`gmx` executable not found on the system PATH",
));
}
Err(e) => return Err(e),
};
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
return Err(io::Error::other(format!(
"gmx {} failed: {}",
args.first().unwrap_or(&"?"),
stderr,
)));
}
Ok(())
}
pub fn run_gmx_stdin(dir: &Path, args: &[&str], stdin_data: &[u8]) -> io::Result<()> {
let mut cmd = Command::new("gmx");
cmd.current_dir(dir)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) if e.kind() == ErrorKind::NotFound => {
return Err(io::Error::new(
ErrorKind::NotFound,
"`gmx` executable not found on the system PATH",
));
}
Err(e) => return Err(e),
};
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(stdin_data)?;
}
let out = child.wait_with_output()?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
return Err(io::Error::other(format!(
"gmx {} failed: {}",
args.first().unwrap_or(&"?"),
stderr,
)));
}
Ok(())
}