libspot_rs/
tail.rs

1//! Tail structure for GPD modeling
2//!
3//! This module implements the Tail structure that models the tail of a distribution
4//! using Generalized Pareto Distribution (GPD) parameters.
5
6use crate::error::SpotResult;
7
8use crate::estimator::{grimshaw_estimator, mom_estimator};
9use crate::math::{xexp, xlog, xpow};
10use crate::peaks::Peaks;
11
12/// Structure that embeds GPD parameters (GPD tail actually)
13#[derive(Debug, Clone)]
14pub struct Tail {
15    /// GPD gamma parameter
16    gamma: f64,
17    /// GPD sigma parameter
18    sigma: f64,
19    /// Underlying Peaks structure
20    peaks: Peaks,
21}
22
23impl Tail {
24    /// Initialize a new Tail structure with the given size
25    pub fn new(size: usize) -> SpotResult<Self> {
26        Ok(Self {
27            gamma: f64::NAN,
28            sigma: f64::NAN,
29            peaks: Peaks::new(size)?,
30        })
31    }
32
33    /// Add a new data point into the tail
34    pub fn push(&mut self, x: f64) {
35        self.peaks.push(x);
36    }
37
38    /// Fit the GPD parameters using the available estimators
39    /// Returns the log-likelihood of the best fit
40    pub fn fit(&mut self) -> f64 {
41        if self.peaks.size() == 0 {
42            return f64::NAN;
43        }
44
45        // Match C implementation exactly: try each estimator and pick best
46        let mut max_llhood = f64::NAN;
47        let mut tmp_gamma;
48        let mut tmp_sigma;
49
50        // Try MoM estimator first (index 0 in C)
51        let llhood = {
52            let (gamma, sigma, llhood) = mom_estimator(&self.peaks);
53            tmp_gamma = gamma;
54            tmp_sigma = sigma;
55            llhood
56        };
57
58        if max_llhood.is_nan() || llhood > max_llhood {
59            max_llhood = llhood;
60            self.gamma = tmp_gamma;
61            self.sigma = tmp_sigma;
62        }
63
64        // Try Grimshaw estimator (index 1 in C)
65        let llhood = {
66            let (gamma, sigma, llhood) = grimshaw_estimator(&self.peaks);
67            tmp_gamma = gamma;
68            tmp_sigma = sigma;
69            llhood
70        };
71
72        if max_llhood.is_nan() || llhood > max_llhood {
73            // Back to original logic
74            max_llhood = llhood;
75            self.gamma = tmp_gamma;
76            self.sigma = tmp_sigma;
77        }
78
79        max_llhood
80    }
81
82    /// Compute the probability P(X > z) = p given the tail threshold difference d = z - t
83    pub fn probability(&self, s: f64, d: f64) -> f64 {
84        if self.gamma.is_nan() || self.sigma.is_nan() || self.sigma <= 0.0 {
85            return f64::NAN;
86        }
87
88        // Use exact equality check like C implementation (no tolerance)
89        if self.gamma == 0.0 {
90            s * xexp(-d / self.sigma)
91        } else {
92            let r = d * (self.gamma / self.sigma);
93            s * xpow(1.0 + r, -1.0 / self.gamma)
94        }
95    }
96
97    /// Compute the extreme quantile for given probability q
98    /// s is the ratio Nt/n (an estimator of P(X>t) = 1-F(t))
99    /// q is the desired low probability
100    pub fn quantile(&self, s: f64, q: f64) -> f64 {
101        if self.gamma.is_nan() || self.sigma.is_nan() || self.sigma <= 0.0 {
102            return f64::NAN;
103        }
104
105        let r = q / s;
106        // Use exact equality check like C implementation (no tolerance)
107        if self.gamma == 0.0 {
108            -self.sigma * xlog(r)
109        } else {
110            (self.sigma / self.gamma) * (xpow(r, -self.gamma) - 1.0)
111        }
112    }
113
114    /// Get the current gamma parameter
115    pub fn gamma(&self) -> f64 {
116        self.gamma
117    }
118
119    /// Get the current sigma parameter
120    pub fn sigma(&self) -> f64 {
121        self.sigma
122    }
123
124    /// Get the current size of the tail data
125    pub fn size(&self) -> usize {
126        self.peaks.size()
127    }
128
129    /// Get access to the underlying peaks structure
130    pub fn peaks(&self) -> &Peaks {
131        &self.peaks
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::error::SpotError;
139
140    #[test]
141    fn test_tail_creation() {
142        let tail = Tail::new(10).unwrap();
143        assert_eq!(tail.size(), 0);
144        assert!(tail.gamma().is_nan());
145        assert!(tail.sigma().is_nan());
146    }
147
148    #[test]
149    fn test_tail_zero_size() {
150        let result = Tail::new(0);
151        assert!(result.is_err());
152        assert_eq!(result.unwrap_err(), SpotError::MemoryAllocationFailed);
153    }
154
155    #[test]
156    fn test_tail_push() {
157        let mut tail = Tail::new(5).unwrap();
158
159        tail.push(1.0);
160        assert_eq!(tail.size(), 1);
161
162        tail.push(2.0);
163        tail.push(3.0);
164        assert_eq!(tail.size(), 3);
165    }
166
167    #[test]
168    fn test_tail_fit_empty() {
169        let mut tail = Tail::new(5).unwrap();
170        let llhood = tail.fit();
171        assert!(llhood.is_nan());
172        assert!(tail.gamma().is_nan());
173        assert!(tail.sigma().is_nan());
174    }
175
176    #[test]
177    fn test_tail_fit_with_data() {
178        let mut tail = Tail::new(10).unwrap();
179
180        // Add some sample data
181        for value in [1.0, 1.5, 2.0, 2.5, 3.0, 1.2, 1.8, 2.2] {
182            tail.push(value);
183        }
184
185        let llhood = tail.fit();
186        assert!(!llhood.is_nan());
187        assert!(llhood.is_finite());
188
189        // Parameters should be fitted
190        assert!(!tail.gamma().is_nan());
191        assert!(!tail.sigma().is_nan());
192        assert!(tail.sigma() > 0.0); // Sigma should be positive
193    }
194
195    #[test]
196    fn test_tail_quantile_gamma_zero() {
197        let mut tail = Tail::new(10).unwrap();
198
199        // Manually set parameters for testing
200        tail.gamma = 0.0;
201        tail.sigma = 1.0;
202
203        let q = tail.quantile(0.1, 0.01);
204        assert!(!q.is_nan());
205        assert!(q > 0.0); // Should be positive for low probability
206    }
207
208    #[test]
209    fn test_tail_quantile_gamma_nonzero() {
210        let mut tail = Tail::new(10).unwrap();
211
212        // Manually set parameters for testing
213        tail.gamma = 0.1;
214        tail.sigma = 1.0;
215
216        let q = tail.quantile(0.1, 0.01);
217        assert!(!q.is_nan());
218        assert!(q.is_finite());
219    }
220
221    #[test]
222    fn test_tail_probability_gamma_zero() {
223        let mut tail = Tail::new(10).unwrap();
224
225        // Manually set parameters for testing
226        tail.gamma = 0.0;
227        tail.sigma = 1.0;
228
229        let p = tail.probability(0.1, 2.0);
230        assert!(!p.is_nan());
231        assert!((0.0..=0.1).contains(&p));
232    }
233
234    #[test]
235    fn test_tail_probability_gamma_nonzero() {
236        let mut tail = Tail::new(10).unwrap();
237
238        // Manually set parameters for testing
239        tail.gamma = 0.1;
240        tail.sigma = 1.0;
241
242        let p = tail.probability(0.1, 2.0);
243        assert!(!p.is_nan());
244        assert!(p >= 0.0);
245    }
246
247    #[test]
248    fn test_tail_invalid_parameters() {
249        let mut tail = Tail::new(10).unwrap();
250
251        // Test with invalid sigma
252        tail.gamma = 0.1;
253        tail.sigma = 0.0;
254
255        let q = tail.quantile(0.1, 0.01);
256        assert!(q.is_nan());
257
258        let p = tail.probability(0.1, 2.0);
259        assert!(p.is_nan());
260    }
261
262    #[test]
263    fn test_tail_consistency() {
264        let mut tail = Tail::new(10).unwrap();
265
266        // Add some data and fit
267        for value in [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0] {
268            tail.push(value);
269        }
270
271        let _llhood = tail.fit();
272
273        // Test that quantile and probability are somewhat consistent
274        let s = 0.1;
275        let q = 0.01;
276        let quantile_val = tail.quantile(s, q);
277
278        if !quantile_val.is_nan() && quantile_val.is_finite() {
279            let prob_val = tail.probability(s, quantile_val);
280            if !prob_val.is_nan() && prob_val.is_finite() {
281                // The probability should be approximately q
282                // Allow for some numerical error
283                assert!((prob_val - q).abs() < q * 0.1 || prob_val < q * 2.0);
284            }
285        }
286    }
287}