use crate::error::{NeuralError, Result};
#[derive(Debug, Clone)]
pub struct Graph {
pub num_nodes: usize,
pub adjacency: Vec<Vec<f32>>,
pub node_features: Vec<Vec<f32>>,
pub edge_weights: Option<Vec<Vec<f32>>>,
}
impl Graph {
pub fn new(num_nodes: usize, feature_dim: usize) -> Self {
Graph {
num_nodes,
adjacency: vec![vec![0.0_f32; num_nodes]; num_nodes],
node_features: vec![vec![0.0_f32; feature_dim]; num_nodes],
edge_weights: None,
}
}
pub fn add_edge(&mut self, src: usize, dst: usize, weight: f32) -> Result<()> {
if src >= self.num_nodes || dst >= self.num_nodes {
return Err(NeuralError::InvalidArgument(format!(
"Edge ({src}, {dst}) out of bounds for graph with {} nodes",
self.num_nodes
)));
}
self.adjacency[src][dst] = weight;
Ok(())
}
pub fn add_undirected_edge(&mut self, src: usize, dst: usize) -> Result<()> {
self.add_edge(src, dst, 1.0)?;
self.add_edge(dst, src, 1.0)?;
Ok(())
}
pub fn set_node_features(&mut self, node: usize, features: Vec<f32>) -> Result<()> {
if node >= self.num_nodes {
return Err(NeuralError::InvalidArgument(format!(
"Node index {node} out of bounds for graph with {} nodes",
self.num_nodes
)));
}
self.node_features[node] = features;
Ok(())
}
pub fn degree_matrix(&self) -> Vec<f32> {
(0..self.num_nodes)
.map(|i| self.adjacency[i].iter().copied().sum::<f32>())
.collect()
}
pub fn normalized_adjacency(&self) -> Vec<Vec<f32>> {
let n = self.num_nodes;
let mut a_tilde: Vec<Vec<f32>> = (0..n)
.map(|i| {
let mut row = self.adjacency[i].clone();
row[i] += 1.0; row
})
.collect();
let d_tilde: Vec<f32> = (0..n)
.map(|i| a_tilde[i].iter().copied().sum::<f32>())
.collect();
let d_inv_sqrt: Vec<f32> = d_tilde
.iter()
.map(|&d| if d > 0.0 { 1.0 / d.sqrt() } else { 0.0 })
.collect();
for i in 0..n {
for j in 0..n {
a_tilde[i][j] *= d_inv_sqrt[i] * d_inv_sqrt[j];
}
}
a_tilde
}
pub fn neighbors(&self, node: usize) -> Vec<usize> {
if node >= self.num_nodes {
return Vec::new();
}
self.adjacency[node]
.iter()
.enumerate()
.filter_map(|(j, &w)| if w > 0.0 { Some(j) } else { None })
.collect()
}
pub fn num_edges(&self) -> usize {
self.adjacency
.iter()
.flat_map(|row| row.iter())
.filter(|&&w| w > 0.0)
.count()
}
pub fn feature_dim(&self) -> usize {
self.node_features.first().map(|v| v.len()).unwrap_or(0)
}
}
#[derive(Debug, Clone)]
pub struct SparseGraph {
pub num_nodes: usize,
pub row_ptr: Vec<usize>,
pub col_idx: Vec<usize>,
pub values: Vec<f32>,
pub node_features: Vec<Vec<f32>>,
}
impl SparseGraph {
pub fn from_edges(
num_nodes: usize,
edges: &[(usize, usize, f32)],
features: Vec<Vec<f32>>,
) -> Result<Self> {
if features.len() != num_nodes {
return Err(NeuralError::InvalidArgument(format!(
"features.len() ({}) must equal num_nodes ({})",
features.len(),
num_nodes
)));
}
for &(src, dst, _) in edges {
if src >= num_nodes || dst >= num_nodes {
return Err(NeuralError::InvalidArgument(format!(
"Edge ({src}, {dst}) out of bounds for graph with {num_nodes} nodes"
)));
}
}
let mut sorted_edges: Vec<(usize, usize, f32)> = edges.to_vec();
sorted_edges.sort_by_key(|&(s, d, _)| (s, d));
let num_edges = sorted_edges.len();
let mut row_ptr = vec![0usize; num_nodes + 1];
let mut col_idx = Vec::with_capacity(num_edges);
let mut values = Vec::with_capacity(num_edges);
for &(src, _, _) in &sorted_edges {
row_ptr[src + 1] += 1;
}
for i in 0..num_nodes {
row_ptr[i + 1] += row_ptr[i];
}
for &(_, dst, w) in &sorted_edges {
col_idx.push(dst);
values.push(w);
}
Ok(SparseGraph {
num_nodes,
row_ptr,
col_idx,
values,
node_features: features,
})
}
pub fn neighbors(&self, node: usize) -> &[usize] {
if node >= self.num_nodes {
return &[];
}
&self.col_idx[self.row_ptr[node]..self.row_ptr[node + 1]]
}
pub fn edge_weights_of(&self, node: usize) -> &[f32] {
if node >= self.num_nodes {
return &[];
}
&self.values[self.row_ptr[node]..self.row_ptr[node + 1]]
}
fn degrees(&self) -> Vec<f32> {
(0..self.num_nodes)
.map(|i| self.edge_weights_of(i).iter().copied().sum::<f32>())
.collect()
}
pub fn normalized_laplacian(&self) -> Result<Self> {
let d = self.degrees();
let d_inv_sqrt: Vec<f32> = d
.iter()
.map(|°| if deg > 0.0 { 1.0 / deg.sqrt() } else { 0.0 })
.collect();
let d_ref = &d_inv_sqrt;
let new_values: Vec<f32> = (0..self.num_nodes)
.flat_map(|i| {
let start = self.row_ptr[i];
let end = self.row_ptr[i + 1];
let di = d_ref[i];
let col_slice: Vec<usize> = self.col_idx[start..end].to_vec();
let val_slice: Vec<f32> = self.values[start..end].to_vec();
let d_clone: Vec<f32> = d_ref.to_vec();
(0..(end - start)).map(move |idx| {
let j = col_slice[idx];
let a_ij = val_slice[idx];
-di * a_ij * d_clone[j]
})
})
.collect();
Ok(SparseGraph {
num_nodes: self.num_nodes,
row_ptr: self.row_ptr.clone(),
col_idx: self.col_idx.clone(),
values: new_values,
node_features: self.node_features.clone(),
})
}
pub fn num_edges(&self) -> usize {
self.col_idx.len()
}
pub fn feature_dim(&self) -> usize {
self.node_features.first().map(|v| v.len()).unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_graph_new_and_add_edge() {
let mut g = Graph::new(4, 3);
g.add_edge(0, 1, 1.0).expect("add_edge failed");
g.add_edge(1, 2, 0.5).expect("add_edge failed");
g.add_undirected_edge(2, 3).expect("add_undirected failed");
assert_eq!(g.num_edges(), 4); }
#[test]
fn test_graph_neighbors() {
let mut g = Graph::new(3, 2);
g.add_edge(0, 1, 1.0).expect("ok");
g.add_edge(0, 2, 1.0).expect("ok");
let nb = g.neighbors(0);
assert_eq!(nb.len(), 2);
assert!(nb.contains(&1));
assert!(nb.contains(&2));
let nb1 = g.neighbors(1);
assert!(nb1.is_empty());
}
#[test]
fn test_graph_degree_matrix() {
let mut g = Graph::new(3, 1);
g.add_edge(0, 1, 2.0).expect("ok");
g.add_edge(0, 2, 3.0).expect("ok");
let d = g.degree_matrix();
assert!((d[0] - 5.0).abs() < 1e-6);
assert!((d[1]).abs() < 1e-6);
}
#[test]
fn test_normalized_adjacency() {
let mut g = Graph::new(2, 1);
g.add_edge(0, 1, 1.0).expect("ok");
g.add_edge(1, 0, 1.0).expect("ok");
let norm = g.normalized_adjacency();
let expected_diag = 0.5_f32;
assert!((norm[0][0] - expected_diag).abs() < 1e-5, "diag={}", norm[0][0]);
}
#[test]
fn test_sparse_graph_from_edges() {
let edges = vec![(0, 1, 1.0_f32), (1, 2, 1.0), (2, 0, 1.0)];
let features = vec![vec![1.0_f32, 0.0], vec![0.0, 1.0], vec![1.0, 1.0]];
let sg = SparseGraph::from_edges(3, &edges, features).expect("from_edges failed");
assert_eq!(sg.num_edges(), 3);
assert_eq!(sg.neighbors(0), &[1]);
assert_eq!(sg.neighbors(1), &[2]);
assert_eq!(sg.neighbors(2), &[0]);
}
#[test]
fn test_sparse_graph_invalid_features_len() {
let edges = vec![(0, 1, 1.0_f32)];
let features = vec![vec![1.0_f32]]; let result = SparseGraph::from_edges(2, &edges, features);
assert!(result.is_err());
}
#[test]
fn test_sparse_normalized_laplacian() {
let edges = vec![(0, 1, 1.0_f32), (1, 0, 1.0)];
let features = vec![vec![0.0_f32; 2]; 2];
let sg = SparseGraph::from_edges(2, &edges, features).expect("ok");
let lap = sg.normalized_laplacian().expect("laplacian ok");
assert!((lap.values[0] - (-1.0)).abs() < 1e-5);
}
#[test]
fn test_graph_out_of_bounds_edge() {
let mut g = Graph::new(3, 1);
assert!(g.add_edge(0, 99, 1.0).is_err());
}
}