Skip to main content

torsh_graph/
matching.rs

1//! Graph Matching and Similarity Learning
2//!
3//! Advanced implementation of graph matching algorithms and graph similarity
4//! learning methods for comparing and aligning graph structures.
5//!
6//! # Features:
7//! - Graph isomorphism testing and subgraph matching
8//! - Graph edit distance computation
9//! - Graph kernel methods for similarity
10//! - Neural graph matching networks
11//! - Graph alignment and correspondence learning
12//! - Siamese and triplet networks for graph similarity
13// Framework infrastructure - components designed for future use
14#![allow(dead_code)]
15/// Crate-local result alias: the error type defaults to [`TorshError`],
16/// so both `Result<T>` and `Result<T, OtherError>` stay valid.
17type Result<T, E = torsh_core::error::TorshError> = std::result::Result<T, E>;
18
19use crate::parameter::Parameter;
20use crate::GraphData;
21use std::collections::{HashMap, HashSet, VecDeque};
22use torsh_tensor::{
23    creation::{from_vec, randn, zeros},
24    Tensor,
25};
26
27/// Graph Edit Distance (GED) computation
28pub struct GraphEditDistance {
29    /// Cost for node insertion/deletion
30    pub node_cost: f32,
31    /// Cost for edge insertion/deletion
32    pub edge_cost: f32,
33    /// Cost for node substitution
34    pub node_subst_cost: f32,
35    /// Cost for edge substitution
36    pub edge_subst_cost: f32,
37}
38
39impl GraphEditDistance {
40    /// Create a new GED calculator with default costs
41    pub fn new() -> Self {
42        Self {
43            node_cost: 1.0,
44            edge_cost: 1.0,
45            node_subst_cost: 1.0,
46            edge_subst_cost: 1.0,
47        }
48    }
49
50    /// Compute approximate graph edit distance between two graphs
51    ///
52    /// # Errors
53    /// Propagates tensor-operation failures from the feature distance.
54    pub fn compute(&self, graph1: &GraphData, graph2: &GraphData) -> Result<f32> {
55        let n1 = graph1.num_nodes;
56        let n2 = graph2.num_nodes;
57
58        // Node operations cost
59        let node_ops = ((n1 as i32 - n2 as i32).abs() as f32) * self.node_cost;
60
61        // Edge operations cost (simplified)
62        let e1 = graph1.num_edges;
63        let e2 = graph2.num_edges;
64        let edge_ops = ((e1 as i32 - e2 as i32).abs() as f32) * self.edge_cost;
65
66        // Feature dissimilarity (using L2 distance)
67        let feature_cost = self.compute_feature_distance(graph1, graph2)?;
68
69        Ok(node_ops + edge_ops + feature_cost)
70    }
71
72    /// Compute feature distance between graphs
73    fn compute_feature_distance(&self, graph1: &GraphData, graph2: &GraphData) -> Result<f32> {
74        let f1_data = graph1.x.to_vec()?;
75        let f2_data = graph2.x.to_vec()?;
76
77        let min_len = f1_data.len().min(f2_data.len());
78        let mut dist = 0.0;
79
80        for i in 0..min_len {
81            dist += (f1_data[i] - f2_data[i]).powi(2);
82        }
83
84        // Add penalty for size mismatch
85        dist += ((f1_data.len() as i32 - f2_data.len() as i32).abs() as f32) * self.node_subst_cost;
86
87        Ok(dist.sqrt())
88    }
89
90    /// Find approximate node correspondence between two graphs
91    pub fn node_correspondence(
92        &self,
93        graph1: &GraphData,
94        graph2: &GraphData,
95    ) -> Result<Vec<(usize, usize)>> {
96        let mut correspondences = Vec::new();
97        let n1 = graph1.num_nodes;
98        let n2 = graph2.num_nodes;
99
100        // Simple greedy matching based on feature similarity
101        let mut matched_nodes2 = HashSet::new();
102
103        for i in 0..n1 {
104            let mut best_match = None;
105            let mut best_similarity = f32::NEG_INFINITY;
106
107            for j in 0..n2 {
108                if matched_nodes2.contains(&j) {
109                    continue;
110                }
111
112                let similarity = self.node_similarity(graph1, i, graph2, j)?;
113                if similarity > best_similarity {
114                    best_similarity = similarity;
115                    best_match = Some(j);
116                }
117            }
118
119            if let Some(j) = best_match {
120                correspondences.push((i, j));
121                matched_nodes2.insert(j);
122            }
123        }
124
125        Ok(correspondences)
126    }
127
128    /// Compute similarity between two nodes
129    fn node_similarity(
130        &self,
131        graph1: &GraphData,
132        node1: usize,
133        graph2: &GraphData,
134        node2: usize,
135    ) -> Result<f32> {
136        let f1 = graph1.x.slice_tensor(0, node1, node1 + 1)?;
137        let f2 = graph2.x.slice_tensor(0, node2, node2 + 1)?;
138
139        // Cosine similarity
140        let dot = f1.dot(&f2.t()?)?.item()?;
141        let norm1 = f1.norm()?.item()?;
142        let norm2 = f2.norm()?.item()?;
143
144        if norm1 > 0.0 && norm2 > 0.0 {
145            Ok(dot / (norm1 * norm2))
146        } else {
147            Ok(0.0)
148        }
149    }
150}
151
152impl Default for GraphEditDistance {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158/// Graph Kernel methods for similarity computation
159pub struct GraphKernel {
160    kernel_type: GraphKernelType,
161}
162
163#[derive(Debug, Clone, Copy)]
164pub enum GraphKernelType {
165    /// Random walk kernel
166    RandomWalk,
167    /// Shortest path kernel
168    ShortestPath,
169    /// Weisfeiler-Lehman kernel
170    WeisfeilerLehman,
171    /// Graphlet kernel
172    Graphlet,
173}
174
175impl GraphKernel {
176    /// Create a new graph kernel
177    pub fn new(kernel_type: GraphKernelType) -> Self {
178        Self { kernel_type }
179    }
180
181    /// Compute kernel similarity between two graphs
182    ///
183    /// # Errors
184    /// Propagates tensor-operation failures from the underlying kernel.
185    pub fn compute(&self, graph1: &GraphData, graph2: &GraphData) -> Result<f32> {
186        match self.kernel_type {
187            GraphKernelType::RandomWalk => self.random_walk_kernel(graph1, graph2),
188            GraphKernelType::ShortestPath => self.shortest_path_kernel(graph1, graph2),
189            GraphKernelType::WeisfeilerLehman => self.wl_kernel(graph1, graph2),
190            GraphKernelType::Graphlet => self.graphlet_kernel(graph1, graph2),
191        }
192    }
193
194    /// Random walk kernel
195    fn random_walk_kernel(&self, graph1: &GraphData, graph2: &GraphData) -> Result<f32> {
196        // Simplified: count common random walk patterns
197        let walks1 = self.sample_random_walks(graph1, 10, 5)?;
198        let walks2 = self.sample_random_walks(graph2, 10, 5)?;
199
200        let mut common_count = 0;
201        for w1 in &walks1 {
202            if walks2.contains(w1) {
203                common_count += 1;
204            }
205        }
206
207        Ok(common_count as f32 / (walks1.len() + walks2.len()) as f32)
208    }
209
210    /// Sample random walks from a graph
211    fn sample_random_walks(
212        &self,
213        graph: &GraphData,
214        num_walks: usize,
215        walk_length: usize,
216    ) -> Result<Vec<Vec<usize>>> {
217        let mut rng = scirs2_core::random::thread_rng();
218        let mut walks = Vec::new();
219        let edge_data = graph.edge_index.to_vec()?;
220
221        // Build adjacency list
222        let mut adj_list: HashMap<usize, Vec<usize>> = HashMap::new();
223        for i in (0..edge_data.len()).step_by(2) {
224            if i + 1 < edge_data.len() {
225                let src = edge_data[i] as usize;
226                let dst = edge_data[i + 1] as usize;
227                adj_list.entry(src).or_insert_with(Vec::new).push(dst);
228            }
229        }
230
231        // Sample walks
232        for _ in 0..num_walks {
233            if graph.num_nodes == 0 {
234                break;
235            }
236
237            let mut walk = Vec::new();
238            let mut current_node = rng.gen_range(0..graph.num_nodes);
239            walk.push(current_node);
240
241            for _ in 0..walk_length {
242                if let Some(neighbors) = adj_list.get(&current_node) {
243                    if neighbors.is_empty() {
244                        break;
245                    }
246                    let idx = rng.gen_range(0..neighbors.len());
247                    current_node = neighbors[idx];
248                    walk.push(current_node);
249                } else {
250                    break;
251                }
252            }
253
254            walks.push(walk);
255        }
256
257        Ok(walks)
258    }
259
260    /// Shortest path kernel
261    fn shortest_path_kernel(&self, graph1: &GraphData, graph2: &GraphData) -> Result<f32> {
262        // Compare shortest path distributions
263        let sp1 = self.compute_shortest_paths_distribution(graph1)?;
264        let sp2 = self.compute_shortest_paths_distribution(graph2)?;
265
266        // Compute histogram intersection
267        let mut intersection = 0.0;
268        for i in 0..sp1.len().min(sp2.len()) {
269            intersection += sp1[i].min(sp2[i]);
270        }
271
272        Ok(intersection)
273    }
274
275    /// Compute distribution of shortest path lengths
276    fn compute_shortest_paths_distribution(&self, graph: &GraphData) -> Result<Vec<f32>> {
277        let max_path_len = 10;
278        let mut distribution = vec![0.0; max_path_len];
279
280        // Simplified: use BFS to compute some shortest paths
281        let edge_data = graph.edge_index.to_vec()?;
282        let mut adj_list: HashMap<usize, Vec<usize>> = HashMap::new();
283
284        for i in (0..edge_data.len()).step_by(2) {
285            if i + 1 < edge_data.len() {
286                let src = edge_data[i] as usize;
287                let dst = edge_data[i + 1] as usize;
288                adj_list.entry(src).or_insert_with(Vec::new).push(dst);
289            }
290        }
291
292        // BFS from a few random nodes
293        let num_samples = graph.num_nodes.min(5);
294        for start in 0..num_samples {
295            let path_lengths = self.bfs_shortest_paths(&adj_list, start, graph.num_nodes);
296            for length in path_lengths {
297                if length < max_path_len {
298                    distribution[length] += 1.0;
299                }
300            }
301        }
302
303        // Normalize
304        let sum: f32 = distribution.iter().sum();
305        if sum > 0.0 {
306            for val in &mut distribution {
307                *val /= sum;
308            }
309        }
310
311        Ok(distribution)
312    }
313
314    /// BFS to compute shortest path lengths
315    fn bfs_shortest_paths(
316        &self,
317        adj_list: &HashMap<usize, Vec<usize>>,
318        start: usize,
319        num_nodes: usize,
320    ) -> Vec<usize> {
321        let mut distances = vec![usize::MAX; num_nodes];
322        let mut queue = VecDeque::new();
323
324        distances[start] = 0;
325        queue.push_back(start);
326
327        while let Some(node) = queue.pop_front() {
328            if let Some(neighbors) = adj_list.get(&node) {
329                for &neighbor in neighbors {
330                    if neighbor < num_nodes && distances[neighbor] == usize::MAX {
331                        distances[neighbor] = distances[node] + 1;
332                        queue.push_back(neighbor);
333                    }
334                }
335            }
336        }
337
338        distances.into_iter().filter(|&d| d != usize::MAX).collect()
339    }
340
341    /// Weisfeiler-Lehman kernel
342    fn wl_kernel(&self, graph1: &GraphData, graph2: &GraphData) -> Result<f32> {
343        // Simplified WL: compare node label histograms after one iteration
344        let labels1 = self.wl_iteration(graph1)?;
345        let labels2 = self.wl_iteration(graph2)?;
346
347        // Compute label histogram similarity
348        let mut hist1: HashMap<usize, f32> = HashMap::new();
349        let mut hist2: HashMap<usize, f32> = HashMap::new();
350
351        for &label in &labels1 {
352            *hist1.entry(label).or_insert(0.0) += 1.0;
353        }
354        for &label in &labels2 {
355            *hist2.entry(label).or_insert(0.0) += 1.0;
356        }
357
358        // Histogram intersection
359        let all_labels: HashSet<_> = hist1.keys().chain(hist2.keys()).collect();
360        let mut intersection = 0.0;
361
362        for &&label in &all_labels {
363            let count1 = hist1.get(&label).copied().unwrap_or(0.0);
364            let count2 = hist2.get(&label).copied().unwrap_or(0.0);
365            intersection += count1.min(count2);
366        }
367
368        Ok(intersection / (labels1.len() + labels2.len()) as f32)
369    }
370
371    /// One iteration of Weisfeiler-Lehman relabeling
372    fn wl_iteration(&self, graph: &GraphData) -> Result<Vec<usize>> {
373        let num_nodes = graph.num_nodes;
374        let labels = vec![0; num_nodes]; // Initial labels
375
376        // Build adjacency list
377        let edge_data = graph.edge_index.to_vec()?;
378        let mut adj_list: HashMap<usize, Vec<usize>> = HashMap::new();
379
380        for i in (0..edge_data.len()).step_by(2) {
381            if i + 1 < edge_data.len() {
382                let src = edge_data[i] as usize;
383                let dst = edge_data[i + 1] as usize;
384                adj_list.entry(src).or_insert_with(Vec::new).push(dst);
385            }
386        }
387
388        // Update labels based on neighborhood
389        let mut new_labels = vec![0; num_nodes];
390        for node in 0..num_nodes {
391            let mut neighbor_labels = vec![labels[node]];
392            if let Some(neighbors) = adj_list.get(&node) {
393                for &neighbor in neighbors {
394                    if neighbor < num_nodes {
395                        neighbor_labels.push(labels[neighbor]);
396                    }
397                }
398            }
399            neighbor_labels.sort_unstable();
400
401            // Hash neighbor labels to create new label (simplified)
402            new_labels[node] = neighbor_labels
403                .iter()
404                .fold(0usize, |acc, &l| acc.wrapping_mul(31).wrapping_add(l));
405        }
406
407        Ok(new_labels)
408    }
409
410    /// Graphlet kernel
411    fn graphlet_kernel(&self, graph1: &GraphData, graph2: &GraphData) -> Result<f32> {
412        // Simplified: count small subgraph patterns (triangles, stars, etc.)
413        let graphlets1 = self.count_graphlets(graph1)?;
414        let graphlets2 = self.count_graphlets(graph2)?;
415
416        // Compare graphlet counts
417        let mut similarity = 0.0;
418        for (pattern, &count1) in &graphlets1 {
419            if let Some(&count2) = graphlets2.get(pattern) {
420                similarity += count1.min(count2);
421            }
422        }
423
424        Ok(similarity / (graph1.num_nodes + graph2.num_nodes) as f32)
425    }
426
427    /// Count small graphlet patterns
428    fn count_graphlets(&self, graph: &GraphData) -> Result<HashMap<String, f32>> {
429        let mut counts = HashMap::new();
430
431        // Build adjacency list
432        let edge_data = graph.edge_index.to_vec()?;
433        let mut adj_list: HashMap<usize, Vec<usize>> = HashMap::new();
434
435        for i in (0..edge_data.len()).step_by(2) {
436            if i + 1 < edge_data.len() {
437                let src = edge_data[i] as usize;
438                let dst = edge_data[i + 1] as usize;
439                adj_list.entry(src).or_insert_with(Vec::new).push(dst);
440            }
441        }
442
443        // Count triangles
444        let mut triangles = 0.0;
445        for (_node, neighbors) in &adj_list {
446            for i in 0..neighbors.len() {
447                for j in (i + 1)..neighbors.len() {
448                    let n1 = neighbors[i];
449                    let n2 = neighbors[j];
450
451                    if let Some(n1_neighbors) = adj_list.get(&n1) {
452                        if n1_neighbors.contains(&n2) {
453                            triangles += 1.0;
454                        }
455                    }
456                }
457            }
458        }
459        counts.insert("triangle".to_string(), triangles / 3.0); // Each triangle counted 3 times
460
461        // Count stars (nodes with degree >= 3)
462        let mut stars = 0.0;
463        for neighbors in adj_list.values() {
464            if neighbors.len() >= 3 {
465                stars += 1.0;
466            }
467        }
468        counts.insert("star".to_string(), stars);
469
470        Ok(counts)
471    }
472}
473
474/// Neural Graph Matching Network
475#[derive(Debug)]
476pub struct GraphMatchingNetwork {
477    node_embedding_dim: usize,
478    hidden_dim: usize,
479
480    // Node embedding layers
481    node_encoder1: Parameter,
482    node_encoder2: Parameter,
483
484    // Cross-graph attention
485    attention_query: Parameter,
486    attention_key: Parameter,
487    attention_value: Parameter,
488
489    // Matching score layers
490    matching_layer1: Parameter,
491    matching_layer2: Parameter,
492    output_layer: Parameter,
493
494    bias: Option<Parameter>,
495}
496
497impl GraphMatchingNetwork {
498    /// Create a new graph matching network
499    pub fn new(node_embedding_dim: usize, hidden_dim: usize, use_bias: bool) -> Result<Self> {
500        let node_encoder1 = Parameter::new(randn(&[node_embedding_dim, hidden_dim])?);
501        let node_encoder2 = Parameter::new(randn(&[hidden_dim, hidden_dim])?);
502
503        let attention_query = Parameter::new(randn(&[hidden_dim, hidden_dim])?);
504        let attention_key = Parameter::new(randn(&[hidden_dim, hidden_dim])?);
505        let attention_value = Parameter::new(randn(&[hidden_dim, hidden_dim])?);
506
507        let matching_layer1 = Parameter::new(randn(&[hidden_dim * 2, hidden_dim])?);
508        let matching_layer2 = Parameter::new(randn(&[hidden_dim, (hidden_dim / 2)])?);
509        let output_layer = Parameter::new(randn(&[(hidden_dim / 2), 1])?);
510
511        let bias = if use_bias {
512            Some(Parameter::new(zeros(&[1])?))
513        } else {
514            None
515        };
516
517        Ok(Self {
518            node_embedding_dim,
519            hidden_dim,
520            node_encoder1,
521            node_encoder2,
522            attention_query,
523            attention_key,
524            attention_value,
525            matching_layer1,
526            matching_layer2,
527            output_layer,
528            bias,
529        })
530    }
531
532    /// Compute similarity score between two graphs
533    pub fn compute_similarity(&self, graph1: &GraphData, graph2: &GraphData) -> Result<f32> {
534        // Encode both graphs
535        let h1 = self.encode_graph(&graph1.x)?;
536        let h2 = self.encode_graph(&graph2.x)?;
537
538        // Cross-graph attention
539        let attended1 = self.cross_attention(&h1, &h2)?;
540        let attended2 = self.cross_attention(&h2, &h1)?;
541
542        // Pool to graph-level representations
543        let g1 = attended1.mean(Some(&[0]), false)?;
544        let g2 = attended2.mean(Some(&[0]), false)?;
545
546        // Concatenate
547        let g1_data = g1.to_vec()?;
548        let g2_data = g2.to_vec()?;
549        let mut concat_data = g1_data;
550        concat_data.extend(g2_data);
551
552        let concat = from_vec(
553            concat_data,
554            &[1, self.hidden_dim * 2],
555            torsh_core::device::DeviceType::Cpu,
556        )?;
557
558        // Matching layers
559        let mut h = concat.matmul(&self.matching_layer1.clone_data())?;
560        h = self.relu(&h)?;
561
562        h = h.matmul(&self.matching_layer2.clone_data())?;
563        h = self.relu(&h)?;
564
565        let mut score = h.matmul(&self.output_layer.clone_data())?;
566        if let Some(ref bias) = self.bias {
567            score = score.add(&bias.clone_data())?;
568        }
569
570        // Sigmoid activation
571        let score_val = score.item()?;
572        Ok(1.0 / (1.0 + (-score_val).exp()))
573    }
574
575    /// Encode graph features
576    fn encode_graph(&self, x: &Tensor) -> Result<Tensor> {
577        let mut h = x.matmul(&self.node_encoder1.clone_data())?;
578        h = self.relu(&h)?;
579        h = h.matmul(&self.node_encoder2.clone_data())?;
580        self.relu(&h)
581    }
582
583    /// Cross-graph attention mechanism
584    fn cross_attention(&self, query_graph: &Tensor, key_value_graph: &Tensor) -> Result<Tensor> {
585        let _q = query_graph.matmul(&self.attention_query.clone_data())?;
586        let _k = key_value_graph.matmul(&self.attention_key.clone_data())?;
587        let v = key_value_graph.matmul(&self.attention_value.clone_data())?;
588
589        // Simplified attention: mean pooling
590        // In practice, would compute q @ k^T / sqrt(d), then softmax, then @ v
591        Ok(v.mean(Some(&[0]), false)?.unsqueeze(0)?)
592    }
593
594    fn relu(&self, x: &Tensor) -> Result<Tensor> {
595        let data = x.to_vec()?;
596        let activated: Vec<f32> = data.iter().map(|&v| v.max(0.0)).collect();
597        Ok(from_vec(
598            activated,
599            x.shape().dims(),
600            torsh_core::device::DeviceType::Cpu,
601        )?)
602    }
603
604    fn parameters(&self) -> Vec<Tensor> {
605        let mut params = vec![
606            self.node_encoder1.clone_data(),
607            self.node_encoder2.clone_data(),
608            self.attention_query.clone_data(),
609            self.attention_key.clone_data(),
610            self.attention_value.clone_data(),
611            self.matching_layer1.clone_data(),
612            self.matching_layer2.clone_data(),
613            self.output_layer.clone_data(),
614        ];
615
616        if let Some(ref b) = self.bias {
617            params.push(b.clone_data());
618        }
619
620        params
621    }
622}
623
624/// Siamese Graph Network for similarity learning
625#[derive(Debug)]
626pub struct SiameseGraphNetwork {
627    embedding_network: Parameter,
628    hidden_dim: usize,
629    output_dim: usize,
630}
631
632impl SiameseGraphNetwork {
633    /// Create a new Siamese graph network
634    pub fn new(input_dim: usize, hidden_dim: usize, output_dim: usize) -> Result<Self> {
635        let embedding_network = Parameter::new(randn(&[input_dim, hidden_dim])?);
636
637        Ok(Self {
638            embedding_network,
639            hidden_dim,
640            output_dim,
641        })
642    }
643
644    /// Compute embeddings for a graph
645    pub fn embed(&self, graph: &GraphData) -> Result<Tensor> {
646        let mut h = graph.x.matmul(&self.embedding_network.clone_data())?;
647        h = self.relu(&h)?;
648
649        // Global pooling
650        Ok(h.mean(Some(&[0]), false)?)
651    }
652
653    /// Compute contrastive loss between similar and dissimilar pairs
654    pub fn contrastive_loss(
655        &self,
656        graph1: &GraphData,
657        graph2: &GraphData,
658        is_similar: bool,
659        margin: f32,
660    ) -> Result<f32> {
661        let emb1 = self.embed(graph1)?;
662        let emb2 = self.embed(graph2)?;
663
664        // Euclidean distance
665        let diff = emb1.sub(&emb2)?;
666        let dist_sq = diff.dot(&diff)?.item()?;
667        let dist = dist_sq.sqrt();
668
669        if is_similar {
670            // Pull similar graphs closer
671            Ok(dist_sq)
672        } else {
673            // Push dissimilar graphs apart
674            Ok((margin - dist).max(0.0).powi(2))
675        }
676    }
677
678    fn relu(&self, x: &Tensor) -> Result<Tensor> {
679        let data = x.to_vec()?;
680        let activated: Vec<f32> = data.iter().map(|&v| v.max(0.0)).collect();
681        Ok(from_vec(
682            activated,
683            x.shape().dims(),
684            torsh_core::device::DeviceType::Cpu,
685        )?)
686    }
687
688    fn parameters(&self) -> Vec<Tensor> {
689        vec![self.embedding_network.clone_data()]
690    }
691}
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696    use torsh_core::device::DeviceType;
697
698    #[test]
699    fn test_graph_edit_distance() {
700        let features1 = randn(&[4, 3]).unwrap();
701        let features2 = randn(&[5, 3]).unwrap();
702        let edges1 = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0];
703        let edges2 = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0];
704
705        let edge_index1 = from_vec(edges1, &[2, 3], DeviceType::Cpu).unwrap();
706        let edge_index2 = from_vec(edges2, &[2, 4], DeviceType::Cpu).unwrap();
707
708        let graph1 = GraphData::new(features1, edge_index1);
709        let graph2 = GraphData::new(features2, edge_index2);
710
711        let ged = GraphEditDistance::new();
712        let distance = ged
713            .compute(&graph1, &graph2)
714            .expect("operation should succeed");
715
716        assert!(distance > 0.0);
717    }
718
719    #[test]
720    fn test_node_correspondence() {
721        let features1 = randn(&[3, 4]).unwrap();
722        let features2 = randn(&[3, 4]).unwrap();
723        let edges = vec![0.0, 1.0, 1.0, 2.0];
724
725        let edge_index = from_vec(edges, &[2, 2], DeviceType::Cpu).unwrap();
726        let graph1 = GraphData::new(features1, edge_index.clone());
727        let graph2 = GraphData::new(features2, edge_index);
728
729        let ged = GraphEditDistance::new();
730        let correspondences = ged
731            .node_correspondence(&graph1, &graph2)
732            .expect("operation should succeed");
733
734        assert_eq!(correspondences.len(), 3);
735    }
736
737    #[test]
738    fn test_random_walk_kernel() {
739        let features1 = randn(&[4, 3]).unwrap();
740        let features2 = randn(&[4, 3]).unwrap();
741        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0];
742
743        let edge_index = from_vec(edges, &[2, 3], DeviceType::Cpu).unwrap();
744        let graph1 = GraphData::new(features1, edge_index.clone());
745        let graph2 = GraphData::new(features2, edge_index);
746
747        let kernel = GraphKernel::new(GraphKernelType::RandomWalk);
748        let similarity = kernel
749            .compute(&graph1, &graph2)
750            .expect("operation should succeed");
751
752        assert!(similarity >= 0.0 && similarity <= 1.0);
753    }
754
755    #[test]
756    fn test_shortest_path_kernel() {
757        let features1 = randn(&[5, 3]).unwrap();
758        let features2 = randn(&[5, 3]).unwrap();
759        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0];
760
761        let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
762        let graph1 = GraphData::new(features1, edge_index.clone());
763        let graph2 = GraphData::new(features2, edge_index);
764
765        let kernel = GraphKernel::new(GraphKernelType::ShortestPath);
766        let similarity = kernel
767            .compute(&graph1, &graph2)
768            .expect("operation should succeed");
769
770        assert!(similarity >= 0.0 && similarity <= 1.0);
771    }
772
773    #[test]
774    fn test_weisfeiler_lehman_kernel() {
775        let features1 = randn(&[4, 3]).unwrap();
776        let features2 = randn(&[4, 3]).unwrap();
777        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0];
778
779        let edge_index = from_vec(edges, &[2, 3], DeviceType::Cpu).unwrap();
780        let graph1 = GraphData::new(features1, edge_index.clone());
781        let graph2 = GraphData::new(features2, edge_index);
782
783        let kernel = GraphKernel::new(GraphKernelType::WeisfeilerLehman);
784        let similarity = kernel
785            .compute(&graph1, &graph2)
786            .expect("operation should succeed");
787
788        assert!(similarity >= 0.0);
789    }
790
791    #[test]
792    fn test_graph_matching_network() {
793        let features1 = randn(&[4, 8]).unwrap();
794        let features2 = randn(&[5, 8]).unwrap();
795        let edges1 = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0];
796        let edges2 = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0];
797
798        let edge_index1 = from_vec(edges1, &[2, 3], DeviceType::Cpu).unwrap();
799        let edge_index2 = from_vec(edges2, &[2, 4], DeviceType::Cpu).unwrap();
800
801        let graph1 = GraphData::new(features1, edge_index1);
802        let graph2 = GraphData::new(features2, edge_index2);
803
804        let gmn = GraphMatchingNetwork::new(8, 16, true).expect("operation should succeed");
805        let similarity = gmn
806            .compute_similarity(&graph1, &graph2)
807            .expect("operation should succeed");
808
809        assert!(similarity >= 0.0 && similarity <= 1.0);
810    }
811
812    #[test]
813    fn test_siamese_network() {
814        let features1 = randn(&[3, 6]).unwrap();
815        let features2 = randn(&[3, 6]).unwrap();
816        let edges = vec![0.0, 1.0, 1.0, 2.0];
817
818        let edge_index = from_vec(edges, &[2, 2], DeviceType::Cpu).unwrap();
819        let graph1 = GraphData::new(features1, edge_index.clone());
820        let graph2 = GraphData::new(features2, edge_index);
821
822        let siamese = SiameseGraphNetwork::new(6, 12, 8).expect("operation should succeed");
823
824        let emb1 = siamese.embed(&graph1);
825        let emb2 = siamese.embed(&graph2).expect("operation should succeed");
826
827        assert_eq!(
828            emb1.expect("operation should succeed").shape().dims(),
829            &[12]
830        );
831        assert_eq!(emb2.shape().dims(), &[12]);
832
833        // Test contrastive loss for similar graphs
834        let loss_similar = siamese
835            .contrastive_loss(&graph1, &graph2, true, 1.0)
836            .expect("operation should succeed");
837        assert!(loss_similar >= 0.0);
838
839        // Test contrastive loss for dissimilar graphs
840        let loss_dissimilar = siamese
841            .contrastive_loss(&graph1, &graph2, false, 1.0)
842            .expect("operation should succeed");
843        assert!(loss_dissimilar >= 0.0);
844    }
845
846    #[test]
847    fn test_graphlet_kernel() {
848        let features1 = randn(&[5, 3]).unwrap();
849        let features2 = randn(&[5, 3]).unwrap();
850        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 0.0, 2.0, 3.0, 3.0, 4.0];
851
852        let edge_index = from_vec(edges, &[2, 5], DeviceType::Cpu).unwrap();
853        let graph1 = GraphData::new(features1, edge_index.clone());
854        let graph2 = GraphData::new(features2, edge_index);
855
856        let kernel = GraphKernel::new(GraphKernelType::Graphlet);
857        let similarity = kernel
858            .compute(&graph1, &graph2)
859            .expect("operation should succeed");
860
861        assert!(similarity >= 0.0);
862    }
863}