gamlr/
offset_estimator.rs1use alloc::vec::Vec;
2
3const MAX_ALPHA: f64 = 4.0;
4const MIN_ALPHA: f64 = 1.0;
5const A: u64 = 6364136223846793005;
7const C: u64 = 1442695040888963407;
8pub struct LcgRng {
23 state: u64,
24 a: u64,
25 c: u64,
26 m: u64,
27}
28
29impl LcgRng {
30 pub fn new(seed: u64) -> Self {
31 LcgRng {
32 state: seed,
33 a: A,
34 c: C,
35 m: u64::MAX,
36 }
37 }
38
39 pub fn gen_range(&mut self, range: core::ops::Range<f64>) -> f64 {
41 let random_u64 = self.next_u64();
42 let random_f64 = random_u64 as f64 / u64::MAX as f64;
43 range.start + random_f64 * (range.end - range.start)
44 }
45
46 fn marsaglia_polar_sample(&mut self) -> f64 {
54 loop {
55 let u: f64 = self.gen_range(-1.0..1.0);
56 let v: f64 = self.gen_range(-1.0..1.0);
57 let s = u * u + v * v;
58 if s < 1.0 && s != 0.0 {
59 let z0 = u * libm::sqrt(-2.0 * libm::log(s) / s);
60 return z0;
61 }
62 }
63 }
64
65 fn next_u64(&mut self) -> u64 {
66 self.state = (self.a.wrapping_mul(self.state).wrapping_add(self.c)) % self.m;
67 self.state
68 }
69}
70
71fn estimate_gamma_parameters(x: &[f64]) -> (f64, f64) {
74 let n = x.len() as f64;
75 let mean_x = x.iter().sum::<f64>() / n;
76 let sum_sq_diff = x.iter().map(|&xi| libm::pow(xi - mean_x, 2.0)).sum::<f64>();
77 let var_x = sum_sq_diff / (n - 1.0);
78
79 let alpha = libm::pow(mean_x, 2.0) / var_x;
80 let beta = var_x / mean_x;
81
82 (alpha, beta)
83}
84
85fn sort_values<'a, I>(values: I) -> Vec<f64>
87where
88 I: IntoIterator<Item = &'a f64>,
89 I::IntoIter: ExactSizeIterator + Clone,
90{
91 let mut sorted_values: Vec<_> = values.into_iter().cloned().collect();
92 sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
93 sorted_values
94}
95
96fn generate_random_gamma_values(alpha: f64, beta: f64, num_samples: usize, seed: u64) -> Vec<f64> {
101 let mut rng = LcgRng::new(seed);
102 (0..num_samples)
103 .map(|_| {
104 let d = alpha - 1.0 / 3.0;
105 let c = (1.0 / 3.0) / libm::sqrt(d);
106
107 loop {
108 let x = rng.marsaglia_polar_sample();
109 let v = 1.0 + c * x;
110 if v <= 0.0 {
111 continue;
112 }
113
114 let v = v * v * v;
115 let u = rng.gen_range(0.0..1.0);
116
117 let x_squared = x * x;
118
119 if u < 1.0 - 0.0331 * x_squared * x_squared
120 || libm::log(u) < 0.5 * x_squared + d * (1.0 - v + libm::log(v))
121 {
122 break d * v * beta;
123 }
124 }
125 })
126 .collect()
127}
128
129pub fn estimate<I>(time_values: I, seed: Option<u64>) -> f64
135where
136 I: IntoIterator<Item = f64>,
137{
138 let time_values_vec: Vec<f64> = time_values.into_iter().collect();
139 let n = time_values_vec.len();
140 let (mut alpha, beta) = estimate_gamma_parameters(&time_values_vec);
141 alpha = alpha.max(MIN_ALPHA).min(MAX_ALPHA);
142 let lcg_seed = LcgRng::new(0).next_u64();
143 let random_values = generate_random_gamma_values(alpha, beta, n, seed.unwrap_or(lcg_seed));
144 let sorted = sort_values(&time_values_vec);
145 let random_sorted = sort_values(&random_values);
146
147 estimate_offset(&sorted, &random_sorted)
148}
149
150pub fn estimate_offset(x_sort: &[f64], y: &[f64]) -> f64 {
155 let n = x_sort.len();
156 let mut y_regression = Vec::new();
157 let mut x_regression = Vec::new();
158
159 for i in 0..n {
160 let rank = (i + 1) as f64;
161 let p_value = (rank - 0.5) / n as f64;
162 y_regression.push(y[i]);
163 x_regression.push(x_sort[i] - p_value);
164 }
165
166 let x_mean = x_regression.iter().sum::<f64>() / x_regression.len() as f64;
167 let y_mean = y_regression.iter().sum::<f64>() / y_regression.len() as f64;
168
169 let beta = {
171 let numerator = x_regression
172 .iter()
173 .zip(y_regression.iter())
174 .map(|(x, y)| (x - x_mean) * (y - y_mean))
175 .sum::<f64>();
176 let denominator = x_regression
177 .iter()
178 .map(|x| libm::pow(x - x_mean, 2.0))
179 .sum::<f64>();
180 numerator / denominator
181 };
182 let gamma = { y_mean - beta * x_mean };
183
184 -gamma / beta
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn test_lcg_rng_output_range() {
194 let mut rng = LcgRng::new(12345);
195 for _ in 0..100 {
196 let num = rng.gen_range(0.0..1.0);
197 assert!(num >= 0.0 && num < 1.0);
198 }
199 }
200
201 #[test]
202 fn test_lcg_rng_consistency() {
203 let mut rng1 = LcgRng::new(12345);
204 let mut rng2 = LcgRng::new(12345);
205 for _ in 0..100 {
206 assert_eq!(rng1.gen_range(0.0..1.0), rng2.gen_range(0.0..1.0));
207 }
208 }
209
210 #[test]
211 fn test_generate_random_gamma_values() {
212 let alpha = 2.0;
213 let beta = 1.5;
214 let num_samples = 100;
215 let seed = 12345;
216 let gamma_values = generate_random_gamma_values(alpha, beta, num_samples, seed);
217 assert_eq!(gamma_values.len(), num_samples);
218 for &value in gamma_values.iter() {
219 assert!(value >= 0.0);
220 }
221 }
222
223 #[test]
224 fn test_estimate_gamma_parameters() {
225 let data = alloc::vec![1.53, 2.00, 2.75, 3.10, 4.93, 5.33];
226 let (alpha, beta) = estimate_gamma_parameters(&data);
227
228 let expected_alpha = 4.48;
229 let expected_beta = 0.73;
230
231 assert!(
232 (alpha - expected_alpha).abs() < 1e-2,
233 "Alpha {alpha:} estimation incorrect"
234 );
235 assert!(
236 (beta - expected_beta).abs() < 1e-2,
237 "Beta {beta:} estimation incorrect"
238 );
239 }
240
241 #[test]
242 fn test_generate_gamma_values() {
243 let alpha = 4.0;
244 let beta = 10.0;
245 let n = 100;
246 let seed = 500;
247 let values = generate_random_gamma_values(alpha, beta, n, seed);
248
249 let (alpha_hat, beta_hat) = estimate_gamma_parameters(&values);
250
251 assert!(
252 (alpha_hat - alpha).abs() / alpha < 1e-1,
253 "Alpha {alpha_hat:} does not match expected value"
254 );
255 assert!(
256 (beta_hat - beta).abs() / beta < 1e-1,
257 "Beta {beta_hat:} does not match expected value"
258 );
259 }
260
261 #[test]
262 fn test_estimate_offset() {
263 let alpha1 = 4.0;
264 let beta1 = 100.0;
265 let n = 100;
266 let seed = 500;
267 let mut values_sorted = generate_random_gamma_values(alpha1, beta1, n, seed);
268 values_sorted.sort_unstable_by(|a, b| a.partial_cmp(b).expect("Can't sort NaN, aborting"));
269 let offset = estimate_offset(&values_sorted, &values_sorted);
270
271 assert!(
272 offset.abs() < 1e-1,
273 "Mean offset {offset:} does not match expected value"
274 );
275 }
276
277 #[test]
278 fn test_estimate() {
279 let alpha = 4.0;
280 let beta = 100.0;
281 let n = 10000;
282 let seed = 10000;
283 let values = generate_random_gamma_values(alpha, beta, n, seed);
284 let offset = estimate(values, Some(seed));
285
286 assert!(
287 offset.abs() < 1e-1,
288 "Mean offset {offset:} does not match expected value"
289 );
290 }
291}