Skip to main content

hyperpaths_rs/
hyperpath.rs

1//! Implementation of the Spiess-Florian algorithm for transit assignment.
2//! See the ref. at spiess_floarian.tex LaTeX file.
3
4use std::collections::{HashMap, HashSet};
5use std::sync::atomic::{AtomicBool, Ordering};
6
7use crate::hyperpath_queue::PriorityQueue;
8use crate::transit_network::Link;
9
10/// Strategy is the optimal strategy as defined in the Spiess-Florian algorithm.
11pub struct Strategy<'a> {
12    /// u_{i} - expected travel time from node i to destination
13    pub labels: HashMap<String, f64>,
14    /// f_{i} - combined frequency of attractive links at node i.
15    /// `f64::INFINITY` marks a node whose basket is a single no-wait link.
16    pub freqs: HashMap<String, f64>,
17    /// \overline{A} - attractive links forming the hyperpath
18    pub a_set: Vec<&'a Link>,
19}
20
21/// The waiting-time constant of the Spiess-Florian expected travel time:
22///   u_i = (1 + sum(f_a * (c_a + u_j))) / f_i
23/// When the first attractive link arrives at a node the sum is empty and
24/// the numerator starts from this constant.
25pub(crate) const ALPHA: f64 = 1.0;
26
27pub static VERBOSE: AtomicBool = AtomicBool::new(false);
28
29pub(crate) fn verbose() -> bool {
30    VERBOSE.load(Ordering::Relaxed)
31}
32
33pub fn find_optimal_strategy<'a>(
34    all_links: &'a [Link],
35    all_stops: &HashSet<String>,
36    destination: &str,
37) -> Strategy<'a> {
38    /* 1.1 Initialization */
39    if verbose() {
40        println!("1.1 Initialization \\\\");
41    }
42
43    // Integer arena: map every node name to a dense index once, so the hot
44    // loops below index slices instead of hashing strings on every access.
45    // all_stops is interned first (indices [0, n_stops)) so the returned
46    // labels/freqs keep exactly the all_stops key set; any link endpoint
47    // outside all_stops (out of contract) is appended after and left out.
48    let mut n_id: HashMap<&str, usize> = HashMap::with_capacity(all_stops.len());
49    let mut n_name: Vec<&str> = Vec::with_capacity(all_stops.len());
50    macro_rules! intern {
51        ($s:expr) => {{
52            let s: &str = $s;
53            match n_id.get(s) {
54                Some(&id) => id,
55                None => {
56                    let id = n_name.len();
57                    n_id.insert(s, id);
58                    n_name.push(s);
59                    id
60                }
61            }
62        }};
63    }
64
65    for stop in all_stops {
66        intern!(stop.as_str());
67    }
68    let n_stops = n_name.len();
69
70    let m = all_links.len();
71    let mut l_from = vec![0usize; m];
72    let mut l_to = vec![0usize; m];
73    let mut l_cost = vec![0.0f64; m];
74    let mut l_head = vec![0.0f64; m];
75    for (k, link) in all_links.iter().enumerate() {
76        l_from[k] = intern!(link.from_node.as_str());
77        l_to[k] = intern!(link.to_node.as_str());
78        l_cost[k] = link.travel_cost;
79        l_head[k] = link.headway;
80    }
81    let dest_id = intern!(destination);
82    let n = n_name.len();
83
84    // u_i and f_i as dense slices instead of HashMap<String, f64>.
85    let mut u = vec![f64::INFINITY; n];
86    let mut f = vec![0.0f64; n];
87    u[dest_id] = 0.0;
88    if verbose() {
89        for (id, name) in n_name.iter().enumerate() {
90            println!("$f_{{{}}} = 0$ \\\\ ", name);
91            if id == dest_id {
92                println!("$u_{{{}}} = 0$ \\\\ ", name);
93            } else {
94                println!("$u_{{{}}} = Infinity$ \\\\ ", name);
95            }
96        }
97    }
98
99    // overline_a holds accepted link indices in acceptance order; a no-wait
100    // link replaces a node's whole basket, and replaced slots become None and
101    // are compacted at the end. a_set_idx[node] are that node's positions.
102    let mut overline_a: Vec<Option<usize>> = Vec::with_capacity(m / 2);
103    let mut a_set_idx: Vec<Vec<usize>> = vec![Vec::new(); n];
104
105    // Adjacency by head node: the link indices whose to-node == node, so that
106    // when u[node] improves exactly those incoming links are re-keyed.
107    let mut adj_by_to: Vec<Vec<usize>> = vec![Vec::new(); n];
108    for k in 0..m {
109        adj_by_to[l_to[k]].push(k);
110    }
111
112    // One priority-queue entry per link, pushed in link order so the entry id
113    // equals the link index; this lets the update step reach a link's entry
114    // directly, with no scan.
115    let mut pq = PriorityQueue::with_capacity(m);
116    for k in 0..m {
117        pq.push(k, u[l_to[k]] + l_cost[k]);
118    }
119    pq.init();
120    if verbose() {
121        pq.print();
122    }
123    while pq.len() > 0 {
124        /* 1.2 Get next link */
125        if verbose() {
126            pq.print();
127        }
128        let entry_id = match pq.pop() {
129            Some(id) => id,
130            None => break,
131        };
132        let priority = pq.priority(entry_id);
133        if priority.is_infinite() && priority > 0.0 {
134            break;
135        }
136        let k = pq.link(entry_id);
137        let i = l_from[k];
138        let j = l_to[k];
139        let sum_uc = u[j] + l_cost[k];
140
141        /* 1.3 Update node label */
142        if verbose() {
143            println!("Process: $a = (i, j) = ({}, {})$, \\\\ ", n_name[i], n_name[j]);
144        }
145        // A node already served by a no-wait link is final: the no-wait
146        // link absorbs all flow (its share f_a/f_i is 1 in the limit),
147        // so no other link may enter the basket
148        if f[i].is_infinite() {
149            continue;
150        }
151        // Strict improvement test: a link is accepted only if it
152        // strictly improves the label. Step 1.3 of Spiess & Florian
153        // (1989) prints the nonstrict u_i >= u_j + c_a, but the two
154        // rules differ only at exact equality, where the update is a
155        // no-op (the combination formula returns u_i unchanged; for
156        // f_a = inf the basket is replaced at the same value): labels,
157        // expected travel times and every number published in the
158        // paper are identical either way. The strict form is what
159        // part 2 needs. Step 2.2 loads links "in reverse topological
160        // order (decreasing u_j + c_a)" (p. 94) and Proposition 4
161        // claims flow conservation "by construction" - both presume an
162        // acyclic strategy, which the nonstrict rule does not
163        // guarantee: in an expanded route graph a boarding link (cost
164        // 0) into a route node whose label came from its own alighting
165        // link (cost 0) has key exactly u_i, so >= admits a zero-cost
166        // stop -> node -> stop cycle and the one-pass loading strands
167        // the volume entering it (see
168        // test_board_alight_loop_conservation). Rejecting at equality
169        // keeps the strategy acyclic and stays optimal: for the
170        // rejected link mu_a = 0 satisfies dual feasibility (20) as an
171        // equality and complementary slackness (24) holds since
172        // v_a = 0, a degenerate optimum. The prose of p. 94 ("if this
173        // time is smaller than u_i, link a is included") describes
174        // exactly this strict rule. All step, equation and page
175        // references above are to the original paper, not to the
176        // spiess_floarian.tex excerpt in this repository.
177        if u[i] <= sum_uc {
178            continue;
179        }
180        if verbose() {
181            println!(
182                "\\quad $u_i \\leq u_j + c_a : {} \\leq {}$ - FALSE \\\\ ",
183                u[i], sum_uc
184            );
185        }
186        if l_head[k] <= 0.0 {
187            // No-wait link (infinite frequency): the modified step 1.3
188            // given by the paper on p. 96 - the exact limit of the label
189            // update formula as f_a -> inf. The link replaces the whole
190            // attractive basket:
191            //   u_i := u_j + c_a, f_i := inf, A_i := {a}
192            u[i] = sum_uc;
193            f[i] = f64::INFINITY;
194            for &idx in &a_set_idx[i] {
195                overline_a[idx] = None;
196            }
197            a_set_idx[i].clear();
198            overline_a.push(Some(k));
199            a_set_idx[i].push(overline_a.len() - 1);
200            if verbose() {
201                println!(
202                    "\\quad no-wait link: $u_i = u_j + c_a = {}$, $f_i = \\infty$, basket replaced by $({}, {})$ \\\\ ",
203                    sum_uc, n_name[i], n_name[j]
204                );
205            }
206        } else {
207            let freq = 1.0 / l_head[k];
208            if verbose() {
209                println!("\\quad $f_a = {}$ \\\\ ", freq);
210                println!("\\quad $u_j + c_a = {}$ \\\\ ", sum_uc);
211                println!("\\quad $u_i = {}$ \\\\ ", u[i]);
212            }
213            let new_u = if f[i] == 0.0 {
214                // First link in the basket: u_i = (1 + f_a*(u_j+c_a)) / f_a
215                (ALPHA + freq * sum_uc) / freq
216            } else {
217                (f[i] * u[i] + freq * sum_uc) / (f[i] + freq)
218            };
219            u[i] = new_u;
220            f[i] += freq;
221            overline_a.push(Some(k));
222            a_set_idx[i].push(overline_a.len() - 1);
223            if verbose() {
224                println!(
225                    "\\quad$u_i = \\frac{{f_i * u_i + f_a * (u_j + c_a)}}{{f_i + f_a}} = {}$, $f_i = {}$ \\\\ ",
226                    new_u, f[i]
227                );
228                println!(
229                    "\\quad $\\overline{{A}} = \\overline{{A}} \\cup {{({}, {})}}$ \\\\ ",
230                    n_name[i], n_name[j]
231                );
232            }
233        }
234
235        // u[i] improved: re-key exactly the links entering i. The entry id
236        // equals the link index, so update reaches each directly.
237        for &kk in &adj_by_to[i] {
238            pq.update(kk, u[i] + l_cost[kk]);
239        }
240        if verbose() {
241            println!("Node labels: \\\\");
242            for (id, name) in n_name.iter().enumerate() {
243                println!("${} -> (u_i, f_i) = ({}, {})$ \\\\ ", name, u[id], f[id]);
244            }
245        }
246    }
247
248    // Compact the attractive set: drop entries replaced by no-wait links.
249    // The append order is preserved, i.e. non-decreasing u_j + c_a.
250    let a_set: Vec<&'a Link> = overline_a
251        .into_iter()
252        .flatten()
253        .map(|k| &all_links[k])
254        .collect();
255
256    // Translate the arena labels/freqs back to the public string-keyed maps,
257    // for the all_stops key set only.
258    let mut labels: HashMap<String, f64> = HashMap::with_capacity(n_stops);
259    let mut freqs: HashMap<String, f64> = HashMap::with_capacity(n_stops);
260    for id in 0..n_stops {
261        labels.insert(n_name[id].to_string(), u[id]);
262        freqs.insert(n_name[id].to_string(), f[id]);
263    }
264
265    Strategy {
266        labels,
267        freqs,
268        a_set,
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_hyper_paths() {
278        VERBOSE.store(true, Ordering::Relaxed);
279        let all_nodes: HashSet<String> = ["A", "X", "X2", "Y", "Y3", "B"]
280            .iter()
281            .map(|s| s.to_string())
282            .collect();
283        let all_links = vec![
284            Link::new("A", "B", "Line 1", 25.0, 6.0),
285            Link::new("A", "X2", "Line 2", 7.0, 6.0),
286            Link::new("X2", "X", "Line 2", 0.0, 0.0),
287            Link::new("X", "X2", "Line 2", 0.0, 6.0),
288            Link::new("X2", "Y", "Line 2", 6.0, 0.0),
289            Link::new("Y3", "Y", "Line 3", 0.0, 15.0),
290            Link::new("Y", "B", "Line 4", 10.0, 3.0),
291            Link::new("X", "Y3", "Line 3", 4.0, 15.0),
292            Link::new("Y", "Y3", "Line 3", 0.0, 15.0),
293            Link::new("Y3", "B", "Line 3", 4.0, 0.0),
294        ];
295        let destination_node = "B";
296        let ops = find_optimal_strategy(&all_links, &all_nodes, destination_node);
297
298        const EPS: f64 = 1e-9;
299
300        // With exact no-wait handling the labels match the paper exactly:
301        // no big-M artifacts like 4.000000000000001
302        let expected_labels: HashMap<&str, f64> = HashMap::from([
303            ("A", 27.75),
304            ("X", 19.071428571428573),
305            ("X2", 17.5),
306            ("Y", 11.5),
307            ("Y3", 4.0),
308            ("B", 0.0),
309        ]);
310        // +Inf marks nodes whose basket is a single no-wait link
311        let expected_freqs: HashMap<&str, f64> = HashMap::from([
312            ("A", 1.0 / 3.0),
313            ("X", 7.0 / 30.0),
314            ("X2", f64::INFINITY),
315            ("Y", 0.4),
316            ("Y3", f64::INFINITY),
317            ("B", 0.0),
318        ]);
319        // Matches the paper order (Spiess & Florian 1989, p. 93-94)
320        let expected_a_set: Vec<&Link> = vec![
321            // Y3->B
322            &all_links[9],
323            // Y->Y3
324            &all_links[8],
325            // X->Y3
326            &all_links[7],
327            // Y->B
328            &all_links[6],
329            // X2->Y
330            &all_links[4],
331            // X->X2
332            &all_links[3],
333            // A->X2
334            &all_links[1],
335            // A->B
336            &all_links[0],
337        ];
338
339        assert_eq!(
340            ops.labels.len(),
341            expected_labels.len(),
342            "Incorrect number of labels"
343        );
344        assert_eq!(
345            ops.freqs.len(),
346            expected_freqs.len(),
347            "Incorrect number of frequencies"
348        );
349        assert_eq!(
350            ops.a_set.len(),
351            expected_a_set.len(),
352            "Incorrect number of links in attractive set"
353        );
354
355        for (k, v) in &ops.labels {
356            assert!(
357                expected_labels.contains_key(k.as_str()),
358                "Incorrect label key {} has met",
359                k
360            );
361            let want = expected_labels[k.as_str()];
362            assert!(
363                (v - want).abs() <= EPS,
364                "Incorrect label value for node {}: got {}, want {}",
365                k,
366                v,
367                want
368            );
369        }
370        for (k, v) in &ops.freqs {
371            assert!(
372                expected_freqs.contains_key(k.as_str()),
373                "Incorrect frequency key {} has met",
374                k
375            );
376            let want = expected_freqs[k.as_str()];
377            if want.is_infinite() {
378                assert!(
379                    v.is_infinite() && *v > 0.0,
380                    "Frequency for node {} must be +Inf, got {}",
381                    k,
382                    v
383                );
384            } else {
385                assert!(
386                    (v - want).abs() <= EPS,
387                    "Incorrect frequency value for node {}: got {}, want {}",
388                    k,
389                    v,
390                    want
391                );
392            }
393        }
394        for (i, v) in ops.a_set.iter().enumerate() {
395            println!("{:?} {:?}", v, expected_a_set[i]);
396            assert!(
397                std::ptr::eq(*v, expected_a_set[i]),
398                "Incorrect link in attractive set at index {}",
399                i
400            );
401        }
402    }
403
404    #[test]
405    fn test_no_wait_replaces_basket() {
406        // A boarding link enters the basket of I first (key 4), then a
407        // cheaper no-wait chain I->W->D (key 5 < current u_I = 10) must
408        // replace it entirely: exact label, infinite frequency, single link.
409        let all_nodes: HashSet<String> = ["I", "W", "D"].iter().map(|s| s.to_string()).collect();
410        let all_links = vec![
411            // boarding link, key u_D + 4 = 4, accepted first: u_I = 6 + 4 = 10
412            Link::new("I", "D", "Bus", 4.0, 6.0),
413            // no-wait walk, key u_W + 3 = 5, replaces the basket: u_I = 5
414            Link::new("I", "W", "Walk", 3.0, 0.0),
415            // no-wait walk, key 2
416            Link::new("W", "D", "Walk", 2.0, 0.0),
417        ];
418        let ops = find_optimal_strategy(&all_links, &all_nodes, "D");
419
420        assert!((ops.labels["I"] - 5.0).abs() <= 1e-12);
421        assert!((ops.labels["W"] - 2.0).abs() <= 1e-12);
422        assert!(ops.freqs["I"].is_infinite());
423        assert!(ops.freqs["W"].is_infinite());
424
425        // The replaced boarding link I->D must not remain attractive
426        assert_eq!(
427            ops.a_set.len(),
428            2,
429            "basket of I must hold only the no-wait link"
430        );
431        for link in &ops.a_set {
432            assert_eq!(
433                link.headway, 0.0,
434                "only no-wait links expected in the attractive set"
435            );
436        }
437    }
438}