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