1use 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#[derive(
24 Default, Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, Reflect,
25)]
26#[serde(rename_all = "snake_case")]
27pub enum CuIkSolver {
28 #[default]
30 DampedLeastSquares,
31 Pseudoinverse,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect)]
43#[serde(default, deny_unknown_fields)]
44pub struct CuIkParams {
45 pub solver: CuIkSolver,
47 pub max_iterations: u32,
49 pub position_tolerance: Length64,
51 pub orientation_tolerance: Angle64,
53 pub damping: f64,
55 pub max_step: Angle64,
57 pub max_linear_step: Length64,
59 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 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#[derive(
104 Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect,
105)]
106#[serde(rename_all = "snake_case")]
107pub enum CuIkStatus {
108 Converged {
110 iterations: u32,
111 position_error: Length64,
112 orientation_error: Angle64,
113 },
114 MaxIterationsReached {
116 iterations: u32,
117 position_error: Length64,
118 orientation_error: Angle64,
119 },
120 #[default]
122 Uninitialized,
123}
124
125impl CuIkStatus {
126 pub const fn is_converged(&self) -> bool {
128 matches!(self, Self::Converged { .. })
129 }
130}
131
132#[derive(
134 Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Encode, Decode, Reflect,
135)]
136#[serde(rename_all = "snake_case")]
137pub enum CuTwistFrame {
138 #[default]
140 Base,
141 Body,
143}
144
145#[derive(
147 Default, Debug, Clone, Copy, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect,
148)]
149pub struct CuTwist6D {
150 pub linear: [Velocity64; 3],
152 pub angular: [AngularVelocity64; 3],
154 pub frame: CuTwistFrame,
156}
157
158impl CuTwist6D {
159 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 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#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect)]
192#[serde(default, deny_unknown_fields)]
193pub struct CuDiffIkParams {
194 pub base_damping: f64,
196 pub max_singularity_damping: f64,
198 pub singularity_threshold_sq: f64,
201 pub limit_margin: Angle64,
203 pub linear_limit_margin: Length64,
205 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 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#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect)]
251#[serde(rename_all = "snake_case")]
252pub enum CuDiffIkStatus {
253 Nominal { manipulability_sq: f64 },
255 SingularityDamped {
257 manipulability_sq: f64,
258 effective_damping: f64,
259 },
260 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 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 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 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 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 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(¤t_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 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 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(¤t_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
622fn 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 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
652fn 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 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
679fn 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}