Skip to main content

feagi_npu_plasticity/
pattern_detector.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*
5 * Copyright 2025 Neuraville Inc.
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 */
10
11//! High-performance temporal pattern detection using native HashSets
12//!
13//! This module replaces the Python pyroaring implementation with pure Rust,
14//! using standard library HashSets for pattern detection and xxHash64 for
15//! deterministic pattern hashing.
16//!
17//! xxHash64 provides:
18//! - 10x faster hashing than SHA-256 (~50ns vs ~500ns per pattern)
19//! - Cross-platform determinism (x86, ARM, RISC-V)
20//! - Collision resistance suitable for FEAGI's scale (2^64 hash space)
21
22use std::collections::{HashMap, HashSet};
23use std::sync::{Arc, Mutex};
24use xxhash_rust::xxh64::xxh64;
25
26/// Configuration for pattern detection
27#[derive(Debug, Clone)]
28pub struct PatternConfig {
29    /// Default temporal depth (timesteps to look back)
30    pub default_temporal_depth: u32,
31
32    /// Minimum neurons required for pattern recognition
33    pub min_activity_threshold: usize,
34
35    /// Maximum patterns to cache
36    pub max_pattern_cache_size: usize,
37}
38
39impl Default for PatternConfig {
40    fn default() -> Self {
41        Self {
42            default_temporal_depth: 3,
43            min_activity_threshold: 1,
44            max_pattern_cache_size: 10000,
45        }
46    }
47}
48
49/// Temporal pattern representation
50#[derive(Debug, Clone, PartialEq, Eq, Hash)]
51pub struct TemporalPattern {
52    /// xxHash64 hash of the pattern (8 bytes, deterministic across platforms)
53    pub pattern_hash: u64,
54
55    /// Temporal depth used for this pattern
56    pub temporal_depth: u32,
57
58    /// Upstream cortical area indices
59    pub upstream_areas: Vec<u32>,
60
61    /// Neuron counts per timestep
62    pub timestep_neuron_counts: Vec<usize>,
63
64    /// Total activity across all timesteps
65    pub total_activity: usize,
66}
67
68/// Statistics for pattern detection
69#[derive(Debug, Clone, Default)]
70pub struct PatternDetectorStats {
71    pub patterns_detected: usize,
72    pub cache_hits: usize,
73    pub cache_misses: usize,
74    pub empty_patterns: usize,
75    pub set_operations: usize,
76}
77
78/// High-performance temporal pattern detector
79pub struct PatternDetector {
80    config: PatternConfig,
81
82    /// Pattern cache (pattern_hash -> pattern)
83    pattern_cache: Arc<Mutex<HashMap<u64, TemporalPattern>>>,
84
85    /// LRU access order for cache eviction
86    cache_access_order: Arc<Mutex<Vec<u64>>>,
87
88    /// Per-area temporal depth configuration
89    area_temporal_depths: Arc<Mutex<HashMap<u32, u32>>>,
90
91    /// Statistics
92    stats: Arc<Mutex<PatternDetectorStats>>,
93}
94
95impl PatternDetector {
96    /// Create a new pattern detector
97    pub fn new(config: PatternConfig) -> Self {
98        Self {
99            config,
100            pattern_cache: Arc::new(Mutex::new(HashMap::new())),
101            cache_access_order: Arc::new(Mutex::new(Vec::new())),
102            area_temporal_depths: Arc::new(Mutex::new(HashMap::new())),
103            stats: Arc::new(Mutex::new(PatternDetectorStats::default())),
104        }
105    }
106
107    /// Detect temporal pattern from firing history
108    pub fn detect_pattern(
109        &self,
110        memory_area_idx: u32,
111        upstream_areas: &[u32],
112        _current_timestep: u64,
113        timestep_bitmaps: Vec<HashSet<u32>>,
114        temporal_depth: Option<u32>,
115    ) -> Option<TemporalPattern> {
116        if upstream_areas.is_empty() {
117            return None;
118        }
119
120        // Get temporal depth for this area
121        let area_temporal_depth =
122            temporal_depth.unwrap_or_else(|| self.get_area_temporal_depth(memory_area_idx));
123
124        if timestep_bitmaps.is_empty() {
125            let mut stats = self.stats.lock().unwrap();
126            stats.empty_patterns += 1;
127            return None;
128        }
129
130        // Check if pattern has sufficient activity
131        let total_activity: usize = timestep_bitmaps.iter().map(|set| set.len()).sum();
132
133        if total_activity < self.config.min_activity_threshold {
134            let mut stats = self.stats.lock().unwrap();
135            stats.empty_patterns += 1;
136            return None;
137        }
138
139        // Create deterministic pattern hash
140        let pattern_hash = self.create_pattern_hash(&timestep_bitmaps);
141
142        // Check cache first
143        {
144            let cache = self.pattern_cache.lock().unwrap();
145            if let Some(pattern) = cache.get(&pattern_hash) {
146                self.update_cache_access(pattern_hash);
147                let mut stats = self.stats.lock().unwrap();
148                stats.cache_hits += 1;
149                return Some(pattern.clone());
150            }
151        }
152
153        // Create new pattern
154        let timestep_neuron_counts: Vec<usize> =
155            timestep_bitmaps.iter().map(|set| set.len()).collect();
156
157        let mut sorted_upstream = upstream_areas.to_vec();
158        sorted_upstream.sort_unstable();
159
160        let pattern = TemporalPattern {
161            pattern_hash,
162            temporal_depth: area_temporal_depth,
163            upstream_areas: sorted_upstream,
164            timestep_neuron_counts,
165            total_activity,
166        };
167
168        // Cache the pattern
169        self.add_to_cache(pattern.clone());
170
171        let mut stats = self.stats.lock().unwrap();
172        stats.patterns_detected += 1;
173        stats.cache_misses += 1;
174
175        Some(pattern)
176    }
177
178    /// Create deterministic xxHash64 from bitmap sequence
179    ///
180    /// CRITICAL: This must produce identical output across:
181    /// - x86-64, ARM64, RISC-V architectures
182    /// - Linux, Windows, macOS, RTOS operating systems
183    /// - Different compiler versions
184    ///
185    /// xxHash64 guarantees cross-platform determinism through:
186    /// - Explicit little-endian serialization
187    /// - Sorted neuron IDs (order-independent input)
188    /// - Fixed seed value (0)
189    fn create_pattern_hash(&self, timestep_bitmaps: &[HashSet<u32>]) -> u64 {
190        let mut buffer = Vec::new();
191
192        // Serialize each bitmap in temporal order
193        for bitmap in timestep_bitmaps {
194            // Sort neuron IDs for determinism
195            let mut sorted_ids: Vec<u32> = bitmap.iter().copied().collect();
196            sorted_ids.sort_unstable();
197
198            // Length prefix (4 bytes, little-endian)
199            let len = sorted_ids.len() as u32;
200            buffer.extend_from_slice(&len.to_le_bytes());
201
202            // Neuron IDs (4 bytes each, little-endian)
203            for id in sorted_ids {
204                buffer.extend_from_slice(&id.to_le_bytes());
205            }
206        }
207
208        // Fixed seed = 0 for determinism
209        xxh64(&buffer, 0)
210    }
211
212    /// Add pattern to cache with LRU eviction
213    fn add_to_cache(&self, pattern: TemporalPattern) {
214        let pattern_hash = pattern.pattern_hash;
215
216        let mut cache = self.pattern_cache.lock().unwrap();
217        let mut access_order = self.cache_access_order.lock().unwrap();
218
219        // Add to cache
220        cache.insert(pattern_hash, pattern);
221        access_order.push(pattern_hash);
222
223        // Evict oldest if cache is full
224        if cache.len() > self.config.max_pattern_cache_size {
225            if let Some(oldest_hash) = access_order.first().copied() {
226                access_order.remove(0);
227                cache.remove(&oldest_hash);
228            }
229        }
230    }
231
232    /// Update cache access order for LRU
233    fn update_cache_access(&self, pattern_hash: u64) {
234        let mut access_order = self.cache_access_order.lock().unwrap();
235        if let Some(pos) = access_order.iter().position(|&h| h == pattern_hash) {
236            access_order.remove(pos);
237        }
238        access_order.push(pattern_hash);
239    }
240
241    /// Configure temporal depth for a specific memory area
242    pub fn configure_area_temporal_depth(&self, memory_area_idx: u32, temporal_depth: u32) {
243        let mut depths = self.area_temporal_depths.lock().unwrap();
244        depths.insert(memory_area_idx, temporal_depth);
245    }
246
247    /// Get temporal depth for a memory area
248    fn get_area_temporal_depth(&self, memory_area_idx: u32) -> u32 {
249        let depths = self.area_temporal_depths.lock().unwrap();
250        depths
251            .get(&memory_area_idx)
252            .copied()
253            .unwrap_or(self.config.default_temporal_depth)
254    }
255
256    /// Get detection statistics
257    pub fn get_stats(&self) -> PatternDetectorStats {
258        self.stats.lock().unwrap().clone()
259    }
260
261    /// Clear pattern cache
262    pub fn clear_cache(&self) {
263        let mut cache = self.pattern_cache.lock().unwrap();
264        let mut access_order = self.cache_access_order.lock().unwrap();
265        cache.clear();
266        access_order.clear();
267    }
268
269    /// Reset statistics
270    pub fn reset_stats(&self) {
271        let mut stats = self.stats.lock().unwrap();
272        *stats = PatternDetectorStats::default();
273    }
274}
275
276/// Batch pattern detector for multiple memory areas
277pub struct BatchPatternDetector {
278    pub(crate) base_config: PatternConfig,
279    pub(crate) detectors: Arc<Mutex<HashMap<u32, PatternDetector>>>,
280}
281
282impl BatchPatternDetector {
283    /// Create a new batch pattern detector
284    pub fn new(base_config: PatternConfig) -> Self {
285        Self {
286            base_config,
287            detectors: Arc::new(Mutex::new(HashMap::new())),
288        }
289    }
290
291    /// Get or create detector for memory area
292    pub fn get_detector(&self, memory_area_idx: u32, temporal_depth: u32) -> PatternDetector {
293        let mut detectors = self.detectors.lock().unwrap();
294
295        detectors.entry(memory_area_idx).or_insert_with(|| {
296            let detector = PatternDetector::new(self.base_config.clone());
297            detector.configure_area_temporal_depth(memory_area_idx, temporal_depth);
298            detector
299        });
300
301        // Clone the detector for thread-safe access
302        detectors.get(&memory_area_idx).unwrap().clone()
303    }
304
305    /// Get statistics for all detectors
306    pub fn get_batch_stats(&self) -> HashMap<u32, PatternDetectorStats> {
307        let detectors = self.detectors.lock().unwrap();
308        detectors
309            .iter()
310            .map(|(&area_idx, detector)| (area_idx, detector.get_stats()))
311            .collect()
312    }
313}
314
315impl Clone for PatternDetector {
316    fn clone(&self) -> Self {
317        Self {
318            config: self.config.clone(),
319            pattern_cache: Arc::clone(&self.pattern_cache),
320            cache_access_order: Arc::clone(&self.cache_access_order),
321            area_temporal_depths: Arc::clone(&self.area_temporal_depths),
322            stats: Arc::clone(&self.stats),
323        }
324    }
325}
326
327impl Clone for BatchPatternDetector {
328    fn clone(&self) -> Self {
329        Self {
330            base_config: self.base_config.clone(),
331            detectors: Arc::clone(&self.detectors),
332        }
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_pattern_config_default() {
342        let config = PatternConfig::default();
343        assert_eq!(config.default_temporal_depth, 3);
344        assert_eq!(config.min_activity_threshold, 1);
345        assert_eq!(config.max_pattern_cache_size, 10000);
346    }
347
348    #[test]
349    fn test_pattern_detection() {
350        let config = PatternConfig::default();
351        let detector = PatternDetector::new(config);
352
353        // Create test pattern
354        let mut bitmap1 = HashSet::new();
355        bitmap1.insert(1);
356        bitmap1.insert(2);
357
358        let mut bitmap2 = HashSet::new();
359        bitmap2.insert(3);
360        bitmap2.insert(4);
361
362        let bitmaps = vec![bitmap1, bitmap2];
363        let upstream_areas = vec![1, 2];
364
365        let pattern = detector.detect_pattern(
366            100, // memory area
367            &upstream_areas,
368            10, // timestep
369            bitmaps,
370            None,
371        );
372
373        assert!(pattern.is_some());
374        let pattern = pattern.unwrap();
375        assert_eq!(pattern.temporal_depth, 3);
376        assert_eq!(pattern.total_activity, 4);
377        assert_eq!(pattern.timestep_neuron_counts, vec![2, 2]);
378        assert_eq!(pattern.upstream_areas, vec![1, 2]);
379    }
380
381    #[test]
382    fn test_pattern_detection_empty() {
383        let config = PatternConfig::default();
384        let detector = PatternDetector::new(config);
385
386        let bitmaps = vec![];
387        let upstream_areas = vec![1];
388
389        let pattern = detector.detect_pattern(100, &upstream_areas, 10, bitmaps, None);
390        assert!(pattern.is_none());
391
392        let stats = detector.get_stats();
393        assert_eq!(stats.empty_patterns, 1);
394    }
395
396    #[test]
397    fn test_pattern_detection_no_upstream() {
398        let config = PatternConfig::default();
399        let detector = PatternDetector::new(config);
400
401        let mut bitmap = HashSet::new();
402        bitmap.insert(1);
403
404        let bitmaps = vec![bitmap];
405        let upstream_areas = vec![];
406
407        let pattern = detector.detect_pattern(100, &upstream_areas, 10, bitmaps, None);
408        assert!(pattern.is_none());
409    }
410
411    #[test]
412    fn test_pattern_detection_below_threshold() {
413        let config = PatternConfig {
414            min_activity_threshold: 10,
415            ..Default::default()
416        };
417        let detector = PatternDetector::new(config);
418
419        let mut bitmap = HashSet::new();
420        bitmap.insert(1);
421        bitmap.insert(2);
422
423        let bitmaps = vec![bitmap];
424        let upstream_areas = vec![1];
425
426        let pattern = detector.detect_pattern(100, &upstream_areas, 10, bitmaps, None);
427        assert!(pattern.is_none());
428
429        let stats = detector.get_stats();
430        assert_eq!(stats.empty_patterns, 1);
431    }
432
433    #[test]
434    fn test_pattern_cache() {
435        let config = PatternConfig::default();
436        let detector = PatternDetector::new(config);
437
438        let mut bitmap = HashSet::new();
439        bitmap.insert(1);
440        bitmap.insert(2);
441
442        let bitmaps = vec![bitmap.clone()];
443        let upstream_areas = vec![1];
444
445        // First detection - cache miss
446        let pattern1 = detector.detect_pattern(100, &upstream_areas, 10, bitmaps.clone(), None);
447        assert!(pattern1.is_some());
448
449        let stats = detector.get_stats();
450        assert_eq!(stats.cache_misses, 1);
451        assert_eq!(stats.cache_hits, 0);
452        assert_eq!(stats.patterns_detected, 1);
453
454        // Second detection - cache hit
455        let pattern2 = detector.detect_pattern(100, &upstream_areas, 11, bitmaps, None);
456        assert!(pattern2.is_some());
457
458        let stats = detector.get_stats();
459        assert_eq!(stats.cache_hits, 1);
460        assert_eq!(stats.patterns_detected, 1); // Still 1, cache hit doesn't create new pattern
461
462        // Patterns should be equal
463        assert_eq!(
464            pattern1.unwrap().pattern_hash,
465            pattern2.unwrap().pattern_hash
466        );
467    }
468
469    #[test]
470    fn test_cache_eviction() {
471        let config = PatternConfig {
472            max_pattern_cache_size: 2,
473            ..Default::default()
474        };
475        let detector = PatternDetector::new(config);
476
477        // Create 3 different patterns
478        for i in 0..3 {
479            let mut bitmap = HashSet::new();
480            bitmap.insert(i);
481            detector.detect_pattern(100, &[1], 10, vec![bitmap], None);
482        }
483
484        // Cache should have at most 2 patterns
485        let cache = detector.pattern_cache.lock().unwrap();
486        assert!(cache.len() <= 2);
487    }
488
489    #[test]
490    fn test_deterministic_hashing() {
491        let config = PatternConfig::default();
492        let detector = PatternDetector::new(config);
493
494        let mut bitmap = HashSet::new();
495        bitmap.insert(3);
496        bitmap.insert(1);
497        bitmap.insert(2);
498
499        let hash1 = detector.create_pattern_hash(&[bitmap.clone()]);
500        let hash2 = detector.create_pattern_hash(&[bitmap]);
501
502        assert_eq!(hash1, hash2);
503    }
504
505    #[test]
506    fn test_different_patterns_different_hashes() {
507        let config = PatternConfig::default();
508        let detector = PatternDetector::new(config);
509
510        let mut bitmap1 = HashSet::new();
511        bitmap1.insert(1);
512        bitmap1.insert(2);
513
514        let mut bitmap2 = HashSet::new();
515        bitmap2.insert(1);
516        bitmap2.insert(3); // Different from bitmap1
517
518        let hash1 = detector.create_pattern_hash(&[bitmap1]);
519        let hash2 = detector.create_pattern_hash(&[bitmap2]);
520
521        assert_ne!(hash1, hash2);
522    }
523
524    #[test]
525    fn test_temporal_order_sensitivity() {
526        let config = PatternConfig::default();
527        let detector = PatternDetector::new(config);
528
529        let mut bitmap1 = HashSet::new();
530        bitmap1.insert(1);
531
532        let mut bitmap2 = HashSet::new();
533        bitmap2.insert(2);
534
535        // Different temporal orders should produce different hashes
536        let hash1 = detector.create_pattern_hash(&[bitmap1.clone(), bitmap2.clone()]);
537        let hash2 = detector.create_pattern_hash(&[bitmap2, bitmap1]);
538
539        assert_ne!(hash1, hash2);
540    }
541
542    #[test]
543    fn test_configure_area_temporal_depth() {
544        let config = PatternConfig::default();
545        let default_temp_depth = config.default_temporal_depth;
546        let detector = PatternDetector::new(config);
547
548        detector.configure_area_temporal_depth(100, 5);
549
550        let depth = detector.get_area_temporal_depth(100);
551        assert_eq!(depth, 5);
552
553        // Unconfigured area should use default
554        let default_depth = detector.get_area_temporal_depth(999);
555        assert_eq!(default_depth, default_temp_depth);
556    }
557
558    #[test]
559    fn test_clear_cache() {
560        let config = PatternConfig::default();
561        let detector = PatternDetector::new(config);
562
563        let mut bitmap = HashSet::new();
564        bitmap.insert(1);
565        detector.detect_pattern(100, &[1], 10, vec![bitmap], None);
566
567        // Cache should have entries
568        {
569            let cache = detector.pattern_cache.lock().unwrap();
570            assert!(!cache.is_empty());
571        }
572
573        detector.clear_cache();
574
575        // Cache should be empty
576        let cache = detector.pattern_cache.lock().unwrap();
577        assert!(cache.is_empty());
578    }
579
580    #[test]
581    fn test_reset_stats() {
582        let config = PatternConfig::default();
583        let detector = PatternDetector::new(config);
584
585        let mut bitmap = HashSet::new();
586        bitmap.insert(1);
587        detector.detect_pattern(100, &[1], 10, vec![bitmap], None);
588
589        let stats = detector.get_stats();
590        assert!(stats.patterns_detected > 0);
591
592        detector.reset_stats();
593
594        let stats = detector.get_stats();
595        assert_eq!(stats.patterns_detected, 0);
596        assert_eq!(stats.cache_hits, 0);
597        assert_eq!(stats.cache_misses, 0);
598    }
599
600    #[test]
601    fn test_batch_pattern_detector() {
602        let config = PatternConfig::default();
603        let batch_detector = BatchPatternDetector::new(config);
604
605        let detector = batch_detector.get_detector(100, 5);
606
607        let mut bitmap = HashSet::new();
608        bitmap.insert(1);
609        let pattern = detector.detect_pattern(100, &[1], 10, vec![bitmap], None);
610
611        assert!(pattern.is_some());
612    }
613
614    #[test]
615    fn test_batch_stats() {
616        let config = PatternConfig::default();
617        let batch_detector = BatchPatternDetector::new(config);
618
619        // Create patterns for multiple areas
620        for area_idx in [100, 200, 300] {
621            let detector = batch_detector.get_detector(area_idx, 3);
622            let mut bitmap = HashSet::new();
623            bitmap.insert(area_idx);
624            detector.detect_pattern(area_idx, &[1], 10, vec![bitmap], None);
625        }
626
627        let stats = batch_detector.get_batch_stats();
628        assert_eq!(stats.len(), 3);
629        assert!(stats.contains_key(&100));
630        assert!(stats.contains_key(&200));
631        assert!(stats.contains_key(&300));
632    }
633}