Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dijkstra Algorithm with Src, Dst and Weight #1321

Merged
merged 13 commits into from
Oct 6, 2023
7 changes: 6 additions & 1 deletion docs/source/reference/algorithms/pathing.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
Pathing
*********

.. autofunction:: raphtory.algorithms.temporally_reachable_nodes
.. autofunction:: raphtory.algorithms.temporally_reachable_nodes

.. autofunction:: raphtory.algorithms.single_source_shortest_path

.. autofunction:: raphtory.algorithms.dijkstra_single_source_shortest_paths

1 change: 1 addition & 0 deletions python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ fn raphtory(py: Python<'_>, m: &PyModule) -> PyResult<()> {
let algorithm_module = PyModule::new(py, "algorithms")?;
add_functions!(
algorithm_module,
dijkstra_single_source_shortest_paths,
global_reciprocity,
all_local_reciprocity,
triplet_count,
Expand Down
34 changes: 34 additions & 0 deletions python/tests/test_algorithms.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

def test_degree_centrality():
from raphtory import Graph
from raphtory.algorithms import degree_centrality
Expand Down Expand Up @@ -51,3 +53,35 @@ def test_single_source_shortest_path():
res_two.get_all()
== {"1": ["1"], "3": ["1", "4", "3"], "2": ["1", "2"], "4": ["1", "4"]}
)


def test_dijsktra_shortest_paths():
from raphtory import Graph
from raphtory.algorithms import dijkstra_single_source_shortest_paths
g = Graph()
g.add_edge(0, "A", "B", {"weight": 4.0})
g.add_edge(1, "A", "C", {"weight": 4.0})
g.add_edge(2, "B", "C", {"weight": 2.0})
g.add_edge(3, "C", "D", {"weight": 3.0})
g.add_edge(4, "C", "E", {"weight": 1.0})
g.add_edge(5, "C", "F", {"weight": 6.0})
g.add_edge(6, "D", "F", {"weight": 2.0})
g.add_edge(7, "E", "F", {"weight": 3.0})
res_one = dijkstra_single_source_shortest_paths(g, "A", ["F"])
res_two = dijkstra_single_source_shortest_paths(g, "B", ["D", "E", "F"])
assert res_one.get("F")[0] == 8.0
assert res_one.get("F")[1] == ["A", "C", "E", "F"]
assert res_two.get("D")[0] == 5.0
assert res_two.get("F")[0] == 6.0
assert res_two.get("D")[1] == ["B", "C", "D"]
assert res_two.get("F")[1] == ["B", "C", "E", "F"]

with pytest.raises(ValueError) as excinfo:
dijkstra_single_source_shortest_paths(g, "HH", ["F"])
assert "Source vertex not found" in str(excinfo.value)

with pytest.raises(ValueError) as excinfo:
dijkstra_single_source_shortest_paths(g, "A", ["F"], weight="NO")
assert "Weight property not found on edges" in str(excinfo.value)


274 changes: 274 additions & 0 deletions raphtory/src/algorithms/pathing/dijkstra.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
/// Dijkstra's algorithm
use crate::{
core::entities::vertices::input_vertex::InputVertex,
core::PropType,
prelude::Prop,
prelude::{EdgeViewOps, GraphViewOps, VertexViewOps},
};
use std::{
cmp::Ordering,
collections::{BinaryHeap, HashMap, HashSet},
};

/// A state in the Dijkstra algorithm with a cost and a vertex name.
#[derive(PartialEq)]
struct State {
cost: Prop,
vertex: String, // TODO MOVE AWAY VERTEX FROM STRING INTO VERTEXVIEW
}

impl Eq for State {}

impl Ord for State {
fn cmp(&self, other: &State) -> Ordering {
self.partial_cmp(other).unwrap_or(Ordering::Equal)
}
}

impl PartialOrd for State {
fn partial_cmp(&self, other: &State) -> Option<Ordering> {
other.cost.partial_cmp(&self.cost)
}
}

/// Finds the shortest paths from a single source to multiple targets in a graph.
///
/// # Arguments
///
/// * `graph`: The graph to search in.
/// * `source`: The source vertex.
/// * `targets`: A vector of target vertices.
/// * `weight`: The name of the weight property for the edges.
///
/// # Returns
///
/// Returns a `HashMap` where the key is the target vertex and the value is a tuple containing
/// the total cost and a vector of vertices representing the shortest path.
///
pub fn dijkstra_single_source_shortest_paths<G: GraphViewOps, T: InputVertex>(
graph: &G,
source: T,
targets: Vec<T>,
weight: String,
) -> Result<HashMap<String, (Prop, Vec<String>)>, &'static str> {
let source_vertex = match graph.vertex(source) {
Some(src) => src,
None => return Err("Source vertex not found"),
};
let weight_type = match graph.edge_meta().temporal_prop_meta().get_id(&weight) {
Some(weight_id) => graph.edge_meta().temporal_prop_meta().get_dtype(weight_id),
None => graph
.edge_meta()
.const_prop_meta()
.get_id(&weight)
.map(|weight_id| {
graph
.edge_meta()
.const_prop_meta()
.get_dtype(weight_id)
.unwrap()
}),
};
if weight_type.is_none() {
return Err("Weight property not found on edges");
}

let target_nodes: Vec<String> = targets
.iter()
.filter_map(|p| match graph.has_vertex(p.clone()) {
true => Some(graph.vertex(p.clone())?.name()),
false => None,
})
.collect();

// Turn below into a generic function, then add a closure to ensure the prop is correctly unwrapped
// after the calc is done
let cost_val = match weight_type.unwrap() {
PropType::Empty => return Err("Weight type: Empty, not supported"),
PropType::Str => return Err("Weight type: Str, not supported"),
PropType::F32 => Prop::F32(0f32),
PropType::F64 => Prop::F64(0f64),
PropType::U8 => Prop::U8(0u8),
PropType::U16 => Prop::U16(0u16),
PropType::U32 => Prop::U32(0u32),
PropType::U64 => Prop::U64(0u64),
PropType::I32 => Prop::I32(0i32),
PropType::I64 => Prop::I64(0i64),
PropType::Bool => return Err("Weight type: Bool, not supported"),
PropType::List => return Err("Weight type: List, not supported"),
PropType::Map => return Err("Weight type: Map, not supported"),
PropType::DTime => return Err("Weight type: DTime, not supported"),
PropType::Graph => return Err("Weight type: Graph, not supported"),
};
let max_val = match weight_type.unwrap() {
PropType::Empty => return Err("Weight type: Empty, not supported"),
PropType::Str => return Err("Weight type: Str, not supported"),
PropType::F32 => Prop::F32(f32::MAX),
PropType::F64 => Prop::F64(f64::MAX),
PropType::U8 => Prop::U8(u8::MAX),
PropType::U16 => Prop::U16(u16::MAX),
PropType::U32 => Prop::U32(u32::MAX),
PropType::U64 => Prop::U64(u64::MAX),
PropType::I32 => Prop::I32(i32::MAX),
PropType::I64 => Prop::I64(i64::MAX),
PropType::Bool => return Err("Weight type: Bool, not supported"),
PropType::List => return Err("Weight type: List, not supported"),
PropType::Map => return Err("Weight type: Map, not supported"),
PropType::DTime => return Err("Weight type: DTime, not supported"),
PropType::Graph => return Err("Weight type: Graph, not supported"),
};
let mut heap = BinaryHeap::new();
heap.push(State {
cost: cost_val.clone(),
vertex: source_vertex.name(),
});

let mut dist: HashMap<String, Prop> = HashMap::new();
let mut predecessor: HashMap<String, String> = HashMap::new();
let mut visited: HashSet<String> = HashSet::new();
let mut paths: HashMap<String, (Prop, Vec<String>)> = HashMap::new();

dist.insert(source_vertex.name(), cost_val.clone());

while let Some(State {
cost,
vertex: vertex_name,
}) = heap.pop()
{
if target_nodes.contains(&vertex_name) && !paths.contains_key(&vertex_name) {
let mut path = vec![vertex_name.clone()];
let mut current_vertex_name = vertex_name.clone();
while let Some(prev_vertex) = predecessor.get(&current_vertex_name) {
path.push(prev_vertex.clone());
current_vertex_name = prev_vertex.clone();
}
path.reverse();
paths.insert(vertex_name.clone(), (cost.clone(), path));
}
if !visited.insert(vertex_name.clone()) {
continue;
}
// Replace this loop with your actual logic to iterate over the outgoing edges
for edge in graph.vertex(vertex_name.clone()).unwrap().out_edges() {
let next_vertex_name = edge.dst().name();
let edge_val = match edge.properties().get(&weight) {
Some(prop) => prop,
_ => continue,
};
let next_cost = cost.clone().add(edge_val).unwrap();
if next_cost
< *dist
.entry(next_vertex_name.clone())
.or_insert(max_val.clone())
{
heap.push(State {
cost: next_cost.clone(),
vertex: next_vertex_name.clone(),
});
dist.insert(next_vertex_name.clone(), next_cost);
predecessor.insert(next_vertex_name, vertex_name.clone());
}
}
}
Ok(paths)
}

#[cfg(test)]
mod dijkstra_tests {
use super::*;
use crate::{
db::{api::mutation::AdditionOps, graph::graph::Graph},
prelude::Prop,
};

fn load_graph(edges: Vec<(i64, &str, &str, Vec<(&str, f32)>)>) -> Graph {
let graph = Graph::new();

for (t, src, dst, props) in edges {
graph.add_edge(t, src, dst, props, None).unwrap();
}
graph
}

fn basic_graph() -> Graph {
load_graph(vec![
(0, "A", "B", vec![("weight", 4.0f32)]),
(1, "A", "C", vec![("weight", 4.0f32)]),
(2, "B", "C", vec![("weight", 2.0f32)]),
(3, "C", "D", vec![("weight", 3.0f32)]),
(4, "C", "E", vec![("weight", 1.0f32)]),
(5, "C", "F", vec![("weight", 6.0f32)]),
(6, "D", "F", vec![("weight", 2.0f32)]),
(7, "E", "F", vec![("weight", 3.0f32)]),
])
}

#[test]
fn test_dijkstra_multiple_targets() {
let graph = basic_graph();

let targets: Vec<&str> = vec!["D", "F"];
let results =
dijkstra_single_source_shortest_paths(&graph, "A", targets, "weight".to_string());

let results = results.unwrap();

assert_eq!(results.get("D").unwrap().0, Prop::F32(7.0f32));
assert_eq!(results.get("D").unwrap().1, vec!["A", "C", "D"]);

assert_eq!(results.get("F").unwrap().0, Prop::F32(8.0f32));
assert_eq!(results.get("F").unwrap().1, vec!["A", "C", "E", "F"]);

let targets: Vec<&str> = vec!["D", "E", "F"];
let results =
dijkstra_single_source_shortest_paths(&graph, "B", targets, "weight".to_string());
let results = results.unwrap();
assert_eq!(results.get("D").unwrap().0, Prop::F32(5.0f32));
assert_eq!(results.get("E").unwrap().0, Prop::F32(3.0f32));
assert_eq!(results.get("F").unwrap().0, Prop::F32(6.0f32));
assert_eq!(results.get("D").unwrap().1, vec!["B", "C", "D"]);
assert_eq!(results.get("E").unwrap().1, vec!["B", "C", "E"]);
assert_eq!(results.get("F").unwrap().1, vec!["B", "C", "E", "F"]);
}

#[test]
fn test_dijkstra_multiple_targets_u64() {
let edges = vec![
(0, "A", "B", vec![("weight", 4u64)]),
(1, "A", "C", vec![("weight", 4u64)]),
(2, "B", "C", vec![("weight", 2u64)]),
(3, "C", "D", vec![("weight", 3u64)]),
(4, "C", "E", vec![("weight", 1u64)]),
(5, "C", "F", vec![("weight", 6u64)]),
(6, "D", "F", vec![("weight", 2u64)]),
(7, "E", "F", vec![("weight", 3u64)]),
];

let graph = Graph::new();

for (t, src, dst, props) in edges {
graph.add_edge(t, src, dst, props, None).unwrap();
}

let targets: Vec<&str> = vec!["D", "F"];
let results =
dijkstra_single_source_shortest_paths(&graph, "A", targets, "weight".to_string());
let results = results.unwrap();
assert_eq!(results.get("D").unwrap().0, Prop::U64(7u64));
assert_eq!(results.get("D").unwrap().1, vec!["A", "C", "D"]);

assert_eq!(results.get("F").unwrap().0, Prop::U64(8u64));
assert_eq!(results.get("F").unwrap().1, vec!["A", "C", "E", "F"]);

let targets: Vec<&str> = vec!["D", "E", "F"];
let results =
dijkstra_single_source_shortest_paths(&graph, "B", targets, "weight".to_string());
let results = results.unwrap();
assert_eq!(results.get("D").unwrap().0, Prop::U64(5u64));
assert_eq!(results.get("E").unwrap().0, Prop::U64(3u64));
assert_eq!(results.get("F").unwrap().0, Prop::U64(6u64));
assert_eq!(results.get("D").unwrap().1, vec!["B", "C", "D"]);
assert_eq!(results.get("E").unwrap().1, vec!["B", "C", "E"]);
assert_eq!(results.get("F").unwrap().1, vec!["B", "C", "E", "F"]);
}
}
1 change: 1 addition & 0 deletions raphtory/src/algorithms/pathing/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod dijkstra;
pub mod single_source_shortest_path;
pub mod temporal_reachability;
Loading