Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Temporal bipartite projection #1396

Merged
merged 12 commits into from
May 28, 2024
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions raphtory/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
1 change: 1 addition & 0 deletions raphtory/src/algorithms/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ pub mod layout;
pub mod metrics;
pub mod motifs;
pub mod pathing;
pub mod projections;
1 change: 1 addition & 0 deletions raphtory/src/algorithms/projections/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod temporal_bipartite_projection;
142 changes: 142 additions & 0 deletions raphtory/src/algorithms/projections/temporal_bipartite_projection.rs
Original file line number Diff line number Diff line change
@@ -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<G: StaticGraphViewOps>(
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: StaticGraphViewOps, V: AsNodeRef>(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<Visitor> = 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"));
}
}
21 changes: 21 additions & 0 deletions raphtory/src/python/packages/algorithms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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:
Expand Down
Loading