1type 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
10pub const DEFAULT_NODE_FEATURES: usize = 16;
13use torsh_tensor::{
14 creation::{from_vec, zeros},
15 Tensor,
16};
17pub struct GraphDataLoader {
21 batch_size: usize,
22 shuffle: bool,
23 num_node_features: usize,
24}
25
26impl GraphDataLoader {
27 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 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 pub fn num_node_features(&self) -> usize {
66 self.num_node_features
67 }
68
69 pub fn batch_size(&self) -> usize {
71 self.batch_size
72 }
73
74 pub fn shuffle(&self) -> bool {
76 self.shuffle
77 }
78
79 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
155pub mod converters {
157 use super::*;
158
159 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 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 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 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 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 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 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 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
356pub mod augmentation {
358 use super::*;
359 use scirs2_core::random::thread_rng;
360 use torsh_tensor::creation::randn;
361
362 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 let mut self_loops = Vec::new();
372 for i in 0..num_nodes {
373 self_loops.push(i as f32); }
375 for i in 0..num_nodes {
376 self_loops.push(i as f32); }
378
379 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 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 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 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 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 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 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 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 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 let norm: f32 = node_features.iter().map(|&x| x * x).sum::<f32>().sqrt();
485 let norm = norm.max(1e-8); 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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); }
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 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 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 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); assert!(mapping[2].is_none()); }
847
848 #[test]
849 fn test_from_adjacency_matrix() {
850 let adj_data = vec![
852 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, ];
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); }
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 let adj_data = adj.to_vec().unwrap();
877 assert_eq!(adj_data[0 * 3 + 1], 1.0); assert_eq!(adj_data[1 * 3 + 2], 1.0); assert_eq!(adj_data[2 * 3 + 0], 1.0); }
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 let edges = vec![(0, 1), (1, 2), (2, 3)];
895 let graph1 = from_edge_list(&edges, 4).unwrap();
896
897 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}