scirs2_graph/dynamic/
link_streams.rs1use std::collections::HashSet;
9
10#[derive(Debug, Clone)]
12pub struct TemporalEdge {
13 pub src: usize,
15 pub dst: usize,
17 pub start: f64,
19 pub end: f64,
21}
22
23impl TemporalEdge {
24 pub fn new(src: usize, dst: usize, start: f64, end: f64) -> Self {
26 Self {
27 src,
28 dst,
29 start,
30 end,
31 }
32 }
33
34 pub fn duration(&self) -> f64 {
36 self.end - self.start
37 }
38
39 pub fn is_active_at(&self, t: f64) -> bool {
41 t >= self.start && t <= self.end
42 }
43}
44
45pub struct LinkStream {
47 pub edges: Vec<TemporalEdge>,
49 pub nodes: Vec<usize>,
51 pub time_start: f64,
53 pub time_end: f64,
55}
56
57impl LinkStream {
58 pub fn new(time_start: f64, time_end: f64) -> Self {
60 Self {
61 edges: Vec::new(),
62 nodes: Vec::new(),
63 time_start,
64 time_end,
65 }
66 }
67
68 pub fn add_edge(&mut self, src: usize, dst: usize, start: f64, end: f64) {
70 for &n in &[src, dst] {
71 if !self.nodes.contains(&n) {
72 self.nodes.push(n);
73 }
74 }
75 self.edges.push(TemporalEdge::new(src, dst, start, end));
76 }
77
78 pub fn degree_at(&self, u: usize, t: f64) -> usize {
80 self.edges
81 .iter()
82 .filter(|e| (e.src == u || e.dst == u) && e.is_active_at(t))
83 .count()
84 }
85
86 pub fn average_degree(&self, u: usize, n_samples: usize) -> f64 {
90 if n_samples == 0 {
91 return 0.0;
92 }
93 let dt = (self.time_end - self.time_start) / n_samples as f64;
94 let sum: f64 = (0..n_samples)
95 .map(|i| {
96 let t = self.time_start + (i as f64 + 0.5) * dt;
97 self.degree_at(u, t) as f64
98 })
99 .sum();
100 sum / n_samples as f64
101 }
102
103 pub fn contact_duration(&self, u: usize, v: usize) -> f64 {
105 self.edges
106 .iter()
107 .filter(|e| (e.src == u && e.dst == v) || (e.src == v && e.dst == u))
108 .map(|e| e.duration())
109 .sum()
110 }
111
112 pub fn inter_contact_times(&self, u: usize, v: usize) -> Vec<f64> {
118 let mut contacts: Vec<(f64, f64)> = self
119 .edges
120 .iter()
121 .filter(|e| (e.src == u && e.dst == v) || (e.src == v && e.dst == u))
122 .map(|e| (e.start, e.end))
123 .collect();
124 contacts.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
125
126 contacts
127 .windows(2)
128 .map(|w| w[1].0 - w[0].1)
129 .filter(|&gap| gap > 0.0)
130 .collect()
131 }
132
133 pub fn clustering_coefficient_at(&self, u: usize, t: f64) -> f64 {
138 let neighbors: Vec<usize> = self
139 .edges
140 .iter()
141 .filter(|e| e.is_active_at(t) && (e.src == u || e.dst == u))
142 .map(|e| if e.src == u { e.dst } else { e.src })
143 .collect::<HashSet<_>>()
144 .into_iter()
145 .collect();
146
147 let k = neighbors.len();
148 if k < 2 {
149 return 0.0;
150 }
151
152 let mut triangle_count = 0usize;
153 for i in 0..k {
154 for j in (i + 1)..k {
155 let nb1 = neighbors[i];
156 let nb2 = neighbors[j];
157 let connected = self.edges.iter().any(|e| {
158 e.is_active_at(t)
159 && ((e.src == nb1 && e.dst == nb2) || (e.src == nb2 && e.dst == nb1))
160 });
161 if connected {
162 triangle_count += 1;
163 }
164 }
165 }
166
167 let max_pairs = k * (k - 1) / 2;
169 triangle_count as f64 / max_pairs as f64
170 }
171
172 pub fn n_nodes(&self) -> usize {
174 self.nodes.len()
175 }
176
177 pub fn n_edges(&self) -> usize {
179 self.edges.len()
180 }
181
182 pub fn total_interaction_time(&self) -> f64 {
184 self.edges.iter().map(|e| e.duration()).sum()
185 }
186
187 pub fn stream_density(&self) -> f64 {
191 let n = self.nodes.len();
192 if n < 2 {
193 return 0.0;
194 }
195 let duration = self.time_end - self.time_start;
196 if duration <= 0.0 {
197 return 0.0;
198 }
199 let max_pairs = n * (n - 1) / 2;
200 self.total_interaction_time() / (max_pairs as f64 * duration)
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 fn build_stream() -> LinkStream {
209 let mut ls = LinkStream::new(0.0, 10.0);
210 ls.add_edge(0, 1, 0.0, 3.0);
211 ls.add_edge(0, 2, 1.0, 4.0);
212 ls.add_edge(1, 2, 2.0, 5.0);
213 ls
214 }
215
216 #[test]
217 fn test_contact_duration() {
218 let ls = build_stream();
219 assert!((ls.contact_duration(0, 1) - 3.0).abs() < 1e-9);
220 assert!((ls.contact_duration(0, 2) - 3.0).abs() < 1e-9);
221 }
222
223 #[test]
224 fn test_degree_at() {
225 let ls = build_stream();
226 assert_eq!(
228 ls.degree_at(0, 2.5),
229 2,
230 "node 0 should have degree 2 at t=2.5"
231 );
232 assert_eq!(ls.degree_at(1, 2.5), 2);
233 }
234
235 #[test]
236 fn test_inter_contact_times() {
237 let mut ls = LinkStream::new(0.0, 20.0);
238 ls.add_edge(0, 1, 0.0, 2.0);
239 ls.add_edge(0, 1, 5.0, 7.0);
240 ls.add_edge(0, 1, 12.0, 14.0);
241 let ict = ls.inter_contact_times(0, 1);
242 assert_eq!(ict.len(), 2, "ict={ict:?}");
243 assert!((ict[0] - 3.0).abs() < 1e-9, "ict[0]={}", ict[0]);
244 assert!((ict[1] - 5.0).abs() < 1e-9, "ict[1]={}", ict[1]);
245 }
246
247 #[test]
248 fn test_clustering_coefficient() {
249 let ls = build_stream();
250 let cc = ls.clustering_coefficient_at(0, 2.5);
252 assert!((cc - 1.0).abs() < 1e-9, "cc={cc}");
253 }
254
255 #[test]
256 fn test_total_interaction_time() {
257 let ls = build_stream();
258 assert!((ls.total_interaction_time() - 9.0).abs() < 1e-9);
260 }
261}