Skip to main content

rust_igraph/algorithms/properties/
summary.rs

1//! Graph summary — a quick one-call overview of structural properties.
2
3use std::fmt::Write as FmtWrite;
4
5use crate::algorithms::connectivity::components::connected_components;
6use crate::algorithms::connectivity::is_connected::{ConnectednessMode, is_connected};
7use crate::algorithms::properties::basic::density;
8use crate::algorithms::properties::degree::{DegreeMode, max_degree, min_degree};
9use crate::algorithms::properties::multiplicity::{has_loop, has_multiple};
10use crate::core::{Graph, IgraphResult};
11
12/// Result of [`graph_summary`] containing precomputed graph statistics.
13#[derive(Debug, Clone)]
14#[allow(clippy::struct_excessive_bools)]
15pub struct GraphSummary {
16    /// Whether the graph is directed.
17    pub directed: bool,
18    /// Number of vertices.
19    pub vcount: u32,
20    /// Number of edges.
21    pub ecount: usize,
22    /// Whether the graph is connected (weakly, for directed graphs).
23    pub connected: bool,
24    /// Number of (weakly) connected components.
25    pub component_count: u32,
26    /// Edge density, or `None` for trivial graphs (0 or 1 vertex).
27    pub density: Option<f64>,
28    /// Minimum degree across all vertices.
29    pub min_degree: u32,
30    /// Maximum degree across all vertices.
31    pub max_degree: u32,
32    /// Whether the graph has self-loops.
33    pub has_loops: bool,
34    /// Whether the graph has multi-edges (parallel edges).
35    pub has_multiple: bool,
36}
37
38impl std::fmt::Display for GraphSummary {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        let kind = if self.directed {
41            "Directed"
42        } else {
43            "Undirected"
44        };
45        write!(
46            f,
47            "{kind} graph: {v} vertices, {e} edges",
48            v = self.vcount,
49            e = self.ecount
50        )?;
51
52        if self.vcount > 0 {
53            let conn = if self.connected {
54                "connected"
55            } else {
56                "disconnected"
57            };
58            write!(f, ", {conn}")?;
59            if !self.connected {
60                write!(f, " ({} components)", self.component_count)?;
61            }
62        }
63
64        if let Some(d) = self.density {
65            write!(f, ", density={d:.4}")?;
66        }
67
68        if self.vcount > 0 {
69            write!(f, ", degree=[{}, {}]", self.min_degree, self.max_degree)?;
70        }
71
72        let mut flags = Vec::new();
73        if self.has_loops {
74            flags.push("loops");
75        }
76        if self.has_multiple {
77            flags.push("multi");
78        }
79        if !flags.is_empty() {
80            write!(f, " [{}]", flags.join(", "))?;
81        }
82
83        Ok(())
84    }
85}
86
87/// Compute a quick structural summary of a graph.
88///
89/// Returns a [`GraphSummary`] struct with key properties pre-computed:
90/// vertex/edge counts, connectivity, density, degree range, and whether
91/// the graph has self-loops or multi-edges. The struct implements
92/// [`Display`](std::fmt::Display) for pretty-printing.
93///
94/// This is the Rust counterpart of python-igraph's `Graph.summary()`.
95///
96/// # Examples
97///
98/// ```
99/// use rust_igraph::{Graph, graph_summary};
100///
101/// let mut g = Graph::with_vertices(5);
102/// g.add_edge(0, 1).unwrap();
103/// g.add_edge(1, 2).unwrap();
104/// g.add_edge(2, 3).unwrap();
105/// g.add_edge(3, 4).unwrap();
106///
107/// let s = graph_summary(&g).unwrap();
108/// assert_eq!(s.vcount, 5);
109/// assert_eq!(s.ecount, 4);
110/// assert!(s.connected);
111/// assert_eq!(s.component_count, 1);
112/// assert_eq!(s.min_degree, 1);
113/// assert_eq!(s.max_degree, 2);
114/// assert!(!s.has_loops);
115/// assert!(!s.has_multiple);
116/// println!("{s}");
117/// ```
118///
119/// ```
120/// use rust_igraph::{Graph, graph_summary};
121///
122/// let g = Graph::with_vertices(0);
123/// let s = graph_summary(&g).unwrap();
124/// assert_eq!(s.vcount, 0);
125/// assert_eq!(s.ecount, 0);
126/// assert!(s.connected); // vacuously
127/// ```
128pub fn graph_summary(graph: &Graph) -> IgraphResult<GraphSummary> {
129    let vcount = graph.vcount();
130    let ecount = graph.ecount();
131    let directed = graph.is_directed();
132
133    let (connected, component_count) = if vcount == 0 {
134        (true, 0)
135    } else {
136        let conn = is_connected(graph, ConnectednessMode::Weak)?;
137        let cc = connected_components(graph)?;
138        (conn, cc.count)
139    };
140
141    let dens = density(graph)?;
142
143    let (min_deg, max_deg) = if vcount == 0 {
144        (0, 0)
145    } else {
146        let mn = min_degree(graph, DegreeMode::All)?;
147        let mx = max_degree(graph, DegreeMode::All)?;
148        (mn, mx)
149    };
150
151    let loops = has_loop(graph)?;
152    let multi = has_multiple(graph)?;
153
154    Ok(GraphSummary {
155        directed,
156        vcount,
157        ecount,
158        connected,
159        component_count,
160        density: dens,
161        min_degree: min_deg,
162        max_degree: max_deg,
163        has_loops: loops,
164        has_multiple: multi,
165    })
166}
167
168/// Format a detailed multi-line summary of a graph.
169///
170/// Similar to [`graph_summary`] but returns a multi-line string with
171/// additional detail, formatted for terminal/log output.
172///
173/// # Examples
174///
175/// ```
176/// use rust_igraph::{Graph, graph_summary_string};
177///
178/// let mut g = Graph::with_vertices(4);
179/// g.add_edge(0, 1).unwrap();
180/// g.add_edge(1, 2).unwrap();
181/// g.add_edge(2, 3).unwrap();
182/// g.add_edge(3, 0).unwrap();
183///
184/// let s = graph_summary_string(&g).unwrap();
185/// assert!(s.contains("Vertices: 4"));
186/// assert!(s.contains("Edges: 4"));
187/// assert!(s.contains("Connected: yes"));
188/// ```
189pub fn graph_summary_string(graph: &Graph) -> IgraphResult<String> {
190    let s = graph_summary(graph)?;
191    let mut out = String::new();
192
193    let kind = if s.directed { "Directed" } else { "Undirected" };
194    let _ = writeln!(out, "--- Graph Summary ---");
195    let _ = writeln!(out, "Type: {kind}");
196    let _ = writeln!(out, "Vertices: {}", s.vcount);
197    let _ = writeln!(out, "Edges: {}", s.ecount);
198
199    let conn_str = if s.connected { "yes" } else { "no" };
200    let _ = writeln!(out, "Connected: {conn_str}");
201    if !s.connected {
202        let _ = writeln!(out, "Components: {}", s.component_count);
203    }
204
205    if let Some(d) = s.density {
206        let _ = writeln!(out, "Density: {d:.6}");
207    }
208
209    if s.vcount > 0 {
210        let _ = writeln!(out, "Degree range: [{}, {}]", s.min_degree, s.max_degree);
211    }
212
213    let simple = !s.has_loops && !s.has_multiple;
214    let _ = writeln!(out, "Simple: {}", if simple { "yes" } else { "no" });
215    if s.has_loops {
216        let _ = writeln!(out, "  Self-loops: yes");
217    }
218    if s.has_multiple {
219        let _ = writeln!(out, "  Multi-edges: yes");
220    }
221
222    let _ = write!(out, "--------------------");
223    Ok(out)
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn summary_empty_graph() {
232        let g = Graph::with_vertices(0);
233        let s = graph_summary(&g).unwrap();
234        assert_eq!(s.vcount, 0);
235        assert_eq!(s.ecount, 0);
236        assert!(s.connected);
237        assert_eq!(s.component_count, 0);
238        assert_eq!(s.density, None);
239        assert_eq!(s.min_degree, 0);
240        assert_eq!(s.max_degree, 0);
241        assert!(!s.has_loops);
242        assert!(!s.has_multiple);
243    }
244
245    #[test]
246    fn summary_path_graph() {
247        let mut g = Graph::with_vertices(5);
248        g.add_edge(0, 1).unwrap();
249        g.add_edge(1, 2).unwrap();
250        g.add_edge(2, 3).unwrap();
251        g.add_edge(3, 4).unwrap();
252
253        let s = graph_summary(&g).unwrap();
254        assert!(!s.directed);
255        assert_eq!(s.vcount, 5);
256        assert_eq!(s.ecount, 4);
257        assert!(s.connected);
258        assert_eq!(s.component_count, 1);
259        assert_eq!(s.min_degree, 1);
260        assert_eq!(s.max_degree, 2);
261        assert!(!s.has_loops);
262        assert!(!s.has_multiple);
263    }
264
265    #[test]
266    fn summary_disconnected_graph() {
267        let mut g = Graph::with_vertices(6);
268        g.add_edge(0, 1).unwrap();
269        g.add_edge(1, 2).unwrap();
270        g.add_edge(3, 4).unwrap();
271        // vertex 5 is isolated
272
273        let s = graph_summary(&g).unwrap();
274        assert!(!s.connected);
275        assert_eq!(s.component_count, 3);
276    }
277
278    #[test]
279    fn summary_directed_graph() {
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
285        let s = graph_summary(&g).unwrap();
286        assert!(s.directed);
287        assert!(s.connected);
288        assert_eq!(s.ecount, 3);
289    }
290
291    #[test]
292    fn summary_with_loops() {
293        let mut g = Graph::with_vertices(3);
294        g.add_edge(0, 1).unwrap();
295        g.add_edge(1, 1).unwrap(); // self-loop
296
297        let s = graph_summary(&g).unwrap();
298        assert!(s.has_loops);
299    }
300
301    #[test]
302    fn summary_with_multiedges() {
303        let mut g = Graph::with_vertices(2);
304        g.add_edge(0, 1).unwrap();
305        g.add_edge(0, 1).unwrap(); // parallel edge
306
307        let s = graph_summary(&g).unwrap();
308        assert!(s.has_multiple);
309    }
310
311    #[test]
312    fn summary_display_format() {
313        let mut g = Graph::with_vertices(4);
314        g.add_edge(0, 1).unwrap();
315        g.add_edge(1, 2).unwrap();
316        g.add_edge(2, 3).unwrap();
317        g.add_edge(3, 0).unwrap();
318
319        let s = graph_summary(&g).unwrap();
320        let display = format!("{s}");
321        assert!(display.contains("Undirected graph"));
322        assert!(display.contains("4 vertices"));
323        assert!(display.contains("4 edges"));
324        assert!(display.contains("connected"));
325        assert!(display.contains("density="));
326    }
327
328    #[test]
329    fn summary_string_format() {
330        let mut g = Graph::with_vertices(3);
331        g.add_edge(0, 1).unwrap();
332        g.add_edge(1, 2).unwrap();
333
334        let out = graph_summary_string(&g).unwrap();
335        assert!(out.contains("Vertices: 3"));
336        assert!(out.contains("Edges: 2"));
337        assert!(out.contains("Connected: yes"));
338        assert!(out.contains("Simple: yes"));
339    }
340
341    #[test]
342    fn summary_singleton() {
343        let g = Graph::with_vertices(1);
344        let s = graph_summary(&g).unwrap();
345        assert_eq!(s.vcount, 1);
346        assert_eq!(s.ecount, 0);
347        assert!(s.connected);
348        assert_eq!(s.component_count, 1);
349        assert_eq!(s.density, None);
350        assert_eq!(s.min_degree, 0);
351        assert_eq!(s.max_degree, 0);
352    }
353}