1use crate::parser::{parse_declared_sections, parse_single_frame, LineStream};
6use crate::{error, types};
7use std::path::Path;
8
9pub struct MemchrLines<'a> {
16 bytes: &'a [u8],
17 pos: usize,
18 peeked: Option<&'a str>,
19}
20
21impl<'a> MemchrLines<'a> {
22 pub fn new(text: &'a str) -> Self {
23 Self {
24 bytes: text.as_bytes(),
25 pos: 0,
26 peeked: None,
27 }
28 }
29
30 #[inline]
31 fn read_one(&mut self) -> Option<&'a str> {
32 if self.pos >= self.bytes.len() {
33 return None;
34 }
35 let rest = &self.bytes[self.pos..];
36 let (line_bytes, advance) = match memchr::memchr(b'\n', rest) {
37 Some(i) => (&rest[..i], i + 1),
38 None => (rest, rest.len()),
39 };
40 self.pos += advance;
41 let trimmed = if line_bytes.last() == Some(&b'\r') {
42 &line_bytes[..line_bytes.len() - 1]
43 } else {
44 line_bytes
45 };
46 Some(unsafe { std::str::from_utf8_unchecked(trimmed) })
48 }
49
50 #[inline]
51 pub fn next_line(&mut self) -> Option<&'a str> {
52 if let Some(p) = self.peeked.take() {
53 return Some(p);
54 }
55 self.read_one()
56 }
57
58 #[inline]
59 pub fn peek_line(&mut self) -> Option<&'a str> {
60 if self.peeked.is_none() {
61 self.peeked = self.read_one();
62 }
63 self.peeked
64 }
65
66 fn clear_peek(&mut self) {
68 if let Some(p) = self.peeked.take() {
69 let start = p.as_ptr() as usize - self.bytes.as_ptr() as usize;
71 self.pos = start;
72 }
73 }
74}
75
76impl<'a> Iterator for MemchrLines<'a> {
77 type Item = &'a str;
78 fn next(&mut self) -> Option<&'a str> {
79 self.next_line()
80 }
81}
82
83impl<'a> LineStream<'a> for MemchrLines<'a> {
84 #[inline]
85 fn next_line(&mut self) -> Option<&'a str> {
86 MemchrLines::next_line(self)
87 }
88 #[inline]
89 fn peek_line(&mut self) -> Option<&'a str> {
90 MemchrLines::peek_line(self)
91 }
92}
93
94pub struct ConFrameIterator<'a> {
105 pub(crate) lines: MemchrLines<'a>,
106}
107
108impl<'a> ConFrameIterator<'a> {
109 pub fn new(file_contents: &'a str) -> Self {
115 ConFrameIterator {
116 lines: MemchrLines::new(file_contents),
117 }
118 }
119
120 fn advance_lines(&mut self, n: usize) -> Result<(), error::ParseError> {
122 self.lines.clear_peek();
123 for _ in 0..n {
124 let rest = &self.lines.bytes[self.lines.pos..];
125 match memchr::memchr(b'\n', rest) {
126 Some(pos) => self.lines.pos += pos + 1,
127 None => {
128 if rest.is_empty() {
129 return Err(error::ParseError::IncompleteFrame);
130 }
131 self.lines.pos = self.lines.bytes.len();
132 return Err(error::ParseError::IncompleteFrame);
133 }
134 }
135 }
136 Ok(())
137 }
138
139 fn read_line_str(&mut self) -> Option<&'a str> {
141 self.lines.clear_peek();
142 self.lines.next_line()
143 }
144
145 pub fn forward_fast(&mut self) -> Option<Result<(), error::ParseError>> {
149 self.lines.clear_peek();
150 if self.lines.pos >= self.lines.bytes.len() {
151 return None;
152 }
153 if let Err(e) = self.advance_lines(6) {
155 return Some(Err(e));
156 }
157 let natm_types: usize = match self.read_line_str() {
159 Some(line) => match crate::parser::parse_line_of_n::<usize>(line, 1) {
160 Ok(v) => v[0],
161 Err(e) => return Some(Err(e)),
162 },
163 None => return Some(Err(error::ParseError::IncompleteHeader)),
164 };
165 let natms_per_type: Vec<usize> = match self.read_line_str() {
167 Some(line) => match crate::parser::parse_line_of_n(line, natm_types) {
168 Ok(v) => v,
169 Err(e) => return Some(Err(e)),
170 },
171 None => return Some(Err(error::ParseError::IncompleteHeader)),
172 };
173 if let Err(e) = self.advance_lines(1) {
175 return Some(Err(e));
176 }
177 let total_atoms: usize = natms_per_type.iter().sum();
178 let coord_block_lines = total_atoms + natm_types * 2;
179 if let Err(e) = self.advance_lines(coord_block_lines) {
180 return Some(Err(e));
181 }
182 self.lines.clear_peek();
184 loop {
185 let rest = &self.lines.bytes[self.lines.pos..];
186 if rest.is_empty() {
187 break;
188 }
189 let next_eol = memchr::memchr(b'\n', rest);
190 let line = match next_eol {
191 Some(pos) => &rest[..pos],
192 None => rest,
193 };
194 let is_blank = line.iter().all(|b| matches!(b, b' ' | b'\t' | b'\r'));
195 if !is_blank {
196 break;
197 }
198 self.lines.pos += next_eol.map(|p| p + 1).unwrap_or(rest.len());
200 if let Err(e) = self.advance_lines(coord_block_lines) {
201 return Some(Err(e));
202 }
203 }
204 Some(Ok(()))
205 }
206
207 pub fn forward(&mut self) -> Option<Result<(), error::ParseError>> {
219 self.forward_fast()
221 }
222
223 pub fn next_with_raw_span(
236 &mut self,
237 file_contents: &'a str,
238 ) -> Option<Result<(types::ConFrame, &'a str), error::ParseError>> {
239 let base = file_contents.as_ptr() as usize;
240 let start = {
241 let line = self.lines.peek_line()?;
242 line.as_ptr() as usize - base
243 };
244 let frame = match self.next()? {
245 Ok(f) => f,
246 Err(e) => return Some(Err(e)),
247 };
248 let end = match self.lines.peek_line() {
249 Some(line) => line.as_ptr() as usize - base,
250 None => file_contents.len(),
251 };
252 debug_assert!(end >= start && end <= file_contents.len());
253 Some(Ok((frame, &file_contents[start..end])))
254 }
255}
256
257impl<'a> Iterator for ConFrameIterator<'a> {
258 type Item = Result<types::ConFrame, error::ParseError>;
263
264 fn next(&mut self) -> Option<Self::Item> {
270 self.lines.peek_line()?;
272 let mut frame = match parse_single_frame(&mut self.lines) {
274 Ok(f) => f,
275 Err(e) => return Some(Err(e)),
276 };
277 let sections = match parse_declared_sections(
281 &mut self.lines,
282 &mut frame.header,
283 &mut frame.atom_data,
284 ) {
285 Ok(n) => n,
286 Err(e) => return Some(Err(e)),
287 };
288 if sections > 0 {
289 frame.sync_arrays_from_atom_data();
290 }
291 Some(Ok(frame))
292 }
293}
294
295#[cfg(test)]
296mod aos_soa_agreement_tests {
297 use super::*;
298 use std::path::PathBuf;
299
300 #[test]
301 fn iterator_vel_forces_soa_matches_aos() {
302 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
303 .join("resources/test/tiny_cuh2_vel_forces.con");
304 let text = std::fs::read_to_string(&p).expect("fixture");
305 let fr = ConFrameIterator::new(&text)
306 .next()
307 .expect("frame")
308 .expect("parse");
309 let n = fr.atom_data.len();
310 assert!(n > 0);
311 assert_eq!(fr.positions.nrows(), n);
312 let has_vel = fr.atom_data.iter().any(|a| a.velocity.is_some());
313 let has_frc = fr.atom_data.iter().any(|a| a.force.is_some());
314 if has_vel {
315 assert_eq!(
316 fr.velocities.nrows(),
317 n,
318 "SoA velocities must match AoS after section parse"
319 );
320 }
321 if has_frc {
322 assert_eq!(fr.forces.nrows(), n, "SoA forces must match AoS");
323 }
324 for (i, a) in fr.atom_data.iter().enumerate() {
325 let p = fr.positions.as_f64_row(i);
326 assert_eq!([a.x, a.y, a.z], p);
327 if let Some(v) = a.velocity {
328 assert_eq!(v, fr.velocities.as_f64_row(i));
329 }
330 if let Some(f) = a.force {
331 assert_eq!(f, fr.forces.as_f64_row(i));
332 }
333 }
334 }
335
336 #[test]
339 fn sync_skips_pos_when_nrows_matches_keeps_force_soa() {
340 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
341 .join("resources/test/tiny_cuh2_forces.con");
342 let text = std::fs::read_to_string(&p).expect("fixture");
343 let fr = ConFrameIterator::new(&text)
344 .next()
345 .expect("frame")
346 .expect("parse");
347 let n = fr.atom_data.len();
348 assert_eq!(fr.positions.nrows(), n);
349 assert_eq!(fr.forces.nrows(), n);
350 let p0 = fr.positions.as_f64_row(0);
353 let mut fr2 = fr.clone();
354 fr2.sync_arrays_from_atom_data();
355 assert_eq!(fr2.positions.as_f64_row(0), p0);
356 assert_eq!(fr2.forces.nrows(), n);
357 assert_eq!(
358 fr2.forces.as_f64_row(0),
359 fr2.atom_data[0].force.expect("force")
360 );
361 }
362}
363
364#[cfg(feature = "parallel")]
375pub(crate) const PARALLEL_BYTES_THRESHOLD: usize = 48 * 1024;
376
377pub fn read_all_frames(path: &Path) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
378 let contents = crate::compression::read_file_contents(path)?;
379 let text = contents.as_str()?;
380 #[cfg(feature = "parallel")]
381 {
382 if text.len() >= PARALLEL_BYTES_THRESHOLD {
383 let parts = parse_frames_parallel(text);
384 let mut frames = Vec::with_capacity(parts.len());
385 for r in parts {
386 frames.push(r?);
387 }
388 return Ok(frames);
389 }
390 }
391 let iter = ConFrameIterator::new(text);
392 let frames: Result<Vec<_>, _> = iter.collect();
393 Ok(frames?)
394}
395
396pub fn count_frames(path: &Path) -> Result<usize, Box<dyn std::error::Error>> {
401 let contents = crate::compression::read_file_contents(path)?;
402 let text = contents.as_str()?;
403 let mut n = 0usize;
404 let mut iter = ConFrameIterator::new(text);
405 loop {
406 match iter.forward_fast() {
407 Some(Ok(())) => n += 1,
408 Some(Err(e)) => return Err(Box::new(e)),
409 None => break,
410 }
411 }
412 Ok(n)
413}
414
415pub fn read_first_frame(path: &Path) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
420 let contents = crate::compression::read_file_contents(path)?;
421 let text = contents.as_str()?;
422 let mut iter = ConFrameIterator::new(text);
423 match iter.next() {
424 Some(Ok(frame)) => Ok(frame),
425 Some(Err(e)) => Err(Box::new(e)),
426 None => Err("No frames found in file".into()),
427 }
428}
429
430#[cfg(feature = "parallel")]
445pub fn parse_frames_parallel(
446 file_contents: &str,
447) -> Vec<Result<types::ConFrame, error::ParseError>> {
448 parse_frames_parallel_with_threads(file_contents, None)
449}
450
451#[cfg(feature = "parallel")]
460pub fn parse_frames_parallel_with_threads(
461 file_contents: &str,
462 num_threads: Option<usize>,
463) -> Vec<Result<types::ConFrame, error::ParseError>> {
464 use rayon::prelude::*;
465
466 let mut boundaries: Vec<usize> = Vec::new();
469 let mut scanner = ConFrameIterator::new(file_contents);
470 loop {
471 scanner.lines.clear_peek();
472 let start = scanner.lines.pos;
473 if start >= scanner.lines.bytes.len() {
474 break;
475 }
476 boundaries.push(start);
477 match scanner.forward_fast() {
478 Some(Ok(())) => {}
479 Some(Err(_)) | None => break,
480 }
481 }
482
483 let parse_chunks = || {
484 let num_frames = boundaries.len();
485 (0..num_frames)
486 .into_par_iter()
487 .map(|i| {
488 let start = boundaries[i];
489 let end = if i + 1 < num_frames {
490 boundaries[i + 1]
491 } else {
492 file_contents.len()
493 };
494 let chunk = &file_contents[start..end];
495 let mut iter = ConFrameIterator::new(chunk);
496 match iter.next() {
497 Some(result) => result,
498 None => Err(error::ParseError::IncompleteFrame),
499 }
500 })
501 .collect()
502 };
503
504 match num_threads {
505 None => parse_chunks(),
506 Some(n) => {
507 let n = n.max(1);
508 let pool = rayon::ThreadPoolBuilder::new()
509 .num_threads(n)
510 .build()
511 .expect("rayon pool");
512 pool.install(parse_chunks)
513 }
514 }
515}
516
517#[cfg(all(test, feature = "parallel"))]
518mod parallel_strong_scale_tests {
519 use super::*;
520 use std::path::PathBuf;
521
522 fn multi_frame_fixture() -> String {
523 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
524 .join("resources/test/tiny_cuh2.con");
525 let one = std::fs::read_to_string(p).expect("fixture");
526 one.repeat(8)
528 }
529
530 fn sequential_frames(text: &str) -> Vec<types::ConFrame> {
531 ConFrameIterator::new(text)
532 .map(|r| r.expect("seq frame"))
533 .collect()
534 }
535
536 fn frames_payload_key(f: &types::ConFrame) -> (usize, Vec<(String, f64, f64, f64)>) {
537 let atoms: Vec<_> = f
538 .atom_data
539 .iter()
540 .map(|a| (a.symbol.to_string(), a.x, a.y, a.z))
541 .collect();
542 (f.atom_data.len(), atoms)
543 }
544
545 #[test]
546 fn parallel_workers_match_sequential_payloads() {
547 let text = multi_frame_fixture();
548 let seq = sequential_frames(&text);
549 assert!(seq.len() >= 8);
550 let seq_keys: Vec<_> = seq.iter().map(frames_payload_key).collect();
551
552 for workers in [1usize, 2, 4] {
553 let par = parse_frames_parallel_with_threads(&text, Some(workers));
554 assert_eq!(par.len(), seq.len(), "workers={workers}");
555 let par_keys: Vec<_> = par
556 .into_iter()
557 .map(|r| frames_payload_key(&r.expect("par frame")))
558 .collect();
559 assert_eq!(par_keys, seq_keys, "workers={workers} frame payloads");
560 }
561
562 let par_default = parse_frames_parallel(&text);
564 let def_keys: Vec<_> = par_default
565 .into_iter()
566 .map(|r| frames_payload_key(&r.expect("par")))
567 .collect();
568 assert_eq!(def_keys, seq_keys);
569 }
570}