#[derive(Debug, Clone)]
pub struct ShiryaevRobertsF64 {
pre_mean: f64,
post_mean: f64,
variance: f64,
threshold: f64,
r: f64,
count: u64,
min_samples: u64,
}
#[derive(Debug, Clone)]
pub struct ShiryaevRobertsF64Builder {
pre_mean: Option<f64>,
post_mean: Option<f64>,
variance: Option<f64>,
threshold: Option<f64>,
min_samples: u64,
}
impl ShiryaevRobertsF64 {
#[inline]
#[must_use]
pub fn builder() -> ShiryaevRobertsF64Builder {
ShiryaevRobertsF64Builder {
pre_mean: None,
post_mean: None,
variance: None,
threshold: None,
min_samples: 0,
}
}
#[inline]
#[must_use]
pub fn update(&mut self, sample: f64) -> Option<bool> {
self.count += 1;
let delta_mean = self.post_mean - self.pre_mean;
let midpoint = f64::midpoint(self.pre_mean, self.post_mean);
let log_lr = delta_mean * (sample - midpoint) / self.variance;
self.r = (1.0 + self.r) * crate::math::exp(log_lr);
if self.count < self.min_samples {
return None;
}
Some(self.r > self.threshold)
}
#[inline]
#[must_use]
pub fn statistic(&self) -> f64 {
self.r
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.count
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.count >= self.min_samples
}
#[inline]
pub fn reset(&mut self) {
self.r = 0.0;
self.count = 0;
}
}
impl ShiryaevRobertsF64Builder {
#[inline]
#[must_use]
pub fn pre_change_mean(mut self, mean: f64) -> Self {
self.pre_mean = Some(mean);
self
}
#[inline]
#[must_use]
pub fn post_change_mean(mut self, mean: f64) -> Self {
self.post_mean = Some(mean);
self
}
#[inline]
#[must_use]
pub fn variance(mut self, variance: f64) -> Self {
self.variance = Some(variance);
self
}
#[inline]
#[must_use]
pub fn threshold(mut self, threshold: f64) -> Self {
self.threshold = Some(threshold);
self
}
#[inline]
#[must_use]
pub fn min_samples(mut self, min: u64) -> Self {
self.min_samples = min;
self
}
#[inline]
pub fn build(self) -> Result<ShiryaevRobertsF64, crate::ConfigError> {
let pre_mean = self
.pre_mean
.ok_or(crate::ConfigError::Missing("pre_change_mean"))?;
let post_mean = self
.post_mean
.ok_or(crate::ConfigError::Missing("post_change_mean"))?;
let variance = self
.variance
.ok_or(crate::ConfigError::Missing("variance"))?;
let threshold = self
.threshold
.ok_or(crate::ConfigError::Missing("threshold"))?;
if variance <= 0.0 {
return Err(crate::ConfigError::Invalid("variance must be positive"));
}
if threshold <= 0.0 {
return Err(crate::ConfigError::Invalid("threshold must be positive"));
}
if (post_mean - pre_mean).abs() <= f64::EPSILON {
return Err(crate::ConfigError::Invalid(
"pre and post change means must differ",
));
}
Ok(ShiryaevRobertsF64 {
pre_mean,
post_mean,
variance,
threshold,
r: 0.0,
count: 0,
min_samples: self.min_samples,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_detection_at_pre_change_mean() {
let mut sr = ShiryaevRobertsF64::builder()
.pre_change_mean(100.0)
.post_change_mean(110.0)
.variance(25.0)
.threshold(100.0)
.build()
.unwrap();
for _ in 0..100 {
let result = sr.update(100.0);
assert_eq!(result, Some(false), "should not detect at pre-change mean");
}
}
#[test]
fn detects_shift_to_post_change_mean() {
let mut sr = ShiryaevRobertsF64::builder()
.pre_change_mean(100.0)
.post_change_mean(110.0)
.variance(25.0)
.threshold(100.0)
.build()
.unwrap();
let mut detected = false;
for _ in 0..100 {
if sr.update(110.0) == Some(true) {
detected = true;
break;
}
}
assert!(detected, "should detect shift to post-change mean");
}
#[test]
fn statistic_grows_under_alternative() {
let mut sr = ShiryaevRobertsF64::builder()
.pre_change_mean(0.0)
.post_change_mean(5.0)
.variance(1.0)
.threshold(1000.0)
.build()
.unwrap();
let _ = sr.update(5.0);
let r1 = sr.statistic();
let _ = sr.update(5.0);
let r2 = sr.statistic();
assert!(r2 > r1, "R should grow under alternative hypothesis");
}
#[test]
fn reset_clears_statistic() {
let mut sr = ShiryaevRobertsF64::builder()
.pre_change_mean(0.0)
.post_change_mean(5.0)
.variance(1.0)
.threshold(100.0)
.build()
.unwrap();
let _ = sr.update(5.0);
assert!(sr.statistic() > 0.0);
sr.reset();
#[allow(clippy::float_cmp)]
{
assert_eq!(sr.statistic(), 0.0);
}
assert_eq!(sr.count(), 0);
}
#[test]
fn priming() {
let mut sr = ShiryaevRobertsF64::builder()
.pre_change_mean(0.0)
.post_change_mean(5.0)
.variance(1.0)
.threshold(100.0)
.min_samples(5)
.build()
.unwrap();
for _ in 0..4 {
assert_eq!(sr.update(5.0), None);
}
assert!(sr.update(5.0).is_some());
}
#[test]
fn errors_without_variance() {
let result = ShiryaevRobertsF64::builder()
.pre_change_mean(0.0)
.post_change_mean(5.0)
.threshold(100.0)
.build();
assert!(matches!(
result,
Err(crate::ConfigError::Missing("variance"))
));
}
#[test]
fn errors_on_equal_means() {
let result = ShiryaevRobertsF64::builder()
.pre_change_mean(5.0)
.post_change_mean(5.0)
.variance(1.0)
.threshold(100.0)
.build();
assert!(matches!(result, Err(crate::ConfigError::Invalid(_))));
}
}