1use crate::Model;
2
3pub struct GeneralisedLogistic;
28
29#[inline]
30fn parts(xi: f64, p: &[f64]) -> (f64, f64, f64) {
31 let u = (p[1] - p[2] * xi).exp();
32 let s = 1.0 + u;
33 let q = s.powf(-1.0 / p[3]);
34 (u, s, q)
35}
36
37#[inline]
38fn jac_at(xi: f64, p: &[f64], out: &mut [f64]) {
39 if p[3] == 0.0 {
40 out[..4].fill(f64::NAN);
41 return;
42 }
43 let (u, s, q) = parts(xi, p);
44 let common = p[0] * q * u / (p[3] * s);
45 out[0] = q;
46 out[1] = -common;
47 out[2] = common * xi;
48 out[3] = p[0] * q * s.ln() / (p[3] * p[3]);
49}
50
51impl Model for GeneralisedLogistic {
52 fn eval(&self, x: &[f64], p: &[f64]) -> f64 {
53 if p[3] == 0.0 {
54 return f64::NAN;
55 }
56 parts(x[0], p).2 * p[0]
57 }
58
59 fn jacobian_into(&self, x: &[f64], p: &[f64], out: &mut [f64]) {
60 jac_at(x[0], p, out);
61 }
62
63 fn jacobian(&self, x: &[f64], p: &[f64]) -> Vec<f64> {
64 let mut out = vec![0.0_f64; 4];
65 jac_at(x[0], p, &mut out);
66 out
67 }
68
69 fn param_names(&self) -> Vec<std::borrow::Cow<'static, str>> {
70 vec![
71 "amplitude".into(),
72 "shift".into(),
73 "rate".into(),
74 "shape".into(),
75 ]
76 }
77
78 fn eval_slice_into(&self, xs: &[f64], params: &[f64], out: &mut [f64]) {
79 debug_assert_eq!(out.len(), xs.len());
80 for (slot, &xi) in out.iter_mut().zip(xs.iter()) {
81 *slot = if params[3] == 0.0 {
82 f64::NAN
83 } else {
84 params[0] * parts(xi, params).2
85 };
86 }
87 }
88
89 fn jac_slice_into(&self, xs: &[f64], params: &[f64], out: &mut [f64]) {
90 debug_assert_eq!(out.len(), xs.len() * 4);
91 for (i, &xi) in xs.iter().enumerate() {
92 jac_at(xi, params, &mut out[i * 4..i * 4 + 4]);
93 }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use approx::assert_relative_eq;
101
102 fn model() -> GeneralisedLogistic {
103 GeneralisedLogistic
104 }
105
106 const RAT43: [f64; 4] = [
108 6.9964151270e02,
109 5.2771253025e00,
110 7.5962938329e-01,
111 1.2792483859e00,
112 ];
113
114 #[test]
120 fn eval_rat43_reproduces_the_certified_fit() {
121 assert_relative_eq!(
122 model().eval(&[1.0], &RAT43),
123 20.301_882_778_86,
124 max_relative = 1e-10
125 );
126 assert_relative_eq!(
127 model().eval(&[15.0], &RAT43),
128 698.438_251,
129 max_relative = 1e-6
130 );
131 }
132
133 #[test]
134 fn shape_one_is_the_plain_logistic() {
135 let p = [72.0_f64, 2.6, 0.067, 1.0];
137 for x in [9.0, 30.0, 80.0] {
138 let want = p[0] / (1.0 + (p[1] - p[2] * x).exp());
139 assert_relative_eq!(model().eval(&[x], &p), want, max_relative = 1e-12);
140 }
141 }
142
143 #[test]
144 fn zero_shape_returns_nan() {
145 let p = [1.0_f64, 0.5, 0.5, 0.0];
146 assert!(model().eval(&[1.0], &p).is_nan());
147 assert!(model().jacobian(&[1.0], &p).iter().all(|v| v.is_nan()));
148 }
149
150 #[test]
151 fn jacobian_matches_finite_difference() {
152 let param_sets = [
156 RAT43, [1e-3_f64, 0.0, 1e-2, 0.5], [5.0_f64, -2.0, 3.0, -1.5], ];
160 for p in param_sets {
161 for &x in &[0.1_f64, 1.0, 5.0, 7.5, 15.0] {
162 let j = model().jacobian(&[x], &p);
163 for k in 0..4 {
164 let h = 1e-6 * p[k].abs().max(1.0);
165 let mut pp = p;
166 pp[k] += h;
167 let mut pm = p;
168 pm[k] -= h;
169 let fd = (model().eval(&[x], &pp) - model().eval(&[x], &pm)) / (2.0 * h);
170 assert_relative_eq!(j[k], fd, max_relative = 1e-6, epsilon = 1e-7);
171 }
172 }
173 }
174 }
175
176 #[test]
177 fn slices_match_scalar() {
178 let xs = [1.0_f64, 5.0, 10.0, 15.0];
179 let mut ys = vec![0.0_f64; xs.len()];
180 model().eval_slice_into(&xs, &RAT43, &mut ys);
181 let mut js = vec![0.0_f64; xs.len() * 4];
182 model().jac_slice_into(&xs, &RAT43, &mut js);
183 for (i, &xi) in xs.iter().enumerate() {
184 assert_relative_eq!(ys[i], model().eval(&[xi], &RAT43), epsilon = 1e-12);
185 let j = model().jacobian(&[xi], &RAT43);
186 for k in 0..4 {
187 assert_relative_eq!(js[i * 4 + k], j[k], epsilon = 1e-12);
188 }
189 }
190 }
191}