Skip to main content

hyperpaths_rs/
demand.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::hyperpath::{verbose, Strategy};
4use crate::transit_network::Link;
5
6/// Volumes holds the assigned demand according to the optimal strategy.
7pub struct Volumes {
8    /// Link volumes: links[from_node][to_node] = flow
9    pub links: HashMap<String, HashMap<String, f64>>,
10    /// Node volumes: accumulated flow through each node
11    pub nodes: HashMap<String, f64>,
12}
13
14pub fn assign_demand<'a>(
15    all_links: &'a [Link],
16    all_stops: &HashSet<String>,
17    optimal_strategy: &Strategy<'a>,
18    trips: &HashMap<String, HashMap<String, f64>>,
19    destination: &str,
20) -> Volumes {
21    // The attractive set is built in acceptance order, which is
22    // non-decreasing u_j + c_a (heap pops), so its reverse is exactly the
23    // paper's decreasing loading order - no sorting needed, as the paper
24    // notes on p. 97: the processing order of step 2.2 "is the inverse of
25    // the order used in part 1 of the algorithm". At zero-cost ties
26    // (no-wait chains produce exactly equal keys) reverse acceptance
27    // order also guarantees that a node's inflow links are loaded before
28    // its outflow links: a link (i, j) is accepted before the links into i
29    // are updated and popped.
30    let sorted = &optimal_strategy.a_set;
31
32    let mut node_volumes: HashMap<String, f64> = HashMap::with_capacity(all_stops.len());
33    for i in all_stops {
34        node_volumes.insert(i.clone(), 0.0);
35    }
36    for (origin, dests) in trips {
37        if let Some(&trips_num) = dests.get(destination) {
38            node_volumes.insert(origin.clone(), trips_num);
39            *node_volumes.entry(destination.to_string()).or_insert(0.0) += trips_num;
40        }
41    }
42    // Destination absorbs flow: negate so arrivals cancel it to zero.
43    *node_volumes.entry(destination.to_string()).or_insert(0.0) *= -1.0;
44
45    let mut v: HashMap<String, HashMap<String, f64>> = HashMap::new();
46    for a in all_links {
47        v.entry(a.from_node.clone())
48            .or_default()
49            .insert(a.to_node.clone(), 0.0);
50    }
51
52    for a in sorted.iter().rev() {
53        let f_i = optimal_strategy.freqs.get(&a.from_node).copied().unwrap_or(0.0);
54        let node_volume = node_volumes.get(&a.from_node).copied().unwrap_or(0.0);
55        let va = if f_i.is_infinite() {
56			// A no-wait basket holds exactly one link (the one that replaced it);
57            // per the paper's modified step 2.2 (p. 96) the link takes
58			// the whole node volume: v_a := V_i
59            node_volume
60        } else {
61            // A finite basket holds only boarding links (headway > 0)
62            let freq = 1.0 / a.headway;
63            (freq / f_i) * node_volume
64        };
65        if verbose() {
66            let to_volume = node_volumes.get(&a.to_node).copied().unwrap_or(0.0);
67            println!(
68                "Assigning demand for link: ({}, {}) \\\\ ",
69                a.from_node, a.to_node
70            );
71            println!(
72                "\\quad $v_{{({}, {})}} = {}$, $V_{{{}}} = {} + {}$ \\\\ ",
73                a.from_node, a.to_node, va, a.to_node, to_volume, va
74            );
75        }
76        v.entry(a.from_node.clone())
77            .or_default()
78            .insert(a.to_node.clone(), va);
79        *node_volumes.entry(a.to_node.clone()).or_insert(0.0) += va;
80    }
81    if verbose() {
82        println!("Final node volumes: \\\\");
83        for (k, volume) in &node_volumes {
84            println!("\\quad $V_{{{}}} = {}$ \\\\ ", k, volume);
85        }
86    }
87
88    Volumes {
89        links: v,
90        nodes: node_volumes,
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_zero_cost_chain_loading() {
100        // Regression for the loading order at exact zero-cost ties: the
101        // alighting link (B1 -> S2) and the walking link (S2 -> S3) both have
102        // key u_j + c_a = 4 and equal tail labels, so no sort comparator can
103        // recover the dependency between them. Reverse acceptance order must
104        // load the inflow of S2 before its outflow.
105        use crate::hyperpath::find_optimal_strategy;
106
107        let all_nodes: HashSet<String> = ["S1", "B0", "B1", "S2", "S3"]
108            .iter()
109            .map(|s| s.to_string())
110            .collect();
111        let all_links = vec![
112            // boarding: wait for the bus (headway 5), no riding yet
113            Link::new("S1", "B0", "Bus", 0.0, 5.0),
114            // on-board segment
115            Link::new("B0", "B1", "Bus", 10.0, 0.0),
116            // alighting, key u_S2 + 0 = 4
117            Link::new("B1", "S2", "Bus", 0.0, 0.0),
118            // walking to the destination, key u_S3 + 4 = 4
119            Link::new("S2", "S3", "Walk", 4.0, 0.0),
120        ];
121        let ops = find_optimal_strategy(&all_links, &all_nodes, "S3");
122        // 5 wait + 10 ride + 0 alight + 4 walk
123        assert!((ops.labels["S1"] - 19.0).abs() <= 1e-12);
124
125        let trips: HashMap<String, HashMap<String, f64>> = HashMap::from([(
126            "S1".to_string(),
127            HashMap::from([("S3".to_string(), 100.0)]),
128        )]);
129        let volumes = assign_demand(&all_links, &all_nodes, &ops, &trips, "S3");
130        assert!((volumes.links["S1"]["B0"] - 100.0).abs() <= 1e-12);
131        assert!((volumes.links["B0"]["B1"] - 100.0).abs() <= 1e-12);
132        assert!((volumes.links["B1"]["S2"] - 100.0).abs() <= 1e-12);
133        assert!((volumes.links["S2"]["S3"] - 100.0).abs() <= 1e-12);
134    }
135
136    #[test]
137    fn test_assign_demand() {
138        let all_nodes: HashSet<String> = ["A", "X", "X2", "Y", "Y3", "B"]
139            .iter()
140            .map(|s| s.to_string())
141            .collect();
142        let all_links = vec![
143            Link::new("A", "B", "Line 1", 25.0, 6.0),
144            Link::new("A", "X2", "Line 2", 7.0, 6.0),
145            Link::new("X2", "X", "Line 2", 0.0, 0.0),
146            Link::new("X", "X2", "Line 2", 0.0, 6.0),
147            Link::new("X2", "Y", "Line 2", 6.0, 0.0),
148            Link::new("Y3", "Y", "Line 3", 0.0, 15.0),
149            Link::new("Y", "B", "Line 4", 10.0, 3.0),
150            Link::new("X", "Y3", "Line 3", 4.0, 15.0),
151            Link::new("Y", "Y3", "Line 3", 0.0, 15.0),
152            Link::new("Y3", "B", "Line 3", 4.0, 0.0),
153        ];
154        let destination_node = "B";
155        let od_matrix: HashMap<String, HashMap<String, f64>> = HashMap::from([(
156            "A".to_string(),
157            HashMap::from([("B".to_string(), 1.0)]),
158        )]);
159        let optimal_strategy = Strategy {
160            labels: HashMap::from([
161                ("A".to_string(), 27.75),
162                ("X".to_string(), 19.071428571428573),
163                ("X2".to_string(), 17.5),
164                ("Y".to_string(), 11.5),
165                ("Y3".to_string(), 4.0),
166                ("B".to_string(), 0.0),
167            ]),
168            freqs: HashMap::from([
169                ("A".to_string(), 1.0 / 3.0),
170                ("X".to_string(), 7.0 / 30.0),
171                ("X2".to_string(), f64::INFINITY),
172                ("Y".to_string(), 0.4),
173                ("Y3".to_string(), f64::INFINITY),
174                ("B".to_string(), 0.0),
175            ]),
176            a_set: vec![
177                // Y3->B
178                &all_links[9],
179                // Y->Y3
180                &all_links[8],
181                // X->Y3
182                &all_links[7],
183                // Y->B
184                &all_links[6],
185                // X2->Y
186                &all_links[4],
187                // X->X2
188                &all_links[3],
189                // A->X2
190                &all_links[1],
191                // A->B
192                &all_links[0],
193            ],
194        };
195        let volumes = assign_demand(
196            &all_links,
197            &all_nodes,
198            &optimal_strategy,
199            &od_matrix,
200            destination_node,
201        );
202
203        let correct_links: HashMap<&str, HashMap<&str, f64>> = HashMap::from([
204            ("A", HashMap::from([("B", 0.5), ("X2", 0.5)])),
205            ("X2", HashMap::from([("X", 0.0), ("Y", 0.5)])),
206            ("X", HashMap::from([("X2", 0.0), ("Y3", 0.0)])),
207            (
208                "Y",
209                HashMap::from([("Y3", 1.0 / 12.0), ("B", 5.0 / 12.0)]),
210            ),
211            ("Y3", HashMap::from([("Y", 0.0), ("B", 1.0 / 12.0)])),
212        ]);
213        let correct_nodes: HashMap<&str, f64> = HashMap::from([
214            ("A", 1.0),
215            ("X2", 0.5),
216            ("X", 0.0),
217            ("Y3", 1.0 / 12.0),
218            ("Y", 0.5),
219            ("B", 0.0),
220        ]);
221
222        assert_eq!(
223            volumes.links.len(),
224            correct_links.len(),
225            "Incorrect number of links in volumes data"
226        );
227        assert_eq!(
228            volumes.nodes.len(),
229            correct_nodes.len(),
230            "Incorrect number of nodes in volumes data"
231        );
232
233        const EPS: f64 = 1e-9;
234        for (from_node, to_map) in &volumes.links {
235            assert!(
236                correct_links.contains_key(from_node.as_str()),
237                "No 'FromNode' in correct volumes data"
238            );
239            for (to_node, volume) in to_map {
240                assert!(
241                    correct_links[from_node.as_str()].contains_key(to_node.as_str()),
242                    "No 'ToNode' in correct volumes data"
243                );
244                let want = correct_links[from_node.as_str()][to_node.as_str()];
245                assert!(
246                    (volume - want).abs() <= EPS,
247                    "Incorrect volume in link ({}, {}): got {}, want {}",
248                    from_node,
249                    to_node,
250                    volume,
251                    want
252                );
253            }
254        }
255        for (node, node_volume) in &volumes.nodes {
256            assert!(
257                correct_nodes.contains_key(node.as_str()),
258                "No node in correct volumes data"
259            );
260            let want = correct_nodes[node.as_str()];
261            assert!(
262                (node_volume - want).abs() <= EPS,
263                "Incorrect volume in node {}: got {}, want {}",
264                node,
265                node_volume,
266                want
267            );
268        }
269    }
270}