///|
/// Detects a persistent step change by comparing a reference and a current block.
pub struct StepChangeDetector {
reference : DoubleWindow
current : DoubleWindow
threshold : Double
mut index : Int
}
///|
pub fn StepChangeDetector::new(
window_size? : Int = 16,
threshold? : Double = 3.0,
) -> StepChangeDetector {
let size = if window_size < 2 { 2 } else { window_size }
{
reference: DoubleWindow::new(size),
current: DoubleWindow::new(size),
threshold: if threshold <= 0.0 {
3.0
} else {
threshold
},
index: 0,
}
}
///|
pub fn StepChangeDetector::update(
self : StepChangeDetector,
value : Double,
) -> DetectionResult {
self.index += 1
if !is_finite(value) {
return DetectionResult::quiet(index=self.index)
}
if self.current.is_full() {
self.reference.clear()
for item in self.current.to_array() {
ignore(self.reference.push(item))
}
self.current.clear()
}
ignore(self.current.push(value))
if !self.reference.is_full() || !self.current.is_full() {
return DetectionResult::quiet(index=self.index)
}
let score = mean_shift_score(
self.reference.to_array() + self.current.to_array(),
self.reference.length(),
min_segment=self.reference.length() / 2,
)
let direction = direction_for_delta(
self.current.mean() - self.reference.mean(),
)
DetectionResult::new(
score >= self.threshold,
score / self.threshold,
clamp_probability(score / (score + 1.0)),
direction,
self.index,
evidence=score,
)
}
///|
pub struct MeanVarianceDetector {
baseline : DoubleWindow
current : DoubleWindow
mean_threshold : Double
variance_threshold : Double
mut index : Int
}
///|
pub fn MeanVarianceDetector::new(
window_size? : Int = 16,
mean_threshold? : Double = 2.0,
variance_threshold? : Double = 2.0,
) -> MeanVarianceDetector {
let size = if window_size < 2 { 2 } else { window_size }
{
baseline: DoubleWindow::new(size),
current: DoubleWindow::new(size),
mean_threshold: if mean_threshold <= 0.0 {
2.0
} else {
mean_threshold
},
variance_threshold: if variance_threshold <= 1.0 {
2.0
} else {
variance_threshold
},
index: 0,
}
}
///|
pub fn MeanVarianceDetector::update(
self : MeanVarianceDetector,
value : Double,
) -> DetectionResult {
self.index += 1
if !is_finite(value) {
return DetectionResult::quiet(index=self.index)
}
if self.current.is_full() {
self.baseline.clear()
for item in self.current.to_array() {
ignore(self.baseline.push(item))
}
self.current.clear()
}
ignore(self.current.push(value))
if !self.baseline.is_full() || !self.current.is_full() {
return DetectionResult::quiet(index=self.index)
}
let mean_scale = self.baseline.standard_deviation() + 1.0e-12
let mean_score = absolute(self.current.mean() - self.baseline.mean()) /
mean_scale
let base_variance = self.baseline.variance() + 1.0e-12
let ratio = self.current.variance() / base_variance
let variance_score = if ratio >= 1.0 {
ratio / self.variance_threshold
} else {
1.0 / ratio / self.variance_threshold
}
let score = if mean_score > variance_score {
mean_score / self.mean_threshold
} else {
variance_score
}
let direction = if mean_score > variance_score {
direction_for_delta(self.current.mean() - self.baseline.mean())
} else if ratio >= 1.0 {
VarianceIncrease
} else {
VarianceDecrease
}
DetectionResult::new(
score >= 1.0,
score,
clamp_probability(score / (score + 1.0)),
direction,
self.index,
evidence=ratio,
)
}
///|
pub struct DualSidedDetector {
positive : Cusum
negative : Cusum
mut index : Int
}
///|
pub fn DualSidedDetector::new(
target? : Double = 0.0,
limit? : Double = 5.0,
drift? : Double = 0.5,
) -> DualSidedDetector {
{
positive: Cusum::new(target_mean=target, control_limit=limit, drift~),
negative: Cusum::new(target_mean=target, control_limit=limit, drift~),
index: 0,
}
}
///|
pub fn DualSidedDetector::update(
self : DualSidedDetector,
value : Double,
) -> DetectionResult {
self.index += 1
let positive = self.positive.update_result(value, index=self.index)
let negative = self.negative.update_result(-value, index=self.index)
if positive.score > negative.score {
positive
} else {
{
changed: negative.changed,
score: negative.score,
confidence: negative.confidence,
direction: if negative.changed {
Decrease
} else {
Unknown
},
index: self.index,
evidence: negative.evidence,
}
}
}
///|
pub struct PersistenceDetector {
detector : EwmaDetector
rule : ConsecutiveRule
mut index : Int
}
///|
pub fn PersistenceDetector::new(
warmup? : Int = 8,
persistence? : Int = 3,
) -> PersistenceDetector {
{
detector: EwmaDetector::new(warmup~),
rule: ConsecutiveRule::new(required=persistence),
index: 0,
}
}
///|
pub fn PersistenceDetector::update(
self : PersistenceDetector,
value : Double,
) -> DetectionResult {
self.index += 1
let result = self.detector.update(value)
let changed = self.rule.push(result.changed)
{
changed,
score: result.score,
confidence: result.confidence,
direction: result.direction,
index: self.index,
evidence: result.evidence,
}
}