1use crate::error::ParseError;
2use crate::helpers::symbol_to_atomic_number;
3use crate::types::{
4 AtomDatum, ConFrame, FrameHeader, PreboxHeader, SECTION_FORCES, SECTION_VELOCITIES,
5 decode_fixed_bitmask, meta,
6};
7use serde_json::Value;
8use std::collections::BTreeMap;
9use std::iter::Peekable;
10use std::sync::Arc;
11
12pub fn parse_line_of_n_f64(line: &str, n: usize) -> Result<Vec<f64>, ParseError> {
23 let mut values = Vec::with_capacity(n);
24 for token in line.split_ascii_whitespace() {
25 let val: f64 = fast_float2::parse(token)
26 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
27 values.push(val);
28 }
29 if values.len() == n {
30 Ok(values)
31 } else {
32 Err(ParseError::InvalidVectorLength {
33 expected: n,
34 found: values.len(),
35 })
36 }
37}
38
39pub fn parse_line_of_range_f64(
45 line: &str,
46 min: usize,
47 max: usize,
48 defaults: &[f64],
49) -> Result<Vec<f64>, ParseError> {
50 let mut values = Vec::with_capacity(max);
51 for token in line.split_ascii_whitespace() {
52 let val: f64 = fast_float2::parse(token)
53 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
54 values.push(val);
55 }
56 if values.len() < min || values.len() > max {
57 return Err(ParseError::InvalidVectorLength {
58 expected: max,
59 found: values.len(),
60 });
61 }
62 while values.len() < max {
64 let idx = values.len();
65 values.push(defaults[idx]);
66 }
67 Ok(values)
68}
69
70fn metadata_json_error(message: impl Into<String>) -> ParseError {
71 ParseError::InvalidMetadataJson(message.into())
72}
73
74fn validate_metadata_number(key: &str, value: &Value) -> Result<(), ParseError> {
75 if value.as_f64().is_some() {
76 Ok(())
77 } else {
78 Err(metadata_json_error(format!(
79 "{key} must be a finite number"
80 )))
81 }
82}
83
84fn validate_metadata_integer(key: &str, value: &Value) -> Result<(), ParseError> {
85 if value.as_u64().is_some() {
86 Ok(())
87 } else {
88 Err(metadata_json_error(format!(
89 "{key} must be a non-negative integer"
90 )))
91 }
92}
93
94pub fn validate_metadata_schema(
102 json_obj: &serde_json::Map<String, Value>,
103) -> Result<(), ParseError> {
104 let strict_requested = match json_obj.get(meta::VALIDATE) {
105 Some(Value::Bool(b)) => *b,
106 Some(_) => return Err(metadata_json_error("validate must be a boolean")),
107 None => false,
108 };
109
110 match json_obj.get(meta::SECTIONS) {
111 Some(Value::Array(values)) => {
112 if values.iter().any(|entry| !entry.is_string()) {
113 return Err(metadata_json_error("sections must be an array of strings"));
114 }
115 }
116 Some(_) => return Err(metadata_json_error("sections must be an array of strings")),
117 None if strict_requested => {
118 return Err(metadata_json_error(
119 "validate=true requires a sections array, even when empty",
120 ));
121 }
122 None => {}
123 }
124
125 for (key, value) in json_obj {
126 match key.as_str() {
127 meta::CON_SPEC_VERSION | meta::SECTIONS | meta::VALIDATE => {}
128 meta::ENERGY
129 | meta::TIME
130 | meta::TIMESTEP
131 | meta::CONVERGENCE_FMAX
132 | meta::CONVERGENCE_ENERGY
133 | meta::FMAX => validate_metadata_number(key, value)?,
134 meta::FRAME_INDEX | meta::NEB_BEAD | meta::NEB_BAND => {
135 validate_metadata_integer(key, value)?
136 }
137 meta::GENERATOR if !value.is_string() => {
138 return Err(metadata_json_error("generator must be a string"));
139 }
140 meta::GENERATOR => {}
141 meta::UNITS | meta::POTENTIAL if !value.is_object() => {
142 return Err(metadata_json_error(format!("{key} must be an object")));
143 }
144 meta::POTENTIAL => {
145 if let Some(potential_type) = value.get("type")
146 && !potential_type.is_string()
147 {
148 return Err(metadata_json_error("potential.type must be a string"));
149 }
150 }
151 meta::UNITS => {}
152 meta::PBC => validate_pbc_metadata(value)?,
153 meta::LATTICE_VECTORS => validate_lattice_vectors_metadata(value)?,
154 meta::CONVERGED if !value.is_boolean() => {
155 return Err(metadata_json_error("converged must be a boolean"));
156 }
157 meta::CONVERGED => {}
158 _ => {}
159 }
160 }
161
162 Ok(())
163}
164
165fn validate_pbc_metadata(value: &Value) -> Result<(), ParseError> {
166 let Some(values) = value.as_array() else {
167 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
168 };
169 if values.len() != 3 || values.iter().any(|entry| !entry.is_boolean()) {
170 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
171 }
172 Ok(())
173}
174
175fn validate_lattice_vectors_metadata(value: &Value) -> Result<(), ParseError> {
176 let Some(rows) = value.as_array() else {
177 return Err(metadata_json_error(
178 "lattice_vectors must be a 3x3 numeric array",
179 ));
180 };
181 if rows.len() != 3 {
182 return Err(metadata_json_error(
183 "lattice_vectors must be a 3x3 numeric array",
184 ));
185 }
186 for row in rows {
187 let Some(entries) = row.as_array() else {
188 return Err(metadata_json_error(
189 "lattice_vectors must be a 3x3 numeric array",
190 ));
191 };
192 if entries.len() != 3 || entries.iter().any(|entry| entry.as_f64().is_none()) {
193 return Err(metadata_json_error(
194 "lattice_vectors must be a 3x3 numeric array",
195 ));
196 }
197 }
198 Ok(())
199}
200
201pub fn parse_line_of_n<T: std::str::FromStr>(line: &str, n: usize) -> Result<Vec<T>, ParseError>
229where
230 ParseError: From<<T as std::str::FromStr>::Err>,
231{
232 let values: Vec<T> = line
233 .split_whitespace()
234 .map(|s| s.parse::<T>())
235 .collect::<Result<_, _>>()?;
236
237 if values.len() == n {
238 Ok(values)
239 } else {
240 Err(ParseError::InvalidVectorLength {
241 expected: n,
242 found: values.len(),
243 })
244 }
245}
246
247pub fn parse_frame_header<'a>(
268 lines: &mut impl Iterator<Item = &'a str>,
269) -> Result<FrameHeader, ParseError> {
270 let prebox1 = lines
271 .next()
272 .ok_or(ParseError::IncompleteHeader)?
273 .to_string();
274 let prebox2_raw = lines.next().ok_or(ParseError::IncompleteHeader)?;
275
276 let trimmed = prebox2_raw.trim();
279 let (spec_version, metadata, sections, validate, sections_declared) = if trimmed.starts_with('{') {
280 let json_val: serde_json::Value = serde_json::from_str(trimmed)
281 .map_err(|e| ParseError::InvalidMetadataJson(e.to_string()))?;
282 let json_obj = json_val
283 .as_object()
284 .ok_or_else(|| ParseError::InvalidMetadataJson("expected a JSON object".to_string()))?;
285 let ver = json_obj
286 .get(meta::CON_SPEC_VERSION)
287 .and_then(|v| v.as_u64())
288 .ok_or(ParseError::MissingSpecVersion)? as u32;
289 if ver > crate::CON_SPEC_VERSION {
290 return Err(ParseError::UnsupportedSpecVersion(ver));
291 }
292
293 let mut sections: Vec<String> = Vec::new();
297 let mut metadata = BTreeMap::new();
298 let mut sections_declared = false;
299 let mut validate = false;
300 for (k, v) in json_obj {
301 match k.as_str() {
302 meta::CON_SPEC_VERSION => {}
303 meta::SECTIONS => {
304 sections_declared = true;
305 let arr = v.as_array().ok_or_else(|| {
306 metadata_json_error("sections must be an array of strings")
307 })?;
308 sections.reserve(arr.len());
309 for entry in arr {
310 let s = entry.as_str().ok_or_else(|| {
311 metadata_json_error("sections must be an array of strings")
312 })?;
313 sections.push(s.to_string());
314 }
315 }
316 meta::VALIDATE => {
317 validate = match v {
318 Value::Bool(b) => *b,
319 _ => return Err(metadata_json_error("validate must be a boolean")),
320 };
321 metadata.insert(k.clone(), v.clone());
322 }
323 _ => {
324 metadata.insert(k.clone(), v.clone());
325 }
326 }
327 }
328
329 if validate {
332 validate_metadata_schema(json_obj)?;
333 }
334
335 (ver, metadata, sections, validate, sections_declared)
336 } else {
337 (1_u32, BTreeMap::new(), Vec::new(), false, false)
339 };
340 let prebox2 = prebox2_raw.to_string();
341
342 let boxl_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
343 let angles_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
344 let postbox1 = lines
345 .next()
346 .ok_or(ParseError::IncompleteHeader)?
347 .to_string();
348 let postbox2 = lines
349 .next()
350 .ok_or(ParseError::IncompleteHeader)?
351 .to_string();
352 let natm_types =
353 parse_line_of_n::<usize>(lines.next().ok_or(ParseError::IncompleteHeader)?, 1)?[0];
354 let natms_per_type = parse_line_of_n::<usize>(
355 lines.next().ok_or(ParseError::IncompleteHeader)?,
356 natm_types,
357 )?;
358 let masses_per_type = parse_line_of_n_f64(
359 lines.next().ok_or(ParseError::IncompleteHeader)?,
360 natm_types,
361 )?;
362 if validate {
363 validate_header_geometry(&boxl_vec, &angles_vec, natm_types, &natms_per_type)?;
364 validate_masses(&masses_per_type)?;
365 }
366 Ok(FrameHeader {
367 prebox_header: PreboxHeader {
368 user: prebox1,
369 metadata_line: prebox2,
370 },
371 boxl: boxl_vec.try_into().unwrap(),
372 angles: angles_vec.try_into().unwrap(),
373 postbox_header: [postbox1, postbox2],
374 natm_types,
375 natms_per_type,
376 masses_per_type,
377 spec_version,
378 metadata,
379 sections,
380 strict_validation: validate,
381 sections_declared,
382 })
383}
384
385pub fn parse_single_frame<'a>(
434 lines: &mut impl Iterator<Item = &'a str>,
435) -> Result<ConFrame, ParseError> {
436 let header = parse_frame_header(lines)?;
437 let validate = header.strict_validation;
438 let total_atoms: usize = header.natms_per_type.iter().sum();
439 let mut atom_data = Vec::with_capacity(total_atoms);
440
441 let mut global_atom_idx: u64 = 0;
442 for (type_idx, num_atoms) in header.natms_per_type.iter().enumerate() {
443 let symbol_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
447 let symbol: Arc<str> = Arc::from(symbol_line.trim());
448 let coord_label = lines.next().ok_or(ParseError::IncompleteFrame)?;
449 if validate {
450 validate_coordinate_component(type_idx, symbol.as_ref(), coord_label)?;
451 }
452 for _ in 0..*num_atoms {
453 let coord_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
454 let defaults = [0.0, 0.0, 0.0, 0.0, global_atom_idx as f64];
456 let vals = parse_line_of_range_f64(coord_line, 4, 5, &defaults)?;
457 let (fixed, atom_id) = if validate {
458 parse_identity_columns(coord_line, "coordinate")?
459 } else {
460 (decode_fixed_bitmask(vals[3] as u8), vals[4] as u64)
461 };
462 atom_data.push(AtomDatum {
463 symbol: Arc::clone(&symbol),
465 x: vals[0],
466 y: vals[1],
467 z: vals[2],
468 fixed,
469 atom_id,
470 velocity: None,
471 force: None,
472 });
473 global_atom_idx += 1;
474 }
475 }
476 Ok(ConFrame { header, atom_data })
477}
478
479fn validate_header_geometry(
480 boxl: &[f64],
481 angles: &[f64],
482 natm_types: usize,
483 natms_per_type: &[usize],
484) -> Result<(), ParseError> {
485 if boxl.iter().any(|length| !length.is_finite() || *length <= 0.0)
486 || angles
487 .iter()
488 .any(|angle| !angle.is_finite() || *angle <= 0.0 || *angle >= 180.0)
489 {
490 return Err(ParseError::ValidationError(
491 "cell geometry must have positive lengths and angles between 0 and 180 degrees"
492 .to_string(),
493 ));
494 }
495 if natm_types == 0 || natms_per_type.contains(&0) {
496 return Err(ParseError::ValidationError(
497 "atom counts must contain at least one atom per component".to_string(),
498 ));
499 }
500 Ok(())
501}
502
503fn validate_masses(masses_per_type: &[f64]) -> Result<(), ParseError> {
504 if masses_per_type
505 .iter()
506 .any(|mass| !mass.is_finite() || *mass <= 0.0)
507 {
508 return Err(ParseError::ValidationError(
509 "component masses must be positive".to_string(),
510 ));
511 }
512 Ok(())
513}
514
515fn validate_coordinate_component(
516 type_idx: usize,
517 symbol: &str,
518 label: &str,
519) -> Result<(), ParseError> {
520 let expected_label = format!("Coordinates of Component {}", type_idx + 1);
521 if label.trim() != expected_label {
522 return Err(ParseError::ValidationError(format!(
523 "expected coordinate label {expected_label:?}, found {label:?}"
524 )));
525 }
526 if symbol != "X" && symbol_to_atomic_number(symbol) == 0 {
527 return Err(ParseError::ValidationError(format!(
528 "unknown component symbol {symbol}"
529 )));
530 }
531 Ok(())
532}
533
534fn parse_identity_columns(line: &str, row_kind: &str) -> Result<([bool; 3], u64), ParseError> {
535 let columns = line.split_ascii_whitespace().collect::<Vec<_>>();
536 if columns.len() != 5 {
537 return Err(ParseError::ValidationError(format!(
538 "{row_kind} rows require fixed_flag and atom_id columns in validate mode"
539 )));
540 }
541 let fixed_flag = columns[3].parse::<u8>().map_err(|_| {
542 ParseError::ValidationError(format!("{row_kind} fixed_flag must be an integer bitmask"))
543 })?;
544 if fixed_flag > 7 {
545 return Err(ParseError::ValidationError(format!(
546 "{row_kind} fixed_flag must be between 0 and 7"
547 )));
548 }
549 let atom_id = columns[4].parse::<u64>().map_err(|_| {
550 ParseError::ValidationError(format!("{row_kind} atom_id must be an integer"))
551 })?;
552 Ok((decode_fixed_bitmask(fixed_flag), atom_id))
553}
554
555fn validate_section_component(
556 section: &str,
557 type_idx: usize,
558 atom_idx: usize,
559 symbol: &str,
560 label: &str,
561 header: &FrameHeader,
562 atom_data: &[AtomDatum],
563) -> Result<(), ParseError> {
564 let expected_label = format!("{section} of Component {}", type_idx + 1);
565 if label.trim() != expected_label {
566 return Err(ParseError::ValidationError(format!(
567 "expected section label {expected_label:?}, found {label:?}"
568 )));
569 }
570
571 if header.natms_per_type[type_idx] == 0 {
572 return Ok(());
573 }
574
575 let expected_symbol = atom_data
576 .get(atom_idx)
577 .map(|atom| atom.symbol.as_ref())
578 .ok_or_else(|| {
579 ParseError::ValidationError(format!(
580 "{section} component {} has no coordinate atom to validate against",
581 type_idx + 1
582 ))
583 })?;
584 if symbol != expected_symbol {
585 return Err(ParseError::ValidationError(format!(
586 "{section} component {} symbol mismatch: expected {expected_symbol}, found {symbol}",
587 type_idx + 1
588 )));
589 }
590
591 Ok(())
592}
593
594fn validate_section_atom_identity(
595 section: &str,
596 atom_idx: usize,
597 fixed: [bool; 3],
598 atom_id: u64,
599 atom_data: &[AtomDatum],
600) -> Result<(), ParseError> {
601 let atom = atom_data.get(atom_idx).ok_or_else(|| {
602 ParseError::ValidationError(format!(
603 "{section} row {atom_idx} has no coordinate atom to validate against"
604 ))
605 })?;
606
607 if atom.fixed != fixed {
608 return Err(ParseError::ValidationError(format!(
609 "{section} row {atom_idx} fixed mask mismatch for atom_id {}",
610 atom.atom_id
611 )));
612 }
613 if atom.atom_id != atom_id {
614 return Err(ParseError::ValidationError(format!(
615 "{section} row {atom_idx} atom_id mismatch: expected {}, found {atom_id}",
616 atom.atom_id
617 )));
618 }
619
620 Ok(())
621}
622
623pub fn parse_velocity_section<'a, I>(
636 lines: &mut Peekable<I>,
637 header: &FrameHeader,
638 atom_data: &mut [AtomDatum],
639) -> Result<bool, ParseError>
640where
641 I: Iterator<Item = &'a str>,
642{
643 let validate = header.strict_validation;
644 match lines.peek() {
646 Some(line) if line.trim().is_empty() => {
647 lines.next();
649 }
650 _ => return Ok(false),
651 }
652
653 let mut atom_idx: usize = 0;
654 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
655 let symbol = lines
657 .next()
658 .ok_or(ParseError::IncompleteVelocitySection)?
659 .trim();
660
661 let comp_line = lines.next().ok_or(ParseError::IncompleteVelocitySection)?;
663 if !comp_line.contains("Velocities of Component") {
665 return Err(ParseError::IncompleteVelocitySection);
666 }
667 if validate {
668 validate_section_component(
669 "Velocities",
670 type_idx,
671 atom_idx,
672 symbol,
673 comp_line,
674 header,
675 atom_data,
676 )?;
677 }
678
679 for _ in 0..num_atoms {
680 let vel_line = lines.next().ok_or(ParseError::IncompleteVelocitySection)?;
681 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
683 let vals = parse_line_of_range_f64(vel_line, 4, 5, &defaults)?;
684 if validate {
685 let (fixed, atom_id) = parse_identity_columns(vel_line, "velocities")?;
686 validate_section_atom_identity("velocities", atom_idx, fixed, atom_id, atom_data)?;
687 }
688 if atom_idx < atom_data.len() {
689 atom_data[atom_idx].velocity = Some([vals[0], vals[1], vals[2]]);
690 }
691 atom_idx += 1;
692 }
693 }
694
695 Ok(true)
696}
697
698pub fn parse_force_section<'a, I>(
706 lines: &mut Peekable<I>,
707 header: &FrameHeader,
708 atom_data: &mut [AtomDatum],
709) -> Result<bool, ParseError>
710where
711 I: Iterator<Item = &'a str>,
712{
713 let validate = header.strict_validation;
714 match lines.peek() {
716 Some(line) if line.trim().is_empty() => {
717 lines.next();
718 }
719 _ => return Ok(false),
720 }
721
722 let mut atom_idx: usize = 0;
723 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
724 let symbol = lines
725 .next()
726 .ok_or(ParseError::IncompleteForceSection)?
727 .trim();
728
729 let comp_line = lines.next().ok_or(ParseError::IncompleteForceSection)?;
730 if !comp_line.contains("Forces of Component") {
731 return Err(ParseError::IncompleteForceSection);
732 }
733 if validate {
734 validate_section_component(
735 "Forces", type_idx, atom_idx, symbol, comp_line, header, atom_data,
736 )?;
737 }
738
739 for _ in 0..num_atoms {
740 let force_line = lines.next().ok_or(ParseError::IncompleteForceSection)?;
741 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
742 let vals = parse_line_of_range_f64(force_line, 4, 5, &defaults)?;
743 if validate {
744 let (fixed, atom_id) = parse_identity_columns(force_line, "forces")?;
745 validate_section_atom_identity("forces", atom_idx, fixed, atom_id, atom_data)?;
746 }
747 if atom_idx < atom_data.len() {
748 atom_data[atom_idx].force = Some([vals[0], vals[1], vals[2]]);
749 }
750 atom_idx += 1;
751 }
752 }
753
754 Ok(true)
755}
756
757pub fn parse_declared_sections<'a, I>(
763 lines: &mut Peekable<I>,
764 header: &mut FrameHeader,
765 atom_data: &mut [AtomDatum],
766) -> Result<(), ParseError>
767where
768 I: Iterator<Item = &'a str>,
769{
770 if !header.sections_declared && header.sections.is_empty() {
771 let found = parse_velocity_section(lines, header, atom_data)?;
773 if found {
774 header.sections.push(SECTION_VELOCITIES.into());
775 }
776 } else {
777 let sections = std::mem::take(&mut header.sections);
778 for section in §ions {
779 match section.as_str() {
780 SECTION_VELOCITIES => {
781 let found = parse_velocity_section(lines, header, atom_data)?;
782 if !found {
783 return Err(ParseError::IncompleteVelocitySection);
784 }
785 }
786 SECTION_FORCES => {
787 let found = parse_force_section(lines, header, atom_data)?;
788 if !found {
789 return Err(ParseError::IncompleteForceSection);
790 }
791 }
792 other => return Err(ParseError::UnknownSection(other.to_string())),
793 }
794 }
795 header.sections = sections;
796 }
797 Ok(())
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803 use crate::iterators::ConFrameIterator;
804
805 #[test]
806 fn test_parse_line_of_n_success() {
807 let line = "1.0 2.5 -3.0";
808 let values = parse_line_of_n::<f64>(line, 3).unwrap();
809 assert_eq!(values, vec![1.0, 2.5, -3.0]);
810 }
811
812 #[test]
813 fn test_parse_line_of_n_too_short() {
814 let line = "1.0 2.5";
815 let result = parse_line_of_n::<f64>(line, 3);
816 assert!(result.is_err());
817 assert!(matches!(
818 result.unwrap_err(),
819 ParseError::InvalidVectorLength {
820 expected: 3,
821 found: 2
822 }
823 ));
824 }
825
826 #[test]
827 fn test_parse_line_of_n_too_long() {
828 let line = "1.0 2.5 -3.0 4.0";
829 let result = parse_line_of_n::<f64>(line, 3);
830 assert!(result.is_err());
831 assert!(matches!(
832 result.unwrap_err(),
833 ParseError::InvalidVectorLength {
834 expected: 3,
835 found: 4
836 }
837 ));
838 }
839
840 #[test]
841 fn test_parse_line_of_n_invalid_float() {
842 let line = "1.0 abc -3.0";
843 let result = parse_line_of_n::<f64>(line, 3);
844 assert!(result.is_err());
845 assert!(matches!(
846 result.unwrap_err(),
847 ParseError::InvalidNumberFormat(_)
848 ));
849 }
850
851 #[test]
852 fn test_parse_frame_header_success() {
853 let lines = [
854 "PREBOX1",
855 "{\"con_spec_version\":2}",
856 "10.0 20.0 30.0",
857 "90.0 90.0 90.0",
858 "POSTBOX1",
859 "POSTBOX2",
860 "2",
861 "1 1",
862 "12.011 1.008",
863 ];
864 let mut line_it = lines.iter().copied();
865 match parse_frame_header(&mut line_it) {
866 Ok(header) => {
867 assert_eq!(header.prebox_header.user, "PREBOX1");
868 assert_eq!(header.spec_version, 2);
869 assert_eq!(header.boxl, [10.0, 20.0, 30.0]);
870 assert_eq!(header.angles, [90.0, 90.0, 90.0]);
871 assert_eq!(header.postbox_header, ["POSTBOX1", "POSTBOX2"]);
872 assert_eq!(header.natm_types, 2);
873 assert_eq!(header.natms_per_type, vec![1, 1]);
874 assert_eq!(header.masses_per_type, vec![12.011, 1.008]);
875 }
876 Err(e) => {
877 panic!(
878 "Parsing failed when it should have succeeded. Error: {:?}",
879 e
880 );
881 }
882 }
883 }
884
885 #[test]
886 fn test_parse_frame_header_missing_line() {
887 let lines = [
888 "PREBOX1",
889 "{\"con_spec_version\":2}",
890 "10.0 20.0 30.0",
891 "90.0 90.0 90.0",
892 "POSTBOX1",
893 "POSTBOX2",
894 "2",
895 "1 1",
896 ];
898 let mut line_it = lines.iter().copied();
899 let result = parse_frame_header(&mut line_it);
900 assert!(result.is_err());
901 assert!(matches!(result.unwrap_err(), ParseError::IncompleteHeader));
902 }
903
904 #[test]
905 fn test_parse_frame_header_missing_spec_version() {
906 let lines = vec![
907 "PREBOX1",
908 "{}",
909 "10.0 20.0 30.0",
910 "90.0 90.0 90.0",
911 "POSTBOX1",
912 "POSTBOX2",
913 "2",
914 "1 1",
915 "12.011 1.008",
916 ];
917 let mut line_it = lines.iter().copied();
918 let result = parse_frame_header(&mut line_it);
919 assert!(result.is_err());
920 assert!(matches!(
921 result.unwrap_err(),
922 ParseError::MissingSpecVersion
923 ));
924 }
925
926 #[test]
927 fn test_parse_frame_header_legacy_no_json() {
928 let lines = vec![
930 "PREBOX1",
931 "0.0000 TIME",
932 "10.0 20.0 30.0",
933 "90.0 90.0 90.0",
934 "POSTBOX1",
935 "POSTBOX2",
936 "2",
937 "1 1",
938 "12.011 1.008",
939 ];
940 let mut line_it = lines.iter().copied();
941 let header = parse_frame_header(&mut line_it).unwrap();
942 assert_eq!(header.spec_version, 1);
943 assert!(header.metadata.is_empty());
944 }
945
946 #[test]
947 fn test_parse_frame_header_malformed_json() {
948 let lines = vec![
950 "PREBOX1",
951 "{broken json",
952 "10.0 20.0 30.0",
953 "90.0 90.0 90.0",
954 "POSTBOX1",
955 "POSTBOX2",
956 "2",
957 "1 1",
958 "12.011 1.008",
959 ];
960 let mut line_it = lines.iter().copied();
961 let result = parse_frame_header(&mut line_it);
962 assert!(result.is_err());
963 assert!(matches!(
964 result.unwrap_err(),
965 ParseError::InvalidMetadataJson(_)
966 ));
967 }
968
969 #[test]
970 fn test_parse_frame_header_unsupported_version() {
971 let lines = vec![
972 "PREBOX1",
973 "{\"con_spec_version\":999}",
974 "10.0 20.0 30.0",
975 "90.0 90.0 90.0",
976 "POSTBOX1",
977 "POSTBOX2",
978 "2",
979 "1 1",
980 "12.011 1.008",
981 ];
982 let mut line_it = lines.iter().copied();
983 let result = parse_frame_header(&mut line_it);
984 assert!(result.is_err());
985 assert!(matches!(
986 result.unwrap_err(),
987 ParseError::UnsupportedSpecVersion(999)
988 ));
989 }
990
991 #[test]
992 fn test_parse_frame_header_extra_metadata_preserved() {
993 let lines = vec![
994 "PREBOX1",
995 "{\"con_spec_version\":2,\"generator\":\"test\"}",
996 "10.0 20.0 30.0",
997 "90.0 90.0 90.0",
998 "POSTBOX1",
999 "POSTBOX2",
1000 "2",
1001 "1 1",
1002 "12.011 1.008",
1003 ];
1004 let mut line_it = lines.iter().copied();
1005 let header = parse_frame_header(&mut line_it).unwrap();
1006 assert_eq!(header.spec_version, 2);
1007 assert_eq!(
1008 header.metadata.get("generator"),
1009 Some(&serde_json::Value::String("test".to_string()))
1010 );
1011 }
1012
1013 #[test]
1014 fn test_parse_frame_header_invalid_natms_per_type() {
1015 let lines = vec![
1016 "PREBOX1",
1017 "{\"con_spec_version\":2}",
1018 "10.0 20.0 30.0",
1019 "90.0 90.0 90.0",
1020 "POSTBOX1",
1021 "POSTBOX2",
1022 "2",
1023 "1 1 1", "12.011 1.008",
1025 ];
1026 let mut line_it = lines.iter().copied();
1027 let result = parse_frame_header(&mut line_it);
1028 assert!(result.is_err());
1029 assert!(matches!(
1030 result.unwrap_err(),
1031 ParseError::InvalidVectorLength {
1032 expected: 2,
1033 found: 3
1034 }
1035 ));
1036 }
1037
1038 #[test]
1039 fn test_parse_single_frame_success() {
1040 let lines = vec![
1041 "PREBOX1",
1042 "{\"con_spec_version\":2}",
1043 "10.0 20.0 30.0",
1044 "90.0 90.0 90.0",
1045 "POSTBOX1",
1046 "POSTBOX2",
1047 "2",
1048 "3 3",
1049 "12.011 1.008",
1050 "1",
1051 "Coordinates of Component 1",
1052 "0.0 0.0 0.0 0.0 1",
1053 "1.0940 0.0 0.0 0.0 2",
1054 "-0.5470 0.9499 0.0 0.0 3",
1055 "2",
1056 "Coordinates of Component 2",
1057 "5.0 5.0 5.0 0.0 4",
1058 "6.0940 5.0 5.0 0.0 5",
1059 "5.5470 5.9499 5.0 0.0 6",
1060 ];
1061 let mut line_it = lines.iter().copied();
1062 let frame = parse_single_frame(&mut line_it).unwrap();
1063
1064 assert_eq!(frame.header.natm_types, 2);
1065 assert_eq!(frame.header.natms_per_type, vec![3, 3]);
1066 assert_eq!(frame.header.masses_per_type, vec![12.011, 1.008]);
1067 assert_eq!(frame.atom_data.len(), 6);
1068 assert_eq!(&*frame.atom_data[0].symbol, "1");
1069 assert_eq!(frame.atom_data[0].atom_id, 1);
1070 assert_eq!(&*frame.atom_data[5].symbol, "2");
1071 assert_eq!(frame.atom_data[5].atom_id, 6);
1072 }
1073
1074 #[test]
1075 fn test_parse_single_frame_missing_line() {
1076 let lines = vec![
1078 "PREBOX1",
1079 "{\"con_spec_version\":2}",
1080 "10.0 20.0 30.0",
1081 "90.0 90.0 90.0",
1082 "POSTBOX1",
1083 "POSTBOX2",
1084 "2",
1085 "3 3",
1086 "12.011 1.008",
1087 "1",
1088 "Coordinates of Component 1",
1089 "0.0 0.0 0.0 0.0 1",
1090 "1.0940 0.0 0.0 0.0 2",
1091 "-0.5470 0.9499 0.0 0.0 3",
1092 ];
1094 let mut line_it = lines.iter().copied();
1095 let result = parse_single_frame(&mut line_it);
1096 assert!(result.is_err());
1097 assert!(matches!(result.unwrap_err(), ParseError::IncompleteFrame));
1098 }
1099
1100 #[test]
1101 fn test_parse_single_frame_missing_atom_index_defaults_sequential() {
1102 let lines = vec![
1104 "PREBOX1",
1105 "{\"con_spec_version\":2}",
1106 "10.0 20.0 30.0",
1107 "90.0 90.0 90.0",
1108 "POSTBOX1",
1109 "POSTBOX2",
1110 "2",
1111 "3 3",
1112 "12.011 1.008",
1113 "1",
1114 "Coordinates of Component 1",
1115 "0.0 0.0 0.0 0.0 1",
1116 "1.0940 0.0 0.0 0.0 2",
1117 "-0.5470 0.9499 0.0 0.0 3",
1118 "2",
1119 "Coordinates of Component 2",
1120 "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", ];
1124 let mut line_it = lines.iter().copied();
1125 let frame = parse_single_frame(&mut line_it).unwrap();
1126 assert_eq!(frame.atom_data.len(), 6);
1127 assert_eq!(frame.atom_data[0].atom_id, 1);
1129 assert_eq!(frame.atom_data[1].atom_id, 2);
1130 assert_eq!(frame.atom_data[2].atom_id, 3);
1131 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); }
1136
1137 #[test]
1138 fn test_parse_single_frame_too_few_columns_fails() {
1139 let lines = vec![
1141 "PREBOX1",
1142 "{\"con_spec_version\":2}",
1143 "10.0 20.0 30.0",
1144 "90.0 90.0 90.0",
1145 "POSTBOX1",
1146 "POSTBOX2",
1147 "1",
1148 "1",
1149 "12.011",
1150 "C",
1151 "Coordinates of Component 1",
1152 "0.0 0.0 0.0", ];
1154 let mut line_it = lines.iter().copied();
1155 let result = parse_single_frame(&mut line_it);
1156 assert!(result.is_err());
1157 assert!(matches!(
1158 result.unwrap_err(),
1159 ParseError::InvalidVectorLength {
1160 expected: 5,
1161 found: 3
1162 }
1163 ));
1164 }
1165
1166 #[test]
1167 fn test_parse_velocity_section_present() {
1168 let lines = vec![
1169 "PREBOX1",
1170 "{\"con_spec_version\":2}",
1171 "10.0 20.0 30.0",
1172 "90.0 90.0 90.0",
1173 "POSTBOX1",
1174 "POSTBOX2",
1175 "2",
1176 "1 1",
1177 "63.546 1.008",
1178 "Cu",
1179 "Coordinates of Component 1",
1180 "0.0 0.0 0.0 1.0 0",
1181 "H",
1182 "Coordinates of Component 2",
1183 "1.0 2.0 3.0 0.0 1",
1184 "",
1185 "Cu",
1186 "Velocities of Component 1",
1187 "0.1 0.2 0.3 1.0 0",
1188 "H",
1189 "Velocities of Component 2",
1190 "0.4 0.5 0.6 0.0 1",
1191 ];
1192 let mut line_it = lines.iter().copied().peekable();
1193 let mut frame =
1195 parse_single_frame(&mut line_it).expect("coordinate parsing should succeed");
1196 assert!(!frame.has_velocities());
1197
1198 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
1200 .expect("velocity parsing should succeed");
1201 assert!(has_vel);
1202 assert_eq!(frame.atom_data[0].velocity, Some([0.1, 0.2, 0.3]));
1203 assert_eq!(frame.atom_data[1].velocity, Some([0.4, 0.5, 0.6]));
1204 }
1205
1206 #[test]
1207 fn test_validate_true_accepts_matching_section_identity() {
1208 let text = r#"
1209PREBOX1
1210{"con_spec_version":2,"sections":["velocities"],"validate":true}
121110.0 20.0 30.0
121290.0 90.0 90.0
1213POSTBOX1
1214POSTBOX2
12152
12161 1
121763.546 1.008
1218Cu
1219Coordinates of Component 1
12200.0 0.0 0.0 5 0
1221H
1222Coordinates of Component 2
12231.0 2.0 3.0 0 1
1224
1225Cu
1226Velocities of Component 1
12270.1 0.2 0.3 5 0
1228H
1229Velocities of Component 2
12300.4 0.5 0.6 0 1
1231"#;
1232 let mut iter = ConFrameIterator::new(text.trim());
1233 let frame = iter.next().unwrap().unwrap();
1234
1235 assert!(frame.has_velocities());
1236 assert_eq!(
1237 frame
1238 .header
1239 .metadata
1240 .get("validate")
1241 .and_then(|v| v.as_bool()),
1242 Some(true)
1243 );
1244 }
1245
1246 #[test]
1247 fn test_validate_true_rejects_section_atom_id_mismatch() {
1248 let text = r#"
1249PREBOX1
1250{"con_spec_version":2,"sections":["velocities"],"validate":true}
125110.0 20.0 30.0
125290.0 90.0 90.0
1253POSTBOX1
1254POSTBOX2
12551
12561
125763.546
1258Cu
1259Coordinates of Component 1
12600.0 0.0 0.0 5 0
1261
1262Cu
1263Velocities of Component 1
12640.1 0.2 0.3 5 99
1265"#;
1266 let mut iter = ConFrameIterator::new(text.trim());
1267 let err = iter.next().unwrap().unwrap_err();
1268
1269 assert!(matches!(err, ParseError::ValidationError(_)));
1270 assert!(err.to_string().contains("atom_id mismatch"));
1271 }
1272
1273 #[test]
1274 fn test_validate_true_rejects_section_symbol_mismatch() {
1275 let text = r#"
1276PREBOX1
1277{"con_spec_version":2,"sections":["forces"],"validate":true}
127810.0 20.0 30.0
127990.0 90.0 90.0
1280POSTBOX1
1281POSTBOX2
12821
12831
128463.546
1285Cu
1286Coordinates of Component 1
12870.0 0.0 0.0 5 0
1288
1289H
1290Forces of Component 1
12910.1 0.2 0.3 5 0
1292"#;
1293 let mut iter = ConFrameIterator::new(text.trim());
1294 let err = iter.next().unwrap().unwrap_err();
1295
1296 assert!(matches!(err, ParseError::ValidationError(_)));
1297 assert!(err.to_string().contains("symbol mismatch"));
1298 }
1299
1300 #[test]
1301 fn test_validate_absent_allows_legacy_duplicate_identity_mismatch() {
1302 let text = r#"
1303PREBOX1
1304{"con_spec_version":2,"sections":["velocities"]}
130510.0 20.0 30.0
130690.0 90.0 90.0
1307POSTBOX1
1308POSTBOX2
13091
13101
131163.546
1312Cu
1313Coordinates of Component 1
13140.0 0.0 0.0 5 0
1315
1316Cu
1317Velocities of Component 1
13180.1 0.2 0.3 0 99
1319"#;
1320 let mut iter = ConFrameIterator::new(text.trim());
1321 let frame = iter.next().unwrap().unwrap();
1322
1323 assert!(frame.has_velocities());
1324 assert_eq!(frame.atom_data[0].atom_id, 0);
1325 assert_eq!(frame.atom_data[0].fixed, [true, false, true]);
1326 }
1327
1328 #[test]
1329 fn test_validate_must_be_boolean_when_present() {
1330 let lines = vec![
1331 "PREBOX1",
1332 "{\"con_spec_version\":2,\"validate\":\"yes\",\"sections\":[]}",
1333 "10.0 20.0 30.0",
1334 "90.0 90.0 90.0",
1335 "POSTBOX1",
1336 "POSTBOX2",
1337 "1",
1338 "1",
1339 "12.011",
1340 ];
1341 let mut line_it = lines.iter().copied();
1342 let err = parse_frame_header(&mut line_it).unwrap_err();
1343
1344 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1345 assert!(err.to_string().contains("validate"));
1346 }
1347
1348 #[test]
1349 fn test_validate_true_requires_sections_key() {
1350 let lines = vec![
1351 "PREBOX1",
1352 "{\"con_spec_version\":2,\"validate\":true}",
1353 "10.0 20.0 30.0",
1354 "90.0 90.0 90.0",
1355 "POSTBOX1",
1356 "POSTBOX2",
1357 "1",
1358 "1",
1359 "12.011",
1360 ];
1361 let mut line_it = lines.iter().copied();
1362 let err = parse_frame_header(&mut line_it).unwrap_err();
1363
1364 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1365 assert!(err.to_string().contains("sections"));
1366 }
1367
1368 #[test]
1369 fn test_sections_must_be_string_array_when_present() {
1370 let lines = vec![
1371 "PREBOX1",
1372 "{\"con_spec_version\":2,\"sections\":[\"velocities\",7]}",
1373 "10.0 20.0 30.0",
1374 "90.0 90.0 90.0",
1375 "POSTBOX1",
1376 "POSTBOX2",
1377 "1",
1378 "1",
1379 "12.011",
1380 ];
1381 let mut line_it = lines.iter().copied();
1382 let err = parse_frame_header(&mut line_it).unwrap_err();
1383
1384 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1385 assert!(err.to_string().contains("sections"));
1386 }
1387
1388 #[test]
1389 fn test_validate_true_rejects_non_integer_coordinate_identity_columns() {
1390 let text = r#"
1391PREBOX1
1392{"con_spec_version":2,"sections":[],"validate":true}
139310.0 20.0 30.0
139490.0 90.0 90.0
1395POSTBOX1
1396POSTBOX2
13971
13981
139963.546
1400Cu
1401Coordinates of Component 1
14020.0 0.0 0.0 5.0 0
1403"#;
1404 let mut iter = ConFrameIterator::new(text.trim());
1405 let err = iter.next().unwrap().unwrap_err();
1406
1407 assert!(matches!(err, ParseError::ValidationError(_)));
1408 assert!(err.to_string().contains("fixed_flag"));
1409 }
1410
1411 #[test]
1412 fn test_validate_true_rejects_non_exact_coordinate_label() {
1413 let text = r#"
1414PREBOX1
1415{"con_spec_version":2,"sections":[],"validate":true}
141610.0 20.0 30.0
141790.0 90.0 90.0
1418POSTBOX1
1419POSTBOX2
14201
14211
142263.546
1423Cu
1424Coordinates Component 1
14250.0 0.0 0.0 5 0
1426"#;
1427 let mut iter = ConFrameIterator::new(text.trim());
1428 let err = iter.next().unwrap().unwrap_err();
1429
1430 assert!(matches!(err, ParseError::ValidationError(_)));
1431 assert!(err.to_string().contains("Coordinates of Component 1"));
1432 }
1433
1434 #[test]
1435 fn test_validate_true_rejects_unknown_component_symbol() {
1436 let text = r#"
1437PREBOX1
1438{"con_spec_version":2,"sections":[],"validate":true}
143910.0 20.0 30.0
144090.0 90.0 90.0
1441POSTBOX1
1442POSTBOX2
14431
14441
144563.546
1446Qq
1447Coordinates of Component 1
14480.0 0.0 0.0 0 0
1449"#;
1450 let mut iter = ConFrameIterator::new(text.trim());
1451 let err = iter.next().unwrap().unwrap_err();
1452
1453 assert!(matches!(err, ParseError::ValidationError(_)));
1454 assert!(err.to_string().contains("symbol"));
1455 }
1456
1457 #[test]
1458 fn test_declared_section_must_be_present() {
1459 let text = r#"
1460PREBOX1
1461{"con_spec_version":2,"sections":["velocities"]}
146210.0 20.0 30.0
146390.0 90.0 90.0
1464POSTBOX1
1465POSTBOX2
14661
14671
146863.546
1469Cu
1470Coordinates of Component 1
14710.0 0.0 0.0 0 0
1472"#;
1473 let mut iter = ConFrameIterator::new(text.trim());
1474 let err = iter.next().unwrap().unwrap_err();
1475
1476 assert!(matches!(err, ParseError::IncompleteVelocitySection));
1477 }
1478
1479 #[test]
1480 fn test_non_finite_cell_geometry_rejected_in_strict_mode() {
1481 let lines = vec![
1482 "PREBOX1",
1483 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
1484 "10.0 NaN 30.0",
1485 "90.0 90.0 90.0",
1486 "POSTBOX1",
1487 "POSTBOX2",
1488 "1",
1489 "1",
1490 "12.011",
1491 ];
1492 let mut line_it = lines.iter().copied();
1493 let err = parse_frame_header(&mut line_it).unwrap_err();
1494
1495 assert!(matches!(err, ParseError::ValidationError(_)));
1496 assert!(err.to_string().contains("cell"));
1497 }
1498
1499 #[test]
1500 fn test_validate_true_rejects_non_physical_cell_geometry() {
1501 let lines = vec![
1502 "PREBOX1",
1503 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
1504 "0.0 20.0 30.0",
1505 "90.0 180.0 90.0",
1506 "POSTBOX1",
1507 "POSTBOX2",
1508 "1",
1509 "1",
1510 "12.011",
1511 ];
1512 let mut line_it = lines.iter().copied();
1513 let err = parse_frame_header(&mut line_it).unwrap_err();
1514
1515 assert!(matches!(err, ParseError::ValidationError(_)));
1516 assert!(err.to_string().contains("cell"));
1517 }
1518
1519 #[test]
1520 fn test_validate_true_rejects_reserved_metadata_type_mismatch() {
1521 let lines = vec![
1522 "PREBOX1",
1523 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"energy\":\"low\"}",
1524 "10.0 20.0 30.0",
1525 "90.0 90.0 90.0",
1526 "POSTBOX1",
1527 "POSTBOX2",
1528 "1",
1529 "1",
1530 "12.011",
1531 ];
1532 let mut line_it = lines.iter().copied();
1533 let err = parse_frame_header(&mut line_it).unwrap_err();
1534
1535 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1536 assert!(err.to_string().contains("energy"));
1537 }
1538
1539 #[test]
1540 fn test_parse_velocity_section_absent() {
1541 let lines = vec![
1542 "PREBOX1",
1543 "{\"con_spec_version\":2}",
1544 "10.0 20.0 30.0",
1545 "90.0 90.0 90.0",
1546 "POSTBOX1",
1547 "POSTBOX2",
1548 "1",
1549 "1",
1550 "12.011",
1551 "C",
1552 "Coordinates of Component 1",
1553 "0.0 0.0 0.0 0.0 1",
1554 ];
1555 let mut line_it = lines.iter().copied().peekable();
1556 let mut frame = parse_single_frame(&mut line_it).expect("parse should succeed");
1557 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
1558 .expect("should succeed with no velocities");
1559 assert!(!has_vel);
1560 assert_eq!(frame.atom_data[0].velocity, None);
1561 }
1562
1563 #[test]
1564 fn test_parse_line_of_range_f64_exact() {
1565 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0 42", 4, 5, &[0.0; 5]).unwrap();
1566 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 42.0]);
1567 }
1568
1569 #[test]
1570 fn test_parse_line_of_range_f64_padded() {
1571 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
1572 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0", 4, 5, &defaults).unwrap();
1573 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 99.0]);
1574 }
1575
1576 #[test]
1577 fn test_parse_line_of_range_f64_too_few() {
1578 let result = parse_line_of_range_f64("1.0 2.0 3.0", 4, 5, &[0.0; 5]);
1579 assert!(result.is_err());
1580 }
1581
1582 #[test]
1583 fn test_parse_line_of_range_f64_too_many() {
1584 let result = parse_line_of_range_f64("1.0 2.0 3.0 0.0 5.0 6.0", 4, 5, &[0.0; 5]);
1585 assert!(result.is_err());
1586 }
1587
1588 #[test]
1589 fn test_parse_all_four_column_lines() {
1590 let lines = vec![
1592 "PREBOX1",
1593 "{\"con_spec_version\":2}",
1594 "10.0 10.0 10.0",
1595 "90.0 90.0 90.0",
1596 "POSTBOX1",
1597 "POSTBOX2",
1598 "1",
1599 "3",
1600 "12.011",
1601 "C",
1602 "Coordinates of Component 1",
1603 "0.0 0.0 0.0 0",
1604 "1.0 0.0 0.0 0",
1605 "2.0 0.0 0.0 1",
1606 ];
1607 let mut line_it = lines.iter().copied();
1608 let frame = parse_single_frame(&mut line_it).unwrap();
1609 assert_eq!(frame.atom_data[0].atom_id, 0);
1610 assert_eq!(frame.atom_data[1].atom_id, 1);
1611 assert_eq!(frame.atom_data[2].atom_id, 2);
1612 assert!(frame.atom_data[2].is_fixed());
1613 }
1614}