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 hub_pos = select_hub(&vd, method);
104        if hub_pos.is_none() {
105            break;
106        }
107        let hub_pos = hub_pos.unwrap();
108        let (hub_vertex, hub_degree) = vd[hub_pos];
109
110        if hub_degree == 0 {
111            break;
112        }
113
114        // Remove hub from the working list
115        vd.swap_remove(hub_pos);
116
117        // Sort remaining by degree descending to find top-d neighbors
118        vd.sort_unstable_by_key(|x| std::cmp::Reverse(x.1));
119
120        let d = hub_degree as usize;
121        if d > vd.len() {
122            return Err(IgraphError::InvalidArgument(
123                "degree sequence is not graphical (not realizable as simple graph)".to_string(),
124            ));
125        }
126
127        // Check that the d-th highest degree vertex has degree > 0
128        if vd[d - 1].1 == 0 {
129            return Err(IgraphError::InvalidArgument(
130                "degree sequence is not graphical (not realizable as simple graph)".to_string(),
131            ));
132        }
133
134        // Connect hub to the d vertices with highest remaining degree
135        for item in vd.iter_mut().take(d) {
136            edges.push((hub_vertex, item.0));
137            item.1 -= 1;
138        }
139    }
140
141    // Verify all degrees are exhausted
142    for &(v, d) in &vd {
143        if d != 0 {
144            return Err(IgraphError::InvalidArgument(format!(
145                "degree sequence is not graphical: vertex {v} has {d} remaining degree"
146            )));
147        }
148    }
149
150    let mut graph = Graph::new(n_u32, false)?;
151    graph.add_edges(edges)?;
152    Ok(graph)
153}
154
155fn select_hub(vd: &[(u32, u32)], method: RealizeDegseqMethod) -> Option<usize> {
156    if vd.is_empty() {
157        return None;
158    }
159
160    match method {
161        RealizeDegseqMethod::Largest => {
162            let mut best = 0;
163            for (i, item) in vd.iter().enumerate().skip(1) {
164                if item.1 > vd[best].1 {
165                    best = i;
166                }
167            }
168            if vd[best].1 == 0 { None } else { Some(best) }
169        }
170        RealizeDegseqMethod::Smallest => {
171            let mut best: Option<usize> = None;
172            for (i, item) in vd.iter().enumerate() {
173                if item.1 == 0 {
174                    continue;
175                }
176                match best {
177                    None => best = Some(i),
178                    Some(b) => {
179                        if item.1 < vd[b].1 {
180                            best = Some(i);
181                        }
182                    }
183                }
184            }
185            best
186        }
187        RealizeDegseqMethod::Index => {
188            // Find the first non-zero degree by original index order
189            let mut best: Option<usize> = None;
190            for (i, item) in vd.iter().enumerate() {
191                if item.1 == 0 {
192                    continue;
193                }
194                match best {
195                    None => best = Some(i),
196                    Some(b) => {
197                        if item.0 < vd[b].0 {
198                            best = Some(i);
199                        }
200                    }
201                }
202            }
203            best
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    fn verify_degrees(graph: &Graph, expected: &[u32]) {
213        let n = graph.vcount();
214        assert_eq!(n as usize, expected.len());
215        for v in 0..n {
216            let deg = graph.degree(v).unwrap();
217            assert_eq!(
218                deg, expected[v as usize] as usize,
219                "vertex {v}: got degree {deg}, expected {}",
220                expected[v as usize]
221            );
222        }
223    }
224
225    #[test]
226    fn empty_sequence() {
227        let g = realize_degree_sequence(&[], RealizeDegseqMethod::Largest).unwrap();
228        assert_eq!(g.vcount(), 0);
229        assert_eq!(g.ecount(), 0);
230    }
231
232    #[test]
233    fn all_zeros() {
234        let g = realize_degree_sequence(&[0, 0, 0], RealizeDegseqMethod::Largest).unwrap();
235        assert_eq!(g.vcount(), 3);
236        assert_eq!(g.ecount(), 0);
237    }
238
239    #[test]
240    fn single_edge() {
241        let g = realize_degree_sequence(&[1, 1], RealizeDegseqMethod::Largest).unwrap();
242        assert_eq!(g.vcount(), 2);
243        assert_eq!(g.ecount(), 1);
244        verify_degrees(&g, &[1, 1]);
245    }
246
247    #[test]
248    fn path_graph() {
249        let g = realize_degree_sequence(&[1, 2, 2, 1], RealizeDegseqMethod::Largest).unwrap();
250        assert_eq!(g.vcount(), 4);
251        assert_eq!(g.ecount(), 3);
252        verify_degrees(&g, &[1, 2, 2, 1]);
253    }
254
255    #[test]
256    fn complete_graph_k4() {
257        let g = realize_degree_sequence(&[3, 3, 3, 3], RealizeDegseqMethod::Largest).unwrap();
258        assert_eq!(g.vcount(), 4);
259        assert_eq!(g.ecount(), 6);
260        verify_degrees(&g, &[3, 3, 3, 3]);
261    }
262
263    #[test]
264    fn star_graph() {
265        let g = realize_degree_sequence(&[4, 1, 1, 1, 1], RealizeDegseqMethod::Largest).unwrap();
266        assert_eq!(g.vcount(), 5);
267        assert_eq!(g.ecount(), 4);
268        verify_degrees(&g, &[4, 1, 1, 1, 1]);
269    }
270
271    #[test]
272    fn regular_graph_3() {
273        // 3-regular graph on 4 vertices = K4
274        let g = realize_degree_sequence(&[3, 3, 3, 3], RealizeDegseqMethod::Smallest).unwrap();
275        assert_eq!(g.ecount(), 6);
276        verify_degrees(&g, &[3, 3, 3, 3]);
277    }
278
279    #[test]
280    fn odd_sum_fails() {
281        let result = realize_degree_sequence(&[1, 2, 2], RealizeDegseqMethod::Largest);
282        assert!(result.is_err());
283    }
284
285    #[test]
286    fn degree_too_large_fails() {
287        // degree 4 with only 4 vertices → degree >= n
288        let result = realize_degree_sequence(&[4, 1, 1, 1], RealizeDegseqMethod::Largest);
289        assert!(result.is_err());
290    }
291
292    #[test]
293    fn non_graphical_sequence_fails() {
294        // [3, 3, 3, 1] has even sum (10) but is not graphical
295        let result = realize_degree_sequence(&[3, 3, 3, 1], RealizeDegseqMethod::Largest);
296        assert!(result.is_err());
297    }
298
299    #[test]
300    fn method_smallest_connected() {
301        // The smallest method tends to produce connected graphs
302        let g = realize_degree_sequence(&[2, 2, 2, 2, 2], RealizeDegseqMethod::Smallest).unwrap();
303        assert_eq!(g.vcount(), 5);
304        assert_eq!(g.ecount(), 5);
305        verify_degrees(&g, &[2, 2, 2, 2, 2]);
306    }
307
308    #[test]
309    fn method_index() {
310        let g = realize_degree_sequence(&[2, 2, 1, 1], RealizeDegseqMethod::Index).unwrap();
311        assert_eq!(g.ecount(), 3);
312        verify_degrees(&g, &[2, 2, 1, 1]);
313    }
314
315    #[test]
316    fn larger_graph() {
317        // Realize a degree sequence for a graph with 8 vertices
318        let g = realize_degree_sequence(&[3, 3, 2, 2, 2, 2, 1, 1], RealizeDegseqMethod::Largest)
319            .unwrap();
320        assert_eq!(g.vcount(), 8);
321        assert_eq!(g.ecount(), 8);
322        verify_degrees(&g, &[3, 3, 2, 2, 2, 2, 1, 1]);
323    }
324
325    #[test]
326    fn single_vertex_zero_degree() {
327        let g = realize_degree_sequence(&[0], RealizeDegseqMethod::Largest).unwrap();
328        assert_eq!(g.vcount(), 1);
329        assert_eq!(g.ecount(), 0);
330    }
331}