rs_opw_kinematics/
kinematics_impl.rs

1//! Provides implementation of inverse and direct kinematics.
2
3use std::f64::{consts::PI};
4use crate::kinematic_traits::{Kinematics, Solutions, Pose, Singularity, Joints, JOINTS_AT_ZERO};
5use crate::kinematic_traits::{J4, J5, J6};
6use crate::parameters::opw_kinematics::{Parameters};
7use crate::utils::opw_kinematics::{is_valid};
8use nalgebra::{Isometry3, Matrix3, OVector, Rotation3, Translation3, U3, Unit, UnitQuaternion,
9               Vector3};
10use crate::constraints::{BY_CONSTRAINS, BY_PREV, Constraints};
11
12const DEBUG: bool = false;
13
14#[derive(Debug, Copy, Clone)]
15pub struct OPWKinematics {
16    /// The parameters that were used to construct this solver.
17    parameters: Parameters,
18    constraints: Option<Constraints>,
19
20    unit_z: Unit<OVector<f64, U3>>,
21}
22
23impl OPWKinematics {
24    /// Creates a new `OPWKinematics` instance with the given parameters.
25    #[allow(dead_code)]
26    pub fn new(parameters: Parameters) -> Self {
27        OPWKinematics {
28            parameters,
29            unit_z: Unit::new_normalize(Vector3::z_axis().into_inner()),
30            constraints: None,
31        }
32    }
33
34    /// Create a new instance that takes also Constraints.
35    /// If constraints are set, all solutions returned by this solver are constraint compliant.
36    pub fn new_with_constraints(parameters: Parameters, constraints: Constraints) -> Self {
37        OPWKinematics {
38            parameters,
39            unit_z: Unit::new_normalize(Vector3::z_axis().into_inner()),
40            constraints: Some(constraints),
41        }
42    }
43}
44
45
46const MM: f64 = 0.001;
47const DISTANCE_TOLERANCE: f64 = 0.001 * MM;
48const ANGULAR_TOLERANCE: f64 = 1E-6;
49
50// Use for singularity checks.
51const SINGULARITY_ANGLE_THR: f64 = 0.01 * PI / 180.0;
52
53impl Kinematics for OPWKinematics {
54    /// Return the solution that is constraint compliant anv values are valid
55    /// (no NaNs, etc) but otherwise not sorted.
56    /// If this is 5 degree of freedom robot only, the 6 joint is set to 0.0
57    /// The rotation of pose in this case is only approximate.
58    fn inverse(&self, pose: &Pose) -> Solutions {
59        if self.parameters.dof == 5 {
60            // For 5 DOF robot, we can only do 5 DOF approximate inverse.
61            self.inverse_intern_5_dof(pose, f64::NAN)
62        } else {
63            self.filter_constraints_compliant(self.inverse_intern(&pose))
64        }
65    }
66
67    // Replaces singularity with correct solution
68    // If this is 5 degree of freedom robot only, the 6 joint is set to as it was previous.
69    // The rotation of pose in this case is only approximate.
70    fn inverse_continuing(&self, pose: &Pose, prev: &Joints) -> Solutions {
71        if self.parameters.dof == 5 {
72            return self.inverse_intern_5_dof(pose, prev[5]);
73        }
74
75        let previous;
76        if prev[0].is_nan() {
77            // Special value CONSTRAINT_CENTERED has been used
78            previous = self.constraint_centers();
79        } else {
80            previous = prev;
81        }
82
83        const SINGULARITY_SHIFT: f64 = DISTANCE_TOLERANCE / 8.;
84        const SINGULARITY_SHIFTS: [[f64; 3]; 4] =
85            [[0., 0., 0., ], [SINGULARITY_SHIFT, 0., 0.],
86                [0., SINGULARITY_SHIFT, 0.], [0., 0., SINGULARITY_SHIFT]];
87
88        let mut solutions: Vec<Joints> = Vec::with_capacity(9);
89        let pt = pose.translation;
90
91        let rotation = pose.rotation;
92        'shifts: for d in SINGULARITY_SHIFTS {
93            let shifted = Pose::from_parts(
94                Translation3::new(pt.x + d[0], pt.y + d[1], pt.z + d[2]), rotation);
95            let ik = self.inverse_intern(&shifted);
96            // Self::dump_shifted_solutions(d, &ik);
97            if solutions.is_empty() {
98                // Unshifted version that comes first is always included into results
99                solutions.extend(&ik);
100            }
101
102            for s_idx in 0..ik.len() {
103                let singularity =
104                    self.kinematic_singularity(&ik[s_idx]);
105                if singularity.is_some() && is_valid(&ik[s_idx]) {
106                    let s;
107                    let s_n;
108                    if let Some(Singularity::A) = singularity {
109                        let mut now = ik[s_idx];
110                        if are_angles_close(now[J5], 0.) {
111                            // J5 = 0 singularity, J4 and J6 rotate same direction
112                            s = previous[J4] + previous[J6];
113                            s_n = now[J4] + now[J6];
114                        } else {
115                            // J5 = -180 or 180 singularity, even if the robot would need
116                            // specific design to rotate J5 to this angle without self-colliding.
117                            // J4 and J6 rotate in opposite directions
118                            s = previous[J4] - previous[J6];
119                            s_n = now[J4] - now[J6];
120
121                            // Fix J5 sign to match the previous
122                            normalize_near(&mut now[J5], previous[J5]);
123                        }
124
125                        let mut angle = s_n - s;
126                        while angle > PI {
127                            angle -= 2.0 * PI;
128                        }
129                        while angle < -PI {
130                            angle += 2.0 * PI;
131                        }
132                        let j_d = angle / 2.0;
133
134                        now[J4] = previous[J4] + j_d;
135                        now[J6] = previous[J6] - j_d;
136
137                        // Check last time if the pose is ok
138                        let check_pose = self.forward(&now);
139                        if compare_poses(&pose, &check_pose, DISTANCE_TOLERANCE, ANGULAR_TOLERANCE) &&
140                            self.constraints_compliant(now) {
141                            // Guard against the case our solution is out of constraints.
142                            solutions.push(now);
143                            // We only expect one singularity case hence once we found, we can end
144                            break 'shifts;
145                        }
146                    }
147
148                    break;
149                }
150            }
151        }
152        // Before any sorting, normalize all angles to be close to
153        // 'previous'
154        for s_idx in 0..solutions.len() {
155            for joint_idx in 0..6 {
156                normalize_near(&mut solutions[s_idx][joint_idx], previous[joint_idx]);
157            }
158        }
159        self.sort_by_closeness(&mut solutions, &previous);
160        self.filter_constraints_compliant(solutions)
161    }
162
163    fn forward(&self, joints: &Joints) -> Pose {
164        let p = &self.parameters;
165
166        // Apply sign corrections and offsets
167        let q1 = joints[0] * p.sign_corrections[0] as f64 - p.offsets[0];
168        let q2 = joints[1] * p.sign_corrections[1] as f64 - p.offsets[1];
169        let q3 = joints[2] * p.sign_corrections[2] as f64 - p.offsets[2];
170        let q4 = joints[3] * p.sign_corrections[3] as f64 - p.offsets[3];
171        let q5 = joints[4] * p.sign_corrections[4] as f64 - p.offsets[4];
172        let q6 = joints[5] * p.sign_corrections[5] as f64 - p.offsets[5];
173
174        let psi3 = f64::atan2(p.a2, p.c3);
175        let k = f64::sqrt(p.a2 * p.a2 + p.c3 * p.c3);
176
177        // Precompute q23_psi3 for better readability and reuse
178        let q23_psi3 = q2 + q3 + psi3;
179        let sin_q23_psi3 = q23_psi3.sin();
180        let cos_q23_psi3 = q23_psi3.cos();
181
182        let cx1 = p.c2 * f64::sin(q2) + k * sin_q23_psi3 + p.a1;
183        let cy1 = p.b;
184        let cz1 = p.c2 * f64::cos(q2) + k * cos_q23_psi3;
185
186        let cx0 = cx1 * f64::cos(q1) - cy1 * f64::sin(q1);
187        let cy0 = cx1 * f64::sin(q1) + cy1 * f64::cos(q1);
188        let cz0 = cz1 + p.c1;
189
190        // Precompute sines and cosines for efficiency
191        let (s1, c1) = q1.sin_cos();
192        let (s2, c2) = q2.sin_cos();
193        let (s3, c3) = q3.sin_cos();
194        let (s4, c4) = q4.sin_cos();
195        let (s5, c5) = q5.sin_cos();
196        let (s6, c6) = q6.sin_cos();
197
198        // Compute rotation matrix r_0c
199        let r_0c = Matrix3::new(
200            c1 * c2 * c3 - c1 * s2 * s3, -s1, c1 * c2 * s3 + c1 * s2 * c3,
201            s1 * c2 * c3 - s1 * s2 * s3, c1, s1 * c2 * s3 + s1 * s2 * c3,
202            -s2 * c3 - c2 * s3, 0.0, -s2 * s3 + c2 * c3,
203        );
204
205        // Compute rotation matrix r_ce
206        let r_ce = Matrix3::new(
207            c4 * c5 * c6 - s4 * s6, -c4 * c5 * s6 - s4 * c6, c4 * s5,
208            s4 * c5 * c6 + c4 * s6, -s4 * c5 * s6 + c4 * c6, s4 * s5,
209            -s5 * c6, s5 * s6, c5,
210        );
211
212        // Compute the final rotation matrix r_oe
213        let r_oe = r_0c * r_ce;
214
215        // Calculate the final translation
216        let translation = Vector3::new(cx0, cy0, cz0) + p.c4 * r_oe * *self.unit_z;
217
218        // Convert the rotation matrix to a quaternion
219        let rotation = Rotation3::from_matrix_unchecked(r_oe);
220
221        // Return the pose combining translation and rotation
222        Pose::from_parts(
223            Translation3::from(translation),
224            UnitQuaternion::from_rotation_matrix(&rotation),
225        )
226    }
227
228    fn forward_with_joint_poses(&self, joints: &Joints) -> [Pose; 6] {
229        let p = &self.parameters;
230
231        let q1 = joints[0] * p.sign_corrections[0] as f64 - p.offsets[0];
232        let q2 = joints[1] * p.sign_corrections[1] as f64 - p.offsets[1];
233        let q3 = joints[2] * p.sign_corrections[2] as f64 - p.offsets[2];
234        let q4 = joints[3] * p.sign_corrections[3] as f64 - p.offsets[3];
235        let q5 = joints[4] * p.sign_corrections[4] as f64 - p.offsets[4];
236        let q6 = joints[5] * p.sign_corrections[5] as f64 - p.offsets[5];
237
238        // Pose 1 is lifted by c1 as per URDF concepts (there is the base link that sits at 0,0,0)
239        let pose1 = Isometry3::from_parts(
240            Translation3::new(0.0, 0.0, p.c1),
241            UnitQuaternion::from_axis_angle(&Vector3::z_axis(), q1),
242        );
243
244        // Pose 2: The c2 - spanning arm is by c1 up, by a1 along x, and rotated around z by 
245        let pose2 = pose1 * Isometry3::from_parts(
246            Translation3::new(p.a1, p.b, 0.0),
247            UnitQuaternion::from_axis_angle(&nalgebra::Vector3::y_axis(), q2),
248        );
249
250        // Pose 3: The c3 - spanning arm goes starts further away by the length of c2. 
251        let pose3 = pose2 * Isometry3::from_parts(
252            Translation3::new(0.0, 0.0, p.c2),
253            UnitQuaternion::from_axis_angle(&nalgebra::Vector3::y_axis(), q3),
254        );
255
256        // Pose 4: this part uses pose3 as a base and just rotates around z.
257        let pose4 = pose3 * Isometry3::from_parts(
258            Translation3::new(p.a2, 0.0, 0.0),
259            UnitQuaternion::from_axis_angle(&nalgebra::Vector3::z_axis(), q4),
260        );
261         
262        // Pose 5 is the movable "nose" close to the tool center point.
263        let pose5 = pose4 * Isometry3::from_parts(
264            Translation3::new(0.0, 0.0, p.c3),
265            UnitQuaternion::from_axis_angle(&nalgebra::Vector3::y_axis(), q5),
266        );
267
268        // Pose 6 is pose of the tool-accepting joint that is often round and the
269        // rotation not visible in rendering without tool
270        let pose6 = pose5 * Isometry3::from_parts(
271            Translation3::new(0.0, 0.0, p.c4),
272            UnitQuaternion::from_axis_angle(&nalgebra::Vector3::z_axis(), q6),
273        );
274
275        [pose1, pose2, pose3, pose4, pose5, pose6]
276    }
277
278
279    fn inverse_5dof(&self, pose: &Pose, j6: f64) -> Solutions {
280        self.filter_constraints_compliant(self.inverse_intern_5_dof(&pose, j6))
281    }
282
283    fn inverse_continuing_5dof(&self, pose: &Pose, prev: &Joints) -> Solutions {
284        let previous;
285        if prev[0].is_nan() {
286            // Special value CONSTRAINT_CENTERED has been used
287            previous = self.constraint_centers();
288        } else {
289            previous = prev;
290        }
291
292        let mut solutions = self.inverse_intern_5_dof(pose, prev[5]);
293
294        // Before any sorting, normalize all angles to be close to
295        // 'previous'
296        for s_idx in 0..solutions.len() {
297            for joint_idx in 0..6 {
298                normalize_near(&mut solutions[s_idx][joint_idx], previous[joint_idx]);
299            }
300        }
301        self.sort_by_closeness(&mut solutions, &previous);
302        self.filter_constraints_compliant(solutions)
303    }
304
305    fn kinematic_singularity(&self, joints: &Joints) -> Option<Singularity> {
306        if is_close_to_multiple_of_pi(joints[J5], SINGULARITY_ANGLE_THR) {
307            Some(Singularity::A)
308        } else {
309            None
310        }
311    }
312
313    fn constraints(&self) -> &Option<Constraints> {
314        &self.constraints
315    }
316}
317
318impl OPWKinematics {
319    fn inverse_intern(&self, pose: &Pose) -> Solutions {
320        let params = &self.parameters;
321
322        // Adjust to wrist center
323        let matrix = pose.rotation.to_rotation_matrix();
324        let translation_vector = &pose.translation.vector; // Get the translation vector component
325        let scaled_z_axis = params.c4 * matrix.transform_vector(&Vector3::z_axis()); // Scale and rotate the z-axis vector
326
327        let c = translation_vector - scaled_z_axis;
328
329        let nx1 = ((c.x * c.x + c.y * c.y) - params.b * params.b).sqrt() - params.a1;
330
331        let tmp1 = c.y.atan2(c.x); // Rust's method call syntax for atan2(y, x)
332        let tmp2 = params.b.atan2(nx1 + params.a1);
333
334        let theta1_i = tmp1 - tmp2;
335        let theta1_ii = tmp1 + tmp2 - PI;
336
337        let tmp3 = c.z - params.c1; // Access z directly for nalgebra's Vector3
338        let s1_2 = nx1 * nx1 + tmp3 * tmp3;
339
340        let tmp4 = nx1 + 2.0 * params.a1;
341        let s2_2 = tmp4 * tmp4 + tmp3 * tmp3;
342        let kappa_2 = params.a2 * params.a2 + params.c3 * params.c3;
343
344        let c2_2 = params.c2 * params.c2;
345
346        let tmp5 = s1_2 + c2_2 - kappa_2;
347
348        let s1 = f64::sqrt(s1_2);
349        let s2 = f64::sqrt(s2_2);
350
351        let tmp13 = f64::acos(tmp5 / (2.0 * s1 * params.c2));
352        let tmp14 = f64::atan2(nx1, c.z - params.c1);
353        let theta2_i = -tmp13 + tmp14;
354        let theta2_ii = tmp13 + tmp14;
355
356        let tmp6 = s2_2 + c2_2 - kappa_2;
357
358        let tmp15 = f64::acos(tmp6 / (2.0 * s2 * params.c2));
359        let tmp16 = f64::atan2(nx1 + 2.0 * params.a1, c.z - params.c1);
360        let theta2_iii = -tmp15 - tmp16;
361        let theta2_iv = tmp15 - tmp16;
362
363        // theta3
364        let tmp7 = s1_2 - c2_2 - kappa_2;
365        let tmp8 = s2_2 - c2_2 - kappa_2;
366        let tmp9 = 2.0 * params.c2 * f64::sqrt(kappa_2);
367        let tmp10 = f64::atan2(params.a2, params.c3);
368
369        let tmp11 = f64::acos(tmp7 / tmp9);
370        let theta3_i = tmp11 - tmp10;
371        let theta3_ii = -tmp11 - tmp10;
372
373        let tmp12 = f64::acos(tmp8 / tmp9);
374        let theta3_iii = tmp12 - tmp10;
375        let theta3_iv = -tmp12 - tmp10;
376
377        let (theta1_i_sin, theta1_i_cos) = theta1_i.sin_cos();
378        let (theta1_ii_sin, theta1_ii_cos) = theta1_ii.sin_cos();
379
380        // orientation part
381        let sin1: [f64; 4] = [
382            theta1_i_sin, theta1_i_sin, theta1_ii_sin, theta1_ii_sin,
383        ];
384
385        let cos1: [f64; 4] = [
386            theta1_i_cos, theta1_i_cos, theta1_ii_cos, theta1_ii_cos
387        ];
388
389        let s23: [f64; 4] = [
390            (theta2_i + theta3_i).sin(),
391            (theta2_ii + theta3_ii).sin(),
392            (theta2_iii + theta3_iii).sin(),
393            (theta2_iv + theta3_iv).sin(),
394        ];
395
396        let c23: [f64; 4] = [
397            (theta2_i + theta3_i).cos(),
398            (theta2_ii + theta3_ii).cos(),
399            (theta2_iii + theta3_iii).cos(),
400            (theta2_iv + theta3_iv).cos(),
401        ];
402
403        let m: [f64; 4] = [
404            matrix[(0, 2)] * s23[0] * cos1[0] + matrix[(1, 2)] * s23[0] * sin1[0] + matrix[(2, 2)] * c23[0],
405            matrix[(0, 2)] * s23[1] * cos1[1] + matrix[(1, 2)] * s23[1] * sin1[1] + matrix[(2, 2)] * c23[1],
406            matrix[(0, 2)] * s23[2] * cos1[2] + matrix[(1, 2)] * s23[2] * sin1[2] + matrix[(2, 2)] * c23[2],
407            matrix[(0, 2)] * s23[3] * cos1[3] + matrix[(1, 2)] * s23[3] * sin1[3] + matrix[(2, 2)] * c23[3],
408        ];
409
410        let theta5_i = f64::atan2((1.0 - m[0] * m[0]).sqrt(), m[0]);
411        let theta5_ii = f64::atan2((1.0 - m[1] * m[1]).sqrt(), m[1]);
412        let theta5_iii = f64::atan2((1.0 - m[2] * m[2]).sqrt(), m[2]);
413        let theta5_iv = f64::atan2((1.0 - m[3] * m[3]).sqrt(), m[3]);
414
415        let theta5_v = -theta5_i;
416        let theta5_vi = -theta5_ii;
417        let theta5_vii = -theta5_iii;
418        let theta5_viii = -theta5_iv;
419
420        let theta4_i;
421        let theta6_i;
422
423        let theta4_iy = matrix[(1, 2)] * cos1[0] - matrix[(0, 2)] * sin1[0];
424        let theta4_ix = matrix[(0, 2)] * c23[0] * cos1[0] + matrix[(1, 2)] * c23[0] * sin1[0] - matrix[(2, 2)] * s23[0];
425        theta4_i = theta4_iy.atan2(theta4_ix);
426
427        let theta6_iy = matrix[(0, 1)] * s23[0] * cos1[0] + matrix[(1, 1)] * s23[0] * sin1[0] + matrix[(2, 1)] * c23[0];
428        let theta6_ix = -matrix[(0, 0)] * s23[0] * cos1[0] - matrix[(1, 0)] * s23[0] * sin1[0] - matrix[(2, 0)] * c23[0];
429        theta6_i = theta6_iy.atan2(theta6_ix);
430
431        let theta4_ii;
432        let theta6_ii;
433
434        let theta4_iiy = matrix[(1, 2)] * cos1[1] - matrix[(0, 2)] * sin1[1];
435        let theta4_iix = matrix[(0, 2)] * c23[1] * cos1[1] + matrix[(1, 2)] * c23[1] * sin1[1] - matrix[(2, 2)] * s23[1];
436        theta4_ii = theta4_iiy.atan2(theta4_iix);
437
438        let theta6_iiy = matrix[(0, 1)] * s23[1] * cos1[1] + matrix[(1, 1)] * s23[1] * sin1[1] + matrix[(2, 1)] * c23[1];
439        let theta6_iix = -matrix[(0, 0)] * s23[1] * cos1[1] - matrix[(1, 0)] * s23[1] * sin1[1] - matrix[(2, 0)] * c23[1];
440        theta6_ii = theta6_iiy.atan2(theta6_iix);
441
442        let theta4_iii;
443        let theta6_iii;
444
445        let theta4_iiiy = matrix[(1, 2)] * cos1[2] - matrix[(0, 2)] * sin1[2];
446        let theta4_iiix = matrix[(0, 2)] * c23[2] * cos1[2] + matrix[(1, 2)] * c23[2] * sin1[2] - matrix[(2, 2)] * s23[2];
447        theta4_iii = theta4_iiiy.atan2(theta4_iiix);
448
449        let theta6_iiiy = matrix[(0, 1)] * s23[2] * cos1[2] + matrix[(1, 1)] * s23[2] * sin1[2] + matrix[(2, 1)] * c23[2];
450        let theta6_iiix = -matrix[(0, 0)] * s23[2] * cos1[2] - matrix[(1, 0)] * s23[2] * sin1[2] - matrix[(2, 0)] * c23[2];
451        theta6_iii = theta6_iiiy.atan2(theta6_iiix);
452
453        let theta4_iv;
454        let theta6_iv;
455
456        let theta4_ivy = matrix[(1, 2)] * cos1[3] - matrix[(0, 2)] * sin1[3];
457        let theta4_ivx = matrix[(0, 2)] * c23[3] * cos1[3] + matrix[(1, 2)] * c23[3] * sin1[3] - matrix[(2, 2)] * s23[3];
458        theta4_iv = theta4_ivy.atan2(theta4_ivx);
459
460        let theta6_ivy = matrix[(0, 1)] * s23[3] * cos1[3] + matrix[(1, 1)] * s23[3] * sin1[3] + matrix[(2, 1)] * c23[3];
461        let theta6_ivx = -matrix[(0, 0)] * s23[3] * cos1[3] - matrix[(1, 0)] * s23[3] * sin1[3] - matrix[(2, 0)] * c23[3];
462        theta6_iv = theta6_ivy.atan2(theta6_ivx);
463
464        let theta4_v = theta4_i + PI;
465        let theta4_vi = theta4_ii + PI;
466        let theta4_vii = theta4_iii + PI;
467        let theta4_viii = theta4_iv + PI;
468
469        let theta6_v = theta6_i - PI;
470        let theta6_vi = theta6_ii - PI;
471        let theta6_vii = theta6_iii - PI;
472        let theta6_viii = theta6_iv - PI;
473
474        let theta: [[f64; 6]; 8] = [
475            [theta1_i, theta2_i, theta3_i, theta4_i, theta5_i, theta6_i],
476            [theta1_i, theta2_ii, theta3_ii, theta4_ii, theta5_ii, theta6_ii],
477            [theta1_ii, theta2_iii, theta3_iii, theta4_iii, theta5_iii, theta6_iii],
478            [theta1_ii, theta2_iv, theta3_iv, theta4_iv, theta5_iv, theta6_iv],
479            [theta1_i, theta2_i, theta3_i, theta4_v, theta5_v, theta6_v],
480            [theta1_i, theta2_ii, theta3_ii, theta4_vi, theta5_vi, theta6_vi],
481            [theta1_ii, theta2_iii, theta3_iii, theta4_vii, theta5_vii, theta6_vii],
482            [theta1_ii, theta2_iv, theta3_iv, theta4_viii, theta5_viii, theta6_viii],
483        ];
484
485        let mut sols: [[f64; 6]; 8] = [[f64::NAN; 6]; 8];
486        for si in 0..sols.len() {
487            for ji in 0..6 {
488                sols[si][ji] = (theta[si][ji] + params.offsets[ji]) *
489                    params.sign_corrections[ji] as f64;
490            }
491        }
492
493        let mut result: Solutions = Vec::with_capacity(8);
494
495        // Debug check. Solution failing cross-verification is flagged
496        // as invalid. This loop also normalizes valid solutions to 0
497        for si in 0..sols.len() {
498            let mut valid = true;
499            for ji in 0..6 {
500                let mut angle = sols[si][ji];
501                if angle.is_finite() {
502                    while angle > PI {
503                        angle -= 2.0 * PI;
504                    }
505                    while angle < -PI {
506                        angle += 2.0 * PI;
507                    }
508                    sols[si][ji] = angle;
509                } else {
510                    valid = false;
511                    break;
512                }
513            };
514            if valid {
515                let check_pose = self.forward(&sols[si]);
516                if compare_poses(&pose, &check_pose, DISTANCE_TOLERANCE, ANGULAR_TOLERANCE) {
517                    result.push(sols[si]);
518                } else {
519                    if DEBUG {
520                        println!("********** Pose Failure sol {} *********", si);
521                    }
522                }
523            }
524        }
525
526        result
527    }
528
529    fn inverse_intern_5_dof(&self, pose: &Pose, j6: f64) -> Solutions {
530        let params = &self.parameters;
531
532        // Adjust to wrist center
533        let matrix = pose.rotation.to_rotation_matrix();
534        let translation_vector = &pose.translation.vector; // Get the translation vector component
535        let scaled_z_axis = params.c4 * matrix.transform_vector(&Vector3::z_axis()); // Scale and rotate the z-axis vector
536
537        let c = translation_vector - scaled_z_axis;
538
539        let nx1 = ((c.x * c.x + c.y * c.y) - params.b * params.b).sqrt() - params.a1;
540
541        let tmp1 = c.y.atan2(c.x); // Rust's method call syntax for atan2(y, x)
542        let tmp2 = params.b.atan2(nx1 + params.a1);
543
544        let theta1_i = tmp1 - tmp2;
545        let theta1_ii = tmp1 + tmp2 - PI;
546
547        let tmp3 = c.z - params.c1; // Access z directly for nalgebra's Vector3
548        let s1_2 = nx1 * nx1 + tmp3 * tmp3;
549
550        let tmp4 = nx1 + 2.0 * params.a1;
551        let s2_2 = tmp4 * tmp4 + tmp3 * tmp3;
552        let kappa_2 = params.a2 * params.a2 + params.c3 * params.c3;
553
554        let c2_2 = params.c2 * params.c2;
555
556        let tmp5 = s1_2 + c2_2 - kappa_2;
557
558        let s1 = f64::sqrt(s1_2);
559        let s2 = f64::sqrt(s2_2);
560
561        let tmp13 = f64::acos(tmp5 / (2.0 * s1 * params.c2));
562        let tmp14 = f64::atan2(nx1, c.z - params.c1);
563        let theta2_i = -tmp13 + tmp14;
564        let theta2_ii = tmp13 + tmp14;
565
566        let tmp6 = s2_2 + c2_2 - kappa_2;
567
568        let tmp15 = f64::acos(tmp6 / (2.0 * s2 * params.c2));
569        let tmp16 = f64::atan2(nx1 + 2.0 * params.a1, c.z - params.c1);
570        let theta2_iii = -tmp15 - tmp16;
571        let theta2_iv = tmp15 - tmp16;
572
573        // theta3
574        let tmp7 = s1_2 - c2_2 - kappa_2;
575        let tmp8 = s2_2 - c2_2 - kappa_2;
576        let tmp9 = 2.0 * params.c2 * f64::sqrt(kappa_2);
577        let tmp10 = f64::atan2(params.a2, params.c3);
578
579        let tmp11 = f64::acos(tmp7 / tmp9);
580        let theta3_i = tmp11 - tmp10;
581        let theta3_ii = -tmp11 - tmp10;
582
583        let tmp12 = f64::acos(tmp8 / tmp9);
584        let theta3_iii = tmp12 - tmp10;
585        let theta3_iv = -tmp12 - tmp10;
586
587        let (theta1_i_sin, theta1_i_cos) = theta1_i.sin_cos();
588        let (theta1_ii_sin, theta1_ii_cos) = theta1_ii.sin_cos();
589
590        // orientation part
591        let sin1: [f64; 4] = [
592            theta1_i_sin, theta1_i_sin, theta1_ii_sin, theta1_ii_sin,
593        ];
594
595        let cos1: [f64; 4] = [
596            theta1_i_cos, theta1_i_cos, theta1_ii_cos, theta1_ii_cos
597        ];
598
599        let s23: [f64; 4] = [
600            (theta2_i + theta3_i).sin(),
601            (theta2_ii + theta3_ii).sin(),
602            (theta2_iii + theta3_iii).sin(),
603            (theta2_iv + theta3_iv).sin(),
604        ];
605
606        let c23: [f64; 4] = [
607            (theta2_i + theta3_i).cos(),
608            (theta2_ii + theta3_ii).cos(),
609            (theta2_iii + theta3_iii).cos(),
610            (theta2_iv + theta3_iv).cos(),
611        ];
612
613        let m: [f64; 4] = [
614            matrix[(0, 2)] * s23[0] * cos1[0] + matrix[(1, 2)] * s23[0] * sin1[0] + matrix[(2, 2)] * c23[0],
615            matrix[(0, 2)] * s23[1] * cos1[1] + matrix[(1, 2)] * s23[1] * sin1[1] + matrix[(2, 2)] * c23[1],
616            matrix[(0, 2)] * s23[2] * cos1[2] + matrix[(1, 2)] * s23[2] * sin1[2] + matrix[(2, 2)] * c23[2],
617            matrix[(0, 2)] * s23[3] * cos1[3] + matrix[(1, 2)] * s23[3] * sin1[3] + matrix[(2, 2)] * c23[3],
618        ];
619
620        let theta5_i = f64::atan2((1.0 - m[0] * m[0]).sqrt(), m[0]);
621        let theta5_ii = f64::atan2((1.0 - m[1] * m[1]).sqrt(), m[1]);
622        let theta5_iii = f64::atan2((1.0 - m[2] * m[2]).sqrt(), m[2]);
623        let theta5_iv = f64::atan2((1.0 - m[3] * m[3]).sqrt(), m[3]);
624
625        let theta5_v = -theta5_i;
626        let theta5_vi = -theta5_ii;
627        let theta5_vii = -theta5_iii;
628        let theta5_viii = -theta5_iv;
629
630        let theta4_i;
631
632        let theta4_iy = matrix[(1, 2)] * cos1[0] - matrix[(0, 2)] * sin1[0];
633        let theta4_ix = matrix[(0, 2)] * c23[0] * cos1[0] + matrix[(1, 2)] * c23[0] * sin1[0] - matrix[(2, 2)] * s23[0];
634        theta4_i = theta4_iy.atan2(theta4_ix);
635
636        let theta4_ii;
637
638        let theta4_iiy = matrix[(1, 2)] * cos1[1] - matrix[(0, 2)] * sin1[1];
639        let theta4_iix = matrix[(0, 2)] * c23[1] * cos1[1] + matrix[(1, 2)] * c23[1] * sin1[1] - matrix[(2, 2)] * s23[1];
640        theta4_ii = theta4_iiy.atan2(theta4_iix);
641
642        let theta4_iii;
643
644        let theta4_iiiy = matrix[(1, 2)] * cos1[2] - matrix[(0, 2)] * sin1[2];
645        let theta4_iiix = matrix[(0, 2)] * c23[2] * cos1[2] + matrix[(1, 2)] * c23[2] * sin1[2] - matrix[(2, 2)] * s23[2];
646        theta4_iii = theta4_iiiy.atan2(theta4_iiix);
647
648        let theta4_iv;
649
650        let theta4_ivy = matrix[(1, 2)] * cos1[3] - matrix[(0, 2)] * sin1[3];
651        let theta4_ivx = matrix[(0, 2)] * c23[3] * cos1[3] + matrix[(1, 2)] * c23[3] * sin1[3] - matrix[(2, 2)] * s23[3];
652        theta4_iv = theta4_ivy.atan2(theta4_ivx);
653
654        let theta4_v = theta4_i + PI;
655        let theta4_vi = theta4_ii + PI;
656        let theta4_vii = theta4_iii + PI;
657        let theta4_viii = theta4_iv + PI;
658
659        let theta: [[f64; 5]; 8] = [
660            [theta1_i, theta2_i, theta3_i, theta4_i, theta5_i],
661            [theta1_i, theta2_ii, theta3_ii, theta4_ii, theta5_ii],
662            [theta1_ii, theta2_iii, theta3_iii, theta4_iii, theta5_iii],
663            [theta1_ii, theta2_iv, theta3_iv, theta4_iv, theta5_iv],
664            [theta1_i, theta2_i, theta3_i, theta4_v, theta5_v],
665            [theta1_i, theta2_ii, theta3_ii, theta4_vi, theta5_vi],
666            [theta1_ii, theta2_iii, theta3_iii, theta4_vii, theta5_vii],
667            [theta1_ii, theta2_iv, theta3_iv, theta4_viii, theta5_viii],
668        ];
669
670        let mut sols: [[f64; 6]; 8] = [[f64::NAN; 6]; 8];
671        for si in 0..sols.len() {
672            for ji in 0..5 {
673                sols[si][ji] = (theta[si][ji] + params.offsets[ji]) *
674                    params.sign_corrections[ji] as f64;
675            }
676            sols[si][5] = j6 // J6 goes directly to response and is not more adjusted
677        }
678
679        let mut result: Solutions = Vec::with_capacity(8);
680
681        // Debug check. Solution failing cross-verification is flagged
682        // as invalid. This loop also normalizes valid solutions to 0
683        for si in 0..sols.len() {
684            let mut valid = true;
685            for ji in 0..5 { // J6 is not processed in this loop
686                let mut angle = sols[si][ji];
687                if angle.is_finite() {
688                    while angle > PI {
689                        angle -= 2.0 * PI;
690                    }
691                    while angle < -PI {
692                        angle += 2.0 * PI;
693                    }
694                    sols[si][ji] = angle;
695                } else {
696                    valid = false;
697                    break;
698                }
699            };
700            if valid {
701                let check_xyz = self.forward(&sols[si]).translation;
702                if Self::compare_xyz_only(&pose.translation, &check_xyz, DISTANCE_TOLERANCE) {
703                    result.push(sols[si]);
704                } else {
705                    if DEBUG {
706                        println!("********** Pose Failure 5DOF sol {} *********", si);
707                    }
708                }
709            }
710        }
711
712        result
713    }
714
715    fn compare_xyz_only(pose_translation: &Translation3<f64>, check_xyz: &Translation3<f64>, tolerance: f64) -> bool {
716        (pose_translation.vector - check_xyz.vector).norm() <= tolerance
717    }
718
719    fn filter_constraints_compliant(&self, solutions: Solutions) -> Solutions {
720        match &self.constraints {
721            Some(constraints) => constraints.filter(&solutions),
722            None => solutions
723        }
724    }
725
726    fn constraints_compliant(&self, solution: Joints) -> bool {
727        match &self.constraints {
728            Some(constraints) => constraints.compliant(&solution),
729            None => true
730        }
731    }
732
733    /// Sorts the solutions vector by closeness to the `previous` joint.
734    /// Joints must be pre-normalized to be as close as possible, not away by 360 degrees
735    fn sort_by_closeness(&self, solutions: &mut Solutions, previous: &Joints) {
736        let sorting_weight = self.constraints.as_ref()
737            .map_or(BY_PREV, |c| c.sorting_weight);
738        if sorting_weight == BY_PREV {
739            // If no constraints or they weight is zero, use simpler version
740            solutions.sort_by(|a, b| {
741                let distance_a = calculate_distance(a, previous);
742                let distance_b = calculate_distance(b, previous);
743                distance_a.partial_cmp(&distance_b).unwrap_or(std::cmp::Ordering::Equal)
744            });
745        } else {
746            let constraints = self.constraints.as_ref().unwrap();
747            solutions.sort_by(|a, b| {
748                let prev_a;
749                let prev_b;
750                if sorting_weight != BY_CONSTRAINS {
751                    prev_a = calculate_distance(a, previous);
752                    prev_b = calculate_distance(b, previous);
753                } else {
754                    // Do not calculate unneeded distances if these values are to be ignored.
755                    prev_a = 0.0;
756                    prev_b = 0.0;
757                }
758
759                let constr_a = calculate_distance(a, &constraints.centers);
760                let constr_b = calculate_distance(b, &constraints.centers);
761
762                let distance_a = prev_a * (1.0 - sorting_weight) + constr_a * sorting_weight;
763                let distance_b = prev_b * (1.0 - sorting_weight) + constr_b * sorting_weight;
764                distance_a.partial_cmp(&distance_b).unwrap_or(std::cmp::Ordering::Equal)
765            });
766        }
767    }
768
769    /// Get constraint centers in case we have the already constructed instance of the
770    fn constraint_centers(&self) -> &Joints {
771        self.constraints.as_ref()
772            .map_or(&JOINTS_AT_ZERO, |c| &c.centers)
773    }
774}
775
776// Adjusted helper function to check for n*pi where n is any integer
777fn is_close_to_multiple_of_pi(joint_value: f64, threshold: f64) -> bool {
778
779    // Normalize angle within [0, 2*PI)
780    let normalized_angle = joint_value.rem_euclid(2.0 * PI);
781    // Check if the normalized angle is close to 0 or PI
782    normalized_angle < threshold ||
783        (PI - normalized_angle).abs() < threshold
784}
785
786fn are_angles_close(angle1: f64, angle2: f64) -> bool {
787    let mut diff = (angle1 - angle2).abs();
788    diff = diff % (2.0 * PI);
789    while diff > PI {
790        diff = (2.0 * PI) - diff;
791    }
792    diff < SINGULARITY_ANGLE_THR
793}
794
795/// Normalizes the angle `now` to be as close as possible to `must_be_near`
796///
797/// # Arguments
798///
799/// * `now` - A mutable reference to the angle to be normalized, radians
800/// * `must_be_near` - The reference angle, radians
801fn normalize_near(now: &mut f64, must_be_near: f64) {
802    let two_pi = 2.0 * PI;
803    // Smallest signed difference in (-π, π]
804    let diff = (*now - must_be_near + PI).rem_euclid(two_pi) - PI;
805    *now = must_be_near + diff;
806}
807
808fn calculate_distance(joint1: &Joints, joint2: &Joints) -> f64 {
809    joint1.iter()
810        .zip(joint2.iter())
811        .map(|(a, b)| (a - b).abs())
812        .sum()
813}
814
815// Compare two poses with the given tolerance.
816fn compare_poses(ta: &Isometry3<f64>, tb: &Isometry3<f64>,
817                 distance_tolerance: f64, angular_tolerance: f64) -> bool {
818    let translation_distance = (ta.translation.vector - tb.translation.vector).norm();
819    let angular_distance = ta.rotation.angle_to(&tb.rotation);
820
821    if translation_distance.abs() > distance_tolerance {
822        if DEBUG {
823            println!("Positioning error: {}", translation_distance);
824        }
825        return false;
826    }
827
828    if angular_distance.abs() > angular_tolerance {
829        if DEBUG {
830            println!("Orientation errors: {}", angular_distance);
831        }
832        return false;
833    }
834    true
835}
836
837#[allow(dead_code)]
838fn dump_shifted_solutions(d: [f64; 3], ik: &Solutions) {
839    println!("Shifted solutions {} {} {}", d[0], d[1], d[2]);
840    for sol_idx in 0..ik.len() {
841        let mut row_str = String::new();
842        for joint_idx in 0..6 {
843            let computed = ik[sol_idx][joint_idx];
844            row_str.push_str(&format!("{:5.2} ", computed.to_degrees()));
845        }
846        println!("[{}]", row_str.trim_end()); // Trim trailing space for aesthetics
847    }
848}
849
850#[cfg(test)]
851mod tests {
852    use super::*;
853    use crate::kinematic_traits::{Joints, Kinematics};
854    use crate::kinematics_impl::OPWKinematics;
855    use crate::parameters::opw_kinematics::Parameters;
856
857    #[test]
858    fn test_inverse_continuing_large_j6_angles() {
859        let robot = OPWKinematics::new(Parameters::irb2400_10());
860
861        let angles_deg:[f64; 10] = [-90000.0, -9000.0, -900.0, -90.0, -9.0, 9.0, 90.0, 900.0, 9000.0, 90000.0];
862
863        for &angle_deg in &angles_deg {
864            let j6_rad = angle_deg.to_radians();
865
866            let pose = robot.forward(&[0.0, 0.1, 0.2, 0.3, 0.1, j6_rad]);
867
868            let previous: Joints = [0.0, 0.1, 0.2, 0.3, 0.1, j6_rad];
869            let solutions = robot.inverse_continuing(&pose, &previous);
870
871            assert!(!solutions.is_empty(), "No solutions found for angle {} degrees", angle_deg);
872
873            let solution_j6 = solutions[0][J6];
874
875            // Normalize near previous angle
876            let mut normalized_solution_j6 = solution_j6;
877            normalize_near(&mut normalized_solution_j6, previous[J6]);
878
879            let diff = (normalized_solution_j6 - previous[J6]).abs();
880
881            // Allow small epsilon due to floating-point errors
882            assert!(diff < 1e-6, "J6 mismatch for angle {} degrees: difference was {} radians", angle_deg, diff);
883        }
884    }
885
886    #[test]
887    fn test_inverse_continuing_adds_blended_solution_at_j5_pi() {
888        use std::f64::consts::PI;
889        use crate::kinematic_traits::{Joints, Kinematics, J4, J5, J6};
890
891        // Use a known-good robot model; adjust if you prefer a different preset.
892        let robot = OPWKinematics::new(Parameters::irb2400_10());
893
894        // Previous configuration with the wrist at the π singularity.
895        // For J5 ≈ π, the *orientation* depends on (J4 - J6),
896        // and the continuity-preserving update is δ4 = -δ6.
897        let previous: Joints = [0.0, 0.1, 0.2, 0.3, PI, -0.8];
898
899        // Create a target pose by moving J4 and J6 in OPPOSITE directions by ±Δ
900        // while keeping J5 at π. This changes (J4 - J6) by 2Δ.
901        let delta = 0.20_f64;
902        let target: Joints = [
903            previous[0],
904            previous[1],
905            previous[2],
906            previous[J4] + delta,
907            previous[J5],             // keep J5 at π
908            previous[J6] - delta,
909        ];
910
911        // Pose generated from the "target" configuration
912        let pose = robot.forward(&target);
913
914        // Baseline: plain IK solutions (no continuity logic)
915        let base = robot.inverse(&pose);
916        assert!(
917            !base.is_empty(),
918            "baseline IK returned no solutions for the target pose"
919        );
920
921        // Continuation: should add exactly one 'blended' solution near `previous`
922        // when J5 ≈ π (after the bug fix).
923        let cont = robot.inverse_continuing(&pose, &previous);
924        assert!(
925            !cont.is_empty(),
926            "inverse_continuing returned no solutions"
927        );
928        assert_eq!(
929            cont.len(),
930            base.len() + 1,
931            "expected one additional blended continuity solution at the J5≈π singularity"
932        );
933
934        // The top solution is sorted by closeness to `previous`; it should be the blended one.
935        let mut best = cont[0];
936        for j in 0..6 {
937            normalize_near(&mut best[j], previous[j]);
938        }
939
940        // Opposite-direction motion relative to previous: δ4 + δ6 ≈ 0
941        let d4 = best[J4] - previous[J4];
942        let d6 = best[J6] - previous[J6];
943        assert!(
944            (d4 + d6).abs() < 1e-6,
945            "expected opposite-direction update at J5≈π, got δ4={} δ6={}",
946            d4,
947            d6
948        );
949
950        // And (J4 - J6) must match the pose-implied (target) value (mod 2π)
951        let mut best_diff = best[J4] - best[J6];
952        let target_diff = target[J4] - target[J6];
953        normalize_near(&mut best_diff, target_diff);
954        assert!(
955            (best_diff - target_diff).abs() < 1e-6,
956            "q4 - q6 mismatch: got {}, want {}",
957            best_diff,
958            target_diff
959        );
960    }
961
962}
963