1use crate::constraints::LoopConstraints;
2use crate::model::{EdgeId, EdgeTravel, VertexId, WalkGraph};
3use crate::route::{Route, rank_routes};
4use crate::trail::RoutingLaw;
5use crate::{Coord, RouteShape};
6use serde::{Deserialize, Serialize};
7use std::cmp::Ordering;
8use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum SearchStage {
12 Preparing,
13 Exploring,
14 Ranking,
15}
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub struct SearchProgress {
19 pub stage: SearchStage,
20 pub explored: usize,
21 pub limit: usize,
22 pub candidates: usize,
23}
24
25pub trait SearchMonitor {
26 fn cancelled(&self) -> bool;
27 fn report(&self, progress: SearchProgress);
28 fn preview(&self, _routes: &[Route]) {}
29}
30
31impl SearchMonitor for () {
32 fn cancelled(&self) -> bool {
33 false
34 }
35
36 fn report(&self, _progress: SearchProgress) {}
37}
38
39#[derive(Clone, Copy)]
40pub struct SearchScope<'a> {
41 graph: &'a WalkGraph,
42 allowed: Option<&'a [bool]>,
43 adjacency: Option<&'a [Vec<EdgeId>]>,
44 edicts: Option<&'a EdgeEdicts>,
45 edge_count: usize,
46}
47
48impl<'a> SearchScope<'a> {
49 #[must_use]
50 pub const fn all(graph: &'a WalkGraph) -> Self {
51 Self {
52 graph,
53 allowed: None,
54 adjacency: None,
55 edicts: None,
56 edge_count: graph.edges.len(),
57 }
58 }
59
60 #[must_use]
61 pub fn restricted(graph: &'a WalkGraph, allowed: &'a [bool]) -> Self {
62 assert_eq!(
63 allowed.len(),
64 graph.edges.len(),
65 "search mask must cover every graph edge"
66 );
67 Self {
68 graph,
69 allowed: Some(allowed),
70 adjacency: None,
71 edicts: None,
72 edge_count: allowed.iter().filter(|allowed| **allowed).count(),
73 }
74 }
75
76 #[must_use]
77 pub fn projected(
78 graph: &'a WalkGraph,
79 allowed: &'a [bool],
80 adjacency: &'a [Vec<EdgeId>],
81 ) -> Self {
82 assert_eq!(allowed.len(), graph.edges.len());
83 assert_eq!(adjacency.len(), graph.vertices.len());
84 Self {
85 graph,
86 allowed: Some(allowed),
87 adjacency: Some(adjacency),
88 edicts: None,
89 edge_count: allowed.iter().filter(|allowed| **allowed).count(),
90 }
91 }
92
93 #[must_use]
94 pub fn obeying(mut self, edicts: &'a EdgeEdicts) -> Self {
95 self.edicts = Some(edicts);
96 self.edge_count = self
97 .graph
98 .edges
99 .iter()
100 .filter(|edge| self.allows(edge.id))
101 .count();
102 self
103 }
104
105 fn fanout(self, vertex: VertexId) -> Vec<EdgeId> {
106 self.adjacency.unwrap_or(&self.graph.adjacency)[vertex.0]
107 .iter()
108 .copied()
109 .filter(|edge| self.allows(*edge))
110 .collect()
111 }
112
113 fn allows(self, edge: EdgeId) -> bool {
114 self.allowed.is_none_or(|allowed| allowed[edge.0])
115 && self
116 .edicts
117 .is_none_or(|edicts| !edicts.forbidden.contains(&edge))
118 }
119}
120
121#[derive(Clone, Copy, Debug, Eq, PartialEq)]
122pub enum EdgeDisposition {
123 Free,
124 Required,
125 Forbidden,
126}
127
128#[derive(Clone, Debug, Default, Eq, PartialEq)]
129pub struct EdgeEdicts {
130 required: BTreeSet<EdgeId>,
131 forbidden: BTreeSet<EdgeId>,
132}
133
134impl EdgeEdicts {
135 #[must_use]
136 pub fn disposition(&self, edge: EdgeId) -> EdgeDisposition {
137 if self.required.contains(&edge) {
138 EdgeDisposition::Required
139 } else if self.forbidden.contains(&edge) {
140 EdgeDisposition::Forbidden
141 } else {
142 EdgeDisposition::Free
143 }
144 }
145
146 pub fn toggle_required(&mut self, edge: EdgeId) {
147 if !self.required.remove(&edge) {
148 self.forbidden.remove(&edge);
149 self.required.insert(edge);
150 }
151 }
152
153 pub fn toggle_forbidden(&mut self, edge: EdgeId) {
154 if !self.forbidden.remove(&edge) {
155 self.required.remove(&edge);
156 self.forbidden.insert(edge);
157 }
158 }
159
160 pub fn clear(&mut self) {
161 self.required.clear();
162 self.forbidden.clear();
163 }
164
165 #[must_use]
166 pub fn is_empty(&self) -> bool {
167 self.required.is_empty() && self.forbidden.is_empty()
168 }
169
170 #[must_use]
171 pub fn required_count(&self) -> usize {
172 self.required.len()
173 }
174
175 #[must_use]
176 pub fn forbidden_count(&self) -> usize {
177 self.forbidden.len()
178 }
179
180 pub fn required(&self) -> impl Iterator<Item = EdgeId> + '_ {
181 self.required.iter().copied()
182 }
183
184 pub fn forbidden(&self) -> impl Iterator<Item = EdgeId> + '_ {
185 self.forbidden.iter().copied()
186 }
187
188 #[must_use]
189 pub fn admits(&self, route: &Route) -> bool {
190 self.required.iter().all(|edge| route.edges.contains(edge))
191 && route
192 .edges
193 .iter()
194 .all(|edge| !self.forbidden.contains(edge))
195 }
196
197 pub fn validate(&self, graph: &WalkGraph) -> crate::Result<()> {
198 if let Some(edge) = self
199 .required
200 .iter()
201 .chain(&self.forbidden)
202 .find(|edge| edge.0 >= graph.edges.len())
203 {
204 return Err(crate::TrailgenError::InvalidData(format!(
205 "segment edict references missing edge {}",
206 edge.0
207 )));
208 }
209 Ok(())
210 }
211}
212
213#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
214pub struct SearchParams {
215 pub max_hops: usize,
216 pub max_frontier: usize,
217 pub keep: usize,
218 #[serde(default = "default_closure_paths")]
219 pub closure_paths: usize,
220 #[serde(default)]
221 pub seed: u64,
222 #[serde(default)]
223 pub routing: RoutingLaw,
224}
225
226const fn default_closure_paths() -> usize {
227 4
228}
229
230impl Default for SearchParams {
231 fn default() -> Self {
232 Self {
233 max_hops: 256,
234 max_frontier: 200_000,
235 keep: 12,
236 closure_paths: default_closure_paths(),
237 seed: 0,
238 routing: RoutingLaw::default(),
239 }
240 }
241}
242
243#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
244#[serde(rename_all = "kebab-case")]
245pub enum SolverKind {
246 #[default]
247 Auto,
248 Heuristic,
249 Exact,
250}
251
252impl SolverKind {
253 const AUTO_EXACT_EDGE_LIMIT: usize = 32;
254
255 #[must_use]
256 pub const fn resolve(self, graph: &WalkGraph) -> Self {
257 self.resolve_edge_count(graph.edges.len())
258 }
259
260 const fn resolve_edge_count(self, edge_count: usize) -> Self {
261 match self {
262 Self::Auto if edge_count <= Self::AUTO_EXACT_EDGE_LIMIT => Self::Exact,
263 Self::Auto => Self::Heuristic,
264 resolved => resolved,
265 }
266 }
267
268 #[must_use]
269 pub const fn label(self) -> &'static str {
270 match self {
271 Self::Auto => "auto",
272 Self::Heuristic => "loop-hunter",
273 Self::Exact => "exact-enumerator",
274 }
275 }
276
277 #[must_use]
278 pub fn solve(
279 self,
280 params: SearchParams,
281 graph: &WalkGraph,
282 start: VertexId,
283 constraints: &LoopConstraints,
284 count: usize,
285 ) -> Vec<Route> {
286 self.solve_monitored(params, graph, start, constraints, count, &())
287 }
288
289 #[must_use]
290 pub fn solve_monitored(
291 self,
292 params: SearchParams,
293 graph: &WalkGraph,
294 start: VertexId,
295 constraints: &LoopConstraints,
296 count: usize,
297 monitor: &dyn SearchMonitor,
298 ) -> Vec<Route> {
299 self.solve_scoped(
300 params,
301 SearchScope::all(graph),
302 start,
303 constraints,
304 count,
305 monitor,
306 )
307 }
308
309 #[must_use]
310 pub fn solve_scoped(
311 self,
312 params: SearchParams,
313 scope: SearchScope<'_>,
314 start: VertexId,
315 constraints: &LoopConstraints,
316 count: usize,
317 monitor: &dyn SearchMonitor,
318 ) -> Vec<Route> {
319 if constraints.allowed_shapes.as_slice() == [RouteShape::OutAndBack] {
320 return support_out_and_backs(params, scope, start, constraints, count, monitor);
321 }
322 match self.resolve_edge_count(scope.edge_count) {
323 Self::Auto => unreachable!("auto solver must resolve to a concrete backend"),
324 Self::Heuristic => {
325 LoopHunter { params }.solve_monitored(scope, start, constraints, count, monitor)
326 }
327 Self::Exact => ExactLoopSolver { params }.solve_monitored(
328 scope,
329 start,
330 constraints,
331 count,
332 monitor,
333 ),
334 }
335 }
336
337 #[must_use]
338 #[allow(clippy::too_many_arguments)]
339 pub fn revise_scoped(
340 self,
341 mut params: SearchParams,
342 scope: SearchScope<'_>,
343 start: VertexId,
344 constraints: &LoopConstraints,
345 count: usize,
346 edicts: &EdgeEdicts,
347 incumbents: &[Route],
348 monitor: &dyn SearchMonitor,
349 ) -> Vec<Route> {
350 if edicts.validate(scope.graph).is_err() {
351 return Vec::new();
352 }
353 let scope = scope.obeying(edicts);
354 let amplification = usize::from(edicts.required_count() == 0 && !incumbents.is_empty()) + 1;
355 let hunt = count
356 .max(1)
357 .saturating_mul(amplification)
358 .min(params.max_frontier.max(count));
359 params.keep = params.keep.max(hunt);
360 let mut routes = incumbents
361 .iter()
362 .filter(|route| {
363 route.start == start
364 && route.edges.iter().all(|edge| scope.allows(*edge))
365 && edicts.admits(route)
366 && scope.graph.walk_edges(start, &route.edges).is_some()
367 })
368 .map(|route| {
369 Route::from_edges(
370 route.name.clone(),
371 scope.graph,
372 start,
373 route.edges.clone(),
374 constraints,
375 )
376 })
377 .filter(|route| constraints.allows_shape(route.metrics.shape))
378 .collect::<Vec<_>>();
379 if !monitor.cancelled() {
380 if edicts.required_count() == 0 || !constraints.allows_shape(RouteShape::Loop) {
381 routes.extend(
382 self.solve_scoped(params, scope, start, constraints, hunt, monitor)
383 .into_iter()
384 .filter(|route| edicts.admits(route)),
385 );
386 } else {
387 routes.extend(support_loop_portfolio_obeying(
388 params,
389 scope,
390 start,
391 constraints,
392 hunt,
393 monitor,
394 edicts,
395 ));
396 }
397 }
398 finish_routes(
399 routes,
400 scope.graph,
401 constraints,
402 count,
403 params.keep,
404 monitor,
405 )
406 }
407}
408
409#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
410pub struct LoopHunter {
411 pub params: SearchParams,
412}
413
414#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
415pub struct ExactLoopSolver {
416 pub params: SearchParams,
417}
418
419#[derive(Clone)]
420struct State {
421 at: VertexId,
422 edges: Vec<EdgeId>,
423 used: BTreeSet<EdgeId>,
424 distance_m: f64,
425}
426
427struct SearchMeter<'a> {
428 monitor: &'a dyn SearchMonitor,
429 explored: usize,
430 limit: usize,
431 reported: usize,
432 previewed: usize,
433}
434
435impl<'a> SearchMeter<'a> {
436 const REPORT_STRIDE: usize = 32;
437
438 fn new(monitor: &'a dyn SearchMonitor, limit: usize) -> Self {
439 Self {
440 monitor,
441 explored: 0,
442 limit,
443 reported: 0,
444 previewed: 0,
445 }
446 }
447
448 fn advance(&mut self, routes: &[Route]) -> bool {
449 if self.explored >= self.limit || self.monitor.cancelled() {
450 return false;
451 }
452 if routes.len() > self.previewed {
453 self.previewed = routes.len();
454 self.monitor.preview(routes);
455 }
456 self.explored += 1;
457 if self.explored == 1 || self.explored - self.reported >= Self::REPORT_STRIDE {
458 self.emit(SearchStage::Exploring, routes.len());
459 }
460 true
461 }
462
463 fn finish(&mut self, routes: &[Route]) {
464 if routes.len() > self.previewed {
465 self.monitor.preview(routes);
466 }
467 self.emit(SearchStage::Ranking, routes.len());
468 }
469
470 fn emit(&mut self, stage: SearchStage, candidates: usize) {
471 self.reported = self.explored;
472 self.monitor.report(SearchProgress {
473 stage,
474 explored: self.explored,
475 limit: self.limit,
476 candidates,
477 });
478 }
479}
480
481pub trait RouteSolver {
482 fn solve(
483 &self,
484 graph: &WalkGraph,
485 start: VertexId,
486 constraints: &LoopConstraints,
487 count: usize,
488 ) -> Vec<Route>;
489}
490
491impl LoopHunter {
492 #[must_use]
493 pub fn hunt(
494 self,
495 graph: &WalkGraph,
496 start: VertexId,
497 constraints: &LoopConstraints,
498 count: usize,
499 ) -> Vec<Route> {
500 self.solve(graph, start, constraints, count)
501 }
502
503 fn solve_monitored(
504 &self,
505 scope: SearchScope<'_>,
506 start: VertexId,
507 constraints: &LoopConstraints,
508 count: usize,
509 monitor: &dyn SearchMonitor,
510 ) -> Vec<Route> {
511 if constraints.allowed_shapes.as_slice() == [RouteShape::Loop] {
512 return support_loop_portfolio(self.params, scope, start, constraints, count, monitor);
513 }
514 let graph = scope.graph;
515 let mut stack = vec![State {
516 at: start,
517 edges: Vec::new(),
518 used: BTreeSet::new(),
519 distance_m: 0.0,
520 }];
521 let mut routes = Vec::<Route>::new();
522 let mut meter = SearchMeter::new(monitor, self.params.max_frontier);
523 let Some(mut closer) = LoopCloser::forge(scope, start, self.params, constraints, monitor)
524 else {
525 return Vec::new();
526 };
527
528 while let Some(state) = stack.pop() {
529 if !meter.advance(&routes) {
530 break;
531 }
532 closer.strike(&state, constraints, &mut routes);
533 if state.edges.len() >= self.params.max_hops {
534 continue;
535 }
536 let mut fanout = scope.fanout(state.at);
537 sort_heuristic_fanout(
538 graph,
539 &mut fanout,
540 self.params.seed,
541 state.edges.len(),
542 state.at,
543 self.params.routing,
544 );
545
546 for edge_id in fanout {
547 if monitor.cancelled() {
548 return Vec::new();
549 }
550 if state.used.contains(&edge_id) {
551 continue;
552 }
553 if self.params.routing.edge_cost(graph, edge_id).is_none() {
554 continue;
555 }
556 if !graph.turn_allowed(state.edges.last().copied(), state.at, edge_id) {
557 continue;
558 }
559 let edge = &graph.edges[edge_id.0];
560 let Some(next) = edge.traverse(state.at) else {
561 continue;
562 };
563 let distance_m = state.distance_m + edge.attr.length_m;
564 if distance_m > constraints.max_distance_m * 1.35 {
565 continue;
566 }
567 let mut edges = state.edges.clone();
568 edges.push(edge_id);
569 if constraints.allows_shape(RouteShape::OutAndBack) {
570 let out_and_back = mirrored_route(&edges);
571 if route_distance(graph, &out_and_back) <= constraints.max_distance_m * 1.35 {
572 push_allowed_route(&mut routes, graph, start, out_and_back, constraints);
573 }
574 }
575
576 let mut used = state.used.clone();
577 used.insert(edge_id);
578 if next == start && edges.len() >= 2 {
579 push_allowed_route(&mut routes, graph, start, edges.clone(), constraints);
580 if constraints.allows_shape(RouteShape::FigureEight) {
581 stack.push(State {
582 at: next,
583 edges,
584 used,
585 distance_m,
586 });
587 }
588 continue;
589 }
590
591 stack.push(State {
592 at: next,
593 edges,
594 used,
595 distance_m,
596 });
597 }
598 }
599
600 if monitor.cancelled() {
601 return Vec::new();
602 }
603 meter.finish(&routes);
604 finish_routes(routes, graph, constraints, count, self.params.keep, monitor)
605 }
606}
607
608impl RouteSolver for LoopHunter {
609 fn solve(
610 &self,
611 graph: &WalkGraph,
612 start: VertexId,
613 constraints: &LoopConstraints,
614 count: usize,
615 ) -> Vec<Route> {
616 self.solve_monitored(SearchScope::all(graph), start, constraints, count, &())
617 }
618}
619
620impl ExactLoopSolver {
621 #[must_use]
622 pub fn enumerate(
623 self,
624 graph: &WalkGraph,
625 start: VertexId,
626 constraints: &LoopConstraints,
627 count: usize,
628 ) -> Vec<Route> {
629 self.solve(graph, start, constraints, count)
630 }
631
632 fn solve_monitored(
633 &self,
634 scope: SearchScope<'_>,
635 start: VertexId,
636 constraints: &LoopConstraints,
637 count: usize,
638 monitor: &dyn SearchMonitor,
639 ) -> Vec<Route> {
640 let graph = scope.graph;
641 let mut stack = vec![State {
642 at: start,
643 edges: Vec::new(),
644 used: BTreeSet::new(),
645 distance_m: 0.0,
646 }];
647 let mut routes = Vec::<Route>::new();
648 let mut meter = SearchMeter::new(monitor, self.params.max_frontier);
649
650 while let Some(state) = stack.pop() {
651 if !meter.advance(&routes) {
652 break;
653 }
654 if state.edges.len() >= self.params.max_hops {
655 continue;
656 }
657
658 let mut fanout = scope.fanout(state.at);
659 fanout.sort();
660 fanout.reverse();
661
662 for edge_id in fanout {
663 if monitor.cancelled() {
664 return Vec::new();
665 }
666 if state.used.contains(&edge_id) {
667 continue;
668 }
669 if self.params.routing.edge_cost(graph, edge_id).is_none() {
670 continue;
671 }
672 if !graph.turn_allowed(state.edges.last().copied(), state.at, edge_id) {
673 continue;
674 }
675 let edge = &graph.edges[edge_id.0];
676 let Some(next) = edge.traverse(state.at) else {
677 continue;
678 };
679 let distance_m = state.distance_m + edge.attr.length_m;
680 if distance_m > constraints.max_distance_m * 1.35 {
681 continue;
682 }
683
684 let mut edges = state.edges.clone();
685 edges.push(edge_id);
686 if constraints.allows_shape(RouteShape::OutAndBack) {
687 let out_and_back = mirrored_route(&edges);
688 if route_distance(graph, &out_and_back) <= constraints.max_distance_m * 1.35 {
689 push_allowed_route(&mut routes, graph, start, out_and_back, constraints);
690 }
691 }
692
693 let mut used = state.used.clone();
694 used.insert(edge_id);
695 if next == start && edges.len() >= 2 {
696 push_allowed_route(&mut routes, graph, start, edges.clone(), constraints);
697 if !constraints.allows_shape(RouteShape::FigureEight) {
698 continue;
699 }
700 }
701
702 stack.push(State {
703 at: next,
704 edges,
705 used,
706 distance_m,
707 });
708 }
709 }
710
711 if monitor.cancelled() {
712 return Vec::new();
713 }
714 meter.finish(&routes);
715 finish_routes(routes, graph, constraints, count, self.params.keep, monitor)
716 }
717}
718
719impl RouteSolver for ExactLoopSolver {
720 fn solve(
721 &self,
722 graph: &WalkGraph,
723 start: VertexId,
724 constraints: &LoopConstraints,
725 count: usize,
726 ) -> Vec<Route> {
727 self.solve_monitored(SearchScope::all(graph), start, constraints, count, &())
728 }
729}
730
731#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
732struct SupportWalk {
733 at: VertexId,
734 previous: Option<EdgeId>,
735}
736
737#[derive(Clone, Copy, Debug, PartialEq)]
738struct SupportFrontier {
739 cost: f64,
740 walk: SupportWalk,
741}
742
743impl Eq for SupportFrontier {}
744
745impl Ord for SupportFrontier {
746 fn cmp(&self, rhs: &Self) -> Ordering {
747 rhs.cost
748 .total_cmp(&self.cost)
749 .then_with(|| rhs.walk.cmp(&self.walk))
750 }
751}
752
753impl PartialOrd for SupportFrontier {
754 fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
755 Some(self.cmp(rhs))
756 }
757}
758
759fn support_out_and_backs(
760 params: SearchParams,
761 scope: SearchScope<'_>,
762 start: VertexId,
763 constraints: &LoopConstraints,
764 count: usize,
765 monitor: &dyn SearchMonitor,
766) -> Vec<Route> {
767 let graph = scope.graph;
768 let law = params.routing;
769 let origin = SupportWalk {
770 at: start,
771 previous: None,
772 };
773 let mut frontier = BinaryHeap::from([SupportFrontier {
774 cost: 0.0,
775 walk: origin,
776 }]);
777 let mut distance = BTreeMap::from([(origin, 0.0)]);
778 let mut predecessor = BTreeMap::<SupportWalk, (SupportWalk, EdgeId)>::new();
779 let mut emitted = BTreeSet::new();
780 let mut routes = Vec::new();
781 let mut meter = SearchMeter::new(monitor, params.max_frontier);
782 let maximum_outward_m = constraints.max_distance_m * 0.675;
783 let maximum_cost = maximum_outward_m * (1.0 + law.road_aversion);
784
785 while let Some(SupportFrontier { cost, walk }) = frontier.pop() {
786 if cost > maximum_cost || !meter.advance(&routes) {
787 break;
788 }
789 if distance
790 .get(&walk)
791 .is_some_and(|best| cost > *best + f64::EPSILON)
792 {
793 continue;
794 }
795 let path = support_path(origin, walk, &predecessor);
796 let outward_m = route_distance(graph, &path);
797 if walk.at != start
798 && emitted.insert(walk.at)
799 && !path.is_empty()
800 && path.len() <= params.max_hops
801 && outward_m <= maximum_outward_m
802 && path.iter().copied().collect::<BTreeSet<_>>().len() == path.len()
803 {
804 push_allowed_route(
805 &mut routes,
806 graph,
807 start,
808 mirrored_route(&path),
809 constraints,
810 );
811 }
812 if path.len() >= params.max_hops || outward_m > maximum_outward_m {
813 continue;
814 }
815 for edge in scope.fanout(walk.at) {
816 if monitor.cancelled() {
817 return Vec::new();
818 }
819 if !graph.turn_allowed(walk.previous, walk.at, edge) {
820 continue;
821 }
822 let Some(edge_cost) = law.edge_cost(graph, edge) else {
823 continue;
824 };
825 let next_cost = cost + edge_cost;
826 if next_cost > maximum_cost {
827 continue;
828 }
829 let Some(at) = graph.edges[edge.0].traverse(walk.at) else {
830 continue;
831 };
832 let next = SupportWalk {
833 at,
834 previous: Some(edge),
835 };
836 if distance
837 .get(&next)
838 .is_none_or(|best| next_cost < *best - f64::EPSILON)
839 {
840 distance.insert(next, next_cost);
841 predecessor.insert(next, (walk, edge));
842 frontier.push(SupportFrontier {
843 cost: next_cost,
844 walk: next,
845 });
846 }
847 }
848 }
849 if monitor.cancelled() {
850 return Vec::new();
851 }
852 meter.finish(&routes);
853 finish_routes(routes, graph, constraints, count, params.keep, monitor)
854}
855
856const SUPPORT_RINGS: u32 = 12;
857const SUPPORT_SECTORS: u32 = 16;
858
859#[derive(Clone, Debug)]
860struct SupportDesign {
861 lower_bound_m: f64,
862 supports: Vec<VertexId>,
863}
864
865fn support_loop_portfolio(
866 params: SearchParams,
867 scope: SearchScope<'_>,
868 start: VertexId,
869 constraints: &LoopConstraints,
870 count: usize,
871 monitor: &dyn SearchMonitor,
872) -> Vec<Route> {
873 support_loop_portfolio_obeying(
874 params,
875 scope,
876 start,
877 constraints,
878 count,
879 monitor,
880 &EdgeEdicts::default(),
881 )
882}
883
884#[allow(clippy::too_many_arguments)]
885fn support_loop_portfolio_obeying(
886 params: SearchParams,
887 scope: SearchScope<'_>,
888 start: VertexId,
889 constraints: &LoopConstraints,
890 count: usize,
891 monitor: &dyn SearchMonitor,
892 edicts: &EdgeEdicts,
893) -> Vec<Route> {
894 let graph = scope.graph;
895 let skeleton =
896 RoutingSkeleton::forge_preserving(scope, start, params.routing, edicts.required());
897 let compulsory = edicts
898 .required()
899 .map(|edge| {
900 skeleton
901 .arcs
902 .iter()
903 .find(|arc| arc.edges.as_slice() == [edge])
904 .map(|arc| arc.id)
905 })
906 .collect::<Option<Vec<_>>>();
907 let Some(compulsory) = compulsory else {
908 return Vec::new();
909 };
910 let radial = radial_distances(&skeleton, start, constraints.max_distance_m * 0.55, monitor);
911 if monitor.cancelled() {
912 return Vec::new();
913 }
914 let landmarks = support_landmarks(graph, start, &radial, constraints);
915 let designs = support_designs(graph, &radial, &landmarks, constraints, params);
916 let limit = designs.len().min(params.max_frontier);
917 let mut meter = SearchMeter::new(monitor, limit);
918 let mut workspace = ArcWorkspace::new(skeleton.arcs.len());
919 let mut banned = vec![false; graph.vertices.len()];
920 let mut barred = vec![false; skeleton.arcs.len()];
921 let mut routes = Vec::new();
922 let mut forge = SupportForge {
923 skeleton: &skeleton,
924 start,
925 constraints,
926 monitor,
927 outbound: vec![None; graph.vertices.len()],
928 };
929 for (attempt, design) in designs.into_iter().take(limit).enumerate() {
930 if !meter.advance(&routes) {
931 break;
932 }
933 let mut compulsory_order = compulsory.clone();
934 if compulsory_order.len() > 1 {
935 let width = compulsory_order.len();
936 compulsory_order.rotate_left(attempt % width);
937 if attempt / width % 2 == 1 {
938 compulsory_order.reverse();
939 }
940 }
941 if let Some(edges) = forge.loop_through(
942 &compulsory_order,
943 &design.supports,
944 &mut workspace,
945 &mut banned,
946 &mut barred,
947 ) && edicts.required().all(|edge| edges.contains(&edge))
948 {
949 push_allowed_route(&mut routes, graph, start, edges, constraints);
950 }
951 }
952 if monitor.cancelled() {
953 return Vec::new();
954 }
955 meter.finish(&routes);
956 finish_routes(routes, graph, constraints, count, params.keep, monitor)
957}
958
959fn support_designs(
960 graph: &WalkGraph,
961 radial: &[f64],
962 landmarks: &[VertexId],
963 constraints: &LoopConstraints,
964 params: SearchParams,
965) -> Vec<SupportDesign> {
966 let pool = params.keep.max(1).saturating_mul(8);
967 let feasibility_floor_m = constraints.min_distance_m;
968 let mut designs = landmarks
969 .iter()
970 .copied()
971 .map(|pivot| SupportDesign {
972 lower_bound_m: radial[pivot.0] * 2.0,
973 supports: vec![pivot],
974 })
975 .collect::<Vec<_>>();
976
977 let mut pairs = Vec::new();
978 for first in landmarks.iter().copied() {
979 for second in landmarks.iter().copied().filter(|second| *second != first) {
980 pairs.push((
981 radial[first.0]
982 + graph.vertices[first.0]
983 .coord
984 .haversine_m(graph.vertices[second.0].coord)
985 + radial[second.0],
986 [first, second],
987 ));
988 }
989 }
990 pairs.sort_by(|left, right| {
991 support_rank(left.0, &left.1, feasibility_floor_m, params.seed).cmp(&support_rank(
992 right.0,
993 &right.1,
994 feasibility_floor_m,
995 params.seed,
996 ))
997 });
998 designs.extend(
999 pairs
1000 .into_iter()
1001 .take(pool)
1002 .map(|(lower_bound_m, supports)| SupportDesign {
1003 lower_bound_m,
1004 supports: supports.into(),
1005 }),
1006 );
1007
1008 let mut triples = Vec::new();
1009 for first in landmarks.iter().copied() {
1010 for second in landmarks.iter().copied().filter(|second| *second != first) {
1011 for third in landmarks
1012 .iter()
1013 .copied()
1014 .filter(|third| *third != first && *third != second)
1015 {
1016 triples.push((
1017 radial[first.0]
1018 + graph.vertices[first.0]
1019 .coord
1020 .haversine_m(graph.vertices[second.0].coord)
1021 + graph.vertices[second.0]
1022 .coord
1023 .haversine_m(graph.vertices[third.0].coord)
1024 + radial[third.0],
1025 [first, second, third],
1026 ));
1027 }
1028 }
1029 }
1030 triples.sort_by(|left, right| {
1031 support_rank(left.0, &left.1, feasibility_floor_m, params.seed).cmp(&support_rank(
1032 right.0,
1033 &right.1,
1034 feasibility_floor_m,
1035 params.seed,
1036 ))
1037 });
1038 designs.extend(triples.into_iter().take(pool.saturating_mul(2)).map(
1039 |(lower_bound_m, supports)| SupportDesign {
1040 lower_bound_m,
1041 supports: supports.into(),
1042 },
1043 ));
1044 designs.sort_by(|left, right| {
1045 support_rank(
1046 left.lower_bound_m,
1047 &left.supports,
1048 feasibility_floor_m,
1049 params.seed,
1050 )
1051 .cmp(&support_rank(
1052 right.lower_bound_m,
1053 &right.supports,
1054 feasibility_floor_m,
1055 params.seed,
1056 ))
1057 });
1058 designs
1059}
1060
1061fn support_rank(
1062 lower_bound_m: f64,
1063 supports: &[VertexId],
1064 feasibility_floor_m: f64,
1065 seed: u64,
1066) -> (u64, u64) {
1067 let deviation = (lower_bound_m - feasibility_floor_m).abs().to_bits();
1068 let hash = supports
1069 .iter()
1070 .fold(seed, |hash, vertex| splitmix64(hash ^ vertex.0 as u64));
1071 (deviation, hash)
1072}
1073
1074fn support_landmarks(
1075 graph: &WalkGraph,
1076 start: VertexId,
1077 radial: &[f64],
1078 constraints: &LoopConstraints,
1079) -> Vec<VertexId> {
1080 let ceiling_m = constraints.max_distance_m * 0.5;
1081 if ceiling_m <= 0.0 || !ceiling_m.is_finite() {
1082 return Vec::new();
1083 }
1084 let floor_m = ceiling_m / 16.0;
1085 let origin = graph.vertices[start.0].coord;
1086 let reachable = graph
1087 .vertices
1088 .iter()
1089 .filter(|vertex| vertex.id != start && radial[vertex.id.0].is_finite())
1090 .collect::<Vec<_>>();
1091 let mut landmarks = BTreeSet::new();
1092 for ring in 0..SUPPORT_RINGS {
1093 let target_m =
1094 floor_m + (ceiling_m - floor_m) * (f64::from(ring) + 0.5) / f64::from(SUPPORT_RINGS);
1095 for sector in 0..SUPPORT_SECTORS {
1096 landmarks.extend(
1097 reachable
1098 .iter()
1099 .copied()
1100 .filter(|vertex| bearing_sector(origin, vertex.coord) == sector)
1101 .min_by(|left, right| {
1102 (radial[left.id.0] - target_m)
1103 .abs()
1104 .total_cmp(&(radial[right.id.0] - target_m).abs())
1105 .then_with(|| left.id.cmp(&right.id))
1106 })
1107 .map(|vertex| vertex.id),
1108 );
1109 }
1110 }
1111 landmarks.into_iter().collect()
1112}
1113
1114fn bearing_sector(origin: Coord, point: Coord) -> u32 {
1115 let x = (point.lon - origin.lon) * origin.lat.to_radians().cos();
1116 let y = point.lat - origin.lat;
1117 let turn = (y.atan2(x) + std::f64::consts::PI) / std::f64::consts::TAU;
1118 (0..SUPPORT_SECTORS - 1)
1119 .find(|sector| turn < f64::from(*sector + 1) / f64::from(SUPPORT_SECTORS))
1120 .unwrap_or(SUPPORT_SECTORS - 1)
1121}
1122
1123#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
1124struct ArcId(usize);
1125
1126struct RoutingArc {
1127 id: ArcId,
1128 a: VertexId,
1129 b: VertexId,
1130 edges: Vec<EdgeId>,
1131 distance_m: f64,
1132 routing_cost_m: f64,
1133 forward: bool,
1134 backward: bool,
1135}
1136
1137impl RoutingArc {
1138 fn traverse(&self, from: VertexId) -> Option<VertexId> {
1139 if from == self.a && self.forward {
1140 Some(self.b)
1141 } else if from == self.b && self.backward {
1142 Some(self.a)
1143 } else {
1144 None
1145 }
1146 }
1147
1148 fn first_edge_from(&self, from: VertexId) -> Option<EdgeId> {
1149 self.traverse(from)?;
1150 if from == self.a {
1151 self.edges.first().copied()
1152 } else {
1153 self.edges.last().copied()
1154 }
1155 }
1156
1157 fn last_edge_at(&self, at: VertexId) -> Option<EdgeId> {
1158 if at == self.b && self.forward {
1159 self.edges.last().copied()
1160 } else if at == self.a && self.backward {
1161 self.edges.first().copied()
1162 } else {
1163 None
1164 }
1165 }
1166
1167 fn append_edges_from(&self, from: VertexId, output: &mut Vec<EdgeId>) -> Option<VertexId> {
1168 let at = self.traverse(from)?;
1169 if from == self.a {
1170 output.extend(self.edges.iter().copied());
1171 } else {
1172 output.extend(self.edges.iter().rev().copied());
1173 }
1174 Some(at)
1175 }
1176}
1177
1178struct RoutingSkeleton<'graph> {
1179 graph: &'graph WalkGraph,
1180 arcs: Vec<RoutingArc>,
1181 adjacency: Vec<Vec<ArcId>>,
1182}
1183
1184impl<'graph> RoutingSkeleton<'graph> {
1185 fn forge_preserving(
1186 scope: SearchScope<'graph>,
1187 start: VertexId,
1188 law: RoutingLaw,
1189 compulsory: impl IntoIterator<Item = EdgeId>,
1190 ) -> Self {
1191 let graph = scope.graph;
1192 let incidence = routing_incidence(scope, law);
1193 let mut preserved = preserved_vertices(graph, &incidence, start);
1194 for edge in compulsory {
1195 preserved[graph.edges[edge.0].a.0] = true;
1196 preserved[graph.edges[edge.0].b.0] = true;
1197 }
1198 let arcs = skeleton_arcs(graph, &incidence, &preserved, law);
1199 let adjacency = arc_adjacency(graph.vertices.len(), &arcs);
1200 Self {
1201 graph,
1202 arcs,
1203 adjacency,
1204 }
1205 }
1206
1207 fn turn_allowed(&self, previous: Option<ArcId>, via: VertexId, next: ArcId) -> bool {
1208 let prior = previous.and_then(|arc| self.arcs[arc.0].last_edge_at(via));
1209 self.arcs[next.0]
1210 .first_edge_from(via)
1211 .is_some_and(|edge| self.graph.turn_allowed(prior, via, edge))
1212 }
1213
1214 fn expand(&self, start: VertexId, arcs: &[ArcId]) -> Option<Vec<EdgeId>> {
1215 let mut at = start;
1216 let mut edges = Vec::new();
1217 for arc in arcs {
1218 at = self.arcs[arc.0].append_edges_from(at, &mut edges)?;
1219 }
1220 Some(edges)
1221 }
1222}
1223
1224fn routing_incidence(scope: SearchScope<'_>, law: RoutingLaw) -> Vec<Vec<EdgeId>> {
1225 let graph = scope.graph;
1226 let mut incidence = vec![Vec::new(); graph.vertices.len()];
1227 for edge in graph
1228 .edges
1229 .iter()
1230 .filter(|edge| scope.allows(edge.id) && law.edge_cost(graph, edge.id).is_some())
1231 {
1232 incidence[edge.a.0].push(edge.id);
1233 incidence[edge.b.0].push(edge.id);
1234 }
1235 incidence
1236}
1237
1238fn preserved_vertices(graph: &WalkGraph, incidence: &[Vec<EdgeId>], start: VertexId) -> Vec<bool> {
1239 let mut barred_turn = vec![false; graph.vertices.len()];
1240 for ban in &graph.turn_bans {
1241 barred_turn[ban.via.0] = true;
1242 }
1243 let mut preserved = graph
1244 .vertices
1245 .iter()
1246 .map(|vertex| {
1247 let edges = &incidence[vertex.id.0];
1248 let distinct_neighbours = edges.len() == 2
1249 && graph.edges[edges[0].0].other(vertex.id)
1250 != graph.edges[edges[1].0].other(vertex.id);
1251 vertex.id == start
1252 || barred_turn[vertex.id.0]
1253 || !distinct_neighbours
1254 || edges
1255 .iter()
1256 .any(|edge| graph.edges[edge.0].attr.travel != EdgeTravel::Both)
1257 })
1258 .collect::<Vec<_>>();
1259 let roots = preserved.clone();
1260 for vertex in graph.vertices.iter().filter(|vertex| roots[vertex.id.0]) {
1261 for edge in &incidence[vertex.id.0] {
1262 let neighbour = graph.edges[edge.0]
1263 .other(vertex.id)
1264 .expect("an incident edge contains its vertex");
1265 preserved[neighbour.0] = true;
1266 }
1267 }
1268 preserved
1269}
1270
1271fn skeleton_arcs(
1272 graph: &WalkGraph,
1273 incidence: &[Vec<EdgeId>],
1274 preserved: &[bool],
1275 law: RoutingLaw,
1276) -> Vec<RoutingArc> {
1277 let mut visited = vec![false; graph.edges.len()];
1278 let mut arcs = Vec::new();
1279 for vertex in graph
1280 .vertices
1281 .iter()
1282 .filter(|vertex| preserved[vertex.id.0])
1283 {
1284 for first in incidence[vertex.id.0].iter().copied() {
1285 if visited[first.0] {
1286 continue;
1287 }
1288 let mut edges = Vec::new();
1289 let mut at = vertex.id;
1290 let mut edge_id = first;
1291 let endpoint = loop {
1292 visited[edge_id.0] = true;
1293 edges.push(edge_id);
1294 let next = graph.edges[edge_id.0]
1295 .other(at)
1296 .expect("an incident edge contains its vertex");
1297 if preserved[next.0] {
1298 break next;
1299 }
1300 edge_id = incidence[next.0]
1301 .iter()
1302 .copied()
1303 .find(|candidate| *candidate != edge_id)
1304 .expect("an elided vertex has exactly two incident edges");
1305 at = next;
1306 };
1307 let id = ArcId(arcs.len());
1308 let distance_m = edges
1309 .iter()
1310 .map(|edge| graph.edges[edge.0].attr.length_m)
1311 .sum();
1312 let routing_cost_m = edges
1313 .iter()
1314 .map(|edge| {
1315 law.edge_cost(graph, *edge)
1316 .expect("a skeleton contains only lawful edges")
1317 })
1318 .sum();
1319 arcs.push(RoutingArc {
1320 id,
1321 a: vertex.id,
1322 b: endpoint,
1323 forward: chain_traversable(graph, vertex.id, edges.iter().copied()),
1324 backward: chain_traversable(graph, endpoint, edges.iter().rev().copied()),
1325 edges,
1326 distance_m,
1327 routing_cost_m,
1328 });
1329 }
1330 }
1331 arcs
1332}
1333
1334fn arc_adjacency(vertex_count: usize, arcs: &[RoutingArc]) -> Vec<Vec<ArcId>> {
1335 let mut adjacency = vec![Vec::new(); vertex_count];
1336 for arc in arcs {
1337 if arc.forward {
1338 adjacency[arc.a.0].push(arc.id);
1339 }
1340 if arc.backward {
1341 adjacency[arc.b.0].push(arc.id);
1342 }
1343 }
1344 adjacency
1345}
1346
1347fn chain_traversable(
1348 graph: &WalkGraph,
1349 mut at: VertexId,
1350 edges: impl IntoIterator<Item = EdgeId>,
1351) -> bool {
1352 for edge in edges {
1353 let Some(next) = graph.edges[edge.0].traverse(at) else {
1354 return false;
1355 };
1356 at = next;
1357 }
1358 true
1359}
1360
1361#[derive(Clone, Copy, Debug, PartialEq)]
1362struct RadialFrontier {
1363 distance_m: f64,
1364 at: VertexId,
1365}
1366
1367impl Eq for RadialFrontier {}
1368
1369impl Ord for RadialFrontier {
1370 fn cmp(&self, other: &Self) -> Ordering {
1371 other
1372 .distance_m
1373 .total_cmp(&self.distance_m)
1374 .then_with(|| other.at.cmp(&self.at))
1375 }
1376}
1377
1378impl PartialOrd for RadialFrontier {
1379 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1380 Some(self.cmp(other))
1381 }
1382}
1383
1384fn radial_distances(
1385 skeleton: &RoutingSkeleton<'_>,
1386 start: VertexId,
1387 maximum_m: f64,
1388 monitor: &dyn SearchMonitor,
1389) -> Vec<f64> {
1390 let graph = skeleton.graph;
1391 let mut distance = vec![f64::INFINITY; graph.vertices.len()];
1392 distance[start.0] = 0.0;
1393 let mut heap = BinaryHeap::from([RadialFrontier {
1394 distance_m: 0.0,
1395 at: start,
1396 }]);
1397 while let Some(frontier) = heap.pop() {
1398 if monitor.cancelled() {
1399 break;
1400 }
1401 if frontier.distance_m > distance[frontier.at.0] {
1402 continue;
1403 }
1404 for arc_id in &skeleton.adjacency[frontier.at.0] {
1405 let arc = &skeleton.arcs[arc_id.0];
1406 let Some(at) = arc.traverse(frontier.at) else {
1407 continue;
1408 };
1409 let next_m = frontier.distance_m + arc.distance_m;
1410 if next_m <= maximum_m && next_m < distance[at.0] {
1411 distance[at.0] = next_m;
1412 heap.push(RadialFrontier {
1413 distance_m: next_m,
1414 at,
1415 });
1416 }
1417 }
1418 }
1419 distance
1420}
1421
1422struct SupportForge<'graph, 'constraint, 'monitor> {
1423 skeleton: &'graph RoutingSkeleton<'graph>,
1424 start: VertexId,
1425 constraints: &'constraint LoopConstraints,
1426 monitor: &'monitor dyn SearchMonitor,
1427 outbound: Vec<Option<MeasuredPath>>,
1428}
1429
1430impl SupportForge<'_, '_, '_> {
1431 fn loop_through(
1432 &mut self,
1433 compulsory: &[ArcId],
1434 supports: &[VertexId],
1435 workspace: &mut ArcWorkspace,
1436 banned: &mut [bool],
1437 barred: &mut [bool],
1438 ) -> Option<Vec<EdgeId>> {
1439 banned.fill(false);
1440 barred.fill(false);
1441 for arc in compulsory {
1442 barred[arc.0] = true;
1443 }
1444 banned[self.start.0] = true;
1445 let maximum_m = self.constraints.max_distance_m;
1446 let mut arcs = Vec::new();
1447 let mut at = self.start;
1448 let mut spent_m = 0.0;
1449 for exact in compulsory {
1450 let arc = &self.skeleton.arcs[exact.0];
1451 let approach_budget = maximum_m - spent_m - arc.distance_m;
1452 let mut approaches = [(arc.a, arc.forward), (arc.b, arc.backward)]
1453 .into_iter()
1454 .filter(|(target, traversable)| {
1455 *traversable && (*target == at || !banned[target.0])
1456 })
1457 .filter_map(|(target, _)| {
1458 shortest_path_avoiding(
1459 AvoidanceHunt {
1460 skeleton: self.skeleton,
1461 from: at,
1462 target,
1463 previous: arcs.last().copied(),
1464 banned,
1465 barred,
1466 max_distance_m: approach_budget,
1467 monitor: self.monitor,
1468 },
1469 workspace,
1470 )
1471 .map(|path| (target, path))
1472 })
1473 .collect::<Vec<_>>();
1474 approaches.sort_by(|left, right| left.1.distance_m.total_cmp(&right.1.distance_m));
1475 let (target, path) = approaches.into_iter().next()?;
1476 ban_internal_vertices(self.skeleton, at, &path.arcs, banned);
1477 for connector in &path.arcs {
1478 barred[connector.0] = true;
1479 }
1480 if at != self.start {
1481 banned[at.0] = true;
1482 }
1483 spent_m += path.distance_m;
1484 arcs.extend(path.arcs);
1485 at = target;
1486 if !self.skeleton.turn_allowed(arcs.last().copied(), at, *exact) {
1487 return None;
1488 }
1489 at = arc.traverse(at)?;
1490 arcs.push(*exact);
1491 spent_m += arc.distance_m;
1492 if spent_m > maximum_m {
1493 return None;
1494 }
1495 }
1496 for target in supports.iter().copied().chain(std::iter::once(self.start)) {
1497 let hunt = AvoidanceHunt {
1498 skeleton: self.skeleton,
1499 from: at,
1500 target,
1501 previous: arcs.last().copied(),
1502 banned,
1503 barred,
1504 max_distance_m: maximum_m - spent_m,
1505 monitor: self.monitor,
1506 };
1507 let path = if at == self.start && arcs.is_empty() {
1508 if self.outbound[target.0].is_none() {
1509 self.outbound[target.0] = shortest_path_avoiding(hunt, workspace);
1510 }
1511 self.outbound[target.0].clone()?
1512 } else {
1513 shortest_path_avoiding(hunt, workspace)?
1514 };
1515 ban_internal_vertices(self.skeleton, at, &path.arcs, banned);
1516 for arc in &path.arcs {
1517 barred[arc.0] = true;
1518 }
1519 if at != self.start {
1520 banned[at.0] = true;
1521 }
1522 spent_m += path.distance_m;
1523 arcs.extend(path.arcs);
1524 at = target;
1525 }
1526 let graph = self.skeleton.graph;
1527 let edges = self.skeleton.expand(self.start, &arcs)?;
1528 (edge_simple(&edges) && graph.walk_edges(self.start, &edges) == Some(self.start))
1529 .then_some(edges)
1530 }
1531}
1532
1533fn ban_internal_vertices(
1534 skeleton: &RoutingSkeleton<'_>,
1535 mut at: VertexId,
1536 arcs: &[ArcId],
1537 banned: &mut [bool],
1538) {
1539 for arc in arcs.iter().copied().take(arcs.len().saturating_sub(1)) {
1540 at = skeleton.arcs[arc.0]
1541 .traverse(at)
1542 .expect("a recovered path is a legal walk");
1543 banned[at.0] = true;
1544 }
1545}
1546
1547#[derive(Clone, Copy)]
1548struct AvoidanceHunt<'a> {
1549 skeleton: &'a RoutingSkeleton<'a>,
1550 from: VertexId,
1551 target: VertexId,
1552 previous: Option<ArcId>,
1553 banned: &'a [bool],
1554 barred: &'a [bool],
1555 max_distance_m: f64,
1556 monitor: &'a dyn SearchMonitor,
1557}
1558
1559#[derive(Clone)]
1560struct MeasuredPath {
1561 arcs: Vec<ArcId>,
1562 distance_m: f64,
1563}
1564
1565fn shortest_path_avoiding(
1566 hunt: AvoidanceHunt<'_>,
1567 workspace: &mut ArcWorkspace,
1568) -> Option<MeasuredPath> {
1569 if hunt.max_distance_m < 0.0 {
1570 return None;
1571 }
1572 let skeleton = hunt.skeleton;
1573 let target = skeleton.graph.vertices[hunt.target.0].coord;
1574 let origin_bound_m = skeleton.graph.vertices[hunt.from.0]
1575 .coord
1576 .haversine_m(target);
1577 if origin_bound_m > hunt.max_distance_m {
1578 return None;
1579 }
1580 workspace.begin();
1581 let origin = ArcWalk {
1582 at: hunt.from,
1583 previous: hunt.previous,
1584 };
1585 let origin_label = workspace.admit(
1586 arc_slot(skeleton, origin),
1587 ArcLabel {
1588 routing_cost_m: 0.0,
1589 distance_m: 0.0,
1590 predecessor: None,
1591 arc: None,
1592 live: true,
1593 },
1594 )?;
1595 workspace.heap.push(ArcFrontier {
1596 rank_m: origin_bound_m,
1597 routing_cost_m: 0.0,
1598 distance_m: 0.0,
1599 walk: origin,
1600 label: origin_label,
1601 });
1602 let mut expanded = 0usize;
1603 let expansion_cap = return_expansion_cap(1, skeleton.graph.edges.len());
1604 while let Some(frontier) = workspace.heap.pop() {
1605 if hunt.monitor.cancelled() || expanded >= expansion_cap {
1606 return None;
1607 }
1608 if !workspace.labels[frontier.label].live {
1609 continue;
1610 }
1611 expanded += 1;
1612 if frontier.walk.at == hunt.target {
1613 let arcs = recover_arc_path(&workspace.labels, frontier.label);
1614 return arc_simple(&arcs).then_some(MeasuredPath {
1615 arcs,
1616 distance_m: frontier.distance_m,
1617 });
1618 }
1619 for arc_id in &skeleton.adjacency[frontier.walk.at.0] {
1620 if hunt.barred[arc_id.0]
1621 || frontier.walk.previous == Some(*arc_id)
1622 || !skeleton.turn_allowed(frontier.walk.previous, frontier.walk.at, *arc_id)
1623 {
1624 continue;
1625 }
1626 let arc = &skeleton.arcs[arc_id.0];
1627 let Some(at) = arc.traverse(frontier.walk.at) else {
1628 continue;
1629 };
1630 if at != hunt.target && hunt.banned[at.0] {
1631 continue;
1632 }
1633 let distance_m = frontier.distance_m + arc.distance_m;
1634 let remaining_bound_m = skeleton.graph.vertices[at.0].coord.haversine_m(target);
1635 if distance_m + remaining_bound_m > hunt.max_distance_m {
1636 continue;
1637 }
1638 let routing_cost_m = frontier.routing_cost_m + arc.routing_cost_m;
1639 let walk = ArcWalk {
1640 at,
1641 previous: Some(*arc_id),
1642 };
1643 let Some(label) = workspace.admit(
1644 arc_slot(skeleton, walk),
1645 ArcLabel {
1646 routing_cost_m,
1647 distance_m,
1648 predecessor: Some(frontier.label),
1649 arc: Some(*arc_id),
1650 live: true,
1651 },
1652 ) else {
1653 continue;
1654 };
1655 workspace.heap.push(ArcFrontier {
1656 rank_m: routing_cost_m + remaining_bound_m,
1657 routing_cost_m,
1658 distance_m,
1659 walk,
1660 label,
1661 });
1662 }
1663 }
1664 None
1665}
1666
1667#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
1668struct ArcWalk {
1669 at: VertexId,
1670 previous: Option<ArcId>,
1671}
1672
1673#[derive(Clone, Debug)]
1674struct ArcLabel {
1675 routing_cost_m: f64,
1676 distance_m: f64,
1677 predecessor: Option<usize>,
1678 arc: Option<ArcId>,
1679 live: bool,
1680}
1681
1682impl ArcLabel {
1683 fn dominates(&self, routing_cost_m: f64, distance_m: f64) -> bool {
1684 self.live && self.routing_cost_m <= routing_cost_m && self.distance_m <= distance_m
1685 }
1686
1687 fn is_dominated_by(&self, routing_cost_m: f64, distance_m: f64) -> bool {
1688 routing_cost_m <= self.routing_cost_m && distance_m <= self.distance_m
1689 }
1690}
1691
1692#[derive(Clone, Copy, Debug, PartialEq)]
1693struct ArcFrontier {
1694 rank_m: f64,
1695 routing_cost_m: f64,
1696 distance_m: f64,
1697 walk: ArcWalk,
1698 label: usize,
1699}
1700
1701impl Eq for ArcFrontier {}
1702
1703impl Ord for ArcFrontier {
1704 fn cmp(&self, other: &Self) -> Ordering {
1705 other
1706 .rank_m
1707 .total_cmp(&self.rank_m)
1708 .then_with(|| other.routing_cost_m.total_cmp(&self.routing_cost_m))
1709 .then_with(|| other.distance_m.total_cmp(&self.distance_m))
1710 .then_with(|| other.walk.cmp(&self.walk))
1711 .then_with(|| other.label.cmp(&self.label))
1712 }
1713}
1714
1715impl PartialOrd for ArcFrontier {
1716 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1717 Some(self.cmp(other))
1718 }
1719}
1720
1721struct ArcWorkspace {
1722 skylines: Vec<Vec<usize>>,
1723 touched: Vec<usize>,
1724 labels: Vec<ArcLabel>,
1725 heap: BinaryHeap<ArcFrontier>,
1726}
1727
1728impl ArcWorkspace {
1729 fn new(arc_count: usize) -> Self {
1730 Self {
1731 skylines: vec![Vec::new(); closure_slot_count(arc_count)],
1732 touched: Vec::new(),
1733 labels: Vec::new(),
1734 heap: BinaryHeap::new(),
1735 }
1736 }
1737
1738 fn begin(&mut self) {
1739 for slot in self.touched.drain(..) {
1740 self.skylines[slot].clear();
1741 }
1742 self.labels.clear();
1743 self.heap.clear();
1744 }
1745
1746 fn admit(&mut self, slot: usize, label: ArcLabel) -> Option<usize> {
1747 let peers = &mut self.skylines[slot];
1748 let pristine = peers.is_empty();
1749 if peers
1750 .iter()
1751 .any(|id| self.labels[*id].dominates(label.routing_cost_m, label.distance_m))
1752 {
1753 return None;
1754 }
1755 for id in peers.iter().copied() {
1756 let incumbent = &mut self.labels[id];
1757 if incumbent.is_dominated_by(label.routing_cost_m, label.distance_m) {
1758 incumbent.live = false;
1759 }
1760 }
1761 peers.retain(|id| self.labels[*id].live);
1762 let id = self.labels.len();
1763 self.labels.push(label);
1764 if pristine {
1765 self.touched.push(slot);
1766 }
1767 peers.push(id);
1768 Some(id)
1769 }
1770}
1771
1772fn arc_slot(skeleton: &RoutingSkeleton<'_>, walk: ArcWalk) -> usize {
1773 let Some(arc_id) = walk.previous else {
1774 return skeleton.arcs.len() * 2;
1775 };
1776 let arc = &skeleton.arcs[arc_id.0];
1777 arc_id.0 * 2
1778 + if walk.at == arc.a {
1779 0
1780 } else {
1781 debug_assert_eq!(walk.at, arc.b);
1782 1
1783 }
1784}
1785
1786fn recover_arc_path(labels: &[ArcLabel], mut label: usize) -> Vec<ArcId> {
1787 let mut arcs = Vec::new();
1788 while let Some(predecessor) = labels[label].predecessor {
1789 arcs.push(
1790 labels[label]
1791 .arc
1792 .expect("a non-origin arc label records its arc"),
1793 );
1794 label = predecessor;
1795 }
1796 arcs.reverse();
1797 arcs
1798}
1799
1800fn arc_simple(arcs: &[ArcId]) -> bool {
1801 let mut seen = BTreeSet::new();
1802 arcs.iter().all(|arc| seen.insert(*arc))
1803}
1804
1805fn support_path(
1806 origin: SupportWalk,
1807 mut cursor: SupportWalk,
1808 predecessor: &BTreeMap<SupportWalk, (SupportWalk, EdgeId)>,
1809) -> Vec<EdgeId> {
1810 let mut edges = Vec::new();
1811 while cursor != origin {
1812 let Some((prior, edge)) = predecessor.get(&cursor).copied() else {
1813 return Vec::new();
1814 };
1815 edges.push(edge);
1816 cursor = prior;
1817 }
1818 edges.reverse();
1819 edges
1820}
1821
1822fn closes_allowed(constraints: &LoopConstraints) -> bool {
1823 constraints.allows_shape(RouteShape::Loop) || constraints.allows_shape(RouteShape::FigureEight)
1824}
1825
1826fn push_allowed_route(
1827 routes: &mut Vec<Route>,
1828 graph: &WalkGraph,
1829 start: VertexId,
1830 edges: Vec<EdgeId>,
1831 constraints: &LoopConstraints,
1832) {
1833 if graph.walk_edges(start, &edges).is_none() {
1834 return;
1835 }
1836 let route = Route::from_edges(
1837 format!("candidate-{}", routes.len() + 1),
1838 graph,
1839 start,
1840 edges,
1841 constraints,
1842 );
1843 if constraints.allows_shape(route.metrics.shape) {
1844 routes.push(route);
1845 }
1846}
1847
1848fn finish_routes(
1849 mut routes: Vec<Route>,
1850 graph: &WalkGraph,
1851 constraints: &LoopConstraints,
1852 count: usize,
1853 keep: usize,
1854 monitor: &dyn SearchMonitor,
1855) -> Vec<Route> {
1856 if monitor.cancelled() {
1857 return Vec::new();
1858 }
1859 let mut seen = BTreeSet::new();
1860 routes.retain(|route| seen.insert(route_signature(route)));
1861 rank_routes(&mut routes, constraints);
1862 if monitor.cancelled() {
1863 return Vec::new();
1864 }
1865 routes = diverse_portfolio(routes, graph, graphless_limit(count, keep), monitor);
1866 if monitor.cancelled() {
1867 return Vec::new();
1868 }
1869 for (i, route) in routes.iter_mut().enumerate() {
1870 route.name = format!("candidate-{}", i + 1);
1871 }
1872 routes
1873}
1874
1875const fn graphless_limit(count: usize, keep: usize) -> usize {
1876 if count < 1 {
1877 1
1878 } else if count < keep {
1879 count
1880 } else {
1881 keep
1882 }
1883}
1884
1885fn diverse_portfolio(
1886 routes: Vec<Route>,
1887 graph: &WalkGraph,
1888 limit: usize,
1889 monitor: &dyn SearchMonitor,
1890) -> Vec<Route> {
1891 if routes.len() <= limit {
1892 return routes;
1893 }
1894 let tier = routes
1895 .iter()
1896 .position(|route| !route.verdict.satisfied)
1897 .unwrap_or(routes.len());
1898 let mut misses = routes;
1899 let near = misses.split_off(tier);
1900 let mut chosen = Vec::with_capacity(limit);
1901 admit_diverse_tier(misses, graph, limit, &mut chosen, monitor);
1902 admit_diverse_tier(near, graph, limit, &mut chosen, monitor);
1903 chosen
1904}
1905
1906fn admit_diverse_tier(
1907 routes: Vec<Route>,
1908 graph: &WalkGraph,
1909 limit: usize,
1910 chosen: &mut Vec<Route>,
1911 monitor: &dyn SearchMonitor,
1912) {
1913 let mut pool = routes.into_iter().map(Some).collect::<Vec<_>>();
1914 for exclusion_radius in [0.35, 0.20, 0.08, 0.0] {
1915 for candidate in &mut pool {
1916 if chosen.len() >= limit || monitor.cancelled() {
1917 break;
1918 }
1919 let admit = candidate.as_ref().is_some_and(|route| {
1920 chosen
1921 .iter()
1922 .all(|known| route_distance_between(graph, route, known) >= exclusion_radius)
1923 });
1924 if admit {
1925 chosen.push(candidate.take().expect("admitted route exists"));
1926 }
1927 }
1928 }
1929}
1930
1931fn route_distance_between(graph: &WalkGraph, left: &Route, right: &Route) -> f64 {
1932 let counts = |route: &Route| {
1933 let mut counts = BTreeMap::<EdgeId, u32>::new();
1934 for edge in &route.edges {
1935 let count = counts.entry(*edge).or_default();
1936 *count = count
1937 .checked_add(1)
1938 .expect("a route has fewer than 2³² legs");
1939 }
1940 counts
1941 };
1942 let left_counts = counts(left);
1943 let right_counts = counts(right);
1944 let shared_m = left_counts
1945 .iter()
1946 .map(|(edge, count)| {
1947 graph.edges[edge.0].attr.length_m
1948 * f64::from(*count.min(right_counts.get(edge).unwrap_or(&0)))
1949 })
1950 .sum::<f64>();
1951 let basis_m = left
1952 .metrics
1953 .distance_m
1954 .min(right.metrics.distance_m)
1955 .max(1.0);
1956 1.0 - shared_m / basis_m
1957}
1958
1959#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
1960struct RouteSignature {
1961 shape: RouteShape,
1962 edge_counts: Vec<(EdgeId, usize)>,
1963}
1964
1965fn route_signature(route: &Route) -> RouteSignature {
1966 let mut edge_counts = BTreeMap::<EdgeId, usize>::new();
1967 for edge in &route.edges {
1968 *edge_counts.entry(*edge).or_default() += 1;
1969 }
1970 RouteSignature {
1971 shape: route.metrics.shape,
1972 edge_counts: edge_counts.into_iter().collect(),
1973 }
1974}
1975
1976fn route_distance(graph: &WalkGraph, edges: &[EdgeId]) -> f64 {
1977 edges
1978 .iter()
1979 .map(|edge_id| graph.edges[edge_id.0].attr.length_m)
1980 .sum()
1981}
1982
1983#[derive(Clone, Copy)]
1984struct ReturnHunt<'a> {
1985 graph: &'a WalkGraph,
1986 from: VertexId,
1987 target: VertexId,
1988 previous: Option<EdgeId>,
1989 barred: &'a BTreeSet<EdgeId>,
1990 max_distance_m: f64,
1991 keep: usize,
1992 law: RoutingLaw,
1993 monitor: &'a dyn SearchMonitor,
1994 allowed: Option<&'a [bool]>,
1995}
1996
1997struct LoopCloser<'graph, 'monitor> {
1998 graph: &'graph WalkGraph,
1999 target: VertexId,
2000 keep: usize,
2001 law: RoutingLaw,
2002 monitor: &'monitor dyn SearchMonitor,
2003 allowed: Option<&'graph [bool]>,
2004 active: bool,
2005 workspace: ClosureWorkspace,
2006 oracle: Option<ClosureOracle>,
2007}
2008
2009impl<'graph, 'monitor> LoopCloser<'graph, 'monitor> {
2010 fn forge(
2011 scope: SearchScope<'graph>,
2012 target: VertexId,
2013 params: SearchParams,
2014 constraints: &LoopConstraints,
2015 monitor: &'monitor dyn SearchMonitor,
2016 ) -> Option<Self> {
2017 let active = closes_allowed(constraints);
2018 let oracle = if active && params.closure_paths <= 1 {
2019 Some(ClosureOracle::forge(
2020 scope.graph,
2021 target,
2022 scope.allowed,
2023 params.routing,
2024 monitor,
2025 )?)
2026 } else {
2027 None
2028 };
2029 Some(Self {
2030 graph: scope.graph,
2031 target,
2032 keep: params.closure_paths,
2033 law: params.routing,
2034 monitor,
2035 allowed: scope.allowed,
2036 active,
2037 workspace: ClosureWorkspace::new(scope.graph.edges.len()),
2038 oracle,
2039 })
2040 }
2041
2042 fn strike(&mut self, state: &State, constraints: &LoopConstraints, routes: &mut Vec<Route>) {
2043 if !self.active || state.at == self.target || state.edges.is_empty() {
2044 return;
2045 }
2046 let max_distance_m = constraints.max_distance_m.mul_add(1.35, -state.distance_m);
2047 let hunt = ReturnHunt {
2048 graph: self.graph,
2049 from: state.at,
2050 target: self.target,
2051 previous: state.edges.last().copied(),
2052 barred: &state.used,
2053 max_distance_m,
2054 keep: self.keep,
2055 law: self.law,
2056 monitor: self.monitor,
2057 allowed: self.allowed,
2058 };
2059 for return_edges in shortest_return_paths(hunt, &mut self.workspace, self.oracle.as_ref()) {
2060 let mut edges = state.edges.clone();
2061 edges.extend(return_edges);
2062 push_allowed_route(routes, self.graph, self.target, edges, constraints);
2063 }
2064 }
2065}
2066
2067fn shortest_return_paths(
2068 hunt: ReturnHunt<'_>,
2069 workspace: &mut ClosureWorkspace,
2070 oracle: Option<&ClosureOracle>,
2071) -> Vec<Vec<EdgeId>> {
2072 if hunt.max_distance_m < 0.0 {
2073 return Vec::new();
2074 }
2075 if hunt.keep <= 1 {
2076 return shortest_return_path(
2077 hunt,
2078 workspace,
2079 oracle.expect("single-path closure search has an oracle"),
2080 )
2081 .into_iter()
2082 .collect();
2083 }
2084 enumerate_return_paths(hunt)
2085}
2086
2087fn shortest_return_path(
2088 hunt: ReturnHunt<'_>,
2089 workspace: &mut ClosureWorkspace,
2090 oracle: &ClosureOracle,
2091) -> Option<Vec<EdgeId>> {
2092 workspace.begin();
2096 let origin = SupportWalk {
2097 at: hunt.from,
2098 previous: hunt.previous,
2099 };
2100 let origin_label = workspace
2101 .admit(
2102 closure_slot(hunt.graph, origin),
2103 ClosureLabel {
2104 routing_cost_m: 0.0,
2105 distance_m: 0.0,
2106 predecessor: None,
2107 edge: None,
2108 live: true,
2109 },
2110 )
2111 .expect("an empty closure skyline admits its origin");
2112 let rank_m = oracle.cost(hunt.graph, origin);
2113 if !rank_m.is_finite() {
2114 return None;
2115 }
2116 workspace.heap.push(ClosureFrontier {
2117 rank_m,
2118 routing_cost_m: 0.0,
2119 distance_m: 0.0,
2120 walk: origin,
2121 label: origin_label,
2122 });
2123 let expansion_cap = return_expansion_cap(1, hunt.graph.edges.len());
2124 let mut expanded = 0usize;
2125
2126 while let Some(frontier) = workspace.heap.pop() {
2127 if hunt.monitor.cancelled() {
2128 return None;
2129 }
2130 if expanded >= expansion_cap {
2131 break;
2132 }
2133 if !workspace.labels[frontier.label].live {
2134 continue;
2135 }
2136 expanded += 1;
2137 if frontier.walk.at == hunt.target && frontier.label != origin_label {
2138 let path = recover_closure_path(&workspace.labels, frontier.label);
2139 if edge_simple(&path) {
2140 return Some(path);
2141 }
2142 continue;
2143 }
2144 for edge_id in &hunt.graph.adjacency[frontier.walk.at.0] {
2145 if hunt.allowed.is_some_and(|allowed| !allowed[edge_id.0])
2146 || hunt.barred.contains(edge_id)
2147 || frontier.walk.previous == Some(*edge_id)
2148 || !hunt
2149 .graph
2150 .turn_allowed(frontier.walk.previous, frontier.walk.at, *edge_id)
2151 {
2152 continue;
2153 }
2154 let Some(edge_cost) = hunt.law.edge_cost(hunt.graph, *edge_id) else {
2155 continue;
2156 };
2157 let edge = &hunt.graph.edges[edge_id.0];
2158 let Some(at) = edge.traverse(frontier.walk.at) else {
2159 continue;
2160 };
2161 let distance_m = frontier.distance_m + edge.attr.length_m;
2162 if distance_m > hunt.max_distance_m {
2163 continue;
2164 }
2165 let routing_cost_m = frontier.routing_cost_m + edge_cost;
2166 let walk = SupportWalk {
2167 at,
2168 previous: Some(*edge_id),
2169 };
2170 let remaining_cost_m = oracle.cost(hunt.graph, walk);
2171 if !remaining_cost_m.is_finite() {
2172 continue;
2173 }
2174 let Some(label) = workspace.admit(
2175 closure_slot(hunt.graph, walk),
2176 ClosureLabel {
2177 routing_cost_m,
2178 distance_m,
2179 predecessor: Some(frontier.label),
2180 edge: Some(*edge_id),
2181 live: true,
2182 },
2183 ) else {
2184 continue;
2185 };
2186 workspace.heap.push(ClosureFrontier {
2187 rank_m: routing_cost_m + remaining_cost_m,
2188 routing_cost_m,
2189 distance_m,
2190 walk,
2191 label,
2192 });
2193 }
2194 }
2195 None
2196}
2197
2198fn enumerate_return_paths(hunt: ReturnHunt<'_>) -> Vec<Vec<EdgeId>> {
2199 let keep = hunt.keep.max(1);
2200 let expansion_cap = return_expansion_cap(keep, hunt.graph.edges.len());
2201 let mut heap = BinaryHeap::new();
2202 heap.push(ReturnPathState {
2203 routing_cost_m: 0.0,
2204 distance_m: 0.0,
2205 at: hunt.from,
2206 previous: hunt.previous,
2207 edges: Vec::new(),
2208 used: hunt.barred.clone(),
2209 });
2210 let mut expanded = 0usize;
2211 let mut paths = Vec::new();
2212
2213 while let Some(state) = heap.pop() {
2214 if hunt.monitor.cancelled() {
2215 return Vec::new();
2216 }
2217 if paths.len() >= keep || expanded >= expansion_cap {
2218 break;
2219 }
2220 expanded += 1;
2221 if state.at == hunt.target && !state.edges.is_empty() {
2222 paths.push(state.edges);
2223 continue;
2224 }
2225 for edge_id in &hunt.graph.adjacency[state.at.0] {
2226 if hunt.allowed.is_some_and(|allowed| !allowed[edge_id.0]) {
2227 continue;
2228 }
2229 if state.used.contains(edge_id) {
2230 continue;
2231 }
2232 if !hunt.graph.turn_allowed(state.previous, state.at, *edge_id) {
2233 continue;
2234 }
2235 let Some(edge_cost) = hunt.law.edge_cost(hunt.graph, *edge_id) else {
2236 continue;
2237 };
2238 let edge = &hunt.graph.edges[edge_id.0];
2239 let Some(next) = edge.traverse(state.at) else {
2240 continue;
2241 };
2242 let distance_m = state.distance_m + edge.attr.length_m;
2243 if distance_m > hunt.max_distance_m {
2244 continue;
2245 }
2246 let mut edges = state.edges.clone();
2247 edges.push(*edge_id);
2248 let mut used = state.used.clone();
2249 used.insert(*edge_id);
2250 heap.push(ReturnPathState {
2251 routing_cost_m: state.routing_cost_m + edge_cost,
2252 distance_m,
2253 at: next,
2254 previous: Some(*edge_id),
2255 edges,
2256 used,
2257 });
2258 }
2259 }
2260 paths
2261}
2262
2263fn return_expansion_cap(keep: usize, edge_count: usize) -> usize {
2264 keep.saturating_mul(edge_count.max(1))
2265 .saturating_mul(8)
2266 .max(64)
2267}
2268
2269fn sort_heuristic_fanout(
2270 graph: &WalkGraph,
2271 fanout: &mut [EdgeId],
2272 seed: u64,
2273 depth: usize,
2274 at: VertexId,
2275 law: RoutingLaw,
2276) {
2277 fanout.sort_by(|a, b| {
2278 branch_score(graph, *b, seed, depth, at, law)
2279 .total_cmp(&branch_score(graph, *a, seed, depth, at, law))
2280 .then_with(|| b.cmp(a))
2281 });
2282}
2283
2284fn branch_score(
2285 graph: &WalkGraph,
2286 edge_id: EdgeId,
2287 seed: u64,
2288 depth: usize,
2289 at: VertexId,
2290 law: RoutingLaw,
2291) -> f64 {
2292 let edge = &graph.edges[edge_id.0];
2293 let road_detour_km = law
2294 .edge_cost(graph, edge_id)
2295 .map_or(f64::MAX, |cost| (cost - edge.attr.length_m) / 1_000.0);
2296 (edge.traversal_from(at).lower_limb_load_km + road_detour_km)
2297 .mul_add(1_024.0, seeded_unit(seed, depth, at, edge_id) * 128.0)
2298}
2299
2300fn seeded_unit(seed: u64, depth: usize, at: VertexId, edge_id: EdgeId) -> f64 {
2301 let hash = splitmix64(seed ^ ((depth as u64) << 48) ^ ((at.0 as u64) << 24) ^ edge_id.0 as u64);
2302 let bits = u32::try_from(hash >> 32).expect("shifted splitmix output fits in u32");
2303 f64::from(bits) * (1.0 / f64::from(u32::MAX))
2304}
2305
2306const fn splitmix64(mut x: u64) -> u64 {
2307 x = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
2308 x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2309 x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2310 x ^ (x >> 31)
2311}
2312
2313#[derive(Clone, Debug, PartialEq)]
2314struct ClosureLabel {
2315 routing_cost_m: f64,
2316 distance_m: f64,
2317 predecessor: Option<usize>,
2318 edge: Option<EdgeId>,
2319 live: bool,
2320}
2321
2322impl ClosureLabel {
2323 fn dominates(&self, routing_cost_m: f64, distance_m: f64) -> bool {
2324 self.live && self.routing_cost_m <= routing_cost_m && self.distance_m <= distance_m
2325 }
2326
2327 fn is_dominated_by(&self, routing_cost_m: f64, distance_m: f64) -> bool {
2328 routing_cost_m <= self.routing_cost_m && distance_m <= self.distance_m
2329 }
2330}
2331
2332struct ClosureWorkspace {
2333 skylines: Vec<Vec<usize>>,
2334 touched: Vec<usize>,
2335 labels: Vec<ClosureLabel>,
2336 heap: BinaryHeap<ClosureFrontier>,
2337}
2338
2339impl ClosureWorkspace {
2340 fn new(edge_count: usize) -> Self {
2341 Self {
2342 skylines: vec![Vec::new(); closure_slot_count(edge_count)],
2343 touched: Vec::new(),
2344 labels: Vec::new(),
2345 heap: BinaryHeap::new(),
2346 }
2347 }
2348
2349 fn begin(&mut self) {
2350 for slot in self.touched.drain(..) {
2351 self.skylines[slot].clear();
2352 }
2353 self.labels.clear();
2354 self.heap.clear();
2355 }
2356
2357 fn admit(&mut self, slot: usize, label: ClosureLabel) -> Option<usize> {
2358 let peers = &mut self.skylines[slot];
2359 let pristine = peers.is_empty();
2360 if peers
2361 .iter()
2362 .any(|id| self.labels[*id].dominates(label.routing_cost_m, label.distance_m))
2363 {
2364 return None;
2365 }
2366 for id in peers.iter().copied() {
2367 let incumbent = &mut self.labels[id];
2368 if incumbent.is_dominated_by(label.routing_cost_m, label.distance_m) {
2369 incumbent.live = false;
2370 }
2371 }
2372 peers.retain(|id| self.labels[*id].live);
2373 let id = self.labels.len();
2374 self.labels.push(label);
2375 if pristine {
2376 self.touched.push(slot);
2377 }
2378 peers.push(id);
2379 Some(id)
2380 }
2381}
2382
2383struct ClosureOracle {
2387 cost_m: Vec<f64>,
2388}
2389
2390impl ClosureOracle {
2391 fn forge(
2392 graph: &WalkGraph,
2393 target: VertexId,
2394 allowed: Option<&[bool]>,
2395 law: RoutingLaw,
2396 monitor: &dyn SearchMonitor,
2397 ) -> Option<Self> {
2398 let mut cost_m = vec![f64::INFINITY; closure_slot_count(graph.edges.len())];
2399 let mut heap = BinaryHeap::new();
2400 let mut incoming = vec![Vec::new(); graph.vertices.len()];
2401 for edge in &graph.edges {
2402 if allowed.is_some_and(|allowed| !allowed[edge.id.0])
2403 || law.edge_cost(graph, edge.id).is_none()
2404 {
2405 continue;
2406 }
2407 if edge.traverse(edge.a) == Some(edge.b) {
2408 incoming[edge.b.0].push(edge.id);
2409 }
2410 if edge.traverse(edge.b) == Some(edge.a) {
2411 incoming[edge.a.0].push(edge.id);
2412 }
2413 }
2414 for edge_id in &incoming[target.0] {
2415 let walk = SupportWalk {
2416 at: target,
2417 previous: Some(*edge_id),
2418 };
2419 let slot = closure_slot(graph, walk);
2420 cost_m[slot] = 0.0;
2421 heap.push(SupportFrontier { cost: 0.0, walk });
2422 }
2423
2424 while let Some(SupportFrontier { cost, walk }) = heap.pop() {
2425 if monitor.cancelled() {
2426 return None;
2427 }
2428 if cost > cost_m[closure_slot(graph, walk)] {
2429 continue;
2430 }
2431 let edge_id = walk
2432 .previous
2433 .expect("an oracle frontier always follows an edge");
2434 let edge = &graph.edges[edge_id.0];
2435 let Some(via) = edge.other(walk.at) else {
2436 continue;
2437 };
2438 if edge.traverse(via) != Some(walk.at) {
2439 continue;
2440 }
2441 let edge_cost_m = law
2442 .edge_cost(graph, edge_id)
2443 .expect("an oracle frontier only contains lawful edges");
2444 for prior in &incoming[via.0] {
2445 if *prior == edge_id {
2446 continue;
2447 }
2448 if !graph.turn_allowed(Some(*prior), via, edge_id) {
2449 continue;
2450 }
2451 let predecessor = SupportWalk {
2452 at: via,
2453 previous: Some(*prior),
2454 };
2455 let slot = closure_slot(graph, predecessor);
2456 let candidate_m = cost + edge_cost_m;
2457 if candidate_m < cost_m[slot] {
2458 cost_m[slot] = candidate_m;
2459 heap.push(SupportFrontier {
2460 cost: candidate_m,
2461 walk: predecessor,
2462 });
2463 }
2464 }
2465 }
2466 Some(Self { cost_m })
2467 }
2468
2469 fn cost(&self, graph: &WalkGraph, walk: SupportWalk) -> f64 {
2470 self.cost_m[closure_slot(graph, walk)]
2471 }
2472}
2473
2474fn closure_slot(graph: &WalkGraph, walk: SupportWalk) -> usize {
2475 let Some(edge_id) = walk.previous else {
2476 return graph.edges.len() * 2;
2477 };
2478 let edge = &graph.edges[edge_id.0];
2479 edge_id.0 * 2
2480 + if walk.at == edge.a {
2481 0
2482 } else {
2483 debug_assert_eq!(walk.at, edge.b);
2484 1
2485 }
2486}
2487
2488fn closure_slot_count(edge_count: usize) -> usize {
2489 edge_count
2490 .checked_mul(2)
2491 .and_then(|slots| slots.checked_add(1))
2492 .expect("a trail graph has fewer than usize::MAX / 2 edges")
2493}
2494
2495#[derive(Clone, Copy, Debug, PartialEq)]
2496struct ClosureFrontier {
2497 rank_m: f64,
2498 routing_cost_m: f64,
2499 distance_m: f64,
2500 walk: SupportWalk,
2501 label: usize,
2502}
2503
2504impl Eq for ClosureFrontier {}
2505
2506impl Ord for ClosureFrontier {
2507 fn cmp(&self, rhs: &Self) -> Ordering {
2508 rhs.rank_m
2509 .total_cmp(&self.rank_m)
2510 .then_with(|| rhs.routing_cost_m.total_cmp(&self.routing_cost_m))
2511 .then_with(|| rhs.distance_m.total_cmp(&self.distance_m))
2512 .then_with(|| rhs.walk.cmp(&self.walk))
2513 .then_with(|| rhs.label.cmp(&self.label))
2514 }
2515}
2516
2517impl PartialOrd for ClosureFrontier {
2518 fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
2519 Some(self.cmp(rhs))
2520 }
2521}
2522
2523fn recover_closure_path(labels: &[ClosureLabel], mut label: usize) -> Vec<EdgeId> {
2524 let mut path = Vec::new();
2525 loop {
2526 let current = &labels[label];
2527 let Some(edge) = current.edge else {
2528 break;
2529 };
2530 path.push(edge);
2531 label = current
2532 .predecessor
2533 .expect("every closure edge has a predecessor");
2534 }
2535 path.reverse();
2536 path
2537}
2538
2539fn edge_simple(path: &[EdgeId]) -> bool {
2540 let mut seen = BTreeSet::new();
2541 path.iter().all(|edge| seen.insert(*edge))
2542}
2543
2544#[derive(Clone, Debug, PartialEq)]
2545struct ReturnPathState {
2546 routing_cost_m: f64,
2547 distance_m: f64,
2548 at: VertexId,
2549 previous: Option<EdgeId>,
2550 edges: Vec<EdgeId>,
2551 used: BTreeSet<EdgeId>,
2552}
2553
2554impl Eq for ReturnPathState {}
2555
2556impl Ord for ReturnPathState {
2557 fn cmp(&self, rhs: &Self) -> Ordering {
2558 rhs.routing_cost_m
2559 .total_cmp(&self.routing_cost_m)
2560 .then_with(|| rhs.distance_m.total_cmp(&self.distance_m))
2561 .then_with(|| rhs.at.cmp(&self.at))
2562 .then_with(|| rhs.edges.cmp(&self.edges))
2563 }
2564}
2565
2566impl PartialOrd for ReturnPathState {
2567 fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
2568 Some(self.cmp(rhs))
2569 }
2570}
2571
2572fn mirrored_route(edges: &[EdgeId]) -> Vec<EdgeId> {
2573 edges
2574 .iter()
2575 .copied()
2576 .chain(edges.iter().rev().copied())
2577 .collect()
2578}
2579
2580#[cfg(test)]
2581mod tests {
2582 use super::*;
2583 use crate::{
2584 Access, Coord, CrossingControl, EdgeTravel, GeometryClaim, GraphBuilder, JunctionPolicy,
2585 LineString, Provenance, SegmentDraft, Terrain, TrailStanding, WayKind, WayRealm,
2586 };
2587 use std::cell::{Cell, RefCell};
2588
2589 struct RecordingMonitor {
2590 cancel_after: usize,
2591 checks: Cell<usize>,
2592 progress: RefCell<Vec<SearchProgress>>,
2593 previews: RefCell<Vec<usize>>,
2594 }
2595
2596 impl RecordingMonitor {
2597 fn patient() -> Self {
2598 Self {
2599 cancel_after: usize::MAX,
2600 checks: Cell::new(0),
2601 progress: RefCell::new(Vec::new()),
2602 previews: RefCell::new(Vec::new()),
2603 }
2604 }
2605 }
2606
2607 impl SearchMonitor for RecordingMonitor {
2608 fn cancelled(&self) -> bool {
2609 let checks = self.checks.get().saturating_add(1);
2610 self.checks.set(checks);
2611 checks >= self.cancel_after
2612 }
2613
2614 fn report(&self, progress: SearchProgress) {
2615 self.progress.borrow_mut().push(progress);
2616 }
2617
2618 fn preview(&self, routes: &[Route]) {
2619 self.previews.borrow_mut().push(routes.len());
2620 }
2621 }
2622
2623 fn branch(points: Vec<Coord>, name: &str) -> SegmentDraft {
2624 SegmentDraft {
2625 geometry: LineString::new(points).expect("valid branch"),
2626 junctions: JunctionPolicy::Planar,
2627 turn_ref: None,
2628 junction_keys: None,
2629 turn_restrictions: Vec::new(),
2630 way_kind: WayKind::Path,
2631 realm: WayRealm::default(),
2632 geometry_claim: GeometryClaim::default(),
2633 crossing_control: CrossingControl::default(),
2634 standing: TrailStanding::Established,
2635 marking: crate::TrailMarking::default(),
2636 terrain: Terrain::Trail,
2637 terrain_confidence: Some(1.0),
2638 surface: None,
2639 access: Access::Open,
2640 travel: EdgeTravel::Both,
2641 road_exposure: 0.0,
2642 confidence: 1.0,
2643 provenance: vec![Provenance::fixture(name)],
2644 }
2645 }
2646
2647 fn named_edge(graph: &WalkGraph, name: &str) -> EdgeId {
2648 graph
2649 .edges
2650 .iter()
2651 .find(|edge| {
2652 edge.attr
2653 .provenance
2654 .iter()
2655 .any(|source| source.source_id.as_deref() == Some(name))
2656 })
2657 .map(|edge| edge.id)
2658 .expect("fixture edge exists")
2659 }
2660
2661 #[test]
2662 fn support_lower_bounds_target_the_feasibility_floor() {
2663 let low = Coord::new(0.001, 0.0);
2664 let midpoint = Coord::new(0.002, 0.0);
2665 let graph = GraphBuilder::default()
2666 .build(&[branch(
2667 vec![Coord::new(0.0, 0.0), low, midpoint],
2668 "lower-bound-axis",
2669 )])
2670 .expect("build lower-bound fixture");
2671 let low = graph.nearest_vertex(low).expect("low landmark");
2672 let midpoint = graph.nearest_vertex(midpoint).expect("midpoint landmark");
2673 let mut radial = vec![f64::INFINITY; graph.vertices.len()];
2674 radial[low.0] = 25_000.0;
2675 radial[midpoint.0] = 37_500.0;
2676 let constraints = LoopConstraints {
2677 min_distance_m: 50_000.0,
2678 max_distance_m: 100_000.0,
2679 ..LoopConstraints::default()
2680 };
2681
2682 let designs = support_designs(
2683 &graph,
2684 &radial,
2685 &[low, midpoint],
2686 &constraints,
2687 SearchParams {
2688 keep: 1,
2689 ..SearchParams::default()
2690 },
2691 );
2692
2693 assert_eq!(designs[0].supports, [low]);
2694 assert!((designs[0].lower_bound_m - constraints.min_distance_m).abs() <= f64::EPSILON);
2695 }
2696
2697 #[test]
2698 fn routing_skeleton_crushes_shape_points_without_losing_support() {
2699 let corners = [
2700 Coord::new(0.0, 0.0),
2701 Coord::new(0.01, 0.0),
2702 Coord::new(0.01, 0.01),
2703 Coord::new(0.0, 0.01),
2704 Coord::new(0.0, 0.0),
2705 ];
2706 let mut points = Vec::new();
2707 for side in corners.windows(2) {
2708 for step in 0..40_u32 {
2709 let t = f64::from(step) / 40.0;
2710 points.push(Coord::new(
2711 (side[1].lon - side[0].lon).mul_add(t, side[0].lon),
2712 (side[1].lat - side[0].lat).mul_add(t, side[0].lat),
2713 ));
2714 }
2715 }
2716 points.push(corners[4]);
2717 let graph = GraphBuilder::default()
2718 .build(&[branch(points, "fine-ring")])
2719 .expect("build fine ring");
2720 let start = graph.nearest_vertex(corners[0]).expect("ring origin");
2721 let skeleton = RoutingSkeleton::forge_preserving(
2722 SearchScope::all(&graph),
2723 start,
2724 RoutingLaw::default(),
2725 std::iter::empty(),
2726 );
2727
2728 assert_eq!(graph.edges.len(), 160);
2729 assert_eq!(skeleton.arcs.len(), 3);
2730 assert_eq!(
2731 skeleton
2732 .arcs
2733 .iter()
2734 .map(|arc| arc.edges.len())
2735 .sum::<usize>(),
2736 graph.edges.len()
2737 );
2738 }
2739
2740 #[test]
2741 fn warmed_revisions_obey_required_and_forbidden_segments() {
2742 let ring = [
2743 Coord::new(0.0, 0.0),
2744 Coord::new(0.002, 0.0),
2745 Coord::new(0.002, 0.002),
2746 Coord::new(0.0, 0.002),
2747 Coord::new(0.0, 0.0),
2748 ];
2749 let graph = GraphBuilder::default()
2750 .build(&[branch(ring.to_vec(), "ring")])
2751 .expect("build ring");
2752 let start = graph.nearest_vertex(ring[0]).expect("ring origin");
2753 let constraints = LoopConstraints {
2754 min_distance_m: 0.0,
2755 max_distance_m: 10_000.0,
2756 max_lower_limb_load_km: f64::MAX,
2757 max_repeated_edge_fraction: 0.0,
2758 allowed_shapes: vec![RouteShape::Loop],
2759 ..LoopConstraints::default()
2760 };
2761 let params = SearchParams {
2762 max_frontier: 1_000,
2763 keep: 12,
2764 ..SearchParams::default()
2765 };
2766 let incumbents = SolverKind::Auto.solve(params, &graph, start, &constraints, 3);
2767 let incumbent = incumbents.first().expect("ring yields a loop");
2768 let compulsory = incumbent.edges[0];
2769 let mut edicts = EdgeEdicts::default();
2770 edicts.toggle_required(compulsory);
2771 let revised = SolverKind::Auto.revise_scoped(
2772 params,
2773 SearchScope::all(&graph),
2774 start,
2775 &constraints,
2776 3,
2777 &edicts,
2778 &[],
2779 &(),
2780 );
2781 assert!(!revised.is_empty());
2782 assert!(revised.iter().all(|route| edicts.admits(route)));
2783
2784 edicts.toggle_forbidden(compulsory);
2785 assert_eq!(edicts.disposition(compulsory), EdgeDisposition::Forbidden);
2786 let revised = SolverKind::Auto.revise_scoped(
2787 params,
2788 SearchScope::all(&graph),
2789 start,
2790 &constraints,
2791 3,
2792 &edicts,
2793 &incumbents,
2794 &(),
2795 );
2796 assert!(
2797 revised
2798 .iter()
2799 .all(|route| !route.edges.contains(&compulsory))
2800 );
2801 edicts.toggle_forbidden(compulsory);
2802 assert_eq!(edicts.disposition(compulsory), EdgeDisposition::Free);
2803 }
2804
2805 #[test]
2806 fn closure_oracle_preserves_cost_distance_pareto_labels() {
2807 let approach = Coord::new(-0.001, 0.0);
2808 let from = Coord::new(0.0, 0.0);
2809 let bend = Coord::new(0.002, 0.003);
2810 let junction = Coord::new(0.004, 0.0);
2811 let merge = Coord::new(0.005, 0.0);
2812 let target = Coord::new(0.008, 0.0);
2813 let mut road = branch(vec![from, junction], "short-road");
2814 road.way_kind = WayKind::Roadway;
2815 road.terrain = Terrain::Road;
2816 road.road_exposure = 1.0;
2817 let graph = GraphBuilder::default()
2818 .build(&[
2819 branch(vec![approach, from], "approach"),
2820 branch(vec![from, bend, junction], "long-clean"),
2821 road,
2822 branch(vec![junction, merge], "common"),
2823 branch(vec![merge, target], "tail"),
2824 ])
2825 .expect("build cost-distance closure fixture");
2826 let from = graph.nearest_vertex(from).expect("closure origin");
2827 let target = graph.nearest_vertex(target).expect("closure target");
2828 let approach = named_edge(&graph, "approach");
2829 let road = named_edge(&graph, "short-road");
2830 let barred = BTreeSet::from([approach]);
2831 let law = RoutingLaw { road_aversion: 2.0 };
2832 let oracle =
2833 ClosureOracle::forge(&graph, target, None, law, &()).expect("forge closure oracle");
2834 let mut workspace = ClosureWorkspace::new(graph.edges.len());
2835 let paths = shortest_return_paths(
2836 ReturnHunt {
2837 graph: &graph,
2838 from,
2839 target,
2840 previous: Some(approach),
2841 barred: &barred,
2842 max_distance_m: 1_000.0,
2843 keep: 1,
2844 law,
2845 monitor: &(),
2846 allowed: None,
2847 },
2848 &mut workspace,
2849 Some(&oracle),
2850 );
2851
2852 assert_eq!(paths.len(), 1);
2853 let path = &paths[0];
2854 assert_eq!(graph.walk_edges(from, path), Some(target));
2855 assert!(edge_simple(path));
2856 assert!(path.contains(&road));
2857 assert!(route_distance(&graph, path) <= 1_000.0);
2858 }
2859
2860 #[test]
2861 fn out_and_back_portfolio_spends_slots_on_distinct_spines_first() {
2862 let origin = Coord::new(0.0, 0.0);
2863 let graph = GraphBuilder::default()
2864 .build(&[
2865 branch(
2866 vec![
2867 origin,
2868 Coord::new(0.001, 0.0),
2869 Coord::new(0.002, 0.0),
2870 Coord::new(0.003, 0.0),
2871 ],
2872 "east",
2873 ),
2874 branch(
2875 vec![
2876 origin,
2877 Coord::new(0.0, 0.001),
2878 Coord::new(0.0, 0.002),
2879 Coord::new(0.0, 0.003),
2880 ],
2881 "north",
2882 ),
2883 branch(
2884 vec![
2885 origin,
2886 Coord::new(-0.001, 0.0),
2887 Coord::new(-0.002, 0.0),
2888 Coord::new(-0.003, 0.0),
2889 ],
2890 "west",
2891 ),
2892 ])
2893 .expect("build branching graph");
2894 let start = graph.nearest_vertex(origin).expect("origin vertex");
2895 let constraints = LoopConstraints {
2896 min_distance_m: 200.0,
2897 max_distance_m: 800.0,
2898 max_lower_limb_load_km: f64::MAX,
2899 max_repeated_edge_fraction: 1.0,
2900 allowed_shapes: vec![RouteShape::OutAndBack],
2901 ..LoopConstraints::default()
2902 };
2903 let routes = SolverKind::Auto.solve(
2904 SearchParams {
2905 max_hops: 4,
2906 max_frontier: 100,
2907 keep: 12,
2908 closure_paths: 1,
2909 seed: 0,
2910 routing: RoutingLaw::default(),
2911 },
2912 &graph,
2913 start,
2914 &constraints,
2915 3,
2916 );
2917 assert_eq!(routes.len(), 3);
2918 assert_eq!(
2919 routes
2920 .iter()
2921 .map(|route| route.edges[0])
2922 .collect::<BTreeSet<_>>()
2923 .len(),
2924 3
2925 );
2926 }
2927
2928 #[test]
2929 fn restricted_scope_never_leaks_a_forbidden_edge() {
2930 let origin = Coord::new(0.0, 0.0);
2931 let graph = GraphBuilder::default()
2932 .build(&[
2933 branch(
2934 vec![origin, Coord::new(0.001, 0.0), Coord::new(0.002, 0.0)],
2935 "allowed",
2936 ),
2937 branch(
2938 vec![origin, Coord::new(0.0, 0.001), Coord::new(0.0, 0.002)],
2939 "forbidden",
2940 ),
2941 ])
2942 .expect("build scoped graph");
2943 let allowed = graph
2944 .edges
2945 .iter()
2946 .map(|edge| {
2947 edge.geometry
2948 .points
2949 .iter()
2950 .all(|point| point.lat.abs() < f64::EPSILON)
2951 })
2952 .collect::<Vec<_>>();
2953 let constraints = LoopConstraints {
2954 min_distance_m: 100.0,
2955 max_distance_m: 1_000.0,
2956 max_lower_limb_load_km: f64::MAX,
2957 max_repeated_edge_fraction: 1.0,
2958 allowed_shapes: vec![RouteShape::OutAndBack],
2959 ..LoopConstraints::default()
2960 };
2961 let routes = SolverKind::Auto.solve_scoped(
2962 SearchParams::default(),
2963 SearchScope::restricted(&graph, &allowed),
2964 graph.nearest_vertex(origin).expect("origin vertex"),
2965 &constraints,
2966 3,
2967 &(),
2968 );
2969
2970 assert!(!routes.is_empty());
2971 assert!(
2972 routes
2973 .iter()
2974 .flat_map(|route| &route.edges)
2975 .all(|edge| allowed[edge.0])
2976 );
2977 }
2978
2979 #[test]
2980 fn diversity_never_spends_a_slot_on_a_near_miss_while_matches_remain() {
2981 let origin = Coord::new(0.0, 0.0);
2982 let graph = GraphBuilder::default()
2983 .build(&[
2984 branch(
2985 vec![
2986 origin,
2987 Coord::new(0.001, 0.0),
2988 Coord::new(0.002, 0.0),
2989 Coord::new(0.003, 0.0),
2990 ],
2991 "exact-spine",
2992 ),
2993 branch(vec![origin, Coord::new(0.0, 0.0004)], "short-near-miss"),
2994 ])
2995 .expect("build tiered graph");
2996 let start = graph.nearest_vertex(origin).expect("origin vertex");
2997 let constraints = LoopConstraints {
2998 min_distance_m: 200.0,
2999 max_distance_m: 800.0,
3000 max_lower_limb_load_km: f64::MAX,
3001 max_repeated_edge_fraction: 1.0,
3002 allowed_shapes: vec![RouteShape::OutAndBack],
3003 ..LoopConstraints::default()
3004 };
3005 let routes = SolverKind::Auto.solve(
3006 SearchParams {
3007 keep: 12,
3008 max_frontier: 100,
3009 ..SearchParams::default()
3010 },
3011 &graph,
3012 start,
3013 &constraints,
3014 3,
3015 );
3016 assert_eq!(routes.len(), 3);
3017 assert!(routes.iter().all(|route| route.verdict.satisfied));
3018 }
3019
3020 #[test]
3021 fn monitored_search_reports_monotone_effort_and_ranking() {
3022 let origin = Coord::new(0.0, 0.0);
3023 let graph = GraphBuilder::default()
3024 .build(&[branch(
3025 vec![
3026 origin,
3027 Coord::new(0.001, 0.0),
3028 Coord::new(0.002, 0.0),
3029 Coord::new(0.003, 0.0),
3030 ],
3031 "monitored",
3032 )])
3033 .expect("build monitored graph");
3034 let start = graph.nearest_vertex(origin).expect("origin vertex");
3035 let constraints = LoopConstraints {
3036 min_distance_m: 0.0,
3037 max_distance_m: 1_000.0,
3038 max_lower_limb_load_km: f64::MAX,
3039 max_repeated_edge_fraction: 1.0,
3040 allowed_shapes: vec![RouteShape::OutAndBack],
3041 ..LoopConstraints::default()
3042 };
3043 let monitor = RecordingMonitor::patient();
3044
3045 let _routes = SolverKind::Auto.solve_monitored(
3046 SearchParams {
3047 max_frontier: 100,
3048 ..SearchParams::default()
3049 },
3050 &graph,
3051 start,
3052 &constraints,
3053 3,
3054 &monitor,
3055 );
3056
3057 let progress = monitor.progress.borrow();
3058 assert_eq!(
3059 progress.first().map(|progress| progress.stage),
3060 Some(SearchStage::Exploring)
3061 );
3062 assert_eq!(
3063 progress.last().map(|progress| progress.stage),
3064 Some(SearchStage::Ranking)
3065 );
3066 assert!(
3067 progress
3068 .windows(2)
3069 .all(|pair| pair[0].explored <= pair[1].explored)
3070 );
3071 assert!(progress.iter().all(|progress| progress.explored <= 100));
3072 let previews = monitor.previews.borrow();
3073 assert!(previews.first().is_some_and(|count| *count > 0));
3074 assert!(previews.windows(2).all(|pair| pair[0] < pair[1]));
3075 assert_eq!(
3076 previews.last().copied(),
3077 progress.last().map(|progress| progress.candidates)
3078 );
3079 }
3080
3081 #[test]
3082 fn monitored_search_obeys_cooperative_cancellation() {
3083 let origin = Coord::new(0.0, 0.0);
3084 let graph = GraphBuilder::default()
3085 .build(&[branch(
3086 vec![
3087 origin,
3088 Coord::new(0.001, 0.0),
3089 Coord::new(0.002, 0.0),
3090 Coord::new(0.003, 0.0),
3091 ],
3092 "cancelled",
3093 )])
3094 .expect("build cancellable graph");
3095 let start = graph.nearest_vertex(origin).expect("origin vertex");
3096 let constraints = LoopConstraints {
3097 min_distance_m: 0.0,
3098 max_distance_m: 1_000.0,
3099 max_lower_limb_load_km: f64::MAX,
3100 max_repeated_edge_fraction: 1.0,
3101 allowed_shapes: vec![RouteShape::OutAndBack],
3102 ..LoopConstraints::default()
3103 };
3104 let monitor = RecordingMonitor {
3105 cancel_after: 3,
3106 checks: Cell::new(0),
3107 progress: RefCell::new(Vec::new()),
3108 previews: RefCell::new(Vec::new()),
3109 };
3110
3111 let routes = SolverKind::Auto.solve_monitored(
3112 SearchParams::default(),
3113 &graph,
3114 start,
3115 &constraints,
3116 3,
3117 &monitor,
3118 );
3119
3120 assert!(routes.is_empty());
3121 assert!(
3122 monitor
3123 .progress
3124 .borrow()
3125 .iter()
3126 .all(|progress| progress.stage != SearchStage::Ranking)
3127 );
3128 }
3129}