use errorfunctions::{ComplexErrorFunctions, RealErrorFunctions};
use num_complex::Complex64;
use serde::{Deserialize, Serialize};
use std::f64::consts::{LN_2, PI, SQRT_2};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PeakShape {
Constant,
Linear,
Gaussian,
Lorentzian,
PseudoVoigt,
Voigt,
ErfStep,
ArctanStep,
}
impl PeakShape {
pub(crate) fn is_peak(self) -> bool {
matches!(
self,
Self::Gaussian | Self::Lorentzian | Self::PseudoVoigt | Self::Voigt
)
}
pub(crate) fn is_step(self) -> bool {
matches!(self, Self::ErfStep | Self::ArctanStep)
}
pub(crate) fn parameter_names(self) -> &'static [&'static str] {
match self {
Self::Constant => &["offset"],
Self::Linear => &["offset", "slope", "reference"],
Self::Gaussian | Self::Lorentzian => &["center", "area", "width"],
Self::PseudoVoigt => &["center", "area", "width", "fraction"],
Self::Voigt => &["center", "area", "width", "lorentz_width"],
Self::ErfStep | Self::ArctanStep => &["center", "height", "scale"],
}
}
}
pub(crate) fn gaussian(x: f64, width: f64) -> f64 {
let sigma = width / (2. * (2. * LN_2).sqrt());
(-0.5 * (x / sigma).powi(2)).exp() / (sigma * (2. * PI).sqrt())
}
pub(crate) fn lorentzian(x: f64, width: f64) -> f64 {
let gamma = width / 2.;
let u = x / gamma;
if u.abs() > 1e150 {
gamma / x / x / PI
} else {
1. / (PI * gamma * (1. + u * u))
}
}
pub(crate) fn voigt(x: f64, gaussian_width: f64, lorentz_width: f64) -> f64 {
if gaussian_width == 0. {
return lorentzian(x, lorentz_width);
}
if lorentz_width == 0. {
return gaussian(x, gaussian_width);
}
let sigma = gaussian_width / (2. * (2. * LN_2).sqrt());
Complex64::new(x / sigma / SQRT_2, lorentz_width / 2. / sigma / SQRT_2)
.w()
.re
/ (sigma * (2. * PI).sqrt())
}
pub(crate) fn evaluate(shape: PeakShape, x: f64, p: &[f64]) -> f64 {
let offset = x - p[0];
match shape {
PeakShape::Constant => p[0],
PeakShape::Linear => p[0] + p[1] * (x - p[2]),
PeakShape::Gaussian => p[1] * gaussian(offset, p[2]),
PeakShape::Lorentzian => p[1] * lorentzian(offset, p[2]),
PeakShape::PseudoVoigt => {
p[1] * ((1. - p[3]) * gaussian(offset, p[2]) + p[3] * lorentzian(offset, p[2]))
}
PeakShape::Voigt => p[1] * voigt(offset, p[2], p[3]),
PeakShape::ErfStep => p[1] * (1. + RealErrorFunctions::erf(offset / p[2])) / 2.,
PeakShape::ArctanStep => p[1] * (0.5 + (offset / p[2]).atan() / PI),
}
}
pub(crate) fn fwhm(shape: PeakShape, p: &[f64]) -> Option<f64> {
if !shape.is_peak() {
return None;
}
if shape != PeakShape::Voigt {
return Some(p[2]);
}
if p[2] == 0. {
return Some(p[3]);
}
if p[3] == 0. {
return Some(p[2]);
}
let scale = p[2].max(p[3]);
let (gaussian, lorentzian) = (p[2] / scale, p[3] / scale);
let target = voigt(0., gaussian, lorentzian) / 2.;
let (mut low, mut high) = (0., gaussian + lorentzian);
for _ in 0..64 {
if voigt(high, gaussian, lorentzian) <= target {
break;
}
high *= 2.;
}
for _ in 0..80 {
let mid = low + (high - low) / 2.;
if voigt(mid, gaussian, lorentzian) > target {
low = mid;
} else {
high = mid;
}
}
let width = (low + high) * scale;
width.is_finite().then_some(width)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn analytic_heights_widths_and_step_limits() {
let width = 4.;
assert!((gaussian(0., width) - 2. * (LN_2 / PI).sqrt() / width).abs() < 1e-15);
assert!((lorentzian(0., width) - 2. / (PI * width)).abs() < 1e-15);
for shape in [
PeakShape::Gaussian,
PeakShape::Lorentzian,
PeakShape::PseudoVoigt,
PeakShape::Voigt,
] {
let p = [5., 3., width, 0.4];
let half_width = fwhm(shape, &p).unwrap() / 2.;
let center = evaluate(shape, 5., &p);
assert!((evaluate(shape, 5. + half_width, &p) / center - 0.5).abs() < 1e-12);
}
for shape in [PeakShape::ErfStep, PeakShape::ArctanStep] {
let p = [3., 2., 0.5];
assert_eq!(evaluate(shape, 3., &p), 1.);
assert!((evaluate(shape, 1e12, &p) - 2.).abs() < 1e-11);
assert!(evaluate(shape, -1e12, &p).abs() < 1e-11);
assert_eq!(fwhm(shape, &p), None);
}
}
#[test]
fn voigt_zero_width_limits_and_center_reference() {
let sigma = 1.;
let gamma = 0.5;
let width = 2. * (2. * LN_2).sqrt() * sigma;
let y = gamma / sigma / SQRT_2;
let expected = (y * y).exp() * RealErrorFunctions::erfc(y) / (sigma * (2. * PI).sqrt());
assert!((voigt(0., width, 2. * gamma) - expected).abs() < 2e-15);
for x in [-10., -1., 0., 1., 10.] {
assert_eq!(voigt(x, width, 0.), gaussian(x, width));
assert_eq!(voigt(x, 0., 2. * gamma), lorentzian(x, 2. * gamma));
}
let reference = fwhm(PeakShape::Voigt, &[0., 1., 1., 0.5]).unwrap();
for scale in [1e-200, 1e200] {
let width = fwhm(PeakShape::Voigt, &[0., 1., scale, scale * 0.5]).unwrap();
assert!((width / scale - reference).abs() < 1e-14);
}
assert_eq!(fwhm(PeakShape::Voigt, &[0., 1., f64::MAX, f64::MAX]), None);
}
}