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::collections::BTreeMap;
10use std::iter::Peekable;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicUsize, Ordering};
13
14pub trait LineStream<'a> {
19 fn next_line(&mut self) -> Option<&'a str>;
20 fn peek_line(&mut self) -> Option<&'a str>;
21 fn remainder_is_whitespace(&mut self) -> bool {
25 false
26 }
27 fn blank_line_opens_section(&mut self) -> bool {
31 false
32 }
33
34 fn blank_opens_velocity_block(&mut self) -> bool {
38 true
39 }
40
41 fn next_coord_row(
47 &mut self,
48 spec_version: u32,
49 default_id: u64,
50 ) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
51 let line = self.next_line().ok_or(ParseError::IncompleteFrame)?;
52 parse_coord_row(line, spec_version, default_id)
53 }
54
55 fn next_vector_row(
59 &mut self,
60 spec_version: u32,
61 ) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
62 let line = self.next_line().ok_or(ParseError::IncompleteFrame)?;
63 parse_vector_section_row(line, spec_version)
64 }
65}
66
67fn take_vector_row<'a>(
68 lines: &mut impl LineStream<'a>,
69 spec_version: u32,
70 section: &str,
71) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
72 match lines.next_vector_row(spec_version) {
73 Ok(row) => Ok(row),
74 Err(ParseError::IncompleteFrame) => Err(ParseError::IncompleteSection(section.to_string())),
75 Err(err) => Err(err),
76 }
77}
78
79impl<'a, I> LineStream<'a> for Peekable<I>
80where
81 I: Iterator<Item = &'a str> + Clone,
82{
83 #[inline]
84 fn next_line(&mut self) -> Option<&'a str> {
85 Iterator::next(self)
86 }
87 #[inline]
88 fn peek_line(&mut self) -> Option<&'a str> {
89 Peekable::peek(self).copied()
90 }
91 fn blank_line_opens_section(&mut self) -> bool {
92 let mut probe = self.clone();
93 let Some(blank) = probe.next_line() else {
94 return false;
95 };
96 if !blank.trim().is_empty() {
97 return false;
98 }
99 section_follows(&mut probe)
100 }
101
102 fn blank_opens_velocity_block(&mut self) -> bool {
103 match self.peek_line() {
104 Some(line) if line.trim().is_empty() => self.blank_line_opens_section(),
105 _ => true,
106 }
107 }
108}
109
110pub(crate) fn line_has_component_label(line: &str) -> bool {
112 let mut words = line.split_ascii_whitespace();
113 while let Some(word) = words.next() {
114 if word.eq_ignore_ascii_case("of")
115 && words
116 .next()
117 .is_some_and(|next| next.eq_ignore_ascii_case("component"))
118 {
119 return true;
120 }
121 }
122 false
123}
124
125fn section_follows<'a>(lines: &mut impl LineStream<'a>) -> bool {
126 let Some(symbol) = lines.next_line() else {
127 return false;
128 };
129 if symbol.trim().is_empty() || symbol.trim_start().starts_with('{') {
130 return false;
131 }
132 let Some(label) = lines.next_line() else {
133 return false;
134 };
135 line_has_component_label(label)
136}
137
138#[inline]
147pub fn parse_line_of_range_f64_stack(
148 line: &str,
149 min: usize,
150 max: usize,
151 defaults: &[f64],
152 out: &mut [f64; 5],
153) -> Result<usize, ParseError> {
154 debug_assert!(max <= 5 && min <= max && defaults.len() >= max);
155 let bytes = line.as_bytes();
156 let n = bytes.len();
157 let mut i = 0usize;
158 let mut found = 0usize;
159 while found < max {
160 while i < n && bytes[i].is_ascii_whitespace() {
161 i += 1;
162 }
163 if i >= n {
164 break;
165 }
166 let end = bytes[i..]
167 .iter()
168 .position(|b| b.is_ascii_whitespace())
169 .map(|k| i + k)
170 .unwrap_or(n);
171 let token = std::str::from_utf8(&bytes[i..end]).unwrap_or("");
172 if is_nonfinite_spelling(token) {
173 return Err(ParseError::NonFiniteValue(token.to_string()));
174 }
175 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..])
176 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
177 let next = i + consumed;
178 if next != end {
180 return Err(ParseError::InvalidNumberFormat(format!(
181 "invalid float: {token}"
182 )));
183 }
184 if !val.is_finite() {
185 return Err(ParseError::NonFiniteValue(token.to_string()));
186 }
187 out[found] = val;
188 found += 1;
189 i = next;
190 }
191 while i < n && bytes[i].is_ascii_whitespace() {
192 i += 1;
193 }
194 if i < n {
196 return Err(ParseError::InvalidVectorLength {
197 expected: max,
198 found: found + 1,
199 });
200 }
201 if found < min || found > max {
202 return Err(ParseError::InvalidVectorLength {
203 expected: max,
204 found,
205 });
206 }
207 while found < max {
208 out[found] = defaults[found];
209 found += 1;
210 }
211 Ok(found)
212}
213
214pub fn parse_line_of_n_f64(line: &str, n: usize) -> Result<Vec<f64>, ParseError> {
226 if n <= 5 {
227 let defaults = [0.0f64; 5];
228 let mut buf = [0.0f64; 5];
229 parse_line_of_range_f64_stack(line, n, n, &defaults, &mut buf)?;
230 return Ok(buf[..n].to_vec());
231 }
232 let mut values = Vec::with_capacity(n);
233 for token in line.split_ascii_whitespace() {
234 values.push(parse_finite_float_token(token)?);
235 }
236 if values.len() == n {
237 Ok(values)
238 } else {
239 Err(ParseError::InvalidVectorLength {
240 expected: n,
241 found: values.len(),
242 })
243 }
244}
245
246pub fn parse_line_of_range_f64(
253 line: &str,
254 min: usize,
255 max: usize,
256 defaults: &[f64],
257) -> Result<Vec<f64>, ParseError> {
258 if max <= 5 {
259 let mut buf = [0.0f64; 5];
260 parse_line_of_range_f64_stack(line, min, max, defaults, &mut buf)?;
261 return Ok(buf[..max].to_vec());
262 }
263 let mut values = Vec::with_capacity(max);
264 for token in line.split_ascii_whitespace() {
265 values.push(parse_finite_float_token(token)?);
266 }
267 if values.len() < min || values.len() > max {
268 return Err(ParseError::InvalidVectorLength {
269 expected: max,
270 found: values.len(),
271 });
272 }
273 while values.len() < max {
274 let idx = values.len();
275 values.push(defaults[idx]);
276 }
277 Ok(values)
278}
279
280fn is_nonfinite_spelling(token: &str) -> bool {
284 let rest = token
285 .strip_prefix('+')
286 .or_else(|| token.strip_prefix('-'))
287 .unwrap_or(token);
288 match rest.as_bytes().first() {
289 Some(b) if b.is_ascii_digit() || *b == b'.' => return false,
290 None => return false,
291 _ => {}
292 }
293 rest.eq_ignore_ascii_case("nan")
294 || rest.eq_ignore_ascii_case("inf")
295 || rest.eq_ignore_ascii_case("infinity")
296}
297
298fn parse_finite_float_token(token: &str) -> Result<f64, ParseError> {
299 if is_nonfinite_spelling(token) {
300 return Err(ParseError::NonFiniteValue(token.to_string()));
301 }
302 let (val, consumed) = fast_float2::parse_partial::<f64, _>(token.as_bytes())
303 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
304 if consumed != token.len() {
305 return Err(ParseError::InvalidNumberFormat(format!(
306 "invalid float: {token}"
307 )));
308 }
309 if !val.is_finite() {
310 return Err(ParseError::NonFiniteValue(token.to_string()));
311 }
312 Ok(val)
313}
314
315fn skip_ascii_ws(bytes: &[u8], mut i: usize) -> usize {
316 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
317 i += 1;
318 }
319 i
320}
321
322fn take_token(line: &str, i: usize) -> Option<(&str, usize)> {
323 let bytes = line.as_bytes();
324 if i >= bytes.len() {
325 return None;
326 }
327 let mut j = i;
328 while j < bytes.len() && !bytes[j].is_ascii_whitespace() {
329 j += 1;
330 }
331 Some((&line[i..j], j))
332}
333
334fn parse_fixed_flag(token: &str, spec_version: u32) -> Result<[bool; 3], ParseError> {
337 let bad = || {
338 ParseError::InvalidFixedFlag(format!(
339 "column 4 flag {token:?} is not a legal integer for spec {spec_version}"
340 ))
341 };
342 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
343 return Err(bad());
344 }
345 let val: u64 = token.parse::<u64>().map_err(|_| bad())?;
346 if spec_version < 2 {
347 return match val {
348 0 => Ok([false, false, false]),
349 1 => Ok([true, true, true]),
350 _ => Err(bad()),
351 };
352 }
353 if val > 7 {
354 return Err(bad());
355 }
356 let v = val as u8;
357 Ok([v & 1 != 0, v & 2 != 0, v & 4 != 0])
358}
359
360fn parse_atom_id_token(token: &str) -> Result<u64, ParseError> {
363 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
364 return Err(ParseError::InvalidNumberFormat(format!(
365 "invalid integer: {token}"
366 )));
367 }
368 token.parse::<u64>().map_err(|_| {
369 ParseError::InvalidNumberFormat(format!("integer out of range 0..=2^64-1: {token}"))
370 })
371}
372
373fn parse_coord_row(
377 line: &str,
378 spec_version: u32,
379 default_id: u64,
380) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
381 parse_vector_row(line, spec_version, Some(default_id))
382}
383
384fn parse_vector_section_row(
386 line: &str,
387 spec_version: u32,
388) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
389 parse_vector_row(line, spec_version, None)
390}
391
392fn parse_vector_row(
393 line: &str,
394 spec_version: u32,
395 default_id: Option<u64>,
396) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
397 let (row, end) = parse_coord_fields(line.as_bytes(), 0, spec_version, default_id)?;
398 if end != line.len() {
399 let found = 5 + count_tokens_on_line(line.as_bytes(), end);
400 return Err(ParseError::InvalidVectorLength { expected: 5, found });
401 }
402 Ok(row)
403}
404
405#[inline(always)]
407fn is_field_ws(b: u8) -> bool {
408 matches!(b, b' ' | b'\t' | b'\r' | 0x0c)
409}
410
411#[inline(always)]
412fn skip_field_ws(bytes: &[u8], mut i: usize) -> usize {
413 while i < bytes.len() && is_field_ws(bytes[i]) {
414 i += 1;
415 }
416 i
417}
418
419fn token_on_line(bytes: &[u8], i: usize) -> String {
420 let mut j = i;
421 while j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
422 j += 1;
423 }
424 String::from_utf8_lossy(&bytes[i..j]).into_owned()
425}
426
427fn count_tokens_on_line(bytes: &[u8], mut i: usize) -> usize {
428 let mut n = 0usize;
429 loop {
430 i = skip_field_ws(bytes, i);
431 if i >= bytes.len() || bytes[i] == b'\n' {
432 break;
433 }
434 n += 1;
435 while i < bytes.len() && !is_field_ws(bytes[i]) && bytes[i] != b'\n' {
436 i += 1;
437 }
438 }
439 n
440}
441
442fn utf8_or_number(bytes: &[u8], line_start: usize, i: usize) -> ParseError {
444 let end = bytes[line_start..]
445 .iter()
446 .position(|b| *b == b'\n')
447 .map(|p| line_start + p)
448 .unwrap_or(bytes.len());
449 if std::str::from_utf8(&bytes[line_start..end]).is_err() {
450 return ParseError::ValidationError("invalid UTF-8".into());
451 }
452 ParseError::InvalidNumberFormat(format!("invalid float: {}", token_on_line(bytes, i)))
453}
454
455#[inline(always)]
456fn parse_f64_at(bytes: &[u8], i: usize, line_start: usize) -> Result<(f64, usize), ParseError> {
457 if i >= bytes.len() || bytes[i] == b'\n' {
458 return Err(ParseError::InvalidVectorLength {
459 expected: 5,
460 found: 0,
461 });
462 }
463 if bytes[i] >= 0x80 {
464 return Err(utf8_or_number(bytes, line_start, i));
465 }
466 if looks_like_nonfinite(bytes, i) {
467 let token = token_on_line(bytes, i);
468 if is_nonfinite_spelling(&token) {
469 return Err(ParseError::NonFiniteValue(token));
470 }
471 }
472 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..]).map_err(|_| {
473 ParseError::InvalidNumberFormat(format!("invalid float: {}", token_on_line(bytes, i)))
474 })?;
475 let next = i + consumed;
476 let boundary = next >= bytes.len() || is_field_ws(bytes[next]) || bytes[next] == b'\n';
477 if !boundary || !val.is_finite() {
478 let token = token_on_line(bytes, i);
479 if is_nonfinite_spelling(&token) || !val.is_finite() {
480 return Err(ParseError::NonFiniteValue(token));
481 }
482 return Err(ParseError::InvalidNumberFormat(format!(
483 "invalid float: {token}"
484 )));
485 }
486 Ok((val, next))
487}
488
489#[inline(always)]
490fn parse_flag_at(
491 bytes: &[u8],
492 i: usize,
493 spec_version: u32,
494) -> Result<([bool; 3], usize), ParseError> {
495 if i >= bytes.len() || bytes[i] == b'\n' {
496 return Err(ParseError::InvalidVectorLength {
497 expected: 5,
498 found: 3,
499 });
500 }
501 let bad = || {
502 ParseError::InvalidFixedFlag(format!(
503 "column 4 flag {:?} is not a legal integer for spec {spec_version}",
504 token_on_line(bytes, i)
505 ))
506 };
507 if !bytes[i].is_ascii_digit() {
508 return Err(bad());
509 }
510 let mut j = i;
511 let mut val: u64 = 0;
512 while j < bytes.len() && bytes[j].is_ascii_digit() {
513 let d = u64::from(bytes[j] - b'0');
514 val = val
515 .checked_mul(10)
516 .and_then(|v| v.checked_add(d))
517 .ok_or_else(bad)?;
518 j += 1;
519 }
520 if j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
521 return Err(bad());
522 }
523 let fixed = if spec_version < 2 {
524 match val {
525 0 => [false, false, false],
526 1 => [true, true, true],
527 _ => return Err(bad()),
528 }
529 } else if val <= 7 {
530 let v = val as u8;
531 [v & 1 != 0, v & 2 != 0, v & 4 != 0]
532 } else {
533 return Err(bad());
534 };
535 Ok((fixed, j))
536}
537
538#[inline(always)]
539fn parse_id_at(bytes: &[u8], i: usize) -> Result<(u64, usize), ParseError> {
540 if i >= bytes.len() || bytes[i] == b'\n' || !bytes[i].is_ascii_digit() {
541 return Err(ParseError::InvalidNumberFormat(format!(
542 "invalid integer: {}",
543 token_on_line(bytes, i)
544 )));
545 }
546 let mut j = i;
547 let mut val: u64 = 0;
548 while j < bytes.len() && bytes[j].is_ascii_digit() {
549 let d = u64::from(bytes[j] - b'0');
550 val = val
551 .checked_mul(10)
552 .and_then(|v| v.checked_add(d))
553 .ok_or_else(|| {
554 ParseError::InvalidNumberFormat(format!(
555 "integer out of range 0..=2^64-1: {}",
556 token_on_line(bytes, i)
557 ))
558 })?;
559 j += 1;
560 }
561 if j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
562 return Err(ParseError::InvalidNumberFormat(format!(
563 "invalid integer: {}",
564 token_on_line(bytes, i)
565 )));
566 }
567 Ok((val, j))
568}
569
570type ParsedCoord = ([f64; 3], [bool; 3], u64);
572
573#[inline(always)]
580fn parse_coord_fields(
581 bytes: &[u8],
582 i: usize,
583 spec_version: u32,
584 default_id: Option<u64>,
585) -> Result<(ParsedCoord, usize), ParseError> {
586 if let Some(parsed) = parse_coord_fields_fast(bytes, i, spec_version, default_id) {
587 return Ok(parsed);
588 }
589 parse_coord_fields_detailed(bytes, i, spec_version, default_id)
590}
591
592#[inline(always)]
594fn parse_coord_fields_fast(
595 bytes: &[u8],
596 i: usize,
597 spec_version: u32,
598 default_id: Option<u64>,
599) -> Option<(ParsedCoord, usize)> {
600 let mut i = skip_field_ws(bytes, i);
601 let mut xyz = [0.0f64; 3];
602 for dest in &mut xyz {
603 if i >= bytes.len() || bytes[i] == b'\n' || bytes[i] >= 0x80 {
604 return None;
605 }
606 let (val, next) = crate::fast_dec::parse_f64_hot(bytes, i)?;
607 if next < bytes.len() && !is_field_ws(bytes[next]) && bytes[next] != b'\n' {
608 return None;
609 }
610 *dest = val;
611 i = skip_field_ws(bytes, next);
612 }
613 let (fixed, next) = parse_flag_fast(bytes, i, spec_version)?;
614 i = skip_field_ws(bytes, next);
615 let (atom_id, next) = if i >= bytes.len() || bytes[i] == b'\n' {
616 (default_id?, i)
617 } else {
618 parse_u64_fast(bytes, i)?
619 };
620 i = skip_field_ws(bytes, next);
621 if i < bytes.len() && bytes[i] != b'\n' {
622 return None;
623 }
624 if i < bytes.len() && bytes[i] == b'\n' {
625 i += 1;
626 }
627 Some(((xyz, fixed, atom_id), i))
628}
629
630#[inline(always)]
631fn parse_u64_fast(bytes: &[u8], i: usize) -> Option<(u64, usize)> {
632 if i >= bytes.len() || !bytes[i].is_ascii_digit() {
633 return None;
634 }
635 let mut j = i;
636 let mut val: u64 = 0;
637 let mut digits = 0u32;
638 while j < bytes.len() && bytes[j].is_ascii_digit() {
639 digits += 1;
640 if digits > 18 {
642 return None;
643 }
644 val = val * 10 + u64::from(bytes[j] - b'0');
645 j += 1;
646 }
647 if j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
648 return None;
649 }
650 Some((val, j))
651}
652
653#[inline(always)]
654fn parse_flag_fast(bytes: &[u8], i: usize, spec_version: u32) -> Option<([bool; 3], usize)> {
655 let (val, j) = parse_u64_fast(bytes, i)?;
656 let fixed = if spec_version < 2 {
657 match val {
658 0 => [false, false, false],
659 1 => [true, true, true],
660 _ => return None,
661 }
662 } else if val <= 7 {
663 let v = val as u8;
664 [v & 1 != 0, v & 2 != 0, v & 4 != 0]
665 } else {
666 return None;
667 };
668 Some((fixed, j))
669}
670
671#[cold]
672#[inline(never)]
673fn parse_coord_fields_detailed(
674 bytes: &[u8],
675 i: usize,
676 spec_version: u32,
677 default_id: Option<u64>,
678) -> Result<(ParsedCoord, usize), ParseError> {
679 let line_start = i;
680 let mut i = skip_field_ws(bytes, i);
681 if i >= bytes.len() || bytes[i] == b'\n' {
682 return Err(ParseError::InvalidVectorLength {
683 expected: 5,
684 found: 0,
685 });
686 }
687 let mut xyz = [0.0f64; 3];
688 for (slot, dest) in xyz.iter_mut().enumerate() {
689 if i >= bytes.len() || bytes[i] == b'\n' {
690 return Err(ParseError::InvalidVectorLength {
691 expected: 5,
692 found: slot,
693 });
694 }
695 if bytes[i] >= 0x80 {
696 return Err(utf8_or_number(bytes, line_start, i));
697 }
698 let (val, next) = parse_f64_at(bytes, i, line_start)?;
699 *dest = val;
700 i = skip_field_ws(bytes, next);
701 }
702 let (fixed, next) = parse_flag_at(bytes, i, spec_version)?;
703 i = skip_field_ws(bytes, next);
704 let (atom_id, next) = if i >= bytes.len() || bytes[i] == b'\n' {
705 (
706 default_id.ok_or(ParseError::InvalidVectorLength {
707 expected: 5,
708 found: 4,
709 })?,
710 i,
711 )
712 } else {
713 parse_id_at(bytes, i)?
714 };
715 i = skip_field_ws(bytes, next);
716 if i < bytes.len() && bytes[i] != b'\n' {
717 let found = 5 + count_tokens_on_line(bytes, i);
718 return Err(ParseError::InvalidVectorLength { expected: 5, found });
719 }
720 if i < bytes.len() && bytes[i] == b'\n' {
721 i += 1;
722 }
723 Ok(((xyz, fixed, atom_id), i))
724}
725
726pub(crate) fn parse_coord_row_at(
731 bytes: &[u8],
732 i: usize,
733 spec_version: u32,
734 default_id: u64,
735) -> Result<(ParsedCoord, usize), ParseError> {
736 if i >= bytes.len() {
737 return Err(ParseError::IncompleteFrame);
738 }
739 parse_coord_fields(bytes, i, spec_version, Some(default_id))
740}
741
742pub(crate) fn parse_vector_row_at(
744 bytes: &[u8],
745 i: usize,
746 spec_version: u32,
747) -> Result<(ParsedCoord, usize), ParseError> {
748 if i >= bytes.len() {
749 return Err(ParseError::IncompleteFrame);
750 }
751 parse_coord_fields(bytes, i, spec_version, None)
752}
753
754fn parse_scalar_section_row(
756 line: &str,
757 spec_version: u32,
758) -> Result<(f64, [bool; 3], u64), ParseError> {
759 let bytes = line.as_bytes();
760 let i = skip_ascii_ws(bytes, 0);
761 let (value, i) = parse_finite_at(line, i)?;
762 let i = skip_ascii_ws(bytes, i);
763 let (flag_tok, i) = take_token(line, i).ok_or(ParseError::InvalidVectorLength {
764 expected: 3,
765 found: 1,
766 })?;
767 let fixed = parse_fixed_flag(flag_tok, spec_version)?;
768 let i = skip_ascii_ws(bytes, i);
769 let (id_tok, i) = take_token(line, i).ok_or(ParseError::InvalidVectorLength {
770 expected: 3,
771 found: 2,
772 })?;
773 let atom_id = parse_atom_id_token(id_tok)?;
774 let i = skip_ascii_ws(bytes, i);
775 if i < bytes.len() {
776 let found = 3 + line[i..].split_ascii_whitespace().count();
777 return Err(ParseError::InvalidVectorLength { expected: 3, found });
778 }
779 Ok((value, fixed, atom_id))
780}
781
782fn parse_finite_at(line: &str, i: usize) -> Result<(f64, usize), ParseError> {
783 let bytes = line.as_bytes();
784 if i >= bytes.len() {
785 return Err(ParseError::InvalidVectorLength {
786 expected: 3,
787 found: 0,
788 });
789 }
790 if looks_like_nonfinite(bytes, i) {
793 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
794 if is_nonfinite_spelling(token) {
795 return Err(ParseError::NonFiniteValue(token.to_string()));
796 }
797 }
798 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..]).map_err(|_| {
799 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
800 ParseError::InvalidNumberFormat(format!("invalid float: {token}"))
801 })?;
802 let next = i + consumed;
803 let boundary = next == bytes.len() || bytes[next].is_ascii_whitespace();
804 if !boundary || !val.is_finite() {
805 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
806 if is_nonfinite_spelling(token) || !val.is_finite() {
807 return Err(ParseError::NonFiniteValue(token.to_string()));
808 }
809 return Err(ParseError::InvalidNumberFormat(format!(
810 "invalid float: {token}"
811 )));
812 }
813 Ok((val, next))
814}
815
816fn looks_like_nonfinite(bytes: &[u8], i: usize) -> bool {
819 let mut j = i;
820 if bytes[j] == b'+' || bytes[j] == b'-' {
821 j += 1;
822 if j >= bytes.len() {
823 return false;
824 }
825 }
826 let b = bytes[j];
827 !b.is_ascii_digit() && b != b'.'
828}
829
830fn labels_equal(found: &str, expected: &str) -> bool {
832 let mut found_words = found.split_ascii_whitespace();
833 let mut expected_words = expected.split_ascii_whitespace();
834 loop {
835 match (found_words.next(), expected_words.next()) {
836 (Some(a), Some(b)) => {
837 if !a.eq_ignore_ascii_case(b) {
838 return false;
839 }
840 }
841 (None, None) => return true,
842 _ => return false,
843 }
844 }
845}
846
847fn label_has_prefix(line: &str, prefix: &str) -> bool {
849 let mut found_words = line.split_ascii_whitespace();
850 for word in prefix.split_ascii_whitespace() {
851 match found_words.next() {
852 Some(got) if got.eq_ignore_ascii_case(word) => {}
853 _ => return false,
854 }
855 }
856 true
857}
858
859fn token_count(line: &str) -> usize {
860 line.split_ascii_whitespace().count()
861}
862
863fn json_u64(value: &Value) -> Option<u64> {
864 value.as_u64()
865}
866
867fn validate_section_arity_value(value: &Value) -> Result<(), ParseError> {
868 let obj = value.as_object().ok_or_else(|| {
869 metadata_json_error("section_arity must be an object mapping section names to integers")
870 })?;
871 for (name, entry) in obj {
872 let n = json_u64(entry).ok_or_else(|| {
873 metadata_json_error(format!("section_arity.{name} must be an integer >= 1"))
874 })?;
875 if n < 1 {
876 return Err(ParseError::InvalidCount(format!(
877 "section_arity.{name} must be >= 1"
878 )));
879 }
880 if let Some(expect) = crate::types::reserved_section_value_arity(name)
882 && n != expect
883 {
884 return Err(ParseError::InvalidVectorLength {
885 expected: usize::try_from(expect).unwrap_or(usize::MAX),
886 found: usize::try_from(n).unwrap_or(usize::MAX),
887 });
888 }
889 }
890 Ok(())
891}
892
893pub(crate) fn parse_count_line(line: &str, n: usize) -> Result<Vec<usize>, ParseError> {
897 let mut values = Vec::with_capacity(n);
898 for token in line.split_ascii_whitespace() {
899 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
900 return Err(ParseError::InvalidCount(format!(
901 "count field is not an unsigned integer: {token}"
902 )));
903 }
904 let val: u64 = token.parse::<u64>().map_err(|_| {
905 ParseError::InvalidCount(format!("count field is out of range: {token}"))
906 })?;
907 let val = usize::try_from(val).map_err(|_| {
908 ParseError::InvalidCount(format!("count field is out of range: {token}"))
909 })?;
910 values.push(val);
911 }
912 if values.len() == n {
913 Ok(values)
914 } else {
915 Err(ParseError::InvalidVectorLength {
916 expected: n,
917 found: values.len(),
918 })
919 }
920}
921
922pub(crate) fn line_is_whitespace(line: &str) -> bool {
924 line.split_ascii_whitespace().next().is_none()
925}
926
927pub(crate) fn read_zero_atom_tail<'a>(
933 mut next_line: impl FnMut() -> Option<&'a str>,
934) -> Result<(), ParseError> {
935 let bad = || {
936 ParseError::InvalidCount("a zero-atom frame requires whitespace-only lines 8 and 9".into())
937 };
938 match next_line() {
939 None => Ok(()),
940 Some(line) if line_is_whitespace(line) => match next_line() {
941 None => Ok(()),
942 Some(line) if line_is_whitespace(line) => Ok(()),
943 Some(_) => Err(bad()),
944 },
945 Some(_) => Err(bad()),
946 }
947}
948
949fn metadata_json_error(message: impl Into<String>) -> ParseError {
950 ParseError::InvalidMetadataJson(message.into())
951}
952
953fn validate_metadata_number(key: &str, value: &Value) -> Result<(), ParseError> {
954 if value.as_f64().is_some() {
955 Ok(())
956 } else {
957 Err(metadata_json_error(format!(
958 "{key} must be a finite number"
959 )))
960 }
961}
962
963fn validate_metadata_integer(key: &str, value: &Value) -> Result<(), ParseError> {
964 if value.as_u64().is_some() {
965 Ok(())
966 } else {
967 Err(metadata_json_error(format!(
968 "{key} must be a non-negative integer"
969 )))
970 }
971}
972
973pub fn validate_metadata_schema(
981 json_obj: &serde_json::Map<String, Value>,
982) -> Result<(), ParseError> {
983 let strict_requested = match json_obj.get(meta::VALIDATE) {
984 Some(Value::Bool(b)) => *b,
985 Some(_) => return Err(metadata_json_error("validate must be a boolean")),
986 None => false,
987 };
988
989 match json_obj.get(meta::SECTIONS) {
990 Some(Value::Array(values)) => {
991 if values.iter().any(|entry| !entry.is_string()) {
992 return Err(metadata_json_error("sections must be an array of strings"));
993 }
994 }
995 Some(_) => return Err(metadata_json_error("sections must be an array of strings")),
996 None if strict_requested => {
997 return Err(metadata_json_error(
998 "validate=true requires a sections array, even when empty",
999 ));
1000 }
1001 None => {}
1002 }
1003
1004 for (key, value) in json_obj {
1005 match key.as_str() {
1006 meta::CON_SPEC_VERSION | meta::SECTIONS | meta::VALIDATE => {}
1007 meta::CON_SPEC_MINOR => {
1008 if json_u64(value).is_none() {
1009 return Err(metadata_json_error(
1010 "con_spec_minor must be a JSON integer >= 0",
1011 ));
1012 }
1013 }
1014 meta::SECTION_ARITY => validate_section_arity_value(value)?,
1015 meta::ENERGY
1016 | meta::TIME
1017 | meta::TIMESTEP
1018 | meta::CONVERGENCE_FMAX
1019 | meta::CONVERGENCE_ENERGY
1020 | meta::FMAX => validate_metadata_number(key, value)?,
1021 meta::FRAME_INDEX | meta::NEB_BEAD | meta::NEB_BAND => {
1022 validate_metadata_integer(key, value)?
1023 }
1024 meta::GENERATOR if !value.is_string() => {
1025 return Err(metadata_json_error("generator must be a string"));
1026 }
1027 meta::GENERATOR => {}
1028 meta::UNITS | meta::POTENTIAL if !value.is_object() => {
1029 return Err(metadata_json_error(format!("{key} must be an object")));
1030 }
1031 meta::POTENTIAL => {
1032 if let Some(potential_type) = value.get("type")
1033 && !potential_type.is_string()
1034 {
1035 return Err(metadata_json_error("potential.type must be a string"));
1036 }
1037 }
1038 meta::UNITS => {
1039 }
1041 meta::PBC => validate_pbc_metadata(value)?,
1042 meta::BONDS => validate_bonds_metadata(value)?,
1043 meta::LATTICE_VECTORS => validate_lattice_vectors_metadata(value)?,
1044 meta::CONVERGED if !value.is_boolean() => {
1045 return Err(metadata_json_error("converged must be a boolean"));
1046 }
1047 meta::CONVERGED => {}
1048 _ => {}
1049 }
1050 }
1051
1052 Ok(())
1053}
1054
1055fn validate_pbc_metadata(value: &Value) -> Result<(), ParseError> {
1056 let Some(values) = value.as_array() else {
1057 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
1058 };
1059 if values.len() != 3 || values.iter().any(|entry| !entry.is_boolean()) {
1060 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
1061 }
1062 Ok(())
1063}
1064
1065fn validate_bonds_metadata(value: &Value) -> Result<(), ParseError> {
1072 let Some(items) = value.as_array() else {
1073 return Err(metadata_json_error("bonds must be an array"));
1074 };
1075 for (idx, item) in items.iter().enumerate() {
1076 if let Some(pair) = item.as_array() {
1077 if pair.len() != 2 {
1078 return Err(metadata_json_error(format!(
1079 "bonds[{idx}] pair must have exactly two indices"
1080 )));
1081 }
1082 for (k, entry) in pair.iter().enumerate() {
1083 let Some(n) = entry.as_u64() else {
1084 return Err(metadata_json_error(format!(
1085 "bonds[{idx}][{k}] must be a non-negative integer"
1086 )));
1087 };
1088 if n > u32::MAX as u64 {
1089 return Err(metadata_json_error(format!(
1090 "bonds[{idx}][{k}] index exceeds u32"
1091 )));
1092 }
1093 }
1094 continue;
1095 }
1096 if let Some(obj) = item.as_object() {
1097 for key in ["i", "j"] {
1098 let Some(entry) = obj.get(key) else {
1099 return Err(metadata_json_error(format!(
1100 "bonds[{idx}] object must include \"{key}\""
1101 )));
1102 };
1103 let Some(n) = entry.as_u64() else {
1104 return Err(metadata_json_error(format!(
1105 "bonds[{idx}].{key} must be a non-negative integer"
1106 )));
1107 };
1108 if n > u32::MAX as u64 {
1109 return Err(metadata_json_error(format!(
1110 "bonds[{idx}].{key} index exceeds u32"
1111 )));
1112 }
1113 }
1114 if let Some(order) = obj.get("order")
1115 && order.as_i64().is_none()
1116 {
1117 return Err(metadata_json_error(format!(
1118 "bonds[{idx}].order must be an integer when present"
1119 )));
1120 }
1121 continue;
1122 }
1123 return Err(metadata_json_error(format!(
1124 "bonds[{idx}] must be [i, j] or {{\"i\": i, \"j\": j, \"order\"?: ...}}"
1125 )));
1126 }
1127 Ok(())
1128}
1129
1130fn validate_lattice_vectors_metadata(value: &Value) -> Result<(), ParseError> {
1131 let Some(rows) = value.as_array() else {
1132 return Err(metadata_json_error(
1133 "lattice_vectors must be a 3x3 numeric array",
1134 ));
1135 };
1136 if rows.len() != 3 {
1137 return Err(metadata_json_error(
1138 "lattice_vectors must be a 3x3 numeric array",
1139 ));
1140 }
1141 for row in rows {
1142 let Some(entries) = row.as_array() else {
1143 return Err(metadata_json_error(
1144 "lattice_vectors must be a 3x3 numeric array",
1145 ));
1146 };
1147 if entries.len() != 3 || entries.iter().any(|entry| entry.as_f64().is_none()) {
1148 return Err(metadata_json_error(
1149 "lattice_vectors must be a 3x3 numeric array",
1150 ));
1151 }
1152 }
1153 Ok(())
1154}
1155
1156pub fn parse_line_of_n<T: std::str::FromStr>(line: &str, n: usize) -> Result<Vec<T>, ParseError>
1184where
1185 ParseError: From<<T as std::str::FromStr>::Err>,
1186{
1187 let values: Vec<T> = line
1188 .split_whitespace()
1189 .map(|s| s.parse::<T>())
1190 .collect::<Result<_, _>>()?;
1191
1192 if values.len() == n {
1193 Ok(values)
1194 } else {
1195 Err(ParseError::InvalidVectorLength {
1196 expected: n,
1197 found: values.len(),
1198 })
1199 }
1200}
1201
1202pub fn parse_frame_header<'a>(
1223 lines: &mut impl Iterator<Item = &'a str>,
1224) -> Result<FrameHeader, ParseError> {
1225 let prebox1 = lines
1226 .next()
1227 .ok_or(ParseError::IncompleteHeader)?
1228 .to_string();
1229 if prebox1.starts_with('\u{feff}') {
1231 return Err(ParseError::ValidationError(
1232 "file begins with a byte-order mark".into(),
1233 ));
1234 }
1235 let prebox2_raw = lines.next().ok_or(ParseError::IncompleteHeader)?;
1236
1237 let trimmed = prebox2_raw.trim();
1242 let (spec_version, metadata, sections, validate, sections_declared) =
1243 if trimmed == "{\"con_spec_version\":2}" {
1244 (2, BTreeMap::new(), Vec::new(), false, false)
1247 } else if trimmed.starts_with('{') {
1248 let json_val: serde_json::Value = serde_json::from_str(trimmed)
1249 .map_err(|e| ParseError::InvalidMetadataJson(e.to_string()))?;
1250 let json_obj = json_val.as_object().ok_or_else(|| {
1251 ParseError::InvalidMetadataJson("expected a JSON object".to_string())
1252 })?;
1253 let ver_value = json_obj
1254 .get(meta::CON_SPEC_VERSION)
1255 .ok_or(ParseError::MissingSpecVersion)?;
1256 let ver = json_u64(ver_value).ok_or_else(|| {
1257 ParseError::InvalidMetadataJson(
1258 "con_spec_version must be a JSON integer, the major version".into(),
1259 )
1260 })?;
1261 if ver > u64::from(u32::MAX) {
1262 return Err(ParseError::UnsupportedVersion(u32::MAX));
1263 }
1264 let ver = ver as u32;
1265 if ver == 0 || ver == 1 || ver > crate::CON_SPEC_VERSION {
1269 return Err(ParseError::UnsupportedVersion(ver));
1270 }
1271 if ver >= 3 {
1272 match json_obj.get(meta::UNITS) {
1273 Some(u) => crate::units::validate_v3_units_metadata(u)
1274 .map_err(|e| ParseError::InvalidUnits(format!("v3 units: {e}")))?,
1275 None => {
1276 return Err(ParseError::InvalidUnits(
1277 "con_spec_version >= 3 requires metadata \"units\" with length and energy"
1278 .into(),
1279 ));
1280 }
1281 }
1282 }
1283
1284 let mut sections: Vec<String> = Vec::new();
1288 let mut metadata = BTreeMap::new();
1289 let mut sections_declared = false;
1290 let mut validate = false;
1291 for (k, v) in json_obj {
1292 match k.as_str() {
1293 meta::CON_SPEC_VERSION => {}
1294 meta::CON_SPEC_MINOR => {
1295 if json_u64(v).is_none() {
1296 return Err(metadata_json_error(
1297 "con_spec_minor must be a JSON integer >= 0",
1298 ));
1299 }
1300 metadata.insert(k.clone(), v.clone());
1301 }
1302 meta::SECTION_ARITY => {
1303 validate_section_arity_value(v)?;
1304 metadata.insert(k.clone(), v.clone());
1305 }
1306 meta::SECTIONS => {
1307 sections_declared = true;
1308 let arr = v.as_array().ok_or_else(|| {
1309 metadata_json_error("sections must be an array of strings")
1310 })?;
1311 sections.reserve(arr.len());
1312 for entry in arr {
1313 let s = entry.as_str().ok_or_else(|| {
1314 metadata_json_error("sections must be an array of strings")
1315 })?;
1316 sections.push(s.to_string());
1317 }
1318 }
1319 meta::VALIDATE => {
1320 validate = match v {
1321 Value::Bool(b) => *b,
1322 _ => return Err(metadata_json_error("validate must be a boolean")),
1323 };
1324 metadata.insert(k.clone(), v.clone());
1325 }
1326 _ => {
1327 metadata.insert(k.clone(), v.clone());
1328 }
1329 }
1330 }
1331
1332 if validate {
1335 validate_metadata_schema(json_obj)?;
1336 }
1337
1338 (ver, metadata, sections, validate, sections_declared)
1339 } else {
1340 (1_u32, BTreeMap::new(), Vec::new(), false, false)
1342 };
1343 let prebox2 = prebox2_raw.to_string();
1344
1345 let boxl_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
1346 let angles_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
1347 let postbox1 = lines
1348 .next()
1349 .ok_or(ParseError::IncompleteHeader)?
1350 .to_string();
1351 let postbox2 = lines
1352 .next()
1353 .ok_or(ParseError::IncompleteHeader)?
1354 .to_string();
1355 let natm_types_line = lines.next().ok_or(ParseError::IncompleteHeader)?;
1356 let natm_types = parse_count_line(natm_types_line, 1)?[0];
1357 let (natms_per_type, masses_per_type) = if natm_types == 0 {
1360 read_zero_atom_tail(|| lines.next())?;
1361 (Vec::new(), Vec::new())
1362 } else {
1363 let natms_per_type = parse_count_line(
1364 lines.next().ok_or(ParseError::IncompleteHeader)?,
1365 natm_types,
1366 )?;
1367 if natms_per_type.contains(&0) {
1368 return Err(ParseError::InvalidCount(
1369 "each per-type count must be >= 1".into(),
1370 ));
1371 }
1372 let masses_per_type = parse_line_of_n_f64(
1373 lines.next().ok_or(ParseError::IncompleteHeader)?,
1374 natm_types,
1375 )?;
1376 (natms_per_type, masses_per_type)
1377 };
1378 if validate {
1379 validate_header_geometry(&boxl_vec, &angles_vec)?;
1380 validate_masses(&masses_per_type)?;
1381 }
1382 Ok(FrameHeader {
1383 prebox_header: PreboxHeader {
1384 user: prebox1,
1385 metadata_line: prebox2,
1386 },
1387 boxl: boxl_vec.try_into().unwrap(),
1388 angles: angles_vec.try_into().unwrap(),
1389 postbox_header: [postbox1, postbox2],
1390 natm_types,
1391 natms_per_type,
1392 masses_per_type,
1393 spec_version,
1394 metadata,
1395 sections,
1396 strict_validation: validate,
1397 sections_declared,
1398 })
1399}
1400
1401pub fn parse_single_frame<'a>(
1450 lines: &mut impl Iterator<Item = &'a str>,
1451) -> Result<ConFrame, ParseError> {
1452 parse_single_frame_ls(&mut IterStream(lines))
1456}
1457
1458struct IterStream<I>(I);
1461
1462impl<'a, I> Iterator for IterStream<I>
1463where
1464 I: Iterator<Item = &'a str>,
1465{
1466 type Item = &'a str;
1467 fn next(&mut self) -> Option<&'a str> {
1468 self.0.next()
1469 }
1470}
1471
1472impl<'a, I> LineStream<'a> for IterStream<I>
1473where
1474 I: Iterator<Item = &'a str>,
1475{
1476 fn next_line(&mut self) -> Option<&'a str> {
1477 Iterator::next(self)
1478 }
1479 fn peek_line(&mut self) -> Option<&'a str> {
1480 None
1481 }
1482}
1483
1484pub(crate) fn parse_single_frame_ls<'a, L>(lines: &mut L) -> Result<ConFrame, ParseError>
1491where
1492 L: LineStream<'a> + Iterator<Item = &'a str>,
1493{
1494 let header = parse_frame_header(lines)?;
1495 let validate = header.strict_validation;
1496 let total_atoms: usize = header.natms_per_type.iter().sum();
1497 use crate::storage_dtype::{ElementKind, FloatArray2, StorageDtypes};
1498 let dt = StorageDtypes::from_metadata(&header.metadata).unwrap_or_default();
1499 let f64_positions = dt.positions == ElementKind::Float64;
1500 let mut pos_flat = Vec::with_capacity(total_atoms.saturating_mul(3));
1501 let mut positions_other = if f64_positions {
1502 None
1503 } else {
1504 Some(FloatArray2::zeros(dt.positions, total_atoms, 3))
1505 };
1506 let mut ids = Vec::with_capacity(total_atoms);
1507 let mut atom_data: Vec<AtomDatum> = Vec::with_capacity(total_atoms);
1508
1509 let mut global_atom_idx: u64 = 0;
1510 for (type_idx, num_atoms) in header.natms_per_type.iter().enumerate() {
1511 let symbol_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
1512 let symbol: Arc<str> = Arc::from(symbol_line.trim());
1513 let coord_label = lines.next().ok_or(ParseError::IncompleteFrame)?;
1514 if validate {
1515 validate_coordinate_component(type_idx, symbol.as_ref(), coord_label)?;
1516 }
1517 global_atom_idx = ingest_coord_rows(
1518 lines,
1519 *num_atoms,
1520 header.spec_version,
1521 global_atom_idx,
1522 &symbol,
1523 CoordSink {
1524 pos_flat: &mut pos_flat,
1525 ids: &mut ids,
1526 positions_other: &mut positions_other,
1527 atom_data: &mut atom_data,
1528 },
1529 )?;
1530 }
1531 let positions = if f64_positions {
1532 FloatArray2::from_f64_row_major(total_atoms, 3, pos_flat)
1533 } else {
1534 positions_other.expect("non-f64 positions allocated")
1535 };
1536 Ok(crate::types::con_frame_coords_only_with_ids(
1537 header, atom_data, positions, ids,
1538 ))
1539}
1540
1541struct CoordSink<'a> {
1542 pos_flat: &'a mut Vec<f64>,
1543 ids: &'a mut Vec<u64>,
1544 positions_other: &'a mut Option<crate::storage_dtype::FloatArray2>,
1545 atom_data: &'a mut Vec<AtomDatum>,
1546}
1547
1548fn ingest_coord_rows<'a, L>(
1550 lines: &mut L,
1551 n: usize,
1552 spec_version: u32,
1553 global_atom_idx: u64,
1554 symbol: &Arc<str>,
1555 sink: CoordSink<'_>,
1556) -> Result<u64, ParseError>
1557where
1558 L: LineStream<'a>,
1559{
1560 let CoordSink {
1561 pos_flat,
1562 ids,
1563 positions_other,
1564 atom_data,
1565 } = sink;
1566 if positions_other.is_none() {
1567 return ingest_coord_rows_f64(
1568 lines,
1569 n,
1570 spec_version,
1571 global_atom_idx,
1572 symbol,
1573 pos_flat,
1574 ids,
1575 atom_data,
1576 );
1577 }
1578 let mut global_atom_idx = global_atom_idx;
1579 for _ in 0..n {
1580 let (xyz, fixed, atom_id) = lines.next_coord_row(spec_version, global_atom_idx)?;
1581 if let Some(pos) = positions_other.as_mut() {
1582 pos.set_f64_row(ids.len(), xyz);
1583 }
1584 pos_flat.extend_from_slice(&xyz);
1585 ids.push(atom_id);
1586 atom_data.push(AtomDatum::coords(
1587 Arc::clone(symbol),
1588 xyz[0],
1589 xyz[1],
1590 xyz[2],
1591 fixed,
1592 atom_id,
1593 ));
1594 global_atom_idx += 1;
1595 }
1596 Ok(global_atom_idx)
1597}
1598
1599#[inline(never)]
1604#[allow(clippy::too_many_arguments)]
1605fn ingest_coord_rows_f64<'a, L>(
1606 lines: &mut L,
1607 n: usize,
1608 spec_version: u32,
1609 mut global_atom_idx: u64,
1610 symbol: &Arc<str>,
1611 pos_flat: &mut Vec<f64>,
1612 ids: &mut Vec<u64>,
1613 atom_data: &mut Vec<AtomDatum>,
1614) -> Result<u64, ParseError>
1615where
1616 L: LineStream<'a>,
1617{
1618 let mut shares = symbol_shares(symbol, n);
1619 for _ in 0..n {
1620 let (xyz, fixed, atom_id) = lines.next_coord_row(spec_version, global_atom_idx)?;
1621 let sym = shares.take();
1622 pos_flat.extend_from_slice(&xyz);
1623 ids.push(atom_id);
1624 atom_data.push(AtomDatum::coords(
1625 sym, xyz[0], xyz[1], xyz[2], fixed, atom_id,
1626 ));
1627 global_atom_idx += 1;
1628 }
1629 Ok(global_atom_idx)
1630}
1631
1632struct SymbolShares {
1638 data: *const u8,
1639 len: usize,
1640 left: usize,
1641}
1642
1643impl SymbolShares {
1644 fn raw(&self) -> *const str {
1645 std::ptr::slice_from_raw_parts(self.data, self.len) as *const str
1646 }
1647
1648 fn take(&mut self) -> Arc<str> {
1649 debug_assert!(self.left > 0);
1650 self.left -= 1;
1651 unsafe { Arc::from_raw(self.raw()) }
1652 }
1653}
1654
1655impl Drop for SymbolShares {
1656 fn drop(&mut self) {
1657 for _ in 0..self.left {
1658 drop(unsafe { Arc::from_raw(self.raw()) });
1659 }
1660 self.left = 0;
1661 }
1662}
1663
1664fn symbol_shares(symbol: &Arc<str>, n: usize) -> SymbolShares {
1665 if n == 0 {
1666 return SymbolShares {
1667 data: std::ptr::null(),
1668 len: 0,
1669 left: 0,
1670 };
1671 }
1672 let data = Arc::as_ptr(symbol);
1673 let offset = arc_str_header(symbol.as_ref());
1674 let bytes = data as *const u8;
1678 let strong = unsafe { bytes.sub(offset) as *const AtomicUsize };
1679 unsafe { (*strong).fetch_add(n, Ordering::Relaxed) };
1680 SymbolShares {
1681 data: bytes,
1682 len: symbol.len(),
1683 left: n,
1684 }
1685}
1686
1687fn arc_str_header(text: &str) -> usize {
1688 let header = 2 * std::mem::size_of::<usize>();
1689 let align = std::mem::align_of_val(text);
1690 let rem = header % align;
1691 if rem == 0 {
1692 header
1693 } else {
1694 header + (align - rem)
1695 }
1696}
1697
1698#[cfg(test)]
1699mod symbol_share_tests {
1700 use super::symbol_shares;
1701 use std::sync::Arc;
1702
1703 #[test]
1704 fn one_bump_matches_n_clones() {
1705 for text in ["", "Cu", "Gold", "a-much-longer-component-symbol"] {
1706 let symbol: Arc<str> = Arc::from(text);
1707 let mut shares = symbol_shares(&symbol, 3);
1708 assert_eq!(Arc::strong_count(&symbol), 4);
1709 let owned = [shares.take(), shares.take(), shares.take()];
1710 assert!(owned.iter().all(|arc| arc.as_ref() == text));
1711 drop(owned);
1712 assert_eq!(Arc::strong_count(&symbol), 1);
1713 let mut partial = symbol_shares(&symbol, 2);
1714 let one = partial.take();
1715 drop(partial);
1716 drop(one);
1717 assert_eq!(Arc::strong_count(&symbol), 1);
1718 }
1719 }
1720}
1721
1722fn validate_header_geometry(boxl: &[f64], angles: &[f64]) -> Result<(), ParseError> {
1723 if boxl
1724 .iter()
1725 .any(|length| !length.is_finite() || *length <= 0.0)
1726 || angles
1727 .iter()
1728 .any(|angle| !angle.is_finite() || *angle <= 0.0 || *angle >= 180.0)
1729 {
1730 return Err(ParseError::ValidationError(
1731 "cell geometry must have positive lengths and angles between 0 and 180 degrees"
1732 .to_string(),
1733 ));
1734 }
1735 Ok(())
1736}
1737
1738fn validate_masses(masses_per_type: &[f64]) -> Result<(), ParseError> {
1739 if masses_per_type
1740 .iter()
1741 .any(|mass| !mass.is_finite() || *mass <= 0.0)
1742 {
1743 return Err(ParseError::ValidationError(
1744 "component masses must be positive".to_string(),
1745 ));
1746 }
1747 Ok(())
1748}
1749
1750fn validate_coordinate_component(
1751 type_idx: usize,
1752 symbol: &str,
1753 label: &str,
1754) -> Result<(), ParseError> {
1755 let expected_label = format!("Coordinates of Component {}", type_idx + 1);
1756 if !labels_equal(label, &expected_label) {
1757 return Err(ParseError::ValidationError(format!(
1758 "expected coordinate label {expected_label:?}, found {label:?}"
1759 )));
1760 }
1761 if symbol != "X" && symbol_to_atomic_number(symbol) == 0 {
1762 return Err(ParseError::ValidationError(format!(
1763 "unknown component symbol {symbol}"
1764 )));
1765 }
1766 Ok(())
1767}
1768
1769fn validate_section_component(
1770 section: &str,
1771 type_idx: usize,
1772 atom_idx: usize,
1773 symbol: &str,
1774 label: &str,
1775 header: &FrameHeader,
1776 atom_data: &[AtomDatum],
1777) -> Result<(), ParseError> {
1778 let expected_label = format!("{section} of Component {}", type_idx + 1);
1779 if !labels_equal(label, &expected_label) {
1780 return Err(ParseError::ValidationError(format!(
1781 "expected section label {expected_label:?}, found {label:?}"
1782 )));
1783 }
1784
1785 if header.natms_per_type[type_idx] == 0 {
1786 return Ok(());
1787 }
1788
1789 let expected_symbol = atom_data
1790 .get(atom_idx)
1791 .map(|atom| atom.symbol.as_ref())
1792 .ok_or_else(|| {
1793 ParseError::ValidationError(format!(
1794 "{section} component {} has no coordinate atom to validate against",
1795 type_idx + 1
1796 ))
1797 })?;
1798 if symbol != expected_symbol {
1799 return Err(ParseError::ValidationError(format!(
1800 "{section} component {} symbol mismatch: expected {expected_symbol}, found {symbol}",
1801 type_idx + 1
1802 )));
1803 }
1804
1805 Ok(())
1806}
1807
1808fn validate_section_atom_identity(
1809 section: &str,
1810 atom_idx: usize,
1811 fixed: [bool; 3],
1812 atom_id: u64,
1813 atom_data: &[AtomDatum],
1814) -> Result<(), ParseError> {
1815 let atom = atom_data.get(atom_idx).ok_or_else(|| {
1816 ParseError::ValidationError(format!(
1817 "{section} row {atom_idx} has no coordinate atom to validate against"
1818 ))
1819 })?;
1820
1821 if atom.fixed != fixed {
1822 return Err(ParseError::ValidationError(format!(
1823 "{section} row {atom_idx} fixed mask mismatch for atom_id {}",
1824 atom.atom_id
1825 )));
1826 }
1827 if atom.atom_id != atom_id {
1828 return Err(ParseError::ValidationError(format!(
1829 "{section} row {atom_idx} atom_id mismatch: expected {}, found {atom_id}",
1830 atom.atom_id
1831 )));
1832 }
1833
1834 Ok(())
1835}
1836
1837fn frame_minor(header: &FrameHeader) -> u64 {
1842 header
1843 .metadata
1844 .get(meta::CON_SPEC_MINOR)
1845 .and_then(|v| v.as_u64())
1846 .unwrap_or(0)
1847}
1848
1849fn section_row_arity(header: &FrameHeader, name: &str) -> Option<usize> {
1850 let obj = header.metadata.get(meta::SECTION_ARITY)?.as_object()?;
1851 let n = obj.get(name)?.as_u64()?;
1852 if n >= 1 { Some(n as usize) } else { None }
1853}
1854
1855fn skip_unknown_section<'a>(
1862 lines: &mut impl LineStream<'a>,
1863 header: &FrameHeader,
1864 name: &str,
1865 arity: usize,
1866) -> Result<(), ParseError> {
1867 if !open_section(lines)? {
1868 return Err(ParseError::IncompleteSection(name.to_string()));
1869 }
1870 let with_flag = arity.saturating_add(1);
1871 let with_id = arity.saturating_add(2);
1872 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1873 if lines.next_line().is_none() {
1874 return Err(ParseError::IncompleteSection(name.to_string()));
1875 }
1876 let label = lines
1877 .next_line()
1878 .ok_or_else(|| ParseError::IncompleteSection(name.to_string()))?;
1879 let expected = format!("{name} of Component {}", type_idx + 1);
1880 if !labels_equal(label, &expected) {
1881 return Err(ParseError::IncompleteSection(name.to_string()));
1882 }
1883 for _ in 0..num_atoms {
1884 let row = lines
1885 .next_line()
1886 .ok_or_else(|| ParseError::IncompleteSection(name.to_string()))?;
1887 let found = token_count(row);
1888 if found != with_flag && found != with_id {
1889 let expected = if found < with_flag {
1890 with_flag
1891 } else {
1892 with_id
1893 };
1894 return Err(ParseError::InvalidVectorLength { expected, found });
1895 }
1896 }
1897 }
1898 Ok(())
1899}
1900
1901fn open_section<'a>(lines: &mut impl LineStream<'a>) -> Result<bool, ParseError> {
1902 match lines.peek_line() {
1903 Some(line) if line.trim().is_empty() => {
1904 if lines.remainder_is_whitespace() {
1905 return Ok(false);
1906 }
1907 lines.next_line();
1908 Ok(true)
1909 }
1910 _ => Ok(false),
1911 }
1912}
1913
1914pub fn parse_velocity_section<'a>(
1927 lines: &mut impl LineStream<'a>,
1928 header: &FrameHeader,
1929 atom_data: &mut [AtomDatum],
1930) -> Result<bool, ParseError> {
1931 let validate = header.strict_validation;
1932 match lines.peek_line() {
1935 Some(line) if line.trim().is_empty() => {
1936 if lines.remainder_is_whitespace() {
1937 return Ok(false);
1938 }
1939 if !lines.blank_line_opens_section() {
1943 return Ok(false);
1944 }
1945 lines.next_line();
1946 }
1947 _ => return Ok(false),
1948 }
1949
1950 let mut atom_idx: usize = 0;
1951 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1952 let symbol = lines
1954 .next_line()
1955 .ok_or(ParseError::IncompleteSection(SECTION_VELOCITIES.into()))?
1956 .trim();
1957
1958 let comp_line = lines
1960 .next_line()
1961 .ok_or(ParseError::IncompleteSection(SECTION_VELOCITIES.into()))?;
1962 if !label_has_prefix(comp_line, "Velocities of Component") {
1963 return Err(ParseError::IncompleteSection(SECTION_VELOCITIES.into()));
1964 }
1965 if validate {
1966 validate_section_component(
1967 "Velocities",
1968 type_idx,
1969 atom_idx,
1970 symbol,
1971 comp_line,
1972 header,
1973 atom_data,
1974 )?;
1975 }
1976
1977 for _ in 0..num_atoms {
1978 let (vals, fixed, atom_id) =
1979 take_vector_row(lines, header.spec_version, SECTION_VELOCITIES)?;
1980 if validate {
1981 validate_section_atom_identity("velocities", atom_idx, fixed, atom_id, atom_data)?;
1982 }
1983 if atom_idx < atom_data.len() {
1984 atom_data[atom_idx].set_velocity(Some(vals));
1985 }
1986 atom_idx += 1;
1987 }
1988 }
1989
1990 Ok(true)
1991}
1992
1993pub fn parse_force_section<'a>(
2001 lines: &mut impl LineStream<'a>,
2002 header: &FrameHeader,
2003 atom_data: &mut [AtomDatum],
2004) -> Result<bool, ParseError> {
2005 let validate = header.strict_validation;
2006 if !open_section(lines)? {
2007 return Ok(false);
2008 }
2009
2010 let mut atom_idx: usize = 0;
2011 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
2012 let symbol = lines
2013 .next_line()
2014 .ok_or(ParseError::IncompleteSection(SECTION_FORCES.into()))?
2015 .trim();
2016
2017 let comp_line = lines
2018 .next_line()
2019 .ok_or(ParseError::IncompleteSection(SECTION_FORCES.into()))?;
2020 if !label_has_prefix(comp_line, "Forces of Component") {
2021 return Err(ParseError::IncompleteSection(SECTION_FORCES.into()));
2022 }
2023 if validate {
2024 validate_section_component(
2025 "Forces", type_idx, atom_idx, symbol, comp_line, header, atom_data,
2026 )?;
2027 }
2028
2029 for _ in 0..num_atoms {
2030 let (vals, fixed, atom_id) =
2031 take_vector_row(lines, header.spec_version, SECTION_FORCES)?;
2032 if validate {
2033 validate_section_atom_identity("forces", atom_idx, fixed, atom_id, atom_data)?;
2034 }
2035 if atom_idx < atom_data.len() {
2036 atom_data[atom_idx].set_force(Some(vals));
2037 }
2038 atom_idx += 1;
2039 }
2040 }
2041
2042 Ok(true)
2043}
2044
2045pub fn parse_energy_section<'a>(
2058 lines: &mut impl LineStream<'a>,
2059 header: &FrameHeader,
2060 atom_data: &mut [AtomDatum],
2061) -> Result<bool, ParseError> {
2062 let validate = header.strict_validation;
2063 if !open_section(lines)? {
2064 return Ok(false);
2065 }
2066
2067 let mut atom_idx: usize = 0;
2068 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
2069 let symbol = lines
2070 .next_line()
2071 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?
2072 .trim();
2073
2074 let comp_line = lines
2075 .next_line()
2076 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?;
2077 if !label_has_prefix(comp_line, "Energies of Component") {
2078 return Err(ParseError::IncompleteSection(SECTION_ENERGIES.into()));
2079 }
2080 if validate {
2081 validate_section_component(
2082 "Energies", type_idx, atom_idx, symbol, comp_line, header, atom_data,
2083 )?;
2084 }
2085
2086 for _ in 0..num_atoms {
2087 let energy_line = lines
2088 .next_line()
2089 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?;
2090 let (value, fixed, atom_id) =
2091 parse_scalar_section_row(energy_line, header.spec_version)?;
2092 if validate {
2093 validate_section_atom_identity("energies", atom_idx, fixed, atom_id, atom_data)?;
2094 }
2095 if atom_idx < atom_data.len() {
2096 atom_data[atom_idx].set_energy(Some(value));
2097 }
2098 atom_idx += 1;
2099 }
2100 }
2101
2102 Ok(true)
2103}
2104
2105pub fn parse_declared_sections<'a>(
2111 lines: &mut impl LineStream<'a>,
2112 header: &mut FrameHeader,
2113 atom_data: &mut [AtomDatum],
2114) -> Result<usize, ParseError> {
2115 let mut applied = 0usize;
2116 if header.natm_types == 0 {
2119 if !header.sections.is_empty() {
2120 return Err(ParseError::InvalidCount(
2121 "a zero-atom frame cannot declare sections".into(),
2122 ));
2123 }
2124 return Ok(0);
2125 }
2126 if !header.sections_declared && header.sections.is_empty() {
2127 let found =
2131 lines.blank_opens_velocity_block() && parse_velocity_section(lines, header, atom_data)?;
2132 if found {
2133 header.sections.push(SECTION_VELOCITIES.into());
2134 applied = 1;
2135 }
2136 } else {
2137 let sections = std::mem::take(&mut header.sections);
2138 for section in §ions {
2139 match section.as_str() {
2140 SECTION_VELOCITIES => {
2141 let found = parse_velocity_section(lines, header, atom_data)?;
2142 if !found {
2143 return Err(ParseError::IncompleteSection(SECTION_VELOCITIES.into()));
2144 }
2145 applied += 1;
2146 }
2147 SECTION_FORCES => {
2148 let found = parse_force_section(lines, header, atom_data)?;
2149 if !found {
2150 return Err(ParseError::IncompleteSection(SECTION_FORCES.into()));
2151 }
2152 applied += 1;
2153 }
2154 SECTION_ENERGIES => {
2155 let found = parse_energy_section(lines, header, atom_data)?;
2156 if !found {
2157 return Err(ParseError::IncompleteSection(SECTION_ENERGIES.into()));
2158 }
2159 applied += 1;
2160 }
2161 SECTION_CHARGES => {
2162 let found = parse_charge_section(lines, header, atom_data)?;
2163 if !found {
2164 return Err(ParseError::IncompleteSection(SECTION_CHARGES.into()));
2165 }
2166 applied += 1;
2167 }
2168 SECTION_SPINS => {
2169 let found = parse_spin_section(lines, header, atom_data)?;
2170 if !found {
2171 return Err(ParseError::IncompleteSection(SECTION_SPINS.into()));
2172 }
2173 applied += 1;
2174 }
2175 SECTION_MAGMOMS => {
2176 let found = parse_magmom_section(lines, header, atom_data)?;
2177 if !found {
2178 return Err(ParseError::IncompleteSection(SECTION_MAGMOMS.into()));
2179 }
2180 applied += 1;
2181 }
2182 SECTION_DISPLACEMENTS => {
2183 let found = parse_displacement_section(lines, header, atom_data)?;
2184 if !found {
2185 return Err(ParseError::IncompleteSection(SECTION_DISPLACEMENTS.into()));
2186 }
2187 applied += 1;
2188 }
2189 SECTION_SPREADS => {
2190 let found = parse_spread_section(lines, header, atom_data)?;
2191 if !found {
2192 return Err(ParseError::IncompleteSection(SECTION_SPREADS.into()));
2193 }
2194 applied += 1;
2195 }
2196 other => {
2197 if frame_minor(header) > u64::from(crate::CON_SPEC_MINOR)
2198 && let Some(arity) = section_row_arity(header, other) {
2199 skip_unknown_section(lines, header, other, arity)?;
2200 continue;
2201 }
2202 return Err(ParseError::UnknownSection(other.to_string()));
2203 }
2204 }
2205 }
2206 header.sections = sections;
2207 }
2208 reject_velocities_without_time(header)?;
2209 Ok(applied)
2210}
2211
2212fn reject_velocities_without_time(header: &FrameHeader) -> Result<(), ParseError> {
2216 if header.spec_version < 3 || frame_minor(header) < 1 {
2217 return Ok(());
2218 }
2219 if !header
2220 .sections
2221 .iter()
2222 .any(|name| name == SECTION_VELOCITIES)
2223 {
2224 return Ok(());
2225 }
2226 let time_ok = header
2227 .metadata
2228 .get(meta::UNITS)
2229 .and_then(|units| units.get("time"))
2230 .and_then(|value| value.as_str())
2231 .is_some_and(|unit| !unit.trim().is_empty());
2232 if time_ok {
2233 Ok(())
2234 } else {
2235 Err(ParseError::InvalidUnits(
2236 "minor >= 1 requires units.time when a velocities section is present".into(),
2237 ))
2238 }
2239}
2240
2241fn parse_scalar_atom_section<'a>(
2244 lines: &mut impl LineStream<'a>,
2245 header: &FrameHeader,
2246 atom_data: &mut [AtomDatum],
2247 section_name: &str,
2248 component_label: &str,
2249 mut set_value: impl FnMut(&mut AtomDatum, f64),
2250) -> Result<bool, ParseError> {
2251 let validate = header.strict_validation;
2252 if !open_section(lines)? {
2253 return Ok(false);
2254 }
2255
2256 let mut atom_idx: usize = 0;
2257 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
2258 let symbol = lines
2259 .next_line()
2260 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?
2261 .trim();
2262
2263 let comp_line = lines
2264 .next_line()
2265 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
2266 if !label_has_prefix(comp_line, component_label) {
2267 return Err(ParseError::IncompleteSection(section_name.into()));
2268 }
2269 if validate {
2270 validate_section_component(
2271 component_label.trim_end_matches(" of Component").trim(),
2272 type_idx,
2273 atom_idx,
2274 symbol,
2275 comp_line,
2276 header,
2277 atom_data,
2278 )?;
2279 }
2280
2281 for _ in 0..num_atoms {
2282 let data_line = lines
2283 .next_line()
2284 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
2285 let (value, fixed, atom_id) = parse_scalar_section_row(data_line, header.spec_version)?;
2286 if validate {
2287 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
2288 }
2289 if atom_idx < atom_data.len() {
2290 set_value(&mut atom_data[atom_idx], value);
2291 }
2292 atom_idx += 1;
2293 }
2294 }
2295 Ok(true)
2296}
2297
2298pub fn parse_charge_section<'a>(
2299 lines: &mut impl LineStream<'a>,
2300 header: &FrameHeader,
2301 atom_data: &mut [AtomDatum],
2302) -> Result<bool, ParseError> {
2303 parse_scalar_atom_section(
2304 lines,
2305 header,
2306 atom_data,
2307 SECTION_CHARGES,
2308 "Charges of Component",
2309 |a, v| a.set_charge(Some(v)),
2310 )
2311}
2312
2313pub fn parse_spin_section<'a>(
2314 lines: &mut impl LineStream<'a>,
2315 header: &FrameHeader,
2316 atom_data: &mut [AtomDatum],
2317) -> Result<bool, ParseError> {
2318 parse_scalar_atom_section(
2319 lines,
2320 header,
2321 atom_data,
2322 SECTION_SPINS,
2323 "Spins of Component",
2324 |a, v| a.set_spin(Some(v)),
2325 )
2326}
2327
2328fn parse_vector_named_section<'a>(
2329 lines: &mut impl LineStream<'a>,
2330 header: &FrameHeader,
2331 atom_data: &mut [AtomDatum],
2332 names: (&str, &str, &str),
2333 mut set_value: impl FnMut(&mut AtomDatum, [f64; 3]),
2334 mut check_row: impl FnMut(usize, [f64; 3]) -> Result<(), ParseError>,
2335) -> Result<bool, ParseError> {
2336 let (section_name, component_label, label_kind) = names;
2337 let validate = header.strict_validation;
2338 if !open_section(lines)? {
2339 return Ok(false);
2340 }
2341 let mut atom_idx: usize = 0;
2342 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
2343 let symbol = lines
2344 .next_line()
2345 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?
2346 .trim();
2347 let comp_line = lines
2348 .next_line()
2349 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
2350 if !label_has_prefix(comp_line, component_label) {
2351 return Err(ParseError::IncompleteSection(section_name.into()));
2352 }
2353 if validate {
2354 validate_section_component(
2355 label_kind, type_idx, atom_idx, symbol, comp_line, header, atom_data,
2356 )?;
2357 }
2358 for _ in 0..num_atoms {
2359 let (vals, fixed, atom_id) = take_vector_row(lines, header.spec_version, section_name)?;
2360 check_row(atom_idx, vals)?;
2361 if validate {
2362 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
2363 }
2364 if atom_idx < atom_data.len() {
2365 set_value(&mut atom_data[atom_idx], vals);
2366 }
2367 atom_idx += 1;
2368 }
2369 }
2370 Ok(true)
2371}
2372
2373pub fn parse_magmom_section<'a>(
2375 lines: &mut impl LineStream<'a>,
2376 header: &FrameHeader,
2377 atom_data: &mut [AtomDatum],
2378) -> Result<bool, ParseError> {
2379 parse_vector_named_section(
2380 lines,
2381 header,
2382 atom_data,
2383 (SECTION_MAGMOMS, "Magmoms of Component", "Magmoms"),
2384 |a, v| a.set_magmom(Some(v)),
2385 |_, _| Ok(()),
2386 )
2387}
2388
2389pub fn parse_displacement_section<'a>(
2391 lines: &mut impl LineStream<'a>,
2392 header: &FrameHeader,
2393 atom_data: &mut [AtomDatum],
2394) -> Result<bool, ParseError> {
2395 parse_vector_named_section(
2396 lines,
2397 header,
2398 atom_data,
2399 (
2400 SECTION_DISPLACEMENTS,
2401 "Displacements of Component",
2402 "Displacements",
2403 ),
2404 |a, v| a.set_displacement(Some(v)),
2405 |_, _| Ok(()),
2406 )
2407}
2408
2409pub fn parse_spread_section<'a>(
2412 lines: &mut impl LineStream<'a>,
2413 header: &FrameHeader,
2414 atom_data: &mut [AtomDatum],
2415) -> Result<bool, ParseError> {
2416 parse_vector_named_section(
2417 lines,
2418 header,
2419 atom_data,
2420 (SECTION_SPREADS, "Spreads of Component", "Spreads"),
2421 |a, v| a.set_spread(Some(v)),
2422 |atom_idx, vals| {
2423 if !vals.iter().all(|v| v.is_finite()) {
2424 return Err(ParseError::NonFiniteValue(format!(
2425 "spreads atom {atom_idx}"
2426 )));
2427 }
2428 if vals.iter().any(|v| *v < 0.0) {
2429 return Err(crate::types::invalid_spread(atom_idx));
2430 }
2431 Ok(())
2432 },
2433 )
2434}
2435
2436#[cfg(test)]
2437mod tests {
2438 use super::*;
2439 use crate::iterators::ConFrameIterator;
2440
2441 #[test]
2442 fn test_parse_line_of_n_success() {
2443 let line = "1.0 2.5 -3.0";
2444 let values = parse_line_of_n::<f64>(line, 3).unwrap();
2445 assert_eq!(values, vec![1.0, 2.5, -3.0]);
2446 }
2447
2448 #[test]
2449 fn test_parse_line_of_n_too_short() {
2450 let line = "1.0 2.5";
2451 let result = parse_line_of_n::<f64>(line, 3);
2452 assert!(result.is_err());
2453 assert!(matches!(
2454 result.unwrap_err(),
2455 ParseError::InvalidVectorLength {
2456 expected: 3,
2457 found: 2
2458 }
2459 ));
2460 }
2461
2462 #[test]
2463 fn test_parse_line_of_n_too_long() {
2464 let line = "1.0 2.5 -3.0 4.0";
2465 let result = parse_line_of_n::<f64>(line, 3);
2466 assert!(result.is_err());
2467 assert!(matches!(
2468 result.unwrap_err(),
2469 ParseError::InvalidVectorLength {
2470 expected: 3,
2471 found: 4
2472 }
2473 ));
2474 }
2475
2476 #[test]
2477 fn test_parse_line_of_n_invalid_float() {
2478 let line = "1.0 abc -3.0";
2479 let result = parse_line_of_n::<f64>(line, 3);
2480 assert!(result.is_err());
2481 assert!(matches!(
2482 result.unwrap_err(),
2483 ParseError::InvalidNumberFormat(_)
2484 ));
2485 }
2486
2487 #[test]
2488 fn test_parse_frame_header_success() {
2489 let lines = [
2490 "PREBOX1",
2491 "{\"con_spec_version\":2}",
2492 "10.0 20.0 30.0",
2493 "90.0 90.0 90.0",
2494 "POSTBOX1",
2495 "POSTBOX2",
2496 "2",
2497 "1 1",
2498 "12.011 1.008",
2499 ];
2500 let mut line_it = lines.iter().copied();
2501 match parse_frame_header(&mut line_it) {
2502 Ok(header) => {
2503 assert_eq!(header.prebox_header.user, "PREBOX1");
2504 assert_eq!(header.spec_version, 2);
2505 assert_eq!(header.boxl, [10.0, 20.0, 30.0]);
2506 assert_eq!(header.angles, [90.0, 90.0, 90.0]);
2507 assert_eq!(header.postbox_header, ["POSTBOX1", "POSTBOX2"]);
2508 assert_eq!(header.natm_types, 2);
2509 assert_eq!(header.natms_per_type, vec![1, 1]);
2510 assert_eq!(header.masses_per_type, vec![12.011, 1.008]);
2511 }
2512 Err(e) => {
2513 panic!("Parsing failed when it should have succeeded. Error: {e:?}");
2514 }
2515 }
2516 }
2517
2518 #[test]
2519 fn test_parse_frame_header_missing_line() {
2520 let lines = [
2521 "PREBOX1",
2522 "{\"con_spec_version\":2}",
2523 "10.0 20.0 30.0",
2524 "90.0 90.0 90.0",
2525 "POSTBOX1",
2526 "POSTBOX2",
2527 "2",
2528 "1 1",
2529 ];
2531 let mut line_it = lines.iter().copied();
2532 let result = parse_frame_header(&mut line_it);
2533 assert!(result.is_err());
2534 assert!(matches!(result.unwrap_err(), ParseError::IncompleteHeader));
2535 }
2536
2537 #[test]
2538 fn test_parse_frame_header_missing_spec_version() {
2539 let lines = [
2540 "PREBOX1",
2541 "{}",
2542 "10.0 20.0 30.0",
2543 "90.0 90.0 90.0",
2544 "POSTBOX1",
2545 "POSTBOX2",
2546 "2",
2547 "1 1",
2548 "12.011 1.008",
2549 ];
2550 let mut line_it = lines.iter().copied();
2551 let result = parse_frame_header(&mut line_it);
2552 assert!(result.is_err());
2553 assert!(matches!(
2554 result.unwrap_err(),
2555 ParseError::MissingSpecVersion
2556 ));
2557 }
2558
2559 #[test]
2560 fn test_parse_frame_header_legacy_no_json() {
2561 let lines = [
2563 "PREBOX1",
2564 "0.0000 TIME",
2565 "10.0 20.0 30.0",
2566 "90.0 90.0 90.0",
2567 "POSTBOX1",
2568 "POSTBOX2",
2569 "2",
2570 "1 1",
2571 "12.011 1.008",
2572 ];
2573 let mut line_it = lines.iter().copied();
2574 let header = parse_frame_header(&mut line_it).unwrap();
2575 assert_eq!(header.spec_version, 1);
2576 assert!(header.metadata.is_empty());
2577 }
2578
2579 #[test]
2580 fn test_parse_frame_header_malformed_json() {
2581 let lines = [
2583 "PREBOX1",
2584 "{broken json",
2585 "10.0 20.0 30.0",
2586 "90.0 90.0 90.0",
2587 "POSTBOX1",
2588 "POSTBOX2",
2589 "2",
2590 "1 1",
2591 "12.011 1.008",
2592 ];
2593 let mut line_it = lines.iter().copied();
2594 let result = parse_frame_header(&mut line_it);
2595 assert!(result.is_err());
2596 assert!(matches!(
2597 result.unwrap_err(),
2598 ParseError::InvalidMetadataJson(_)
2599 ));
2600 }
2601
2602 #[test]
2603 fn test_parse_frame_header_unsupported_version() {
2604 let lines = [
2605 "PREBOX1",
2606 "{\"con_spec_version\":999}",
2607 "10.0 20.0 30.0",
2608 "90.0 90.0 90.0",
2609 "POSTBOX1",
2610 "POSTBOX2",
2611 "2",
2612 "1 1",
2613 "12.011 1.008",
2614 ];
2615 let mut line_it = lines.iter().copied();
2616 let result = parse_frame_header(&mut line_it);
2617 assert!(result.is_err());
2618 assert!(matches!(
2619 result.unwrap_err(),
2620 ParseError::UnsupportedVersion(999)
2621 ));
2622 }
2623
2624 #[test]
2625 fn test_v3_missing_units_rejected() {
2626 let lines = [
2627 "PREBOX1",
2628 "{\"con_spec_version\":3}",
2629 "10.0 20.0 30.0",
2630 "90.0 90.0 90.0",
2631 "POSTBOX1",
2632 "POSTBOX2",
2633 "1",
2634 "1",
2635 "1.0",
2636 ];
2637 let mut line_it = lines.iter().copied();
2638 let err = parse_frame_header(&mut line_it).unwrap_err();
2639 let msg = err.to_string();
2640 assert!(
2641 msg.contains("units") || msg.contains("v3"),
2642 "expected units error, got {msg}"
2643 );
2644 }
2645
2646 #[test]
2647 fn test_v3_invalid_units_rejected() {
2648 let lines = [
2649 "PREBOX1",
2650 r#"{"con_spec_version":3,"units":{"length":"eV","energy":"angstrom"}}"#,
2651 "10.0 20.0 30.0",
2652 "90.0 90.0 90.0",
2653 "POSTBOX1",
2654 "POSTBOX2",
2655 "1",
2656 "1",
2657 "1.0",
2658 ];
2659 let mut line_it = lines.iter().copied();
2660 assert!(parse_frame_header(&mut line_it).is_err());
2661 }
2662
2663 #[test]
2664 fn test_v3_valid_units_exposes_length_energy() {
2665 let lines = [
2666 "PREBOX1",
2667 r#"{"con_spec_version":3,"units":{"length":"angstrom","energy":"eV","mass":"amu","time":"fs"}}"#,
2668 "10.0 20.0 30.0",
2669 "90.0 90.0 90.0",
2670 "POSTBOX1",
2671 "POSTBOX2",
2672 "1",
2673 "1",
2674 "1.0",
2675 ];
2676 let mut line_it = lines.iter().copied();
2677 let header = parse_frame_header(&mut line_it).unwrap();
2678 assert_eq!(header.spec_version, 3);
2679 assert_eq!(header.length_unit(), Some("angstrom"));
2680 assert_eq!(header.energy_unit(), Some("eV"));
2681 let f = header.conversion_factor_to("length", "nm").unwrap();
2682 assert!((f - 0.1).abs() < 1e-12);
2683 }
2684
2685 #[test]
2686 fn test_parse_frame_header_extra_metadata_preserved() {
2687 let lines = [
2688 "PREBOX1",
2689 "{\"con_spec_version\":2,\"generator\":\"test\"}",
2690 "10.0 20.0 30.0",
2691 "90.0 90.0 90.0",
2692 "POSTBOX1",
2693 "POSTBOX2",
2694 "2",
2695 "1 1",
2696 "12.011 1.008",
2697 ];
2698 let mut line_it = lines.iter().copied();
2699 let header = parse_frame_header(&mut line_it).unwrap();
2700 assert_eq!(header.spec_version, 2);
2701 assert_eq!(
2702 header.metadata.get("generator"),
2703 Some(&serde_json::Value::String("test".to_string()))
2704 );
2705 }
2706
2707 #[test]
2708 fn test_parse_frame_header_invalid_natms_per_type() {
2709 let lines = [
2710 "PREBOX1",
2711 "{\"con_spec_version\":2}",
2712 "10.0 20.0 30.0",
2713 "90.0 90.0 90.0",
2714 "POSTBOX1",
2715 "POSTBOX2",
2716 "2",
2717 "1 1 1", "12.011 1.008",
2719 ];
2720 let mut line_it = lines.iter().copied();
2721 let result = parse_frame_header(&mut line_it);
2722 assert!(result.is_err());
2723 assert!(matches!(
2724 result.unwrap_err(),
2725 ParseError::InvalidVectorLength {
2726 expected: 2,
2727 found: 3
2728 }
2729 ));
2730 }
2731
2732 #[test]
2733 fn test_parse_single_frame_success() {
2734 let lines = vec![
2735 "PREBOX1",
2736 "{\"con_spec_version\":2}",
2737 "10.0 20.0 30.0",
2738 "90.0 90.0 90.0",
2739 "POSTBOX1",
2740 "POSTBOX2",
2741 "2",
2742 "3 3",
2743 "12.011 1.008",
2744 "1",
2745 "Coordinates of Component 1",
2746 "0.0 0.0 0.0 0 1",
2747 "1.0940 0.0 0.0 0 2",
2748 "-0.5470 0.9499 0.0 0 3",
2749 "2",
2750 "Coordinates of Component 2",
2751 "5.0 5.0 5.0 0 4",
2752 "6.0940 5.0 5.0 0 5",
2753 "5.5470 5.9499 5.0 0 6",
2754 ];
2755 let mut line_it = lines.iter().copied();
2756 let frame = parse_single_frame(&mut line_it).unwrap();
2757
2758 assert_eq!(frame.header.natm_types, 2);
2759 assert_eq!(frame.header.natms_per_type, vec![3, 3]);
2760 assert_eq!(frame.header.masses_per_type, vec![12.011, 1.008]);
2761 assert_eq!(frame.atom_data.len(), 6);
2762 assert_eq!(&*frame.atom_data[0].symbol, "1");
2763 assert_eq!(frame.atom_data[0].atom_id, 1);
2764 assert_eq!(&*frame.atom_data[5].symbol, "2");
2765 assert_eq!(frame.atom_data[5].atom_id, 6);
2766 }
2767
2768 #[test]
2769 fn test_column_4_value_1_is_x_only_on_spec_2() {
2770 let lines = [
2771 "Generated by probe",
2772 "{\"con_spec_version\":2}",
2773 "20.0 20.0 20.0",
2774 "90.0 90.0 90.0",
2775 "",
2776 "",
2777 "1",
2778 "1",
2779 "63.5",
2780 "Cu",
2781 "Coordinates of component 1",
2782 "1.0 2.0 3.0 1 1",
2783 ];
2784 let mut line_it = lines.iter().copied();
2785 let frame = parse_single_frame(&mut line_it).unwrap();
2786 assert_eq!(frame.header.spec_version, 2);
2787 assert_eq!(frame.atom_data[0].fixed, [true, false, false]);
2788 }
2789
2790 #[test]
2791 fn test_column_4_value_1_is_all_fixed_on_spec_1() {
2792 let lines = [
2793 "Generated by probe",
2794 "0.0000 TIME",
2795 "20.0 20.0 20.0",
2796 "90.0 90.0 90.0",
2797 "",
2798 "",
2799 "1",
2800 "1",
2801 "63.5",
2802 "Cu",
2803 "Coordinates of component 1",
2804 "1.0 2.0 3.0 1 1",
2805 ];
2806 let mut line_it = lines.iter().copied();
2807 let frame = parse_single_frame(&mut line_it).unwrap();
2808 assert_eq!(frame.header.spec_version, 1);
2809 assert_eq!(frame.atom_data[0].fixed, [true, true, true]);
2810 }
2811
2812 #[test]
2813 fn test_parse_single_frame_missing_line() {
2814 let lines = vec![
2816 "PREBOX1",
2817 "{\"con_spec_version\":2}",
2818 "10.0 20.0 30.0",
2819 "90.0 90.0 90.0",
2820 "POSTBOX1",
2821 "POSTBOX2",
2822 "2",
2823 "3 3",
2824 "12.011 1.008",
2825 "1",
2826 "Coordinates of Component 1",
2827 "0.0 0.0 0.0 0 1",
2828 "1.0940 0.0 0.0 0 2",
2829 "-0.5470 0.9499 0.0 0 3",
2830 ];
2832 let mut line_it = lines.iter().copied();
2833 let result = parse_single_frame(&mut line_it);
2834 assert!(result.is_err());
2835 assert!(matches!(result.unwrap_err(), ParseError::IncompleteFrame));
2836 }
2837
2838 #[test]
2839 fn test_parse_single_frame_missing_atom_index_defaults_sequential() {
2840 let lines = vec![
2842 "PREBOX1",
2843 "{\"con_spec_version\":2}",
2844 "10.0 20.0 30.0",
2845 "90.0 90.0 90.0",
2846 "POSTBOX1",
2847 "POSTBOX2",
2848 "2",
2849 "3 3",
2850 "12.011 1.008",
2851 "1",
2852 "Coordinates of Component 1",
2853 "0.0 0.0 0.0 0 1",
2854 "1.0940 0.0 0.0 0 2",
2855 "-0.5470 0.9499 0.0 0 3",
2856 "2",
2857 "Coordinates of Component 2",
2858 "5.0 5.0 5.0 0", "6.0940 5.0 5.0 0 10", "5.5470 5.9499 5.0 0", ];
2862 let mut line_it = lines.iter().copied();
2863 let frame = parse_single_frame(&mut line_it).unwrap();
2864 assert_eq!(frame.atom_data.len(), 6);
2865 assert_eq!(frame.atom_data[0].atom_id, 1);
2867 assert_eq!(frame.atom_data[1].atom_id, 2);
2868 assert_eq!(frame.atom_data[2].atom_id, 3);
2869 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); }
2874
2875 #[test]
2876 fn test_parse_single_frame_too_few_columns_fails() {
2877 let lines = [
2879 "PREBOX1",
2880 "{\"con_spec_version\":2}",
2881 "10.0 20.0 30.0",
2882 "90.0 90.0 90.0",
2883 "POSTBOX1",
2884 "POSTBOX2",
2885 "1",
2886 "1",
2887 "12.011",
2888 "C",
2889 "Coordinates of Component 1",
2890 "0.0 0.0 0.0", ];
2892 let mut line_it = lines.iter().copied();
2893 let result = parse_single_frame(&mut line_it);
2894 assert!(result.is_err());
2895 assert!(matches!(
2896 result.unwrap_err(),
2897 ParseError::InvalidVectorLength {
2898 expected: 5,
2899 found: 3
2900 }
2901 ));
2902 }
2903
2904 #[test]
2905 fn test_parse_velocity_section_present() {
2906 let lines = vec![
2907 "PREBOX1",
2908 "{\"con_spec_version\":2}",
2909 "10.0 20.0 30.0",
2910 "90.0 90.0 90.0",
2911 "POSTBOX1",
2912 "POSTBOX2",
2913 "2",
2914 "1 1",
2915 "63.546 1.008",
2916 "Cu",
2917 "Coordinates of Component 1",
2918 "0.0 0.0 0.0 1 0",
2919 "H",
2920 "Coordinates of Component 2",
2921 "1.0 2.0 3.0 0 1",
2922 "",
2923 "Cu",
2924 "Velocities of Component 1",
2925 "0.1 0.2 0.3 1 0",
2926 "H",
2927 "Velocities of Component 2",
2928 "0.4 0.5 0.6 0 1",
2929 ];
2930 let mut line_it = lines.iter().copied().peekable();
2931 let mut frame =
2933 parse_single_frame(&mut line_it).expect("coordinate parsing should succeed");
2934 assert!(!frame.has_velocities());
2935
2936 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
2938 .expect("velocity parsing should succeed");
2939 assert!(has_vel);
2940 assert_eq!(frame.atom_data[0].velocity(), Some([0.1, 0.2, 0.3]));
2941 assert_eq!(frame.atom_data[1].velocity(), Some([0.4, 0.5, 0.6]));
2942 }
2943
2944 #[test]
2945 fn test_validate_true_accepts_matching_section_identity() {
2946 let text = r#"
2947PREBOX1
2948{"con_spec_version":2,"sections":["velocities"],"validate":true}
294910.0 20.0 30.0
295090.0 90.0 90.0
2951POSTBOX1
2952POSTBOX2
29532
29541 1
295563.546 1.008
2956Cu
2957Coordinates of Component 1
29580.0 0.0 0.0 5 0
2959H
2960Coordinates of Component 2
29611.0 2.0 3.0 0 1
2962
2963Cu
2964Velocities of Component 1
29650.1 0.2 0.3 5 0
2966H
2967Velocities of Component 2
29680.4 0.5 0.6 0 1
2969"#;
2970 let mut iter = ConFrameIterator::new(text.trim());
2971 let frame = iter.next().unwrap().unwrap();
2972
2973 assert!(frame.has_velocities());
2974 assert_eq!(
2975 frame
2976 .header
2977 .metadata
2978 .get("validate")
2979 .and_then(|v| v.as_bool()),
2980 Some(true)
2981 );
2982 }
2983
2984 #[test]
2985 fn test_validate_true_rejects_section_atom_id_mismatch() {
2986 let text = r#"
2987PREBOX1
2988{"con_spec_version":2,"sections":["velocities"],"validate":true}
298910.0 20.0 30.0
299090.0 90.0 90.0
2991POSTBOX1
2992POSTBOX2
29931
29941
299563.546
2996Cu
2997Coordinates of Component 1
29980.0 0.0 0.0 5 0
2999
3000Cu
3001Velocities of Component 1
30020.1 0.2 0.3 5 99
3003"#;
3004 let mut iter = ConFrameIterator::new(text.trim());
3005 let err = iter.next().unwrap().unwrap_err();
3006
3007 assert!(matches!(err, ParseError::ValidationError(_)));
3008 assert!(err.to_string().contains("atom_id mismatch"));
3009 }
3010
3011 #[test]
3012 fn test_validate_true_rejects_section_symbol_mismatch() {
3013 let text = r#"
3014PREBOX1
3015{"con_spec_version":2,"sections":["forces"],"validate":true}
301610.0 20.0 30.0
301790.0 90.0 90.0
3018POSTBOX1
3019POSTBOX2
30201
30211
302263.546
3023Cu
3024Coordinates of Component 1
30250.0 0.0 0.0 5 0
3026
3027H
3028Forces of Component 1
30290.1 0.2 0.3 5 0
3030"#;
3031 let mut iter = ConFrameIterator::new(text.trim());
3032 let err = iter.next().unwrap().unwrap_err();
3033
3034 assert!(matches!(err, ParseError::ValidationError(_)));
3035 assert!(err.to_string().contains("symbol mismatch"));
3036 }
3037
3038 #[test]
3039 fn test_validate_absent_allows_legacy_duplicate_identity_mismatch() {
3040 let text = r#"
3041PREBOX1
3042{"con_spec_version":2,"sections":["velocities"]}
304310.0 20.0 30.0
304490.0 90.0 90.0
3045POSTBOX1
3046POSTBOX2
30471
30481
304963.546
3050Cu
3051Coordinates of Component 1
30520.0 0.0 0.0 5 0
3053
3054Cu
3055Velocities of Component 1
30560.1 0.2 0.3 0 99
3057"#;
3058 let mut iter = ConFrameIterator::new(text.trim());
3059 let frame = iter.next().unwrap().unwrap();
3060
3061 assert!(frame.has_velocities());
3062 assert_eq!(frame.atom_data[0].atom_id, 0);
3063 assert_eq!(frame.atom_data[0].fixed, [true, false, true]);
3064 }
3065
3066 #[test]
3067 fn test_validate_must_be_boolean_when_present() {
3068 let lines = [
3069 "PREBOX1",
3070 "{\"con_spec_version\":2,\"validate\":\"yes\",\"sections\":[]}",
3071 "10.0 20.0 30.0",
3072 "90.0 90.0 90.0",
3073 "POSTBOX1",
3074 "POSTBOX2",
3075 "1",
3076 "1",
3077 "12.011",
3078 ];
3079 let mut line_it = lines.iter().copied();
3080 let err = parse_frame_header(&mut line_it).unwrap_err();
3081
3082 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
3083 assert!(err.to_string().contains("validate"));
3084 }
3085
3086 #[test]
3087 fn test_validate_true_requires_sections_key() {
3088 let lines = [
3089 "PREBOX1",
3090 "{\"con_spec_version\":2,\"validate\":true}",
3091 "10.0 20.0 30.0",
3092 "90.0 90.0 90.0",
3093 "POSTBOX1",
3094 "POSTBOX2",
3095 "1",
3096 "1",
3097 "12.011",
3098 ];
3099 let mut line_it = lines.iter().copied();
3100 let err = parse_frame_header(&mut line_it).unwrap_err();
3101
3102 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
3103 assert!(err.to_string().contains("sections"));
3104 }
3105
3106 #[test]
3107 fn test_sections_must_be_string_array_when_present() {
3108 let lines = [
3109 "PREBOX1",
3110 "{\"con_spec_version\":2,\"sections\":[\"velocities\",7]}",
3111 "10.0 20.0 30.0",
3112 "90.0 90.0 90.0",
3113 "POSTBOX1",
3114 "POSTBOX2",
3115 "1",
3116 "1",
3117 "12.011",
3118 ];
3119 let mut line_it = lines.iter().copied();
3120 let err = parse_frame_header(&mut line_it).unwrap_err();
3121
3122 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
3123 assert!(err.to_string().contains("sections"));
3124 }
3125
3126 #[test]
3127 fn test_validate_true_rejects_non_integer_coordinate_identity_columns() {
3128 let text = r#"
3129PREBOX1
3130{"con_spec_version":2,"sections":[],"validate":true}
313110.0 20.0 30.0
313290.0 90.0 90.0
3133POSTBOX1
3134POSTBOX2
31351
31361
313763.546
3138Cu
3139Coordinates of Component 1
31400.0 0.0 0.0 5.0 0
3141"#;
3142 let mut iter = ConFrameIterator::new(text.trim());
3143 let err = iter.next().unwrap().unwrap_err();
3144
3145 assert!(matches!(err, ParseError::InvalidFixedFlag(_)));
3146 assert!(err.to_string().contains("5.0"));
3147 }
3148
3149 #[test]
3150 fn test_validate_true_rejects_non_exact_coordinate_label() {
3151 let text = r#"
3152PREBOX1
3153{"con_spec_version":2,"sections":[],"validate":true}
315410.0 20.0 30.0
315590.0 90.0 90.0
3156POSTBOX1
3157POSTBOX2
31581
31591
316063.546
3161Cu
3162Coordinates Component 1
31630.0 0.0 0.0 5 0
3164"#;
3165 let mut iter = ConFrameIterator::new(text.trim());
3166 let err = iter.next().unwrap().unwrap_err();
3167
3168 assert!(matches!(err, ParseError::ValidationError(_)));
3169 assert!(err.to_string().contains("Coordinates of Component 1"));
3170 }
3171
3172 #[test]
3173 fn test_validate_true_rejects_unknown_component_symbol() {
3174 let text = r#"
3175PREBOX1
3176{"con_spec_version":2,"sections":[],"validate":true}
317710.0 20.0 30.0
317890.0 90.0 90.0
3179POSTBOX1
3180POSTBOX2
31811
31821
318363.546
3184Qq
3185Coordinates of Component 1
31860.0 0.0 0.0 0 0
3187"#;
3188 let mut iter = ConFrameIterator::new(text.trim());
3189 let err = iter.next().unwrap().unwrap_err();
3190
3191 assert!(matches!(err, ParseError::ValidationError(_)));
3192 assert!(err.to_string().contains("symbol"));
3193 }
3194
3195 #[test]
3196 fn test_declared_section_must_be_present() {
3197 let text = r#"
3198PREBOX1
3199{"con_spec_version":2,"sections":["velocities"]}
320010.0 20.0 30.0
320190.0 90.0 90.0
3202POSTBOX1
3203POSTBOX2
32041
32051
320663.546
3207Cu
3208Coordinates of Component 1
32090.0 0.0 0.0 0 0
3210"#;
3211 let mut iter = ConFrameIterator::new(text.trim());
3212 let err = iter.next().unwrap().unwrap_err();
3213
3214 assert!(matches!(
3215 err,
3216 ParseError::IncompleteSection(ref name) if name == "velocities"
3217 ));
3218 }
3219
3220 #[test]
3221 fn test_non_finite_cell_geometry_rejected_in_strict_mode() {
3222 let lines = [
3223 "PREBOX1",
3224 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
3225 "10.0 NaN 30.0",
3226 "90.0 90.0 90.0",
3227 "POSTBOX1",
3228 "POSTBOX2",
3229 "1",
3230 "1",
3231 "12.011",
3232 ];
3233 let mut line_it = lines.iter().copied();
3234 let err = parse_frame_header(&mut line_it).unwrap_err();
3235
3236 assert!(matches!(err, ParseError::NonFiniteValue(_)));
3237 assert!(err.to_string().contains("NaN"));
3238 }
3239
3240 #[test]
3241 fn test_validate_true_rejects_non_physical_cell_geometry() {
3242 let lines = [
3243 "PREBOX1",
3244 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
3245 "0.0 20.0 30.0",
3246 "90.0 180.0 90.0",
3247 "POSTBOX1",
3248 "POSTBOX2",
3249 "1",
3250 "1",
3251 "12.011",
3252 ];
3253 let mut line_it = lines.iter().copied();
3254 let err = parse_frame_header(&mut line_it).unwrap_err();
3255
3256 assert!(matches!(err, ParseError::ValidationError(_)));
3257 assert!(err.to_string().contains("cell"));
3258 }
3259
3260 #[test]
3261 fn test_validate_true_rejects_reserved_metadata_type_mismatch() {
3262 let lines = [
3263 "PREBOX1",
3264 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"energy\":\"low\"}",
3265 "10.0 20.0 30.0",
3266 "90.0 90.0 90.0",
3267 "POSTBOX1",
3268 "POSTBOX2",
3269 "1",
3270 "1",
3271 "12.011",
3272 ];
3273 let mut line_it = lines.iter().copied();
3274 let err = parse_frame_header(&mut line_it).unwrap_err();
3275
3276 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
3277 assert!(err.to_string().contains("energy"));
3278 }
3279
3280 #[test]
3281 fn test_validate_true_rejects_malformed_bonds() {
3282 let lines = [
3283 "PREBOX1",
3284 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"bonds\":[[0]]}",
3285 "10.0 20.0 30.0",
3286 "90.0 90.0 90.0",
3287 "POSTBOX1",
3288 "POSTBOX2",
3289 "1",
3290 "1",
3291 "12.011",
3292 ];
3293 let mut line_it = lines.iter().copied();
3294 let err = parse_frame_header(&mut line_it).unwrap_err();
3295 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
3296 assert!(err.to_string().contains("bonds"));
3297 }
3298
3299 #[test]
3300 fn test_bonds_metadata_round_trip_in_header() {
3301 use crate::types::{Bond, meta};
3302 let lines = [
3303 "PREBOX1",
3304 "{\"con_spec_version\":2,\"bonds\":[[0,1],{\"i\":0,\"j\":2,\"order\":1}]}",
3305 "10.0 20.0 30.0",
3306 "90.0 90.0 90.0",
3307 "POSTBOX1",
3308 "POSTBOX2",
3309 "1",
3310 "1",
3311 "12.011",
3312 ];
3313 let mut line_it = lines.iter().copied();
3314 let header = parse_frame_header(&mut line_it).expect("header");
3315 let bonds = header.bonds();
3316 assert_eq!(bonds.len(), 2);
3317 assert_eq!(bonds[0], Bond::new(0, 1));
3318 assert_eq!(bonds[1].i, 0);
3319 assert_eq!(bonds[1].j, 2);
3320 assert_eq!(bonds[1].order, Some(1));
3321 assert!(header.metadata.contains_key(meta::BONDS));
3322 }
3323
3324 #[test]
3325 fn test_parse_velocity_section_absent() {
3326 let lines = [
3327 "PREBOX1",
3328 "{\"con_spec_version\":2}",
3329 "10.0 20.0 30.0",
3330 "90.0 90.0 90.0",
3331 "POSTBOX1",
3332 "POSTBOX2",
3333 "1",
3334 "1",
3335 "12.011",
3336 "C",
3337 "Coordinates of Component 1",
3338 "0.0 0.0 0.0 0 1",
3339 ];
3340 let mut line_it = lines.iter().copied().peekable();
3341 let mut frame = parse_single_frame(&mut line_it).expect("parse should succeed");
3342 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
3343 .expect("should succeed with no velocities");
3344 assert!(!has_vel);
3345 assert_eq!(frame.atom_data[0].velocity(), None);
3346 }
3347
3348 #[test]
3349 fn test_parse_line_of_range_f64_exact() {
3350 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0 42", 4, 5, &[0.0; 5]).unwrap();
3351 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 42.0]);
3352 }
3353
3354 #[test]
3355 fn test_byte_scan_stack_parses_realistic_coord_line() {
3356 let line = " 0.63939999999999997 0.90449999999999997 -0.00009999999999977 1 0";
3358 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
3359 let mut buf = [0.0f64; 5];
3360 let n = parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
3361 assert_eq!(n, 5);
3362 assert!((buf[0] - 0.6394).abs() < 1e-4);
3363 assert!((buf[1] - 0.9045).abs() < 1e-4);
3364 assert_eq!(buf[3] as u8, 1);
3365 assert_eq!(buf[4] as u64, 0);
3366 let bad = "1.0 2.0 3.0 1 0 EXTRA";
3368 assert!(parse_line_of_range_f64_stack(bad, 4, 5, &defaults, &mut buf).is_err());
3369 let glued = "1.0 2.0 3.0abc 1 0";
3371 assert!(parse_line_of_range_f64_stack(glued, 4, 5, &defaults, &mut buf).is_err());
3372 }
3373
3374 #[test]
3375 fn test_parse_line_of_range_f64_stack_matches_vec_api() {
3376 let defaults = [0.0, 0.0, 0.0, 0.0, 7.0];
3377 let line = "1.0 2.0 3.0 0.0";
3378 let mut buf = [0.0f64; 5];
3379 parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
3380 let via_vec = parse_line_of_range_f64(line, 4, 5, &defaults).unwrap();
3381 assert_eq!(&buf[..5], via_vec.as_slice());
3382 assert_eq!(buf[4], 7.0);
3383 }
3384
3385 #[test]
3386 fn test_parse_line_of_range_f64_padded() {
3387 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
3388 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0", 4, 5, &defaults).unwrap();
3389 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 99.0]);
3390 }
3391
3392 #[test]
3393 fn test_parse_line_of_range_f64_too_few() {
3394 let result = parse_line_of_range_f64("1.0 2.0 3.0", 4, 5, &[0.0; 5]);
3395 assert!(result.is_err());
3396 }
3397
3398 #[test]
3399 fn test_parse_line_of_range_f64_too_many() {
3400 let result = parse_line_of_range_f64("1.0 2.0 3.0 0.0 5.0 6.0", 4, 5, &[0.0; 5]);
3401 assert!(result.is_err());
3402 }
3403
3404 #[test]
3405 fn test_parse_all_four_column_lines() {
3406 let lines = [
3408 "PREBOX1",
3409 "{\"con_spec_version\":2}",
3410 "10.0 10.0 10.0",
3411 "90.0 90.0 90.0",
3412 "POSTBOX1",
3413 "POSTBOX2",
3414 "1",
3415 "3",
3416 "12.011",
3417 "C",
3418 "Coordinates of Component 1",
3419 "0.0 0.0 0.0 0",
3420 "1.0 0.0 0.0 0",
3421 "2.0 0.0 0.0 1",
3422 ];
3423 let mut line_it = lines.iter().copied();
3424 let frame = parse_single_frame(&mut line_it).unwrap();
3425 assert_eq!(frame.atom_data[0].atom_id, 0);
3426 assert_eq!(frame.atom_data[1].atom_id, 1);
3427 assert_eq!(frame.atom_data[2].atom_id, 2);
3428 assert!(frame.atom_data[2].is_fixed());
3429 }
3430}