gamlr/
offset_estimator.rs

1use alloc::vec::Vec;
2
3const MAX_ALPHA: f64 = 4.0;
4const MIN_ALPHA: f64 = 1.0;
5/// Predefined constants from "The Art of Computer Programming, Volume 2, Section 3.2.1" by Donald E. Knuth.
6const A: u64 = 6364136223846793005;
7const C: u64 = 1442695040888963407;
8/// A simple Linear Congruential Generator (LCG) for generating pseudorandom numbers.
9///
10/// # Parameters
11///
12/// * `state: u64` - The current internal state of the generator.
13/// * `a: u64` - The multiplier constant.
14/// * `c: u64` - The increment constant.
15/// * `m: u64` - The modulus constant.
16///
17/// # References
18///
19/// * D. H. Lehmer. "Mathematical methods in large-scale computing units".
20///   Proceedings of a Second Symposium on Large Scale Digital Calculating Machinery;
21///   Annals of the Computation Laboratory, Harvard Univ. 26 (1951): 141-146.
22pub 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    /// Generates a random value drawn from a uniform distribution over the provided range.
40    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    /// Generates a random value drawn from a standard normal distribution using the Marsaglia polar method.
47    ///
48    /// This function implements the Marsaglia polar method, an algorithm for generating
49    /// independent, standard normally distributed (Gaussian) random numbers.
50    /// References:
51    /// George Marsaglia. "Generating a Variable from the Tail of the Normal Distribution".
52    /// Technometrics, Vol. 6, No. 3 (Aug., 1964), pp. 101-102.
53    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
71/// Estimates the alpha and beta parameters for the Gamma distribution based on the sample data provided,
72/// using the median instead of the mean.
73fn 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
85/// Sorts the input values in ascending order and returns the sorted vector.
86fn 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
96/// Generates random values drawn from a Gamma distribution using the method described in:
97///
98/// George Marsaglia, Wai Wan Tsang. "A Simple Method for Generating Gamma Variables".
99/// ACM Transactions on Mathematical Software, Vol. 26, No. 3, September 2000, Pages 363-372.
100fn 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
129/// Estimates the offset between two networked devices based on one-way delay time (OWD) measurements
130/// using the method described in:
131///
132/// Edmar Mota-Garcia and Rogelio Hasimoto-Beltran: "A new model-based clock-offset approximation over IP networks"
133/// Computer Communications, Volume 53, 2014, Pages 26-36, ISSN 0140-3664, https://doi.org/10.1016/j.comcom.2014.07.006.
134pub 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
150/// Calculates the offset between the generated gamma values and the sorted time values.
151///
152/// Edmar Mota-Garcia and Rogelio Hasimoto-Beltran: "A new model-based clock-offset approximation over IP networks"
153/// Computer Communications, Volume 53, 2014, Pages 26-36, ISSN 0140-3664, https://doi.org/10.1016/j.comcom.2014.07.006.
154pub 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    // Perform linear regression to estimate the slope (beta) and intercept (gamma)
170    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    // Return the point where the regression line crosses the x-axis (y = 0)
185    -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}