Skip to main content

deke_linear/
follower.rs

1//! Stage A→B→C orchestration plus the `deke-types` `Planner`/`Retimer` trait
2//! wiring.
3
4use std::time::Duration;
5
6use deke_types::glam::DAffine3;
7use deke_types::{
8    ContinuousFKChain, DekeError, DekeResult, IkSolver, Planner, Retimer, SRobotPath, SRobotQ,
9    SRobotQLike, SRobotTraj, Validator,
10};
11
12use crate::constraints::{FollowConfig, LinearConstraints, PlannerOptions};
13use crate::diagnostic::{LinearFollowDiagnostic, LinearPlannerDiagnostic, LinearRetimerDiagnostic};
14use crate::error::LinearError;
15use crate::path::{CartesianRun, condition};
16use crate::planner::CartesianLinearPlanner;
17use crate::retimer::ConstantSpeedRetimer;
18
19/// End-to-end constant-TCP-speed Cartesian follower.
20#[derive(Clone, Debug)]
21pub struct LinearFollower<'a, const N: usize, FK> {
22    fk: &'a FK,
23}
24
25impl<'a, const N: usize, FK> LinearFollower<'a, N, FK>
26where
27    FK: ContinuousFKChain<N, f64> + IkSolver<N, f64>,
28{
29    pub fn new(fk: &'a FK) -> Self {
30        Self { fk }
31    }
32
33    /// Follow `poses` (a Cartesian polyline of full TCP poses) at a constant TCP
34    /// speed, splitting at sharp corners and stopping at rest there.
35    ///
36    /// Every candidate configuration is checked against `validator` *inside* the
37    /// planner DP, so the planner routes through collision-free configurations
38    /// (and, for a redundant tool axis, rotates the yaw to keep the arm clear).
39    /// Pass [`NoopValidator`] + `&()` to plan without obstacle checks. The
40    /// stitched output trajectory is re-checked with `validate_motion` as a
41    /// backstop.
42    pub fn follow<V: Validator<N, (), f64>>(
43        &self,
44        poses: &[DAffine3],
45        cfg: &FollowConfig<N>,
46        validator: &V,
47        ctx: &V::Context<'_>,
48    ) -> Result<(SRobotTraj<N, f64>, LinearFollowDiagnostic), LinearError> {
49        let runs = condition(poses, &cfg.conditioning)?;
50        let planner = CartesianLinearPlanner::new(self.fk);
51        let retimer = ConstantSpeedRetimer::new(self.fk);
52
53        let mut all: Vec<SRobotQ<N, f64>> = Vec::new();
54        let mut diag = LinearFollowDiagnostic {
55            runs: runs.len(),
56            ..Default::default()
57        };
58
59        let redundant = cfg
60            .redundant
61            .as_ref()
62            .map(|_| crate::redundant::RedundantLinearPlanner::new(self.fk));
63
64        let mut seed: Option<SRobotQ<N, f64>> = None;
65        for (i, run) in runs.iter().enumerate() {
66            let jpath = match (&redundant, &cfg.redundant) {
67                (Some(rp), Some(ropts)) => {
68                    let (path, rdiag) =
69                        rp.plan_run(run, &cfg.planner, ropts, validator, ctx, seed.as_ref(), i)?;
70                    diag.redundant.push(rdiag);
71                    path
72                }
73                _ => {
74                    let (path, pdiag) =
75                        planner.plan_run(run, &cfg.planner, validator, ctx, seed.as_ref(), i)?;
76                    diag.planner.push(pdiag);
77                    path
78                }
79            };
80            seed = Some(*jpath.last());
81            let (traj, rdiag) = retimer.retime_path(&cfg.constraints, &jpath, i)?;
82            let samples = traj.path().iter().copied();
83            if all.is_empty() {
84                all.extend(samples);
85            } else {
86                all.extend(samples.skip(1));
87            }
88            diag.retimer.push(rdiag);
89        }
90
91        // Backstop: the retimer interpolates between planned (validated) waypoints,
92        // so re-check the stitched trajectory as continuous motion.
93        validator
94            .validate_motion(&all, ctx)
95            .map_err(LinearError::from)?;
96
97        let dt = cfg.constraints.output_dt;
98        diag.total_samples = all.len();
99        diag.total_duration =
100            Duration::from_secs_f64(all.len().saturating_sub(1) as f64 * dt.as_secs_f64());
101        let path = SRobotPath::try_new(all).map_err(LinearError::from)?;
102        Ok((SRobotTraj::new(dt, path), diag))
103    }
104}
105
106impl<'a, const N: usize, FK> Planner<N, f64> for CartesianLinearPlanner<'a, N, FK>
107where
108    FK: ContinuousFKChain<N, f64> + IkSolver<N, f64>,
109{
110    type Diagnostic = LinearPlannerDiagnostic;
111    type Config = PlannerOptions<N>;
112    type Waypoints = CartesianRun;
113
114    fn plan<E: Into<DekeError>, V: Validator<N, (), f64>>(
115        &self,
116        config: &Self::Config,
117        waypoints: &Self::Waypoints,
118        validator: &V,
119        ctx: &V::Context<'_>,
120    ) -> (DekeResult<SRobotPath<N, f64>>, Self::Diagnostic) {
121        match self.plan_run(waypoints, config, validator, ctx, None, 0) {
122            Ok((path, diag)) => (Ok(path), diag),
123            Err(e) => (
124                Err(e.into()),
125                LinearPlannerDiagnostic {
126                    samples: 0,
127                    min_manipulability: 0.0,
128                    total_cost: f64::INFINITY,
129                },
130            ),
131        }
132    }
133}
134
135impl<'a, const N: usize, FK> Retimer<N, f64> for ConstantSpeedRetimer<'a, N, FK>
136where
137    FK: ContinuousFKChain<N, f64>,
138{
139    type Diagnostic = LinearRetimerDiagnostic;
140    type Constraints = LinearConstraints<N>;
141
142    fn retime<V: Validator<N, (), f64>>(
143        &self,
144        constraints: &Self::Constraints,
145        path: &SRobotPath<N, f64>,
146        validator: &V,
147        ctx: &V::Context<'_>,
148    ) -> (DekeResult<SRobotTraj<N, f64>>, Self::Diagnostic) {
149        match self.retime_path(constraints, path, 0) {
150            Ok((traj, diag)) => {
151                let samples: Vec<SRobotQ<N, f64>> = traj.path().iter().copied().collect();
152                if let Err(e) = validator.validate_motion(&samples, ctx) {
153                    return (Err(e), diag);
154                }
155                (Ok(traj), diag)
156            }
157            Err(e) => (
158                Err(e.into()),
159                LinearRetimerDiagnostic {
160                    output_samples: 0,
161                    duration: Duration::ZERO,
162                    arc_length: 0.0,
163                    commanded_speed: constraints.tcp_speed,
164                    peak_speed: 0.0,
165                },
166            ),
167        }
168    }
169}
170
171/// A validator that accepts everything — for callers that handle collision
172/// checking elsewhere (or not at all).
173#[derive(Debug, Clone, Default)]
174pub struct NoopValidator<const N: usize>;
175
176impl<const N: usize> Validator<N, (), f64> for NoopValidator<N> {
177    type Context<'ctx> = ();
178
179    fn validate<'ctx, E: Into<DekeError>, A: SRobotQLike<N, E, f64>>(
180        &self,
181        _q: A,
182        _ctx: &Self::Context<'ctx>,
183    ) -> DekeResult<()> {
184        Ok(())
185    }
186
187    fn validate_motion<'ctx>(
188        &self,
189        _qs: &[SRobotQ<N, f64>],
190        _ctx: &Self::Context<'ctx>,
191    ) -> DekeResult<()> {
192        Ok(())
193    }
194}