1use crate::error::SpotResult;
7
8use crate::estimator::{grimshaw_estimator, mom_estimator};
9use crate::math::{xexp, xlog, xpow};
10use crate::peaks::Peaks;
11
12#[derive(Debug, Clone)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20pub struct Tail {
21 #[cfg_attr(feature = "serde", serde(with = "crate::ser::nan_safe_f64"))]
23 gamma: f64,
24 #[cfg_attr(feature = "serde", serde(with = "crate::ser::nan_safe_f64"))]
26 sigma: f64,
27 peaks: Peaks,
29}
30
31impl Tail {
32 pub fn new(size: usize) -> SpotResult<Self> {
34 Ok(Self {
35 gamma: f64::NAN,
36 sigma: f64::NAN,
37 peaks: Peaks::new(size)?,
38 })
39 }
40
41 pub fn push(&mut self, x: f64) {
43 self.peaks.push(x);
44 }
45
46 pub(crate) fn reset(&mut self) {
48 self.gamma = f64::NAN;
49 self.sigma = f64::NAN;
50 self.peaks.reset();
51 }
52
53 pub fn fit(&mut self) -> f64 {
56 if self.peaks.size() == 0 {
57 return f64::NAN;
58 }
59
60 let mut max_llhood = f64::NAN;
62 let mut tmp_gamma;
63 let mut tmp_sigma;
64
65 let llhood = {
67 let (gamma, sigma, llhood) = mom_estimator(&self.peaks);
68 tmp_gamma = gamma;
69 tmp_sigma = sigma;
70 llhood
71 };
72
73 if max_llhood.is_nan() || llhood > max_llhood {
74 max_llhood = llhood;
75 self.gamma = tmp_gamma;
76 self.sigma = tmp_sigma;
77 }
78
79 let llhood = {
81 let (gamma, sigma, llhood) = grimshaw_estimator(&self.peaks);
82 tmp_gamma = gamma;
83 tmp_sigma = sigma;
84 llhood
85 };
86
87 if max_llhood.is_nan() || llhood > max_llhood {
88 max_llhood = llhood;
90 self.gamma = tmp_gamma;
91 self.sigma = tmp_sigma;
92 }
93
94 max_llhood
95 }
96
97 pub fn probability(&self, s: f64, d: f64) -> f64 {
99 if self.gamma.is_nan() || self.sigma.is_nan() || self.sigma <= 0.0 {
100 return f64::NAN;
101 }
102
103 if self.gamma == 0.0 {
105 s * xexp(-d / self.sigma)
106 } else {
107 let r = d * (self.gamma / self.sigma);
108 s * xpow(1.0 + r, -1.0 / self.gamma)
109 }
110 }
111
112 pub fn quantile(&self, s: f64, q: f64) -> f64 {
116 if self.gamma.is_nan() || self.sigma.is_nan() || self.sigma <= 0.0 {
117 return f64::NAN;
118 }
119
120 let r = q / s;
121 if self.gamma == 0.0 {
123 -self.sigma * xlog(r)
124 } else {
125 (self.sigma / self.gamma) * (xpow(r, -self.gamma) - 1.0)
126 }
127 }
128
129 pub fn gamma(&self) -> f64 {
131 self.gamma
132 }
133
134 pub fn sigma(&self) -> f64 {
136 self.sigma
137 }
138
139 pub fn size(&self) -> usize {
141 self.peaks.size()
142 }
143
144 pub fn peaks(&self) -> &Peaks {
146 &self.peaks
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::error::SpotError;
154
155 #[test]
156 fn test_tail_reset_clears_gpd_params_and_peaks() {
157 let mut tail = Tail::new(50).unwrap();
158 for i in 0..40 {
159 tail.push(0.1 + i as f64 * 0.05);
160 }
161 let _ = tail.fit();
162 assert!(tail.size() > 0);
163 tail.reset();
167
168 assert_eq!(tail.size(), 0);
169 assert!(tail.gamma().is_nan());
170 assert!(tail.sigma().is_nan());
171 }
172
173 #[test]
174 fn test_tail_creation() {
175 let tail = Tail::new(10).unwrap();
176 assert_eq!(tail.size(), 0);
177 assert!(tail.gamma().is_nan());
178 assert!(tail.sigma().is_nan());
179 }
180
181 #[test]
182 fn test_tail_zero_size() {
183 let result = Tail::new(0);
184 assert!(result.is_err());
185 assert_eq!(result.unwrap_err(), SpotError::MemoryAllocationFailed);
186 }
187
188 #[test]
189 fn test_tail_push() {
190 let mut tail = Tail::new(5).unwrap();
191
192 tail.push(1.0);
193 assert_eq!(tail.size(), 1);
194
195 tail.push(2.0);
196 tail.push(3.0);
197 assert_eq!(tail.size(), 3);
198 }
199
200 #[test]
201 fn test_tail_fit_empty() {
202 let mut tail = Tail::new(5).unwrap();
203 let llhood = tail.fit();
204 assert!(llhood.is_nan());
205 assert!(tail.gamma().is_nan());
206 assert!(tail.sigma().is_nan());
207 }
208
209 #[test]
210 fn test_tail_fit_with_data() {
211 let mut tail = Tail::new(10).unwrap();
212
213 for value in [1.0, 1.5, 2.0, 2.5, 3.0, 1.2, 1.8, 2.2] {
215 tail.push(value);
216 }
217
218 let llhood = tail.fit();
219 assert!(!llhood.is_nan());
220 assert!(llhood.is_finite());
221
222 assert!(!tail.gamma().is_nan());
224 assert!(!tail.sigma().is_nan());
225 assert!(tail.sigma() > 0.0); }
227
228 #[test]
229 fn test_tail_quantile_gamma_zero() {
230 let mut tail = Tail::new(10).unwrap();
231
232 tail.gamma = 0.0;
234 tail.sigma = 1.0;
235
236 let q = tail.quantile(0.1, 0.01);
237 assert!(!q.is_nan());
238 assert!(q > 0.0); }
240
241 #[test]
242 fn test_tail_quantile_gamma_nonzero() {
243 let mut tail = Tail::new(10).unwrap();
244
245 tail.gamma = 0.1;
247 tail.sigma = 1.0;
248
249 let q = tail.quantile(0.1, 0.01);
250 assert!(!q.is_nan());
251 assert!(q.is_finite());
252 }
253
254 #[test]
255 fn test_tail_probability_gamma_zero() {
256 let mut tail = Tail::new(10).unwrap();
257
258 tail.gamma = 0.0;
260 tail.sigma = 1.0;
261
262 let p = tail.probability(0.1, 2.0);
263 assert!(!p.is_nan());
264 assert!((0.0..=0.1).contains(&p));
265 }
266
267 #[test]
268 fn test_tail_probability_gamma_nonzero() {
269 let mut tail = Tail::new(10).unwrap();
270
271 tail.gamma = 0.1;
273 tail.sigma = 1.0;
274
275 let p = tail.probability(0.1, 2.0);
276 assert!(!p.is_nan());
277 assert!(p >= 0.0);
278 }
279
280 #[test]
281 fn test_tail_invalid_parameters() {
282 let mut tail = Tail::new(10).unwrap();
283
284 tail.gamma = 0.1;
286 tail.sigma = 0.0;
287
288 let q = tail.quantile(0.1, 0.01);
289 assert!(q.is_nan());
290
291 let p = tail.probability(0.1, 2.0);
292 assert!(p.is_nan());
293 }
294
295 #[test]
296 fn test_tail_consistency() {
297 let mut tail = Tail::new(10).unwrap();
298
299 for value in [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0] {
301 tail.push(value);
302 }
303
304 let _llhood = tail.fit();
305
306 let s = 0.1;
308 let q = 0.01;
309 let quantile_val = tail.quantile(s, q);
310
311 if !quantile_val.is_nan() && quantile_val.is_finite() {
312 let prob_val = tail.probability(s, quantile_val);
313 if !prob_val.is_nan() && prob_val.is_finite() {
314 assert!((prob_val - q).abs() < q * 0.1 || prob_val < q * 2.0);
317 }
318 }
319 }
320}