Skip to main content

chematic_core/
element.rs

1//! Periodic-table elements indexed by atomic number (1–118).
2
3/// An element represented as its atomic number, wrapped in a newtype.
4///
5/// Zero-copy: implements `Copy`; comparison and hashing are O(1).
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
7pub struct Element(u8);
8
9impl Element {
10    // -- commonly used element constants ----------------------------------
11    pub const H: Element = Element(1);
12    pub const HE: Element = Element(2);
13    pub const LI: Element = Element(3);
14    pub const BE: Element = Element(4);
15    pub const B: Element = Element(5);
16    pub const C: Element = Element(6);
17    pub const N: Element = Element(7);
18    pub const O: Element = Element(8);
19    pub const F: Element = Element(9);
20    pub const NE: Element = Element(10);
21    pub const NA: Element = Element(11);
22    pub const MG: Element = Element(12);
23    pub const AL: Element = Element(13);
24    pub const SI: Element = Element(14);
25    pub const P: Element = Element(15);
26    pub const S: Element = Element(16);
27    pub const CL: Element = Element(17);
28    pub const AR: Element = Element(18);
29    pub const K: Element = Element(19);
30    pub const CA: Element = Element(20);
31    pub const SC: Element = Element(21);
32    pub const TI: Element = Element(22);
33    pub const V: Element = Element(23);
34    pub const CR: Element = Element(24);
35    pub const MN: Element = Element(25);
36    pub const FE: Element = Element(26);
37    pub const CO: Element = Element(27);
38    pub const NI: Element = Element(28);
39    pub const CU: Element = Element(29);
40    pub const ZN: Element = Element(30);
41    pub const GA: Element = Element(31);
42    pub const GE: Element = Element(32);
43    pub const AS: Element = Element(33);
44    pub const SE: Element = Element(34);
45    pub const BR: Element = Element(35);
46    pub const KR: Element = Element(36);
47    pub const RB: Element = Element(37);
48    pub const SR: Element = Element(38);
49    pub const Y: Element = Element(39);
50    pub const ZR: Element = Element(40);
51    pub const NB: Element = Element(41);
52    pub const MO: Element = Element(42);
53    pub const TC: Element = Element(43);
54    pub const RU: Element = Element(44);
55    pub const RH: Element = Element(45);
56    pub const PD: Element = Element(46);
57    pub const AG: Element = Element(47);
58    pub const CD: Element = Element(48);
59    pub const IN: Element = Element(49);
60    pub const SN: Element = Element(50);
61    pub const SB: Element = Element(51);
62    pub const TE: Element = Element(52);
63    pub const I: Element = Element(53);
64    pub const XE: Element = Element(54);
65    pub const CS: Element = Element(55);
66    pub const BA: Element = Element(56);
67    pub const LA: Element = Element(57);
68    pub const CE: Element = Element(58);
69    pub const PR: Element = Element(59);
70    pub const ND: Element = Element(60);
71    pub const PM: Element = Element(61);
72    pub const SM: Element = Element(62);
73    pub const EU: Element = Element(63);
74    pub const GD: Element = Element(64);
75    pub const TB: Element = Element(65);
76    pub const DY: Element = Element(66);
77    pub const HO: Element = Element(67);
78    pub const ER: Element = Element(68);
79    pub const TM: Element = Element(69);
80    pub const YB: Element = Element(70);
81    pub const LU: Element = Element(71);
82    pub const HF: Element = Element(72);
83    pub const TA: Element = Element(73);
84    pub const W: Element = Element(74);
85    pub const RE: Element = Element(75);
86    pub const OS: Element = Element(76);
87    pub const IR: Element = Element(77);
88    pub const PT: Element = Element(78);
89    pub const AU: Element = Element(79);
90    pub const HG: Element = Element(80);
91    pub const TL: Element = Element(81);
92    pub const PB: Element = Element(82);
93    pub const BI: Element = Element(83);
94    pub const PO: Element = Element(84);
95    pub const AT: Element = Element(85);
96    pub const RN: Element = Element(86);
97    pub const FR: Element = Element(87);
98    pub const RA: Element = Element(88);
99    pub const AC: Element = Element(89);
100    pub const TH: Element = Element(90);
101    pub const PA: Element = Element(91);
102    pub const U: Element = Element(92);
103    pub const NP: Element = Element(93);
104    pub const PU: Element = Element(94);
105    pub const AM: Element = Element(95);
106    pub const CM: Element = Element(96);
107    pub const BK: Element = Element(97);
108    pub const CF: Element = Element(98);
109    pub const ES: Element = Element(99);
110    pub const FM: Element = Element(100);
111    pub const MD: Element = Element(101);
112    pub const NO: Element = Element(102);
113    pub const LR: Element = Element(103);
114    pub const RF: Element = Element(104);
115    pub const DB: Element = Element(105);
116    pub const SG: Element = Element(106);
117    pub const BH: Element = Element(107);
118    pub const HS: Element = Element(108);
119    pub const MT: Element = Element(109);
120    pub const DS: Element = Element(110);
121    pub const RG: Element = Element(111);
122    pub const CN: Element = Element(112);
123    pub const NH: Element = Element(113);
124    pub const FL: Element = Element(114);
125    pub const MC: Element = Element(115);
126    pub const LV: Element = Element(116);
127    pub const TS: Element = Element(117);
128    pub const OG: Element = Element(118);
129
130    // -- constructors -------------------------------------------------------
131
132    /// Create an Element from an atomic number. Returns `None` if `n` is outside 1–118.
133    #[inline]
134    pub const fn from_atomic_number(n: u8) -> Option<Self> {
135        if n >= 1 && n <= 118 {
136            Some(Self(n))
137        } else {
138            None
139        }
140    }
141
142    /// Create an Element from its symbol (title-case, e.g. "C", "Cl").
143    /// Case-sensitive: "C" is carbon, "c" is not accepted.
144    pub fn from_symbol(s: &str) -> Option<Self> {
145        match s {
146            "H" => Some(Self::H),
147            "He" => Some(Self::HE),
148            "Li" => Some(Self::LI),
149            "Be" => Some(Self::BE),
150            "B" => Some(Self::B),
151            "C" => Some(Self::C),
152            "N" => Some(Self::N),
153            "O" => Some(Self::O),
154            "F" => Some(Self::F),
155            "Ne" => Some(Self::NE),
156            "Na" => Some(Self::NA),
157            "Mg" => Some(Self::MG),
158            "Al" => Some(Self::AL),
159            "Si" => Some(Self::SI),
160            "P" => Some(Self::P),
161            "S" => Some(Self::S),
162            "Cl" => Some(Self::CL),
163            "Ar" => Some(Self::AR),
164            "K" => Some(Self::K),
165            "Ca" => Some(Self::CA),
166            "Sc" => Some(Self::SC),
167            "Ti" => Some(Self::TI),
168            "V" => Some(Self::V),
169            "Cr" => Some(Self::CR),
170            "Mn" => Some(Self::MN),
171            "Fe" => Some(Self::FE),
172            "Co" => Some(Self::CO),
173            "Ni" => Some(Self::NI),
174            "Cu" => Some(Self::CU),
175            "Zn" => Some(Self::ZN),
176            "Ga" => Some(Self::GA),
177            "Ge" => Some(Self::GE),
178            "As" => Some(Self::AS),
179            "Se" => Some(Self::SE),
180            "Br" => Some(Self::BR),
181            "Kr" => Some(Self::KR),
182            "Rb" => Some(Self::RB),
183            "Sr" => Some(Self::SR),
184            "Y" => Some(Self::Y),
185            "Zr" => Some(Self::ZR),
186            "Nb" => Some(Self::NB),
187            "Mo" => Some(Self::MO),
188            "Tc" => Some(Self::TC),
189            "Ru" => Some(Self::RU),
190            "Rh" => Some(Self::RH),
191            "Pd" => Some(Self::PD),
192            "Ag" => Some(Self::AG),
193            "Cd" => Some(Self::CD),
194            "In" => Some(Self::IN),
195            "Sn" => Some(Self::SN),
196            "Sb" => Some(Self::SB),
197            "Te" => Some(Self::TE),
198            "I" => Some(Self::I),
199            "Xe" => Some(Self::XE),
200            "Cs" => Some(Self::CS),
201            "Ba" => Some(Self::BA),
202            "La" => Some(Self::LA),
203            "Ce" => Some(Self::CE),
204            "Pr" => Some(Self::PR),
205            "Nd" => Some(Self::ND),
206            "Pm" => Some(Self::PM),
207            "Sm" => Some(Self::SM),
208            "Eu" => Some(Self::EU),
209            "Gd" => Some(Self::GD),
210            "Tb" => Some(Self::TB),
211            "Dy" => Some(Self::DY),
212            "Ho" => Some(Self::HO),
213            "Er" => Some(Self::ER),
214            "Tm" => Some(Self::TM),
215            "Yb" => Some(Self::YB),
216            "Lu" => Some(Self::LU),
217            "Hf" => Some(Self::HF),
218            "Ta" => Some(Self::TA),
219            "W" => Some(Self::W),
220            "Re" => Some(Self::RE),
221            "Os" => Some(Self::OS),
222            "Ir" => Some(Self::IR),
223            "Pt" => Some(Self::PT),
224            "Au" => Some(Self::AU),
225            "Hg" => Some(Self::HG),
226            "Tl" => Some(Self::TL),
227            "Pb" => Some(Self::PB),
228            "Bi" => Some(Self::BI),
229            "Po" => Some(Self::PO),
230            "At" => Some(Self::AT),
231            "Rn" => Some(Self::RN),
232            "Fr" => Some(Self::FR),
233            "Ra" => Some(Self::RA),
234            "Ac" => Some(Self::AC),
235            "Th" => Some(Self::TH),
236            "Pa" => Some(Self::PA),
237            "U" => Some(Self::U),
238            "Np" => Some(Self::NP),
239            "Pu" => Some(Self::PU),
240            "Am" => Some(Self::AM),
241            "Cm" => Some(Self::CM),
242            "Bk" => Some(Self::BK),
243            "Cf" => Some(Self::CF),
244            "Es" => Some(Self::ES),
245            "Fm" => Some(Self::FM),
246            "Md" => Some(Self::MD),
247            "No" => Some(Self::NO),
248            "Lr" => Some(Self::LR),
249            "Rf" => Some(Self::RF),
250            "Db" => Some(Self::DB),
251            "Sg" => Some(Self::SG),
252            "Bh" => Some(Self::BH),
253            "Hs" => Some(Self::HS),
254            "Mt" => Some(Self::MT),
255            "Ds" => Some(Self::DS),
256            "Rg" => Some(Self::RG),
257            "Cn" => Some(Self::CN),
258            "Nh" => Some(Self::NH),
259            "Fl" => Some(Self::FL),
260            "Mc" => Some(Self::MC),
261            "Lv" => Some(Self::LV),
262            "Ts" => Some(Self::TS),
263            "Og" => Some(Self::OG),
264            _ => None,
265        }
266    }
267
268    /// Return the IUPAC element symbol (title-case).
269    #[inline]
270    pub fn symbol(self) -> &'static str {
271        SYMBOLS[(self.0 as usize) - 1]
272    }
273
274    /// Return the atomic number (1–118).
275    #[inline]
276    pub fn atomic_number(self) -> u8 {
277        self.0
278    }
279
280    /// Returns true if this element is in the OpenSMILES organic subset
281    /// (B, C, N, O, P, S, F, Cl, Br, I) — these may carry implicit H without brackets.
282    #[inline]
283    pub fn is_organic_subset(self) -> bool {
284        matches!(self.0, 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 35 | 53)
285    }
286
287    /// van der Waals radius in Å (Bondi 1964 + Alvarez 2013).
288    /// Unknown/synthetic elements return 1.70 (carbon fallback).
289    #[inline]
290    pub fn vdw_radius(self) -> f32 {
291        VDW_RADII[(self.0 as usize) - 1]
292    }
293
294    /// Single-bond covalent radius in Å (Alvarez 2008).
295    /// Unknown elements return 0.77 (sp3 carbon fallback).
296    #[inline]
297    pub fn covalent_radius(self) -> f32 {
298        COVALENT_RADII[(self.0 as usize) - 1]
299    }
300
301    /// Monoisotopic (exact) mass of the most abundant isotope (u).
302    /// Used as CIP rule 4 tiebreaker: higher atomic mass has higher priority.
303    pub fn atomic_mass(self) -> f64 {
304        ATOMIC_MASSES[(self.0 as usize) - 1]
305    }
306
307    /// Normal valence list (ascending). Empty = undefined (transition metals, etc.).
308    /// Used for computing implicit H counts in organic-subset atoms.
309    pub fn normal_valences(self) -> &'static [u8] {
310        match self.0 {
311            1 => &[1],           // H
312            5 => &[3],           // B
313            6 => &[4],           // C
314            7 => &[3, 5],        // N
315            8 => &[2],           // O
316            9 => &[1],           // F
317            14 => &[4],          // Si
318            15 => &[3, 5],       // P
319            16 => &[2, 4, 6],    // S
320            17 => &[1, 3, 5, 7], // Cl
321            33 => &[3, 5],       // As
322            34 => &[2, 4, 6],    // Se
323            35 => &[1, 3, 5, 7], // Br
324            // Te: source-verified against RDKit atomic_data.cpp @ pinned commit
325            // 8afba32ec539dcb2369bc84549d802aca3f7eb39 ("52  Te  ...  2  4  6" in the
326            // trailing "list of allowed valences" columns) AND cross-checked against
327            // installed RDKit 2026.03.3: GetPeriodicTable().GetValenceList(52) == [2, 4, 6],
328            // GetDefaultValence(52) == 2. Matches S/Se's list by coincidence, not analogy.
329            52 => &[2, 4, 6],    // Te
330            53 => &[1, 3, 5, 7], // I
331            _ => &[],
332        }
333    }
334}
335
336impl core::fmt::Display for Element {
337    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
338        f.write_str(self.symbol())
339    }
340}
341
342/// Element symbol table indexed by (atomic_number - 1), covering elements 1–118.
343static SYMBOLS: [&str; 118] = [
344    "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl",
345    "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As",
346    "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In",
347    "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb",
348    "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl",
349    "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk",
350    "Cf", "Es", "Fm", "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn", "Nh",
351    "Fl", "Mc", "Lv", "Ts", "Og",
352];
353
354/// van der Waals radii (Å) indexed by (atomic_number - 1).
355/// Sources: Bondi J.Phys.Chem. 1964; Alvarez Dalton Trans. 2013, 10.1039/c3dt50599e.
356/// Elements without published values use 1.70 (C fallback).
357#[rustfmt::skip]
358static VDW_RADII: [f32; 118] = [
359    // H     He    Li    Be    B     C     N     O     F     Ne
360    1.20, 1.40, 1.82, 1.53, 1.92, 1.70, 1.55, 1.52, 1.47, 1.54,
361    // Na    Mg    Al    Si    P     S     Cl    Ar    K     Ca
362    2.27, 1.73, 1.84, 2.10, 1.80, 1.80, 1.75, 1.88, 2.75, 2.31,
363    // Sc    Ti    V     Cr    Mn    Fe    Co    Ni    Cu    Zn
364    2.11, 2.00, 1.92, 1.93, 1.97, 1.96, 2.00, 1.63, 1.40, 1.39,
365    // Ga    Ge    As    Se    Br    Kr    Rb    Sr    Y     Zr
366    1.87, 2.11, 1.85, 1.90, 1.85, 2.02, 3.03, 2.49, 2.34, 2.23,
367    // Nb    Mo    Tc    Ru    Rh    Pd    Ag    Cd    In    Sn
368    2.18, 2.17, 2.16, 2.13, 2.10, 1.63, 1.72, 1.58, 1.93, 2.17,
369    // Sb    Te    I     Xe    Cs    Ba    La    Ce    Pr    Nd
370    2.06, 2.06, 1.98, 2.16, 3.43, 2.68, 2.57, 2.56, 2.54, 2.51,
371    // Pm    Sm    Eu    Gd    Tb    Dy    Ho    Er    Tm    Yb
372    2.50, 2.48, 2.45, 2.43, 2.42, 2.40, 2.38, 2.37, 2.36, 2.35,
373    // Lu    Hf    Ta    W     Re    Os    Ir    Pt    Au    Hg
374    2.34, 2.23, 2.22, 2.18, 2.16, 2.16, 2.13, 1.75, 1.66, 1.55,
375    // Tl    Pb    Bi    Po    At    Rn    Fr    Ra    Ac    Th
376    1.96, 2.02, 2.07, 1.97, 2.02, 2.20, 3.48, 2.83, 2.64, 2.54,
377    // Pa    U     Np    Pu    Am    Cm    Bk    Cf    Es    Fm
378    2.52, 2.41, 2.39, 2.43, 2.44, 2.45, 2.44, 2.45, 2.45, 1.70,
379    // Md    No    Lr    Rf    Db    Sg    Bh    Hs    Mt    Ds
380    1.70, 1.70, 1.70, 1.70, 1.70, 1.70, 1.70, 1.70, 1.70, 1.70,
381    // Rg    Cn    Nh    Fl    Mc    Lv    Ts    Og
382    1.70, 1.70, 1.70, 1.70, 1.70, 1.70, 1.70, 1.70,
383];
384
385/// Single-bond covalent radii (Å) indexed by (atomic_number - 1).
386/// Source: Alvarez et al. Dalton Trans. 2008, 2832–2838, 10.1039/b801115j.
387/// Elements 104–118 use computational estimates; others default to 0.77.
388#[rustfmt::skip]
389static COVALENT_RADII: [f32; 118] = [
390    // H     He    Li    Be    B     C     N     O     F     Ne
391    0.31, 0.28, 1.28, 0.96, 0.84, 0.77, 0.71, 0.66, 0.57, 0.58,
392    // Na    Mg    Al    Si    P     S     Cl    Ar    K     Ca
393    1.66, 1.41, 1.21, 1.11, 1.07, 1.05, 1.02, 1.06, 2.03, 1.76,
394    // Sc    Ti    V     Cr    Mn    Fe    Co    Ni    Cu    Zn
395    1.70, 1.60, 1.53, 1.39, 1.50, 1.52, 1.38, 1.24, 1.32, 1.22,
396    // Ga    Ge    As    Se    Br    Kr    Rb    Sr    Y     Zr
397    1.22, 1.20, 1.19, 1.20, 1.20, 1.16, 2.20, 1.95, 1.90, 1.75,
398    // Nb    Mo    Tc    Ru    Rh    Pd    Ag    Cd    In    Sn
399    1.64, 1.54, 1.47, 1.46, 1.42, 1.39, 1.45, 1.44, 1.42, 1.39,
400    // Sb    Te    I     Xe    Cs    Ba    La    Ce    Pr    Nd
401    1.39, 1.38, 1.39, 1.40, 2.44, 2.15, 2.07, 2.04, 2.03, 2.01,
402    // Pm    Sm    Eu    Gd    Tb    Dy    Ho    Er    Tm    Yb
403    1.99, 1.98, 1.98, 1.96, 1.94, 1.92, 1.92, 1.89, 1.90, 1.87,
404    // Lu    Hf    Ta    W     Re    Os    Ir    Pt    Au    Hg
405    1.87, 1.75, 1.70, 1.62, 1.51, 1.44, 1.41, 1.36, 1.36, 1.32,
406    // Tl    Pb    Bi    Po    At    Rn    Fr    Ra    Ac    Th
407    1.45, 1.46, 1.48, 1.40, 1.50, 1.50, 2.60, 2.21, 2.15, 2.06,
408    // Pa    U     Np    Pu    Am    Cm    Bk    Cf    Es    Fm
409    2.00, 1.96, 1.90, 1.87, 1.80, 1.69, 1.68, 1.68, 1.65, 1.67,
410    // Md    No    Lr    Rf    Db    Sg    Bh    Hs    Mt    Ds
411    1.73, 1.76, 1.61, 1.57, 1.49, 1.43, 1.41, 1.34, 1.29, 1.28,
412    // Rg    Cn    Nh    Fl    Mc    Lv    Ts    Og
413    1.21, 1.22, 1.36, 1.43, 1.62, 1.75, 1.65, 1.57,
414];
415
416/// Monoisotopic atomic masses (u) indexed by (atomic_number - 1).
417/// Source: NIST, exact masses of most abundant isotopes, used for CIP rule 4 (atomic mass tiebreaker).
418#[rustfmt::skip]
419static ATOMIC_MASSES: [f64; 118] = [
420    // H        He       Li       Be       B        C        N        O        F        Ne
421    1.007825, 4.002603, 7.016003, 9.012182, 11.009305, 12.000000, 14.003074, 15.994915, 18.998403, 19.992440,
422    // Na       Mg       Al       Si       P        S        Cl       Ar       K        Ca
423    22.989770, 23.985042, 26.981538, 27.976927, 30.973762, 31.972071, 34.968853, 39.962383, 38.963707, 39.962591,
424    // Sc       Ti       V        Cr       Mn       Fe       Co       Ni       Cu       Zn
425    44.955910, 47.947947, 50.943963, 51.940512, 54.938045, 55.934840, 58.933195, 57.935343, 62.929598, 63.929145,
426    // Ga       Ge       As       Se       Br       Kr       Rb       Sr       Y        Zr
427    68.925581, 73.921218, 74.921596, 79.916521, 78.918337, 83.911507, 84.911789, 87.905614, 88.905848, 89.904704,
428    // Nb       Mo       Tc       Ru       Rh       Pd       Ag       Cd       In       Sn
429    92.906378, 97.905408, 98.906255, 101.904435, 102.905504, 105.903483, 106.905093, 113.903358, 114.903878, 119.902197,
430    // Sb       Te       I        Xe       Cs       Ba       La       Ce       Pr       Nd
431    120.903818, 129.906224, 126.904477, 131.904154, 132.905447, 137.905241, 138.906349, 139.905434, 140.907648, 141.907720,
432    // Pm       Sm       Eu       Gd       Tb       Dy       Ho       Er       Tm       Yb
433    145.000000, 151.919729, 152.921227, 157.924103, 158.925343, 163.929171, 164.930319, 165.930292, 168.934211, 173.938858,
434    // Lu       Hf       Ta       W        Re       Os       Ir       Pt       Au       Hg
435    174.940768, 179.946549, 180.947885, 183.950931, 186.955751, 191.961479, 192.960837, 194.964791, 196.966569, 201.972326,
436    // Tl       Pb       Bi       Po       At       Rn       Fr       Ra       Ac       Th
437    202.972329, 207.982019, 208.980384, 209.000000, 210.000000, 222.000000, 223.000000, 226.000000, 227.000000, 232.038054,
438    // Pa       U        Np       Pu       Am       Cm       Bk       Cf       Es       Fm
439    231.035884, 238.028915, 237.000000, 244.000000, 243.000000, 247.000000, 247.000000, 251.000000, 252.000000, 257.000000,
440    // Md       No       Lr       Rf       Db       Sg       Bh       Hs       Mt       Ds
441    258.000000, 259.000000, 266.000000, 267.000000, 268.000000, 269.000000, 270.000000, 270.000000, 278.000000, 281.000000,
442    // Rg       Cn       Nh       Fl       Mc       Lv       Ts       Og
443    280.000000, 285.000000, 286.000000, 289.000000, 290.000000, 293.000000, 294.000000, 294.000000,
444];
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    #[test]
451    fn test_roundtrip_symbol() {
452        for n in 1u8..=118 {
453            let elem = Element::from_atomic_number(n).unwrap();
454            let sym = elem.symbol();
455            let back =
456                Element::from_symbol(sym).unwrap_or_else(|| panic!("no elem for symbol {sym}"));
457            assert_eq!(elem, back, "roundtrip failed for atomic number {n}");
458        }
459    }
460
461    #[test]
462    fn test_organic_subset() {
463        for sym in &["B", "C", "N", "O", "P", "S", "F", "Cl", "Br", "I"] {
464            assert!(
465                Element::from_symbol(sym).unwrap().is_organic_subset(),
466                "{sym} should be in organic subset"
467            );
468        }
469        assert!(!Element::H.is_organic_subset());
470        assert!(!Element::FE.is_organic_subset());
471    }
472
473    #[test]
474    fn test_vdw_radii() {
475        assert!((Element::H.vdw_radius() - 1.20).abs() < 1e-4);
476        assert!((Element::C.vdw_radius() - 1.70).abs() < 1e-4);
477        assert!((Element::N.vdw_radius() - 1.55).abs() < 1e-4);
478        assert!((Element::O.vdw_radius() - 1.52).abs() < 1e-4);
479        assert!((Element::S.vdw_radius() - 1.80).abs() < 1e-4);
480        assert!((Element::CL.vdw_radius() - 1.75).abs() < 1e-4);
481        assert!((Element::BR.vdw_radius() - 1.85).abs() < 1e-4);
482        assert!((Element::I.vdw_radius() - 1.98).abs() < 1e-4);
483        // Heavy elements fall back to 1.70
484        assert!((Element::OG.vdw_radius() - 1.70).abs() < 1e-4);
485    }
486
487    #[test]
488    fn test_covalent_radii() {
489        assert!((Element::H.covalent_radius() - 0.31).abs() < 1e-4);
490        assert!((Element::C.covalent_radius() - 0.77).abs() < 1e-4);
491        assert!((Element::N.covalent_radius() - 0.71).abs() < 1e-4);
492        assert!((Element::O.covalent_radius() - 0.66).abs() < 1e-4);
493        assert!((Element::CL.covalent_radius() - 1.02).abs() < 1e-4);
494    }
495
496    #[test]
497    fn test_valences() {
498        assert_eq!(Element::C.normal_valences(), &[4]);
499        assert_eq!(Element::N.normal_valences(), &[3, 5]);
500        assert_eq!(Element::S.normal_valences(), &[2, 4, 6]);
501    }
502
503    #[test]
504    fn test_te_valence_source_verified() {
505        // Source-verified (not assumed from S/Se by analogy):
506        //  - RDKit atomic_data.cpp @ pinned commit 8afba32ec539dcb2369bc84549d802aca3f7eb39,
507        //    row "52  Te  5  1.38  1.378  2.1  127.6  6  130  129.9062244  2  4  6"
508        //    (trailing columns = "list of allowed valences" per the file's own header comment).
509        //  - Cross-checked against installed RDKit 2026.03.3:
510        //    GetPeriodicTable().GetValenceList(52) == [2, 4, 6], GetDefaultValence(52) == 2.
511        // It happens to equal S/Se's list, but that's a verified coincidence, not the basis
512        // for this entry.
513        assert_eq!(Element::TE.normal_valences(), &[2, 4, 6]);
514    }
515
516    #[test]
517    fn test_s_se_valences_unchanged_regression_guard() {
518        // Adding the Te arm must not disturb the pre-existing S/Se entries.
519        assert_eq!(Element::S.normal_valences(), &[2, 4, 6]);
520        assert_eq!(Element::SE.normal_valences(), &[2, 4, 6]);
521    }
522
523    #[test]
524    fn test_te_not_in_organic_subset() {
525        // Te is outside the OpenSMILES organic subset (only B,C,N,O,P,S,F,Cl,Br,I are),
526        // so it always requires bracket notation and implicit_hcount() short-circuits
527        // to 0 for it regardless of normal_valences(). This must stay true.
528        assert!(!Element::TE.is_organic_subset());
529    }
530}