Skip to main content

midstreamer_temporal_compare/
lib.rs

1//! # Temporal-Compare
2//!
3//! Advanced temporal sequence comparison and pattern matching.
4//!
5//! ## Features
6//! - Dynamic Time Warping (DTW)
7//! - Longest Common Subsequence (LCS)
8//! - Edit Distance (Levenshtein)
9//! - Pattern matching and detection
10//! - Efficient caching
11
12use dashmap::DashMap;
13use lru::LruCache;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::fmt;
17use std::hash::Hash;
18use std::num::NonZeroUsize;
19use std::sync::{Arc, Mutex};
20use thiserror::Error;
21
22/// Errors that can occur during temporal comparison
23#[derive(Debug, Error)]
24pub enum TemporalError {
25    #[error("Sequence too long: {0}")]
26    SequenceTooLong(usize),
27
28    #[error("Invalid algorithm: {0}")]
29    InvalidAlgorithm(String),
30
31    #[error("Cache error: {0}")]
32    CacheError(String),
33
34    #[error("Invalid pattern length: min={0}, max={1}")]
35    InvalidPatternLength(usize, usize),
36
37    #[error("Pattern not found")]
38    PatternNotFound,
39}
40
41/// A temporal sequence element
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43pub struct TemporalElement<T> {
44    pub value: T,
45    pub timestamp: u64,
46}
47
48/// A temporal sequence
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct Sequence<T> {
51    pub elements: Vec<TemporalElement<T>>,
52}
53
54impl<T> Sequence<T> {
55    pub fn new() -> Self {
56        Self {
57            elements: Vec::new(),
58        }
59    }
60
61    pub fn push(&mut self, value: T, timestamp: u64) {
62        self.elements.push(TemporalElement { value, timestamp });
63    }
64
65    pub fn len(&self) -> usize {
66        self.elements.len()
67    }
68
69    pub fn is_empty(&self) -> bool {
70        self.elements.is_empty()
71    }
72}
73
74impl<T> Default for Sequence<T> {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80/// Comparison algorithm types
81#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
82pub enum ComparisonAlgorithm {
83    /// Dynamic Time Warping
84    DTW,
85    /// Longest Common Subsequence
86    LCS,
87    /// Edit Distance (Levenshtein)
88    EditDistance,
89    /// Euclidean distance
90    Euclidean,
91}
92
93/// Result of a temporal comparison
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct ComparisonResult {
96    pub distance: f64,
97    pub algorithm: ComparisonAlgorithm,
98    pub alignment: Option<Vec<(usize, usize)>>,
99}
100
101/// Statistics about cache performance
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct CacheStats {
104    pub hits: u64,
105    pub misses: u64,
106    pub size: usize,
107    pub capacity: usize,
108}
109
110impl CacheStats {
111    pub fn hit_rate(&self) -> f64 {
112        if self.hits + self.misses == 0 {
113            0.0
114        } else {
115            self.hits as f64 / (self.hits + self.misses) as f64
116        }
117    }
118}
119
120/// A detected pattern in a sequence
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct Pattern<T> {
123    /// The pattern sequence
124    pub sequence: Vec<T>,
125    /// Starting indices of all occurrences
126    pub occurrences: Vec<usize>,
127    /// Confidence score (0.0 to 1.0)
128    pub confidence: f64,
129}
130
131impl<T> Pattern<T> {
132    /// Create a new pattern
133    pub fn new(sequence: Vec<T>, occurrences: Vec<usize>, confidence: f64) -> Self {
134        Self {
135            sequence,
136            occurrences,
137            confidence,
138        }
139    }
140
141    /// Get the number of times this pattern occurs
142    pub fn frequency(&self) -> usize {
143        self.occurrences.len()
144    }
145
146    /// Get the length of the pattern
147    pub fn length(&self) -> usize {
148        self.sequence.len()
149    }
150}
151
152/// Match result for similarity search
153#[derive(Debug, Clone, PartialEq)]
154pub struct SimilarityMatch {
155    /// Starting index in the haystack
156    pub start_index: usize,
157    /// Similarity score (0.0 to 1.0, higher is more similar)
158    pub similarity: f64,
159    /// DTW distance (lower is better)
160    pub distance: f64,
161}
162
163impl SimilarityMatch {
164    pub fn new(start_index: usize, distance: f64) -> Self {
165        // Convert distance to similarity score (inverse exponential decay)
166        let similarity = (-distance / 10.0).exp();
167        Self {
168            start_index,
169            similarity,
170            distance,
171        }
172    }
173}
174
175/// Temporal comparator with caching
176pub struct TemporalComparator<T> {
177    cache: Arc<Mutex<LruCache<String, ComparisonResult>>>,
178    pattern_cache: Arc<Mutex<LruCache<String, Vec<Pattern<T>>>>>,
179    similarity_cache: Arc<Mutex<LruCache<String, Vec<SimilarityMatch>>>>,
180    cache_hits: Arc<DashMap<String, u64>>,
181    cache_misses: Arc<DashMap<String, u64>>,
182    max_sequence_length: usize,
183}
184
185impl<T> TemporalComparator<T>
186where
187    T: Clone + PartialEq + fmt::Debug + Serialize + Hash + Eq,
188{
189    /// Create a new temporal comparator
190    pub fn new(cache_size: usize, max_sequence_length: usize) -> Self {
191        Self {
192            cache: Arc::new(Mutex::new(LruCache::new(
193                NonZeroUsize::new(cache_size).unwrap(),
194            ))),
195            pattern_cache: Arc::new(Mutex::new(LruCache::new(
196                NonZeroUsize::new(cache_size).unwrap(),
197            ))),
198            similarity_cache: Arc::new(Mutex::new(LruCache::new(
199                NonZeroUsize::new(cache_size).unwrap(),
200            ))),
201            cache_hits: Arc::new(DashMap::new()),
202            cache_misses: Arc::new(DashMap::new()),
203            max_sequence_length,
204        }
205    }
206
207    /// Compare two sequences using the specified algorithm
208    pub fn compare(
209        &self,
210        seq1: &Sequence<T>,
211        seq2: &Sequence<T>,
212        algorithm: ComparisonAlgorithm,
213    ) -> Result<ComparisonResult, TemporalError> {
214        // Check sequence length
215        if seq1.len() > self.max_sequence_length || seq2.len() > self.max_sequence_length {
216            return Err(TemporalError::SequenceTooLong(seq1.len().max(seq2.len())));
217        }
218
219        // Generate cache key
220        let cache_key = self.cache_key(seq1, seq2, algorithm);
221
222        // Check cache
223        if let Ok(mut cache) = self.cache.lock() {
224            if let Some(result) = cache.get(&cache_key) {
225                self.record_cache_hit(&cache_key);
226                return Ok(result.clone());
227            }
228        }
229
230        self.record_cache_miss(&cache_key);
231
232        // Compute comparison
233        let result = match algorithm {
234            ComparisonAlgorithm::DTW => self.dtw(seq1, seq2),
235            ComparisonAlgorithm::LCS => self.lcs(seq1, seq2),
236            ComparisonAlgorithm::EditDistance => self.edit_distance(seq1, seq2),
237            ComparisonAlgorithm::Euclidean => self.euclidean(seq1, seq2),
238        }?;
239
240        // Store in cache
241        if let Ok(mut cache) = self.cache.lock() {
242            cache.put(cache_key, result.clone());
243        }
244
245        Ok(result)
246    }
247
248    /// Dynamic Time Warping implementation
249    fn dtw(
250        &self,
251        seq1: &Sequence<T>,
252        seq2: &Sequence<T>,
253    ) -> Result<ComparisonResult, TemporalError> {
254        let n = seq1.len();
255        let m = seq2.len();
256
257        if n == 0 || m == 0 {
258            return Ok(ComparisonResult {
259                distance: (n + m) as f64,
260                algorithm: ComparisonAlgorithm::DTW,
261                alignment: None,
262            });
263        }
264
265        // Initialize DTW matrix
266        let mut dtw = vec![vec![f64::INFINITY; m + 1]; n + 1];
267        dtw[0][0] = 0.0;
268
269        // Fill DTW matrix
270        for i in 1..=n {
271            for j in 1..=m {
272                let cost = if seq1.elements[i - 1].value == seq2.elements[j - 1].value {
273                    0.0
274                } else {
275                    1.0
276                };
277
278                dtw[i][j] = cost + dtw[i - 1][j - 1].min(dtw[i - 1][j]).min(dtw[i][j - 1]);
279            }
280        }
281
282        // Backtrack for alignment
283        let mut alignment = Vec::new();
284        let (mut i, mut j) = (n, m);
285
286        while i > 0 && j > 0 {
287            alignment.push((i - 1, j - 1));
288
289            let min_val = dtw[i - 1][j - 1].min(dtw[i - 1][j]).min(dtw[i][j - 1]);
290
291            if dtw[i - 1][j - 1] == min_val {
292                i -= 1;
293                j -= 1;
294            } else if dtw[i - 1][j] == min_val {
295                i -= 1;
296            } else {
297                j -= 1;
298            }
299        }
300
301        alignment.reverse();
302
303        Ok(ComparisonResult {
304            distance: dtw[n][m],
305            algorithm: ComparisonAlgorithm::DTW,
306            alignment: Some(alignment),
307        })
308    }
309
310    /// Longest Common Subsequence implementation
311    fn lcs(
312        &self,
313        seq1: &Sequence<T>,
314        seq2: &Sequence<T>,
315    ) -> Result<ComparisonResult, TemporalError> {
316        let n = seq1.len();
317        let m = seq2.len();
318
319        let mut dp = vec![vec![0; m + 1]; n + 1];
320
321        for i in 1..=n {
322            for j in 1..=m {
323                if seq1.elements[i - 1].value == seq2.elements[j - 1].value {
324                    dp[i][j] = dp[i - 1][j - 1] + 1;
325                } else {
326                    dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
327                }
328            }
329        }
330
331        let lcs_length = dp[n][m];
332        let distance = (n + m - 2 * lcs_length) as f64;
333
334        Ok(ComparisonResult {
335            distance,
336            algorithm: ComparisonAlgorithm::LCS,
337            alignment: None,
338        })
339    }
340
341    /// Edit Distance (Levenshtein) implementation
342    fn edit_distance(
343        &self,
344        seq1: &Sequence<T>,
345        seq2: &Sequence<T>,
346    ) -> Result<ComparisonResult, TemporalError> {
347        let n = seq1.len();
348        let m = seq2.len();
349
350        let mut dp = vec![vec![0; m + 1]; n + 1];
351
352        // DP base cases: indexed-loop form is the textbook shape;
353        // the iter_mut().enumerate() rewrite suggested by clippy is
354        // less readable for two-dimensional matrices.
355        #[allow(clippy::needless_range_loop)]
356        for i in 0..=n {
357            dp[i][0] = i;
358        }
359        #[allow(clippy::needless_range_loop)]
360        for j in 0..=m {
361            dp[0][j] = j;
362        }
363
364        for i in 1..=n {
365            for j in 1..=m {
366                let cost = if seq1.elements[i - 1].value == seq2.elements[j - 1].value {
367                    0
368                } else {
369                    1
370                };
371
372                dp[i][j] = (dp[i - 1][j] + 1)
373                    .min(dp[i][j - 1] + 1)
374                    .min(dp[i - 1][j - 1] + cost);
375            }
376        }
377
378        Ok(ComparisonResult {
379            distance: dp[n][m] as f64,
380            algorithm: ComparisonAlgorithm::EditDistance,
381            alignment: None,
382        })
383    }
384
385    /// Euclidean distance (for numeric sequences)
386    fn euclidean(
387        &self,
388        seq1: &Sequence<T>,
389        seq2: &Sequence<T>,
390    ) -> Result<ComparisonResult, TemporalError> {
391        let n = seq1.len().min(seq2.len());
392        let mut sum: f64 = 0.0;
393
394        for i in 0..n {
395            // Simplified: just count mismatches
396            if seq1.elements[i].value != seq2.elements[i].value {
397                sum += 1.0;
398            }
399        }
400
401        Ok(ComparisonResult {
402            distance: sum.sqrt(), // f64 type is now explicit from declaration
403            algorithm: ComparisonAlgorithm::Euclidean,
404            alignment: None,
405        })
406    }
407
408    /// Generate cache key for a comparison
409    fn cache_key(
410        &self,
411        seq1: &Sequence<T>,
412        seq2: &Sequence<T>,
413        algorithm: ComparisonAlgorithm,
414    ) -> String {
415        format!(
416            "{:?}:{:?}:{:?}",
417            seq1.elements.len(),
418            seq2.elements.len(),
419            algorithm
420        )
421    }
422
423    fn record_cache_hit(&self, key: &str) {
424        self.cache_hits
425            .entry(key.to_string())
426            .and_modify(|v| *v += 1)
427            .or_insert(1);
428    }
429
430    fn record_cache_miss(&self, key: &str) {
431        self.cache_misses
432            .entry(key.to_string())
433            .and_modify(|v| *v += 1)
434            .or_insert(1);
435    }
436
437    /// Get cache statistics
438    pub fn cache_stats(&self) -> CacheStats {
439        let hits: u64 = self.cache_hits.iter().map(|r| *r.value()).sum();
440        let misses: u64 = self.cache_misses.iter().map(|r| *r.value()).sum();
441
442        let (size, capacity) = if let Ok(cache) = self.cache.lock() {
443            (cache.len(), cache.cap().get())
444        } else {
445            (0, 0)
446        };
447
448        CacheStats {
449            hits,
450            misses,
451            size,
452            capacity,
453        }
454    }
455
456    /// Clear the cache
457    pub fn clear_cache(&self) {
458        if let Ok(mut cache) = self.cache.lock() {
459            cache.clear();
460        }
461        if let Ok(mut cache) = self.pattern_cache.lock() {
462            cache.clear();
463        }
464        if let Ok(mut cache) = self.similarity_cache.lock() {
465            cache.clear();
466        }
467        self.cache_hits.clear();
468        self.cache_misses.clear();
469    }
470
471    /// Find similar sequences within a haystack using generic types
472    pub fn find_similar_generic(
473        &self,
474        haystack: &[T],
475        needle: &[T],
476        threshold: f64,
477    ) -> Result<Vec<SimilarityMatch>, TemporalError> {
478        if needle.is_empty() || haystack.len() < needle.len() {
479            return Ok(Vec::new());
480        }
481
482        // Generate cache key
483        let cache_key = format!(
484            "similar:{:?}:{:?}:{}",
485            haystack.len(),
486            needle.len(),
487            threshold
488        );
489
490        // Check cache
491        if let Ok(mut cache) = self.similarity_cache.lock() {
492            if let Some(results) = cache.get(&cache_key) {
493                self.record_cache_hit(&cache_key);
494                return Ok(results.clone());
495            }
496        }
497
498        self.record_cache_miss(&cache_key);
499
500        let needle_len = needle.len();
501        let mut matches = Vec::new();
502
503        // Sliding window approach
504        for start_idx in 0..=(haystack.len() - needle_len) {
505            let window = &haystack[start_idx..start_idx + needle_len];
506
507            // Convert to Sequence for comparison
508            let mut seq1 = Sequence::new();
509            for (i, item) in window.iter().enumerate() {
510                seq1.push(item.clone(), i as u64);
511            }
512
513            let mut seq2 = Sequence::new();
514            for (i, item) in needle.iter().enumerate() {
515                seq2.push(item.clone(), i as u64);
516            }
517
518            // Compute DTW distance
519            if let Ok(result) = self.dtw(&seq1, &seq2) {
520                // Normalize distance by pattern length
521                let normalized_distance = result.distance / needle_len as f64;
522
523                if normalized_distance <= threshold {
524                    matches.push(SimilarityMatch::new(start_idx, result.distance));
525                }
526            }
527        }
528
529        // Sort by distance (best matches first)
530        matches.sort_by(|a, b| {
531            a.distance
532                .partial_cmp(&b.distance)
533                .unwrap_or(std::cmp::Ordering::Equal)
534        });
535
536        // Store in cache
537        if let Ok(mut cache) = self.similarity_cache.lock() {
538            cache.put(cache_key, matches.clone());
539        }
540
541        Ok(matches)
542    }
543
544    /// Detect recurring patterns in a sequence
545    pub fn detect_recurring_patterns(
546        &self,
547        sequence: &[T],
548        min_length: usize,
549        max_length: usize,
550    ) -> Result<Vec<Pattern<T>>, TemporalError> {
551        if min_length > max_length {
552            return Err(TemporalError::InvalidPatternLength(min_length, max_length));
553        }
554
555        if sequence.len() < min_length {
556            return Ok(Vec::new());
557        }
558
559        // Generate cache key
560        let cache_key = format!(
561            "patterns:{:?}:{}:{}",
562            sequence.len(),
563            min_length,
564            max_length
565        );
566
567        // Check cache
568        if let Ok(mut cache) = self.pattern_cache.lock() {
569            if let Some(patterns) = cache.get(&cache_key) {
570                self.record_cache_hit(&cache_key);
571                return Ok(patterns.clone());
572            }
573        }
574
575        self.record_cache_miss(&cache_key);
576
577        let mut pattern_map: HashMap<Vec<T>, Vec<usize>> = HashMap::new();
578
579        // Search for patterns of each length
580        for pattern_len in min_length..=max_length.min(sequence.len()) {
581            for start_idx in 0..=(sequence.len() - pattern_len) {
582                let pattern_seq = sequence[start_idx..start_idx + pattern_len].to_vec();
583
584                pattern_map.entry(pattern_seq).or_default().push(start_idx);
585            }
586        }
587
588        // Filter patterns that occur at least twice
589        let mut patterns: Vec<Pattern<T>> = pattern_map
590            .into_iter()
591            .filter(|(_, occurrences)| occurrences.len() >= 2)
592            .map(|(seq, occurrences)| {
593                // Calculate confidence based on frequency and pattern length
594                let frequency = occurrences.len() as f64;
595                let pattern_len = seq.len() as f64;
596                let total_possible = (sequence.len() - seq.len() + 1) as f64;
597
598                // Confidence is weighted by frequency and pattern length
599                let confidence =
600                    ((frequency / total_possible) * (pattern_len / max_length as f64)).min(1.0);
601
602                Pattern::new(seq, occurrences, confidence)
603            })
604            .collect();
605
606        // Sort by frequency (most common first), then by confidence
607        patterns.sort_by(|a, b| {
608            b.frequency().cmp(&a.frequency()).then_with(|| {
609                b.confidence
610                    .partial_cmp(&a.confidence)
611                    .unwrap_or(std::cmp::Ordering::Equal)
612            })
613        });
614
615        // Store in cache
616        if let Ok(mut cache) = self.pattern_cache.lock() {
617            cache.put(cache_key, patterns.clone());
618        }
619
620        Ok(patterns)
621    }
622}
623
624impl<T> Default for TemporalComparator<T>
625where
626    T: Clone + PartialEq + fmt::Debug + Serialize + Hash + Eq,
627{
628    fn default() -> Self {
629        Self::new(1000, 10000)
630    }
631}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636
637    #[test]
638    fn test_sequence_creation() {
639        let mut seq: Sequence<i32> = Sequence::new();
640        seq.push(1, 100);
641        seq.push(2, 200);
642
643        assert_eq!(seq.len(), 2);
644        assert!(!seq.is_empty());
645    }
646
647    #[test]
648    fn test_dtw() {
649        let comparator = TemporalComparator::new(100, 1000);
650
651        let mut seq1: Sequence<i32> = Sequence::new();
652        seq1.push(1, 100);
653        seq1.push(2, 200);
654        seq1.push(3, 300);
655
656        let mut seq2: Sequence<i32> = Sequence::new();
657        seq2.push(1, 100);
658        seq2.push(2, 200);
659        seq2.push(3, 300);
660
661        let result = comparator
662            .compare(&seq1, &seq2, ComparisonAlgorithm::DTW)
663            .unwrap();
664        assert_eq!(result.distance, 0.0);
665    }
666
667    #[test]
668    fn test_cache() {
669        let comparator = TemporalComparator::new(100, 1000);
670
671        let mut seq1: Sequence<i32> = Sequence::new();
672        seq1.push(1, 1);
673        seq1.push(2, 2);
674
675        let mut seq2: Sequence<i32> = Sequence::new();
676        seq2.push(1, 1);
677        seq2.push(2, 2);
678
679        // First comparison - cache miss
680        comparator
681            .compare(&seq1, &seq2, ComparisonAlgorithm::DTW)
682            .unwrap();
683
684        // Second comparison - cache hit
685        comparator
686            .compare(&seq1, &seq2, ComparisonAlgorithm::DTW)
687            .unwrap();
688
689        let stats = comparator.cache_stats();
690        assert_eq!(stats.hits, 1);
691        assert_eq!(stats.misses, 1);
692    }
693
694    #[test]
695    fn test_find_similar_generic_integers() {
696        let comparator: TemporalComparator<i32> = TemporalComparator::new(100, 1000);
697
698        let haystack = vec![1, 2, 3, 4, 5, 3, 4, 5];
699        let needle = vec![3, 4, 5];
700
701        let matches = comparator
702            .find_similar_generic(&haystack, &needle, 0.1)
703            .unwrap();
704
705        assert_eq!(matches.len(), 2);
706        assert_eq!(matches[0].start_index, 2);
707        assert_eq!(matches[1].start_index, 5);
708        assert!(matches[0].similarity > 0.9); // High similarity for exact match
709    }
710
711    #[test]
712    fn test_detect_recurring_patterns_simple() {
713        let comparator: TemporalComparator<char> = TemporalComparator::new(100, 1000);
714
715        let sequence = vec!['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b', 'c'];
716
717        let patterns = comparator
718            .detect_recurring_patterns(&sequence, 2, 4)
719            .unwrap();
720
721        assert!(!patterns.is_empty());
722        // Should find 'abc' pattern recurring
723        let abc_pattern = patterns.iter().find(|p| p.sequence == vec!['a', 'b', 'c']);
724        assert!(abc_pattern.is_some());
725
726        let pattern = abc_pattern.unwrap();
727        assert_eq!(pattern.frequency(), 3);
728        assert!(pattern.confidence > 0.0);
729    }
730}