Skip to main content

hyperpaths_rs/
demand.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::hyperpath::{Strategy, verbose};
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
54            .freqs
55            .get(&a.from_node)
56            .copied()
57            .unwrap_or(0.0);
58        let node_volume = node_volumes.get(&a.from_node).copied().unwrap_or(0.0);
59        let va = if f_i.is_infinite() {
60            // A no-wait basket holds exactly one link (the one that replaced it);
61            // per the paper's modified step 2.2 (p. 96) the link takes
62            // the whole node volume: v_a := V_i
63            node_volume
64        } else {
65            // A finite basket holds only boarding links (headway > 0)
66            let freq = 1.0 / a.headway;
67            (freq / f_i) * node_volume
68        };
69        if verbose() {
70            let to_volume = node_volumes.get(&a.to_node).copied().unwrap_or(0.0);
71            println!(
72                "Assigning demand for link: ({}, {}) \\\\ ",
73                a.from_node, a.to_node
74            );
75            println!(
76                "\\quad $v_{{({}, {})}} = {}$, $V_{{{}}} = {} + {}$ \\\\ ",
77                a.from_node, a.to_node, va, a.to_node, to_volume, va
78            );
79        }
80        v.entry(a.from_node.clone())
81            .or_default()
82            .insert(a.to_node.clone(), va);
83        *node_volumes.entry(a.to_node.clone()).or_insert(0.0) += va;
84    }
85    if verbose() {
86        println!("Final node volumes: \\\\");
87        for (k, volume) in &node_volumes {
88            println!("\\quad $V_{{{}}} = {}$ \\\\ ", k, volume);
89        }
90    }
91
92    Volumes {
93        links: v,
94        nodes: node_volumes,
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_zero_cost_chain_loading() {
104        // Regression for the loading order at exact zero-cost ties: the
105        // alighting link (B1 -> S2) and the walking link (S2 -> S3) both have
106        // key u_j + c_a = 4 and equal tail labels, so no sort comparator can
107        // recover the dependency between them. Reverse acceptance order must
108        // load the inflow of S2 before its outflow.
109        use crate::hyperpath::find_optimal_strategy;
110
111        let all_nodes: HashSet<String> = ["S1", "B0", "B1", "S2", "S3"]
112            .iter()
113            .map(|s| s.to_string())
114            .collect();
115        let all_links = vec![
116            // boarding: wait for the bus (headway 5), no riding yet
117            Link::new("S1", "B0", "Bus", 0.0, 5.0),
118            // on-board segment
119            Link::new("B0", "B1", "Bus", 10.0, 0.0),
120            // alighting, key u_S2 + 0 = 4
121            Link::new("B1", "S2", "Bus", 0.0, 0.0),
122            // walking to the destination, key u_S3 + 4 = 4
123            Link::new("S2", "S3", "Walk", 4.0, 0.0),
124        ];
125        let ops = find_optimal_strategy(&all_links, &all_nodes, "S3");
126        // 5 wait + 10 ride + 0 alight + 4 walk
127        assert!((ops.labels["S1"] - 19.0).abs() <= 1e-12);
128
129        let trips: HashMap<String, HashMap<String, f64>> =
130            HashMap::from([("S1".to_string(), HashMap::from([("S3".to_string(), 100.0)]))]);
131        let volumes = assign_demand(&all_links, &all_nodes, &ops, &trips, "S3");
132        assert!((volumes.links["S1"]["B0"] - 100.0).abs() <= 1e-12);
133        assert!((volumes.links["B0"]["B1"] - 100.0).abs() <= 1e-12);
134        assert!((volumes.links["B1"]["S2"] - 100.0).abs() <= 1e-12);
135        assert!((volumes.links["S2"]["S3"] - 100.0).abs() <= 1e-12);
136    }
137
138    #[test]
139    fn test_board_alight_loop_conservation() {
140        // Regression for the strict acceptance test in step 1.3. Line F is
141        // useless onward from S2 (it rides only to the dead end S3), so its
142        // route node F2 gets its label through the alighting link:
143        // u_F2 = u_S2 + 0. The boarding link S2 -> F2 then has key exactly
144        // u_S2; the paper's nonstrict test (u_i >= u_j + c_a) would accept
145        // it, close the zero-cost cycle S2 -> F2 -> S2 and strand part of
146        // the volume in phase 2. Strict acceptance must keep line F out and
147        // deliver all 100 trips through line R.
148        use crate::hyperpath::find_optimal_strategy;
149
150        let all_nodes: HashSet<String> = ["S1", "S2", "S3", "R2", "F2"]
151            .iter()
152            .map(|s| s.to_string())
153            .collect();
154        let all_links = vec![
155            // line R: boarding at S2, riding to the destination S1
156            Link::new("S2", "R2", "R", 0.0, 6.0),
157            Link::new("R2", "S1", "R", 5.0, 0.0),
158            // line F: boarding at S2, riding only to the dead end S3
159            Link::new("S2", "F2", "F", 0.0, 6.0),
160            Link::new("F2", "S3", "F", 5.0, 0.0),
161            // alighting back at S2, sets u_F2 = u_S2
162            Link::new("F2", "S2", "F", 0.0, 0.0),
163        ];
164        let ops = find_optimal_strategy(&all_links, &all_nodes, "S1");
165        // 6 wait + 5 ride
166        assert!((ops.labels["S2"] - 11.0).abs() <= 1e-12);
167
168        let trips: HashMap<String, HashMap<String, f64>> =
169            HashMap::from([("S2".to_string(), HashMap::from([("S1".to_string(), 100.0)]))]);
170        let volumes = assign_demand(&all_links, &all_nodes, &ops, &trips, "S1");
171        assert!((volumes.links["S2"]["R2"] - 100.0).abs() <= 1e-12);
172        assert!((volumes.links["R2"]["S1"] - 100.0).abs() <= 1e-12);
173        // the useless line carries nothing
174        let board_f = volumes
175            .links
176            .get("S2")
177            .and_then(|m| m.get("F2"))
178            .copied()
179            .unwrap_or(0.0);
180        let alight_f = volumes
181            .links
182            .get("F2")
183            .and_then(|m| m.get("S2"))
184            .copied()
185            .unwrap_or(0.0);
186        assert!(board_f.abs() <= 1e-12);
187        assert!(alight_f.abs() <= 1e-12);
188    }
189
190    #[test]
191    fn test_assign_demand() {
192        let all_nodes: HashSet<String> = ["A", "X", "X2", "Y", "Y3", "B"]
193            .iter()
194            .map(|s| s.to_string())
195            .collect();
196        let all_links = vec![
197            Link::new("A", "B", "Line 1", 25.0, 6.0),
198            Link::new("A", "X2", "Line 2", 7.0, 6.0),
199            Link::new("X2", "X", "Line 2", 0.0, 0.0),
200            Link::new("X", "X2", "Line 2", 0.0, 6.0),
201            Link::new("X2", "Y", "Line 2", 6.0, 0.0),
202            Link::new("Y3", "Y", "Line 3", 0.0, 15.0),
203            Link::new("Y", "B", "Line 4", 10.0, 3.0),
204            Link::new("X", "Y3", "Line 3", 4.0, 15.0),
205            Link::new("Y", "Y3", "Line 3", 0.0, 15.0),
206            Link::new("Y3", "B", "Line 3", 4.0, 0.0),
207        ];
208        let destination_node = "B";
209        let od_matrix: HashMap<String, HashMap<String, f64>> =
210            HashMap::from([("A".to_string(), HashMap::from([("B".to_string(), 1.0)]))]);
211        let optimal_strategy = Strategy {
212            labels: HashMap::from([
213                ("A".to_string(), 27.75),
214                ("X".to_string(), 19.071428571428573),
215                ("X2".to_string(), 17.5),
216                ("Y".to_string(), 11.5),
217                ("Y3".to_string(), 4.0),
218                ("B".to_string(), 0.0),
219            ]),
220            freqs: HashMap::from([
221                ("A".to_string(), 1.0 / 3.0),
222                ("X".to_string(), 7.0 / 30.0),
223                ("X2".to_string(), f64::INFINITY),
224                ("Y".to_string(), 0.4),
225                ("Y3".to_string(), f64::INFINITY),
226                ("B".to_string(), 0.0),
227            ]),
228            a_set: vec![
229                // Y3->B
230                &all_links[9],
231                // Y->Y3
232                &all_links[8],
233                // X->Y3
234                &all_links[7],
235                // Y->B
236                &all_links[6],
237                // X2->Y
238                &all_links[4],
239                // X->X2
240                &all_links[3],
241                // A->X2
242                &all_links[1],
243                // A->B
244                &all_links[0],
245            ],
246        };
247        let volumes = assign_demand(
248            &all_links,
249            &all_nodes,
250            &optimal_strategy,
251            &od_matrix,
252            destination_node,
253        );
254
255        let correct_links: HashMap<&str, HashMap<&str, f64>> = HashMap::from([
256            ("A", HashMap::from([("B", 0.5), ("X2", 0.5)])),
257            ("X2", HashMap::from([("X", 0.0), ("Y", 0.5)])),
258            ("X", HashMap::from([("X2", 0.0), ("Y3", 0.0)])),
259            ("Y", HashMap::from([("Y3", 1.0 / 12.0), ("B", 5.0 / 12.0)])),
260            ("Y3", HashMap::from([("Y", 0.0), ("B", 1.0 / 12.0)])),
261        ]);
262        let correct_nodes: HashMap<&str, f64> = HashMap::from([
263            ("A", 1.0),
264            ("X2", 0.5),
265            ("X", 0.0),
266            ("Y3", 1.0 / 12.0),
267            ("Y", 0.5),
268            ("B", 0.0),
269        ]);
270
271        assert_eq!(
272            volumes.links.len(),
273            correct_links.len(),
274            "Incorrect number of links in volumes data"
275        );
276        assert_eq!(
277            volumes.nodes.len(),
278            correct_nodes.len(),
279            "Incorrect number of nodes in volumes data"
280        );
281
282        const EPS: f64 = 1e-9;
283        for (from_node, to_map) in &volumes.links {
284            assert!(
285                correct_links.contains_key(from_node.as_str()),
286                "No 'FromNode' in correct volumes data"
287            );
288            for (to_node, volume) in to_map {
289                assert!(
290                    correct_links[from_node.as_str()].contains_key(to_node.as_str()),
291                    "No 'ToNode' in correct volumes data"
292                );
293                let want = correct_links[from_node.as_str()][to_node.as_str()];
294                assert!(
295                    (volume - want).abs() <= EPS,
296                    "Incorrect volume in link ({}, {}): got {}, want {}",
297                    from_node,
298                    to_node,
299                    volume,
300                    want
301                );
302            }
303        }
304        for (node, node_volume) in &volumes.nodes {
305            assert!(
306                correct_nodes.contains_key(node.as_str()),
307                "No node in correct volumes data"
308            );
309            let want = correct_nodes[node.as_str()];
310            assert!(
311                (node_volume - want).abs() <= EPS,
312                "Incorrect volume in node {}: got {}, want {}",
313                node,
314                node_volume,
315                want
316            );
317        }
318    }
319}