Skip to main content

rust_igraph/algorithms/
fundamental_cycles.rs

1//! Fundamental cycle basis (ALGO-CY-003).
2//!
3//! Computes a fundamental cycle basis associated with a BFS tree.
4//! Each non-tree edge creates exactly one fundamental cycle when
5//! combined with the unique tree path between its endpoints.
6//!
7//! Edge directions are ignored. Multi-edges and self-loops are
8//! supported.
9//!
10//! Counterpart of `igraph_fundamental_cycles`.
11
12use std::collections::VecDeque;
13
14use crate::core::graph::EdgeId;
15use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
16
17/// Compute the fundamental cycle basis of a graph.
18///
19/// Returns a list of cycles, where each cycle is a `Vec<EdgeId>`
20/// listing the edge IDs forming that cycle. The cycle basis is
21/// associated with a BFS tree rooted at each connected component.
22///
23/// If `start_vid` is `Some(v)`, only the component containing `v`
24/// is processed. If `None`, all components are processed.
25///
26/// If `bfs_cutoff` is `Some(k)`, only cycles of length at most
27/// `2*k + 1` are returned. If `None`, all fundamental cycles are
28/// found.
29///
30/// Edge directions are ignored (the graph is treated as undirected).
31///
32/// # Errors
33///
34/// Returns an error if `start_vid` is out of range.
35///
36/// # Examples
37///
38/// ```
39/// use rust_igraph::{Graph, fundamental_cycles};
40///
41/// // Triangle: one fundamental cycle of length 3.
42/// let mut g = Graph::with_vertices(3);
43/// g.add_edge(0, 1).unwrap();
44/// g.add_edge(1, 2).unwrap();
45/// g.add_edge(2, 0).unwrap();
46/// let cycles = fundamental_cycles(&g, None, None).unwrap();
47/// assert_eq!(cycles.len(), 1);
48/// assert_eq!(cycles[0].len(), 3);
49///
50/// // Tree: no cycles.
51/// let mut g = Graph::with_vertices(4);
52/// g.add_edge(0, 1).unwrap();
53/// g.add_edge(0, 2).unwrap();
54/// g.add_edge(0, 3).unwrap();
55/// let cycles = fundamental_cycles(&g, None, None).unwrap();
56/// assert!(cycles.is_empty());
57/// ```
58pub fn fundamental_cycles(
59    graph: &Graph,
60    start_vid: Option<VertexId>,
61    bfs_cutoff: Option<u32>,
62) -> IgraphResult<Vec<Vec<EdgeId>>> {
63    let n = graph.vcount();
64
65    if let Some(v) = start_vid {
66        if v >= n {
67            return Err(IgraphError::InvalidArgument(format!(
68                "start vertex {v} out of range (vcount = {n})"
69            )));
70        }
71    }
72
73    let adj = build_incident_all(graph)?;
74
75    let mut visited: Vec<u8> = vec![0; n as usize];
76    let mut result: Vec<Vec<EdgeId>> = Vec::new();
77
78    if let Some(v) = start_vid {
79        fundamental_cycles_bfs(graph, &adj, &mut result, v, bfs_cutoff, &mut visited, 0)?;
80    } else {
81        for v in 0..n {
82            if visited[v as usize] == 0 {
83                fundamental_cycles_bfs(graph, &adj, &mut result, v, bfs_cutoff, &mut visited, 0)?;
84            }
85        }
86    }
87
88    Ok(result)
89}
90
91fn build_incident_all(graph: &Graph) -> IgraphResult<Vec<Vec<(EdgeId, VertexId)>>> {
92    let n = graph.vcount() as usize;
93    let mut adj: Vec<Vec<(EdgeId, VertexId)>> = vec![Vec::new(); n];
94
95    for eid in 0..graph.ecount() {
96        #[allow(clippy::cast_possible_truncation)]
97        let eid32 = eid as EdgeId;
98        let (from, to) = graph.edge(eid32)?;
99        if from == to {
100            adj[from as usize].push((eid32, to));
101        } else {
102            adj[from as usize].push((eid32, to));
103            adj[to as usize].push((eid32, from));
104        }
105    }
106
107    Ok(adj)
108}
109
110fn fundamental_cycles_bfs(
111    graph: &Graph,
112    adj: &[Vec<(EdgeId, VertexId)>],
113    result: &mut Vec<Vec<EdgeId>>,
114    start: VertexId,
115    bfs_cutoff: Option<u32>,
116    visited: &mut [u8],
117    _mark: u8,
118) -> IgraphResult<()> {
119    let n = graph.vcount() as usize;
120    let mut pred_edge: Vec<i64> = vec![-1; n];
121    let mut queue: VecDeque<(VertexId, u32)> = VecDeque::new();
122
123    visited[start as usize] = 1; // queued
124    queue.push_back((start, 0));
125
126    while let Some((v, vdist)) = queue.pop_front() {
127        for &(eid, u) in &adj[v as usize] {
128            if i64::from(eid) == pred_edge[v as usize] {
129                continue;
130            }
131
132            let u_state = visited[u as usize];
133
134            if u_state == 2 {
135                continue;
136            }
137            if u_state == 1 {
138                let cycle = trace_cycle(graph, pred_edge.as_slice(), eid, u, v)?;
139                result.push(cycle);
140            } else if bfs_cutoff.is_none() || vdist < bfs_cutoff.unwrap_or(0) {
141                visited[u as usize] = 1;
142                pred_edge[u as usize] = i64::from(eid);
143                queue.push_back((u, vdist.saturating_add(1)));
144            }
145        }
146
147        visited[v as usize] = 2; // processed
148    }
149
150    Ok(())
151}
152
153fn trace_cycle(
154    graph: &Graph,
155    pred_edge: &[i64],
156    closing_edge: EdgeId,
157    u: VertexId,
158    v: VertexId,
159) -> IgraphResult<Vec<EdgeId>> {
160    let mut u_back: Vec<EdgeId> = Vec::new();
161    let mut v_back: Vec<EdgeId> = vec![closing_edge];
162
163    let mut up = u;
164    let mut vp = v;
165
166    loop {
167        if up == vp {
168            break;
169        }
170
171        let u_pred = pred_edge[up as usize];
172        if u_pred < 0 {
173            break;
174        }
175        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
176        let u_eid = u_pred as EdgeId;
177        u_back.push(u_eid);
178        up = graph.edge_other(u_eid, up)?;
179
180        if up == vp {
181            break;
182        }
183
184        let v_pred = pred_edge[vp as usize];
185        if v_pred < 0 {
186            break;
187        }
188        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
189        let v_eid = v_pred as EdgeId;
190        v_back.push(v_eid);
191        vp = graph.edge_other(v_eid, vp)?;
192    }
193
194    // Build cycle: v_back + reversed u_back
195    let mut cycle = v_back;
196    cycle.extend(u_back.into_iter().rev());
197    Ok(cycle)
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn empty_graph() {
206        let g = Graph::with_vertices(0);
207        let cycles = fundamental_cycles(&g, None, None).unwrap();
208        assert!(cycles.is_empty());
209    }
210
211    #[test]
212    fn tree_no_cycles() {
213        let mut g = Graph::with_vertices(5);
214        g.add_edge(0, 1).unwrap();
215        g.add_edge(0, 2).unwrap();
216        g.add_edge(1, 3).unwrap();
217        g.add_edge(1, 4).unwrap();
218        let cycles = fundamental_cycles(&g, None, None).unwrap();
219        assert!(cycles.is_empty());
220    }
221
222    #[test]
223    fn single_triangle() {
224        let mut g = Graph::with_vertices(3);
225        g.add_edge(0, 1).unwrap();
226        g.add_edge(1, 2).unwrap();
227        g.add_edge(2, 0).unwrap();
228        let cycles = fundamental_cycles(&g, None, None).unwrap();
229        assert_eq!(cycles.len(), 1);
230        assert_eq!(cycles[0].len(), 3);
231    }
232
233    #[test]
234    fn k4_three_cycles() {
235        let mut g = Graph::with_vertices(4);
236        g.add_edge(0, 1).unwrap(); // 0
237        g.add_edge(0, 2).unwrap(); // 1
238        g.add_edge(0, 3).unwrap(); // 2
239        g.add_edge(1, 2).unwrap(); // 3
240        g.add_edge(1, 3).unwrap(); // 4
241        g.add_edge(2, 3).unwrap(); // 5
242        let cycles = fundamental_cycles(&g, None, None).unwrap();
243        // K4: 6 edges - 4 vertices + 1 = 3 fundamental cycles
244        assert_eq!(cycles.len(), 3);
245    }
246
247    #[test]
248    fn two_components() {
249        let mut g = Graph::with_vertices(6);
250        // Triangle 0-1-2
251        g.add_edge(0, 1).unwrap();
252        g.add_edge(1, 2).unwrap();
253        g.add_edge(2, 0).unwrap();
254        // Triangle 3-4-5
255        g.add_edge(3, 4).unwrap();
256        g.add_edge(4, 5).unwrap();
257        g.add_edge(5, 3).unwrap();
258        let cycles = fundamental_cycles(&g, None, None).unwrap();
259        assert_eq!(cycles.len(), 2);
260    }
261
262    #[test]
263    fn start_vertex_single_component() {
264        let mut g = Graph::with_vertices(6);
265        // Triangle 0-1-2
266        g.add_edge(0, 1).unwrap();
267        g.add_edge(1, 2).unwrap();
268        g.add_edge(2, 0).unwrap();
269        // Triangle 3-4-5
270        g.add_edge(3, 4).unwrap();
271        g.add_edge(4, 5).unwrap();
272        g.add_edge(5, 3).unwrap();
273        let cycles = fundamental_cycles(&g, Some(0), None).unwrap();
274        assert_eq!(cycles.len(), 1);
275    }
276
277    #[test]
278    fn start_vertex_out_of_range() {
279        let g = Graph::with_vertices(3);
280        assert!(fundamental_cycles(&g, Some(5), None).is_err());
281    }
282
283    #[test]
284    fn cycle_rank_formula() {
285        // For a connected graph: cycle_rank = |E| - |V| + 1
286        let mut g = Graph::with_vertices(5);
287        g.add_edge(0, 1).unwrap();
288        g.add_edge(1, 2).unwrap();
289        g.add_edge(2, 3).unwrap();
290        g.add_edge(3, 4).unwrap();
291        g.add_edge(4, 0).unwrap(); // creates one cycle
292        g.add_edge(0, 2).unwrap(); // creates another
293        let cycles = fundamental_cycles(&g, None, None).unwrap();
294        // rank = 6 - 5 + 1 = 2
295        assert_eq!(cycles.len(), 2);
296    }
297
298    #[test]
299    fn self_loop() {
300        let mut g = Graph::with_vertices(2);
301        g.add_edge(0, 0).unwrap(); // self-loop
302        g.add_edge(0, 1).unwrap();
303        let cycles = fundamental_cycles(&g, None, None).unwrap();
304        assert_eq!(cycles.len(), 1);
305        assert_eq!(cycles[0].len(), 1); // self-loop is a cycle of 1 edge
306    }
307
308    #[test]
309    fn multi_edge() {
310        let mut g = Graph::with_vertices(2);
311        g.add_edge(0, 1).unwrap();
312        g.add_edge(0, 1).unwrap(); // parallel edge
313        let cycles = fundamental_cycles(&g, None, None).unwrap();
314        assert_eq!(cycles.len(), 1);
315        assert_eq!(cycles[0].len(), 2); // two edges form a cycle
316    }
317
318    #[test]
319    fn directed_graph_treated_as_undirected() {
320        let mut g = Graph::new(3, true).unwrap();
321        g.add_edge(0, 1).unwrap();
322        g.add_edge(1, 2).unwrap();
323        g.add_edge(2, 0).unwrap();
324        let cycles = fundamental_cycles(&g, None, None).unwrap();
325        assert_eq!(cycles.len(), 1);
326    }
327
328    #[test]
329    fn bfs_cutoff_limits_cycles() {
330        // Create a cycle of length 5 and a cycle of length 3
331        let mut g = Graph::with_vertices(5);
332        g.add_edge(0, 1).unwrap();
333        g.add_edge(1, 2).unwrap();
334        g.add_edge(2, 3).unwrap();
335        g.add_edge(3, 4).unwrap();
336        g.add_edge(4, 0).unwrap(); // cycle of length 5
337        g.add_edge(0, 2).unwrap(); // creates a shorter cycle through 0-1-2-0
338
339        // With cutoff = 1, only cycles of length 2*1+1=3 or shorter
340        let cycles = fundamental_cycles(&g, None, Some(1)).unwrap();
341        for cycle in &cycles {
342            assert!(cycle.len() <= 3);
343        }
344    }
345
346    #[test]
347    fn isolated_vertices() {
348        let g = Graph::with_vertices(10);
349        let cycles = fundamental_cycles(&g, None, None).unwrap();
350        assert!(cycles.is_empty());
351    }
352
353    #[test]
354    fn single_vertex() {
355        let g = Graph::with_vertices(1);
356        let cycles = fundamental_cycles(&g, None, None).unwrap();
357        assert!(cycles.is_empty());
358    }
359}