use std::{
fmt::Write as _,
fs,
fs::File,
io,
io::{BufWriter, ErrorKind, Write},
path::Path,
};
use lin_alg::f64::Vec3;
use na_seq::Element;
use crate::{el_from_atom_name, gromacs::MoleculeInput};
#[derive(Clone, Debug, PartialEq)]
pub struct AtomGro {
pub mol_id: u32,
pub mol_name: String,
pub element: Element,
pub atom_type: String,
pub serial_number: u32,
pub posit: Vec3,
pub velocity: Option<Vec3>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Gro {
pub atoms: Vec<AtomGro>,
pub head_text: String,
pub box_vec: Vec3,
}
impl Gro {
pub fn new(text: &str) -> io::Result<Self> {
let lines: Vec<&str> = text.lines().collect();
if lines.len() < 3 {
return Err(io::Error::new(
ErrorKind::InvalidData,
"Not enough lines to parse a GRO file",
));
}
let head_text = lines[0].to_owned();
let num_atoms = lines[1]
.trim()
.parse::<usize>()
.map_err(|_| io::Error::new(ErrorKind::InvalidData, "Could not parse atom count"))?;
if lines.len() < 2 + num_atoms + 1 {
return Err(io::Error::new(
ErrorKind::InvalidData,
"File has fewer lines than declared atom count",
));
}
let mut atoms = Vec::with_capacity(num_atoms);
for line in &lines[2..2 + num_atoms] {
let gro_slice = |a: usize, b: usize| -> &str {
let len = line.len();
line[a.min(len)..b.min(len)].trim()
};
if line.len() < 44 {
return Err(io::Error::new(
ErrorKind::InvalidData,
format!("Atom line is shorter than 44 chars: {line}"),
));
}
let mol_id = gro_slice(0, 5).parse::<u32>().map_err(|_| {
io::Error::new(ErrorKind::InvalidData, "Could not parse residue ID")
})?;
let mol_name = gro_slice(5, 10).to_owned(); let atom_type = gro_slice(10, 15).to_owned();
let serial_number = gro_slice(15, 20).parse::<u32>().map_err(|_| {
io::Error::new(ErrorKind::InvalidData, "Could not parse atom serial number")
})?;
let x = gro_slice(20, 28).parse::<f64>().map_err(|_| {
io::Error::new(ErrorKind::InvalidData, "Could not parse X coordinate")
})?;
let y = gro_slice(28, 36).parse::<f64>().map_err(|_| {
io::Error::new(ErrorKind::InvalidData, "Could not parse Y coordinate")
})?;
let z = gro_slice(36, 44).parse::<f64>().map_err(|_| {
io::Error::new(ErrorKind::InvalidData, "Could not parse Z coordinate")
})?;
let velocity = if line.len() >= 68 {
let vx = gro_slice(44, 52).parse::<f64>().ok();
let vy = gro_slice(52, 60).parse::<f64>().ok();
let vz = gro_slice(60, 68).parse::<f64>().ok();
match (vx, vy, vz) {
(Some(vx), Some(vy), Some(vz)) => Some(Vec3 {
x: vx,
y: vy,
z: vz,
}),
_ => None,
}
} else {
None
};
atoms.push(AtomGro {
mol_id,
mol_name,
element: el_from_atom_name(&atom_type),
atom_type,
serial_number,
posit: Vec3 { x, y, z },
velocity,
});
}
let box_line = lines[2 + num_atoms];
let box_cols: Vec<f64> = box_line
.split_whitespace()
.filter_map(|s| s.parse::<f64>().ok())
.collect();
let box_vec = if box_cols.len() >= 3 {
Vec3 {
x: box_cols[0],
y: box_cols[1],
z: box_cols[2],
}
} else {
Vec3::new_zero()
};
Ok(Self {
atoms,
head_text,
box_vec,
})
}
pub fn write_to(&self, w: &mut impl Write) -> io::Result<()> {
writeln!(w, "{}", self.head_text)?;
writeln!(w, "{:>5}", self.atoms.len())?;
for atom in &self.atoms {
match atom.velocity {
Some(v) => writeln!(
w,
"{:>5}{:<5}{:>5}{:>5}{:>8.3}{:>8.3}{:>8.3}{:>8.4}{:>8.4}{:>8.4}",
atom.mol_id % 100_000,
&atom.mol_name[..atom.mol_name.len().min(5)],
&atom.atom_type[..atom.atom_type.len().min(5)],
atom.serial_number % 100_000,
atom.posit.x,
atom.posit.y,
atom.posit.z,
v.x,
v.y,
v.z,
)?,
None => writeln!(
w,
"{:>5}{:<5}{:>5}{:>5}{:>8.3}{:>8.3}{:>8.3}",
atom.mol_id % 100_000,
&atom.mol_name[..atom.mol_name.len().min(5)],
&atom.atom_type[..atom.atom_type.len().min(5)],
atom.serial_number % 100_000,
atom.posit.x,
atom.posit.y,
atom.posit.z,
)?,
}
}
writeln!(
w,
"{:>10.5}{:>10.5}{:>10.5}",
self.box_vec.x, self.box_vec.y, self.box_vec.z,
)?;
Ok(())
}
pub fn save(&self, path: &Path) -> io::Result<()> {
let file = File::create(path)?;
let mut w = BufWriter::new(file);
self.write_to(&mut w)
}
pub fn load(path: &Path) -> io::Result<Self> {
let data_str = fs::read_to_string(path)?;
Self::new(&data_str)
}
}
pub fn make_gro(mols: &[MoleculeInput], box_nm: &Option<(f64, f64, f64)>) -> io::Result<String> {
make_gro_with_origin(mols, box_nm, None)
}
pub fn make_gro_with_origin(
mols: &[MoleculeInput],
box_nm: &Option<(f64, f64, f64)>,
coordinate_origin_a: Option<Vec3>,
) -> io::Result<String> {
for mol in mols {
if let Some(copy_atom_posits) = &mol.copy_atom_posits {
if copy_atom_posits.len() != mol.count {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"Molecule {} has {} packed coordinate sets for {} copies",
mol.name,
copy_atom_posits.len(),
mol.count,
),
));
}
if let Some((copy_i, posits)) = copy_atom_posits
.iter()
.enumerate()
.find(|(_, posits)| posits.len() != mol.atoms.len())
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"Molecule {} copy {} has {} atom positions for a {}-atom topology",
mol.name,
copy_i + 1,
posits.len(),
mol.atoms.len(),
),
));
}
}
}
let total_atoms: usize = mols.iter().map(|m| m.atoms.len() * m.count).sum();
let (shift_x, shift_y, shift_z) = if let Some(origin) = coordinate_origin_a {
(-origin.x / 10.0, -origin.y / 10.0, -origin.z / 10.0)
} else if let Some((bx, by, bz)) = box_nm {
let (mut sx, mut sy, mut sz) = (0.0, 0.0, 0.0);
let mut n = 0usize;
for mol in mols {
for copy_i in 0..mol.count {
for (atom_i, atom) in mol.atoms.iter().enumerate() {
let posit = mol
.copy_atom_posits
.as_ref()
.map(|copies| copies[copy_i][atom_i])
.unwrap_or(atom.posit);
sx += posit.x;
sy += posit.y;
sz += 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 mols {
for copy_i in 0..mol.count {
for (atom_i, atom) in mol.atoms.iter().enumerate() {
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 posit = mol
.copy_atom_posits
.as_ref()
.map(|copies| copies[copy_i][atom_i])
.unwrap_or(atom.posit);
let x_nm = posit.x / 10.0 + shift_x;
let y_nm = posit.y / 10.0 + shift_y;
let z_nm = 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) = box_nm.unwrap_or((0.0, 0.0, 0.0));
let _ = writeln!(s, "{:>10.5}{:>10.5}{:>10.5}", bx, by, bz);
Ok(s)
}