1#![allow(dead_code, clippy::new_without_default)]
2
3use std::f64::consts::PI;
11
12const DEFAULT_GRID_SIZE: usize = 100;
14
15#[derive(Debug, Clone)]
21struct Arm2Link {
22 link_lengths: [f64; 2],
23 points: [[f64; 2]; 3],
24}
25
26impl Arm2Link {
27 fn new(link_lengths: [f64; 2]) -> Self {
28 let mut arm = Arm2Link {
29 link_lengths,
30 points: [[0.0; 2]; 3],
31 };
32 arm.update_joints(0.0, 0.0);
33 arm
34 }
35
36 fn update_joints(&mut self, theta1: f64, theta2: f64) {
37 self.points[0] = [0.0, 0.0];
38 self.points[1] = [
39 self.link_lengths[0] * theta1.cos(),
40 self.link_lengths[0] * theta1.sin(),
41 ];
42 let cumulative = theta1 + theta2;
43 self.points[2] = [
44 self.points[1][0] + self.link_lengths[1] * cumulative.cos(),
45 self.points[1][1] + self.link_lengths[1] * cumulative.sin(),
46 ];
47 }
48}
49
50#[derive(Debug, Clone, Copy)]
52pub struct CircleObstacle {
53 pub x: f64,
54 pub y: f64,
55 pub radius: f64,
56}
57
58impl CircleObstacle {
59 pub fn new(x: f64, y: f64, radius: f64) -> Self {
60 CircleObstacle { x, y, radius }
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
66pub struct GridCell {
67 pub i: usize,
68 pub j: usize,
69}
70
71#[derive(Debug, Clone)]
73pub struct ArmPath {
74 pub route_cells: Vec<GridCell>,
76 pub joint_angles: Vec<[f64; 2]>,
78}
79
80#[derive(Debug, Clone)]
85pub struct JointSpaceGrid {
86 pub size: usize,
88 pub data: Vec<bool>,
90}
91
92impl JointSpaceGrid {
93 pub fn new(size: usize) -> Self {
95 JointSpaceGrid {
96 size,
97 data: vec![false; size * size],
98 }
99 }
100
101 pub fn get(&self, i: usize, j: usize) -> bool {
103 self.data[i * self.size + j]
104 }
105
106 pub fn set(&mut self, i: usize, j: usize, val: bool) {
108 self.data[i * self.size + j] = val;
109 }
110
111 pub fn index_to_angle(&self, idx: usize) -> f64 {
113 2.0 * PI * idx as f64 / self.size as f64 - PI
114 }
115}
116
117pub fn detect_collision(a: &[f64; 2], b: &[f64; 2], obstacle: &CircleObstacle) -> bool {
124 let line_vec = [b[0] - a[0], b[1] - a[1]];
125 let line_mag = (line_vec[0] * line_vec[0] + line_vec[1] * line_vec[1]).sqrt();
126 if line_mag < 1e-12 {
127 let dx = a[0] - obstacle.x;
129 let dy = a[1] - obstacle.y;
130 return (dx * dx + dy * dy).sqrt() <= obstacle.radius;
131 }
132
133 let line_unit = [line_vec[0] / line_mag, line_vec[1] / line_mag];
134 let circle_vec = [obstacle.x - a[0], obstacle.y - a[1]];
135 let proj = circle_vec[0] * line_unit[0] + circle_vec[1] * line_unit[1];
136
137 let closest = if proj <= 0.0 {
138 *a
139 } else if proj >= line_mag {
140 *b
141 } else {
142 [a[0] + line_unit[0] * proj, a[1] + line_unit[1] * proj]
143 };
144
145 let dx = closest[0] - obstacle.x;
146 let dy = closest[1] - obstacle.y;
147 (dx * dx + dy * dy).sqrt() <= obstacle.radius
148}
149
150pub fn build_occupancy_grid(
155 link_lengths: &[f64; 2],
156 obstacles: &[CircleObstacle],
157 grid_size: usize,
158) -> JointSpaceGrid {
159 let mut grid = JointSpaceGrid::new(grid_size);
160 let mut arm = Arm2Link::new(*link_lengths);
161
162 for i in 0..grid_size {
163 let theta1 = grid.index_to_angle(i);
164 for j in 0..grid_size {
165 let theta2 = grid.index_to_angle(j);
166 arm.update_joints(theta1, theta2);
167
168 let mut collision = false;
169 for k in 0..2 {
171 if collision {
172 break;
173 }
174 for obs in obstacles {
175 if detect_collision(&arm.points[k], &arm.points[k + 1], obs) {
176 collision = true;
177 break;
178 }
179 }
180 }
181 grid.set(i, j, collision);
182 }
183 }
184 grid
185}
186
187pub fn build_occupancy_grid_default(
189 link_lengths: &[f64; 2],
190 obstacles: &[CircleObstacle],
191) -> JointSpaceGrid {
192 build_occupancy_grid(link_lengths, obstacles, DEFAULT_GRID_SIZE)
193}
194
195fn find_neighbors(i: usize, j: usize, m: usize) -> [(usize, usize); 4] {
197 let up = if i == 0 { m - 1 } else { i - 1 };
198 let down = if i + 1 >= m { 0 } else { i + 1 };
199 let left = if j == 0 { m - 1 } else { j - 1 };
200 let right = if j + 1 >= m { 0 } else { j + 1 };
201 [(up, j), (down, j), (i, left), (i, right)]
202}
203
204fn calc_heuristic_map(m: usize, goal: GridCell) -> Vec<Vec<f64>> {
206 let mut hmap = vec![vec![0.0f64; m]; m];
207 for (i, hmap_row) in hmap.iter_mut().enumerate() {
208 for (j, hmap_val) in hmap_row.iter_mut().enumerate() {
209 let di = {
210 let d = (i as isize - goal.i as isize).unsigned_abs();
211 d.min(m - d)
212 };
213 let dj = {
214 let d = (j as isize - goal.j as isize).unsigned_abs();
215 d.min(m - d)
216 };
217 *hmap_val = (di + dj) as f64;
218 }
219 }
220 hmap
221}
222
223pub fn astar_torus(grid: &JointSpaceGrid, start: GridCell, goal: GridCell) -> Option<ArmPath> {
229 let m = grid.size;
230
231 let mut cell_state = vec![vec![0u8; m]; m];
233 for (i, state_row) in cell_state.iter_mut().enumerate() {
234 for (j, state_val) in state_row.iter_mut().enumerate() {
235 if grid.get(i, j) {
236 *state_val = 1; }
238 }
239 }
240
241 if cell_state[start.i][start.j] == 1 || cell_state[goal.i][goal.j] == 1 {
243 return None;
244 }
245
246 cell_state[start.i][start.j] = 4;
247 cell_state[goal.i][goal.j] = 5;
248
249 let heuristic_map = calc_heuristic_map(m, goal);
250
251 let mut f_score = vec![vec![f64::INFINITY; m]; m];
253 let mut g_score = vec![vec![f64::INFINITY; m]; m];
254 let mut parent: Vec<Vec<Option<(usize, usize)>>> = vec![vec![None; m]; m];
255
256 f_score[start.i][start.j] = heuristic_map[start.i][start.j];
257 g_score[start.i][start.j] = 0.0;
258
259 loop {
260 let mut min_f = f64::INFINITY;
262 let mut current = (0, 0);
263 for (i, f_row) in f_score.iter().enumerate() {
264 for (j, &f_val) in f_row.iter().enumerate() {
265 if f_val < min_f {
266 min_f = f_val;
267 current = (i, j);
268 }
269 }
270 }
271
272 if min_f.is_infinite() {
273 return None; }
275
276 if current == (goal.i, goal.j) {
277 break; }
279
280 cell_state[current.0][current.1] = 2;
282 f_score[current.0][current.1] = f64::INFINITY; let neighbors = find_neighbors(current.0, current.1, m);
285 for &(ni, nj) in &neighbors {
286 let state = cell_state[ni][nj];
287 if state == 0 || state == 5 {
288 let tentative_g = g_score[current.0][current.1] + 1.0;
289 if tentative_g < g_score[ni][nj] {
290 g_score[ni][nj] = tentative_g;
291 f_score[ni][nj] = tentative_g + heuristic_map[ni][nj];
292 parent[ni][nj] = Some(current);
293 if state != 5 {
294 cell_state[ni][nj] = 3; }
296 }
297 }
298 }
299 }
300
301 let mut route = vec![GridCell {
303 i: goal.i,
304 j: goal.j,
305 }];
306 let mut cur = (goal.i, goal.j);
307 while let Some(p) = parent[cur.0][cur.1] {
308 route.push(GridCell { i: p.0, j: p.1 });
309 cur = p;
310 }
311 route.reverse();
312
313 let joint_angles: Vec<[f64; 2]> = route
315 .iter()
316 .map(|cell| [grid.index_to_angle(cell.i), grid.index_to_angle(cell.j)])
317 .collect();
318
319 Some(ArmPath {
320 route_cells: route,
321 joint_angles,
322 })
323}
324
325pub fn plan_arm_path(
330 link_lengths: &[f64; 2],
331 obstacles: &[CircleObstacle],
332 start: GridCell,
333 goal: GridCell,
334 grid_size: usize,
335) -> Option<ArmPath> {
336 let grid = build_occupancy_grid(link_lengths, obstacles, grid_size);
337 astar_torus(&grid, start, goal)
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn test_detect_collision_hit() {
346 let a = [0.0, 0.0];
347 let b = [2.0, 0.0];
348 let obs = CircleObstacle::new(1.0, 0.0, 0.5);
349 assert!(detect_collision(&a, &b, &obs));
350 }
351
352 #[test]
353 fn test_detect_collision_miss() {
354 let a = [0.0, 0.0];
355 let b = [2.0, 0.0];
356 let obs = CircleObstacle::new(1.0, 2.0, 0.5);
357 assert!(!detect_collision(&a, &b, &obs));
358 }
359
360 #[test]
361 fn test_detect_collision_endpoint() {
362 let a = [0.0, 0.0];
363 let b = [1.0, 0.0];
364 let obs = CircleObstacle::new(1.0, 0.0, 0.1);
366 assert!(detect_collision(&a, &b, &obs));
367 }
368
369 #[test]
370 fn test_detect_collision_before_segment() {
371 let a = [1.0, 0.0];
372 let b = [2.0, 0.0];
373 let obs = CircleObstacle::new(0.0, 0.0, 0.5);
374 assert!(!detect_collision(&a, &b, &obs));
375 }
376
377 #[test]
378 fn test_detect_collision_degenerate_segment() {
379 let a = [0.0, 0.0];
380 let b = [0.0, 0.0];
381 let obs = CircleObstacle::new(0.0, 0.0, 0.5);
382 assert!(detect_collision(&a, &b, &obs));
383 }
384
385 #[test]
386 fn test_joint_space_grid_basic() {
387 let grid = JointSpaceGrid::new(10);
388 assert!(!grid.get(0, 0));
389 assert_eq!(grid.size, 10);
390 }
391
392 #[test]
393 fn test_grid_set_get() {
394 let mut grid = JointSpaceGrid::new(10);
395 grid.set(3, 5, true);
396 assert!(grid.get(3, 5));
397 assert!(!grid.get(5, 3));
398 }
399
400 #[test]
401 fn test_index_to_angle() {
402 let grid = JointSpaceGrid::new(100);
403 assert!((grid.index_to_angle(0) - (-PI)).abs() < 1e-10);
405 assert!(grid.index_to_angle(50).abs() < 1e-10);
407 }
408
409 #[test]
410 fn test_find_neighbors_middle() {
411 let neighbors = find_neighbors(5, 5, 10);
412 assert_eq!(neighbors, [(4, 5), (6, 5), (5, 4), (5, 6)]);
413 }
414
415 #[test]
416 fn test_find_neighbors_wrap() {
417 let neighbors = find_neighbors(0, 0, 10);
418 assert_eq!(neighbors, [(9, 0), (1, 0), (0, 9), (0, 1)]);
419 }
420
421 #[test]
422 fn test_heuristic_map_at_goal() {
423 let goal = GridCell { i: 5, j: 5 };
424 let hmap = calc_heuristic_map(10, goal);
425 assert!((hmap[5][5] - 0.0).abs() < 1e-10);
426 }
427
428 #[test]
429 fn test_heuristic_map_toroidal() {
430 let goal = GridCell { i: 0, j: 0 };
431 let hmap = calc_heuristic_map(10, goal);
432 assert!((hmap[9][9] - 2.0).abs() < 1e-10);
434 }
435
436 #[test]
437 fn test_astar_torus_empty_grid() {
438 let grid = JointSpaceGrid::new(20);
439 let start = GridCell { i: 2, j: 2 };
440 let goal = GridCell { i: 18, j: 18 };
441 let result = astar_torus(&grid, start, goal);
442 assert!(result.is_some());
443 let path = result.unwrap();
444 assert_eq!(path.route_cells.first().unwrap(), &start);
445 assert_eq!(path.route_cells.last().unwrap(), &goal);
446 assert_eq!(path.route_cells.len(), path.joint_angles.len());
447 }
448
449 #[test]
450 fn test_astar_torus_wrapping() {
451 let grid = JointSpaceGrid::new(10);
453 let start = GridCell { i: 0, j: 0 };
454 let goal = GridCell { i: 9, j: 9 };
455 let result = astar_torus(&grid, start, goal);
456 assert!(result.is_some());
457 let path = result.unwrap();
458 assert_eq!(path.route_cells.len(), 3); }
461
462 #[test]
463 fn test_astar_torus_blocked() {
464 let mut grid = JointSpaceGrid::new(5);
467 for di in [-1i32, 0, 1] {
468 for dj in [-1i32, 0, 1] {
469 if di == 0 && dj == 0 {
470 continue;
471 }
472 let ni = ((3 + di).rem_euclid(5)) as usize;
473 let nj = ((3 + dj).rem_euclid(5)) as usize;
474 grid.set(ni, nj, true);
475 }
476 }
477 let start = GridCell { i: 0, j: 0 };
478 let goal = GridCell { i: 3, j: 3 };
479 let result = astar_torus(&grid, start, goal);
480 assert!(result.is_none());
481 }
482
483 #[test]
484 fn test_build_occupancy_grid_no_obstacles() {
485 let link_lengths = [1.0, 1.0];
486 let grid = build_occupancy_grid(&link_lengths, &[], 20);
487 for i in 0..20 {
489 for j in 0..20 {
490 assert!(!grid.get(i, j));
491 }
492 }
493 }
494
495 #[test]
496 fn test_build_occupancy_grid_with_obstacle() {
497 let link_lengths = [1.0, 1.0];
498 let obstacles = vec![CircleObstacle::new(1.5, 0.0, 0.3)];
500 let grid = build_occupancy_grid(&link_lengths, &obstacles, 20);
501 assert!(grid.get(10, 10));
504 }
505
506 #[test]
507 fn test_plan_arm_path_simple() {
508 let link_lengths = [1.0, 1.0];
510 let start = GridCell { i: 5, j: 5 };
511 let goal = GridCell { i: 15, j: 15 };
512 let result = plan_arm_path(&link_lengths, &[], start, goal, 20);
513 assert!(result.is_some());
514 let path = result.unwrap();
515 assert!(!path.route_cells.is_empty());
516 assert_eq!(path.route_cells[0], start);
517 assert_eq!(*path.route_cells.last().unwrap(), goal);
518 }
519
520 #[test]
521 fn test_plan_arm_path_with_obstacles() {
522 let link_lengths = [1.0, 1.0];
523 let obstacles = vec![
524 CircleObstacle::new(1.75, 0.75, 0.6),
525 CircleObstacle::new(0.55, 1.5, 0.5),
526 ];
527 let grid = build_occupancy_grid(&link_lengths, &obstacles, 50);
528
529 let start = GridCell { i: 5, j: 25 };
531 let goal = GridCell { i: 30, j: 30 };
532
533 if !grid.get(start.i, start.j) && !grid.get(goal.i, goal.j) {
535 let result = astar_torus(&grid, start, goal);
536 if let Some(path) = result {
537 for cell in &path.route_cells {
539 assert!(!grid.get(cell.i, cell.j));
540 }
541 }
542 }
543 }
544
545 #[test]
546 fn test_arm_path_joint_angles_match_cells() {
547 let grid = JointSpaceGrid::new(20);
548 let start = GridCell { i: 3, j: 7 };
549 let goal = GridCell { i: 10, j: 15 };
550 let result = astar_torus(&grid, start, goal).unwrap();
551
552 for (cell, angles) in result.route_cells.iter().zip(result.joint_angles.iter()) {
553 let expected_theta1 = grid.index_to_angle(cell.i);
554 let expected_theta2 = grid.index_to_angle(cell.j);
555 assert!((angles[0] - expected_theta1).abs() < 1e-10);
556 assert!((angles[1] - expected_theta2).abs() < 1e-10);
557 }
558 }
559
560 #[test]
561 fn test_start_equals_goal() {
562 let grid = JointSpaceGrid::new(10);
563 let cell = GridCell { i: 5, j: 5 };
564 let result = astar_torus(&grid, cell, cell);
565 assert!(result.is_some());
566 let path = result.unwrap();
567 assert_eq!(path.route_cells.len(), 1);
568 }
569}