Skip to main content

torsh_graph/
pool.rs

1//! Graph pooling layers
2// Framework infrastructure - components designed for future use
3#![allow(dead_code)]
4/// Crate-local result alias: the error type defaults to [`TorshError`],
5/// so both `Result<T>` and `Result<T, OtherError>` stay valid.
6type Result<T, E = torsh_core::error::TorshError> = std::result::Result<T, E>;
7
8use crate::parameter::Parameter;
9use crate::GraphData;
10use torsh_tensor::{
11    creation::{from_vec, randn, zeros},
12    Tensor,
13};
14
15/// Global pooling operations for graphs
16pub mod global {
17    use super::*;
18
19    /// Global mean pooling
20    pub fn global_mean_pool(graph: &GraphData) -> Result<Tensor> {
21        // Average node features across the graph
22        Ok(graph.x.mean(Some(&[0]), false)?)
23    }
24
25    /// Global max pooling
26    pub fn global_max_pool(graph: &GraphData) -> Result<Tensor> {
27        // Max node features across the graph - simplified using max without indices
28        Ok(graph.x.max(Some(0), false)?)
29    }
30
31    /// Global sum pooling
32    pub fn global_sum_pool(graph: &GraphData) -> Result<Tensor> {
33        // Sum node features across the graph (along node dimension)
34        Ok(graph.x.sum_dim(&[0], false)?)
35    }
36
37    /// Global attention pooling
38    pub struct GlobalAttentionPool {
39        gate_nn: Parameter,
40        feat_nn: Parameter,
41    }
42
43    impl GlobalAttentionPool {
44        /// Create a new global attention pooling layer
45        pub fn new(input_dim: usize, hidden_dim: usize) -> Result<Self> {
46            let gate_nn = Parameter::new(randn(&[input_dim, hidden_dim])?);
47            let feat_nn = Parameter::new(randn(&[input_dim, hidden_dim])?);
48
49            Ok(Self { gate_nn, feat_nn })
50        }
51
52        /// Apply attention-based global pooling
53        pub fn forward(&self, graph: &GraphData) -> Result<Tensor> {
54            // Compute gate and feature transformations
55            let gate = graph.x.matmul(&self.gate_nn.clone_data())?.sigmoid()?;
56            let feat = graph.x.matmul(&self.feat_nn.clone_data())?;
57
58            // Apply attention weights
59            let weighted_features = feat.mul(&gate)?;
60
61            // Sum over nodes (axis 0), preserving feature dimension
62            Ok(weighted_features.sum_dim(&[0], false)?)
63        }
64
65        /// Get parameters
66        pub fn parameters(&self) -> Vec<Tensor> {
67            vec![self.gate_nn.clone_data(), self.feat_nn.clone_data()]
68        }
69    }
70
71    /// Set2Set pooling for variable-sized graphs
72    pub struct Set2Set {
73        input_dim: usize,
74        hidden_dim: usize,
75        num_layers: usize,
76        num_iters: usize,
77        lstm_weights: Vec<Parameter>,
78        attention_weights: Parameter,
79        projection_weights: Parameter,
80    }
81
82    impl Set2Set {
83        /// Create a new Set2Set pooling layer
84        pub fn new(
85            input_dim: usize,
86            hidden_dim: usize,
87            num_layers: usize,
88            num_iters: usize,
89        ) -> Result<Self> {
90            // Simple LSTM-like weights (simplified implementation)
91            let mut lstm_weights = Vec::new();
92            for _ in 0..num_layers {
93                lstm_weights.push(Parameter::new(randn(&[
94                    hidden_dim * 4,
95                    hidden_dim + input_dim,
96                ])?));
97            }
98
99            let attention_weights = Parameter::new(randn(&[hidden_dim, input_dim])?);
100            let projection_weights = Parameter::new(randn(&[input_dim, hidden_dim])?);
101
102            Ok(Self {
103                input_dim,
104                hidden_dim,
105                num_layers,
106                num_iters,
107                lstm_weights,
108                attention_weights,
109                projection_weights,
110            })
111        }
112
113        /// Apply Set2Set pooling
114        pub fn forward(&self, graph: &GraphData) -> Result<Tensor> {
115            let _num_nodes = graph.num_nodes;
116            let mut query = zeros(&[1, self.hidden_dim])?;
117
118            // Simplified Set2Set implementation
119            for _ in 0..self.num_iters {
120                // Compute attention scores
121                let scores = query
122                    .matmul(&self.attention_weights.clone_data())?
123                    .matmul(&graph.x.t()?)?
124                    .softmax(-1)?;
125
126                // Weighted sum of node features
127                let attended = scores.matmul(&graph.x)?;
128
129                // Project attended features to hidden dimension
130                let projected_attended = attended.matmul(&self.projection_weights.clone_data())?;
131
132                // Update query (simplified LSTM step)
133                query = query.add(&projected_attended)?;
134            }
135
136            Ok(query.squeeze(0)?)
137        }
138
139        /// Get parameters
140        pub fn parameters(&self) -> Vec<Tensor> {
141            let mut params: Vec<Tensor> =
142                self.lstm_weights.iter().map(|p| p.clone_data()).collect();
143            params.push(self.attention_weights.clone_data());
144            params.push(self.projection_weights.clone_data());
145            params
146        }
147    }
148}
149
150/// Hierarchical pooling layers
151pub mod hierarchical {
152    use super::*;
153
154    /// DiffPool: Differentiable graph pooling
155    pub struct DiffPool {
156        embed_dim: usize,
157        assign_dim: usize,
158        embed_gnn: Parameter,
159        assign_gnn: Parameter,
160        link_pred_loss_weight: f64,
161        entropy_loss_weight: f64,
162    }
163
164    impl DiffPool {
165        /// Create a new DiffPool layer
166        pub fn new(embed_dim: usize, assign_dim: usize) -> Result<Self> {
167            let embed_gnn = Parameter::new(randn(&[embed_dim, embed_dim])?);
168            let assign_gnn = Parameter::new(randn(&[embed_dim, assign_dim])?);
169
170            Ok(Self {
171                embed_dim,
172                assign_dim,
173                embed_gnn,
174                assign_gnn,
175                link_pred_loss_weight: 1.0,
176                entropy_loss_weight: 1.0,
177            })
178        }
179
180        /// Apply differentiable pooling
181        pub fn forward(&self, graph: &GraphData) -> Result<(GraphData, Tensor)> {
182            let num_nodes = graph.num_nodes;
183
184            // Generate node embeddings
185            let node_embeddings = graph.x.matmul(&self.embed_gnn.clone_data())?;
186
187            // Generate assignment matrix (soft clustering)
188            let assignment_logits = graph.x.matmul(&self.assign_gnn.clone_data())?;
189            let assignment_matrix = assignment_logits.softmax(-1)?;
190
191            // Pool node features using assignment matrix
192            let pooled_features = assignment_matrix.t()?.matmul(&node_embeddings)?;
193
194            // Create new adjacency matrix
195            let adjacency = self.compute_adjacency_matrix(&graph.edge_index, num_nodes)?;
196            let pooled_adj = assignment_matrix
197                .t()?
198                .matmul(&adjacency)?
199                .matmul(&assignment_matrix)?;
200
201            // Extract edges from pooled adjacency matrix
202            let (new_edge_index, _) = self.adjacency_to_edge_index(&pooled_adj)?;
203
204            // Compute auxiliary losses for training
205            let link_pred_loss =
206                self.compute_link_prediction_loss(&adjacency, &assignment_matrix)?;
207            let entropy_loss = self.compute_entropy_loss(&assignment_matrix)?;
208            let total_aux_loss = link_pred_loss
209                .mul_scalar(self.link_pred_loss_weight as f32)?
210                .add(&entropy_loss.mul_scalar(self.entropy_loss_weight as f32)?)?;
211
212            let pooled_graph = GraphData {
213                x: pooled_features,
214                edge_index: new_edge_index,
215                edge_attr: None,
216                batch: None,
217                num_nodes: self.assign_dim,
218                num_edges: 0, // Will be computed from edge_index
219            };
220
221            Ok((pooled_graph, total_aux_loss))
222        }
223
224        /// Compute adjacency matrix from edge index
225        fn compute_adjacency_matrix(
226            &self,
227            edge_index: &Tensor,
228            num_nodes: usize,
229        ) -> Result<Tensor> {
230            let mut adjacency = zeros(&[num_nodes, num_nodes])?;
231            let edge_data = edge_index.to_vec()?;
232            let edge_list: Vec<Vec<i64>> = vec![
233                edge_data[0..edge_data.len() / 2]
234                    .iter()
235                    .map(|&x| x as i64)
236                    .collect(),
237                edge_data[edge_data.len() / 2..]
238                    .iter()
239                    .map(|&x| x as i64)
240                    .collect(),
241            ];
242
243            for j in 0..edge_list[0].len() {
244                let src = edge_list[0][j] as usize;
245                let dst = edge_list[1][j] as usize;
246                if src < num_nodes && dst < num_nodes {
247                    // Simplified adjacency matrix setting - use direct indexing approach
248                    let mut adj_data = adjacency.to_vec()?;
249                    adj_data[src * num_nodes + dst] = 1.0;
250                    adjacency = torsh_tensor::creation::from_vec(
251                        adj_data,
252                        &[num_nodes, num_nodes],
253                        torsh_core::device::DeviceType::Cpu,
254                    )?;
255                }
256            }
257
258            Ok(adjacency)
259        }
260
261        /// Convert adjacency matrix to edge index
262        fn adjacency_to_edge_index(&self, adjacency: &Tensor) -> Result<(Tensor, usize)> {
263            let adj_data = adjacency.to_vec()?;
264            let mut edges = Vec::new();
265
266            // Convert flattened vector to 2D indexing using tensor shape
267            let shape = adjacency.shape();
268            let (rows, cols) = (shape.dims()[0], shape.dims()[1]);
269            for i in 0..rows {
270                for j in 0..cols {
271                    let idx = i * cols + j;
272                    if idx < adj_data.len() && adj_data[idx] > 0.5 {
273                        // Threshold for edge existence
274                        edges.push([i as f32, j as f32]);
275                    }
276                }
277            }
278
279            if edges.is_empty() {
280                Ok((zeros(&[2, 0])?, 0))
281            } else {
282                let num_edges = edges.len();
283                let mut edge_vec = Vec::with_capacity(2 * num_edges);
284
285                for edge in &edges {
286                    edge_vec.push(edge[0]);
287                }
288                for edge in &edges {
289                    edge_vec.push(edge[1]);
290                }
291
292                Ok((
293                    from_vec(
294                        edge_vec.iter().map(|&x| x as f32).collect(),
295                        &[2, num_edges],
296                        torsh_core::device::DeviceType::Cpu,
297                    )?,
298                    num_edges,
299                ))
300            }
301        }
302
303        /// Compute link prediction auxiliary loss
304        fn compute_link_prediction_loss(
305            &self,
306            adjacency: &Tensor,
307            assignment: &Tensor,
308        ) -> Result<Tensor> {
309            // Predict adjacency matrix from assignment
310            let predicted_adj = assignment.matmul(&assignment.t()?)?;
311
312            // Compute binary cross-entropy loss
313            let eps = 1e-8;
314            let eps_tensor =
315                torsh_tensor::creation::ones_like(adjacency)?.mul_scalar(eps as f32)?;
316            let one_tensor = torsh_tensor::creation::ones_like(adjacency)?;
317            let pos_loss = adjacency.mul(&predicted_adj.add(&eps_tensor)?.ln()?)?;
318            let neg_loss = one_tensor
319                .sub(adjacency)?
320                .mul(&one_tensor.sub(&predicted_adj)?.add(&eps_tensor)?.ln()?)?;
321
322            Ok(pos_loss.add(&neg_loss)?.mean(None, false)?.neg()?)
323        }
324
325        /// Compute entropy auxiliary loss to encourage discrete assignments
326        fn compute_entropy_loss(&self, assignment: &Tensor) -> Result<Tensor> {
327            let eps = 1e-8;
328            let eps_tensor =
329                torsh_tensor::creation::ones_like(assignment)?.mul_scalar(eps as f32)?;
330            let entropy = assignment
331                .mul(&assignment.add(&eps_tensor)?.ln()?)?
332                .sum()?
333                .mean(None, false)?
334                .neg()?;
335            Ok(entropy)
336        }
337
338        /// Get parameters
339        pub fn parameters(&self) -> Vec<Tensor> {
340            vec![self.embed_gnn.clone_data(), self.assign_gnn.clone_data()]
341        }
342    }
343
344    /// TopK pooling
345    pub struct TopKPool {
346        ratio: f32,
347        min_score: Option<f32>,
348        score_layer: Parameter,
349    }
350
351    impl TopKPool {
352        /// Create a new TopK pooling layer
353        pub fn new(input_dim: usize, ratio: f32, min_score: Option<f32>) -> Result<Self> {
354            let score_layer = Parameter::new(randn(&[input_dim, 1])?);
355
356            Ok(Self {
357                ratio,
358                min_score,
359                score_layer,
360            })
361        }
362
363        /// Apply TopK pooling
364        pub fn forward(&self, graph: &GraphData) -> Result<GraphData> {
365            let num_nodes = graph.num_nodes;
366            let k = (num_nodes as f32 * self.ratio).ceil() as usize;
367
368            // Compute node importance scores
369            let scores = graph
370                .x
371                .matmul(&self.score_layer.clone_data())?
372                .squeeze(-1)?;
373
374            // Get top-k node indices
375            let (top_scores, top_indices) = self.topk(&scores, k)?;
376
377            // Filter nodes based on minimum score if specified
378            let (selected_indices, _selected_scores) = if let Some(min_score) = self.min_score {
379                let valid_mask = top_scores.gt_scalar(min_score)?;
380                // Convert boolean mask to f32 for compatibility
381                let mask_data = valid_mask.to_vec()?;
382                let mask_f32 = mask_data
383                    .iter()
384                    .map(|&x| if x { 1.0 } else { 0.0 })
385                    .collect();
386                let mask_tensor = from_vec(
387                    mask_f32,
388                    valid_mask.shape().dims(),
389                    torsh_core::device::DeviceType::Cpu,
390                )?;
391                let valid_indices = self.masked_select(&top_indices, &mask_tensor)?;
392                let valid_scores = self.masked_select(&top_scores, &mask_tensor)?;
393                (valid_indices, valid_scores)
394            } else {
395                (top_indices, top_scores)
396            };
397
398            // Extract features for selected nodes
399            let selected_features = self.index_select(&graph.x, &selected_indices, 0)?;
400
401            // Filter edges to only include those between selected nodes
402            let (new_edge_index, new_num_edges) =
403                self.filter_edges(&graph.edge_index, &selected_indices)?;
404
405            Ok(GraphData {
406                x: selected_features,
407                edge_index: new_edge_index,
408                edge_attr: graph.edge_attr.clone(), // Could be filtered similarly
409                batch: None,                        // Batch information would need to be updated
410                num_nodes: selected_indices.shape().dims()[0],
411                num_edges: new_num_edges,
412            })
413        }
414
415        /// Compute top-k indices and values
416        fn topk(&self, tensor: &Tensor, k: usize) -> Result<(Tensor, Tensor)> {
417            let values = tensor.to_vec()?;
418            let mut indexed_values: Vec<(f32, usize)> = values
419                .into_iter()
420                .enumerate()
421                .map(|(i, v)| (v, i))
422                .collect();
423
424            // Sort by value in descending order
425            indexed_values
426                .sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
427
428            // Take top k
429            indexed_values.truncate(k);
430
431            let top_values: Vec<f32> = indexed_values.iter().map(|(v, _)| *v).collect();
432            let top_indices: Vec<f32> = indexed_values.iter().map(|(_, i)| *i as f32).collect();
433
434            let values_tensor = from_vec(top_values, &[k], torsh_core::device::DeviceType::Cpu)?;
435            let indices_tensor = from_vec(top_indices, &[k], torsh_core::device::DeviceType::Cpu)?;
436
437            Ok((values_tensor, indices_tensor))
438        }
439
440        /// Select elements based on a boolean mask
441        fn masked_select(&self, tensor: &Tensor, mask: &Tensor) -> Result<Tensor> {
442            let values = tensor.to_vec()?;
443            let mask_values = mask.to_vec()?;
444
445            let selected: Vec<f32> = values
446                .into_iter()
447                .zip(mask_values.into_iter())
448                .filter_map(|(v, m)| if m > 0.5 { Some(v) } else { None })
449                .collect();
450
451            let selected_len = selected.len();
452            Ok(from_vec(
453                selected,
454                &[selected_len],
455                torsh_core::device::DeviceType::Cpu,
456            )?)
457        }
458
459        /// Select rows/columns from a tensor based on indices
460        fn index_select(&self, tensor: &Tensor, indices: &Tensor, dim: i64) -> Result<Tensor> {
461            let idx_values = indices.to_vec()?;
462
463            if dim == 0 {
464                // Select rows
465                let tensor_data = tensor.to_vec()?;
466                let shape = tensor.shape();
467                let cols = shape.dims()[1];
468                let original_data: Vec<Vec<f32>> = tensor_data
469                    .chunks(cols)
470                    .map(|chunk| chunk.to_vec())
471                    .collect();
472                let mut selected_rows = Vec::new();
473
474                for &idx in &idx_values {
475                    let idx_usize = idx as usize;
476                    if idx_usize < original_data.len() {
477                        selected_rows.extend_from_slice(&original_data[idx_usize]);
478                    }
479                }
480
481                let num_rows = idx_values.len();
482                let num_cols = if num_rows > 0 {
483                    selected_rows.len() / num_rows
484                } else {
485                    0
486                };
487
488                Ok(from_vec(
489                    selected_rows,
490                    &[num_rows, num_cols],
491                    torsh_core::device::DeviceType::Cpu,
492                )?)
493            } else {
494                // For simplicity, only implement row selection
495                Ok(tensor.clone())
496            }
497        }
498
499        /// Filter edges to only include those between selected nodes
500        fn filter_edges(
501            &self,
502            edge_index: &Tensor,
503            selected_nodes: &Tensor,
504        ) -> Result<(Tensor, usize)> {
505            let edge_data = edge_index.to_vec()?;
506            let edges = vec![
507                edge_data[0..edge_data.len() / 2]
508                    .iter()
509                    .map(|&x| x as i64)
510                    .collect::<Vec<i64>>(),
511                edge_data[edge_data.len() / 2..]
512                    .iter()
513                    .map(|&x| x as i64)
514                    .collect::<Vec<i64>>(),
515            ];
516            let selected_indices = selected_nodes.to_vec()?;
517
518            // Create a mapping from old node indices to new ones
519            let mut node_mapping = std::collections::HashMap::new();
520            for (new_idx, &old_idx) in selected_indices.iter().enumerate() {
521                node_mapping.insert(old_idx as i64, new_idx as i64);
522            }
523
524            // Filter and remap edges
525            let mut filtered_edges = Vec::new();
526            for j in 0..edges[0].len() {
527                let src = edges[0][j];
528                let dst = edges[1][j];
529
530                if let (Some(&new_src), Some(&new_dst)) =
531                    (node_mapping.get(&src), node_mapping.get(&dst))
532                {
533                    filtered_edges.push([new_src, new_dst]);
534                }
535            }
536
537            if filtered_edges.is_empty() {
538                Ok((zeros(&[2, 0])?, 0))
539            } else {
540                let num_edges = filtered_edges.len();
541                let mut edge_vec = Vec::with_capacity(2 * num_edges);
542
543                for edge in &filtered_edges {
544                    edge_vec.push(edge[0]);
545                }
546                for edge in &filtered_edges {
547                    edge_vec.push(edge[1]);
548                }
549
550                Ok((
551                    from_vec(
552                        edge_vec.iter().map(|&x| x as f32).collect(),
553                        &[2, num_edges],
554                        torsh_core::device::DeviceType::Cpu,
555                    )?,
556                    num_edges,
557                ))
558            }
559        }
560
561        /// Get parameters
562        pub fn parameters(&self) -> Vec<Tensor> {
563            vec![self.score_layer.clone_data()]
564        }
565    }
566
567    /// MinCut pooling for graph coarsening
568    pub struct MinCutPool {
569        input_dim: usize,
570        output_dim: usize,
571        assignment_layer: Parameter,
572    }
573
574    impl MinCutPool {
575        /// Create a new MinCut pooling layer
576        pub fn new(input_dim: usize, output_dim: usize) -> Result<Self> {
577            let assignment_layer = Parameter::new(randn(&[input_dim, output_dim])?);
578
579            Ok(Self {
580                input_dim,
581                output_dim,
582                assignment_layer,
583            })
584        }
585
586        /// Apply MinCut pooling
587        pub fn forward(&self, graph: &GraphData) -> Result<(GraphData, Tensor)> {
588            // Compute soft assignment matrix
589            let assignment_logits = graph.x.matmul(&self.assignment_layer.clone_data())?;
590            let assignment_matrix = assignment_logits.softmax(-1)?;
591
592            // Pool node features
593            let pooled_features = assignment_matrix.t()?.matmul(&graph.x)?;
594
595            // Compute adjacency matrix
596            let adjacency = self.compute_adjacency_matrix(&graph.edge_index, graph.num_nodes)?;
597
598            // Pool adjacency matrix
599            let pooled_adj = assignment_matrix
600                .t()?
601                .matmul(&adjacency)?
602                .matmul(&assignment_matrix)?;
603
604            // Create new edge index
605            let (new_edge_index, new_num_edges) = self.adjacency_to_edge_index(&pooled_adj)?;
606
607            // Compute MinCut loss
608            let mincut_loss = self.compute_mincut_loss(&adjacency, &assignment_matrix)?;
609            let orthogonality_loss = self.compute_orthogonality_loss(&assignment_matrix)?;
610            let total_loss = mincut_loss.add(&orthogonality_loss)?;
611
612            let pooled_graph = GraphData {
613                x: pooled_features,
614                edge_index: new_edge_index,
615                edge_attr: None,
616                batch: None,
617                num_nodes: self.output_dim,
618                num_edges: new_num_edges,
619            };
620
621            Ok((pooled_graph, total_loss))
622        }
623
624        /// Compute adjacency matrix from edge index
625        fn compute_adjacency_matrix(
626            &self,
627            edge_index: &Tensor,
628            num_nodes: usize,
629        ) -> Result<Tensor> {
630            let mut adjacency = zeros(&[num_nodes, num_nodes])?;
631            let edge_data = edge_index.to_vec()?;
632            let edge_list: Vec<Vec<i64>> = vec![
633                edge_data[0..edge_data.len() / 2]
634                    .iter()
635                    .map(|&x| x as i64)
636                    .collect(),
637                edge_data[edge_data.len() / 2..]
638                    .iter()
639                    .map(|&x| x as i64)
640                    .collect(),
641            ];
642
643            for j in 0..edge_list[0].len() {
644                let src = edge_list[0][j] as usize;
645                let dst = edge_list[1][j] as usize;
646                if src < num_nodes && dst < num_nodes {
647                    // Simplified adjacency matrix setting - use direct indexing approach
648                    let mut adj_data = adjacency.to_vec()?;
649                    adj_data[src * num_nodes + dst] = 1.0;
650                    adjacency = torsh_tensor::creation::from_vec(
651                        adj_data,
652                        &[num_nodes, num_nodes],
653                        torsh_core::device::DeviceType::Cpu,
654                    )?;
655                }
656            }
657
658            Ok(adjacency)
659        }
660
661        /// Convert adjacency matrix to edge index
662        fn adjacency_to_edge_index(&self, adjacency: &Tensor) -> Result<(Tensor, usize)> {
663            let adj_data = adjacency.to_vec()?;
664            let mut edges = Vec::new();
665
666            // Convert flattened vector to 2D indexing using tensor shape
667            let shape = adjacency.shape();
668            let (rows, cols) = (shape.dims()[0], shape.dims()[1]);
669            for i in 0..rows {
670                for j in 0..cols {
671                    let idx = i * cols + j;
672                    if idx < adj_data.len() && adj_data[idx] > 0.1 {
673                        // Threshold for edge existence
674                        edges.push([i as f32, j as f32]);
675                    }
676                }
677            }
678
679            if edges.is_empty() {
680                Ok((zeros(&[2, 0])?, 0))
681            } else {
682                let num_edges = edges.len();
683                let mut edge_vec = Vec::with_capacity(2 * num_edges);
684
685                for edge in &edges {
686                    edge_vec.push(edge[0]);
687                }
688                for edge in &edges {
689                    edge_vec.push(edge[1]);
690                }
691
692                Ok((
693                    from_vec(
694                        edge_vec.iter().map(|&x| x as f32).collect(),
695                        &[2, num_edges],
696                        torsh_core::device::DeviceType::Cpu,
697                    )?,
698                    num_edges,
699                ))
700            }
701        }
702
703        /// Compute MinCut loss
704        fn compute_mincut_loss(&self, adjacency: &Tensor, assignment: &Tensor) -> Result<Tensor> {
705            // MinCut loss encourages nodes in different clusters to have few connections
706            let cut = assignment.t()?.matmul(adjacency)?.matmul(assignment)?;
707            // Compute degree for each cluster (sum along node dimension)
708            let degree = assignment.sum_dim(&[0], false)?;
709
710            // Normalized cut - outer product of degrees
711            let degree_unsqueezed = degree.unsqueeze(0)?;
712            let degree_t = degree.unsqueeze(1)?;
713            let degree_product = degree_t.matmul(&degree_unsqueezed)?;
714            let eps_tensor =
715                torsh_tensor::creation::ones_like(&degree_product)?.mul_scalar(1e-8_f32)?;
716            let normalized_cut = cut.div(&degree_product.add(&eps_tensor)?)?;
717            // Simplified trace computation - sum of diagonal elements
718            let diag_sum = normalized_cut.sum()?;
719            Ok(diag_sum.neg()?)
720        }
721
722        /// Compute orthogonality loss to encourage balanced clusters
723        fn compute_orthogonality_loss(&self, assignment: &Tensor) -> Result<Tensor> {
724            let cluster_sizes = assignment.sum()?;
725            let normalized_sizes = cluster_sizes.div(&cluster_sizes.sum()?)?;
726
727            // Entropy loss to encourage balanced clusters
728            let eps = 1e-8;
729            let eps_tensor =
730                torsh_tensor::creation::ones_like(&normalized_sizes)?.mul_scalar(eps as f32)?;
731            let entropy_loss = normalized_sizes
732                .mul(&normalized_sizes.add(&eps_tensor)?.ln()?)?
733                .sum()?
734                .neg()?;
735            Ok(entropy_loss.neg()?)
736        }
737
738        /// Get parameters
739        pub fn parameters(&self) -> Vec<Tensor> {
740            vec![self.assignment_layer.clone_data()]
741        }
742    }
743}