spectrafit_models/
doniach.rs1use crate::Model;
2
3pub struct DoniachSunjic;
16
17impl Model for DoniachSunjic {
18 fn eval(&self, x: &[f64], params: &[f64]) -> f64 {
19 let (a, c, sigma, gamma) = (params[0], params[1], params[2], params[3]);
20 let u = (x[0] - c) / sigma;
21 let num = (std::f64::consts::FRAC_PI_2 * gamma + (1.0 - gamma) * u.atan()).cos();
22 let den = (1.0 + u * u).powf((1.0 - gamma) / 2.0);
23 a * num / den
24 }
25
26 fn jacobian(&self, x: &[f64], params: &[f64]) -> Vec<f64> {
42 let (a, c, sigma, gamma) = (params[0], params[1], params[2], params[3]);
43 let u = (x[0] - c) / sigma;
44 let u2_1 = 1.0 + u * u;
45 let atan_u = u.atan();
46 let phi = std::f64::consts::FRAC_PI_2 * gamma + (1.0 - gamma) * atan_u;
47 let d = u2_1.powf((1.0 - gamma) / 2.0);
48 let cos_phi = phi.cos();
49 let sin_phi = phi.sin();
50
51 let da = cos_phi / d;
52
53 let df_du_coeff = a * (1.0 - gamma) / (u2_1 * d);
56 let du_core = -sin_phi - cos_phi * u;
57 let dc = df_du_coeff * du_core * (-1.0 / sigma);
58 let ds = df_du_coeff * du_core * (-u / sigma);
59
60 let dg =
62 a / d * (-sin_phi * (std::f64::consts::FRAC_PI_2 - atan_u) + 0.5 * cos_phi * u2_1.ln());
63
64 vec![da, dc, ds, dg]
65 }
66
67 #[inline]
68 fn jacobian_into(&self, x: &[f64], params: &[f64], out: &mut [f64]) {
69 let jac = self.jacobian(x, params);
70 out[..4].copy_from_slice(&jac);
71 }
72
73 fn param_names(&self) -> Vec<std::borrow::Cow<'static, str>> {
74 vec![
75 "amplitude".into(),
76 "center".into(),
77 "sigma".into(),
78 "gamma".into(),
79 ]
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use approx::assert_relative_eq;
87
88 #[test]
89 fn symmetric_at_gamma_zero_is_lorentzian_at_center() {
90 let m = DoniachSunjic;
92 assert_relative_eq!(m.eval(&[0.0], &[2.0, 0.0, 1.0, 0.0]), 2.0, epsilon = 1e-12);
93 }
94
95 #[test]
96 fn asymmetry_breaks_mirror_symmetry() {
97 let m = DoniachSunjic;
99 let left = m.eval(&[-1.0], &[1.0, 0.0, 1.0, 0.2]);
100 let right = m.eval(&[1.0], &[1.0, 0.0, 1.0, 0.2]);
101 assert!((left - right).abs() > 1e-3);
102 }
103
104 #[test]
105 fn param_names_are_canonical() {
106 assert_eq!(
107 DoniachSunjic
108 .param_names()
109 .iter()
110 .map(|c| c.as_ref())
111 .collect::<Vec<_>>(),
112 &["amplitude", "center", "sigma", "gamma"]
113 );
114 }
115
116 #[test]
129 fn jacobian_matches_central_difference_across_regimes() {
130 let m = DoniachSunjic;
135 let param_sets = [
136 [2.0, 0.0, 1.0, 0.3], [1e-3, 0.0, 0.05, 0.1], [5.0, -2.0, 3.0, 0.7], ];
140 for p in param_sets {
141 for &x in &[-3.0_f64, -0.5, 0.0, p[1], 0.5, 1.0, 7.0] {
142 let j = m.jacobian(&[x], &p);
143 for i in 0..p.len() {
144 let h = 1e-6 * p[i].abs().max(1.0);
145 let (mut a, mut b) = (p, p);
146 a[i] += h;
147 b[i] -= h;
148 let fd = (m.eval(&[x], &a) - m.eval(&[x], &b)) / (2.0 * h);
149 assert_relative_eq!(j[i], fd, epsilon = 1e-7, max_relative = 1e-6);
150 }
151 }
152 }
153 }
154
155 #[test]
156 fn gamma_zero_equals_lorentzian_everywhere() {
157 use crate::lorentzian::Lorentzian;
158 let ds = DoniachSunjic;
159 let lor = Lorentzian;
160 let a = 2.5_f64;
161 let c = 0.4_f64;
162 let sigma = 0.9_f64;
163 let p_ds = [a, c, sigma, 0.0]; let p_lor = [a, c, sigma];
165 for &xi in &[-2.0_f64, -0.5, c, 0.5, 2.0] {
166 assert_relative_eq!(
167 ds.eval(&[xi], &p_ds),
168 lor.eval(&[xi], &p_lor),
169 epsilon = 1e-12
170 );
171 }
172 }
173}