1use crate::parser::{LineStream, parse_declared_sections};
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 check_utf8: bool,
22 utf8_bad: bool,
23}
24
25impl<'a> MemchrLines<'a> {
26 pub fn new(text: &'a str) -> Self {
27 Self::from_parts(text.as_bytes(), false)
28 }
29
30 pub fn from_bytes(bytes: &'a [u8]) -> Self {
36 Self::from_parts(bytes, true)
37 }
38
39 fn from_parts(bytes: &'a [u8], check_utf8: bool) -> Self {
40 Self {
41 bytes,
42 pos: 0,
43 peeked: None,
44 check_utf8,
45 utf8_bad: false,
46 }
47 }
48
49 #[inline]
50 fn read_one(&mut self) -> Option<&'a str> {
51 if self.utf8_bad || self.pos >= self.bytes.len() {
52 return None;
53 }
54 let rest = &self.bytes[self.pos..];
55 let (line_bytes, advance) = match memchr::memchr(b'\n', rest) {
56 Some(i) => (&rest[..i], i + 1),
57 None => (rest, rest.len()),
58 };
59 let trimmed = if line_bytes.last() == Some(&b'\r') {
60 &line_bytes[..line_bytes.len() - 1]
61 } else {
62 line_bytes
63 };
64 if self.check_utf8 {
65 match std::str::from_utf8(trimmed) {
66 Ok(s) => {
67 self.pos += advance;
68 Some(s)
69 }
70 Err(_) => {
71 self.utf8_bad = true;
74 None
75 }
76 }
77 } else {
78 self.pos += advance;
79 Some(unsafe { std::str::from_utf8_unchecked(trimmed) })
81 }
82 }
83
84 #[inline]
85 pub fn next_line(&mut self) -> Option<&'a str> {
86 if let Some(p) = self.peeked.take() {
87 return Some(p);
88 }
89 self.read_one()
90 }
91
92 #[inline]
93 pub fn peek_line(&mut self) -> Option<&'a str> {
94 if self.peeked.is_none() {
95 self.peeked = self.read_one();
96 }
97 self.peeked
98 }
99
100 fn clear_peek(&mut self) {
102 if let Some(p) = self.peeked.take() {
103 let start = p.as_ptr() as usize - self.bytes.as_ptr() as usize;
105 self.pos = start;
106 }
107 }
108
109 fn utf8_error(&self) -> Option<error::ParseError> {
110 self.utf8_bad.then(invalid_utf8)
111 }
112
113 fn prefer_utf8(&self, err: error::ParseError) -> error::ParseError {
115 if self.utf8_bad { invalid_utf8() } else { err }
116 }
117}
118
119fn invalid_utf8() -> error::ParseError {
120 error::ParseError::ValidationError("invalid UTF-8".into())
121}
122
123impl<'a> Iterator for MemchrLines<'a> {
124 type Item = &'a str;
125 fn next(&mut self) -> Option<&'a str> {
126 self.next_line()
127 }
128}
129
130impl<'a> LineStream<'a> for MemchrLines<'a> {
131 #[inline]
132 fn next_line(&mut self) -> Option<&'a str> {
133 MemchrLines::next_line(self)
134 }
135 #[inline]
136 fn peek_line(&mut self) -> Option<&'a str> {
137 MemchrLines::peek_line(self)
138 }
139 #[inline]
140 fn next_coord_row(
141 &mut self,
142 spec_version: u32,
143 default_id: u64,
144 ) -> Result<([f64; 3], [bool; 3], u64), crate::error::ParseError> {
145 if let Some(p) = self.peeked.take() {
146 return crate::parser::parse_coord_row_at(p.as_bytes(), 0, spec_version, default_id)
147 .map(|(row, _)| row);
148 }
149 let (row, pos) =
150 crate::parser::parse_coord_row_at(self.bytes, self.pos, spec_version, default_id)?;
151 self.pos = pos;
152 Ok(row)
153 }
154 fn remainder_is_whitespace(&mut self) -> bool {
155 if self.utf8_bad {
156 return false;
157 }
158 if let Some(line) = self.peek_line()
159 && !line.trim().is_empty()
160 {
161 return false;
162 }
163 if self.utf8_bad {
166 return false;
167 }
168 self.bytes[self.pos..]
169 .iter()
170 .all(|b| b.is_ascii_whitespace())
171 }
172 fn blank_line_opens_section(&mut self) -> bool {
173 let Some(peeked) = self.peeked else {
175 return false;
176 };
177 if !peeked.trim().is_empty() {
178 return false;
179 }
180 match probe_section(&self.bytes[self.pos..]) {
181 SectionProbe::Opens => true,
182 SectionProbe::Closed => false,
183 SectionProbe::InvalidUtf8 => {
186 self.utf8_bad = true;
187 false
188 }
189 }
190 }
191
192 fn blank_opens_velocity_block(&mut self) -> bool {
193 match self.peek_line() {
194 Some(line) if line.trim().is_empty() => self.blank_line_opens_section(),
195 _ => true,
196 }
197 }
198}
199
200fn split_raw_line(bytes: &[u8]) -> Option<(&[u8], &[u8])> {
201 if bytes.is_empty() {
202 return None;
203 }
204 match memchr::memchr(b'\n', bytes) {
205 Some(i) => Some((&bytes[..i], &bytes[i + 1..])),
206 None => Some((bytes, &[])),
207 }
208}
209
210fn strip_cr(line: &[u8]) -> &[u8] {
211 if line.last() == Some(&b'\r') {
212 &line[..line.len() - 1]
213 } else {
214 line
215 }
216}
217
218enum SectionProbe {
219 Opens,
220 Closed,
221 InvalidUtf8,
222}
223
224fn probe_section(after_blank: &[u8]) -> SectionProbe {
227 let Some((raw1, after1)) = split_raw_line(after_blank) else {
228 return SectionProbe::Closed;
229 };
230 let line1 = strip_cr(raw1);
231 if line1.iter().all(|b| matches!(b, b' ' | b'\t')) {
232 return SectionProbe::Closed;
233 }
234 let Ok(text1) = std::str::from_utf8(line1) else {
235 return SectionProbe::InvalidUtf8;
236 };
237 if text1.trim_start().starts_with('{') {
238 return SectionProbe::Closed;
239 }
240 let Some((raw2, _)) = split_raw_line(after1) else {
241 return SectionProbe::Closed;
242 };
243 let Ok(text2) = std::str::from_utf8(strip_cr(raw2)) else {
244 return SectionProbe::InvalidUtf8;
245 };
246 if crate::parser::line_has_component_label(text2) {
247 SectionProbe::Opens
248 } else {
249 SectionProbe::Closed
250 }
251}
252
253fn blank_opens_section(after_blank: &[u8]) -> bool {
254 matches!(probe_section(after_blank), SectionProbe::Opens)
255}
256
257pub struct ConFrameIterator<'a> {
268 pub(crate) lines: MemchrLines<'a>,
269}
270
271impl<'a> ConFrameIterator<'a> {
272 pub fn new(file_contents: &'a str) -> Self {
278 ConFrameIterator {
279 lines: MemchrLines::new(file_contents),
280 }
281 }
282
283 pub(crate) fn starting_at(file_contents: &'a str, pos: usize) -> Self {
288 debug_assert!(pos == 0 || file_contents.as_bytes()[pos - 1] == b'\n');
289 let mut it = Self::new(file_contents);
290 it.lines.pos = pos;
291 it
292 }
293
294 pub fn from_bytes(bytes: &'a [u8]) -> Self {
300 ConFrameIterator {
301 lines: MemchrLines::from_bytes(bytes),
302 }
303 }
304
305 fn advance_lines(&mut self, n: usize) -> Result<(), error::ParseError> {
311 self.lines.clear_peek();
312 for i in 0..n {
313 let rest = &self.lines.bytes[self.lines.pos..];
314 match memchr::memchr(b'\n', rest) {
315 Some(pos) => self.lines.pos += pos + 1,
316 None => {
317 if rest.is_empty() {
318 return Err(error::ParseError::IncompleteFrame);
319 }
320 self.lines.pos = self.lines.bytes.len();
321 if i + 1 == n {
322 return Ok(());
323 }
324 return Err(error::ParseError::IncompleteFrame);
325 }
326 }
327 }
328 Ok(())
329 }
330
331 fn advance_lines_to(&mut self, n: usize, end: Option<usize>) -> Result<(), error::ParseError> {
337 self.lines.clear_peek();
338 let pos = self.lines.pos;
339 let bytes = self.lines.bytes;
340 if let Some(end) = end
341 && pos < end
342 && end <= bytes.len()
343 && bytes[end - 1] == b'\n'
344 && memchr::memchr_iter(b'\n', &bytes[pos..end]).count() == n
345 {
346 self.lines.pos = end;
347 return Ok(());
348 }
349 self.advance_lines(n)
350 }
351
352 fn read_line_str(&mut self) -> Option<&'a str> {
354 self.lines.clear_peek();
355 self.lines.next_line()
356 }
357
358 pub fn forward_fast(&mut self) -> Option<Result<(), error::ParseError>> {
362 self.forward_fast_to(None)
363 }
364
365 pub(crate) fn forward_fast_to(
370 &mut self,
371 end: Option<usize>,
372 ) -> Option<Result<(), error::ParseError>> {
373 self.lines.clear_peek();
374 if self.lines.pos >= self.lines.bytes.len() {
375 return None;
376 }
377 if let Err(e) = self.advance_lines(6) {
379 return Some(Err(e));
380 }
381 let natm_types: usize = match self.read_line_str() {
383 Some(line) => match crate::parser::parse_count_line(line, 1) {
384 Ok(v) => v[0],
385 Err(e) => return Some(Err(e)),
386 },
387 None => return Some(Err(error::ParseError::IncompleteHeader)),
388 };
389 if natm_types == 0 {
390 return Some(crate::parser::read_zero_atom_tail(|| self.read_line_str()));
393 }
394 let natms_per_type: Vec<usize> = match self.read_line_str() {
396 Some(line) => match crate::parser::parse_count_line(line, natm_types) {
397 Ok(v) => v,
398 Err(e) => return Some(Err(e)),
399 },
400 None => return Some(Err(error::ParseError::IncompleteHeader)),
401 };
402 if natms_per_type.iter().any(|n| *n == 0) {
403 return Some(Err(error::ParseError::InvalidCount(
404 "each per-type count must be >= 1".into(),
405 )));
406 }
407 if let Err(e) = self.advance_lines(1) {
409 return Some(Err(e));
410 }
411 let total_atoms: usize = natms_per_type.iter().sum();
412 let coord_block_lines = total_atoms + natm_types * 2;
413 if let Err(e) = self.advance_lines_to(coord_block_lines, end) {
414 return Some(Err(e));
415 }
416 self.lines.clear_peek();
418 loop {
419 let rest = &self.lines.bytes[self.lines.pos..];
420 if rest.is_empty() {
421 break;
422 }
423 let next_eol = memchr::memchr(b'\n', rest);
424 let line = match next_eol {
425 Some(pos) => &rest[..pos],
426 None => rest,
427 };
428 let is_blank = line.iter().all(|b| matches!(b, b' ' | b'\t' | b'\r'));
429 if !is_blank {
430 break;
431 }
432 let after = match next_eol {
433 Some(pos) => &rest[pos + 1..],
434 None => &[][..],
435 };
436 if after.iter().all(|b| b.is_ascii_whitespace()) {
439 self.lines.pos = self.lines.bytes.len();
440 break;
441 }
442 if !blank_opens_section(after) {
445 break;
446 }
447 self.lines.pos += next_eol.map(|p| p + 1).unwrap_or(rest.len());
449 if let Err(e) = self.advance_lines_to(coord_block_lines, end) {
450 return Some(Err(e));
451 }
452 }
453 Some(Ok(()))
454 }
455
456 pub fn forward(&mut self) -> Option<Result<(), error::ParseError>> {
468 self.forward_fast()
470 }
471
472 pub fn byte_pos(&self) -> usize {
474 self.lines.pos
475 }
476
477 pub fn skip_frames(&mut self, n: usize) -> Result<usize, error::ParseError> {
480 let mut skipped = 0usize;
481 for _ in 0..n {
482 match self.forward() {
483 None => break,
484 Some(Err(e)) => return Err(e),
485 Some(Ok(())) => skipped += 1,
486 }
487 }
488 Ok(skipped)
489 }
490
491 pub fn next_with_raw_span(
504 &mut self,
505 file_contents: &'a str,
506 ) -> Option<Result<(types::ConFrame, &'a str), error::ParseError>> {
507 let base = file_contents.as_ptr() as usize;
508 let start = {
509 let line = self.lines.peek_line()?;
510 line.as_ptr() as usize - base
511 };
512 let frame = match self.next()? {
513 Ok(f) => f,
514 Err(e) => return Some(Err(e)),
515 };
516 let end = match self.lines.peek_line() {
517 Some(line) => line.as_ptr() as usize - base,
518 None => file_contents.len(),
519 };
520 debug_assert!(end >= start && end <= file_contents.len());
521 Some(Ok((frame, &file_contents[start..end])))
522 }
523
524 pub(crate) fn next_with_end(
526 &mut self,
527 ) -> Option<Result<(types::ConFrame, usize), error::ParseError>> {
528 let frame = match self.next()? {
529 Ok(f) => f,
530 Err(e) => return Some(Err(e)),
531 };
532 let end = match self.lines.peek_line() {
533 Some(line) => line.as_ptr() as usize - self.lines.bytes.as_ptr() as usize,
534 None => self.lines.pos,
535 };
536 Some(Ok((frame, end)))
537 }
538}
539
540impl<'a> Iterator for ConFrameIterator<'a> {
541 type Item = Result<types::ConFrame, error::ParseError>;
546
547 fn next(&mut self) -> Option<Self::Item> {
554 if let Some(err) = self.lines.utf8_error() {
557 return Some(Err(err));
558 }
559 if self.lines.remainder_is_whitespace() {
563 if let Some(err) = self.lines.utf8_error() {
564 return Some(Err(err));
565 }
566 while self.lines.next_line().is_some() {}
567 return None;
568 }
569 if let Some(err) = self.lines.utf8_error() {
570 return Some(Err(err));
571 }
572 let mut frame = match crate::parser::parse_single_frame_ls(&mut self.lines) {
574 Ok(f) => f,
575 Err(e) => return Some(Err(self.lines.prefer_utf8(e))),
576 };
577 let sections =
581 match parse_declared_sections(&mut self.lines, &mut frame.header, &mut frame.atom_data)
582 {
583 Ok(n) => n,
584 Err(e) => return Some(Err(self.lines.prefer_utf8(e))),
587 };
588 if sections > 0 {
589 frame.sync_arrays_from_atom_data();
590 }
591 Some(Ok(frame))
592 }
593}
594
595#[cfg(test)]
596mod aos_soa_agreement_tests {
597 use super::*;
598 use std::path::PathBuf;
599
600 #[test]
601 fn iterator_vel_forces_soa_matches_aos() {
602 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
603 .join("resources/test/tiny_cuh2_vel_forces.con");
604 let text = std::fs::read_to_string(&p).expect("fixture");
605 let fr = ConFrameIterator::new(&text)
606 .next()
607 .expect("frame")
608 .expect("parse");
609 let n = fr.atom_data.len();
610 assert!(n > 0);
611 assert_eq!(fr.positions.nrows(), n);
612 let has_vel = fr.atom_data.iter().any(|a| a.velocity().is_some());
613 let has_frc = fr.atom_data.iter().any(|a| a.force().is_some());
614 if has_vel {
615 assert_eq!(
616 fr.velocities.nrows(),
617 n,
618 "SoA velocities must match AoS after section parse"
619 );
620 }
621 if has_frc {
622 assert_eq!(fr.forces.nrows(), n, "SoA forces must match AoS");
623 }
624 for (i, a) in fr.atom_data.iter().enumerate() {
625 let p = fr.positions.as_f64_row(i);
626 assert_eq!([a.x, a.y, a.z], p);
627 if let Some(v) = a.velocity() {
628 assert_eq!(v, fr.velocities.as_f64_row(i));
629 }
630 if let Some(f) = a.force() {
631 assert_eq!(f, fr.forces.as_f64_row(i));
632 }
633 }
634 }
635
636 #[test]
639 fn sync_skips_pos_when_nrows_matches_keeps_force_soa() {
640 let p =
641 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2_forces.con");
642 let text = std::fs::read_to_string(&p).expect("fixture");
643 let fr = ConFrameIterator::new(&text)
644 .next()
645 .expect("frame")
646 .expect("parse");
647 let n = fr.atom_data.len();
648 assert_eq!(fr.positions.nrows(), n);
649 assert_eq!(fr.forces.nrows(), n);
650 let p0 = fr.positions.as_f64_row(0);
653 let mut fr2 = fr.clone();
654 fr2.sync_arrays_from_atom_data();
655 assert_eq!(fr2.positions.as_f64_row(0), p0);
656 assert_eq!(fr2.forces.nrows(), n);
657 assert_eq!(
658 fr2.forces.as_f64_row(0),
659 fr2.atom_data[0].force().expect("force")
660 );
661 }
662}
663
664#[cfg(feature = "parallel")]
675pub(crate) const PARALLEL_BYTES_THRESHOLD: usize = 48 * 1024;
676
677pub fn read_all_frames(path: &Path) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
678 read_all_frames_with_threads(path, None)
679}
680
681pub fn frames_from_text(
689 text: &str,
690 num_threads: Option<usize>,
691) -> Result<Vec<types::ConFrame>, error::ParseError> {
692 #[cfg(feature = "parallel")]
693 {
694 let use_parallel = match num_threads {
695 Some(1) => false,
696 Some(n) if n >= 2 => true,
697 _ => text.len() >= PARALLEL_BYTES_THRESHOLD,
699 };
700 if use_parallel {
701 let pool = num_threads.filter(|&n| n >= 2);
702 let parts = parse_frames_parallel_with_threads(text, pool);
703 let mut frames = Vec::with_capacity(parts.len());
704 for r in parts {
705 frames.push(r?);
706 }
707 return Ok(frames);
708 }
709 }
710 #[cfg(not(feature = "parallel"))]
711 {
712 let _ = num_threads;
713 }
714 ConFrameIterator::new(text).collect()
715}
716
717pub fn read_all_frames_with_threads(
723 path: &Path,
724 num_threads: Option<usize>,
725) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
726 let contents = crate::compression::read_file_contents(path)?;
727 match contents {
728 crate::compression::FileContents::Owned(text) => Ok(frames_from_text(&text, num_threads)?),
729 crate::compression::FileContents::Mapped(map) => Ok(frames_from_bytes(&map, num_threads)?),
732 }
733}
734
735pub fn frames_from_bytes(
737 bytes: &[u8],
738 num_threads: Option<usize>,
739) -> Result<Vec<types::ConFrame>, error::ParseError> {
740 #[cfg(feature = "parallel")]
741 {
742 let use_parallel = match num_threads {
743 Some(1) => false,
744 Some(n) if n >= 2 => true,
745 _ => bytes.len() >= PARALLEL_BYTES_THRESHOLD,
746 };
747 if use_parallel {
748 let pool = num_threads.filter(|&n| n >= 2);
749 let parts = parse_frames_parallel_bytes(bytes, pool);
750 let mut frames = Vec::with_capacity(parts.len());
751 for r in parts {
752 frames.push(r?);
753 }
754 return Ok(frames);
755 }
756 }
757 #[cfg(not(feature = "parallel"))]
758 {
759 let _ = num_threads;
760 }
761 let mut frames = Vec::new();
762 for r in ConFrameIterator::from_bytes(bytes) {
763 frames.push(r?);
764 }
765 Ok(frames)
766}
767
768#[cfg(feature = "parallel")]
769fn parse_frames_parallel_bytes(
770 bytes: &[u8],
771 num_threads: Option<usize>,
772) -> Vec<Result<types::ConFrame, error::ParseError>> {
773 use rayon::prelude::*;
774
775 let mut scanner = ConFrameIterator::from_bytes(bytes);
776 let mut boundaries = Vec::new();
777 loop {
778 scanner.lines.clear_peek();
779 let start = scanner.lines.pos;
780 if start >= bytes.len() || bytes[start..].iter().all(u8::is_ascii_whitespace) {
782 break;
783 }
784 boundaries.push(start);
785 match scanner.forward_fast() {
786 Some(Ok(())) => {}
787 Some(Err(_)) | None => break,
788 }
789 }
790 let parse_chunks = || {
791 let num_frames = boundaries.len();
792 (0..num_frames)
793 .into_par_iter()
794 .map(|i| {
795 let start = boundaries[i];
796 let end = if i + 1 < num_frames {
797 boundaries[i + 1]
798 } else {
799 bytes.len()
800 };
801 let mut iter = ConFrameIterator::from_bytes(&bytes[start..end]);
802 match iter.next() {
803 Some(result) => result,
804 None => Err(error::ParseError::IncompleteFrame),
805 }
806 })
807 .collect()
808 };
809 match num_threads {
810 None => parse_chunks(),
811 Some(n) => {
812 let n = n.max(1);
813 match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
814 Ok(pool) => pool.install(parse_chunks),
815 Err(_) => parse_chunks(),
816 }
817 }
818 }
819}
820
821pub fn count_frames(path: &Path) -> Result<usize, Box<dyn std::error::Error>> {
826 let contents = crate::compression::read_file_contents(path)?;
827 let text = contents.as_str()?;
828 let mut n = 0usize;
829 let mut iter = ConFrameIterator::new(text);
830 loop {
831 match iter.forward_fast() {
832 Some(Ok(())) => n += 1,
833 Some(Err(e)) => return Err(Box::new(e)),
834 None => break,
835 }
836 }
837 Ok(n)
838}
839
840pub fn read_first_frame(path: &Path) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
845 let contents = crate::compression::read_file_contents(path)?;
846 let text = contents.as_str()?;
847 let mut iter = ConFrameIterator::new(text);
848 match iter.next() {
849 Some(Ok(frame)) => Ok(frame),
850 Some(Err(e)) => Err(Box::new(e)),
851 None => Err("No frames found in file".into()),
852 }
853}
854
855pub fn read_nth_frame(
859 path: &Path,
860 index: usize,
861) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
862 let contents = crate::compression::read_file_contents(path)?;
863 let text = contents.as_str()?;
864 read_nth_frame_from_text(text, index)
865}
866
867pub fn read_nth_frame_from_text(
869 text: &str,
870 index: usize,
871) -> Result<types::ConFrame, Box<dyn std::error::Error>> {
872 let mut iter = ConFrameIterator::new(text);
873 let skipped = iter.skip_frames(index)?;
874 if skipped < index {
875 return Err(format!("frame {index} out of range (only {skipped} frames)").into());
876 }
877 match iter.next() {
878 Some(Ok(frame)) => Ok(frame),
879 Some(Err(e)) => Err(Box::new(e)),
880 None => Err(format!("frame {index} out of range").into()),
881 }
882}
883
884#[cfg(feature = "parallel")]
890pub fn read_all_frames_indexed(
891 path: &Path,
892 num_threads: Option<usize>,
893) -> Result<Vec<types::ConFrame>, Box<dyn std::error::Error>> {
894 let contents = crate::compression::read_file_contents(path)?;
895 let text = contents.as_str_sliced()?;
896 let idx = match contents {
897 crate::compression::FileContents::Mapped(_) => {
898 crate::frame_index::load_or_build(path, text, true)?
899 }
900 crate::compression::FileContents::Owned(_) => {
901 let on_disk = std::fs::metadata(path)?.len();
902 crate::frame_index::load_or_build(path, text, on_disk == text.len() as u64)?
903 }
904 };
905 let offs: Vec<usize> = idx.offsets.iter().map(|&o| o as usize).collect();
906 parse_frames_at_offsets(text, &offs, num_threads)
907 .into_iter()
908 .map(|r| r.map_err(|e| Box::new(e) as Box<dyn std::error::Error>))
909 .collect()
910}
911
912pub fn frame_start_offsets(file_contents: &str) -> Vec<usize> {
915 let mut boundaries = Vec::new();
916 let mut scanner = ConFrameIterator::new(file_contents);
917 loop {
918 scanner.lines.clear_peek();
919 let start = scanner.lines.pos;
920 if start >= scanner.lines.bytes.len() {
921 break;
922 }
923 boundaries.push(start);
924 match scanner.forward_fast() {
925 Some(Ok(())) => {}
926 Some(Err(_)) | None => break,
927 }
928 }
929 boundaries
930}
931
932#[cfg(feature = "parallel")]
947pub fn parse_frames_parallel(
948 file_contents: &str,
949) -> Vec<Result<types::ConFrame, error::ParseError>> {
950 parse_frames_parallel_with_threads(file_contents, None)
951}
952
953#[cfg(feature = "parallel")]
962pub fn parse_frames_parallel_with_threads(
963 file_contents: &str,
964 num_threads: Option<usize>,
965) -> Vec<Result<types::ConFrame, error::ParseError>> {
966 let boundaries = frame_start_offsets(file_contents);
968 parse_frames_at_offsets(file_contents, &boundaries, num_threads)
969}
970
971#[cfg(feature = "parallel")]
980pub fn parse_frames_at_offsets(
981 file_contents: &str,
982 boundaries: &[usize],
983 num_threads: Option<usize>,
984) -> Vec<Result<types::ConFrame, error::ParseError>> {
985 use rayon::prelude::*;
986
987 let parse_chunks = || {
988 let num_frames = boundaries.len();
989 (0..num_frames)
990 .into_par_iter()
991 .map(|i| {
992 let start = boundaries[i];
993 let end = if i + 1 < num_frames {
994 boundaries[i + 1]
995 } else {
996 file_contents.len()
997 };
998 let chunk = &file_contents[start..end];
999 let mut iter = ConFrameIterator::new(chunk);
1000 match iter.next() {
1001 Some(result) => result,
1002 None => Err(error::ParseError::IncompleteFrame),
1003 }
1004 })
1005 .collect()
1006 };
1007
1008 match num_threads {
1009 None => parse_chunks(),
1010 Some(n) => {
1011 let n = n.max(1);
1012 match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
1013 Ok(pool) => pool.install(parse_chunks),
1014 Err(_) => parse_chunks(),
1015 }
1016 }
1017 }
1018}
1019
1020#[cfg(all(test, feature = "parallel"))]
1021mod parallel_strong_scale_tests {
1022 use super::*;
1023 use std::path::PathBuf;
1024
1025 fn multi_frame_fixture() -> String {
1026 let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/tiny_cuh2.con");
1027 let one = std::fs::read_to_string(p).expect("fixture");
1028 one.repeat(8)
1030 }
1031
1032 fn sequential_frames(text: &str) -> Vec<types::ConFrame> {
1033 ConFrameIterator::new(text)
1034 .map(|r| r.expect("seq frame"))
1035 .collect()
1036 }
1037
1038 fn frames_payload_key(f: &types::ConFrame) -> (usize, Vec<(String, f64, f64, f64)>) {
1039 let atoms: Vec<_> = f
1040 .atom_data
1041 .iter()
1042 .map(|a| (a.symbol.to_string(), a.x, a.y, a.z))
1043 .collect();
1044 (f.atom_data.len(), atoms)
1045 }
1046
1047 #[test]
1048 fn parallel_workers_match_sequential_payloads() {
1049 let text = multi_frame_fixture();
1050 let seq = sequential_frames(&text);
1051 assert!(seq.len() >= 8);
1052 let seq_keys: Vec<_> = seq.iter().map(frames_payload_key).collect();
1053
1054 for workers in [1usize, 2, 4] {
1055 let par = parse_frames_parallel_with_threads(&text, Some(workers));
1056 assert_eq!(par.len(), seq.len(), "workers={workers}");
1057 let par_keys: Vec<_> = par
1058 .into_iter()
1059 .map(|r| frames_payload_key(&r.expect("par frame")))
1060 .collect();
1061 assert_eq!(par_keys, seq_keys, "workers={workers} frame payloads");
1062 }
1063
1064 let par_default = parse_frames_parallel(&text);
1066 let def_keys: Vec<_> = par_default
1067 .into_iter()
1068 .map(|r| frames_payload_key(&r.expect("par")))
1069 .collect();
1070 assert_eq!(def_keys, seq_keys);
1071 }
1072}