use super::{measure, Metric, MetricError};
use crate::xafs::{
analysis::{spectrum_e0, AnalysisInput, AnalysisSpace},
xasspectrum::XASSpectrum,
};
use serde::{Deserialize, Serialize};
use std::ops::RangeInclusive;
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
pub enum MeasurementSpace {
Mu,
#[default]
Norm,
Flat,
Chi {
kweight: u8,
},
Fourier,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
pub enum AxisOrigin {
Absolute,
#[default]
E0,
Reference {
energy_ev: f64,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Measurement {
pub metric: Metric,
pub space: MeasurementSpace,
pub origin: AxisOrigin,
}
impl Measurement {
fn new(metric: Metric) -> Self {
Self {
metric,
space: MeasurementSpace::Norm,
origin: AxisOrigin::E0,
}
}
pub fn point(offset_ev: f64) -> Self {
Self::new(Metric::Point { x: offset_ev })
}
pub fn mean(range_ev: RangeInclusive<f64>) -> Self {
Self::new(Metric::Mean {
start: *range_ev.start(),
end: *range_ev.end(),
})
}
pub fn integral(range_ev: RangeInclusive<f64>) -> Self {
Self::new(Metric::Integral {
start: *range_ev.start(),
end: *range_ev.end(),
})
}
pub fn maximum(range_ev: RangeInclusive<f64>) -> Self {
Self::new(Metric::Maximum {
start: *range_ev.start(),
end: *range_ev.end(),
})
}
pub fn flat(mut self) -> Self {
self.space = MeasurementSpace::Flat;
self
}
pub fn raw_mu(mut self) -> Self {
self.space = MeasurementSpace::Mu;
self
}
pub fn chi(mut self, kweight: u8) -> Self {
self.space = MeasurementSpace::Chi { kweight };
self.origin = AxisOrigin::Absolute;
self
}
pub fn fourier(mut self) -> Self {
self.space = MeasurementSpace::Fourier;
self.origin = AxisOrigin::Absolute;
self
}
pub fn absolute(mut self) -> Self {
self.origin = AxisOrigin::Absolute;
self
}
pub fn arrays(&self, spectrum: &XASSpectrum) -> Result<MeasurementArrays, MetricError> {
let failure =
|e: crate::xafs::errors::AnalysisError| MetricError::Preparation(e.to_string());
let mut prepared;
let source = if matches!(self.origin, AxisOrigin::E0) && spectrum_e0(spectrum).is_none() {
prepared = spectrum.clone();
prepared
.find_e0()
.map_err(|e| MetricError::Preparation(e.to_string()))?;
&prepared
} else {
spectrum
};
if !matches!(
self.space,
MeasurementSpace::Mu | MeasurementSpace::Norm | MeasurementSpace::Flat
) && self.origin != AxisOrigin::Absolute
{
return Err(MetricError::Quantity(
"k and R coordinates must be absolute".into(),
));
}
match self.space {
MeasurementSpace::Mu => {
if source.preserves_prepared_values() {
return Err(MetricError::Quantity(
"prepared absorption is not raw μ(E)".into(),
));
}
let x = source
.energy
.as_ref()
.ok_or(MetricError::Shape)?
.as_slice()
.to_vec();
let y = source
.mu
.as_ref()
.ok_or(MetricError::Shape)?
.as_slice()
.to_vec();
Ok(MeasurementArrays {
axis: x,
signal: y,
e0_ev: spectrum_e0(source),
})
}
MeasurementSpace::Norm | MeasurementSpace::Flat | MeasurementSpace::Chi { .. } => {
let space = match self.space {
MeasurementSpace::Flat => AnalysisSpace::Flat,
MeasurementSpace::Chi { kweight } => AnalysisSpace::Chi {
kweight: kweight as f64,
},
_ => AnalysisSpace::Norm,
};
let input = AnalysisInput::new(source, space).map_err(failure)?;
Ok(MeasurementArrays {
axis: input.x.as_slice().to_vec(),
signal: input.y.as_slice().to_vec(),
e0_ev: input.e0,
})
}
MeasurementSpace::Fourier => {
let mut sp = source.clone();
if sp.r().is_none() || sp.chir_mag().is_none() {
sp.fft()
.map_err(|e| MetricError::Preparation(e.to_string()))?;
}
Ok(MeasurementArrays {
axis: sp.r().ok_or(MetricError::Shape)?.as_slice().to_vec(),
signal: sp.chir_mag().ok_or(MetricError::Shape)?.as_slice().to_vec(),
e0_ev: spectrum_e0(&sp),
})
}
}
}
pub fn resolved_metric(&self, e0: Option<f64>) -> Result<Metric, MetricError> {
let origin = match self.origin {
AxisOrigin::Absolute => 0.0,
AxisOrigin::E0 => {
e0.ok_or_else(|| MetricError::Preparation("E₀ is unavailable".into()))?
}
AxisOrigin::Reference { energy_ev } => energy_ev,
};
if !origin.is_finite() {
return Err(MetricError::Range);
}
Ok(self.metric.shifted(origin))
}
pub fn unit(&self, fourier_kweight: f64) -> String {
let (signal, axis) = match self.space {
MeasurementSpace::Mu => ("source μ units".into(), "eV"),
MeasurementSpace::Norm | MeasurementSpace::Flat => ("1".into(), "eV"),
MeasurementSpace::Chi { kweight } => (
if kweight == 0 {
"1".into()
} else {
format!("Å^-{kweight}")
},
"Å^-1",
),
MeasurementSpace::Fourier => (format!("Å^-{}", fourier_kweight + 1.0), "Å"),
};
match self.metric {
Metric::Integral { .. } => format!("{signal} · {axis}"),
Metric::Centroid { .. } => axis.into(),
_ => signal,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct MeasurementArrays {
pub axis: Vec<f64>,
pub signal: Vec<f64>,
pub e0_ev: Option<f64>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MeasurementResult {
pub measurement: Measurement,
pub value: f64,
pub position: Option<f64>,
pub range: [f64; 2],
pub standard_error: Option<f64>,
pub unit: String,
pub e0_ev: Option<f64>,
}
impl XASSpectrum {
pub fn measure(&self, measurement: &Measurement) -> Result<MeasurementResult, MetricError> {
self.measure_impl(measurement, None)
}
pub fn measure_with_errors(
&self,
measurement: &Measurement,
standard_errors: &[f64],
) -> Result<MeasurementResult, MetricError> {
self.measure_impl(measurement, Some(standard_errors))
}
fn measure_impl(
&self,
measurement: &Measurement,
errors: Option<&[f64]>,
) -> Result<MeasurementResult, MetricError> {
let MeasurementArrays {
axis: x,
signal: y,
e0_ev: e0,
} = measurement.arrays(self)?;
let metric = measurement.resolved_metric(e0)?;
let value = if let Some(errors) = errors {
super::measure_with_errors(&x, &y, errors, metric)?
} else {
measure(&x, &y, metric)?
};
let weight = self
.xftf
.as_ref()
.and_then(|fft| fft.kweight)
.unwrap_or(2.)
.max(0.)
.floor();
Ok(MeasurementResult {
measurement: measurement.clone(),
value: value.value,
position: value.position,
range: [value.start, value.end],
standard_error: value.standard_error,
unit: measurement.unit(weight),
e0_ev: e0,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_call_api_preserves_inputs_and_requires_explicit_flat() {
let x = [98., 99., 100., 101., 102., 103.];
let y = [0., 0.1, 0.5, 0.8, 1., 1.];
let spectrum = XASSpectrum::from_prepared(&x, &y, AnalysisSpace::Norm, 100.).unwrap();
let before = serde_json::to_value(&spectrum).unwrap();
let result = spectrum.measure(&Measurement::mean(0.0..=2.0)).unwrap();
assert_eq!(result.range[0], 100.);
assert!((result.value - 0.775).abs() < 1e-14);
assert_eq!(result.unit, "1");
assert!(spectrum.measure(&Measurement::point(0.).flat()).is_err());
assert!(spectrum
.measure(&Measurement::point(100.).raw_mu().absolute())
.is_err());
assert_eq!(serde_json::to_value(&spectrum).unwrap(), before);
}
}