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                        .expect("Test: operation failed");
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().expect("Operation failed");
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).expect("Operation failed") +=
499                            1.0 / paths.len() as f64;
500                    }
501                }
502            }
503        }
504    }
505
506    centrality
507}
508
509#[cfg(test)]
510mod tests {
511    use super::*;
512
513    #[test]
514    fn test_time_instant() {
515        let t1 = TimeInstant::new(100);
516        let t2 = TimeInstant::new(200);
517
518        assert!(t1 < t2);
519        assert_eq!(t1.value(), 100);
520        assert_eq!(t2.value(), 200);
521    }
522
523    #[test]
524    fn test_time_interval() {
525        let interval = TimeInterval::new(100, 200).expect("Operation failed");
526
527        assert_eq!(interval.duration(), 100);
528        assert!(interval.contains(TimeInstant::new(150)));
529        assert!(!interval.contains(TimeInstant::new(50)));
530        assert!(!interval.contains(TimeInstant::new(200))); // End is exclusive
531
532        // Test invalid interval
533        assert!(TimeInterval::new(200, 100).is_err());
534    }
535
536    #[test]
537    fn test_interval_overlap() {
538        let interval1 = TimeInterval::new(100, 200).expect("Operation failed");
539        let interval2 = TimeInterval::new(150, 250).expect("Operation failed");
540        let interval3 = TimeInterval::new(300, 400).expect("Operation failed");
541
542        assert!(interval1.overlaps(&interval2));
543        assert!(!interval1.overlaps(&interval3));
544
545        let intersection = interval1
546            .intersection(&interval2)
547            .expect("Operation failed");
548        assert_eq!(intersection.start.time, 150);
549        assert_eq!(intersection.end.time, 200);
550
551        assert!(interval1.intersection(&interval3).is_none());
552    }
553
554    #[test]
555    fn test_temporal_graph_creation() {
556        let mut tgraph: TemporalGraph<&str, f64> = TemporalGraph::new();
557
558        let interval1 = TimeInterval::new(0, 100).expect("Operation failed");
559        let interval2 = TimeInterval::new(50, 150).expect("Operation failed");
560
561        tgraph.add_node("A", interval1);
562        tgraph.add_node("B", interval2);
563
564        let edge_interval = TimeInterval::new(60, 90).expect("Operation failed");
565        tgraph
566            .add_edge("A", "B", 1.0, edge_interval)
567            .expect("Operation failed");
568
569        // Test snapshot at different times
570        let snapshot_at_70 = tgraph.snapshot_at(TimeInstant::new(70));
571        assert_eq!(snapshot_at_70.node_count(), 2);
572        assert_eq!(snapshot_at_70.edge_count(), 1);
573
574        let snapshot_at_120 = tgraph.snapshot_at(TimeInstant::new(120));
575        assert_eq!(snapshot_at_120.node_count(), 1); // Only B is active
576        assert_eq!(snapshot_at_120.edge_count(), 0);
577    }
578
579    #[test]
580    fn test_temporal_connectivity() {
581        let mut tgraph: TemporalGraph<i32, f64> = TemporalGraph::new();
582
583        let node_interval = TimeInterval::new(0, 200).expect("Operation failed");
584        tgraph.add_node(1, node_interval);
585        tgraph.add_node(2, node_interval);
586
587        let edge_interval = TimeInterval::new(50, 150).expect("Operation failed");
588        tgraph
589            .add_edge(1, 2, 1.0, edge_interval)
590            .expect("Operation failed");
591
592        // Test connectivity at different times
593        assert!(!tgraph.are_connected_at(&1, &2, TimeInstant::new(30)));
594        assert!(tgraph.are_connected_at(&1, &2, TimeInstant::new(100)));
595        assert!(!tgraph.are_connected_at(&1, &2, TimeInstant::new(170)));
596    }
597
598    #[test]
599    fn test_change_times() {
600        let mut tgraph: TemporalGraph<&str, f64> = TemporalGraph::new();
601
602        let interval1 = TimeInterval::new(0, 100).expect("Operation failed");
603        let interval2 = TimeInterval::new(50, 150).expect("Operation failed");
604
605        tgraph.add_node("A", interval1);
606        tgraph
607            .add_edge("A", "B", 1.0, interval2)
608            .expect("Operation failed");
609
610        let change_times = tgraph.change_times();
611        let times: Vec<u64> = change_times.iter().map(|t| t.time).collect();
612
613        assert!(times.contains(&0));
614        assert!(times.contains(&50));
615        assert!(times.contains(&100));
616        assert!(times.contains(&150));
617    }
618
619    #[test]
620    fn test_temporal_reachability() {
621        let mut tgraph: TemporalGraph<i32, f64> = TemporalGraph::new();
622
623        let node_interval = TimeInterval::new(0, 300).expect("Operation failed");
624        for i in 1..=4 {
625            tgraph.add_node(i, node_interval);
626        }
627
628        // Create a temporal path: 1 -> 2 -> 3 -> 4
629        tgraph
630            .add_edge(
631                1,
632                2,
633                1.0,
634                TimeInterval::new(10, 50).expect("Operation failed"),
635            )
636            .expect("Test: operation failed");
637        tgraph
638            .add_edge(
639                2,
640                3,
641                1.0,
642                TimeInterval::new(60, 100).expect("Operation failed"),
643            )
644            .expect("Test: operation failed");
645        tgraph
646            .add_edge(
647                3,
648                4,
649                1.0,
650                TimeInterval::new(110, 150).expect("Operation failed"),
651            )
652            .expect("Test: operation failed");
653
654        let reachable = temporal_reachability(&tgraph, &1, TimeInstant::new(0), 200);
655
656        assert!(reachable.contains(&1));
657        assert!(reachable.contains(&2));
658        assert!(reachable.contains(&3));
659        assert!(reachable.contains(&4));
660        assert_eq!(reachable.len(), 4);
661
662        // Test with limited time window
663        let reachable_limited = temporal_reachability(&tgraph, &1, TimeInstant::new(0), 80);
664        assert!(reachable_limited.contains(&1));
665        assert!(reachable_limited.contains(&2));
666        assert!(reachable_limited.contains(&3));
667        assert!(!reachable_limited.contains(&4)); // Can't reach 4 in time
668    }
669
670    #[test]
671    fn test_temporal_paths() {
672        let mut tgraph: TemporalGraph<&str, f64> = TemporalGraph::new();
673
674        let node_interval = TimeInterval::new(0, 200).expect("Operation failed");
675        for &node in &["A", "B", "C"] {
676            tgraph.add_node(node, node_interval);
677        }
678
679        // Direct path and indirect path
680        tgraph
681            .add_edge(
682                "A",
683                "C",
684                1.0,
685                TimeInterval::new(10, 50).expect("Operation failed"),
686            )
687            .expect("Test: operation failed");
688        tgraph
689            .add_edge(
690                "A",
691                "B",
692                1.0,
693                TimeInterval::new(20, 60).expect("Operation failed"),
694            )
695            .expect("Test: operation failed");
696        tgraph
697            .add_edge(
698                "B",
699                "C",
700                1.0,
701                TimeInterval::new(70, 110).expect("Operation failed"),
702            )
703            .expect("Test: operation failed");
704
705        let paths = tgraph.temporal_paths(&"A", &"C", TimeInstant::new(0), 150);
706
707        assert!(!paths.is_empty());
708
709        // Should find both direct and indirect paths
710        let has_direct = paths.iter().any(|p| p.nodes.len() == 2);
711        let has_indirect = paths.iter().any(|p| p.nodes.len() == 3);
712
713        assert!(has_direct || has_indirect);
714    }
715
716    #[test]
717    fn test_edge_count_at_time() {
718        let mut tgraph: TemporalGraph<i32, f64> = TemporalGraph::new();
719
720        let node_interval = TimeInterval::new(0, 200).expect("Operation failed");
721        tgraph.add_node(1, node_interval);
722        tgraph.add_node(2, node_interval);
723        tgraph.add_node(3, node_interval);
724
725        tgraph
726            .add_edge(
727                1,
728                2,
729                1.0,
730                TimeInterval::new(10, 50).expect("Operation failed"),
731            )
732            .expect("Test: operation failed");
733        tgraph
734            .add_edge(
735                2,
736                3,
737                1.0,
738                TimeInterval::new(30, 70).expect("Operation failed"),
739            )
740            .expect("Test: operation failed");
741
742        assert_eq!(tgraph.edge_count_at(TimeInstant::new(5)), 0);
743        assert_eq!(tgraph.edge_count_at(TimeInstant::new(40)), 2);
744        assert_eq!(tgraph.edge_count_at(TimeInstant::new(60)), 1);
745        assert_eq!(tgraph.edge_count_at(TimeInstant::new(80)), 0);
746    }
747}