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_opens_velocity_block(&mut self) -> bool {
30 true
31 }
32
33 fn next_coord_row(
39 &mut self,
40 spec_version: u32,
41 default_id: u64,
42 ) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
43 let line = self.next_line().ok_or(ParseError::IncompleteFrame)?;
44 parse_coord_row(line, spec_version, default_id)
45 }
46}
47
48impl<'a, I> LineStream<'a> for Peekable<I>
49where
50 I: Iterator<Item = &'a str>,
51{
52 #[inline]
53 fn next_line(&mut self) -> Option<&'a str> {
54 Iterator::next(self)
55 }
56 #[inline]
57 fn peek_line(&mut self) -> Option<&'a str> {
58 Peekable::peek(self).copied()
59 }
60}
61
62#[inline]
71pub fn parse_line_of_range_f64_stack(
72 line: &str,
73 min: usize,
74 max: usize,
75 defaults: &[f64],
76 out: &mut [f64; 5],
77) -> Result<usize, ParseError> {
78 debug_assert!(max <= 5 && min <= max && defaults.len() >= max);
79 let bytes = line.as_bytes();
80 let n = bytes.len();
81 let mut i = 0usize;
82 let mut found = 0usize;
83 while found < max {
84 while i < n && bytes[i].is_ascii_whitespace() {
85 i += 1;
86 }
87 if i >= n {
88 break;
89 }
90 let end = bytes[i..]
91 .iter()
92 .position(|b| b.is_ascii_whitespace())
93 .map(|k| i + k)
94 .unwrap_or(n);
95 let token = std::str::from_utf8(&bytes[i..end]).unwrap_or("");
96 if is_nonfinite_spelling(token) {
97 return Err(ParseError::NonFiniteValue(token.to_string()));
98 }
99 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..])
100 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
101 let next = i + consumed;
102 if next != end {
104 return Err(ParseError::InvalidNumberFormat(format!(
105 "invalid float: {token}"
106 )));
107 }
108 if !val.is_finite() {
109 return Err(ParseError::NonFiniteValue(token.to_string()));
110 }
111 out[found] = val;
112 found += 1;
113 i = next;
114 }
115 while i < n && bytes[i].is_ascii_whitespace() {
116 i += 1;
117 }
118 if i < n {
120 return Err(ParseError::InvalidVectorLength {
121 expected: max,
122 found: found + 1,
123 });
124 }
125 if found < min || found > max {
126 return Err(ParseError::InvalidVectorLength {
127 expected: max,
128 found,
129 });
130 }
131 while found < max {
132 out[found] = defaults[found];
133 found += 1;
134 }
135 Ok(found)
136}
137
138pub fn parse_line_of_n_f64(line: &str, n: usize) -> Result<Vec<f64>, ParseError> {
150 if n <= 5 {
151 let defaults = [0.0f64; 5];
152 let mut buf = [0.0f64; 5];
153 parse_line_of_range_f64_stack(line, n, n, &defaults, &mut buf)?;
154 return Ok(buf[..n].to_vec());
155 }
156 let mut values = Vec::with_capacity(n);
157 for token in line.split_ascii_whitespace() {
158 values.push(parse_finite_float_token(token)?);
159 }
160 if values.len() == n {
161 Ok(values)
162 } else {
163 Err(ParseError::InvalidVectorLength {
164 expected: n,
165 found: values.len(),
166 })
167 }
168}
169
170pub fn parse_line_of_range_f64(
177 line: &str,
178 min: usize,
179 max: usize,
180 defaults: &[f64],
181) -> Result<Vec<f64>, ParseError> {
182 if max <= 5 {
183 let mut buf = [0.0f64; 5];
184 parse_line_of_range_f64_stack(line, min, max, defaults, &mut buf)?;
185 return Ok(buf[..max].to_vec());
186 }
187 let mut values = Vec::with_capacity(max);
188 for token in line.split_ascii_whitespace() {
189 values.push(parse_finite_float_token(token)?);
190 }
191 if values.len() < min || values.len() > max {
192 return Err(ParseError::InvalidVectorLength {
193 expected: max,
194 found: values.len(),
195 });
196 }
197 while values.len() < max {
198 let idx = values.len();
199 values.push(defaults[idx]);
200 }
201 Ok(values)
202}
203
204fn is_nonfinite_spelling(token: &str) -> bool {
208 let rest = token
209 .strip_prefix('+')
210 .or_else(|| token.strip_prefix('-'))
211 .unwrap_or(token);
212 match rest.as_bytes().first() {
213 Some(b) if b.is_ascii_digit() || *b == b'.' => return false,
214 None => return false,
215 _ => {}
216 }
217 rest.eq_ignore_ascii_case("nan")
218 || rest.eq_ignore_ascii_case("inf")
219 || rest.eq_ignore_ascii_case("infinity")
220}
221
222fn parse_finite_float_token(token: &str) -> Result<f64, ParseError> {
223 if is_nonfinite_spelling(token) {
224 return Err(ParseError::NonFiniteValue(token.to_string()));
225 }
226 let (val, consumed) = fast_float2::parse_partial::<f64, _>(token.as_bytes())
227 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
228 if consumed != token.len() {
229 return Err(ParseError::InvalidNumberFormat(format!(
230 "invalid float: {token}"
231 )));
232 }
233 if !val.is_finite() {
234 return Err(ParseError::NonFiniteValue(token.to_string()));
235 }
236 Ok(val)
237}
238
239fn skip_ascii_ws(bytes: &[u8], mut i: usize) -> usize {
240 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
241 i += 1;
242 }
243 i
244}
245
246fn take_token(line: &str, i: usize) -> Option<(&str, usize)> {
247 let bytes = line.as_bytes();
248 if i >= bytes.len() {
249 return None;
250 }
251 let mut j = i;
252 while j < bytes.len() && !bytes[j].is_ascii_whitespace() {
253 j += 1;
254 }
255 Some((&line[i..j], j))
256}
257
258fn parse_fixed_flag(token: &str, spec_version: u32) -> Result<[bool; 3], ParseError> {
261 let bad = || {
262 ParseError::InvalidFixedFlag(format!(
263 "column 4 flag {token:?} is not a legal integer for spec {spec_version}"
264 ))
265 };
266 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
267 return Err(bad());
268 }
269 let val: u64 = token.parse::<u64>().map_err(|_| bad())?;
270 if spec_version < 2 {
271 return match val {
272 0 => Ok([false, false, false]),
273 1 => Ok([true, true, true]),
274 _ => Err(bad()),
275 };
276 }
277 if val > 7 {
278 return Err(bad());
279 }
280 let v = val as u8;
281 Ok([v & 1 != 0, v & 2 != 0, v & 4 != 0])
282}
283
284fn parse_atom_id_token(token: &str) -> Result<u64, ParseError> {
287 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
288 return Err(ParseError::InvalidNumberFormat(format!(
289 "invalid integer: {token}"
290 )));
291 }
292 token.parse::<u64>().map_err(|_| {
293 ParseError::InvalidNumberFormat(format!("integer out of range 0..=2^64-1: {token}"))
294 })
295}
296
297fn parse_coord_row(
301 line: &str,
302 spec_version: u32,
303 default_id: u64,
304) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
305 parse_vector_row(line, spec_version, Some(default_id))
306}
307
308fn parse_vector_section_row(
310 line: &str,
311 spec_version: u32,
312) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
313 parse_vector_row(line, spec_version, None)
314}
315
316fn parse_vector_row(
317 line: &str,
318 spec_version: u32,
319 default_id: Option<u64>,
320) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
321 let (row, end) = parse_coord_fields(line.as_bytes(), 0, spec_version, default_id)?;
322 if end != line.len() {
323 let found = 5 + count_tokens_on_line(line.as_bytes(), end);
324 return Err(ParseError::InvalidVectorLength { expected: 5, found });
325 }
326 Ok(row)
327}
328
329#[inline(always)]
331fn is_field_ws(b: u8) -> bool {
332 matches!(b, b' ' | b'\t' | b'\r' | 0x0c)
333}
334
335#[inline(always)]
336fn skip_field_ws(bytes: &[u8], mut i: usize) -> usize {
337 while i < bytes.len() && is_field_ws(bytes[i]) {
338 i += 1;
339 }
340 i
341}
342
343fn token_on_line(bytes: &[u8], i: usize) -> String {
344 let mut j = i;
345 while j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
346 j += 1;
347 }
348 String::from_utf8_lossy(&bytes[i..j]).into_owned()
349}
350
351fn count_tokens_on_line(bytes: &[u8], mut i: usize) -> usize {
352 let mut n = 0usize;
353 loop {
354 i = skip_field_ws(bytes, i);
355 if i >= bytes.len() || bytes[i] == b'\n' {
356 break;
357 }
358 n += 1;
359 while i < bytes.len() && !is_field_ws(bytes[i]) && bytes[i] != b'\n' {
360 i += 1;
361 }
362 }
363 n
364}
365
366fn utf8_or_number(bytes: &[u8], line_start: usize, i: usize) -> ParseError {
368 let end = bytes[line_start..]
369 .iter()
370 .position(|b| *b == b'\n')
371 .map(|p| line_start + p)
372 .unwrap_or(bytes.len());
373 if std::str::from_utf8(&bytes[line_start..end]).is_err() {
374 return ParseError::ValidationError("invalid UTF-8".into());
375 }
376 ParseError::InvalidNumberFormat(format!("invalid float: {}", token_on_line(bytes, i)))
377}
378
379#[inline(always)]
380fn parse_f64_at(bytes: &[u8], i: usize, line_start: usize) -> Result<(f64, usize), ParseError> {
381 if i >= bytes.len() || bytes[i] == b'\n' {
382 return Err(ParseError::InvalidVectorLength {
383 expected: 5,
384 found: 0,
385 });
386 }
387 if bytes[i] >= 0x80 {
388 return Err(utf8_or_number(bytes, line_start, i));
389 }
390 if looks_like_nonfinite(bytes, i) {
391 let token = token_on_line(bytes, i);
392 if is_nonfinite_spelling(&token) {
393 return Err(ParseError::NonFiniteValue(token));
394 }
395 }
396 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..]).map_err(|_| {
397 ParseError::InvalidNumberFormat(format!("invalid float: {}", token_on_line(bytes, i)))
398 })?;
399 let next = i + consumed;
400 let boundary = next >= bytes.len() || is_field_ws(bytes[next]) || bytes[next] == b'\n';
401 if !boundary || !val.is_finite() {
402 let token = token_on_line(bytes, i);
403 if is_nonfinite_spelling(&token) || !val.is_finite() {
404 return Err(ParseError::NonFiniteValue(token));
405 }
406 return Err(ParseError::InvalidNumberFormat(format!(
407 "invalid float: {token}"
408 )));
409 }
410 Ok((val, next))
411}
412
413#[inline(always)]
414fn parse_flag_at(
415 bytes: &[u8],
416 i: usize,
417 spec_version: u32,
418) -> Result<([bool; 3], usize), ParseError> {
419 if i >= bytes.len() || bytes[i] == b'\n' {
420 return Err(ParseError::InvalidVectorLength {
421 expected: 5,
422 found: 3,
423 });
424 }
425 let bad = || {
426 ParseError::InvalidFixedFlag(format!(
427 "column 4 flag {:?} is not a legal integer for spec {spec_version}",
428 token_on_line(bytes, i)
429 ))
430 };
431 if !bytes[i].is_ascii_digit() {
432 return Err(bad());
433 }
434 let mut j = i;
435 let mut val: u64 = 0;
436 while j < bytes.len() && bytes[j].is_ascii_digit() {
437 let d = u64::from(bytes[j] - b'0');
438 val = val
439 .checked_mul(10)
440 .and_then(|v| v.checked_add(d))
441 .ok_or_else(bad)?;
442 j += 1;
443 }
444 if j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
445 return Err(bad());
446 }
447 let fixed = if spec_version < 2 {
448 match val {
449 0 => [false, false, false],
450 1 => [true, true, true],
451 _ => return Err(bad()),
452 }
453 } else if val <= 7 {
454 let v = val as u8;
455 [v & 1 != 0, v & 2 != 0, v & 4 != 0]
456 } else {
457 return Err(bad());
458 };
459 Ok((fixed, j))
460}
461
462#[inline(always)]
463fn parse_id_at(bytes: &[u8], i: usize) -> Result<(u64, usize), ParseError> {
464 if i >= bytes.len() || bytes[i] == b'\n' || !bytes[i].is_ascii_digit() {
465 return Err(ParseError::InvalidNumberFormat(format!(
466 "invalid integer: {}",
467 token_on_line(bytes, i)
468 )));
469 }
470 let mut j = i;
471 let mut val: u64 = 0;
472 while j < bytes.len() && bytes[j].is_ascii_digit() {
473 let d = u64::from(bytes[j] - b'0');
474 val = val
475 .checked_mul(10)
476 .and_then(|v| v.checked_add(d))
477 .ok_or_else(|| {
478 ParseError::InvalidNumberFormat(format!(
479 "integer out of range 0..=2^64-1: {}",
480 token_on_line(bytes, i)
481 ))
482 })?;
483 j += 1;
484 }
485 if j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
486 return Err(ParseError::InvalidNumberFormat(format!(
487 "invalid integer: {}",
488 token_on_line(bytes, i)
489 )));
490 }
491 Ok((val, j))
492}
493
494#[inline(always)]
501fn parse_coord_fields(
502 bytes: &[u8],
503 i: usize,
504 spec_version: u32,
505 default_id: Option<u64>,
506) -> Result<(([f64; 3], [bool; 3], u64), usize), ParseError> {
507 if let Some(parsed) = parse_coord_fields_fast(bytes, i, spec_version, default_id) {
508 return Ok(parsed);
509 }
510 parse_coord_fields_detailed(bytes, i, spec_version, default_id)
511}
512
513#[inline(always)]
515fn parse_coord_fields_fast(
516 bytes: &[u8],
517 i: usize,
518 spec_version: u32,
519 default_id: Option<u64>,
520) -> Option<(([f64; 3], [bool; 3], u64), usize)> {
521 let mut i = skip_field_ws(bytes, i);
522 let mut xyz = [0.0f64; 3];
523 for slot in 0..3 {
524 if i >= bytes.len() || bytes[i] == b'\n' || bytes[i] >= 0x80 {
525 return None;
526 }
527 if looks_like_nonfinite(bytes, i) {
528 return None;
529 }
530 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..]).ok()?;
531 if !val.is_finite() {
532 return None;
533 }
534 let next = i + consumed;
535 if next < bytes.len() && !is_field_ws(bytes[next]) && bytes[next] != b'\n' {
536 return None;
537 }
538 xyz[slot] = val;
539 i = skip_field_ws(bytes, next);
540 }
541 let (fixed, next) = parse_flag_fast(bytes, i, spec_version)?;
542 i = skip_field_ws(bytes, next);
543 let (atom_id, next) = if i >= bytes.len() || bytes[i] == b'\n' {
544 (default_id?, i)
545 } else {
546 parse_u64_fast(bytes, i)?
547 };
548 i = skip_field_ws(bytes, next);
549 if i < bytes.len() && bytes[i] != b'\n' {
550 return None;
551 }
552 if i < bytes.len() && bytes[i] == b'\n' {
553 i += 1;
554 }
555 Some(((xyz, fixed, atom_id), i))
556}
557
558#[inline(always)]
559fn parse_u64_fast(bytes: &[u8], i: usize) -> Option<(u64, usize)> {
560 if i >= bytes.len() || !bytes[i].is_ascii_digit() {
561 return None;
562 }
563 let mut j = i;
564 let mut val: u64 = 0;
565 let mut digits = 0u32;
566 while j < bytes.len() && bytes[j].is_ascii_digit() {
567 digits += 1;
568 if digits > 18 {
570 return None;
571 }
572 val = val * 10 + u64::from(bytes[j] - b'0');
573 j += 1;
574 }
575 if j < bytes.len() && !is_field_ws(bytes[j]) && bytes[j] != b'\n' {
576 return None;
577 }
578 Some((val, j))
579}
580
581#[inline(always)]
582fn parse_flag_fast(bytes: &[u8], i: usize, spec_version: u32) -> Option<([bool; 3], usize)> {
583 let (val, j) = parse_u64_fast(bytes, i)?;
584 let fixed = if spec_version < 2 {
585 match val {
586 0 => [false, false, false],
587 1 => [true, true, true],
588 _ => return None,
589 }
590 } else if val <= 7 {
591 let v = val as u8;
592 [v & 1 != 0, v & 2 != 0, v & 4 != 0]
593 } else {
594 return None;
595 };
596 Some((fixed, j))
597}
598
599#[cold]
600#[inline(never)]
601fn parse_coord_fields_detailed(
602 bytes: &[u8],
603 i: usize,
604 spec_version: u32,
605 default_id: Option<u64>,
606) -> Result<(([f64; 3], [bool; 3], u64), usize), ParseError> {
607 let line_start = i;
608 let mut i = skip_field_ws(bytes, i);
609 if i >= bytes.len() || bytes[i] == b'\n' {
610 return Err(ParseError::InvalidVectorLength {
611 expected: 5,
612 found: 0,
613 });
614 }
615 let mut xyz = [0.0f64; 3];
616 for slot in 0..3 {
617 if i >= bytes.len() || bytes[i] == b'\n' {
618 return Err(ParseError::InvalidVectorLength {
619 expected: 5,
620 found: slot,
621 });
622 }
623 if bytes[i] >= 0x80 {
624 return Err(utf8_or_number(bytes, line_start, i));
625 }
626 let (val, next) = parse_f64_at(bytes, i, line_start)?;
627 xyz[slot] = val;
628 i = skip_field_ws(bytes, next);
629 }
630 let (fixed, next) = parse_flag_at(bytes, i, spec_version)?;
631 i = skip_field_ws(bytes, next);
632 let (atom_id, next) = if i >= bytes.len() || bytes[i] == b'\n' {
633 (
634 default_id.ok_or(ParseError::InvalidVectorLength {
635 expected: 5,
636 found: 4,
637 })?,
638 i,
639 )
640 } else {
641 parse_id_at(bytes, i)?
642 };
643 i = skip_field_ws(bytes, next);
644 if i < bytes.len() && bytes[i] != b'\n' {
645 let found = 5 + count_tokens_on_line(bytes, i);
646 return Err(ParseError::InvalidVectorLength { expected: 5, found });
647 }
648 if i < bytes.len() && bytes[i] == b'\n' {
649 i += 1;
650 }
651 Ok(((xyz, fixed, atom_id), i))
652}
653
654pub(crate) fn parse_coord_row_at(
659 bytes: &[u8],
660 i: usize,
661 spec_version: u32,
662 default_id: u64,
663) -> Result<(([f64; 3], [bool; 3], u64), usize), ParseError> {
664 if i >= bytes.len() {
665 return Err(ParseError::IncompleteFrame);
666 }
667 parse_coord_fields(bytes, i, spec_version, Some(default_id))
668}
669
670fn parse_scalar_section_row(
672 line: &str,
673 spec_version: u32,
674) -> Result<(f64, [bool; 3], u64), ParseError> {
675 let bytes = line.as_bytes();
676 let i = skip_ascii_ws(bytes, 0);
677 let (value, i) = parse_finite_at(line, i)?;
678 let i = skip_ascii_ws(bytes, i);
679 let (flag_tok, i) = take_token(line, i).ok_or(ParseError::InvalidVectorLength {
680 expected: 3,
681 found: 1,
682 })?;
683 let fixed = parse_fixed_flag(flag_tok, spec_version)?;
684 let i = skip_ascii_ws(bytes, i);
685 let (id_tok, i) = take_token(line, i).ok_or(ParseError::InvalidVectorLength {
686 expected: 3,
687 found: 2,
688 })?;
689 let atom_id = parse_atom_id_token(id_tok)?;
690 let i = skip_ascii_ws(bytes, i);
691 if i < bytes.len() {
692 let found = 3 + line[i..].split_ascii_whitespace().count();
693 return Err(ParseError::InvalidVectorLength { expected: 3, found });
694 }
695 Ok((value, fixed, atom_id))
696}
697
698fn parse_finite_at(line: &str, i: usize) -> Result<(f64, usize), ParseError> {
699 let bytes = line.as_bytes();
700 if i >= bytes.len() {
701 return Err(ParseError::InvalidVectorLength {
702 expected: 3,
703 found: 0,
704 });
705 }
706 if looks_like_nonfinite(bytes, i) {
709 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
710 if is_nonfinite_spelling(token) {
711 return Err(ParseError::NonFiniteValue(token.to_string()));
712 }
713 }
714 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..]).map_err(|_| {
715 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
716 ParseError::InvalidNumberFormat(format!("invalid float: {token}"))
717 })?;
718 let next = i + consumed;
719 let boundary = next == bytes.len() || bytes[next].is_ascii_whitespace();
720 if !boundary || !val.is_finite() {
721 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
722 if is_nonfinite_spelling(token) || !val.is_finite() {
723 return Err(ParseError::NonFiniteValue(token.to_string()));
724 }
725 return Err(ParseError::InvalidNumberFormat(format!(
726 "invalid float: {token}"
727 )));
728 }
729 Ok((val, next))
730}
731
732fn looks_like_nonfinite(bytes: &[u8], i: usize) -> bool {
735 let mut j = i;
736 if bytes[j] == b'+' || bytes[j] == b'-' {
737 j += 1;
738 if j >= bytes.len() {
739 return false;
740 }
741 }
742 let b = bytes[j];
743 !b.is_ascii_digit() && b != b'.'
744}
745
746fn labels_equal(found: &str, expected: &str) -> bool {
748 let mut found_words = found.split_ascii_whitespace();
749 let mut expected_words = expected.split_ascii_whitespace();
750 loop {
751 match (found_words.next(), expected_words.next()) {
752 (Some(a), Some(b)) => {
753 if !a.eq_ignore_ascii_case(b) {
754 return false;
755 }
756 }
757 (None, None) => return true,
758 _ => return false,
759 }
760 }
761}
762
763fn label_has_prefix(line: &str, prefix: &str) -> bool {
765 let mut found_words = line.split_ascii_whitespace();
766 for word in prefix.split_ascii_whitespace() {
767 match found_words.next() {
768 Some(got) if got.eq_ignore_ascii_case(word) => {}
769 _ => return false,
770 }
771 }
772 true
773}
774
775fn token_count(line: &str) -> usize {
776 line.split_ascii_whitespace().count()
777}
778
779fn json_u64(value: &Value) -> Option<u64> {
780 value.as_u64()
781}
782
783fn validate_section_arity_value(value: &Value) -> Result<(), ParseError> {
784 let obj = value.as_object().ok_or_else(|| {
785 metadata_json_error("section_arity must be an object mapping section names to integers")
786 })?;
787 for (name, entry) in obj {
788 let n = json_u64(entry).ok_or_else(|| {
789 metadata_json_error(format!("section_arity.{name} must be an integer >= 1"))
790 })?;
791 if n < 1 {
792 return Err(ParseError::InvalidCount(format!(
793 "section_arity.{name} must be >= 1"
794 )));
795 }
796 if let Some(expect) = crate::types::reserved_section_value_arity(name)
798 && n != expect
799 {
800 return Err(ParseError::InvalidVectorLength {
801 expected: usize::try_from(expect).unwrap_or(usize::MAX),
802 found: usize::try_from(n).unwrap_or(usize::MAX),
803 });
804 }
805 }
806 Ok(())
807}
808
809pub(crate) fn parse_count_line(line: &str, n: usize) -> Result<Vec<usize>, ParseError> {
813 let mut values = Vec::with_capacity(n);
814 for token in line.split_ascii_whitespace() {
815 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
816 return Err(ParseError::InvalidCount(format!(
817 "count field is not an unsigned integer: {token}"
818 )));
819 }
820 let val: u64 = token.parse::<u64>().map_err(|_| {
821 ParseError::InvalidCount(format!("count field is out of range: {token}"))
822 })?;
823 let val = usize::try_from(val).map_err(|_| {
824 ParseError::InvalidCount(format!("count field is out of range: {token}"))
825 })?;
826 values.push(val);
827 }
828 if values.len() == n {
829 Ok(values)
830 } else {
831 Err(ParseError::InvalidVectorLength {
832 expected: n,
833 found: values.len(),
834 })
835 }
836}
837
838pub(crate) fn line_is_whitespace(line: &str) -> bool {
840 line.split_ascii_whitespace().next().is_none()
841}
842
843pub(crate) fn read_zero_atom_tail<'a>(
849 mut next_line: impl FnMut() -> Option<&'a str>,
850) -> Result<(), ParseError> {
851 let bad = || {
852 ParseError::InvalidCount("a zero-atom frame requires whitespace-only lines 8 and 9".into())
853 };
854 match next_line() {
855 None => Ok(()),
856 Some(line) if line_is_whitespace(line) => match next_line() {
857 None => Ok(()),
858 Some(line) if line_is_whitespace(line) => Ok(()),
859 Some(_) => Err(bad()),
860 },
861 Some(_) => Err(bad()),
862 }
863}
864
865fn metadata_json_error(message: impl Into<String>) -> ParseError {
866 ParseError::InvalidMetadataJson(message.into())
867}
868
869fn validate_metadata_number(key: &str, value: &Value) -> Result<(), ParseError> {
870 if value.as_f64().is_some() {
871 Ok(())
872 } else {
873 Err(metadata_json_error(format!(
874 "{key} must be a finite number"
875 )))
876 }
877}
878
879fn validate_metadata_integer(key: &str, value: &Value) -> Result<(), ParseError> {
880 if value.as_u64().is_some() {
881 Ok(())
882 } else {
883 Err(metadata_json_error(format!(
884 "{key} must be a non-negative integer"
885 )))
886 }
887}
888
889pub fn validate_metadata_schema(
897 json_obj: &serde_json::Map<String, Value>,
898) -> Result<(), ParseError> {
899 let strict_requested = match json_obj.get(meta::VALIDATE) {
900 Some(Value::Bool(b)) => *b,
901 Some(_) => return Err(metadata_json_error("validate must be a boolean")),
902 None => false,
903 };
904
905 match json_obj.get(meta::SECTIONS) {
906 Some(Value::Array(values)) => {
907 if values.iter().any(|entry| !entry.is_string()) {
908 return Err(metadata_json_error("sections must be an array of strings"));
909 }
910 }
911 Some(_) => return Err(metadata_json_error("sections must be an array of strings")),
912 None if strict_requested => {
913 return Err(metadata_json_error(
914 "validate=true requires a sections array, even when empty",
915 ));
916 }
917 None => {}
918 }
919
920 for (key, value) in json_obj {
921 match key.as_str() {
922 meta::CON_SPEC_VERSION | meta::SECTIONS | meta::VALIDATE => {}
923 meta::CON_SPEC_MINOR => {
924 if json_u64(value).is_none() {
925 return Err(metadata_json_error(
926 "con_spec_minor must be a JSON integer >= 0",
927 ));
928 }
929 }
930 meta::SECTION_ARITY => validate_section_arity_value(value)?,
931 meta::ENERGY
932 | meta::TIME
933 | meta::TIMESTEP
934 | meta::CONVERGENCE_FMAX
935 | meta::CONVERGENCE_ENERGY
936 | meta::FMAX => validate_metadata_number(key, value)?,
937 meta::FRAME_INDEX | meta::NEB_BEAD | meta::NEB_BAND => {
938 validate_metadata_integer(key, value)?
939 }
940 meta::GENERATOR if !value.is_string() => {
941 return Err(metadata_json_error("generator must be a string"));
942 }
943 meta::GENERATOR => {}
944 meta::UNITS | meta::POTENTIAL if !value.is_object() => {
945 return Err(metadata_json_error(format!("{key} must be an object")));
946 }
947 meta::POTENTIAL => {
948 if let Some(potential_type) = value.get("type")
949 && !potential_type.is_string()
950 {
951 return Err(metadata_json_error("potential.type must be a string"));
952 }
953 }
954 meta::UNITS => {
955 }
957 meta::PBC => validate_pbc_metadata(value)?,
958 meta::BONDS => validate_bonds_metadata(value)?,
959 meta::LATTICE_VECTORS => validate_lattice_vectors_metadata(value)?,
960 meta::CONVERGED if !value.is_boolean() => {
961 return Err(metadata_json_error("converged must be a boolean"));
962 }
963 meta::CONVERGED => {}
964 _ => {}
965 }
966 }
967
968 Ok(())
969}
970
971fn validate_pbc_metadata(value: &Value) -> Result<(), ParseError> {
972 let Some(values) = value.as_array() else {
973 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
974 };
975 if values.len() != 3 || values.iter().any(|entry| !entry.is_boolean()) {
976 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
977 }
978 Ok(())
979}
980
981fn validate_bonds_metadata(value: &Value) -> Result<(), ParseError> {
988 let Some(items) = value.as_array() else {
989 return Err(metadata_json_error("bonds must be an array"));
990 };
991 for (idx, item) in items.iter().enumerate() {
992 if let Some(pair) = item.as_array() {
993 if pair.len() != 2 {
994 return Err(metadata_json_error(format!(
995 "bonds[{idx}] pair must have exactly two indices"
996 )));
997 }
998 for (k, entry) in pair.iter().enumerate() {
999 let Some(n) = entry.as_u64() else {
1000 return Err(metadata_json_error(format!(
1001 "bonds[{idx}][{k}] must be a non-negative integer"
1002 )));
1003 };
1004 if n > u32::MAX as u64 {
1005 return Err(metadata_json_error(format!(
1006 "bonds[{idx}][{k}] index exceeds u32"
1007 )));
1008 }
1009 }
1010 continue;
1011 }
1012 if let Some(obj) = item.as_object() {
1013 for key in ["i", "j"] {
1014 let Some(entry) = obj.get(key) else {
1015 return Err(metadata_json_error(format!(
1016 "bonds[{idx}] object must include \"{key}\""
1017 )));
1018 };
1019 let Some(n) = entry.as_u64() else {
1020 return Err(metadata_json_error(format!(
1021 "bonds[{idx}].{key} must be a non-negative integer"
1022 )));
1023 };
1024 if n > u32::MAX as u64 {
1025 return Err(metadata_json_error(format!(
1026 "bonds[{idx}].{key} index exceeds u32"
1027 )));
1028 }
1029 }
1030 if let Some(order) = obj.get("order")
1031 && order.as_i64().is_none()
1032 {
1033 return Err(metadata_json_error(format!(
1034 "bonds[{idx}].order must be an integer when present"
1035 )));
1036 }
1037 continue;
1038 }
1039 return Err(metadata_json_error(format!(
1040 "bonds[{idx}] must be [i, j] or {{\"i\": i, \"j\": j, \"order\"?: ...}}"
1041 )));
1042 }
1043 Ok(())
1044}
1045
1046fn validate_lattice_vectors_metadata(value: &Value) -> Result<(), ParseError> {
1047 let Some(rows) = value.as_array() else {
1048 return Err(metadata_json_error(
1049 "lattice_vectors must be a 3x3 numeric array",
1050 ));
1051 };
1052 if rows.len() != 3 {
1053 return Err(metadata_json_error(
1054 "lattice_vectors must be a 3x3 numeric array",
1055 ));
1056 }
1057 for row in rows {
1058 let Some(entries) = row.as_array() else {
1059 return Err(metadata_json_error(
1060 "lattice_vectors must be a 3x3 numeric array",
1061 ));
1062 };
1063 if entries.len() != 3 || entries.iter().any(|entry| entry.as_f64().is_none()) {
1064 return Err(metadata_json_error(
1065 "lattice_vectors must be a 3x3 numeric array",
1066 ));
1067 }
1068 }
1069 Ok(())
1070}
1071
1072pub fn parse_line_of_n<T: std::str::FromStr>(line: &str, n: usize) -> Result<Vec<T>, ParseError>
1100where
1101 ParseError: From<<T as std::str::FromStr>::Err>,
1102{
1103 let values: Vec<T> = line
1104 .split_whitespace()
1105 .map(|s| s.parse::<T>())
1106 .collect::<Result<_, _>>()?;
1107
1108 if values.len() == n {
1109 Ok(values)
1110 } else {
1111 Err(ParseError::InvalidVectorLength {
1112 expected: n,
1113 found: values.len(),
1114 })
1115 }
1116}
1117
1118pub fn parse_frame_header<'a>(
1139 lines: &mut impl Iterator<Item = &'a str>,
1140) -> Result<FrameHeader, ParseError> {
1141 let prebox1 = lines
1142 .next()
1143 .ok_or(ParseError::IncompleteHeader)?
1144 .to_string();
1145 if prebox1.starts_with('\u{feff}') {
1147 return Err(ParseError::ValidationError(
1148 "file begins with a byte-order mark".into(),
1149 ));
1150 }
1151 let prebox2_raw = lines.next().ok_or(ParseError::IncompleteHeader)?;
1152
1153 let trimmed = prebox2_raw.trim();
1158 let (spec_version, metadata, sections, validate, sections_declared) =
1159 if trimmed.starts_with('{') {
1160 let json_val: serde_json::Value = serde_json::from_str(trimmed)
1161 .map_err(|e| ParseError::InvalidMetadataJson(e.to_string()))?;
1162 let json_obj = json_val.as_object().ok_or_else(|| {
1163 ParseError::InvalidMetadataJson("expected a JSON object".to_string())
1164 })?;
1165 let ver_value = json_obj
1166 .get(meta::CON_SPEC_VERSION)
1167 .ok_or(ParseError::MissingSpecVersion)?;
1168 let ver = json_u64(ver_value).ok_or_else(|| {
1169 ParseError::InvalidMetadataJson(
1170 "con_spec_version must be a JSON integer, the major version".into(),
1171 )
1172 })?;
1173 if ver > u64::from(u32::MAX) {
1174 return Err(ParseError::UnsupportedVersion(u32::MAX));
1175 }
1176 let ver = ver as u32;
1177 if ver == 0 || ver == 1 || ver > crate::CON_SPEC_VERSION {
1181 return Err(ParseError::UnsupportedVersion(ver));
1182 }
1183 if ver >= 3 {
1184 match json_obj.get(meta::UNITS) {
1185 Some(u) => crate::units::validate_v3_units_metadata(u)
1186 .map_err(|e| ParseError::InvalidUnits(format!("v3 units: {e}")))?,
1187 None => {
1188 return Err(ParseError::InvalidUnits(
1189 "con_spec_version >= 3 requires metadata \"units\" with length and energy"
1190 .into(),
1191 ));
1192 }
1193 }
1194 }
1195
1196 let mut sections: Vec<String> = Vec::new();
1200 let mut metadata = BTreeMap::new();
1201 let mut sections_declared = false;
1202 let mut validate = false;
1203 for (k, v) in json_obj {
1204 match k.as_str() {
1205 meta::CON_SPEC_VERSION => {}
1206 meta::CON_SPEC_MINOR => {
1207 if json_u64(v).is_none() {
1208 return Err(metadata_json_error(
1209 "con_spec_minor must be a JSON integer >= 0",
1210 ));
1211 }
1212 metadata.insert(k.clone(), v.clone());
1213 }
1214 meta::SECTION_ARITY => {
1215 validate_section_arity_value(v)?;
1216 metadata.insert(k.clone(), v.clone());
1217 }
1218 meta::SECTIONS => {
1219 sections_declared = true;
1220 let arr = v.as_array().ok_or_else(|| {
1221 metadata_json_error("sections must be an array of strings")
1222 })?;
1223 sections.reserve(arr.len());
1224 for entry in arr {
1225 let s = entry.as_str().ok_or_else(|| {
1226 metadata_json_error("sections must be an array of strings")
1227 })?;
1228 sections.push(s.to_string());
1229 }
1230 }
1231 meta::VALIDATE => {
1232 validate = match v {
1233 Value::Bool(b) => *b,
1234 _ => return Err(metadata_json_error("validate must be a boolean")),
1235 };
1236 metadata.insert(k.clone(), v.clone());
1237 }
1238 _ => {
1239 metadata.insert(k.clone(), v.clone());
1240 }
1241 }
1242 }
1243
1244 if validate {
1247 validate_metadata_schema(json_obj)?;
1248 }
1249
1250 (ver, metadata, sections, validate, sections_declared)
1251 } else {
1252 (1_u32, BTreeMap::new(), Vec::new(), false, false)
1254 };
1255 let prebox2 = prebox2_raw.to_string();
1256
1257 let boxl_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
1258 let angles_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
1259 let postbox1 = lines
1260 .next()
1261 .ok_or(ParseError::IncompleteHeader)?
1262 .to_string();
1263 let postbox2 = lines
1264 .next()
1265 .ok_or(ParseError::IncompleteHeader)?
1266 .to_string();
1267 let natm_types_line = lines.next().ok_or(ParseError::IncompleteHeader)?;
1268 let natm_types = parse_count_line(natm_types_line, 1)?[0];
1269 let (natms_per_type, masses_per_type) = if natm_types == 0 {
1272 read_zero_atom_tail(|| lines.next())?;
1273 (Vec::new(), Vec::new())
1274 } else {
1275 let natms_per_type = parse_count_line(
1276 lines.next().ok_or(ParseError::IncompleteHeader)?,
1277 natm_types,
1278 )?;
1279 if natms_per_type.iter().any(|n| *n == 0) {
1280 return Err(ParseError::InvalidCount(
1281 "each per-type count must be >= 1".into(),
1282 ));
1283 }
1284 let masses_per_type = parse_line_of_n_f64(
1285 lines.next().ok_or(ParseError::IncompleteHeader)?,
1286 natm_types,
1287 )?;
1288 (natms_per_type, masses_per_type)
1289 };
1290 if validate {
1291 validate_header_geometry(&boxl_vec, &angles_vec)?;
1292 validate_masses(&masses_per_type)?;
1293 }
1294 Ok(FrameHeader {
1295 prebox_header: PreboxHeader {
1296 user: prebox1,
1297 metadata_line: prebox2,
1298 },
1299 boxl: boxl_vec.try_into().unwrap(),
1300 angles: angles_vec.try_into().unwrap(),
1301 postbox_header: [postbox1, postbox2],
1302 natm_types,
1303 natms_per_type,
1304 masses_per_type,
1305 spec_version,
1306 metadata,
1307 sections,
1308 strict_validation: validate,
1309 sections_declared,
1310 })
1311}
1312
1313pub fn parse_single_frame<'a>(
1362 lines: &mut impl Iterator<Item = &'a str>,
1363) -> Result<ConFrame, ParseError> {
1364 let mut peekable = lines.peekable();
1365 parse_single_frame_ls(&mut peekable)
1366}
1367
1368pub(crate) fn parse_single_frame_ls<'a, L>(lines: &mut L) -> Result<ConFrame, ParseError>
1375where
1376 L: LineStream<'a> + Iterator<Item = &'a str>,
1377{
1378 let header = parse_frame_header(lines)?;
1379 let validate = header.strict_validation;
1380 let total_atoms: usize = header.natms_per_type.iter().sum();
1381 use crate::storage_dtype::{ElementKind, FloatArray2, StorageDtypes};
1382 let dt = StorageDtypes::from_metadata(&header.metadata).unwrap_or_default();
1383 let f64_positions = dt.positions == ElementKind::Float64;
1384 let mut pos_flat = Vec::with_capacity(total_atoms.saturating_mul(3));
1385 let mut positions_other = if f64_positions {
1386 None
1387 } else {
1388 Some(FloatArray2::zeros(dt.positions, total_atoms, 3))
1389 };
1390 let mut ids = Vec::with_capacity(total_atoms);
1391 let mut atom_data: Vec<AtomDatum> = Vec::with_capacity(total_atoms);
1392
1393 let mut global_atom_idx: u64 = 0;
1394 for (type_idx, num_atoms) in header.natms_per_type.iter().enumerate() {
1395 let symbol_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
1396 let symbol: Arc<str> = Arc::from(symbol_line.trim());
1397 let coord_label = lines.next().ok_or(ParseError::IncompleteFrame)?;
1398 if validate {
1399 validate_coordinate_component(type_idx, symbol.as_ref(), coord_label)?;
1400 }
1401 global_atom_idx = ingest_coord_rows(
1402 lines,
1403 *num_atoms,
1404 header.spec_version,
1405 global_atom_idx,
1406 &symbol,
1407 &mut pos_flat,
1408 &mut ids,
1409 &mut positions_other,
1410 &mut atom_data,
1411 )?;
1412 }
1413 let positions = if f64_positions {
1414 FloatArray2::from_f64_row_major(total_atoms, 3, pos_flat)
1415 } else {
1416 positions_other.expect("non-f64 positions allocated")
1417 };
1418 Ok(crate::types::con_frame_coords_only_with_ids(
1419 header, atom_data, positions, ids,
1420 ))
1421}
1422
1423#[inline(always)]
1425fn ingest_coord_rows<'a, L>(
1426 lines: &mut L,
1427 n: usize,
1428 spec_version: u32,
1429 mut global_atom_idx: u64,
1430 symbol: &Arc<str>,
1431 pos_flat: &mut Vec<f64>,
1432 ids: &mut Vec<u64>,
1433 positions_other: &mut Option<crate::storage_dtype::FloatArray2>,
1434 atom_data: &mut Vec<AtomDatum>,
1435) -> Result<u64, ParseError>
1436where
1437 L: LineStream<'a>,
1438{
1439 for _ in 0..n {
1440 let (xyz, fixed, atom_id) = lines.next_coord_row(spec_version, global_atom_idx)?;
1441 if let Some(pos) = positions_other.as_mut() {
1442 pos.set_f64_row(ids.len(), xyz);
1443 }
1444 pos_flat.push(xyz[0]);
1445 pos_flat.push(xyz[1]);
1446 pos_flat.push(xyz[2]);
1447 ids.push(atom_id);
1448 atom_data.push(AtomDatum::coords(
1449 Arc::clone(symbol),
1450 xyz[0],
1451 xyz[1],
1452 xyz[2],
1453 fixed,
1454 atom_id,
1455 ));
1456 global_atom_idx += 1;
1457 }
1458 Ok(global_atom_idx)
1459}
1460
1461fn validate_header_geometry(boxl: &[f64], angles: &[f64]) -> Result<(), ParseError> {
1462 if boxl
1463 .iter()
1464 .any(|length| !length.is_finite() || *length <= 0.0)
1465 || angles
1466 .iter()
1467 .any(|angle| !angle.is_finite() || *angle <= 0.0 || *angle >= 180.0)
1468 {
1469 return Err(ParseError::ValidationError(
1470 "cell geometry must have positive lengths and angles between 0 and 180 degrees"
1471 .to_string(),
1472 ));
1473 }
1474 Ok(())
1475}
1476
1477fn validate_masses(masses_per_type: &[f64]) -> Result<(), ParseError> {
1478 if masses_per_type
1479 .iter()
1480 .any(|mass| !mass.is_finite() || *mass <= 0.0)
1481 {
1482 return Err(ParseError::ValidationError(
1483 "component masses must be positive".to_string(),
1484 ));
1485 }
1486 Ok(())
1487}
1488
1489fn validate_coordinate_component(
1490 type_idx: usize,
1491 symbol: &str,
1492 label: &str,
1493) -> Result<(), ParseError> {
1494 let expected_label = format!("Coordinates of Component {}", type_idx + 1);
1495 if !labels_equal(label, &expected_label) {
1496 return Err(ParseError::ValidationError(format!(
1497 "expected coordinate label {expected_label:?}, found {label:?}"
1498 )));
1499 }
1500 if symbol != "X" && symbol_to_atomic_number(symbol) == 0 {
1501 return Err(ParseError::ValidationError(format!(
1502 "unknown component symbol {symbol}"
1503 )));
1504 }
1505 Ok(())
1506}
1507
1508fn validate_section_component(
1509 section: &str,
1510 type_idx: usize,
1511 atom_idx: usize,
1512 symbol: &str,
1513 label: &str,
1514 header: &FrameHeader,
1515 atom_data: &[AtomDatum],
1516) -> Result<(), ParseError> {
1517 let expected_label = format!("{section} of Component {}", type_idx + 1);
1518 if !labels_equal(label, &expected_label) {
1519 return Err(ParseError::ValidationError(format!(
1520 "expected section label {expected_label:?}, found {label:?}"
1521 )));
1522 }
1523
1524 if header.natms_per_type[type_idx] == 0 {
1525 return Ok(());
1526 }
1527
1528 let expected_symbol = atom_data
1529 .get(atom_idx)
1530 .map(|atom| atom.symbol.as_ref())
1531 .ok_or_else(|| {
1532 ParseError::ValidationError(format!(
1533 "{section} component {} has no coordinate atom to validate against",
1534 type_idx + 1
1535 ))
1536 })?;
1537 if symbol != expected_symbol {
1538 return Err(ParseError::ValidationError(format!(
1539 "{section} component {} symbol mismatch: expected {expected_symbol}, found {symbol}",
1540 type_idx + 1
1541 )));
1542 }
1543
1544 Ok(())
1545}
1546
1547fn validate_section_atom_identity(
1548 section: &str,
1549 atom_idx: usize,
1550 fixed: [bool; 3],
1551 atom_id: u64,
1552 atom_data: &[AtomDatum],
1553) -> Result<(), ParseError> {
1554 let atom = atom_data.get(atom_idx).ok_or_else(|| {
1555 ParseError::ValidationError(format!(
1556 "{section} row {atom_idx} has no coordinate atom to validate against"
1557 ))
1558 })?;
1559
1560 if atom.fixed != fixed {
1561 return Err(ParseError::ValidationError(format!(
1562 "{section} row {atom_idx} fixed mask mismatch for atom_id {}",
1563 atom.atom_id
1564 )));
1565 }
1566 if atom.atom_id != atom_id {
1567 return Err(ParseError::ValidationError(format!(
1568 "{section} row {atom_idx} atom_id mismatch: expected {}, found {atom_id}",
1569 atom.atom_id
1570 )));
1571 }
1572
1573 Ok(())
1574}
1575
1576fn frame_minor(header: &FrameHeader) -> u64 {
1581 header
1582 .metadata
1583 .get(meta::CON_SPEC_MINOR)
1584 .and_then(|v| v.as_u64())
1585 .unwrap_or(0)
1586}
1587
1588fn section_row_arity(header: &FrameHeader, name: &str) -> Option<usize> {
1589 let obj = header.metadata.get(meta::SECTION_ARITY)?.as_object()?;
1590 let n = obj.get(name)?.as_u64()?;
1591 if n >= 1 { Some(n as usize) } else { None }
1592}
1593
1594fn skip_unknown_section<'a>(
1601 lines: &mut impl LineStream<'a>,
1602 header: &FrameHeader,
1603 name: &str,
1604 arity: usize,
1605) -> Result<(), ParseError> {
1606 if !open_section(lines)? {
1607 return Err(ParseError::IncompleteSection(name.to_string()));
1608 }
1609 let with_flag = arity.saturating_add(1);
1610 let with_id = arity.saturating_add(2);
1611 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1612 if lines.next_line().is_none() {
1613 return Err(ParseError::IncompleteSection(name.to_string()));
1614 }
1615 let label = lines
1616 .next_line()
1617 .ok_or_else(|| ParseError::IncompleteSection(name.to_string()))?;
1618 let expected = format!("{name} of Component {}", type_idx + 1);
1619 if !labels_equal(label, &expected) {
1620 return Err(ParseError::IncompleteSection(name.to_string()));
1621 }
1622 for _ in 0..num_atoms {
1623 let row = lines
1624 .next_line()
1625 .ok_or_else(|| ParseError::IncompleteSection(name.to_string()))?;
1626 let found = token_count(row);
1627 if found != with_flag && found != with_id {
1628 let expected = if found < with_flag {
1629 with_flag
1630 } else {
1631 with_id
1632 };
1633 return Err(ParseError::InvalidVectorLength { expected, found });
1634 }
1635 }
1636 }
1637 Ok(())
1638}
1639
1640fn open_section<'a>(lines: &mut impl LineStream<'a>) -> Result<bool, ParseError> {
1641 match lines.peek_line() {
1642 Some(line) if line.trim().is_empty() => {
1643 if lines.remainder_is_whitespace() {
1644 return Ok(false);
1645 }
1646 lines.next_line();
1647 Ok(true)
1648 }
1649 _ => Ok(false),
1650 }
1651}
1652
1653pub fn parse_velocity_section<'a>(
1666 lines: &mut impl LineStream<'a>,
1667 header: &FrameHeader,
1668 atom_data: &mut [AtomDatum],
1669) -> Result<bool, ParseError> {
1670 let validate = header.strict_validation;
1671 match lines.peek_line() {
1674 Some(line) if line.trim().is_empty() => {
1675 if lines.remainder_is_whitespace() {
1676 return Ok(false);
1677 }
1678 lines.next_line();
1679 }
1680 _ => return Ok(false),
1681 }
1682
1683 let mut atom_idx: usize = 0;
1684 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1685 let symbol = lines
1687 .next_line()
1688 .ok_or(ParseError::IncompleteSection(SECTION_VELOCITIES.into()))?
1689 .trim();
1690
1691 let comp_line = lines
1693 .next_line()
1694 .ok_or(ParseError::IncompleteSection(SECTION_VELOCITIES.into()))?;
1695 if !label_has_prefix(comp_line, "Velocities of Component") {
1696 return Err(ParseError::IncompleteSection(SECTION_VELOCITIES.into()));
1697 }
1698 if validate {
1699 validate_section_component(
1700 "Velocities",
1701 type_idx,
1702 atom_idx,
1703 symbol,
1704 comp_line,
1705 header,
1706 atom_data,
1707 )?;
1708 }
1709
1710 for _ in 0..num_atoms {
1711 let vel_line = lines
1712 .next_line()
1713 .ok_or(ParseError::IncompleteSection(SECTION_VELOCITIES.into()))?;
1714 let (vals, fixed, atom_id) = parse_vector_section_row(vel_line, header.spec_version)?;
1715 if validate {
1716 validate_section_atom_identity("velocities", atom_idx, fixed, atom_id, atom_data)?;
1717 }
1718 if atom_idx < atom_data.len() {
1719 atom_data[atom_idx].set_velocity(Some(vals));
1720 }
1721 atom_idx += 1;
1722 }
1723 }
1724
1725 Ok(true)
1726}
1727
1728pub fn parse_force_section<'a>(
1736 lines: &mut impl LineStream<'a>,
1737 header: &FrameHeader,
1738 atom_data: &mut [AtomDatum],
1739) -> Result<bool, ParseError> {
1740 let validate = header.strict_validation;
1741 if !open_section(lines)? {
1742 return Ok(false);
1743 }
1744
1745 let mut atom_idx: usize = 0;
1746 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1747 let symbol = lines
1748 .next_line()
1749 .ok_or(ParseError::IncompleteSection(SECTION_FORCES.into()))?
1750 .trim();
1751
1752 let comp_line = lines
1753 .next_line()
1754 .ok_or(ParseError::IncompleteSection(SECTION_FORCES.into()))?;
1755 if !label_has_prefix(comp_line, "Forces of Component") {
1756 return Err(ParseError::IncompleteSection(SECTION_FORCES.into()));
1757 }
1758 if validate {
1759 validate_section_component(
1760 "Forces", type_idx, atom_idx, symbol, comp_line, header, atom_data,
1761 )?;
1762 }
1763
1764 for _ in 0..num_atoms {
1765 let force_line = lines
1766 .next_line()
1767 .ok_or(ParseError::IncompleteSection(SECTION_FORCES.into()))?;
1768 let (vals, fixed, atom_id) = parse_vector_section_row(force_line, header.spec_version)?;
1769 if validate {
1770 validate_section_atom_identity("forces", atom_idx, fixed, atom_id, atom_data)?;
1771 }
1772 if atom_idx < atom_data.len() {
1773 atom_data[atom_idx].set_force(Some(vals));
1774 }
1775 atom_idx += 1;
1776 }
1777 }
1778
1779 Ok(true)
1780}
1781
1782pub fn parse_energy_section<'a>(
1795 lines: &mut impl LineStream<'a>,
1796 header: &FrameHeader,
1797 atom_data: &mut [AtomDatum],
1798) -> Result<bool, ParseError> {
1799 let validate = header.strict_validation;
1800 if !open_section(lines)? {
1801 return Ok(false);
1802 }
1803
1804 let mut atom_idx: usize = 0;
1805 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1806 let symbol = lines
1807 .next_line()
1808 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?
1809 .trim();
1810
1811 let comp_line = lines
1812 .next_line()
1813 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?;
1814 if !label_has_prefix(comp_line, "Energies of Component") {
1815 return Err(ParseError::IncompleteSection(SECTION_ENERGIES.into()));
1816 }
1817 if validate {
1818 validate_section_component(
1819 "Energies", type_idx, atom_idx, symbol, comp_line, header, atom_data,
1820 )?;
1821 }
1822
1823 for _ in 0..num_atoms {
1824 let energy_line = lines
1825 .next_line()
1826 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?;
1827 let (value, fixed, atom_id) =
1828 parse_scalar_section_row(energy_line, header.spec_version)?;
1829 if validate {
1830 validate_section_atom_identity("energies", atom_idx, fixed, atom_id, atom_data)?;
1831 }
1832 if atom_idx < atom_data.len() {
1833 atom_data[atom_idx].set_energy(Some(value));
1834 }
1835 atom_idx += 1;
1836 }
1837 }
1838
1839 Ok(true)
1840}
1841
1842pub fn parse_declared_sections<'a>(
1848 lines: &mut impl LineStream<'a>,
1849 header: &mut FrameHeader,
1850 atom_data: &mut [AtomDatum],
1851) -> Result<usize, ParseError> {
1852 let mut applied = 0usize;
1853 if header.natm_types == 0 {
1856 if !header.sections.is_empty() {
1857 return Err(ParseError::InvalidCount(
1858 "a zero-atom frame cannot declare sections".into(),
1859 ));
1860 }
1861 return Ok(0);
1862 }
1863 if !header.sections_declared && header.sections.is_empty() {
1864 let found =
1868 lines.blank_opens_velocity_block() && parse_velocity_section(lines, header, atom_data)?;
1869 if found {
1870 header.sections.push(SECTION_VELOCITIES.into());
1871 applied = 1;
1872 }
1873 } else {
1874 let sections = std::mem::take(&mut header.sections);
1875 for section in §ions {
1876 match section.as_str() {
1877 SECTION_VELOCITIES => {
1878 let found = parse_velocity_section(lines, header, atom_data)?;
1879 if !found {
1880 return Err(ParseError::IncompleteSection(SECTION_VELOCITIES.into()));
1881 }
1882 applied += 1;
1883 }
1884 SECTION_FORCES => {
1885 let found = parse_force_section(lines, header, atom_data)?;
1886 if !found {
1887 return Err(ParseError::IncompleteSection(SECTION_FORCES.into()));
1888 }
1889 applied += 1;
1890 }
1891 SECTION_ENERGIES => {
1892 let found = parse_energy_section(lines, header, atom_data)?;
1893 if !found {
1894 return Err(ParseError::IncompleteSection(SECTION_ENERGIES.into()));
1895 }
1896 applied += 1;
1897 }
1898 SECTION_CHARGES => {
1899 let found = parse_charge_section(lines, header, atom_data)?;
1900 if !found {
1901 return Err(ParseError::IncompleteSection(SECTION_CHARGES.into()));
1902 }
1903 applied += 1;
1904 }
1905 SECTION_SPINS => {
1906 let found = parse_spin_section(lines, header, atom_data)?;
1907 if !found {
1908 return Err(ParseError::IncompleteSection(SECTION_SPINS.into()));
1909 }
1910 applied += 1;
1911 }
1912 SECTION_MAGMOMS => {
1913 let found = parse_magmom_section(lines, header, atom_data)?;
1914 if !found {
1915 return Err(ParseError::IncompleteSection(SECTION_MAGMOMS.into()));
1916 }
1917 applied += 1;
1918 }
1919 SECTION_DISPLACEMENTS => {
1920 let found = parse_displacement_section(lines, header, atom_data)?;
1921 if !found {
1922 return Err(ParseError::IncompleteSection(SECTION_DISPLACEMENTS.into()));
1923 }
1924 applied += 1;
1925 }
1926 SECTION_SPREADS => {
1927 let found = parse_spread_section(lines, header, atom_data)?;
1928 if !found {
1929 return Err(ParseError::IncompleteSection(SECTION_SPREADS.into()));
1930 }
1931 applied += 1;
1932 }
1933 other => {
1934 if frame_minor(header) > u64::from(crate::CON_SPEC_MINOR) {
1935 if let Some(arity) = section_row_arity(header, other) {
1936 skip_unknown_section(lines, header, other, arity)?;
1937 continue;
1938 }
1939 }
1940 return Err(ParseError::UnknownSection(other.to_string()));
1941 }
1942 }
1943 }
1944 header.sections = sections;
1945 }
1946 reject_velocities_without_time(header)?;
1947 Ok(applied)
1948}
1949
1950fn reject_velocities_without_time(header: &FrameHeader) -> Result<(), ParseError> {
1954 if header.spec_version < 3 || frame_minor(header) < 1 {
1955 return Ok(());
1956 }
1957 if !header
1958 .sections
1959 .iter()
1960 .any(|name| name == SECTION_VELOCITIES)
1961 {
1962 return Ok(());
1963 }
1964 let time_ok = header
1965 .metadata
1966 .get(meta::UNITS)
1967 .and_then(|units| units.get("time"))
1968 .and_then(|value| value.as_str())
1969 .is_some_and(|unit| !unit.trim().is_empty());
1970 if time_ok {
1971 Ok(())
1972 } else {
1973 Err(ParseError::InvalidUnits(
1974 "minor >= 1 requires units.time when a velocities section is present".into(),
1975 ))
1976 }
1977}
1978
1979fn parse_scalar_atom_section<'a>(
1982 lines: &mut impl LineStream<'a>,
1983 header: &FrameHeader,
1984 atom_data: &mut [AtomDatum],
1985 section_name: &str,
1986 component_label: &str,
1987 mut set_value: impl FnMut(&mut AtomDatum, f64),
1988) -> Result<bool, ParseError> {
1989 let validate = header.strict_validation;
1990 if !open_section(lines)? {
1991 return Ok(false);
1992 }
1993
1994 let mut atom_idx: usize = 0;
1995 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1996 let symbol = lines
1997 .next_line()
1998 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?
1999 .trim();
2000
2001 let comp_line = lines
2002 .next_line()
2003 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
2004 if !label_has_prefix(comp_line, component_label) {
2005 return Err(ParseError::IncompleteSection(section_name.into()));
2006 }
2007 if validate {
2008 validate_section_component(
2009 component_label.trim_end_matches(" of Component").trim(),
2010 type_idx,
2011 atom_idx,
2012 symbol,
2013 comp_line,
2014 header,
2015 atom_data,
2016 )?;
2017 }
2018
2019 for _ in 0..num_atoms {
2020 let data_line = lines
2021 .next_line()
2022 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
2023 let (value, fixed, atom_id) = parse_scalar_section_row(data_line, header.spec_version)?;
2024 if validate {
2025 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
2026 }
2027 if atom_idx < atom_data.len() {
2028 set_value(&mut atom_data[atom_idx], value);
2029 }
2030 atom_idx += 1;
2031 }
2032 }
2033 Ok(true)
2034}
2035
2036pub fn parse_charge_section<'a>(
2037 lines: &mut impl LineStream<'a>,
2038 header: &FrameHeader,
2039 atom_data: &mut [AtomDatum],
2040) -> Result<bool, ParseError> {
2041 parse_scalar_atom_section(
2042 lines,
2043 header,
2044 atom_data,
2045 SECTION_CHARGES,
2046 "Charges of Component",
2047 |a, v| a.set_charge(Some(v)),
2048 )
2049}
2050
2051pub fn parse_spin_section<'a>(
2052 lines: &mut impl LineStream<'a>,
2053 header: &FrameHeader,
2054 atom_data: &mut [AtomDatum],
2055) -> Result<bool, ParseError> {
2056 parse_scalar_atom_section(
2057 lines,
2058 header,
2059 atom_data,
2060 SECTION_SPINS,
2061 "Spins of Component",
2062 |a, v| a.set_spin(Some(v)),
2063 )
2064}
2065
2066fn parse_vector_named_section<'a>(
2067 lines: &mut impl LineStream<'a>,
2068 header: &FrameHeader,
2069 atom_data: &mut [AtomDatum],
2070 section_name: &str,
2071 component_label: &str,
2072 label_kind: &str,
2073 mut set_value: impl FnMut(&mut AtomDatum, [f64; 3]),
2074 mut check_row: impl FnMut(usize, [f64; 3]) -> Result<(), ParseError>,
2075) -> Result<bool, ParseError> {
2076 let validate = header.strict_validation;
2077 if !open_section(lines)? {
2078 return Ok(false);
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 let comp_line = lines
2087 .next_line()
2088 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
2089 if !label_has_prefix(comp_line, component_label) {
2090 return Err(ParseError::IncompleteSection(section_name.into()));
2091 }
2092 if validate {
2093 validate_section_component(
2094 label_kind, type_idx, atom_idx, symbol, comp_line, header, atom_data,
2095 )?;
2096 }
2097 for _ in 0..num_atoms {
2098 let row = lines
2099 .next_line()
2100 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
2101 let (vals, fixed, atom_id) = parse_vector_section_row(row, header.spec_version)?;
2102 check_row(atom_idx, vals)?;
2103 if validate {
2104 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
2105 }
2106 if atom_idx < atom_data.len() {
2107 set_value(&mut atom_data[atom_idx], vals);
2108 }
2109 atom_idx += 1;
2110 }
2111 }
2112 Ok(true)
2113}
2114
2115pub fn parse_magmom_section<'a>(
2117 lines: &mut impl LineStream<'a>,
2118 header: &FrameHeader,
2119 atom_data: &mut [AtomDatum],
2120) -> Result<bool, ParseError> {
2121 parse_vector_named_section(
2122 lines,
2123 header,
2124 atom_data,
2125 SECTION_MAGMOMS,
2126 "Magmoms of Component",
2127 "Magmoms",
2128 |a, v| a.set_magmom(Some(v)),
2129 |_, _| Ok(()),
2130 )
2131}
2132
2133pub fn parse_displacement_section<'a>(
2135 lines: &mut impl LineStream<'a>,
2136 header: &FrameHeader,
2137 atom_data: &mut [AtomDatum],
2138) -> Result<bool, ParseError> {
2139 parse_vector_named_section(
2140 lines,
2141 header,
2142 atom_data,
2143 SECTION_DISPLACEMENTS,
2144 "Displacements of Component",
2145 "Displacements",
2146 |a, v| a.set_displacement(Some(v)),
2147 |_, _| Ok(()),
2148 )
2149}
2150
2151pub fn parse_spread_section<'a>(
2154 lines: &mut impl LineStream<'a>,
2155 header: &FrameHeader,
2156 atom_data: &mut [AtomDatum],
2157) -> Result<bool, ParseError> {
2158 parse_vector_named_section(
2159 lines,
2160 header,
2161 atom_data,
2162 SECTION_SPREADS,
2163 "Spreads of Component",
2164 "Spreads",
2165 |a, v| a.set_spread(Some(v)),
2166 |atom_idx, vals| {
2167 if !vals.iter().all(|v| v.is_finite()) {
2168 return Err(ParseError::NonFiniteValue(format!(
2169 "spreads atom {atom_idx}"
2170 )));
2171 }
2172 if vals.iter().any(|v| *v < 0.0) {
2173 return Err(crate::types::invalid_spread(atom_idx));
2174 }
2175 Ok(())
2176 },
2177 )
2178}
2179
2180#[cfg(test)]
2181mod tests {
2182 use super::*;
2183 use crate::iterators::ConFrameIterator;
2184
2185 #[test]
2186 fn test_parse_line_of_n_success() {
2187 let line = "1.0 2.5 -3.0";
2188 let values = parse_line_of_n::<f64>(line, 3).unwrap();
2189 assert_eq!(values, vec![1.0, 2.5, -3.0]);
2190 }
2191
2192 #[test]
2193 fn test_parse_line_of_n_too_short() {
2194 let line = "1.0 2.5";
2195 let result = parse_line_of_n::<f64>(line, 3);
2196 assert!(result.is_err());
2197 assert!(matches!(
2198 result.unwrap_err(),
2199 ParseError::InvalidVectorLength {
2200 expected: 3,
2201 found: 2
2202 }
2203 ));
2204 }
2205
2206 #[test]
2207 fn test_parse_line_of_n_too_long() {
2208 let line = "1.0 2.5 -3.0 4.0";
2209 let result = parse_line_of_n::<f64>(line, 3);
2210 assert!(result.is_err());
2211 assert!(matches!(
2212 result.unwrap_err(),
2213 ParseError::InvalidVectorLength {
2214 expected: 3,
2215 found: 4
2216 }
2217 ));
2218 }
2219
2220 #[test]
2221 fn test_parse_line_of_n_invalid_float() {
2222 let line = "1.0 abc -3.0";
2223 let result = parse_line_of_n::<f64>(line, 3);
2224 assert!(result.is_err());
2225 assert!(matches!(
2226 result.unwrap_err(),
2227 ParseError::InvalidNumberFormat(_)
2228 ));
2229 }
2230
2231 #[test]
2232 fn test_parse_frame_header_success() {
2233 let lines = [
2234 "PREBOX1",
2235 "{\"con_spec_version\":2}",
2236 "10.0 20.0 30.0",
2237 "90.0 90.0 90.0",
2238 "POSTBOX1",
2239 "POSTBOX2",
2240 "2",
2241 "1 1",
2242 "12.011 1.008",
2243 ];
2244 let mut line_it = lines.iter().copied();
2245 match parse_frame_header(&mut line_it) {
2246 Ok(header) => {
2247 assert_eq!(header.prebox_header.user, "PREBOX1");
2248 assert_eq!(header.spec_version, 2);
2249 assert_eq!(header.boxl, [10.0, 20.0, 30.0]);
2250 assert_eq!(header.angles, [90.0, 90.0, 90.0]);
2251 assert_eq!(header.postbox_header, ["POSTBOX1", "POSTBOX2"]);
2252 assert_eq!(header.natm_types, 2);
2253 assert_eq!(header.natms_per_type, vec![1, 1]);
2254 assert_eq!(header.masses_per_type, vec![12.011, 1.008]);
2255 }
2256 Err(e) => {
2257 panic!(
2258 "Parsing failed when it should have succeeded. Error: {:?}",
2259 e
2260 );
2261 }
2262 }
2263 }
2264
2265 #[test]
2266 fn test_parse_frame_header_missing_line() {
2267 let lines = [
2268 "PREBOX1",
2269 "{\"con_spec_version\":2}",
2270 "10.0 20.0 30.0",
2271 "90.0 90.0 90.0",
2272 "POSTBOX1",
2273 "POSTBOX2",
2274 "2",
2275 "1 1",
2276 ];
2278 let mut line_it = lines.iter().copied();
2279 let result = parse_frame_header(&mut line_it);
2280 assert!(result.is_err());
2281 assert!(matches!(result.unwrap_err(), ParseError::IncompleteHeader));
2282 }
2283
2284 #[test]
2285 fn test_parse_frame_header_missing_spec_version() {
2286 let lines = vec![
2287 "PREBOX1",
2288 "{}",
2289 "10.0 20.0 30.0",
2290 "90.0 90.0 90.0",
2291 "POSTBOX1",
2292 "POSTBOX2",
2293 "2",
2294 "1 1",
2295 "12.011 1.008",
2296 ];
2297 let mut line_it = lines.iter().copied();
2298 let result = parse_frame_header(&mut line_it);
2299 assert!(result.is_err());
2300 assert!(matches!(
2301 result.unwrap_err(),
2302 ParseError::MissingSpecVersion
2303 ));
2304 }
2305
2306 #[test]
2307 fn test_parse_frame_header_legacy_no_json() {
2308 let lines = vec![
2310 "PREBOX1",
2311 "0.0000 TIME",
2312 "10.0 20.0 30.0",
2313 "90.0 90.0 90.0",
2314 "POSTBOX1",
2315 "POSTBOX2",
2316 "2",
2317 "1 1",
2318 "12.011 1.008",
2319 ];
2320 let mut line_it = lines.iter().copied();
2321 let header = parse_frame_header(&mut line_it).unwrap();
2322 assert_eq!(header.spec_version, 1);
2323 assert!(header.metadata.is_empty());
2324 }
2325
2326 #[test]
2327 fn test_parse_frame_header_malformed_json() {
2328 let lines = vec![
2330 "PREBOX1",
2331 "{broken json",
2332 "10.0 20.0 30.0",
2333 "90.0 90.0 90.0",
2334 "POSTBOX1",
2335 "POSTBOX2",
2336 "2",
2337 "1 1",
2338 "12.011 1.008",
2339 ];
2340 let mut line_it = lines.iter().copied();
2341 let result = parse_frame_header(&mut line_it);
2342 assert!(result.is_err());
2343 assert!(matches!(
2344 result.unwrap_err(),
2345 ParseError::InvalidMetadataJson(_)
2346 ));
2347 }
2348
2349 #[test]
2350 fn test_parse_frame_header_unsupported_version() {
2351 let lines = vec![
2352 "PREBOX1",
2353 "{\"con_spec_version\":999}",
2354 "10.0 20.0 30.0",
2355 "90.0 90.0 90.0",
2356 "POSTBOX1",
2357 "POSTBOX2",
2358 "2",
2359 "1 1",
2360 "12.011 1.008",
2361 ];
2362 let mut line_it = lines.iter().copied();
2363 let result = parse_frame_header(&mut line_it);
2364 assert!(result.is_err());
2365 assert!(matches!(
2366 result.unwrap_err(),
2367 ParseError::UnsupportedVersion(999)
2368 ));
2369 }
2370
2371 #[test]
2372 fn test_v3_missing_units_rejected() {
2373 let lines = vec![
2374 "PREBOX1",
2375 "{\"con_spec_version\":3}",
2376 "10.0 20.0 30.0",
2377 "90.0 90.0 90.0",
2378 "POSTBOX1",
2379 "POSTBOX2",
2380 "1",
2381 "1",
2382 "1.0",
2383 ];
2384 let mut line_it = lines.iter().copied();
2385 let err = parse_frame_header(&mut line_it).unwrap_err();
2386 let msg = err.to_string();
2387 assert!(
2388 msg.contains("units") || msg.contains("v3"),
2389 "expected units error, got {msg}"
2390 );
2391 }
2392
2393 #[test]
2394 fn test_v3_invalid_units_rejected() {
2395 let lines = vec![
2396 "PREBOX1",
2397 r#"{"con_spec_version":3,"units":{"length":"eV","energy":"angstrom"}}"#,
2398 "10.0 20.0 30.0",
2399 "90.0 90.0 90.0",
2400 "POSTBOX1",
2401 "POSTBOX2",
2402 "1",
2403 "1",
2404 "1.0",
2405 ];
2406 let mut line_it = lines.iter().copied();
2407 assert!(parse_frame_header(&mut line_it).is_err());
2408 }
2409
2410 #[test]
2411 fn test_v3_valid_units_exposes_length_energy() {
2412 let lines = vec![
2413 "PREBOX1",
2414 r#"{"con_spec_version":3,"units":{"length":"angstrom","energy":"eV","mass":"amu","time":"fs"}}"#,
2415 "10.0 20.0 30.0",
2416 "90.0 90.0 90.0",
2417 "POSTBOX1",
2418 "POSTBOX2",
2419 "1",
2420 "1",
2421 "1.0",
2422 ];
2423 let mut line_it = lines.iter().copied();
2424 let header = parse_frame_header(&mut line_it).unwrap();
2425 assert_eq!(header.spec_version, 3);
2426 assert_eq!(header.length_unit(), Some("angstrom"));
2427 assert_eq!(header.energy_unit(), Some("eV"));
2428 let f = header.conversion_factor_to("length", "nm").unwrap();
2429 assert!((f - 0.1).abs() < 1e-12);
2430 }
2431
2432 #[test]
2433 fn test_parse_frame_header_extra_metadata_preserved() {
2434 let lines = vec![
2435 "PREBOX1",
2436 "{\"con_spec_version\":2,\"generator\":\"test\"}",
2437 "10.0 20.0 30.0",
2438 "90.0 90.0 90.0",
2439 "POSTBOX1",
2440 "POSTBOX2",
2441 "2",
2442 "1 1",
2443 "12.011 1.008",
2444 ];
2445 let mut line_it = lines.iter().copied();
2446 let header = parse_frame_header(&mut line_it).unwrap();
2447 assert_eq!(header.spec_version, 2);
2448 assert_eq!(
2449 header.metadata.get("generator"),
2450 Some(&serde_json::Value::String("test".to_string()))
2451 );
2452 }
2453
2454 #[test]
2455 fn test_parse_frame_header_invalid_natms_per_type() {
2456 let lines = vec![
2457 "PREBOX1",
2458 "{\"con_spec_version\":2}",
2459 "10.0 20.0 30.0",
2460 "90.0 90.0 90.0",
2461 "POSTBOX1",
2462 "POSTBOX2",
2463 "2",
2464 "1 1 1", "12.011 1.008",
2466 ];
2467 let mut line_it = lines.iter().copied();
2468 let result = parse_frame_header(&mut line_it);
2469 assert!(result.is_err());
2470 assert!(matches!(
2471 result.unwrap_err(),
2472 ParseError::InvalidVectorLength {
2473 expected: 2,
2474 found: 3
2475 }
2476 ));
2477 }
2478
2479 #[test]
2480 fn test_parse_single_frame_success() {
2481 let lines = vec![
2482 "PREBOX1",
2483 "{\"con_spec_version\":2}",
2484 "10.0 20.0 30.0",
2485 "90.0 90.0 90.0",
2486 "POSTBOX1",
2487 "POSTBOX2",
2488 "2",
2489 "3 3",
2490 "12.011 1.008",
2491 "1",
2492 "Coordinates of Component 1",
2493 "0.0 0.0 0.0 0 1",
2494 "1.0940 0.0 0.0 0 2",
2495 "-0.5470 0.9499 0.0 0 3",
2496 "2",
2497 "Coordinates of Component 2",
2498 "5.0 5.0 5.0 0 4",
2499 "6.0940 5.0 5.0 0 5",
2500 "5.5470 5.9499 5.0 0 6",
2501 ];
2502 let mut line_it = lines.iter().copied();
2503 let frame = parse_single_frame(&mut line_it).unwrap();
2504
2505 assert_eq!(frame.header.natm_types, 2);
2506 assert_eq!(frame.header.natms_per_type, vec![3, 3]);
2507 assert_eq!(frame.header.masses_per_type, vec![12.011, 1.008]);
2508 assert_eq!(frame.atom_data.len(), 6);
2509 assert_eq!(&*frame.atom_data[0].symbol, "1");
2510 assert_eq!(frame.atom_data[0].atom_id, 1);
2511 assert_eq!(&*frame.atom_data[5].symbol, "2");
2512 assert_eq!(frame.atom_data[5].atom_id, 6);
2513 }
2514
2515 #[test]
2516 fn test_column_4_value_1_is_x_only_on_spec_2() {
2517 let lines = vec![
2518 "Generated by probe",
2519 "{\"con_spec_version\":2}",
2520 "20.0 20.0 20.0",
2521 "90.0 90.0 90.0",
2522 "",
2523 "",
2524 "1",
2525 "1",
2526 "63.5",
2527 "Cu",
2528 "Coordinates of component 1",
2529 "1.0 2.0 3.0 1 1",
2530 ];
2531 let mut line_it = lines.iter().copied();
2532 let frame = parse_single_frame(&mut line_it).unwrap();
2533 assert_eq!(frame.header.spec_version, 2);
2534 assert_eq!(frame.atom_data[0].fixed, [true, false, false]);
2535 }
2536
2537 #[test]
2538 fn test_column_4_value_1_is_all_fixed_on_spec_1() {
2539 let lines = vec![
2540 "Generated by probe",
2541 "0.0000 TIME",
2542 "20.0 20.0 20.0",
2543 "90.0 90.0 90.0",
2544 "",
2545 "",
2546 "1",
2547 "1",
2548 "63.5",
2549 "Cu",
2550 "Coordinates of component 1",
2551 "1.0 2.0 3.0 1 1",
2552 ];
2553 let mut line_it = lines.iter().copied();
2554 let frame = parse_single_frame(&mut line_it).unwrap();
2555 assert_eq!(frame.header.spec_version, 1);
2556 assert_eq!(frame.atom_data[0].fixed, [true, true, true]);
2557 }
2558
2559 #[test]
2560 fn test_parse_single_frame_missing_line() {
2561 let lines = vec![
2563 "PREBOX1",
2564 "{\"con_spec_version\":2}",
2565 "10.0 20.0 30.0",
2566 "90.0 90.0 90.0",
2567 "POSTBOX1",
2568 "POSTBOX2",
2569 "2",
2570 "3 3",
2571 "12.011 1.008",
2572 "1",
2573 "Coordinates of Component 1",
2574 "0.0 0.0 0.0 0 1",
2575 "1.0940 0.0 0.0 0 2",
2576 "-0.5470 0.9499 0.0 0 3",
2577 ];
2579 let mut line_it = lines.iter().copied();
2580 let result = parse_single_frame(&mut line_it);
2581 assert!(result.is_err());
2582 assert!(matches!(result.unwrap_err(), ParseError::IncompleteFrame));
2583 }
2584
2585 #[test]
2586 fn test_parse_single_frame_missing_atom_index_defaults_sequential() {
2587 let lines = vec![
2589 "PREBOX1",
2590 "{\"con_spec_version\":2}",
2591 "10.0 20.0 30.0",
2592 "90.0 90.0 90.0",
2593 "POSTBOX1",
2594 "POSTBOX2",
2595 "2",
2596 "3 3",
2597 "12.011 1.008",
2598 "1",
2599 "Coordinates of Component 1",
2600 "0.0 0.0 0.0 0 1",
2601 "1.0940 0.0 0.0 0 2",
2602 "-0.5470 0.9499 0.0 0 3",
2603 "2",
2604 "Coordinates of Component 2",
2605 "5.0 5.0 5.0 0", "6.0940 5.0 5.0 0 10", "5.5470 5.9499 5.0 0", ];
2609 let mut line_it = lines.iter().copied();
2610 let frame = parse_single_frame(&mut line_it).unwrap();
2611 assert_eq!(frame.atom_data.len(), 6);
2612 assert_eq!(frame.atom_data[0].atom_id, 1);
2614 assert_eq!(frame.atom_data[1].atom_id, 2);
2615 assert_eq!(frame.atom_data[2].atom_id, 3);
2616 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); }
2621
2622 #[test]
2623 fn test_parse_single_frame_too_few_columns_fails() {
2624 let lines = vec![
2626 "PREBOX1",
2627 "{\"con_spec_version\":2}",
2628 "10.0 20.0 30.0",
2629 "90.0 90.0 90.0",
2630 "POSTBOX1",
2631 "POSTBOX2",
2632 "1",
2633 "1",
2634 "12.011",
2635 "C",
2636 "Coordinates of Component 1",
2637 "0.0 0.0 0.0", ];
2639 let mut line_it = lines.iter().copied();
2640 let result = parse_single_frame(&mut line_it);
2641 assert!(result.is_err());
2642 assert!(matches!(
2643 result.unwrap_err(),
2644 ParseError::InvalidVectorLength {
2645 expected: 5,
2646 found: 3
2647 }
2648 ));
2649 }
2650
2651 #[test]
2652 fn test_parse_velocity_section_present() {
2653 let lines = vec![
2654 "PREBOX1",
2655 "{\"con_spec_version\":2}",
2656 "10.0 20.0 30.0",
2657 "90.0 90.0 90.0",
2658 "POSTBOX1",
2659 "POSTBOX2",
2660 "2",
2661 "1 1",
2662 "63.546 1.008",
2663 "Cu",
2664 "Coordinates of Component 1",
2665 "0.0 0.0 0.0 1 0",
2666 "H",
2667 "Coordinates of Component 2",
2668 "1.0 2.0 3.0 0 1",
2669 "",
2670 "Cu",
2671 "Velocities of Component 1",
2672 "0.1 0.2 0.3 1 0",
2673 "H",
2674 "Velocities of Component 2",
2675 "0.4 0.5 0.6 0 1",
2676 ];
2677 let mut line_it = lines.iter().copied().peekable();
2678 let mut frame =
2680 parse_single_frame(&mut line_it).expect("coordinate parsing should succeed");
2681 assert!(!frame.has_velocities());
2682
2683 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
2685 .expect("velocity parsing should succeed");
2686 assert!(has_vel);
2687 assert_eq!(frame.atom_data[0].velocity(), Some([0.1, 0.2, 0.3]));
2688 assert_eq!(frame.atom_data[1].velocity(), Some([0.4, 0.5, 0.6]));
2689 }
2690
2691 #[test]
2692 fn test_validate_true_accepts_matching_section_identity() {
2693 let text = r#"
2694PREBOX1
2695{"con_spec_version":2,"sections":["velocities"],"validate":true}
269610.0 20.0 30.0
269790.0 90.0 90.0
2698POSTBOX1
2699POSTBOX2
27002
27011 1
270263.546 1.008
2703Cu
2704Coordinates of Component 1
27050.0 0.0 0.0 5 0
2706H
2707Coordinates of Component 2
27081.0 2.0 3.0 0 1
2709
2710Cu
2711Velocities of Component 1
27120.1 0.2 0.3 5 0
2713H
2714Velocities of Component 2
27150.4 0.5 0.6 0 1
2716"#;
2717 let mut iter = ConFrameIterator::new(text.trim());
2718 let frame = iter.next().unwrap().unwrap();
2719
2720 assert!(frame.has_velocities());
2721 assert_eq!(
2722 frame
2723 .header
2724 .metadata
2725 .get("validate")
2726 .and_then(|v| v.as_bool()),
2727 Some(true)
2728 );
2729 }
2730
2731 #[test]
2732 fn test_validate_true_rejects_section_atom_id_mismatch() {
2733 let text = r#"
2734PREBOX1
2735{"con_spec_version":2,"sections":["velocities"],"validate":true}
273610.0 20.0 30.0
273790.0 90.0 90.0
2738POSTBOX1
2739POSTBOX2
27401
27411
274263.546
2743Cu
2744Coordinates of Component 1
27450.0 0.0 0.0 5 0
2746
2747Cu
2748Velocities of Component 1
27490.1 0.2 0.3 5 99
2750"#;
2751 let mut iter = ConFrameIterator::new(text.trim());
2752 let err = iter.next().unwrap().unwrap_err();
2753
2754 assert!(matches!(err, ParseError::ValidationError(_)));
2755 assert!(err.to_string().contains("atom_id mismatch"));
2756 }
2757
2758 #[test]
2759 fn test_validate_true_rejects_section_symbol_mismatch() {
2760 let text = r#"
2761PREBOX1
2762{"con_spec_version":2,"sections":["forces"],"validate":true}
276310.0 20.0 30.0
276490.0 90.0 90.0
2765POSTBOX1
2766POSTBOX2
27671
27681
276963.546
2770Cu
2771Coordinates of Component 1
27720.0 0.0 0.0 5 0
2773
2774H
2775Forces of Component 1
27760.1 0.2 0.3 5 0
2777"#;
2778 let mut iter = ConFrameIterator::new(text.trim());
2779 let err = iter.next().unwrap().unwrap_err();
2780
2781 assert!(matches!(err, ParseError::ValidationError(_)));
2782 assert!(err.to_string().contains("symbol mismatch"));
2783 }
2784
2785 #[test]
2786 fn test_validate_absent_allows_legacy_duplicate_identity_mismatch() {
2787 let text = r#"
2788PREBOX1
2789{"con_spec_version":2,"sections":["velocities"]}
279010.0 20.0 30.0
279190.0 90.0 90.0
2792POSTBOX1
2793POSTBOX2
27941
27951
279663.546
2797Cu
2798Coordinates of Component 1
27990.0 0.0 0.0 5 0
2800
2801Cu
2802Velocities of Component 1
28030.1 0.2 0.3 0 99
2804"#;
2805 let mut iter = ConFrameIterator::new(text.trim());
2806 let frame = iter.next().unwrap().unwrap();
2807
2808 assert!(frame.has_velocities());
2809 assert_eq!(frame.atom_data[0].atom_id, 0);
2810 assert_eq!(frame.atom_data[0].fixed, [true, false, true]);
2811 }
2812
2813 #[test]
2814 fn test_validate_must_be_boolean_when_present() {
2815 let lines = vec![
2816 "PREBOX1",
2817 "{\"con_spec_version\":2,\"validate\":\"yes\",\"sections\":[]}",
2818 "10.0 20.0 30.0",
2819 "90.0 90.0 90.0",
2820 "POSTBOX1",
2821 "POSTBOX2",
2822 "1",
2823 "1",
2824 "12.011",
2825 ];
2826 let mut line_it = lines.iter().copied();
2827 let err = parse_frame_header(&mut line_it).unwrap_err();
2828
2829 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2830 assert!(err.to_string().contains("validate"));
2831 }
2832
2833 #[test]
2834 fn test_validate_true_requires_sections_key() {
2835 let lines = vec![
2836 "PREBOX1",
2837 "{\"con_spec_version\":2,\"validate\":true}",
2838 "10.0 20.0 30.0",
2839 "90.0 90.0 90.0",
2840 "POSTBOX1",
2841 "POSTBOX2",
2842 "1",
2843 "1",
2844 "12.011",
2845 ];
2846 let mut line_it = lines.iter().copied();
2847 let err = parse_frame_header(&mut line_it).unwrap_err();
2848
2849 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2850 assert!(err.to_string().contains("sections"));
2851 }
2852
2853 #[test]
2854 fn test_sections_must_be_string_array_when_present() {
2855 let lines = vec![
2856 "PREBOX1",
2857 "{\"con_spec_version\":2,\"sections\":[\"velocities\",7]}",
2858 "10.0 20.0 30.0",
2859 "90.0 90.0 90.0",
2860 "POSTBOX1",
2861 "POSTBOX2",
2862 "1",
2863 "1",
2864 "12.011",
2865 ];
2866 let mut line_it = lines.iter().copied();
2867 let err = parse_frame_header(&mut line_it).unwrap_err();
2868
2869 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2870 assert!(err.to_string().contains("sections"));
2871 }
2872
2873 #[test]
2874 fn test_validate_true_rejects_non_integer_coordinate_identity_columns() {
2875 let text = r#"
2876PREBOX1
2877{"con_spec_version":2,"sections":[],"validate":true}
287810.0 20.0 30.0
287990.0 90.0 90.0
2880POSTBOX1
2881POSTBOX2
28821
28831
288463.546
2885Cu
2886Coordinates of Component 1
28870.0 0.0 0.0 5.0 0
2888"#;
2889 let mut iter = ConFrameIterator::new(text.trim());
2890 let err = iter.next().unwrap().unwrap_err();
2891
2892 assert!(matches!(err, ParseError::InvalidFixedFlag(_)));
2893 assert!(err.to_string().contains("5.0"));
2894 }
2895
2896 #[test]
2897 fn test_validate_true_rejects_non_exact_coordinate_label() {
2898 let text = r#"
2899PREBOX1
2900{"con_spec_version":2,"sections":[],"validate":true}
290110.0 20.0 30.0
290290.0 90.0 90.0
2903POSTBOX1
2904POSTBOX2
29051
29061
290763.546
2908Cu
2909Coordinates Component 1
29100.0 0.0 0.0 5 0
2911"#;
2912 let mut iter = ConFrameIterator::new(text.trim());
2913 let err = iter.next().unwrap().unwrap_err();
2914
2915 assert!(matches!(err, ParseError::ValidationError(_)));
2916 assert!(err.to_string().contains("Coordinates of Component 1"));
2917 }
2918
2919 #[test]
2920 fn test_validate_true_rejects_unknown_component_symbol() {
2921 let text = r#"
2922PREBOX1
2923{"con_spec_version":2,"sections":[],"validate":true}
292410.0 20.0 30.0
292590.0 90.0 90.0
2926POSTBOX1
2927POSTBOX2
29281
29291
293063.546
2931Qq
2932Coordinates of Component 1
29330.0 0.0 0.0 0 0
2934"#;
2935 let mut iter = ConFrameIterator::new(text.trim());
2936 let err = iter.next().unwrap().unwrap_err();
2937
2938 assert!(matches!(err, ParseError::ValidationError(_)));
2939 assert!(err.to_string().contains("symbol"));
2940 }
2941
2942 #[test]
2943 fn test_declared_section_must_be_present() {
2944 let text = r#"
2945PREBOX1
2946{"con_spec_version":2,"sections":["velocities"]}
294710.0 20.0 30.0
294890.0 90.0 90.0
2949POSTBOX1
2950POSTBOX2
29511
29521
295363.546
2954Cu
2955Coordinates of Component 1
29560.0 0.0 0.0 0 0
2957"#;
2958 let mut iter = ConFrameIterator::new(text.trim());
2959 let err = iter.next().unwrap().unwrap_err();
2960
2961 assert!(matches!(
2962 err,
2963 ParseError::IncompleteSection(ref name) if name == "velocities"
2964 ));
2965 }
2966
2967 #[test]
2968 fn test_non_finite_cell_geometry_rejected_in_strict_mode() {
2969 let lines = vec![
2970 "PREBOX1",
2971 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
2972 "10.0 NaN 30.0",
2973 "90.0 90.0 90.0",
2974 "POSTBOX1",
2975 "POSTBOX2",
2976 "1",
2977 "1",
2978 "12.011",
2979 ];
2980 let mut line_it = lines.iter().copied();
2981 let err = parse_frame_header(&mut line_it).unwrap_err();
2982
2983 assert!(matches!(err, ParseError::NonFiniteValue(_)));
2984 assert!(err.to_string().contains("NaN"));
2985 }
2986
2987 #[test]
2988 fn test_validate_true_rejects_non_physical_cell_geometry() {
2989 let lines = vec![
2990 "PREBOX1",
2991 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
2992 "0.0 20.0 30.0",
2993 "90.0 180.0 90.0",
2994 "POSTBOX1",
2995 "POSTBOX2",
2996 "1",
2997 "1",
2998 "12.011",
2999 ];
3000 let mut line_it = lines.iter().copied();
3001 let err = parse_frame_header(&mut line_it).unwrap_err();
3002
3003 assert!(matches!(err, ParseError::ValidationError(_)));
3004 assert!(err.to_string().contains("cell"));
3005 }
3006
3007 #[test]
3008 fn test_validate_true_rejects_reserved_metadata_type_mismatch() {
3009 let lines = vec![
3010 "PREBOX1",
3011 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"energy\":\"low\"}",
3012 "10.0 20.0 30.0",
3013 "90.0 90.0 90.0",
3014 "POSTBOX1",
3015 "POSTBOX2",
3016 "1",
3017 "1",
3018 "12.011",
3019 ];
3020 let mut line_it = lines.iter().copied();
3021 let err = parse_frame_header(&mut line_it).unwrap_err();
3022
3023 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
3024 assert!(err.to_string().contains("energy"));
3025 }
3026
3027 #[test]
3028 fn test_validate_true_rejects_malformed_bonds() {
3029 let lines = vec![
3030 "PREBOX1",
3031 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"bonds\":[[0]]}",
3032 "10.0 20.0 30.0",
3033 "90.0 90.0 90.0",
3034 "POSTBOX1",
3035 "POSTBOX2",
3036 "1",
3037 "1",
3038 "12.011",
3039 ];
3040 let mut line_it = lines.iter().copied();
3041 let err = parse_frame_header(&mut line_it).unwrap_err();
3042 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
3043 assert!(err.to_string().contains("bonds"));
3044 }
3045
3046 #[test]
3047 fn test_bonds_metadata_round_trip_in_header() {
3048 use crate::types::{Bond, meta};
3049 let lines = vec![
3050 "PREBOX1",
3051 "{\"con_spec_version\":2,\"bonds\":[[0,1],{\"i\":0,\"j\":2,\"order\":1}]}",
3052 "10.0 20.0 30.0",
3053 "90.0 90.0 90.0",
3054 "POSTBOX1",
3055 "POSTBOX2",
3056 "1",
3057 "1",
3058 "12.011",
3059 ];
3060 let mut line_it = lines.iter().copied();
3061 let header = parse_frame_header(&mut line_it).expect("header");
3062 let bonds = header.bonds();
3063 assert_eq!(bonds.len(), 2);
3064 assert_eq!(bonds[0], Bond::new(0, 1));
3065 assert_eq!(bonds[1].i, 0);
3066 assert_eq!(bonds[1].j, 2);
3067 assert_eq!(bonds[1].order, Some(1));
3068 assert!(header.metadata.contains_key(meta::BONDS));
3069 }
3070
3071 #[test]
3072 fn test_parse_velocity_section_absent() {
3073 let lines = vec![
3074 "PREBOX1",
3075 "{\"con_spec_version\":2}",
3076 "10.0 20.0 30.0",
3077 "90.0 90.0 90.0",
3078 "POSTBOX1",
3079 "POSTBOX2",
3080 "1",
3081 "1",
3082 "12.011",
3083 "C",
3084 "Coordinates of Component 1",
3085 "0.0 0.0 0.0 0 1",
3086 ];
3087 let mut line_it = lines.iter().copied().peekable();
3088 let mut frame = parse_single_frame(&mut line_it).expect("parse should succeed");
3089 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
3090 .expect("should succeed with no velocities");
3091 assert!(!has_vel);
3092 assert_eq!(frame.atom_data[0].velocity(), None);
3093 }
3094
3095 #[test]
3096 fn test_parse_line_of_range_f64_exact() {
3097 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0 42", 4, 5, &[0.0; 5]).unwrap();
3098 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 42.0]);
3099 }
3100
3101 #[test]
3102 fn test_byte_scan_stack_parses_realistic_coord_line() {
3103 let line = " 0.63939999999999997 0.90449999999999997 -0.00009999999999977 1 0";
3105 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
3106 let mut buf = [0.0f64; 5];
3107 let n = parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
3108 assert_eq!(n, 5);
3109 assert!((buf[0] - 0.6394).abs() < 1e-4);
3110 assert!((buf[1] - 0.9045).abs() < 1e-4);
3111 assert_eq!(buf[3] as u8, 1);
3112 assert_eq!(buf[4] as u64, 0);
3113 let bad = "1.0 2.0 3.0 1 0 EXTRA";
3115 assert!(parse_line_of_range_f64_stack(bad, 4, 5, &defaults, &mut buf).is_err());
3116 let glued = "1.0 2.0 3.0abc 1 0";
3118 assert!(parse_line_of_range_f64_stack(glued, 4, 5, &defaults, &mut buf).is_err());
3119 }
3120
3121 #[test]
3122 fn test_parse_line_of_range_f64_stack_matches_vec_api() {
3123 let defaults = [0.0, 0.0, 0.0, 0.0, 7.0];
3124 let line = "1.0 2.0 3.0 0.0";
3125 let mut buf = [0.0f64; 5];
3126 parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
3127 let via_vec = parse_line_of_range_f64(line, 4, 5, &defaults).unwrap();
3128 assert_eq!(&buf[..5], via_vec.as_slice());
3129 assert_eq!(buf[4], 7.0);
3130 }
3131
3132 #[test]
3133 fn test_parse_line_of_range_f64_padded() {
3134 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
3135 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0", 4, 5, &defaults).unwrap();
3136 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 99.0]);
3137 }
3138
3139 #[test]
3140 fn test_parse_line_of_range_f64_too_few() {
3141 let result = parse_line_of_range_f64("1.0 2.0 3.0", 4, 5, &[0.0; 5]);
3142 assert!(result.is_err());
3143 }
3144
3145 #[test]
3146 fn test_parse_line_of_range_f64_too_many() {
3147 let result = parse_line_of_range_f64("1.0 2.0 3.0 0.0 5.0 6.0", 4, 5, &[0.0; 5]);
3148 assert!(result.is_err());
3149 }
3150
3151 #[test]
3152 fn test_parse_all_four_column_lines() {
3153 let lines = vec![
3155 "PREBOX1",
3156 "{\"con_spec_version\":2}",
3157 "10.0 10.0 10.0",
3158 "90.0 90.0 90.0",
3159 "POSTBOX1",
3160 "POSTBOX2",
3161 "1",
3162 "3",
3163 "12.011",
3164 "C",
3165 "Coordinates of Component 1",
3166 "0.0 0.0 0.0 0",
3167 "1.0 0.0 0.0 0",
3168 "2.0 0.0 0.0 1",
3169 ];
3170 let mut line_it = lines.iter().copied();
3171 let frame = parse_single_frame(&mut line_it).unwrap();
3172 assert_eq!(frame.atom_data[0].atom_id, 0);
3173 assert_eq!(frame.atom_data[1].atom_id, 1);
3174 assert_eq!(frame.atom_data[2].atom_id, 2);
3175 assert!(frame.atom_data[2].is_fixed());
3176 }
3177}