1use crate::error::ParseError;
2use crate::helpers::symbol_to_atomic_number;
3use crate::types::{
4 AtomDatum, ConFrame, FrameHeader, PreboxHeader, SECTION_CHARGES, SECTION_ENERGIES,
5 SECTION_FORCES, SECTION_MAGMOMS, SECTION_SPINS, SECTION_VELOCITIES,
6 decode_fixed_bitmask, 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}
21
22impl<'a, I> LineStream<'a> for Peekable<I>
23where
24 I: Iterator<Item = &'a str>,
25{
26 #[inline]
27 fn next_line(&mut self) -> Option<&'a str> {
28 Iterator::next(self)
29 }
30 #[inline]
31 fn peek_line(&mut self) -> Option<&'a str> {
32 Peekable::peek(self).copied()
33 }
34}
35
36#[inline]
45pub fn parse_line_of_range_f64_stack(
46 line: &str,
47 min: usize,
48 max: usize,
49 defaults: &[f64],
50 out: &mut [f64; 5],
51) -> Result<usize, ParseError> {
52 debug_assert!(max <= 5 && min <= max && defaults.len() >= max);
53 let bytes = line.as_bytes();
54 let n = bytes.len();
55 let mut i = 0usize;
56 let mut found = 0usize;
57 while found < max {
58 while i < n && bytes[i].is_ascii_whitespace() {
59 i += 1;
60 }
61 if i >= n {
62 break;
63 }
64 let (val, consumed) = fast_float2::parse_partial::<f64, _>(&bytes[i..]).map_err(|_| {
65 let end = bytes[i..]
67 .iter()
68 .position(|b| b.is_ascii_whitespace())
69 .map(|k| i + k)
70 .unwrap_or(n);
71 let token = String::from_utf8_lossy(&bytes[i..end]);
72 ParseError::InvalidNumberFormat(format!("invalid float: {token}"))
73 })?;
74 let next = i + consumed;
75 if next < n && !bytes[next].is_ascii_whitespace() {
77 let end = bytes[i..]
78 .iter()
79 .position(|b| b.is_ascii_whitespace())
80 .map(|k| i + k)
81 .unwrap_or(n);
82 let token = String::from_utf8_lossy(&bytes[i..end]);
83 return Err(ParseError::InvalidNumberFormat(format!(
84 "invalid float: {token}"
85 )));
86 }
87 out[found] = val;
88 found += 1;
89 i = next;
90 }
91 while i < n && bytes[i].is_ascii_whitespace() {
92 i += 1;
93 }
94 if i < n {
96 return Err(ParseError::InvalidVectorLength {
97 expected: max,
98 found: found + 1,
99 });
100 }
101 if found < min || found > max {
102 return Err(ParseError::InvalidVectorLength {
103 expected: max,
104 found,
105 });
106 }
107 while found < max {
108 out[found] = defaults[found];
109 found += 1;
110 }
111 Ok(found)
112}
113
114pub fn parse_line_of_n_f64(line: &str, n: usize) -> Result<Vec<f64>, ParseError> {
126 if n <= 5 {
127 let defaults = [0.0f64; 5];
128 let mut buf = [0.0f64; 5];
129 parse_line_of_range_f64_stack(line, n, n, &defaults, &mut buf)?;
130 return Ok(buf[..n].to_vec());
131 }
132 let mut values = Vec::with_capacity(n);
133 for token in line.split_ascii_whitespace() {
134 let val: f64 = fast_float2::parse(token)
135 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
136 values.push(val);
137 }
138 if values.len() == n {
139 Ok(values)
140 } else {
141 Err(ParseError::InvalidVectorLength {
142 expected: n,
143 found: values.len(),
144 })
145 }
146}
147
148pub fn parse_line_of_range_f64(
155 line: &str,
156 min: usize,
157 max: usize,
158 defaults: &[f64],
159) -> Result<Vec<f64>, ParseError> {
160 if max <= 5 {
161 let mut buf = [0.0f64; 5];
162 parse_line_of_range_f64_stack(line, min, max, defaults, &mut buf)?;
163 return Ok(buf[..max].to_vec());
164 }
165 let mut values = Vec::with_capacity(max);
166 for token in line.split_ascii_whitespace() {
167 let val: f64 = fast_float2::parse(token)
168 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
169 values.push(val);
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 metadata_json_error(message: impl Into<String>) -> ParseError {
185 ParseError::InvalidMetadataJson(message.into())
186}
187
188fn validate_metadata_number(key: &str, value: &Value) -> Result<(), ParseError> {
189 if value.as_f64().is_some() {
190 Ok(())
191 } else {
192 Err(metadata_json_error(format!(
193 "{key} must be a finite number"
194 )))
195 }
196}
197
198fn validate_metadata_integer(key: &str, value: &Value) -> Result<(), ParseError> {
199 if value.as_u64().is_some() {
200 Ok(())
201 } else {
202 Err(metadata_json_error(format!(
203 "{key} must be a non-negative integer"
204 )))
205 }
206}
207
208pub fn validate_metadata_schema(
216 json_obj: &serde_json::Map<String, Value>,
217) -> Result<(), ParseError> {
218 let strict_requested = match json_obj.get(meta::VALIDATE) {
219 Some(Value::Bool(b)) => *b,
220 Some(_) => return Err(metadata_json_error("validate must be a boolean")),
221 None => false,
222 };
223
224 match json_obj.get(meta::SECTIONS) {
225 Some(Value::Array(values)) => {
226 if values.iter().any(|entry| !entry.is_string()) {
227 return Err(metadata_json_error("sections must be an array of strings"));
228 }
229 }
230 Some(_) => return Err(metadata_json_error("sections must be an array of strings")),
231 None if strict_requested => {
232 return Err(metadata_json_error(
233 "validate=true requires a sections array, even when empty",
234 ));
235 }
236 None => {}
237 }
238
239 for (key, value) in json_obj {
240 match key.as_str() {
241 meta::CON_SPEC_VERSION | meta::SECTIONS | meta::VALIDATE => {}
242 meta::ENERGY
243 | meta::TIME
244 | meta::TIMESTEP
245 | meta::CONVERGENCE_FMAX
246 | meta::CONVERGENCE_ENERGY
247 | meta::FMAX => validate_metadata_number(key, value)?,
248 meta::FRAME_INDEX | meta::NEB_BEAD | meta::NEB_BAND => {
249 validate_metadata_integer(key, value)?
250 }
251 meta::GENERATOR if !value.is_string() => {
252 return Err(metadata_json_error("generator must be a string"));
253 }
254 meta::GENERATOR => {}
255 meta::UNITS | meta::POTENTIAL if !value.is_object() => {
256 return Err(metadata_json_error(format!("{key} must be an object")));
257 }
258 meta::POTENTIAL => {
259 if let Some(potential_type) = value.get("type")
260 && !potential_type.is_string()
261 {
262 return Err(metadata_json_error("potential.type must be a string"));
263 }
264 }
265 meta::UNITS => {
266 }
268 meta::PBC => validate_pbc_metadata(value)?,
269 meta::BONDS => validate_bonds_metadata(value)?,
270 meta::LATTICE_VECTORS => validate_lattice_vectors_metadata(value)?,
271 meta::CONVERGED if !value.is_boolean() => {
272 return Err(metadata_json_error("converged must be a boolean"));
273 }
274 meta::CONVERGED => {}
275 _ => {}
276 }
277 }
278
279 Ok(())
280}
281
282fn validate_pbc_metadata(value: &Value) -> Result<(), ParseError> {
283 let Some(values) = value.as_array() else {
284 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
285 };
286 if values.len() != 3 || values.iter().any(|entry| !entry.is_boolean()) {
287 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
288 }
289 Ok(())
290}
291
292fn validate_bonds_metadata(value: &Value) -> Result<(), ParseError> {
299 let Some(items) = value.as_array() else {
300 return Err(metadata_json_error("bonds must be an array"));
301 };
302 for (idx, item) in items.iter().enumerate() {
303 if let Some(pair) = item.as_array() {
304 if pair.len() != 2 {
305 return Err(metadata_json_error(format!(
306 "bonds[{idx}] pair must have exactly two indices"
307 )));
308 }
309 for (k, entry) in pair.iter().enumerate() {
310 let Some(n) = entry.as_u64() else {
311 return Err(metadata_json_error(format!(
312 "bonds[{idx}][{k}] must be a non-negative integer"
313 )));
314 };
315 if n > u32::MAX as u64 {
316 return Err(metadata_json_error(format!(
317 "bonds[{idx}][{k}] index exceeds u32"
318 )));
319 }
320 }
321 continue;
322 }
323 if let Some(obj) = item.as_object() {
324 for key in ["i", "j"] {
325 let Some(entry) = obj.get(key) else {
326 return Err(metadata_json_error(format!(
327 "bonds[{idx}] object must include \"{key}\""
328 )));
329 };
330 let Some(n) = entry.as_u64() else {
331 return Err(metadata_json_error(format!(
332 "bonds[{idx}].{key} must be a non-negative integer"
333 )));
334 };
335 if n > u32::MAX as u64 {
336 return Err(metadata_json_error(format!(
337 "bonds[{idx}].{key} index exceeds u32"
338 )));
339 }
340 }
341 if let Some(order) = obj.get("order")
342 && order.as_i64().is_none()
343 {
344 return Err(metadata_json_error(format!(
345 "bonds[{idx}].order must be an integer when present"
346 )));
347 }
348 continue;
349 }
350 return Err(metadata_json_error(format!(
351 "bonds[{idx}] must be [i, j] or {{\"i\": i, \"j\": j, \"order\"?: ...}}"
352 )));
353 }
354 Ok(())
355}
356
357fn validate_lattice_vectors_metadata(value: &Value) -> Result<(), ParseError> {
358 let Some(rows) = value.as_array() else {
359 return Err(metadata_json_error(
360 "lattice_vectors must be a 3x3 numeric array",
361 ));
362 };
363 if rows.len() != 3 {
364 return Err(metadata_json_error(
365 "lattice_vectors must be a 3x3 numeric array",
366 ));
367 }
368 for row in rows {
369 let Some(entries) = row.as_array() else {
370 return Err(metadata_json_error(
371 "lattice_vectors must be a 3x3 numeric array",
372 ));
373 };
374 if entries.len() != 3 || entries.iter().any(|entry| entry.as_f64().is_none()) {
375 return Err(metadata_json_error(
376 "lattice_vectors must be a 3x3 numeric array",
377 ));
378 }
379 }
380 Ok(())
381}
382
383pub fn parse_line_of_n<T: std::str::FromStr>(line: &str, n: usize) -> Result<Vec<T>, ParseError>
411where
412 ParseError: From<<T as std::str::FromStr>::Err>,
413{
414 let values: Vec<T> = line
415 .split_whitespace()
416 .map(|s| s.parse::<T>())
417 .collect::<Result<_, _>>()?;
418
419 if values.len() == n {
420 Ok(values)
421 } else {
422 Err(ParseError::InvalidVectorLength {
423 expected: n,
424 found: values.len(),
425 })
426 }
427}
428
429pub fn parse_frame_header<'a>(
450 lines: &mut impl Iterator<Item = &'a str>,
451) -> Result<FrameHeader, ParseError> {
452 let prebox1 = lines
453 .next()
454 .ok_or(ParseError::IncompleteHeader)?
455 .to_string();
456 let prebox2_raw = lines.next().ok_or(ParseError::IncompleteHeader)?;
457
458 let trimmed = prebox2_raw.trim();
461 let (spec_version, metadata, sections, validate, sections_declared) = if trimmed.starts_with('{') {
462 let json_val: serde_json::Value = serde_json::from_str(trimmed)
463 .map_err(|e| ParseError::InvalidMetadataJson(e.to_string()))?;
464 let json_obj = json_val
465 .as_object()
466 .ok_or_else(|| ParseError::InvalidMetadataJson("expected a JSON object".to_string()))?;
467 let ver = json_obj
468 .get(meta::CON_SPEC_VERSION)
469 .and_then(|v| v.as_u64())
470 .ok_or(ParseError::MissingSpecVersion)? as u32;
471 if ver > crate::CON_SPEC_VERSION {
472 return Err(ParseError::UnsupportedSpecVersion(ver));
473 }
474 if ver >= 3 {
475 match json_obj.get(meta::UNITS) {
476 Some(u) => crate::units::validate_v3_units_metadata(u).map_err(|e| {
477 ParseError::ValidationError(format!("v3 units: {e}"))
478 })?,
479 None => {
480 return Err(ParseError::ValidationError(
481 "con_spec_version >= 3 requires metadata \"units\" with length and energy"
482 .into(),
483 ));
484 }
485 }
486 }
487
488 let mut sections: Vec<String> = Vec::new();
492 let mut metadata = BTreeMap::new();
493 let mut sections_declared = false;
494 let mut validate = false;
495 for (k, v) in json_obj {
496 match k.as_str() {
497 meta::CON_SPEC_VERSION => {}
498 meta::SECTIONS => {
499 sections_declared = true;
500 let arr = v.as_array().ok_or_else(|| {
501 metadata_json_error("sections must be an array of strings")
502 })?;
503 sections.reserve(arr.len());
504 for entry in arr {
505 let s = entry.as_str().ok_or_else(|| {
506 metadata_json_error("sections must be an array of strings")
507 })?;
508 sections.push(s.to_string());
509 }
510 }
511 meta::VALIDATE => {
512 validate = match v {
513 Value::Bool(b) => *b,
514 _ => return Err(metadata_json_error("validate must be a boolean")),
515 };
516 metadata.insert(k.clone(), v.clone());
517 }
518 _ => {
519 metadata.insert(k.clone(), v.clone());
520 }
521 }
522 }
523
524 if validate {
527 validate_metadata_schema(json_obj)?;
528 }
529
530 (ver, metadata, sections, validate, sections_declared)
531 } else {
532 (1_u32, BTreeMap::new(), Vec::new(), false, false)
534 };
535 let prebox2 = prebox2_raw.to_string();
536
537 let boxl_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
538 let angles_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
539 let postbox1 = lines
540 .next()
541 .ok_or(ParseError::IncompleteHeader)?
542 .to_string();
543 let postbox2 = lines
544 .next()
545 .ok_or(ParseError::IncompleteHeader)?
546 .to_string();
547 let natm_types =
548 parse_line_of_n::<usize>(lines.next().ok_or(ParseError::IncompleteHeader)?, 1)?[0];
549 let natms_per_type = parse_line_of_n::<usize>(
550 lines.next().ok_or(ParseError::IncompleteHeader)?,
551 natm_types,
552 )?;
553 let masses_per_type = parse_line_of_n_f64(
554 lines.next().ok_or(ParseError::IncompleteHeader)?,
555 natm_types,
556 )?;
557 if validate {
558 validate_header_geometry(&boxl_vec, &angles_vec, natm_types, &natms_per_type)?;
559 validate_masses(&masses_per_type)?;
560 }
561 Ok(FrameHeader {
562 prebox_header: PreboxHeader {
563 user: prebox1,
564 metadata_line: prebox2,
565 },
566 boxl: boxl_vec.try_into().unwrap(),
567 angles: angles_vec.try_into().unwrap(),
568 postbox_header: [postbox1, postbox2],
569 natm_types,
570 natms_per_type,
571 masses_per_type,
572 spec_version,
573 metadata,
574 sections,
575 strict_validation: validate,
576 sections_declared,
577 })
578}
579
580pub fn parse_single_frame<'a>(
629 lines: &mut impl Iterator<Item = &'a str>,
630) -> Result<ConFrame, ParseError> {
631 let header = parse_frame_header(lines)?;
632 let validate = header.strict_validation;
633 let total_atoms: usize = header.natms_per_type.iter().sum();
634 let mut atom_data = Vec::with_capacity(total_atoms);
635 use crate::storage_dtype::{ElementKind, FloatArray2, StorageDtypes};
638 let dt = StorageDtypes::from_metadata(&header.metadata).unwrap_or_default();
639 let f64_positions = dt.positions == ElementKind::Float64;
640 let mut pos_flat = if f64_positions {
641 vec![0.0f64; total_atoms.saturating_mul(3)]
642 } else {
643 Vec::new()
644 };
645 let mut positions_other = if f64_positions {
646 None
647 } else {
648 Some(FloatArray2::zeros(dt.positions, total_atoms, 3))
649 };
650
651 let mut global_atom_idx: u64 = 0;
652 let mut atom_i = 0usize;
653 for (type_idx, num_atoms) in header.natms_per_type.iter().enumerate() {
654 let symbol_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
658 let symbol: Arc<str> = Arc::from(symbol_line.trim());
659 let coord_label = lines.next().ok_or(ParseError::IncompleteFrame)?;
660 if validate {
661 validate_coordinate_component(type_idx, symbol.as_ref(), coord_label)?;
662 }
663 for _ in 0..*num_atoms {
664 let coord_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
665 let defaults = [0.0, 0.0, 0.0, 0.0, global_atom_idx as f64];
667 let mut vals = [0.0f64; 5];
668 parse_line_of_range_f64_stack(coord_line, 4, 5, &defaults, &mut vals)?;
669 let (fixed, atom_id) = if validate {
670 parse_identity_columns(coord_line, "coordinate", 3, 4, 5)?
671 } else {
672 (decode_fixed_bitmask(vals[3] as u8), vals[4] as u64)
673 };
674 let xyz = [vals[0], vals[1], vals[2]];
675 if f64_positions {
676 let o = atom_i * 3;
677 pos_flat[o] = xyz[0];
678 pos_flat[o + 1] = xyz[1];
679 pos_flat[o + 2] = xyz[2];
680 } else if let Some(ref mut pos) = positions_other {
681 pos.set_f64_row(atom_i, xyz);
682 }
683 atom_data.push(AtomDatum {
684 symbol: Arc::clone(&symbol),
686 x: xyz[0],
687 y: xyz[1],
688 z: xyz[2],
689 fixed,
690 atom_id,
691 velocity: None,
692 force: None,
693 energy: None,
694 charge: None,
695 spin: None,
696 magmom: None,
697 });
698 global_atom_idx += 1;
699 atom_i += 1;
700 }
701 }
702 let positions = if f64_positions {
703 FloatArray2::from_f64_row_major(total_atoms, 3, pos_flat)
704 } else {
705 positions_other.expect("non-f64 positions allocated")
706 };
707 Ok(crate::types::con_frame_from_atom_data_with_positions(
709 header, atom_data, positions,
710 ))
711}
712
713fn validate_header_geometry(
714 boxl: &[f64],
715 angles: &[f64],
716 natm_types: usize,
717 natms_per_type: &[usize],
718) -> Result<(), ParseError> {
719 if boxl.iter().any(|length| !length.is_finite() || *length <= 0.0)
720 || angles
721 .iter()
722 .any(|angle| !angle.is_finite() || *angle <= 0.0 || *angle >= 180.0)
723 {
724 return Err(ParseError::ValidationError(
725 "cell geometry must have positive lengths and angles between 0 and 180 degrees"
726 .to_string(),
727 ));
728 }
729 if natm_types == 0 || natms_per_type.contains(&0) {
730 return Err(ParseError::ValidationError(
731 "atom counts must contain at least one atom per component".to_string(),
732 ));
733 }
734 Ok(())
735}
736
737fn validate_masses(masses_per_type: &[f64]) -> Result<(), ParseError> {
738 if masses_per_type
739 .iter()
740 .any(|mass| !mass.is_finite() || *mass <= 0.0)
741 {
742 return Err(ParseError::ValidationError(
743 "component masses must be positive".to_string(),
744 ));
745 }
746 Ok(())
747}
748
749fn validate_coordinate_component(
750 type_idx: usize,
751 symbol: &str,
752 label: &str,
753) -> Result<(), ParseError> {
754 let expected_label = format!("Coordinates of Component {}", type_idx + 1);
755 if label.trim() != expected_label {
756 return Err(ParseError::ValidationError(format!(
757 "expected coordinate label {expected_label:?}, found {label:?}"
758 )));
759 }
760 if symbol != "X" && symbol_to_atomic_number(symbol) == 0 {
761 return Err(ParseError::ValidationError(format!(
762 "unknown component symbol {symbol}"
763 )));
764 }
765 Ok(())
766}
767
768fn parse_identity_columns(
783 line: &str,
784 row_kind: &str,
785 fixed_idx: usize,
786 atom_id_idx: usize,
787 n_cols: usize,
788) -> Result<([bool; 3], u64), ParseError> {
789 let columns = line.split_ascii_whitespace().collect::<Vec<_>>();
790 if columns.len() != n_cols {
791 return Err(ParseError::ValidationError(format!(
792 "{row_kind} rows require {n_cols} columns including fixed_flag and atom_id in validate mode"
793 )));
794 }
795 let fixed_flag = columns[fixed_idx].parse::<u8>().map_err(|_| {
796 ParseError::ValidationError(format!("{row_kind} fixed_flag must be an integer bitmask"))
797 })?;
798 if fixed_flag > 7 {
799 return Err(ParseError::ValidationError(format!(
800 "{row_kind} fixed_flag must be between 0 and 7"
801 )));
802 }
803 let atom_id = columns[atom_id_idx].parse::<u64>().map_err(|_| {
804 ParseError::ValidationError(format!("{row_kind} atom_id must be an integer"))
805 })?;
806 Ok((decode_fixed_bitmask(fixed_flag), atom_id))
807}
808
809
810fn validate_section_component(
811 section: &str,
812 type_idx: usize,
813 atom_idx: usize,
814 symbol: &str,
815 label: &str,
816 header: &FrameHeader,
817 atom_data: &[AtomDatum],
818) -> Result<(), ParseError> {
819 let expected_label = format!("{section} of Component {}", type_idx + 1);
820 if label.trim() != expected_label {
821 return Err(ParseError::ValidationError(format!(
822 "expected section label {expected_label:?}, found {label:?}"
823 )));
824 }
825
826 if header.natms_per_type[type_idx] == 0 {
827 return Ok(());
828 }
829
830 let expected_symbol = atom_data
831 .get(atom_idx)
832 .map(|atom| atom.symbol.as_ref())
833 .ok_or_else(|| {
834 ParseError::ValidationError(format!(
835 "{section} component {} has no coordinate atom to validate against",
836 type_idx + 1
837 ))
838 })?;
839 if symbol != expected_symbol {
840 return Err(ParseError::ValidationError(format!(
841 "{section} component {} symbol mismatch: expected {expected_symbol}, found {symbol}",
842 type_idx + 1
843 )));
844 }
845
846 Ok(())
847}
848
849fn validate_section_atom_identity(
850 section: &str,
851 atom_idx: usize,
852 fixed: [bool; 3],
853 atom_id: u64,
854 atom_data: &[AtomDatum],
855) -> Result<(), ParseError> {
856 let atom = atom_data.get(atom_idx).ok_or_else(|| {
857 ParseError::ValidationError(format!(
858 "{section} row {atom_idx} has no coordinate atom to validate against"
859 ))
860 })?;
861
862 if atom.fixed != fixed {
863 return Err(ParseError::ValidationError(format!(
864 "{section} row {atom_idx} fixed mask mismatch for atom_id {}",
865 atom.atom_id
866 )));
867 }
868 if atom.atom_id != atom_id {
869 return Err(ParseError::ValidationError(format!(
870 "{section} row {atom_idx} atom_id mismatch: expected {}, found {atom_id}",
871 atom.atom_id
872 )));
873 }
874
875 Ok(())
876}
877
878pub fn parse_velocity_section<'a>(
891 lines: &mut impl LineStream<'a>,
892 header: &FrameHeader,
893 atom_data: &mut [AtomDatum],
894) -> Result<bool, ParseError> {
895 let validate = header.strict_validation;
896 match lines.peek_line() {
898 Some(line) if line.trim().is_empty() => {
899 lines.next_line();
901 }
902 _ => return Ok(false),
903 }
904
905 let mut atom_idx: usize = 0;
906 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
907 let symbol = lines
909 .next_line()
910 .ok_or(ParseError::IncompleteVelocitySection)?
911 .trim();
912
913 let comp_line = lines.next_line().ok_or(ParseError::IncompleteVelocitySection)?;
915 if !comp_line.contains("Velocities of Component") {
917 return Err(ParseError::IncompleteVelocitySection);
918 }
919 if validate {
920 validate_section_component(
921 "Velocities",
922 type_idx,
923 atom_idx,
924 symbol,
925 comp_line,
926 header,
927 atom_data,
928 )?;
929 }
930
931 for _ in 0..num_atoms {
932 let vel_line = lines.next_line().ok_or(ParseError::IncompleteVelocitySection)?;
933 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
935 let mut vals = [0.0f64; 5];
936 parse_line_of_range_f64_stack(vel_line, 4, 5, &defaults, &mut vals)?;
937 if validate {
938 let (fixed, atom_id) =
939 parse_identity_columns(vel_line, "velocities", 3, 4, 5)?;
940 validate_section_atom_identity("velocities", atom_idx, fixed, atom_id, atom_data)?;
941 }
942 if atom_idx < atom_data.len() {
943 atom_data[atom_idx].velocity = Some([vals[0], vals[1], vals[2]]);
944 }
945 atom_idx += 1;
946 }
947 }
948
949 Ok(true)
950}
951
952pub fn parse_force_section<'a>(
960 lines: &mut impl LineStream<'a>,
961 header: &FrameHeader,
962 atom_data: &mut [AtomDatum],
963) -> Result<bool, ParseError> {
964 let validate = header.strict_validation;
965 match lines.peek_line() {
967 Some(line) if line.trim().is_empty() => {
968 lines.next_line();
969 }
970 _ => return Ok(false),
971 }
972
973 let mut atom_idx: usize = 0;
974 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
975 let symbol = lines
976 .next_line()
977 .ok_or(ParseError::IncompleteForceSection)?
978 .trim();
979
980 let comp_line = lines.next_line().ok_or(ParseError::IncompleteForceSection)?;
981 if !comp_line.contains("Forces of Component") {
982 return Err(ParseError::IncompleteForceSection);
983 }
984 if validate {
985 validate_section_component(
986 "Forces", type_idx, atom_idx, symbol, comp_line, header, atom_data,
987 )?;
988 }
989
990 for _ in 0..num_atoms {
991 let force_line = lines.next_line().ok_or(ParseError::IncompleteForceSection)?;
992 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
993 let mut vals = [0.0f64; 5];
994 parse_line_of_range_f64_stack(force_line, 4, 5, &defaults, &mut vals)?;
995 if validate {
996 let (fixed, atom_id) =
997 parse_identity_columns(force_line, "forces", 3, 4, 5)?;
998 validate_section_atom_identity("forces", atom_idx, fixed, atom_id, atom_data)?;
999 }
1000 if atom_idx < atom_data.len() {
1001 atom_data[atom_idx].force = Some([vals[0], vals[1], vals[2]]);
1002 }
1003 atom_idx += 1;
1004 }
1005 }
1006
1007 Ok(true)
1008}
1009
1010pub fn parse_energy_section<'a>(
1023 lines: &mut impl LineStream<'a>,
1024 header: &FrameHeader,
1025 atom_data: &mut [AtomDatum],
1026) -> Result<bool, ParseError> {
1027 let validate = header.strict_validation;
1028 match lines.peek_line() {
1029 Some(line) if line.trim().is_empty() => {
1030 lines.next_line();
1031 }
1032 _ => return Ok(false),
1033 }
1034
1035 let mut atom_idx: usize = 0;
1036 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1037 let symbol = lines
1038 .next_line()
1039 .ok_or(ParseError::IncompleteEnergySection)?
1040 .trim();
1041
1042 let comp_line = lines.next_line().ok_or(ParseError::IncompleteEnergySection)?;
1043 if !comp_line.contains("Energies of Component") {
1044 return Err(ParseError::IncompleteEnergySection);
1045 }
1046 if validate {
1047 validate_section_component(
1048 "Energies", type_idx, atom_idx, symbol, comp_line, header, atom_data,
1049 )?;
1050 }
1051
1052 for _ in 0..num_atoms {
1053 let energy_line = lines.next_line().ok_or(ParseError::IncompleteEnergySection)?;
1054 let defaults = [0.0, 0.0, atom_idx as f64];
1057 let vals = parse_line_of_range_f64(energy_line, 1, 3, &defaults)?;
1058 if validate {
1059 let (fixed, atom_id) =
1060 parse_identity_columns(energy_line, "energies", 1, 2, 3)?;
1061 validate_section_atom_identity("energies", atom_idx, fixed, atom_id, atom_data)?;
1062 }
1063 if atom_idx < atom_data.len() {
1064 atom_data[atom_idx].energy = Some(vals[0]);
1065 }
1066 atom_idx += 1;
1067 }
1068 }
1069
1070 Ok(true)
1071}
1072
1073pub fn parse_declared_sections<'a>(
1079 lines: &mut impl LineStream<'a>,
1080 header: &mut FrameHeader,
1081 atom_data: &mut [AtomDatum],
1082) -> Result<usize, ParseError> {
1083 let mut applied = 0usize;
1084 if !header.sections_declared && header.sections.is_empty() {
1085 let found = parse_velocity_section(lines, header, atom_data)?;
1087 if found {
1088 header.sections.push(SECTION_VELOCITIES.into());
1089 applied = 1;
1090 }
1091 } else {
1092 let sections = std::mem::take(&mut header.sections);
1093 for section in §ions {
1094 match section.as_str() {
1095 SECTION_VELOCITIES => {
1096 let found = parse_velocity_section(lines, header, atom_data)?;
1097 if !found {
1098 return Err(ParseError::IncompleteVelocitySection);
1099 }
1100 applied += 1;
1101 }
1102 SECTION_FORCES => {
1103 let found = parse_force_section(lines, header, atom_data)?;
1104 if !found {
1105 return Err(ParseError::IncompleteForceSection);
1106 }
1107 applied += 1;
1108 }
1109 SECTION_ENERGIES => {
1110 let found = parse_energy_section(lines, header, atom_data)?;
1111 if !found {
1112 return Err(ParseError::IncompleteEnergySection);
1113 }
1114 applied += 1;
1115 }
1116 SECTION_CHARGES => {
1117 let found = parse_charge_section(lines, header, atom_data)?;
1118 if !found {
1119 return Err(ParseError::IncompleteSection(SECTION_CHARGES.into()));
1120 }
1121 applied += 1;
1122 }
1123 SECTION_SPINS => {
1124 let found = parse_spin_section(lines, header, atom_data)?;
1125 if !found {
1126 return Err(ParseError::IncompleteSection(SECTION_SPINS.into()));
1127 }
1128 applied += 1;
1129 }
1130 SECTION_MAGMOMS => {
1131 let found = parse_magmom_section(lines, header, atom_data)?;
1132 if !found {
1133 return Err(ParseError::IncompleteSection(SECTION_MAGMOMS.into()));
1134 }
1135 applied += 1;
1136 }
1137 other => return Err(ParseError::UnknownSection(other.to_string())),
1138 }
1139 }
1140 header.sections = sections;
1141 }
1142 Ok(applied)
1143}
1144
1145fn parse_scalar_atom_section<'a>(
1148 lines: &mut impl LineStream<'a>,
1149 header: &FrameHeader,
1150 atom_data: &mut [AtomDatum],
1151 section_name: &str,
1152 component_label: &str,
1153 mut set_value: impl FnMut(&mut AtomDatum, f64),
1154) -> Result<bool, ParseError> {
1155 let validate = header.strict_validation;
1156 match lines.peek_line() {
1157 Some(line) if line.trim().is_empty() => {
1158 lines.next_line();
1159 }
1160 _ => return Ok(false),
1161 }
1162
1163 let mut atom_idx: usize = 0;
1164 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1165 let symbol = lines
1166 .next_line()
1167 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?
1168 .trim();
1169
1170 let comp_line = lines
1171 .next_line()
1172 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
1173 if !comp_line.contains(component_label) {
1174 return Err(ParseError::IncompleteSection(section_name.into()));
1175 }
1176 if validate {
1177 validate_section_component(
1178 component_label.trim_end_matches(" of Component").trim(),
1179 type_idx,
1180 atom_idx,
1181 symbol,
1182 comp_line,
1183 header,
1184 atom_data,
1185 )?;
1186 }
1187
1188 for _ in 0..num_atoms {
1189 let data_line = lines
1190 .next_line()
1191 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
1192 let defaults = [0.0, 0.0, atom_idx as f64];
1193 let vals = parse_line_of_range_f64(data_line, 1, 3, &defaults)?;
1194 if validate {
1195 let (fixed, atom_id) =
1196 parse_identity_columns(data_line, section_name, 1, 2, 3)?;
1197 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
1198 }
1199 if atom_idx < atom_data.len() {
1200 set_value(&mut atom_data[atom_idx], vals[0]);
1201 }
1202 atom_idx += 1;
1203 }
1204 }
1205 Ok(true)
1206}
1207
1208pub fn parse_charge_section<'a>(
1209 lines: &mut impl LineStream<'a>,
1210 header: &FrameHeader,
1211 atom_data: &mut [AtomDatum],
1212) -> Result<bool, ParseError> {
1213 parse_scalar_atom_section(
1214 lines,
1215 header,
1216 atom_data,
1217 SECTION_CHARGES,
1218 "Charges of Component",
1219 |a, v| a.charge = Some(v),
1220 )
1221}
1222
1223pub fn parse_spin_section<'a>(
1224 lines: &mut impl LineStream<'a>,
1225 header: &FrameHeader,
1226 atom_data: &mut [AtomDatum],
1227) -> Result<bool, ParseError> {
1228 parse_scalar_atom_section(
1229 lines,
1230 header,
1231 atom_data,
1232 SECTION_SPINS,
1233 "Spins of Component",
1234 |a, v| a.spin = Some(v),
1235 )
1236}
1237
1238pub fn parse_magmom_section<'a>(
1240 lines: &mut impl LineStream<'a>,
1241 header: &FrameHeader,
1242 atom_data: &mut [AtomDatum],
1243) -> Result<bool, ParseError> {
1244 let validate = header.strict_validation;
1245 match lines.peek_line() {
1246 Some(line) if line.trim().is_empty() => {
1247 lines.next_line();
1248 }
1249 _ => return Ok(false),
1250 }
1251
1252 let mut atom_idx: usize = 0;
1253 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1254 let symbol = lines
1255 .next_line()
1256 .ok_or_else(|| ParseError::IncompleteSection(SECTION_MAGMOMS.into()))?
1257 .trim();
1258
1259 let comp_line = lines
1260 .next_line()
1261 .ok_or_else(|| ParseError::IncompleteSection(SECTION_MAGMOMS.into()))?;
1262 if !comp_line.contains("Magmoms of Component") {
1263 return Err(ParseError::IncompleteSection(SECTION_MAGMOMS.into()));
1264 }
1265 if validate {
1266 validate_section_component(
1267 "Magmoms",
1268 type_idx,
1269 atom_idx,
1270 symbol,
1271 comp_line,
1272 header,
1273 atom_data,
1274 )?;
1275 }
1276
1277 for _ in 0..num_atoms {
1278 let mm_line = lines
1279 .next_line()
1280 .ok_or_else(|| ParseError::IncompleteSection(SECTION_MAGMOMS.into()))?;
1281 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
1282 let mut vals = [0.0f64; 5];
1283 parse_line_of_range_f64_stack(mm_line, 4, 5, &defaults, &mut vals)?;
1284 if validate {
1285 let (fixed, atom_id) =
1286 parse_identity_columns(mm_line, SECTION_MAGMOMS, 3, 4, 5)?;
1287 validate_section_atom_identity(
1288 SECTION_MAGMOMS,
1289 atom_idx,
1290 fixed,
1291 atom_id,
1292 atom_data,
1293 )?;
1294 }
1295 if atom_idx < atom_data.len() {
1296 atom_data[atom_idx].magmom = Some([vals[0], vals[1], vals[2]]);
1297 }
1298 atom_idx += 1;
1299 }
1300 }
1301 Ok(true)
1302}
1303
1304#[cfg(test)]
1305mod tests {
1306 use super::*;
1307 use crate::iterators::ConFrameIterator;
1308
1309 #[test]
1310 fn test_parse_line_of_n_success() {
1311 let line = "1.0 2.5 -3.0";
1312 let values = parse_line_of_n::<f64>(line, 3).unwrap();
1313 assert_eq!(values, vec![1.0, 2.5, -3.0]);
1314 }
1315
1316 #[test]
1317 fn test_parse_line_of_n_too_short() {
1318 let line = "1.0 2.5";
1319 let result = parse_line_of_n::<f64>(line, 3);
1320 assert!(result.is_err());
1321 assert!(matches!(
1322 result.unwrap_err(),
1323 ParseError::InvalidVectorLength {
1324 expected: 3,
1325 found: 2
1326 }
1327 ));
1328 }
1329
1330 #[test]
1331 fn test_parse_line_of_n_too_long() {
1332 let line = "1.0 2.5 -3.0 4.0";
1333 let result = parse_line_of_n::<f64>(line, 3);
1334 assert!(result.is_err());
1335 assert!(matches!(
1336 result.unwrap_err(),
1337 ParseError::InvalidVectorLength {
1338 expected: 3,
1339 found: 4
1340 }
1341 ));
1342 }
1343
1344 #[test]
1345 fn test_parse_line_of_n_invalid_float() {
1346 let line = "1.0 abc -3.0";
1347 let result = parse_line_of_n::<f64>(line, 3);
1348 assert!(result.is_err());
1349 assert!(matches!(
1350 result.unwrap_err(),
1351 ParseError::InvalidNumberFormat(_)
1352 ));
1353 }
1354
1355 #[test]
1356 fn test_parse_frame_header_success() {
1357 let lines = [
1358 "PREBOX1",
1359 "{\"con_spec_version\":2}",
1360 "10.0 20.0 30.0",
1361 "90.0 90.0 90.0",
1362 "POSTBOX1",
1363 "POSTBOX2",
1364 "2",
1365 "1 1",
1366 "12.011 1.008",
1367 ];
1368 let mut line_it = lines.iter().copied();
1369 match parse_frame_header(&mut line_it) {
1370 Ok(header) => {
1371 assert_eq!(header.prebox_header.user, "PREBOX1");
1372 assert_eq!(header.spec_version, 2);
1373 assert_eq!(header.boxl, [10.0, 20.0, 30.0]);
1374 assert_eq!(header.angles, [90.0, 90.0, 90.0]);
1375 assert_eq!(header.postbox_header, ["POSTBOX1", "POSTBOX2"]);
1376 assert_eq!(header.natm_types, 2);
1377 assert_eq!(header.natms_per_type, vec![1, 1]);
1378 assert_eq!(header.masses_per_type, vec![12.011, 1.008]);
1379 }
1380 Err(e) => {
1381 panic!(
1382 "Parsing failed when it should have succeeded. Error: {:?}",
1383 e
1384 );
1385 }
1386 }
1387 }
1388
1389 #[test]
1390 fn test_parse_frame_header_missing_line() {
1391 let lines = [
1392 "PREBOX1",
1393 "{\"con_spec_version\":2}",
1394 "10.0 20.0 30.0",
1395 "90.0 90.0 90.0",
1396 "POSTBOX1",
1397 "POSTBOX2",
1398 "2",
1399 "1 1",
1400 ];
1402 let mut line_it = lines.iter().copied();
1403 let result = parse_frame_header(&mut line_it);
1404 assert!(result.is_err());
1405 assert!(matches!(result.unwrap_err(), ParseError::IncompleteHeader));
1406 }
1407
1408 #[test]
1409 fn test_parse_frame_header_missing_spec_version() {
1410 let lines = vec![
1411 "PREBOX1",
1412 "{}",
1413 "10.0 20.0 30.0",
1414 "90.0 90.0 90.0",
1415 "POSTBOX1",
1416 "POSTBOX2",
1417 "2",
1418 "1 1",
1419 "12.011 1.008",
1420 ];
1421 let mut line_it = lines.iter().copied();
1422 let result = parse_frame_header(&mut line_it);
1423 assert!(result.is_err());
1424 assert!(matches!(
1425 result.unwrap_err(),
1426 ParseError::MissingSpecVersion
1427 ));
1428 }
1429
1430 #[test]
1431 fn test_parse_frame_header_legacy_no_json() {
1432 let lines = vec![
1434 "PREBOX1",
1435 "0.0000 TIME",
1436 "10.0 20.0 30.0",
1437 "90.0 90.0 90.0",
1438 "POSTBOX1",
1439 "POSTBOX2",
1440 "2",
1441 "1 1",
1442 "12.011 1.008",
1443 ];
1444 let mut line_it = lines.iter().copied();
1445 let header = parse_frame_header(&mut line_it).unwrap();
1446 assert_eq!(header.spec_version, 1);
1447 assert!(header.metadata.is_empty());
1448 }
1449
1450 #[test]
1451 fn test_parse_frame_header_malformed_json() {
1452 let lines = vec![
1454 "PREBOX1",
1455 "{broken json",
1456 "10.0 20.0 30.0",
1457 "90.0 90.0 90.0",
1458 "POSTBOX1",
1459 "POSTBOX2",
1460 "2",
1461 "1 1",
1462 "12.011 1.008",
1463 ];
1464 let mut line_it = lines.iter().copied();
1465 let result = parse_frame_header(&mut line_it);
1466 assert!(result.is_err());
1467 assert!(matches!(
1468 result.unwrap_err(),
1469 ParseError::InvalidMetadataJson(_)
1470 ));
1471 }
1472
1473 #[test]
1474 fn test_parse_frame_header_unsupported_version() {
1475 let lines = vec![
1476 "PREBOX1",
1477 "{\"con_spec_version\":999}",
1478 "10.0 20.0 30.0",
1479 "90.0 90.0 90.0",
1480 "POSTBOX1",
1481 "POSTBOX2",
1482 "2",
1483 "1 1",
1484 "12.011 1.008",
1485 ];
1486 let mut line_it = lines.iter().copied();
1487 let result = parse_frame_header(&mut line_it);
1488 assert!(result.is_err());
1489 assert!(matches!(
1490 result.unwrap_err(),
1491 ParseError::UnsupportedSpecVersion(999)
1492 ));
1493 }
1494
1495 #[test]
1496 fn test_v3_missing_units_rejected() {
1497 let lines = vec![
1498 "PREBOX1",
1499 "{\"con_spec_version\":3}",
1500 "10.0 20.0 30.0",
1501 "90.0 90.0 90.0",
1502 "POSTBOX1",
1503 "POSTBOX2",
1504 "1",
1505 "1",
1506 "1.0",
1507 ];
1508 let mut line_it = lines.iter().copied();
1509 let err = parse_frame_header(&mut line_it).unwrap_err();
1510 let msg = err.to_string();
1511 assert!(
1512 msg.contains("units") || msg.contains("v3"),
1513 "expected units error, got {msg}"
1514 );
1515 }
1516
1517 #[test]
1518 fn test_v3_invalid_units_rejected() {
1519 let lines = vec![
1520 "PREBOX1",
1521 r#"{"con_spec_version":3,"units":{"length":"eV","energy":"angstrom"}}"#,
1522 "10.0 20.0 30.0",
1523 "90.0 90.0 90.0",
1524 "POSTBOX1",
1525 "POSTBOX2",
1526 "1",
1527 "1",
1528 "1.0",
1529 ];
1530 let mut line_it = lines.iter().copied();
1531 assert!(parse_frame_header(&mut line_it).is_err());
1532 }
1533
1534 #[test]
1535 fn test_v3_valid_units_exposes_length_energy() {
1536 let lines = vec![
1537 "PREBOX1",
1538 r#"{"con_spec_version":3,"units":{"length":"angstrom","energy":"eV","mass":"amu","time":"fs"}}"#,
1539 "10.0 20.0 30.0",
1540 "90.0 90.0 90.0",
1541 "POSTBOX1",
1542 "POSTBOX2",
1543 "1",
1544 "1",
1545 "1.0",
1546 ];
1547 let mut line_it = lines.iter().copied();
1548 let header = parse_frame_header(&mut line_it).unwrap();
1549 assert_eq!(header.spec_version, 3);
1550 assert_eq!(header.length_unit(), Some("angstrom"));
1551 assert_eq!(header.energy_unit(), Some("eV"));
1552 let f = header.conversion_factor_to("length", "nm").unwrap();
1553 assert!((f - 0.1).abs() < 1e-12);
1554 }
1555
1556 #[test]
1557 fn test_parse_frame_header_extra_metadata_preserved() {
1558 let lines = vec![
1559 "PREBOX1",
1560 "{\"con_spec_version\":2,\"generator\":\"test\"}",
1561 "10.0 20.0 30.0",
1562 "90.0 90.0 90.0",
1563 "POSTBOX1",
1564 "POSTBOX2",
1565 "2",
1566 "1 1",
1567 "12.011 1.008",
1568 ];
1569 let mut line_it = lines.iter().copied();
1570 let header = parse_frame_header(&mut line_it).unwrap();
1571 assert_eq!(header.spec_version, 2);
1572 assert_eq!(
1573 header.metadata.get("generator"),
1574 Some(&serde_json::Value::String("test".to_string()))
1575 );
1576 }
1577
1578 #[test]
1579 fn test_parse_frame_header_invalid_natms_per_type() {
1580 let lines = vec![
1581 "PREBOX1",
1582 "{\"con_spec_version\":2}",
1583 "10.0 20.0 30.0",
1584 "90.0 90.0 90.0",
1585 "POSTBOX1",
1586 "POSTBOX2",
1587 "2",
1588 "1 1 1", "12.011 1.008",
1590 ];
1591 let mut line_it = lines.iter().copied();
1592 let result = parse_frame_header(&mut line_it);
1593 assert!(result.is_err());
1594 assert!(matches!(
1595 result.unwrap_err(),
1596 ParseError::InvalidVectorLength {
1597 expected: 2,
1598 found: 3
1599 }
1600 ));
1601 }
1602
1603 #[test]
1604 fn test_parse_single_frame_success() {
1605 let lines = vec![
1606 "PREBOX1",
1607 "{\"con_spec_version\":2}",
1608 "10.0 20.0 30.0",
1609 "90.0 90.0 90.0",
1610 "POSTBOX1",
1611 "POSTBOX2",
1612 "2",
1613 "3 3",
1614 "12.011 1.008",
1615 "1",
1616 "Coordinates of Component 1",
1617 "0.0 0.0 0.0 0.0 1",
1618 "1.0940 0.0 0.0 0.0 2",
1619 "-0.5470 0.9499 0.0 0.0 3",
1620 "2",
1621 "Coordinates of Component 2",
1622 "5.0 5.0 5.0 0.0 4",
1623 "6.0940 5.0 5.0 0.0 5",
1624 "5.5470 5.9499 5.0 0.0 6",
1625 ];
1626 let mut line_it = lines.iter().copied();
1627 let frame = parse_single_frame(&mut line_it).unwrap();
1628
1629 assert_eq!(frame.header.natm_types, 2);
1630 assert_eq!(frame.header.natms_per_type, vec![3, 3]);
1631 assert_eq!(frame.header.masses_per_type, vec![12.011, 1.008]);
1632 assert_eq!(frame.atom_data.len(), 6);
1633 assert_eq!(&*frame.atom_data[0].symbol, "1");
1634 assert_eq!(frame.atom_data[0].atom_id, 1);
1635 assert_eq!(&*frame.atom_data[5].symbol, "2");
1636 assert_eq!(frame.atom_data[5].atom_id, 6);
1637 }
1638
1639 #[test]
1640 fn test_parse_single_frame_missing_line() {
1641 let lines = vec![
1643 "PREBOX1",
1644 "{\"con_spec_version\":2}",
1645 "10.0 20.0 30.0",
1646 "90.0 90.0 90.0",
1647 "POSTBOX1",
1648 "POSTBOX2",
1649 "2",
1650 "3 3",
1651 "12.011 1.008",
1652 "1",
1653 "Coordinates of Component 1",
1654 "0.0 0.0 0.0 0.0 1",
1655 "1.0940 0.0 0.0 0.0 2",
1656 "-0.5470 0.9499 0.0 0.0 3",
1657 ];
1659 let mut line_it = lines.iter().copied();
1660 let result = parse_single_frame(&mut line_it);
1661 assert!(result.is_err());
1662 assert!(matches!(result.unwrap_err(), ParseError::IncompleteFrame));
1663 }
1664
1665 #[test]
1666 fn test_parse_single_frame_missing_atom_index_defaults_sequential() {
1667 let lines = vec![
1669 "PREBOX1",
1670 "{\"con_spec_version\":2}",
1671 "10.0 20.0 30.0",
1672 "90.0 90.0 90.0",
1673 "POSTBOX1",
1674 "POSTBOX2",
1675 "2",
1676 "3 3",
1677 "12.011 1.008",
1678 "1",
1679 "Coordinates of Component 1",
1680 "0.0 0.0 0.0 0.0 1",
1681 "1.0940 0.0 0.0 0.0 2",
1682 "-0.5470 0.9499 0.0 0.0 3",
1683 "2",
1684 "Coordinates of Component 2",
1685 "5.0 5.0 5.0 0.0", "6.0940 5.0 5.0 0.0 10", "5.5470 5.9499 5.0 0.0", ];
1689 let mut line_it = lines.iter().copied();
1690 let frame = parse_single_frame(&mut line_it).unwrap();
1691 assert_eq!(frame.atom_data.len(), 6);
1692 assert_eq!(frame.atom_data[0].atom_id, 1);
1694 assert_eq!(frame.atom_data[1].atom_id, 2);
1695 assert_eq!(frame.atom_data[2].atom_id, 3);
1696 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); }
1701
1702 #[test]
1703 fn test_parse_single_frame_too_few_columns_fails() {
1704 let lines = vec![
1706 "PREBOX1",
1707 "{\"con_spec_version\":2}",
1708 "10.0 20.0 30.0",
1709 "90.0 90.0 90.0",
1710 "POSTBOX1",
1711 "POSTBOX2",
1712 "1",
1713 "1",
1714 "12.011",
1715 "C",
1716 "Coordinates of Component 1",
1717 "0.0 0.0 0.0", ];
1719 let mut line_it = lines.iter().copied();
1720 let result = parse_single_frame(&mut line_it);
1721 assert!(result.is_err());
1722 assert!(matches!(
1723 result.unwrap_err(),
1724 ParseError::InvalidVectorLength {
1725 expected: 5,
1726 found: 3
1727 }
1728 ));
1729 }
1730
1731 #[test]
1732 fn test_parse_velocity_section_present() {
1733 let lines = vec![
1734 "PREBOX1",
1735 "{\"con_spec_version\":2}",
1736 "10.0 20.0 30.0",
1737 "90.0 90.0 90.0",
1738 "POSTBOX1",
1739 "POSTBOX2",
1740 "2",
1741 "1 1",
1742 "63.546 1.008",
1743 "Cu",
1744 "Coordinates of Component 1",
1745 "0.0 0.0 0.0 1.0 0",
1746 "H",
1747 "Coordinates of Component 2",
1748 "1.0 2.0 3.0 0.0 1",
1749 "",
1750 "Cu",
1751 "Velocities of Component 1",
1752 "0.1 0.2 0.3 1.0 0",
1753 "H",
1754 "Velocities of Component 2",
1755 "0.4 0.5 0.6 0.0 1",
1756 ];
1757 let mut line_it = lines.iter().copied().peekable();
1758 let mut frame =
1760 parse_single_frame(&mut line_it).expect("coordinate parsing should succeed");
1761 assert!(!frame.has_velocities());
1762
1763 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
1765 .expect("velocity parsing should succeed");
1766 assert!(has_vel);
1767 assert_eq!(frame.atom_data[0].velocity, Some([0.1, 0.2, 0.3]));
1768 assert_eq!(frame.atom_data[1].velocity, Some([0.4, 0.5, 0.6]));
1769 }
1770
1771 #[test]
1772 fn test_validate_true_accepts_matching_section_identity() {
1773 let text = r#"
1774PREBOX1
1775{"con_spec_version":2,"sections":["velocities"],"validate":true}
177610.0 20.0 30.0
177790.0 90.0 90.0
1778POSTBOX1
1779POSTBOX2
17802
17811 1
178263.546 1.008
1783Cu
1784Coordinates of Component 1
17850.0 0.0 0.0 5 0
1786H
1787Coordinates of Component 2
17881.0 2.0 3.0 0 1
1789
1790Cu
1791Velocities of Component 1
17920.1 0.2 0.3 5 0
1793H
1794Velocities of Component 2
17950.4 0.5 0.6 0 1
1796"#;
1797 let mut iter = ConFrameIterator::new(text.trim());
1798 let frame = iter.next().unwrap().unwrap();
1799
1800 assert!(frame.has_velocities());
1801 assert_eq!(
1802 frame
1803 .header
1804 .metadata
1805 .get("validate")
1806 .and_then(|v| v.as_bool()),
1807 Some(true)
1808 );
1809 }
1810
1811 #[test]
1812 fn test_validate_true_rejects_section_atom_id_mismatch() {
1813 let text = r#"
1814PREBOX1
1815{"con_spec_version":2,"sections":["velocities"],"validate":true}
181610.0 20.0 30.0
181790.0 90.0 90.0
1818POSTBOX1
1819POSTBOX2
18201
18211
182263.546
1823Cu
1824Coordinates of Component 1
18250.0 0.0 0.0 5 0
1826
1827Cu
1828Velocities of Component 1
18290.1 0.2 0.3 5 99
1830"#;
1831 let mut iter = ConFrameIterator::new(text.trim());
1832 let err = iter.next().unwrap().unwrap_err();
1833
1834 assert!(matches!(err, ParseError::ValidationError(_)));
1835 assert!(err.to_string().contains("atom_id mismatch"));
1836 }
1837
1838 #[test]
1839 fn test_validate_true_rejects_section_symbol_mismatch() {
1840 let text = r#"
1841PREBOX1
1842{"con_spec_version":2,"sections":["forces"],"validate":true}
184310.0 20.0 30.0
184490.0 90.0 90.0
1845POSTBOX1
1846POSTBOX2
18471
18481
184963.546
1850Cu
1851Coordinates of Component 1
18520.0 0.0 0.0 5 0
1853
1854H
1855Forces of Component 1
18560.1 0.2 0.3 5 0
1857"#;
1858 let mut iter = ConFrameIterator::new(text.trim());
1859 let err = iter.next().unwrap().unwrap_err();
1860
1861 assert!(matches!(err, ParseError::ValidationError(_)));
1862 assert!(err.to_string().contains("symbol mismatch"));
1863 }
1864
1865 #[test]
1866 fn test_validate_absent_allows_legacy_duplicate_identity_mismatch() {
1867 let text = r#"
1868PREBOX1
1869{"con_spec_version":2,"sections":["velocities"]}
187010.0 20.0 30.0
187190.0 90.0 90.0
1872POSTBOX1
1873POSTBOX2
18741
18751
187663.546
1877Cu
1878Coordinates of Component 1
18790.0 0.0 0.0 5 0
1880
1881Cu
1882Velocities of Component 1
18830.1 0.2 0.3 0 99
1884"#;
1885 let mut iter = ConFrameIterator::new(text.trim());
1886 let frame = iter.next().unwrap().unwrap();
1887
1888 assert!(frame.has_velocities());
1889 assert_eq!(frame.atom_data[0].atom_id, 0);
1890 assert_eq!(frame.atom_data[0].fixed, [true, false, true]);
1891 }
1892
1893 #[test]
1894 fn test_validate_must_be_boolean_when_present() {
1895 let lines = vec![
1896 "PREBOX1",
1897 "{\"con_spec_version\":2,\"validate\":\"yes\",\"sections\":[]}",
1898 "10.0 20.0 30.0",
1899 "90.0 90.0 90.0",
1900 "POSTBOX1",
1901 "POSTBOX2",
1902 "1",
1903 "1",
1904 "12.011",
1905 ];
1906 let mut line_it = lines.iter().copied();
1907 let err = parse_frame_header(&mut line_it).unwrap_err();
1908
1909 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1910 assert!(err.to_string().contains("validate"));
1911 }
1912
1913 #[test]
1914 fn test_validate_true_requires_sections_key() {
1915 let lines = vec![
1916 "PREBOX1",
1917 "{\"con_spec_version\":2,\"validate\":true}",
1918 "10.0 20.0 30.0",
1919 "90.0 90.0 90.0",
1920 "POSTBOX1",
1921 "POSTBOX2",
1922 "1",
1923 "1",
1924 "12.011",
1925 ];
1926 let mut line_it = lines.iter().copied();
1927 let err = parse_frame_header(&mut line_it).unwrap_err();
1928
1929 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1930 assert!(err.to_string().contains("sections"));
1931 }
1932
1933 #[test]
1934 fn test_sections_must_be_string_array_when_present() {
1935 let lines = vec![
1936 "PREBOX1",
1937 "{\"con_spec_version\":2,\"sections\":[\"velocities\",7]}",
1938 "10.0 20.0 30.0",
1939 "90.0 90.0 90.0",
1940 "POSTBOX1",
1941 "POSTBOX2",
1942 "1",
1943 "1",
1944 "12.011",
1945 ];
1946 let mut line_it = lines.iter().copied();
1947 let err = parse_frame_header(&mut line_it).unwrap_err();
1948
1949 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1950 assert!(err.to_string().contains("sections"));
1951 }
1952
1953 #[test]
1954 fn test_validate_true_rejects_non_integer_coordinate_identity_columns() {
1955 let text = r#"
1956PREBOX1
1957{"con_spec_version":2,"sections":[],"validate":true}
195810.0 20.0 30.0
195990.0 90.0 90.0
1960POSTBOX1
1961POSTBOX2
19621
19631
196463.546
1965Cu
1966Coordinates of Component 1
19670.0 0.0 0.0 5.0 0
1968"#;
1969 let mut iter = ConFrameIterator::new(text.trim());
1970 let err = iter.next().unwrap().unwrap_err();
1971
1972 assert!(matches!(err, ParseError::ValidationError(_)));
1973 assert!(err.to_string().contains("fixed_flag"));
1974 }
1975
1976 #[test]
1977 fn test_validate_true_rejects_non_exact_coordinate_label() {
1978 let text = r#"
1979PREBOX1
1980{"con_spec_version":2,"sections":[],"validate":true}
198110.0 20.0 30.0
198290.0 90.0 90.0
1983POSTBOX1
1984POSTBOX2
19851
19861
198763.546
1988Cu
1989Coordinates Component 1
19900.0 0.0 0.0 5 0
1991"#;
1992 let mut iter = ConFrameIterator::new(text.trim());
1993 let err = iter.next().unwrap().unwrap_err();
1994
1995 assert!(matches!(err, ParseError::ValidationError(_)));
1996 assert!(err.to_string().contains("Coordinates of Component 1"));
1997 }
1998
1999 #[test]
2000 fn test_validate_true_rejects_unknown_component_symbol() {
2001 let text = r#"
2002PREBOX1
2003{"con_spec_version":2,"sections":[],"validate":true}
200410.0 20.0 30.0
200590.0 90.0 90.0
2006POSTBOX1
2007POSTBOX2
20081
20091
201063.546
2011Qq
2012Coordinates of Component 1
20130.0 0.0 0.0 0 0
2014"#;
2015 let mut iter = ConFrameIterator::new(text.trim());
2016 let err = iter.next().unwrap().unwrap_err();
2017
2018 assert!(matches!(err, ParseError::ValidationError(_)));
2019 assert!(err.to_string().contains("symbol"));
2020 }
2021
2022 #[test]
2023 fn test_declared_section_must_be_present() {
2024 let text = r#"
2025PREBOX1
2026{"con_spec_version":2,"sections":["velocities"]}
202710.0 20.0 30.0
202890.0 90.0 90.0
2029POSTBOX1
2030POSTBOX2
20311
20321
203363.546
2034Cu
2035Coordinates of Component 1
20360.0 0.0 0.0 0 0
2037"#;
2038 let mut iter = ConFrameIterator::new(text.trim());
2039 let err = iter.next().unwrap().unwrap_err();
2040
2041 assert!(matches!(err, ParseError::IncompleteVelocitySection));
2042 }
2043
2044 #[test]
2045 fn test_non_finite_cell_geometry_rejected_in_strict_mode() {
2046 let lines = vec![
2047 "PREBOX1",
2048 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
2049 "10.0 NaN 30.0",
2050 "90.0 90.0 90.0",
2051 "POSTBOX1",
2052 "POSTBOX2",
2053 "1",
2054 "1",
2055 "12.011",
2056 ];
2057 let mut line_it = lines.iter().copied();
2058 let err = parse_frame_header(&mut line_it).unwrap_err();
2059
2060 assert!(matches!(err, ParseError::ValidationError(_)));
2061 assert!(err.to_string().contains("cell"));
2062 }
2063
2064 #[test]
2065 fn test_validate_true_rejects_non_physical_cell_geometry() {
2066 let lines = vec![
2067 "PREBOX1",
2068 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
2069 "0.0 20.0 30.0",
2070 "90.0 180.0 90.0",
2071 "POSTBOX1",
2072 "POSTBOX2",
2073 "1",
2074 "1",
2075 "12.011",
2076 ];
2077 let mut line_it = lines.iter().copied();
2078 let err = parse_frame_header(&mut line_it).unwrap_err();
2079
2080 assert!(matches!(err, ParseError::ValidationError(_)));
2081 assert!(err.to_string().contains("cell"));
2082 }
2083
2084 #[test]
2085 fn test_validate_true_rejects_reserved_metadata_type_mismatch() {
2086 let lines = vec![
2087 "PREBOX1",
2088 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"energy\":\"low\"}",
2089 "10.0 20.0 30.0",
2090 "90.0 90.0 90.0",
2091 "POSTBOX1",
2092 "POSTBOX2",
2093 "1",
2094 "1",
2095 "12.011",
2096 ];
2097 let mut line_it = lines.iter().copied();
2098 let err = parse_frame_header(&mut line_it).unwrap_err();
2099
2100 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2101 assert!(err.to_string().contains("energy"));
2102 }
2103
2104 #[test]
2105 fn test_validate_true_rejects_malformed_bonds() {
2106 let lines = vec![
2107 "PREBOX1",
2108 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"bonds\":[[0]]}",
2109 "10.0 20.0 30.0",
2110 "90.0 90.0 90.0",
2111 "POSTBOX1",
2112 "POSTBOX2",
2113 "1",
2114 "1",
2115 "12.011",
2116 ];
2117 let mut line_it = lines.iter().copied();
2118 let err = parse_frame_header(&mut line_it).unwrap_err();
2119 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2120 assert!(err.to_string().contains("bonds"));
2121 }
2122
2123 #[test]
2124 fn test_bonds_metadata_round_trip_in_header() {
2125 use crate::types::{meta, Bond};
2126 let lines = vec![
2127 "PREBOX1",
2128 "{\"con_spec_version\":2,\"bonds\":[[0,1],{\"i\":0,\"j\":2,\"order\":1}]}",
2129 "10.0 20.0 30.0",
2130 "90.0 90.0 90.0",
2131 "POSTBOX1",
2132 "POSTBOX2",
2133 "1",
2134 "1",
2135 "12.011",
2136 ];
2137 let mut line_it = lines.iter().copied();
2138 let header = parse_frame_header(&mut line_it).expect("header");
2139 let bonds = header.bonds();
2140 assert_eq!(bonds.len(), 2);
2141 assert_eq!(bonds[0], Bond::new(0, 1));
2142 assert_eq!(bonds[1].i, 0);
2143 assert_eq!(bonds[1].j, 2);
2144 assert_eq!(bonds[1].order, Some(1));
2145 assert!(header.metadata.contains_key(meta::BONDS));
2146 }
2147
2148 #[test]
2149 fn test_parse_velocity_section_absent() {
2150 let lines = vec![
2151 "PREBOX1",
2152 "{\"con_spec_version\":2}",
2153 "10.0 20.0 30.0",
2154 "90.0 90.0 90.0",
2155 "POSTBOX1",
2156 "POSTBOX2",
2157 "1",
2158 "1",
2159 "12.011",
2160 "C",
2161 "Coordinates of Component 1",
2162 "0.0 0.0 0.0 0.0 1",
2163 ];
2164 let mut line_it = lines.iter().copied().peekable();
2165 let mut frame = parse_single_frame(&mut line_it).expect("parse should succeed");
2166 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
2167 .expect("should succeed with no velocities");
2168 assert!(!has_vel);
2169 assert_eq!(frame.atom_data[0].velocity, None);
2170 }
2171
2172 #[test]
2173 fn test_parse_line_of_range_f64_exact() {
2174 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0 42", 4, 5, &[0.0; 5]).unwrap();
2175 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 42.0]);
2176 }
2177
2178 #[test]
2179 fn test_byte_scan_stack_parses_realistic_coord_line() {
2180 let line = " 0.63939999999999997 0.90449999999999997 -0.00009999999999977 1 0";
2182 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
2183 let mut buf = [0.0f64; 5];
2184 let n = parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
2185 assert_eq!(n, 5);
2186 assert!((buf[0] - 0.6394).abs() < 1e-4);
2187 assert!((buf[1] - 0.9045).abs() < 1e-4);
2188 assert_eq!(buf[3] as u8, 1);
2189 assert_eq!(buf[4] as u64, 0);
2190 let bad = "1.0 2.0 3.0 1 0 EXTRA";
2192 assert!(parse_line_of_range_f64_stack(bad, 4, 5, &defaults, &mut buf).is_err());
2193 let glued = "1.0 2.0 3.0abc 1 0";
2195 assert!(parse_line_of_range_f64_stack(glued, 4, 5, &defaults, &mut buf).is_err());
2196 }
2197
2198 #[test]
2199 fn test_parse_line_of_range_f64_stack_matches_vec_api() {
2200 let defaults = [0.0, 0.0, 0.0, 0.0, 7.0];
2201 let line = "1.0 2.0 3.0 0.0";
2202 let mut buf = [0.0f64; 5];
2203 parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
2204 let via_vec = parse_line_of_range_f64(line, 4, 5, &defaults).unwrap();
2205 assert_eq!(&buf[..5], via_vec.as_slice());
2206 assert_eq!(buf[4], 7.0);
2207 }
2208
2209 #[test]
2210 fn test_parse_line_of_range_f64_padded() {
2211 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
2212 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0", 4, 5, &defaults).unwrap();
2213 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 99.0]);
2214 }
2215
2216 #[test]
2217 fn test_parse_line_of_range_f64_too_few() {
2218 let result = parse_line_of_range_f64("1.0 2.0 3.0", 4, 5, &[0.0; 5]);
2219 assert!(result.is_err());
2220 }
2221
2222 #[test]
2223 fn test_parse_line_of_range_f64_too_many() {
2224 let result = parse_line_of_range_f64("1.0 2.0 3.0 0.0 5.0 6.0", 4, 5, &[0.0; 5]);
2225 assert!(result.is_err());
2226 }
2227
2228 #[test]
2229 fn test_parse_all_four_column_lines() {
2230 let lines = vec![
2232 "PREBOX1",
2233 "{\"con_spec_version\":2}",
2234 "10.0 10.0 10.0",
2235 "90.0 90.0 90.0",
2236 "POSTBOX1",
2237 "POSTBOX2",
2238 "1",
2239 "3",
2240 "12.011",
2241 "C",
2242 "Coordinates of Component 1",
2243 "0.0 0.0 0.0 0",
2244 "1.0 0.0 0.0 0",
2245 "2.0 0.0 0.0 1",
2246 ];
2247 let mut line_it = lines.iter().copied();
2248 let frame = parse_single_frame(&mut line_it).unwrap();
2249 assert_eq!(frame.atom_data[0].atom_id, 0);
2250 assert_eq!(frame.atom_data[1].atom_id, 1);
2251 assert_eq!(frame.atom_data[2].atom_id, 2);
2252 assert!(frame.atom_data[2].is_fixed());
2253 }
2254}