Skip to main content

routingkit_cch/
lib.rs

1#![doc = include_str!("../README.md")]
2
3#[cxx::bridge]
4pub mod ffi {
5    extern "C++" {
6        include!("routingkit_cch_wrapper.h");
7
8        type CCH; // CustomizableContractionHierarchy
9        type CCHMetric; // CustomizableContractionHierarchyMetric
10        type CCHQuery; // CustomizableContractionHierarchyQuery
11        type CCHPartial; // CustomizableContractionHierarchyPartialCustomization
12
13        /// Build a Customizable Contraction Hierarchy.
14        /// Arguments:
15        /// * order: node permutation (size = number of nodes)
16        /// * tail/head: directed arc lists (same length)
17        /// * filter_always_inf_arcs: if true, arcs with infinite weight placeholders are stripped
18        unsafe fn cch_new(
19            order: &[u32],
20            tail: &[u32],
21            head: &[u32],
22            log_message: fn(&str),
23            filter_always_inf_arcs: bool,
24        ) -> UniquePtr<CCH>;
25
26        /// Create a metric (weights binding) for an existing CCH.
27        /// Keeps pointer to weights in CCHMetric; weights length must equal arc count.
28        unsafe fn cch_metric_new(cch: &CCH, weights: &[u32]) -> UniquePtr<CCHMetric>;
29
30        /// Run customization to compute upward/downward shortcut weights.
31        /// Must be called after creating a metric and before queries.
32        /// Cost: Depends on separator quality; usually near-linear in m * small constant; may allocate temporary buffers.
33        unsafe fn cch_metric_customize(metric: Pin<&mut CCHMetric>);
34
35        /// Rebind the metric to a new weight vector (same CCH), reusing the internal
36        /// shortcut-weight buffers. A customize call must follow before further queries.
37        unsafe fn cch_metric_reset(metric: Pin<&mut CCHMetric>, weights: &[u32]);
38
39        /// Parallel customization; thread_count==0 picks an internal default (#procs if OpenMP, else 1).
40        unsafe fn cch_metric_parallel_customize(metric: Pin<&mut CCHMetric>, thread_count: u32);
41
42        /// Allocate a new reusable partial customization helper bound to a CCH.
43        unsafe fn cch_partial_new(cch: &CCH) -> UniquePtr<CCHPartial>;
44
45        /// Reset the partial updater to empty state (no updated arcs).
46        unsafe fn cch_partial_reset(partial: Pin<&mut CCHPartial>);
47
48        /// Mark an arc as updated (weight changed) for the next partial customize call.
49        /// The actual weight change must already happened in the metric's weight vector.
50        unsafe fn cch_partial_update_arc(partial: Pin<&mut CCHPartial>, arc: u32);
51
52        /// Run partial customization to update shortcut weights affected by the marked arcs.
53        unsafe fn cch_partial_customize(partial: Pin<&mut CCHPartial>, metric: Pin<&mut CCHMetric>);
54
55        /// Allocate a new reusable query object bound to a metric.
56        unsafe fn cch_query_new(metric: &CCHMetric) -> UniquePtr<CCHQuery>;
57
58        /// Reset a query so it can be reused with the (same or updated) metric.
59        /// Clears internal state, sources/targets, distance labels.
60        unsafe fn cch_query_reset(query: Pin<&mut CCHQuery>, metric: &CCHMetric);
61
62        /// Add a source node with initial distance (0 for standard shortest path).
63        /// Multiple sources allowed (multi-source query).
64        unsafe fn cch_query_add_source(query: Pin<&mut CCHQuery>, s: u32, dist: u32);
65
66        /// Add a target node with initial distance (0 typical).
67        /// Multiple targets allowed (multi-target query).
68        unsafe fn cch_query_add_target(query: Pin<&mut CCHQuery>, t: u32, dist: u32);
69
70        /// Execute the shortest path search (multi-source / multi-target if several added).
71        /// Must be called after adding at least one source & target.
72        unsafe fn cch_query_run(query: Pin<&mut CCHQuery>);
73
74        /// Get shortest distance after run(). Undefined if run() not called.
75        unsafe fn cch_query_distance(query: &CCHQuery) -> u32;
76
77        /// Extract the node path for the current query result.
78        /// Reconstructs path; may traverse parent pointers.
79        unsafe fn cch_query_node_path(query: &CCHQuery) -> Vec<u32>;
80
81        /// Extract the arc (edge) path corresponding to the shortest path.
82        /// Each entry is an original arc id (after shortcut unpacking).
83        unsafe fn cch_query_arc_path(query: &CCHQuery) -> Vec<u32>;
84
85        /// Pin a fixed set of targets for one-to-many queries.
86        /// The query must be in a freshly constructed/reset state.
87        unsafe fn cch_query_pin_targets(query: Pin<&mut CCHQuery>, targets: &[u32]);
88
89        /// Run the one-to-many search from the added sources to all pinned targets.
90        unsafe fn cch_query_run_to_pinned_targets(query: Pin<&mut CCHQuery>);
91
92        /// Distances to the pinned targets (in pinning order); inf_weight = unreachable.
93        /// Must be called after run_to_pinned_targets.
94        unsafe fn cch_query_distances_to_targets(query: &CCHQuery) -> Vec<u32>;
95
96        /// Clear the sources and distance labels while keeping the pinned targets,
97        /// so another one-to-many run can be performed.
98        unsafe fn cch_query_reset_source(query: Pin<&mut CCHQuery>);
99
100        /// Pin a fixed set of sources for many-to-one queries.
101        /// The query must be in a freshly constructed/reset state.
102        unsafe fn cch_query_pin_sources(query: Pin<&mut CCHQuery>, sources: &[u32]);
103
104        /// Run the many-to-one search from all pinned sources to the added targets.
105        unsafe fn cch_query_run_to_pinned_sources(query: Pin<&mut CCHQuery>);
106
107        /// Distances from the pinned sources (in pinning order); inf_weight = unreachable.
108        /// Must be called after run_to_pinned_sources.
109        unsafe fn cch_query_distances_to_sources(query: &CCHQuery) -> Vec<u32>;
110
111        /// Clear the targets and distance labels while keeping the pinned sources,
112        /// so another many-to-one run can be performed.
113        unsafe fn cch_query_reset_target(query: Pin<&mut CCHQuery>);
114
115        /// Compute a high-quality nested dissection order using inertial flow separators.
116        /// Inputs:
117        /// * node_count: number of nodes
118        /// * tail/head: directed arcs (treated as undirected for ordering)
119        /// * latitude/longitude: per-node coords (len = node_count)
120        /// Returns: order permutation (position -> node id).
121        unsafe fn cch_compute_order_inertial(
122            node_count: u32,
123            tail: &[u32],
124            head: &[u32],
125            latitude: &[f32],
126            longitude: &[f32],
127        ) -> Vec<u32>;
128
129        /// Fast fallback order: nodes sorted by (degree, id) ascending.
130        /// Lower quality than nested dissection but zero extra data needed.
131        unsafe fn cch_compute_order_degree(node_count: u32, tail: &[u32], head: &[u32])
132        -> Vec<u32>;
133    }
134}
135
136// Thread-safety markers
137// Safety rationale:
138// - CCH (CustomizableContractionHierarchy) after construction is immutable.
139// - CCHMetric after successful customize() is read-only for queries; underlying RoutingKit code
140//   does not mutate metric during query operations (queries keep their own state).
141// - CCHQuery holds mutable per-query state and must not be shared across threads concurrently;
142//   we allow moving it between threads (Send) but not Sync.
143// If RoutingKit later introduces internal mutation or lazy caching inside CCHMetric, these impls
144// would need re-audit.
145unsafe impl Send for ffi::CCH {}
146unsafe impl Sync for ffi::CCH {}
147unsafe impl Send for ffi::CCHMetric {}
148unsafe impl Sync for ffi::CCHMetric {}
149unsafe impl Send for ffi::CCHQuery {}
150// (No Sync for CCHQuery)
151
152// Rust wrapper over FFI
153use cxx::UniquePtr;
154use ffi::*;
155pub use ffi::{
156    cch_compute_order_degree as compute_order_degree_unchecked,
157    cch_compute_order_inertial as compute_order_inertial_unchecked,
158};
159
160/// RoutingKit's internal "infinity" weight (`RoutingKit::inf_weight`).
161/// A distance equal to this value means "unreachable"; input weights must not exceed it.
162pub const INF_WEIGHT: u32 = 2_147_483_647;
163
164fn is_permutation(arr: &[u32]) -> bool {
165    let n = arr.len();
166    let mut seen = vec![false; n];
167    for &val in arr {
168        if (val as usize) >= n || seen[val as usize] {
169            return false;
170        }
171        seen[val as usize] = true;
172    }
173    true
174}
175
176/// Lightweight fill-in reducing order heuristic: sort nodes by (degree, id) ascending.
177/// Fast but lower quality than nested dissection. Use if no coordinates or other data available.
178/// Panics if tail/head have inconsistent lengths or contain invalid node ids.
179pub fn compute_order_degree(node_count: u32, tail: &[u32], head: &[u32]) -> Vec<u32> {
180    assert!(
181        tail.iter()
182            .chain(head)
183            .max()
184            .map_or(true, |&v| v < node_count),
185        "tail/head contain node ids outside valid range"
186    );
187    assert!(
188        tail.len() == head.len(),
189        "tail and head arrays must have the same length"
190    );
191    unsafe { cch_compute_order_degree(node_count, tail, head) }
192}
193
194/// High-quality nested dissection order using inertial flow separators.
195/// Requires per-node coordinates (latitude/longitude) as input.
196/// Panics if tail/head have inconsistent lengths or contain invalid node ids,
197/// or if latitude/longitude lengths do not match node_count.
198pub fn compute_order_inertial(
199    node_count: u32,
200    tail: &[u32],
201    head: &[u32],
202    latitude: &[f32],
203    longitude: &[f32],
204) -> Vec<u32> {
205    assert!(
206        tail.iter()
207            .chain(head)
208            .max()
209            .map_or(true, |&v| v < node_count),
210        "tail/head contain node ids outside valid range"
211    );
212    assert!(
213        tail.len() == head.len(),
214        "tail and head arrays must have the same length"
215    );
216    assert!(
217        latitude.len() == (node_count as usize) && longitude.len() == (node_count as usize),
218        "latitude/longitude length must equal node count"
219    );
220    unsafe { cch_compute_order_inertial(node_count, tail, head, latitude, longitude) }
221}
222
223/// Immutable Customizable Contraction Hierarchy index.
224pub struct CCH {
225    inner: UniquePtr<ffi::CCH>,
226    edge_count: usize,
227    node_count: usize,
228}
229
230impl CCH {
231    /// Construct a new immutable Customizable Contraction Hierarchy index.
232    ///
233    /// Parameters:
234    /// * `order` – permutation of node ids (length = node count) produced by a fill‑in reducing
235    ///   nested dissection heuristic (e.g. [`compute_order_inertial`]) or a lightweight fallback.
236    /// * `tail`, `head` – parallel arrays encoding each directed arc `i` as `(tail[i], head[i])`.
237    ///   The ordering routine treats them as undirected; here they stay directed for queries.
238    /// * `log_message` – callback for logging progress messages during construction.
239    ///   May be `|_| {}` if you do not want any messages.
240    /// * `filter_always_inf_arcs` – if `true`, arcs whose weight will always be interpreted as
241    ///   an application defined 'infinity' placeholder may be removed during construction to
242    ///   reduce index size. (Typically keep `false` unless you prepared such a list.)
243    ///
244    /// Cost: preprocessing is more expensive than a single customization but usually far cheaper
245    /// than building a full classical CH of the same quality. Construction copies the input
246    /// slices; you may drop them afterwards.
247    ///
248    /// Thread-safety: resulting object is `Send + Sync` and read-only.
249    ///
250    /// Panics if `order` is not a valid permutation of node ids,
251    /// or if `tail`/`head` have inconsistent lengths or contain invalid node ids.
252    pub fn new(
253        order: &[u32],
254        tail: &[u32],
255        head: &[u32],
256        log_message: fn(&str),
257        filter_always_inf_arcs: bool,
258    ) -> Self {
259        assert!(
260            is_permutation(order),
261            "order array is not a valid permutation"
262        );
263        assert!(
264            tail.len() == head.len(),
265            "tail and head arrays must have the same length"
266        );
267        assert!(
268            tail.iter()
269                .chain(head)
270                .max()
271                .map_or(true, |&v| (v as usize) < order.len()),
272            "tail/head contain node ids outside valid range"
273        );
274        unsafe { Self::new_unchecked(order, tail, head, log_message, filter_always_inf_arcs) }
275    }
276
277    pub unsafe fn new_unchecked(
278        order: &[u32],
279        tail: &[u32],
280        head: &[u32],
281        log_message: fn(&str),
282        filter_always_inf_arcs: bool,
283    ) -> Self {
284        let cch = unsafe { cch_new(order, tail, head, log_message, filter_always_inf_arcs) };
285        CCH {
286            inner: cch,
287            edge_count: tail.len(),
288            node_count: order.len(),
289        }
290    }
291}
292
293/// A customized metric (weight binding) for a given [`CCH`].
294/// Keeps ownership of the weight vector so it can be mutated for partial updates.
295/// Thread-safety: `Send + Sync`; read-only for queries.
296pub struct CCHMetric<'a> {
297    inner: UniquePtr<ffi::CCHMetric>,
298    weights: Box<[u32]>, // The C++ side stores only a raw pointer; it is valid for the lifetime of `self`.
299    cch: &'a CCH,
300}
301
302impl<'a> CCHMetric<'a> {
303    /// Create and customize a metric (weight binding) for a given [`CCH`].
304    /// Owns the weight vector so that future partial updates can safely mutate it.
305    pub fn new(cch: &'a CCH, weights: Vec<u32>) -> Self {
306        assert!(
307            weights.len() == cch.edge_count,
308            "weights length must equal arc count",
309        );
310        let boxed: Box<[u32]> = weights.into_boxed_slice();
311        // Temporarily borrow as slice for FFI creation
312        let metric = unsafe {
313            let mut metric = cch_metric_new(&cch.inner, &boxed);
314            cch_metric_customize(metric.as_mut().unwrap());
315            metric
316        };
317        CCHMetric {
318            inner: metric,
319            weights: boxed,
320            cch,
321        }
322    }
323
324    /// Parallel customization variant.
325    #[cfg(feature = "openmp")]
326    pub fn parallel_new(cch: &'a CCH, weights: Vec<u32>, thread_count: u32) -> Self {
327        assert!(
328            weights.len() == cch.edge_count,
329            "weights length must equal arc count",
330        );
331        let boxed: Box<[u32]> = weights.into_boxed_slice();
332        let metric = unsafe {
333            let mut metric = cch_metric_new(&cch.inner, &boxed);
334            cch_metric_parallel_customize(metric.as_mut().unwrap(), thread_count);
335            metric
336        };
337        CCHMetric {
338            inner: metric,
339            weights: boxed,
340            cch,
341        }
342    }
343
344    /// weights slice
345    pub fn weights(&self) -> &[u32] {
346        &self.weights
347    }
348
349    /// Rebind this metric to a new weight vector and re-run customization,
350    /// reusing the internal shortcut-weight buffers instead of allocating a new metric.
351    ///
352    /// This is the intended cheap path for full re-customization (e.g. periodic
353    /// traffic updates). Requires exclusive access, so no queries may be borrowing
354    /// the metric (enforced by the borrow checker).
355    ///
356    /// Panics if `weights` length does not equal the arc count.
357    pub fn reset(&mut self, weights: Vec<u32>) {
358        assert!(
359            weights.len() == self.cch.edge_count,
360            "weights length must equal arc count",
361        );
362        self.weights = weights.into_boxed_slice();
363        unsafe {
364            cch_metric_reset(self.inner.as_mut().unwrap(), &self.weights);
365            cch_metric_customize(self.inner.as_mut().unwrap());
366        }
367    }
368
369    /// Like [`CCHMetric::reset`], but customizes in parallel.
370    /// `thread_count == 0` picks an internal default.
371    #[cfg(feature = "openmp")]
372    pub fn parallel_reset(&mut self, weights: Vec<u32>, thread_count: u32) {
373        assert!(
374            weights.len() == self.cch.edge_count,
375            "weights length must equal arc count",
376        );
377        self.weights = weights.into_boxed_slice();
378        unsafe {
379            cch_metric_reset(self.inner.as_mut().unwrap(), &self.weights);
380            cch_metric_parallel_customize(self.inner.as_mut().unwrap(), thread_count);
381        }
382    }
383}
384
385/// Reusable partial customization helper. Construct once if you perform many small incremental
386/// weight updates; this avoids reallocating O(m) internal buffers each call.
387pub struct CCHMetricPartialUpdater<'a> {
388    partial: UniquePtr<ffi::CCHPartial>,
389    cch: &'a CCH,
390}
391
392impl<'a> CCHMetricPartialUpdater<'a> {
393    /// Create a reusable partial updater bound to a given CCH. You can then apply it to any
394    /// metric built from the same CCH (even if you rebuild metrics with different weight sets).
395    pub fn new(cch: &'a CCH) -> Self {
396        let partial = unsafe { cch_partial_new(cch.inner.as_ref().unwrap()) };
397        CCHMetricPartialUpdater { partial, cch }
398    }
399
400    /// Apply a batch of (arc, new_weight) updates to the given metric and run partial customize.
401    pub fn apply<T>(&mut self, metric: &mut CCHMetric<'a>, updates: &T)
402    where
403        T: for<'b> std::ops::Index<&'b u32, Output = u32>,
404        for<'b> &'b T: IntoIterator<Item = (&'b u32, &'b u32)>,
405    {
406        assert!(
407            std::ptr::eq(metric.cch, self.cch),
408            "CCHMetricPartialUpdater must be used with metrics from the same CCH"
409        );
410        for (k, v) in updates {
411            metric.weights[*k as usize] = *v; // safe: length invariant unchanged (Box<[u32]>)
412        }
413        unsafe {
414            cch_partial_reset(self.partial.as_mut().unwrap());
415            for (k, _) in updates {
416                cch_partial_update_arc(self.partial.as_mut().unwrap(), *k);
417            }
418            cch_partial_customize(
419                self.partial.as_mut().unwrap(),
420                metric.inner.as_mut().unwrap(),
421            );
422        }
423    }
424}
425
426/// A reusable shortest-path query object bound to a given [`CCHMetric`].
427/// Thread-safety: `Send` but not `Sync`; holds mutable per-query state.
428pub struct CCHQuery<'a> {
429    inner: UniquePtr<ffi::CCHQuery>,
430    metric: &'a CCHMetric<'a>,
431    state: [bool; 2],
432}
433
434impl<'a> CCHQuery<'a> {
435    /// Allocate a new reusable shortest-path query bound to a given customized [`CCHMetric`].
436    ///
437    /// The query object stores its own frontier / label buffers and can be reset and reused for
438    /// many (s, t) pairs or multi-source / multi-target batches. You may have multiple query
439    /// objects referencing the same metric concurrently (read-only access to metric data).
440    pub fn new(metric: &'a CCHMetric<'a>) -> Self {
441        let inner = unsafe { cch_query_new(&metric.inner) };
442        CCHQuery {
443            inner,
444            metric,
445            state: [false; 2],
446        }
447    }
448
449    /// Add a source node with an initial distance (normally 0). Multiple calls allow a multi-
450    /// source query. Distances let you model already-traversed partial paths.
451    pub fn add_source(&mut self, s: u32, dist: u32) {
452        assert!(
453            (s as usize) < self.metric.cch.node_count,
454            "source node id out of range",
455        );
456        unsafe {
457            cch_query_add_source(self.inner.as_mut().unwrap(), s, dist);
458        }
459        self.state[0] = true;
460    }
461
462    /// Add a target node with an initial distance (normally 0). Multiple calls allow multi-target
463    /// queries; the algorithm stops when the frontiers settle the optimal distance to any target.
464    pub fn add_target(&mut self, t: u32, dist: u32) {
465        assert!(
466            (t as usize) < self.metric.cch.node_count,
467            "target node id out of range",
468        );
469        unsafe {
470            cch_query_add_target(self.inner.as_mut().unwrap(), t, dist);
471        }
472        self.state[1] = true;
473    }
474
475    /// Execute the forward/backward upward/downward search to settle the shortest path between the
476    /// added sources and targets. Must be called after at least one source and one target.
477    /// Returns a [`CCHQueryResult`] holding a mutable reference to the query.
478    /// The query is automatically reset when the result is dropped.
479    pub fn run<'b>(&'b mut self) -> CCHQueryResult<'b, 'a> {
480        assert!(
481            self.state.iter().all(|&x| x),
482            "must add at least one source and one target before running the query"
483        );
484        unsafe {
485            cch_query_run(self.inner.as_mut().unwrap());
486        }
487        CCHQueryResult { query: self }
488    }
489}
490
491/// The result of a single execution of a [`CCHQuery`].
492/// Holds a mutable reference to the query to ensure it is not reset or reused
493/// while the result is still alive. So you need to drop the result before reusing the query.
494/// When the result is dropped, the query is automatically reset.
495pub struct CCHQueryResult<'b, 'a> {
496    query: &'b mut CCHQuery<'a>,
497}
498
499impl<'b, 'a> CCHQueryResult<'b, 'a> {
500    /// Return the shortest path distance result of [`CCHQuery::run`].
501    /// Returns `None` if no target is reachable.
502    pub fn distance(&self) -> Option<u32> {
503        let res = unsafe { cch_query_distance(self.query.inner.as_ref().unwrap()) };
504        if res == INF_WEIGHT {
505            // internally distance equals `inf_weight` means unreachable
506            None
507        } else {
508            Some(res)
509        }
510    }
511
512    /// Reconstruct and return the node id sequence of the current best path.
513    /// Returns empty vec if no target is reachable.
514    pub fn node_path(&self) -> Vec<u32> {
515        unsafe { cch_query_node_path(self.query.inner.as_ref().unwrap()) }
516    }
517
518    /// Reconstruct and return the original arc ids along the shortest path.
519    /// Returns empty vec if no target is reachable.
520    pub fn arc_path(&self) -> Vec<u32> {
521        unsafe { cch_query_arc_path(self.query.inner.as_ref().unwrap()) }
522    }
523}
524
525impl<'b, 'a> Drop for CCHQueryResult<'b, 'a> {
526    /// reset the query
527    fn drop(&mut self) {
528        unsafe {
529            cch_query_reset(
530                self.query.inner.as_mut().unwrap(),
531                self.query.metric.inner.as_ref().unwrap(),
532            );
533        }
534        self.query.state.iter_mut().for_each(|x| *x = false);
535    }
536}
537
538/// A reusable one-to-many query with a fixed (pinned) set of targets.
539///
540/// Pinning precomputes the search space of the targets once; afterwards each
541/// [`CCHOneToMany::distances_from`] call answers distances from a source to *all*
542/// pinned targets in a single elimination-tree sweep, which is much faster than
543/// running one point-to-point query per target. Use this for distance tables /
544/// matrices (one `CCHOneToMany` per row) or k-nearest style workloads.
545///
546/// Thread-safety: `Send` but not `Sync`; holds mutable per-query state.
547pub struct CCHOneToMany<'a> {
548    inner: UniquePtr<ffi::CCHQuery>,
549    metric: &'a CCHMetric<'a>,
550    target_count: usize,
551}
552
553impl<'a> CCHOneToMany<'a> {
554    /// Create a one-to-many query bound to `metric` with a fixed set of pinned targets.
555    /// Distances returned later are aligned with the order of `targets`
556    /// (duplicates are allowed and answered per entry).
557    ///
558    /// Panics if any target node id is out of range.
559    pub fn new(metric: &'a CCHMetric<'a>, targets: &[u32]) -> Self {
560        assert!(!targets.is_empty(), "must provide at least one target");
561        assert!(
562            targets
563                .iter()
564                .all(|&t| (t as usize) < metric.cch.node_count),
565            "target node id out of range",
566        );
567        let mut inner = unsafe { cch_query_new(&metric.inner) };
568        unsafe { cch_query_pin_targets(inner.as_mut().unwrap(), targets) };
569        CCHOneToMany {
570            inner,
571            metric,
572            target_count: targets.len(),
573        }
574    }
575
576    /// Number of pinned targets (= length of the distance vectors returned).
577    pub fn target_count(&self) -> usize {
578        self.target_count
579    }
580
581    /// Replace the pinned target set, reusing the query's internal O(n) label
582    /// buffers (cheaper than constructing a new [`CCHOneToMany`]).
583    ///
584    /// Panics if any target node id is out of range.
585    pub fn repin_targets(&mut self, targets: &[u32]) {
586        assert!(!targets.is_empty(), "must provide at least one target");
587        assert!(
588            targets
589                .iter()
590                .all(|&t| (t as usize) < self.metric.cch.node_count),
591            "target node id out of range",
592        );
593        // Validation done above: the FFI section below cannot panic.
594        unsafe {
595            cch_query_reset(
596                self.inner.as_mut().unwrap(),
597                self.metric.inner.as_ref().unwrap(),
598            );
599            cch_query_pin_targets(self.inner.as_mut().unwrap(), targets);
600        }
601        self.target_count = targets.len();
602    }
603
604    /// Compute the shortest distances from `source` to every pinned target.
605    /// Result is aligned with the pinned target order; `None` = unreachable.
606    pub fn distances_from(&mut self, source: u32) -> Vec<Option<u32>> {
607        self.distances_from_multi(&[(source, 0)])
608    }
609
610    /// Multi-source variant: for each pinned target `t`, returns
611    /// `min` over the given `(source, initial_dist)` pairs of `initial_dist + dist(source, t)`.
612    ///
613    /// Panics if `sources` is empty or contains node ids out of range.
614    pub fn distances_from_multi(&mut self, sources: &[(u32, u32)]) -> Vec<Option<u32>> {
615        assert!(!sources.is_empty(), "must provide at least one source");
616        assert!(
617            sources
618                .iter()
619                .all(|&(s, _)| (s as usize) < self.metric.cch.node_count),
620            "source node id out of range",
621        );
622        // All validation is done above: the FFI section below cannot panic, so the
623        // query always returns to the "targets pinned, no sources" state.
624        unsafe {
625            for &(s, d) in sources {
626                cch_query_add_source(self.inner.as_mut().unwrap(), s, d);
627            }
628            cch_query_run_to_pinned_targets(self.inner.as_mut().unwrap());
629            let dist = cch_query_distances_to_targets(self.inner.as_ref().unwrap());
630            cch_query_reset_source(self.inner.as_mut().unwrap());
631            dist.into_iter()
632                .map(|d| if d == INF_WEIGHT { None } else { Some(d) })
633                .collect()
634        }
635    }
636}
637
638/// A reusable many-to-one query with a fixed (pinned) set of sources.
639///
640/// The mirror image of [`CCHOneToMany`]: pinning precomputes the search space of
641/// the sources once; each [`CCHManyToOne::distances_to`] call answers distances
642/// from *all* pinned sources to a target in a single sweep.
643///
644/// Thread-safety: `Send` but not `Sync`; holds mutable per-query state.
645pub struct CCHManyToOne<'a> {
646    inner: UniquePtr<ffi::CCHQuery>,
647    metric: &'a CCHMetric<'a>,
648    source_count: usize,
649}
650
651impl<'a> CCHManyToOne<'a> {
652    /// Create a many-to-one query bound to `metric` with a fixed set of pinned sources.
653    /// Distances returned later are aligned with the order of `sources`
654    /// (duplicates are allowed and answered per entry).
655    ///
656    /// Panics if any source node id is out of range.
657    pub fn new(metric: &'a CCHMetric<'a>, sources: &[u32]) -> Self {
658        assert!(!sources.is_empty(), "must provide at least one source");
659        assert!(
660            sources
661                .iter()
662                .all(|&s| (s as usize) < metric.cch.node_count),
663            "source node id out of range",
664        );
665        let mut inner = unsafe { cch_query_new(&metric.inner) };
666        unsafe { cch_query_pin_sources(inner.as_mut().unwrap(), sources) };
667        CCHManyToOne {
668            inner,
669            metric,
670            source_count: sources.len(),
671        }
672    }
673
674    /// Number of pinned sources (= length of the distance vectors returned).
675    pub fn source_count(&self) -> usize {
676        self.source_count
677    }
678
679    /// Replace the pinned source set, reusing the query's internal O(n) label
680    /// buffers (cheaper than constructing a new [`CCHManyToOne`]).
681    ///
682    /// Panics if any source node id is out of range.
683    pub fn repin_sources(&mut self, sources: &[u32]) {
684        assert!(!sources.is_empty(), "must provide at least one source");
685        assert!(
686            sources
687                .iter()
688                .all(|&s| (s as usize) < self.metric.cch.node_count),
689            "source node id out of range",
690        );
691        // Validation done above: the FFI section below cannot panic.
692        unsafe {
693            cch_query_reset(
694                self.inner.as_mut().unwrap(),
695                self.metric.inner.as_ref().unwrap(),
696            );
697            cch_query_pin_sources(self.inner.as_mut().unwrap(), sources);
698        }
699        self.source_count = sources.len();
700    }
701
702    /// Compute the shortest distances from every pinned source to `target`.
703    /// Result is aligned with the pinned source order; `None` = unreachable.
704    pub fn distances_to(&mut self, target: u32) -> Vec<Option<u32>> {
705        self.distances_to_multi(&[(target, 0)])
706    }
707
708    /// Multi-target variant: for each pinned source `s`, returns
709    /// `min` over the given `(target, initial_dist)` pairs of `dist(s, target) + initial_dist`.
710    ///
711    /// Panics if `targets` is empty or contains node ids out of range.
712    pub fn distances_to_multi(&mut self, targets: &[(u32, u32)]) -> Vec<Option<u32>> {
713        assert!(!targets.is_empty(), "must provide at least one target");
714        assert!(
715            targets
716                .iter()
717                .all(|&(t, _)| (t as usize) < self.metric.cch.node_count),
718            "target node id out of range",
719        );
720        // All validation is done above: the FFI section below cannot panic, so the
721        // query always returns to the "sources pinned, no targets" state.
722        unsafe {
723            for &(t, d) in targets {
724                cch_query_add_target(self.inner.as_mut().unwrap(), t, d);
725            }
726            cch_query_run_to_pinned_sources(self.inner.as_mut().unwrap());
727            let dist = cch_query_distances_to_sources(self.inner.as_ref().unwrap());
728            cch_query_reset_target(self.inner.as_mut().unwrap());
729            dist.into_iter()
730                .map(|d| if d == INF_WEIGHT { None } else { Some(d) })
731                .collect()
732        }
733    }
734}
735
736#[cfg(feature = "pyo3")]
737mod python_binding;