use crate::error::{GraphError, Result};
use crate::temporal_graph::{TemporalEdge, TemporalGraph};
use scirs2_core::rand_prelude::IndexedRandom;
use scirs2_core::random::prelude::*;
pub use crate::temporal_graph::{activity_driven_model, activity_driven_model_seeded, burstiness};
pub fn temporal_barabasi_albert<R: Rng>(n: usize, m: usize, rng: &mut R) -> Result<TemporalGraph> {
if n < 2 {
return Err(GraphError::InvalidGraph(
"temporal_barabasi_albert: n must be ≥ 2".to_string(),
));
}
if m == 0 {
return Err(GraphError::InvalidGraph(
"temporal_barabasi_albert: m must be ≥ 1".to_string(),
));
}
if m >= n {
return Err(GraphError::InvalidGraph(format!(
"temporal_barabasi_albert: m={m} must be < n={n}"
)));
}
let mut tg = TemporalGraph::new(n);
let mut degree = vec![0usize; n];
let seed_size = m + 1;
for u in 0..seed_size {
for v in (u + 1)..seed_size {
tg.add_edge(TemporalEdge::new(u, v, 0.0));
tg.add_edge(TemporalEdge::new(v, u, 0.0));
degree[u] += 1;
degree[v] += 1;
}
}
for v in seed_size..n {
let t_arrive = v as f64 / n as f64;
let candidates: Vec<usize> = (0..v).collect();
let weights: Vec<f64> = candidates.iter().map(|&u| (degree[u] + 1) as f64).collect();
let chosen = weighted_sample_without_replacement(&candidates, &weights, m, rng);
for &u in &chosen {
tg.add_edge(TemporalEdge::new(v, u, t_arrive));
tg.add_edge(TemporalEdge::new(u, v, t_arrive));
degree[u] += 1;
degree[v] += 1;
}
}
Ok(tg)
}
fn weighted_sample_without_replacement<R: Rng>(
items: &[usize],
weights: &[f64],
k: usize,
rng: &mut R,
) -> Vec<usize> {
if items.is_empty() || k == 0 {
return Vec::new();
}
let k = k.min(items.len());
let mut keyed: Vec<(f64, usize)> = items
.iter()
.zip(weights.iter())
.map(|(&item, &w)| {
let u: f64 = rng.random::<f64>().max(1e-300);
let key = if w > 0.0 { u.powf(1.0 / w) } else { 0.0 };
(key, item)
})
.collect();
keyed.sort_unstable_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
keyed[..k].iter().map(|&(_, item)| item).collect()
}
#[derive(Debug, Clone)]
pub struct TemporalWalk {
pub steps: Vec<(usize, f64)>,
}
impl TemporalWalk {
pub fn nodes(&self) -> Vec<usize> {
self.steps.iter().map(|&(n, _)| n).collect()
}
pub fn len(&self) -> usize {
self.steps.len().saturating_sub(1)
}
pub fn is_empty(&self) -> bool {
self.steps.len() <= 1
}
}
pub fn temporal_random_walk<R: Rng>(
tg: &TemporalGraph,
source: usize,
max_length: usize,
num_walks: usize,
start_time: Option<f64>,
rng: &mut R,
) -> Result<Vec<TemporalWalk>> {
if source >= tg.node_count() {
return Err(GraphError::InvalidGraph(format!(
"temporal_random_walk: source={source} ≥ n={}",
tg.node_count()
)));
}
if max_length == 0 {
return Err(GraphError::InvalidGraph(
"temporal_random_walk: max_length must be ≥ 1".to_string(),
));
}
let mut walks = Vec::with_capacity(num_walks);
let sorted_edges = tg.sorted_edges_cloned();
let n = tg.node_count();
let mut adj: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
for edge in &sorted_edges {
adj[edge.source].push((edge.target, edge.timestamp));
adj[edge.target].push((edge.source, edge.timestamp));
}
for nbrs in adj.iter_mut() {
nbrs.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
}
let t0 = start_time.unwrap_or(f64::NEG_INFINITY);
for _ in 0..num_walks {
let mut walk_steps = vec![(source, t0)];
let mut current_node = source;
let mut current_time = t0;
for _ in 0..max_length {
let candidates: Vec<(usize, f64)> = adj[current_node]
.iter()
.filter(|&&(_, t)| t >= current_time)
.copied()
.collect();
if candidates.is_empty() {
break;
}
let &(next_node, next_time) = candidates.choose(rng).expect("candidates is non-empty");
walk_steps.push((next_node, next_time));
current_node = next_node;
current_time = next_time;
}
walks.push(TemporalWalk { steps: walk_steps });
}
Ok(walks)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tba_basic() {
let mut rng = StdRng::seed_from_u64(42);
let tg = temporal_barabasi_albert(20, 2, &mut rng)
.expect("temporal_barabasi_albert should succeed");
assert_eq!(tg.node_count(), 20);
assert!(tg.n_edges() > 0);
}
#[test]
fn test_tba_scale_free_degree() {
let mut rng = StdRng::seed_from_u64(7);
let tg = temporal_barabasi_albert(100, 3, &mut rng)
.expect("temporal_barabasi_albert should succeed");
assert_eq!(tg.node_count(), 100);
assert!(tg.n_edges() > 0);
}
#[test]
fn test_tba_invalid_params() {
let mut rng = StdRng::seed_from_u64(0);
assert!(temporal_barabasi_albert(1, 1, &mut rng).is_err());
assert!(temporal_barabasi_albert(10, 0, &mut rng).is_err());
assert!(temporal_barabasi_albert(5, 5, &mut rng).is_err());
}
#[test]
fn test_tba_timestamps_increasing() {
let mut rng = StdRng::seed_from_u64(13);
let tg = temporal_barabasi_albert(30, 2, &mut rng)
.expect("temporal_barabasi_albert should succeed");
for edge in tg.sorted_edges_cloned() {
assert!(
edge.timestamp >= 0.0,
"All timestamps should be non-negative"
);
}
}
#[test]
fn test_random_walk_basic() {
let mut rng = StdRng::seed_from_u64(1);
let tg = temporal_barabasi_albert(20, 2, &mut rng)
.expect("temporal_barabasi_albert should succeed");
let walks = temporal_random_walk(&tg, 0, 5, 3, None, &mut rng)
.expect("temporal_random_walk should succeed");
assert_eq!(walks.len(), 3);
for walk in &walks {
assert!(walk.len() <= 5);
assert_eq!(walk.steps[0].0, 0);
}
}
#[test]
fn test_random_walk_time_respecting() {
let mut rng = StdRng::seed_from_u64(2);
let tg = temporal_barabasi_albert(30, 2, &mut rng)
.expect("temporal_barabasi_albert should succeed");
let walks = temporal_random_walk(&tg, 0, 10, 5, None, &mut rng)
.expect("temporal_random_walk should succeed");
for walk in &walks {
let timestamps: Vec<f64> = walk.steps.iter().map(|&(_, t)| t).collect();
for window in timestamps.windows(2) {
assert!(
window[1] >= window[0],
"Walk timestamps must be non-decreasing: {:?}",
timestamps
);
}
}
}
#[test]
fn test_random_walk_with_start_time() {
let mut rng = StdRng::seed_from_u64(3);
let tg = temporal_barabasi_albert(20, 2, &mut rng)
.expect("temporal_barabasi_albert should succeed");
let start_t = 0.5;
let walks = temporal_random_walk(&tg, 0, 5, 2, Some(start_t), &mut rng)
.expect("temporal_random_walk should succeed");
for walk in &walks {
for &(_, t) in &walk.steps[1..] {
assert!(
t >= start_t,
"Edge timestamps should be ≥ start_time={start_t}, got {t}"
);
}
}
}
#[test]
fn test_random_walk_invalid_source() {
let mut rng = StdRng::seed_from_u64(0);
let tg = temporal_barabasi_albert(10, 2, &mut rng)
.expect("temporal_barabasi_albert should succeed");
assert!(temporal_random_walk(&tg, 100, 5, 1, None, &mut rng).is_err());
}
#[test]
fn test_random_walk_zero_max_length() {
let mut rng = StdRng::seed_from_u64(0);
let tg = temporal_barabasi_albert(10, 2, &mut rng)
.expect("temporal_barabasi_albert should succeed");
assert!(temporal_random_walk(&tg, 0, 0, 1, None, &mut rng).is_err());
}
#[test]
fn test_temporal_walk_is_empty() {
let walk = TemporalWalk {
steps: vec![(0, f64::NEG_INFINITY)],
};
assert!(walk.is_empty());
assert_eq!(walk.len(), 0);
assert_eq!(walk.nodes(), vec![0]);
}
#[test]
fn test_temporal_walk_nodes() {
let walk = TemporalWalk {
steps: vec![(0, 0.0), (1, 1.0), (2, 2.0)],
};
assert_eq!(walk.nodes(), vec![0, 1, 2]);
assert_eq!(walk.len(), 2);
assert!(!walk.is_empty());
}
}