use crate::cut_and_project::{CutAndProjectCompiler, TileCoord, TileType};
#[derive(Debug, Clone)]
pub struct AgentTile {
pub agent_index: usize,
pub tile: TileCoord,
pub assignment_distance: f64,
}
#[derive(Debug, Clone)]
pub struct FleetTiling {
pub source_dim: usize,
pub tiles: Vec<TileCoord>,
pub assignments: Vec<AgentTile>,
pub thick_count: usize,
pub thin_count: usize,
pub thick_thin_ratio: f64,
}
pub fn compile_fleet_tiling(agent_embeddings: &[Vec<f64>]) -> FleetTiling {
assert!(
!agent_embeddings.is_empty(),
"Need at least one agent embedding"
);
let source_dim = agent_embeddings[0].len();
let target_dim = 2;
let compiler = CutAndProjectCompiler::new(source_dim, target_dim)
.with_pca_projection(agent_embeddings);
let lattice_range = ((agent_embeddings.len() as f64).sqrt().ceil() as i32).max(3);
let tiles = compiler.compile(lattice_range);
let mut assignments = Vec::with_capacity(agent_embeddings.len());
for (idx, emb) in agent_embeddings.iter().enumerate() {
let mut ax = 0.0f64;
let mut ay = 0.0f64;
let proj = &compiler.projection();
for (r, row) in proj.iter().enumerate() {
let mut val = 0.0f64;
for (c, &coeff) in row.iter().enumerate() {
if c < emb.len() {
val += coeff * emb[c];
}
}
if r == 0 {
ax = val;
} else if r == 1 {
ay = val;
}
}
let mut best_dist = f64::INFINITY;
let mut best_tile_idx = 0usize;
for (ti, tile) in tiles.iter().enumerate() {
let dx = tile.x - ax;
let dy = tile.y - ay;
let dist = (dx * dx + dy * dy).sqrt();
if dist < best_dist {
best_dist = dist;
best_tile_idx = ti;
}
}
assignments.push(AgentTile {
agent_index: idx,
tile: tiles[best_tile_idx].clone(),
assignment_distance: best_dist,
});
}
let thick_count = tiles.iter().filter(|t| t.tile_type == TileType::Thick).count();
let thin_count = tiles.iter().filter(|t| t.tile_type == TileType::Thin).count();
let thick_thin_ratio = if thin_count > 0 {
thick_count as f64 / thin_count as f64
} else if thick_count > 0 {
f64::INFINITY
} else {
0.0
};
FleetTiling {
source_dim,
tiles,
assignments,
thick_count,
thin_count,
thick_thin_ratio,
}
}
impl CutAndProjectCompiler {
pub fn projection(&self) -> &Vec<Vec<f64>> {
&self.projection
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_agents(n: usize) -> Vec<Vec<f64>> {
(0..n)
.map(|i| {
let t = i as f64 * 0.5;
vec![t.cos(), t.sin(), (t * 0.3).cos(), (t * 0.3).sin(), 0.01 * t]
})
.collect()
}
#[test]
fn test_fleet_tiling_compiles() {
let agents = make_agents(10);
let fleet = compile_fleet_tiling(&agents);
assert!(!fleet.tiles.is_empty());
assert_eq!(fleet.assignments.len(), 10);
}
#[test]
fn test_every_agent_assigned() {
let agents = make_agents(20);
let fleet = compile_fleet_tiling(&agents);
for i in 0..20 {
assert!(
fleet.assignments.iter().any(|a| a.agent_index == i),
"Agent {} should be assigned",
i
);
}
}
#[test]
fn test_source_dim_preserved() {
let agents = make_agents(5);
let fleet = compile_fleet_tiling(&agents);
assert_eq!(fleet.source_dim, 5);
}
#[test]
fn test_fleet_tile_types_valid() {
let agents = make_agents(10);
let fleet = compile_fleet_tiling(&agents);
for t in &fleet.tiles {
assert!(t.tile_type == TileType::Thick || t.tile_type == TileType::Thin);
}
}
#[test]
fn test_assignment_distances_finite() {
let agents = make_agents(15);
let fleet = compile_fleet_tiling(&agents);
for a in &fleet.assignments {
assert!(
a.assignment_distance.is_finite(),
"Assignment distance should be finite"
);
}
}
#[test]
fn test_thick_thin_sum() {
let agents = make_agents(10);
let fleet = compile_fleet_tiling(&agents);
assert_eq!(
fleet.thick_count + fleet.thin_count,
fleet.tiles.len(),
"Thick + thin should equal total tiles"
);
}
#[test]
fn test_single_agent() {
let agents = vec![vec![1.0, 0.0, 0.0, 0.0, 0.0]];
let fleet = compile_fleet_tiling(&agents);
assert_eq!(fleet.assignments.len(), 1);
assert!(!fleet.tiles.is_empty());
}
#[test]
fn test_many_agents() {
let agents = make_agents(100);
let fleet = compile_fleet_tiling(&agents);
assert_eq!(fleet.assignments.len(), 100);
assert!(!fleet.tiles.is_empty());
}
}