use super::*;
use crate::xafs::{background::BackgroundMethod, normalization::NormalizationMethod};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WaveletPreparation {
pub software_version: String,
pub backend: String,
pub e0: Option<f64>,
pub edge_step: Option<f64>,
pub normalization: Option<NormalizationMethod>,
pub background: Option<BackgroundMethod>,
}
impl WaveletPreparation {
pub(super) fn from_spectrum(source: &crate::Spectrum) -> Self {
let mut settings = crate::Spectrum::new();
settings.normalization = source.normalization.clone();
settings.background = source.background.clone();
if let Some(NormalizationMethod::MBack(m)) = &mut settings.normalization {
if let Some(result) = &m.result {
m.options.pre_edge = Some(result.pre_edge);
m.options.post_edge = Some(result.post_edge);
m.e0 = Some(result.e0);
}
}
settings.invalidate_derived();
Self {
software_version: env!("CARGO_PKG_VERSION").into(),
backend: if cfg!(feature = "ndarray-compat") {
"ndarray-compat"
} else {
"nalgebra"
}
.into(),
e0: source.e0(),
edge_step: source
.normalization
.as_ref()
.and_then(|n| n.get_edge_step()),
normalization: settings.normalization,
background: settings.background,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct MapData {
pub method: String,
pub settings: Wavelet,
pub size: WaveletSize,
pub input_k: Vec<f64>,
pub input_chi: Vec<f64>,
pub k: Vec<f64>,
pub r: Vec<f64>,
pub prepared_chi: Vec<f64>,
pub window: Vec<f64>,
pub support: Vec<bool>,
pub real: Vec<f64>,
pub imaginary: Vec<f64>,
pub preparation: Option<WaveletPreparation>,
pub warnings: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "MapData")]
pub struct WaveletMap {
#[serde(flatten)]
pub(super) data: MapData,
}
impl TryFrom<MapData> for WaveletMap {
type Error = WaveletError;
fn try_from(data: MapData) -> Result<Self> {
let expected = calculate::layout(&data.settings, &data.input_k)?;
if data.method != "cauchy_v1"
|| data.k != expected.k
|| data.r != expected.r
|| data.size != expected.size
|| data.input_chi.len() != data.input_k.len()
|| data.real.len() != data.size.cells
|| data.imaginary.len() != data.size.cells
|| data.prepared_chi.len() != data.k.len()
|| data.window.len() != data.k.len()
|| data.support.len() != data.k.len()
{
return Err(invalid(
"stored map dimensions, grid or method do not match its definition",
));
}
if data
.input_chi
.iter()
.chain(&data.real)
.chain(&data.imaginary)
.chain(&data.prepared_chi)
.chain(&data.window)
.any(|v| !v.is_finite())
{
return Err(invalid("stored map contains nonfinite numerical data"));
}
if data
.real
.iter()
.zip(&data.imaginary)
.any(|(a, b)| !a.hypot(*b).is_finite())
{
return Err(invalid("stored complex magnitude overflowed"));
}
Ok(Self { data })
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WaveletRegionValue {
pub value: f64,
pub k_range: [f64; 2],
pub r_range: [f64; 2],
pub unit: String,
pub method: String,
}
impl WaveletMap {
pub fn method(&self) -> &str {
&self.data.method
}
pub fn settings(&self) -> &Wavelet {
&self.data.settings
}
pub fn size(&self) -> &WaveletSize {
&self.data.size
}
pub fn input_k(&self) -> &[f64] {
&self.data.input_k
}
pub fn input_chi(&self) -> &[f64] {
&self.data.input_chi
}
pub fn k(&self) -> &[f64] {
&self.data.k
}
pub fn r(&self) -> &[f64] {
&self.data.r
}
pub fn prepared_chi(&self) -> &[f64] {
&self.data.prepared_chi
}
pub fn window(&self) -> &[f64] {
&self.data.window
}
pub fn support(&self) -> &[bool] {
&self.data.support
}
pub fn real(&self) -> &[f64] {
&self.data.real
}
pub fn imaginary(&self) -> &[f64] {
&self.data.imaginary
}
pub fn preparation(&self) -> Option<&WaveletPreparation> {
self.data.preparation.as_ref()
}
pub fn warnings(&self) -> &[String] {
&self.data.warnings
}
pub fn magnitude(&self) -> Vec<f64> {
self.real()
.iter()
.zip(self.imaginary())
.map(|(a, b)| a.hypot(*b))
.collect()
}
pub fn phase(&self, relative_floor: f64) -> Result<Vec<Option<f64>>> {
if !relative_floor.is_finite() || !(0. ..=1.).contains(&relative_floor) {
return Err(invalid(
"phase magnitude floor must be a fraction from 0 to 1",
));
}
let max = self
.real()
.iter()
.zip(self.imaginary())
.map(|(a, b)| a.hypot(*b))
.fold(0., f64::max);
Ok(self
.real()
.iter()
.zip(self.imaginary())
.map(|(re, im)| {
let magnitude = re.hypot(*im);
(magnitude > 0. && magnitude >= max * relative_floor).then(|| im.atan2(*re))
})
.collect())
}
pub fn slice_at_r(&self, radius: f64) -> Result<Vec<f64>> {
let (a, b, t) = bracket(self.r(), radius)?;
Ok((0..self.k().len())
.map(|i| self.magnitude_cell(a, i) * (1. - t) + self.magnitude_cell(b, i) * t)
.collect())
}
pub fn slice_at_k(&self, k: f64) -> Result<Vec<f64>> {
let (a, b, t) = bracket(self.k(), k)?;
Ok((0..self.r().len())
.map(|i| self.magnitude_cell(i, a) * (1. - t) + self.magnitude_cell(i, b) * t)
.collect())
}
fn magnitude_cell(&self, row: usize, column: usize) -> f64 {
let i = row * self.k().len() + column;
self.real()[i].hypot(self.imaginary()[i])
}
fn sample(&self, k: f64, r: f64) -> Result<f64> {
let (ka, kb, kt) = bracket(self.k(), k)?;
let (ra, rb, rt) = bracket(self.r(), r)?;
let a = self.magnitude_cell(ra, ka) * (1. - kt) + self.magnitude_cell(ra, kb) * kt;
let b = self.magnitude_cell(rb, ka) * (1. - kt) + self.magnitude_cell(rb, kb) * kt;
Ok(a * (1. - rt) + b * rt)
}
pub fn integral(
&self,
k: RangeInclusive<f64>,
r: RangeInclusive<f64>,
) -> Result<WaveletRegionValue> {
let kr = [*k.start(), *k.end()];
let rr = [*r.start(), *r.end()];
if kr[0] < self.settings().k_range[0] || kr[1] > self.settings().k_range[1] {
return Err(invalid(
"wavelet integral must lie inside selected measured k support",
));
}
let ks = integration_grid(self.k(), kr)?;
let rs = integration_grid(self.r(), rr)?;
let mut value = 0.;
for rw in rs.windows(2) {
for kw in ks.windows(2) {
value += 0.25
* (kw[1] - kw[0])
* (rw[1] - rw[0])
* (self.sample(kw[0], rw[0])?
+ self.sample(kw[1], rw[0])?
+ self.sample(kw[0], rw[1])?
+ self.sample(kw[1], rw[1])?);
}
}
if !value.is_finite() {
return Err(invalid("wavelet integral overflowed"));
}
Ok(WaveletRegionValue {
value,
k_range: kr,
r_range: rr,
unit: if self.settings().kweight == 0 {
"dimensionless".into()
} else {
format!("Å^-{}", self.settings().kweight)
},
method: "bilinear_magnitude_v1".into(),
})
}
pub fn mean(
&self,
k: RangeInclusive<f64>,
r: RangeInclusive<f64>,
) -> Result<WaveletRegionValue> {
let area = (k.end() - k.start()) * (r.end() - r.start());
let mut result = self.integral(k, r)?;
result.value /= area;
if !result.value.is_finite() {
return Err(invalid("wavelet mean overflowed"));
}
result.method = "bilinear_magnitude_mean_v1".into();
Ok(result)
}
pub fn maximum(
&self,
k: RangeInclusive<f64>,
r: RangeInclusive<f64>,
) -> Result<WaveletRegionValue> {
let kr = [*k.start(), *k.end()];
let rr = [*r.start(), *r.end()];
if kr[0] < self.settings().k_range[0] || kr[1] > self.settings().k_range[1] {
return Err(invalid(
"wavelet region must lie inside selected measured k support",
));
}
let ks = integration_grid(self.k(), kr)?;
let rs = integration_grid(self.r(), rr)?;
let mut value: f64 = 0.;
for r in rs {
for &k in &ks {
value = value.max(self.sample(k, r)?);
}
}
Ok(WaveletRegionValue {
value,
k_range: kr,
r_range: rr,
unit: if self.settings().kweight == 0 {
"dimensionless".into()
} else {
format!("Å^-{}", self.settings().kweight)
},
method: "bilinear_magnitude_maximum_v1".into(),
})
}
}
fn bracket(axis: &[f64], x: f64) -> Result<(usize, usize, f64)> {
if !x.is_finite() || x < axis[0] || x > axis[axis.len() - 1] {
return Err(invalid(
"requested coordinate is outside the retained scientific map",
));
}
let b = axis.partition_point(|v| *v < x);
if b == 0 {
return Ok((0, 0, 0.));
}
Ok((b - 1, b, (x - axis[b - 1]) / (axis[b] - axis[b - 1])))
}
fn integration_grid(axis: &[f64], range: [f64; 2]) -> Result<Vec<f64>> {
if !range[0].is_finite() || !range[1].is_finite() || range[0] >= range[1] {
return Err(invalid("integral ranges must be finite and increasing"));
}
bracket(axis, range[0])?;
bracket(axis, range[1])?;
let mut grid = vec![range[0]];
grid.extend(
axis.iter()
.copied()
.filter(|v| *v > range[0] && *v < range[1]),
);
grid.push(range[1]);
Ok(grid)
}