1pub mod cut_and_project;
27pub mod compiler;
28
29const PHI: f64 = 1.618033988749895;
30const INV_PHI: f64 = 0.618033988749895;
31const GOLDEN_ANGLE: f64 = 2.399963229728653; #[derive(Debug, Clone)]
35pub struct RecallResult {
36 pub tile_id: u64,
37 pub content: u64,
38 pub confidence: f64,
39 pub distance: f64,
40 pub heading: f64,
41}
42
43#[derive(Debug, Clone)]
45#[allow(dead_code)]
46struct Tile {
47 tile_id: u64,
48 content: u64,
49 x: f64,
50 y: f64,
51 color: u8, level: u32, }
54
55pub struct PenroseMemory {
61 embedding_dim: usize,
62 tiles: Vec<Tile>,
63 next_id: u64,
64}
65
66impl PenroseMemory {
67 pub fn new(embedding_dim: usize) -> Self {
69 Self {
70 embedding_dim,
71 tiles: Vec::new(),
72 next_id: 1,
73 }
74 }
75
76 fn project_to_2d(&self, embedding: &[f64]) -> (f64, f64) {
81 let mut x = 0.0_f64;
82 let mut y = 0.0_f64;
83 let dim = self.embedding_dim.min(embedding.len());
84
85 for i in 0..dim {
86 let val = if i < embedding.len() { embedding[i] } else { 0.0 };
87 let angle = (i as f64) * GOLDEN_ANGLE;
88 let magnitude = val.abs();
89 x += magnitude * angle.cos();
90 y += magnitude * angle.sin();
91 }
92
93 let scale = if dim > 0 { 1.0 / (dim as f64).sqrt() } else { 1.0 };
95 (x * scale, y * scale)
96 }
97
98 fn tile_bit(&self, qx: i64, qy: i64) -> bool {
103 let hash = (qx.wrapping_mul(0x9E3779B97F4A7C15u64 as i64))
105 .wrapping_add(qy.wrapping_mul(0x517CC1B727220A95u64 as i64));
106 let idx = (hash.wrapping_abs() % 10000) as u64;
107
108 let current = ((idx as f64) * INV_PHI).floor() as u64;
109 let next = (((idx + 1) as f64) * INV_PHI).floor() as u64;
110 next != current
111 }
112
113 fn three_color(&self, qx: i64, qy: i64) -> u8 {
119 let hash = (qx.wrapping_mul(0x517CC1B727220A95u64 as i64))
120 .wrapping_add(qy.wrapping_mul(0x9E3779B97F4A7C15u64 as i64));
121 (hash.wrapping_abs() % 3) as u8
122 }
123
124 fn quantize(&self, x: f64, y: f64) -> (i64, i64) {
126 let scale = PHI;
127 (
128 (x / scale).round() as i64,
129 (y / scale).round() as i64,
130 )
131 }
132
133 fn euclidean_distance(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
135 ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
136 }
137
138 fn heading_to(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
140 (y2 - y1).atan2(x2 - x1)
141 }
142
143 pub fn store(&mut self, embedding: &[f64], content: u64) -> u64 {
148 let (x, y) = self.project_to_2d(embedding);
149 let (qx, qy) = self.quantize(x, y);
150 let color = self.three_color(qx, qy);
151 let id = self.next_id;
152 self.next_id += 1;
153
154 self.tiles.push(Tile {
155 tile_id: id,
156 content,
157 x,
158 y,
159 color,
160 level: 0,
161 });
162
163 id
164 }
165
166 pub fn recall(&self, query: &[f64], max_steps: usize) -> Vec<RecallResult> {
175 if self.tiles.is_empty() {
176 return Vec::new();
177 }
178
179 let (qx, qy) = self.project_to_2d(query);
180
181 let mut results: Vec<RecallResult> = self.tiles.iter().map(|tile| {
182 let dist = Self::euclidean_distance(qx, qy, tile.x, tile.y);
183 let heading = Self::heading_to(qx, qy, tile.x, tile.y);
184
185 let sigma = 2.0; let confidence = (-dist * dist / (2.0 * sigma * sigma)).exp();
189
190 let mut path_confidence = 1.0;
192 if max_steps > 0 && dist > 0.0 {
193 let mut verified = 0usize;
194 let mut total = 0usize;
195 for step in 1..=max_steps {
196 let t = step as f64 / (max_steps + 1) as f64;
197 let ix = qx + t * (tile.x - qx);
198 let iy = qy + t * (tile.y - qy);
199 let (iqx, iqy) = self.quantize(ix, iy);
200
201 let bit = self.tile_bit(iqx, iqy);
203 let neighbors = [
204 (iqx + 1, iqy), (iqx - 1, iqy),
205 (iqx, iqy + 1), (iqx, iqy - 1),
206 ];
207 let any_diff = neighbors.iter().any(|&(nx, ny)| self.tile_bit(nx, ny) != bit);
208 if any_diff { verified += 1; }
209 total += 1;
210 }
211 path_confidence = if total > 0 { verified as f64 / total as f64 } else { 1.0 };
212 }
213
214 RecallResult {
215 tile_id: tile.tile_id,
216 content: tile.content,
217 confidence: confidence * path_confidence,
218 distance: dist,
219 heading,
220 }
221 }).collect();
222
223 results.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
225 results
226 }
227
228 pub fn navigate(&self, tile_id: u64, distance: f64, heading: f64) -> Vec<u64> {
234 let start = match self.tiles.iter().find(|t| t.tile_id == tile_id) {
235 Some(t) => (t.x, t.y),
236 None => return Vec::new(),
237 };
238
239 let dest_x = start.0 + distance * heading.cos();
241 let dest_y = start.1 + distance * heading.sin();
242
243 let arrival_radius = PHI * 0.5;
245
246 self.tiles.iter()
247 .filter(|t| {
248 let d = Self::euclidean_distance(dest_x, dest_y, t.x, t.y);
249 d <= arrival_radius
250 })
251 .map(|t| t.tile_id)
252 .collect()
253 }
254
255 pub fn consolidate(&mut self) {
261 if self.tiles.len() < 2 {
262 return;
263 }
264
265 let merge_distance = PHI; let mut merged: Vec<bool> = vec![false; self.tiles.len()];
269 let mut new_tiles: Vec<Tile> = Vec::new();
270
271 for i in 0..self.tiles.len() {
272 if merged[i] || self.tiles[i].level > 0 {
273 continue;
274 }
275
276 let mut cluster_x = self.tiles[i].x;
277 let mut cluster_y = self.tiles[i].y;
278 let mut cluster_content = self.tiles[i].content;
279 let mut cluster_size = 1usize;
280 let mut cluster_ids = vec![i];
281
282 for j in (i + 1)..self.tiles.len() {
283 if merged[j] || self.tiles[j].level > 0 {
284 continue;
285 }
286 let d = Self::euclidean_distance(cluster_x, cluster_y, self.tiles[j].x, self.tiles[j].y);
287 if d < merge_distance {
288 cluster_content ^= self.tiles[j].content;
289 cluster_x = (cluster_x * cluster_size as f64 + self.tiles[j].x) / (cluster_size + 1) as f64;
290 cluster_y = (cluster_y * cluster_size as f64 + self.tiles[j].y) / (cluster_size + 1) as f64;
291 cluster_size += 1;
292 cluster_ids.push(j);
293 }
294 }
295
296 if cluster_size > 1 {
297 for &idx in &cluster_ids {
298 merged[idx] = true;
299 }
300 let (qx, qy) = self.quantize(cluster_x, cluster_y);
301 new_tiles.push(Tile {
302 tile_id: self.next_id,
303 content: cluster_content,
304 x: cluster_x,
305 y: cluster_y,
306 color: self.three_color(qx, qy),
307 level: 1,
308 });
309 self.next_id += 1;
310 }
311 }
312
313 let mut kept: Vec<Tile> = self.tiles.drain(..)
315 .enumerate()
316 .filter(|(i, _)| !merged[*i])
317 .map(|(_, t)| t)
318 .collect();
319 kept.extend(new_tiles);
320 self.tiles = kept;
321 }
322
323 pub fn len(&self) -> usize {
325 self.tiles.len()
326 }
327
328 pub fn matching_rule_holds(&self, qx: i64, qy: i64) -> bool {
331 let bit = self.tile_bit(qx, qy);
332 let neighbors = [
333 (qx + 1, qy), (qx - 1, qy),
334 (qx, qy + 1), (qx, qy - 1),
335 (qx + 1, qy - 1), (qx - 1, qy + 1),
336 ];
337 neighbors.iter().any(|&(nx, ny)| self.tile_bit(nx, ny) != bit)
338 }
339
340 pub fn get_tile_bit(&self, qx: i64, qy: i64) -> bool {
342 self.tile_bit(qx, qy)
343 }
344
345 pub fn get_color(&self, qx: i64, qy: i64) -> u8 {
347 self.three_color(qx, qy)
348 }
349
350 pub fn is_empty(&self) -> bool {
352 self.tiles.is_empty()
353 }
354}
355
356impl Default for PenroseMemory {
357 fn default() -> Self {
358 Self::new(1536)
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
368 fn test_store_recall_roundtrip() {
369 let mut pm = PenroseMemory::new(4);
370 let id = pm.store(&[0.1, 0.2, 0.3, 0.4], 42);
371 let results = pm.recall(&[0.1, 0.2, 0.3, 0.4], 3);
372 assert!(!results.is_empty());
373 assert_eq!(results[0].content, 42);
374 assert_eq!(results[0].tile_id, id);
375 }
376
377 #[test]
379 fn test_different_embeddings_different_tiles() {
380 let mut pm = PenroseMemory::new(4);
381 let id1 = pm.store(&[1.0, 0.0, 0.0, 0.0], 1);
382 let id2 = pm.store(&[0.0, 0.0, 0.0, 1.0], 2);
383 assert_ne!(id1, id2);
384
385 let results1 = pm.recall(&[1.0, 0.0, 0.0, 0.0], 3);
386 let results2 = pm.recall(&[0.0, 0.0, 0.0, 1.0], 3);
387 assert_eq!(results1[0].content, 1);
388 assert_eq!(results2[0].content, 2);
389 }
390
391 #[test]
393 fn test_nearby_embeddings_nearby_tiles() {
394 let mut pm = PenroseMemory::new(4);
395 pm.store(&[1.0, 2.0, 3.0, 4.0], 100);
396
397 let results = pm.recall(&[1.01, 2.01, 3.01, 4.01], 3);
399 assert!(!results.is_empty());
400 assert!(results[0].distance < 0.1, "Nearby embeddings should be close");
401 assert_eq!(results[0].content, 100);
402 }
403
404 #[test]
406 fn test_matching_rules_hold() {
407 let pm = PenroseMemory::new(4);
408 let mut valid = 0usize;
409 let total = 1000;
410 for q in 0..total {
411 if pm.matching_rule_holds(q, 0) {
412 valid += 1;
413 }
414 }
415 let ratio = valid as f64 / total as f64;
417 assert!(ratio > 0.75, "Matching rules should hold at most positions, got {}", ratio);
418 }
419
420 #[test]
422 fn test_fibonacci_ratio() {
423 let pm = PenroseMemory::new(4);
424 let n = 10000;
425 let thick_count = (0..n).filter(|&q| pm.tile_bit(q, 0)).count();
426 let ratio = thick_count as f64 / n as f64;
427 assert!(
428 (ratio - INV_PHI).abs() < 0.03,
429 "Ratio {:.4} should approach 1/φ ≈ {:.4}",
430 ratio, INV_PHI
431 );
432 }
433
434 #[test]
436 fn test_three_coloring_covers_all() {
437 let pm = PenroseMemory::new(4);
438 let mut colors = [false; 3];
439 for q in 0..300 {
440 let c = pm.get_color(q, 0) as usize;
441 assert!(c < 3);
442 colors[c] = true;
443 }
444 assert!(colors[0] && colors[1] && colors[2], "All 3 colors should appear");
446 }
447
448 #[test]
450 fn test_consolidation_reduces_count() {
451 let mut pm = PenroseMemory::new(4);
452 for i in 0..20u64 {
454 let val = 1.0 + (i as f64) * 0.001;
455 pm.store(&[val, 2.0, 3.0, 4.0], i);
456 }
457 let before = pm.len();
458 pm.consolidate();
459 let after = pm.len();
460 assert!(after < before, "Consolidation should reduce tile count: {} -> {}", before, after);
461 }
462
463 #[test]
465 fn test_region_fingerprints_unique() {
466 let pm = PenroseMemory::new(4);
467 let mut fingerprints = std::collections::HashSet::new();
468 for q in 0..100i64 {
469 let mut fp = 0u16;
471 for dx in -1..=1 {
472 for dy in -1..=1 {
473 let bit = pm.tile_bit(q + dx, dy) as u16;
474 fp = (fp << 1) | bit;
475 }
476 }
477 fingerprints.insert(fp);
478 }
479 assert!(fingerprints.len() > 50, "Should have many unique fingerprints, got {}", fingerprints.len());
481 }
482
483 #[test]
485 fn test_aperiodic_patterns() {
486 let pm = PenroseMemory::new(4);
487 let pattern: Vec<bool> = (0..100).map(|q| pm.tile_bit(q, 0)).collect();
488
489 for period in 2..20 {
491 let mut periodic = true;
492 for i in period..100 {
493 if pattern[i] != pattern[i - period] {
494 periodic = false;
495 break;
496 }
497 }
498 assert!(!periodic, "Pattern should not be periodic with period {}", period);
499 }
500 }
501
502 #[test]
504 fn test_empty_recall() {
505 let pm = PenroseMemory::new(4);
506 let results = pm.recall(&[1.0, 2.0, 3.0, 4.0], 5);
507 assert!(results.is_empty());
508 }
509
510 #[test]
512 fn test_dead_reckoning_navigation() {
513 let mut pm = PenroseMemory::new(4);
514 let id = pm.store(&[1.0, 0.0, 0.0, 0.0], 10);
515 let results = pm.navigate(id, 0.0, 0.0);
517 assert!(results.contains(&id), "Zero-distance navigation should find the same tile");
518 }
519
520 #[test]
522 fn test_large_embedding() {
523 let mut pm = PenroseMemory::new(1536);
524 let embedding: Vec<f64> = (0..1536).map(|i| (i as f64).sin() * 0.1).collect();
525 let id = pm.store(&embedding, 999);
526 let results = pm.recall(&embedding, 3);
527 assert!(!results.is_empty());
528 assert_eq!(results[0].content, 999);
529 assert_eq!(results[0].tile_id, id);
530 }
531
532 #[test]
534 fn test_store_many_memories() {
535 let mut pm = PenroseMemory::new(8);
536 for i in 0..1000u64 {
537 let emb: Vec<f64> = (0..8).map(|j| (i * 8 + j) as f64 * 0.001).collect();
538 pm.store(&emb, i);
539 }
540 assert_eq!(pm.len(), 1000);
541 }
542
543 #[test]
545 fn test_confidence_decreases_with_distance() {
546 let mut pm = PenroseMemory::new(4);
547 pm.store(&[1.0, 2.0, 3.0, 4.0], 42);
548
549 let close = pm.recall(&[1.0, 2.0, 3.0, 4.0], 0);
551 let far = pm.recall(&[10.0, 20.0, 30.0, 40.0], 0);
553
554 assert!(!close.is_empty());
555 assert!(!far.is_empty());
556 assert!(
557 close[0].confidence > far[0].confidence,
558 "Close confidence ({}) should be > far confidence ({})",
559 close[0].confidence, far[0].confidence
560 );
561 }
562
563 #[test]
565 fn test_multi_step_recall_finds_closer() {
566 let mut pm = PenroseMemory::new(4);
567 pm.store(&[1.0, 0.0, 0.0, 0.0], 10);
568 pm.store(&[0.0, 0.0, 0.0, 1.0], 20);
569
570 let results = pm.recall(&[0.9, 0.0, 0.0, 0.0], 5);
572 assert!(!results.is_empty());
573 assert_eq!(results[0].content, 10, "Should find closest match first");
574 }
575}