Skip to content

Commit

Permalink
implementation of temporal rich club (#1692)
Browse files Browse the repository at this point in the history
* implementation of temporal rich club

* run cargo fmt

* Remove the dependence on unstable feature and add documentation.

* add some comments into the functions

* remove commented out code

* rm self loops
  • Loading branch information
narnolddd authored Sep 6, 2024
1 parent b1105e0 commit b317933
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 3 deletions.
4 changes: 2 additions & 2 deletions raphtory/src/algorithms/centrality/pagerank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ pub fn unweighted_page_rank<G: StaticGraphViewOps>(
}

#[cfg(test)]
mod page_rank_tests {
pub mod page_rank_tests {
use super::*;
use crate::{
db::{api::mutation::AdditionOps, graph::graph::Graph},
Expand Down Expand Up @@ -352,7 +352,7 @@ mod page_rank_tests {
});
}

fn assert_eq_f64<T: Borrow<f64> + PartialEq + std::fmt::Debug>(
pub fn assert_eq_f64<T: Borrow<f64> + PartialEq + std::fmt::Debug>(
a: Option<T>,
b: Option<T>,
decimals: u8,
Expand Down
1 change: 1 addition & 0 deletions raphtory/src/algorithms/motifs/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod global_temporal_three_node_motifs;
pub mod local_temporal_three_node_motifs;
pub mod local_triangle_count;
pub mod temporal_rich_club_coefficient;
pub mod three_node_motifs;
pub mod triangle_count;
pub mod triplet_count;
188 changes: 188 additions & 0 deletions raphtory/src/algorithms/motifs/temporal_rich_club_coefficient.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
use std::{
borrow::Borrow,
cmp::{self, max, min},
collections::HashSet,
default,
};

use raphtory_api::core::entities::VID;
use rustc_hash::FxHashSet;

use rayon::prelude::*;

use crate::prelude::{EdgeViewOps, GraphViewOps, NodeViewOps};

struct SlidingWindows<I> {
iter: I,
window_size: usize,
}

impl<I> SlidingWindows<I> {
fn new(iter: I, window_size: usize) -> Self {
SlidingWindows { iter, window_size }
}
}

impl<I> Iterator for SlidingWindows<I>
where
I: Iterator,
I::Item: Clone,
{
type Item = Vec<I::Item>;

fn next(&mut self) -> Option<Self::Item> {
let mut window = Vec::with_capacity(self.window_size);
for _ in 0..self.window_size {
if let Some(item) = self.iter.next() {
window.push(item);
} else {
return None;
}
}
Some(window)
}
}

pub fn temporal_rich_club_coefficient<'a, I, G1, G2>(
agg_graph: G2,
views: I,
k: usize,
window_size: usize,
) -> f64
where
I: IntoIterator<Item = G1>,
G1: GraphViewOps<'a>,
G2: GraphViewOps<'a>,
{
// Extract the set of nodes with degree greater than or equal to k
let s_k: HashSet<VID> = agg_graph
.nodes()
.into_iter()
.filter(|v| v.degree() >= k)
.map(|v| v.node)
.collect();

if s_k.len() <= 1 {
return 0.0 as f64;
}

let temp_rich_club_val = SlidingWindows::new(views.into_iter(), window_size)
.map(|window| intermediate_rich_club_coef(s_k.clone(), window))
.reduce(f64::max)
.unwrap_or(0.0);

temp_rich_club_val
}

fn intermediate_rich_club_coef<'a, I, G1>(s_k: HashSet<VID>, views: I) -> f64
where
I: IntoIterator<Item = G1>,
G1: GraphViewOps<'a>,
{
// Extract the edges among the top degree nodes which are stable over that subset of snapshots by computing their intersection
let stable_edges = views
.into_iter()
.map(|g| {
let new_edges: HashSet<UndirEdge> = g
.subgraph(s_k.clone())
.edges()
.into_iter()
.filter(|e| e.src() != e.dst())
.map(|e| undir_edge(e.src().node, e.dst().node))
.collect();
new_edges
})
.into_iter()
.reduce(|acc_edges, item_edges| acc_edges.intersection(&item_edges).cloned().collect());
// Compute the density with respect to the possible number of edges between those s_k nodes.
match stable_edges {
Some(edges) => {
let poss_edges = (s_k.len() * (s_k.len() - 1)) / 2;
return (edges.len() as f64) / (poss_edges as f64);
}
None => return 0 as f64,
}
}

#[derive(Hash, Eq, PartialEq, Debug, Clone)]
pub struct UndirEdge {
src: VID,
dst: VID,
}

// So that we can make a set of undirected edges
fn undir_edge<T: Into<VID>>(src: T, dst: T) -> UndirEdge {
let src_id: VID = src.into();
let dst_id: VID = dst.into();
UndirEdge {
src: min(src_id, dst_id),
dst: max(src_id, dst_id),
}
}

#[cfg(test)]
mod rich_club_test {
use super::*;
use crate::{
algorithms::centrality::pagerank::page_rank_tests::assert_eq_f64,
db::{api::mutation::AdditionOps, graph::graph::Graph},
prelude::{TimeOps, NO_PROPS},
test_storage,
};

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

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

fn load_sample_graph() -> Graph {
let edges = vec![
(1, 1, 2),
(1, 1, 3),
(1, 1, 4),
(1, 2, 3),
(1, 2, 4),
(1, 3, 4),
(1, 4, 5),
(2, 1, 2),
(2, 1, 3),
(2, 1, 4),
(2, 3, 4),
(2, 2, 6),
(3, 1, 2),
(3, 2, 4),
(3, 3, 4),
(3, 1, 4),
(3, 1, 3),
(3, 1, 7),
(4, 1, 2),
(4, 1, 3),
(4, 1, 4),
(4, 2, 8),
(5, 1, 2),
(5, 1, 3),
(5, 1, 4),
(5, 2, 4),
(5, 3, 9),
];
load_graph(edges)
}

#[test]
// Using the toy example from the paper
fn toy_graph_test() {
let g = load_sample_graph();
let g_rolling = g.rolling(1, Some(1)).unwrap();

let rc_coef_1 = temporal_rich_club_coefficient(g.clone(), g_rolling.clone(), 3, 1);
let rc_coef_3 = temporal_rich_club_coefficient(g.clone(), g_rolling.clone(), 3, 3);
let rc_coef_5 = temporal_rich_club_coefficient(g.clone(), g_rolling.clone(), 3, 5);
assert_eq_f64(Some(rc_coef_1), Some(1.0), 3);
assert_eq_f64(Some(rc_coef_3), Some(0.66666), 3);
assert_eq_f64(Some(rc_coef_5), Some(0.5), 3);
}
}
41 changes: 40 additions & 1 deletion raphtory/src/python/packages/algorithms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ use crate::{
},
local_temporal_three_node_motifs::temporal_three_node_motif as local_three_node_rs,
local_triangle_count::local_triangle_count as local_triangle_count_rs,
temporal_rich_club_coefficient::temporal_rich_club_coefficient as temporal_rich_club_rs,
},
pathing::{
dijkstra::dijkstra_single_source_shortest_paths as dijkstra_single_source_shortest_paths_rs,
Expand All @@ -54,8 +55,9 @@ use crate::{
utils::PyTime,
},
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
use pyo3::prelude::*;
use pyo3::{prelude::*, types::PyIterator};
use rand::{prelude::StdRng, SeedableRng};
use raphtory_api::core::entities::GID;
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -802,3 +804,40 @@ pub fn cohesive_fruchterman_reingold(
.map(|(id, vector)| (id, [vector.x, vector.y]))
.collect()
}

/// Temporal rich club coefficient
///
/// The traditional rich-club coefficient in a static undirected graph measures the density of connections between the highest
/// degree nodes. It takes a single parameter k, creates a subgraph of the nodes of degree greater than or equal to k, and
/// returns the density of this subgraph.
///
/// In a temporal graph taking the form of a sequence of static snapshots, the temporal rich club coefficient takes a parameter k
/// and a window size delta (both positive integers). It measures the maximal density of the highest connected nodes (of degree
/// greater than or equal to k in the aggregate graph) that persists at least a delta number of consecutive snapshots. For an in-depth
/// definition and usage example, please read to the following paper: Pedreschi, N., Battaglia, D., & Barrat, A. (2022). The temporal
/// rich club phenomenon. Nature Physics, 18(8), 931-938.
///
/// Arguments:
/// graph (GraphView): the aggregate graph
/// views (iterator(GraphView)): sequence of graphs (can be obtained by calling g.rolling(..) on an aggregate graph g)
/// k (int): min degree of nodes to include in rich-club
/// delta (int): the number of consecutive snapshots over which the edges should persist
///
/// Returns:
/// the rich-club coefficient as a float.
#[pyfunction]
#[pyo3[signature = (graph, views, k, delta)]]
pub fn temporal_rich_club_coefficient(
graph: PyGraphView,
views: &PyAny,
k: usize,
delta: usize,
) -> f64 {
let py_iterator = PyIterator::from_object(views).unwrap();
let iter = py_iterator.map(|item| {
item.and_then(PyGraphView::extract)
.map(|pgv: PyGraphView| pgv.graph)
.unwrap()
});
temporal_rich_club_rs(graph.graph, iter, k, delta)
}

0 comments on commit b317933

Please sign in to comment.