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::{gro::make_gro, output::read_trr},
md_params::ForceFieldParams,
};
const TEMP_DIR: &str = "gromacs_out";
const MD_OUT_DIR: &str = "md_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 = "mols_in_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_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(MDP_NAME), &self.make_mdp())?;
save_txt_to_file(dir.join(GRO_NAME), &make_gro(&self.molecules, &self.box_nm))?;
save_txt_to_file(dir.join(TOP_NAME), &self.make_top()?)?;
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 out_dir = Path::new(MD_OUT_DIR);
fs::create_dir_all(out_dir)?;
let run_n = (1_usize..)
.find(|&n| {
!out_dir.join(format!("traj_{n}.trr")).exists()
&& !out_dir.join(format!("traj_{n}.xtc")).exists()
})
.unwrap_or(1);
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: String = 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".to_string()
} else {
structure_gro.to_string()
};
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 trr_src = dir.join(TRR_NAME);
let trr_dest = out_dir.join(format!("traj_{run_n}.trr"));
if trr_src.exists() {
fs::copy(&trr_src, &trr_dest)?;
}
let xtc_src = dir.join(XTC_NAME);
let xtc_dest = if xtc_src.exists() {
let dest = out_dir.join(format!("traj_{run_n}.xtc"));
fs::copy(&xtc_src, &dest)?;
Some(dest)
} else {
None
};
let gro_src = dir.join(&md_input_gro);
let gro_dest = out_dir.join(format!("mols_in_{run_n}.gro"));
if gro_src.exists() {
fs::copy(&gro_src, &gro_dest)?;
}
let log_text = read_text(dir.join("md.log")).unwrap_or_default();
let trr_frames = read_trr(
&trr_src,
FrameSlice::Time {
start: None,
end: None,
},
)?;
let energies = OutputEnergy::from_edr(&dir.join(ENERGY_OUT_NAME)).unwrap_or_default();
let mut result = GromacsOutput::new(log_text, trr_frames, energies, solute_atom_count)?;
result.trr_path = if trr_dest.exists() {
Some(trr_dest)
} else {
None
};
result.xtc_path = xtc_dest;
result.gro_path = if gro_dest.exists() {
Some(gro_dest)
} else {
None
};
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(())
}