Skip to main content

uni_algo/algo/algorithms/
elementary_circuits.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! Elementary Circuits Algorithm (Johnson's Algorithm).
5//!
6//! Finds all elementary circuits (cycles) in a directed graph.
7//! Uses Johnson's algorithm with SCC decomposition optimization.
8
9use crate::algo::GraphProjection;
10use crate::algo::algorithms::{Algorithm, Scc, SccConfig};
11use std::collections::{HashMap, HashSet};
12use uni_common::core::id::Vid;
13
14pub struct ElementaryCircuits;
15
16#[derive(Debug, Clone)]
17pub struct ElementaryCircuitsConfig {
18    pub min_length: usize,
19    pub max_length: usize,
20    pub limit: usize,
21}
22
23impl Default for ElementaryCircuitsConfig {
24    fn default() -> Self {
25        Self {
26            min_length: 2,  // Simple cycle > 1 node? Or self loop allowed? Usually > 0.
27            max_length: 10, // Limit depth to avoid explosion
28            limit: 1000,
29        }
30    }
31}
32
33pub struct ElementaryCircuitsResult {
34    pub cycles: Vec<Vec<Vid>>,
35}
36
37impl Algorithm for ElementaryCircuits {
38    type Config = ElementaryCircuitsConfig;
39    type Result = ElementaryCircuitsResult;
40
41    fn name() -> &'static str {
42        "elementary_circuits"
43    }
44
45    fn run(graph: &GraphProjection, config: Self::Config) -> Self::Result {
46        // 1. Decompose into SCCs
47        // We only care about SCCs with size > 1 or self-loop.
48        let scc_res = Scc::run(graph, SccConfig::default());
49
50        let mut scc_map: HashMap<u64, Vec<u32>> = HashMap::new();
51        for (vid, cid) in scc_res.components {
52            if let Some(slot) = graph.to_slot(vid) {
53                scc_map.entry(cid).or_default().push(slot);
54            }
55        }
56
57        let mut cycles = Vec::new();
58        let mut count = 0;
59
60        for nodes in scc_map.values() {
61            if nodes.len() < 2 {
62                // Check self loop
63                if nodes.len() == 1 {
64                    let u = nodes[0];
65                    if graph.out_neighbors(u).contains(&u) && config.min_length <= 1 {
66                        cycles.push(vec![graph.to_vid(u)]);
67                        count += 1;
68                    }
69                }
70                continue;
71            }
72
73            if count >= config.limit {
74                break;
75            }
76
77            // Run Johnson's on this SCC
78            // Subgraph induced by `nodes`.
79            // We need mapping from global slot to local index 0..sub_n
80            // Or just work with global slots but filtered.
81
82            // To properly implement Johnson, we need to order nodes.
83            let mut scc_nodes = nodes.clone();
84            scc_nodes.sort(); // Process in order
85
86            for (i, &start_node) in scc_nodes.iter().enumerate() {
87                if count >= config.limit {
88                    break;
89                }
90
91                // Subgraph induced by {v | v >= start_node}
92                let sub_scc = &scc_nodes[i..];
93
94                // DFS state
95                let mut stack = Vec::new();
96                let mut blocked = HashSet::new();
97                let mut block_map: HashMap<u32, HashSet<u32>> = HashMap::new();
98
99                let mut ctx = CircuitCtx {
100                    graph,
101                    scc_nodes: sub_scc,
102                    stack: &mut stack,
103                    blocked: &mut blocked,
104                    block_map: &mut block_map,
105                    cycles: &mut cycles,
106                    config: &config,
107                    count: &mut count,
108                };
109
110                circuit(start_node, start_node, &mut ctx);
111            }
112        }
113
114        ElementaryCircuitsResult { cycles }
115    }
116}
117
118struct CircuitCtx<'a> {
119    graph: &'a GraphProjection,
120    scc_nodes: &'a [u32],
121    stack: &'a mut Vec<Vid>,
122    blocked: &'a mut HashSet<u32>,
123    block_map: &'a mut HashMap<u32, HashSet<u32>>,
124    cycles: &'a mut Vec<Vec<Vid>>,
125    config: &'a ElementaryCircuitsConfig,
126    count: &'a mut usize,
127}
128
129fn circuit(u: u32, start_node: u32, ctx: &mut CircuitCtx) -> bool {
130    if *ctx.count >= ctx.config.limit {
131        return false;
132    }
133
134    let vid = ctx.graph.to_vid(u);
135    ctx.stack.push(vid);
136    ctx.blocked.insert(u);
137    let mut found = false;
138
139    // Neighbors in SCC
140    for &v in ctx.graph.out_neighbors(u) {
141        if !ctx.scc_nodes.contains(&v) {
142            continue;
143        }
144
145        if v == start_node {
146            if ctx.stack.len() >= ctx.config.min_length && ctx.stack.len() <= ctx.config.max_length
147            {
148                ctx.cycles.push(ctx.stack.clone());
149                *ctx.count += 1;
150                found = true;
151            }
152        } else if !ctx.blocked.contains(&v)
153            && ctx.stack.len() < ctx.config.max_length
154            && circuit(v, start_node, ctx)
155        {
156            found = true;
157        }
158    }
159
160    if found {
161        unblock(u, ctx.blocked, ctx.block_map);
162    } else {
163        for &v in ctx.graph.out_neighbors(u) {
164            if ctx.scc_nodes.contains(&v) {
165                ctx.block_map.entry(v).or_default().insert(u);
166            }
167        }
168    }
169
170    ctx.stack.pop();
171    found
172}
173
174fn unblock(u: u32, blocked: &mut HashSet<u32>, block_map: &mut HashMap<u32, HashSet<u32>>) {
175    blocked.remove(&u);
176    if let Some(list) = block_map.remove(&u) {
177        for w in list {
178            if blocked.contains(&w) {
179                unblock(w, blocked, block_map);
180            }
181        }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use crate::algo::test_utils::build_test_graph;
189
190    #[test]
191    fn test_elementary_circuits() {
192        // 0->1->2->0 (Cycle 1)
193        // 1->2->1 (Cycle 2 - nested)
194        // 0->1, 1->2, 2->0, 2->1
195
196        let vids = vec![Vid::from(0), Vid::from(1), Vid::from(2)];
197        let edges = vec![
198            (Vid::from(0), Vid::from(1)),
199            (Vid::from(1), Vid::from(2)),
200            (Vid::from(2), Vid::from(0)),
201            (Vid::from(2), Vid::from(1)),
202        ];
203        let graph = build_test_graph(vids, edges);
204
205        let config = ElementaryCircuitsConfig {
206            min_length: 2,
207            ..Default::default()
208        };
209        let result = ElementaryCircuits::run(&graph, config);
210
211        assert_eq!(result.cycles.len(), 2);
212        // Cycles: [0,1,2], [1,2]
213    }
214}