Skip to main content

repose_kinematics/
ik.rs

1//! Allocation-free Damped Least-Squares (Levenberg-Marquardt) position/pose inverse kinematics
2//! and velocity/twist differential inverse kinematics with singularity and joint-limit damping.
3
4use crate::chain::CuKinematicChain;
5use crate::joint::CuJointPosition;
6use crate::joint::CuJointVelocity;
7use bincode::Decode;
8use bincode::Encode;
9use cu_spatial_payloads::Transform3D;
10use cu29::prelude::*;
11use cu29::units::si::angle::radian;
12use cu29::units::si::angular_velocity::radian_per_second;
13use cu29::units::si::f64::Angle as Angle64;
14use cu29::units::si::f64::AngularVelocity as AngularVelocity64;
15use cu29::units::si::f64::Length as Length64;
16use cu29::units::si::f64::Velocity as Velocity64;
17use cu29::units::si::length::meter;
18use cu29::units::si::velocity::meter_per_second;
19use serde::Deserialize;
20use serde::Serialize;
21
22/// Which inverse-kinematics solver [`CuIkParams`] selects.
23#[derive(
24    Default, Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, Reflect,
25)]
26#[serde(rename_all = "snake_case")]
27pub enum CuIkSolver {
28    /// Damped least squares: stable near singularities, uses `damping`.
29    #[default]
30    DampedLeastSquares,
31    /// Undamped pseudoinverse: exact steps away from singularities; the
32    /// solve errors instead of damping when the Jacobian is singular.
33    Pseudoinverse,
34}
35
36/// Parameters of the inverse kinematics solver selected by `solver`.
37///
38/// RON (every field optional, SI values):
39/// `{"solver": "damped_least_squares", "max_iterations": 64, "position_tolerance": 1e-4,
40///   "orientation_tolerance": 1e-3, "damping": 0.05, "max_step": 0.25,
41///   "max_linear_step": 0.025, "enforce_joint_limits": true}`
42#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect)]
43#[serde(default, deny_unknown_fields)]
44pub struct CuIkParams {
45    /// The solver algorithm.
46    pub solver: CuIkSolver,
47    /// Maximum solver iterations per call.
48    pub max_iterations: u32,
49    /// Position convergence threshold.
50    pub position_tolerance: Length64,
51    /// Orientation convergence threshold.
52    pub orientation_tolerance: Angle64,
53    /// Damping factor $\lambda > 0$ stabilizing near-singular configurations (dimensionless).
54    pub damping: f64,
55    /// Maximum per-iteration revolute joint step.
56    pub max_step: Angle64,
57    /// Maximum per-iteration prismatic displacement.
58    pub max_linear_step: Length64,
59    /// Whether to clamp joint coordinates to joint limits at each step.
60    pub enforce_joint_limits: bool,
61}
62
63impl Default for CuIkParams {
64    fn default() -> Self {
65        Self {
66            solver: CuIkSolver::default(),
67            max_iterations: 64,
68            position_tolerance: Length64::new::<meter>(1e-4),
69            orientation_tolerance: Angle64::new::<radian>(1e-3),
70            damping: 0.05,
71            max_step: Angle64::new::<radian>(0.25),
72            max_linear_step: Length64::new::<meter>(0.025),
73            enforce_joint_limits: true,
74        }
75    }
76}
77
78impl CuIkParams {
79    /// Rejects non-positive or non-finite damping, tolerances, and step sizes.
80    pub fn validate(&self) -> CuResult<()> {
81        if self.solver == CuIkSolver::DampedLeastSquares
82            && (self.damping <= 0.0 || !self.damping.is_finite())
83        {
84            return Err(CuError::from(
85                "repose_kinematics: IK damping must be positive and finite",
86            ));
87        }
88        let positive = |v: f64| v.is_finite() && v > 0.0;
89        if !positive(self.position_tolerance.get::<meter>())
90            || !positive(self.orientation_tolerance.get::<radian>())
91            || !positive(self.max_step.get::<radian>())
92            || !positive(self.max_linear_step.get::<meter>())
93        {
94            return Err(CuError::from(
95                "repose_kinematics: IK tolerances and max_step must be positive",
96            ));
97        }
98        Ok(())
99    }
100}
101
102/// Outcome of a bounded inverse kinematics solve.
103#[derive(
104    Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect,
105)]
106#[serde(rename_all = "snake_case")]
107pub enum CuIkStatus {
108    /// Target pose reached within position and orientation tolerances.
109    Converged {
110        iterations: u32,
111        position_error: Length64,
112        orientation_error: Angle64,
113    },
114    /// Iteration budget exhausted; `out_q` holds the best-so-far configuration.
115    MaxIterationsReached {
116        iterations: u32,
117        position_error: Length64,
118        orientation_error: Angle64,
119    },
120    /// Default uninitialized state prior to any solve.
121    #[default]
122    Uninitialized,
123}
124
125impl CuIkStatus {
126    /// Returns `true` if the solver converged within tolerances.
127    pub const fn is_converged(&self) -> bool {
128        matches!(self, Self::Converged { .. })
129    }
130}
131
132/// Coordinate frame in which a 6D task-space twist command is expressed.
133#[derive(
134    Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Encode, Decode, Reflect,
135)]
136#[serde(rename_all = "snake_case")]
137pub enum CuTwistFrame {
138    /// Twist expressed in the robot base frame (paired with `geometric_jacobian`).
139    #[default]
140    Base,
141    /// Twist expressed in the end-effector tool/body frame (paired with `body_jacobian`).
142    Body,
143}
144
145/// 6D spatial velocity (linear + angular twist) payload with explicit frame semantics.
146#[derive(
147    Default, Debug, Clone, Copy, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect,
148)]
149pub struct CuTwist6D {
150    /// Linear velocity of the tool origin.
151    pub linear: [Velocity64; 3],
152    /// Angular velocity.
153    pub angular: [AngularVelocity64; 3],
154    /// Frame both vectors are expressed in.
155    pub frame: CuTwistFrame,
156}
157
158impl CuTwist6D {
159    /// Creates a twist from linear `[vx, vy, vz]` and angular `[wx, wy, wz]` velocities
160    /// expressed in `frame`.
161    pub const fn new(
162        linear: [Velocity64; 3],
163        angular: [AngularVelocity64; 3],
164        frame: CuTwistFrame,
165    ) -> Self {
166        Self {
167            linear,
168            angular,
169            frame,
170        }
171    }
172
173    /// Extracts the raw 6-vector `[vx, vy, vz, wx, wy, wz]` in `m/s` and `rad/s`.
174    pub(crate) fn to_raw_array(self) -> [f64; 6] {
175        [
176            self.linear[0].get::<meter_per_second>(),
177            self.linear[1].get::<meter_per_second>(),
178            self.linear[2].get::<meter_per_second>(),
179            self.angular[0].get::<radian_per_second>(),
180            self.angular[1].get::<radian_per_second>(),
181            self.angular[2].get::<radian_per_second>(),
182        ]
183    }
184}
185
186/// Parameters of velocity/twist differential inverse kinematics (Jacobian servoing).
187///
188/// RON (every field optional, SI values):
189/// `{"base_damping": 0.02, "max_singularity_damping": 0.25, "singularity_threshold_sq": 1e-4,
190///   "limit_margin": 0.15, "linear_limit_margin": 0.015, "enforce_velocity_limits": true}`
191#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect)]
192#[serde(default, deny_unknown_fields)]
193pub struct CuDiffIkParams {
194    /// Baseline damping factor $\lambda_0 > 0$ used far from singularities (dimensionless).
195    pub base_damping: f64,
196    /// Maximum additional damping $\lambda_{\max} \ge 0$ added at exact singularity (dimensionless).
197    pub max_singularity_damping: f64,
198    /// Yoshikawa manipulability squared threshold $w_{\text{thresh}}^2$ below which adaptive
199    /// singularity damping ramps up smoothly (dimensionless; `0` disables it).
200    pub singularity_threshold_sq: f64,
201    /// Revolute joint-limit proximity margin within which motion toward a limit is smoothly damped.
202    pub limit_margin: Angle64,
203    /// Prismatic joint-limit damping distance.
204    pub linear_limit_margin: Length64,
205    /// Whether to uniformly scale output joint velocities when any joint exceeds `max_velocity`.
206    pub enforce_velocity_limits: bool,
207}
208
209impl Default for CuDiffIkParams {
210    fn default() -> Self {
211        Self {
212            base_damping: 0.02,
213            max_singularity_damping: 0.25,
214            singularity_threshold_sq: 1e-4,
215            limit_margin: Angle64::new::<radian>(0.15),
216            linear_limit_margin: Length64::new::<meter>(0.015),
217            enforce_velocity_limits: true,
218        }
219    }
220}
221
222impl CuDiffIkParams {
223    /// Rejects invalid damping parameters and negative or non-finite limit margins.
224    pub fn validate(&self) -> CuResult<()> {
225        if self.base_damping <= 0.0
226            || !self.base_damping.is_finite()
227            || self.max_singularity_damping < 0.0
228            || !self.max_singularity_damping.is_finite()
229            || self.singularity_threshold_sq < 0.0
230            || !self.singularity_threshold_sq.is_finite()
231        {
232            return Err(CuError::from(
233                "repose_kinematics: differential IK damping parameters must be valid finite numbers",
234            ));
235        }
236        if !self.limit_margin.get::<radian>().is_finite()
237            || self.limit_margin.get::<radian>() < 0.0
238            || !self.linear_limit_margin.get::<meter>().is_finite()
239            || self.linear_limit_margin.get::<meter>() < 0.0
240        {
241            return Err(CuError::from(
242                "Joint-limit damping margins must be finite and nonnegative",
243            ));
244        }
245        Ok(())
246    }
247}
248
249/// Status reported by a single differential IK velocity solve.
250#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect)]
251#[serde(rename_all = "snake_case")]
252pub enum CuDiffIkStatus {
253    /// Nominal tracking outside the singularity damping zone and within velocity limits.
254    Nominal { manipulability_sq: f64 },
255    /// Near-singular configuration triggered adaptive manipulability damping.
256    SingularityDamped {
257        manipulability_sq: f64,
258        effective_damping: f64,
259    },
260    /// Joint velocities were uniformly scaled in `(0.0, 1.0]` to respect joint velocity limits.
261    VelocityScaled { manipulability_sq: f64, scale: f64 },
262}
263
264impl Default for CuDiffIkStatus {
265    fn default() -> Self {
266        Self::Nominal {
267            manipulability_sq: 0.0,
268        }
269    }
270}
271
272impl<const N: usize> CuKinematicChain<N> {
273    /// Computes Yoshikawa's manipulability index squared $w^2 = \max(0, \det(J J^T))$ at `q`.
274    pub fn manipulability_squared(&self, q: &[CuJointPosition; N]) -> CuResult<f64> {
275        let mut jac = [[0.0_f64; N]; 6];
276        self.geometric_jacobian(q, &mut jac)?;
277        Ok(manipulability_sq_from_jacobian(&jac))
278    }
279
280    /// Computes joint velocities `out_qd` in place for a desired 6D Cartesian twist using
281    /// singularity-robust and joint-limit-aware damped least squares.
282    pub fn differential_ik(
283        &self,
284        q: &[CuJointPosition; N],
285        twist: &CuTwist6D,
286        cfg: &CuDiffIkParams,
287        out_qd: &mut [CuJointVelocity; N],
288    ) -> CuResult<CuDiffIkStatus> {
289        cfg.validate()?;
290        let v_raw = twist.to_raw_array();
291        let mut idx_v = 0;
292        while idx_v < 6 {
293            if !v_raw[idx_v].is_finite() {
294                return Err(CuError::from(
295                    "repose_kinematics: non-finite twist component in differential_ik",
296                ));
297            }
298            idx_v += 1;
299        }
300
301        let mut jac = [[0.0_f64; N]; 6];
302        match twist.frame {
303            CuTwistFrame::Base => self.geometric_jacobian(q, &mut jac)?,
304            CuTwistFrame::Body => self.body_jacobian(q, &mut jac)?,
305        }
306
307        let w_sq = manipulability_sq_from_jacobian(&jac);
308        let (lambda_sq, singularity_damped, effective_damping) =
309            if cfg.singularity_threshold_sq > 0.0 && w_sq < cfg.singularity_threshold_sq {
310                let ratio = 1.0 - (w_sq / cfg.singularity_threshold_sq);
311                let l_sq = cfg.base_damping * cfg.base_damping
312                    + cfg.max_singularity_damping * cfg.max_singularity_damping * ratio * ratio;
313                (l_sq, true, libm::sqrt(l_sq))
314            } else {
315                (cfg.base_damping * cfg.base_damping, false, cfg.base_damping)
316            };
317
318        // Build 6x6 damped normal matrix A = J * J^T + lambda_eff^2 * I
319        let mut a = [[0.0_f64; 6]; 6];
320        let mut r = 0;
321        while r < 6 {
322            let mut c = 0;
323            while c < 6 {
324                let mut sum = 0.0;
325                let mut k = 0;
326                while k < N {
327                    sum += jac[r][k] * jac[c][k];
328                    k += 1;
329                }
330                a[r][c] = sum;
331                c += 1;
332            }
333            a[r][r] += lambda_sq;
334            r += 1;
335        }
336
337        let y = solve_6x6(&mut a, &v_raw)?;
338
339        // Compute raw joint velocities qd = J^T * y and apply smooth joint-limit margin damping
340        // when moving toward a joint limit.
341
342        let joints = self.joints();
343        let mut raw_qd = [0.0_f64; N];
344        let mut velocity_scale = 1.0_f64;
345
346        let mut j = 0;
347        while j < N {
348            let mut qd_j = 0.0;
349            let mut row = 0;
350            while row < 6 {
351                qd_j += jac[row][j] * y[row];
352                row += 1;
353            }
354
355            let margin = match q[j] {
356                CuJointPosition::Prismatic(_) => cfg.linear_limit_margin.get::<meter>(),
357                _ => cfg.limit_margin.get::<radian>(),
358            };
359            if margin > 0.0
360                && !matches!(
361                    joints[j].limits,
362                    crate::joint::CuJointLimits::Continuous { .. }
363                )
364            {
365                let q_rad = q[j].si_value();
366                let lower = joints[j].limits.lower().si_value();
367                let upper = joints[j].limits.upper().si_value();
368                if qd_j < 0.0 {
369                    let dist_low = (q_rad - lower).max(0.0);
370                    if dist_low < margin {
371                        let s = dist_low / margin;
372                        qd_j *= s * s;
373                    }
374                } else if qd_j > 0.0 {
375                    let dist_high = (upper - q_rad).max(0.0);
376                    if dist_high < margin {
377                        let s = dist_high / margin;
378                        qd_j *= s * s;
379                    }
380                }
381            }
382
383            raw_qd[j] = qd_j;
384
385            if cfg.enforce_velocity_limits {
386                let max_v = joints[j].limits.max_velocity().si_value();
387                let abs_v = libm::fabs(qd_j);
388                if abs_v > max_v {
389                    let candidate_scale = max_v / abs_v;
390                    if candidate_scale < velocity_scale {
391                        velocity_scale = candidate_scale;
392                    }
393                }
394            }
395            j += 1;
396        }
397
398        let mut i = 0;
399        while i < N {
400            out_qd[i] = CuJointVelocity::from_coordinate(q[i], raw_qd[i] * velocity_scale);
401            i += 1;
402        }
403
404        if velocity_scale < 1.0 {
405            Ok(CuDiffIkStatus::VelocityScaled {
406                manipulability_sq: w_sq,
407                scale: velocity_scale,
408            })
409        } else if singularity_damped {
410            Ok(CuDiffIkStatus::SingularityDamped {
411                manipulability_sq: w_sq,
412                effective_damping,
413            })
414        } else {
415            Ok(CuDiffIkStatus::Nominal {
416                manipulability_sq: w_sq,
417            })
418        }
419    }
420
421    /// Solves inverse kinematics in place using Damped Least-Squares (DLS).
422    ///
423    /// Writes the resulting joint configuration into `out_q` without heap allocation.
424    pub fn inverse_kinematics_dls(
425        &self,
426        target: &Transform3D<f64>,
427        seed: &[CuJointPosition; N],
428        cfg: &CuIkParams,
429        out_q: &mut [CuJointPosition; N],
430    ) -> CuResult<CuIkStatus> {
431        cfg.validate()?;
432        let pos_tol = cfg.position_tolerance.get::<meter>();
433        let ori_tol = cfg.orientation_tolerance.get::<radian>();
434        let max_step = cfg.max_step.get::<radian>();
435
436        for (joint, &position) in self.joints().iter().zip(seed) {
437            joint.validate_coordinate(position)?;
438        }
439        if target.to_matrix().iter().flatten().any(|v| !v.is_finite()) {
440            return Err(CuError::from("Non-finite IK target"));
441        }
442        *out_q = *seed;
443        if cfg.enforce_joint_limits {
444            self.clamp_to_limits(out_q)?;
445        }
446
447        let mut current_pose = Transform3D::<f64>::identity();
448        let mut jac = [[0.0_f64; N]; 6];
449        let lambda_sq = match cfg.solver {
450            CuIkSolver::DampedLeastSquares => cfg.damping * cfg.damping,
451            CuIkSolver::Pseudoinverse => 0.0,
452        };
453
454        let mut iter = 0;
455        while iter < cfg.max_iterations {
456            self.forward_kinematics(out_q, &mut current_pose)?;
457            let (err_vec, pos_err, ori_err) = pose_error_world(&current_pose, target);
458
459            if pos_err <= pos_tol && ori_err <= ori_tol {
460                return Ok(CuIkStatus::Converged {
461                    iterations: iter,
462                    position_error: Length64::new::<meter>(pos_err),
463                    orientation_error: Angle64::new::<radian>(ori_err),
464                });
465            }
466
467            self.geometric_jacobian(out_q, &mut jac)?;
468
469            // Build 6x6 damped normal matrix A = J * J^T + lambda^2 * I
470            let mut a = [[0.0_f64; 6]; 6];
471            let mut r = 0;
472            while r < 6 {
473                let mut c = 0;
474                while c < 6 {
475                    let mut sum = 0.0;
476                    let mut k = 0;
477                    while k < N {
478                        sum += jac[r][k] * jac[c][k];
479                        k += 1;
480                    }
481                    a[r][c] = sum;
482                    c += 1;
483                }
484                a[r][r] += lambda_sq;
485                r += 1;
486            }
487
488            let y = solve_6x6(&mut a, &err_vec)?;
489
490            // Compute dq = J^T * y and scale if max_step is exceeded
491            let mut dq = [0.0_f64; N];
492            let mut max_abs_dq = 0.0_f64;
493            let mut j = 0;
494            while j < N {
495                let mut delta = 0.0;
496                let mut row = 0;
497                while row < 6 {
498                    delta += jac[row][j] * y[row];
499                    row += 1;
500                }
501                dq[j] = delta;
502                let joint_step = match out_q[j] {
503                    CuJointPosition::Prismatic(_) => cfg.max_linear_step.get::<meter>(),
504                    _ => max_step,
505                };
506                let abs_d = libm::fabs(delta) * max_step / joint_step;
507                if abs_d > max_abs_dq {
508                    max_abs_dq = abs_d;
509                }
510                j += 1;
511            }
512
513            let scale = if max_abs_dq > max_step {
514                max_step / max_abs_dq
515            } else {
516                1.0
517            };
518
519            let mut idx = 0;
520            while idx < N {
521                let next_rad = out_q[idx].si_value() + dq[idx] * scale;
522                out_q[idx] = out_q[idx].with_si_value(next_rad);
523                idx += 1;
524            }
525
526            if cfg.enforce_joint_limits {
527                self.clamp_to_limits(out_q)?;
528            }
529
530            iter += 1;
531        }
532
533        self.forward_kinematics(out_q, &mut current_pose)?;
534        let (_, pos_err, ori_err) = pose_error_world(&current_pose, target);
535        if pos_err <= pos_tol && ori_err <= ori_tol {
536            Ok(CuIkStatus::Converged {
537                iterations: cfg.max_iterations,
538                position_error: Length64::new::<meter>(pos_err),
539                orientation_error: Angle64::new::<radian>(ori_err),
540            })
541        } else {
542            Ok(CuIkStatus::MaxIterationsReached {
543                iterations: cfg.max_iterations,
544                position_error: Length64::new::<meter>(pos_err),
545                orientation_error: Angle64::new::<radian>(ori_err),
546            })
547        }
548    }
549}
550
551fn manipulability_sq_from_jacobian<const N: usize>(jac: &[[f64; N]; 6]) -> f64 {
552    if N < 6 {
553        return 0.0;
554    }
555    let mut jjt = [[0.0_f64; 6]; 6];
556    let mut r = 0;
557    while r < 6 {
558        let mut c = 0;
559        while c < 6 {
560            let mut sum = 0.0;
561            let mut k = 0;
562            while k < N {
563                sum += jac[r][k] * jac[c][k];
564                k += 1;
565            }
566            jjt[r][c] = sum;
567            c += 1;
568        }
569        r += 1;
570    }
571    det_6x6(&mut jjt).max(0.0)
572}
573
574fn det_6x6(a: &mut [[f64; 6]; 6]) -> f64 {
575    let mut sign = 1.0_f64;
576    let mut k = 0;
577    while k < 6 {
578        let mut pivot_row = k;
579        let mut max_val = libm::fabs(a[k][k]);
580        let mut i = k + 1;
581        while i < 6 {
582            let v = libm::fabs(a[i][k]);
583            if v > max_val {
584                max_val = v;
585                pivot_row = i;
586            }
587            i += 1;
588        }
589
590        if max_val < 1e-18 {
591            return 0.0;
592        }
593
594        if pivot_row != k {
595            a.swap(k, pivot_row);
596            sign = -sign;
597        }
598
599        let pivot = a[k][k];
600        let mut row = k + 1;
601        while row < 6 {
602            let factor = a[row][k] / pivot;
603            let mut col = k + 1;
604            while col < 6 {
605                a[row][col] -= factor * a[k][col];
606                col += 1;
607            }
608            row += 1;
609        }
610        k += 1;
611    }
612
613    let mut det = sign;
614    let mut d = 0;
615    while d < 6 {
616        det *= a[d][d];
617        d += 1;
618    }
619    det
620}
621
622/// Computes the 6D task-space error vector `[dp_x, dp_y, dp_z, dw_x, dw_y, dw_z]` in the base frame.
623fn pose_error_world(current: &Transform3D<f64>, target: &Transform3D<f64>) -> ([f64; 6], f64, f64) {
624    let cur = current.to_matrix();
625    let tgt = target.to_matrix();
626
627    let dx = tgt[0][3] - cur[0][3];
628    let dy = tgt[1][3] - cur[1][3];
629    let dz = tgt[2][3] - cur[2][3];
630    let pos_err = libm::sqrt(dx * dx + dy * dy + dz * dz);
631
632    // Relative rotation in base frame: R_err = R_target * R_current^T
633    let mut r_err = [[0.0_f64; 3]; 3];
634    let mut i = 0;
635    while i < 3 {
636        let mut j = 0;
637        while j < 3 {
638            r_err[i][j] = tgt[i][0] * cur[j][0] + tgt[i][1] * cur[j][1] + tgt[i][2] * cur[j][2];
639            j += 1;
640        }
641        i += 1;
642    }
643
644    let (rot_vec, ori_err) = rotation_matrix_log(r_err);
645    (
646        [dx, dy, dz, rot_vec[0], rot_vec[1], rot_vec[2]],
647        pos_err,
648        ori_err,
649    )
650}
651
652/// Matrix logarithm $\log(R) \in \mathfrak{so}(3)$ mapped to a 3D rotation vector and angle.
653fn rotation_matrix_log(r: [[f64; 3]; 3]) -> ([f64; 3], f64) {
654    let trace = r[0][0] + r[1][1] + r[2][2];
655    let cos_theta = ((trace - 1.0) * 0.5).clamp(-1.0, 1.0);
656    let theta = libm::acos(cos_theta);
657
658    let vx = r[2][1] - r[1][2];
659    let vy = r[0][2] - r[2][0];
660    let vz = r[1][0] - r[0][1];
661
662    if theta < 1e-7 {
663        return ([0.5 * vx, 0.5 * vy, 0.5 * vz], theta);
664    }
665
666    let sin_theta = libm::sin(theta);
667    if libm::fabs(sin_theta) < 1e-7 {
668        // Near pi: extract axis from diagonal entries of (R + I) / 2
669        let ax = libm::sqrt((0.5 * (r[0][0] + 1.0)).max(0.0));
670        let ay = libm::sqrt((0.5 * (r[1][1] + 1.0)).max(0.0)).copysign(r[0][1]);
671        let az = libm::sqrt((0.5 * (r[2][2] + 1.0)).max(0.0)).copysign(r[0][2]);
672        return ([ax * theta, ay * theta, az * theta], theta);
673    }
674
675    let factor = theta / (2.0 * sin_theta);
676    ([vx * factor, vy * factor, vz * factor], theta)
677}
678
679/// Solves a $6 \times 6$ linear system $A x = b$ in place using Gaussian elimination with partial pivoting.
680fn solve_6x6(a: &mut [[f64; 6]; 6], b: &[f64; 6]) -> CuResult<[f64; 6]> {
681    let mut rhs = *b;
682    let mut k = 0;
683    while k < 6 {
684        let mut pivot_row = k;
685        let mut max_val = libm::fabs(a[k][k]);
686        let mut i = k + 1;
687        while i < 6 {
688            let v = libm::fabs(a[i][k]);
689            if v > max_val {
690                max_val = v;
691                pivot_row = i;
692            }
693            i += 1;
694        }
695
696        if max_val < 1e-15 {
697            return Err(CuError::from(
698                "repose_kinematics: singular damped normal matrix",
699            ));
700        }
701
702        if pivot_row != k {
703            a.swap(k, pivot_row);
704            rhs.swap(k, pivot_row);
705        }
706
707        let pivot = a[k][k];
708        let mut row = k + 1;
709        while row < 6 {
710            let factor = a[row][k] / pivot;
711            a[row][k] = 0.0;
712            let mut col = k + 1;
713            while col < 6 {
714                a[row][col] -= factor * a[k][col];
715                col += 1;
716            }
717            rhs[row] -= factor * rhs[k];
718            row += 1;
719        }
720        k += 1;
721    }
722
723    let mut x = [0.0_f64; 6];
724    let mut idx = 6;
725    while idx > 0 {
726        idx -= 1;
727        let mut sum = rhs[idx];
728        let mut col = idx + 1;
729        while col < 6 {
730            sum -= a[idx][col] * x[col];
731            col += 1;
732        }
733        x[idx] = sum / a[idx][idx];
734    }
735    Ok(x)
736}