1use crate::{EdgeId, EdgeIndex, RoutingLaw, VertexId, WalkGraph};
2use std::{
3 cmp::Ordering,
4 collections::{BTreeSet, BinaryHeap},
5};
6
7#[derive(Clone, Debug)]
11pub struct WalkRouter {
12 forbidden_turns: BTreeSet<(EdgeId, VertexId, EdgeId)>,
13}
14
15#[derive(Clone)]
18pub struct WalkRealmIndex {
19 allowed: Vec<bool>,
20 adjacency: Vec<Vec<EdgeId>>,
21 edges: EdgeIndex,
22}
23
24impl WalkRealmIndex {
25 #[must_use]
26 pub fn finder(graph: &WalkGraph) -> Self {
27 Self::forge(
28 graph,
29 graph
30 .edges
31 .iter()
32 .map(|edge| edge.attr.realm.admitted_by_finder())
33 .collect(),
34 )
35 }
36
37 fn forge(graph: &WalkGraph, allowed: Vec<bool>) -> Self {
38 let mut adjacency = vec![Vec::new(); graph.vertices.len()];
39 for (vertex, fanout) in graph.adjacency.iter().enumerate() {
40 adjacency[vertex].extend(fanout.iter().copied().filter(|edge| allowed[edge.0]));
41 }
42 let edges = EdgeIndex::forge_allowed(graph, &allowed);
43 Self {
44 allowed,
45 adjacency,
46 edges,
47 }
48 }
49
50 #[must_use]
51 pub fn allowed(&self) -> &[bool] {
52 &self.allowed
53 }
54
55 #[must_use]
56 pub fn adjacency(&self) -> &[Vec<EdgeId>] {
57 &self.adjacency
58 }
59
60 #[must_use]
61 pub const fn edges(&self) -> &EdgeIndex {
62 &self.edges
63 }
64}
65
66impl WalkRouter {
67 #[must_use]
68 pub fn forge(graph: &WalkGraph) -> Self {
69 Self {
70 forbidden_turns: graph
71 .turn_bans
72 .iter()
73 .map(|ban| (ban.from, ban.via, ban.to))
74 .collect(),
75 }
76 }
77
78 #[must_use]
79 pub fn workspace(&self, graph: &WalkGraph) -> RoutingWorkspace {
80 RoutingWorkspace::forge(graph.edges.len())
81 }
82
83 #[must_use]
84 pub fn shortest_path(
85 &self,
86 graph: &WalkGraph,
87 workspace: &mut RoutingWorkspace,
88 request: RouteRequest<'_>,
89 ) -> Option<Vec<EdgeId>> {
90 if request.from == request.target {
91 return Some(Vec::new());
92 }
93 workspace.begin(graph.edges.len());
94 let origin = workspace.origin_slot();
95 workspace.set(origin, 0.0, None);
96 let mut frontier = BinaryHeap::from([Frontier {
97 estimate: heuristic(graph, request.from, request.target),
98 cost: 0.0,
99 slot: origin,
100 at: request.from,
101 previous: request.previous,
102 }]);
103
104 while let Some(here) = frontier.pop() {
105 if request
106 .cost_ceiling
107 .is_some_and(|ceiling| here.cost > ceiling)
108 || workspace
109 .cost(here.slot)
110 .is_none_or(|best| here.cost.total_cmp(&best).is_gt())
111 {
112 continue;
113 }
114 if here.at == request.target {
115 return workspace.reconstruct(origin, here.slot);
116 }
117 for edge_id in graph.adjacency[here.at.0].iter().copied() {
118 if request
119 .forbidden_edges
120 .is_some_and(|edges| edges.contains(&edge_id))
121 || !self.turn_allowed(here.previous, here.at, edge_id)
122 {
123 continue;
124 }
125 let Some(step_cost) = request.law.edge_cost(graph, edge_id) else {
126 continue;
127 };
128 let cost = here.cost + step_cost;
129 if request.cost_ceiling.is_some_and(|ceiling| cost > ceiling) {
130 continue;
131 }
132 let edge = &graph.edges[edge_id.0];
133 let Some(at) = edge.traverse(here.at) else {
134 continue;
135 };
136 if at != request.target
137 && request
138 .forbidden_vertices
139 .is_some_and(|vertices| vertices.contains(&at))
140 {
141 continue;
142 }
143 let slot = state_slot(edge_id, at, edge.a);
144 if workspace
145 .cost(slot)
146 .is_some_and(|best| !cost.total_cmp(&best).is_lt())
147 {
148 continue;
149 }
150 workspace.set(
151 slot,
152 cost,
153 Some(Predecessor {
154 slot: here.slot,
155 edge: edge_id,
156 }),
157 );
158 frontier.push(Frontier {
159 estimate: cost + heuristic(graph, at, request.target),
160 cost,
161 slot,
162 at,
163 previous: Some(edge_id),
164 });
165 }
166 }
167 None
168 }
169
170 fn turn_allowed(&self, from: Option<EdgeId>, via: VertexId, to: EdgeId) -> bool {
171 from.is_none_or(|from| !self.forbidden_turns.contains(&(from, via, to)))
172 }
173}
174
175#[derive(Clone, Copy)]
176pub struct RouteRequest<'a> {
177 pub from: VertexId,
178 pub target: VertexId,
179 pub previous: Option<EdgeId>,
180 pub law: RoutingLaw,
181 pub cost_ceiling: Option<f64>,
182 pub forbidden_edges: Option<&'a BTreeSet<EdgeId>>,
183 pub forbidden_vertices: Option<&'a BTreeSet<VertexId>>,
184}
185
186#[derive(Clone, Debug)]
187pub struct RoutingWorkspace {
188 generation: u32,
189 labels: Vec<Label>,
190}
191
192impl RoutingWorkspace {
193 fn forge(edge_count: usize) -> Self {
194 Self {
195 generation: 0,
196 labels: vec![Label::default(); edge_count.saturating_mul(2).saturating_add(1)],
197 }
198 }
199
200 fn begin(&mut self, edge_count: usize) {
201 let slots = edge_count.saturating_mul(2).saturating_add(1);
202 if self.labels.len() != slots {
203 self.labels.resize(slots, Label::default());
204 }
205 self.generation = self.generation.wrapping_add(1);
206 if self.generation == 0 {
207 self.labels.fill(Label::default());
208 self.generation = 1;
209 }
210 }
211
212 const fn origin_slot(&self) -> usize {
213 self.labels.len() - 1
214 }
215
216 fn cost(&self, slot: usize) -> Option<f64> {
217 let label = &self.labels[slot];
218 (label.generation == self.generation).then_some(label.cost)
219 }
220
221 fn predecessor(&self, slot: usize) -> Option<Predecessor> {
222 let label = &self.labels[slot];
223 (label.generation == self.generation)
224 .then_some(label.predecessor)
225 .flatten()
226 }
227
228 fn set(&mut self, slot: usize, cost: f64, predecessor: Option<Predecessor>) {
229 self.labels[slot] = Label {
230 generation: self.generation,
231 cost,
232 predecessor,
233 };
234 }
235
236 fn reconstruct(&self, origin: usize, mut cursor: usize) -> Option<Vec<EdgeId>> {
237 let mut edges = Vec::new();
238 while cursor != origin {
239 let predecessor = self.predecessor(cursor)?;
240 edges.push(predecessor.edge);
241 cursor = predecessor.slot;
242 }
243 edges.reverse();
244 Some(edges)
245 }
246}
247
248#[derive(Clone, Copy, Debug, Default)]
249struct Label {
250 generation: u32,
251 cost: f64,
252 predecessor: Option<Predecessor>,
253}
254
255#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
256struct Predecessor {
257 slot: usize,
258 edge: EdgeId,
259}
260
261#[derive(Clone, Copy, Debug, PartialEq)]
262struct Frontier {
263 estimate: f64,
264 cost: f64,
265 slot: usize,
266 at: VertexId,
267 previous: Option<EdgeId>,
268}
269
270impl Eq for Frontier {}
271
272impl Ord for Frontier {
273 fn cmp(&self, other: &Self) -> Ordering {
274 other
275 .estimate
276 .total_cmp(&self.estimate)
277 .then_with(|| other.cost.total_cmp(&self.cost))
278 .then_with(|| other.slot.cmp(&self.slot))
279 }
280}
281
282impl PartialOrd for Frontier {
283 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
284 Some(self.cmp(other))
285 }
286}
287
288fn state_slot(edge: EdgeId, at: VertexId, edge_a: VertexId) -> usize {
289 edge.0 * 2 + usize::from(at != edge_a)
290}
291
292fn heuristic(graph: &WalkGraph, from: VertexId, target: VertexId) -> f64 {
293 graph.vertices[from.0]
294 .coord
295 .haversine_m(graph.vertices[target.0].coord)
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use crate::{GraphBuilder, io::geojson};
302 use std::collections::BTreeMap;
303
304 #[test]
305 fn astar_matches_reference_dijkstra_across_vetoes() {
306 let drafts =
307 geojson::network_from_str(include_str!("../tests/fixtures/mini_network.geojson"))
308 .unwrap();
309 let graph = GraphBuilder::default().build(&drafts).unwrap();
310 let router = WalkRouter::forge(&graph);
311 let mut workspace = router.workspace(&graph);
312 let mut entropy = 0x9e37_79b9_7f4a_7c15_u64;
313
314 for trial in 0..256 {
315 entropy = entropy
316 .wrapping_mul(6_364_136_223_846_793_005)
317 .wrapping_add(1_442_695_040_888_963_407);
318 let from = VertexId(
319 usize::from(u16::try_from(entropy & u64::from(u16::MAX)).expect("masked to u16"))
320 % graph.vertices.len(),
321 );
322 let target = VertexId(
323 usize::from(
324 u16::try_from((entropy >> 16) & u64::from(u16::MAX)).expect("masked to u16"),
325 ) % graph.vertices.len(),
326 );
327 let law = RoutingLaw {
328 road_aversion: [0.0, 1.0, 3.0][trial % 3],
329 };
330 let forbidden_edges = graph
331 .edges
332 .iter()
333 .filter_map(|edge| {
334 let rotation = u32::try_from(edge.id.0 % 64).expect("rotation is below 64");
335 (entropy.rotate_left(rotation).trailing_zeros() >= 5).then_some(edge.id)
336 })
337 .collect::<BTreeSet<_>>();
338 let forbidden_vertices = graph
339 .vertices
340 .iter()
341 .filter_map(|vertex| {
342 (vertex.id != from
343 && vertex.id != target
344 && entropy
345 .rotate_right(
346 u32::try_from(vertex.id.0 % 64).expect("rotation is below 64"),
347 )
348 .trailing_zeros()
349 >= 6)
350 .then_some(vertex.id)
351 })
352 .collect::<BTreeSet<_>>();
353 let request = RouteRequest {
354 from,
355 target,
356 previous: None,
357 law,
358 cost_ceiling: (trial % 5 == 0).then_some(2_000.0),
359 forbidden_edges: Some(&forbidden_edges),
360 forbidden_vertices: Some(&forbidden_vertices),
361 };
362
363 let expected = reference_cost(&graph, request);
364 let actual = router
365 .shortest_path(&graph, &mut workspace, request)
366 .map(|path| path_cost(&graph, request, &path));
367 assert_eq!(actual.is_some(), expected.is_some(), "trial {trial}");
368 if let (Some(actual), Some(expected)) = (actual, expected) {
369 assert!((actual - expected).abs() < 1.0e-9, "trial {trial}");
370 }
371 }
372 }
373
374 fn path_cost(graph: &WalkGraph, request: RouteRequest<'_>, path: &[EdgeId]) -> f64 {
375 path.iter()
376 .map(|edge| request.law.edge_cost(graph, *edge).unwrap())
377 .sum()
378 }
379
380 fn reference_cost(graph: &WalkGraph, request: RouteRequest<'_>) -> Option<f64> {
381 if request.from == request.target {
382 return Some(0.0);
383 }
384 let origin = (request.from, request.previous);
385 let mut labels = BTreeMap::<(VertexId, Option<EdgeId>), f64>::from([(origin, 0.0)]);
386 let mut settled = BTreeSet::new();
387 loop {
388 let (&state, &cost) = labels
389 .iter()
390 .filter(|(state, _)| !settled.contains(*state))
391 .min_by(|(left_state, left), (right_state, right)| {
392 (**left)
393 .total_cmp(right)
394 .then_with(|| left_state.cmp(right_state))
395 })?;
396 settled.insert(state);
397 let (at, previous) = state;
398 if at == request.target {
399 return Some(cost);
400 }
401 for edge in graph.adjacency[at.0].iter().copied() {
402 if request
403 .forbidden_edges
404 .is_some_and(|forbidden| forbidden.contains(&edge))
405 || !graph.turn_allowed(previous, at, edge)
406 {
407 continue;
408 }
409 let Some(next) = graph.edges[edge.0].traverse(at) else {
410 continue;
411 };
412 if next != request.target
413 && request
414 .forbidden_vertices
415 .is_some_and(|forbidden| forbidden.contains(&next))
416 {
417 continue;
418 }
419 let Some(step_cost) = request.law.edge_cost(graph, edge) else {
420 continue;
421 };
422 let next_cost = cost + step_cost;
423 if request
424 .cost_ceiling
425 .is_some_and(|ceiling| next_cost > ceiling)
426 {
427 continue;
428 }
429 let next_state = (next, Some(edge));
430 if labels
431 .get(&next_state)
432 .is_none_or(|known| next_cost < *known)
433 {
434 labels.insert(next_state, next_cost);
435 }
436 }
437 }
438 }
439}