Skip to main content

readcon_core/
helpers.rs

1//! Element symbol and atomic number lookup.
2//!
3//! Both lookups cover hydrogen through uranium (Z = 1..=92) plus the
4//! hydrogen isotopes deuterium ("D") and tritium ("T") which both
5//! map to Z = 1 since they share hydrogen's nucleon charge. The
6//! reverse lookup ([`atomic_number_to_symbol`]) returns the standard
7//! "H" for Z = 1; callers that need to distinguish isotopes do so out
8//! of band (e.g. an `isotope` metadata key on the frame, or by
9//! storing "D" / "T" as the per-atom symbol).
10//!
11//! The mapping is *informational, not binding*. The CON spec does not
12//! mandate that an atom's symbol correspond to any periodic-table
13//! element; consumers commonly store ghost atoms (e.g. for QM/MM
14//! link atoms, virtual sites, dummy positions in NEB chains) with
15//! whatever symbol fits their workflow. Unknown inputs return a
16//! stable sentinel: [`symbol_to_atomic_number`] returns 0 for
17//! unknown symbols and [`atomic_number_to_symbol`] returns "X" for
18//! unknown atomic numbers.
19//!
20//! The same lookup is exposed to C/C++ via [`crate::ffi::rkr_symbol_to_z`]
21//! and [`crate::ffi::rkr_z_to_symbol`], and to Python as
22//! `readcon.symbol_to_atomic_number` / `readcon.atomic_number_to_symbol`,
23//! so downstream tools can drop their own copies of the periodic table.
24
25/// Returns the atomic number for a chemical symbol, or 0 if unknown.
26pub fn symbol_to_atomic_number(symbol: &str) -> u64 {
27    match symbol {
28        "H" | "D" | "T" => 1,
29        "He" => 2,
30        "Li" => 3,
31        "Be" => 4,
32        "B" => 5,
33        "C" => 6,
34        "N" => 7,
35        "O" => 8,
36        "F" => 9,
37        "Ne" => 10,
38        "Na" => 11,
39        "Mg" => 12,
40        "Al" => 13,
41        "Si" => 14,
42        "P" => 15,
43        "S" => 16,
44        "Cl" => 17,
45        "Ar" => 18,
46        "K" => 19,
47        "Ca" => 20,
48        "Sc" => 21,
49        "Ti" => 22,
50        "V" => 23,
51        "Cr" => 24,
52        "Mn" => 25,
53        "Fe" => 26,
54        "Co" => 27,
55        "Ni" => 28,
56        "Cu" => 29,
57        "Zn" => 30,
58        "Ga" => 31,
59        "Ge" => 32,
60        "As" => 33,
61        "Se" => 34,
62        "Br" => 35,
63        "Kr" => 36,
64        "Rb" => 37,
65        "Sr" => 38,
66        "Y" => 39,
67        "Zr" => 40,
68        "Nb" => 41,
69        "Mo" => 42,
70        "Tc" => 43,
71        "Ru" => 44,
72        "Rh" => 45,
73        "Pd" => 46,
74        "Ag" => 47,
75        "Cd" => 48,
76        "In" => 49,
77        "Sn" => 50,
78        "Sb" => 51,
79        "Te" => 52,
80        "I" => 53,
81        "Xe" => 54,
82        "Cs" => 55,
83        "Ba" => 56,
84        "La" => 57,
85        "Ce" => 58,
86        "Pr" => 59,
87        "Nd" => 60,
88        "Pm" => 61,
89        "Sm" => 62,
90        "Eu" => 63,
91        "Gd" => 64,
92        "Tb" => 65,
93        "Dy" => 66,
94        "Ho" => 67,
95        "Er" => 68,
96        "Tm" => 69,
97        "Yb" => 70,
98        "Lu" => 71,
99        "Hf" => 72,
100        "Ta" => 73,
101        "W" => 74,
102        "Re" => 75,
103        "Os" => 76,
104        "Ir" => 77,
105        "Pt" => 78,
106        "Au" => 79,
107        "Hg" => 80,
108        "Tl" => 81,
109        "Pb" => 82,
110        "Bi" => 83,
111        "Po" => 84,
112        "At" => 85,
113        "Rn" => 86,
114        "Fr" => 87,
115        "Ra" => 88,
116        "Ac" => 89,
117        "Th" => 90,
118        "Pa" => 91,
119        "U" => 92,
120        _ => 0, // Unknown
121    }
122}
123
124/// Returns the chemical symbol for an atomic number, or "X" if unknown.
125pub fn atomic_number_to_symbol(atomic_number: u64) -> &'static str {
126    match atomic_number {
127        1 => "H",
128        2 => "He",
129        3 => "Li",
130        4 => "Be",
131        5 => "B",
132        6 => "C",
133        7 => "N",
134        8 => "O",
135        9 => "F",
136        10 => "Ne",
137        11 => "Na",
138        12 => "Mg",
139        13 => "Al",
140        14 => "Si",
141        15 => "P",
142        16 => "S",
143        17 => "Cl",
144        18 => "Ar",
145        19 => "K",
146        20 => "Ca",
147        21 => "Sc",
148        22 => "Ti",
149        23 => "V",
150        24 => "Cr",
151        25 => "Mn",
152        26 => "Fe",
153        27 => "Co",
154        28 => "Ni",
155        29 => "Cu",
156        30 => "Zn",
157        31 => "Ga",
158        32 => "Ge",
159        33 => "As",
160        34 => "Se",
161        35 => "Br",
162        36 => "Kr",
163        37 => "Rb",
164        38 => "Sr",
165        39 => "Y",
166        40 => "Zr",
167        41 => "Nb",
168        42 => "Mo",
169        43 => "Tc",
170        44 => "Ru",
171        45 => "Rh",
172        46 => "Pd",
173        47 => "Ag",
174        48 => "Cd",
175        49 => "In",
176        50 => "Sn",
177        51 => "Sb",
178        52 => "Te",
179        53 => "I",
180        54 => "Xe",
181        55 => "Cs",
182        56 => "Ba",
183        57 => "La",
184        58 => "Ce",
185        59 => "Pr",
186        60 => "Nd",
187        61 => "Pm",
188        62 => "Sm",
189        63 => "Eu",
190        64 => "Gd",
191        65 => "Tb",
192        66 => "Dy",
193        67 => "Ho",
194        68 => "Er",
195        69 => "Tm",
196        70 => "Yb",
197        71 => "Lu",
198        72 => "Hf",
199        73 => "Ta",
200        74 => "W",
201        75 => "Re",
202        76 => "Os",
203        77 => "Ir",
204        78 => "Pt",
205        79 => "Au",
206        80 => "Hg",
207        81 => "Tl",
208        82 => "Pb",
209        83 => "Bi",
210        84 => "Po",
211        85 => "At",
212        86 => "Rn",
213        87 => "Fr",
214        88 => "Ra",
215        89 => "Ac",
216        90 => "Th",
217        91 => "Pa",
218        92 => "U",
219        _ => "X", // Represents an unknown element
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn known_round_trip() {
229        for z in 1u64..=92 {
230            let symbol = atomic_number_to_symbol(z);
231            assert_eq!(
232                symbol_to_atomic_number(symbol),
233                z,
234                "round-trip failed for Z={z} (symbol={symbol})"
235            );
236        }
237    }
238
239    #[test]
240    fn unknown_symbol_returns_zero() {
241        assert_eq!(symbol_to_atomic_number(""), 0);
242        assert_eq!(symbol_to_atomic_number("Xx"), 0);
243        assert_eq!(symbol_to_atomic_number("h"), 0); // case-sensitive
244    }
245
246    #[test]
247    fn hydrogen_isotopes_map_to_z_one() {
248        // Deuterium and tritium share hydrogen's nucleon charge; the
249        // reverse lookup uses canonical "H" so callers that need to
250        // round-trip isotope identity store it out of band.
251        assert_eq!(symbol_to_atomic_number("D"), 1);
252        assert_eq!(symbol_to_atomic_number("T"), 1);
253        // Ghost-atom / dummy-site placeholders (anything outside
254        // H..U plus D/T) stay sentinel-0 so consumers can detect
255        // and route them differently.
256        assert_eq!(symbol_to_atomic_number("Gh"), 0);
257        assert_eq!(symbol_to_atomic_number("Dum"), 0);
258        assert_eq!(symbol_to_atomic_number("M"), 0);
259    }
260
261    #[test]
262    fn unknown_z_returns_x() {
263        assert_eq!(atomic_number_to_symbol(0), "X");
264        assert_eq!(atomic_number_to_symbol(93), "X");
265        assert_eq!(atomic_number_to_symbol(u64::MAX), "X");
266    }
267}