use serde::Serialize;
#[derive(Debug, Clone)]
pub enum Pattern {
Spike {
threshold: f64,
max_duration_micros: Option<u64>,
},
Dip {
threshold: f64,
max_duration_micros: Option<u64>,
},
Rising { min_points: usize },
Falling { min_points: usize },
Plateau { tolerance: f64, min_points: usize },
}
#[derive(Debug, Clone, Serialize)]
pub struct PatternMatch {
pub start_index: usize,
pub end_index: usize,
pub start_timestamp: u64,
pub end_timestamp: u64,
pub confidence: f64,
}
pub fn find_pattern(values: &[(u64, f64)], pattern: &Pattern) -> Vec<PatternMatch> {
match pattern {
Pattern::Spike {
threshold,
max_duration_micros,
} => find_spikes(values, *threshold, *max_duration_micros),
Pattern::Dip {
threshold,
max_duration_micros,
} => find_dips(values, *threshold, *max_duration_micros),
Pattern::Rising { min_points } => find_rising(values, *min_points),
Pattern::Falling { min_points } => find_falling(values, *min_points),
Pattern::Plateau {
tolerance,
min_points,
} => find_plateaus(values, *tolerance, *min_points),
}
}
fn find_spikes(values: &[(u64, f64)], threshold: f64, max_dur: Option<u64>) -> Vec<PatternMatch> {
let mut matches = Vec::new();
if values.len() < 3 {
return matches;
}
for i in 1..values.len() - 1 {
let prev = values[i - 1].1;
let curr = values[i].1;
let next = values[i + 1].1;
let rise = curr - prev;
let fall = curr - next;
if rise >= threshold && fall >= threshold {
let duration = values[i + 1].0.saturating_sub(values[i - 1].0);
if max_dur.map_or(true, |max| duration <= max) {
let amplitude = rise.min(fall);
let confidence = (amplitude / threshold).min(1.0);
matches.push(PatternMatch {
start_index: i - 1,
end_index: i + 1,
start_timestamp: values[i - 1].0,
end_timestamp: values[i + 1].0,
confidence,
});
}
}
}
matches
}
fn find_dips(values: &[(u64, f64)], threshold: f64, max_dur: Option<u64>) -> Vec<PatternMatch> {
let mut matches = Vec::new();
if values.len() < 3 {
return matches;
}
for i in 1..values.len() - 1 {
let prev = values[i - 1].1;
let curr = values[i].1;
let next = values[i + 1].1;
let drop = prev - curr;
let recovery = next - curr;
if drop >= threshold && recovery >= threshold {
let duration = values[i + 1].0.saturating_sub(values[i - 1].0);
if max_dur.map_or(true, |max| duration <= max) {
let amplitude = drop.min(recovery);
let confidence = (amplitude / threshold).min(1.0);
matches.push(PatternMatch {
start_index: i - 1,
end_index: i + 1,
start_timestamp: values[i - 1].0,
end_timestamp: values[i + 1].0,
confidence,
});
}
}
}
matches
}
fn find_rising(values: &[(u64, f64)], min_points: usize) -> Vec<PatternMatch> {
find_monotone(values, min_points, true)
}
fn find_falling(values: &[(u64, f64)], min_points: usize) -> Vec<PatternMatch> {
find_monotone(values, min_points, false)
}
fn find_monotone(values: &[(u64, f64)], min_points: usize, rising: bool) -> Vec<PatternMatch> {
let mut matches = Vec::new();
if values.len() < min_points || min_points < 2 {
return matches;
}
let mut start = 0;
for i in 1..values.len() {
let is_monotone = if rising {
values[i].1 > values[i - 1].1
} else {
values[i].1 < values[i - 1].1
};
if !is_monotone {
let len = i - start;
if len >= min_points {
matches.push(PatternMatch {
start_index: start,
end_index: i - 1,
start_timestamp: values[start].0,
end_timestamp: values[i - 1].0,
confidence: (len as f64 / min_points as f64).min(1.0),
});
}
start = i;
}
}
let len = values.len() - start;
if len >= min_points {
matches.push(PatternMatch {
start_index: start,
end_index: values.len() - 1,
start_timestamp: values[start].0,
end_timestamp: values[values.len() - 1].0,
confidence: (len as f64 / min_points as f64).min(1.0),
});
}
matches
}
fn find_plateaus(values: &[(u64, f64)], tolerance: f64, min_points: usize) -> Vec<PatternMatch> {
let mut matches = Vec::new();
if values.len() < min_points || min_points < 2 {
return matches;
}
let mut start = 0;
let mut base_value = values[0].1;
for i in 1..values.len() {
if (values[i].1 - base_value).abs() > tolerance {
let len = i - start;
if len >= min_points {
matches.push(PatternMatch {
start_index: start,
end_index: i - 1,
start_timestamp: values[start].0,
end_timestamp: values[i - 1].0,
confidence: (len as f64 / min_points as f64).min(1.0),
});
}
start = i;
base_value = values[i].1;
}
}
let len = values.len() - start;
if len >= min_points {
matches.push(PatternMatch {
start_index: start,
end_index: values.len() - 1,
start_timestamp: values[start].0,
end_timestamp: values[values.len() - 1].0,
confidence: (len as f64 / min_points as f64).min(1.0),
});
}
matches
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_spike() {
let values = vec![(0, 10.0), (1, 10.0), (2, 50.0), (3, 10.0), (4, 10.0)];
let matches = find_pattern(
&values,
&Pattern::Spike {
threshold: 30.0,
max_duration_micros: None,
},
);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].start_index, 1);
assert_eq!(matches[0].end_index, 3);
}
#[test]
fn test_find_dip() {
let values = vec![(0, 50.0), (1, 50.0), (2, 10.0), (3, 50.0), (4, 50.0)];
let matches = find_pattern(
&values,
&Pattern::Dip {
threshold: 30.0,
max_duration_micros: None,
},
);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].start_index, 1);
}
#[test]
fn test_find_rising() {
let values = vec![(0, 1.0), (1, 2.0), (2, 3.0), (3, 4.0), (4, 3.0), (5, 2.0)];
let matches = find_pattern(&values, &Pattern::Rising { min_points: 3 });
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].start_index, 0);
assert_eq!(matches[0].end_index, 3);
}
#[test]
fn test_find_falling() {
let values = vec![(0, 10.0), (1, 8.0), (2, 6.0), (3, 4.0), (4, 5.0)];
let matches = find_pattern(&values, &Pattern::Falling { min_points: 3 });
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].end_index, 3);
}
#[test]
fn test_find_plateau() {
let values = vec![
(0, 10.0),
(1, 10.1),
(2, 9.9),
(3, 10.0),
(4, 10.05),
(5, 50.0),
(6, 50.1),
];
let matches = find_pattern(
&values,
&Pattern::Plateau {
tolerance: 0.5,
min_points: 3,
},
);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].start_index, 0);
assert_eq!(matches[0].end_index, 4);
}
#[test]
fn test_no_pattern_in_short_series() {
let values = vec![(0, 1.0), (1, 2.0)];
assert!(find_pattern(
&values,
&Pattern::Spike {
threshold: 1.0,
max_duration_micros: None
}
)
.is_empty());
assert!(find_pattern(&values, &Pattern::Rising { min_points: 3 }).is_empty());
}
}