use std::f64::consts::PI;
use crate::constants::{AVOGADRO, PLANCK_HC, R_ELECTRON_CM};
use crate::db::XrayDb;
use crate::elam::{CrossSectionKind, clamp_log_energy};
use crate::error::{Result, XrayDbError};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RefractiveIndex {
pub delta: f64,
pub beta: f64,
pub attenuation_length_cm: f64,
}
fn validate_density(density: f64) -> Result<()> {
if density.is_finite() && density > 0.0 {
return Ok(());
}
Err(XrayDbError::invalid_input(format!(
"density must be finite and > 0 g/cm³, got {density}"
)))
}
impl XrayDb {
pub fn material_mu_at(
&self,
formula: &str,
density: f64,
energy: f64,
kind: CrossSectionKind,
) -> Result<f64> {
validate_density(density)?;
let fractions = self.mass_fractions(formula)?;
self.mu_from_fractions_at(&fractions, density, energy, kind)
}
pub fn material_mu(
&self,
formula: &str,
density: f64,
energies: &[f64],
kind: CrossSectionKind,
) -> Result<Vec<f64>> {
validate_density(density)?;
let fractions = self.mass_fractions(formula)?;
self.mu_from_fractions(&fractions, density, energies, kind)
}
pub fn material_mu_from_mass_fractions(
&self,
composition: &[(&str, f64)],
density: f64,
energies: &[f64],
kind: CrossSectionKind,
) -> Result<Vec<f64>> {
validate_density(density)?;
let fractions = self.normalize_mass_fractions(composition)?;
self.mu_from_fractions(&fractions, density, energies, kind)
}
pub fn mass_fractions(&self, formula: &str) -> Result<Vec<(String, f64)>> {
if formula.contains("wt%") {
return self.weight_percent_mass_fractions(formula);
}
self.stoichiometric_mass_fractions(formula)
}
fn weight_percent_mass_fractions(&self, formula: &str) -> Result<Vec<(String, f64)>> {
let compact: String = formula.chars().filter(|c| !c.is_whitespace()).collect();
let mut components: Vec<(String, Option<f64>)> = Vec::new();
let mut rest = compact.as_str();
while let Some(marker) = rest.find("wt%") {
let (head, tail) = rest.split_at(marker);
let split = head
.rfind(|c: char| !(c.is_ascii_digit() || c == '.'))
.map_or(0, |i| i + 1);
let (sub_formula, percent_text) = head.split_at(split);
let percent: f64 = percent_text.parse().map_err(|_| {
XrayDbError::invalid_formula(
formula,
format!("expected a number before 'wt%', found '{percent_text}'"),
)
})?;
if !percent.is_finite() || percent < 0.0 {
return Err(XrayDbError::invalid_formula(
formula,
format!("weight percent must be finite and >= 0, got {percent}"),
));
}
components.push((sub_formula.to_string(), Some(percent)));
rest = &tail["wt%".len()..];
}
if !rest.is_empty() {
components.push((rest.to_string(), None));
}
let named_total: f64 = components.iter().filter_map(|&(_, p)| p).sum();
let balance_count = components.iter().filter(|(_, p)| p.is_none()).count();
if balance_count > 1 {
return Err(XrayDbError::invalid_formula(
formula,
"a wt% mixture may have at most one balance component",
));
}
if named_total > 100.0 + 1e-9 {
return Err(XrayDbError::invalid_formula(
formula,
format!("weight percentages sum to {named_total}%, which exceeds 100%"),
));
}
let balance_weight = if balance_count == 1 {
100.0 - named_total
} else {
0.0
};
let mut totals: Vec<(String, f64)> = Vec::new();
let mut grand_total = 0.0_f64;
for (sub_formula, percent) in &components {
let weight = percent.unwrap_or(balance_weight) / 100.0;
if weight <= 0.0 {
continue;
}
for (symbol, fraction) in self.stoichiometric_mass_fractions(sub_formula)? {
let scaled = weight * fraction;
match totals.iter_mut().find(|(s, _)| *s == symbol) {
Some((_, acc)) => *acc += scaled,
None => totals.push((symbol, scaled)),
}
grand_total += scaled;
}
}
if !grand_total.is_finite() || grand_total <= 0.0 {
return Err(XrayDbError::invalid_formula(
formula,
"wt% mixture has zero total weight",
));
}
totals.sort_by(|a, b| a.0.cmp(&b.0));
for (_, fraction) in &mut totals {
*fraction /= grand_total;
}
Ok(totals)
}
fn stoichiometric_mass_fractions(&self, formula: &str) -> Result<Vec<(String, f64)>> {
let composition = self.parse_formula(formula)?;
let mut masses: Vec<(String, f64)> = Vec::with_capacity(composition.len());
let mut total_mass = 0.0_f64;
for (symbol, count) in &composition {
if !count.is_finite() || *count < 0.0 {
return Err(XrayDbError::invalid_formula(
formula,
format!("invalid stoichiometry for {symbol}: {count}"),
));
}
if *count == 0.0 {
continue;
}
let mass = count * self.molar_mass(symbol)?;
if !mass.is_finite() || mass < 0.0 {
return Err(XrayDbError::invalid_formula(
formula,
format!("invalid mass contribution for {symbol}"),
));
}
masses.push((symbol.clone(), mass));
total_mass += mass;
}
if !total_mass.is_finite() || total_mass <= 0.0 {
return Err(XrayDbError::invalid_formula(formula, "zero total mass"));
}
for (_, mass) in &mut masses {
*mass /= total_mass;
}
Ok(masses)
}
pub fn xray_delta_beta(
&self,
formula: &str,
density: f64,
energy: f64,
) -> Result<RefractiveIndex> {
validate_density(density)?;
let fractions = self.mass_fractions(formula)?;
let mut moles: Vec<(String, f64)> = Vec::with_capacity(fractions.len());
for (symbol, weight) in fractions {
let molar_mass = self.molar_mass(&symbol)?;
if molar_mass.is_nan() || molar_mass <= 0.0 {
return Err(XrayDbError::invalid_formula(
formula,
format!("element {symbol} has non-positive molar mass"),
));
}
moles.push((symbol, weight / molar_mass));
}
self.delta_beta_from_amounts(formula, &moles, density, energy)
}
fn delta_beta_from_amounts(
&self,
formula: &str,
composition: &[(String, f64)],
density: f64,
energy: f64,
) -> Result<RefractiveIndex> {
let wavelength = 1.0e-7 * PLANCK_HC / energy;
let mut total_weight = 0.0_f64;
let mut sum_f1 = 0.0_f64;
let mut sum_f2 = 0.0_f64;
for (symbol, count) in composition {
total_weight += count * self.molar_mass(symbol)?;
let z = f64::from(self.atomic_number(symbol)?);
sum_f1 += count * (z + self.f1_chantler_at(symbol, energy)?);
sum_f2 += count * self.f2_chantler_at(symbol, energy)?;
}
if !total_weight.is_finite() || total_weight <= 0.0 {
return Err(XrayDbError::invalid_formula(formula, "zero total mass"));
}
let prefactor = R_ELECTRON_CM * wavelength * wavelength * density * AVOGADRO
/ (2.0 * PI * total_weight);
let delta = prefactor * sum_f1;
let beta = prefactor * sum_f2;
let attenuation_length_cm = if beta > 0.0 {
wavelength / (4.0 * PI * beta)
} else {
f64::INFINITY
};
Ok(RefractiveIndex {
delta,
beta,
attenuation_length_cm,
})
}
fn normalize_mass_fractions(&self, composition: &[(&str, f64)]) -> Result<Vec<(String, f64)>> {
if composition.is_empty() {
return Err(XrayDbError::invalid_input(
"composition mass fractions must not be empty",
));
}
let mut by_symbol: Vec<(String, f64)> = Vec::with_capacity(composition.len());
let mut total = 0.0_f64;
for &(symbol, fraction) in composition {
if !fraction.is_finite() || fraction < 0.0 {
return Err(XrayDbError::invalid_input(format!(
"invalid mass fraction for '{symbol}': {fraction}"
)));
}
if fraction == 0.0 {
continue;
}
let canonical = self.symbol(symbol)?.to_string();
match by_symbol.iter_mut().find(|(s, _)| *s == canonical) {
Some((_, acc)) => *acc += fraction,
None => by_symbol.push((canonical, fraction)),
}
total += fraction;
}
if !total.is_finite() || total <= 0.0 {
return Err(XrayDbError::invalid_input(
"composition mass fractions must include at least one positive value",
));
}
by_symbol.sort_by(|a, b| a.0.cmp(&b.0));
for (_, fraction) in &mut by_symbol {
*fraction /= total;
}
Ok(by_symbol)
}
pub(crate) fn mu_from_fractions_at(
&self,
fractions: &[(String, f64)],
density: f64,
energy: f64,
kind: CrossSectionKind,
) -> Result<f64> {
let log_energy = clamp_log_energy(energy);
let mut mu = 0.0_f64;
for (symbol, fraction) in fractions {
if *fraction == 0.0 {
continue;
}
mu += fraction * self.mu_elam_sym(symbol, symbol, log_energy, kind)?;
}
Ok(mu * density)
}
pub(crate) fn mu_from_fractions(
&self,
fractions: &[(String, f64)],
density: f64,
energies: &[f64],
kind: CrossSectionKind,
) -> Result<Vec<f64>> {
let mut mu = vec![0.0_f64; energies.len()];
for (symbol, fraction) in fractions {
if *fraction == 0.0 {
continue;
}
let elem_mu = self.mu_elam(symbol, energies, kind)?;
for (acc, &value) in mu.iter_mut().zip(elem_mu.iter()) {
*acc += fraction * value;
}
}
for value in &mut mu {
*value *= density;
}
Ok(mu)
}
}