Skip to main content

trailgen_core/
routing.rs

1use crate::{EdgeId, EdgeIndex, RoutingLaw, VertexId, WalkGraph};
2use std::{
3    cmp::Ordering,
4    collections::{BTreeSet, BinaryHeap},
5};
6
7/// Immutable graph-side indices shared by every realization against one
8/// corpus. Search scratch lives separately so a worker can reuse it without
9/// synchronization or per-leg maps.
10#[derive(Clone, Debug)]
11pub struct WalkRouter {
12    forbidden_turns: BTreeSet<(EdgeId, VertexId, EdgeId)>,
13}
14
15/// A materialized induced graph projection. Keeping its CSR separate prevents
16/// urban degree from taxing Finder at every expansion.
17#[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    #[test]
375    fn equal_cost_routing_is_reproducible() {
376        let drafts =
377            geojson::network_from_str(include_str!("../tests/fixtures/mini_network.geojson"))
378                .unwrap();
379        let graph = GraphBuilder::default().build(&drafts).unwrap();
380        let router = WalkRouter::forge(&graph);
381        let mut workspace = router.workspace(&graph);
382        let request = RouteRequest {
383            from: VertexId(0),
384            target: VertexId(graph.vertices.len() - 1),
385            previous: None,
386            law: RoutingLaw::default(),
387            cost_ceiling: None,
388            forbidden_edges: None,
389            forbidden_vertices: None,
390        };
391        let expected = router
392            .shortest_path(&graph, &mut workspace, request)
393            .unwrap();
394
395        for _ in 0..64 {
396            assert_eq!(
397                router.shortest_path(&graph, &mut workspace, request),
398                Some(expected.clone())
399            );
400        }
401    }
402
403    fn path_cost(graph: &WalkGraph, request: RouteRequest<'_>, path: &[EdgeId]) -> f64 {
404        path.iter()
405            .map(|edge| request.law.edge_cost(graph, *edge).unwrap())
406            .sum()
407    }
408
409    fn reference_cost(graph: &WalkGraph, request: RouteRequest<'_>) -> Option<f64> {
410        if request.from == request.target {
411            return Some(0.0);
412        }
413        let origin = (request.from, request.previous);
414        let mut labels = BTreeMap::<(VertexId, Option<EdgeId>), f64>::from([(origin, 0.0)]);
415        let mut settled = BTreeSet::new();
416        loop {
417            let (&state, &cost) = labels
418                .iter()
419                .filter(|(state, _)| !settled.contains(*state))
420                .min_by(|(left_state, left), (right_state, right)| {
421                    (**left)
422                        .total_cmp(right)
423                        .then_with(|| left_state.cmp(right_state))
424                })?;
425            settled.insert(state);
426            let (at, previous) = state;
427            if at == request.target {
428                return Some(cost);
429            }
430            for edge in graph.adjacency[at.0].iter().copied() {
431                if request
432                    .forbidden_edges
433                    .is_some_and(|forbidden| forbidden.contains(&edge))
434                    || !graph.turn_allowed(previous, at, edge)
435                {
436                    continue;
437                }
438                let Some(next) = graph.edges[edge.0].traverse(at) else {
439                    continue;
440                };
441                if next != request.target
442                    && request
443                        .forbidden_vertices
444                        .is_some_and(|forbidden| forbidden.contains(&next))
445                {
446                    continue;
447                }
448                let Some(step_cost) = request.law.edge_cost(graph, edge) else {
449                    continue;
450                };
451                let next_cost = cost + step_cost;
452                if request
453                    .cost_ceiling
454                    .is_some_and(|ceiling| next_cost > ceiling)
455                {
456                    continue;
457                }
458                let next_state = (next, Some(edge));
459                if labels
460                    .get(&next_state)
461                    .is_none_or(|known| next_cost < *known)
462                {
463                    labels.insert(next_state, next_cost);
464                }
465            }
466        }
467    }
468}