use nucleide_material::Material;
use nucleide_mcnp_io::inp::{materials_from_str, FracKind};
use nucleide_nuclei::{dialects::to_zaid, NuclideId};
use crate::{Code, EmitOptions, Emitted, Error, Result};
const MAX_LINE_LEN: usize = 128;
pub fn emit_mcnp(mat: &Material, opts: &EmitOptions) -> Result<Emitted> {
let fracs = mat.weight_fractions().map_err(|_| Error::Degenerate)?;
let mut accounted = Vec::with_capacity(fracs.len());
let mut pairs = Vec::with_capacity(fracs.len());
for (id, w) in &fracs {
pairs.push((to_zaid(*id), -*w));
accounted.push((*id, mat.comp.get(id).copied().unwrap_or(0.0)));
}
let mut text = format!("m{}", opts.mcnp_number);
let mut line_len = text.len();
for (zaid, frac) in &pairs {
let piece = format!(" {zaid}.{} {frac}", opts.xs_suffix);
if line_len + piece.len() > MAX_LINE_LEN {
text.push_str("\n ");
line_len = 5;
}
text.push_str(&piece);
line_len += piece.len();
}
text.push('\n');
let reparsed = verify_round_trip(&text, opts, &fracs)?;
Ok(Emitted {
code: Code::Mcnp,
text,
accounted,
dropped: Vec::new(),
reparsed,
})
}
fn verify_round_trip(
text: &str,
opts: &EmitOptions,
fracs: &std::collections::BTreeMap<NuclideId, f64>,
) -> Result<bool> {
let mats = materials_from_str(text).map_err(|e| Error::Reparse {
code: Code::Mcnp,
detail: e.to_string(),
})?;
let back = mats
.iter()
.find(|m| m.number == opts.mcnp_number)
.ok_or_else(|| Error::Reparse {
code: Code::Mcnp,
detail: format!("m{} missing after re-parse", opts.mcnp_number),
})?;
if back.fraction_type != FracKind::Mass {
return Ok(false);
}
let mut recovered: std::collections::BTreeMap<NuclideId, f64> =
std::collections::BTreeMap::new();
for (id, f) in &back.fractions {
*recovered.entry(*id).or_insert(0.0) += f.abs();
}
for (id, w) in fracs {
let got = if id.a() == 0 {
recovered
.iter()
.filter(|(k, _)| k.z() == id.z())
.map(|(_, v)| v)
.sum()
} else {
recovered.get(id).copied().unwrap_or(0.0)
};
if (got - w).abs() > 1e-9 * w.abs().max(1e-300) {
return Ok(false);
}
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
pub(crate) fn uo2() -> 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.add_nuclide(NuclideId::from_name("O16").unwrap(), 100.0);
mat
}
#[test]
fn mcnp_card_shape_and_round_trip() {
let opts = EmitOptions::new("uo2");
let out = emit_mcnp(&uo2(), &opts).unwrap();
assert!(out.text.starts_with("m1 "));
assert!(out.text.contains("92235.80c -0.025"));
assert!(out.text.contains("8016.80c -0.5"));
assert!(out.reparsed);
assert!(out.dropped.is_empty());
assert!((out.mass_out() - 200.0).abs() < 1e-12);
}
#[test]
fn mcnp_long_cards_wrap_within_128_columns() {
let mut mat = Material::new();
for (i, name) in [
"U235", "U238", "Pu239", "Pu240", "Pu241", "O16", "H1", "B10",
]
.iter()
.enumerate()
{
mat.add_nuclide(NuclideId::from_name(name).unwrap(), (i + 1) as f64);
}
let opts = EmitOptions::new("many");
let out = emit_mcnp(&mat, &opts).unwrap();
assert!(out.text.lines().count() > 1);
for line in out.text.lines() {
assert!(line.len() <= 128, "{line:?}");
}
assert!(out.reparsed);
assert!((out.mass_out() - 36.0).abs() < 1e-12);
}
#[test]
fn mcnp_empty_is_degenerate() {
let opts = EmitOptions::new("void");
assert!(matches!(
emit_mcnp(&Material::new(), &opts),
Err(Error::Degenerate)
));
}
#[test]
fn mcnp_natural_placeholders_round_trip_by_z() {
let mut mat = Material::new();
mat.add_nuclide(NuclideId::from_nucid(10_000_000), 2.0); mat.add_nuclide(NuclideId::from_nucid(80_000_000), 16.0); let out = emit_mcnp(&mat, &EmitOptions::new("water")).unwrap();
assert!(out.text.starts_with("m1 "));
assert!(out.text.contains("1000.80c"));
assert!(out.text.contains("8000.80c"));
assert!(out.reparsed);
assert!((out.mass_out() - 18.0).abs() < 1e-12);
}
#[test]
fn mcnp_broken_suffix_fails_reparse_verification() {
let opts = EmitOptions {
xs_suffix: "80c broken".to_string(),
..EmitOptions::new("uo2")
};
match emit_mcnp(&uo2(), &opts).unwrap_err() {
Error::Reparse { code, detail } => {
assert_eq!(code, Code::Mcnp);
assert!(!detail.is_empty());
}
other => panic!("{other:?}"),
}
}
}