1pub mod cut_and_project;
27pub mod compiler;
28pub mod tensor_tile;
29
30const PHI: f64 = 1.618033988749895;
31const INV_PHI: f64 = 0.618033988749895;
32const GOLDEN_ANGLE: f64 = 2.399963229728653; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
36pub enum TileLifecycle {
37 Active,
38 Superseded,
39 Retracted,
40}
41
42impl std::fmt::Display for TileLifecycle {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 TileLifecycle::Active => write!(f, "Active"),
46 TileLifecycle::Superseded => write!(f, "Superseded"),
47 TileLifecycle::Retracted => write!(f, "Retracted"),
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct LamportClock {
55 time: u64,
56}
57
58impl LamportClock {
59 pub fn new() -> Self { Self { time: 0 } }
60 pub fn tick(&mut self) -> u64 { self.time += 1; self.time }
61 pub fn merge(&mut self, remote: u64) -> u64 { self.time = self.time.max(remote) + 1; self.time }
62 pub fn now(&self) -> u64 { self.time }
63}
64
65#[derive(Debug, Clone)]
67pub struct MemoryPrediction {
68 pub predicted_tile_id: Option<u64>,
69 pub predicted_heading: f64,
70 pub predicted_distance: f64,
71 pub lamport: u64,
72 pub confirmed: bool,
73 pub actual_tile_id: Option<u64>,
74 pub actual_distance: Option<f64>,
75}
76
77#[derive(Debug, Clone)]
79pub struct RecallResult {
80 pub tile_id: u64,
81 pub content: u64,
82 pub confidence: f64,
83 pub distance: f64,
84 pub heading: f64,
85}
86
87#[derive(Debug, Clone)]
89#[allow(dead_code)]
90struct Tile {
91 tile_id: u64,
92 content: u64,
93 x: f64,
94 y: f64,
95 color: u8, level: u32, lifecycle: TileLifecycle,
98 lamport: u64,
99}
100
101pub struct PenroseMemory {
107 embedding_dim: usize,
108 tiles: Vec<Tile>,
109 next_id: u64,
110}
111
112impl PenroseMemory {
113 pub fn new(embedding_dim: usize) -> Self {
115 Self {
116 embedding_dim,
117 tiles: Vec::new(),
118 next_id: 1,
119 }
120 }
121
122 fn project_to_2d(&self, embedding: &[f64]) -> (f64, f64) {
127 let mut x = 0.0_f64;
128 let mut y = 0.0_f64;
129 let dim = self.embedding_dim.min(embedding.len());
130
131 for i in 0..dim {
132 let val = if i < embedding.len() { embedding[i] } else { 0.0 };
133 let angle = (i as f64) * GOLDEN_ANGLE;
134 let magnitude = val.abs();
135 x += magnitude * angle.cos();
136 y += magnitude * angle.sin();
137 }
138
139 let scale = if dim > 0 { 1.0 / (dim as f64).sqrt() } else { 1.0 };
141 (x * scale, y * scale)
142 }
143
144 fn tile_bit(&self, qx: i64, qy: i64) -> bool {
149 let hash = (qx.wrapping_mul(0x9E3779B97F4A7C15u64 as i64))
151 .wrapping_add(qy.wrapping_mul(0x517CC1B727220A95u64 as i64));
152 let idx = (hash.wrapping_abs() % 10000) as u64;
153
154 let current = ((idx as f64) * INV_PHI).floor() as u64;
155 let next = (((idx + 1) as f64) * INV_PHI).floor() as u64;
156 next != current
157 }
158
159 fn three_color(&self, qx: i64, qy: i64) -> u8 {
165 let hash = (qx.wrapping_mul(0x517CC1B727220A95u64 as i64))
166 .wrapping_add(qy.wrapping_mul(0x9E3779B97F4A7C15u64 as i64));
167 (hash.wrapping_abs() % 3) as u8
168 }
169
170 fn quantize(&self, x: f64, y: f64) -> (i64, i64) {
172 let scale = PHI;
173 (
174 (x / scale).round() as i64,
175 (y / scale).round() as i64,
176 )
177 }
178
179 fn euclidean_distance(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
181 ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
182 }
183
184 fn heading_to(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
186 (y2 - y1).atan2(x2 - x1)
187 }
188
189 pub fn store(&mut self, embedding: &[f64], content: u64) -> u64 {
194 let (x, y) = self.project_to_2d(embedding);
195 let (qx, qy) = self.quantize(x, y);
196 let color = self.three_color(qx, qy);
197 let id = self.next_id;
198 self.next_id += 1;
199
200 self.tiles.push(Tile {
201 tile_id: id,
202 content,
203 x,
204 y,
205 color,
206 level: 0,
207 lifecycle: TileLifecycle::Active,
208 lamport: 0,
209 });
210
211 id
212 }
213
214 pub fn recall(&self, query: &[f64], max_steps: usize) -> Vec<RecallResult> {
223 if self.tiles.is_empty() {
224 return Vec::new();
225 }
226
227 let (qx, qy) = self.project_to_2d(query);
228
229 let mut results: Vec<RecallResult> = self.tiles.iter()
230 .filter(|t| t.lifecycle == TileLifecycle::Active)
231 .map(|tile| {
232 let dist = Self::euclidean_distance(qx, qy, tile.x, tile.y);
233 let heading = Self::heading_to(qx, qy, tile.x, tile.y);
234
235 let sigma = 2.0; let confidence = (-dist * dist / (2.0 * sigma * sigma)).exp();
239
240 let mut path_confidence = 1.0;
242 if max_steps > 0 && dist > 0.0 {
243 let mut verified = 0usize;
244 let mut total = 0usize;
245 for step in 1..=max_steps {
246 let t = step as f64 / (max_steps + 1) as f64;
247 let ix = qx + t * (tile.x - qx);
248 let iy = qy + t * (tile.y - qy);
249 let (iqx, iqy) = self.quantize(ix, iy);
250
251 let bit = self.tile_bit(iqx, iqy);
253 let neighbors = [
254 (iqx + 1, iqy), (iqx - 1, iqy),
255 (iqx, iqy + 1), (iqx, iqy - 1),
256 ];
257 let any_diff = neighbors.iter().any(|&(nx, ny)| self.tile_bit(nx, ny) != bit);
258 if any_diff { verified += 1; }
259 total += 1;
260 }
261 path_confidence = if total > 0 { verified as f64 / total as f64 } else { 1.0 };
262 }
263
264 RecallResult {
265 tile_id: tile.tile_id,
266 content: tile.content,
267 confidence: confidence * path_confidence,
268 distance: dist,
269 heading,
270 }
271 }).collect();
272
273 results.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
275 results
276 }
277
278 pub fn navigate(&self, tile_id: u64, distance: f64, heading: f64) -> Vec<u64> {
284 let start = match self.tiles.iter().find(|t| t.tile_id == tile_id) {
285 Some(t) => (t.x, t.y),
286 None => return Vec::new(),
287 };
288
289 let dest_x = start.0 + distance * heading.cos();
291 let dest_y = start.1 + distance * heading.sin();
292
293 let arrival_radius = PHI * 0.5;
295
296 self.tiles.iter()
297 .filter(|t| {
298 let d = Self::euclidean_distance(dest_x, dest_y, t.x, t.y);
299 d <= arrival_radius
300 })
301 .map(|t| t.tile_id)
302 .collect()
303 }
304
305 pub fn consolidate(&mut self) {
311 if self.tiles.len() < 2 {
312 return;
313 }
314
315 let merge_distance = PHI; let mut merged: Vec<bool> = vec![false; self.tiles.len()];
319 let mut new_tiles: Vec<Tile> = Vec::new();
320
321 for i in 0..self.tiles.len() {
322 if merged[i] || self.tiles[i].level > 0 {
323 continue;
324 }
325
326 let mut cluster_x = self.tiles[i].x;
327 let mut cluster_y = self.tiles[i].y;
328 let mut cluster_content = self.tiles[i].content;
329 let mut cluster_size = 1usize;
330 let mut cluster_ids = vec![i];
331
332 for j in (i + 1)..self.tiles.len() {
333 if merged[j] || self.tiles[j].level > 0 {
334 continue;
335 }
336 let d = Self::euclidean_distance(cluster_x, cluster_y, self.tiles[j].x, self.tiles[j].y);
337 if d < merge_distance {
338 cluster_content ^= self.tiles[j].content;
339 cluster_x = (cluster_x * cluster_size as f64 + self.tiles[j].x) / (cluster_size + 1) as f64;
340 cluster_y = (cluster_y * cluster_size as f64 + self.tiles[j].y) / (cluster_size + 1) as f64;
341 cluster_size += 1;
342 cluster_ids.push(j);
343 }
344 }
345
346 if cluster_size > 1 {
347 for &idx in &cluster_ids {
348 merged[idx] = true;
349 }
350 let (qx, qy) = self.quantize(cluster_x, cluster_y);
351 new_tiles.push(Tile {
352 tile_id: self.next_id,
353 content: cluster_content,
354 x: cluster_x,
355 y: cluster_y,
356 color: self.three_color(qx, qy),
357 level: 1,
358 lifecycle: TileLifecycle::Active,
359 lamport: 0,
360 });
361 self.next_id += 1;
362 }
363 }
364
365 let mut kept: Vec<Tile> = self.tiles.drain(..)
367 .enumerate()
368 .filter(|(i, _)| !merged[*i])
369 .map(|(_, t)| t)
370 .collect();
371 kept.extend(new_tiles);
372 self.tiles = kept;
373 }
374
375 pub fn len(&self) -> usize {
377 self.tiles.len()
378 }
379
380 pub fn matching_rule_holds(&self, qx: i64, qy: i64) -> bool {
383 let bit = self.tile_bit(qx, qy);
384 let neighbors = [
385 (qx + 1, qy), (qx - 1, qy),
386 (qx, qy + 1), (qx, qy - 1),
387 (qx + 1, qy - 1), (qx - 1, qy + 1),
388 ];
389 neighbors.iter().any(|&(nx, ny)| self.tile_bit(nx, ny) != bit)
390 }
391
392 pub fn get_tile_bit(&self, qx: i64, qy: i64) -> bool {
394 self.tile_bit(qx, qy)
395 }
396
397 pub fn get_color(&self, qx: i64, qy: i64) -> u8 {
399 self.three_color(qx, qy)
400 }
401
402 pub fn is_empty(&self) -> bool {
404 self.tiles.is_empty()
405 }
406
407 pub fn supersede_tile(&mut self, tile_id: u64) -> Option<u64> {
411 let tile = self.tiles.iter_mut().find(|t| t.tile_id == tile_id)?;
412 if tile.lifecycle != TileLifecycle::Active {
413 return None;
414 }
415 tile.lifecycle = TileLifecycle::Superseded;
416 Some(tile.content)
417 }
418
419 pub fn retract_tile(&mut self, tile_id: u64) -> bool {
421 if let Some(tile) = self.tiles.iter_mut().find(|t| t.tile_id == tile_id) {
422 if tile.lifecycle == TileLifecycle::Active {
423 tile.lifecycle = TileLifecycle::Retracted;
424 return true;
425 }
426 }
427 false
428 }
429
430 pub fn active_tile_ids(&self) -> Vec<u64> {
432 self.tiles.iter()
433 .filter(|t| t.lifecycle == TileLifecycle::Active)
434 .map(|t| t.tile_id)
435 .collect()
436 }
437
438 pub fn lifecycle_stats(&self) -> (usize, usize, usize) {
440 let active = self.tiles.iter().filter(|t| t.lifecycle == TileLifecycle::Active).count();
441 let superseded = self.tiles.iter().filter(|t| t.lifecycle == TileLifecycle::Superseded).count();
442 let retracted = self.tiles.iter().filter(|t| t.lifecycle == TileLifecycle::Retracted).count();
443 (active, superseded, retracted)
444 }
445
446 pub fn predict_recall(&self, query: &[f64], clock: &mut LamportClock) -> MemoryPrediction {
451 let (qx, qy) = self.project_to_2d(query);
452
453 let best = self.tiles.iter()
455 .filter(|t| t.lifecycle == TileLifecycle::Active)
456 .min_by(|a, b| {
457 let da = Self::euclidean_distance(qx, qy, a.x, a.y);
458 let db = Self::euclidean_distance(qx, qy, b.x, b.y);
459 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
460 });
461
462 match best {
463 Some(tile) => MemoryPrediction {
464 predicted_tile_id: Some(tile.tile_id),
465 predicted_heading: Self::heading_to(qx, qy, tile.x, tile.y),
466 predicted_distance: Self::euclidean_distance(qx, qy, tile.x, tile.y),
467 lamport: clock.tick(),
468 confirmed: false,
469 actual_tile_id: None,
470 actual_distance: None,
471 },
472 None => MemoryPrediction {
473 predicted_tile_id: None,
474 predicted_heading: 0.0,
475 predicted_distance: f64::MAX,
476 lamport: clock.tick(),
477 confirmed: false,
478 actual_tile_id: None,
479 actual_distance: None,
480 },
481 }
482 }
483
484 pub fn confirm_prediction(&self, prediction: &mut MemoryPrediction, actual: &[RecallResult]) -> bool {
486 if let (Some(pred_id), Some(first)) = (prediction.predicted_tile_id, actual.first()) {
487 prediction.actual_tile_id = Some(first.tile_id);
488 prediction.actual_distance = Some(first.distance);
489 prediction.confirmed = pred_id == first.tile_id;
490 prediction.confirmed
491 } else {
492 prediction.confirmed = actual.is_empty() && prediction.predicted_tile_id.is_none();
493 prediction.confirmed
494 }
495 }
496}
497
498impl Default for PenroseMemory {
499 fn default() -> Self {
500 Self::new(1536)
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[test]
510 fn test_store_recall_roundtrip() {
511 let mut pm = PenroseMemory::new(4);
512 let id = pm.store(&[0.1, 0.2, 0.3, 0.4], 42);
513 let results = pm.recall(&[0.1, 0.2, 0.3, 0.4], 3);
514 assert!(!results.is_empty());
515 assert_eq!(results[0].content, 42);
516 assert_eq!(results[0].tile_id, id);
517 }
518
519 #[test]
521 fn test_different_embeddings_different_tiles() {
522 let mut pm = PenroseMemory::new(4);
523 let id1 = pm.store(&[1.0, 0.0, 0.0, 0.0], 1);
524 let id2 = pm.store(&[0.0, 0.0, 0.0, 1.0], 2);
525 assert_ne!(id1, id2);
526
527 let results1 = pm.recall(&[1.0, 0.0, 0.0, 0.0], 3);
528 let results2 = pm.recall(&[0.0, 0.0, 0.0, 1.0], 3);
529 assert_eq!(results1[0].content, 1);
530 assert_eq!(results2[0].content, 2);
531 }
532
533 #[test]
535 fn test_nearby_embeddings_nearby_tiles() {
536 let mut pm = PenroseMemory::new(4);
537 pm.store(&[1.0, 2.0, 3.0, 4.0], 100);
538
539 let results = pm.recall(&[1.01, 2.01, 3.01, 4.01], 3);
541 assert!(!results.is_empty());
542 assert!(results[0].distance < 0.1, "Nearby embeddings should be close");
543 assert_eq!(results[0].content, 100);
544 }
545
546 #[test]
548 fn test_matching_rules_hold() {
549 let pm = PenroseMemory::new(4);
550 let mut valid = 0usize;
551 let total = 1000;
552 for q in 0..total {
553 if pm.matching_rule_holds(q, 0) {
554 valid += 1;
555 }
556 }
557 let ratio = valid as f64 / total as f64;
559 assert!(ratio > 0.75, "Matching rules should hold at most positions, got {}", ratio);
560 }
561
562 #[test]
564 fn test_fibonacci_ratio() {
565 let pm = PenroseMemory::new(4);
566 let n = 10000;
567 let thick_count = (0..n).filter(|&q| pm.tile_bit(q, 0)).count();
568 let ratio = thick_count as f64 / n as f64;
569 assert!(
570 (ratio - INV_PHI).abs() < 0.03,
571 "Ratio {:.4} should approach 1/φ ≈ {:.4}",
572 ratio, INV_PHI
573 );
574 }
575
576 #[test]
578 fn test_three_coloring_covers_all() {
579 let pm = PenroseMemory::new(4);
580 let mut colors = [false; 3];
581 for q in 0..300 {
582 let c = pm.get_color(q, 0) as usize;
583 assert!(c < 3);
584 colors[c] = true;
585 }
586 assert!(colors[0] && colors[1] && colors[2], "All 3 colors should appear");
588 }
589
590 #[test]
592 fn test_consolidation_reduces_count() {
593 let mut pm = PenroseMemory::new(4);
594 for i in 0..20u64 {
596 let val = 1.0 + (i as f64) * 0.001;
597 pm.store(&[val, 2.0, 3.0, 4.0], i);
598 }
599 let before = pm.len();
600 pm.consolidate();
601 let after = pm.len();
602 assert!(after < before, "Consolidation should reduce tile count: {} -> {}", before, after);
603 }
604
605 #[test]
607 fn test_region_fingerprints_unique() {
608 let pm = PenroseMemory::new(4);
609 let mut fingerprints = std::collections::HashSet::new();
610 for q in 0..100i64 {
611 let mut fp = 0u16;
613 for dx in -1..=1 {
614 for dy in -1..=1 {
615 let bit = pm.tile_bit(q + dx, dy) as u16;
616 fp = (fp << 1) | bit;
617 }
618 }
619 fingerprints.insert(fp);
620 }
621 assert!(fingerprints.len() > 50, "Should have many unique fingerprints, got {}", fingerprints.len());
623 }
624
625 #[test]
627 fn test_aperiodic_patterns() {
628 let pm = PenroseMemory::new(4);
629 let pattern: Vec<bool> = (0..100).map(|q| pm.tile_bit(q, 0)).collect();
630
631 for period in 2..20 {
633 let mut periodic = true;
634 for i in period..100 {
635 if pattern[i] != pattern[i - period] {
636 periodic = false;
637 break;
638 }
639 }
640 assert!(!periodic, "Pattern should not be periodic with period {}", period);
641 }
642 }
643
644 #[test]
646 fn test_empty_recall() {
647 let pm = PenroseMemory::new(4);
648 let results = pm.recall(&[1.0, 2.0, 3.0, 4.0], 5);
649 assert!(results.is_empty());
650 }
651
652 #[test]
654 fn test_dead_reckoning_navigation() {
655 let mut pm = PenroseMemory::new(4);
656 let id = pm.store(&[1.0, 0.0, 0.0, 0.0], 10);
657 let results = pm.navigate(id, 0.0, 0.0);
659 assert!(results.contains(&id), "Zero-distance navigation should find the same tile");
660 }
661
662 #[test]
664 fn test_large_embedding() {
665 let mut pm = PenroseMemory::new(1536);
666 let embedding: Vec<f64> = (0..1536).map(|i| (i as f64).sin() * 0.1).collect();
667 let id = pm.store(&embedding, 999);
668 let results = pm.recall(&embedding, 3);
669 assert!(!results.is_empty());
670 assert_eq!(results[0].content, 999);
671 assert_eq!(results[0].tile_id, id);
672 }
673
674 #[test]
676 fn test_store_many_memories() {
677 let mut pm = PenroseMemory::new(8);
678 for i in 0..1000u64 {
679 let emb: Vec<f64> = (0..8).map(|j| (i * 8 + j) as f64 * 0.001).collect();
680 pm.store(&emb, i);
681 }
682 assert_eq!(pm.len(), 1000);
683 }
684
685 #[test]
687 fn test_confidence_decreases_with_distance() {
688 let mut pm = PenroseMemory::new(4);
689 pm.store(&[1.0, 2.0, 3.0, 4.0], 42);
690
691 let close = pm.recall(&[1.0, 2.0, 3.0, 4.0], 0);
693 let far = pm.recall(&[10.0, 20.0, 30.0, 40.0], 0);
695
696 assert!(!close.is_empty());
697 assert!(!far.is_empty());
698 assert!(
699 close[0].confidence > far[0].confidence,
700 "Close confidence ({}) should be > far confidence ({})",
701 close[0].confidence, far[0].confidence
702 );
703 }
704
705 #[test]
707 fn test_multi_step_recall_finds_closer() {
708 let mut pm = PenroseMemory::new(4);
709 pm.store(&[1.0, 0.0, 0.0, 0.0], 10);
710 pm.store(&[0.0, 0.0, 0.0, 1.0], 20);
711
712 let results = pm.recall(&[0.9, 0.0, 0.0, 0.0], 5);
713 assert!(!results.is_empty());
714 assert_eq!(results[0].content, 10, "Should find closest match first");
715 }
716
717 #[test]
720 fn test_new_tile_is_active() {
721 let mut pm = PenroseMemory::new(4);
722 pm.store(&[1.0, 2.0, 3.0, 4.0], 42);
723 let ids = pm.active_tile_ids();
724 assert_eq!(ids.len(), 1);
725 let (active, sup, ret) = pm.lifecycle_stats();
726 assert_eq!(active, 1);
727 assert_eq!(sup, 0);
728 assert_eq!(ret, 0);
729 }
730
731 #[test]
732 fn test_supersede_excludes_from_recall() {
733 let mut pm = PenroseMemory::new(4);
734 let id1 = pm.store(&[1.0, 0.0, 0.0, 0.0], 10);
735 let _id2 = pm.store(&[0.9, 0.0, 0.0, 0.0], 20);
736
737 pm.supersede_tile(id1);
738
739 let results = pm.recall(&[1.0, 0.0, 0.0, 0.0], 3);
740 assert!(!results.iter().any(|r| r.tile_id == id1), "Superseded tile should not appear in recall");
741
742 let (active, sup, _) = pm.lifecycle_stats();
743 assert_eq!(active, 1);
744 assert_eq!(sup, 1);
745 }
746
747 #[test]
748 fn test_retract_excludes_from_recall() {
749 let mut pm = PenroseMemory::new(4);
750 let id = pm.store(&[1.0, 0.0, 0.0, 0.0], 10);
751
752 let retracted = pm.retract_tile(id);
753 assert!(retracted);
754
755 let results = pm.recall(&[1.0, 0.0, 0.0, 0.0], 3);
756 assert!(results.is_empty(), "Retracted tile should not appear in recall");
757 }
758
759 #[test]
760 fn test_cannot_supersede_retracted() {
761 let mut pm = PenroseMemory::new(4);
762 let id = pm.store(&[1.0, 2.0, 3.0, 4.0], 42);
763 pm.retract_tile(id);
764 let result = pm.supersede_tile(id);
765 assert!(result.is_none(), "Cannot supersede a retracted tile");
766 }
767
768 #[test]
771 fn test_predict_recall_finds_tile() {
772 let mut pm = PenroseMemory::new(4);
773 let id = pm.store(&[1.0, 2.0, 3.0, 4.0], 42);
774 let mut clock = LamportClock::new();
775
776 let pred = pm.predict_recall(&[1.0, 2.0, 3.0, 4.0], &mut clock);
777 assert_eq!(pred.predicted_tile_id, Some(id));
778 assert!(pred.predicted_distance < 0.01);
779 assert_eq!(pred.lamport, 1);
780 assert!(!pred.confirmed);
781 }
782
783 #[test]
784 fn test_confirm_prediction_matches() {
785 let mut pm = PenroseMemory::new(4);
786 let _id = pm.store(&[1.0, 2.0, 3.0, 4.0], 42);
787 let mut clock = LamportClock::new();
788
789 let mut pred = pm.predict_recall(&[1.0, 2.0, 3.0, 4.0], &mut clock);
790 let actual = pm.recall(&[1.0, 2.0, 3.0, 4.0], 3);
791
792 let confirmed = pm.confirm_prediction(&mut pred, &actual);
793 assert!(confirmed, "Prediction should match actual recall");
794 assert!(pred.confirmed);
795 }
796
797 #[test]
798 fn test_lamport_clock_monotonic() {
799 let mut clock = LamportClock::new();
800 let t1 = clock.tick();
801 let t2 = clock.tick();
802 let t3 = clock.merge(100); assert!(t1 < t2);
804 assert_eq!(t3, 101);
805 }
806}