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