diff --git a/comparison-benchmark/rust/raphtory-rust-benchmark/src/main.rs b/comparison-benchmark/rust/raphtory-rust-benchmark/src/main.rs index 82589809e9..4729720117 100644 --- a/comparison-benchmark/rust/raphtory-rust-benchmark/src/main.rs +++ b/comparison-benchmark/rust/raphtory-rust-benchmark/src/main.rs @@ -197,14 +197,12 @@ fn main() { // page rank with time now = Instant::now(); - let _page_rank: Vec<_> = unweighted_page_rank(&g, 1000, None, None, true) - .into_iter() - .collect(); + let _page_rank = unweighted_page_rank(&g, 1000, None, None, true); println!("Page rank: {} seconds", now.elapsed().as_secs_f64()); // connected community_detection with time now = Instant::now(); - let _cc: AlgorithmResult = weakly_connected_components(&g, usize::MAX, None); + let _cc = weakly_connected_components(&g, usize::MAX, None); println!( "Connected community_detection: {} seconds", now.elapsed().as_secs_f64() diff --git a/docs/source/reference/algorithms/centrality.rst b/docs/source/reference/algorithms/centrality.rst index fe8a3a2c8c..f87cc8deb0 100644 --- a/docs/source/reference/algorithms/centrality.rst +++ b/docs/source/reference/algorithms/centrality.rst @@ -7,3 +7,4 @@ Centrality .. autofunction:: raphtory.algorithms.hits +.. autofunction:: raphtory.algorithms.betweenness_centrality \ No newline at end of file diff --git a/examples/rust/src/bin/hulongbay/main.rs b/examples/rust/src/bin/hulongbay/main.rs index 20f14d48da..58a0d57241 100644 --- a/examples/rust/src/bin/hulongbay/main.rs +++ b/examples/rust/src/bin/hulongbay/main.rs @@ -125,6 +125,7 @@ fn try_main() -> Result<(), Box> { let components = weakly_connected_components(&graph, 5, Some(16)); components + .result .into_iter() .counts_by(|(_, cc)| cc) .iter() diff --git a/examples/rust/src/bin/lotr/main.rs b/examples/rust/src/bin/lotr/main.rs index 81d87357f4..49b67cff9d 100644 --- a/examples/rust/src/bin/lotr/main.rs +++ b/examples/rust/src/bin/lotr/main.rs @@ -1,4 +1,3 @@ -use itertools::Itertools; use raphtory::{ algorithms::pathing::temporal_reachability::temporally_reachable_nodes, core::utils::hashing, graph_loader::source::csv_loader::CsvLoader, prelude::*, @@ -104,9 +103,12 @@ fn main() { assert!(graph.has_vertex(gandalf)); assert_eq!(graph.vertex(gandalf).unwrap().name(), "Gandalf"); - let r = temporally_reachable_nodes(&graph, None, 20, 31930, vec!["Gandalf"], None); - assert_eq!( - r.result.keys().sorted().collect_vec(), - vec!["Gandalf", "Saruman", "Wormtongue"] - ) + let r: Vec = temporally_reachable_nodes(&graph, None, 20, 31930, vec!["Gandalf"], None) + .get_all_values() + .into_iter() + .flatten() + .map(|(_, s)| s) + .collect(); + + assert_eq!(r, vec!["Gandalf", "Saruman", "Wormtongue"]) } diff --git a/python/src/lib.rs b/python/src/lib.rs index 56a3067c45..6c79be4d63 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -69,6 +69,7 @@ fn raphtory(py: Python<'_>, m: &PyModule) -> PyResult<()> { algorithm_module, dijkstra_single_source_shortest_paths, global_reciprocity, + betweenness_centrality, all_local_reciprocity, triplet_count, local_triangle_count, @@ -107,6 +108,7 @@ fn raphtory(py: Python<'_>, m: &PyModule) -> PyResult<()> { neo4j_movie_graph, stable_coin_graph, reddit_hyperlink_graph, + karate_club_graph, ); m.add_submodule(graph_loader_module)?; diff --git a/python/tests/test_algorithms.py b/python/tests/test_algorithms.py index 4d1a772122..0c2f92d395 100644 --- a/python/tests/test_algorithms.py +++ b/python/tests/test_algorithms.py @@ -1,20 +1,208 @@ import pytest +import pandas as pd +import pandas.core.frame +from raphtory import Graph, GraphWithDeletions, PyDirection +from raphtory import algorithms +from raphtory import graph_loader + +def gen_graph(): + g = Graph() + g.add_edge(10, 1, 3, {}) + g.add_edge(11, 1, 2, {}) + g.add_edge(12, 1, 2, {}) + g.add_edge(9, 1, 2, {}) + g.add_edge(12, 2, 4, {}) + g.add_edge(13, 2, 5, {}) + g.add_edge(14, 5, 5, {}) + g.add_edge(14, 5, 4, {}) + g.add_edge(5, 4, 6, {}) + g.add_edge(15, 4, 7, {}) + g.add_edge(10, 4, 7, {}) + g.add_edge(10, 5, 8, {}) + return g + + +def test_connected_components(): + g = gen_graph() + actual = algorithms.weakly_connected_components(g, 20) + expected = {"1": 1, "2": 1, "3": 1, "4": 1, "5": 1, "6": 1, "7": 1, "8": 1} + assert actual.get_all_with_names() == expected + assert actual.get("1") == 1 + + +def test_empty_algo(): + g = Graph() + assert algorithms.weakly_connected_components(g, 20).get_all_with_names() == {} + assert algorithms.pagerank(g, 20).get_all_with_names() == {} + + +def test_algo_result_windowed_graph(): + g = Graph() + g.add_edge(0, 1, 2, {}) + g.add_edge(1, 1, 2, {}) + g.add_edge(2, 3, 4, {}) + g.add_edge(3, 5, 6, {}) + g.add_edge(10, 10, 11, {}) + + res_full_graph = algorithms.weakly_connected_components(g, 20) + assert sorted(res_full_graph.get_all_with_names().items()) == [('1', 1), ('10', 10), ('11', 10), ('2', 1), ('3', 3), ('4', 3), ('5', 5), ('6', 5)] + + g_window = g.window(0, 2) + res_window = algorithms.weakly_connected_components(g_window, 20) + assert sorted(res_window.get_all_with_names().items()) == [('1', 1), ('2', 1)] + + g_window = g.window(2, 3) + res_window = algorithms.weakly_connected_components(g_window, 20) + assert sorted(res_window.get_all_with_names().items()) == [('3', 3), ('4', 3)] + + +def test_algo_result_layered_graph(): + g = Graph() + g.add_edge(0, 1, 2, {}, layer="ZERO-TWO") + g.add_edge(1, 1, 3, {}, layer="ZERO-TWO") + g.add_edge(2, 4, 5, {}, layer="ZERO-TWO") + g.add_edge(3, 6, 7, {}, layer="THREE-FIVE") + g.add_edge(4, 8, 9, {}, layer="THREE-FIVE") + + g_layer_zero_two = g.layer("ZERO-TWO") + g_layer_three_five = g.layer("THREE-FIVE") + + res_zero_two = algorithms.weakly_connected_components(g_layer_zero_two, 20) + assert sorted(res_zero_two.get_all_with_names().items()) == [('1', 1), ('2', 1), ('3', 1), ('4', 4), ('5', 4), ('6', 6), ('7', 7), ('8', 8), ('9', 9)] + + res_three_five = algorithms.weakly_connected_components(g_layer_three_five, 20) + assert sorted(res_three_five.get_all_with_names().items()) == [('1', 1), ('2', 2), ('3', 3), ('4', 4), ('5', 5), ('6', 6), ('7', 6), ('8', 8), ('9', 8)] + + +def test_algo_result_window_and_layered_graph(): + g = Graph() + g.add_edge(0, 1, 2, {}, layer="ZERO-TWO") + g.add_edge(1, 1, 3, {}, layer="ZERO-TWO") + g.add_edge(2, 4, 5, {}, layer="ZERO-TWO") + g.add_edge(3, 6, 7, {}, layer="THREE-FIVE") + g.add_edge(4, 8, 9, {}, layer="THREE-FIVE") + + g_layer_zero_two = g.window(0, 1).layer("ZERO-TWO") + g_layer_three_five = g.window(4, 5).layer("THREE-FIVE") + + res_zero_two = algorithms.weakly_connected_components(g_layer_zero_two, 20) + assert sorted(res_zero_two.get_all_with_names().items()) == [('1', 1), ('2', 1)] + + res_three_five = algorithms.weakly_connected_components(g_layer_three_five, 20) + assert sorted(res_three_five.get_all_with_names().items()) == [('8', 8), ('9', 8)] + + +def test_algo_result(): + g = gen_graph() + + actual = algorithms.weakly_connected_components(g, 20) + expected = {"1": 1, "2": 1, "3": 1, "4": 1, "5": 1, "6": 1, "7": 1, "8": 1} + assert actual.get_all_with_names() == expected + assert actual.get("1") == 1 + assert actual.get("not a node") == None + expected_array = [ + (g.vertex("1"), 1), + (g.vertex("2"), 1), + (g.vertex("3"), 1), + (g.vertex("4"), 1), + (g.vertex("5"), 1), + (g.vertex("6"), 1), + (g.vertex("7"), 1), + (g.vertex("8"), 1), + ] + assert actual.sort_by_vertex_name(False) == expected_array + assert sorted(actual.top_k(8)) == expected_array + assert len(actual.group_by()[1]) == 8 + assert type(actual.to_df()) == pandas.core.frame.DataFrame + df = actual.to_df() + expected_result = pd.DataFrame({"Key": [1], "Value": [1]}) + row_with_one = df[df["Key"] == 1] + row_with_one.reset_index(inplace=True, drop=True) + print(row_with_one) + assert row_with_one.equals(expected_result) + # Algo Str u64 + actual = algorithms.weakly_connected_components(g) + all_res = actual.get_all_with_names() + sorted_res = {k: all_res[k] for k in sorted(all_res)} + assert sorted_res == { + "1": 1, + "2": 1, + "3": 1, + "4": 1, + "5": 1, + "6": 1, + "7": 1, + "8": 1, + } + # algo str f64 + actual = algorithms.pagerank(g) + expected_result = { + "3": 0.10274080842110422, + "2": 0.10274080842110422, + "4": 0.1615298183542792, + "6": 0.14074777909144864, + "1": 0.07209850165402759, + "5": 0.1615298183542792, + "7": 0.14074777909144864, + "8": 0.11786468661230831, + } + assert actual.get_all_with_names() == expected_result + assert actual.get("Not a node") == None + assert len(actual.to_df()) == 8 + # algo str vector + actual = algorithms.temporally_reachable_nodes(g, 20, 11, [1, 2], [4, 5]) + assert sorted(actual.get_all_with_names()) == ["1", "2", "3", "4", "5", "6", "7", "8"] + + +def test_page_rank(): + g = gen_graph() + actual = algorithms.pagerank(g) + expected = { + "1": 0.07209850165402759, + "2": 0.10274080842110422, + "3": 0.10274080842110422, + "4": 0.1615298183542792, + "5": 0.1615298183542792, + "6": 0.14074777909144864, + "7": 0.14074777909144864, + "8": 0.11786468661230831, + } + assert actual.get_all_with_names() == expected + + +def test_temporal_reachability(): + g = gen_graph() + + actual = algorithms.temporally_reachable_nodes(g, 20, 11, [1, 2], [4, 5]) + expected = { + "1": [(11, "start")], + "2": [(11, "start"), (12, "1"), (11, "1")], + "3": [], + "4": [(12, "2")], + "5": [(13, "2")], + "6": [], + "7": [], + "8": [], + } + + assert actual.get_all_with_names() == expected + def test_degree_centrality(): from raphtory import Graph from raphtory.algorithms import degree_centrality g = Graph() - g.add_edge(0, 0, 1, {}) - g.add_edge(0, 0, 2, {}) - g.add_edge(0, 0, 3, {}) g.add_edge(0, 1, 2, {}) g.add_edge(0, 1, 3, {}) - assert degree_centrality(g).get_all() == { - "0": 1.0, + g.add_edge(0, 1, 4, {}) + g.add_edge(0, 2, 3, {}) + g.add_edge(0, 2, 4, {}) + assert degree_centrality(g).get_all_with_names() == { "1": 1.0, - "2": 2 / 3, + "2": 1.0, "3": 2 / 3, + "4": 2 / 3, } @@ -45,12 +233,12 @@ def test_single_source_shortest_path(): g.add_edge(0, 2, 4, {}) res_one = single_source_shortest_path(g, 1, 1) res_two = single_source_shortest_path(g, 1, 2) - assert res_one.get_all() == {"1": ["1"], "2": ["1", "2"], "4": ["1", "4"]} + assert res_one.get_all_with_names() == {"1": ["1"], "2": ["1", "2"], "3": None, "4": ["1", "4"]} assert ( - res_two.get_all() + res_two.get_all_with_names() == {"1": ["1"], "2": ["1", "2"], "3": ["1", "2", "3"], "4": ["1", "4"]} ) or ( - res_two.get_all() + res_two.get_all_with_names() == {"1": ["1"], "3": ["1", "4", "3"], "2": ["1", "2"], "4": ["1", "4"]} ) @@ -84,4 +272,62 @@ def test_dijsktra_shortest_paths(): dijkstra_single_source_shortest_paths(g, "A", ["F"], weight="NO") assert "Weight property not found on edges" in str(excinfo.value) - \ No newline at end of file + +def test_betweenness_centrality(): + from raphtory import Graph + from raphtory.algorithms import betweenness_centrality + g = Graph() + edges = [ + (0, 1), + (0, 2), + (0, 3), + (1, 2), + (1, 3), + (1, 4), + (2, 3), + (2, 4), + (2, 5), + (3, 2), + (3, 1), + (3, 3) + ] + for e in edges: + g.add_edge(0, e[0], e[1], {}) + + res = betweenness_centrality(g, normalized=False) + assert res.get_all_with_names() == { "0": 0.0, '1': 1.0, "2": 4.0, "3": 1.0, "4": 0.0, "5": 0.0 } + + res = betweenness_centrality(g, normalized=True) + assert res.get_all_with_names() == { "0": 0.0, '1': 0.05, "2": 0.2, "3": 0.05, "4": 0.0, "5": 0.0} + + +def test_hits_algorithm(): + g = graph_loader.lotr_graph() + assert algorithms.hits(g).get("Aldor") == ( + 0.0035840950440615416, + 0.007476256228983402, + ) + + +def test_balance_algorithm(): + g = Graph() + edges_str = [ + ("1", "2", 10.0, 1), + ("1", "4", 20.0, 2), + ("2", "3", 5.0, 3), + ("3", "2", 2.0, 4), + ("3", "1", 1.0, 5), + ("4", "3", 10.0, 6), + ("4", "1", 5.0, 7), + ("1", "5", 2.0, 8), + ] + for src, dst, val, time in edges_str: + g.add_edge(time, src, dst, {"value_dec": val}) + result = algorithms.balance(g, "value_dec", PyDirection("BOTH"), None).get_all_with_names() + assert result == {"1": -26.0, "2": 7.0, "3": 12.0, "4": 5.0, "5": 2.0} + + result = algorithms.balance(g, "value_dec", PyDirection("IN"), None).get_all_with_names() + assert result == {"1": 6.0, "2": 12.0, "3": 15.0, "4": 20.0, "5": 2.0} + + result = algorithms.balance(g, "value_dec", PyDirection("OUT"), None).get_all_with_names() + assert result == {"1": -32.0, "2": -5.0, "3": -3.0, "4": -15.0, "5": 0.0} diff --git a/python/tests/test_graph_gen.py b/python/tests/test_graph_gen.py new file mode 100644 index 0000000000..8e5340113c --- /dev/null +++ b/python/tests/test_graph_gen.py @@ -0,0 +1,6 @@ +def test_karate_club(): + from raphtory.graph_loader import karate_club_graph + g = karate_club_graph() + assert g.count_vertices() == 34 + assert g.count_edges() == 155 + diff --git a/python/tests/test_graphdb.py b/python/tests/test_graphdb.py index c33de5d640..5cfb92ae35 100644 --- a/python/tests/test_graphdb.py +++ b/python/tests/test_graphdb.py @@ -1233,132 +1233,7 @@ def test_lotr_edge_history(): assert g.edge("Frodo", "Gandalf").window(100, 1000).history() == [329, 555, 861] -def gen_graph(): - g = Graph() - g.add_edge(10, 1, 3, {}) - g.add_edge(11, 1, 2, {}) - g.add_edge(12, 1, 2, {}) - g.add_edge(9, 1, 2, {}) - g.add_edge(12, 2, 4, {}) - g.add_edge(13, 2, 5, {}) - g.add_edge(14, 5, 5, {}) - g.add_edge(14, 5, 4, {}) - g.add_edge(5, 4, 6, {}) - g.add_edge(15, 4, 7, {}) - g.add_edge(10, 4, 7, {}) - g.add_edge(10, 5, 8, {}) - return g - -def test_connected_components(): - g = gen_graph() - actual = algorithms.weakly_connected_components(g, 20) - expected = {"1": 1, "2": 1, "3": 1, "4": 1, "5": 1, "6": 1, "7": 1, "8": 1} - assert actual.get_all() == expected - assert actual.get("1") == 1 - - -def test_empty_algo(): - g = Graph() - assert algorithms.weakly_connected_components(g, 20).get_all() == {} - assert algorithms.pagerank(g, 20).get_all() == {} - - -def test_algo_result(): - g = gen_graph() - - actual = algorithms.weakly_connected_components(g, 20) - expected = {"1": 1, "2": 1, "3": 1, "4": 1, "5": 1, "6": 1, "7": 1, "8": 1} - assert actual.get_all() == expected - assert actual.get("1") == 1 - assert actual.get("not a node") == None - expected_array = [ - ("1", 1), - ("2", 1), - ("3", 1), - ("4", 1), - ("5", 1), - ("6", 1), - ("7", 1), - ("8", 1), - ] - assert sorted(actual.sort_by_value()) == expected_array - assert actual.sort_by_key() == sorted(expected_array, reverse=True) - assert actual.sort_by_key(reverse=False) == expected_array - assert sorted(actual.top_k(8)) == expected_array - assert len(actual.group_by()[1]) == 8 - assert type(actual.to_df()) == pandas.core.frame.DataFrame - df = actual.to_df() - expected_result = pd.DataFrame({"Key": ["1"], "Value": [1]}) - row_with_one = df[df["Key"] == "1"] - row_with_one.reset_index(inplace=True, drop=True) - assert row_with_one.equals(expected_result) - # Algo Str u64 - actual = algorithms.weakly_connected_components(g) - all_res = actual.get_all() - sorted_res = {k: all_res[k] for k in sorted(all_res)} - assert sorted_res == { - "1": 1, - "2": 1, - "3": 1, - "4": 1, - "5": 1, - "6": 1, - "7": 1, - "8": 1, - } - # algo str f64 - actual = algorithms.pagerank(g) - expected_result = { - "3": 0.10274080842110422, - "2": 0.10274080842110422, - "4": 0.1615298183542792, - "6": 0.14074777909144864, - "1": 0.07209850165402759, - "5": 0.1615298183542792, - "7": 0.14074777909144864, - "8": 0.11786468661230831, - } - assert actual.get_all() == expected_result - assert actual.get("Not a node") == None - assert len(actual.to_df()) == 8 - # algo str vector - actual = algorithms.temporally_reachable_nodes(g, 20, 11, [1, 2], [4, 5]) - assert sorted(actual.get_all()) == ["1", "2", "3", "4", "5", "6", "7", "8"] - - -def test_page_rank(): - g = gen_graph() - actual = algorithms.pagerank(g) - expected = { - "1": 0.07209850165402759, - "2": 0.10274080842110422, - "3": 0.10274080842110422, - "4": 0.1615298183542792, - "5": 0.1615298183542792, - "6": 0.14074777909144864, - "7": 0.14074777909144864, - "8": 0.11786468661230831, - } - assert actual.get_all() == expected - - -def test_temporal_reachability(): - g = gen_graph() - - actual = algorithms.temporally_reachable_nodes(g, 20, 11, [1, 2], [4, 5]) - expected = { - "1": [(11, "start")], - "2": [(11, "start"), (12, "1"), (11, "1")], - "3": [], - "4": [(12, "2")], - "5": [(13, "2")], - "6": [], - "7": [], - "8": [], - } - - assert actual.get_all() == expected # def test_generic_taint_loader(): @@ -1725,37 +1600,6 @@ def test_edge_explode_layers(): print(e_layers) -def test_hits_algorithm(): - g = graph_loader.lotr_graph() - assert algorithms.hits(g).get("Aldor") == ( - 0.0035840950440615416, - 0.007476256228983402, - ) - - -def test_balance_algorithm(): - g = Graph() - edges_str = [ - ("1", "2", 10.0, 1), - ("1", "4", 20.0, 2), - ("2", "3", 5.0, 3), - ("3", "2", 2.0, 4), - ("3", "1", 1.0, 5), - ("4", "3", 10.0, 6), - ("4", "1", 5.0, 7), - ("1", "5", 2.0, 8), - ] - for src, dst, val, time in edges_str: - g.add_edge(time, src, dst, {"value_dec": val}) - result = algorithms.balance(g, "value_dec", PyDirection("BOTH"), None).get_all() - assert result == {"1": -26.0, "2": 7.0, "3": 12.0, "4": 5.0, "5": 2.0} - - result = algorithms.balance(g, "value_dec", PyDirection("IN"), None).get_all() - assert result == {"1": 6.0, "2": 12.0, "3": 15.0, "4": 20.0, "5": 2.0} - - result = algorithms.balance(g, "value_dec", PyDirection("OUT"), None).get_all() - assert result == {"1": -32.0, "2": -5.0, "3": -3.0, "4": -15.0, "5": 0.0} - def test_starend_edges(): g = Graph() diff --git a/raphtory-graphql/src/model/algorithm.rs b/raphtory-graphql/src/model/algorithm.rs index 141d508b8d..6f449636a5 100644 --- a/raphtory-graphql/src/model/algorithm.rs +++ b/raphtory-graphql/src/model/algorithm.rs @@ -96,6 +96,15 @@ impl From<(String, f64)> for Pagerank { } } +impl From<(String, Option)> for Pagerank { + fn from((name, rank): (String, Option)) -> Self { + Self { + name, + rank: rank.unwrap_or_default(), // use 0.0 if rank is None + } + } +} + impl From<(String, OrderedFloat)> for Pagerank { fn from((name, rank): (String, OrderedFloat)) -> Self { let rank = rank.into_inner(); @@ -134,6 +143,7 @@ impl Algorithm for Pagerank { let tol = ctx.args.get("tol").map(|v| v.f64()).transpose()?; let binding = unweighted_page_rank(graph, iter_count, threads, tol, true); let result = binding + .get_all_with_names() .into_iter() .map(|pair| FieldValue::owned_any(Pagerank::from(pair))); Ok(Some(FieldValue::list(result))) diff --git a/raphtory/src/algorithms/algorithm_result.rs b/raphtory/src/algorithms/algorithm_result.rs index a0ac1e1d7e..65cc213268 100644 --- a/raphtory/src/algorithms/algorithm_result.rs +++ b/raphtory/src/algorithms/algorithm_result.rs @@ -1,11 +1,11 @@ -use itertools::Itertools; +use crate::{ + core::entities::vertices::vertex_ref::VertexRef, + prelude::{GraphViewOps, VertexViewOps}, +}; use num_traits::Float; use ordered_float::OrderedFloat; use std::{ - borrow::Borrow, collections::{hash_map::Iter, HashMap}, - fmt, - fmt::Debug, hash::Hash, marker::PhantomData, }; @@ -55,16 +55,19 @@ pub struct AlgorithmRepr { /// /// This `AlgorithmResult` is returned for all algorithms that return a HashMap /// -pub struct AlgorithmResult { +pub struct AlgorithmResult { /// The result hashmap that stores keys of type `H` and values of type `Y`. pub algo_repr: AlgorithmRepr, - pub result: HashMap, + pub graph: G, + pub result: HashMap, marker: PhantomData, } -impl AlgorithmResult +// use pyo3::{prelude::*, types::IntoPyDict}; + +impl AlgorithmResult where - K: Clone + Hash + Eq + Ord, + G: GraphViewOps, V: Clone, { /// Creates a new instance of `AlgorithmResult` with the provided hashmap. @@ -72,15 +75,17 @@ where /// Arguments: /// /// * `algo_name`: The name of the algorithm. - /// * `num_vertices`: The number of vertices in the graph. /// * `result_type`: The type of the result. - /// * `result`: A `HashMap` with keys of type `H` and values of type `Y`. - pub fn new(algo_name: &str, result_type: &str, result: HashMap) -> Self { + /// * `result`: A `Vec` with values of type `V`. + /// * `graph`: The Raphtory Graph object + pub fn new(graph: G, algo_name: &str, result_type: &str, result: HashMap) -> Self { + // let result_type = type_name::(); Self { algo_repr: AlgorithmRepr { algo_name: algo_name.to_string(), result_type: result_type.to_string(), }, + graph, result, marker: PhantomData, } @@ -97,24 +102,53 @@ where algo_info_str } - /// Returns a reference to the entire `result` hashmap. - pub fn get_all(&self) -> &HashMap { - &self.result + /// Returns a reference to the entire `result` vector of values. + pub fn get_all_values(&self) -> Vec { + self.result.clone().into_values().collect() } /// Returns the value corresponding to the provided key in the `result` hashmap. /// /// Arguments: /// `key`: The key of type `H` for which the value is to be retrieved. - pub fn get(&self, key: &Q) -> Option<&V> - where - Q: Hash + Eq + ?Sized, - K: Borrow, - { - self.result.get(key) + pub fn get(&self, v_ref: VertexRef) -> Option<&V> { + if self.graph.has_vertex(v_ref) { + let internal_id = self.graph.vertex(v_ref).unwrap().vertex.0; + self.result.get(&internal_id) + } else { + None + } } - /// Sorts the `AlgorithmResult` by its keys in ascending or descending order. + /// Returns a hashmap with vertex names and values + /// + /// Returns: + /// a hashmap with vertex names and values + pub fn get_all_with_names(&self) -> HashMap> { + self.graph + .vertices() + .iter() + .map(|vertex| { + let name = vertex.name(); + let value = self.result.get(&vertex.vertex.0).cloned(); + (name.to_string(), value) + }) + .collect() + } + + /// Returns a `HashMap` containing `VertexView` keys and `Option` values. + /// + /// Returns: + /// a `HashMap` containing `VertexView` keys and `Option` values. + pub fn get_all(&self) -> HashMap, Option> { + self.graph + .vertices() + .iter() + .map(|vertex| (vertex.clone(), self.result.get(&vertex.vertex.0).cloned())) + .collect() + } + + /// Sorts the `AlgorithmResult` by its vertex id in ascending or descending order. /// /// Arguments: /// @@ -122,14 +156,44 @@ where /// /// Returns: /// - /// A sorted vector of tuples containing keys of type `H` and values of type `Y`. - pub fn sort_by_key(&self, reverse: bool) -> Vec<(K, V)> { - let mut sorted: Vec<(K, V)> = self.result.clone().into_iter().collect(); + /// A sorted vector of tuples containing vertex names and values. + pub fn sort_by_vertex(&self, reverse: bool) -> Vec<(VertexView, Option)> { + let mut sorted: Vec<(VertexView, Option)> = self.get_all().into_iter().collect(); sorted.sort_by(|(a, _), (b, _)| if reverse { b.cmp(a) } else { a.cmp(b) }); sorted } - pub fn iter(&self) -> Iter<'_, K, V> { + /// Sorts a collection of vertex views by their names in either ascending or descending order. + /// + /// Arguments: + /// + /// * `reverse`: A boolean value indicating whether the sorting should be in reverse order or not. If + /// `reverse` is `true`, the sorting will be in reverse order (descending), otherwise it will be in + /// ascending order. + /// + /// Returns: + /// + /// The function `sort_by_vertex_name` returns a vector of tuples, where each tuple contains a + /// `VertexView` and an optional `V` value. + pub fn sort_by_vertex_name(&self, reverse: bool) -> Vec<(VertexView, Option)> { + let mut sorted: Vec<(VertexView, Option)> = self.get_all().into_iter().collect(); + sorted.sort_by(|(a, _), (b, _)| { + if reverse { + b.name().cmp(&a.name()) + } else { + a.name().cmp(&b.name()) + } + }); + sorted + } + + /// The `iter` function returns an iterator over the elements of the `result` field. + /// + /// Returns: + /// + /// The `iter` method returns an iterator over the elements of the `result` field of the struct. The + /// iterator yields references to tuples containing a `usize` key and a `V` value. + pub fn iter(&self) -> Iter<'_, usize, V> { self.result.iter() } @@ -141,15 +205,29 @@ where /// /// Returns: /// - /// A sorted vector of tuples containing keys of type `H` and values of type `Y`. - pub fn sort_by std::cmp::Ordering>( + /// A sorted vector of tuples containing vertex names and values. + pub fn sort_by_values std::cmp::Ordering>( &self, mut cmp: F, reverse: bool, - ) -> Vec<(K, V)> { - let mut sorted: Vec<(K, V)> = self.result.clone().into_iter().collect(); - sorted.sort_by(|(_, a), (_, b)| if reverse { cmp(b, a) } else { cmp(a, b) }); - sorted + ) -> Vec<(VertexView, Option)> { + let mut all_as_vec: Vec<(VertexView, Option)> = self.get_all().into_iter().collect(); + all_as_vec.sort_by(|a, b| { + let order = match (&a.1, &b.1) { + (Some(a_value), Some(b_value)) => cmp(a_value, b_value), + (Some(_), None) => std::cmp::Ordering::Greater, // Put Some(_) values before None + (None, Some(_)) => std::cmp::Ordering::Less, // Put Some(_) values before None + (None, None) => std::cmp::Ordering::Equal, // Equal if both are None + }; + + // Reverse the order if `reverse` is true + if reverse { + order.reverse() + } else { + order + } + }); + all_as_vec } /// Retrieves the top-k elements from the `AlgorithmResult` based on its values. @@ -172,90 +250,94 @@ where k: usize, percentage: bool, reverse: bool, - ) -> Vec<(K, V)> { + ) -> Vec<(VertexView, Option)> { let k = if percentage { let total_count = self.result.len(); (total_count as f64 * (k as f64 / 100.0)) as usize } else { k }; - self.sort_by(cmp, reverse).into_iter().take(k).collect() + self.sort_by_values(cmp, reverse) + .into_iter() + .take(k) + .collect() } - pub fn min_by std::cmp::Ordering>(&self, mut cmp: F) -> Option<(K, V)> { - self.result - .iter() - .min_by(|a, b| cmp(a.1, b.1)) - .map(|(k, v)| (k.clone(), v.clone())) + pub fn min_by std::cmp::Ordering>( + &self, + mut cmp: F, + ) -> Option<(VertexView, Option)> { + let min_element = self + .get_all() + .into_iter() + .filter_map(|(k, v)| v.map(|v| (k, v))) + .min_by(|(_, a_value), (_, b_value)| cmp(a_value, b_value)); + + // Clone the key and value + min_element.map(|(k, v)| (k.clone(), Some(v.clone()))) } - pub fn max_by std::cmp::Ordering>(&self, mut cmp: F) -> Option<(K, V)> { - self.result - .iter() - .max_by(|a, b| cmp(a.1, b.1)) - .map(|(k, v)| (k.clone(), v.clone())) + pub fn max_by std::cmp::Ordering>( + &self, + mut cmp: F, + ) -> Option<(VertexView, Option)> { + let max_element = self + .get_all() + .into_iter() + .filter_map(|(k, v)| v.map(|v| (k, v))) + .max_by(|(_, a_value), (_, b_value)| cmp(a_value, b_value)); + + // Clone the key and value + max_element.map(|(k, v)| (k.clone(), Some(v.clone()))) } - pub fn median_by std::cmp::Ordering>(&self, mut cmp: F) -> Option<(K, V)> { - let mut items: Vec<_> = self.result.iter().collect(); + pub fn median_by std::cmp::Ordering>( + &self, + mut cmp: F, + ) -> Option<(VertexView, Option)> { + // Assuming self.result is Vec<(String, Option)> + let mut items: Vec<(VertexView, V)> = self + .get_all() + .into_iter() + .filter_map(|(k, v)| v.map(|v| (k, v))) + .collect(); let len = items.len(); if len == 0 { return None; } - items.sort_by(|(_, a), (_, b)| cmp(a, b)); - let median_index = len / 2; - Some((items[median_index].0.clone(), items[median_index].1.clone())) - } -} -impl IntoIterator for AlgorithmResult -where - K: Clone + Hash + Eq + Ord, - V: Clone, - for<'a> &'a O: From<&'a V>, -{ - type Item = (K, V); - type IntoIter = std::collections::hash_map::IntoIter; + items.sort_by(|(_, a_value), (_, b_value)| cmp(a_value, b_value)); + let median_index = len / 2; - fn into_iter(self) -> Self::IntoIter { - self.result.into_iter() + Some(( + items[median_index].0.clone(), + Some(items[median_index].1.clone()), + )) } } -impl<'a, K, V, O> IntoIterator for &'a AlgorithmResult +impl AlgorithmResult where - K: Clone + Hash + Ord, + G: GraphViewOps, V: Clone, + O: Ord, + V: AsOrd, { - type Item = (&'a K, &'a V); - type IntoIter = Iter<'a, K, V>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} + pub fn group_by(&self) -> HashMap> + where + V: Eq + Hash, + { + let mut groups: HashMap> = HashMap::new(); -impl FromIterator<(K, V)> for AlgorithmResult { - fn from_iter>(iter: T) -> Self { - let result = iter.into_iter().collect(); - Self { - algo_repr: AlgorithmRepr { - algo_name: String::new(), - result_type: String::new(), - }, - result, - marker: PhantomData, + for vertex in self.graph.vertices().iter() { + if let Some(value) = self.result.get(&vertex.vertex.0) { + let entry = groups.entry(value.clone()).or_insert_with(Vec::new); + entry.push(vertex.name().to_string()); + } } + groups } -} -impl AlgorithmResult -where - K: Clone + Hash + Eq + Ord, - V: Clone, - O: Ord, - V: AsOrd, -{ /// Sorts the `AlgorithmResult` by its values in ascending or descending order. /// /// Arguments: @@ -265,8 +347,8 @@ where /// Returns: /// /// A sorted vector of tuples containing keys of type `H` and values of type `Y`. - pub fn sort_by_value(&self, reverse: bool) -> Vec<(K, V)> { - self.sort_by(|a, b| O::cmp(a.as_ord(), b.as_ord()), reverse) + pub fn sort_by_value(&self, reverse: bool) -> Vec<(VertexView, Option)> { + self.sort_by_values(|a, b| O::cmp(a.as_ord(), b.as_ord()), reverse) } /// Retrieves the top-k elements from the `AlgorithmResult` based on its values. @@ -283,7 +365,12 @@ where /// If `percentage` is `true`, the returned vector contains the top `k` percentage of elements. /// If `percentage` is `false`, the returned vector contains the top `k` elements. /// Returns empty vec if the result is empty or if `k` is 0. - pub fn top_k(&self, k: usize, percentage: bool, reverse: bool) -> Vec<(K, V)> { + pub fn top_k( + &self, + k: usize, + percentage: bool, + reverse: bool, + ) -> Vec<(VertexView, Option)> { self.top_k_by( |a, b| O::cmp(a.as_ord(), b.as_ord()), k, @@ -292,167 +379,239 @@ where ) } - pub fn min(&self) -> Option<(K, V)> { + /// Returns a tuple of the min result with its key + pub fn min(&self) -> Option<(VertexView, Option)> { self.min_by(|a, b| O::cmp(a.as_ord(), b.as_ord())) } - pub fn max(&self) -> Option<(K, V)> { + /// Returns a tuple of the max result with its key + pub fn max(&self) -> Option<(VertexView, Option)> { self.max_by(|a, b| O::cmp(a.as_ord(), b.as_ord())) } - pub fn median(&self) -> Option<(K, V)> { + /// Returns a tuple of the median result with its key + pub fn median(&self) -> Option<(VertexView, Option)> { self.median_by(|a, b| O::cmp(a.as_ord(), b.as_ord())) } } -impl AlgorithmResult -where - K: Clone + Hash + Eq + Ord, - V: Clone + Hash + Eq, -{ - /// Groups the `AlgorithmResult` by its values. - /// - /// Returns: - /// - /// A `HashMap` where keys are unique values from the `AlgorithmResult` and values are vectors - /// containing keys of type `H` that share the same value. - pub fn group_by(&self) -> HashMap> { - let mut grouped: HashMap> = HashMap::new(); - for (key, value) in &self.result { - grouped.entry(value.clone()).or_default().push(key.clone()); +use crate::db::graph::vertex::VertexView; +use std::fmt; + +impl fmt::Display for AlgorithmResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "AlgorithmResultNew {{")?; + writeln!(f, " Algorithm Name: {}", self.algo_repr.algo_name)?; + writeln!(f, " Result Type: {}", self.algo_repr.result_type)?; + writeln!(f, " Number of Vertices: {}", self.result.len())?; + writeln!(f, " Results: [")?; + + for vertex in self.graph.vertices().iter() { + let value = self.result.get(&vertex.vertex.0); + writeln!(f, " {}: {:?}", vertex.name(), value)?; } - grouped + + writeln!(f, " ]")?; + writeln!(f, "}}") } } -impl Debug for AlgorithmResult { +impl fmt::Debug for AlgorithmResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let map_string = self - .result - .iter() - .map(|(key, value)| format!("{:?}: {:?}, ", key, value)) - .join(", "); - write!(f, "{{{}}}", map_string) + writeln!(f, "AlgorithmResultNew {{")?; + writeln!(f, " Algorithm Name: {:?}", self.algo_repr.algo_name)?; + writeln!(f, " Result Type: {:?}", self.algo_repr.result_type)?; + writeln!(f, " Number of Vertices: {:?}", self.result.len())?; + writeln!(f, " Results: [")?; + + for vertex in self.graph.vertices().iter() { + let value = self.result.get(&vertex.vertex.0); + writeln!(f, " {:?}: {:?}", vertex.name(), value)?; + } + + writeln!(f, " ]")?; + writeln!(f, "}}") } } /// Add tests for all functions #[cfg(test)] mod algorithm_result_test { - use crate::algorithms::algorithm_result::AlgorithmResult; + use super::*; + use crate::{ + algorithms::community_detection::connected_components::weakly_connected_components, + db::{ + api::{mutation::AdditionOps, view::GraphViewOps}, + graph::graph::Graph, + }, + prelude::{NO_PROPS, *}, + }; use ordered_float::OrderedFloat; - use std::collections::HashMap; - fn create_algo_result_u64() -> AlgorithmResult { - let mut map: HashMap = HashMap::new(); - map.insert("A".to_string(), 10); - map.insert("B".to_string(), 20); - map.insert("C".to_string(), 30); - AlgorithmResult::new("create_algo_result_u64_test", "", map) - } - - fn group_by_test() -> AlgorithmResult { - let mut map: HashMap = HashMap::new(); - map.insert("A".to_string(), 10); - map.insert("B".to_string(), 20); - map.insert("C".to_string(), 30); - map.insert("D".to_string(), 10); - AlgorithmResult::new("group_by_test", "", map) - } - - fn create_algo_result_f64() -> AlgorithmResult> { - let mut map: HashMap = HashMap::new(); - map.insert("A".to_string(), 10.0); - map.insert("B".to_string(), 20.0); - map.insert("C".to_string(), 30.0); - AlgorithmResult::new("create_algo_result_f64", "", map) + fn create_algo_result_u64() -> AlgorithmResult { + let g = create_graph(); + let mut map: HashMap = HashMap::new(); + map.insert(g.vertex("A").unwrap().vertex.0, 10); + map.insert(g.vertex("B").unwrap().vertex.0, 20); + map.insert(g.vertex("C").unwrap().vertex.0, 30); + let results_type = std::any::type_name::(); + AlgorithmResult::new(g, "create_algo_result_u64_test", results_type, map) + } + + fn create_graph() -> Graph { + let g = Graph::new(); + g.add_vertex(0, "A", NO_PROPS) + .expect("Could not add vertex to graph"); + g.add_vertex(0, "B", NO_PROPS) + .expect("Could not add vertex to graph"); + g.add_vertex(0, "C", NO_PROPS) + .expect("Could not add vertex to graph"); + g.add_vertex(0, "D", NO_PROPS) + .expect("Could not add vertex to graph"); + g + } + + fn group_by_test() -> AlgorithmResult { + let g = create_graph(); + let mut map: HashMap = HashMap::new(); + map.insert(g.vertex("A").unwrap().vertex.0, 10); + map.insert(g.vertex("B").unwrap().vertex.0, 20); + map.insert(g.vertex("C").unwrap().vertex.0, 30); + map.insert(g.vertex("D").unwrap().vertex.0, 10); + let results_type = std::any::type_name::(); + AlgorithmResult::new(g, "group_by_test", results_type, map) + } + + fn create_algo_result_f64() -> AlgorithmResult> { + let g = Graph::new(); + g.add_vertex(0, "A", NO_PROPS) + .expect("Could not add vertex to graph"); + g.add_vertex(0, "B", NO_PROPS) + .expect("Could not add vertex to graph"); + g.add_vertex(0, "C", NO_PROPS) + .expect("Could not add vertex to graph"); + g.add_vertex(0, "D", NO_PROPS) + .expect("Could not add vertex to graph"); + let mut map: HashMap = HashMap::new(); + map.insert(g.vertex("A").unwrap().vertex.0, 10.0); + map.insert(g.vertex("B").unwrap().vertex.0, 20.0); + map.insert(g.vertex("C").unwrap().vertex.0, 30.0); + let results_type = std::any::type_name::(); + AlgorithmResult::new(g, "create_algo_result_u64_test", results_type, map) } fn create_algo_result_tuple( - ) -> AlgorithmResult, OrderedFloat)> { - let mut map: HashMap = HashMap::new(); - map.insert("A".to_string(), (10.0, 20.0)); - map.insert("B".to_string(), (20.0, 30.0)); - map.insert("C".to_string(), (30.0, 40.0)); - AlgorithmResult::new("create_algo_result_tuple", "", map) - } - - fn create_algo_result_hashmap_vec() -> AlgorithmResult> { - let mut map: HashMap> = HashMap::new(); - map.insert("A".to_string(), vec![(11, "H".to_string())]); - map.insert("B".to_string(), vec![]); - map.insert( - "C".to_string(), + ) -> AlgorithmResult, OrderedFloat)> { + let g = create_graph(); + let mut res: HashMap = HashMap::new(); + res.insert(g.vertex("A").unwrap().vertex.0, (10.0, 20.0)); + res.insert(g.vertex("B").unwrap().vertex.0, (20.0, 30.0)); + res.insert(g.vertex("C").unwrap().vertex.0, (30.0, 40.0)); + let results_type = std::any::type_name::<(f32, f32)>(); + AlgorithmResult::new(g, "create_algo_result_tuple", results_type, res) + } + + fn create_algo_result_hashmap_vec() -> AlgorithmResult> { + let g = create_graph(); + let mut res: HashMap> = HashMap::new(); + res.insert(g.vertex("A").unwrap().vertex.0, vec![(11, "H".to_string())]); + res.insert(g.vertex("B").unwrap().vertex.0, vec![]); + res.insert( + g.vertex("C").unwrap().vertex.0, vec![(22, "E".to_string()), (33, "F".to_string())], ); - AlgorithmResult::new("create_algo_result_hashmap_vec", "", map) + let results_type = std::any::type_name::<(i32, String)>(); + AlgorithmResult::new(g, "create_algo_result_hashmap_vec", results_type, res) } #[test] fn test_min_max_value() { let algo_result = create_algo_result_u64(); - assert_eq!(algo_result.min(), Some(("A".to_string(), 10u64))); - assert_eq!(algo_result.max(), Some(("C".to_string(), 30u64))); - assert_eq!(algo_result.median(), Some(("B".to_string(), 20u64))); + let v_a = algo_result.graph.vertex("A".to_string()).unwrap(); + let v_b = algo_result.graph.vertex("B".to_string()).unwrap(); + let v_c = algo_result.graph.vertex("C".to_string()).unwrap(); + assert_eq!(algo_result.min(), Some((v_a.clone(), Some(10u64)))); + assert_eq!(algo_result.max(), Some((v_c.clone(), Some(30u64)))); + assert_eq!(algo_result.median(), Some((v_b.clone(), Some(20u64)))); let algo_result = create_algo_result_f64(); - assert_eq!(algo_result.min(), Some(("A".to_string(), 10.0))); - assert_eq!(algo_result.max(), Some(("C".to_string(), 30.0))); - assert_eq!(algo_result.median(), Some(("B".to_string(), 20.0))); + assert_eq!(algo_result.min(), Some((v_a, Some(10.0)))); + assert_eq!(algo_result.max(), Some((v_c, Some(30.0)))); + assert_eq!(algo_result.median(), Some((v_b, Some(20.0)))); } #[test] fn test_get() { let algo_result = create_algo_result_u64(); - assert_eq!(algo_result.get(&"C".to_string()), Some(&30)); - assert_eq!(algo_result.get(&"D".to_string()), None); + let vertex_c = algo_result.graph.vertex("C").unwrap(); + let vertex_d = algo_result.graph.vertex("D").unwrap(); + assert_eq!(algo_result.get(vertex_c.clone().into()), Some(&30u64)); + assert_eq!(algo_result.get(vertex_d.into()), None); let algo_result = create_algo_result_f64(); - assert_eq!(algo_result.get(&"C".to_string()), Some(&30.0)); + assert_eq!(algo_result.get(vertex_c.clone().into()), Some(&30.0f64)); let algo_result = create_algo_result_tuple(); - assert_eq!(algo_result.get(&"C".to_string()).unwrap().0, 30.0); + assert_eq!(algo_result.get(vertex_c.clone().into()).unwrap().0, 30.0f32); let algo_result = create_algo_result_hashmap_vec(); - assert_eq!(algo_result.get(&"C".to_string()).unwrap()[0].0, 22); + let answer = algo_result + .get(vertex_c.clone().into()) + .unwrap() + .get(0) + .unwrap() + .0; + assert_eq!(answer, 22i32); } #[test] fn test_sort() { let algo_result = create_algo_result_u64(); let sorted = algo_result.sort_by_value(true); - assert_eq!(sorted[0].0, "C"); + let v_c = algo_result.graph.vertex("C").unwrap(); + let v_d = algo_result.graph.vertex("D").unwrap(); + let v_a = algo_result.graph.vertex("A").unwrap(); + assert_eq!(sorted[0].0, v_c.clone()); let sorted = algo_result.sort_by_value(false); - assert_eq!(sorted[0].0, "A"); + assert_eq!(sorted[0].0, v_d.clone()); let algo_result = create_algo_result_f64(); let sorted = algo_result.sort_by_value(true); - assert_eq!(sorted[0].0, "C"); + assert_eq!(sorted[0].0, v_c.clone()); let sorted = algo_result.sort_by_value(false); - assert_eq!(sorted[0].0, "A"); + assert_eq!(sorted[0].0, v_d); + assert_eq!(sorted[1].0, v_a); let algo_result = create_algo_result_tuple(); - assert_eq!(algo_result.sort_by_value(true)[0].0, "C"); + assert_eq!(algo_result.sort_by_value(true)[0].0, v_c.clone()); let algo_result = create_algo_result_hashmap_vec(); - assert_eq!(algo_result.sort_by_value(true)[0].0, "C"); + assert_eq!(algo_result.sort_by_value(true)[0].0, v_c); } #[test] fn test_top_k() { let algo_result = create_algo_result_u64(); + let v_c = algo_result.graph.vertex("C").unwrap(); + let v_d = algo_result.graph.vertex("D").unwrap(); + let v_a = algo_result.graph.vertex("A").unwrap(); + let v_b = algo_result.graph.vertex("B").unwrap(); + let top_k = algo_result.top_k(2, false, false); - assert_eq!(top_k[0].0, "A"); + assert_eq!(top_k[0].0, v_d.clone()); let top_k = algo_result.top_k(2, false, true); - assert_eq!(top_k[0].0, "C"); + assert_eq!(top_k[0].0, v_c.clone()); let algo_result = create_algo_result_f64(); let top_k = algo_result.top_k(2, false, false); - assert_eq!(top_k[0].0, "A"); + assert_eq!(top_k[0].0, v_d.clone()); + assert_eq!(top_k[1].0, v_a.clone()); let top_k = algo_result.top_k(2, false, true); - assert_eq!(top_k[0].0, "C"); + assert_eq!(top_k[0].0, v_c); let algo_result = create_algo_result_tuple(); - assert_eq!(algo_result.top_k(2, false, false)[0].0, "A"); + assert_eq!(algo_result.top_k(2, false, false)[0].0, v_d.clone()); + assert_eq!(algo_result.top_k(2, false, false)[1].0, v_a); let algo_result = create_algo_result_hashmap_vec(); - assert_eq!(algo_result.top_k(2, false, false)[0].0, "B"); + assert_eq!(algo_result.top_k(2, false, false)[0].0, v_d); + assert_eq!(algo_result.top_k(2, false, false)[1].0, v_b); } #[test] @@ -475,65 +634,125 @@ mod algorithm_result_test { } #[test] - fn test_get_all() { + fn test_get_all_with_names() { let algo_result = create_algo_result_u64(); - let all = algo_result.get_all(); + let all = algo_result.get_all_values(); assert_eq!(all.len(), 3); - assert!(all.contains_key("A")); let algo_result = create_algo_result_f64(); - let all = algo_result.get_all(); + let all = algo_result.get_all_values(); assert_eq!(all.len(), 3); - assert!(all.contains_key("A")); let algo_result = create_algo_result_tuple(); - assert_eq!(algo_result.get_all().get("A").unwrap().0, 10.0); - assert_eq!(algo_result.get_all().len(), 3); + let algo_results_hashmap = algo_result.get_all_with_names(); + let tuple_result = algo_results_hashmap.get("A").unwrap(); + assert_eq!(tuple_result.unwrap().0, 10.0); + assert_eq!(algo_result.get_all_values().len(), 3); let algo_result = create_algo_result_hashmap_vec(); - assert_eq!(algo_result.get_all().get("A").unwrap()[0].0, 11); - assert_eq!(algo_result.get_all().len(), 3); + let algo_results_hashmap = algo_result.get_all_with_names(); + let tuple_result = algo_results_hashmap.get("A").unwrap(); + assert_eq!(tuple_result.clone().unwrap().get(0).unwrap().0, 11); + assert_eq!(algo_result.get_all_values().len(), 3); } #[test] - fn test_sort_by_key() { + fn test_sort_by_vertex() { let algo_result = create_algo_result_u64(); - let sorted = algo_result.sort_by_key(true); - let my_array: Vec<(String, u64)> = vec![ - ("C".to_string(), 30u64), - ("B".to_string(), 20u64), - ("A".to_string(), 10u64), + let v_c = algo_result.graph.vertex("C").unwrap(); + let v_d = algo_result.graph.vertex("D").unwrap(); + let v_a = algo_result.graph.vertex("A").unwrap(); + let v_b = algo_result.graph.vertex("B").unwrap(); + let sorted = algo_result.sort_by_vertex(true); + let my_array: Vec<(VertexView, Option)> = vec![ + (v_d, None), + (v_c, Some(30u64)), + (v_b, Some(20u64)), + (v_a, Some(10u64)), ]; assert_eq!(my_array, sorted); - // + let algo_result = create_algo_result_f64(); - let sorted = algo_result.sort_by_key(true); - let my_array: Vec<(String, f64)> = vec![ - ("C".to_string(), 30.0), - ("B".to_string(), 20.0), - ("A".to_string(), 10.0), + let v_c = algo_result.graph.vertex("C").unwrap(); + let v_d = algo_result.graph.vertex("D").unwrap(); + let v_a = algo_result.graph.vertex("A").unwrap(); + let v_b = algo_result.graph.vertex("B").unwrap(); + let sorted = algo_result.sort_by_vertex(true); + let my_array: Vec<(VertexView, Option)> = vec![ + (v_d, None), + (v_c, Some(30.0)), + (v_b, Some(20.0)), + (v_a, Some(10.0)), ]; assert_eq!(my_array, sorted); - // + let algo_result = create_algo_result_tuple(); - let sorted = algo_result.sort_by_key(true); - let my_array: Vec<(String, (f32, f32))> = vec![ - ("C".to_string(), (30.0, 40.0)), - ("B".to_string(), (20.0, 30.0)), - ("A".to_string(), (10.0, 20.0)), + let v_c = algo_result.graph.vertex("C").unwrap(); + let v_d = algo_result.graph.vertex("D").unwrap(); + let v_a = algo_result.graph.vertex("A").unwrap(); + let v_b = algo_result.graph.vertex("B").unwrap(); + + let sorted = algo_result.sort_by_vertex(true); + let my_array: Vec<(VertexView, Option<(f32, f32)>)> = vec![ + (v_d, None), + (v_c, Some((30.0, 40.0))), + (v_b, Some((20.0, 30.0))), + (v_a, Some((10.0, 20.0))), ]; assert_eq!(my_array, sorted); // let algo_result = create_algo_result_hashmap_vec(); - let sorted = algo_result.sort_by_key(true); - let my_array: Vec<(String, Vec<(i64, String)>)> = vec![ - ( - "C".to_string(), - vec![(22, "E".to_string()), (33, "F".to_string())], - ), - ("B".to_string(), vec![]), - ("A".to_string(), vec![(11, "H".to_string())]), + let v_c = algo_result.graph.vertex("C").unwrap(); + let v_d = algo_result.graph.vertex("D").unwrap(); + let v_a = algo_result.graph.vertex("A").unwrap(); + let v_b = algo_result.graph.vertex("B").unwrap(); + + let sorted = algo_result.sort_by_vertex(true); + let vec_c = vec![(22, "E".to_string()), (33, "F".to_string())]; + let vec_b = vec![]; + let vec_a = vec![(11, "H".to_string())]; + let my_array: Vec<(VertexView, Option>)> = vec![ + (v_d, None), + (v_c, Some(vec_c)), + (v_b, Some(vec_b)), + (v_a, Some(vec_a)), ]; assert_eq!(my_array, sorted); } + + #[test] + fn test_get_all() { + let algo_result = create_algo_result_u64(); + let gotten_all = algo_result.get_all(); + let names: Vec = gotten_all.keys().map(|vv| vv.name()).collect(); + println!("{:?}", names) + } + + #[test] + fn test_windowed_graph() { + let g = Graph::new(); + g.add_edge(0, 1, 2, NO_PROPS, Some("ZERO-TWO")) + .expect("Unable to add edge"); + g.add_edge(1, 1, 3, NO_PROPS, Some("ZERO-TWO")) + .expect("Unable to add edge"); + g.add_edge(2, 4, 5, NO_PROPS, Some("ZERO-TWO")) + .expect("Unable to add edge"); + g.add_edge(3, 6, 7, NO_PROPS, Some("THREE-FIVE")) + .expect("Unable to add edge"); + g.add_edge(4, 8, 9, NO_PROPS, Some("THREE-FIVE")) + .expect("Unable to add edge"); + let g_layer = g.layer(vec!["ZERO-TWO"]).unwrap(); + let res_window = weakly_connected_components(&g_layer, 20, None); + let mut expected_result: HashMap> = HashMap::new(); + expected_result.insert("8".to_string(), Some(8)); + expected_result.insert("1".to_string(), Some(1)); + expected_result.insert("3".to_string(), Some(1)); + expected_result.insert("2".to_string(), Some(1)); + expected_result.insert("5".to_string(), Some(4)); + expected_result.insert("6".to_string(), Some(6)); + expected_result.insert("7".to_string(), Some(7)); + expected_result.insert("4".to_string(), Some(4)); + expected_result.insert("9".to_string(), Some(9)); + assert_eq!(res_window.get_all_with_names(), expected_result); + } } diff --git a/raphtory/src/algorithms/centrality/betweenness.rs b/raphtory/src/algorithms/centrality/betweenness.rs new file mode 100644 index 0000000000..88fd4e8232 --- /dev/null +++ b/raphtory/src/algorithms/centrality/betweenness.rs @@ -0,0 +1,164 @@ +use crate::{ + algorithms::algorithm_result::AlgorithmResult, + core::entities::VID, + db::graph::vertex::VertexView, + prelude::{GraphViewOps, VertexViewOps}, +}; +use ordered_float::OrderedFloat; +use std::collections::{HashMap, VecDeque}; + +/// Computes the betweenness centrality for nodes in a given graph. +/// +/// # Parameters +/// +/// - `g`: A reference to the graph. +/// - `k`: An `Option` specifying the number of nodes to consider for the centrality computation. Defaults to all nodes if `None`. +/// - `normalized`: A `Option` indicating whether to normalize the centrality values. +/// +/// # Returns +/// +/// Returns an `AlgorithmResult` containing the betweenness centrality of each node. +pub fn betweenness_centrality( + g: &G, + k: Option, + normalized: Option, +) -> AlgorithmResult> { + // Initialize a hashmap to store betweenness centrality values. + let mut betweenness: HashMap = HashMap::new(); + + // Get the vertices and the total number of vertices in the graph. + let nodes = g.vertices(); + let n = g.count_vertices(); + let k_sample = k.unwrap_or(n); + + // Main loop over each node to compute betweenness centrality. + for node in nodes.iter().take(k_sample) { + let mut stack = Vec::new(); + let mut predecessors: HashMap> = HashMap::new(); + let mut sigma: HashMap = HashMap::new(); + let mut dist: HashMap = HashMap::new(); + let mut queue = VecDeque::new(); + + // Initialize distance and sigma values for each node. + for node in nodes.iter() { + dist.insert(node.vertex.0, -1); + sigma.insert(node.vertex.0, 0.0); + } + dist.insert(node.vertex.0, 0); + sigma.insert(node.vertex.0, 1.0); + queue.push_back(node.vertex.0); + + // BFS loop to find shortest paths. + while let Some(current_node_id) = queue.pop_front() { + stack.push(current_node_id); + for neighbor in + VertexView::new_internal(g.clone(), VID::from(current_node_id)).out_neighbours() + { + // Path discovery + if dist[&neighbor.vertex.0] < 0 { + queue.push_back(neighbor.vertex.0); + dist.insert(neighbor.vertex.0, dist[¤t_node_id] + 1); + } + // Path counting + if dist[&neighbor.vertex.0] == dist[¤t_node_id] + 1 { + sigma.insert( + neighbor.vertex.0, + sigma[&neighbor.vertex.0] + sigma[¤t_node_id], + ); + predecessors + .entry(neighbor.vertex.0) + .or_insert_with(Vec::new) + .push(current_node_id); + } + } + } + + let mut delta: HashMap = HashMap::new(); + for node in nodes.iter() { + delta.insert(node.vertex.0, 0.0); + } + + // Accumulation + while let Some(w) = stack.pop() { + for v in predecessors.get(&w).unwrap_or(&Vec::new()) { + let coeff = (sigma[v] / sigma[&w]) * (1.0 + delta[&w]); + let new_delta_v = delta[v] + coeff; + delta.insert(*v, new_delta_v); + } + if w != node.vertex.0 { + let updated_betweenness = betweenness.entry(w).or_insert(0.0); + *updated_betweenness += delta[&w]; + } + } + } + + // Normalization + if let Some(true) = normalized { + let factor = 1.0 / ((n as f64 - 1.0) * (n as f64 - 2.0)); + for node in nodes.iter() { + if betweenness.contains_key(&node.vertex.0) { + betweenness.insert(node.vertex.0, betweenness[&node.vertex.0] * factor); + } else { + betweenness.insert(node.vertex.0, 0.0f64); + } + } + } else { + for node in nodes.iter() { + if !betweenness.contains_key(&node.vertex.0) { + betweenness.insert(node.vertex.0, 0.0f64); + } + } + } + + // Construct and return the AlgorithmResult + let results_type = std::any::type_name::(); + AlgorithmResult::new(g.clone(), "Betweenness", results_type, betweenness) +} + +#[cfg(test)] +mod betweenness_centrality_test { + use super::*; + use crate::prelude::*; + + #[test] + fn test_betweenness_centrality() { + let graph = Graph::new(); + let vs = vec![ + (1, 2), + (1, 3), + (1, 4), + (2, 3), + (2, 4), + (2, 5), + (3, 4), + (3, 5), + (3, 6), + (4, 3), + (4, 2), + (4, 4), + ]; + for (src, dst) in &vs { + graph.add_edge(0, *src, *dst, NO_PROPS, None).unwrap(); + } + let mut expected: HashMap> = HashMap::new(); + expected.insert("1".to_string(), Some(0.0)); + expected.insert("2".to_string(), Some(1.0)); + expected.insert("3".to_string(), Some(4.0)); + expected.insert("4".to_string(), Some(1.0)); + expected.insert("5".to_string(), Some(0.0)); + expected.insert("6".to_string(), Some(0.0)); + + let res = betweenness_centrality(&graph, None, Some(false)); + assert_eq!(res.get_all_with_names(), expected); + + let mut expected: HashMap> = HashMap::new(); + expected.insert("1".to_string(), Some(0.0)); + expected.insert("2".to_string(), Some(0.05)); + expected.insert("3".to_string(), Some(0.2)); + expected.insert("4".to_string(), Some(0.05)); + expected.insert("5".to_string(), Some(0.0)); + expected.insert("6".to_string(), Some(0.0)); + let res = betweenness_centrality(&graph, None, Some(true)); + assert_eq!(res.get_all_with_names(), expected); + } +} diff --git a/raphtory/src/algorithms/centrality/degree_centrality.rs b/raphtory/src/algorithms/centrality/degree_centrality.rs index e067ffbad4..ca3bb98995 100644 --- a/raphtory/src/algorithms/centrality/degree_centrality.rs +++ b/raphtory/src/algorithms/centrality/degree_centrality.rs @@ -7,10 +7,9 @@ use crate::{ task_runner::TaskRunner, vertex::eval_vertex::EvalVertexView, }, - prelude::{GraphViewOps, VertexViewOps}, + prelude::*, }; use ordered_float::OrderedFloat; -use std::collections::HashMap; /// Computes the degree centrality of all vertices in the graph. The values are normalized /// by dividing each result with the maximum possible degree. Graphs with self-loops can have @@ -18,7 +17,7 @@ use std::collections::HashMap; pub fn degree_centrality( g: &G, threads: Option, -) -> AlgorithmResult> { +) -> AlgorithmResult> { let max_degree = max_degree(g); let mut ctx: Context = g.into(); @@ -42,22 +41,18 @@ pub fn degree_centrality( ); let mut runner: TaskRunner = TaskRunner::new(ctx); - let results_type = std::any::type_name::>(); - - AlgorithmResult::new( - "Reciprocity", - results_type, - runner.run( - vec![], - vec![Job::new(step1)], - None, - |_, ess, _, _| ess.finalize(&min, |min| min), - threads, - 1, - None, - None, - ), - ) + let runner_result = runner.run( + vec![], + vec![Job::new(step1)], + None, + |_, ess, _, _| ess.finalize(&min, |min| min), + threads, + 1, + None, + None, + ); + let results_type = std::any::type_name::(); + AlgorithmResult::new(g.clone(), "Degree Centrality", results_type, runner_result) } #[cfg(test)] @@ -72,17 +67,18 @@ mod degree_centrality_test { #[test] fn test_degree_centrality() { let graph = Graph::new(); - let vs = vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)]; + let vs = vec![(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)]; for (src, dst) in &vs { graph.add_edge(0, *src, *dst, NO_PROPS, None).unwrap(); } - let mut hash_map_result: HashMap = HashMap::new(); - hash_map_result.insert("0".to_string(), 1.0); - hash_map_result.insert("1".to_string(), 1.0); - hash_map_result.insert("2".to_string(), 2.0 / 3.0); - hash_map_result.insert("3".to_string(), 2.0 / 3.0); + let mut hash_map_result: HashMap> = HashMap::new(); + hash_map_result.insert("1".to_string(), Some(1.0)); + hash_map_result.insert("2".to_string(), Some(1.0)); + hash_map_result.insert("3".to_string(), Some(2.0 / 3.0)); + hash_map_result.insert("4".to_string(), Some(2.0 / 3.0)); - let res = degree_centrality(&graph, None); - assert_eq!(res.get_all(), &hash_map_result); + let binding = degree_centrality(&graph, None); + let res = binding.get_all_with_names(); + assert_eq!(res, hash_map_result); } } diff --git a/raphtory/src/algorithms/centrality/hits.rs b/raphtory/src/algorithms/centrality/hits.rs index f4f57bf5c1..697ce74407 100644 --- a/raphtory/src/algorithms/centrality/hits.rs +++ b/raphtory/src/algorithms/centrality/hits.rs @@ -51,7 +51,7 @@ pub fn hits( g: &G, iter_count: usize, threads: Option, -) -> AlgorithmResult, OrderedFloat)> { +) -> AlgorithmResult, OrderedFloat)> { let mut ctx: Context = g.into(); let recv_hub_score = sum::(2); @@ -159,20 +159,21 @@ pub fn hits( None, ); - let mut results: HashMap = HashMap::new(); + let mut results: HashMap = HashMap::new(); hub_scores.into_iter().for_each(|(k, v)| { - results.insert(k, (v, 0.0)); + results.insert(g.vertex(k).unwrap().vertex.0, (v, 0.0)); }); auth_scores.into_iter().for_each(|(k, v)| { - let (a, _) = results.get(&k).unwrap(); - results.insert(k, (*a, v)); + let vid = g.vertex(k).unwrap().vertex.0; + let (a, _) = results.get(&vid).unwrap(); + results.insert(vid, (*a, v)); }); - let results_type = std::any::type_name::>(); + let results_type = std::any::type_name::<(f32, f32)>(); - AlgorithmResult::new("Hits", results_type, results) + AlgorithmResult::new(g.clone(), "Hits", results_type, results) } #[cfg(test)] @@ -212,49 +213,20 @@ mod hits_tests { (8, 1), ]); - let results = hits(&graph, 20, None); - - // NetworkX results - // >>> G = nx.DiGraph() - // >>> G.add_edge(1, 4) - // >>> G.add_edge(2, 3) - // >>> G.add_edge(2, 5) - // >>> G.add_edge(3, 1) - // >>> G.add_edge(4, 2) - // >>> G.add_edge(4, 3) - // >>> G.add_edge(5, 2) - // >>> G.add_edge(5, 3) - // >>> G.add_edge(5, 4) - // >>> G.add_edge(5, 6) - // >>> G.add_edge(6,3) - // >>> G.add_edge(6,8) - // >>> G.add_edge(7,1) - // >>> G.add_edge(7,3) - // >>> G.add_edge(8,1) - // >>> nx.hits(G) - // ( - // (1, (0.04305010876408988, 0.08751958702900825)), - // (2, (0.14444089276992705, 0.18704574169397797)), - // (3, (0.02950848945012511, 0.3690360954887363)), - // (4, (0.1874910015340169, 0.12768284011810235)), - // (5, (0.26762580040598083, 0.05936290157587567)), - // (6, (0.144440892769927, 0.10998993251842377)), - // (7, (0.15393432485580819, 5.645162243895331e-17)) - // (8, (0.02950848945012511, 0.05936290157587556)), - // ) + let results = hits(&graph, 20, None).get_all_with_names(); assert_eq!( - results.sort_by_key(false), - vec![ - ("1".to_string(), (0.0431365, 0.096625775)), - ("2".to_string(), (0.14359662, 0.18366566)), - ("3".to_string(), (0.030866561, 0.36886504)), - ("4".to_string(), (0.1865414, 0.12442485)), - ("5".to_string(), (0.26667944, 0.05943252)), - ("6".to_string(), (0.14359662, 0.10755368)), - ("7".to_string(), (0.15471625, 0.0)), - ("8".to_string(), (0.030866561, 0.05943252)) - ] + results, + HashMap::from([ + ("1".to_string(), Some((0.0431365, 0.096625775))), + ("2".to_string(), Some((0.14359662, 0.18366566))), + ("3".to_string(), Some((0.030866561, 0.36886504))), + ("4".to_string(), Some((0.1865414, 0.12442485))), + ("5".to_string(), Some((0.26667944, 0.05943252))), + ("6".to_string(), Some((0.14359662, 0.10755368))), + ("7".to_string(), Some((0.15471625, 0.0))), + ("8".to_string(), Some((0.030866561, 0.05943252))) + ]) ); } } diff --git a/raphtory/src/algorithms/centrality/mod.rs b/raphtory/src/algorithms/centrality/mod.rs index 729d9deeb9..64fce608c6 100644 --- a/raphtory/src/algorithms/centrality/mod.rs +++ b/raphtory/src/algorithms/centrality/mod.rs @@ -1,3 +1,4 @@ +pub mod betweenness; pub mod degree_centrality; pub mod hits; pub mod pagerank; diff --git a/raphtory/src/algorithms/centrality/pagerank.rs b/raphtory/src/algorithms/centrality/pagerank.rs index a4c8125966..746627a229 100644 --- a/raphtory/src/algorithms/centrality/pagerank.rs +++ b/raphtory/src/algorithms/centrality/pagerank.rs @@ -1,7 +1,7 @@ use crate::{ algorithms::algorithm_result::AlgorithmResult, core::{ - entities::{vertices::vertex_ref::VertexRef, VID}, + entities::vertices::vertex_ref::VertexRef, state::{accumulator_id::accumulators, compute_state::ComputeStateVec}, }, db::{ @@ -58,7 +58,7 @@ pub fn unweighted_page_rank( threads: Option, tol: Option, use_l2_norm: bool, -) -> AlgorithmResult> { +) -> AlgorithmResult> { let n = g.count_vertices(); let total_edges = g.count_edges(); @@ -157,8 +157,8 @@ pub fn unweighted_page_rank( let mut runner: TaskRunner = TaskRunner::new(ctx); let num_vertices = g.count_vertices(); - let results_type = std::any::type_name::>(); - let out: HashMap = runner.run( + + let out: HashMap = runner.run( vec![Job::new(step1)], vec![Job::new(step2), Job::new(step3), Job::new(step4), step5], Some(vec![PageRankState::new(num_vertices); num_vertices]), @@ -170,9 +170,9 @@ pub fn unweighted_page_rank( .enumerate() .filter_map(|(v_ref, score)| { g.has_vertex_ref(VertexRef::Internal(v_ref.into()), &layers, edge_filter) - .then_some((v_ref.into(), score.score)) + .then_some((v_ref, score.score)) }) - .collect::>() + .collect::>() }, threads, iter_count, @@ -180,12 +180,8 @@ pub fn unweighted_page_rank( None, ); - let res: HashMap = out - .into_iter() - .map(|(k, v)| (g.vertex_name(k), v)) - .collect(); - - AlgorithmResult::new("Pagerank", results_type, res) + let results_type = std::any::type_name::(); + AlgorithmResult::new(g.clone(), "Pagerank", results_type, out) } #[cfg(test)] @@ -219,10 +215,10 @@ mod page_rank_tests { let results = unweighted_page_rank(&graph, 1000, Some(1), None, true); - assert_eq_f64(results.get("1"), Some(&0.38694), 5); - assert_eq_f64(results.get("2"), Some(&0.20195), 5); - assert_eq_f64(results.get("4"), Some(&0.20195), 5); - assert_eq_f64(results.get("3"), Some(&0.20916), 5); + assert_eq_f64(results.get("1".into()), Some(&0.38694), 5); + assert_eq_f64(results.get("2".into()), Some(&0.20195), 5); + assert_eq_f64(results.get("4".into()), Some(&0.20195), 5); + assert_eq_f64(results.get("3".into()), Some(&0.20916), 5); } #[test] @@ -261,17 +257,17 @@ mod page_rank_tests { let results = unweighted_page_rank(&graph, 1000, Some(4), None, true); - assert_eq_f64(results.get("10"), Some(&0.072082), 5); - assert_eq_f64(results.get("8"), Some(&0.136473), 5); - assert_eq_f64(results.get("3"), Some(&0.15484), 5); - assert_eq_f64(results.get("6"), Some(&0.07208), 5); - assert_eq_f64(results.get("11"), Some(&0.06186), 5); - assert_eq_f64(results.get("2"), Some(&0.03557), 5); - assert_eq_f64(results.get("1"), Some(&0.11284), 5); - assert_eq_f64(results.get("4"), Some(&0.07944), 5); - assert_eq_f64(results.get("7"), Some(&0.01638), 5); - assert_eq_f64(results.get("9"), Some(&0.06186), 5); - assert_eq_f64(results.get("5"), Some(&0.19658), 5); + assert_eq_f64(results.get("10".into()), Some(&0.072082), 5); + assert_eq_f64(results.get("8".into()), Some(&0.136473), 5); + assert_eq_f64(results.get("3".into()), Some(&0.15484), 5); + assert_eq_f64(results.get("6".into()), Some(&0.07208), 5); + assert_eq_f64(results.get("11".into()), Some(&0.06186), 5); + assert_eq_f64(results.get("2".into()), Some(&0.03557), 5); + assert_eq_f64(results.get("1".into()), Some(&0.11284), 5); + assert_eq_f64(results.get("4".into()), Some(&0.07944), 5); + assert_eq_f64(results.get("7".into()), Some(&0.01638), 5); + assert_eq_f64(results.get("9".into()), Some(&0.06186), 5); + assert_eq_f64(results.get("5".into()), Some(&0.19658), 5); } #[test] @@ -286,8 +282,8 @@ mod page_rank_tests { let results = unweighted_page_rank(&graph, 1000, Some(4), None, false); - assert_eq_f64(results.get("1"), Some(&0.5), 3); - assert_eq_f64(results.get("2"), Some(&0.5), 3); + assert_eq_f64(results.get("1".into()), Some(&0.5), 3); + assert_eq_f64(results.get("2".into()), Some(&0.5), 3); } #[test] @@ -302,9 +298,9 @@ mod page_rank_tests { let results = unweighted_page_rank(&graph, 10, Some(4), None, false); - assert_eq_f64(results.get("1"), Some(&0.303), 3); - assert_eq_f64(results.get("2"), Some(&0.393), 3); - assert_eq_f64(results.get("3"), Some(&0.303), 3); + assert_eq_f64(results.get("1".into()), Some(&0.303), 3); + assert_eq_f64(results.get("2".into()), Some(&0.393), 3); + assert_eq_f64(results.get("3".into()), Some(&0.303), 3); } #[test] @@ -338,17 +334,17 @@ mod page_rank_tests { let results = unweighted_page_rank(&graph, 1000, Some(4), None, true); - assert_eq_f64(results.get("1"), Some(&0.055), 3); - assert_eq_f64(results.get("2"), Some(&0.079), 3); - assert_eq_f64(results.get("3"), Some(&0.113), 3); - assert_eq_f64(results.get("4"), Some(&0.055), 3); - assert_eq_f64(results.get("5"), Some(&0.070), 3); - assert_eq_f64(results.get("6"), Some(&0.083), 3); - assert_eq_f64(results.get("7"), Some(&0.093), 3); - assert_eq_f64(results.get("8"), Some(&0.102), 3); - assert_eq_f64(results.get("9"), Some(&0.110), 3); - assert_eq_f64(results.get("10"), Some(&0.117), 3); - assert_eq_f64(results.get("11"), Some(&0.122), 3); + assert_eq_f64(results.get("1".into()), Some(&0.055), 3); + assert_eq_f64(results.get("2".into()), Some(&0.079), 3); + assert_eq_f64(results.get("3".into()), Some(&0.113), 3); + assert_eq_f64(results.get("4".into()), Some(&0.055), 3); + assert_eq_f64(results.get("5".into()), Some(&0.070), 3); + assert_eq_f64(results.get("6".into()), Some(&0.083), 3); + assert_eq_f64(results.get("7".into()), Some(&0.093), 3); + assert_eq_f64(results.get("8".into()), Some(&0.102), 3); + assert_eq_f64(results.get("9".into()), Some(&0.110), 3); + assert_eq_f64(results.get("10".into()), Some(&0.117), 3); + assert_eq_f64(results.get("11".into()), Some(&0.122), 3); } fn assert_eq_f64 + PartialEq + std::fmt::Debug>( diff --git a/raphtory/src/algorithms/community_detection/connected_components.rs b/raphtory/src/algorithms/community_detection/connected_components.rs index 90fa006fd2..6fade0ea7b 100644 --- a/raphtory/src/algorithms/community_detection/connected_components.rs +++ b/raphtory/src/algorithms/community_detection/connected_components.rs @@ -26,8 +26,8 @@ struct WccState { /// # Arguments /// /// * `g` - A reference to the graph -/// * `window` - A range indicating the temporal window to consider /// * `iter_count` - The number of iterations to run +/// * `threads` - Number of threads to use /// /// Returns: /// @@ -37,12 +37,11 @@ pub fn weakly_connected_components( graph: &G, iter_count: usize, threads: Option, -) -> AlgorithmResult +) -> AlgorithmResult where G: GraphViewOps, { let ctx: Context = graph.into(); - let step1 = ATask::new(move |vv| { let min_neighbour_id = vv.neighbours().id().min(); let id = vv.id(); @@ -71,23 +70,23 @@ where ); let mut runner: TaskRunner = TaskRunner::new(ctx); - let results_type = std::any::type_name::>(); + let results_type = std::any::type_name::(); let res = runner.run( vec![Job::new(step1)], vec![Job::read_only(step2)], None, - |_, _, _, local| { - let layers = graph.layer_ids(); + |_, _, _, local: Vec| { + let layers: crate::core::entities::LayerIds = graph.layer_ids(); let edge_filter = graph.edge_filter(); local .iter() .enumerate() - .filter_map(|(v_ref, state)| { - let v_ref = VID(v_ref); + .filter_map(|(v_ref_id, state)| { + let v_ref = VID(v_ref_id); graph .has_vertex_ref(VertexRef::Internal(v_ref), &layers, edge_filter) - .then_some((graph.vertex_name(v_ref), state.component)) + .then_some((v_ref_id, state.component)) }) .collect::>() }, @@ -96,17 +95,18 @@ where None, None, ); - AlgorithmResult::new("Connected Components", results_type, res) + AlgorithmResult::new(graph.clone(), "Connected Components", results_type, res) } #[cfg(test)] mod cc_test { use crate::prelude::*; + use std::cmp::Reverse; use super::*; use crate::db::api::mutation::AdditionOps; use itertools::*; - use std::{cmp::Reverse, iter::once}; + use std::iter::once; #[test] fn run_loop_simple_connected_components() { @@ -125,22 +125,21 @@ mod cc_test { for (src, dst, ts) in edges { graph.add_edge(ts, src, dst, NO_PROPS, None).unwrap(); } - let results: AlgorithmResult = - weakly_connected_components(&graph, usize::MAX, None); + let results = weakly_connected_components(&graph, usize::MAX, None).get_all_with_names(); assert_eq!( - *results.get_all(), + results, vec![ - ("1".to_string(), 1), - ("2".to_string(), 1), - ("3".to_string(), 1), - ("4".to_string(), 1), - ("5".to_string(), 1), - ("6".to_string(), 1), - ("7".to_string(), 7), - ("8".to_string(), 7), + ("1".to_string(), Some(1)), + ("2".to_string(), Some(1)), + ("3".to_string(), Some(1)), + ("4".to_string(), Some(1)), + ("5".to_string(), Some(1)), + ("6".to_string(), Some(1)), + ("7".to_string(), Some(7)), + ("8".to_string(), Some(7)), ] .into_iter() - .collect::>() + .collect::>>() ); } @@ -178,26 +177,25 @@ mod cc_test { graph.add_edge(ts, src, dst, NO_PROPS, None).unwrap(); } - let results: AlgorithmResult = - weakly_connected_components(&graph, usize::MAX, None); + let results = weakly_connected_components(&graph, usize::MAX, None).get_all_with_names(); assert_eq!( - *results.get_all(), + results, vec![ - ("1".to_string(), 1), - ("2".to_string(), 1), - ("3".to_string(), 1), - ("4".to_string(), 1), - ("5".to_string(), 1), - ("6".to_string(), 1), - ("7".to_string(), 1), - ("8".to_string(), 1), - ("9".to_string(), 1), - ("10".to_string(), 1), - ("11".to_string(), 1), + ("1".to_string(), Some(1)), + ("2".to_string(), Some(1)), + ("3".to_string(), Some(1)), + ("4".to_string(), Some(1)), + ("5".to_string(), Some(1)), + ("6".to_string(), Some(1)), + ("7".to_string(), Some(1)), + ("8".to_string(), Some(1)), + ("9".to_string(), Some(1)), + ("10".to_string(), Some(1)), + ("11".to_string(), Some(1)), ] .into_iter() - .collect::>() + .collect::>>() ); } @@ -212,14 +210,13 @@ mod cc_test { graph.add_edge(ts, src, dst, NO_PROPS, None).unwrap(); } - let results: AlgorithmResult = - weakly_connected_components(&graph, usize::MAX, None); + let results = weakly_connected_components(&graph, usize::MAX, None).get_all_with_names(); assert_eq!( - *results.get_all(), - vec![("1".to_string(), 1)] + results, + vec![("1".to_string(), Some(1))] .into_iter() - .collect::>() + .collect::>>() ); } @@ -231,37 +228,35 @@ mod cc_test { graph.add_edge(9, 3, 4, NO_PROPS, None).expect("add edge"); graph.add_edge(9, 4, 3, NO_PROPS, None).expect("add edge"); - let results: AlgorithmResult = - weakly_connected_components(&graph, usize::MAX, None); + let results = weakly_connected_components(&graph, usize::MAX, None).get_all_with_names(); let expected = vec![ - ("1".to_string(), 1), - ("2".to_string(), 1), - ("3".to_string(), 3), - ("4".to_string(), 3), + ("1".to_string(), Some(1)), + ("2".to_string(), Some(1)), + ("3".to_string(), Some(3)), + ("4".to_string(), Some(3)), ] .into_iter() - .collect::>(); + .collect::>>(); - assert_eq!(*results.get_all(), expected); + assert_eq!(results, expected); let wg = graph.window(0, 2); - let results: AlgorithmResult = - weakly_connected_components(&wg, usize::MAX, None); + let results = weakly_connected_components(&wg, usize::MAX, None).get_all_with_names(); let expected = vec![("1", 1), ("2", 1)] .into_iter() - .map(|(k, v)| (k.to_string(), v)) - .collect::>(); + .map(|(k, v)| (k.to_string(), Some(v))) + .collect::>>(); - assert_eq!(*results.get_all(), expected); + assert_eq!(results, expected); } #[quickcheck] - fn circle_graph_the_smallest_value_is_the_cc(vs: Vec) { + fn circle_graph_edges(vs: Vec) { if !vs.is_empty() { let vs = vs.into_iter().unique().collect::>(); - let smallest = vs.iter().min().unwrap(); + let _smallest = vs.iter().min().unwrap(); let first = vs[0]; // pairs of vertices from vs one after the next @@ -273,35 +268,44 @@ mod cc_test { assert_eq!(edges[0].0, first); assert_eq!(edges.last().unwrap().1, first); + } + } + #[quickcheck] + fn circle_graph_the_smallest_value_is_the_cc(vs: Vec) { + if !vs.is_empty() { let graph = Graph::new(); + let vs = vs.into_iter().unique().collect::>(); + + let smallest = vs.iter().min().unwrap(); + + let first = vs[0]; + + // pairs of vertices from vs one after the next + let edges = vs + .iter() + .zip(chain!(vs.iter().skip(1), once(&first))) + .map(|(a, b)| (*a, *b)) + .collect::>(); + for (src, dst) in edges.iter() { graph.add_edge(0, *src, *dst, NO_PROPS, None).unwrap(); } // now we do connected community_detection over window 0..1 - - let res: AlgorithmResult = - weakly_connected_components(&graph, usize::MAX, None); + let res = weakly_connected_components(&graph, usize::MAX, None).group_by(); let actual = res - .get_all() - .iter() - .group_by(|(_, cc)| *cc) .into_iter() - .map(|(cc, group)| (cc, Reverse(group.count()))) + .map(|(cc, group)| (cc, Reverse(group.len()))) .sorted_by(|l, r| l.1.cmp(&r.1)) - .map(|(cc, count)| (*cc, count.0)) + .map(|(cc, count)| (cc, count.0)) .take(1) - .next(); - - assert_eq!( - actual, - Some((*smallest, edges.len())), - "actual: {:?}", - actual - ); + .next() + .unwrap(); + + assert_eq!(actual, (*smallest, edges.len())); } } } diff --git a/raphtory/src/algorithms/metrics/balance.rs b/raphtory/src/algorithms/metrics/balance.rs index 134e10215a..f13aa50722 100644 --- a/raphtory/src/algorithms/metrics/balance.rs +++ b/raphtory/src/algorithms/metrics/balance.rs @@ -23,7 +23,6 @@ use crate::{ prelude::{EdgeListOps, PropUnwrap, VertexViewOps}, }; use ordered_float::OrderedFloat; -use std::collections::HashMap; /// Computes the net sum of weights for a given vertex based on edge direction. /// @@ -103,7 +102,7 @@ pub fn balance( name: String, direction: Direction, threads: Option, -) -> AlgorithmResult> { +) -> AlgorithmResult> { let mut ctx: Context = graph.into(); let min = sum(0); ctx.agg(min); @@ -113,22 +112,19 @@ pub fn balance( Step::Done }); let mut runner: TaskRunner = TaskRunner::new(ctx); - let results_type = std::any::type_name::>(); + let runner_result = runner.run( + vec![], + vec![Job::new(step1)], + None, + |_, ess, _, _| ess.finalize(&min, |min| min), + threads, + 1, + None, + None, + ); - AlgorithmResult::new( - "Balance", - results_type, - runner.run( - vec![], - vec![Job::new(step1)], - None, - |_, ess, _, _| ess.finalize(&min, |min| min), - threads, - 1, - None, - None, - ), - ) + let results_type = std::any::type_name::(); + AlgorithmResult::new(graph.clone(), "Balance", results_type, runner_result) } #[cfg(test)] @@ -137,8 +133,10 @@ mod sum_weight_test { algorithms::metrics::balance::balance, core::{Direction, Prop}, db::{api::mutation::AdditionOps, graph::graph::Graph}, + prelude::GraphViewOps, }; use pretty_assertions::assert_eq; + use std::collections::HashMap; #[test] fn test_sum_float_weights() { @@ -168,33 +166,38 @@ mod sum_weight_test { } let res = balance(&graph, "value_dec".to_string(), Direction::BOTH, None); - let expected = vec![ - ("1".to_string(), -26.0), - ("2".to_string(), 7.0), - ("3".to_string(), 12.0), - ("4".to_string(), 5.0), - ("5".to_string(), 2.0), - ]; - assert_eq!(res.sort_by_key(false), expected); + let vertex_one = graph.vertex("1").unwrap(); + let vertex_two = graph.vertex("2").unwrap(); + let vertex_three = graph.vertex("3").unwrap(); + let vertex_four = graph.vertex("4").unwrap(); + let vertex_five = graph.vertex("5").unwrap(); + let expected = HashMap::from([ + (vertex_one.clone(), Some(-26.0)), + (vertex_two.clone(), Some(7.0)), + (vertex_three.clone(), Some(12.0)), + (vertex_four.clone(), Some(5.0)), + (vertex_five.clone(), Some(2.0)), + ]); + assert_eq!(res.get_all(), expected); let res = balance(&graph, "value_dec".to_string(), Direction::IN, None); - let expected = vec![ - ("1".to_string(), 6.0), - ("2".to_string(), 12.0), - ("3".to_string(), 15.0), - ("4".to_string(), 20.0), - ("5".to_string(), 2.0), - ]; - assert_eq!(res.sort_by_key(false), expected); + let expected = HashMap::from([ + (vertex_one.clone(), Some(6.0)), + (vertex_two.clone(), Some(12.0)), + (vertex_three.clone(), Some(15.0)), + (vertex_four.clone(), Some(20.0)), + (vertex_five.clone(), Some(2.0)), + ]); + assert_eq!(res.get_all(), expected); let res = balance(&graph, "value_dec".to_string(), Direction::OUT, None); - let expected = vec![ - ("1".to_string(), -32.0), - ("2".to_string(), -5.0), - ("3".to_string(), -3.0), - ("4".to_string(), -15.0), - ("5".to_string(), 0.0), - ]; - assert_eq!(res.sort_by_key(false), expected); + let expected = HashMap::from([ + (vertex_one, Some(-32.0)), + (vertex_two, Some(-5.0)), + (vertex_three, Some(-3.0)), + (vertex_four, Some(-15.0)), + (vertex_five, Some(0.0)), + ]); + assert_eq!(res.get_all(), expected); } } diff --git a/raphtory/src/algorithms/metrics/reciprocity.rs b/raphtory/src/algorithms/metrics/reciprocity.rs index ffe68a8fd9..92bcee1e6c 100644 --- a/raphtory/src/algorithms/metrics/reciprocity.rs +++ b/raphtory/src/algorithms/metrics/reciprocity.rs @@ -40,7 +40,7 @@ //! g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); //! } //! -//! println!("all_local_reciprocity: {:?}", all_local_reciprocity(&g, None)); +//! println!("all_local_reciprocity: {:?}", all_local_reciprocity(&g, None).get_all_with_names()); //! println!("global_reciprocity: {:?}", global_reciprocity(&g, None)); //! ``` use crate::{ @@ -60,7 +60,7 @@ use crate::{ }, }; use ordered_float::OrderedFloat; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; /// Gets the unique edge counts excluding cycles for a vertex. Returns a tuple of usize /// (out neighbours, in neighbours, the intersection of the out and in neighbours) @@ -116,7 +116,7 @@ pub fn global_reciprocity(g: &G, threads: Option) -> f64 pub fn all_local_reciprocity( g: &G, threads: Option, -) -> AlgorithmResult> { +) -> AlgorithmResult> { let mut ctx: Context = g.into(); let min = sum(0); @@ -134,21 +134,23 @@ pub fn all_local_reciprocity( }); let mut runner: TaskRunner = TaskRunner::new(ctx); - let results_type = std::any::type_name::>(); + let runner_result = runner.run( + vec![], + vec![Job::new(step1)], + None, + |_, ess, _, _| ess.finalize(&min, |min| min), + threads, + 1, + None, + None, + ); + let results_type = std::any::type_name::(); AlgorithmResult::new( - "Reciprocity", + g.clone(), + "All Local Reciprocity", results_type, - runner.run( - vec![], - vec![Job::new(step1)], - None, - |_, ess, _, _| ess.finalize(&min, |min| min), - threads, - 1, - None, - None, - ), + runner_result, ) } @@ -192,6 +194,6 @@ mod reciprocity_test { hash_map_result.insert("5".to_string(), 0.0); let res = all_local_reciprocity(&graph, None); - assert_eq!(res.get("1"), hash_map_result.get("1")); + assert_eq!(res.get("1".into()), hash_map_result.get("1")); } } diff --git a/raphtory/src/algorithms/motifs/three_node_local_single_thread.rs b/raphtory/src/algorithms/motifs/three_node_local_single_thread.rs index c05ecddff3..b60167c3aa 100644 --- a/raphtory/src/algorithms/motifs/three_node_local_single_thread.rs +++ b/raphtory/src/algorithms/motifs/three_node_local_single_thread.rs @@ -105,7 +105,7 @@ fn twonode_motif_count(graph: &G, v: u64, delta: i64) -> [usize fn triangle_motif_count( graph: &G, delta: i64, -) -> AlgorithmResult> { +) -> AlgorithmResult, Vec> { let mut counts: HashMap> = HashMap::new(); for u in graph.vertices() { counts.insert(u.id(), vec![0; 8]); @@ -236,8 +236,19 @@ fn triangle_motif_count( } } - let results_type = std::any::type_name::>>(); - AlgorithmResult::new("Three node local single thread", results_type, counts) + // Made this as i did not want to modify/damage the above working algorithm + let new_counts: HashMap> = counts + .iter() + .map(|(uid, val)| (graph.vertex(*uid).unwrap().vertex.0, val.to_owned())) + .collect(); + + let results_type = std::any::type_name::>(); + AlgorithmResult::new( + graph.clone(), + "Three node local single thread", + results_type, + new_counts, + ) } /// Computes the number of each type of motif that each node participates in. @@ -263,8 +274,8 @@ fn triangle_motif_count( pub fn local_temporal_three_node_motifs( graph: &G, delta: i64, -) -> AlgorithmResult> { - let mut counts = triangle_motif_count(graph, delta).get_all().to_owned(); +) -> AlgorithmResult, Vec> { + let mut counts = triangle_motif_count(graph, delta); for v in graph.vertices() { let vid = v.id(); @@ -278,12 +289,17 @@ pub fn local_temporal_three_node_motifs( let mut final_cts = Vec::new(); final_cts.extend(stars.into_iter()); final_cts.extend(two_nodes.into_iter()); - final_cts.extend(counts.get(&vid).unwrap().into_iter()); - counts.insert(vid, final_cts); + final_cts.extend(counts.get(v.clone().into()).unwrap().into_iter()); + counts.result.insert(v.vertex.0, final_cts); } - let results_type = std::any::type_name::>>(); - AlgorithmResult::new("Three node local single thread", results_type, counts) + let results_type = std::any::type_name::>(); + AlgorithmResult::new( + graph.clone(), + "Three node local single thread", + results_type, + counts.result, + ) } /// Computes the number of each type of motif there is in the graph. @@ -306,10 +322,13 @@ pub fn local_temporal_three_node_motifs( /// pub fn global_temporal_three_node_motifs(graph: &G, delta: i64) -> Vec { let counts = local_temporal_three_node_motifs(graph, delta) - .get_all() + .get_all_values() .to_owned(); - let mut tmp_counts = counts.values().fold(vec![0; 40], |acc, x| { - acc.iter().zip(x.iter()).map(|(x1, x2)| x1 + x2).collect() + let mut tmp_counts = counts.iter().fold(vec![0; 40], |acc, x| { + acc.iter() + .zip(x.iter()) + .map(|(x1, x2)| x1 + x2) + .collect::>() }); for ind in 32..40 { tmp_counts[ind] /= 3; @@ -360,9 +379,9 @@ mod local_motif_test { } // let counts = star_motif_count(&graph, 1, 100); - let counts = local_temporal_three_node_motifs(&graph, 10); + let counts_result = local_temporal_three_node_motifs(&graph.clone(), 10); // FIXME: Should test this - let _global_counts = global_temporal_three_node_motifs(&graph, 10); + let _global_counts = global_temporal_three_node_motifs(&graph.clone(), 10); let expected: HashMap> = HashMap::from([ ( 1, @@ -443,7 +462,10 @@ mod local_motif_test { ), ]); for ind in 1..12 { - assert_eq!(counts.get(&ind).unwrap(), expected.get(&ind).unwrap()); + assert_eq!( + counts_result.get(ind.into()).unwrap(), + expected.get(&ind).unwrap() + ) } // print!("{:?}", global_counts); } diff --git a/raphtory/src/algorithms/pathing/single_source_shortest_path.rs b/raphtory/src/algorithms/pathing/single_source_shortest_path.rs index f6139b6ad4..3da5c9a60e 100644 --- a/raphtory/src/algorithms/pathing/single_source_shortest_path.rs +++ b/raphtory/src/algorithms/pathing/single_source_shortest_path.rs @@ -4,7 +4,9 @@ //! It finds the shortest paths from a given source vertex to all other vertices in a graph. use crate::{ algorithms::algorithm_result::AlgorithmResult, - core::entities::vertices::input_vertex::InputVertex, prelude::*, + core::entities::{vertices::input_vertex::InputVertex, VID}, + db::graph::vertex::VertexView, + prelude::*, }; use std::collections::HashMap; @@ -24,32 +26,38 @@ pub fn single_source_shortest_path( g: &G, source: T, cutoff: Option, -) -> AlgorithmResult> { - let results_type = std::any::type_name::>>(); - let mut paths: HashMap> = HashMap::new(); +) -> AlgorithmResult, Vec> { + let results_type = std::any::type_name::>(); + let mut paths: HashMap> = HashMap::new(); if g.has_vertex(source.clone()) { let source_node = g.vertex(source).unwrap(); + let vertex_internal_id = source_node.vertex.0; let mut level = 0; - let mut nextlevel: HashMap = HashMap::new(); - nextlevel.insert(source_node.name(), 1); + let mut nextlevel: HashMap = HashMap::new(); + nextlevel.insert(vertex_internal_id, "1".to_string()); - paths.insert(source_node.name(), vec![source_node.name()]); + paths.insert(vertex_internal_id, vec![source_node.name()]); if let Some(0) = cutoff { - return AlgorithmResult::new("Single Source Shortest Path", results_type, paths); + return AlgorithmResult::new( + g.clone(), + "Single Source Shortest Path", + results_type, + paths, + ); } while !nextlevel.is_empty() { - let thislevel: HashMap = nextlevel.clone(); + let thislevel: HashMap = nextlevel.clone(); nextlevel.clear(); - for v in thislevel.keys() { - for w in g.vertex(v.clone()).unwrap().neighbours() { - if !paths.contains_key(&w.name()) { + let vertex = VertexView::new_internal(g.clone(), VID::from(*v)); + for w in vertex.neighbours() { + if !paths.contains_key(&w.vertex.0) { let mut new_path = paths.get(v).unwrap().clone(); new_path.push(w.name()); - paths.insert(w.name(), new_path); - nextlevel.insert(w.name(), 1); + paths.insert(w.vertex.0, new_path); + nextlevel.insert(w.vertex.0, "1".to_string()); } } } @@ -61,7 +69,12 @@ pub fn single_source_shortest_path( } } } - AlgorithmResult::new("Single Source Shortest Path", results_type, paths) + AlgorithmResult::new( + g.clone(), + "Single Source Shortest Path", + results_type, + paths, + ) } #[cfg(test)] @@ -92,26 +105,37 @@ mod sssp_tests { ]); let binding = single_source_shortest_path(&graph, 1, Some(4)); - let results = binding.get_all(); - let expected = HashMap::from([ - ("1".to_string(), vec!["1".to_string()]), - ("2".to_string(), vec!["1".to_string(), "2".to_string()]), - ("3".to_string(), vec!["1".to_string(), "3".to_string()]), - ("4".to_string(), vec!["1".to_string(), "4".to_string()]), + let results = binding.get_all_with_names(); + let expected: HashMap>> = HashMap::from([ + ("1".to_string(), Some(vec!["1".to_string()])), + ( + "2".to_string(), + Some(vec!["1".to_string(), "2".to_string()]), + ), + ( + "3".to_string(), + Some(vec!["1".to_string(), "3".to_string()]), + ), + ( + "4".to_string(), + Some(vec!["1".to_string(), "4".to_string()]), + ), ( "5".to_string(), - vec!["1".to_string(), "4".to_string(), "5".to_string()], + Some(vec!["1".to_string(), "4".to_string(), "5".to_string()]), ), ( "6".to_string(), - vec![ + Some(vec![ "1".to_string(), "4".to_string(), "5".to_string(), "6".to_string(), - ], + ]), ), ]); - assert_eq!(results, &expected) + assert_eq!(results, expected); + let binding = single_source_shortest_path(&graph, 5, Some(4)); + println!("{:?}", binding.get_all_with_names()); } } diff --git a/raphtory/src/algorithms/pathing/temporal_reachability.rs b/raphtory/src/algorithms/pathing/temporal_reachability.rs index 449195c426..3007a66d19 100644 --- a/raphtory/src/algorithms/pathing/temporal_reachability.rs +++ b/raphtory/src/algorithms/pathing/temporal_reachability.rs @@ -70,7 +70,7 @@ pub fn temporally_reachable_nodes( start_time: i64, seed_nodes: Vec, stop_nodes: Option>, -) -> AlgorithmResult> { +) -> AlgorithmResult, Vec<(i64, String)>> { let mut ctx: Context = g.into(); let infected_nodes = seed_nodes.into_iter().map(|n| n.id()).collect_vec(); @@ -180,28 +180,26 @@ pub fn temporally_reachable_nodes( })); let mut runner: TaskRunner = TaskRunner::new(ctx); - let results_type = std::any::type_name::>>(); - AlgorithmResult::new( - "Temporal Reachability", - results_type, - runner.run( - vec![Job::new(step1)], - vec![Job::new(step2), step3], - None, - |_, ess, _, _| { - ess.finalize(&taint_history, |taint_history| { - taint_history - .into_iter() - .map(|tmsg| (tmsg.event_time, tmsg.src_vertex)) - .collect_vec() - }) - }, - threads, - max_hops, - None, - None, - ), - ) + let result: HashMap> = runner.run( + vec![Job::new(step1)], + vec![Job::new(step2), step3], + None, + |_, ess, _, _| { + ess.finalize(&taint_history, |taint_history| { + taint_history + .into_iter() + .map(|tmsg| (tmsg.event_time, tmsg.src_vertex)) + .collect_vec() + }) + }, + threads, + max_hops, + None, + None, + ); + + let results_type = std::any::type_name::>(); + AlgorithmResult::new(g.clone(), "Temporal Reachability", results_type, result) } #[cfg(test)] @@ -209,6 +207,19 @@ mod generic_taint_tests { use super::*; use crate::db::{api::mutation::AdditionOps, graph::graph::Graph}; + fn sort_inner_by_string( + data: HashMap>>, + ) -> Vec<(String, Option>)> { + let mut vec: Vec<_> = data.into_iter().collect(); + vec.sort_by(|a, b| a.0.cmp(&b.0)); + for (_, value_option) in &mut vec { + if let Some(inner_vec) = value_option { + inner_vec.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| b.1.cmp(&a.1))); + } + } + vec + } + fn load_graph(edges: Vec<(i64, u64, u64)>) -> Graph { let graph = Graph::new(); @@ -224,8 +235,8 @@ mod generic_taint_tests { start_time: i64, infected_nodes: Vec, stop_nodes: Option>, - ) -> Vec<(String, Vec<(i64, String)>)> { - let results: Vec<(String, Vec<(i64, String)>)> = temporally_reachable_nodes( + ) -> HashMap>> { + temporally_reachable_nodes( &graph, None, iter_count, @@ -233,15 +244,7 @@ mod generic_taint_tests { infected_nodes, stop_nodes, ) - .sort_by_key(false) - .into_iter() - .map(|(k, mut v)| { - v.sort(); - (k, v) - }) - .collect_vec(); - - results + .get_all_with_names() } #[test] @@ -259,27 +262,24 @@ mod generic_taint_tests { (10, 5, 8), ]); - let results = test_generic_taint(graph, 20, 11, vec![2], None); - - assert_eq!( - results, - Vec::from([ - ("1".to_string(), vec![]), - ("2".to_string(), vec![(11, "start".to_string())]), - ("3".to_string(), vec![]), - ( - "4".to_string(), - vec![(12, "2".to_string()), (14, "5".to_string())] - ), - ( - "5".to_string(), - vec![(13, "2".to_string()), (14, "5".to_string())] - ), - ("6".to_string(), vec![]), - ("7".to_string(), vec![(15, "4".to_string())]), - ("8".to_string(), vec![]), - ]) - ); + let results = sort_inner_by_string(test_generic_taint(graph, 20, 11, vec![2], None)); + let expected: Vec<(String, Option>)> = Vec::from([ + ("1".to_string(), Some(vec![])), + ("2".to_string(), Some(vec![(11i64, "start".to_string())])), + ("3".to_string(), Some(vec![])), + ( + "4".to_string(), + Some(vec![(12i64, "2".to_string()), (14i64, "5".to_string())]), + ), + ( + "5".to_string(), + Some(vec![(13i64, "2".to_string()), (14i64, "5".to_string())]), + ), + ("6".to_string(), Some(vec![])), + ("7".to_string(), Some(vec![(15i64, "4".to_string())])), + ("8".to_string(), Some(vec![])), + ]); + assert_eq!(results, expected); } #[test] @@ -297,30 +297,27 @@ mod generic_taint_tests { (10, 5, 8), ]); - let results = test_generic_taint(graph, 20, 11, vec![1, 2], None); - - assert_eq!( - results, - Vec::from([ - ("1".to_string(), vec![(11, "start".to_string())]), - ( - "2".to_string(), - vec![(11, "1".to_string()), (11, "start".to_string())] - ), - ("3".to_string(), vec![]), - ( - "4".to_string(), - vec![(12, "2".to_string()), (14, "5".to_string())] - ), - ( - "5".to_string(), - vec![(13, "2".to_string()), (14, "5".to_string())] - ), - ("6".to_string(), vec![]), - ("7".to_string(), vec![(15, "4".to_string())]), - ("8".to_string(), vec![]), - ]) - ); + let results = sort_inner_by_string(test_generic_taint(graph, 20, 11, vec![1, 2], None)); + let expected: Vec<(String, Option>)> = Vec::from([ + ("1".to_string(), Some(vec![(11i64, "start".to_string())])), + ( + "2".to_string(), + Some(vec![(11i64, "start".to_string()), (11i64, "1".to_string())]), + ), + ("3".to_string(), Some(vec![])), + ( + "4".to_string(), + Some(vec![(12i64, "2".to_string()), (14i64, "5".to_string())]), + ), + ( + "5".to_string(), + Some(vec![(13i64, "2".to_string()), (14i64, "5".to_string())]), + ), + ("6".to_string(), Some(vec![])), + ("7".to_string(), Some(vec![(15i64, "4".to_string())])), + ("8".to_string(), Some(vec![])), + ]); + assert_eq!(results, expected); } #[test] @@ -338,24 +335,27 @@ mod generic_taint_tests { (10, 5, 8), ]); - let results = test_generic_taint(graph, 20, 11, vec![1, 2], Some(vec![4, 5])); - - assert_eq!( - results, - Vec::from([ - ("1".to_string(), vec![(11, "start".to_string())]), - ( - "2".to_string(), - vec![(11, "1".to_string()), (11, "start".to_string())] - ), - ("3".to_string(), vec![]), - ("4".to_string(), vec![(12, "2".to_string())]), - ("5".to_string(), vec![(13, "2".to_string())]), - ("6".to_string(), vec![]), - ("7".to_string(), vec![]), - ("8".to_string(), vec![]), - ]) - ); + let results = sort_inner_by_string(test_generic_taint( + graph, + 20, + 11, + vec![1, 2], + Some(vec![4, 5]), + )); + let expected: Vec<(String, Option>)> = Vec::from([ + ("1".to_string(), Some(vec![(11i64, "start".to_string())])), + ( + "2".to_string(), + Some(vec![(11i64, "start".to_string()), (11i64, "1".to_string())]), + ), + ("3".to_string(), Some(vec![])), + ("4".to_string(), Some(vec![(12i64, "2".to_string())])), + ("5".to_string(), Some(vec![(13i64, "2".to_string())])), + ("6".to_string(), Some(vec![])), + ("7".to_string(), Some(vec![])), + ("8".to_string(), Some(vec![])), + ]); + assert_eq!(results, expected); } #[test] @@ -375,27 +375,30 @@ mod generic_taint_tests { (10, 5, 8), ]); - let results = test_generic_taint(graph, 20, 11, vec![1, 2], Some(vec![4, 5])); - - assert_eq!( - results, - Vec::from([ - ("1".to_string(), vec![(11, "start".to_string())]), - ( - "2".to_string(), - vec![ - (11, "1".to_string()), - (11, "start".to_string()), - (12, "1".to_string()) - ] - ), - ("3".to_string(), vec![]), - ("4".to_string(), vec![(12, "2".to_string())]), - ("5".to_string(), vec![(13, "2".to_string())]), - ("6".to_string(), vec![]), - ("7".to_string(), vec![]), - ("8".to_string(), vec![]), - ]) - ); + let results = sort_inner_by_string(test_generic_taint( + graph, + 20, + 11, + vec![1, 2], + Some(vec![4, 5]), + )); + let expected: Vec<(String, Option>)> = Vec::from([ + ("1".to_string(), Some(vec![(11i64, "start".to_string())])), + ( + "2".to_string(), + Some(vec![ + (11i64, "start".to_string()), + (11i64, "1".to_string()), + (12i64, "1".to_string()), + ]), + ), + ("3".to_string(), Some(vec![])), + ("4".to_string(), Some(vec![(12i64, "2".to_string())])), + ("5".to_string(), Some(vec![(13i64, "2".to_string())])), + ("6".to_string(), Some(vec![])), + ("7".to_string(), Some(vec![])), + ("8".to_string(), Some(vec![])), + ]); + assert_eq!(results, expected); } } diff --git a/raphtory/src/core/entities/mod.rs b/raphtory/src/core/entities/mod.rs index 8b0db056e3..692d0879cb 100644 --- a/raphtory/src/core/entities/mod.rs +++ b/raphtory/src/core/entities/mod.rs @@ -15,7 +15,7 @@ pub mod graph; pub mod properties; pub mod vertices; -// the only reason this is public is because the phisical ids of the vertices don't move +// the only reason this is public is because the physical ids of the vertices don't move #[repr(transparent)] #[derive( Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize, Default, diff --git a/raphtory/src/core/state/compute_state.rs b/raphtory/src/core/state/compute_state.rs index 771a805287..5288bcd815 100644 --- a/raphtory/src/core/state/compute_state.rs +++ b/raphtory/src/core/state/compute_state.rs @@ -44,7 +44,7 @@ pub trait ComputeState: std::fmt::Debug + Clone + Send + Sync { &self, ss: usize, g: &G, - ) -> HashMap + ) -> HashMap where OUT: StateType, A: 'static; @@ -192,8 +192,8 @@ impl ComputeState for ComputeStateVec { fn finalize, G: GraphViewOps>( &self, ss: usize, - g: &G, - ) -> HashMap + _g: &G, + ) -> HashMap where OUT: StateType, A: 'static, @@ -210,7 +210,7 @@ impl ComputeState for ComputeStateVec { .enumerate() .map(|(p_id, a)| { let out = ACC::finish(a); - (g.vertex_name(p_id.into()), out) + (p_id, out) }) .collect() } diff --git a/raphtory/src/core/state/mod.rs b/raphtory/src/core/state/mod.rs index c6d2cf227a..ece5626acf 100644 --- a/raphtory/src/core/state/mod.rs +++ b/raphtory/src/core/state/mod.rs @@ -79,11 +79,7 @@ mod state_test { actual.sort(); assert_eq!( actual, - vec![ - ("1".to_string(), actual_min), - ("2".to_string(), actual_min), - ("3".to_string(), actual_min), - ] + vec![(0, actual_min), (1, actual_min), (2, actual_min),] ); } @@ -116,11 +112,7 @@ mod state_test { actual.sort(); assert_eq!( actual, - vec![ - ("1".to_string(), actual_avg), - ("2".to_string(), actual_avg), - ("3".to_string(), actual_avg), - ] + vec![(0, actual_avg), (1, actual_avg), (2, actual_avg),] ); } @@ -146,9 +138,9 @@ mod state_test { assert_eq!( actual, vec![ - ("1".to_string(), expected.clone()), - ("2".to_string(), expected.clone()), - ("3".to_string(), expected.clone()), + (0, expected.clone()), + (1, expected.clone()), + (2, expected.clone()), ] ); } @@ -181,11 +173,7 @@ mod state_test { actual.sort(); assert_eq!( actual, - vec![ - ("1".to_string(), actual_sum), - ("2".to_string(), actual_sum), - ("3".to_string(), actual_sum), - ] + vec![(0, actual_sum), (1, actual_sum), (2, actual_sum),] ); } @@ -310,14 +298,7 @@ mod state_test { actual.sort(); - assert_eq!( - actual, - vec![ - ("1".to_string(), actual_sum_1), - ("2".to_string(), actual_sum_1), - ("3".to_string(), 0), - ] - ); + assert_eq!(actual, vec![(0, actual_sum_1), (1, actual_sum_1), (2, 0),]); let mut actual = part1_state .clone() @@ -329,11 +310,7 @@ mod state_test { assert_eq!( actual, - vec![ - ("1".to_string(), actual_min_1), - ("2".to_string(), actual_min_1), - ("3".to_string(), i32::MAX), - ] + vec![(0, actual_min_1), (1, actual_min_1), (2, i32::MAX),] ); let mut actual = part2_state @@ -344,14 +321,7 @@ mod state_test { actual.sort(); - assert_eq!( - actual, - vec![ - ("1".to_string(), actual_sum_2), - ("2".to_string(), 0), - ("3".to_string(), actual_sum_2), - ] - ); + assert_eq!(actual, vec![(0, actual_sum_2), (1, 0), (2, actual_sum_2),]); let mut actual = part2_state .clone() @@ -363,11 +333,7 @@ mod state_test { assert_eq!( actual, - vec![ - ("1".to_string(), actual_min_2), - ("2".to_string(), i32::MAX), - ("3".to_string(), actual_min_2), - ] + vec![(0, actual_min_2), (1, i32::MAX), (2, actual_min_2),] ); ShuffleComputeState::merge_mut(&mut part1_state, &part2_state, sum, 0); @@ -382,9 +348,9 @@ mod state_test { assert_eq!( actual, vec![ - ("1".to_string(), (actual_sum_1 + actual_sum_2)), - ("2".to_string(), actual_sum_1), - ("3".to_string(), actual_sum_2), + (0, (actual_sum_1 + actual_sum_2)), + (1, actual_sum_1), + (2, actual_sum_2), ] ); @@ -400,9 +366,9 @@ mod state_test { assert_eq!( actual, vec![ - ("1".to_string(), actual_min_1.min(actual_min_2)), - ("2".to_string(), actual_min_1), - ("3".to_string(), actual_min_2), + (0, actual_min_1.min(actual_min_2)), + (1, actual_min_1), + (2, actual_min_2), ] ); } diff --git a/raphtory/src/core/state/morcel_state.rs b/raphtory/src/core/state/morcel_state.rs index bbb919b3ff..4ac112e987 100644 --- a/raphtory/src/core/state/morcel_state.rs +++ b/raphtory/src/core/state/morcel_state.rs @@ -31,7 +31,7 @@ impl MorcelComputeState { ss: usize, agg_ref: &AccId, g: &G, - ) -> Option> + ) -> Option> where OUT: StateType, A: 'static, @@ -156,7 +156,7 @@ impl MorcelComputeState { ss: usize, agg_ref: &AccId, g: &G, - ) -> HashMap + ) -> HashMap where OUT: StateType, A: 'static, @@ -164,6 +164,6 @@ impl MorcelComputeState { self.states .get(&agg_ref.id()) .map(|s| s.finalize::(ss, g)) - .unwrap_or(HashMap::::default()) + .unwrap_or(HashMap::::default()) } } diff --git a/raphtory/src/core/state/shuffle_state.rs b/raphtory/src/core/state/shuffle_state.rs index 95afaf3c45..2163fe5871 100644 --- a/raphtory/src/core/state/shuffle_state.rs +++ b/raphtory/src/core/state/shuffle_state.rs @@ -209,9 +209,9 @@ impl ShuffleComputeState { &self, agg_def: &AccId, ss: usize, - g: &G, + _g: &G, f: F, - ) -> HashMap + ) -> HashMap where OUT: StateType, A: StateType, @@ -222,7 +222,7 @@ impl ShuffleComputeState { let out = a .map(|a| ACC::finish(a)) .unwrap_or_else(|| ACC::finish(&ACC::zero())); - (g.vertex_name(v_id.into()).to_string(), f(out)) + (v_id, f(out)) }) .collect() } @@ -299,7 +299,7 @@ impl EvalShardState { self, agg_def: &AccId, f: F, - ) -> HashMap + ) -> HashMap where OUT: StateType, A: StateType, @@ -341,7 +341,7 @@ impl EvalLocalState { self, agg_def: &AccId, f: F, - ) -> HashMap + ) -> HashMap where OUT: StateType, A: StateType, @@ -353,7 +353,7 @@ impl EvalLocalState { if let Some(state) = Arc::try_unwrap(state).ok().flatten() { state.finalize(agg_def, self.ss, &self.g, f) } else { - HashMap::::new() + HashMap::::new() } }) .collect() diff --git a/raphtory/src/db/graph/vertex.rs b/raphtory/src/db/graph/vertex.rs index 07317c6d55..48151788e4 100644 --- a/raphtory/src/db/graph/vertex.rs +++ b/raphtory/src/db/graph/vertex.rs @@ -27,8 +27,12 @@ use crate::{ }, prelude::*, }; +use std::{ + fmt, + hash::{Hash, Hasher}, +}; -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct VertexView { pub graph: G, pub vertex: VID, @@ -52,6 +56,44 @@ impl From<&VertexView> for VertexRef { } } +impl fmt::Debug for VertexView { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "VertexView {{ graph: {}{}, vertex: {} }}", + self.graph.count_vertices(), + self.graph.count_edges(), + self.vertex.0 + ) + } +} + +impl fmt::Display for VertexView { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "VertexView {{ graph: {}{}, vertex: {} }}", + self.graph.count_vertices(), + self.graph.count_edges(), + self.vertex.0 + ) + } +} + +impl PartialOrd> for VertexView { + fn partial_cmp(&self, other: &VertexView) -> Option { + self.vertex.0.partial_cmp(&other.vertex.0) + } +} + +impl Ord for VertexView { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.vertex.0.cmp(&other.vertex.0) + } +} + +impl Eq for VertexView {} + impl VertexView { /// Creates a new `VertexView` wrapping an internal vertex reference and a graph, internalising any global vertex ids. pub fn new(graph: G, vertex: VertexRef) -> VertexView { @@ -70,6 +112,15 @@ impl VertexView { } } +impl Hash for VertexView { + fn hash(&self, state: &mut H) { + // Hash the graph + "1".to_string().hash(state); + // Hash the vertex ID + self.id().hash(state); + } +} + impl TemporalPropertiesOps for VertexView { fn get_temporal_prop_id(&self, name: &str) -> Option { self.graph diff --git a/raphtory/src/graph_loader/example/karate_club.rs b/raphtory/src/graph_loader/example/karate_club.rs new file mode 100644 index 0000000000..4960878f10 --- /dev/null +++ b/raphtory/src/graph_loader/example/karate_club.rs @@ -0,0 +1,118 @@ +use crate::{db::api::mutation::AdditionOps, prelude::*}; + +/// `karate_club_graph` constructs a karate club graph. +/// +/// This function uses the Zachary's karate club dataset to create +/// a graph object. Vertices represent members of the club, and edges +/// represent relationships between them. Vertex properties indicate +/// the club to which each member belongs. +/// +/// BACKGROUND These are data collected from the members of a university karate club by Wayne +/// Zachary. The ZACHE matrix represents the presence or absence of ties among the members of the +/// club; the ZACHC matrix indicates the relative strength of the associations (number of +/// situations in and outside the club in which interactions occurred). +/// +/// Zachary (1977) used these data and an information flow model of network conflict resolution +/// to explain the split-up of this group following disputes among the members. +/// +/// REFERENCE +/// Zachary W. (1977). An information flow model for conflict and fission in small groups. +/// Journal of Anthropological Research, 33, 452-473. +/// +/// +/// Returns: +/// A `Graph` object representing the karate club network. +pub fn karate_club_graph() -> Graph { + // Raw adjacency matrix data for Zachary's karate club. + let zachary_dat_raw = " +0 4 5 3 3 3 3 2 2 0 2 3 2 3 0 0 0 2 0 2 0 2 0 0 0 0 0 0 0 0 0 2 0 0 +4 0 6 3 0 0 0 4 0 0 0 0 0 5 0 0 0 1 0 2 0 2 0 0 0 0 0 0 0 0 2 0 0 0 +5 6 0 3 0 0 0 4 5 1 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 0 0 0 3 0 +3 3 3 0 0 0 0 3 0 0 0 0 3 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +3 0 0 0 0 0 2 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +3 0 0 0 0 0 5 0 0 0 3 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +3 0 0 0 2 5 0 0 0 0 0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +2 4 4 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +2 0 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 4 3 +0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 +2 0 0 0 3 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +1 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +3 5 3 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 4 +0 0 0 0 0 3 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 +2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 1 +2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 0 4 0 2 0 0 5 4 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 3 0 0 0 2 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 2 0 0 0 0 0 0 7 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 0 0 0 2 +0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 3 0 0 0 0 0 0 0 0 4 +0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 0 4 0 0 0 0 0 3 2 +0 2 0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 3 +2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 7 0 0 2 0 0 0 4 4 +0 0 2 0 0 0 0 0 3 0 0 0 0 0 3 3 0 0 1 0 3 0 2 5 0 0 0 0 0 4 3 4 0 5 +0 0 0 0 0 0 0 0 4 2 0 0 0 3 2 4 0 0 2 1 1 0 3 4 0 0 2 4 2 2 3 4 5 0"; + + // Club membership + let club1: Vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 16, 17, 19, 21]; + + // Fill in base graph + let graph = Graph::new(); + for i in 0..34 { + graph + .add_vertex(0, i, NO_PROPS) + .map_err(|err| println!("{:?}", err)) + .ok(); + } + + // Add edges + for (row, line) in zachary_dat_raw.lines().enumerate() { + let this_row: Vec = line + .split_whitespace() + .map(|b| b.parse().unwrap()) + .collect(); + for (col, &entry) in this_row.iter().enumerate() { + if entry >= 1 { + let actual_row = row - 1; + graph + .add_edge(0, actual_row as u64, col as u64, [("weight", entry)], None) + .map_err(|err| println!("{:?}", err)) + .ok(); + } + } + } + + // Add properties + for v in 0..34 { + let vertex = graph.vertex(v).unwrap(); + let mut const_prop = "Officer"; + if club1.contains(&v) { + const_prop = "Mr. Hi"; + } + vertex + .add_constant_properties([("club", const_prop)]) + .map_err(|err| println!("{:?}", err)) + .ok(); + } + + graph +} + +#[cfg(test)] +mod karate_test { + use super::*; + + #[test] + fn test_graph_sizes() { + let g = karate_club_graph(); + assert_eq!(g.count_vertices(), 34); + assert_eq!(g.count_edges(), 155); + } +} diff --git a/raphtory/src/graph_loader/example/mod.rs b/raphtory/src/graph_loader/example/mod.rs index 288fa0c80c..6a3b542df1 100644 --- a/raphtory/src/graph_loader/example/mod.rs +++ b/raphtory/src/graph_loader/example/mod.rs @@ -1,4 +1,5 @@ pub mod company_house; +pub mod karate_club; pub mod lotr_graph; pub mod neo4j_examples; pub mod reddit_hyperlinks; diff --git a/raphtory/src/graph_loader/mod.rs b/raphtory/src/graph_loader/mod.rs index 919c34b91e..9516673954 100644 --- a/raphtory/src/graph_loader/mod.rs +++ b/raphtory/src/graph_loader/mod.rs @@ -196,6 +196,13 @@ mod graph_loader_test { assert_eq!(g_at_min.count_vertices(), 0); } + #[test] + fn test_karate_graph() { + let g = crate::graph_loader::example::karate_club::karate_club_graph(); + assert_eq!(g.count_vertices(), 34); + assert_eq!(g.count_edges(), 155); + } + #[test] fn db_lotr() { let g = Graph::new(); diff --git a/raphtory/src/python/graph/algorithm_result.rs b/raphtory/src/python/graph/algorithm_result.rs index 7cc16ce705..7ca9d35ea4 100644 --- a/raphtory/src/python/graph/algorithm_result.rs +++ b/raphtory/src/python/graph/algorithm_result.rs @@ -1,14 +1,16 @@ -use crate::python::types::repr::Repr; +use crate::{ + core::entities::vertices::vertex_ref::VertexRef, db::api::view::internal::DynamicGraph, + python::types::repr::Repr, +}; use ordered_float::OrderedFloat; use pyo3::prelude::*; -/// Create a macro for py_algorithm_result macro_rules! py_algorithm_result { - ($name:ident, $rustKey:ty, $rustValue:ty, $rustSortValue:ty) => { + ($objectName:ident, $rustGraph:ty, $rustValue:ty, $rustSortValue:ty) => { #[pyclass] - pub struct $name( + pub struct $objectName( $crate::algorithms::algorithm_result::AlgorithmResult< - $rustKey, + $rustGraph, $rustValue, $rustSortValue, >, @@ -16,7 +18,7 @@ macro_rules! py_algorithm_result { impl Repr for $crate::algorithms::algorithm_result::AlgorithmResult< - $rustKey, + $rustGraph, $rustValue, $rustSortValue, > @@ -34,30 +36,43 @@ macro_rules! py_algorithm_result { impl pyo3::IntoPy for $crate::algorithms::algorithm_result::AlgorithmResult< - $rustKey, + $rustGraph, $rustValue, $rustSortValue, > { fn into_py(self, py: Python<'_>) -> pyo3::PyObject { - $name(self).into_py(py) + $objectName(self).into_py(py) } } }; - ($name:ident, $rustKey:ty, $rustValue:ty) => { - py_algorithm_result!($name, $rustKey, $rustValue, $rustValue); + ($name:ident, $rustGraph:ty, $rustKey:ty, $rustSortValue:ty) => { + py_algorithm_result!($name, $rustGraph, $rustKey, $rustSortValue); }; } #[macro_export] macro_rules! py_algorithm_result_base { - ($name:ident, $rustKey:ty, $rustValue:ty) => { + ($objectName:ident, $rustGraph:ty, $rustValue:ty, $rustOrderedValue:ty) => { #[pymethods] - impl $name { - /// Returns a reference to the entire `result` hashmap. - fn get_all(&self) -> std::collections::HashMap<$rustKey, $rustValue> { - self.0.get_all().clone() + impl $objectName { + /// Returns a Dict containing all the vertices (as keys) and their corresponding values (values) or none. + /// + /// Returns: + /// A dict of vertices and their values + fn get_all( + &self, + ) -> std::collections::HashMap< + $crate::db::graph::vertex::VertexView<$rustGraph>, + Option<$rustValue>, + > { + self.0.get_all() + } + + /// Returns a a list of all values + fn get_all_values(&self) -> std::vec::Vec<$rustValue> { + self.0.get_all_values().clone() } /// Returns a formatted string representation of the algorithm. @@ -65,19 +80,45 @@ macro_rules! py_algorithm_result_base { self.0.repr() } - /// Returns the value corresponding to the provided key in the `result` hashmap. + /// Returns the value corresponding to the provided key /// /// Arguments: /// key: The key of type `H` for which the value is to be retrieved. - fn get(&self, key: $rustKey) -> Option<$rustValue> { - self.0.get(&key).cloned() + fn get(&self, key: VertexRef) -> Option<$rustValue> { + self.0.get(key).cloned() + } + + /// Returns a dict with vertex names and values + /// + /// Returns: + /// a dict with vertex names and values + fn get_all_with_names(&self) -> std::collections::HashMap> { + self.0.get_all_with_names() + } + + /// Sorts by vertex id in ascending or descending order. + /// + /// Arguments: + /// `reverse`: If `true`, sorts the result in descending order; otherwise, sorts in ascending order. + /// + /// Returns: + /// A sorted list of tuples containing vertex names and values. + #[pyo3(signature = (reverse=true))] + fn sort_by_vertex( + &self, + reverse: bool, + ) -> std::vec::Vec<( + $crate::db::graph::vertex::VertexView<$rustGraph>, + Option<$rustValue>, + )> { + self.0.sort_by_vertex(reverse) } /// Creates a dataframe from the result /// /// Returns: /// A `pandas.DataFrame` containing the result - pub fn to_df(&self) -> PyResult { + fn to_df(&self) -> PyResult { let hashmap = &self.0.result; let mut keys = Vec::new(); let mut values = Vec::new(); @@ -100,9 +141,9 @@ macro_rules! py_algorithm_result_base { #[macro_export] macro_rules! py_algorithm_result_partial_ord { - ($name:ident, $rustKey:ty, $rustValue:ty) => { + ($objectName:ident, $rustGraph:ty, $rustValue:ty, $rustOrderedValue:ty) => { #[pymethods] - impl $name { + impl $objectName { /// Sorts the `AlgorithmResult` by its values in ascending or descending order. /// /// Arguments: @@ -111,20 +152,35 @@ macro_rules! py_algorithm_result_partial_ord { /// Returns: /// A sorted vector of tuples containing keys of type `H` and values of type `Y`. #[pyo3(signature = (reverse=true))] - fn sort_by_value(&self, reverse: bool) -> Vec<($rustKey, $rustValue)> { + fn sort_by_value( + &self, + reverse: bool, + ) -> std::vec::Vec<( + $crate::db::graph::vertex::VertexView<$rustGraph>, + Option<$rustValue>, + )> { self.0.sort_by_value(reverse) } - /// Sorts the `AlgorithmResult` by its keys in ascending or descending order. + /// The function `sort_by_vertex_name` sorts a vector of tuples containing a vertex and an optional + /// value by the vertex name in either ascending or descending order. /// /// Arguments: - /// reverse (bool): If `true`, sorts the result in descending order; otherwise, sorts in ascending order. + /// reverse (bool): A boolean value indicating whether the sorting should be done in reverse order or not. + /// If reverse is true, the sorting will be done in descending order, otherwise it will be done in + /// ascending order. /// /// Returns: - /// A sorted vector of tuples containing keys of type `H` and values of type `Y`. + /// The function sort_by_vertex_name returns a vector of tuples. Each tuple contains a Vertex and value #[pyo3(signature = (reverse=true))] - fn sort_by_key(&self, reverse: bool) -> Vec<($rustKey, $rustValue)> { - self.0.sort_by_key(reverse) + fn sort_by_vertex_name( + &self, + reverse: bool, + ) -> std::vec::Vec<( + $crate::db::graph::vertex::VertexView<$rustGraph>, + Option<$rustValue>, + )> { + self.0.sort_by_vertex_name(reverse) } /// Retrieves the top-k elements from the `AlgorithmResult` based on its values. @@ -145,54 +201,122 @@ macro_rules! py_algorithm_result_partial_ord { k: usize, percentage: bool, reverse: bool, - ) -> Vec<($rustKey, $rustValue)> { + ) -> std::vec::Vec<( + $crate::db::graph::vertex::VertexView<$rustGraph>, + Option<$rustValue>, + )> { self.0.top_k(k, percentage, reverse) } + + /// Returns a tuple of the min result with its key + fn min( + &self, + ) -> Option<( + $crate::db::graph::vertex::VertexView<$rustGraph>, + Option<$rustValue>, + )> { + self.0.min().map(|(k, v)| (k, v.map(|val| val))) + } + + /// Returns a tuple of the max result with its key + fn max( + &self, + ) -> Option<( + $crate::db::graph::vertex::VertexView<$rustGraph>, + Option<$rustValue>, + )> { + self.0.max().map(|(k, v)| (k, v.map(|val| val))) + } + + /// Returns a tuple of the median result with its key + fn median( + &self, + ) -> Option<( + $crate::db::graph::vertex::VertexView<$rustGraph>, + Option<$rustValue>, + )> { + self.0.median().map(|(k, v)| (k, v.map(|val| val))) + } } - py_algorithm_result_base!($name, $rustKey, $rustValue); + py_algorithm_result_base!($objectName, $rustGraph, $rustValue, $rustOrderedValue); }; } #[macro_export] -macro_rules! py_algorithm_result_ord_hash_eq { - ($name:ident, $rustKey:ty, $rustValue:ty) => { +macro_rules! py_algorithm_result_new_ord_hash_eq { + ($objectName:ident, $rustGraph:ty, $rustValue:ty, $rustOrderedValue:ty) => { #[pymethods] - impl $name { + impl $objectName { /// Groups the `AlgorithmResult` by its values. /// /// Returns: /// A `HashMap` where keys are unique values from the `AlgorithmResult` and values are vectors /// containing keys of type `H` that share the same value. - fn group_by(&self) -> std::collections::HashMap<$rustValue, Vec<$rustKey>> { + fn group_by(&self) -> std::collections::HashMap<$rustValue, Vec> { self.0.group_by() } } - py_algorithm_result_partial_ord!($name, $rustKey, $rustValue); + py_algorithm_result_partial_ord!($objectName, $rustGraph, $rustValue, $rustOrderedValue); }; } -py_algorithm_result!(AlgorithmResult, String, String); -py_algorithm_result_ord_hash_eq!(AlgorithmResult, String, String); +py_algorithm_result!(AlgorithmResult, DynamicGraph, String, String); +py_algorithm_result_new_ord_hash_eq!(AlgorithmResult, DynamicGraph, String, String); -py_algorithm_result!(AlgorithmResultStrU64, String, u64); -py_algorithm_result_ord_hash_eq!(AlgorithmResultStrU64, String, u64); +py_algorithm_result!(AlgorithmResultStrF64, DynamicGraph, f64, OrderedFloat); +py_algorithm_result_partial_ord!(AlgorithmResultStrF64, DynamicGraph, f64, OrderedFloat); + +py_algorithm_result!(AlgorithmResultStrU64, DynamicGraph, u64, u64); +py_algorithm_result_new_ord_hash_eq!(AlgorithmResultStrU64, DynamicGraph, u64, u64); py_algorithm_result!( AlgorithmResultStrTupleF32F32, - String, + DynamicGraph, (f32, f32), (OrderedFloat, OrderedFloat) ); -py_algorithm_result_partial_ord!(AlgorithmResultStrTupleF32F32, String, (f32, f32)); - -py_algorithm_result!(AlgorithmResultStrVecI64Str, String, Vec<(i64, String)>); -py_algorithm_result_ord_hash_eq!(AlgorithmResultStrVecI64Str, String, Vec<(i64, String)>); +py_algorithm_result_partial_ord!( + AlgorithmResultStrTupleF32F32, + DynamicGraph, + (f32, f32), + (f32, f32) +); -py_algorithm_result!(AlgorithmResultU64VecUsize, u64, Vec); -py_algorithm_result_ord_hash_eq!(AlgorithmResultU64VecUsize, u64, Vec); +py_algorithm_result!( + AlgorithmResultStrVecI64Str, + DynamicGraph, + Vec<(i64, String)>, + Vec<(i64, String)> +); +py_algorithm_result_new_ord_hash_eq!( + AlgorithmResultStrVecI64Str, + DynamicGraph, + Vec<(i64, String)>, + Vec<(i64, String)> +); -py_algorithm_result!(AlgorithmResultStrF64, String, f64, OrderedFloat); -py_algorithm_result_partial_ord!(AlgorithmResultStrF64, String, f64); +py_algorithm_result!( + AlgorithmResultU64VecUsize, + DynamicGraph, + Vec, + Vec +); +py_algorithm_result_new_ord_hash_eq!( + AlgorithmResultU64VecUsize, + DynamicGraph, + Vec, + Vec +); -py_algorithm_result!(AlgorithmResultStrVecStr, String, Vec); -py_algorithm_result_ord_hash_eq!(AlgorithmResultStrVecStr, String, Vec); +py_algorithm_result!( + AlgorithmResultStrVecStr, + DynamicGraph, + Vec, + Vec +); +py_algorithm_result_new_ord_hash_eq!( + AlgorithmResultStrVecStr, + DynamicGraph, + Vec, + Vec +); diff --git a/raphtory/src/python/graph/pandas/dataframe.rs b/raphtory/src/python/graph/pandas/dataframe.rs index a3c424be1b..3294d7f13a 100644 --- a/raphtory/src/python/graph/pandas/dataframe.rs +++ b/raphtory/src/python/graph/pandas/dataframe.rs @@ -5,14 +5,10 @@ use arrow2::{ offset::Offset, types::NativeType, }; -use futures_util::StreamExt; use itertools::Itertools; use pyo3::{ - create_exception, - exceptions::PyException, - ffi::Py_uintptr_t, - types::{IntoPyDict, PyDict}, - PyAny, PyErr, PyResult, Python, + create_exception, exceptions::PyException, ffi::Py_uintptr_t, types::IntoPyDict, PyAny, PyErr, + PyResult, Python, }; pub(crate) struct PretendDF { @@ -141,7 +137,7 @@ pub(crate) fn process_pandas_py_df( df }; - let df_columns: Vec = dropped_df.getattr("columns")?.extract()?; + let _df_columns: Vec = dropped_df.getattr("columns")?.extract()?; let table = pa_table.call_method("from_pandas", (dropped_df,), None)?; diff --git a/raphtory/src/python/graph/vertex.rs b/raphtory/src/python/graph/vertex.rs index ed9639011c..8aabde0857 100644 --- a/raphtory/src/python/graph/vertex.rs +++ b/raphtory/src/python/graph/vertex.rs @@ -84,7 +84,10 @@ impl PyVertex { match op { CompareOp::Eq => (self.vertex.id() == other.id()).into_py(py), CompareOp::Ne => (self.vertex.id() != other.id()).into_py(py), - _ => py.NotImplemented(), + CompareOp::Lt => (self.vertex.id() < other.id()).into_py(py), + CompareOp::Le => (self.vertex.id() <= other.id()).into_py(py), + CompareOp::Gt => (self.vertex.id() > other.id()).into_py(py), + CompareOp::Ge => (self.vertex.id() >= other.id()).into_py(py), } } diff --git a/raphtory/src/python/packages/algorithms.rs b/raphtory/src/python/packages/algorithms.rs index 2db587f403..1dc84f9e47 100644 --- a/raphtory/src/python/packages/algorithms.rs +++ b/raphtory/src/python/packages/algorithms.rs @@ -8,6 +8,7 @@ use crate::{ algorithms::{ algorithm_result::AlgorithmResult, centrality::{ + betweenness::betweenness_centrality as betweenness_rs, degree_centrality::degree_centrality as degree_centrality_rs, hits::hits as hits_rs, pagerank::unweighted_page_rank, }, @@ -41,7 +42,7 @@ use crate::{ python::{graph::views::graph_view::PyGraphView, utils::PyInputVertex}, usecase_algorithms::netflow_one_path_vertex::netflow_one_path_vertex as netflow_one_path_vertex_rs, }; -use crate::{core::Prop, python::graph::edge::PyDirection}; +use crate::{core::Prop, db::api::view::internal::DynamicGraph, python::graph::edge::PyDirection}; use ordered_float::OrderedFloat; use pyo3::prelude::*; @@ -77,7 +78,7 @@ pub fn local_triangle_count(g: &PyGraphView, v: VertexRef) -> Option { pub fn weakly_connected_components( g: &PyGraphView, iter_count: usize, -) -> AlgorithmResult { +) -> AlgorithmResult { connected_components::weakly_connected_components(&g.graph, iter_count, None) } @@ -101,7 +102,7 @@ pub fn pagerank( g: &PyGraphView, iter_count: usize, max_diff: Option, -) -> AlgorithmResult> { +) -> AlgorithmResult> { unweighted_page_rank(&g.graph, iter_count, None, max_diff, true) } @@ -127,7 +128,7 @@ pub fn temporally_reachable_nodes( start_time: i64, seed_nodes: Vec, stop_nodes: Option>, -) -> AlgorithmResult> { +) -> AlgorithmResult, Vec<(i64, String)>> { temporal_reachability_rs(&g.graph, None, max_hops, start_time, seed_nodes, stop_nodes) } @@ -251,7 +252,9 @@ pub fn global_reciprocity(g: &PyGraphView) -> f64 { /// AlgorithmResult : AlgorithmResult with string keys and float values mapping each vertex name to its reciprocity value. /// #[pyfunction] -pub fn all_local_reciprocity(g: &PyGraphView) -> AlgorithmResult> { +pub fn all_local_reciprocity( + g: &PyGraphView, +) -> AlgorithmResult> { all_local_reciprocity_rs(&g.graph, None) } @@ -388,7 +391,7 @@ pub fn hits( g: &PyGraphView, iter_count: usize, threads: Option, -) -> AlgorithmResult, OrderedFloat)> { +) -> AlgorithmResult, OrderedFloat)> { hits_rs(&g.graph, iter_count, threads) } @@ -415,7 +418,7 @@ pub fn balance( name: String, direction: PyDirection, threads: Option, -) -> AlgorithmResult> { +) -> AlgorithmResult> { balance_rs(&g.graph, name.clone(), direction.into(), threads) } @@ -440,7 +443,7 @@ pub fn netflow_one_path_vertex(g: &PyGraphView, no_time: bool, threads: Option, -) -> AlgorithmResult> { +) -> AlgorithmResult> { degree_centrality_rs(&g.graph, threads) } @@ -486,7 +489,7 @@ pub fn single_source_shortest_path( g: &PyGraphView, source: PyInputVertex, cutoff: Option, -) -> AlgorithmResult> { +) -> AlgorithmResult, Vec> { single_source_shortest_path_rs(&g.graph, source, cutoff) } @@ -514,3 +517,23 @@ pub fn dijkstra_single_source_shortest_paths( Err(err_msg) => Err(PyErr::new::(err_msg)), } } + +/// Computes the betweenness centrality for nodes in a given graph. +/// +/// Arguments: +/// g (Raphtory Graph): A reference to the graph. +/// k (int, optional): Specifies the number of nodes to consider for the centrality computation. Defaults to all nodes if `None`. +/// normalized (boolean, optional): Indicates whether to normalize the centrality values. +/// +/// # Returns +/// +/// Returns an `AlgorithmResult` containing the betweenness centrality of each node. +#[pyfunction] +#[pyo3[signature = (g, k=None, normalized=true)]] +pub fn betweenness_centrality( + g: &PyGraphView, + k: Option, + normalized: Option, +) -> AlgorithmResult> { + betweenness_rs(&g.graph, k, normalized) +} diff --git a/raphtory/src/python/packages/graph_loader.rs b/raphtory/src/python/packages/graph_loader.rs index 171fb2389e..0736a13cac 100644 --- a/raphtory/src/python/packages/graph_loader.rs +++ b/raphtory/src/python/packages/graph_loader.rs @@ -97,3 +97,29 @@ pub fn neo4j_movie_graph( ); PyGraph::py_from_db_graph(g) } + +/// `karate_club_graph` constructs a karate club graph. +/// +/// This function uses the Zachary's karate club dataset to create +/// a graph object. Vertices represent members of the club, and edges +/// represent relationships between them. Vertex properties indicate +/// the club to which each member belongs. +/// +/// Background: +/// These are data collected from the members of a university karate club by Wayne +/// Zachary. The ZACHE matrix represents the presence or absence of ties among the members of the +/// club; the ZACHC matrix indicates the relative strength of the associations (number of +/// situations in and outside the club in which interactions occurred). +/// Zachary (1977) used these data and an information flow model of network conflict resolution +/// to explain the split-up of this group following disputes among the members. +/// +/// Reference: +/// Zachary W. (1977). An information flow model for conflict and fission in small groups. Journal of Anthropological Research, 33, 452-473. +/// +/// Returns: +/// A `Graph` object representing the karate club network. +#[pyfunction] +#[pyo3(signature = ())] +pub fn karate_club_graph() -> PyResult> { + PyGraph::py_from_db_graph(crate::graph_loader::example::karate_club::karate_club_graph()) +}