1use std::collections::HashMap;
14
15pub const NUM_ATOM37: usize = 37;
18pub const NUM_RESIDUES: usize = 21; #[rustfmt::skip]
25pub const ATOM37_NAMES: [&str; NUM_ATOM37] = [
26 "N", "CA", "C", "CB", "O",
27 "CG", "CG1", "CG2", "OG", "OG1",
28 "SG", "CD", "CD1", "CD2", "ND1",
29 "ND2", "OD1", "OD2", "SD", "CE",
30 "CE1", "CE2", "CE3", "NE", "NE1",
31 "NE2", "OE1", "OE2", "CH2", "NH1",
32 "NH2", "OH", "CZ", "CZ2", "CZ3",
33 "NZ", "OXT",
34];
35
36pub fn atom_order() -> HashMap<String, usize> {
38 ATOM37_NAMES
39 .iter()
40 .enumerate()
41 .map(|(i, &name)| (name.to_string(), i))
42 .collect()
43}
44
45pub fn atom37_index(name: &str) -> Option<usize> {
47 ATOM37_NAMES.iter().position(|&n| n == name)
48}
49
50#[rustfmt::skip]
59pub fn chi_angles_atoms(res3: &str) -> &'static [(&'static str, &'static str, &'static str, &'static str)] {
60 match res3 {
61 "ALA" | "GLY" | "UNK" => &[],
62 "ARG" => &[
63 ("N","CA","CB","CG"), ("CA","CB","CG","CD"),
64 ("CB","CG","CD","NE"), ("CG","CD","NE","CZ"),
65 ],
66 "ASN" => &[
67 ("N","CA","CB","CG"), ("CA","CB","CG","OD1"),
68 ],
69 "ASP" => &[
70 ("N","CA","CB","CG"), ("CA","CB","CG","OD1"),
71 ],
72 "CYS" => &[
73 ("N","CA","CB","SG"),
74 ],
75 "GLN" => &[
76 ("N","CA","CB","CG"), ("CA","CB","CG","CD"), ("CB","CG","CD","OE1"),
77 ],
78 "GLU" => &[
79 ("N","CA","CB","CG"), ("CA","CB","CG","CD"), ("CB","CG","CD","OE1"),
80 ],
81 "HIS" => &[
82 ("N","CA","CB","CG"), ("CA","CB","CG","ND1"),
83 ],
84 "ILE" => &[
85 ("N","CA","CB","CG1"), ("CA","CB","CG1","CD1"),
86 ],
87 "LEU" => &[
88 ("N","CA","CB","CG"), ("CA","CB","CG","CD1"),
89 ],
90 "LYS" => &[
91 ("N","CA","CB","CG"), ("CA","CB","CG","CD"),
92 ("CB","CG","CD","CE"), ("CG","CD","CE","NZ"),
93 ],
94 "MET" => &[
95 ("N","CA","CB","CG"), ("CA","CB","CG","SD"), ("CB","CG","SD","CE"),
96 ],
97 "PHE" => &[
98 ("N","CA","CB","CG"), ("CA","CB","CG","CD1"),
99 ],
100 "PRO" => &[
101 ("N","CA","CB","CG"), ("CA","CB","CG","CD"),
102 ],
103 "SER" => &[
104 ("N","CA","CB","OG"),
105 ],
106 "THR" => &[
107 ("N","CA","CB","OG1"),
108 ],
109 "TRP" => &[
110 ("N","CA","CB","CG"), ("CA","CB","CG","CD1"),
111 ],
112 "TYR" => &[
113 ("N","CA","CB","CG"), ("CA","CB","CG","CD1"),
114 ],
115 "VAL" => &[
116 ("N","CA","CB","CG1"),
117 ],
118 _ => &[],
119 }
120}
121
122#[rustfmt::skip]
130pub fn atom14_to_atom37_for_residue(res3: &str) -> [Option<usize>; 14] {
131 let names: [Option<&str>; 14] = match res3 {
132 "ALA" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),None,None,None,None,None,None,None,None,None],
133 "CYS" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("SG"),None,None,None,None,None,None,None,None],
134 "ASP" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG"),Some("OD1"),Some("OD2"),None,None,None,None,None,None],
135 "GLU" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG"),Some("CD"),Some("OE1"),Some("OE2"),None,None,None,None,None],
136 "PHE" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG"),Some("CD1"),Some("CD2"),Some("CE1"),Some("CE2"),Some("CZ"),None,None,None],
137 "GLY" => [Some("N"),Some("CA"),Some("C"),Some("O"),None,None,None,None,None,None,None,None,None,None],
138 "HIS" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG"),Some("ND1"),Some("CD2"),Some("CE1"),Some("NE2"),None,None,None,None],
139 "ILE" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG1"),Some("CG2"),Some("CD1"),None,None,None,None,None,None],
140 "LYS" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG"),Some("CD"),Some("CE"),Some("NZ"),None,None,None,None,None],
141 "LEU" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG"),Some("CD1"),Some("CD2"),None,None,None,None,None,None],
142 "MET" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG"),Some("SD"),Some("CE"),None,None,None,None,None,None],
143 "ASN" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG"),Some("OD1"),Some("ND2"),None,None,None,None,None,None],
144 "PRO" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG"),Some("CD"),None,None,None,None,None,None,None],
145 "GLN" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG"),Some("CD"),Some("OE1"),Some("NE2"),None,None,None,None,None],
146 "ARG" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG"),Some("CD"),Some("NE"),Some("CZ"),Some("NH1"),Some("NH2"),None,None,None],
147 "SER" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("OG"),None,None,None,None,None,None,None,None],
148 "THR" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("OG1"),Some("CG2"),None,None,None,None,None,None,None],
149 "VAL" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG1"),Some("CG2"),None,None,None,None,None,None,None],
150 "TRP" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG"),Some("CD1"),Some("CD2"),Some("CE2"),Some("CE3"),Some("NE1"),Some("CZ2"),Some("CZ3"),Some("CH2")],
151 "TYR" => [Some("N"),Some("CA"),Some("C"),Some("O"),Some("CB"),Some("CG"),Some("CD1"),Some("CD2"),Some("CE1"),Some("CE2"),Some("CZ"),Some("OH"),None,None],
152 _ => [None; 14],
153 };
154
155 let mut out = [None; 14];
156 for (i, name_opt) in names.iter().enumerate() {
157 out[i] = name_opt.and_then(atom37_index);
158 }
159 out
160}
161
162pub fn vdw_radius(element: &str) -> f32 {
168 match element {
169 "H" => 1.20,
170 "C" => 1.70,
171 "N" => 1.55,
172 "O" => 1.52,
173 "S" => 1.80,
174 "P" => 1.80,
175 "F" => 1.47,
176 "CL" | "Cl" => 1.75,
177 "BR" | "Br" => 1.85,
178 "I" => 1.98,
179 "SE" | "Se" => 1.90,
180 _ => 1.70,
181 }
182}
183
184#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn test_atom_order_completeness() {
192 let order = atom_order();
193 assert_eq!(order.len(), NUM_ATOM37);
194 assert_eq!(order["N"], 0);
196 assert_eq!(order["CA"], 1);
197 assert_eq!(order["C"], 2);
198 assert_eq!(order["CB"], 3);
199 assert_eq!(order["O"], 4);
200 }
201
202 #[test]
203 fn test_atom37_index_roundtrip() {
204 for (i, &name) in ATOM37_NAMES.iter().enumerate() {
205 assert_eq!(atom37_index(name), Some(i));
206 }
207 assert_eq!(atom37_index("ZZZ"), None);
208 }
209
210 #[test]
211 fn test_chi_gly_ala_empty() {
212 assert!(chi_angles_atoms("GLY").is_empty());
213 assert!(chi_angles_atoms("ALA").is_empty());
214 }
215
216 #[test]
217 fn test_chi_arg_four_angles() {
218 assert_eq!(chi_angles_atoms("ARG").len(), 4);
219 assert_eq!(chi_angles_atoms("ARG")[0], ("N", "CA", "CB", "CG"));
220 }
221
222 #[test]
223 fn test_chi_val_one_angle() {
224 let chis = chi_angles_atoms("VAL");
225 assert_eq!(chis.len(), 1);
226 assert_eq!(chis[0], ("N", "CA", "CB", "CG1"));
227 }
228
229 #[test]
230 fn test_atom14_to_atom37_gly() {
231 let mapping = atom14_to_atom37_for_residue("GLY");
232 assert_eq!(mapping[0], Some(0)); assert_eq!(mapping[1], Some(1)); assert_eq!(mapping[2], Some(2)); assert_eq!(mapping[3], Some(4)); assert_eq!(mapping[4], None); for i in 4..14 {
239 assert_eq!(mapping[i], None, "GLY slot {i} should be None");
240 }
241 }
242
243 #[test]
244 fn test_atom14_to_atom37_ala() {
245 let mapping = atom14_to_atom37_for_residue("ALA");
246 assert_eq!(mapping[4], Some(3)); assert_eq!(mapping[5], None);
248 }
249
250 #[test]
251 fn test_atom14_to_atom37_trp_full() {
252 let mapping = atom14_to_atom37_for_residue("TRP");
254 assert!(
255 mapping.iter().all(|m| m.is_some()),
256 "TRP should fill all 14 slots"
257 );
258 }
259
260 #[test]
261 fn test_atom14_to_atom37_unk() {
262 let mapping = atom14_to_atom37_for_residue("UNK");
263 assert!(mapping.iter().all(|m| m.is_none()));
264 }
265
266 #[test]
267 fn test_vdw_radius_known_elements() {
268 assert!((vdw_radius("C") - 1.70).abs() < 1e-6);
269 assert!((vdw_radius("N") - 1.55).abs() < 1e-6);
270 assert!((vdw_radius("O") - 1.52).abs() < 1e-6);
271 assert!((vdw_radius("S") - 1.80).abs() < 1e-6);
272 }
273
274 #[test]
275 fn test_vdw_radius_unknown_defaults_to_carbon() {
276 assert!((vdw_radius("X") - 1.70).abs() < 1e-6);
277 }
278}