rust_igraph/algorithms/paths/
floyd_warshall.rs1use crate::core::{Graph, IgraphError, IgraphResult};
19
20pub fn floyd_warshall_distances(
56 graph: &Graph,
57 weights: Option<&[f64]>,
58) -> IgraphResult<Vec<Vec<Option<f64>>>> {
59 let n = graph.vcount();
60 let m = graph.ecount();
61 let n_us = n as usize;
62 let directed = graph.is_directed();
63
64 if let Some(ws) = weights {
65 if ws.len() != m {
66 return Err(IgraphError::InvalidArgument(format!(
67 "weights vector size ({}) differs from edge count ({})",
68 ws.len(),
69 m
70 )));
71 }
72 for (e, &w) in ws.iter().enumerate() {
73 if w.is_nan() {
74 return Err(IgraphError::InvalidArgument(format!(
75 "weight at edge {e} is NaN"
76 )));
77 }
78 }
79 }
80
81 let mut dist = vec![f64::INFINITY; n_us * n_us];
85 for v in 0..n_us {
86 dist[v * n_us + v] = 0.0;
87 }
88
89 let m_u32 = u32::try_from(m).map_err(|_| IgraphError::Internal("edge count overflows u32"))?;
91 for eid in 0..m_u32 {
92 let (src, tgt) = graph.edge(eid)?;
93 let edge_w = weights.map_or(1.0, |ws| ws[eid as usize]);
94
95 if edge_w < 0.0 {
96 if !directed {
97 return Err(IgraphError::InvalidArgument(format!(
98 "negative edge weight ({edge_w}) on undirected graph: every undirected edge induces a 2-cycle, so this is a negative cycle"
99 )));
100 }
101 if src == tgt {
102 return Err(IgraphError::InvalidArgument(format!(
103 "self-loop with negative weight ({edge_w}) at vertex {src} is a negative cycle"
104 )));
105 }
106 }
107 if edge_w == f64::INFINITY {
108 continue;
110 }
111
112 let s_us = src as usize;
113 let t_us = tgt as usize;
114 let fwd = s_us * n_us + t_us;
116 if edge_w < dist[fwd] {
117 dist[fwd] = edge_w;
118 }
119 if !directed {
120 let rev = t_us * n_us + s_us;
121 if edge_w < dist[rev] {
122 dist[rev] = edge_w;
123 }
124 }
125 }
126
127 if n_us > 1 {
128 for k in 0..n_us {
130 for i in 0..n_us {
131 let dik = dist[i * n_us + k];
132 if dik.is_infinite() {
133 continue;
134 }
135 for j in 0..n_us {
136 let dkj = dist[k * n_us + j];
137 if dkj.is_infinite() {
138 continue;
139 }
140 let candidate = dik + dkj;
141 let cell = i * n_us + j;
142 if candidate < dist[cell] {
143 dist[cell] = candidate;
144 }
145 }
146 let diag = i * n_us + i;
147 if dist[diag] < 0.0 {
148 return Err(IgraphError::InvalidArgument(format!(
149 "negative cycle found while computing Floyd-Warshall distances (vertex {i} closes a cycle of weight {})",
150 dist[diag]
151 )));
152 }
153 }
154 }
155 }
156
157 let mut out = Vec::with_capacity(n_us);
158 for i in 0..n_us {
159 let mut row = Vec::with_capacity(n_us);
160 for j in 0..n_us {
161 let d = dist[i * n_us + j];
162 row.push(if d.is_infinite() { None } else { Some(d) });
163 }
164 out.push(row);
165 }
166 Ok(out)
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 fn make_directed(n: u32) -> Graph {
174 Graph::new(n, true).expect("directed graph")
175 }
176
177 #[test]
178 fn empty_graph_returns_empty_matrix() {
179 let g = Graph::with_vertices(0);
180 let d = floyd_warshall_distances(&g, None).unwrap();
181 assert!(d.is_empty());
182 }
183
184 #[test]
185 fn singleton_distance_zero() {
186 let g = Graph::with_vertices(1);
187 let d = floyd_warshall_distances(&g, None).unwrap();
188 assert_eq!(d, vec![vec![Some(0.0)]]);
189 }
190
191 #[test]
192 fn unweighted_triangle_unit_distances() {
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(0, 2).unwrap();
197 let d = floyd_warshall_distances(&g, None).unwrap();
198 for (i, row) in d.iter().enumerate() {
199 assert_eq!(row[i], Some(0.0));
200 for (j, cell) in row.iter().enumerate() {
201 if i != j {
202 assert_eq!(*cell, Some(1.0));
203 }
204 }
205 }
206 }
207
208 #[test]
209 fn weighted_triangle_picks_shortcut() {
210 let mut g = Graph::with_vertices(3);
211 g.add_edge(0, 1).unwrap(); g.add_edge(0, 2).unwrap(); g.add_edge(1, 2).unwrap(); let d = floyd_warshall_distances(&g, Some(&[1.0, 4.0, 2.0])).unwrap();
215 assert_eq!(d[0][2], Some(3.0)); assert_eq!(d[2][0], Some(3.0)); }
218
219 #[test]
220 fn unreachable_pair_yields_none() {
221 let mut g = Graph::with_vertices(3);
222 g.add_edge(0, 1).unwrap();
223 let d = floyd_warshall_distances(&g, None).unwrap();
224 assert_eq!(d[0][2], None);
225 assert_eq!(d[2][0], None);
226 assert_eq!(d[1][2], None);
227 }
228
229 #[test]
230 fn directed_graph_respects_orientation() {
231 let mut g = make_directed(3);
232 g.add_edge(0, 1).unwrap();
233 g.add_edge(1, 2).unwrap();
234 let d = floyd_warshall_distances(&g, None).unwrap();
235 assert_eq!(d[0][2], Some(2.0));
236 assert_eq!(d[2][0], None); }
238
239 #[test]
240 fn directed_graph_with_negative_weight() {
241 let mut g = make_directed(3);
243 g.add_edge(0, 1).unwrap();
244 g.add_edge(1, 2).unwrap();
245 let d = floyd_warshall_distances(&g, Some(&[-5.0, 10.0])).unwrap();
246 assert_eq!(d[0][2], Some(5.0));
247 assert_eq!(d[0][1], Some(-5.0));
248 }
249
250 #[test]
251 fn directed_negative_cycle_errors() {
252 let mut g = make_directed(3);
254 g.add_edge(0, 1).unwrap();
255 g.add_edge(1, 2).unwrap();
256 g.add_edge(2, 0).unwrap();
257 let result = floyd_warshall_distances(&g, Some(&[-1.0, -1.0, -1.0]));
258 assert!(result.is_err());
259 }
260
261 #[test]
262 fn directed_self_loop_negative_errors() {
263 let mut g = make_directed(2);
264 g.add_edge(0, 0).unwrap();
265 let result = floyd_warshall_distances(&g, Some(&[-0.5]));
266 assert!(result.is_err());
267 }
268
269 #[test]
270 fn undirected_negative_weight_rejected() {
271 let mut g = Graph::with_vertices(2);
272 g.add_edge(0, 1).unwrap();
273 let result = floyd_warshall_distances(&g, Some(&[-1.0]));
274 assert!(result.is_err());
275 }
276
277 #[test]
278 fn weights_size_mismatch_errors() {
279 let mut g = Graph::with_vertices(2);
280 g.add_edge(0, 1).unwrap();
281 let result = floyd_warshall_distances(&g, Some(&[1.0, 2.0]));
282 assert!(result.is_err());
283 }
284
285 #[test]
286 fn nan_weight_errors() {
287 let mut g = Graph::with_vertices(2);
288 g.add_edge(0, 1).unwrap();
289 let result = floyd_warshall_distances(&g, Some(&[f64::NAN]));
290 assert!(result.is_err());
291 }
292
293 #[test]
294 fn infinity_weight_silently_ignored() {
295 let mut g = Graph::with_vertices(3);
298 g.add_edge(0, 1).unwrap();
299 g.add_edge(0, 2).unwrap();
300 g.add_edge(1, 2).unwrap();
301 let d = floyd_warshall_distances(&g, Some(&[f64::INFINITY, 1.0, 1.0])).unwrap();
302 assert_eq!(d[0][1], Some(2.0));
303 }
304
305 #[test]
306 fn parallel_edges_pick_minimum() {
307 let mut g = Graph::with_vertices(2);
309 g.add_edge(0, 1).unwrap();
310 g.add_edge(0, 1).unwrap();
311 let d = floyd_warshall_distances(&g, Some(&[5.0, 1.0])).unwrap();
312 assert_eq!(d[0][1], Some(1.0));
313 }
314
315 #[test]
316 fn diagonal_always_zero_for_isolated_vertex() {
317 let g = Graph::with_vertices(5);
318 let d = floyd_warshall_distances(&g, None).unwrap();
319 for (i, row) in d.iter().enumerate() {
320 assert_eq!(row[i], Some(0.0));
321 for (j, cell) in row.iter().enumerate() {
322 if i != j {
323 assert_eq!(*cell, None);
324 }
325 }
326 }
327 }
328
329 #[test]
330 fn unit_weights_match_unweighted() {
331 let mut g = Graph::with_vertices(4);
332 g.add_edge(0, 1).unwrap();
333 g.add_edge(1, 2).unwrap();
334 g.add_edge(2, 3).unwrap();
335 g.add_edge(0, 3).unwrap();
336 let unweighted = floyd_warshall_distances(&g, None).unwrap();
337 let weighted = floyd_warshall_distances(&g, Some(&[1.0, 1.0, 1.0, 1.0])).unwrap();
338 assert_eq!(unweighted, weighted);
339 }
340}