1use crate::parser::{LineStream, parse_declared_sections, parse_single_frame};
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 fn remainder_is_whitespace(&mut self) -> bool {
93 if let Some(line) = self.peek_line()
94 && !line.trim().is_empty()
95 {
96 return false;
97 }
98 self.bytes[self.pos..]
99 .iter()
100 .all(|b| b.is_ascii_whitespace())
101 }
102}
103
104pub struct ConFrameIterator<'a> {
115 pub(crate) lines: MemchrLines<'a>,
116}
117
118impl<'a> ConFrameIterator<'a> {
119 pub fn new(file_contents: &'a str) -> Self {
125 ConFrameIterator {
126 lines: MemchrLines::new(file_contents),
127 }
128 }
129
130 fn advance_lines(&mut self, n: usize) -> Result<(), error::ParseError> {
132 self.lines.clear_peek();
133 for _ in 0..n {
134 let rest = &self.lines.bytes[self.lines.pos..];
135 match memchr::memchr(b'\n', rest) {
136 Some(pos) => self.lines.pos += pos + 1,
137 None => {
138 if rest.is_empty() {
139 return Err(error::ParseError::IncompleteFrame);
140 }
141 self.lines.pos = self.lines.bytes.len();
142 return Err(error::ParseError::IncompleteFrame);
143 }
144 }
145 }
146 Ok(())
147 }
148
149 fn read_line_str(&mut self) -> Option<&'a str> {
151 self.lines.clear_peek();
152 self.lines.next_line()
153 }
154
155 pub fn forward_fast(&mut self) -> Option<Result<(), error::ParseError>> {
159 self.lines.clear_peek();
160 if self.lines.pos >= self.lines.bytes.len() {
161 return None;
162 }
163 if let Err(e) = self.advance_lines(6) {
165 return Some(Err(e));
166 }
167 let natm_types: usize = match self.read_line_str() {
169 Some(line) => match crate::parser::parse_count_line(line, 1) {
170 Ok(v) => v[0],
171 Err(e) => return Some(Err(e)),
172 },
173 None => return Some(Err(error::ParseError::IncompleteHeader)),
174 };
175 if natm_types == 0 {
176 return Some(crate::parser::read_zero_atom_tail(|| self.read_line_str()));
179 }
180 let natms_per_type: Vec<usize> = match self.read_line_str() {
182 Some(line) => match crate::parser::parse_count_line(line, natm_types) {
183 Ok(v) => v,
184 Err(e) => return Some(Err(e)),
185 },
186 None => return Some(Err(error::ParseError::IncompleteHeader)),
187 };
188 if natms_per_type.iter().any(|n| *n == 0) {
189 return Some(Err(error::ParseError::InvalidCount(
190 "each per-type count must be >= 1".into(),
191 )));
192 }
193 if let Err(e) = self.advance_lines(1) {
195 return Some(Err(e));
196 }
197 let total_atoms: usize = natms_per_type.iter().sum();
198 let coord_block_lines = total_atoms + natm_types * 2;
199 if let Err(e) = self.advance_lines(coord_block_lines) {
200 return Some(Err(e));
201 }
202 self.lines.clear_peek();
204 loop {
205 let rest = &self.lines.bytes[self.lines.pos..];
206 if rest.is_empty() {
207 break;
208 }
209 let next_eol = memchr::memchr(b'\n', rest);
210 let line = match next_eol {
211 Some(pos) => &rest[..pos],
212 None => rest,
213 };
214 let is_blank = line.iter().all(|b| matches!(b, b' ' | b'\t' | b'\r'));
215 if !is_blank {
216 break;
217 }
218 let after = match next_eol {
219 Some(pos) => &rest[pos + 1..],
220 None => &[][..],
221 };
222 if after.iter().all(|b| b.is_ascii_whitespace()) {
225 self.lines.pos = self.lines.bytes.len();
226 break;
227 }
228 self.lines.pos += next_eol.map(|p| p + 1).unwrap_or(rest.len());
230 if let Err(e) = self.advance_lines(coord_block_lines) {
231 return Some(Err(e));
232 }
233 }
234 Some(Ok(()))
235 }
236
237 pub fn forward(&mut self) -> Option<Result<(), error::ParseError>> {
249 self.forward_fast()
251 }
252
253 pub fn byte_pos(&self) -> usize {
255 self.lines.pos
256 }
257
258 pub fn skip_frames(&mut self, n: usize) -> Result<usize, error::ParseError> {
261 let mut skipped = 0usize;
262 for _ in 0..n {
263 match self.forward() {
264 None => break,
265 Some(Err(e)) => return Err(e),
266 Some(Ok(())) => skipped += 1,
267 }
268 }
269 Ok(skipped)
270 }
271
272 pub fn next_with_raw_span(
285 &mut self,
286 file_contents: &'a str,
287 ) -> Option<Result<(types::ConFrame, &'a str), error::ParseError>> {
288 let base = file_contents.as_ptr() as usize;
289 let start = {
290 let line = self.lines.peek_line()?;
291 line.as_ptr() as usize - base
292 };
293 let frame = match self.next()? {
294 Ok(f) => f,
295 Err(e) => return Some(Err(e)),
296 };
297 let end = match self.lines.peek_line() {
298 Some(line) => line.as_ptr() as usize - base,
299 None => file_contents.len(),
300 };
301 debug_assert!(end >= start && end <= file_contents.len());
302 Some(Ok((frame, &file_contents[start..end])))
303 }
304}
305
306impl<'a> Iterator for ConFrameIterator<'a> {
307 type Item = Result<types::ConFrame, error::ParseError>;
312
313 fn next(&mut self) -> Option<Self::Item> {
319 if self.lines.remainder_is_whitespace() {
323 while self.lines.next_line().is_some() {}
324 return None;
325 }
326 let mut frame = match parse_single_frame(&mut self.lines) {
328 Ok(f) => f,
329 Err(e) => return Some(Err(e)),
330 };
331 let sections =
335 match parse_declared_sections(&mut self.lines, &mut frame.header, &mut frame.atom_data)
336 {
337 Ok(n) => n,
338 Err(e) => return Some(Err(e)),
339 };
340 if sections > 0 {
341 frame.sync_arrays_from_atom_data();
342 }
343 Some(Ok(frame))
344 }
345}
346
347#[cfg(test)]
348mod aos_soa_agreement_tests {
349 use super::*;
350 use std::path::PathBuf;
351
352 #[test]
353 fn iterator_vel_forces_soa_matches_aos() {
354 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
355 .join("resources/test/tiny_cuh2_vel_forces.con");
356 let text = std::fs::read_to_string(&p).expect("fixture");
357 let fr = ConFrameIterator::new(&text)
358 .next()
359 .expect("frame")
360 .expect("parse");
361 let n = fr.atom_data.len();
362 assert!(n > 0);
363 assert_eq!(fr.positions.nrows(), n);
364 let has_vel = fr.atom_data.iter().any(|a| a.velocity.is_some());
365 let has_frc = fr.atom_data.iter().any(|a| a.force.is_some());
366 if has_vel {
367 assert_eq!(
368 fr.velocities.nrows(),
369 n,
370 "SoA velocities must match AoS after section parse"
371 );
372 }
373 if has_frc {
374 assert_eq!(fr.forces.nrows(), n, "SoA forces must match AoS");
375 }
376 for (i, a) in fr.atom_data.iter().enumerate() {
377 let p = fr.positions.as_f64_row(i);
378 assert_eq!([a.x, a.y, a.z], p);
379 if let Some(v) = a.velocity {
380 assert_eq!(v, fr.velocities.as_f64_row(i));
381 }
382 if let Some(f) = a.force {
383 assert_eq!(f, fr.forces.as_f64_row(i));
384 }
385 }
386 }
387
388 #[test]
391 fn sync_skips_pos_when_nrows_matches_keeps_force_soa() {
392 let p =
393 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2_forces.con");
394 let text = std::fs::read_to_string(&p).expect("fixture");
395 let fr = ConFrameIterator::new(&text)
396 .next()
397 .expect("frame")
398 .expect("parse");
399 let n = fr.atom_data.len();
400 assert_eq!(fr.positions.nrows(), n);
401 assert_eq!(fr.forces.nrows(), n);
402 let p0 = fr.positions.as_f64_row(0);
405 let mut fr2 = fr.clone();
406 fr2.sync_arrays_from_atom_data();
407 assert_eq!(fr2.positions.as_f64_row(0), p0);
408 assert_eq!(fr2.forces.nrows(), n);
409 assert_eq!(
410 fr2.forces.as_f64_row(0),
411 fr2.atom_data[0].force.expect("force")
412 );
413 }
414}
415
416#[cfg(feature = "parallel")]
427pub(crate) const PARALLEL_BYTES_THRESHOLD: usize = 48 * 1024;
428
429pub fn read_all_frames(path: &Path) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
430 read_all_frames_with_threads(path, None)
431}
432
433pub fn frames_from_text(
441 text: &str,
442 num_threads: Option<usize>,
443) -> Result<Vec<types::ConFrame>, error::ParseError> {
444 #[cfg(feature = "parallel")]
445 {
446 let use_parallel = match num_threads {
447 Some(1) => false,
448 Some(n) if n >= 2 => true,
449 _ => text.len() >= PARALLEL_BYTES_THRESHOLD,
451 };
452 if use_parallel {
453 let pool = num_threads.filter(|&n| n >= 2);
454 let parts = parse_frames_parallel_with_threads(text, pool);
455 let mut frames = Vec::with_capacity(parts.len());
456 for r in parts {
457 frames.push(r?);
458 }
459 return Ok(frames);
460 }
461 }
462 #[cfg(not(feature = "parallel"))]
463 {
464 let _ = num_threads;
465 }
466 ConFrameIterator::new(text).collect()
467}
468
469pub fn read_all_frames_with_threads(
475 path: &Path,
476 num_threads: Option<usize>,
477) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
478 let contents = crate::compression::read_file_contents(path)?;
479 let text = contents.as_str()?;
480 Ok(frames_from_text(text, num_threads)?)
481}
482
483pub fn count_frames(path: &Path) -> Result<usize, Box<dyn std::error::Error>> {
488 let contents = crate::compression::read_file_contents(path)?;
489 let text = contents.as_str()?;
490 let mut n = 0usize;
491 let mut iter = ConFrameIterator::new(text);
492 loop {
493 match iter.forward_fast() {
494 Some(Ok(())) => n += 1,
495 Some(Err(e)) => return Err(Box::new(e)),
496 None => break,
497 }
498 }
499 Ok(n)
500}
501
502pub fn read_first_frame(path: &Path) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
507 let contents = crate::compression::read_file_contents(path)?;
508 let text = contents.as_str()?;
509 let mut iter = ConFrameIterator::new(text);
510 match iter.next() {
511 Some(Ok(frame)) => Ok(frame),
512 Some(Err(e)) => Err(Box::new(e)),
513 None => Err("No frames found in file".into()),
514 }
515}
516
517pub fn read_nth_frame(
521 path: &Path,
522 index: usize,
523) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
524 let contents = crate::compression::read_file_contents(path)?;
525 let text = contents.as_str()?;
526 read_nth_frame_from_text(text, index)
527}
528
529pub fn read_nth_frame_from_text(
531 text: &str,
532 index: usize,
533) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
534 let mut iter = ConFrameIterator::new(text);
535 let skipped = iter.skip_frames(index)?;
536 if skipped < index {
537 return Err(format!("frame {index} out of range (only {skipped} frames)").into());
538 }
539 match iter.next() {
540 Some(Ok(frame)) => Ok(frame),
541 Some(Err(e)) => Err(Box::new(e)),
542 None => Err(format!("frame {index} out of range").into()),
543 }
544}
545
546pub fn frame_start_offsets(file_contents: &str) -> Vec<usize> {
549 let mut boundaries = Vec::new();
550 let mut scanner = ConFrameIterator::new(file_contents);
551 loop {
552 scanner.lines.clear_peek();
553 let start = scanner.lines.pos;
554 if start >= scanner.lines.bytes.len() {
555 break;
556 }
557 boundaries.push(start);
558 match scanner.forward_fast() {
559 Some(Ok(())) => {}
560 Some(Err(_)) | None => break,
561 }
562 }
563 boundaries
564}
565
566#[cfg(feature = "parallel")]
581pub fn parse_frames_parallel(
582 file_contents: &str,
583) -> Vec<Result<types::ConFrame, error::ParseError>> {
584 parse_frames_parallel_with_threads(file_contents, None)
585}
586
587#[cfg(feature = "parallel")]
596pub fn parse_frames_parallel_with_threads(
597 file_contents: &str,
598 num_threads: Option<usize>,
599) -> Vec<Result<types::ConFrame, error::ParseError>> {
600 use rayon::prelude::*;
601
602 let boundaries = frame_start_offsets(file_contents);
604
605 let parse_chunks = || {
606 let num_frames = boundaries.len();
607 (0..num_frames)
608 .into_par_iter()
609 .map(|i| {
610 let start = boundaries[i];
611 let end = if i + 1 < num_frames {
612 boundaries[i + 1]
613 } else {
614 file_contents.len()
615 };
616 let chunk = &file_contents[start..end];
617 let mut iter = ConFrameIterator::new(chunk);
618 match iter.next() {
619 Some(result) => result,
620 None => Err(error::ParseError::IncompleteFrame),
621 }
622 })
623 .collect()
624 };
625
626 match num_threads {
627 None => parse_chunks(),
628 Some(n) => {
629 let n = n.max(1);
630 match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
631 Ok(pool) => pool.install(parse_chunks),
632 Err(_) => parse_chunks(),
633 }
634 }
635 }
636}
637
638#[cfg(all(test, feature = "parallel"))]
639mod parallel_strong_scale_tests {
640 use super::*;
641 use std::path::PathBuf;
642
643 fn multi_frame_fixture() -> String {
644 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2.con");
645 let one = std::fs::read_to_string(p).expect("fixture");
646 one.repeat(8)
648 }
649
650 fn sequential_frames(text: &str) -> Vec<types::ConFrame> {
651 ConFrameIterator::new(text)
652 .map(|r| r.expect("seq frame"))
653 .collect()
654 }
655
656 fn frames_payload_key(f: &types::ConFrame) -> (usize, Vec<(String, f64, f64, f64)>) {
657 let atoms: Vec<_> = f
658 .atom_data
659 .iter()
660 .map(|a| (a.symbol.to_string(), a.x, a.y, a.z))
661 .collect();
662 (f.atom_data.len(), atoms)
663 }
664
665 #[test]
666 fn parallel_workers_match_sequential_payloads() {
667 let text = multi_frame_fixture();
668 let seq = sequential_frames(&text);
669 assert!(seq.len() >= 8);
670 let seq_keys: Vec<_> = seq.iter().map(frames_payload_key).collect();
671
672 for workers in [1usize, 2, 4] {
673 let par = parse_frames_parallel_with_threads(&text, Some(workers));
674 assert_eq!(par.len(), seq.len(), "workers={workers}");
675 let par_keys: Vec<_> = par
676 .into_iter()
677 .map(|r| frames_payload_key(&r.expect("par frame")))
678 .collect();
679 assert_eq!(par_keys, seq_keys, "workers={workers} frame payloads");
680 }
681
682 let par_default = parse_frames_parallel(&text);
684 let def_keys: Vec<_> = par_default
685 .into_iter()
686 .map(|r| frames_payload_key(&r.expect("par")))
687 .collect();
688 assert_eq!(def_keys, seq_keys);
689 }
690}