1use std::collections::{HashMap, HashSet};
2
3use crate::hyperpath::{Strategy, verbose};
4use crate::transit_network::Link;
5
6pub struct Volumes {
8 pub links: HashMap<String, HashMap<String, f64>>,
10 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 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 *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 node_volume
64 } else {
65 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 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 Link::new("S1", "B0", "Bus", 0.0, 5.0),
118 Link::new("B0", "B1", "Bus", 10.0, 0.0),
120 Link::new("B1", "S2", "Bus", 0.0, 0.0),
122 Link::new("S2", "S3", "Walk", 4.0, 0.0),
124 ];
125 let ops = find_optimal_strategy(&all_links, &all_nodes, "S3");
126 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 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 Link::new("S2", "R2", "R", 0.0, 6.0),
157 Link::new("R2", "S1", "R", 5.0, 0.0),
158 Link::new("S2", "F2", "F", 0.0, 6.0),
160 Link::new("F2", "S3", "F", 5.0, 0.0),
161 Link::new("F2", "S2", "F", 0.0, 0.0),
163 ];
164 let ops = find_optimal_strategy(&all_links, &all_nodes, "S1");
165 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 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 &all_links[9],
231 &all_links[8],
233 &all_links[7],
235 &all_links[6],
237 &all_links[4],
239 &all_links[3],
241 &all_links[1],
243 &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}