Skip to main content

torsh_graph/
hypergraph.rs

1//! Hypergraph Neural Networks
2//!
3//! Advanced implementation of hypergraph neural networks for multi-relational learning.
4//! Hypergraphs generalize graphs by allowing edges (hyperedges) to connect any number of nodes,
5//! enabling modeling of complex multi-way relationships in data.
6//!
7//! # Features:
8//! - Hypergraph data structures with efficient storage
9//! - Multiple hypergraph convolution layers (HGCN, HyperGAT, HGNN)
10//! - Hypergraph attention mechanisms
11//! - Advanced pooling and coarsening operations
12//! - Spectral hypergraph methods
13//! - Dynamic hypergraph construction
14// Framework infrastructure - components designed for future use
15#![allow(dead_code)]
16/// Crate-local result alias: the error type defaults to [`TorshError`],
17/// so both `Result<T>` and `Result<T, OtherError>` stay valid.
18type Result<T, E = torsh_core::error::TorshError> = std::result::Result<T, E>;
19
20use crate::parameter::Parameter;
21use crate::{GraphData, GraphLayer};
22use torsh_tensor::{
23    creation::{from_vec, randn, zeros},
24    Tensor,
25};
26
27/// Hypergraph data structure representing multi-way relationships
28#[derive(Debug, Clone)]
29pub struct HypergraphData {
30    /// Node feature matrix (num_nodes x num_features)
31    pub x: Tensor,
32    /// Hyperedge incidence matrix (num_nodes x num_hyperedges)
33    pub incidence_matrix: Tensor,
34    /// Hyperedge weights (optional)
35    pub hyperedge_weights: Option<Tensor>,
36    /// Hyperedge features (optional)
37    pub hyperedge_features: Option<Tensor>,
38    /// Node degrees (sum of incident hyperedge weights)
39    pub node_degrees: Tensor,
40    /// Hyperedge cardinalities (number of nodes per hyperedge)
41    pub hyperedge_cardinalities: Tensor,
42    /// Number of nodes
43    pub num_nodes: usize,
44    /// Number of hyperedges
45    pub num_hyperedges: usize,
46}
47
48impl HypergraphData {
49    /// Create a new hypergraph from node features and incidence matrix
50    pub fn new(x: Tensor, incidence_matrix: Tensor) -> Result<Self> {
51        let num_nodes = x.shape().dims()[0];
52        let num_hyperedges = incidence_matrix.shape().dims()[1];
53
54        // Compute node degrees (sum over hyperedges - axis 1)
55        let node_degrees = incidence_matrix.sum_dim(&[1], false)?;
56
57        // Compute hyperedge cardinalities (sum over nodes - axis 0)
58        let hyperedge_cardinalities = incidence_matrix.sum_dim(&[0], false)?;
59
60        Ok(Self {
61            x,
62            incidence_matrix,
63            hyperedge_weights: None,
64            hyperedge_features: None,
65            node_degrees,
66            hyperedge_cardinalities,
67            num_nodes,
68            num_hyperedges,
69        })
70    }
71
72    /// Add hyperedge weights
73    pub fn with_hyperedge_weights(mut self, weights: Tensor) -> Self {
74        self.hyperedge_weights = Some(weights);
75        self
76    }
77
78    /// Add hyperedge features
79    pub fn with_hyperedge_features(mut self, features: Tensor) -> Self {
80        self.hyperedge_features = Some(features);
81        self
82    }
83
84    /// Convert to regular graph using clique expansion
85    pub fn to_graph_clique_expansion(&self) -> Result<GraphData> {
86        let incidence_data = self.incidence_matrix.to_vec()?;
87        let mut edges = Vec::new();
88
89        // For each hyperedge, create clique (all pairs of nodes)
90        for e in 0..self.num_hyperedges {
91            let mut nodes_in_hyperedge = Vec::new();
92
93            // Find nodes in this hyperedge
94            for v in 0..self.num_nodes {
95                let idx = v * self.num_hyperedges + e;
96                if incidence_data[idx] > 0.0 {
97                    nodes_in_hyperedge.push(v as f32);
98                }
99            }
100
101            // Create all pairs within the hyperedge
102            for i in 0..nodes_in_hyperedge.len() {
103                for j in (i + 1)..nodes_in_hyperedge.len() {
104                    edges.extend_from_slice(&[nodes_in_hyperedge[i], nodes_in_hyperedge[j]]);
105                    edges.extend_from_slice(&[nodes_in_hyperedge[j], nodes_in_hyperedge[i]]);
106                }
107            }
108        }
109
110        let edge_index = if edges.is_empty() {
111            zeros(&[2, 0])?
112        } else {
113            let num_edges = edges.len() / 2;
114            from_vec(edges, &[2, num_edges], torsh_core::device::DeviceType::Cpu)?
115        };
116
117        Ok(GraphData::new(self.x.clone(), edge_index))
118    }
119
120    /// Convert to regular graph using star expansion
121    pub fn to_graph_star_expansion(&self) -> Result<GraphData> {
122        let incidence_data = self.incidence_matrix.to_vec()?;
123        let mut edges = Vec::new();
124
125        // For each hyperedge, create a star with center at virtual node
126        let virtual_node_offset = self.num_nodes;
127
128        for e in 0..self.num_hyperedges {
129            let virtual_node = (virtual_node_offset + e) as f32;
130
131            // Connect all nodes in hyperedge to virtual center
132            for v in 0..self.num_nodes {
133                let idx = v * self.num_hyperedges + e;
134                if incidence_data[idx] > 0.0 {
135                    let node = v as f32;
136                    edges.extend_from_slice(&[node, virtual_node]);
137                    edges.extend_from_slice(&[virtual_node, node]);
138                }
139            }
140        }
141
142        let edge_index = if edges.is_empty() {
143            zeros(&[2, 0])?
144        } else {
145            let num_edges = edges.len() / 2;
146            from_vec(edges, &[2, num_edges], torsh_core::device::DeviceType::Cpu)?
147        };
148
149        // Extend node features with virtual nodes
150        let virtual_features: Tensor = randn(&[self.num_hyperedges, self.x.shape().dims()[1]])?;
151        // Concatenate original and virtual node features
152        let node_data = self.x.to_vec()?;
153        let virtual_data = virtual_features.to_vec()?;
154        let mut extended_data = node_data;
155        extended_data.extend(virtual_data);
156
157        let total_nodes = self.num_nodes + self.num_hyperedges;
158        let features_dim = self.x.shape().dims()[1];
159        let extended_x = from_vec(
160            extended_data,
161            &[total_nodes, features_dim],
162            torsh_core::device::DeviceType::Cpu,
163        )?;
164
165        Ok(GraphData::new(extended_x, edge_index))
166    }
167}
168
169/// Hypergraph Convolutional Network (HGCN) layer
170#[derive(Debug)]
171pub struct HGCNConv {
172    in_features: usize,
173    out_features: usize,
174    weight: Parameter,
175    bias: Option<Parameter>,
176    use_attention: bool,
177    attention_weight: Option<Parameter>,
178    dropout: f32,
179}
180
181impl HGCNConv {
182    /// Create a new HGCN layer
183    pub fn new(
184        in_features: usize,
185        out_features: usize,
186        bias: bool,
187        use_attention: bool,
188        dropout: f32,
189    ) -> Result<Self> {
190        let weight = Parameter::new(randn(&[in_features, out_features])?);
191        let bias = if bias {
192            Some(Parameter::new(zeros(&[out_features])?))
193        } else {
194            None
195        };
196
197        let attention_weight = if use_attention {
198            Some(Parameter::new(randn(&[out_features])?))
199        } else {
200            None
201        };
202
203        Ok(Self {
204            in_features,
205            out_features,
206            weight,
207            bias,
208            use_attention,
209            attention_weight,
210            dropout,
211        })
212    }
213
214    /// Forward pass through HGCN layer
215    pub fn forward(&self, hypergraph: &HypergraphData) -> Result<HypergraphData> {
216        // Simplified implementation for API compatibility
217        // Step 1: Transform node features
218        let node_features_transformed = hypergraph.x.matmul(&self.weight.clone_data())?;
219
220        // Step 2: Simplified hypergraph convolution (skip complex aggregation for now)
221        let output_features = if let Some(ref bias) = self.bias {
222            node_features_transformed.add(&bias.clone_data())?
223        } else {
224            node_features_transformed
225        };
226
227        // Create output hypergraph with updated node features
228        Ok(HypergraphData {
229            x: output_features,
230            incidence_matrix: hypergraph.incidence_matrix.clone(),
231            hyperedge_weights: hypergraph.hyperedge_weights.clone(),
232            hyperedge_features: hypergraph.hyperedge_features.clone(),
233            node_degrees: hypergraph.node_degrees.clone(),
234            hyperedge_cardinalities: hypergraph.hyperedge_cardinalities.clone(),
235            num_nodes: hypergraph.num_nodes,
236            num_hyperedges: hypergraph.num_hyperedges,
237        })
238    }
239
240    /// Apply attention mechanism to hyperedge features
241    fn apply_attention(
242        &self,
243        hyperedge_features: &Tensor,
244        _hypergraph: &HypergraphData,
245    ) -> Result<Tensor> {
246        if let Some(ref attention_weight) = self.attention_weight {
247            // Compute attention scores
248            let attention_scores = hyperedge_features.matmul(&attention_weight.clone_data())?;
249            let attention_probs = attention_scores.softmax(-1)?;
250
251            // Apply attention to features
252            let attention_expanded = attention_probs.unsqueeze(-1)?;
253            Ok(hyperedge_features.mul(&attention_expanded)?)
254        } else {
255            Ok(hyperedge_features.clone())
256        }
257    }
258
259    /// Normalize aggregated features by node degrees
260    fn normalize_by_degrees(
261        &self,
262        features: &Tensor,
263        hypergraph: &HypergraphData,
264    ) -> Result<Tensor> {
265        let degrees = &hypergraph.node_degrees;
266        let epsilon = 1e-8;
267
268        // Add epsilon to prevent division by zero
269        let safe_degrees = degrees.add_scalar(epsilon)?;
270        let inv_degrees = safe_degrees.reciprocal()?;
271
272        // Expand inverse degrees to match feature dimensions
273        // First squeeze to ensure we have shape [num_nodes] rather than [num_nodes, 1]
274        let inv_degrees_squeezed = if inv_degrees.shape().dims().len() > 1 {
275            inv_degrees.squeeze_tensor(1)?
276        } else {
277            inv_degrees
278        };
279        let inv_degrees_expanded = inv_degrees_squeezed.unsqueeze(-1)?;
280        Ok(features.mul(&inv_degrees_expanded)?)
281    }
282}
283
284impl GraphLayer for HGCNConv {
285    fn forward(&self, graph: &GraphData) -> Result<GraphData> {
286        // Convert regular graph to hypergraph and back for compatibility
287        let hypergraph = graph_to_hypergraph(graph)?;
288        let output_hypergraph = HGCNConv::forward(self, &hypergraph)?;
289        output_hypergraph.to_graph_clique_expansion()
290    }
291
292    fn parameters(&self) -> Vec<Tensor> {
293        let mut params = vec![self.weight.clone_data()];
294        if let Some(ref bias) = self.bias {
295            params.push(bias.clone_data());
296        }
297        if let Some(ref attention_weight) = self.attention_weight {
298            params.push(attention_weight.clone_data());
299        }
300        params
301    }
302}
303
304/// Hypergraph Attention Network (HyperGAT) layer
305#[derive(Debug)]
306pub struct HyperGATConv {
307    in_features: usize,
308    out_features: usize,
309    heads: usize,
310    query_weight: Parameter,
311    key_weight: Parameter,
312    value_weight: Parameter,
313    hyperedge_attention: Parameter,
314    output_weight: Parameter,
315    bias: Option<Parameter>,
316    dropout: f32,
317}
318
319impl HyperGATConv {
320    /// Create a new HyperGAT layer
321    pub fn new(
322        in_features: usize,
323        out_features: usize,
324        heads: usize,
325        dropout: f32,
326        bias: bool,
327    ) -> Result<Self> {
328        let head_dim = out_features / heads;
329
330        let query_weight = Parameter::new(randn(&[in_features, out_features])?);
331        let key_weight = Parameter::new(randn(&[in_features, out_features])?);
332        let value_weight = Parameter::new(randn(&[in_features, out_features])?);
333        let hyperedge_attention = Parameter::new(randn(&[heads, 2 * head_dim])?);
334        let output_weight = Parameter::new(randn(&[out_features, out_features])?);
335
336        let bias = if bias {
337            Some(Parameter::new(zeros(&[out_features])?))
338        } else {
339            None
340        };
341
342        Ok(Self {
343            in_features,
344            out_features,
345            heads,
346            query_weight,
347            key_weight,
348            value_weight,
349            hyperedge_attention,
350            output_weight,
351            bias,
352            dropout,
353        })
354    }
355
356    /// Forward pass through HyperGAT layer
357    pub fn forward(&self, hypergraph: &HypergraphData) -> Result<HypergraphData> {
358        let num_nodes = hypergraph.num_nodes;
359        let head_dim = self.out_features / self.heads;
360
361        // Linear transformations
362        let queries = hypergraph.x.matmul(&self.query_weight.clone_data())?;
363        let keys = hypergraph.x.matmul(&self.key_weight.clone_data())?;
364        let values = hypergraph.x.matmul(&self.value_weight.clone_data())?;
365
366        // Reshape for multi-head attention
367        let q = queries.view(&[num_nodes as i32, self.heads as i32, head_dim as i32])?;
368        let k = keys.view(&[num_nodes as i32, self.heads as i32, head_dim as i32])?;
369        let v = values.view(&[num_nodes as i32, self.heads as i32, head_dim as i32])?;
370
371        // Perform hyperedge-based attention
372        let attended_features = self.hyperedge_attention_mechanism(&q, &k, &v, hypergraph);
373
374        // Reshape back and apply output transformation
375        let concatenated =
376            attended_features?.view(&[num_nodes as i32, self.out_features as i32])?;
377        let mut output = concatenated.matmul(&self.output_weight.clone_data())?;
378
379        // Add bias if present
380        if let Some(ref bias) = self.bias {
381            output = output.add(&bias.clone_data())?;
382        }
383
384        // Create output hypergraph
385        Ok(HypergraphData {
386            x: output,
387            incidence_matrix: hypergraph.incidence_matrix.clone(),
388            hyperedge_weights: hypergraph.hyperedge_weights.clone(),
389            hyperedge_features: hypergraph.hyperedge_features.clone(),
390            node_degrees: hypergraph.node_degrees.clone(),
391            hyperedge_cardinalities: hypergraph.hyperedge_cardinalities.clone(),
392            num_nodes: hypergraph.num_nodes,
393            num_hyperedges: hypergraph.num_hyperedges,
394        })
395    }
396
397    /// Hyperedge-based attention mechanism
398    fn hyperedge_attention_mechanism(
399        &self,
400        q: &Tensor,
401        k: &Tensor,
402        v: &Tensor,
403        hypergraph: &HypergraphData,
404    ) -> Result<Tensor> {
405        let num_nodes = hypergraph.num_nodes;
406        let head_dim = self.out_features / self.heads;
407
408        // Initialize output
409        let mut output = zeros(&[num_nodes, self.heads, head_dim])?;
410
411        let incidence_data = hypergraph.incidence_matrix.to_vec()?;
412
413        // Process each hyperedge separately
414        for e in 0..hypergraph.num_hyperedges {
415            let mut nodes_in_hyperedge = Vec::new();
416
417            // Find nodes in this hyperedge
418            for v in 0..num_nodes {
419                let idx = v * hypergraph.num_hyperedges + e;
420                if incidence_data[idx] > 0.0 {
421                    nodes_in_hyperedge.push(v);
422                }
423            }
424
425            if nodes_in_hyperedge.len() < 2 {
426                continue; // Skip hyperedges with less than 2 nodes
427            }
428
429            // Compute attention within hyperedge for each head
430            for head in 0..self.heads {
431                self.compute_hyperedge_attention(head, &nodes_in_hyperedge, q, k, v, &mut output)?;
432            }
433        }
434
435        Ok(output)
436    }
437
438    /// Compute attention for a specific hyperedge and head
439    fn compute_hyperedge_attention(
440        &self,
441        head: usize,
442        nodes: &[usize],
443        q: &Tensor,
444        k: &Tensor,
445        v: &Tensor,
446        output: &mut Tensor,
447    ) -> Result<()> {
448        let head_dim = self.out_features / self.heads;
449        let scale = 1.0 / (head_dim as f32).sqrt();
450
451        // For simplicity, use mean pooling within hyperedge
452        // In practice, this would use more sophisticated attention
453        for &node_i in nodes {
454            let mut aggregated = zeros(&[head_dim])?;
455            let mut total_weight = 0.0;
456
457            for &node_j in nodes {
458                if node_i != node_j {
459                    // Get query and key for these nodes and head
460                    let q_i = q
461                        .slice_tensor(0, node_i, node_i + 1)?
462                        .slice_tensor(1, head, head + 1)?
463                        .squeeze_tensor(0)?
464                        .squeeze_tensor(0)?;
465
466                    let k_j = k
467                        .slice_tensor(0, node_j, node_j + 1)?
468                        .slice_tensor(1, head, head + 1)?
469                        .squeeze_tensor(0)?
470                        .squeeze_tensor(0)?;
471
472                    let v_j = v
473                        .slice_tensor(0, node_j, node_j + 1)?
474                        .slice_tensor(1, head, head + 1)?
475                        .squeeze_tensor(0)?
476                        .squeeze_tensor(0)?;
477
478                    // Compute attention weight (simplified)
479                    let attention_score = q_i.dot(&k_j)?.mul_scalar(scale)?;
480                    let weight = attention_score.exp()?.item()?;
481
482                    // Aggregate values
483                    let weighted_value = v_j.mul_scalar(weight)?;
484                    aggregated = aggregated.add(&weighted_value)?;
485                    total_weight += weight;
486                }
487            }
488
489            // Normalize and update output
490            if total_weight > 0.0 {
491                aggregated = aggregated.div_scalar(total_weight)?;
492
493                // Update output tensor (simplified assignment)
494                let aggregated_data = aggregated.to_vec()?;
495                for (j, &val) in aggregated_data.iter().enumerate() {
496                    output.set_item(&[node_i, head, j], val)?;
497                }
498            }
499        }
500
501        Ok(())
502    }
503}
504
505impl GraphLayer for HyperGATConv {
506    fn forward(&self, graph: &GraphData) -> Result<GraphData> {
507        let hypergraph = graph_to_hypergraph(graph)?;
508        let output_hypergraph = HyperGATConv::forward(self, &hypergraph)?;
509        output_hypergraph.to_graph_clique_expansion()
510    }
511
512    fn parameters(&self) -> Vec<Tensor> {
513        let mut params = vec![
514            self.query_weight.clone_data(),
515            self.key_weight.clone_data(),
516            self.value_weight.clone_data(),
517            self.hyperedge_attention.clone_data(),
518            self.output_weight.clone_data(),
519        ];
520
521        if let Some(ref bias) = self.bias {
522            params.push(bias.clone_data());
523        }
524
525        params
526    }
527}
528
529/// Hypergraph Neural Network (HGNN) layer based on spectral methods
530#[derive(Debug)]
531pub struct HGNNConv {
532    in_features: usize,
533    out_features: usize,
534    weight: Parameter,
535    bias: Option<Parameter>,
536    use_spectral: bool,
537}
538
539impl HGNNConv {
540    /// Create a new HGNN layer
541    pub fn new(
542        in_features: usize,
543        out_features: usize,
544        bias: bool,
545        use_spectral: bool,
546    ) -> Result<Self> {
547        let weight = Parameter::new(randn(&[in_features, out_features])?);
548        let bias = if bias {
549            Some(Parameter::new(zeros(&[out_features])?))
550        } else {
551            None
552        };
553
554        Ok(Self {
555            in_features,
556            out_features,
557            weight,
558            bias,
559            use_spectral,
560        })
561    }
562
563    /// Forward pass through HGNN layer
564    pub fn forward(&self, hypergraph: &HypergraphData) -> Result<HypergraphData> {
565        // Transform node features
566        let x_transformed = hypergraph.x.matmul(&self.weight.clone_data())?;
567
568        // Compute hypergraph Laplacian and apply convolution
569        let output_features = if self.use_spectral {
570            self.spectral_convolution(&x_transformed, hypergraph)
571        } else {
572            self.spatial_convolution(&x_transformed, hypergraph)
573        };
574
575        // Add bias if present
576        let output_features = output_features?;
577        let final_features = if let Some(ref bias) = self.bias {
578            output_features.add(&bias.clone_data())?
579        } else {
580            output_features
581        };
582
583        Ok(HypergraphData {
584            x: final_features,
585            incidence_matrix: hypergraph.incidence_matrix.clone(),
586            hyperedge_weights: hypergraph.hyperedge_weights.clone(),
587            hyperedge_features: hypergraph.hyperedge_features.clone(),
588            node_degrees: hypergraph.node_degrees.clone(),
589            hyperedge_cardinalities: hypergraph.hyperedge_cardinalities.clone(),
590            num_nodes: hypergraph.num_nodes,
591            num_hyperedges: hypergraph.num_hyperedges,
592        })
593    }
594
595    /// Spectral convolution using hypergraph Laplacian
596    fn spectral_convolution(
597        &self,
598        features: &Tensor,
599        hypergraph: &HypergraphData,
600    ) -> Result<Tensor> {
601        // Compute normalized hypergraph Laplacian
602        let laplacian = self.compute_hypergraph_laplacian(hypergraph);
603
604        // Apply Laplacian: L @ X
605        Ok(laplacian?.matmul(features)?)
606    }
607
608    /// Spatial convolution using incidence matrix
609    fn spatial_convolution(
610        &self,
611        features: &Tensor,
612        hypergraph: &HypergraphData,
613    ) -> Result<Tensor> {
614        // Node-to-hyperedge aggregation
615        let incidence_t = hypergraph.incidence_matrix.transpose(0, 1)?;
616        let hyperedge_features = incidence_t.matmul(features)?;
617
618        // Hyperedge-to-node aggregation
619        let aggregated = hypergraph.incidence_matrix.matmul(&hyperedge_features)?;
620
621        // Normalize by node degrees
622        self.normalize_by_degrees(&aggregated, hypergraph)
623    }
624
625    /// Compute normalized hypergraph Laplacian
626    fn compute_hypergraph_laplacian(&self, hypergraph: &HypergraphData) -> Result<Tensor> {
627        let h = &hypergraph.incidence_matrix;
628        let num_nodes = hypergraph.num_nodes;
629
630        // Compute degree matrices
631        let node_degrees = h.sum_dim(&[1], false)?;
632        let hyperedge_degrees = h.sum_dim(&[0], false)?;
633
634        // Create diagonal degree matrices (simplified)
635        let mut d_v = zeros(&[num_nodes, num_nodes])?;
636        let mut d_e = zeros(&[hypergraph.num_hyperedges, hypergraph.num_hyperedges])?;
637
638        let node_deg_data = node_degrees.to_vec()?;
639        let hyperedge_deg_data = hyperedge_degrees.to_vec()?;
640
641        // Fill diagonal matrices
642        for i in 0..num_nodes {
643            let degree = node_deg_data[i].max(1e-8); // Avoid division by zero
644            d_v.set_item(&[i, i], degree.powf(-0.5))?;
645        }
646
647        for i in 0..hypergraph.num_hyperedges {
648            let degree = hyperedge_deg_data[i].max(1e-8);
649            d_e.set_item(&[i, i], degree.recip())?;
650        }
651
652        // Compute normalized Laplacian: I - D_v^{-1/2} H D_e H^T D_v^{-1/2}
653        let h_t = h.transpose(0, 1)?;
654        let intermediate = d_v.matmul(h)?.matmul(&d_e)?.matmul(&h_t)?.matmul(&d_v)?;
655
656        let identity = eye(num_nodes);
657        Ok(identity?.sub(&intermediate)?)
658    }
659
660    /// Normalize features by node degrees
661    fn normalize_by_degrees(
662        &self,
663        features: &Tensor,
664        hypergraph: &HypergraphData,
665    ) -> Result<Tensor> {
666        let degrees = &hypergraph.node_degrees;
667        let epsilon = 1e-8;
668
669        let safe_degrees = degrees.add_scalar(epsilon)?;
670        let inv_sqrt_degrees = safe_degrees.pow_scalar(-0.5)?;
671
672        // First squeeze to ensure we have shape [num_nodes] rather than [num_nodes, 1]
673        let inv_degrees_squeezed = if inv_sqrt_degrees.shape().dims().len() > 1 {
674            inv_sqrt_degrees.squeeze_tensor(1)?
675        } else {
676            inv_sqrt_degrees
677        };
678        let inv_degrees_expanded = inv_degrees_squeezed.unsqueeze(-1)?;
679
680        Ok(features.mul(&inv_degrees_expanded)?)
681    }
682}
683
684impl GraphLayer for HGNNConv {
685    fn forward(&self, graph: &GraphData) -> Result<GraphData> {
686        let hypergraph = graph_to_hypergraph(graph)?;
687        let output_hypergraph = HGNNConv::forward(self, &hypergraph)?;
688        output_hypergraph.to_graph_clique_expansion()
689    }
690
691    fn parameters(&self) -> Vec<Tensor> {
692        let mut params = vec![self.weight.clone_data()];
693        if let Some(ref bias) = self.bias {
694            params.push(bias.clone_data());
695        }
696        params
697    }
698}
699
700/// Hypergraph pooling operations
701pub mod pooling {
702    use super::*;
703
704    /// Global hypergraph pooling
705    pub fn global_hypergraph_pool(
706        hypergraph: &HypergraphData,
707        method: PoolingMethod,
708    ) -> Result<Tensor> {
709        match method {
710            PoolingMethod::Mean => Ok(hypergraph.x.mean(Some(&[0]), false)?),
711            PoolingMethod::Max => Ok(hypergraph.x.max(Some(0), false)?),
712            PoolingMethod::Sum => Ok(hypergraph.x.sum_dim(&[0], false)?),
713            PoolingMethod::Attention => attention_pool(hypergraph),
714        }
715    }
716
717    /// Hyperedge-aware pooling
718    pub fn hyperedge_pool(hypergraph: &HypergraphData, method: PoolingMethod) -> Result<Tensor> {
719        let incidence_t = hypergraph.incidence_matrix.transpose(0, 1)?;
720
721        match method {
722            PoolingMethod::Mean => {
723                // Average pooling over hyperedges
724                let hyperedge_features = incidence_t.matmul(&hypergraph.x)?;
725                Ok(hyperedge_features.mean(Some(&[0]), false)?)
726            }
727            PoolingMethod::Max => {
728                let hyperedge_features = incidence_t.matmul(&hypergraph.x)?;
729                Ok(hyperedge_features.max(Some(0), false)?)
730            }
731            PoolingMethod::Sum => {
732                let hyperedge_features = incidence_t.matmul(&hypergraph.x)?;
733                Ok(hyperedge_features.sum_dim(&[0], false)?)
734            }
735            PoolingMethod::Attention => {
736                // Attention over hyperedges
737                attention_pool(hypergraph)
738            }
739        }
740    }
741
742    /// Hierarchical hypergraph pooling
743    pub fn hierarchical_hypergraph_pool(
744        hypergraph: &HypergraphData,
745        num_clusters: usize,
746    ) -> Result<HypergraphData> {
747        // Simplified clustering-based pooling
748        let cluster_assignments = cluster_nodes(hypergraph, num_clusters);
749        coarsen_hypergraph(hypergraph, &cluster_assignments)
750    }
751
752    /// Attention-based pooling
753    fn attention_pool(hypergraph: &HypergraphData) -> Result<Tensor> {
754        // Simplified attention pooling
755        let attention_scores = hypergraph.x.sum_dim(&[1], false)?;
756        let attention_weights = attention_scores.softmax(0)?;
757        let attention_expanded = attention_weights.unsqueeze(-1)?;
758
759        let weighted_features = hypergraph.x.mul(&attention_expanded)?;
760        Ok(weighted_features.sum_dim(&[0], false)?)
761    }
762
763    /// Simple node clustering for hierarchical pooling
764    fn cluster_nodes(hypergraph: &HypergraphData, num_clusters: usize) -> Vec<usize> {
765        let num_nodes = hypergraph.num_nodes;
766        let mut assignments = vec![0; num_nodes];
767
768        // Simple clustering by node index (for demonstration)
769        for i in 0..num_nodes {
770            assignments[i] = i % num_clusters;
771        }
772
773        assignments
774    }
775
776    /// Coarsen hypergraph based on cluster assignments
777    fn coarsen_hypergraph(
778        hypergraph: &HypergraphData,
779        cluster_assignments: &[usize],
780    ) -> Result<HypergraphData> {
781        let num_clusters = cluster_assignments.iter().max().copied().unwrap_or(0) + 1;
782        let original_features = hypergraph.x.shape().dims()[1];
783
784        // Average node features within clusters (simplified implementation)
785        let mut coarse_features_data = vec![0.0; num_clusters * original_features];
786        let mut cluster_counts = vec![0; num_clusters];
787
788        let node_data = hypergraph.x.to_vec()?;
789
790        for (node, &cluster) in cluster_assignments.iter().enumerate() {
791            cluster_counts[cluster] += 1;
792            for feat in 0..original_features {
793                let node_feat_idx = node * original_features + feat;
794                let cluster_feat_idx = cluster * original_features + feat;
795                coarse_features_data[cluster_feat_idx] += node_data[node_feat_idx];
796            }
797        }
798
799        // Normalize by cluster size
800        for cluster in 0..num_clusters {
801            if cluster_counts[cluster] > 0 {
802                for feat in 0..original_features {
803                    let cluster_feat_idx = cluster * original_features + feat;
804                    coarse_features_data[cluster_feat_idx] /= cluster_counts[cluster] as f32;
805                }
806            }
807        }
808
809        let coarse_features = from_vec(
810            coarse_features_data,
811            &[num_clusters, original_features],
812            torsh_core::device::DeviceType::Cpu,
813        )?;
814
815        // Create coarse incidence matrix (simplified)
816        let coarse_incidence = zeros(&[num_clusters, hypergraph.num_hyperedges])?;
817
818        HypergraphData::new(coarse_features, coarse_incidence)
819    }
820
821    /// Pooling methods
822    #[derive(Debug, Clone, Copy)]
823    pub enum PoolingMethod {
824        Mean,
825        Max,
826        Sum,
827        Attention,
828    }
829}
830
831/// Utility functions for hypergraph operations
832pub mod utils {
833    use super::*;
834
835    /// Convert edge list to hypergraph
836    pub fn edge_list_to_hypergraph(
837        edges: &[(Vec<usize>, f32)],
838        num_nodes: usize,
839    ) -> Result<HypergraphData> {
840        let num_hyperedges = edges.len();
841        let mut incidence_data = vec![0.0; num_nodes * num_hyperedges];
842        let mut weights = Vec::new();
843
844        for (e, (edge_nodes, weight)) in edges.iter().enumerate() {
845            weights.push(*weight);
846            for &node in edge_nodes {
847                if node < num_nodes {
848                    incidence_data[node * num_hyperedges + e] = 1.0;
849                }
850            }
851        }
852
853        let features = randn(&[num_nodes, 16])?; // Default features
854        let incidence_matrix = from_vec(
855            incidence_data,
856            &[num_nodes, num_hyperedges],
857            torsh_core::device::DeviceType::Cpu,
858        )?;
859
860        let hyperedge_weights = from_vec(
861            weights,
862            &[num_hyperedges],
863            torsh_core::device::DeviceType::Cpu,
864        )?;
865
866        Ok(HypergraphData::new(features, incidence_matrix)?
867            .with_hyperedge_weights(hyperedge_weights))
868    }
869
870    /// Generate random hypergraph
871    pub fn random_hypergraph(
872        num_nodes: usize,
873        num_hyperedges: usize,
874        edge_prob: f32,
875        features_dim: usize,
876    ) -> Result<HypergraphData> {
877        let mut rng = scirs2_core::random::thread_rng();
878        let mut incidence_data = vec![0.0; num_nodes * num_hyperedges];
879
880        // Generate random hyperedges
881        for e in 0..num_hyperedges {
882            for v in 0..num_nodes {
883                if rng.gen_range(0.0..1.0) < edge_prob {
884                    incidence_data[v * num_hyperedges + e] = 1.0;
885                }
886            }
887        }
888
889        let features = randn(&[num_nodes, features_dim])?;
890        let incidence_matrix = from_vec(
891            incidence_data,
892            &[num_nodes, num_hyperedges],
893            torsh_core::device::DeviceType::Cpu,
894        )?;
895
896        HypergraphData::new(features, incidence_matrix)
897    }
898
899    /// Hypergraph metrics
900    pub fn hypergraph_metrics(hypergraph: &HypergraphData) -> Result<HypergraphMetrics> {
901        let node_degrees = hypergraph.node_degrees.to_vec()?;
902        let hyperedge_cardinalities = hypergraph.hyperedge_cardinalities.to_vec()?;
903
904        let avg_node_degree = node_degrees.iter().sum::<f32>() / node_degrees.len() as f32;
905        let avg_hyperedge_size =
906            hyperedge_cardinalities.iter().sum::<f32>() / hyperedge_cardinalities.len() as f32;
907
908        let density = node_degrees.iter().sum::<f32>()
909            / (hypergraph.num_nodes * hypergraph.num_hyperedges) as f32;
910
911        Ok(HypergraphMetrics {
912            avg_node_degree,
913            avg_hyperedge_size,
914            density,
915            num_nodes: hypergraph.num_nodes,
916            num_hyperedges: hypergraph.num_hyperedges,
917        })
918    }
919
920    /// Hypergraph statistics
921    #[derive(Debug, Clone)]
922    pub struct HypergraphMetrics {
923        pub avg_node_degree: f32,
924        pub avg_hyperedge_size: f32,
925        pub density: f32,
926        pub num_nodes: usize,
927        pub num_hyperedges: usize,
928    }
929}
930
931/// Convert regular graph to hypergraph (each edge becomes a hyperedge)
932pub fn graph_to_hypergraph(graph: &GraphData) -> Result<HypergraphData> {
933    let edge_data = crate::utils::tensor_to_vec2::<f32>(&graph.edge_index)?;
934    let num_edges = edge_data[0].len();
935    let num_nodes = graph.num_nodes;
936
937    // Each edge becomes a hyperedge connecting two nodes
938    let mut incidence_data = vec![0.0; num_nodes * num_edges];
939
940    for e in 0..num_edges {
941        let src = edge_data[0][e] as usize;
942        let dst = edge_data[1][e] as usize;
943
944        if src < num_nodes && dst < num_nodes {
945            incidence_data[src * num_edges + e] = 1.0;
946            incidence_data[dst * num_edges + e] = 1.0;
947        }
948    }
949
950    let incidence_matrix = from_vec(
951        incidence_data,
952        &[num_nodes, num_edges],
953        torsh_core::device::DeviceType::Cpu,
954    )?;
955
956    HypergraphData::new(graph.x.clone(), incidence_matrix)
957}
958
959/// Create identity matrix
960fn eye(n: usize) -> Result<Tensor> {
961    let mut data = vec![0.0; n * n];
962    for i in 0..n {
963        data[i * n + i] = 1.0;
964    }
965    Ok(from_vec(
966        data,
967        &[n, n],
968        torsh_core::device::DeviceType::Cpu,
969    )?)
970}
971
972#[cfg(test)]
973mod tests {
974    use super::*;
975    use torsh_core::device::DeviceType;
976
977    #[test]
978    fn test_hypergraph_creation() {
979        let features = randn(&[4, 3]).unwrap();
980        let incidence_data = vec![
981            1.0, 0.0, 1.0, // Node 0 in hyperedges 0 and 2
982            1.0, 1.0, 0.0, // Node 1 in hyperedges 0 and 1
983            0.0, 1.0, 1.0, // Node 2 in hyperedges 1 and 2
984            0.0, 0.0, 1.0, // Node 3 in hyperedge 2
985        ];
986        let incidence_matrix = from_vec(incidence_data, &[4, 3], DeviceType::Cpu).unwrap();
987
988        let hypergraph =
989            HypergraphData::new(features, incidence_matrix).expect("operation should succeed");
990
991        assert_eq!(hypergraph.num_nodes, 4);
992        assert_eq!(hypergraph.num_hyperedges, 3);
993        assert_eq!(hypergraph.x.shape().dims(), &[4, 3]);
994        assert_eq!(hypergraph.incidence_matrix.shape().dims(), &[4, 3]);
995    }
996
997    #[test]
998    fn test_hgcn_layer() {
999        let features = randn(&[3, 4]).unwrap();
1000        let incidence_matrix =
1001            from_vec(vec![1.0, 0.0, 1.0, 1.0, 0.0, 1.0], &[3, 2], DeviceType::Cpu).unwrap();
1002        let hypergraph =
1003            HypergraphData::new(features, incidence_matrix).expect("operation should succeed");
1004
1005        let hgcn = HGCNConv::new(4, 8, true, false, 0.1);
1006        let output = hgcn
1007            .expect("operation should succeed")
1008            .forward(&hypergraph)
1009            .expect("operation should succeed");
1010
1011        assert_eq!(output.x.shape().dims(), &[3, 8]);
1012        assert_eq!(output.num_nodes, 3);
1013        assert_eq!(output.num_hyperedges, 2);
1014    }
1015
1016    #[test]
1017    fn test_hypergraph_to_graph_conversion() {
1018        let features = randn(&[3, 4]).unwrap();
1019        let incidence_matrix =
1020            from_vec(vec![1.0, 0.0, 1.0, 1.0, 0.0, 1.0], &[3, 2], DeviceType::Cpu).unwrap();
1021        let hypergraph =
1022            HypergraphData::new(features, incidence_matrix).expect("operation should succeed");
1023
1024        let graph = hypergraph
1025            .to_graph_clique_expansion()
1026            .expect("operation should succeed");
1027        assert_eq!(graph.num_nodes, 3);
1028
1029        let star_graph = hypergraph
1030            .to_graph_star_expansion()
1031            .expect("operation should succeed");
1032        assert_eq!(star_graph.num_nodes, 5); // 3 original + 2 virtual nodes
1033    }
1034
1035    #[test]
1036    fn test_hypergraph_pooling() {
1037        let features = randn(&[4, 6]).unwrap();
1038        let incidence_matrix = from_vec(
1039            vec![1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0],
1040            &[4, 2],
1041            DeviceType::Cpu,
1042        )
1043        .unwrap();
1044        let hypergraph =
1045            HypergraphData::new(features, incidence_matrix).expect("operation should succeed");
1046
1047        let pooled_mean =
1048            pooling::global_hypergraph_pool(&hypergraph, pooling::PoolingMethod::Mean);
1049        assert_eq!(
1050            pooled_mean
1051                .expect("operation should succeed")
1052                .shape()
1053                .dims(),
1054            &[6]
1055        );
1056
1057        let pooled_max = pooling::global_hypergraph_pool(&hypergraph, pooling::PoolingMethod::Max);
1058        assert_eq!(
1059            pooled_max.expect("operation should succeed").shape().dims(),
1060            &[6]
1061        );
1062    }
1063
1064    #[test]
1065    fn test_hypergraph_utils() {
1066        let edges = vec![
1067            (vec![0, 1, 2], 1.0),
1068            (vec![1, 3], 0.8),
1069            (vec![0, 2, 3], 1.2),
1070        ];
1071
1072        let hypergraph =
1073            utils::edge_list_to_hypergraph(&edges, 4).expect("operation should succeed");
1074        assert_eq!(hypergraph.num_nodes, 4);
1075        assert_eq!(hypergraph.num_hyperedges, 3);
1076
1077        let metrics = utils::hypergraph_metrics(&hypergraph).expect("operation should succeed");
1078        assert!(metrics.avg_node_degree > 0.0);
1079        assert!(metrics.avg_hyperedge_size > 0.0);
1080    }
1081
1082    #[test]
1083    fn test_random_hypergraph_generation() {
1084        let hypergraph = utils::random_hypergraph(5, 3, 0.6, 8).expect("operation should succeed");
1085        assert_eq!(hypergraph.num_nodes, 5);
1086        assert_eq!(hypergraph.num_hyperedges, 3);
1087        assert_eq!(hypergraph.x.shape().dims(), &[5, 8]);
1088    }
1089
1090    #[test]
1091    fn test_hypergat_layer() {
1092        let features = randn(&[4, 6]).unwrap();
1093        let incidence_matrix = from_vec(
1094            vec![1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0],
1095            &[4, 2],
1096            DeviceType::Cpu,
1097        )
1098        .unwrap();
1099        let hypergraph =
1100            HypergraphData::new(features, incidence_matrix).expect("operation should succeed");
1101
1102        let hypergat = HyperGATConv::new(6, 12, 3, 0.1, true);
1103        let output = hypergat
1104            .expect("operation should succeed")
1105            .forward(&hypergraph)
1106            .expect("operation should succeed");
1107
1108        assert_eq!(output.x.shape().dims(), &[4, 12]);
1109        assert_eq!(output.num_nodes, 4);
1110    }
1111}