deke_multipath/lib.rs
1//! Optimal ordering and orientation of *required* robot paths.
2//!
3//! Given a set of paths that must each be traversed exactly once — some of
4//! which may be reversible or have several interchangeable realizations — this
5//! crate chooses one option per required path and orders them to minimise total
6//! motion cost, starting at a configuration and optionally ending at one. It
7//! returns the full stitched motion: connector segments interleaved with the
8//! chosen required paths.
9//!
10//! The ordering is an asymmetric generalized TSP (see [`agtsp`]); cost is a
11//! pluggable joint-space metric ([`TransitionCost`]). The planner is only used
12//! to *generate* connector paths and is optional — [`plan_multipath`] uses a
13//! planner for obstacle-aware connectors, [`plan_multipath_straight`] emits
14//! validated straight-line connectors instead.
15//!
16//! ```no_run
17//! # use deke_multipath::*;
18//! # use deke_types::{SRobotQ, SRobotPath, Planner, Validator};
19//! # fn go<'ctx, const N: usize, P, V, MW>(
20//! # paths: Vec<ReqPath<N>>, planner: &P, cfg: &P::Config, make_wp: MW,
21//! # validator: &V, ctx: &V::Context<'ctx>, start: SRobotQ<N, f64>, weights: SRobotQ<N, f64>,
22//! # ) -> MultipathResult<Vec<SRobotPath<N, f64>>>
23//! # where P: Planner<N, f64> + Sync, P::Config: Sync, V: Validator<N, (), f64>,
24//! # V::Context<'ctx>: Sync,
25//! # MW: Fn(SRobotQ<N, f64>, SRobotQ<N, f64>) -> P::Waypoints + Sync {
26//! let settings = MultiPathSettings::new(start);
27//! let cost = TransitionCost::JointWeighted(weights);
28//! let transition = TransitionPlanner { planner, config: cfg, make_waypoints: make_wp };
29//! plan_multipath(&paths, &cost, &settings, &transition, validator, ctx)
30//! # }
31//! ```
32//!
33//! # Feature flags
34//!
35//! - `rayon` — use rayon to plan connectors concurrently and to fan out the
36//! heuristic's seed tours. Worth enabling when connector planning is the
37//! bottleneck (real collision-checked RRT, where each plan costs milliseconds)
38//! or for large heuristic instances; for trivially cheap connectors the
39//! thread-pool dispatch can cost more than it saves. The exact Held–Karp
40//! solver stays sequential — its layers depend on one another, so it does not
41//! parallelize without a structural rewrite. Enabling it adds `Sync` bounds on
42//! the planner, its config, the waypoint builder, and the validator context
43//! (all satisfied by the deke planners/validators).
44
45mod agtsp;
46mod cost;
47mod error;
48mod reqpath;
49
50use deke_types::{DekeError, Planner, SRobotPath, SRobotQ, Validator};
51
52use agtsp::Problem;
53use cost::build_matrices;
54use reqpath::{DirectedOption, expand};
55
56pub use cost::{TransitionCost, weighted_distance};
57pub use error::{MultipathError, MultipathResult};
58pub use reqpath::ReqPath;
59
60/// `Sync` exactly when the `rayon` feature is enabled. The public API requires
61/// thread-safety only when work is actually dispatched across the rayon pool;
62/// the sequential default build imposes no such bound. Blanket-implemented for
63/// every type, so callers never name or implement it — it only appears in the
64/// public bounds, hence `#[doc(hidden)] pub`.
65#[doc(hidden)]
66#[cfg(feature = "rayon")]
67pub trait MaybeSync: Sync {}
68#[cfg(feature = "rayon")]
69impl<T: Sync> MaybeSync for T {}
70
71#[doc(hidden)]
72#[cfg(not(feature = "rayon"))]
73pub trait MaybeSync {}
74#[cfg(not(feature = "rayon"))]
75impl<T> MaybeSync for T {}
76
77/// Knobs for a multipath solve. Build with [`MultiPathSettings::new`].
78pub struct MultiPathSettings<const N: usize> {
79 /// Configuration the motion starts from.
80 pub start: SRobotQ<N, f64>,
81 /// Optional configuration the motion must finish at.
82 pub end: Option<SRobotQ<N, f64>>,
83 /// Connectors whose endpoints coincide within this (unweighted) joint
84 /// distance are skipped — no point planning a move that goes nowhere.
85 pub dedup_tol: f64,
86 /// Above this many DP cells the solver switches from exact Held–Karp to the
87 /// nearest-neighbour + 2-opt heuristic.
88 pub exact_cell_budget: usize,
89}
90
91impl<const N: usize> MultiPathSettings<N> {
92 pub fn new(start: SRobotQ<N, f64>) -> Self {
93 Self {
94 start,
95 end: None,
96 dedup_tol: 1e-6,
97 exact_cell_budget: agtsp::DEFAULT_CELL_BUDGET,
98 }
99 }
100
101 pub fn with_end(mut self, end: SRobotQ<N, f64>) -> Self {
102 self.end = Some(end);
103 self
104 }
105}
106
107/// Everything needed to generate real connector paths with a deke planner. The
108/// `make_waypoints` closure bridges the planner's associated `Waypoints` type —
109/// for the RRT planners that is `|s, e| StartEnd { start: s, end: e }`.
110pub struct TransitionPlanner<'a, const N: usize, P, MW>
111where
112 P: Planner<N, f64>,
113{
114 pub planner: &'a P,
115 pub config: &'a P::Config,
116 pub make_waypoints: MW,
117}
118
119/// Solve the ordering and stitch the full motion using `transition` to plan
120/// obstacle-aware connectors between the chosen paths.
121pub fn plan_multipath<'ctx, const N: usize, P, V, MW>(
122 req_paths: &[ReqPath<N>],
123 cost: &TransitionCost<N>,
124 settings: &MultiPathSettings<N>,
125 transition: &TransitionPlanner<'_, N, P, MW>,
126 validator: &V,
127 ctx: &V::Context<'ctx>,
128) -> MultipathResult<Vec<SRobotPath<N, f64>>>
129where
130 P: Planner<N, f64> + MaybeSync,
131 P::Config: MaybeSync,
132 V: Validator<N, (), f64>,
133 V::Context<'ctx>: MaybeSync,
134 MW: Fn(SRobotQ<N, f64>, SRobotQ<N, f64>) -> P::Waypoints + MaybeSync,
135{
136 run(req_paths, cost, settings, |from, to| {
137 let waypoints = (transition.make_waypoints)(*from, *to);
138 let (path, _diag) =
139 transition
140 .planner
141 .plan::<DekeError, _>(transition.config, &waypoints, validator, ctx);
142 Ok(path?)
143 })
144}
145
146/// Solve the ordering and stitch the full motion using straight joint-space
147/// connectors. Each connector is checked with `validator.validate_motion`; if a
148/// straight line between two required paths is infeasible the solve fails (there
149/// is no planner to route around the obstacle).
150pub fn plan_multipath_straight<'ctx, const N: usize, V>(
151 req_paths: &[ReqPath<N>],
152 cost: &TransitionCost<N>,
153 settings: &MultiPathSettings<N>,
154 validator: &V,
155 ctx: &V::Context<'ctx>,
156) -> MultipathResult<Vec<SRobotPath<N, f64>>>
157where
158 V: Validator<N, (), f64>,
159 V::Context<'ctx>: MaybeSync,
160{
161 run(req_paths, cost, settings, |from, to| {
162 validator.validate_motion(&[*from, *to], ctx)?;
163 Ok(SRobotPath::from_two(*from, *to))
164 })
165}
166
167fn run<const N: usize, C>(
168 req_paths: &[ReqPath<N>],
169 cost: &TransitionCost<N>,
170 settings: &MultiPathSettings<N>,
171 connect: C,
172) -> MultipathResult<Vec<SRobotPath<N, f64>>>
173where
174 C: Fn(&SRobotQ<N, f64>, &SRobotQ<N, f64>) -> MultipathResult<SRobotPath<N, f64>> + MaybeSync,
175{
176 let (options, n_clusters) = expand(req_paths)?;
177 let matrices = build_matrices(&options, cost, &settings.start, settings.end.as_ref());
178 let cluster_ids: Vec<usize> = options.iter().map(|o| o.cluster).collect();
179 let problem = Problem {
180 cluster_ids: &cluster_ids,
181 n_clusters,
182 transition: &matrices.transition,
183 options: options.len(),
184 start: &matrices.start,
185 end: &matrices.end,
186 };
187 let solution =
188 agtsp::solve(&problem, settings.exact_cell_budget).ok_or(MultipathError::NoFeasibleTour)?;
189 tracing::debug!(
190 n_required = req_paths.len(),
191 n_options = options.len(),
192 cost = solution.cost,
193 "deke-multipath: ordering solved"
194 );
195 assemble(options, &solution.order, settings, connect)
196}
197
198/// One element of the stitched plan before connectors are realized: either a
199/// chosen required path (already oriented) or a connector that still needs to be
200/// planned between two configurations.
201enum Segment<const N: usize> {
202 Connector(SRobotQ<N, f64>, SRobotQ<N, f64>),
203 Required(SRobotPath<N, f64>),
204}
205
206fn assemble<const N: usize, C>(
207 options: Vec<DirectedOption<N>>,
208 order: &[usize],
209 settings: &MultiPathSettings<N>,
210 connect: C,
211) -> MultipathResult<Vec<SRobotPath<N, f64>>>
212where
213 C: Fn(&SRobotQ<N, f64>, &SRobotQ<N, f64>) -> MultipathResult<SRobotPath<N, f64>> + MaybeSync,
214{
215 // Lay out the plan as connector/required segments. Connectors are
216 // independent point-to-point plans, so they can be realized concurrently
217 // (the costly RRT case); the required paths just pass through. The tour
218 // visits each option at most once, so the chosen paths are moved out rather
219 // than cloned.
220 let mut options: Vec<Option<DirectedOption<N>>> = options.into_iter().map(Some).collect();
221 let mut segments: Vec<Segment<N>> = Vec::new();
222 let mut cursor = settings.start;
223 for &oi in order {
224 let path = options[oi]
225 .take()
226 .expect("each option appears once in the tour")
227 .path;
228 let first = *path.first();
229 if cursor.distance(&first) > settings.dedup_tol {
230 segments.push(Segment::Connector(cursor, first));
231 }
232 cursor = *path.last();
233 segments.push(Segment::Required(path));
234 }
235 if let Some(end) = settings.end
236 && cursor.distance(&end) > settings.dedup_tol
237 {
238 segments.push(Segment::Connector(cursor, end));
239 }
240
241 let realize = |segment: Segment<N>| -> MultipathResult<SRobotPath<N, f64>> {
242 match segment {
243 Segment::Connector(from, to) => connect(&from, &to),
244 Segment::Required(path) => Ok(path),
245 }
246 };
247
248 #[cfg(feature = "rayon")]
249 {
250 use rayon::prelude::*;
251 segments.into_par_iter().map(realize).collect()
252 }
253 #[cfg(not(feature = "rayon"))]
254 {
255 segments.into_iter().map(realize).collect()
256 }
257}