use nucleide_material::Material;
use nucleide_nuclei::{armi::armi_name_to_nucid, NuclideId};
use crate::{Error, Result};
pub fn from_armi_mass_fracs<I, S>(pairs: I, density: Option<f64>) -> Result<Material>
where
I: IntoIterator<Item = (S, f64)>,
S: AsRef<str>,
{
let mut mat = Material::new();
for (key, mass) in pairs {
let id = armi_key_to_nucid(key.as_ref())?;
if !mass.is_finite() || mass < 0.0 {
return Err(Error::ArmiKey {
key: key.as_ref().trim().to_string(),
reason: format!("mass {mass} is not a non-negative finite value"),
});
}
mat.add_nuclide(id, mass);
}
mat.set_density(density);
Ok(mat)
}
fn armi_key_to_nucid(key: &str) -> Result<NuclideId> {
let t = key.trim();
if let Some(sym) = elemental_symbol(t).or_else(|| strip_db_prefix(t).and_then(elemental_symbol))
{
return Err(Error::ArmiKey {
key: t.to_string(),
reason: format!(
"elemental key `{sym}`: pass ARMI post-expansion nuclide mass fractions \
(material.massFrac); the emitter never reimplements \
expandElementalMassFracsToNuclides — expand first"
),
});
}
match bare_core(t).as_deref() {
Some("AM242") => {
return Err(Error::ArmiKey {
key: t.to_string(),
reason: "ambiguous bare `AM242` (ARMI means the m-state): pass `AM242M` for \
Am-242m or `AM242G` for ground explicitly"
.to_string(),
});
}
Some("AM242G") => return Ok(NuclideId::new(95, 242, 0)?),
_ => {}
}
armi_name_to_nucid(t)
.map_err(|e| Error::Nuclei(nucleide_nuclei::Error::BadNumber(format!("{t}: {e}"))))
}
fn elemental_symbol(t: &str) -> Option<String> {
if t.is_empty() || !t.bytes().all(|b| b.is_ascii_alphabetic()) {
return None;
}
let mut chars = t.chars();
let mut canon = String::with_capacity(t.len());
if let Some(first) = chars.next() {
canon.extend(first.to_uppercase());
}
canon.extend(chars.flat_map(|c| c.to_lowercase()));
if nucleide_nuclei::element_z(&canon).is_some() {
Some(canon)
} else {
None
}
}
fn strip_db_prefix(t: &str) -> Option<&str> {
let mut chars = t.chars();
let first = chars.next()?;
let second = chars.next()?;
if (first == 'n' || first == 'N') && second.is_ascii_alphabetic() {
Some(&t[1..])
} else {
None
}
}
fn bare_core(t: &str) -> Option<String> {
if t.is_empty() {
return None;
}
let u = t.to_ascii_uppercase();
let core = match u.strip_prefix('N') {
Some(rest) if rest.starts_with(|c: char| c.is_ascii_alphabetic()) => rest,
_ => u.as_str(),
};
Some(core.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{emit_all, Code, EmitOptions};
fn gnds_metal() -> Material {
let mut mat = Material::new();
mat.add_nuclide(NuclideId::from_name("U235").unwrap(), 5.0);
mat.add_nuclide(NuclideId::from_name("U238").unwrap(), 95.0);
mat.set_density(Some(19.1));
mat
}
#[test]
fn armi_matches_gnds_cards_with_goldens() {
let armi = from_armi_mass_fracs([("nU235", 5.0), ("nU238", 95.0)], Some(19.1)).unwrap();
let gnds = gnds_metal();
let opts = EmitOptions::new("umetal");
let armi_out = emit_all(&armi, &opts).unwrap();
let gnds_out = emit_all(&gnds, &opts).unwrap();
assert_eq!(armi_out.len(), 5);
for (a, g) in armi_out.iter().zip(gnds_out.iter()) {
assert_eq!(a.code, g.code);
assert_eq!(a.text, g.text, "{:?}", a.code);
}
let mcnp = &armi_out[Code::Mcnp as usize].text;
assert_eq!(mcnp, "m1 92235.80c -0.05 92238.80c -0.95\n");
let serpent = &armi_out[Code::Serpent as usize].text;
assert_eq!(
serpent,
"mat umetal -19.1\n 92235.03c -0.05\n 92238.03c -0.95\n"
);
}
#[test]
fn isomer_mcc3_and_zaid_keys_resolve() {
let mat = from_armi_mass_fracs(
[
("nAm242m", 2.0),
("U-2355", 4.0),
("U235_7", 1.0),
("PU2397", 3.0),
("92238", 5.0),
("2350920", 1.0),
("1001", 2.0),
],
Some(10.0),
)
.unwrap();
let get = |name: &str| {
mat.comp
.get(&NuclideId::from_name(name).unwrap())
.copied()
.unwrap_or(0.0)
};
assert!((get("Am242_m1") - 2.0).abs() < 1e-12);
assert!((get("U235") - 6.0).abs() < 1e-12);
assert!((get("Pu239") - 3.0).abs() < 1e-12);
assert!((get("U238") - 5.0).abs() < 1e-12);
assert!((get("H1") - 2.0).abs() < 1e-12);
let opts = EmitOptions::new("mix");
let out = emit_all(&mat, &opts).unwrap();
let mcnp = &out[Code::Mcnp as usize].text;
for zaid in [
"95242.80c",
"92235.80c",
"94239.80c",
"92238.80c",
"1001.80c",
] {
assert!(mcnp.contains(zaid), "{mcnp}");
}
}
#[test]
fn garbage_keys_fail_naming_the_key() {
for bad in ["nXx999", "DUMP1", "DUMP2", "LFP38", "NOTANUCLIDE", ""] {
let err = from_armi_mass_fracs([(bad, 1.0)], Some(1.0)).unwrap_err();
assert!(matches!(err, Error::Nuclei(_)), "{bad}: {err}");
assert!(err.to_string().contains(bad), "{bad}: {err}");
}
}
#[test]
fn bare_am242_rejected_but_explicit_states_accepted() {
for bare in ["AM242", "am242", "nAm242", "NAm242"] {
let err = from_armi_mass_fracs([(bare, 1.0)], Some(1.0)).unwrap_err();
assert!(matches!(err, Error::ArmiKey { .. }), "{bare}: {err}");
assert!(err.to_string().contains("AM242M"), "{bare}: {err}");
}
let m = from_armi_mass_fracs([("AM242M", 1.0)], None).unwrap();
assert!(m
.comp
.contains_key(&NuclideId::from_name("Am242_m1").unwrap()));
for g in ["AM242G", "nAm242G"] {
let m = from_armi_mass_fracs([(g, 1.0)], None).unwrap();
let id = NuclideId::new(95, 242, 0).unwrap();
assert!(m.comp.contains_key(&id), "{g}");
assert_eq!(m.comp.len(), 1);
}
}
#[test]
fn elemental_keys_rejected_with_expand_first() {
for el in [
"ZR", "FE", "NA", "O", "C", "W", "Zr", "zr", "nZr", "U", "XE",
] {
let err = from_armi_mass_fracs([(el, 1.0)], Some(1.0)).unwrap_err();
assert!(matches!(err, Error::ArmiKey { .. }), "{el}: {err}");
assert!(err.to_string().contains("expand first"), "{el}: {err}");
assert!(err.to_string().contains(el.trim()), "{el}: {err}");
}
}
#[test]
fn negative_and_non_finite_masses_rejected() {
for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -1.0] {
let err = from_armi_mass_fracs([("nU235", bad)], Some(1.0)).unwrap_err();
assert!(matches!(err, Error::ArmiKey { .. }), "{bad}: {err}");
assert!(err.to_string().contains("nU235"), "{bad}: {err}");
}
}
#[test]
fn density_flows_to_material_like_emit_drift_inner() {
let with = from_armi_mass_fracs([("nU235", 1.0)], Some(19.1)).unwrap();
assert_eq!(with.density(), Some(19.1));
let without = from_armi_mass_fracs([("nU235", 1.0)], None).unwrap();
assert_eq!(without.density(), None);
let opts = EmitOptions::new("umetal");
assert!(matches!(
crate::serpent::emit_serpent(&without, &opts),
Err(Error::MissingDensity)
));
assert!(crate::mcnp::emit_mcnp(&without, &opts).unwrap().reparsed);
}
}