Skip to content

Commit f438698

Browse files
Add MOM estimator options (#30)
* Add MOM estimator options * Fix repeated SPOT fit tail reset
1 parent a0e2f02 commit f438698

7 files changed

Lines changed: 434 additions & 13 deletions

File tree

crates/libspot-rs/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,32 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
3636

3737
## Features
3838

39+
### Estimator and threshold options
40+
41+
`SpotDetector::new(config)` keeps the default behavior: it uses `SpotEstimator::Best`, the P2 initial threshold, and the historical `>=` excess update rule.
42+
43+
To reproduce algorithms that explicitly use FluxEV-style MOM-SPOT, configure all options explicitly:
44+
45+
```rust,ignore
46+
use libspot_rs::{
47+
SpotConfig, SpotDetector, SpotEstimator, SpotExcessUpdate, SpotInitialThreshold,
48+
};
49+
50+
let config = SpotConfig {
51+
q: 0.001,
52+
level: 0.98,
53+
max_excess: 10_000,
54+
..SpotConfig::default()
55+
};
56+
57+
let mut detector = SpotDetector::new_with_full_options(
58+
config,
59+
SpotEstimator::Mom,
60+
SpotInitialThreshold::Empirical,
61+
SpotExcessUpdate::Greater,
62+
)?;
63+
```
64+
3965
### Serialization (Model Persistence)
4066

4167
Serialization support is **enabled by default**. SPOT detectors can be serialized and deserialized for model deployment:

crates/libspot-rs/src/config.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
//! Configuration types for SPOT detector
22
3+
/// GPD parameter estimator used by SPOT.
4+
///
5+
/// The default keeps the historical libspot-rs behavior: try the supported
6+
/// estimators and keep the fit with the best log-likelihood.
7+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9+
pub enum SpotEstimator {
10+
/// Try the available estimators and keep the best log-likelihood.
11+
#[default]
12+
Best,
13+
/// Force Method of Moments estimation.
14+
Mom,
15+
}
16+
17+
/// Initial excess-threshold selection strategy.
18+
///
19+
/// The default keeps the historical libspot-rs behavior.
20+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22+
pub enum SpotInitialThreshold {
23+
/// Use the P2 streaming quantile estimator.
24+
#[default]
25+
P2,
26+
/// Use the empirical sorted quantile from the initial batch.
27+
Empirical,
28+
}
29+
30+
/// Streaming excess update condition.
31+
///
32+
/// The default keeps the historical libspot-rs behavior.
33+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35+
pub enum SpotExcessUpdate {
36+
/// Update the tail when `value >= excess_threshold`.
37+
#[default]
38+
GreaterOrEqual,
39+
/// Update the tail only when `value > excess_threshold`.
40+
Greater,
41+
}
42+
343
/// Configuration parameters for SPOT detector
444
///
545
/// # Serialization
@@ -72,4 +112,22 @@ mod tests {
72112
assert_relative_eq!(config1.level, config2.level);
73113
assert_eq!(config1.max_excess, config2.max_excess);
74114
}
115+
116+
#[test]
117+
fn test_spot_estimator_default_keeps_existing_behavior() {
118+
assert_eq!(SpotEstimator::default(), SpotEstimator::Best);
119+
}
120+
121+
#[test]
122+
fn test_spot_initial_threshold_default_keeps_existing_behavior() {
123+
assert_eq!(SpotInitialThreshold::default(), SpotInitialThreshold::P2);
124+
}
125+
126+
#[test]
127+
fn test_spot_excess_update_default_keeps_existing_behavior() {
128+
assert_eq!(
129+
SpotExcessUpdate::default(),
130+
SpotExcessUpdate::GreaterOrEqual
131+
);
132+
}
75133
}

crates/libspot-rs/src/estimator.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@ pub fn mom_estimator(peaks: &Peaks) -> (f64, f64, f64) {
3030
(gamma, sigma, log_likelihood)
3131
}
3232

33+
/// Method of Moments estimator using sample variance.
34+
///
35+
/// This matches the MOM estimator used by FluxEV's SPOT integration.
36+
pub fn mom_sample_variance_estimator(peaks: &Peaks) -> (f64, f64, f64) {
37+
let nt_local = peaks.size();
38+
if nt_local < 2 {
39+
return (f64::NAN, f64::NAN, f64::NAN);
40+
}
41+
42+
let e = peaks.mean();
43+
let mut sum_sq_delta = 0.0;
44+
for &value in peaks.container().raw_data().iter().take(nt_local) {
45+
let delta = value - e;
46+
sum_sq_delta += delta * delta;
47+
}
48+
let v = sum_sq_delta / (nt_local as f64 - 1.0);
49+
50+
if e.is_nan() || v.is_nan() || v <= 0.0 {
51+
return (f64::NAN, f64::NAN, f64::NAN);
52+
}
53+
54+
let r = e * e / v;
55+
let gamma = 0.5 * (1.0 - r);
56+
let sigma = 0.5 * e * (1.0 + r);
57+
58+
(gamma, sigma, 0.0)
59+
}
60+
3361
/// Grimshaw estimator for GPD parameters
3462
pub fn grimshaw_estimator(peaks: &Peaks) -> (f64, f64, f64) {
3563
let mini = peaks.min();
@@ -299,6 +327,21 @@ mod tests {
299327
assert!(sigma > 0.0); // Sigma should be positive
300328
}
301329

330+
#[test]
331+
fn test_mom_sample_variance_estimator_normal_case() {
332+
let mut peaks = Peaks::new(10).unwrap();
333+
for value in [1.0, 2.0, 3.0, 4.0] {
334+
peaks.push(value);
335+
}
336+
337+
let (gamma, sigma, llhood) = mom_sample_variance_estimator(&peaks);
338+
339+
assert!(gamma.is_finite());
340+
assert!(sigma.is_finite());
341+
assert!(sigma > 0.0);
342+
assert_relative_eq!(llhood, 0.0);
343+
}
344+
302345
#[test]
303346
fn test_log_likelihood_gamma_zero() {
304347
let mut peaks = Peaks::new(10).unwrap();

crates/libspot-rs/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ mod tail;
5959
mod ubend;
6060

6161
// Re-export public types
62-
pub use config::SpotConfig;
62+
pub use config::{SpotConfig, SpotEstimator, SpotExcessUpdate, SpotInitialThreshold};
6363
pub use error::{SpotError, SpotResult};
6464
pub use peaks::Peaks;
6565
pub use spot::SpotDetector;

0 commit comments

Comments
 (0)