pub mod lcf;
pub mod mcr;
pub mod metrics;
pub mod pca;
pub mod peakfit;
use std::borrow::{Borrow, Cow};
use nalgebra::DVector;
use serde::{Deserialize, Serialize};
use super::errors::AnalysisError;
use super::normalization::Normalization;
use super::tools::{dmude, interp_linear};
use super::xasspectrum::XASSpectrum;
pub use lcf::{
lcf, lcf_batch, lcf_batch_with_progress, lcf_combinatorial, LcfComponent, LcfConfig, LcfResult,
};
pub use mcr::{mcr_als, mcr_als_with_progress, McrAnchor, McrConfig, McrResult, McrTermination};
pub use pca::{
pca_train, PcaConfig, PcaCountBasis, PcaCountSuggestion, PcaFit, PcaModel,
PcaReconstructionError,
};
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
pub enum AnalysisSpace {
#[default]
Norm,
Flat,
Deriv,
Chi {
kweight: f64,
},
}
pub type LcfSpace = AnalysisSpace;
impl AnalysisSpace {
pub fn is_k_space(&self) -> bool {
matches!(self, Self::Chi { .. })
}
pub fn default_range(&self) -> (f64, f64) {
if self.is_k_space() {
(3.0, 12.0)
} else {
(-20.0, 30.0)
}
}
pub fn arrays(
&self,
spectrum: &XASSpectrum,
) -> Result<(DVector<f64>, DVector<f64>), AnalysisError> {
let input = AnalysisInput::new(spectrum, *self)?;
Ok((input.x, input.y))
}
fn prepare<'a>(
&self,
spectrum: &'a XASSpectrum,
) -> Result<Cow<'a, XASSpectrum>, AnalysisError> {
let ready = match self {
Self::Norm | Self::Deriv => spectrum
.normalization
.as_ref()
.and_then(|n| n.get_norm())
.is_some(),
Self::Flat => spectrum
.normalization
.as_ref()
.and_then(|n| n.get_flat())
.is_some(),
Self::Chi { .. } => spectrum.k().is_some() && spectrum.chi().is_some(),
};
if ready {
return Ok(Cow::Borrowed(spectrum));
}
if *self == Self::Flat
&& spectrum.preserves_prepared_values()
&& spectrum.prepared_space() == Some(Self::Norm)
{
return Err(AnalysisError::InvalidInput {
spectrum: spectrum_label(spectrum, "unnamed spectrum".into()),
reason: "a prepared Norm component has no Flat array; explicitly set a normalization method to refit it, or select Norm".into(),
});
}
let mut prepared = spectrum.clone();
let result = if self.is_k_space() {
prepared.calc_background()
} else {
prepared.normalize()
};
result.map_err(|source| AnalysisError::Preparation {
spectrum: spectrum_label(spectrum, "unnamed spectrum".into()),
source: Box::new(source),
})?;
Ok(Cow::Owned(prepared))
}
fn stored_arrays(
&self,
spectrum: &XASSpectrum,
) -> Result<(DVector<f64>, DVector<f64>), AnalysisError> {
let missing = |field: &str| AnalysisError::MissingArray {
field: field.to_string(),
};
match self {
Self::Norm | Self::Flat | Self::Deriv => {
let energy = spectrum.energy.clone().ok_or_else(|| missing("energy"))?;
let y = match self {
Self::Flat => spectrum
.flat()
.ok_or_else(|| missing("flat (run normalize() first)"))?,
_ => spectrum
.norm()
.ok_or_else(|| missing("norm (run normalize() first)"))?,
};
if energy.len() != y.len() {
return Err(super::errors::DataError::LengthMismatch {
energy_len: energy.len(),
mu_len: y.len(),
}
.into());
}
if energy.len() < 2 {
return Err(super::errors::DataError::InsufficientData {
min: 2,
actual: energy.len(),
}
.into());
}
let y = if matches!(self, Self::Deriv) {
dmude(&energy, &y)
} else {
y
};
Ok((energy, y))
}
Self::Chi { kweight } => {
let k = spectrum
.k()
.ok_or_else(|| missing("k (run calc_background() first)"))?;
let chi = spectrum
.chi()
.ok_or_else(|| missing("chi (run calc_background() first)"))?;
if k.len() != chi.len() {
return Err(super::errors::DataError::LengthMismatch {
energy_len: k.len(),
mu_len: chi.len(),
}
.into());
}
let y = DVector::from_fn(k.len(), |i, _| chi[i] * k[i].powf(*kweight));
Ok((DVector::from_column_slice(k), y))
}
}
}
}
pub(crate) fn spectrum_e0(spectrum: &XASSpectrum) -> Option<f64> {
spectrum
.e0
.or_else(|| spectrum.normalization.as_ref().and_then(|n| n.get_e0()))
}
pub(crate) fn spectrum_label(spectrum: &XASSpectrum, fallback: String) -> String {
spectrum.name.clone().unwrap_or(fallback)
}
pub(crate) struct AnalysisInput {
pub x: DVector<f64>,
pub y: DVector<f64>,
pub e0: Option<f64>,
pub label: String,
}
impl AnalysisInput {
pub fn new(spectrum: &XASSpectrum, space: AnalysisSpace) -> Result<Self, AnalysisError> {
Self::with_label(spectrum, space, "unnamed spectrum".into())
}
pub fn with_label(
spectrum: &XASSpectrum,
space: AnalysisSpace,
fallback: String,
) -> Result<Self, AnalysisError> {
let label = spectrum_label(spectrum, fallback);
let prepared = space.prepare(spectrum).map_err(|error| match error {
AnalysisError::Preparation { source, .. } => AnalysisError::Preparation {
spectrum: label.clone(),
source,
},
AnalysisError::InvalidInput { reason, .. } => AnalysisError::InvalidInput {
spectrum: label.clone(),
reason,
},
other => other,
})?;
let (x, y) = space.stored_arrays(&prepared)?;
if x.len() < 2
|| x.iter().chain(y.iter()).any(|v| !v.is_finite())
|| x.as_slice().windows(2).any(|w| w[0] >= w[1])
{
return Err(AnalysisError::InvalidInput {
spectrum: label,
reason: "need at least two finite samples on a strictly increasing axis".into(),
});
}
Ok(Self {
x,
y,
e0: spectrum_e0(&prepared),
label,
})
}
pub fn bounds(
&self,
space: AnalysisSpace,
range: Option<(f64, f64)>,
) -> Result<(f64, f64), AnalysisError> {
let (lo, hi) = range.unwrap_or_else(|| space.default_range());
validate_range(lo, hi)?;
let origin = if space.is_k_space() {
0.0
} else {
self.e0
.filter(|v| v.is_finite())
.ok_or_else(|| AnalysisError::InvalidInput {
spectrum: self.label.clone(),
reason: "need a finite E0 to resolve energy offsets".into(),
})?
};
let bounds = (origin + lo, origin + hi);
validate_range(bounds.0, bounds.1)?;
Ok(bounds)
}
pub fn select(
&self,
bounds: (f64, f64),
min: usize,
) -> Result<(DVector<f64>, DVector<f64>), AnalysisError> {
self.cover(bounds)?;
let (lo, hi) = bounds;
let idx: Vec<_> = (0..self.x.len())
.filter(|&i| self.x[i] >= lo && self.x[i] <= hi)
.collect();
if idx.len() < min {
return Err(AnalysisError::EmptyRange {
lo,
hi,
n_points: idx.len(),
min,
});
}
Ok((
DVector::from_iterator(idx.len(), idx.iter().map(|&i| self.x[i])),
DVector::from_iterator(idx.len(), idx.iter().map(|&i| self.y[i])),
))
}
pub fn cover(&self, (lo, hi): (f64, f64)) -> Result<(), AnalysisError> {
let available_lo = self.x[0];
let available_hi = self.x[self.x.len() - 1];
if available_lo > lo || available_hi < hi {
return Err(AnalysisError::IncompleteCoverage {
spectrum: self.label.clone(),
lo,
hi,
available_lo,
available_hi,
});
}
Ok(())
}
pub fn interpolate(
&self,
grid: &DVector<f64>,
bounds: (f64, f64),
) -> Result<DVector<f64>, AnalysisError> {
self.cover(bounds)?;
Ok(interp_linear(grid, &self.x, &self.y)?)
}
}
pub fn validate_range(lo: f64, hi: f64) -> Result<(), AnalysisError> {
if !lo.is_finite() || !hi.is_finite() || lo >= hi {
return Err(AnalysisError::InvalidRange { lo, hi });
}
Ok(())
}
pub(crate) fn on_grid(
spectrum: &XASSpectrum,
space: AnalysisSpace,
grid: &DVector<f64>,
) -> Result<DVector<f64>, AnalysisError> {
let input = AnalysisInput::new(spectrum, space)?;
input.interpolate(grid, (grid[0], grid[grid.len() - 1]))
}
pub fn r_factor(data: &DVector<f64>, fit: &DVector<f64>) -> f64 {
let num: f64 = data
.iter()
.zip(fit.iter())
.map(|(d, f)| (d - f).powi(2))
.sum();
let den: f64 = data.iter().map(|d| d * d).sum();
if den > 0.0 {
num / den
} else {
f64::NAN
}
}
pub(crate) fn as_refs<S: Borrow<XASSpectrum>>(spectra: &[S]) -> Vec<&XASSpectrum> {
spectra.iter().map(|s| s.borrow()).collect()
}