1use deke_types::glam::{DAffine3, DQuat, DVec3};
15use deke_types::{
16 ContinuousFKChain, DekeError, DekeResult, IkOutcome, IkSolver, Planner, SRobotPath, SRobotQ,
17 Validator,
18};
19
20use crate::constraints::PlannerOptions;
21use crate::diagnostic::RedundantDiagnostic;
22use crate::error::LinearError;
23use crate::path::CartesianRun;
24use crate::planner::is_reconfiguration;
25use crate::util::{interp, ladder_dp};
26
27#[derive(Clone, Copy, Debug, PartialEq)]
31pub enum RedundantAxis {
32 PosX,
33 NegX,
34 PosY,
35 NegY,
36 PosZ,
37 NegZ,
38 Custom(DVec3),
40}
41
42impl RedundantAxis {
43 pub fn vector(&self) -> DVec3 {
45 match self {
46 RedundantAxis::PosX => DVec3::X,
47 RedundantAxis::NegX => DVec3::NEG_X,
48 RedundantAxis::PosY => DVec3::Y,
49 RedundantAxis::NegY => DVec3::NEG_Y,
50 RedundantAxis::PosZ => DVec3::Z,
51 RedundantAxis::NegZ => DVec3::NEG_Z,
52 RedundantAxis::Custom(v) => v.normalize_or_zero(),
53 }
54 }
55}
56
57#[derive(Clone, Debug)]
59pub struct RedundantOptions {
60 pub axis: RedundantAxis,
62 pub yaw_window: (f64, f64),
64 pub yaw_samples: usize,
66 pub dp_ds: f64,
68 pub yaw_rate_weight: f64,
70 pub max_yaw_step: f64,
72}
73
74impl Default for RedundantOptions {
75 fn default() -> Self {
76 Self {
77 axis: RedundantAxis::PosZ,
78 yaw_window: (-std::f64::consts::PI, std::f64::consts::PI),
79 yaw_samples: 24,
80 dp_ds: 5e-3,
81 yaw_rate_weight: 0.2,
82 max_yaw_step: 0.6,
83 }
84 }
85}
86
87#[derive(Clone, Debug)]
90pub struct RedundantConfig<const N: usize> {
91 pub planner: PlannerOptions<N>,
92 pub redundant: RedundantOptions,
93}
94
95struct YawNode<const N: usize> {
96 yaw: f64,
97 q: SRobotQ<N, f64>,
98 cost: f64,
99}
100
101#[derive(Clone, Debug)]
103pub struct RedundantLinearPlanner<'a, const N: usize, FK> {
104 fk: &'a FK,
105}
106
107impl<'a, const N: usize, FK> RedundantLinearPlanner<'a, N, FK>
108where
109 FK: ContinuousFKChain<N, f64> + IkSolver<N, f64>,
110{
111 pub fn new(fk: &'a FK) -> Self {
112 Self { fk }
113 }
114
115 #[allow(clippy::too_many_arguments)]
116 pub(crate) fn plan_run<V: Validator<N, (), f64>>(
117 &self,
118 run: &CartesianRun,
119 planner: &PlannerOptions<N>,
120 red: &RedundantOptions,
121 validator: &V,
122 ctx: &V::Context<'_>,
123 seed: Option<&SRobotQ<N, f64>>,
124 run_idx: usize,
125 ) -> Result<(SRobotPath<N, f64>, RedundantDiagnostic), LinearError> {
126 let axis = red.axis.vector();
127 let length = run.length();
128 let n_dp = ((length / red.dp_ds).ceil() as usize).max(1) + 1;
129
130 let stations = self.build_stations(
131 run, red, planner, axis, length, n_dp, validator, ctx, run_idx,
132 )?;
133
134 let (coarse_s, coarse_psi) = solve_global(&stations, red, planner, length, n_dp)
135 .ok_or(LinearError::NoContinuousTrack { run: run_idx })?;
136
137 self.refine(
138 run,
139 planner,
140 axis,
141 length,
142 &coarse_s,
143 &coarse_psi,
144 validator,
145 ctx,
146 seed,
147 run_idx,
148 )
149 }
150
151 #[allow(clippy::too_many_arguments)]
152 fn build_stations<V: Validator<N, (), f64>>(
153 &self,
154 run: &CartesianRun,
155 red: &RedundantOptions,
156 planner: &PlannerOptions<N>,
157 axis: DVec3,
158 length: f64,
159 n_dp: usize,
160 validator: &V,
161 ctx: &V::Context<'_>,
162 run_idx: usize,
163 ) -> Result<Vec<Vec<YawNode<N>>>, LinearError> {
164 let samples = red.yaw_samples.max(1);
165 let yaw_at = |m: usize| -> f64 {
166 if samples <= 1 {
167 0.5 * (red.yaw_window.0 + red.yaw_window.1)
168 } else {
169 red.yaw_window.0
170 + (red.yaw_window.1 - red.yaw_window.0) * m as f64 / (samples - 1) as f64
171 }
172 };
173
174 let mut stations = Vec::with_capacity(n_dp);
175 for k in 0..n_dp {
176 let s = (k as f64 * red.dp_ds).min(length);
177 let ref_pose = run.eval(s);
178 let ref_rot = DQuat::from_mat3(&ref_pose.matrix3);
179 let mut nodes = Vec::with_capacity(samples * 4);
180 let mut had_ik = false;
181 for m in 0..samples {
182 let psi = yaw_at(m);
183 let rot = ref_rot * DQuat::from_axis_angle(axis, psi);
184 let target = DAffine3::from_rotation_translation(rot, ref_pose.translation);
185 if let IkOutcome::Solved(sols) = self.fk.ik(target)? {
186 had_ik |= !sols.is_empty();
187 for q in sols {
188 if validator.validate(q, ctx).is_err() {
189 continue;
190 }
191 let w = self.fk.manipulability(&q).map_err(LinearError::Deke)?;
192 nodes.push(YawNode {
193 yaw: psi,
194 q,
195 cost: planner.manip_weight / (w + 1e-9),
196 });
197 }
198 }
199 }
200 if nodes.is_empty() {
201 return Err(if had_ik {
203 LinearError::Obstructed { run: run_idx, s }
204 } else {
205 LinearError::Unreachable { run: run_idx, s }
206 });
207 }
208 stations.push(nodes);
209 }
210 Ok(stations)
211 }
212
213 #[allow(clippy::too_many_arguments)]
214 fn refine<V: Validator<N, (), f64>>(
215 &self,
216 run: &CartesianRun,
217 planner: &PlannerOptions<N>,
218 axis: DVec3,
219 length: f64,
220 coarse_s: &[f64],
221 coarse_psi: &[f64],
222 validator: &V,
223 ctx: &V::Context<'_>,
224 seed: Option<&SRobotQ<N, f64>>,
225 run_idx: usize,
226 ) -> Result<(SRobotPath<N, f64>, RedundantDiagnostic), LinearError> {
227 let n_fine = ((length / planner.sample_ds).ceil() as usize).max(1) + 1;
228 let mut fine: Vec<SRobotQ<N, f64>> = Vec::with_capacity(n_fine);
229 let mut min_manip = f64::INFINITY;
230 let mut yaw_lo = f64::INFINITY;
231 let mut yaw_hi = f64::NEG_INFINITY;
232 let mut prev: Option<(SRobotQ<N, f64>, f64)> = None;
233
234 for i in 0..n_fine {
235 let s = (i as f64 * planner.sample_ds).min(length);
236 let psi = interp(coarse_s, coarse_psi, s);
237 yaw_lo = yaw_lo.min(psi);
238 yaw_hi = yaw_hi.max(psi);
239 let ref_pose = run.eval(s);
240 let rot = DQuat::from_mat3(&ref_pose.matrix3) * DQuat::from_axis_angle(axis, psi);
241 let target = DAffine3::from_rotation_translation(rot, ref_pose.translation);
242 let raw = match self.fk.ik(target)? {
243 IkOutcome::Solved(sols) if !sols.is_empty() => sols,
244 _ => return Err(LinearError::Unreachable { run: run_idx, s }),
245 };
246 let sols: Vec<SRobotQ<N, f64>> = raw
247 .into_iter()
248 .filter(|q| validator.validate(*q, ctx).is_ok())
249 .collect();
250 if sols.is_empty() {
251 return Err(LinearError::Obstructed { run: run_idx, s });
252 }
253
254 let q = match prev {
255 Some((pq, _)) => sols
256 .into_iter()
257 .min_by(|a, b| pq.distance(a).total_cmp(&pq.distance(b)))
258 .unwrap(),
259 None => match seed {
260 Some(s) => sols
261 .into_iter()
262 .filter(|q| !is_reconfiguration(s, q, f64::INFINITY, planner))
263 .min_by(|a, b| s.distance(a).total_cmp(&s.distance(b)))
264 .ok_or(LinearError::NoContinuousTrack { run: run_idx })?,
265 None => {
266 let mut best = sols[0];
267 let mut best_w = -1.0;
268 for q in sols {
269 let w = self.fk.manipulability(&q).map_err(LinearError::Deke)?;
270 if w > best_w {
271 best_w = w;
272 best = q;
273 }
274 }
275 best
276 }
277 },
278 };
279
280 if let Some((pq, ps)) = prev {
281 let dsf = (s - ps).max(1e-12);
282 if is_reconfiguration(&pq, &q, dsf, planner) {
283 return Err(LinearError::NoContinuousTrack { run: run_idx });
284 }
285 }
286
287 min_manip = min_manip.min(self.fk.manipulability(&q).map_err(LinearError::Deke)?);
288 fine.push(q);
289 prev = Some((q, s));
290 }
291
292 let path = SRobotPath::try_new(fine).map_err(LinearError::from)?;
293 Ok((
294 path,
295 RedundantDiagnostic {
296 samples: n_fine,
297 min_manipulability: min_manip,
298 yaw_range: (yaw_lo, yaw_hi),
299 },
300 ))
301 }
302}
303
304impl<'a, const N: usize, FK> Planner<N, f64> for RedundantLinearPlanner<'a, N, FK>
305where
306 FK: ContinuousFKChain<N, f64> + IkSolver<N, f64>,
307{
308 type Diagnostic = RedundantDiagnostic;
309 type Config = RedundantConfig<N>;
310 type Waypoints = CartesianRun;
311
312 fn plan<E: Into<DekeError>, V: Validator<N, (), f64>>(
313 &self,
314 config: &Self::Config,
315 waypoints: &Self::Waypoints,
316 validator: &V,
317 ctx: &V::Context<'_>,
318 ) -> (DekeResult<SRobotPath<N, f64>>, Self::Diagnostic) {
319 match self.plan_run(
320 waypoints,
321 &config.planner,
322 &config.redundant,
323 validator,
324 ctx,
325 None,
326 0,
327 ) {
328 Ok((path, diag)) => (Ok(path), diag),
329 Err(e) => (
330 Err(e.into()),
331 RedundantDiagnostic {
332 samples: 0,
333 min_manipulability: 0.0,
334 yaw_range: (0.0, 0.0),
335 },
336 ),
337 }
338 }
339}
340
341fn solve_global<const N: usize>(
344 stations: &[Vec<YawNode<N>>],
345 red: &RedundantOptions,
346 planner: &PlannerOptions<N>,
347 length: f64,
348 n_dp: usize,
349) -> Option<(Vec<f64>, Vec<f64>)> {
350 let ds_at = |k: usize| {
351 ((k as f64 * red.dp_ds).min(length) - ((k - 1) as f64 * red.dp_ds).min(length)).max(1e-9)
352 };
353 let layer_sizes: Vec<usize> = stations.iter().map(Vec::len).collect();
354 let (chosen, _) = ladder_dp(
355 &layer_sizes,
356 |k, i| stations[k][i].cost,
357 |k, p, i| {
358 let na = &stations[k - 1][p];
359 let nb = &stations[k][i];
360 let dyaw = (nb.yaw - na.yaw).abs();
361 let ds = ds_at(k);
362 if dyaw > red.max_yaw_step || is_reconfiguration(&na.q, &nb.q, ds, planner) {
363 None
364 } else {
365 Some(na.q.distance(&nb.q) + red.yaw_rate_weight * dyaw / ds)
366 }
367 },
368 )?;
369
370 let s = (0..n_dp)
371 .map(|k| (k as f64 * red.dp_ds).min(length))
372 .collect();
373 let psi = chosen
374 .iter()
375 .enumerate()
376 .map(|(k, &i)| stations[k][i].yaw)
377 .collect();
378 Some((s, psi))
379}