Skip to main content

spectrafit_models/
step.rs

1use crate::Model;
2
3/// Arctan step: `A · (½ + (1/π) · arctan((x − x₀) / σ))`
4///
5/// Parameters (in order): `[amplitude, center, sigma]`
6///
7/// ∂/∂amplitude = (½ + (1/π)·arctan(ε))
8/// ∂/∂center    = −A / (π · σ · (1 + ε²))       [ε = (x−x₀)/σ]
9/// ∂/∂sigma     = −A · ε / (π · σ · (1 + ε²))
10pub struct ArctanStep;
11
12impl Model for ArctanStep {
13    fn eval(&self, x: &[f64], params: &[f64]) -> f64 {
14        let (a, x0, sigma) = (params[0], params[1], params[2]);
15        let eps = (x[0] - x0) / sigma;
16        a * (0.5 + eps.atan() / std::f64::consts::PI)
17    }
18
19    fn jacobian(&self, x: &[f64], params: &[f64]) -> Vec<f64> {
20        let (a, x0, sigma) = (params[0], params[1], params[2]);
21        let pi = std::f64::consts::PI;
22        let eps = (x[0] - x0) / sigma;
23        let eps2_1 = 1.0 + eps * eps;
24        let atan_term = 0.5 + eps.atan() / pi;
25
26        let da = atan_term;
27        let dx0 = -a / (pi * sigma * eps2_1);
28        let ds = -a * eps / (pi * sigma * eps2_1);
29
30        vec![da, dx0, ds]
31    }
32
33    #[inline]
34    fn jacobian_into(&self, x: &[f64], params: &[f64], out: &mut [f64]) {
35        let (a, x0, sigma) = (params[0], params[1], params[2]);
36        let pi = std::f64::consts::PI;
37        let eps = (x[0] - x0) / sigma;
38        let eps2_1 = 1.0 + eps * eps;
39        out[0] = 0.5 + eps.atan() / pi;
40        out[1] = -a / (pi * sigma * eps2_1);
41        out[2] = -a * eps / (pi * sigma * eps2_1);
42    }
43
44    fn param_names(&self) -> Vec<std::borrow::Cow<'static, str>> {
45        vec!["amplitude".into(), "center".into(), "sigma".into()]
46    }
47}
48
49/// Tanh step: `(A / 2) · (1 + tanh((x − x₀) / σ))`
50///
51/// Parameters (in order): `[amplitude, center, sigma]`
52///
53/// ∂/∂amplitude = ½ · (1 + tanh(ε))
54/// ∂/∂center    = −A / (2σ) · sech²(ε)
55/// ∂/∂sigma     = −A · ε / (2σ) · sech²(ε)
56pub struct TanhStep;
57
58impl Model for TanhStep {
59    fn eval(&self, x: &[f64], params: &[f64]) -> f64 {
60        let (a, x0, sigma) = (params[0], params[1], params[2]);
61        let eps = (x[0] - x0) / sigma;
62        a * 0.5 * (1.0 + eps.tanh())
63    }
64
65    fn jacobian(&self, x: &[f64], params: &[f64]) -> Vec<f64> {
66        let (a, x0, sigma) = (params[0], params[1], params[2]);
67        let eps = (x[0] - x0) / sigma;
68        let t = eps.tanh();
69        let sech2 = 1.0 - t * t; // sech²(ε)
70
71        let da = 0.5 * (1.0 + t);
72        let dx0 = -a * sech2 / (2.0 * sigma);
73        let ds = -a * eps * sech2 / (2.0 * sigma);
74
75        vec![da, dx0, ds]
76    }
77
78    #[inline]
79    fn jacobian_into(&self, x: &[f64], params: &[f64], out: &mut [f64]) {
80        let (a, x0, sigma) = (params[0], params[1], params[2]);
81        let eps = (x[0] - x0) / sigma;
82        let t = eps.tanh();
83        let sech2 = 1.0 - t * t;
84        out[0] = 0.5 * (1.0 + t);
85        out[1] = -a * sech2 / (2.0 * sigma);
86        out[2] = -a * eps * sech2 / (2.0 * sigma);
87    }
88
89    fn param_names(&self) -> Vec<std::borrow::Cow<'static, str>> {
90        vec!["amplitude".into(), "center".into(), "sigma".into()]
91    }
92}
93
94/// Erfc step: `(A / 2) · erfc((x − x₀) / (σ · √2))`
95///
96/// Parameters (in order): `[amplitude, center, sigma]`
97///
98/// Uses `libm::erfc`. Commonly used for XANES / XPS pre-edge backgrounds.
99///
100/// ∂/∂amplitude = ½ · erfc(u)
101/// ∂/∂center    = A / (σ · √(2π)) · exp(−u²)    [u = (x−x₀)/(σ√2)]
102/// ∂/∂sigma     = A · u / (σ · √(2π)) · exp(−u²)
103pub struct ErfcStep;
104
105impl Model for ErfcStep {
106    fn eval(&self, x: &[f64], params: &[f64]) -> f64 {
107        let (a, x0, sigma) = (params[0], params[1], params[2]);
108        let u = (x[0] - x0) / (sigma * std::f64::consts::SQRT_2);
109        0.5 * a * libm::erfc(u)
110    }
111
112    fn jacobian(&self, x: &[f64], params: &[f64]) -> Vec<f64> {
113        let (a, x0, sigma) = (params[0], params[1], params[2]);
114        let sqrt2 = std::f64::consts::SQRT_2;
115        let u = (x[0] - x0) / (sigma * sqrt2);
116        let gauss = (-u * u).exp() / (sigma * (2.0 * std::f64::consts::PI).sqrt());
117
118        let da = 0.5 * libm::erfc(u);
119        let dx0 = a * gauss;
120        // ∂f/∂σ = A·exp(−u²)·u / (σ·√π) = √2 · u · (gauss as computed above)
121        let ds = a * u * gauss * std::f64::consts::SQRT_2;
122
123        vec![da, dx0, ds]
124    }
125
126    #[inline]
127    fn jacobian_into(&self, x: &[f64], params: &[f64], out: &mut [f64]) {
128        let (a, x0, sigma) = (params[0], params[1], params[2]);
129        let sqrt2 = std::f64::consts::SQRT_2;
130        let u = (x[0] - x0) / (sigma * sqrt2);
131        let gauss = (-u * u).exp() / (sigma * (2.0 * std::f64::consts::PI).sqrt());
132        out[0] = 0.5 * libm::erfc(u);
133        out[1] = a * gauss;
134        out[2] = a * u * gauss * sqrt2;
135    }
136
137    fn param_names(&self) -> Vec<std::borrow::Cow<'static, str>> {
138        vec!["amplitude".into(), "center".into(), "sigma".into()]
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use approx::assert_relative_eq;
146
147    // ── ArctanStep ────────────────────────────────────────────────────────────
148
149    #[test]
150    fn arctan_at_center_is_half_amplitude() {
151        let v = ArctanStep.eval(&[0.0], &[2.0, 0.0, 1.0]);
152        assert_relative_eq!(v, 1.0, epsilon = 1e-12); // A/2 = 1
153    }
154
155    #[test]
156    fn arctan_large_x_approaches_amplitude() {
157        let v = ArctanStep.eval(&[1e6], &[1.0, 0.0, 1.0]);
158        assert_relative_eq!(v, 1.0, epsilon = 1e-6);
159    }
160
161    #[test]
162    fn arctan_jacobian_shape() {
163        let j = ArctanStep.jacobian(&[0.5], &[1.0, 0.0, 1.0]);
164        assert_eq!(j.len(), 3);
165    }
166
167    #[test]
168    fn arctan_jacobian_finite_diff_check() {
169        // Upgraded from a single-point forward difference (h=1e-5,
170        // epsilon=1e-4) to a central difference (O(h^2)) swept over three
171        // parameter regimes and several x-points relative to the centre.
172        let param_sets = [
173            [2.0, 0.5, 0.8],   // nominal
174            [1e-3, 0.0, 0.05], // small amplitude/width
175            [5.0, -2.0, 3.0],  // offset centre, wide
176        ];
177        for p in param_sets {
178            let (c, sigma) = (p[1], p[2]);
179            for &mult in &[-3.0_f64, -1.0, -0.1, 0.0, 0.1, 1.0, 3.0] {
180                let x = [c + mult * sigma];
181                let j_analytical = ArctanStep.jacobian(&x, &p);
182                for i in 0..p.len() {
183                    let h = 1e-6 * p[i].abs().max(1.0);
184                    let (mut a, mut b) = (p, p);
185                    a[i] += h;
186                    b[i] -= h;
187                    let fd = (ArctanStep.eval(&x, &a) - ArctanStep.eval(&x, &b)) / (2.0 * h);
188                    assert_relative_eq!(j_analytical[i], fd, epsilon = 1e-7, max_relative = 1e-6);
189                }
190            }
191        }
192    }
193
194    // ── TanhStep ──────────────────────────────────────────────────────────────
195
196    #[test]
197    fn tanh_at_center_is_half_amplitude() {
198        let v = TanhStep.eval(&[0.0], &[2.0, 0.0, 1.0]);
199        assert_relative_eq!(v, 1.0, epsilon = 1e-12);
200    }
201
202    #[test]
203    fn tanh_large_x_approaches_amplitude() {
204        let v = TanhStep.eval(&[1e2], &[1.0, 0.0, 1.0]);
205        assert_relative_eq!(v, 1.0, epsilon = 1e-6);
206    }
207
208    #[test]
209    fn tanh_jacobian_finite_diff_check() {
210        // Upgraded from a single-point forward difference (h=1e-5,
211        // epsilon=1e-4) to a central difference (O(h^2)) swept over three
212        // parameter regimes and several x-points relative to the centre.
213        let param_sets = [
214            [1.5, 1.0, 0.5],   // nominal
215            [1e-3, 0.0, 0.05], // small amplitude/width
216            [5.0, -2.0, 3.0],  // offset centre, wide
217        ];
218        for p in param_sets {
219            let (c, sigma) = (p[1], p[2]);
220            for &mult in &[-3.0_f64, -1.0, -0.1, 0.0, 0.1, 1.0, 3.0] {
221                let x = [c + mult * sigma];
222                let j_analytical = TanhStep.jacobian(&x, &p);
223                for i in 0..p.len() {
224                    let h = 1e-6 * p[i].abs().max(1.0);
225                    let (mut a, mut b) = (p, p);
226                    a[i] += h;
227                    b[i] -= h;
228                    let fd = (TanhStep.eval(&x, &a) - TanhStep.eval(&x, &b)) / (2.0 * h);
229                    assert_relative_eq!(j_analytical[i], fd, epsilon = 1e-7, max_relative = 1e-6);
230                }
231            }
232        }
233    }
234
235    // ── ErfcStep ──────────────────────────────────────────────────────────────
236
237    #[test]
238    fn erfc_at_center_is_half_amplitude() {
239        // erfc(0) = 1 → A/2
240        let v = ErfcStep.eval(&[0.0], &[2.0, 0.0, 1.0]);
241        assert_relative_eq!(v, 1.0, epsilon = 1e-12);
242    }
243
244    #[test]
245    fn erfc_large_neg_x_approaches_amplitude() {
246        let v = ErfcStep.eval(&[-1e6], &[1.0, 0.0, 1.0]);
247        assert_relative_eq!(v, 1.0, epsilon = 1e-6);
248    }
249
250    #[test]
251    fn erfc_jacobian_finite_diff_check() {
252        // Upgraded from a single-point forward difference (h=1e-5,
253        // epsilon=1e-5) to a central difference (O(h^2)) swept over three
254        // parameter regimes and several x-points relative to the centre.
255        let param_sets = [
256            [1.0, 0.0, 1.0],   // nominal
257            [1e-3, 0.0, 0.05], // small amplitude/width
258            [5.0, -2.0, 3.0],  // offset centre, wide
259        ];
260        for p in param_sets {
261            let (c, sigma) = (p[1], p[2]);
262            for &mult in &[-3.0_f64, -1.0, -0.1, 0.0, 0.1, 1.0, 3.0] {
263                let x = [c + mult * sigma];
264                let j_analytical = ErfcStep.jacobian(&x, &p);
265                for i in 0..p.len() {
266                    let h = 1e-6 * p[i].abs().max(1.0);
267                    let (mut a, mut b) = (p, p);
268                    a[i] += h;
269                    b[i] -= h;
270                    let fd = (ErfcStep.eval(&x, &a) - ErfcStep.eval(&x, &b)) / (2.0 * h);
271                    assert_relative_eq!(j_analytical[i], fd, epsilon = 1e-7, max_relative = 1e-6);
272                }
273            }
274        }
275    }
276}