Skip to main content

penrose_memory/
tensor_tile.rs

1//! Tensor-valued Penrose tiling — reverse actualization through the lattice.
2//!
3//! Each tile carries a rank-2 tensor whose shape is determined by tile type.
4//! The five source coordinates (a–e) control orthogonal basis functions
5//! (fill modes) that generate the tensor's values.
6
7use crate::cut_and_project::{TileCoord, TileType};
8
9/// A tensor-valued Penrose tile — carries a 2D tensor field on each tile.
10#[derive(Debug, Clone)]
11pub struct TensorTile {
12    /// (x, y) position in the Penrose floor.
13    pub position: [f64; 2],
14    /// Rotation in radians.
15    pub orientation: f64,
16    /// Thick or Thin rhomb.
17    pub tile_type: TileType,
18    /// 5D lattice coordinates.
19    pub source_coords: [i32; 5],
20    /// Flat tensor data (row-major).
21    pub tensor: Vec<f32>,
22    /// (rows, cols) — varies by tile type.
23    pub tensor_shape: (usize, usize),
24}
25
26impl TensorTile {
27    /// Create a new tensor tile with the tensor initialized to zeros.
28    ///
29    /// - Thick tiles get shape `(5, 5)` to match the 5D source.
30    /// - Thin tiles get shape `(3, 8)` so the proportion 8/3 ≈ φ.
31    pub fn new(
32        source_coords: [i32; 5],
33        tile_type: TileType,
34        orientation: f64,
35        position: [f64; 2],
36    ) -> Self {
37        let shape = match tile_type {
38            TileType::Thick => (5, 5),
39            TileType::Thin => (3, 8),
40            TileType::Rejected => (1, 1), // fallback — shouldn't appear in output
41        };
42        let len = shape.0 * shape.1;
43        Self {
44            position,
45            orientation,
46            tile_type,
47            source_coords,
48            tensor: vec![0.0f32; len],
49            tensor_shape: shape,
50        }
51    }
52
53    /// Fill the tensor using five orthogonal basis modes controlled by the
54    /// five source coordinates.
55    ///
56    /// | coord | mode | description |
57    /// |-------|------|-------------|
58    /// | a (0) | constant | base intensity |
59    /// | b (1) | linear gradient along rows | |
60    /// | c (2) | sinusoidal along columns | |
61    /// | d (3) | pseudo-random texture seed | |
62    /// | e (4) | phase shift | |
63    pub fn fill_from_source(&mut self) {
64        let (rows, cols) = self.tensor_shape;
65        let src = self.source_coords;
66
67        // Normalize source coordinates to [0, 1] range.
68        let a = (src[0].unsigned_abs() as f32).max(1.0) / 100.0;
69        let b = src[1] as f32;
70        let c = (src[2].unsigned_abs() as f32).max(1.0);
71        let d = src[3].unsigned_abs() as u32;
72        let e = src[4] as f32;
73
74        for i in 0..rows {
75            for j in 0..cols {
76                let idx = i * cols + j;
77                let i_f = i as f32;
78                let j_f = j as f32;
79                let m = rows as f32;
80                let n = cols as f32;
81
82                // Mode 0 (a): constant fill based on source[0] normalized.
83                let mode_a = a * (src[0].unsigned_abs() as f32 + 1.0) / 10.0;
84
85                // Mode 1 (b): linear gradient along rows based on source[1].
86                let mode_b = b * i_f / m;
87
88                // Mode 2 (c): sinusoidal along columns based on source[2].
89                let mode_c = (2.0 * std::f32::consts::PI * c * j_f / n).sin();
90
91                // Mode 3 (d): pseudo-random seed based on source[3].
92                let hash = Self::simple_hash(i as u32, j as u32, d);
93                let mode_d = (hash as f32) / (u32::MAX as f32);
94
95                // Mode 4 (e): phase shift based on source[4].
96                let mode_e = (2.0 * std::f32::consts::PI * c * i_f / m + e / 10.0).sin();
97
98                // Combine all modes additively.
99                self.tensor[idx] = mode_a + mode_b + mode_c + mode_d + mode_e;
100            }
101        }
102    }
103
104    /// Simple deterministic hash for pseudo-random texture generation.
105    fn simple_hash(i: u32, j: u32, seed: u32) -> u32 {
106        // FNV-1a–inspired mixing
107        let mut h = 2166136261u32;
108        h ^= i.wrapping_mul(seed.wrapping_add(1));
109        h = h.wrapping_mul(16777619);
110        h ^= j.wrapping_mul(seed.wrapping_add(7));
111        h = h.wrapping_mul(16777619);
112        h ^= seed;
113        h = h.wrapping_mul(16777619);
114        h
115    }
116
117    /// Zero out tensor values below the given threshold.
118    pub fn apply_threshold(&mut self, threshold: f32) {
119        for v in &mut self.tensor {
120            if *v < threshold {
121                *v = 0.0;
122            }
123        }
124    }
125
126    /// Index into the flat tensor at (row, col).
127    pub fn tensor_at(&self, row: usize, col: usize) -> f32 {
128        let (rows, cols) = self.tensor_shape;
129        assert!(row < rows && col < cols, "index out of bounds");
130        self.tensor[row * cols + col]
131    }
132
133    /// L1 norm — sum of absolute values (constraint energy).
134    pub fn l1_norm(&self) -> f32 {
135        self.tensor.iter().map(|v| v.abs()).sum()
136    }
137
138    /// L2 norm — Euclidean norm (signal strength).
139    pub fn l2_norm(&self) -> f32 {
140        let sum_sq: f32 = self.tensor.iter().map(|v| v * v).sum();
141        sum_sq.sqrt()
142    }
143
144    /// Number of elements in the tensor.
145    pub fn tensor_len(&self) -> usize {
146        self.tensor_shape.0 * self.tensor_shape.1
147    }
148}
149
150// ──────────────────────────────────────────────────────────────────────
151// TensorTiling — collection of tensor tiles with adjacency info
152// ──────────────────────────────────────────────────────────────────────
153
154/// A complete tensor-valued tiling with adjacency information.
155#[derive(Debug, Clone)]
156pub struct TensorTiling {
157    pub tiles: Vec<TensorTile>,
158    /// (tile_i, tile_j, shared_edge_orientation in radians).
159    pub adjacency: Vec<(usize, usize, f64)>,
160}
161
162impl TensorTiling {
163    /// Create a new tiling, auto-detecting adjacency from tile positions.
164    ///
165    /// Two tiles are considered adjacent if their centres are within
166    /// `2.0` units of each other (typical Penrose edge length ≈ 1.0).
167    pub fn new(tiles: Vec<TensorTile>) -> Self {
168        let adjacency = Self::detect_adjacency(&tiles);
169        Self { tiles, adjacency }
170    }
171
172    /// Detect adjacency: tiles within threshold distance share an edge.
173    fn detect_adjacency(tiles: &[TensorTile]) -> Vec<(usize, usize, f64)> {
174        let threshold = 2.0;
175        let threshold_sq = threshold * threshold;
176        let mut edges = Vec::new();
177
178        for i in 0..tiles.len() {
179            for j in (i + 1)..tiles.len() {
180                let dx = tiles[i].position[0] - tiles[j].position[0];
181                let dy = tiles[i].position[1] - tiles[j].position[1];
182                let dist_sq = dx * dx + dy * dy;
183                if dist_sq < threshold_sq {
184                    // Shared edge orientation = angle of the line connecting centres.
185                    let orientation = dy.atan2(dx);
186                    edges.push((i, j, orientation));
187                }
188            }
189        }
190        edges
191    }
192
193    /// Apply a function to every tile in the tiling.
194    pub fn apply_kernel<F>(&mut self, f: F)
195    where
196        F: Fn(&mut TensorTile),
197    {
198        for tile in &mut self.tiles {
199            f(tile);
200        }
201    }
202
203    /// Constraint check: sum of L1 border mismatches between adjacent tiles.
204    ///
205    /// For each pair of adjacent tiles, compare the last row/first row (or
206    /// last col/first col depending on orientation). The mismatch is the L1
207    /// distance between these border vectors.
208    pub fn constraint_check(&self) -> f32 {
209        let mut total_mismatch = 0.0f32;
210
211        for &(i, j, _orientation) in &self.adjacency {
212            let tile_a = &self.tiles[i];
213            let tile_b = &self.tiles[j];
214            let (rows_a, cols_a) = tile_a.tensor_shape;
215            let (rows_b, cols_b) = tile_b.tensor_shape;
216
217            // Use the narrower dimension for comparison.
218            let border_len = cols_a.min(cols_b);
219
220            // Compare tile A's last row with tile B's first row.
221            for col in 0..border_len {
222                let va = tile_a.tensor_at(rows_a - 1, col.min(cols_a - 1));
223                let vb = tile_b.tensor_at(0, col.min(cols_b - 1));
224                total_mismatch += (va - vb).abs();
225            }
226
227            // Also compare first columns if shapes allow.
228            let col_border_len = rows_a.min(rows_b);
229            for row in 0..col_border_len {
230                let va = tile_a.tensor_at(row.min(rows_a - 1), cols_a - 1);
231                let vb = tile_b.tensor_at(row.min(rows_b - 1), 0);
232                total_mismatch += (va - vb).abs();
233            }
234        }
235
236        total_mismatch
237    }
238}
239
240// ──────────────────────────────────────────────────────────────────────
241// Top-level generation function
242// ──────────────────────────────────────────────────────────────────────
243
244/// Generate a tensor tiling from 5D lattice points and their projected tiles.
245///
246/// Takes 5D lattice points and baseline `TileCoord`s (from the cut-and-project
247/// compiler), lifts them into `TensorTile`s with filled tensors.
248pub fn generate_tensor_tiling(
249    _lattice_points: &[[i32; 5]],
250    baseline_tiles: &[TileCoord],
251) -> TensorTiling {
252    let mut tensor_tiles = Vec::with_capacity(baseline_tiles.len());
253
254    for tc in baseline_tiles {
255        // Convert source_coords Vec<i32> to [i32; 5].
256        let mut coords = [0i32; 5];
257        for (k, &v) in tc.source_coords.iter().enumerate().take(5) {
258            coords[k] = v;
259        }
260
261        let mut tt = TensorTile::new(
262            coords,
263            tc.tile_type,
264            0.0, // orientation derived from position for now
265            [tc.x, tc.y],
266        );
267        tt.fill_from_source();
268        tensor_tiles.push(tt);
269    }
270
271    TensorTiling::new(tensor_tiles)
272}
273
274// ──────────────────────────────────────────────────────────────────────
275// Tests
276// ──────────────────────────────────────────────────────────────────────
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281    use crate::cut_and_project::{CutAndProjectCompiler, TileType};
282
283    #[test]
284    fn test_tensor_tile_creation() {
285        let tile = TensorTile::new(
286            [1, 2, 3, 4, 5],
287            TileType::Thick,
288            0.0,
289            [1.0, 2.0],
290        );
291        assert_eq!(tile.tensor_shape, (5, 5));
292        assert_eq!(tile.tensor.len(), 25);
293        // All zeros initially.
294        assert!(tile.tensor.iter().all(|&v| v == 0.0));
295    }
296
297    #[test]
298    fn test_tensor_tile_creation_thin() {
299        let tile = TensorTile::new(
300            [1, 2, 3, 4, 5],
301            TileType::Thin,
302            0.0,
303            [0.0, 0.0],
304        );
305        assert_eq!(tile.tensor_shape, (3, 8));
306        assert_eq!(tile.tensor.len(), 24);
307    }
308
309    #[test]
310    fn test_tensor_tile_fill_modes() {
311        // Create five tiles varying only one source coordinate at a time.
312        // Each should produce different tensors.
313        let bases: [[i32; 5]; 5] = [
314            [10, 0, 0, 0, 0],
315            [0, 10, 0, 0, 0],
316            [0, 0, 10, 0, 0],
317            [0, 0, 0, 10, 0],
318            [0, 0, 0, 0, 10],
319        ];
320
321        let filled: Vec<Vec<f32>> = bases
322            .iter()
323            .map(|&coords| {
324                let mut tile =
325                    TensorTile::new(coords, TileType::Thick, 0.0, [0.0, 0.0]);
326                tile.fill_from_source();
327                tile.tensor.clone()
328            })
329            .collect();
330
331        // Each pair of fill modes should differ.
332        for i in 0..filled.len() {
333            for j in (i + 1)..filled.len() {
334                assert_ne!(
335                    filled[i], filled[j],
336                    "Fill modes {} and {} produced identical tensors",
337                    i, j
338                );
339            }
340        }
341    }
342
343    #[test]
344    fn test_threshold_filter() {
345        let mut tile = TensorTile::new(
346            [5, 5, 5, 5, 5],
347            TileType::Thick,
348            0.0,
349            [0.0, 0.0],
350        );
351        tile.fill_from_source();
352
353        // Remember which values were above/below 0.5.
354        let before = tile.tensor.clone();
355        tile.apply_threshold(0.5);
356
357        // Every value below 0.5 should now be 0.
358        for (idx, &v) in tile.tensor.iter().enumerate() {
359            if before[idx] < 0.5 {
360                assert_eq!(v, 0.0, "value {} was below threshold but not zeroed", before[idx]);
361            } else {
362                assert_eq!(v, before[idx], "value {} was above threshold but changed", before[idx]);
363            }
364        }
365    }
366
367    #[test]
368    fn test_tiling_adjacency() {
369        // Two tiles very close together should be detected as adjacent.
370        let tiles = vec![
371            TensorTile::new([1, 0, 0, 0, 0], TileType::Thick, 0.0, [0.0, 0.0]),
372            TensorTile::new([0, 1, 0, 0, 0], TileType::Thick, 0.0, [0.5, 0.5]),
373        ];
374        let tiling = TensorTiling::new(tiles);
375        assert!(
376            !tiling.adjacency.is_empty(),
377            "Two close tiles should detect adjacency"
378        );
379    }
380
381    #[test]
382    fn test_tiling_no_adjacency_far_tiles() {
383        // Two tiles far apart should NOT be adjacent.
384        let tiles = vec![
385            TensorTile::new([1, 0, 0, 0, 0], TileType::Thick, 0.0, [0.0, 0.0]),
386            TensorTile::new([0, 1, 0, 0, 0], TileType::Thick, 0.0, [100.0, 100.0]),
387        ];
388        let tiling = TensorTiling::new(tiles);
389        assert!(
390            tiling.adjacency.is_empty(),
391            "Far tiles should not be adjacent"
392        );
393    }
394
395    #[test]
396    fn test_constraint_check_identical_vs_different() {
397        // Two identical tiles: low mismatch.
398        let mut tile_a =
399            TensorTile::new([2, 2, 2, 2, 2], TileType::Thick, 0.0, [0.0, 0.0]);
400        tile_a.fill_from_source();
401
402        // Clone for identical comparison.
403        let mut tile_b_identical = tile_a.clone();
404        tile_b_identical.position = [0.5, 0.5]; // close enough for adjacency
405
406        let tiling_identical = TensorTiling::new(vec![tile_a.clone(), tile_b_identical]);
407        let mismatch_identical = tiling_identical.constraint_check();
408
409        // Two very different tiles: higher mismatch.
410        let mut tile_c =
411            TensorTile::new([50, 50, 50, 50, 50], TileType::Thick, 0.0, [0.0, 0.0]);
412        tile_c.fill_from_source();
413        let mut tile_d = tile_c.clone();
414        tile_d.position = [0.5, 0.5];
415
416        let tiling_different = TensorTiling::new(vec![tile_a, tile_d]);
417        let mismatch_different = tiling_different.constraint_check();
418
419        // Different tiles should have mismatch ≥ identical tiles.
420        assert!(
421            mismatch_different >= mismatch_identical,
422            "Different tiles (mismatch={}) should have >= mismatch than identical ({})",
423            mismatch_different,
424            mismatch_identical,
425        );
426    }
427
428    #[test]
429    fn test_norms() {
430        let mut tile =
431            TensorTile::new([3, 3, 3, 3, 3], TileType::Thick, 0.0, [0.0, 0.0]);
432        tile.fill_from_source();
433        let l1 = tile.l1_norm();
434        let l2 = tile.l2_norm();
435        assert!(l1 > 0.0, "L1 norm should be positive after fill");
436        assert!(l2 > 0.0, "L2 norm should be positive after fill");
437        // L1 ≥ L2 always holds.
438        assert!(l1 >= l2, "L1 ({}) should be >= L2 ({})", l1, l2);
439    }
440
441    #[test]
442    fn test_generate_tensor_tiling() {
443        let compiler = CutAndProjectCompiler::new(5, 2).with_golden_projection();
444        let baseline = compiler.compile(2);
445        if baseline.is_empty() {
446            return; // range too small to get tiles — not an error
447        }
448        let lattice: Vec<[i32; 5]> = baseline
449            .iter()
450            .map(|tc| {
451                let mut c = [0i32; 5];
452                for (k, &v) in tc.source_coords.iter().enumerate().take(5) {
453                    c[k] = v;
454                }
455                c
456            })
457            .collect();
458        let tiling = generate_tensor_tiling(&lattice, &baseline);
459        assert_eq!(tiling.tiles.len(), baseline.len());
460        // All tensors should be filled (not all zeros).
461        let any_filled = tiling
462            .tiles
463            .iter()
464            .any(|t| t.tensor.iter().any(|&v| v != 0.0));
465        assert!(any_filled, "At least one tensor should have non-zero values");
466    }
467}