Skip to main content

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}