use core::marker::PhantomData;
use alloc::{boxed::Box, vec::Vec};
use qtty::unit::Ratio;
use qtty::{DimMul, Dimension, Prod, Quantity, Unit};
use crate::data::Provenance;
use crate::grid::OutOfRange;
use crate::spectrum::algo::{self, CubicSplineCoeffs};
use crate::spectrum::{Interpolation, SpectrumError};
#[derive(Debug, Clone)]
pub struct SampledSpectrum<X: Unit, Y: Unit> {
xs: Box<[f64]>,
ys: Box<[f64]>,
interp: Interpolation,
oor: OutOfRange,
spline: Option<CubicSplineCoeffs>,
provenance: Option<Provenance>,
_x: PhantomData<X>,
_y: PhantomData<Y>,
}
impl<X: Unit, Y: Unit> SampledSpectrum<X, Y> {
pub fn from_raw(
xs: Vec<f64>,
ys: Vec<f64>,
interp: Interpolation,
oor: OutOfRange,
provenance: Option<Provenance>,
) -> Result<Self, SpectrumError> {
algo::validate(&xs, &ys)?;
let spline = if matches!(interp, Interpolation::CubicSpline) {
Some(CubicSplineCoeffs::natural(&xs, &ys)?)
} else {
None
};
Ok(Self {
xs: xs.into_boxed_slice(),
ys: ys.into_boxed_slice(),
interp,
oor,
spline,
provenance,
_x: PhantomData,
_y: PhantomData,
})
}
pub fn from_sorted(
xs: &[f64],
ys: &[f64],
interp: Interpolation,
oor: OutOfRange,
) -> Result<Self, SpectrumError> {
Self::from_raw(xs.to_vec(), ys.to_vec(), interp, oor, None)
}
#[inline]
pub fn xs_raw(&self) -> &[f64] {
&self.xs
}
#[inline]
pub fn ys_raw(&self) -> &[f64] {
&self.ys
}
#[inline]
pub fn len(&self) -> usize {
self.xs.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.xs.is_empty()
}
#[inline]
pub fn interpolation(&self) -> Interpolation {
self.interp
}
#[inline]
pub fn out_of_range(&self) -> OutOfRange {
self.oor
}
pub fn provenance(&self) -> Option<&Provenance> {
self.provenance.as_ref()
}
pub fn set_provenance(&mut self, provenance: Option<Provenance>) {
self.provenance = provenance;
}
#[must_use]
pub fn with_provenance(mut self, provenance: Provenance) -> Self {
self.provenance = Some(provenance);
self
}
#[inline]
pub fn interp_at(&self, x: Quantity<X>) -> Quantity<Y> {
let raw = self
.eval(x.value(), OutOfRange::ClampToEndpoints)
.expect("ClampToEndpoints never errors");
Quantity::<Y>::new(raw)
}
#[inline]
pub fn try_interp_at(&self, x: Quantity<X>) -> Result<Quantity<Y>, SpectrumError> {
let raw = self.eval(x.value(), self.oor)?;
Ok(Quantity::<Y>::new(raw))
}
pub fn integrate(&self) -> Quantity<Prod<Y, X>>
where
Y::Dim: DimMul<X::Dim>,
<Y::Dim as DimMul<X::Dim>>::Output: Dimension,
Prod<Y, X>: Unit,
{
Quantity::<Prod<Y, X>>::new(algo::trapz(&self.xs, &self.ys))
}
pub fn integrate_range(&self, lo: Quantity<X>, hi: Quantity<X>) -> Quantity<Prod<Y, X>>
where
Y::Dim: DimMul<X::Dim>,
<Y::Dim as DimMul<X::Dim>>::Output: Dimension,
Prod<Y, X>: Unit,
{
Quantity::<Prod<Y, X>>::new(algo::trapz_range(
&self.xs,
&self.ys,
lo.value(),
hi.value(),
))
}
pub fn integrate_weighted<Yw: Unit>(
&self,
weight: &SampledSpectrum<X, Yw>,
) -> Quantity<Prod<Prod<Y, Yw>, X>>
where
Y::Dim: DimMul<Yw::Dim>,
<Y::Dim as DimMul<Yw::Dim>>::Output: DimMul<X::Dim>,
<<Y::Dim as DimMul<Yw::Dim>>::Output as DimMul<X::Dim>>::Output: Dimension,
Prod<Y, Yw>: Unit,
Prod<Prod<Y, Yw>, X>: Unit,
{
Quantity::<Prod<Prod<Y, Yw>, X>>::new(algo::trapz_weighted(
&self.xs,
&self.ys,
weight.xs_raw(),
weight.ys_raw(),
))
}
fn eval(&self, x: f64, oor: OutOfRange) -> Result<f64, SpectrumError> {
match self.interp {
Interpolation::CubicSpline => {
let coeffs = self
.spline
.as_ref()
.expect("spline precomputed at construction");
algo::interp_cubic_spline(&self.xs, &self.ys, coeffs, x, oor)
}
_ => algo::interp(&self.xs, &self.ys, x, self.interp, oor),
}
}
#[inline]
pub fn domain(&self) -> (Quantity<X>, Quantity<X>) {
(
Quantity::<X>::new(self.xs[0]),
Quantity::<X>::new(self.xs[self.xs.len() - 1]),
)
}
#[inline]
pub fn contains(&self, x: Quantity<X>) -> bool {
let v = x.value();
v >= self.xs[0] && v <= self.xs[self.xs.len() - 1]
}
#[must_use]
pub fn overlap_domain<Yo: Unit>(
&self,
other: &SampledSpectrum<X, Yo>,
) -> Option<(Quantity<X>, Quantity<X>)> {
let (alo, ahi) = self.domain();
let (blo, bhi) = other.domain();
let lo = if alo.value() >= blo.value() { alo } else { blo };
let hi = if ahi.value() <= bhi.value() { ahi } else { bhi };
if lo.value() <= hi.value() {
Some((lo, hi))
} else {
None
}
}
pub fn resample_onto(&self, xs: &[Quantity<X>]) -> Result<Self, SpectrumError> {
let raw: Vec<f64> = xs.iter().map(|q| q.value()).collect();
self.resample_onto_raw(&raw)
}
pub fn resample_onto_raw(&self, xs: &[f64]) -> Result<Self, SpectrumError> {
let mut ys = Vec::with_capacity(xs.len());
for &x in xs {
ys.push(self.eval(x, self.oor)?);
}
Self::from_raw(
xs.to_vec(),
ys,
Interpolation::Linear,
OutOfRange::ClampToEndpoints,
None,
)
}
pub fn normalize_area(&self) -> Result<SampledSpectrum<X, Ratio>, SpectrumError> {
let area = algo::trapz(&self.xs, &self.ys);
if !area.is_finite() || area == 0.0 {
return Err(SpectrumError::InvalidValue {
what: alloc::string::String::from("integrated area must be finite and non-zero"),
});
}
let inv = area.recip();
let ys: Vec<f64> = self.ys.iter().map(|&y| y * inv).collect();
let xs: Vec<f64> = self.xs.to_vec();
SampledSpectrum::<X, Ratio>::from_raw(
xs,
ys,
self.interp,
self.oor,
self.provenance.clone(),
)
}
pub fn normalize_area_raw(&self) -> Result<Self, SpectrumError> {
let area = algo::trapz(&self.xs, &self.ys);
if !area.is_finite() || area == 0.0 {
return Err(SpectrumError::InvalidValue {
what: alloc::string::String::from("integrated area must be finite and non-zero"),
});
}
let inv = area.recip();
let ys: Vec<f64> = self.ys.iter().map(|&y| y * inv).collect();
let xs: Vec<f64> = self.xs.to_vec();
Self::from_raw(xs, ys, self.interp, self.oor, self.provenance.clone())
}
pub fn normalize_peak(&self) -> Result<SampledSpectrum<X, Ratio>, SpectrumError> {
let peak = self.ys.iter().copied().fold(f64::NEG_INFINITY, f64::max);
if !peak.is_finite() || peak == 0.0 {
return Err(SpectrumError::InvalidValue {
what: alloc::string::String::from("peak value must be finite and non-zero"),
});
}
let inv = peak.recip();
let ys: Vec<f64> = self.ys.iter().map(|&y| y * inv).collect();
let xs: Vec<f64> = self.xs.to_vec();
SampledSpectrum::<X, Ratio>::from_raw(
xs,
ys,
self.interp,
self.oor,
self.provenance.clone(),
)
}
pub fn normalize_peak_raw(&self) -> Result<Self, SpectrumError> {
let peak = self.ys.iter().copied().fold(f64::NEG_INFINITY, f64::max);
if !peak.is_finite() || peak == 0.0 {
return Err(SpectrumError::InvalidValue {
what: alloc::string::String::from("peak value must be finite and non-zero"),
});
}
let inv = peak.recip();
let ys: Vec<f64> = self.ys.iter().map(|&y| y * inv).collect();
let xs: Vec<f64> = self.xs.to_vec();
Self::from_raw(xs, ys, self.interp, self.oor, self.provenance.clone())
}
pub fn map_values_to<Y2: Unit, F>(&self, f: F) -> Result<SampledSpectrum<X, Y2>, SpectrumError>
where
F: Fn(Quantity<Y>) -> Quantity<Y2>,
{
let ys: Vec<f64> = self
.ys
.iter()
.map(|&y| f(Quantity::<Y>::new(y)).value())
.collect();
let xs: Vec<f64> = self.xs.to_vec();
SampledSpectrum::<X, Y2>::from_raw(xs, ys, self.interp, self.oor, self.provenance.clone())
}
pub fn map_values_raw<F>(&self, f: F) -> Result<Self, SpectrumError>
where
F: Fn(f64) -> f64,
{
let ys: Vec<f64> = self.ys.iter().map(|&y| f(y)).collect();
let xs: Vec<f64> = self.xs.to_vec();
Self::from_raw(xs, ys, self.interp, self.oor, self.provenance.clone())
}
pub fn zip_with_to<Yo: Unit, Yout: Unit, F>(
&self,
other: &SampledSpectrum<X, Yo>,
f: F,
) -> Result<SampledSpectrum<X, Yout>, SpectrumError>
where
F: Fn(Quantity<Y>, Quantity<Yo>) -> Quantity<Yout>,
{
let mut ys = Vec::with_capacity(self.xs.len());
for (&x, &ya) in self.xs.iter().zip(self.ys.iter()) {
let yb = other.eval(x, other.oor)?;
ys.push(f(Quantity::<Y>::new(ya), Quantity::<Yo>::new(yb)).value());
}
let xs: Vec<f64> = self.xs.to_vec();
SampledSpectrum::<X, Yout>::from_raw(xs, ys, self.interp, self.oor, self.provenance.clone())
}
pub fn zip_with_raw<Yo: Unit, F>(
&self,
other: &SampledSpectrum<X, Yo>,
f: F,
) -> Result<Self, SpectrumError>
where
F: Fn(f64, f64) -> f64,
{
let mut ys = Vec::with_capacity(self.xs.len());
for (&x, &ya) in self.xs.iter().zip(self.ys.iter()) {
let yb = other.eval(x, other.oor)?;
ys.push(f(ya, yb));
}
let xs: Vec<f64> = self.xs.to_vec();
Self::from_raw(xs, ys, self.interp, self.oor, self.provenance.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use qtty::unit::{Nanometer, Ratio};
fn make_linear() -> SampledSpectrum<Nanometer, Ratio> {
let xs: Vec<f64> = (0..5).map(|i| i as f64).collect();
let ys: Vec<f64> = xs.iter().map(|&x| 2.0 * x + 1.0).collect();
SampledSpectrum::from_raw(
xs,
ys,
Interpolation::Linear,
OutOfRange::ClampToEndpoints,
None,
)
.unwrap()
}
#[test]
fn interp_at_midpoint() {
let s = make_linear();
let val: f64 = s.interp_at(Quantity::<Nanometer>::new(2.5)).value();
assert_abs_diff_eq!(val, 6.0, epsilon = 1e-12);
}
#[test]
fn interp_at_clamps_oob_when_oor_is_error() {
let xs = vec![0.0_f64, 1.0, 2.0];
let ys = vec![1.0_f64, 2.0, 3.0];
let s = SampledSpectrum::<Nanometer, Ratio>::from_raw(
xs,
ys,
Interpolation::Linear,
OutOfRange::Error,
None,
)
.unwrap();
let val = s.interp_at(Quantity::<Nanometer>::new(-5.0)).value();
assert_abs_diff_eq!(val, 1.0, epsilon = 1e-12);
}
#[test]
fn try_interp_at_errors_when_oor_is_error() {
let xs = vec![0.0_f64, 1.0, 2.0];
let ys = vec![1.0_f64, 2.0, 3.0];
let s = SampledSpectrum::<Nanometer, Ratio>::from_raw(
xs,
ys,
Interpolation::Linear,
OutOfRange::Error,
None,
)
.unwrap();
assert!(s.try_interp_at(Quantity::<Nanometer>::new(-5.0)).is_err());
}
#[test]
fn xs_raw_ys_raw_zero_copy() {
let s = make_linear();
assert_eq!(s.xs_raw().len(), 5);
assert_eq!(s.ys_raw().len(), 5);
}
#[test]
fn from_sorted_works() {
let xs = [0.0_f64, 1.0, 2.0];
let ys = [1.0_f64, 2.0, 3.0];
let s = SampledSpectrum::<Nanometer, Ratio>::from_sorted(
&xs,
&ys,
Interpolation::Linear,
OutOfRange::ClampToEndpoints,
)
.unwrap();
assert_eq!(s.xs_raw(), &[0.0, 1.0, 2.0]);
}
}