1#![warn(missing_docs)]
2use std::fmt;
18
19pub mod armi;
20pub mod data;
21pub mod dialects;
22pub mod fgr15;
23pub mod irdff;
24pub mod particles;
25pub mod rxname;
26
27pub use dialects::DialectError;
28pub use particles::Error as ParticlesError;
29pub use rxname::Error as RxnameError;
30
31pub const ELEMENTS: [&str; 119] = [
33 "", "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S",
34 "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge",
35 "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd",
36 "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd",
37 "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg",
38 "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm",
39 "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn",
40 "Nh", "Fl", "Mc", "Lv", "Ts", "Og",
41];
42
43pub type Result<T> = std::result::Result<T, Error>;
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48#[non_exhaustive]
49pub enum Error {
50 BadZ(u32),
52 BadA {
54 z: u32,
56 a: u32,
58 },
59 MassNumberTooLarge(u32),
61 BadState(u32),
63 MissingMassNumber(String),
65 BadNumber(String),
67 UnknownElement(String),
69}
70
71impl fmt::Display for Error {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 Error::BadZ(z) => write!(f, "atomic number {z} out of range 1..=118"),
75 Error::BadA { z, a } => write!(f, "mass number {a} < atomic number {z}"),
76 Error::MassNumberTooLarge(a) => write!(f, "mass number {a} > 999 unsupported"),
77 Error::BadState(s) => write!(f, "metastable state {s} > 9 unsupported"),
78 Error::MissingMassNumber(s) => write!(f, "no mass number in name `{s}`"),
79 Error::BadNumber(s) => write!(f, "invalid numeric component `{s}`"),
80 Error::UnknownElement(s) => write!(f, "unknown element symbol `{s}`"),
81 }
82 }
83}
84
85impl std::error::Error for Error {}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
94pub struct NuclideId(u32);
95
96impl NuclideId {
97 pub const fn new(z: u32, a: u32, state: u32) -> Result<Self> {
103 if z == 0 || z > 118 {
104 return Err(Error::BadZ(z));
105 }
106 if a < z {
107 return Err(Error::BadA { z, a });
108 }
109 if a > 999 {
110 return Err(Error::MassNumberTooLarge(a));
111 }
112 if state > 9 {
113 return Err(Error::BadState(state));
114 }
115 Ok(Self((z * 1000 + a) * 10_000 + state))
116 }
117
118 pub const fn from_nucid(nucid: u32) -> Self {
131 Self(nucid)
132 }
133
134 pub const fn try_from_nucid(nucid: u32) -> Result<Self> {
145 let tail = nucid % 10_000;
146 if tail > 9 {
147 return Err(Error::BadState(tail));
148 }
149 let z = nucid / 10_000_000;
150 let a = (nucid % 10_000_000) / 10_000;
151 let state = nucid % 10;
152 if z == 0 || z > 118 {
153 return Err(Error::BadZ(z));
154 }
155 if a < z {
156 return Err(Error::BadA { z, a });
157 }
158 if a > 999 {
159 return Err(Error::MassNumberTooLarge(a));
160 }
161 if state > 9 {
162 return Err(Error::BadState(state));
163 }
164 Ok(Self(nucid))
165 }
166
167 pub const fn is_valid(&self) -> bool {
173 if self.0 % 10_000 > 9 {
174 return false;
175 }
176 let z = self.z();
177 let a = self.a();
178 let state = self.state();
179 z != 0 && z <= 118 && a >= z && a <= 999 && state <= 9
180 }
181
182 pub const fn nucid(&self) -> u32 {
184 self.0
185 }
186
187 pub const fn z(&self) -> u32 {
189 self.0 / 10_000_000
190 }
191
192 pub const fn a(&self) -> u32 {
194 (self.0 % 10_000_000) / 10_000
195 }
196
197 pub const fn state(&self) -> u32 {
199 self.0 % 10
200 }
201
202 pub const fn zzaaam(&self) -> u32 {
204 self.z() * 10_000 + self.a() * 10 + self.state()
205 }
206
207 pub fn from_zzaaam(v: u32) -> Result<Self> {
209 let state = v % 10;
210 let rest = v / 10;
211 let a = rest % 1_000;
212 let z = rest / 1_000;
213 Self::new(z, a, state)
214 }
215
216 pub fn from_name(name: &str) -> Result<Self> {
222 let trimmed = name.trim();
223 let cleaned: String = trimmed.chars().filter(|&c| c != '-').collect();
224 let upper = cleaned.to_ascii_uppercase();
225 let digit_start = upper
226 .find(|c: char| c.is_ascii_digit())
227 .ok_or_else(|| Error::MissingMassNumber(trimmed.to_string()))?;
228 let sym_upper = &upper[..digit_start];
229 let rest = &upper[digit_start..];
230
231 let sym = canonicalize_symbol(sym_upper);
232 let z = element_z(&sym).ok_or_else(|| Error::UnknownElement(sym_upper.to_string()))?;
233
234 let (a_str, state_str) = if let Some((head, tail)) = rest.split_once('_') {
237 let tail = tail.strip_prefix('M').unwrap_or(tail);
239 (head, Some(tail))
240 } else if let Some((head, tail)) = rest.split_once('M') {
241 (head, Some(tail))
243 } else {
244 (rest, None)
245 };
246
247 let a: u32 = a_str
248 .parse()
249 .map_err(|_| Error::BadNumber(a_str.to_string()))?;
250 let state = match state_str {
251 None => 0,
252 Some("") => 1,
253 Some(n) => n.parse().map_err(|_| Error::BadNumber(format!("M{n}")))?,
254 };
255
256 Self::new(z, a, state)
257 }
258
259 pub fn to_name(&self) -> String {
268 if self.is_valid() {
269 let sym = ELEMENTS[self.z() as usize];
271 match self.state() {
272 0 => format!("{}{}", sym, self.a()),
273 s => format!("{}{}_m{}", sym, self.a(), s),
274 }
275 } else {
276 format!("Z{}A{}[m{}]", self.z(), self.a(), self.state())
277 }
278 }
279}
280
281impl fmt::Display for NuclideId {
282 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283 f.write_str(&self.to_name())
284 }
285}
286
287impl std::str::FromStr for NuclideId {
288 type Err = Error;
289 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
290 NuclideId::from_name(s)
291 }
292}
293
294pub fn element_symbol(z: u32) -> Option<&'static str> {
296 ELEMENTS
297 .get(z as usize)
298 .and_then(|s| if s.is_empty() { None } else { Some(*s) })
299}
300
301pub fn element_z(symbol: &str) -> Option<u32> {
303 ELEMENTS.iter().position(|s| *s == symbol).map(|z| z as u32)
304}
305
306fn canonicalize_symbol(sym: &str) -> String {
308 let mut chars = sym.chars();
309 let mut out = String::with_capacity(sym.len());
310 if let Some(first) = chars.next() {
311 out.extend(first.to_uppercase());
312 }
313 out.push_str(&chars.as_str().to_lowercase());
314 out
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn parse_ground_states() {
323 assert_eq!(NuclideId::from_name("U235").unwrap().nucid(), 922_350_000);
324 assert_eq!(NuclideId::from_name("H1").unwrap().nucid(), 10_010_000);
325 assert_eq!(
326 NuclideId::from_name("Og294").unwrap().nucid(),
327 1_182_940_000
328 );
329 }
330
331 #[test]
332 fn parse_metastables() {
333 let am = NuclideId::from_name("Am242_m1").unwrap();
334 assert_eq!((am.z(), am.a(), am.state()), (95, 242, 1));
335 assert_eq!(am.nucid(), 952_420_001);
336
337 let ba = NuclideId::from_name("Ba137m").unwrap();
338 assert_eq!((ba.z(), ba.a(), ba.state()), (56, 137, 1));
339 assert_eq!(ba.zzaaam(), 561_371);
340 }
341
342 #[test]
343 fn round_trip_display() {
344 for name in ["U235", "H1", "Am242_m1", "Pu239"] {
345 assert_eq!(NuclideId::from_name(name).unwrap().to_name(), name);
346 }
347 }
348
349 #[test]
350 fn zzaaam_round_trip() {
351 let u5 = NuclideId::from_name("U235").unwrap();
352 assert_eq!(u5.zzaaam(), 922_350);
353 assert_eq!(
354 NuclideId::from_zzaaam(922_350).map(|n| n.to_name()),
355 Ok("U235".to_string())
356 );
357 }
358
359 #[test]
360 fn rejects_bad_input() {
361 assert!(matches!(
362 NuclideId::from_name("Xx999"),
363 Err(Error::UnknownElement(_))
364 ));
365 assert!(matches!(
366 NuclideId::from_name("U"),
367 Err(Error::MissingMassNumber(_))
368 ));
369 assert!(matches!(NuclideId::new(0, 1, 0), Err(Error::BadZ(0))));
370 assert!(matches!(NuclideId::new(6, 3, 0), Err(Error::BadA { .. })));
371 }
372
373 #[test]
374 fn elements_table_sanity() {
375 assert_eq!(element_z("U"), Some(92));
376 assert_eq!(element_symbol(92), Some("U"));
377 assert_eq!(element_z("Xx"), None);
378 }
379
380 #[test]
381 fn rejects_mass_number_overflow() {
382 assert!(matches!(
383 NuclideId::new(92, 999_999, 0),
384 Err(Error::MassNumberTooLarge(999_999))
385 ));
386 assert!(matches!(
387 NuclideId::from_name("U999999"),
388 Err(Error::MassNumberTooLarge(999_999))
389 ));
390 }
391
392 #[test]
393 fn parses_pyne_normalized_forms() {
394 assert_eq!(NuclideId::from_name("U-235").unwrap().nucid(), 922_350_000);
395 assert_eq!(NuclideId::from_name("u235").unwrap().nucid(), 922_350_000);
396 assert_eq!(NuclideId::from_name("Am242M").unwrap().nucid(), 952_420_001);
397 assert_eq!(
398 NuclideId::from_name("Am-242M").unwrap().nucid(),
399 952_420_001
400 );
401 }
402
403 #[test]
404 fn try_from_nucid_validates_raw_integers() {
405 assert_eq!(
407 NuclideId::try_from_nucid(922_350_000).unwrap(),
408 NuclideId::from_nucid(922_350_000)
409 );
410 assert!(NuclideId::try_from_nucid(10_010_000).unwrap().is_valid());
411 assert!(NuclideId::from_nucid(922_350_000).is_valid());
412 assert!(!NuclideId::from_nucid(0).is_valid());
414 assert!(matches!(NuclideId::try_from_nucid(0), Err(Error::BadZ(0))));
415 assert!(matches!(
416 NuclideId::try_from_nucid(u32::MAX),
417 Err(Error::BadState(7295))
418 ));
419 assert!(matches!(
420 NuclideId::try_from_nucid(1_190_000_000),
421 Err(Error::BadZ(119))
422 ));
423 assert!(matches!(
425 NuclideId::try_from_nucid(920_050_000),
426 Err(Error::BadA { z: 92, a: 5 })
427 ));
428 assert!(matches!(
429 NuclideId::try_from_nucid(922_350_010),
430 Err(Error::BadState(10))
431 ));
432 assert!(!NuclideId::from_nucid(920_050_000).is_valid());
433 assert!(!NuclideId::from_nucid(922_350_010).is_valid());
434 }
435
436 #[test]
437 fn to_name_falls_back_for_invalid_raw_ids() {
438 assert_eq!(NuclideId::from_nucid(0).to_name(), "Z0A0[m0]");
440 assert_eq!(
441 NuclideId::from_nucid(u32::MAX).to_name(),
442 format!(
443 "Z{}A{}[m{}]",
444 NuclideId::from_nucid(u32::MAX).z(),
445 NuclideId::from_nucid(u32::MAX).a(),
446 NuclideId::from_nucid(u32::MAX).state()
447 )
448 );
449 assert_eq!(NuclideId::from_nucid(920_050_000).to_name(), "Z92A5[m0]");
450 assert_eq!(NuclideId::from_nucid(922_350_010).to_name(), "Z92A235[m0]");
451 for raw in [0, u32::MAX, 920_050_000, 1_190_000_000] {
453 let name = NuclideId::from_nucid(raw).to_name();
454 assert!(NuclideId::from_name(&name).is_err(), "{name}");
455 assert_eq!(
456 NuclideId::from_nucid(raw).to_string(),
457 name,
458 "Display follows to_name"
459 );
460 }
461 }
462
463 #[test]
464 fn every_validated_id_round_trips_through_name() {
465 let mut ids = vec![
467 NuclideId::new(1, 1, 0).unwrap(),
468 NuclideId::new(92, 235, 0).unwrap(),
469 NuclideId::new(95, 242, 9).unwrap(),
470 NuclideId::new(118, 294, 0).unwrap(),
471 NuclideId::from_name("Am242_m1").unwrap(),
472 NuclideId::from_zzaaam(922_350).unwrap(),
473 NuclideId::try_from_nucid(922_350_000).unwrap(),
474 ];
475 for z in [1, 2, 26, 92, 95, 118] {
476 for a in [z, z + 1, 999] {
477 for s in [0, 1, 9] {
478 if let Ok(id) = NuclideId::new(z, a.min(999), s) {
479 ids.push(id);
480 }
481 }
482 }
483 }
484 for id in ids {
485 assert!(id.is_valid());
486 assert_eq!(NuclideId::from_name(&id.to_name()).unwrap(), id);
487 }
488 }
489
490 #[test]
491 fn error_arms_construct_and_display() {
492 assert!(matches!(
493 NuclideId::new(92, 235, 10),
494 Err(Error::BadState(10))
495 ));
496 assert!(matches!(
497 NuclideId::from_name("U235_mX"),
498 Err(Error::BadNumber(_))
499 ));
500 assert!(NuclideId::from_name("U235_mX")
501 .unwrap_err()
502 .to_string()
503 .contains("MX"));
504 assert!(matches!(
505 NuclideId::from_name("U23X5"),
506 Err(Error::BadNumber(_))
507 ));
508 assert!(!Error::BadState(10).to_string().is_empty());
509 assert!(!Error::BadNumber("MX".to_string()).to_string().is_empty());
510 }
511}