Skip to main content

torsh_graph/
datasets.rs

1//! Graph dataset loaders with support for popular formats
2//!
3//! Implementation of comprehensive graph dataset loading capabilities
4//! as specified in TODO.md, including GraphML, GML, and other formats.
5
6// Framework infrastructure - components designed for future use
7#![allow(dead_code)]
8use crate::GraphData;
9use serde::{Deserialize, Serialize};
10use serde_json;
11use std::collections::HashMap;
12use std::fs::File;
13use std::io::{BufRead, BufReader, Result as IoResult};
14use std::path::Path;
15use torsh_core::device::DeviceType;
16use torsh_tensor::{creation::from_vec, Tensor};
17
18/// Graph dataset loader trait
19pub trait GraphDatasetLoader {
20    /// Load graph from file
21    fn load_from_file<P: AsRef<Path>>(&self, path: P) -> IoResult<GraphData>;
22
23    /// Load multiple graphs from directory
24    fn load_from_directory<P: AsRef<Path>>(&self, path: P) -> IoResult<Vec<GraphData>>;
25
26    /// Get supported file extensions
27    fn supported_extensions(&self) -> Vec<&'static str>;
28}
29
30/// Edge list format loader (simple format: src dst)
31pub struct EdgeListLoader {
32    /// Number of node features (will be filled with random values)
33    pub num_features: usize,
34    /// Whether edges are directed
35    pub directed: bool,
36    /// Delimiter for parsing
37    pub delimiter: char,
38}
39
40impl EdgeListLoader {
41    pub fn new(num_features: usize, directed: bool) -> Self {
42        Self {
43            num_features,
44            directed,
45            delimiter: ' ',
46        }
47    }
48
49    pub fn with_delimiter(mut self, delimiter: char) -> Self {
50        self.delimiter = delimiter;
51        self
52    }
53}
54
55impl GraphDatasetLoader for EdgeListLoader {
56    fn load_from_file<P: AsRef<Path>>(&self, path: P) -> IoResult<GraphData> {
57        let file = File::open(path)?;
58        let reader = BufReader::new(file);
59
60        let mut edges = Vec::new();
61        let mut max_node_id = 0usize;
62
63        // Read edges
64        for line in reader.lines() {
65            let line = line?;
66            let line = line.trim();
67
68            // Skip comments and empty lines
69            if line.is_empty() || line.starts_with('#') {
70                continue;
71            }
72
73            let parts: Vec<&str> = line.split(self.delimiter).collect();
74            if parts.len() >= 2 {
75                if let (Ok(src), Ok(dst)) = (parts[0].parse::<usize>(), parts[1].parse::<usize>()) {
76                    edges.extend_from_slice(&[src, dst]);
77                    max_node_id = max_node_id.max(src).max(dst);
78
79                    // Add reverse edge for undirected graphs
80                    if !self.directed && src != dst {
81                        edges.extend_from_slice(&[dst, src]);
82                    }
83                }
84            }
85        }
86
87        let num_nodes = max_node_id + 1;
88        let num_edges = edges.len() / 2;
89
90        // Create node features (random for now)
91        let features = (0..num_nodes * self.num_features)
92            .map(|i| (i as f32 * 0.1) % 1.0)
93            .collect::<Vec<f32>>();
94        let x =
95            from_vec(features, &[num_nodes, self.num_features], DeviceType::Cpu).map_err(|e| {
96                std::io::Error::new(
97                    std::io::ErrorKind::InvalidData,
98                    format!("Tensor creation failed: {:?}", e),
99                )
100            })?;
101
102        // Create edge index
103        let edge_index = from_vec(
104            edges.into_iter().map(|e| e as i64).collect(),
105            &[2, num_edges],
106            DeviceType::Cpu,
107        )
108        .map_err(|e| {
109            std::io::Error::new(
110                std::io::ErrorKind::InvalidData,
111                format!("Edge tensor creation failed: {:?}", e),
112            )
113        })?;
114
115        Ok(GraphData::new(
116            x,
117            edge_index.to_f32_simd().map_err(|e| {
118                std::io::Error::new(
119                    std::io::ErrorKind::InvalidData,
120                    format!("Edge index conversion failed: {:?}", e),
121                )
122            })?,
123        ))
124    }
125
126    fn load_from_directory<P: AsRef<Path>>(&self, path: P) -> IoResult<Vec<GraphData>> {
127        let mut graphs = Vec::new();
128        let dir = std::fs::read_dir(path)?;
129
130        for entry in dir {
131            let entry = entry?;
132            let path = entry.path();
133
134            if let Some(ext) = path.extension() {
135                if self
136                    .supported_extensions()
137                    .contains(&ext.to_str().unwrap_or(""))
138                {
139                    match self.load_from_file(&path) {
140                        Ok(graph) => graphs.push(graph),
141                        Err(e) => eprintln!("Warning: Failed to load {}: {}", path.display(), e),
142                    }
143                }
144            }
145        }
146
147        Ok(graphs)
148    }
149
150    fn supported_extensions(&self) -> Vec<&'static str> {
151        vec!["edges", "edgelist", "txt"]
152    }
153}
154
155/// GML (Graph Modelling Language) format loader
156pub struct GMLLoader;
157
158impl GMLLoader {
159    pub fn new() -> Self {
160        Self
161    }
162}
163
164impl GraphDatasetLoader for GMLLoader {
165    fn load_from_file<P: AsRef<Path>>(&self, path: P) -> IoResult<GraphData> {
166        let content = std::fs::read_to_string(path)?;
167        self.parse_gml(&content)
168    }
169
170    fn load_from_directory<P: AsRef<Path>>(&self, path: P) -> IoResult<Vec<GraphData>> {
171        let mut graphs = Vec::new();
172        let dir = std::fs::read_dir(path)?;
173
174        for entry in dir {
175            let entry = entry?;
176            let path = entry.path();
177
178            if let Some(ext) = path.extension() {
179                if self
180                    .supported_extensions()
181                    .contains(&ext.to_str().unwrap_or(""))
182                {
183                    match self.load_from_file(&path) {
184                        Ok(graph) => graphs.push(graph),
185                        Err(e) => {
186                            eprintln!("Warning: Failed to load GML {}: {}", path.display(), e)
187                        }
188                    }
189                }
190            }
191        }
192
193        Ok(graphs)
194    }
195
196    fn supported_extensions(&self) -> Vec<&'static str> {
197        vec!["gml"]
198    }
199}
200
201impl GMLLoader {
202    fn parse_gml(&self, content: &str) -> IoResult<GraphData> {
203        let mut nodes = HashMap::new();
204        let mut edges = Vec::new();
205        let mut in_node = false;
206        let mut in_edge = false;
207        let mut current_node_id: Option<usize> = None;
208        let mut current_edge_src: Option<usize> = None;
209        let mut current_edge_dst: Option<usize> = None;
210
211        for line in content.lines() {
212            let line = line.trim();
213
214            if line.starts_with("node") {
215                in_node = true;
216                in_edge = false;
217            } else if line.starts_with("edge") {
218                in_edge = true;
219                in_node = false;
220            } else if line == "]" {
221                // End of node or edge block
222                if in_node {
223                    if let Some(id) = current_node_id {
224                        nodes.insert(id, vec![1.0; 4]); // Default features
225                    }
226                    current_node_id = None;
227                    in_node = false;
228                } else if in_edge {
229                    if let (Some(src), Some(dst)) = (current_edge_src, current_edge_dst) {
230                        edges.extend_from_slice(&[src, dst]);
231                    }
232                    current_edge_src = None;
233                    current_edge_dst = None;
234                    in_edge = false;
235                }
236            } else if in_node && line.starts_with("id") {
237                if let Some(id_str) = line.split_whitespace().nth(1) {
238                    current_node_id = id_str.parse().ok();
239                }
240            } else if in_edge && line.starts_with("source") {
241                if let Some(src_str) = line.split_whitespace().nth(1) {
242                    current_edge_src = src_str.parse().ok();
243                }
244            } else if in_edge && line.starts_with("target") {
245                if let Some(dst_str) = line.split_whitespace().nth(1) {
246                    current_edge_dst = dst_str.parse().ok();
247                }
248            }
249        }
250
251        // Convert to tensors
252        let num_nodes = nodes.len();
253        let num_edges = edges.len() / 2;
254
255        if num_nodes == 0 {
256            return Err(std::io::Error::new(
257                std::io::ErrorKind::InvalidData,
258                "No nodes found in GML file",
259            ));
260        }
261
262        // Create ordered node features
263        let mut node_features = Vec::new();
264        let mut node_mapping = HashMap::new();
265        let mut new_id = 0;
266
267        let mut sorted_nodes: Vec<_> = nodes.keys().collect();
268        sorted_nodes.sort();
269
270        for &original_id in sorted_nodes {
271            node_mapping.insert(original_id, new_id);
272            node_features.extend_from_slice(&nodes[&original_id]);
273            new_id += 1;
274        }
275
276        // Remap edges
277        let remapped_edges: Vec<i64> = edges
278            .iter()
279            .map(|&id| *node_mapping.get(&id).unwrap_or(&0) as i64)
280            .collect();
281
282        let x = from_vec(node_features, &[num_nodes, 4], DeviceType::Cpu).map_err(|e| {
283            std::io::Error::new(
284                std::io::ErrorKind::InvalidData,
285                format!("Node features tensor creation failed: {:?}", e),
286            )
287        })?;
288
289        let edge_index: Tensor<i64> = from_vec(remapped_edges, &[2, num_edges], DeviceType::Cpu)
290            .map_err(|e| {
291                std::io::Error::new(
292                    std::io::ErrorKind::InvalidData,
293                    format!("Edge index tensor creation failed: {:?}", e),
294                )
295            })?;
296
297        Ok(GraphData::new(
298            x,
299            edge_index.to_f32_simd().map_err(|e| {
300                std::io::Error::new(
301                    std::io::ErrorKind::InvalidData,
302                    format!("Edge index conversion failed: {:?}", e),
303                )
304            })?,
305        ))
306    }
307}
308
309/// JSON format loader for graph data
310#[derive(Debug, Serialize, Deserialize)]
311pub struct JsonGraphData {
312    pub nodes: Vec<JsonNode>,
313    pub edges: Vec<JsonEdge>,
314    pub directed: Option<bool>,
315    pub multigraph: Option<bool>,
316}
317
318#[derive(Debug, Serialize, Deserialize)]
319pub struct JsonNode {
320    pub id: usize,
321    pub features: Option<Vec<f32>>,
322    pub label: Option<String>,
323}
324
325#[derive(Debug, Serialize, Deserialize)]
326pub struct JsonEdge {
327    pub source: usize,
328    pub target: usize,
329    pub weight: Option<f32>,
330    pub label: Option<String>,
331}
332
333pub struct JSONLoader {
334    pub default_features: usize,
335}
336
337impl JSONLoader {
338    pub fn new(default_features: usize) -> Self {
339        Self { default_features }
340    }
341}
342
343impl GraphDatasetLoader for JSONLoader {
344    fn load_from_file<P: AsRef<Path>>(&self, path: P) -> IoResult<GraphData> {
345        let content = std::fs::read_to_string(path)?;
346        let data: JsonGraphData = serde_json::from_str(&content)
347            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
348
349        self.convert_json_to_graph(data)
350    }
351
352    fn load_from_directory<P: AsRef<Path>>(&self, path: P) -> IoResult<Vec<GraphData>> {
353        let mut graphs = Vec::new();
354        let dir = std::fs::read_dir(path)?;
355
356        for entry in dir {
357            let entry = entry?;
358            let path = entry.path();
359
360            if let Some(ext) = path.extension() {
361                if self
362                    .supported_extensions()
363                    .contains(&ext.to_str().unwrap_or(""))
364                {
365                    match self.load_from_file(&path) {
366                        Ok(graph) => graphs.push(graph),
367                        Err(e) => {
368                            eprintln!("Warning: Failed to load JSON {}: {}", path.display(), e)
369                        }
370                    }
371                }
372            }
373        }
374
375        Ok(graphs)
376    }
377
378    fn supported_extensions(&self) -> Vec<&'static str> {
379        vec!["json"]
380    }
381}
382
383impl JSONLoader {
384    fn convert_json_to_graph(&self, data: JsonGraphData) -> IoResult<GraphData> {
385        let num_nodes = data.nodes.len();
386        let _num_edges = data.edges.len();
387
388        if num_nodes == 0 {
389            return Err(std::io::Error::new(
390                std::io::ErrorKind::InvalidData,
391                "No nodes in JSON graph data",
392            ));
393        }
394
395        // Create node ID mapping
396        let mut node_mapping = HashMap::new();
397        for (i, node) in data.nodes.iter().enumerate() {
398            node_mapping.insert(node.id, i);
399        }
400
401        // Extract node features
402        let mut node_features = Vec::new();
403        for node in &data.nodes {
404            if let Some(ref features) = node.features {
405                node_features.extend_from_slice(features);
406            } else {
407                // Use default features
408                node_features.extend((0..self.default_features).map(|i| i as f32 * 0.1));
409            }
410        }
411
412        let feature_dim = node_features.len() / num_nodes;
413
414        // Extract edges
415        let mut edges = Vec::new();
416        for edge in &data.edges {
417            if let (Some(&src), Some(&dst)) = (
418                node_mapping.get(&edge.source),
419                node_mapping.get(&edge.target),
420            ) {
421                edges.extend_from_slice(&[src as i64, dst as i64]);
422
423                // Add reverse edge for undirected graphs
424                if !data.directed.unwrap_or(false) && src != dst {
425                    edges.extend_from_slice(&[dst as i64, src as i64]);
426                }
427            }
428        }
429
430        let final_num_edges = edges.len() / 2;
431
432        let x =
433            from_vec(node_features, &[num_nodes, feature_dim], DeviceType::Cpu).map_err(|e| {
434                std::io::Error::new(
435                    std::io::ErrorKind::InvalidData,
436                    format!("Node features failed: {:?}", e),
437                )
438            })?;
439
440        let edge_index = from_vec(edges, &[2, final_num_edges], DeviceType::Cpu).map_err(|e| {
441            std::io::Error::new(
442                std::io::ErrorKind::InvalidData,
443                format!("Edge index failed: {:?}", e),
444            )
445        })?;
446
447        Ok(GraphData::new(
448            x,
449            edge_index.to_f32_simd().map_err(|e| {
450                std::io::Error::new(
451                    std::io::ErrorKind::InvalidData,
452                    format!("Edge index conversion failed: {:?}", e),
453                )
454            })?,
455        ))
456    }
457}
458
459/// Graph dataset collections for common benchmarks
460pub struct GraphDatasetCollection;
461
462impl GraphDatasetCollection {
463    /// Create a synthetic dataset for testing
464    pub fn create_synthetic_dataset(
465        num_graphs: usize,
466        nodes_per_graph: usize,
467        edge_probability: f64,
468        num_features: usize,
469    ) -> Result<Vec<GraphData>, torsh_core::error::TorshError> {
470        use crate::scirs2_integration::generation;
471
472        (0..num_graphs)
473            .map(|_| {
474                let mut graph = generation::erdos_renyi(nodes_per_graph, edge_probability)?;
475
476                // Ensure correct feature dimension
477                if graph.x.shape().dims()[1] != num_features {
478                    let new_features = (0..nodes_per_graph * num_features)
479                        .map(|i| (i as f32 * 0.01) % 1.0)
480                        .collect();
481                    let x = from_vec(
482                        new_features,
483                        &[nodes_per_graph, num_features],
484                        DeviceType::Cpu,
485                    )?;
486                    graph.x = x;
487                }
488
489                Ok(graph)
490            })
491            .collect()
492    }
493
494    /// Load a collection of graphs with data augmentation
495    pub fn load_with_augmentation<L: GraphDatasetLoader>(
496        loader: L,
497        path: impl AsRef<Path>,
498        augmentation_factor: usize,
499    ) -> IoResult<Vec<GraphData>> {
500        let base_graphs = loader.load_from_directory(path)?;
501        let mut augmented_graphs = base_graphs.clone();
502
503        for _ in 1..augmentation_factor {
504            for graph in &base_graphs {
505                // Simple augmentation: add noise to features
506                let augmented = Self::add_feature_noise(graph, 0.1)?;
507                augmented_graphs.push(augmented);
508            }
509        }
510
511        Ok(augmented_graphs)
512    }
513
514    /// Add noise to node features for data augmentation
515    ///
516    /// # Errors
517    /// Returns an error when the node-feature tensor cannot be read back or the
518    /// noisy tensor cannot be allocated.
519    pub fn add_feature_noise(graph: &GraphData, noise_level: f32) -> IoResult<GraphData> {
520        let mut rng = scirs2_core::random::thread_rng();
521        let features = graph.x.to_vec().map_err(|e| {
522            std::io::Error::new(
523                std::io::ErrorKind::InvalidData,
524                format!("node feature conversion failed: {e:?}"),
525            )
526        })?;
527        let noisy_features: Vec<f32> = features
528            .iter()
529            .map(|&x| x + (rng.random::<f32>() - 0.5) * 2.0 * noise_level)
530            .collect();
531
532        let noisy_x =
533            from_vec(noisy_features, graph.x.shape().dims(), DeviceType::Cpu).map_err(|e| {
534                std::io::Error::new(
535                    std::io::ErrorKind::InvalidData,
536                    format!("noisy feature tensor creation failed: {e:?}"),
537                )
538            })?;
539
540        Ok(GraphData {
541            x: noisy_x,
542            edge_index: graph.edge_index.clone(),
543            edge_attr: graph.edge_attr.clone(),
544            batch: graph.batch.clone(),
545            num_nodes: graph.num_nodes,
546            num_edges: graph.num_edges,
547        })
548    }
549
550    /// Split dataset into train/validation/test
551    pub fn train_val_test_split(
552        graphs: Vec<GraphData>,
553        train_ratio: f64,
554        val_ratio: f64,
555    ) -> (Vec<GraphData>, Vec<GraphData>, Vec<GraphData>) {
556        let n = graphs.len();
557        let train_size = (n as f64 * train_ratio) as usize;
558        let val_size = (n as f64 * val_ratio) as usize;
559
560        let mut train_graphs = Vec::new();
561        let mut val_graphs = Vec::new();
562        let mut test_graphs = Vec::new();
563
564        for (i, graph) in graphs.into_iter().enumerate() {
565            if i < train_size {
566                train_graphs.push(graph);
567            } else if i < train_size + val_size {
568                val_graphs.push(graph);
569            } else {
570                test_graphs.push(graph);
571            }
572        }
573
574        (train_graphs, val_graphs, test_graphs)
575    }
576}
577
578/// Graph sampler for batch processing
579pub struct GraphSampler {
580    batch_size: usize,
581    shuffle: bool,
582}
583
584impl GraphSampler {
585    pub fn new(batch_size: usize, shuffle: bool) -> Self {
586        Self {
587            batch_size,
588            shuffle,
589        }
590    }
591
592    /// Sample batches from a dataset
593    pub fn sample_batches<'a>(&self, graphs: &'a [GraphData]) -> Vec<Vec<&'a GraphData>> {
594        let mut indices: Vec<usize> = (0..graphs.len()).collect();
595
596        if self.shuffle {
597            let mut rng = scirs2_core::random::thread_rng();
598            for i in (1..indices.len()).rev() {
599                let j = (rng.random::<f64>() * (i + 1) as f64) as usize;
600                indices.swap(i, j);
601            }
602        }
603
604        indices
605            .chunks(self.batch_size)
606            .map(|chunk| chunk.iter().map(|&i| &graphs[i]).collect())
607            .collect()
608    }
609}
610
611/// Dynamic graph handling for temporal networks
612pub struct TemporalGraphLoader {
613    pub time_steps: usize,
614    pub node_features: usize,
615}
616
617impl TemporalGraphLoader {
618    pub fn new(time_steps: usize, node_features: usize) -> Self {
619        Self {
620            time_steps,
621            node_features,
622        }
623    }
624
625    /// Load temporal graph sequence
626    pub fn load_temporal_sequence<P: AsRef<Path>>(&self, base_path: P) -> IoResult<Vec<GraphData>> {
627        let mut graphs = Vec::new();
628        let base_path = base_path.as_ref();
629
630        for t in 0..self.time_steps {
631            let file_path = base_path.join(format!("graph_t{}.edges", t));
632
633            if file_path.exists() {
634                let loader = EdgeListLoader::new(self.node_features, true);
635                match loader.load_from_file(&file_path) {
636                    Ok(graph) => graphs.push(graph),
637                    Err(e) => {
638                        return Err(std::io::Error::new(
639                            std::io::ErrorKind::InvalidData,
640                            format!("failed to load timestep {t}: {e}"),
641                        ));
642                    }
643                }
644            }
645        }
646
647        Ok(graphs)
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654    use std::io::Write;
655    use tempfile::NamedTempFile;
656
657    #[test]
658    fn test_edge_list_loader() {
659        let mut temp_file = NamedTempFile::new().unwrap();
660        writeln!(temp_file, "0 1").unwrap();
661        writeln!(temp_file, "1 2").unwrap();
662        writeln!(temp_file, "2 0").unwrap();
663
664        let loader = EdgeListLoader::new(3, false);
665        let graph = loader.load_from_file(temp_file.path()).unwrap();
666
667        assert_eq!(graph.num_nodes, 3);
668        assert_eq!(graph.num_edges, 6); // Undirected, so doubled
669        assert_eq!(graph.x.shape().dims(), &[3, 3]);
670    }
671
672    #[test]
673    fn test_json_loader() {
674        let json_data = r#"{
675            "nodes": [
676                {"id": 0, "features": [1.0, 2.0]},
677                {"id": 1, "features": [3.0, 4.0]}
678            ],
679            "edges": [
680                {"source": 0, "target": 1}
681            ],
682            "directed": true
683        }"#;
684
685        let mut temp_file = NamedTempFile::new().unwrap();
686        temp_file.write_all(json_data.as_bytes()).unwrap();
687
688        let loader = JSONLoader::new(2);
689        let graph = loader.load_from_file(temp_file.path()).unwrap();
690
691        assert_eq!(graph.num_nodes, 2);
692        assert_eq!(graph.num_edges, 1);
693        assert_eq!(graph.x.shape().dims(), &[2, 2]);
694    }
695
696    #[test]
697    fn test_graph_dataset_collection() {
698        let graphs = GraphDatasetCollection::create_synthetic_dataset(5, 10, 0.2, 4)
699            .expect("operation should succeed");
700
701        assert_eq!(graphs.len(), 5);
702        for graph in graphs {
703            assert_eq!(graph.num_nodes, 10);
704            assert_eq!(graph.x.shape().dims(), &[10, 4]);
705        }
706    }
707
708    #[test]
709    fn test_train_val_test_split() {
710        let graphs = GraphDatasetCollection::create_synthetic_dataset(100, 10, 0.1, 3)
711            .expect("operation should succeed");
712        let (train, val, test) = GraphDatasetCollection::train_val_test_split(graphs, 0.7, 0.2);
713
714        assert_eq!(train.len(), 70);
715        assert_eq!(val.len(), 20);
716        assert_eq!(test.len(), 10);
717    }
718
719    #[test]
720    fn test_graph_sampler() {
721        let graphs = GraphDatasetCollection::create_synthetic_dataset(10, 5, 0.3, 2)
722            .expect("operation should succeed");
723        let sampler = GraphSampler::new(3, false);
724        let batches = sampler.sample_batches(&graphs);
725
726        assert_eq!(batches.len(), 4); // 10 graphs with batch_size 3 = 4 batches
727        assert_eq!(batches[0].len(), 3);
728        assert_eq!(batches[3].len(), 1); // Last batch has remainder
729    }
730
731    #[test]
732    fn test_feature_noise_augmentation() {
733        let base_graph = GraphDatasetCollection::create_synthetic_dataset(1, 5, 0.4, 3)
734            .expect("operation should succeed")[0]
735            .clone();
736        let noisy_graph = GraphDatasetCollection::add_feature_noise(&base_graph, 0.1)
737            .expect("operation should succeed");
738
739        assert_eq!(noisy_graph.num_nodes, base_graph.num_nodes);
740        assert_eq!(noisy_graph.num_edges, base_graph.num_edges);
741        assert_eq!(noisy_graph.x.shape().dims(), base_graph.x.shape().dims());
742
743        // Features should be different due to noise
744        let original_features = base_graph.x.to_vec().unwrap();
745        let noisy_features = noisy_graph.x.to_vec().unwrap();
746
747        let mut differences = 0;
748        for (orig, noisy) in original_features.iter().zip(noisy_features.iter()) {
749            if (orig - noisy).abs() > 1e-6 {
750                differences += 1;
751            }
752        }
753
754        // Should have some differences due to noise
755        assert!(differences > 0);
756    }
757}