use crate::reaction::Reaction;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use serde_json;
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::sync::{Arc, Mutex};
static GLOBAL_NUCLIDE_CACHE: Lazy<Mutex<HashMap<String, Arc<Nuclide>>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
#[derive(Debug, Clone)]
pub enum ReactionIdentifier {
Mt(i32),
Name(String),
}
impl From<i32> for ReactionIdentifier {
fn from(mt: i32) -> Self {
ReactionIdentifier::Mt(mt)
}
}
impl From<String> for ReactionIdentifier {
fn from(name: String) -> Self {
ReactionIdentifier::Name(name)
}
}
impl From<&str> for ReactionIdentifier {
fn from(name: &str) -> Self {
ReactionIdentifier::Name(name.to_string())
}
}
#[allow(dead_code)]
pub fn clear_nuclide_cache() {
match GLOBAL_NUCLIDE_CACHE.lock() {
Ok(mut cache) => cache.clear(),
Err(poisoned) => {
let mut cache = poisoned.into_inner();
cache.clear();
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Nuclide {
pub name: Option<String>,
pub element: Option<String>,
pub atomic_symbol: Option<String>,
pub atomic_number: Option<u32>,
pub neutron_number: Option<u32>,
pub mass_number: Option<u32>,
pub library: Option<String>,
pub energy: Option<HashMap<String, Vec<f64>>>,
#[serde(default)]
pub reactions: HashMap<String, HashMap<i32, Reaction>>, pub fissionable: bool,
#[serde(skip, default)]
pub available_temperatures: Vec<String>, #[serde(skip, default)]
pub loaded_temperatures: Vec<String>, #[serde(skip, default)]
pub data_path: Option<String>, }
impl Nuclide {
pub fn sample_reaction<R: rand::Rng + ?Sized>(
&self,
energy: f64,
temperature: &str,
rng: &mut R,
) -> Option<&Reaction> {
let temp_reactions = if let Some(r) = self.reactions.get(temperature) {
r
} else if let Some(r) = self.reactions.get(&format!("{}K", temperature)) {
r
} else if let Some((temp, r)) = self.reactions.iter().next() {
println!("[sample_reaction] Requested temperature '{}' not found. Using available temperature '{}'.", temperature, temp);
r
} else {
println!("[sample_reaction] No reaction data available for any temperature.");
return None;
};
let total_mt = 1;
let fission_mt = 18;
let absorption_mt = 101;
let elastic_mt = 2;
let nonelastic_mt = 3;
let get_xs = |mt: i32| -> f64 {
temp_reactions
.get(&mt)
.and_then(|reaction| reaction.cross_section_at(energy))
.unwrap_or(0.0)
};
let total_xs = get_xs(total_mt);
if total_xs <= 0.0 {
return None;
}
let xi = rng.gen_range(0.0..total_xs);
let mut accum = 0.0;
let xs_absorption = get_xs(absorption_mt);
accum += xs_absorption;
if xi < accum && xs_absorption > 0.0 {
return temp_reactions.get(&absorption_mt);
}
let xs_elastic = get_xs(elastic_mt);
accum += xs_elastic;
if xi < accum && xs_elastic > 0.0 {
return temp_reactions.get(&elastic_mt);
}
let xs_fission = if self.fissionable {
get_xs(fission_mt)
} else {
0.0
};
accum += xs_fission;
if xi < accum && xs_fission > 0.0 {
return temp_reactions.get(&fission_mt);
}
temp_reactions.get(&nonelastic_mt)
}
pub fn energy_grid(&self, temperature: &str) -> Option<&Vec<f64>> {
self.energy
.as_ref()
.and_then(|energy_map| energy_map.get(temperature))
}
pub fn temperatures(&self) -> Option<Vec<String>> {
let mut temps = std::collections::HashSet::new();
for temp in self.reactions.keys() {
temps.insert(temp.clone());
}
if let Some(energy_map) = &self.energy {
for temp in energy_map.keys() {
temps.insert(temp.clone());
}
}
if temps.is_empty() {
None
} else {
let mut temps_vec: Vec<String> = temps.into_iter().collect();
temps_vec.sort();
Some(temps_vec)
}
}
pub fn reaction_mts(&self) -> Option<Vec<i32>> {
let mut mts = std::collections::HashSet::new();
for temp_reactions in self.reactions.values() {
for &mt in temp_reactions.keys() {
mts.insert(mt);
}
}
if mts.is_empty() {
None
} else {
let mut mts_vec: Vec<i32> = mts.into_iter().collect();
mts_vec.sort();
Some(mts_vec)
}
}
pub fn microscopic_cross_section<R>(
&mut self,
reaction: R,
temperature: Option<&str>,
) -> Result<(Vec<f64>, Vec<f64>), Box<dyn std::error::Error>>
where
R: Into<ReactionIdentifier>,
{
let mt = match reaction.into() {
ReactionIdentifier::Mt(mt_num) => mt_num,
ReactionIdentifier::Name(name) => {
crate::data::REACTION_MT.get(name.as_str())
.copied()
.ok_or_else(|| format!("Unknown reaction name '{}'. Available reactions can be found in REACTION_MT mapping.", name))?
}
};
if self.loaded_temperatures.is_empty() {
if let Some(name) = self.name.clone() {
self.auto_load_from_config(&name, temperature)?;
} else {
return Err("No data loaded and no nuclide name available for automatic loading".into());
}
}
if let Some(temp) = temperature {
let temp_normalized = format!("{}K", temp);
let temp_without_k = if temp.ends_with('K') { &temp[..temp.len()-1] } else { temp };
let needs_temp_load = !self.loaded_temperatures.contains(&temp.to_string())
&& !self.loaded_temperatures.contains(&temp_normalized)
&& !self.loaded_temperatures.contains(&temp_without_k.to_string())
&& (self.available_temperatures.contains(&temp.to_string())
|| self.available_temperatures.contains(&temp_normalized)
|| self.available_temperatures.contains(&temp_without_k.to_string()));
if needs_temp_load {
if let Some(name) = self.name.clone() {
self.auto_load_additional_temperature(&name, temp)?;
}
}
}
self.get_microscopic_cross_section_data(mt, temperature)
}
fn auto_load_from_config(&mut self, nuclide_name: &str, temperature: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let path_or_url = {
let cfg = crate::config::CONFIG.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
cfg.get_cross_section(nuclide_name)
};
if let Some(path_or_url) = path_or_url {
let temps_to_load = if let Some(temp) = temperature {
let mut temps = std::collections::HashSet::new();
temps.insert(temp.to_string());
Some(temps)
} else {
None };
let loaded_nuclide = if crate::url_cache::is_keyword(&path_or_url) {
load_nuclide_for_python(Some(&path_or_url), Some(nuclide_name), temps_to_load.as_ref())?
} else {
load_nuclide_for_python(Some(&path_or_url), None, temps_to_load.as_ref())?
};
*self = loaded_nuclide;
Ok(())
} else {
Err(format!("No configuration found for nuclide '{}'. Use Config.set_cross_sections() to configure data sources.", nuclide_name).into())
}
}
fn auto_load_additional_temperature(&mut self, nuclide_name: &str, temperature: &str) -> Result<(), Box<dyn std::error::Error>> {
let path_or_url = {
let cfg = crate::config::CONFIG.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
cfg.get_cross_section(nuclide_name)
};
if let Some(path_or_url) = path_or_url {
let mut temps_to_load = std::collections::HashSet::new();
for temp in &self.loaded_temperatures {
temps_to_load.insert(temp.clone());
}
temps_to_load.insert(temperature.to_string());
let loaded_nuclide = if crate::url_cache::is_keyword(&path_or_url) {
load_nuclide_for_python(Some(&path_or_url), Some(nuclide_name), Some(&temps_to_load))?
} else {
load_nuclide_for_python(Some(&path_or_url), None, Some(&temps_to_load))?
};
*self = loaded_nuclide;
Ok(())
} else {
Err(format!("No configuration found for nuclide '{}' to load additional temperature", nuclide_name).into())
}
}
fn get_microscopic_cross_section_data(
&self,
mt: i32,
temperature: Option<&str>,
) -> Result<(Vec<f64>, Vec<f64>), Box<dyn std::error::Error>> {
let temp_key = if let Some(temp) = temperature {
if self.reactions.contains_key(temp) {
temp
} else if self.reactions.contains_key(&format!("{}K", temp)) {
&format!("{}K", temp)
} else {
return Err(format!(
"Temperature '{}' not found in loaded data. Available temperatures: [{}]",
temp,
self.loaded_temperatures.join(", ")
).into());
}
} else {
if self.loaded_temperatures.len() == 1 {
&self.loaded_temperatures[0]
} else if self.loaded_temperatures.is_empty() {
return Err("No temperatures loaded in nuclide data".into());
} else {
return Err(format!(
"Multiple temperatures loaded [{}], must specify which one to use",
self.loaded_temperatures.join(", ")
).into());
}
};
let temp_reactions = self.reactions.get(temp_key)
.ok_or_else(|| format!(
"Temperature '{}' not found in reactions. Available temperatures: [{}]",
temp_key,
self.loaded_temperatures.join(", ")
))?;
let reaction = temp_reactions.get(&mt)
.ok_or_else(|| {
let available_mts: Vec<String> = temp_reactions.keys()
.map(|mt| mt.to_string())
.collect();
format!(
"MT {} not found for temperature '{}'. Available MTs: [{}]",
mt,
temp_key,
available_mts.join(", ")
)
})?;
if reaction.cross_section.is_empty() {
return Err(format!("No cross section data available for MT {} at temperature '{}'", mt, temp_key).into());
}
if reaction.energy.is_empty() {
return Err(format!("No energy grid available for MT {} at temperature '{}'", mt, temp_key).into());
}
Ok((reaction.cross_section.clone(), reaction.energy.clone()))
}
}
fn parse_nuclide_from_json_value(
json_value: serde_json::Value,
temps_filter: Option<&std::collections::HashSet<String>>,
) -> Result<Nuclide, Box<dyn std::error::Error>> {
let mut nuclide = Nuclide {
name: None,
element: None,
atomic_symbol: None,
atomic_number: None,
neutron_number: None,
mass_number: None,
library: None,
energy: None,
reactions: HashMap::new(),
fissionable: false,
available_temperatures: Vec::new(),
loaded_temperatures: Vec::new(),
data_path: None,
};
if let Some(name) = json_value.get("name").and_then(|v| v.as_str()) {
nuclide.name = Some(name.to_string());
}
if let Some(element) = json_value.get("element").and_then(|v| v.as_str()) {
nuclide.element = Some(element.to_string());
}
if let Some(symbol) = json_value.get("atomic_symbol").and_then(|v| v.as_str()) {
nuclide.atomic_symbol = Some(symbol.to_string());
if symbol == "Li" && nuclide.element.is_none() {
nuclide.element = Some("lithium".to_string());
}
}
if let Some(num) = json_value.get("atomic_number").and_then(|v| v.as_u64()) {
nuclide.atomic_number = Some(num as u32);
}
if let Some(num) = json_value.get("mass_number").and_then(|v| v.as_u64()) {
nuclide.mass_number = Some(num as u32);
}
if let Some(num) = json_value.get("neutron_number").and_then(|v| v.as_u64()) {
nuclide.neutron_number = Some(num as u32);
} else if nuclide.mass_number.is_some() && nuclide.atomic_number.is_some() {
nuclide.neutron_number =
Some(nuclide.mass_number.unwrap() - nuclide.atomic_number.unwrap());
}
if let Some(lib) = json_value.get("library").and_then(|v| v.as_str()) {
nuclide.library = Some(lib.to_string());
}
let mut all_temperatures = std::collections::HashSet::new();
if let Some(temps_array) = json_value.get("temperatures").and_then(|v| v.as_array()) {
for temp_value in temps_array {
if let Some(temp_str) = temp_value.as_str() {
if temp_str != "0" {
all_temperatures.insert(temp_str.to_string());
}
}
}
}
if let Some(reactions_obj) = json_value.get("reactions").and_then(|v| v.as_object()) {
for temp in reactions_obj.keys() {
if temp != "0" {
all_temperatures.insert(temp.clone());
}
}
}
if let Some(energy_obj) = json_value.get("energy").and_then(|v| v.as_object()) {
for temp in energy_obj.keys() {
if temp != "0" {
all_temperatures.insert(temp.clone());
}
}
}
let mut available_temps: Vec<String> = all_temperatures.iter().cloned().collect();
available_temps.sort();
nuclide.available_temperatures = available_temps;
if let Some(reactions_obj) = json_value.get("reactions").and_then(|v| v.as_object()) {
for (temp, mt_reactions) in reactions_obj {
if temp == "0" {
continue;
}
if let Some(filter) = temps_filter {
if !filter.is_empty() && !filter.contains(temp) {
continue;
}
}
let mut temp_reactions: HashMap<i32, Reaction> = HashMap::new();
if let Some(mt_obj) = mt_reactions.as_object() {
for (mt, reaction_data) in mt_obj {
if let Some(reaction_obj) = reaction_data.as_object() {
let mut reaction = Reaction {
cross_section: Vec::new(),
threshold_idx: 0,
interpolation: Vec::new(),
energy: Vec::new(),
mt_number: 0,
};
if let Some(xs) = reaction_obj
.get("cross_section")
.or_else(|| reaction_obj.get("xs"))
{
if let Some(xs_arr) = xs.as_array() {
reaction.cross_section =
xs_arr.iter().filter_map(|v| v.as_f64()).collect();
}
}
if let Some(idx) =
reaction_obj.get("threshold_idx").and_then(|v| v.as_u64())
{
reaction.threshold_idx = idx as usize;
}
if let Some(interp) = reaction_obj.get("interpolation") {
if let Some(interp_arr) = interp.as_array() {
reaction.interpolation = interp_arr
.iter()
.filter_map(|v| v.as_i64().map(|i| i as i32))
.collect();
}
}
if let Some(energy) = reaction_obj.get("energy") {
if let Some(energy_arr) = energy.as_array() {
reaction.energy =
energy_arr.iter().filter_map(|v| v.as_f64()).collect();
}
}
if reaction.energy.is_empty() {
if let Some(energy_grids) = &nuclide.energy {
if let Some(energy_grid) = energy_grids.get(temp) {
if reaction.threshold_idx < energy_grid.len() {
reaction.energy =
energy_grid[reaction.threshold_idx..].to_vec();
}
}
}
}
if let Ok(mt_int) = mt.parse::<i32>() {
reaction.mt_number = mt_int;
temp_reactions.insert(mt_int, reaction);
}
}
}
}
if !temp_reactions.is_empty() {
nuclide.reactions.insert(temp.clone(), temp_reactions);
}
}
}
if let Some(energy_obj) = json_value.get("energy").and_then(|v| v.as_object()) {
let mut energy_map = HashMap::new();
for (temp, energy_arr) in energy_obj {
if temp == "0" {
continue;
}
if let Some(filter) = temps_filter {
if !filter.is_empty() && !filter.contains(temp) {
continue;
}
}
if let Some(energy_values) = energy_arr.as_array() {
let energy_vec: Vec<f64> =
energy_values.iter().filter_map(|v| v.as_f64()).collect();
energy_map.insert(temp.clone(), energy_vec);
}
}
if !energy_map.is_empty() {
nuclide.energy = Some(energy_map);
}
}
if let Some(energy_map) = &nuclide.energy {
use std::collections::HashSet;
let reaction_temps: HashSet<String> = nuclide.reactions.keys().cloned().collect();
let energy_temps: HashSet<String> = energy_map.keys().cloned().collect();
if reaction_temps.is_empty() && !energy_temps.is_empty() {
return Err(format!(
"Energy grid has temperatures {:?} but no reactions were loaded.",
energy_temps
)
.into());
}
if !reaction_temps.is_empty() && energy_temps.is_empty() {
return Err(format!(
"Reactions have temperatures {:?} but no energy grids were loaded.",
reaction_temps
)
.into());
}
if reaction_temps != energy_temps {
let only_in_reactions: Vec<String> =
reaction_temps.difference(&energy_temps).cloned().collect();
let only_in_energy: Vec<String> =
energy_temps.difference(&reaction_temps).cloned().collect();
return Err(format!(
"Mismatched temperature sets. Only in reactions: {:?}. Only in energy: {:?}.",
only_in_reactions, only_in_energy
)
.into());
}
} else if !nuclide.reactions.is_empty() {
let temps: Vec<String> = nuclide.reactions.keys().cloned().collect();
return Err(format!(
"Reactions contain temperatures {:?} but no top-level energy map present.",
temps
)
.into());
} else {
nuclide.available_temperatures.clear();
}
if nuclide.energy.is_none() && !nuclide.reactions.is_empty() {
let mut energy_grids = HashMap::new();
for (temp, temp_reactions) in &nuclide.reactions {
if let Some((_, reaction)) = temp_reactions.iter().next() {
if !reaction.energy.is_empty() {
energy_grids.insert(temp.clone(), reaction.energy.clone());
}
}
}
if !energy_grids.is_empty() {
nuclide.energy = Some(energy_grids);
}
}
if let Some(energy_map) = &nuclide.energy {
for (temp, temp_reactions) in nuclide.reactions.iter_mut() {
if let Some(energy_grid) = energy_map.get(temp) {
for reaction in temp_reactions.values_mut() {
if reaction.energy.is_empty() {
if reaction.threshold_idx < energy_grid.len() {
reaction.energy = energy_grid[reaction.threshold_idx..].to_vec();
}
}
}
}
}
}
if nuclide.name.is_none() {
if let (Some(symbol), Some(mass)) = (&nuclide.atomic_symbol, nuclide.mass_number) {
nuclide.name = Some(format!("{}{}", symbol, mass));
}
}
let fission_mt_list = [18, 19, 20, 21, 38];
if nuclide
.reactions
.values()
.any(|temp_reactions| temp_reactions.keys().any(|mt| fission_mt_list.contains(mt)))
{
nuclide.fissionable = true;
}
let mut loaded_temps: Vec<String> = nuclide.reactions.keys().cloned().collect();
loaded_temps.sort();
nuclide.loaded_temperatures = loaded_temps;
Ok(nuclide)
}
pub fn read_nuclide_from_json<P: AsRef<Path>>(
path_or_name: P,
temps: Option<&std::collections::HashSet<String>>,
) -> Result<Nuclide, Box<dyn std::error::Error>> {
let candidate_ref = path_or_name.as_ref();
let candidate_str = candidate_ref.to_string_lossy();
if crate::url_cache::is_keyword(&candidate_str) {
let mut cfg = crate::config::CONFIG.lock().unwrap();
cfg.set_cross_section(candidate_str.as_ref(), Some(&candidate_str));
}
read_nuclide_from_json_with_name(path_or_name, temps, None)
}
pub fn read_nuclide_from_json_with_name<P: AsRef<Path>>(
path_or_name: P,
temps: Option<&std::collections::HashSet<String>>,
nuclide_name_hint: Option<&str>,
) -> Result<Nuclide, Box<dyn std::error::Error>> {
let candidate_ref = path_or_name.as_ref();
let candidate_str = candidate_ref.to_string_lossy();
let resolved_path = if candidate_ref.exists() {
candidate_ref.to_path_buf()
} else if crate::url_cache::is_url(&candidate_str) {
if let Some(name) = nuclide_name_hint {
crate::url_cache::resolve_path_or_url(&candidate_str, name)?
} else {
return Err("Direct URL loading without nuclide name not yet supported. Use config approach instead.".into());
}
} else if crate::url_cache::is_keyword(&candidate_str) {
let nuclide_name = nuclide_name_hint.unwrap_or(candidate_str.as_ref());
crate::url_cache::resolve_path_or_url(&candidate_str, nuclide_name)?
} else {
let cfg = crate::config::CONFIG.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
let path_or_url = cfg
.cross_sections
.get(candidate_str.as_ref())
.ok_or_else(|| {
format!(
"Input '{}' is neither an existing file nor a key in Config cross_sections",
candidate_str
)
})?;
crate::url_cache::resolve_path_or_url(path_or_url, candidate_str.as_ref())?
};
let file = File::open(&resolved_path)?;
let reader = BufReader::new(file);
let json_value: serde_json::Value = serde_json::from_reader(reader)?;
let mut nuclide = parse_nuclide_from_json_value(json_value, temps)?;
nuclide.data_path = Some(resolved_path.to_string_lossy().to_string());
Ok(nuclide)
}
pub fn read_nuclide_from_json_str(
json_content: &str,
) -> Result<Nuclide, Box<dyn std::error::Error>> {
let json_value: serde_json::Value = serde_json::from_str(json_content)?;
let mut nuclide = parse_nuclide_from_json_value(json_value, None)?;
nuclide.data_path = None;
Ok(nuclide)
}
pub fn get_or_load_nuclide(
nuclide_name: &str,
json_path_map: &HashMap<String, String>,
temperatures_to_include: Option<&std::collections::HashSet<String>>,
) -> Result<Arc<Nuclide>, Box<dyn std::error::Error>> {
use std::collections::HashSet;
let requested: HashSet<String> = temperatures_to_include
.map(|s| s.iter().cloned().collect())
.unwrap_or_else(HashSet::new);
let mut path_or_url = json_path_map.get(nuclide_name).cloned();
if path_or_url.is_none() && crate::url_cache::is_keyword(nuclide_name) {
let mut cfg = crate::config::CONFIG.lock().unwrap();
cfg.set_cross_section(nuclide_name, Some(nuclide_name));
path_or_url = Some(nuclide_name.to_string());
}
let path_or_url = path_or_url.ok_or_else(|| {
format!(
"No JSON file provided for nuclide '{}'. Please supply a path for all nuclides.",
nuclide_name
)
})?;
let resolved_path = crate::url_cache::resolve_path_or_url(&path_or_url, nuclide_name)?;
let normalized_source = match std::fs::canonicalize(&resolved_path) {
Ok(canonical) => canonical.to_string_lossy().to_string(),
Err(_) => {
resolved_path.to_string_lossy().to_string()
}
};
let cache_key = format!("{}@{}", nuclide_name, normalized_source);
{
let cache = match GLOBAL_NUCLIDE_CACHE.lock() {
Ok(cache) => cache,
Err(poisoned) => poisoned.into_inner()
};
if let Some(existing) = cache.get(&cache_key) {
if requested.is_empty()
|| requested.is_subset(&existing.loaded_temperatures.iter().cloned().collect())
{
return Ok(Arc::clone(existing));
}
}
}
let mut union_set = requested.clone();
{
let cache = match GLOBAL_NUCLIDE_CACHE.lock() {
Ok(cache) => cache,
Err(poisoned) => poisoned.into_inner()
};
if let Some(existing) = cache.get(&cache_key) {
for t in &existing.loaded_temperatures {
union_set.insert(t.clone());
}
}
}
let file = File::open(&resolved_path)?;
let reader = BufReader::new(file);
let json_value: serde_json::Value = serde_json::from_reader(reader)?;
let filter_option = if union_set.is_empty() {
None
} else {
Some(&union_set)
};
let all_temps_nuclide = parse_nuclide_from_json_value(json_value.clone(), None)?;
let mut nuclide = parse_nuclide_from_json_value(json_value, filter_option)?;
nuclide.data_path = Some(resolved_path.to_string_lossy().to_string());
nuclide.available_temperatures = all_temps_nuclide.available_temperatures;
let name_disp = nuclide.name.as_deref().unwrap_or(nuclide_name);
println!(
"Reading {} from {}, available temperatures: {}, loaded temperatures: {}",
name_disp,
resolved_path.to_string_lossy(),
nuclide.available_temperatures.len(),
nuclide.loaded_temperatures.len()
);
let arc = Arc::new(nuclide);
{
let mut cache = match GLOBAL_NUCLIDE_CACHE.lock() {
Ok(cache) => cache,
Err(poisoned) => poisoned.into_inner()
};
cache.insert(cache_key, Arc::clone(&arc));
}
Ok(arc)
}
#[allow(dead_code)]
pub fn load_nuclide_for_python(
path_or_keyword: Option<&str>,
nuclide_name: Option<&str>,
temperatures: Option<&std::collections::HashSet<String>>,
) -> Result<Nuclide, Box<dyn std::error::Error>> {
let identifier = if let Some(p) = path_or_keyword {
p
} else if let Some(n) = nuclide_name {
n
} else {
return Err("Either path or nuclide name must be provided".into());
};
if crate::url_cache::is_keyword(identifier) {
let mut cfg = crate::config::CONFIG.lock().unwrap();
cfg.set_cross_section(identifier, Some(identifier));
}
let full_nuclide = if path_or_keyword.is_some() && nuclide_name.is_some() {
read_nuclide_from_json_with_name(identifier, None, nuclide_name)?
} else {
read_nuclide_from_json(identifier, None)?
};
let mut filtered_nuclide = if temperatures.is_some() {
if path_or_keyword.is_some() && nuclide_name.is_some() {
read_nuclide_from_json_with_name(identifier, temperatures, nuclide_name)?
} else {
read_nuclide_from_json(identifier, temperatures)?
}
} else {
full_nuclide.clone()
};
filtered_nuclide.available_temperatures = full_nuclide.available_temperatures;
Ok(filtered_nuclide)
}
#[allow(dead_code)]
pub fn load_nuclide_from_path_or_keyword(
path_or_keyword: &str,
) -> Result<Nuclide, Box<dyn std::error::Error>> {
if crate::url_cache::is_keyword(path_or_keyword) {
let mut cfg = crate::config::CONFIG.lock().unwrap();
cfg.set_cross_section(path_or_keyword, Some(path_or_keyword));
}
read_nuclide_from_json(path_or_keyword, None)
}
mod tests {
#[test]
fn test_get_or_load_nuclide_uses_cache() {
use std::collections::HashMap;
let li6_path = std::path::Path::new("tests/Li6.json");
assert!(li6_path.exists(), "tests/Li6.json missing");
{
let mut cache = match super::GLOBAL_NUCLIDE_CACHE.lock() {
Ok(cache) => cache,
Err(poisoned) => poisoned.into_inner()
};
let keys_to_remove: Vec<String> = cache.keys()
.filter(|k| k.starts_with("Li6@") && k.contains("Li6.json"))
.cloned()
.collect();
for key in keys_to_remove {
cache.remove(&key);
}
}
let raw = super::read_nuclide_from_json(li6_path, None).expect("Direct read failed");
assert_eq!(raw.name.as_deref(), Some("Li6"));
let mut json_map = HashMap::new();
json_map.insert("Li6".to_string(), "tests/Li6.json".to_string());
let first =
super::get_or_load_nuclide("Li6", &json_map, None).expect("Initial cached load failed");
{
let cache = match super::GLOBAL_NUCLIDE_CACHE.lock() {
Ok(cache) => cache,
Err(poisoned) => poisoned.into_inner()
};
let found = cache.keys().any(|k| k.starts_with("Li6@") && k.contains("Li6.json"));
assert!(found, "Li6 should be present after cached load");
}
let second =
super::get_or_load_nuclide("Li6", &json_map, None).expect("Second cached load failed");
assert!(
std::sync::Arc::ptr_eq(&first, &second),
"Expected identical Arc pointer from cache on second load"
);
assert_eq!(
first.name.as_deref(),
raw.name.as_deref(),
"Names differ between raw and cached"
);
}
#[cfg(test)]
#[test]
fn test_sample_reaction_li6() {
use rand::rngs::StdRng;
use rand::SeedableRng;
let path = std::path::Path::new("tests/Li6.json");
let nuclide = super::read_nuclide_from_json(path, None).expect("Failed to load Li6.json");
let temperature = "294";
let energies = (0..10).map(|i| 1.0 + i as f64 * (15e6 - 1e6) / 9.0);
for energy in energies {
let mut rng1 = StdRng::seed_from_u64(42);
let mut rng2 = StdRng::seed_from_u64(42);
let mut rng3 = StdRng::seed_from_u64(43);
let reaction1 = nuclide.sample_reaction(energy, temperature, &mut rng1);
let reaction2 = nuclide.sample_reaction(energy, temperature, &mut rng2);
let reaction3 = nuclide.sample_reaction(energy, temperature, &mut rng3);
assert!(
reaction1.is_some(),
"sample_reaction returned None at energy {}",
energy
);
assert!(
reaction2.is_some(),
"Repeat sample with same seed returned None at energy {}",
energy
);
assert!(
reaction3.is_some(),
"Sample with different seed returned None at energy {}",
energy
);
let reaction1 = reaction1.unwrap();
let reaction2 = reaction2.unwrap();
let reaction3 = reaction3.unwrap();
assert_eq!(
reaction1.mt_number, reaction2.mt_number,
"Different MT for same seed at energy {}",
energy
);
assert_eq!(
reaction1.cross_section, reaction2.cross_section,
"Different cross_section for same seed at energy {}",
energy
);
println!(
"Energy: {:e}, MT (seed 42): {}, MT (seed 43): {}",
energy, reaction1.mt_number, reaction3.mt_number
);
assert!(
reaction1.mt_number > 0,
"Sampled reaction has invalid MT number at energy {}",
energy
);
assert!(
!reaction1.cross_section.is_empty(),
"Sampled reaction has empty cross section at energy {}",
energy
);
}
}
#[test]
fn test_reaction_mts_li6() {
let path = std::path::Path::new("tests/Li6.json");
let nuclide = super::read_nuclide_from_json(path, None).expect("Failed to load Li6.json");
let mts = nuclide.reaction_mts().expect("No MTs found");
let expected = vec![
102, 103, 105, 2, 203, 204, 205, 207, 24, 301, 444, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81,
];
for mt in &expected {
assert!(mts.contains(mt), "Expected MT {} in Li6", mt);
}
}
#[test]
fn test_reaction_mts_li7() {
let path = std::path::Path::new("tests/Li7.json");
let nuclide = super::read_nuclide_from_json(path, None).expect("Failed to load Li7.json");
let mts = nuclide.reaction_mts().expect("No MTs found");
assert!(mts.contains(&1), "MT=1 should be present");
assert!(mts.contains(&3), "MT=3 should be present");
assert!(mts.contains(&4), "MT=4 should be present");
assert!(mts.contains(&27), "MT=27 should be present");
assert!(mts.contains(&101), "MT=101 should be present");
assert!(mts.contains(&2), "MT=2 should be present");
assert!(mts.contains(&16), "MT=16 should be present");
assert!(mts.contains(&24), "MT=24 should be present");
assert!(mts.contains(&51), "MT=51 should be present");
assert!(!mts.is_empty(), "MT list should not be empty");
}
#[test]
fn test_fissionable_false_for_be9_and_fe58() {
let nuclide_be9 =
super::read_nuclide_from_json("tests/Be9.json", None).expect("Failed to load Be9.json");
assert_eq!(
nuclide_be9.fissionable, false,
"Be9 should not be fissionable"
);
let path_fe58 = std::path::Path::new("tests/Fe58.json");
let nuclide_fe58 =
super::read_nuclide_from_json(path_fe58, None).expect("Failed to load Fe58.json");
assert_eq!(
nuclide_fe58.fissionable, false,
"Fe58 should not be fissionable"
);
}
#[test]
fn test_li6_reactions_contain_specific_mts() {
let path = std::path::Path::new("tests/Li6.json");
let nuclide = super::read_nuclide_from_json(path, None).expect("Failed to load Li6.json");
let required = [2, 24, 51, 444];
for mt in &required {
let mut found = false;
for temp_reactions in nuclide.reactions.values() {
if let Some(reaction) = temp_reactions.get(mt) {
assert_ne!(
reaction.mt_number, 0,
"Reaction MT number for MT {} is 0",
mt
);
found = true;
break;
}
}
assert!(found, "MT {} not found in any temperature reactions", mt);
}
}
#[test]
fn test_available_temperatures_be9() {
let path_be9 = std::path::Path::new("tests/Be9.json");
let nuclide_be9 =
super::read_nuclide_from_json(path_be9, None).expect("Failed to load Be9.json");
assert_eq!(
nuclide_be9.available_temperatures,
vec!["294".to_string(), "300".to_string()],
"available_temperatures should be ['294','300']"
);
assert_eq!(
nuclide_be9.loaded_temperatures,
vec!["294".to_string(), "300".to_string()],
"By default all temps are loaded"
);
let temps_method = nuclide_be9
.temperatures()
.expect("temperatures() returned None");
assert_eq!(
temps_method,
vec!["294".to_string(), "300".to_string()],
"temperatures() should return ['294','300']"
);
}
#[test]
fn test_be9_mt_numbers_per_temperature() {
let path_be9 = std::path::Path::new("tests/Be9.json");
let nuclide_be9 =
super::read_nuclide_from_json(path_be9, None).expect("Failed to load Be9.json");
let mut expected_294: Vec<i32> = vec![
1, 2, 3, 16, 27, 101, 102, 103, 104, 105, 107, 203, 204, 205, 207, 301, 444, 875, 876,
877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890,
];
expected_294.sort();
let mut expected_300: Vec<i32> = vec![
1, 2, 3, 16, 27, 101, 102, 103, 104, 105, 107, 203, 204, 205, 207, 301,
];
expected_300.sort();
let get_sorted_mts = |temp: &str| -> Vec<i32> {
let mut mts: Vec<i32> = nuclide_be9
.reactions
.get(temp)
.expect("Temperature not found in reactions")
.keys()
.cloned()
.collect();
mts.sort();
mts
};
let mts_294 = get_sorted_mts("294");
let mts_300 = get_sorted_mts("300");
assert_eq!(
mts_294, expected_294,
"Be9 294K MT list mismatch. Got {:?}",
mts_294
);
assert_eq!(
mts_300, expected_300,
"Be9 300K MT list mismatch. Got {:?}",
mts_300
);
for mt in &mts_300 {
assert!(
expected_294.contains(mt),
"MT {} at 300K not present in 294K expected list (unexpected new MT)",
mt
);
}
}
#[test]
fn test_available_temperatures_fe56_includes_294() {
let nuclide_fe56 = super::read_nuclide_from_json("tests/Fe56.json", None)
.expect("Failed to load Fe56.json");
assert!(
nuclide_fe56
.available_temperatures
.iter()
.any(|t| t == "294"),
"Fe56 available_temperatures should contain '294'"
);
let temps_method = nuclide_fe56
.temperatures()
.expect("temperatures() returned None");
assert!(
temps_method.iter().any(|t| t == "294"),
"Fe56 temperatures() should contain '294'"
);
}
#[test]
fn test_clear_nuclide_cache() {
let li6_path = std::path::Path::new("tests/Li6.json");
let nuclide =
super::read_nuclide_from_json(li6_path, None).expect("Failed to load Li6.json");
let nuclide_arc = std::sync::Arc::new(nuclide);
{
let mut cache = match super::GLOBAL_NUCLIDE_CACHE.lock() {
Ok(cache) => cache,
Err(poisoned) => poisoned.into_inner()
};
cache.insert(
"Test_Nuclide".to_string(),
std::sync::Arc::clone(&nuclide_arc),
);
assert!(
cache.contains_key("Test_Nuclide"),
"Test nuclide should be in cache before clearing"
);
}
super::clear_nuclide_cache();
let cache = match super::GLOBAL_NUCLIDE_CACHE.lock() {
Ok(cache) => cache,
Err(poisoned) => poisoned.into_inner()
};
assert!(
cache.is_empty(),
"Cache should be empty after calling clear_nuclide_cache"
);
}
#[test]
#[cfg(feature = "download")]
fn test_nuclide_from_url_energy_grid_positive() {
{
let mut cfg = crate::config::CONFIG.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
cfg.cross_sections.clear();
}
{
let mut cfg = crate::config::CONFIG.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
cfg.set_cross_section("Li6", Some("tendl-21"));
}
let nuclide = super::read_nuclide_from_json("Li6", None)
.expect("Failed to load Li6 from keyword");
let temps = nuclide.temperatures()
.expect("Nuclide should have temperatures available");
assert!(
!temps.is_empty(),
"Nuclide should have at least one temperature"
);
let temp = &temps[0];
let energy_grid = nuclide.energy_grid(temp)
.expect("Energy grid should be available for the temperature");
assert!(
!energy_grid.is_empty(),
"Energy grid should not be empty"
);
for (i, &energy) in energy_grid.iter().enumerate() {
assert!(
energy > 0.0,
"Energy at index {} should be positive, but got {}",
i,
energy
);
}
for i in 1..energy_grid.len() {
assert!(
energy_grid[i] >= energy_grid[i - 1],
"Energy grid should be sorted: energy[{}] = {} < energy[{}] = {}",
i,
energy_grid[i],
i - 1,
energy_grid[i - 1]
);
}
println!(
"Successfully loaded Li6 from URL with {} energy points at temperature {}",
energy_grid.len(),
temp
);
}
#[test]
fn test_microscopic_cross_section_with_temperature() {
let mut nuclide = super::read_nuclide_from_json("tests/Be9.json", None)
.expect("Failed to load Be9.json");
let result = nuclide.microscopic_cross_section(2, Some("294"));
assert!(result.is_ok(), "Should successfully get MT=2 data for 294K");
let (xs, energy) = result.unwrap();
assert!(!xs.is_empty(), "Cross section data should not be empty");
assert!(!energy.is_empty(), "Energy data should not be empty");
assert_eq!(xs.len(), energy.len(), "Cross section and energy arrays should have same length");
let result_300 = nuclide.microscopic_cross_section(2, Some("300"));
assert!(result_300.is_ok(), "Should successfully get MT=2 data for 300K");
let (xs_300, energy_300) = result_300.unwrap();
assert!(!xs_300.is_empty(), "Cross section data should not be empty for 300K");
assert!(!energy_300.is_empty(), "Energy data should not be empty for 300K");
}
#[test]
fn test_microscopic_cross_section_single_temperature() {
let temps_filter = std::collections::HashSet::from(["294".to_string()]);
let mut nuclide = super::read_nuclide_from_json("tests/Be9.json", Some(&temps_filter))
.expect("Failed to load Be9.json with temperature filter");
let result = nuclide.microscopic_cross_section(2, None);
assert!(result.is_ok(), "Should successfully get MT=2 data without temperature");
let (xs, energy) = result.unwrap();
assert!(!xs.is_empty(), "Cross section data should not be empty");
assert!(!energy.is_empty(), "Energy data should not be empty");
assert_eq!(xs.len(), energy.len(), "Cross section and energy arrays should have same length");
}
#[test]
fn test_microscopic_cross_section_multiple_temperatures_error() {
let mut nuclide = super::read_nuclide_from_json("tests/Be9.json", None)
.expect("Failed to load Be9.json");
let result = nuclide.microscopic_cross_section(2, None);
assert!(result.is_err(), "Should error when multiple temperatures loaded without specifying");
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Multiple temperatures loaded"),
"Error should mention multiple temperatures: {}", error_msg);
assert!(error_msg.contains("[294, 300]"),
"Error should list the loaded temperatures: {}", error_msg);
}
#[test]
fn test_microscopic_cross_section_invalid_temperature() {
let mut nuclide = super::read_nuclide_from_json("tests/Be9.json", None)
.expect("Failed to load Be9.json");
let result = nuclide.microscopic_cross_section(2, Some("500"));
assert!(result.is_err(), "Should error for invalid temperature");
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Temperature '500' not found"),
"Error should mention temperature not found: {}", error_msg);
assert!(error_msg.contains("Available temperatures:"),
"Error should list available temperatures: {}", error_msg);
assert!(error_msg.contains("294") && error_msg.contains("300"),
"Error should list the actual available temperatures: {}", error_msg);
}
#[test]
fn test_microscopic_cross_section_invalid_mt() {
let mut nuclide = super::read_nuclide_from_json("tests/Be9.json", None)
.expect("Failed to load Be9.json");
let result = nuclide.microscopic_cross_section(9999, Some("294"));
assert!(result.is_err(), "Should error for invalid MT number");
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("MT 9999 not found"),
"Error should mention MT not found: {}", error_msg);
assert!(error_msg.contains("Available MTs:"),
"Error should list available MTs: {}", error_msg);
assert!(error_msg.contains("1") && error_msg.contains("2"),
"Error should list some actual available MTs: {}", error_msg);
}
#[test]
fn test_microscopic_cross_section_multiple_mt_numbers() {
let mut nuclide = super::read_nuclide_from_json("tests/Be9.json", None)
.expect("Failed to load Be9.json");
let test_mts = [1, 2, 3, 16, 27, 101, 102];
for mt in test_mts {
let result = nuclide.microscopic_cross_section(mt, Some("294"));
if result.is_ok() {
let (xs, energy) = result.unwrap();
assert!(!xs.is_empty(), "MT={} should have cross section data", mt);
assert!(!energy.is_empty(), "MT={} should have energy data", mt);
assert_eq!(xs.len(), energy.len(), "MT={} data length mismatch", mt);
for &e in &energy {
assert!(e > 0.0, "MT={} energy values should be positive", mt);
}
for &x in &xs {
assert!(x >= 0.0, "MT={} cross sections should be non-negative", mt);
}
}
}
}
#[test]
fn test_microscopic_cross_section_lithium() {
let mut nuclide = super::read_nuclide_from_json("tests/Li6.json", None)
.expect("Failed to load Li6.json");
let result = nuclide.microscopic_cross_section(2, None);
assert!(result.is_ok(), "Should successfully get Li6 elastic scattering data");
let (xs, energy) = result.unwrap();
assert!(!xs.is_empty(), "Li6 elastic scattering data should not be empty");
assert!(!energy.is_empty(), "Li6 energy data should not be empty");
let result_explicit = nuclide.microscopic_cross_section(2, Some("294"));
assert!(result_explicit.is_ok(), "Should work with explicit temperature");
let (xs_explicit, energy_explicit) = result_explicit.unwrap();
assert_eq!(xs, xs_explicit, "Results should be identical with/without explicit temperature");
assert_eq!(energy, energy_explicit, "Energy should be identical with/without explicit temperature");
}
#[test]
fn test_microscopic_cross_section_temperature_with_k_suffix() {
let mut nuclide = super::read_nuclide_from_json("tests/Be9.json", None)
.expect("Failed to load Be9.json");
let result_without_k = nuclide.microscopic_cross_section(2, Some("294"));
let result_with_k = nuclide.microscopic_cross_section(2, Some("294K"));
if result_without_k.is_ok() && result_with_k.is_ok() {
let (xs1, energy1) = result_without_k.unwrap();
let (xs2, energy2) = result_with_k.unwrap();
assert_eq!(xs1, xs2, "Temperature with/without K suffix should give same result");
assert_eq!(energy1, energy2, "Energy with/without K suffix should give same result");
} else {
assert!(result_without_k.is_ok() || result_with_k.is_ok(),
"At least one temperature format should work");
}
}
#[test]
fn test_auto_loading_from_config() {
super::clear_nuclide_cache();
{
let mut cfg = crate::config::CONFIG.lock().unwrap();
cfg.set_cross_section("Be9", Some("tests/Be9.json"));
}
let mut nuclide = super::Nuclide {
name: Some("Be9".to_string()),
element: None,
atomic_symbol: None,
atomic_number: None,
neutron_number: None,
mass_number: None,
library: None,
energy: None,
reactions: std::collections::HashMap::new(),
fissionable: false,
available_temperatures: Vec::new(),
loaded_temperatures: Vec::new(),
data_path: None,
};
assert!(nuclide.loaded_temperatures.is_empty(), "Should start with no loaded temperatures");
assert!(nuclide.reactions.is_empty(), "Should start with no reactions");
let result = nuclide.microscopic_cross_section(2, Some("294"));
assert!(result.is_ok(), "Auto-loading should succeed: {:?}", result.err());
let (xs, energy) = result.unwrap();
assert!(!xs.is_empty(), "Auto-loaded cross section data should not be empty");
assert!(!energy.is_empty(), "Auto-loaded energy data should not be empty");
assert!(!nuclide.loaded_temperatures.is_empty(), "Should have loaded temperatures after auto-load");
assert!(!nuclide.reactions.is_empty(), "Should have reactions after auto-load");
assert!(nuclide.available_temperatures.contains(&"294".to_string()), "Should know 294 is available");
assert!(nuclide.available_temperatures.contains(&"300".to_string()), "Should know 300 is available");
{
let mut cfg = crate::config::CONFIG.lock().unwrap();
cfg.cross_sections.remove("Be9");
}
}
#[test]
fn test_auto_loading_additional_temperature() {
super::clear_nuclide_cache();
{
let mut cfg = crate::config::CONFIG.lock().unwrap();
cfg.set_cross_section("Be9", Some("tests/Be9.json"));
}
let temps_filter = std::collections::HashSet::from(["294".to_string()]);
let mut nuclide = super::read_nuclide_from_json("tests/Be9.json", Some(&temps_filter))
.expect("Failed to load Be9.json with temperature filter");
assert_eq!(nuclide.loaded_temperatures, vec!["294".to_string()], "Should only have 294K loaded");
assert!(nuclide.available_temperatures.contains(&"300".to_string()), "Should know 300K is available");
let result = nuclide.microscopic_cross_section(2, Some("300"));
assert!(result.is_ok(), "Auto-loading additional temperature should succeed: {:?}", result.err());
let (xs, energy) = result.unwrap();
assert!(!xs.is_empty(), "Auto-loaded 300K cross section data should not be empty");
assert!(!energy.is_empty(), "Auto-loaded 300K energy data should not be empty");
assert!(nuclide.loaded_temperatures.contains(&"294".to_string()), "Should still have 294K");
assert!(nuclide.loaded_temperatures.contains(&"300".to_string()), "Should now have 300K");
{
let mut cfg = crate::config::CONFIG.lock().unwrap();
cfg.cross_sections.remove("Be9");
}
}
#[test]
fn test_auto_loading_without_config_fails() {
super::clear_nuclide_cache();
{
let mut cfg = crate::config::CONFIG.lock().unwrap();
cfg.cross_sections.remove("TestNuclide");
}
let mut nuclide = super::Nuclide {
name: Some("TestNuclide".to_string()),
element: None,
atomic_symbol: None,
atomic_number: None,
neutron_number: None,
mass_number: None,
library: None,
energy: None,
reactions: std::collections::HashMap::new(),
fissionable: false,
available_temperatures: Vec::new(),
loaded_temperatures: Vec::new(),
data_path: None,
};
let result = nuclide.microscopic_cross_section(2, Some("294"));
assert!(result.is_err(), "Auto-loading without config should fail");
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("No configuration found"), "Error should mention missing configuration");
assert!(error_msg.contains("TestNuclide"), "Error should mention the nuclide name");
assert!(error_msg.contains("Config.set_cross_sections"), "Error should suggest how to fix it");
}
#[test]
fn test_cache_optimization_keyword_vs_path() {
use std::collections::HashMap;
super::clear_nuclide_cache();
let mut li6_map = HashMap::new();
li6_map.insert("Li6".to_string(), "tests/Li6.json".to_string());
let _first = super::get_or_load_nuclide("Li6", &li6_map, None)
.expect("Initial file load failed");
let absolute_path = std::fs::canonicalize("tests/Li6.json").unwrap();
let mut abs_map = HashMap::new();
abs_map.insert("Li6".to_string(), absolute_path.to_string_lossy().to_string());
let _second = super::get_or_load_nuclide("Li6", &abs_map, None)
.expect("Absolute path load failed");
{
let cache = match super::GLOBAL_NUCLIDE_CACHE.lock() {
Ok(cache) => cache,
Err(poisoned) => poisoned.into_inner()
};
let li6_entries: Vec<_> = cache.keys()
.filter(|k| k.starts_with("Li6@") && k.contains("Li6.json"))
.collect();
assert_eq!(li6_entries.len(), 1,
"Expected exactly 1 cache entry for Li6, found: {:?}", li6_entries);
}
}
#[test]
fn test_auto_loading_no_name_fails() {
let mut nuclide = super::Nuclide {
name: None,
element: None,
atomic_symbol: None,
atomic_number: None,
neutron_number: None,
mass_number: None,
library: None,
energy: None,
reactions: std::collections::HashMap::new(),
fissionable: false,
available_temperatures: Vec::new(),
loaded_temperatures: Vec::new(),
data_path: None,
};
let result = nuclide.microscopic_cross_section(2, Some("294"));
assert!(result.is_err(), "Auto-loading without nuclide name should fail");
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("no nuclide name available"), "Error should mention missing name");
}
#[test]
fn test_fendl_3_2c_keyword() {
use crate::url_cache::is_keyword;
assert!(is_keyword("fendl-3.2c"), "fendl-3.2c should be a recognized keyword");
#[cfg(feature = "download")]
{
use crate::url_cache::expand_keyword_to_url;
let expanded = expand_keyword_to_url("fendl-3.2c", "Li6");
assert!(expanded.is_some(), "fendl-3.2c keyword should expand to URL");
let url = expanded.unwrap();
assert!(url.contains("https://raw.githubusercontent.com/fusion-neutronics/cross_section_data_fendl_3.2c"),
"Expanded URL should contain correct base URL");
assert!(url.ends_with("Li6.json"), "Expanded URL should end with nuclide name and .json");
assert_eq!(url, "https://raw.githubusercontent.com/fusion-neutronics/cross_section_data_fendl_3.2c/refs/heads/main/fendl3.2c_data/Li6.json");
}
}
#[test]
fn test_microscopic_cross_section_string_reactions() {
let mut nuclide = super::read_nuclide_from_json("tests/Be9.json", None)
.expect("Failed to load Be9.json");
let result_mt = nuclide.microscopic_cross_section(2, Some("294"));
assert!(result_mt.is_ok(), "Should work with MT number");
let (xs_mt, energy_mt) = result_mt.unwrap();
let result_str = nuclide.microscopic_cross_section("(n,elastic)", Some("294"));
assert!(result_str.is_ok(), "Should work with reaction name string");
let (xs_str, energy_str) = result_str.unwrap();
assert_eq!(xs_mt, xs_str, "Cross section data should be identical for MT 2 and '(n,elastic)'");
assert_eq!(energy_mt, energy_str, "Energy data should be identical for MT 2 and '(n,elastic)'");
let test_cases = vec![
("(n,gamma)", 102),
("(n,p)", 103),
("(n,a)", 107),
];
for (reaction_name, expected_mt) in test_cases {
let result_str = nuclide.microscopic_cross_section(reaction_name, Some("294"));
let result_mt = nuclide.microscopic_cross_section(expected_mt, Some("294"));
if result_str.is_ok() && result_mt.is_ok() {
let (xs_str, energy_str) = result_str.unwrap();
let (xs_mt, energy_mt) = result_mt.unwrap();
assert_eq!(xs_str, xs_mt, "Cross section should match for {} and MT {}", reaction_name, expected_mt);
assert_eq!(energy_str, energy_mt, "Energy should match for {} and MT {}", reaction_name, expected_mt);
}
else {
assert_eq!(result_str.is_ok(), result_mt.is_ok(),
"String and MT results should both succeed or both fail for {} vs MT {}",
reaction_name, expected_mt);
}
}
}
#[test]
fn test_microscopic_cross_section_invalid_reaction_string() {
let mut nuclide = super::read_nuclide_from_json("tests/Be9.json", None)
.expect("Failed to load Be9.json");
let result = nuclide.microscopic_cross_section("(n,invalid)", Some("294"));
assert!(result.is_err(), "Should fail for invalid reaction name");
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Unknown reaction name"), "Error should mention unknown reaction name");
assert!(error_msg.contains("(n,invalid)"), "Error should include the invalid reaction name");
assert!(error_msg.contains("REACTION_MT"), "Error should mention where to find available reactions");
}
#[test]
fn test_microscopic_cross_section_fission_alias() {
let mut nuclide = super::read_nuclide_from_json("tests/Be9.json", None)
.expect("Failed to load Be9.json");
let result = nuclide.microscopic_cross_section("fission", Some("294"));
assert!(result.is_err(), "Should fail because Be9 doesn't have fission reactions");
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("MT 18 not found"), "Error should be about missing MT 18, not unknown reaction name");
}
#[test]
fn test_nuclide_different_data_sources() {
crate::nuclide::clear_nuclide_cache();
let mut li6_file = super::read_nuclide_from_json("tests/Li6.json", None)
.expect("Failed to load Li6 from file");
let mut li7_file = super::read_nuclide_from_json("tests/Li7.json", None)
.expect("Failed to load Li7 from file");
let (xs_li6, _) = li6_file.microscopic_cross_section("(n,gamma)", Some("294"))
.expect("Failed to get Li6 cross section");
let (xs_li7, _) = li7_file.microscopic_cross_section("(n,gamma)", Some("294"))
.expect("Failed to get Li7 cross section");
let data_different = xs_li6.len() != xs_li7.len() ||
xs_li6.iter().zip(&xs_li7).any(|(a, b)| (a - b).abs() > 1e-10);
assert!(data_different, "Li6 and Li7 data should be different");
println!("Li6: {} points, Li7: {} points", xs_li6.len(), xs_li7.len());
}
#[test]
fn test_nuclide_file_vs_keyword_sources() {
crate::nuclide::clear_nuclide_cache();
let mut li6_file = super::read_nuclide_from_json("tests/Li6.json", None)
.expect("Failed to load Li6 from file");
let mut li7_file = super::read_nuclide_from_json("tests/Li7.json", None)
.expect("Failed to load Li7 from file");
assert_eq!(li6_file.name.as_deref(), Some("Li6"));
assert_eq!(li7_file.name.as_deref(), Some("Li7"));
let (xs_li6, _) = li6_file.microscopic_cross_section("(n,gamma)", Some("294"))
.expect("Failed to get Li6 cross section");
let (xs_li7, _) = li7_file.microscopic_cross_section("(n,gamma)", Some("294"))
.expect("Failed to get Li7 cross section");
assert!(!xs_li6.is_empty() && !xs_li7.is_empty(), "Both should have cross section data");
}
#[test]
fn test_nuclide_cache_respects_data_source_boundaries() {
crate::nuclide::clear_nuclide_cache();
let mut li6_1 = super::read_nuclide_from_json("tests/Li6.json", None)
.expect("Failed to load Li6 first time");
let mut li7 = super::read_nuclide_from_json("tests/Li7.json", None)
.expect("Failed to load Li7");
let mut li6_2 = super::read_nuclide_from_json("tests/Li6.json", None)
.expect("Failed to load Li6 second time");
let (xs_li6_1, _) = li6_1.microscopic_cross_section("(n,gamma)", Some("294"))
.expect("Failed to get Li6 cross section first time");
let (xs_li7, _) = li7.microscopic_cross_section("(n,gamma)", Some("294"))
.expect("Failed to get Li7 cross section");
let (xs_li6_2, _) = li6_2.microscopic_cross_section("(n,gamma)", Some("294"))
.expect("Failed to get Li6 cross section second time");
assert_eq!(xs_li6_1, xs_li6_2, "Li6 loads should be identical (cache working)");
let li6_vs_li7_different = xs_li6_1.len() != xs_li7.len() ||
xs_li6_1.iter().zip(&xs_li7).any(|(a, b)| (a - b).abs() > 1e-10);
assert!(li6_vs_li7_different, "Li6 and Li7 should have different data");
}
}