1use crate::Model;
2
3pub struct PowerLawOffset;
26
27impl Model for PowerLawOffset {
28 fn eval(&self, x: &[f64], p: &[f64]) -> f64 {
29 let amplitude = p[0];
30 let offset = p[1];
31 let shape = p[2];
32 let u = offset + x[0];
33 if u <= 0.0 {
34 return f64::NAN;
35 }
36 let exponent = -1.0 / shape;
37 amplitude * u.powf(exponent)
38 }
39
40 fn jacobian_into(&self, x: &[f64], p: &[f64], out: &mut [f64]) {
41 let amplitude = p[0];
42 let offset = p[1];
43 let shape = p[2];
44 let u = offset + x[0];
45 if u <= 0.0 {
46 out[0] = f64::NAN;
47 out[1] = f64::NAN;
48 out[2] = f64::NAN;
49 return;
50 }
51 let exponent = -1.0 / shape;
52 let u_pow = u.powf(exponent); let u_pow_m1 = u_pow / u; out[0] = u_pow; out[1] = amplitude * exponent * u_pow_m1; out[2] = amplitude * u_pow * u.ln() / (shape * shape); }
59
60 fn jacobian(&self, x: &[f64], p: &[f64]) -> Vec<f64> {
61 let amplitude = p[0];
62 let offset = p[1];
63 let shape = p[2];
64 let u = offset + x[0];
65 if u <= 0.0 {
66 return vec![f64::NAN, f64::NAN, f64::NAN];
67 }
68 let exponent = -1.0 / shape;
69 let u_pow = u.powf(exponent);
70 let u_pow_m1 = u_pow / u;
71 vec![
72 u_pow, amplitude * exponent * u_pow_m1, amplitude * u_pow * u.ln() / (shape * shape), ]
76 }
77
78 fn param_names(&self) -> Vec<std::borrow::Cow<'static, str>> {
79 vec!["amplitude".into(), "offset".into(), "shape".into()]
80 }
81
82 fn eval_slice_into(&self, xs: &[f64], params: &[f64], out: &mut [f64]) {
83 debug_assert_eq!(out.len(), xs.len());
84 let amplitude = params[0];
85 let offset = params[1];
86 let shape = params[2];
87 let exponent = -1.0 / shape;
88 for (slot, &xi) in out.iter_mut().zip(xs.iter()) {
89 let u = offset + xi;
90 *slot = if u <= 0.0 {
91 f64::NAN
92 } else {
93 amplitude * u.powf(exponent)
94 };
95 }
96 }
97
98 fn jac_slice_into(&self, xs: &[f64], params: &[f64], out: &mut [f64]) {
99 debug_assert_eq!(out.len(), xs.len() * 3);
100 let amplitude = params[0];
101 let offset = params[1];
102 let shape = params[2];
103 let exponent = -1.0 / shape;
104 let inv_shape2 = 1.0 / (shape * shape);
105 for (i, &xi) in xs.iter().enumerate() {
106 let u = offset + xi;
107 if u <= 0.0 {
108 out[i * 3] = f64::NAN;
109 out[i * 3 + 1] = f64::NAN;
110 out[i * 3 + 2] = f64::NAN;
111 } else {
112 let u_pow = u.powf(exponent);
113 let u_pow_m1 = u_pow / u;
114 out[i * 3] = u_pow;
115 out[i * 3 + 1] = amplitude * exponent * u_pow_m1;
116 out[i * 3 + 2] = amplitude * u_pow * u.ln() * inv_shape2;
117 }
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use approx::assert_relative_eq;
126
127 fn model() -> PowerLawOffset {
128 PowerLawOffset
129 }
130
131 #[test]
136 fn eval_first_bennett5_point_approx() {
137 let p = [-2523.5_f64, 46.74, 0.9322];
138 let y = model().eval(&[7.447168], &p);
139 assert!((y - (-34.8)).abs() < 0.3, "Expected ~-34.8, got {y}");
140 }
141
142 #[test]
144 fn eval_negative_u_returns_nan() {
145 let p = [1.0_f64, 1.0, 0.5];
146 let y = model().eval(&[-5.0], &p); assert!(y.is_nan(), "Expected NaN for u≤0, got {y}");
148 }
149
150 #[test]
151 fn jacobian_shape() {
152 let j = model().jacobian(&[5.0], &[1.0, 2.0, 0.5]);
153 assert_eq!(j.len(), 3);
154 assert!(
155 j.iter().all(|v| v.is_finite()),
156 "Jacobian must be finite: {j:?}"
157 );
158 }
159
160 const POWER_LAW_OFFSET_REGIMES: [[f64; 3]; 3] = [
167 [-2500.0, 46.7, 0.93], [1e-3, 1.0, 0.5], [50.0, 5.0, -2.0], ];
171 const POWER_LAW_OFFSET_XS: [f64; 5] = [1.0, 3.0, 8.0, 15.0, 40.0];
172
173 fn check_power_law_offset_param_central_diff(idx: usize) {
174 for p in POWER_LAW_OFFSET_REGIMES {
175 for &x in &POWER_LAW_OFFSET_XS {
176 let j = model().jacobian(&[x], &p);
177 let h = 1e-6 * p[idx].abs().max(1.0);
178 let mut pp = p;
179 pp[idx] += h;
180 let mut pm = p;
181 pm[idx] -= h;
182 let fd = (model().eval(&[x], &pp) - model().eval(&[x], &pm)) / (2.0 * h);
183 assert_relative_eq!(j[idx], fd, epsilon = 1e-7, max_relative = 1e-6);
184 }
185 }
186 }
187
188 #[test]
190 fn jacobian_fd_amplitude() {
191 check_power_law_offset_param_central_diff(0);
192 }
193
194 #[test]
196 fn jacobian_fd_offset() {
197 check_power_law_offset_param_central_diff(1);
198 }
199
200 #[test]
202 fn jacobian_fd_shape() {
203 check_power_law_offset_param_central_diff(2);
204 }
205
206 #[test]
207 fn jacobian_into_matches_jacobian() {
208 let x = &[10.0_f64];
209 let p = [-2500.0_f64, 46.7, 0.93];
210 let j_vec = model().jacobian(x, &p);
211 let mut out = [0.0_f64; 3];
212 model().jacobian_into(x, &p, &mut out);
213 assert_relative_eq!(out[0], j_vec[0], epsilon = 1e-12);
214 assert_relative_eq!(out[1], j_vec[1], epsilon = 1e-12);
215 assert_relative_eq!(out[2], j_vec[2], epsilon = 1e-12);
216 }
217
218 #[test]
219 fn eval_slice_matches_scalar() {
220 let xs: Vec<f64> = (0..10).map(|i| 7.0 + i as f64 * 0.5).collect();
221 let p = [-2500.0_f64, 46.7, 0.93];
222 let mut out = vec![0.0_f64; xs.len()];
223 model().eval_slice_into(&xs, &p, &mut out);
224 for (&xi, &bi) in xs.iter().zip(out.iter()) {
225 assert_relative_eq!(bi, model().eval(&[xi], &p), epsilon = 1e-10);
226 }
227 }
228
229 #[test]
230 fn jac_slice_matches_scalar() {
231 let xs: Vec<f64> = (0..8).map(|i| 8.0 + i as f64 * 0.4).collect();
232 let p = [-2500.0_f64, 46.7, 0.93];
233 let n = xs.len();
234 let mut out = vec![0.0_f64; n * 3];
235 model().jac_slice_into(&xs, &p, &mut out);
236 for (i, xi) in xs.iter().enumerate() {
237 let j = model().jacobian(&[*xi], &p);
238 for k in 0..3 {
239 assert_relative_eq!(out[i * 3 + k], j[k], epsilon = 1e-10);
240 }
241 }
242 }
243}