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