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