Skip to main content

scirs2_graph/algorithms/
motifs.rs

1//! Graph motif finding and subgraph analysis algorithms
2//!
3//! This module contains algorithms for finding small recurring subgraph patterns (motifs),
4//! subgraph isomorphism, graph kernels, and graphlet analysis.
5//!
6//! # Algorithms
7//! - **Motif counting**: Enumeration of 3-node and 4-node motifs
8//! - **VF2 subgraph isomorphism**: State-space search for subgraph matching
9//! - **Weisfeiler-Lehman subtree kernel**: Graph similarity via label refinement
10//! - **Graphlet degree distribution**: Node-level graphlet statistics
11//! - **Frequent subgraph mining**: gSpan-like approach for common patterns
12
13use crate::base::{EdgeWeight, Graph, IndexType, Node};
14use std::collections::{HashMap, HashSet};
15use std::hash::Hash;
16
17/// Motif types for enumeration
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum MotifType {
20    /// Triangle (3-cycle)
21    Triangle,
22    /// Square (4-cycle)
23    Square,
24    /// Star with 3 leaves
25    Star3,
26    /// Clique of size 4
27    Clique4,
28    /// Path of length 3 (4 nodes)
29    Path3,
30    /// Bi-fan motif (2 nodes connected to 2 other nodes)
31    BiFan,
32    /// Feed-forward loop
33    FeedForwardLoop,
34    /// Bi-directional motif
35    BiDirectional,
36}
37
38/// Result of VF2 subgraph isomorphism
39#[derive(Debug, Clone)]
40pub struct VF2Result<N: Node> {
41    /// Whether a match was found
42    pub is_match: bool,
43    /// All found mappings from pattern to target
44    pub mappings: Vec<HashMap<N, N>>,
45    /// Number of states explored
46    pub states_explored: usize,
47}
48
49/// Result of Weisfeiler-Lehman subtree kernel computation
50#[derive(Debug, Clone)]
51pub struct WLKernelResult {
52    /// Kernel matrix (similarity between pairs of graphs)
53    pub kernel_matrix: Vec<Vec<f64>>,
54    /// Feature vectors for each graph
55    pub feature_vectors: Vec<HashMap<String, usize>>,
56    /// Number of iterations performed
57    pub iterations: usize,
58}
59
60/// Graphlet degree distribution for a node
61#[derive(Debug, Clone)]
62pub struct GraphletDegreeVector {
63    /// Number of graphlet orbits the node participates in
64    pub orbit_counts: Vec<usize>,
65}
66
67/// Result of graphlet degree distribution analysis
68#[derive(Debug, Clone)]
69pub struct GraphletDDResult<N: Node> {
70    /// GDV for each node
71    pub node_gdvs: HashMap<N, GraphletDegreeVector>,
72    /// Agreement between GDVs (similarity metric)
73    pub gdv_agreement: f64,
74}
75
76/// Frequent subgraph pattern
77#[derive(Debug, Clone)]
78pub struct FrequentPattern<N: Node> {
79    /// Nodes in the pattern
80    pub nodes: Vec<N>,
81    /// Edges in the pattern (as index pairs into nodes)
82    pub edges: Vec<(usize, usize)>,
83    /// Support count (number of occurrences)
84    pub support: usize,
85}
86
87// ============================================================================
88// Motif finding
89// ============================================================================
90
91/// Find all occurrences of a specified motif in the graph.
92pub fn find_motifs<N, E, Ix>(graph: &Graph<N, E, Ix>, motiftype: MotifType) -> Vec<Vec<N>>
93where
94    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
95    E: EdgeWeight + Send + Sync,
96    Ix: IndexType + Send + Sync,
97{
98    match motiftype {
99        MotifType::Triangle => find_triangles(graph),
100        MotifType::Square => find_squares(graph),
101        MotifType::Star3 => find_star3s(graph),
102        MotifType::Clique4 => find_clique4s(graph),
103        MotifType::Path3 => find_path3s(graph),
104        MotifType::BiFan => find_bi_fans(graph),
105        MotifType::FeedForwardLoop => find_feed_forward_loops(graph),
106        MotifType::BiDirectional => find_bidirectional_motifs(graph),
107    }
108}
109
110/// Count all 3-node and 4-node motif types in the graph.
111pub fn count_motif_frequencies<N, E, Ix>(graph: &Graph<N, E, Ix>) -> HashMap<MotifType, usize>
112where
113    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
114    E: EdgeWeight + Send + Sync,
115    Ix: IndexType + Send + Sync,
116{
117    use scirs2_core::parallel_ops::*;
118
119    let motif_types = vec![
120        MotifType::Triangle,
121        MotifType::Square,
122        MotifType::Star3,
123        MotifType::Clique4,
124        MotifType::Path3,
125        MotifType::BiFan,
126        MotifType::FeedForwardLoop,
127        MotifType::BiDirectional,
128    ];
129
130    motif_types
131        .par_iter()
132        .map(|motif_type| {
133            let count = find_motifs(graph, *motif_type).len();
134            (*motif_type, count)
135        })
136        .collect()
137}
138
139/// Count 3-node motifs efficiently: open triads vs closed triads (triangles).
140pub fn count_3node_motifs<N, E, Ix>(graph: &Graph<N, E, Ix>) -> (usize, usize)
141where
142    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
143    E: EdgeWeight + Send + Sync,
144    Ix: IndexType + Send + Sync,
145{
146    let triangles = find_triangles(graph).len();
147    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
148
149    // Count open triads (paths of length 2)
150    let mut open_triads = 0usize;
151    for node in &nodes {
152        let deg = graph.degree(node);
153        if deg >= 2 {
154            // Number of pairs of neighbors: C(deg, 2)
155            open_triads += deg * (deg - 1) / 2;
156        }
157    }
158    // Each triangle contributes 3 "closed" triads, so open = total - 3*triangles
159    let open = open_triads.saturating_sub(3 * triangles);
160
161    (triangles, open)
162}
163
164/// Count 4-node motifs: paths, stars, squares, cliques.
165pub fn count_4node_motifs<N, E, Ix>(graph: &Graph<N, E, Ix>) -> HashMap<&'static str, usize>
166where
167    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
168    E: EdgeWeight + Send + Sync,
169    Ix: IndexType + Send + Sync,
170{
171    let mut counts = HashMap::new();
172    counts.insert("path3", find_path3s(graph).len());
173    counts.insert("star3", find_star3s(graph).len());
174    counts.insert("square", find_squares(graph).len());
175    counts.insert("clique4", find_clique4s(graph).len());
176    counts
177}
178
179/// Efficient motif detection using sampling for large graphs.
180pub fn sample_motif_frequencies<N, E, Ix>(
181    graph: &Graph<N, E, Ix>,
182    sample_size: usize,
183    rng: &mut impl scirs2_core::random::Rng,
184) -> HashMap<MotifType, f64>
185where
186    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
187    E: EdgeWeight + Send + Sync,
188    Ix: IndexType + Send + Sync,
189{
190    use scirs2_core::random::seq::SliceRandom;
191
192    let all_nodes: Vec<_> = graph.nodes().into_iter().cloned().collect();
193    if all_nodes.len() <= sample_size {
194        return count_motif_frequencies(graph)
195            .into_iter()
196            .map(|(k, v)| (k, v as f64))
197            .collect();
198    }
199
200    let mut sampled_nodes = all_nodes.clone();
201    sampled_nodes.shuffle(rng);
202    sampled_nodes.truncate(sample_size);
203
204    let mut subgraph = crate::generators::create_graph::<N, E>();
205    for node in &sampled_nodes {
206        let _ = subgraph.add_node(node.clone());
207    }
208
209    for node1 in &sampled_nodes {
210        if let Ok(neighbors) = graph.neighbors(node1) {
211            for node2 in neighbors {
212                if sampled_nodes.contains(&node2) && node1 != &node2 {
213                    if let Ok(weight) = graph.edge_weight(node1, &node2) {
214                        let _ = subgraph.add_edge(node1.clone(), node2, weight);
215                    }
216                }
217            }
218        }
219    }
220
221    let subgraph_counts = count_motif_frequencies(&subgraph);
222    let scaling_factor = (all_nodes.len() as f64) / (sample_size as f64);
223
224    subgraph_counts
225        .into_iter()
226        .map(|(motif_type, count)| (motif_type, count as f64 * scaling_factor))
227        .collect()
228}
229
230// ============================================================================
231// VF2 Subgraph Isomorphism
232// ============================================================================
233
234/// VF2 subgraph isomorphism algorithm.
235///
236/// Determines if `pattern` is isomorphic to a subgraph of `target`.
237/// Uses the VF2 state-space representation for efficient pruning.
238///
239/// # Arguments
240/// * `pattern` - The pattern graph to search for
241/// * `target` - The target graph to search in
242/// * `max_matches` - Maximum number of matches to find (0 = all)
243pub fn vf2_subgraph_isomorphism<N, E, Ix>(
244    pattern: &Graph<N, E, Ix>,
245    target: &Graph<N, E, Ix>,
246    max_matches: usize,
247) -> VF2Result<N>
248where
249    N: Node + Clone + Hash + Eq + std::fmt::Debug,
250    E: EdgeWeight,
251    Ix: IndexType,
252{
253    let p_nodes: Vec<N> = pattern.nodes().into_iter().cloned().collect();
254    let t_nodes: Vec<N> = target.nodes().into_iter().cloned().collect();
255
256    if p_nodes.is_empty() {
257        return VF2Result {
258            is_match: true,
259            mappings: vec![HashMap::new()],
260            states_explored: 0,
261        };
262    }
263
264    if p_nodes.len() > t_nodes.len() {
265        return VF2Result {
266            is_match: false,
267            mappings: vec![],
268            states_explored: 0,
269        };
270    }
271
272    // Build adjacency sets for fast lookup
273    let p_adj = build_adj_set(pattern, &p_nodes);
274    let t_adj = build_adj_set(target, &t_nodes);
275
276    let p_idx: HashMap<N, usize> = p_nodes
277        .iter()
278        .enumerate()
279        .map(|(i, n)| (n.clone(), i))
280        .collect();
281    let t_idx: HashMap<N, usize> = t_nodes
282        .iter()
283        .enumerate()
284        .map(|(i, n)| (n.clone(), i))
285        .collect();
286
287    let mut state = VF2State {
288        core_p: vec![None; p_nodes.len()],
289        core_t: vec![None; t_nodes.len()],
290        in_p: vec![0usize; p_nodes.len()],
291        out_p: vec![0usize; p_nodes.len()],
292        in_t: vec![0usize; t_nodes.len()],
293        out_t: vec![0usize; t_nodes.len()],
294        depth: 0,
295    };
296
297    let mut mappings = Vec::new();
298    let mut states_explored = 0;
299    let limit = if max_matches == 0 {
300        usize::MAX
301    } else {
302        max_matches
303    };
304
305    vf2_match(
306        &p_nodes,
307        &t_nodes,
308        &p_adj,
309        &t_adj,
310        &p_idx,
311        &t_idx,
312        &mut state,
313        &mut mappings,
314        &mut states_explored,
315        limit,
316    );
317
318    VF2Result {
319        is_match: !mappings.is_empty(),
320        mappings,
321        states_explored,
322    }
323}
324
325/// Internal VF2 state
326struct VF2State {
327    core_p: Vec<Option<usize>>, // pattern node -> target node
328    core_t: Vec<Option<usize>>, // target node -> pattern node
329    in_p: Vec<usize>,
330    out_p: Vec<usize>,
331    in_t: Vec<usize>,
332    out_t: Vec<usize>,
333    depth: usize,
334}
335
336fn build_adj_set<N, E, Ix>(graph: &Graph<N, E, Ix>, nodes: &[N]) -> Vec<HashSet<usize>>
337where
338    N: Node + Clone + Hash + Eq + std::fmt::Debug,
339    E: EdgeWeight,
340    Ix: IndexType,
341{
342    let idx_map: HashMap<N, usize> = nodes
343        .iter()
344        .enumerate()
345        .map(|(i, n)| (n.clone(), i))
346        .collect();
347    let mut adj = vec![HashSet::new(); nodes.len()];
348    for (i, node) in nodes.iter().enumerate() {
349        if let Ok(neighbors) = graph.neighbors(node) {
350            for neighbor in &neighbors {
351                if let Some(&j) = idx_map.get(neighbor) {
352                    adj[i].insert(j);
353                }
354            }
355        }
356    }
357    adj
358}
359
360fn vf2_match<N: Node + Clone + Hash + Eq + std::fmt::Debug>(
361    p_nodes: &[N],
362    t_nodes: &[N],
363    p_adj: &[HashSet<usize>],
364    t_adj: &[HashSet<usize>],
365    _p_idx: &HashMap<N, usize>,
366    _t_idx: &HashMap<N, usize>,
367    state: &mut VF2State,
368    mappings: &mut Vec<HashMap<N, N>>,
369    states_explored: &mut usize,
370    limit: usize,
371) {
372    *states_explored += 1;
373
374    if mappings.len() >= limit {
375        return;
376    }
377
378    if state.depth == p_nodes.len() {
379        // Complete matching found
380        let mut mapping = HashMap::new();
381        for (pi, ti_opt) in state.core_p.iter().enumerate() {
382            if let Some(ti) = ti_opt {
383                mapping.insert(p_nodes[pi].clone(), t_nodes[*ti].clone());
384            }
385        }
386        mappings.push(mapping);
387        return;
388    }
389
390    // Find candidate pairs
391    let candidates = vf2_candidates(state, p_nodes.len(), t_nodes.len());
392
393    for (p, t) in candidates {
394        if vf2_feasible(p, t, p_adj, t_adj, state, p_nodes.len(), t_nodes.len()) {
395            // Add pair to mapping
396            state.core_p[p] = Some(t);
397            state.core_t[t] = Some(p);
398            let old_depth = state.depth;
399            state.depth += 1;
400
401            // Update in/out sets
402            let mut in_p_changes = Vec::new();
403            let mut out_p_changes = Vec::new();
404            let mut in_t_changes = Vec::new();
405            let mut out_t_changes = Vec::new();
406
407            for &neighbor in &p_adj[p] {
408                if state.in_p[neighbor] == 0 && state.core_p[neighbor].is_none() {
409                    state.in_p[neighbor] = state.depth;
410                    in_p_changes.push(neighbor);
411                }
412                if state.out_p[neighbor] == 0 && state.core_p[neighbor].is_none() {
413                    state.out_p[neighbor] = state.depth;
414                    out_p_changes.push(neighbor);
415                }
416            }
417            for &neighbor in &t_adj[t] {
418                if state.in_t[neighbor] == 0 && state.core_t[neighbor].is_none() {
419                    state.in_t[neighbor] = state.depth;
420                    in_t_changes.push(neighbor);
421                }
422                if state.out_t[neighbor] == 0 && state.core_t[neighbor].is_none() {
423                    state.out_t[neighbor] = state.depth;
424                    out_t_changes.push(neighbor);
425                }
426            }
427
428            vf2_match(
429                p_nodes,
430                t_nodes,
431                p_adj,
432                t_adj,
433                _p_idx,
434                _t_idx,
435                state,
436                mappings,
437                states_explored,
438                limit,
439            );
440
441            // Restore state
442            state.core_p[p] = None;
443            state.core_t[t] = None;
444            state.depth = old_depth;
445
446            for idx in in_p_changes {
447                state.in_p[idx] = 0;
448            }
449            for idx in out_p_changes {
450                state.out_p[idx] = 0;
451            }
452            for idx in in_t_changes {
453                state.in_t[idx] = 0;
454            }
455            for idx in out_t_changes {
456                state.out_t[idx] = 0;
457            }
458
459            if mappings.len() >= limit {
460                return;
461            }
462        }
463    }
464}
465
466fn vf2_candidates(state: &VF2State, n_p: usize, n_t: usize) -> Vec<(usize, usize)> {
467    // Find the first unmapped pattern node
468    let p_node = (0..n_p).find(|&i| state.core_p[i].is_none());
469
470    if let Some(p) = p_node {
471        // Try all unmapped target nodes
472        let candidates: Vec<(usize, usize)> = (0..n_t)
473            .filter(|&t| state.core_t[t].is_none())
474            .map(|t| (p, t))
475            .collect();
476        candidates
477    } else {
478        vec![]
479    }
480}
481
482fn vf2_feasible(
483    p: usize,
484    t: usize,
485    p_adj: &[HashSet<usize>],
486    t_adj: &[HashSet<usize>],
487    state: &VF2State,
488    _n_p: usize,
489    _n_t: usize,
490) -> bool {
491    // Check: for every neighbor of p that is mapped, t must be a neighbor of the mapping
492    for &p_neighbor in &p_adj[p] {
493        if let Some(t_mapped) = state.core_p[p_neighbor] {
494            if !t_adj[t].contains(&t_mapped) {
495                return false;
496            }
497        }
498    }
499
500    // Check: for every neighbor of t that is mapped, the reverse must hold
501    for &t_neighbor in &t_adj[t] {
502        if let Some(p_mapped) = state.core_t[t_neighbor] {
503            if !p_adj[p].contains(&p_mapped) {
504                return false;
505            }
506        }
507    }
508
509    // Lookahead: degree compatibility
510    // The number of unmapped neighbors of p in pattern must not exceed
511    // the number of unmapped neighbors of t in target
512    let p_unmapped_neighbors = p_adj[p]
513        .iter()
514        .filter(|&&n| state.core_p[n].is_none())
515        .count();
516    let t_unmapped_neighbors = t_adj[t]
517        .iter()
518        .filter(|&&n| state.core_t[n].is_none())
519        .count();
520
521    p_unmapped_neighbors <= t_unmapped_neighbors
522}
523
524// ============================================================================
525// Weisfeiler-Lehman Subtree Kernel
526// ============================================================================
527
528/// Compute the Weisfeiler-Lehman subtree kernel between a collection of graphs.
529///
530/// The WL kernel iteratively refines node labels by aggregating neighbor labels.
531/// After `h` iterations, it computes a feature vector of label histograms
532/// and returns the dot product as the kernel value.
533///
534/// # Arguments
535/// * `graphs` - Collection of graphs to compare
536/// * `iterations` - Number of WL iterations (depth of subtree consideration)
537pub fn wl_subtree_kernel<N, E, Ix>(graphs: &[&Graph<N, E, Ix>], iterations: usize) -> WLKernelResult
538where
539    N: Node + Clone + Hash + Eq + std::fmt::Debug,
540    E: EdgeWeight,
541    Ix: IndexType,
542{
543    let num_graphs = graphs.len();
544
545    if num_graphs == 0 {
546        return WLKernelResult {
547            kernel_matrix: vec![],
548            feature_vectors: vec![],
549            iterations: 0,
550        };
551    }
552
553    // Initialize labels: use degree as initial label
554    let mut graph_labels: Vec<HashMap<N, String>> = graphs
555        .iter()
556        .map(|g| {
557            let mut labels = HashMap::new();
558            for node in g.nodes() {
559                labels.insert(node.clone(), format!("{}", g.degree(node)));
560            }
561            labels
562        })
563        .collect();
564
565    let mut all_features: Vec<HashMap<String, usize>> = vec![HashMap::new(); num_graphs];
566
567    // Collect initial labels as features
568    for (gi, labels) in graph_labels.iter().enumerate() {
569        for label in labels.values() {
570            *all_features[gi].entry(label.clone()).or_insert(0) += 1;
571        }
572    }
573
574    // WL iterations
575    for _iter in 0..iterations {
576        let mut new_graph_labels: Vec<HashMap<N, String>> = Vec::with_capacity(num_graphs);
577
578        for (gi, graph) in graphs.iter().enumerate() {
579            let mut new_labels = HashMap::new();
580
581            for node in graph.nodes() {
582                let own_label = graph_labels[gi].get(node).cloned().unwrap_or_default();
583
584                // Collect sorted neighbor labels
585                let mut neighbor_labels: Vec<String> = Vec::new();
586                if let Ok(neighbors) = graph.neighbors(node) {
587                    for neighbor in &neighbors {
588                        if let Some(label) = graph_labels[gi].get(neighbor) {
589                            neighbor_labels.push(label.clone());
590                        }
591                    }
592                }
593                neighbor_labels.sort();
594
595                // New label = hash of (own_label, sorted neighbor labels)
596                let new_label = format!("{own_label}|{}", neighbor_labels.join(","));
597                new_labels.insert(node.clone(), new_label.clone());
598
599                *all_features[gi].entry(new_label).or_insert(0) += 1;
600            }
601
602            new_graph_labels.push(new_labels);
603        }
604
605        graph_labels = new_graph_labels;
606    }
607
608    // Compute kernel matrix from feature vectors
609    let mut kernel_matrix = vec![vec![0.0f64; num_graphs]; num_graphs];
610
611    for i in 0..num_graphs {
612        for j in i..num_graphs {
613            // Dot product of feature vectors
614            let mut dot = 0.0;
615            for (key, &count_i) in &all_features[i] {
616                if let Some(&count_j) = all_features[j].get(key) {
617                    dot += (count_i as f64) * (count_j as f64);
618                }
619            }
620            kernel_matrix[i][j] = dot;
621            kernel_matrix[j][i] = dot;
622        }
623    }
624
625    WLKernelResult {
626        kernel_matrix,
627        feature_vectors: all_features,
628        iterations,
629    }
630}
631
632// ============================================================================
633// Graphlet Degree Distribution
634// ============================================================================
635
636/// Compute graphlet degree vectors for all nodes.
637///
638/// For each node, counts how many times it participates in each
639/// graphlet orbit. Currently supports up to 4-node graphlets:
640/// - Orbit 0: Edge (degree)
641/// - Orbit 1: Path of length 2 (center)
642/// - Orbit 2: Path of length 2 (endpoint)
643/// - Orbit 3: Triangle
644/// - Orbit 4: Path of length 3 (endpoint)
645/// - Orbit 5: Path of length 3 (internal)
646/// - Orbit 6: Star with 3 leaves (center)
647/// - Orbit 7: Star with 3 leaves (leaf)
648/// - Orbit 8: 4-cycle
649/// - Orbit 9: 4-clique
650pub fn graphlet_degree_distribution<N, E, Ix>(graph: &Graph<N, E, Ix>) -> GraphletDDResult<N>
651where
652    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
653    E: EdgeWeight + Send + Sync,
654    Ix: IndexType + Send + Sync,
655{
656    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
657    let n = nodes.len();
658    let num_orbits = 10;
659
660    let node_to_idx: HashMap<N, usize> = nodes
661        .iter()
662        .enumerate()
663        .map(|(i, n)| (n.clone(), i))
664        .collect();
665
666    // Build adjacency list as indices
667    let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
668    for (i, node) in nodes.iter().enumerate() {
669        if let Ok(neighbors) = graph.neighbors(node) {
670            for neighbor in &neighbors {
671                if let Some(&j) = node_to_idx.get(neighbor) {
672                    adj[i].push(j);
673                }
674            }
675        }
676    }
677
678    let mut orbits = vec![vec![0usize; num_orbits]; n];
679
680    // Orbit 0: Degree (number of edges)
681    for i in 0..n {
682        orbits[i][0] = adj[i].len();
683    }
684
685    // Orbit 1,2: Paths of length 2
686    for i in 0..n {
687        for &j in &adj[i] {
688            for &k in &adj[j] {
689                if k != i && !adj[i].contains(&k) {
690                    orbits[j][1] += 1; // center of path
691                    orbits[i][2] += 1; // endpoint of path
692                }
693            }
694        }
695    }
696    // Correct for double counting
697    for i in 0..n {
698        orbits[i][1] /= 2;
699    }
700
701    // Orbit 3: Triangles
702    for i in 0..n {
703        for (idx_j, &j) in adj[i].iter().enumerate() {
704            if j <= i {
705                continue;
706            }
707            for &k in adj[i].iter().skip(idx_j + 1) {
708                if k <= j {
709                    continue;
710                }
711                if adj[j].contains(&k) {
712                    orbits[i][3] += 1;
713                    orbits[j][3] += 1;
714                    orbits[k][3] += 1;
715                }
716            }
717        }
718    }
719
720    // Orbit 6,7: Star with 3 leaves
721    for i in 0..n {
722        let deg = adj[i].len();
723        if deg >= 3 {
724            // C(deg, 3) = number of 3-leaf stars centered at i
725            // (only count those where leaves are NOT connected to each other)
726            let mut star_count = 0;
727            for j_idx in 0..adj[i].len() {
728                for k_idx in (j_idx + 1)..adj[i].len() {
729                    for l_idx in (k_idx + 1)..adj[i].len() {
730                        let j = adj[i][j_idx];
731                        let k = adj[i][k_idx];
732                        let l = adj[i][l_idx];
733                        if !adj[j].contains(&k) && !adj[k].contains(&l) && !adj[j].contains(&l) {
734                            star_count += 1;
735                            orbits[j][7] += 1;
736                            orbits[k][7] += 1;
737                            orbits[l][7] += 1;
738                        }
739                    }
740                }
741            }
742            orbits[i][6] = star_count;
743        }
744    }
745
746    // Orbits 4,5: Path of length 3
747    for i in 0..n {
748        for &j in &adj[i] {
749            for &k in &adj[j] {
750                if k == i {
751                    continue;
752                }
753                for &l in &adj[k] {
754                    if l == j || l == i {
755                        continue;
756                    }
757                    // Path i-j-k-l
758                    if !adj[i].contains(&k) && !adj[i].contains(&l) && !adj[j].contains(&l) {
759                        orbits[i][4] += 1; // endpoint
760                        orbits[l][4] += 1; // endpoint
761                        orbits[j][5] += 1; // internal
762                        orbits[k][5] += 1; // internal
763                    }
764                }
765            }
766        }
767    }
768    // Correct for double-counting (each path counted from both endpoints)
769    for i in 0..n {
770        orbits[i][4] /= 2;
771        orbits[i][5] /= 2;
772    }
773
774    // Build result
775    let mut node_gdvs = HashMap::new();
776    for (i, node) in nodes.iter().enumerate() {
777        node_gdvs.insert(
778            node.clone(),
779            GraphletDegreeVector {
780                orbit_counts: orbits[i].clone(),
781            },
782        );
783    }
784
785    // Compute GDV agreement (average over all node pairs)
786    let mut total_agreement = 0.0;
787    let mut pair_count = 0;
788    for i in 0..n {
789        for j in (i + 1)..n {
790            let mut agree = 0.0;
791            for o in 0..num_orbits {
792                let a = orbits[i][o] as f64;
793                let b = orbits[j][o] as f64;
794                let max_val = a.max(b);
795                if max_val > 0.0 {
796                    agree += 1.0 - (a - b).abs() / max_val;
797                } else {
798                    agree += 1.0;
799                }
800            }
801            total_agreement += agree / num_orbits as f64;
802            pair_count += 1;
803        }
804    }
805
806    let gdv_agreement = if pair_count > 0 {
807        total_agreement / pair_count as f64
808    } else {
809        1.0
810    };
811
812    GraphletDDResult {
813        node_gdvs,
814        gdv_agreement,
815    }
816}
817
818// ============================================================================
819// Frequent Subgraph Mining (simplified gSpan-like)
820// ============================================================================
821
822/// Find frequent subgraph patterns via edge enumeration.
823///
824/// A simplified version of gSpan: enumerates small connected subgraphs
825/// and counts their support. Returns patterns with support >= min_support.
826///
827/// # Arguments
828/// * `graph` - The graph to mine
829/// * `min_support` - Minimum number of occurrences for a pattern to be frequent
830/// * `max_size` - Maximum number of nodes in a pattern
831pub fn frequent_subgraph_mining<N, E, Ix>(
832    graph: &Graph<N, E, Ix>,
833    min_support: usize,
834    max_size: usize,
835) -> Vec<FrequentPattern<N>>
836where
837    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
838    E: EdgeWeight + Send + Sync,
839    Ix: IndexType + Send + Sync,
840{
841    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
842    let n = nodes.len();
843
844    if n == 0 || max_size == 0 {
845        return vec![];
846    }
847
848    let node_to_idx: HashMap<N, usize> = nodes
849        .iter()
850        .enumerate()
851        .map(|(i, n)| (n.clone(), i))
852        .collect();
853
854    let mut adj: Vec<HashSet<usize>> = vec![HashSet::new(); n];
855    for (i, node) in nodes.iter().enumerate() {
856        if let Ok(neighbors) = graph.neighbors(node) {
857            for neighbor in &neighbors {
858                if let Some(&j) = node_to_idx.get(neighbor) {
859                    adj[i].insert(j);
860                }
861            }
862        }
863    }
864
865    let mut patterns: Vec<FrequentPattern<N>> = Vec::new();
866
867    // Level 1: Single edges
868    let mut edge_canonical: HashMap<(usize, usize), usize> = HashMap::new();
869    for i in 0..n {
870        for &j in &adj[i] {
871            if i < j {
872                let deg_i = adj[i].len();
873                let deg_j = adj[j].len();
874                let canonical = if deg_i <= deg_j {
875                    (deg_i, deg_j)
876                } else {
877                    (deg_j, deg_i)
878                };
879                *edge_canonical.entry(canonical).or_insert(0) += 1;
880            }
881        }
882    }
883
884    for (&(deg_a, deg_b), &count) in &edge_canonical {
885        if count >= min_support {
886            // Find one representative instance
887            for i in 0..n {
888                for &j in &adj[i] {
889                    if i < j {
890                        let di = adj[i].len();
891                        let dj = adj[j].len();
892                        let c = if di <= dj { (di, dj) } else { (dj, di) };
893                        if c == (deg_a, deg_b) {
894                            patterns.push(FrequentPattern {
895                                nodes: vec![nodes[i].clone(), nodes[j].clone()],
896                                edges: vec![(0, 1)],
897                                support: count,
898                            });
899                            break;
900                        }
901                    }
902                }
903                if !patterns.is_empty()
904                    && patterns.last().map(|p| p.support == count).unwrap_or(false)
905                {
906                    break;
907                }
908            }
909        }
910    }
911
912    // Level 2: Triangles (3-node connected subgraphs with 3 edges)
913    if max_size >= 3 {
914        let triangles = find_triangles(graph);
915        if triangles.len() >= min_support {
916            if let Some(tri) = triangles.first() {
917                patterns.push(FrequentPattern {
918                    nodes: tri.clone(),
919                    edges: vec![(0, 1), (1, 2), (0, 2)],
920                    support: triangles.len(),
921                });
922            }
923        }
924
925        // Paths of length 2 (3 nodes, 2 edges)
926        let mut path2_count = 0;
927        let mut path2_example: Option<Vec<N>> = None;
928        for i in 0..n {
929            for &j in &adj[i] {
930                for &k in &adj[j] {
931                    if k > i && !adj[i].contains(&k) {
932                        path2_count += 1;
933                        if path2_example.is_none() {
934                            path2_example =
935                                Some(vec![nodes[i].clone(), nodes[j].clone(), nodes[k].clone()]);
936                        }
937                    }
938                }
939            }
940        }
941        path2_count /= 2; // Each path counted twice
942
943        if path2_count >= min_support {
944            if let Some(example) = path2_example {
945                patterns.push(FrequentPattern {
946                    nodes: example,
947                    edges: vec![(0, 1), (1, 2)],
948                    support: path2_count,
949                });
950            }
951        }
952    }
953
954    // Level 3: 4-node patterns
955    if max_size >= 4 {
956        let squares = find_squares(graph);
957        if squares.len() >= min_support {
958            if let Some(sq) = squares.first() {
959                patterns.push(FrequentPattern {
960                    nodes: sq.clone(),
961                    edges: vec![(0, 1), (1, 2), (2, 3), (3, 0)],
962                    support: squares.len(),
963                });
964            }
965        }
966    }
967
968    // Sort by support descending
969    patterns.sort_by_key(|p| std::cmp::Reverse(p.support));
970    patterns
971}
972
973// ============================================================================
974// Internal motif finding functions
975// ============================================================================
976
977fn find_triangles<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Vec<Vec<N>>
978where
979    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
980    E: EdgeWeight + Send + Sync,
981    Ix: IndexType + Send + Sync,
982{
983    use scirs2_core::parallel_ops::*;
984    use std::sync::Mutex;
985
986    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
987    let triangles = Mutex::new(Vec::new());
988
989    nodes.par_iter().enumerate().for_each(|(_i, node_i)| {
990        if let Ok(neighbors_i) = graph.neighbors(node_i) {
991            let neighbors_i: Vec<_> = neighbors_i;
992
993            for (j, node_j) in neighbors_i.iter().enumerate() {
994                for node_k in neighbors_i.iter().skip(j + 1) {
995                    if graph.has_edge(node_j, node_k) {
996                        if let Ok(mut triangles_guard) = triangles.lock() {
997                            let mut triangle = vec![node_i.clone(), node_j.clone(), node_k.clone()];
998                            triangle.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
999
1000                            if !triangles_guard.iter().any(|t| t == &triangle) {
1001                                triangles_guard.push(triangle);
1002                            }
1003                        }
1004                    }
1005                }
1006            }
1007        }
1008    });
1009
1010    triangles.into_inner().unwrap_or_default()
1011}
1012
1013fn find_squares<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Vec<Vec<N>>
1014where
1015    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
1016    E: EdgeWeight + Send + Sync,
1017    Ix: IndexType + Send + Sync,
1018{
1019    let mut squares = Vec::new();
1020    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1021
1022    for i in 0..nodes.len() {
1023        for j in i + 1..nodes.len() {
1024            if !graph.has_edge(&nodes[i], &nodes[j]) {
1025                continue;
1026            }
1027            for k in j + 1..nodes.len() {
1028                if !graph.has_edge(&nodes[j], &nodes[k]) {
1029                    continue;
1030                }
1031                for l in k + 1..nodes.len() {
1032                    if graph.has_edge(&nodes[k], &nodes[l])
1033                        && graph.has_edge(&nodes[l], &nodes[i])
1034                        && !graph.has_edge(&nodes[i], &nodes[k])
1035                        && !graph.has_edge(&nodes[j], &nodes[l])
1036                    {
1037                        squares.push(vec![
1038                            nodes[i].clone(),
1039                            nodes[j].clone(),
1040                            nodes[k].clone(),
1041                            nodes[l].clone(),
1042                        ]);
1043                    }
1044                }
1045            }
1046        }
1047    }
1048
1049    squares
1050}
1051
1052fn find_star3s<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Vec<Vec<N>>
1053where
1054    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
1055    E: EdgeWeight + Send + Sync,
1056    Ix: IndexType + Send + Sync,
1057{
1058    let mut stars = Vec::new();
1059    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1060
1061    for center in &nodes {
1062        if let Ok(neighbors) = graph.neighbors(center) {
1063            let neighbor_list: Vec<N> = neighbors;
1064
1065            if neighbor_list.len() >= 3 {
1066                for i in 0..neighbor_list.len() {
1067                    for j in i + 1..neighbor_list.len() {
1068                        for k in j + 1..neighbor_list.len() {
1069                            if !graph.has_edge(&neighbor_list[i], &neighbor_list[j])
1070                                && !graph.has_edge(&neighbor_list[j], &neighbor_list[k])
1071                                && !graph.has_edge(&neighbor_list[i], &neighbor_list[k])
1072                            {
1073                                stars.push(vec![
1074                                    center.clone(),
1075                                    neighbor_list[i].clone(),
1076                                    neighbor_list[j].clone(),
1077                                    neighbor_list[k].clone(),
1078                                ]);
1079                            }
1080                        }
1081                    }
1082                }
1083            }
1084        }
1085    }
1086
1087    stars
1088}
1089
1090fn find_clique4s<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Vec<Vec<N>>
1091where
1092    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
1093    E: EdgeWeight + Send + Sync,
1094    Ix: IndexType + Send + Sync,
1095{
1096    let mut cliques = Vec::new();
1097    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1098
1099    for i in 0..nodes.len() {
1100        for j in i + 1..nodes.len() {
1101            if !graph.has_edge(&nodes[i], &nodes[j]) {
1102                continue;
1103            }
1104            for k in j + 1..nodes.len() {
1105                if !graph.has_edge(&nodes[i], &nodes[k]) || !graph.has_edge(&nodes[j], &nodes[k]) {
1106                    continue;
1107                }
1108                for l in k + 1..nodes.len() {
1109                    if graph.has_edge(&nodes[i], &nodes[l])
1110                        && graph.has_edge(&nodes[j], &nodes[l])
1111                        && graph.has_edge(&nodes[k], &nodes[l])
1112                    {
1113                        cliques.push(vec![
1114                            nodes[i].clone(),
1115                            nodes[j].clone(),
1116                            nodes[k].clone(),
1117                            nodes[l].clone(),
1118                        ]);
1119                    }
1120                }
1121            }
1122        }
1123    }
1124
1125    cliques
1126}
1127
1128fn find_path3s<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Vec<Vec<N>>
1129where
1130    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
1131    E: EdgeWeight + Send + Sync,
1132    Ix: IndexType + Send + Sync,
1133{
1134    use scirs2_core::parallel_ops::*;
1135    use std::sync::Mutex;
1136
1137    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1138    let paths = Mutex::new(Vec::new());
1139
1140    nodes.par_iter().for_each(|start_node| {
1141        if let Ok(neighbors1) = graph.neighbors(start_node) {
1142            for middle1 in neighbors1 {
1143                if let Ok(neighbors2) = graph.neighbors(&middle1) {
1144                    for middle2 in neighbors2 {
1145                        if middle2 == *start_node {
1146                            continue;
1147                        }
1148
1149                        if let Ok(neighbors3) = graph.neighbors(&middle2) {
1150                            for end_node in neighbors3 {
1151                                if end_node == middle1 || end_node == *start_node {
1152                                    continue;
1153                                }
1154
1155                                if !graph.has_edge(start_node, &middle2)
1156                                    && !graph.has_edge(start_node, &end_node)
1157                                    && !graph.has_edge(&middle1, &end_node)
1158                                {
1159                                    let mut path = vec![
1160                                        start_node.clone(),
1161                                        middle1.clone(),
1162                                        middle2.clone(),
1163                                        end_node.clone(),
1164                                    ];
1165                                    path.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
1166
1167                                    if let Ok(mut paths_guard) = paths.lock() {
1168                                        if !paths_guard.iter().any(|p| p == &path) {
1169                                            paths_guard.push(path);
1170                                        }
1171                                    }
1172                                }
1173                            }
1174                        }
1175                    }
1176                }
1177            }
1178        }
1179    });
1180
1181    paths.into_inner().unwrap_or_default()
1182}
1183
1184fn find_bi_fans<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Vec<Vec<N>>
1185where
1186    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
1187    E: EdgeWeight + Send + Sync,
1188    Ix: IndexType + Send + Sync,
1189{
1190    use scirs2_core::parallel_ops::*;
1191    use std::sync::Mutex;
1192
1193    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1194    let bi_fans = Mutex::new(Vec::new());
1195
1196    nodes.par_iter().enumerate().for_each(|(i, node1)| {
1197        for node2 in nodes.iter().skip(i + 1) {
1198            if let (Ok(neighbors1), Ok(neighbors2)) =
1199                (graph.neighbors(node1), graph.neighbors(node2))
1200            {
1201                let neighbors1: HashSet<_> = neighbors1.into_iter().collect();
1202                let neighbors2: HashSet<_> = neighbors2.into_iter().collect();
1203
1204                let common: Vec<_> = neighbors1
1205                    .intersection(&neighbors2)
1206                    .filter(|&n| n != node1 && n != node2)
1207                    .cloned()
1208                    .collect();
1209
1210                if common.len() >= 2 {
1211                    for (j, fan1) in common.iter().enumerate() {
1212                        for fan2 in common.iter().skip(j + 1) {
1213                            let mut bi_fan =
1214                                vec![node1.clone(), node2.clone(), fan1.clone(), fan2.clone()];
1215                            bi_fan.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
1216
1217                            if let Ok(mut bi_fans_guard) = bi_fans.lock() {
1218                                if !bi_fans_guard.iter().any(|bf| bf == &bi_fan) {
1219                                    bi_fans_guard.push(bi_fan);
1220                                }
1221                            }
1222                        }
1223                    }
1224                }
1225            }
1226        }
1227    });
1228
1229    bi_fans.into_inner().unwrap_or_default()
1230}
1231
1232fn find_feed_forward_loops<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Vec<Vec<N>>
1233where
1234    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
1235    E: EdgeWeight + Send + Sync,
1236    Ix: IndexType + Send + Sync,
1237{
1238    use scirs2_core::parallel_ops::*;
1239    use std::sync::Mutex;
1240
1241    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1242    let ffls = Mutex::new(Vec::new());
1243
1244    nodes.par_iter().for_each(|node_a| {
1245        if let Ok(out_neighbors_a) = graph.neighbors(node_a) {
1246            let out_neighbors_a: Vec<_> = out_neighbors_a;
1247
1248            for (i, node_b) in out_neighbors_a.iter().enumerate() {
1249                for node_c in out_neighbors_a.iter().skip(i + 1) {
1250                    if graph.has_edge(node_b, node_c)
1251                        && !graph.has_edge(node_b, node_a)
1252                        && !graph.has_edge(node_c, node_a)
1253                        && !graph.has_edge(node_c, node_b)
1254                    {
1255                        let mut ffl = vec![node_a.clone(), node_b.clone(), node_c.clone()];
1256                        ffl.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
1257
1258                        if let Ok(mut ffls_guard) = ffls.lock() {
1259                            if !ffls_guard.iter().any(|f| f == &ffl) {
1260                                ffls_guard.push(ffl);
1261                            }
1262                        }
1263                    }
1264                }
1265            }
1266        }
1267    });
1268
1269    ffls.into_inner().unwrap_or_default()
1270}
1271
1272fn find_bidirectional_motifs<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Vec<Vec<N>>
1273where
1274    N: Node + Clone + Hash + Eq + std::fmt::Debug + Send + Sync,
1275    E: EdgeWeight + Send + Sync,
1276    Ix: IndexType + Send + Sync,
1277{
1278    use scirs2_core::parallel_ops::*;
1279    use std::sync::Mutex;
1280
1281    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
1282    let bidirectionals = Mutex::new(Vec::new());
1283
1284    nodes.par_iter().enumerate().for_each(|(i, node1)| {
1285        for node2 in nodes.iter().skip(i + 1) {
1286            if graph.has_edge(node1, node2) && graph.has_edge(node2, node1) {
1287                let mut motif = vec![node1.clone(), node2.clone()];
1288                motif.sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
1289
1290                if let Ok(mut bidirectionals_guard) = bidirectionals.lock() {
1291                    if !bidirectionals_guard.iter().any(|m| m == &motif) {
1292                        bidirectionals_guard.push(motif);
1293                    }
1294                }
1295            }
1296        }
1297    });
1298
1299    bidirectionals.into_inner().unwrap_or_default()
1300}
1301
1302// ============================================================================
1303// Tests
1304// ============================================================================
1305
1306#[cfg(test)]
1307mod tests {
1308    use super::*;
1309    use crate::error::Result as GraphResult;
1310    use crate::generators::create_graph;
1311
1312    #[test]
1313    fn test_find_triangles() -> GraphResult<()> {
1314        let mut graph = create_graph::<&str, ()>();
1315        graph.add_edge("A", "B", ())?;
1316        graph.add_edge("B", "C", ())?;
1317        graph.add_edge("C", "A", ())?;
1318        graph.add_edge("A", "D", ())?;
1319
1320        let triangles = find_motifs(&graph, MotifType::Triangle);
1321        assert_eq!(triangles.len(), 1);
1322        let triangle = &triangles[0];
1323        assert_eq!(triangle.len(), 3);
1324        assert!(triangle.contains(&"A"));
1325        assert!(triangle.contains(&"B"));
1326        assert!(triangle.contains(&"C"));
1327        Ok(())
1328    }
1329
1330    #[test]
1331    fn test_find_squares() -> GraphResult<()> {
1332        let mut graph = create_graph::<&str, ()>();
1333        graph.add_edge("A", "B", ())?;
1334        graph.add_edge("B", "C", ())?;
1335        graph.add_edge("C", "D", ())?;
1336        graph.add_edge("D", "A", ())?;
1337
1338        let squares = find_motifs(&graph, MotifType::Square);
1339        assert_eq!(squares.len(), 1);
1340        assert_eq!(squares[0].len(), 4);
1341        Ok(())
1342    }
1343
1344    #[test]
1345    fn test_find_star3() -> GraphResult<()> {
1346        let mut graph = create_graph::<&str, ()>();
1347        graph.add_edge("A", "B", ())?;
1348        graph.add_edge("A", "C", ())?;
1349        graph.add_edge("A", "D", ())?;
1350
1351        let stars = find_motifs(&graph, MotifType::Star3);
1352        assert_eq!(stars.len(), 1);
1353        assert_eq!(stars[0].len(), 4);
1354        assert!(stars[0].contains(&"A"));
1355        Ok(())
1356    }
1357
1358    #[test]
1359    fn test_find_clique4() -> GraphResult<()> {
1360        let mut graph = create_graph::<&str, ()>();
1361        let nodes = ["A", "B", "C", "D"];
1362        for i in 0..nodes.len() {
1363            for j in i + 1..nodes.len() {
1364                graph.add_edge(nodes[i], nodes[j], ())?;
1365            }
1366        }
1367
1368        let cliques = find_motifs(&graph, MotifType::Clique4);
1369        assert_eq!(cliques.len(), 1);
1370        assert_eq!(cliques[0].len(), 4);
1371        Ok(())
1372    }
1373
1374    #[test]
1375    fn test_vf2_exact_match() -> GraphResult<()> {
1376        let mut pattern = create_graph::<i32, ()>();
1377        pattern.add_edge(0, 1, ())?;
1378        pattern.add_edge(1, 2, ())?;
1379
1380        let mut target = create_graph::<i32, ()>();
1381        target.add_edge(10, 11, ())?;
1382        target.add_edge(11, 12, ())?;
1383
1384        let result = vf2_subgraph_isomorphism(&pattern, &target, 0);
1385        assert!(result.is_match);
1386        assert!(!result.mappings.is_empty());
1387        Ok(())
1388    }
1389
1390    #[test]
1391    fn test_vf2_no_match() -> GraphResult<()> {
1392        let mut pattern = create_graph::<i32, ()>();
1393        pattern.add_edge(0, 1, ())?;
1394        pattern.add_edge(1, 2, ())?;
1395        pattern.add_edge(2, 0, ())?; // Triangle
1396
1397        let mut target = create_graph::<i32, ()>();
1398        target.add_edge(10, 11, ())?;
1399        target.add_edge(11, 12, ())?;
1400        // Path, not triangle
1401
1402        let result = vf2_subgraph_isomorphism(&pattern, &target, 0);
1403        assert!(!result.is_match);
1404        Ok(())
1405    }
1406
1407    #[test]
1408    fn test_vf2_subgraph() -> GraphResult<()> {
1409        let mut pattern = create_graph::<i32, ()>();
1410        pattern.add_edge(0, 1, ())?;
1411        pattern.add_edge(1, 2, ())?;
1412
1413        let mut target = create_graph::<i32, ()>();
1414        target.add_edge(10, 11, ())?;
1415        target.add_edge(11, 12, ())?;
1416        target.add_edge(12, 13, ())?;
1417        target.add_edge(10, 13, ())?;
1418
1419        let result = vf2_subgraph_isomorphism(&pattern, &target, 0);
1420        assert!(result.is_match);
1421        // Multiple matches possible
1422        assert!(!result.mappings.is_empty());
1423        Ok(())
1424    }
1425
1426    #[test]
1427    fn test_wl_kernel_identical() -> GraphResult<()> {
1428        let mut g1 = create_graph::<i32, ()>();
1429        g1.add_edge(0, 1, ())?;
1430        g1.add_edge(1, 2, ())?;
1431
1432        let mut g2 = create_graph::<i32, ()>();
1433        g2.add_edge(10, 11, ())?;
1434        g2.add_edge(11, 12, ())?;
1435
1436        let result = wl_subtree_kernel(&[&g1, &g2], 3);
1437        assert_eq!(result.kernel_matrix.len(), 2);
1438        // Identical structure should have equal kernel values
1439        assert!((result.kernel_matrix[0][0] - result.kernel_matrix[1][1]).abs() < 1e-6);
1440        assert!((result.kernel_matrix[0][1] - result.kernel_matrix[0][0]).abs() < 1e-6);
1441        Ok(())
1442    }
1443
1444    #[test]
1445    fn test_wl_kernel_different() -> GraphResult<()> {
1446        let mut g1 = create_graph::<i32, ()>();
1447        g1.add_edge(0, 1, ())?;
1448        g1.add_edge(1, 2, ())?;
1449
1450        let mut g2 = create_graph::<i32, ()>();
1451        g2.add_edge(0, 1, ())?;
1452        g2.add_edge(1, 2, ())?;
1453        g2.add_edge(2, 0, ())?; // Triangle
1454
1455        let result = wl_subtree_kernel(&[&g1, &g2], 2);
1456        // Different structures should have different self-kernel values
1457        assert!(result.kernel_matrix[0][0] > 0.0);
1458        assert!(result.kernel_matrix[1][1] > 0.0);
1459        Ok(())
1460    }
1461
1462    #[test]
1463    fn test_graphlet_degree_distribution() -> GraphResult<()> {
1464        let mut graph = create_graph::<i32, ()>();
1465        graph.add_edge(0, 1, ())?;
1466        graph.add_edge(1, 2, ())?;
1467        graph.add_edge(2, 0, ())?;
1468        graph.add_edge(2, 3, ())?;
1469
1470        let result = graphlet_degree_distribution(&graph);
1471        assert_eq!(result.node_gdvs.len(), 4);
1472
1473        // Node 2 has degree 3 (connected to 0, 1, 3)
1474        let gdv_2 = &result.node_gdvs[&2];
1475        assert_eq!(gdv_2.orbit_counts[0], 3); // degree
1476
1477        // All nodes participate in at least one triangle except node 3
1478        let gdv_3 = &result.node_gdvs[&3];
1479        assert_eq!(gdv_3.orbit_counts[3], 0); // no triangles for node 3
1480
1481        assert!(result.gdv_agreement >= 0.0 && result.gdv_agreement <= 1.0);
1482        Ok(())
1483    }
1484
1485    #[test]
1486    fn test_count_3node_motifs() -> GraphResult<()> {
1487        let mut graph = create_graph::<i32, ()>();
1488        graph.add_edge(0, 1, ())?;
1489        graph.add_edge(1, 2, ())?;
1490        graph.add_edge(2, 0, ())?;
1491
1492        let (triangles, open) = count_3node_motifs(&graph);
1493        assert_eq!(triangles, 1);
1494        assert_eq!(open, 0); // All triads are closed in a triangle
1495        Ok(())
1496    }
1497
1498    #[test]
1499    fn test_count_4node_motifs() -> GraphResult<()> {
1500        let mut graph = create_graph::<i32, ()>();
1501        graph.add_edge(0, 1, ())?;
1502        graph.add_edge(1, 2, ())?;
1503        graph.add_edge(2, 3, ())?;
1504
1505        let counts = count_4node_motifs(&graph);
1506        assert!(counts.contains_key("path3"));
1507        assert_eq!(counts["path3"], 1);
1508        Ok(())
1509    }
1510
1511    #[test]
1512    fn test_frequent_subgraph_mining() -> GraphResult<()> {
1513        let mut graph = create_graph::<i32, ()>();
1514        // Create a graph with multiple triangles
1515        graph.add_edge(0, 1, ())?;
1516        graph.add_edge(1, 2, ())?;
1517        graph.add_edge(2, 0, ())?;
1518        graph.add_edge(2, 3, ())?;
1519        graph.add_edge(3, 4, ())?;
1520        graph.add_edge(4, 2, ())?;
1521
1522        let patterns = frequent_subgraph_mining(&graph, 1, 3);
1523        assert!(!patterns.is_empty());
1524        // Should find edges and triangles
1525        Ok(())
1526    }
1527
1528    #[test]
1529    fn test_frequent_subgraph_empty() {
1530        let graph = create_graph::<i32, ()>();
1531        let patterns = frequent_subgraph_mining(&graph, 1, 3);
1532        assert!(patterns.is_empty());
1533    }
1534}