rs_opw_kinematics/
jacobian.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
//!
//!  This package provides support for the Jacobian matrix.
//!
//!  The Jacobian matrix, as described here, represents the relationship between the joint velocities
//!  and the end-effector velocities:
//! ```text
//!  | ∂vx/∂θ₁  ∂vx/∂θ₂  ∂vx/∂θ₃  ∂vx/∂θ₄  ∂vx/∂θ₅  ∂vx/∂θ₆ |
//!  | ∂vy/∂θ₁  ∂vy/∂θ₂  ∂vy/∂θ₃  ∂vy/∂θ₄  ∂vy/∂θ₅  ∂vy/∂θ₆ |
//!  | ∂vz/∂θ₁  ∂vz/∂θ₂  ∂vz/∂θ₃  ∂vz/∂θ₄  ∂vz/∂θ₅  ∂vz/∂θ₆ |
//!  | ∂ωx/∂θ₁  ∂ωx/∂θ₂  ∂ωx/∂θ₃  ∂ωx/∂θ₄  ∂ωx/∂θ₅  ∂ωx/∂θ₆ |
//!  | ∂ωy/∂θ₁  ∂ωy/∂θ₂  ∂ωy/∂θ₃  ∂ωy/∂θ₄  ∂ωy/∂θ₅  ∂ωy/∂θ₆ |
//!  | ∂ωz/∂θ₁  ∂ωz/∂θ₂  ∂ωz/∂θ₃  ∂ωz/∂θ₄  ∂ωz/∂θ₅  ∂ωz/∂θ₆ |
//! ```
//!  The first three rows correspond to the linear velocities: vx, vy, vz.
//!  The last three rows correspond to the angular velocities: roll (ωx), pitch (ωy), and yaw (ωz).
//!  θ₁, θ₂, θ₃, θ₄, θ₅, θ₆ are the joint angles.
//!  ∂ denotes a partial derivative.

extern crate nalgebra as na;

use na::{Matrix6, Vector6, Isometry3};
use na::linalg::SVD;
use crate::kinematic_traits::{Joints, Kinematics};
use crate::utils::vector6_to_joints;

/// This structure holds Jacobian matrix and provides methods to
/// extract velocity and torgue information from it.
///
///
///  This package provides support for the Jacobian matrix.
///
///  The Jacobian matrix, as described here, represents the relationship between the joint velocities
///  and the end-effector velocities:
/// ```text
///  | ∂vx/∂θ₁  ∂vx/∂θ₂  ∂vx/∂θ₃  ∂vx/∂θ₄  ∂vx/∂θ₅  ∂vx/∂θ₆ |
///  | ∂vy/∂θ₁  ∂vy/∂θ₂  ∂vy/∂θ₃  ∂vy/∂θ₄  ∂vy/∂θ₅  ∂vy/∂θ₆ |
///  | ∂vz/∂θ₁  ∂vz/∂θ₂  ∂vz/∂θ₃  ∂vz/∂θ₄  ∂vz/∂θ₅  ∂vz/∂θ₆ |
///  | ∂ωx/∂θ₁  ∂ωx/∂θ₂  ∂ωx/∂θ₃  ∂ωx/∂θ₄  ∂ωx/∂θ₅  ∂ωx/∂θ₆ |
///  | ∂ωy/∂θ₁  ∂ωy/∂θ₂  ∂ωy/∂θ₃  ∂ωy/∂θ₄  ∂ωy/∂θ₅  ∂ωy/∂θ₆ |
///  | ∂ωz/∂θ₁  ∂ωz/∂θ₂  ∂ωz/∂θ₃  ∂ωz/∂θ₄  ∂ωz/∂θ₅  ∂ωz/∂θ₆ |
/// ```
///  The first three rows correspond to the linear velocities: vx, vy, vz.
///  The last three rows correspond to the angular velocities: roll (ωx), pitch (ωy), and yaw (ωz).
///  θ₁, θ₂, θ₃, θ₄, θ₅, θ₆ are the joint angles.
///  ∂ denotes a partial derivative.
pub struct Jacobian {
    /// A 6x6 matrix representing the Jacobian
    ///
    /// The Jacobian matrix maps the joint velocities to the end-effector velocities.
    /// Each column corresponds to a joint, and each row corresponds to a degree of freedom
    /// of the end-effector (linear and angular velocities).    
    matrix: Matrix6<f64>,

    /// The disturbance value used for computing the Jacobian
    epsilon: f64,
}

impl Jacobian {
    /// Constructs a new Jacobian struct by computing the Jacobian matrix for the given robot and joint configuration
    ///
    /// # Arguments
    ///
    /// * `robot` - A reference to the robot implementing the Kinematics trait
    /// * `qs` - A reference to the joint configuration
    /// * `epsilon` - A small value used for numerical differentiation
    ///
    /// # Returns
    ///
    /// A new instance of `Jacobian`
    pub fn new(robot: &impl Kinematics, qs: &Joints, epsilon: f64) -> Self {
        let matrix = compute_jacobian(robot, qs, epsilon);
        Self { matrix, epsilon }
    }

    /// Computes the joint velocities required to achieve a desired end-effector velocity:
    ///
    /// Q' = J⁻¹ x'
    ///
    /// where Q' are joint velocities, J⁻¹ is the inverted Jacobian matrix and x' is the vector
    /// of velocities of the tool center point. First 3 components are velocities along x,y and z
    /// axis, the other 3 are angular rotation velocities around x (roll), y (pitch) and z (yaw) axis
    ///
    /// # Arguments
    ///
    /// * `desired_end_effector_velocity` - An Isometry3 representing the desired linear and
    ///         angular velocity of the end-effector. The x' vector is extracted from the isometry.
    ///
    /// # Returns
    ///
    /// `Result<Joints, &'static str>` - Joint positions, with values representing joint velocities rather than angles,
    /// or an error message if the computation fails.
    ///
    /// This method extracts the linear and angular velocities from the provided Isometry3
    /// and combines them into a single 6D vector. It then computes the joint velocities required
    /// to achieve the desired end-effector velocity using the `velocities_from_vector` method.
    pub fn velocities(&self, desired_end_effector_velocity: &Isometry3<f64>) -> Result<Joints, &'static str> {
        // Extract the linear velocity (translation) and angular velocity (rotation)
        let linear_velocity = desired_end_effector_velocity.translation.vector;
        let angular_velocity = desired_end_effector_velocity.rotation.scaled_axis();

        // Combine into a single 6D vector
        let desired_velocity = Vector6::new(
            linear_velocity.x, linear_velocity.y, linear_velocity.z,
            angular_velocity.x, angular_velocity.y, angular_velocity.z,
        );

        // Compute the joint velocities from the 6D vector
        self.velocities_from_vector(&desired_velocity)
    }

    /// Computes the joint velocities required to achieve a desired end-effector velocity:
    ///
    /// Q' = J⁻¹ x'
    ///
    /// where Q' are joint velocities, J⁻¹ is the inverted Jacobian matrix and x' is the vector
    /// of velocities of the tool center point. First 3 components are velocities along x,y and z
    /// axis. The remaining 3 are angular rotation velocities are assumed to be zero.
    ///
    /// # Arguments
    ///
    /// * `vx, vy, vz` - x, y and z components of end effector velocity (linear).
    ///
    /// # Returns
    ///
    /// `Result<Joints, &'static str>` - Joint positions, with values representing
    /// joint velocities rather than angles or an error message if the computation fails.
    pub fn velocities_fixed(&self, vx: f64, vy: f64, vz: f64) -> Result<Joints, &'static str> {

        // Combine into a single 6D vector with 0 rotational part
        let desired_velocity = Vector6::new(
            vx, vy, vz,
            0.0, 0.0, 0.0,
        );

        // Compute the joint velocities from the 6D vector
        self.velocities_from_vector(&desired_velocity)
    }

    /// Computes the joint velocities required to achieve a desired end-effector velocity:
    ///
    /// Q' = J⁻¹ X'
    ///
    /// where Q' are joint velocities, J⁻¹ is the inverted Jacobian matrix and x' is the vector
    /// of velocities of the tool center point. First 3 components are velocities along x,y and z
    /// axis, the other 3 are angular rotation velocities around x (roll), y (pitch) and z (yaw) axis
    ///
    /// # Arguments
    ///
    /// * `X'` - A 6D vector representing the desired linear and angular velocity of the
    ///     end-effector as defined above.
    ///
    /// # Returns
    ///
    /// `Result<Joints, &'static str>` - Joint positions, with values representing joint velocities rather than angles,
    /// or an error message if the computation fails.
    ///
    /// This method tries to compute the joint velocities using the inverse of the Jacobian matrix.
    /// If the Jacobian matrix is not invertible, it falls back to using the pseudoinverse.
    #[allow(non_snake_case)] // Standard Math notation calls for single uppercase name
    pub fn velocities_from_vector(&self, X: &Vector6<f64>) -> Result<Joints, &'static str> {
        // Try to calculate the joint velocities using the inverse of the Jacobian matrix
        let joint_velocities: Vector6<f64>;
        if let Some(jacobian_inverse) = self.matrix.try_inverse() {
            joint_velocities = jacobian_inverse * X;
        } else {
            // If the inverse does not exist, use the pseudoinverse
            let svd = SVD::new(self.matrix.clone(), true, true);
            match svd.pseudo_inverse(self.epsilon) {
                Ok(jacobian_pseudoinverse) => {
                    joint_velocities = jacobian_pseudoinverse * X;
                }
                Err(_) => {
                    return Err("Unable to compute the pseudoinverse of the Jacobian matrix");
                }
            }
        }
        // Convert the resulting Vector6 to Joints
        Ok(vector6_to_joints(joint_velocities))
    }

    /// Computes the joint torques required to achieve a desired end-effector force/torque
    /// This function computes
    ///
    /// t = JᵀF
    ///
    /// where Jᵀ is transposed Jacobian as defined above and f is the desired force vector that
    /// is extracted from the passed Isometry3.
    ///
    /// # Arguments
    ///
    /// * `desired_force_torque` - isometry structure representing forces (in Newtons, N) and torgues
    ///                            (in Newton - meters, Nm) rather than dimensions and angles.
    ///
    /// # Returns
    ///
    /// Joint positions, with values representing joint torques,
    /// or an error message if the computation fails.
    pub fn torques(&self, desired_force_isometry: &Isometry3<f64>) -> Joints {

        // Extract the linear velocity (translation) and angular velocity (rotation)
        let linear_force = desired_force_isometry.translation.vector;
        let angular_torgue = desired_force_isometry.rotation.scaled_axis();

        // Combine into a single 6D vector
        let desired_force_torgue_vector = Vector6::new(
            linear_force.x, linear_force.y, linear_force.z,
            angular_torgue.x, angular_torgue.y, angular_torgue.z,
        );

        let joint_torques = self.matrix.transpose() * desired_force_torgue_vector;
        vector6_to_joints(joint_torques)
    }

    /// Computes the joint torques required to achieve a desired end-effector force/torque
    /// This function computes
    ///
    /// t = JᵀF
    ///
    /// where Jᵀ is transposed Jacobian as defined above and f is the desired force and torgue
    /// vector. The first 3 components are forces along x, y and z in Newtons, the other 3
    /// components are rotations around x (roll), y (pitch) and z (yaw) axis in Newton meters.
    ///
    /// # Arguments
    ///
    /// * `F` - A 6D vector representing the desired force and torque at the end-effector
    ///     as explained above.
    ///
    /// # Returns
    ///
    /// Joint positions, with values representing joint torques,
    /// or an error message if the computation fails.
    #[allow(non_snake_case)] // Standard Math notation calls for single uppercase name
    pub fn torques_from_vector(&self, F: &Vector6<f64>) -> Joints {
        let joint_torques = self.matrix.transpose() * F;
        vector6_to_joints(joint_torques)
    }
}

/// Function to compute the Jacobian matrix for a given robot and joint configuration
///
/// # Arguments
///
/// * `robot` - A reference to the robot implementing the Kinematics trait
/// * `qs` - A reference to the joint configuration
/// * `epsilon` - A small value used for numerical differentiation
///
/// # Returns
///
/// A 6x6 matrix representing the Jacobian
///
/// The Jacobian matrix maps the joint velocities to the end-effector velocities.
/// Each column corresponds to a joint, and each row corresponds to a degree of freedom
/// of the end-effector (linear and angular velocities).
pub(crate) fn compute_jacobian(robot: &impl Kinematics, joints: &Joints, epsilon: f64) -> Matrix6<f64> {
    let mut jacobian = Matrix6::zeros();
    let current_pose = robot.forward(joints);
    let current_position = current_pose.translation.vector;
    let current_orientation = current_pose.rotation;

    // Parallelize the loop using rayon
    let jacobian_columns: Vec<_> = (0..6).into_iter().map(|i| {
        let mut perturbed_qs = *joints;
        perturbed_qs[i] += epsilon;
        let perturbed_pose = robot.forward(&perturbed_qs);
        let perturbed_position = perturbed_pose.translation.vector;
        let perturbed_orientation = perturbed_pose.rotation;

        let delta_position = (perturbed_position - current_position) / epsilon;
        let delta_orientation = (perturbed_orientation * current_orientation.inverse()).scaled_axis() / epsilon;

        (delta_position, delta_orientation)
    }).collect();

    for (i, (delta_position, delta_orientation)) in jacobian_columns.into_iter().enumerate() {
        jacobian.fixed_view_mut::<3, 1>(0, i).copy_from(&delta_position);
        jacobian.fixed_view_mut::<3, 1>(3, i).copy_from(&delta_orientation);
    }

    jacobian
}

#[cfg(test)]
mod tests {
    use nalgebra::{Translation3, UnitQuaternion, Vector3};
    use crate::constraints::Constraints;
    use crate::kinematic_traits::{Pose, Singularity, Solutions};
    use super::*;

    const EPSILON: f64 = 1e-6;

    /// Example implementation of the Kinematics trait for a single rotary joint robot
    /// When the first joint rotates, it affects the Y-position and the Z-orientation of the end-effector.
    /// The derivative of the Y-position with respect to the first joint should be 1.
    /// The derivative of the Z-orientation with respect to the first joint should be 1.
    /// No other joint affects the end-effector in this simple robot model.
    pub struct SingleRotaryJointRobot;

    impl Kinematics for SingleRotaryJointRobot {
        fn inverse(&self, _pose: &Pose) -> Solutions {
            panic!() // Not used in this test
        }

        fn inverse_5dof(&self, _pose: &Pose, _j6: f64) -> Solutions {
            panic!() // Not used in this test
        }

        fn inverse_continuing_5dof(&self, _pose: &Pose, _prev: &Joints) -> Solutions {
            panic!() // Not used in this test
        }

        fn forward_with_joint_poses(&self, _joints: &Joints) -> [Pose; 6] {
            panic!() // Not used in this test
        }        

        /// Simple inverse kinematics for a single rotary joint of the length 1.
        fn inverse_continuing(&self, pose: &Pose, _previous: &Joints) -> Solutions {
            let angle = pose.translation.vector[1].atan2(pose.translation.vector[0]);
            vec![[angle, 0.0, 0.0, 0.0, 0.0, 0.0]]
        }

        fn forward(&self, qs: &Joints) -> Pose {
            // Forward kinematics for a single rotary joint robot
            let angle = qs[0];
            let rotation = UnitQuaternion::from_euler_angles(0.0, 0.0, angle);
            let translation = Translation3::new(angle.cos(), angle.sin(), 0.0);
            Isometry3::from_parts(translation, rotation)
        }

        fn kinematic_singularity(&self, _qs: &Joints) -> Option<Singularity> {
            None
        }

        fn constraints(&self) -> &Option<Constraints> {
            &None
        }
    }


    fn assert_matrix_approx_eq(left: &Matrix6<f64>, right: &Matrix6<f64>, epsilon: f64) {
        for i in 0..6 {
            for j in 0..6 {
                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)]);
            }
        }
    }

    #[test]
    fn test_forward_kinematics() {
        let robot = SingleRotaryJointRobot;
        let joints: Joints = [std::f64::consts::FRAC_PI_2, 0.0, 0.0, 0.0, 0.0, 0.0];
        let pose = robot.forward(&joints);
        assert!((pose.translation.vector[0] - 0.0).abs() < EPSILON);
        assert!((pose.translation.vector[1] - 1.0).abs() < EPSILON);
    }

    #[test]
    fn test_inverse_kinematics() {
        let robot = SingleRotaryJointRobot;
        let pose = Isometry3::new(Vector3::new(0.0, 1.0, 0.0), na::zero());
        let previous: Joints = [0.0; 6];
        let solutions = robot.inverse_continuing(&pose, &previous);
        assert_eq!(solutions.len(), 1);
        assert!((solutions[0][0] - std::f64::consts::FRAC_PI_2).abs() < EPSILON);
    }

    #[test]
    fn test_compute_jacobian() {
        let robot = SingleRotaryJointRobot;

        // This loop was used to profile rayon performance. No improvement was found so not used.
        for _e in 0..2 {
            let joints: Joints = [0.0; 6];
            let jacobian = compute_jacobian(&robot, &joints, EPSILON);
            let mut expected_jacobian = Matrix6::zeros();

            expected_jacobian[(0, 0)] = 0.0; // No effect on X position
            expected_jacobian[(1, 0)] = 1.0; // Y position is affected by the first joint
            expected_jacobian[(2, 0)] = 0.0; // No effect on Z position

            expected_jacobian[(3, 0)] = 0.0; // No effect on X orientation
            expected_jacobian[(4, 0)] = 0.0; // No effect on Y orientation
            expected_jacobian[(5, 0)] = 1.0; // Z orientation is affected by the first joint

            assert_matrix_approx_eq(&jacobian, &expected_jacobian, EPSILON);
        }
    }

    #[test]
    fn test_velocities_from_iso() {
        let robot = SingleRotaryJointRobot;
        let initial_qs = [0.0; 6];
        let jacobian = Jacobian::new(&robot, &initial_qs, EPSILON);

        // Given an end effector located 1 meter away from the axis of rotation, 
        // with the joint rotating at a speed of 1 radian per second, the tip velocity is
        // one meter per second. Given we start from the angle 0, it all goes to the y component.
        let desired_velocity_isometry =
            Isometry3::new(Vector3::new(0.0, 1.0, 0.0),
                           Vector3::new(0.0, 0.0, 1.0));
        let result = jacobian.velocities(&desired_velocity_isometry);

        assert!(result.is_ok());
        let joint_velocities = result.unwrap();
        println!("Computed joint velocities: {:?}", joint_velocities);

        // Add assertions to verify the expected values
        assert!((joint_velocities[0] - 1.0).abs() < EPSILON);
        assert_eq!(joint_velocities[1], 0.0);
        assert_eq!(joint_velocities[2], 0.0);
        assert_eq!(joint_velocities[3], 0.0);
        assert_eq!(joint_velocities[4], 0.0);
        assert_eq!(joint_velocities[5], 0.0);
    }

    #[test]
    fn test_compute_joint_torques() {
        let robot = SingleRotaryJointRobot;
        let initial_qs = [0.0; 6];
        let jacobian = Jacobian::new(&robot, &initial_qs, EPSILON);

        // For a single joint robot, that we want on the torgue is what we need to put
        let desired_force_torque =
            Isometry3::new(Vector3::new(0.0, 0.0, 0.0),
                           Vector3::new(0.0, 0.0, 1.234));

        let joint_torques = jacobian.torques(&desired_force_torque);
        println!("Computed joint torques: {:?}", joint_torques);

        assert_eq!(joint_torques[0], 1.234);
        assert_eq!(joint_torques[1], 0.0);
        assert_eq!(joint_torques[2], 0.0);
        assert_eq!(joint_torques[3], 0.0);
        assert_eq!(joint_torques[4], 0.0);
        assert_eq!(joint_torques[5], 0.0);
    }
}