Skip to main content

torsh_graph/conv/
sage.rs

1//! GraphSAGE (Sample and Aggregate) layer implementation
2/// Crate-local result alias: the error type defaults to [`TorshError`],
3/// so both `Result<T>` and `Result<T, OtherError>` stay valid.
4type Result<T, E = torsh_core::error::TorshError> = std::result::Result<T, E>;
5
6use crate::parameter::Parameter;
7use crate::{GraphData, GraphLayer};
8use torsh_tensor::{
9    creation::{randn, zeros},
10    Tensor,
11};
12
13/// GraphSAGE convolution layer
14#[derive(Debug)]
15pub struct SAGEConv {
16    in_features: usize,
17    out_features: usize,
18    weight_neighbor: Parameter,
19    weight_self: Parameter,
20    bias: Option<Parameter>,
21}
22
23impl SAGEConv {
24    /// Create a new GraphSAGE convolution layer
25    pub fn new(in_features: usize, out_features: usize, bias: bool) -> Result<Self> {
26        let weight_neighbor = Parameter::new(randn(&[in_features, out_features])?);
27        let weight_self = Parameter::new(randn(&[in_features, out_features])?);
28        let bias = if bias {
29            Some(Parameter::new(zeros(&[out_features])?))
30        } else {
31            None
32        };
33
34        Ok(Self {
35            in_features,
36            out_features,
37            weight_neighbor,
38            weight_self,
39            bias,
40        })
41    }
42
43    /// Get input feature dimension
44    pub fn in_features(&self) -> usize {
45        self.in_features
46    }
47
48    /// Get output feature dimension
49    pub fn out_features(&self) -> usize {
50        self.out_features
51    }
52
53    /// Apply GraphSAGE convolution
54    pub fn forward(&self, graph: &GraphData) -> Result<GraphData> {
55        let num_nodes = graph.num_nodes;
56        let edge_data = crate::utils::tensor_to_vec2::<f32>(&graph.edge_index)?;
57
58        // Build adjacency list for efficient neighbor aggregation
59        let mut adjacency_list: Vec<Vec<usize>> = vec![Vec::new(); num_nodes];
60        for j in 0..edge_data[0].len() {
61            let src = edge_data[0][j] as usize;
62            let dst = edge_data[1][j] as usize;
63            adjacency_list[dst].push(src);
64        }
65
66        // Aggregate neighbor features (mean aggregation)
67        let mut neighbor_features = zeros(&[num_nodes, self.in_features])?;
68
69        for node in 0..num_nodes {
70            if !adjacency_list[node].is_empty() {
71                let mut aggregated = zeros(&[self.in_features])?;
72
73                for &neighbor in &adjacency_list[node] {
74                    let neighbor_slice = graph.x.slice(0, neighbor, neighbor + 1)?.to_tensor()?;
75                    let neighbor_feat = neighbor_slice.squeeze(0)?;
76                    aggregated = aggregated.add(&neighbor_feat)?;
77                }
78
79                // Mean aggregation
80                aggregated = aggregated.div_scalar(adjacency_list[node].len() as f32)?;
81                // Store aggregated features for this node
82                let aggregated_data = aggregated.to_vec()?;
83                for (i, &value) in aggregated_data.iter().enumerate() {
84                    neighbor_features.set_item(&[node, i], value)?;
85                }
86            }
87        }
88
89        // Transform neighbor features and self features
90        let neighbor_transformed = neighbor_features.matmul(&self.weight_neighbor.clone_data())?;
91        let self_transformed = graph.x.matmul(&self.weight_self.clone_data())?;
92
93        // Combine neighbor and self representations
94        let mut output_features = neighbor_transformed.add(&self_transformed)?;
95
96        // Add bias if present
97        if let Some(ref bias) = self.bias {
98            output_features = output_features.add(&bias.clone_data())?;
99        }
100
101        // L2 normalize the output features (common in GraphSAGE)
102        // For simplicity, using standard normalization instead of row-wise normalization
103        let norm_val = output_features.norm()?;
104        let epsilon = 1e-8_f32;
105        let norm_scalar = norm_val.item()?.max(epsilon);
106        output_features = output_features.div_scalar(norm_scalar)?;
107
108        // Create output graph
109        Ok(GraphData {
110            x: output_features,
111            edge_index: graph.edge_index.clone(),
112            edge_attr: graph.edge_attr.clone(),
113            batch: graph.batch.clone(),
114            num_nodes: graph.num_nodes,
115            num_edges: graph.num_edges,
116        })
117    }
118}
119
120impl GraphLayer for SAGEConv {
121    fn forward(&self, graph: &GraphData) -> Result<GraphData> {
122        self.forward(graph)
123    }
124
125    fn parameters(&self) -> Vec<Tensor> {
126        let mut params = vec![
127            self.weight_neighbor.clone_data(),
128            self.weight_self.clone_data(),
129        ];
130        if let Some(ref bias) = self.bias {
131            params.push(bias.clone_data());
132        }
133        params
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use torsh_core::device::DeviceType;
141    use torsh_tensor::creation::from_vec;
142
143    #[test]
144    fn test_sage_creation() {
145        let sage = SAGEConv::new(10, 20, true);
146        let params = sage.expect("operation should succeed").parameters();
147        assert_eq!(params.len(), 3); // weight_neighbor + weight_self + bias
148    }
149
150    #[test]
151    fn test_sage_forward() {
152        let sage = SAGEConv::new(4, 8, false);
153
154        // Create test graph
155        let x = from_vec(
156            vec![
157                1.0, 2.0, 3.0, 4.0, // node 0
158                5.0, 6.0, 7.0, 8.0, // node 1
159                9.0, 10.0, 11.0, 12.0, // node 2
160            ],
161            &[3, 4],
162            DeviceType::Cpu,
163        )
164        .expect("operation should succeed");
165        let edge_index = from_vec(vec![0.0, 1.0, 2.0, 1.0, 2.0, 0.0], &[2, 3], DeviceType::Cpu)
166            .expect("from vec should succeed");
167        let graph = GraphData::new(x, edge_index);
168
169        let output = sage
170            .expect("operation should succeed")
171            .forward(&graph)
172            .expect("operation should succeed");
173        assert_eq!(output.x.shape().dims(), &[3, 8]);
174        assert_eq!(output.num_nodes, 3);
175
176        // Check that output is finite (simplified test since norm_dim doesn't exist)
177        let output_values = output
178            .x
179            .to_vec()
180            .expect("tensor to_vec conversion should succeed");
181        for &val in &output_values {
182            assert!(val.is_finite(), "Output should be finite");
183        }
184    }
185}