use std::fmt;
use std::io;
use std::path::Path;
use crate::chemfiles_import::{self, ChemfilesImportError};
use crate::compression;
use crate::iterators::ConFrameIterator;
use crate::types::ConFrame;
use crate::writer::ConFrameWriter;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConvertReport {
pub n_frames: usize,
pub n_atoms_last: usize,
pub native_con: bool,
}
#[derive(Debug)]
pub enum ConvertError {
InputMissing(String),
Empty,
Io(io::Error),
Parse(String),
Chemfiles(ChemfilesImportError),
}
impl fmt::Display for ConvertError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConvertError::InputMissing(p) => write!(f, "input not found: {p}"),
ConvertError::Empty => write!(f, "no frames produced from input"),
ConvertError::Io(e) => write!(f, "I/O error: {e}"),
ConvertError::Parse(msg) => write!(f, "parse error: {msg}"),
ConvertError::Chemfiles(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for ConvertError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConvertError::Io(e) => Some(e),
ConvertError::Chemfiles(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for ConvertError {
fn from(e: io::Error) -> Self {
ConvertError::Io(e)
}
}
impl From<ChemfilesImportError> for ConvertError {
fn from(e: ChemfilesImportError) -> Self {
ConvertError::Chemfiles(e)
}
}
pub fn path_looks_like_con(path: &Path) -> bool {
let name = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_ascii_lowercase();
let base = name
.strip_suffix(".gz")
.or_else(|| name.strip_suffix(".zst"))
.unwrap_or(&name);
base.ends_with(".con") || base.ends_with(".convel")
}
pub fn read_frames_for_convert(input: &Path) -> Result<(Vec<ConFrame>, bool), ConvertError> {
if !input.is_file() {
return Err(ConvertError::InputMissing(input.display().to_string()));
}
if path_looks_like_con(input) {
let contents = compression::read_file_contents(input).map_err(|e| {
ConvertError::Io(io::Error::other(e.to_string()))
})?;
let text = contents
.as_str()
.map_err(|e| ConvertError::Parse(format!("input is not valid UTF-8: {e}")))?;
let mut frames = Vec::new();
for item in ConFrameIterator::new(text) {
match item {
Ok(f) => frames.push(f),
Err(e) => {
return Err(ConvertError::Parse(e.to_string()));
}
}
}
if frames.is_empty() {
return Err(ConvertError::Empty);
}
Ok((frames, true))
} else {
if !chemfiles_import::chemfiles_enabled() {
return Err(ConvertError::Chemfiles(
ChemfilesImportError::FeatureDisabled,
));
}
let frames = chemfiles_import::con_frames_from_trajectory_path(input)?;
if frames.is_empty() {
return Err(ConvertError::Empty);
}
Ok((frames, false))
}
}
pub fn convert_path_to_con(input: &Path, output: &Path) -> Result<ConvertReport, ConvertError> {
let (frames, native_con) = read_frames_for_convert(input)?;
let n_frames = frames.len();
let n_atoms_last = frames.last().map(|f| f.atom_data.len()).unwrap_or(0);
let mut writer = ConFrameWriter::from_path(output)?;
writer
.extend(frames.iter())
.map_err(|e| ConvertError::Io(io::Error::other(e.to_string())))?;
Ok(ConvertReport {
n_frames,
n_atoms_last,
native_con,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
fn fixture(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test")
.join(name)
}
#[test]
fn path_looks_like_con_suffixes() {
assert!(path_looks_like_con(Path::new("a.con")));
assert!(path_looks_like_con(Path::new("a.convel")));
assert!(path_looks_like_con(Path::new("a.con.gz")));
assert!(!path_looks_like_con(Path::new("a.xyz")));
assert!(!path_looks_like_con(Path::new("a.pdb")));
}
#[test]
fn convert_native_con_roundtrip() {
let dir = tempfile_dir();
let out = dir.join("out.con");
let report = convert_path_to_con(&fixture("tiny_multi_cuh2.con"), &out).unwrap();
assert!(report.native_con);
assert_eq!(report.n_frames, 2);
assert_eq!(report.n_atoms_last, 4);
let (back, native) = read_frames_for_convert(&out).unwrap();
assert!(native);
assert_eq!(back.len(), 2);
assert_eq!(back[0].atom_data.len(), 4);
assert_eq!(back[0].atom_data[0].atom_id, 0);
}
#[test]
#[cfg(feature = "chemfiles")]
fn convert_xyz_via_chemfiles() {
let dir = tempfile_dir();
let xyz = dir.join("water.xyz");
fs::write(
&xyz,
"3\nwater migrate\nO 0 0 0\nH 0.96 0 0\nH -0.24 0.93 0\n",
)
.unwrap();
let out = dir.join("water.con");
let report = convert_path_to_con(&xyz, &out).unwrap();
assert!(!report.native_con);
assert_eq!(report.n_frames, 1);
assert_eq!(report.n_atoms_last, 3);
let (back, _) = read_frames_for_convert(&out).unwrap();
assert_eq!(back[0].atom_data.len(), 3);
let symbols: Vec<_> = back[0]
.atom_data
.iter()
.map(|a| a.symbol.as_ref())
.collect();
assert!(symbols.contains(&"O"));
assert_eq!(symbols.iter().filter(|s| **s == "H").count(), 2);
}
#[test]
#[cfg(not(feature = "chemfiles"))]
fn convert_xyz_fails_without_chemfiles() {
let dir = tempfile_dir();
let xyz = dir.join("water.xyz");
fs::write(&xyz, "3\nx\nO 0 0 0\nH 1 0 0\nH 0 1 0\n").unwrap();
let out = dir.join("water.con");
let err = convert_path_to_con(&xyz, &out).unwrap_err();
assert!(matches!(
err,
ConvertError::Chemfiles(ChemfilesImportError::FeatureDisabled)
));
}
fn tempfile_dir() -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"readcon-convert-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::create_dir_all(&dir).unwrap();
dir
}
}