rs_opw_kinematics/path_plan/
cartesian.rs

1//! Cartesian stroke
2
3use crate::kinematic_traits::{Joints, Kinematics, Pose, Solutions};
4use crate::kinematics_with_shape::KinematicsWithShape;
5use crate::rrt::RRTPlanner;
6use crate::utils;
7use crate::utils::{dump_joints, transition_costs};
8use bitflags::bitflags;
9use nalgebra::Translation3;
10use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
11use std::fmt;
12use std::sync::Arc;
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::time::Instant;
15
16/// Reasonable default transition costs. Rotation of smaller joints is more tolerable.
17/// The sum of all weights is 6.0
18pub const DEFAULT_TRANSITION_COSTS: [f64; 6] = [1.2, 1.1, 1.1, 0.9, 0.9, 0.8];
19
20/// Class doing Cartesian planning
21pub struct Cartesian<'a> {
22    pub robot: &'a KinematicsWithShape,
23
24    /// Check step size in meters. Objects and features of the robotic cell smaller
25    /// than this may not be noticed during collision checks.
26    pub check_step_m: f64,
27
28    /// Check step size in radians. Objects and features of the robotic cell smaller
29    /// than this may not be noticed during collision checks.
30    pub check_step_rad: f64,
31
32    /// Maximum allowed transition cost between Joints
33    pub max_transition_cost: f64,
34
35    /// Transition cost coefficients (smaller joints are allowed to rotate more)
36    pub transition_coefficients: Joints,
37
38    /// If movement between adjacent poses results transition costs over this threshold,
39    /// the Cartesian segment is divided by half, checking boths side separatedly while
40    /// collision checking also the middle segment.
41    pub linear_recursion_depth: usize,
42
43    /// RRT planner that plans the onboarding part, and may potentially plan other
44    /// parts that involve collision free relocation of the robot, but without
45    /// Cartesian (linear) movement.
46    pub rrt: RRTPlanner,
47
48    /// If set, linear interpolated poses are included in the output.
49    /// Otherwise, they are discarded, many robots can do Cartesian stroke
50    /// much better on they own
51    pub include_linear_interpolation: bool,
52
53    /// Debug mode for logging
54    pub debug: bool,
55}
56
57bitflags! {
58    /// Flags that can be set on AnnotatedJoints in the output
59    #[derive(Clone, Copy)]
60    pub struct PathFlags: u32 {
61        const NONE = 0b0000_0000;
62
63        /// Position is part of the movement from the initil ("home") pose to the landing
64        /// pose. It may include very arbitrary joint movements, so Cartesian stroke
65        /// may not work on this part of the trajectory.
66        const ONBOARDING =          1 << 1;
67
68        /// Position directly matches one of the stroke poses given in the input.
69        const TRACE =               1 << 2;
70
71        /// Position is a linear interpolation between two poses of the trace. These poses
72        /// are not needed for the robots that have built-in support for Cartesian stroke,
73        /// but may be important for more developed models that only rotate between
74        /// the given joint positions without guarantees that TCP movement is linear.
75        const LIN_INTERP =          1 << 3;
76
77        /// Position corresponds the starting pose ("land") that is normally little above
78        /// the start of the required trace
79        const LAND =                1 << 4;
80
81        /// The tool is movind down from the landing position to the first stroke position.
82        /// Activate the tool mechanism if any (start the drill, open sprayer, etc).
83        const LANDING =             1 << 5;
84
85        /// Position corresponds the ennding pose ("park") that is normally little above
86        /// the end of the required trace. This is the last pose to include into the
87        /// output.
88        const PARK =                1 << 6;
89
90        /// The tool is moving up from the last trace point to the parking position
91        /// (shut down the effector mechanism if any)
92        const PARKING =             1 << 7;
93
94        /// Used with raster projector, indicates the movement considered "forwards"
95        const FORWARDS =             1 << 8;
96
97        /// Used with raster projector, indicates the movement considered "backwards"
98        const BACKWARDS =            1 << 9;
99
100        /// Mildly altered to make the stroke possible
101        const ALTERED =              1 << 10;
102
103        /// Combined flag representing the "original" position, so the one that was
104        /// given in the input.
105        const ORIGINAL = Self::TRACE.bits() | Self::LAND.bits() | Self::PARK.bits();
106
107        /// Special flag used in debugging to mark out anything of interest. Largest can be stored
108        /// in u32
109        const DEBUG = 1 << 31;
110
111        // The movement INTO this pose is Cartesian stroke
112        const CARTESIAN = Self::LIN_INTERP.bits() | Self::LAND.bits() | Self::PARK.bits();
113    }
114}
115
116#[derive(Clone, Copy)]
117pub(crate) struct AnnotatedPose {
118    pub (crate) pose: Pose,
119    pub (crate) flags: PathFlags,
120}
121
122/// Annotated joints specifying if it is joint-joint or Cartesian move (to this joint, not from)
123#[derive(Clone, Copy)]
124pub struct AnnotatedJoints {
125    pub joints: Joints,
126    pub flags: PathFlags,
127}
128
129impl AnnotatedPose {
130    pub(crate) fn interpolate(&self, other: &AnnotatedPose, p: f64) -> AnnotatedPose {
131        assert!((0.0..=1.0).contains(&p));
132
133        // Interpolate translation (linearly)
134        let self_translation = &self.pose.translation.vector;
135        let other_translation = &other.pose.translation.vector;
136
137        let translation = self_translation.lerp(&other_translation, p);
138        let rotation = self.pose.rotation.slerp(&other.pose.rotation, p);
139
140        AnnotatedPose {
141            pose: Pose::from_parts(Translation3::from(translation), rotation),
142            flags: PathFlags::LIN_INTERP,
143        }
144    }
145}
146
147fn flag_representation(flags: &PathFlags) -> String {
148    const FLAG_MAP: &[(PathFlags, &str)] = &[
149        (PathFlags::LIN_INTERP, "LIN_INTERP"),
150        (PathFlags::LAND, "LAND"),
151        (PathFlags::PARK, "PARK"),
152        (PathFlags::TRACE, "TRACE"),
153        (PathFlags::CARTESIAN, "CARTESIAN"),
154        (PathFlags::ONBOARDING, "ONBOARDING"),
155    ];
156
157    FLAG_MAP
158        .iter()
159        .filter(|(flag, _)| flags.contains(*flag))
160        .map(|(_, name)| *name)
161        .collect::<Vec<_>>()
162        .join(" | ")
163}
164
165impl fmt::Debug for AnnotatedPose {
166    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
167        let translation = self.pose.translation.vector;
168        let rotation = self.pose.rotation;
169        write!(
170            formatter,
171            "{}: [{:.3}, {:.3}, {:.3}], quat {{ w: {:.3}, i: {:.3}, j: {:.3}, k: {:.3} }}",
172            flag_representation(&self.flags),
173            translation.x,
174            translation.y,
175            translation.z,
176            rotation.w,
177            rotation.i,
178            rotation.j,
179            rotation.k
180        )
181    }
182}
183
184impl fmt::Debug for AnnotatedJoints {
185    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
186        write!(
187            formatter,
188            "{}: {:.2}, {:.2}, {:.2}, {:.2}, {:.2}, {:.2} ",
189            flag_representation(&self.flags),
190            self.joints[0].to_degrees(),
191            self.joints[1].to_degrees(),
192            self.joints[2].to_degrees(),
193            self.joints[3].to_degrees(),
194            self.joints[4].to_degrees(),
195            self.joints[5].to_degrees(),
196        )
197    }
198}
199
200struct Transition {
201    from: AnnotatedPose,
202    to: AnnotatedPose,
203    previous: Joints,
204    solutions: Solutions,
205}
206
207impl Cartesian<'_> {
208
209    /// Path plan for the given vector of poses. The returned path must be transitionable
210    /// and collision free.
211    pub fn plan(
212        &self,
213        from: &Joints,
214        land: &Pose,
215        steps: Vec<Pose>,
216        park: &Pose,
217    ) -> Result<Vec<AnnotatedJoints>, String> {
218        if self.robot.collides(from) {
219            return Err("Onboarding point collides".into());
220        }
221        let strategies = self.robot.inverse_continuing(land, from);
222        if strategies.is_empty() {
223            return Err("Unable to start from onboarding point".into());
224        }
225        let poses = self.with_intermediate_poses(land, &steps, park);
226        println!("Probing {} strategies", strategies.len());
227
228        let stop = Arc::new(AtomicBool::new(false));
229
230        strategies
231            .par_iter()
232            .find_map_any(|strategy| {
233                match self.probe_strategy(
234                    strategy, &poses, &stop
235                ) {
236                    Ok(outcome) => {
237                        println!("Strategy worked out: {:?}", strategy);
238                        stop.store(true, Ordering::Relaxed);
239                        Some(Ok(outcome))
240                    }
241                    Err(msg) => {
242                        if self.debug {
243                            println!("Strategy failed: {:?}, {}", strategy, msg);
244                        }
245                        None // Continue searching
246                    }
247                }
248            })
249            .unwrap_or_else(|| {
250                Err(format!(
251                    "No strategy worked out of {} tried",
252                    strategies.len()
253                ))
254            })
255    }
256
257    /// Probe the given strategy
258    fn probe_strategy(
259        &self,
260        work_path_start: &Joints,
261        poses: &Vec<AnnotatedPose>,
262        stop: &AtomicBool,
263    ) -> Result<Vec<AnnotatedJoints>, String> {
264        println!("Cartesian planning started, computing strategy {work_path_start:?}");
265
266        let started = Instant::now();
267        let mut trace = Vec::with_capacity(100 + poses.len() + 10);
268        // Push the strategy point, from here the move must be already CARTESIAN
269        trace.push(AnnotatedJoints {
270            joints: *work_path_start,
271            flags: PathFlags::LAND, 
272        }.clone());
273
274        let mut step = 1;
275
276        let mut pairs_iterator = poses.windows(2);
277
278        while let Some([from, to]) = pairs_iterator.next() {
279            let prev = trace
280                .last()
281                .expect("Should have start and strategy points")
282                .clone();
283
284            let transition = self.step_adaptive_linear_transition(&prev.joints, from, to, 0);
285            match transition {
286                Ok(extension) => {
287                    for (p, step) in extension.iter().enumerate() {
288                        let flags = if p < extension.len() - 1 {
289                            (to.flags | PathFlags::LIN_INTERP)
290                                & !(PathFlags::TRACE | PathFlags::PARK)
291                        } else {
292                            to.flags
293                        };
294                        trace.push(AnnotatedJoints {
295                            joints: step.clone(),
296                            flags: flags,
297                        });
298                    }
299                }
300
301                Err(failed_transition) => {
302                    let mut success = false;
303                    // Try with altered pose
304                    println!(
305                        "Closing step {:?} with RRT", step
306                    );
307                    let solutions = self.robot.inverse_continuing(&to.pose, &prev.joints);
308                    for next in solutions {
309                        let path = self.rrt.plan_rrt(&prev.joints, &next, self.robot, stop);
310                        if let Ok(path) = path {
311                            println!("  ... closed with RRT {} steps", path.len());
312                            for step in path {
313                                trace.push(AnnotatedJoints {
314                                    joints: step,
315                                    flags: to.flags & !PathFlags::LIN_INTERP,
316                                });
317                            }
318                            success = true;
319                            break;
320                        }
321                    }
322
323                    if !success {
324                        self.log_failed_transition(&failed_transition, step);
325                        return Err(format!("Failed to transition at step {}", step));
326                    }
327                }
328            }
329            if to.flags.contains(PathFlags::TRACE) {
330                step += 1;
331            }
332        }
333
334        if self.debug {
335            println!(
336                "Cartesian planning till collision check took {:?}",
337                started.elapsed()
338            );
339        }
340        if stop.load(Ordering::Relaxed) {
341            return Err("Stopped".into());
342        }
343
344        Ok(trace)
345    }
346
347    /// Transition cartesian way from 'from' into 'to' while assuming 'from'
348    /// Returns all path, not including "starting", that should not be empty
349    /// as it succeeded. Returns description of the transition on failure.
350    fn step_adaptive_linear_transition(
351        &self,
352        starting: &Joints,
353        from: &AnnotatedPose, // FK of "starting"
354        to: &AnnotatedPose,
355        depth: usize,
356    ) -> Result<Vec<Joints>, Transition> {
357        pub const DIV_RATIO: f64 = 0.5;
358
359        // Not checked for collisions yet
360        let solutions = self
361            .robot
362            .kinematics
363            .inverse_continuing(&to.pose, &starting);
364
365        // Solutions are already sorted best first
366        for next in &solutions {
367            // Internal "miniposes" generated through recursion are not checked for collision.
368            // They only check agains continuity of the robot movement (no unexpected jerks)
369            let cost = transition_costs(starting, next, &self.transition_coefficients);
370            if cost <= self.max_transition_cost {
371                return Ok(vec![next.clone()]); // Track minimal cost observed
372            }
373        }
374
375        // Transitioning not successful.
376        // Recursive call reduces step, the goal is to check if there is a continuous
377        // linear path on any step.
378        if depth < self.linear_recursion_depth {
379            // Try to bridge till them middle first, and then from the middle
380            // This will result in a shorter distance between from and to.
381            let midpose = from.interpolate(to, DIV_RATIO);
382            let first_track =
383                self.step_adaptive_linear_transition(starting, from, &midpose, depth + 1)?;
384            let mid_step = first_track.last().unwrap().clone();
385
386            // If both bridgings were successful, return the final position that resulted from
387            // bridging from middle joints to the final pose on this step
388            let second_track =
389                self.step_adaptive_linear_transition(&mid_step, &midpose, to, depth + 1)?;
390
391            Ok(first_track
392                .into_iter()
393                .chain(second_track.into_iter())
394                .collect())
395        } else {
396            Err(Transition {
397                from: from.clone(),
398                to: to.clone(),
399                previous: starting.clone(),
400                solutions: solutions,
401            })
402        }
403    }
404
405    fn log_failed_transition(&self, transition: &Transition, step: i32) {
406        if !self.debug {
407            return;
408        }
409        println!("Step {} from [1..n] failed", step);
410        println!(
411            "No transition with cost below {}:",
412            self.max_transition_cost.to_degrees()
413        );
414        println!(
415            "   from: {:?} collides {}",
416            transition.from,
417            self.robot.collides(&transition.previous)
418        );
419        dump_joints(&transition.previous);
420        println!("   to: {:?}", transition.to);
421        println!("   Possible transitions:");
422        for s in &transition.solutions {
423            dump_joints(s);
424            println!(
425                "   transition {} collides: {}",
426                utils::transition_costs(&transition.previous, s, &DEFAULT_TRANSITION_COSTS)
427                    .to_degrees(),
428                self.robot.collides(s)
429            );
430        }
431    }
432
433    fn with_intermediate_poses(
434        &self,
435        land: &Pose,
436        steps: &Vec<Pose>,
437        park: &Pose,
438    ) -> Vec<AnnotatedPose> {
439        let mut poses = Vec::with_capacity(10 * steps.len() + 2);
440
441        // Add the landing pose
442        poses.push(AnnotatedPose {
443            pose: *land,
444            flags: PathFlags::LAND,
445        });
446
447        if !steps.is_empty() {
448            // Add intermediate poses between land and the first step
449            self.add_intermediate_poses(land, &steps[0], &mut poses);
450
451            // Add the steps and intermediate poses between them
452            for i in 0..steps.len() - 1 {
453                poses.push(AnnotatedPose {
454                    pose: steps[i].clone(),
455                    flags: PathFlags::TRACE,
456                });
457
458                self.add_intermediate_poses(&steps[i], &steps[i + 1], &mut poses);
459            }
460
461            // Add the last step
462            let last = *steps.last().unwrap();
463            poses.push(AnnotatedPose {
464                pose: last.clone(),
465                flags: PathFlags::TRACE,
466            });
467
468            // Add intermediate poses between the last step and park
469            self.add_intermediate_poses(&last, park, &mut poses);
470        } else {
471            // If no steps, add intermediate poses between land and park directly
472            self.add_intermediate_poses(land, park, &mut poses);
473        }
474
475        // Add the parking pose
476        poses.push(AnnotatedPose {
477            pose: park.clone(),
478            flags: PathFlags::PARK,
479        });
480
481        poses
482    }
483
484    /// Add intermediate poses. start and end poses are not added.
485    fn add_intermediate_poses(&self, start: &Pose, end: &Pose, poses: &mut Vec<AnnotatedPose>) {
486        // Calculate the translation difference and distance
487        let translation_diff = end.translation.vector - start.translation.vector;
488        let translation_distance = translation_diff.norm();
489
490        // Calculate the rotation difference and angle
491        let rotation_diff = end.rotation * start.rotation.inverse();
492        let rotation_angle = rotation_diff.angle();
493
494        // Calculate the number of steps required for translation and rotation
495        let translation_steps = (translation_distance / self.check_step_m).ceil() as usize;
496        let rotation_steps = (rotation_angle / self.check_step_rad).ceil() as usize;
497
498        // Choose the greater step count to achieve finer granularity between poses
499        let steps = translation_steps.max(rotation_steps).max(1);
500
501        // Calculate incremental translation and rotation per chosen step count
502        let translation_step = translation_diff / steps as f64;
503
504        // Generate each intermediate pose. Start and end poses are excluded.
505        for i in 1..steps {
506            let fraction = i as f64 / steps as f64;
507
508            // Interpolate translation and rotation
509            let intermediate_translation = start.translation.vector + translation_step * i as f64;
510            let intermediate_rotation = start.rotation.slerp(&end.rotation, fraction);
511
512            // Construct the intermediate pose
513            let intermediate_pose =
514                Pose::from_parts(intermediate_translation.into(), intermediate_rotation);
515
516            poses.push(AnnotatedPose {
517                pose: intermediate_pose,
518                flags: PathFlags::LIN_INTERP,
519            });
520        }
521    }
522}