Skip to main content

lindhard/
elements.rs

1//! Element table, Z = 1..=92.
2//!
3//! The table is `const` data with no I/O, so table builders and a WASM front
4//! end can use it directly. Provenance is also recorded in
5//! `docs/data-provenance.md`.
6//!
7//! # Sources
8//!
9//! * **Atomic weight** (`atomic_weight`, g/mol, i.e. relative atomic mass):
10//!   the CIAAW / IUPAC standard atomic weights as tabulated at
11//!   <https://www.ciaaw.org/atomic-weights.htm> ("Standard Atomic Weights
12//!   2024": the 2021 report, T. Prohaska et al., "Standard atomic weights of
13//!   the elements 2021", Pure Appl. Chem. 94, 573 (2022),
14//!   doi:10.1515/pac-2019-0603, plus the CIAAW 2024 revisions of Gd, Lu and
15//!   Zr). For the 14 elements whose standard weight is an interval (H, Li, B,
16//!   C, N, O, Mg, Si, S, Cl, Ar, Br, Tl, Pb) the single value is the
17//!   abridged value of <https://www.ciaaw.org/abridged-atomic-weights.htm>,
18//!   which the 2021 report (Table 1, column 7) says replaces, and equals, the
19//!   former conventional atomic weights; for H that value is printed 1.0080,
20//!   the former conventional 1.008. Elements with no standard atomic weight
21//!   (Tc, Pm, Po, At, Rn, Fr, Ra, Ac) carry the mass number of their
22//!   longest-lived isotope as listed by CIAAW at
23//!   <https://www.ciaaw.org/radioactive-elements.htm> (from NUBASE2020,
24//!   F. G. Kondev et al., Chin. Phys. C 45, 030001 (2021)) and have
25//!   `weight_is_mass_number == true`; that is a convention, not a measurement
26//!   of a natural mixture. For Tc that page lists 97 and 98 as equally
27//!   long-lived; 98 is used. All 92 entries checked against these pages on
28//!   2026-10-06 (#15).
29//! * **Density** (`density_g_cm3`): density of the solid, g/cm³, as printed
30//!   in A. Thompson et al., *X-Ray Data Booklet*, LBNL/PUB-490 Rev. 3
31//!   (Lawrence Berkeley National Laboratory, October 2009), section 5.2,
32//!   Table 5-2 "Properties of the elements", pp. 5-5 to 5-8
33//!   (<https://xdb.lbl.gov/xdb-new.pdf>, PDF pages 153-156). Section 5.2 says
34//!   the data were taken mostly from D. R. Lide (ed.), *CRC Handbook of
35//!   Chemistry and Physics*, 80th ed. (CRC Press, 1999), and that densities
36//!   are specific gravities at 20 °C unless a superscript gives another
37//!   temperature (25 °C for Si, Sc, Ni, Zn, Ge, Y, La, Ce, Nd, Pm, Sm, Eu,
38//!   Gd, Dy, Ho, Er, Tm, Lu; V 18.7 °C, Ga 29.6 °C, Ir 17 °C). Tc and Pa are
39//!   marked "calculated" there, U "~18.95". The table names no allotrope
40//!   except graphite for C. All populated entries checked on 2026-10-07
41//!   (#15; the PDF's SHA-256 is recorded in `docs/data-provenance.md`), and
42//!   37 values set to the printed ones. Four entries the booklet does not pin
43//!   were checked on 2026-10-07 (#15) against other open primary
44//!   compilations, all U.S. government reports:
45//!   - Cr 7.20 (was 7.15, which lay **outside** the booklet's printed range
46//!     7.18-7.20): the upper end of that range, and the density "calculated
47//!     from the NBS lattice constant", 7.200 at 25 °C, in H. E. Swanson,
48//!     R. K. Fuyat and G. M. Ugrinic, *Standard X-ray Diffraction Powder
49//!     Patterns*, NBS Circular 539, Vol. V (1955), p. 20
50//!     (<https://nvlpubs.nist.gov/nistpubs/Legacy/circ/nbscircular539v5.pdf>).
51//!     This is an X-ray (crystal) density.
52//!   - Au 19.30: the booklet prints "~19.3"; NBS Circular 539, Vol. I
53//!     (Swanson and Tatge, 1953), p. 33, prints 19.302 at 25 °C from its
54//!     lattice constant
55//!     (<https://nvlpubs.nist.gov/nistpubs/Legacy/circ/nbscircular539v1.pdf>).
56//!   - Ra 5.0: no density in the booklet; "a specific gravity of
57//!     approximately 5.0" in H. W. Kirby and M. L. Salutsky, *The
58//!     Radiochemistry of Radium*, NAS-NS 3057 (1964), p. 3, citing the
59//!     International Critical Tables (<https://www.osti.gov/servlets/purl/4560824>).
60//!   - Ac 10.07: no density in the booklet; the crystal density of fcc Ac
61//!     metal (a = 5.311 Å), Table 4, p. 11, of J. D. Farr, A. L. Giorgi,
62//!     M. G. Bowman and R. K. Money, "The crystal structure of actinium metal
63//!     and actinium hydride", Los Alamos report LA-1545 (1953)
64//!     (<https://www.osti.gov/servlets/purl/4397640>), published as
65//!     J. Inorg. Nucl. Chem. 18, 42 (1961).
66//!
67//!   Still **not verified** to their stored digits: C 2.267 (printed as the
68//!   range 1.9-2.3) and Mn 7.44 (the upper end of the printed range
69//!   7.21-7.44). For Mn, the X-ray density of alpha-Mn, "(calculated) 7.475
70//!   g/cm³" at 25 °C (a = 8.9121 Å, Z = 58), is printed in M. C. Morris
71//!   et al., *Standard X-ray Diffraction Powder Patterns*, NBS Monograph 25,
72//!   Section 17 (1980), p. 50
73//!   (<https://nvlpubs.nist.gov/nistpubs/Legacy/MONO/nbsmonograph25-17.pdf>,
74//!   checked 2026-10-07, #15). It lies above the booklet's range, so unlike
75//!   Cr it does not pin a value inside that range; 7.44 is kept, unverified.
76//!   Si keeps 2.329: the booklet prints 2.33
77//!   at 25 °C, and 2.329 is the crystal density `M(Si) / V_m(Si)` from the
78//!   CODATA 2022 "molar volume of silicon" (1.205 883 199e-5 m³/mol,
79//!   <https://physics.nist.gov/cuu/Constants/Table/allascii.txt>) for any
80//!   weight in the standard interval [28.084, 28.086]. The specific gravities
81//!   are used as g/cm³; the booklet does not state the reference water
82//!   state. Later CRC editions may differ; none was seen. It is `None` where
83//!   no solid density at ambient conditions exists (gases: H, He, N, O, F,
84//!   Ne, Cl, Ar, Kr, Xe, Rn; liquids: Br, Hg; no data: At, Fr). Densities are
85//!   a default for a pure element only; compounds need an explicit density
86//!   override.
87//! * **Sublimation (cohesive) enthalpy** (`default_surface_binding_ev`): where
88//!   a measured value is tabulated, the cohesive energy per atom (eV) of
89//!   C. Kittel, *Introduction to Solid State Physics*, 8th ed. (Wiley, 2005),
90//!   chapter 3 (cohesive energies of the elements), which is the enthalpy of
91//!   atomization at 0 K. Using it as the surface binding energy `E_s` is the
92//!   usual binary-collision convention (Sigmund, Phys. Rev. 184, 383 (1969)),
93//!   **not** a measured surface barrier. Elements left `None` must have
94//!   `E_s` set explicitly by the user rather than have one invented.
95//!   **Verified** on 2026-10-07 (#15) against Table 1 "Cohesive energies",
96//!   chapter 3, p. 50 of the 8th edition (Wiley, 2005, ISBN 0-471-41526-X;
97//!   edition, publisher and ISBN read from the title and copyright pages of
98//!   the copy used, see `docs/data-provenance.md`). The table prints each
99//!   element in kJ/mol, eV/atom and kcal/mol ("energy required to form
100//!   separated neutral atoms in their ground electronic state from the solid
101//!   at 0 K at 1 atm. The data were supplied by Prof. Leo Brewer."); the
102//!   eV/atom value is used, to the digits printed. All 35 populated entries
103//!   were compared: 33 equal the printed value, and Na (was 1.11) and K (was
104//!   0.93) were set to the printed 1.113 and 0.934. The table also prints
105//!   values for elements left `None` here (among them N, O, P, S, Sc, Mn,
106//!   Se, the rare gases and most lanthanides and actinides); those defaults
107//!   stay unset, since #15 audits the populated entries only. Earlier
108//!   cross-check, kept as a test: L. Brewer, "The cohesive energies of the
109//!   elements", report LBL-3720 (1975), Table I, kcal/gram-atom
110//!   (<https://www.osti.gov/servlets/purl/7187973>).
111//! * **Displacement energy** (`default_displacement_ev`): threshold
112//!   displacement energies as recommended in ASTM E521 ("Standard Practice for
113//!   Investigating the Effects of Neutron Radiation Damage Using Charged-Particle
114//!   Irradiation"), the convention underlying the NRT damage model
115//!   (Norgett, Robinson, Torrens, Nucl. Eng. Des. 33, 50 (1975)). These are
116//!   conventions for damage accounting, not measured properties of a specific
117//!   crystal direction. `None` where no convention value is recorded.
118//!   **Not verified**: the standard could not be reached (see
119//!   `docs/data-provenance.md`). The values are also not those of the
120//!   Greenwood-Smither compilation used by SPECTER and NJOY (they differ for
121//!   Al, Ti, Cu, Nb and Ag), which is not E521 either.
122
123/// One element of the periodic table with its default data.
124#[derive(Debug, Clone, Copy, PartialEq)]
125pub struct Element {
126    /// Atomic number.
127    pub z: u8,
128    /// Element symbol, e.g. `"Si"`.
129    pub symbol: &'static str,
130    /// Standard (conventional) atomic weight in g/mol; see the module docs.
131    pub atomic_weight: f64,
132    /// True if `atomic_weight` is the mass number of the longest-lived isotope
133    /// because IUPAC publishes no standard atomic weight for the element.
134    pub weight_is_mass_number: bool,
135    /// Default solid density in g/cm³, if one is defined; see the module docs.
136    pub density_g_cm3: Option<f64>,
137    /// Default surface binding energy `E_s` in eV (cohesive-energy convention).
138    pub default_surface_binding_ev: Option<f64>,
139    /// Default displacement energy `E_d` in eV (ASTM E521 convention).
140    pub default_displacement_ev: Option<f64>,
141}
142
143impl Element {
144    /// Atomic mass in kg: `atomic_weight` times the atomic mass constant.
145    ///
146    /// (The molar-mass constant differs from 1 g/mol by about 1e-9, CODATA
147    /// 2022 `M_u` = 1.000 000 001 05e-3 kg/mol; the standard-atomic-weight
148    /// uncertainty is far larger.)
149    pub fn mass_kg(&self) -> f64 {
150        self.atomic_weight * crate::constants::ATOMIC_MASS_UNIT
151    }
152
153    /// Default density in kg/m³, if defined.
154    pub fn density_kg_m3(&self) -> Option<f64> {
155        self.density_g_cm3.map(crate::units::g_cm3_to_kg_m3)
156    }
157}
158
159/// Number of elements in the table.
160pub const NUM_ELEMENTS: usize = 92;
161
162/// Look an element up by atomic number (1..=92).
163pub fn element(z: u8) -> Option<&'static Element> {
164    if z == 0 {
165        return None;
166    }
167    ELEMENTS.get(usize::from(z) - 1)
168}
169
170/// Look an element up by case-sensitive symbol (`"Si"`, not `"si"`).
171pub fn element_by_symbol(symbol: &str) -> Option<&'static Element> {
172    ELEMENTS.iter().find(|e| e.symbol == symbol)
173}
174
175const fn el(
176    z: u8,
177    symbol: &'static str,
178    atomic_weight: f64,
179    weight_is_mass_number: bool,
180    density_g_cm3: Option<f64>,
181    default_surface_binding_ev: Option<f64>,
182    default_displacement_ev: Option<f64>,
183) -> Element {
184    Element {
185        z,
186        symbol,
187        atomic_weight,
188        weight_is_mass_number,
189        density_g_cm3,
190        default_surface_binding_ev,
191        default_displacement_ev,
192    }
193}
194
195/// The table, indexed by `Z - 1`.
196// Sn's cohesive energy (3.14 eV) is data, not pi.
197#[allow(clippy::approx_constant)]
198pub static ELEMENTS: [Element; NUM_ELEMENTS] = [
199    el(1, "H", 1.008, false, None, None, None),
200    el(2, "He", 4.002602, false, None, None, None),
201    el(3, "Li", 6.94, false, Some(0.534), Some(1.63), None),
202    el(4, "Be", 9.0121831, false, Some(1.848), Some(3.32), None),
203    el(5, "B", 10.81, false, Some(2.34), Some(5.81), None),
204    el(6, "C", 12.011, false, Some(2.267), Some(7.37), None),
205    el(7, "N", 14.007, false, None, None, None),
206    el(8, "O", 15.999, false, None, None, None),
207    el(9, "F", 18.998403162, false, None, None, None),
208    el(10, "Ne", 20.1797, false, None, None, None),
209    el(11, "Na", 22.98976928, false, Some(0.971), Some(1.113), None),
210    el(12, "Mg", 24.305, false, Some(1.738), Some(1.51), None),
211    el(
212        13,
213        "Al",
214        26.9815384,
215        false,
216        Some(2.6989),
217        Some(3.39),
218        Some(25.0),
219    ),
220    el(14, "Si", 28.085, false, Some(2.329), Some(4.63), None),
221    el(15, "P", 30.973761998, false, Some(1.82), None, None),
222    el(16, "S", 32.06, false, Some(2.07), None, None),
223    el(17, "Cl", 35.45, false, None, None, None),
224    el(18, "Ar", 39.95, false, None, None, None),
225    el(19, "K", 39.0983, false, Some(0.862), Some(0.934), None),
226    el(20, "Ca", 40.078, false, Some(1.55), Some(1.84), None),
227    el(21, "Sc", 44.955907, false, Some(2.989), None, None),
228    el(22, "Ti", 47.867, false, Some(4.54), Some(4.85), Some(30.0)),
229    el(23, "V", 50.9415, false, Some(6.11), Some(5.31), Some(40.0)),
230    el(24, "Cr", 51.9961, false, Some(7.20), Some(4.1), Some(40.0)),
231    el(25, "Mn", 54.938043, false, Some(7.44), None, None),
232    el(26, "Fe", 55.845, false, Some(7.874), Some(4.28), Some(40.0)),
233    el(
234        27,
235        "Co",
236        58.933194,
237        false,
238        Some(8.90),
239        Some(4.39),
240        Some(40.0),
241    ),
242    el(
243        28,
244        "Ni",
245        58.6934,
246        false,
247        Some(8.902),
248        Some(4.44),
249        Some(40.0),
250    ),
251    el(29, "Cu", 63.546, false, Some(8.96), Some(3.49), Some(30.0)),
252    el(30, "Zn", 65.38, false, Some(7.133), Some(1.35), None),
253    el(31, "Ga", 69.723, false, Some(5.904), Some(2.81), None),
254    el(32, "Ge", 72.630, false, Some(5.323), Some(3.85), None),
255    el(33, "As", 74.921595, false, Some(5.73), Some(2.96), None),
256    el(34, "Se", 78.971, false, Some(4.79), None, None),
257    el(35, "Br", 79.904, false, None, None, None),
258    el(36, "Kr", 83.798, false, None, None, None),
259    el(37, "Rb", 85.4678, false, Some(1.532), None, None),
260    el(38, "Sr", 87.62, false, Some(2.54), None, None),
261    el(39, "Y", 88.905838, false, Some(4.469), None, None),
262    el(40, "Zr", 91.222, false, Some(6.506), Some(6.25), Some(40.0)),
263    el(
264        41,
265        "Nb",
266        92.90637,
267        false,
268        Some(8.57),
269        Some(7.57),
270        Some(60.0),
271    ),
272    el(42, "Mo", 95.95, false, Some(10.22), Some(6.82), Some(60.0)),
273    el(43, "Tc", 98.0, true, Some(11.50), None, None),
274    el(44, "Ru", 101.07, false, Some(12.41), None, None),
275    el(45, "Rh", 102.90549, false, Some(12.41), None, None),
276    el(46, "Pd", 106.42, false, Some(12.02), Some(3.89), None),
277    el(
278        47,
279        "Ag",
280        107.8682,
281        false,
282        Some(10.50),
283        Some(2.95),
284        Some(30.0),
285    ),
286    el(48, "Cd", 112.414, false, Some(8.65), Some(1.16), None),
287    el(49, "In", 114.818, false, Some(7.31), Some(2.52), None),
288    el(50, "Sn", 118.710, false, Some(7.31), Some(3.14), None),
289    el(51, "Sb", 121.760, false, Some(6.691), Some(2.75), None),
290    el(52, "Te", 127.60, false, Some(6.24), None, None),
291    el(53, "I", 126.90447, false, Some(4.93), None, None),
292    el(54, "Xe", 131.293, false, None, None, None),
293    el(55, "Cs", 132.90545196, false, Some(1.873), None, None),
294    el(56, "Ba", 137.327, false, Some(3.5), None, None),
295    el(57, "La", 138.90547, false, Some(6.145), None, None),
296    el(58, "Ce", 140.116, false, Some(6.770), None, None),
297    el(59, "Pr", 140.90766, false, Some(6.773), None, None),
298    el(60, "Nd", 144.242, false, Some(7.008), None, None),
299    el(61, "Pm", 145.0, true, Some(7.264), None, None),
300    el(62, "Sm", 150.36, false, Some(7.52), None, None),
301    el(63, "Eu", 151.964, false, Some(5.244), None, None),
302    el(64, "Gd", 157.249, false, Some(7.901), None, None),
303    el(65, "Tb", 158.925354, false, Some(8.23), None, None),
304    el(66, "Dy", 162.500, false, Some(8.551), None, None),
305    el(67, "Ho", 164.930329, false, Some(8.795), None, None),
306    el(68, "Er", 167.259, false, Some(9.066), None, None),
307    el(69, "Tm", 168.934219, false, Some(9.321), None, None),
308    el(70, "Yb", 173.045, false, Some(6.966), None, None),
309    el(71, "Lu", 174.96669, false, Some(9.841), None, None),
310    el(72, "Hf", 178.486, false, Some(13.31), None, None),
311    el(
312        73,
313        "Ta",
314        180.94788,
315        false,
316        Some(16.654),
317        Some(8.1),
318        Some(90.0),
319    ),
320    el(74, "W", 183.84, false, Some(19.3), Some(8.9), Some(90.0)),
321    el(75, "Re", 186.207, false, Some(21.02), None, None),
322    el(76, "Os", 190.23, false, Some(22.57), None, None),
323    el(77, "Ir", 192.217, false, Some(22.42), None, None),
324    el(78, "Pt", 195.084, false, Some(21.45), Some(5.84), None),
325    el(
326        79,
327        "Au",
328        196.966570,
329        false,
330        Some(19.30),
331        Some(3.81),
332        Some(30.0),
333    ),
334    el(80, "Hg", 200.592, false, None, None, None),
335    el(81, "Tl", 204.38, false, Some(11.85), None, None),
336    el(82, "Pb", 207.2, false, Some(11.35), Some(2.03), Some(25.0)),
337    el(83, "Bi", 208.98040, false, Some(9.747), None, None),
338    el(84, "Po", 209.0, true, Some(9.32), None, None),
339    el(85, "At", 210.0, true, None, None, None),
340    el(86, "Rn", 222.0, true, None, None, None),
341    el(87, "Fr", 223.0, true, None, None, None),
342    el(88, "Ra", 226.0, true, Some(5.0), None, None),
343    el(89, "Ac", 227.0, true, Some(10.07), None, None),
344    el(90, "Th", 232.0377, false, Some(11.72), None, None),
345    el(91, "Pa", 231.03588, false, Some(15.37), None, None),
346    el(92, "U", 238.02891, false, Some(18.95), None, None),
347];
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn table_is_ordered_and_complete() {
355        for (i, e) in ELEMENTS.iter().enumerate() {
356            assert_eq!(usize::from(e.z), i + 1);
357            assert!(e.atomic_weight > 0.9);
358            if let Some(d) = e.density_g_cm3 {
359                assert!(d > 0.0 && d < 30.0);
360            }
361        }
362        assert_eq!(element(14).unwrap().symbol, "Si");
363        assert_eq!(element(92).unwrap().symbol, "U");
364        assert!(element(0).is_none() && element(93).is_none());
365        assert_eq!(element_by_symbol("Ga").unwrap().z, 31);
366        assert!(element_by_symbol("Xx").is_none());
367    }
368
369    #[test]
370    fn mass_number_flag_is_exactly_the_elements_without_standard_weight() {
371        // Tc, Pm, Po, At, Rn, Fr, Ra, Ac have no CIAAW standard atomic weight.
372        let flagged: Vec<u8> = ELEMENTS
373            .iter()
374            .filter(|e| e.weight_is_mass_number)
375            .map(|e| e.z)
376            .collect();
377        assert_eq!(flagged, [43, 61, 84, 85, 86, 87, 88, 89]);
378        // A mass number is an integer.
379        for e in ELEMENTS.iter().filter(|e| e.weight_is_mass_number) {
380            assert_eq!(e.atomic_weight, e.atomic_weight.trunc());
381        }
382    }
383
384    #[test]
385    fn symbols_are_unique() {
386        for (i, a) in ELEMENTS.iter().enumerate() {
387            for b in &ELEMENTS[i + 1..] {
388                assert_ne!(a.symbol, b.symbol);
389            }
390        }
391    }
392
393    fn by(symbol: &str) -> &'static Element {
394        element_by_symbol(symbol).unwrap()
395    }
396
397    /// Standard atomic weights as printed by CIAAW, "Standard Atomic Weights
398    /// 2024", <https://www.ciaaw.org/atomic-weights.htm> (read 2026-10-06),
399    /// to the digits printed there. For interval elements the value is the
400    /// conventional (abridged) one of
401    /// <https://www.ciaaw.org/abridged-atomic-weights.htm>. That value need
402    /// not lie inside the interval (O: 15.999 against [15.999 03, 15.999 77]);
403    /// the 2021 report (Prohaska et al. 2022, text above Table 1) defines its
404    /// printed uncertainty as the smallest symmetric one covering the
405    /// interval, so the check is `value +- uncertainty` contains the interval.
406    #[test]
407    fn atomic_weights_match_ciaaw_spot_values() {
408        // Single-valued standard weights, value(uncertainty) as printed.
409        for (sym, w) in [
410            ("P", 30.973_761_998), // 30.973 761 998(5)
411            ("Ga", 69.723),        // 69.723(1)
412            ("As", 74.921_595),    // 74.921 595(6)
413            ("Au", 196.966_570),   // 196.966 570(4)
414            // Corrected in #15 (were 44.955908, 88.90584, 91.224, 157.25, 174.9668).
415            ("Sc", 44.955_907), // 44.955 907(4)
416            ("Y", 88.905_838),  // 88.905 838(2)
417            ("Zr", 91.222),     // 91.222(3), CIAAW 2024 revision
418            ("Gd", 157.249),    // 157.249(2), CIAAW 2024 revision
419            ("Lu", 174.966_69), // 174.966 69(5), CIAAW 2024 revision
420        ] {
421            assert_eq!(by(sym).atomic_weight, w, "{sym}");
422            assert!(!by(sym).weight_is_mass_number, "{sym}");
423        }
424        // Interval elements: (conventional value +- its abridged uncertainty,
425        // standard-weight interval).
426        for (sym, w, u, lo, hi) in [
427            ("Si", 28.085, 0.001, 28.084, 28.086),
428            ("O", 15.999, 0.001, 15.999_03, 15.999_77),
429            ("B", 10.81, 0.02, 10.806, 10.821),
430            ("Ar", 39.95, 0.16, 39.792, 39.963),
431            ("Pb", 207.2, 1.1, 206.14, 207.94),
432        ] {
433            let e = by(sym);
434            assert_eq!(e.atomic_weight, w, "{sym}");
435            assert!(w - u <= lo && hi <= w + u, "{sym}");
436            assert!(!e.weight_is_mass_number, "{sym}");
437        }
438    }
439
440    /// Mass numbers of the longest-lived isotopes, CIAAW "Radioactive
441    /// elements", <https://www.ciaaw.org/radioactive-elements.htm> (NUBASE2020;
442    /// read 2026-10-06). Tc is listed with 97 and 98 as equally long-lived.
443    #[test]
444    fn mass_number_fallbacks_match_ciaaw() {
445        for (sym, a) in [
446            ("Tc", 98.0),
447            ("Pm", 145.0),
448            ("Po", 209.0),
449            ("At", 210.0),
450            ("Rn", 222.0),
451            ("Fr", 223.0),
452            ("Ra", 226.0),
453            ("Ac", 227.0),
454        ] {
455            assert_eq!(by(sym).atomic_weight, a, "{sym}");
456            assert!(by(sym).weight_is_mass_number, "{sym}");
457        }
458    }
459
460    /// `E_s` against the cited source: C. Kittel, *Introduction to Solid State
461    /// Physics*, 8th ed. (Wiley, 2005), ch. 3, Table 1 "Cohesive energies",
462    /// p. 50, eV/atom row, to the digits printed (read 2026-10-07, #15).
463    /// Every populated entry is listed, so a new or changed default fails
464    /// here until it is checked against the table.
465    #[test]
466    // Sn's cohesive energy (3.14 eV) is data, not pi.
467    #[allow(clippy::approx_constant)]
468    fn surface_binding_matches_kittel_8th_ed_table_1() {
469        let kittel: [(&str, f64); 35] = [
470            ("Li", 1.63),
471            ("Be", 3.32),
472            ("B", 5.81),
473            ("C", 7.37),
474            ("Na", 1.113), // corrected in #15 (was 1.11)
475            ("Mg", 1.51),
476            ("Al", 3.39),
477            ("Si", 4.63),
478            ("K", 0.934), // corrected in #15 (was 0.93)
479            ("Ca", 1.84),
480            ("Ti", 4.85),
481            ("V", 5.31),
482            ("Cr", 4.10),
483            ("Fe", 4.28),
484            ("Co", 4.39),
485            ("Ni", 4.44),
486            ("Cu", 3.49),
487            ("Zn", 1.35),
488            ("Ga", 2.81),
489            ("Ge", 3.85),
490            ("As", 2.96),
491            ("Zr", 6.25),
492            ("Nb", 7.57),
493            ("Mo", 6.82),
494            ("Pd", 3.89),
495            ("Ag", 2.95),
496            ("Cd", 1.16),
497            ("In", 2.52),
498            ("Sn", 3.14),
499            ("Sb", 2.75),
500            ("Ta", 8.10),
501            ("W", 8.90),
502            ("Pt", 5.84),
503            ("Au", 3.81),
504            ("Pb", 2.03),
505        ];
506        for (sym, ev) in kittel {
507            assert_eq!(by(sym).default_surface_binding_ev, Some(ev), "{sym}");
508        }
509        let populated = ELEMENTS
510            .iter()
511            .filter(|e| e.default_surface_binding_ev.is_some())
512            .count();
513        assert_eq!(populated, kittel.len());
514        // Kittel prints O 2.60 and P 3.43 eV/atom, but these defaults are
515        // unset by design (#15 audits populated entries only); the user sets
516        // E_s for them.
517        assert_eq!(by("O").default_surface_binding_ev, None);
518        assert_eq!(by("P").default_surface_binding_ev, None);
519    }
520
521    /// Cross-check of `E_s` against L. Brewer, LBL-3720 (1975), Table I,
522    /// atomization energy to the gaseous ground state at 0 K, kcal/gram-atom
523    /// (<https://www.osti.gov/servlets/purl/7187973>), a second source
524    /// beside the cited Kittel table (whose data Brewer supplied).
525    /// 1 kcal/mol = 4184 J / (e N_A) eV per atom (thermochemical calorie).
526    #[test]
527    fn surface_binding_cross_checks_against_brewer_lbl_3720() {
528        use crate::constants::{AVOGADRO, ELEMENTARY_CHARGE};
529        let kcal_ev = 4184.0 / (ELEMENTARY_CHARGE * AVOGADRO);
530        // Agree to the printed digit (half a unit in the last place, 0.005 eV).
531        for (sym, kcal) in [("Si", 106.7), ("Ga", 64.8), ("As", 68.2), ("Au", 87.96)] {
532            let es = by(sym).default_surface_binding_ev.unwrap();
533            assert!(
534                (es - kcal * kcal_ev).abs() <= 0.005,
535                "{sym}: {es} vs {}",
536                kcal * kcal_ev
537            );
538        }
539        // B (beta): Brewer 133 +- 3 kcal = 5.77 +- 0.13 eV; the stored 5.81 is
540        // Kittel's printed value (Table 1 also prints 134 kcal/mol for B),
541        // inside Brewer's uncertainty.
542        let b = by("B").default_surface_binding_ev.unwrap();
543        assert!((b - 133.0 * kcal_ev).abs() <= 3.0 * kcal_ev, "{b}");
544    }
545
546    /// Densities as printed in the X-Ray Data Booklet, LBNL/PUB-490 Rev. 3
547    /// (2009), Table 5-2, pp. 5-5 to 5-8 (<https://xdb.lbl.gov/xdb-new.pdf>,
548    /// read 2026-10-07; from the CRC Handbook, 80th ed.), g/cm³, to the
549    /// digits printed there. Superscripts give the temperature where it is
550    /// not 20 °C.
551    #[test]
552    fn densities_match_x_ray_data_booklet_spot_values() {
553        for (sym, rho) in [
554            ("B", 2.34),
555            ("P", 1.82),
556            ("Ga", 5.904), // 5.904^29.6
557            ("Cu", 8.96),
558            ("Sc", 2.989), // 2.989^25; reviewer-flagged, unchanged
559            // Corrected in #15 (were 5.727, 10.49, 1.93, 22.59, 19.1).
560            ("As", 5.73),
561            ("Ag", 10.50),
562            ("Cs", 1.873),
563            ("Os", 22.57),
564            ("U", 18.95), // printed "~18.95"
565        ] {
566            assert_eq!(by(sym).density_g_cm3, Some(rho), "{sym}");
567        }
568        // Au is printed only as "~19.3"; the stored 19.30 agrees to that
569        // precision, its second decimal is not verified.
570        let au = by("Au").density_g_cm3.unwrap();
571        assert!((au - 19.3).abs() < 0.05, "{au}");
572    }
573
574    /// Densities the X-Ray Data Booklet does not pin, checked against other
575    /// open primary compilations (read 2026-10-07; see the module docs and
576    /// `docs/data-provenance.md`), g/cm³.
577    #[test]
578    fn densities_match_other_primary_compilations() {
579        // NBS Circular 539 Vol. I (1953), p. 33: Au 19.302 at 25 °C from the
580        // lattice constant; the booklet prints "~19.3". Agrees to the stored
581        // digits.
582        let au = by("Au").density_g_cm3.unwrap();
583        assert!((au - 19.302).abs() < 0.005, "{au}");
584        // NBS Circular 539 Vol. V (1955), p. 20: Cr 7.200 at 25 °C from the
585        // lattice constant, the upper end of the booklet's 7.18-7.20.
586        // Corrected in #15 (was 7.15, outside that range).
587        let cr = by("Cr").density_g_cm3.unwrap();
588        assert_eq!(cr, 7.20);
589        assert!((7.18..=7.20).contains(&cr));
590        // Kirby and Salutsky, NAS-NS 3057 (1964), p. 3: Ra "approximately
591        // 5.0". Reviewer-flagged; unchanged.
592        assert_eq!(by("Ra").density_g_cm3, Some(5.0));
593        // Farr et al., LA-1545 (1953), Table 4: Ac 10.07 (fcc, a = 5.311 Å).
594        assert_eq!(by("Ac").density_g_cm3, Some(10.07));
595    }
596
597    /// The Ac entry is the crystal density of its fcc cell as LA-1545 prints
598    /// it: 4 atoms of mass number 227 in a cube of edge 5.311 Å, rounded to
599    /// the printed 10.07 g/cm³ (the report's a carries +- 0.010 Å).
600    #[test]
601    fn actinium_density_matches_its_lattice_constant() {
602        use crate::constants::AVOGADRO;
603        let ac = by("Ac");
604        let a_cm = 5.311e-8;
605        let rho = 4.0 * ac.atomic_weight / (AVOGADRO * a_cm * a_cm * a_cm);
606        assert!((rho - ac.density_g_cm3.unwrap()).abs() < 0.01, "{rho}");
607    }
608
609    /// Mn is not pinned by any source read so far. The X-Ray Data Booklet
610    /// prints the range 7.21-7.44; NBS Monograph 25, Section 17 (1980), p. 50,
611    /// prints the alpha-Mn X-ray density "(calculated) 7.475 g/cm³" at 25 °C
612    /// from a = 8.9121 Å and Z = 58 atoms per cell (volume printed as
613    /// 707.85 ų). That page is reproduced here from its printed lattice
614    /// constant with CODATA 2022 N_A and the CIAAW weight; it lies above the
615    /// booklet's range, so the stored upper end, 7.44, is kept and stays
616    /// unverified.
617    #[test]
618    fn manganese_density_is_bounded_but_not_pinned() {
619        use crate::constants::AVOGADRO;
620        let mn = by("Mn");
621        let a_cm: f64 = 8.9121e-8;
622        assert!((a_cm * a_cm * a_cm * 1e24 - 707.85).abs() < 0.005);
623        let rho_x = 58.0 * mn.atomic_weight / (AVOGADRO * a_cm * a_cm * a_cm);
624        assert!((rho_x - 7.475).abs() < 0.0005, "{rho_x}");
625        let stored = mn.density_g_cm3.unwrap();
626        assert_eq!(stored, 7.44);
627        assert!((7.21..=7.44).contains(&stored));
628        assert!(rho_x > 7.44);
629    }
630
631    /// Si: the CODATA 2022 "molar volume of silicon", V_m(Si) =
632    /// 1.205 883 199e-5 m³/mol
633    /// (<https://physics.nist.gov/cuu/Constants/Table/allascii.txt>), which
634    /// equals N_A a³ / 8 for the listed lattice parameter a = 5.431 020 511e-10
635    /// m, gives the crystal density M(Si) / V_m(Si). For every weight in the
636    /// CIAAW standard interval [28.084, 28.086] it rounds to the stored 2.329
637    /// g/cm³; the X-Ray Data Booklet prints 2.33 at 25 °C.
638    #[test]
639    fn silicon_density_matches_codata_molar_volume() {
640        use crate::constants::AVOGADRO;
641        let a = 5.431_020_511e-10; // m
642        let v_m = 1.205_883_199e-5; // m^3/mol
643        assert!((AVOGADRO * a * a * a / 8.0 / v_m - 1.0).abs() < 1e-9);
644        let si = by("Si").density_g_cm3.unwrap();
645        assert_eq!(si, 2.329);
646        for m_g in [28.084, 28.085, 28.086] {
647            // g/mol / (m^3/mol) = g/m^3; 1e-6 converts to g/cm^3.
648            let rho = m_g / v_m * 1e-6;
649            assert!((rho - si).abs() < 0.0005, "{m_g}: {rho}");
650        }
651        assert!((si - 2.33).abs() < 0.005);
652    }
653
654    /// Defaults that are unset by design stay unset. `E_d` values are not
655    /// pinned: ASTM E521 could not be reached, so no populated value is
656    /// source-verified.
657    #[test]
658    fn unset_defaults_are_preserved() {
659        // Gases have no solid density.
660        for sym in ["H", "N", "O", "F", "Cl", "Ar", "Rn"] {
661            assert_eq!(by(sym).density_g_cm3, None, "{sym}");
662        }
663        // No E_d convention value is recorded for the semiconductor and dopant
664        // elements; the user must set one.
665        for sym in ["Si", "O", "Ga", "As", "B", "P"] {
666            assert_eq!(by(sym).default_displacement_ev, None, "{sym}");
667        }
668    }
669}