ferritin_core/model/conformation.rs
1//! Per-model coordinate data that varies across trajectory frames.
2//!
3//! [`AtomicConformation`] holds everything that differs between frames: Cartesian
4//! coordinates, occupancy, B-factors, and per-atom confidence scores. Topology
5//! (connectivity, sequence, etc.) lives in [`super::hierarchy::AtomicHierarchy`].
6
7/// Per-model coordinate data — varies across trajectory frames.
8///
9/// Coordinates are stored as struct-of-arrays (SoA): `x[i]`, `y[i]`, `z[i]`
10/// for atom `i`. This layout is cache-friendly for common operations that
11/// process one coordinate component at a time (e.g., computing pairwise
12/// distances, centroid calculations).
13///
14/// # Host-side only
15///
16/// Coordinates are plain `Vec<f32>` — no GPU tensors or device types.
17/// ferritin-core is intentionally framework-agnostic.
18#[derive(Clone, Debug)]
19pub struct AtomicConformation {
20 /// X coordinates (Å), one per atom.
21 pub x: Vec<f32>,
22 /// Y coordinates (Å), one per atom.
23 pub y: Vec<f32>,
24 /// Z coordinates (Å), one per atom.
25 pub z: Vec<f32>,
26 /// Occupancy for each atom (0.0–1.0), or `None` if not recorded.
27 pub occupancy: Option<Vec<f32>>,
28 /// Isotropic B-factor (temperature factor) for each atom, or `None`.
29 /// For predicted structures use [`confidence`] instead to avoid semantic ambiguity.
30 pub b_iso: Option<Vec<f32>>,
31 /// Per-atom confidence score (e.g. pLDDT from AlphaFold / ESM3), or `None`.
32 ///
33 /// Kept separate from `b_iso` so callers can distinguish experimental
34 /// B-factors from model confidence without inspecting metadata.
35 pub confidence: Option<Vec<f32>>,
36}
37
38impl AtomicConformation {
39 /// Number of atoms.
40 pub fn n_atoms(&self) -> usize {
41 self.x.len()
42 }
43
44 /// Get `[x, y, z]` for atom `i`.
45 ///
46 /// # Panics
47 /// Panics if `i >= n_atoms()`.
48 pub fn coord(&self, i: usize) -> [f32; 3] {
49 [self.x[i], self.y[i], self.z[i]]
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 #[test]
58 fn test_conformation_coord() {
59 let conf = AtomicConformation {
60 x: vec![1.0, 4.0, 7.0],
61 y: vec![2.0, 5.0, 8.0],
62 z: vec![3.0, 6.0, 9.0],
63 occupancy: Some(vec![1.0, 0.5, 1.0]),
64 b_iso: None,
65 confidence: Some(vec![90.0, 85.0, 92.0]),
66 };
67
68 assert_eq!(conf.n_atoms(), 3);
69 assert_eq!(conf.coord(0), [1.0, 2.0, 3.0]);
70 assert_eq!(conf.coord(1), [4.0, 5.0, 6.0]);
71 assert_eq!(conf.coord(2), [7.0, 8.0, 9.0]);
72 }
73
74 #[test]
75 fn test_conformation_optional_fields() {
76 let conf = AtomicConformation {
77 x: vec![0.0],
78 y: vec![0.0],
79 z: vec![0.0],
80 occupancy: None,
81 b_iso: None,
82 confidence: None,
83 };
84 assert_eq!(conf.n_atoms(), 1);
85 assert_eq!(conf.coord(0), [0.0, 0.0, 0.0]);
86 assert!(conf.occupancy.is_none());
87 assert!(conf.b_iso.is_none());
88 assert!(conf.confidence.is_none());
89 }
90}