rust_igraph/algorithms/properties/
signless_laplacian.rs1#![allow(
18 clippy::cast_possible_truncation,
19 clippy::cast_precision_loss,
20 clippy::many_single_char_names,
21 clippy::needless_range_loop,
22 clippy::similar_names,
23 clippy::too_many_lines
24)]
25
26use crate::core::{Graph, IgraphError, IgraphResult};
27
28fn dense_signless_laplacian(graph: &Graph) -> Vec<f64> {
30 let n = graph.vcount() as usize;
31 let mut q = vec![0.0_f64; n * n];
32 for (u, v) in graph.edges() {
33 let ui = u as usize;
34 let vi = v as usize;
35 q[ui * n + vi] += 1.0;
36 q[vi * n + ui] += 1.0;
37 q[ui * n + ui] += 1.0;
38 q[vi * n + vi] += 1.0;
39 }
40 q
41}
42
43fn jacobi_eigen_ascending(mat: &mut [f64], n: usize) -> Vec<f64> {
46 if n == 0 {
47 return Vec::new();
48 }
49 if n == 1 {
50 return vec![mat[0]];
51 }
52
53 let max_sweeps = 100;
54 for _ in 0..max_sweeps {
55 let mut max_off = 0.0_f64;
56 let mut p = 0;
57 let mut q = 1;
58 for i in 0..n {
59 for j in (i + 1)..n {
60 let val = mat[i * n + j].abs();
61 if val > max_off {
62 max_off = val;
63 p = i;
64 q = j;
65 }
66 }
67 }
68
69 if max_off < 1e-14 {
70 break;
71 }
72
73 let app = mat[p * n + p];
74 let aqq = mat[q * n + q];
75 let apq = mat[p * n + q];
76
77 let (cos, sin) = if (app - aqq).abs() < 1e-300 {
78 let s = std::f64::consts::FRAC_1_SQRT_2;
79 (s, s)
80 } else {
81 let tau = (aqq - app) / (2.0 * apq);
82 let t = if tau >= 0.0 {
83 1.0 / (tau + (1.0 + tau * tau).sqrt())
84 } else {
85 -1.0 / (-tau + (1.0 + tau * tau).sqrt())
86 };
87 let c = 1.0 / (1.0 + t * t).sqrt();
88 (c, t * c)
89 };
90
91 for i in 0..n {
92 if i == p || i == q {
93 continue;
94 }
95 let aip = mat[i * n + p];
96 let aiq = mat[i * n + q];
97 mat[i * n + p] = cos * aip - sin * aiq;
98 mat[p * n + i] = mat[i * n + p];
99 mat[i * n + q] = sin * aip + cos * aiq;
100 mat[q * n + i] = mat[i * n + q];
101 }
102
103 let new_pp = cos * cos * app - 2.0 * sin * cos * apq + sin * sin * aqq;
104 let new_qq = sin * sin * app + 2.0 * sin * cos * apq + cos * cos * aqq;
105 mat[p * n + p] = new_pp;
106 mat[q * n + q] = new_qq;
107 mat[p * n + q] = 0.0;
108 mat[q * n + p] = 0.0;
109 }
110
111 let mut eigenvalues: Vec<f64> = (0..n).map(|i| mat[i * n + i]).collect();
112 eigenvalues.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
113 eigenvalues
114}
115
116pub fn signless_laplacian_spectrum(graph: &Graph) -> IgraphResult<Vec<f64>> {
136 if graph.is_directed() {
137 return Err(IgraphError::InvalidArgument(
138 "signless_laplacian_spectrum is defined for undirected graphs only".into(),
139 ));
140 }
141 let n = graph.vcount() as usize;
142 if n == 0 {
143 return Ok(Vec::new());
144 }
145 let mut q = dense_signless_laplacian(graph);
146 Ok(jacobi_eigen_ascending(&mut q, n))
147}
148
149pub fn signless_laplacian_smallest(graph: &Graph) -> IgraphResult<f64> {
171 let spec = signless_laplacian_spectrum(graph)?;
172 if spec.is_empty() {
173 return Ok(0.0);
174 }
175 Ok(spec[0].max(0.0))
176}
177
178pub fn signless_laplacian_spectral_radius(graph: &Graph) -> IgraphResult<f64> {
194 let spec = signless_laplacian_spectrum(graph)?;
195 if spec.is_empty() {
196 return Ok(0.0);
197 }
198 Ok(spec[spec.len() - 1])
199}
200
201pub fn signless_laplacian_energy(graph: &Graph) -> IgraphResult<f64> {
219 if graph.is_directed() {
220 return Err(IgraphError::InvalidArgument(
221 "signless_laplacian_energy is defined for undirected graphs only".into(),
222 ));
223 }
224 let n = graph.vcount() as usize;
225 if n == 0 {
226 return Ok(0.0);
227 }
228
229 let spec = signless_laplacian_spectrum(graph)?;
230 let m = graph.ecount() as f64;
231 let mean = 2.0 * m / n as f64;
232
233 Ok(spec.iter().map(|&q| (q - mean).abs()).sum())
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 fn k3() -> Graph {
241 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
242 }
243
244 fn k4() -> Graph {
245 Graph::from_edges(
246 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
247 false,
248 Some(4),
249 )
250 .unwrap()
251 }
252
253 fn path4() -> Graph {
254 Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
255 }
256
257 fn cycle4() -> Graph {
258 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
259 }
260
261 fn star4() -> Graph {
262 Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false, Some(4)).unwrap()
263 }
264
265 fn k22() -> Graph {
266 Graph::from_edges(&[(0, 2), (0, 3), (1, 2), (1, 3)], false, Some(4)).unwrap()
267 }
268
269 #[test]
272 fn sls_empty() {
273 let g = Graph::with_vertices(0);
274 let spec = signless_laplacian_spectrum(&g).unwrap();
275 assert!(spec.is_empty());
276 }
277
278 #[test]
279 fn sls_single() {
280 let g = Graph::with_vertices(1);
281 let spec = signless_laplacian_spectrum(&g).unwrap();
282 assert_eq!(spec.len(), 1);
283 assert!(spec[0].abs() < 1e-10);
284 }
285
286 #[test]
287 fn sls_k3() {
288 let g = k3();
289 let spec = signless_laplacian_spectrum(&g).unwrap();
290 assert_eq!(spec.len(), 3);
291 assert!((spec[0] - 1.0).abs() < 0.1);
294 assert!((spec[1] - 1.0).abs() < 0.1);
295 assert!((spec[2] - 4.0).abs() < 0.1);
296 }
297
298 #[test]
299 fn sls_k4() {
300 let g = k4();
301 let spec = signless_laplacian_spectrum(&g).unwrap();
302 assert!((spec[0] - 2.0).abs() < 0.1);
305 assert!((spec[3] - 6.0).abs() < 0.1);
306 }
307
308 #[test]
309 fn sls_nonneg() {
310 let g = star4();
311 let spec = signless_laplacian_spectrum(&g).unwrap();
312 for &v in &spec {
313 assert!(v >= -0.01, "eigenvalue {v} < 0");
314 }
315 }
316
317 #[test]
318 fn sls_ascending() {
319 let g = cycle4();
320 let spec = signless_laplacian_spectrum(&g).unwrap();
321 for i in 1..spec.len() {
322 assert!(spec[i] >= spec[i - 1] - 1e-10);
323 }
324 }
325
326 #[test]
327 fn sls_bipartite_has_zero() {
328 let g = k22();
329 let spec = signless_laplacian_spectrum(&g).unwrap();
330 assert!(spec[0].abs() < 0.01);
331 }
332
333 #[test]
334 fn sls_path_bipartite_has_zero() {
335 let g = path4();
336 let spec = signless_laplacian_spectrum(&g).unwrap();
337 assert!(spec[0] < 0.01);
338 }
339
340 #[test]
341 fn sls_isolated_vertices() {
342 let g = Graph::with_vertices(3);
343 let spec = signless_laplacian_spectrum(&g).unwrap();
344 for &v in &spec {
345 assert!(v.abs() < 0.01);
346 }
347 }
348
349 #[test]
350 fn sls_directed_error() {
351 let g = Graph::from_edges(&[(0, 1)], true, Some(2)).unwrap();
352 assert!(signless_laplacian_spectrum(&g).is_err());
353 }
354
355 #[test]
358 fn slsmall_bipartite_zero() {
359 let g = k22();
360 let q1 = signless_laplacian_smallest(&g).unwrap();
361 assert!(q1.abs() < 0.01);
362 }
363
364 #[test]
365 fn slsmall_nonbipartite_positive() {
366 let g = k3();
367 let q1 = signless_laplacian_smallest(&g).unwrap();
368 assert!(q1 > 0.5);
369 }
370
371 #[test]
372 fn slsmall_single() {
373 let g = Graph::with_vertices(1);
374 let q1 = signless_laplacian_smallest(&g).unwrap();
375 assert!(q1.abs() < 1e-10);
376 }
377
378 #[test]
381 fn slsr_k3() {
382 let g = k3();
383 let qn = signless_laplacian_spectral_radius(&g).unwrap();
384 assert!((qn - 4.0).abs() < 0.1);
386 }
387
388 #[test]
389 fn slsr_k4() {
390 let g = k4();
391 let qn = signless_laplacian_spectral_radius(&g).unwrap();
392 assert!((qn - 6.0).abs() < 0.1);
394 }
395
396 #[test]
397 fn slsr_at_most_2maxdeg() {
398 let g = star4();
399 let qn = signless_laplacian_spectral_radius(&g).unwrap();
400 assert!(qn <= 6.01);
402 }
403
404 #[test]
407 fn sle_k3() {
408 let g = k3();
409 let qe = signless_laplacian_energy(&g).unwrap();
410 assert!((qe - 4.0).abs() < 0.1);
412 }
413
414 #[test]
415 fn sle_nonneg() {
416 let g = cycle4();
417 let qe = signless_laplacian_energy(&g).unwrap();
418 assert!(qe >= -1e-10);
419 }
420
421 #[test]
422 fn sle_empty() {
423 let g = Graph::with_vertices(0);
424 let qe = signless_laplacian_energy(&g).unwrap();
425 assert!(qe.abs() < 1e-10);
426 }
427
428 #[test]
431 fn trace_equals_twice_edges() {
432 let g = star4();
433 let spec = signless_laplacian_spectrum(&g).unwrap();
434 let trace: f64 = spec.iter().sum();
435 let m = g.ecount() as f64;
437 assert!((trace - 2.0 * m).abs() < 0.1);
438 }
439
440 #[test]
441 fn q_and_l_share_same_trace() {
442 let g = cycle4();
445 let q_spec = signless_laplacian_spectrum(&g).unwrap();
446 let q_trace: f64 = q_spec.iter().sum();
447 let m = g.ecount() as f64;
448 assert!((q_trace - 2.0 * m).abs() < 0.1);
449 }
450
451 #[test]
452 fn regular_q_eigenvalue_relation() {
453 let g = cycle4();
457 let spec = signless_laplacian_spectrum(&g).unwrap();
458 assert!(spec[0].abs() < 0.1);
460 assert!((spec[1] - 2.0).abs() < 0.1);
461 assert!((spec[2] - 2.0).abs() < 0.1);
462 assert!((spec[3] - 4.0).abs() < 0.1);
463 }
464}