use crate::xasdata::reader::ColumnFile;
#[derive(Clone, Debug)]
pub enum MuSpec {
Transmission {
energy: usize,
i0: usize,
it: usize,
},
Fluorescence {
energy: usize,
i0: usize,
channels: Vec<usize>,
},
Reference {
energy: usize,
it: usize,
iref: usize,
},
Raw {
energy: usize,
mu: usize,
},
}
#[derive(Debug, PartialEq, Eq)]
pub enum XmuError {
BadColumn(usize),
NoChannels,
}
impl std::fmt::Display for XmuError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
XmuError::BadColumn(i) => write!(f, "column index {i} is out of range"),
XmuError::NoChannels => write!(f, "no fluorescence channels selected"),
}
}
}
impl std::error::Error for XmuError {}
fn col(cf: &ColumnFile, i: usize) -> Result<&[f64], XmuError> {
cf.column(i).ok_or(XmuError::BadColumn(i))
}
impl MuSpec {
pub fn energy_col(&self) -> usize {
match *self {
MuSpec::Transmission { energy, .. }
| MuSpec::Fluorescence { energy, .. }
| MuSpec::Reference { energy, .. }
| MuSpec::Raw { energy, .. } => energy,
}
}
}
pub fn build_mu(cf: &ColumnFile, spec: &MuSpec) -> Result<(Vec<f64>, Vec<f64>), XmuError> {
let energy = col(cf, spec.energy_col())?.to_vec();
let mu = match spec {
MuSpec::Transmission { i0, it, .. } => {
let i0 = col(cf, *i0)?;
let it = col(cf, *it)?;
transmission_mu(i0, it)
}
MuSpec::Fluorescence { i0, channels, .. } => {
if channels.is_empty() {
return Err(XmuError::NoChannels);
}
let i0 = col(cf, *i0)?;
let mut chans: Vec<&[f64]> = Vec::with_capacity(channels.len());
for &c in channels {
chans.push(col(cf, c)?);
}
let signal = sum_channels(&chans);
fluorescence_mu(&signal, i0)
}
MuSpec::Reference { it, iref, .. } => {
let it = col(cf, *it)?;
let iref = col(cf, *iref)?;
transmission_mu(it, iref)
}
MuSpec::Raw { mu, .. } => col(cf, *mu)?.to_vec(),
};
Ok((energy, mu))
}
pub fn transmission_mu(num: &[f64], den: &[f64]) -> Vec<f64> {
num.iter().zip(den).map(|(&n, &d)| (n / d).ln()).collect()
}
pub fn fluorescence_mu(signal: &[f64], i0: &[f64]) -> Vec<f64> {
signal.iter().zip(i0).map(|(&s, &d)| s / d).collect()
}
pub fn sum_channels(channels: &[&[f64]]) -> Vec<f64> {
let n = channels.iter().map(|c| c.len()).max().unwrap_or(0);
let mut out = vec![0.0; n];
for ch in channels {
for (o, &v) in out.iter_mut().zip(ch.iter()) {
*o += v;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xasdata::reader::ColumnFile;
fn cf3() -> ColumnFile {
ColumnFile::from_text("# energy i0 it\n100 10 5\n200 20 8\n").unwrap()
}
#[test]
fn transmission_matches_ln_ratio() {
let cf = cf3();
let spec = MuSpec::Transmission {
energy: 0,
i0: 1,
it: 2,
};
let (e, mu) = build_mu(&cf, &spec).unwrap();
assert_eq!(e, vec![100.0, 200.0]);
assert!((mu[0] - (10.0f64 / 5.0).ln()).abs() < 1e-15);
assert!((mu[1] - (20.0f64 / 8.0).ln()).abs() < 1e-15);
}
#[test]
fn fluorescence_sums_channels_over_i0() {
let cf = ColumnFile::from_text("# e i0 c1 c2\n1 100 3 4\n2 200 6 8\n").unwrap();
let spec = MuSpec::Fluorescence {
energy: 0,
i0: 1,
channels: vec![2, 3],
};
let (_e, mu) = build_mu(&cf, &spec).unwrap();
assert!((mu[0] - 7.0 / 100.0).abs() < 1e-15);
assert!((mu[1] - 14.0 / 200.0).abs() < 1e-15);
}
#[test]
fn raw_passes_column_through() {
let cf = ColumnFile::from_text("# e mu\n1 0.5\n2 1.5\n").unwrap();
let spec = MuSpec::Raw { energy: 0, mu: 1 };
let (_e, mu) = build_mu(&cf, &spec).unwrap();
assert_eq!(mu, vec![0.5, 1.5]);
}
#[test]
fn bad_column_errors() {
let cf = cf3();
let spec = MuSpec::Transmission {
energy: 0,
i0: 1,
it: 9,
};
assert_eq!(build_mu(&cf, &spec), Err(XmuError::BadColumn(9)));
}
#[test]
fn empty_channels_error() {
let cf = cf3();
let spec = MuSpec::Fluorescence {
energy: 0,
i0: 1,
channels: vec![],
};
assert_eq!(build_mu(&cf, &spec), Err(XmuError::NoChannels));
}
#[test]
fn sum_channels_handles_ragged() {
let a = [1.0, 2.0, 3.0];
let b = [10.0, 20.0];
assert_eq!(sum_channels(&[&a, &b]), vec![11.0, 22.0, 3.0]);
assert_eq!(sum_channels(&[]), Vec::<f64>::new());
}
}