spectral_fleet/
embedding.rs1use crate::fleet_graph::FleetGraph;
8use crate::laplacian::{combinatorial_laplacian, eigen_decompose};
9
10#[derive(Debug, Clone)]
12pub struct Embedding {
13 pub dimensions: usize,
15 pub coordinates: Vec<Vec<f64>>,
17 pub reconstruction_error: f64,
19}
20
21impl Embedding {
22 pub fn new(dimensions: usize, coordinates: Vec<Vec<f64>>) -> Self {
23 Self {
24 dimensions,
25 coordinates,
26 reconstruction_error: 0.0,
27 }
28 }
29
30 pub fn pairwise_distances(&self) -> Vec<Vec<f64>> {
32 let n = self.coordinates.len();
33 let mut dists = vec![vec![0.0; n]; n];
34 for i in 0..n {
35 for j in (i + 1)..n {
36 let d: f64 = self.coordinates[i]
37 .iter()
38 .zip(self.coordinates[j].iter())
39 .map(|(&a, &b)| (a - b).powi(2))
40 .sum::<f64>()
41 .sqrt();
42 dists[i][j] = d;
43 dists[j][i] = d;
44 }
45 }
46 dists
47 }
48}
49
50pub fn spectral_embedding(graph: &FleetGraph, dimensions: usize) -> Embedding {
52 let n = graph.node_count();
53 if n == 0 {
54 return Embedding::new(dimensions, vec![]);
55 }
56
57 let lap = combinatorial_laplacian(graph);
58 let (eigenvalues, eigenvectors) = eigen_decompose(&lap, 1000, 1e-10);
59
60 let num_vecs = dimensions.min(eigenvalues.len().saturating_sub(1)).max(1);
63
64 let mut coordinates = Vec::with_capacity(n);
65 for i in 0..n {
66 let mut point = Vec::with_capacity(num_vecs);
67 for j in 1..=num_vecs {
68 if j < eigenvectors.len() {
69 point.push(eigenvectors[j][i]);
70 } else {
71 point.push(0.0);
72 }
73 }
74 coordinates.push(point);
75 }
76
77 let embedding = Embedding::new(num_vecs, coordinates);
79 let error = compute_reconstruction_error(graph, &embedding);
80
81 Embedding {
82 dimensions: num_vecs,
83 coordinates: embedding.coordinates,
84 reconstruction_error: error,
85 }
86}
87
88fn compute_reconstruction_error(graph: &FleetGraph, embedding: &Embedding) -> f64 {
91 let n = graph.node_count();
92 if n < 2 {
93 return 0.0;
94 }
95
96 let adj = graph.undirected_adjacency();
97 let embed_dists = embedding.pairwise_distances();
98
99 let mut graph_dists = vec![vec![f64::INFINITY; n]; n];
101 for s in 0..n {
102 graph_dists[s][s] = 0.0;
103 let mut queue = std::collections::VecDeque::new();
104 queue.push_back(s);
105 while let Some(u) = queue.pop_front() {
106 for v in 0..n {
107 if adj[u][v] > 0.0 && graph_dists[s][v] == f64::INFINITY {
108 graph_dists[s][v] = graph_dists[s][u] + 1.0;
109 queue.push_back(v);
110 }
111 }
112 }
113 }
114
115 let mut stress = 0.0;
117 let mut count = 0;
118 for i in 0..n {
119 for j in (i + 1)..n {
120 let gd = graph_dists[i][j];
121 if gd.is_finite() && gd > 0.0 {
122 let ed = embed_dists[i][j];
123 stress += ((ed - gd) / gd).powi(2);
124 count += 1;
125 }
126 }
127 }
128 if count > 0 {
129 stress / count as f64
130 } else {
131 0.0
132 }
133}
134
135pub fn tsne_embedding(
138 graph: &FleetGraph,
139 perplexity: f64,
140 iterations: usize,
141) -> Embedding {
142 let n = graph.node_count();
143 if n == 0 {
144 return Embedding::new(2, vec![]);
145 }
146
147 let initial = spectral_embedding(graph, 2);
149
150 let adj = graph.undirected_adjacency();
151
152 let mut high_dists = vec![vec![f64::INFINITY; n]; n];
154 for s in 0..n {
155 high_dists[s][s] = 0.0;
156 let mut queue = std::collections::VecDeque::new();
157 queue.push_back(s);
158 while let Some(u) = queue.pop_front() {
159 for v in 0..n {
160 if adj[u][v] > 0.0 && high_dists[s][v] == f64::INFINITY {
161 high_dists[s][v] = high_dists[s][u] + 1.0;
162 queue.push_back(v);
163 }
164 }
165 }
166 }
167
168 let sigma = perplexity;
170 let mut p = vec![vec![0.0; n]; n];
171 for i in 0..n {
172 for j in 0..n {
173 if i != j && high_dists[i][j].is_finite() {
174 p[i][j] = (-high_dists[i][j].powi(2) / (2.0 * sigma * sigma)).exp();
175 }
176 }
177 let row_sum: f64 = p[i].iter().sum();
179 if row_sum > 0.0 {
180 for j in 0..n {
181 p[i][j] /= row_sum;
182 }
183 }
184 }
185
186 for i in 0..n {
188 for j in 0..n {
189 p[i][j] = (p[i][j] + p[j][i]) / (2.0 * n as f64);
190 }
191 }
192
193 let mut y: Vec<Vec<f64>> = initial.coordinates.clone();
195 for point in &mut y {
197 while point.len() < 2 {
198 point.push(0.0);
199 }
200 point.truncate(2);
201 }
202
203 let mut gains = vec![vec![1.0_f64; 2]; n];
205 let mut y_vel = vec![vec![0.0_f64; 2]; n];
206 let learning_rate = 100.0;
207 let momentum = 0.8;
208
209 for _ in 0..iterations {
210 let mut q = vec![vec![0.0; n]; n];
212 let mut q_sum = 0.0;
213 for i in 0..n {
214 for j in (i + 1)..n {
215 let dist_sq: f64 = y[i]
216 .iter()
217 .zip(y[j].iter())
218 .map(|(&a, &b)| (a - b).powi(2))
219 .sum();
220 let val = 1.0 / (1.0 + dist_sq);
221 q[i][j] = val;
222 q[j][i] = val;
223 q_sum += 2.0 * val;
224 }
225 }
226 if q_sum > 0.0 {
227 for i in 0..n {
228 for j in 0..n {
229 q[i][j] /= q_sum;
230 }
231 }
232 }
233
234 let mut gradients = vec![vec![0.0; 2]; n];
236 for i in 0..n {
237 for j in 0..n {
238 if i == j {
239 continue;
240 }
241 let mult = 4.0 * (p[i][j] - q[i][j]);
242 let dist_sq: f64 = y[i]
243 .iter()
244 .zip(y[j].iter())
245 .map(|(&a, &b)| (a - b).powi(2))
246 .sum();
247 let factor = mult / (1.0 + dist_sq);
248 for d in 0..2 {
249 gradients[i][d] += factor * (y[i][d] - y[j][d]);
250 }
251 }
252 }
253
254 for i in 0..n {
256 for d in 0..2 {
257 let grad_sign = gradients[i][d].signum();
258 let vel_sign = y_vel[i][d].signum();
259 gains[i][d] = if grad_sign != vel_sign {
260 gains[i][d] + 0.2_f64
261 } else {
262 gains[i][d] * 0.8_f64
263 };
264 gains[i][d] = f64::max(gains[i][d], 0.01_f64);
265 y_vel[i][d] = momentum * y_vel[i][d] - learning_rate * gains[i][d] * gradients[i][d];
266 y[i][d] += y_vel[i][d];
267 }
268 }
269 }
270
271 let mut center = vec![0.0; 2];
273 for point in &y {
274 for d in 0..2 {
275 center[d] += point[d];
276 }
277 }
278 for d in 0..2 {
279 center[d] /= n as f64;
280 }
281 for point in &mut y {
282 for d in 0..2 {
283 point[d] -= center[d];
284 }
285 }
286
287 let mut embedding = Embedding::new(2, y);
288 embedding.reconstruction_error = compute_reconstruction_error(graph, &embedding);
289 embedding
290}
291
292pub fn render_ascii(embedding: &Embedding, width: usize, height: usize) -> String {
294 if embedding.coordinates.is_empty() {
295 return String::new();
296 }
297
298 let coords = &embedding.coordinates;
299 let dims = coords[0].len();
300 if dims < 2 {
301 return String::new();
302 }
303
304 let (mut min_x, mut max_x) = (f64::MAX, f64::MIN);
306 let (mut min_y, mut max_y) = (f64::MAX, f64::MIN);
307 for p in coords {
308 min_x = min_x.min(p[0]);
309 max_x = max_x.max(p[0]);
310 min_y = min_y.min(p[1]);
311 max_y = max_y.max(p[1]);
312 }
313
314 let range_x = (max_x - min_x).max(1e-10);
315 let range_y = (max_y - min_y).max(1e-10);
316
317 let mut canvas = vec![vec![' '; width]; height];
318
319 for (idx, p) in coords.iter().enumerate() {
320 let x = ((p[0] - min_x) / range_x * (width - 1) as f64).round() as usize;
321 let y = ((p[1] - min_y) / range_y * (height - 1) as f64).round() as usize;
322 let x = x.min(width - 1);
323 let y = y.min(height - 1);
324 let ch = if idx < 26 {
325 (b'A' + idx as u8) as char
326 } else {
327 '#'
328 };
329 canvas[height - 1 - y][x] = ch;
330 }
331
332 canvas
333 .iter()
334 .map(|row| row.iter().collect::<String>())
335 .collect::<Vec<_>>()
336 .join("\n")
337}