Skip to main content

osmnxr_core/
routing.rs

1//! Shortest-path routing over a [`Graph`].
2
3use crate::graph::Graph;
4use std::cmp::Ordering;
5use std::collections::{BinaryHeap, HashMap, HashSet};
6
7/// Min-heap entry ordered by ascending distance.
8struct State {
9    dist: f64,
10    node: usize,
11}
12
13impl PartialEq for State {
14    fn eq(&self, other: &Self) -> bool {
15        self.dist == other.dist
16    }
17}
18impl Eq for State {}
19impl PartialOrd for State {
20    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
21        Some(self.cmp(other))
22    }
23}
24impl Ord for State {
25    fn cmp(&self, other: &Self) -> Ordering {
26        // Reverse so `BinaryHeap` (a max-heap) yields the smallest distance.
27        other
28            .dist
29            .partial_cmp(&self.dist)
30            .unwrap_or(Ordering::Equal)
31    }
32}
33
34/// Dijkstra single-source shortest distances. Returns a vector of length
35/// `n_nodes`; unreachable nodes are `f64::INFINITY`.
36pub fn dijkstra(graph: &Graph, source: usize) -> Vec<f64> {
37    let mut dist = vec![f64::INFINITY; graph.n_nodes];
38    dist[source] = 0.0;
39    let mut heap = BinaryHeap::new();
40    heap.push(State {
41        dist: 0.0,
42        node: source,
43    });
44    while let Some(State { dist: d, node }) = heap.pop() {
45        if d > dist[node] {
46            continue;
47        }
48        for (next, w) in graph.neighbors(node) {
49            let nd = d + w;
50            if nd < dist[next] {
51                dist[next] = nd;
52                heap.push(State {
53                    dist: nd,
54                    node: next,
55                });
56            }
57        }
58    }
59    dist
60}
61
62/// Dijkstra with predecessor tracking, returning the shortest path from
63/// `source` to `target` as `(node path, traversed CSR slots, cost)`, or
64/// `None` when the target is unreachable.
65pub fn shortest_path_route(
66    graph: &Graph,
67    source: usize,
68    target: usize,
69) -> Option<(Vec<usize>, Vec<usize>, f64)> {
70    dijkstra_combined(graph, graph, 0.0, source, target).map(|(p, e, c, _)| (p, e, c))
71}
72
73/// Dijkstra from `source` to `target` that ignores `blocked_nodes` and the
74/// directed edges in `blocked_edges`. Returns `(path, cost)`; an empty path and
75/// `f64::INFINITY` when the target is unreachable. Used by Yen's algorithm.
76fn shortest_path_blocked(
77    graph: &Graph,
78    source: usize,
79    target: usize,
80    blocked_nodes: &HashSet<usize>,
81    blocked_edges: &HashSet<(usize, usize)>,
82) -> (Vec<usize>, f64) {
83    let mut dist = vec![f64::INFINITY; graph.n_nodes];
84    let mut prev = vec![usize::MAX; graph.n_nodes];
85    dist[source] = 0.0;
86    let mut heap = BinaryHeap::new();
87    heap.push(State {
88        dist: 0.0,
89        node: source,
90    });
91    while let Some(State { dist: d, node }) = heap.pop() {
92        if node == target {
93            break;
94        }
95        if d > dist[node] {
96            continue;
97        }
98        for (next, w) in graph.neighbors(node) {
99            if blocked_nodes.contains(&next) || blocked_edges.contains(&(node, next)) {
100                continue;
101            }
102            let nd = d + w;
103            if nd < dist[next] {
104                dist[next] = nd;
105                prev[next] = node;
106                heap.push(State {
107                    dist: nd,
108                    node: next,
109                });
110            }
111        }
112    }
113    if dist[target].is_infinite() {
114        return (Vec::new(), f64::INFINITY);
115    }
116    let mut path = vec![target];
117    let mut cur = target;
118    while cur != source {
119        cur = prev[cur];
120        path.push(cur);
121    }
122    path.reverse();
123    (path, dist[target])
124}
125
126/// Input index of the minimum-weight directed edge for each `(u, v)` pair,
127/// for scoring paths and recovering traversed edges.
128fn edge_argmin_map(from: &[usize], to: &[usize], weight: &[f64]) -> HashMap<(usize, usize), usize> {
129    let mut m: HashMap<(usize, usize), usize> = HashMap::new();
130    for i in 0..from.len() {
131        let key = (from[i], to[i]);
132        match m.entry(key) {
133            std::collections::hash_map::Entry::Occupied(mut e) => {
134                if weight[i] < weight[*e.get()] {
135                    e.insert(i);
136                }
137            }
138            std::collections::hash_map::Entry::Vacant(e) => {
139                e.insert(i);
140            }
141        }
142    }
143    m
144}
145
146fn path_cost(path: &[usize], amap: &HashMap<(usize, usize), usize>, weight: &[f64]) -> f64 {
147    let mut c = 0.0;
148    for w in path.windows(2) {
149        c += amap
150            .get(&(w[0], w[1]))
151            .map(|&i| weight[i])
152            .unwrap_or(f64::INFINITY);
153    }
154    c
155}
156
157/// Input indices of the (minimum-weight) edges along a node path.
158fn path_edges(path: &[usize], amap: &HashMap<(usize, usize), usize>) -> Vec<usize> {
159    path.windows(2)
160        .filter_map(|w| amap.get(&(w[0], w[1])).copied())
161        .collect()
162}
163
164/// A route as node path plus the input-order indices of the edges traversed
165/// (so callers can recover attributes/geometry, parallel edges included).
166pub struct Route {
167    pub path: Vec<usize>,
168    pub edges: Vec<usize>,
169    pub cost: f64,
170}
171
172/// Yen's algorithm for the `k` loopless shortest paths from `source` to
173/// `target`. Returns up to `k` routes in increasing cost order; Yen operates
174/// on the simple-graph abstraction where parallel edges collapse to the
175/// minimum weight, so each route's `edges` are those minimum-weight edges.
176pub fn k_shortest_paths(
177    graph: &Graph,
178    from: &[usize],
179    to: &[usize],
180    weight: &[f64],
181    source: usize,
182    target: usize,
183    k: usize,
184) -> Vec<Route> {
185    let amap = edge_argmin_map(from, to, weight);
186    let empty_n: HashSet<usize> = HashSet::new();
187    let empty_e: HashSet<(usize, usize)> = HashSet::new();
188
189    let mut accepted: Vec<(Vec<usize>, f64)> = Vec::new();
190    let (first, first_cost) = shortest_path_blocked(graph, source, target, &empty_n, &empty_e);
191    if first.is_empty() {
192        return Vec::new();
193    }
194    accepted.push((first, first_cost));
195
196    let mut candidates: Vec<(Vec<usize>, f64)> = Vec::new();
197    while accepted.len() < k {
198        let last = &accepted[accepted.len() - 1].0;
199        for i in 0..last.len() - 1 {
200            let spur_node = last[i];
201            let root_path = &last[0..=i];
202
203            let mut blocked_edges: HashSet<(usize, usize)> = HashSet::new();
204            for (p, _) in &accepted {
205                if p.len() > i && &p[0..=i] == root_path {
206                    blocked_edges.insert((p[i], p[i + 1]));
207                }
208            }
209            let mut blocked_nodes: HashSet<usize> = HashSet::new();
210            for &n in &root_path[0..root_path.len() - 1] {
211                blocked_nodes.insert(n);
212            }
213
214            let (spur, _) =
215                shortest_path_blocked(graph, spur_node, target, &blocked_nodes, &blocked_edges);
216            if spur.is_empty() {
217                continue;
218            }
219            let mut total = root_path[0..root_path.len() - 1].to_vec();
220            total.extend_from_slice(&spur);
221            let cost = path_cost(&total, &amap, weight);
222            if !candidates.iter().any(|(p, _)| *p == total)
223                && !accepted.iter().any(|(p, _)| *p == total)
224            {
225                candidates.push((total, cost));
226            }
227        }
228        if candidates.is_empty() {
229            break;
230        }
231        candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
232        accepted.push(candidates.remove(0));
233    }
234    accepted
235        .into_iter()
236        .map(|(path, cost)| {
237            let edges = path_edges(&path, &amap);
238            Route { path, edges, cost }
239        })
240        .collect()
241}
242
243/// Row-major shortest-distance matrix from each source to each target.
244pub fn distance_matrix(graph: &Graph, sources: &[usize], targets: &[usize]) -> Vec<f64> {
245    let mut out = Vec::with_capacity(sources.len() * targets.len());
246    for &s in sources {
247        let dist = dijkstra(graph, s);
248        for &t in targets {
249            out.push(dist[t]);
250        }
251    }
252    out
253}
254
255/// Cost of the arc `i -> j` in a row-major `n x n` cost matrix.
256#[inline]
257fn arc(cost: &[f64], n: usize, i: usize, j: usize) -> f64 {
258    cost[i * n + j]
259}
260
261/// Total cost of a tour over a row-major `n x n` cost matrix; the closing arc
262/// back to the first stop is included when `closed`.
263fn tour_cost(cost: &[f64], n: usize, tour: &[usize], closed: bool) -> f64 {
264    let mut c = 0.0;
265    for w in tour.windows(2) {
266        c += arc(cost, n, w[0], w[1]);
267    }
268    if closed && tour.len() > 1 {
269        c += arc(cost, n, tour[tour.len() - 1], tour[0]);
270    }
271    c
272}
273
274/// Nearest-neighbour tour construction from `start`. A fixed `end` (for an
275/// open route) is reserved and appended last.
276fn nearest_neighbor_tour(cost: &[f64], n: usize, start: usize, end: Option<usize>) -> Vec<usize> {
277    let mut visited = vec![false; n];
278    let mut tour = Vec::with_capacity(n);
279    tour.push(start);
280    visited[start] = true;
281    let reserved = match end {
282        Some(e) if e != start => {
283            visited[e] = true;
284            1
285        }
286        _ => 0,
287    };
288    while tour.len() < n - reserved {
289        let last = *tour.last().unwrap();
290        let mut best = usize::MAX;
291        let mut best_cost = f64::INFINITY;
292        for (j, &seen) in visited.iter().enumerate() {
293            if !seen && (best == usize::MAX || arc(cost, n, last, j) < best_cost) {
294                best = j;
295                best_cost = arc(cost, n, last, j);
296            }
297        }
298        visited[best] = true;
299        tour.push(best);
300    }
301    if let Some(e) = end {
302        if e != start {
303            tour.push(e);
304        }
305    }
306    tour
307}
308
309/// Local search over three move families, each evaluated by recomputing the
310/// candidate's full tour cost, which keeps every move exact on asymmetric
311/// matrices: or-opt (relocate a segment of 1..=3 stops, forward or reversed),
312/// pairwise swaps, and 2-opt segment reversals. The first stop stays fixed
313/// (and the last, when `fix_end`). Repeats until a full pass yields no strict
314/// improvement; returns the final tour cost.
315fn local_search(cost: &[f64], n: usize, tour: &mut Vec<usize>, closed: bool, fix_end: bool) -> f64 {
316    let len = tour.len();
317    let mut best_cost = tour_cost(cost, n, tour, closed);
318    if len < 3 {
319        return best_cost;
320    }
321    let tail = if fix_end { 1 } else { 0 };
322    loop {
323        let mut improved = false;
324        // or-opt: relocate segments of 1..=3 stops, forward or reversed
325        for seg_len in 1..=3usize.min(len - 2) {
326            if 1 + seg_len + tail > len {
327                break;
328            }
329            for i in 1..=(len - seg_len - tail) {
330                let segment: Vec<usize> = tour[i..i + seg_len].to_vec();
331                let mut rest: Vec<usize> = Vec::with_capacity(len - seg_len);
332                rest.extend_from_slice(&tour[..i]);
333                rest.extend_from_slice(&tour[i + seg_len..]);
334                let last_slot = rest.len() - tail;
335                for j in 1..=last_slot {
336                    for reversed in [false, true] {
337                        if reversed && seg_len == 1 {
338                            continue;
339                        }
340                        let mut cand = Vec::with_capacity(len);
341                        cand.extend_from_slice(&rest[..j]);
342                        if reversed {
343                            cand.extend(segment.iter().rev());
344                        } else {
345                            cand.extend_from_slice(&segment);
346                        }
347                        cand.extend_from_slice(&rest[j..]);
348                        let c = tour_cost(cost, n, &cand, closed);
349                        if c < best_cost {
350                            best_cost = c;
351                            *tour = cand;
352                            improved = true;
353                        }
354                    }
355                }
356            }
357        }
358        // swaps and 2-opt reversals over movable positions
359        let hi = len - tail;
360        for i in 1..hi {
361            for j in (i + 1)..hi {
362                let mut cand = tour.clone();
363                cand.swap(i, j);
364                let c = tour_cost(cost, n, &cand, closed);
365                if c < best_cost {
366                    best_cost = c;
367                    *tour = cand;
368                    improved = true;
369                }
370                let mut cand = tour.clone();
371                cand[i..=j].reverse();
372                let c = tour_cost(cost, n, &cand, closed);
373                if c < best_cost {
374                    best_cost = c;
375                    *tour = cand;
376                    improved = true;
377                }
378            }
379        }
380        if !improved {
381            break;
382        }
383    }
384    best_cost
385}
386
387/// Tiny deterministic xorshift64 RNG, so results are reproducible and the R
388/// session's RNG stream is untouched.
389struct Rng(u64);
390
391impl Rng {
392    fn next(&mut self) -> u64 {
393        let mut x = self.0;
394        x ^= x << 13;
395        x ^= x >> 7;
396        x ^= x << 17;
397        self.0 = x;
398        x
399    }
400
401    /// Uniform draw in `lo..hi` (hi exclusive).
402    fn range(&mut self, lo: usize, hi: usize) -> usize {
403        lo + (self.next() % (hi - lo) as u64) as usize
404    }
405}
406
407/// Double-bridge perturbation: cuts the tour after the fixed first stop into
408/// four consecutive parts A B C D and reassembles them as A C B D. Segment
409/// orientation is preserved, so the kick is valid on asymmetric matrices, and
410/// the first and last positions stay put.
411fn double_bridge(rng: &mut Rng, tour: &[usize]) -> Vec<usize> {
412    let len = tour.len();
413    if len < 4 {
414        return tour.to_vec();
415    }
416    let mut cuts = [rng.range(1, len), rng.range(1, len), rng.range(1, len)];
417    cuts.sort_unstable();
418    let [a, b, c] = cuts;
419    if a == b || b == c {
420        return tour.to_vec();
421    }
422    let mut out = Vec::with_capacity(len);
423    out.extend_from_slice(&tour[..a]);
424    out.extend_from_slice(&tour[b..c]);
425    out.extend_from_slice(&tour[a..b]);
426    out.extend_from_slice(&tour[c..]);
427    out
428}
429
430/// Heuristic travelling-salesman tour over a row-major `n x n` cost matrix.
431/// Builds a nearest-neighbour tour and, when `improve`, polishes it with an
432/// iterated local search: or-opt/swap/2-opt descent restarted from
433/// double-bridge perturbations, keeping the best tour found. The tour starts
434/// at `start`; when `closed` it returns there (and `end` is ignored),
435/// otherwise a fixed `end` pins the final stop and `None` leaves it free.
436/// Returns the visiting order as indices into the matrix.
437pub fn tsp_tour(
438    cost: &[f64],
439    n: usize,
440    start: usize,
441    end: Option<usize>,
442    closed: bool,
443    improve: bool,
444) -> Vec<usize> {
445    if n == 0 {
446        return Vec::new();
447    }
448    if n == 1 {
449        return vec![start];
450    }
451    // Unreachable pairs come in as Inf; use a large finite penalty instead so
452    // cost comparisons stay well-defined (the R layer rejects routes that
453    // still contain such arcs).
454    let cost: Vec<f64> = cost
455        .iter()
456        .map(|&c| if c.is_finite() { c } else { 1e15 })
457        .collect();
458    let end = if closed { None } else { end };
459    let mut tour = nearest_neighbor_tour(&cost, n, start, end);
460    if !improve {
461        return tour;
462    }
463    let fix_end = end.is_some();
464    let mut best_cost = local_search(&cost, n, &mut tour, closed, fix_end);
465
466    // Iterated local search with a work budget that shrinks as the O(n^3)
467    // descent grows, and an early stop after a stretch of fruitless kicks.
468    let kicks = (100_000_000 / (n * n * n).max(1)).clamp(25, 400);
469    let patience = (kicks / 4).max(20);
470    let mut rng = Rng(0x9E3779B97F4A7C15);
471    let mut since_improved = 0usize;
472    for _ in 0..kicks {
473        let mut cand = double_bridge(&mut rng, &tour);
474        let c = local_search(&cost, n, &mut cand, closed, fix_end);
475        if c < best_cost {
476            best_cost = c;
477            tour = cand;
478            since_improved = 0;
479        } else {
480            since_improved += 1;
481            if since_improved >= patience {
482                break;
483            }
484        }
485    }
486    tour
487}
488
489/// A Pareto-optimal route found by [`pareto_paths`]. `edges` holds the
490/// input-order indices of the edges actually traversed, so callers can
491/// recover attributes/geometry exactly even across parallel edges.
492pub struct ParetoRoute {
493    pub path: Vec<usize>,
494    pub edges: Vec<usize>,
495    pub cost: f64,
496    pub resource: f64,
497}
498
499/// A label in the bicriteria search: the node reached, the CSR slot of the
500/// edge that reached it (`usize::MAX` for the source) and the arena index of
501/// the predecessor label (`usize::MAX` for the source). The accumulated
502/// cost/resource pair travels in the heap entry instead.
503struct Label {
504    node: usize,
505    edge: usize,
506    pred: usize,
507}
508
509/// Input-edge index occupying each CSR slot of the graph that
510/// [`Graph::from_edges`] builds from `from` (it replays the same
511/// construction order).
512pub fn csr_edge_order(from: &[usize], n_nodes: usize) -> Vec<usize> {
513    let m = from.len();
514    let mut degree = vec![0usize; n_nodes + 1];
515    for &u in from {
516        degree[u + 1] += 1;
517    }
518    for i in 0..n_nodes {
519        degree[i + 1] += degree[i];
520    }
521    let mut cursor = degree;
522    let mut order = vec![0usize; m];
523    for e in 0..m {
524        let pos = cursor[from[e]];
525        order[pos] = e;
526        cursor[from[e]] += 1;
527    }
528    order
529}
530
531/// Heap entry ordered lexicographically by ascending `(cost, resource)`.
532struct BiState {
533    cost: f64,
534    resource: f64,
535    label: usize,
536}
537
538impl PartialEq for BiState {
539    fn eq(&self, other: &Self) -> bool {
540        self.cost == other.cost && self.resource == other.resource
541    }
542}
543impl Eq for BiState {}
544impl PartialOrd for BiState {
545    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
546        Some(self.cmp(other))
547    }
548}
549impl Ord for BiState {
550    fn cmp(&self, other: &Self) -> Ordering {
551        // Reverse so `BinaryHeap` (a max-heap) yields the lex-smallest pair.
552        other
553            .cost
554            .partial_cmp(&self.cost)
555            .unwrap_or(Ordering::Equal)
556            .then(
557                other
558                    .resource
559                    .partial_cmp(&self.resource)
560                    .unwrap_or(Ordering::Equal),
561            )
562    }
563}
564
565/// Bicriteria shortest paths from `source` to `target` by label-setting
566/// (Martins' algorithm): every edge carries a `(cost, resource)` pair and the
567/// result is the Pareto front of nondominated `(cost, resource)` path values,
568/// in increasing cost / decreasing resource order. With a finite `budget` the
569/// front is restricted to paths with `resource <= budget`, so its first entry
570/// solves the constrained shortest path problem exactly. `max_routes` caps the
571/// number of routes returned (`usize::MAX` for the whole front).
572///
573/// Labels are popped lexicographically by `(cost, resource)`, which makes the
574/// per-node dominance test a single comparison against the best resource seen
575/// at that node. Reverse-Dijkstra lower bounds on the remaining resource prune
576/// labels that cannot reach the target within budget, and labels whose
577/// projected resource cannot improve on the target's front are dropped early.
578/// Weights must be nonnegative; the search is exact.
579#[allow(clippy::too_many_arguments)] // edge list + query, as the R wrapper passes it
580pub fn pareto_paths(
581    from: &[usize],
582    to: &[usize],
583    w_cost: &[f64],
584    w_res: &[f64],
585    n_nodes: usize,
586    source: usize,
587    target: usize,
588    budget: f64,
589    max_routes: usize,
590) -> Vec<ParetoRoute> {
591    if max_routes == 0 {
592        return Vec::new();
593    }
594    if source == target {
595        return vec![ParetoRoute {
596            path: vec![source],
597            edges: Vec::new(),
598            cost: 0.0,
599            resource: 0.0,
600        }];
601    }
602    // Two CSR graphs over the same edge order, so slot `e` holds the same
603    // edge in both and `(targets[e], cost.weights[e], res.weights[e])` is one
604    // directed edge with its pair of weights.
605    let g_cost = Graph::from_edges(from, to, w_cost, n_nodes);
606    let g_res = Graph::from_edges(from, to, w_res, n_nodes);
607
608    // Reverse-graph Dijkstra bounds: least possible remaining resource from
609    // each node to the target (also detects nodes that cannot reach it).
610    let rev_res = Graph::from_edges(to, from, w_res, n_nodes);
611    let lb_res = dijkstra(&rev_res, target);
612    let edge_order = csr_edge_order(from, n_nodes);
613
614    let mut arena: Vec<Label> = Vec::new();
615    let mut heap: BinaryHeap<BiState> = BinaryHeap::new();
616    // Best (lowest) resource among labels made permanent at each node; pops
617    // arrive in increasing cost, so a new label is dominated iff it cannot
618    // strictly improve this.
619    let mut min_res = vec![f64::INFINITY; n_nodes];
620    let mut routes: Vec<ParetoRoute> = Vec::new();
621
622    if lb_res[source].is_finite() && lb_res[source] <= budget {
623        arena.push(Label {
624            node: source,
625            edge: usize::MAX,
626            pred: usize::MAX,
627        });
628        heap.push(BiState {
629            cost: 0.0,
630            resource: 0.0,
631            label: 0,
632        });
633    }
634
635    while let Some(BiState {
636        cost,
637        resource,
638        label,
639    }) = heap.pop()
640    {
641        let node = arena[label].node;
642        if min_res[node] <= resource {
643            continue; // dominated by (or duplicating) a permanent label
644        }
645        min_res[node] = resource;
646        if node == target {
647            let mut path = Vec::new();
648            let mut edges = Vec::new();
649            let mut cur = label;
650            while cur != usize::MAX {
651                path.push(arena[cur].node);
652                if arena[cur].edge != usize::MAX {
653                    edges.push(edge_order[arena[cur].edge]);
654                }
655                cur = arena[cur].pred;
656            }
657            path.reverse();
658            edges.reverse();
659            routes.push(ParetoRoute {
660                path,
661                edges,
662                cost,
663                resource,
664            });
665            if routes.len() >= max_routes {
666                break;
667            }
668            continue;
669        }
670        let start = g_cost.offsets[node];
671        let end = g_cost.offsets[node + 1];
672        for e in start..end {
673            let next = g_cost.targets[e];
674            let nc = cost + g_cost.weights[e];
675            let nr = resource + g_res.weights[e];
676            // Within budget even in the best case, able to improve the next
677            // node's front, and able to improve the target's front.
678            if !lb_res[next].is_finite()
679                || nr + lb_res[next] > budget
680                || min_res[next] <= nr
681                || min_res[target] <= nr + lb_res[next]
682            {
683                continue;
684            }
685            arena.push(Label {
686                node: next,
687                edge: e,
688                pred: label,
689            });
690            heap.push(BiState {
691                cost: nc,
692                resource: nr,
693                label: arena.len() - 1,
694            });
695        }
696    }
697    routes
698}
699
700/// Outcome of the Lagrangian constrained-shortest-path solver: a feasible
701/// path, its true cost/resource, the best dual lower bound on the optimal
702/// cost (`cost == lower_bound` proves optimality), the multiplier that
703/// produced it and the number of Dijkstra runs spent.
704pub struct LagrangianRoute {
705    pub path: Vec<usize>,
706    pub edges: Vec<usize>,
707    pub cost: f64,
708    pub resource: f64,
709    pub lower_bound: f64,
710    pub lambda: f64,
711    pub iterations: usize,
712}
713
714/// Dijkstra on the combined weight `w1 + lambda * w2` over two CSR graphs
715/// that share the same edge slots, tracking predecessors so the path's two
716/// weight components can be summed exactly (parallel edges included).
717/// Returns `(path, edge_slots, sum_w1, sum_w2)`, or `None` when `target` is
718/// unreachable.
719fn dijkstra_combined(
720    g1: &Graph,
721    g2: &Graph,
722    lambda: f64,
723    source: usize,
724    target: usize,
725) -> Option<(Vec<usize>, Vec<usize>, f64, f64)> {
726    let n = g1.n_nodes;
727    let mut dist = vec![f64::INFINITY; n];
728    let mut pred_node = vec![usize::MAX; n];
729    let mut pred_edge = vec![usize::MAX; n];
730    dist[source] = 0.0;
731    let mut heap = BinaryHeap::new();
732    heap.push(State {
733        dist: 0.0,
734        node: source,
735    });
736    while let Some(State { dist: d, node }) = heap.pop() {
737        if node == target {
738            break;
739        }
740        if d > dist[node] {
741            continue;
742        }
743        for e in g1.offsets[node]..g1.offsets[node + 1] {
744            let next = g1.targets[e];
745            let nd = d + g1.weights[e] + lambda * g2.weights[e];
746            if nd < dist[next] {
747                dist[next] = nd;
748                pred_node[next] = node;
749                pred_edge[next] = e;
750                heap.push(State {
751                    dist: nd,
752                    node: next,
753                });
754            }
755        }
756    }
757    if dist[target].is_infinite() {
758        return None;
759    }
760    let mut path = vec![target];
761    let mut edges = Vec::new();
762    let (mut w1, mut w2) = (0.0, 0.0);
763    let mut cur = target;
764    while cur != source {
765        let e = pred_edge[cur];
766        edges.push(e);
767        w1 += g1.weights[e];
768        w2 += g2.weights[e];
769        cur = pred_node[cur];
770        path.push(cur);
771    }
772    path.reverse();
773    edges.reverse();
774    Some((path, edges, w1, w2))
775}
776
777/// Constrained shortest path by Lagrangian relaxation (the LARAC algorithm):
778/// minimize cost subject to `resource <= budget`, dualizing the constraint so
779/// each iteration is a plain Dijkstra on `cost + lambda * resource`. A binary
780/// search on the two support paths (cheapest-infeasible and
781/// feasible-so-far) drives `lambda` to the maximizer of the dual, which takes
782/// few iterations and scales to graphs where exact label-setting is slow.
783///
784/// Returns the best feasible path found with the best dual lower bound on
785/// the optimal cost: when `cost == lower_bound` the path is provably
786/// optimal; otherwise it is feasible and within `cost - lower_bound` of the
787/// optimum (the duality gap — close it with [`pareto_paths`], which is
788/// exact). Returns `None` when the target is unreachable or no path fits the
789/// budget.
790#[allow(clippy::too_many_arguments)] // edge list + query, as the R wrapper passes it
791pub fn csp_lagrangian(
792    from: &[usize],
793    to: &[usize],
794    w_cost: &[f64],
795    w_res: &[f64],
796    n_nodes: usize,
797    source: usize,
798    target: usize,
799    budget: f64,
800    max_iter: usize,
801) -> Option<LagrangianRoute> {
802    if budget < 0.0 {
803        return None;
804    }
805    if source == target {
806        return Some(LagrangianRoute {
807            path: vec![source],
808            edges: Vec::new(),
809            cost: 0.0,
810            resource: 0.0,
811            lower_bound: 0.0,
812            lambda: 0.0,
813            iterations: 0,
814        });
815    }
816    let g_cost = Graph::from_edges(from, to, w_cost, n_nodes);
817    let g_res = Graph::from_edges(from, to, w_res, n_nodes);
818    let edge_order = csr_edge_order(from, n_nodes);
819    let to_input =
820        |slots: Vec<usize>| -> Vec<usize> { slots.into_iter().map(|s| edge_order[s]).collect() };
821
822    // lambda = 0: the unconstrained shortest path. Feasible means done.
823    let (p0, e0, c0, r0) = dijkstra_combined(&g_cost, &g_res, 0.0, source, target)?;
824    if r0 <= budget {
825        return Some(LagrangianRoute {
826            path: p0,
827            edges: to_input(e0),
828            cost: c0,
829            resource: r0,
830            lower_bound: c0,
831            lambda: 0.0,
832            iterations: 1,
833        });
834    }
835    // lambda -> infinity: the minimum-resource path. Infeasible means no
836    // path fits the budget at all.
837    let (pr, er, rmin, c_of_rmin) = dijkstra_combined(&g_res, &g_cost, 0.0, source, target)?;
838    if rmin > budget {
839        return None;
840    }
841
842    let mut inf = (p0, e0, c0, r0); // infeasible support: cheap, over budget
843    let mut feas = (pr, er, c_of_rmin, rmin); // feasible support: within budget
844    let mut best = feas.clone(); // cheapest feasible path seen so far
845    let mut best_lb = c0; // L(0)
846    let mut best_lambda = 0.0;
847    let mut iterations = 2usize;
848
849    for _ in 0..max_iter {
850        // The multiplier where the two supports' combined costs tie.
851        let lam = (feas.2 - inf.2) / (inf.3 - feas.3);
852        if !lam.is_finite() || lam <= 0.0 {
853            break;
854        }
855        let (p, e, c, r) = match dijkstra_combined(&g_cost, &g_res, lam, source, target) {
856            Some(x) => x,
857            None => break,
858        };
859        iterations += 1;
860        let lb = c + lam * (r - budget); // L(lam), a valid lower bound
861        if lb > best_lb {
862            best_lb = lb;
863            best_lambda = lam;
864        }
865        // Dual optimum: the new path cannot beat the supports' tied value.
866        let tie = inf.2 + lam * inf.3;
867        if (c + lam * r) >= tie - 1e-9 * (1.0 + tie.abs()) {
868            break;
869        }
870        if r <= budget {
871            feas = (p, e, c, r);
872            if feas.2 < best.2 {
873                best = feas.clone();
874            }
875        } else {
876            inf = (p, e, c, r);
877        }
878    }
879
880    let lower_bound = best_lb.min(best.2); // guard against float noise
881    Some(LagrangianRoute {
882        path: best.0,
883        edges: to_input(best.1),
884        cost: best.2,
885        resource: best.3,
886        lower_bound,
887        lambda: best_lambda,
888        iterations,
889    })
890}
891
892#[cfg(test)]
893mod tests {
894    use super::*;
895
896    fn line_graph() -> Graph {
897        // 0 -1-> 1 -1-> 2 -1-> 3, plus a shortcut 0 -10-> 3
898        Graph::from_edges(&[0, 1, 2, 0], &[1, 2, 3, 3], &[1.0, 1.0, 1.0, 10.0], 4)
899    }
900
901    #[test]
902    fn dijkstra_distances() {
903        let g = line_graph();
904        let d = dijkstra(&g, 0);
905        assert_eq!(d, vec![0.0, 1.0, 2.0, 3.0]);
906    }
907
908    #[test]
909    fn path_prefers_shorter_route() {
910        let g = line_graph();
911        let (path, slots, cost) = shortest_path_route(&g, 0, 3).unwrap();
912        assert_eq!(path, vec![0, 1, 2, 3]);
913        assert_eq!(slots.len(), 3);
914        assert!((cost - 3.0).abs() < 1e-9);
915        // CSR slots map back to the input edges actually traversed.
916        let order = csr_edge_order(&[0, 1, 2, 0], 4);
917        let edges: Vec<usize> = slots.iter().map(|&s| order[s]).collect();
918        assert_eq!(edges, vec![0, 1, 2]);
919    }
920
921    #[test]
922    fn unreachable_is_none() {
923        let g = Graph::from_edges(&[0], &[1], &[1.0], 3);
924        assert!(shortest_path_route(&g, 0, 2).is_none());
925    }
926
927    #[test]
928    fn shortest_route_traverses_the_cheaper_parallel_edge() {
929        // Parallel edges 0 -> 1 with weights 5 (index 0) and 1 (index 1).
930        let from = vec![0, 0, 1];
931        let to = vec![1, 1, 2];
932        let w = vec![5.0, 1.0, 1.0];
933        let g = Graph::from_edges(&from, &to, &w, 3);
934        let (path, slots, cost) = shortest_path_route(&g, 0, 2).unwrap();
935        assert_eq!(path, vec![0, 1, 2]);
936        assert!((cost - 2.0).abs() < 1e-9);
937        let order = csr_edge_order(&from, 3);
938        let edges: Vec<usize> = slots.iter().map(|&s| order[s]).collect();
939        assert_eq!(edges, vec![1, 2]);
940    }
941
942    #[test]
943    fn yen_finds_two_distinct_paths() {
944        // 0->1->3 (cost 2) and 0->2->3 (cost 4)
945        let from = vec![0, 1, 0, 2];
946        let to = vec![1, 3, 2, 3];
947        let w = vec![1.0, 1.0, 2.0, 2.0];
948        let g = Graph::from_edges(&from, &to, &w, 4);
949        let paths = k_shortest_paths(&g, &from, &to, &w, 0, 3, 2);
950        assert_eq!(paths.len(), 2);
951        assert_eq!(paths[0].path, vec![0, 1, 3]);
952        assert_eq!(paths[0].edges, vec![0, 1]);
953        assert!((paths[0].cost - 2.0).abs() < 1e-9);
954        assert_eq!(paths[1].path, vec![0, 2, 3]);
955        assert_eq!(paths[1].edges, vec![2, 3]);
956        assert!((paths[1].cost - 4.0).abs() < 1e-9);
957    }
958
959    #[test]
960    fn yen_caps_at_available_paths() {
961        let from = vec![0, 1];
962        let to = vec![1, 2];
963        let w = vec![1.0, 1.0];
964        let g = Graph::from_edges(&from, &to, &w, 3);
965        let paths = k_shortest_paths(&g, &from, &to, &w, 0, 2, 5);
966        assert_eq!(paths.len(), 1);
967    }
968
969    #[test]
970    fn distance_matrix_shape_and_values() {
971        let g = line_graph();
972        let dm = distance_matrix(&g, &[0, 1], &[2, 3]);
973        assert_eq!(dm, vec![2.0, 3.0, 1.0, 2.0]);
974    }
975
976    /// 4 stops on a line at x = 0, 1, 2, 10 with symmetric |xi - xj| costs.
977    fn line_costs() -> Vec<f64> {
978        let xs: [f64; 4] = [0.0, 1.0, 2.0, 10.0];
979        let mut c = Vec::with_capacity(16);
980        for i in 0..4 {
981            for j in 0..4 {
982                c.push((xs[i] - xs[j]).abs());
983            }
984        }
985        c
986    }
987
988    #[test]
989    fn tsp_closed_tour_is_a_permutation_returning_home() {
990        let c = line_costs();
991        let tour = tsp_tour(&c, 4, 0, None, true, true);
992        let mut sorted = tour.clone();
993        sorted.sort_unstable();
994        assert_eq!(sorted, vec![0, 1, 2, 3]);
995        assert_eq!(tour[0], 0);
996        // Optimal closed tour on a line sweeps out and back: cost 2 * 10.
997        assert!((tour_cost(&c, 4, &tour, true) - 20.0).abs() < 1e-9);
998    }
999
1000    #[test]
1001    fn tsp_open_route_respects_fixed_end() {
1002        let c = line_costs();
1003        let tour = tsp_tour(&c, 4, 3, Some(0), false, true);
1004        assert_eq!(tour[0], 3);
1005        assert_eq!(tour[3], 0);
1006        // Optimal open route 3 -> 2 -> 1 -> 0 costs 10.
1007        assert!((tour_cost(&c, 4, &tour, false) - 10.0).abs() < 1e-9);
1008    }
1009
1010    #[test]
1011    fn or_opt_fixes_a_greedy_mistake() {
1012        // Open free-end route from 0. Greedy takes the cheap bait arc 0 -> 1,
1013        // then 1 -> 3, and is forced onto 3 -> 2 (cost 100): 1 + 2 + 100 = 103.
1014        // The optimum is 0 -> 2 -> 3 -> 1: 2 + 1 + 3 = 6, which or-opt reaches
1015        // by relocating the segment [1, 3] reversed.
1016        let c = vec![
1017            0.0, 1.0, 2.0, 50.0, //
1018            1.0, 0.0, 100.0, 2.0, //
1019            2.0, 100.0, 0.0, 1.0, //
1020            50.0, 3.0, 100.0, 0.0,
1021        ];
1022        let nn = tsp_tour(&c, 4, 0, None, false, false);
1023        let improved = tsp_tour(&c, 4, 0, None, false, true);
1024        assert_eq!(nn, vec![0, 1, 3, 2]);
1025        assert!((tour_cost(&c, 4, &nn, false) - 103.0).abs() < 1e-9);
1026        assert_eq!(improved, vec![0, 2, 3, 1]);
1027        assert!((tour_cost(&c, 4, &improved, false) - 6.0).abs() < 1e-9);
1028    }
1029
1030    #[test]
1031    fn tsp_handles_asymmetric_costs() {
1032        // Cheap arcs only along the directed cycle 0 -> 1 -> 2 -> 0 (cost 1
1033        // each); every other arc costs 10. Closed tour 0 -> 1 -> 2 costs 3,
1034        // the reverse costs 30, so the solver must respect arc direction.
1035        let c = vec![0.0, 1.0, 10.0, 10.0, 0.0, 1.0, 1.0, 10.0, 0.0];
1036        let tour = tsp_tour(&c, 3, 0, None, true, true);
1037        assert_eq!(tour, vec![0, 1, 2]);
1038    }
1039
1040    /// Minimum tour cost by brute force over all orders of stops 1..n, with
1041    /// stop 0 fixed first (and the closing arc added when `closed`).
1042    fn brute_force(cost: &[f64], n: usize, closed: bool) -> f64 {
1043        fn go(
1044            cost: &[f64],
1045            n: usize,
1046            tour: &mut Vec<usize>,
1047            rest: &mut Vec<usize>,
1048            closed: bool,
1049            best: &mut f64,
1050        ) {
1051            if rest.is_empty() {
1052                let c = tour_cost(cost, n, tour, closed);
1053                if c < *best {
1054                    *best = c;
1055                }
1056                return;
1057            }
1058            for i in 0..rest.len() {
1059                let x = rest.remove(i);
1060                tour.push(x);
1061                go(cost, n, tour, rest, closed, best);
1062                tour.pop();
1063                rest.insert(i, x);
1064            }
1065        }
1066        let mut best = f64::INFINITY;
1067        let mut tour = vec![0usize];
1068        let mut rest: Vec<usize> = (1..n).collect();
1069        go(cost, n, &mut tour, &mut rest, closed, &mut best);
1070        best
1071    }
1072
1073    #[test]
1074    fn ils_matches_brute_force_on_random_asymmetric_instances() {
1075        let n = 7;
1076        for seed in 1..=8u64 {
1077            let mut rng = Rng(seed);
1078            let mut cost = vec![0.0; n * n];
1079            for i in 0..n {
1080                for j in 0..n {
1081                    if i != j {
1082                        cost[i * n + j] = (rng.next() % 1000) as f64 + 1.0;
1083                    }
1084                }
1085            }
1086            for closed in [true, false] {
1087                let tour = tsp_tour(&cost, n, 0, None, closed, true);
1088                let got = tour_cost(&cost, n, &tour, closed);
1089                let best = brute_force(&cost, n, closed);
1090                assert!(
1091                    (got - best).abs() < 1e-9,
1092                    "seed {seed} closed {closed}: got {got}, optimum {best}"
1093                );
1094            }
1095        }
1096    }
1097
1098    #[test]
1099    fn ils_improves_on_nearest_neighbor_at_fifty_stops() {
1100        let n = 50;
1101        let mut rng = Rng(7);
1102        let mut cost = vec![0.0; n * n];
1103        for i in 0..n {
1104            for j in 0..n {
1105                if i != j {
1106                    cost[i * n + j] = (rng.next() % 1000) as f64 + 1.0;
1107                }
1108            }
1109        }
1110        let nn = tsp_tour(&cost, n, 0, None, true, false);
1111        let ils = tsp_tour(&cost, n, 0, None, true, true);
1112        assert!(tour_cost(&cost, n, &ils, true) < tour_cost(&cost, n, &nn, true));
1113    }
1114
1115    /// Diamond with a cheap high-resource route, an expensive zero-resource
1116    /// route and a dominated direct edge.
1117    fn diamond() -> (Vec<usize>, Vec<usize>, Vec<f64>, Vec<f64>) {
1118        let from = vec![0, 1, 0, 2, 0];
1119        let to = vec![1, 3, 2, 3, 3];
1120        let cost = vec![1.0, 1.0, 3.0, 3.0, 10.0];
1121        let res = vec![5.0, 5.0, 0.0, 0.0, 2.0];
1122        (from, to, cost, res)
1123    }
1124
1125    #[test]
1126    fn pareto_front_on_diamond() {
1127        let (from, to, cost, res) = diamond();
1128        let routes = pareto_paths(&from, &to, &cost, &res, 4, 0, 3, f64::INFINITY, usize::MAX);
1129        let vals: Vec<(f64, f64)> = routes.iter().map(|r| (r.cost, r.resource)).collect();
1130        assert_eq!(vals, vec![(2.0, 10.0), (6.0, 0.0)]);
1131        assert_eq!(routes[0].path, vec![0, 1, 3]);
1132        assert_eq!(routes[1].path, vec![0, 2, 3]);
1133    }
1134
1135    #[test]
1136    fn budget_picks_the_cheapest_feasible_route() {
1137        let (from, to, cost, res) = diamond();
1138        // Budget 5 rules out the (2, 10) route; (6, 0) beats (10, 2).
1139        let routes = pareto_paths(&from, &to, &cost, &res, 4, 0, 3, 5.0, 1);
1140        assert_eq!(routes.len(), 1);
1141        assert_eq!(routes[0].path, vec![0, 2, 3]);
1142        // Budget 2 admits the dominated-direct edge as the only option left
1143        // besides (6, 0); the cheapest feasible is still (6, 0).
1144        let routes = pareto_paths(&from, &to, &cost, &res, 4, 0, 3, 2.0, usize::MAX);
1145        let vals: Vec<(f64, f64)> = routes.iter().map(|r| (r.cost, r.resource)).collect();
1146        assert_eq!(vals, vec![(6.0, 0.0)]);
1147    }
1148
1149    #[test]
1150    fn infeasible_budget_and_unreachable_target_are_empty() {
1151        let (from, to, cost, res) = diamond();
1152        assert!(pareto_paths(&from, &to, &cost, &res, 4, 0, 3, -1.0, usize::MAX).is_empty());
1153        // Node 3 has no out-edges, so nothing reaches a 5th isolated node.
1154        assert!(
1155            pareto_paths(&from, &to, &cost, &res, 5, 0, 4, f64::INFINITY, usize::MAX).is_empty()
1156        );
1157    }
1158
1159    #[test]
1160    fn pareto_trivial_and_capped() {
1161        let (from, to, cost, res) = diamond();
1162        let same = pareto_paths(&from, &to, &cost, &res, 4, 2, 2, f64::INFINITY, usize::MAX);
1163        assert_eq!(same.len(), 1);
1164        assert_eq!(same[0].path, vec![2]);
1165        let capped = pareto_paths(&from, &to, &cost, &res, 4, 0, 3, f64::INFINITY, 1);
1166        assert_eq!(capped.len(), 1);
1167        assert_eq!(capped[0].path, vec![0, 1, 3]);
1168    }
1169
1170    /// All simple paths from `s` to `t` as `(cost, resource)` pairs.
1171    fn enumerate_values(
1172        from: &[usize],
1173        to: &[usize],
1174        cost: &[f64],
1175        res: &[f64],
1176        n: usize,
1177        s: usize,
1178        t: usize,
1179    ) -> Vec<(f64, f64)> {
1180        fn go(
1181            edges: &[(usize, usize, f64, f64)],
1182            t: usize,
1183            node: usize,
1184            seen: &mut Vec<bool>,
1185            c: f64,
1186            r: f64,
1187            out: &mut Vec<(f64, f64)>,
1188        ) {
1189            if node == t {
1190                out.push((c, r));
1191                return;
1192            }
1193            for &(u, v, wc, wr) in edges {
1194                if u == node && !seen[v] {
1195                    seen[v] = true;
1196                    go(edges, t, v, seen, c + wc, r + wr, out);
1197                    seen[v] = false;
1198                }
1199            }
1200        }
1201        let edges: Vec<(usize, usize, f64, f64)> = (0..from.len())
1202            .map(|i| (from[i], to[i], cost[i], res[i]))
1203            .collect();
1204        let mut seen = vec![false; n];
1205        seen[s] = true;
1206        let mut out = Vec::new();
1207        go(&edges, t, s, &mut seen, 0.0, 0.0, &mut out);
1208        out
1209    }
1210
1211    /// Nondominated subset of `(cost, resource)` values, deduplicated and
1212    /// sorted by increasing cost.
1213    fn pareto_filter(mut vals: Vec<(f64, f64)>) -> Vec<(f64, f64)> {
1214        vals.sort_by(|a, b| {
1215            a.0.partial_cmp(&b.0)
1216                .unwrap()
1217                .then(a.1.partial_cmp(&b.1).unwrap())
1218        });
1219        let mut front: Vec<(f64, f64)> = Vec::new();
1220        for (c, r) in vals {
1221            if front.iter().all(|&(fc, fr)| !(fc <= c && fr <= r)) {
1222                front.push((c, r));
1223            }
1224        }
1225        front
1226    }
1227
1228    #[test]
1229    fn pareto_matches_brute_force_on_random_graphs() {
1230        for seed in 1..=12u64 {
1231            let mut rng = Rng(seed);
1232            let n = 6usize;
1233            let mut from = Vec::new();
1234            let mut to = Vec::new();
1235            let mut cost = Vec::new();
1236            let mut res = Vec::new();
1237            for u in 0..n {
1238                for v in 0..n {
1239                    if u != v && rng.next() % 100 < 45 {
1240                        from.push(u);
1241                        to.push(v);
1242                        cost.push((rng.next() % 20) as f64 + 1.0);
1243                        // About half the edges consume no resource at all.
1244                        res.push(if rng.next() % 2 == 0 {
1245                            0.0
1246                        } else {
1247                            (rng.next() % 10) as f64
1248                        });
1249                    }
1250                }
1251            }
1252            let all = enumerate_values(&from, &to, &cost, &res, n, 0, n - 1);
1253            let want = pareto_filter(all.clone());
1254            let got: Vec<(f64, f64)> = pareto_paths(
1255                &from,
1256                &to,
1257                &cost,
1258                &res,
1259                n,
1260                0,
1261                n - 1,
1262                f64::INFINITY,
1263                usize::MAX,
1264            )
1265            .iter()
1266            .map(|r| (r.cost, r.resource))
1267            .collect();
1268            assert_eq!(got, want, "seed {seed}: front mismatch");
1269
1270            // Constrained answers agree with brute force for several budgets.
1271            for budget in [0.0, 3.0, 7.0, 15.0] {
1272                let best = all
1273                    .iter()
1274                    .filter(|&&(_, r)| r <= budget)
1275                    .map(|&(c, _)| c)
1276                    .fold(f64::INFINITY, f64::min);
1277                let solved = pareto_paths(&from, &to, &cost, &res, n, 0, n - 1, budget, 1);
1278                match solved.first() {
1279                    Some(r) => {
1280                        assert!(
1281                            (r.cost - best).abs() < 1e-9 && r.resource <= budget,
1282                            "seed {seed} budget {budget}: got ({}, {}), optimum {best}",
1283                            r.cost,
1284                            r.resource
1285                        );
1286                    }
1287                    None => assert!(
1288                        best.is_infinite(),
1289                        "seed {seed} budget {budget}: solver found nothing, optimum {best}"
1290                    ),
1291                }
1292            }
1293        }
1294    }
1295
1296    #[test]
1297    fn lagrangian_shortcuts_when_unconstrained_is_feasible() {
1298        let (from, to, cost, res) = diamond();
1299        let r = csp_lagrangian(&from, &to, &cost, &res, 4, 0, 3, 100.0, 50).unwrap();
1300        assert_eq!(r.path, vec![0, 1, 3]);
1301        assert_eq!((r.cost, r.resource, r.lower_bound), (2.0, 10.0, 2.0));
1302        assert_eq!(r.iterations, 1);
1303    }
1304
1305    #[test]
1306    fn lagrangian_finds_the_optimum_with_a_duality_gap() {
1307        let (from, to, cost, res) = diamond();
1308        // Budget 5: optimum is (6, 0); the dual peaks at L(0.4) = 4, so the
1309        // path is right but the certificate has a gap of 2.
1310        let r = csp_lagrangian(&from, &to, &cost, &res, 4, 0, 3, 5.0, 50).unwrap();
1311        assert_eq!(r.path, vec![0, 2, 3]);
1312        assert_eq!((r.cost, r.resource), (6.0, 0.0));
1313        assert!(r.lower_bound <= 6.0 && r.lower_bound >= 2.0);
1314        assert!((r.lambda - 0.4).abs() < 1e-9);
1315    }
1316
1317    #[test]
1318    fn lagrangian_infeasible_and_unreachable_are_none() {
1319        let (from, to, cost, res) = diamond();
1320        // Node 1 is only reachable over 5 resource units.
1321        assert!(csp_lagrangian(&from, &to, &cost, &res, 4, 0, 1, 1.0, 50).is_none());
1322        assert!(csp_lagrangian(&from, &to, &cost, &res, 5, 0, 4, 100.0, 50).is_none());
1323        assert!(csp_lagrangian(&from, &to, &cost, &res, 4, 0, 3, -1.0, 50).is_none());
1324    }
1325
1326    #[test]
1327    fn lagrangian_is_sandwiched_by_the_exact_solver_on_random_graphs() {
1328        for seed in 1..=12u64 {
1329            let mut rng = Rng(seed * 7 + 1);
1330            let n = 7usize;
1331            let mut from = Vec::new();
1332            let mut to = Vec::new();
1333            let mut cost = Vec::new();
1334            let mut res = Vec::new();
1335            for u in 0..n {
1336                for v in 0..n {
1337                    if u != v && rng.next() % 100 < 40 {
1338                        from.push(u);
1339                        to.push(v);
1340                        cost.push((rng.next() % 20) as f64 + 1.0);
1341                        res.push(if rng.next() % 2 == 0 {
1342                            0.0
1343                        } else {
1344                            (rng.next() % 10) as f64
1345                        });
1346                    }
1347                }
1348            }
1349            for budget in [0.0, 2.0, 5.0, 9.0, 20.0] {
1350                let exact = pareto_paths(&from, &to, &cost, &res, n, 0, n - 1, budget, 1);
1351                let lagr = csp_lagrangian(&from, &to, &cost, &res, n, 0, n - 1, budget, 50);
1352                match (exact.first(), lagr) {
1353                    (Some(e), Some(l)) => {
1354                        assert!(
1355                            l.resource <= budget,
1356                            "seed {seed} budget {budget}: infeasible Lagrangian answer"
1357                        );
1358                        assert!(
1359                            l.lower_bound <= e.cost + 1e-9 && e.cost <= l.cost + 1e-9,
1360                            "seed {seed} budget {budget}: lb {} / exact {} / lagr {}",
1361                            l.lower_bound,
1362                            e.cost,
1363                            l.cost
1364                        );
1365                    }
1366                    (None, None) => {}
1367                    (e, l) => panic!(
1368                        "seed {seed} budget {budget}: feasibility disagreement (exact {:?}, lagr {:?})",
1369                        e.map(|x| x.cost),
1370                        l.map(|x| x.cost)
1371                    ),
1372                }
1373            }
1374        }
1375    }
1376
1377    #[test]
1378    fn pareto_paths_are_consistent_with_their_values() {
1379        let (from, to, cost, res) = diamond();
1380        let amap = edge_argmin_map(&from, &to, &cost);
1381        for r in pareto_paths(&from, &to, &cost, &res, 4, 0, 3, f64::INFINITY, usize::MAX) {
1382            assert!((path_cost(&r.path, &amap, &cost) - r.cost).abs() < 1e-9);
1383        }
1384    }
1385
1386    #[test]
1387    fn returned_edges_are_the_traversed_ones() {
1388        // Two parallel edges 0 -> 1: cheap-cost/high-resource (index 0) and
1389        // expensive-cost/zero-resource (index 1), then 1 -> 2. The edge list
1390        // must name the parallel edge each route actually used.
1391        let from = vec![0, 0, 1];
1392        let to = vec![1, 1, 2];
1393        let cost = vec![1.0, 5.0, 1.0];
1394        let res = vec![5.0, 0.0, 0.0];
1395        let routes = pareto_paths(&from, &to, &cost, &res, 3, 0, 2, f64::INFINITY, usize::MAX);
1396        let vals: Vec<(f64, f64)> = routes.iter().map(|r| (r.cost, r.resource)).collect();
1397        assert_eq!(vals, vec![(2.0, 5.0), (6.0, 0.0)]);
1398        assert_eq!(routes[0].edges, vec![0, 2]);
1399        assert_eq!(routes[1].edges, vec![1, 2]);
1400        let l = csp_lagrangian(&from, &to, &cost, &res, 3, 0, 2, 0.0, 50).unwrap();
1401        assert_eq!(l.edges, vec![1, 2]);
1402        assert_eq!((l.cost, l.resource), (6.0, 0.0));
1403    }
1404
1405    #[test]
1406    fn returned_edges_align_with_paths_and_values() {
1407        let (from, to, cost, res) = diamond();
1408        let check = |path: &[usize], edges: &[usize], want_c: f64, want_r: f64| {
1409            assert_eq!(edges.len() + 1, path.len());
1410            let (mut c, mut r) = (0.0, 0.0);
1411            for (i, &e) in edges.iter().enumerate() {
1412                assert_eq!(from[e], path[i]);
1413                assert_eq!(to[e], path[i + 1]);
1414                c += cost[e];
1415                r += res[e];
1416            }
1417            assert!((c - want_c).abs() < 1e-9 && (r - want_r).abs() < 1e-9);
1418        };
1419        for rt in pareto_paths(&from, &to, &cost, &res, 4, 0, 3, f64::INFINITY, usize::MAX) {
1420            check(&rt.path, &rt.edges, rt.cost, rt.resource);
1421        }
1422        let l = csp_lagrangian(&from, &to, &cost, &res, 4, 0, 3, 5.0, 50).unwrap();
1423        check(&l.path, &l.edges, l.cost, l.resource);
1424    }
1425
1426    #[test]
1427    fn tsp_trivial_sizes() {
1428        assert_eq!(tsp_tour(&[0.0], 1, 0, None, true, true), vec![0]);
1429        let c = vec![0.0, 2.0, 3.0, 0.0];
1430        assert_eq!(tsp_tour(&c, 2, 1, None, false, true), vec![1, 0]);
1431    }
1432}