spectrafit_models/
exp_over_linear.rs1use crate::Model;
2
3pub struct ExpOverLinear;
24
25#[inline]
26fn ed(xi: f64, p: &[f64]) -> (f64, f64) {
27 ((-p[0] * xi).exp(), p[1] + p[2] * xi)
28}
29
30#[inline]
31fn jac_at(xi: f64, p: &[f64], out: &mut [f64]) {
32 let (e, d) = ed(xi, p);
33 if d == 0.0 {
34 out[..3].fill(f64::NAN);
35 return;
36 }
37 let inv_d = 1.0 / d;
38 let e_over_d2 = e * inv_d * inv_d;
39 out[0] = -xi * e * inv_d;
40 out[1] = -e_over_d2;
41 out[2] = -xi * e_over_d2;
42}
43
44impl Model for ExpOverLinear {
45 fn eval(&self, x: &[f64], p: &[f64]) -> f64 {
46 let (e, d) = ed(x[0], p);
47 if d == 0.0 {
48 return f64::NAN;
49 }
50 e / d
51 }
52
53 fn jacobian_into(&self, x: &[f64], p: &[f64], out: &mut [f64]) {
54 jac_at(x[0], p, out);
55 }
56
57 fn jacobian(&self, x: &[f64], p: &[f64]) -> Vec<f64> {
58 let mut out = vec![0.0_f64; 3];
59 jac_at(x[0], p, &mut out);
60 out
61 }
62
63 fn param_names(&self) -> Vec<std::borrow::Cow<'static, str>> {
64 vec!["rate".into(), "lin_const".into(), "lin_slope".into()]
65 }
66
67 fn eval_slice_into(&self, xs: &[f64], params: &[f64], out: &mut [f64]) {
68 debug_assert_eq!(out.len(), xs.len());
69 for (slot, &xi) in out.iter_mut().zip(xs.iter()) {
70 let (e, d) = ed(xi, params);
71 *slot = if d == 0.0 { f64::NAN } else { e / d };
72 }
73 }
74
75 fn jac_slice_into(&self, xs: &[f64], params: &[f64], out: &mut [f64]) {
76 debug_assert_eq!(out.len(), xs.len() * 3);
77 for (i, &xi) in xs.iter().enumerate() {
78 jac_at(xi, params, &mut out[i * 3..i * 3 + 3]);
79 }
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use approx::assert_relative_eq;
87
88 fn model() -> ExpOverLinear {
89 ExpOverLinear
90 }
91
92 const CHWIRUT: [f64; 3] = [1.6657666537e-01, 5.1653291286e-03, 1.2150007096e-02];
94
95 #[test]
100 fn eval_chwirut_reproduces_the_certified_fit() {
101 assert_relative_eq!(
102 model().eval(&[0.5], &CHWIRUT),
103 81.855_746_144_55,
104 max_relative = 1e-10
105 );
106 assert_relative_eq!(model().eval(&[6.0], &CHWIRUT), 4.715_0, max_relative = 1e-3);
107 }
108
109 #[test]
110 fn zero_denominator_returns_nan() {
111 let p = [0.1_f64, -1.0, 1.0];
113 assert!(model().eval(&[1.0], &p).is_nan());
114 assert!(model().jacobian(&[1.0], &p).iter().all(|v| v.is_nan()));
115 }
116
117 #[test]
118 fn jacobian_matches_finite_difference() {
119 let param_sets = [
124 CHWIRUT, [1e-3_f64, 1e-2, 1e-3], [5.0_f64, -2.0, 8.0], ];
128 for p in param_sets {
129 for &x in &[0.1_f64, 0.5, 2.5, 6.0, 12.0] {
130 let j = model().jacobian(&[x], &p);
131 for k in 0..3 {
132 let h = 1e-6 * p[k].abs().max(1.0);
133 let mut pp = p;
134 pp[k] += h;
135 let mut pm = p;
136 pm[k] -= h;
137 let fd = (model().eval(&[x], &pp) - model().eval(&[x], &pm)) / (2.0 * h);
138 assert_relative_eq!(j[k], fd, max_relative = 1e-6, epsilon = 1e-7);
139 }
140 }
141 }
142 }
143
144 #[test]
145 fn slices_match_scalar() {
146 let xs = [0.5_f64, 1.0, 3.0, 6.0];
147 let mut ys = vec![0.0_f64; xs.len()];
148 model().eval_slice_into(&xs, &CHWIRUT, &mut ys);
149 let mut js = vec![0.0_f64; xs.len() * 3];
150 model().jac_slice_into(&xs, &CHWIRUT, &mut js);
151 for (i, &xi) in xs.iter().enumerate() {
152 assert_relative_eq!(ys[i], model().eval(&[xi], &CHWIRUT), epsilon = 1e-12);
153 let j = model().jacobian(&[xi], &CHWIRUT);
154 for k in 0..3 {
155 assert_relative_eq!(js[i * 3 + k], j[k], epsilon = 1e-12);
156 }
157 }
158 }
159}