use super::{EnergyConversion, MeasurementScan, ReadError, SignalConversion, SpectrumMapping};
use crate::xafs::xasspectrum::XASSpectrum;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ColumnSelector {
Index(usize),
Name(String),
}
impl From<usize> for ColumnSelector {
fn from(value: usize) -> Self {
Self::Index(value)
}
}
impl From<&str> for ColumnSelector {
fn from(value: &str) -> Self {
Self::Name(value.into())
}
}
impl From<String> for ColumnSelector {
fn from(value: String) -> Self {
Self::Name(value)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum SignalSelection {
Direct {
column: ColumnSelector,
},
Transmission {
incident: ColumnSelector,
transmitted: ColumnSelector,
},
Ratio {
detectors: Vec<ColumnSelector>,
incident: ColumnSelector,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SpectrumSelection {
pub energy_column: ColumnSelector,
#[serde(default)]
pub energy: Option<EnergyConversion>,
pub signal: SignalSelection,
}
impl SpectrumSelection {
pub fn direct(energy: impl Into<ColumnSelector>, mu: impl Into<ColumnSelector>) -> Self {
Self {
energy_column: energy.into(),
energy: None,
signal: SignalSelection::Direct { column: mu.into() },
}
}
pub fn transmission(
energy: impl Into<ColumnSelector>,
i0: impl Into<ColumnSelector>,
it: impl Into<ColumnSelector>,
) -> Self {
Self {
energy_column: energy.into(),
energy: None,
signal: SignalSelection::Transmission {
incident: i0.into(),
transmitted: it.into(),
},
}
}
pub fn fluorescence(
energy: impl Into<ColumnSelector>,
i0: impl Into<ColumnSelector>,
detectors: impl IntoIterator<Item = impl Into<ColumnSelector>>,
) -> Self {
Self {
energy_column: energy.into(),
energy: None,
signal: SignalSelection::Ratio {
incident: i0.into(),
detectors: detectors.into_iter().map(Into::into).collect(),
},
}
}
pub fn with_energy(mut self, energy: EnergyConversion) -> Self {
self.energy = Some(energy);
self
}
pub fn resolve(&self, scan: &MeasurementScan) -> Result<SpectrumMapping, ReadError> {
let energy_column = scan.column_index(&self.energy_column)?;
let energy = match &self.energy {
Some(energy) => energy.clone(),
None => scan.selected_energy(energy_column)?,
};
let signal = match &self.signal {
SignalSelection::Direct { column } => SignalConversion::Direct {
column: scan.column_index(column)?,
},
SignalSelection::Transmission {
incident,
transmitted,
} => SignalConversion::Transmission {
incident: scan.column_index(incident)?,
transmitted: scan.column_index(transmitted)?,
},
SignalSelection::Ratio {
detectors,
incident,
} => SignalConversion::Ratio {
detectors: detectors
.iter()
.map(|c| scan.column_index(c))
.collect::<Result<_, _>>()?,
incident: scan.column_index(incident)?,
},
};
Ok(SpectrumMapping {
energy_column,
energy,
signal,
})
}
}
impl MeasurementScan {
pub fn column_index(&self, selector: &ColumnSelector) -> Result<usize, ReadError> {
match selector {
ColumnSelector::Index(index) if *index < self.columns.len() => Ok(*index),
ColumnSelector::Index(index) => Err(ReadError::new(format!(
"Scan '{}': column {index} is out of range",
self.id
))),
ColumnSelector::Name(name) => {
let mut matches = self
.columns
.iter()
.enumerate()
.filter(|(_, c)| c.name == *name);
let Some((index, _)) = matches.next() else {
return Err(ReadError::new(format!(
"Scan '{}': no column named {name:?}; inspect scan.columns",
self.id
)));
};
if matches.next().is_some() {
return Err(ReadError::new(format!(
"Scan '{}': column name {name:?} is ambiguous; use a zero-based index",
self.id
)));
}
Ok(index)
}
}
}
fn selected_energy(&self, column: usize) -> Result<EnergyConversion, ReadError> {
let mut detected = self
.signals
.iter()
.filter(|s| s.mapping.energy_column == column)
.map(|s| &s.mapping.energy);
if let Some(energy) = detected.next() {
if detected.any(|other| other != energy) {
return Err(ReadError::new(
"Conflicting axis calibrations; specify the energy conversion explicitly",
));
}
return Ok(energy.clone());
}
match self.columns[column].units.as_deref().map(str::to_ascii_lowercase).as_deref() {
Some("ev") => Ok(EnergyConversion::Ev),
Some("kev") => Ok(EnergyConversion::Kev),
_ => Err(ReadError::new(format!("Scan '{}': column {column} has no supported energy calibration; specify energy_unit or an explicit energy conversion", self.id))),
}
}
pub fn arrays_with(
&self,
selection: &SpectrumSelection,
) -> Result<(Vec<f64>, Vec<f64>), ReadError> {
self.arrays(Some(&selection.resolve(self)?))
}
pub fn to_spectrum_with(
&self,
selection: &SpectrumSelection,
) -> Result<XASSpectrum, ReadError> {
self.to_spectrum(Some(&selection.resolve(self)?))
}
}