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::LATTICE_VECTORS => validate_lattice_vectors_metadata(value)?,
155 meta::CONVERGED if !value.is_boolean() => {
156 return Err(metadata_json_error("converged must be a boolean"));
157 }
158 meta::CONVERGED => {}
159 _ => {}
160 }
161 }
162
163 Ok(())
164}
165
166fn validate_pbc_metadata(value: &Value) -> Result<(), ParseError> {
167 let Some(values) = value.as_array() else {
168 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
169 };
170 if values.len() != 3 || values.iter().any(|entry| !entry.is_boolean()) {
171 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
172 }
173 Ok(())
174}
175
176fn validate_lattice_vectors_metadata(value: &Value) -> Result<(), ParseError> {
177 let Some(rows) = value.as_array() else {
178 return Err(metadata_json_error(
179 "lattice_vectors must be a 3x3 numeric array",
180 ));
181 };
182 if rows.len() != 3 {
183 return Err(metadata_json_error(
184 "lattice_vectors must be a 3x3 numeric array",
185 ));
186 }
187 for row in rows {
188 let Some(entries) = row.as_array() else {
189 return Err(metadata_json_error(
190 "lattice_vectors must be a 3x3 numeric array",
191 ));
192 };
193 if entries.len() != 3 || entries.iter().any(|entry| entry.as_f64().is_none()) {
194 return Err(metadata_json_error(
195 "lattice_vectors must be a 3x3 numeric array",
196 ));
197 }
198 }
199 Ok(())
200}
201
202pub fn parse_line_of_n<T: std::str::FromStr>(line: &str, n: usize) -> Result<Vec<T>, ParseError>
230where
231 ParseError: From<<T as std::str::FromStr>::Err>,
232{
233 let values: Vec<T> = line
234 .split_whitespace()
235 .map(|s| s.parse::<T>())
236 .collect::<Result<_, _>>()?;
237
238 if values.len() == n {
239 Ok(values)
240 } else {
241 Err(ParseError::InvalidVectorLength {
242 expected: n,
243 found: values.len(),
244 })
245 }
246}
247
248pub fn parse_frame_header<'a>(
269 lines: &mut impl Iterator<Item = &'a str>,
270) -> Result<FrameHeader, ParseError> {
271 let prebox1 = lines
272 .next()
273 .ok_or(ParseError::IncompleteHeader)?
274 .to_string();
275 let prebox2_raw = lines.next().ok_or(ParseError::IncompleteHeader)?;
276
277 let trimmed = prebox2_raw.trim();
280 let (spec_version, metadata, sections, validate, sections_declared) = if trimmed.starts_with('{') {
281 let json_val: serde_json::Value = serde_json::from_str(trimmed)
282 .map_err(|e| ParseError::InvalidMetadataJson(e.to_string()))?;
283 let json_obj = json_val
284 .as_object()
285 .ok_or_else(|| ParseError::InvalidMetadataJson("expected a JSON object".to_string()))?;
286 let ver = json_obj
287 .get(meta::CON_SPEC_VERSION)
288 .and_then(|v| v.as_u64())
289 .ok_or(ParseError::MissingSpecVersion)? as u32;
290 if ver > crate::CON_SPEC_VERSION {
291 return Err(ParseError::UnsupportedSpecVersion(ver));
292 }
293
294 let mut sections: Vec<String> = Vec::new();
298 let mut metadata = BTreeMap::new();
299 let mut sections_declared = false;
300 let mut validate = false;
301 for (k, v) in json_obj {
302 match k.as_str() {
303 meta::CON_SPEC_VERSION => {}
304 meta::SECTIONS => {
305 sections_declared = true;
306 let arr = v.as_array().ok_or_else(|| {
307 metadata_json_error("sections must be an array of strings")
308 })?;
309 sections.reserve(arr.len());
310 for entry in arr {
311 let s = entry.as_str().ok_or_else(|| {
312 metadata_json_error("sections must be an array of strings")
313 })?;
314 sections.push(s.to_string());
315 }
316 }
317 meta::VALIDATE => {
318 validate = match v {
319 Value::Bool(b) => *b,
320 _ => return Err(metadata_json_error("validate must be a boolean")),
321 };
322 metadata.insert(k.clone(), v.clone());
323 }
324 _ => {
325 metadata.insert(k.clone(), v.clone());
326 }
327 }
328 }
329
330 if validate {
333 validate_metadata_schema(json_obj)?;
334 }
335
336 (ver, metadata, sections, validate, sections_declared)
337 } else {
338 (1_u32, BTreeMap::new(), Vec::new(), false, false)
340 };
341 let prebox2 = prebox2_raw.to_string();
342
343 let boxl_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
344 let angles_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
345 let postbox1 = lines
346 .next()
347 .ok_or(ParseError::IncompleteHeader)?
348 .to_string();
349 let postbox2 = lines
350 .next()
351 .ok_or(ParseError::IncompleteHeader)?
352 .to_string();
353 let natm_types =
354 parse_line_of_n::<usize>(lines.next().ok_or(ParseError::IncompleteHeader)?, 1)?[0];
355 let natms_per_type = parse_line_of_n::<usize>(
356 lines.next().ok_or(ParseError::IncompleteHeader)?,
357 natm_types,
358 )?;
359 let masses_per_type = parse_line_of_n_f64(
360 lines.next().ok_or(ParseError::IncompleteHeader)?,
361 natm_types,
362 )?;
363 if validate {
364 validate_header_geometry(&boxl_vec, &angles_vec, natm_types, &natms_per_type)?;
365 validate_masses(&masses_per_type)?;
366 }
367 Ok(FrameHeader {
368 prebox_header: PreboxHeader {
369 user: prebox1,
370 metadata_line: prebox2,
371 },
372 boxl: boxl_vec.try_into().unwrap(),
373 angles: angles_vec.try_into().unwrap(),
374 postbox_header: [postbox1, postbox2],
375 natm_types,
376 natms_per_type,
377 masses_per_type,
378 spec_version,
379 metadata,
380 sections,
381 strict_validation: validate,
382 sections_declared,
383 })
384}
385
386pub fn parse_single_frame<'a>(
435 lines: &mut impl Iterator<Item = &'a str>,
436) -> Result<ConFrame, ParseError> {
437 let header = parse_frame_header(lines)?;
438 let validate = header.strict_validation;
439 let total_atoms: usize = header.natms_per_type.iter().sum();
440 let mut atom_data = Vec::with_capacity(total_atoms);
441
442 let mut global_atom_idx: u64 = 0;
443 for (type_idx, num_atoms) in header.natms_per_type.iter().enumerate() {
444 let symbol_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
448 let symbol: Arc<str> = Arc::from(symbol_line.trim());
449 let coord_label = lines.next().ok_or(ParseError::IncompleteFrame)?;
450 if validate {
451 validate_coordinate_component(type_idx, symbol.as_ref(), coord_label)?;
452 }
453 for _ in 0..*num_atoms {
454 let coord_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
455 let defaults = [0.0, 0.0, 0.0, 0.0, global_atom_idx as f64];
457 let vals = parse_line_of_range_f64(coord_line, 4, 5, &defaults)?;
458 let (fixed, atom_id) = if validate {
459 parse_identity_columns(coord_line, "coordinate", 3, 4, 5)?
460 } else {
461 (decode_fixed_bitmask(vals[3] as u8), vals[4] as u64)
462 };
463 atom_data.push(AtomDatum {
464 symbol: Arc::clone(&symbol),
466 x: vals[0],
467 y: vals[1],
468 z: vals[2],
469 fixed,
470 atom_id,
471 velocity: None,
472 force: None,
473 energy: None,
474 });
475 global_atom_idx += 1;
476 }
477 }
478 Ok(ConFrame { header, atom_data })
479}
480
481fn validate_header_geometry(
482 boxl: &[f64],
483 angles: &[f64],
484 natm_types: usize,
485 natms_per_type: &[usize],
486) -> Result<(), ParseError> {
487 if boxl.iter().any(|length| !length.is_finite() || *length <= 0.0)
488 || angles
489 .iter()
490 .any(|angle| !angle.is_finite() || *angle <= 0.0 || *angle >= 180.0)
491 {
492 return Err(ParseError::ValidationError(
493 "cell geometry must have positive lengths and angles between 0 and 180 degrees"
494 .to_string(),
495 ));
496 }
497 if natm_types == 0 || natms_per_type.contains(&0) {
498 return Err(ParseError::ValidationError(
499 "atom counts must contain at least one atom per component".to_string(),
500 ));
501 }
502 Ok(())
503}
504
505fn validate_masses(masses_per_type: &[f64]) -> Result<(), ParseError> {
506 if masses_per_type
507 .iter()
508 .any(|mass| !mass.is_finite() || *mass <= 0.0)
509 {
510 return Err(ParseError::ValidationError(
511 "component masses must be positive".to_string(),
512 ));
513 }
514 Ok(())
515}
516
517fn validate_coordinate_component(
518 type_idx: usize,
519 symbol: &str,
520 label: &str,
521) -> Result<(), ParseError> {
522 let expected_label = format!("Coordinates of Component {}", type_idx + 1);
523 if label.trim() != expected_label {
524 return Err(ParseError::ValidationError(format!(
525 "expected coordinate label {expected_label:?}, found {label:?}"
526 )));
527 }
528 if symbol != "X" && symbol_to_atomic_number(symbol) == 0 {
529 return Err(ParseError::ValidationError(format!(
530 "unknown component symbol {symbol}"
531 )));
532 }
533 Ok(())
534}
535
536fn parse_identity_columns(
551 line: &str,
552 row_kind: &str,
553 fixed_idx: usize,
554 atom_id_idx: usize,
555 n_cols: usize,
556) -> Result<([bool; 3], u64), ParseError> {
557 let columns = line.split_ascii_whitespace().collect::<Vec<_>>();
558 if columns.len() != n_cols {
559 return Err(ParseError::ValidationError(format!(
560 "{row_kind} rows require {n_cols} columns including fixed_flag and atom_id in validate mode"
561 )));
562 }
563 let fixed_flag = columns[fixed_idx].parse::<u8>().map_err(|_| {
564 ParseError::ValidationError(format!("{row_kind} fixed_flag must be an integer bitmask"))
565 })?;
566 if fixed_flag > 7 {
567 return Err(ParseError::ValidationError(format!(
568 "{row_kind} fixed_flag must be between 0 and 7"
569 )));
570 }
571 let atom_id = columns[atom_id_idx].parse::<u64>().map_err(|_| {
572 ParseError::ValidationError(format!("{row_kind} atom_id must be an integer"))
573 })?;
574 Ok((decode_fixed_bitmask(fixed_flag), atom_id))
575}
576
577
578fn validate_section_component(
579 section: &str,
580 type_idx: usize,
581 atom_idx: usize,
582 symbol: &str,
583 label: &str,
584 header: &FrameHeader,
585 atom_data: &[AtomDatum],
586) -> Result<(), ParseError> {
587 let expected_label = format!("{section} of Component {}", type_idx + 1);
588 if label.trim() != expected_label {
589 return Err(ParseError::ValidationError(format!(
590 "expected section label {expected_label:?}, found {label:?}"
591 )));
592 }
593
594 if header.natms_per_type[type_idx] == 0 {
595 return Ok(());
596 }
597
598 let expected_symbol = atom_data
599 .get(atom_idx)
600 .map(|atom| atom.symbol.as_ref())
601 .ok_or_else(|| {
602 ParseError::ValidationError(format!(
603 "{section} component {} has no coordinate atom to validate against",
604 type_idx + 1
605 ))
606 })?;
607 if symbol != expected_symbol {
608 return Err(ParseError::ValidationError(format!(
609 "{section} component {} symbol mismatch: expected {expected_symbol}, found {symbol}",
610 type_idx + 1
611 )));
612 }
613
614 Ok(())
615}
616
617fn validate_section_atom_identity(
618 section: &str,
619 atom_idx: usize,
620 fixed: [bool; 3],
621 atom_id: u64,
622 atom_data: &[AtomDatum],
623) -> Result<(), ParseError> {
624 let atom = atom_data.get(atom_idx).ok_or_else(|| {
625 ParseError::ValidationError(format!(
626 "{section} row {atom_idx} has no coordinate atom to validate against"
627 ))
628 })?;
629
630 if atom.fixed != fixed {
631 return Err(ParseError::ValidationError(format!(
632 "{section} row {atom_idx} fixed mask mismatch for atom_id {}",
633 atom.atom_id
634 )));
635 }
636 if atom.atom_id != atom_id {
637 return Err(ParseError::ValidationError(format!(
638 "{section} row {atom_idx} atom_id mismatch: expected {}, found {atom_id}",
639 atom.atom_id
640 )));
641 }
642
643 Ok(())
644}
645
646pub fn parse_velocity_section<'a, I>(
659 lines: &mut Peekable<I>,
660 header: &FrameHeader,
661 atom_data: &mut [AtomDatum],
662) -> Result<bool, ParseError>
663where
664 I: Iterator<Item = &'a str>,
665{
666 let validate = header.strict_validation;
667 match lines.peek() {
669 Some(line) if line.trim().is_empty() => {
670 lines.next();
672 }
673 _ => return Ok(false),
674 }
675
676 let mut atom_idx: usize = 0;
677 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
678 let symbol = lines
680 .next()
681 .ok_or(ParseError::IncompleteVelocitySection)?
682 .trim();
683
684 let comp_line = lines.next().ok_or(ParseError::IncompleteVelocitySection)?;
686 if !comp_line.contains("Velocities of Component") {
688 return Err(ParseError::IncompleteVelocitySection);
689 }
690 if validate {
691 validate_section_component(
692 "Velocities",
693 type_idx,
694 atom_idx,
695 symbol,
696 comp_line,
697 header,
698 atom_data,
699 )?;
700 }
701
702 for _ in 0..num_atoms {
703 let vel_line = lines.next().ok_or(ParseError::IncompleteVelocitySection)?;
704 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
706 let vals = parse_line_of_range_f64(vel_line, 4, 5, &defaults)?;
707 if validate {
708 let (fixed, atom_id) =
709 parse_identity_columns(vel_line, "velocities", 3, 4, 5)?;
710 validate_section_atom_identity("velocities", atom_idx, fixed, atom_id, atom_data)?;
711 }
712 if atom_idx < atom_data.len() {
713 atom_data[atom_idx].velocity = Some([vals[0], vals[1], vals[2]]);
714 }
715 atom_idx += 1;
716 }
717 }
718
719 Ok(true)
720}
721
722pub fn parse_force_section<'a, I>(
730 lines: &mut Peekable<I>,
731 header: &FrameHeader,
732 atom_data: &mut [AtomDatum],
733) -> Result<bool, ParseError>
734where
735 I: Iterator<Item = &'a str>,
736{
737 let validate = header.strict_validation;
738 match lines.peek() {
740 Some(line) if line.trim().is_empty() => {
741 lines.next();
742 }
743 _ => return Ok(false),
744 }
745
746 let mut atom_idx: usize = 0;
747 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
748 let symbol = lines
749 .next()
750 .ok_or(ParseError::IncompleteForceSection)?
751 .trim();
752
753 let comp_line = lines.next().ok_or(ParseError::IncompleteForceSection)?;
754 if !comp_line.contains("Forces of Component") {
755 return Err(ParseError::IncompleteForceSection);
756 }
757 if validate {
758 validate_section_component(
759 "Forces", type_idx, atom_idx, symbol, comp_line, header, atom_data,
760 )?;
761 }
762
763 for _ in 0..num_atoms {
764 let force_line = lines.next().ok_or(ParseError::IncompleteForceSection)?;
765 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
766 let vals = parse_line_of_range_f64(force_line, 4, 5, &defaults)?;
767 if validate {
768 let (fixed, atom_id) =
769 parse_identity_columns(force_line, "forces", 3, 4, 5)?;
770 validate_section_atom_identity("forces", atom_idx, fixed, atom_id, atom_data)?;
771 }
772 if atom_idx < atom_data.len() {
773 atom_data[atom_idx].force = Some([vals[0], vals[1], vals[2]]);
774 }
775 atom_idx += 1;
776 }
777 }
778
779 Ok(true)
780}
781
782pub fn parse_energy_section<'a, I>(
795 lines: &mut Peekable<I>,
796 header: &FrameHeader,
797 atom_data: &mut [AtomDatum],
798) -> Result<bool, ParseError>
799where
800 I: Iterator<Item = &'a str>,
801{
802 let validate = header.strict_validation;
803 match lines.peek() {
804 Some(line) if line.trim().is_empty() => {
805 lines.next();
806 }
807 _ => return Ok(false),
808 }
809
810 let mut atom_idx: usize = 0;
811 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
812 let symbol = lines
813 .next()
814 .ok_or(ParseError::IncompleteEnergySection)?
815 .trim();
816
817 let comp_line = lines.next().ok_or(ParseError::IncompleteEnergySection)?;
818 if !comp_line.contains("Energies of Component") {
819 return Err(ParseError::IncompleteEnergySection);
820 }
821 if validate {
822 validate_section_component(
823 "Energies", type_idx, atom_idx, symbol, comp_line, header, atom_data,
824 )?;
825 }
826
827 for _ in 0..num_atoms {
828 let energy_line = lines.next().ok_or(ParseError::IncompleteEnergySection)?;
829 let defaults = [0.0, 0.0, atom_idx as f64];
832 let vals = parse_line_of_range_f64(energy_line, 1, 3, &defaults)?;
833 if validate {
834 let (fixed, atom_id) =
835 parse_identity_columns(energy_line, "energies", 1, 2, 3)?;
836 validate_section_atom_identity("energies", atom_idx, fixed, atom_id, atom_data)?;
837 }
838 if atom_idx < atom_data.len() {
839 atom_data[atom_idx].energy = Some(vals[0]);
840 }
841 atom_idx += 1;
842 }
843 }
844
845 Ok(true)
846}
847
848pub fn parse_declared_sections<'a, I>(
854 lines: &mut Peekable<I>,
855 header: &mut FrameHeader,
856 atom_data: &mut [AtomDatum],
857) -> Result<(), ParseError>
858where
859 I: Iterator<Item = &'a str>,
860{
861 if !header.sections_declared && header.sections.is_empty() {
862 let found = parse_velocity_section(lines, header, atom_data)?;
864 if found {
865 header.sections.push(SECTION_VELOCITIES.into());
866 }
867 } else {
868 let sections = std::mem::take(&mut header.sections);
869 for section in §ions {
870 match section.as_str() {
871 SECTION_VELOCITIES => {
872 let found = parse_velocity_section(lines, header, atom_data)?;
873 if !found {
874 return Err(ParseError::IncompleteVelocitySection);
875 }
876 }
877 SECTION_FORCES => {
878 let found = parse_force_section(lines, header, atom_data)?;
879 if !found {
880 return Err(ParseError::IncompleteForceSection);
881 }
882 }
883 SECTION_ENERGIES => {
884 let found = parse_energy_section(lines, header, atom_data)?;
885 if !found {
886 return Err(ParseError::IncompleteEnergySection);
887 }
888 }
889 other => return Err(ParseError::UnknownSection(other.to_string())),
890 }
891 }
892 header.sections = sections;
893 }
894 Ok(())
895}
896
897#[cfg(test)]
898mod tests {
899 use super::*;
900 use crate::iterators::ConFrameIterator;
901
902 #[test]
903 fn test_parse_line_of_n_success() {
904 let line = "1.0 2.5 -3.0";
905 let values = parse_line_of_n::<f64>(line, 3).unwrap();
906 assert_eq!(values, vec![1.0, 2.5, -3.0]);
907 }
908
909 #[test]
910 fn test_parse_line_of_n_too_short() {
911 let line = "1.0 2.5";
912 let result = parse_line_of_n::<f64>(line, 3);
913 assert!(result.is_err());
914 assert!(matches!(
915 result.unwrap_err(),
916 ParseError::InvalidVectorLength {
917 expected: 3,
918 found: 2
919 }
920 ));
921 }
922
923 #[test]
924 fn test_parse_line_of_n_too_long() {
925 let line = "1.0 2.5 -3.0 4.0";
926 let result = parse_line_of_n::<f64>(line, 3);
927 assert!(result.is_err());
928 assert!(matches!(
929 result.unwrap_err(),
930 ParseError::InvalidVectorLength {
931 expected: 3,
932 found: 4
933 }
934 ));
935 }
936
937 #[test]
938 fn test_parse_line_of_n_invalid_float() {
939 let line = "1.0 abc -3.0";
940 let result = parse_line_of_n::<f64>(line, 3);
941 assert!(result.is_err());
942 assert!(matches!(
943 result.unwrap_err(),
944 ParseError::InvalidNumberFormat(_)
945 ));
946 }
947
948 #[test]
949 fn test_parse_frame_header_success() {
950 let lines = [
951 "PREBOX1",
952 "{\"con_spec_version\":2}",
953 "10.0 20.0 30.0",
954 "90.0 90.0 90.0",
955 "POSTBOX1",
956 "POSTBOX2",
957 "2",
958 "1 1",
959 "12.011 1.008",
960 ];
961 let mut line_it = lines.iter().copied();
962 match parse_frame_header(&mut line_it) {
963 Ok(header) => {
964 assert_eq!(header.prebox_header.user, "PREBOX1");
965 assert_eq!(header.spec_version, 2);
966 assert_eq!(header.boxl, [10.0, 20.0, 30.0]);
967 assert_eq!(header.angles, [90.0, 90.0, 90.0]);
968 assert_eq!(header.postbox_header, ["POSTBOX1", "POSTBOX2"]);
969 assert_eq!(header.natm_types, 2);
970 assert_eq!(header.natms_per_type, vec![1, 1]);
971 assert_eq!(header.masses_per_type, vec![12.011, 1.008]);
972 }
973 Err(e) => {
974 panic!(
975 "Parsing failed when it should have succeeded. Error: {:?}",
976 e
977 );
978 }
979 }
980 }
981
982 #[test]
983 fn test_parse_frame_header_missing_line() {
984 let lines = [
985 "PREBOX1",
986 "{\"con_spec_version\":2}",
987 "10.0 20.0 30.0",
988 "90.0 90.0 90.0",
989 "POSTBOX1",
990 "POSTBOX2",
991 "2",
992 "1 1",
993 ];
995 let mut line_it = lines.iter().copied();
996 let result = parse_frame_header(&mut line_it);
997 assert!(result.is_err());
998 assert!(matches!(result.unwrap_err(), ParseError::IncompleteHeader));
999 }
1000
1001 #[test]
1002 fn test_parse_frame_header_missing_spec_version() {
1003 let lines = vec![
1004 "PREBOX1",
1005 "{}",
1006 "10.0 20.0 30.0",
1007 "90.0 90.0 90.0",
1008 "POSTBOX1",
1009 "POSTBOX2",
1010 "2",
1011 "1 1",
1012 "12.011 1.008",
1013 ];
1014 let mut line_it = lines.iter().copied();
1015 let result = parse_frame_header(&mut line_it);
1016 assert!(result.is_err());
1017 assert!(matches!(
1018 result.unwrap_err(),
1019 ParseError::MissingSpecVersion
1020 ));
1021 }
1022
1023 #[test]
1024 fn test_parse_frame_header_legacy_no_json() {
1025 let lines = vec![
1027 "PREBOX1",
1028 "0.0000 TIME",
1029 "10.0 20.0 30.0",
1030 "90.0 90.0 90.0",
1031 "POSTBOX1",
1032 "POSTBOX2",
1033 "2",
1034 "1 1",
1035 "12.011 1.008",
1036 ];
1037 let mut line_it = lines.iter().copied();
1038 let header = parse_frame_header(&mut line_it).unwrap();
1039 assert_eq!(header.spec_version, 1);
1040 assert!(header.metadata.is_empty());
1041 }
1042
1043 #[test]
1044 fn test_parse_frame_header_malformed_json() {
1045 let lines = vec![
1047 "PREBOX1",
1048 "{broken json",
1049 "10.0 20.0 30.0",
1050 "90.0 90.0 90.0",
1051 "POSTBOX1",
1052 "POSTBOX2",
1053 "2",
1054 "1 1",
1055 "12.011 1.008",
1056 ];
1057 let mut line_it = lines.iter().copied();
1058 let result = parse_frame_header(&mut line_it);
1059 assert!(result.is_err());
1060 assert!(matches!(
1061 result.unwrap_err(),
1062 ParseError::InvalidMetadataJson(_)
1063 ));
1064 }
1065
1066 #[test]
1067 fn test_parse_frame_header_unsupported_version() {
1068 let lines = vec![
1069 "PREBOX1",
1070 "{\"con_spec_version\":999}",
1071 "10.0 20.0 30.0",
1072 "90.0 90.0 90.0",
1073 "POSTBOX1",
1074 "POSTBOX2",
1075 "2",
1076 "1 1",
1077 "12.011 1.008",
1078 ];
1079 let mut line_it = lines.iter().copied();
1080 let result = parse_frame_header(&mut line_it);
1081 assert!(result.is_err());
1082 assert!(matches!(
1083 result.unwrap_err(),
1084 ParseError::UnsupportedSpecVersion(999)
1085 ));
1086 }
1087
1088 #[test]
1089 fn test_parse_frame_header_extra_metadata_preserved() {
1090 let lines = vec![
1091 "PREBOX1",
1092 "{\"con_spec_version\":2,\"generator\":\"test\"}",
1093 "10.0 20.0 30.0",
1094 "90.0 90.0 90.0",
1095 "POSTBOX1",
1096 "POSTBOX2",
1097 "2",
1098 "1 1",
1099 "12.011 1.008",
1100 ];
1101 let mut line_it = lines.iter().copied();
1102 let header = parse_frame_header(&mut line_it).unwrap();
1103 assert_eq!(header.spec_version, 2);
1104 assert_eq!(
1105 header.metadata.get("generator"),
1106 Some(&serde_json::Value::String("test".to_string()))
1107 );
1108 }
1109
1110 #[test]
1111 fn test_parse_frame_header_invalid_natms_per_type() {
1112 let lines = vec![
1113 "PREBOX1",
1114 "{\"con_spec_version\":2}",
1115 "10.0 20.0 30.0",
1116 "90.0 90.0 90.0",
1117 "POSTBOX1",
1118 "POSTBOX2",
1119 "2",
1120 "1 1 1", "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::InvalidVectorLength {
1129 expected: 2,
1130 found: 3
1131 }
1132 ));
1133 }
1134
1135 #[test]
1136 fn test_parse_single_frame_success() {
1137 let lines = vec![
1138 "PREBOX1",
1139 "{\"con_spec_version\":2}",
1140 "10.0 20.0 30.0",
1141 "90.0 90.0 90.0",
1142 "POSTBOX1",
1143 "POSTBOX2",
1144 "2",
1145 "3 3",
1146 "12.011 1.008",
1147 "1",
1148 "Coordinates of Component 1",
1149 "0.0 0.0 0.0 0.0 1",
1150 "1.0940 0.0 0.0 0.0 2",
1151 "-0.5470 0.9499 0.0 0.0 3",
1152 "2",
1153 "Coordinates of Component 2",
1154 "5.0 5.0 5.0 0.0 4",
1155 "6.0940 5.0 5.0 0.0 5",
1156 "5.5470 5.9499 5.0 0.0 6",
1157 ];
1158 let mut line_it = lines.iter().copied();
1159 let frame = parse_single_frame(&mut line_it).unwrap();
1160
1161 assert_eq!(frame.header.natm_types, 2);
1162 assert_eq!(frame.header.natms_per_type, vec![3, 3]);
1163 assert_eq!(frame.header.masses_per_type, vec![12.011, 1.008]);
1164 assert_eq!(frame.atom_data.len(), 6);
1165 assert_eq!(&*frame.atom_data[0].symbol, "1");
1166 assert_eq!(frame.atom_data[0].atom_id, 1);
1167 assert_eq!(&*frame.atom_data[5].symbol, "2");
1168 assert_eq!(frame.atom_data[5].atom_id, 6);
1169 }
1170
1171 #[test]
1172 fn test_parse_single_frame_missing_line() {
1173 let lines = vec![
1175 "PREBOX1",
1176 "{\"con_spec_version\":2}",
1177 "10.0 20.0 30.0",
1178 "90.0 90.0 90.0",
1179 "POSTBOX1",
1180 "POSTBOX2",
1181 "2",
1182 "3 3",
1183 "12.011 1.008",
1184 "1",
1185 "Coordinates of Component 1",
1186 "0.0 0.0 0.0 0.0 1",
1187 "1.0940 0.0 0.0 0.0 2",
1188 "-0.5470 0.9499 0.0 0.0 3",
1189 ];
1191 let mut line_it = lines.iter().copied();
1192 let result = parse_single_frame(&mut line_it);
1193 assert!(result.is_err());
1194 assert!(matches!(result.unwrap_err(), ParseError::IncompleteFrame));
1195 }
1196
1197 #[test]
1198 fn test_parse_single_frame_missing_atom_index_defaults_sequential() {
1199 let lines = vec![
1201 "PREBOX1",
1202 "{\"con_spec_version\":2}",
1203 "10.0 20.0 30.0",
1204 "90.0 90.0 90.0",
1205 "POSTBOX1",
1206 "POSTBOX2",
1207 "2",
1208 "3 3",
1209 "12.011 1.008",
1210 "1",
1211 "Coordinates of Component 1",
1212 "0.0 0.0 0.0 0.0 1",
1213 "1.0940 0.0 0.0 0.0 2",
1214 "-0.5470 0.9499 0.0 0.0 3",
1215 "2",
1216 "Coordinates of Component 2",
1217 "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", ];
1221 let mut line_it = lines.iter().copied();
1222 let frame = parse_single_frame(&mut line_it).unwrap();
1223 assert_eq!(frame.atom_data.len(), 6);
1224 assert_eq!(frame.atom_data[0].atom_id, 1);
1226 assert_eq!(frame.atom_data[1].atom_id, 2);
1227 assert_eq!(frame.atom_data[2].atom_id, 3);
1228 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); }
1233
1234 #[test]
1235 fn test_parse_single_frame_too_few_columns_fails() {
1236 let lines = vec![
1238 "PREBOX1",
1239 "{\"con_spec_version\":2}",
1240 "10.0 20.0 30.0",
1241 "90.0 90.0 90.0",
1242 "POSTBOX1",
1243 "POSTBOX2",
1244 "1",
1245 "1",
1246 "12.011",
1247 "C",
1248 "Coordinates of Component 1",
1249 "0.0 0.0 0.0", ];
1251 let mut line_it = lines.iter().copied();
1252 let result = parse_single_frame(&mut line_it);
1253 assert!(result.is_err());
1254 assert!(matches!(
1255 result.unwrap_err(),
1256 ParseError::InvalidVectorLength {
1257 expected: 5,
1258 found: 3
1259 }
1260 ));
1261 }
1262
1263 #[test]
1264 fn test_parse_velocity_section_present() {
1265 let lines = vec![
1266 "PREBOX1",
1267 "{\"con_spec_version\":2}",
1268 "10.0 20.0 30.0",
1269 "90.0 90.0 90.0",
1270 "POSTBOX1",
1271 "POSTBOX2",
1272 "2",
1273 "1 1",
1274 "63.546 1.008",
1275 "Cu",
1276 "Coordinates of Component 1",
1277 "0.0 0.0 0.0 1.0 0",
1278 "H",
1279 "Coordinates of Component 2",
1280 "1.0 2.0 3.0 0.0 1",
1281 "",
1282 "Cu",
1283 "Velocities of Component 1",
1284 "0.1 0.2 0.3 1.0 0",
1285 "H",
1286 "Velocities of Component 2",
1287 "0.4 0.5 0.6 0.0 1",
1288 ];
1289 let mut line_it = lines.iter().copied().peekable();
1290 let mut frame =
1292 parse_single_frame(&mut line_it).expect("coordinate parsing should succeed");
1293 assert!(!frame.has_velocities());
1294
1295 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
1297 .expect("velocity parsing should succeed");
1298 assert!(has_vel);
1299 assert_eq!(frame.atom_data[0].velocity, Some([0.1, 0.2, 0.3]));
1300 assert_eq!(frame.atom_data[1].velocity, Some([0.4, 0.5, 0.6]));
1301 }
1302
1303 #[test]
1304 fn test_validate_true_accepts_matching_section_identity() {
1305 let text = r#"
1306PREBOX1
1307{"con_spec_version":2,"sections":["velocities"],"validate":true}
130810.0 20.0 30.0
130990.0 90.0 90.0
1310POSTBOX1
1311POSTBOX2
13122
13131 1
131463.546 1.008
1315Cu
1316Coordinates of Component 1
13170.0 0.0 0.0 5 0
1318H
1319Coordinates of Component 2
13201.0 2.0 3.0 0 1
1321
1322Cu
1323Velocities of Component 1
13240.1 0.2 0.3 5 0
1325H
1326Velocities of Component 2
13270.4 0.5 0.6 0 1
1328"#;
1329 let mut iter = ConFrameIterator::new(text.trim());
1330 let frame = iter.next().unwrap().unwrap();
1331
1332 assert!(frame.has_velocities());
1333 assert_eq!(
1334 frame
1335 .header
1336 .metadata
1337 .get("validate")
1338 .and_then(|v| v.as_bool()),
1339 Some(true)
1340 );
1341 }
1342
1343 #[test]
1344 fn test_validate_true_rejects_section_atom_id_mismatch() {
1345 let text = r#"
1346PREBOX1
1347{"con_spec_version":2,"sections":["velocities"],"validate":true}
134810.0 20.0 30.0
134990.0 90.0 90.0
1350POSTBOX1
1351POSTBOX2
13521
13531
135463.546
1355Cu
1356Coordinates of Component 1
13570.0 0.0 0.0 5 0
1358
1359Cu
1360Velocities of Component 1
13610.1 0.2 0.3 5 99
1362"#;
1363 let mut iter = ConFrameIterator::new(text.trim());
1364 let err = iter.next().unwrap().unwrap_err();
1365
1366 assert!(matches!(err, ParseError::ValidationError(_)));
1367 assert!(err.to_string().contains("atom_id mismatch"));
1368 }
1369
1370 #[test]
1371 fn test_validate_true_rejects_section_symbol_mismatch() {
1372 let text = r#"
1373PREBOX1
1374{"con_spec_version":2,"sections":["forces"],"validate":true}
137510.0 20.0 30.0
137690.0 90.0 90.0
1377POSTBOX1
1378POSTBOX2
13791
13801
138163.546
1382Cu
1383Coordinates of Component 1
13840.0 0.0 0.0 5 0
1385
1386H
1387Forces of Component 1
13880.1 0.2 0.3 5 0
1389"#;
1390 let mut iter = ConFrameIterator::new(text.trim());
1391 let err = iter.next().unwrap().unwrap_err();
1392
1393 assert!(matches!(err, ParseError::ValidationError(_)));
1394 assert!(err.to_string().contains("symbol mismatch"));
1395 }
1396
1397 #[test]
1398 fn test_validate_absent_allows_legacy_duplicate_identity_mismatch() {
1399 let text = r#"
1400PREBOX1
1401{"con_spec_version":2,"sections":["velocities"]}
140210.0 20.0 30.0
140390.0 90.0 90.0
1404POSTBOX1
1405POSTBOX2
14061
14071
140863.546
1409Cu
1410Coordinates of Component 1
14110.0 0.0 0.0 5 0
1412
1413Cu
1414Velocities of Component 1
14150.1 0.2 0.3 0 99
1416"#;
1417 let mut iter = ConFrameIterator::new(text.trim());
1418 let frame = iter.next().unwrap().unwrap();
1419
1420 assert!(frame.has_velocities());
1421 assert_eq!(frame.atom_data[0].atom_id, 0);
1422 assert_eq!(frame.atom_data[0].fixed, [true, false, true]);
1423 }
1424
1425 #[test]
1426 fn test_validate_must_be_boolean_when_present() {
1427 let lines = vec![
1428 "PREBOX1",
1429 "{\"con_spec_version\":2,\"validate\":\"yes\",\"sections\":[]}",
1430 "10.0 20.0 30.0",
1431 "90.0 90.0 90.0",
1432 "POSTBOX1",
1433 "POSTBOX2",
1434 "1",
1435 "1",
1436 "12.011",
1437 ];
1438 let mut line_it = lines.iter().copied();
1439 let err = parse_frame_header(&mut line_it).unwrap_err();
1440
1441 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1442 assert!(err.to_string().contains("validate"));
1443 }
1444
1445 #[test]
1446 fn test_validate_true_requires_sections_key() {
1447 let lines = vec![
1448 "PREBOX1",
1449 "{\"con_spec_version\":2,\"validate\":true}",
1450 "10.0 20.0 30.0",
1451 "90.0 90.0 90.0",
1452 "POSTBOX1",
1453 "POSTBOX2",
1454 "1",
1455 "1",
1456 "12.011",
1457 ];
1458 let mut line_it = lines.iter().copied();
1459 let err = parse_frame_header(&mut line_it).unwrap_err();
1460
1461 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1462 assert!(err.to_string().contains("sections"));
1463 }
1464
1465 #[test]
1466 fn test_sections_must_be_string_array_when_present() {
1467 let lines = vec![
1468 "PREBOX1",
1469 "{\"con_spec_version\":2,\"sections\":[\"velocities\",7]}",
1470 "10.0 20.0 30.0",
1471 "90.0 90.0 90.0",
1472 "POSTBOX1",
1473 "POSTBOX2",
1474 "1",
1475 "1",
1476 "12.011",
1477 ];
1478 let mut line_it = lines.iter().copied();
1479 let err = parse_frame_header(&mut line_it).unwrap_err();
1480
1481 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1482 assert!(err.to_string().contains("sections"));
1483 }
1484
1485 #[test]
1486 fn test_validate_true_rejects_non_integer_coordinate_identity_columns() {
1487 let text = r#"
1488PREBOX1
1489{"con_spec_version":2,"sections":[],"validate":true}
149010.0 20.0 30.0
149190.0 90.0 90.0
1492POSTBOX1
1493POSTBOX2
14941
14951
149663.546
1497Cu
1498Coordinates of Component 1
14990.0 0.0 0.0 5.0 0
1500"#;
1501 let mut iter = ConFrameIterator::new(text.trim());
1502 let err = iter.next().unwrap().unwrap_err();
1503
1504 assert!(matches!(err, ParseError::ValidationError(_)));
1505 assert!(err.to_string().contains("fixed_flag"));
1506 }
1507
1508 #[test]
1509 fn test_validate_true_rejects_non_exact_coordinate_label() {
1510 let text = r#"
1511PREBOX1
1512{"con_spec_version":2,"sections":[],"validate":true}
151310.0 20.0 30.0
151490.0 90.0 90.0
1515POSTBOX1
1516POSTBOX2
15171
15181
151963.546
1520Cu
1521Coordinates Component 1
15220.0 0.0 0.0 5 0
1523"#;
1524 let mut iter = ConFrameIterator::new(text.trim());
1525 let err = iter.next().unwrap().unwrap_err();
1526
1527 assert!(matches!(err, ParseError::ValidationError(_)));
1528 assert!(err.to_string().contains("Coordinates of Component 1"));
1529 }
1530
1531 #[test]
1532 fn test_validate_true_rejects_unknown_component_symbol() {
1533 let text = r#"
1534PREBOX1
1535{"con_spec_version":2,"sections":[],"validate":true}
153610.0 20.0 30.0
153790.0 90.0 90.0
1538POSTBOX1
1539POSTBOX2
15401
15411
154263.546
1543Qq
1544Coordinates of Component 1
15450.0 0.0 0.0 0 0
1546"#;
1547 let mut iter = ConFrameIterator::new(text.trim());
1548 let err = iter.next().unwrap().unwrap_err();
1549
1550 assert!(matches!(err, ParseError::ValidationError(_)));
1551 assert!(err.to_string().contains("symbol"));
1552 }
1553
1554 #[test]
1555 fn test_declared_section_must_be_present() {
1556 let text = r#"
1557PREBOX1
1558{"con_spec_version":2,"sections":["velocities"]}
155910.0 20.0 30.0
156090.0 90.0 90.0
1561POSTBOX1
1562POSTBOX2
15631
15641
156563.546
1566Cu
1567Coordinates of Component 1
15680.0 0.0 0.0 0 0
1569"#;
1570 let mut iter = ConFrameIterator::new(text.trim());
1571 let err = iter.next().unwrap().unwrap_err();
1572
1573 assert!(matches!(err, ParseError::IncompleteVelocitySection));
1574 }
1575
1576 #[test]
1577 fn test_non_finite_cell_geometry_rejected_in_strict_mode() {
1578 let lines = vec![
1579 "PREBOX1",
1580 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
1581 "10.0 NaN 30.0",
1582 "90.0 90.0 90.0",
1583 "POSTBOX1",
1584 "POSTBOX2",
1585 "1",
1586 "1",
1587 "12.011",
1588 ];
1589 let mut line_it = lines.iter().copied();
1590 let err = parse_frame_header(&mut line_it).unwrap_err();
1591
1592 assert!(matches!(err, ParseError::ValidationError(_)));
1593 assert!(err.to_string().contains("cell"));
1594 }
1595
1596 #[test]
1597 fn test_validate_true_rejects_non_physical_cell_geometry() {
1598 let lines = vec![
1599 "PREBOX1",
1600 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
1601 "0.0 20.0 30.0",
1602 "90.0 180.0 90.0",
1603 "POSTBOX1",
1604 "POSTBOX2",
1605 "1",
1606 "1",
1607 "12.011",
1608 ];
1609 let mut line_it = lines.iter().copied();
1610 let err = parse_frame_header(&mut line_it).unwrap_err();
1611
1612 assert!(matches!(err, ParseError::ValidationError(_)));
1613 assert!(err.to_string().contains("cell"));
1614 }
1615
1616 #[test]
1617 fn test_validate_true_rejects_reserved_metadata_type_mismatch() {
1618 let lines = vec![
1619 "PREBOX1",
1620 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"energy\":\"low\"}",
1621 "10.0 20.0 30.0",
1622 "90.0 90.0 90.0",
1623 "POSTBOX1",
1624 "POSTBOX2",
1625 "1",
1626 "1",
1627 "12.011",
1628 ];
1629 let mut line_it = lines.iter().copied();
1630 let err = parse_frame_header(&mut line_it).unwrap_err();
1631
1632 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1633 assert!(err.to_string().contains("energy"));
1634 }
1635
1636 #[test]
1637 fn test_parse_velocity_section_absent() {
1638 let lines = vec![
1639 "PREBOX1",
1640 "{\"con_spec_version\":2}",
1641 "10.0 20.0 30.0",
1642 "90.0 90.0 90.0",
1643 "POSTBOX1",
1644 "POSTBOX2",
1645 "1",
1646 "1",
1647 "12.011",
1648 "C",
1649 "Coordinates of Component 1",
1650 "0.0 0.0 0.0 0.0 1",
1651 ];
1652 let mut line_it = lines.iter().copied().peekable();
1653 let mut frame = parse_single_frame(&mut line_it).expect("parse should succeed");
1654 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
1655 .expect("should succeed with no velocities");
1656 assert!(!has_vel);
1657 assert_eq!(frame.atom_data[0].velocity, None);
1658 }
1659
1660 #[test]
1661 fn test_parse_line_of_range_f64_exact() {
1662 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0 42", 4, 5, &[0.0; 5]).unwrap();
1663 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 42.0]);
1664 }
1665
1666 #[test]
1667 fn test_parse_line_of_range_f64_padded() {
1668 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
1669 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0", 4, 5, &defaults).unwrap();
1670 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 99.0]);
1671 }
1672
1673 #[test]
1674 fn test_parse_line_of_range_f64_too_few() {
1675 let result = parse_line_of_range_f64("1.0 2.0 3.0", 4, 5, &[0.0; 5]);
1676 assert!(result.is_err());
1677 }
1678
1679 #[test]
1680 fn test_parse_line_of_range_f64_too_many() {
1681 let result = parse_line_of_range_f64("1.0 2.0 3.0 0.0 5.0 6.0", 4, 5, &[0.0; 5]);
1682 assert!(result.is_err());
1683 }
1684
1685 #[test]
1686 fn test_parse_all_four_column_lines() {
1687 let lines = vec![
1689 "PREBOX1",
1690 "{\"con_spec_version\":2}",
1691 "10.0 10.0 10.0",
1692 "90.0 90.0 90.0",
1693 "POSTBOX1",
1694 "POSTBOX2",
1695 "1",
1696 "3",
1697 "12.011",
1698 "C",
1699 "Coordinates of Component 1",
1700 "0.0 0.0 0.0 0",
1701 "1.0 0.0 0.0 0",
1702 "2.0 0.0 0.0 1",
1703 ];
1704 let mut line_it = lines.iter().copied();
1705 let frame = parse_single_frame(&mut line_it).unwrap();
1706 assert_eq!(frame.atom_data[0].atom_id, 0);
1707 assert_eq!(frame.atom_data[1].atom_id, 1);
1708 assert_eq!(frame.atom_data[2].atom_id, 2);
1709 assert!(frame.atom_data[2].is_fixed());
1710 }
1711}