1#![allow(
2 dead_code,
3 clippy::needless_borrows_for_generic_args,
4 clippy::new_without_default
5)]
6
7use std::f64::consts::PI;
15
16const KP: f64 = 2.0;
18const DT: f64 = 0.1;
19const DEFAULT_N_ITERATIONS: usize = 10000;
20const DEFAULT_GOAL_THRESHOLD: f64 = 0.1;
21
22#[derive(Debug, Clone)]
24pub struct NLinkArm {
25 pub link_lengths: Vec<f64>,
27 pub joint_angles: Vec<f64>,
29 pub points: Vec<[f64; 2]>,
31 pub end_effector: [f64; 2],
33 pub n_links: usize,
35}
36
37impl NLinkArm {
38 pub fn new(link_lengths: Vec<f64>, joint_angles: Vec<f64>) -> Self {
44 assert_eq!(
45 link_lengths.len(),
46 joint_angles.len(),
47 "link_lengths and joint_angles must have the same length"
48 );
49 let n_links = link_lengths.len();
50 let mut arm = NLinkArm {
51 link_lengths,
52 joint_angles,
53 points: vec![[0.0, 0.0]; n_links + 1],
54 end_effector: [0.0, 0.0],
55 n_links,
56 };
57 arm.update_points();
58 arm
59 }
60
61 pub fn update_joints(&mut self, joint_angles: &[f64]) {
63 assert_eq!(joint_angles.len(), self.n_links);
64 self.joint_angles = joint_angles.to_vec();
65 self.update_points();
66 }
67
68 pub fn update_points(&mut self) {
70 self.points[0] = [0.0, 0.0];
71 for i in 1..=self.n_links {
72 let cumulative_angle: f64 = self.joint_angles[..i].iter().sum();
73 self.points[i][0] =
74 self.points[i - 1][0] + self.link_lengths[i - 1] * cumulative_angle.cos();
75 self.points[i][1] =
76 self.points[i - 1][1] + self.link_lengths[i - 1] * cumulative_angle.sin();
77 }
78 self.end_effector = self.points[self.n_links];
79 }
80
81 pub fn workspace_radius(&self) -> f64 {
83 self.link_lengths.iter().sum()
84 }
85}
86
87pub fn forward_kinematics(link_lengths: &[f64], joint_angles: &[f64]) -> [f64; 2] {
91 let n = link_lengths.len();
92 let mut x = 0.0;
93 let mut y = 0.0;
94 for i in 1..=n {
95 let cumulative_angle: f64 = joint_angles[..i].iter().sum();
96 x += link_lengths[i - 1] * cumulative_angle.cos();
97 y += link_lengths[i - 1] * cumulative_angle.sin();
98 }
99 [x, y]
100}
101
102fn jacobian(link_lengths: &[f64], joint_angles: &[f64]) -> Vec<Vec<f64>> {
107 let n = link_lengths.len();
108 let mut j0 = vec![0.0; n];
109 let mut j1 = vec![0.0; n];
110 for (i, (j0_val, j1_val)) in j0.iter_mut().zip(j1.iter_mut()).enumerate() {
111 for k in i..n {
112 let cumulative_angle: f64 = joint_angles[..=k].iter().sum();
113 *j0_val -= link_lengths[k] * cumulative_angle.sin();
114 *j1_val += link_lengths[k] * cumulative_angle.cos();
115 }
116 }
117 vec![j0, j1]
118}
119
120fn pseudo_inverse_2xn(j: &[Vec<f64>]) -> Vec<Vec<f64>> {
123 let n = j[0].len();
124
125 let mut jjt = [[0.0f64; 2]; 2];
127 for row in 0..2 {
128 for col in 0..2 {
129 for (jr, jc) in j[row].iter().zip(j[col].iter()) {
130 jjt[row][col] += jr * jc;
131 }
132 }
133 }
134
135 let det = jjt[0][0] * jjt[1][1] - jjt[0][1] * jjt[1][0];
137 if det.abs() < 1e-12 {
138 return vec![vec![0.0; 2]; n];
140 }
141 let inv_det = 1.0 / det;
142 let inv = [
143 [jjt[1][1] * inv_det, -jjt[0][1] * inv_det],
144 [-jjt[1][0] * inv_det, jjt[0][0] * inv_det],
145 ];
146
147 let mut result = vec![vec![0.0; 2]; n];
149 for (i, result_row) in result.iter_mut().enumerate().take(n) {
150 for (col, result_cell) in result_row.iter_mut().enumerate().take(2) {
151 for k in 0..2 {
152 *result_cell += j[k][i] * inv[k][col];
153 }
154 }
155 }
156 result
157}
158
159pub fn distance_to_goal(current: &[f64; 2], goal: &[f64; 2]) -> ([f64; 2], f64) {
161 let dx = goal[0] - current[0];
162 let dy = goal[1] - current[1];
163 ([dx, dy], (dx * dx + dy * dy).sqrt())
164}
165
166pub fn angle_mod(angle: f64) -> f64 {
168 let mut a = angle % (2.0 * PI);
169 if a > PI {
170 a -= 2.0 * PI;
171 } else if a < -PI {
172 a += 2.0 * PI;
173 }
174 a
175}
176
177fn ang_diff(theta1: &[f64], theta2: &[f64]) -> Vec<f64> {
179 theta1
180 .iter()
181 .zip(theta2.iter())
182 .map(|(a, b)| angle_mod(a - b))
183 .collect()
184}
185
186pub fn inverse_kinematics(
191 link_lengths: &[f64],
192 initial_joint_angles: &[f64],
193 goal_pos: &[f64; 2],
194) -> Option<Vec<f64>> {
195 inverse_kinematics_with_params(
196 link_lengths,
197 initial_joint_angles,
198 goal_pos,
199 DEFAULT_N_ITERATIONS,
200 DEFAULT_GOAL_THRESHOLD,
201 )
202}
203
204pub fn inverse_kinematics_with_params(
206 link_lengths: &[f64],
207 initial_joint_angles: &[f64],
208 goal_pos: &[f64; 2],
209 max_iterations: usize,
210 goal_threshold: f64,
211) -> Option<Vec<f64>> {
212 let mut joint_angles = initial_joint_angles.to_vec();
213
214 for _iteration in 0..max_iterations {
215 let current_pos = forward_kinematics(link_lengths, &joint_angles);
216 let (errors, distance) = distance_to_goal(¤t_pos, goal_pos);
217 if distance < goal_threshold {
218 return Some(joint_angles);
219 }
220
221 let j = jacobian(link_lengths, &joint_angles);
222 let j_pinv = pseudo_inverse_2xn(&j);
223
224 for i in 0..joint_angles.len() {
226 joint_angles[i] += j_pinv[i][0] * errors[0] + j_pinv[i][1] * errors[1];
227 }
228 }
229 None
230}
231
232pub fn move_to_goal(
237 arm: &mut NLinkArm,
238 goal_angles: &[f64],
239 goal_pos: &[f64; 2],
240 goal_threshold: f64,
241) -> Vec<Vec<f64>> {
242 let mut trajectory = Vec::new();
243
244 for _ in 0..10000 {
245 let (_, distance) = distance_to_goal(&arm.end_effector, goal_pos);
246 if distance < goal_threshold {
247 break;
248 }
249 let diff = ang_diff(goal_angles, &arm.joint_angles);
250 let new_angles: Vec<f64> = arm
251 .joint_angles
252 .iter()
253 .zip(diff.iter())
254 .map(|(a, d)| a + KP * d * DT)
255 .collect();
256 arm.update_joints(&new_angles);
257 trajectory.push(arm.joint_angles.clone());
258 }
259 trajectory
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use std::f64::consts::FRAC_PI_4;
266
267 #[test]
268 fn test_forward_kinematics_straight() {
269 let links = vec![1.0, 1.0, 1.0];
270 let angles = vec![0.0, 0.0, 0.0];
271 let [x, y] = forward_kinematics(&links, &angles);
272 assert!((x - 3.0).abs() < 1e-10);
273 assert!(y.abs() < 1e-10);
274 }
275
276 #[test]
277 fn test_forward_kinematics_bent() {
278 let links = vec![1.0, 1.0];
279 let angles = vec![0.0, FRAC_PI_4];
280 let [x, y] = forward_kinematics(&links, &angles);
281 let expected_x = 1.0 + (FRAC_PI_4).cos();
284 let expected_y = (FRAC_PI_4).sin();
285 assert!((x - expected_x).abs() < 1e-10);
286 assert!((y - expected_y).abs() < 1e-10);
287 }
288
289 #[test]
290 fn test_nlink_arm_new() {
291 let arm = NLinkArm::new(vec![1.0, 1.0], vec![0.0, 0.0]);
292 assert_eq!(arm.n_links, 2);
293 assert!((arm.end_effector[0] - 2.0).abs() < 1e-10);
294 assert!(arm.end_effector[1].abs() < 1e-10);
295 assert_eq!(arm.points.len(), 3);
296 }
297
298 #[test]
299 fn test_nlink_arm_update_joints() {
300 let mut arm = NLinkArm::new(vec![1.0, 1.0], vec![0.0, 0.0]);
301 arm.update_joints(&[PI / 2.0, 0.0]);
302 assert!(arm.end_effector[0].abs() < 1e-10);
304 assert!((arm.end_effector[1] - 2.0).abs() < 1e-10);
305 }
306
307 #[test]
308 fn test_inverse_kinematics_reachable() {
309 let links = vec![1.0, 1.0];
311 let initial = vec![0.3, 0.3]; let goal = [1.0, 1.0]; let result = inverse_kinematics(&links, &initial, &goal);
315 assert!(result.is_some(), "IK should converge for reachable target");
316
317 let angles = result.unwrap();
318 let pos = forward_kinematics(&links, &angles);
319 let dist = ((pos[0] - goal[0]).powi(2) + (pos[1] - goal[1]).powi(2)).sqrt();
320 assert!(dist < DEFAULT_GOAL_THRESHOLD);
321 }
322
323 #[test]
324 fn test_inverse_kinematics_unreachable() {
325 let links = vec![1.0, 1.0];
326 let initial = vec![0.0, 0.0];
327 let goal = [10.0, 10.0];
329
330 let result = inverse_kinematics_with_params(&links, &initial, &goal, 500, 0.01);
331 assert!(result.is_none());
332 }
333
334 #[test]
335 fn test_angle_mod() {
336 assert!((angle_mod(0.0)).abs() < 1e-10);
337 assert!((angle_mod(2.0 * PI) - 0.0).abs() < 1e-10);
338 assert!((angle_mod(PI + 0.1) - (-PI + 0.1)).abs() < 1e-10);
339 assert!((angle_mod(-PI - 0.1) - (PI - 0.1)).abs() < 1e-10);
340 }
341
342 #[test]
343 fn test_distance_to_goal() {
344 let (errors, dist) = distance_to_goal(&[0.0, 0.0], &[3.0, 4.0]);
345 assert!((errors[0] - 3.0).abs() < 1e-10);
346 assert!((errors[1] - 4.0).abs() < 1e-10);
347 assert!((dist - 5.0).abs() < 1e-10);
348 }
349
350 #[test]
351 fn test_jacobian_shape() {
352 let links = vec![1.0, 1.0, 1.0];
353 let angles = vec![0.0, 0.0, 0.0];
354 let j = jacobian(&links, &angles);
355 assert_eq!(j.len(), 2);
356 assert_eq!(j[0].len(), 3);
357 assert_eq!(j[1].len(), 3);
358 }
359
360 #[test]
361 fn test_move_to_goal_converges() {
362 let mut arm = NLinkArm::new(vec![1.0, 1.0], vec![0.5, 0.5]);
363 let goal = [1.5, 0.5];
364 let goal_angles =
365 inverse_kinematics(&arm.link_lengths.clone(), &arm.joint_angles.clone(), &goal)
366 .unwrap();
367 let traj = move_to_goal(&mut arm, &goal_angles, &goal, 0.1);
368 assert!(!traj.is_empty());
369 let (_, dist) = distance_to_goal(&arm.end_effector, &goal);
370 assert!(dist < 0.2);
371 }
372
373 #[test]
374 fn test_10_link_arm() {
375 let n = 10;
376 let links = vec![0.5; n]; let initial = vec![0.1; n]; let goal = [3.0, 1.0]; let result = inverse_kinematics(&links, &initial, &goal);
381 assert!(result.is_some(), "10-link IK should converge");
382
383 let angles = result.unwrap();
384 let pos = forward_kinematics(&links, &angles);
385 let dist = ((pos[0] - goal[0]).powi(2) + (pos[1] - goal[1]).powi(2)).sqrt();
386 assert!(dist < DEFAULT_GOAL_THRESHOLD);
387 }
388
389 #[test]
390 fn test_workspace_radius() {
391 let arm = NLinkArm::new(vec![1.0, 2.0, 0.5], vec![0.0, 0.0, 0.0]);
392 assert!((arm.workspace_radius() - 3.5).abs() < 1e-10);
393 }
394
395 #[test]
396 fn test_pseudo_inverse_identity_like() {
397 let j = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
399 let pinv = pseudo_inverse_2xn(&j);
400 assert!((pinv[0][0] - 1.0).abs() < 1e-10);
401 assert!((pinv[0][1] - 0.0).abs() < 1e-10);
402 assert!((pinv[1][0] - 0.0).abs() < 1e-10);
403 assert!((pinv[1][1] - 1.0).abs() < 1e-10);
404 }
405}