use std::collections::VecDeque;
use crate::algorithms::paths::dijkstra::dijkstra_distances;
use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
fn validate_weights(graph: &Graph, weights: &[f64]) -> IgraphResult<()> {
let m = graph.ecount();
if weights.len() != m {
return Err(IgraphError::InvalidArgument(format!(
"weights vector size ({}) differs from edge count ({})",
weights.len(),
m
)));
}
for (e, &w) in weights.iter().enumerate() {
if w.is_nan() {
return Err(IgraphError::InvalidArgument(format!(
"weight at edge {e} is NaN"
)));
}
}
Ok(())
}
fn compute_potentials(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<f64>> {
let n = graph.vcount();
let n_usize = n as usize;
let mut dist: Vec<f64> = vec![0.0; n_usize];
let mut queue: VecDeque<VertexId> = VecDeque::with_capacity(n_usize);
let mut in_queue: Vec<bool> = vec![true; n_usize];
let mut num_queued: Vec<u32> = vec![0; n_usize];
for v in 0..n {
queue.push_back(v);
}
while let Some(j) = queue.pop_front() {
let j_idx = j as usize;
in_queue[j_idx] = false;
num_queued[j_idx] = num_queued[j_idx]
.checked_add(1)
.ok_or(IgraphError::Internal("num_queued overflow"))?;
if num_queued[j_idx] > n {
return Err(IgraphError::InvalidArgument(
"negative cycle detected while computing Johnson potentials".to_string(),
));
}
let incidents = graph.incident(j)?;
for eid in incidents {
let w = weights[eid as usize];
if w == f64::INFINITY {
continue;
}
let target = graph.edge_other(eid, j)?;
let target_idx = target as usize;
let altdist = dist[j_idx] + w;
if altdist < dist[target_idx] {
dist[target_idx] = altdist;
if !in_queue[target_idx] {
in_queue[target_idx] = true;
queue.push_back(target);
}
}
}
}
Ok(dist)
}
pub fn johnson_distances(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<Vec<Option<f64>>>> {
validate_weights(graph, weights)?;
let vcount = graph.vcount();
let has_negative = weights.iter().any(|&w| w < 0.0);
if !has_negative {
let mut out = Vec::with_capacity(vcount as usize);
for v in 0..vcount {
out.push(dijkstra_distances(graph, v, weights)?);
}
return Ok(out);
}
if !graph.is_directed() {
return Err(IgraphError::InvalidArgument(
"Johnson's algorithm cannot be applied to undirected graphs with negative weights (would form a negative cycle)".to_string(),
));
}
let potentials = compute_potentials(graph, weights)?;
let ecount = graph.ecount();
let mut reweighted: Vec<f64> = Vec::with_capacity(ecount);
for (e, &orig_w) in weights.iter().enumerate() {
let eid = u32::try_from(e)
.map_err(|_| IgraphError::Internal("edge id exceeds u32::MAX in johnson"))?;
let (from, to) = graph.edge(eid)?;
let mut new_w = orig_w + potentials[from as usize] - potentials[to as usize];
if new_w < 0.0 {
new_w = 0.0;
}
reweighted.push(new_w);
}
let mut out = Vec::with_capacity(vcount as usize);
for src in 0..vcount {
let dprime = dijkstra_distances(graph, src, &reweighted)?;
let mut row: Vec<Option<f64>> = Vec::with_capacity(vcount as usize);
for (target, dp) in dprime.into_iter().enumerate() {
let recovered = dp.map(|d| d - potentials[src as usize] + potentials[target]);
row.push(recovered);
}
out.push(row);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_graph_returns_empty_matrix() {
let g = Graph::with_vertices(0);
let d = johnson_distances(&g, &[]).unwrap();
assert!(d.is_empty());
}
#[test]
fn no_edges_diagonal_zero_off_diagonal_none() {
let g = Graph::with_vertices(3);
let d = johnson_distances(&g, &[]).unwrap();
assert_eq!(d.len(), 3);
for (u, row) in d.iter().enumerate() {
for (v, entry) in row.iter().enumerate() {
if u == v {
assert_eq!(entry, &Some(0.0));
} else {
assert_eq!(entry, &None);
}
}
}
}
#[test]
fn positive_weights_match_pairwise_dijkstra() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(1, 2).unwrap();
let weights = [1.0, 4.0, 2.0];
let d = johnson_distances(&g, &weights).unwrap();
assert_eq!(d[0], vec![Some(0.0), Some(1.0), Some(3.0)]);
assert_eq!(d[1], vec![Some(1.0), Some(0.0), Some(2.0)]);
assert_eq!(d[2], vec![Some(3.0), Some(2.0), Some(0.0)]);
}
#[test]
fn directed_with_negative_edge_diamond() {
let mut g = Graph::new(4, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(1, 3).unwrap();
g.add_edge(2, 3).unwrap();
let weights = [3.0, 1.0, -2.0, 4.0];
let d = johnson_distances(&g, &weights).unwrap();
assert_eq!(d[0], vec![Some(0.0), Some(3.0), Some(1.0), Some(1.0)]);
assert_eq!(d[1], vec![None, Some(0.0), None, Some(-2.0)]);
assert_eq!(d[2], vec![None, None, Some(0.0), Some(4.0)]);
assert_eq!(d[3], vec![None, None, None, Some(0.0)]);
}
#[test]
fn unreachable_components_yield_none() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(2, 3).unwrap();
let d = johnson_distances(&g, &[1.0, 2.0]).unwrap();
assert_eq!(d[0][0], Some(0.0));
assert_eq!(d[0][1], Some(1.0));
assert_eq!(d[0][2], None);
assert_eq!(d[0][3], None);
assert_eq!(d[2][3], Some(2.0));
}
#[test]
fn undirected_with_negative_weight_errors() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
let err = johnson_distances(&g, &[-1.0]).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn negative_cycle_in_directed_errors() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
let err = johnson_distances(&g, &[1.0, 1.0, -3.0]).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn weights_size_mismatch_errors() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
let err = johnson_distances(&g, &[1.0, 2.0]).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn nan_weight_errors() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
let err = johnson_distances(&g, &[f64::NAN]).unwrap_err();
assert!(matches!(err, IgraphError::InvalidArgument(_)));
}
#[test]
fn zero_weights_ok() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let d = johnson_distances(&g, &[0.0, 0.0]).unwrap();
assert_eq!(d[0], vec![Some(0.0), Some(0.0), Some(0.0)]);
}
#[test]
fn directed_negative_edges_no_cycle_complex() {
let mut g = Graph::new(4, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(1, 3).unwrap();
g.add_edge(2, 1).unwrap();
g.add_edge(2, 3).unwrap();
let weights = [5.0, -3.0, 1.0, 4.0, 2.0];
let d = johnson_distances(&g, &weights).unwrap();
assert_eq!(d[0][0], Some(0.0));
assert_eq!(d[0][1], Some(1.0));
assert_eq!(d[0][2], Some(-3.0));
assert_eq!(d[0][3], Some(-1.0));
}
}