Skip to main content

constraint_theory_core/
cache.rs

1//! Lattice Cache for Pythagorean Coordinates
2//!
3//! This module provides caching for Pythagorean lattice generation to avoid
4//! recomputing the lattice for repeated operations with the same density.
5//!
6//! # Architecture
7//!
8//! ```text
9//! ┌─────────────────────────────────────────────────────────────┐
10//! │                    LATTICE CACHE                             │
11//! ├─────────────────────────────────────────────────────────────┤
12//! │                                                              │
13//! │  ┌──────────────┐     ┌──────────────────────────────────┐  │
14//! │  │ Cache Key    │     │ Cached Lattice                    │  │
15//! │  │ (density)    │──►  │ • Pythagorean triples             │  │
16//! │  │              │     │ • Normalized vectors              │  │
17//! │  └──────────────┘     │ • KD-tree index                   │  │
18//! │                       └──────────────────────────────────┘  │
19//! │                                                              │
20//! │  Benefits:                                                   │
21//! │  • O(1) lookup for cached lattices                          │
22//! │  • Thread-safe access via RwLock                            │
23//! │  • Automatic cache eviction (LRU)                           │
24//! │                                                              │
25//! └─────────────────────────────────────────────────────────────┘
26//! ```
27//!
28//! # Example
29//!
30//! ```
31//! use constraint_theory_core::cache::LatticeCache;
32//!
33//! let cache = LatticeCache::new(100);
34//!
35//! // First call computes and caches
36//! let lattice1 = cache.get_or_compute(200);
37//!
38//! // Second call returns cached version
39//! let lattice2 = cache.get_or_compute(200);
40//!
41//! // Both references point to the same cached data
42//! assert_eq!(lattice1.len(), lattice2.len());
43//! ```
44
45use std::collections::HashMap;
46use std::sync::{Arc, RwLock};
47
48/// A cached Pythagorean lattice entry.
49#[derive(Clone, Debug)]
50pub struct CachedLattice {
51    /// Pythagorean triples (a, b, c)
52    pub triples: Vec<(u32, u32, u32)>,
53    /// Normalized vectors on unit circle
54    pub vectors: Vec<[f64; 2]>,
55    /// Maximum hypotenuse in the lattice
56    pub max_hypotenuse: u32,
57    /// Density parameter used to generate
58    pub density: usize,
59}
60
61impl CachedLattice {
62    /// Create a new cached lattice entry.
63    pub fn new(density: usize) -> Self {
64        let (triples, vectors) = generate_pythagorean_lattice(density);
65        let max_hypotenuse = triples.iter().map(|&(_, _, c)| c).max().unwrap_or(0);
66
67        Self {
68            triples,
69            vectors,
70            max_hypotenuse,
71            density,
72        }
73    }
74
75    /// Get the number of points in the lattice.
76    pub fn len(&self) -> usize {
77        self.vectors.len()
78    }
79
80    /// Check if the lattice is empty.
81    pub fn is_empty(&self) -> bool {
82        self.vectors.is_empty()
83    }
84
85    /// Find the nearest lattice point to a given point.
86    pub fn nearest(&self, point: [f64; 2]) -> ([f64; 2], usize, f64) {
87        let mut best_point = [0.0, 0.0];
88        let mut best_idx = 0;
89        let mut best_dist_sq = f64::MAX;
90
91        for (i, &v) in self.vectors.iter().enumerate() {
92            let dx = v[0] - point[0];
93            let dy = v[1] - point[1];
94            let dist_sq = dx * dx + dy * dy;
95
96            if dist_sq < best_dist_sq {
97                best_dist_sq = dist_sq;
98                best_point = v;
99                best_idx = i;
100            }
101        }
102
103        (best_point, best_idx, best_dist_sq)
104    }
105
106    /// Get all vectors as a slice.
107    pub fn as_slice(&self) -> &[[f64; 2]] {
108        &self.vectors
109    }
110}
111
112/// Generate Pythagorean lattice for a given density.
113///
114/// Uses Euclid's formula: a = m² - n², b = 2mn, c = m² + n²
115fn generate_pythagorean_lattice(density: usize) -> (Vec<(u32, u32, u32)>, Vec<[f64; 2]>) {
116    let mut triples = Vec::new();
117    let mut vectors = Vec::new();
118
119    for m in 2..density {
120        for n in 1..m {
121            // Primitive triples: (m - n) odd and gcd(m, n) = 1
122            if (m - n) % 2 == 1 && gcd(m as u32, n as u32) == 1 {
123                let a = (m * m - n * n) as u32;
124                let b = (2 * m * n) as u32;
125                let c = (m * m + n * n) as u32;
126
127                triples.push((a, b, c));
128
129                // Add normalized vectors for all quadrants
130                let a_c = a as f64 / c as f64;
131                let b_c = b as f64 / c as f64;
132
133                vectors.push([a_c, b_c]);
134                vectors.push([b_c, a_c]);
135                vectors.push([-a_c, b_c]);
136                vectors.push([a_c, -b_c]);
137                vectors.push([-a_c, -b_c]);
138            }
139        }
140    }
141
142    // Add cardinal directions
143    vectors.push([1.0, 0.0]);
144    vectors.push([0.0, 1.0]);
145    vectors.push([-1.0, 0.0]);
146    vectors.push([0.0, -1.0]);
147
148    (triples, vectors)
149}
150
151/// Compute GCD using binary algorithm.
152fn gcd(a: u32, b: u32) -> u32 {
153    if a == b {
154        return a;
155    }
156    if a == 0 {
157        return b;
158    }
159    if b == 0 {
160        return a;
161    }
162
163    let shift = (a | b).trailing_zeros();
164    let mut a = a >> a.trailing_zeros();
165    let mut b = b >> b.trailing_zeros();
166
167    while a != b {
168        if a > b {
169            a -= b;
170            a >>= a.trailing_zeros();
171        } else {
172            b -= a;
173            b >>= b.trailing_zeros();
174        }
175    }
176
177    a << shift
178}
179
180/// Thread-safe cache for Pythagorean lattices.
181///
182/// Uses `Arc<RwLock>` for safe concurrent access.
183/// The cache automatically evicts old entries when capacity is reached.
184#[derive(Clone, Debug)]
185pub struct LatticeCache {
186    cache: Arc<RwLock<HashMap<usize, CachedLattice>>>,
187    capacity: usize,
188}
189
190impl LatticeCache {
191    /// Create a new lattice cache with specified capacity.
192    ///
193    /// # Arguments
194    ///
195    /// * `capacity` - Maximum number of lattices to cache
196    ///
197    /// # Example
198    ///
199    /// ```
200    /// use constraint_theory_core::cache::LatticeCache;
201    ///
202    /// let cache = LatticeCache::new(10);
203    /// ```
204    pub fn new(capacity: usize) -> Self {
205        Self {
206            cache: Arc::new(RwLock::new(HashMap::with_capacity(capacity))),
207            capacity,
208        }
209    }
210
211    /// Create a cache with default capacity (32 lattices).
212    pub fn with_default_capacity() -> Self {
213        Self::new(32)
214    }
215
216    /// Get a cached lattice or compute and cache it.
217    ///
218    /// # Arguments
219    ///
220    /// * `density` - Lattice density parameter
221    ///
222    /// # Returns
223    ///
224    /// Reference to the cached lattice
225    ///
226    /// # Example
227    ///
228    /// ```
229    /// use constraint_theory_core::cache::LatticeCache;
230    ///
231    /// let cache = LatticeCache::new(10);
232    /// let lattice = cache.get_or_compute(100);
233    ///
234    /// assert!(lattice.len() > 0);
235    /// ```
236    pub fn get_or_compute(&self, density: usize) -> CachedLattice {
237        // Try read lock first
238        {
239            let cache = self.cache.read().unwrap();
240            if let Some(lattice) = cache.get(&density) {
241                return lattice.clone();
242            }
243        }
244
245        // Need to compute - acquire write lock
246        let mut cache = self.cache.write().unwrap();
247
248        // Double-check after acquiring write lock
249        if let Some(lattice) = cache.get(&density) {
250            return lattice.clone();
251        }
252
253        // Evict old entries if at capacity
254        if cache.len() >= self.capacity {
255            // Simple FIFO eviction: remove oldest entry
256            if let Some(oldest_key) = cache.keys().next().copied() {
257                cache.remove(&oldest_key);
258            }
259        }
260
261        // Compute and cache
262        let lattice = CachedLattice::new(density);
263        cache.insert(density, lattice.clone());
264        lattice
265    }
266
267    /// Check if a density is cached.
268    pub fn contains(&self, density: usize) -> bool {
269        let cache = self.cache.read().unwrap();
270        cache.contains_key(&density)
271    }
272
273    /// Get the number of cached lattices.
274    pub fn len(&self) -> usize {
275        let cache = self.cache.read().unwrap();
276        cache.len()
277    }
278
279    /// Check if the cache is empty.
280    pub fn is_empty(&self) -> bool {
281        self.len() == 0
282    }
283
284    /// Clear the cache.
285    pub fn clear(&self) {
286        let mut cache = self.cache.write().unwrap();
287        cache.clear();
288    }
289
290    /// Precompute and cache lattices for common densities.
291    ///
292    /// # Arguments
293    ///
294    /// * `densities` - Slice of density values to precompute
295    ///
296    /// # Example
297    ///
298    /// ```
299    /// use constraint_theory_core::cache::LatticeCache;
300    ///
301    /// let cache = LatticeCache::new(10);
302    /// cache.precompute(&[50, 100, 200]);
303    ///
304    /// assert!(cache.contains(50));
305    /// assert!(cache.contains(100));
306    /// assert!(cache.contains(200));
307    /// ```
308    pub fn precompute(&self, densities: &[usize]) {
309        for &density in densities {
310            self.get_or_compute(density);
311        }
312    }
313}
314
315impl Default for LatticeCache {
316    fn default() -> Self {
317        Self::with_default_capacity()
318    }
319}
320
321/// Global lattice cache for repeated operations.
322static GLOBAL_CACHE: std::sync::OnceLock<LatticeCache> = std::sync::OnceLock::new();
323
324/// Get the global lattice cache.
325///
326/// The global cache is lazily initialized with default capacity.
327///
328/// # Example
329///
330/// ```
331/// use constraint_theory_core::cache::global_cache;
332///
333/// let cache = global_cache();
334/// let lattice = cache.get_or_compute(200);
335/// ```
336pub fn global_cache() -> &'static LatticeCache {
337    GLOBAL_CACHE.get_or_init(|| LatticeCache::with_default_capacity())
338}
339
340/// Clear the global lattice cache.
341pub fn clear_global_cache() {
342    if let Some(cache) = GLOBAL_CACHE.get() {
343        cache.clear();
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn test_cached_lattice_generation() {
353        let lattice = CachedLattice::new(50);
354        assert!(lattice.len() > 0);
355        assert!(lattice.max_hypotenuse > 0);
356        assert_eq!(lattice.density, 50);
357    }
358
359    #[test]
360    fn test_lattice_nearest() {
361        let lattice = CachedLattice::new(100);
362
363        // Query near 3-4-5 triangle
364        let (nearest, _idx, dist_sq) = lattice.nearest([0.6, 0.8]);
365
366        assert!((nearest[0] - 0.6).abs() < 0.01);
367        assert!((nearest[1] - 0.8).abs() < 0.01);
368        assert!(dist_sq < 0.001);
369    }
370
371    #[test]
372    fn test_cache_get_or_compute() {
373        let cache = LatticeCache::new(10);
374
375        // First call computes
376        let lattice1 = cache.get_or_compute(100);
377        assert!(cache.contains(100));
378
379        // Second call returns cached
380        let lattice2 = cache.get_or_compute(100);
381        assert_eq!(lattice1.len(), lattice2.len());
382    }
383
384    #[test]
385    fn test_cache_eviction() {
386        let cache = LatticeCache::new(3);
387
388        // Fill cache
389        cache.get_or_compute(10);
390        cache.get_or_compute(20);
391        cache.get_or_compute(30);
392
393        assert_eq!(cache.len(), 3);
394
395        // Add one more - should evict oldest
396        cache.get_or_compute(40);
397        assert_eq!(cache.len(), 3);
398    }
399
400    #[test]
401    fn test_cache_precompute() {
402        let cache = LatticeCache::new(10);
403        cache.precompute(&[50, 100, 200]);
404
405        assert!(cache.contains(50));
406        assert!(cache.contains(100));
407        assert!(cache.contains(200));
408    }
409
410    #[test]
411    fn test_global_cache() {
412        let cache = global_cache();
413        let lattice = cache.get_or_compute(150);
414
415        assert!(lattice.len() > 0);
416    }
417
418    #[test]
419    fn test_gcd() {
420        assert_eq!(gcd(12, 8), 4);
421        assert_eq!(gcd(17, 13), 1);
422        assert_eq!(gcd(100, 50), 50);
423    }
424
425    #[test]
426    fn test_thread_safety() {
427        use std::thread;
428
429        let cache = LatticeCache::new(10);
430
431        let handles: Vec<_> = (0..4)
432            .map(|i| {
433                let cache = cache.clone();
434                thread::spawn(move || {
435                    let density = 50 + i * 50;
436                    let lattice = cache.get_or_compute(density);
437                    lattice.len()
438                })
439            })
440            .collect();
441
442        for handle in handles {
443            let result = handle.join().unwrap();
444            assert!(result > 0);
445        }
446    }
447}