Skip to main content

torsh_graph/
data.rs

1//! Graph data loading and manipulation utilities
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::GraphData;
7use torsh_core::device::DeviceType;
8use torsh_core::error::TorshError;
9
10/// Default number of synthetic node features generated for graph formats that
11/// only carry connectivity information.
12pub const DEFAULT_NODE_FEATURES: usize = 16;
13use torsh_tensor::{
14    creation::{from_vec, zeros},
15    Tensor,
16};
17// Direct implementation for now
18
19/// Graph data loader
20pub struct GraphDataLoader {
21    batch_size: usize,
22    shuffle: bool,
23    num_node_features: usize,
24}
25
26impl GraphDataLoader {
27    /// Create a new graph data loader with validation
28    ///
29    /// # Arguments
30    /// * `batch_size` - Size of batches, must be > 0
31    /// * `shuffle` - Whether to shuffle data
32    ///
33    /// # Returns
34    /// * `Ok(Self)` - Successfully created data loader
35    /// * `Err` - If batch_size is 0
36    ///
37    /// # Example
38    /// ```
39    /// use torsh_graph::data::GraphDataLoader;
40    /// let loader = GraphDataLoader::new(32, true).unwrap();
41    /// assert_eq!(loader.batch_size(), 32);
42    /// ```
43    pub fn new(batch_size: usize, shuffle: bool) -> Result<Self, Box<dyn std::error::Error>> {
44        if batch_size == 0 {
45            return Err("Batch size must be greater than 0".into());
46        }
47
48        Ok(Self {
49            batch_size,
50            shuffle,
51            num_node_features: DEFAULT_NODE_FEATURES,
52        })
53    }
54
55    /// Set the number of synthetic node features generated for formats that
56    /// carry connectivity only (edge lists).
57    ///
58    /// Defaults to [`DEFAULT_NODE_FEATURES`].
59    pub fn with_node_features(mut self, num_node_features: usize) -> Self {
60        self.num_node_features = num_node_features.max(1);
61        self
62    }
63
64    /// Number of synthetic node features used for feature-less formats
65    pub fn num_node_features(&self) -> usize {
66        self.num_node_features
67    }
68
69    /// Get batch size
70    pub fn batch_size(&self) -> usize {
71        self.batch_size
72    }
73
74    /// Get shuffle setting
75    pub fn shuffle(&self) -> bool {
76        self.shuffle
77    }
78
79    /// Load every supported graph file in `path` into a `GraphData` list.
80    ///
81    /// Files are dispatched on their extension to the parsers in
82    /// [`crate::datasets`]:
83    ///
84    /// | Extension | Loader |
85    /// |---|---|
86    /// | `edges`, `edgelist`, `txt` | [`EdgeListLoader`] |
87    /// | `gml` | [`GMLLoader`] |
88    /// | `json` | [`JSONLoader`] |
89    ///
90    /// Files with any other extension (and sub-directories) are ignored.
91    /// Entries are visited in sorted order so the returned dataset is
92    /// deterministic.
93    ///
94    /// # Errors
95    /// Returns an error if the directory cannot be read, or if a file with a
96    /// supported extension fails to parse. A failing file is reported rather
97    /// than silently skipped, so a mis-typed path can never masquerade as an
98    /// empty dataset.
99    ///
100    /// # Example
101    /// ```no_run
102    /// use torsh_graph::data::GraphDataLoader;
103    /// let loader = GraphDataLoader::new(4, false).unwrap();
104    /// let graphs = loader.from_directory("./my_graphs").unwrap();
105    /// println!("loaded {} graphs", graphs.len());
106    /// ```
107    pub fn from_directory(&self, path: &str) -> Result<Vec<GraphData>> {
108        use crate::datasets::{EdgeListLoader, GMLLoader, GraphDatasetLoader, JSONLoader};
109
110        let entries = std::fs::read_dir(path)
111            .map_err(|e| TorshError::IoError(format!("failed to read directory '{path}': {e}")))?;
112
113        let mut files: Vec<std::path::PathBuf> = Vec::new();
114        for entry in entries {
115            let entry = entry
116                .map_err(|e| TorshError::IoError(format!("failed to read '{path}' entry: {e}")))?;
117            let file_path = entry.path();
118            if file_path.is_file() {
119                files.push(file_path);
120            }
121        }
122        files.sort();
123
124        let edge_list = EdgeListLoader::new(self.num_node_features, false);
125        let gml = GMLLoader::new();
126        let json = JSONLoader::new(self.num_node_features);
127
128        let mut graphs = Vec::new();
129        for file_path in files {
130            let extension = file_path
131                .extension()
132                .and_then(|ext| ext.to_str())
133                .unwrap_or_default()
134                .to_ascii_lowercase();
135
136            let loaded = match extension.as_str() {
137                "edges" | "edgelist" | "txt" => Some(edge_list.load_from_file(&file_path)),
138                "gml" => Some(gml.load_from_file(&file_path)),
139                "json" => Some(json.load_from_file(&file_path)),
140                _ => None,
141            };
142
143            if let Some(result) = loaded {
144                let graph = result.map_err(|e| {
145                    TorshError::IoError(format!("failed to load '{}': {e}", file_path.display()))
146                })?;
147                graphs.push(graph);
148            }
149        }
150
151        Ok(graphs)
152    }
153}
154
155/// Convert between different graph representations
156pub mod converters {
157    use super::*;
158
159    // TODO: Implement when scirs2_graph API is stable
160    // pub fn from_scirs2_graph(graph: &Graph) -> GraphData { ... }
161
162    /// Convert from edge list to GraphData with validation
163    ///
164    /// # Arguments
165    /// * `edges` - List of (source, target) node pairs
166    /// * `num_nodes` - Total number of nodes in the graph
167    ///
168    /// # Returns
169    /// * `Ok(GraphData)` - Successfully created graph
170    /// * `Err` - If edges contain invalid node indices
171    ///
172    /// # Example
173    /// ```
174    /// use torsh_graph::data::converters;
175    /// let edges = vec![(0, 1), (1, 2), (2, 0)];
176    /// let graph = converters::from_edge_list(&edges, 3).unwrap();
177    /// assert_eq!(graph.num_nodes, 3);
178    /// assert_eq!(graph.num_edges, 3);
179    /// ```
180    pub fn from_edge_list(
181        edges: &[(usize, usize)],
182        num_nodes: usize,
183    ) -> Result<GraphData, Box<dyn std::error::Error>> {
184        if num_nodes == 0 {
185            return Err("Number of nodes must be greater than 0".into());
186        }
187
188        // Validate edge indices
189        for (i, (src, dst)) in edges.iter().enumerate() {
190            if *src >= num_nodes {
191                return Err(format!(
192                    "Edge {} has invalid source node {} (max: {})",
193                    i,
194                    src,
195                    num_nodes - 1
196                )
197                .into());
198            }
199            if *dst >= num_nodes {
200                return Err(format!(
201                    "Edge {} has invalid target node {} (max: {})",
202                    i,
203                    dst,
204                    num_nodes - 1
205                )
206                .into());
207            }
208        }
209
210        let num_edges = edges.len();
211        let mut edge_index = vec![0i64; 2 * num_edges];
212
213        for (i, (src, dst)) in edges.iter().enumerate() {
214            edge_index[i] = *src as i64;
215            edge_index[num_edges + i] = *dst as i64;
216        }
217
218        let x = zeros(&[num_nodes, 1])?;
219        let edge_index = from_vec(edge_index, &[2, num_edges], DeviceType::Cpu)?;
220
221        Ok(GraphData::new(x, edge_index.to_f32_simd()?))
222    }
223
224    /// Convert from adjacency matrix to GraphData
225    ///
226    /// Converts a dense adjacency matrix to GraphData format.
227    /// Non-zero entries in the matrix are treated as edges.
228    ///
229    /// # Arguments
230    /// * `adj` - Square adjacency matrix (num_nodes x num_nodes)
231    ///
232    /// # Returns
233    /// GraphData with edges extracted from non-zero matrix entries
234    pub fn from_adjacency_matrix(adj: &Tensor) -> Result<GraphData, Box<dyn std::error::Error>> {
235        let shape = adj.shape();
236        let dims = shape.dims();
237
238        if dims.len() != 2 {
239            return Err("Adjacency matrix must be 2D".into());
240        }
241
242        let num_nodes = dims[0];
243        if dims[0] != dims[1] {
244            return Err("Adjacency matrix must be square".into());
245        }
246
247        // Extract edges from adjacency matrix
248        let adj_data = adj.to_vec()?;
249        let mut edges = Vec::new();
250
251        for i in 0..num_nodes {
252            for j in 0..num_nodes {
253                let idx = i * num_nodes + j;
254                if idx < adj_data.len() && adj_data[idx].abs() > 1e-8 {
255                    edges.push((i, j));
256                }
257            }
258        }
259
260        let num_edges = edges.len();
261
262        // Create edge index tensor
263        let mut edge_index_vec = Vec::new();
264        for &(src, _dst) in &edges {
265            edge_index_vec.push(src as f32);
266        }
267        for &(_src, dst) in &edges {
268            edge_index_vec.push(dst as f32);
269        }
270
271        let edge_index = from_vec(edge_index_vec, &[2, num_edges], DeviceType::Cpu)?;
272        let x = zeros(&[num_nodes, 1])?;
273
274        Ok(GraphData::new(x, edge_index))
275    }
276
277    /// Convert GraphData to adjacency matrix
278    ///
279    /// Creates a dense adjacency matrix from a GraphData structure.
280    ///
281    /// # Arguments
282    /// * `graph` - Input graph data
283    ///
284    /// # Returns
285    /// Dense adjacency matrix (num_nodes x num_nodes)
286    pub fn to_adjacency_matrix(graph: &GraphData) -> Result<Tensor, Box<dyn std::error::Error>> {
287        let num_nodes = graph.num_nodes;
288        let mut adj_data = vec![0.0f32; num_nodes * num_nodes];
289
290        let edge_data = graph.edge_index.to_vec()?;
291
292        for i in 0..graph.num_edges {
293            let src = edge_data[i] as usize;
294            let dst = edge_data[graph.num_edges + i] as usize;
295
296            if src < num_nodes && dst < num_nodes {
297                adj_data[src * num_nodes + dst] = 1.0;
298            }
299        }
300
301        Ok(from_vec(
302            adj_data,
303            &[num_nodes, num_nodes],
304            DeviceType::Cpu,
305        )?)
306    }
307
308    /// Convert GraphData to weighted adjacency matrix
309    ///
310    /// Creates a weighted adjacency matrix using edge attributes.
311    ///
312    /// # Arguments
313    /// * `graph` - Input graph data with edge attributes
314    ///
315    /// # Returns
316    /// Weighted adjacency matrix (num_nodes x num_nodes)
317    pub fn to_weighted_adjacency_matrix(
318        graph: &GraphData,
319    ) -> Result<Tensor, Box<dyn std::error::Error>> {
320        let num_nodes = graph.num_nodes;
321        let mut adj_data = vec![0.0f32; num_nodes * num_nodes];
322
323        let edge_data = graph.edge_index.to_vec()?;
324
325        if let Some(ref edge_attr) = graph.edge_attr {
326            let weights = edge_attr.to_vec()?;
327
328            for i in 0..graph.num_edges {
329                let src = edge_data[i] as usize;
330                let dst = edge_data[graph.num_edges + i] as usize;
331
332                if src < num_nodes && dst < num_nodes && i < weights.len() {
333                    adj_data[src * num_nodes + dst] = weights[i];
334                }
335            }
336        } else {
337            // No edge attributes, use binary adjacency
338            for i in 0..graph.num_edges {
339                let src = edge_data[i] as usize;
340                let dst = edge_data[graph.num_edges + i] as usize;
341
342                if src < num_nodes && dst < num_nodes {
343                    adj_data[src * num_nodes + dst] = 1.0;
344                }
345            }
346        }
347
348        Ok(from_vec(
349            adj_data,
350            &[num_nodes, num_nodes],
351            DeviceType::Cpu,
352        )?)
353    }
354}
355
356/// Graph augmentation utilities
357pub mod augmentation {
358    use super::*;
359    use scirs2_core::random::thread_rng;
360    use torsh_tensor::creation::randn;
361
362    /// Add self-loops to a graph
363    ///
364    /// Adds self-connections (i->i) for all nodes in the graph.
365    /// This is commonly used in GCN-style architectures.
366    pub fn add_self_loops(graph: &mut GraphData) -> Result<(), Box<dyn std::error::Error>> {
367        let num_nodes = graph.num_nodes;
368        let existing_edges = graph.edge_index.to_vec()?;
369
370        // Create self-loop edges
371        let mut self_loops = Vec::new();
372        for i in 0..num_nodes {
373            self_loops.push(i as f32); // source
374        }
375        for i in 0..num_nodes {
376            self_loops.push(i as f32); // destination
377        }
378
379        // Concatenate existing edges with self-loops
380        let mut all_edges = existing_edges;
381        all_edges.extend(self_loops);
382
383        let new_num_edges = graph.num_edges + num_nodes;
384        graph.edge_index = from_vec(all_edges, &[2, new_num_edges], DeviceType::Cpu)?;
385        graph.num_edges = new_num_edges;
386
387        Ok(())
388    }
389
390    /// Remove isolated nodes from the graph
391    ///
392    /// Removes nodes that have no incoming or outgoing edges.
393    /// Returns the mapping from old node indices to new node indices.
394    pub fn remove_isolated_nodes(
395        graph: &mut GraphData,
396    ) -> Result<Vec<Option<usize>>, Box<dyn std::error::Error>> {
397        let edge_data = graph.edge_index.to_vec()?;
398        let num_nodes = graph.num_nodes;
399
400        // Find which nodes have edges
401        let mut has_edge = vec![false; num_nodes];
402        for i in 0..graph.num_edges {
403            let src = edge_data[i] as usize;
404            let dst = edge_data[graph.num_edges + i] as usize;
405            if src < num_nodes {
406                has_edge[src] = true;
407            }
408            if dst < num_nodes {
409                has_edge[dst] = true;
410            }
411        }
412
413        // Create mapping from old to new indices
414        let mut old_to_new = vec![None; num_nodes];
415        let mut new_idx = 0;
416        for (old_idx, &has_edges) in has_edge.iter().enumerate() {
417            if has_edges {
418                old_to_new[old_idx] = Some(new_idx);
419                new_idx += 1;
420            }
421        }
422
423        let new_num_nodes = new_idx;
424
425        // Remap edges
426        let mut new_edges = Vec::new();
427        for i in 0..graph.num_edges {
428            let src = edge_data[i] as usize;
429            let dst = edge_data[graph.num_edges + i] as usize;
430
431            if src < num_nodes && dst < num_nodes {
432                if let (Some(new_src), Some(new_dst)) = (old_to_new[src], old_to_new[dst]) {
433                    new_edges.push(new_src as f32);
434                    new_edges.push(new_dst as f32);
435                }
436            }
437        }
438
439        let new_num_edges = new_edges.len() / 2;
440
441        // Remap node features
442        let feature_dim = graph.x.shape().dims()[1];
443        let mut new_features = Vec::new();
444        for old_idx in 0..num_nodes {
445            if old_to_new[old_idx].is_some() {
446                // Extract features for this node
447                for f in 0..feature_dim {
448                    let idx = old_idx * feature_dim + f;
449                    if let Ok(feat_data) = graph.x.to_vec() {
450                        if idx < feat_data.len() {
451                            new_features.push(feat_data[idx]);
452                        }
453                    }
454                }
455            }
456        }
457
458        // Update graph
459        graph.x = from_vec(new_features, &[new_num_nodes, feature_dim], DeviceType::Cpu)?;
460        graph.edge_index = from_vec(new_edges, &[2, new_num_edges], DeviceType::Cpu)?;
461        graph.num_nodes = new_num_nodes;
462        graph.num_edges = new_num_edges;
463
464        Ok(old_to_new)
465    }
466
467    /// Normalize node features to unit norm
468    ///
469    /// Normalizes each node's feature vector to have L2 norm = 1.
470    /// This helps stabilize training and makes features scale-invariant.
471    pub fn normalize_features(graph: &mut GraphData) -> Result<(), Box<dyn std::error::Error>> {
472        let feature_data = graph.x.to_vec()?;
473        let num_nodes = graph.num_nodes;
474        let feature_dim = graph.x.shape().dims()[1];
475
476        let mut normalized = Vec::new();
477
478        for node in 0..num_nodes {
479            let start = node * feature_dim;
480            let end = start + feature_dim;
481            let node_features = &feature_data[start..end];
482
483            // Compute L2 norm
484            let norm: f32 = node_features.iter().map(|&x| x * x).sum::<f32>().sqrt();
485            let norm = norm.max(1e-8); // Avoid division by zero
486
487            // Normalize
488            for &feat in node_features {
489                normalized.push(feat / norm);
490            }
491        }
492
493        graph.x = from_vec(normalized, &[num_nodes, feature_dim], DeviceType::Cpu)?;
494        Ok(())
495    }
496
497    /// Drop edges randomly for augmentation
498    ///
499    /// Randomly removes a fraction of edges from the graph.
500    /// This is useful for regularization and data augmentation.
501    pub fn edge_dropout(
502        graph: &mut GraphData,
503        drop_rate: f32,
504    ) -> Result<(), Box<dyn std::error::Error>> {
505        let mut rng = thread_rng();
506        let edge_data = graph.edge_index.to_vec()?;
507
508        // An edge is a (source, destination) pair: row 0 holds the sources and
509        // row 1 the destinations. The keep/drop decision must be made ONCE per
510        // edge and applied to both endpoints — deciding sources and destinations
511        // with two independent RNG passes (as this did) drops the endpoints out
512        // of sync and produces a malformed, mismatched-length edge_index.
513        let mut kept_sources = Vec::new();
514        let mut kept_dests = Vec::new();
515        for i in 0..graph.num_edges {
516            if rng.gen_range(0.0..1.0) > drop_rate {
517                kept_sources.push(edge_data[i]);
518                kept_dests.push(edge_data[graph.num_edges + i]);
519            }
520        }
521
522        let kept_count = kept_sources.len();
523        let mut kept_edges = kept_sources;
524        kept_edges.extend(kept_dests);
525
526        graph.edge_index = from_vec(kept_edges, &[2, kept_count], DeviceType::Cpu)?;
527        graph.num_edges = kept_count;
528
529        Ok(())
530    }
531
532    /// Mask node features randomly for augmentation
533    ///
534    /// Randomly masks (zeros out) a fraction of node features.
535    /// Useful for contrastive learning and robustness.
536    pub fn feature_masking(
537        graph: &mut GraphData,
538        mask_rate: f32,
539    ) -> Result<(), Box<dyn std::error::Error>> {
540        let mut rng = thread_rng();
541        let mut feature_data = graph.x.to_vec()?;
542
543        for feat in feature_data.iter_mut() {
544            if rng.gen_range(0.0..1.0) < mask_rate {
545                *feat = 0.0;
546            }
547        }
548
549        let shape = graph.x.shape().dims().to_vec();
550        graph.x = from_vec(feature_data, &shape, DeviceType::Cpu)?;
551
552        Ok(())
553    }
554
555    /// Add random noise to node features
556    ///
557    /// Adds Gaussian noise to node features for augmentation.
558    /// This improves model robustness and generalization.
559    pub fn feature_noise(
560        graph: &mut GraphData,
561        noise_std: f32,
562    ) -> Result<(), Box<dyn std::error::Error>> {
563        let shape_binding = graph.x.shape();
564        let shape = shape_binding.dims();
565        let noise: Tensor = randn::<f32>(shape)?;
566
567        let noise_data = noise.to_vec()?;
568        let mut feature_data = graph.x.to_vec()?;
569
570        for (feat, &n) in feature_data.iter_mut().zip(noise_data.iter()) {
571            let noise_val: f32 = n * noise_std;
572            *feat = *feat + noise_val;
573        }
574
575        graph.x = from_vec(feature_data, shape, DeviceType::Cpu)?;
576
577        Ok(())
578    }
579
580    /// Drop nodes randomly (subgraph sampling)
581    ///
582    /// Randomly removes a fraction of nodes and their associated edges.
583    /// Useful for creating mini-batches from large graphs.
584    pub fn node_dropout(
585        graph: &mut GraphData,
586        drop_rate: f32,
587    ) -> Result<(), Box<dyn std::error::Error>> {
588        let mut rng = thread_rng();
589        let num_nodes = graph.num_nodes;
590
591        // Determine which nodes to keep
592        let mut keep_node = vec![false; num_nodes];
593        for i in 0..num_nodes {
594            if rng.gen_range(0.0..1.0) > drop_rate {
595                keep_node[i] = true;
596            }
597        }
598
599        // Create mapping from old to new indices
600        let mut old_to_new = vec![None; num_nodes];
601        let mut new_idx = 0;
602        for (old_idx, &keep) in keep_node.iter().enumerate() {
603            if keep {
604                old_to_new[old_idx] = Some(new_idx);
605                new_idx += 1;
606            }
607        }
608
609        let new_num_nodes = new_idx;
610
611        // Filter edges and remap
612        let edge_data = graph.edge_index.to_vec()?;
613        let mut new_edges = Vec::new();
614
615        for i in 0..graph.num_edges {
616            let src = edge_data[i] as usize;
617            let dst = edge_data[graph.num_edges + i] as usize;
618
619            if src < num_nodes && dst < num_nodes && keep_node[src] && keep_node[dst] {
620                if let (Some(new_src), Some(new_dst)) = (old_to_new[src], old_to_new[dst]) {
621                    new_edges.push(new_src as f32);
622                    new_edges.push(new_dst as f32);
623                }
624            }
625        }
626
627        let new_num_edges = new_edges.len() / 2;
628
629        // Filter node features
630        let feature_dim = graph.x.shape().dims()[1];
631        let feature_data = graph.x.to_vec()?;
632        let mut new_features = Vec::new();
633
634        for old_idx in 0..num_nodes {
635            if keep_node[old_idx] {
636                let start = old_idx * feature_dim;
637                let end = start + feature_dim;
638                new_features.extend_from_slice(&feature_data[start..end]);
639            }
640        }
641
642        // Update graph
643        graph.x = from_vec(new_features, &[new_num_nodes, feature_dim], DeviceType::Cpu)?;
644        graph.edge_index = from_vec(new_edges, &[2, new_num_edges], DeviceType::Cpu)?;
645        graph.num_nodes = new_num_nodes;
646        graph.num_edges = new_num_edges;
647
648        Ok(())
649    }
650
651    /// Apply random walk-based subgraph sampling
652    ///
653    /// Samples a subgraph by performing random walks from random starting nodes.
654    /// This preserves local graph structure better than random node dropout.
655    pub fn random_walk_subgraph(
656        graph: &GraphData,
657        num_walks: usize,
658        walk_length: usize,
659    ) -> Result<GraphData, Box<dyn std::error::Error>> {
660        let mut rng = thread_rng();
661        let edge_data = graph.edge_index.to_vec()?;
662
663        // Build adjacency list
664        let mut adj_list: Vec<Vec<usize>> = vec![Vec::new(); graph.num_nodes];
665        for i in 0..graph.num_edges {
666            let src = edge_data[i] as usize;
667            let dst = edge_data[graph.num_edges + i] as usize;
668            if src < graph.num_nodes && dst < graph.num_nodes {
669                adj_list[src].push(dst);
670            }
671        }
672
673        // Perform random walks
674        let mut visited_nodes = std::collections::HashSet::new();
675
676        for _ in 0..num_walks {
677            let mut current = rng.gen_range(0..graph.num_nodes);
678            visited_nodes.insert(current);
679
680            for _ in 0..walk_length {
681                if adj_list[current].is_empty() {
682                    break;
683                }
684                let next_idx = rng.gen_range(0..adj_list[current].len());
685                current = adj_list[current][next_idx];
686                visited_nodes.insert(current);
687            }
688        }
689
690        // Create subgraph with visited nodes
691        let visited: Vec<usize> = visited_nodes.into_iter().collect();
692        let mut old_to_new = vec![None; graph.num_nodes];
693        for (new_idx, &old_idx) in visited.iter().enumerate() {
694            old_to_new[old_idx] = Some(new_idx);
695        }
696
697        let new_num_nodes = visited.len();
698
699        // Extract edges
700        let mut new_edges = Vec::new();
701        for i in 0..graph.num_edges {
702            let src = edge_data[i] as usize;
703            let dst = edge_data[graph.num_edges + i] as usize;
704
705            if src < graph.num_nodes && dst < graph.num_nodes {
706                if let (Some(new_src), Some(new_dst)) = (old_to_new[src], old_to_new[dst]) {
707                    new_edges.push(new_src as f32);
708                    new_edges.push(new_dst as f32);
709                }
710            }
711        }
712
713        let new_num_edges = new_edges.len() / 2;
714
715        // Extract features
716        let feature_dim = graph.x.shape().dims()[1];
717        let feature_data = graph.x.to_vec()?;
718        let mut new_features = Vec::new();
719
720        for &old_idx in &visited {
721            let start = old_idx * feature_dim;
722            let end = start + feature_dim;
723            if end <= feature_data.len() {
724                new_features.extend_from_slice(&feature_data[start..end]);
725            }
726        }
727
728        // Create new graph
729        let x = from_vec(new_features, &[new_num_nodes, feature_dim], DeviceType::Cpu)?;
730        let edge_index = from_vec(new_edges, &[2, new_num_edges], DeviceType::Cpu)?;
731
732        Ok(GraphData::new(x, edge_index))
733    }
734}
735
736#[cfg(test)]
737mod tests {
738    use super::augmentation::*;
739    use super::converters::*;
740    use super::*;
741    use torsh_tensor::creation::randn;
742
743    #[test]
744    fn test_add_self_loops() {
745        let x = randn(&[3, 4]).unwrap();
746        let edge_index = from_vec(vec![0.0, 1.0, 1.0, 2.0], &[2, 2], DeviceType::Cpu).unwrap();
747        let mut graph = GraphData::new(x, edge_index);
748
749        let original_edges = graph.num_edges;
750        add_self_loops(&mut graph).unwrap();
751
752        assert_eq!(graph.num_edges, original_edges + 3); // Added 3 self-loops
753    }
754
755    #[test]
756    fn test_normalize_features() {
757        let x = randn(&[5, 8]).unwrap();
758        let edge_index = from_vec(vec![0.0, 1.0, 1.0, 2.0], &[2, 2], DeviceType::Cpu).unwrap();
759        let mut graph = GraphData::new(x, edge_index);
760
761        normalize_features(&mut graph).unwrap();
762
763        // Check that features are normalized
764        let normalized_data = graph.x.to_vec().unwrap();
765        let feature_dim = 8;
766
767        for node in 0..5 {
768            let start = node * feature_dim;
769            let end = start + feature_dim;
770            let node_features = &normalized_data[start..end];
771            let norm: f32 = node_features.iter().map(|&x| x * x).sum::<f32>().sqrt();
772            assert!(
773                (norm - 1.0).abs() < 1e-5,
774                "Features should be normalized to unit norm"
775            );
776        }
777    }
778
779    #[test]
780    fn test_edge_dropout() {
781        let x = randn(&[4, 3]).unwrap();
782        let edge_index = from_vec(
783            vec![0.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 0.0],
784            &[2, 4],
785            DeviceType::Cpu,
786        )
787        .unwrap();
788        let mut graph = GraphData::new(x, edge_index);
789
790        let original_edges = graph.num_edges;
791        edge_dropout(&mut graph, 0.5).unwrap();
792
793        assert!(graph.num_edges <= original_edges);
794        assert_eq!(graph.edge_index.shape().dims()[1], graph.num_edges);
795    }
796
797    #[test]
798    fn test_feature_masking() {
799        let x = randn(&[3, 5]).unwrap();
800        let edge_index = from_vec(vec![0.0, 1.0, 1.0, 2.0], &[2, 2], DeviceType::Cpu).unwrap();
801        let mut graph = GraphData::new(x.clone(), edge_index);
802
803        feature_masking(&mut graph, 0.3).unwrap();
804
805        let masked_data = graph.x.to_vec().unwrap();
806        let zero_count = masked_data.iter().filter(|&&x| x == 0.0).count();
807
808        // Some features should be masked
809        assert!(zero_count > 0, "Some features should be masked");
810    }
811
812    #[test]
813    fn test_node_dropout() {
814        let x = randn(&[5, 4]).unwrap();
815        let edge_index = from_vec(
816            vec![0.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 4.0],
817            &[2, 4],
818            DeviceType::Cpu,
819        )
820        .unwrap();
821        let mut graph = GraphData::new(x, edge_index);
822
823        let original_nodes = graph.num_nodes;
824        node_dropout(&mut graph, 0.3).unwrap();
825
826        assert!(graph.num_nodes <= original_nodes);
827        assert_eq!(graph.x.shape().dims()[0], graph.num_nodes);
828    }
829
830    #[test]
831    fn test_remove_isolated_nodes() {
832        let x = randn(&[5, 3]).unwrap();
833        // Create graph where node 2 is isolated
834        let edge_index = from_vec(
835            vec![0.0, 1.0, 3.0, 4.0, 1.0, 0.0, 4.0, 3.0],
836            &[2, 4],
837            DeviceType::Cpu,
838        )
839        .unwrap();
840        let mut graph = GraphData::new(x, edge_index);
841
842        let mapping = remove_isolated_nodes(&mut graph).unwrap();
843
844        assert_eq!(graph.num_nodes, 4); // Node 2 should be removed
845        assert!(mapping[2].is_none()); // Node 2 should have no mapping
846    }
847
848    #[test]
849    fn test_from_adjacency_matrix() {
850        // Create a simple 3x3 adjacency matrix
851        let adj_data = vec![
852            0.0, 1.0, 0.0, // Node 0 connects to node 1
853            1.0, 0.0, 1.0, // Node 1 connects to nodes 0 and 2
854            0.0, 1.0, 0.0, // Node 2 connects to node 1
855        ];
856        let adj = from_vec(adj_data, &[3, 3], DeviceType::Cpu).unwrap();
857
858        let graph = from_adjacency_matrix(&adj).unwrap();
859
860        assert_eq!(graph.num_nodes, 3);
861        assert_eq!(graph.num_edges, 4); // 4 directed edges
862    }
863
864    #[test]
865    fn test_to_adjacency_matrix() {
866        let x = randn(&[3, 2]).unwrap();
867        let edge_index =
868            from_vec(vec![0.0, 1.0, 2.0, 1.0, 2.0, 0.0], &[2, 3], DeviceType::Cpu).unwrap();
869        let graph = GraphData::new(x, edge_index);
870
871        let adj = to_adjacency_matrix(&graph).unwrap();
872
873        assert_eq!(adj.shape().dims(), &[3, 3]);
874
875        // Check that edges are present in adjacency matrix
876        let adj_data = adj.to_vec().unwrap();
877        assert_eq!(adj_data[0 * 3 + 1], 1.0); // Edge 0->1
878        assert_eq!(adj_data[1 * 3 + 2], 1.0); // Edge 1->2
879        assert_eq!(adj_data[2 * 3 + 0], 1.0); // Edge 2->0
880    }
881
882    #[test]
883    fn test_from_edge_list() {
884        let edges = vec![(0, 1), (1, 2), (2, 0), (1, 0)];
885        let graph = from_edge_list(&edges, 3).unwrap();
886
887        assert_eq!(graph.num_nodes, 3);
888        assert_eq!(graph.num_edges, 4);
889    }
890
891    #[test]
892    fn test_adjacency_round_trip() {
893        // Create graph from edge list
894        let edges = vec![(0, 1), (1, 2), (2, 3)];
895        let graph1 = from_edge_list(&edges, 4).unwrap();
896
897        // Convert to adjacency matrix and back
898        let adj = to_adjacency_matrix(&graph1).unwrap();
899        let graph2 = from_adjacency_matrix(&adj).unwrap();
900
901        assert_eq!(graph1.num_nodes, graph2.num_nodes);
902        assert_eq!(graph1.num_edges, graph2.num_edges);
903    }
904}