Skip to main content

rust_igraph/algorithms/properties/
is_windmill.rs

1//! Windmill graph predicate (ALGO-PR-092).
2//!
3//! A windmill graph `Wd(k, n)` is formed by joining n copies of the
4//! complete graph `K_k` at a single shared vertex (the "hub"). It has
5//! 1 + n*(k-1) vertices and n*k*(k-1)/2 edges.
6//!
7//! Special cases: `Wd(3, n)` is the friendship graph (n triangles
8//! sharing a vertex). `Wd(2, n)` is a star `K_{1,n}`.
9//!
10//! For directed graphs, the function returns `false`.
11
12use crate::algorithms::properties::is_simple::is_simple;
13use crate::core::{Graph, IgraphResult};
14
15/// Check whether a graph is a windmill graph.
16///
17/// A windmill graph has a single hub vertex connected to n copies of
18/// `K_k` (complete graphs of the same size k ≥ 2). The hub is the
19/// only vertex shared across copies.
20///
21/// Returns `false` for directed graphs and non-simple graphs.
22/// Returns `true` for `K_1` (trivially `Wd(k, 0)` for any k).
23///
24/// On success returns `Some((k, n))` where k is the clique size and
25/// n is the number of cliques. Returns `None` if not a windmill.
26///
27/// # Examples
28///
29/// ```
30/// use rust_igraph::{Graph, is_windmill};
31///
32/// // Friendship graph: 2 triangles sharing vertex 0
33/// let mut g = Graph::with_vertices(5);
34/// g.add_edge(0, 1).unwrap();
35/// g.add_edge(0, 2).unwrap();
36/// g.add_edge(1, 2).unwrap();
37/// g.add_edge(0, 3).unwrap();
38/// g.add_edge(0, 4).unwrap();
39/// g.add_edge(3, 4).unwrap();
40/// assert_eq!(is_windmill(&g).unwrap(), Some((3, 2)));
41/// ```
42pub fn is_windmill(graph: &Graph) -> IgraphResult<Option<(u32, u32)>> {
43    if graph.is_directed() {
44        return Ok(None);
45    }
46
47    let n = graph.vcount();
48    if n == 0 {
49        return Ok(None);
50    }
51    if n == 1 {
52        return Ok(Some((2, 0)));
53    }
54
55    if !is_simple(graph)? {
56        return Ok(None);
57    }
58
59    // Find the hub: vertex with maximum degree
60    let mut hub = 0u32;
61    let mut hub_deg = graph.degree(0)?;
62    for v in 1..n {
63        let d = graph.degree(v)?;
64        if d > hub_deg {
65            hub_deg = d;
66            hub = v;
67        }
68    }
69
70    // The hub must be connected to all other vertices
71    if hub_deg != (n - 1) as usize {
72        return Ok(None);
73    }
74
75    // All non-hub vertices must have the same degree
76    let mut non_hub_deg = 0usize;
77    for v in 0..n {
78        if v == hub {
79            continue;
80        }
81        let d = graph.degree(v)?;
82        if non_hub_deg == 0 {
83            non_hub_deg = d;
84        } else if d != non_hub_deg {
85            return Ok(None);
86        }
87    }
88
89    // In Wd(k, num_copies): non-hub vertices have degree k-1
90    // (connected to k-2 other vertices in their clique + the hub)
91    let k_minus_1 = non_hub_deg;
92    if k_minus_1 == 0 {
93        return Ok(None);
94    }
95    let k = u32::try_from(k_minus_1 + 1).unwrap_or(u32::MAX);
96
97    // Number of copies
98    let non_hub_count = (n - 1) as usize;
99    if non_hub_count % k_minus_1 != 0 {
100        return Ok(None);
101    }
102    let num_copies = u32::try_from(non_hub_count / k_minus_1).unwrap_or(u32::MAX);
103
104    // Verify edge count: num_copies * k*(k-1)/2
105    let k64 = u64::from(k);
106    let expected_edges =
107        u64::from(num_copies).saturating_mul(k64.saturating_mul(k64.saturating_sub(1)) / 2);
108    if graph.ecount() as u64 != expected_edges {
109        return Ok(None);
110    }
111
112    // Verify structure: each non-hub vertex's neighbors (excluding hub)
113    // must form a clique of size k-2 among themselves, and all must be
114    // neighbors of the hub (which they are since hub connects to everyone).
115    // Additionally, no two non-hub vertices from different cliques should
116    // be connected.
117    // Assign cliques: pick an unvisited non-hub vertex, its non-hub
118    // neighbors form its clique mates.
119    let mut visited = vec![false; n as usize];
120    visited[hub as usize] = true;
121
122    for v in 0..n {
123        if visited[v as usize] {
124            continue;
125        }
126
127        let nbrs = graph.neighbors(v)?;
128        let clique_mates: Vec<u32> = nbrs.iter().filter(|&&w| w != hub).copied().collect();
129
130        if clique_mates.len() != k_minus_1 - 1 {
131            return Ok(None);
132        }
133
134        // All clique mates must be unvisited (belong to same clique)
135        for &mate in &clique_mates {
136            if visited[mate as usize] {
137                return Ok(None);
138            }
139        }
140
141        // Verify clique mates are connected to each other
142        for (i, &u) in clique_mates.iter().enumerate() {
143            let u_nbrs = graph.neighbors(u)?;
144            for &w in &clique_mates[i + 1..] {
145                if !u_nbrs.contains(&w) {
146                    return Ok(None);
147                }
148            }
149        }
150
151        // Mark all clique members as visited
152        visited[v as usize] = true;
153        for &mate in &clique_mates {
154            visited[mate as usize] = true;
155        }
156    }
157
158    Ok(Some((k, num_copies)))
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn empty_graph() {
167        let g = Graph::with_vertices(0);
168        assert_eq!(is_windmill(&g).unwrap(), None);
169    }
170
171    #[test]
172    fn single_vertex() {
173        let g = Graph::with_vertices(1);
174        assert_eq!(is_windmill(&g).unwrap(), Some((2, 0)));
175    }
176
177    #[test]
178    fn single_triangle_wd31() {
179        let mut g = Graph::with_vertices(3);
180        g.add_edge(0, 1).unwrap();
181        g.add_edge(1, 2).unwrap();
182        g.add_edge(2, 0).unwrap();
183        assert_eq!(is_windmill(&g).unwrap(), Some((3, 1)));
184    }
185
186    #[test]
187    fn friendship_wd32() {
188        // 2 triangles sharing vertex 0
189        let mut g = Graph::with_vertices(5);
190        g.add_edge(0, 1).unwrap();
191        g.add_edge(0, 2).unwrap();
192        g.add_edge(1, 2).unwrap();
193        g.add_edge(0, 3).unwrap();
194        g.add_edge(0, 4).unwrap();
195        g.add_edge(3, 4).unwrap();
196        assert_eq!(is_windmill(&g).unwrap(), Some((3, 2)));
197    }
198
199    #[test]
200    fn friendship_wd33() {
201        // 3 triangles sharing vertex 0
202        let mut g = Graph::with_vertices(7);
203        g.add_edge(0, 1).unwrap();
204        g.add_edge(0, 2).unwrap();
205        g.add_edge(1, 2).unwrap();
206        g.add_edge(0, 3).unwrap();
207        g.add_edge(0, 4).unwrap();
208        g.add_edge(3, 4).unwrap();
209        g.add_edge(0, 5).unwrap();
210        g.add_edge(0, 6).unwrap();
211        g.add_edge(5, 6).unwrap();
212        assert_eq!(is_windmill(&g).unwrap(), Some((3, 3)));
213    }
214
215    #[test]
216    fn wd42() {
217        // 2 copies of K4 sharing vertex 0
218        // Copy 1: {0,1,2,3}, Copy 2: {0,4,5,6}
219        let mut g = Graph::with_vertices(7);
220        // Copy 1
221        for i in 0..4u32 {
222            for j in (i + 1)..4 {
223                g.add_edge(i, j).unwrap();
224            }
225        }
226        // Copy 2
227        for &i in &[0u32, 4, 5, 6] {
228            for &j in &[0u32, 4, 5, 6] {
229                if i < j {
230                    g.add_edge(i, j).unwrap();
231                }
232            }
233        }
234        assert_eq!(is_windmill(&g).unwrap(), Some((4, 2)));
235    }
236
237    #[test]
238    fn star_is_wd2n() {
239        // K_{1,3} = Wd(2, 3)
240        let mut g = Graph::with_vertices(4);
241        g.add_edge(0, 1).unwrap();
242        g.add_edge(0, 2).unwrap();
243        g.add_edge(0, 3).unwrap();
244        assert_eq!(is_windmill(&g).unwrap(), Some((2, 3)));
245    }
246
247    #[test]
248    fn path_not_windmill() {
249        let mut g = Graph::with_vertices(4);
250        g.add_edge(0, 1).unwrap();
251        g.add_edge(1, 2).unwrap();
252        g.add_edge(2, 3).unwrap();
253        assert_eq!(is_windmill(&g).unwrap(), None);
254    }
255
256    #[test]
257    fn cycle_c4_not_windmill() {
258        let mut g = Graph::with_vertices(4);
259        g.add_edge(0, 1).unwrap();
260        g.add_edge(1, 2).unwrap();
261        g.add_edge(2, 3).unwrap();
262        g.add_edge(3, 0).unwrap();
263        assert_eq!(is_windmill(&g).unwrap(), None);
264    }
265
266    #[test]
267    fn k4_is_wd41() {
268        let mut g = Graph::with_vertices(4);
269        g.add_edge(0, 1).unwrap();
270        g.add_edge(0, 2).unwrap();
271        g.add_edge(0, 3).unwrap();
272        g.add_edge(1, 2).unwrap();
273        g.add_edge(1, 3).unwrap();
274        g.add_edge(2, 3).unwrap();
275        assert_eq!(is_windmill(&g).unwrap(), Some((4, 1)));
276    }
277
278    #[test]
279    fn directed_returns_none() {
280        let mut g = Graph::new(3, true).unwrap();
281        g.add_edge(0, 1).unwrap();
282        g.add_edge(1, 2).unwrap();
283        g.add_edge(2, 0).unwrap();
284        assert_eq!(is_windmill(&g).unwrap(), None);
285    }
286
287    #[test]
288    fn butterfly_not_windmill() {
289        // Butterfly: two triangles sharing an edge, not a vertex
290        let mut g = Graph::with_vertices(4);
291        g.add_edge(0, 1).unwrap();
292        g.add_edge(1, 2).unwrap();
293        g.add_edge(2, 0).unwrap();
294        g.add_edge(0, 3).unwrap();
295        g.add_edge(3, 1).unwrap();
296        // This is actually NOT a butterfly (bowtie). Let me verify:
297        // Vertices: 0,1,2,3. Edges: 0-1, 1-2, 2-0, 0-3, 3-1
298        // Degrees: 0→3, 1→3, 2→2, 3→2
299        // Two vertices have max degree 3 (not n-1=3, which is correct)
300        // But they both share degree n-1=3, so hub candidate is ambiguous
301        // Hub=0: non-hub neighbors all of degree? 1→3, 2→2, 3→2. Not uniform.
302        assert_eq!(is_windmill(&g).unwrap(), None);
303    }
304}