1use crate::error::ParseError;
2use crate::helpers::symbol_to_atomic_number;
3use crate::types::{
4 AtomDatum, ConFrame, FrameHeader, PreboxHeader, SECTION_CHARGES, SECTION_DISPLACEMENTS,
5 SECTION_ENERGIES, SECTION_FORCES, SECTION_MAGMOMS, SECTION_SPINS, SECTION_SPREADS,
6 SECTION_VELOCITIES, meta,
7};
8use serde_json::Value;
9use std::iter::Peekable;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicUsize, Ordering};
12
13#[doc(hidden)]
23pub struct CoordRows {
24 pub xyz: Vec<f64>,
25 pub ids: Vec<u64>,
26 pub flags: Vec<[bool; 3]>,
27 pub atoms: Vec<crate::types::AtomDatum>,
28 pub consumed: usize,
29}
30
31pub(crate) fn wide_workers() -> usize {
36 WIDE_WORKERS.with(|slot| slot.get())
37}
38
39pub(crate) fn set_wide_workers(n: usize) {
40 WIDE_WORKERS.with(|slot| slot.set(n.max(1)));
41}
42
43std::thread_local! {
44 static WIDE_WORKERS: std::cell::Cell<usize> = const { std::cell::Cell::new(1) };
45}
46
47#[cfg(feature = "parallel")]
53pub(crate) fn fill_rows_from_breaks(
54 bytes: &[u8],
55 breaks: &[usize],
56 row0: usize,
57 spec_version: u32,
58 start_id: u64,
59 workers: usize,
60 xyz: *mut f64,
61 ids: *mut u64,
62 flags: *mut u8,
63) -> Result<(), ParseError> {
64 let n = breaks.len();
65 if n == 0 {
66 return Ok(());
67 }
68 let chunk = n.div_ceil(workers.max(1)).max(1);
69 let groups = n.div_ceil(chunk);
70 let xyz_ptr = xyz as usize;
71 let ids_ptr = ids as usize;
72 let flags_ptr = flags as usize;
73 let fill = || {
74 use rayon::prelude::*;
75 (0..groups).into_par_iter().try_for_each(|group| {
76 let first = group * chunk;
77 let last = (first + chunk).min(n);
78 for row_index in first..last {
79 let start = if row_index == 0 {
80 row0
81 } else {
82 breaks[row_index - 1] + 1
83 };
84 let end = breaks[row_index] + 1;
85 if end > bytes.len() || start > end {
86 return Err(ParseError::IncompleteFrame);
87 }
88 let default_id = start_id + row_index as u64;
89 let ((row, fixed, atom_id), _next) =
90 parse_coord_row_at(&bytes[start..end], 0, spec_version, default_id)?;
91 unsafe {
92 std::ptr::copy_nonoverlapping(
93 row.as_ptr(),
94 (xyz_ptr as *mut f64).add(row_index * 3),
95 3,
96 );
97 if ids_ptr != 0 {
98 *((ids_ptr as *mut u64).add(row_index)) = atom_id;
99 }
100 if flags_ptr != 0 {
101 let mask = u8::from(fixed[0])
102 | (u8::from(fixed[1]) << 1)
103 | (u8::from(fixed[2]) << 2);
104 *((flags_ptr as *mut u8).add(row_index)) = mask;
105 }
106 }
107 }
108 Ok(())
109 })
110 };
111 on_pool(workers, fill)
112}
113
114#[cfg(feature = "parallel")]
119pub(crate) fn on_pool<T: Send>(workers: usize, body: impl FnOnce() -> T + Send) -> T {
120 if workers < 2 {
121 return body();
122 }
123 #[cfg(miri)]
127 {
128 return match rayon::ThreadPoolBuilder::new().num_threads(workers).build() {
129 Ok(pool) => pool.install(body),
130 Err(_) => body(),
131 };
132 }
133 #[cfg(not(miri))]
134 {
135 let pool = {
136 use std::sync::{Mutex, OnceLock};
137 static POOLS: OnceLock<Mutex<Vec<(usize, &'static rayon::ThreadPool)>>> =
138 OnceLock::new();
139 let pools = POOLS.get_or_init(|| Mutex::new(Vec::new()));
140 let mut guard = pools.lock().unwrap_or_else(|err| err.into_inner());
141 if let Some((_, pool)) = guard.iter().find(|(size, _)| *size == workers) {
142 *pool
143 } else {
144 let next_core = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
145 let built = match rayon::ThreadPoolBuilder::new()
146 .num_threads(workers)
147 .spawn_handler(move |thread| {
148 let core = next_core.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
152 std::thread::Builder::new().spawn(move || {
153 pin_worker(core);
154 thread.run();
155 })?;
156 Ok(())
157 })
158 .build()
159 {
160 Ok(pool) => pool,
161 Err(_) => return body(),
162 };
163 let pool: &'static rayon::ThreadPool = Box::leak(Box::new(built));
164 guard.push((workers, pool));
165 pool
166 }
167 };
168 pool.install(body)
169 }
170}
171
172pub(crate) fn pin_worker(core: usize) {
173 #[cfg(all(not(miri), target_os = "linux"))]
175 unsafe {
176 let mut set: libc::cpu_set_t = std::mem::zeroed();
177 libc::CPU_ZERO(&mut set);
178 libc::CPU_SET(core, &mut set);
179 libc::sched_setaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &set);
180 }
181 #[cfg(any(miri, not(target_os = "linux")))]
182 {
183 let _ = core;
184 }
185}
186
187#[cfg(feature = "parallel")]
194pub(crate) fn parallel_coord_rows(
195 bytes: &[u8],
196 n: usize,
197 spec_version: u32,
198 start_id: u64,
199 workers: usize,
200 symbol: Option<&std::sync::Arc<str>>,
201) -> Result<CoordRows, ParseError> {
202 if n == 0 {
203 return Ok(CoordRows {
204 xyz: Vec::new(),
205 ids: Vec::new(),
206 flags: Vec::new(),
207 atoms: Vec::new(),
208 consumed: 0,
209 });
210 }
211 let (chunks, consumed) = plan_coord_chunks(bytes, 0, n, workers.max(1))?;
215 let mut xyz = Vec::<f64>::with_capacity(n.saturating_mul(3));
216 let mut ids = Vec::<u64>::with_capacity(n);
217 let mut flags = Vec::<[bool; 3]>::with_capacity(n);
218 let mut atoms_raw: Vec<std::mem::MaybeUninit<crate::types::AtomDatum>> = Vec::new();
219 if symbol.is_some() {
220 atoms_raw = Vec::with_capacity(n);
221 unsafe { atoms_raw.set_len(n) };
222 }
223 let xyz_ptr = xyz.as_mut_ptr() as usize;
224 let ids_ptr = ids.as_mut_ptr() as usize;
225 let flags_ptr = flags.as_mut_ptr() as usize;
226 let atoms_ptr = atoms_raw.as_mut_ptr() as usize;
227 let mut row0 = Vec::with_capacity(chunks.len());
228 let mut acc = 0usize;
229 for (_, nrows) in &chunks {
230 row0.push(acc);
231 acc += *nrows;
232 }
233 let fill_chunk = |index: usize| -> Result<(), ParseError> {
234 let (start, nrows) = chunks[index];
235 let mut row = row0[index];
236 let mut pos = start;
237 let mut shares = symbol.map(|symbol| symbol_shares(symbol, nrows));
238 for _ in 0..nrows {
239 let ((xyz, fixed, atom_id), next) =
240 parse_coord_row_at(bytes, pos, spec_version, start_id + row as u64)?;
241 unsafe {
242 std::ptr::copy_nonoverlapping(xyz.as_ptr(), (xyz_ptr as *mut f64).add(row * 3), 3);
243 *((ids_ptr as *mut u64).add(row)) = atom_id;
244 *((flags_ptr as *mut [bool; 3]).add(row)) = fixed;
245 if let Some(shares) = shares.as_mut() {
246 let slot = (xyz_ptr as *mut f64).add(row * 3);
247 let atom =
248 crate::types::AtomDatum::at_slot(shares.take(), slot, fixed, atom_id);
249 (*(atoms_ptr as *mut std::mem::MaybeUninit<crate::types::AtomDatum>).add(row))
250 .write(atom);
251 }
252 }
253 pos = next;
254 row += 1;
255 }
256 Ok(())
257 };
258 let built = if workers < 2 || chunks.len() < 2 {
259 (0..chunks.len()).try_for_each(fill_chunk)
260 } else {
261 on_pool(workers, || {
262 use rayon::prelude::*;
263 (0..chunks.len()).into_par_iter().try_for_each(fill_chunk)
264 })
265 };
266 if let Err(err) = built {
267 std::mem::forget(atoms_raw);
268 return Err(err);
269 }
270 unsafe {
273 xyz.set_len(n * 3);
274 ids.set_len(n);
275 flags.set_len(n);
276 }
277 let atoms = if symbol.is_some() {
278 let mut raw = std::mem::ManuallyDrop::new(atoms_raw);
279 unsafe {
280 Vec::from_raw_parts(
281 raw.as_mut_ptr() as *mut crate::types::AtomDatum,
282 n,
283 raw.capacity(),
284 )
285 }
286 } else {
287 Vec::new()
288 };
289 Ok(CoordRows {
290 xyz,
291 ids,
292 flags,
293 atoms,
294 consumed,
295 })
296}
297
298#[cfg(feature = "parallel")]
304pub(crate) fn parse_coord_run(
305 bytes: &[u8],
306 start: usize,
307 nrows: usize,
308 spec_version: u32,
309 row_base: usize,
310 symbol: Option<&std::sync::Arc<str>>,
311 xyz: *mut f64,
312 ids: *mut u64,
313 flags: *mut [bool; 3],
314 atoms: *mut std::mem::MaybeUninit<crate::types::AtomDatum>,
315) -> Result<usize, ParseError> {
316 let mut pos = start;
317 let mut shares = symbol.map(|symbol| symbol_shares(symbol, nrows));
318 for step in 0..nrows {
319 let row = row_base + step;
320 let ((xyz_row, fixed, atom_id), next) =
321 parse_coord_row_at(bytes, pos, spec_version, row as u64)?;
322 unsafe {
323 let dest = xyz.add(row * 3);
324 std::ptr::copy_nonoverlapping(xyz_row.as_ptr(), dest, 3);
325 *ids.add(row) = atom_id;
326 *flags.add(row) = fixed;
327 if let Some(shares) = shares.as_mut() {
328 let atom = crate::types::AtomDatum::at_slot(shares.take(), dest, fixed, atom_id);
329 (*atoms.add(row)).write(atom);
330 }
331 }
332 pos = next;
333 }
334 Ok(pos)
335}
336
337#[cfg(feature = "parallel")]
338#[allow(dead_code)]
339fn coord_line_ranges(
340 bytes: &[u8],
341 n: usize,
342 workers: usize,
343) -> Result<Vec<(usize, usize)>, ParseError> {
344 let newlines = newline_index(bytes, workers);
345 let mut ranges = Vec::with_capacity(n);
346 let mut start = 0usize;
347 for index in 0..n {
348 if start > bytes.len() {
349 return Err(ParseError::IncompleteFrame);
350 }
351 let end = if index < newlines.len() {
352 newlines[index] + 1
353 } else if index == newlines.len() && start < bytes.len() {
354 bytes.len()
355 } else {
356 return Err(ParseError::IncompleteFrame);
357 };
358 if end < start {
359 return Err(ParseError::IncompleteFrame);
360 }
361 ranges.push((start, end));
362 start = end;
363 }
364 Ok(ranges)
365}
366
367#[cfg(feature = "parallel")]
375pub(crate) fn newline_index(bytes: &[u8], workers: usize) -> Vec<usize> {
376 newline_index_at(bytes, 0, workers)
377}
378
379#[cfg(feature = "parallel")]
381pub(crate) fn newline_index_at(bytes: &[u8], base: usize, workers: usize) -> Vec<usize> {
382 if bytes.is_empty() || workers < 2 || bytes.len() < (1 << 20) {
383 return memchr::memchr_iter(b'\n', bytes)
384 .map(|index| base + index)
385 .collect();
386 }
387 let cuts = newline_cuts(bytes, workers);
388 newline_index_exact(&cuts, bytes, base)
389}
390
391#[cfg(feature = "parallel")]
393#[derive(Clone)]
394#[repr(C, align(64))]
395struct ChunkTally {
396 n: usize,
397}
398
399#[cfg(feature = "parallel")]
400pub(crate) fn newline_cuts(bytes: &[u8], workers: usize) -> Vec<usize> {
401 let workers = workers.min(bytes.len()).max(1);
402 let mut cuts = Vec::with_capacity(workers + 1);
403 cuts.push(0usize);
404 for index in 1..workers {
405 let mut at = index * bytes.len() / workers;
406 if at > 0 && at < bytes.len() && bytes[at - 1] != b'\n' {
409 if let Some(rel) = memchr::memchr(b'\n', &bytes[at..]) {
410 at += rel + 1;
411 } else {
412 at = bytes.len();
413 }
414 }
415 if cuts.last().copied() != Some(at) {
416 cuts.push(at);
417 }
418 }
419 if cuts.last().copied() != Some(bytes.len()) {
420 cuts.push(bytes.len());
421 }
422 cuts
423}
424
425#[cfg(feature = "parallel")]
428fn count_newline_chunks(bytes: &[u8], cuts: &[usize]) -> Vec<usize> {
429 let nchunks = cuts.len().saturating_sub(1);
430 let mut tallies = vec![ChunkTally { n: 0 }; nchunks];
431 if nchunks == 0 {
432 return Vec::new();
433 }
434 let ptr = tallies.as_mut_ptr() as usize;
435 let count = || {
436 use rayon::prelude::*;
437 (0..nchunks).into_par_iter().for_each(|index| {
438 let n = memchr::memchr_iter(b'\n', &bytes[cuts[index]..cuts[index + 1]]).count();
439 unsafe {
440 (*(ptr as *mut ChunkTally).add(index)).n = n;
441 }
442 });
443 };
444 if nchunks < 2 {
445 count();
446 } else {
447 on_pool(nchunks, count);
448 }
449 tallies.into_iter().map(|tally| tally.n).collect()
450}
451
452#[cfg(feature = "parallel")]
459pub(crate) fn plan_coord_chunks(
460 bytes: &[u8],
461 start: usize,
462 n_rows: usize,
463 workers: usize,
464) -> Result<(Vec<(usize, usize)>, usize), ParseError> {
465 if n_rows == 0 {
466 return Ok((Vec::new(), start));
467 }
468 if start >= bytes.len() {
469 return Err(ParseError::IncompleteFrame);
470 }
471 let mut window = n_rows.saturating_mul(96).max(1);
472 loop {
473 let end = start.saturating_add(window).min(bytes.len());
474 let region = &bytes[start..end];
475 let cuts = newline_cuts(region, workers);
476 let counts = count_newline_chunks(region, &cuts);
477 let total: usize = counts.iter().sum();
478 if total >= n_rows || end == bytes.len() {
479 if total < n_rows {
480 return Err(ParseError::IncompleteFrame);
481 }
482 return chunks_from_counts(bytes, start, &cuts, &counts, n_rows);
483 }
484 window = window.saturating_mul(2);
485 }
486}
487
488#[cfg(feature = "parallel")]
489fn chunks_from_counts(
490 bytes: &[u8],
491 start: usize,
492 cuts: &[usize],
493 counts: &[usize],
494 n_rows: usize,
495) -> Result<(Vec<(usize, usize)>, usize), ParseError> {
496 let mut need = n_rows;
497 let mut chunks = Vec::new();
498 let mut end_abs = start;
499 for index in 0..counts.len() {
500 if need == 0 {
501 break;
502 }
503 let have = counts[index];
504 if have == 0 {
505 continue;
506 }
507 let from = start + cuts[index];
508 let to = start + cuts[index + 1];
509 let take = have.min(need);
510 let chunk_end = if take == have && to > from && bytes[to - 1] == b'\n' {
511 to
512 } else {
513 let mut seen = 0usize;
514 let mut found = None;
515 for rel in memchr::memchr_iter(b'\n', &bytes[from..to]) {
516 seen += 1;
517 if seen == take {
518 found = Some(from + rel + 1);
519 break;
520 }
521 }
522 found.ok_or(ParseError::IncompleteFrame)?
523 };
524 chunks.push((from, take));
525 end_abs = chunk_end;
526 need -= take;
527 }
528 if need != 0 {
529 return Err(ParseError::IncompleteFrame);
530 }
531 Ok((chunks, end_abs))
532}
533
534#[cfg(feature = "parallel")]
539pub(crate) fn fill_rows_from_chunks(
540 bytes: &[u8],
541 chunks: &[(usize, usize)],
542 spec_version: u32,
543 start_id: u64,
544 workers: usize,
545 xyz: *mut f64,
546 ids: *mut u64,
547 flags: *mut u8,
548) -> Result<(), ParseError> {
549 if chunks.is_empty() {
550 return Ok(());
551 }
552 let mut row0 = Vec::with_capacity(chunks.len());
553 let mut acc = 0usize;
554 for (_, nrows) in chunks {
555 row0.push(acc);
556 acc += *nrows;
557 }
558 let xyz_ptr = xyz as usize;
559 let ids_ptr = ids as usize;
560 let flags_ptr = flags as usize;
561 let fill = || {
562 use rayon::prelude::*;
563 chunks
564 .par_iter()
565 .zip(row0.par_iter().copied())
566 .try_for_each(|(&(start, nrows), mut row)| {
567 let mut pos = start;
568 for _ in 0..nrows {
569 let ((xyz, fixed, atom_id), next) =
570 parse_coord_row_at(bytes, pos, spec_version, start_id + row as u64)?;
571 unsafe {
572 std::ptr::copy_nonoverlapping(
573 xyz.as_ptr(),
574 (xyz_ptr as *mut f64).add(row * 3),
575 3,
576 );
577 if ids_ptr != 0 {
578 *((ids_ptr as *mut u64).add(row)) = atom_id;
579 }
580 if flags_ptr != 0 {
581 let mask = u8::from(fixed[0])
582 | (u8::from(fixed[1]) << 1)
583 | (u8::from(fixed[2]) << 2);
584 *((flags_ptr as *mut u8).add(row)) = mask;
585 }
586 }
587 pos = next;
588 row += 1;
589 }
590 Ok(())
591 })
592 };
593 if workers < 2 {
594 let mut row_at = 0usize;
595 for &(start, nrows) in chunks {
596 let mut pos = start;
597 for _ in 0..nrows {
598 let ((row_xyz, fixed, atom_id), next) =
599 parse_coord_row_at(bytes, pos, spec_version, start_id + row_at as u64)?;
600 unsafe {
601 std::ptr::copy_nonoverlapping(row_xyz.as_ptr(), xyz.add(row_at * 3), 3);
602 if ids_ptr != 0 {
603 *((ids_ptr as *mut u64).add(row_at)) = atom_id;
604 }
605 if flags_ptr != 0 {
606 let mask = u8::from(fixed[0])
607 | (u8::from(fixed[1]) << 1)
608 | (u8::from(fixed[2]) << 2);
609 *((flags_ptr as *mut u8).add(row_at)) = mask;
610 }
611 }
612 pos = next;
613 row_at += 1;
614 }
615 }
616 Ok(())
617 } else if chunks.len() < 2 {
618 fill()
619 } else {
620 on_pool(workers, fill)
621 }
622}
623
624#[cfg(feature = "parallel")]
626fn newline_index_exact(cuts: &[usize], bytes: &[u8], base: usize) -> Vec<usize> {
627 let nchunks = cuts.len() - 1;
628 let counts = count_newline_chunks(bytes, cuts);
629 let mut slots = vec![0usize; nchunks];
630 let mut total = 0usize;
631 for index in 0..nchunks {
632 slots[index] = total;
633 total += counts[index];
634 }
635 let mut out = Vec::<usize>::with_capacity(total);
636 let ptr = out.as_mut_ptr() as usize;
637 let store = || {
638 use rayon::prelude::*;
639 (0..nchunks).into_par_iter().for_each(|index| {
640 let from = cuts[index];
641 let mut at = slots[index];
642 for rel in memchr::memchr_iter(b'\n', &bytes[from..cuts[index + 1]]) {
643 unsafe {
644 *((ptr as *mut usize).add(at)) = base + from + rel;
645 }
646 at += 1;
647 }
648 });
649 };
650 if nchunks < 2 {
651 store();
652 } else {
653 on_pool(nchunks.max(1), store);
654 }
655 unsafe { out.set_len(total) };
657 out
658}
659
660#[cfg(feature = "parallel")]
661#[allow(dead_code)]
662fn write_coord_group(
663 bytes: &[u8],
664 group: &[(usize, usize)],
665 row0: usize,
666 spec_version: u32,
667 start_id: u64,
668 symbol: Option<&std::sync::Arc<str>>,
669 xyz_ptr: usize,
670 ids_ptr: usize,
671 flags_ptr: usize,
672 atoms_ptr: usize,
673) -> Result<(), ParseError> {
674 let mut shares = symbol.map(|symbol| symbol_shares(symbol, group.len()));
675 for (offset, &(start, end)) in group.iter().enumerate() {
676 let row_index = row0 + offset;
677 let default_id = start_id + row_index as u64;
678 let ((row, fixed, atom_id), _next) =
679 parse_coord_row_at(&bytes[start..end], 0, spec_version, default_id)?;
680 unsafe {
681 std::ptr::copy_nonoverlapping(
682 row.as_ptr(),
683 (xyz_ptr as *mut f64).add(row_index * 3),
684 3,
685 );
686 *((ids_ptr as *mut u64).add(row_index)) = atom_id;
687 *((flags_ptr as *mut [bool; 3]).add(row_index)) = fixed;
688 if let Some(shares) = shares.as_mut() {
689 let slot = (xyz_ptr as *mut f64).add(row_index * 3);
690 let atom = crate::types::AtomDatum::at_slot(shares.take(), slot, fixed, atom_id);
691 (*(atoms_ptr as *mut std::mem::MaybeUninit<crate::types::AtomDatum>)
692 .add(row_index))
693 .write(atom);
694 }
695 }
696 }
697 Ok(())
698}
699
700pub trait LineStream<'a> {
701 fn next_line(&mut self) -> Option<&'a str>;
702 fn peek_line(&mut self) -> Option<&'a str>;
703 fn try_take_coord_block(
706 &mut self,
707 _n: usize,
708 _spec_version: u32,
709 _start_id: u64,
710 _symbol: &std::sync::Arc<str>,
711 _workers: usize,
712 ) -> Option<CoordRows> {
713 None
714 }
715 fn remainder_is_whitespace(&mut self) -> bool {
719 false
720 }
721 fn blank_line_opens_section(&mut self) -> bool {
725 false
726 }
727
728 fn blank_opens_velocity_block(&mut self) -> bool {
732 true
733 }
734
735 fn next_coord_row(
741 &mut self,
742 spec_version: u32,
743 default_id: u64,
744 ) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
745 let line = self.next_line().ok_or(ParseError::IncompleteFrame)?;
746 parse_coord_row(line, spec_version, default_id)
747 }
748
749 fn next_vector_row(
753 &mut self,
754 spec_version: u32,
755 ) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
756 let line = self.next_line().ok_or(ParseError::IncompleteFrame)?;
757 parse_vector_section_row(line, spec_version)
758 }
759}
760
761fn take_vector_row<'a>(
762 lines: &mut impl LineStream<'a>,
763 spec_version: u32,
764 section: &str,
765) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
766 match lines.next_vector_row(spec_version) {
767 Ok(row) => Ok(row),
768 Err(ParseError::IncompleteFrame) => Err(ParseError::IncompleteSection(section.to_string())),
769 Err(err) => Err(err),
770 }
771}
772
773impl<'a, I> LineStream<'a> for Peekable<I>
774where
775 I: Iterator<Item = &'a str> + Clone,
776{
777 #[inline]
778 fn next_line(&mut self) -> Option<&'a str> {
779 Iterator::next(self)
780 }
781 #[inline]
782 fn peek_line(&mut self) -> Option<&'a str> {
783 Peekable::peek(self).copied()
784 }
785 fn blank_line_opens_section(&mut self) -> bool {
786 let mut probe = self.clone();
787 let Some(blank) = probe.next_line() else {
788 return false;
789 };
790 if !blank.trim().is_empty() {
791 return false;
792 }
793 section_follows(&mut probe)
794 }
795
796 fn blank_opens_velocity_block(&mut self) -> bool {
797 match self.peek_line() {
798 Some(line) if line.trim().is_empty() => self.blank_line_opens_section(),
799 _ => true,
800 }
801 }
802}
803
804pub(crate) fn line_has_component_label(line: &str) -> bool {
806 let mut words = line.split_ascii_whitespace();
807 while let Some(word) = words.next() {
808 if word.eq_ignore_ascii_case("of")
809 && words
810 .next()
811 .is_some_and(|next| next.eq_ignore_ascii_case("component"))
812 {
813 return true;
814 }
815 }
816 false
817}
818
819fn section_follows<'a>(lines: &mut impl LineStream<'a>) -> bool {
820 let Some(symbol) = lines.next_line() else {
821 return false;
822 };
823 if symbol.trim().is_empty() || symbol.trim_start().starts_with('{') {
824 return false;
825 }
826 let Some(label) = lines.next_line() else {
827 return false;
828 };
829 line_has_component_label(label)
830}
831
832pub const NUMERIC_STACK_WIDTH: usize = COORD_ROW_MIN_COLUMNS + 1;
836
837#[inline]
847pub fn parse_line_of_range_f64_stack(
848 line: &str,
849 min: usize,
850 max: usize,
851 defaults: &[f64],
852 out: &mut [f64; NUMERIC_STACK_WIDTH],
853) -> Result<usize, ParseError> {
854 debug_assert!(max <= NUMERIC_STACK_WIDTH && min <= max && defaults.len() >= max);
855 let bytes = line.as_bytes();
856 let n = bytes.len();
857 let mut i = 0usize;
858 let mut found = 0usize;
859 while found < max {
860 while i < n && bytes[i].is_ascii_whitespace() {
861 i += 1;
862 }
863 if i >= n {
864 break;
865 }
866 let end = bytes[i..]
867 .iter()
868 .position(|b| b.is_ascii_whitespace())
869 .map(|k| i + k)
870 .unwrap_or(n);
871 let token = std::str::from_utf8(&bytes[i..end]).unwrap_or("");
872 if is_nonfinite_spelling(token) {
873 return Err(ParseError::NonFiniteValue(token.to_string()));
874 }
875 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..])
876 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
877 let next = i + consumed;
878 if next != end {
880 return Err(ParseError::InvalidNumberFormat(format!(
881 "invalid float: {token}"
882 )));
883 }
884 if !val.is_finite() {
885 return Err(ParseError::NonFiniteValue(token.to_string()));
886 }
887 out[found] = val;
888 found += 1;
889 i = next;
890 }
891 while i < n && bytes[i].is_ascii_whitespace() {
892 i += 1;
893 }
894 if i < n {
896 return Err(ParseError::InvalidVectorLength {
897 expected: max,
898 found: found + 1,
899 });
900 }
901 if found < min || found > max {
902 return Err(ParseError::InvalidVectorLength {
903 expected: max,
904 found,
905 });
906 }
907 while found < max {
908 out[found] = defaults[found];
909 found += 1;
910 }
911 Ok(found)
912}
913
914pub fn parse_line_of_n_f64(line: &str, n: usize) -> Result<Vec<f64>, ParseError> {
926 if n <= NUMERIC_STACK_WIDTH {
927 let defaults = [0.0f64; NUMERIC_STACK_WIDTH];
928 let mut buf = [0.0f64; NUMERIC_STACK_WIDTH];
929 parse_line_of_range_f64_stack(line, n, n, &defaults, &mut buf)?;
930 return Ok(buf[..n].to_vec());
931 }
932 let mut values = Vec::with_capacity(n.min(max_tokens_on_line(line)));
933 for token in line.split_ascii_whitespace() {
934 values.push(parse_finite_float_token(token)?);
935 }
936 if values.len() == n {
937 Ok(values)
938 } else {
939 Err(ParseError::InvalidVectorLength {
940 expected: n,
941 found: values.len(),
942 })
943 }
944}
945
946pub fn parse_line_of_range_f64(
954 line: &str,
955 min: usize,
956 max: usize,
957 defaults: &[f64],
958) -> Result<Vec<f64>, ParseError> {
959 if max <= NUMERIC_STACK_WIDTH {
960 let mut buf = [0.0f64; NUMERIC_STACK_WIDTH];
961 parse_line_of_range_f64_stack(line, min, max, defaults, &mut buf)?;
962 return Ok(buf[..max].to_vec());
963 }
964 let mut values = Vec::with_capacity(max);
965 for token in line.split_ascii_whitespace() {
966 values.push(parse_finite_float_token(token)?);
967 }
968 if values.len() < min || values.len() > max {
969 return Err(ParseError::InvalidVectorLength {
970 expected: max,
971 found: values.len(),
972 });
973 }
974 while values.len() < max {
975 let idx = values.len();
976 values.push(defaults[idx]);
977 }
978 Ok(values)
979}
980
981fn is_nonfinite_spelling(token: &str) -> bool {
985 let rest = token
986 .strip_prefix('+')
987 .or_else(|| token.strip_prefix('-'))
988 .unwrap_or(token);
989 match rest.as_bytes().first() {
990 Some(b) if b.is_ascii_digit() || *b == b'.' => return false,
991 None => return false,
992 _ => {}
993 }
994 rest.eq_ignore_ascii_case("nan")
995 || rest.eq_ignore_ascii_case("inf")
996 || rest.eq_ignore_ascii_case("infinity")
997}
998
999fn parse_finite_float_token(token: &str) -> Result<f64, ParseError> {
1000 if is_nonfinite_spelling(token) {
1001 return Err(ParseError::NonFiniteValue(token.to_string()));
1002 }
1003 let (val, consumed) = fast_float2::parse_partial::<f64, _>(token.as_bytes())
1004 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
1005 if consumed != token.len() {
1006 return Err(ParseError::InvalidNumberFormat(format!(
1007 "invalid float: {token}"
1008 )));
1009 }
1010 if !val.is_finite() {
1011 return Err(ParseError::NonFiniteValue(token.to_string()));
1012 }
1013 Ok(val)
1014}
1015
1016fn skip_ascii_ws(bytes: &[u8], mut i: usize) -> usize {
1017 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
1018 i += 1;
1019 }
1020 i
1021}
1022
1023fn take_token(line: &str, i: usize) -> Option<(&str, usize)> {
1024 let bytes = line.as_bytes();
1025 if i >= bytes.len() {
1026 return None;
1027 }
1028 let mut j = i;
1029 while j < bytes.len() && !bytes[j].is_ascii_whitespace() {
1030 j += 1;
1031 }
1032 Some((&line[i..j], j))
1033}
1034
1035fn parse_fixed_flag(token: &str, spec_version: u32) -> Result<[bool; 3], ParseError> {
1038 let bad = || {
1039 ParseError::InvalidFixedFlag(format!(
1040 "column 4 flag {token:?} is not a legal integer for spec {spec_version}"
1041 ))
1042 };
1043 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
1044 return Err(bad());
1045 }
1046 let val: u64 = token.parse::<u64>().map_err(|_| bad())?;
1047 if spec_version < 2 {
1048 return match val {
1049 0 => Ok([false, false, false]),
1050 1 => Ok([true, true, true]),
1051 _ => Err(bad()),
1052 };
1053 }
1054 if val > 7 {
1055 return Err(bad());
1056 }
1057 let v = val as u8;
1058 Ok([v & 1 != 0, v & 2 != 0, v & 4 != 0])
1059}
1060
1061fn parse_atom_id_token(token: &str) -> Result<u64, ParseError> {
1064 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
1065 return Err(ParseError::InvalidNumberFormat(format!(
1066 "invalid integer: {token}"
1067 )));
1068 }
1069 token.parse::<u64>().map_err(|_| {
1070 ParseError::InvalidNumberFormat(format!("integer out of range 0..=2^64-1: {token}"))
1071 })
1072}
1073
1074fn parse_coord_row(
1078 line: &str,
1079 spec_version: u32,
1080 default_id: u64,
1081) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
1082 parse_vector_row(line, spec_version, Some(default_id))
1083}
1084
1085fn parse_vector_section_row(
1087 line: &str,
1088 spec_version: u32,
1089) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
1090 parse_vector_row(line, spec_version, None)
1091}
1092
1093fn parse_vector_row(
1094 line: &str,
1095 spec_version: u32,
1096 default_id: Option<u64>,
1097) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
1098 let (row, end) = parse_coord_fields(line.as_bytes(), 0, spec_version, default_id)?;
1099 if end != line.len() {
1100 let found = 5 + count_tokens_on_line(line.as_bytes(), end);
1101 return Err(ParseError::InvalidVectorLength { expected: 5, found });
1102 }
1103 Ok(row)
1104}
1105
1106#[inline(always)]
1108fn is_field_ws(b: u8) -> bool {
1109 matches!(b, b' ' | b'\t' | b'\r' | 0x0c)
1110}
1111
1112#[inline(always)]
1113fn skip_field_ws(bytes: &[u8], mut i: usize) -> usize {
1114 while i < bytes.len() && is_field_ws(bytes[i]) {
1115 i += 1;
1116 }
1117 i
1118}
1119
1120fn token_on_line(bytes: &[u8], i: usize) -> String {
1121 let mut j = i;
1122 while j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
1123 j += 1;
1124 }
1125 String::from_utf8_lossy(&bytes[i..j]).into_owned()
1126}
1127
1128fn count_tokens_on_line(bytes: &[u8], mut i: usize) -> usize {
1129 let mut n = 0usize;
1130 loop {
1131 i = skip_field_ws(bytes, i);
1132 if i >= bytes.len() || bytes[i] == b'\n' {
1133 break;
1134 }
1135 n += 1;
1136 while i < bytes.len() && !is_field_ws(bytes[i]) && bytes[i] != b'\n' {
1137 i += 1;
1138 }
1139 }
1140 n
1141}
1142
1143fn utf8_or_number(bytes: &[u8], line_start: usize, i: usize) -> ParseError {
1145 let end = bytes[line_start..]
1146 .iter()
1147 .position(|b| *b == b'\n')
1148 .map(|p| line_start + p)
1149 .unwrap_or(bytes.len());
1150 if std::str::from_utf8(&bytes[line_start..end]).is_err() {
1151 return ParseError::ValidationError("invalid UTF-8".into());
1152 }
1153 ParseError::InvalidNumberFormat(format!("invalid float: {}", token_on_line(bytes, i)))
1154}
1155
1156#[inline(always)]
1157fn parse_f64_at(bytes: &[u8], i: usize, line_start: usize) -> Result<(f64, usize), ParseError> {
1158 if i >= bytes.len() || bytes[i] == b'\n' {
1159 return Err(ParseError::InvalidVectorLength {
1160 expected: 5,
1161 found: 0,
1162 });
1163 }
1164 if bytes[i] >= 0x80 {
1165 return Err(utf8_or_number(bytes, line_start, i));
1166 }
1167 if looks_like_nonfinite(bytes, i) {
1168 let token = token_on_line(bytes, i);
1169 if is_nonfinite_spelling(&token) {
1170 return Err(ParseError::NonFiniteValue(token));
1171 }
1172 }
1173 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..]).map_err(|_| {
1174 ParseError::InvalidNumberFormat(format!("invalid float: {}", token_on_line(bytes, i)))
1175 })?;
1176 let next = i + consumed;
1177 let boundary = next >= bytes.len() || is_field_ws(bytes[next]) || bytes[next] == b'\n';
1178 if !boundary || !val.is_finite() {
1179 let token = token_on_line(bytes, i);
1180 if is_nonfinite_spelling(&token) || !val.is_finite() {
1181 return Err(ParseError::NonFiniteValue(token));
1182 }
1183 return Err(ParseError::InvalidNumberFormat(format!(
1184 "invalid float: {token}"
1185 )));
1186 }
1187 Ok((val, next))
1188}
1189
1190#[inline(always)]
1191fn parse_flag_at(
1192 bytes: &[u8],
1193 i: usize,
1194 spec_version: u32,
1195) -> Result<([bool; 3], usize), ParseError> {
1196 if i >= bytes.len() || bytes[i] == b'\n' {
1197 return Err(ParseError::InvalidVectorLength {
1198 expected: 5,
1199 found: 3,
1200 });
1201 }
1202 let bad = || {
1203 ParseError::InvalidFixedFlag(format!(
1204 "column 4 flag {:?} is not a legal integer for spec {spec_version}",
1205 token_on_line(bytes, i)
1206 ))
1207 };
1208 if !bytes[i].is_ascii_digit() {
1209 return Err(bad());
1210 }
1211 let mut j = i;
1212 let mut val: u64 = 0;
1213 while j < bytes.len() && bytes[j].is_ascii_digit() {
1214 let d = u64::from(bytes[j] - b'0');
1215 val = val
1216 .checked_mul(10)
1217 .and_then(|v| v.checked_add(d))
1218 .ok_or_else(bad)?;
1219 j += 1;
1220 }
1221 if j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
1222 return Err(bad());
1223 }
1224 let fixed = if spec_version < 2 {
1225 match val {
1226 0 => [false, false, false],
1227 1 => [true, true, true],
1228 _ => return Err(bad()),
1229 }
1230 } else if val <= 7 {
1231 let v = val as u8;
1232 [v & 1 != 0, v & 2 != 0, v & 4 != 0]
1233 } else {
1234 return Err(bad());
1235 };
1236 Ok((fixed, j))
1237}
1238
1239#[inline(always)]
1240fn parse_id_at(bytes: &[u8], i: usize) -> Result<(u64, usize), ParseError> {
1241 if i >= bytes.len() || bytes[i] == b'\n' || !bytes[i].is_ascii_digit() {
1242 return Err(ParseError::InvalidNumberFormat(format!(
1243 "invalid integer: {}",
1244 token_on_line(bytes, i)
1245 )));
1246 }
1247 let mut j = i;
1248 let mut val: u64 = 0;
1249 while j < bytes.len() && bytes[j].is_ascii_digit() {
1250 let d = u64::from(bytes[j] - b'0');
1251 val = val
1252 .checked_mul(10)
1253 .and_then(|v| v.checked_add(d))
1254 .ok_or_else(|| {
1255 ParseError::InvalidNumberFormat(format!(
1256 "integer out of range 0..=2^64-1: {}",
1257 token_on_line(bytes, i)
1258 ))
1259 })?;
1260 j += 1;
1261 }
1262 if j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
1263 return Err(ParseError::InvalidNumberFormat(format!(
1264 "invalid integer: {}",
1265 token_on_line(bytes, i)
1266 )));
1267 }
1268 Ok((val, j))
1269}
1270
1271type ParsedCoord = ([f64; 3], [bool; 3], u64);
1273
1274#[inline(always)]
1281fn parse_coord_fields(
1282 bytes: &[u8],
1283 i: usize,
1284 spec_version: u32,
1285 default_id: Option<u64>,
1286) -> Result<(ParsedCoord, usize), ParseError> {
1287 if let Some(parsed) = parse_coord_fields_fast(bytes, i, spec_version, default_id) {
1288 return Ok(parsed);
1289 }
1290 parse_coord_fields_detailed(bytes, i, spec_version, default_id)
1291}
1292
1293#[inline(always)]
1295fn parse_coord_fields_fast(
1296 bytes: &[u8],
1297 i: usize,
1298 spec_version: u32,
1299 default_id: Option<u64>,
1300) -> Option<(ParsedCoord, usize)> {
1301 let mut i = skip_field_ws(bytes, i);
1302 let mut xyz = [0.0f64; 3];
1303 for dest in &mut xyz {
1304 if i >= bytes.len() || bytes[i] == b'\n' || bytes[i] >= 0x80 {
1305 return None;
1306 }
1307 let (val, next) = crate::fast_dec::parse_f64_hot(bytes, i)?;
1308 if next < bytes.len() && !is_field_ws(bytes[next]) && bytes[next] != b'\n' {
1309 return None;
1310 }
1311 *dest = val;
1312 i = skip_field_ws(bytes, next);
1313 }
1314 let (fixed, next) = parse_flag_fast(bytes, i, spec_version)?;
1315 i = skip_field_ws(bytes, next);
1316 let (atom_id, next) = if i >= bytes.len() || bytes[i] == b'\n' {
1317 (default_id?, i)
1318 } else {
1319 parse_u64_fast(bytes, i)?
1320 };
1321 i = skip_field_ws(bytes, next);
1322 if i < bytes.len() && bytes[i] != b'\n' {
1323 return None;
1324 }
1325 if i < bytes.len() && bytes[i] == b'\n' {
1326 i += 1;
1327 }
1328 Some(((xyz, fixed, atom_id), i))
1329}
1330
1331#[inline(always)]
1332fn parse_u64_fast(bytes: &[u8], i: usize) -> Option<(u64, usize)> {
1333 if i >= bytes.len() || !bytes[i].is_ascii_digit() {
1334 return None;
1335 }
1336 let mut j = i;
1337 let mut val: u64 = 0;
1338 let mut digits = 0u32;
1339 while j < bytes.len() && bytes[j].is_ascii_digit() {
1340 digits += 1;
1341 if digits > 18 {
1343 return None;
1344 }
1345 val = val * 10 + u64::from(bytes[j] - b'0');
1346 j += 1;
1347 }
1348 if j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
1349 return None;
1350 }
1351 Some((val, j))
1352}
1353
1354#[inline(always)]
1355fn parse_flag_fast(bytes: &[u8], i: usize, spec_version: u32) -> Option<([bool; 3], usize)> {
1356 let (val, j) = parse_u64_fast(bytes, i)?;
1357 let fixed = if spec_version < 2 {
1358 match val {
1359 0 => [false, false, false],
1360 1 => [true, true, true],
1361 _ => return None,
1362 }
1363 } else if val <= 7 {
1364 let v = val as u8;
1365 [v & 1 != 0, v & 2 != 0, v & 4 != 0]
1366 } else {
1367 return None;
1368 };
1369 Some((fixed, j))
1370}
1371
1372#[cold]
1373#[inline(never)]
1374fn parse_coord_fields_detailed(
1375 bytes: &[u8],
1376 i: usize,
1377 spec_version: u32,
1378 default_id: Option<u64>,
1379) -> Result<(ParsedCoord, usize), ParseError> {
1380 let line_start = i;
1381 let mut i = skip_field_ws(bytes, i);
1382 if i >= bytes.len() || bytes[i] == b'\n' {
1383 return Err(ParseError::InvalidVectorLength {
1384 expected: 5,
1385 found: 0,
1386 });
1387 }
1388 let mut xyz = [0.0f64; 3];
1389 for (slot, dest) in xyz.iter_mut().enumerate() {
1390 if i >= bytes.len() || bytes[i] == b'\n' {
1391 return Err(ParseError::InvalidVectorLength {
1392 expected: 5,
1393 found: slot,
1394 });
1395 }
1396 if bytes[i] >= 0x80 {
1397 return Err(utf8_or_number(bytes, line_start, i));
1398 }
1399 let (val, next) = parse_f64_at(bytes, i, line_start)?;
1400 *dest = val;
1401 i = skip_field_ws(bytes, next);
1402 }
1403 let (fixed, next) = parse_flag_at(bytes, i, spec_version)?;
1404 i = skip_field_ws(bytes, next);
1405 let (atom_id, next) = if i >= bytes.len() || bytes[i] == b'\n' {
1406 (
1407 default_id.ok_or(ParseError::InvalidVectorLength {
1408 expected: 5,
1409 found: 4,
1410 })?,
1411 i,
1412 )
1413 } else {
1414 parse_id_at(bytes, i)?
1415 };
1416 i = skip_field_ws(bytes, next);
1417 if i < bytes.len() && bytes[i] != b'\n' {
1418 let found = 5 + count_tokens_on_line(bytes, i);
1419 return Err(ParseError::InvalidVectorLength { expected: 5, found });
1420 }
1421 if i < bytes.len() && bytes[i] == b'\n' {
1422 i += 1;
1423 }
1424 Ok(((xyz, fixed, atom_id), i))
1425}
1426
1427pub(crate) fn parse_coord_row_at(
1432 bytes: &[u8],
1433 i: usize,
1434 spec_version: u32,
1435 default_id: u64,
1436) -> Result<(ParsedCoord, usize), ParseError> {
1437 if i >= bytes.len() {
1438 return Err(ParseError::IncompleteFrame);
1439 }
1440 parse_coord_fields(bytes, i, spec_version, Some(default_id))
1441}
1442
1443pub(crate) fn parse_vector_row_at(
1445 bytes: &[u8],
1446 i: usize,
1447 spec_version: u32,
1448) -> Result<(ParsedCoord, usize), ParseError> {
1449 if i >= bytes.len() {
1450 return Err(ParseError::IncompleteFrame);
1451 }
1452 parse_coord_fields(bytes, i, spec_version, None)
1453}
1454
1455fn parse_scalar_section_row(
1457 line: &str,
1458 spec_version: u32,
1459) -> Result<(f64, [bool; 3], u64), ParseError> {
1460 let bytes = line.as_bytes();
1461 let i = skip_ascii_ws(bytes, 0);
1462 let (value, i) = parse_finite_at(line, i)?;
1463 let i = skip_ascii_ws(bytes, i);
1464 let (flag_tok, i) = take_token(line, i).ok_or(ParseError::InvalidVectorLength {
1465 expected: 3,
1466 found: 1,
1467 })?;
1468 let fixed = parse_fixed_flag(flag_tok, spec_version)?;
1469 let i = skip_ascii_ws(bytes, i);
1470 let (id_tok, i) = take_token(line, i).ok_or(ParseError::InvalidVectorLength {
1471 expected: 3,
1472 found: 2,
1473 })?;
1474 let atom_id = parse_atom_id_token(id_tok)?;
1475 let i = skip_ascii_ws(bytes, i);
1476 if i < bytes.len() {
1477 let found = 3 + line[i..].split_ascii_whitespace().count();
1478 return Err(ParseError::InvalidVectorLength { expected: 3, found });
1479 }
1480 Ok((value, fixed, atom_id))
1481}
1482
1483fn parse_finite_at(line: &str, i: usize) -> Result<(f64, usize), ParseError> {
1484 let bytes = line.as_bytes();
1485 if i >= bytes.len() {
1486 return Err(ParseError::InvalidVectorLength {
1487 expected: 3,
1488 found: 0,
1489 });
1490 }
1491 if looks_like_nonfinite(bytes, i) {
1494 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
1495 if is_nonfinite_spelling(token) {
1496 return Err(ParseError::NonFiniteValue(token.to_string()));
1497 }
1498 }
1499 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..]).map_err(|_| {
1500 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
1501 ParseError::InvalidNumberFormat(format!("invalid float: {token}"))
1502 })?;
1503 let next = i + consumed;
1504 let boundary = next == bytes.len() || bytes[next].is_ascii_whitespace();
1505 if !boundary || !val.is_finite() {
1506 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
1507 if is_nonfinite_spelling(token) || !val.is_finite() {
1508 return Err(ParseError::NonFiniteValue(token.to_string()));
1509 }
1510 return Err(ParseError::InvalidNumberFormat(format!(
1511 "invalid float: {token}"
1512 )));
1513 }
1514 Ok((val, next))
1515}
1516
1517fn looks_like_nonfinite(bytes: &[u8], i: usize) -> bool {
1520 let mut j = i;
1521 if bytes[j] == b'+' || bytes[j] == b'-' {
1522 j += 1;
1523 if j >= bytes.len() {
1524 return false;
1525 }
1526 }
1527 let b = bytes[j];
1528 !b.is_ascii_digit() && b != b'.'
1529}
1530
1531fn labels_equal(found: &str, expected: &str) -> bool {
1533 let mut found_words = found.split_ascii_whitespace();
1534 let mut expected_words = expected.split_ascii_whitespace();
1535 loop {
1536 match (found_words.next(), expected_words.next()) {
1537 (Some(a), Some(b)) => {
1538 if !a.eq_ignore_ascii_case(b) {
1539 return false;
1540 }
1541 }
1542 (None, None) => return true,
1543 _ => return false,
1544 }
1545 }
1546}
1547
1548fn label_has_prefix(line: &str, prefix: &str) -> bool {
1550 let mut found_words = line.split_ascii_whitespace();
1551 for word in prefix.split_ascii_whitespace() {
1552 match found_words.next() {
1553 Some(got) if got.eq_ignore_ascii_case(word) => {}
1554 _ => return false,
1555 }
1556 }
1557 true
1558}
1559
1560fn token_count(line: &str) -> usize {
1561 line.split_ascii_whitespace().count()
1562}
1563
1564fn max_tokens_on_line(line: &str) -> usize {
1569 line.len().div_ceil(2)
1570}
1571
1572fn json_u64(value: &Value) -> Option<u64> {
1573 value.as_u64()
1574}
1575
1576fn validate_section_arity_value(value: &Value) -> Result<(), ParseError> {
1577 let obj = value.as_object().ok_or_else(|| {
1578 metadata_json_error("section_arity must be an object mapping section names to integers")
1579 })?;
1580 for (name, entry) in obj {
1581 let n = json_u64(entry).ok_or_else(|| {
1582 metadata_json_error(format!("section_arity.{name} must be an integer >= 1"))
1583 })?;
1584 if n < 1 {
1585 return Err(ParseError::InvalidCount(format!(
1586 "section_arity.{name} must be >= 1"
1587 )));
1588 }
1589 if let Some(expect) = crate::types::reserved_section_value_arity(name)
1591 && n != expect
1592 {
1593 return Err(ParseError::InvalidVectorLength {
1594 expected: usize::try_from(expect).unwrap_or(usize::MAX),
1595 found: usize::try_from(n).unwrap_or(usize::MAX),
1596 });
1597 }
1598 }
1599 Ok(())
1600}
1601
1602pub(crate) fn parse_count_line(line: &str, n: usize) -> Result<Vec<usize>, ParseError> {
1606 let mut values = Vec::with_capacity(n.min(max_tokens_on_line(line)));
1607 for token in line.split_ascii_whitespace() {
1608 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
1609 return Err(ParseError::InvalidCount(format!(
1610 "count field is not an unsigned integer: {token}"
1611 )));
1612 }
1613 let val: u64 = token.parse::<u64>().map_err(|_| {
1614 ParseError::InvalidCount(format!("count field is out of range: {token}"))
1615 })?;
1616 let val = usize::try_from(val).map_err(|_| {
1617 ParseError::InvalidCount(format!("count field is out of range: {token}"))
1618 })?;
1619 values.push(val);
1620 }
1621 if values.len() == n {
1622 Ok(values)
1623 } else {
1624 Err(ParseError::InvalidVectorLength {
1625 expected: n,
1626 found: values.len(),
1627 })
1628 }
1629}
1630
1631pub(crate) fn line_is_whitespace(line: &str) -> bool {
1633 line.split_ascii_whitespace().next().is_none()
1634}
1635
1636pub(crate) fn read_zero_atom_tail<'a>(
1642 mut next_line: impl FnMut() -> Option<&'a str>,
1643) -> Result<(), ParseError> {
1644 let bad = || {
1645 ParseError::InvalidCount("a zero-atom frame requires whitespace-only lines 8 and 9".into())
1646 };
1647 match next_line() {
1648 None => Ok(()),
1649 Some(line) if line_is_whitespace(line) => match next_line() {
1650 None => Ok(()),
1651 Some(line) if line_is_whitespace(line) => Ok(()),
1652 Some(_) => Err(bad()),
1653 },
1654 Some(_) => Err(bad()),
1655 }
1656}
1657
1658fn metadata_json_error(message: impl Into<String>) -> ParseError {
1659 ParseError::InvalidMetadataJson(message.into())
1660}
1661
1662fn validate_metadata_number(key: &str, value: &Value) -> Result<(), ParseError> {
1663 if value.as_f64().is_some() {
1664 Ok(())
1665 } else {
1666 Err(metadata_json_error(format!(
1667 "{key} must be a finite number"
1668 )))
1669 }
1670}
1671
1672fn validate_metadata_integer(key: &str, value: &Value) -> Result<(), ParseError> {
1673 if value.as_u64().is_some() {
1674 Ok(())
1675 } else {
1676 Err(metadata_json_error(format!(
1677 "{key} must be a non-negative integer"
1678 )))
1679 }
1680}
1681
1682pub fn validate_metadata_schema(
1690 json_obj: &serde_json::Map<String, Value>,
1691) -> Result<(), ParseError> {
1692 let strict_requested = match json_obj.get(meta::VALIDATE) {
1693 Some(Value::Bool(b)) => *b,
1694 Some(_) => return Err(metadata_json_error("validate must be a boolean")),
1695 None => false,
1696 };
1697
1698 match json_obj.get(meta::SECTIONS) {
1699 Some(Value::Array(values)) => {
1700 if values.iter().any(|entry| !entry.is_string()) {
1701 return Err(metadata_json_error("sections must be an array of strings"));
1702 }
1703 }
1704 Some(_) => return Err(metadata_json_error("sections must be an array of strings")),
1705 None if strict_requested => {
1706 return Err(metadata_json_error(
1707 "validate=true requires a sections array, even when empty",
1708 ));
1709 }
1710 None => {}
1711 }
1712
1713 for (key, value) in json_obj {
1714 match key.as_str() {
1715 meta::CON_SPEC_VERSION | meta::SECTIONS | meta::VALIDATE => {}
1716 meta::CON_SPEC_MINOR => {
1717 if json_u64(value).is_none() {
1718 return Err(metadata_json_error(
1719 "con_spec_minor must be a JSON integer >= 0",
1720 ));
1721 }
1722 }
1723 meta::SECTION_ARITY => validate_section_arity_value(value)?,
1724 meta::ENERGY
1725 | meta::TIME
1726 | meta::TIMESTEP
1727 | meta::CONVERGENCE_FMAX
1728 | meta::CONVERGENCE_ENERGY
1729 | meta::FMAX => validate_metadata_number(key, value)?,
1730 meta::FRAME_INDEX | meta::NEB_BEAD | meta::NEB_BAND => {
1731 validate_metadata_integer(key, value)?
1732 }
1733 meta::GENERATOR if !value.is_string() => {
1734 return Err(metadata_json_error("generator must be a string"));
1735 }
1736 meta::GENERATOR => {}
1737 meta::UNITS | meta::POTENTIAL if !value.is_object() => {
1738 return Err(metadata_json_error(format!("{key} must be an object")));
1739 }
1740 meta::POTENTIAL => {
1741 if let Some(potential_type) = value.get("type")
1742 && !potential_type.is_string()
1743 {
1744 return Err(metadata_json_error("potential.type must be a string"));
1745 }
1746 }
1747 meta::UNITS => {
1748 }
1750 meta::PBC => validate_pbc_metadata(value)?,
1751 meta::BONDS => validate_bonds_metadata(value)?,
1752 meta::LATTICE_VECTORS => validate_lattice_vectors_metadata(value)?,
1753 meta::CONVERGED if !value.is_boolean() => {
1754 return Err(metadata_json_error("converged must be a boolean"));
1755 }
1756 meta::CONVERGED => {}
1757 _ => {}
1758 }
1759 }
1760
1761 Ok(())
1762}
1763
1764fn validate_pbc_metadata(value: &Value) -> Result<(), ParseError> {
1765 let Some(values) = value.as_array() else {
1766 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
1767 };
1768 if values.len() != 3 || values.iter().any(|entry| !entry.is_boolean()) {
1769 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
1770 }
1771 Ok(())
1772}
1773
1774fn validate_bonds_metadata(value: &Value) -> Result<(), ParseError> {
1781 let Some(items) = value.as_array() else {
1782 return Err(metadata_json_error("bonds must be an array"));
1783 };
1784 for (idx, item) in items.iter().enumerate() {
1785 if let Some(pair) = item.as_array() {
1786 if pair.len() != 2 {
1787 return Err(metadata_json_error(format!(
1788 "bonds[{idx}] pair must have exactly two indices"
1789 )));
1790 }
1791 for (k, entry) in pair.iter().enumerate() {
1792 let Some(n) = entry.as_u64() else {
1793 return Err(metadata_json_error(format!(
1794 "bonds[{idx}][{k}] must be a non-negative integer"
1795 )));
1796 };
1797 if n > u32::MAX as u64 {
1798 return Err(metadata_json_error(format!(
1799 "bonds[{idx}][{k}] index exceeds u32"
1800 )));
1801 }
1802 }
1803 continue;
1804 }
1805 if let Some(obj) = item.as_object() {
1806 for key in ["i", "j"] {
1807 let Some(entry) = obj.get(key) else {
1808 return Err(metadata_json_error(format!(
1809 "bonds[{idx}] object must include \"{key}\""
1810 )));
1811 };
1812 let Some(n) = entry.as_u64() else {
1813 return Err(metadata_json_error(format!(
1814 "bonds[{idx}].{key} must be a non-negative integer"
1815 )));
1816 };
1817 if n > u32::MAX as u64 {
1818 return Err(metadata_json_error(format!(
1819 "bonds[{idx}].{key} index exceeds u32"
1820 )));
1821 }
1822 }
1823 if let Some(order) = obj.get("order")
1824 && order.as_i64().is_none()
1825 {
1826 return Err(metadata_json_error(format!(
1827 "bonds[{idx}].order must be an integer when present"
1828 )));
1829 }
1830 continue;
1831 }
1832 return Err(metadata_json_error(format!(
1833 "bonds[{idx}] must be [i, j] or {{\"i\": i, \"j\": j, \"order\"?: ...}}"
1834 )));
1835 }
1836 Ok(())
1837}
1838
1839fn validate_lattice_vectors_metadata(value: &Value) -> Result<(), ParseError> {
1840 let Some(rows) = value.as_array() else {
1841 return Err(metadata_json_error(
1842 "lattice_vectors must be a 3x3 numeric array",
1843 ));
1844 };
1845 if rows.len() != 3 {
1846 return Err(metadata_json_error(
1847 "lattice_vectors must be a 3x3 numeric array",
1848 ));
1849 }
1850 for row in rows {
1851 let Some(entries) = row.as_array() else {
1852 return Err(metadata_json_error(
1853 "lattice_vectors must be a 3x3 numeric array",
1854 ));
1855 };
1856 if entries.len() != 3 || entries.iter().any(|entry| entry.as_f64().is_none()) {
1857 return Err(metadata_json_error(
1858 "lattice_vectors must be a 3x3 numeric array",
1859 ));
1860 }
1861 }
1862 Ok(())
1863}
1864
1865pub fn parse_line_of_n<T: std::str::FromStr>(line: &str, n: usize) -> Result<Vec<T>, ParseError>
1893where
1894 ParseError: From<<T as std::str::FromStr>::Err>,
1895{
1896 let values: Vec<T> = line
1897 .split_whitespace()
1898 .map(|s| s.parse::<T>())
1899 .collect::<Result<_, _>>()?;
1900
1901 if values.len() == n {
1902 Ok(values)
1903 } else {
1904 Err(ParseError::InvalidVectorLength {
1905 expected: n,
1906 found: values.len(),
1907 })
1908 }
1909}
1910
1911pub fn parse_frame_header<'a>(
1932 lines: &mut impl Iterator<Item = &'a str>,
1933) -> Result<FrameHeader, ParseError> {
1934 let prebox1 = lines
1935 .next()
1936 .ok_or(ParseError::IncompleteHeader)?
1937 .to_string();
1938 if prebox1.starts_with('\u{feff}') {
1940 return Err(ParseError::ValidationError(
1941 "file begins with a byte-order mark".into(),
1942 ));
1943 }
1944 let prebox2_raw = lines.next().ok_or(ParseError::IncompleteHeader)?;
1945
1946 let trimmed = prebox2_raw.trim();
1951 let (spec_version, sections, validate, sections_declared) =
1952 if trimmed == "{\"con_spec_version\":2}" {
1953 (2, Vec::new(), false, false)
1956 } else if let Some(ver) = simple_metadata_version(trimmed) {
1957 (ver, Vec::new(), false, false)
1961 } else if trimmed.starts_with('{') {
1962 let json_val: serde_json::Value = serde_json::from_str(trimmed)
1963 .map_err(|e| ParseError::InvalidMetadataJson(e.to_string()))?;
1964 let json_obj = json_val.as_object().ok_or_else(|| {
1965 ParseError::InvalidMetadataJson("expected a JSON object".to_string())
1966 })?;
1967 let ver_value = json_obj
1968 .get(meta::CON_SPEC_VERSION)
1969 .ok_or(ParseError::MissingSpecVersion)?;
1970 let ver = json_u64(ver_value).ok_or_else(|| {
1971 ParseError::InvalidMetadataJson(
1972 "con_spec_version must be a JSON integer, the major version".into(),
1973 )
1974 })?;
1975 if ver > u64::from(u32::MAX) {
1976 return Err(ParseError::UnsupportedVersion(u32::MAX));
1977 }
1978 let ver = ver as u32;
1979 if ver == 0 || ver == 1 || ver > crate::CON_SPEC_VERSION {
1983 return Err(ParseError::UnsupportedVersion(ver));
1984 }
1985 if ver >= 3 {
1986 match json_obj.get(meta::UNITS) {
1987 Some(u) => crate::units::validate_v3_units_metadata(u)
1988 .map_err(|e| ParseError::InvalidUnits(format!("v3 units: {e}")))?,
1989 None => {
1990 return Err(ParseError::InvalidUnits(
1991 "con_spec_version >= 3 requires metadata \"units\" with length and energy"
1992 .into(),
1993 ));
1994 }
1995 }
1996 }
1997
1998 let mut sections: Vec<String> = Vec::new();
2002 let mut sections_declared = false;
2003 let mut validate = false;
2004 for (k, v) in json_obj {
2005 match k.as_str() {
2006 meta::CON_SPEC_VERSION => {}
2007 meta::CON_SPEC_MINOR => {
2008 if json_u64(v).is_none() {
2009 return Err(metadata_json_error(
2010 "con_spec_minor must be a JSON integer >= 0",
2011 ));
2012 }
2013 }
2014 meta::SECTION_ARITY => {
2015 validate_section_arity_value(v)?;
2016 }
2017 meta::SECTIONS => {
2018 sections_declared = true;
2019 let arr = v.as_array().ok_or_else(|| {
2020 metadata_json_error("sections must be an array of strings")
2021 })?;
2022 sections.reserve(arr.len());
2023 for entry in arr {
2024 let s = entry.as_str().ok_or_else(|| {
2025 metadata_json_error("sections must be an array of strings")
2026 })?;
2027 sections.push(s.to_string());
2028 }
2029 }
2030 meta::VALIDATE => {
2031 validate = match v {
2032 Value::Bool(b) => *b,
2033 _ => return Err(metadata_json_error("validate must be a boolean")),
2034 };
2035 }
2036 _ => {}
2037 }
2038 }
2039
2040 if validate {
2043 validate_metadata_schema(json_obj)?;
2044 }
2045
2046 (ver, sections, validate, sections_declared)
2047 } else {
2048 (1_u32, Vec::new(), false, false)
2050 };
2051 let prebox2 = prebox2_raw.to_string();
2052
2053 let boxl_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
2054 let angles_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
2055 let postbox1 = lines
2056 .next()
2057 .ok_or(ParseError::IncompleteHeader)?
2058 .to_string();
2059 let postbox2 = lines
2060 .next()
2061 .ok_or(ParseError::IncompleteHeader)?
2062 .to_string();
2063 let natm_types_line = lines.next().ok_or(ParseError::IncompleteHeader)?;
2064 let natm_types = parse_count_line(natm_types_line, 1)?[0];
2065 let (natms_per_type, masses_per_type) = if natm_types == 0 {
2068 read_zero_atom_tail(|| lines.next())?;
2069 (Vec::new(), Vec::new())
2070 } else {
2071 let natms_per_type = parse_count_line(
2072 lines.next().ok_or(ParseError::IncompleteHeader)?,
2073 natm_types,
2074 )?;
2075 if natms_per_type.contains(&0) {
2076 return Err(ParseError::InvalidCount(
2077 "each per-type count must be >= 1".into(),
2078 ));
2079 }
2080 let masses_per_type = parse_line_of_n_f64(
2081 lines.next().ok_or(ParseError::IncompleteHeader)?,
2082 natm_types,
2083 )?;
2084 (natms_per_type, masses_per_type)
2085 };
2086 if validate {
2087 validate_header_geometry(&boxl_vec, &angles_vec)?;
2088 validate_masses(&masses_per_type)?;
2089 }
2090 let shared_line = intern_repeated_line(&prebox2);
2091 let metadata = crate::types::FrameMeta::from_shared(std::sync::Arc::clone(&shared_line));
2092 Ok(FrameHeader {
2093 prebox_header: PreboxHeader {
2094 user: prebox1,
2095 metadata_line: shared_line,
2096 },
2097 boxl: boxl_vec.try_into().unwrap(),
2098 angles: angles_vec.try_into().unwrap(),
2099 postbox_header: [postbox1, postbox2],
2100 natm_types,
2101 natms_per_type,
2102 masses_per_type,
2103 spec_version,
2104 metadata,
2105 sections,
2106 strict_validation: validate,
2107 sections_declared,
2108 })
2109}
2110
2111thread_local! {
2121 static SEQUENTIAL_PLAIN: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
2122}
2123
2124pub(crate) fn reset_sequential_plain() {
2125 SEQUENTIAL_PLAIN.with(|flag| flag.set(true));
2126}
2127
2128pub(crate) fn note_not_sequential_plain() {
2129 SEQUENTIAL_PLAIN.with(|flag| flag.set(false));
2130}
2131
2132pub(crate) fn take_sequential_plain() -> bool {
2133 SEQUENTIAL_PLAIN.with(|flag| flag.get())
2134}
2135
2136fn intern_repeated_line(line: &str) -> std::sync::Arc<str> {
2137 thread_local! {
2138 static RECENT: std::cell::RefCell<Vec<std::sync::Arc<str>>> =
2139 std::cell::RefCell::new(Vec::new());
2140 }
2141 RECENT.with(|slot| {
2142 let mut slot = slot.borrow_mut();
2143 if let Some(found) = slot.iter().find(|item| item.as_ref() == line) {
2144 return std::sync::Arc::clone(found);
2145 }
2146 let fresh: std::sync::Arc<str> = std::sync::Arc::from(line);
2147 if slot.len() == 8 {
2148 slot.remove(0);
2149 }
2150 slot.push(std::sync::Arc::clone(&fresh));
2151 fresh
2152 })
2153}
2154
2155fn simple_metadata_version(line: &str) -> Option<u32> {
2156 let bytes = line.as_bytes();
2157 let mut i = 0usize;
2158 let skip = |i: &mut usize| {
2159 while *i < bytes.len() && bytes[*i].is_ascii_whitespace() {
2160 *i += 1;
2161 }
2162 };
2163 let eat = |i: &mut usize, byte: u8| -> bool {
2164 skip(i);
2165 if *i < bytes.len() && bytes[*i] == byte {
2166 *i += 1;
2167 true
2168 } else {
2169 false
2170 }
2171 };
2172 let quoted = |i: &mut usize| -> Option<&str> {
2173 skip(i);
2174 if *i >= bytes.len() || bytes[*i] != b'"' {
2175 return None;
2176 }
2177 *i += 1;
2178 let start = *i;
2179 while *i < bytes.len() && bytes[*i] != b'"' {
2180 if bytes[*i] == b'\\' {
2181 return None;
2182 }
2183 *i += 1;
2184 }
2185 if *i >= bytes.len() {
2186 return None;
2187 }
2188 let text = std::str::from_utf8(&bytes[start..*i]).ok()?;
2189 *i += 1;
2190 Some(text)
2191 };
2192 let integer = |i: &mut usize| -> Option<u64> {
2193 skip(i);
2194 let start = *i;
2195 if *i < bytes.len() && bytes[*i] == b'-' {
2196 return None;
2197 }
2198 while *i < bytes.len() && bytes[*i].is_ascii_digit() {
2199 *i += 1;
2200 }
2201 if start == *i || (bytes[start] == b'0' && *i > start + 1) {
2202 return None;
2203 }
2204 std::str::from_utf8(&bytes[start..*i]).ok()?.parse().ok()
2205 };
2206 if !eat(&mut i, b'{') {
2207 return None;
2208 }
2209 let mut version = None;
2210 let mut saw_minor = false;
2211 let mut length = None;
2212 let mut energy = None;
2213 let mut extra_unit = false;
2214 loop {
2215 skip(&mut i);
2216 if i < bytes.len() && bytes[i] == b'}' {
2217 i += 1;
2218 break;
2219 }
2220 if version.is_some() || saw_minor || length.is_some() || energy.is_some() || extra_unit {
2221 if !eat(&mut i, b',') {
2222 return None;
2223 }
2224 }
2225 let key = quoted(&mut i)?;
2226 if !eat(&mut i, b':') {
2227 return None;
2228 }
2229 match key {
2230 "con_spec_version" if version.is_none() => version = Some(integer(&mut i)?),
2231 "con_spec_minor" if !saw_minor => {
2232 integer(&mut i)?;
2233 saw_minor = true;
2234 }
2235 "units" if length.is_none() && energy.is_none() && !extra_unit => {
2236 if !eat(&mut i, b'{') {
2237 return None;
2238 }
2239 loop {
2240 skip(&mut i);
2241 if i < bytes.len() && bytes[i] == b'}' {
2242 i += 1;
2243 break;
2244 }
2245 if length.is_some() || energy.is_some() || extra_unit {
2246 if !eat(&mut i, b',') {
2247 return None;
2248 }
2249 }
2250 let ukey = quoted(&mut i)?;
2251 if !eat(&mut i, b':') {
2252 return None;
2253 }
2254 let value = quoted(&mut i)?;
2255 match ukey {
2256 "length" if length.is_none() => length = Some(value),
2257 "energy" if energy.is_none() => energy = Some(value),
2258 "mass" | "time" | "velocity" | "force" => {
2259 if crate::units::validate_unit_for_quantity(ukey, value).is_err() {
2260 return None;
2261 }
2262 extra_unit = true;
2263 }
2264 _ => return None,
2265 }
2266 }
2267 }
2268 _ => return None,
2269 }
2270 }
2271 skip(&mut i);
2272 if i != bytes.len() {
2273 return None;
2274 }
2275 let ver = u32::try_from(version?).ok()?;
2276 if ver == 0 || ver == 1 || ver > crate::CON_SPEC_VERSION {
2277 return None;
2278 }
2279 if ver >= 3 {
2280 let length = length?;
2281 let energy = energy?;
2282 if length.trim().is_empty() || energy.trim().is_empty() {
2283 return None;
2284 }
2285 crate::units::validate_unit_for_quantity("length", length).ok()?;
2286 crate::units::validate_unit_for_quantity("energy", energy).ok()?;
2287 }
2288 Some(ver)
2289}
2290
2291pub fn parse_single_frame<'a>(
2344 lines: &mut impl Iterator<Item = &'a str>,
2345) -> Result<ConFrame, ParseError> {
2346 let max_rows = lines.size_hint().1;
2350 parse_single_frame_ls(&mut IterStream(lines), max_rows)
2351}
2352
2353struct IterStream<I>(I);
2356
2357impl<'a, I> Iterator for IterStream<I>
2358where
2359 I: Iterator<Item = &'a str>,
2360{
2361 type Item = &'a str;
2362 fn next(&mut self) -> Option<&'a str> {
2363 self.0.next()
2364 }
2365}
2366
2367impl<'a, I> LineStream<'a> for IterStream<I>
2368where
2369 I: Iterator<Item = &'a str>,
2370{
2371 fn next_line(&mut self) -> Option<&'a str> {
2372 Iterator::next(self)
2373 }
2374 fn peek_line(&mut self) -> Option<&'a str> {
2375 None
2376 }
2377}
2378
2379struct CoordShell {
2380 atoms: Vec<crate::types::AtomDatum>,
2381 coords: Vec<f64>,
2382 ids: Vec<u64>,
2383}
2384
2385thread_local! {
2386 static COORD_SHELLS: std::cell::RefCell<Vec<CoordShell>> =
2387 const { std::cell::RefCell::new(Vec::new()) };
2388}
2389
2390pub(crate) fn prestash_coord_shells(n_frames: usize, n_atoms: usize) {
2396 if n_frames == 0 || n_atoms < 128 {
2401 return;
2402 }
2403 COORD_SHELLS.with(|pool| {
2404 let mut pool = pool.borrow_mut();
2405 pool.reserve(n_frames);
2406 for _ in 0..n_frames {
2407 pool.push(CoordShell {
2408 atoms: Vec::with_capacity(n_atoms),
2409 coords: Vec::with_capacity(n_atoms.saturating_mul(3)),
2410 ids: Vec::with_capacity(n_atoms),
2411 });
2412 }
2413 });
2414}
2415
2416pub(crate) fn clear_coord_shells() {
2417 COORD_SHELLS.with(|pool| pool.borrow_mut().clear());
2418}
2419
2420fn take_coord_shell(n_atoms: usize) -> Option<CoordShell> {
2421 COORD_SHELLS.with(|pool| {
2422 let shell = pool.borrow_mut().pop()?;
2423 if shell.atoms.capacity() >= n_atoms
2424 && shell.coords.capacity() >= n_atoms.saturating_mul(3)
2425 && shell.ids.capacity() >= n_atoms
2426 {
2427 Some(shell)
2428 } else {
2429 None
2430 }
2431 })
2432}
2433
2434const COORD_ROW_MIN_COLUMNS: usize = 4;
2437
2438pub(crate) const MIN_COORD_ROW_BYTES: usize = 2 * COORD_ROW_MIN_COLUMNS - 1;
2443
2444pub(crate) fn parse_single_frame_ls<'a, L>(
2457 lines: &mut L,
2458 max_rows: Option<usize>,
2459) -> Result<ConFrame, ParseError>
2460where
2461 L: LineStream<'a> + Iterator<Item = &'a str>,
2462{
2463 reset_sequential_plain();
2464 let header = parse_frame_header(lines)?;
2465 let Some(total_atoms) = fitting_atom_count(&header.natms_per_type, max_rows) else {
2469 return parse_oversized_frame(lines, header, max_rows);
2470 };
2471 let validate = header.strict_validation;
2472 use crate::storage_dtype::{ElementKind, FloatArray2};
2473 let dt = header.metadata.dtypes();
2474 let f64_positions = dt.positions == ElementKind::Float64;
2475 if !f64_positions {
2476 note_not_sequential_plain();
2477 }
2478 let shell = take_coord_shell(total_atoms);
2479 let (mut pos_flat, mut ids, mut atom_data) = match shell {
2480 Some(shell) => (shell.coords, shell.ids, shell.atoms),
2481 None => (
2482 Vec::with_capacity(total_atoms.saturating_mul(3)),
2483 Vec::with_capacity(total_atoms),
2484 Vec::with_capacity(total_atoms),
2485 ),
2486 };
2487 let mut positions_other = if f64_positions {
2488 None
2489 } else {
2490 Some(FloatArray2::zeros(dt.positions, total_atoms, 3))
2491 };
2492 let mut global_atom_idx: u64 = 0;
2493 for (type_idx, num_atoms) in header.natms_per_type.iter().enumerate() {
2494 let symbol_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
2495 let symbol: Arc<str> = Arc::from(symbol_line.trim());
2496 let coord_label = lines.next().ok_or(ParseError::IncompleteFrame)?;
2497 if validate {
2498 validate_coordinate_component(type_idx, symbol.as_ref(), coord_label)?;
2499 }
2500 global_atom_idx = ingest_coord_rows(
2501 lines,
2502 *num_atoms,
2503 header.spec_version,
2504 global_atom_idx,
2505 &symbol,
2506 CoordSink {
2507 pos_flat: &mut pos_flat,
2508 ids: &mut ids,
2509 positions_other: &mut positions_other,
2510 atom_data: &mut atom_data,
2511 },
2512 )?;
2513 }
2514 let positions = if f64_positions {
2515 FloatArray2::from_f64_row_major(total_atoms, 3, pos_flat)
2516 } else {
2517 positions_other.expect("non-f64 positions allocated")
2518 };
2519 Ok(crate::types::con_frame_coords_only_with_ids(
2520 header, atom_data, positions, ids,
2521 ))
2522}
2523
2524#[inline]
2530fn fitting_atom_count(counts: &[usize], max_rows: Option<usize>) -> Option<usize> {
2531 let mut total = 0usize;
2532 for &count in counts {
2533 total = total.checked_add(count)?;
2534 }
2535 if total > (isize::MAX as usize) / 4 {
2536 return None;
2537 }
2538 match max_rows {
2539 Some(rows) if total > rows => None,
2540 None if total > (1 << 20) => None,
2541 _ => Some(total),
2542 }
2543}
2544
2545#[cold]
2550#[inline(never)]
2551fn parse_oversized_frame<'a, L>(
2552 lines: &mut L,
2553 header: FrameHeader,
2554 max_rows: Option<usize>,
2555) -> Result<ConFrame, ParseError>
2556where
2557 L: LineStream<'a> + Iterator<Item = &'a str>,
2558{
2559 let validate = header.strict_validation;
2560 let total_atoms = header
2561 .natms_per_type
2562 .iter()
2563 .fold(0usize, |sum, &n| sum.saturating_add(n));
2564 let reserve_atoms = max_rows.map_or(0, |rows| total_atoms.min(rows));
2565 use crate::storage_dtype::FloatArray2;
2566 let dt = header.metadata.dtypes();
2567 let mut pos_flat = Vec::new();
2568 let _ = pos_flat.try_reserve_exact(reserve_atoms.saturating_mul(3));
2569 let mut ids = Vec::new();
2570 let _ = ids.try_reserve_exact(reserve_atoms);
2571 let mut atom_data: Vec<AtomDatum> = Vec::new();
2572 let _ = atom_data.try_reserve_exact(reserve_atoms);
2573
2574 let mut global_atom_idx: u64 = 0;
2575 for (type_idx, num_atoms) in header.natms_per_type.iter().enumerate() {
2576 let symbol_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
2577 let symbol: Arc<str> = Arc::from(symbol_line.trim());
2578 let coord_label = lines.next().ok_or(ParseError::IncompleteFrame)?;
2579 if validate {
2580 validate_coordinate_component(type_idx, symbol.as_ref(), coord_label)?;
2581 }
2582 global_atom_idx = ingest_coord_rows_cloned(
2583 lines,
2584 *num_atoms,
2585 header.spec_version,
2586 global_atom_idx,
2587 &symbol,
2588 &mut pos_flat,
2589 &mut ids,
2590 &mut atom_data,
2591 )?;
2592 }
2593 let mut positions = FloatArray2::from_f64_row_major(total_atoms, 3, pos_flat);
2594 positions.project_to(dt.positions);
2595 Ok(crate::types::con_frame_coords_only_with_ids(
2596 header, atom_data, positions, ids,
2597 ))
2598}
2599
2600struct CoordSink<'a> {
2601 pos_flat: &'a mut Vec<f64>,
2602 ids: &'a mut Vec<u64>,
2603 positions_other: &'a mut Option<crate::storage_dtype::FloatArray2>,
2604 atom_data: &'a mut Vec<AtomDatum>,
2605}
2606
2607fn ingest_coord_rows<'a, L>(
2609 lines: &mut L,
2610 n: usize,
2611 spec_version: u32,
2612 global_atom_idx: u64,
2613 symbol: &Arc<str>,
2614 sink: CoordSink<'_>,
2615) -> Result<u64, ParseError>
2616where
2617 L: LineStream<'a>,
2618{
2619 let CoordSink {
2620 pos_flat,
2621 ids,
2622 positions_other,
2623 atom_data,
2624 } = sink;
2625 if positions_other.is_none() {
2626 return ingest_coord_rows_f64(
2627 lines,
2628 n,
2629 spec_version,
2630 global_atom_idx,
2631 symbol,
2632 pos_flat,
2633 ids,
2634 atom_data,
2635 );
2636 }
2637 let mut global_atom_idx = global_atom_idx;
2638 for _ in 0..n {
2639 let (xyz, fixed, atom_id) = lines.next_coord_row(spec_version, global_atom_idx)?;
2640 if let Some(pos) = positions_other.as_mut() {
2641 pos.set_f64_row(ids.len(), xyz);
2642 }
2643 let index = pos_flat.len() / 3;
2644 pos_flat.extend_from_slice(&xyz);
2645 let slot = unsafe { pos_flat.as_mut_ptr().add(index * 3) };
2646 ids.push(atom_id);
2647 atom_data.push(AtomDatum::at_slot(Arc::clone(symbol), slot, fixed, atom_id));
2648 global_atom_idx += 1;
2649 }
2650 Ok(global_atom_idx)
2651}
2652
2653#[inline(never)]
2658#[allow(clippy::too_many_arguments)]
2659fn ingest_coord_rows_f64<'a, L>(
2660 lines: &mut L,
2661 n: usize,
2662 spec_version: u32,
2663 mut global_atom_idx: u64,
2664 symbol: &Arc<str>,
2665 pos_flat: &mut Vec<f64>,
2666 ids: &mut Vec<u64>,
2667 atom_data: &mut Vec<AtomDatum>,
2668) -> Result<u64, ParseError>
2669where
2670 L: LineStream<'a>,
2671{
2672 let workers = wide_workers();
2673 if workers >= 2 && n >= 4096 {
2674 if let Some(mut block) =
2675 lines.try_take_coord_block(n, spec_version, global_atom_idx, symbol, workers)
2676 {
2677 let base = pos_flat.len();
2678 if pos_flat.is_empty() && block.xyz.capacity() >= pos_flat.capacity() {
2682 *pos_flat = block.xyz;
2683 } else {
2684 pos_flat.extend_from_slice(&block.xyz);
2685 }
2686 let ptr = pos_flat.as_mut_ptr();
2687 for (i, atom) in block.atoms.iter_mut().enumerate() {
2688 atom.retarget(unsafe { ptr.add(base + i * 3) });
2689 }
2690 if ids.is_empty() {
2691 *ids = block.ids;
2692 } else {
2693 ids.extend_from_slice(&block.ids);
2694 }
2695 if atom_data.is_empty() {
2696 *atom_data = block.atoms;
2697 } else {
2698 atom_data.extend(block.atoms);
2699 }
2700 return Ok(global_atom_idx + n as u64);
2701 }
2702 }
2703 let mut shares = symbol_shares(symbol, n);
2704 for _ in 0..n {
2705 let (xyz, fixed, atom_id) = lines.next_coord_row(spec_version, global_atom_idx)?;
2706 if fixed != [false, false, false] || atom_id != global_atom_idx {
2707 note_not_sequential_plain();
2708 }
2709 let sym = shares.take();
2710 let index = pos_flat.len() / 3;
2711 pos_flat.extend_from_slice(&xyz);
2712 let slot = unsafe { pos_flat.as_mut_ptr().add(index * 3) };
2713 ids.push(atom_id);
2714 atom_data.push(AtomDatum::at_slot(sym, slot, fixed, atom_id));
2715 global_atom_idx += 1;
2716 }
2717 Ok(global_atom_idx)
2718}
2719
2720fn ingest_coord_rows_cloned<'a, L>(
2722 lines: &mut L,
2723 n: usize,
2724 spec_version: u32,
2725 mut global_atom_idx: u64,
2726 symbol: &Arc<str>,
2727 pos_flat: &mut Vec<f64>,
2728 ids: &mut Vec<u64>,
2729 atom_data: &mut Vec<AtomDatum>,
2730) -> Result<u64, ParseError>
2731where
2732 L: LineStream<'a>,
2733{
2734 for _ in 0..n {
2735 let (xyz, fixed, atom_id) = lines.next_coord_row(spec_version, global_atom_idx)?;
2736 pos_flat.extend_from_slice(&xyz);
2737 ids.push(atom_id);
2738 atom_data.push(AtomDatum::coords(
2739 Arc::clone(symbol),
2740 xyz[0],
2741 xyz[1],
2742 xyz[2],
2743 fixed,
2744 atom_id,
2745 ));
2746 global_atom_idx += 1;
2747 }
2748 Ok(global_atom_idx)
2749}
2750
2751struct SymbolShares {
2757 data: *const u8,
2758 len: usize,
2759 left: usize,
2760}
2761
2762impl SymbolShares {
2763 fn raw(&self) -> *const str {
2764 std::ptr::slice_from_raw_parts(self.data, self.len) as *const str
2765 }
2766
2767 fn take(&mut self) -> Arc<str> {
2768 debug_assert!(self.left > 0);
2769 self.left -= 1;
2770 unsafe { Arc::from_raw(self.raw()) }
2771 }
2772}
2773
2774impl Drop for SymbolShares {
2775 fn drop(&mut self) {
2776 for _ in 0..self.left {
2777 drop(unsafe { Arc::from_raw(self.raw()) });
2778 }
2779 self.left = 0;
2780 }
2781}
2782
2783fn symbol_shares(symbol: &Arc<str>, n: usize) -> SymbolShares {
2784 if n == 0 {
2785 return SymbolShares {
2786 data: std::ptr::null(),
2787 len: 0,
2788 left: 0,
2789 };
2790 }
2791 let data = Arc::as_ptr(symbol);
2792 let offset = arc_str_header(symbol.as_ref());
2793 let bytes = data as *const u8;
2797 let strong = unsafe { bytes.sub(offset) as *const AtomicUsize };
2798 unsafe { (*strong).fetch_add(n, Ordering::Relaxed) };
2799 SymbolShares {
2800 data: bytes,
2801 len: symbol.len(),
2802 left: n,
2803 }
2804}
2805
2806fn arc_str_header(text: &str) -> usize {
2807 let header = 2 * std::mem::size_of::<usize>();
2808 let align = std::mem::align_of_val(text);
2809 let rem = header % align;
2810 if rem == 0 {
2811 header
2812 } else {
2813 header + (align - rem)
2814 }
2815}
2816
2817#[cfg(test)]
2818mod symbol_share_tests {
2819 use super::symbol_shares;
2820 use std::sync::Arc;
2821
2822 #[test]
2823 fn one_bump_matches_n_clones() {
2824 for text in ["", "Cu", "Gold", "a-much-longer-component-symbol"] {
2825 let symbol: Arc<str> = Arc::from(text);
2826 let mut shares = symbol_shares(&symbol, 3);
2827 assert_eq!(Arc::strong_count(&symbol), 4);
2828 let owned = [shares.take(), shares.take(), shares.take()];
2829 assert!(owned.iter().all(|arc| arc.as_ref() == text));
2830 drop(owned);
2831 assert_eq!(Arc::strong_count(&symbol), 1);
2832 let mut partial = symbol_shares(&symbol, 2);
2833 let one = partial.take();
2834 drop(partial);
2835 drop(one);
2836 assert_eq!(Arc::strong_count(&symbol), 1);
2837 }
2838 }
2839}
2840
2841fn validate_header_geometry(boxl: &[f64], angles: &[f64]) -> Result<(), ParseError> {
2842 if boxl
2843 .iter()
2844 .any(|length| !length.is_finite() || *length <= 0.0)
2845 || angles
2846 .iter()
2847 .any(|angle| !angle.is_finite() || *angle <= 0.0 || *angle >= 180.0)
2848 {
2849 return Err(ParseError::ValidationError(
2850 "cell geometry must have positive lengths and angles between 0 and 180 degrees"
2851 .to_string(),
2852 ));
2853 }
2854 Ok(())
2855}
2856
2857fn validate_masses(masses_per_type: &[f64]) -> Result<(), ParseError> {
2858 if masses_per_type
2859 .iter()
2860 .any(|mass| !mass.is_finite() || *mass <= 0.0)
2861 {
2862 return Err(ParseError::ValidationError(
2863 "component masses must be positive".to_string(),
2864 ));
2865 }
2866 Ok(())
2867}
2868
2869fn validate_coordinate_component(
2870 type_idx: usize,
2871 symbol: &str,
2872 label: &str,
2873) -> Result<(), ParseError> {
2874 let expected_label = format!("Coordinates of Component {}", type_idx + 1);
2875 if !labels_equal(label, &expected_label) {
2876 return Err(ParseError::ValidationError(format!(
2877 "expected coordinate label {expected_label:?}, found {label:?}"
2878 )));
2879 }
2880 if symbol != "X" && symbol_to_atomic_number(symbol) == 0 {
2881 return Err(ParseError::ValidationError(format!(
2882 "unknown component symbol {symbol}"
2883 )));
2884 }
2885 Ok(())
2886}
2887
2888fn validate_section_component(
2889 section: &str,
2890 type_idx: usize,
2891 atom_idx: usize,
2892 symbol: &str,
2893 label: &str,
2894 header: &FrameHeader,
2895 atom_data: &[AtomDatum],
2896) -> Result<(), ParseError> {
2897 let expected_label = format!("{section} of Component {}", type_idx + 1);
2898 if !labels_equal(label, &expected_label) {
2899 return Err(ParseError::ValidationError(format!(
2900 "expected section label {expected_label:?}, found {label:?}"
2901 )));
2902 }
2903
2904 if header.natms_per_type[type_idx] == 0 {
2905 return Ok(());
2906 }
2907
2908 let expected_symbol = atom_data
2909 .get(atom_idx)
2910 .map(|atom| atom.symbol.as_ref())
2911 .ok_or_else(|| {
2912 ParseError::ValidationError(format!(
2913 "{section} component {} has no coordinate atom to validate against",
2914 type_idx + 1
2915 ))
2916 })?;
2917 if symbol != expected_symbol {
2918 return Err(ParseError::ValidationError(format!(
2919 "{section} component {} symbol mismatch: expected {expected_symbol}, found {symbol}",
2920 type_idx + 1
2921 )));
2922 }
2923
2924 Ok(())
2925}
2926
2927fn validate_section_atom_identity(
2928 section: &str,
2929 atom_idx: usize,
2930 fixed: [bool; 3],
2931 atom_id: u64,
2932 atom_data: &[AtomDatum],
2933) -> Result<(), ParseError> {
2934 let atom = atom_data.get(atom_idx).ok_or_else(|| {
2935 ParseError::ValidationError(format!(
2936 "{section} row {atom_idx} has no coordinate atom to validate against"
2937 ))
2938 })?;
2939
2940 if atom.fixed() != fixed {
2941 return Err(ParseError::ValidationError(format!(
2942 "{section} row {atom_idx} fixed mask mismatch for atom_id {}",
2943 atom.atom_id
2944 )));
2945 }
2946 if atom.atom_id != atom_id {
2947 return Err(ParseError::ValidationError(format!(
2948 "{section} row {atom_idx} atom_id mismatch: expected {}, found {atom_id}",
2949 atom.atom_id
2950 )));
2951 }
2952
2953 Ok(())
2954}
2955
2956fn frame_minor(header: &FrameHeader) -> u64 {
2961 header.metadata.spec_minor()
2962}
2963
2964fn section_row_arity(header: &FrameHeader, name: &str) -> Option<usize> {
2965 let obj = header.metadata.get(meta::SECTION_ARITY)?.as_object()?;
2966 let n = obj.get(name)?.as_u64()?;
2967 if n >= 1 { Some(n as usize) } else { None }
2968}
2969
2970fn skip_unknown_section<'a>(
2977 lines: &mut impl LineStream<'a>,
2978 header: &FrameHeader,
2979 name: &str,
2980 arity: usize,
2981) -> Result<(), ParseError> {
2982 if !open_section(lines)? {
2983 return Err(ParseError::IncompleteSection(name.to_string()));
2984 }
2985 let with_flag = arity.saturating_add(1);
2986 let with_id = arity.saturating_add(2);
2987 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
2988 if lines.next_line().is_none() {
2989 return Err(ParseError::IncompleteSection(name.to_string()));
2990 }
2991 let label = lines
2992 .next_line()
2993 .ok_or_else(|| ParseError::IncompleteSection(name.to_string()))?;
2994 let expected = format!("{name} of Component {}", type_idx + 1);
2995 if !labels_equal(label, &expected) {
2996 return Err(ParseError::IncompleteSection(name.to_string()));
2997 }
2998 for _ in 0..num_atoms {
2999 let row = lines
3000 .next_line()
3001 .ok_or_else(|| ParseError::IncompleteSection(name.to_string()))?;
3002 let found = token_count(row);
3003 if found != with_flag && found != with_id {
3004 let expected = if found < with_flag {
3005 with_flag
3006 } else {
3007 with_id
3008 };
3009 return Err(ParseError::InvalidVectorLength { expected, found });
3010 }
3011 }
3012 }
3013 Ok(())
3014}
3015
3016fn open_section<'a>(lines: &mut impl LineStream<'a>) -> Result<bool, ParseError> {
3017 match lines.peek_line() {
3018 Some(line) if line.trim().is_empty() => {
3019 if lines.remainder_is_whitespace() {
3020 return Ok(false);
3021 }
3022 lines.next_line();
3023 Ok(true)
3024 }
3025 _ => Ok(false),
3026 }
3027}
3028
3029pub fn parse_velocity_section<'a>(
3043 lines: &mut impl LineStream<'a>,
3044 header: &FrameHeader,
3045 atom_data: &mut [AtomDatum],
3046 column: &mut Vec<f64>,
3047) -> Result<bool, ParseError> {
3048 let validate = header.strict_validation;
3049 match lines.peek_line() {
3052 Some(line) if line.trim().is_empty() => {
3053 if lines.remainder_is_whitespace() {
3054 return Ok(false);
3055 }
3056 if !lines.blank_line_opens_section() {
3060 return Ok(false);
3061 }
3062 lines.next_line();
3063 }
3064 _ => return Ok(false),
3065 }
3066
3067 let n = atom_data.len();
3068 column.clear();
3069 column.resize(n.saturating_mul(3), 0.0);
3070 let mut atom_idx: usize = 0;
3071 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
3072 let symbol = lines
3074 .next_line()
3075 .ok_or(ParseError::IncompleteSection(SECTION_VELOCITIES.into()))?
3076 .trim();
3077
3078 let comp_line = lines
3080 .next_line()
3081 .ok_or(ParseError::IncompleteSection(SECTION_VELOCITIES.into()))?;
3082 if !label_has_prefix(comp_line, "Velocities of Component") {
3083 return Err(ParseError::IncompleteSection(SECTION_VELOCITIES.into()));
3084 }
3085 if validate {
3086 validate_section_component(
3087 "Velocities",
3088 type_idx,
3089 atom_idx,
3090 symbol,
3091 comp_line,
3092 header,
3093 atom_data,
3094 )?;
3095 }
3096
3097 for _ in 0..num_atoms {
3098 let (vals, fixed, atom_id) =
3099 take_vector_row(lines, header.spec_version, SECTION_VELOCITIES)?;
3100 if validate {
3101 validate_section_atom_identity("velocities", atom_idx, fixed, atom_id, atom_data)?;
3102 }
3103 if atom_idx < n {
3104 let base = atom_idx * 3;
3105 column[base] = vals[0];
3106 column[base + 1] = vals[1];
3107 column[base + 2] = vals[2];
3108 }
3109 atom_idx += 1;
3110 }
3111 }
3112
3113 Ok(true)
3114}
3115
3116pub fn parse_force_section<'a>(
3124 lines: &mut impl LineStream<'a>,
3125 header: &FrameHeader,
3126 atom_data: &mut [AtomDatum],
3127 column: &mut Vec<f64>,
3128) -> Result<bool, ParseError> {
3129 let validate = header.strict_validation;
3130 if !open_section(lines)? {
3131 return Ok(false);
3132 }
3133
3134 let n = atom_data.len();
3135 column.clear();
3136 column.resize(n.saturating_mul(3), 0.0);
3137 let mut atom_idx: usize = 0;
3138 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
3139 let symbol = lines
3140 .next_line()
3141 .ok_or(ParseError::IncompleteSection(SECTION_FORCES.into()))?
3142 .trim();
3143
3144 let comp_line = lines
3145 .next_line()
3146 .ok_or(ParseError::IncompleteSection(SECTION_FORCES.into()))?;
3147 if !label_has_prefix(comp_line, "Forces of Component") {
3148 return Err(ParseError::IncompleteSection(SECTION_FORCES.into()));
3149 }
3150 if validate {
3151 validate_section_component(
3152 "Forces", type_idx, atom_idx, symbol, comp_line, header, atom_data,
3153 )?;
3154 }
3155
3156 for _ in 0..num_atoms {
3157 let (vals, fixed, atom_id) =
3158 take_vector_row(lines, header.spec_version, SECTION_FORCES)?;
3159 if validate {
3160 validate_section_atom_identity("forces", atom_idx, fixed, atom_id, atom_data)?;
3161 }
3162 if atom_idx < n {
3163 let base = atom_idx * 3;
3164 column[base] = vals[0];
3165 column[base + 1] = vals[1];
3166 column[base + 2] = vals[2];
3167 }
3168 atom_idx += 1;
3169 }
3170 }
3171
3172 Ok(true)
3173}
3174
3175pub fn parse_energy_section<'a>(
3188 lines: &mut impl LineStream<'a>,
3189 header: &FrameHeader,
3190 atom_data: &mut [AtomDatum],
3191) -> Result<bool, ParseError> {
3192 let validate = header.strict_validation;
3193 if !open_section(lines)? {
3194 return Ok(false);
3195 }
3196
3197 let mut atom_idx: usize = 0;
3198 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
3199 let symbol = lines
3200 .next_line()
3201 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?
3202 .trim();
3203
3204 let comp_line = lines
3205 .next_line()
3206 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?;
3207 if !label_has_prefix(comp_line, "Energies of Component") {
3208 return Err(ParseError::IncompleteSection(SECTION_ENERGIES.into()));
3209 }
3210 if validate {
3211 validate_section_component(
3212 "Energies", type_idx, atom_idx, symbol, comp_line, header, atom_data,
3213 )?;
3214 }
3215
3216 for _ in 0..num_atoms {
3217 let energy_line = lines
3218 .next_line()
3219 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?;
3220 let (value, fixed, atom_id) =
3221 parse_scalar_section_row(energy_line, header.spec_version)?;
3222 if validate {
3223 validate_section_atom_identity("energies", atom_idx, fixed, atom_id, atom_data)?;
3224 }
3225 if atom_idx < atom_data.len() {
3226 atom_data[atom_idx].set_energy(Some(value));
3227 }
3228 atom_idx += 1;
3229 }
3230 }
3231
3232 Ok(true)
3233}
3234
3235pub fn parse_declared_sections<'a>(
3241 lines: &mut impl LineStream<'a>,
3242 frame: &mut ConFrame,
3243) -> Result<usize, ParseError> {
3244 let mut applied = 0usize;
3245 if frame.header.natm_types == 0 {
3248 if !frame.header.sections.is_empty() {
3249 return Err(ParseError::InvalidCount(
3250 "a zero-atom frame cannot declare sections".into(),
3251 ));
3252 }
3253 return Ok(0);
3254 }
3255 if !frame.header.sections_declared && frame.header.sections.is_empty() {
3256 let mut column = Vec::new();
3260 let found = lines.blank_opens_velocity_block()
3261 && parse_velocity_section(lines, &frame.header, &mut frame.atom_data, &mut column)?;
3262 if found {
3263 frame.adopt_velocity_column(column);
3264 frame.header.sections.push(SECTION_VELOCITIES.into());
3265 applied = 1;
3266 }
3267 } else {
3268 let sections = std::mem::take(&mut frame.header.sections);
3269 for section in §ions {
3270 match section.as_str() {
3271 SECTION_VELOCITIES => {
3272 let mut column = Vec::new();
3273 let found = parse_velocity_section(
3274 lines,
3275 &frame.header,
3276 &mut frame.atom_data,
3277 &mut column,
3278 )?;
3279 if !found {
3280 return Err(ParseError::IncompleteSection(SECTION_VELOCITIES.into()));
3281 }
3282 frame.adopt_velocity_column(column);
3283 applied += 1;
3284 }
3285 SECTION_FORCES => {
3286 let mut column = Vec::new();
3287 let found = parse_force_section(
3288 lines,
3289 &frame.header,
3290 &mut frame.atom_data,
3291 &mut column,
3292 )?;
3293 if !found {
3294 return Err(ParseError::IncompleteSection(SECTION_FORCES.into()));
3295 }
3296 frame.adopt_force_column(column);
3297 applied += 1;
3298 }
3299 SECTION_ENERGIES => {
3300 let found = parse_energy_section(lines, &frame.header, &mut frame.atom_data)?;
3301 if !found {
3302 return Err(ParseError::IncompleteSection(SECTION_ENERGIES.into()));
3303 }
3304 applied += 1;
3305 }
3306 SECTION_CHARGES => {
3307 let found = parse_charge_section(lines, &frame.header, &mut frame.atom_data)?;
3308 if !found {
3309 return Err(ParseError::IncompleteSection(SECTION_CHARGES.into()));
3310 }
3311 applied += 1;
3312 }
3313 SECTION_SPINS => {
3314 let found = parse_spin_section(lines, &frame.header, &mut frame.atom_data)?;
3315 if !found {
3316 return Err(ParseError::IncompleteSection(SECTION_SPINS.into()));
3317 }
3318 applied += 1;
3319 }
3320 SECTION_MAGMOMS => {
3321 let found = parse_magmom_section(lines, &frame.header, &mut frame.atom_data)?;
3322 if !found {
3323 return Err(ParseError::IncompleteSection(SECTION_MAGMOMS.into()));
3324 }
3325 applied += 1;
3326 }
3327 SECTION_DISPLACEMENTS => {
3328 let found =
3329 parse_displacement_section(lines, &frame.header, &mut frame.atom_data)?;
3330 if !found {
3331 return Err(ParseError::IncompleteSection(SECTION_DISPLACEMENTS.into()));
3332 }
3333 applied += 1;
3334 }
3335 SECTION_SPREADS => {
3336 let found = parse_spread_section(lines, &frame.header, &mut frame.atom_data)?;
3337 if !found {
3338 return Err(ParseError::IncompleteSection(SECTION_SPREADS.into()));
3339 }
3340 applied += 1;
3341 }
3342 other => {
3343 if frame_minor(&frame.header) > u64::from(crate::CON_SPEC_MINOR)
3344 && let Some(arity) = section_row_arity(&frame.header, other)
3345 {
3346 skip_unknown_section(lines, &frame.header, other, arity)?;
3347 continue;
3348 }
3349 return Err(ParseError::UnknownSection(other.to_string()));
3350 }
3351 }
3352 }
3353 frame.header.sections = sections;
3354 }
3355 reject_velocities_without_time(&frame.header)?;
3356 Ok(applied)
3357}
3358
3359fn reject_velocities_without_time(header: &FrameHeader) -> Result<(), ParseError> {
3363 if header.spec_version < 3 || frame_minor(header) < 1 {
3364 return Ok(());
3365 }
3366 if !header
3367 .sections
3368 .iter()
3369 .any(|name| name == SECTION_VELOCITIES)
3370 {
3371 return Ok(());
3372 }
3373 let time_ok = header
3374 .metadata
3375 .get(meta::UNITS)
3376 .and_then(|units| units.get("time"))
3377 .and_then(|value| value.as_str())
3378 .is_some_and(|unit| !unit.trim().is_empty());
3379 if time_ok {
3380 Ok(())
3381 } else {
3382 Err(ParseError::InvalidUnits(
3383 "minor >= 1 requires units.time when a velocities section is present".into(),
3384 ))
3385 }
3386}
3387
3388fn parse_scalar_atom_section<'a>(
3391 lines: &mut impl LineStream<'a>,
3392 header: &FrameHeader,
3393 atom_data: &mut [AtomDatum],
3394 section_name: &str,
3395 component_label: &str,
3396 mut set_value: impl FnMut(&mut AtomDatum, f64),
3397) -> Result<bool, ParseError> {
3398 let validate = header.strict_validation;
3399 if !open_section(lines)? {
3400 return Ok(false);
3401 }
3402
3403 let mut atom_idx: usize = 0;
3404 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
3405 let symbol = lines
3406 .next_line()
3407 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?
3408 .trim();
3409
3410 let comp_line = lines
3411 .next_line()
3412 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
3413 if !label_has_prefix(comp_line, component_label) {
3414 return Err(ParseError::IncompleteSection(section_name.into()));
3415 }
3416 if validate {
3417 validate_section_component(
3418 component_label.trim_end_matches(" of Component").trim(),
3419 type_idx,
3420 atom_idx,
3421 symbol,
3422 comp_line,
3423 header,
3424 atom_data,
3425 )?;
3426 }
3427
3428 for _ in 0..num_atoms {
3429 let data_line = lines
3430 .next_line()
3431 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
3432 let (value, fixed, atom_id) = parse_scalar_section_row(data_line, header.spec_version)?;
3433 if validate {
3434 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
3435 }
3436 if atom_idx < atom_data.len() {
3437 set_value(&mut atom_data[atom_idx], value);
3438 }
3439 atom_idx += 1;
3440 }
3441 }
3442 Ok(true)
3443}
3444
3445pub fn parse_charge_section<'a>(
3446 lines: &mut impl LineStream<'a>,
3447 header: &FrameHeader,
3448 atom_data: &mut [AtomDatum],
3449) -> Result<bool, ParseError> {
3450 parse_scalar_atom_section(
3451 lines,
3452 header,
3453 atom_data,
3454 SECTION_CHARGES,
3455 "Charges of Component",
3456 |a, v| a.set_charge(Some(v)),
3457 )
3458}
3459
3460pub fn parse_spin_section<'a>(
3461 lines: &mut impl LineStream<'a>,
3462 header: &FrameHeader,
3463 atom_data: &mut [AtomDatum],
3464) -> Result<bool, ParseError> {
3465 parse_scalar_atom_section(
3466 lines,
3467 header,
3468 atom_data,
3469 SECTION_SPINS,
3470 "Spins of Component",
3471 |a, v| a.set_spin(Some(v)),
3472 )
3473}
3474
3475fn parse_vector_named_section<'a>(
3476 lines: &mut impl LineStream<'a>,
3477 header: &FrameHeader,
3478 atom_data: &mut [AtomDatum],
3479 names: (&str, &str, &str),
3480 mut set_value: impl FnMut(&mut AtomDatum, [f64; 3]),
3481 mut check_row: impl FnMut(usize, [f64; 3]) -> Result<(), ParseError>,
3482) -> Result<bool, ParseError> {
3483 let (section_name, component_label, label_kind) = names;
3484 let validate = header.strict_validation;
3485 if !open_section(lines)? {
3486 return Ok(false);
3487 }
3488 let mut atom_idx: usize = 0;
3489 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
3490 let symbol = lines
3491 .next_line()
3492 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?
3493 .trim();
3494 let comp_line = lines
3495 .next_line()
3496 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
3497 if !label_has_prefix(comp_line, component_label) {
3498 return Err(ParseError::IncompleteSection(section_name.into()));
3499 }
3500 if validate {
3501 validate_section_component(
3502 label_kind, type_idx, atom_idx, symbol, comp_line, header, atom_data,
3503 )?;
3504 }
3505 for _ in 0..num_atoms {
3506 let (vals, fixed, atom_id) = take_vector_row(lines, header.spec_version, section_name)?;
3507 check_row(atom_idx, vals)?;
3508 if validate {
3509 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
3510 }
3511 if atom_idx < atom_data.len() {
3512 set_value(&mut atom_data[atom_idx], vals);
3513 }
3514 atom_idx += 1;
3515 }
3516 }
3517 Ok(true)
3518}
3519
3520pub fn parse_magmom_section<'a>(
3522 lines: &mut impl LineStream<'a>,
3523 header: &FrameHeader,
3524 atom_data: &mut [AtomDatum],
3525) -> Result<bool, ParseError> {
3526 parse_vector_named_section(
3527 lines,
3528 header,
3529 atom_data,
3530 (SECTION_MAGMOMS, "Magmoms of Component", "Magmoms"),
3531 |a, v| a.set_magmom(Some(v)),
3532 |_, _| Ok(()),
3533 )
3534}
3535
3536pub fn parse_displacement_section<'a>(
3538 lines: &mut impl LineStream<'a>,
3539 header: &FrameHeader,
3540 atom_data: &mut [AtomDatum],
3541) -> Result<bool, ParseError> {
3542 parse_vector_named_section(
3543 lines,
3544 header,
3545 atom_data,
3546 (
3547 SECTION_DISPLACEMENTS,
3548 "Displacements of Component",
3549 "Displacements",
3550 ),
3551 |a, v| a.set_displacement(Some(v)),
3552 |_, _| Ok(()),
3553 )
3554}
3555
3556pub fn parse_spread_section<'a>(
3559 lines: &mut impl LineStream<'a>,
3560 header: &FrameHeader,
3561 atom_data: &mut [AtomDatum],
3562) -> Result<bool, ParseError> {
3563 parse_vector_named_section(
3564 lines,
3565 header,
3566 atom_data,
3567 (SECTION_SPREADS, "Spreads of Component", "Spreads"),
3568 |a, v| a.set_spread(Some(v)),
3569 |atom_idx, vals| {
3570 if !vals.iter().all(|v| v.is_finite()) {
3571 return Err(ParseError::NonFiniteValue(format!(
3572 "spreads atom {atom_idx}"
3573 )));
3574 }
3575 if vals.iter().any(|v| *v < 0.0) {
3576 return Err(crate::types::invalid_spread(atom_idx));
3577 }
3578 Ok(())
3579 },
3580 )
3581}
3582
3583#[cfg(test)]
3584mod tests {
3585 use super::*;
3586 use crate::iterators::ConFrameIterator;
3587
3588 #[test]
3589 fn test_parse_line_of_n_success() {
3590 let line = "1.0 2.5 -3.0";
3591 let values = parse_line_of_n::<f64>(line, 3).unwrap();
3592 assert_eq!(values, vec![1.0, 2.5, -3.0]);
3593 }
3594
3595 #[test]
3596 fn test_parse_line_of_n_too_short() {
3597 let line = "1.0 2.5";
3598 let result = parse_line_of_n::<f64>(line, 3);
3599 assert!(result.is_err());
3600 assert!(matches!(
3601 result.unwrap_err(),
3602 ParseError::InvalidVectorLength {
3603 expected: 3,
3604 found: 2
3605 }
3606 ));
3607 }
3608
3609 #[test]
3610 fn test_parse_line_of_n_too_long() {
3611 let line = "1.0 2.5 -3.0 4.0";
3612 let result = parse_line_of_n::<f64>(line, 3);
3613 assert!(result.is_err());
3614 assert!(matches!(
3615 result.unwrap_err(),
3616 ParseError::InvalidVectorLength {
3617 expected: 3,
3618 found: 4
3619 }
3620 ));
3621 }
3622
3623 #[test]
3624 fn test_parse_line_of_n_invalid_float() {
3625 let line = "1.0 abc -3.0";
3626 let result = parse_line_of_n::<f64>(line, 3);
3627 assert!(result.is_err());
3628 assert!(matches!(
3629 result.unwrap_err(),
3630 ParseError::InvalidNumberFormat(_)
3631 ));
3632 }
3633
3634 #[test]
3635 fn test_parse_frame_header_success() {
3636 let lines = [
3637 "PREBOX1",
3638 "{\"con_spec_version\":2}",
3639 "10.0 20.0 30.0",
3640 "90.0 90.0 90.0",
3641 "POSTBOX1",
3642 "POSTBOX2",
3643 "2",
3644 "1 1",
3645 "12.011 1.008",
3646 ];
3647 let mut line_it = lines.iter().copied();
3648 match parse_frame_header(&mut line_it) {
3649 Ok(header) => {
3650 assert_eq!(header.prebox_header.user, "PREBOX1");
3651 assert_eq!(header.spec_version, 2);
3652 assert_eq!(header.boxl, [10.0, 20.0, 30.0]);
3653 assert_eq!(header.angles, [90.0, 90.0, 90.0]);
3654 assert_eq!(header.postbox_header, ["POSTBOX1", "POSTBOX2"]);
3655 assert_eq!(header.natm_types, 2);
3656 assert_eq!(header.natms_per_type, vec![1, 1]);
3657 assert_eq!(header.masses_per_type, vec![12.011, 1.008]);
3658 }
3659 Err(e) => {
3660 panic!("Parsing failed when it should have succeeded. Error: {e:?}");
3661 }
3662 }
3663 }
3664
3665 #[test]
3666 fn test_parse_frame_header_missing_line() {
3667 let lines = [
3668 "PREBOX1",
3669 "{\"con_spec_version\":2}",
3670 "10.0 20.0 30.0",
3671 "90.0 90.0 90.0",
3672 "POSTBOX1",
3673 "POSTBOX2",
3674 "2",
3675 "1 1",
3676 ];
3678 let mut line_it = lines.iter().copied();
3679 let result = parse_frame_header(&mut line_it);
3680 assert!(result.is_err());
3681 assert!(matches!(result.unwrap_err(), ParseError::IncompleteHeader));
3682 }
3683
3684 #[test]
3685 fn test_parse_frame_header_missing_spec_version() {
3686 let lines = [
3687 "PREBOX1",
3688 "{}",
3689 "10.0 20.0 30.0",
3690 "90.0 90.0 90.0",
3691 "POSTBOX1",
3692 "POSTBOX2",
3693 "2",
3694 "1 1",
3695 "12.011 1.008",
3696 ];
3697 let mut line_it = lines.iter().copied();
3698 let result = parse_frame_header(&mut line_it);
3699 assert!(result.is_err());
3700 assert!(matches!(
3701 result.unwrap_err(),
3702 ParseError::MissingSpecVersion
3703 ));
3704 }
3705
3706 #[test]
3707 fn test_parse_frame_header_legacy_no_json() {
3708 let lines = [
3710 "PREBOX1",
3711 "0.0000 TIME",
3712 "10.0 20.0 30.0",
3713 "90.0 90.0 90.0",
3714 "POSTBOX1",
3715 "POSTBOX2",
3716 "2",
3717 "1 1",
3718 "12.011 1.008",
3719 ];
3720 let mut line_it = lines.iter().copied();
3721 let header = parse_frame_header(&mut line_it).unwrap();
3722 assert_eq!(header.spec_version, 1);
3723 assert!(header.metadata.is_empty());
3724 }
3725
3726 #[test]
3727 fn test_parse_frame_header_malformed_json() {
3728 let lines = [
3730 "PREBOX1",
3731 "{broken json",
3732 "10.0 20.0 30.0",
3733 "90.0 90.0 90.0",
3734 "POSTBOX1",
3735 "POSTBOX2",
3736 "2",
3737 "1 1",
3738 "12.011 1.008",
3739 ];
3740 let mut line_it = lines.iter().copied();
3741 let result = parse_frame_header(&mut line_it);
3742 assert!(result.is_err());
3743 assert!(matches!(
3744 result.unwrap_err(),
3745 ParseError::InvalidMetadataJson(_)
3746 ));
3747 }
3748
3749 #[test]
3750 fn test_parse_frame_header_unsupported_version() {
3751 let lines = [
3752 "PREBOX1",
3753 "{\"con_spec_version\":999}",
3754 "10.0 20.0 30.0",
3755 "90.0 90.0 90.0",
3756 "POSTBOX1",
3757 "POSTBOX2",
3758 "2",
3759 "1 1",
3760 "12.011 1.008",
3761 ];
3762 let mut line_it = lines.iter().copied();
3763 let result = parse_frame_header(&mut line_it);
3764 assert!(result.is_err());
3765 assert!(matches!(
3766 result.unwrap_err(),
3767 ParseError::UnsupportedVersion(999)
3768 ));
3769 }
3770
3771 #[test]
3772 fn test_v3_missing_units_rejected() {
3773 let lines = [
3774 "PREBOX1",
3775 "{\"con_spec_version\":3}",
3776 "10.0 20.0 30.0",
3777 "90.0 90.0 90.0",
3778 "POSTBOX1",
3779 "POSTBOX2",
3780 "1",
3781 "1",
3782 "1.0",
3783 ];
3784 let mut line_it = lines.iter().copied();
3785 let err = parse_frame_header(&mut line_it).unwrap_err();
3786 let msg = err.to_string();
3787 assert!(
3788 msg.contains("units") || msg.contains("v3"),
3789 "expected units error, got {msg}"
3790 );
3791 }
3792
3793 #[test]
3794 fn test_v3_invalid_units_rejected() {
3795 let lines = [
3796 "PREBOX1",
3797 r#"{"con_spec_version":3,"units":{"length":"eV","energy":"angstrom"}}"#,
3798 "10.0 20.0 30.0",
3799 "90.0 90.0 90.0",
3800 "POSTBOX1",
3801 "POSTBOX2",
3802 "1",
3803 "1",
3804 "1.0",
3805 ];
3806 let mut line_it = lines.iter().copied();
3807 assert!(parse_frame_header(&mut line_it).is_err());
3808 }
3809
3810 #[test]
3811 fn test_v3_valid_units_exposes_length_energy() {
3812 let lines = [
3813 "PREBOX1",
3814 r#"{"con_spec_version":3,"units":{"length":"angstrom","energy":"eV","mass":"amu","time":"fs"}}"#,
3815 "10.0 20.0 30.0",
3816 "90.0 90.0 90.0",
3817 "POSTBOX1",
3818 "POSTBOX2",
3819 "1",
3820 "1",
3821 "1.0",
3822 ];
3823 let mut line_it = lines.iter().copied();
3824 let header = parse_frame_header(&mut line_it).unwrap();
3825 assert_eq!(header.spec_version, 3);
3826 assert_eq!(header.length_unit(), Some("angstrom"));
3827 assert_eq!(header.energy_unit(), Some("eV"));
3828 let f = header.conversion_factor_to("length", "nm").unwrap();
3829 assert!((f - 0.1).abs() < 1e-12);
3830 }
3831
3832 #[test]
3833 fn test_parse_frame_header_extra_metadata_preserved() {
3834 let lines = [
3835 "PREBOX1",
3836 "{\"con_spec_version\":2,\"generator\":\"test\"}",
3837 "10.0 20.0 30.0",
3838 "90.0 90.0 90.0",
3839 "POSTBOX1",
3840 "POSTBOX2",
3841 "2",
3842 "1 1",
3843 "12.011 1.008",
3844 ];
3845 let mut line_it = lines.iter().copied();
3846 let header = parse_frame_header(&mut line_it).unwrap();
3847 assert_eq!(header.spec_version, 2);
3848 assert_eq!(
3849 header.metadata.get("generator"),
3850 Some(&serde_json::Value::String("test".to_string()))
3851 );
3852 }
3853
3854 #[test]
3855 fn test_parse_frame_header_invalid_natms_per_type() {
3856 let lines = [
3857 "PREBOX1",
3858 "{\"con_spec_version\":2}",
3859 "10.0 20.0 30.0",
3860 "90.0 90.0 90.0",
3861 "POSTBOX1",
3862 "POSTBOX2",
3863 "2",
3864 "1 1 1", "12.011 1.008",
3866 ];
3867 let mut line_it = lines.iter().copied();
3868 let result = parse_frame_header(&mut line_it);
3869 assert!(result.is_err());
3870 assert!(matches!(
3871 result.unwrap_err(),
3872 ParseError::InvalidVectorLength {
3873 expected: 2,
3874 found: 3
3875 }
3876 ));
3877 }
3878
3879 #[test]
3880 fn test_parse_single_frame_success() {
3881 let lines = vec![
3882 "PREBOX1",
3883 "{\"con_spec_version\":2}",
3884 "10.0 20.0 30.0",
3885 "90.0 90.0 90.0",
3886 "POSTBOX1",
3887 "POSTBOX2",
3888 "2",
3889 "3 3",
3890 "12.011 1.008",
3891 "1",
3892 "Coordinates of Component 1",
3893 "0.0 0.0 0.0 0 1",
3894 "1.0940 0.0 0.0 0 2",
3895 "-0.5470 0.9499 0.0 0 3",
3896 "2",
3897 "Coordinates of Component 2",
3898 "5.0 5.0 5.0 0 4",
3899 "6.0940 5.0 5.0 0 5",
3900 "5.5470 5.9499 5.0 0 6",
3901 ];
3902 let mut line_it = lines.iter().copied();
3903 let frame = parse_single_frame(&mut line_it).unwrap();
3904
3905 assert_eq!(frame.header.natm_types, 2);
3906 assert_eq!(frame.header.natms_per_type, vec![3, 3]);
3907 assert_eq!(frame.header.masses_per_type, vec![12.011, 1.008]);
3908 assert_eq!(frame.atom_data.len(), 6);
3909 assert_eq!(&*frame.atom_data[0].symbol, "1");
3910 assert_eq!(frame.atom_data[0].atom_id, 1);
3911 assert_eq!(&*frame.atom_data[5].symbol, "2");
3912 assert_eq!(frame.atom_data[5].atom_id, 6);
3913 }
3914
3915 #[test]
3916 fn test_column_4_value_1_is_x_only_on_spec_2() {
3917 let lines = [
3918 "Generated by probe",
3919 "{\"con_spec_version\":2}",
3920 "20.0 20.0 20.0",
3921 "90.0 90.0 90.0",
3922 "",
3923 "",
3924 "1",
3925 "1",
3926 "63.5",
3927 "Cu",
3928 "Coordinates of component 1",
3929 "1.0 2.0 3.0 1 1",
3930 ];
3931 let mut line_it = lines.iter().copied();
3932 let frame = parse_single_frame(&mut line_it).unwrap();
3933 assert_eq!(frame.header.spec_version, 2);
3934 assert_eq!(frame.atom_data[0].fixed(), [true, false, false]);
3935 }
3936
3937 #[test]
3938 fn test_column_4_value_1_is_all_fixed_on_spec_1() {
3939 let lines = [
3940 "Generated by probe",
3941 "0.0000 TIME",
3942 "20.0 20.0 20.0",
3943 "90.0 90.0 90.0",
3944 "",
3945 "",
3946 "1",
3947 "1",
3948 "63.5",
3949 "Cu",
3950 "Coordinates of component 1",
3951 "1.0 2.0 3.0 1 1",
3952 ];
3953 let mut line_it = lines.iter().copied();
3954 let frame = parse_single_frame(&mut line_it).unwrap();
3955 assert_eq!(frame.header.spec_version, 1);
3956 assert_eq!(frame.atom_data[0].fixed(), [true, true, true]);
3957 }
3958
3959 #[test]
3960 fn test_parse_single_frame_missing_line() {
3961 let lines = vec![
3963 "PREBOX1",
3964 "{\"con_spec_version\":2}",
3965 "10.0 20.0 30.0",
3966 "90.0 90.0 90.0",
3967 "POSTBOX1",
3968 "POSTBOX2",
3969 "2",
3970 "3 3",
3971 "12.011 1.008",
3972 "1",
3973 "Coordinates of Component 1",
3974 "0.0 0.0 0.0 0 1",
3975 "1.0940 0.0 0.0 0 2",
3976 "-0.5470 0.9499 0.0 0 3",
3977 ];
3979 let mut line_it = lines.iter().copied();
3980 let result = parse_single_frame(&mut line_it);
3981 assert!(result.is_err());
3982 assert!(matches!(result.unwrap_err(), ParseError::IncompleteFrame));
3983 }
3984
3985 #[test]
3986 fn test_parse_single_frame_missing_atom_index_defaults_sequential() {
3987 let lines = vec![
3989 "PREBOX1",
3990 "{\"con_spec_version\":2}",
3991 "10.0 20.0 30.0",
3992 "90.0 90.0 90.0",
3993 "POSTBOX1",
3994 "POSTBOX2",
3995 "2",
3996 "3 3",
3997 "12.011 1.008",
3998 "1",
3999 "Coordinates of Component 1",
4000 "0.0 0.0 0.0 0 1",
4001 "1.0940 0.0 0.0 0 2",
4002 "-0.5470 0.9499 0.0 0 3",
4003 "2",
4004 "Coordinates of Component 2",
4005 "5.0 5.0 5.0 0", "6.0940 5.0 5.0 0 10", "5.5470 5.9499 5.0 0", ];
4009 let mut line_it = lines.iter().copied();
4010 let frame = parse_single_frame(&mut line_it).unwrap();
4011 assert_eq!(frame.atom_data.len(), 6);
4012 assert_eq!(frame.atom_data[0].atom_id, 1);
4014 assert_eq!(frame.atom_data[1].atom_id, 2);
4015 assert_eq!(frame.atom_data[2].atom_id, 3);
4016 assert_eq!(frame.atom_data[3].atom_id, 3); assert_eq!(frame.atom_data[4].atom_id, 10); assert_eq!(frame.atom_data[5].atom_id, 5); }
4021
4022 #[test]
4023 fn test_parse_single_frame_too_few_columns_fails() {
4024 let lines = [
4026 "PREBOX1",
4027 "{\"con_spec_version\":2}",
4028 "10.0 20.0 30.0",
4029 "90.0 90.0 90.0",
4030 "POSTBOX1",
4031 "POSTBOX2",
4032 "1",
4033 "1",
4034 "12.011",
4035 "C",
4036 "Coordinates of Component 1",
4037 "0.0 0.0 0.0", ];
4039 let mut line_it = lines.iter().copied();
4040 let result = parse_single_frame(&mut line_it);
4041 assert!(result.is_err());
4042 assert!(matches!(
4043 result.unwrap_err(),
4044 ParseError::InvalidVectorLength {
4045 expected: 5,
4046 found: 3
4047 }
4048 ));
4049 }
4050
4051 #[test]
4052 fn test_parse_velocity_section_present() {
4053 let lines = vec![
4054 "PREBOX1",
4055 "{\"con_spec_version\":2}",
4056 "10.0 20.0 30.0",
4057 "90.0 90.0 90.0",
4058 "POSTBOX1",
4059 "POSTBOX2",
4060 "2",
4061 "1 1",
4062 "63.546 1.008",
4063 "Cu",
4064 "Coordinates of Component 1",
4065 "0.0 0.0 0.0 1 0",
4066 "H",
4067 "Coordinates of Component 2",
4068 "1.0 2.0 3.0 0 1",
4069 "",
4070 "Cu",
4071 "Velocities of Component 1",
4072 "0.1 0.2 0.3 1 0",
4073 "H",
4074 "Velocities of Component 2",
4075 "0.4 0.5 0.6 0 1",
4076 ];
4077 let mut line_it = lines.iter().copied().peekable();
4078 let mut frame =
4080 parse_single_frame(&mut line_it).expect("coordinate parsing should succeed");
4081 assert!(!frame.has_velocities());
4082
4083 let mut column = Vec::new();
4085 let has_vel = parse_velocity_section(
4086 &mut line_it,
4087 &frame.header,
4088 &mut frame.atom_data,
4089 &mut column,
4090 )
4091 .expect("velocity parsing should succeed");
4092 assert!(has_vel);
4093 frame.adopt_velocity_column(column);
4094 assert_eq!(frame.atom_data[0].velocity(), Some([0.1, 0.2, 0.3]));
4095 assert_eq!(frame.atom_data[1].velocity(), Some([0.4, 0.5, 0.6]));
4096 }
4097
4098 #[test]
4099 fn test_validate_true_accepts_matching_section_identity() {
4100 let text = r#"
4101PREBOX1
4102{"con_spec_version":2,"sections":["velocities"],"validate":true}
410310.0 20.0 30.0
410490.0 90.0 90.0
4105POSTBOX1
4106POSTBOX2
41072
41081 1
410963.546 1.008
4110Cu
4111Coordinates of Component 1
41120.0 0.0 0.0 5 0
4113H
4114Coordinates of Component 2
41151.0 2.0 3.0 0 1
4116
4117Cu
4118Velocities of Component 1
41190.1 0.2 0.3 5 0
4120H
4121Velocities of Component 2
41220.4 0.5 0.6 0 1
4123"#;
4124 let mut iter = ConFrameIterator::new(text.trim());
4125 let frame = iter.next().unwrap().unwrap();
4126
4127 assert!(frame.has_velocities());
4128 assert_eq!(
4129 frame
4130 .header
4131 .metadata
4132 .get("validate")
4133 .and_then(|v| v.as_bool()),
4134 Some(true)
4135 );
4136 }
4137
4138 #[test]
4139 fn test_validate_true_rejects_section_atom_id_mismatch() {
4140 let text = r#"
4141PREBOX1
4142{"con_spec_version":2,"sections":["velocities"],"validate":true}
414310.0 20.0 30.0
414490.0 90.0 90.0
4145POSTBOX1
4146POSTBOX2
41471
41481
414963.546
4150Cu
4151Coordinates of Component 1
41520.0 0.0 0.0 5 0
4153
4154Cu
4155Velocities of Component 1
41560.1 0.2 0.3 5 99
4157"#;
4158 let mut iter = ConFrameIterator::new(text.trim());
4159 let err = iter.next().unwrap().unwrap_err();
4160
4161 assert!(matches!(err, ParseError::ValidationError(_)));
4162 assert!(err.to_string().contains("atom_id mismatch"));
4163 }
4164
4165 #[test]
4166 fn test_validate_true_rejects_section_symbol_mismatch() {
4167 let text = r#"
4168PREBOX1
4169{"con_spec_version":2,"sections":["forces"],"validate":true}
417010.0 20.0 30.0
417190.0 90.0 90.0
4172POSTBOX1
4173POSTBOX2
41741
41751
417663.546
4177Cu
4178Coordinates of Component 1
41790.0 0.0 0.0 5 0
4180
4181H
4182Forces of Component 1
41830.1 0.2 0.3 5 0
4184"#;
4185 let mut iter = ConFrameIterator::new(text.trim());
4186 let err = iter.next().unwrap().unwrap_err();
4187
4188 assert!(matches!(err, ParseError::ValidationError(_)));
4189 assert!(err.to_string().contains("symbol mismatch"));
4190 }
4191
4192 #[test]
4193 fn test_validate_absent_allows_legacy_duplicate_identity_mismatch() {
4194 let text = r#"
4195PREBOX1
4196{"con_spec_version":2,"sections":["velocities"]}
419710.0 20.0 30.0
419890.0 90.0 90.0
4199POSTBOX1
4200POSTBOX2
42011
42021
420363.546
4204Cu
4205Coordinates of Component 1
42060.0 0.0 0.0 5 0
4207
4208Cu
4209Velocities of Component 1
42100.1 0.2 0.3 0 99
4211"#;
4212 let mut iter = ConFrameIterator::new(text.trim());
4213 let frame = iter.next().unwrap().unwrap();
4214
4215 assert!(frame.has_velocities());
4216 assert_eq!(frame.atom_data[0].atom_id, 0);
4217 assert_eq!(frame.atom_data[0].fixed(), [true, false, true]);
4218 }
4219
4220 #[test]
4221 fn test_validate_must_be_boolean_when_present() {
4222 let lines = [
4223 "PREBOX1",
4224 "{\"con_spec_version\":2,\"validate\":\"yes\",\"sections\":[]}",
4225 "10.0 20.0 30.0",
4226 "90.0 90.0 90.0",
4227 "POSTBOX1",
4228 "POSTBOX2",
4229 "1",
4230 "1",
4231 "12.011",
4232 ];
4233 let mut line_it = lines.iter().copied();
4234 let err = parse_frame_header(&mut line_it).unwrap_err();
4235
4236 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
4237 assert!(err.to_string().contains("validate"));
4238 }
4239
4240 #[test]
4241 fn test_validate_true_requires_sections_key() {
4242 let lines = [
4243 "PREBOX1",
4244 "{\"con_spec_version\":2,\"validate\":true}",
4245 "10.0 20.0 30.0",
4246 "90.0 90.0 90.0",
4247 "POSTBOX1",
4248 "POSTBOX2",
4249 "1",
4250 "1",
4251 "12.011",
4252 ];
4253 let mut line_it = lines.iter().copied();
4254 let err = parse_frame_header(&mut line_it).unwrap_err();
4255
4256 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
4257 assert!(err.to_string().contains("sections"));
4258 }
4259
4260 #[test]
4261 fn test_sections_must_be_string_array_when_present() {
4262 let lines = [
4263 "PREBOX1",
4264 "{\"con_spec_version\":2,\"sections\":[\"velocities\",7]}",
4265 "10.0 20.0 30.0",
4266 "90.0 90.0 90.0",
4267 "POSTBOX1",
4268 "POSTBOX2",
4269 "1",
4270 "1",
4271 "12.011",
4272 ];
4273 let mut line_it = lines.iter().copied();
4274 let err = parse_frame_header(&mut line_it).unwrap_err();
4275
4276 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
4277 assert!(err.to_string().contains("sections"));
4278 }
4279
4280 #[test]
4281 fn test_validate_true_rejects_non_integer_coordinate_identity_columns() {
4282 let text = r#"
4283PREBOX1
4284{"con_spec_version":2,"sections":[],"validate":true}
428510.0 20.0 30.0
428690.0 90.0 90.0
4287POSTBOX1
4288POSTBOX2
42891
42901
429163.546
4292Cu
4293Coordinates of Component 1
42940.0 0.0 0.0 5.0 0
4295"#;
4296 let mut iter = ConFrameIterator::new(text.trim());
4297 let err = iter.next().unwrap().unwrap_err();
4298
4299 assert!(matches!(err, ParseError::InvalidFixedFlag(_)));
4300 assert!(err.to_string().contains("5.0"));
4301 }
4302
4303 #[test]
4304 fn test_validate_true_rejects_non_exact_coordinate_label() {
4305 let text = r#"
4306PREBOX1
4307{"con_spec_version":2,"sections":[],"validate":true}
430810.0 20.0 30.0
430990.0 90.0 90.0
4310POSTBOX1
4311POSTBOX2
43121
43131
431463.546
4315Cu
4316Coordinates Component 1
43170.0 0.0 0.0 5 0
4318"#;
4319 let mut iter = ConFrameIterator::new(text.trim());
4320 let err = iter.next().unwrap().unwrap_err();
4321
4322 assert!(matches!(err, ParseError::ValidationError(_)));
4323 assert!(err.to_string().contains("Coordinates of Component 1"));
4324 }
4325
4326 #[test]
4327 fn test_validate_true_rejects_unknown_component_symbol() {
4328 let text = r#"
4329PREBOX1
4330{"con_spec_version":2,"sections":[],"validate":true}
433110.0 20.0 30.0
433290.0 90.0 90.0
4333POSTBOX1
4334POSTBOX2
43351
43361
433763.546
4338Qq
4339Coordinates of Component 1
43400.0 0.0 0.0 0 0
4341"#;
4342 let mut iter = ConFrameIterator::new(text.trim());
4343 let err = iter.next().unwrap().unwrap_err();
4344
4345 assert!(matches!(err, ParseError::ValidationError(_)));
4346 assert!(err.to_string().contains("symbol"));
4347 }
4348
4349 #[test]
4350 fn test_declared_section_must_be_present() {
4351 let text = r#"
4352PREBOX1
4353{"con_spec_version":2,"sections":["velocities"]}
435410.0 20.0 30.0
435590.0 90.0 90.0
4356POSTBOX1
4357POSTBOX2
43581
43591
436063.546
4361Cu
4362Coordinates of Component 1
43630.0 0.0 0.0 0 0
4364"#;
4365 let mut iter = ConFrameIterator::new(text.trim());
4366 let err = iter.next().unwrap().unwrap_err();
4367
4368 assert!(matches!(
4369 err,
4370 ParseError::IncompleteSection(ref name) if name == "velocities"
4371 ));
4372 }
4373
4374 #[test]
4375 fn test_non_finite_cell_geometry_rejected_in_strict_mode() {
4376 let lines = [
4377 "PREBOX1",
4378 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
4379 "10.0 NaN 30.0",
4380 "90.0 90.0 90.0",
4381 "POSTBOX1",
4382 "POSTBOX2",
4383 "1",
4384 "1",
4385 "12.011",
4386 ];
4387 let mut line_it = lines.iter().copied();
4388 let err = parse_frame_header(&mut line_it).unwrap_err();
4389
4390 assert!(matches!(err, ParseError::NonFiniteValue(_)));
4391 assert!(err.to_string().contains("NaN"));
4392 }
4393
4394 #[test]
4395 fn test_validate_true_rejects_non_physical_cell_geometry() {
4396 let lines = [
4397 "PREBOX1",
4398 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
4399 "0.0 20.0 30.0",
4400 "90.0 180.0 90.0",
4401 "POSTBOX1",
4402 "POSTBOX2",
4403 "1",
4404 "1",
4405 "12.011",
4406 ];
4407 let mut line_it = lines.iter().copied();
4408 let err = parse_frame_header(&mut line_it).unwrap_err();
4409
4410 assert!(matches!(err, ParseError::ValidationError(_)));
4411 assert!(err.to_string().contains("cell"));
4412 }
4413
4414 #[test]
4415 fn test_validate_true_rejects_reserved_metadata_type_mismatch() {
4416 let lines = [
4417 "PREBOX1",
4418 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"energy\":\"low\"}",
4419 "10.0 20.0 30.0",
4420 "90.0 90.0 90.0",
4421 "POSTBOX1",
4422 "POSTBOX2",
4423 "1",
4424 "1",
4425 "12.011",
4426 ];
4427 let mut line_it = lines.iter().copied();
4428 let err = parse_frame_header(&mut line_it).unwrap_err();
4429
4430 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
4431 assert!(err.to_string().contains("energy"));
4432 }
4433
4434 #[test]
4435 fn test_validate_true_rejects_malformed_bonds() {
4436 let lines = [
4437 "PREBOX1",
4438 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"bonds\":[[0]]}",
4439 "10.0 20.0 30.0",
4440 "90.0 90.0 90.0",
4441 "POSTBOX1",
4442 "POSTBOX2",
4443 "1",
4444 "1",
4445 "12.011",
4446 ];
4447 let mut line_it = lines.iter().copied();
4448 let err = parse_frame_header(&mut line_it).unwrap_err();
4449 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
4450 assert!(err.to_string().contains("bonds"));
4451 }
4452
4453 #[test]
4454 fn test_bonds_metadata_round_trip_in_header() {
4455 use crate::types::{Bond, meta};
4456 let lines = [
4457 "PREBOX1",
4458 "{\"con_spec_version\":2,\"bonds\":[[0,1],{\"i\":0,\"j\":2,\"order\":1}]}",
4459 "10.0 20.0 30.0",
4460 "90.0 90.0 90.0",
4461 "POSTBOX1",
4462 "POSTBOX2",
4463 "1",
4464 "1",
4465 "12.011",
4466 ];
4467 let mut line_it = lines.iter().copied();
4468 let header = parse_frame_header(&mut line_it).expect("header");
4469 let bonds = header.bonds();
4470 assert_eq!(bonds.len(), 2);
4471 assert_eq!(bonds[0], Bond::new(0, 1));
4472 assert_eq!(bonds[1].i, 0);
4473 assert_eq!(bonds[1].j, 2);
4474 assert_eq!(bonds[1].order, Some(1));
4475 assert!(header.metadata.contains_key(meta::BONDS));
4476 }
4477
4478 #[test]
4479 fn test_parse_velocity_section_absent() {
4480 let lines = [
4481 "PREBOX1",
4482 "{\"con_spec_version\":2}",
4483 "10.0 20.0 30.0",
4484 "90.0 90.0 90.0",
4485 "POSTBOX1",
4486 "POSTBOX2",
4487 "1",
4488 "1",
4489 "12.011",
4490 "C",
4491 "Coordinates of Component 1",
4492 "0.0 0.0 0.0 0 1",
4493 ];
4494 let mut line_it = lines.iter().copied().peekable();
4495 let mut frame = parse_single_frame(&mut line_it).expect("parse should succeed");
4496 let mut column = Vec::new();
4497 let has_vel = parse_velocity_section(
4498 &mut line_it,
4499 &frame.header,
4500 &mut frame.atom_data,
4501 &mut column,
4502 )
4503 .expect("should succeed with no velocities");
4504 assert!(!has_vel);
4505 assert!(column.is_empty());
4506 assert_eq!(frame.atom_data[0].velocity(), None);
4507 }
4508
4509 #[test]
4512 fn velocity_column_aliases_soa_bytes() {
4513 let text = "\
4514bulk
4515{\"con_spec_version\":2}
45164.0 5.0 6.0
451790.0 90.0 90.0
45180 0
45190 0
45201
45212
452263.546
4523Cu
4524Coordinates of Component 1
45250.0 0.0 0.0 7 0
45261.0 0.0 0.0 0 1
4527
4528Cu
4529Velocities of Component 1
45300.125 -0.25 0.5 7 0
4531-0.125 0.25 -0.5 0 1
4532";
4533 let mut frame = ConFrameIterator::new(text)
4534 .next()
4535 .expect("frame")
4536 .expect("parse");
4537 let soa = frame.velocities.f64_slice().expect("f64 column");
4538 let mut packed = Vec::with_capacity(soa.len());
4539 for atom in &frame.atom_data {
4540 packed.extend_from_slice(&atom.velocity().expect("velocity"));
4541 }
4542 assert_eq!(soa, packed.as_slice());
4543 assert_eq!(soa, &[0.125, -0.25, 0.5, -0.125, 0.25, -0.5][..]);
4544 assert_eq!(frame.atom_data[0].velocity_slot(), soa.as_ptr());
4545 frame.atom_data[0].set_velocity(Some([9.0, 8.0, 7.0]));
4546 assert_eq!(frame.velocities.as_f64_row(0), [9.0, 8.0, 7.0]);
4547 assert_eq!(frame.velocities.as_f64_row(1), [-0.125, 0.25, -0.5]);
4548 }
4549
4550 #[test]
4551 fn force_column_aliases_soa_bytes() {
4552 let text = "\
4553bulk
4554{\"con_spec_version\":2,\"sections\":[\"forces\"]}
45554.0 5.0 6.0
455690.0 90.0 90.0
45570 0
45580 0
45591
45602
456163.546
4562Cu
4563Coordinates of Component 1
45640.0 0.0 0.0 0 0
45651.0 1.0 1.0 7 1
4566
4567Cu
4568Forces of Component 1
45690.1 0.2 0.3 0 0
4570-0.4 0.5 -0.6 7 1
4571";
4572 let mut frame = ConFrameIterator::new(text)
4573 .next()
4574 .expect("frame")
4575 .expect("parse");
4576 let soa = frame.forces.f64_slice().expect("f64 column");
4577 let mut packed = Vec::with_capacity(soa.len());
4578 for atom in &frame.atom_data {
4579 packed.extend_from_slice(&atom.force().expect("force"));
4580 }
4581 assert_eq!(soa, packed.as_slice());
4582 assert_eq!(soa, &[0.1, 0.2, 0.3, -0.4, 0.5, -0.6][..]);
4583 assert_eq!(frame.atom_data[0].force_slot(), soa.as_ptr());
4584 frame.atom_data[1].set_force(Some([1.0, 2.0, 3.0]));
4585 assert_eq!(frame.forces.as_f64_row(1), [1.0, 2.0, 3.0]);
4586 }
4587
4588 #[test]
4589 fn convel_column_parse_shares_the_velocity_slab() {
4590 let n = 8usize;
4591 let mut text = String::new();
4592 text.push_str("bench\n{\"con_spec_version\":2}\n4 5 6\n90 90 90\n0 0\n0 0\n1\n");
4593 text.push_str(&format!("{n}\n63.546\nCu\nCoordinates of Component 1\n"));
4594 for i in 0..n {
4595 text.push_str(&format!("0 0 0 0 {i}\n"));
4596 }
4597 text.push_str("\nCu\nVelocities of Component 1\n");
4598 for i in 0..n {
4599 let v = (i % 1000) as f64 * 0.001;
4600 text.push_str(&format!("{v} 0 0 0 {i}\n"));
4601 }
4602 let frame = ConFrameIterator::new(&text)
4603 .next()
4604 .expect("frame")
4605 .expect("parse");
4606 let soa = frame.velocities.f64_slice().expect("f64");
4607 assert_eq!(frame.atom_data[0].velocity_slot(), soa.as_ptr());
4608 assert_eq!(frame.atom_data[n - 1].velocity().unwrap()[0], 0.007);
4609 assert_eq!(soa[(n - 1) * 3], 0.007);
4610 let bytes = text.into_bytes();
4611 let again = ConFrameIterator::from_bytes(&bytes)
4612 .next()
4613 .expect("frame")
4614 .expect("parse");
4615 let again_soa = again.velocities.f64_slice().expect("f64");
4616 assert_eq!(again.atom_data[0].velocity_slot(), again_soa.as_ptr());
4617 assert_eq!(
4618 again.atom_data[0].velocity().unwrap(),
4619 again.velocities.as_f64_row(0)
4620 );
4621 }
4622
4623 #[test]
4624 fn test_parse_line_of_range_f64_exact() {
4625 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0 42", 4, 5, &[0.0; 5]).unwrap();
4626 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 42.0]);
4627 }
4628
4629 #[test]
4630 fn test_byte_scan_stack_parses_realistic_coord_line() {
4631 let line = " 0.63939999999999997 0.90449999999999997 -0.00009999999999977 1 0";
4633 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
4634 let mut buf = [0.0f64; NUMERIC_STACK_WIDTH];
4635 let n = parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
4636 assert_eq!(n, 5);
4637 assert!((buf[0] - 0.6394).abs() < 1e-4);
4638 assert!((buf[1] - 0.9045).abs() < 1e-4);
4639 assert_eq!(buf[3] as u8, 1);
4640 assert_eq!(buf[4] as u64, 0);
4641 let bad = "1.0 2.0 3.0 1 0 EXTRA";
4643 assert!(parse_line_of_range_f64_stack(bad, 4, 5, &defaults, &mut buf).is_err());
4644 let glued = "1.0 2.0 3.0abc 1 0";
4646 assert!(parse_line_of_range_f64_stack(glued, 4, 5, &defaults, &mut buf).is_err());
4647 }
4648
4649 #[test]
4650 fn test_parse_line_of_range_f64_stack_matches_vec_api() {
4651 let defaults = [0.0, 0.0, 0.0, 0.0, 7.0];
4652 let line = "1.0 2.0 3.0 0.0";
4653 let mut buf = [0.0f64; NUMERIC_STACK_WIDTH];
4654 parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
4655 let via_vec = parse_line_of_range_f64(line, 4, 5, &defaults).unwrap();
4656 assert_eq!(&buf[..5], via_vec.as_slice());
4657 assert_eq!(buf[4], 7.0);
4658 }
4659
4660 #[test]
4661 fn test_parse_line_of_range_f64_padded() {
4662 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
4663 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0", 4, 5, &defaults).unwrap();
4664 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 99.0]);
4665 }
4666
4667 #[test]
4668 fn test_parse_line_of_range_f64_too_few() {
4669 let result = parse_line_of_range_f64("1.0 2.0 3.0", 4, 5, &[0.0; 5]);
4670 assert!(result.is_err());
4671 }
4672
4673 #[test]
4674 fn test_parse_line_of_range_f64_too_many() {
4675 let result = parse_line_of_range_f64("1.0 2.0 3.0 0.0 5.0 6.0", 4, 5, &[0.0; 5]);
4676 assert!(result.is_err());
4677 }
4678
4679 #[test]
4680 fn test_parse_all_four_column_lines() {
4681 let lines = [
4683 "PREBOX1",
4684 "{\"con_spec_version\":2}",
4685 "10.0 10.0 10.0",
4686 "90.0 90.0 90.0",
4687 "POSTBOX1",
4688 "POSTBOX2",
4689 "1",
4690 "3",
4691 "12.011",
4692 "C",
4693 "Coordinates of Component 1",
4694 "0.0 0.0 0.0 0",
4695 "1.0 0.0 0.0 0",
4696 "2.0 0.0 0.0 1",
4697 ];
4698 let mut line_it = lines.iter().copied();
4699 let frame = parse_single_frame(&mut line_it).unwrap();
4700 assert_eq!(frame.atom_data[0].atom_id, 0);
4701 assert_eq!(frame.atom_data[1].atom_id, 1);
4702 assert_eq!(frame.atom_data[2].atom_id, 2);
4703 assert!(frame.atom_data[2].is_fixed());
4704 }
4705
4706 #[test]
4707 fn test_positions_take_the_storage_dtype_of_the_metadata() {
4708 use crate::storage_dtype::{ElementKind, FloatArray2};
4709 let rows = [[0.5, -1.25, 3.0], [1.0e-3, 2.5e4, -7.75]];
4710 let atom_lines: Vec<String> = rows
4711 .iter()
4712 .enumerate()
4713 .map(|(id, [x, y, z])| format!("{x} {y} {z} 0 {id}"))
4714 .collect();
4715 for &kind in ElementKind::all_hosted() {
4716 let metadata = format!(
4717 "{{\"con_spec_version\":3,\"units\":{{\"length\":\"angstrom\",\"energy\":\"eV\"}},\
4718 \"storage_dtypes\":{{\"positions\":\"{}\"}}}}",
4719 kind.as_str()
4720 );
4721 let mut lines = vec![
4722 "PREBOX1",
4723 metadata.as_str(),
4724 "10.0 10.0 10.0",
4725 "90.0 90.0 90.0",
4726 "POSTBOX1",
4727 "POSTBOX2",
4728 "1",
4729 "2",
4730 "1.008",
4731 "H",
4732 "Coordinates of Component 1",
4733 ];
4734 lines.extend(atom_lines.iter().map(String::as_str));
4735 let frame = parse_single_frame(&mut lines.iter().copied()).unwrap();
4736 assert_eq!(frame.positions.kind(), kind);
4737 assert_eq!(frame.positions.nrows(), rows.len());
4738 for (i, &xyz) in rows.iter().enumerate() {
4739 let mut expected = FloatArray2::zeros(kind, 1, 3);
4740 expected.set_f64_row(0, xyz);
4741 assert_eq!(
4742 frame.positions.as_f64_row(i),
4743 expected.as_f64_row(0),
4744 "{kind:?} row {i}"
4745 );
4746 }
4747 }
4748 }
4749}
4750
4751#[cfg(all(test, feature = "parallel"))]
4752mod newline_index_bench {
4753 use super::newline_index_at;
4754
4755 #[test]
4756 fn index_matches_memchr() {
4757 let mut bytes = vec![b'x'; 80_000];
4758 for index in (20..bytes.len()).step_by(23) {
4759 bytes[index] = b'\n';
4760 }
4761 let serial: Vec<usize> = memchr::memchr_iter(b'\n', &bytes).collect();
4762 assert_eq!(newline_index_at(&bytes, 0, 8), serial);
4763 let mut long = Vec::new();
4765 while long.len() < (1 << 20) + 64 {
4766 long.extend_from_slice(b"01234567 1.25 -2.5 3.5 0 7\n");
4767 }
4768 let long_serial: Vec<usize> = memchr::memchr_iter(b'\n', &long).collect();
4769 assert_eq!(newline_index_at(&long, 40, 8), {
4770 let mut shifted = long_serial.clone();
4771 for offset in &mut shifted {
4772 *offset += 40;
4773 }
4774 shifted
4775 });
4776 let bare = vec![b'\n'; (1 << 20) + 64];
4777 let bare_serial: Vec<usize> = memchr::memchr_iter(b'\n', &bare).collect();
4778 assert_eq!(newline_index_at(&bare, 0, 8), bare_serial);
4779 }
4780}