materials_for_mc 0.2.0

Material builder for Monte Carlo simulations
Documentation
use crate::material::Material;
use js_sys::{Array, Map, JSON};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct WasmMaterial {
    inner: Material,
}

#[derive(Serialize, Deserialize)]
struct MacroscopicXsResult {
    energy_grid: Vec<f64>,
    cross_sections: HashMap<i32, Vec<f64>>,
}

#[wasm_bindgen]
impl WasmMaterial {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        WasmMaterial {
            inner: Material::new(),
        }
    }

    #[wasm_bindgen]
    pub fn add_nuclide(&mut self, nuclide: &str, fraction: f64) -> Result<(), JsValue> {
        self.inner
            .add_nuclide(nuclide, fraction)
            .map_err(|e| JsValue::from_str(&e))
    }

    #[wasm_bindgen]
    pub fn add_element(&mut self, element: &str, fraction: f64) -> Result<(), JsValue> {
        self.inner
            .add_element(element, fraction)
            .map_err(|e| JsValue::from_str(&e))
    }

    #[wasm_bindgen]
    pub fn set_density(&mut self, unit: &str, value: f64) -> Result<(), JsValue> {
        self.inner
            .set_density(unit, value)
            .map_err(|e| JsValue::from_str(&e))
    }

    #[wasm_bindgen]
    pub fn set_volume(&mut self, value: f64) -> Result<(), JsValue> {
        self.inner
            .volume(Some(value))
            .map_err(|e| JsValue::from_str(&e))
            .map(|_| ())
    }

    #[wasm_bindgen]
    pub fn set_temperature(&mut self, temperature: &str) {
        self.inner.set_temperature(temperature);
    }

    #[wasm_bindgen]
    pub fn get_nuclides(&self) -> Array {
        let nuclides = self.inner.get_nuclides();
        nuclides
            .into_iter()
            .map(|n| JsValue::from_str(&n))
            .collect::<Array>()
    }

    #[wasm_bindgen]
    pub fn get_atoms_per_barn_cm(&self) -> Result<JsValue, JsValue> {
        // Safe to use try-catch pattern with WASM since panics will be converted to JS exceptions
        if self.inner.density.is_none() {
            return Err(JsValue::from_str(
                "Cannot calculate atoms per cc: Material has no density defined",
            ));
        }

        if self.inner.nuclides.is_empty() {
            return Err(JsValue::from_str(
                "Cannot calculate atoms per cc: Material has no nuclides defined",
            ));
        }

        // Now it's safe to call get_atoms_per_barn_cm without risk of panic
        let atoms_per_barn_cm = self.inner.get_atoms_per_barn_cm();

        let map = Map::new();
        for (nuclide, density) in atoms_per_barn_cm {
            map.set(&JsValue::from_str(&nuclide), &JsValue::from_f64(density));
        }
        Ok(map.into())
    }

    #[wasm_bindgen]
    pub fn calculate_macroscopic_xs(
        &mut self,
        mt_filter: Option<Array>,
        by_nuclide: Option<bool>,
    ) -> Result<JsValue, JsValue> {
        // Check preconditions to avoid panics
        if self.inner.density.is_none() {
            return Err(JsValue::from_str(
                "Cannot calculate macroscopic cross sections: Material has no density defined",
            ));
        }
        if self.inner.nuclides.is_empty() {
            return Err(JsValue::from_str(
                "Cannot calculate macroscopic cross sections: Material has no nuclides defined",
            ));
        }
        // First ensure nuclides are loaded using our WASM-specific function
        if let Err(e) = self.ensure_nuclides_loaded() {
            return Err(JsValue::from_str(&format!("{}", e)));
        }
        // Convert JS Array to Option<Vec<i32>>
        let mt_vec: Vec<i32> = match mt_filter {
            Some(arr) => arr
                .iter()
                .filter_map(|v| v.as_f64().map(|num| num as i32))
                .collect(),
            None => vec![1],
        };
        let by_nuclide = by_nuclide.unwrap_or(false);
        let (energy_grid, xs) = self
            .inner
            .calculate_macroscopic_xs(&mt_vec, by_nuclide);
        let data = MacroscopicXsResult {
            energy_grid,
            cross_sections: xs,
        };
        let serialized = serde_json::to_string(&data)
            .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
        Ok(JSON::parse(&serialized)
            .map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?)
    }

    #[wasm_bindgen]
    pub fn reaction_mts(&mut self) -> Result<Array, JsValue> {
        // Use core implementation now that in-memory nuclides are directly inserted
        match self.inner.reaction_mts() {
            Ok(mts) => Ok(mts.into_iter().map(JsValue::from).collect::<Array>()),
            Err(e) => Err(JsValue::from_str(&format!(
                "Failed to get MT numbers: {}",
                e
            ))),
        }
    }

    #[wasm_bindgen]
    pub fn mean_free_path_neutron(&mut self, energy: f64) -> Option<f64> {
        self.inner.mean_free_path_neutron(energy)
    }

    #[wasm_bindgen]
    pub fn sample_distance_to_collision(&mut self, energy: f64) -> Option<f64> {
        // Use a random number generator compatible with WASM
        use rand::rngs::StdRng;
        use rand::Rng;
        use rand::SeedableRng;
        // For reproducibility, you may want to allow passing a seed, but here we use a random seed
        let mut rng = StdRng::from_entropy();
        self.inner.sample_distance_to_collision(energy, &mut rng)
    }

    #[wasm_bindgen]
    pub fn load_nuclide_data(
        &mut self,
        nuclide_name: &str,
        json_content: &str,
    ) -> Result<(), JsValue> {
        self.inner
            .load_nuclide_from_json_str(nuclide_name, json_content)
            .map_err(|e| JsValue::from_str(&format!("Failed to load nuclide data: {}", e)))
    }

    #[wasm_bindgen]
    pub fn to_string(&self) -> String {
        format!("{:?}", self.inner)
    }
}

// Remove custom ensure_nuclides_loaded override since core handles file based loading; keep thin wrapper for WASM inserted data
impl WasmMaterial {
    pub fn ensure_nuclides_loaded(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        Ok(())
    }
}

#[wasm_bindgen]
impl WasmMaterial {
    /// Sample which nuclide a neutron interacts with at a given energy, using per-nuclide macroscopic total xs
    /// Returns the nuclide name as a String. If seed is provided, uses it for reproducibility.
    #[wasm_bindgen(js_name = sampleInteractingNuclide)]
    pub fn sample_interacting_nuclide_wasm(&self, energy: f64, seed: Option<u64>) -> String {
        use rand::rngs::StdRng;
        use rand::SeedableRng;
        let mut rng = match seed {
            Some(s) => StdRng::seed_from_u64(s),
            None => StdRng::seed_from_u64(12345),
        };
        self.inner.sample_interacting_nuclide(energy, &mut rng)
    }
}