Skip to main content

spectrafit_models/
voigt.rs

1use crate::Model;
2
3/// Pseudo-Voigt kernel: `A * (fraction * L̃ + (1 - fraction) * G̃)`
4///
5/// where `G̃` and `L̃` are unit-amplitude Gaussian and Lorentzian shapes:
6/// - `G̃ = exp(-(x₀-c)² / (2σ²))`
7/// - `L̃ = 1 / (1 + ((x₀-c)/σ)²)`
8///
9/// Parameters (in order): `[amplitude, center, sigma, fraction]`
10///
11/// - `fraction = 0.0` → pure Gaussian
12/// - `fraction = 1.0` → pure Lorentzian
13pub struct Voigt;
14
15impl Model for Voigt {
16    fn eval(&self, x: &[f64], params: &[f64]) -> f64 {
17        let (a, c, sigma, frac) = (params[0], params[1], params[2], params[3]);
18        let dx = x[0] - c;
19        let z = -dx * dx / (2.0 * sigma * sigma);
20        let g_tilde = z.exp();
21        let d = dx / sigma;
22        let big_d = 1.0 + d * d;
23        let l_tilde = 1.0 / big_d;
24
25        a * (frac * l_tilde + (1.0 - frac) * g_tilde)
26    }
27
28    fn jacobian(&self, x: &[f64], params: &[f64]) -> Vec<f64> {
29        let (a, c, sigma, frac) = (params[0], params[1], params[2], params[3]);
30        let dx = x[0] - c;
31        let z = -dx * dx / (2.0 * sigma * sigma);
32        let g_tilde = z.exp();
33
34        let d = dx / sigma;
35        let big_d = 1.0 + d * d;
36        let l_tilde = 1.0 / big_d;
37
38        // Unit-amplitude shape derivatives (w.r.t. center, σ)
39        // ∂G̃/∂c  = G̃ * (x₀-c) / σ²    (note: -(∂z/∂c) = +dx/σ²)
40        let dg_dc = g_tilde * dx / (sigma * sigma);
41        // ∂G̃/∂σ  = G̃ * (x₀-c)² / σ³
42        let dg_ds = g_tilde * dx * dx / (sigma * sigma * sigma);
43
44        // ∂L̃/∂c  = 2*(x₀-c) / (σ²*D²)
45        let dl_dc = 2.0 * dx / (sigma * sigma * big_d * big_d);
46        // ∂L̃/∂σ  = 2*(x₀-c)² / (σ³*D²)
47        let dl_ds = 2.0 * dx * dx / (sigma * sigma * sigma * big_d * big_d);
48
49        // ∂/∂amplitude = frac*L̃ + (1-frac)*G̃
50        let da = frac * l_tilde + (1.0 - frac) * g_tilde;
51        // ∂/∂center    = A*(frac*∂L̃/∂c + (1-frac)*∂G̃/∂c)
52        let dc = a * (frac * dl_dc + (1.0 - frac) * dg_dc);
53        // ∂/∂sigma     = A*(frac*∂L̃/∂σ + (1-frac)*∂G̃/∂σ)
54        let ds = a * (frac * dl_ds + (1.0 - frac) * dg_ds);
55        // ∂/∂frac      = A*(L̃ - G̃)
56        let dfrac = a * (l_tilde - g_tilde);
57
58        vec![da, dc, ds, dfrac]
59    }
60
61    #[inline]
62    fn jacobian_into(&self, x: &[f64], params: &[f64], out: &mut [f64]) {
63        let (a, c, sigma, frac) = (params[0], params[1], params[2], params[3]);
64        let dx = x[0] - c;
65        let s2 = sigma * sigma;
66        let z = -dx * dx / (2.0 * s2);
67        let g_tilde = z.exp();
68        let d = dx / sigma;
69        let big_d = 1.0 + d * d;
70        let big_d2 = big_d * big_d;
71        let l_tilde = 1.0 / big_d;
72        let dg_dc = g_tilde * dx / s2;
73        let dg_ds = g_tilde * dx * dx / (s2 * sigma);
74        let dl_dc = 2.0 * dx / (s2 * big_d2);
75        let dl_ds = 2.0 * dx * dx / (s2 * sigma * big_d2);
76        out[0] = frac * l_tilde + (1.0 - frac) * g_tilde;
77        out[1] = a * (frac * dl_dc + (1.0 - frac) * dg_dc);
78        out[2] = a * (frac * dl_ds + (1.0 - frac) * dg_ds);
79        out[3] = a * (l_tilde - g_tilde);
80    }
81
82    fn param_names(&self) -> Vec<std::borrow::Cow<'static, str>> {
83        vec![
84            "amplitude".into(),
85            "center".into(),
86            "sigma".into(),
87            "fraction".into(),
88        ]
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::gaussian::Gaussian;
96    use crate::lorentzian::Lorentzian;
97    use approx::assert_relative_eq;
98
99    #[test]
100    fn frac_zero_equals_gaussian() {
101        let voigt = Voigt;
102        let gauss = Gaussian;
103        let x = &[0.7f64];
104        let params_v = [2.0, 0.0, 1.0, 0.0]; // frac=0 → pure Gaussian
105        let params_g = [2.0, 0.0, 1.0];
106        assert_relative_eq!(
107            voigt.eval(x, &params_v),
108            gauss.eval(x, &params_g),
109            epsilon = 1e-12
110        );
111    }
112
113    #[test]
114    fn frac_one_equals_lorentzian() {
115        let voigt = Voigt;
116        let lorentz = Lorentzian;
117        let x = &[0.7f64];
118        let params_v = [2.0, 0.0, 1.0, 1.0]; // frac=1 → pure Lorentzian
119        let params_l = [2.0, 0.0, 1.0];
120        assert_relative_eq!(
121            voigt.eval(x, &params_v),
122            lorentz.eval(x, &params_l),
123            epsilon = 1e-12
124        );
125    }
126
127    #[test]
128    fn jacobian_shape() {
129        let v = Voigt;
130        let j = v.jacobian(&[0.5], &[1.0, 0.0, 1.0, 0.5]);
131        assert_eq!(j.len(), 4);
132    }
133
134    // Central-difference (O(h^2)) sweep over three parameter regimes and
135    // several x-points relative to the centre, replacing the old
136    // single-point forward differences (h=1e-6, epsilon=1e-5). Each test
137    // keeps its name (traceability) but now checks only its own parameter
138    // index across the full regime/x-point grid.
139    const VOIGT_REGIMES: [[f64; 4]; 3] = [
140        [2.0, 0.0, 1.0, 0.4],   // nominal
141        [1e-3, 0.0, 0.05, 0.2], // small amplitude/width
142        [5.0, -2.0, 3.0, 0.8],  // offset centre, wide
143    ];
144
145    fn check_voigt_param_central_diff(idx: usize) {
146        let v = Voigt;
147        for p in VOIGT_REGIMES {
148            let (c, sigma) = (p[1], p[2]);
149            for &mult in &[-3.0_f64, -1.0, -0.1, 0.0, 0.1, 1.0, 3.0] {
150                let x = [c + mult * sigma];
151                let j = v.jacobian(&x, &p);
152                let h = 1e-6 * p[idx].abs().max(1.0);
153                let (mut a, mut b) = (p, p);
154                a[idx] += h;
155                b[idx] -= h;
156                let fd = (v.eval(&x, &a) - v.eval(&x, &b)) / (2.0 * h);
157                assert_relative_eq!(j[idx], fd, epsilon = 1e-7, max_relative = 1e-6);
158            }
159        }
160    }
161
162    #[test]
163    fn jacobian_numerical_check_amplitude() {
164        check_voigt_param_central_diff(0);
165    }
166
167    #[test]
168    fn jacobian_numerical_check_center() {
169        check_voigt_param_central_diff(1);
170    }
171
172    #[test]
173    fn jacobian_numerical_check_sigma() {
174        check_voigt_param_central_diff(2);
175    }
176
177    #[test]
178    fn jacobian_numerical_check_frac() {
179        check_voigt_param_central_diff(3);
180    }
181}