1use crate::error::ParseError;
2use crate::helpers::symbol_to_atomic_number;
3use crate::types::{
4 AtomDatum, ConFrame, FrameHeader, PreboxHeader, SECTION_ENERGIES, SECTION_FORCES,
5 SECTION_VELOCITIES,
6 decode_fixed_bitmask, meta,
7};
8use serde_json::Value;
9use std::collections::BTreeMap;
10use std::iter::Peekable;
11use std::sync::Arc;
12
13pub fn parse_line_of_n_f64(line: &str, n: usize) -> Result<Vec<f64>, ParseError> {
24 let mut values = Vec::with_capacity(n);
25 for token in line.split_ascii_whitespace() {
26 let val: f64 = fast_float2::parse(token)
27 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
28 values.push(val);
29 }
30 if values.len() == n {
31 Ok(values)
32 } else {
33 Err(ParseError::InvalidVectorLength {
34 expected: n,
35 found: values.len(),
36 })
37 }
38}
39
40pub fn parse_line_of_range_f64(
46 line: &str,
47 min: usize,
48 max: usize,
49 defaults: &[f64],
50) -> Result<Vec<f64>, ParseError> {
51 let mut values = Vec::with_capacity(max);
52 for token in line.split_ascii_whitespace() {
53 let val: f64 = fast_float2::parse(token)
54 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
55 values.push(val);
56 }
57 if values.len() < min || values.len() > max {
58 return Err(ParseError::InvalidVectorLength {
59 expected: max,
60 found: values.len(),
61 });
62 }
63 while values.len() < max {
65 let idx = values.len();
66 values.push(defaults[idx]);
67 }
68 Ok(values)
69}
70
71fn metadata_json_error(message: impl Into<String>) -> ParseError {
72 ParseError::InvalidMetadataJson(message.into())
73}
74
75fn validate_metadata_number(key: &str, value: &Value) -> Result<(), ParseError> {
76 if value.as_f64().is_some() {
77 Ok(())
78 } else {
79 Err(metadata_json_error(format!(
80 "{key} must be a finite number"
81 )))
82 }
83}
84
85fn validate_metadata_integer(key: &str, value: &Value) -> Result<(), ParseError> {
86 if value.as_u64().is_some() {
87 Ok(())
88 } else {
89 Err(metadata_json_error(format!(
90 "{key} must be a non-negative integer"
91 )))
92 }
93}
94
95pub fn validate_metadata_schema(
103 json_obj: &serde_json::Map<String, Value>,
104) -> Result<(), ParseError> {
105 let strict_requested = match json_obj.get(meta::VALIDATE) {
106 Some(Value::Bool(b)) => *b,
107 Some(_) => return Err(metadata_json_error("validate must be a boolean")),
108 None => false,
109 };
110
111 match json_obj.get(meta::SECTIONS) {
112 Some(Value::Array(values)) => {
113 if values.iter().any(|entry| !entry.is_string()) {
114 return Err(metadata_json_error("sections must be an array of strings"));
115 }
116 }
117 Some(_) => return Err(metadata_json_error("sections must be an array of strings")),
118 None if strict_requested => {
119 return Err(metadata_json_error(
120 "validate=true requires a sections array, even when empty",
121 ));
122 }
123 None => {}
124 }
125
126 for (key, value) in json_obj {
127 match key.as_str() {
128 meta::CON_SPEC_VERSION | meta::SECTIONS | meta::VALIDATE => {}
129 meta::ENERGY
130 | meta::TIME
131 | meta::TIMESTEP
132 | meta::CONVERGENCE_FMAX
133 | meta::CONVERGENCE_ENERGY
134 | meta::FMAX => validate_metadata_number(key, value)?,
135 meta::FRAME_INDEX | meta::NEB_BEAD | meta::NEB_BAND => {
136 validate_metadata_integer(key, value)?
137 }
138 meta::GENERATOR if !value.is_string() => {
139 return Err(metadata_json_error("generator must be a string"));
140 }
141 meta::GENERATOR => {}
142 meta::UNITS | meta::POTENTIAL if !value.is_object() => {
143 return Err(metadata_json_error(format!("{key} must be an object")));
144 }
145 meta::POTENTIAL => {
146 if let Some(potential_type) = value.get("type")
147 && !potential_type.is_string()
148 {
149 return Err(metadata_json_error("potential.type must be a string"));
150 }
151 }
152 meta::UNITS => {}
153 meta::PBC => validate_pbc_metadata(value)?,
154 meta::BONDS => validate_bonds_metadata(value)?,
155 meta::LATTICE_VECTORS => validate_lattice_vectors_metadata(value)?,
156 meta::CONVERGED if !value.is_boolean() => {
157 return Err(metadata_json_error("converged must be a boolean"));
158 }
159 meta::CONVERGED => {}
160 _ => {}
161 }
162 }
163
164 Ok(())
165}
166
167fn validate_pbc_metadata(value: &Value) -> Result<(), ParseError> {
168 let Some(values) = value.as_array() else {
169 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
170 };
171 if values.len() != 3 || values.iter().any(|entry| !entry.is_boolean()) {
172 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
173 }
174 Ok(())
175}
176
177fn validate_bonds_metadata(value: &Value) -> Result<(), ParseError> {
184 let Some(items) = value.as_array() else {
185 return Err(metadata_json_error("bonds must be an array"));
186 };
187 for (idx, item) in items.iter().enumerate() {
188 if let Some(pair) = item.as_array() {
189 if pair.len() != 2 {
190 return Err(metadata_json_error(format!(
191 "bonds[{idx}] pair must have exactly two indices"
192 )));
193 }
194 for (k, entry) in pair.iter().enumerate() {
195 let Some(n) = entry.as_u64() else {
196 return Err(metadata_json_error(format!(
197 "bonds[{idx}][{k}] must be a non-negative integer"
198 )));
199 };
200 if n > u32::MAX as u64 {
201 return Err(metadata_json_error(format!(
202 "bonds[{idx}][{k}] index exceeds u32"
203 )));
204 }
205 }
206 continue;
207 }
208 if let Some(obj) = item.as_object() {
209 for key in ["i", "j"] {
210 let Some(entry) = obj.get(key) else {
211 return Err(metadata_json_error(format!(
212 "bonds[{idx}] object must include \"{key}\""
213 )));
214 };
215 let Some(n) = entry.as_u64() else {
216 return Err(metadata_json_error(format!(
217 "bonds[{idx}].{key} must be a non-negative integer"
218 )));
219 };
220 if n > u32::MAX as u64 {
221 return Err(metadata_json_error(format!(
222 "bonds[{idx}].{key} index exceeds u32"
223 )));
224 }
225 }
226 if let Some(order) = obj.get("order")
227 && order.as_i64().is_none()
228 {
229 return Err(metadata_json_error(format!(
230 "bonds[{idx}].order must be an integer when present"
231 )));
232 }
233 continue;
234 }
235 return Err(metadata_json_error(format!(
236 "bonds[{idx}] must be [i, j] or {{\"i\": i, \"j\": j, \"order\"?: ...}}"
237 )));
238 }
239 Ok(())
240}
241
242fn validate_lattice_vectors_metadata(value: &Value) -> Result<(), ParseError> {
243 let Some(rows) = value.as_array() else {
244 return Err(metadata_json_error(
245 "lattice_vectors must be a 3x3 numeric array",
246 ));
247 };
248 if rows.len() != 3 {
249 return Err(metadata_json_error(
250 "lattice_vectors must be a 3x3 numeric array",
251 ));
252 }
253 for row in rows {
254 let Some(entries) = row.as_array() else {
255 return Err(metadata_json_error(
256 "lattice_vectors must be a 3x3 numeric array",
257 ));
258 };
259 if entries.len() != 3 || entries.iter().any(|entry| entry.as_f64().is_none()) {
260 return Err(metadata_json_error(
261 "lattice_vectors must be a 3x3 numeric array",
262 ));
263 }
264 }
265 Ok(())
266}
267
268pub fn parse_line_of_n<T: std::str::FromStr>(line: &str, n: usize) -> Result<Vec<T>, ParseError>
296where
297 ParseError: From<<T as std::str::FromStr>::Err>,
298{
299 let values: Vec<T> = line
300 .split_whitespace()
301 .map(|s| s.parse::<T>())
302 .collect::<Result<_, _>>()?;
303
304 if values.len() == n {
305 Ok(values)
306 } else {
307 Err(ParseError::InvalidVectorLength {
308 expected: n,
309 found: values.len(),
310 })
311 }
312}
313
314pub fn parse_frame_header<'a>(
335 lines: &mut impl Iterator<Item = &'a str>,
336) -> Result<FrameHeader, ParseError> {
337 let prebox1 = lines
338 .next()
339 .ok_or(ParseError::IncompleteHeader)?
340 .to_string();
341 let prebox2_raw = lines.next().ok_or(ParseError::IncompleteHeader)?;
342
343 let trimmed = prebox2_raw.trim();
346 let (spec_version, metadata, sections, validate, sections_declared) = if trimmed.starts_with('{') {
347 let json_val: serde_json::Value = serde_json::from_str(trimmed)
348 .map_err(|e| ParseError::InvalidMetadataJson(e.to_string()))?;
349 let json_obj = json_val
350 .as_object()
351 .ok_or_else(|| ParseError::InvalidMetadataJson("expected a JSON object".to_string()))?;
352 let ver = json_obj
353 .get(meta::CON_SPEC_VERSION)
354 .and_then(|v| v.as_u64())
355 .ok_or(ParseError::MissingSpecVersion)? as u32;
356 if ver > crate::CON_SPEC_VERSION {
357 return Err(ParseError::UnsupportedSpecVersion(ver));
358 }
359
360 let mut sections: Vec<String> = Vec::new();
364 let mut metadata = BTreeMap::new();
365 let mut sections_declared = false;
366 let mut validate = false;
367 for (k, v) in json_obj {
368 match k.as_str() {
369 meta::CON_SPEC_VERSION => {}
370 meta::SECTIONS => {
371 sections_declared = true;
372 let arr = v.as_array().ok_or_else(|| {
373 metadata_json_error("sections must be an array of strings")
374 })?;
375 sections.reserve(arr.len());
376 for entry in arr {
377 let s = entry.as_str().ok_or_else(|| {
378 metadata_json_error("sections must be an array of strings")
379 })?;
380 sections.push(s.to_string());
381 }
382 }
383 meta::VALIDATE => {
384 validate = match v {
385 Value::Bool(b) => *b,
386 _ => return Err(metadata_json_error("validate must be a boolean")),
387 };
388 metadata.insert(k.clone(), v.clone());
389 }
390 _ => {
391 metadata.insert(k.clone(), v.clone());
392 }
393 }
394 }
395
396 if validate {
399 validate_metadata_schema(json_obj)?;
400 }
401
402 (ver, metadata, sections, validate, sections_declared)
403 } else {
404 (1_u32, BTreeMap::new(), Vec::new(), false, false)
406 };
407 let prebox2 = prebox2_raw.to_string();
408
409 let boxl_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
410 let angles_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
411 let postbox1 = lines
412 .next()
413 .ok_or(ParseError::IncompleteHeader)?
414 .to_string();
415 let postbox2 = lines
416 .next()
417 .ok_or(ParseError::IncompleteHeader)?
418 .to_string();
419 let natm_types =
420 parse_line_of_n::<usize>(lines.next().ok_or(ParseError::IncompleteHeader)?, 1)?[0];
421 let natms_per_type = parse_line_of_n::<usize>(
422 lines.next().ok_or(ParseError::IncompleteHeader)?,
423 natm_types,
424 )?;
425 let masses_per_type = parse_line_of_n_f64(
426 lines.next().ok_or(ParseError::IncompleteHeader)?,
427 natm_types,
428 )?;
429 if validate {
430 validate_header_geometry(&boxl_vec, &angles_vec, natm_types, &natms_per_type)?;
431 validate_masses(&masses_per_type)?;
432 }
433 Ok(FrameHeader {
434 prebox_header: PreboxHeader {
435 user: prebox1,
436 metadata_line: prebox2,
437 },
438 boxl: boxl_vec.try_into().unwrap(),
439 angles: angles_vec.try_into().unwrap(),
440 postbox_header: [postbox1, postbox2],
441 natm_types,
442 natms_per_type,
443 masses_per_type,
444 spec_version,
445 metadata,
446 sections,
447 strict_validation: validate,
448 sections_declared,
449 })
450}
451
452pub fn parse_single_frame<'a>(
501 lines: &mut impl Iterator<Item = &'a str>,
502) -> Result<ConFrame, ParseError> {
503 let header = parse_frame_header(lines)?;
504 let validate = header.strict_validation;
505 let total_atoms: usize = header.natms_per_type.iter().sum();
506 let mut atom_data = Vec::with_capacity(total_atoms);
507
508 let mut global_atom_idx: u64 = 0;
509 for (type_idx, num_atoms) in header.natms_per_type.iter().enumerate() {
510 let symbol_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
514 let symbol: Arc<str> = Arc::from(symbol_line.trim());
515 let coord_label = lines.next().ok_or(ParseError::IncompleteFrame)?;
516 if validate {
517 validate_coordinate_component(type_idx, symbol.as_ref(), coord_label)?;
518 }
519 for _ in 0..*num_atoms {
520 let coord_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
521 let defaults = [0.0, 0.0, 0.0, 0.0, global_atom_idx as f64];
523 let vals = parse_line_of_range_f64(coord_line, 4, 5, &defaults)?;
524 let (fixed, atom_id) = if validate {
525 parse_identity_columns(coord_line, "coordinate", 3, 4, 5)?
526 } else {
527 (decode_fixed_bitmask(vals[3] as u8), vals[4] as u64)
528 };
529 atom_data.push(AtomDatum {
530 symbol: Arc::clone(&symbol),
532 x: vals[0],
533 y: vals[1],
534 z: vals[2],
535 fixed,
536 atom_id,
537 velocity: None,
538 force: None,
539 energy: None,
540 });
541 global_atom_idx += 1;
542 }
543 }
544 Ok(ConFrame { header, atom_data })
545}
546
547fn validate_header_geometry(
548 boxl: &[f64],
549 angles: &[f64],
550 natm_types: usize,
551 natms_per_type: &[usize],
552) -> Result<(), ParseError> {
553 if boxl.iter().any(|length| !length.is_finite() || *length <= 0.0)
554 || angles
555 .iter()
556 .any(|angle| !angle.is_finite() || *angle <= 0.0 || *angle >= 180.0)
557 {
558 return Err(ParseError::ValidationError(
559 "cell geometry must have positive lengths and angles between 0 and 180 degrees"
560 .to_string(),
561 ));
562 }
563 if natm_types == 0 || natms_per_type.contains(&0) {
564 return Err(ParseError::ValidationError(
565 "atom counts must contain at least one atom per component".to_string(),
566 ));
567 }
568 Ok(())
569}
570
571fn validate_masses(masses_per_type: &[f64]) -> Result<(), ParseError> {
572 if masses_per_type
573 .iter()
574 .any(|mass| !mass.is_finite() || *mass <= 0.0)
575 {
576 return Err(ParseError::ValidationError(
577 "component masses must be positive".to_string(),
578 ));
579 }
580 Ok(())
581}
582
583fn validate_coordinate_component(
584 type_idx: usize,
585 symbol: &str,
586 label: &str,
587) -> Result<(), ParseError> {
588 let expected_label = format!("Coordinates of Component {}", type_idx + 1);
589 if label.trim() != expected_label {
590 return Err(ParseError::ValidationError(format!(
591 "expected coordinate label {expected_label:?}, found {label:?}"
592 )));
593 }
594 if symbol != "X" && symbol_to_atomic_number(symbol) == 0 {
595 return Err(ParseError::ValidationError(format!(
596 "unknown component symbol {symbol}"
597 )));
598 }
599 Ok(())
600}
601
602fn parse_identity_columns(
617 line: &str,
618 row_kind: &str,
619 fixed_idx: usize,
620 atom_id_idx: usize,
621 n_cols: usize,
622) -> Result<([bool; 3], u64), ParseError> {
623 let columns = line.split_ascii_whitespace().collect::<Vec<_>>();
624 if columns.len() != n_cols {
625 return Err(ParseError::ValidationError(format!(
626 "{row_kind} rows require {n_cols} columns including fixed_flag and atom_id in validate mode"
627 )));
628 }
629 let fixed_flag = columns[fixed_idx].parse::<u8>().map_err(|_| {
630 ParseError::ValidationError(format!("{row_kind} fixed_flag must be an integer bitmask"))
631 })?;
632 if fixed_flag > 7 {
633 return Err(ParseError::ValidationError(format!(
634 "{row_kind} fixed_flag must be between 0 and 7"
635 )));
636 }
637 let atom_id = columns[atom_id_idx].parse::<u64>().map_err(|_| {
638 ParseError::ValidationError(format!("{row_kind} atom_id must be an integer"))
639 })?;
640 Ok((decode_fixed_bitmask(fixed_flag), atom_id))
641}
642
643
644fn validate_section_component(
645 section: &str,
646 type_idx: usize,
647 atom_idx: usize,
648 symbol: &str,
649 label: &str,
650 header: &FrameHeader,
651 atom_data: &[AtomDatum],
652) -> Result<(), ParseError> {
653 let expected_label = format!("{section} of Component {}", type_idx + 1);
654 if label.trim() != expected_label {
655 return Err(ParseError::ValidationError(format!(
656 "expected section label {expected_label:?}, found {label:?}"
657 )));
658 }
659
660 if header.natms_per_type[type_idx] == 0 {
661 return Ok(());
662 }
663
664 let expected_symbol = atom_data
665 .get(atom_idx)
666 .map(|atom| atom.symbol.as_ref())
667 .ok_or_else(|| {
668 ParseError::ValidationError(format!(
669 "{section} component {} has no coordinate atom to validate against",
670 type_idx + 1
671 ))
672 })?;
673 if symbol != expected_symbol {
674 return Err(ParseError::ValidationError(format!(
675 "{section} component {} symbol mismatch: expected {expected_symbol}, found {symbol}",
676 type_idx + 1
677 )));
678 }
679
680 Ok(())
681}
682
683fn validate_section_atom_identity(
684 section: &str,
685 atom_idx: usize,
686 fixed: [bool; 3],
687 atom_id: u64,
688 atom_data: &[AtomDatum],
689) -> Result<(), ParseError> {
690 let atom = atom_data.get(atom_idx).ok_or_else(|| {
691 ParseError::ValidationError(format!(
692 "{section} row {atom_idx} has no coordinate atom to validate against"
693 ))
694 })?;
695
696 if atom.fixed != fixed {
697 return Err(ParseError::ValidationError(format!(
698 "{section} row {atom_idx} fixed mask mismatch for atom_id {}",
699 atom.atom_id
700 )));
701 }
702 if atom.atom_id != atom_id {
703 return Err(ParseError::ValidationError(format!(
704 "{section} row {atom_idx} atom_id mismatch: expected {}, found {atom_id}",
705 atom.atom_id
706 )));
707 }
708
709 Ok(())
710}
711
712pub fn parse_velocity_section<'a, I>(
725 lines: &mut Peekable<I>,
726 header: &FrameHeader,
727 atom_data: &mut [AtomDatum],
728) -> Result<bool, ParseError>
729where
730 I: Iterator<Item = &'a str>,
731{
732 let validate = header.strict_validation;
733 match lines.peek() {
735 Some(line) if line.trim().is_empty() => {
736 lines.next();
738 }
739 _ => return Ok(false),
740 }
741
742 let mut atom_idx: usize = 0;
743 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
744 let symbol = lines
746 .next()
747 .ok_or(ParseError::IncompleteVelocitySection)?
748 .trim();
749
750 let comp_line = lines.next().ok_or(ParseError::IncompleteVelocitySection)?;
752 if !comp_line.contains("Velocities of Component") {
754 return Err(ParseError::IncompleteVelocitySection);
755 }
756 if validate {
757 validate_section_component(
758 "Velocities",
759 type_idx,
760 atom_idx,
761 symbol,
762 comp_line,
763 header,
764 atom_data,
765 )?;
766 }
767
768 for _ in 0..num_atoms {
769 let vel_line = lines.next().ok_or(ParseError::IncompleteVelocitySection)?;
770 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
772 let vals = parse_line_of_range_f64(vel_line, 4, 5, &defaults)?;
773 if validate {
774 let (fixed, atom_id) =
775 parse_identity_columns(vel_line, "velocities", 3, 4, 5)?;
776 validate_section_atom_identity("velocities", atom_idx, fixed, atom_id, atom_data)?;
777 }
778 if atom_idx < atom_data.len() {
779 atom_data[atom_idx].velocity = Some([vals[0], vals[1], vals[2]]);
780 }
781 atom_idx += 1;
782 }
783 }
784
785 Ok(true)
786}
787
788pub fn parse_force_section<'a, I>(
796 lines: &mut Peekable<I>,
797 header: &FrameHeader,
798 atom_data: &mut [AtomDatum],
799) -> Result<bool, ParseError>
800where
801 I: Iterator<Item = &'a str>,
802{
803 let validate = header.strict_validation;
804 match lines.peek() {
806 Some(line) if line.trim().is_empty() => {
807 lines.next();
808 }
809 _ => return Ok(false),
810 }
811
812 let mut atom_idx: usize = 0;
813 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
814 let symbol = lines
815 .next()
816 .ok_or(ParseError::IncompleteForceSection)?
817 .trim();
818
819 let comp_line = lines.next().ok_or(ParseError::IncompleteForceSection)?;
820 if !comp_line.contains("Forces of Component") {
821 return Err(ParseError::IncompleteForceSection);
822 }
823 if validate {
824 validate_section_component(
825 "Forces", type_idx, atom_idx, symbol, comp_line, header, atom_data,
826 )?;
827 }
828
829 for _ in 0..num_atoms {
830 let force_line = lines.next().ok_or(ParseError::IncompleteForceSection)?;
831 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
832 let vals = parse_line_of_range_f64(force_line, 4, 5, &defaults)?;
833 if validate {
834 let (fixed, atom_id) =
835 parse_identity_columns(force_line, "forces", 3, 4, 5)?;
836 validate_section_atom_identity("forces", atom_idx, fixed, atom_id, atom_data)?;
837 }
838 if atom_idx < atom_data.len() {
839 atom_data[atom_idx].force = Some([vals[0], vals[1], vals[2]]);
840 }
841 atom_idx += 1;
842 }
843 }
844
845 Ok(true)
846}
847
848pub fn parse_energy_section<'a, I>(
861 lines: &mut Peekable<I>,
862 header: &FrameHeader,
863 atom_data: &mut [AtomDatum],
864) -> Result<bool, ParseError>
865where
866 I: Iterator<Item = &'a str>,
867{
868 let validate = header.strict_validation;
869 match lines.peek() {
870 Some(line) if line.trim().is_empty() => {
871 lines.next();
872 }
873 _ => return Ok(false),
874 }
875
876 let mut atom_idx: usize = 0;
877 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
878 let symbol = lines
879 .next()
880 .ok_or(ParseError::IncompleteEnergySection)?
881 .trim();
882
883 let comp_line = lines.next().ok_or(ParseError::IncompleteEnergySection)?;
884 if !comp_line.contains("Energies of Component") {
885 return Err(ParseError::IncompleteEnergySection);
886 }
887 if validate {
888 validate_section_component(
889 "Energies", type_idx, atom_idx, symbol, comp_line, header, atom_data,
890 )?;
891 }
892
893 for _ in 0..num_atoms {
894 let energy_line = lines.next().ok_or(ParseError::IncompleteEnergySection)?;
895 let defaults = [0.0, 0.0, atom_idx as f64];
898 let vals = parse_line_of_range_f64(energy_line, 1, 3, &defaults)?;
899 if validate {
900 let (fixed, atom_id) =
901 parse_identity_columns(energy_line, "energies", 1, 2, 3)?;
902 validate_section_atom_identity("energies", atom_idx, fixed, atom_id, atom_data)?;
903 }
904 if atom_idx < atom_data.len() {
905 atom_data[atom_idx].energy = Some(vals[0]);
906 }
907 atom_idx += 1;
908 }
909 }
910
911 Ok(true)
912}
913
914pub fn parse_declared_sections<'a, I>(
920 lines: &mut Peekable<I>,
921 header: &mut FrameHeader,
922 atom_data: &mut [AtomDatum],
923) -> Result<(), ParseError>
924where
925 I: Iterator<Item = &'a str>,
926{
927 if !header.sections_declared && header.sections.is_empty() {
928 let found = parse_velocity_section(lines, header, atom_data)?;
930 if found {
931 header.sections.push(SECTION_VELOCITIES.into());
932 }
933 } else {
934 let sections = std::mem::take(&mut header.sections);
935 for section in §ions {
936 match section.as_str() {
937 SECTION_VELOCITIES => {
938 let found = parse_velocity_section(lines, header, atom_data)?;
939 if !found {
940 return Err(ParseError::IncompleteVelocitySection);
941 }
942 }
943 SECTION_FORCES => {
944 let found = parse_force_section(lines, header, atom_data)?;
945 if !found {
946 return Err(ParseError::IncompleteForceSection);
947 }
948 }
949 SECTION_ENERGIES => {
950 let found = parse_energy_section(lines, header, atom_data)?;
951 if !found {
952 return Err(ParseError::IncompleteEnergySection);
953 }
954 }
955 other => return Err(ParseError::UnknownSection(other.to_string())),
956 }
957 }
958 header.sections = sections;
959 }
960 Ok(())
961}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966 use crate::iterators::ConFrameIterator;
967
968 #[test]
969 fn test_parse_line_of_n_success() {
970 let line = "1.0 2.5 -3.0";
971 let values = parse_line_of_n::<f64>(line, 3).unwrap();
972 assert_eq!(values, vec![1.0, 2.5, -3.0]);
973 }
974
975 #[test]
976 fn test_parse_line_of_n_too_short() {
977 let line = "1.0 2.5";
978 let result = parse_line_of_n::<f64>(line, 3);
979 assert!(result.is_err());
980 assert!(matches!(
981 result.unwrap_err(),
982 ParseError::InvalidVectorLength {
983 expected: 3,
984 found: 2
985 }
986 ));
987 }
988
989 #[test]
990 fn test_parse_line_of_n_too_long() {
991 let line = "1.0 2.5 -3.0 4.0";
992 let result = parse_line_of_n::<f64>(line, 3);
993 assert!(result.is_err());
994 assert!(matches!(
995 result.unwrap_err(),
996 ParseError::InvalidVectorLength {
997 expected: 3,
998 found: 4
999 }
1000 ));
1001 }
1002
1003 #[test]
1004 fn test_parse_line_of_n_invalid_float() {
1005 let line = "1.0 abc -3.0";
1006 let result = parse_line_of_n::<f64>(line, 3);
1007 assert!(result.is_err());
1008 assert!(matches!(
1009 result.unwrap_err(),
1010 ParseError::InvalidNumberFormat(_)
1011 ));
1012 }
1013
1014 #[test]
1015 fn test_parse_frame_header_success() {
1016 let lines = [
1017 "PREBOX1",
1018 "{\"con_spec_version\":2}",
1019 "10.0 20.0 30.0",
1020 "90.0 90.0 90.0",
1021 "POSTBOX1",
1022 "POSTBOX2",
1023 "2",
1024 "1 1",
1025 "12.011 1.008",
1026 ];
1027 let mut line_it = lines.iter().copied();
1028 match parse_frame_header(&mut line_it) {
1029 Ok(header) => {
1030 assert_eq!(header.prebox_header.user, "PREBOX1");
1031 assert_eq!(header.spec_version, 2);
1032 assert_eq!(header.boxl, [10.0, 20.0, 30.0]);
1033 assert_eq!(header.angles, [90.0, 90.0, 90.0]);
1034 assert_eq!(header.postbox_header, ["POSTBOX1", "POSTBOX2"]);
1035 assert_eq!(header.natm_types, 2);
1036 assert_eq!(header.natms_per_type, vec![1, 1]);
1037 assert_eq!(header.masses_per_type, vec![12.011, 1.008]);
1038 }
1039 Err(e) => {
1040 panic!(
1041 "Parsing failed when it should have succeeded. Error: {:?}",
1042 e
1043 );
1044 }
1045 }
1046 }
1047
1048 #[test]
1049 fn test_parse_frame_header_missing_line() {
1050 let lines = [
1051 "PREBOX1",
1052 "{\"con_spec_version\":2}",
1053 "10.0 20.0 30.0",
1054 "90.0 90.0 90.0",
1055 "POSTBOX1",
1056 "POSTBOX2",
1057 "2",
1058 "1 1",
1059 ];
1061 let mut line_it = lines.iter().copied();
1062 let result = parse_frame_header(&mut line_it);
1063 assert!(result.is_err());
1064 assert!(matches!(result.unwrap_err(), ParseError::IncompleteHeader));
1065 }
1066
1067 #[test]
1068 fn test_parse_frame_header_missing_spec_version() {
1069 let lines = vec![
1070 "PREBOX1",
1071 "{}",
1072 "10.0 20.0 30.0",
1073 "90.0 90.0 90.0",
1074 "POSTBOX1",
1075 "POSTBOX2",
1076 "2",
1077 "1 1",
1078 "12.011 1.008",
1079 ];
1080 let mut line_it = lines.iter().copied();
1081 let result = parse_frame_header(&mut line_it);
1082 assert!(result.is_err());
1083 assert!(matches!(
1084 result.unwrap_err(),
1085 ParseError::MissingSpecVersion
1086 ));
1087 }
1088
1089 #[test]
1090 fn test_parse_frame_header_legacy_no_json() {
1091 let lines = vec![
1093 "PREBOX1",
1094 "0.0000 TIME",
1095 "10.0 20.0 30.0",
1096 "90.0 90.0 90.0",
1097 "POSTBOX1",
1098 "POSTBOX2",
1099 "2",
1100 "1 1",
1101 "12.011 1.008",
1102 ];
1103 let mut line_it = lines.iter().copied();
1104 let header = parse_frame_header(&mut line_it).unwrap();
1105 assert_eq!(header.spec_version, 1);
1106 assert!(header.metadata.is_empty());
1107 }
1108
1109 #[test]
1110 fn test_parse_frame_header_malformed_json() {
1111 let lines = vec![
1113 "PREBOX1",
1114 "{broken json",
1115 "10.0 20.0 30.0",
1116 "90.0 90.0 90.0",
1117 "POSTBOX1",
1118 "POSTBOX2",
1119 "2",
1120 "1 1",
1121 "12.011 1.008",
1122 ];
1123 let mut line_it = lines.iter().copied();
1124 let result = parse_frame_header(&mut line_it);
1125 assert!(result.is_err());
1126 assert!(matches!(
1127 result.unwrap_err(),
1128 ParseError::InvalidMetadataJson(_)
1129 ));
1130 }
1131
1132 #[test]
1133 fn test_parse_frame_header_unsupported_version() {
1134 let lines = vec![
1135 "PREBOX1",
1136 "{\"con_spec_version\":999}",
1137 "10.0 20.0 30.0",
1138 "90.0 90.0 90.0",
1139 "POSTBOX1",
1140 "POSTBOX2",
1141 "2",
1142 "1 1",
1143 "12.011 1.008",
1144 ];
1145 let mut line_it = lines.iter().copied();
1146 let result = parse_frame_header(&mut line_it);
1147 assert!(result.is_err());
1148 assert!(matches!(
1149 result.unwrap_err(),
1150 ParseError::UnsupportedSpecVersion(999)
1151 ));
1152 }
1153
1154 #[test]
1155 fn test_parse_frame_header_extra_metadata_preserved() {
1156 let lines = vec![
1157 "PREBOX1",
1158 "{\"con_spec_version\":2,\"generator\":\"test\"}",
1159 "10.0 20.0 30.0",
1160 "90.0 90.0 90.0",
1161 "POSTBOX1",
1162 "POSTBOX2",
1163 "2",
1164 "1 1",
1165 "12.011 1.008",
1166 ];
1167 let mut line_it = lines.iter().copied();
1168 let header = parse_frame_header(&mut line_it).unwrap();
1169 assert_eq!(header.spec_version, 2);
1170 assert_eq!(
1171 header.metadata.get("generator"),
1172 Some(&serde_json::Value::String("test".to_string()))
1173 );
1174 }
1175
1176 #[test]
1177 fn test_parse_frame_header_invalid_natms_per_type() {
1178 let lines = vec![
1179 "PREBOX1",
1180 "{\"con_spec_version\":2}",
1181 "10.0 20.0 30.0",
1182 "90.0 90.0 90.0",
1183 "POSTBOX1",
1184 "POSTBOX2",
1185 "2",
1186 "1 1 1", "12.011 1.008",
1188 ];
1189 let mut line_it = lines.iter().copied();
1190 let result = parse_frame_header(&mut line_it);
1191 assert!(result.is_err());
1192 assert!(matches!(
1193 result.unwrap_err(),
1194 ParseError::InvalidVectorLength {
1195 expected: 2,
1196 found: 3
1197 }
1198 ));
1199 }
1200
1201 #[test]
1202 fn test_parse_single_frame_success() {
1203 let lines = vec![
1204 "PREBOX1",
1205 "{\"con_spec_version\":2}",
1206 "10.0 20.0 30.0",
1207 "90.0 90.0 90.0",
1208 "POSTBOX1",
1209 "POSTBOX2",
1210 "2",
1211 "3 3",
1212 "12.011 1.008",
1213 "1",
1214 "Coordinates of Component 1",
1215 "0.0 0.0 0.0 0.0 1",
1216 "1.0940 0.0 0.0 0.0 2",
1217 "-0.5470 0.9499 0.0 0.0 3",
1218 "2",
1219 "Coordinates of Component 2",
1220 "5.0 5.0 5.0 0.0 4",
1221 "6.0940 5.0 5.0 0.0 5",
1222 "5.5470 5.9499 5.0 0.0 6",
1223 ];
1224 let mut line_it = lines.iter().copied();
1225 let frame = parse_single_frame(&mut line_it).unwrap();
1226
1227 assert_eq!(frame.header.natm_types, 2);
1228 assert_eq!(frame.header.natms_per_type, vec![3, 3]);
1229 assert_eq!(frame.header.masses_per_type, vec![12.011, 1.008]);
1230 assert_eq!(frame.atom_data.len(), 6);
1231 assert_eq!(&*frame.atom_data[0].symbol, "1");
1232 assert_eq!(frame.atom_data[0].atom_id, 1);
1233 assert_eq!(&*frame.atom_data[5].symbol, "2");
1234 assert_eq!(frame.atom_data[5].atom_id, 6);
1235 }
1236
1237 #[test]
1238 fn test_parse_single_frame_missing_line() {
1239 let lines = vec![
1241 "PREBOX1",
1242 "{\"con_spec_version\":2}",
1243 "10.0 20.0 30.0",
1244 "90.0 90.0 90.0",
1245 "POSTBOX1",
1246 "POSTBOX2",
1247 "2",
1248 "3 3",
1249 "12.011 1.008",
1250 "1",
1251 "Coordinates of Component 1",
1252 "0.0 0.0 0.0 0.0 1",
1253 "1.0940 0.0 0.0 0.0 2",
1254 "-0.5470 0.9499 0.0 0.0 3",
1255 ];
1257 let mut line_it = lines.iter().copied();
1258 let result = parse_single_frame(&mut line_it);
1259 assert!(result.is_err());
1260 assert!(matches!(result.unwrap_err(), ParseError::IncompleteFrame));
1261 }
1262
1263 #[test]
1264 fn test_parse_single_frame_missing_atom_index_defaults_sequential() {
1265 let lines = vec![
1267 "PREBOX1",
1268 "{\"con_spec_version\":2}",
1269 "10.0 20.0 30.0",
1270 "90.0 90.0 90.0",
1271 "POSTBOX1",
1272 "POSTBOX2",
1273 "2",
1274 "3 3",
1275 "12.011 1.008",
1276 "1",
1277 "Coordinates of Component 1",
1278 "0.0 0.0 0.0 0.0 1",
1279 "1.0940 0.0 0.0 0.0 2",
1280 "-0.5470 0.9499 0.0 0.0 3",
1281 "2",
1282 "Coordinates of Component 2",
1283 "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", ];
1287 let mut line_it = lines.iter().copied();
1288 let frame = parse_single_frame(&mut line_it).unwrap();
1289 assert_eq!(frame.atom_data.len(), 6);
1290 assert_eq!(frame.atom_data[0].atom_id, 1);
1292 assert_eq!(frame.atom_data[1].atom_id, 2);
1293 assert_eq!(frame.atom_data[2].atom_id, 3);
1294 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); }
1299
1300 #[test]
1301 fn test_parse_single_frame_too_few_columns_fails() {
1302 let lines = vec![
1304 "PREBOX1",
1305 "{\"con_spec_version\":2}",
1306 "10.0 20.0 30.0",
1307 "90.0 90.0 90.0",
1308 "POSTBOX1",
1309 "POSTBOX2",
1310 "1",
1311 "1",
1312 "12.011",
1313 "C",
1314 "Coordinates of Component 1",
1315 "0.0 0.0 0.0", ];
1317 let mut line_it = lines.iter().copied();
1318 let result = parse_single_frame(&mut line_it);
1319 assert!(result.is_err());
1320 assert!(matches!(
1321 result.unwrap_err(),
1322 ParseError::InvalidVectorLength {
1323 expected: 5,
1324 found: 3
1325 }
1326 ));
1327 }
1328
1329 #[test]
1330 fn test_parse_velocity_section_present() {
1331 let lines = vec![
1332 "PREBOX1",
1333 "{\"con_spec_version\":2}",
1334 "10.0 20.0 30.0",
1335 "90.0 90.0 90.0",
1336 "POSTBOX1",
1337 "POSTBOX2",
1338 "2",
1339 "1 1",
1340 "63.546 1.008",
1341 "Cu",
1342 "Coordinates of Component 1",
1343 "0.0 0.0 0.0 1.0 0",
1344 "H",
1345 "Coordinates of Component 2",
1346 "1.0 2.0 3.0 0.0 1",
1347 "",
1348 "Cu",
1349 "Velocities of Component 1",
1350 "0.1 0.2 0.3 1.0 0",
1351 "H",
1352 "Velocities of Component 2",
1353 "0.4 0.5 0.6 0.0 1",
1354 ];
1355 let mut line_it = lines.iter().copied().peekable();
1356 let mut frame =
1358 parse_single_frame(&mut line_it).expect("coordinate parsing should succeed");
1359 assert!(!frame.has_velocities());
1360
1361 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
1363 .expect("velocity parsing should succeed");
1364 assert!(has_vel);
1365 assert_eq!(frame.atom_data[0].velocity, Some([0.1, 0.2, 0.3]));
1366 assert_eq!(frame.atom_data[1].velocity, Some([0.4, 0.5, 0.6]));
1367 }
1368
1369 #[test]
1370 fn test_validate_true_accepts_matching_section_identity() {
1371 let text = r#"
1372PREBOX1
1373{"con_spec_version":2,"sections":["velocities"],"validate":true}
137410.0 20.0 30.0
137590.0 90.0 90.0
1376POSTBOX1
1377POSTBOX2
13782
13791 1
138063.546 1.008
1381Cu
1382Coordinates of Component 1
13830.0 0.0 0.0 5 0
1384H
1385Coordinates of Component 2
13861.0 2.0 3.0 0 1
1387
1388Cu
1389Velocities of Component 1
13900.1 0.2 0.3 5 0
1391H
1392Velocities of Component 2
13930.4 0.5 0.6 0 1
1394"#;
1395 let mut iter = ConFrameIterator::new(text.trim());
1396 let frame = iter.next().unwrap().unwrap();
1397
1398 assert!(frame.has_velocities());
1399 assert_eq!(
1400 frame
1401 .header
1402 .metadata
1403 .get("validate")
1404 .and_then(|v| v.as_bool()),
1405 Some(true)
1406 );
1407 }
1408
1409 #[test]
1410 fn test_validate_true_rejects_section_atom_id_mismatch() {
1411 let text = r#"
1412PREBOX1
1413{"con_spec_version":2,"sections":["velocities"],"validate":true}
141410.0 20.0 30.0
141590.0 90.0 90.0
1416POSTBOX1
1417POSTBOX2
14181
14191
142063.546
1421Cu
1422Coordinates of Component 1
14230.0 0.0 0.0 5 0
1424
1425Cu
1426Velocities of Component 1
14270.1 0.2 0.3 5 99
1428"#;
1429 let mut iter = ConFrameIterator::new(text.trim());
1430 let err = iter.next().unwrap().unwrap_err();
1431
1432 assert!(matches!(err, ParseError::ValidationError(_)));
1433 assert!(err.to_string().contains("atom_id mismatch"));
1434 }
1435
1436 #[test]
1437 fn test_validate_true_rejects_section_symbol_mismatch() {
1438 let text = r#"
1439PREBOX1
1440{"con_spec_version":2,"sections":["forces"],"validate":true}
144110.0 20.0 30.0
144290.0 90.0 90.0
1443POSTBOX1
1444POSTBOX2
14451
14461
144763.546
1448Cu
1449Coordinates of Component 1
14500.0 0.0 0.0 5 0
1451
1452H
1453Forces of Component 1
14540.1 0.2 0.3 5 0
1455"#;
1456 let mut iter = ConFrameIterator::new(text.trim());
1457 let err = iter.next().unwrap().unwrap_err();
1458
1459 assert!(matches!(err, ParseError::ValidationError(_)));
1460 assert!(err.to_string().contains("symbol mismatch"));
1461 }
1462
1463 #[test]
1464 fn test_validate_absent_allows_legacy_duplicate_identity_mismatch() {
1465 let text = r#"
1466PREBOX1
1467{"con_spec_version":2,"sections":["velocities"]}
146810.0 20.0 30.0
146990.0 90.0 90.0
1470POSTBOX1
1471POSTBOX2
14721
14731
147463.546
1475Cu
1476Coordinates of Component 1
14770.0 0.0 0.0 5 0
1478
1479Cu
1480Velocities of Component 1
14810.1 0.2 0.3 0 99
1482"#;
1483 let mut iter = ConFrameIterator::new(text.trim());
1484 let frame = iter.next().unwrap().unwrap();
1485
1486 assert!(frame.has_velocities());
1487 assert_eq!(frame.atom_data[0].atom_id, 0);
1488 assert_eq!(frame.atom_data[0].fixed, [true, false, true]);
1489 }
1490
1491 #[test]
1492 fn test_validate_must_be_boolean_when_present() {
1493 let lines = vec![
1494 "PREBOX1",
1495 "{\"con_spec_version\":2,\"validate\":\"yes\",\"sections\":[]}",
1496 "10.0 20.0 30.0",
1497 "90.0 90.0 90.0",
1498 "POSTBOX1",
1499 "POSTBOX2",
1500 "1",
1501 "1",
1502 "12.011",
1503 ];
1504 let mut line_it = lines.iter().copied();
1505 let err = parse_frame_header(&mut line_it).unwrap_err();
1506
1507 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1508 assert!(err.to_string().contains("validate"));
1509 }
1510
1511 #[test]
1512 fn test_validate_true_requires_sections_key() {
1513 let lines = vec![
1514 "PREBOX1",
1515 "{\"con_spec_version\":2,\"validate\":true}",
1516 "10.0 20.0 30.0",
1517 "90.0 90.0 90.0",
1518 "POSTBOX1",
1519 "POSTBOX2",
1520 "1",
1521 "1",
1522 "12.011",
1523 ];
1524 let mut line_it = lines.iter().copied();
1525 let err = parse_frame_header(&mut line_it).unwrap_err();
1526
1527 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1528 assert!(err.to_string().contains("sections"));
1529 }
1530
1531 #[test]
1532 fn test_sections_must_be_string_array_when_present() {
1533 let lines = vec![
1534 "PREBOX1",
1535 "{\"con_spec_version\":2,\"sections\":[\"velocities\",7]}",
1536 "10.0 20.0 30.0",
1537 "90.0 90.0 90.0",
1538 "POSTBOX1",
1539 "POSTBOX2",
1540 "1",
1541 "1",
1542 "12.011",
1543 ];
1544 let mut line_it = lines.iter().copied();
1545 let err = parse_frame_header(&mut line_it).unwrap_err();
1546
1547 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1548 assert!(err.to_string().contains("sections"));
1549 }
1550
1551 #[test]
1552 fn test_validate_true_rejects_non_integer_coordinate_identity_columns() {
1553 let text = r#"
1554PREBOX1
1555{"con_spec_version":2,"sections":[],"validate":true}
155610.0 20.0 30.0
155790.0 90.0 90.0
1558POSTBOX1
1559POSTBOX2
15601
15611
156263.546
1563Cu
1564Coordinates of Component 1
15650.0 0.0 0.0 5.0 0
1566"#;
1567 let mut iter = ConFrameIterator::new(text.trim());
1568 let err = iter.next().unwrap().unwrap_err();
1569
1570 assert!(matches!(err, ParseError::ValidationError(_)));
1571 assert!(err.to_string().contains("fixed_flag"));
1572 }
1573
1574 #[test]
1575 fn test_validate_true_rejects_non_exact_coordinate_label() {
1576 let text = r#"
1577PREBOX1
1578{"con_spec_version":2,"sections":[],"validate":true}
157910.0 20.0 30.0
158090.0 90.0 90.0
1581POSTBOX1
1582POSTBOX2
15831
15841
158563.546
1586Cu
1587Coordinates Component 1
15880.0 0.0 0.0 5 0
1589"#;
1590 let mut iter = ConFrameIterator::new(text.trim());
1591 let err = iter.next().unwrap().unwrap_err();
1592
1593 assert!(matches!(err, ParseError::ValidationError(_)));
1594 assert!(err.to_string().contains("Coordinates of Component 1"));
1595 }
1596
1597 #[test]
1598 fn test_validate_true_rejects_unknown_component_symbol() {
1599 let text = r#"
1600PREBOX1
1601{"con_spec_version":2,"sections":[],"validate":true}
160210.0 20.0 30.0
160390.0 90.0 90.0
1604POSTBOX1
1605POSTBOX2
16061
16071
160863.546
1609Qq
1610Coordinates of Component 1
16110.0 0.0 0.0 0 0
1612"#;
1613 let mut iter = ConFrameIterator::new(text.trim());
1614 let err = iter.next().unwrap().unwrap_err();
1615
1616 assert!(matches!(err, ParseError::ValidationError(_)));
1617 assert!(err.to_string().contains("symbol"));
1618 }
1619
1620 #[test]
1621 fn test_declared_section_must_be_present() {
1622 let text = r#"
1623PREBOX1
1624{"con_spec_version":2,"sections":["velocities"]}
162510.0 20.0 30.0
162690.0 90.0 90.0
1627POSTBOX1
1628POSTBOX2
16291
16301
163163.546
1632Cu
1633Coordinates of Component 1
16340.0 0.0 0.0 0 0
1635"#;
1636 let mut iter = ConFrameIterator::new(text.trim());
1637 let err = iter.next().unwrap().unwrap_err();
1638
1639 assert!(matches!(err, ParseError::IncompleteVelocitySection));
1640 }
1641
1642 #[test]
1643 fn test_non_finite_cell_geometry_rejected_in_strict_mode() {
1644 let lines = vec![
1645 "PREBOX1",
1646 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
1647 "10.0 NaN 30.0",
1648 "90.0 90.0 90.0",
1649 "POSTBOX1",
1650 "POSTBOX2",
1651 "1",
1652 "1",
1653 "12.011",
1654 ];
1655 let mut line_it = lines.iter().copied();
1656 let err = parse_frame_header(&mut line_it).unwrap_err();
1657
1658 assert!(matches!(err, ParseError::ValidationError(_)));
1659 assert!(err.to_string().contains("cell"));
1660 }
1661
1662 #[test]
1663 fn test_validate_true_rejects_non_physical_cell_geometry() {
1664 let lines = vec![
1665 "PREBOX1",
1666 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
1667 "0.0 20.0 30.0",
1668 "90.0 180.0 90.0",
1669 "POSTBOX1",
1670 "POSTBOX2",
1671 "1",
1672 "1",
1673 "12.011",
1674 ];
1675 let mut line_it = lines.iter().copied();
1676 let err = parse_frame_header(&mut line_it).unwrap_err();
1677
1678 assert!(matches!(err, ParseError::ValidationError(_)));
1679 assert!(err.to_string().contains("cell"));
1680 }
1681
1682 #[test]
1683 fn test_validate_true_rejects_reserved_metadata_type_mismatch() {
1684 let lines = vec![
1685 "PREBOX1",
1686 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"energy\":\"low\"}",
1687 "10.0 20.0 30.0",
1688 "90.0 90.0 90.0",
1689 "POSTBOX1",
1690 "POSTBOX2",
1691 "1",
1692 "1",
1693 "12.011",
1694 ];
1695 let mut line_it = lines.iter().copied();
1696 let err = parse_frame_header(&mut line_it).unwrap_err();
1697
1698 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1699 assert!(err.to_string().contains("energy"));
1700 }
1701
1702 #[test]
1703 fn test_validate_true_rejects_malformed_bonds() {
1704 let lines = vec![
1705 "PREBOX1",
1706 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"bonds\":[[0]]}",
1707 "10.0 20.0 30.0",
1708 "90.0 90.0 90.0",
1709 "POSTBOX1",
1710 "POSTBOX2",
1711 "1",
1712 "1",
1713 "12.011",
1714 ];
1715 let mut line_it = lines.iter().copied();
1716 let err = parse_frame_header(&mut line_it).unwrap_err();
1717 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1718 assert!(err.to_string().contains("bonds"));
1719 }
1720
1721 #[test]
1722 fn test_bonds_metadata_round_trip_in_header() {
1723 use crate::types::{meta, Bond};
1724 let lines = vec![
1725 "PREBOX1",
1726 "{\"con_spec_version\":2,\"bonds\":[[0,1],{\"i\":0,\"j\":2,\"order\":1}]}",
1727 "10.0 20.0 30.0",
1728 "90.0 90.0 90.0",
1729 "POSTBOX1",
1730 "POSTBOX2",
1731 "1",
1732 "1",
1733 "12.011",
1734 ];
1735 let mut line_it = lines.iter().copied();
1736 let header = parse_frame_header(&mut line_it).expect("header");
1737 let bonds = header.bonds();
1738 assert_eq!(bonds.len(), 2);
1739 assert_eq!(bonds[0], Bond::new(0, 1));
1740 assert_eq!(bonds[1].i, 0);
1741 assert_eq!(bonds[1].j, 2);
1742 assert_eq!(bonds[1].order, Some(1));
1743 assert!(header.metadata.contains_key(meta::BONDS));
1744 }
1745
1746 #[test]
1747 fn test_parse_velocity_section_absent() {
1748 let lines = vec![
1749 "PREBOX1",
1750 "{\"con_spec_version\":2}",
1751 "10.0 20.0 30.0",
1752 "90.0 90.0 90.0",
1753 "POSTBOX1",
1754 "POSTBOX2",
1755 "1",
1756 "1",
1757 "12.011",
1758 "C",
1759 "Coordinates of Component 1",
1760 "0.0 0.0 0.0 0.0 1",
1761 ];
1762 let mut line_it = lines.iter().copied().peekable();
1763 let mut frame = parse_single_frame(&mut line_it).expect("parse should succeed");
1764 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
1765 .expect("should succeed with no velocities");
1766 assert!(!has_vel);
1767 assert_eq!(frame.atom_data[0].velocity, None);
1768 }
1769
1770 #[test]
1771 fn test_parse_line_of_range_f64_exact() {
1772 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0 42", 4, 5, &[0.0; 5]).unwrap();
1773 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 42.0]);
1774 }
1775
1776 #[test]
1777 fn test_parse_line_of_range_f64_padded() {
1778 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
1779 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0", 4, 5, &defaults).unwrap();
1780 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 99.0]);
1781 }
1782
1783 #[test]
1784 fn test_parse_line_of_range_f64_too_few() {
1785 let result = parse_line_of_range_f64("1.0 2.0 3.0", 4, 5, &[0.0; 5]);
1786 assert!(result.is_err());
1787 }
1788
1789 #[test]
1790 fn test_parse_line_of_range_f64_too_many() {
1791 let result = parse_line_of_range_f64("1.0 2.0 3.0 0.0 5.0 6.0", 4, 5, &[0.0; 5]);
1792 assert!(result.is_err());
1793 }
1794
1795 #[test]
1796 fn test_parse_all_four_column_lines() {
1797 let lines = vec![
1799 "PREBOX1",
1800 "{\"con_spec_version\":2}",
1801 "10.0 10.0 10.0",
1802 "90.0 90.0 90.0",
1803 "POSTBOX1",
1804 "POSTBOX2",
1805 "1",
1806 "3",
1807 "12.011",
1808 "C",
1809 "Coordinates of Component 1",
1810 "0.0 0.0 0.0 0",
1811 "1.0 0.0 0.0 0",
1812 "2.0 0.0 0.0 1",
1813 ];
1814 let mut line_it = lines.iter().copied();
1815 let frame = parse_single_frame(&mut line_it).unwrap();
1816 assert_eq!(frame.atom_data[0].atom_id, 0);
1817 assert_eq!(frame.atom_data[1].atom_id, 1);
1818 assert_eq!(frame.atom_data[2].atom_id, 2);
1819 assert!(frame.atom_data[2].is_fixed());
1820 }
1821}