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}