From c756af4249b00deba6a2b489cbe2d58724b7bf10 Mon Sep 17 00:00:00 2001 From: Naomi Arnold <33124479+narnolddd@users.noreply.github.com> Date: Tue, 28 May 2024 16:57:45 +0100 Subject: [PATCH 1/2] Temporal bipartite projection (#1396) * temporal bipartite working * fmt * add num-integer crate * refactor to nodes etc * add some py docs and dependency into cargo * refactor using nbr and put in directory * change to asnoderef * cargo fmt --------- Co-authored-by: Ben Steer --- Cargo.lock | 1 + Cargo.toml | 1 + python/src/lib.rs | 1 + raphtory/Cargo.toml | 1 + raphtory/src/algorithms/mod.rs | 1 + raphtory/src/algorithms/projections/mod.rs | 1 + .../temporal_bipartite_projection.rs | 142 ++++++++++++++++++ raphtory/src/python/packages/algorithms.rs | 21 +++ 8 files changed, 169 insertions(+) create mode 100644 raphtory/src/algorithms/projections/mod.rs create mode 100644 raphtory/src/algorithms/projections/temporal_bipartite_projection.rs diff --git a/Cargo.lock b/Cargo.lock index 1a89c148d9..9953e5fa58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3972,6 +3972,7 @@ dependencies = [ "memmap2", "neo4rs", "num", + "num-integer", "num-traits", "once_cell", "ordered-float 4.2.0", diff --git a/Cargo.toml b/Cargo.toml index 52407e7b92..e68ad6df4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ flate2 = "1.0.28" regex = "1.10.3" genawaiter = "0.99.1" num-traits = "0.2.18" +num-integer = "0.1" rand_distr = "0.4.3" rustc-hash = "1.1.0" twox-hash = "1.6.3" diff --git a/python/src/lib.rs b/python/src/lib.rs index ac67f953df..eb23569098 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -99,6 +99,7 @@ fn raphtory(py: Python<'_>, m: &PyModule) -> PyResult<()> { single_source_shortest_path, global_clustering_coefficient, temporally_reachable_nodes, + temporal_bipartite_graph_projection, local_clustering_coefficient, weakly_connected_components, strongly_connected_components, diff --git a/raphtory/Cargo.toml b/raphtory/Cargo.toml index e68f0825e3..84fb057b8b 100644 --- a/raphtory/Cargo.toml +++ b/raphtory/Cargo.toml @@ -20,6 +20,7 @@ bincode = { workspace = true } chrono = { workspace = true } itertools = { workspace = true } num-traits = { workspace = true } +num-integer = { workspace = true } parking_lot = { workspace = true } once_cell = { workspace = true } rand = { workspace = true } diff --git a/raphtory/src/algorithms/mod.rs b/raphtory/src/algorithms/mod.rs index 9f72d668e5..7003b83617 100644 --- a/raphtory/src/algorithms/mod.rs +++ b/raphtory/src/algorithms/mod.rs @@ -37,3 +37,4 @@ pub mod layout; pub mod metrics; pub mod motifs; pub mod pathing; +pub mod projections; diff --git a/raphtory/src/algorithms/projections/mod.rs b/raphtory/src/algorithms/projections/mod.rs new file mode 100644 index 0000000000..b2264faa2a --- /dev/null +++ b/raphtory/src/algorithms/projections/mod.rs @@ -0,0 +1 @@ +pub mod temporal_bipartite_projection; diff --git a/raphtory/src/algorithms/projections/temporal_bipartite_projection.rs b/raphtory/src/algorithms/projections/temporal_bipartite_projection.rs new file mode 100644 index 0000000000..c9f9f4d53a --- /dev/null +++ b/raphtory/src/algorithms/projections/temporal_bipartite_projection.rs @@ -0,0 +1,142 @@ +use std::any::Any; + +use itertools::Itertools; +use num_integer::average_floor; +// use num::integer::average_floor; +extern crate num_integer; + +use crate::{ + core::entities::nodes::node_ref::{AsNodeRef, NodeRef}, + db::{ + api::{ + mutation::AdditionOps, + view::{internal, *}, + }, + graph::graph::Graph, + }, + prelude::*, +}; + +#[derive(Clone)] +struct Visitor { + name: String, + time: i64, +} + +pub fn temporal_bipartite_projection( + graph: &G, + delta: i64, + pivot_type: String, +) -> Graph { + let new_graph = Graph::new(); + let nodes = graph + .nodes() + .iter() + .filter(|v| v.node_type().unwrap() == pivot_type); + for v in nodes { + populate_edges(graph, &new_graph, v, delta) + } + new_graph +} + +fn populate_edges(g: &G, new_graph: &Graph, v: V, delta: i64) { + if let Some(vertex) = g.node(v) { + // get vector of vertices which need connecting up + let mut visitors = vertex + .edges() + .explode() + .iter() + .map(|e| Visitor { + name: e.nbr().name(), + time: e.time().unwrap(), + }) + .collect_vec(); + visitors.sort_by_key(|vis| vis.time); + + let mut start = 0; + let mut to_process: Vec = vec![]; + for nb in visitors.iter() { + while visitors[start].time + delta < nb.time { + to_process.remove(0); + start += 1 + } + for node in &to_process { + let new_time = average_floor(nb.time, node.time); + new_graph + .add_edge(new_time, node.name.clone(), nb.name.clone(), NO_PROPS, None) + .unwrap(); + } + to_process.push(nb.clone()); + } + } else { + return; + } +} + +#[cfg(test)] +mod bipartite_graph_tests { + use itertools::Itertools; + + use super::temporal_bipartite_projection; + use crate::{ + db::{ + api::{mutation::AdditionOps, view::*}, + graph::graph::Graph, + }, + prelude::{Prop, NO_PROPS}, + }; + + #[test] + fn small_delta_test() { + let g = Graph::new(); + let vs = vec![ + (1, "A", "1"), + (3, "A", "2"), + (3, "B", "2"), + (4, "C", "3"), + (6, "B", "3"), + (8, "A", "3"), + (10, "C", "4"), + (11, "B", "4"), + ]; + for (t, src, dst) in &vs { + g.add_node(*t, *src, NO_PROPS, Some("Left")).unwrap(); + g.add_node(*t, *dst, NO_PROPS, Some("Right")).unwrap(); + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); + } + let new_graph = temporal_bipartite_projection(&g, 1, "Right".to_string()); + assert!(new_graph.has_edge("A", "B")); + assert_eq!(new_graph.edge("A", "B").unwrap().latest_time(), Some(3)); + assert!(new_graph.has_edge("C", "B")); + assert_eq!(new_graph.edge("C", "B").unwrap().latest_time(), Some(10)); + assert!(!new_graph.has_edge("A", "C")); + } + + #[test] + fn larger_delta_test() { + let g = Graph::new(); + let vs = vec![ + (1, "A", "1"), + (3, "A", "2"), + (3, "B", "2"), + (4, "C", "3"), + (6, "B", "3"), + (8, "A", "3"), + (10, "C", "4"), + (11, "B", "4"), + ]; + for (t, src, dst) in &vs { + g.add_node(*t, *src, NO_PROPS, Some("Left")).unwrap(); + g.add_node(*t, *dst, NO_PROPS, Some("Right")).unwrap(); + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); + } + let new_graph = temporal_bipartite_projection(&g, 3, "Right".to_string()); + assert!(new_graph.has_edge("A", "B")); + assert_eq!(new_graph.edge("A", "B").unwrap().earliest_time(), Some(3)); + assert_eq!(new_graph.edge("B", "A").unwrap().latest_time(), Some(7)); + assert!(new_graph.has_edge("C", "B")); + assert_eq!(new_graph.edge("C", "B").unwrap().earliest_time(), Some(5)); + assert_eq!(new_graph.edge("C", "B").unwrap().latest_time(), Some(10)); + assert!(!new_graph.has_edge("A", "C")); + } +} diff --git a/raphtory/src/python/packages/algorithms.rs b/raphtory/src/python/packages/algorithms.rs index c260a3ede3..a3f4652ac0 100644 --- a/raphtory/src/python/packages/algorithms.rs +++ b/raphtory/src/python/packages/algorithms.rs @@ -45,6 +45,7 @@ use crate::{ single_source_shortest_path::single_source_shortest_path as single_source_shortest_path_rs, temporal_reachability::temporally_reachable_nodes as temporal_reachability_rs, }, + projections::temporal_bipartite_projection::temporal_bipartite_projection as temporal_bipartite_rs, }, core::{entities::nodes::node_ref::NodeRef, Prop}, db::{api::view::internal::DynamicGraph, graph::node::NodeView}, @@ -422,6 +423,26 @@ pub fn global_temporal_three_node_motif(g: &PyGraphView, delta: i64) -> [usize; global_temporal_three_node_motif_rs(&g.graph, delta, None) } +/// Projects a temporal bipartite graph into an undirected temporal graph over the pivot node type. Let G be a bipartite graph with node types A and B. Given delta > 0, the projection graph G' pivoting over type B nodes, +/// will make a connection between nodes n1 and n2 (of type A) at time (t1 + t2)/2 if they respectively have an edge at time t1, t2 with the same node of type B in G, and |t2-t1| < delta. +/// +/// Arguments: +/// g (raphtory graph) : A directed raphtory graph +/// delta (int): Time period +/// pivot (string) : node type to pivot over. If a bipartite graph has types A and B, and B is the pivot type, the new graph will consist of type A nodes. +/// +/// Returns: +/// raphtory graph : Projected (unipartite) temporal graph. +#[pyfunction] +#[pyo3(signature = (g, delta, pivot_type))] +pub fn temporal_bipartite_graph_projection( + g: &PyGraphView, + delta: i64, + pivot_type: String, +) -> PyGraphView { + temporal_bipartite_rs(&g.graph, delta, pivot_type).into() +} + /// Computes the global counts of three-edge up-to-three node temporal motifs for a range of timescales. See `global_temporal_three_node_motif` for an interpretation of each row returned. /// /// Arguments: From eae4622700bbec692883550c69ad30bf70493138 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 16:58:21 +0100 Subject: [PATCH 2/2] Bump requests from 2.31.0 to 2.32.0 (#1607) --- updated-dependencies: - dependency-name: requests dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ben Steer --- requirements.in | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.in b/requirements.in index 031c1586da..3de9828cb3 100644 --- a/requirements.in +++ b/requirements.in @@ -3,7 +3,7 @@ pandas matplotlib tqdm raphtory -requests==2.31.0 +requests==2.32.0 pyvis seaborn scipy diff --git a/requirements.txt b/requirements.txt index c521408b0f..a4a4829458 100644 --- a/requirements.txt +++ b/requirements.txt @@ -299,7 +299,7 @@ regex==2023.12.25 # via # nltk # transformers -requests==2.31.0 +requests==2.32.0 # via # -r requirements.in # huggingface-hub