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 {
2200 skip_unknown_section(lines, header, other, arity)?;
2201 continue;
2202 }
2203 return Err(ParseError::UnknownSection(other.to_string()));
2204 }
2205 }
2206 }
2207 header.sections = sections;
2208 }
2209 reject_velocities_without_time(header)?;
2210 Ok(applied)
2211}
2212
2213fn reject_velocities_without_time(header: &FrameHeader) -> Result<(), ParseError> {
2217 if header.spec_version < 3 || frame_minor(header) < 1 {
2218 return Ok(());
2219 }
2220 if !header
2221 .sections
2222 .iter()
2223 .any(|name| name == SECTION_VELOCITIES)
2224 {
2225 return Ok(());
2226 }
2227 let time_ok = header
2228 .metadata
2229 .get(meta::UNITS)
2230 .and_then(|units| units.get("time"))
2231 .and_then(|value| value.as_str())
2232 .is_some_and(|unit| !unit.trim().is_empty());
2233 if time_ok {
2234 Ok(())
2235 } else {
2236 Err(ParseError::InvalidUnits(
2237 "minor >= 1 requires units.time when a velocities section is present".into(),
2238 ))
2239 }
2240}
2241
2242fn parse_scalar_atom_section<'a>(
2245 lines: &mut impl LineStream<'a>,
2246 header: &FrameHeader,
2247 atom_data: &mut [AtomDatum],
2248 section_name: &str,
2249 component_label: &str,
2250 mut set_value: impl FnMut(&mut AtomDatum, f64),
2251) -> Result<bool, ParseError> {
2252 let validate = header.strict_validation;
2253 if !open_section(lines)? {
2254 return Ok(false);
2255 }
2256
2257 let mut atom_idx: usize = 0;
2258 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
2259 let symbol = lines
2260 .next_line()
2261 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?
2262 .trim();
2263
2264 let comp_line = lines
2265 .next_line()
2266 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
2267 if !label_has_prefix(comp_line, component_label) {
2268 return Err(ParseError::IncompleteSection(section_name.into()));
2269 }
2270 if validate {
2271 validate_section_component(
2272 component_label.trim_end_matches(" of Component").trim(),
2273 type_idx,
2274 atom_idx,
2275 symbol,
2276 comp_line,
2277 header,
2278 atom_data,
2279 )?;
2280 }
2281
2282 for _ in 0..num_atoms {
2283 let data_line = lines
2284 .next_line()
2285 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
2286 let (value, fixed, atom_id) = parse_scalar_section_row(data_line, header.spec_version)?;
2287 if validate {
2288 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
2289 }
2290 if atom_idx < atom_data.len() {
2291 set_value(&mut atom_data[atom_idx], value);
2292 }
2293 atom_idx += 1;
2294 }
2295 }
2296 Ok(true)
2297}
2298
2299pub fn parse_charge_section<'a>(
2300 lines: &mut impl LineStream<'a>,
2301 header: &FrameHeader,
2302 atom_data: &mut [AtomDatum],
2303) -> Result<bool, ParseError> {
2304 parse_scalar_atom_section(
2305 lines,
2306 header,
2307 atom_data,
2308 SECTION_CHARGES,
2309 "Charges of Component",
2310 |a, v| a.set_charge(Some(v)),
2311 )
2312}
2313
2314pub fn parse_spin_section<'a>(
2315 lines: &mut impl LineStream<'a>,
2316 header: &FrameHeader,
2317 atom_data: &mut [AtomDatum],
2318) -> Result<bool, ParseError> {
2319 parse_scalar_atom_section(
2320 lines,
2321 header,
2322 atom_data,
2323 SECTION_SPINS,
2324 "Spins of Component",
2325 |a, v| a.set_spin(Some(v)),
2326 )
2327}
2328
2329fn parse_vector_named_section<'a>(
2330 lines: &mut impl LineStream<'a>,
2331 header: &FrameHeader,
2332 atom_data: &mut [AtomDatum],
2333 names: (&str, &str, &str),
2334 mut set_value: impl FnMut(&mut AtomDatum, [f64; 3]),
2335 mut check_row: impl FnMut(usize, [f64; 3]) -> Result<(), ParseError>,
2336) -> Result<bool, ParseError> {
2337 let (section_name, component_label, label_kind) = names;
2338 let validate = header.strict_validation;
2339 if !open_section(lines)? {
2340 return Ok(false);
2341 }
2342 let mut atom_idx: usize = 0;
2343 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
2344 let symbol = lines
2345 .next_line()
2346 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?
2347 .trim();
2348 let comp_line = lines
2349 .next_line()
2350 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
2351 if !label_has_prefix(comp_line, component_label) {
2352 return Err(ParseError::IncompleteSection(section_name.into()));
2353 }
2354 if validate {
2355 validate_section_component(
2356 label_kind, type_idx, atom_idx, symbol, comp_line, header, atom_data,
2357 )?;
2358 }
2359 for _ in 0..num_atoms {
2360 let (vals, fixed, atom_id) = take_vector_row(lines, header.spec_version, section_name)?;
2361 check_row(atom_idx, vals)?;
2362 if validate {
2363 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
2364 }
2365 if atom_idx < atom_data.len() {
2366 set_value(&mut atom_data[atom_idx], vals);
2367 }
2368 atom_idx += 1;
2369 }
2370 }
2371 Ok(true)
2372}
2373
2374pub fn parse_magmom_section<'a>(
2376 lines: &mut impl LineStream<'a>,
2377 header: &FrameHeader,
2378 atom_data: &mut [AtomDatum],
2379) -> Result<bool, ParseError> {
2380 parse_vector_named_section(
2381 lines,
2382 header,
2383 atom_data,
2384 (SECTION_MAGMOMS, "Magmoms of Component", "Magmoms"),
2385 |a, v| a.set_magmom(Some(v)),
2386 |_, _| Ok(()),
2387 )
2388}
2389
2390pub fn parse_displacement_section<'a>(
2392 lines: &mut impl LineStream<'a>,
2393 header: &FrameHeader,
2394 atom_data: &mut [AtomDatum],
2395) -> Result<bool, ParseError> {
2396 parse_vector_named_section(
2397 lines,
2398 header,
2399 atom_data,
2400 (
2401 SECTION_DISPLACEMENTS,
2402 "Displacements of Component",
2403 "Displacements",
2404 ),
2405 |a, v| a.set_displacement(Some(v)),
2406 |_, _| Ok(()),
2407 )
2408}
2409
2410pub fn parse_spread_section<'a>(
2413 lines: &mut impl LineStream<'a>,
2414 header: &FrameHeader,
2415 atom_data: &mut [AtomDatum],
2416) -> Result<bool, ParseError> {
2417 parse_vector_named_section(
2418 lines,
2419 header,
2420 atom_data,
2421 (SECTION_SPREADS, "Spreads of Component", "Spreads"),
2422 |a, v| a.set_spread(Some(v)),
2423 |atom_idx, vals| {
2424 if !vals.iter().all(|v| v.is_finite()) {
2425 return Err(ParseError::NonFiniteValue(format!(
2426 "spreads atom {atom_idx}"
2427 )));
2428 }
2429 if vals.iter().any(|v| *v < 0.0) {
2430 return Err(crate::types::invalid_spread(atom_idx));
2431 }
2432 Ok(())
2433 },
2434 )
2435}
2436
2437#[cfg(test)]
2438mod tests {
2439 use super::*;
2440 use crate::iterators::ConFrameIterator;
2441
2442 #[test]
2443 fn test_parse_line_of_n_success() {
2444 let line = "1.0 2.5 -3.0";
2445 let values = parse_line_of_n::<f64>(line, 3).unwrap();
2446 assert_eq!(values, vec![1.0, 2.5, -3.0]);
2447 }
2448
2449 #[test]
2450 fn test_parse_line_of_n_too_short() {
2451 let line = "1.0 2.5";
2452 let result = parse_line_of_n::<f64>(line, 3);
2453 assert!(result.is_err());
2454 assert!(matches!(
2455 result.unwrap_err(),
2456 ParseError::InvalidVectorLength {
2457 expected: 3,
2458 found: 2
2459 }
2460 ));
2461 }
2462
2463 #[test]
2464 fn test_parse_line_of_n_too_long() {
2465 let line = "1.0 2.5 -3.0 4.0";
2466 let result = parse_line_of_n::<f64>(line, 3);
2467 assert!(result.is_err());
2468 assert!(matches!(
2469 result.unwrap_err(),
2470 ParseError::InvalidVectorLength {
2471 expected: 3,
2472 found: 4
2473 }
2474 ));
2475 }
2476
2477 #[test]
2478 fn test_parse_line_of_n_invalid_float() {
2479 let line = "1.0 abc -3.0";
2480 let result = parse_line_of_n::<f64>(line, 3);
2481 assert!(result.is_err());
2482 assert!(matches!(
2483 result.unwrap_err(),
2484 ParseError::InvalidNumberFormat(_)
2485 ));
2486 }
2487
2488 #[test]
2489 fn test_parse_frame_header_success() {
2490 let lines = [
2491 "PREBOX1",
2492 "{\"con_spec_version\":2}",
2493 "10.0 20.0 30.0",
2494 "90.0 90.0 90.0",
2495 "POSTBOX1",
2496 "POSTBOX2",
2497 "2",
2498 "1 1",
2499 "12.011 1.008",
2500 ];
2501 let mut line_it = lines.iter().copied();
2502 match parse_frame_header(&mut line_it) {
2503 Ok(header) => {
2504 assert_eq!(header.prebox_header.user, "PREBOX1");
2505 assert_eq!(header.spec_version, 2);
2506 assert_eq!(header.boxl, [10.0, 20.0, 30.0]);
2507 assert_eq!(header.angles, [90.0, 90.0, 90.0]);
2508 assert_eq!(header.postbox_header, ["POSTBOX1", "POSTBOX2"]);
2509 assert_eq!(header.natm_types, 2);
2510 assert_eq!(header.natms_per_type, vec![1, 1]);
2511 assert_eq!(header.masses_per_type, vec![12.011, 1.008]);
2512 }
2513 Err(e) => {
2514 panic!("Parsing failed when it should have succeeded. Error: {e:?}");
2515 }
2516 }
2517 }
2518
2519 #[test]
2520 fn test_parse_frame_header_missing_line() {
2521 let lines = [
2522 "PREBOX1",
2523 "{\"con_spec_version\":2}",
2524 "10.0 20.0 30.0",
2525 "90.0 90.0 90.0",
2526 "POSTBOX1",
2527 "POSTBOX2",
2528 "2",
2529 "1 1",
2530 ];
2532 let mut line_it = lines.iter().copied();
2533 let result = parse_frame_header(&mut line_it);
2534 assert!(result.is_err());
2535 assert!(matches!(result.unwrap_err(), ParseError::IncompleteHeader));
2536 }
2537
2538 #[test]
2539 fn test_parse_frame_header_missing_spec_version() {
2540 let lines = [
2541 "PREBOX1",
2542 "{}",
2543 "10.0 20.0 30.0",
2544 "90.0 90.0 90.0",
2545 "POSTBOX1",
2546 "POSTBOX2",
2547 "2",
2548 "1 1",
2549 "12.011 1.008",
2550 ];
2551 let mut line_it = lines.iter().copied();
2552 let result = parse_frame_header(&mut line_it);
2553 assert!(result.is_err());
2554 assert!(matches!(
2555 result.unwrap_err(),
2556 ParseError::MissingSpecVersion
2557 ));
2558 }
2559
2560 #[test]
2561 fn test_parse_frame_header_legacy_no_json() {
2562 let lines = [
2564 "PREBOX1",
2565 "0.0000 TIME",
2566 "10.0 20.0 30.0",
2567 "90.0 90.0 90.0",
2568 "POSTBOX1",
2569 "POSTBOX2",
2570 "2",
2571 "1 1",
2572 "12.011 1.008",
2573 ];
2574 let mut line_it = lines.iter().copied();
2575 let header = parse_frame_header(&mut line_it).unwrap();
2576 assert_eq!(header.spec_version, 1);
2577 assert!(header.metadata.is_empty());
2578 }
2579
2580 #[test]
2581 fn test_parse_frame_header_malformed_json() {
2582 let lines = [
2584 "PREBOX1",
2585 "{broken json",
2586 "10.0 20.0 30.0",
2587 "90.0 90.0 90.0",
2588 "POSTBOX1",
2589 "POSTBOX2",
2590 "2",
2591 "1 1",
2592 "12.011 1.008",
2593 ];
2594 let mut line_it = lines.iter().copied();
2595 let result = parse_frame_header(&mut line_it);
2596 assert!(result.is_err());
2597 assert!(matches!(
2598 result.unwrap_err(),
2599 ParseError::InvalidMetadataJson(_)
2600 ));
2601 }
2602
2603 #[test]
2604 fn test_parse_frame_header_unsupported_version() {
2605 let lines = [
2606 "PREBOX1",
2607 "{\"con_spec_version\":999}",
2608 "10.0 20.0 30.0",
2609 "90.0 90.0 90.0",
2610 "POSTBOX1",
2611 "POSTBOX2",
2612 "2",
2613 "1 1",
2614 "12.011 1.008",
2615 ];
2616 let mut line_it = lines.iter().copied();
2617 let result = parse_frame_header(&mut line_it);
2618 assert!(result.is_err());
2619 assert!(matches!(
2620 result.unwrap_err(),
2621 ParseError::UnsupportedVersion(999)
2622 ));
2623 }
2624
2625 #[test]
2626 fn test_v3_missing_units_rejected() {
2627 let lines = [
2628 "PREBOX1",
2629 "{\"con_spec_version\":3}",
2630 "10.0 20.0 30.0",
2631 "90.0 90.0 90.0",
2632 "POSTBOX1",
2633 "POSTBOX2",
2634 "1",
2635 "1",
2636 "1.0",
2637 ];
2638 let mut line_it = lines.iter().copied();
2639 let err = parse_frame_header(&mut line_it).unwrap_err();
2640 let msg = err.to_string();
2641 assert!(
2642 msg.contains("units") || msg.contains("v3"),
2643 "expected units error, got {msg}"
2644 );
2645 }
2646
2647 #[test]
2648 fn test_v3_invalid_units_rejected() {
2649 let lines = [
2650 "PREBOX1",
2651 r#"{"con_spec_version":3,"units":{"length":"eV","energy":"angstrom"}}"#,
2652 "10.0 20.0 30.0",
2653 "90.0 90.0 90.0",
2654 "POSTBOX1",
2655 "POSTBOX2",
2656 "1",
2657 "1",
2658 "1.0",
2659 ];
2660 let mut line_it = lines.iter().copied();
2661 assert!(parse_frame_header(&mut line_it).is_err());
2662 }
2663
2664 #[test]
2665 fn test_v3_valid_units_exposes_length_energy() {
2666 let lines = [
2667 "PREBOX1",
2668 r#"{"con_spec_version":3,"units":{"length":"angstrom","energy":"eV","mass":"amu","time":"fs"}}"#,
2669 "10.0 20.0 30.0",
2670 "90.0 90.0 90.0",
2671 "POSTBOX1",
2672 "POSTBOX2",
2673 "1",
2674 "1",
2675 "1.0",
2676 ];
2677 let mut line_it = lines.iter().copied();
2678 let header = parse_frame_header(&mut line_it).unwrap();
2679 assert_eq!(header.spec_version, 3);
2680 assert_eq!(header.length_unit(), Some("angstrom"));
2681 assert_eq!(header.energy_unit(), Some("eV"));
2682 let f = header.conversion_factor_to("length", "nm").unwrap();
2683 assert!((f - 0.1).abs() < 1e-12);
2684 }
2685
2686 #[test]
2687 fn test_parse_frame_header_extra_metadata_preserved() {
2688 let lines = [
2689 "PREBOX1",
2690 "{\"con_spec_version\":2,\"generator\":\"test\"}",
2691 "10.0 20.0 30.0",
2692 "90.0 90.0 90.0",
2693 "POSTBOX1",
2694 "POSTBOX2",
2695 "2",
2696 "1 1",
2697 "12.011 1.008",
2698 ];
2699 let mut line_it = lines.iter().copied();
2700 let header = parse_frame_header(&mut line_it).unwrap();
2701 assert_eq!(header.spec_version, 2);
2702 assert_eq!(
2703 header.metadata.get("generator"),
2704 Some(&serde_json::Value::String("test".to_string()))
2705 );
2706 }
2707
2708 #[test]
2709 fn test_parse_frame_header_invalid_natms_per_type() {
2710 let lines = [
2711 "PREBOX1",
2712 "{\"con_spec_version\":2}",
2713 "10.0 20.0 30.0",
2714 "90.0 90.0 90.0",
2715 "POSTBOX1",
2716 "POSTBOX2",
2717 "2",
2718 "1 1 1", "12.011 1.008",
2720 ];
2721 let mut line_it = lines.iter().copied();
2722 let result = parse_frame_header(&mut line_it);
2723 assert!(result.is_err());
2724 assert!(matches!(
2725 result.unwrap_err(),
2726 ParseError::InvalidVectorLength {
2727 expected: 2,
2728 found: 3
2729 }
2730 ));
2731 }
2732
2733 #[test]
2734 fn test_parse_single_frame_success() {
2735 let lines = vec![
2736 "PREBOX1",
2737 "{\"con_spec_version\":2}",
2738 "10.0 20.0 30.0",
2739 "90.0 90.0 90.0",
2740 "POSTBOX1",
2741 "POSTBOX2",
2742 "2",
2743 "3 3",
2744 "12.011 1.008",
2745 "1",
2746 "Coordinates of Component 1",
2747 "0.0 0.0 0.0 0 1",
2748 "1.0940 0.0 0.0 0 2",
2749 "-0.5470 0.9499 0.0 0 3",
2750 "2",
2751 "Coordinates of Component 2",
2752 "5.0 5.0 5.0 0 4",
2753 "6.0940 5.0 5.0 0 5",
2754 "5.5470 5.9499 5.0 0 6",
2755 ];
2756 let mut line_it = lines.iter().copied();
2757 let frame = parse_single_frame(&mut line_it).unwrap();
2758
2759 assert_eq!(frame.header.natm_types, 2);
2760 assert_eq!(frame.header.natms_per_type, vec![3, 3]);
2761 assert_eq!(frame.header.masses_per_type, vec![12.011, 1.008]);
2762 assert_eq!(frame.atom_data.len(), 6);
2763 assert_eq!(&*frame.atom_data[0].symbol, "1");
2764 assert_eq!(frame.atom_data[0].atom_id, 1);
2765 assert_eq!(&*frame.atom_data[5].symbol, "2");
2766 assert_eq!(frame.atom_data[5].atom_id, 6);
2767 }
2768
2769 #[test]
2770 fn test_column_4_value_1_is_x_only_on_spec_2() {
2771 let lines = [
2772 "Generated by probe",
2773 "{\"con_spec_version\":2}",
2774 "20.0 20.0 20.0",
2775 "90.0 90.0 90.0",
2776 "",
2777 "",
2778 "1",
2779 "1",
2780 "63.5",
2781 "Cu",
2782 "Coordinates of component 1",
2783 "1.0 2.0 3.0 1 1",
2784 ];
2785 let mut line_it = lines.iter().copied();
2786 let frame = parse_single_frame(&mut line_it).unwrap();
2787 assert_eq!(frame.header.spec_version, 2);
2788 assert_eq!(frame.atom_data[0].fixed, [true, false, false]);
2789 }
2790
2791 #[test]
2792 fn test_column_4_value_1_is_all_fixed_on_spec_1() {
2793 let lines = [
2794 "Generated by probe",
2795 "0.0000 TIME",
2796 "20.0 20.0 20.0",
2797 "90.0 90.0 90.0",
2798 "",
2799 "",
2800 "1",
2801 "1",
2802 "63.5",
2803 "Cu",
2804 "Coordinates of component 1",
2805 "1.0 2.0 3.0 1 1",
2806 ];
2807 let mut line_it = lines.iter().copied();
2808 let frame = parse_single_frame(&mut line_it).unwrap();
2809 assert_eq!(frame.header.spec_version, 1);
2810 assert_eq!(frame.atom_data[0].fixed, [true, true, true]);
2811 }
2812
2813 #[test]
2814 fn test_parse_single_frame_missing_line() {
2815 let lines = vec![
2817 "PREBOX1",
2818 "{\"con_spec_version\":2}",
2819 "10.0 20.0 30.0",
2820 "90.0 90.0 90.0",
2821 "POSTBOX1",
2822 "POSTBOX2",
2823 "2",
2824 "3 3",
2825 "12.011 1.008",
2826 "1",
2827 "Coordinates of Component 1",
2828 "0.0 0.0 0.0 0 1",
2829 "1.0940 0.0 0.0 0 2",
2830 "-0.5470 0.9499 0.0 0 3",
2831 ];
2833 let mut line_it = lines.iter().copied();
2834 let result = parse_single_frame(&mut line_it);
2835 assert!(result.is_err());
2836 assert!(matches!(result.unwrap_err(), ParseError::IncompleteFrame));
2837 }
2838
2839 #[test]
2840 fn test_parse_single_frame_missing_atom_index_defaults_sequential() {
2841 let lines = vec![
2843 "PREBOX1",
2844 "{\"con_spec_version\":2}",
2845 "10.0 20.0 30.0",
2846 "90.0 90.0 90.0",
2847 "POSTBOX1",
2848 "POSTBOX2",
2849 "2",
2850 "3 3",
2851 "12.011 1.008",
2852 "1",
2853 "Coordinates of Component 1",
2854 "0.0 0.0 0.0 0 1",
2855 "1.0940 0.0 0.0 0 2",
2856 "-0.5470 0.9499 0.0 0 3",
2857 "2",
2858 "Coordinates of Component 2",
2859 "5.0 5.0 5.0 0", "6.0940 5.0 5.0 0 10", "5.5470 5.9499 5.0 0", ];
2863 let mut line_it = lines.iter().copied();
2864 let frame = parse_single_frame(&mut line_it).unwrap();
2865 assert_eq!(frame.atom_data.len(), 6);
2866 assert_eq!(frame.atom_data[0].atom_id, 1);
2868 assert_eq!(frame.atom_data[1].atom_id, 2);
2869 assert_eq!(frame.atom_data[2].atom_id, 3);
2870 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); }
2875
2876 #[test]
2877 fn test_parse_single_frame_too_few_columns_fails() {
2878 let lines = [
2880 "PREBOX1",
2881 "{\"con_spec_version\":2}",
2882 "10.0 20.0 30.0",
2883 "90.0 90.0 90.0",
2884 "POSTBOX1",
2885 "POSTBOX2",
2886 "1",
2887 "1",
2888 "12.011",
2889 "C",
2890 "Coordinates of Component 1",
2891 "0.0 0.0 0.0", ];
2893 let mut line_it = lines.iter().copied();
2894 let result = parse_single_frame(&mut line_it);
2895 assert!(result.is_err());
2896 assert!(matches!(
2897 result.unwrap_err(),
2898 ParseError::InvalidVectorLength {
2899 expected: 5,
2900 found: 3
2901 }
2902 ));
2903 }
2904
2905 #[test]
2906 fn test_parse_velocity_section_present() {
2907 let lines = vec![
2908 "PREBOX1",
2909 "{\"con_spec_version\":2}",
2910 "10.0 20.0 30.0",
2911 "90.0 90.0 90.0",
2912 "POSTBOX1",
2913 "POSTBOX2",
2914 "2",
2915 "1 1",
2916 "63.546 1.008",
2917 "Cu",
2918 "Coordinates of Component 1",
2919 "0.0 0.0 0.0 1 0",
2920 "H",
2921 "Coordinates of Component 2",
2922 "1.0 2.0 3.0 0 1",
2923 "",
2924 "Cu",
2925 "Velocities of Component 1",
2926 "0.1 0.2 0.3 1 0",
2927 "H",
2928 "Velocities of Component 2",
2929 "0.4 0.5 0.6 0 1",
2930 ];
2931 let mut line_it = lines.iter().copied().peekable();
2932 let mut frame =
2934 parse_single_frame(&mut line_it).expect("coordinate parsing should succeed");
2935 assert!(!frame.has_velocities());
2936
2937 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
2939 .expect("velocity parsing should succeed");
2940 assert!(has_vel);
2941 assert_eq!(frame.atom_data[0].velocity(), Some([0.1, 0.2, 0.3]));
2942 assert_eq!(frame.atom_data[1].velocity(), Some([0.4, 0.5, 0.6]));
2943 }
2944
2945 #[test]
2946 fn test_validate_true_accepts_matching_section_identity() {
2947 let text = r#"
2948PREBOX1
2949{"con_spec_version":2,"sections":["velocities"],"validate":true}
295010.0 20.0 30.0
295190.0 90.0 90.0
2952POSTBOX1
2953POSTBOX2
29542
29551 1
295663.546 1.008
2957Cu
2958Coordinates of Component 1
29590.0 0.0 0.0 5 0
2960H
2961Coordinates of Component 2
29621.0 2.0 3.0 0 1
2963
2964Cu
2965Velocities of Component 1
29660.1 0.2 0.3 5 0
2967H
2968Velocities of Component 2
29690.4 0.5 0.6 0 1
2970"#;
2971 let mut iter = ConFrameIterator::new(text.trim());
2972 let frame = iter.next().unwrap().unwrap();
2973
2974 assert!(frame.has_velocities());
2975 assert_eq!(
2976 frame
2977 .header
2978 .metadata
2979 .get("validate")
2980 .and_then(|v| v.as_bool()),
2981 Some(true)
2982 );
2983 }
2984
2985 #[test]
2986 fn test_validate_true_rejects_section_atom_id_mismatch() {
2987 let text = r#"
2988PREBOX1
2989{"con_spec_version":2,"sections":["velocities"],"validate":true}
299010.0 20.0 30.0
299190.0 90.0 90.0
2992POSTBOX1
2993POSTBOX2
29941
29951
299663.546
2997Cu
2998Coordinates of Component 1
29990.0 0.0 0.0 5 0
3000
3001Cu
3002Velocities of Component 1
30030.1 0.2 0.3 5 99
3004"#;
3005 let mut iter = ConFrameIterator::new(text.trim());
3006 let err = iter.next().unwrap().unwrap_err();
3007
3008 assert!(matches!(err, ParseError::ValidationError(_)));
3009 assert!(err.to_string().contains("atom_id mismatch"));
3010 }
3011
3012 #[test]
3013 fn test_validate_true_rejects_section_symbol_mismatch() {
3014 let text = r#"
3015PREBOX1
3016{"con_spec_version":2,"sections":["forces"],"validate":true}
301710.0 20.0 30.0
301890.0 90.0 90.0
3019POSTBOX1
3020POSTBOX2
30211
30221
302363.546
3024Cu
3025Coordinates of Component 1
30260.0 0.0 0.0 5 0
3027
3028H
3029Forces of Component 1
30300.1 0.2 0.3 5 0
3031"#;
3032 let mut iter = ConFrameIterator::new(text.trim());
3033 let err = iter.next().unwrap().unwrap_err();
3034
3035 assert!(matches!(err, ParseError::ValidationError(_)));
3036 assert!(err.to_string().contains("symbol mismatch"));
3037 }
3038
3039 #[test]
3040 fn test_validate_absent_allows_legacy_duplicate_identity_mismatch() {
3041 let text = r#"
3042PREBOX1
3043{"con_spec_version":2,"sections":["velocities"]}
304410.0 20.0 30.0
304590.0 90.0 90.0
3046POSTBOX1
3047POSTBOX2
30481
30491
305063.546
3051Cu
3052Coordinates of Component 1
30530.0 0.0 0.0 5 0
3054
3055Cu
3056Velocities of Component 1
30570.1 0.2 0.3 0 99
3058"#;
3059 let mut iter = ConFrameIterator::new(text.trim());
3060 let frame = iter.next().unwrap().unwrap();
3061
3062 assert!(frame.has_velocities());
3063 assert_eq!(frame.atom_data[0].atom_id, 0);
3064 assert_eq!(frame.atom_data[0].fixed, [true, false, true]);
3065 }
3066
3067 #[test]
3068 fn test_validate_must_be_boolean_when_present() {
3069 let lines = [
3070 "PREBOX1",
3071 "{\"con_spec_version\":2,\"validate\":\"yes\",\"sections\":[]}",
3072 "10.0 20.0 30.0",
3073 "90.0 90.0 90.0",
3074 "POSTBOX1",
3075 "POSTBOX2",
3076 "1",
3077 "1",
3078 "12.011",
3079 ];
3080 let mut line_it = lines.iter().copied();
3081 let err = parse_frame_header(&mut line_it).unwrap_err();
3082
3083 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
3084 assert!(err.to_string().contains("validate"));
3085 }
3086
3087 #[test]
3088 fn test_validate_true_requires_sections_key() {
3089 let lines = [
3090 "PREBOX1",
3091 "{\"con_spec_version\":2,\"validate\":true}",
3092 "10.0 20.0 30.0",
3093 "90.0 90.0 90.0",
3094 "POSTBOX1",
3095 "POSTBOX2",
3096 "1",
3097 "1",
3098 "12.011",
3099 ];
3100 let mut line_it = lines.iter().copied();
3101 let err = parse_frame_header(&mut line_it).unwrap_err();
3102
3103 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
3104 assert!(err.to_string().contains("sections"));
3105 }
3106
3107 #[test]
3108 fn test_sections_must_be_string_array_when_present() {
3109 let lines = [
3110 "PREBOX1",
3111 "{\"con_spec_version\":2,\"sections\":[\"velocities\",7]}",
3112 "10.0 20.0 30.0",
3113 "90.0 90.0 90.0",
3114 "POSTBOX1",
3115 "POSTBOX2",
3116 "1",
3117 "1",
3118 "12.011",
3119 ];
3120 let mut line_it = lines.iter().copied();
3121 let err = parse_frame_header(&mut line_it).unwrap_err();
3122
3123 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
3124 assert!(err.to_string().contains("sections"));
3125 }
3126
3127 #[test]
3128 fn test_validate_true_rejects_non_integer_coordinate_identity_columns() {
3129 let text = r#"
3130PREBOX1
3131{"con_spec_version":2,"sections":[],"validate":true}
313210.0 20.0 30.0
313390.0 90.0 90.0
3134POSTBOX1
3135POSTBOX2
31361
31371
313863.546
3139Cu
3140Coordinates of Component 1
31410.0 0.0 0.0 5.0 0
3142"#;
3143 let mut iter = ConFrameIterator::new(text.trim());
3144 let err = iter.next().unwrap().unwrap_err();
3145
3146 assert!(matches!(err, ParseError::InvalidFixedFlag(_)));
3147 assert!(err.to_string().contains("5.0"));
3148 }
3149
3150 #[test]
3151 fn test_validate_true_rejects_non_exact_coordinate_label() {
3152 let text = r#"
3153PREBOX1
3154{"con_spec_version":2,"sections":[],"validate":true}
315510.0 20.0 30.0
315690.0 90.0 90.0
3157POSTBOX1
3158POSTBOX2
31591
31601
316163.546
3162Cu
3163Coordinates Component 1
31640.0 0.0 0.0 5 0
3165"#;
3166 let mut iter = ConFrameIterator::new(text.trim());
3167 let err = iter.next().unwrap().unwrap_err();
3168
3169 assert!(matches!(err, ParseError::ValidationError(_)));
3170 assert!(err.to_string().contains("Coordinates of Component 1"));
3171 }
3172
3173 #[test]
3174 fn test_validate_true_rejects_unknown_component_symbol() {
3175 let text = r#"
3176PREBOX1
3177{"con_spec_version":2,"sections":[],"validate":true}
317810.0 20.0 30.0
317990.0 90.0 90.0
3180POSTBOX1
3181POSTBOX2
31821
31831
318463.546
3185Qq
3186Coordinates of Component 1
31870.0 0.0 0.0 0 0
3188"#;
3189 let mut iter = ConFrameIterator::new(text.trim());
3190 let err = iter.next().unwrap().unwrap_err();
3191
3192 assert!(matches!(err, ParseError::ValidationError(_)));
3193 assert!(err.to_string().contains("symbol"));
3194 }
3195
3196 #[test]
3197 fn test_declared_section_must_be_present() {
3198 let text = r#"
3199PREBOX1
3200{"con_spec_version":2,"sections":["velocities"]}
320110.0 20.0 30.0
320290.0 90.0 90.0
3203POSTBOX1
3204POSTBOX2
32051
32061
320763.546
3208Cu
3209Coordinates of Component 1
32100.0 0.0 0.0 0 0
3211"#;
3212 let mut iter = ConFrameIterator::new(text.trim());
3213 let err = iter.next().unwrap().unwrap_err();
3214
3215 assert!(matches!(
3216 err,
3217 ParseError::IncompleteSection(ref name) if name == "velocities"
3218 ));
3219 }
3220
3221 #[test]
3222 fn test_non_finite_cell_geometry_rejected_in_strict_mode() {
3223 let lines = [
3224 "PREBOX1",
3225 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
3226 "10.0 NaN 30.0",
3227 "90.0 90.0 90.0",
3228 "POSTBOX1",
3229 "POSTBOX2",
3230 "1",
3231 "1",
3232 "12.011",
3233 ];
3234 let mut line_it = lines.iter().copied();
3235 let err = parse_frame_header(&mut line_it).unwrap_err();
3236
3237 assert!(matches!(err, ParseError::NonFiniteValue(_)));
3238 assert!(err.to_string().contains("NaN"));
3239 }
3240
3241 #[test]
3242 fn test_validate_true_rejects_non_physical_cell_geometry() {
3243 let lines = [
3244 "PREBOX1",
3245 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
3246 "0.0 20.0 30.0",
3247 "90.0 180.0 90.0",
3248 "POSTBOX1",
3249 "POSTBOX2",
3250 "1",
3251 "1",
3252 "12.011",
3253 ];
3254 let mut line_it = lines.iter().copied();
3255 let err = parse_frame_header(&mut line_it).unwrap_err();
3256
3257 assert!(matches!(err, ParseError::ValidationError(_)));
3258 assert!(err.to_string().contains("cell"));
3259 }
3260
3261 #[test]
3262 fn test_validate_true_rejects_reserved_metadata_type_mismatch() {
3263 let lines = [
3264 "PREBOX1",
3265 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"energy\":\"low\"}",
3266 "10.0 20.0 30.0",
3267 "90.0 90.0 90.0",
3268 "POSTBOX1",
3269 "POSTBOX2",
3270 "1",
3271 "1",
3272 "12.011",
3273 ];
3274 let mut line_it = lines.iter().copied();
3275 let err = parse_frame_header(&mut line_it).unwrap_err();
3276
3277 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
3278 assert!(err.to_string().contains("energy"));
3279 }
3280
3281 #[test]
3282 fn test_validate_true_rejects_malformed_bonds() {
3283 let lines = [
3284 "PREBOX1",
3285 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"bonds\":[[0]]}",
3286 "10.0 20.0 30.0",
3287 "90.0 90.0 90.0",
3288 "POSTBOX1",
3289 "POSTBOX2",
3290 "1",
3291 "1",
3292 "12.011",
3293 ];
3294 let mut line_it = lines.iter().copied();
3295 let err = parse_frame_header(&mut line_it).unwrap_err();
3296 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
3297 assert!(err.to_string().contains("bonds"));
3298 }
3299
3300 #[test]
3301 fn test_bonds_metadata_round_trip_in_header() {
3302 use crate::types::{Bond, meta};
3303 let lines = [
3304 "PREBOX1",
3305 "{\"con_spec_version\":2,\"bonds\":[[0,1],{\"i\":0,\"j\":2,\"order\":1}]}",
3306 "10.0 20.0 30.0",
3307 "90.0 90.0 90.0",
3308 "POSTBOX1",
3309 "POSTBOX2",
3310 "1",
3311 "1",
3312 "12.011",
3313 ];
3314 let mut line_it = lines.iter().copied();
3315 let header = parse_frame_header(&mut line_it).expect("header");
3316 let bonds = header.bonds();
3317 assert_eq!(bonds.len(), 2);
3318 assert_eq!(bonds[0], Bond::new(0, 1));
3319 assert_eq!(bonds[1].i, 0);
3320 assert_eq!(bonds[1].j, 2);
3321 assert_eq!(bonds[1].order, Some(1));
3322 assert!(header.metadata.contains_key(meta::BONDS));
3323 }
3324
3325 #[test]
3326 fn test_parse_velocity_section_absent() {
3327 let lines = [
3328 "PREBOX1",
3329 "{\"con_spec_version\":2}",
3330 "10.0 20.0 30.0",
3331 "90.0 90.0 90.0",
3332 "POSTBOX1",
3333 "POSTBOX2",
3334 "1",
3335 "1",
3336 "12.011",
3337 "C",
3338 "Coordinates of Component 1",
3339 "0.0 0.0 0.0 0 1",
3340 ];
3341 let mut line_it = lines.iter().copied().peekable();
3342 let mut frame = parse_single_frame(&mut line_it).expect("parse should succeed");
3343 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
3344 .expect("should succeed with no velocities");
3345 assert!(!has_vel);
3346 assert_eq!(frame.atom_data[0].velocity(), None);
3347 }
3348
3349 #[test]
3350 fn test_parse_line_of_range_f64_exact() {
3351 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0 42", 4, 5, &[0.0; 5]).unwrap();
3352 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 42.0]);
3353 }
3354
3355 #[test]
3356 fn test_byte_scan_stack_parses_realistic_coord_line() {
3357 let line = " 0.63939999999999997 0.90449999999999997 -0.00009999999999977 1 0";
3359 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
3360 let mut buf = [0.0f64; 5];
3361 let n = parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
3362 assert_eq!(n, 5);
3363 assert!((buf[0] - 0.6394).abs() < 1e-4);
3364 assert!((buf[1] - 0.9045).abs() < 1e-4);
3365 assert_eq!(buf[3] as u8, 1);
3366 assert_eq!(buf[4] as u64, 0);
3367 let bad = "1.0 2.0 3.0 1 0 EXTRA";
3369 assert!(parse_line_of_range_f64_stack(bad, 4, 5, &defaults, &mut buf).is_err());
3370 let glued = "1.0 2.0 3.0abc 1 0";
3372 assert!(parse_line_of_range_f64_stack(glued, 4, 5, &defaults, &mut buf).is_err());
3373 }
3374
3375 #[test]
3376 fn test_parse_line_of_range_f64_stack_matches_vec_api() {
3377 let defaults = [0.0, 0.0, 0.0, 0.0, 7.0];
3378 let line = "1.0 2.0 3.0 0.0";
3379 let mut buf = [0.0f64; 5];
3380 parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
3381 let via_vec = parse_line_of_range_f64(line, 4, 5, &defaults).unwrap();
3382 assert_eq!(&buf[..5], via_vec.as_slice());
3383 assert_eq!(buf[4], 7.0);
3384 }
3385
3386 #[test]
3387 fn test_parse_line_of_range_f64_padded() {
3388 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
3389 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0", 4, 5, &defaults).unwrap();
3390 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 99.0]);
3391 }
3392
3393 #[test]
3394 fn test_parse_line_of_range_f64_too_few() {
3395 let result = parse_line_of_range_f64("1.0 2.0 3.0", 4, 5, &[0.0; 5]);
3396 assert!(result.is_err());
3397 }
3398
3399 #[test]
3400 fn test_parse_line_of_range_f64_too_many() {
3401 let result = parse_line_of_range_f64("1.0 2.0 3.0 0.0 5.0 6.0", 4, 5, &[0.0; 5]);
3402 assert!(result.is_err());
3403 }
3404
3405 #[test]
3406 fn test_parse_all_four_column_lines() {
3407 let lines = [
3409 "PREBOX1",
3410 "{\"con_spec_version\":2}",
3411 "10.0 10.0 10.0",
3412 "90.0 90.0 90.0",
3413 "POSTBOX1",
3414 "POSTBOX2",
3415 "1",
3416 "3",
3417 "12.011",
3418 "C",
3419 "Coordinates of Component 1",
3420 "0.0 0.0 0.0 0",
3421 "1.0 0.0 0.0 0",
3422 "2.0 0.0 0.0 1",
3423 ];
3424 let mut line_it = lines.iter().copied();
3425 let frame = parse_single_frame(&mut line_it).unwrap();
3426 assert_eq!(frame.atom_data[0].atom_id, 0);
3427 assert_eq!(frame.atom_data[1].atom_id, 1);
3428 assert_eq!(frame.atom_data[2].atom_id, 2);
3429 assert!(frame.atom_data[2].is_fixed());
3430 }
3431}