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_SPREADS,
6 SECTION_VELOCITIES, 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 spread: None,
702 });
703 global_atom_idx += 1;
704 atom_i += 1;
705 }
706 }
707 let positions = if f64_positions {
708 FloatArray2::from_f64_row_major(total_atoms, 3, pos_flat)
709 } else {
710 positions_other.expect("non-f64 positions allocated")
711 };
712 Ok(crate::types::con_frame_from_atom_data_with_positions(
714 header, atom_data, positions,
715 ))
716}
717
718fn validate_header_geometry(
719 boxl: &[f64],
720 angles: &[f64],
721 natm_types: usize,
722 natms_per_type: &[usize],
723) -> Result<(), ParseError> {
724 if boxl
725 .iter()
726 .any(|length| !length.is_finite() || *length <= 0.0)
727 || angles
728 .iter()
729 .any(|angle| !angle.is_finite() || *angle <= 0.0 || *angle >= 180.0)
730 {
731 return Err(ParseError::ValidationError(
732 "cell geometry must have positive lengths and angles between 0 and 180 degrees"
733 .to_string(),
734 ));
735 }
736 if natm_types == 0 || natms_per_type.contains(&0) {
737 return Err(ParseError::ValidationError(
738 "atom counts must contain at least one atom per component".to_string(),
739 ));
740 }
741 Ok(())
742}
743
744fn validate_masses(masses_per_type: &[f64]) -> Result<(), ParseError> {
745 if masses_per_type
746 .iter()
747 .any(|mass| !mass.is_finite() || *mass <= 0.0)
748 {
749 return Err(ParseError::ValidationError(
750 "component masses must be positive".to_string(),
751 ));
752 }
753 Ok(())
754}
755
756fn validate_coordinate_component(
757 type_idx: usize,
758 symbol: &str,
759 label: &str,
760) -> Result<(), ParseError> {
761 let expected_label = format!("Coordinates of Component {}", type_idx + 1);
762 if label.trim() != expected_label {
763 return Err(ParseError::ValidationError(format!(
764 "expected coordinate label {expected_label:?}, found {label:?}"
765 )));
766 }
767 if symbol != "X" && symbol_to_atomic_number(symbol) == 0 {
768 return Err(ParseError::ValidationError(format!(
769 "unknown component symbol {symbol}"
770 )));
771 }
772 Ok(())
773}
774
775fn parse_identity_columns(
790 line: &str,
791 row_kind: &str,
792 fixed_idx: usize,
793 atom_id_idx: usize,
794 n_cols: usize,
795 spec_version: u32,
796) -> Result<([bool; 3], u64), ParseError> {
797 let columns = line.split_ascii_whitespace().collect::<Vec<_>>();
798 if columns.len() != n_cols {
799 return Err(ParseError::ValidationError(format!(
800 "{row_kind} rows require {n_cols} columns including fixed_flag and atom_id in validate mode"
801 )));
802 }
803 let fixed_flag = columns[fixed_idx].parse::<u8>().map_err(|_| {
804 ParseError::ValidationError(format!("{row_kind} fixed_flag must be an integer bitmask"))
805 })?;
806 if fixed_flag > 7 {
807 return Err(ParseError::ValidationError(format!(
808 "{row_kind} fixed_flag must be between 0 and 7"
809 )));
810 }
811 let atom_id = columns[atom_id_idx].parse::<u64>().map_err(|_| {
812 ParseError::ValidationError(format!("{row_kind} atom_id must be an integer"))
813 })?;
814 Ok((
815 decode_fixed_bitmask_for_spec(fixed_flag, spec_version),
816 atom_id,
817 ))
818}
819
820fn validate_section_component(
821 section: &str,
822 type_idx: usize,
823 atom_idx: usize,
824 symbol: &str,
825 label: &str,
826 header: &FrameHeader,
827 atom_data: &[AtomDatum],
828) -> Result<(), ParseError> {
829 let expected_label = format!("{section} of Component {}", type_idx + 1);
830 if label.trim() != expected_label {
831 return Err(ParseError::ValidationError(format!(
832 "expected section label {expected_label:?}, found {label:?}"
833 )));
834 }
835
836 if header.natms_per_type[type_idx] == 0 {
837 return Ok(());
838 }
839
840 let expected_symbol = atom_data
841 .get(atom_idx)
842 .map(|atom| atom.symbol.as_ref())
843 .ok_or_else(|| {
844 ParseError::ValidationError(format!(
845 "{section} component {} has no coordinate atom to validate against",
846 type_idx + 1
847 ))
848 })?;
849 if symbol != expected_symbol {
850 return Err(ParseError::ValidationError(format!(
851 "{section} component {} symbol mismatch: expected {expected_symbol}, found {symbol}",
852 type_idx + 1
853 )));
854 }
855
856 Ok(())
857}
858
859fn validate_section_atom_identity(
860 section: &str,
861 atom_idx: usize,
862 fixed: [bool; 3],
863 atom_id: u64,
864 atom_data: &[AtomDatum],
865) -> Result<(), ParseError> {
866 let atom = atom_data.get(atom_idx).ok_or_else(|| {
867 ParseError::ValidationError(format!(
868 "{section} row {atom_idx} has no coordinate atom to validate against"
869 ))
870 })?;
871
872 if atom.fixed != fixed {
873 return Err(ParseError::ValidationError(format!(
874 "{section} row {atom_idx} fixed mask mismatch for atom_id {}",
875 atom.atom_id
876 )));
877 }
878 if atom.atom_id != atom_id {
879 return Err(ParseError::ValidationError(format!(
880 "{section} row {atom_idx} atom_id mismatch: expected {}, found {atom_id}",
881 atom.atom_id
882 )));
883 }
884
885 Ok(())
886}
887
888pub fn parse_velocity_section<'a>(
901 lines: &mut impl LineStream<'a>,
902 header: &FrameHeader,
903 atom_data: &mut [AtomDatum],
904) -> Result<bool, ParseError> {
905 let validate = header.strict_validation;
906 match lines.peek_line() {
908 Some(line) if line.trim().is_empty() => {
909 lines.next_line();
911 }
912 _ => return Ok(false),
913 }
914
915 let mut atom_idx: usize = 0;
916 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
917 let symbol = lines
919 .next_line()
920 .ok_or(ParseError::IncompleteVelocitySection)?
921 .trim();
922
923 let comp_line = lines
925 .next_line()
926 .ok_or(ParseError::IncompleteVelocitySection)?;
927 if !comp_line.contains("Velocities of Component") {
929 return Err(ParseError::IncompleteVelocitySection);
930 }
931 if validate {
932 validate_section_component(
933 "Velocities",
934 type_idx,
935 atom_idx,
936 symbol,
937 comp_line,
938 header,
939 atom_data,
940 )?;
941 }
942
943 for _ in 0..num_atoms {
944 let vel_line = lines
945 .next_line()
946 .ok_or(ParseError::IncompleteVelocitySection)?;
947 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
949 let mut vals = [0.0f64; 5];
950 parse_line_of_range_f64_stack(vel_line, 4, 5, &defaults, &mut vals)?;
951 if validate {
952 let (fixed, atom_id) =
953 parse_identity_columns(vel_line, "velocities", 3, 4, 5, header.spec_version)?;
954 validate_section_atom_identity("velocities", atom_idx, fixed, atom_id, atom_data)?;
955 }
956 if atom_idx < atom_data.len() {
957 atom_data[atom_idx].velocity = Some([vals[0], vals[1], vals[2]]);
958 }
959 atom_idx += 1;
960 }
961 }
962
963 Ok(true)
964}
965
966pub fn parse_force_section<'a>(
974 lines: &mut impl LineStream<'a>,
975 header: &FrameHeader,
976 atom_data: &mut [AtomDatum],
977) -> Result<bool, ParseError> {
978 let validate = header.strict_validation;
979 match lines.peek_line() {
981 Some(line) if line.trim().is_empty() => {
982 lines.next_line();
983 }
984 _ => return Ok(false),
985 }
986
987 let mut atom_idx: usize = 0;
988 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
989 let symbol = lines
990 .next_line()
991 .ok_or(ParseError::IncompleteForceSection)?
992 .trim();
993
994 let comp_line = lines
995 .next_line()
996 .ok_or(ParseError::IncompleteForceSection)?;
997 if !comp_line.contains("Forces of Component") {
998 return Err(ParseError::IncompleteForceSection);
999 }
1000 if validate {
1001 validate_section_component(
1002 "Forces", type_idx, atom_idx, symbol, comp_line, header, atom_data,
1003 )?;
1004 }
1005
1006 for _ in 0..num_atoms {
1007 let force_line = lines
1008 .next_line()
1009 .ok_or(ParseError::IncompleteForceSection)?;
1010 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
1011 let mut vals = [0.0f64; 5];
1012 parse_line_of_range_f64_stack(force_line, 4, 5, &defaults, &mut vals)?;
1013 if validate {
1014 let (fixed, atom_id) =
1015 parse_identity_columns(force_line, "forces", 3, 4, 5, header.spec_version)?;
1016 validate_section_atom_identity("forces", atom_idx, fixed, atom_id, atom_data)?;
1017 }
1018 if atom_idx < atom_data.len() {
1019 atom_data[atom_idx].force = Some([vals[0], vals[1], vals[2]]);
1020 }
1021 atom_idx += 1;
1022 }
1023 }
1024
1025 Ok(true)
1026}
1027
1028pub fn parse_energy_section<'a>(
1041 lines: &mut impl LineStream<'a>,
1042 header: &FrameHeader,
1043 atom_data: &mut [AtomDatum],
1044) -> Result<bool, ParseError> {
1045 let validate = header.strict_validation;
1046 match lines.peek_line() {
1047 Some(line) if line.trim().is_empty() => {
1048 lines.next_line();
1049 }
1050 _ => return Ok(false),
1051 }
1052
1053 let mut atom_idx: usize = 0;
1054 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1055 let symbol = lines
1056 .next_line()
1057 .ok_or(ParseError::IncompleteEnergySection)?
1058 .trim();
1059
1060 let comp_line = lines
1061 .next_line()
1062 .ok_or(ParseError::IncompleteEnergySection)?;
1063 if !comp_line.contains("Energies of Component") {
1064 return Err(ParseError::IncompleteEnergySection);
1065 }
1066 if validate {
1067 validate_section_component(
1068 "Energies", type_idx, atom_idx, symbol, comp_line, header, atom_data,
1069 )?;
1070 }
1071
1072 for _ in 0..num_atoms {
1073 let energy_line = lines
1074 .next_line()
1075 .ok_or(ParseError::IncompleteEnergySection)?;
1076 let defaults = [0.0, 0.0, atom_idx as f64];
1079 let vals = parse_line_of_range_f64(energy_line, 1, 3, &defaults)?;
1080 if validate {
1081 let (fixed, atom_id) =
1082 parse_identity_columns(energy_line, "energies", 1, 2, 3, header.spec_version)?;
1083 validate_section_atom_identity("energies", atom_idx, fixed, atom_id, atom_data)?;
1084 }
1085 if atom_idx < atom_data.len() {
1086 atom_data[atom_idx].energy = Some(vals[0]);
1087 }
1088 atom_idx += 1;
1089 }
1090 }
1091
1092 Ok(true)
1093}
1094
1095pub fn parse_declared_sections<'a>(
1101 lines: &mut impl LineStream<'a>,
1102 header: &mut FrameHeader,
1103 atom_data: &mut [AtomDatum],
1104) -> Result<usize, ParseError> {
1105 let mut applied = 0usize;
1106 if !header.sections_declared && header.sections.is_empty() {
1107 let found = parse_velocity_section(lines, header, atom_data)?;
1109 if found {
1110 header.sections.push(SECTION_VELOCITIES.into());
1111 applied = 1;
1112 }
1113 } else {
1114 let sections = std::mem::take(&mut header.sections);
1115 for section in §ions {
1116 match section.as_str() {
1117 SECTION_VELOCITIES => {
1118 let found = parse_velocity_section(lines, header, atom_data)?;
1119 if !found {
1120 return Err(ParseError::IncompleteVelocitySection);
1121 }
1122 applied += 1;
1123 }
1124 SECTION_FORCES => {
1125 let found = parse_force_section(lines, header, atom_data)?;
1126 if !found {
1127 return Err(ParseError::IncompleteForceSection);
1128 }
1129 applied += 1;
1130 }
1131 SECTION_ENERGIES => {
1132 let found = parse_energy_section(lines, header, atom_data)?;
1133 if !found {
1134 return Err(ParseError::IncompleteEnergySection);
1135 }
1136 applied += 1;
1137 }
1138 SECTION_CHARGES => {
1139 let found = parse_charge_section(lines, header, atom_data)?;
1140 if !found {
1141 return Err(ParseError::IncompleteSection(SECTION_CHARGES.into()));
1142 }
1143 applied += 1;
1144 }
1145 SECTION_SPINS => {
1146 let found = parse_spin_section(lines, header, atom_data)?;
1147 if !found {
1148 return Err(ParseError::IncompleteSection(SECTION_SPINS.into()));
1149 }
1150 applied += 1;
1151 }
1152 SECTION_MAGMOMS => {
1153 let found = parse_magmom_section(lines, header, atom_data)?;
1154 if !found {
1155 return Err(ParseError::IncompleteSection(SECTION_MAGMOMS.into()));
1156 }
1157 applied += 1;
1158 }
1159 SECTION_DISPLACEMENTS => {
1160 let found = parse_displacement_section(lines, header, atom_data)?;
1161 if !found {
1162 return Err(ParseError::IncompleteSection(SECTION_DISPLACEMENTS.into()));
1163 }
1164 applied += 1;
1165 }
1166 SECTION_SPREADS => {
1167 let found = parse_spread_section(lines, header, atom_data)?;
1168 if !found {
1169 return Err(ParseError::IncompleteSection(SECTION_SPREADS.into()));
1170 }
1171 applied += 1;
1172 }
1173 other => return Err(ParseError::UnknownSection(other.to_string())),
1174 }
1175 }
1176 header.sections = sections;
1177 }
1178 Ok(applied)
1179}
1180
1181fn parse_scalar_atom_section<'a>(
1184 lines: &mut impl LineStream<'a>,
1185 header: &FrameHeader,
1186 atom_data: &mut [AtomDatum],
1187 section_name: &str,
1188 component_label: &str,
1189 mut set_value: impl FnMut(&mut AtomDatum, f64),
1190) -> Result<bool, ParseError> {
1191 let validate = header.strict_validation;
1192 match lines.peek_line() {
1193 Some(line) if line.trim().is_empty() => {
1194 lines.next_line();
1195 }
1196 _ => return Ok(false),
1197 }
1198
1199 let mut atom_idx: usize = 0;
1200 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1201 let symbol = lines
1202 .next_line()
1203 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?
1204 .trim();
1205
1206 let comp_line = lines
1207 .next_line()
1208 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
1209 if !comp_line.contains(component_label) {
1210 return Err(ParseError::IncompleteSection(section_name.into()));
1211 }
1212 if validate {
1213 validate_section_component(
1214 component_label.trim_end_matches(" of Component").trim(),
1215 type_idx,
1216 atom_idx,
1217 symbol,
1218 comp_line,
1219 header,
1220 atom_data,
1221 )?;
1222 }
1223
1224 for _ in 0..num_atoms {
1225 let data_line = lines
1226 .next_line()
1227 .ok_or_else(|| ParseError::IncompleteSection(section_name.into()))?;
1228 let defaults = [0.0, 0.0, atom_idx as f64];
1229 let vals = parse_line_of_range_f64(data_line, 1, 3, &defaults)?;
1230 if validate {
1231 let (fixed, atom_id) =
1232 parse_identity_columns(data_line, section_name, 1, 2, 3, header.spec_version)?;
1233 validate_section_atom_identity(section_name, atom_idx, fixed, atom_id, atom_data)?;
1234 }
1235 if atom_idx < atom_data.len() {
1236 set_value(&mut atom_data[atom_idx], vals[0]);
1237 }
1238 atom_idx += 1;
1239 }
1240 }
1241 Ok(true)
1242}
1243
1244pub fn parse_charge_section<'a>(
1245 lines: &mut impl LineStream<'a>,
1246 header: &FrameHeader,
1247 atom_data: &mut [AtomDatum],
1248) -> Result<bool, ParseError> {
1249 parse_scalar_atom_section(
1250 lines,
1251 header,
1252 atom_data,
1253 SECTION_CHARGES,
1254 "Charges of Component",
1255 |a, v| a.charge = Some(v),
1256 )
1257}
1258
1259pub fn parse_spin_section<'a>(
1260 lines: &mut impl LineStream<'a>,
1261 header: &FrameHeader,
1262 atom_data: &mut [AtomDatum],
1263) -> Result<bool, ParseError> {
1264 parse_scalar_atom_section(
1265 lines,
1266 header,
1267 atom_data,
1268 SECTION_SPINS,
1269 "Spins of Component",
1270 |a, v| a.spin = Some(v),
1271 )
1272}
1273
1274pub fn parse_magmom_section<'a>(
1276 lines: &mut impl LineStream<'a>,
1277 header: &FrameHeader,
1278 atom_data: &mut [AtomDatum],
1279) -> Result<bool, ParseError> {
1280 let validate = header.strict_validation;
1281 match lines.peek_line() {
1282 Some(line) if line.trim().is_empty() => {
1283 lines.next_line();
1284 }
1285 _ => return Ok(false),
1286 }
1287
1288 let mut atom_idx: usize = 0;
1289 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1290 let symbol = lines
1291 .next_line()
1292 .ok_or_else(|| ParseError::IncompleteSection(SECTION_MAGMOMS.into()))?
1293 .trim();
1294
1295 let comp_line = lines
1296 .next_line()
1297 .ok_or_else(|| ParseError::IncompleteSection(SECTION_MAGMOMS.into()))?;
1298 if !comp_line.contains("Magmoms of Component") {
1299 return Err(ParseError::IncompleteSection(SECTION_MAGMOMS.into()));
1300 }
1301 if validate {
1302 validate_section_component(
1303 "Magmoms", type_idx, atom_idx, symbol, comp_line, header, atom_data,
1304 )?;
1305 }
1306
1307 for _ in 0..num_atoms {
1308 let mm_line = lines
1309 .next_line()
1310 .ok_or_else(|| ParseError::IncompleteSection(SECTION_MAGMOMS.into()))?;
1311 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
1312 let mut vals = [0.0f64; 5];
1313 parse_line_of_range_f64_stack(mm_line, 4, 5, &defaults, &mut vals)?;
1314 if validate {
1315 let (fixed, atom_id) =
1316 parse_identity_columns(mm_line, SECTION_MAGMOMS, 3, 4, 5, header.spec_version)?;
1317 validate_section_atom_identity(
1318 SECTION_MAGMOMS,
1319 atom_idx,
1320 fixed,
1321 atom_id,
1322 atom_data,
1323 )?;
1324 }
1325 if atom_idx < atom_data.len() {
1326 atom_data[atom_idx].magmom = Some([vals[0], vals[1], vals[2]]);
1327 }
1328 atom_idx += 1;
1329 }
1330 }
1331 Ok(true)
1332}
1333
1334pub fn parse_displacement_section<'a>(
1336 lines: &mut impl LineStream<'a>,
1337 header: &FrameHeader,
1338 atom_data: &mut [AtomDatum],
1339) -> Result<bool, ParseError> {
1340 let validate = header.strict_validation;
1341 match lines.peek_line() {
1342 Some(line) if line.trim().is_empty() => {
1343 lines.next_line();
1344 }
1345 _ => return Ok(false),
1346 }
1347
1348 let mut atom_idx: usize = 0;
1349 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1350 let symbol = lines
1351 .next_line()
1352 .ok_or_else(|| ParseError::IncompleteSection(SECTION_DISPLACEMENTS.into()))?
1353 .trim();
1354
1355 let comp_line = lines
1356 .next_line()
1357 .ok_or_else(|| ParseError::IncompleteSection(SECTION_DISPLACEMENTS.into()))?;
1358 if !comp_line.contains("Displacements of Component") {
1359 return Err(ParseError::IncompleteSection(SECTION_DISPLACEMENTS.into()));
1360 }
1361 if validate {
1362 validate_section_component(
1363 "Displacements",
1364 type_idx,
1365 atom_idx,
1366 symbol,
1367 comp_line,
1368 header,
1369 atom_data,
1370 )?;
1371 }
1372
1373 for _ in 0..num_atoms {
1374 let dsp_line = lines
1375 .next_line()
1376 .ok_or_else(|| ParseError::IncompleteSection(SECTION_DISPLACEMENTS.into()))?;
1377 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
1378 let mut vals = [0.0f64; 5];
1379 parse_line_of_range_f64_stack(dsp_line, 4, 5, &defaults, &mut vals)?;
1380 if validate {
1381 let (fixed, atom_id) = parse_identity_columns(
1382 dsp_line,
1383 SECTION_DISPLACEMENTS,
1384 3,
1385 4,
1386 5,
1387 header.spec_version,
1388 )?;
1389 validate_section_atom_identity(
1390 SECTION_DISPLACEMENTS,
1391 atom_idx,
1392 fixed,
1393 atom_id,
1394 atom_data,
1395 )?;
1396 }
1397 if atom_idx < atom_data.len() {
1398 atom_data[atom_idx].displacement = Some([vals[0], vals[1], vals[2]]);
1399 }
1400 atom_idx += 1;
1401 }
1402 }
1403 Ok(true)
1404}
1405
1406pub fn parse_spread_section<'a>(
1409 lines: &mut impl LineStream<'a>,
1410 header: &FrameHeader,
1411 atom_data: &mut [AtomDatum],
1412) -> Result<bool, ParseError> {
1413 let validate = header.strict_validation;
1414 match lines.peek_line() {
1415 Some(line) if line.trim().is_empty() => {
1416 lines.next_line();
1417 }
1418 _ => return Ok(false),
1419 }
1420
1421 let mut atom_idx: usize = 0;
1422 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
1423 let symbol = lines
1424 .next_line()
1425 .ok_or_else(|| ParseError::IncompleteSection(SECTION_SPREADS.into()))?
1426 .trim();
1427
1428 let comp_line = lines
1429 .next_line()
1430 .ok_or_else(|| ParseError::IncompleteSection(SECTION_SPREADS.into()))?;
1431 if !comp_line.contains("Spreads of Component") {
1432 return Err(ParseError::IncompleteSection(SECTION_SPREADS.into()));
1433 }
1434 if validate {
1435 validate_section_component(
1436 "Spreads", type_idx, atom_idx, symbol, comp_line, header, atom_data,
1437 )?;
1438 }
1439
1440 for _ in 0..num_atoms {
1441 let spr_line = lines
1442 .next_line()
1443 .ok_or_else(|| ParseError::IncompleteSection(SECTION_SPREADS.into()))?;
1444 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
1445 let mut vals = [0.0f64; 5];
1446 parse_line_of_range_f64_stack(spr_line, 4, 5, &defaults, &mut vals)?;
1447 if vals[..3].iter().any(|v| !v.is_finite() || *v < 0.0) {
1448 return Err(ParseError::ValidationError(format!(
1449 "spreads: atom {atom_idx} has a negative or non-finite spread"
1450 )));
1451 }
1452 if validate {
1453 let (fixed, atom_id) = parse_identity_columns(
1454 spr_line,
1455 SECTION_SPREADS,
1456 3,
1457 4,
1458 5,
1459 header.spec_version,
1460 )?;
1461 validate_section_atom_identity(
1462 SECTION_SPREADS,
1463 atom_idx,
1464 fixed,
1465 atom_id,
1466 atom_data,
1467 )?;
1468 }
1469 if atom_idx < atom_data.len() {
1470 atom_data[atom_idx].spread = Some([vals[0], vals[1], vals[2]]);
1471 }
1472 atom_idx += 1;
1473 }
1474 }
1475 Ok(true)
1476}
1477
1478#[cfg(test)]
1479mod tests {
1480 use super::*;
1481 use crate::iterators::ConFrameIterator;
1482
1483 #[test]
1484 fn test_parse_line_of_n_success() {
1485 let line = "1.0 2.5 -3.0";
1486 let values = parse_line_of_n::<f64>(line, 3).unwrap();
1487 assert_eq!(values, vec![1.0, 2.5, -3.0]);
1488 }
1489
1490 #[test]
1491 fn test_parse_line_of_n_too_short() {
1492 let line = "1.0 2.5";
1493 let result = parse_line_of_n::<f64>(line, 3);
1494 assert!(result.is_err());
1495 assert!(matches!(
1496 result.unwrap_err(),
1497 ParseError::InvalidVectorLength {
1498 expected: 3,
1499 found: 2
1500 }
1501 ));
1502 }
1503
1504 #[test]
1505 fn test_parse_line_of_n_too_long() {
1506 let line = "1.0 2.5 -3.0 4.0";
1507 let result = parse_line_of_n::<f64>(line, 3);
1508 assert!(result.is_err());
1509 assert!(matches!(
1510 result.unwrap_err(),
1511 ParseError::InvalidVectorLength {
1512 expected: 3,
1513 found: 4
1514 }
1515 ));
1516 }
1517
1518 #[test]
1519 fn test_parse_line_of_n_invalid_float() {
1520 let line = "1.0 abc -3.0";
1521 let result = parse_line_of_n::<f64>(line, 3);
1522 assert!(result.is_err());
1523 assert!(matches!(
1524 result.unwrap_err(),
1525 ParseError::InvalidNumberFormat(_)
1526 ));
1527 }
1528
1529 #[test]
1530 fn test_parse_frame_header_success() {
1531 let lines = [
1532 "PREBOX1",
1533 "{\"con_spec_version\":2}",
1534 "10.0 20.0 30.0",
1535 "90.0 90.0 90.0",
1536 "POSTBOX1",
1537 "POSTBOX2",
1538 "2",
1539 "1 1",
1540 "12.011 1.008",
1541 ];
1542 let mut line_it = lines.iter().copied();
1543 match parse_frame_header(&mut line_it) {
1544 Ok(header) => {
1545 assert_eq!(header.prebox_header.user, "PREBOX1");
1546 assert_eq!(header.spec_version, 2);
1547 assert_eq!(header.boxl, [10.0, 20.0, 30.0]);
1548 assert_eq!(header.angles, [90.0, 90.0, 90.0]);
1549 assert_eq!(header.postbox_header, ["POSTBOX1", "POSTBOX2"]);
1550 assert_eq!(header.natm_types, 2);
1551 assert_eq!(header.natms_per_type, vec![1, 1]);
1552 assert_eq!(header.masses_per_type, vec![12.011, 1.008]);
1553 }
1554 Err(e) => {
1555 panic!(
1556 "Parsing failed when it should have succeeded. Error: {:?}",
1557 e
1558 );
1559 }
1560 }
1561 }
1562
1563 #[test]
1564 fn test_parse_frame_header_missing_line() {
1565 let lines = [
1566 "PREBOX1",
1567 "{\"con_spec_version\":2}",
1568 "10.0 20.0 30.0",
1569 "90.0 90.0 90.0",
1570 "POSTBOX1",
1571 "POSTBOX2",
1572 "2",
1573 "1 1",
1574 ];
1576 let mut line_it = lines.iter().copied();
1577 let result = parse_frame_header(&mut line_it);
1578 assert!(result.is_err());
1579 assert!(matches!(result.unwrap_err(), ParseError::IncompleteHeader));
1580 }
1581
1582 #[test]
1583 fn test_parse_frame_header_missing_spec_version() {
1584 let lines = vec![
1585 "PREBOX1",
1586 "{}",
1587 "10.0 20.0 30.0",
1588 "90.0 90.0 90.0",
1589 "POSTBOX1",
1590 "POSTBOX2",
1591 "2",
1592 "1 1",
1593 "12.011 1.008",
1594 ];
1595 let mut line_it = lines.iter().copied();
1596 let result = parse_frame_header(&mut line_it);
1597 assert!(result.is_err());
1598 assert!(matches!(
1599 result.unwrap_err(),
1600 ParseError::MissingSpecVersion
1601 ));
1602 }
1603
1604 #[test]
1605 fn test_parse_frame_header_legacy_no_json() {
1606 let lines = vec![
1608 "PREBOX1",
1609 "0.0000 TIME",
1610 "10.0 20.0 30.0",
1611 "90.0 90.0 90.0",
1612 "POSTBOX1",
1613 "POSTBOX2",
1614 "2",
1615 "1 1",
1616 "12.011 1.008",
1617 ];
1618 let mut line_it = lines.iter().copied();
1619 let header = parse_frame_header(&mut line_it).unwrap();
1620 assert_eq!(header.spec_version, 1);
1621 assert!(header.metadata.is_empty());
1622 }
1623
1624 #[test]
1625 fn test_parse_frame_header_malformed_json() {
1626 let lines = vec![
1628 "PREBOX1",
1629 "{broken json",
1630 "10.0 20.0 30.0",
1631 "90.0 90.0 90.0",
1632 "POSTBOX1",
1633 "POSTBOX2",
1634 "2",
1635 "1 1",
1636 "12.011 1.008",
1637 ];
1638 let mut line_it = lines.iter().copied();
1639 let result = parse_frame_header(&mut line_it);
1640 assert!(result.is_err());
1641 assert!(matches!(
1642 result.unwrap_err(),
1643 ParseError::InvalidMetadataJson(_)
1644 ));
1645 }
1646
1647 #[test]
1648 fn test_parse_frame_header_unsupported_version() {
1649 let lines = vec![
1650 "PREBOX1",
1651 "{\"con_spec_version\":999}",
1652 "10.0 20.0 30.0",
1653 "90.0 90.0 90.0",
1654 "POSTBOX1",
1655 "POSTBOX2",
1656 "2",
1657 "1 1",
1658 "12.011 1.008",
1659 ];
1660 let mut line_it = lines.iter().copied();
1661 let result = parse_frame_header(&mut line_it);
1662 assert!(result.is_err());
1663 assert!(matches!(
1664 result.unwrap_err(),
1665 ParseError::UnsupportedSpecVersion(999)
1666 ));
1667 }
1668
1669 #[test]
1670 fn test_v3_missing_units_rejected() {
1671 let lines = vec![
1672 "PREBOX1",
1673 "{\"con_spec_version\":3}",
1674 "10.0 20.0 30.0",
1675 "90.0 90.0 90.0",
1676 "POSTBOX1",
1677 "POSTBOX2",
1678 "1",
1679 "1",
1680 "1.0",
1681 ];
1682 let mut line_it = lines.iter().copied();
1683 let err = parse_frame_header(&mut line_it).unwrap_err();
1684 let msg = err.to_string();
1685 assert!(
1686 msg.contains("units") || msg.contains("v3"),
1687 "expected units error, got {msg}"
1688 );
1689 }
1690
1691 #[test]
1692 fn test_v3_invalid_units_rejected() {
1693 let lines = vec![
1694 "PREBOX1",
1695 r#"{"con_spec_version":3,"units":{"length":"eV","energy":"angstrom"}}"#,
1696 "10.0 20.0 30.0",
1697 "90.0 90.0 90.0",
1698 "POSTBOX1",
1699 "POSTBOX2",
1700 "1",
1701 "1",
1702 "1.0",
1703 ];
1704 let mut line_it = lines.iter().copied();
1705 assert!(parse_frame_header(&mut line_it).is_err());
1706 }
1707
1708 #[test]
1709 fn test_v3_valid_units_exposes_length_energy() {
1710 let lines = vec![
1711 "PREBOX1",
1712 r#"{"con_spec_version":3,"units":{"length":"angstrom","energy":"eV","mass":"amu","time":"fs"}}"#,
1713 "10.0 20.0 30.0",
1714 "90.0 90.0 90.0",
1715 "POSTBOX1",
1716 "POSTBOX2",
1717 "1",
1718 "1",
1719 "1.0",
1720 ];
1721 let mut line_it = lines.iter().copied();
1722 let header = parse_frame_header(&mut line_it).unwrap();
1723 assert_eq!(header.spec_version, 3);
1724 assert_eq!(header.length_unit(), Some("angstrom"));
1725 assert_eq!(header.energy_unit(), Some("eV"));
1726 let f = header.conversion_factor_to("length", "nm").unwrap();
1727 assert!((f - 0.1).abs() < 1e-12);
1728 }
1729
1730 #[test]
1731 fn test_parse_frame_header_extra_metadata_preserved() {
1732 let lines = vec![
1733 "PREBOX1",
1734 "{\"con_spec_version\":2,\"generator\":\"test\"}",
1735 "10.0 20.0 30.0",
1736 "90.0 90.0 90.0",
1737 "POSTBOX1",
1738 "POSTBOX2",
1739 "2",
1740 "1 1",
1741 "12.011 1.008",
1742 ];
1743 let mut line_it = lines.iter().copied();
1744 let header = parse_frame_header(&mut line_it).unwrap();
1745 assert_eq!(header.spec_version, 2);
1746 assert_eq!(
1747 header.metadata.get("generator"),
1748 Some(&serde_json::Value::String("test".to_string()))
1749 );
1750 }
1751
1752 #[test]
1753 fn test_parse_frame_header_invalid_natms_per_type() {
1754 let lines = vec![
1755 "PREBOX1",
1756 "{\"con_spec_version\":2}",
1757 "10.0 20.0 30.0",
1758 "90.0 90.0 90.0",
1759 "POSTBOX1",
1760 "POSTBOX2",
1761 "2",
1762 "1 1 1", "12.011 1.008",
1764 ];
1765 let mut line_it = lines.iter().copied();
1766 let result = parse_frame_header(&mut line_it);
1767 assert!(result.is_err());
1768 assert!(matches!(
1769 result.unwrap_err(),
1770 ParseError::InvalidVectorLength {
1771 expected: 2,
1772 found: 3
1773 }
1774 ));
1775 }
1776
1777 #[test]
1778 fn test_parse_single_frame_success() {
1779 let lines = vec![
1780 "PREBOX1",
1781 "{\"con_spec_version\":2}",
1782 "10.0 20.0 30.0",
1783 "90.0 90.0 90.0",
1784 "POSTBOX1",
1785 "POSTBOX2",
1786 "2",
1787 "3 3",
1788 "12.011 1.008",
1789 "1",
1790 "Coordinates of Component 1",
1791 "0.0 0.0 0.0 0.0 1",
1792 "1.0940 0.0 0.0 0.0 2",
1793 "-0.5470 0.9499 0.0 0.0 3",
1794 "2",
1795 "Coordinates of Component 2",
1796 "5.0 5.0 5.0 0.0 4",
1797 "6.0940 5.0 5.0 0.0 5",
1798 "5.5470 5.9499 5.0 0.0 6",
1799 ];
1800 let mut line_it = lines.iter().copied();
1801 let frame = parse_single_frame(&mut line_it).unwrap();
1802
1803 assert_eq!(frame.header.natm_types, 2);
1804 assert_eq!(frame.header.natms_per_type, vec![3, 3]);
1805 assert_eq!(frame.header.masses_per_type, vec![12.011, 1.008]);
1806 assert_eq!(frame.atom_data.len(), 6);
1807 assert_eq!(&*frame.atom_data[0].symbol, "1");
1808 assert_eq!(frame.atom_data[0].atom_id, 1);
1809 assert_eq!(&*frame.atom_data[5].symbol, "2");
1810 assert_eq!(frame.atom_data[5].atom_id, 6);
1811 }
1812
1813 #[test]
1814 fn test_column_4_value_1_is_x_only_on_spec_2() {
1815 let lines = vec![
1816 "Generated by probe",
1817 "{\"con_spec_version\":2}",
1818 "20.0 20.0 20.0",
1819 "90.0 90.0 90.0",
1820 "",
1821 "",
1822 "1",
1823 "1",
1824 "63.5",
1825 "Cu",
1826 "Coordinates of component 1",
1827 "1.0 2.0 3.0 1 1",
1828 ];
1829 let mut line_it = lines.iter().copied();
1830 let frame = parse_single_frame(&mut line_it).unwrap();
1831 assert_eq!(frame.header.spec_version, 2);
1832 assert_eq!(frame.atom_data[0].fixed, [true, false, false]);
1833 }
1834
1835 #[test]
1836 fn test_column_4_value_1_is_all_fixed_on_spec_1() {
1837 let lines = vec![
1838 "Generated by probe",
1839 "0.0000 TIME",
1840 "20.0 20.0 20.0",
1841 "90.0 90.0 90.0",
1842 "",
1843 "",
1844 "1",
1845 "1",
1846 "63.5",
1847 "Cu",
1848 "Coordinates of component 1",
1849 "1.0 2.0 3.0 1 1",
1850 ];
1851 let mut line_it = lines.iter().copied();
1852 let frame = parse_single_frame(&mut line_it).unwrap();
1853 assert_eq!(frame.header.spec_version, 1);
1854 assert_eq!(frame.atom_data[0].fixed, [true, true, true]);
1855 }
1856
1857 #[test]
1858 fn test_parse_single_frame_missing_line() {
1859 let lines = vec![
1861 "PREBOX1",
1862 "{\"con_spec_version\":2}",
1863 "10.0 20.0 30.0",
1864 "90.0 90.0 90.0",
1865 "POSTBOX1",
1866 "POSTBOX2",
1867 "2",
1868 "3 3",
1869 "12.011 1.008",
1870 "1",
1871 "Coordinates of Component 1",
1872 "0.0 0.0 0.0 0.0 1",
1873 "1.0940 0.0 0.0 0.0 2",
1874 "-0.5470 0.9499 0.0 0.0 3",
1875 ];
1877 let mut line_it = lines.iter().copied();
1878 let result = parse_single_frame(&mut line_it);
1879 assert!(result.is_err());
1880 assert!(matches!(result.unwrap_err(), ParseError::IncompleteFrame));
1881 }
1882
1883 #[test]
1884 fn test_parse_single_frame_missing_atom_index_defaults_sequential() {
1885 let lines = vec![
1887 "PREBOX1",
1888 "{\"con_spec_version\":2}",
1889 "10.0 20.0 30.0",
1890 "90.0 90.0 90.0",
1891 "POSTBOX1",
1892 "POSTBOX2",
1893 "2",
1894 "3 3",
1895 "12.011 1.008",
1896 "1",
1897 "Coordinates of Component 1",
1898 "0.0 0.0 0.0 0.0 1",
1899 "1.0940 0.0 0.0 0.0 2",
1900 "-0.5470 0.9499 0.0 0.0 3",
1901 "2",
1902 "Coordinates of Component 2",
1903 "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", ];
1907 let mut line_it = lines.iter().copied();
1908 let frame = parse_single_frame(&mut line_it).unwrap();
1909 assert_eq!(frame.atom_data.len(), 6);
1910 assert_eq!(frame.atom_data[0].atom_id, 1);
1912 assert_eq!(frame.atom_data[1].atom_id, 2);
1913 assert_eq!(frame.atom_data[2].atom_id, 3);
1914 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); }
1919
1920 #[test]
1921 fn test_parse_single_frame_too_few_columns_fails() {
1922 let lines = vec![
1924 "PREBOX1",
1925 "{\"con_spec_version\":2}",
1926 "10.0 20.0 30.0",
1927 "90.0 90.0 90.0",
1928 "POSTBOX1",
1929 "POSTBOX2",
1930 "1",
1931 "1",
1932 "12.011",
1933 "C",
1934 "Coordinates of Component 1",
1935 "0.0 0.0 0.0", ];
1937 let mut line_it = lines.iter().copied();
1938 let result = parse_single_frame(&mut line_it);
1939 assert!(result.is_err());
1940 assert!(matches!(
1941 result.unwrap_err(),
1942 ParseError::InvalidVectorLength {
1943 expected: 5,
1944 found: 3
1945 }
1946 ));
1947 }
1948
1949 #[test]
1950 fn test_parse_velocity_section_present() {
1951 let lines = vec![
1952 "PREBOX1",
1953 "{\"con_spec_version\":2}",
1954 "10.0 20.0 30.0",
1955 "90.0 90.0 90.0",
1956 "POSTBOX1",
1957 "POSTBOX2",
1958 "2",
1959 "1 1",
1960 "63.546 1.008",
1961 "Cu",
1962 "Coordinates of Component 1",
1963 "0.0 0.0 0.0 1.0 0",
1964 "H",
1965 "Coordinates of Component 2",
1966 "1.0 2.0 3.0 0.0 1",
1967 "",
1968 "Cu",
1969 "Velocities of Component 1",
1970 "0.1 0.2 0.3 1.0 0",
1971 "H",
1972 "Velocities of Component 2",
1973 "0.4 0.5 0.6 0.0 1",
1974 ];
1975 let mut line_it = lines.iter().copied().peekable();
1976 let mut frame =
1978 parse_single_frame(&mut line_it).expect("coordinate parsing should succeed");
1979 assert!(!frame.has_velocities());
1980
1981 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
1983 .expect("velocity parsing should succeed");
1984 assert!(has_vel);
1985 assert_eq!(frame.atom_data[0].velocity, Some([0.1, 0.2, 0.3]));
1986 assert_eq!(frame.atom_data[1].velocity, Some([0.4, 0.5, 0.6]));
1987 }
1988
1989 #[test]
1990 fn test_validate_true_accepts_matching_section_identity() {
1991 let text = r#"
1992PREBOX1
1993{"con_spec_version":2,"sections":["velocities"],"validate":true}
199410.0 20.0 30.0
199590.0 90.0 90.0
1996POSTBOX1
1997POSTBOX2
19982
19991 1
200063.546 1.008
2001Cu
2002Coordinates of Component 1
20030.0 0.0 0.0 5 0
2004H
2005Coordinates of Component 2
20061.0 2.0 3.0 0 1
2007
2008Cu
2009Velocities of Component 1
20100.1 0.2 0.3 5 0
2011H
2012Velocities of Component 2
20130.4 0.5 0.6 0 1
2014"#;
2015 let mut iter = ConFrameIterator::new(text.trim());
2016 let frame = iter.next().unwrap().unwrap();
2017
2018 assert!(frame.has_velocities());
2019 assert_eq!(
2020 frame
2021 .header
2022 .metadata
2023 .get("validate")
2024 .and_then(|v| v.as_bool()),
2025 Some(true)
2026 );
2027 }
2028
2029 #[test]
2030 fn test_validate_true_rejects_section_atom_id_mismatch() {
2031 let text = r#"
2032PREBOX1
2033{"con_spec_version":2,"sections":["velocities"],"validate":true}
203410.0 20.0 30.0
203590.0 90.0 90.0
2036POSTBOX1
2037POSTBOX2
20381
20391
204063.546
2041Cu
2042Coordinates of Component 1
20430.0 0.0 0.0 5 0
2044
2045Cu
2046Velocities of Component 1
20470.1 0.2 0.3 5 99
2048"#;
2049 let mut iter = ConFrameIterator::new(text.trim());
2050 let err = iter.next().unwrap().unwrap_err();
2051
2052 assert!(matches!(err, ParseError::ValidationError(_)));
2053 assert!(err.to_string().contains("atom_id mismatch"));
2054 }
2055
2056 #[test]
2057 fn test_validate_true_rejects_section_symbol_mismatch() {
2058 let text = r#"
2059PREBOX1
2060{"con_spec_version":2,"sections":["forces"],"validate":true}
206110.0 20.0 30.0
206290.0 90.0 90.0
2063POSTBOX1
2064POSTBOX2
20651
20661
206763.546
2068Cu
2069Coordinates of Component 1
20700.0 0.0 0.0 5 0
2071
2072H
2073Forces of Component 1
20740.1 0.2 0.3 5 0
2075"#;
2076 let mut iter = ConFrameIterator::new(text.trim());
2077 let err = iter.next().unwrap().unwrap_err();
2078
2079 assert!(matches!(err, ParseError::ValidationError(_)));
2080 assert!(err.to_string().contains("symbol mismatch"));
2081 }
2082
2083 #[test]
2084 fn test_validate_absent_allows_legacy_duplicate_identity_mismatch() {
2085 let text = r#"
2086PREBOX1
2087{"con_spec_version":2,"sections":["velocities"]}
208810.0 20.0 30.0
208990.0 90.0 90.0
2090POSTBOX1
2091POSTBOX2
20921
20931
209463.546
2095Cu
2096Coordinates of Component 1
20970.0 0.0 0.0 5 0
2098
2099Cu
2100Velocities of Component 1
21010.1 0.2 0.3 0 99
2102"#;
2103 let mut iter = ConFrameIterator::new(text.trim());
2104 let frame = iter.next().unwrap().unwrap();
2105
2106 assert!(frame.has_velocities());
2107 assert_eq!(frame.atom_data[0].atom_id, 0);
2108 assert_eq!(frame.atom_data[0].fixed, [true, false, true]);
2109 }
2110
2111 #[test]
2112 fn test_validate_must_be_boolean_when_present() {
2113 let lines = vec![
2114 "PREBOX1",
2115 "{\"con_spec_version\":2,\"validate\":\"yes\",\"sections\":[]}",
2116 "10.0 20.0 30.0",
2117 "90.0 90.0 90.0",
2118 "POSTBOX1",
2119 "POSTBOX2",
2120 "1",
2121 "1",
2122 "12.011",
2123 ];
2124 let mut line_it = lines.iter().copied();
2125 let err = parse_frame_header(&mut line_it).unwrap_err();
2126
2127 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2128 assert!(err.to_string().contains("validate"));
2129 }
2130
2131 #[test]
2132 fn test_validate_true_requires_sections_key() {
2133 let lines = vec![
2134 "PREBOX1",
2135 "{\"con_spec_version\":2,\"validate\":true}",
2136 "10.0 20.0 30.0",
2137 "90.0 90.0 90.0",
2138 "POSTBOX1",
2139 "POSTBOX2",
2140 "1",
2141 "1",
2142 "12.011",
2143 ];
2144 let mut line_it = lines.iter().copied();
2145 let err = parse_frame_header(&mut line_it).unwrap_err();
2146
2147 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2148 assert!(err.to_string().contains("sections"));
2149 }
2150
2151 #[test]
2152 fn test_sections_must_be_string_array_when_present() {
2153 let lines = vec![
2154 "PREBOX1",
2155 "{\"con_spec_version\":2,\"sections\":[\"velocities\",7]}",
2156 "10.0 20.0 30.0",
2157 "90.0 90.0 90.0",
2158 "POSTBOX1",
2159 "POSTBOX2",
2160 "1",
2161 "1",
2162 "12.011",
2163 ];
2164 let mut line_it = lines.iter().copied();
2165 let err = parse_frame_header(&mut line_it).unwrap_err();
2166
2167 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2168 assert!(err.to_string().contains("sections"));
2169 }
2170
2171 #[test]
2172 fn test_validate_true_rejects_non_integer_coordinate_identity_columns() {
2173 let text = r#"
2174PREBOX1
2175{"con_spec_version":2,"sections":[],"validate":true}
217610.0 20.0 30.0
217790.0 90.0 90.0
2178POSTBOX1
2179POSTBOX2
21801
21811
218263.546
2183Cu
2184Coordinates of Component 1
21850.0 0.0 0.0 5.0 0
2186"#;
2187 let mut iter = ConFrameIterator::new(text.trim());
2188 let err = iter.next().unwrap().unwrap_err();
2189
2190 assert!(matches!(err, ParseError::ValidationError(_)));
2191 assert!(err.to_string().contains("fixed_flag"));
2192 }
2193
2194 #[test]
2195 fn test_validate_true_rejects_non_exact_coordinate_label() {
2196 let text = r#"
2197PREBOX1
2198{"con_spec_version":2,"sections":[],"validate":true}
219910.0 20.0 30.0
220090.0 90.0 90.0
2201POSTBOX1
2202POSTBOX2
22031
22041
220563.546
2206Cu
2207Coordinates Component 1
22080.0 0.0 0.0 5 0
2209"#;
2210 let mut iter = ConFrameIterator::new(text.trim());
2211 let err = iter.next().unwrap().unwrap_err();
2212
2213 assert!(matches!(err, ParseError::ValidationError(_)));
2214 assert!(err.to_string().contains("Coordinates of Component 1"));
2215 }
2216
2217 #[test]
2218 fn test_validate_true_rejects_unknown_component_symbol() {
2219 let text = r#"
2220PREBOX1
2221{"con_spec_version":2,"sections":[],"validate":true}
222210.0 20.0 30.0
222390.0 90.0 90.0
2224POSTBOX1
2225POSTBOX2
22261
22271
222863.546
2229Qq
2230Coordinates of Component 1
22310.0 0.0 0.0 0 0
2232"#;
2233 let mut iter = ConFrameIterator::new(text.trim());
2234 let err = iter.next().unwrap().unwrap_err();
2235
2236 assert!(matches!(err, ParseError::ValidationError(_)));
2237 assert!(err.to_string().contains("symbol"));
2238 }
2239
2240 #[test]
2241 fn test_declared_section_must_be_present() {
2242 let text = r#"
2243PREBOX1
2244{"con_spec_version":2,"sections":["velocities"]}
224510.0 20.0 30.0
224690.0 90.0 90.0
2247POSTBOX1
2248POSTBOX2
22491
22501
225163.546
2252Cu
2253Coordinates of Component 1
22540.0 0.0 0.0 0 0
2255"#;
2256 let mut iter = ConFrameIterator::new(text.trim());
2257 let err = iter.next().unwrap().unwrap_err();
2258
2259 assert!(matches!(err, ParseError::IncompleteVelocitySection));
2260 }
2261
2262 #[test]
2263 fn test_non_finite_cell_geometry_rejected_in_strict_mode() {
2264 let lines = vec![
2265 "PREBOX1",
2266 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
2267 "10.0 NaN 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 err = parse_frame_header(&mut line_it).unwrap_err();
2277
2278 assert!(matches!(err, ParseError::ValidationError(_)));
2279 assert!(err.to_string().contains("cell"));
2280 }
2281
2282 #[test]
2283 fn test_validate_true_rejects_non_physical_cell_geometry() {
2284 let lines = vec![
2285 "PREBOX1",
2286 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
2287 "0.0 20.0 30.0",
2288 "90.0 180.0 90.0",
2289 "POSTBOX1",
2290 "POSTBOX2",
2291 "1",
2292 "1",
2293 "12.011",
2294 ];
2295 let mut line_it = lines.iter().copied();
2296 let err = parse_frame_header(&mut line_it).unwrap_err();
2297
2298 assert!(matches!(err, ParseError::ValidationError(_)));
2299 assert!(err.to_string().contains("cell"));
2300 }
2301
2302 #[test]
2303 fn test_validate_true_rejects_reserved_metadata_type_mismatch() {
2304 let lines = vec![
2305 "PREBOX1",
2306 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"energy\":\"low\"}",
2307 "10.0 20.0 30.0",
2308 "90.0 90.0 90.0",
2309 "POSTBOX1",
2310 "POSTBOX2",
2311 "1",
2312 "1",
2313 "12.011",
2314 ];
2315 let mut line_it = lines.iter().copied();
2316 let err = parse_frame_header(&mut line_it).unwrap_err();
2317
2318 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2319 assert!(err.to_string().contains("energy"));
2320 }
2321
2322 #[test]
2323 fn test_validate_true_rejects_malformed_bonds() {
2324 let lines = vec![
2325 "PREBOX1",
2326 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"bonds\":[[0]]}",
2327 "10.0 20.0 30.0",
2328 "90.0 90.0 90.0",
2329 "POSTBOX1",
2330 "POSTBOX2",
2331 "1",
2332 "1",
2333 "12.011",
2334 ];
2335 let mut line_it = lines.iter().copied();
2336 let err = parse_frame_header(&mut line_it).unwrap_err();
2337 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
2338 assert!(err.to_string().contains("bonds"));
2339 }
2340
2341 #[test]
2342 fn test_bonds_metadata_round_trip_in_header() {
2343 use crate::types::{Bond, meta};
2344 let lines = vec![
2345 "PREBOX1",
2346 "{\"con_spec_version\":2,\"bonds\":[[0,1],{\"i\":0,\"j\":2,\"order\":1}]}",
2347 "10.0 20.0 30.0",
2348 "90.0 90.0 90.0",
2349 "POSTBOX1",
2350 "POSTBOX2",
2351 "1",
2352 "1",
2353 "12.011",
2354 ];
2355 let mut line_it = lines.iter().copied();
2356 let header = parse_frame_header(&mut line_it).expect("header");
2357 let bonds = header.bonds();
2358 assert_eq!(bonds.len(), 2);
2359 assert_eq!(bonds[0], Bond::new(0, 1));
2360 assert_eq!(bonds[1].i, 0);
2361 assert_eq!(bonds[1].j, 2);
2362 assert_eq!(bonds[1].order, Some(1));
2363 assert!(header.metadata.contains_key(meta::BONDS));
2364 }
2365
2366 #[test]
2367 fn test_parse_velocity_section_absent() {
2368 let lines = vec![
2369 "PREBOX1",
2370 "{\"con_spec_version\":2}",
2371 "10.0 20.0 30.0",
2372 "90.0 90.0 90.0",
2373 "POSTBOX1",
2374 "POSTBOX2",
2375 "1",
2376 "1",
2377 "12.011",
2378 "C",
2379 "Coordinates of Component 1",
2380 "0.0 0.0 0.0 0.0 1",
2381 ];
2382 let mut line_it = lines.iter().copied().peekable();
2383 let mut frame = parse_single_frame(&mut line_it).expect("parse should succeed");
2384 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
2385 .expect("should succeed with no velocities");
2386 assert!(!has_vel);
2387 assert_eq!(frame.atom_data[0].velocity, None);
2388 }
2389
2390 #[test]
2391 fn test_parse_line_of_range_f64_exact() {
2392 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0 42", 4, 5, &[0.0; 5]).unwrap();
2393 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 42.0]);
2394 }
2395
2396 #[test]
2397 fn test_byte_scan_stack_parses_realistic_coord_line() {
2398 let line = " 0.63939999999999997 0.90449999999999997 -0.00009999999999977 1 0";
2400 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
2401 let mut buf = [0.0f64; 5];
2402 let n = parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
2403 assert_eq!(n, 5);
2404 assert!((buf[0] - 0.6394).abs() < 1e-4);
2405 assert!((buf[1] - 0.9045).abs() < 1e-4);
2406 assert_eq!(buf[3] as u8, 1);
2407 assert_eq!(buf[4] as u64, 0);
2408 let bad = "1.0 2.0 3.0 1 0 EXTRA";
2410 assert!(parse_line_of_range_f64_stack(bad, 4, 5, &defaults, &mut buf).is_err());
2411 let glued = "1.0 2.0 3.0abc 1 0";
2413 assert!(parse_line_of_range_f64_stack(glued, 4, 5, &defaults, &mut buf).is_err());
2414 }
2415
2416 #[test]
2417 fn test_parse_line_of_range_f64_stack_matches_vec_api() {
2418 let defaults = [0.0, 0.0, 0.0, 0.0, 7.0];
2419 let line = "1.0 2.0 3.0 0.0";
2420 let mut buf = [0.0f64; 5];
2421 parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
2422 let via_vec = parse_line_of_range_f64(line, 4, 5, &defaults).unwrap();
2423 assert_eq!(&buf[..5], via_vec.as_slice());
2424 assert_eq!(buf[4], 7.0);
2425 }
2426
2427 #[test]
2428 fn test_parse_line_of_range_f64_padded() {
2429 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
2430 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0", 4, 5, &defaults).unwrap();
2431 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 99.0]);
2432 }
2433
2434 #[test]
2435 fn test_parse_line_of_range_f64_too_few() {
2436 let result = parse_line_of_range_f64("1.0 2.0 3.0", 4, 5, &[0.0; 5]);
2437 assert!(result.is_err());
2438 }
2439
2440 #[test]
2441 fn test_parse_line_of_range_f64_too_many() {
2442 let result = parse_line_of_range_f64("1.0 2.0 3.0 0.0 5.0 6.0", 4, 5, &[0.0; 5]);
2443 assert!(result.is_err());
2444 }
2445
2446 #[test]
2447 fn test_parse_all_four_column_lines() {
2448 let lines = vec![
2450 "PREBOX1",
2451 "{\"con_spec_version\":2}",
2452 "10.0 10.0 10.0",
2453 "90.0 90.0 90.0",
2454 "POSTBOX1",
2455 "POSTBOX2",
2456 "1",
2457 "3",
2458 "12.011",
2459 "C",
2460 "Coordinates of Component 1",
2461 "0.0 0.0 0.0 0",
2462 "1.0 0.0 0.0 0",
2463 "2.0 0.0 0.0 1",
2464 ];
2465 let mut line_it = lines.iter().copied();
2466 let frame = parse_single_frame(&mut line_it).unwrap();
2467 assert_eq!(frame.atom_data[0].atom_id, 0);
2468 assert_eq!(frame.atom_data[1].atom_id, 1);
2469 assert_eq!(frame.atom_data[2].atom_id, 2);
2470 assert!(frame.atom_data[2].is_fixed());
2471 }
2472}