rust_igraph/algorithms/properties/is_dag.rs
1//! DAG predicate (ALGO-PR-020).
2//!
3//! Counterpart of `igraph_is_dag()` from
4//! `references/igraph/src/properties/dag.c:151`. Returns `true` iff
5//! `graph` is a directed acyclic graph. Undirected graphs are
6//! always `false` (a DAG must be directed by definition — matches
7//! upstream).
8//!
9//! Algorithm: Kahn's topological-sort. Repeatedly peel off
10//! zero-in-degree vertices until either the graph is empty (DAG)
11//! or every remaining vertex has at least one incoming edge
12//! (a cycle exists). Self-loops are detected as an early
13//! short-circuit because they contribute a vertex with
14//! `in-degree >= 1` that can never be peeled.
15//!
16//! Time complexity: `O(V + E)`.
17
18use std::collections::VecDeque;
19
20use crate::core::Graph;
21use crate::core::cache::CachedProperty;
22use crate::core::graph::VertexId;
23
24/// Returns `true` iff `graph` is a directed acyclic graph.
25///
26/// Undirected graphs return `false` unconditionally (matches
27/// upstream — DAGs are directed by definition). Empty graphs and
28/// graphs with isolated vertices but no edges return `true`.
29///
30/// Algorithm: Kahn's topological-sort peel. Start with vertices
31/// whose in-degree is 0 in a queue; pop one, "remove" it by
32/// decrementing the in-degree of its out-neighbours. If any
33/// neighbour reaches 0, queue it. A self-loop is short-circuited
34/// to `false` because the loop's tail can never have its in-degree
35/// reduced past itself.
36///
37/// Counterpart of `igraph_is_dag` from
38/// `references/igraph/src/properties/dag.c:151`.
39///
40/// # Examples
41///
42/// ```
43/// use rust_igraph::{Graph, is_dag};
44///
45/// // Linear chain 0 → 1 → 2 — a DAG.
46/// let mut g = Graph::new(3, true).unwrap();
47/// g.add_edge(0, 1).unwrap();
48/// g.add_edge(1, 2).unwrap();
49/// assert!(is_dag(&g));
50///
51/// // Cycle 0 → 1 → 2 → 0 — not a DAG.
52/// let mut g = Graph::new(3, true).unwrap();
53/// g.add_edge(0, 1).unwrap();
54/// g.add_edge(1, 2).unwrap();
55/// g.add_edge(2, 0).unwrap();
56/// assert!(!is_dag(&g));
57/// ```
58#[must_use]
59pub fn is_dag(graph: &Graph) -> bool {
60 if !graph.is_directed() {
61 return false;
62 }
63 if let Some(v) = graph.cache_get(CachedProperty::IsDag) {
64 return v;
65 }
66
67 let n = graph.vcount();
68 let n_us = n as usize;
69
70 // In-degree of every vertex. `incident_in(v)` returns the in-edges
71 // on a directed graph; its length is the in-degree.
72 let mut in_deg: Vec<u32> = Vec::with_capacity(n_us);
73 for v in 0..n {
74 let Ok(in_edges) = graph.incident_in(v) else {
75 return false;
76 };
77 let deg = u32::try_from(in_edges.len()).unwrap_or(u32::MAX);
78 in_deg.push(deg);
79 }
80
81 // Queue of vertices with current in-degree 0.
82 let mut sources: VecDeque<VertexId> = VecDeque::new();
83 for v in 0..n {
84 if in_deg[v as usize] == 0 {
85 sources.push_back(v);
86 }
87 }
88
89 let mut peeled: u32 = 0;
90 while let Some(v) = sources.pop_front() {
91 peeled = peeled.saturating_add(1);
92
93 // Walk out-edges; for each neighbour, drop its in-degree.
94 let Ok(out_eids) = graph.incident(v) else {
95 return false;
96 };
97 for eid in out_eids {
98 let Ok(nei) = graph.edge_other(eid, v) else {
99 return false;
100 };
101 if nei == v {
102 // Self-loop: vertex v depends on itself; it can
103 // never be peeled away. The graph has a cycle.
104 return false;
105 }
106 let nei_idx = nei as usize;
107 in_deg[nei_idx] = in_deg[nei_idx].saturating_sub(1);
108 if in_deg[nei_idx] == 0 {
109 sources.push_back(nei);
110 }
111 }
112 }
113
114 let result = peeled == n;
115 graph.cache_set(CachedProperty::IsDag, result);
116 result
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::core::Graph;
123
124 #[test]
125 fn empty_directed_graph_is_dag() {
126 let g = Graph::new(0, true).unwrap();
127 assert!(is_dag(&g));
128 }
129
130 #[test]
131 fn isolated_vertices_only_is_dag() {
132 let g = Graph::new(5, true).unwrap();
133 assert!(is_dag(&g));
134 }
135
136 #[test]
137 fn linear_chain_is_dag() {
138 let mut g = Graph::new(4, true).unwrap();
139 g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
140 assert!(is_dag(&g));
141 }
142
143 #[test]
144 fn binary_tree_is_dag() {
145 // 0 → 1, 0 → 2, 1 → 3, 1 → 4.
146 let mut g = Graph::new(5, true).unwrap();
147 g.add_edges(vec![(0u32, 1u32), (0, 2), (1, 3), (1, 4)])
148 .unwrap();
149 assert!(is_dag(&g));
150 }
151
152 #[test]
153 fn three_cycle_is_not_dag() {
154 let mut g = Graph::new(3, true).unwrap();
155 g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 0)]).unwrap();
156 assert!(!is_dag(&g));
157 }
158
159 #[test]
160 fn self_loop_is_not_dag() {
161 let mut g = Graph::new(2, true).unwrap();
162 g.add_edge(0, 0).unwrap();
163 g.add_edge(0, 1).unwrap();
164 assert!(!is_dag(&g));
165 }
166
167 #[test]
168 fn undirected_graph_is_not_dag_even_if_acyclic() {
169 // Undirected path 0-1-2 — acyclic as a graph but the
170 // upstream contract is "DAGs are directed".
171 let mut g = Graph::with_vertices(3);
172 g.add_edges(vec![(0u32, 1u32), (1, 2)]).unwrap();
173 assert!(!is_dag(&g));
174 }
175
176 #[test]
177 fn dag_with_multiple_roots() {
178 // Two disjoint DAG branches feeding into a sink.
179 // 0 → 2, 1 → 2.
180 let mut g = Graph::new(3, true).unwrap();
181 g.add_edges(vec![(0u32, 2u32), (1, 2)]).unwrap();
182 assert!(is_dag(&g));
183 }
184
185 #[test]
186 fn dag_with_parallel_edges_still_dag() {
187 // Parallel edges 0→1 don't introduce a cycle.
188 let mut g = Graph::new(2, true).unwrap();
189 g.add_edge(0, 1).unwrap();
190 g.add_edge(0, 1).unwrap();
191 assert!(is_dag(&g));
192 }
193
194 #[test]
195 fn cycle_with_extra_tail_branches_not_dag() {
196 // Cycle 0→1→2→0 plus a tail 3→0. The tail can be peeled
197 // but the cycle remains, so the graph is not a DAG.
198 let mut g = Graph::new(4, true).unwrap();
199 g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 0), (3, 0)])
200 .unwrap();
201 assert!(!is_dag(&g));
202 }
203
204 #[test]
205 fn back_edge_to_root_is_not_dag() {
206 // 0 → 1 → 2 → 0 (3-cycle) — different framing of the
207 // three_cycle case, double-check the back edge is detected.
208 let mut g = Graph::new(3, true).unwrap();
209 g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 0)]).unwrap();
210 assert!(!is_dag(&g));
211 }
212
213 #[test]
214 fn two_disjoint_dags_is_dag() {
215 // Two unrelated 2-vertex DAGs in the same graph.
216 let mut g = Graph::new(4, true).unwrap();
217 g.add_edges(vec![(0u32, 1u32), (2, 3)]).unwrap();
218 assert!(is_dag(&g));
219 }
220}