rust_robotics_control/
n_joint_arm_3d.rs1#![allow(dead_code, clippy::new_without_default)]
2
3use nalgebra::{Matrix3, Matrix3xX, Vector3};
16
17const JACOBIAN_EPSILON: f64 = 1e-6;
19
20const DAMPING_LAMBDA: f64 = 0.5;
22
23#[derive(Debug, Clone)]
25pub struct NLinkArm3D {
26 link_lengths: Vec<f64>,
27 joint_angles: Vec<f64>,
28}
29
30impl NLinkArm3D {
31 pub fn new(link_lengths: Vec<f64>) -> Self {
33 let n = link_lengths.len();
34 Self {
35 link_lengths,
36 joint_angles: vec![0.0; n],
37 }
38 }
39
40 pub fn with_angles(link_lengths: Vec<f64>, joint_angles: Vec<f64>) -> Self {
46 assert_eq!(
47 link_lengths.len(),
48 joint_angles.len(),
49 "link_lengths and joint_angles must have the same length"
50 );
51 Self {
52 link_lengths,
53 joint_angles,
54 }
55 }
56
57 pub fn forward_kinematics(&self) -> Vec<Vector3<f64>> {
66 let n = self.link_lengths.len();
67 let mut points = Vec::with_capacity(n + 1);
68 let mut pos = Vector3::zeros();
69 let mut rot = Matrix3::identity();
70
71 points.push(pos);
72
73 for i in 0..n {
74 let angle = self.joint_angles[i];
75 let local_rot = if i % 2 == 0 {
76 rotation_z(angle)
77 } else {
78 rotation_y(angle)
79 };
80 rot *= local_rot;
81 let direction = rot * Vector3::new(self.link_lengths[i], 0.0, 0.0);
82 pos += direction;
83 points.push(pos);
84 }
85
86 points
87 }
88
89 pub fn end_effector(&self) -> Vector3<f64> {
91 let points = self.forward_kinematics();
92 *points
94 .last()
95 .expect("forward_kinematics always returns at least one point")
96 }
97
98 pub fn jacobian(&self) -> Matrix3xX<f64> {
102 let n = self.joint_angles.len();
103 let mut jac = Matrix3xX::zeros(n);
104
105 for j in 0..n {
106 let mut angles_plus = self.joint_angles.clone();
107 let mut angles_minus = self.joint_angles.clone();
108 angles_plus[j] += JACOBIAN_EPSILON;
109 angles_minus[j] -= JACOBIAN_EPSILON;
110
111 let arm_plus = NLinkArm3D {
112 link_lengths: self.link_lengths.clone(),
113 joint_angles: angles_plus,
114 };
115 let arm_minus = NLinkArm3D {
116 link_lengths: self.link_lengths.clone(),
117 joint_angles: angles_minus,
118 };
119
120 let diff =
121 (arm_plus.end_effector() - arm_minus.end_effector()) / (2.0 * JACOBIAN_EPSILON);
122 jac.set_column(j, &diff);
123 }
124
125 jac
126 }
127
128 pub fn inverse_kinematics(
135 &mut self,
136 target: Vector3<f64>,
137 max_iter: usize,
138 tolerance: f64,
139 ) -> bool {
140 for _ in 0..max_iter {
141 let ee = self.end_effector();
142 let error = target - ee;
143
144 if error.norm() < tolerance {
145 return true;
146 }
147
148 let j = self.jacobian();
149 let jjt = &j * j.transpose();
151 let damped = jjt + Matrix3::identity() * DAMPING_LAMBDA;
152
153 if let Some(decomp) = damped.try_inverse() {
155 let x = decomp * error;
156 let dq = j.transpose() * x;
157
158 for i in 0..self.joint_angles.len() {
159 self.joint_angles[i] += dq[i];
160 }
161 } else {
162 return false;
164 }
165 }
166
167 let ee = self.end_effector();
169 (target - ee).norm() < tolerance
170 }
171
172 pub fn joint_angles(&self) -> &[f64] {
174 &self.joint_angles
175 }
176
177 pub fn link_lengths(&self) -> &[f64] {
179 &self.link_lengths
180 }
181}
182
183fn rotation_z(theta: f64) -> Matrix3<f64> {
185 let (s, c) = theta.sin_cos();
186 Matrix3::new(c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0)
187}
188
189fn rotation_y(theta: f64) -> Matrix3<f64> {
191 let (s, c) = theta.sin_cos();
192 Matrix3::new(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c)
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_3d_arm_creation() {
201 let arm = NLinkArm3D::new(vec![1.0, 1.0, 1.0]);
202 assert_eq!(arm.link_lengths().len(), 3);
203 assert_eq!(arm.joint_angles().len(), 3);
204 assert!(arm.joint_angles().iter().all(|&a| a == 0.0));
205
206 let arm2 = NLinkArm3D::with_angles(vec![1.0, 2.0], vec![0.5, -0.3]);
207 assert_eq!(arm2.link_lengths(), &[1.0, 2.0]);
208 assert_eq!(arm2.joint_angles(), &[0.5, -0.3]);
209 }
210
211 #[test]
212 fn test_forward_kinematics_straight() {
213 let arm = NLinkArm3D::new(vec![1.0, 1.5, 0.5]);
215 let ee = arm.end_effector();
216 let total: f64 = arm.link_lengths().iter().sum();
217 assert!(
218 (ee.x - total).abs() < 1e-10,
219 "Expected x={total}, got x={}",
220 ee.x
221 );
222 assert!(ee.y.abs() < 1e-10, "Expected y=0, got y={}", ee.y);
223 assert!(ee.z.abs() < 1e-10, "Expected z=0, got z={}", ee.z);
224
225 let points = arm.forward_kinematics();
227 assert_eq!(points.len(), 4);
228 assert!((points[0] - Vector3::zeros()).norm() < 1e-10);
229 }
230
231 #[test]
232 fn test_inverse_kinematics_reachable() {
233 let mut arm = NLinkArm3D::with_angles(vec![1.0; 4], vec![0.1, 0.1, 0.1, 0.1]);
235 let target = Vector3::new(3.0, 0.0, 0.0);
236 let converged = arm.inverse_kinematics(target, 500, 0.05);
237 assert!(
238 converged,
239 "IK should converge for reachable in-plane target"
240 );
241
242 let ee = arm.end_effector();
243 let dist = (ee - target).norm();
244 assert!(
245 dist < 0.05,
246 "End effector should be within tolerance, got dist={dist}"
247 );
248 }
249
250 #[test]
251 fn test_inverse_kinematics_3d_target() {
252 let mut arm = NLinkArm3D::with_angles(vec![1.0; 6], vec![0.1; 6]);
255 let target = Vector3::new(2.0, 1.0, 2.0);
256 let converged = arm.inverse_kinematics(target, 1000, 0.05);
257 assert!(converged, "IK should converge for reachable 3D target");
258
259 let ee = arm.end_effector();
260 let dist = (ee - target).norm();
261 assert!(
262 dist < 0.05,
263 "End effector should reach 3D target, got dist={dist}"
264 );
265 }
266}