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}
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 byte_pos(&self) -> usize {
225 self.lines.pos
226 }
227
228 pub fn skip_frames(&mut self, n: usize) -> Result<usize, error::ParseError> {
231 let mut skipped = 0usize;
232 for _ in 0..n {
233 match self.forward() {
234 None => break,
235 Some(Err(e)) => return Err(e),
236 Some(Ok(())) => skipped += 1,
237 }
238 }
239 Ok(skipped)
240 }
241
242 pub fn next_with_raw_span(
255 &mut self,
256 file_contents: &'a str,
257 ) -> Option<Result<(types::ConFrame, &'a str), error::ParseError>> {
258 let base = file_contents.as_ptr() as usize;
259 let start = {
260 let line = self.lines.peek_line()?;
261 line.as_ptr() as usize - base
262 };
263 let frame = match self.next()? {
264 Ok(f) => f,
265 Err(e) => return Some(Err(e)),
266 };
267 let end = match self.lines.peek_line() {
268 Some(line) => line.as_ptr() as usize - base,
269 None => file_contents.len(),
270 };
271 debug_assert!(end >= start && end <= file_contents.len());
272 Some(Ok((frame, &file_contents[start..end])))
273 }
274}
275
276impl<'a> Iterator for ConFrameIterator<'a> {
277 type Item = Result<types::ConFrame, error::ParseError>;
282
283 fn next(&mut self) -> Option<Self::Item> {
289 self.lines.peek_line()?;
291 let mut frame = match parse_single_frame(&mut self.lines) {
293 Ok(f) => f,
294 Err(e) => return Some(Err(e)),
295 };
296 let sections =
300 match parse_declared_sections(&mut self.lines, &mut frame.header, &mut frame.atom_data)
301 {
302 Ok(n) => n,
303 Err(e) => return Some(Err(e)),
304 };
305 if sections > 0 {
306 frame.sync_arrays_from_atom_data();
307 }
308 Some(Ok(frame))
309 }
310}
311
312#[cfg(test)]
313mod aos_soa_agreement_tests {
314 use super::*;
315 use std::path::PathBuf;
316
317 #[test]
318 fn iterator_vel_forces_soa_matches_aos() {
319 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
320 .join("resources/test/tiny_cuh2_vel_forces.con");
321 let text = std::fs::read_to_string(&p).expect("fixture");
322 let fr = ConFrameIterator::new(&text)
323 .next()
324 .expect("frame")
325 .expect("parse");
326 let n = fr.atom_data.len();
327 assert!(n > 0);
328 assert_eq!(fr.positions.nrows(), n);
329 let has_vel = fr.atom_data.iter().any(|a| a.velocity.is_some());
330 let has_frc = fr.atom_data.iter().any(|a| a.force.is_some());
331 if has_vel {
332 assert_eq!(
333 fr.velocities.nrows(),
334 n,
335 "SoA velocities must match AoS after section parse"
336 );
337 }
338 if has_frc {
339 assert_eq!(fr.forces.nrows(), n, "SoA forces must match AoS");
340 }
341 for (i, a) in fr.atom_data.iter().enumerate() {
342 let p = fr.positions.as_f64_row(i);
343 assert_eq!([a.x, a.y, a.z], p);
344 if let Some(v) = a.velocity {
345 assert_eq!(v, fr.velocities.as_f64_row(i));
346 }
347 if let Some(f) = a.force {
348 assert_eq!(f, fr.forces.as_f64_row(i));
349 }
350 }
351 }
352
353 #[test]
356 fn sync_skips_pos_when_nrows_matches_keeps_force_soa() {
357 let p =
358 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2_forces.con");
359 let text = std::fs::read_to_string(&p).expect("fixture");
360 let fr = ConFrameIterator::new(&text)
361 .next()
362 .expect("frame")
363 .expect("parse");
364 let n = fr.atom_data.len();
365 assert_eq!(fr.positions.nrows(), n);
366 assert_eq!(fr.forces.nrows(), n);
367 let p0 = fr.positions.as_f64_row(0);
370 let mut fr2 = fr.clone();
371 fr2.sync_arrays_from_atom_data();
372 assert_eq!(fr2.positions.as_f64_row(0), p0);
373 assert_eq!(fr2.forces.nrows(), n);
374 assert_eq!(
375 fr2.forces.as_f64_row(0),
376 fr2.atom_data[0].force.expect("force")
377 );
378 }
379}
380
381#[cfg(feature = "parallel")]
392pub(crate) const PARALLEL_BYTES_THRESHOLD: usize = 48 * 1024;
393
394pub fn read_all_frames(path: &Path) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
395 read_all_frames_with_threads(path, None)
396}
397
398pub fn frames_from_text(
406 text: &str,
407 num_threads: Option<usize>,
408) -> Result<Vec<types::ConFrame>, error::ParseError> {
409 #[cfg(feature = "parallel")]
410 {
411 let use_parallel = match num_threads {
412 Some(1) => false,
413 Some(n) if n >= 2 => true,
414 _ => text.len() >= PARALLEL_BYTES_THRESHOLD,
416 };
417 if use_parallel {
418 let pool = num_threads.filter(|&n| n >= 2);
419 let parts = parse_frames_parallel_with_threads(text, pool);
420 let mut frames = Vec::with_capacity(parts.len());
421 for r in parts {
422 frames.push(r?);
423 }
424 return Ok(frames);
425 }
426 }
427 #[cfg(not(feature = "parallel"))]
428 {
429 let _ = num_threads;
430 }
431 ConFrameIterator::new(text).collect()
432}
433
434pub fn read_all_frames_with_threads(
440 path: &Path,
441 num_threads: Option<usize>,
442) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
443 let contents = crate::compression::read_file_contents(path)?;
444 let text = contents.as_str()?;
445 Ok(frames_from_text(text, num_threads)?)
446}
447
448pub fn count_frames(path: &Path) -> Result<usize, Box<dyn std::error::Error>> {
453 let contents = crate::compression::read_file_contents(path)?;
454 let text = contents.as_str()?;
455 let mut n = 0usize;
456 let mut iter = ConFrameIterator::new(text);
457 loop {
458 match iter.forward_fast() {
459 Some(Ok(())) => n += 1,
460 Some(Err(e)) => return Err(Box::new(e)),
461 None => break,
462 }
463 }
464 Ok(n)
465}
466
467pub fn read_first_frame(path: &Path) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
472 let contents = crate::compression::read_file_contents(path)?;
473 let text = contents.as_str()?;
474 let mut iter = ConFrameIterator::new(text);
475 match iter.next() {
476 Some(Ok(frame)) => Ok(frame),
477 Some(Err(e)) => Err(Box::new(e)),
478 None => Err("No frames found in file".into()),
479 }
480}
481
482pub fn read_nth_frame(
486 path: &Path,
487 index: usize,
488) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
489 let contents = crate::compression::read_file_contents(path)?;
490 let text = contents.as_str()?;
491 read_nth_frame_from_text(text, index)
492}
493
494pub fn read_nth_frame_from_text(
496 text: &str,
497 index: usize,
498) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
499 let mut iter = ConFrameIterator::new(text);
500 let skipped = iter.skip_frames(index)?;
501 if skipped < index {
502 return Err(format!("frame {index} out of range (only {skipped} frames)").into());
503 }
504 match iter.next() {
505 Some(Ok(frame)) => Ok(frame),
506 Some(Err(e)) => Err(Box::new(e)),
507 None => Err(format!("frame {index} out of range").into()),
508 }
509}
510
511pub fn frame_start_offsets(file_contents: &str) -> Vec<usize> {
514 let mut boundaries = Vec::new();
515 let mut scanner = ConFrameIterator::new(file_contents);
516 loop {
517 scanner.lines.clear_peek();
518 let start = scanner.lines.pos;
519 if start >= scanner.lines.bytes.len() {
520 break;
521 }
522 boundaries.push(start);
523 match scanner.forward_fast() {
524 Some(Ok(())) => {}
525 Some(Err(_)) | None => break,
526 }
527 }
528 boundaries
529}
530
531#[cfg(feature = "parallel")]
546pub fn parse_frames_parallel(
547 file_contents: &str,
548) -> Vec<Result<types::ConFrame, error::ParseError>> {
549 parse_frames_parallel_with_threads(file_contents, None)
550}
551
552#[cfg(feature = "parallel")]
561pub fn parse_frames_parallel_with_threads(
562 file_contents: &str,
563 num_threads: Option<usize>,
564) -> Vec<Result<types::ConFrame, error::ParseError>> {
565 use rayon::prelude::*;
566
567 let boundaries = frame_start_offsets(file_contents);
569
570 let parse_chunks = || {
571 let num_frames = boundaries.len();
572 (0..num_frames)
573 .into_par_iter()
574 .map(|i| {
575 let start = boundaries[i];
576 let end = if i + 1 < num_frames {
577 boundaries[i + 1]
578 } else {
579 file_contents.len()
580 };
581 let chunk = &file_contents[start..end];
582 let mut iter = ConFrameIterator::new(chunk);
583 match iter.next() {
584 Some(result) => result,
585 None => Err(error::ParseError::IncompleteFrame),
586 }
587 })
588 .collect()
589 };
590
591 match num_threads {
592 None => parse_chunks(),
593 Some(n) => {
594 let n = n.max(1);
595 match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
596 Ok(pool) => pool.install(parse_chunks),
597 Err(_) => parse_chunks(),
598 }
599 }
600 }
601}
602
603#[cfg(all(test, feature = "parallel"))]
604mod parallel_strong_scale_tests {
605 use super::*;
606 use std::path::PathBuf;
607
608 fn multi_frame_fixture() -> String {
609 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2.con");
610 let one = std::fs::read_to_string(p).expect("fixture");
611 one.repeat(8)
613 }
614
615 fn sequential_frames(text: &str) -> Vec<types::ConFrame> {
616 ConFrameIterator::new(text)
617 .map(|r| r.expect("seq frame"))
618 .collect()
619 }
620
621 fn frames_payload_key(f: &types::ConFrame) -> (usize, Vec<(String, f64, f64, f64)>) {
622 let atoms: Vec<_> = f
623 .atom_data
624 .iter()
625 .map(|a| (a.symbol.to_string(), a.x, a.y, a.z))
626 .collect();
627 (f.atom_data.len(), atoms)
628 }
629
630 #[test]
631 fn parallel_workers_match_sequential_payloads() {
632 let text = multi_frame_fixture();
633 let seq = sequential_frames(&text);
634 assert!(seq.len() >= 8);
635 let seq_keys: Vec<_> = seq.iter().map(frames_payload_key).collect();
636
637 for workers in [1usize, 2, 4] {
638 let par = parse_frames_parallel_with_threads(&text, Some(workers));
639 assert_eq!(par.len(), seq.len(), "workers={workers}");
640 let par_keys: Vec<_> = par
641 .into_iter()
642 .map(|r| frames_payload_key(&r.expect("par frame")))
643 .collect();
644 assert_eq!(par_keys, seq_keys, "workers={workers} frame payloads");
645 }
646
647 let par_default = parse_frames_parallel(&text);
649 let def_keys: Vec<_> = par_default
650 .into_iter()
651 .map(|r| frames_payload_key(&r.expect("par")))
652 .collect();
653 assert_eq!(def_keys, seq_keys);
654 }
655}