use crate::parser::{parse_declared_sections, parse_single_frame};
use crate::{error, types};
use std::iter::Peekable;
use std::path::Path;
pub struct ConFrameIterator<'a> {
pub(crate) lines: Peekable<std::str::Lines<'a>>,
bytes: &'a [u8],
cursor: usize,
}
impl<'a> ConFrameIterator<'a> {
pub fn new(file_contents: &'a str) -> Self {
ConFrameIterator {
lines: file_contents.lines().peekable(),
bytes: file_contents.as_bytes(),
cursor: 0,
}
}
fn advance_lines(&mut self, n: usize) -> Result<(), error::ParseError> {
for _ in 0..n {
let rest = &self.bytes[self.cursor..];
match memchr::memchr(b'\n', rest) {
Some(pos) => self.cursor += pos + 1,
None => {
if rest.is_empty() {
return Err(error::ParseError::IncompleteFrame);
}
self.cursor = self.bytes.len();
return Err(error::ParseError::IncompleteFrame);
}
}
}
Ok(())
}
fn read_line_str(&mut self) -> Option<&'a str> {
let rest = &self.bytes[self.cursor..];
if rest.is_empty() {
return None;
}
let (line_bytes, advance) = match memchr::memchr(b'\n', rest) {
Some(pos) => (&rest[..pos], pos + 1),
None => (rest, rest.len()),
};
let trimmed = if line_bytes.last() == Some(&b'\r') {
&line_bytes[..line_bytes.len() - 1]
} else {
line_bytes
};
let line = unsafe { std::str::from_utf8_unchecked(trimmed) };
self.cursor += advance;
Some(line)
}
pub fn forward_fast(&mut self) -> Option<Result<(), error::ParseError>> {
if self.cursor >= self.bytes.len() {
return None;
}
if let Err(e) = self.advance_lines(6) {
return Some(Err(e));
}
let natm_types: usize = match self.read_line_str() {
Some(line) => match crate::parser::parse_line_of_n::<usize>(line, 1) {
Ok(v) => v[0],
Err(e) => return Some(Err(e)),
},
None => return Some(Err(error::ParseError::IncompleteHeader)),
};
let natms_per_type: Vec<usize> = match self.read_line_str() {
Some(line) => match crate::parser::parse_line_of_n(line, natm_types) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
},
None => return Some(Err(error::ParseError::IncompleteHeader)),
};
if let Err(e) = self.advance_lines(1) {
return Some(Err(e));
}
let total_atoms: usize = natms_per_type.iter().sum();
let coord_block_lines = total_atoms + natm_types * 2;
if let Err(e) = self.advance_lines(coord_block_lines) {
return Some(Err(e));
}
loop {
let rest = &self.bytes[self.cursor..];
let next_eol = memchr::memchr(b'\n', rest);
let line = match next_eol {
Some(pos) => &rest[..pos],
None => rest,
};
let is_blank = line
.iter()
.all(|b| matches!(b, b' ' | b'\t' | b'\r'));
if !is_blank || line.is_empty() && self.cursor >= self.bytes.len() {
break;
}
if !is_blank {
break;
}
self.cursor += next_eol.map(|p| p + 1).unwrap_or(rest.len());
if let Err(e) = self.advance_lines(coord_block_lines) {
return Some(Err(e));
}
}
Some(Ok(()))
}
pub fn forward(&mut self) -> Option<Result<(), error::ParseError>> {
self.lines.peek()?;
for _ in 0..6 {
if self.lines.next().is_none() {
return Some(Err(error::ParseError::IncompleteHeader));
}
}
let natm_types: usize = match self.lines.next() {
Some(line) => match crate::parser::parse_line_of_n::<usize>(line, 1) {
Ok(v) => v[0],
Err(e) => return Some(Err(e)),
},
None => return Some(Err(error::ParseError::IncompleteHeader)),
};
let natms_per_type: Vec<usize> = match self.lines.next() {
Some(line) => match crate::parser::parse_line_of_n(line, natm_types) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
},
None => return Some(Err(error::ParseError::IncompleteHeader)),
};
if self.lines.next().is_none() {
return Some(Err(error::ParseError::IncompleteHeader));
}
let total_atoms: usize = natms_per_type.iter().sum();
let non_atom_lines = natm_types * 2;
let lines_to_skip = total_atoms + non_atom_lines;
for _ in 0..lines_to_skip {
if self.lines.next().is_none() {
return Some(Err(error::ParseError::IncompleteFrame));
}
}
loop {
match self.lines.peek() {
Some(line) if line.trim().is_empty() => {
self.lines.next(); let section_lines = total_atoms + non_atom_lines;
for _ in 0..section_lines {
if self.lines.next().is_none() {
return Some(Err(error::ParseError::IncompleteFrame));
}
}
}
_ => break,
}
}
Some(Ok(()))
}
pub fn next_with_raw_span(
&mut self,
file_contents: &'a str,
) -> Option<Result<(types::ConFrame, &'a str), error::ParseError>> {
let base = file_contents.as_ptr() as usize;
let start = {
let line = self.lines.peek()?;
line.as_ptr() as usize - base
};
let frame = match self.next()? {
Ok(f) => f,
Err(e) => return Some(Err(e)),
};
let end = match self.lines.peek() {
Some(line) => line.as_ptr() as usize - base,
None => file_contents.len(),
};
debug_assert!(end >= start && end <= file_contents.len());
Some(Ok((frame, &file_contents[start..end])))
}
}
impl<'a> Iterator for ConFrameIterator<'a> {
type Item = Result<types::ConFrame, error::ParseError>;
fn next(&mut self) -> Option<Self::Item> {
self.lines.peek()?;
let mut frame = match parse_single_frame(&mut self.lines) {
Ok(f) => f,
Err(e) => return Some(Err(e)),
};
match parse_declared_sections(&mut self.lines, &mut frame.header, &mut frame.atom_data) {
Ok(_) => {}
Err(e) => return Some(Err(e)),
}
frame.sync_arrays_from_atom_data();
Some(Ok(frame))
}
}
#[cfg(test)]
mod aos_soa_agreement_tests {
use super::*;
use std::path::PathBuf;
#[test]
fn iterator_vel_forces_soa_matches_aos() {
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/tiny_cuh2_vel_forces.con");
let text = std::fs::read_to_string(&p).expect("fixture");
let fr = ConFrameIterator::new(&text)
.next()
.expect("frame")
.expect("parse");
let n = fr.atom_data.len();
assert!(n > 0);
assert_eq!(fr.positions.nrows(), n);
let has_vel = fr.atom_data.iter().any(|a| a.velocity.is_some());
let has_frc = fr.atom_data.iter().any(|a| a.force.is_some());
if has_vel {
assert_eq!(
fr.velocities.nrows(),
n,
"SoA velocities must match AoS after section parse"
);
}
if has_frc {
assert_eq!(fr.forces.nrows(), n, "SoA forces must match AoS");
}
for (i, a) in fr.atom_data.iter().enumerate() {
let p = fr.positions.as_f64_row(i);
assert_eq!([a.x, a.y, a.z], p);
if let Some(v) = a.velocity {
assert_eq!(v, fr.velocities.as_f64_row(i));
}
if let Some(f) = a.force {
assert_eq!(f, fr.forces.as_f64_row(i));
}
}
}
#[test]
fn sync_skips_pos_when_nrows_matches_keeps_force_soa() {
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/tiny_cuh2_forces.con");
let text = std::fs::read_to_string(&p).expect("fixture");
let fr = ConFrameIterator::new(&text)
.next()
.expect("frame")
.expect("parse");
let n = fr.atom_data.len();
assert_eq!(fr.positions.nrows(), n);
assert_eq!(fr.forces.nrows(), n);
let p0 = fr.positions.as_f64_row(0);
let mut fr2 = fr.clone();
fr2.sync_arrays_from_atom_data();
assert_eq!(fr2.positions.as_f64_row(0), p0);
assert_eq!(fr2.forces.nrows(), n);
assert_eq!(
fr2.forces.as_f64_row(0),
fr2.atom_data[0].force.expect("force")
);
}
}
pub fn read_all_frames(path: &Path) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
let contents = crate::compression::read_file_contents(path)?;
let text = contents.as_str()?;
let iter = ConFrameIterator::new(text);
let frames: Result<Vec<_>, _> = iter.collect();
Ok(frames?)
}
pub fn read_first_frame(path: &Path) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
let contents = crate::compression::read_file_contents(path)?;
let text = contents.as_str()?;
let mut iter = ConFrameIterator::new(text);
match iter.next() {
Some(Ok(frame)) => Ok(frame),
Some(Err(e)) => Err(Box::new(e)),
None => Err("No frames found in file".into()),
}
}
#[cfg(feature = "parallel")]
pub fn parse_frames_parallel(
file_contents: &str,
) -> Vec<Result<types::ConFrame, error::ParseError>> {
use rayon::prelude::*;
let mut boundaries: Vec<usize> = Vec::new();
let mut scanner = ConFrameIterator::new(file_contents);
loop {
let start = scanner.cursor;
if start >= scanner.bytes.len() {
break;
}
boundaries.push(start);
match scanner.forward_fast() {
Some(Ok(())) => {}
Some(Err(_)) | None => break,
}
}
let num_frames = boundaries.len();
(0..num_frames)
.into_par_iter()
.map(|i| {
let start = boundaries[i];
let end = if i + 1 < num_frames {
boundaries[i + 1]
} else {
file_contents.len()
};
let chunk = &file_contents[start..end];
let mut iter = ConFrameIterator::new(chunk);
match iter.next() {
Some(result) => result,
None => Err(error::ParseError::IncompleteFrame),
}
})
.collect()
}