use crate::atom::{element_symbol, AtomStr, ATOM_NAME_EXPECT};
use crate::prelude::*;
use std::{
fs::File,
io::{BufRead, BufReader, BufWriter, Write},
num::{ParseFloatError, ParseIntError},
path::Path,
};
use thiserror::Error;
pub struct XyzFileHandler {
reader: Option<BufReader<File>>,
writer: Option<BufWriter<File>>,
stored_topology: Option<Topology>,
stored_state: Option<State>,
at_least_one_state_read: bool,
}
#[derive(Debug, Error)]
pub enum XyzHandlerError {
#[error("can't open xyz file for reading")]
OpenRead(#[source] std::io::Error),
#[error("can't open xyz file for writing")]
OpenWrite(#[source] std::io::Error),
#[error("xyz file is empty or contains no atoms")]
Empty,
#[error("unexpected end of file reached")]
Eof,
#[error("malformed atom count line")]
BadAtomCount(#[from] ParseIntError),
#[error("malformed coordinate on atom line {0}")]
BadCoord(usize, #[source] ParseFloatError),
#[error("truncated atom line {0}")]
TruncatedLine(usize),
#[error("io error")]
Io(#[from] std::io::Error),
}
impl FileFormatHandler for XyzFileHandler {
fn open(fname: impl AsRef<Path>) -> Result<Self, FileFormatError>
where
Self: Sized,
{
Ok(Self {
reader: Some(BufReader::new(
File::open(fname).map_err(XyzHandlerError::OpenRead)?,
)),
writer: None,
stored_topology: None,
stored_state: None,
at_least_one_state_read: false,
})
}
fn create(fname: impl AsRef<Path>) -> Result<Self, FileFormatError>
where
Self: Sized,
{
Ok(Self {
reader: None,
writer: Some(BufWriter::new(
File::create(fname).map_err(XyzHandlerError::OpenWrite)?,
)),
stored_topology: None,
stored_state: None,
at_least_one_state_read: false,
})
}
fn read(&mut self) -> Result<(Topology, State), FileFormatError> {
let buf = self.reader.as_mut().unwrap();
if buf.fill_buf()?.is_empty() {
return if self.at_least_one_state_read {
Err(XyzHandlerError::Eof)?
} else {
Err(XyzHandlerError::Empty)?
};
}
let mut line = String::new();
let n = buf.read_line(&mut line)?;
if n == 0 || line.trim().is_empty() {
return if self.at_least_one_state_read {
Err(XyzHandlerError::Eof)?
} else {
Err(XyzHandlerError::Empty)?
};
}
let natoms: usize = line.trim().parse().map_err(XyzHandlerError::BadAtomCount)?;
line.clear();
buf.read_line(&mut line)?;
let mut atoms: Vec<Atom> = Vec::with_capacity(natoms);
let mut coords: Vec<Pos> = Vec::with_capacity(natoms);
for i in 0..natoms {
line.clear();
buf.read_line(&mut line)?;
let mut tokens = line.split_whitespace();
let elem = tokens.next().ok_or(XyzHandlerError::TruncatedLine(i))?;
let x: f32 = tokens
.next()
.ok_or(XyzHandlerError::TruncatedLine(i))?
.parse()
.map_err(|e| XyzHandlerError::BadCoord(i, e))?;
let y: f32 = tokens
.next()
.ok_or(XyzHandlerError::TruncatedLine(i))?
.parse()
.map_err(|e| XyzHandlerError::BadCoord(i, e))?;
let z: f32 = tokens
.next()
.ok_or(XyzHandlerError::TruncatedLine(i))?
.parse()
.map_err(|e| XyzHandlerError::BadCoord(i, e))?;
coords.push(Pos::new(x * 0.1, y * 0.1, z * 0.1));
let mut at = Atom {
name: AtomStr::try_from_str(elem).expect(ATOM_NAME_EXPECT),
resname: AtomStr::try_from_str("MOL").unwrap(),
resid: 1,
chain: 'A',
occupancy: 1.0,
type_name: AtomStr::try_from_str("").unwrap(),
..Default::default()
};
at.guess_element_and_mass_from_name();
atoms.push(at);
}
let mut top = Topology::default();
top.atoms = atoms;
top.assign_resindex();
self.at_least_one_state_read = true;
Ok((top, State { coords, time: 0.0, pbox: None }))
}
fn read_topology(&mut self) -> Result<Topology, FileFormatError> {
if self.stored_topology.is_some() {
Ok(self.stored_topology.take().unwrap())
} else {
let (top, st) = self.read()?;
self.stored_state.get_or_insert(st);
Ok(top)
}
}
fn read_state(&mut self) -> Result<State, FileFormatError> {
if self.stored_state.is_some() {
Ok(self.stored_state.take().unwrap())
} else {
let (top, st) = self.read()?;
self.stored_topology.get_or_insert(top);
Ok(st)
}
}
fn write(&mut self, data: &dyn SaveTopologyState) -> Result<(), FileFormatError> {
let w = self.writer.as_mut().ok_or(FileFormatError::NotWritable)?;
let n = data.len();
writeln!(w, "{n}")?;
writeln!(w)?; for (at, pos) in data.iter_atoms_dyn().zip(data.iter_pos_dyn()) {
let elem = element_symbol(at.atomic_number);
let sym = if elem.is_empty() { at.name.as_str() } else { elem };
writeln!(
w,
"{} {:>12.6} {:>12.6} {:>12.6}",
sym,
pos.x * 10.0,
pos.y * 10.0,
pos.z * 10.0
)?;
}
Ok(())
}
}