1use crate::algorithms::community::lanczos::{EigenWhich, lanczos_top_k};
14use crate::core::{Graph, IgraphError, IgraphResult};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SpectralWhich {
19 LargestAlgebraic,
21 SmallestAlgebraic,
23 LargestMagnitude,
25}
26
27#[derive(Debug, Clone)]
29pub struct AdjacencySpectralEmbeddingResult {
30 pub eigenvalues: Vec<f64>,
32 pub embedding: Vec<Vec<f64>>,
35}
36
37#[allow(clippy::too_many_arguments)]
79pub fn adjacency_spectral_embedding(
80 graph: &Graph,
81 no: usize,
82 weights: Option<&[f64]>,
83 which: SpectralWhich,
84 scaled: bool,
85 cvec: Option<&[f64]>,
86) -> IgraphResult<AdjacencySpectralEmbeddingResult> {
87 let n = graph.vcount() as usize;
88
89 if no == 0 {
90 return Err(IgraphError::InvalidArgument(
91 "no must be at least 1".to_string(),
92 ));
93 }
94 if no > n {
95 return Err(IgraphError::InvalidArgument(format!(
96 "no ({no}) exceeds vertex count ({n})"
97 )));
98 }
99
100 if let Some(w) = weights {
101 if w.len() != graph.ecount() {
102 return Err(IgraphError::InvalidArgument(format!(
103 "weights length ({}) differs from edge count ({})",
104 w.len(),
105 graph.ecount()
106 )));
107 }
108 for &wv in w {
109 if !wv.is_finite() {
110 return Err(IgraphError::InvalidArgument(
111 "edge weights must be finite".to_string(),
112 ));
113 }
114 }
115 }
116
117 if let Some(cv) = cvec {
118 if cv.len() != 1 && cv.len() != n {
119 return Err(IgraphError::InvalidArgument(format!(
120 "cvec length ({}) must be 1 or vcount ({n})",
121 cv.len()
122 )));
123 }
124 }
125
126 if n == 0 {
127 return Ok(AdjacencySpectralEmbeddingResult {
128 eigenvalues: Vec::new(),
129 embedding: Vec::new(),
130 });
131 }
132
133 if graph.ecount() == 0 {
134 return Ok(AdjacencySpectralEmbeddingResult {
135 eigenvalues: vec![0.0; no],
136 embedding: vec![vec![0.0; no]; n],
137 });
138 }
139
140 let adj = build_adjacency(graph)?;
142
143 let matvec = |x: &[f64], y: &mut [f64]| {
145 adjacency_matvec(&adj, weights, cvec, n, x, y);
146 };
147
148 let eigen_which = match which {
149 SpectralWhich::LargestAlgebraic => EigenWhich::LargestAlgebraic,
150 SpectralWhich::SmallestAlgebraic => EigenWhich::SmallestAlgebraic,
151 SpectralWhich::LargestMagnitude => EigenWhich::LargestMagnitude,
152 };
153
154 let max_iter = n.saturating_mul(50).max(1000);
155 let result = lanczos_top_k(n, &matvec, no, eigen_which, max_iter);
156
157 let actual_no = result.eigenvalues.len();
158
159 let mut eigenvalues = result.eigenvalues;
161 zapsmall_vec(&mut eigenvalues);
162
163 let mut embedding = vec![vec![0.0; actual_no]; n];
165 for (d, evec) in result.eigenvectors.iter().enumerate().take(actual_no) {
166 for (v, row) in embedding.iter_mut().enumerate() {
167 row[d] = evec[v];
168 }
169 }
170
171 for row in &mut embedding {
173 zapsmall_vec(row);
174 }
175
176 if scaled {
178 for (d, &eval) in eigenvalues.iter().enumerate().take(actual_no) {
179 let scale = eval.abs().sqrt();
180 for row in &mut embedding {
181 row[d] *= scale;
182 }
183 }
184 }
185
186 Ok(AdjacencySpectralEmbeddingResult {
187 eigenvalues,
188 embedding,
189 })
190}
191
192type AdjList = Vec<Vec<(usize, usize)>>; fn build_adjacency(graph: &Graph) -> IgraphResult<AdjList> {
197 let n = graph.vcount() as usize;
198 let mut adj: AdjList = vec![Vec::new(); n];
199 for eid in 0..graph.ecount() {
200 #[allow(clippy::cast_possible_truncation)]
201 let eid32 = eid as u32;
202 let s = graph.edge_source(eid32)? as usize;
203 let t = graph.edge_target(eid32)? as usize;
204 adj[s].push((t, eid));
205 if s != t {
206 adj[t].push((s, eid));
207 }
208 }
209 Ok(adj)
210}
211
212fn adjacency_matvec(
213 adj: &AdjList,
214 weights: Option<&[f64]>,
215 cvec: Option<&[f64]>,
216 n: usize,
217 x: &[f64],
218 y: &mut [f64],
219) {
220 for i in 0..n {
221 y[i] = 0.0;
222 for &(nei, eid) in &adj[i] {
223 let w = match weights {
224 Some(wt) => wt[eid],
225 None => 1.0,
226 };
227 y[i] += w * x[nei];
228 }
229 if let Some(cv) = cvec {
231 let c = if cv.len() == 1 { cv[0] } else { cv[i] };
232 y[i] += c * x[i];
233 }
234 }
235}
236
237fn zapsmall_vec(v: &mut [f64]) {
238 if v.is_empty() {
239 return;
240 }
241 let max_abs = v.iter().map(|x| x.abs()).fold(0.0_f64, f64::max);
242 if max_abs == 0.0 {
243 return;
244 }
245 let threshold = max_abs * f64::EPSILON.sqrt();
246 for x in v {
247 if x.abs() < threshold {
248 *x = 0.0;
249 }
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn empty_graph() {
259 let g = Graph::with_vertices(0);
260 let r =
261 adjacency_spectral_embedding(&g, 1, None, SpectralWhich::LargestAlgebraic, false, None);
262 assert!(r.is_err()); }
264
265 #[test]
266 fn no_edges() {
267 let g = Graph::with_vertices(4);
268 let r =
269 adjacency_spectral_embedding(&g, 2, None, SpectralWhich::LargestAlgebraic, false, None)
270 .unwrap();
271 assert_eq!(r.eigenvalues.len(), 2);
272 assert_eq!(r.embedding.len(), 4);
273 for &e in &r.eigenvalues {
274 assert!(
275 (e).abs() < 1e-10,
276 "eigenvalue should be 0 for edgeless graph"
277 );
278 }
279 }
280
281 #[test]
282 fn path_graph_2d() {
283 let mut g = Graph::with_vertices(4);
285 g.add_edge(0, 1).unwrap();
286 g.add_edge(1, 2).unwrap();
287 g.add_edge(2, 3).unwrap();
288
289 let r =
290 adjacency_spectral_embedding(&g, 2, None, SpectralWhich::LargestAlgebraic, false, None)
291 .unwrap();
292 assert_eq!(r.eigenvalues.len(), 2);
293 assert!(r.eigenvalues[0] > 0.0, "got {}", r.eigenvalues[0]);
295 assert!(r.eigenvalues[1] > 0.0, "got {}", r.eigenvalues[1]);
296 #[allow(unknown_lints, clippy::manual_midpoint)]
298 let golden = (1.0 + 5.0_f64.sqrt()) / 2.0;
299 assert!(
300 (r.eigenvalues[0] - golden).abs() < 1e-4,
301 "expected {golden}, got {}",
302 r.eigenvalues[0]
303 );
304 }
305
306 #[test]
307 fn complete_graph_k4() {
308 let mut g = Graph::with_vertices(4);
310 for i in 0..4u32 {
311 for j in (i + 1)..4 {
312 g.add_edge(i, j).unwrap();
313 }
314 }
315 let r =
316 adjacency_spectral_embedding(&g, 1, None, SpectralWhich::LargestAlgebraic, false, None)
317 .unwrap();
318 assert!(
319 (r.eigenvalues[0] - 3.0).abs() < 1e-4,
320 "expected 3.0, got {}",
321 r.eigenvalues[0]
322 );
323 }
324
325 #[test]
326 fn scaled_embedding() {
327 let mut g = Graph::with_vertices(4);
328 for i in 0..4u32 {
329 for j in (i + 1)..4 {
330 g.add_edge(i, j).unwrap();
331 }
332 }
333 let r_unscaled =
334 adjacency_spectral_embedding(&g, 1, None, SpectralWhich::LargestAlgebraic, false, None)
335 .unwrap();
336 let r_scaled =
337 adjacency_spectral_embedding(&g, 1, None, SpectralWhich::LargestAlgebraic, true, None)
338 .unwrap();
339
340 let scale = r_unscaled.eigenvalues[0].abs().sqrt();
341 for v in 0..4 {
342 let expected = r_unscaled.embedding[v][0] * scale;
343 assert!(
344 (r_scaled.embedding[v][0] - expected).abs() < 1e-8,
345 "vertex {v}: expected {expected}, got {}",
346 r_scaled.embedding[v][0]
347 );
348 }
349 }
350
351 #[test]
352 fn cvec_scalar_augmentation() {
353 let mut g = Graph::with_vertices(4);
355 for i in 0..4u32 {
356 for j in (i + 1)..4 {
357 g.add_edge(i, j).unwrap();
358 }
359 }
360 let r_plain =
361 adjacency_spectral_embedding(&g, 1, None, SpectralWhich::LargestAlgebraic, false, None)
362 .unwrap();
363 let r_shifted = adjacency_spectral_embedding(
364 &g,
365 1,
366 None,
367 SpectralWhich::LargestAlgebraic,
368 false,
369 Some(&[2.0]),
370 )
371 .unwrap();
372 assert!(
374 (r_shifted.eigenvalues[0] - r_plain.eigenvalues[0] - 2.0).abs() < 1e-4,
375 "expected shift of 2.0, got {} vs {}",
376 r_shifted.eigenvalues[0],
377 r_plain.eigenvalues[0]
378 );
379 }
380
381 #[test]
382 fn smallest_algebraic() {
383 let mut g = Graph::with_vertices(4);
385 for i in 0..4u32 {
386 for j in (i + 1)..4 {
387 g.add_edge(i, j).unwrap();
388 }
389 }
390 let r = adjacency_spectral_embedding(
391 &g,
392 1,
393 None,
394 SpectralWhich::SmallestAlgebraic,
395 false,
396 None,
397 )
398 .unwrap();
399 assert!(
400 (r.eigenvalues[0] - (-1.0)).abs() < 1e-4,
401 "expected -1.0, got {}",
402 r.eigenvalues[0]
403 );
404 }
405
406 #[test]
407 fn weighted_graph() {
408 let mut g = Graph::with_vertices(4);
412 g.add_edge(0, 1).unwrap();
413 g.add_edge(0, 2).unwrap();
414 g.add_edge(0, 3).unwrap();
415 let weights = vec![2.0, 2.0, 2.0];
416 let r = adjacency_spectral_embedding(
417 &g,
418 1,
419 Some(&weights),
420 SpectralWhich::LargestAlgebraic,
421 false,
422 None,
423 )
424 .unwrap();
425 let expected = 2.0 * 3.0_f64.sqrt();
426 assert!(
427 (r.eigenvalues[0] - expected).abs() < 1e-4,
428 "expected {expected}, got {}",
429 r.eigenvalues[0]
430 );
431 }
432
433 #[test]
434 fn invalid_no() {
435 let g = Graph::with_vertices(3);
436 let r =
437 adjacency_spectral_embedding(&g, 5, None, SpectralWhich::LargestAlgebraic, false, None);
438 assert!(r.is_err());
439 }
440
441 #[test]
442 fn embedding_dimension() {
443 let mut g = Graph::with_vertices(6);
444 g.add_edge(0, 1).unwrap();
445 g.add_edge(1, 2).unwrap();
446 g.add_edge(2, 3).unwrap();
447 g.add_edge(3, 4).unwrap();
448 g.add_edge(4, 5).unwrap();
449 let r =
450 adjacency_spectral_embedding(&g, 3, None, SpectralWhich::LargestAlgebraic, false, None)
451 .unwrap();
452 assert_eq!(r.eigenvalues.len(), 3);
453 assert_eq!(r.embedding.len(), 6);
454 for row in &r.embedding {
455 assert_eq!(row.len(), 3);
456 }
457 }
458}