use std::cmp::{Ordering, Reverse};
use std::collections::BinaryHeap;
use rusqlite::Connection;
use crate::error::Result;
use super::csr::CsrGraph;
pub const SHORTEST_PATH_W_MIN: f64 = 1e-9;
#[derive(Clone, Copy, PartialEq)]
struct OrderedFloat(f64);
impl Eq for OrderedFloat {}
impl Ord for OrderedFloat {
fn cmp(&self, other: &Self) -> Ordering {
self.0.total_cmp(&other.0)
}
}
impl PartialOrd for OrderedFloat {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn edge_cost(weight: f64) -> Option<f64> {
if weight < SHORTEST_PATH_W_MIN {
return None;
}
let w = weight.min(1.0);
Some(-w.ln())
}
pub fn dijkstra(g: &CsrGraph, src: u32) -> Vec<(u32, f64)> {
let n = g.node_count();
let mut result = Vec::new();
if (src as usize) >= n {
return result;
}
let mut dist = vec![f64::INFINITY; n];
let mut visited = vec![false; n];
let mut heap: BinaryHeap<(Reverse<OrderedFloat>, u32)> = BinaryHeap::new();
dist[src as usize] = 0.0;
heap.push((Reverse(OrderedFloat(0.0)), src));
while let Some((Reverse(OrderedFloat(d)), u)) = heap.pop() {
if visited[u as usize] || d > dist[u as usize] {
continue;
}
visited[u as usize] = true;
result.push((u, d));
for (v, w) in g.out_neighbors(u) {
let Some(cost) = edge_cost(w) else {
continue;
};
let nd = d + cost;
if nd < dist[v as usize] {
dist[v as usize] = nd;
heap.push((Reverse(OrderedFloat(nd)), v));
}
}
}
result.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
result
}
pub fn shortest_path(g: &CsrGraph, src: u32, dst: u32) -> Option<Vec<u32>> {
let n = g.node_count();
if (src as usize) >= n || (dst as usize) >= n {
return None;
}
let mut dist = vec![f64::INFINITY; n];
let mut prev: Vec<Option<u32>> = vec![None; n];
let mut visited = vec![false; n];
let mut heap: BinaryHeap<(Reverse<OrderedFloat>, u32)> = BinaryHeap::new();
dist[src as usize] = 0.0;
heap.push((Reverse(OrderedFloat(0.0)), src));
while let Some((Reverse(OrderedFloat(d)), u)) = heap.pop() {
if u == dst {
break;
}
if visited[u as usize] {
continue;
}
visited[u as usize] = true;
for (v, w) in g.out_neighbors(u) {
let Some(cost) = edge_cost(w) else {
continue;
};
let nd = d + cost;
if nd < dist[v as usize] {
dist[v as usize] = nd;
prev[v as usize] = Some(u);
heap.push((Reverse(OrderedFloat(nd)), v));
}
}
}
if dist[dst as usize] == f64::INFINITY {
return None;
}
let mut path = vec![dst];
let mut cur = dst;
while let Some(p) = prev[cur as usize] {
path.push(p);
cur = p;
}
path.reverse();
Some(path)
}
pub fn shortest_path_ids(conn: &Connection, src: &str, dst: &str) -> Result<Option<Vec<String>>> {
let g = CsrGraph::build_csr(conn)?;
let (Some(s), Some(d)) = (g.node_index(src), g.node_index(dst)) else {
return Ok(None);
};
Ok(
shortest_path(&g, s, d)
.map(|path| path.iter().map(|&i| g.node_id(i).to_string()).collect()),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::algo::csr::CsrGraph;
use crate::graph::types::GraphEdge;
fn edge(id: &str, s: &str, t: &str, w: f64) -> GraphEdge {
GraphEdge {
id: id.into(),
source: s.into(),
target: t.into(),
relation: "related".into(),
weight: w,
ts: "2026-01-01T00:00:00Z".into(),
}
}
fn graph(nodes: &[&str], edges: &[GraphEdge]) -> CsrGraph {
let node_ids: Vec<String> = nodes.iter().map(|s| s.to_string()).collect();
CsrGraph::from_edges(&node_ids, edges)
}
#[test]
fn strong_two_hop_beats_weak_direct() {
let g = graph(
&["A", "B", "C"],
&[
edge("e1", "A", "B", 0.5),
edge("e2", "B", "C", 0.5),
edge("e3", "A", "C", 0.1),
],
);
let a = g.node_index("A").unwrap();
let b = g.node_index("B").unwrap();
let c = g.node_index("C").unwrap();
let path = shortest_path(&g, a, c).expect("path exists");
assert_eq!(path, vec![a, b, c]);
}
#[test]
fn dijkstra_returns_sorted_reachable() {
let g = graph(
&["A", "B", "C"],
&[edge("e1", "A", "B", 1.0), edge("e2", "B", "C", 1.0)],
);
let a = g.node_index("A").unwrap();
let res = dijkstra(&g, a);
let ids: Vec<u32> = res.iter().map(|(i, _)| *i).collect();
assert!(ids.contains(&a));
assert_eq!(res.len(), 3);
for w in res.windows(2) {
assert!(w[0].1 <= w[1].1 + 1e-12);
}
}
#[test]
fn unreachable_returns_none() {
let g = graph(
&["A", "B", "C", "D"],
&[edge("e1", "A", "B", 1.0), edge("e2", "C", "D", 1.0)],
);
let a = g.node_index("A").unwrap();
let d = g.node_index("D").unwrap();
assert!(shortest_path(&g, a, d).is_none());
}
#[test]
fn self_path_is_singleton() {
let g = graph(&["A", "B"], &[edge("e1", "A", "B", 1.0)]);
let a = g.node_index("A").unwrap();
assert_eq!(shortest_path(&g, a, a), Some(vec![a]));
}
#[test]
fn impassable_weight_skipped() {
let g = graph(
&["A", "B", "C"],
&[
edge("e1", "A", "B", 1e-12),
edge("e2", "A", "C", 0.9),
edge("e3", "C", "B", 0.9),
],
);
let a = g.node_index("A").unwrap();
let b = g.node_index("B").unwrap();
let c = g.node_index("C").unwrap();
let path = shortest_path(&g, a, b).expect("path via C exists");
assert_eq!(path, vec![a, c, b]);
}
#[test]
fn out_of_range_returns_none() {
let g = graph(&["A"], &[]);
assert!(shortest_path(&g, 0, 5).is_none());
assert!(dijkstra(&g, 5).is_empty());
}
#[test]
fn weight_above_one_clamped_non_negative() {
let g = graph(&["A", "B"], &[edge("e1", "A", "B", 2.0)]);
let a = g.node_index("A").unwrap();
let b = g.node_index("B").unwrap();
let res = dijkstra(&g, a);
let b_dist = res.iter().find(|(i, _)| *i == b).unwrap().1;
assert!(b_dist.abs() < 1e-12, "weight > 1 clamps to zero cost");
assert!(
res.iter().all(|(_, d)| *d >= -1e-12),
"no negative distances despite contract-violating weight"
);
}
}