use crate::base::{Graph, IndexType};
use crate::error::{GraphError, Result};
use scirs2_core::random::{Rng, RngExt, SeedableRng};
use std::collections::{BinaryHeap, HashMap, HashSet};
#[derive(Debug, Clone, PartialEq)]
pub struct TemporalEdge {
pub source: usize,
pub target: usize,
pub timestamp: f64,
pub weight: Option<f64>,
}
impl TemporalEdge {
pub fn new(source: usize, target: usize, timestamp: f64) -> Self {
TemporalEdge {
source,
target,
timestamp,
weight: None,
}
}
pub fn with_weight(source: usize, target: usize, timestamp: f64, weight: f64) -> Self {
TemporalEdge {
source,
target,
timestamp,
weight: Some(weight),
}
}
}
#[derive(Debug, Clone)]
pub struct TemporalGraph {
n_nodes: usize,
edges: Vec<TemporalEdge>,
sorted: bool,
}
impl TemporalGraph {
pub fn new(n_nodes: usize) -> Self {
TemporalGraph {
n_nodes,
edges: Vec::new(),
sorted: true,
}
}
pub fn n_nodes(&self) -> usize {
self.n_nodes
}
pub fn n_edges(&self) -> usize {
self.edges.len()
}
pub fn node_count(&self) -> usize {
self.n_nodes
}
pub fn sorted_edges_cloned(&self) -> Vec<TemporalEdge> {
let mut v = self.edges.clone();
if !self.sorted {
v.sort_by(|a, b| {
a.timestamp
.partial_cmp(&b.timestamp)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
v
}
pub fn add_edge(&mut self, edge: TemporalEdge) {
if let Some(last) = self.edges.last() {
if edge.timestamp < last.timestamp {
self.sorted = false;
}
}
self.edges.push(edge);
}
fn ensure_sorted(&mut self) {
if !self.sorted {
self.edges.sort_by(|a, b| {
a.timestamp
.partial_cmp(&b.timestamp)
.unwrap_or(std::cmp::Ordering::Equal)
});
self.sorted = true;
}
}
pub fn edges(&mut self) -> &[TemporalEdge] {
self.ensure_sorted();
&self.edges
}
pub fn edges_in_window(&mut self, t_start: f64, t_end: f64) -> &[TemporalEdge] {
self.ensure_sorted();
let lo = self.edges.partition_point(|e| e.timestamp < t_start);
let hi = self.edges.partition_point(|e| e.timestamp < t_end);
&self.edges[lo..hi]
}
pub fn snapshot(&mut self, t_start: f64, t_end: f64) -> Graph<usize, f64> {
let window = self.edges_in_window(t_start, t_end);
let mut edge_weights: HashMap<(usize, usize), f64> = HashMap::new();
let mut active_nodes: HashSet<usize> = HashSet::new();
for e in window {
active_nodes.insert(e.source);
active_nodes.insert(e.target);
let key = (e.source.min(e.target), e.source.max(e.target));
let w = e.weight.unwrap_or(1.0);
*edge_weights.entry(key).or_insert(0.0) += w;
}
let mut graph: Graph<usize, f64> = Graph::new();
for &n in &active_nodes {
graph.add_node(n);
}
for ((u, v), w) in edge_weights {
let _ = graph.add_edge(u, v, w);
}
graph
}
pub fn temporal_neighbors(
&mut self,
node: usize,
t_start: f64,
t_end: f64,
) -> Vec<(usize, f64)> {
let window = self.edges_in_window(t_start, t_end);
let mut first_contact: HashMap<usize, f64> = HashMap::new();
for e in window {
if e.source == node {
first_contact
.entry(e.target)
.and_modify(|t| *t = t.min(e.timestamp))
.or_insert(e.timestamp);
} else if e.target == node {
first_contact
.entry(e.source)
.and_modify(|t| *t = t.min(e.timestamp))
.or_insert(e.timestamp);
}
}
first_contact.into_iter().collect()
}
pub fn temporal_path(
&mut self,
source: usize,
target: usize,
t_start: f64,
t_end: f64,
) -> Option<Vec<usize>> {
if source >= self.n_nodes || target >= self.n_nodes {
return None;
}
if source == target {
return Some(vec![source]);
}
self.ensure_sorted();
let edges = &self.edges;
let mut arrival: Vec<f64> = vec![f64::INFINITY; self.n_nodes];
arrival[source] = t_start;
let mut pred: Vec<Option<usize>> = vec![None; self.n_nodes];
#[derive(PartialEq)]
struct State {
neg_arrival: ordered_float::OrderedFloat<f64>,
node: usize,
}
impl Eq for State {}
impl PartialOrd for State {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for State {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.neg_arrival
.cmp(&other.neg_arrival)
.then(self.node.cmp(&other.node))
}
}
let mut heap = BinaryHeap::new();
heap.push(State {
neg_arrival: ordered_float::OrderedFloat(-t_start),
node: source,
});
while let Some(State { neg_arrival, node }) = heap.pop() {
let arr = -neg_arrival.0;
if arr > arrival[node] {
continue;
}
if node == target {
let mut path = Vec::new();
let mut cur = target;
loop {
path.push(cur);
match pred[cur] {
None => break,
Some(p) => cur = p,
}
}
path.reverse();
return Some(path);
}
let lo = edges.partition_point(|e| e.timestamp < arr);
for e in &edges[lo..] {
if e.timestamp >= t_end {
break;
}
let neighbour = if e.source == node {
e.target
} else if e.target == node {
e.source
} else {
continue;
};
if e.timestamp < arrival[neighbour] {
arrival[neighbour] = e.timestamp;
pred[neighbour] = Some(node);
heap.push(State {
neg_arrival: ordered_float::OrderedFloat(-e.timestamp),
node: neighbour,
});
}
}
}
None
}
pub fn temporal_betweenness(&mut self, t_start: f64, t_end: f64) -> Vec<f64> {
let n = self.n_nodes;
let mut bet = vec![0.0f64; n];
for s in 0..n {
for t in 0..n {
if s == t {
continue;
}
if let Some(path) = self.temporal_path(s, t, t_start, t_end) {
let len = path.len();
if len > 2 {
let credit = 1.0 / (len - 2) as f64;
for &v in &path[1..len - 1] {
bet[v] += credit;
}
}
}
}
}
let norm = (n.saturating_sub(1) * n.saturating_sub(2)) as f64;
if norm > 0.0 {
for b in &mut bet {
*b /= norm;
}
}
bet
}
pub fn aggregate_graph(&mut self) -> Graph<usize, f64> {
let t_start = self
.edges
.iter()
.map(|e| e.timestamp)
.fold(f64::INFINITY, f64::min);
let t_end = self
.edges
.iter()
.map(|e| e.timestamp)
.fold(f64::NEG_INFINITY, f64::max);
if t_start.is_infinite() {
let mut g: Graph<usize, f64> = Graph::new();
for i in 0..self.n_nodes {
g.add_node(i);
}
return g;
}
self.snapshot(t_start, t_end + 1.0)
}
}
pub fn burstiness(node_events: &[f64]) -> f64 {
if node_events.len() < 2 {
return 0.0;
}
let mut sorted = node_events.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let iet: Vec<f64> = sorted.windows(2).map(|w| w[1] - w[0]).collect();
let n = iet.len() as f64;
let mu = iet.iter().sum::<f64>() / n;
if mu == 0.0 {
return 0.0;
}
let variance = iet.iter().map(|x| (x - mu).powi(2)).sum::<f64>() / n;
let sigma = variance.sqrt();
(sigma - mu) / (sigma + mu)
}
pub fn activity_driven_model<R: Rng>(
n_nodes: usize,
n_steps: usize,
activity_rates: &[f64],
rng: &mut R,
) -> Result<TemporalGraph> {
if activity_rates.len() != n_nodes {
return Err(GraphError::InvalidGraph(format!(
"activity_rates length {} != n_nodes {}",
activity_rates.len(),
n_nodes
)));
}
for (i, &a) in activity_rates.iter().enumerate() {
if !(0.0..=1.0).contains(&a) {
return Err(GraphError::InvalidGraph(format!(
"activity_rates[{i}] = {a} is outside [0, 1]"
)));
}
}
if n_nodes < 2 {
return Err(GraphError::InvalidGraph(
"need at least 2 nodes for activity-driven model".to_string(),
));
}
let mut tg = TemporalGraph::new(n_nodes);
for step in 0..n_steps {
let t = step as f64;
for i in 0..n_nodes {
if rng.random::<f64>() < activity_rates[i] {
let offset: usize = rng.random_range(0..(n_nodes - 1));
let j = if offset < i { offset } else { offset + 1 };
tg.add_edge(TemporalEdge::new(i, j, t));
}
}
}
tg.ensure_sorted();
Ok(tg)
}
pub fn activity_driven_model_seeded(
n_nodes: usize,
n_steps: usize,
activity_rates: &[f64],
seed: u64,
) -> Result<TemporalGraph> {
use scirs2_core::random::ChaCha20Rng;
let mut rng = ChaCha20Rng::seed_from_u64(seed);
activity_driven_model(n_nodes, n_steps, activity_rates, &mut rng)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_chain() -> TemporalGraph {
let mut tg = TemporalGraph::new(4);
tg.add_edge(TemporalEdge::new(0, 1, 1.0));
tg.add_edge(TemporalEdge::new(1, 2, 2.0));
tg.add_edge(TemporalEdge::new(2, 3, 3.0));
tg
}
#[test]
fn test_add_and_sort() {
let mut tg = TemporalGraph::new(3);
tg.add_edge(TemporalEdge::new(0, 1, 5.0));
tg.add_edge(TemporalEdge::new(1, 2, 2.0)); tg.add_edge(TemporalEdge::new(0, 2, 8.0));
let edges = tg.edges();
assert_eq!(edges.len(), 3);
let timestamps: Vec<f64> = edges.iter().map(|e| e.timestamp).collect();
assert!(timestamps.windows(2).all(|w| w[0] <= w[1]));
}
#[test]
fn test_snapshot() {
let mut tg = make_chain();
let snap = tg.snapshot(0.0, 2.5);
assert_eq!(snap.edge_count(), 2);
}
#[test]
fn test_temporal_neighbors() {
let mut tg = make_chain();
let nbrs = tg.temporal_neighbors(1, 0.0, 10.0);
let nbr_ids: Vec<usize> = nbrs.iter().map(|(n, _)| *n).collect();
assert!(nbr_ids.contains(&0));
assert!(nbr_ids.contains(&2));
}
#[test]
fn test_temporal_path_found() {
let mut tg = make_chain();
let path = tg.temporal_path(0, 3, 0.0, 10.0);
assert!(path.is_some());
let p = path.expect("path should exist");
assert_eq!(p.first(), Some(&0));
assert_eq!(p.last(), Some(&3));
}
#[test]
fn test_temporal_path_no_backwards() {
let mut tg = make_chain();
let path = tg.temporal_path(0, 3, 0.0, 2.5);
assert!(path.is_none());
}
#[test]
fn test_temporal_path_same_source_target() {
let mut tg = make_chain();
let path = tg.temporal_path(2, 2, 0.0, 10.0);
assert_eq!(path, Some(vec![2]));
}
#[test]
fn test_temporal_betweenness_chain() {
let mut tg = make_chain();
let bet = tg.temporal_betweenness(0.0, 10.0);
assert_eq!(bet.len(), 4);
assert_eq!(bet[0], 0.0);
assert_eq!(bet[3], 0.0);
}
#[test]
fn test_burstiness_regular() {
let events: Vec<f64> = (0..10).map(|i| i as f64).collect();
let b = burstiness(&events);
assert!(
(b - (-1.0)).abs() < 1e-9,
"perfectly regular events should have B=-1 (periodic), got {b}"
);
}
#[test]
fn test_burstiness_few_events() {
assert_eq!(burstiness(&[]), 0.0);
assert_eq!(burstiness(&[1.0]), 0.0);
}
#[test]
fn test_burstiness_bursty() {
let mut events: Vec<f64> = (0..10).map(|i| i as f64 * 0.001).collect();
events.push(1000.0);
let b = burstiness(&events);
assert!(b > 0.0, "bursty sequence should have B>0, got {b}");
}
#[test]
fn test_activity_driven_model() {
let rates = vec![0.5; 5];
let tg = activity_driven_model_seeded(5, 20, &rates, 42).expect("model generation failed");
assert_eq!(tg.n_nodes(), 5);
assert!(tg.n_edges() > 0);
}
#[test]
fn test_activity_driven_model_validation() {
let err = activity_driven_model_seeded(5, 10, &[0.5; 3], 0);
assert!(err.is_err());
let err = activity_driven_model_seeded(3, 10, &[0.5, 1.5, 0.3], 0);
assert!(err.is_err());
let err = activity_driven_model_seeded(1, 10, &[0.5], 0);
assert!(err.is_err());
}
#[test]
fn test_aggregate_graph() {
let mut tg = make_chain();
let agg = tg.aggregate_graph();
assert_eq!(agg.node_count(), 4);
assert_eq!(agg.edge_count(), 3);
}
#[test]
fn test_weighted_edge() {
let e = TemporalEdge::with_weight(0, 1, 5.0, 2.5);
assert_eq!(e.weight, Some(2.5));
assert_eq!(e.timestamp, 5.0);
}
#[test]
fn test_edges_in_window() {
let mut tg = TemporalGraph::new(3);
for t in [1.0, 2.0, 3.0, 4.0, 5.0] {
tg.add_edge(TemporalEdge::new(0, 1, t));
}
let window = tg.edges_in_window(2.0, 4.0);
assert_eq!(window.len(), 2); }
}