1#![allow(dead_code)]
16type Result<T, E = torsh_core::error::TorshError> = std::result::Result<T, E>;
19
20use crate::parameter::Parameter;
21use crate::{GraphData, GraphLayer};
22use scirs2_core::ndarray::Array2;
23use torsh_tensor::{
24 creation::{from_vec, randn, zeros},
25 Tensor,
26};
27
28#[derive(Debug, Clone, Copy)]
30pub enum LaplacianType {
31 Unnormalized,
33 Symmetric,
35 RandomWalk,
37}
38
39pub struct SpectralGraphAnalysis;
41
42impl SpectralGraphAnalysis {
43 pub fn compute_laplacian(
45 graph: &GraphData,
46 laplacian_type: LaplacianType,
47 ) -> Result<Array2<f32>> {
48 let num_nodes = graph.num_nodes;
49 let edge_data = graph.edge_index.to_vec()?;
50
51 let mut adj = Array2::zeros((num_nodes, num_nodes));
53
54 for i in (0..edge_data.len()).step_by(2) {
55 if i + 1 < edge_data.len() {
56 let src = edge_data[i] as usize;
57 let dst = edge_data[i + 1] as usize;
58
59 if src < num_nodes && dst < num_nodes {
60 adj[[src, dst]] = 1.0;
61 adj[[dst, src]] = 1.0; }
63 }
64 }
65
66 let mut degrees = vec![0.0; num_nodes];
68 for i in 0..num_nodes {
69 for j in 0..num_nodes {
70 degrees[i] += adj[[i, j]];
71 }
72 }
73
74 match laplacian_type {
76 LaplacianType::Unnormalized => {
77 let mut laplacian = Array2::zeros((num_nodes, num_nodes));
78 for i in 0..num_nodes {
79 laplacian[[i, i]] = degrees[i];
80 for j in 0..num_nodes {
81 laplacian[[i, j]] -= adj[[i, j]];
82 }
83 }
84 Ok(laplacian)
85 }
86 LaplacianType::Symmetric => {
87 let mut laplacian = Array2::zeros((num_nodes, num_nodes));
88
89 let mut d_inv_sqrt = vec![0.0; num_nodes];
91 for i in 0..num_nodes {
92 d_inv_sqrt[i] = if degrees[i] > 0.0 {
93 1.0 / degrees[i].sqrt()
94 } else {
95 0.0
96 };
97 }
98
99 for i in 0..num_nodes {
101 laplacian[[i, i]] = 1.0;
102 for j in 0..num_nodes {
103 laplacian[[i, j]] -= d_inv_sqrt[i] * adj[[i, j]] * d_inv_sqrt[j];
104 }
105 }
106 Ok(laplacian)
107 }
108 LaplacianType::RandomWalk => {
109 let mut laplacian = Array2::zeros((num_nodes, num_nodes));
110
111 let mut d_inv = vec![0.0; num_nodes];
113 for i in 0..num_nodes {
114 d_inv[i] = if degrees[i] > 0.0 {
115 1.0 / degrees[i]
116 } else {
117 0.0
118 };
119 }
120
121 for i in 0..num_nodes {
123 laplacian[[i, i]] = 1.0;
124 for j in 0..num_nodes {
125 laplacian[[i, j]] -= d_inv[i] * adj[[i, j]];
126 }
127 }
128 Ok(laplacian)
129 }
130 }
131 }
132
133 pub fn spectral_embedding(graph: &GraphData, num_components: usize) -> Result<Tensor> {
135 let laplacian = Self::compute_laplacian(graph, LaplacianType::Symmetric)?;
136 let num_nodes = graph.num_nodes;
137
138 let mut embeddings = Vec::new();
141
142 for _comp in 0..num_components {
143 let mut v = vec![0.0; num_nodes];
145 let mut rng = scirs2_core::random::thread_rng();
146 for val in v.iter_mut() {
147 *val = rng.gen_range(-0.5..0.5);
148 }
149
150 for _ in 0..50 {
152 let mut new_v = vec![0.0; num_nodes];
153
154 for i in 0..num_nodes {
155 for j in 0..num_nodes {
156 new_v[i] += laplacian[[i, j]] * v[j];
157 }
158 }
159
160 let norm: f32 = new_v.iter().map(|x| x * x).sum::<f32>().sqrt();
162 if norm > 0.0 {
163 for val in new_v.iter_mut() {
164 *val /= norm;
165 }
166 }
167
168 v = new_v;
169 }
170
171 embeddings.extend(v);
172 }
173
174 Ok(from_vec(
175 embeddings,
176 &[num_nodes, num_components],
177 torsh_core::device::DeviceType::Cpu,
178 )?)
179 }
180
181 pub fn compute_spectrum(graph: &GraphData, num_eigenvalues: usize) -> Vec<f32> {
183 let _laplacian = Self::compute_laplacian(graph, LaplacianType::Symmetric);
184 let num_nodes = graph.num_nodes;
185
186 let mut eigenvalues = Vec::new();
189
190 for k in 0..num_eigenvalues.min(num_nodes) {
191 let lambda =
192 2.0 * (1.0 - ((k as f32 * std::f32::consts::PI) / (num_nodes as f32)).cos());
193 eigenvalues.push(lambda);
194 }
195
196 eigenvalues
197 }
198
199 pub fn spectral_clustering(graph: &GraphData, num_clusters: usize) -> Result<Vec<usize>> {
201 let num_nodes = graph.num_nodes;
202
203 let embedding = Self::spectral_embedding(graph, num_clusters);
205 let embedding_data = embedding?.to_vec()?;
206
207 let mut labels = vec![0; num_nodes];
209 let mut centroids = vec![vec![0.0; num_clusters]; num_clusters];
210
211 let mut rng = scirs2_core::random::thread_rng();
213 for k in 0..num_clusters {
214 let idx = rng.gen_range(0..num_nodes);
215 for d in 0..num_clusters {
216 centroids[k][d] = embedding_data[idx * num_clusters + d];
217 }
218 }
219
220 for _ in 0..100 {
222 for i in 0..num_nodes {
224 let mut min_dist = f32::MAX;
225 let mut best_cluster = 0;
226
227 for k in 0..num_clusters {
228 let mut dist = 0.0;
229 for d in 0..num_clusters {
230 let diff = embedding_data[i * num_clusters + d] - centroids[k][d];
231 dist += diff * diff;
232 }
233
234 if dist < min_dist {
235 min_dist = dist;
236 best_cluster = k;
237 }
238 }
239
240 labels[i] = best_cluster;
241 }
242
243 let mut counts = vec![0; num_clusters];
245 let mut new_centroids = vec![vec![0.0; num_clusters]; num_clusters];
246
247 for i in 0..num_nodes {
248 let cluster = labels[i];
249 counts[cluster] += 1;
250
251 for d in 0..num_clusters {
252 new_centroids[cluster][d] += embedding_data[i * num_clusters + d];
253 }
254 }
255
256 for k in 0..num_clusters {
257 if counts[k] > 0 {
258 for d in 0..num_clusters {
259 new_centroids[k][d] /= counts[k] as f32;
260 }
261 }
262 }
263
264 centroids = new_centroids;
265 }
266
267 Ok(labels)
268 }
269}
270
271#[derive(Debug)]
273pub struct ChebConv {
274 in_features: usize,
275 out_features: usize,
276 k: usize, weights: Vec<Parameter>,
280
281 bias: Option<Parameter>,
282}
283
284impl ChebConv {
285 pub fn new(in_features: usize, out_features: usize, k: usize, use_bias: bool) -> Result<Self> {
287 let mut weights = Vec::new();
288
289 for _ in 0..k {
290 weights.push(Parameter::new(randn(&[in_features, out_features])?));
291 }
292
293 let bias = if use_bias {
294 Some(Parameter::new(zeros(&[out_features])?))
295 } else {
296 None
297 };
298
299 Ok(Self {
300 in_features,
301 out_features,
302 k,
303 weights,
304 bias,
305 })
306 }
307
308 pub fn forward(&self, graph: &GraphData) -> Result<GraphData> {
310 let num_nodes = graph.num_nodes;
311
312 let laplacian = SpectralGraphAnalysis::compute_laplacian(graph, LaplacianType::Symmetric)?;
314
315 let lap_data: Vec<f32> = laplacian.iter().copied().collect();
317 let lap_tensor = from_vec(
318 lap_data,
319 &[num_nodes, num_nodes],
320 torsh_core::device::DeviceType::Cpu,
321 )?;
322
323 let mut chebyshev_polynomials = Vec::new();
325
326 chebyshev_polynomials.push(graph.x.clone());
328
329 if self.k > 1 {
331 let t1 = lap_tensor.matmul(&graph.x)?;
332 chebyshev_polynomials.push(t1);
333 }
334
335 for i in 2..self.k {
337 let term1 = lap_tensor.matmul(&chebyshev_polynomials[i - 1])?;
338 let term1_scaled = term1.mul_scalar(2.0)?;
339 let t_k = term1_scaled.sub(&chebyshev_polynomials[i - 2])?;
340 chebyshev_polynomials.push(t_k);
341 }
342
343 let mut output = zeros::<f32>(&[num_nodes, self.out_features])?;
345
346 for (i, t_k) in chebyshev_polynomials.iter().enumerate().take(self.k) {
347 let weighted = t_k.matmul(&self.weights[i].clone_data())?;
348 output = output.add(&weighted)?;
349 }
350
351 if let Some(ref bias) = self.bias {
353 output = output.add(&bias.clone_data())?;
354 }
355
356 let mut output_graph = graph.clone();
357 output_graph.x = output;
358 Ok(output_graph)
359 }
360}
361
362impl GraphLayer for ChebConv {
363 fn forward(&self, graph: &GraphData) -> Result<GraphData> {
364 self.forward(graph)
365 }
366
367 fn parameters(&self) -> Vec<Tensor> {
368 let mut params: Vec<_> = self.weights.iter().map(|w| w.clone_data()).collect();
369
370 if let Some(ref bias) = self.bias {
371 params.push(bias.clone_data());
372 }
373
374 params
375 }
376}
377
378#[derive(Debug)]
380pub struct SpectralConv {
381 in_features: usize,
382 out_features: usize,
383 num_filters: usize,
384
385 spectral_weights: Parameter,
387
388 spatial_weight: Parameter,
390
391 bias: Option<Parameter>,
392}
393
394impl SpectralConv {
395 pub fn new(
397 in_features: usize,
398 out_features: usize,
399 num_filters: usize,
400 use_bias: bool,
401 ) -> Result<Self> {
402 let spectral_weights = Parameter::new(randn(&[num_filters, in_features])?);
403 let spatial_weight = Parameter::new(randn(&[in_features, out_features])?);
404
405 let bias = if use_bias {
406 Some(Parameter::new(zeros(&[out_features])?))
407 } else {
408 None
409 };
410
411 Ok(Self {
412 in_features,
413 out_features,
414 num_filters,
415 spectral_weights,
416 spatial_weight,
417 bias,
418 })
419 }
420
421 pub fn forward(&self, graph: &GraphData) -> Result<GraphData> {
423 let _num_nodes = graph.num_nodes;
424
425 let spectral_features = SpectralGraphAnalysis::spectral_embedding(graph, self.num_filters);
427
428 let filtered = spectral_features?.matmul(&self.spectral_weights.clone_data())?;
432
433 let combined = filtered.add(&graph.x)?;
435
436 let mut output = combined.matmul(&self.spatial_weight.clone_data())?;
438
439 if let Some(ref bias) = self.bias {
441 output = output.add(&bias.clone_data())?;
442 }
443
444 let mut output_graph = graph.clone();
445 output_graph.x = output;
446 Ok(output_graph)
447 }
448}
449
450impl GraphLayer for SpectralConv {
451 fn forward(&self, graph: &GraphData) -> Result<GraphData> {
452 self.forward(graph)
453 }
454
455 fn parameters(&self) -> Vec<Tensor> {
456 let mut params = vec![
457 self.spectral_weights.clone_data(),
458 self.spatial_weight.clone_data(),
459 ];
460
461 if let Some(ref bias) = self.bias {
462 params.push(bias.clone_data());
463 }
464
465 params
466 }
467}
468
469pub struct GraphSignalProcessing;
471
472impl GraphSignalProcessing {
473 pub fn graph_fourier_transform(graph: &GraphData, signal: &Tensor) -> Result<Tensor> {
475 let num_nodes = graph.num_nodes;
477 let embedding = SpectralGraphAnalysis::spectral_embedding(graph, num_nodes);
478
479 Ok(embedding?.t()?.matmul(signal)?)
481 }
482
483 pub fn inverse_graph_fourier_transform(
485 graph: &GraphData,
486 spectral_signal: &Tensor,
487 ) -> Result<Tensor> {
488 let num_nodes = graph.num_nodes;
489 let embedding = SpectralGraphAnalysis::spectral_embedding(graph, num_nodes)?;
490
491 Ok(embedding.matmul(spectral_signal)?)
493 }
494
495 pub fn low_pass_filter(graph: &GraphData, signal: &Tensor, cutoff: usize) -> Result<Tensor> {
497 let spectral = Self::graph_fourier_transform(graph, signal)?;
499
500 let mut filtered_data = spectral.to_vec()?;
502 let _signal_dim = signal.shape().dims()[1];
503
504 for i in cutoff..filtered_data.len() {
505 filtered_data[i] = 0.0;
506 }
507
508 let filtered_spectral = from_vec(
509 filtered_data,
510 spectral.shape().dims(),
511 torsh_core::device::DeviceType::Cpu,
512 )?;
513
514 Self::inverse_graph_fourier_transform(graph, &filtered_spectral)
516 }
517
518 pub fn high_pass_filter(graph: &GraphData, signal: &Tensor, cutoff: usize) -> Result<Tensor> {
520 let spectral = Self::graph_fourier_transform(graph, signal)?;
522
523 let mut filtered_data = spectral.to_vec()?;
525
526 for i in 0..cutoff.min(filtered_data.len()) {
527 filtered_data[i] = 0.0;
528 }
529
530 let filtered_spectral = from_vec(
531 filtered_data,
532 spectral.shape().dims(),
533 torsh_core::device::DeviceType::Cpu,
534 )?;
535
536 Self::inverse_graph_fourier_transform(graph, &filtered_spectral)
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544 use torsh_core::device::DeviceType;
545
546 #[test]
547 fn test_laplacian_computation() {
548 let features = randn(&[4, 3]).unwrap();
549 let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 0.0];
550 let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
551 let graph = GraphData::new(features, edge_index);
552
553 let laplacian = SpectralGraphAnalysis::compute_laplacian(&graph, LaplacianType::Symmetric);
554
555 assert_eq!(laplacian.expect("operation should succeed").shape(), [4, 4]);
556 }
557
558 #[test]
559 fn test_spectral_embedding() {
560 let features = randn(&[5, 3]).unwrap();
561 let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0];
562 let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
563 let graph = GraphData::new(features, edge_index);
564
565 let embedding = SpectralGraphAnalysis::spectral_embedding(&graph, 3);
566
567 assert_eq!(
568 embedding.expect("operation should succeed").shape().dims(),
569 &[5, 3]
570 );
571 }
572
573 #[test]
574 fn test_spectral_clustering() {
575 let features = randn(&[6, 2]).unwrap();
576 let edges = vec![
577 0.0, 1.0, 1.0, 2.0, 3.0, 4.0, 4.0, 5.0, ];
580 let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
581 let graph = GraphData::new(features, edge_index);
582
583 let labels = SpectralGraphAnalysis::spectral_clustering(&graph, 2);
584
585 assert_eq!(labels.expect("operation should succeed").len(), 6);
586 }
587
588 #[test]
589 fn test_cheb_conv() {
590 let features = randn(&[4, 6]).unwrap();
591 let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0];
592 let edge_index = from_vec(edges, &[2, 3], DeviceType::Cpu).unwrap();
593 let graph = GraphData::new(features, edge_index);
594
595 let cheb = ChebConv::new(6, 8, 3, true);
596 let output = cheb
597 .expect("operation should succeed")
598 .forward(&graph)
599 .expect("operation should succeed");
600
601 assert_eq!(output.x.shape().dims(), &[4, 8]);
602 }
603
604 #[test]
605 fn test_spectral_conv() {
606 let features = randn(&[5, 4]).unwrap();
607 let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0];
608 let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
609 let graph = GraphData::new(features, edge_index);
610
611 let spec_conv = SpectralConv::new(4, 6, 3, true);
612 let output = spec_conv
613 .expect("operation should succeed")
614 .forward(&graph)
615 .expect("operation should succeed");
616
617 assert_eq!(output.x.shape().dims(), &[5, 6]);
618 }
619
620 #[test]
621 fn test_graph_fourier_transform() {
622 let features = randn(&[4, 3]).unwrap();
623 let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0];
624 let edge_index = from_vec(edges, &[2, 3], DeviceType::Cpu).unwrap();
625 let graph = GraphData::new(features.clone(), edge_index);
626
627 let spectral = GraphSignalProcessing::graph_fourier_transform(&graph, &features)
628 .expect("operation should succeed");
629 let reconstructed =
630 GraphSignalProcessing::inverse_graph_fourier_transform(&graph, &spectral);
631
632 assert_eq!(
633 reconstructed
634 .expect("operation should succeed")
635 .shape()
636 .dims(),
637 features.shape().dims()
638 );
639 }
640
641 #[test]
642 fn test_low_pass_filter() {
643 let features = randn(&[5, 4]).unwrap();
644 let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0];
645 let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
646 let graph = GraphData::new(features.clone(), edge_index);
647
648 let filtered = GraphSignalProcessing::low_pass_filter(&graph, &features, 2);
649
650 assert_eq!(
651 filtered.expect("operation should succeed").shape().dims(),
652 features.shape().dims()
653 );
654 }
655}