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}
27
28impl<'a, I> LineStream<'a> for Peekable<I>
29where
30 I: Iterator<Item = &'a str>,
31{
32 #[inline]
33 fn next_line(&mut self) -> Option<&'a str> {
34 Iterator::next(self)
35 }
36 #[inline]
37 fn peek_line(&mut self) -> Option<&'a str> {
38 Peekable::peek(self).copied()
39 }
40}
41
42#[inline]
51pub fn parse_line_of_range_f64_stack(
52 line: &str,
53 min: usize,
54 max: usize,
55 defaults: &[f64],
56 out: &mut [f64; 5],
57) -> Result<usize, ParseError> {
58 debug_assert!(max <= 5 && min <= max && defaults.len() >= max);
59 let bytes = line.as_bytes();
60 let n = bytes.len();
61 let mut i = 0usize;
62 let mut found = 0usize;
63 while found < max {
64 while i < n && bytes[i].is_ascii_whitespace() {
65 i += 1;
66 }
67 if i >= n {
68 break;
69 }
70 let end = bytes[i..]
71 .iter()
72 .position(|b| b.is_ascii_whitespace())
73 .map(|k| i + k)
74 .unwrap_or(n);
75 let token = std::str::from_utf8(&bytes[i..end]).unwrap_or("");
76 if is_nonfinite_spelling(token) {
77 return Err(ParseError::NonFiniteValue(token.to_string()));
78 }
79 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..])
80 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
81 let next = i + consumed;
82 if next != end {
84 return Err(ParseError::InvalidNumberFormat(format!(
85 "invalid float: {token}"
86 )));
87 }
88 if !val.is_finite() {
89 return Err(ParseError::NonFiniteValue(token.to_string()));
90 }
91 out[found] = val;
92 found += 1;
93 i = next;
94 }
95 while i < n && bytes[i].is_ascii_whitespace() {
96 i += 1;
97 }
98 if i < n {
100 return Err(ParseError::InvalidVectorLength {
101 expected: max,
102 found: found + 1,
103 });
104 }
105 if found < min || found > max {
106 return Err(ParseError::InvalidVectorLength {
107 expected: max,
108 found,
109 });
110 }
111 while found < max {
112 out[found] = defaults[found];
113 found += 1;
114 }
115 Ok(found)
116}
117
118pub fn parse_line_of_n_f64(line: &str, n: usize) -> Result<Vec<f64>, ParseError> {
130 if n <= 5 {
131 let defaults = [0.0f64; 5];
132 let mut buf = [0.0f64; 5];
133 parse_line_of_range_f64_stack(line, n, n, &defaults, &mut buf)?;
134 return Ok(buf[..n].to_vec());
135 }
136 let mut values = Vec::with_capacity(n);
137 for token in line.split_ascii_whitespace() {
138 values.push(parse_finite_float_token(token)?);
139 }
140 if values.len() == n {
141 Ok(values)
142 } else {
143 Err(ParseError::InvalidVectorLength {
144 expected: n,
145 found: values.len(),
146 })
147 }
148}
149
150pub fn parse_line_of_range_f64(
157 line: &str,
158 min: usize,
159 max: usize,
160 defaults: &[f64],
161) -> Result<Vec<f64>, ParseError> {
162 if max <= 5 {
163 let mut buf = [0.0f64; 5];
164 parse_line_of_range_f64_stack(line, min, max, defaults, &mut buf)?;
165 return Ok(buf[..max].to_vec());
166 }
167 let mut values = Vec::with_capacity(max);
168 for token in line.split_ascii_whitespace() {
169 values.push(parse_finite_float_token(token)?);
170 }
171 if values.len() < min || values.len() > max {
172 return Err(ParseError::InvalidVectorLength {
173 expected: max,
174 found: values.len(),
175 });
176 }
177 while values.len() < max {
178 let idx = values.len();
179 values.push(defaults[idx]);
180 }
181 Ok(values)
182}
183
184fn is_nonfinite_spelling(token: &str) -> bool {
188 let rest = token
189 .strip_prefix('+')
190 .or_else(|| token.strip_prefix('-'))
191 .unwrap_or(token);
192 match rest.as_bytes().first() {
193 Some(b) if b.is_ascii_digit() || *b == b'.' => return false,
194 None => return false,
195 _ => {}
196 }
197 rest.eq_ignore_ascii_case("nan")
198 || rest.eq_ignore_ascii_case("inf")
199 || rest.eq_ignore_ascii_case("infinity")
200}
201
202fn parse_finite_float_token(token: &str) -> Result<f64, ParseError> {
203 if is_nonfinite_spelling(token) {
204 return Err(ParseError::NonFiniteValue(token.to_string()));
205 }
206 let (val, consumed) = fast_float2::parse_partial::<f64, _>(token.as_bytes())
207 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
208 if consumed != token.len() {
209 return Err(ParseError::InvalidNumberFormat(format!(
210 "invalid float: {token}"
211 )));
212 }
213 if !val.is_finite() {
214 return Err(ParseError::NonFiniteValue(token.to_string()));
215 }
216 Ok(val)
217}
218
219fn skip_ascii_ws(bytes: &[u8], mut i: usize) -> usize {
220 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
221 i += 1;
222 }
223 i
224}
225
226fn take_token(line: &str, i: usize) -> Option<(&str, usize)> {
227 let bytes = line.as_bytes();
228 if i >= bytes.len() {
229 return None;
230 }
231 let mut j = i;
232 while j < bytes.len() && !bytes[j].is_ascii_whitespace() {
233 j += 1;
234 }
235 Some((&line[i..j], j))
236}
237
238fn parse_fixed_flag(token: &str, spec_version: u32) -> Result<[bool; 3], ParseError> {
241 let bad = || {
242 ParseError::InvalidFixedFlag(format!(
243 "column 4 flag {token:?} is not a legal integer for spec {spec_version}"
244 ))
245 };
246 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
247 return Err(bad());
248 }
249 let val: u64 = token.parse::<u64>().map_err(|_| bad())?;
250 if spec_version < 2 {
251 return match val {
252 0 => Ok([false, false, false]),
253 1 => Ok([true, true, true]),
254 _ => Err(bad()),
255 };
256 }
257 if val > 7 {
258 return Err(bad());
259 }
260 let v = val as u8;
261 Ok([v & 1 != 0, v & 2 != 0, v & 4 != 0])
262}
263
264fn parse_atom_id_token(token: &str) -> Result<u64, ParseError> {
267 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
268 return Err(ParseError::InvalidNumberFormat(format!(
269 "invalid integer: {token}"
270 )));
271 }
272 token.parse::<u64>().map_err(|_| {
273 ParseError::InvalidNumberFormat(format!("integer out of range 0..=2^64-1: {token}"))
274 })
275}
276
277fn parse_coord_row(
281 line: &str,
282 spec_version: u32,
283 default_id: u64,
284) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
285 parse_vector_row(line, spec_version, Some(default_id))
286}
287
288fn parse_vector_section_row(
290 line: &str,
291 spec_version: u32,
292) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
293 parse_vector_row(line, spec_version, None)
294}
295
296fn parse_vector_row(
297 line: &str,
298 spec_version: u32,
299 default_id: Option<u64>,
300) -> Result<([f64; 3], [bool; 3], u64), ParseError> {
301 let bytes = line.as_bytes();
302 let mut i = skip_ascii_ws(bytes, 0);
303 let mut xyz = [0.0f64; 3];
304 for slot in 0..3 {
305 if i >= bytes.len() {
306 return Err(ParseError::InvalidVectorLength {
307 expected: 5,
308 found: slot,
309 });
310 }
311 let (val, next) = parse_finite_at(line, i)?;
312 xyz[slot] = val;
313 i = skip_ascii_ws(bytes, next);
314 }
315 let (flag_tok, i) = take_token(line, i).ok_or(ParseError::InvalidVectorLength {
316 expected: 5,
317 found: 3,
318 })?;
319 let fixed = parse_fixed_flag(flag_tok, spec_version)?;
320 let i = skip_ascii_ws(bytes, i);
321 let (atom_id, i) = match take_token(line, i) {
322 Some((tok, next)) => (parse_atom_id_token(tok)?, next),
323 None => (
324 default_id.ok_or(ParseError::InvalidVectorLength {
325 expected: 5,
326 found: 4,
327 })?,
328 i,
329 ),
330 };
331 let i = skip_ascii_ws(bytes, i);
332 if i < bytes.len() {
333 let found = 5 + line[i..].split_ascii_whitespace().count();
334 return Err(ParseError::InvalidVectorLength { expected: 5, found });
335 }
336 Ok((xyz, fixed, atom_id))
337}
338
339fn parse_scalar_section_row(
341 line: &str,
342 spec_version: u32,
343) -> Result<(f64, [bool; 3], u64), ParseError> {
344 let bytes = line.as_bytes();
345 let i = skip_ascii_ws(bytes, 0);
346 let (value, i) = parse_finite_at(line, i)?;
347 let i = skip_ascii_ws(bytes, i);
348 let (flag_tok, i) = take_token(line, i).ok_or(ParseError::InvalidVectorLength {
349 expected: 3,
350 found: 1,
351 })?;
352 let fixed = parse_fixed_flag(flag_tok, spec_version)?;
353 let i = skip_ascii_ws(bytes, i);
354 let (id_tok, i) = take_token(line, i).ok_or(ParseError::InvalidVectorLength {
355 expected: 3,
356 found: 2,
357 })?;
358 let atom_id = parse_atom_id_token(id_tok)?;
359 let i = skip_ascii_ws(bytes, i);
360 if i < bytes.len() {
361 let found = 3 + line[i..].split_ascii_whitespace().count();
362 return Err(ParseError::InvalidVectorLength { expected: 3, found });
363 }
364 Ok((value, fixed, atom_id))
365}
366
367fn parse_finite_at(line: &str, i: usize) -> Result<(f64, usize), ParseError> {
368 let bytes = line.as_bytes();
369 if i >= bytes.len() {
370 return Err(ParseError::InvalidVectorLength {
371 expected: 3,
372 found: 0,
373 });
374 }
375 if looks_like_nonfinite(bytes, i) {
378 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
379 if is_nonfinite_spelling(token) {
380 return Err(ParseError::NonFiniteValue(token.to_string()));
381 }
382 }
383 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..]).map_err(|_| {
384 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
385 ParseError::InvalidNumberFormat(format!("invalid float: {token}"))
386 })?;
387 let next = i + consumed;
388 let boundary = next == bytes.len() || bytes[next].is_ascii_whitespace();
389 if !boundary || !val.is_finite() {
390 let token = take_token(line, i).map(|(tok, _)| tok).unwrap_or("");
391 if is_nonfinite_spelling(token) || !val.is_finite() {
392 return Err(ParseError::NonFiniteValue(token.to_string()));
393 }
394 return Err(ParseError::InvalidNumberFormat(format!(
395 "invalid float: {token}"
396 )));
397 }
398 Ok((val, next))
399}
400
401fn looks_like_nonfinite(bytes: &[u8], i: usize) -> bool {
404 let mut j = i;
405 if bytes[j] == b'+' || bytes[j] == b'-' {
406 j += 1;
407 if j >= bytes.len() {
408 return false;
409 }
410 }
411 let b = bytes[j];
412 !b.is_ascii_digit() && b != b'.'
413}
414
415fn labels_equal(found: &str, expected: &str) -> bool {
417 let mut found_words = found.split_ascii_whitespace();
418 let mut expected_words = expected.split_ascii_whitespace();
419 loop {
420 match (found_words.next(), expected_words.next()) {
421 (Some(a), Some(b)) => {
422 if !a.eq_ignore_ascii_case(b) {
423 return false;
424 }
425 }
426 (None, None) => return true,
427 _ => return false,
428 }
429 }
430}
431
432fn label_has_prefix(line: &str, prefix: &str) -> bool {
434 let mut found_words = line.split_ascii_whitespace();
435 for word in prefix.split_ascii_whitespace() {
436 match found_words.next() {
437 Some(got) if got.eq_ignore_ascii_case(word) => {}
438 _ => return false,
439 }
440 }
441 true
442}
443
444fn token_count(line: &str) -> usize {
445 line.split_ascii_whitespace().count()
446}
447
448fn json_u64(value: &Value) -> Option<u64> {
449 value.as_u64()
450}
451
452fn validate_section_arity_value(value: &Value) -> Result<(), ParseError> {
453 let obj = value.as_object().ok_or_else(|| {
454 metadata_json_error("section_arity must be an object mapping section names to integers")
455 })?;
456 for (name, entry) in obj {
457 let n = json_u64(entry).ok_or_else(|| {
458 metadata_json_error(format!("section_arity.{name} must be an integer >= 1"))
459 })?;
460 if n < 1 {
461 return Err(ParseError::InvalidCount(format!(
462 "section_arity.{name} must be >= 1"
463 )));
464 }
465 if let Some(expect) = crate::types::reserved_section_value_arity(name)
467 && n != expect
468 {
469 return Err(ParseError::InvalidVectorLength {
470 expected: usize::try_from(expect).unwrap_or(usize::MAX),
471 found: usize::try_from(n).unwrap_or(usize::MAX),
472 });
473 }
474 }
475 Ok(())
476}
477
478pub(crate) fn parse_count_line(line: &str, n: usize) -> Result<Vec<usize>, ParseError> {
482 let mut values = Vec::with_capacity(n);
483 for token in line.split_ascii_whitespace() {
484 if token.is_empty() || !token.bytes().all(|b| b.is_ascii_digit()) {
485 return Err(ParseError::InvalidCount(format!(
486 "count field is not an unsigned integer: {token}"
487 )));
488 }
489 let val: u64 = token.parse::<u64>().map_err(|_| {
490 ParseError::InvalidCount(format!("count field is out of range: {token}"))
491 })?;
492 let val = usize::try_from(val).map_err(|_| {
493 ParseError::InvalidCount(format!("count field is out of range: {token}"))
494 })?;
495 values.push(val);
496 }
497 if values.len() == n {
498 Ok(values)
499 } else {
500 Err(ParseError::InvalidVectorLength {
501 expected: n,
502 found: values.len(),
503 })
504 }
505}
506
507pub(crate) fn line_is_whitespace(line: &str) -> bool {
509 line.split_ascii_whitespace().next().is_none()
510}
511
512pub(crate) fn read_zero_atom_tail<'a>(
518 mut next_line: impl FnMut() -> Option<&'a str>,
519) -> Result<(), ParseError> {
520 let bad = || {
521 ParseError::InvalidCount("a zero-atom frame requires whitespace-only lines 8 and 9".into())
522 };
523 match next_line() {
524 None => Ok(()),
525 Some(line) if line_is_whitespace(line) => match next_line() {
526 None => Ok(()),
527 Some(line) if line_is_whitespace(line) => Ok(()),
528 Some(_) => Err(bad()),
529 },
530 Some(_) => Err(bad()),
531 }
532}
533
534fn metadata_json_error(message: impl Into<String>) -> ParseError {
535 ParseError::InvalidMetadataJson(message.into())
536}
537
538fn validate_metadata_number(key: &str, value: &Value) -> Result<(), ParseError> {
539 if value.as_f64().is_some() {
540 Ok(())
541 } else {
542 Err(metadata_json_error(format!(
543 "{key} must be a finite number"
544 )))
545 }
546}
547
548fn validate_metadata_integer(key: &str, value: &Value) -> Result<(), ParseError> {
549 if value.as_u64().is_some() {
550 Ok(())
551 } else {
552 Err(metadata_json_error(format!(
553 "{key} must be a non-negative integer"
554 )))
555 }
556}
557
558pub fn validate_metadata_schema(
566 json_obj: &serde_json::Map<String, Value>,
567) -> Result<(), ParseError> {
568 let strict_requested = match json_obj.get(meta::VALIDATE) {
569 Some(Value::Bool(b)) => *b,
570 Some(_) => return Err(metadata_json_error("validate must be a boolean")),
571 None => false,
572 };
573
574 match json_obj.get(meta::SECTIONS) {
575 Some(Value::Array(values)) => {
576 if values.iter().any(|entry| !entry.is_string()) {
577 return Err(metadata_json_error("sections must be an array of strings"));
578 }
579 }
580 Some(_) => return Err(metadata_json_error("sections must be an array of strings")),
581 None if strict_requested => {
582 return Err(metadata_json_error(
583 "validate=true requires a sections array, even when empty",
584 ));
585 }
586 None => {}
587 }
588
589 for (key, value) in json_obj {
590 match key.as_str() {
591 meta::CON_SPEC_VERSION | meta::SECTIONS | meta::VALIDATE => {}
592 meta::CON_SPEC_MINOR => {
593 if json_u64(value).is_none() {
594 return Err(metadata_json_error(
595 "con_spec_minor must be a JSON integer >= 0",
596 ));
597 }
598 }
599 meta::SECTION_ARITY => validate_section_arity_value(value)?,
600 meta::ENERGY
601 | meta::TIME
602 | meta::TIMESTEP
603 | meta::CONVERGENCE_FMAX
604 | meta::CONVERGENCE_ENERGY
605 | meta::FMAX => validate_metadata_number(key, value)?,
606 meta::FRAME_INDEX | meta::NEB_BEAD | meta::NEB_BAND => {
607 validate_metadata_integer(key, value)?
608 }
609 meta::GENERATOR if !value.is_string() => {
610 return Err(metadata_json_error("generator must be a string"));
611 }
612 meta::GENERATOR => {}
613 meta::UNITS | meta::POTENTIAL if !value.is_object() => {
614 return Err(metadata_json_error(format!("{key} must be an object")));
615 }
616 meta::POTENTIAL => {
617 if let Some(potential_type) = value.get("type")
618 && !potential_type.is_string()
619 {
620 return Err(metadata_json_error("potential.type must be a string"));
621 }
622 }
623 meta::UNITS => {
624 }
626 meta::PBC => validate_pbc_metadata(value)?,
627 meta::BONDS => validate_bonds_metadata(value)?,
628 meta::LATTICE_VECTORS => validate_lattice_vectors_metadata(value)?,
629 meta::CONVERGED if !value.is_boolean() => {
630 return Err(metadata_json_error("converged must be a boolean"));
631 }
632 meta::CONVERGED => {}
633 _ => {}
634 }
635 }
636
637 Ok(())
638}
639
640fn validate_pbc_metadata(value: &Value) -> Result<(), ParseError> {
641 let Some(values) = value.as_array() else {
642 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
643 };
644 if values.len() != 3 || values.iter().any(|entry| !entry.is_boolean()) {
645 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
646 }
647 Ok(())
648}
649
650fn validate_bonds_metadata(value: &Value) -> Result<(), ParseError> {
657 let Some(items) = value.as_array() else {
658 return Err(metadata_json_error("bonds must be an array"));
659 };
660 for (idx, item) in items.iter().enumerate() {
661 if let Some(pair) = item.as_array() {
662 if pair.len() != 2 {
663 return Err(metadata_json_error(format!(
664 "bonds[{idx}] pair must have exactly two indices"
665 )));
666 }
667 for (k, entry) in pair.iter().enumerate() {
668 let Some(n) = entry.as_u64() else {
669 return Err(metadata_json_error(format!(
670 "bonds[{idx}][{k}] must be a non-negative integer"
671 )));
672 };
673 if n > u32::MAX as u64 {
674 return Err(metadata_json_error(format!(
675 "bonds[{idx}][{k}] index exceeds u32"
676 )));
677 }
678 }
679 continue;
680 }
681 if let Some(obj) = item.as_object() {
682 for key in ["i", "j"] {
683 let Some(entry) = obj.get(key) else {
684 return Err(metadata_json_error(format!(
685 "bonds[{idx}] object must include \"{key}\""
686 )));
687 };
688 let Some(n) = entry.as_u64() else {
689 return Err(metadata_json_error(format!(
690 "bonds[{idx}].{key} must be a non-negative integer"
691 )));
692 };
693 if n > u32::MAX as u64 {
694 return Err(metadata_json_error(format!(
695 "bonds[{idx}].{key} index exceeds u32"
696 )));
697 }
698 }
699 if let Some(order) = obj.get("order")
700 && order.as_i64().is_none()
701 {
702 return Err(metadata_json_error(format!(
703 "bonds[{idx}].order must be an integer when present"
704 )));
705 }
706 continue;
707 }
708 return Err(metadata_json_error(format!(
709 "bonds[{idx}] must be [i, j] or {{\"i\": i, \"j\": j, \"order\"?: ...}}"
710 )));
711 }
712 Ok(())
713}
714
715fn validate_lattice_vectors_metadata(value: &Value) -> Result<(), ParseError> {
716 let Some(rows) = value.as_array() else {
717 return Err(metadata_json_error(
718 "lattice_vectors must be a 3x3 numeric array",
719 ));
720 };
721 if rows.len() != 3 {
722 return Err(metadata_json_error(
723 "lattice_vectors must be a 3x3 numeric array",
724 ));
725 }
726 for row in rows {
727 let Some(entries) = row.as_array() else {
728 return Err(metadata_json_error(
729 "lattice_vectors must be a 3x3 numeric array",
730 ));
731 };
732 if entries.len() != 3 || entries.iter().any(|entry| entry.as_f64().is_none()) {
733 return Err(metadata_json_error(
734 "lattice_vectors must be a 3x3 numeric array",
735 ));
736 }
737 }
738 Ok(())
739}
740
741pub fn parse_line_of_n<T: std::str::FromStr>(line: &str, n: usize) -> Result<Vec<T>, ParseError>
769where
770 ParseError: From<<T as std::str::FromStr>::Err>,
771{
772 let values: Vec<T> = line
773 .split_whitespace()
774 .map(|s| s.parse::<T>())
775 .collect::<Result<_, _>>()?;
776
777 if values.len() == n {
778 Ok(values)
779 } else {
780 Err(ParseError::InvalidVectorLength {
781 expected: n,
782 found: values.len(),
783 })
784 }
785}
786
787pub fn parse_frame_header<'a>(
808 lines: &mut impl Iterator<Item = &'a str>,
809) -> Result<FrameHeader, ParseError> {
810 let prebox1 = lines
811 .next()
812 .ok_or(ParseError::IncompleteHeader)?
813 .to_string();
814 if prebox1.starts_with('\u{feff}') {
816 return Err(ParseError::ValidationError(
817 "file begins with a byte-order mark".into(),
818 ));
819 }
820 let prebox2_raw = lines.next().ok_or(ParseError::IncompleteHeader)?;
821
822 let trimmed = prebox2_raw.trim();
827 let (spec_version, metadata, sections, validate, sections_declared) =
828 if trimmed.starts_with('{') {
829 let json_val: serde_json::Value = serde_json::from_str(trimmed)
830 .map_err(|e| ParseError::InvalidMetadataJson(e.to_string()))?;
831 let json_obj = json_val.as_object().ok_or_else(|| {
832 ParseError::InvalidMetadataJson("expected a JSON object".to_string())
833 })?;
834 let ver_value = json_obj
835 .get(meta::CON_SPEC_VERSION)
836 .ok_or(ParseError::MissingSpecVersion)?;
837 let ver = json_u64(ver_value).ok_or_else(|| {
838 ParseError::InvalidMetadataJson(
839 "con_spec_version must be a JSON integer, the major version".into(),
840 )
841 })?;
842 if ver > u64::from(u32::MAX) {
843 return Err(ParseError::UnsupportedVersion(u32::MAX));
844 }
845 let ver = ver as u32;
846 if ver == 0 || ver == 1 || ver > crate::CON_SPEC_VERSION {
850 return Err(ParseError::UnsupportedVersion(ver));
851 }
852 if ver >= 3 {
853 match json_obj.get(meta::UNITS) {
854 Some(u) => crate::units::validate_v3_units_metadata(u)
855 .map_err(|e| ParseError::InvalidUnits(format!("v3 units: {e}")))?,
856 None => {
857 return Err(ParseError::InvalidUnits(
858 "con_spec_version >= 3 requires metadata \"units\" with length and energy"
859 .into(),
860 ));
861 }
862 }
863 }
864
865 let mut sections: Vec<String> = Vec::new();
869 let mut metadata = BTreeMap::new();
870 let mut sections_declared = false;
871 let mut validate = false;
872 for (k, v) in json_obj {
873 match k.as_str() {
874 meta::CON_SPEC_VERSION => {}
875 meta::CON_SPEC_MINOR => {
876 if json_u64(v).is_none() {
877 return Err(metadata_json_error(
878 "con_spec_minor must be a JSON integer >= 0",
879 ));
880 }
881 metadata.insert(k.clone(), v.clone());
882 }
883 meta::SECTION_ARITY => {
884 validate_section_arity_value(v)?;
885 metadata.insert(k.clone(), v.clone());
886 }
887 meta::SECTIONS => {
888 sections_declared = true;
889 let arr = v.as_array().ok_or_else(|| {
890 metadata_json_error("sections must be an array of strings")
891 })?;
892 sections.reserve(arr.len());
893 for entry in arr {
894 let s = entry.as_str().ok_or_else(|| {
895 metadata_json_error("sections must be an array of strings")
896 })?;
897 sections.push(s.to_string());
898 }
899 }
900 meta::VALIDATE => {
901 validate = match v {
902 Value::Bool(b) => *b,
903 _ => return Err(metadata_json_error("validate must be a boolean")),
904 };
905 metadata.insert(k.clone(), v.clone());
906 }
907 _ => {
908 metadata.insert(k.clone(), v.clone());
909 }
910 }
911 }
912
913 if validate {
916 validate_metadata_schema(json_obj)?;
917 }
918
919 (ver, metadata, sections, validate, sections_declared)
920 } else {
921 (1_u32, BTreeMap::new(), Vec::new(), false, false)
923 };
924 let prebox2 = prebox2_raw.to_string();
925
926 let boxl_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
927 let angles_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
928 let postbox1 = lines
929 .next()
930 .ok_or(ParseError::IncompleteHeader)?
931 .to_string();
932 let postbox2 = lines
933 .next()
934 .ok_or(ParseError::IncompleteHeader)?
935 .to_string();
936 let natm_types_line = lines.next().ok_or(ParseError::IncompleteHeader)?;
937 let natm_types = parse_count_line(natm_types_line, 1)?[0];
938 let (natms_per_type, masses_per_type) = if natm_types == 0 {
941 read_zero_atom_tail(|| lines.next())?;
942 (Vec::new(), Vec::new())
943 } else {
944 let natms_per_type = parse_count_line(
945 lines.next().ok_or(ParseError::IncompleteHeader)?,
946 natm_types,
947 )?;
948 if natms_per_type.iter().any(|n| *n == 0) {
949 return Err(ParseError::InvalidCount(
950 "each per-type count must be >= 1".into(),
951 ));
952 }
953 let masses_per_type = parse_line_of_n_f64(
954 lines.next().ok_or(ParseError::IncompleteHeader)?,
955 natm_types,
956 )?;
957 (natms_per_type, masses_per_type)
958 };
959 if validate {
960 validate_header_geometry(&boxl_vec, &angles_vec)?;
961 validate_masses(&masses_per_type)?;
962 }
963 Ok(FrameHeader {
964 prebox_header: PreboxHeader {
965 user: prebox1,
966 metadata_line: prebox2,
967 },
968 boxl: boxl_vec.try_into().unwrap(),
969 angles: angles_vec.try_into().unwrap(),
970 postbox_header: [postbox1, postbox2],
971 natm_types,
972 natms_per_type,
973 masses_per_type,
974 spec_version,
975 metadata,
976 sections,
977 strict_validation: validate,
978 sections_declared,
979 })
980}
981
982pub fn parse_single_frame<'a>(
1031 lines: &mut impl Iterator<Item = &'a str>,
1032) -> Result<ConFrame, ParseError> {
1033 let header = parse_frame_header(lines)?;
1034 let validate = header.strict_validation;
1035 let total_atoms: usize = header.natms_per_type.iter().sum();
1036 let mut atom_data = Vec::with_capacity(total_atoms);
1037 use crate::storage_dtype::{ElementKind, FloatArray2, StorageDtypes};
1040 let dt = StorageDtypes::from_metadata(&header.metadata).unwrap_or_default();
1041 let f64_positions = dt.positions == ElementKind::Float64;
1042 let mut pos_flat = if f64_positions {
1043 vec![0.0f64; total_atoms.saturating_mul(3)]
1044 } else {
1045 Vec::new()
1046 };
1047 let mut positions_other = if f64_positions {
1048 None
1049 } else {
1050 Some(FloatArray2::zeros(dt.positions, total_atoms, 3))
1051 };
1052
1053 let mut global_atom_idx: u64 = 0;
1054 let mut atom_i = 0usize;
1055 for (type_idx, num_atoms) in header.natms_per_type.iter().enumerate() {
1056 let symbol_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
1060 let symbol: Arc<str> = Arc::from(symbol_line.trim());
1061 let coord_label = lines.next().ok_or(ParseError::IncompleteFrame)?;
1062 if validate {
1063 validate_coordinate_component(type_idx, symbol.as_ref(), coord_label)?;
1064 }
1065 for _ in 0..*num_atoms {
1066 let coord_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
1067 let (xyz, fixed, atom_id) =
1070 parse_coord_row(coord_line, header.spec_version, global_atom_idx)?;
1071 if f64_positions {
1072 let o = atom_i * 3;
1073 pos_flat[o] = xyz[0];
1074 pos_flat[o + 1] = xyz[1];
1075 pos_flat[o + 2] = xyz[2];
1076 } else if let Some(ref mut pos) = positions_other {
1077 pos.set_f64_row(atom_i, xyz);
1078 }
1079 atom_data.push(AtomDatum {
1080 symbol: Arc::clone(&symbol),
1082 x: xyz[0],
1083 y: xyz[1],
1084 z: xyz[2],
1085 fixed,
1086 atom_id,
1087 velocity: None,
1088 force: None,
1089 energy: None,
1090 charge: None,
1091 spin: None,
1092 magmom: None,
1093 displacement: None,
1094 spread: None,
1095 });
1096 global_atom_idx += 1;
1097 atom_i += 1;
1098 }
1099 }
1100 let positions = if f64_positions {
1101 FloatArray2::from_f64_row_major(total_atoms, 3, pos_flat)
1102 } else {
1103 positions_other.expect("non-f64 positions allocated")
1104 };
1105 Ok(crate::types::con_frame_from_atom_data_with_positions(
1107 header, atom_data, positions,
1108 ))
1109}
1110
1111fn validate_header_geometry(boxl: &[f64], angles: &[f64]) -> Result<(), ParseError> {
1112 if boxl
1113 .iter()
1114 .any(|length| !length.is_finite() || *length <= 0.0)
1115 || angles
1116 .iter()
1117 .any(|angle| !angle.is_finite() || *angle <= 0.0 || *angle >= 180.0)
1118 {
1119 return Err(ParseError::ValidationError(
1120 "cell geometry must have positive lengths and angles between 0 and 180 degrees"
1121 .to_string(),
1122 ));
1123 }
1124 Ok(())
1125}
1126
1127fn validate_masses(masses_per_type: &[f64]) -> Result<(), ParseError> {
1128 if masses_per_type
1129 .iter()
1130 .any(|mass| !mass.is_finite() || *mass <= 0.0)
1131 {
1132 return Err(ParseError::ValidationError(
1133 "component masses must be positive".to_string(),
1134 ));
1135 }
1136 Ok(())
1137}
1138
1139fn validate_coordinate_component(
1140 type_idx: usize,
1141 symbol: &str,
1142 label: &str,
1143) -> Result<(), ParseError> {
1144 let expected_label = format!("Coordinates of Component {}", type_idx + 1);
1145 if !labels_equal(label, &expected_label) {
1146 return Err(ParseError::ValidationError(format!(
1147 "expected coordinate label {expected_label:?}, found {label:?}"
1148 )));
1149 }
1150 if symbol != "X" && symbol_to_atomic_number(symbol) == 0 {
1151 return Err(ParseError::ValidationError(format!(
1152 "unknown component symbol {symbol}"
1153 )));
1154 }
1155 Ok(())
1156}
1157
1158fn validate_section_component(
1159 section: &str,
1160 type_idx: usize,
1161 atom_idx: usize,
1162 symbol: &str,
1163 label: &str,
1164 header: &FrameHeader,
1165 atom_data: &[AtomDatum],
1166) -> Result<(), ParseError> {
1167 let expected_label = format!("{section} of Component {}", type_idx + 1);
1168 if !labels_equal(label, &expected_label) {
1169 return Err(ParseError::ValidationError(format!(
1170 "expected section label {expected_label:?}, found {label:?}"
1171 )));
1172 }
1173
1174 if header.natms_per_type[type_idx] == 0 {
1175 return Ok(());
1176 }
1177
1178 let expected_symbol = atom_data
1179 .get(atom_idx)
1180 .map(|atom| atom.symbol.as_ref())
1181 .ok_or_else(|| {
1182 ParseError::ValidationError(format!(
1183 "{section} component {} has no coordinate atom to validate against",
1184 type_idx + 1
1185 ))
1186 })?;
1187 if symbol != expected_symbol {
1188 return Err(ParseError::ValidationError(format!(
1189 "{section} component {} symbol mismatch: expected {expected_symbol}, found {symbol}",
1190 type_idx + 1
1191 )));
1192 }
1193
1194 Ok(())
1195}
1196
1197fn validate_section_atom_identity(
1198 section: &str,
1199 atom_idx: usize,
1200 fixed: [bool; 3],
1201 atom_id: u64,
1202 atom_data: &[AtomDatum],
1203) -> Result<(), ParseError> {
1204 let atom = atom_data.get(atom_idx).ok_or_else(|| {
1205 ParseError::ValidationError(format!(
1206 "{section} row {atom_idx} has no coordinate atom to validate against"
1207 ))
1208 })?;
1209
1210 if atom.fixed != fixed {
1211 return Err(ParseError::ValidationError(format!(
1212 "{section} row {atom_idx} fixed mask mismatch for atom_id {}",
1213 atom.atom_id
1214 )));
1215 }
1216 if atom.atom_id != atom_id {
1217 return Err(ParseError::ValidationError(format!(
1218 "{section} row {atom_idx} atom_id mismatch: expected {}, found {atom_id}",
1219 atom.atom_id
1220 )));
1221 }
1222
1223 Ok(())
1224}
1225
1226fn frame_minor(header: &FrameHeader) -> u64 {
1231 header
1232 .metadata
1233 .get(meta::CON_SPEC_MINOR)
1234 .and_then(|v| v.as_u64())
1235 .unwrap_or(0)
1236}
1237
1238fn section_row_arity(header: &FrameHeader, name: &str) -> Option<usize> {
1239 let obj = header.metadata.get(meta::SECTION_ARITY)?.as_object()?;
1240 let n = obj.get(name)?.as_u64()?;
1241 if n >= 1 { Some(n as usize) } else { None }
1242}
1243
1244fn skip_unknown_section<'a>(
1251 lines: &mut impl LineStream<'a>,
1252 header: &FrameHeader,
1253 name: &str,
1254 arity: usize,
1255) -> Result<(), ParseError> {
1256 if !open_section(lines)? {
1257 return Err(ParseError::IncompleteSection(name.to_string()));
1258 }
1259 let with_flag = arity.saturating_add(1);
1260 let with_id = arity.saturating_add(2);
1261 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1262 if lines.next_line().is_none() {
1263 return Err(ParseError::IncompleteSection(name.to_string()));
1264 }
1265 let label = lines
1266 .next_line()
1267 .ok_or_else(|| ParseError::IncompleteSection(name.to_string()))?;
1268 let expected = format!("{name} of Component {}", type_idx + 1);
1269 if !labels_equal(label, &expected) {
1270 return Err(ParseError::IncompleteSection(name.to_string()));
1271 }
1272 for _ in 0..num_atoms {
1273 let row = lines
1274 .next_line()
1275 .ok_or_else(|| ParseError::IncompleteSection(name.to_string()))?;
1276 let found = token_count(row);
1277 if found != with_flag && found != with_id {
1278 let expected = if found < with_flag {
1279 with_flag
1280 } else {
1281 with_id
1282 };
1283 return Err(ParseError::InvalidVectorLength { expected, found });
1284 }
1285 }
1286 }
1287 Ok(())
1288}
1289
1290fn open_section<'a>(lines: &mut impl LineStream<'a>) -> Result<bool, ParseError> {
1291 match lines.peek_line() {
1292 Some(line) if line.trim().is_empty() => {
1293 if lines.remainder_is_whitespace() {
1294 return Ok(false);
1295 }
1296 lines.next_line();
1297 Ok(true)
1298 }
1299 _ => Ok(false),
1300 }
1301}
1302
1303pub fn parse_velocity_section<'a>(
1316 lines: &mut impl LineStream<'a>,
1317 header: &FrameHeader,
1318 atom_data: &mut [AtomDatum],
1319) -> Result<bool, ParseError> {
1320 let validate = header.strict_validation;
1321 match lines.peek_line() {
1324 Some(line) if line.trim().is_empty() => {
1325 if lines.remainder_is_whitespace() {
1326 return Ok(false);
1327 }
1328 lines.next_line();
1329 }
1330 _ => return Ok(false),
1331 }
1332
1333 let mut atom_idx: usize = 0;
1334 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1335 let symbol = lines
1337 .next_line()
1338 .ok_or(ParseError::IncompleteSection(SECTION_VELOCITIES.into()))?
1339 .trim();
1340
1341 let comp_line = lines
1343 .next_line()
1344 .ok_or(ParseError::IncompleteSection(SECTION_VELOCITIES.into()))?;
1345 if !label_has_prefix(comp_line, "Velocities of Component") {
1346 return Err(ParseError::IncompleteSection(SECTION_VELOCITIES.into()));
1347 }
1348 if validate {
1349 validate_section_component(
1350 "Velocities",
1351 type_idx,
1352 atom_idx,
1353 symbol,
1354 comp_line,
1355 header,
1356 atom_data,
1357 )?;
1358 }
1359
1360 for _ in 0..num_atoms {
1361 let vel_line = lines
1362 .next_line()
1363 .ok_or(ParseError::IncompleteSection(SECTION_VELOCITIES.into()))?;
1364 let (vals, fixed, atom_id) = parse_vector_section_row(vel_line, header.spec_version)?;
1365 if validate {
1366 validate_section_atom_identity("velocities", atom_idx, fixed, atom_id, atom_data)?;
1367 }
1368 if atom_idx < atom_data.len() {
1369 atom_data[atom_idx].velocity = Some(vals);
1370 }
1371 atom_idx += 1;
1372 }
1373 }
1374
1375 Ok(true)
1376}
1377
1378pub fn parse_force_section<'a>(
1386 lines: &mut impl LineStream<'a>,
1387 header: &FrameHeader,
1388 atom_data: &mut [AtomDatum],
1389) -> Result<bool, ParseError> {
1390 let validate = header.strict_validation;
1391 if !open_section(lines)? {
1392 return Ok(false);
1393 }
1394
1395 let mut atom_idx: usize = 0;
1396 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1397 let symbol = lines
1398 .next_line()
1399 .ok_or(ParseError::IncompleteSection(SECTION_FORCES.into()))?
1400 .trim();
1401
1402 let comp_line = lines
1403 .next_line()
1404 .ok_or(ParseError::IncompleteSection(SECTION_FORCES.into()))?;
1405 if !label_has_prefix(comp_line, "Forces of Component") {
1406 return Err(ParseError::IncompleteSection(SECTION_FORCES.into()));
1407 }
1408 if validate {
1409 validate_section_component(
1410 "Forces", type_idx, atom_idx, symbol, comp_line, header, atom_data,
1411 )?;
1412 }
1413
1414 for _ in 0..num_atoms {
1415 let force_line = lines
1416 .next_line()
1417 .ok_or(ParseError::IncompleteSection(SECTION_FORCES.into()))?;
1418 let (vals, fixed, atom_id) = parse_vector_section_row(force_line, header.spec_version)?;
1419 if validate {
1420 validate_section_atom_identity("forces", atom_idx, fixed, atom_id, atom_data)?;
1421 }
1422 if atom_idx < atom_data.len() {
1423 atom_data[atom_idx].force = Some(vals);
1424 }
1425 atom_idx += 1;
1426 }
1427 }
1428
1429 Ok(true)
1430}
1431
1432pub fn parse_energy_section<'a>(
1445 lines: &mut impl LineStream<'a>,
1446 header: &FrameHeader,
1447 atom_data: &mut [AtomDatum],
1448) -> Result<bool, ParseError> {
1449 let validate = header.strict_validation;
1450 if !open_section(lines)? {
1451 return Ok(false);
1452 }
1453
1454 let mut atom_idx: usize = 0;
1455 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1456 let symbol = lines
1457 .next_line()
1458 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?
1459 .trim();
1460
1461 let comp_line = lines
1462 .next_line()
1463 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?;
1464 if !label_has_prefix(comp_line, "Energies of Component") {
1465 return Err(ParseError::IncompleteSection(SECTION_ENERGIES.into()));
1466 }
1467 if validate {
1468 validate_section_component(
1469 "Energies", type_idx, atom_idx, symbol, comp_line, header, atom_data,
1470 )?;
1471 }
1472
1473 for _ in 0..num_atoms {
1474 let energy_line = lines
1475 .next_line()
1476 .ok_or(ParseError::IncompleteSection(SECTION_ENERGIES.into()))?;
1477 let (value, fixed, atom_id) =
1478 parse_scalar_section_row(energy_line, header.spec_version)?;
1479 if validate {
1480 validate_section_atom_identity("energies", atom_idx, fixed, atom_id, atom_data)?;
1481 }
1482 if atom_idx < atom_data.len() {
1483 atom_data[atom_idx].energy = Some(value);
1484 }
1485 atom_idx += 1;
1486 }
1487 }
1488
1489 Ok(true)
1490}
1491
1492pub fn parse_declared_sections<'a>(
1498 lines: &mut impl LineStream<'a>,
1499 header: &mut FrameHeader,
1500 atom_data: &mut [AtomDatum],
1501) -> Result<usize, ParseError> {
1502 let mut applied = 0usize;
1503 if header.natm_types == 0 {
1506 if !header.sections.is_empty() {
1507 return Err(ParseError::InvalidCount(
1508 "a zero-atom frame cannot declare sections".into(),
1509 ));
1510 }
1511 return Ok(0);
1512 }
1513 if !header.sections_declared && header.sections.is_empty() {
1514 let found = parse_velocity_section(lines, header, atom_data)?;
1516 if found {
1517 header.sections.push(SECTION_VELOCITIES.into());
1518 applied = 1;
1519 }
1520 } else {
1521 let sections = std::mem::take(&mut header.sections);
1522 for section in §ions {
1523 match section.as_str() {
1524 SECTION_VELOCITIES => {
1525 let found = parse_velocity_section(lines, header, atom_data)?;
1526 if !found {
1527 return Err(ParseError::IncompleteSection(SECTION_VELOCITIES.into()));
1528 }
1529 applied += 1;
1530 }
1531 SECTION_FORCES => {
1532 let found = parse_force_section(lines, header, atom_data)?;
1533 if !found {
1534 return Err(ParseError::IncompleteSection(SECTION_FORCES.into()));
1535 }
1536 applied += 1;
1537 }
1538 SECTION_ENERGIES => {
1539 let found = parse_energy_section(lines, header, atom_data)?;
1540 if !found {
1541 return Err(ParseError::IncompleteSection(SECTION_ENERGIES.into()));
1542 }
1543 applied += 1;
1544 }
1545 SECTION_CHARGES => {
1546 let found = parse_charge_section(lines, header, atom_data)?;
1547 if !found {
1548 return Err(ParseError::IncompleteSection(SECTION_CHARGES.into()));
1549 }
1550 applied += 1;
1551 }
1552 SECTION_SPINS => {
1553 let found = parse_spin_section(lines, header, atom_data)?;
1554 if !found {
1555 return Err(ParseError::IncompleteSection(SECTION_SPINS.into()));
1556 }
1557 applied += 1;
1558 }
1559 SECTION_MAGMOMS => {
1560 let found = parse_magmom_section(lines, header, atom_data)?;
1561 if !found {
1562 return Err(ParseError::IncompleteSection(SECTION_MAGMOMS.into()));
1563 }
1564 applied += 1;
1565 }
1566 SECTION_DISPLACEMENTS => {
1567 let found = parse_displacement_section(lines, header, atom_data)?;
1568 if !found {
1569 return Err(ParseError::IncompleteSection(SECTION_DISPLACEMENTS.into()));
1570 }
1571 applied += 1;
1572 }
1573 SECTION_SPREADS => {
1574 let found = parse_spread_section(lines, header, atom_data)?;
1575 if !found {
1576 return Err(ParseError::IncompleteSection(SECTION_SPREADS.into()));
1577 }
1578 applied += 1;
1579 }
1580 other => {
1581 if frame_minor(header) > u64::from(crate::CON_SPEC_MINOR) {
1582 if let Some(arity) = section_row_arity(header, other) {
1583 skip_unknown_section(lines, header, other, arity)?;
1584 continue;
1585 }
1586 }
1587 return Err(ParseError::UnknownSection(other.to_string()));
1588 }
1589 }
1590 }
1591 header.sections = sections;
1592 }
1593 reject_velocities_without_time(header)?;
1594 Ok(applied)
1595}
1596
1597fn reject_velocities_without_time(header: &FrameHeader) -> Result<(), ParseError> {
1601 if header.spec_version < 3 || frame_minor(header) < 1 {
1602 return Ok(());
1603 }
1604 if !header
1605 .sections
1606 .iter()
1607 .any(|name| name == SECTION_VELOCITIES)
1608 {
1609 return Ok(());
1610 }
1611 let time_ok = header
1612 .metadata
1613 .get(meta::UNITS)
1614 .and_then(|units| units.get("time"))
1615 .and_then(|value| value.as_str())
1616 .is_some_and(|unit| !unit.trim().is_empty());
1617 if time_ok {
1618 Ok(())
1619 } else {
1620 Err(ParseError::InvalidUnits(
1621 "minor >= 1 requires units.time when a velocities section is present".into(),
1622 ))
1623 }
1624}
1625
1626fn parse_scalar_atom_section<'a>(
1629 lines: &mut impl LineStream<'a>,
1630 header: &FrameHeader,
1631 atom_data: &mut [AtomDatum],
1632 section_name: &str,
1633 component_label: &str,
1634 mut set_value: impl FnMut(&mut AtomDatum, f64),
1635) -> Result<bool, ParseError> {
1636 let validate = header.strict_validation;
1637 if !open_section(lines)? {
1638 return Ok(false);
1639 }
1640
1641 let mut atom_idx: usize = 0;
1642 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1643 let symbol = lines
1644 .next_line()
1645 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?
1646 .trim();
1647
1648 let comp_line = lines
1649 .next_line()
1650 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
1651 if !label_has_prefix(comp_line, component_label) {
1652 return Err(ParseError::IncompleteSection(section_name.into()));
1653 }
1654 if validate {
1655 validate_section_component(
1656 component_label.trim_end_matches(" of Component").trim(),
1657 type_idx,
1658 atom_idx,
1659 symbol,
1660 comp_line,
1661 header,
1662 atom_data,
1663 )?;
1664 }
1665
1666 for _ in 0..num_atoms {
1667 let data_line = lines
1668 .next_line()
1669 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
1670 let (value, fixed, atom_id) = parse_scalar_section_row(data_line, header.spec_version)?;
1671 if validate {
1672 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
1673 }
1674 if atom_idx < atom_data.len() {
1675 set_value(&mut atom_data[atom_idx], value);
1676 }
1677 atom_idx += 1;
1678 }
1679 }
1680 Ok(true)
1681}
1682
1683pub fn parse_charge_section<'a>(
1684 lines: &mut impl LineStream<'a>,
1685 header: &FrameHeader,
1686 atom_data: &mut [AtomDatum],
1687) -> Result<bool, ParseError> {
1688 parse_scalar_atom_section(
1689 lines,
1690 header,
1691 atom_data,
1692 SECTION_CHARGES,
1693 "Charges of Component",
1694 |a, v| a.charge = Some(v),
1695 )
1696}
1697
1698pub fn parse_spin_section<'a>(
1699 lines: &mut impl LineStream<'a>,
1700 header: &FrameHeader,
1701 atom_data: &mut [AtomDatum],
1702) -> Result<bool, ParseError> {
1703 parse_scalar_atom_section(
1704 lines,
1705 header,
1706 atom_data,
1707 SECTION_SPINS,
1708 "Spins of Component",
1709 |a, v| a.spin = Some(v),
1710 )
1711}
1712
1713fn parse_vector_named_section<'a>(
1714 lines: &mut impl LineStream<'a>,
1715 header: &FrameHeader,
1716 atom_data: &mut [AtomDatum],
1717 section_name: &str,
1718 component_label: &str,
1719 label_kind: &str,
1720 mut set_value: impl FnMut(&mut AtomDatum, [f64; 3]),
1721 mut check_row: impl FnMut(usize, [f64; 3]) -> Result<(), ParseError>,
1722) -> Result<bool, ParseError> {
1723 let validate = header.strict_validation;
1724 if !open_section(lines)? {
1725 return Ok(false);
1726 }
1727 let mut atom_idx: usize = 0;
1728 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1729 let symbol = lines
1730 .next_line()
1731 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?
1732 .trim();
1733 let comp_line = lines
1734 .next_line()
1735 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
1736 if !label_has_prefix(comp_line, component_label) {
1737 return Err(ParseError::IncompleteSection(section_name.into()));
1738 }
1739 if validate {
1740 validate_section_component(
1741 label_kind, type_idx, atom_idx, symbol, comp_line, header, atom_data,
1742 )?;
1743 }
1744 for _ in 0..num_atoms {
1745 let row = lines
1746 .next_line()
1747 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
1748 let (vals, fixed, atom_id) = parse_vector_section_row(row, header.spec_version)?;
1749 check_row(atom_idx, vals)?;
1750 if validate {
1751 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
1752 }
1753 if atom_idx < atom_data.len() {
1754 set_value(&mut atom_data[atom_idx], vals);
1755 }
1756 atom_idx += 1;
1757 }
1758 }
1759 Ok(true)
1760}
1761
1762pub fn parse_magmom_section<'a>(
1764 lines: &mut impl LineStream<'a>,
1765 header: &FrameHeader,
1766 atom_data: &mut [AtomDatum],
1767) -> Result<bool, ParseError> {
1768 parse_vector_named_section(
1769 lines,
1770 header,
1771 atom_data,
1772 SECTION_MAGMOMS,
1773 "Magmoms of Component",
1774 "Magmoms",
1775 |a, v| a.magmom = Some(v),
1776 |_, _| Ok(()),
1777 )
1778}
1779
1780pub fn parse_displacement_section<'a>(
1782 lines: &mut impl LineStream<'a>,
1783 header: &FrameHeader,
1784 atom_data: &mut [AtomDatum],
1785) -> Result<bool, ParseError> {
1786 parse_vector_named_section(
1787 lines,
1788 header,
1789 atom_data,
1790 SECTION_DISPLACEMENTS,
1791 "Displacements of Component",
1792 "Displacements",
1793 |a, v| a.displacement = Some(v),
1794 |_, _| Ok(()),
1795 )
1796}
1797
1798pub fn parse_spread_section<'a>(
1801 lines: &mut impl LineStream<'a>,
1802 header: &FrameHeader,
1803 atom_data: &mut [AtomDatum],
1804) -> Result<bool, ParseError> {
1805 parse_vector_named_section(
1806 lines,
1807 header,
1808 atom_data,
1809 SECTION_SPREADS,
1810 "Spreads of Component",
1811 "Spreads",
1812 |a, v| a.spread = Some(v),
1813 |atom_idx, vals| {
1814 if !vals.iter().all(|v| v.is_finite()) {
1815 return Err(ParseError::NonFiniteValue(format!(
1816 "spreads atom {atom_idx}"
1817 )));
1818 }
1819 if vals.iter().any(|v| *v < 0.0) {
1820 return Err(crate::types::invalid_spread(atom_idx));
1821 }
1822 Ok(())
1823 },
1824 )
1825}
1826
1827#[cfg(test)]
1828mod tests {
1829 use super::*;
1830 use crate::iterators::ConFrameIterator;
1831
1832 #[test]
1833 fn test_parse_line_of_n_success() {
1834 let line = "1.0 2.5 -3.0";
1835 let values = parse_line_of_n::<f64>(line, 3).unwrap();
1836 assert_eq!(values, vec![1.0, 2.5, -3.0]);
1837 }
1838
1839 #[test]
1840 fn test_parse_line_of_n_too_short() {
1841 let line = "1.0 2.5";
1842 let result = parse_line_of_n::<f64>(line, 3);
1843 assert!(result.is_err());
1844 assert!(matches!(
1845 result.unwrap_err(),
1846 ParseError::InvalidVectorLength {
1847 expected: 3,
1848 found: 2
1849 }
1850 ));
1851 }
1852
1853 #[test]
1854 fn test_parse_line_of_n_too_long() {
1855 let line = "1.0 2.5 -3.0 4.0";
1856 let result = parse_line_of_n::<f64>(line, 3);
1857 assert!(result.is_err());
1858 assert!(matches!(
1859 result.unwrap_err(),
1860 ParseError::InvalidVectorLength {
1861 expected: 3,
1862 found: 4
1863 }
1864 ));
1865 }
1866
1867 #[test]
1868 fn test_parse_line_of_n_invalid_float() {
1869 let line = "1.0 abc -3.0";
1870 let result = parse_line_of_n::<f64>(line, 3);
1871 assert!(result.is_err());
1872 assert!(matches!(
1873 result.unwrap_err(),
1874 ParseError::InvalidNumberFormat(_)
1875 ));
1876 }
1877
1878 #[test]
1879 fn test_parse_frame_header_success() {
1880 let lines = [
1881 "PREBOX1",
1882 "{\"con_spec_version\":2}",
1883 "10.0 20.0 30.0",
1884 "90.0 90.0 90.0",
1885 "POSTBOX1",
1886 "POSTBOX2",
1887 "2",
1888 "1 1",
1889 "12.011 1.008",
1890 ];
1891 let mut line_it = lines.iter().copied();
1892 match parse_frame_header(&mut line_it) {
1893 Ok(header) => {
1894 assert_eq!(header.prebox_header.user, "PREBOX1");
1895 assert_eq!(header.spec_version, 2);
1896 assert_eq!(header.boxl, [10.0, 20.0, 30.0]);
1897 assert_eq!(header.angles, [90.0, 90.0, 90.0]);
1898 assert_eq!(header.postbox_header, ["POSTBOX1", "POSTBOX2"]);
1899 assert_eq!(header.natm_types, 2);
1900 assert_eq!(header.natms_per_type, vec![1, 1]);
1901 assert_eq!(header.masses_per_type, vec![12.011, 1.008]);
1902 }
1903 Err(e) => {
1904 panic!(
1905 "Parsing failed when it should have succeeded. Error: {:?}",
1906 e
1907 );
1908 }
1909 }
1910 }
1911
1912 #[test]
1913 fn test_parse_frame_header_missing_line() {
1914 let lines = [
1915 "PREBOX1",
1916 "{\"con_spec_version\":2}",
1917 "10.0 20.0 30.0",
1918 "90.0 90.0 90.0",
1919 "POSTBOX1",
1920 "POSTBOX2",
1921 "2",
1922 "1 1",
1923 ];
1925 let mut line_it = lines.iter().copied();
1926 let result = parse_frame_header(&mut line_it);
1927 assert!(result.is_err());
1928 assert!(matches!(result.unwrap_err(), ParseError::IncompleteHeader));
1929 }
1930
1931 #[test]
1932 fn test_parse_frame_header_missing_spec_version() {
1933 let lines = vec![
1934 "PREBOX1",
1935 "{}",
1936 "10.0 20.0 30.0",
1937 "90.0 90.0 90.0",
1938 "POSTBOX1",
1939 "POSTBOX2",
1940 "2",
1941 "1 1",
1942 "12.011 1.008",
1943 ];
1944 let mut line_it = lines.iter().copied();
1945 let result = parse_frame_header(&mut line_it);
1946 assert!(result.is_err());
1947 assert!(matches!(
1948 result.unwrap_err(),
1949 ParseError::MissingSpecVersion
1950 ));
1951 }
1952
1953 #[test]
1954 fn test_parse_frame_header_legacy_no_json() {
1955 let lines = vec![
1957 "PREBOX1",
1958 "0.0000 TIME",
1959 "10.0 20.0 30.0",
1960 "90.0 90.0 90.0",
1961 "POSTBOX1",
1962 "POSTBOX2",
1963 "2",
1964 "1 1",
1965 "12.011 1.008",
1966 ];
1967 let mut line_it = lines.iter().copied();
1968 let header = parse_frame_header(&mut line_it).unwrap();
1969 assert_eq!(header.spec_version, 1);
1970 assert!(header.metadata.is_empty());
1971 }
1972
1973 #[test]
1974 fn test_parse_frame_header_malformed_json() {
1975 let lines = vec![
1977 "PREBOX1",
1978 "{broken json",
1979 "10.0 20.0 30.0",
1980 "90.0 90.0 90.0",
1981 "POSTBOX1",
1982 "POSTBOX2",
1983 "2",
1984 "1 1",
1985 "12.011 1.008",
1986 ];
1987 let mut line_it = lines.iter().copied();
1988 let result = parse_frame_header(&mut line_it);
1989 assert!(result.is_err());
1990 assert!(matches!(
1991 result.unwrap_err(),
1992 ParseError::InvalidMetadataJson(_)
1993 ));
1994 }
1995
1996 #[test]
1997 fn test_parse_frame_header_unsupported_version() {
1998 let lines = vec![
1999 "PREBOX1",
2000 "{\"con_spec_version\":999}",
2001 "10.0 20.0 30.0",
2002 "90.0 90.0 90.0",
2003 "POSTBOX1",
2004 "POSTBOX2",
2005 "2",
2006 "1 1",
2007 "12.011 1.008",
2008 ];
2009 let mut line_it = lines.iter().copied();
2010 let result = parse_frame_header(&mut line_it);
2011 assert!(result.is_err());
2012 assert!(matches!(
2013 result.unwrap_err(),
2014 ParseError::UnsupportedVersion(999)
2015 ));
2016 }
2017
2018 #[test]
2019 fn test_v3_missing_units_rejected() {
2020 let lines = vec![
2021 "PREBOX1",
2022 "{\"con_spec_version\":3}",
2023 "10.0 20.0 30.0",
2024 "90.0 90.0 90.0",
2025 "POSTBOX1",
2026 "POSTBOX2",
2027 "1",
2028 "1",
2029 "1.0",
2030 ];
2031 let mut line_it = lines.iter().copied();
2032 let err = parse_frame_header(&mut line_it).unwrap_err();
2033 let msg = err.to_string();
2034 assert!(
2035 msg.contains("units") || msg.contains("v3"),
2036 "expected units error, got {msg}"
2037 );
2038 }
2039
2040 #[test]
2041 fn test_v3_invalid_units_rejected() {
2042 let lines = vec![
2043 "PREBOX1",
2044 r#"{"con_spec_version":3,"units":{"length":"eV","energy":"angstrom"}}"#,
2045 "10.0 20.0 30.0",
2046 "90.0 90.0 90.0",
2047 "POSTBOX1",
2048 "POSTBOX2",
2049 "1",
2050 "1",
2051 "1.0",
2052 ];
2053 let mut line_it = lines.iter().copied();
2054 assert!(parse_frame_header(&mut line_it).is_err());
2055 }
2056
2057 #[test]
2058 fn test_v3_valid_units_exposes_length_energy() {
2059 let lines = vec![
2060 "PREBOX1",
2061 r#"{"con_spec_version":3,"units":{"length":"angstrom","energy":"eV","mass":"amu","time":"fs"}}"#,
2062 "10.0 20.0 30.0",
2063 "90.0 90.0 90.0",
2064 "POSTBOX1",
2065 "POSTBOX2",
2066 "1",
2067 "1",
2068 "1.0",
2069 ];
2070 let mut line_it = lines.iter().copied();
2071 let header = parse_frame_header(&mut line_it).unwrap();
2072 assert_eq!(header.spec_version, 3);
2073 assert_eq!(header.length_unit(), Some("angstrom"));
2074 assert_eq!(header.energy_unit(), Some("eV"));
2075 let f = header.conversion_factor_to("length", "nm").unwrap();
2076 assert!((f - 0.1).abs() < 1e-12);
2077 }
2078
2079 #[test]
2080 fn test_parse_frame_header_extra_metadata_preserved() {
2081 let lines = vec![
2082 "PREBOX1",
2083 "{\"con_spec_version\":2,\"generator\":\"test\"}",
2084 "10.0 20.0 30.0",
2085 "90.0 90.0 90.0",
2086 "POSTBOX1",
2087 "POSTBOX2",
2088 "2",
2089 "1 1",
2090 "12.011 1.008",
2091 ];
2092 let mut line_it = lines.iter().copied();
2093 let header = parse_frame_header(&mut line_it).unwrap();
2094 assert_eq!(header.spec_version, 2);
2095 assert_eq!(
2096 header.metadata.get("generator"),
2097 Some(&serde_json::Value::String("test".to_string()))
2098 );
2099 }
2100
2101 #[test]
2102 fn test_parse_frame_header_invalid_natms_per_type() {
2103 let lines = vec![
2104 "PREBOX1",
2105 "{\"con_spec_version\":2}",
2106 "10.0 20.0 30.0",
2107 "90.0 90.0 90.0",
2108 "POSTBOX1",
2109 "POSTBOX2",
2110 "2",
2111 "1 1 1", "12.011 1.008",
2113 ];
2114 let mut line_it = lines.iter().copied();
2115 let result = parse_frame_header(&mut line_it);
2116 assert!(result.is_err());
2117 assert!(matches!(
2118 result.unwrap_err(),
2119 ParseError::InvalidVectorLength {
2120 expected: 2,
2121 found: 3
2122 }
2123 ));
2124 }
2125
2126 #[test]
2127 fn test_parse_single_frame_success() {
2128 let lines = vec![
2129 "PREBOX1",
2130 "{\"con_spec_version\":2}",
2131 "10.0 20.0 30.0",
2132 "90.0 90.0 90.0",
2133 "POSTBOX1",
2134 "POSTBOX2",
2135 "2",
2136 "3 3",
2137 "12.011 1.008",
2138 "1",
2139 "Coordinates of Component 1",
2140 "0.0 0.0 0.0 0 1",
2141 "1.0940 0.0 0.0 0 2",
2142 "-0.5470 0.9499 0.0 0 3",
2143 "2",
2144 "Coordinates of Component 2",
2145 "5.0 5.0 5.0 0 4",
2146 "6.0940 5.0 5.0 0 5",
2147 "5.5470 5.9499 5.0 0 6",
2148 ];
2149 let mut line_it = lines.iter().copied();
2150 let frame = parse_single_frame(&mut line_it).unwrap();
2151
2152 assert_eq!(frame.header.natm_types, 2);
2153 assert_eq!(frame.header.natms_per_type, vec![3, 3]);
2154 assert_eq!(frame.header.masses_per_type, vec![12.011, 1.008]);
2155 assert_eq!(frame.atom_data.len(), 6);
2156 assert_eq!(&*frame.atom_data[0].symbol, "1");
2157 assert_eq!(frame.atom_data[0].atom_id, 1);
2158 assert_eq!(&*frame.atom_data[5].symbol, "2");
2159 assert_eq!(frame.atom_data[5].atom_id, 6);
2160 }
2161
2162 #[test]
2163 fn test_column_4_value_1_is_x_only_on_spec_2() {
2164 let lines = vec![
2165 "Generated by probe",
2166 "{\"con_spec_version\":2}",
2167 "20.0 20.0 20.0",
2168 "90.0 90.0 90.0",
2169 "",
2170 "",
2171 "1",
2172 "1",
2173 "63.5",
2174 "Cu",
2175 "Coordinates of component 1",
2176 "1.0 2.0 3.0 1 1",
2177 ];
2178 let mut line_it = lines.iter().copied();
2179 let frame = parse_single_frame(&mut line_it).unwrap();
2180 assert_eq!(frame.header.spec_version, 2);
2181 assert_eq!(frame.atom_data[0].fixed, [true, false, false]);
2182 }
2183
2184 #[test]
2185 fn test_column_4_value_1_is_all_fixed_on_spec_1() {
2186 let lines = vec![
2187 "Generated by probe",
2188 "0.0000 TIME",
2189 "20.0 20.0 20.0",
2190 "90.0 90.0 90.0",
2191 "",
2192 "",
2193 "1",
2194 "1",
2195 "63.5",
2196 "Cu",
2197 "Coordinates of component 1",
2198 "1.0 2.0 3.0 1 1",
2199 ];
2200 let mut line_it = lines.iter().copied();
2201 let frame = parse_single_frame(&mut line_it).unwrap();
2202 assert_eq!(frame.header.spec_version, 1);
2203 assert_eq!(frame.atom_data[0].fixed, [true, true, true]);
2204 }
2205
2206 #[test]
2207 fn test_parse_single_frame_missing_line() {
2208 let lines = vec![
2210 "PREBOX1",
2211 "{\"con_spec_version\":2}",
2212 "10.0 20.0 30.0",
2213 "90.0 90.0 90.0",
2214 "POSTBOX1",
2215 "POSTBOX2",
2216 "2",
2217 "3 3",
2218 "12.011 1.008",
2219 "1",
2220 "Coordinates of Component 1",
2221 "0.0 0.0 0.0 0 1",
2222 "1.0940 0.0 0.0 0 2",
2223 "-0.5470 0.9499 0.0 0 3",
2224 ];
2226 let mut line_it = lines.iter().copied();
2227 let result = parse_single_frame(&mut line_it);
2228 assert!(result.is_err());
2229 assert!(matches!(result.unwrap_err(), ParseError::IncompleteFrame));
2230 }
2231
2232 #[test]
2233 fn test_parse_single_frame_missing_atom_index_defaults_sequential() {
2234 let lines = vec![
2236 "PREBOX1",
2237 "{\"con_spec_version\":2}",
2238 "10.0 20.0 30.0",
2239 "90.0 90.0 90.0",
2240 "POSTBOX1",
2241 "POSTBOX2",
2242 "2",
2243 "3 3",
2244 "12.011 1.008",
2245 "1",
2246 "Coordinates of Component 1",
2247 "0.0 0.0 0.0 0 1",
2248 "1.0940 0.0 0.0 0 2",
2249 "-0.5470 0.9499 0.0 0 3",
2250 "2",
2251 "Coordinates of Component 2",
2252 "5.0 5.0 5.0 0", "6.0940 5.0 5.0 0 10", "5.5470 5.9499 5.0 0", ];
2256 let mut line_it = lines.iter().copied();
2257 let frame = parse_single_frame(&mut line_it).unwrap();
2258 assert_eq!(frame.atom_data.len(), 6);
2259 assert_eq!(frame.atom_data[0].atom_id, 1);
2261 assert_eq!(frame.atom_data[1].atom_id, 2);
2262 assert_eq!(frame.atom_data[2].atom_id, 3);
2263 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); }
2268
2269 #[test]
2270 fn test_parse_single_frame_too_few_columns_fails() {
2271 let lines = vec![
2273 "PREBOX1",
2274 "{\"con_spec_version\":2}",
2275 "10.0 20.0 30.0",
2276 "90.0 90.0 90.0",
2277 "POSTBOX1",
2278 "POSTBOX2",
2279 "1",
2280 "1",
2281 "12.011",
2282 "C",
2283 "Coordinates of Component 1",
2284 "0.0 0.0 0.0", ];
2286 let mut line_it = lines.iter().copied();
2287 let result = parse_single_frame(&mut line_it);
2288 assert!(result.is_err());
2289 assert!(matches!(
2290 result.unwrap_err(),
2291 ParseError::InvalidVectorLength {
2292 expected: 5,
2293 found: 3
2294 }
2295 ));
2296 }
2297
2298 #[test]
2299 fn test_parse_velocity_section_present() {
2300 let lines = vec![
2301 "PREBOX1",
2302 "{\"con_spec_version\":2}",
2303 "10.0 20.0 30.0",
2304 "90.0 90.0 90.0",
2305 "POSTBOX1",
2306 "POSTBOX2",
2307 "2",
2308 "1 1",
2309 "63.546 1.008",
2310 "Cu",
2311 "Coordinates of Component 1",
2312 "0.0 0.0 0.0 1 0",
2313 "H",
2314 "Coordinates of Component 2",
2315 "1.0 2.0 3.0 0 1",
2316 "",
2317 "Cu",
2318 "Velocities of Component 1",
2319 "0.1 0.2 0.3 1 0",
2320 "H",
2321 "Velocities of Component 2",
2322 "0.4 0.5 0.6 0 1",
2323 ];
2324 let mut line_it = lines.iter().copied().peekable();
2325 let mut frame =
2327 parse_single_frame(&mut line_it).expect("coordinate parsing should succeed");
2328 assert!(!frame.has_velocities());
2329
2330 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
2332 .expect("velocity parsing should succeed");
2333 assert!(has_vel);
2334 assert_eq!(frame.atom_data[0].velocity, Some([0.1, 0.2, 0.3]));
2335 assert_eq!(frame.atom_data[1].velocity, Some([0.4, 0.5, 0.6]));
2336 }
2337
2338 #[test]
2339 fn test_validate_true_accepts_matching_section_identity() {
2340 let text = r#"
2341PREBOX1
2342{"con_spec_version":2,"sections":["velocities"],"validate":true}
234310.0 20.0 30.0
234490.0 90.0 90.0
2345POSTBOX1
2346POSTBOX2
23472
23481 1
234963.546 1.008
2350Cu
2351Coordinates of Component 1
23520.0 0.0 0.0 5 0
2353H
2354Coordinates of Component 2
23551.0 2.0 3.0 0 1
2356
2357Cu
2358Velocities of Component 1
23590.1 0.2 0.3 5 0
2360H
2361Velocities of Component 2
23620.4 0.5 0.6 0 1
2363"#;
2364 let mut iter = ConFrameIterator::new(text.trim());
2365 let frame = iter.next().unwrap().unwrap();
2366
2367 assert!(frame.has_velocities());
2368 assert_eq!(
2369 frame
2370 .header
2371 .metadata
2372 .get("validate")
2373 .and_then(|v| v.as_bool()),
2374 Some(true)
2375 );
2376 }
2377
2378 #[test]
2379 fn test_validate_true_rejects_section_atom_id_mismatch() {
2380 let text = r#"
2381PREBOX1
2382{"con_spec_version":2,"sections":["velocities"],"validate":true}
238310.0 20.0 30.0
238490.0 90.0 90.0
2385POSTBOX1
2386POSTBOX2
23871
23881
238963.546
2390Cu
2391Coordinates of Component 1
23920.0 0.0 0.0 5 0
2393
2394Cu
2395Velocities of Component 1
23960.1 0.2 0.3 5 99
2397"#;
2398 let mut iter = ConFrameIterator::new(text.trim());
2399 let err = iter.next().unwrap().unwrap_err();
2400
2401 assert!(matches!(err, ParseError::ValidationError(_)));
2402 assert!(err.to_string().contains("atom_id mismatch"));
2403 }
2404
2405 #[test]
2406 fn test_validate_true_rejects_section_symbol_mismatch() {
2407 let text = r#"
2408PREBOX1
2409{"con_spec_version":2,"sections":["forces"],"validate":true}
241010.0 20.0 30.0
241190.0 90.0 90.0
2412POSTBOX1
2413POSTBOX2
24141
24151
241663.546
2417Cu
2418Coordinates of Component 1
24190.0 0.0 0.0 5 0
2420
2421H
2422Forces of Component 1
24230.1 0.2 0.3 5 0
2424"#;
2425 let mut iter = ConFrameIterator::new(text.trim());
2426 let err = iter.next().unwrap().unwrap_err();
2427
2428 assert!(matches!(err, ParseError::ValidationError(_)));
2429 assert!(err.to_string().contains("symbol mismatch"));
2430 }
2431
2432 #[test]
2433 fn test_validate_absent_allows_legacy_duplicate_identity_mismatch() {
2434 let text = r#"
2435PREBOX1
2436{"con_spec_version":2,"sections":["velocities"]}
243710.0 20.0 30.0
243890.0 90.0 90.0
2439POSTBOX1
2440POSTBOX2
24411
24421
244363.546
2444Cu
2445Coordinates of Component 1
24460.0 0.0 0.0 5 0
2447
2448Cu
2449Velocities of Component 1
24500.1 0.2 0.3 0 99
2451"#;
2452 let mut iter = ConFrameIterator::new(text.trim());
2453 let frame = iter.next().unwrap().unwrap();
2454
2455 assert!(frame.has_velocities());
2456 assert_eq!(frame.atom_data[0].atom_id, 0);
2457 assert_eq!(frame.atom_data[0].fixed, [true, false, true]);
2458 }
2459
2460 #[test]
2461 fn test_validate_must_be_boolean_when_present() {
2462 let lines = vec![
2463 "PREBOX1",
2464 "{\"con_spec_version\":2,\"validate\":\"yes\",\"sections\":[]}",
2465 "10.0 20.0 30.0",
2466 "90.0 90.0 90.0",
2467 "POSTBOX1",
2468 "POSTBOX2",
2469 "1",
2470 "1",
2471 "12.011",
2472 ];
2473 let mut line_it = lines.iter().copied();
2474 let err = parse_frame_header(&mut line_it).unwrap_err();
2475
2476 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2477 assert!(err.to_string().contains("validate"));
2478 }
2479
2480 #[test]
2481 fn test_validate_true_requires_sections_key() {
2482 let lines = vec![
2483 "PREBOX1",
2484 "{\"con_spec_version\":2,\"validate\":true}",
2485 "10.0 20.0 30.0",
2486 "90.0 90.0 90.0",
2487 "POSTBOX1",
2488 "POSTBOX2",
2489 "1",
2490 "1",
2491 "12.011",
2492 ];
2493 let mut line_it = lines.iter().copied();
2494 let err = parse_frame_header(&mut line_it).unwrap_err();
2495
2496 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2497 assert!(err.to_string().contains("sections"));
2498 }
2499
2500 #[test]
2501 fn test_sections_must_be_string_array_when_present() {
2502 let lines = vec![
2503 "PREBOX1",
2504 "{\"con_spec_version\":2,\"sections\":[\"velocities\",7]}",
2505 "10.0 20.0 30.0",
2506 "90.0 90.0 90.0",
2507 "POSTBOX1",
2508 "POSTBOX2",
2509 "1",
2510 "1",
2511 "12.011",
2512 ];
2513 let mut line_it = lines.iter().copied();
2514 let err = parse_frame_header(&mut line_it).unwrap_err();
2515
2516 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2517 assert!(err.to_string().contains("sections"));
2518 }
2519
2520 #[test]
2521 fn test_validate_true_rejects_non_integer_coordinate_identity_columns() {
2522 let text = r#"
2523PREBOX1
2524{"con_spec_version":2,"sections":[],"validate":true}
252510.0 20.0 30.0
252690.0 90.0 90.0
2527POSTBOX1
2528POSTBOX2
25291
25301
253163.546
2532Cu
2533Coordinates of Component 1
25340.0 0.0 0.0 5.0 0
2535"#;
2536 let mut iter = ConFrameIterator::new(text.trim());
2537 let err = iter.next().unwrap().unwrap_err();
2538
2539 assert!(matches!(err, ParseError::InvalidFixedFlag(_)));
2540 assert!(err.to_string().contains("5.0"));
2541 }
2542
2543 #[test]
2544 fn test_validate_true_rejects_non_exact_coordinate_label() {
2545 let text = r#"
2546PREBOX1
2547{"con_spec_version":2,"sections":[],"validate":true}
254810.0 20.0 30.0
254990.0 90.0 90.0
2550POSTBOX1
2551POSTBOX2
25521
25531
255463.546
2555Cu
2556Coordinates Component 1
25570.0 0.0 0.0 5 0
2558"#;
2559 let mut iter = ConFrameIterator::new(text.trim());
2560 let err = iter.next().unwrap().unwrap_err();
2561
2562 assert!(matches!(err, ParseError::ValidationError(_)));
2563 assert!(err.to_string().contains("Coordinates of Component 1"));
2564 }
2565
2566 #[test]
2567 fn test_validate_true_rejects_unknown_component_symbol() {
2568 let text = r#"
2569PREBOX1
2570{"con_spec_version":2,"sections":[],"validate":true}
257110.0 20.0 30.0
257290.0 90.0 90.0
2573POSTBOX1
2574POSTBOX2
25751
25761
257763.546
2578Qq
2579Coordinates of Component 1
25800.0 0.0 0.0 0 0
2581"#;
2582 let mut iter = ConFrameIterator::new(text.trim());
2583 let err = iter.next().unwrap().unwrap_err();
2584
2585 assert!(matches!(err, ParseError::ValidationError(_)));
2586 assert!(err.to_string().contains("symbol"));
2587 }
2588
2589 #[test]
2590 fn test_declared_section_must_be_present() {
2591 let text = r#"
2592PREBOX1
2593{"con_spec_version":2,"sections":["velocities"]}
259410.0 20.0 30.0
259590.0 90.0 90.0
2596POSTBOX1
2597POSTBOX2
25981
25991
260063.546
2601Cu
2602Coordinates of Component 1
26030.0 0.0 0.0 0 0
2604"#;
2605 let mut iter = ConFrameIterator::new(text.trim());
2606 let err = iter.next().unwrap().unwrap_err();
2607
2608 assert!(matches!(
2609 err,
2610 ParseError::IncompleteSection(ref name) if name == "velocities"
2611 ));
2612 }
2613
2614 #[test]
2615 fn test_non_finite_cell_geometry_rejected_in_strict_mode() {
2616 let lines = vec![
2617 "PREBOX1",
2618 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
2619 "10.0 NaN 30.0",
2620 "90.0 90.0 90.0",
2621 "POSTBOX1",
2622 "POSTBOX2",
2623 "1",
2624 "1",
2625 "12.011",
2626 ];
2627 let mut line_it = lines.iter().copied();
2628 let err = parse_frame_header(&mut line_it).unwrap_err();
2629
2630 assert!(matches!(err, ParseError::NonFiniteValue(_)));
2631 assert!(err.to_string().contains("NaN"));
2632 }
2633
2634 #[test]
2635 fn test_validate_true_rejects_non_physical_cell_geometry() {
2636 let lines = vec![
2637 "PREBOX1",
2638 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
2639 "0.0 20.0 30.0",
2640 "90.0 180.0 90.0",
2641 "POSTBOX1",
2642 "POSTBOX2",
2643 "1",
2644 "1",
2645 "12.011",
2646 ];
2647 let mut line_it = lines.iter().copied();
2648 let err = parse_frame_header(&mut line_it).unwrap_err();
2649
2650 assert!(matches!(err, ParseError::ValidationError(_)));
2651 assert!(err.to_string().contains("cell"));
2652 }
2653
2654 #[test]
2655 fn test_validate_true_rejects_reserved_metadata_type_mismatch() {
2656 let lines = vec![
2657 "PREBOX1",
2658 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"energy\":\"low\"}",
2659 "10.0 20.0 30.0",
2660 "90.0 90.0 90.0",
2661 "POSTBOX1",
2662 "POSTBOX2",
2663 "1",
2664 "1",
2665 "12.011",
2666 ];
2667 let mut line_it = lines.iter().copied();
2668 let err = parse_frame_header(&mut line_it).unwrap_err();
2669
2670 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2671 assert!(err.to_string().contains("energy"));
2672 }
2673
2674 #[test]
2675 fn test_validate_true_rejects_malformed_bonds() {
2676 let lines = vec![
2677 "PREBOX1",
2678 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"bonds\":[[0]]}",
2679 "10.0 20.0 30.0",
2680 "90.0 90.0 90.0",
2681 "POSTBOX1",
2682 "POSTBOX2",
2683 "1",
2684 "1",
2685 "12.011",
2686 ];
2687 let mut line_it = lines.iter().copied();
2688 let err = parse_frame_header(&mut line_it).unwrap_err();
2689 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2690 assert!(err.to_string().contains("bonds"));
2691 }
2692
2693 #[test]
2694 fn test_bonds_metadata_round_trip_in_header() {
2695 use crate::types::{Bond, meta};
2696 let lines = vec![
2697 "PREBOX1",
2698 "{\"con_spec_version\":2,\"bonds\":[[0,1],{\"i\":0,\"j\":2,\"order\":1}]}",
2699 "10.0 20.0 30.0",
2700 "90.0 90.0 90.0",
2701 "POSTBOX1",
2702 "POSTBOX2",
2703 "1",
2704 "1",
2705 "12.011",
2706 ];
2707 let mut line_it = lines.iter().copied();
2708 let header = parse_frame_header(&mut line_it).expect("header");
2709 let bonds = header.bonds();
2710 assert_eq!(bonds.len(), 2);
2711 assert_eq!(bonds[0], Bond::new(0, 1));
2712 assert_eq!(bonds[1].i, 0);
2713 assert_eq!(bonds[1].j, 2);
2714 assert_eq!(bonds[1].order, Some(1));
2715 assert!(header.metadata.contains_key(meta::BONDS));
2716 }
2717
2718 #[test]
2719 fn test_parse_velocity_section_absent() {
2720 let lines = vec![
2721 "PREBOX1",
2722 "{\"con_spec_version\":2}",
2723 "10.0 20.0 30.0",
2724 "90.0 90.0 90.0",
2725 "POSTBOX1",
2726 "POSTBOX2",
2727 "1",
2728 "1",
2729 "12.011",
2730 "C",
2731 "Coordinates of Component 1",
2732 "0.0 0.0 0.0 0 1",
2733 ];
2734 let mut line_it = lines.iter().copied().peekable();
2735 let mut frame = parse_single_frame(&mut line_it).expect("parse should succeed");
2736 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
2737 .expect("should succeed with no velocities");
2738 assert!(!has_vel);
2739 assert_eq!(frame.atom_data[0].velocity, None);
2740 }
2741
2742 #[test]
2743 fn test_parse_line_of_range_f64_exact() {
2744 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0 42", 4, 5, &[0.0; 5]).unwrap();
2745 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 42.0]);
2746 }
2747
2748 #[test]
2749 fn test_byte_scan_stack_parses_realistic_coord_line() {
2750 let line = " 0.63939999999999997 0.90449999999999997 -0.00009999999999977 1 0";
2752 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
2753 let mut buf = [0.0f64; 5];
2754 let n = parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
2755 assert_eq!(n, 5);
2756 assert!((buf[0] - 0.6394).abs() < 1e-4);
2757 assert!((buf[1] - 0.9045).abs() < 1e-4);
2758 assert_eq!(buf[3] as u8, 1);
2759 assert_eq!(buf[4] as u64, 0);
2760 let bad = "1.0 2.0 3.0 1 0 EXTRA";
2762 assert!(parse_line_of_range_f64_stack(bad, 4, 5, &defaults, &mut buf).is_err());
2763 let glued = "1.0 2.0 3.0abc 1 0";
2765 assert!(parse_line_of_range_f64_stack(glued, 4, 5, &defaults, &mut buf).is_err());
2766 }
2767
2768 #[test]
2769 fn test_parse_line_of_range_f64_stack_matches_vec_api() {
2770 let defaults = [0.0, 0.0, 0.0, 0.0, 7.0];
2771 let line = "1.0 2.0 3.0 0.0";
2772 let mut buf = [0.0f64; 5];
2773 parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
2774 let via_vec = parse_line_of_range_f64(line, 4, 5, &defaults).unwrap();
2775 assert_eq!(&buf[..5], via_vec.as_slice());
2776 assert_eq!(buf[4], 7.0);
2777 }
2778
2779 #[test]
2780 fn test_parse_line_of_range_f64_padded() {
2781 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
2782 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0", 4, 5, &defaults).unwrap();
2783 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 99.0]);
2784 }
2785
2786 #[test]
2787 fn test_parse_line_of_range_f64_too_few() {
2788 let result = parse_line_of_range_f64("1.0 2.0 3.0", 4, 5, &[0.0; 5]);
2789 assert!(result.is_err());
2790 }
2791
2792 #[test]
2793 fn test_parse_line_of_range_f64_too_many() {
2794 let result = parse_line_of_range_f64("1.0 2.0 3.0 0.0 5.0 6.0", 4, 5, &[0.0; 5]);
2795 assert!(result.is_err());
2796 }
2797
2798 #[test]
2799 fn test_parse_all_four_column_lines() {
2800 let lines = vec![
2802 "PREBOX1",
2803 "{\"con_spec_version\":2}",
2804 "10.0 10.0 10.0",
2805 "90.0 90.0 90.0",
2806 "POSTBOX1",
2807 "POSTBOX2",
2808 "1",
2809 "3",
2810 "12.011",
2811 "C",
2812 "Coordinates of Component 1",
2813 "0.0 0.0 0.0 0",
2814 "1.0 0.0 0.0 0",
2815 "2.0 0.0 0.0 1",
2816 ];
2817 let mut line_it = lines.iter().copied();
2818 let frame = parse_single_frame(&mut line_it).unwrap();
2819 assert_eq!(frame.atom_data[0].atom_id, 0);
2820 assert_eq!(frame.atom_data[1].atom_id, 1);
2821 assert_eq!(frame.atom_data[2].atom_id, 2);
2822 assert!(frame.atom_data[2].is_fixed());
2823 }
2824}