scirs2_graph/
temporal.rs

1//! Temporal graph structures and algorithms
2//!
3//! This module provides data structures and algorithms for temporal graphs,
4//! where edges and nodes can have time-dependent properties.
5
6use crate::base::{EdgeWeight, Graph, IndexType, Node};
7use crate::error::{GraphError, Result};
8use std::collections::{BTreeMap, HashMap, HashSet};
9
10/// Represents a time instant or interval
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct TimeInstant {
13    /// Time value (could represent seconds, milliseconds, etc.)
14    pub time: u64,
15}
16
17impl TimeInstant {
18    /// Create a new time instant
19    pub fn new(time: u64) -> Self {
20        TimeInstant { time }
21    }
22
23    /// Get the time value
24    pub fn value(&self) -> u64 {
25        self.time
26    }
27}
28
29/// Represents a time interval [start, end)
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct TimeInterval {
32    /// Start time (inclusive)
33    pub start: TimeInstant,
34    /// End time (exclusive)
35    pub end: TimeInstant,
36}
37
38impl TimeInterval {
39    /// Create a new time interval
40    pub fn new(start: u64, end: u64) -> Result<Self> {
41        if start >= end {
42            return Err(GraphError::InvalidGraph(
43                "Start time must be before end time".to_string(),
44            ));
45        }
46        Ok(TimeInterval {
47            start: TimeInstant::new(start),
48            end: TimeInstant::new(end),
49        })
50    }
51
52    /// Check if this interval contains a time instant
53    pub fn contains(&self, time: TimeInstant) -> bool {
54        time >= self.start && time < self.end
55    }
56
57    /// Check if this interval overlaps with another
58    pub fn overlaps(&self, other: &TimeInterval) -> bool {
59        self.start < other.end && other.start < self.end
60    }
61
62    /// Get the duration of this interval
63    pub fn duration(&self) -> u64 {
64        self.end.time - self.start.time
65    }
66
67    /// Get the intersection of two intervals
68    pub fn intersection(&self, other: &TimeInterval) -> Option<TimeInterval> {
69        if !self.overlaps(other) {
70            return None;
71        }
72
73        let start = self.start.max(other.start);
74        let end = self.end.min(other.end);
75
76        if start < end {
77            Some(TimeInterval { start, end })
78        } else {
79            None
80        }
81    }
82}
83
84/// A temporal edge with time-dependent properties
85#[derive(Debug, Clone)]
86pub struct TemporalEdge<N: Node, E: EdgeWeight> {
87    /// Source node
88    pub source: N,
89    /// Target node
90    pub target: N,
91    /// Edge weight
92    pub weight: E,
93    /// Time interval when this edge exists
94    pub interval: TimeInterval,
95}
96
97/// A temporal graph where edges have time intervals
98#[derive(Debug, Clone)]
99pub struct TemporalGraph<N: Node, E: EdgeWeight, Ix: IndexType = u32> {
100    /// All nodes in the graph
101    nodes: HashSet<N>,
102    /// Temporal edges sorted by time
103    edges: BTreeMap<TimeInstant, Vec<TemporalEdge<N, E>>>,
104    /// Node appearance times
105    node_intervals: HashMap<N, TimeInterval>,
106    /// Edge ID counter
107    edge_counter: usize,
108    /// Index type phantom
109    _phantom: std::marker::PhantomData<Ix>,
110}
111
112impl<N: Node + std::fmt::Debug, E: EdgeWeight, Ix: IndexType> Default for TemporalGraph<N, E, Ix> {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118impl<N: Node + std::fmt::Debug, E: EdgeWeight, Ix: IndexType> TemporalGraph<N, E, Ix> {
119    /// Create a new empty temporal graph
120    pub fn new() -> Self {
121        TemporalGraph {
122            nodes: HashSet::new(),
123            edges: BTreeMap::new(),
124            node_intervals: HashMap::new(),
125            edge_counter: 0,
126            _phantom: std::marker::PhantomData,
127        }
128    }
129
130    /// Add a node with a time interval when it exists
131    pub fn add_node(&mut self, node: N, interval: TimeInterval) {
132        self.nodes.insert(node.clone());
133        self.node_intervals.insert(node, interval);
134    }
135
136    /// Add a temporal edge
137    pub fn add_edge(
138        &mut self,
139        source: N,
140        target: N,
141        weight: E,
142        interval: TimeInterval,
143    ) -> Result<usize> {
144        // Ensure nodes exist and are active during the edge interval
145        if let Some(source_interval) = self.node_intervals.get(&source) {
146            if source_interval.intersection(&interval).is_none() {
147                return Err(GraphError::InvalidGraph(
148                    "Source node not active during edge interval".to_string(),
149                ));
150            }
151        } else {
152            // Add node with the edge interval
153            self.add_node(source.clone(), interval);
154        }
155
156        if let Some(target_interval) = self.node_intervals.get(&target) {
157            if target_interval.intersection(&interval).is_none() {
158                return Err(GraphError::InvalidGraph(
159                    "Target node not active during edge interval".to_string(),
160                ));
161            }
162        } else {
163            // Add node with the edge interval
164            self.add_node(target.clone(), interval);
165        }
166
167        let edge = TemporalEdge {
168            source,
169            target,
170            weight,
171            interval,
172        };
173
174        // Add edge to the start time
175        self.edges.entry(interval.start).or_default().push(edge);
176
177        let edge_id = self.edge_counter;
178        self.edge_counter += 1;
179        Ok(edge_id)
180    }
181
182    /// Get a snapshot of the graph at a specific time
183    pub fn snapshot_at(&self, time: TimeInstant) -> Graph<N, E, Ix>
184    where
185        N: Clone,
186        E: Clone,
187    {
188        let mut snapshot = Graph::new();
189
190        // Add active nodes
191        for (node, interval) in &self.node_intervals {
192            if interval.contains(time) {
193                snapshot.add_node(node.clone());
194            }
195        }
196
197        // Add active edges
198        for edges in self.edges.values() {
199            for edge in edges {
200                if edge.interval.contains(time) {
201                    snapshot
202                        .add_edge(
203                            edge.source.clone(),
204                            edge.target.clone(),
205                            edge.weight.clone(),
206                        )
207                        .unwrap();
208                }
209            }
210        }
211
212        snapshot
213    }
214
215    /// Get all edges active during a time interval
216    pub fn edges_in_interval(&self, interval: TimeInterval) -> Vec<&TemporalEdge<N, E>> {
217        let mut result = Vec::new();
218
219        for edges in self.edges.values() {
220            for edge in edges {
221                if edge.interval.overlaps(&interval) {
222                    result.push(edge);
223                }
224            }
225        }
226
227        result
228    }
229
230    /// Get all time instants when the graph structure changes
231    pub fn change_times(&self) -> Vec<TimeInstant> {
232        let mut times = HashSet::new();
233
234        // Add node start and end times
235        for interval in self.node_intervals.values() {
236            times.insert(interval.start);
237            times.insert(interval.end);
238        }
239
240        // Add edge start and end times
241        for edges in self.edges.values() {
242            for edge in edges {
243                times.insert(edge.interval.start);
244                times.insert(edge.interval.end);
245            }
246        }
247
248        let mut times: Vec<_> = times.into_iter().collect();
249        times.sort();
250        times
251    }
252
253    /// Get the time interval when the graph is active
254    pub fn active_interval(&self) -> Option<TimeInterval> {
255        if self.nodes.is_empty() {
256            return None;
257        }
258
259        let change_times = self.change_times();
260        if change_times.len() < 2 {
261            return None;
262        }
263
264        let start = change_times[0];
265        let end = change_times[change_times.len() - 1];
266
267        TimeInterval::new(start.time, end.time).ok()
268    }
269
270    /// Count nodes active at a specific time
271    pub fn node_count_at(&self, time: TimeInstant) -> usize {
272        self.node_intervals
273            .values()
274            .filter(|interval| interval.contains(time))
275            .count()
276    }
277
278    /// Count edges active at a specific time
279    pub fn edge_count_at(&self, time: TimeInstant) -> usize {
280        let mut count = 0;
281        for edges in self.edges.values() {
282            for edge in edges {
283                if edge.interval.contains(time) {
284                    count += 1;
285                }
286            }
287        }
288        count
289    }
290
291    /// Get all nodes in the temporal graph
292    pub fn nodes(&self) -> impl Iterator<Item = &N> {
293        self.nodes.iter()
294    }
295
296    /// Get all temporal edges
297    pub fn temporal_edges(&self) -> Vec<&TemporalEdge<N, E>> {
298        let mut result = Vec::new();
299        for edges in self.edges.values() {
300            result.extend(edges.iter());
301        }
302        result
303    }
304
305    /// Check if two nodes are connected at a specific time
306    pub fn are_connected_at(&self, node1: &N, node2: &N, time: TimeInstant) -> bool {
307        for edges in self.edges.values() {
308            for edge in edges {
309                if edge.interval.contains(time)
310                    && ((edge.source == *node1 && edge.target == *node2)
311                        || (edge.source == *node2 && edge.target == *node1))
312                {
313                    return true;
314                }
315            }
316        }
317        false
318    }
319
320    /// Find temporal paths between two nodes
321    pub fn temporal_paths(
322        &self,
323        source: &N,
324        target: &N,
325        start_time: TimeInstant,
326        max_duration: u64,
327    ) -> Vec<TemporalPath<N, E>>
328    where
329        N: Clone + Ord,
330        E: Clone,
331    {
332        let mut paths = Vec::new();
333        let end_time = TimeInstant::new(start_time.time + max_duration);
334
335        // Use BFS to find temporal paths
336        let mut queue = std::collections::VecDeque::new();
337        queue.push_back(TemporalPath {
338            nodes: vec![source.clone()],
339            edges: Vec::new(),
340            total_weight: None,
341            start_time,
342            end_time: start_time,
343        });
344
345        while let Some(current_path) = queue.pop_front() {
346            let current_node = current_path.nodes.last().unwrap();
347
348            if current_node == target {
349                paths.push(current_path);
350                continue;
351            }
352
353            if current_path.end_time >= end_time {
354                continue;
355            }
356
357            // Find next possible edges
358            for edges in self.edges.values() {
359                for edge in edges {
360                    if edge.source == *current_node
361                        && edge.interval.start >= current_path.end_time
362                        && edge.interval.start <= end_time
363                        && !current_path.nodes.contains(&edge.target)
364                    {
365                        let mut new_path = current_path.clone();
366                        new_path.nodes.push(edge.target.clone());
367                        new_path.edges.push(edge.clone());
368                        new_path.end_time = edge.interval.end;
369
370                        queue.push_back(new_path);
371                    }
372                }
373            }
374        }
375
376        paths
377    }
378}
379
380/// Represents a path in a temporal graph
381#[derive(Debug, Clone)]
382pub struct TemporalPath<N: Node, E: EdgeWeight> {
383    /// Nodes in the path
384    pub nodes: Vec<N>,
385    /// Edges in the path
386    pub edges: Vec<TemporalEdge<N, E>>,
387    /// Total weight of the path
388    pub total_weight: Option<E>,
389    /// Start time of the path
390    pub start_time: TimeInstant,
391    /// End time of the path
392    pub end_time: TimeInstant,
393}
394
395impl<N: Node, E: EdgeWeight> TemporalPath<N, E> {
396    /// Get the duration of this path
397    pub fn duration(&self) -> u64 {
398        self.end_time.time - self.start_time.time
399    }
400
401    /// Get the number of hops in this path
402    pub fn hop_count(&self) -> usize {
403        self.edges.len()
404    }
405}
406
407/// Compute reachability in a temporal graph
408///
409/// Returns all nodes reachable from a source node within a time window.
410#[allow(dead_code)]
411pub fn temporal_reachability<N, E, Ix>(
412    temporal_graph: &TemporalGraph<N, E, Ix>,
413    source: &N,
414    start_time: TimeInstant,
415    max_duration: u64,
416) -> HashSet<N>
417where
418    N: Node + Clone + Ord + std::fmt::Debug,
419    E: EdgeWeight + Clone,
420    Ix: IndexType,
421{
422    let mut reachable = HashSet::new();
423    let mut visited = HashSet::new();
424    let mut queue = std::collections::VecDeque::new();
425
426    queue.push_back((source.clone(), start_time));
427    visited.insert((source.clone(), start_time));
428    reachable.insert(source.clone());
429
430    let end_time = TimeInstant::new(start_time.time + max_duration);
431
432    while let Some((current_node, current_time)) = queue.pop_front() {
433        if current_time >= end_time {
434            continue;
435        }
436
437        // Find outgoing edges from current node at or after current _time
438        for edges in temporal_graph.edges.values() {
439            for edge in edges {
440                if edge.source == current_node
441                    && edge.interval.start >= current_time
442                    && edge.interval.start <= end_time
443                {
444                    let next_time = edge.interval.end;
445                    let next_node = edge.target.clone();
446
447                    if !visited.contains(&(next_node.clone(), next_time)) {
448                        visited.insert((next_node.clone(), next_time));
449                        reachable.insert(next_node.clone());
450                        queue.push_back((next_node, next_time));
451                    }
452                }
453            }
454        }
455    }
456
457    reachable
458}
459
460/// Compute temporal centrality measures
461#[allow(dead_code)]
462pub fn temporal_betweenness_centrality<N, E, Ix>(
463    temporal_graph: &TemporalGraph<N, E, Ix>,
464    time_window: TimeInterval,
465) -> HashMap<N, f64>
466where
467    N: Node + Clone + Ord + std::fmt::Debug,
468    E: EdgeWeight + Clone,
469    Ix: IndexType,
470{
471    let mut centrality = HashMap::new();
472
473    // Initialize centrality for all nodes
474    for node in temporal_graph.nodes() {
475        centrality.insert(node.clone(), 0.0);
476    }
477
478    let nodes: Vec<N> = temporal_graph.nodes().cloned().collect();
479
480    // For each pair of nodes, find temporal paths and count betweenness
481    for i in 0..nodes.len() {
482        for j in (i + 1)..nodes.len() {
483            let source = &nodes[i];
484            let target = &nodes[j];
485
486            let paths = temporal_graph.temporal_paths(
487                source,
488                target,
489                time_window.start,
490                time_window.duration(),
491            );
492
493            if !paths.is_empty() {
494                // Count how many paths go through each intermediate node
495                for path in &paths {
496                    for k in 1..(path.nodes.len() - 1) {
497                        let intermediate = &path.nodes[k];
498                        *centrality.get_mut(intermediate).unwrap() += 1.0 / paths.len() as f64;
499                    }
500                }
501            }
502        }
503    }
504
505    centrality
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511
512    #[test]
513    fn test_time_instant() {
514        let t1 = TimeInstant::new(100);
515        let t2 = TimeInstant::new(200);
516
517        assert!(t1 < t2);
518        assert_eq!(t1.value(), 100);
519        assert_eq!(t2.value(), 200);
520    }
521
522    #[test]
523    fn test_time_interval() {
524        let interval = TimeInterval::new(100, 200).unwrap();
525
526        assert_eq!(interval.duration(), 100);
527        assert!(interval.contains(TimeInstant::new(150)));
528        assert!(!interval.contains(TimeInstant::new(50)));
529        assert!(!interval.contains(TimeInstant::new(200))); // End is exclusive
530
531        // Test invalid interval
532        assert!(TimeInterval::new(200, 100).is_err());
533    }
534
535    #[test]
536    fn test_interval_overlap() {
537        let interval1 = TimeInterval::new(100, 200).unwrap();
538        let interval2 = TimeInterval::new(150, 250).unwrap();
539        let interval3 = TimeInterval::new(300, 400).unwrap();
540
541        assert!(interval1.overlaps(&interval2));
542        assert!(!interval1.overlaps(&interval3));
543
544        let intersection = interval1.intersection(&interval2).unwrap();
545        assert_eq!(intersection.start.time, 150);
546        assert_eq!(intersection.end.time, 200);
547
548        assert!(interval1.intersection(&interval3).is_none());
549    }
550
551    #[test]
552    fn test_temporal_graph_creation() {
553        let mut tgraph: TemporalGraph<&str, f64> = TemporalGraph::new();
554
555        let interval1 = TimeInterval::new(0, 100).unwrap();
556        let interval2 = TimeInterval::new(50, 150).unwrap();
557
558        tgraph.add_node("A", interval1);
559        tgraph.add_node("B", interval2);
560
561        let edge_interval = TimeInterval::new(60, 90).unwrap();
562        tgraph.add_edge("A", "B", 1.0, edge_interval).unwrap();
563
564        // Test snapshot at different times
565        let snapshot_at_70 = tgraph.snapshot_at(TimeInstant::new(70));
566        assert_eq!(snapshot_at_70.node_count(), 2);
567        assert_eq!(snapshot_at_70.edge_count(), 1);
568
569        let snapshot_at_120 = tgraph.snapshot_at(TimeInstant::new(120));
570        assert_eq!(snapshot_at_120.node_count(), 1); // Only B is active
571        assert_eq!(snapshot_at_120.edge_count(), 0);
572    }
573
574    #[test]
575    fn test_temporal_connectivity() {
576        let mut tgraph: TemporalGraph<i32, f64> = TemporalGraph::new();
577
578        let node_interval = TimeInterval::new(0, 200).unwrap();
579        tgraph.add_node(1, node_interval);
580        tgraph.add_node(2, node_interval);
581
582        let edge_interval = TimeInterval::new(50, 150).unwrap();
583        tgraph.add_edge(1, 2, 1.0, edge_interval).unwrap();
584
585        // Test connectivity at different times
586        assert!(!tgraph.are_connected_at(&1, &2, TimeInstant::new(30)));
587        assert!(tgraph.are_connected_at(&1, &2, TimeInstant::new(100)));
588        assert!(!tgraph.are_connected_at(&1, &2, TimeInstant::new(170)));
589    }
590
591    #[test]
592    fn test_change_times() {
593        let mut tgraph: TemporalGraph<&str, f64> = TemporalGraph::new();
594
595        let interval1 = TimeInterval::new(0, 100).unwrap();
596        let interval2 = TimeInterval::new(50, 150).unwrap();
597
598        tgraph.add_node("A", interval1);
599        tgraph.add_edge("A", "B", 1.0, interval2).unwrap();
600
601        let change_times = tgraph.change_times();
602        let times: Vec<u64> = change_times.iter().map(|t| t.time).collect();
603
604        assert!(times.contains(&0));
605        assert!(times.contains(&50));
606        assert!(times.contains(&100));
607        assert!(times.contains(&150));
608    }
609
610    #[test]
611    fn test_temporal_reachability() {
612        let mut tgraph: TemporalGraph<i32, f64> = TemporalGraph::new();
613
614        let node_interval = TimeInterval::new(0, 300).unwrap();
615        for i in 1..=4 {
616            tgraph.add_node(i, node_interval);
617        }
618
619        // Create a temporal path: 1 -> 2 -> 3 -> 4
620        tgraph
621            .add_edge(1, 2, 1.0, TimeInterval::new(10, 50).unwrap())
622            .unwrap();
623        tgraph
624            .add_edge(2, 3, 1.0, TimeInterval::new(60, 100).unwrap())
625            .unwrap();
626        tgraph
627            .add_edge(3, 4, 1.0, TimeInterval::new(110, 150).unwrap())
628            .unwrap();
629
630        let reachable = temporal_reachability(&tgraph, &1, TimeInstant::new(0), 200);
631
632        assert!(reachable.contains(&1));
633        assert!(reachable.contains(&2));
634        assert!(reachable.contains(&3));
635        assert!(reachable.contains(&4));
636        assert_eq!(reachable.len(), 4);
637
638        // Test with limited time window
639        let reachable_limited = temporal_reachability(&tgraph, &1, TimeInstant::new(0), 80);
640        assert!(reachable_limited.contains(&1));
641        assert!(reachable_limited.contains(&2));
642        assert!(reachable_limited.contains(&3));
643        assert!(!reachable_limited.contains(&4)); // Can't reach 4 in time
644    }
645
646    #[test]
647    fn test_temporal_paths() {
648        let mut tgraph: TemporalGraph<&str, f64> = TemporalGraph::new();
649
650        let node_interval = TimeInterval::new(0, 200).unwrap();
651        for &node in &["A", "B", "C"] {
652            tgraph.add_node(node, node_interval);
653        }
654
655        // Direct path and indirect path
656        tgraph
657            .add_edge("A", "C", 1.0, TimeInterval::new(10, 50).unwrap())
658            .unwrap();
659        tgraph
660            .add_edge("A", "B", 1.0, TimeInterval::new(20, 60).unwrap())
661            .unwrap();
662        tgraph
663            .add_edge("B", "C", 1.0, TimeInterval::new(70, 110).unwrap())
664            .unwrap();
665
666        let paths = tgraph.temporal_paths(&"A", &"C", TimeInstant::new(0), 150);
667
668        assert!(!paths.is_empty());
669
670        // Should find both direct and indirect paths
671        let has_direct = paths.iter().any(|p| p.nodes.len() == 2);
672        let has_indirect = paths.iter().any(|p| p.nodes.len() == 3);
673
674        assert!(has_direct || has_indirect);
675    }
676
677    #[test]
678    fn test_edge_count_at_time() {
679        let mut tgraph: TemporalGraph<i32, f64> = TemporalGraph::new();
680
681        let node_interval = TimeInterval::new(0, 200).unwrap();
682        tgraph.add_node(1, node_interval);
683        tgraph.add_node(2, node_interval);
684        tgraph.add_node(3, node_interval);
685
686        tgraph
687            .add_edge(1, 2, 1.0, TimeInterval::new(10, 50).unwrap())
688            .unwrap();
689        tgraph
690            .add_edge(2, 3, 1.0, TimeInterval::new(30, 70).unwrap())
691            .unwrap();
692
693        assert_eq!(tgraph.edge_count_at(TimeInstant::new(5)), 0);
694        assert_eq!(tgraph.edge_count_at(TimeInstant::new(40)), 2);
695        assert_eq!(tgraph.edge_count_at(TimeInstant::new(60)), 1);
696        assert_eq!(tgraph.edge_count_at(TimeInstant::new(80)), 0);
697    }
698}