Skip to main content

rust_igraph/algorithms/properties/
basic.rs

1//! Basic graph metrics — density and mean shortest-path length (ALGO-PR-003).
2//!
3//! Counterparts of:
4//! - `igraph_density()`             from `references/igraph/src/properties/basic_properties.c:71`
5//! - `igraph_average_path_length()` from `references/igraph/src/paths/shortest_paths.c:329`
6//!
7//! Phase-1 minimal slice:
8//! - **Density** matches upstream's default `loops = false` mode:
9//!   `m / (n*(n-1)/2)` for undirected, `m / (n*(n-1))` for directed.
10//!   Self-loops are not subtracted; if the graph has self-loops the
11//!   result may exceed 1 (this matches upstream — caller's responsibility).
12//! - **Mean distance** matches upstream's `unconn = true` default —
13//!   unreachable pairs are skipped from the average. Returns `None`
14//!   when no connected pairs exist (graph too small or fully disconnected).
15
16use crate::algorithms::paths::distances::distances;
17use crate::core::{Graph, IgraphResult};
18
19/// Edge density of `graph`. Counterpart of
20/// `igraph_density(_, NULL_weights, _, /*loops=*/false)`.
21///
22/// Returns `None` when:
23/// - `vcount() == 0` (matches upstream's `IGRAPH_NAN`).
24/// - `vcount() == 1` and `loops=false` (no possible edges).
25///
26/// Self-loops in the input graph are *not* removed; they inflate the
27/// numerator. Use `simplify` (when it lands) to drop them first.
28///
29/// # Examples
30///
31/// ```
32/// use rust_igraph::{Graph, density};
33///
34/// // K3 (triangle) on 3 vertices, undirected: 3 edges, max = 3 → 1.0.
35/// let mut g = Graph::with_vertices(3);
36/// g.add_edge(0, 1).unwrap();
37/// g.add_edge(1, 2).unwrap();
38/// g.add_edge(2, 0).unwrap();
39/// assert_eq!(density(&g).unwrap(), Some(1.0));
40/// ```
41pub fn density(graph: &Graph) -> IgraphResult<Option<f64>> {
42    let n = graph.vcount();
43    if n == 0 || n == 1 {
44        return Ok(None);
45    }
46    let m = graph.ecount();
47    let directed = graph.is_directed();
48
49    // Sub-1 worst case for n: u32::try_from(n) is fine since n is u32 by type.
50    let n_f = f64::from(n);
51    #[allow(clippy::cast_precision_loss)]
52    let m_f = m as f64;
53
54    // Match upstream's float ordering exactly so f64 results agree to
55    // the bit. See `igraph_density()` in
56    // `references/igraph/src/properties/basic_properties.c:99-103`.
57    let result = if directed {
58        m_f / n_f / (n_f - 1.0)
59    } else {
60        m_f / n_f * 2.0 / (n_f - 1.0)
61    };
62    Ok(Some(result))
63}
64
65/// Mean unweighted shortest-path length over all reachable ordered pairs.
66/// Counterpart of `igraph_average_path_length(_, NULL_weights, _, _, /*directed=*/true, /*unconn=*/true)`.
67///
68/// Returns `None` when no connected pairs exist (e.g. `vcount() < 2`,
69/// or all pairs are unreachable). Edge directions matter for directed
70/// graphs (uses `distances` which follows out-edges).
71///
72/// # Examples
73///
74/// ```
75/// use rust_igraph::{Graph, mean_distance};
76///
77/// // Path 0-1-2-3-4: ordered pairs at distance 1, 2, 3, 4 (4 each direction
78/// // → 8, 6, 4, 2 contributions); 20 pairs total. Mean = 40/20 = 2.0.
79/// let mut g = Graph::with_vertices(5);
80/// for i in 0..4u32 { g.add_edge(i, i + 1).unwrap(); }
81/// assert_eq!(mean_distance(&g).unwrap(), Some(2.0));
82/// ```
83pub fn mean_distance(graph: &Graph) -> IgraphResult<Option<f64>> {
84    let n = graph.vcount();
85    if n < 2 {
86        return Ok(None);
87    }
88    let mut sum: u64 = 0;
89    let mut count: u64 = 0;
90    for v in 0..n {
91        let d = distances(graph, v)?;
92        let v_us = v as usize;
93        for (target, &val) in d.iter().enumerate() {
94            if target == v_us {
95                continue;
96            }
97            if let Some(dist) = val {
98                sum += u64::from(dist);
99                count += 1;
100            }
101        }
102    }
103    if count == 0 {
104        return Ok(None);
105    }
106    #[allow(clippy::cast_precision_loss)]
107    let mean = (sum as f64) / (count as f64);
108    Ok(Some(mean))
109}
110
111/// Mean degree of the graph.
112///
113/// For directed graphs, returns `ecount / vcount` (the average out-degree,
114/// which equals the average in-degree). For undirected graphs, returns
115/// `2 * ecount / vcount`.
116///
117/// If `count_loops` is false, self-loop edges are excluded from the count.
118///
119/// Returns `None` for graphs with no vertices.
120///
121/// # Examples
122///
123/// ```
124/// use rust_igraph::{Graph, mean_degree};
125///
126/// let mut g = Graph::with_vertices(4);
127/// g.add_edge(0, 1).unwrap();
128/// g.add_edge(1, 2).unwrap();
129/// g.add_edge(2, 3).unwrap();
130/// // 3 edges, 4 vertices, undirected → mean = 2*3/4 = 1.5
131/// assert!((mean_degree(&g, true).unwrap().unwrap() - 1.5).abs() < 1e-10);
132/// ```
133pub fn mean_degree(graph: &Graph, count_loops: bool) -> IgraphResult<Option<f64>> {
134    let n = graph.vcount();
135    if n == 0 {
136        return Ok(None);
137    }
138
139    let mut ecount = graph.ecount();
140
141    if !count_loops {
142        let loop_count = crate::algorithms::properties::multiplicity::count_loops(graph)?;
143        ecount -= loop_count;
144    }
145
146    let directed = graph.is_directed();
147    let n_f = f64::from(n);
148    #[allow(clippy::cast_precision_loss)]
149    let m_f = ecount as f64;
150
151    let result = if directed { m_f / n_f } else { 2.0 * m_f / n_f };
152    Ok(Some(result))
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn density_empty_graph_is_none() {
161        let g = Graph::with_vertices(0);
162        assert_eq!(density(&g).unwrap(), None);
163    }
164
165    #[test]
166    fn density_singleton_is_none() {
167        let g = Graph::with_vertices(1);
168        assert_eq!(density(&g).unwrap(), None);
169    }
170
171    #[test]
172    fn density_complete_undirected_is_one() {
173        let mut g = Graph::with_vertices(4);
174        for u in 0..4u32 {
175            for v in (u + 1)..4 {
176                g.add_edge(u, v).unwrap();
177            }
178        }
179        assert_eq!(density(&g).unwrap(), Some(1.0));
180    }
181
182    #[test]
183    fn density_no_edges_is_zero() {
184        let g = Graph::with_vertices(5);
185        assert_eq!(density(&g).unwrap(), Some(0.0));
186    }
187
188    #[test]
189    fn density_directed_complete_is_one() {
190        // Directed complete graph (no self-loops): n*(n-1) edges = max.
191        let mut g = Graph::new(3, true).unwrap();
192        for u in 0..3u32 {
193            for v in 0..3u32 {
194                if u != v {
195                    g.add_edge(u, v).unwrap();
196                }
197            }
198        }
199        assert_eq!(density(&g).unwrap(), Some(1.0));
200    }
201
202    #[test]
203    fn density_path_5_is_2_over_10() {
204        // 4 edges among C(5,2)=10 possible → 0.4.
205        let mut g = Graph::with_vertices(5);
206        for i in 0..4 {
207            g.add_edge(i, i + 1).unwrap();
208        }
209        assert_eq!(density(&g).unwrap(), Some(0.4));
210    }
211
212    #[test]
213    fn mean_distance_n_lt_2_is_none() {
214        let g = Graph::with_vertices(0);
215        assert_eq!(mean_distance(&g).unwrap(), None);
216        let g = Graph::with_vertices(1);
217        assert_eq!(mean_distance(&g).unwrap(), None);
218    }
219
220    #[test]
221    fn mean_distance_isolated_vertices_is_none() {
222        let g = Graph::with_vertices(5);
223        assert_eq!(mean_distance(&g).unwrap(), None);
224    }
225
226    #[test]
227    fn mean_distance_path_5_is_2() {
228        let mut g = Graph::with_vertices(5);
229        for i in 0..4 {
230            g.add_edge(i, i + 1).unwrap();
231        }
232        assert_eq!(mean_distance(&g).unwrap(), Some(2.0));
233    }
234
235    #[test]
236    fn mean_distance_triangle_is_1() {
237        let mut g = Graph::with_vertices(3);
238        g.add_edge(0, 1).unwrap();
239        g.add_edge(1, 2).unwrap();
240        g.add_edge(2, 0).unwrap();
241        assert_eq!(mean_distance(&g).unwrap(), Some(1.0));
242    }
243
244    #[test]
245    fn mean_distance_two_components_only_within_components() {
246        // {0-1-2} and {3-4}: connected pairs are within each component.
247        // {0-1-2}: 6 ordered pairs (0↔1=1, 0↔2=2, 1↔2=1) → sum 2*(1+2+1) = 8.
248        // {3-4}: 2 ordered pairs (3↔4) → sum 2.
249        // Total: 8 connected pairs, sum 10, mean = 10/8 = 1.25.
250        let mut g = Graph::with_vertices(5);
251        g.add_edge(0, 1).unwrap();
252        g.add_edge(1, 2).unwrap();
253        g.add_edge(3, 4).unwrap();
254        assert_eq!(mean_distance(&g).unwrap(), Some(1.25));
255    }
256
257    #[test]
258    fn mean_distance_directed_uses_out_edges() {
259        // 0 -> 1 -> 2: only 3 ordered reachable pairs: (0,1)=1, (0,2)=2, (1,2)=1.
260        // Sum = 4, count = 3, mean = 4/3.
261        let mut g = Graph::new(3, true).unwrap();
262        g.add_edge(0, 1).unwrap();
263        g.add_edge(1, 2).unwrap();
264        let four_thirds = 4.0_f64 / 3.0;
265        assert_eq!(mean_distance(&g).unwrap(), Some(four_thirds));
266    }
267
268    #[test]
269    fn mean_degree_empty_graph() {
270        let g = Graph::with_vertices(0);
271        assert_eq!(mean_degree(&g, true).unwrap(), None);
272    }
273
274    #[test]
275    fn mean_degree_no_edges() {
276        let g = Graph::with_vertices(5);
277        let md = mean_degree(&g, true).unwrap().unwrap();
278        assert!((md - 0.0).abs() < 1e-10);
279    }
280
281    #[test]
282    fn mean_degree_undirected_path() {
283        let mut g = Graph::with_vertices(4);
284        g.add_edge(0, 1).unwrap();
285        g.add_edge(1, 2).unwrap();
286        g.add_edge(2, 3).unwrap();
287        // 3 edges, 4 vertices, undirected: 2*3/4 = 1.5
288        let md = mean_degree(&g, true).unwrap().unwrap();
289        assert!((md - 1.5).abs() < 1e-10);
290    }
291
292    #[test]
293    fn mean_degree_directed() {
294        let mut g = Graph::new(3, true).unwrap();
295        g.add_edge(0, 1).unwrap();
296        g.add_edge(0, 2).unwrap();
297        g.add_edge(1, 2).unwrap();
298        // 3 edges, 3 vertices, directed: 3/3 = 1.0
299        let md = mean_degree(&g, true).unwrap().unwrap();
300        assert!((md - 1.0).abs() < 1e-10);
301    }
302
303    #[test]
304    fn mean_degree_with_self_loops() {
305        let mut g = Graph::with_vertices(3);
306        g.add_edge(0, 1).unwrap();
307        g.add_edge(1, 2).unwrap();
308        g.add_edge(0, 0).unwrap(); // self-loop
309        // With loops: 3 edges, undirected: 2*3/3 = 2.0
310        let md_with = mean_degree(&g, true).unwrap().unwrap();
311        assert!((md_with - 2.0).abs() < 1e-10);
312        // Without loops: 2 edges, undirected: 2*2/3 = 4/3
313        let md_without = mean_degree(&g, false).unwrap().unwrap();
314        assert!((md_without - 4.0 / 3.0).abs() < 1e-10);
315    }
316
317    #[test]
318    fn mean_degree_complete_undirected() {
319        // K4: 6 edges, 4 vertices: 2*6/4 = 3.0
320        let mut g = Graph::with_vertices(4);
321        for u in 0..4u32 {
322            for v in (u + 1)..4 {
323                g.add_edge(u, v).unwrap();
324            }
325        }
326        let md = mean_degree(&g, true).unwrap().unwrap();
327        assert!((md - 3.0).abs() < 1e-10);
328    }
329}