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_for_spec, 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(
671 coord_line,
672 "coordinate",
673 3,
674 4,
675 5,
676 header.spec_version,
677 )?
678 } else {
679 (
680 decode_fixed_bitmask_for_spec(vals[3] as u8, header.spec_version),
681 vals[4] as u64,
682 )
683 };
684 let xyz = [vals[0], vals[1], vals[2]];
685 if f64_positions {
686 let o = atom_i * 3;
687 pos_flat[o] = xyz[0];
688 pos_flat[o + 1] = xyz[1];
689 pos_flat[o + 2] = xyz[2];
690 } else if let Some(ref mut pos) = positions_other {
691 pos.set_f64_row(atom_i, xyz);
692 }
693 atom_data.push(AtomDatum {
694 symbol: Arc::clone(&symbol),
696 x: xyz[0],
697 y: xyz[1],
698 z: xyz[2],
699 fixed,
700 atom_id,
701 velocity: None,
702 force: None,
703 energy: None,
704 charge: None,
705 spin: None,
706 magmom: None,
707 });
708 global_atom_idx += 1;
709 atom_i += 1;
710 }
711 }
712 let positions = if f64_positions {
713 FloatArray2::from_f64_row_major(total_atoms, 3, pos_flat)
714 } else {
715 positions_other.expect("non-f64 positions allocated")
716 };
717 Ok(crate::types::con_frame_from_atom_data_with_positions(
719 header, atom_data, positions,
720 ))
721}
722
723fn validate_header_geometry(
724 boxl: &[f64],
725 angles: &[f64],
726 natm_types: usize,
727 natms_per_type: &[usize],
728) -> Result<(), ParseError> {
729 if boxl.iter().any(|length| !length.is_finite() || *length <= 0.0)
730 || angles
731 .iter()
732 .any(|angle| !angle.is_finite() || *angle <= 0.0 || *angle >= 180.0)
733 {
734 return Err(ParseError::ValidationError(
735 "cell geometry must have positive lengths and angles between 0 and 180 degrees"
736 .to_string(),
737 ));
738 }
739 if natm_types == 0 || natms_per_type.contains(&0) {
740 return Err(ParseError::ValidationError(
741 "atom counts must contain at least one atom per component".to_string(),
742 ));
743 }
744 Ok(())
745}
746
747fn validate_masses(masses_per_type: &[f64]) -> Result<(), ParseError> {
748 if masses_per_type
749 .iter()
750 .any(|mass| !mass.is_finite() || *mass <= 0.0)
751 {
752 return Err(ParseError::ValidationError(
753 "component masses must be positive".to_string(),
754 ));
755 }
756 Ok(())
757}
758
759fn validate_coordinate_component(
760 type_idx: usize,
761 symbol: &str,
762 label: &str,
763) -> Result<(), ParseError> {
764 let expected_label = format!("Coordinates of Component {}", type_idx + 1);
765 if label.trim() != expected_label {
766 return Err(ParseError::ValidationError(format!(
767 "expected coordinate label {expected_label:?}, found {label:?}"
768 )));
769 }
770 if symbol != "X" && symbol_to_atomic_number(symbol) == 0 {
771 return Err(ParseError::ValidationError(format!(
772 "unknown component symbol {symbol}"
773 )));
774 }
775 Ok(())
776}
777
778fn parse_identity_columns(
793 line: &str,
794 row_kind: &str,
795 fixed_idx: usize,
796 atom_id_idx: usize,
797 n_cols: usize,
798 spec_version: u32,
799) -> Result<([bool; 3], u64), ParseError> {
800 let columns = line.split_ascii_whitespace().collect::<Vec<_>>();
801 if columns.len() != n_cols {
802 return Err(ParseError::ValidationError(format!(
803 "{row_kind} rows require {n_cols} columns including fixed_flag and atom_id in validate mode"
804 )));
805 }
806 let fixed_flag = columns[fixed_idx].parse::<u8>().map_err(|_| {
807 ParseError::ValidationError(format!("{row_kind} fixed_flag must be an integer bitmask"))
808 })?;
809 if fixed_flag > 7 {
810 return Err(ParseError::ValidationError(format!(
811 "{row_kind} fixed_flag must be between 0 and 7"
812 )));
813 }
814 let atom_id = columns[atom_id_idx].parse::<u64>().map_err(|_| {
815 ParseError::ValidationError(format!("{row_kind} atom_id must be an integer"))
816 })?;
817 Ok((decode_fixed_bitmask_for_spec(fixed_flag, spec_version), atom_id))
818}
819
820
821fn validate_section_component(
822 section: &str,
823 type_idx: usize,
824 atom_idx: usize,
825 symbol: &str,
826 label: &str,
827 header: &FrameHeader,
828 atom_data: &[AtomDatum],
829) -> Result<(), ParseError> {
830 let expected_label = format!("{section} of Component {}", type_idx + 1);
831 if label.trim() != expected_label {
832 return Err(ParseError::ValidationError(format!(
833 "expected section label {expected_label:?}, found {label:?}"
834 )));
835 }
836
837 if header.natms_per_type[type_idx] == 0 {
838 return Ok(());
839 }
840
841 let expected_symbol = atom_data
842 .get(atom_idx)
843 .map(|atom| atom.symbol.as_ref())
844 .ok_or_else(|| {
845 ParseError::ValidationError(format!(
846 "{section} component {} has no coordinate atom to validate against",
847 type_idx + 1
848 ))
849 })?;
850 if symbol != expected_symbol {
851 return Err(ParseError::ValidationError(format!(
852 "{section} component {} symbol mismatch: expected {expected_symbol}, found {symbol}",
853 type_idx + 1
854 )));
855 }
856
857 Ok(())
858}
859
860fn validate_section_atom_identity(
861 section: &str,
862 atom_idx: usize,
863 fixed: [bool; 3],
864 atom_id: u64,
865 atom_data: &[AtomDatum],
866) -> Result<(), ParseError> {
867 let atom = atom_data.get(atom_idx).ok_or_else(|| {
868 ParseError::ValidationError(format!(
869 "{section} row {atom_idx} has no coordinate atom to validate against"
870 ))
871 })?;
872
873 if atom.fixed != fixed {
874 return Err(ParseError::ValidationError(format!(
875 "{section} row {atom_idx} fixed mask mismatch for atom_id {}",
876 atom.atom_id
877 )));
878 }
879 if atom.atom_id != atom_id {
880 return Err(ParseError::ValidationError(format!(
881 "{section} row {atom_idx} atom_id mismatch: expected {}, found {atom_id}",
882 atom.atom_id
883 )));
884 }
885
886 Ok(())
887}
888
889pub fn parse_velocity_section<'a>(
902 lines: &mut impl LineStream<'a>,
903 header: &FrameHeader,
904 atom_data: &mut [AtomDatum],
905) -> Result<bool, ParseError> {
906 let validate = header.strict_validation;
907 match lines.peek_line() {
909 Some(line) if line.trim().is_empty() => {
910 lines.next_line();
912 }
913 _ => return Ok(false),
914 }
915
916 let mut atom_idx: usize = 0;
917 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
918 let symbol = lines
920 .next_line()
921 .ok_or(ParseError::IncompleteVelocitySection)?
922 .trim();
923
924 let comp_line = lines.next_line().ok_or(ParseError::IncompleteVelocitySection)?;
926 if !comp_line.contains("Velocities of Component") {
928 return Err(ParseError::IncompleteVelocitySection);
929 }
930 if validate {
931 validate_section_component(
932 "Velocities",
933 type_idx,
934 atom_idx,
935 symbol,
936 comp_line,
937 header,
938 atom_data,
939 )?;
940 }
941
942 for _ in 0..num_atoms {
943 let vel_line = lines.next_line().ok_or(ParseError::IncompleteVelocitySection)?;
944 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
946 let mut vals = [0.0f64; 5];
947 parse_line_of_range_f64_stack(vel_line, 4, 5, &defaults, &mut vals)?;
948 if validate {
949 let (fixed, atom_id) = parse_identity_columns(
950 vel_line,
951 "velocities",
952 3,
953 4,
954 5,
955 header.spec_version,
956 )?;
957 validate_section_atom_identity("velocities", atom_idx, fixed, atom_id, atom_data)?;
958 }
959 if atom_idx < atom_data.len() {
960 atom_data[atom_idx].velocity = Some([vals[0], vals[1], vals[2]]);
961 }
962 atom_idx += 1;
963 }
964 }
965
966 Ok(true)
967}
968
969pub fn parse_force_section<'a>(
977 lines: &mut impl LineStream<'a>,
978 header: &FrameHeader,
979 atom_data: &mut [AtomDatum],
980) -> Result<bool, ParseError> {
981 let validate = header.strict_validation;
982 match lines.peek_line() {
984 Some(line) if line.trim().is_empty() => {
985 lines.next_line();
986 }
987 _ => return Ok(false),
988 }
989
990 let mut atom_idx: usize = 0;
991 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
992 let symbol = lines
993 .next_line()
994 .ok_or(ParseError::IncompleteForceSection)?
995 .trim();
996
997 let comp_line = lines.next_line().ok_or(ParseError::IncompleteForceSection)?;
998 if !comp_line.contains("Forces of Component") {
999 return Err(ParseError::IncompleteForceSection);
1000 }
1001 if validate {
1002 validate_section_component(
1003 "Forces", type_idx, atom_idx, symbol, comp_line, header, atom_data,
1004 )?;
1005 }
1006
1007 for _ in 0..num_atoms {
1008 let force_line = lines.next_line().ok_or(ParseError::IncompleteForceSection)?;
1009 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
1010 let mut vals = [0.0f64; 5];
1011 parse_line_of_range_f64_stack(force_line, 4, 5, &defaults, &mut vals)?;
1012 if validate {
1013 let (fixed, atom_id) = parse_identity_columns(
1014 force_line,
1015 "forces",
1016 3,
1017 4,
1018 5,
1019 header.spec_version,
1020 )?;
1021 validate_section_atom_identity("forces", atom_idx, fixed, atom_id, atom_data)?;
1022 }
1023 if atom_idx < atom_data.len() {
1024 atom_data[atom_idx].force = Some([vals[0], vals[1], vals[2]]);
1025 }
1026 atom_idx += 1;
1027 }
1028 }
1029
1030 Ok(true)
1031}
1032
1033pub fn parse_energy_section<'a>(
1046 lines: &mut impl LineStream<'a>,
1047 header: &FrameHeader,
1048 atom_data: &mut [AtomDatum],
1049) -> Result<bool, ParseError> {
1050 let validate = header.strict_validation;
1051 match lines.peek_line() {
1052 Some(line) if line.trim().is_empty() => {
1053 lines.next_line();
1054 }
1055 _ => return Ok(false),
1056 }
1057
1058 let mut atom_idx: usize = 0;
1059 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1060 let symbol = lines
1061 .next_line()
1062 .ok_or(ParseError::IncompleteEnergySection)?
1063 .trim();
1064
1065 let comp_line = lines.next_line().ok_or(ParseError::IncompleteEnergySection)?;
1066 if !comp_line.contains("Energies of Component") {
1067 return Err(ParseError::IncompleteEnergySection);
1068 }
1069 if validate {
1070 validate_section_component(
1071 "Energies", type_idx, atom_idx, symbol, comp_line, header, atom_data,
1072 )?;
1073 }
1074
1075 for _ in 0..num_atoms {
1076 let energy_line = lines.next_line().ok_or(ParseError::IncompleteEnergySection)?;
1077 let defaults = [0.0, 0.0, atom_idx as f64];
1080 let vals = parse_line_of_range_f64(energy_line, 1, 3, &defaults)?;
1081 if validate {
1082 let (fixed, atom_id) = parse_identity_columns(
1083 energy_line,
1084 "energies",
1085 1,
1086 2,
1087 3,
1088 header.spec_version,
1089 )?;
1090 validate_section_atom_identity("energies", atom_idx, fixed, atom_id, atom_data)?;
1091 }
1092 if atom_idx < atom_data.len() {
1093 atom_data[atom_idx].energy = Some(vals[0]);
1094 }
1095 atom_idx += 1;
1096 }
1097 }
1098
1099 Ok(true)
1100}
1101
1102pub fn parse_declared_sections<'a>(
1108 lines: &mut impl LineStream<'a>,
1109 header: &mut FrameHeader,
1110 atom_data: &mut [AtomDatum],
1111) -> Result<usize, ParseError> {
1112 let mut applied = 0usize;
1113 if !header.sections_declared && header.sections.is_empty() {
1114 let found = parse_velocity_section(lines, header, atom_data)?;
1116 if found {
1117 header.sections.push(SECTION_VELOCITIES.into());
1118 applied = 1;
1119 }
1120 } else {
1121 let sections = std::mem::take(&mut header.sections);
1122 for section in §ions {
1123 match section.as_str() {
1124 SECTION_VELOCITIES => {
1125 let found = parse_velocity_section(lines, header, atom_data)?;
1126 if !found {
1127 return Err(ParseError::IncompleteVelocitySection);
1128 }
1129 applied += 1;
1130 }
1131 SECTION_FORCES => {
1132 let found = parse_force_section(lines, header, atom_data)?;
1133 if !found {
1134 return Err(ParseError::IncompleteForceSection);
1135 }
1136 applied += 1;
1137 }
1138 SECTION_ENERGIES => {
1139 let found = parse_energy_section(lines, header, atom_data)?;
1140 if !found {
1141 return Err(ParseError::IncompleteEnergySection);
1142 }
1143 applied += 1;
1144 }
1145 SECTION_CHARGES => {
1146 let found = parse_charge_section(lines, header, atom_data)?;
1147 if !found {
1148 return Err(ParseError::IncompleteSection(SECTION_CHARGES.into()));
1149 }
1150 applied += 1;
1151 }
1152 SECTION_SPINS => {
1153 let found = parse_spin_section(lines, header, atom_data)?;
1154 if !found {
1155 return Err(ParseError::IncompleteSection(SECTION_SPINS.into()));
1156 }
1157 applied += 1;
1158 }
1159 SECTION_MAGMOMS => {
1160 let found = parse_magmom_section(lines, header, atom_data)?;
1161 if !found {
1162 return Err(ParseError::IncompleteSection(SECTION_MAGMOMS.into()));
1163 }
1164 applied += 1;
1165 }
1166 other => return Err(ParseError::UnknownSection(other.to_string())),
1167 }
1168 }
1169 header.sections = sections;
1170 }
1171 Ok(applied)
1172}
1173
1174fn parse_scalar_atom_section<'a>(
1177 lines: &mut impl LineStream<'a>,
1178 header: &FrameHeader,
1179 atom_data: &mut [AtomDatum],
1180 section_name: &str,
1181 component_label: &str,
1182 mut set_value: impl FnMut(&mut AtomDatum, f64),
1183) -> Result<bool, ParseError> {
1184 let validate = header.strict_validation;
1185 match lines.peek_line() {
1186 Some(line) if line.trim().is_empty() => {
1187 lines.next_line();
1188 }
1189 _ => return Ok(false),
1190 }
1191
1192 let mut atom_idx: usize = 0;
1193 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1194 let symbol = lines
1195 .next_line()
1196 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?
1197 .trim();
1198
1199 let comp_line = lines
1200 .next_line()
1201 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
1202 if !comp_line.contains(component_label) {
1203 return Err(ParseError::IncompleteSection(section_name.into()));
1204 }
1205 if validate {
1206 validate_section_component(
1207 component_label.trim_end_matches(" of Component").trim(),
1208 type_idx,
1209 atom_idx,
1210 symbol,
1211 comp_line,
1212 header,
1213 atom_data,
1214 )?;
1215 }
1216
1217 for _ in 0..num_atoms {
1218 let data_line = lines
1219 .next_line()
1220 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
1221 let defaults = [0.0, 0.0, atom_idx as f64];
1222 let vals = parse_line_of_range_f64(data_line, 1, 3, &defaults)?;
1223 if validate {
1224 let (fixed, atom_id) = parse_identity_columns(
1225 data_line,
1226 section_name,
1227 1,
1228 2,
1229 3,
1230 header.spec_version,
1231 )?;
1232 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
1233 }
1234 if atom_idx < atom_data.len() {
1235 set_value(&mut atom_data[atom_idx], vals[0]);
1236 }
1237 atom_idx += 1;
1238 }
1239 }
1240 Ok(true)
1241}
1242
1243pub fn parse_charge_section<'a>(
1244 lines: &mut impl LineStream<'a>,
1245 header: &FrameHeader,
1246 atom_data: &mut [AtomDatum],
1247) -> Result<bool, ParseError> {
1248 parse_scalar_atom_section(
1249 lines,
1250 header,
1251 atom_data,
1252 SECTION_CHARGES,
1253 "Charges of Component",
1254 |a, v| a.charge = Some(v),
1255 )
1256}
1257
1258pub fn parse_spin_section<'a>(
1259 lines: &mut impl LineStream<'a>,
1260 header: &FrameHeader,
1261 atom_data: &mut [AtomDatum],
1262) -> Result<bool, ParseError> {
1263 parse_scalar_atom_section(
1264 lines,
1265 header,
1266 atom_data,
1267 SECTION_SPINS,
1268 "Spins of Component",
1269 |a, v| a.spin = Some(v),
1270 )
1271}
1272
1273pub fn parse_magmom_section<'a>(
1275 lines: &mut impl LineStream<'a>,
1276 header: &FrameHeader,
1277 atom_data: &mut [AtomDatum],
1278) -> Result<bool, ParseError> {
1279 let validate = header.strict_validation;
1280 match lines.peek_line() {
1281 Some(line) if line.trim().is_empty() => {
1282 lines.next_line();
1283 }
1284 _ => return Ok(false),
1285 }
1286
1287 let mut atom_idx: usize = 0;
1288 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1289 let symbol = lines
1290 .next_line()
1291 .ok_or_else(|| ParseError::IncompleteSection(SECTION_MAGMOMS.into()))?
1292 .trim();
1293
1294 let comp_line = lines
1295 .next_line()
1296 .ok_or_else(|| ParseError::IncompleteSection(SECTION_MAGMOMS.into()))?;
1297 if !comp_line.contains("Magmoms of Component") {
1298 return Err(ParseError::IncompleteSection(SECTION_MAGMOMS.into()));
1299 }
1300 if validate {
1301 validate_section_component(
1302 "Magmoms",
1303 type_idx,
1304 atom_idx,
1305 symbol,
1306 comp_line,
1307 header,
1308 atom_data,
1309 )?;
1310 }
1311
1312 for _ in 0..num_atoms {
1313 let mm_line = lines
1314 .next_line()
1315 .ok_or_else(|| ParseError::IncompleteSection(SECTION_MAGMOMS.into()))?;
1316 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
1317 let mut vals = [0.0f64; 5];
1318 parse_line_of_range_f64_stack(mm_line, 4, 5, &defaults, &mut vals)?;
1319 if validate {
1320 let (fixed, atom_id) = parse_identity_columns(
1321 mm_line,
1322 SECTION_MAGMOMS,
1323 3,
1324 4,
1325 5,
1326 header.spec_version,
1327 )?;
1328 validate_section_atom_identity(
1329 SECTION_MAGMOMS,
1330 atom_idx,
1331 fixed,
1332 atom_id,
1333 atom_data,
1334 )?;
1335 }
1336 if atom_idx < atom_data.len() {
1337 atom_data[atom_idx].magmom = Some([vals[0], vals[1], vals[2]]);
1338 }
1339 atom_idx += 1;
1340 }
1341 }
1342 Ok(true)
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347 use super::*;
1348 use crate::iterators::ConFrameIterator;
1349
1350 #[test]
1351 fn test_parse_line_of_n_success() {
1352 let line = "1.0 2.5 -3.0";
1353 let values = parse_line_of_n::<f64>(line, 3).unwrap();
1354 assert_eq!(values, vec![1.0, 2.5, -3.0]);
1355 }
1356
1357 #[test]
1358 fn test_parse_line_of_n_too_short() {
1359 let line = "1.0 2.5";
1360 let result = parse_line_of_n::<f64>(line, 3);
1361 assert!(result.is_err());
1362 assert!(matches!(
1363 result.unwrap_err(),
1364 ParseError::InvalidVectorLength {
1365 expected: 3,
1366 found: 2
1367 }
1368 ));
1369 }
1370
1371 #[test]
1372 fn test_parse_line_of_n_too_long() {
1373 let line = "1.0 2.5 -3.0 4.0";
1374 let result = parse_line_of_n::<f64>(line, 3);
1375 assert!(result.is_err());
1376 assert!(matches!(
1377 result.unwrap_err(),
1378 ParseError::InvalidVectorLength {
1379 expected: 3,
1380 found: 4
1381 }
1382 ));
1383 }
1384
1385 #[test]
1386 fn test_parse_line_of_n_invalid_float() {
1387 let line = "1.0 abc -3.0";
1388 let result = parse_line_of_n::<f64>(line, 3);
1389 assert!(result.is_err());
1390 assert!(matches!(
1391 result.unwrap_err(),
1392 ParseError::InvalidNumberFormat(_)
1393 ));
1394 }
1395
1396 #[test]
1397 fn test_parse_frame_header_success() {
1398 let lines = [
1399 "PREBOX1",
1400 "{\"con_spec_version\":2}",
1401 "10.0 20.0 30.0",
1402 "90.0 90.0 90.0",
1403 "POSTBOX1",
1404 "POSTBOX2",
1405 "2",
1406 "1 1",
1407 "12.011 1.008",
1408 ];
1409 let mut line_it = lines.iter().copied();
1410 match parse_frame_header(&mut line_it) {
1411 Ok(header) => {
1412 assert_eq!(header.prebox_header.user, "PREBOX1");
1413 assert_eq!(header.spec_version, 2);
1414 assert_eq!(header.boxl, [10.0, 20.0, 30.0]);
1415 assert_eq!(header.angles, [90.0, 90.0, 90.0]);
1416 assert_eq!(header.postbox_header, ["POSTBOX1", "POSTBOX2"]);
1417 assert_eq!(header.natm_types, 2);
1418 assert_eq!(header.natms_per_type, vec![1, 1]);
1419 assert_eq!(header.masses_per_type, vec![12.011, 1.008]);
1420 }
1421 Err(e) => {
1422 panic!(
1423 "Parsing failed when it should have succeeded. Error: {:?}",
1424 e
1425 );
1426 }
1427 }
1428 }
1429
1430 #[test]
1431 fn test_parse_frame_header_missing_line() {
1432 let lines = [
1433 "PREBOX1",
1434 "{\"con_spec_version\":2}",
1435 "10.0 20.0 30.0",
1436 "90.0 90.0 90.0",
1437 "POSTBOX1",
1438 "POSTBOX2",
1439 "2",
1440 "1 1",
1441 ];
1443 let mut line_it = lines.iter().copied();
1444 let result = parse_frame_header(&mut line_it);
1445 assert!(result.is_err());
1446 assert!(matches!(result.unwrap_err(), ParseError::IncompleteHeader));
1447 }
1448
1449 #[test]
1450 fn test_parse_frame_header_missing_spec_version() {
1451 let lines = vec![
1452 "PREBOX1",
1453 "{}",
1454 "10.0 20.0 30.0",
1455 "90.0 90.0 90.0",
1456 "POSTBOX1",
1457 "POSTBOX2",
1458 "2",
1459 "1 1",
1460 "12.011 1.008",
1461 ];
1462 let mut line_it = lines.iter().copied();
1463 let result = parse_frame_header(&mut line_it);
1464 assert!(result.is_err());
1465 assert!(matches!(
1466 result.unwrap_err(),
1467 ParseError::MissingSpecVersion
1468 ));
1469 }
1470
1471 #[test]
1472 fn test_parse_frame_header_legacy_no_json() {
1473 let lines = vec![
1475 "PREBOX1",
1476 "0.0000 TIME",
1477 "10.0 20.0 30.0",
1478 "90.0 90.0 90.0",
1479 "POSTBOX1",
1480 "POSTBOX2",
1481 "2",
1482 "1 1",
1483 "12.011 1.008",
1484 ];
1485 let mut line_it = lines.iter().copied();
1486 let header = parse_frame_header(&mut line_it).unwrap();
1487 assert_eq!(header.spec_version, 1);
1488 assert!(header.metadata.is_empty());
1489 }
1490
1491 #[test]
1492 fn test_parse_frame_header_malformed_json() {
1493 let lines = vec![
1495 "PREBOX1",
1496 "{broken json",
1497 "10.0 20.0 30.0",
1498 "90.0 90.0 90.0",
1499 "POSTBOX1",
1500 "POSTBOX2",
1501 "2",
1502 "1 1",
1503 "12.011 1.008",
1504 ];
1505 let mut line_it = lines.iter().copied();
1506 let result = parse_frame_header(&mut line_it);
1507 assert!(result.is_err());
1508 assert!(matches!(
1509 result.unwrap_err(),
1510 ParseError::InvalidMetadataJson(_)
1511 ));
1512 }
1513
1514 #[test]
1515 fn test_parse_frame_header_unsupported_version() {
1516 let lines = vec![
1517 "PREBOX1",
1518 "{\"con_spec_version\":999}",
1519 "10.0 20.0 30.0",
1520 "90.0 90.0 90.0",
1521 "POSTBOX1",
1522 "POSTBOX2",
1523 "2",
1524 "1 1",
1525 "12.011 1.008",
1526 ];
1527 let mut line_it = lines.iter().copied();
1528 let result = parse_frame_header(&mut line_it);
1529 assert!(result.is_err());
1530 assert!(matches!(
1531 result.unwrap_err(),
1532 ParseError::UnsupportedSpecVersion(999)
1533 ));
1534 }
1535
1536 #[test]
1537 fn test_v3_missing_units_rejected() {
1538 let lines = vec![
1539 "PREBOX1",
1540 "{\"con_spec_version\":3}",
1541 "10.0 20.0 30.0",
1542 "90.0 90.0 90.0",
1543 "POSTBOX1",
1544 "POSTBOX2",
1545 "1",
1546 "1",
1547 "1.0",
1548 ];
1549 let mut line_it = lines.iter().copied();
1550 let err = parse_frame_header(&mut line_it).unwrap_err();
1551 let msg = err.to_string();
1552 assert!(
1553 msg.contains("units") || msg.contains("v3"),
1554 "expected units error, got {msg}"
1555 );
1556 }
1557
1558 #[test]
1559 fn test_v3_invalid_units_rejected() {
1560 let lines = vec![
1561 "PREBOX1",
1562 r#"{"con_spec_version":3,"units":{"length":"eV","energy":"angstrom"}}"#,
1563 "10.0 20.0 30.0",
1564 "90.0 90.0 90.0",
1565 "POSTBOX1",
1566 "POSTBOX2",
1567 "1",
1568 "1",
1569 "1.0",
1570 ];
1571 let mut line_it = lines.iter().copied();
1572 assert!(parse_frame_header(&mut line_it).is_err());
1573 }
1574
1575 #[test]
1576 fn test_v3_valid_units_exposes_length_energy() {
1577 let lines = vec![
1578 "PREBOX1",
1579 r#"{"con_spec_version":3,"units":{"length":"angstrom","energy":"eV","mass":"amu","time":"fs"}}"#,
1580 "10.0 20.0 30.0",
1581 "90.0 90.0 90.0",
1582 "POSTBOX1",
1583 "POSTBOX2",
1584 "1",
1585 "1",
1586 "1.0",
1587 ];
1588 let mut line_it = lines.iter().copied();
1589 let header = parse_frame_header(&mut line_it).unwrap();
1590 assert_eq!(header.spec_version, 3);
1591 assert_eq!(header.length_unit(), Some("angstrom"));
1592 assert_eq!(header.energy_unit(), Some("eV"));
1593 let f = header.conversion_factor_to("length", "nm").unwrap();
1594 assert!((f - 0.1).abs() < 1e-12);
1595 }
1596
1597 #[test]
1598 fn test_parse_frame_header_extra_metadata_preserved() {
1599 let lines = vec![
1600 "PREBOX1",
1601 "{\"con_spec_version\":2,\"generator\":\"test\"}",
1602 "10.0 20.0 30.0",
1603 "90.0 90.0 90.0",
1604 "POSTBOX1",
1605 "POSTBOX2",
1606 "2",
1607 "1 1",
1608 "12.011 1.008",
1609 ];
1610 let mut line_it = lines.iter().copied();
1611 let header = parse_frame_header(&mut line_it).unwrap();
1612 assert_eq!(header.spec_version, 2);
1613 assert_eq!(
1614 header.metadata.get("generator"),
1615 Some(&serde_json::Value::String("test".to_string()))
1616 );
1617 }
1618
1619 #[test]
1620 fn test_parse_frame_header_invalid_natms_per_type() {
1621 let lines = vec![
1622 "PREBOX1",
1623 "{\"con_spec_version\":2}",
1624 "10.0 20.0 30.0",
1625 "90.0 90.0 90.0",
1626 "POSTBOX1",
1627 "POSTBOX2",
1628 "2",
1629 "1 1 1", "12.011 1.008",
1631 ];
1632 let mut line_it = lines.iter().copied();
1633 let result = parse_frame_header(&mut line_it);
1634 assert!(result.is_err());
1635 assert!(matches!(
1636 result.unwrap_err(),
1637 ParseError::InvalidVectorLength {
1638 expected: 2,
1639 found: 3
1640 }
1641 ));
1642 }
1643
1644 #[test]
1645 fn test_parse_single_frame_success() {
1646 let lines = vec![
1647 "PREBOX1",
1648 "{\"con_spec_version\":2}",
1649 "10.0 20.0 30.0",
1650 "90.0 90.0 90.0",
1651 "POSTBOX1",
1652 "POSTBOX2",
1653 "2",
1654 "3 3",
1655 "12.011 1.008",
1656 "1",
1657 "Coordinates of Component 1",
1658 "0.0 0.0 0.0 0.0 1",
1659 "1.0940 0.0 0.0 0.0 2",
1660 "-0.5470 0.9499 0.0 0.0 3",
1661 "2",
1662 "Coordinates of Component 2",
1663 "5.0 5.0 5.0 0.0 4",
1664 "6.0940 5.0 5.0 0.0 5",
1665 "5.5470 5.9499 5.0 0.0 6",
1666 ];
1667 let mut line_it = lines.iter().copied();
1668 let frame = parse_single_frame(&mut line_it).unwrap();
1669
1670 assert_eq!(frame.header.natm_types, 2);
1671 assert_eq!(frame.header.natms_per_type, vec![3, 3]);
1672 assert_eq!(frame.header.masses_per_type, vec![12.011, 1.008]);
1673 assert_eq!(frame.atom_data.len(), 6);
1674 assert_eq!(&*frame.atom_data[0].symbol, "1");
1675 assert_eq!(frame.atom_data[0].atom_id, 1);
1676 assert_eq!(&*frame.atom_data[5].symbol, "2");
1677 assert_eq!(frame.atom_data[5].atom_id, 6);
1678 }
1679
1680 #[test]
1681 fn test_column_4_value_1_is_x_only_on_spec_2() {
1682 let lines = vec![
1683 "Generated by probe",
1684 "{\"con_spec_version\":2}",
1685 "20.0 20.0 20.0",
1686 "90.0 90.0 90.0",
1687 "",
1688 "",
1689 "1",
1690 "1",
1691 "63.5",
1692 "Cu",
1693 "Coordinates of component 1",
1694 "1.0 2.0 3.0 1 1",
1695 ];
1696 let mut line_it = lines.iter().copied();
1697 let frame = parse_single_frame(&mut line_it).unwrap();
1698 assert_eq!(frame.header.spec_version, 2);
1699 assert_eq!(frame.atom_data[0].fixed, [true, false, false]);
1700 }
1701
1702 #[test]
1703 fn test_column_4_value_1_is_all_fixed_on_spec_1() {
1704 let lines = vec![
1705 "Generated by probe",
1706 "0.0000 TIME",
1707 "20.0 20.0 20.0",
1708 "90.0 90.0 90.0",
1709 "",
1710 "",
1711 "1",
1712 "1",
1713 "63.5",
1714 "Cu",
1715 "Coordinates of component 1",
1716 "1.0 2.0 3.0 1 1",
1717 ];
1718 let mut line_it = lines.iter().copied();
1719 let frame = parse_single_frame(&mut line_it).unwrap();
1720 assert_eq!(frame.header.spec_version, 1);
1721 assert_eq!(frame.atom_data[0].fixed, [true, true, true]);
1722 }
1723
1724 #[test]
1725 fn test_parse_single_frame_missing_line() {
1726 let lines = vec![
1728 "PREBOX1",
1729 "{\"con_spec_version\":2}",
1730 "10.0 20.0 30.0",
1731 "90.0 90.0 90.0",
1732 "POSTBOX1",
1733 "POSTBOX2",
1734 "2",
1735 "3 3",
1736 "12.011 1.008",
1737 "1",
1738 "Coordinates of Component 1",
1739 "0.0 0.0 0.0 0.0 1",
1740 "1.0940 0.0 0.0 0.0 2",
1741 "-0.5470 0.9499 0.0 0.0 3",
1742 ];
1744 let mut line_it = lines.iter().copied();
1745 let result = parse_single_frame(&mut line_it);
1746 assert!(result.is_err());
1747 assert!(matches!(result.unwrap_err(), ParseError::IncompleteFrame));
1748 }
1749
1750 #[test]
1751 fn test_parse_single_frame_missing_atom_index_defaults_sequential() {
1752 let lines = vec![
1754 "PREBOX1",
1755 "{\"con_spec_version\":2}",
1756 "10.0 20.0 30.0",
1757 "90.0 90.0 90.0",
1758 "POSTBOX1",
1759 "POSTBOX2",
1760 "2",
1761 "3 3",
1762 "12.011 1.008",
1763 "1",
1764 "Coordinates of Component 1",
1765 "0.0 0.0 0.0 0.0 1",
1766 "1.0940 0.0 0.0 0.0 2",
1767 "-0.5470 0.9499 0.0 0.0 3",
1768 "2",
1769 "Coordinates of Component 2",
1770 "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", ];
1774 let mut line_it = lines.iter().copied();
1775 let frame = parse_single_frame(&mut line_it).unwrap();
1776 assert_eq!(frame.atom_data.len(), 6);
1777 assert_eq!(frame.atom_data[0].atom_id, 1);
1779 assert_eq!(frame.atom_data[1].atom_id, 2);
1780 assert_eq!(frame.atom_data[2].atom_id, 3);
1781 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); }
1786
1787 #[test]
1788 fn test_parse_single_frame_too_few_columns_fails() {
1789 let lines = vec![
1791 "PREBOX1",
1792 "{\"con_spec_version\":2}",
1793 "10.0 20.0 30.0",
1794 "90.0 90.0 90.0",
1795 "POSTBOX1",
1796 "POSTBOX2",
1797 "1",
1798 "1",
1799 "12.011",
1800 "C",
1801 "Coordinates of Component 1",
1802 "0.0 0.0 0.0", ];
1804 let mut line_it = lines.iter().copied();
1805 let result = parse_single_frame(&mut line_it);
1806 assert!(result.is_err());
1807 assert!(matches!(
1808 result.unwrap_err(),
1809 ParseError::InvalidVectorLength {
1810 expected: 5,
1811 found: 3
1812 }
1813 ));
1814 }
1815
1816 #[test]
1817 fn test_parse_velocity_section_present() {
1818 let lines = vec![
1819 "PREBOX1",
1820 "{\"con_spec_version\":2}",
1821 "10.0 20.0 30.0",
1822 "90.0 90.0 90.0",
1823 "POSTBOX1",
1824 "POSTBOX2",
1825 "2",
1826 "1 1",
1827 "63.546 1.008",
1828 "Cu",
1829 "Coordinates of Component 1",
1830 "0.0 0.0 0.0 1.0 0",
1831 "H",
1832 "Coordinates of Component 2",
1833 "1.0 2.0 3.0 0.0 1",
1834 "",
1835 "Cu",
1836 "Velocities of Component 1",
1837 "0.1 0.2 0.3 1.0 0",
1838 "H",
1839 "Velocities of Component 2",
1840 "0.4 0.5 0.6 0.0 1",
1841 ];
1842 let mut line_it = lines.iter().copied().peekable();
1843 let mut frame =
1845 parse_single_frame(&mut line_it).expect("coordinate parsing should succeed");
1846 assert!(!frame.has_velocities());
1847
1848 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
1850 .expect("velocity parsing should succeed");
1851 assert!(has_vel);
1852 assert_eq!(frame.atom_data[0].velocity, Some([0.1, 0.2, 0.3]));
1853 assert_eq!(frame.atom_data[1].velocity, Some([0.4, 0.5, 0.6]));
1854 }
1855
1856 #[test]
1857 fn test_validate_true_accepts_matching_section_identity() {
1858 let text = r#"
1859PREBOX1
1860{"con_spec_version":2,"sections":["velocities"],"validate":true}
186110.0 20.0 30.0
186290.0 90.0 90.0
1863POSTBOX1
1864POSTBOX2
18652
18661 1
186763.546 1.008
1868Cu
1869Coordinates of Component 1
18700.0 0.0 0.0 5 0
1871H
1872Coordinates of Component 2
18731.0 2.0 3.0 0 1
1874
1875Cu
1876Velocities of Component 1
18770.1 0.2 0.3 5 0
1878H
1879Velocities of Component 2
18800.4 0.5 0.6 0 1
1881"#;
1882 let mut iter = ConFrameIterator::new(text.trim());
1883 let frame = iter.next().unwrap().unwrap();
1884
1885 assert!(frame.has_velocities());
1886 assert_eq!(
1887 frame
1888 .header
1889 .metadata
1890 .get("validate")
1891 .and_then(|v| v.as_bool()),
1892 Some(true)
1893 );
1894 }
1895
1896 #[test]
1897 fn test_validate_true_rejects_section_atom_id_mismatch() {
1898 let text = r#"
1899PREBOX1
1900{"con_spec_version":2,"sections":["velocities"],"validate":true}
190110.0 20.0 30.0
190290.0 90.0 90.0
1903POSTBOX1
1904POSTBOX2
19051
19061
190763.546
1908Cu
1909Coordinates of Component 1
19100.0 0.0 0.0 5 0
1911
1912Cu
1913Velocities of Component 1
19140.1 0.2 0.3 5 99
1915"#;
1916 let mut iter = ConFrameIterator::new(text.trim());
1917 let err = iter.next().unwrap().unwrap_err();
1918
1919 assert!(matches!(err, ParseError::ValidationError(_)));
1920 assert!(err.to_string().contains("atom_id mismatch"));
1921 }
1922
1923 #[test]
1924 fn test_validate_true_rejects_section_symbol_mismatch() {
1925 let text = r#"
1926PREBOX1
1927{"con_spec_version":2,"sections":["forces"],"validate":true}
192810.0 20.0 30.0
192990.0 90.0 90.0
1930POSTBOX1
1931POSTBOX2
19321
19331
193463.546
1935Cu
1936Coordinates of Component 1
19370.0 0.0 0.0 5 0
1938
1939H
1940Forces of Component 1
19410.1 0.2 0.3 5 0
1942"#;
1943 let mut iter = ConFrameIterator::new(text.trim());
1944 let err = iter.next().unwrap().unwrap_err();
1945
1946 assert!(matches!(err, ParseError::ValidationError(_)));
1947 assert!(err.to_string().contains("symbol mismatch"));
1948 }
1949
1950 #[test]
1951 fn test_validate_absent_allows_legacy_duplicate_identity_mismatch() {
1952 let text = r#"
1953PREBOX1
1954{"con_spec_version":2,"sections":["velocities"]}
195510.0 20.0 30.0
195690.0 90.0 90.0
1957POSTBOX1
1958POSTBOX2
19591
19601
196163.546
1962Cu
1963Coordinates of Component 1
19640.0 0.0 0.0 5 0
1965
1966Cu
1967Velocities of Component 1
19680.1 0.2 0.3 0 99
1969"#;
1970 let mut iter = ConFrameIterator::new(text.trim());
1971 let frame = iter.next().unwrap().unwrap();
1972
1973 assert!(frame.has_velocities());
1974 assert_eq!(frame.atom_data[0].atom_id, 0);
1975 assert_eq!(frame.atom_data[0].fixed, [true, false, true]);
1976 }
1977
1978 #[test]
1979 fn test_validate_must_be_boolean_when_present() {
1980 let lines = vec![
1981 "PREBOX1",
1982 "{\"con_spec_version\":2,\"validate\":\"yes\",\"sections\":[]}",
1983 "10.0 20.0 30.0",
1984 "90.0 90.0 90.0",
1985 "POSTBOX1",
1986 "POSTBOX2",
1987 "1",
1988 "1",
1989 "12.011",
1990 ];
1991 let mut line_it = lines.iter().copied();
1992 let err = parse_frame_header(&mut line_it).unwrap_err();
1993
1994 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1995 assert!(err.to_string().contains("validate"));
1996 }
1997
1998 #[test]
1999 fn test_validate_true_requires_sections_key() {
2000 let lines = vec![
2001 "PREBOX1",
2002 "{\"con_spec_version\":2,\"validate\":true}",
2003 "10.0 20.0 30.0",
2004 "90.0 90.0 90.0",
2005 "POSTBOX1",
2006 "POSTBOX2",
2007 "1",
2008 "1",
2009 "12.011",
2010 ];
2011 let mut line_it = lines.iter().copied();
2012 let err = parse_frame_header(&mut line_it).unwrap_err();
2013
2014 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2015 assert!(err.to_string().contains("sections"));
2016 }
2017
2018 #[test]
2019 fn test_sections_must_be_string_array_when_present() {
2020 let lines = vec![
2021 "PREBOX1",
2022 "{\"con_spec_version\":2,\"sections\":[\"velocities\",7]}",
2023 "10.0 20.0 30.0",
2024 "90.0 90.0 90.0",
2025 "POSTBOX1",
2026 "POSTBOX2",
2027 "1",
2028 "1",
2029 "12.011",
2030 ];
2031 let mut line_it = lines.iter().copied();
2032 let err = parse_frame_header(&mut line_it).unwrap_err();
2033
2034 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2035 assert!(err.to_string().contains("sections"));
2036 }
2037
2038 #[test]
2039 fn test_validate_true_rejects_non_integer_coordinate_identity_columns() {
2040 let text = r#"
2041PREBOX1
2042{"con_spec_version":2,"sections":[],"validate":true}
204310.0 20.0 30.0
204490.0 90.0 90.0
2045POSTBOX1
2046POSTBOX2
20471
20481
204963.546
2050Cu
2051Coordinates of Component 1
20520.0 0.0 0.0 5.0 0
2053"#;
2054 let mut iter = ConFrameIterator::new(text.trim());
2055 let err = iter.next().unwrap().unwrap_err();
2056
2057 assert!(matches!(err, ParseError::ValidationError(_)));
2058 assert!(err.to_string().contains("fixed_flag"));
2059 }
2060
2061 #[test]
2062 fn test_validate_true_rejects_non_exact_coordinate_label() {
2063 let text = r#"
2064PREBOX1
2065{"con_spec_version":2,"sections":[],"validate":true}
206610.0 20.0 30.0
206790.0 90.0 90.0
2068POSTBOX1
2069POSTBOX2
20701
20711
207263.546
2073Cu
2074Coordinates Component 1
20750.0 0.0 0.0 5 0
2076"#;
2077 let mut iter = ConFrameIterator::new(text.trim());
2078 let err = iter.next().unwrap().unwrap_err();
2079
2080 assert!(matches!(err, ParseError::ValidationError(_)));
2081 assert!(err.to_string().contains("Coordinates of Component 1"));
2082 }
2083
2084 #[test]
2085 fn test_validate_true_rejects_unknown_component_symbol() {
2086 let text = r#"
2087PREBOX1
2088{"con_spec_version":2,"sections":[],"validate":true}
208910.0 20.0 30.0
209090.0 90.0 90.0
2091POSTBOX1
2092POSTBOX2
20931
20941
209563.546
2096Qq
2097Coordinates of Component 1
20980.0 0.0 0.0 0 0
2099"#;
2100 let mut iter = ConFrameIterator::new(text.trim());
2101 let err = iter.next().unwrap().unwrap_err();
2102
2103 assert!(matches!(err, ParseError::ValidationError(_)));
2104 assert!(err.to_string().contains("symbol"));
2105 }
2106
2107 #[test]
2108 fn test_declared_section_must_be_present() {
2109 let text = r#"
2110PREBOX1
2111{"con_spec_version":2,"sections":["velocities"]}
211210.0 20.0 30.0
211390.0 90.0 90.0
2114POSTBOX1
2115POSTBOX2
21161
21171
211863.546
2119Cu
2120Coordinates of Component 1
21210.0 0.0 0.0 0 0
2122"#;
2123 let mut iter = ConFrameIterator::new(text.trim());
2124 let err = iter.next().unwrap().unwrap_err();
2125
2126 assert!(matches!(err, ParseError::IncompleteVelocitySection));
2127 }
2128
2129 #[test]
2130 fn test_non_finite_cell_geometry_rejected_in_strict_mode() {
2131 let lines = vec![
2132 "PREBOX1",
2133 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
2134 "10.0 NaN 30.0",
2135 "90.0 90.0 90.0",
2136 "POSTBOX1",
2137 "POSTBOX2",
2138 "1",
2139 "1",
2140 "12.011",
2141 ];
2142 let mut line_it = lines.iter().copied();
2143 let err = parse_frame_header(&mut line_it).unwrap_err();
2144
2145 assert!(matches!(err, ParseError::ValidationError(_)));
2146 assert!(err.to_string().contains("cell"));
2147 }
2148
2149 #[test]
2150 fn test_validate_true_rejects_non_physical_cell_geometry() {
2151 let lines = vec![
2152 "PREBOX1",
2153 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
2154 "0.0 20.0 30.0",
2155 "90.0 180.0 90.0",
2156 "POSTBOX1",
2157 "POSTBOX2",
2158 "1",
2159 "1",
2160 "12.011",
2161 ];
2162 let mut line_it = lines.iter().copied();
2163 let err = parse_frame_header(&mut line_it).unwrap_err();
2164
2165 assert!(matches!(err, ParseError::ValidationError(_)));
2166 assert!(err.to_string().contains("cell"));
2167 }
2168
2169 #[test]
2170 fn test_validate_true_rejects_reserved_metadata_type_mismatch() {
2171 let lines = vec![
2172 "PREBOX1",
2173 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"energy\":\"low\"}",
2174 "10.0 20.0 30.0",
2175 "90.0 90.0 90.0",
2176 "POSTBOX1",
2177 "POSTBOX2",
2178 "1",
2179 "1",
2180 "12.011",
2181 ];
2182 let mut line_it = lines.iter().copied();
2183 let err = parse_frame_header(&mut line_it).unwrap_err();
2184
2185 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2186 assert!(err.to_string().contains("energy"));
2187 }
2188
2189 #[test]
2190 fn test_validate_true_rejects_malformed_bonds() {
2191 let lines = vec![
2192 "PREBOX1",
2193 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"bonds\":[[0]]}",
2194 "10.0 20.0 30.0",
2195 "90.0 90.0 90.0",
2196 "POSTBOX1",
2197 "POSTBOX2",
2198 "1",
2199 "1",
2200 "12.011",
2201 ];
2202 let mut line_it = lines.iter().copied();
2203 let err = parse_frame_header(&mut line_it).unwrap_err();
2204 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2205 assert!(err.to_string().contains("bonds"));
2206 }
2207
2208 #[test]
2209 fn test_bonds_metadata_round_trip_in_header() {
2210 use crate::types::{meta, Bond};
2211 let lines = vec![
2212 "PREBOX1",
2213 "{\"con_spec_version\":2,\"bonds\":[[0,1],{\"i\":0,\"j\":2,\"order\":1}]}",
2214 "10.0 20.0 30.0",
2215 "90.0 90.0 90.0",
2216 "POSTBOX1",
2217 "POSTBOX2",
2218 "1",
2219 "1",
2220 "12.011",
2221 ];
2222 let mut line_it = lines.iter().copied();
2223 let header = parse_frame_header(&mut line_it).expect("header");
2224 let bonds = header.bonds();
2225 assert_eq!(bonds.len(), 2);
2226 assert_eq!(bonds[0], Bond::new(0, 1));
2227 assert_eq!(bonds[1].i, 0);
2228 assert_eq!(bonds[1].j, 2);
2229 assert_eq!(bonds[1].order, Some(1));
2230 assert!(header.metadata.contains_key(meta::BONDS));
2231 }
2232
2233 #[test]
2234 fn test_parse_velocity_section_absent() {
2235 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 "1",
2243 "1",
2244 "12.011",
2245 "C",
2246 "Coordinates of Component 1",
2247 "0.0 0.0 0.0 0.0 1",
2248 ];
2249 let mut line_it = lines.iter().copied().peekable();
2250 let mut frame = parse_single_frame(&mut line_it).expect("parse should succeed");
2251 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
2252 .expect("should succeed with no velocities");
2253 assert!(!has_vel);
2254 assert_eq!(frame.atom_data[0].velocity, None);
2255 }
2256
2257 #[test]
2258 fn test_parse_line_of_range_f64_exact() {
2259 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0 42", 4, 5, &[0.0; 5]).unwrap();
2260 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 42.0]);
2261 }
2262
2263 #[test]
2264 fn test_byte_scan_stack_parses_realistic_coord_line() {
2265 let line = " 0.63939999999999997 0.90449999999999997 -0.00009999999999977 1 0";
2267 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
2268 let mut buf = [0.0f64; 5];
2269 let n = parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
2270 assert_eq!(n, 5);
2271 assert!((buf[0] - 0.6394).abs() < 1e-4);
2272 assert!((buf[1] - 0.9045).abs() < 1e-4);
2273 assert_eq!(buf[3] as u8, 1);
2274 assert_eq!(buf[4] as u64, 0);
2275 let bad = "1.0 2.0 3.0 1 0 EXTRA";
2277 assert!(parse_line_of_range_f64_stack(bad, 4, 5, &defaults, &mut buf).is_err());
2278 let glued = "1.0 2.0 3.0abc 1 0";
2280 assert!(parse_line_of_range_f64_stack(glued, 4, 5, &defaults, &mut buf).is_err());
2281 }
2282
2283 #[test]
2284 fn test_parse_line_of_range_f64_stack_matches_vec_api() {
2285 let defaults = [0.0, 0.0, 0.0, 0.0, 7.0];
2286 let line = "1.0 2.0 3.0 0.0";
2287 let mut buf = [0.0f64; 5];
2288 parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
2289 let via_vec = parse_line_of_range_f64(line, 4, 5, &defaults).unwrap();
2290 assert_eq!(&buf[..5], via_vec.as_slice());
2291 assert_eq!(buf[4], 7.0);
2292 }
2293
2294 #[test]
2295 fn test_parse_line_of_range_f64_padded() {
2296 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
2297 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0", 4, 5, &defaults).unwrap();
2298 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 99.0]);
2299 }
2300
2301 #[test]
2302 fn test_parse_line_of_range_f64_too_few() {
2303 let result = parse_line_of_range_f64("1.0 2.0 3.0", 4, 5, &[0.0; 5]);
2304 assert!(result.is_err());
2305 }
2306
2307 #[test]
2308 fn test_parse_line_of_range_f64_too_many() {
2309 let result = parse_line_of_range_f64("1.0 2.0 3.0 0.0 5.0 6.0", 4, 5, &[0.0; 5]);
2310 assert!(result.is_err());
2311 }
2312
2313 #[test]
2314 fn test_parse_all_four_column_lines() {
2315 let lines = vec![
2317 "PREBOX1",
2318 "{\"con_spec_version\":2}",
2319 "10.0 10.0 10.0",
2320 "90.0 90.0 90.0",
2321 "POSTBOX1",
2322 "POSTBOX2",
2323 "1",
2324 "3",
2325 "12.011",
2326 "C",
2327 "Coordinates of Component 1",
2328 "0.0 0.0 0.0 0",
2329 "1.0 0.0 0.0 0",
2330 "2.0 0.0 0.0 1",
2331 ];
2332 let mut line_it = lines.iter().copied();
2333 let frame = parse_single_frame(&mut line_it).unwrap();
2334 assert_eq!(frame.atom_data[0].atom_id, 0);
2335 assert_eq!(frame.atom_data[1].atom_id, 1);
2336 assert_eq!(frame.atom_data[2].atom_id, 2);
2337 assert!(frame.atom_data[2].is_fixed());
2338 }
2339}