use super::graph::TemporalGraph;
#[derive(Debug, Clone, Default)]
pub struct TemporalMotifCounts {
pub m1_triangle_fwd: usize,
pub m2_triangle_bck: usize,
pub m3_star_out: usize,
pub m4_star_in: usize,
pub m5_path_fwd: usize,
pub m6_path_bck: usize,
pub m7_path_in: usize,
pub m8_path_mix: usize,
}
impl TemporalMotifCounts {
pub fn total(&self) -> usize {
self.m1_triangle_fwd
+ self.m2_triangle_bck
+ self.m3_star_out
+ self.m4_star_in
+ self.m5_path_fwd
+ self.m6_path_bck
+ self.m7_path_in
+ self.m8_path_mix
}
pub fn triangle_count(&self) -> usize {
self.m1_triangle_fwd + self.m2_triangle_bck + self.m3_star_out + self.m4_star_in
}
pub fn path_count(&self) -> usize {
self.m5_path_fwd + self.m6_path_bck + self.m7_path_in + self.m8_path_mix
}
}
pub fn count_temporal_triangles(tg: &mut TemporalGraph, delta: f64) -> usize {
tg.ensure_sorted();
let edges = tg.edges.clone(); let m = edges.len();
let n = tg.nodes;
let mut adj_set: Vec<std::collections::HashSet<usize>> =
vec![std::collections::HashSet::new(); n];
for e in &edges {
adj_set[e.source].insert(e.target);
adj_set[e.target].insert(e.source);
}
let mut count = 0usize;
for i in 0..m {
let e1 = &edges[i];
for j in (i + 1)..m {
let e2 = &edges[j];
if e2.timestamp > e1.timestamp + delta {
break; }
for k in (j + 1)..m {
let e3 = &edges[k];
if e3.timestamp > e1.timestamp + delta {
break;
}
let nodes_used = distinct_triangle_nodes(
e1.source, e1.target, e2.source, e2.target, e3.source, e3.target,
);
if let Some((u, v, w)) = nodes_used {
let _ = (u, v, w); count += 1;
}
}
}
}
count
}
fn distinct_triangle_nodes(
s1: usize,
t1: usize,
s2: usize,
t2: usize,
s3: usize,
t3: usize,
) -> Option<(usize, usize, usize)> {
let mut nodes = [s1, t1, s2, t2, s3, t3];
nodes.sort_unstable();
let mut distinct: Vec<usize> = Vec::with_capacity(3);
for &n in &nodes {
if distinct.last() != Some(&n) {
distinct.push(n);
}
}
if distinct.len() != 3 {
return None;
}
let (a, b, c) = (distinct[0], distinct[1], distinct[2]);
let pair_ab = (s1 == a && t1 == b) || (s1 == b && t1 == a);
let pair_ac = (s1 == a && t1 == c) || (s1 == c && t1 == a);
let pair_bc = (s1 == b && t1 == c) || (s1 == c && t1 == b);
let e1_ok = pair_ab || pair_ac || pair_bc;
if !e1_ok {
return None;
}
let pair2_ab = (s2 == a && t2 == b) || (s2 == b && t2 == a);
let pair2_ac = (s2 == a && t2 == c) || (s2 == c && t2 == a);
let pair2_bc = (s2 == b && t2 == c) || (s2 == c && t2 == b);
let e2_ok = pair2_ab || pair2_ac || pair2_bc;
if !e2_ok {
return None;
}
let pair3_ab = (s3 == a && t3 == b) || (s3 == b && t3 == a);
let pair3_ac = (s3 == a && t3 == c) || (s3 == c && t3 == a);
let pair3_bc = (s3 == b && t3 == c) || (s3 == c && t3 == b);
let e3_ok = pair3_ab || pair3_ac || pair3_bc;
if !e3_ok {
return None;
}
let pairs = [
ordered_pair(s1, t1),
ordered_pair(s2, t2),
ordered_pair(s3, t3),
];
let p_ab = ordered_pair(a, b);
let p_ac = ordered_pair(a, c);
let p_bc = ordered_pair(b, c);
let covers_ab = pairs.contains(&p_ab);
let covers_ac = pairs.contains(&p_ac);
let covers_bc = pairs.contains(&p_bc);
if covers_ab && covers_ac && covers_bc {
Some((a, b, c))
} else {
None
}
}
#[inline]
fn ordered_pair(a: usize, b: usize) -> (usize, usize) {
(a.min(b), a.max(b))
}
pub fn temporal_motif_count(tg: &mut TemporalGraph, delta: f64) -> TemporalMotifCounts {
tg.ensure_sorted();
let edges = tg.edges.clone();
let m = edges.len();
let mut counts = TemporalMotifCounts::default();
for i in 0..m {
let e1 = &edges[i];
for j in (i + 1)..m {
let e2 = &edges[j];
if e2.timestamp > e1.timestamp + delta {
break;
}
classify_two_edge_motif(e1.source, e1.target, e2.source, e2.target, &mut counts);
for k in (j + 1)..m {
let e3 = &edges[k];
if e3.timestamp > e1.timestamp + delta {
break;
}
classify_three_edge_motif(
e1.source,
e1.target,
e2.source,
e2.target,
e3.source,
e3.target,
&mut counts,
);
}
}
}
counts
}
fn classify_two_edge_motif(
s1: usize,
t1: usize,
s2: usize,
t2: usize,
counts: &mut TemporalMotifCounts,
) {
if t1 == s2 && s1 != t2 {
counts.m5_path_fwd += 1;
return;
}
if s1 == t2 && t1 != s2 {
counts.m8_path_mix += 1;
return;
}
if s1 == s2 && t1 != t2 {
counts.m6_path_bck += 1;
return;
}
if t1 == t2 && s1 != s2 {
counts.m7_path_in += 1;
}
}
fn classify_three_edge_motif(
s1: usize,
t1: usize,
s2: usize,
t2: usize,
s3: usize,
t3: usize,
counts: &mut TemporalMotifCounts,
) {
if distinct_triangle_nodes(s1, t1, s2, t2, s3, t3).is_none() {
return;
}
let mut node_set = [s1, t1, s2, t2, s3, t3];
node_set.sort_unstable();
let mut nodes: Vec<usize> = node_set.to_vec();
nodes.dedup();
if nodes.len() != 3 {
return;
}
if t1 == s2 && s1 == s3 && t2 == t3 {
counts.m1_triangle_fwd += 1;
return;
}
if t1 == t2 && s1 == s3 && s2 == t3 {
counts.m2_triangle_bck += 1;
return;
}
if s1 == s2 && t1 == s3 && t2 == t3 {
counts.m3_star_out += 1;
return;
}
if t1 == t2 && s1 == s3 && s2 == t3 {
counts.m4_star_in += 1;
}
}
#[cfg(test)]
mod tests {
use super::super::graph::TemporalEdge;
use super::*;
fn make_triangle_fwd() -> TemporalGraph {
let mut tg = TemporalGraph::new(3);
tg.add_edge(TemporalEdge::new(0, 1, 1.0));
tg.add_edge(TemporalEdge::new(1, 2, 2.0));
tg.add_edge(TemporalEdge::new(0, 2, 3.0));
tg
}
fn make_path() -> TemporalGraph {
let mut tg = TemporalGraph::new(3);
tg.add_edge(TemporalEdge::new(0, 1, 1.0));
tg.add_edge(TemporalEdge::new(1, 2, 2.0));
tg
}
#[test]
fn test_count_temporal_triangles_basic() {
let mut tg = make_triangle_fwd();
let count = count_temporal_triangles(&mut tg, 5.0);
assert!(count >= 1, "should find at least one temporal triangle");
}
#[test]
fn test_count_temporal_triangles_no_match() {
let mut tg = make_path();
let count = count_temporal_triangles(&mut tg, 5.0);
assert_eq!(count, 0, "open path has no triangle");
}
#[test]
fn test_count_temporal_triangles_delta_too_small() {
let mut tg = make_triangle_fwd();
let count = count_temporal_triangles(&mut tg, 0.5);
assert_eq!(count, 0, "edges spread > 0.5 should give no triangle");
}
#[test]
fn test_temporal_motif_count_path() {
let mut tg = make_path();
let counts = temporal_motif_count(&mut tg, 5.0);
assert!(
counts.m5_path_fwd >= 1,
"expected at least one M5 forward path, got {}",
counts.m5_path_fwd
);
}
#[test]
fn test_temporal_motif_count_total_is_sum() {
let mut tg = make_triangle_fwd();
let counts = temporal_motif_count(&mut tg, 5.0);
assert_eq!(
counts.total(),
counts.triangle_count() + counts.path_count(),
"total should equal triangle + path counts"
);
}
#[test]
fn test_temporal_motif_count_no_edges() {
let mut tg = TemporalGraph::new(5);
let counts = temporal_motif_count(&mut tg, 10.0);
assert_eq!(counts.total(), 0);
}
#[test]
fn test_motif_counts_struct() {
let c = TemporalMotifCounts {
m1_triangle_fwd: 2,
m5_path_fwd: 3,
..Default::default()
};
assert_eq!(c.total(), 5);
assert_eq!(c.triangle_count(), 2);
assert_eq!(c.path_count(), 3);
}
}