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}