#![allow(dead_code)]
use crate::parameter::Parameter;
use crate::{GraphData, GraphLayer};
use scirs2_core::ndarray::Array2;
use torsh_tensor::{
creation::{from_vec, randn, zeros},
Tensor,
};
#[derive(Debug, Clone, Copy)]
pub enum LaplacianType {
Unnormalized,
Symmetric,
RandomWalk,
}
pub struct SpectralGraphAnalysis;
impl SpectralGraphAnalysis {
pub fn compute_laplacian(graph: &GraphData, laplacian_type: LaplacianType) -> Array2<f32> {
let num_nodes = graph.num_nodes;
let edge_data = graph
.edge_index
.to_vec()
.expect("conversion should succeed");
let mut adj = Array2::zeros((num_nodes, num_nodes));
for i in (0..edge_data.len()).step_by(2) {
if i + 1 < edge_data.len() {
let src = edge_data[i] as usize;
let dst = edge_data[i + 1] as usize;
if src < num_nodes && dst < num_nodes {
adj[[src, dst]] = 1.0;
adj[[dst, src]] = 1.0; }
}
}
let mut degrees = vec![0.0; num_nodes];
for i in 0..num_nodes {
for j in 0..num_nodes {
degrees[i] += adj[[i, j]];
}
}
match laplacian_type {
LaplacianType::Unnormalized => {
let mut laplacian = Array2::zeros((num_nodes, num_nodes));
for i in 0..num_nodes {
laplacian[[i, i]] = degrees[i];
for j in 0..num_nodes {
laplacian[[i, j]] -= adj[[i, j]];
}
}
laplacian
}
LaplacianType::Symmetric => {
let mut laplacian = Array2::zeros((num_nodes, num_nodes));
let mut d_inv_sqrt = vec![0.0; num_nodes];
for i in 0..num_nodes {
d_inv_sqrt[i] = if degrees[i] > 0.0 {
1.0 / degrees[i].sqrt()
} else {
0.0
};
}
for i in 0..num_nodes {
laplacian[[i, i]] = 1.0;
for j in 0..num_nodes {
laplacian[[i, j]] -= d_inv_sqrt[i] * adj[[i, j]] * d_inv_sqrt[j];
}
}
laplacian
}
LaplacianType::RandomWalk => {
let mut laplacian = Array2::zeros((num_nodes, num_nodes));
let mut d_inv = vec![0.0; num_nodes];
for i in 0..num_nodes {
d_inv[i] = if degrees[i] > 0.0 {
1.0 / degrees[i]
} else {
0.0
};
}
for i in 0..num_nodes {
laplacian[[i, i]] = 1.0;
for j in 0..num_nodes {
laplacian[[i, j]] -= d_inv[i] * adj[[i, j]];
}
}
laplacian
}
}
}
pub fn spectral_embedding(graph: &GraphData, num_components: usize) -> Tensor {
let laplacian = Self::compute_laplacian(graph, LaplacianType::Symmetric);
let num_nodes = graph.num_nodes;
let mut embeddings = Vec::new();
for _comp in 0..num_components {
let mut v = vec![0.0; num_nodes];
let mut rng = scirs2_core::random::thread_rng();
for val in v.iter_mut() {
*val = rng.gen_range(-0.5..0.5);
}
for _ in 0..50 {
let mut new_v = vec![0.0; num_nodes];
for i in 0..num_nodes {
for j in 0..num_nodes {
new_v[i] += laplacian[[i, j]] * v[j];
}
}
let norm: f32 = new_v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
for val in new_v.iter_mut() {
*val /= norm;
}
}
v = new_v;
}
embeddings.extend(v);
}
from_vec(
embeddings,
&[num_nodes, num_components],
torsh_core::device::DeviceType::Cpu,
)
.expect("from_vec embeddings should succeed")
}
pub fn compute_spectrum(graph: &GraphData, num_eigenvalues: usize) -> Vec<f32> {
let _laplacian = Self::compute_laplacian(graph, LaplacianType::Symmetric);
let num_nodes = graph.num_nodes;
let mut eigenvalues = Vec::new();
for k in 0..num_eigenvalues.min(num_nodes) {
let lambda =
2.0 * (1.0 - ((k as f32 * std::f32::consts::PI) / (num_nodes as f32)).cos());
eigenvalues.push(lambda);
}
eigenvalues
}
pub fn spectral_clustering(graph: &GraphData, num_clusters: usize) -> Vec<usize> {
let num_nodes = graph.num_nodes;
let embedding = Self::spectral_embedding(graph, num_clusters);
let embedding_data = embedding.to_vec().expect("conversion should succeed");
let mut labels = vec![0; num_nodes];
let mut centroids = vec![vec![0.0; num_clusters]; num_clusters];
let mut rng = scirs2_core::random::thread_rng();
for k in 0..num_clusters {
let idx = rng.gen_range(0..num_nodes);
for d in 0..num_clusters {
centroids[k][d] = embedding_data[idx * num_clusters + d];
}
}
for _ in 0..100 {
for i in 0..num_nodes {
let mut min_dist = f32::MAX;
let mut best_cluster = 0;
for k in 0..num_clusters {
let mut dist = 0.0;
for d in 0..num_clusters {
let diff = embedding_data[i * num_clusters + d] - centroids[k][d];
dist += diff * diff;
}
if dist < min_dist {
min_dist = dist;
best_cluster = k;
}
}
labels[i] = best_cluster;
}
let mut counts = vec![0; num_clusters];
let mut new_centroids = vec![vec![0.0; num_clusters]; num_clusters];
for i in 0..num_nodes {
let cluster = labels[i];
counts[cluster] += 1;
for d in 0..num_clusters {
new_centroids[cluster][d] += embedding_data[i * num_clusters + d];
}
}
for k in 0..num_clusters {
if counts[k] > 0 {
for d in 0..num_clusters {
new_centroids[k][d] /= counts[k] as f32;
}
}
}
centroids = new_centroids;
}
labels
}
}
#[derive(Debug)]
pub struct ChebConv {
in_features: usize,
out_features: usize,
k: usize,
weights: Vec<Parameter>,
bias: Option<Parameter>,
}
impl ChebConv {
pub fn new(in_features: usize, out_features: usize, k: usize, use_bias: bool) -> Self {
let mut weights = Vec::new();
for _ in 0..k {
weights.push(Parameter::new(
randn(&[in_features, out_features]).expect("randn weights should succeed"),
));
}
let bias = if use_bias {
Some(Parameter::new(
zeros(&[out_features]).expect("zeros bias should succeed"),
))
} else {
None
};
Self {
in_features,
out_features,
k,
weights,
bias,
}
}
pub fn forward(&self, graph: &GraphData) -> GraphData {
let num_nodes = graph.num_nodes;
let laplacian = SpectralGraphAnalysis::compute_laplacian(graph, LaplacianType::Symmetric);
let lap_data: Vec<f32> = laplacian.iter().copied().collect();
let lap_tensor = from_vec(
lap_data,
&[num_nodes, num_nodes],
torsh_core::device::DeviceType::Cpu,
)
.expect("from_vec laplacian should succeed");
let mut chebyshev_polynomials = Vec::new();
chebyshev_polynomials.push(graph.x.clone());
if self.k > 1 {
let t1 = lap_tensor
.matmul(&graph.x)
.expect("operation should succeed");
chebyshev_polynomials.push(t1);
}
for i in 2..self.k {
let term1 = lap_tensor
.matmul(&chebyshev_polynomials[i - 1])
.expect("operation should succeed");
let term1_scaled = term1.mul_scalar(2.0).expect("operation should succeed");
let t_k = term1_scaled
.sub(&chebyshev_polynomials[i - 2])
.expect("operation should succeed");
chebyshev_polynomials.push(t_k);
}
let mut output =
zeros::<f32>(&[num_nodes, self.out_features]).expect("zeros output should succeed");
for (i, t_k) in chebyshev_polynomials.iter().enumerate().take(self.k) {
let weighted = t_k
.matmul(&self.weights[i].clone_data())
.expect("operation should succeed");
output = output.add(&weighted).expect("operation should succeed");
}
if let Some(ref bias) = self.bias {
output = output
.add(&bias.clone_data())
.expect("operation should succeed");
}
let mut output_graph = graph.clone();
output_graph.x = output;
output_graph
}
}
impl GraphLayer for ChebConv {
fn forward(&self, graph: &GraphData) -> GraphData {
self.forward(graph)
}
fn parameters(&self) -> Vec<Tensor> {
let mut params: Vec<_> = self.weights.iter().map(|w| w.clone_data()).collect();
if let Some(ref bias) = self.bias {
params.push(bias.clone_data());
}
params
}
}
#[derive(Debug)]
pub struct SpectralConv {
in_features: usize,
out_features: usize,
num_filters: usize,
spectral_weights: Parameter,
spatial_weight: Parameter,
bias: Option<Parameter>,
}
impl SpectralConv {
pub fn new(
in_features: usize,
out_features: usize,
num_filters: usize,
use_bias: bool,
) -> Self {
let spectral_weights = Parameter::new(
randn(&[num_filters, in_features]).expect("randn spectral_weights should succeed"),
);
let spatial_weight = Parameter::new(
randn(&[in_features, out_features]).expect("randn spatial_weight should succeed"),
);
let bias = if use_bias {
Some(Parameter::new(
zeros(&[out_features]).expect("zeros bias should succeed"),
))
} else {
None
};
Self {
in_features,
out_features,
num_filters,
spectral_weights,
spatial_weight,
bias,
}
}
pub fn forward(&self, graph: &GraphData) -> GraphData {
let _num_nodes = graph.num_nodes;
let spectral_features = SpectralGraphAnalysis::spectral_embedding(graph, self.num_filters);
let filtered = spectral_features
.matmul(&self.spectral_weights.clone_data())
.expect("operation should succeed");
let combined = filtered.add(&graph.x).expect("operation should succeed");
let mut output = combined
.matmul(&self.spatial_weight.clone_data())
.expect("operation should succeed");
if let Some(ref bias) = self.bias {
output = output
.add(&bias.clone_data())
.expect("operation should succeed");
}
let mut output_graph = graph.clone();
output_graph.x = output;
output_graph
}
}
impl GraphLayer for SpectralConv {
fn forward(&self, graph: &GraphData) -> GraphData {
self.forward(graph)
}
fn parameters(&self) -> Vec<Tensor> {
let mut params = vec![
self.spectral_weights.clone_data(),
self.spatial_weight.clone_data(),
];
if let Some(ref bias) = self.bias {
params.push(bias.clone_data());
}
params
}
}
pub struct GraphSignalProcessing;
impl GraphSignalProcessing {
pub fn graph_fourier_transform(graph: &GraphData, signal: &Tensor) -> Tensor {
let num_nodes = graph.num_nodes;
let embedding = SpectralGraphAnalysis::spectral_embedding(graph, num_nodes);
embedding
.t()
.expect("operation should succeed")
.matmul(signal)
.expect("operation should succeed")
}
pub fn inverse_graph_fourier_transform(graph: &GraphData, spectral_signal: &Tensor) -> Tensor {
let num_nodes = graph.num_nodes;
let embedding = SpectralGraphAnalysis::spectral_embedding(graph, num_nodes);
embedding
.matmul(spectral_signal)
.expect("operation should succeed")
}
pub fn low_pass_filter(graph: &GraphData, signal: &Tensor, cutoff: usize) -> Tensor {
let spectral = Self::graph_fourier_transform(graph, signal);
let mut filtered_data = spectral.to_vec().expect("conversion should succeed");
let _signal_dim = signal.shape().dims()[1];
for i in cutoff..filtered_data.len() {
filtered_data[i] = 0.0;
}
let filtered_spectral = from_vec(
filtered_data,
spectral.shape().dims(),
torsh_core::device::DeviceType::Cpu,
)
.expect("from_vec filtered_spectral should succeed");
Self::inverse_graph_fourier_transform(graph, &filtered_spectral)
}
pub fn high_pass_filter(graph: &GraphData, signal: &Tensor, cutoff: usize) -> Tensor {
let spectral = Self::graph_fourier_transform(graph, signal);
let mut filtered_data = spectral.to_vec().expect("conversion should succeed");
for i in 0..cutoff.min(filtered_data.len()) {
filtered_data[i] = 0.0;
}
let filtered_spectral = from_vec(
filtered_data,
spectral.shape().dims(),
torsh_core::device::DeviceType::Cpu,
)
.expect("from_vec filtered_spectral should succeed");
Self::inverse_graph_fourier_transform(graph, &filtered_spectral)
}
}
#[cfg(test)]
mod tests {
use super::*;
use torsh_core::device::DeviceType;
#[test]
fn test_laplacian_computation() {
let features = randn(&[4, 3]).unwrap();
let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 0.0];
let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
let graph = GraphData::new(features, edge_index);
let laplacian = SpectralGraphAnalysis::compute_laplacian(&graph, LaplacianType::Symmetric);
assert_eq!(laplacian.shape(), [4, 4]);
}
#[test]
fn test_spectral_embedding() {
let features = randn(&[5, 3]).unwrap();
let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0];
let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
let graph = GraphData::new(features, edge_index);
let embedding = SpectralGraphAnalysis::spectral_embedding(&graph, 3);
assert_eq!(embedding.shape().dims(), &[5, 3]);
}
#[test]
fn test_spectral_clustering() {
let features = randn(&[6, 2]).unwrap();
let edges = vec![
0.0, 1.0, 1.0, 2.0, 3.0, 4.0, 4.0, 5.0, ];
let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
let graph = GraphData::new(features, edge_index);
let labels = SpectralGraphAnalysis::spectral_clustering(&graph, 2);
assert_eq!(labels.len(), 6);
}
#[test]
fn test_cheb_conv() {
let features = randn(&[4, 6]).unwrap();
let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0];
let edge_index = from_vec(edges, &[2, 3], DeviceType::Cpu).unwrap();
let graph = GraphData::new(features, edge_index);
let cheb = ChebConv::new(6, 8, 3, true);
let output = cheb.forward(&graph);
assert_eq!(output.x.shape().dims(), &[4, 8]);
}
#[test]
fn test_spectral_conv() {
let features = randn(&[5, 4]).unwrap();
let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0];
let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
let graph = GraphData::new(features, edge_index);
let spec_conv = SpectralConv::new(4, 6, 3, true);
let output = spec_conv.forward(&graph);
assert_eq!(output.x.shape().dims(), &[5, 6]);
}
#[test]
fn test_graph_fourier_transform() {
let features = randn(&[4, 3]).unwrap();
let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0];
let edge_index = from_vec(edges, &[2, 3], DeviceType::Cpu).unwrap();
let graph = GraphData::new(features.clone(), edge_index);
let spectral = GraphSignalProcessing::graph_fourier_transform(&graph, &features);
let reconstructed =
GraphSignalProcessing::inverse_graph_fourier_transform(&graph, &spectral);
assert_eq!(reconstructed.shape().dims(), features.shape().dims());
}
#[test]
fn test_low_pass_filter() {
let features = randn(&[5, 4]).unwrap();
let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0];
let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
let graph = GraphData::new(features.clone(), edge_index);
let filtered = GraphSignalProcessing::low_pass_filter(&graph, &features, 2);
assert_eq!(filtered.shape().dims(), features.shape().dims());
}
}