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#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn density_empty_graph_is_none() {
117 let g = Graph::with_vertices(0);
118 assert_eq!(density(&g).unwrap(), None);
119 }
120
121 #[test]
122 fn density_singleton_is_none() {
123 let g = Graph::with_vertices(1);
124 assert_eq!(density(&g).unwrap(), None);
125 }
126
127 #[test]
128 fn density_complete_undirected_is_one() {
129 let mut g = Graph::with_vertices(4);
130 for u in 0..4u32 {
131 for v in (u + 1)..4 {
132 g.add_edge(u, v).unwrap();
133 }
134 }
135 assert_eq!(density(&g).unwrap(), Some(1.0));
136 }
137
138 #[test]
139 fn density_no_edges_is_zero() {
140 let g = Graph::with_vertices(5);
141 assert_eq!(density(&g).unwrap(), Some(0.0));
142 }
143
144 #[test]
145 fn density_directed_complete_is_one() {
146 // Directed complete graph (no self-loops): n*(n-1) edges = max.
147 let mut g = Graph::new(3, true).unwrap();
148 for u in 0..3u32 {
149 for v in 0..3u32 {
150 if u != v {
151 g.add_edge(u, v).unwrap();
152 }
153 }
154 }
155 assert_eq!(density(&g).unwrap(), Some(1.0));
156 }
157
158 #[test]
159 fn density_path_5_is_2_over_10() {
160 // 4 edges among C(5,2)=10 possible → 0.4.
161 let mut g = Graph::with_vertices(5);
162 for i in 0..4 {
163 g.add_edge(i, i + 1).unwrap();
164 }
165 assert_eq!(density(&g).unwrap(), Some(0.4));
166 }
167
168 #[test]
169 fn mean_distance_n_lt_2_is_none() {
170 let g = Graph::with_vertices(0);
171 assert_eq!(mean_distance(&g).unwrap(), None);
172 let g = Graph::with_vertices(1);
173 assert_eq!(mean_distance(&g).unwrap(), None);
174 }
175
176 #[test]
177 fn mean_distance_isolated_vertices_is_none() {
178 let g = Graph::with_vertices(5);
179 assert_eq!(mean_distance(&g).unwrap(), None);
180 }
181
182 #[test]
183 fn mean_distance_path_5_is_2() {
184 let mut g = Graph::with_vertices(5);
185 for i in 0..4 {
186 g.add_edge(i, i + 1).unwrap();
187 }
188 assert_eq!(mean_distance(&g).unwrap(), Some(2.0));
189 }
190
191 #[test]
192 fn mean_distance_triangle_is_1() {
193 let mut g = Graph::with_vertices(3);
194 g.add_edge(0, 1).unwrap();
195 g.add_edge(1, 2).unwrap();
196 g.add_edge(2, 0).unwrap();
197 assert_eq!(mean_distance(&g).unwrap(), Some(1.0));
198 }
199
200 #[test]
201 fn mean_distance_two_components_only_within_components() {
202 // {0-1-2} and {3-4}: connected pairs are within each component.
203 // {0-1-2}: 6 ordered pairs (0↔1=1, 0↔2=2, 1↔2=1) → sum 2*(1+2+1) = 8.
204 // {3-4}: 2 ordered pairs (3↔4) → sum 2.
205 // Total: 8 connected pairs, sum 10, mean = 10/8 = 1.25.
206 let mut g = Graph::with_vertices(5);
207 g.add_edge(0, 1).unwrap();
208 g.add_edge(1, 2).unwrap();
209 g.add_edge(3, 4).unwrap();
210 assert_eq!(mean_distance(&g).unwrap(), Some(1.25));
211 }
212
213 #[test]
214 fn mean_distance_directed_uses_out_edges() {
215 // 0 -> 1 -> 2: only 3 ordered reachable pairs: (0,1)=1, (0,2)=2, (1,2)=1.
216 // Sum = 4, count = 3, mean = 4/3.
217 let mut g = Graph::new(3, true).unwrap();
218 g.add_edge(0, 1).unwrap();
219 g.add_edge(1, 2).unwrap();
220 let four_thirds = 4.0_f64 / 3.0;
221 assert_eq!(mean_distance(&g).unwrap(), Some(four_thirds));
222 }
223}