Skip to main content

nereids_core/
elements.rs

1//! Element and isotope reference data.
2//!
3//! Provides element symbols, names, and natural isotopic abundances
4//! for all elements relevant to neutron resonance imaging.
5//!
6//! Delegates to the `endf-mat` crate for raw lookup data.
7
8use crate::error::NereidsError;
9use crate::types::Isotope;
10
11/// Element symbol lookup by atomic number Z.
12///
13/// Returns `Some("n")` for Z=0 (neutron). Returns `None` for Z > 118.
14pub fn element_symbol(z: u32) -> Option<&'static str> {
15    endf_mat::element_symbol(z)
16}
17
18/// Element name lookup by atomic number Z.
19pub fn element_name(z: u32) -> Option<&'static str> {
20    endf_mat::element_name(z)
21}
22
23/// Parse an isotope string like "U-238", "Pu-239", "Fe-56".
24///
25/// Returns `None` if the string cannot be parsed or validation fails.
26pub fn parse_isotope_str(s: &str) -> Option<Isotope> {
27    let parts: Vec<&str> = s.split('-').collect();
28    if parts.len() != 2 {
29        return None;
30    }
31    let symbol = parts[0].trim();
32    let a: u32 = parts[1].trim().parse().ok()?;
33    let z = symbol_to_z(symbol)?;
34    Isotope::new(z, a).ok()
35}
36
37/// Look up atomic number Z from element symbol (case-insensitive).
38pub fn symbol_to_z(symbol: &str) -> Option<u32> {
39    endf_mat::symbol_to_z(symbol)
40}
41
42/// Natural isotopic abundance for a given isotope, as a fraction (0.0 to 1.0).
43///
44/// Returns `None` if the isotope is not in the database (e.g., synthetic isotopes).
45/// Data from IUPAC 2016 recommended values (via NIST).
46pub fn natural_abundance(isotope: &Isotope) -> Option<f64> {
47    endf_mat::natural_abundance(isotope.z(), isotope.a())
48}
49
50/// Get all naturally occurring isotopes for element Z.
51///
52/// Silently skips any isotopes that fail validation (should never happen
53/// for data from the `endf_mat` crate, but defensive nonetheless).
54pub fn natural_isotopes(z: u32) -> Vec<(Isotope, f64)> {
55    endf_mat::natural_isotopes(z)
56        .into_iter()
57        .filter_map(|(a, frac)| Isotope::new(z, a).ok().map(|iso| (iso, frac)))
58        .collect()
59}
60
61/// Get all isotopes with ENDF evaluations for element Z.
62///
63/// Unlike [`natural_isotopes`], this includes synthetic and transuranic
64/// isotopes (Tc, Pm, Np, Pu, Am, etc.) that have no natural abundance
65/// but do have evaluated nuclear data files.
66pub fn known_isotopes(z: u32) -> Vec<Isotope> {
67    endf_mat::known_isotopes(z)
68        .into_iter()
69        .filter_map(|a| Isotope::new(z, a).ok())
70        .collect()
71}
72
73/// Whether the ENDF/B-VIII.0 sublibrary has an evaluation for (Z, A).
74pub fn has_endf_evaluation(z: u32, a: u32) -> bool {
75    endf_mat::has_endf_evaluation(z, a)
76}
77
78/// Compute ENDF ZA identifier: Z * 1000 + A.
79pub fn za_from_isotope(isotope: &Isotope) -> u32 {
80    endf_mat::za(isotope.z(), isotope.a())
81}
82
83/// Parse ENDF ZA identifier back to an [`Isotope`].
84///
85/// # Errors
86/// Returns `NereidsError::InvalidParameter` when the ZA value produces
87/// an invalid isotope (Z > A, or A == 0 — e.g. ZA = 26000 for natural
88/// iron).  Callers should propagate or handle this gracefully instead of
89/// panicking, since real ENDF files may contain such entries.
90pub fn isotope_from_za(za: u32) -> Result<Isotope, NereidsError> {
91    Isotope::new(endf_mat::z_from_za(za), endf_mat::a_from_za(za))
92}
93
94/// Format isotope as standard string, e.g. "U-238".
95pub fn isotope_to_string(isotope: &Isotope) -> String {
96    match element_symbol(isotope.z()) {
97        Some(sym) => format!("{}-{}", sym, isotope.a()),
98        None => format!("Z{}-{}", isotope.z(), isotope.a()),
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_element_symbol() {
108        assert_eq!(element_symbol(1), Some("H"));
109        assert_eq!(element_symbol(92), Some("U"));
110        assert_eq!(element_symbol(26), Some("Fe"));
111        assert_eq!(element_symbol(0), Some("n"));
112    }
113
114    #[test]
115    fn test_parse_isotope_str() {
116        let u238 = parse_isotope_str("U-238").unwrap();
117        assert_eq!(u238.z(), 92);
118        assert_eq!(u238.a(), 238);
119
120        let fe56 = parse_isotope_str("Fe-56").unwrap();
121        assert_eq!(fe56.z(), 26);
122        assert_eq!(fe56.a(), 56);
123
124        assert!(parse_isotope_str("invalid").is_none());
125    }
126
127    #[test]
128    fn test_za_roundtrip() {
129        let iso = Isotope::new(92, 238).unwrap();
130        let za = za_from_isotope(&iso);
131        assert_eq!(za, 92238);
132        let back = isotope_from_za(za).unwrap();
133        assert_eq!(back, iso);
134    }
135
136    #[test]
137    fn test_isotope_from_za_natural_element_returns_error() {
138        // ZA=26000 → Z=26, A=0 (natural iron). A==0 fails validation.
139        let result = isotope_from_za(26000);
140        assert!(result.is_err());
141        assert!(result.unwrap_err().to_string().contains("must be positive"));
142    }
143
144    #[test]
145    fn test_isotope_from_za_invalid_z_greater_than_a() {
146        // Contrived ZA where Z > A (malformed data).
147        // ZA=999001 → Z=999, A=1 → Z > A.
148        let result = isotope_from_za(999001);
149        assert!(result.is_err());
150        assert!(result.unwrap_err().to_string().contains("cannot exceed"));
151    }
152
153    #[test]
154    fn test_natural_abundance() {
155        let u238 = Isotope::new(92, 238).unwrap();
156        let abund = natural_abundance(&u238).unwrap();
157        assert!((abund - 0.992742).abs() < 1e-6);
158    }
159
160    #[test]
161    fn test_natural_isotopes() {
162        let fe_isotopes = natural_isotopes(26);
163        assert_eq!(fe_isotopes.len(), 4);
164        let total: f64 = fe_isotopes.iter().map(|(_, a)| a).sum();
165        assert!((total - 1.0).abs() < 0.001);
166    }
167
168    #[test]
169    fn test_isotope_to_string() {
170        let iso = Isotope::new(92, 238).unwrap();
171        assert_eq!(isotope_to_string(&iso), "U-238");
172    }
173
174    #[test]
175    fn test_known_isotopes_plutonium() {
176        let pu = known_isotopes(94);
177        assert!(!pu.is_empty());
178        assert!(pu.iter().any(|iso| iso.a() == 239));
179    }
180
181    #[test]
182    fn test_known_isotopes_synthetic_element() {
183        // Tc has no natural isotopes but has ENDF evaluations
184        assert!(natural_isotopes(43).is_empty());
185        let tc = known_isotopes(43);
186        assert!(!tc.is_empty());
187    }
188
189    #[test]
190    fn test_known_isotopes_superset_of_natural() {
191        let natural: Vec<Isotope> = natural_isotopes(26)
192            .into_iter()
193            .map(|(iso, _)| iso)
194            .collect();
195        let known = known_isotopes(26);
196        for iso in &natural {
197            assert!(known.contains(iso));
198        }
199        // Fe-55 is in ENDF but not natural
200        assert!(known.iter().any(|iso| iso.a() == 55));
201    }
202
203    #[test]
204    fn test_has_endf_evaluation() {
205        assert!(has_endf_evaluation(94, 239));
206        assert!(!has_endf_evaluation(94, 999));
207    }
208}