Skip to main content

scirs2_graph/dynamic/
temporal_path.rs

1//! Temporal path finding: earliest arrival, latest departure, and fastest paths.
2//!
3//! Edges are represented as `(src, dst, departure_time, travel_time)` tuples so
4//! that the departure from a node can be no earlier than the current arrival time
5//! at that node (i.e. *time-respecting* paths).
6
7use std::cmp::Reverse;
8use std::collections::BinaryHeap;
9
10/// A temporal path: a sequence of time-stamped hops through a graph.
11#[derive(Debug, Clone)]
12pub struct TemporalPath {
13    /// Ordered list of node identifiers visited.
14    pub nodes: Vec<usize>,
15    /// Arrival time at each node.
16    pub times: Vec<f64>,
17    /// Departure time of each traversed edge.
18    pub edge_times: Vec<f64>,
19    /// Elapsed time between source departure and final arrival.
20    pub total_duration: f64,
21    /// Number of hops.
22    pub n_hops: usize,
23}
24
25impl TemporalPath {
26    /// Create a one-node path starting at `src` at `start_time`.
27    pub fn new(src: usize, start_time: f64) -> Self {
28        Self {
29            nodes: vec![src],
30            times: vec![start_time],
31            edge_times: Vec::new(),
32            total_duration: 0.0,
33            n_hops: 0,
34        }
35    }
36
37    /// Extend the path by one hop.
38    pub fn extend(&mut self, node: usize, edge_time: f64, arrival_time: f64) {
39        self.nodes.push(node);
40        self.times.push(arrival_time);
41        self.edge_times.push(edge_time);
42        let start = self.times.first().copied().unwrap_or(0.0);
43        self.total_duration = arrival_time - start;
44        self.n_hops += 1;
45    }
46
47    /// Final arrival time of this path.
48    pub fn arrival_time(&self) -> f64 {
49        self.times.last().copied().unwrap_or(0.0)
50    }
51}
52
53/// Fixed-point scale factor used when converting `f64` times to `i64` for the
54/// binary heap.  Values are rounded to nanosecond precision.
55const TIME_SCALE: f64 = 1_000_000_000.0;
56
57/// Dijkstra-style algorithms adapted for temporal (time-respecting) paths.
58pub struct TemporalDijkstra;
59
60impl TemporalDijkstra {
61    /// Find the **earliest-arrival** path from `src` to `dst` in a temporal graph.
62    ///
63    /// # Arguments
64    /// * `src`        – source node id
65    /// * `dst`        – destination node id
66    /// * `start_time` – earliest time the source may be departed
67    /// * `edges`      – temporal edge list: `(u, v, departure_time, travel_time)`
68    /// * `n_nodes`    – total number of nodes (node ids must be in `0..n_nodes`)
69    ///
70    /// Returns `None` when no time-respecting path exists.
71    pub fn earliest_arrival(
72        src: usize,
73        dst: usize,
74        start_time: f64,
75        edges: &[(usize, usize, f64, f64)],
76        n_nodes: usize,
77    ) -> Option<TemporalPath> {
78        let mut arrival = vec![f64::INFINITY; n_nodes];
79        arrival[src] = start_time;
80
81        // Build per-node adjacency lists sorted by departure time.
82        let mut adj: Vec<Vec<(usize, f64, f64)>> = vec![Vec::new(); n_nodes];
83        for &(u, v, dep, travel) in edges {
84            adj[u].push((v, dep, travel));
85        }
86        for list in &mut adj {
87            list.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
88        }
89
90        // Min-heap: (arrival_time_scaled, node)
91        let mut pq: BinaryHeap<Reverse<(i64, usize)>> = BinaryHeap::new();
92        pq.push(Reverse(((start_time * TIME_SCALE) as i64, src)));
93
94        // predecessor[v] = (predecessor_node, edge_departure_time)
95        let mut prev: Vec<Option<(usize, f64)>> = vec![None; n_nodes];
96
97        while let Some(Reverse((t_scaled, u))) = pq.pop() {
98            let t = t_scaled as f64 / TIME_SCALE;
99            if t > arrival[u] {
100                continue;
101            }
102            if u == dst {
103                break;
104            }
105
106            // Walk temporal edges departing at or after current arrival.
107            for &(v, dep, travel) in &adj[u] {
108                if dep >= t {
109                    let arrive = dep + travel;
110                    if arrive < arrival[v] {
111                        arrival[v] = arrive;
112                        prev[v] = Some((u, dep));
113                        pq.push(Reverse(((arrive * TIME_SCALE) as i64, v)));
114                    }
115                }
116            }
117        }
118
119        if arrival[dst].is_infinite() {
120            return None;
121        }
122
123        // Reconstruct node sequence by walking predecessors backwards.
124        let mut rev_nodes = vec![dst];
125        let mut cursor = dst;
126        while let Some((p, _)) = prev[cursor] {
127            rev_nodes.push(p);
128            cursor = p;
129            if cursor == src {
130                break;
131            }
132        }
133        rev_nodes.reverse();
134
135        // Build TemporalPath by replaying the chosen edges.
136        let mut path = TemporalPath::new(src, start_time);
137        let mut curr_arr = start_time;
138
139        for window in rev_nodes.windows(2) {
140            let (u, v) = (window[0], window[1]);
141            // Find the earliest-departing edge from u to v that we can catch.
142            let chosen = adj[u]
143                .iter()
144                .find(|&&(nv, dep, _)| nv == v && dep >= curr_arr);
145            if let Some(&(_, dep, travel)) = chosen {
146                path.extend(v, dep, dep + travel);
147                curr_arr = dep + travel;
148            }
149        }
150
151        Some(path)
152    }
153
154    /// Find the **latest-departure** path that still arrives at `dst` by `deadline`.
155    ///
156    /// Uses a time-reversed earliest-arrival search on a reversed graph.
157    pub fn latest_departure(
158        src: usize,
159        dst: usize,
160        deadline: f64,
161        edges: &[(usize, usize, f64, f64)],
162        n_nodes: usize,
163    ) -> Option<TemporalPath> {
164        // Reverse the graph: each edge (u→v, dep, travel) becomes (v→u, arr, travel)
165        // where arr = dep + travel is used as the "reversed departure" time, and we
166        // maximise this time (i.e. minimise negative time).
167        let reversed: Vec<(usize, usize, f64, f64)> = edges
168            .iter()
169            .map(|&(u, v, dep, travel)| (v, u, dep + travel, travel))
170            .collect();
171
172        // Run earliest-arrival on reversed graph from dst, treating the deadline as
173        // a "start time" in the reversed timeline.  We want paths where we depart
174        // as late as possible, which corresponds to the reversed departure time
175        // being as large as possible.  We invert time by using `deadline - time`.
176        let inverted: Vec<(usize, usize, f64, f64)> = reversed
177            .iter()
178            .map(|&(u, v, arr, travel)| (u, v, deadline - arr, travel))
179            .collect();
180
181        let path = Self::earliest_arrival(dst, src, 0.0, &inverted, n_nodes)?;
182
183        // Convert inverted times back to real times.
184        let real_times: Vec<f64> = path.times.iter().map(|&t| deadline - t).collect();
185        let real_edges: Vec<f64> = path.edge_times.iter().map(|&t| deadline - t).collect();
186
187        let mut result = TemporalPath::new(src, real_times.last().copied().unwrap_or(0.0));
188        // Nodes are src-order in original graph but we reconstructed from dst; reverse.
189        let mut rev_nodes = path.nodes.clone();
190        rev_nodes.reverse();
191        let mut rev_times = real_times;
192        rev_times.reverse();
193        let mut rev_edges = real_edges;
194        rev_edges.reverse();
195
196        result.nodes = rev_nodes;
197        result.times = rev_times;
198        result.edge_times = rev_edges;
199        result.n_hops = path.n_hops;
200        result.total_duration = path.total_duration;
201
202        Some(result)
203    }
204
205    /// Find the **fastest** temporal path (minimises total elapsed duration).
206    ///
207    /// The search extends the state space with the departure time so that we
208    /// track `(node, departure_time)` pairs and minimise `arrival - start_time`.
209    pub fn fastest_path(
210        src: usize,
211        dst: usize,
212        start_time: f64,
213        edges: &[(usize, usize, f64, f64)],
214        n_nodes: usize,
215    ) -> Option<TemporalPath> {
216        // State: (duration_scaled, node, actual_arrival_time_scaled)
217        // We minimise duration = arrival_time - start_time.
218        let mut adj: Vec<Vec<(usize, f64, f64)>> = vec![Vec::new(); n_nodes];
219        for &(u, v, dep, travel) in edges {
220            adj[u].push((v, dep, travel));
221        }
222        for list in &mut adj {
223            list.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
224        }
225
226        // best_duration[node] = shortest duration found so far to reach node
227        let mut best_dur = vec![f64::INFINITY; n_nodes];
228        best_dur[src] = 0.0;
229
230        // heap: (duration_scaled, node, arrival_time_scaled)
231        let mut pq: BinaryHeap<Reverse<(i64, usize, i64)>> = BinaryHeap::new();
232        pq.push(Reverse((0, src, (start_time * TIME_SCALE) as i64)));
233
234        let mut prev: Vec<Option<(usize, f64)>> = vec![None; n_nodes];
235
236        while let Some(Reverse((dur_scaled, u, arr_scaled))) = pq.pop() {
237            let dur = dur_scaled as f64 / TIME_SCALE;
238            let arr = arr_scaled as f64 / TIME_SCALE;
239            if dur > best_dur[u] {
240                continue;
241            }
242            if u == dst {
243                break;
244            }
245
246            for &(v, dep, travel) in &adj[u] {
247                if dep >= arr {
248                    let new_arr = dep + travel;
249                    let new_dur = new_arr - start_time;
250                    if new_dur < best_dur[v] {
251                        best_dur[v] = new_dur;
252                        prev[v] = Some((u, dep));
253                        pq.push(Reverse((
254                            (new_dur * TIME_SCALE) as i64,
255                            v,
256                            (new_arr * TIME_SCALE) as i64,
257                        )));
258                    }
259                }
260            }
261        }
262
263        if best_dur[dst].is_infinite() {
264            return None;
265        }
266
267        // Reconstruct path.
268        let mut rev_nodes = vec![dst];
269        let mut cursor = dst;
270        while let Some((p, _)) = prev[cursor] {
271            rev_nodes.push(p);
272            cursor = p;
273            if cursor == src {
274                break;
275            }
276        }
277        rev_nodes.reverse();
278
279        let mut path = TemporalPath::new(src, start_time);
280        let mut curr_arr = start_time;
281        for window in rev_nodes.windows(2) {
282            let (u, v) = (window[0], window[1]);
283            let chosen = adj[u]
284                .iter()
285                .find(|&&(nv, dep, _)| nv == v && dep >= curr_arr);
286            if let Some(&(_, dep, travel)) = chosen {
287                path.extend(v, dep, dep + travel);
288                curr_arr = dep + travel;
289            }
290        }
291
292        Some(path)
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    /// Build a small temporal graph:
301    ///
302    /// 0→1 at t=1 (travel 1)
303    /// 1→2 at t=3 (travel 1)
304    /// 0→2 at t=5 (travel 1)   ← direct but later
305    fn small_edges() -> Vec<(usize, usize, f64, f64)> {
306        vec![(0, 1, 1.0, 1.0), (1, 2, 3.0, 1.0), (0, 2, 5.0, 1.0)]
307    }
308
309    #[test]
310    fn test_earliest_arrival_two_hop() {
311        let edges = small_edges();
312        let path =
313            TemporalDijkstra::earliest_arrival(0, 2, 0.0, &edges, 3).expect("path should exist");
314        // Via 0→1 (arr=2) then 1→2 (arr=4) is earlier than 0→2 direct (arr=6)
315        assert_eq!(path.nodes, vec![0, 1, 2], "nodes={:?}", path.nodes);
316        assert!((path.arrival_time() - 4.0).abs() < 1e-9);
317    }
318
319    #[test]
320    fn test_earliest_arrival_direct_faster() {
321        // 0→2 direct at t=1 (arr=2) vs 0→1 at t=1 (arr=2) then 1→2 at t=5 (arr=6)
322        let edges = vec![(0, 1, 1.0, 1.0), (1, 2, 5.0, 1.0), (0, 2, 1.0, 1.0)];
323        let path =
324            TemporalDijkstra::earliest_arrival(0, 2, 0.0, &edges, 3).expect("path should exist");
325        assert_eq!(path.nodes, vec![0, 2], "expected direct path");
326        assert!((path.arrival_time() - 2.0).abs() < 1e-9);
327    }
328
329    #[test]
330    fn test_no_path_returns_none() {
331        // Edge only goes 0→1; destination is 2 with no incoming edge.
332        let edges = vec![(0, 1, 1.0, 1.0)];
333        let result = TemporalDijkstra::earliest_arrival(0, 2, 0.0, &edges, 3);
334        assert!(result.is_none());
335    }
336
337    #[test]
338    fn test_fastest_path() {
339        let edges = small_edges();
340        let path = TemporalDijkstra::fastest_path(0, 2, 0.0, &edges, 3).expect("path should exist");
341        // Duration via 0→1→2 = 4.0; via direct 0→2 = 6.0
342        assert!(
343            path.total_duration <= 4.0 + 1e-9,
344            "duration={}",
345            path.total_duration
346        );
347    }
348}