py_pathfinding/algorithms/
jps.rs

1// https://github.com/mikolalysenko/l1-path-finder
2
3// https://en.wikipedia.org/wiki/Jump_point_search
4use std::collections::BinaryHeap;
5
6use std::cmp::Ordering;
7use std::f32::consts::SQRT_2;
8use std::f32::EPSILON;
9use std::ops::Sub;
10
11use ndarray::Array;
12use ndarray::Array2;
13
14use fnv::FnvHashMap;
15// use fnv::FnvHasher;
16
17#[allow(dead_code)]
18fn absdiff<T>(x: T, y: T) -> T
19where
20    T: Sub<Output = T> + PartialOrd,
21{
22    if x < y {
23        y - x
24    } else {
25        x - y
26    }
27}
28
29fn manhattan_heuristic(source: &Point2d, target: &Point2d) -> f32 {
30    (absdiff(source.x, target.x) + absdiff(source.y, target.y)) as f32
31}
32
33static SQRT_2_MINUS_2: f32 = SQRT_2 - 2.0;
34
35fn octal_heuristic(source: &Point2d, target: &Point2d) -> f32 {
36    let dx = absdiff(source.x, target.x);
37    let dy = absdiff(source.y, target.y);
38    let min = std::cmp::min(dx, dy);
39    dx as f32 + dy as f32 + SQRT_2_MINUS_2 * min as f32
40}
41
42fn euclidean_heuristic(source: &Point2d, target: &Point2d) -> f32 {
43    let x = source.x as i32 - target.x as i32;
44    let xx = x * x;
45    let y = source.y as i32 - target.y as i32;
46    let yy = y * y;
47    let sum = xx + yy;
48    (sum as f32).sqrt()
49}
50
51//fn no_heuristic(_source: &Point2d, _target: &Point2d) -> f32 {
52//    0.0
53//}
54
55/// A direction that is required to be given to a jump point, so that the traversal function knows in which direction it needs to traverse
56#[derive(Debug, Copy, Clone, PartialEq)]
57struct Direction {
58    x: i32,
59    y: i32,
60}
61
62impl Direction {
63    /// Returns true if a direction is in diagonal direction
64    fn is_diagonal(self) -> bool {
65        match self {
66            // Non diagonal movement
67            Direction { x: 0, y: 1 }
68            | Direction { x: 1, y: 0 }
69            | Direction { x: -1, y: 0 }
70            | Direction { x: 0, y: -1 } => false,
71            _ => true,
72        }
73    }
74
75    /// Returns direction for 90 degree left turns
76    fn left(self) -> Direction {
77        match (self.x, self.y) {
78            (1, 0) => Direction { x: 0, y: 1 },
79            (0, 1) => Direction { x: -1, y: 0 },
80            (-1, 0) => Direction { x: 0, y: -1 },
81            (0, -1) => Direction { x: 1, y: 0 },
82            // Diagonal
83            (1, 1) => Direction { x: -1, y: 1 },
84            (-1, 1) => Direction { x: -1, y: -1 },
85            (-1, -1) => Direction { x: 1, y: -1 },
86            (1, -1) => Direction { x: 1, y: 1 },
87            _ => panic!("This shouldnt happen"),
88        }
89    }
90
91    /// Returns direction for 90 degree right turns
92    fn right(self) -> Direction {
93        match (self.x, self.y) {
94            (1, 0) => Direction { x: 0, y: -1 },
95            (0, 1) => Direction { x: 1, y: 0 },
96            (-1, 0) => Direction { x: 0, y: 1 },
97            (0, -1) => Direction { x: -1, y: 0 },
98            // Diagonal
99            (1, 1) => Direction { x: 1, y: -1 },
100            (-1, 1) => Direction { x: 1, y: 1 },
101            (-1, -1) => Direction { x: -1, y: 1 },
102            (1, -1) => Direction { x: -1, y: -1 },
103            _ => panic!("This shouldnt happen"),
104        }
105    }
106
107    /// Returns direction for 45 degree left turns
108    fn half_left(self) -> Direction {
109        match (self.x, self.y) {
110            (1, 0) => Direction { x: 1, y: 1 },
111            (0, 1) => Direction { x: -1, y: 1 },
112            (-1, 0) => Direction { x: -1, y: -1 },
113            (0, -1) => Direction { x: 1, y: -1 },
114            // Diagonal
115            (1, 1) => Direction { x: 0, y: 1 },
116            (-1, 1) => Direction { x: -1, y: 0 },
117            (-1, -1) => Direction { x: 0, y: -1 },
118            (1, -1) => Direction { x: 1, y: 0 },
119            _ => panic!("This shouldnt happen"),
120        }
121    }
122
123    /// Returns direction for 45 degree right turns
124    fn half_right(self) -> Direction {
125        match (self.x, self.y) {
126            (1, 0) => Direction { x: 1, y: -1 },
127            (0, 1) => Direction { x: 1, y: 1 },
128            (-1, 0) => Direction { x: -1, y: 1 },
129            (0, -1) => Direction { x: -1, y: -1 },
130            // Diagonal
131            (1, 1) => Direction { x: 1, y: 0 },
132            (-1, 1) => Direction { x: 0, y: 1 },
133            (-1, -1) => Direction { x: -1, y: 0 },
134            (1, -1) => Direction { x: 0, y: -1 },
135            _ => panic!("This shouldnt happen"),
136        }
137    }
138
139    /// Returns direction for 135 degree left turns
140    fn left135(self) -> Direction {
141        match (self.x, self.y) {
142            // Diagonal
143            (1, 1) => Direction { x: -1, y: 0 },
144            (-1, 1) => Direction { x: 0, y: -1 },
145            (-1, -1) => Direction { x: 1, y: 0 },
146            (1, -1) => Direction { x: 0, y: 1 },
147            _ => panic!("This shouldnt happen"),
148        }
149    }
150
151    /// Returns direction for 135 degree right turns
152    fn right135(self) -> Direction {
153        match (self.x, self.y) {
154            // Diagonal
155            (1, 1) => Direction { x: 0, y: -1 },
156            (-1, 1) => Direction { x: 1, y: 0 },
157            (-1, -1) => Direction { x: 0, y: 1 },
158            (1, -1) => Direction { x: -1, y: 0 },
159            _ => panic!("This shouldnt happen"),
160        }
161    }
162}
163
164/// A struct for saving coordinates
165#[derive(Debug, Hash, Eq, PartialEq, Copy, Clone)]
166pub struct Point2d {
167    x: usize,
168    y: usize,
169}
170
171impl Point2d {
172    pub fn new(x: usize, y: usize) -> Self {
173        Point2d { x, y }
174    }
175
176    pub fn unpack(&self) -> (usize, usize) {
177        (self.x, self.y)
178    }
179
180    /// Helper function for quickly summing a point with a direction
181    fn add_direction(&self, other: Direction) -> Point2d {
182        Point2d {
183            x: (self.x as i32 + other.x) as usize,
184            y: (self.y as i32 + other.y) as usize,
185        }
186    }
187
188    /// Returns the direction from self to target point
189    fn get_direction(&self, target: &Point2d) -> Direction {
190        let x: i32;
191        let y: i32;
192        match self.x.cmp(&target.x) {
193            Ordering::Greater => x = -1,
194            Ordering::Less => x = 1,
195            Ordering::Equal => x = 0,
196        }
197        match self.y.cmp(&target.y) {
198            Ordering::Greater => y = -1,
199            Ordering::Less => y = 1,
200            Ordering::Equal => y = 0,
201        }
202        Direction { x, y }
203    }
204}
205
206/// A jump point which contains information about the start location, direction it should traverse towards, cost to start, total cost
207#[derive(Debug)]
208struct JumpPoint {
209    start: Point2d,
210    direction: Direction,
211    cost_to_start: f32,
212    total_cost_estimate: f32,
213}
214
215/// A comparison method to compare two f32 numbers
216impl PartialEq for JumpPoint {
217    fn eq(&self, other: &Self) -> bool {
218        absdiff(self.total_cost_estimate, other.total_cost_estimate) < EPSILON
219    }
220}
221
222/// A comparison method to compare two f32 numbers
223impl PartialOrd for JumpPoint {
224    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
225        other
226            .total_cost_estimate
227            .partial_cmp(&self.total_cost_estimate)
228    }
229}
230
231/// The result of this implementation doesnt seem to matter - instead what matters, is that it is implemented at all
232impl Ord for JumpPoint {
233    fn cmp(&self, other: &Self) -> Ordering {
234        other
235            .total_cost_estimate
236            .partial_cmp(&self.total_cost_estimate)
237            .unwrap()
238    }
239}
240
241/// Same as Ord
242impl Eq for JumpPoint {}
243
244/// The pathfinder struct which can calculate paths on the grid
245pub struct PathFinder {
246    /// Pathfinder assumes that the borders of the grid are walls.
247    /// [
248    ///     [0, 0, 0, 0],
249    ///     [0, 1, 1, 0],
250    ///     [0, 1, 1, 0],
251    ///     [0, 0, 0, 0],
252    /// ]
253    grid: Array2<u8>,
254    /// May be 'manhattan', 'octal' or 'euclidean'. 'octal' should probably be the best choice here
255    heuristic: String,
256    jump_points: BinaryHeap<JumpPoint>,
257    /// Contains points which were already visited to construct the path once the goal is found
258    came_from: FnvHashMap<Point2d, Point2d>,
259}
260
261impl PathFinder {
262    /// Returns the answer to 'already visited?', if not visited it adds it to the dictionary
263    fn add_came_from(&mut self, p1: &Point2d, p2: &Point2d) -> bool {
264        if !self.came_from.contains_key(p1) {
265            self.came_from.insert(*p1, *p2);
266            return false;
267        }
268        true
269    }
270
271    /// Checks if the point is in the grid. Careful, the 2d grid needs to be similar to numpy arrays, so row major. Grid[y][x]
272    fn is_in_grid_bounds(&self, point: &Point2d) -> bool {
273        let dim = self.grid.raw_dim();
274        let (height, width) = (dim[0], dim[1]);
275        // No need to test for 0 <= point.x since usize is used
276        return point.x < width && point.y < height;
277    }
278
279    /// Checks if the point is in the grid. Careful, the 2d grid needs to be similar to numpy arrays, so row major. Grid[y][x]
280    fn is_in_grid(&self, point: &Point2d) -> bool {
281        self.grid[[point.y, point.x]] == 1
282    }
283
284    /// Returns an option of a Point2d if the point in that direction is not a wall.
285    fn new_point_in_grid(&self, point: &Point2d, direction: Direction) -> Option<Point2d> {
286        let new_point = point.add_direction(direction);
287        if self.is_in_grid(&new_point) {
288            return Some(new_point);
289        }
290        None
291    }
292
293    /// Checks if the target point has already been visited.
294    fn goal_reached(&self, target: &Point2d) -> bool {
295        self.came_from.contains_key(&target)
296    }
297
298    /// Construct the path from the came_from hashmap
299    /// 1)
300    ///     The path can be constructed as minimal as possible (leaving gaps in the path), e.g.
301    ///     [
302    ///         (0, 0), // Diagonal movement to top right
303    ///         (5, 5), // Vertical movement to the top
304    ///         (5, 7)
305    ///     ]
306    /// 2)
307    ///     The path can be constructed as fully, e.g.
308    ///     [
309    ///         (0, 0),
310    ///         (1, 1),
311    ///         (2, 2),
312    ///         (3, 3),
313    ///         (4, 4),
314    ///         (5, 5),
315    ///         (5, 6),
316    ///         (5, 7)
317    ///     ]
318    fn construct_path(
319        &self,
320        source: &Point2d,
321        target: &Point2d,
322        construct_full_path: bool,
323    ) -> Vec<Point2d> {
324        if construct_full_path {
325            let mut path: Vec<Point2d> = Vec::with_capacity(100);
326            let mut pos = *target;
327            path.push(pos);
328            while &pos != source {
329                let temp_target = *self.came_from.get(&pos).unwrap();
330                let dir = pos.get_direction(&temp_target);
331                let mut temp_pos = pos.add_direction(dir);
332                while temp_pos != temp_target {
333                    path.push(temp_pos);
334                    temp_pos = temp_pos.add_direction(dir);
335                }
336                pos = temp_target;
337            }
338            path.push(*source);
339            path.reverse();
340            path
341        } else {
342            let mut path: Vec<Point2d> = Vec::with_capacity(20);
343            path.push(*target);
344            let mut pos = self.came_from.get(target).unwrap();
345            while pos != source {
346                pos = self.came_from.get(&pos).unwrap();
347                path.push(*pos);
348            }
349            path.reverse();
350            path
351        }
352    }
353
354    /// The find path algorithm which creates 8 starting jump points around the start point, then only traverses those
355    pub fn find_path(&mut self, source: &Point2d, target: &Point2d) -> Vec<Point2d> {
356        if !self.is_in_grid_bounds(source) {
357            println!(
358                "Returning early, source position is not within grid bounds: {:?}",
359                source
360            );
361            return vec![];
362        }
363
364        if !self.is_in_grid_bounds(target) {
365            println!(
366                "Returning early, target position is not within grid bounds: {:?}",
367                target
368            );
369            return vec![];
370        }
371
372        if !self.is_in_grid(&source) {
373            println!(
374                "Returning early, source position is at the position of a wall: {:?}",
375                source
376            );
377            return vec![];
378        }
379        if !self.is_in_grid(&target) {
380            println!(
381                "Returning early, target position is at the position of a wall: {:?}",
382                target
383            );
384            return vec![];
385        }
386
387        let heuristic: fn(&Point2d, &Point2d) -> f32;
388        match self.heuristic.as_ref() {
389            "manhattan" => heuristic = manhattan_heuristic,
390            "octal" => heuristic = octal_heuristic,
391            "euclidean" => heuristic = euclidean_heuristic,
392            // Memory overflow!
393            // "none" => heuristic = no_heuristic,
394            _ => heuristic = octal_heuristic,
395        }
396
397        // Clear from last run
398        self.jump_points.clear();
399        // TODO hashmap clear seems super slow, replace with Array2<u8> that only stores the direction JPS came from
400        self.came_from.clear();
401
402        // Add 4 starting nodes (diagonal traversals) around source point
403        for dir in [
404            Direction { x: 1, y: 1 },
405            Direction { x: -1, y: 1 },
406            Direction { x: -1, y: -1 },
407            Direction { x: 1, y: -1 },
408        ]
409        .iter()
410        {
411            self.jump_points.push(JumpPoint {
412                start: *source,
413                direction: *dir,
414                cost_to_start: 0.0,
415                total_cost_estimate: 0.0 + heuristic(&source, target),
416            });
417        }
418
419        while let Some(JumpPoint {
420            start,
421            direction,
422            cost_to_start,
423            ..
424        }) = self.jump_points.pop()
425        {
426            if self.goal_reached(&target) {
427                return self.construct_path(source, target, false);
428            }
429
430            self.traverse(&start, direction, &target, cost_to_start, heuristic);
431        }
432
433        vec![]
434    }
435
436    /// Traverse in a direction (diagonal, horizontal, vertical) until a wall is reached.
437    /// If a jump point is encountered (after a wall on left or right side), add it to binary heap of jump points.
438    /// If the traversal is diagonal, always split off 45 degree left and right turns and traverse them immediately without adding them to binary heap beforehand.
439    fn traverse(
440        &mut self,
441        start: &Point2d,
442        direction: Direction,
443        target: &Point2d,
444        cost_to_start: f32,
445        heuristic: fn(&Point2d, &Point2d) -> f32,
446    ) {
447        // How far we moved from the start of the function call
448        let mut traversed_count: u32 = 0;
449        let add_nodes: Vec<(Direction, Direction)> = if direction.is_diagonal() {
450            // The first two entries will be checked for left_blocked and right_blocked, if a wall was encountered but that position is now free (forced neighbors?)
451            // If the vec has more than 2 elements, then the remaining will not be checked for walls (this is the case in diagonal movement where it forks off to horizontal+vertical movement)
452            // (blocked_direction from current_node, traversal_direction)
453            let (half_left, half_right) = (direction.half_left(), direction.half_right());
454            vec![
455                (direction.left135(), direction.left()),
456                (direction.right135(), direction.right()),
457                (half_left, half_left),
458                (half_right, half_right),
459            ]
460        } else {
461            vec![
462                (direction.left(), direction.half_left()),
463                (direction.right(), direction.half_right()),
464            ]
465        };
466        let mut current_point = *start;
467        // Stores wall status - if a side is no longer blocked: create jump point and fork path
468        let (mut left_blocked, mut right_blocked) = (false, false);
469        loop {
470            // Goal found, construct path
471            if current_point == *target {
472                self.add_came_from(&current_point, &start);
473                //                println!("Found goal: {:?} {:?}", current_point, direction);
474                //                println!("Size of open list: {:?}", self.jump_points.len());
475                //                println!("Size of came from: {:?}", self.came_from.len());
476                return;
477            }
478            // We loop over each direction that isnt the traversal direction
479            // For diagonal traversal this is 2 checks (left is wall, right is wall), and 2 forks (horizontal+vertical movement)
480            // For non-diagonal traversal this is only checking if there are walls on the side
481            for (index, (check_dir, traversal_dir)) in add_nodes.iter().enumerate() {
482                // Check if in that direction is a wall
483                let check_point_is_in_grid =
484                    self.is_in_grid(&current_point.add_direction(*check_dir));
485
486                if (index == 0 && left_blocked || index == 1 && right_blocked || index > 1)
487                    && traversed_count != 0
488                    && check_point_is_in_grid
489                {
490                    // If there is no longer a wall in that direction, add jump point to binary heap
491                    let new_cost_to_start = if traversal_dir.is_diagonal() {
492                        cost_to_start + SQRT_2 * traversed_count as f32
493                    } else {
494                        cost_to_start + traversed_count as f32
495                    };
496
497                    if index < 2 {
498                        if self.add_came_from(&current_point, &start) {
499                            // We were already at this point because a new jump point was created here - this means we either are going in a circle or we come from a path that is longer?
500                            break;
501                        }
502                        // Add forced neighbor to min-heap
503                        self.jump_points.push(JumpPoint {
504                            start: current_point,
505                            direction: *traversal_dir,
506                            cost_to_start: new_cost_to_start,
507                            total_cost_estimate: new_cost_to_start
508                                + heuristic(&current_point, target),
509                        });
510
511                        // Mark the side no longer as blocked
512                        if index == 0 {
513                            left_blocked = false;
514                        } else {
515                            right_blocked = false;
516                        }
517                    // If this is non-diagonal traversal, this is used to store a 'came_from' point
518                    } else {
519                        // If this is diagonal traversal, instantly traverse the non-diagonal directions without adding them to min-heap first
520                        self.traverse(
521                            &current_point,
522                            *traversal_dir,
523                            target,
524                            new_cost_to_start,
525                            heuristic,
526                        );
527                        // The non-diagonal traversal created a jump point and added it to the min-heap, so to backtrack from target/goal, we need to add this position to 'came_from'
528                        self.add_came_from(&current_point, &start);
529                    }
530                } else if index == 0 && !check_point_is_in_grid {
531                    // If this direction (left) has now a wall, mark as blocked
532                    left_blocked = true;
533                } else if index == 1 && !check_point_is_in_grid {
534                    // If this direction (right) has now a wall, mark as blocked
535                    right_blocked = true
536                }
537            }
538
539            current_point = current_point.add_direction(direction);
540            if !self.is_in_grid(&current_point) {
541                // Next traversal point is a wall - this traversal is done
542                break;
543            }
544            // Next traversal point is pathable
545            traversed_count += 1;
546        }
547    }
548
549    /// Quickly create new pathfinder
550    pub fn new(grid: Array2<u8>, heuristic: &String) -> Self {
551        PathFinder {
552            grid,
553            heuristic: heuristic.clone(),
554            jump_points: BinaryHeap::with_capacity(50),
555            came_from: FnvHashMap::default(),
556        }
557    }
558
559    /// A helper function to set up a square grid, border of the grid is set to 0, other values are set to 1
560    pub fn create_square_grid(size: usize) -> Array2<u8> {
561        // https://stackoverflow.com/a/59043086/10882657
562        let mut ndarray = Array2::<u8>::ones((size, size));
563        // Set boundaries
564        for y in 0..size {
565            ndarray[[y, 0]] = 0;
566            ndarray[[y, size - 1]] = 0;
567        }
568        for x in 0..size {
569            ndarray[[0, x]] = 0;
570            ndarray[[size - 1, x]] = 0;
571        }
572        ndarray
573    }
574}
575
576/// A helper function to run the pathfinding algorithm
577pub fn jps_test(pf: &mut PathFinder, source: &Point2d, target: &Point2d) -> Vec<Point2d> {
578    pf.find_path(&source, &target)
579}
580
581
582use std::fs::File;
583use std::io::Read;
584
585/// A helper function to read the grid from file
586pub fn read_grid_from_file(path: String) -> Result<(Array2<u8>, u32, u32), std::io::Error> {
587    let mut file = File::open(path).unwrap();
588    let mut data = String::new();
589
590    file.read_to_string(&mut data).unwrap();
591    let mut height = 0;
592    let mut width = 0;
593    // Create one dimensional vec
594    let mut my_vec = Vec::new();
595    for line in data.lines() {
596        width = line.len();
597        height += 1;
598        for char in line.chars() {
599            my_vec.push(char as u8 - 48);
600        }
601    }
602
603    let array = Array::from(my_vec).into_shape((height, width)).unwrap();
604    Ok((array, height as u32, width as u32))
605}
606
607#[cfg(test)] // Only compiles when running tests
608mod tests {
609    use super::*;
610    #[allow(unused_imports)]
611    use test::Bencher;
612
613    #[bench]
614    fn bench_jps_test_from_file(b: &mut Bencher) {
615        // Setup
616        let result = read_grid_from_file(String::from("AutomatonLE.txt"));
617        let (array, _height, _width) = result.unwrap();
618        // Spawn to spawn
619        let source = Point2d { x: 32, y: 51 };
620        let target = Point2d { x: 150, y: 129 };
621
622        // Main ramp to main ramp
623        // let source = Point2d { x: 32, y: 51 };
624        // let target = Point2d { x: 150, y: 129 };
625        let mut pf = PathFinder::new(array, &String::from("octal"));
626        let path = jps_test(&mut pf, &source, &target);
627        assert_ne!(0, path.len());
628        // Run bench
629        b.iter(|| jps_test(&mut pf, &source, &target));
630    }
631
632    #[bench]
633    fn bench_jps_test_from_file_no_path(b: &mut Bencher) {
634        // Setup
635        let result = read_grid_from_file(String::from("AutomatonLE.txt"));
636        let (mut array, _height, _width) = result.unwrap();
637        // Spawn to spawn
638        let source = Point2d { x: 32, y: 51 };
639        let target = Point2d { x: 150, y: 129 };
640
641        // Block entrance to main base
642        for x in 145..=150 {
643            for y in 129..=135 {
644                array[[y, x]] = 0;
645            }
646        }
647
648        let mut pf = PathFinder::new(array, &String::from("octal"));
649        let path = jps_test(&mut pf, &source, &target);
650        assert_eq!(0, path.len());
651        // Run bench
652        b.iter(|| jps_test(&mut pf, &source, &target));
653    }
654
655    #[bench]
656    fn bench_jps_test_out_of_bounds1(b: &mut Bencher) {
657        let grid = PathFinder::create_square_grid(30);
658        let mut pf = PathFinder::new(grid, &String::from("octal"));
659        let source: Point2d = Point2d { x: 500, y: 5 };
660        let target: Point2d = Point2d { x: 10, y: 12 };
661        let path = jps_test(&mut pf, &source, &target);
662        assert_eq!(0, path.len());
663        b.iter(|| jps_test(&mut pf, &source, &target));
664    }
665
666    #[bench]
667    fn bench_jps_test_out_of_bounds2(b: &mut Bencher) {
668        let grid = PathFinder::create_square_grid(30);
669        let mut pf = PathFinder::new(grid, &String::from("octal"));
670        let source: Point2d = Point2d { x: 5, y: 5 };
671        let target: Point2d = Point2d { x: 500, y: 12 };
672        let path = jps_test(&mut pf, &source, &target);
673        assert_eq!(0, path.len());
674        b.iter(|| jps_test(&mut pf, &source, &target));
675    }
676
677    #[bench]
678    fn bench_jps_test(b: &mut Bencher) {
679        let grid = PathFinder::create_square_grid(30);
680        let mut pf = PathFinder::new(grid, &String::from("octal"));
681        let source: Point2d = Point2d { x: 5, y: 5 };
682        let target: Point2d = Point2d { x: 10, y: 12 };
683        b.iter(|| jps_test(&mut pf, &source, &target));
684    }
685}