1use crate::types::{ConFrame, SECTION_ENERGIES, SECTION_FORCES, SECTION_VELOCITIES};
20use std::collections::BTreeMap;
21
22pub const SECTIONS_MASK_FORCES: u8 = 1 << 0;
24pub const SECTIONS_MASK_VELOCITIES: u8 = 1 << 1;
26pub const SECTIONS_MASK_ENERGIES: u8 = 1 << 2;
28
29pub fn composition_formula(counts: &[(String, u32)]) -> String {
32 let mut parts: Vec<(String, u32)> = counts
33 .iter()
34 .filter(|(s, c)| !s.is_empty() && *c > 0)
35 .cloned()
36 .collect();
37 parts.sort_by(|a, b| a.0.cmp(&b.0));
38 parts
39 .into_iter()
40 .map(|(s, c)| format!("{s}:{c}"))
41 .collect::<Vec<_>>()
42 .join("|")
43}
44
45pub fn species_counts_from_symbols(
47 symbols: impl Iterator<Item = impl AsRef<str>>,
48) -> Vec<(String, u32)> {
49 let mut m = BTreeMap::new();
50 for s in symbols {
51 let s = s.as_ref();
52 if s.is_empty() {
53 continue;
54 }
55 *m.entry(s.to_string()).or_insert(0u32) += 1;
56 }
57 m.into_iter().collect()
58}
59
60pub fn frame_species_counts(frame: &ConFrame) -> Vec<(String, u32)> {
62 species_counts_from_symbols(frame.atom_data.iter().map(|a| a.symbol.as_ref()))
63}
64
65pub fn frame_composition_formula(frame: &ConFrame) -> String {
67 composition_formula(&frame_species_counts(frame))
68}
69
70pub fn finite_energy(frame: &ConFrame) -> Option<f64> {
72 frame.header.energy().filter(|e| e.is_finite()).or_else(|| {
73 frame
74 .header
75 .metadata
76 .get("energy")
77 .and_then(|v| v.as_f64())
78 .filter(|e| e.is_finite())
79 })
80}
81
82pub fn frame_fmax(frame: &ConFrame) -> Option<f64> {
84 let mut m = None;
85 for a in &frame.atom_data {
86 if let Some(f) = a.force {
87 let mag = (f[0] * f[0] + f[1] * f[1] + f[2] * f[2]).sqrt();
88 if mag.is_finite() {
89 m = Some(m.map_or(mag, |cur: f64| cur.max(mag)));
90 }
91 }
92 }
93 m
94}
95
96pub fn frame_total_mass(frame: &ConFrame) -> Option<f64> {
98 let h = &frame.header;
99 if h.masses_per_type.is_empty() || h.natms_per_type.is_empty() {
100 return None;
101 }
102 let n = h.masses_per_type.len().min(h.natms_per_type.len());
103 let mut m = 0.0f64;
104 for i in 0..n {
105 let mi = h.masses_per_type[i];
106 let ni = h.natms_per_type[i] as f64;
107 if !mi.is_finite() || !ni.is_finite() {
108 return None;
109 }
110 m += mi * ni;
111 }
112 m.is_finite().then_some(m)
113}
114
115fn scalar_triple(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
116 a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0])
117 + a[2] * (b[0] * c[1] - b[1] * c[0])
118}
119
120pub fn frame_cell_volume(frame: &ConFrame) -> Option<f64> {
122 if let Some(lv) = frame.header.lattice_vectors() {
123 let det = scalar_triple(lv[0], lv[1], lv[2]).abs();
124 return det.is_finite().then_some(det);
125 }
126 let [a, b, c] = frame.header.boxl;
127 let [alpha, beta, gamma] = frame.header.angles;
128 if ![a, b, c, alpha, beta, gamma]
129 .iter()
130 .all(|x| x.is_finite() && *x > 0.0)
131 {
132 return None;
133 }
134 let ar = alpha.to_radians();
135 let br = beta.to_radians();
136 let gr = gamma.to_radians();
137 let ca = ar.cos();
138 let cb = br.cos();
139 let cg = gr.cos();
140 let sg = gr.sin();
141 if sg.abs() < 1e-15 {
142 return None;
143 }
144 let t = 1.0 - ca * ca - cb * cb - cg * cg + 2.0 * ca * cb * cg;
145 if t <= 0.0 {
146 return None;
147 }
148 let v = a * b * c * t.sqrt();
149 v.is_finite().then_some(v)
150}
151
152pub fn frame_has_forces(frame: &ConFrame) -> bool {
154 frame
155 .header
156 .sections
157 .iter()
158 .any(|s| s.eq_ignore_ascii_case(SECTION_FORCES))
159 || frame.atom_data.iter().any(|a| a.force.is_some())
160 || frame.has_forces()
161}
162
163pub fn frame_has_velocities(frame: &ConFrame) -> bool {
165 frame
166 .header
167 .sections
168 .iter()
169 .any(|s| s.eq_ignore_ascii_case(SECTION_VELOCITIES))
170 || frame.atom_data.iter().any(|a| a.velocity.is_some())
171 || frame.has_velocities()
172}
173
174pub fn frame_has_energies(frame: &ConFrame) -> bool {
176 frame
177 .header
178 .sections
179 .iter()
180 .any(|s| s.eq_ignore_ascii_case(SECTION_ENERGIES))
181 || frame.has_energies()
182 || finite_energy(frame).is_some()
183}
184
185pub fn sections_present_mask(frame: &ConFrame) -> u8 {
187 let mut m = 0u8;
188 if frame_has_forces(frame) {
189 m |= SECTIONS_MASK_FORCES;
190 }
191 if frame_has_velocities(frame) {
192 m |= SECTIONS_MASK_VELOCITIES;
193 }
194 if frame_has_energies(frame) {
195 m |= SECTIONS_MASK_ENERGIES;
196 }
197 m
198}
199
200fn meta_f64(frame: &ConFrame, key: &str) -> Option<f64> {
201 let v = frame.header.metadata.get(key)?;
202 if let Some(f) = v.as_f64() {
203 return f.is_finite().then_some(f);
204 }
205 if let Some(i) = v.as_i64() {
206 let f = i as f64;
207 return f.is_finite().then_some(f);
208 }
209 if let Some(u) = v.as_u64() {
210 let f = u as f64;
211 return f.is_finite().then_some(f);
212 }
213 None
214}
215
216#[derive(Clone, Debug, PartialEq)]
218pub struct FrameIndexProjection {
219 pub n_atoms: u32,
220 pub symbols: Vec<String>,
222 pub species_counts: Vec<(String, u32)>,
224 pub formula: String,
226 pub energy: Option<f64>,
228 pub fmax: Option<f64>,
230 pub total_mass: Option<f64>,
231 pub cell_volume: Option<f64>,
232 pub pbc: Option<[bool; 3]>,
234 pub sections_mask: u8,
235 pub has_forces: bool,
236 pub has_velocities: bool,
237 pub has_energy: bool,
238 pub time: Option<f64>,
239 pub timestep: Option<f64>,
240 pub frame_index: Option<f64>,
241 pub neb_bead: Option<f64>,
242 pub neb_band: Option<f64>,
243 pub charge: Option<f64>,
244 pub magmom: Option<f64>,
245}
246
247impl FrameIndexProjection {
248 pub fn from_frame(frame: &ConFrame) -> Self {
250 let species_counts = frame_species_counts(frame);
251 let symbols: Vec<String> = species_counts.iter().map(|(s, _)| s.clone()).collect();
252 let formula = composition_formula(&species_counts);
253 let energy = finite_energy(frame);
254 let has_forces = frame_has_forces(frame);
255 let has_velocities = frame_has_velocities(frame);
256 let has_energy = frame_has_energies(frame);
257 let sections_mask = {
258 let mut m = 0u8;
259 if has_forces {
260 m |= SECTIONS_MASK_FORCES;
261 }
262 if has_velocities {
263 m |= SECTIONS_MASK_VELOCITIES;
264 }
265 if has_energy {
266 m |= SECTIONS_MASK_ENERGIES;
267 }
268 m
269 };
270 let time = frame
271 .header
272 .time()
273 .filter(|t| t.is_finite())
274 .or_else(|| meta_f64(frame, "time"));
275 let timestep = frame
276 .header
277 .timestep()
278 .filter(|t| t.is_finite())
279 .or_else(|| meta_f64(frame, "timestep"));
280 let frame_index = frame
281 .header
282 .frame_index()
283 .map(|i| i as f64)
284 .filter(|t| t.is_finite())
285 .or_else(|| meta_f64(frame, "frame_index"));
286 let neb_bead = frame
287 .header
288 .neb_bead()
289 .map(|i| i as f64)
290 .filter(|t| t.is_finite())
291 .or_else(|| meta_f64(frame, "neb_bead"));
292 let neb_band = meta_f64(frame, "neb_band").or_else(|| {
293 frame
294 .header
295 .metadata
296 .get("neb_band")
297 .and_then(|v| v.as_u64())
298 .map(|u| u as f64)
299 .filter(|t| t.is_finite())
300 });
301 Self {
302 n_atoms: frame.atom_data.len() as u32,
303 symbols,
304 species_counts,
305 formula,
306 energy,
307 fmax: frame_fmax(frame),
308 total_mass: frame_total_mass(frame),
309 cell_volume: frame_cell_volume(frame),
310 pbc: frame.header.pbc(),
311 sections_mask,
312 has_forces,
313 has_velocities,
314 has_energy,
315 time,
316 timestep,
317 frame_index,
318 neb_bead,
319 neb_band,
320 charge: meta_f64(frame, "charge"),
321 magmom: meta_f64(frame, "magmom"),
322 }
323 }
324}
325
326#[derive(Clone, Copy, Debug, PartialEq, Eq)]
328pub struct FrameByteSpan {
329 pub start: usize,
330 pub end: usize,
331}
332
333impl FrameByteSpan {
334 pub fn len(self) -> usize {
335 self.end.saturating_sub(self.start)
336 }
337
338 #[must_use]
339 pub fn is_empty(self) -> bool {
340 self.len() == 0
341 }
342
343 pub fn slice(self, file_contents: &str) -> Option<&str> {
344 file_contents.get(self.start..self.end)
345 }
346}
347
348pub fn frame_byte_spans_skip(
355 file_contents: &str,
356) -> Result<Vec<FrameByteSpan>, crate::error::ParseError> {
357 let starts = crate::iterators::frame_start_offsets(file_contents);
358 let n = file_contents.len();
359 Ok(starts
360 .iter()
361 .enumerate()
362 .map(|(i, &start)| FrameByteSpan {
363 start,
364 end: starts.get(i + 1).copied().unwrap_or(n),
365 })
366 .collect())
367}
368
369pub fn parse_frame_at(
371 file_contents: &str,
372 index: usize,
373) -> Result<crate::types::ConFrame, crate::error::ParseError> {
374 let spans = frame_byte_spans_skip(file_contents)?;
375 let span = spans.get(index).ok_or(crate::error::ParseError::IndexOutOfBounds {
376 index,
377 len: spans.len(),
378 })?;
379 let slice = span
380 .slice(file_contents)
381 .ok_or(crate::error::ParseError::IncompleteFrame)?;
382 match crate::iterators::ConFrameIterator::new(slice).next() {
383 Some(r) => r,
384 None => Err(crate::error::ParseError::IncompleteFrame),
385 }
386}
387
388pub fn frame_byte_spans(
389 file_contents: &str,
390) -> Result<Vec<FrameByteSpan>, crate::error::ParseError> {
391 let mut it = crate::iterators::ConFrameIterator::new(file_contents);
392 let mut out = Vec::new();
393 while let Some(item) = it.next_with_raw_span(file_contents) {
394 let (_frame, span) = item?;
395 let start = span.as_ptr() as usize - file_contents.as_ptr() as usize;
396 let end = start + span.len();
397 out.push(FrameByteSpan { start, end });
398 }
399 Ok(out)
400}
401
402pub fn symbol_histogram(
404 file_contents: &str,
405) -> Result<BTreeMap<String, u32>, crate::error::ParseError> {
406 let mut hist = BTreeMap::new();
407 for item in crate::iterators::ConFrameIterator::new(file_contents) {
408 let frame = item?;
409 for a in &frame.atom_data {
410 if a.symbol.is_empty() {
411 continue;
412 }
413 *hist.entry(a.symbol.to_string()).or_insert(0u32) += 1;
414 }
415 }
416 Ok(hist)
417}
418
419pub fn spans_cover_buffer(file_contents: &str) -> Result<bool, crate::error::ParseError> {
422 let spans = frame_byte_spans(file_contents)?;
423 if spans.is_empty() {
424 return Ok(file_contents.is_empty());
425 }
426 if spans[0].start != 0 {
427 return Ok(false);
428 }
429 for w in spans.windows(2) {
430 if w[0].end != w[1].start {
431 return Ok(false);
432 }
433 }
434 Ok(spans.last().map(|s| s.end) == Some(file_contents.len())
435 || spans.last().map(|s| s.end) == Some(file_contents.trim_end().len()))
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use crate::iterators::ConFrameIterator;
442 use crate::writer::ConFrameWriter;
443 use std::io::Cursor;
444
445 fn fixture_text() -> String {
446 let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
447 .join("resources/test/tiny_cuh2.con");
448 std::fs::read_to_string(p).unwrap()
449 }
450
451 #[test]
452 fn formula_canonical_order() {
453 let f1 = composition_formula(&[("H".into(), 2), ("Cu".into(), 2)]);
454 let f2 = composition_formula(&[("Cu".into(), 2), ("H".into(), 2)]);
455 assert_eq!(f1, "Cu:2|H:2");
456 assert_eq!(f1, f2);
457 }
458
459 #[test]
460 fn projection_from_fixture() {
461 let text = fixture_text();
462 let fr = ConFrameIterator::new(&text).next().unwrap().unwrap();
463 let p = FrameIndexProjection::from_frame(&fr);
464 assert!(p.n_atoms >= 1);
465 assert!(p.formula.contains(':'));
466 assert!(p.total_mass.is_some_and(|m| m > 0.0));
467 assert!(p.cell_volume.is_some_and(|v| v > 0.0));
468 assert_eq!(
469 p.sections_mask & SECTIONS_MASK_FORCES == SECTIONS_MASK_FORCES,
470 p.has_forces
471 );
472 }
473
474 #[test]
475 fn finite_energy_rejects_nan() {
476 let text = fixture_text();
477 let mut fr = ConFrameIterator::new(&text).next().unwrap().unwrap();
478 fr.header
479 .metadata
480 .insert("energy".into(), serde_json::json!(f64::NAN));
481 assert!(finite_energy(&fr).is_none() || finite_energy(&fr).unwrap().is_finite());
483 let mut fr2 = fr.clone();
485 if let Some(e) = fr2.header.energy() {
486 let _ = e;
487 }
488 fr2.header
490 .metadata
491 .insert("energy".into(), serde_json::json!(f64::INFINITY));
492 let e = fr2
493 .header
494 .energy()
495 .or_else(|| fr2.header.metadata.get("energy").and_then(|v| v.as_f64()));
496 if let Some(x) = e {
497 if !x.is_finite() {
498 assert!(finite_energy(&fr2).is_none());
499 }
500 }
501 }
502
503 #[test]
504 fn skip_spans_seek_second_frame() {
505 let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
506 .join("resources/test/tiny_multi_cuh2.con");
507 let text = std::fs::read_to_string(p).unwrap();
508 let skip = frame_byte_spans_skip(&text).unwrap();
509 assert_eq!(skip.len(), 2);
510 let a = parse_frame_at(&text, 0).unwrap();
511 let b = parse_frame_at(&text, 1).unwrap();
512 let mut it = ConFrameIterator::new(&text);
513 let fa = it.next().unwrap().unwrap();
514 let fb = it.next().unwrap().unwrap();
515 assert_eq!(a.atom_data[0].x, fa.atom_data[0].x);
516 assert_eq!(b.atom_data[0].x, fb.atom_data[0].x);
517 assert!(parse_frame_at(&text, 9).is_err());
518 }
519
520 #[test]
521 fn span_preserving_concat() {
522 let text = fixture_text();
523 let spans = frame_byte_spans(&text).unwrap();
524 assert!(!spans.is_empty());
525 let mut acc = String::new();
526 for s in &spans {
527 acc.push_str(s.slice(&text).unwrap());
528 }
529 let n_orig = ConFrameIterator::new(&text).count();
531 let n_acc = ConFrameIterator::new(&acc).count();
532 assert_eq!(n_orig, n_acc);
533 assert!(spans_cover_buffer(&text).unwrap());
534 }
535
536 #[test]
537 fn symbol_histogram_nonempty() {
538 let h = symbol_histogram(&fixture_text()).unwrap();
539 assert!(!h.is_empty());
540 }
541
542 #[test]
544 fn projection_matches_iterator_frame_fields() {
545 let text = fixture_text();
546 let fr = ConFrameIterator::new(&text).next().unwrap().unwrap();
547 let p = FrameIndexProjection::from_frame(&fr);
548 assert_eq!(p.n_atoms as usize, fr.atom_data.len());
549 assert_eq!(p.n_atoms, fr.positions.nrows() as u32);
550 let expect_formula = frame_composition_formula(&fr);
551 assert_eq!(p.formula, expect_formula);
552 assert!(!p.formula.is_empty());
553 assert_eq!(p.species_counts, frame_species_counts(&fr));
554 assert!(p.total_mass.is_some_and(|m| m > 0.0));
555 assert!(p.cell_volume.is_some_and(|v| v > 0.0));
556 let expect_forces = frame_has_forces(&fr);
557 let expect_vel = frame_has_velocities(&fr);
558 assert_eq!(p.has_forces, expect_forces);
559 assert_eq!(p.has_velocities, expect_vel);
560 let mut expect_mask = 0u8;
561 if expect_forces {
562 expect_mask |= SECTIONS_MASK_FORCES;
563 }
564 if expect_vel {
565 expect_mask |= SECTIONS_MASK_VELOCITIES;
566 }
567 if frame_has_energies(&fr) {
568 expect_mask |= SECTIONS_MASK_ENERGIES;
569 }
570 assert_eq!(p.sections_mask, expect_mask);
571 assert_eq!(p.sections_mask, sections_present_mask(&fr));
572 }
573
574 #[test]
575 fn canonical_writer_deterministic() {
576 let text = fixture_text();
577 let fr = ConFrameIterator::new(&text).next().unwrap().unwrap();
578 let mut a = Cursor::new(Vec::new());
579 let mut b = Cursor::new(Vec::new());
580 {
581 let mut wa = ConFrameWriter::new(&mut a).canonical(true);
582 let mut wb = ConFrameWriter::new(&mut b).canonical(true);
583 wa.write_frame(&fr).unwrap();
584 wb.write_frame(&fr).unwrap();
585 }
586 assert_eq!(a.into_inner(), b.into_inner());
587 }
588}