Skip to main content

rust_igraph/algorithms/constructors/
realize_degree_sequence.rs

1//! Degree sequence realization (ALGO-CO-033).
2//!
3//! Counterpart of `igraph_realize_degree_sequence()` from
4//! `references/igraph/src/misc/degree_sequence.cpp`.
5//!
6//! Constructs a simple graph with a given degree sequence using the
7//! Havel-Hakimi algorithm.
8
9use crate::core::error::IgraphError;
10use crate::core::{Graph, IgraphResult, VertexId};
11
12/// Method for selecting the next vertex during degree sequence realization.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum RealizeDegseqMethod {
15    /// Select the vertex with largest remaining degree first.
16    /// Classic Havel-Hakimi; tends to produce high positive assortativity.
17    Largest,
18    /// Select the vertex with smallest remaining degree first.
19    /// Tends to produce connected graphs when a connected realization exists.
20    Smallest,
21    /// Select vertices in index order (position in the degree vector).
22    Index,
23}
24
25/// Realize an undirected simple graph from a degree sequence.
26///
27/// Uses the Havel-Hakimi algorithm: repeatedly pick a vertex and connect
28/// it to the vertices with the highest remaining degrees.
29///
30/// # Arguments
31///
32/// * `degrees` — the target degree for each vertex.
33/// * `method` — vertex selection order (see [`RealizeDegseqMethod`]).
34///
35/// # Errors
36///
37/// Returns `InvalidArgument` if:
38/// - The degree sum is odd (no realization exists).
39/// - Any degree exceeds `n - 1` (no simple realization exists).
40/// - The sequence is not graphical (Havel-Hakimi fails).
41///
42/// # Examples
43///
44/// ```
45/// use rust_igraph::{realize_degree_sequence, RealizeDegseqMethod};
46///
47/// // Realize a path graph: degrees [1, 2, 2, 1]
48/// let g = realize_degree_sequence(&[1, 2, 2, 1], RealizeDegseqMethod::Largest).unwrap();
49/// assert_eq!(g.vcount(), 4);
50/// assert_eq!(g.ecount(), 3);
51/// ```
52pub fn realize_degree_sequence(
53    degrees: &[u32],
54    method: RealizeDegseqMethod,
55) -> IgraphResult<Graph> {
56    let n = degrees.len();
57
58    if n == 0 {
59        return Graph::new(0, false);
60    }
61
62    // Validate: no degree >= n (simple graph constraint)
63    let n_u32 = u32::try_from(n)
64        .map_err(|_| IgraphError::InvalidArgument("vertex count exceeds u32::MAX".to_string()))?;
65
66    for (i, &d) in degrees.iter().enumerate() {
67        if d >= n_u32 {
68            return Err(IgraphError::InvalidArgument(format!(
69                "degree[{i}] = {d} >= n = {n_u32}, cannot realize as simple graph"
70            )));
71        }
72    }
73
74    // Sum of degrees must be even
75    let deg_sum: u64 = degrees.iter().map(|&d| u64::from(d)).sum();
76    if deg_sum % 2 != 0 {
77        return Err(IgraphError::InvalidArgument(
78            "degree sum must be even for an undirected graph".to_string(),
79        ));
80    }
81
82    let num_edges = (deg_sum / 2) as usize;
83
84    if num_edges == 0 {
85        return Graph::new(n_u32, false);
86    }
87
88    // Build (vertex_index, remaining_degree) pairs
89    let mut vd: Vec<(u32, u32)> = degrees
90        .iter()
91        .enumerate()
92        .map(|(i, &d)| {
93            #[allow(clippy::cast_possible_truncation)]
94            let idx = i as u32;
95            (idx, d)
96        })
97        .collect();
98
99    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(num_edges);
100
101    for _ in 0..n {
102        // Select hub vertex based on method
103        let Some(hub_pos) = select_hub(&vd, method) else {
104            break;
105        };
106        let (hub_vertex, hub_degree) = vd[hub_pos];
107
108        if hub_degree == 0 {
109            break;
110        }
111
112        // Remove hub from the working list
113        vd.swap_remove(hub_pos);
114
115        // Sort remaining by degree descending to find top-d neighbors
116        vd.sort_unstable_by_key(|x| std::cmp::Reverse(x.1));
117
118        let d = hub_degree as usize;
119        if d > vd.len() {
120            return Err(IgraphError::InvalidArgument(
121                "degree sequence is not graphical (not realizable as simple graph)".to_string(),
122            ));
123        }
124
125        // Check that the d-th highest degree vertex has degree > 0
126        if vd[d - 1].1 == 0 {
127            return Err(IgraphError::InvalidArgument(
128                "degree sequence is not graphical (not realizable as simple graph)".to_string(),
129            ));
130        }
131
132        // Connect hub to the d vertices with highest remaining degree
133        for item in vd.iter_mut().take(d) {
134            edges.push((hub_vertex, item.0));
135            item.1 -= 1;
136        }
137    }
138
139    // Verify all degrees are exhausted
140    for &(v, d) in &vd {
141        if d != 0 {
142            return Err(IgraphError::InvalidArgument(format!(
143                "degree sequence is not graphical: vertex {v} has {d} remaining degree"
144            )));
145        }
146    }
147
148    let mut graph = Graph::new(n_u32, false)?;
149    graph.add_edges(edges)?;
150    Ok(graph)
151}
152
153fn select_hub(vd: &[(u32, u32)], method: RealizeDegseqMethod) -> Option<usize> {
154    if vd.is_empty() {
155        return None;
156    }
157
158    match method {
159        RealizeDegseqMethod::Largest => {
160            let mut best = 0;
161            for (i, item) in vd.iter().enumerate().skip(1) {
162                if item.1 > vd[best].1 {
163                    best = i;
164                }
165            }
166            if vd[best].1 == 0 { None } else { Some(best) }
167        }
168        RealizeDegseqMethod::Smallest => {
169            let mut best: Option<usize> = None;
170            for (i, item) in vd.iter().enumerate() {
171                if item.1 == 0 {
172                    continue;
173                }
174                match best {
175                    None => best = Some(i),
176                    Some(b) => {
177                        if item.1 < vd[b].1 {
178                            best = Some(i);
179                        }
180                    }
181                }
182            }
183            best
184        }
185        RealizeDegseqMethod::Index => {
186            // Find the first non-zero degree by original index order
187            let mut best: Option<usize> = None;
188            for (i, item) in vd.iter().enumerate() {
189                if item.1 == 0 {
190                    continue;
191                }
192                match best {
193                    None => best = Some(i),
194                    Some(b) => {
195                        if item.0 < vd[b].0 {
196                            best = Some(i);
197                        }
198                    }
199                }
200            }
201            best
202        }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    fn verify_degrees(graph: &Graph, expected: &[u32]) {
211        let n = graph.vcount();
212        assert_eq!(n as usize, expected.len());
213        for v in 0..n {
214            let deg = graph.degree(v).unwrap();
215            assert_eq!(
216                deg, expected[v as usize] as usize,
217                "vertex {v}: got degree {deg}, expected {}",
218                expected[v as usize]
219            );
220        }
221    }
222
223    #[test]
224    fn empty_sequence() {
225        let g = realize_degree_sequence(&[], RealizeDegseqMethod::Largest).unwrap();
226        assert_eq!(g.vcount(), 0);
227        assert_eq!(g.ecount(), 0);
228    }
229
230    #[test]
231    fn all_zeros() {
232        let g = realize_degree_sequence(&[0, 0, 0], RealizeDegseqMethod::Largest).unwrap();
233        assert_eq!(g.vcount(), 3);
234        assert_eq!(g.ecount(), 0);
235    }
236
237    #[test]
238    fn single_edge() {
239        let g = realize_degree_sequence(&[1, 1], RealizeDegseqMethod::Largest).unwrap();
240        assert_eq!(g.vcount(), 2);
241        assert_eq!(g.ecount(), 1);
242        verify_degrees(&g, &[1, 1]);
243    }
244
245    #[test]
246    fn path_graph() {
247        let g = realize_degree_sequence(&[1, 2, 2, 1], RealizeDegseqMethod::Largest).unwrap();
248        assert_eq!(g.vcount(), 4);
249        assert_eq!(g.ecount(), 3);
250        verify_degrees(&g, &[1, 2, 2, 1]);
251    }
252
253    #[test]
254    fn complete_graph_k4() {
255        let g = realize_degree_sequence(&[3, 3, 3, 3], RealizeDegseqMethod::Largest).unwrap();
256        assert_eq!(g.vcount(), 4);
257        assert_eq!(g.ecount(), 6);
258        verify_degrees(&g, &[3, 3, 3, 3]);
259    }
260
261    #[test]
262    fn star_graph() {
263        let g = realize_degree_sequence(&[4, 1, 1, 1, 1], RealizeDegseqMethod::Largest).unwrap();
264        assert_eq!(g.vcount(), 5);
265        assert_eq!(g.ecount(), 4);
266        verify_degrees(&g, &[4, 1, 1, 1, 1]);
267    }
268
269    #[test]
270    fn regular_graph_3() {
271        // 3-regular graph on 4 vertices = K4
272        let g = realize_degree_sequence(&[3, 3, 3, 3], RealizeDegseqMethod::Smallest).unwrap();
273        assert_eq!(g.ecount(), 6);
274        verify_degrees(&g, &[3, 3, 3, 3]);
275    }
276
277    #[test]
278    fn odd_sum_fails() {
279        let result = realize_degree_sequence(&[1, 2, 2], RealizeDegseqMethod::Largest);
280        assert!(result.is_err());
281    }
282
283    #[test]
284    fn degree_too_large_fails() {
285        // degree 4 with only 4 vertices → degree >= n
286        let result = realize_degree_sequence(&[4, 1, 1, 1], RealizeDegseqMethod::Largest);
287        assert!(result.is_err());
288    }
289
290    #[test]
291    fn non_graphical_sequence_fails() {
292        // [3, 3, 3, 1] has even sum (10) but is not graphical
293        let result = realize_degree_sequence(&[3, 3, 3, 1], RealizeDegseqMethod::Largest);
294        assert!(result.is_err());
295    }
296
297    #[test]
298    fn method_smallest_connected() {
299        // The smallest method tends to produce connected graphs
300        let g = realize_degree_sequence(&[2, 2, 2, 2, 2], RealizeDegseqMethod::Smallest).unwrap();
301        assert_eq!(g.vcount(), 5);
302        assert_eq!(g.ecount(), 5);
303        verify_degrees(&g, &[2, 2, 2, 2, 2]);
304    }
305
306    #[test]
307    fn method_index() {
308        let g = realize_degree_sequence(&[2, 2, 1, 1], RealizeDegseqMethod::Index).unwrap();
309        assert_eq!(g.ecount(), 3);
310        verify_degrees(&g, &[2, 2, 1, 1]);
311    }
312
313    #[test]
314    fn larger_graph() {
315        // Realize a degree sequence for a graph with 8 vertices
316        let g = realize_degree_sequence(&[3, 3, 2, 2, 2, 2, 1, 1], RealizeDegseqMethod::Largest)
317            .unwrap();
318        assert_eq!(g.vcount(), 8);
319        assert_eq!(g.ecount(), 8);
320        verify_degrees(&g, &[3, 3, 2, 2, 2, 2, 1, 1]);
321    }
322
323    #[test]
324    fn single_vertex_zero_degree() {
325        let g = realize_degree_sequence(&[0], RealizeDegseqMethod::Largest).unwrap();
326        assert_eq!(g.vcount(), 1);
327        assert_eq!(g.ecount(), 0);
328    }
329}