spectrafit_models/
log_normal.rs1use crate::Model;
2
3pub struct LogNormal;
15
16impl Model for LogNormal {
17 fn eval(&self, x: &[f64], params: &[f64]) -> f64 {
18 let (a, c, sigma) = (params[0], params[1], params[2]);
19 if x[0] > 0.0 {
20 let l = (x[0] / c).ln();
21 let z = -(l * l) / (2.0 * sigma * sigma);
22 a * z.exp()
23 } else {
24 0.0
25 }
26 }
27
28 fn jacobian(&self, x: &[f64], params: &[f64]) -> Vec<f64> {
35 let mut p = params.to_vec();
36 (0..params.len())
37 .map(|i| {
38 let h = 1e-7_f64 * params[i].abs().max(1e-7);
39 p[i] = params[i] + h;
40 let f_plus = self.eval(x, &p);
41 p[i] = params[i] - h;
42 let f_minus = self.eval(x, &p);
43 p[i] = params[i];
44 (f_plus - f_minus) / (2.0 * h)
45 })
46 .collect()
47 }
48
49 fn param_names(&self) -> Vec<std::borrow::Cow<'static, str>> {
50 vec!["amplitude".into(), "center".into(), "sigma".into()]
51 }
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57 use approx::assert_relative_eq;
58
59 #[test]
60 fn eval_at_center_equals_amplitude() {
61 let m = LogNormal;
63 let v = m.eval(&[2.5], &[3.0, 2.5, 0.4]);
64 assert_relative_eq!(v, 3.0, epsilon = 1e-12);
65 }
66
67 #[test]
68 fn eval_non_positive_is_zero() {
69 let m = LogNormal;
71 assert_eq!(m.eval(&[0.0], &[1.0, 2.0, 0.5]), 0.0);
72 assert_eq!(m.eval(&[-1.0], &[1.0, 2.0, 0.5]), 0.0);
73 }
74
75 #[test]
76 fn param_names_are_canonical() {
77 assert_eq!(
78 LogNormal
79 .param_names()
80 .iter()
81 .map(|c| c.as_ref())
82 .collect::<Vec<_>>(),
83 &["amplitude", "center", "sigma"]
84 );
85 }
86
87 #[test]
88 fn jacobian_shape() {
89 let j = LogNormal.jacobian(&[1.5], &[2.0, 2.0, 0.6]);
90 assert_eq!(j.len(), 3);
91 }
92
93 #[test]
94 fn jacobian_amplitude_numerical_check() {
95 let m = LogNormal;
97 let j = m.jacobian(&[2.0], &[3.0, 2.0, 0.5]);
98 assert_relative_eq!(j[0], 1.0, epsilon = 1e-6);
99 }
100
101 #[test]
102 fn jacobian_center_zero_at_peak() {
103 let m = LogNormal;
105 let j = m.jacobian(&[2.0], &[3.0, 2.0, 0.5]);
106 assert_relative_eq!(j[1], 0.0, epsilon = 1e-6);
107 }
108}