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