mod formula;
#[cfg(test)]
mod tests;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{collections::BTreeMap, io::Write, sync::OnceLock};
use xraydb::{CrossSectionKind, XrayDb};
#[derive(Debug, thiserror::Error)]
pub enum AtomicDataError {
#[error("Atomic reference: {0}")]
Invalid(String),
#[error("Atomic data unavailable: {0}")]
Unavailable(String),
#[error("The recorded atomic reference is not available in this build")]
VersionMismatch,
}
type Result<T> = std::result::Result<T, AtomicDataError>;
fn invalid(e: impl std::fmt::Display) -> AtomicDataError {
AtomicDataError::Invalid(e.to_string())
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AtomicDataIdentity {
pub provider: String,
pub data_version: String,
pub data_sha256: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AtomicTable {
ChantlerF2LogLogV1,
ElamTotalV1,
ElamTransitionsV1,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AtomicReference {
pub data: AtomicDataIdentity,
pub table: AtomicTable,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AtomicCurve {
pub energy_ev: Vec<f64>,
pub values: Vec<f64>,
pub unit: String,
pub reference: AtomicReference,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AtomicEdge {
pub element: String,
pub edge: String,
pub energy_ev: f64,
pub reference: AtomicReference,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AtomicLine {
pub name: String,
pub energy_ev: f64,
pub intensity: f64,
pub initial_level: String,
pub final_level: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum EmissionSelection {
Line(String),
Family(String),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AtomicEmission {
pub element: String,
pub selection: EmissionSelection,
pub energy_ev: f64,
pub lines: Vec<AtomicLine>,
pub reference: AtomicReference,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ElementFraction {
pub element: String,
pub atoms: f64,
pub molar_mass: f64,
pub mass_fraction: f64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CompoundAttenuation {
pub formula: String,
pub composition: Vec<ElementFraction>,
pub curve: AtomicCurve,
}
pub trait AtomicDataProvider: Send + Sync {
fn identity(&self) -> &AtomicDataIdentity;
fn edge(&self, element: &str, edge: &str) -> Result<AtomicEdge>;
fn edges(&self, element: &str) -> Result<Vec<AtomicEdge>>;
fn emission(&self, element: &str, selection: &EmissionSelection) -> Result<AtomicEmission>;
fn f2(&self, element: &str, energy_ev: &[f64]) -> Result<AtomicCurve>;
fn attenuation(&self, element: &str, energy_ev: &[f64]) -> Result<AtomicCurve>;
fn compound_attenuation(&self, formula: &str, energy_ev: &[f64])
-> Result<CompoundAttenuation>;
}
#[derive(Clone, Copy)]
pub struct AtomicData {
db: XrayDb,
identity: &'static AtomicDataIdentity,
}
static IDENTITY: OnceLock<std::result::Result<AtomicDataIdentity, String>> = OnceLock::new();
impl AtomicData {
pub fn new() -> Result<Self> {
let db = XrayDb::try_new().map_err(|e| AtomicDataError::Unavailable(e.to_string()))?;
let identity = IDENTITY
.get_or_init(|| {
struct HashWriter(Sha256);
impl Write for HashWriter {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.0.update(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut writer = HashWriter(Sha256::new());
serde_json::to_writer(&mut writer, db.raw()).map_err(|e| e.to_string())?;
Ok(AtomicDataIdentity {
provider: "xraydb-rs/0.4.1/rexafs-atomic-v1".into(),
data_version: db
.raw()
.version
.iter()
.max_by_key(|v| {
v.tag
.split('.')
.map(|n| n.parse::<u32>().unwrap_or(0))
.collect::<Vec<_>>()
})
.map_or("unknown", |v| v.tag.as_str())
.into(),
data_sha256: writer
.0
.finalize()
.iter()
.map(|b| format!("{b:02x}"))
.collect(),
})
})
.as_ref()
.map_err(|e| AtomicDataError::Unavailable(e.clone()))?;
Ok(Self { db, identity })
}
pub fn identity(&self) -> &AtomicDataIdentity {
self.identity
}
pub fn require_reference(&self, reference: &AtomicReference) -> Result<()> {
if &reference.data != self.identity {
return Err(AtomicDataError::VersionMismatch);
}
Ok(())
}
fn reference(&self, table: AtomicTable) -> AtomicReference {
AtomicReference {
data: self.identity.clone(),
table,
}
}
pub fn edge(&self, element: &str, edge: &str) -> Result<AtomicEdge> {
let symbol = self.db.symbol(element).map_err(invalid)?;
let edge = edge.to_ascii_uppercase();
let energy_ev = self.db.xray_edge(symbol, &edge).map_err(invalid)?.energy;
if !energy_ev.is_finite() || energy_ev <= 0. {
return Err(invalid("invalid tabulated edge"));
}
Ok(AtomicEdge {
element: symbol.into(),
edge,
energy_ev,
reference: self.reference(AtomicTable::ElamTransitionsV1),
})
}
pub fn edges(&self, element: &str) -> Result<Vec<AtomicEdge>> {
let mut labels = self
.db
.xray_edges(element)
.map_err(invalid)?
.into_keys()
.collect::<Vec<_>>();
labels.sort();
labels.iter().map(|e| self.edge(element, e)).collect()
}
pub fn emission(&self, element: &str, selection: &EmissionSelection) -> Result<AtomicEmission> {
let symbol = self.db.symbol(element).map_err(invalid)?;
let wanted = match selection {
EmissionSelection::Line(s) | EmissionSelection::Family(s) => s,
};
if wanted.is_empty() {
return Err(invalid("select an emission line or family"));
}
let mut lines = self
.db
.xray_lines(symbol, None, None)
.map_err(invalid)?
.into_iter()
.filter(|(name, _)| match selection {
EmissionSelection::Line(_) => name == wanted,
EmissionSelection::Family(_) => {
name.trim_end_matches(|c: char| c.is_ascii_digit()) == wanted
}
})
.map(|(name, line)| AtomicLine {
name,
energy_ev: line.energy,
intensity: line.intensity,
initial_level: line.initial_level,
final_level: line.final_level,
})
.collect::<Vec<_>>();
lines.sort_by(|a, b| a.name.cmp(&b.name));
let first = lines
.first()
.ok_or_else(|| invalid(format!("unknown emission {wanted} for {symbol}")))?;
if lines.iter().any(|l| l.initial_level != first.initial_level) {
return Err(invalid(
"emission family spans different initial shells; select one line",
));
}
let sum = lines.iter().map(|l| l.intensity).sum::<f64>();
if !sum.is_finite()
|| sum <= 0.
|| lines.iter().any(|l| {
!l.energy_ev.is_finite()
|| l.energy_ev <= 0.
|| !l.intensity.is_finite()
|| l.intensity < 0.
})
{
return Err(invalid("invalid emission records"));
}
let energy_ev = lines
.iter()
.map(|l| l.energy_ev * (l.intensity / sum))
.sum();
Ok(AtomicEmission {
element: symbol.into(),
selection: selection.clone(),
energy_ev,
lines,
reference: self.reference(AtomicTable::ElamTransitionsV1),
})
}
pub fn f2(&self, element: &str, energy_ev: &[f64]) -> Result<AtomicCurve> {
checked_energies(
energy_ev,
self.db.chantler_energy_range(element).map_err(invalid)?,
)?;
self.curve(
energy_ev,
self.db.f2_chantler(element, energy_ev).map_err(invalid)?,
"electron",
AtomicTable::ChantlerF2LogLogV1,
)
}
pub fn attenuation(&self, element: &str, energy_ev: &[f64]) -> Result<AtomicCurve> {
checked_energies(energy_ev, XrayDb::elam_energy_range())?;
self.curve(
energy_ev,
self.db
.mu_elam(element, energy_ev, CrossSectionKind::Total)
.map_err(invalid)?,
"cm^2/g",
AtomicTable::ElamTotalV1,
)
}
pub fn compound_attenuation(
&self,
formula: &str,
energy_ev: &[f64],
) -> Result<CompoundAttenuation> {
let atoms = formula::parse(formula, &self.db)?;
let mut composition = Vec::new();
let mut total = 0.;
for (element, atoms) in atoms {
let molar_mass = self.db.molar_mass(&element).map_err(invalid)?;
total += atoms * molar_mass;
composition.push(ElementFraction {
element,
atoms,
molar_mass,
mass_fraction: 0.,
});
}
if !total.is_finite() || total <= 0. {
return Err(invalid("formula mass must be finite and positive"));
}
let mut values = vec![0.; energy_ev.len()];
for part in &mut composition {
part.mass_fraction = part.atoms * part.molar_mass / total;
let attenuation = self.attenuation(&part.element, energy_ev)?;
for (sum, value) in values.iter_mut().zip(attenuation.values) {
*sum += part.mass_fraction * value;
}
}
let curve = self.curve(energy_ev, values, "cm^2/g", AtomicTable::ElamTotalV1)?;
Ok(CompoundAttenuation {
formula: formula.into(),
composition,
curve,
})
}
fn curve(
&self,
energy_ev: &[f64],
values: Vec<f64>,
unit: &str,
table: AtomicTable,
) -> Result<AtomicCurve> {
if values.len() != energy_ev.len() || values.iter().any(|v| !v.is_finite() || *v <= 0.) {
return Err(invalid("tabulated values must be finite and positive"));
}
Ok(AtomicCurve {
energy_ev: energy_ev.to_vec(),
values,
unit: unit.into(),
reference: self.reference(table),
})
}
}
fn checked_energies(energy: &[f64], range: (f64, f64)) -> Result<()> {
if energy.is_empty() {
return Err(invalid("provide at least one energy in eV"));
}
for (i, &value) in energy.iter().enumerate() {
if !value.is_finite() || value < range.0 || value > range.1 || value <= 0. {
return Err(invalid(format!(
"energy[{i}]={value} eV is outside {}..={} eV",
range.0, range.1
)));
}
}
Ok(())
}
impl AtomicDataProvider for AtomicData {
fn identity(&self) -> &AtomicDataIdentity {
self.identity()
}
fn edge(&self, e: &str, s: &str) -> Result<AtomicEdge> {
self.edge(e, s)
}
fn edges(&self, e: &str) -> Result<Vec<AtomicEdge>> {
self.edges(e)
}
fn emission(&self, e: &str, s: &EmissionSelection) -> Result<AtomicEmission> {
self.emission(e, s)
}
fn f2(&self, e: &str, x: &[f64]) -> Result<AtomicCurve> {
self.f2(e, x)
}
fn attenuation(&self, e: &str, x: &[f64]) -> Result<AtomicCurve> {
self.attenuation(e, x)
}
fn compound_attenuation(&self, f: &str, x: &[f64]) -> Result<CompoundAttenuation> {
self.compound_attenuation(f, x)
}
}