spectrafit_models/
saturating_exponential.rs1use crate::math_backend::batch_exp;
2use crate::Model;
3
4pub struct SaturatingExponential;
17
18impl Model for SaturatingExponential {
19 fn eval(&self, x: &[f64], p: &[f64]) -> f64 {
20 let xi = x[0];
21 p[0] * (1.0 - (-p[1] * xi).exp())
22 }
23
24 fn jacobian_into(&self, x: &[f64], p: &[f64], out: &mut [f64]) {
25 let xi = x[0];
26 let e = (-p[1] * xi).exp();
27 out[0] = 1.0 - e; out[1] = p[0] * xi * e; }
30
31 fn jacobian(&self, x: &[f64], p: &[f64]) -> Vec<f64> {
32 let xi = x[0];
33 let e = (-p[1] * xi).exp();
34 vec![
35 1.0 - e, p[0] * xi * e, ]
38 }
39
40 fn param_names(&self) -> Vec<std::borrow::Cow<'static, str>> {
41 vec!["amplitude".into(), "rate".into()]
42 }
43
44 fn eval_slice_into(&self, xs: &[f64], params: &[f64], out: &mut [f64]) {
45 debug_assert_eq!(out.len(), xs.len());
46 let (amplitude, rate) = (params[0], params[1]);
47 let args: Vec<f64> = xs.iter().map(|xi| -rate * xi).collect();
48 let mut exps = vec![0.0_f64; xs.len()];
49 batch_exp(&mut exps, &args);
50 for (slot, e) in out.iter_mut().zip(exps.iter()) {
51 *slot = amplitude * (1.0 - e);
52 }
53 }
54
55 fn jac_slice_into(&self, xs: &[f64], params: &[f64], out: &mut [f64]) {
56 debug_assert_eq!(out.len(), xs.len() * 2);
57 let (amplitude, rate) = (params[0], params[1]);
58 let args: Vec<f64> = xs.iter().map(|xi| -rate * xi).collect();
59 let mut exps = vec![0.0_f64; xs.len()];
60 batch_exp(&mut exps, &args);
61 for (i, (xi, e)) in xs.iter().zip(exps.iter()).enumerate() {
62 out[i * 2] = 1.0 - e; out[i * 2 + 1] = amplitude * xi * e; }
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use approx::assert_relative_eq;
72
73 fn model() -> SaturatingExponential {
74 SaturatingExponential
75 }
76
77 #[test]
79 fn eval_at_zero() {
80 let v = model().eval(&[0.0], &[3.0, 0.5]);
81 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
82 }
83
84 #[test]
86 fn eval_zero_rate() {
87 let v = model().eval(&[2.0], &[3.0, 0.0]);
88 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
89 }
90
91 #[test]
93 fn eval_saturates_to_amplitude() {
94 let amplitude = 5.0;
95 let v = model().eval(&[1000.0], &[amplitude, 1.0]);
96 assert_relative_eq!(v, amplitude, epsilon = 1e-9);
97 }
98
99 #[test]
100 fn jacobian_shape() {
101 let j = model().jacobian(&[1.0], &[3.0, 0.5]);
102 assert_eq!(j.len(), 2);
103 }
104
105 const SAT_EXP_REGIMES: [[f64; 2]; 3] = [
110 [3.0, 0.5], [1e-3, 1e-3], [50.0, 2.0], ];
114 const SAT_EXP_XS: [f64; 5] = [0.1, 0.5, 1.5, 4.0, 10.0];
115
116 fn check_sat_exp_param_central_diff(idx: usize) {
117 for p in SAT_EXP_REGIMES {
118 for &x in &SAT_EXP_XS {
119 let j = model().jacobian(&[x], &p);
120 let h = 1e-6 * p[idx].abs().max(1.0);
121 let mut pp = p;
122 pp[idx] += h;
123 let mut pm = p;
124 pm[idx] -= h;
125 let fd = (model().eval(&[x], &pp) - model().eval(&[x], &pm)) / (2.0 * h);
126 assert_relative_eq!(j[idx], fd, epsilon = 1e-7, max_relative = 1e-6);
127 }
128 }
129 }
130
131 #[test]
132 fn jacobian_numerical_amplitude() {
133 check_sat_exp_param_central_diff(0);
134 }
135
136 #[test]
137 fn jacobian_numerical_rate() {
138 check_sat_exp_param_central_diff(1);
139 }
140
141 #[test]
142 fn jacobian_into_matches_jacobian() {
143 let x = &[2.0f64];
144 let p = [3.0, 0.5];
145 let j_vec = model().jacobian(x, &p);
146 let mut out = [0.0_f64; 2];
147 model().jacobian_into(x, &p, &mut out);
148 assert_relative_eq!(out[0], j_vec[0], epsilon = 1e-12);
149 assert_relative_eq!(out[1], j_vec[1], epsilon = 1e-12);
150 }
151
152 #[test]
153 fn eval_slice_matches_scalar() {
154 let xs: Vec<f64> = (0..10).map(|i| i as f64 * 0.5).collect();
155 let p = [3.0, 0.5];
156 let mut out = vec![0.0_f64; xs.len()];
157 model().eval_slice_into(&xs, &p, &mut out);
158 for (xi, &bi) in xs.iter().zip(out.iter()) {
159 assert_relative_eq!(bi, model().eval(&[*xi], &p), epsilon = 1e-10);
160 }
161 }
162
163 #[test]
164 fn jac_slice_matches_scalar() {
165 let xs: Vec<f64> = (0..8).map(|i| i as f64 * 0.5).collect();
166 let p = [3.0, 0.5];
167 let n = xs.len();
168 let mut out = vec![0.0_f64; n * 2];
169 model().jac_slice_into(&xs, &p, &mut out);
170 for (i, xi) in xs.iter().enumerate() {
171 let j = model().jacobian(&[*xi], &p);
172 for k in 0..2 {
173 assert_relative_eq!(out[i * 2 + k], j[k], epsilon = 1e-10);
174 }
175 }
176 }
177}