Skip to main content

penrose_memory/
compiler.rs

1//! High-level fleet tiling API.
2//!
3//! Takes real agent embedding vectors, fits a PCA projection, and compiles
4//! an aperiodic tiling that assigns each agent to a unique tile.
5
6use crate::cut_and_project::{CutAndProjectCompiler, TileCoord, TileType};
7
8/// An agent's position in the fleet tiling.
9#[derive(Debug, Clone)]
10pub struct AgentTile {
11    /// Agent index in the input embedding array.
12    pub agent_index: usize,
13    /// Assigned tile coordinates.
14    pub tile: TileCoord,
15    /// Distance from the agent's embedded position to the tile centre.
16    pub assignment_distance: f64,
17}
18
19/// Complete fleet tiling result.
20#[derive(Debug, Clone)]
21pub struct FleetTiling {
22    /// Source dimension of the embedding space.
23    pub source_dim: usize,
24    /// All compiled tiles.
25    pub tiles: Vec<TileCoord>,
26    /// Agent-to-tile assignments.
27    pub assignments: Vec<AgentTile>,
28    /// Number of thick tiles.
29    pub thick_count: usize,
30    /// Number of thin tiles.
31    pub thin_count: usize,
32    /// Thick:thin ratio.
33    pub thick_thin_ratio: f64,
34}
35
36/// Compile a fleet tiling from agent embedding vectors.
37///
38/// 1. Fits a PCA projection from the data.
39/// 2. Runs cut-and-project with the learned projection.
40/// 3. Assigns each agent to the nearest compiled tile.
41///
42/// Returns a `FleetTiling` with tile assignments for each agent.
43pub fn compile_fleet_tiling(agent_embeddings: &[Vec<f64>]) -> FleetTiling {
44    assert!(
45        !agent_embeddings.is_empty(),
46        "Need at least one agent embedding"
47    );
48
49    let source_dim = agent_embeddings[0].len();
50    let target_dim = 2;
51
52    // Build compiler with PCA projection learned from agent embeddings.
53    let compiler = CutAndProjectCompiler::new(source_dim, target_dim)
54        .with_pca_projection(agent_embeddings);
55
56    // Choose lattice range based on number of agents: enough tiles to cover.
57    let lattice_range = ((agent_embeddings.len() as f64).sqrt().ceil() as i32).max(3);
58
59    let tiles = compiler.compile(lattice_range);
60
61    // Assign each agent to the nearest tile.
62    let mut assignments = Vec::with_capacity(agent_embeddings.len());
63    for (idx, emb) in agent_embeddings.iter().enumerate() {
64        // Project the agent embedding to 2D using the same projection.
65        let mut ax = 0.0f64;
66        let mut ay = 0.0f64;
67        let proj = &compiler.projection();
68        for (r, row) in proj.iter().enumerate() {
69            let mut val = 0.0f64;
70            for (c, &coeff) in row.iter().enumerate() {
71                if c < emb.len() {
72                    val += coeff * emb[c];
73                }
74            }
75            if r == 0 {
76                ax = val;
77            } else if r == 1 {
78                ay = val;
79            }
80        }
81
82        // Find nearest tile.
83        let mut best_dist = f64::INFINITY;
84        let mut best_tile_idx = 0usize;
85        for (ti, tile) in tiles.iter().enumerate() {
86            let dx = tile.x - ax;
87            let dy = tile.y - ay;
88            let dist = (dx * dx + dy * dy).sqrt();
89            if dist < best_dist {
90                best_dist = dist;
91                best_tile_idx = ti;
92            }
93        }
94
95        assignments.push(AgentTile {
96            agent_index: idx,
97            tile: tiles[best_tile_idx].clone(),
98            assignment_distance: best_dist,
99        });
100    }
101
102    let thick_count = tiles.iter().filter(|t| t.tile_type == TileType::Thick).count();
103    let thin_count = tiles.iter().filter(|t| t.tile_type == TileType::Thin).count();
104    let thick_thin_ratio = if thin_count > 0 {
105        thick_count as f64 / thin_count as f64
106    } else if thick_count > 0 {
107        f64::INFINITY
108    } else {
109        0.0
110    };
111
112    FleetTiling {
113        source_dim,
114        tiles,
115        assignments,
116        thick_count,
117        thin_count,
118        thick_thin_ratio,
119    }
120}
121
122/// Public projection accessor on CutAndProjectCompiler.
123impl CutAndProjectCompiler {
124    /// Get a reference to the projection matrix.
125    pub fn projection(&self) -> &Vec<Vec<f64>> {
126        &self.projection
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    // Helper: generate synthetic agent embeddings in 5D.
135    fn make_agents(n: usize) -> Vec<Vec<f64>> {
136        (0..n)
137            .map(|i| {
138                let t = i as f64 * 0.5;
139                vec![t.cos(), t.sin(), (t * 0.3).cos(), (t * 0.3).sin(), 0.01 * t]
140            })
141            .collect()
142    }
143
144    // 1. Basic fleet tiling compiles.
145    #[test]
146    fn test_fleet_tiling_compiles() {
147        let agents = make_agents(10);
148        let fleet = compile_fleet_tiling(&agents);
149        assert!(!fleet.tiles.is_empty());
150        assert_eq!(fleet.assignments.len(), 10);
151    }
152
153    // 2. Every agent gets an assignment.
154    #[test]
155    fn test_every_agent_assigned() {
156        let agents = make_agents(20);
157        let fleet = compile_fleet_tiling(&agents);
158        for i in 0..20 {
159            assert!(
160                fleet.assignments.iter().any(|a| a.agent_index == i),
161                "Agent {} should be assigned",
162                i
163            );
164        }
165    }
166
167    // 3. Source dimension preserved.
168    #[test]
169    fn test_source_dim_preserved() {
170        let agents = make_agents(5);
171        let fleet = compile_fleet_tiling(&agents);
172        assert_eq!(fleet.source_dim, 5);
173    }
174
175    // 4. Tile types are valid.
176    #[test]
177    fn test_fleet_tile_types_valid() {
178        let agents = make_agents(10);
179        let fleet = compile_fleet_tiling(&agents);
180        for t in &fleet.tiles {
181            assert!(t.tile_type == TileType::Thick || t.tile_type == TileType::Thin);
182        }
183    }
184
185    // 5. Assignment distances are finite.
186    #[test]
187    fn test_assignment_distances_finite() {
188        let agents = make_agents(15);
189        let fleet = compile_fleet_tiling(&agents);
190        for a in &fleet.assignments {
191            assert!(
192                a.assignment_distance.is_finite(),
193                "Assignment distance should be finite"
194            );
195        }
196    }
197
198    // 6. Thick and thin counts sum to total.
199    #[test]
200    fn test_thick_thin_sum() {
201        let agents = make_agents(10);
202        let fleet = compile_fleet_tiling(&agents);
203        assert_eq!(
204            fleet.thick_count + fleet.thin_count,
205            fleet.tiles.len(),
206            "Thick + thin should equal total tiles"
207        );
208    }
209
210    // 7. Single agent works.
211    #[test]
212    fn test_single_agent() {
213        let agents = vec![vec![1.0, 0.0, 0.0, 0.0, 0.0]];
214        let fleet = compile_fleet_tiling(&agents);
215        assert_eq!(fleet.assignments.len(), 1);
216        assert!(!fleet.tiles.is_empty());
217    }
218
219    // 8. Many agents (100).
220    #[test]
221    fn test_many_agents() {
222        let agents = make_agents(100);
223        let fleet = compile_fleet_tiling(&agents);
224        assert_eq!(fleet.assignments.len(), 100);
225        assert!(!fleet.tiles.is_empty());
226    }
227}