use crate::parser::{parse_declared_sections, parse_single_frame, LineStream};
use crate::{error, types};
use std::path::Path;
pub struct MemchrLines<'a> {
bytes: &'a [u8],
pos: usize,
peeked: Option<&'a str>,
}
impl<'a> MemchrLines<'a> {
pub fn new(text: &'a str) -> Self {
Self {
bytes: text.as_bytes(),
pos: 0,
peeked: None,
}
}
#[inline]
fn read_one(&mut self) -> Option<&'a str> {
if self.pos >= self.bytes.len() {
return None;
}
let rest = &self.bytes[self.pos..];
let (line_bytes, advance) = match memchr::memchr(b'\n', rest) {
Some(i) => (&rest[..i], i + 1),
None => (rest, rest.len()),
};
self.pos += advance;
let trimmed = if line_bytes.last() == Some(&b'\r') {
&line_bytes[..line_bytes.len() - 1]
} else {
line_bytes
};
Some(unsafe { std::str::from_utf8_unchecked(trimmed) })
}
#[inline]
pub fn next_line(&mut self) -> Option<&'a str> {
if let Some(p) = self.peeked.take() {
return Some(p);
}
self.read_one()
}
#[inline]
pub fn peek_line(&mut self) -> Option<&'a str> {
if self.peeked.is_none() {
self.peeked = self.read_one();
}
self.peeked
}
fn clear_peek(&mut self) {
if let Some(p) = self.peeked.take() {
let start = p.as_ptr() as usize - self.bytes.as_ptr() as usize;
self.pos = start;
}
}
}
impl<'a> Iterator for MemchrLines<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
self.next_line()
}
}
impl<'a> LineStream<'a> for MemchrLines<'a> {
#[inline]
fn next_line(&mut self) -> Option<&'a str> {
MemchrLines::next_line(self)
}
#[inline]
fn peek_line(&mut self) -> Option<&'a str> {
MemchrLines::peek_line(self)
}
}
pub struct ConFrameIterator<'a> {
pub(crate) lines: MemchrLines<'a>,
}
impl<'a> ConFrameIterator<'a> {
pub fn new(file_contents: &'a str) -> Self {
ConFrameIterator {
lines: MemchrLines::new(file_contents),
}
}
fn advance_lines(&mut self, n: usize) -> Result<(), error::ParseError> {
self.lines.clear_peek();
for _ in 0..n {
let rest = &self.lines.bytes[self.lines.pos..];
match memchr::memchr(b'\n', rest) {
Some(pos) => self.lines.pos += pos + 1,
None => {
if rest.is_empty() {
return Err(error::ParseError::IncompleteFrame);
}
self.lines.pos = self.lines.bytes.len();
return Err(error::ParseError::IncompleteFrame);
}
}
}
Ok(())
}
fn read_line_str(&mut self) -> Option<&'a str> {
self.lines.clear_peek();
self.lines.next_line()
}
pub fn forward_fast(&mut self) -> Option<Result<(), error::ParseError>> {
self.lines.clear_peek();
if self.lines.pos >= self.lines.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));
}
self.lines.clear_peek();
loop {
let rest = &self.lines.bytes[self.lines.pos..];
if rest.is_empty() {
break;
}
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 {
break;
}
self.lines.pos += 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.forward_fast()
}
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()?;
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_line() {
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_line()?;
let mut frame = match parse_single_frame(&mut self.lines) {
Ok(f) => f,
Err(e) => return Some(Err(e)),
};
let sections = match parse_declared_sections(
&mut self.lines,
&mut frame.header,
&mut frame.atom_data,
) {
Ok(n) => n,
Err(e) => return Some(Err(e)),
};
if sections > 0 {
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")
);
}
}
#[cfg(feature = "parallel")]
pub(crate) const PARALLEL_BYTES_THRESHOLD: usize = 48 * 1024;
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()?;
#[cfg(feature = "parallel")]
{
if text.len() >= PARALLEL_BYTES_THRESHOLD {
let parts = parse_frames_parallel(text);
let mut frames = Vec::with_capacity(parts.len());
for r in parts {
frames.push(r?);
}
return Ok(frames);
}
}
let iter = ConFrameIterator::new(text);
let frames: Result<Vec<_>, _> = iter.collect();
Ok(frames?)
}
pub fn count_frames(path: &Path) -> Result<usize, Box<dyn std::error::Error>> {
let contents = crate::compression::read_file_contents(path)?;
let text = contents.as_str()?;
let mut n = 0usize;
let mut iter = ConFrameIterator::new(text);
loop {
match iter.forward_fast() {
Some(Ok(())) => n += 1,
Some(Err(e)) => return Err(Box::new(e)),
None => break,
}
}
Ok(n)
}
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>> {
parse_frames_parallel_with_threads(file_contents, None)
}
#[cfg(feature = "parallel")]
pub fn parse_frames_parallel_with_threads(
file_contents: &str,
num_threads: Option<usize>,
) -> Vec<Result<types::ConFrame, error::ParseError>> {
use rayon::prelude::*;
let mut boundaries: Vec<usize> = Vec::new();
let mut scanner = ConFrameIterator::new(file_contents);
loop {
scanner.lines.clear_peek();
let start = scanner.lines.pos;
if start >= scanner.lines.bytes.len() {
break;
}
boundaries.push(start);
match scanner.forward_fast() {
Some(Ok(())) => {}
Some(Err(_)) | None => break,
}
}
let parse_chunks = || {
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()
};
match num_threads {
None => parse_chunks(),
Some(n) => {
let n = n.max(1);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(n)
.build()
.expect("rayon pool");
pool.install(parse_chunks)
}
}
}
#[cfg(all(test, feature = "parallel"))]
mod parallel_strong_scale_tests {
use super::*;
use std::path::PathBuf;
fn multi_frame_fixture() -> String {
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test/tiny_cuh2.con");
let one = std::fs::read_to_string(p).expect("fixture");
one.repeat(8)
}
fn sequential_frames(text: &str) -> Vec<types::ConFrame> {
ConFrameIterator::new(text)
.map(|r| r.expect("seq frame"))
.collect()
}
fn frames_payload_key(f: &types::ConFrame) -> (usize, Vec<(String, f64, f64, f64)>) {
let atoms: Vec<_> = f
.atom_data
.iter()
.map(|a| (a.symbol.to_string(), a.x, a.y, a.z))
.collect();
(f.atom_data.len(), atoms)
}
#[test]
fn parallel_workers_match_sequential_payloads() {
let text = multi_frame_fixture();
let seq = sequential_frames(&text);
assert!(seq.len() >= 8);
let seq_keys: Vec<_> = seq.iter().map(frames_payload_key).collect();
for workers in [1usize, 2, 4] {
let par = parse_frames_parallel_with_threads(&text, Some(workers));
assert_eq!(par.len(), seq.len(), "workers={workers}");
let par_keys: Vec<_> = par
.into_iter()
.map(|r| frames_payload_key(&r.expect("par frame")))
.collect();
assert_eq!(par_keys, seq_keys, "workers={workers} frame payloads");
}
let par_default = parse_frames_parallel(&text);
let def_keys: Vec<_> = par_default
.into_iter()
.map(|r| frames_payload_key(&r.expect("par")))
.collect();
assert_eq!(def_keys, seq_keys);
}
}