Skip to main content

ferritin_core/model/
tables.rs

1//! Table structs for per-atom, per-residue, and per-chain data.
2//!
3//! These tables hold topology data that is constant across trajectory frames.
4//! All tables use struct-of-arrays (SoA) layout for cache-friendly iteration.
5
6/// Residue classification: polymer (protein/nucleic acid) or non-polymer (ligand/solvent).
7#[derive(Clone, Debug, PartialEq)]
8pub enum ResidueGroup {
9    /// Polymer residue: part of a protein, DNA, or RNA chain.
10    Polymer,
11    /// Non-polymer residue: ligand, solvent, ion, etc.
12    NonPolymer,
13}
14
15/// Per-atom topology data (not coordinates — those live in [`AtomicConformation`]).
16///
17/// Each field is a parallel array indexed by atom index.
18#[derive(Clone, Debug)]
19pub struct AtomsTable {
20    /// Atom name, e.g. "CA", "N", "CB".
21    pub atom_name: Vec<String>,
22    /// Element symbol, e.g. "C", "N", "O", "S".
23    pub element: Vec<String>,
24    /// Alternative location indicator (e.g. `Some('A')`), or `None`.
25    pub alt_loc: Vec<Option<char>>,
26    /// Formal charge, or `None` if unspecified.
27    pub formal_charge: Vec<Option<i8>>,
28}
29
30impl AtomsTable {
31    /// Number of atoms.
32    pub fn len(&self) -> usize {
33        self.atom_name.len()
34    }
35
36    /// Returns `true` if the table contains no atoms.
37    pub fn is_empty(&self) -> bool {
38        self.atom_name.is_empty()
39    }
40}
41
42/// Per-residue topology data.
43///
44/// Each field is a parallel array indexed by residue index.
45#[derive(Clone, Debug)]
46pub struct ResiduesTable {
47    /// Residue name (CCD component ID), e.g. "ALA", "HOH", "ATP".
48    pub comp_id: Vec<String>,
49    /// Internal sequential residue index (label_seq_id in mmCIF).
50    pub label_seq_id: Vec<i32>,
51    /// Author-assigned sequence number (auth_seq_id in mmCIF).
52    /// Used as the primary sort key for canonical iteration order within a chain.
53    pub auth_seq_id: Vec<i32>,
54    /// Insertion code (e.g. `Some('A')`), or `None`. Secondary sort key for canonical order.
55    pub ins_code: Vec<Option<char>>,
56    /// Whether this residue belongs to a polymer or non-polymer group.
57    pub group: Vec<ResidueGroup>,
58}
59
60impl ResiduesTable {
61    /// Number of residues.
62    pub fn len(&self) -> usize {
63        self.comp_id.len()
64    }
65
66    /// Returns `true` if the table contains no residues.
67    pub fn is_empty(&self) -> bool {
68        self.comp_id.is_empty()
69    }
70}
71
72/// Per-chain topology data.
73///
74/// Each field is a parallel array indexed by chain index.
75#[derive(Clone, Debug)]
76pub struct ChainsTable {
77    /// Internal chain identifier (label_asym_id in mmCIF).
78    pub label_asym_id: Vec<String>,
79    /// Author-assigned chain identifier (auth_asym_id in mmCIF).
80    pub auth_asym_id: Vec<String>,
81    /// Entity identifier this chain belongs to.
82    pub entity_id: Vec<String>,
83}
84
85impl ChainsTable {
86    /// Number of chains.
87    pub fn len(&self) -> usize {
88        self.label_asym_id.len()
89    }
90
91    /// Returns `true` if the table contains no chains.
92    pub fn is_empty(&self) -> bool {
93        self.label_asym_id.is_empty()
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_atoms_table_len() {
103        let table = AtomsTable {
104            atom_name: vec!["N".into(), "CA".into(), "C".into()],
105            element: vec!["N".into(), "C".into(), "C".into()],
106            alt_loc: vec![None, None, None],
107            formal_charge: vec![None, None, None],
108        };
109        assert_eq!(table.len(), 3);
110        assert!(!table.is_empty());
111
112        let empty = AtomsTable {
113            atom_name: vec![],
114            element: vec![],
115            alt_loc: vec![],
116            formal_charge: vec![],
117        };
118        assert_eq!(empty.len(), 0);
119        assert!(empty.is_empty());
120    }
121
122    #[test]
123    fn test_residue_group_variants() {
124        let polymer = ResidueGroup::Polymer;
125        let non_polymer = ResidueGroup::NonPolymer;
126
127        assert_eq!(polymer, ResidueGroup::Polymer);
128        assert_eq!(non_polymer, ResidueGroup::NonPolymer);
129        assert_ne!(polymer, non_polymer);
130
131        // Verify Clone + Debug work
132        let cloned = polymer.clone();
133        assert_eq!(cloned, ResidueGroup::Polymer);
134        assert!(!format!("{:?}", non_polymer).is_empty());
135    }
136}