rs_opw_kinematics/
tool.rs

1//! Provides tool and base for the robot. 
2//! Both Tool and Base take arbitrary implementation of Kinematics and are such
3//! implementations themselves. Hence, they can be cascaded, like base, having the robot,
4//! that robot having a tool:
5//!
6//! ```
7//! use std::sync::Arc;
8//! use nalgebra::{Isometry3, Translation3, UnitQuaternion};
9//! use rs_opw_kinematics::kinematic_traits::{Joints, Kinematics, Pose};
10//! use rs_opw_kinematics::kinematics_impl::OPWKinematics;
11//! use rs_opw_kinematics::parameters::opw_kinematics::Parameters;
12//! let robot_alone = OPWKinematics::new(Parameters::staubli_tx2_160l());
13//!
14//! // Half meter high pedestal
15//! let pedestal = 0.5;
16//! let base_translation = Isometry3::from_parts(
17//!   Translation3::new(0.0, 0.0, pedestal).into(),
18//!   UnitQuaternion::identity(),
19//! );
20//!
21//! let robot_with_base = rs_opw_kinematics::tool::Base {
22//!   robot: Arc::new(robot_alone),
23//!   base: base_translation,
24//! };
25//!
26//! // Tool extends 1 meter in the Z direction, envisioning something like sword
27//! let sword = 1.0;
28//! let tool_translation = Isometry3::from_parts(
29//!   Translation3::new(0.0, 0.0, sword).into(),
30//!   UnitQuaternion::identity(),
31//! );
32//!
33//! // Create the Tool instance with the transformation
34//! let robot_complete = rs_opw_kinematics::tool::Tool {
35//!   robot: Arc::new(robot_with_base),
36//!   tool: tool_translation,
37//! };
38//!
39//! let joints: Joints = [0.0, 0.1, 0.2, 0.3, 0.0, 0.5]; // Joints are alias of [f64; 6]
40//! let tcp_pose: Pose = robot_complete.forward(&joints);
41//! println!("The sword tip is at: {:?}", tcp_pose);
42//! ```
43
44extern crate nalgebra as na;
45
46use std::sync::Arc;
47use na::{Isometry3};
48use nalgebra::{Translation3};
49use crate::constraints::Constraints;
50use crate::kinematic_traits::{Joints, Kinematics, Pose, Singularity, Solutions};
51
52
53/// Defines the fixed tool that can be attached to the last joint (joint 6) of robot.
54/// The tool moves with the robot, providing additional translation and, if needed,
55/// rotation. The tool itself fully implements the Kinematics,
56/// providing both inverse and forward kinematics for the robot with a tool (with
57/// "pose" being assumed as the position and rotation of the tip of the tool (tool center point). 
58#[derive(Clone)]
59pub struct Tool {
60    pub robot: Arc<dyn Kinematics>,  // The robot
61
62    /// Transformation from the robot's tip joint to the tool's TCP.    
63    pub tool: Isometry3<f64>,
64}
65
66/// Defines the fixed base that can hold the robot.
67/// The base moves the robot to its installed location, providing also rotation if 
68/// required (physical robots work well and may be installed upside down, or at some 
69/// angle like 45 degrees). Base itself fully implements the Kinematics,
70/// providing both inverse and forward kinematics for the robot on a base.
71#[derive(Clone)]
72pub struct Base {
73    pub robot: Arc<dyn Kinematics>,  // The robot
74
75    /// Transformation from the world origin to the robots base.
76    pub base: Isometry3<f64>,
77}
78
79
80impl Kinematics for Tool {
81    fn inverse(&self, tcp: &Pose) -> Solutions {
82        self.robot.inverse(&(tcp * self.tool.inverse()))
83    }
84
85    fn inverse_5dof(&self, tcp: &Pose, j6: f64) -> Solutions {
86        self.robot.inverse_5dof(&(tcp * self.tool.inverse()), j6)
87    }
88
89    fn inverse_continuing_5dof(&self, tcp: &Pose, previous: &Joints) -> Solutions {
90        self.robot.inverse_continuing(&(tcp * self.tool.inverse()), previous)
91    }
92
93    fn inverse_continuing(&self, tcp: &Pose, previous: &Joints) -> Solutions {
94        self.robot.inverse_continuing(&(tcp * self.tool.inverse()), previous)
95    }
96
97    fn forward(&self, qs: &Joints) -> Pose {
98        // Calculate the pose of the tip joint using the robot's kinematics
99        let tip_joint = self.robot.forward(qs);
100        let tcp = tip_joint * self.tool;
101        tcp
102    }
103
104    /// Tool does not add transform to any joints. J6 stays where it was,
105    /// and it is also the base transform for the tool. Tool's tip is
106    /// the "robot pose", but this point does not have the object to render or
107    /// check for collisions. Hence the pose of J6 is not longer TCP on inverse kinematics.
108    fn forward_with_joint_poses(&self, joints: &Joints) -> [Pose; 6] {
109        self.robot.forward_with_joint_poses(joints)
110    }
111
112    /// There is nothing that the tool would add to singularities
113    fn kinematic_singularity(&self, qs: &Joints) -> Option<Singularity> {
114        self.robot.kinematic_singularity(qs)
115    }
116
117    fn constraints(&self) -> &Option<Constraints> {
118        self.robot.constraints()
119    }    
120}
121
122impl Kinematics for Base {
123    fn inverse(&self, tcp: &Pose) -> Solutions {
124        self.robot.inverse(&(self.base.inverse() * tcp))
125    }
126
127    fn inverse_continuing(&self, tcp: &Pose, previous: &Joints) -> Solutions {
128        self.robot.inverse_continuing(&(self.base.inverse() * tcp), &previous)
129    }
130
131    fn inverse_5dof(&self, tcp: &Pose, j6: f64) -> Solutions {
132        self.robot.inverse_5dof(&(self.base.inverse() * tcp), j6)
133    }
134
135    fn inverse_continuing_5dof(&self, tcp: &Pose, previous: &Joints) -> Solutions {
136        self.robot.inverse_continuing_5dof(&(self.base.inverse() * tcp), &previous)
137    }    
138
139    fn forward(&self, joints: &Joints) -> Pose {
140        self.base * self.robot.forward(joints)
141    }
142
143    fn forward_with_joint_poses(&self, joints: &Joints) -> [Pose; 6] {
144        // Compute the forward kinematics for the robot itself
145        let mut poses = self.robot.forward_with_joint_poses(joints);
146
147        // Apply the base transformation to each pose
148        for pose in poses.iter_mut() {
149            *pose = self.base * *pose;
150        }
151
152        poses
153    }    
154
155    fn kinematic_singularity(&self, qs: &Joints) -> Option<Singularity> {
156        self.robot.kinematic_singularity(qs)
157    }
158
159    fn constraints(&self) -> &Option<Constraints> {
160        self.robot.constraints()
161    }    
162}
163
164// Define the Cart (linear axis, prismatic joint) structure that can hold the robot. 
165// The cart is moving in parallel to Cartesian axis (x, y, z) and provides the forward kinematics.
166// Same as joint positions for the robot, cart prismatic joints are not stored
167// in this structure. The linear axis can be itself placed on the base.
168#[derive(Clone)]
169pub struct LinearAxis {
170    robot: Arc<dyn Kinematics>,
171    axis: u32,
172    /// The base of the axis (not the robot on the axis)
173    pub base: Isometry3<f64>,
174}
175
176/// A platform for a robot that can ride in x, y and z directions. This way it is less
177/// restricted than LinearAxis but tasks focusing with moving in one dimension only
178/// may prefer abstracting which dimension it is.
179#[derive(Clone)]
180pub struct Gantry {
181    robot: Arc<dyn Kinematics>,
182    /// The base of the gantry crane (not the robot on the gantry crane)
183    pub base: Isometry3<f64>,
184}
185
186/// A platform for a robot that can ride in one of the x, y and z directions. 
187/// (a more focused version of the Gantry, intended for algorithms that focus with the single
188/// direction variable.) 
189impl LinearAxis {
190    // Compute the forward transformation including the cart's offset and the robot's kinematics
191    pub fn forward(&self, distance: f64, joint_angles: &[f64; 6]) -> Isometry3<f64> {
192        let cart_translation = match self.axis {
193            0 => Translation3::new(distance, 0.0, 0.0),
194            1 => Translation3::new(0.0, distance, 0.0),
195            2 => Translation3::new(0.0, 0.0, distance),
196            _ => panic!("Invalid axis index; must be 0 (x), 1 (y), or 2 (z)"),
197        };
198        let robot_pose = self.robot.forward(joint_angles);
199        self.base * cart_translation * robot_pose
200    }
201}
202
203impl Gantry {
204    // Compute the forward transformation including the cart's offset and the robot's kinematics
205    pub fn forward(&self, translation: &Translation3<f64>, joint_angles: &[f64; 6]) -> Isometry3<f64> {
206        let robot_pose = self.robot.forward(joint_angles);
207        self.base * translation * robot_pose
208    }
209}
210
211
212#[cfg(test)]
213mod tests {
214    use std::f64::consts::PI;
215    use super::*;
216    use nalgebra::{Isometry3, Translation3, UnitQuaternion};
217    use crate::kinematics_impl::OPWKinematics;
218    use crate::parameters::opw_kinematics::Parameters;
219
220    /// Asserts that two `Translation3<f64>` instances are approximately equal within a given tolerance.
221    pub(crate) fn assert_diff(a: &Translation3<f64>, b: &Translation3<f64>, expected_diff: [f64; 3], epsilon: f64) {
222        let actual_diff = a.vector - b.vector;
223
224        assert!(
225            (actual_diff.x - expected_diff[0]).abs() <= epsilon,
226            "X difference is not as expected: actual difference = {}, expected difference = {}",
227            actual_diff.x, expected_diff[0]
228        );
229        assert!(
230            (actual_diff.y - expected_diff[1]).abs() <= epsilon,
231            "Y difference is not as expected: actual difference = {}, expected difference = {}",
232            actual_diff.y, expected_diff[1]
233        );
234        assert!(
235            (actual_diff.z - expected_diff[2]).abs() <= epsilon,
236            "Z difference is not as expected: actual difference = {}, expected difference = {}",
237            actual_diff.z, expected_diff[2]
238        );
239    }
240
241    fn diff(robot_without: &dyn Kinematics, robot_with: &dyn Kinematics, joints: &[f64; 6]) -> (Pose, Pose) {
242        let tcp_without_tool = robot_without.forward(&joints);
243        let tcp_with_tool = robot_with.forward(&joints);
244        (tcp_without_tool, tcp_with_tool)
245    }
246
247    #[test]
248    fn test_tool() {
249        // Parameters for Staubli TX40 robot are assumed to be correctly set in OPWKinematics::new
250        let robot_without_tool = OPWKinematics::new(Parameters::staubli_tx2_160l());
251
252        // Tool extends 1 meter in the Z direction
253        let tool_translation = Isometry3::from_parts(
254            Translation3::new(0.0, 0.0, 1.0).into(),
255            UnitQuaternion::identity(),
256        );
257
258        // Create the Tool instance with the transformation
259        let robot_with_tool = Tool {
260            robot: Arc::new(robot_without_tool),
261            tool: tool_translation,
262        };
263
264        // Joints are all at zero position
265        let joints = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
266
267        let (tcp_without_tool, tcp_with_tool) = diff(&robot_without_tool, &robot_with_tool, &joints);
268        assert_diff(&tcp_with_tool.translation, &tcp_without_tool.translation, [0., 0., 1.], 1E-6);
269
270        // Rotating J6 by any angle should not change anything.
271        // Joints are all at zero position
272        let joints = [0.0, 0.0, 0.0, 0.0, 0.0, PI / 6.0];
273        let (tcp_without_tool, tcp_with_tool) = diff(&robot_without_tool, &robot_with_tool, &joints);
274        assert_diff(&tcp_with_tool.translation, &tcp_without_tool.translation, [0., 0., 1.], 1E-6);
275
276        // Rotating J5 by 90 degrees result in offset
277        let joints = [0.0, 0.0, 0.0, 0.0, PI / 2.0, 0.0];
278        let (tcp_without_tool, tcp_with_tool) = diff(&robot_without_tool, &robot_with_tool, &joints);
279        assert_diff(&tcp_with_tool.translation, &tcp_without_tool.translation, [1.0, 0.0, 0.], 1E-6);
280
281        // Rotate base joint around, sign must change.
282        let joints = [PI, 0.0, 0.0, 0.0, PI / 2.0, 0.0];
283        let (tcp_without_tool, tcp_with_tool) = diff(&robot_without_tool, &robot_with_tool, &joints);
284        assert_diff(&tcp_with_tool.translation, &tcp_without_tool.translation, [-1.0, 0.0, 0.], 1E-6);
285
286        // Rotate base joint 90 degrees, must become Y
287        let joints = [PI / 2.0, 0.0, 0.0, 0.0, PI / 2.0, 0.0];
288        let (tcp_without_tool, tcp_with_tool) = diff(&robot_without_tool, &robot_with_tool, &joints);
289        assert_diff(&tcp_with_tool.translation, &tcp_without_tool.translation, [0.0, 1.0, 0.], 1E-6);
290
291        // Rotate base joint 45 degrees, must divide between X and Y
292        let joints = [PI / 4.0, 0.0, 0.0, 0.0, PI / 2.0, 0.0];
293        let catet = 45.0_f64.to_radians().sin();
294        let (tcp_without_tool, tcp_with_tool) = diff(&robot_without_tool, &robot_with_tool, &joints);
295        assert_diff(&tcp_with_tool.translation, &tcp_without_tool.translation,
296                    [catet, catet, 0.], 1E-6);
297
298        // Rotate base joint 45 degrees, must divide between X and Y, and also raise 45 deg up
299        let joints = [PI / 4.0, 0.0, 0.0, 0.0, PI / 4.0, 0.0];
300        let (tcp_without_tool, tcp_with_tool) = diff(&robot_without_tool, &robot_with_tool, &joints);
301        assert_diff(&tcp_with_tool.translation, &tcp_without_tool.translation,
302                    [0.5, 0.5, 2.0_f64.sqrt() / 2.0], 1E-6);
303    }
304
305    #[test]
306    fn test_base() {
307        // Parameters for Staubli TX40 robot are assumed to be correctly set in OPWKinematics::new
308        let robot_without_base = OPWKinematics::new(Parameters::staubli_tx2_160l());
309
310        // 1 meter high pedestal
311        let base_translation = Isometry3::from_parts(
312            Translation3::new(0.0, 0.0, 1.0).into(),
313            UnitQuaternion::identity(),
314        );
315
316        let robot_with_base = Base {
317            robot: Arc::new(robot_without_base),
318            base: base_translation,
319        };
320
321        // Joints are all at zero position
322        let joints = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
323
324        let (tcp_without_base, tcp_with_base) = diff(&robot_without_base, &robot_with_base, &joints);
325        assert_diff(&tcp_with_base.translation, &tcp_without_base.translation, [0., 0., 1.], 1E-6);
326
327        // Rotate base joint around, sign must not change.
328        let joints = [PI / 3.0, 0.0, 0.0, 0.0, PI / 2.0, 0.0];
329        let (tcp_without_base, tcp_with_base) = diff(&robot_without_base, &robot_with_base, &joints);
330        assert_diff(&tcp_with_base.translation, &tcp_without_base.translation, [0.0, 0.0, 1.0], 1E-6);
331    }
332
333    #[test]
334    fn test_linear_axis_forward() {
335        let robot_without_base = OPWKinematics::new(Parameters::staubli_tx2_160l());
336        let base_translation = Isometry3::from_parts(
337            Translation3::new(0.1, 0.2, 0.3).into(),
338            UnitQuaternion::identity(),
339        );
340
341        let cart = LinearAxis {
342            robot: Arc::new(robot_without_base),
343            axis: 1,
344            base: base_translation,
345        };
346
347        let joint_angles = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
348        let result = cart.forward(7.0, &joint_angles);
349
350        assert_eq!(result.translation.vector.y,
351                   robot_without_base.forward(&joint_angles).translation.vector.y + 7.0 + 0.2);
352    }
353
354    #[test]
355    fn test_gantry_forward() {
356        let robot_without_base = OPWKinematics::new(Parameters::staubli_tx2_160l());
357
358        let base_translation = Isometry3::from_parts(
359            Translation3::new(0.1, 0.2, 0.3).into(),
360            UnitQuaternion::identity(),
361        );
362
363        let gantry = Gantry {
364            robot: Arc::new(robot_without_base),
365            base: base_translation,
366        };
367
368        let joint_angles = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
369        let r = gantry.forward(
370            &Translation3::new(7.0, 8.0, 9.0),
371            &joint_angles).translation;
372
373        let alone = robot_without_base.forward(&joint_angles).translation;
374
375        // Gantry riding plus gantry base.
376        assert_eq!(r.x, alone.x + 7.1);
377        assert_eq!(r.y, alone.y + 8.2);
378        assert_eq!(r.z, alone.z + 9.3);
379    }
380
381    /// Complete test that includes robot on linear axis, standing on the base and equipped
382    /// witht he tool.
383    #[test]
384    fn test_complete_robot() {
385        fn diff(alone: &dyn Kinematics, riding: &LinearAxis, axis: f64, joints: &[f64; 6]) -> (Pose, Pose) {
386            let tcp_alone = alone.forward(&joints);
387            let tcp = riding.forward(axis, &joints);
388            (tcp_alone, tcp)
389        }
390
391        let robot_alone = OPWKinematics::new(Parameters::staubli_tx2_160l());
392
393        // Half meter high pedestal
394        let pedestal = 0.5;
395        let base_translation = Isometry3::from_parts(
396            Translation3::new(0.0, 0.0, pedestal).into(),
397            UnitQuaternion::identity(),
398        );
399
400        let robot_with_base = Base {
401            robot: Arc::new(robot_alone),
402            base: base_translation,
403        };
404
405        // Tool extends 1 meter in the Z direction, envisioning something like sword
406        let sword = 1.0;
407        let tool_translation = Isometry3::from_parts(
408            Translation3::new(0.0, 0.0, sword).into(),
409            UnitQuaternion::identity(),
410        );
411
412        // Create the Tool instance with the transformation
413        let robot_complete = Tool {
414            robot: Arc::new(robot_with_base),
415            tool: tool_translation,
416        };
417
418
419        // Gantry is based with 0.75 horizontal offset along y
420        let gantry_base = 0.75;
421        let gantry_translation = Isometry3::from_parts(
422            Translation3::new(0.0, gantry_base, 0.0).into(),
423            UnitQuaternion::identity(),
424        );
425
426        let riding_robot = LinearAxis {
427            robot: Arc::new(robot_complete),
428            axis: 0,
429            base: gantry_translation,
430        };
431
432        // Joints are all at zero position
433        let joints = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
434        let axis = 0.0;
435
436        let (tcp_alone, tcp) = diff(&robot_alone, &riding_robot, axis, &joints);
437        assert_diff(&tcp.translation, &tcp_alone.translation,
438                    [0., gantry_base, pedestal + sword], 1E-6);
439
440        // Rotating J6 by any angle should not change anything.
441        // Joints are all at zero position
442        let joints = [0.0, 0.0, 0.0, 0.0, 0.0, PI / 6.0];
443        let (tcp_alone, tcp) = diff(&robot_alone, &riding_robot, axis, &joints);
444        assert_diff(&tcp.translation, &tcp_alone.translation,
445                    [0., gantry_base, pedestal + sword], 1E-6);
446
447        // Rotating J5 by 90 degrees result in offset horizontally for the sword.
448        let joints = [0.0, 0.0, 0.0, 0.0, PI / 2.0, 0.0];
449        let (tcp_alone, tcp) = diff(&robot_alone, &riding_robot, axis, &joints);
450        assert_diff(&tcp.translation, &tcp_alone.translation,
451                    [sword, gantry_base, pedestal], 1E-6);
452
453        // Rotate base joint around, sign for the sword must change.
454        let joints = [PI, 0.0, 0.0, 0.0, PI / 2.0, 0.0];
455        let (tcp_alone, tcp) = diff(&robot_alone, &riding_robot, axis, &joints);
456        assert_diff(&tcp.translation, &tcp_alone.translation,
457                    [-sword, gantry_base, pedestal], 1E-6);
458
459        // Rotate base joint 90 degrees, swords contributes to Y now
460        let joints = [PI / 2.0, 0.0, 0.0, 0.0, PI / 2.0, 0.0];
461        let (tcp_alone, tcp) = diff(&robot_alone, &riding_robot, axis, &joints);
462        assert_diff(&tcp.translation, &tcp_alone.translation,
463                    [0.0, gantry_base + sword, pedestal], 1E-6);
464
465        // Rotate base joint 45 degrees, the effect of sword must divide between X and Y
466        let joints = [PI / 4.0, 0.0, 0.0, 0.0, PI / 2.0, 0.0];
467        let catet = 45.0_f64.to_radians().sin();
468        let (tcp_alone, tcp) = diff(&robot_alone, &riding_robot, axis, &joints);
469        assert_diff(&tcp.translation, &tcp_alone.translation,
470                    [catet, catet + gantry_base, pedestal], 1E-6);
471
472        // Rotate base joint 45 degrees, must divide between X and Y, and also raise 45 deg up
473        let joints = [PI / 4.0, 0.0, 0.0, 0.0, PI / 4.0, 0.0];
474        let (tcp_alone, tcp) = diff(&robot_alone, &riding_robot, axis, &joints);
475        assert_diff(&tcp.translation, &tcp_alone.translation,
476                    [sword * 0.5, sword * 0.5 + gantry_base, sword * 2.0_f64.sqrt() / 2.0 + pedestal], 1E-6);
477
478
479        // Ride the gantry 10 meters along x.
480        let ride = 10.0;
481        let tcp_translation = riding_robot.forward(ride, &joints).translation;
482        assert_diff(&tcp_translation, &tcp_alone.translation,
483                    [sword * 0.5 + ride, sword * 0.5 + gantry_base,
484                        sword * 2.0_f64.sqrt() / 2.0 + pedestal], 1E-6);
485    }
486}
487