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;
12
13pub trait LineStream<'a> {
18 fn next_line(&mut self) -> Option<&'a str>;
19 fn peek_line(&mut self) -> Option<&'a str>;
20 fn remainder_is_whitespace(&mut self) -> bool {
24 false
25 }
26 fn blank_line_opens_section(&mut self) -> bool {
30 false
31 }
32
33 fn blank_opens_velocity_block(&mut self) -> bool {
37 true
38 }
39
40 fn next_coord_row(
46 &mut self,
47 spec_version: u32,
48 default_id: u64,
49 ) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
50 let line = self.next_line().ok_or(ParseError::IncompleteFrame)?;
51 parse_coord_row(line, spec_version, default_id)
52 }
53}
54
55impl<'a, I> LineStream<'a> for Peekable<I>
56where
57 I: Iterator<Item = &'a str> + Clone,
58{
59 #[inline]
60 fn next_line(&mut self) -> Option<&'a str> {
61 Iterator::next(self)
62 }
63 #[inline]
64 fn peek_line(&mut self) -> Option<&'a str> {
65 Peekable::peek(self).copied()
66 }
67 fn blank_line_opens_section(&mut self) -> bool {
68 let mut probe = self.clone();
69 let Some(blank) = probe.next_line() else {
70 return false;
71 };
72 if !blank.trim().is_empty() {
73 return false;
74 }
75 section_follows(&mut probe)
76 }
77
78 fn blank_opens_velocity_block(&mut self) -> bool {
79 match self.peek_line() {
80 Some(line) if line.trim().is_empty() => self.blank_line_opens_section(),
81 _ => true,
82 }
83 }
84}
85
86pub(crate) fn line_has_component_label(line: &str) -> bool {
88 let mut words = line.split_ascii_whitespace();
89 while let Some(word) = words.next() {
90 if word.eq_ignore_ascii_case("of")
91 && words
92 .next()
93 .is_some_and(|next| next.eq_ignore_ascii_case("component"))
94 {
95 return true;
96 }
97 }
98 false
99}
100
101fn section_follows<'a>(lines: &mut impl LineStream<'a>) -> bool {
102 let Some(symbol) = lines.next_line() else {
103 return false;
104 };
105 if symbol.trim().is_empty() || symbol.trim_start().starts_with('{') {
106 return false;
107 }
108 let Some(label) = lines.next_line() else {
109 return false;
110 };
111 line_has_component_label(label)
112}
113
114#[inline]
123pub fn parse_line_of_range_f64_stack(
124 line: &str,
125 min: usize,
126 max: usize,
127 defaults: &[f64],
128 out: &mut [f64; 5],
129) -> Result<usize, ParseError> {
130 debug_assert!(max <= 5 && min <= max && defaults.len() >= max);
131 let bytes = line.as_bytes();
132 let n = bytes.len();
133 let mut i = 0usize;
134 let mut found = 0usize;
135 while found < max {
136 while i < n && bytes[i].is_ascii_whitespace() {
137 i += 1;
138 }
139 if i >= n {
140 break;
141 }
142 let end = bytes[i..]
143 .iter()
144 .position(|b| b.is_ascii_whitespace())
145 .map(|k| i + k)
146 .unwrap_or(n);
147 let token = std::str::from_utf8(&bytes[i..end]).unwrap_or("");
148 if is_nonfinite_spelling(token) {
149 return Err(ParseError::NonFiniteValue(token.to_string()));
150 }
151 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..])
152 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
153 let next = i + consumed;
154 if next != end {
156 return Err(ParseError::InvalidNumberFormat(format!(
157 "invalid float: {token}"
158 )));
159 }
160 if !val.is_finite() {
161 return Err(ParseError::NonFiniteValue(token.to_string()));
162 }
163 out[found] = val;
164 found += 1;
165 i = next;
166 }
167 while i < n && bytes[i].is_ascii_whitespace() {
168 i += 1;
169 }
170 if i < n {
172 return Err(ParseError::InvalidVectorLength {
173 expected: max,
174 found: found + 1,
175 });
176 }
177 if found < min || found > max {
178 return Err(ParseError::InvalidVectorLength {
179 expected: max,
180 found,
181 });
182 }
183 while found < max {
184 out[found] = defaults[found];
185 found += 1;
186 }
187 Ok(found)
188}
189
190pub fn parse_line_of_n_f64(line: &str, n: usize) -> Result<Vec<f64>, ParseError> {
202 if n <= 5 {
203 let defaults = [0.0f64; 5];
204 let mut buf = [0.0f64; 5];
205 parse_line_of_range_f64_stack(line, n, n, &defaults, &mut buf)?;
206 return Ok(buf[..n].to_vec());
207 }
208 let mut values = Vec::with_capacity(n);
209 for token in line.split_ascii_whitespace() {
210 values.push(parse_finite_float_token(token)?);
211 }
212 if values.len() == n {
213 Ok(values)
214 } else {
215 Err(ParseError::InvalidVectorLength {
216 expected: n,
217 found: values.len(),
218 })
219 }
220}
221
222pub fn parse_line_of_range_f64(
229 line: &str,
230 min: usize,
231 max: usize,
232 defaults: &[f64],
233) -> Result<Vec<f64>, ParseError> {
234 if max <= 5 {
235 let mut buf = [0.0f64; 5];
236 parse_line_of_range_f64_stack(line, min, max, defaults, &mut buf)?;
237 return Ok(buf[..max].to_vec());
238 }
239 let mut values = Vec::with_capacity(max);
240 for token in line.split_ascii_whitespace() {
241 values.push(parse_finite_float_token(token)?);
242 }
243 if values.len() < min || values.len() > max {
244 return Err(ParseError::InvalidVectorLength {
245 expected: max,
246 found: values.len(),
247 });
248 }
249 while values.len() < max {
250 let idx = values.len();
251 values.push(defaults[idx]);
252 }
253 Ok(values)
254}
255
256fn is_nonfinite_spelling(token: &str) -> bool {
260 let rest = token
261 .strip_prefix('+')
262 .or_else(|| token.strip_prefix('-'))
263 .unwrap_or(token);
264 match rest.as_bytes().first() {
265 Some(b) if b.is_ascii_digit() || *b == b'.' => return false,
266 None => return false,
267 _ => {}
268 }
269 rest.eq_ignore_ascii_case("nan")
270 || rest.eq_ignore_ascii_case("inf")
271 || rest.eq_ignore_ascii_case("infinity")
272}
273
274fn parse_finite_float_token(token: &str) -> Result<f64, ParseError> {
275 if is_nonfinite_spelling(token) {
276 return Err(ParseError::NonFiniteValue(token.to_string()));
277 }
278 let (val, consumed) = fast_float2::parse_partial::<f64, _>(token.as_bytes())
279 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
280 if consumed != token.len() {
281 return Err(ParseError::InvalidNumberFormat(format!(
282 "invalid float: {token}"
283 )));
284 }
285 if !val.is_finite() {
286 return Err(ParseError::NonFiniteValue(token.to_string()));
287 }
288 Ok(val)
289}
290
291fn skip_ascii_ws(bytes: &[u8], mut i: usize) -> usize {
292 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
293 i += 1;
294 }
295 i
296}
297
298fn take_token(line: &str, i: usize) -> Option<(&str, usize)> {
299 let bytes = line.as_bytes();
300 if i >= bytes.len() {
301 return None;
302 }
303 let mut j = i;
304 while j < bytes.len() && !bytes[j].is_ascii_whitespace() {
305 j += 1;
306 }
307 Some((&line[i..j], j))
308}
309
310fn parse_fixed_flag(token: &str, spec_version: u32) -> Result<[bool; 3], ParseError> {
313 let bad = || {
314 ParseError::InvalidFixedFlag(format!(
315 "column 4 flag {token:?} is not a legal integer for spec {spec_version}"
316 ))
317 };
318 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
319 return Err(bad());
320 }
321 let val: u64 = token.parse::<u64>().map_err(|_| bad())?;
322 if spec_version < 2 {
323 return match val {
324 0 => Ok([false, false, false]),
325 1 => Ok([true, true, true]),
326 _ => Err(bad()),
327 };
328 }
329 if val > 7 {
330 return Err(bad());
331 }
332 let v = val as u8;
333 Ok([v & 1 != 0, v & 2 != 0, v & 4 != 0])
334}
335
336fn parse_atom_id_token(token: &str) -> Result<u64, ParseError> {
339 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
340 return Err(ParseError::InvalidNumberFormat(format!(
341 "invalid integer: {token}"
342 )));
343 }
344 token.parse::<u64>().map_err(|_| {
345 ParseError::InvalidNumberFormat(format!("integer out of range 0..=2^64-1: {token}"))
346 })
347}
348
349fn parse_coord_row(
353 line: &str,
354 spec_version: u32,
355 default_id: u64,
356) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
357 parse_vector_row(line, spec_version, Some(default_id))
358}
359
360fn parse_vector_section_row(
362 line: &str,
363 spec_version: u32,
364) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
365 parse_vector_row(line, spec_version, None)
366}
367
368fn parse_vector_row(
369 line: &str,
370 spec_version: u32,
371 default_id: Option<u64>,
372) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
373 let (row, end) = parse_coord_fields(line.as_bytes(), 0, spec_version, default_id)?;
374 if end != line.len() {
375 let found = 5 + count_tokens_on_line(line.as_bytes(), end);
376 return Err(ParseError::InvalidVectorLength { expected: 5, found });
377 }
378 Ok(row)
379}
380
381#[inline(always)]
383fn is_field_ws(b: u8) -> bool {
384 matches!(b, b' ' | b'\t' | b'\r' | 0x0c)
385}
386
387#[inline(always)]
388fn skip_field_ws(bytes: &[u8], mut i: usize) -> usize {
389 while i < bytes.len() && is_field_ws(bytes[i]) {
390 i += 1;
391 }
392 i
393}
394
395fn token_on_line(bytes: &[u8], i: usize) -> String {
396 let mut j = i;
397 while j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
398 j += 1;
399 }
400 String::from_utf8_lossy(&bytes[i..j]).into_owned()
401}
402
403fn count_tokens_on_line(bytes: &[u8], mut i: usize) -> usize {
404 let mut n = 0usize;
405 loop {
406 i = skip_field_ws(bytes, i);
407 if i >= bytes.len() || bytes[i] == b'\n' {
408 break;
409 }
410 n += 1;
411 while i < bytes.len() && !is_field_ws(bytes[i]) && bytes[i] != b'\n' {
412 i += 1;
413 }
414 }
415 n
416}
417
418fn utf8_or_number(bytes: &[u8], line_start: usize, i: usize) -> ParseError {
420 let end = bytes[line_start..]
421 .iter()
422 .position(|b| *b == b'\n')
423 .map(|p| line_start + p)
424 .unwrap_or(bytes.len());
425 if std::str::from_utf8(&bytes[line_start..end]).is_err() {
426 return ParseError::ValidationError("invalid UTF-8".into());
427 }
428 ParseError::InvalidNumberFormat(format!("invalid float: {}", token_on_line(bytes, i)))
429}
430
431#[inline(always)]
432fn parse_f64_at(bytes: &[u8], i: usize, line_start: usize) -> Result<(f64, usize), ParseError> {
433 if i >= bytes.len() || bytes[i] == b'\n' {
434 return Err(ParseError::InvalidVectorLength {
435 expected: 5,
436 found: 0,
437 });
438 }
439 if bytes[i] >= 0x80 {
440 return Err(utf8_or_number(bytes, line_start, i));
441 }
442 if looks_like_nonfinite(bytes, i) {
443 let token = token_on_line(bytes, i);
444 if is_nonfinite_spelling(&token) {
445 return Err(ParseError::NonFiniteValue(token));
446 }
447 }
448 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..]).map_err(|_| {
449 ParseError::InvalidNumberFormat(format!("invalid float: {}", token_on_line(bytes, i)))
450 })?;
451 let next = i + consumed;
452 let boundary = next >= bytes.len() || is_field_ws(bytes[next]) || bytes[next] == b'\n';
453 if !boundary || !val.is_finite() {
454 let token = token_on_line(bytes, i);
455 if is_nonfinite_spelling(&token) || !val.is_finite() {
456 return Err(ParseError::NonFiniteValue(token));
457 }
458 return Err(ParseError::InvalidNumberFormat(format!(
459 "invalid float: {token}"
460 )));
461 }
462 Ok((val, next))
463}
464
465#[inline(always)]
466fn parse_flag_at(
467 bytes: &[u8],
468 i: usize,
469 spec_version: u32,
470) -> Result<([bool; 3], usize), ParseError> {
471 if i >= bytes.len() || bytes[i] == b'\n' {
472 return Err(ParseError::InvalidVectorLength {
473 expected: 5,
474 found: 3,
475 });
476 }
477 let bad = || {
478 ParseError::InvalidFixedFlag(format!(
479 "column 4 flag {:?} is not a legal integer for spec {spec_version}",
480 token_on_line(bytes, i)
481 ))
482 };
483 if !bytes[i].is_ascii_digit() {
484 return Err(bad());
485 }
486 let mut j = i;
487 let mut val: u64 = 0;
488 while j < bytes.len() && bytes[j].is_ascii_digit() {
489 let d = u64::from(bytes[j] - b'0');
490 val = val
491 .checked_mul(10)
492 .and_then(|v| v.checked_add(d))
493 .ok_or_else(bad)?;
494 j += 1;
495 }
496 if j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
497 return Err(bad());
498 }
499 let fixed = if spec_version < 2 {
500 match val {
501 0 => [false, false, false],
502 1 => [true, true, true],
503 _ => return Err(bad()),
504 }
505 } else if val <= 7 {
506 let v = val as u8;
507 [v & 1 != 0, v & 2 != 0, v & 4 != 0]
508 } else {
509 return Err(bad());
510 };
511 Ok((fixed, j))
512}
513
514#[inline(always)]
515fn parse_id_at(bytes: &[u8], i: usize) -> Result<(u64, usize), ParseError> {
516 if i >= bytes.len() || bytes[i] == b'\n' || !bytes[i].is_ascii_digit() {
517 return Err(ParseError::InvalidNumberFormat(format!(
518 "invalid integer: {}",
519 token_on_line(bytes, i)
520 )));
521 }
522 let mut j = i;
523 let mut val: u64 = 0;
524 while j < bytes.len() && bytes[j].is_ascii_digit() {
525 let d = u64::from(bytes[j] - b'0');
526 val = val
527 .checked_mul(10)
528 .and_then(|v| v.checked_add(d))
529 .ok_or_else(|| {
530 ParseError::InvalidNumberFormat(format!(
531 "integer out of range 0..=2^64-1: {}",
532 token_on_line(bytes, i)
533 ))
534 })?;
535 j += 1;
536 }
537 if j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
538 return Err(ParseError::InvalidNumberFormat(format!(
539 "invalid integer: {}",
540 token_on_line(bytes, i)
541 )));
542 }
543 Ok((val, j))
544}
545
546#[inline(always)]
553fn parse_coord_fields(
554 bytes: &[u8],
555 i: usize,
556 spec_version: u32,
557 default_id: Option<u64>,
558) -> Result<(([f64; 3], [bool; 3], u64), usize), ParseError> {
559 if let Some(parsed) = parse_coord_fields_fast(bytes, i, spec_version, default_id) {
560 return Ok(parsed);
561 }
562 parse_coord_fields_detailed(bytes, i, spec_version, default_id)
563}
564
565#[inline(always)]
567fn parse_coord_fields_fast(
568 bytes: &[u8],
569 i: usize,
570 spec_version: u32,
571 default_id: Option<u64>,
572) -> Option<(([f64; 3], [bool; 3], u64), usize)> {
573 let mut i = skip_field_ws(bytes, i);
574 let mut xyz = [0.0f64; 3];
575 for slot in 0..3 {
576 if i >= bytes.len() || bytes[i] == b'\n' || bytes[i] >= 0x80 {
577 return None;
578 }
579 if looks_like_nonfinite(bytes, i) {
580 return None;
581 }
582 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..]).ok()?;
583 if !val.is_finite() {
584 return None;
585 }
586 let next = i + consumed;
587 if next < bytes.len() && !is_field_ws(bytes[next]) && bytes[next] != b'\n' {
588 return None;
589 }
590 xyz[slot] = val;
591 i = skip_field_ws(bytes, next);
592 }
593 let (fixed, next) = parse_flag_fast(bytes, i, spec_version)?;
594 i = skip_field_ws(bytes, next);
595 let (atom_id, next) = if i >= bytes.len() || bytes[i] == b'\n' {
596 (default_id?, i)
597 } else {
598 parse_u64_fast(bytes, i)?
599 };
600 i = skip_field_ws(bytes, next);
601 if i < bytes.len() && bytes[i] != b'\n' {
602 return None;
603 }
604 if i < bytes.len() && bytes[i] == b'\n' {
605 i += 1;
606 }
607 Some(((xyz, fixed, atom_id), i))
608}
609
610#[inline(always)]
611fn parse_u64_fast(bytes: &[u8], i: usize) -> Option<(u64, usize)> {
612 if i >= bytes.len() || !bytes[i].is_ascii_digit() {
613 return None;
614 }
615 let mut j = i;
616 let mut val: u64 = 0;
617 let mut digits = 0u32;
618 while j < bytes.len() && bytes[j].is_ascii_digit() {
619 digits += 1;
620 if digits > 18 {
622 return None;
623 }
624 val = val * 10 + u64::from(bytes[j] - b'0');
625 j += 1;
626 }
627 if j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
628 return None;
629 }
630 Some((val, j))
631}
632
633#[inline(always)]
634fn parse_flag_fast(bytes: &[u8], i: usize, spec_version: u32) -> Option<([bool; 3], usize)> {
635 let (val, j) = parse_u64_fast(bytes, i)?;
636 let fixed = if spec_version < 2 {
637 match val {
638 0 => [false, false, false],
639 1 => [true, true, true],
640 _ => return None,
641 }
642 } else if val <= 7 {
643 let v = val as u8;
644 [v & 1 != 0, v & 2 != 0, v & 4 != 0]
645 } else {
646 return None;
647 };
648 Some((fixed, j))
649}
650
651#[cold]
652#[inline(never)]
653fn parse_coord_fields_detailed(
654 bytes: &[u8],
655 i: usize,
656 spec_version: u32,
657 default_id: Option<u64>,
658) -> Result<(([f64; 3], [bool; 3], u64), usize), ParseError> {
659 let line_start = i;
660 let mut i = skip_field_ws(bytes, i);
661 if i >= bytes.len() || bytes[i] == b'\n' {
662 return Err(ParseError::InvalidVectorLength {
663 expected: 5,
664 found: 0,
665 });
666 }
667 let mut xyz = [0.0f64; 3];
668 for slot in 0..3 {
669 if i >= bytes.len() || bytes[i] == b'\n' {
670 return Err(ParseError::InvalidVectorLength {
671 expected: 5,
672 found: slot,
673 });
674 }
675 if bytes[i] >= 0x80 {
676 return Err(utf8_or_number(bytes, line_start, i));
677 }
678 let (val, next) = parse_f64_at(bytes, i, line_start)?;
679 xyz[slot] = val;
680 i = skip_field_ws(bytes, next);
681 }
682 let (fixed, next) = parse_flag_at(bytes, i, spec_version)?;
683 i = skip_field_ws(bytes, next);
684 let (atom_id, next) = if i >= bytes.len() || bytes[i] == b'\n' {
685 (
686 default_id.ok_or(ParseError::InvalidVectorLength {
687 expected: 5,
688 found: 4,
689 })?,
690 i,
691 )
692 } else {
693 parse_id_at(bytes, i)?
694 };
695 i = skip_field_ws(bytes, next);
696 if i < bytes.len() && bytes[i] != b'\n' {
697 let found = 5 + count_tokens_on_line(bytes, i);
698 return Err(ParseError::InvalidVectorLength { expected: 5, found });
699 }
700 if i < bytes.len() && bytes[i] == b'\n' {
701 i += 1;
702 }
703 Ok(((xyz, fixed, atom_id), i))
704}
705
706pub(crate) fn parse_coord_row_at(
711 bytes: &[u8],
712 i: usize,
713 spec_version: u32,
714 default_id: u64,
715) -> Result<(([f64; 3], [bool; 3], u64), usize), ParseError> {
716 if i >= bytes.len() {
717 return Err(ParseError::IncompleteFrame);
718 }
719 parse_coord_fields(bytes, i, spec_version, Some(default_id))
720}
721
722fn parse_scalar_section_row(
724 line: &str,
725 spec_version: u32,
726) -> Result<(f64, [bool; 3], u64), ParseError> {
727 let bytes = line.as_bytes();
728 let i = skip_ascii_ws(bytes, 0);
729 let (value, i) = parse_finite_at(line, i)?;
730 let i = skip_ascii_ws(bytes, i);
731 let (flag_tok, i) = take_token(line, i).ok_or(ParseError::InvalidVectorLength {
732 expected: 3,
733 found: 1,
734 })?;
735 let fixed = parse_fixed_flag(flag_tok, spec_version)?;
736 let i = skip_ascii_ws(bytes, i);
737 let (id_tok, i) = take_token(line, i).ok_or(ParseError::InvalidVectorLength {
738 expected: 3,
739 found: 2,
740 })?;
741 let atom_id = parse_atom_id_token(id_tok)?;
742 let i = skip_ascii_ws(bytes, i);
743 if i < bytes.len() {
744 let found = 3 + line[i..].split_ascii_whitespace().count();
745 return Err(ParseError::InvalidVectorLength { expected: 3, found });
746 }
747 Ok((value, fixed, atom_id))
748}
749
750fn parse_finite_at(line: &str, i: usize) -> Result<(f64, usize), ParseError> {
751 let bytes = line.as_bytes();
752 if i >= bytes.len() {
753 return Err(ParseError::InvalidVectorLength {
754 expected: 3,
755 found: 0,
756 });
757 }
758 if looks_like_nonfinite(bytes, i) {
761 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
762 if is_nonfinite_spelling(token) {
763 return Err(ParseError::NonFiniteValue(token.to_string()));
764 }
765 }
766 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..]).map_err(|_| {
767 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
768 ParseError::InvalidNumberFormat(format!("invalid float: {token}"))
769 })?;
770 let next = i + consumed;
771 let boundary = next == bytes.len() || bytes[next].is_ascii_whitespace();
772 if !boundary || !val.is_finite() {
773 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
774 if is_nonfinite_spelling(token) || !val.is_finite() {
775 return Err(ParseError::NonFiniteValue(token.to_string()));
776 }
777 return Err(ParseError::InvalidNumberFormat(format!(
778 "invalid float: {token}"
779 )));
780 }
781 Ok((val, next))
782}
783
784fn looks_like_nonfinite(bytes: &[u8], i: usize) -> bool {
787 let mut j = i;
788 if bytes[j] == b'+' || bytes[j] == b'-' {
789 j += 1;
790 if j >= bytes.len() {
791 return false;
792 }
793 }
794 let b = bytes[j];
795 !b.is_ascii_digit() && b != b'.'
796}
797
798fn labels_equal(found: &str, expected: &str) -> bool {
800 let mut found_words = found.split_ascii_whitespace();
801 let mut expected_words = expected.split_ascii_whitespace();
802 loop {
803 match (found_words.next(), expected_words.next()) {
804 (Some(a), Some(b)) => {
805 if !a.eq_ignore_ascii_case(b) {
806 return false;
807 }
808 }
809 (None, None) => return true,
810 _ => return false,
811 }
812 }
813}
814
815fn label_has_prefix(line: &str, prefix: &str) -> bool {
817 let mut found_words = line.split_ascii_whitespace();
818 for word in prefix.split_ascii_whitespace() {
819 match found_words.next() {
820 Some(got) if got.eq_ignore_ascii_case(word) => {}
821 _ => return false,
822 }
823 }
824 true
825}
826
827fn token_count(line: &str) -> usize {
828 line.split_ascii_whitespace().count()
829}
830
831fn json_u64(value: &Value) -> Option<u64> {
832 value.as_u64()
833}
834
835fn validate_section_arity_value(value: &Value) -> Result<(), ParseError> {
836 let obj = value.as_object().ok_or_else(|| {
837 metadata_json_error("section_arity must be an object mapping section names to integers")
838 })?;
839 for (name, entry) in obj {
840 let n = json_u64(entry).ok_or_else(|| {
841 metadata_json_error(format!("section_arity.{name} must be an integer >= 1"))
842 })?;
843 if n < 1 {
844 return Err(ParseError::InvalidCount(format!(
845 "section_arity.{name} must be >= 1"
846 )));
847 }
848 if let Some(expect) = crate::types::reserved_section_value_arity(name)
850 && n != expect
851 {
852 return Err(ParseError::InvalidVectorLength {
853 expected: usize::try_from(expect).unwrap_or(usize::MAX),
854 found: usize::try_from(n).unwrap_or(usize::MAX),
855 });
856 }
857 }
858 Ok(())
859}
860
861pub(crate) fn parse_count_line(line: &str, n: usize) -> Result<Vec<usize>, ParseError> {
865 let mut values = Vec::with_capacity(n);
866 for token in line.split_ascii_whitespace() {
867 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
868 return Err(ParseError::InvalidCount(format!(
869 "count field is not an unsigned integer: {token}"
870 )));
871 }
872 let val: u64 = token.parse::<u64>().map_err(|_| {
873 ParseError::InvalidCount(format!("count field is out of range: {token}"))
874 })?;
875 let val = usize::try_from(val).map_err(|_| {
876 ParseError::InvalidCount(format!("count field is out of range: {token}"))
877 })?;
878 values.push(val);
879 }
880 if values.len() == n {
881 Ok(values)
882 } else {
883 Err(ParseError::InvalidVectorLength {
884 expected: n,
885 found: values.len(),
886 })
887 }
888}
889
890pub(crate) fn line_is_whitespace(line: &str) -> bool {
892 line.split_ascii_whitespace().next().is_none()
893}
894
895pub(crate) fn read_zero_atom_tail<'a>(
901 mut next_line: impl FnMut() -> Option<&'a str>,
902) -> Result<(), ParseError> {
903 let bad = || {
904 ParseError::InvalidCount("a zero-atom frame requires whitespace-only lines 8 and 9".into())
905 };
906 match next_line() {
907 None => Ok(()),
908 Some(line) if line_is_whitespace(line) => match next_line() {
909 None => Ok(()),
910 Some(line) if line_is_whitespace(line) => Ok(()),
911 Some(_) => Err(bad()),
912 },
913 Some(_) => Err(bad()),
914 }
915}
916
917fn metadata_json_error(message: impl Into<String>) -> ParseError {
918 ParseError::InvalidMetadataJson(message.into())
919}
920
921fn validate_metadata_number(key: &str, value: &Value) -> Result<(), ParseError> {
922 if value.as_f64().is_some() {
923 Ok(())
924 } else {
925 Err(metadata_json_error(format!(
926 "{key} must be a finite number"
927 )))
928 }
929}
930
931fn validate_metadata_integer(key: &str, value: &Value) -> Result<(), ParseError> {
932 if value.as_u64().is_some() {
933 Ok(())
934 } else {
935 Err(metadata_json_error(format!(
936 "{key} must be a non-negative integer"
937 )))
938 }
939}
940
941pub fn validate_metadata_schema(
949 json_obj: &serde_json::Map<String, Value>,
950) -> Result<(), ParseError> {
951 let strict_requested = match json_obj.get(meta::VALIDATE) {
952 Some(Value::Bool(b)) => *b,
953 Some(_) => return Err(metadata_json_error("validate must be a boolean")),
954 None => false,
955 };
956
957 match json_obj.get(meta::SECTIONS) {
958 Some(Value::Array(values)) => {
959 if values.iter().any(|entry| !entry.is_string()) {
960 return Err(metadata_json_error("sections must be an array of strings"));
961 }
962 }
963 Some(_) => return Err(metadata_json_error("sections must be an array of strings")),
964 None if strict_requested => {
965 return Err(metadata_json_error(
966 "validate=true requires a sections array, even when empty",
967 ));
968 }
969 None => {}
970 }
971
972 for (key, value) in json_obj {
973 match key.as_str() {
974 meta::CON_SPEC_VERSION | meta::SECTIONS | meta::VALIDATE => {}
975 meta::CON_SPEC_MINOR => {
976 if json_u64(value).is_none() {
977 return Err(metadata_json_error(
978 "con_spec_minor must be a JSON integer >= 0",
979 ));
980 }
981 }
982 meta::SECTION_ARITY => validate_section_arity_value(value)?,
983 meta::ENERGY
984 | meta::TIME
985 | meta::TIMESTEP
986 | meta::CONVERGENCE_FMAX
987 | meta::CONVERGENCE_ENERGY
988 | meta::FMAX => validate_metadata_number(key, value)?,
989 meta::FRAME_INDEX | meta::NEB_BEAD | meta::NEB_BAND => {
990 validate_metadata_integer(key, value)?
991 }
992 meta::GENERATOR if !value.is_string() => {
993 return Err(metadata_json_error("generator must be a string"));
994 }
995 meta::GENERATOR => {}
996 meta::UNITS | meta::POTENTIAL if !value.is_object() => {
997 return Err(metadata_json_error(format!("{key} must be an object")));
998 }
999 meta::POTENTIAL => {
1000 if let Some(potential_type) = value.get("type")
1001 && !potential_type.is_string()
1002 {
1003 return Err(metadata_json_error("potential.type must be a string"));
1004 }
1005 }
1006 meta::UNITS => {
1007 }
1009 meta::PBC => validate_pbc_metadata(value)?,
1010 meta::BONDS => validate_bonds_metadata(value)?,
1011 meta::LATTICE_VECTORS => validate_lattice_vectors_metadata(value)?,
1012 meta::CONVERGED if !value.is_boolean() => {
1013 return Err(metadata_json_error("converged must be a boolean"));
1014 }
1015 meta::CONVERGED => {}
1016 _ => {}
1017 }
1018 }
1019
1020 Ok(())
1021}
1022
1023fn validate_pbc_metadata(value: &Value) -> Result<(), ParseError> {
1024 let Some(values) = value.as_array() else {
1025 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
1026 };
1027 if values.len() != 3 || values.iter().any(|entry| !entry.is_boolean()) {
1028 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
1029 }
1030 Ok(())
1031}
1032
1033fn validate_bonds_metadata(value: &Value) -> Result<(), ParseError> {
1040 let Some(items) = value.as_array() else {
1041 return Err(metadata_json_error("bonds must be an array"));
1042 };
1043 for (idx, item) in items.iter().enumerate() {
1044 if let Some(pair) = item.as_array() {
1045 if pair.len() != 2 {
1046 return Err(metadata_json_error(format!(
1047 "bonds[{idx}] pair must have exactly two indices"
1048 )));
1049 }
1050 for (k, entry) in pair.iter().enumerate() {
1051 let Some(n) = entry.as_u64() else {
1052 return Err(metadata_json_error(format!(
1053 "bonds[{idx}][{k}] must be a non-negative integer"
1054 )));
1055 };
1056 if n > u32::MAX as u64 {
1057 return Err(metadata_json_error(format!(
1058 "bonds[{idx}][{k}] index exceeds u32"
1059 )));
1060 }
1061 }
1062 continue;
1063 }
1064 if let Some(obj) = item.as_object() {
1065 for key in ["i", "j"] {
1066 let Some(entry) = obj.get(key) else {
1067 return Err(metadata_json_error(format!(
1068 "bonds[{idx}] object must include \"{key}\""
1069 )));
1070 };
1071 let Some(n) = entry.as_u64() else {
1072 return Err(metadata_json_error(format!(
1073 "bonds[{idx}].{key} must be a non-negative integer"
1074 )));
1075 };
1076 if n > u32::MAX as u64 {
1077 return Err(metadata_json_error(format!(
1078 "bonds[{idx}].{key} index exceeds u32"
1079 )));
1080 }
1081 }
1082 if let Some(order) = obj.get("order")
1083 && order.as_i64().is_none()
1084 {
1085 return Err(metadata_json_error(format!(
1086 "bonds[{idx}].order must be an integer when present"
1087 )));
1088 }
1089 continue;
1090 }
1091 return Err(metadata_json_error(format!(
1092 "bonds[{idx}] must be [i, j] or {{\"i\": i, \"j\": j, \"order\"?: ...}}"
1093 )));
1094 }
1095 Ok(())
1096}
1097
1098fn validate_lattice_vectors_metadata(value: &Value) -> Result<(), ParseError> {
1099 let Some(rows) = value.as_array() else {
1100 return Err(metadata_json_error(
1101 "lattice_vectors must be a 3x3 numeric array",
1102 ));
1103 };
1104 if rows.len() != 3 {
1105 return Err(metadata_json_error(
1106 "lattice_vectors must be a 3x3 numeric array",
1107 ));
1108 }
1109 for row in rows {
1110 let Some(entries) = row.as_array() else {
1111 return Err(metadata_json_error(
1112 "lattice_vectors must be a 3x3 numeric array",
1113 ));
1114 };
1115 if entries.len() != 3 || entries.iter().any(|entry| entry.as_f64().is_none()) {
1116 return Err(metadata_json_error(
1117 "lattice_vectors must be a 3x3 numeric array",
1118 ));
1119 }
1120 }
1121 Ok(())
1122}
1123
1124pub fn parse_line_of_n<T: std::str::FromStr>(line: &str, n: usize) -> Result<Vec<T>, ParseError>
1152where
1153 ParseError: From<<T as std::str::FromStr>::Err>,
1154{
1155 let values: Vec<T> = line
1156 .split_whitespace()
1157 .map(|s| s.parse::<T>())
1158 .collect::<Result<_, _>>()?;
1159
1160 if values.len() == n {
1161 Ok(values)
1162 } else {
1163 Err(ParseError::InvalidVectorLength {
1164 expected: n,
1165 found: values.len(),
1166 })
1167 }
1168}
1169
1170pub fn parse_frame_header<'a>(
1191 lines: &mut impl Iterator<Item = &'a str>,
1192) -> Result<FrameHeader, ParseError> {
1193 let prebox1 = lines
1194 .next()
1195 .ok_or(ParseError::IncompleteHeader)?
1196 .to_string();
1197 if prebox1.starts_with('\u{feff}') {
1199 return Err(ParseError::ValidationError(
1200 "file begins with a byte-order mark".into(),
1201 ));
1202 }
1203 let prebox2_raw = lines.next().ok_or(ParseError::IncompleteHeader)?;
1204
1205 let trimmed = prebox2_raw.trim();
1210 let (spec_version, metadata, sections, validate, sections_declared) =
1211 if trimmed.starts_with('{') {
1212 let json_val: serde_json::Value = serde_json::from_str(trimmed)
1213 .map_err(|e| ParseError::InvalidMetadataJson(e.to_string()))?;
1214 let json_obj = json_val.as_object().ok_or_else(|| {
1215 ParseError::InvalidMetadataJson("expected a JSON object".to_string())
1216 })?;
1217 let ver_value = json_obj
1218 .get(meta::CON_SPEC_VERSION)
1219 .ok_or(ParseError::MissingSpecVersion)?;
1220 let ver = json_u64(ver_value).ok_or_else(|| {
1221 ParseError::InvalidMetadataJson(
1222 "con_spec_version must be a JSON integer, the major version".into(),
1223 )
1224 })?;
1225 if ver > u64::from(u32::MAX) {
1226 return Err(ParseError::UnsupportedVersion(u32::MAX));
1227 }
1228 let ver = ver as u32;
1229 if ver == 0 || ver == 1 || ver > crate::CON_SPEC_VERSION {
1233 return Err(ParseError::UnsupportedVersion(ver));
1234 }
1235 if ver >= 3 {
1236 match json_obj.get(meta::UNITS) {
1237 Some(u) => crate::units::validate_v3_units_metadata(u)
1238 .map_err(|e| ParseError::InvalidUnits(format!("v3 units: {e}")))?,
1239 None => {
1240 return Err(ParseError::InvalidUnits(
1241 "con_spec_version >= 3 requires metadata \"units\" with length and energy"
1242 .into(),
1243 ));
1244 }
1245 }
1246 }
1247
1248 let mut sections: Vec<String> = Vec::new();
1252 let mut metadata = BTreeMap::new();
1253 let mut sections_declared = false;
1254 let mut validate = false;
1255 for (k, v) in json_obj {
1256 match k.as_str() {
1257 meta::CON_SPEC_VERSION => {}
1258 meta::CON_SPEC_MINOR => {
1259 if json_u64(v).is_none() {
1260 return Err(metadata_json_error(
1261 "con_spec_minor must be a JSON integer >= 0",
1262 ));
1263 }
1264 metadata.insert(k.clone(), v.clone());
1265 }
1266 meta::SECTION_ARITY => {
1267 validate_section_arity_value(v)?;
1268 metadata.insert(k.clone(), v.clone());
1269 }
1270 meta::SECTIONS => {
1271 sections_declared = true;
1272 let arr = v.as_array().ok_or_else(|| {
1273 metadata_json_error("sections must be an array of strings")
1274 })?;
1275 sections.reserve(arr.len());
1276 for entry in arr {
1277 let s = entry.as_str().ok_or_else(|| {
1278 metadata_json_error("sections must be an array of strings")
1279 })?;
1280 sections.push(s.to_string());
1281 }
1282 }
1283 meta::VALIDATE => {
1284 validate = match v {
1285 Value::Bool(b) => *b,
1286 _ => return Err(metadata_json_error("validate must be a boolean")),
1287 };
1288 metadata.insert(k.clone(), v.clone());
1289 }
1290 _ => {
1291 metadata.insert(k.clone(), v.clone());
1292 }
1293 }
1294 }
1295
1296 if validate {
1299 validate_metadata_schema(json_obj)?;
1300 }
1301
1302 (ver, metadata, sections, validate, sections_declared)
1303 } else {
1304 (1_u32, BTreeMap::new(), Vec::new(), false, false)
1306 };
1307 let prebox2 = prebox2_raw.to_string();
1308
1309 let boxl_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
1310 let angles_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
1311 let postbox1 = lines
1312 .next()
1313 .ok_or(ParseError::IncompleteHeader)?
1314 .to_string();
1315 let postbox2 = lines
1316 .next()
1317 .ok_or(ParseError::IncompleteHeader)?
1318 .to_string();
1319 let natm_types_line = lines.next().ok_or(ParseError::IncompleteHeader)?;
1320 let natm_types = parse_count_line(natm_types_line, 1)?[0];
1321 let (natms_per_type, masses_per_type) = if natm_types == 0 {
1324 read_zero_atom_tail(|| lines.next())?;
1325 (Vec::new(), Vec::new())
1326 } else {
1327 let natms_per_type = parse_count_line(
1328 lines.next().ok_or(ParseError::IncompleteHeader)?,
1329 natm_types,
1330 )?;
1331 if natms_per_type.iter().any(|n| *n == 0) {
1332 return Err(ParseError::InvalidCount(
1333 "each per-type count must be >= 1".into(),
1334 ));
1335 }
1336 let masses_per_type = parse_line_of_n_f64(
1337 lines.next().ok_or(ParseError::IncompleteHeader)?,
1338 natm_types,
1339 )?;
1340 (natms_per_type, masses_per_type)
1341 };
1342 if validate {
1343 validate_header_geometry(&boxl_vec, &angles_vec)?;
1344 validate_masses(&masses_per_type)?;
1345 }
1346 Ok(FrameHeader {
1347 prebox_header: PreboxHeader {
1348 user: prebox1,
1349 metadata_line: prebox2,
1350 },
1351 boxl: boxl_vec.try_into().unwrap(),
1352 angles: angles_vec.try_into().unwrap(),
1353 postbox_header: [postbox1, postbox2],
1354 natm_types,
1355 natms_per_type,
1356 masses_per_type,
1357 spec_version,
1358 metadata,
1359 sections,
1360 strict_validation: validate,
1361 sections_declared,
1362 })
1363}
1364
1365pub fn parse_single_frame<'a>(
1414 lines: &mut impl Iterator<Item = &'a str>,
1415) -> Result<ConFrame, ParseError> {
1416 parse_single_frame_ls(&mut IterStream(lines))
1420}
1421
1422struct IterStream<I>(I);
1425
1426impl<'a, I> Iterator for IterStream<I>
1427where
1428 I: Iterator<Item = &'a str>,
1429{
1430 type Item = &'a str;
1431 fn next(&mut self) -> Option<&'a str> {
1432 self.0.next()
1433 }
1434}
1435
1436impl<'a, I> LineStream<'a> for IterStream<I>
1437where
1438 I: Iterator<Item = &'a str>,
1439{
1440 fn next_line(&mut self) -> Option<&'a str> {
1441 Iterator::next(self)
1442 }
1443 fn peek_line(&mut self) -> Option<&'a str> {
1444 None
1445 }
1446}
1447
1448pub(crate) fn parse_single_frame_ls<'a, L>(lines: &mut L) -> Result<ConFrame, ParseError>
1455where
1456 L: LineStream<'a> + Iterator<Item = &'a str>,
1457{
1458 let header = parse_frame_header(lines)?;
1459 let validate = header.strict_validation;
1460 let total_atoms: usize = header.natms_per_type.iter().sum();
1461 use crate::storage_dtype::{ElementKind, FloatArray2, StorageDtypes};
1462 let dt = StorageDtypes::from_metadata(&header.metadata).unwrap_or_default();
1463 let f64_positions = dt.positions == ElementKind::Float64;
1464 let mut pos_flat = Vec::with_capacity(total_atoms.saturating_mul(3));
1465 let mut positions_other = if f64_positions {
1466 None
1467 } else {
1468 Some(FloatArray2::zeros(dt.positions, total_atoms, 3))
1469 };
1470 let mut ids = Vec::with_capacity(total_atoms);
1471 let mut atom_data: Vec<AtomDatum> = Vec::with_capacity(total_atoms);
1472
1473 let mut global_atom_idx: u64 = 0;
1474 for (type_idx, num_atoms) in header.natms_per_type.iter().enumerate() {
1475 let symbol_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
1476 let symbol: Arc<str> = Arc::from(symbol_line.trim());
1477 let coord_label = lines.next().ok_or(ParseError::IncompleteFrame)?;
1478 if validate {
1479 validate_coordinate_component(type_idx, symbol.as_ref(), coord_label)?;
1480 }
1481 global_atom_idx = ingest_coord_rows(
1482 lines,
1483 *num_atoms,
1484 header.spec_version,
1485 global_atom_idx,
1486 &symbol,
1487 &mut pos_flat,
1488 &mut ids,
1489 &mut positions_other,
1490 &mut atom_data,
1491 )?;
1492 }
1493 let positions = if f64_positions {
1494 FloatArray2::from_f64_row_major(total_atoms, 3, pos_flat)
1495 } else {
1496 positions_other.expect("non-f64 positions allocated")
1497 };
1498 Ok(crate::types::con_frame_coords_only_with_ids(
1499 header, atom_data, positions, ids,
1500 ))
1501}
1502
1503#[inline(always)]
1505fn ingest_coord_rows<'a, L>(
1506 lines: &mut L,
1507 n: usize,
1508 spec_version: u32,
1509 mut global_atom_idx: u64,
1510 symbol: &Arc<str>,
1511 pos_flat: &mut Vec<f64>,
1512 ids: &mut Vec<u64>,
1513 positions_other: &mut Option<crate::storage_dtype::FloatArray2>,
1514 atom_data: &mut Vec<AtomDatum>,
1515) -> Result<u64, ParseError>
1516where
1517 L: LineStream<'a>,
1518{
1519 for _ in 0..n {
1520 let (xyz, fixed, atom_id) = lines.next_coord_row(spec_version, global_atom_idx)?;
1521 if let Some(pos) = positions_other.as_mut() {
1522 pos.set_f64_row(ids.len(), xyz);
1523 }
1524 pos_flat.push(xyz[0]);
1525 pos_flat.push(xyz[1]);
1526 pos_flat.push(xyz[2]);
1527 ids.push(atom_id);
1528 atom_data.push(AtomDatum::coords(
1529 Arc::clone(symbol),
1530 xyz[0],
1531 xyz[1],
1532 xyz[2],
1533 fixed,
1534 atom_id,
1535 ));
1536 global_atom_idx += 1;
1537 }
1538 Ok(global_atom_idx)
1539}
1540
1541fn validate_header_geometry(boxl: &[f64], angles: &[f64]) -> Result<(), ParseError> {
1542 if boxl
1543 .iter()
1544 .any(|length| !length.is_finite() || *length <= 0.0)
1545 || angles
1546 .iter()
1547 .any(|angle| !angle.is_finite() || *angle <= 0.0 || *angle >= 180.0)
1548 {
1549 return Err(ParseError::ValidationError(
1550 "cell geometry must have positive lengths and angles between 0 and 180 degrees"
1551 .to_string(),
1552 ));
1553 }
1554 Ok(())
1555}
1556
1557fn validate_masses(masses_per_type: &[f64]) -> Result<(), ParseError> {
1558 if masses_per_type
1559 .iter()
1560 .any(|mass| !mass.is_finite() || *mass <= 0.0)
1561 {
1562 return Err(ParseError::ValidationError(
1563 "component masses must be positive".to_string(),
1564 ));
1565 }
1566 Ok(())
1567}
1568
1569fn validate_coordinate_component(
1570 type_idx: usize,
1571 symbol: &str,
1572 label: &str,
1573) -> Result<(), ParseError> {
1574 let expected_label = format!("Coordinates of Component {}", type_idx + 1);
1575 if !labels_equal(label, &expected_label) {
1576 return Err(ParseError::ValidationError(format!(
1577 "expected coordinate label {expected_label:?}, found {label:?}"
1578 )));
1579 }
1580 if symbol != "X" && symbol_to_atomic_number(symbol) == 0 {
1581 return Err(ParseError::ValidationError(format!(
1582 "unknown component symbol {symbol}"
1583 )));
1584 }
1585 Ok(())
1586}
1587
1588fn validate_section_component(
1589 section: &str,
1590 type_idx: usize,
1591 atom_idx: usize,
1592 symbol: &str,
1593 label: &str,
1594 header: &FrameHeader,
1595 atom_data: &[AtomDatum],
1596) -> Result<(), ParseError> {
1597 let expected_label = format!("{section} of Component {}", type_idx + 1);
1598 if !labels_equal(label, &expected_label) {
1599 return Err(ParseError::ValidationError(format!(
1600 "expected section label {expected_label:?}, found {label:?}"
1601 )));
1602 }
1603
1604 if header.natms_per_type[type_idx] == 0 {
1605 return Ok(());
1606 }
1607
1608 let expected_symbol = atom_data
1609 .get(atom_idx)
1610 .map(|atom| atom.symbol.as_ref())
1611 .ok_or_else(|| {
1612 ParseError::ValidationError(format!(
1613 "{section} component {} has no coordinate atom to validate against",
1614 type_idx + 1
1615 ))
1616 })?;
1617 if symbol != expected_symbol {
1618 return Err(ParseError::ValidationError(format!(
1619 "{section} component {} symbol mismatch: expected {expected_symbol}, found {symbol}",
1620 type_idx + 1
1621 )));
1622 }
1623
1624 Ok(())
1625}
1626
1627fn validate_section_atom_identity(
1628 section: &str,
1629 atom_idx: usize,
1630 fixed: [bool; 3],
1631 atom_id: u64,
1632 atom_data: &[AtomDatum],
1633) -> Result<(), ParseError> {
1634 let atom = atom_data.get(atom_idx).ok_or_else(|| {
1635 ParseError::ValidationError(format!(
1636 "{section} row {atom_idx} has no coordinate atom to validate against"
1637 ))
1638 })?;
1639
1640 if atom.fixed != fixed {
1641 return Err(ParseError::ValidationError(format!(
1642 "{section} row {atom_idx} fixed mask mismatch for atom_id {}",
1643 atom.atom_id
1644 )));
1645 }
1646 if atom.atom_id != atom_id {
1647 return Err(ParseError::ValidationError(format!(
1648 "{section} row {atom_idx} atom_id mismatch: expected {}, found {atom_id}",
1649 atom.atom_id
1650 )));
1651 }
1652
1653 Ok(())
1654}
1655
1656fn frame_minor(header: &FrameHeader) -> u64 {
1661 header
1662 .metadata
1663 .get(meta::CON_SPEC_MINOR)
1664 .and_then(|v| v.as_u64())
1665 .unwrap_or(0)
1666}
1667
1668fn section_row_arity(header: &FrameHeader, name: &str) -> Option<usize> {
1669 let obj = header.metadata.get(meta::SECTION_ARITY)?.as_object()?;
1670 let n = obj.get(name)?.as_u64()?;
1671 if n >= 1 { Some(n as usize) } else { None }
1672}
1673
1674fn skip_unknown_section<'a>(
1681 lines: &mut impl LineStream<'a>,
1682 header: &FrameHeader,
1683 name: &str,
1684 arity: usize,
1685) -> Result<(), ParseError> {
1686 if !open_section(lines)? {
1687 return Err(ParseError::IncompleteSection(name.to_string()));
1688 }
1689 let with_flag = arity.saturating_add(1);
1690 let with_id = arity.saturating_add(2);
1691 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1692 if lines.next_line().is_none() {
1693 return Err(ParseError::IncompleteSection(name.to_string()));
1694 }
1695 let label = lines
1696 .next_line()
1697 .ok_or_else(|| ParseError::IncompleteSection(name.to_string()))?;
1698 let expected = format!("{name} of Component {}", type_idx + 1);
1699 if !labels_equal(label, &expected) {
1700 return Err(ParseError::IncompleteSection(name.to_string()));
1701 }
1702 for _ in 0..num_atoms {
1703 let row = lines
1704 .next_line()
1705 .ok_or_else(|| ParseError::IncompleteSection(name.to_string()))?;
1706 let found = token_count(row);
1707 if found != with_flag && found != with_id {
1708 let expected = if found < with_flag {
1709 with_flag
1710 } else {
1711 with_id
1712 };
1713 return Err(ParseError::InvalidVectorLength { expected, found });
1714 }
1715 }
1716 }
1717 Ok(())
1718}
1719
1720fn open_section<'a>(lines: &mut impl LineStream<'a>) -> Result<bool, ParseError> {
1721 match lines.peek_line() {
1722 Some(line) if line.trim().is_empty() => {
1723 if lines.remainder_is_whitespace() {
1724 return Ok(false);
1725 }
1726 lines.next_line();
1727 Ok(true)
1728 }
1729 _ => Ok(false),
1730 }
1731}
1732
1733pub fn parse_velocity_section<'a>(
1746 lines: &mut impl LineStream<'a>,
1747 header: &FrameHeader,
1748 atom_data: &mut [AtomDatum],
1749) -> Result<bool, ParseError> {
1750 let validate = header.strict_validation;
1751 match lines.peek_line() {
1754 Some(line) if line.trim().is_empty() => {
1755 if lines.remainder_is_whitespace() {
1756 return Ok(false);
1757 }
1758 if !lines.blank_line_opens_section() {
1762 return Ok(false);
1763 }
1764 lines.next_line();
1765 }
1766 _ => return Ok(false),
1767 }
1768
1769 let mut atom_idx: usize = 0;
1770 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1771 let symbol = lines
1773 .next_line()
1774 .ok_or(ParseError::IncompleteSection(SECTION_VELOCITIES.into()))?
1775 .trim();
1776
1777 let comp_line = lines
1779 .next_line()
1780 .ok_or(ParseError::IncompleteSection(SECTION_VELOCITIES.into()))?;
1781 if !label_has_prefix(comp_line, "Velocities of Component") {
1782 return Err(ParseError::IncompleteSection(SECTION_VELOCITIES.into()));
1783 }
1784 if validate {
1785 validate_section_component(
1786 "Velocities",
1787 type_idx,
1788 atom_idx,
1789 symbol,
1790 comp_line,
1791 header,
1792 atom_data,
1793 )?;
1794 }
1795
1796 for _ in 0..num_atoms {
1797 let vel_line = lines
1798 .next_line()
1799 .ok_or(ParseError::IncompleteSection(SECTION_VELOCITIES.into()))?;
1800 let (vals, fixed, atom_id) = parse_vector_section_row(vel_line, header.spec_version)?;
1801 if validate {
1802 validate_section_atom_identity("velocities", atom_idx, fixed, atom_id, atom_data)?;
1803 }
1804 if atom_idx < atom_data.len() {
1805 atom_data[atom_idx].set_velocity(Some(vals));
1806 }
1807 atom_idx += 1;
1808 }
1809 }
1810
1811 Ok(true)
1812}
1813
1814pub fn parse_force_section<'a>(
1822 lines: &mut impl LineStream<'a>,
1823 header: &FrameHeader,
1824 atom_data: &mut [AtomDatum],
1825) -> Result<bool, ParseError> {
1826 let validate = header.strict_validation;
1827 if !open_section(lines)? {
1828 return Ok(false);
1829 }
1830
1831 let mut atom_idx: usize = 0;
1832 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1833 let symbol = lines
1834 .next_line()
1835 .ok_or(ParseError::IncompleteSection(SECTION_FORCES.into()))?
1836 .trim();
1837
1838 let comp_line = lines
1839 .next_line()
1840 .ok_or(ParseError::IncompleteSection(SECTION_FORCES.into()))?;
1841 if !label_has_prefix(comp_line, "Forces of Component") {
1842 return Err(ParseError::IncompleteSection(SECTION_FORCES.into()));
1843 }
1844 if validate {
1845 validate_section_component(
1846 "Forces", type_idx, atom_idx, symbol, comp_line, header, atom_data,
1847 )?;
1848 }
1849
1850 for _ in 0..num_atoms {
1851 let force_line = lines
1852 .next_line()
1853 .ok_or(ParseError::IncompleteSection(SECTION_FORCES.into()))?;
1854 let (vals, fixed, atom_id) = parse_vector_section_row(force_line, header.spec_version)?;
1855 if validate {
1856 validate_section_atom_identity("forces", atom_idx, fixed, atom_id, atom_data)?;
1857 }
1858 if atom_idx < atom_data.len() {
1859 atom_data[atom_idx].set_force(Some(vals));
1860 }
1861 atom_idx += 1;
1862 }
1863 }
1864
1865 Ok(true)
1866}
1867
1868pub fn parse_energy_section<'a>(
1881 lines: &mut impl LineStream<'a>,
1882 header: &FrameHeader,
1883 atom_data: &mut [AtomDatum],
1884) -> Result<bool, ParseError> {
1885 let validate = header.strict_validation;
1886 if !open_section(lines)? {
1887 return Ok(false);
1888 }
1889
1890 let mut atom_idx: usize = 0;
1891 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1892 let symbol = lines
1893 .next_line()
1894 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?
1895 .trim();
1896
1897 let comp_line = lines
1898 .next_line()
1899 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?;
1900 if !label_has_prefix(comp_line, "Energies of Component") {
1901 return Err(ParseError::IncompleteSection(SECTION_ENERGIES.into()));
1902 }
1903 if validate {
1904 validate_section_component(
1905 "Energies", type_idx, atom_idx, symbol, comp_line, header, atom_data,
1906 )?;
1907 }
1908
1909 for _ in 0..num_atoms {
1910 let energy_line = lines
1911 .next_line()
1912 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?;
1913 let (value, fixed, atom_id) =
1914 parse_scalar_section_row(energy_line, header.spec_version)?;
1915 if validate {
1916 validate_section_atom_identity("energies", atom_idx, fixed, atom_id, atom_data)?;
1917 }
1918 if atom_idx < atom_data.len() {
1919 atom_data[atom_idx].set_energy(Some(value));
1920 }
1921 atom_idx += 1;
1922 }
1923 }
1924
1925 Ok(true)
1926}
1927
1928pub fn parse_declared_sections<'a>(
1934 lines: &mut impl LineStream<'a>,
1935 header: &mut FrameHeader,
1936 atom_data: &mut [AtomDatum],
1937) -> Result<usize, ParseError> {
1938 let mut applied = 0usize;
1939 if header.natm_types == 0 {
1942 if !header.sections.is_empty() {
1943 return Err(ParseError::InvalidCount(
1944 "a zero-atom frame cannot declare sections".into(),
1945 ));
1946 }
1947 return Ok(0);
1948 }
1949 if !header.sections_declared && header.sections.is_empty() {
1950 let found =
1954 lines.blank_opens_velocity_block() && parse_velocity_section(lines, header, atom_data)?;
1955 if found {
1956 header.sections.push(SECTION_VELOCITIES.into());
1957 applied = 1;
1958 }
1959 } else {
1960 let sections = std::mem::take(&mut header.sections);
1961 for section in §ions {
1962 match section.as_str() {
1963 SECTION_VELOCITIES => {
1964 let found = parse_velocity_section(lines, header, atom_data)?;
1965 if !found {
1966 return Err(ParseError::IncompleteSection(SECTION_VELOCITIES.into()));
1967 }
1968 applied += 1;
1969 }
1970 SECTION_FORCES => {
1971 let found = parse_force_section(lines, header, atom_data)?;
1972 if !found {
1973 return Err(ParseError::IncompleteSection(SECTION_FORCES.into()));
1974 }
1975 applied += 1;
1976 }
1977 SECTION_ENERGIES => {
1978 let found = parse_energy_section(lines, header, atom_data)?;
1979 if !found {
1980 return Err(ParseError::IncompleteSection(SECTION_ENERGIES.into()));
1981 }
1982 applied += 1;
1983 }
1984 SECTION_CHARGES => {
1985 let found = parse_charge_section(lines, header, atom_data)?;
1986 if !found {
1987 return Err(ParseError::IncompleteSection(SECTION_CHARGES.into()));
1988 }
1989 applied += 1;
1990 }
1991 SECTION_SPINS => {
1992 let found = parse_spin_section(lines, header, atom_data)?;
1993 if !found {
1994 return Err(ParseError::IncompleteSection(SECTION_SPINS.into()));
1995 }
1996 applied += 1;
1997 }
1998 SECTION_MAGMOMS => {
1999 let found = parse_magmom_section(lines, header, atom_data)?;
2000 if !found {
2001 return Err(ParseError::IncompleteSection(SECTION_MAGMOMS.into()));
2002 }
2003 applied += 1;
2004 }
2005 SECTION_DISPLACEMENTS => {
2006 let found = parse_displacement_section(lines, header, atom_data)?;
2007 if !found {
2008 return Err(ParseError::IncompleteSection(SECTION_DISPLACEMENTS.into()));
2009 }
2010 applied += 1;
2011 }
2012 SECTION_SPREADS => {
2013 let found = parse_spread_section(lines, header, atom_data)?;
2014 if !found {
2015 return Err(ParseError::IncompleteSection(SECTION_SPREADS.into()));
2016 }
2017 applied += 1;
2018 }
2019 other => {
2020 if frame_minor(header) > u64::from(crate::CON_SPEC_MINOR) {
2021 if let Some(arity) = section_row_arity(header, other) {
2022 skip_unknown_section(lines, header, other, arity)?;
2023 continue;
2024 }
2025 }
2026 return Err(ParseError::UnknownSection(other.to_string()));
2027 }
2028 }
2029 }
2030 header.sections = sections;
2031 }
2032 reject_velocities_without_time(header)?;
2033 Ok(applied)
2034}
2035
2036fn reject_velocities_without_time(header: &FrameHeader) -> Result<(), ParseError> {
2040 if header.spec_version < 3 || frame_minor(header) < 1 {
2041 return Ok(());
2042 }
2043 if !header
2044 .sections
2045 .iter()
2046 .any(|name| name == SECTION_VELOCITIES)
2047 {
2048 return Ok(());
2049 }
2050 let time_ok = header
2051 .metadata
2052 .get(meta::UNITS)
2053 .and_then(|units| units.get("time"))
2054 .and_then(|value| value.as_str())
2055 .is_some_and(|unit| !unit.trim().is_empty());
2056 if time_ok {
2057 Ok(())
2058 } else {
2059 Err(ParseError::InvalidUnits(
2060 "minor >= 1 requires units.time when a velocities section is present".into(),
2061 ))
2062 }
2063}
2064
2065fn parse_scalar_atom_section<'a>(
2068 lines: &mut impl LineStream<'a>,
2069 header: &FrameHeader,
2070 atom_data: &mut [AtomDatum],
2071 section_name: &str,
2072 component_label: &str,
2073 mut set_value: impl FnMut(&mut AtomDatum, f64),
2074) -> Result<bool, ParseError> {
2075 let validate = header.strict_validation;
2076 if !open_section(lines)? {
2077 return Ok(false);
2078 }
2079
2080 let mut atom_idx: usize = 0;
2081 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
2082 let symbol = lines
2083 .next_line()
2084 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?
2085 .trim();
2086
2087 let comp_line = lines
2088 .next_line()
2089 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
2090 if !label_has_prefix(comp_line, component_label) {
2091 return Err(ParseError::IncompleteSection(section_name.into()));
2092 }
2093 if validate {
2094 validate_section_component(
2095 component_label.trim_end_matches(" of Component").trim(),
2096 type_idx,
2097 atom_idx,
2098 symbol,
2099 comp_line,
2100 header,
2101 atom_data,
2102 )?;
2103 }
2104
2105 for _ in 0..num_atoms {
2106 let data_line = lines
2107 .next_line()
2108 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
2109 let (value, fixed, atom_id) = parse_scalar_section_row(data_line, header.spec_version)?;
2110 if validate {
2111 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
2112 }
2113 if atom_idx < atom_data.len() {
2114 set_value(&mut atom_data[atom_idx], value);
2115 }
2116 atom_idx += 1;
2117 }
2118 }
2119 Ok(true)
2120}
2121
2122pub fn parse_charge_section<'a>(
2123 lines: &mut impl LineStream<'a>,
2124 header: &FrameHeader,
2125 atom_data: &mut [AtomDatum],
2126) -> Result<bool, ParseError> {
2127 parse_scalar_atom_section(
2128 lines,
2129 header,
2130 atom_data,
2131 SECTION_CHARGES,
2132 "Charges of Component",
2133 |a, v| a.set_charge(Some(v)),
2134 )
2135}
2136
2137pub fn parse_spin_section<'a>(
2138 lines: &mut impl LineStream<'a>,
2139 header: &FrameHeader,
2140 atom_data: &mut [AtomDatum],
2141) -> Result<bool, ParseError> {
2142 parse_scalar_atom_section(
2143 lines,
2144 header,
2145 atom_data,
2146 SECTION_SPINS,
2147 "Spins of Component",
2148 |a, v| a.set_spin(Some(v)),
2149 )
2150}
2151
2152fn parse_vector_named_section<'a>(
2153 lines: &mut impl LineStream<'a>,
2154 header: &FrameHeader,
2155 atom_data: &mut [AtomDatum],
2156 section_name: &str,
2157 component_label: &str,
2158 label_kind: &str,
2159 mut set_value: impl FnMut(&mut AtomDatum, [f64; 3]),
2160 mut check_row: impl FnMut(usize, [f64; 3]) -> Result<(), ParseError>,
2161) -> Result<bool, ParseError> {
2162 let validate = header.strict_validation;
2163 if !open_section(lines)? {
2164 return Ok(false);
2165 }
2166 let mut atom_idx: usize = 0;
2167 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
2168 let symbol = lines
2169 .next_line()
2170 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?
2171 .trim();
2172 let comp_line = lines
2173 .next_line()
2174 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
2175 if !label_has_prefix(comp_line, component_label) {
2176 return Err(ParseError::IncompleteSection(section_name.into()));
2177 }
2178 if validate {
2179 validate_section_component(
2180 label_kind, type_idx, atom_idx, symbol, comp_line, header, atom_data,
2181 )?;
2182 }
2183 for _ in 0..num_atoms {
2184 let row = lines
2185 .next_line()
2186 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
2187 let (vals, fixed, atom_id) = parse_vector_section_row(row, header.spec_version)?;
2188 check_row(atom_idx, vals)?;
2189 if validate {
2190 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
2191 }
2192 if atom_idx < atom_data.len() {
2193 set_value(&mut atom_data[atom_idx], vals);
2194 }
2195 atom_idx += 1;
2196 }
2197 }
2198 Ok(true)
2199}
2200
2201pub fn parse_magmom_section<'a>(
2203 lines: &mut impl LineStream<'a>,
2204 header: &FrameHeader,
2205 atom_data: &mut [AtomDatum],
2206) -> Result<bool, ParseError> {
2207 parse_vector_named_section(
2208 lines,
2209 header,
2210 atom_data,
2211 SECTION_MAGMOMS,
2212 "Magmoms of Component",
2213 "Magmoms",
2214 |a, v| a.set_magmom(Some(v)),
2215 |_, _| Ok(()),
2216 )
2217}
2218
2219pub fn parse_displacement_section<'a>(
2221 lines: &mut impl LineStream<'a>,
2222 header: &FrameHeader,
2223 atom_data: &mut [AtomDatum],
2224) -> Result<bool, ParseError> {
2225 parse_vector_named_section(
2226 lines,
2227 header,
2228 atom_data,
2229 SECTION_DISPLACEMENTS,
2230 "Displacements of Component",
2231 "Displacements",
2232 |a, v| a.set_displacement(Some(v)),
2233 |_, _| Ok(()),
2234 )
2235}
2236
2237pub fn parse_spread_section<'a>(
2240 lines: &mut impl LineStream<'a>,
2241 header: &FrameHeader,
2242 atom_data: &mut [AtomDatum],
2243) -> Result<bool, ParseError> {
2244 parse_vector_named_section(
2245 lines,
2246 header,
2247 atom_data,
2248 SECTION_SPREADS,
2249 "Spreads of Component",
2250 "Spreads",
2251 |a, v| a.set_spread(Some(v)),
2252 |atom_idx, vals| {
2253 if !vals.iter().all(|v| v.is_finite()) {
2254 return Err(ParseError::NonFiniteValue(format!(
2255 "spreads atom {atom_idx}"
2256 )));
2257 }
2258 if vals.iter().any(|v| *v < 0.0) {
2259 return Err(crate::types::invalid_spread(atom_idx));
2260 }
2261 Ok(())
2262 },
2263 )
2264}
2265
2266#[cfg(test)]
2267mod tests {
2268 use super::*;
2269 use crate::iterators::ConFrameIterator;
2270
2271 #[test]
2272 fn test_parse_line_of_n_success() {
2273 let line = "1.0 2.5 -3.0";
2274 let values = parse_line_of_n::<f64>(line, 3).unwrap();
2275 assert_eq!(values, vec![1.0, 2.5, -3.0]);
2276 }
2277
2278 #[test]
2279 fn test_parse_line_of_n_too_short() {
2280 let line = "1.0 2.5";
2281 let result = parse_line_of_n::<f64>(line, 3);
2282 assert!(result.is_err());
2283 assert!(matches!(
2284 result.unwrap_err(),
2285 ParseError::InvalidVectorLength {
2286 expected: 3,
2287 found: 2
2288 }
2289 ));
2290 }
2291
2292 #[test]
2293 fn test_parse_line_of_n_too_long() {
2294 let line = "1.0 2.5 -3.0 4.0";
2295 let result = parse_line_of_n::<f64>(line, 3);
2296 assert!(result.is_err());
2297 assert!(matches!(
2298 result.unwrap_err(),
2299 ParseError::InvalidVectorLength {
2300 expected: 3,
2301 found: 4
2302 }
2303 ));
2304 }
2305
2306 #[test]
2307 fn test_parse_line_of_n_invalid_float() {
2308 let line = "1.0 abc -3.0";
2309 let result = parse_line_of_n::<f64>(line, 3);
2310 assert!(result.is_err());
2311 assert!(matches!(
2312 result.unwrap_err(),
2313 ParseError::InvalidNumberFormat(_)
2314 ));
2315 }
2316
2317 #[test]
2318 fn test_parse_frame_header_success() {
2319 let lines = [
2320 "PREBOX1",
2321 "{\"con_spec_version\":2}",
2322 "10.0 20.0 30.0",
2323 "90.0 90.0 90.0",
2324 "POSTBOX1",
2325 "POSTBOX2",
2326 "2",
2327 "1 1",
2328 "12.011 1.008",
2329 ];
2330 let mut line_it = lines.iter().copied();
2331 match parse_frame_header(&mut line_it) {
2332 Ok(header) => {
2333 assert_eq!(header.prebox_header.user, "PREBOX1");
2334 assert_eq!(header.spec_version, 2);
2335 assert_eq!(header.boxl, [10.0, 20.0, 30.0]);
2336 assert_eq!(header.angles, [90.0, 90.0, 90.0]);
2337 assert_eq!(header.postbox_header, ["POSTBOX1", "POSTBOX2"]);
2338 assert_eq!(header.natm_types, 2);
2339 assert_eq!(header.natms_per_type, vec![1, 1]);
2340 assert_eq!(header.masses_per_type, vec![12.011, 1.008]);
2341 }
2342 Err(e) => {
2343 panic!(
2344 "Parsing failed when it should have succeeded. Error: {:?}",
2345 e
2346 );
2347 }
2348 }
2349 }
2350
2351 #[test]
2352 fn test_parse_frame_header_missing_line() {
2353 let lines = [
2354 "PREBOX1",
2355 "{\"con_spec_version\":2}",
2356 "10.0 20.0 30.0",
2357 "90.0 90.0 90.0",
2358 "POSTBOX1",
2359 "POSTBOX2",
2360 "2",
2361 "1 1",
2362 ];
2364 let mut line_it = lines.iter().copied();
2365 let result = parse_frame_header(&mut line_it);
2366 assert!(result.is_err());
2367 assert!(matches!(result.unwrap_err(), ParseError::IncompleteHeader));
2368 }
2369
2370 #[test]
2371 fn test_parse_frame_header_missing_spec_version() {
2372 let lines = vec![
2373 "PREBOX1",
2374 "{}",
2375 "10.0 20.0 30.0",
2376 "90.0 90.0 90.0",
2377 "POSTBOX1",
2378 "POSTBOX2",
2379 "2",
2380 "1 1",
2381 "12.011 1.008",
2382 ];
2383 let mut line_it = lines.iter().copied();
2384 let result = parse_frame_header(&mut line_it);
2385 assert!(result.is_err());
2386 assert!(matches!(
2387 result.unwrap_err(),
2388 ParseError::MissingSpecVersion
2389 ));
2390 }
2391
2392 #[test]
2393 fn test_parse_frame_header_legacy_no_json() {
2394 let lines = vec![
2396 "PREBOX1",
2397 "0.0000 TIME",
2398 "10.0 20.0 30.0",
2399 "90.0 90.0 90.0",
2400 "POSTBOX1",
2401 "POSTBOX2",
2402 "2",
2403 "1 1",
2404 "12.011 1.008",
2405 ];
2406 let mut line_it = lines.iter().copied();
2407 let header = parse_frame_header(&mut line_it).unwrap();
2408 assert_eq!(header.spec_version, 1);
2409 assert!(header.metadata.is_empty());
2410 }
2411
2412 #[test]
2413 fn test_parse_frame_header_malformed_json() {
2414 let lines = vec![
2416 "PREBOX1",
2417 "{broken json",
2418 "10.0 20.0 30.0",
2419 "90.0 90.0 90.0",
2420 "POSTBOX1",
2421 "POSTBOX2",
2422 "2",
2423 "1 1",
2424 "12.011 1.008",
2425 ];
2426 let mut line_it = lines.iter().copied();
2427 let result = parse_frame_header(&mut line_it);
2428 assert!(result.is_err());
2429 assert!(matches!(
2430 result.unwrap_err(),
2431 ParseError::InvalidMetadataJson(_)
2432 ));
2433 }
2434
2435 #[test]
2436 fn test_parse_frame_header_unsupported_version() {
2437 let lines = vec![
2438 "PREBOX1",
2439 "{\"con_spec_version\":999}",
2440 "10.0 20.0 30.0",
2441 "90.0 90.0 90.0",
2442 "POSTBOX1",
2443 "POSTBOX2",
2444 "2",
2445 "1 1",
2446 "12.011 1.008",
2447 ];
2448 let mut line_it = lines.iter().copied();
2449 let result = parse_frame_header(&mut line_it);
2450 assert!(result.is_err());
2451 assert!(matches!(
2452 result.unwrap_err(),
2453 ParseError::UnsupportedVersion(999)
2454 ));
2455 }
2456
2457 #[test]
2458 fn test_v3_missing_units_rejected() {
2459 let lines = vec![
2460 "PREBOX1",
2461 "{\"con_spec_version\":3}",
2462 "10.0 20.0 30.0",
2463 "90.0 90.0 90.0",
2464 "POSTBOX1",
2465 "POSTBOX2",
2466 "1",
2467 "1",
2468 "1.0",
2469 ];
2470 let mut line_it = lines.iter().copied();
2471 let err = parse_frame_header(&mut line_it).unwrap_err();
2472 let msg = err.to_string();
2473 assert!(
2474 msg.contains("units") || msg.contains("v3"),
2475 "expected units error, got {msg}"
2476 );
2477 }
2478
2479 #[test]
2480 fn test_v3_invalid_units_rejected() {
2481 let lines = vec![
2482 "PREBOX1",
2483 r#"{"con_spec_version":3,"units":{"length":"eV","energy":"angstrom"}}"#,
2484 "10.0 20.0 30.0",
2485 "90.0 90.0 90.0",
2486 "POSTBOX1",
2487 "POSTBOX2",
2488 "1",
2489 "1",
2490 "1.0",
2491 ];
2492 let mut line_it = lines.iter().copied();
2493 assert!(parse_frame_header(&mut line_it).is_err());
2494 }
2495
2496 #[test]
2497 fn test_v3_valid_units_exposes_length_energy() {
2498 let lines = vec![
2499 "PREBOX1",
2500 r#"{"con_spec_version":3,"units":{"length":"angstrom","energy":"eV","mass":"amu","time":"fs"}}"#,
2501 "10.0 20.0 30.0",
2502 "90.0 90.0 90.0",
2503 "POSTBOX1",
2504 "POSTBOX2",
2505 "1",
2506 "1",
2507 "1.0",
2508 ];
2509 let mut line_it = lines.iter().copied();
2510 let header = parse_frame_header(&mut line_it).unwrap();
2511 assert_eq!(header.spec_version, 3);
2512 assert_eq!(header.length_unit(), Some("angstrom"));
2513 assert_eq!(header.energy_unit(), Some("eV"));
2514 let f = header.conversion_factor_to("length", "nm").unwrap();
2515 assert!((f - 0.1).abs() < 1e-12);
2516 }
2517
2518 #[test]
2519 fn test_parse_frame_header_extra_metadata_preserved() {
2520 let lines = vec![
2521 "PREBOX1",
2522 "{\"con_spec_version\":2,\"generator\":\"test\"}",
2523 "10.0 20.0 30.0",
2524 "90.0 90.0 90.0",
2525 "POSTBOX1",
2526 "POSTBOX2",
2527 "2",
2528 "1 1",
2529 "12.011 1.008",
2530 ];
2531 let mut line_it = lines.iter().copied();
2532 let header = parse_frame_header(&mut line_it).unwrap();
2533 assert_eq!(header.spec_version, 2);
2534 assert_eq!(
2535 header.metadata.get("generator"),
2536 Some(&serde_json::Value::String("test".to_string()))
2537 );
2538 }
2539
2540 #[test]
2541 fn test_parse_frame_header_invalid_natms_per_type() {
2542 let lines = vec![
2543 "PREBOX1",
2544 "{\"con_spec_version\":2}",
2545 "10.0 20.0 30.0",
2546 "90.0 90.0 90.0",
2547 "POSTBOX1",
2548 "POSTBOX2",
2549 "2",
2550 "1 1 1", "12.011 1.008",
2552 ];
2553 let mut line_it = lines.iter().copied();
2554 let result = parse_frame_header(&mut line_it);
2555 assert!(result.is_err());
2556 assert!(matches!(
2557 result.unwrap_err(),
2558 ParseError::InvalidVectorLength {
2559 expected: 2,
2560 found: 3
2561 }
2562 ));
2563 }
2564
2565 #[test]
2566 fn test_parse_single_frame_success() {
2567 let lines = vec![
2568 "PREBOX1",
2569 "{\"con_spec_version\":2}",
2570 "10.0 20.0 30.0",
2571 "90.0 90.0 90.0",
2572 "POSTBOX1",
2573 "POSTBOX2",
2574 "2",
2575 "3 3",
2576 "12.011 1.008",
2577 "1",
2578 "Coordinates of Component 1",
2579 "0.0 0.0 0.0 0 1",
2580 "1.0940 0.0 0.0 0 2",
2581 "-0.5470 0.9499 0.0 0 3",
2582 "2",
2583 "Coordinates of Component 2",
2584 "5.0 5.0 5.0 0 4",
2585 "6.0940 5.0 5.0 0 5",
2586 "5.5470 5.9499 5.0 0 6",
2587 ];
2588 let mut line_it = lines.iter().copied();
2589 let frame = parse_single_frame(&mut line_it).unwrap();
2590
2591 assert_eq!(frame.header.natm_types, 2);
2592 assert_eq!(frame.header.natms_per_type, vec![3, 3]);
2593 assert_eq!(frame.header.masses_per_type, vec![12.011, 1.008]);
2594 assert_eq!(frame.atom_data.len(), 6);
2595 assert_eq!(&*frame.atom_data[0].symbol, "1");
2596 assert_eq!(frame.atom_data[0].atom_id, 1);
2597 assert_eq!(&*frame.atom_data[5].symbol, "2");
2598 assert_eq!(frame.atom_data[5].atom_id, 6);
2599 }
2600
2601 #[test]
2602 fn test_column_4_value_1_is_x_only_on_spec_2() {
2603 let lines = vec![
2604 "Generated by probe",
2605 "{\"con_spec_version\":2}",
2606 "20.0 20.0 20.0",
2607 "90.0 90.0 90.0",
2608 "",
2609 "",
2610 "1",
2611 "1",
2612 "63.5",
2613 "Cu",
2614 "Coordinates of component 1",
2615 "1.0 2.0 3.0 1 1",
2616 ];
2617 let mut line_it = lines.iter().copied();
2618 let frame = parse_single_frame(&mut line_it).unwrap();
2619 assert_eq!(frame.header.spec_version, 2);
2620 assert_eq!(frame.atom_data[0].fixed, [true, false, false]);
2621 }
2622
2623 #[test]
2624 fn test_column_4_value_1_is_all_fixed_on_spec_1() {
2625 let lines = vec![
2626 "Generated by probe",
2627 "0.0000 TIME",
2628 "20.0 20.0 20.0",
2629 "90.0 90.0 90.0",
2630 "",
2631 "",
2632 "1",
2633 "1",
2634 "63.5",
2635 "Cu",
2636 "Coordinates of component 1",
2637 "1.0 2.0 3.0 1 1",
2638 ];
2639 let mut line_it = lines.iter().copied();
2640 let frame = parse_single_frame(&mut line_it).unwrap();
2641 assert_eq!(frame.header.spec_version, 1);
2642 assert_eq!(frame.atom_data[0].fixed, [true, true, true]);
2643 }
2644
2645 #[test]
2646 fn test_parse_single_frame_missing_line() {
2647 let lines = vec![
2649 "PREBOX1",
2650 "{\"con_spec_version\":2}",
2651 "10.0 20.0 30.0",
2652 "90.0 90.0 90.0",
2653 "POSTBOX1",
2654 "POSTBOX2",
2655 "2",
2656 "3 3",
2657 "12.011 1.008",
2658 "1",
2659 "Coordinates of Component 1",
2660 "0.0 0.0 0.0 0 1",
2661 "1.0940 0.0 0.0 0 2",
2662 "-0.5470 0.9499 0.0 0 3",
2663 ];
2665 let mut line_it = lines.iter().copied();
2666 let result = parse_single_frame(&mut line_it);
2667 assert!(result.is_err());
2668 assert!(matches!(result.unwrap_err(), ParseError::IncompleteFrame));
2669 }
2670
2671 #[test]
2672 fn test_parse_single_frame_missing_atom_index_defaults_sequential() {
2673 let lines = vec![
2675 "PREBOX1",
2676 "{\"con_spec_version\":2}",
2677 "10.0 20.0 30.0",
2678 "90.0 90.0 90.0",
2679 "POSTBOX1",
2680 "POSTBOX2",
2681 "2",
2682 "3 3",
2683 "12.011 1.008",
2684 "1",
2685 "Coordinates of Component 1",
2686 "0.0 0.0 0.0 0 1",
2687 "1.0940 0.0 0.0 0 2",
2688 "-0.5470 0.9499 0.0 0 3",
2689 "2",
2690 "Coordinates of Component 2",
2691 "5.0 5.0 5.0 0", "6.0940 5.0 5.0 0 10", "5.5470 5.9499 5.0 0", ];
2695 let mut line_it = lines.iter().copied();
2696 let frame = parse_single_frame(&mut line_it).unwrap();
2697 assert_eq!(frame.atom_data.len(), 6);
2698 assert_eq!(frame.atom_data[0].atom_id, 1);
2700 assert_eq!(frame.atom_data[1].atom_id, 2);
2701 assert_eq!(frame.atom_data[2].atom_id, 3);
2702 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); }
2707
2708 #[test]
2709 fn test_parse_single_frame_too_few_columns_fails() {
2710 let lines = vec![
2712 "PREBOX1",
2713 "{\"con_spec_version\":2}",
2714 "10.0 20.0 30.0",
2715 "90.0 90.0 90.0",
2716 "POSTBOX1",
2717 "POSTBOX2",
2718 "1",
2719 "1",
2720 "12.011",
2721 "C",
2722 "Coordinates of Component 1",
2723 "0.0 0.0 0.0", ];
2725 let mut line_it = lines.iter().copied();
2726 let result = parse_single_frame(&mut line_it);
2727 assert!(result.is_err());
2728 assert!(matches!(
2729 result.unwrap_err(),
2730 ParseError::InvalidVectorLength {
2731 expected: 5,
2732 found: 3
2733 }
2734 ));
2735 }
2736
2737 #[test]
2738 fn test_parse_velocity_section_present() {
2739 let lines = vec![
2740 "PREBOX1",
2741 "{\"con_spec_version\":2}",
2742 "10.0 20.0 30.0",
2743 "90.0 90.0 90.0",
2744 "POSTBOX1",
2745 "POSTBOX2",
2746 "2",
2747 "1 1",
2748 "63.546 1.008",
2749 "Cu",
2750 "Coordinates of Component 1",
2751 "0.0 0.0 0.0 1 0",
2752 "H",
2753 "Coordinates of Component 2",
2754 "1.0 2.0 3.0 0 1",
2755 "",
2756 "Cu",
2757 "Velocities of Component 1",
2758 "0.1 0.2 0.3 1 0",
2759 "H",
2760 "Velocities of Component 2",
2761 "0.4 0.5 0.6 0 1",
2762 ];
2763 let mut line_it = lines.iter().copied().peekable();
2764 let mut frame =
2766 parse_single_frame(&mut line_it).expect("coordinate parsing should succeed");
2767 assert!(!frame.has_velocities());
2768
2769 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
2771 .expect("velocity parsing should succeed");
2772 assert!(has_vel);
2773 assert_eq!(frame.atom_data[0].velocity(), Some([0.1, 0.2, 0.3]));
2774 assert_eq!(frame.atom_data[1].velocity(), Some([0.4, 0.5, 0.6]));
2775 }
2776
2777 #[test]
2778 fn test_validate_true_accepts_matching_section_identity() {
2779 let text = r#"
2780PREBOX1
2781{"con_spec_version":2,"sections":["velocities"],"validate":true}
278210.0 20.0 30.0
278390.0 90.0 90.0
2784POSTBOX1
2785POSTBOX2
27862
27871 1
278863.546 1.008
2789Cu
2790Coordinates of Component 1
27910.0 0.0 0.0 5 0
2792H
2793Coordinates of Component 2
27941.0 2.0 3.0 0 1
2795
2796Cu
2797Velocities of Component 1
27980.1 0.2 0.3 5 0
2799H
2800Velocities of Component 2
28010.4 0.5 0.6 0 1
2802"#;
2803 let mut iter = ConFrameIterator::new(text.trim());
2804 let frame = iter.next().unwrap().unwrap();
2805
2806 assert!(frame.has_velocities());
2807 assert_eq!(
2808 frame
2809 .header
2810 .metadata
2811 .get("validate")
2812 .and_then(|v| v.as_bool()),
2813 Some(true)
2814 );
2815 }
2816
2817 #[test]
2818 fn test_validate_true_rejects_section_atom_id_mismatch() {
2819 let text = r#"
2820PREBOX1
2821{"con_spec_version":2,"sections":["velocities"],"validate":true}
282210.0 20.0 30.0
282390.0 90.0 90.0
2824POSTBOX1
2825POSTBOX2
28261
28271
282863.546
2829Cu
2830Coordinates of Component 1
28310.0 0.0 0.0 5 0
2832
2833Cu
2834Velocities of Component 1
28350.1 0.2 0.3 5 99
2836"#;
2837 let mut iter = ConFrameIterator::new(text.trim());
2838 let err = iter.next().unwrap().unwrap_err();
2839
2840 assert!(matches!(err, ParseError::ValidationError(_)));
2841 assert!(err.to_string().contains("atom_id mismatch"));
2842 }
2843
2844 #[test]
2845 fn test_validate_true_rejects_section_symbol_mismatch() {
2846 let text = r#"
2847PREBOX1
2848{"con_spec_version":2,"sections":["forces"],"validate":true}
284910.0 20.0 30.0
285090.0 90.0 90.0
2851POSTBOX1
2852POSTBOX2
28531
28541
285563.546
2856Cu
2857Coordinates of Component 1
28580.0 0.0 0.0 5 0
2859
2860H
2861Forces of Component 1
28620.1 0.2 0.3 5 0
2863"#;
2864 let mut iter = ConFrameIterator::new(text.trim());
2865 let err = iter.next().unwrap().unwrap_err();
2866
2867 assert!(matches!(err, ParseError::ValidationError(_)));
2868 assert!(err.to_string().contains("symbol mismatch"));
2869 }
2870
2871 #[test]
2872 fn test_validate_absent_allows_legacy_duplicate_identity_mismatch() {
2873 let text = r#"
2874PREBOX1
2875{"con_spec_version":2,"sections":["velocities"]}
287610.0 20.0 30.0
287790.0 90.0 90.0
2878POSTBOX1
2879POSTBOX2
28801
28811
288263.546
2883Cu
2884Coordinates of Component 1
28850.0 0.0 0.0 5 0
2886
2887Cu
2888Velocities of Component 1
28890.1 0.2 0.3 0 99
2890"#;
2891 let mut iter = ConFrameIterator::new(text.trim());
2892 let frame = iter.next().unwrap().unwrap();
2893
2894 assert!(frame.has_velocities());
2895 assert_eq!(frame.atom_data[0].atom_id, 0);
2896 assert_eq!(frame.atom_data[0].fixed, [true, false, true]);
2897 }
2898
2899 #[test]
2900 fn test_validate_must_be_boolean_when_present() {
2901 let lines = vec![
2902 "PREBOX1",
2903 "{\"con_spec_version\":2,\"validate\":\"yes\",\"sections\":[]}",
2904 "10.0 20.0 30.0",
2905 "90.0 90.0 90.0",
2906 "POSTBOX1",
2907 "POSTBOX2",
2908 "1",
2909 "1",
2910 "12.011",
2911 ];
2912 let mut line_it = lines.iter().copied();
2913 let err = parse_frame_header(&mut line_it).unwrap_err();
2914
2915 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2916 assert!(err.to_string().contains("validate"));
2917 }
2918
2919 #[test]
2920 fn test_validate_true_requires_sections_key() {
2921 let lines = vec![
2922 "PREBOX1",
2923 "{\"con_spec_version\":2,\"validate\":true}",
2924 "10.0 20.0 30.0",
2925 "90.0 90.0 90.0",
2926 "POSTBOX1",
2927 "POSTBOX2",
2928 "1",
2929 "1",
2930 "12.011",
2931 ];
2932 let mut line_it = lines.iter().copied();
2933 let err = parse_frame_header(&mut line_it).unwrap_err();
2934
2935 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2936 assert!(err.to_string().contains("sections"));
2937 }
2938
2939 #[test]
2940 fn test_sections_must_be_string_array_when_present() {
2941 let lines = vec![
2942 "PREBOX1",
2943 "{\"con_spec_version\":2,\"sections\":[\"velocities\",7]}",
2944 "10.0 20.0 30.0",
2945 "90.0 90.0 90.0",
2946 "POSTBOX1",
2947 "POSTBOX2",
2948 "1",
2949 "1",
2950 "12.011",
2951 ];
2952 let mut line_it = lines.iter().copied();
2953 let err = parse_frame_header(&mut line_it).unwrap_err();
2954
2955 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2956 assert!(err.to_string().contains("sections"));
2957 }
2958
2959 #[test]
2960 fn test_validate_true_rejects_non_integer_coordinate_identity_columns() {
2961 let text = r#"
2962PREBOX1
2963{"con_spec_version":2,"sections":[],"validate":true}
296410.0 20.0 30.0
296590.0 90.0 90.0
2966POSTBOX1
2967POSTBOX2
29681
29691
297063.546
2971Cu
2972Coordinates of Component 1
29730.0 0.0 0.0 5.0 0
2974"#;
2975 let mut iter = ConFrameIterator::new(text.trim());
2976 let err = iter.next().unwrap().unwrap_err();
2977
2978 assert!(matches!(err, ParseError::InvalidFixedFlag(_)));
2979 assert!(err.to_string().contains("5.0"));
2980 }
2981
2982 #[test]
2983 fn test_validate_true_rejects_non_exact_coordinate_label() {
2984 let text = r#"
2985PREBOX1
2986{"con_spec_version":2,"sections":[],"validate":true}
298710.0 20.0 30.0
298890.0 90.0 90.0
2989POSTBOX1
2990POSTBOX2
29911
29921
299363.546
2994Cu
2995Coordinates Component 1
29960.0 0.0 0.0 5 0
2997"#;
2998 let mut iter = ConFrameIterator::new(text.trim());
2999 let err = iter.next().unwrap().unwrap_err();
3000
3001 assert!(matches!(err, ParseError::ValidationError(_)));
3002 assert!(err.to_string().contains("Coordinates of Component 1"));
3003 }
3004
3005 #[test]
3006 fn test_validate_true_rejects_unknown_component_symbol() {
3007 let text = r#"
3008PREBOX1
3009{"con_spec_version":2,"sections":[],"validate":true}
301010.0 20.0 30.0
301190.0 90.0 90.0
3012POSTBOX1
3013POSTBOX2
30141
30151
301663.546
3017Qq
3018Coordinates of Component 1
30190.0 0.0 0.0 0 0
3020"#;
3021 let mut iter = ConFrameIterator::new(text.trim());
3022 let err = iter.next().unwrap().unwrap_err();
3023
3024 assert!(matches!(err, ParseError::ValidationError(_)));
3025 assert!(err.to_string().contains("symbol"));
3026 }
3027
3028 #[test]
3029 fn test_declared_section_must_be_present() {
3030 let text = r#"
3031PREBOX1
3032{"con_spec_version":2,"sections":["velocities"]}
303310.0 20.0 30.0
303490.0 90.0 90.0
3035POSTBOX1
3036POSTBOX2
30371
30381
303963.546
3040Cu
3041Coordinates of Component 1
30420.0 0.0 0.0 0 0
3043"#;
3044 let mut iter = ConFrameIterator::new(text.trim());
3045 let err = iter.next().unwrap().unwrap_err();
3046
3047 assert!(matches!(
3048 err,
3049 ParseError::IncompleteSection(ref name) if name == "velocities"
3050 ));
3051 }
3052
3053 #[test]
3054 fn test_non_finite_cell_geometry_rejected_in_strict_mode() {
3055 let lines = vec![
3056 "PREBOX1",
3057 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
3058 "10.0 NaN 30.0",
3059 "90.0 90.0 90.0",
3060 "POSTBOX1",
3061 "POSTBOX2",
3062 "1",
3063 "1",
3064 "12.011",
3065 ];
3066 let mut line_it = lines.iter().copied();
3067 let err = parse_frame_header(&mut line_it).unwrap_err();
3068
3069 assert!(matches!(err, ParseError::NonFiniteValue(_)));
3070 assert!(err.to_string().contains("NaN"));
3071 }
3072
3073 #[test]
3074 fn test_validate_true_rejects_non_physical_cell_geometry() {
3075 let lines = vec![
3076 "PREBOX1",
3077 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
3078 "0.0 20.0 30.0",
3079 "90.0 180.0 90.0",
3080 "POSTBOX1",
3081 "POSTBOX2",
3082 "1",
3083 "1",
3084 "12.011",
3085 ];
3086 let mut line_it = lines.iter().copied();
3087 let err = parse_frame_header(&mut line_it).unwrap_err();
3088
3089 assert!(matches!(err, ParseError::ValidationError(_)));
3090 assert!(err.to_string().contains("cell"));
3091 }
3092
3093 #[test]
3094 fn test_validate_true_rejects_reserved_metadata_type_mismatch() {
3095 let lines = vec![
3096 "PREBOX1",
3097 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"energy\":\"low\"}",
3098 "10.0 20.0 30.0",
3099 "90.0 90.0 90.0",
3100 "POSTBOX1",
3101 "POSTBOX2",
3102 "1",
3103 "1",
3104 "12.011",
3105 ];
3106 let mut line_it = lines.iter().copied();
3107 let err = parse_frame_header(&mut line_it).unwrap_err();
3108
3109 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
3110 assert!(err.to_string().contains("energy"));
3111 }
3112
3113 #[test]
3114 fn test_validate_true_rejects_malformed_bonds() {
3115 let lines = vec![
3116 "PREBOX1",
3117 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"bonds\":[[0]]}",
3118 "10.0 20.0 30.0",
3119 "90.0 90.0 90.0",
3120 "POSTBOX1",
3121 "POSTBOX2",
3122 "1",
3123 "1",
3124 "12.011",
3125 ];
3126 let mut line_it = lines.iter().copied();
3127 let err = parse_frame_header(&mut line_it).unwrap_err();
3128 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
3129 assert!(err.to_string().contains("bonds"));
3130 }
3131
3132 #[test]
3133 fn test_bonds_metadata_round_trip_in_header() {
3134 use crate::types::{Bond, meta};
3135 let lines = vec![
3136 "PREBOX1",
3137 "{\"con_spec_version\":2,\"bonds\":[[0,1],{\"i\":0,\"j\":2,\"order\":1}]}",
3138 "10.0 20.0 30.0",
3139 "90.0 90.0 90.0",
3140 "POSTBOX1",
3141 "POSTBOX2",
3142 "1",
3143 "1",
3144 "12.011",
3145 ];
3146 let mut line_it = lines.iter().copied();
3147 let header = parse_frame_header(&mut line_it).expect("header");
3148 let bonds = header.bonds();
3149 assert_eq!(bonds.len(), 2);
3150 assert_eq!(bonds[0], Bond::new(0, 1));
3151 assert_eq!(bonds[1].i, 0);
3152 assert_eq!(bonds[1].j, 2);
3153 assert_eq!(bonds[1].order, Some(1));
3154 assert!(header.metadata.contains_key(meta::BONDS));
3155 }
3156
3157 #[test]
3158 fn test_parse_velocity_section_absent() {
3159 let lines = vec![
3160 "PREBOX1",
3161 "{\"con_spec_version\":2}",
3162 "10.0 20.0 30.0",
3163 "90.0 90.0 90.0",
3164 "POSTBOX1",
3165 "POSTBOX2",
3166 "1",
3167 "1",
3168 "12.011",
3169 "C",
3170 "Coordinates of Component 1",
3171 "0.0 0.0 0.0 0 1",
3172 ];
3173 let mut line_it = lines.iter().copied().peekable();
3174 let mut frame = parse_single_frame(&mut line_it).expect("parse should succeed");
3175 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
3176 .expect("should succeed with no velocities");
3177 assert!(!has_vel);
3178 assert_eq!(frame.atom_data[0].velocity(), None);
3179 }
3180
3181 #[test]
3182 fn test_parse_line_of_range_f64_exact() {
3183 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0 42", 4, 5, &[0.0; 5]).unwrap();
3184 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 42.0]);
3185 }
3186
3187 #[test]
3188 fn test_byte_scan_stack_parses_realistic_coord_line() {
3189 let line = " 0.63939999999999997 0.90449999999999997 -0.00009999999999977 1 0";
3191 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
3192 let mut buf = [0.0f64; 5];
3193 let n = parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
3194 assert_eq!(n, 5);
3195 assert!((buf[0] - 0.6394).abs() < 1e-4);
3196 assert!((buf[1] - 0.9045).abs() < 1e-4);
3197 assert_eq!(buf[3] as u8, 1);
3198 assert_eq!(buf[4] as u64, 0);
3199 let bad = "1.0 2.0 3.0 1 0 EXTRA";
3201 assert!(parse_line_of_range_f64_stack(bad, 4, 5, &defaults, &mut buf).is_err());
3202 let glued = "1.0 2.0 3.0abc 1 0";
3204 assert!(parse_line_of_range_f64_stack(glued, 4, 5, &defaults, &mut buf).is_err());
3205 }
3206
3207 #[test]
3208 fn test_parse_line_of_range_f64_stack_matches_vec_api() {
3209 let defaults = [0.0, 0.0, 0.0, 0.0, 7.0];
3210 let line = "1.0 2.0 3.0 0.0";
3211 let mut buf = [0.0f64; 5];
3212 parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
3213 let via_vec = parse_line_of_range_f64(line, 4, 5, &defaults).unwrap();
3214 assert_eq!(&buf[..5], via_vec.as_slice());
3215 assert_eq!(buf[4], 7.0);
3216 }
3217
3218 #[test]
3219 fn test_parse_line_of_range_f64_padded() {
3220 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
3221 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0", 4, 5, &defaults).unwrap();
3222 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 99.0]);
3223 }
3224
3225 #[test]
3226 fn test_parse_line_of_range_f64_too_few() {
3227 let result = parse_line_of_range_f64("1.0 2.0 3.0", 4, 5, &[0.0; 5]);
3228 assert!(result.is_err());
3229 }
3230
3231 #[test]
3232 fn test_parse_line_of_range_f64_too_many() {
3233 let result = parse_line_of_range_f64("1.0 2.0 3.0 0.0 5.0 6.0", 4, 5, &[0.0; 5]);
3234 assert!(result.is_err());
3235 }
3236
3237 #[test]
3238 fn test_parse_all_four_column_lines() {
3239 let lines = vec![
3241 "PREBOX1",
3242 "{\"con_spec_version\":2}",
3243 "10.0 10.0 10.0",
3244 "90.0 90.0 90.0",
3245 "POSTBOX1",
3246 "POSTBOX2",
3247 "1",
3248 "3",
3249 "12.011",
3250 "C",
3251 "Coordinates of Component 1",
3252 "0.0 0.0 0.0 0",
3253 "1.0 0.0 0.0 0",
3254 "2.0 0.0 0.0 1",
3255 ];
3256 let mut line_it = lines.iter().copied();
3257 let frame = parse_single_frame(&mut line_it).unwrap();
3258 assert_eq!(frame.atom_data[0].atom_id, 0);
3259 assert_eq!(frame.atom_data[1].atom_id, 1);
3260 assert_eq!(frame.atom_data[2].atom_id, 2);
3261 assert!(frame.atom_data[2].is_fixed());
3262 }
3263}