rs_opw_kinematics/
jacobian.rs

1//!
2//!  This package provides support for the Jacobian matrix.
3//!
4//!  The Jacobian matrix, as described here, represents the relationship between the joint velocities
5//!  and the end-effector velocities:
6//! ```text
7//!  | ∂vx/∂θ₁  ∂vx/∂θ₂  ∂vx/∂θ₃  ∂vx/∂θ₄  ∂vx/∂θ₅  ∂vx/∂θ₆ |
8//!  | ∂vy/∂θ₁  ∂vy/∂θ₂  ∂vy/∂θ₃  ∂vy/∂θ₄  ∂vy/∂θ₅  ∂vy/∂θ₆ |
9//!  | ∂vz/∂θ₁  ∂vz/∂θ₂  ∂vz/∂θ₃  ∂vz/∂θ₄  ∂vz/∂θ₅  ∂vz/∂θ₆ |
10//!  | ∂ωx/∂θ₁  ∂ωx/∂θ₂  ∂ωx/∂θ₃  ∂ωx/∂θ₄  ∂ωx/∂θ₅  ∂ωx/∂θ₆ |
11//!  | ∂ωy/∂θ₁  ∂ωy/∂θ₂  ∂ωy/∂θ₃  ∂ωy/∂θ₄  ∂ωy/∂θ₅  ∂ωy/∂θ₆ |
12//!  | ∂ωz/∂θ₁  ∂ωz/∂θ₂  ∂ωz/∂θ₃  ∂ωz/∂θ₄  ∂ωz/∂θ₅  ∂ωz/∂θ₆ |
13//! ```
14//!  The first three rows correspond to the linear velocities: vx, vy, vz.
15//!  The last three rows correspond to the angular velocities: roll (ωx), pitch (ωy), and yaw (ωz).
16//!  θ₁, θ₂, θ₃, θ₄, θ₅, θ₆ are the joint angles.
17//!  ∂ denotes a partial derivative.
18
19extern crate nalgebra as na;
20
21use na::{Matrix6, Vector6, Isometry3};
22use na::linalg::SVD;
23use crate::kinematic_traits::{Joints, Kinematics};
24use crate::utils::vector6_to_joints;
25
26/// This structure holds Jacobian matrix and provides methods to
27/// extract velocity and torgue information from it.
28///
29///
30///  This package provides support for the Jacobian matrix.
31///
32///  The Jacobian matrix, as described here, represents the relationship between the joint velocities
33///  and the end-effector velocities:
34/// ```text
35///  | ∂vx/∂θ₁  ∂vx/∂θ₂  ∂vx/∂θ₃  ∂vx/∂θ₄  ∂vx/∂θ₅  ∂vx/∂θ₆ |
36///  | ∂vy/∂θ₁  ∂vy/∂θ₂  ∂vy/∂θ₃  ∂vy/∂θ₄  ∂vy/∂θ₅  ∂vy/∂θ₆ |
37///  | ∂vz/∂θ₁  ∂vz/∂θ₂  ∂vz/∂θ₃  ∂vz/∂θ₄  ∂vz/∂θ₅  ∂vz/∂θ₆ |
38///  | ∂ωx/∂θ₁  ∂ωx/∂θ₂  ∂ωx/∂θ₃  ∂ωx/∂θ₄  ∂ωx/∂θ₅  ∂ωx/∂θ₆ |
39///  | ∂ωy/∂θ₁  ∂ωy/∂θ₂  ∂ωy/∂θ₃  ∂ωy/∂θ₄  ∂ωy/∂θ₅  ∂ωy/∂θ₆ |
40///  | ∂ωz/∂θ₁  ∂ωz/∂θ₂  ∂ωz/∂θ₃  ∂ωz/∂θ₄  ∂ωz/∂θ₅  ∂ωz/∂θ₆ |
41/// ```
42///  The first three rows correspond to the linear velocities: vx, vy, vz.
43///  The last three rows correspond to the angular velocities: roll (ωx), pitch (ωy), and yaw (ωz).
44///  θ₁, θ₂, θ₃, θ₄, θ₅, θ₆ are the joint angles.
45///  ∂ denotes a partial derivative.
46pub struct Jacobian {
47    /// A 6x6 matrix representing the Jacobian
48    ///
49    /// The Jacobian matrix maps the joint velocities to the end-effector velocities.
50    /// Each column corresponds to a joint, and each row corresponds to a degree of freedom
51    /// of the end-effector (linear and angular velocities).    
52    matrix: Matrix6<f64>,
53
54    /// The disturbance value used for computing the Jacobian
55    epsilon: f64,
56}
57
58impl Jacobian {
59    /// Constructs a new Jacobian struct by computing the Jacobian matrix for the given robot and joint configuration
60    ///
61    /// # Arguments
62    ///
63    /// * `robot` - A reference to the robot implementing the Kinematics trait
64    /// * `qs` - A reference to the joint configuration
65    /// * `epsilon` - A small value used for numerical differentiation
66    ///
67    /// # Returns
68    ///
69    /// A new instance of `Jacobian`
70    pub fn new(robot: &impl Kinematics, qs: &Joints, epsilon: f64) -> Self {
71        let matrix = compute_jacobian(robot, qs, epsilon);
72        Self { matrix, epsilon }
73    }
74
75    /// Computes the joint velocities required to achieve a desired end-effector velocity:
76    ///
77    /// Q' = J⁻¹ x'
78    ///
79    /// where Q' are joint velocities, J⁻¹ is the inverted Jacobian matrix and x' is the vector
80    /// of velocities of the tool center point. First 3 components are velocities along x,y and z
81    /// axis, the other 3 are angular rotation velocities around x (roll), y (pitch) and z (yaw) axis
82    ///
83    /// # Arguments
84    ///
85    /// * `desired_end_effector_velocity` - An Isometry3 representing the desired linear and
86    ///         angular velocity of the end-effector. The x' vector is extracted from the isometry.
87    ///
88    /// # Returns
89    ///
90    /// `Result<Joints, &'static str>` - Joint positions, with values representing joint velocities rather than angles,
91    /// or an error message if the computation fails.
92    ///
93    /// This method extracts the linear and angular velocities from the provided Isometry3
94    /// and combines them into a single 6D vector. It then computes the joint velocities required
95    /// to achieve the desired end-effector velocity using the `velocities_from_vector` method.
96    pub fn velocities(&self, desired_end_effector_velocity: &Isometry3<f64>) -> Result<Joints, &'static str> {
97        // Extract the linear velocity (translation) and angular velocity (rotation)
98        let linear_velocity = desired_end_effector_velocity.translation.vector;
99        let angular_velocity = desired_end_effector_velocity.rotation.scaled_axis();
100
101        // Combine into a single 6D vector
102        let desired_velocity = Vector6::new(
103            linear_velocity.x, linear_velocity.y, linear_velocity.z,
104            angular_velocity.x, angular_velocity.y, angular_velocity.z,
105        );
106
107        // Compute the joint velocities from the 6D vector
108        self.velocities_from_vector(&desired_velocity)
109    }
110
111    /// Computes the joint velocities required to achieve a desired end-effector velocity:
112    ///
113    /// Q' = J⁻¹ x'
114    ///
115    /// where Q' are joint velocities, J⁻¹ is the inverted Jacobian matrix and x' is the vector
116    /// of velocities of the tool center point. First 3 components are velocities along x,y and z
117    /// axis. The remaining 3 are angular rotation velocities are assumed to be zero.
118    ///
119    /// # Arguments
120    ///
121    /// * `vx, vy, vz` - x, y and z components of end effector velocity (linear).
122    ///
123    /// # Returns
124    ///
125    /// `Result<Joints, &'static str>` - Joint positions, with values representing
126    /// joint velocities rather than angles or an error message if the computation fails.
127    pub fn velocities_fixed(&self, vx: f64, vy: f64, vz: f64) -> Result<Joints, &'static str> {
128
129        // Combine into a single 6D vector with 0 rotational part
130        let desired_velocity = Vector6::new(
131            vx, vy, vz,
132            0.0, 0.0, 0.0,
133        );
134
135        // Compute the joint velocities from the 6D vector
136        self.velocities_from_vector(&desired_velocity)
137    }
138
139    /// Computes the joint velocities required to achieve a desired end-effector velocity:
140    ///
141    /// Q' = J⁻¹ X'
142    ///
143    /// where Q' are joint velocities, J⁻¹ is the inverted Jacobian matrix and x' is the vector
144    /// of velocities of the tool center point. First 3 components are velocities along x,y and z
145    /// axis, the other 3 are angular rotation velocities around x (roll), y (pitch) and z (yaw) axis
146    ///
147    /// # Arguments
148    ///
149    /// * `X'` - A 6D vector representing the desired linear and angular velocity of the
150    ///     end-effector as defined above.
151    ///
152    /// # Returns
153    ///
154    /// `Result<Joints, &'static str>` - Joint positions, with values representing joint velocities rather than angles,
155    /// or an error message if the computation fails.
156    ///
157    /// This method tries to compute the joint velocities using the inverse of the Jacobian matrix.
158    /// If the Jacobian matrix is not invertible, it falls back to using the pseudoinverse.
159    #[allow(non_snake_case)] // Standard Math notation calls for single uppercase name
160    pub fn velocities_from_vector(&self, X: &Vector6<f64>) -> Result<Joints, &'static str> {
161        // Try to calculate the joint velocities using the inverse of the Jacobian matrix
162        let joint_velocities: Vector6<f64>;
163        if let Some(jacobian_inverse) = self.matrix.try_inverse() {
164            joint_velocities = jacobian_inverse * X;
165        } else {
166            // If the inverse does not exist, use the pseudoinverse
167            let svd = SVD::new(self.matrix.clone(), true, true);
168            match svd.pseudo_inverse(self.epsilon) {
169                Ok(jacobian_pseudoinverse) => {
170                    joint_velocities = jacobian_pseudoinverse * X;
171                }
172                Err(_) => {
173                    return Err("Unable to compute the pseudoinverse of the Jacobian matrix");
174                }
175            }
176        }
177        // Convert the resulting Vector6 to Joints
178        Ok(vector6_to_joints(joint_velocities))
179    }
180
181    /// Computes the joint torques required to achieve a desired end-effector force/torque
182    /// This function computes
183    ///
184    /// t = JᵀF
185    ///
186    /// where Jᵀ is transposed Jacobian as defined above and f is the desired force vector that
187    /// is extracted from the passed Isometry3.
188    ///
189    /// # Arguments
190    ///
191    /// * `desired_force_torque` - isometry structure representing forces (in Newtons, N) and torgues
192    ///                            (in Newton - meters, Nm) rather than dimensions and angles.
193    ///
194    /// # Returns
195    ///
196    /// Joint positions, with values representing joint torques,
197    /// or an error message if the computation fails.
198    pub fn torques(&self, desired_force_isometry: &Isometry3<f64>) -> Joints {
199
200        // Extract the linear velocity (translation) and angular velocity (rotation)
201        let linear_force = desired_force_isometry.translation.vector;
202        let angular_torgue = desired_force_isometry.rotation.scaled_axis();
203
204        // Combine into a single 6D vector
205        let desired_force_torgue_vector = Vector6::new(
206            linear_force.x, linear_force.y, linear_force.z,
207            angular_torgue.x, angular_torgue.y, angular_torgue.z,
208        );
209
210        let joint_torques = self.matrix.transpose() * desired_force_torgue_vector;
211        vector6_to_joints(joint_torques)
212    }
213
214    /// Computes the joint torques required to achieve a desired end-effector force/torque
215    /// This function computes
216    ///
217    /// t = JᵀF
218    ///
219    /// where Jᵀ is transposed Jacobian as defined above and f is the desired force and torgue
220    /// vector. The first 3 components are forces along x, y and z in Newtons, the other 3
221    /// components are rotations around x (roll), y (pitch) and z (yaw) axis in Newton meters.
222    ///
223    /// # Arguments
224    ///
225    /// * `F` - A 6D vector representing the desired force and torque at the end-effector
226    ///     as explained above.
227    ///
228    /// # Returns
229    ///
230    /// Joint positions, with values representing joint torques,
231    /// or an error message if the computation fails.
232    #[allow(non_snake_case)] // Standard Math notation calls for single uppercase name
233    pub fn torques_from_vector(&self, F: &Vector6<f64>) -> Joints {
234        let joint_torques = self.matrix.transpose() * F;
235        vector6_to_joints(joint_torques)
236    }
237}
238
239/// Function to compute the Jacobian matrix for a given robot and joint configuration
240///
241/// # Arguments
242///
243/// * `robot` - A reference to the robot implementing the Kinematics trait
244/// * `qs` - A reference to the joint configuration
245/// * `epsilon` - A small value used for numerical differentiation
246///
247/// # Returns
248///
249/// A 6x6 matrix representing the Jacobian
250///
251/// The Jacobian matrix maps the joint velocities to the end-effector velocities.
252/// Each column corresponds to a joint, and each row corresponds to a degree of freedom
253/// of the end-effector (linear and angular velocities).
254pub(crate) fn compute_jacobian(robot: &impl Kinematics, joints: &Joints, epsilon: f64) -> Matrix6<f64> {
255    let mut jacobian = Matrix6::zeros();
256    let current_pose = robot.forward(joints);
257    let current_position = current_pose.translation.vector;
258    let current_orientation = current_pose.rotation;
259
260    // Parallelize the loop using rayon
261    let jacobian_columns: Vec<_> = (0..6).into_iter().map(|i| {
262        let mut perturbed_qs = *joints;
263        perturbed_qs[i] += epsilon;
264        let perturbed_pose = robot.forward(&perturbed_qs);
265        let perturbed_position = perturbed_pose.translation.vector;
266        let perturbed_orientation = perturbed_pose.rotation;
267
268        let delta_position = (perturbed_position - current_position) / epsilon;
269        let delta_orientation = (perturbed_orientation * current_orientation.inverse()).scaled_axis() / epsilon;
270
271        (delta_position, delta_orientation)
272    }).collect();
273
274    for (i, (delta_position, delta_orientation)) in jacobian_columns.into_iter().enumerate() {
275        jacobian.fixed_view_mut::<3, 1>(0, i).copy_from(&delta_position);
276        jacobian.fixed_view_mut::<3, 1>(3, i).copy_from(&delta_orientation);
277    }
278
279    jacobian
280}
281
282#[cfg(test)]
283mod tests {
284    use nalgebra::{Translation3, UnitQuaternion, Vector3};
285    use crate::constraints::Constraints;
286    use crate::kinematic_traits::{Pose, Singularity, Solutions};
287    use super::*;
288
289    const EPSILON: f64 = 1e-6;
290
291    /// Example implementation of the Kinematics trait for a single rotary joint robot
292    /// When the first joint rotates, it affects the Y-position and the Z-orientation of the end-effector.
293    /// The derivative of the Y-position with respect to the first joint should be 1.
294    /// The derivative of the Z-orientation with respect to the first joint should be 1.
295    /// No other joint affects the end-effector in this simple robot model.
296    pub struct SingleRotaryJointRobot;
297
298    impl Kinematics for SingleRotaryJointRobot {
299        fn inverse(&self, _pose: &Pose) -> Solutions {
300            panic!() // Not used in this test
301        }
302
303        fn inverse_5dof(&self, _pose: &Pose, _j6: f64) -> Solutions {
304            panic!() // Not used in this test
305        }
306
307        fn inverse_continuing_5dof(&self, _pose: &Pose, _prev: &Joints) -> Solutions {
308            panic!() // Not used in this test
309        }
310
311        fn forward_with_joint_poses(&self, _joints: &Joints) -> [Pose; 6] {
312            panic!() // Not used in this test
313        }        
314
315        /// Simple inverse kinematics for a single rotary joint of the length 1.
316        fn inverse_continuing(&self, pose: &Pose, _previous: &Joints) -> Solutions {
317            let angle = pose.translation.vector[1].atan2(pose.translation.vector[0]);
318            vec![[angle, 0.0, 0.0, 0.0, 0.0, 0.0]]
319        }
320
321        fn forward(&self, qs: &Joints) -> Pose {
322            // Forward kinematics for a single rotary joint robot
323            let angle = qs[0];
324            let rotation = UnitQuaternion::from_euler_angles(0.0, 0.0, angle);
325            let translation = Translation3::new(angle.cos(), angle.sin(), 0.0);
326            Isometry3::from_parts(translation, rotation)
327        }
328
329        fn kinematic_singularity(&self, _qs: &Joints) -> Option<Singularity> {
330            None
331        }
332
333        fn constraints(&self) -> &Option<Constraints> {
334            &None
335        }
336    }
337
338
339    fn assert_matrix_approx_eq(left: &Matrix6<f64>, right: &Matrix6<f64>, epsilon: f64) {
340        for i in 0..6 {
341            for j in 0..6 {
342                assert!((left[(i, j)] - right[(i, j)]).abs() < epsilon, "left[{0},{1}] = {2} is not approximately equal to right[{0},{1}] = {3}", i, j, left[(i, j)], right[(i, j)]);
343            }
344        }
345    }
346
347    #[test]
348    fn test_forward_kinematics() {
349        let robot = SingleRotaryJointRobot;
350        let joints: Joints = [std::f64::consts::FRAC_PI_2, 0.0, 0.0, 0.0, 0.0, 0.0];
351        let pose = robot.forward(&joints);
352        assert!((pose.translation.vector[0] - 0.0).abs() < EPSILON);
353        assert!((pose.translation.vector[1] - 1.0).abs() < EPSILON);
354    }
355
356    #[test]
357    fn test_inverse_kinematics() {
358        let robot = SingleRotaryJointRobot;
359        let pose = Isometry3::new(Vector3::new(0.0, 1.0, 0.0), na::zero());
360        let previous: Joints = [0.0; 6];
361        let solutions = robot.inverse_continuing(&pose, &previous);
362        assert_eq!(solutions.len(), 1);
363        assert!((solutions[0][0] - std::f64::consts::FRAC_PI_2).abs() < EPSILON);
364    }
365
366    #[test]
367    fn test_compute_jacobian() {
368        let robot = SingleRotaryJointRobot;
369
370        // This loop was used to profile rayon performance. No improvement was found so not used.
371        for _e in 0..2 {
372            let joints: Joints = [0.0; 6];
373            let jacobian = compute_jacobian(&robot, &joints, EPSILON);
374            let mut expected_jacobian = Matrix6::zeros();
375
376            expected_jacobian[(0, 0)] = 0.0; // No effect on X position
377            expected_jacobian[(1, 0)] = 1.0; // Y position is affected by the first joint
378            expected_jacobian[(2, 0)] = 0.0; // No effect on Z position
379
380            expected_jacobian[(3, 0)] = 0.0; // No effect on X orientation
381            expected_jacobian[(4, 0)] = 0.0; // No effect on Y orientation
382            expected_jacobian[(5, 0)] = 1.0; // Z orientation is affected by the first joint
383
384            assert_matrix_approx_eq(&jacobian, &expected_jacobian, EPSILON);
385        }
386    }
387
388    #[test]
389    fn test_velocities_from_iso() {
390        let robot = SingleRotaryJointRobot;
391        let initial_qs = [0.0; 6];
392        let jacobian = Jacobian::new(&robot, &initial_qs, EPSILON);
393
394        // Given an end effector located 1 meter away from the axis of rotation, 
395        // with the joint rotating at a speed of 1 radian per second, the tip velocity is
396        // one meter per second. Given we start from the angle 0, it all goes to the y component.
397        let desired_velocity_isometry =
398            Isometry3::new(Vector3::new(0.0, 1.0, 0.0),
399                           Vector3::new(0.0, 0.0, 1.0));
400        let result = jacobian.velocities(&desired_velocity_isometry);
401
402        assert!(result.is_ok());
403        let joint_velocities = result.unwrap();
404        println!("Computed joint velocities: {:?}", joint_velocities);
405
406        // Add assertions to verify the expected values
407        assert!((joint_velocities[0] - 1.0).abs() < EPSILON);
408        assert_eq!(joint_velocities[1], 0.0);
409        assert_eq!(joint_velocities[2], 0.0);
410        assert_eq!(joint_velocities[3], 0.0);
411        assert_eq!(joint_velocities[4], 0.0);
412        assert_eq!(joint_velocities[5], 0.0);
413    }
414
415    #[test]
416    fn test_compute_joint_torques() {
417        let robot = SingleRotaryJointRobot;
418        let initial_qs = [0.0; 6];
419        let jacobian = Jacobian::new(&robot, &initial_qs, EPSILON);
420
421        // For a single joint robot, that we want on the torgue is what we need to put
422        let desired_force_torque =
423            Isometry3::new(Vector3::new(0.0, 0.0, 0.0),
424                           Vector3::new(0.0, 0.0, 1.234));
425
426        let joint_torques = jacobian.torques(&desired_force_torque);
427        println!("Computed joint torques: {:?}", joint_torques);
428
429        assert_eq!(joint_torques[0], 1.234);
430        assert_eq!(joint_torques[1], 0.0);
431        assert_eq!(joint_torques[2], 0.0);
432        assert_eq!(joint_torques[3], 0.0);
433        assert_eq!(joint_torques[4], 0.0);
434        assert_eq!(joint_torques[5], 0.0);
435    }
436}
437
438
439