1#![allow(dead_code)]
12#![allow(clippy::too_many_arguments)]
13
14use rand::RngExt;
15use std::f64::consts::PI;
16
17#[inline]
23fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
24 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
25}
26
27#[inline]
29fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
30 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
31}
32
33#[inline]
35fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
36 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
37}
38
39#[inline]
41fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
42 [a[0] * s, a[1] * s, a[2] * s]
43}
44
45#[inline]
47fn len3(a: [f64; 3]) -> f64 {
48 dot3(a, a).sqrt()
49}
50
51#[inline]
53fn normalize3(a: [f64; 3]) -> [f64; 3] {
54 let l = len3(a);
55 if l < 1e-14 {
56 [0.0; 3]
57 } else {
58 scale3(a, 1.0 / l)
59 }
60}
61
62#[inline]
64fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
65 [
66 a[1] * b[2] - a[2] * b[1],
67 a[2] * b[0] - a[0] * b[2],
68 a[0] * b[1] - a[1] * b[0],
69 ]
70}
71
72pub type Mat4 = [f64; 16];
80
81#[allow(dead_code)]
83pub fn mat4_identity() -> Mat4 {
84 let mut m = [0.0f64; 16];
85 m[0] = 1.0;
86 m[5] = 1.0;
87 m[10] = 1.0;
88 m[15] = 1.0;
89 m
90}
91
92pub fn mat4_mul(a: &Mat4, b: &Mat4) -> Mat4 {
94 let mut c = [0.0f64; 16];
95 for row in 0..4 {
96 for col in 0..4 {
97 let mut s = 0.0f64;
98 for k in 0..4 {
99 s += a[row * 4 + k] * b[k * 4 + col];
100 }
101 c[row * 4 + col] = s;
102 }
103 }
104 c
105}
106
107pub fn mat4_transform_point(m: &Mat4, p: [f64; 3]) -> [f64; 3] {
109 [
110 m[0] * p[0] + m[1] * p[1] + m[2] * p[2] + m[3],
111 m[4] * p[0] + m[5] * p[1] + m[6] * p[2] + m[7],
112 m[8] * p[0] + m[9] * p[1] + m[10] * p[2] + m[11],
113 ]
114}
115
116pub fn mat4_transform_dir(m: &Mat4, d: [f64; 3]) -> [f64; 3] {
118 [
119 m[0] * d[0] + m[1] * d[1] + m[2] * d[2],
120 m[4] * d[0] + m[5] * d[1] + m[6] * d[2],
121 m[8] * d[0] + m[9] * d[1] + m[10] * d[2],
122 ]
123}
124
125pub fn mat4_translation(m: &Mat4) -> [f64; 3] {
127 [m[3], m[7], m[11]]
128}
129
130fn rot_x(angle: f64) -> Mat4 {
132 let c = angle.cos();
133 let s = angle.sin();
134 let mut m = mat4_identity();
135 m[5] = c;
136 m[6] = -s;
137 m[9] = s;
138 m[10] = c;
139 m
140}
141
142fn rot_z(angle: f64) -> Mat4 {
144 let c = angle.cos();
145 let s = angle.sin();
146 let mut m = mat4_identity();
147 m[0] = c;
148 m[1] = -s;
149 m[4] = s;
150 m[5] = c;
151 m
152}
153
154fn trans(tx: f64, ty: f64, tz: f64) -> Mat4 {
156 let mut m = mat4_identity();
157 m[3] = tx;
158 m[7] = ty;
159 m[11] = tz;
160 m
161}
162
163#[derive(Debug, Clone, Copy)]
171pub struct DhParam {
172 pub theta: f64,
174 pub d: f64,
176 pub a: f64,
178 pub alpha: f64,
180}
181
182impl DhParam {
183 pub fn new(theta: f64, d: f64, a: f64, alpha: f64) -> Self {
185 Self { theta, d, a, alpha }
186 }
187
188 pub fn transform(&self) -> Mat4 {
190 let rz = rot_z(self.theta);
191 let tz = trans(0.0, 0.0, self.d);
192 let tx = trans(self.a, 0.0, 0.0);
193 let rx = rot_x(self.alpha);
194 mat4_mul(&mat4_mul(&mat4_mul(&rz, &tz), &tx), &rx)
195 }
196}
197
198#[derive(Debug, Clone, Copy, PartialEq)]
204pub enum JointType {
205 Revolute,
207 Prismatic,
209 Fixed,
211}
212
213#[derive(Debug, Clone, Copy)]
219pub struct JointLimits {
220 pub min: f64,
222 pub max: f64,
224 pub max_velocity: f64,
226 pub max_effort: f64,
228}
229
230impl JointLimits {
231 pub fn new(min: f64, max: f64, max_velocity: f64, max_effort: f64) -> Self {
233 Self {
234 min,
235 max,
236 max_velocity,
237 max_effort,
238 }
239 }
240
241 pub fn is_within(&self, value: f64) -> bool {
243 value >= self.min && value <= self.max
244 }
245
246 pub fn clamp(&self, value: f64) -> f64 {
248 value.clamp(self.min, self.max)
249 }
250}
251
252#[derive(Debug, Clone)]
258pub struct RobotLink {
259 pub name: String,
261 pub dh: DhParam,
263 pub joint_type: JointType,
265 pub limits: Option<JointLimits>,
267 pub collision_radius: f64,
269 pub collision_half_len: f64,
271}
272
273impl RobotLink {
274 pub fn new(
276 name: impl Into<String>,
277 dh: DhParam,
278 joint_type: JointType,
279 limits: Option<JointLimits>,
280 collision_radius: f64,
281 collision_half_len: f64,
282 ) -> Self {
283 Self {
284 name: name.into(),
285 dh,
286 joint_type,
287 limits,
288 collision_radius,
289 collision_half_len,
290 }
291 }
292}
293
294#[derive(Debug, Clone)]
304pub struct RobotArm {
305 pub name: String,
307 pub links: Vec<RobotLink>,
309 pub base_transform: Mat4,
311}
312
313impl RobotArm {
314 pub fn new(name: impl Into<String>, links: Vec<RobotLink>) -> Self {
316 Self {
317 name: name.into(),
318 links,
319 base_transform: mat4_identity(),
320 }
321 }
322
323 pub fn with_base_transform(mut self, t: Mat4) -> Self {
325 self.base_transform = t;
326 self
327 }
328
329 pub fn dof(&self) -> usize {
331 self.links
332 .iter()
333 .filter(|l| l.joint_type != JointType::Fixed)
334 .count()
335 }
336
337 fn dh_with_q(&self, link_idx: usize, q: f64) -> DhParam {
340 let link = &self.links[link_idx];
341 let mut dh = link.dh;
342 match link.joint_type {
343 JointType::Revolute => dh.theta += q,
344 JointType::Prismatic => dh.d += q,
345 JointType::Fixed => {}
346 }
347 dh
348 }
349
350 pub fn forward_frames(&self, q: &[f64]) -> Vec<Mat4> {
353 let mut frames = Vec::with_capacity(self.links.len() + 1);
354 frames.push(self.base_transform);
355 let mut q_iter = q.iter().copied();
356 for (i, link) in self.links.iter().enumerate() {
357 let qi = if link.joint_type == JointType::Fixed {
358 0.0
359 } else {
360 q_iter.next().unwrap_or(0.0)
361 };
362 let dh = self.dh_with_q(i, qi);
363 let t = mat4_mul(
364 frames.last().expect("collection should not be empty"),
365 &dh.transform(),
366 );
367 frames.push(t);
368 }
369 frames
370 }
371
372 pub fn end_effector(&self, q: &[f64]) -> Mat4 {
374 let frames = self.forward_frames(q);
375 *frames.last().expect("collection should not be empty")
376 }
377
378 pub fn end_effector_pos(&self, q: &[f64]) -> [f64; 3] {
380 mat4_translation(&self.end_effector(q))
381 }
382
383 pub fn jacobian_numerical(&self, q: &[f64]) -> Vec<f64> {
391 let n = q.len();
392 let eps = 1e-7;
393 let p0 = self.end_effector_pos(q);
394 let mut j = vec![0.0f64; 3 * n];
395 let mut qp = q.to_vec();
396 for i in 0..n {
397 qp[i] += eps;
398 let p1 = self.end_effector_pos(&qp);
399 qp[i] = q[i];
400 for row in 0..3 {
401 j[row * n + i] = (p1[row] - p0[row]) / eps;
402 }
403 }
404 j
405 }
406
407 pub fn jacobian_6dof_numerical(&self, q: &[f64]) -> Vec<f64> {
412 let n = q.len();
413 let eps = 1e-7;
414 let t0 = self.end_effector(q);
415 let mut j = vec![0.0f64; 6 * n];
416 let mut qp = q.to_vec();
417 for i in 0..n {
418 qp[i] += eps;
419 let t1 = self.end_effector(&qp);
420 qp[i] = q[i];
421 for row in 0..3 {
423 j[row * n + i] = (t1[row * 4 + 3] - t0[row * 4 + 3]) / eps;
424 }
425 let r_delta = mat4_mul(&mat4_transpose(&t0), &t1);
427 let wx = (r_delta[6] - r_delta[9]) / (2.0 * eps);
429 let wy = (r_delta[8] - r_delta[2]) / (2.0 * eps);
430 let wz = (r_delta[1] - r_delta[4]) / (2.0 * eps);
431 j[3 * n + i] = wx;
432 j[4 * n + i] = wy;
433 j[5 * n + i] = wz;
434 }
435 j
436 }
437
438 pub fn jacobian_analytical(&self, q: &[f64]) -> Vec<f64> {
443 let frames = self.forward_frames(q);
444 let n = q.len();
445 let p_e = mat4_translation(frames.last().expect("collection should not be empty"));
446 let mut j = vec![0.0f64; 3 * n];
447 let mut qi = 0usize;
448 for (i, link) in self.links.iter().enumerate() {
449 if link.joint_type == JointType::Fixed {
450 continue;
451 }
452 let frame = &frames[i]; let z = [frame[2], frame[6], frame[10]];
455 let p_i = mat4_translation(frame);
456 let col: [f64; 3] = match link.joint_type {
457 JointType::Revolute => cross3(z, sub3(p_e, p_i)),
458 JointType::Prismatic => z,
459 JointType::Fixed => unreachable!(),
460 };
461 for row in 0..3 {
462 j[row * n + qi] = col[row];
463 }
464 qi += 1;
465 }
466 j
467 }
468
469 pub fn jacobian_pseudoinverse(j: &[f64], n: usize, lambda: f64) -> Vec<f64> {
478 let mut a = [0.0f64; 9];
480 for r in 0..3 {
481 for c in 0..3 {
482 let mut s = 0.0f64;
483 for k in 0..n {
484 s += j[r * n + k] * j[c * n + k];
485 }
486 a[r * 3 + c] = s;
487 }
488 }
489 let lam2 = lambda * lambda;
491 a[0] += lam2;
492 a[4] += lam2;
493 a[8] += lam2;
494 let inv = mat3_inverse(&a);
496 let mut jp = vec![0.0f64; n * 3];
498 for r in 0..n {
499 for c in 0..3 {
500 let mut s = 0.0f64;
501 for k in 0..3 {
502 s += j[k * n + r] * inv[k * 3 + c];
503 }
504 jp[r * 3 + c] = s;
505 }
506 }
507 jp
508 }
509
510 pub fn ik_newton_raphson(
519 &self,
520 target: [f64; 3],
521 q_init: &[f64],
522 max_iter: usize,
523 tol: f64,
524 lambda: f64,
525 ) -> Result<Vec<f64>, Vec<f64>> {
526 let mut q = q_init.to_vec();
527 let n = q.len();
528 for _iter in 0..max_iter {
529 let p = self.end_effector_pos(&q);
530 let e = sub3(target, p);
531 if len3(e) < tol {
532 return Ok(q);
533 }
534 let j = self.jacobian_analytical(&q);
535 let jp = Self::jacobian_pseudoinverse(&j, n, lambda);
536 for i in 0..n {
538 let dqi = jp[i * 3] * e[0] + jp[i * 3 + 1] * e[1] + jp[i * 3 + 2] * e[2];
539 q[i] += dqi;
540 }
541 for (i, link) in self.links.iter().enumerate() {
543 if link.joint_type == JointType::Fixed {
544 continue;
545 }
546 let qi_idx = self.joint_index(i);
547 if let (Some(qi_idx), Some(lim)) = (qi_idx, &link.limits)
548 && qi_idx < q.len()
549 {
550 q[qi_idx] = lim.clamp(q[qi_idx]);
551 }
552 }
553 }
554 Err(q)
555 }
556
557 fn joint_index(&self, link_idx: usize) -> Option<usize> {
559 let mut idx = 0usize;
560 for (i, l) in self.links.iter().enumerate() {
561 if l.joint_type == JointType::Fixed {
562 continue;
563 }
564 if i == link_idx {
565 return Some(idx);
566 }
567 idx += 1;
568 }
569 None
570 }
571
572 pub fn workspace_monte_carlo(&self, n_samples: usize) -> Vec<[f64; 3]> {
579 let mut rng = rand::rng();
580 let dof = self.dof();
581 let limits: Vec<JointLimits> = self
582 .links
583 .iter()
584 .filter(|l| l.joint_type != JointType::Fixed)
585 .map(|l| l.limits.unwrap_or(JointLimits::new(-PI, PI, 10.0, 100.0)))
586 .collect();
587 let mut points = Vec::with_capacity(n_samples);
588 for _ in 0..n_samples {
589 let q: Vec<f64> = (0..dof)
590 .map(|i| rng.random_range(limits[i].min..limits[i].max))
591 .collect();
592 points.push(self.end_effector_pos(&q));
593 }
594 points
595 }
596
597 pub fn workspace_bounding_box(&self, n_samples: usize) -> Option<([f64; 3], [f64; 3])> {
601 let pts = self.workspace_monte_carlo(n_samples);
602 if pts.is_empty() {
603 return None;
604 }
605 let mut mn = pts[0];
606 let mut mx = pts[0];
607 for p in &pts[1..] {
608 for k in 0..3 {
609 if p[k] < mn[k] {
610 mn[k] = p[k];
611 }
612 if p[k] > mx[k] {
613 mx[k] = p[k];
614 }
615 }
616 }
617 Some((mn, mx))
618 }
619
620 pub fn link_capsules(&self, q: &[f64]) -> Vec<([f64; 3], [f64; 3], f64)> {
628 let frames = self.forward_frames(q);
629 let mut caps = Vec::with_capacity(self.links.len());
630 for (i, link) in self.links.iter().enumerate() {
631 let frame = &frames[i + 1];
632 let center = mat4_translation(frame);
633 let z_world = [frame[2], frame[6], frame[10]];
635 let hl = link.collision_half_len;
636 let p_start = sub3(center, scale3(z_world, hl));
637 let p_end = add3(center, scale3(z_world, hl));
638 caps.push((p_start, p_end, link.collision_radius));
639 }
640 caps
641 }
642
643 pub fn check_collision_sphere(
647 &self,
648 q: &[f64],
649 sphere_center: [f64; 3],
650 sphere_radius: f64,
651 ) -> bool {
652 for (a, b, r) in self.link_capsules(q) {
653 let dist = dist_point_segment(sphere_center, a, b);
654 if dist < r + sphere_radius {
655 return true;
656 }
657 }
658 false
659 }
660
661 pub fn check_collision_aabb(&self, q: &[f64], box_min: [f64; 3], box_max: [f64; 3]) -> bool {
665 for (a, b, r) in self.link_capsules(q) {
666 if capsule_aabb_overlap(a, b, r, box_min, box_max) {
667 return true;
668 }
669 }
670 false
671 }
672
673 pub fn check_self_collision(&self, q: &[f64]) -> bool {
682 let caps = self.link_capsules(q);
683 let n = caps.len();
684 for i in 0..n {
685 for j in (i + 2)..n {
686 let (a1, b1, r1) = caps[i];
687 let (a2, b2, r2) = caps[j];
688 let dist = dist_segment_segment(a1, b1, a2, b2);
689 if dist < r1 + r2 {
690 return true;
691 }
692 }
693 }
694 false
695 }
696
697 pub fn joints_within_limits(&self, q: &[f64]) -> bool {
703 let mut qi = 0usize;
704 for link in &self.links {
705 if link.joint_type == JointType::Fixed {
706 continue;
707 }
708 if qi >= q.len() {
709 break;
710 }
711 if let Some(lim) = &link.limits
712 && !lim.is_within(q[qi])
713 {
714 return false;
715 }
716 qi += 1;
717 }
718 true
719 }
720
721 pub fn clamp_joints(&self, q: &[f64]) -> Vec<f64> {
723 let mut out = q.to_vec();
724 let mut qi = 0usize;
725 for link in &self.links {
726 if link.joint_type == JointType::Fixed {
727 continue;
728 }
729 if qi >= out.len() {
730 break;
731 }
732 if let Some(lim) = &link.limits {
733 out[qi] = lim.clamp(out[qi]);
734 }
735 qi += 1;
736 }
737 out
738 }
739
740 pub fn swept_volume_aabb(
747 &self,
748 q_start: &[f64],
749 q_end: &[f64],
750 n_steps: usize,
751 ) -> ([f64; 3], [f64; 3]) {
752 let n_steps = n_steps.max(2);
753 let mut mn = [f64::INFINITY; 3];
754 let mut mx = [f64::NEG_INFINITY; 3];
755 for step in 0..=n_steps {
756 let t = step as f64 / n_steps as f64;
757 let q: Vec<f64> = q_start
758 .iter()
759 .zip(q_end.iter())
760 .map(|(a, b)| a + t * (b - a))
761 .collect();
762 let p = self.end_effector_pos(&q);
763 for k in 0..3 {
764 if p[k] < mn[k] {
765 mn[k] = p[k];
766 }
767 if p[k] > mx[k] {
768 mx[k] = p[k];
769 }
770 }
771 }
772 (mn, mx)
773 }
774
775 pub fn manipulability(&self, q: &[f64]) -> f64 {
783 let n = q.len();
784 let j = self.jacobian_analytical(q);
785 let mut a = [0.0f64; 9];
787 for r in 0..3 {
788 for c in 0..3 {
789 let mut s = 0.0f64;
790 for k in 0..n {
791 s += j[r * n + k] * j[c * n + k];
792 }
793 a[r * 3 + c] = s;
794 }
795 }
796 mat3_det(&a).abs().sqrt()
797 }
798}
799
800pub fn mat3_det(m: &[f64; 9]) -> f64 {
806 m[0] * (m[4] * m[8] - m[5] * m[7]) - m[1] * (m[3] * m[8] - m[5] * m[6])
807 + m[2] * (m[3] * m[7] - m[4] * m[6])
808}
809
810pub fn mat3_inverse(m: &[f64; 9]) -> [f64; 9] {
814 let det = mat3_det(m);
815 if det.abs() < 1e-14 {
816 return [0.0; 9];
817 }
818 let inv_det = 1.0 / det;
819 [
820 (m[4] * m[8] - m[5] * m[7]) * inv_det,
821 (m[2] * m[7] - m[1] * m[8]) * inv_det,
822 (m[1] * m[5] - m[2] * m[4]) * inv_det,
823 (m[5] * m[6] - m[3] * m[8]) * inv_det,
824 (m[0] * m[8] - m[2] * m[6]) * inv_det,
825 (m[2] * m[3] - m[0] * m[5]) * inv_det,
826 (m[3] * m[7] - m[4] * m[6]) * inv_det,
827 (m[1] * m[6] - m[0] * m[7]) * inv_det,
828 (m[0] * m[4] - m[1] * m[3]) * inv_det,
829 ]
830}
831
832pub fn mat4_transpose(m: &Mat4) -> Mat4 {
834 let mut t = [0.0f64; 16];
835 for r in 0..4 {
836 for c in 0..4 {
837 t[c * 4 + r] = m[r * 4 + c];
838 }
839 }
840 t
841}
842
843pub fn dist_point_segment(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> f64 {
849 let ab = sub3(b, a);
850 let ap = sub3(p, a);
851 let t = dot3(ap, ab) / (dot3(ab, ab) + 1e-30);
852 let t = t.clamp(0.0, 1.0);
853 let closest = add3(a, scale3(ab, t));
854 len3(sub3(p, closest))
855}
856
857pub fn dist_segment_segment(a1: [f64; 3], b1: [f64; 3], a2: [f64; 3], b2: [f64; 3]) -> f64 {
859 let d1 = sub3(b1, a1);
860 let d2 = sub3(b2, a2);
861 let r = sub3(a1, a2);
862 let a = dot3(d1, d1);
863 let e = dot3(d2, d2);
864 let f = dot3(d2, r);
865 let eps = 1e-14;
866
867 let (s, t) = if a < eps && e < eps {
868 (0.0f64, 0.0f64)
869 } else if a < eps {
870 (0.0f64, (f / e).clamp(0.0, 1.0))
871 } else {
872 let c = dot3(d1, r);
873 if e < eps {
874 (((-c) / a).clamp(0.0, 1.0), 0.0)
875 } else {
876 let b = dot3(d1, d2);
877 let denom = a * e - b * b;
878 let s = if denom.abs() > eps {
879 ((b * f - c * e) / denom).clamp(0.0, 1.0)
880 } else {
881 0.0
882 };
883 let t = (b * s + f) / e;
884 if t < 0.0 {
885 let s = ((-c) / a).clamp(0.0, 1.0);
886 (s, 0.0f64)
887 } else if t > 1.0 {
888 let s = ((b - c) / a).clamp(0.0, 1.0);
889 (s, 1.0f64)
890 } else {
891 (s, t)
892 }
893 }
894 };
895 let c1 = add3(a1, scale3(d1, s));
896 let c2 = add3(a2, scale3(d2, t));
897 len3(sub3(c1, c2))
898}
899
900pub fn capsule_aabb_overlap(
902 a: [f64; 3],
903 b: [f64; 3],
904 r: f64,
905 box_min: [f64; 3],
906 box_max: [f64; 3],
907) -> bool {
908 let center = [
910 (box_min[0] + box_max[0]) * 0.5,
911 (box_min[1] + box_max[1]) * 0.5,
912 (box_min[2] + box_max[2]) * 0.5,
913 ];
914 let half = [
915 (box_max[0] - box_min[0]) * 0.5,
916 (box_max[1] - box_min[1]) * 0.5,
917 (box_max[2] - box_min[2]) * 0.5,
918 ];
919 let ab = sub3(b, a);
920 let ac = sub3(center, a);
921 let t = (dot3(ac, ab) / (dot3(ab, ab) + 1e-30)).clamp(0.0, 1.0);
922 let closest_on_seg = add3(a, scale3(ab, t));
923 let mut dist_sq = 0.0f64;
925 for k in 0..3 {
926 let v = closest_on_seg[k];
927 let lo = box_min[k];
928 let hi = box_max[k];
929 if v < lo {
930 dist_sq += (lo - v) * (lo - v);
931 } else if v > hi {
932 dist_sq += (v - hi) * (v - hi);
933 }
934 }
935 let _ = half; dist_sq <= r * r
937}
938
939pub fn build_planar_3dof(lengths: [f64; 3]) -> RobotArm {
947 let lim = JointLimits::new(-PI, PI, 3.0, 50.0);
948 let links = vec![
949 RobotLink::new(
950 "link1",
951 DhParam::new(0.0, 0.0, lengths[0], 0.0),
952 JointType::Revolute,
953 Some(lim),
954 0.03,
955 lengths[0] * 0.5,
956 ),
957 RobotLink::new(
958 "link2",
959 DhParam::new(0.0, 0.0, lengths[1], 0.0),
960 JointType::Revolute,
961 Some(lim),
962 0.03,
963 lengths[1] * 0.5,
964 ),
965 RobotLink::new(
966 "link3",
967 DhParam::new(0.0, 0.0, lengths[2], 0.0),
968 JointType::Revolute,
969 Some(lim),
970 0.03,
971 lengths[2] * 0.5,
972 ),
973 ];
974 RobotArm::new("planar_3dof", links)
975}
976
977pub fn build_6dof_puma_like() -> RobotArm {
979 let lim_base = JointLimits::new(-PI, PI, 2.0, 150.0);
980 let lim_elbow = JointLimits::new(-PI * 0.75, PI * 0.75, 2.0, 100.0);
981 let lim_wrist = JointLimits::new(-PI, PI, 3.0, 50.0);
982
983 let links = vec![
984 RobotLink::new(
985 "joint1",
986 DhParam::new(0.0, 0.36, 0.0, PI / 2.0),
987 JointType::Revolute,
988 Some(lim_base),
989 0.06,
990 0.18,
991 ),
992 RobotLink::new(
993 "joint2",
994 DhParam::new(0.0, 0.0, 0.43, 0.0),
995 JointType::Revolute,
996 Some(lim_elbow),
997 0.05,
998 0.215,
999 ),
1000 RobotLink::new(
1001 "joint3",
1002 DhParam::new(0.0, 0.0, 0.0, PI / 2.0),
1003 JointType::Revolute,
1004 Some(lim_elbow),
1005 0.05,
1006 0.05,
1007 ),
1008 RobotLink::new(
1009 "joint4",
1010 DhParam::new(0.0, 0.43, 0.0, -PI / 2.0),
1011 JointType::Revolute,
1012 Some(lim_wrist),
1013 0.04,
1014 0.215,
1015 ),
1016 RobotLink::new(
1017 "joint5",
1018 DhParam::new(0.0, 0.0, 0.0, PI / 2.0),
1019 JointType::Revolute,
1020 Some(lim_wrist),
1021 0.04,
1022 0.04,
1023 ),
1024 RobotLink::new(
1025 "joint6",
1026 DhParam::new(0.0, 0.1, 0.0, 0.0),
1027 JointType::Revolute,
1028 Some(lim_wrist),
1029 0.03,
1030 0.05,
1031 ),
1032 ];
1033 RobotArm::new("puma_6dof", links)
1034}
1035
1036#[derive(Debug, Clone)]
1042pub struct UrdfJoint {
1043 pub name: String,
1045 pub parent: String,
1047 pub child: String,
1049 pub joint_type: JointType,
1051 pub origin_xyz: [f64; 3],
1053 pub origin_rpy: [f64; 3],
1055 pub axis: [f64; 3],
1057 pub limits: Option<JointLimits>,
1059}
1060
1061#[derive(Debug, Clone)]
1063pub struct UrdfLink {
1064 pub name: String,
1066 pub collision_radius: f64,
1068 pub collision_half_len: f64,
1070}
1071
1072#[derive(Debug, Clone)]
1074pub struct UrdfRobot {
1075 pub name: String,
1077 pub links: Vec<UrdfLink>,
1079 pub joints: Vec<UrdfJoint>,
1081}
1082
1083impl UrdfRobot {
1084 pub fn to_robot_arm(&self) -> RobotArm {
1088 let arm_links: Vec<RobotLink> = self
1089 .joints
1090 .iter()
1091 .map(|jt| {
1092 let collision = self
1093 .links
1094 .iter()
1095 .find(|l| l.name == jt.child)
1096 .map(|l| (l.collision_radius, l.collision_half_len))
1097 .unwrap_or((0.05, 0.1));
1098 let dh = rpy_xyz_to_dh(jt.origin_rpy, jt.origin_xyz);
1100 RobotLink::new(
1101 &jt.name,
1102 dh,
1103 jt.joint_type,
1104 jt.limits,
1105 collision.0,
1106 collision.1,
1107 )
1108 })
1109 .collect();
1110 RobotArm::new(&self.name, arm_links)
1111 }
1112}
1113
1114fn rpy_xyz_to_dh(rpy: [f64; 3], xyz: [f64; 3]) -> DhParam {
1118 let a = (xyz[0] * xyz[0] + xyz[1] * xyz[1]).sqrt();
1119 DhParam::new(rpy[2], xyz[2], a, rpy[0])
1120}
1121
1122pub fn end_effector_velocity(jacobian: &[f64], dq: &[f64]) -> [f64; 3] {
1130 let n = dq.len();
1131 let mut v = [0.0f64; 3];
1132 for row in 0..3 {
1133 for k in 0..n {
1134 v[row] += jacobian[row * n + k] * dq[k];
1135 }
1136 }
1137 v
1138}
1139
1140pub fn joint_velocity_from_cartesian(
1144 jacobian: &[f64],
1145 n_dof: usize,
1146 v_desired: [f64; 3],
1147 lambda: f64,
1148) -> Vec<f64> {
1149 let jp = RobotArm::jacobian_pseudoinverse(jacobian, n_dof, lambda);
1150 let mut dq = vec![0.0f64; n_dof];
1151 for i in 0..n_dof {
1152 dq[i] =
1153 jp[i * 3] * v_desired[0] + jp[i * 3 + 1] * v_desired[1] + jp[i * 3 + 2] * v_desired[2];
1154 }
1155 dq
1156}
1157
1158#[derive(Debug, Clone, Copy, PartialEq)]
1164pub enum SingularityClass {
1165 Regular,
1167 NearSingular,
1169 BoundarySingular,
1171 InteriorSingular,
1173}
1174
1175pub fn classify_singularity(manipulability: f64) -> SingularityClass {
1177 if manipulability < 1e-6 {
1178 SingularityClass::InteriorSingular
1179 } else if manipulability < 1e-3 {
1180 SingularityClass::BoundarySingular
1181 } else if manipulability < 0.05 {
1182 SingularityClass::NearSingular
1183 } else {
1184 SingularityClass::Regular
1185 }
1186}
1187
1188pub fn jacobian_condition_number(jacobian: &[f64], n: usize) -> f64 {
1198 let mut a = [0.0f64; 9];
1200 for r in 0..3 {
1201 for c in 0..3 {
1202 let mut s = 0.0f64;
1203 for k in 0..n {
1204 s += jacobian[r * n + k] * jacobian[c * n + k];
1205 }
1206 a[r * 3 + c] = s;
1207 }
1208 }
1209 let frob: f64 = a.iter().map(|x| x * x).sum::<f64>().sqrt();
1211 let det = mat3_det(&a).abs();
1212 if frob < 1e-14 {
1213 return f64::INFINITY;
1214 }
1215 let min_sv_approx = det / (frob * frob + 1e-30);
1217 frob / (min_sv_approx + 1e-30)
1218}
1219
1220pub fn joint_space_path(q_start: &[f64], q_end: &[f64], n_steps: usize) -> Vec<Vec<f64>> {
1228 let n_steps = n_steps.max(1);
1229 (0..=n_steps)
1230 .map(|step| {
1231 let t = step as f64 / n_steps as f64;
1232 q_start
1233 .iter()
1234 .zip(q_end.iter())
1235 .map(|(a, b)| a + t * (b - a))
1236 .collect()
1237 })
1238 .collect()
1239}
1240
1241pub fn path_collision_free(
1244 arm: &RobotArm,
1245 path: &[Vec<f64>],
1246 sphere_center: [f64; 3],
1247 sphere_radius: f64,
1248) -> bool {
1249 for q in path {
1250 if arm.check_collision_sphere(q, sphere_center, sphere_radius) {
1251 return false;
1252 }
1253 if !arm.joints_within_limits(q) {
1254 return false;
1255 }
1256 }
1257 true
1258}
1259
1260pub fn joint_torques_from_force(jacobian: &[f64], n_dof: usize, force: [f64; 3]) -> Vec<f64> {
1268 let mut tau = vec![0.0f64; n_dof];
1269 for i in 0..n_dof {
1270 tau[i] = jacobian[i] * force[0]
1271 + jacobian[n_dof + i] * force[1]
1272 + jacobian[2 * n_dof + i] * force[2];
1273 }
1274 tau
1275}
1276
1277#[derive(Debug, Clone)]
1285pub struct ReachabilityMap {
1286 pub origin: [f64; 3],
1288 pub voxel_size: f64,
1290 pub dims: [usize; 3],
1292 pub counts: Vec<u32>,
1294}
1295
1296impl ReachabilityMap {
1297 pub fn new(origin: [f64; 3], voxel_size: f64, dims: [usize; 3]) -> Self {
1299 let total = dims[0] * dims[1] * dims[2];
1300 Self {
1301 origin,
1302 voxel_size,
1303 dims,
1304 counts: vec![0u32; total],
1305 }
1306 }
1307
1308 pub fn world_to_voxel(&self, p: [f64; 3]) -> Option<[usize; 3]> {
1310 let mut idx = [0usize; 3];
1311 for k in 0..3 {
1312 let v = (p[k] - self.origin[k]) / self.voxel_size;
1313 if v < 0.0 || v >= self.dims[k] as f64 {
1314 return None;
1315 }
1316 idx[k] = v as usize;
1317 }
1318 Some(idx)
1319 }
1320
1321 pub fn add_point(&mut self, p: [f64; 3]) {
1323 if let Some(idx) = self.world_to_voxel(p) {
1324 let flat = idx[0] * self.dims[1] * self.dims[2] + idx[1] * self.dims[2] + idx[2];
1325 self.counts[flat] = self.counts[flat].saturating_add(1);
1326 }
1327 }
1328
1329 pub fn populate(&mut self, arm: &RobotArm, n_samples: usize) {
1331 let pts = arm.workspace_monte_carlo(n_samples);
1332 for p in pts {
1333 self.add_point(p);
1334 }
1335 }
1336
1337 pub fn reachable_count(&self) -> usize {
1339 self.counts.iter().filter(|&&c| c > 0).count()
1340 }
1341}
1342
1343#[cfg(test)]
1348mod tests {
1349 use super::*;
1350
1351 fn planar_arm() -> RobotArm {
1352 build_planar_3dof([0.5, 0.4, 0.3])
1353 }
1354
1355 fn puma_arm() -> RobotArm {
1356 build_6dof_puma_like()
1357 }
1358
1359 #[test]
1362 fn test_dh_identity_at_zero() {
1363 let dh = DhParam::new(0.0, 0.0, 0.0, 0.0);
1364 let t = dh.transform();
1365 let ident = mat4_identity();
1366 for i in 0..16 {
1367 assert!(
1368 (t[i] - ident[i]).abs() < 1e-12,
1369 "DH identity mismatch at {i}"
1370 );
1371 }
1372 }
1373
1374 #[test]
1375 fn test_dh_pure_translation_a() {
1376 let a = 1.0;
1377 let dh = DhParam::new(0.0, 0.0, a, 0.0);
1378 let t = dh.transform();
1379 assert!((t[3] - a).abs() < 1e-12);
1381 assert!(t[7].abs() < 1e-12);
1382 assert!(t[11].abs() < 1e-12);
1383 }
1384
1385 #[test]
1386 fn test_dh_pure_translation_d() {
1387 let d = 2.0;
1388 let dh = DhParam::new(0.0, d, 0.0, 0.0);
1389 let t = dh.transform();
1390 assert!((t[11] - d).abs() < 1e-12);
1392 }
1393
1394 #[test]
1395 fn test_dh_rotation_theta() {
1396 let dh = DhParam::new(PI / 2.0, 0.0, 0.0, 0.0);
1397 let t = dh.transform();
1398 assert!(t[0].abs() < 1e-12); assert!((t[1] + 1.0).abs() < 1e-12); assert!((t[4] - 1.0).abs() < 1e-12); }
1403
1404 #[test]
1405 fn test_mat4_mul_identity() {
1406 let ident = mat4_identity();
1407 let a = rot_z(0.5);
1408 let result = mat4_mul(&a, &ident);
1409 for i in 0..16 {
1410 assert!(
1411 (result[i] - a[i]).abs() < 1e-12,
1412 "mul ident mismatch at {i}"
1413 );
1414 }
1415 }
1416
1417 #[test]
1418 fn test_mat4_transpose_roundtrip() {
1419 let m = rot_z(1.2);
1420 let tt = mat4_transpose(&mat4_transpose(&m));
1421 for i in 0..16 {
1422 assert!((tt[i] - m[i]).abs() < 1e-12);
1423 }
1424 }
1425
1426 #[test]
1429 fn test_fk_zero_config_planar_x() {
1430 let arm = planar_arm();
1431 let q = vec![0.0f64; 3];
1432 let pos = arm.end_effector_pos(&q);
1433 assert!((pos[0] - 1.2).abs() < 1e-10);
1435 assert!(pos[1].abs() < 1e-10);
1436 assert!(pos[2].abs() < 1e-10);
1437 }
1438
1439 #[test]
1440 fn test_fk_pi_fold() {
1441 let arm = planar_arm();
1442 let q = vec![0.0, PI, 0.0];
1444 let pos = arm.end_effector_pos(&q);
1445 assert!((pos[0] - (-0.2)).abs() < 1e-8);
1447 }
1448
1449 #[test]
1450 fn test_fk_frames_count() {
1451 let arm = planar_arm();
1452 let q = vec![0.0f64; 3];
1453 let frames = arm.forward_frames(&q);
1454 assert_eq!(frames.len(), 4);
1456 }
1457
1458 #[test]
1459 fn test_dof_count() {
1460 let arm = planar_arm();
1461 assert_eq!(arm.dof(), 3);
1462 }
1463
1464 #[test]
1465 fn test_dof_puma() {
1466 let arm = puma_arm();
1467 assert_eq!(arm.dof(), 6);
1468 }
1469
1470 #[test]
1473 fn test_joint_limits_within() {
1474 let lim = JointLimits::new(-PI, PI, 3.0, 50.0);
1475 assert!(lim.is_within(0.0));
1476 assert!(lim.is_within(-PI));
1477 assert!(lim.is_within(PI));
1478 assert!(!lim.is_within(PI + 0.001));
1479 }
1480
1481 #[test]
1482 fn test_joint_limits_clamp() {
1483 let lim = JointLimits::new(-1.0, 1.0, 1.0, 10.0);
1484 assert_eq!(lim.clamp(2.0), 1.0);
1485 assert_eq!(lim.clamp(-2.0), -1.0);
1486 assert_eq!(lim.clamp(0.5), 0.5);
1487 }
1488
1489 #[test]
1490 fn test_joints_within_limits_planar() {
1491 let arm = planar_arm();
1492 assert!(arm.joints_within_limits(&[0.0, 0.0, 0.0]));
1493 assert!(!arm.joints_within_limits(&[4.0, 0.0, 0.0])); }
1495
1496 #[test]
1497 fn test_clamp_joints_planar() {
1498 let arm = planar_arm();
1499 let clamped = arm.clamp_joints(&[5.0, -5.0, 0.0]);
1500 assert!((clamped[0] - PI).abs() < 1e-12);
1501 assert!((clamped[1] - (-PI)).abs() < 1e-12);
1502 }
1503
1504 #[test]
1507 fn test_jacobian_numerical_size() {
1508 let arm = planar_arm();
1509 let q = vec![0.1, 0.2, 0.3];
1510 let j = arm.jacobian_numerical(&q);
1511 assert_eq!(j.len(), 3 * 3);
1512 }
1513
1514 #[test]
1515 fn test_jacobian_analytical_matches_numerical() {
1516 let arm = planar_arm();
1517 let q = vec![0.2, -0.3, 0.5];
1518 let jn = arm.jacobian_numerical(&q);
1519 let ja = arm.jacobian_analytical(&q);
1520 for i in 0..9 {
1521 assert!(
1522 (jn[i] - ja[i]).abs() < 1e-4,
1523 "Jacobian mismatch at {i}: numerical={} analytical={}",
1524 jn[i],
1525 ja[i]
1526 );
1527 }
1528 }
1529
1530 #[test]
1531 fn test_jacobian_pseudoinverse_identity_check() {
1532 let j: Vec<f64> = vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
1534 let jp = RobotArm::jacobian_pseudoinverse(&j, 3, 0.0001);
1535 assert!((jp[0] - 1.0).abs() < 1e-3);
1537 assert!((jp[4] - 1.0).abs() < 1e-3);
1538 assert!((jp[8] - 1.0).abs() < 1e-3);
1539 }
1540
1541 #[test]
1544 fn test_ik_reachable_target() {
1545 let arm = planar_arm();
1546 let target = [0.8, 0.4, 0.0];
1547 let q_init = vec![0.1f64, 0.2, 0.1];
1548 let result = arm.ik_newton_raphson(target, &q_init, 200, 1e-6, 0.01);
1549 let q = match result {
1550 Ok(q) | Err(q) => q,
1551 };
1552 let pos = arm.end_effector_pos(&q);
1553 assert!(
1554 len3(sub3(target, pos)) < 0.05,
1555 "IK did not converge close enough"
1556 );
1557 }
1558
1559 #[test]
1560 fn test_ik_at_zero_config() {
1561 let arm = planar_arm();
1562 let target = arm.end_effector_pos(&[0.0, 0.0, 0.0]);
1563 let q_init = vec![0.01f64, 0.01, 0.01];
1564 let result = arm.ik_newton_raphson(target, &q_init, 200, 1e-8, 0.001);
1565 let q = match result {
1566 Ok(q) | Err(q) => q,
1567 };
1568 let pos = arm.end_effector_pos(&q);
1569 assert!(len3(sub3(target, pos)) < 1e-4);
1570 }
1571
1572 #[test]
1575 fn test_dist_point_segment_basic() {
1576 let d = dist_point_segment([0.0, 1.0, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1577 assert!((d - 1.0).abs() < 1e-12);
1578 }
1579
1580 #[test]
1581 fn test_dist_point_segment_on_segment() {
1582 let d = dist_point_segment([0.5, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1583 assert!(d.abs() < 1e-12);
1584 }
1585
1586 #[test]
1587 fn test_collision_sphere_no_hit() {
1588 let arm = planar_arm();
1589 let q = vec![0.0f64; 3];
1590 let hit = arm.check_collision_sphere(&q, [10.0, 10.0, 10.0], 0.01);
1592 assert!(!hit);
1593 }
1594
1595 #[test]
1596 fn test_collision_sphere_hit() {
1597 let arm = planar_arm();
1598 let q = vec![0.0f64; 3];
1599 let hit = arm.check_collision_sphere(&q, [0.25, 0.0, 0.0], 0.3);
1601 assert!(hit);
1602 }
1603
1604 #[test]
1605 fn test_self_collision_straight() {
1606 let arm = planar_arm();
1607 let q = vec![0.0f64; 3];
1609 assert!(!arm.check_self_collision(&q));
1610 }
1611
1612 #[test]
1613 fn test_capsule_aabb_no_overlap() {
1614 let no_overlap = capsule_aabb_overlap(
1615 [10.0, 0.0, 0.0],
1616 [11.0, 0.0, 0.0],
1617 0.1,
1618 [-1.0, -1.0, -1.0],
1619 [1.0, 1.0, 1.0],
1620 );
1621 assert!(!no_overlap);
1622 }
1623
1624 #[test]
1625 fn test_capsule_aabb_overlap() {
1626 let overlap = capsule_aabb_overlap(
1627 [-0.5, 0.0, 0.0],
1628 [0.5, 0.0, 0.0],
1629 0.1,
1630 [-1.0, -1.0, -1.0],
1631 [1.0, 1.0, 1.0],
1632 );
1633 assert!(overlap);
1634 }
1635
1636 #[test]
1639 fn test_swept_volume_contains_endpoints() {
1640 let arm = planar_arm();
1641 let q0 = vec![0.0f64; 3];
1642 let q1 = vec![PI / 4.0, PI / 4.0, PI / 4.0];
1643 let (mn, mx) = arm.swept_volume_aabb(&q0, &q1, 20);
1644 let p0 = arm.end_effector_pos(&q0);
1645 let p1 = arm.end_effector_pos(&q1);
1646 for k in 0..3 {
1647 assert!(p0[k] >= mn[k] - 1e-9 && p0[k] <= mx[k] + 1e-9);
1648 assert!(p1[k] >= mn[k] - 1e-9 && p1[k] <= mx[k] + 1e-9);
1649 }
1650 }
1651
1652 #[test]
1655 fn test_workspace_monte_carlo_count() {
1656 let arm = planar_arm();
1657 let pts = arm.workspace_monte_carlo(100);
1658 assert_eq!(pts.len(), 100);
1659 }
1660
1661 #[test]
1662 fn test_workspace_bounding_box_some() {
1663 let arm = planar_arm();
1664 let bb = arm.workspace_bounding_box(200);
1665 assert!(bb.is_some());
1666 let (mn, mx) = bb.unwrap();
1667 for k in 0..3 {
1668 assert!(mn[k] <= mx[k]);
1669 }
1670 }
1671
1672 #[test]
1675 fn test_manipulability_positive_planar() {
1676 let arm = planar_arm();
1677 let m = arm.manipulability(&[0.3, 0.4, 0.5]);
1678 assert!(m >= 0.0);
1679 }
1680
1681 #[test]
1682 fn test_manipulability_near_singular() {
1683 let arm = planar_arm();
1684 let m = arm.manipulability(&[0.0, 0.0, 0.0]);
1686 assert!(m >= 0.0);
1688 }
1689
1690 #[test]
1691 fn test_singularity_classification() {
1692 assert_eq!(
1693 classify_singularity(0.0),
1694 SingularityClass::InteriorSingular
1695 );
1696 assert_eq!(classify_singularity(0.1), SingularityClass::Regular);
1697 assert_eq!(
1698 classify_singularity(1e-4),
1699 SingularityClass::BoundarySingular
1700 );
1701 assert_eq!(classify_singularity(0.01), SingularityClass::NearSingular);
1702 }
1703
1704 #[test]
1707 fn test_reachability_map_populate() {
1708 let arm = planar_arm();
1709 let mut rmap = ReachabilityMap::new([-2.0, -2.0, -0.1], 0.1, [40, 40, 2]);
1710 rmap.populate(&arm, 500);
1711 assert!(rmap.reachable_count() > 0);
1712 }
1713
1714 #[test]
1715 fn test_reachability_map_voxel_oob() {
1716 let rmap = ReachabilityMap::new([0.0, 0.0, 0.0], 0.1, [10, 10, 10]);
1717 assert!(rmap.world_to_voxel([1.5, 0.5, 0.5]).is_none());
1718 }
1719
1720 #[test]
1723 fn test_joint_space_path_length() {
1724 let path = joint_space_path(&[0.0, 0.0], &[1.0, 1.0], 10);
1725 assert_eq!(path.len(), 11);
1726 }
1727
1728 #[test]
1729 fn test_joint_space_path_endpoints() {
1730 let q0 = vec![0.0f64, 0.5];
1731 let q1 = vec![1.0f64, -0.5];
1732 let path = joint_space_path(&q0, &q1, 5);
1733 for (a, b) in path[0].iter().zip(q0.iter()) {
1734 assert!((a - b).abs() < 1e-12);
1735 }
1736 for (a, b) in path.last().unwrap().iter().zip(q1.iter()) {
1737 assert!((a - b).abs() < 1e-12);
1738 }
1739 }
1740
1741 #[test]
1744 fn test_joint_torques_zero_force() {
1745 let arm = planar_arm();
1746 let q = vec![0.0f64; 3];
1747 let j = arm.jacobian_analytical(&q);
1748 let tau = joint_torques_from_force(&j, 3, [0.0, 0.0, 0.0]);
1749 for t in &tau {
1750 assert!(t.abs() < 1e-12);
1751 }
1752 }
1753
1754 #[test]
1757 fn test_urdf_to_robot_arm() {
1758 let robot = UrdfRobot {
1759 name: "test_bot".into(),
1760 links: vec![
1761 UrdfLink {
1762 name: "base".into(),
1763 collision_radius: 0.05,
1764 collision_half_len: 0.1,
1765 },
1766 UrdfLink {
1767 name: "link1".into(),
1768 collision_radius: 0.04,
1769 collision_half_len: 0.2,
1770 },
1771 ],
1772 joints: vec![UrdfJoint {
1773 name: "joint1".into(),
1774 parent: "base".into(),
1775 child: "link1".into(),
1776 joint_type: JointType::Revolute,
1777 origin_xyz: [0.0, 0.0, 0.2],
1778 origin_rpy: [0.0, 0.0, 0.0],
1779 axis: [0.0, 0.0, 1.0],
1780 limits: Some(JointLimits::new(-PI, PI, 2.0, 50.0)),
1781 }],
1782 };
1783 let arm = robot.to_robot_arm();
1784 assert_eq!(arm.links.len(), 1);
1785 assert_eq!(arm.dof(), 1);
1786 }
1787
1788 #[test]
1791 fn test_end_effector_velocity_zero_dq() {
1792 let arm = planar_arm();
1793 let q = vec![0.2f64, 0.3, 0.1];
1794 let j = arm.jacobian_analytical(&q);
1795 let v = end_effector_velocity(&j, &[0.0, 0.0, 0.0]);
1796 for vi in &v {
1797 assert!(vi.abs() < 1e-12);
1798 }
1799 }
1800
1801 #[test]
1802 fn test_joint_velocity_from_cartesian_zero() {
1803 let arm = planar_arm();
1804 let q = vec![0.2f64, 0.3, 0.1];
1805 let j = arm.jacobian_analytical(&q);
1806 let dq = joint_velocity_from_cartesian(&j, 3, [0.0, 0.0, 0.0], 0.01);
1807 for d in &dq {
1808 assert!(d.abs() < 1e-10);
1809 }
1810 }
1811
1812 #[test]
1815 fn test_segment_segment_parallel() {
1816 let d = dist_segment_segment(
1817 [0.0, 0.0, 0.0],
1818 [1.0, 0.0, 0.0],
1819 [0.0, 1.0, 0.0],
1820 [1.0, 1.0, 0.0],
1821 );
1822 assert!((d - 1.0).abs() < 1e-10);
1823 }
1824
1825 #[test]
1826 fn test_segment_segment_crossing() {
1827 let d = dist_segment_segment(
1828 [-1.0, 0.0, 0.0],
1829 [1.0, 0.0, 0.0],
1830 [0.0, -1.0, 0.5],
1831 [0.0, 1.0, 0.5],
1832 );
1833 assert!((d - 0.5).abs() < 1e-10);
1835 }
1836
1837 #[test]
1840 fn test_mat3_det_identity() {
1841 let ident = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
1842 assert!((mat3_det(&ident) - 1.0).abs() < 1e-12);
1843 }
1844
1845 #[test]
1846 fn test_mat3_inverse_identity() {
1847 let ident = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
1848 let inv = mat3_inverse(&ident);
1849 for i in 0..9 {
1850 assert!((inv[i] - ident[i]).abs() < 1e-12);
1851 }
1852 }
1853
1854 #[test]
1855 fn test_mat3_inverse_roundtrip() {
1856 let m = [2.0, 1.0, 0.0, 0.5, 3.0, 1.0, 0.0, 1.0, 4.0];
1857 let inv = mat3_inverse(&m);
1858 for r in 0..3 {
1860 for c in 0..3 {
1861 let mut s = 0.0f64;
1862 for k in 0..3 {
1863 s += m[r * 3 + k] * inv[k * 3 + c];
1864 }
1865 let expected = if r == c { 1.0 } else { 0.0 };
1866 assert!((s - expected).abs() < 1e-10, "m*inv[{r},{c}]={s}");
1867 }
1868 }
1869 }
1870}