spectrafit_models/
power_saturation.rs1use crate::Model;
2
3pub struct PowerSaturation;
20
21impl Model for PowerSaturation {
22 fn eval(&self, x: &[f64], p: &[f64]) -> f64 {
23 let xi = x[0];
24 let u = 1.0 + p[1] * xi / 2.0;
25 p[0] * (1.0 - u.powi(-2))
26 }
27
28 fn jacobian_into(&self, x: &[f64], p: &[f64], out: &mut [f64]) {
29 let xi = x[0];
30 let u = 1.0 + p[1] * xi / 2.0;
31 let u_neg2 = u.powi(-2);
32 let u_neg3 = u.powi(-3);
33 out[0] = 1.0 - u_neg2; out[1] = p[0] * xi * u_neg3; }
36
37 fn jacobian(&self, x: &[f64], p: &[f64]) -> Vec<f64> {
38 let xi = x[0];
39 let u = 1.0 + p[1] * xi / 2.0;
40 let u_neg2 = u.powi(-2);
41 let u_neg3 = u.powi(-3);
42 vec![
43 1.0 - u_neg2, p[0] * xi * u_neg3, ]
46 }
47
48 fn param_names(&self) -> Vec<std::borrow::Cow<'static, str>> {
49 vec!["amplitude".into(), "rate".into()]
50 }
51
52 fn eval_slice_into(&self, xs: &[f64], params: &[f64], out: &mut [f64]) {
53 debug_assert_eq!(out.len(), xs.len());
54 let (amplitude, rate) = (params[0], params[1]);
55 for (slot, &xi) in out.iter_mut().zip(xs.iter()) {
56 let u = 1.0 + rate * xi / 2.0;
57 *slot = amplitude * (1.0 - u.powi(-2));
58 }
59 }
60
61 fn jac_slice_into(&self, xs: &[f64], params: &[f64], out: &mut [f64]) {
62 debug_assert_eq!(out.len(), xs.len() * 2);
63 let (amplitude, rate) = (params[0], params[1]);
64 for (i, &xi) in xs.iter().enumerate() {
65 let u = 1.0 + rate * xi / 2.0;
66 let u_neg2 = u.powi(-2);
67 let u_neg3 = u.powi(-3);
68 out[i * 2] = 1.0 - u_neg2; out[i * 2 + 1] = amplitude * xi * u_neg3; }
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use approx::assert_relative_eq;
78
79 fn model() -> PowerSaturation {
80 PowerSaturation
81 }
82
83 #[test]
85 fn eval_at_zero() {
86 let v = model().eval(&[0.0], &[10.0, 0.5]);
87 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
88 }
89
90 #[test]
92 fn eval_zero_rate() {
93 let v = model().eval(&[5.0], &[10.0, 0.0]);
94 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
95 }
96
97 #[test]
99 fn eval_saturates_to_amplitude() {
100 let amplitude = 5.0;
101 let v = model().eval(&[1_000_000.0], &[amplitude, 1.0]);
102 assert_relative_eq!(v, amplitude, epsilon = 1e-6);
103 }
104
105 #[test]
106 fn jacobian_shape() {
107 let j = model().jacobian(&[2.0], &[10.0, 0.5]);
108 assert_eq!(j.len(), 2);
109 }
110
111 const POWER_SAT_REGIMES: [[f64; 2]; 3] = [
117 [10.0, 0.001], [1e-3, 1e-4], [50.0, 2.0], ];
121 const POWER_SAT_XS: [f64; 5] = [0.5, 3.0, 20.0, 100.0, 500.0];
122
123 fn check_power_sat_param_central_diff(idx: usize) {
124 for p in POWER_SAT_REGIMES {
125 for &x in &POWER_SAT_XS {
126 let j = model().jacobian(&[x], &p);
127 let h = 1e-6 * p[idx].abs().max(1.0);
128 let mut pp = p;
129 pp[idx] += h;
130 let mut pm = p;
131 pm[idx] -= h;
132 let fd = (model().eval(&[x], &pp) - model().eval(&[x], &pm)) / (2.0 * h);
133 assert_relative_eq!(j[idx], fd, epsilon = 1e-7, max_relative = 1e-6);
134 }
135 }
136 }
137
138 #[test]
140 fn jacobian_numerical_amplitude() {
141 check_power_sat_param_central_diff(0);
142 }
143
144 #[test]
146 fn jacobian_numerical_rate() {
147 check_power_sat_param_central_diff(1);
148 }
149
150 #[test]
151 fn jacobian_into_matches_jacobian() {
152 let x = &[5.0f64];
153 let p = [300.0, 0.0004];
154 let j_vec = model().jacobian(x, &p);
155 let mut out = [0.0_f64; 2];
156 model().jacobian_into(x, &p, &mut out);
157 assert_relative_eq!(out[0], j_vec[0], epsilon = 1e-12);
158 assert_relative_eq!(out[1], j_vec[1], epsilon = 1e-12);
159 }
160
161 #[test]
162 fn eval_slice_matches_scalar() {
163 let xs: Vec<f64> = (0..10).map(|i| i as f64 * 100.0).collect();
164 let p = [300.0, 0.0004];
165 let mut out = vec![0.0_f64; xs.len()];
166 model().eval_slice_into(&xs, &p, &mut out);
167 for (xi, &bi) in xs.iter().zip(out.iter()) {
168 assert_relative_eq!(bi, model().eval(&[*xi], &p), epsilon = 1e-10);
169 }
170 }
171
172 #[test]
173 fn jac_slice_matches_scalar() {
174 let xs: Vec<f64> = (0..8).map(|i| i as f64 * 100.0).collect();
175 let p = [300.0, 0.0004];
176 let n = xs.len();
177 let mut out = vec![0.0_f64; n * 2];
178 model().jac_slice_into(&xs, &p, &mut out);
179 for (i, xi) in xs.iter().enumerate() {
180 let j = model().jacobian(&[*xi], &p);
181 for k in 0..2 {
182 assert_relative_eq!(out[i * 2 + k], j[k], epsilon = 1e-10);
183 }
184 }
185 }
186
187 #[test]
191 fn known_value() {
192 let x = &[200.0f64];
193 let p = [337.997_463_63, 3.903_909_128_7e-4];
194 let y = model().eval(x, &p);
195 assert!(y > 20.0 && y < 30.0, "Known-value sanity: y={y}");
198 }
199}