1use crate::error::ParseError;
2use crate::helpers::symbol_to_atomic_number;
3use crate::types::{
4 AtomDatum, ConFrame, FrameHeader, PreboxHeader, SECTION_ENERGIES, SECTION_FORCES,
5 SECTION_VELOCITIES,
6 decode_fixed_bitmask, meta,
7};
8use serde_json::Value;
9use std::collections::BTreeMap;
10use std::iter::Peekable;
11use std::sync::Arc;
12
13#[inline]
17pub fn parse_line_of_range_f64_stack(
18 line: &str,
19 min: usize,
20 max: usize,
21 defaults: &[f64],
22 out: &mut [f64; 5],
23) -> Result<usize, ParseError> {
24 debug_assert!(max <= 5 && min <= max && defaults.len() >= max);
25 let mut found = 0usize;
26 for token in line.split_ascii_whitespace() {
27 if found >= max {
28 return Err(ParseError::InvalidVectorLength {
29 expected: max,
30 found: found + 1,
31 });
32 }
33 let val: f64 = fast_float2::parse(token)
34 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
35 out[found] = val;
36 found += 1;
37 }
38 if found < min || found > max {
39 return Err(ParseError::InvalidVectorLength {
40 expected: max,
41 found,
42 });
43 }
44 while found < max {
45 out[found] = defaults[found];
46 found += 1;
47 }
48 Ok(found)
49}
50
51pub fn parse_line_of_n_f64(line: &str, n: usize) -> Result<Vec<f64>, ParseError> {
63 if n <= 5 {
64 let defaults = [0.0f64; 5];
65 let mut buf = [0.0f64; 5];
66 parse_line_of_range_f64_stack(line, n, n, &defaults, &mut buf)?;
67 return Ok(buf[..n].to_vec());
68 }
69 let mut values = Vec::with_capacity(n);
70 for token in line.split_ascii_whitespace() {
71 let val: f64 = fast_float2::parse(token)
72 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
73 values.push(val);
74 }
75 if values.len() == n {
76 Ok(values)
77 } else {
78 Err(ParseError::InvalidVectorLength {
79 expected: n,
80 found: values.len(),
81 })
82 }
83}
84
85pub fn parse_line_of_range_f64(
92 line: &str,
93 min: usize,
94 max: usize,
95 defaults: &[f64],
96) -> Result<Vec<f64>, ParseError> {
97 if max <= 5 {
98 let mut buf = [0.0f64; 5];
99 parse_line_of_range_f64_stack(line, min, max, defaults, &mut buf)?;
100 return Ok(buf[..max].to_vec());
101 }
102 let mut values = Vec::with_capacity(max);
103 for token in line.split_ascii_whitespace() {
104 let val: f64 = fast_float2::parse(token)
105 .map_err(|_| ParseError::InvalidNumberFormat(format!("invalid float: {token}")))?;
106 values.push(val);
107 }
108 if values.len() < min || values.len() > max {
109 return Err(ParseError::InvalidVectorLength {
110 expected: max,
111 found: values.len(),
112 });
113 }
114 while values.len() < max {
115 let idx = values.len();
116 values.push(defaults[idx]);
117 }
118 Ok(values)
119}
120
121fn metadata_json_error(message: impl Into<String>) -> ParseError {
122 ParseError::InvalidMetadataJson(message.into())
123}
124
125fn validate_metadata_number(key: &str, value: &Value) -> Result<(), ParseError> {
126 if value.as_f64().is_some() {
127 Ok(())
128 } else {
129 Err(metadata_json_error(format!(
130 "{key} must be a finite number"
131 )))
132 }
133}
134
135fn validate_metadata_integer(key: &str, value: &Value) -> Result<(), ParseError> {
136 if value.as_u64().is_some() {
137 Ok(())
138 } else {
139 Err(metadata_json_error(format!(
140 "{key} must be a non-negative integer"
141 )))
142 }
143}
144
145pub fn validate_metadata_schema(
153 json_obj: &serde_json::Map<String, Value>,
154) -> Result<(), ParseError> {
155 let strict_requested = match json_obj.get(meta::VALIDATE) {
156 Some(Value::Bool(b)) => *b,
157 Some(_) => return Err(metadata_json_error("validate must be a boolean")),
158 None => false,
159 };
160
161 match json_obj.get(meta::SECTIONS) {
162 Some(Value::Array(values)) => {
163 if values.iter().any(|entry| !entry.is_string()) {
164 return Err(metadata_json_error("sections must be an array of strings"));
165 }
166 }
167 Some(_) => return Err(metadata_json_error("sections must be an array of strings")),
168 None if strict_requested => {
169 return Err(metadata_json_error(
170 "validate=true requires a sections array, even when empty",
171 ));
172 }
173 None => {}
174 }
175
176 for (key, value) in json_obj {
177 match key.as_str() {
178 meta::CON_SPEC_VERSION | meta::SECTIONS | meta::VALIDATE => {}
179 meta::ENERGY
180 | meta::TIME
181 | meta::TIMESTEP
182 | meta::CONVERGENCE_FMAX
183 | meta::CONVERGENCE_ENERGY
184 | meta::FMAX => validate_metadata_number(key, value)?,
185 meta::FRAME_INDEX | meta::NEB_BEAD | meta::NEB_BAND => {
186 validate_metadata_integer(key, value)?
187 }
188 meta::GENERATOR if !value.is_string() => {
189 return Err(metadata_json_error("generator must be a string"));
190 }
191 meta::GENERATOR => {}
192 meta::UNITS | meta::POTENTIAL if !value.is_object() => {
193 return Err(metadata_json_error(format!("{key} must be an object")));
194 }
195 meta::POTENTIAL => {
196 if let Some(potential_type) = value.get("type")
197 && !potential_type.is_string()
198 {
199 return Err(metadata_json_error("potential.type must be a string"));
200 }
201 }
202 meta::UNITS => {
203 }
205 meta::PBC => validate_pbc_metadata(value)?,
206 meta::BONDS => validate_bonds_metadata(value)?,
207 meta::LATTICE_VECTORS => validate_lattice_vectors_metadata(value)?,
208 meta::CONVERGED if !value.is_boolean() => {
209 return Err(metadata_json_error("converged must be a boolean"));
210 }
211 meta::CONVERGED => {}
212 _ => {}
213 }
214 }
215
216 Ok(())
217}
218
219fn validate_pbc_metadata(value: &Value) -> Result<(), ParseError> {
220 let Some(values) = value.as_array() else {
221 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
222 };
223 if values.len() != 3 || values.iter().any(|entry| !entry.is_boolean()) {
224 return Err(metadata_json_error("pbc must be a length-3 boolean array"));
225 }
226 Ok(())
227}
228
229fn validate_bonds_metadata(value: &Value) -> Result<(), ParseError> {
236 let Some(items) = value.as_array() else {
237 return Err(metadata_json_error("bonds must be an array"));
238 };
239 for (idx, item) in items.iter().enumerate() {
240 if let Some(pair) = item.as_array() {
241 if pair.len() != 2 {
242 return Err(metadata_json_error(format!(
243 "bonds[{idx}] pair must have exactly two indices"
244 )));
245 }
246 for (k, entry) in pair.iter().enumerate() {
247 let Some(n) = entry.as_u64() else {
248 return Err(metadata_json_error(format!(
249 "bonds[{idx}][{k}] must be a non-negative integer"
250 )));
251 };
252 if n > u32::MAX as u64 {
253 return Err(metadata_json_error(format!(
254 "bonds[{idx}][{k}] index exceeds u32"
255 )));
256 }
257 }
258 continue;
259 }
260 if let Some(obj) = item.as_object() {
261 for key in ["i", "j"] {
262 let Some(entry) = obj.get(key) else {
263 return Err(metadata_json_error(format!(
264 "bonds[{idx}] object must include \"{key}\""
265 )));
266 };
267 let Some(n) = entry.as_u64() else {
268 return Err(metadata_json_error(format!(
269 "bonds[{idx}].{key} must be a non-negative integer"
270 )));
271 };
272 if n > u32::MAX as u64 {
273 return Err(metadata_json_error(format!(
274 "bonds[{idx}].{key} index exceeds u32"
275 )));
276 }
277 }
278 if let Some(order) = obj.get("order")
279 && order.as_i64().is_none()
280 {
281 return Err(metadata_json_error(format!(
282 "bonds[{idx}].order must be an integer when present"
283 )));
284 }
285 continue;
286 }
287 return Err(metadata_json_error(format!(
288 "bonds[{idx}] must be [i, j] or {{\"i\": i, \"j\": j, \"order\"?: ...}}"
289 )));
290 }
291 Ok(())
292}
293
294fn validate_lattice_vectors_metadata(value: &Value) -> Result<(), ParseError> {
295 let Some(rows) = value.as_array() else {
296 return Err(metadata_json_error(
297 "lattice_vectors must be a 3x3 numeric array",
298 ));
299 };
300 if rows.len() != 3 {
301 return Err(metadata_json_error(
302 "lattice_vectors must be a 3x3 numeric array",
303 ));
304 }
305 for row in rows {
306 let Some(entries) = row.as_array() else {
307 return Err(metadata_json_error(
308 "lattice_vectors must be a 3x3 numeric array",
309 ));
310 };
311 if entries.len() != 3 || entries.iter().any(|entry| entry.as_f64().is_none()) {
312 return Err(metadata_json_error(
313 "lattice_vectors must be a 3x3 numeric array",
314 ));
315 }
316 }
317 Ok(())
318}
319
320pub fn parse_line_of_n<T: std::str::FromStr>(line: &str, n: usize) -> Result<Vec<T>, ParseError>
348where
349 ParseError: From<<T as std::str::FromStr>::Err>,
350{
351 let values: Vec<T> = line
352 .split_whitespace()
353 .map(|s| s.parse::<T>())
354 .collect::<Result<_, _>>()?;
355
356 if values.len() == n {
357 Ok(values)
358 } else {
359 Err(ParseError::InvalidVectorLength {
360 expected: n,
361 found: values.len(),
362 })
363 }
364}
365
366pub fn parse_frame_header<'a>(
387 lines: &mut impl Iterator<Item = &'a str>,
388) -> Result<FrameHeader, ParseError> {
389 let prebox1 = lines
390 .next()
391 .ok_or(ParseError::IncompleteHeader)?
392 .to_string();
393 let prebox2_raw = lines.next().ok_or(ParseError::IncompleteHeader)?;
394
395 let trimmed = prebox2_raw.trim();
398 let (spec_version, metadata, sections, validate, sections_declared) = if trimmed.starts_with('{') {
399 let json_val: serde_json::Value = serde_json::from_str(trimmed)
400 .map_err(|e| ParseError::InvalidMetadataJson(e.to_string()))?;
401 let json_obj = json_val
402 .as_object()
403 .ok_or_else(|| ParseError::InvalidMetadataJson("expected a JSON object".to_string()))?;
404 let ver = json_obj
405 .get(meta::CON_SPEC_VERSION)
406 .and_then(|v| v.as_u64())
407 .ok_or(ParseError::MissingSpecVersion)? as u32;
408 if ver > crate::CON_SPEC_VERSION {
409 return Err(ParseError::UnsupportedSpecVersion(ver));
410 }
411 if ver >= 3 {
412 match json_obj.get(meta::UNITS) {
413 Some(u) => crate::units::validate_v3_units_metadata(u).map_err(|e| {
414 ParseError::ValidationError(format!("v3 units: {e}"))
415 })?,
416 None => {
417 return Err(ParseError::ValidationError(
418 "con_spec_version >= 3 requires metadata \"units\" with length and energy"
419 .into(),
420 ));
421 }
422 }
423 }
424
425 let mut sections: Vec<String> = Vec::new();
429 let mut metadata = BTreeMap::new();
430 let mut sections_declared = false;
431 let mut validate = false;
432 for (k, v) in json_obj {
433 match k.as_str() {
434 meta::CON_SPEC_VERSION => {}
435 meta::SECTIONS => {
436 sections_declared = true;
437 let arr = v.as_array().ok_or_else(|| {
438 metadata_json_error("sections must be an array of strings")
439 })?;
440 sections.reserve(arr.len());
441 for entry in arr {
442 let s = entry.as_str().ok_or_else(|| {
443 metadata_json_error("sections must be an array of strings")
444 })?;
445 sections.push(s.to_string());
446 }
447 }
448 meta::VALIDATE => {
449 validate = match v {
450 Value::Bool(b) => *b,
451 _ => return Err(metadata_json_error("validate must be a boolean")),
452 };
453 metadata.insert(k.clone(), v.clone());
454 }
455 _ => {
456 metadata.insert(k.clone(), v.clone());
457 }
458 }
459 }
460
461 if validate {
464 validate_metadata_schema(json_obj)?;
465 }
466
467 (ver, metadata, sections, validate, sections_declared)
468 } else {
469 (1_u32, BTreeMap::new(), Vec::new(), false, false)
471 };
472 let prebox2 = prebox2_raw.to_string();
473
474 let boxl_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
475 let angles_vec = parse_line_of_n_f64(lines.next().ok_or(ParseError::IncompleteHeader)?, 3)?;
476 let postbox1 = lines
477 .next()
478 .ok_or(ParseError::IncompleteHeader)?
479 .to_string();
480 let postbox2 = lines
481 .next()
482 .ok_or(ParseError::IncompleteHeader)?
483 .to_string();
484 let natm_types =
485 parse_line_of_n::<usize>(lines.next().ok_or(ParseError::IncompleteHeader)?, 1)?[0];
486 let natms_per_type = parse_line_of_n::<usize>(
487 lines.next().ok_or(ParseError::IncompleteHeader)?,
488 natm_types,
489 )?;
490 let masses_per_type = parse_line_of_n_f64(
491 lines.next().ok_or(ParseError::IncompleteHeader)?,
492 natm_types,
493 )?;
494 if validate {
495 validate_header_geometry(&boxl_vec, &angles_vec, natm_types, &natms_per_type)?;
496 validate_masses(&masses_per_type)?;
497 }
498 Ok(FrameHeader {
499 prebox_header: PreboxHeader {
500 user: prebox1,
501 metadata_line: prebox2,
502 },
503 boxl: boxl_vec.try_into().unwrap(),
504 angles: angles_vec.try_into().unwrap(),
505 postbox_header: [postbox1, postbox2],
506 natm_types,
507 natms_per_type,
508 masses_per_type,
509 spec_version,
510 metadata,
511 sections,
512 strict_validation: validate,
513 sections_declared,
514 })
515}
516
517pub fn parse_single_frame<'a>(
566 lines: &mut impl Iterator<Item = &'a str>,
567) -> Result<ConFrame, ParseError> {
568 let header = parse_frame_header(lines)?;
569 let validate = header.strict_validation;
570 let total_atoms: usize = header.natms_per_type.iter().sum();
571 let mut atom_data = Vec::with_capacity(total_atoms);
572 use crate::storage_dtype::{FloatArray2, StorageDtypes};
574 let dt = StorageDtypes::from_metadata(&header.metadata).unwrap_or_default();
575 let mut positions = FloatArray2::zeros(dt.positions, total_atoms, 3);
576
577 let mut global_atom_idx: u64 = 0;
578 let mut atom_i = 0usize;
579 for (type_idx, num_atoms) in header.natms_per_type.iter().enumerate() {
580 let symbol_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
584 let symbol: Arc<str> = Arc::from(symbol_line.trim());
585 let coord_label = lines.next().ok_or(ParseError::IncompleteFrame)?;
586 if validate {
587 validate_coordinate_component(type_idx, symbol.as_ref(), coord_label)?;
588 }
589 for _ in 0..*num_atoms {
590 let coord_line = lines.next().ok_or(ParseError::IncompleteFrame)?;
591 let defaults = [0.0, 0.0, 0.0, 0.0, global_atom_idx as f64];
593 let mut vals = [0.0f64; 5];
594 parse_line_of_range_f64_stack(coord_line, 4, 5, &defaults, &mut vals)?;
595 let (fixed, atom_id) = if validate {
596 parse_identity_columns(coord_line, "coordinate", 3, 4, 5)?
597 } else {
598 (decode_fixed_bitmask(vals[3] as u8), vals[4] as u64)
599 };
600 let xyz = [vals[0], vals[1], vals[2]];
601 positions.set_f64_row(atom_i, xyz);
602 atom_data.push(AtomDatum {
603 symbol: Arc::clone(&symbol),
605 x: xyz[0],
606 y: xyz[1],
607 z: xyz[2],
608 fixed,
609 atom_id,
610 velocity: None,
611 force: None,
612 energy: None,
613 });
614 global_atom_idx += 1;
615 atom_i += 1;
616 }
617 }
618 Ok(crate::types::con_frame_from_atom_data_with_positions(
620 header, atom_data, positions,
621 ))
622}
623
624fn validate_header_geometry(
625 boxl: &[f64],
626 angles: &[f64],
627 natm_types: usize,
628 natms_per_type: &[usize],
629) -> Result<(), ParseError> {
630 if boxl.iter().any(|length| !length.is_finite() || *length <= 0.0)
631 || angles
632 .iter()
633 .any(|angle| !angle.is_finite() || *angle <= 0.0 || *angle >= 180.0)
634 {
635 return Err(ParseError::ValidationError(
636 "cell geometry must have positive lengths and angles between 0 and 180 degrees"
637 .to_string(),
638 ));
639 }
640 if natm_types == 0 || natms_per_type.contains(&0) {
641 return Err(ParseError::ValidationError(
642 "atom counts must contain at least one atom per component".to_string(),
643 ));
644 }
645 Ok(())
646}
647
648fn validate_masses(masses_per_type: &[f64]) -> Result<(), ParseError> {
649 if masses_per_type
650 .iter()
651 .any(|mass| !mass.is_finite() || *mass <= 0.0)
652 {
653 return Err(ParseError::ValidationError(
654 "component masses must be positive".to_string(),
655 ));
656 }
657 Ok(())
658}
659
660fn validate_coordinate_component(
661 type_idx: usize,
662 symbol: &str,
663 label: &str,
664) -> Result<(), ParseError> {
665 let expected_label = format!("Coordinates of Component {}", type_idx + 1);
666 if label.trim() != expected_label {
667 return Err(ParseError::ValidationError(format!(
668 "expected coordinate label {expected_label:?}, found {label:?}"
669 )));
670 }
671 if symbol != "X" && symbol_to_atomic_number(symbol) == 0 {
672 return Err(ParseError::ValidationError(format!(
673 "unknown component symbol {symbol}"
674 )));
675 }
676 Ok(())
677}
678
679fn parse_identity_columns(
694 line: &str,
695 row_kind: &str,
696 fixed_idx: usize,
697 atom_id_idx: usize,
698 n_cols: usize,
699) -> Result<([bool; 3], u64), ParseError> {
700 let columns = line.split_ascii_whitespace().collect::<Vec<_>>();
701 if columns.len() != n_cols {
702 return Err(ParseError::ValidationError(format!(
703 "{row_kind} rows require {n_cols} columns including fixed_flag and atom_id in validate mode"
704 )));
705 }
706 let fixed_flag = columns[fixed_idx].parse::<u8>().map_err(|_| {
707 ParseError::ValidationError(format!("{row_kind} fixed_flag must be an integer bitmask"))
708 })?;
709 if fixed_flag > 7 {
710 return Err(ParseError::ValidationError(format!(
711 "{row_kind} fixed_flag must be between 0 and 7"
712 )));
713 }
714 let atom_id = columns[atom_id_idx].parse::<u64>().map_err(|_| {
715 ParseError::ValidationError(format!("{row_kind} atom_id must be an integer"))
716 })?;
717 Ok((decode_fixed_bitmask(fixed_flag), atom_id))
718}
719
720
721fn validate_section_component(
722 section: &str,
723 type_idx: usize,
724 atom_idx: usize,
725 symbol: &str,
726 label: &str,
727 header: &FrameHeader,
728 atom_data: &[AtomDatum],
729) -> Result<(), ParseError> {
730 let expected_label = format!("{section} of Component {}", type_idx + 1);
731 if label.trim() != expected_label {
732 return Err(ParseError::ValidationError(format!(
733 "expected section label {expected_label:?}, found {label:?}"
734 )));
735 }
736
737 if header.natms_per_type[type_idx] == 0 {
738 return Ok(());
739 }
740
741 let expected_symbol = atom_data
742 .get(atom_idx)
743 .map(|atom| atom.symbol.as_ref())
744 .ok_or_else(|| {
745 ParseError::ValidationError(format!(
746 "{section} component {} has no coordinate atom to validate against",
747 type_idx + 1
748 ))
749 })?;
750 if symbol != expected_symbol {
751 return Err(ParseError::ValidationError(format!(
752 "{section} component {} symbol mismatch: expected {expected_symbol}, found {symbol}",
753 type_idx + 1
754 )));
755 }
756
757 Ok(())
758}
759
760fn validate_section_atom_identity(
761 section: &str,
762 atom_idx: usize,
763 fixed: [bool; 3],
764 atom_id: u64,
765 atom_data: &[AtomDatum],
766) -> Result<(), ParseError> {
767 let atom = atom_data.get(atom_idx).ok_or_else(|| {
768 ParseError::ValidationError(format!(
769 "{section} row {atom_idx} has no coordinate atom to validate against"
770 ))
771 })?;
772
773 if atom.fixed != fixed {
774 return Err(ParseError::ValidationError(format!(
775 "{section} row {atom_idx} fixed mask mismatch for atom_id {}",
776 atom.atom_id
777 )));
778 }
779 if atom.atom_id != atom_id {
780 return Err(ParseError::ValidationError(format!(
781 "{section} row {atom_idx} atom_id mismatch: expected {}, found {atom_id}",
782 atom.atom_id
783 )));
784 }
785
786 Ok(())
787}
788
789pub fn parse_velocity_section<'a, I>(
802 lines: &mut Peekable<I>,
803 header: &FrameHeader,
804 atom_data: &mut [AtomDatum],
805) -> Result<bool, ParseError>
806where
807 I: Iterator<Item = &'a str>,
808{
809 let validate = header.strict_validation;
810 match lines.peek() {
812 Some(line) if line.trim().is_empty() => {
813 lines.next();
815 }
816 _ => return Ok(false),
817 }
818
819 let mut atom_idx: usize = 0;
820 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
821 let symbol = lines
823 .next()
824 .ok_or(ParseError::IncompleteVelocitySection)?
825 .trim();
826
827 let comp_line = lines.next().ok_or(ParseError::IncompleteVelocitySection)?;
829 if !comp_line.contains("Velocities of Component") {
831 return Err(ParseError::IncompleteVelocitySection);
832 }
833 if validate {
834 validate_section_component(
835 "Velocities",
836 type_idx,
837 atom_idx,
838 symbol,
839 comp_line,
840 header,
841 atom_data,
842 )?;
843 }
844
845 for _ in 0..num_atoms {
846 let vel_line = lines.next().ok_or(ParseError::IncompleteVelocitySection)?;
847 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
849 let mut vals = [0.0f64; 5];
850 parse_line_of_range_f64_stack(vel_line, 4, 5, &defaults, &mut vals)?;
851 if validate {
852 let (fixed, atom_id) =
853 parse_identity_columns(vel_line, "velocities", 3, 4, 5)?;
854 validate_section_atom_identity("velocities", atom_idx, fixed, atom_id, atom_data)?;
855 }
856 if atom_idx < atom_data.len() {
857 atom_data[atom_idx].velocity = Some([vals[0], vals[1], vals[2]]);
858 }
859 atom_idx += 1;
860 }
861 }
862
863 Ok(true)
864}
865
866pub fn parse_force_section<'a, I>(
874 lines: &mut Peekable<I>,
875 header: &FrameHeader,
876 atom_data: &mut [AtomDatum],
877) -> Result<bool, ParseError>
878where
879 I: Iterator<Item = &'a str>,
880{
881 let validate = header.strict_validation;
882 match lines.peek() {
884 Some(line) if line.trim().is_empty() => {
885 lines.next();
886 }
887 _ => return Ok(false),
888 }
889
890 let mut atom_idx: usize = 0;
891 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
892 let symbol = lines
893 .next()
894 .ok_or(ParseError::IncompleteForceSection)?
895 .trim();
896
897 let comp_line = lines.next().ok_or(ParseError::IncompleteForceSection)?;
898 if !comp_line.contains("Forces of Component") {
899 return Err(ParseError::IncompleteForceSection);
900 }
901 if validate {
902 validate_section_component(
903 "Forces", type_idx, atom_idx, symbol, comp_line, header, atom_data,
904 )?;
905 }
906
907 for _ in 0..num_atoms {
908 let force_line = lines.next().ok_or(ParseError::IncompleteForceSection)?;
909 let defaults = [0.0, 0.0, 0.0, 0.0, atom_idx as f64];
910 let mut vals = [0.0f64; 5];
911 parse_line_of_range_f64_stack(force_line, 4, 5, &defaults, &mut vals)?;
912 if validate {
913 let (fixed, atom_id) =
914 parse_identity_columns(force_line, "forces", 3, 4, 5)?;
915 validate_section_atom_identity("forces", atom_idx, fixed, atom_id, atom_data)?;
916 }
917 if atom_idx < atom_data.len() {
918 atom_data[atom_idx].force = Some([vals[0], vals[1], vals[2]]);
919 }
920 atom_idx += 1;
921 }
922 }
923
924 Ok(true)
925}
926
927pub fn parse_energy_section<'a, I>(
940 lines: &mut Peekable<I>,
941 header: &FrameHeader,
942 atom_data: &mut [AtomDatum],
943) -> Result<bool, ParseError>
944where
945 I: Iterator<Item = &'a str>,
946{
947 let validate = header.strict_validation;
948 match lines.peek() {
949 Some(line) if line.trim().is_empty() => {
950 lines.next();
951 }
952 _ => return Ok(false),
953 }
954
955 let mut atom_idx: usize = 0;
956 for (type_idx, &num_atoms) in header.natms_per_type.iter().enumerate() {
957 let symbol = lines
958 .next()
959 .ok_or(ParseError::IncompleteEnergySection)?
960 .trim();
961
962 let comp_line = lines.next().ok_or(ParseError::IncompleteEnergySection)?;
963 if !comp_line.contains("Energies of Component") {
964 return Err(ParseError::IncompleteEnergySection);
965 }
966 if validate {
967 validate_section_component(
968 "Energies", type_idx, atom_idx, symbol, comp_line, header, atom_data,
969 )?;
970 }
971
972 for _ in 0..num_atoms {
973 let energy_line = lines.next().ok_or(ParseError::IncompleteEnergySection)?;
974 let defaults = [0.0, 0.0, atom_idx as f64];
977 let vals = parse_line_of_range_f64(energy_line, 1, 3, &defaults)?;
978 if validate {
979 let (fixed, atom_id) =
980 parse_identity_columns(energy_line, "energies", 1, 2, 3)?;
981 validate_section_atom_identity("energies", atom_idx, fixed, atom_id, atom_data)?;
982 }
983 if atom_idx < atom_data.len() {
984 atom_data[atom_idx].energy = Some(vals[0]);
985 }
986 atom_idx += 1;
987 }
988 }
989
990 Ok(true)
991}
992
993pub fn parse_declared_sections<'a, I>(
999 lines: &mut Peekable<I>,
1000 header: &mut FrameHeader,
1001 atom_data: &mut [AtomDatum],
1002) -> Result<(), ParseError>
1003where
1004 I: Iterator<Item = &'a str>,
1005{
1006 if !header.sections_declared && header.sections.is_empty() {
1007 let found = parse_velocity_section(lines, header, atom_data)?;
1009 if found {
1010 header.sections.push(SECTION_VELOCITIES.into());
1011 }
1012 } else {
1013 let sections = std::mem::take(&mut header.sections);
1014 for section in §ions {
1015 match section.as_str() {
1016 SECTION_VELOCITIES => {
1017 let found = parse_velocity_section(lines, header, atom_data)?;
1018 if !found {
1019 return Err(ParseError::IncompleteVelocitySection);
1020 }
1021 }
1022 SECTION_FORCES => {
1023 let found = parse_force_section(lines, header, atom_data)?;
1024 if !found {
1025 return Err(ParseError::IncompleteForceSection);
1026 }
1027 }
1028 SECTION_ENERGIES => {
1029 let found = parse_energy_section(lines, header, atom_data)?;
1030 if !found {
1031 return Err(ParseError::IncompleteEnergySection);
1032 }
1033 }
1034 other => return Err(ParseError::UnknownSection(other.to_string())),
1035 }
1036 }
1037 header.sections = sections;
1038 }
1039 Ok(())
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044 use super::*;
1045 use crate::iterators::ConFrameIterator;
1046
1047 #[test]
1048 fn test_parse_line_of_n_success() {
1049 let line = "1.0 2.5 -3.0";
1050 let values = parse_line_of_n::<f64>(line, 3).unwrap();
1051 assert_eq!(values, vec![1.0, 2.5, -3.0]);
1052 }
1053
1054 #[test]
1055 fn test_parse_line_of_n_too_short() {
1056 let line = "1.0 2.5";
1057 let result = parse_line_of_n::<f64>(line, 3);
1058 assert!(result.is_err());
1059 assert!(matches!(
1060 result.unwrap_err(),
1061 ParseError::InvalidVectorLength {
1062 expected: 3,
1063 found: 2
1064 }
1065 ));
1066 }
1067
1068 #[test]
1069 fn test_parse_line_of_n_too_long() {
1070 let line = "1.0 2.5 -3.0 4.0";
1071 let result = parse_line_of_n::<f64>(line, 3);
1072 assert!(result.is_err());
1073 assert!(matches!(
1074 result.unwrap_err(),
1075 ParseError::InvalidVectorLength {
1076 expected: 3,
1077 found: 4
1078 }
1079 ));
1080 }
1081
1082 #[test]
1083 fn test_parse_line_of_n_invalid_float() {
1084 let line = "1.0 abc -3.0";
1085 let result = parse_line_of_n::<f64>(line, 3);
1086 assert!(result.is_err());
1087 assert!(matches!(
1088 result.unwrap_err(),
1089 ParseError::InvalidNumberFormat(_)
1090 ));
1091 }
1092
1093 #[test]
1094 fn test_parse_frame_header_success() {
1095 let lines = [
1096 "PREBOX1",
1097 "{\"con_spec_version\":2}",
1098 "10.0 20.0 30.0",
1099 "90.0 90.0 90.0",
1100 "POSTBOX1",
1101 "POSTBOX2",
1102 "2",
1103 "1 1",
1104 "12.011 1.008",
1105 ];
1106 let mut line_it = lines.iter().copied();
1107 match parse_frame_header(&mut line_it) {
1108 Ok(header) => {
1109 assert_eq!(header.prebox_header.user, "PREBOX1");
1110 assert_eq!(header.spec_version, 2);
1111 assert_eq!(header.boxl, [10.0, 20.0, 30.0]);
1112 assert_eq!(header.angles, [90.0, 90.0, 90.0]);
1113 assert_eq!(header.postbox_header, ["POSTBOX1", "POSTBOX2"]);
1114 assert_eq!(header.natm_types, 2);
1115 assert_eq!(header.natms_per_type, vec![1, 1]);
1116 assert_eq!(header.masses_per_type, vec![12.011, 1.008]);
1117 }
1118 Err(e) => {
1119 panic!(
1120 "Parsing failed when it should have succeeded. Error: {:?}",
1121 e
1122 );
1123 }
1124 }
1125 }
1126
1127 #[test]
1128 fn test_parse_frame_header_missing_line() {
1129 let lines = [
1130 "PREBOX1",
1131 "{\"con_spec_version\":2}",
1132 "10.0 20.0 30.0",
1133 "90.0 90.0 90.0",
1134 "POSTBOX1",
1135 "POSTBOX2",
1136 "2",
1137 "1 1",
1138 ];
1140 let mut line_it = lines.iter().copied();
1141 let result = parse_frame_header(&mut line_it);
1142 assert!(result.is_err());
1143 assert!(matches!(result.unwrap_err(), ParseError::IncompleteHeader));
1144 }
1145
1146 #[test]
1147 fn test_parse_frame_header_missing_spec_version() {
1148 let lines = vec![
1149 "PREBOX1",
1150 "{}",
1151 "10.0 20.0 30.0",
1152 "90.0 90.0 90.0",
1153 "POSTBOX1",
1154 "POSTBOX2",
1155 "2",
1156 "1 1",
1157 "12.011 1.008",
1158 ];
1159 let mut line_it = lines.iter().copied();
1160 let result = parse_frame_header(&mut line_it);
1161 assert!(result.is_err());
1162 assert!(matches!(
1163 result.unwrap_err(),
1164 ParseError::MissingSpecVersion
1165 ));
1166 }
1167
1168 #[test]
1169 fn test_parse_frame_header_legacy_no_json() {
1170 let lines = vec![
1172 "PREBOX1",
1173 "0.0000 TIME",
1174 "10.0 20.0 30.0",
1175 "90.0 90.0 90.0",
1176 "POSTBOX1",
1177 "POSTBOX2",
1178 "2",
1179 "1 1",
1180 "12.011 1.008",
1181 ];
1182 let mut line_it = lines.iter().copied();
1183 let header = parse_frame_header(&mut line_it).unwrap();
1184 assert_eq!(header.spec_version, 1);
1185 assert!(header.metadata.is_empty());
1186 }
1187
1188 #[test]
1189 fn test_parse_frame_header_malformed_json() {
1190 let lines = vec![
1192 "PREBOX1",
1193 "{broken json",
1194 "10.0 20.0 30.0",
1195 "90.0 90.0 90.0",
1196 "POSTBOX1",
1197 "POSTBOX2",
1198 "2",
1199 "1 1",
1200 "12.011 1.008",
1201 ];
1202 let mut line_it = lines.iter().copied();
1203 let result = parse_frame_header(&mut line_it);
1204 assert!(result.is_err());
1205 assert!(matches!(
1206 result.unwrap_err(),
1207 ParseError::InvalidMetadataJson(_)
1208 ));
1209 }
1210
1211 #[test]
1212 fn test_parse_frame_header_unsupported_version() {
1213 let lines = vec![
1214 "PREBOX1",
1215 "{\"con_spec_version\":999}",
1216 "10.0 20.0 30.0",
1217 "90.0 90.0 90.0",
1218 "POSTBOX1",
1219 "POSTBOX2",
1220 "2",
1221 "1 1",
1222 "12.011 1.008",
1223 ];
1224 let mut line_it = lines.iter().copied();
1225 let result = parse_frame_header(&mut line_it);
1226 assert!(result.is_err());
1227 assert!(matches!(
1228 result.unwrap_err(),
1229 ParseError::UnsupportedSpecVersion(999)
1230 ));
1231 }
1232
1233 #[test]
1234 fn test_v3_missing_units_rejected() {
1235 let lines = vec![
1236 "PREBOX1",
1237 "{\"con_spec_version\":3}",
1238 "10.0 20.0 30.0",
1239 "90.0 90.0 90.0",
1240 "POSTBOX1",
1241 "POSTBOX2",
1242 "1",
1243 "1",
1244 "1.0",
1245 ];
1246 let mut line_it = lines.iter().copied();
1247 let err = parse_frame_header(&mut line_it).unwrap_err();
1248 let msg = err.to_string();
1249 assert!(
1250 msg.contains("units") || msg.contains("v3"),
1251 "expected units error, got {msg}"
1252 );
1253 }
1254
1255 #[test]
1256 fn test_v3_invalid_units_rejected() {
1257 let lines = vec![
1258 "PREBOX1",
1259 r#"{"con_spec_version":3,"units":{"length":"eV","energy":"angstrom"}}"#,
1260 "10.0 20.0 30.0",
1261 "90.0 90.0 90.0",
1262 "POSTBOX1",
1263 "POSTBOX2",
1264 "1",
1265 "1",
1266 "1.0",
1267 ];
1268 let mut line_it = lines.iter().copied();
1269 assert!(parse_frame_header(&mut line_it).is_err());
1270 }
1271
1272 #[test]
1273 fn test_v3_valid_units_exposes_length_energy() {
1274 let lines = vec![
1275 "PREBOX1",
1276 r#"{"con_spec_version":3,"units":{"length":"angstrom","energy":"eV","mass":"amu","time":"fs"}}"#,
1277 "10.0 20.0 30.0",
1278 "90.0 90.0 90.0",
1279 "POSTBOX1",
1280 "POSTBOX2",
1281 "1",
1282 "1",
1283 "1.0",
1284 ];
1285 let mut line_it = lines.iter().copied();
1286 let header = parse_frame_header(&mut line_it).unwrap();
1287 assert_eq!(header.spec_version, 3);
1288 assert_eq!(header.length_unit(), Some("angstrom"));
1289 assert_eq!(header.energy_unit(), Some("eV"));
1290 let f = header.conversion_factor_to("length", "nm").unwrap();
1291 assert!((f - 0.1).abs() < 1e-12);
1292 }
1293
1294 #[test]
1295 fn test_parse_frame_header_extra_metadata_preserved() {
1296 let lines = vec![
1297 "PREBOX1",
1298 "{\"con_spec_version\":2,\"generator\":\"test\"}",
1299 "10.0 20.0 30.0",
1300 "90.0 90.0 90.0",
1301 "POSTBOX1",
1302 "POSTBOX2",
1303 "2",
1304 "1 1",
1305 "12.011 1.008",
1306 ];
1307 let mut line_it = lines.iter().copied();
1308 let header = parse_frame_header(&mut line_it).unwrap();
1309 assert_eq!(header.spec_version, 2);
1310 assert_eq!(
1311 header.metadata.get("generator"),
1312 Some(&serde_json::Value::String("test".to_string()))
1313 );
1314 }
1315
1316 #[test]
1317 fn test_parse_frame_header_invalid_natms_per_type() {
1318 let lines = vec![
1319 "PREBOX1",
1320 "{\"con_spec_version\":2}",
1321 "10.0 20.0 30.0",
1322 "90.0 90.0 90.0",
1323 "POSTBOX1",
1324 "POSTBOX2",
1325 "2",
1326 "1 1 1", "12.011 1.008",
1328 ];
1329 let mut line_it = lines.iter().copied();
1330 let result = parse_frame_header(&mut line_it);
1331 assert!(result.is_err());
1332 assert!(matches!(
1333 result.unwrap_err(),
1334 ParseError::InvalidVectorLength {
1335 expected: 2,
1336 found: 3
1337 }
1338 ));
1339 }
1340
1341 #[test]
1342 fn test_parse_single_frame_success() {
1343 let lines = vec![
1344 "PREBOX1",
1345 "{\"con_spec_version\":2}",
1346 "10.0 20.0 30.0",
1347 "90.0 90.0 90.0",
1348 "POSTBOX1",
1349 "POSTBOX2",
1350 "2",
1351 "3 3",
1352 "12.011 1.008",
1353 "1",
1354 "Coordinates of Component 1",
1355 "0.0 0.0 0.0 0.0 1",
1356 "1.0940 0.0 0.0 0.0 2",
1357 "-0.5470 0.9499 0.0 0.0 3",
1358 "2",
1359 "Coordinates of Component 2",
1360 "5.0 5.0 5.0 0.0 4",
1361 "6.0940 5.0 5.0 0.0 5",
1362 "5.5470 5.9499 5.0 0.0 6",
1363 ];
1364 let mut line_it = lines.iter().copied();
1365 let frame = parse_single_frame(&mut line_it).unwrap();
1366
1367 assert_eq!(frame.header.natm_types, 2);
1368 assert_eq!(frame.header.natms_per_type, vec![3, 3]);
1369 assert_eq!(frame.header.masses_per_type, vec![12.011, 1.008]);
1370 assert_eq!(frame.atom_data.len(), 6);
1371 assert_eq!(&*frame.atom_data[0].symbol, "1");
1372 assert_eq!(frame.atom_data[0].atom_id, 1);
1373 assert_eq!(&*frame.atom_data[5].symbol, "2");
1374 assert_eq!(frame.atom_data[5].atom_id, 6);
1375 }
1376
1377 #[test]
1378 fn test_parse_single_frame_missing_line() {
1379 let lines = vec![
1381 "PREBOX1",
1382 "{\"con_spec_version\":2}",
1383 "10.0 20.0 30.0",
1384 "90.0 90.0 90.0",
1385 "POSTBOX1",
1386 "POSTBOX2",
1387 "2",
1388 "3 3",
1389 "12.011 1.008",
1390 "1",
1391 "Coordinates of Component 1",
1392 "0.0 0.0 0.0 0.0 1",
1393 "1.0940 0.0 0.0 0.0 2",
1394 "-0.5470 0.9499 0.0 0.0 3",
1395 ];
1397 let mut line_it = lines.iter().copied();
1398 let result = parse_single_frame(&mut line_it);
1399 assert!(result.is_err());
1400 assert!(matches!(result.unwrap_err(), ParseError::IncompleteFrame));
1401 }
1402
1403 #[test]
1404 fn test_parse_single_frame_missing_atom_index_defaults_sequential() {
1405 let lines = vec![
1407 "PREBOX1",
1408 "{\"con_spec_version\":2}",
1409 "10.0 20.0 30.0",
1410 "90.0 90.0 90.0",
1411 "POSTBOX1",
1412 "POSTBOX2",
1413 "2",
1414 "3 3",
1415 "12.011 1.008",
1416 "1",
1417 "Coordinates of Component 1",
1418 "0.0 0.0 0.0 0.0 1",
1419 "1.0940 0.0 0.0 0.0 2",
1420 "-0.5470 0.9499 0.0 0.0 3",
1421 "2",
1422 "Coordinates of Component 2",
1423 "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", ];
1427 let mut line_it = lines.iter().copied();
1428 let frame = parse_single_frame(&mut line_it).unwrap();
1429 assert_eq!(frame.atom_data.len(), 6);
1430 assert_eq!(frame.atom_data[0].atom_id, 1);
1432 assert_eq!(frame.atom_data[1].atom_id, 2);
1433 assert_eq!(frame.atom_data[2].atom_id, 3);
1434 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); }
1439
1440 #[test]
1441 fn test_parse_single_frame_too_few_columns_fails() {
1442 let lines = vec![
1444 "PREBOX1",
1445 "{\"con_spec_version\":2}",
1446 "10.0 20.0 30.0",
1447 "90.0 90.0 90.0",
1448 "POSTBOX1",
1449 "POSTBOX2",
1450 "1",
1451 "1",
1452 "12.011",
1453 "C",
1454 "Coordinates of Component 1",
1455 "0.0 0.0 0.0", ];
1457 let mut line_it = lines.iter().copied();
1458 let result = parse_single_frame(&mut line_it);
1459 assert!(result.is_err());
1460 assert!(matches!(
1461 result.unwrap_err(),
1462 ParseError::InvalidVectorLength {
1463 expected: 5,
1464 found: 3
1465 }
1466 ));
1467 }
1468
1469 #[test]
1470 fn test_parse_velocity_section_present() {
1471 let lines = vec![
1472 "PREBOX1",
1473 "{\"con_spec_version\":2}",
1474 "10.0 20.0 30.0",
1475 "90.0 90.0 90.0",
1476 "POSTBOX1",
1477 "POSTBOX2",
1478 "2",
1479 "1 1",
1480 "63.546 1.008",
1481 "Cu",
1482 "Coordinates of Component 1",
1483 "0.0 0.0 0.0 1.0 0",
1484 "H",
1485 "Coordinates of Component 2",
1486 "1.0 2.0 3.0 0.0 1",
1487 "",
1488 "Cu",
1489 "Velocities of Component 1",
1490 "0.1 0.2 0.3 1.0 0",
1491 "H",
1492 "Velocities of Component 2",
1493 "0.4 0.5 0.6 0.0 1",
1494 ];
1495 let mut line_it = lines.iter().copied().peekable();
1496 let mut frame =
1498 parse_single_frame(&mut line_it).expect("coordinate parsing should succeed");
1499 assert!(!frame.has_velocities());
1500
1501 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
1503 .expect("velocity parsing should succeed");
1504 assert!(has_vel);
1505 assert_eq!(frame.atom_data[0].velocity, Some([0.1, 0.2, 0.3]));
1506 assert_eq!(frame.atom_data[1].velocity, Some([0.4, 0.5, 0.6]));
1507 }
1508
1509 #[test]
1510 fn test_validate_true_accepts_matching_section_identity() {
1511 let text = r#"
1512PREBOX1
1513{"con_spec_version":2,"sections":["velocities"],"validate":true}
151410.0 20.0 30.0
151590.0 90.0 90.0
1516POSTBOX1
1517POSTBOX2
15182
15191 1
152063.546 1.008
1521Cu
1522Coordinates of Component 1
15230.0 0.0 0.0 5 0
1524H
1525Coordinates of Component 2
15261.0 2.0 3.0 0 1
1527
1528Cu
1529Velocities of Component 1
15300.1 0.2 0.3 5 0
1531H
1532Velocities of Component 2
15330.4 0.5 0.6 0 1
1534"#;
1535 let mut iter = ConFrameIterator::new(text.trim());
1536 let frame = iter.next().unwrap().unwrap();
1537
1538 assert!(frame.has_velocities());
1539 assert_eq!(
1540 frame
1541 .header
1542 .metadata
1543 .get("validate")
1544 .and_then(|v| v.as_bool()),
1545 Some(true)
1546 );
1547 }
1548
1549 #[test]
1550 fn test_validate_true_rejects_section_atom_id_mismatch() {
1551 let text = r#"
1552PREBOX1
1553{"con_spec_version":2,"sections":["velocities"],"validate":true}
155410.0 20.0 30.0
155590.0 90.0 90.0
1556POSTBOX1
1557POSTBOX2
15581
15591
156063.546
1561Cu
1562Coordinates of Component 1
15630.0 0.0 0.0 5 0
1564
1565Cu
1566Velocities of Component 1
15670.1 0.2 0.3 5 99
1568"#;
1569 let mut iter = ConFrameIterator::new(text.trim());
1570 let err = iter.next().unwrap().unwrap_err();
1571
1572 assert!(matches!(err, ParseError::ValidationError(_)));
1573 assert!(err.to_string().contains("atom_id mismatch"));
1574 }
1575
1576 #[test]
1577 fn test_validate_true_rejects_section_symbol_mismatch() {
1578 let text = r#"
1579PREBOX1
1580{"con_spec_version":2,"sections":["forces"],"validate":true}
158110.0 20.0 30.0
158290.0 90.0 90.0
1583POSTBOX1
1584POSTBOX2
15851
15861
158763.546
1588Cu
1589Coordinates of Component 1
15900.0 0.0 0.0 5 0
1591
1592H
1593Forces of Component 1
15940.1 0.2 0.3 5 0
1595"#;
1596 let mut iter = ConFrameIterator::new(text.trim());
1597 let err = iter.next().unwrap().unwrap_err();
1598
1599 assert!(matches!(err, ParseError::ValidationError(_)));
1600 assert!(err.to_string().contains("symbol mismatch"));
1601 }
1602
1603 #[test]
1604 fn test_validate_absent_allows_legacy_duplicate_identity_mismatch() {
1605 let text = r#"
1606PREBOX1
1607{"con_spec_version":2,"sections":["velocities"]}
160810.0 20.0 30.0
160990.0 90.0 90.0
1610POSTBOX1
1611POSTBOX2
16121
16131
161463.546
1615Cu
1616Coordinates of Component 1
16170.0 0.0 0.0 5 0
1618
1619Cu
1620Velocities of Component 1
16210.1 0.2 0.3 0 99
1622"#;
1623 let mut iter = ConFrameIterator::new(text.trim());
1624 let frame = iter.next().unwrap().unwrap();
1625
1626 assert!(frame.has_velocities());
1627 assert_eq!(frame.atom_data[0].atom_id, 0);
1628 assert_eq!(frame.atom_data[0].fixed, [true, false, true]);
1629 }
1630
1631 #[test]
1632 fn test_validate_must_be_boolean_when_present() {
1633 let lines = vec![
1634 "PREBOX1",
1635 "{\"con_spec_version\":2,\"validate\":\"yes\",\"sections\":[]}",
1636 "10.0 20.0 30.0",
1637 "90.0 90.0 90.0",
1638 "POSTBOX1",
1639 "POSTBOX2",
1640 "1",
1641 "1",
1642 "12.011",
1643 ];
1644 let mut line_it = lines.iter().copied();
1645 let err = parse_frame_header(&mut line_it).unwrap_err();
1646
1647 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1648 assert!(err.to_string().contains("validate"));
1649 }
1650
1651 #[test]
1652 fn test_validate_true_requires_sections_key() {
1653 let lines = vec![
1654 "PREBOX1",
1655 "{\"con_spec_version\":2,\"validate\":true}",
1656 "10.0 20.0 30.0",
1657 "90.0 90.0 90.0",
1658 "POSTBOX1",
1659 "POSTBOX2",
1660 "1",
1661 "1",
1662 "12.011",
1663 ];
1664 let mut line_it = lines.iter().copied();
1665 let err = parse_frame_header(&mut line_it).unwrap_err();
1666
1667 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1668 assert!(err.to_string().contains("sections"));
1669 }
1670
1671 #[test]
1672 fn test_sections_must_be_string_array_when_present() {
1673 let lines = vec![
1674 "PREBOX1",
1675 "{\"con_spec_version\":2,\"sections\":[\"velocities\",7]}",
1676 "10.0 20.0 30.0",
1677 "90.0 90.0 90.0",
1678 "POSTBOX1",
1679 "POSTBOX2",
1680 "1",
1681 "1",
1682 "12.011",
1683 ];
1684 let mut line_it = lines.iter().copied();
1685 let err = parse_frame_header(&mut line_it).unwrap_err();
1686
1687 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1688 assert!(err.to_string().contains("sections"));
1689 }
1690
1691 #[test]
1692 fn test_validate_true_rejects_non_integer_coordinate_identity_columns() {
1693 let text = r#"
1694PREBOX1
1695{"con_spec_version":2,"sections":[],"validate":true}
169610.0 20.0 30.0
169790.0 90.0 90.0
1698POSTBOX1
1699POSTBOX2
17001
17011
170263.546
1703Cu
1704Coordinates of Component 1
17050.0 0.0 0.0 5.0 0
1706"#;
1707 let mut iter = ConFrameIterator::new(text.trim());
1708 let err = iter.next().unwrap().unwrap_err();
1709
1710 assert!(matches!(err, ParseError::ValidationError(_)));
1711 assert!(err.to_string().contains("fixed_flag"));
1712 }
1713
1714 #[test]
1715 fn test_validate_true_rejects_non_exact_coordinate_label() {
1716 let text = r#"
1717PREBOX1
1718{"con_spec_version":2,"sections":[],"validate":true}
171910.0 20.0 30.0
172090.0 90.0 90.0
1721POSTBOX1
1722POSTBOX2
17231
17241
172563.546
1726Cu
1727Coordinates Component 1
17280.0 0.0 0.0 5 0
1729"#;
1730 let mut iter = ConFrameIterator::new(text.trim());
1731 let err = iter.next().unwrap().unwrap_err();
1732
1733 assert!(matches!(err, ParseError::ValidationError(_)));
1734 assert!(err.to_string().contains("Coordinates of Component 1"));
1735 }
1736
1737 #[test]
1738 fn test_validate_true_rejects_unknown_component_symbol() {
1739 let text = r#"
1740PREBOX1
1741{"con_spec_version":2,"sections":[],"validate":true}
174210.0 20.0 30.0
174390.0 90.0 90.0
1744POSTBOX1
1745POSTBOX2
17461
17471
174863.546
1749Qq
1750Coordinates of Component 1
17510.0 0.0 0.0 0 0
1752"#;
1753 let mut iter = ConFrameIterator::new(text.trim());
1754 let err = iter.next().unwrap().unwrap_err();
1755
1756 assert!(matches!(err, ParseError::ValidationError(_)));
1757 assert!(err.to_string().contains("symbol"));
1758 }
1759
1760 #[test]
1761 fn test_declared_section_must_be_present() {
1762 let text = r#"
1763PREBOX1
1764{"con_spec_version":2,"sections":["velocities"]}
176510.0 20.0 30.0
176690.0 90.0 90.0
1767POSTBOX1
1768POSTBOX2
17691
17701
177163.546
1772Cu
1773Coordinates of Component 1
17740.0 0.0 0.0 0 0
1775"#;
1776 let mut iter = ConFrameIterator::new(text.trim());
1777 let err = iter.next().unwrap().unwrap_err();
1778
1779 assert!(matches!(err, ParseError::IncompleteVelocitySection));
1780 }
1781
1782 #[test]
1783 fn test_non_finite_cell_geometry_rejected_in_strict_mode() {
1784 let lines = vec![
1785 "PREBOX1",
1786 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
1787 "10.0 NaN 30.0",
1788 "90.0 90.0 90.0",
1789 "POSTBOX1",
1790 "POSTBOX2",
1791 "1",
1792 "1",
1793 "12.011",
1794 ];
1795 let mut line_it = lines.iter().copied();
1796 let err = parse_frame_header(&mut line_it).unwrap_err();
1797
1798 assert!(matches!(err, ParseError::ValidationError(_)));
1799 assert!(err.to_string().contains("cell"));
1800 }
1801
1802 #[test]
1803 fn test_validate_true_rejects_non_physical_cell_geometry() {
1804 let lines = vec![
1805 "PREBOX1",
1806 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true}",
1807 "0.0 20.0 30.0",
1808 "90.0 180.0 90.0",
1809 "POSTBOX1",
1810 "POSTBOX2",
1811 "1",
1812 "1",
1813 "12.011",
1814 ];
1815 let mut line_it = lines.iter().copied();
1816 let err = parse_frame_header(&mut line_it).unwrap_err();
1817
1818 assert!(matches!(err, ParseError::ValidationError(_)));
1819 assert!(err.to_string().contains("cell"));
1820 }
1821
1822 #[test]
1823 fn test_validate_true_rejects_reserved_metadata_type_mismatch() {
1824 let lines = vec![
1825 "PREBOX1",
1826 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"energy\":\"low\"}",
1827 "10.0 20.0 30.0",
1828 "90.0 90.0 90.0",
1829 "POSTBOX1",
1830 "POSTBOX2",
1831 "1",
1832 "1",
1833 "12.011",
1834 ];
1835 let mut line_it = lines.iter().copied();
1836 let err = parse_frame_header(&mut line_it).unwrap_err();
1837
1838 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1839 assert!(err.to_string().contains("energy"));
1840 }
1841
1842 #[test]
1843 fn test_validate_true_rejects_malformed_bonds() {
1844 let lines = vec![
1845 "PREBOX1",
1846 "{\"con_spec_version\":2,\"sections\":[],\"validate\":true,\"bonds\":[[0]]}",
1847 "10.0 20.0 30.0",
1848 "90.0 90.0 90.0",
1849 "POSTBOX1",
1850 "POSTBOX2",
1851 "1",
1852 "1",
1853 "12.011",
1854 ];
1855 let mut line_it = lines.iter().copied();
1856 let err = parse_frame_header(&mut line_it).unwrap_err();
1857 assert!(matches!(err, ParseError::InvalidMetadataJson(_)));
1858 assert!(err.to_string().contains("bonds"));
1859 }
1860
1861 #[test]
1862 fn test_bonds_metadata_round_trip_in_header() {
1863 use crate::types::{meta, Bond};
1864 let lines = vec![
1865 "PREBOX1",
1866 "{\"con_spec_version\":2,\"bonds\":[[0,1],{\"i\":0,\"j\":2,\"order\":1}]}",
1867 "10.0 20.0 30.0",
1868 "90.0 90.0 90.0",
1869 "POSTBOX1",
1870 "POSTBOX2",
1871 "1",
1872 "1",
1873 "12.011",
1874 ];
1875 let mut line_it = lines.iter().copied();
1876 let header = parse_frame_header(&mut line_it).expect("header");
1877 let bonds = header.bonds();
1878 assert_eq!(bonds.len(), 2);
1879 assert_eq!(bonds[0], Bond::new(0, 1));
1880 assert_eq!(bonds[1].i, 0);
1881 assert_eq!(bonds[1].j, 2);
1882 assert_eq!(bonds[1].order, Some(1));
1883 assert!(header.metadata.contains_key(meta::BONDS));
1884 }
1885
1886 #[test]
1887 fn test_parse_velocity_section_absent() {
1888 let lines = vec![
1889 "PREBOX1",
1890 "{\"con_spec_version\":2}",
1891 "10.0 20.0 30.0",
1892 "90.0 90.0 90.0",
1893 "POSTBOX1",
1894 "POSTBOX2",
1895 "1",
1896 "1",
1897 "12.011",
1898 "C",
1899 "Coordinates of Component 1",
1900 "0.0 0.0 0.0 0.0 1",
1901 ];
1902 let mut line_it = lines.iter().copied().peekable();
1903 let mut frame = parse_single_frame(&mut line_it).expect("parse should succeed");
1904 let has_vel = parse_velocity_section(&mut line_it, &frame.header, &mut frame.atom_data)
1905 .expect("should succeed with no velocities");
1906 assert!(!has_vel);
1907 assert_eq!(frame.atom_data[0].velocity, None);
1908 }
1909
1910 #[test]
1911 fn test_parse_line_of_range_f64_exact() {
1912 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0 42", 4, 5, &[0.0; 5]).unwrap();
1913 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 42.0]);
1914 }
1915
1916 #[test]
1917 fn test_parse_line_of_range_f64_stack_matches_vec_api() {
1918 let defaults = [0.0, 0.0, 0.0, 0.0, 7.0];
1919 let line = "1.0 2.0 3.0 0.0";
1920 let mut buf = [0.0f64; 5];
1921 parse_line_of_range_f64_stack(line, 4, 5, &defaults, &mut buf).unwrap();
1922 let via_vec = parse_line_of_range_f64(line, 4, 5, &defaults).unwrap();
1923 assert_eq!(&buf[..5], via_vec.as_slice());
1924 assert_eq!(buf[4], 7.0);
1925 }
1926
1927 #[test]
1928 fn test_parse_line_of_range_f64_padded() {
1929 let defaults = [0.0, 0.0, 0.0, 0.0, 99.0];
1930 let vals = parse_line_of_range_f64("1.0 2.0 3.0 0.0", 4, 5, &defaults).unwrap();
1931 assert_eq!(vals, vec![1.0, 2.0, 3.0, 0.0, 99.0]);
1932 }
1933
1934 #[test]
1935 fn test_parse_line_of_range_f64_too_few() {
1936 let result = parse_line_of_range_f64("1.0 2.0 3.0", 4, 5, &[0.0; 5]);
1937 assert!(result.is_err());
1938 }
1939
1940 #[test]
1941 fn test_parse_line_of_range_f64_too_many() {
1942 let result = parse_line_of_range_f64("1.0 2.0 3.0 0.0 5.0 6.0", 4, 5, &[0.0; 5]);
1943 assert!(result.is_err());
1944 }
1945
1946 #[test]
1947 fn test_parse_all_four_column_lines() {
1948 let lines = vec![
1950 "PREBOX1",
1951 "{\"con_spec_version\":2}",
1952 "10.0 10.0 10.0",
1953 "90.0 90.0 90.0",
1954 "POSTBOX1",
1955 "POSTBOX2",
1956 "1",
1957 "3",
1958 "12.011",
1959 "C",
1960 "Coordinates of Component 1",
1961 "0.0 0.0 0.0 0",
1962 "1.0 0.0 0.0 0",
1963 "2.0 0.0 0.0 1",
1964 ];
1965 let mut line_it = lines.iter().copied();
1966 let frame = parse_single_frame(&mut line_it).unwrap();
1967 assert_eq!(frame.atom_data[0].atom_id, 0);
1968 assert_eq!(frame.atom_data[1].atom_id, 1);
1969 assert_eq!(frame.atom_data[2].atom_id, 2);
1970 assert!(frame.atom_data[2].is_fixed());
1971 }
1972}