Skip to main content

rust_igraph/algorithms/constructors/
star.rs

1//! Star graph constructor (ALGO-CN-002).
2//!
3//! Counterpart of `igraph_star()` in
4//! `references/igraph/src/constructors/regular.c:75-141`.
5//!
6//! A star graph on `n` vertices designates one vertex as the *centre*
7//! (often vertex `0`); every other vertex is a leaf connected only to
8//! the centre. The shape of the edge is controlled by a four-way
9//! [`StarMode`]:
10//!
11//! * [`StarMode::Out`] — directed, edges flow **from** the centre to
12//!   each leaf (`center → leaf`).
13//! * [`StarMode::In`] — directed, edges flow **from each leaf** to the
14//!   centre (`leaf → center`).
15//! * [`StarMode::Mutual`] — directed, both arcs `center → leaf` and
16//!   `leaf → center` are emitted for every leaf.
17//! * [`StarMode::Undirected`] — undirected; the underlying graph is
18//!   directed = `false` and a single edge `{center, leaf}` is added per
19//!   leaf.
20//!
21//! Edge enumeration mirrors the upstream C loop exactly: leaves are
22//! visited in vertex-id order `[0, center) ∪ (center, n)` and the arc
23//! direction is determined by the mode. For `Mutual` the forward arc
24//! `center → leaf` is always emitted before the back-arc `leaf → center`
25//! for that leaf.
26//!
27//! Edge counts:
28//!
29//! * `n = 0` or `n = 1` — no edges.
30//! * `n ≥ 2`, mode ∈ {Out, In, Undirected} — `n - 1` edges.
31//! * `n ≥ 2`, mode = Mutual — `2(n - 1)` edges.
32//!
33//! Time complexity: O(|V|).
34
35use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
36
37/// Direction policy for [`star_graph`].
38///
39/// Mirrors `igraph_star_mode_t` in the C reference.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum StarMode {
42    /// Directed star, arcs flow from the centre to each leaf.
43    Out,
44    /// Directed star, arcs flow from each leaf to the centre.
45    In,
46    /// Directed star with both arcs per leaf.
47    Mutual,
48    /// Undirected star.
49    Undirected,
50}
51
52impl StarMode {
53    fn is_directed(self) -> bool {
54        !matches!(self, StarMode::Undirected)
55    }
56}
57
58/// Build a star graph on `n` vertices with the given `center` and arc
59/// policy `mode`.
60///
61/// See the module-level docs for the precise role of each mode.
62///
63/// # Errors
64///
65/// * [`IgraphError::InvalidArgument`] — `center >= n` for `n > 0`.
66/// * [`IgraphError::Internal`] — implied edge count would overflow
67///   `usize` (only reachable on 32-bit targets with absurdly large `n`).
68///
69/// # Examples
70///
71/// ```
72/// use rust_igraph::{star_graph, StarMode};
73///
74/// // Undirected star with 5 leaves around vertex 0.
75/// let s = star_graph(6, StarMode::Undirected, 0).unwrap();
76/// assert_eq!(s.vcount(), 6);
77/// assert_eq!(s.ecount(), 5);
78/// assert!(!s.is_directed());
79/// ```
80pub fn star_graph(n: u32, mode: StarMode, center: u32) -> IgraphResult<Graph> {
81    if n == 0 {
82        return Graph::new(0, mode.is_directed());
83    }
84
85    if center >= n {
86        return Err(IgraphError::InvalidArgument(format!(
87            "star_graph: center vertex {center} is out of range for n = {n}"
88        )));
89    }
90
91    let directed = mode.is_directed();
92    let leaves = (n as usize) - 1;
93    let per_leaf = if matches!(mode, StarMode::Mutual) {
94        2
95    } else {
96        1
97    };
98    let total_edges = leaves
99        .checked_mul(per_leaf)
100        .ok_or(IgraphError::Internal("star_graph edge count overflow"))?;
101
102    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
103
104    for leaf in 0..n {
105        if leaf == center {
106            continue;
107        }
108        match mode {
109            StarMode::Out => edges.push((center, leaf)),
110            StarMode::In | StarMode::Undirected => edges.push((leaf, center)),
111            StarMode::Mutual => {
112                edges.push((center, leaf));
113                edges.push((leaf, center));
114            }
115        }
116    }
117
118    let mut g = Graph::new(n, directed)?;
119    g.add_edges(edges)?;
120    Ok(g)
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    fn collect_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
128        let m = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
129        (0..m)
130            .map(|eid| g.edge(eid).expect("edge id in bounds"))
131            .collect()
132    }
133
134    #[test]
135    fn empty_graph_all_modes() {
136        for mode in [
137            StarMode::Out,
138            StarMode::In,
139            StarMode::Mutual,
140            StarMode::Undirected,
141        ] {
142            let g = star_graph(0, mode, 0).expect("star n=0");
143            assert_eq!(g.vcount(), 0);
144            assert_eq!(g.ecount(), 0);
145            assert_eq!(g.is_directed(), mode.is_directed());
146        }
147    }
148
149    #[test]
150    fn singleton_has_no_edges() {
151        for mode in [
152            StarMode::Out,
153            StarMode::In,
154            StarMode::Mutual,
155            StarMode::Undirected,
156        ] {
157            let g = star_graph(1, mode, 0).expect("star n=1");
158            assert_eq!(g.vcount(), 1);
159            assert_eq!(g.ecount(), 0);
160        }
161    }
162
163    #[test]
164    fn out_mode_emits_center_to_leaf_arcs() {
165        let g = star_graph(5, StarMode::Out, 0).expect("out star");
166        assert!(g.is_directed());
167        assert_eq!(g.ecount(), 4);
168        assert_eq!(collect_edges(&g), vec![(0, 1), (0, 2), (0, 3), (0, 4)]);
169    }
170
171    #[test]
172    fn in_mode_emits_leaf_to_center_arcs() {
173        let g = star_graph(5, StarMode::In, 0).expect("in star");
174        assert!(g.is_directed());
175        assert_eq!(g.ecount(), 4);
176        assert_eq!(collect_edges(&g), vec![(1, 0), (2, 0), (3, 0), (4, 0)]);
177    }
178
179    #[test]
180    fn mutual_mode_emits_both_arcs_per_leaf() {
181        let g = star_graph(4, StarMode::Mutual, 0).expect("mutual star");
182        assert!(g.is_directed());
183        assert_eq!(g.ecount(), 6);
184        // Forward arc first, then back-arc — per upstream loop.
185        assert_eq!(
186            collect_edges(&g),
187            vec![(0, 1), (1, 0), (0, 2), (2, 0), (0, 3), (3, 0)]
188        );
189    }
190
191    #[test]
192    fn undirected_mode_emits_one_edge_per_leaf() {
193        let g = star_graph(5, StarMode::Undirected, 0).expect("undirected star");
194        assert!(!g.is_directed());
195        assert_eq!(g.ecount(), 4);
196        // Undirected canonicalisation sorts endpoints as (min, max).
197        assert_eq!(collect_edges(&g), vec![(0, 1), (0, 2), (0, 3), (0, 4)]);
198    }
199
200    #[test]
201    fn center_can_be_any_vertex_out_mode() {
202        let g = star_graph(5, StarMode::Out, 2).expect("centred at 2");
203        // Leaves visited in vertex-id order [0, 1] then [3, 4].
204        assert_eq!(collect_edges(&g), vec![(2, 0), (2, 1), (2, 3), (2, 4)]);
205    }
206
207    #[test]
208    fn center_can_be_any_vertex_undirected_mode() {
209        let g = star_graph(5, StarMode::Undirected, 3).expect("centred at 3");
210        // For undirected mode the raw arc `(leaf, center)` is canonicalised
211        // to `(min(leaf, center), max(leaf, center))` by Graph::add_edges.
212        assert_eq!(collect_edges(&g), vec![(0, 3), (1, 3), (2, 3), (3, 4)]);
213    }
214
215    #[test]
216    fn center_equal_to_n_minus_one_visits_only_lower_leaves() {
217        let g = star_graph(4, StarMode::Out, 3).expect("centred at last");
218        assert_eq!(g.ecount(), 3);
219        assert_eq!(collect_edges(&g), vec![(3, 0), (3, 1), (3, 2)]);
220    }
221
222    #[test]
223    fn center_out_of_range_errors() {
224        let err = star_graph(3, StarMode::Out, 3).unwrap_err();
225        assert!(matches!(err, IgraphError::InvalidArgument(_)));
226        let err = star_graph(3, StarMode::Out, 5).unwrap_err();
227        assert!(matches!(err, IgraphError::InvalidArgument(_)));
228    }
229
230    #[test]
231    fn center_zero_with_n_zero_is_empty_not_error() {
232        // n=0 short-circuits before the center bounds check (matches upstream).
233        let g = star_graph(0, StarMode::Out, 0).expect("n=0 star");
234        assert_eq!(g.vcount(), 0);
235        assert_eq!(g.ecount(), 0);
236    }
237
238    #[test]
239    fn leaf_degrees_undirected_are_one_each() {
240        let g = star_graph(10, StarMode::Undirected, 0).expect("undirected K1,9");
241        for v in 0..10u32 {
242            let deg = g.neighbors(v).expect("neighbors").len();
243            let expected = if v == 0 { 9 } else { 1 };
244            assert_eq!(deg, expected, "vertex {v}");
245        }
246    }
247
248    #[test]
249    fn out_arcs_give_center_out_degree_and_leaves_in_degree_one() {
250        let g = star_graph(6, StarMode::Out, 0).expect("out star K1,5");
251        // In a directed graph, neighbors() returns OUT-neighbours.
252        let center_out = g.neighbors(0).expect("center out").len();
253        assert_eq!(center_out, 5);
254        for v in 1..6u32 {
255            assert_eq!(g.neighbors(v).expect("leaf out").len(), 0);
256        }
257    }
258}
259
260#[cfg(all(test, feature = "proptest-harness"))]
261mod proptest_tests {
262    use super::*;
263    use proptest::prelude::*;
264
265    fn arb_mode() -> impl Strategy<Value = StarMode> {
266        prop_oneof![
267            Just(StarMode::Out),
268            Just(StarMode::In),
269            Just(StarMode::Mutual),
270            Just(StarMode::Undirected),
271        ]
272    }
273
274    proptest! {
275        #[test]
276        fn edge_count_matches_formula(
277            n in 0u32..40,
278            mode in arb_mode(),
279            center_raw in 0u32..40,
280        ) {
281            // Clamp center within [0, n) when n > 0; otherwise the value is unused.
282            let center = if n == 0 { 0 } else { center_raw % n };
283            let g = star_graph(n, mode, center).unwrap();
284            prop_assert_eq!(g.vcount(), n);
285            prop_assert_eq!(g.is_directed(), mode.is_directed());
286
287            let leaves = if n == 0 { 0 } else { n - 1 };
288            let per_leaf = if matches!(mode, StarMode::Mutual) { 2 } else { 1 };
289            let expected = leaves * per_leaf;
290            let m = u32::try_from(g.ecount()).unwrap();
291            prop_assert_eq!(m, expected);
292        }
293
294        #[test]
295        fn invalid_center_returns_error(
296            n in 1u32..20,
297            mode in arb_mode(),
298            offset in 0u32..20,
299        ) {
300            let bad_center = n + offset;
301            prop_assert!(star_graph(n, mode, bad_center).is_err());
302        }
303
304        #[test]
305        fn every_leaf_is_connected_to_center(
306            n in 2u32..30,
307            mode in arb_mode(),
308            center_raw in 0u32..30,
309        ) {
310            let center = center_raw % n;
311            let g = star_graph(n, mode, center).unwrap();
312            // Total degree at the centre = leaves count for directed-asymmetric
313            // modes, 2·leaves for mutual (both arcs touch centre), leaves for
314            // undirected. In every case, every leaf must share an edge with
315            // the centre — i.e. there exists at least one edge whose endpoint
316            // set is {leaf, center} for each leaf.
317            let m = u32::try_from(g.ecount()).unwrap();
318            let edges: Vec<(VertexId, VertexId)> =
319                (0..m).map(|e| g.edge(e).unwrap()).collect();
320            for leaf in 0..n {
321                if leaf == center {
322                    continue;
323                }
324                let found = edges.iter().any(|&(u, v)| {
325                    (u == center && v == leaf) || (u == leaf && v == center)
326                });
327                prop_assert!(found, "leaf {leaf} missing edge with center {center}");
328            }
329        }
330    }
331}