#![allow(clippy::needless_range_loop)]
#![allow(dead_code)]
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
#[derive(Debug, Clone, Default)]
pub struct ParticleFrame {
pub timestep: u64,
pub n_particles: usize,
pub positions: Vec<[f32; 3]>,
pub velocities: Option<Vec<[f32; 3]>>,
pub types: Vec<u8>,
}
impl ParticleFrame {
pub fn new(timestep: u64, positions: Vec<[f32; 3]>, types: Vec<u8>) -> Self {
let n = positions.len();
Self {
timestep,
n_particles: n,
positions,
velocities: None,
types,
}
}
pub fn from_positions(timestep: u64, positions: Vec<[f32; 3]>) -> Self {
let n = positions.len();
let types = vec![0u8; n];
Self::new(timestep, positions, types)
}
pub fn center_of_mass(&self) -> [f32; 3] {
if self.positions.is_empty() {
return [0.0; 3];
}
let n = self.positions.len() as f32;
let mut com = [0.0f32; 3];
for p in &self.positions {
com[0] += p[0];
com[1] += p[1];
com[2] += p[2];
}
[com[0] / n, com[1] / n, com[2] / n]
}
pub fn bounding_box(&self) -> ([f32; 3], [f32; 3]) {
if self.positions.is_empty() {
return ([0.0; 3], [0.0; 3]);
}
let mut lo = self.positions[0];
let mut hi = self.positions[0];
for &p in &self.positions[1..] {
for d in 0..3 {
if p[d] < lo[d] {
lo[d] = p[d];
}
if p[d] > hi[d] {
hi[d] = p[d];
}
}
}
(lo, hi)
}
}
#[derive(Debug, Clone)]
pub struct XyzWriter {
pub path: std::path::PathBuf,
}
impl XyzWriter {
pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn write_frame(&self, frame: &ParticleFrame, comment: &str) -> io::Result<()> {
let f = File::create(&self.path)?;
let mut w = BufWriter::new(f);
write_xyz_frame_to(&mut w, frame, comment)
}
pub fn append_frame(&self, frame: &ParticleFrame, comment: &str) -> io::Result<()> {
let f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
let mut w = BufWriter::new(f);
write_xyz_frame_to(&mut w, frame, comment)
}
pub fn write_trajectory(&self, traj: &ParticleTrajectory) -> io::Result<()> {
let f = File::create(&self.path)?;
let mut w = BufWriter::new(f);
for frame in &traj.frames {
let comment = format!("Timestep={}", frame.timestep);
write_xyz_frame_to(&mut w, frame, &comment)?;
}
Ok(())
}
}
fn write_xyz_frame_to<W: Write>(w: &mut W, frame: &ParticleFrame, comment: &str) -> io::Result<()> {
writeln!(w, "{}", frame.n_particles)?;
writeln!(w, "Timestep={} {}", frame.timestep, comment)?;
for (i, p) in frame.positions.iter().enumerate() {
let t = frame.types.get(i).copied().unwrap_or(0);
writeln!(w, "{} {:.8} {:.8} {:.8}", t, p[0], p[1], p[2])?;
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct XyzReader {
pub path: std::path::PathBuf,
}
impl XyzReader {
pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn read_frame(&self) -> io::Result<ParticleFrame> {
let f = File::open(&self.path)?;
let mut r = BufReader::new(f);
parse_xyz_frame(&mut r)
}
pub fn read_all(&self) -> io::Result<Vec<ParticleFrame>> {
let f = File::open(&self.path)?;
let mut r = BufReader::new(f);
let mut frames = Vec::new();
loop {
match parse_xyz_frame(&mut r) {
Ok(fr) => frames.push(fr),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e),
}
}
Ok(frames)
}
}
fn parse_xyz_frame<R: BufRead>(r: &mut R) -> io::Result<ParticleFrame> {
let mut line = String::new();
r.read_line(&mut line)?;
if line.is_empty() {
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF"));
}
let n: usize = line
.trim()
.parse()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "expected atom count"))?;
line.clear();
r.read_line(&mut line)?;
let timestep = extract_timestep_from_comment(&line);
line.clear();
let mut positions = Vec::with_capacity(n);
let mut types = Vec::with_capacity(n);
for _ in 0..n {
r.read_line(&mut line)?;
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 4 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"short atom line",
));
}
let t: u8 = parts[0].parse().unwrap_or(0);
let x: f32 = parts[1]
.parse()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "bad x"))?;
let y: f32 = parts[2]
.parse()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "bad y"))?;
let z: f32 = parts[3]
.parse()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "bad z"))?;
positions.push([x, y, z]);
types.push(t);
line.clear();
}
Ok(ParticleFrame {
timestep,
n_particles: n,
positions,
velocities: None,
types,
})
}
fn extract_timestep_from_comment(comment: &str) -> u64 {
for token in comment.split_whitespace() {
if let Some(rest) = token.strip_prefix("Timestep=")
&& let Ok(v) = rest.parse::<u64>()
{
return v;
}
}
0
}
#[derive(Debug, Clone)]
pub struct LammpsDumpWriter {
pub path: std::path::PathBuf,
pub write_velocities: bool,
}
impl LammpsDumpWriter {
pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
Self {
path: path.into(),
write_velocities: true,
}
}
pub fn write_frame(
&self,
frame: &ParticleFrame,
box_lo: [f32; 3],
box_hi: [f32; 3],
) -> io::Result<()> {
let f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
let mut w = BufWriter::new(f);
write_lammps_dump_frame(&mut w, frame, box_lo, box_hi, self.write_velocities)
}
}
fn write_lammps_dump_frame<W: Write>(
w: &mut W,
frame: &ParticleFrame,
box_lo: [f32; 3],
box_hi: [f32; 3],
with_vel: bool,
) -> io::Result<()> {
writeln!(w, "ITEM: TIMESTEP")?;
writeln!(w, "{}", frame.timestep)?;
writeln!(w, "ITEM: NUMBER OF ATOMS")?;
writeln!(w, "{}", frame.n_particles)?;
writeln!(w, "ITEM: BOX BOUNDS pp pp pp")?;
for d in 0..3 {
writeln!(w, "{:.6} {:.6}", box_lo[d], box_hi[d])?;
}
let has_vel = with_vel && frame.velocities.is_some();
if has_vel {
writeln!(w, "ITEM: ATOMS id type x y z vx vy vz")?;
} else {
writeln!(w, "ITEM: ATOMS id type x y z")?;
}
let vels = frame.velocities.as_deref();
for (i, p) in frame.positions.iter().enumerate() {
let t = frame.types.get(i).copied().unwrap_or(0);
if has_vel {
let v = vels.map(|vs| vs[i]).unwrap_or([0.0; 3]);
writeln!(
w,
"{} {} {:.8} {:.8} {:.8} {:.8} {:.8} {:.8}",
i + 1,
t,
p[0],
p[1],
p[2],
v[0],
v[1],
v[2]
)?;
} else {
writeln!(w, "{} {} {:.8} {:.8} {:.8}", i + 1, t, p[0], p[1], p[2])?;
}
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct LammpsDumpReader {
pub path: std::path::PathBuf,
}
impl LammpsDumpReader {
pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn read_all(&self) -> io::Result<Vec<ParticleFrame>> {
let f = File::open(&self.path)?;
let r = BufReader::new(f);
parse_lammps_dump_all(r)
}
}
fn parse_lammps_dump_all<R: BufRead>(mut r: R) -> io::Result<Vec<ParticleFrame>> {
let mut frames = Vec::new();
let mut lines = Vec::<String>::new();
{
let mut line = String::new();
while r.read_line(&mut line)? > 0 {
lines.push(line.trim_end().to_string());
line.clear();
}
}
let mut i = 0;
while i < lines.len() {
if lines[i].starts_with("ITEM: TIMESTEP") {
i += 1;
let timestep: u64 = lines
.get(i)
.and_then(|l| l.trim().parse().ok())
.unwrap_or(0);
i += 1; i += 1;
let n_atoms: usize = lines
.get(i)
.and_then(|l| l.trim().parse().ok())
.unwrap_or(0);
i += 1; i += 4;
let header = lines.get(i).cloned().unwrap_or_default();
let cols: Vec<&str> = header.split_whitespace().collect();
let idx_id = col_idx(&cols, "id");
let idx_type = col_idx(&cols, "type");
let idx_x = col_idx(&cols, "x");
let idx_y = col_idx(&cols, "y");
let idx_z = col_idx(&cols, "z");
let idx_vx = col_idx(&cols, "vx");
let idx_vy = col_idx(&cols, "vy");
let idx_vz = col_idx(&cols, "vz");
let has_vel = idx_vx.is_some() && idx_vy.is_some() && idx_vz.is_some();
let data_offset = 2usize;
i += 1;
let mut positions = Vec::with_capacity(n_atoms);
let mut types = Vec::with_capacity(n_atoms);
let mut velocities: Vec<[f32; 3]> = Vec::with_capacity(n_atoms);
for _ in 0..n_atoms {
if i >= lines.len() {
break;
}
let parts: Vec<&str> = lines[i].split_whitespace().collect();
let get = |opt_col: Option<usize>| -> f32 {
opt_col
.and_then(|c| {
let real_c = c.saturating_sub(data_offset);
parts.get(real_c).and_then(|s| s.parse().ok())
})
.unwrap_or(0.0)
};
let get_u8 = |opt_col: Option<usize>| -> u8 {
opt_col
.and_then(|c| {
let real_c = c.saturating_sub(data_offset);
parts.get(real_c).and_then(|s| s.parse::<u8>().ok())
})
.unwrap_or(0)
};
let _ = idx_id; let t = get_u8(idx_type);
let x = get(idx_x);
let y = get(idx_y);
let z = get(idx_z);
positions.push([x, y, z]);
types.push(t);
if has_vel {
let vx = get(idx_vx);
let vy = get(idx_vy);
let vz = get(idx_vz);
velocities.push([vx, vy, vz]);
}
i += 1;
}
let vel_opt = if has_vel && !velocities.is_empty() {
Some(velocities)
} else {
None
};
frames.push(ParticleFrame {
timestep,
n_particles: positions.len(),
positions,
velocities: vel_opt,
types,
});
} else {
i += 1;
}
}
Ok(frames)
}
fn col_idx(cols: &[&str], name: &str) -> Option<usize> {
cols.iter().position(|&c| c == name)
}
#[derive(Debug, Clone, Default)]
pub struct ParticleTrajectory {
pub frames: Vec<ParticleFrame>,
}
impl ParticleTrajectory {
pub fn new() -> Self {
Self { frames: Vec::new() }
}
pub fn n_frames(&self) -> usize {
self.frames.len()
}
pub fn n_particles(&self) -> usize {
self.frames.first().map(|f| f.n_particles).unwrap_or(0)
}
pub fn get(&self, i: usize) -> Option<&ParticleFrame> {
self.frames.get(i)
}
pub fn slice(&self, start: usize, end: usize) -> Self {
let end = end.min(self.frames.len());
Self {
frames: self.frames[start..end].to_vec(),
}
}
pub fn subsample(&self, step: usize) -> Self {
if step == 0 {
return Self::new();
}
Self {
frames: self.frames.iter().step_by(step).cloned().collect(),
}
}
pub fn push(&mut self, frame: ParticleFrame) {
self.frames.push(frame);
}
}
#[derive(Debug, Clone, Default)]
pub struct TrajectoryStats {
pub msd: Vec<f64>,
pub rmsf: Vec<f64>,
pub com_drift: Vec<[f64; 3]>,
}
impl TrajectoryStats {
pub fn compute(traj: &ParticleTrajectory) -> Self {
Self {
msd: compute_msd(traj),
rmsf: compute_rmsf(traj),
com_drift: compute_com_drift(traj),
}
}
}
fn compute_msd(traj: &ParticleTrajectory) -> Vec<f64> {
let nf = traj.n_frames();
let np = traj.n_particles();
if nf == 0 || np == 0 {
return vec![];
}
let mut result = vec![0.0f64; nf];
for lag in 0..nf {
let mut sum = 0.0f64;
let mut count = 0usize;
for t in 0..(nf - lag) {
let f0 = &traj.frames[t];
let f1 = &traj.frames[t + lag];
let n = f0.positions.len().min(f1.positions.len());
for i in 0..n {
let r0 = f0.positions[i];
let r1 = f1.positions[i];
let dr2 =
(r1[0] - r0[0]).powi(2) + (r1[1] - r0[1]).powi(2) + (r1[2] - r0[2]).powi(2);
sum += dr2 as f64;
count += 1;
}
}
result[lag] = if count > 0 { sum / count as f64 } else { 0.0 };
}
result
}
fn compute_rmsf(traj: &ParticleTrajectory) -> Vec<f64> {
let nf = traj.n_frames();
let np = traj.n_particles();
if nf == 0 || np == 0 {
return vec![];
}
let mut mean = vec![[0.0f64; 3]; np];
for frame in &traj.frames {
for (i, p) in frame.positions.iter().enumerate().take(np) {
mean[i][0] += p[0] as f64;
mean[i][1] += p[1] as f64;
mean[i][2] += p[2] as f64;
}
}
for m in &mut mean {
m[0] /= nf as f64;
m[1] /= nf as f64;
m[2] /= nf as f64;
}
let mut var = vec![0.0f64; np];
for frame in &traj.frames {
for (i, p) in frame.positions.iter().enumerate().take(np) {
let dx = p[0] as f64 - mean[i][0];
let dy = p[1] as f64 - mean[i][1];
let dz = p[2] as f64 - mean[i][2];
var[i] += dx * dx + dy * dy + dz * dz;
}
}
var.iter().map(|&v| (v / nf as f64).sqrt()).collect()
}
fn compute_com_drift(traj: &ParticleTrajectory) -> Vec<[f64; 3]> {
traj.frames
.iter()
.map(|f| {
if f.positions.is_empty() {
[0.0; 3]
} else {
let n = f.positions.len() as f64;
let mut com = [0.0f64; 3];
for p in &f.positions {
com[0] += p[0] as f64;
com[1] += p[1] as f64;
com[2] += p[2] as f64;
}
[com[0] / n, com[1] / n, com[2] / n]
}
})
.collect()
}
const BINARY_MAGIC: u32 = 0x4F58_4950;
#[derive(Debug, Clone)]
pub struct BinaryFrameWriter {
pub path: std::path::PathBuf,
}
impl BinaryFrameWriter {
pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn write_frame(&self, frame: &ParticleFrame) -> io::Result<()> {
let f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
let mut w = BufWriter::new(f);
write_binary_frame(&mut w, frame)
}
}
fn write_binary_frame<W: Write>(w: &mut W, frame: &ParticleFrame) -> io::Result<()> {
w.write_all(&BINARY_MAGIC.to_le_bytes())?;
w.write_all(&frame.timestep.to_le_bytes())?;
w.write_all(&(frame.n_particles as u32).to_le_bytes())?;
for p in &frame.positions {
w.write_all(&p[0].to_le_bytes())?;
w.write_all(&p[1].to_le_bytes())?;
w.write_all(&p[2].to_le_bytes())?;
}
for &t in &frame.types {
w.write_all(&[t])?;
}
let has_vel = frame.velocities.is_some();
w.write_all(&[has_vel as u8])?;
if let Some(vels) = &frame.velocities {
for v in vels {
w.write_all(&v[0].to_le_bytes())?;
w.write_all(&v[1].to_le_bytes())?;
w.write_all(&v[2].to_le_bytes())?;
}
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct BinaryFrameReader {
pub path: std::path::PathBuf,
}
impl BinaryFrameReader {
pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn read_all(&self) -> io::Result<Vec<ParticleFrame>> {
let f = File::open(&self.path)?;
let mut r = BufReader::new(f);
let mut frames = Vec::new();
loop {
match read_binary_frame(&mut r) {
Ok(fr) => frames.push(fr),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e),
}
}
Ok(frames)
}
}
fn read_binary_frame<R: Read>(r: &mut R) -> io::Result<ParticleFrame> {
let magic = read_u32_le(r)?;
if magic != BINARY_MAGIC {
return Err(io::Error::new(io::ErrorKind::InvalidData, "bad magic"));
}
let timestep = read_u64_le(r)?;
let n = read_u32_le(r)? as usize;
let mut positions = Vec::with_capacity(n);
for _ in 0..n {
let x = read_f32_le(r)?;
let y = read_f32_le(r)?;
let z = read_f32_le(r)?;
positions.push([x, y, z]);
}
let mut types = vec![0u8; n];
r.read_exact(&mut types)?;
let mut has_vel_buf = [0u8; 1];
r.read_exact(&mut has_vel_buf)?;
let velocities = if has_vel_buf[0] != 0 {
let mut vels = Vec::with_capacity(n);
for _ in 0..n {
let vx = read_f32_le(r)?;
let vy = read_f32_le(r)?;
let vz = read_f32_le(r)?;
vels.push([vx, vy, vz]);
}
Some(vels)
} else {
None
};
Ok(ParticleFrame {
timestep,
n_particles: n,
positions,
velocities,
types,
})
}
fn read_u32_le<R: Read>(r: &mut R) -> io::Result<u32> {
let mut buf = [0u8; 4];
r.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf))
}
fn read_u64_le<R: Read>(r: &mut R) -> io::Result<u64> {
let mut buf = [0u8; 8];
r.read_exact(&mut buf)?;
Ok(u64::from_le_bytes(buf))
}
fn read_f32_le<R: Read>(r: &mut R) -> io::Result<f32> {
let mut buf = [0u8; 4];
r.read_exact(&mut buf)?;
Ok(f32::from_le_bytes(buf))
}
#[derive(Debug, Clone)]
pub struct GroWriter {
pub path: std::path::PathBuf,
}
impl GroWriter {
pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn write_frame(&self, frame: &ParticleFrame, box_vecs: [f32; 3]) -> io::Result<()> {
let f = File::create(&self.path)?;
let mut w = BufWriter::new(f);
writeln!(w, "Generated by oxiphysics-io t={}", frame.timestep)?;
writeln!(w, "{}", frame.n_particles)?;
for (i, p) in frame.positions.iter().enumerate() {
let t = frame.types.get(i).copied().unwrap_or(0);
writeln!(
w,
"{:5}UNK {:>4}{:5}{:8.3}{:8.3}{:8.3}",
(i + 1) % 100_000,
t,
(i + 1) % 100_000,
p[0],
p[1],
p[2]
)?;
}
writeln!(
w,
"{:.5} {:.5} {:.5}",
box_vecs[0], box_vecs[1], box_vecs[2]
)?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct GroReader {
pub path: std::path::PathBuf,
}
impl GroReader {
pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn read_frame(&self) -> io::Result<ParticleFrame> {
let f = File::open(&self.path)?;
let mut r = BufReader::new(f);
parse_gro_frame(&mut r)
}
}
fn parse_gro_frame<R: BufRead>(r: &mut R) -> io::Result<ParticleFrame> {
let mut line = String::new();
r.read_line(&mut line)?;
if line.is_empty() {
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF"));
}
line.clear();
r.read_line(&mut line)?;
let n: usize = line
.trim()
.parse()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "expected atom count"))?;
line.clear();
let mut positions = Vec::with_capacity(n);
let mut types = Vec::with_capacity(n);
for _ in 0..n {
r.read_line(&mut line)?;
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 5 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "short GRO line"));
}
let t: u8 = parts[1].parse().unwrap_or(0);
let x: f32 = parts[3]
.parse()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "bad x"))?;
let y: f32 = parts[4]
.parse()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "bad y"))?;
let z: f32 = if parts.len() > 5 {
parts[5].parse().unwrap_or(0.0)
} else {
0.0
};
positions.push([x, y, z]);
types.push(t);
line.clear();
}
Ok(ParticleFrame {
timestep: 0,
n_particles: n,
positions,
velocities: None,
types,
})
}
#[derive(Debug, Clone, Default)]
pub struct DcdHeader {
pub n_frames: u32,
pub first_step: u32,
pub step_interval: u32,
pub n_atoms: u32,
pub titles: Vec<String>,
}
#[derive(Debug)]
pub struct DcdReader {
pub path: std::path::PathBuf,
}
impl DcdReader {
pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn read_header(&self) -> io::Result<DcdHeader> {
let f = File::open(&self.path)?;
let mut r = BufReader::new(f);
parse_dcd_header(&mut r)
}
pub fn read_all(&self) -> io::Result<(DcdHeader, Vec<ParticleFrame>)> {
let f = File::open(&self.path)?;
let mut r = BufReader::new(f);
let header = parse_dcd_header(&mut r)?;
let n = header.n_atoms as usize;
let mut frames = Vec::with_capacity(header.n_frames as usize);
for step in 0..header.n_frames {
match parse_dcd_frame(&mut r, n, step as u64) {
Ok(fr) => frames.push(fr),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e),
}
}
Ok((header, frames))
}
}
fn parse_dcd_header<R: Read + Seek>(r: &mut R) -> io::Result<DcdHeader> {
let rec_len = read_u32_le(r)?;
if rec_len < 4 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"DCD block1 too short",
));
}
let mut magic = [0u8; 4];
r.read_exact(&mut magic)?;
let n_frames = read_u32_le(r)?;
let first_step = read_u32_le(r)?;
let step_interval = read_u32_le(r)?;
let remaining = rec_len as usize - 4 - 4 * 3;
r.seek(SeekFrom::Current(remaining as i64))?;
let _end_len = read_u32_le(r)?;
let title_block_len = read_u32_le(r)? as usize;
let n_titles = read_u32_le(r)? as usize;
let mut titles = Vec::new();
for _ in 0..n_titles {
let mut buf = vec![0u8; 80];
r.read_exact(&mut buf)?;
titles.push(String::from_utf8_lossy(&buf).trim_end().to_string());
}
let consumed = 4 + n_titles * 80;
if consumed < title_block_len {
r.seek(SeekFrom::Current((title_block_len - consumed) as i64))?;
}
let _end_title = read_u32_le(r)?;
let _len3 = read_u32_le(r)?;
let n_atoms = read_u32_le(r)?;
let _end3 = read_u32_le(r)?;
Ok(DcdHeader {
n_frames,
first_step,
step_interval,
n_atoms,
titles,
})
}
fn parse_dcd_frame<R: Read>(r: &mut R, n_atoms: usize, step: u64) -> io::Result<ParticleFrame> {
let read_f32_block = |r: &mut R| -> io::Result<Vec<f32>> {
let len = read_u32_le(r)? as usize;
let count = len / 4;
let mut vals = Vec::with_capacity(count);
for _ in 0..count {
vals.push(read_f32_le(r)?);
}
let _end = read_u32_le(r)?;
Ok(vals)
};
let xs = read_f32_block(r)?;
let ys = read_f32_block(r)?;
let zs = read_f32_block(r)?;
let n = n_atoms.min(xs.len()).min(ys.len()).min(zs.len());
let positions: Vec<[f32; 3]> = (0..n).map(|i| [xs[i], ys[i], zs[i]]).collect();
let types = vec![0u8; n];
Ok(ParticleFrame {
timestep: step,
n_particles: n,
positions,
velocities: None,
types,
})
}
fn write_f32_fortran_block<W: Write>(w: &mut W, data: &[f32]) -> io::Result<()> {
let byte_len = (data.len() * 4) as u32;
w.write_all(&byte_len.to_le_bytes())?;
for &v in data {
w.write_all(&v.to_le_bytes())?;
}
w.write_all(&byte_len.to_le_bytes())?;
Ok(())
}
#[derive(Debug)]
pub struct DcdWriter {
pub path: std::path::PathBuf,
pub n_atoms: u32,
pub delta: f32,
frames_written: u32,
}
impl DcdWriter {
pub fn new(path: impl Into<std::path::PathBuf>, n_atoms: u32, delta: f32) -> io::Result<Self> {
let path = path.into();
let mut writer = Self {
path,
n_atoms,
delta,
frames_written: 0,
};
writer.write_header(0)?;
Ok(writer)
}
pub fn write_frame(&mut self, frame: &ParticleFrame) -> io::Result<()> {
use std::io::Write as _;
let n = self.n_atoms as usize;
let mut xs = Vec::with_capacity(n);
let mut ys = Vec::with_capacity(n);
let mut zs = Vec::with_capacity(n);
for i in 0..n {
if i < frame.positions.len() {
xs.push(frame.positions[i][0]);
ys.push(frame.positions[i][1]);
zs.push(frame.positions[i][2]);
} else {
xs.push(0.0_f32);
ys.push(0.0_f32);
zs.push(0.0_f32);
}
}
let file = OpenOptions::new().append(true).open(&self.path)?;
let mut w = BufWriter::new(file);
write_f32_fortran_block(&mut w, &xs)?;
write_f32_fortran_block(&mut w, &ys)?;
write_f32_fortran_block(&mut w, &zs)?;
w.flush()?;
self.frames_written += 1;
Ok(())
}
pub fn finalize(&self) -> io::Result<()> {
use std::io::{Seek, SeekFrom, Write as _};
let mut file = OpenOptions::new().write(true).open(&self.path)?;
file.seek(SeekFrom::Start(8))?;
file.write_all(&self.frames_written.to_le_bytes())?;
file.flush()
}
fn write_header(&mut self, n_frames: u32) -> io::Result<()> {
use std::io::Write as _;
let mut f = BufWriter::new(File::create(&self.path)?);
let block1_payload: u32 = 84;
f.write_all(&block1_payload.to_le_bytes())?; f.write_all(b"CORD")?; f.write_all(&n_frames.to_le_bytes())?; f.write_all(&0u32.to_le_bytes())?; f.write_all(&1u32.to_le_bytes())?; f.write_all(&0u32.to_le_bytes())?; f.write_all(&0u32.to_le_bytes())?; f.write_all(&0u32.to_le_bytes())?; f.write_all(&0u32.to_le_bytes())?; f.write_all(&0u32.to_le_bytes())?; f.write_all(&self.delta.to_le_bytes())?; for _ in 0..10u32 {
f.write_all(&0u32.to_le_bytes())?;
}
f.write_all(&0u32.to_le_bytes())?;
f.write_all(&block1_payload.to_le_bytes())?;
let title_payload: u32 = 4 + 80; f.write_all(&title_payload.to_le_bytes())?;
f.write_all(&1u32.to_le_bytes())?; let mut title_buf = [b' '; 80];
let label = b"Created by oxiphysics DcdWriter";
let copy_len = label.len().min(80);
title_buf[..copy_len].copy_from_slice(&label[..copy_len]);
f.write_all(&title_buf)?;
f.write_all(&title_payload.to_le_bytes())?;
f.write_all(&4u32.to_le_bytes())?; f.write_all(&self.n_atoms.to_le_bytes())?;
f.write_all(&4u32.to_le_bytes())?;
f.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn make_frame(n: usize) -> ParticleFrame {
let positions: Vec<[f32; 3]> = (0..n).map(|i| [i as f32, 0.0, 0.0]).collect();
let types = vec![1u8; n];
ParticleFrame::new(0, positions, types)
}
fn make_traj(n_frames: usize, n_particles: usize) -> ParticleTrajectory {
let mut traj = ParticleTrajectory::new();
for t in 0..n_frames {
let positions: Vec<[f32; 3]> = (0..n_particles).map(|_| [t as f32, 0.0, 0.0]).collect();
traj.push(ParticleFrame::new(
t as u64,
positions,
vec![0u8; n_particles],
));
}
traj
}
#[test]
fn test_frame_new_sets_n_particles() {
let f = make_frame(5);
assert_eq!(f.n_particles, 5);
}
#[test]
fn test_frame_from_positions() {
let pos = vec![[1.0f32, 0.0, 0.0], [2.0, 0.0, 0.0]];
let f = ParticleFrame::from_positions(10, pos);
assert_eq!(f.n_particles, 2);
assert_eq!(f.timestep, 10);
assert_eq!(f.types, vec![0u8, 0]);
}
#[test]
fn test_frame_center_of_mass_empty() {
let f = make_frame(0);
assert_eq!(f.center_of_mass(), [0.0; 3]);
}
#[test]
fn test_frame_center_of_mass_single() {
let f = ParticleFrame::from_positions(0, vec![[2.0f32, 4.0, 6.0]]);
let com = f.center_of_mass();
assert!((com[0] - 2.0).abs() < 1e-6);
assert!((com[1] - 4.0).abs() < 1e-6);
assert!((com[2] - 6.0).abs() < 1e-6);
}
#[test]
fn test_frame_center_of_mass_symmetric() {
let pos = vec![[-1.0f32, 0.0, 0.0], [1.0, 0.0, 0.0]];
let f = ParticleFrame::from_positions(0, pos);
let com = f.center_of_mass();
assert!(com[0].abs() < 1e-6);
}
#[test]
fn test_frame_bounding_box_empty() {
let f = make_frame(0);
let (lo, hi) = f.bounding_box();
assert_eq!(lo, [0.0; 3]);
assert_eq!(hi, [0.0; 3]);
}
#[test]
fn test_frame_bounding_box_basic() {
let pos = vec![[1.0f32, -1.0, 0.0], [-1.0, 2.0, 3.0]];
let f = ParticleFrame::from_positions(0, pos);
let (lo, hi) = f.bounding_box();
assert!((lo[0] - (-1.0)).abs() < 1e-6);
assert!((hi[1] - 2.0).abs() < 1e-6);
assert!((hi[2] - 3.0).abs() < 1e-6);
}
#[test]
fn test_xyz_write_frame_internal() {
let frame = make_frame(3);
let mut buf = Vec::new();
write_xyz_frame_to(&mut buf, &frame, "test").unwrap();
let s = String::from_utf8(buf).unwrap();
assert!(s.starts_with("3\n"));
assert!(s.contains("test"));
}
#[test]
fn test_xyz_roundtrip_file() {
let path = std::env::temp_dir().join("oxiphysics_pf_xyz_rt.xyz");
let _ = std::fs::remove_file(&path);
let pos = vec![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]];
let frame = ParticleFrame::new(42, pos.clone(), vec![1u8, 2]);
XyzWriter::new(&path).write_frame(&frame, "test").unwrap();
let loaded = XyzReader::new(&path).read_frame().unwrap();
assert_eq!(loaded.n_particles, 2);
assert_eq!(loaded.timestep, 42);
assert!((loaded.positions[0][0] - 1.0).abs() < 1e-4);
assert!((loaded.positions[1][2] - 6.0).abs() < 1e-4);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_xyz_append_and_read_all() {
let path = std::env::temp_dir().join("oxiphysics_pf_xyz_all.xyz");
let _ = std::fs::remove_file(&path);
let w = XyzWriter::new(&path);
for ts in 0..4u64 {
let f = ParticleFrame::new(ts, vec![[ts as f32, 0.0, 0.0]], vec![0u8]);
w.append_frame(&f, "").unwrap();
}
let frames = XyzReader::new(&path).read_all().unwrap();
assert_eq!(frames.len(), 4);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_xyz_write_trajectory() {
let path = std::env::temp_dir().join("oxiphysics_pf_xyz_traj.xyz");
let _ = std::fs::remove_file(&path);
let mut traj = ParticleTrajectory::new();
for ts in 0..3u64 {
traj.push(ParticleFrame::new(ts, vec![[0.0f32; 3]], vec![0u8]));
}
XyzWriter::new(&path).write_trajectory(&traj).unwrap();
let frames = XyzReader::new(&path).read_all().unwrap();
assert_eq!(frames.len(), 3);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_extract_timestep_from_comment() {
assert_eq!(extract_timestep_from_comment("Timestep=100 other"), 100);
assert_eq!(extract_timestep_from_comment("no timestep here"), 0);
}
#[test]
fn test_lammps_dump_write_internal() {
let frame = make_frame(2);
let mut buf = Vec::new();
write_lammps_dump_frame(&mut buf, &frame, [0.0; 3], [10.0; 3], false).unwrap();
let s = String::from_utf8(buf).unwrap();
assert!(s.contains("ITEM: TIMESTEP"));
assert!(s.contains("ITEM: ATOMS id type x y z"));
}
#[test]
fn test_lammps_dump_write_with_velocities() {
let mut frame = make_frame(2);
frame.velocities = Some(vec![[1.0f32, 0.0, 0.0], [0.0, 1.0, 0.0]]);
let mut buf = Vec::new();
write_lammps_dump_frame(&mut buf, &frame, [0.0; 3], [10.0; 3], true).unwrap();
let s = String::from_utf8(buf).unwrap();
assert!(s.contains("vx vy vz"));
}
#[test]
fn test_lammps_dump_roundtrip_file() {
let path = std::env::temp_dir().join("oxiphysics_pf_lmp.lammpstrj");
let _ = std::fs::remove_file(&path);
let w = LammpsDumpWriter::new(&path);
for ts in 0..3u64 {
let f = ParticleFrame::new(ts, vec![[ts as f32, 0.0, 0.0]], vec![1u8]);
w.write_frame(&f, [0.0; 3], [10.0; 3]).unwrap();
}
let frames = LammpsDumpReader::new(&path).read_all().unwrap();
assert_eq!(frames.len(), 3);
assert_eq!(frames[1].timestep, 1);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_lammps_dump_parse_inline() {
let dump = "\
ITEM: TIMESTEP\n\
5\n\
ITEM: NUMBER OF ATOMS\n\
2\n\
ITEM: BOX BOUNDS pp pp pp\n\
0.0 10.0\n\
0.0 10.0\n\
0.0 10.0\n\
ITEM: ATOMS id type x y z\n\
1 1 1.0 2.0 3.0\n\
2 2 4.0 5.0 6.0\n";
let r = BufReader::new(Cursor::new(dump));
let frames = parse_lammps_dump_all(r).unwrap();
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].timestep, 5);
assert_eq!(frames[0].n_particles, 2);
}
#[test]
fn test_trajectory_push_and_get() {
let mut traj = ParticleTrajectory::new();
traj.push(make_frame(3));
assert_eq!(traj.n_frames(), 1);
assert_eq!(traj.n_particles(), 3);
assert!(traj.get(0).is_some());
assert!(traj.get(1).is_none());
}
#[test]
fn test_trajectory_slice() {
let traj = make_traj(10, 2);
let sub = traj.slice(2, 5);
assert_eq!(sub.n_frames(), 3);
}
#[test]
fn test_trajectory_slice_beyond_end() {
let traj = make_traj(5, 2);
let sub = traj.slice(3, 100);
assert_eq!(sub.n_frames(), 2);
}
#[test]
fn test_trajectory_subsample() {
let traj = make_traj(10, 1);
let sub = traj.subsample(2);
assert_eq!(sub.n_frames(), 5);
}
#[test]
fn test_trajectory_subsample_zero_step() {
let traj = make_traj(10, 1);
let sub = traj.subsample(0);
assert_eq!(sub.n_frames(), 0);
}
#[test]
fn test_stats_msd_stationary() {
let mut traj = ParticleTrajectory::new();
for _ in 0..5 {
traj.push(ParticleFrame::from_positions(0, vec![[0.0f32; 3]]));
}
let stats = TrajectoryStats::compute(&traj);
for &m in &stats.msd {
assert!(m.abs() < 1e-8, "msd = {m}");
}
}
#[test]
fn test_stats_msd_drift() {
let mut traj = ParticleTrajectory::new();
for i in 0..5usize {
traj.push(ParticleFrame::from_positions(
i as u64,
vec![[i as f32, 0.0, 0.0]],
));
}
let stats = TrajectoryStats::compute(&traj);
assert!(
(stats.msd[1] - 1.0).abs() < 1e-6,
"msd[1] = {}",
stats.msd[1]
);
}
#[test]
fn test_stats_rmsf_stationary() {
let mut traj = ParticleTrajectory::new();
for _ in 0..10 {
traj.push(ParticleFrame::from_positions(0, vec![[1.0f32, 2.0, 3.0]]));
}
let stats = TrajectoryStats::compute(&traj);
assert!(stats.rmsf[0].abs() < 1e-8);
}
#[test]
fn test_stats_com_drift_single_frame() {
let mut traj = ParticleTrajectory::new();
traj.push(ParticleFrame::from_positions(0, vec![[2.0f32, 4.0, 6.0]]));
let stats = TrajectoryStats::compute(&traj);
let com = stats.com_drift[0];
assert!((com[0] - 2.0).abs() < 1e-6);
}
#[test]
fn test_stats_empty_trajectory() {
let traj = ParticleTrajectory::new();
let stats = TrajectoryStats::compute(&traj);
assert!(stats.msd.is_empty());
assert!(stats.rmsf.is_empty());
assert!(stats.com_drift.is_empty());
}
#[test]
fn test_binary_frame_roundtrip_internal() {
let frame = make_frame(4);
let mut buf = Vec::new();
write_binary_frame(&mut buf, &frame).unwrap();
let mut cursor = Cursor::new(buf);
let loaded = read_binary_frame(&mut cursor).unwrap();
assert_eq!(loaded.n_particles, 4);
for i in 0..4 {
assert!((loaded.positions[i][0] - i as f32).abs() < 1e-6);
}
}
#[test]
fn test_binary_frame_with_velocities() {
let mut frame = make_frame(2);
frame.velocities = Some(vec![[1.0f32, 0.0, 0.0], [0.0, 1.0, 0.0]]);
let mut buf = Vec::new();
write_binary_frame(&mut buf, &frame).unwrap();
let mut cursor = Cursor::new(buf);
let loaded = read_binary_frame(&mut cursor).unwrap();
assert!(loaded.velocities.is_some());
let vels = loaded.velocities.unwrap();
assert!((vels[0][0] - 1.0).abs() < 1e-6);
}
#[test]
fn test_binary_frame_file_roundtrip() {
let path = std::env::temp_dir().join("oxiphysics_pf_bin_rt.bin");
let _ = std::fs::remove_file(&path);
let w = BinaryFrameWriter::new(&path);
for ts in 0..3u64 {
let f = ParticleFrame::new(ts, vec![[ts as f32, 0.0, 0.0]], vec![0u8]);
w.write_frame(&f).unwrap();
}
let frames = BinaryFrameReader::new(&path).read_all().unwrap();
assert_eq!(frames.len(), 3);
assert_eq!(frames[2].timestep, 2);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_binary_frame_bad_magic() {
let buf = vec![0xFFu8, 0xFF, 0xFF, 0xFF];
let mut cursor = Cursor::new(buf);
let result = read_binary_frame(&mut cursor);
assert!(result.is_err());
}
#[test]
fn test_gro_write_creates_file() {
let path = std::env::temp_dir().join("oxiphysics_pf_gro.gro");
let frame = make_frame(3);
GroWriter::new(&path)
.write_frame(&frame, [10.0; 3])
.unwrap();
assert!(path.exists());
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_gro_write_contains_box() {
let path = std::env::temp_dir().join("oxiphysics_pf_gro_box.gro");
let frame = make_frame(1);
GroWriter::new(&path)
.write_frame(&frame, [5.0f32, 5.0, 5.0])
.unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("5.00000"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_gro_roundtrip_positions() {
let path = std::env::temp_dir().join("oxiphysics_pf_gro_rt.gro");
let pos = vec![[1.5f32, 2.5, 3.5], [4.5, 5.5, 6.5]];
let frame = ParticleFrame::new(0, pos.clone(), vec![0u8, 0]);
GroWriter::new(&path)
.write_frame(&frame, [10.0; 3])
.unwrap();
let loaded = GroReader::new(&path).read_frame().unwrap();
assert_eq!(loaded.n_particles, 2);
assert!((loaded.positions[0][0] - 1.5).abs() < 0.01);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_dcd_writer_creates_file() {
let path = std::env::temp_dir().join("oxiphysics_pf_dcd_create.dcd");
let _ = std::fs::remove_file(&path);
let mut w = DcdWriter::new(&path, 3, 2.0e-3_f32).unwrap();
let frame = make_frame(3);
w.write_frame(&frame).unwrap();
w.finalize().unwrap();
assert!(path.exists());
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_dcd_roundtrip_single_frame() {
let path = std::env::temp_dir().join("oxiphysics_pf_dcd_rt1.dcd");
let _ = std::fs::remove_file(&path);
let frame = make_frame(4);
let mut w = DcdWriter::new(&path, 4, 1.0e-3_f32).unwrap();
w.write_frame(&frame).unwrap();
w.finalize().unwrap();
let (header, frames) = DcdReader::new(&path).read_all().unwrap();
assert_eq!(header.n_frames, 1);
assert_eq!(header.n_atoms, 4);
assert_eq!(frames.len(), 1);
for i in 0..4 {
assert!(
(frames[0].positions[i][0] - i as f32).abs() < 1e-5,
"position mismatch at atom {i}"
);
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_dcd_roundtrip_multi_frame() {
let path = std::env::temp_dir().join("oxiphysics_pf_dcd_rt3.dcd");
let _ = std::fs::remove_file(&path);
let n_atoms = 5u32;
let mut w = DcdWriter::new(&path, n_atoms, 2.0e-3_f32).unwrap();
for ts in 0..3u64 {
let pos: Vec<[f32; 3]> = (0..n_atoms as usize)
.map(|i| [ts as f32 + i as f32, 0.0, 0.0])
.collect();
let types = vec![0u8; n_atoms as usize];
let frame = ParticleFrame::new(ts, pos, types);
w.write_frame(&frame).unwrap();
}
w.finalize().unwrap();
let (header, frames) = DcdReader::new(&path).read_all().unwrap();
assert_eq!(header.n_frames, 3);
assert_eq!(frames.len(), 3);
assert!((frames[2].positions[0][0] - 2.0).abs() < 1e-5);
let _ = std::fs::remove_file(&path);
}
}