Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standard Moving AI pathfinding benchmarks #8

Merged
merged 14 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
target/
.idea
scenarios/
maps/
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ bench = false

[dev-dependencies]
criterion = { version = "0.4", features = ["html_reports"] }
grid_pathfinding_benchmark = { path = "grid_pathfinding_benchmark" }
rand = "0.8.5"

[[bench]]
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
A grid-based pathfinding system. Implements [Jump Point Search](https://en.wikipedia.org/wiki/Jump_point_search) with
[improved pruning rules](https://www.researchgate.net/publication/287338108_Improving_jump_point_search) for speedy pathfinding. Pre-computes
[connected components](https://en.wikipedia.org/wiki/Component_(graph_theory))
to avoid flood-filling behaviour if no path exists. Both [4-neighborhood](https://en.wikipedia.org/wiki/Von_Neumann_neighborhood) and [8-neighborhood](https://en.wikipedia.org/wiki/Moore_neighborhood) grids are supported and a custom variant of JPS is implemented for the 4-neighborhood.
to avoid flood-filling behavior if no path exists. Both [4-neighborhood](https://en.wikipedia.org/wiki/Von_Neumann_neighborhood) and [8-neighborhood](https://en.wikipedia.org/wiki/Moore_neighborhood) grids are supported and a custom variant of JPS is implemented for the 4-neighborhood.

### Example
Below a [simple 8-grid example](examples/simple_8.rs) is given which illustrates how to set a basic problem and find a path.
Below a [simple 8-grid example](examples/simple_8.rs) is given, illustrating how to set a basic problem and find a path.
```rust,no_run
use grid_pathfinding::PathingGrid;
use grid_util::grid::Grid;
Expand Down Expand Up @@ -36,21 +36,23 @@ fn main() {
println!("Path:");
for p in path {
println!("{:?}", p);
}
}
}
```
This assumes an 8-neighborhood, which is the default grid type. The same problem can be solved for a 4-neighborhood, disallowing diagonal moves, by adding the line
```rust,no_run
pathing_grid.allow_diagonal_move = false;
```
prior to component generation, which is done in example [simple_4](examples/simple_4.rs).
before component generation, which is done in example [simple_4](examples/simple_4.rs).



See [examples](examples/) for finding paths with multiple goals and generating waypoints instead of full paths.

### Benchmarks
To run a set of benchmarks, use `cargo bench`. A baseline can be set using
The system can be benchmarked using scenarios from the [Moving AI 2D pathfinding benchmarks](https://movingai.com/benchmarks/grids.html). The [grid_pathfinding_benchmark](grid_pathfinding_benchmark) utility crate provides general support for loading these files. The default benchmark executed using `cargo bench` runs three scenario sets from the [Dragon Age: Origins](https://movingai.com/benchmarks/dao/index.html): `dao/arena`, `dao/den312` and `dao/arena2` (or `dao/den009d` when using the rectilinear algorithm). Running these requires the corresponding map and scenario files to be saved in folders called `maps/dao` and `scenarios/dao`.

A baseline can be set using
```bash
cargo bench -- --save-baseline main
```
Expand All @@ -59,6 +61,10 @@ New runs can be compared to this baseline using
cargo bench -- --baseline main
```

### Performance
Using an i5-6600 quad-core running at 3.3 GHz, running the `dao/arena2` set takes 134 ms using JPS allowing diagonals and with improved pruning disabled. Using default neighbor generation as in normal A* (enabled by setting `GRAPH_PRUNING = false`) makes this take 1.37 s, a factor 10 difference. As a rule, the relative difference increases as maps get larger, with the `dao/arena` set (a smaller map) taking 846 us and 1.34 ms respectively with and without pruning.

An existing C++ [JPS implementation](https://github.com/nathansttt/hog2) runs the same scenarios in about 60 ms. The fastest known solver is the [l1-path-finder](https://mikolalysenko.github.io/l1-path-finder/www/) (implemented in Javascript) which can do this in only 38 ms using A* with landmarks (for a 4-neighborhood). This indicates that there is still a lot of room for improvement in terms of search speed.

### Goal of crate
The long-term goal of this crate is to provide a fast off-the-shelf pathfinding implementation for grids.
92 changes: 23 additions & 69 deletions benches/comparison_bench.rs
Original file line number Diff line number Diff line change
@@ -1,83 +1,37 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use grid_pathfinding::PathingGrid;
use grid_pathfinding_benchmark::*;
use grid_util::grid::Grid;
use grid_util::point::Point;
use rand::prelude::*;

fn random_grid(
n: usize,
rng: &mut StdRng,
allow_diag: bool,
pruning: bool,
fill_rate: f64,
) -> PathingGrid {
let mut pathing_grid: PathingGrid = PathingGrid::new(n, n, false);
pathing_grid.allow_diagonal_move = allow_diag;
pathing_grid.improved_pruning = pruning;
for x in 0..pathing_grid.width() {
for y in 0..pathing_grid.height() {
pathing_grid.set(x, y, rng.gen_bool(fill_rate))
}
}
pathing_grid.generate_components();
pathing_grid
}
fn random_grid_point(grid: &PathingGrid, rng: &mut StdRng) -> Point {
Point::new(
rng.gen_range(0..grid.width()) as i32,
rng.gen_range(0..grid.height()) as i32,
)
}

fn test(pathing_grid: &PathingGrid, start: Point, end: Point) -> Option<Vec<Point>> {
black_box(pathing_grid.get_path_single_goal(start, end, false))
}

fn criterion_benchmark(c: &mut Criterion) {
fn dao_bench(c: &mut Criterion) {
for (allow_diag, pruning) in [(false, false), (true, false), (true, true)] {
const N: usize = 64;
const N_GRIDS: usize = 1000;
const N_PAIRS: usize = 1000;
let mut rng = StdRng::seed_from_u64(0);
let mut random_grids: Vec<PathingGrid> = Vec::new();
for _ in 0..N_GRIDS {
random_grids.push(random_grid(N, &mut rng, allow_diag, pruning, 0.4))
}
let bench_set = if allow_diag {
["dao/arena", "dao/den312d", "dao/arena2"]
} else {
["dao/arena", "dao/den009d", "dao/den312d"]
};
for name in bench_set {
let (bool_grid, scenarios) = get_benchmark(name.to_owned());
let mut pathing_grid: PathingGrid =
PathingGrid::new(bool_grid.width, bool_grid.height, true);
pathing_grid.grid = bool_grid.clone();
pathing_grid.allow_diagonal_move = allow_diag;
pathing_grid.improved_pruning = pruning;
pathing_grid.update_all_neighbours();
pathing_grid.generate_components();
let diag_str = if allow_diag { "8-grid" } else { "4-grid" };
let improved_str = if pruning { " (improved pruning)" } else { "" };

let start = Point::new(0, 0);
let end = Point::new(N as i32 - 1, N as i32 - 1);
let diag_str = if allow_diag { "8-grid" } else { "4-grid" };
let improved_str = if pruning { " (improved pruning)" } else { "" };
c.bench_function(
format!("1000 random 64x64 {diag_str}s{improved_str}").as_str(),
|b| {
c.bench_function(format!("{name}, {diag_str}{improved_str}").as_str(), |b| {
b.iter(|| {
for grid in &random_grids {
test(grid, start, end);
for (start, end) in &scenarios {
black_box(pathing_grid.get_path_single_goal(*start, *end, false));
}
})
},
);
let grid = &random_grids[0];
let mut random_pairs: Vec<(Point, Point)> = Vec::new();
for _ in 0..N_PAIRS {
random_pairs.push((
random_grid_point(&grid, &mut rng),
random_grid_point(&grid, &mut rng),
))
});
}
c.bench_function(
format!("1000 random start goal pairs on a 64x64 {diag_str}{improved_str}").as_str(),
|b| {
b.iter(|| {
for (start, end) in &random_pairs {
test(&grid, start.clone(), end.clone());
}
})
},
);
}
}

criterion_group!(benches, criterion_benchmark);
criterion_group!(benches, dao_bench);
criterion_main!(benches);
33 changes: 33 additions & 0 deletions benches/single_bench.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use grid_pathfinding::PathingGrid;
use grid_pathfinding_benchmark::*;
use grid_util::grid::Grid;

fn dao_bench_single(c: &mut Criterion) {
for (allow_diag, pruning) in [(true, false)] {
let bench_set = ["dao/arena"];
for name in bench_set {
let (bool_grid, scenarios) = get_benchmark(name.to_owned());
let mut pathing_grid: PathingGrid =
PathingGrid::new(bool_grid.width, bool_grid.height, true);
pathing_grid.grid = bool_grid.clone();
pathing_grid.allow_diagonal_move = allow_diag;
pathing_grid.improved_pruning = pruning;
pathing_grid.update_all_neighbours();
pathing_grid.generate_components();
let diag_str = if allow_diag { "8-grid" } else { "4-grid" };
let improved_str = if pruning { " (improved pruning)" } else { "" };

c.bench_function(format!("{name}, {diag_str}{improved_str}").as_str(), |b| {
b.iter(|| {
for (start, end) in &scenarios {
black_box(pathing_grid.get_path_single_goal(*start, *end, false));
}
})
});
}
}
}

criterion_group!(benches, dao_bench_single);
criterion_main!(benches);
43 changes: 43 additions & 0 deletions examples/benchmark_runner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use grid_pathfinding::PathingGrid;
use grid_pathfinding_benchmark::*;
use grid_util::grid::Grid;
use grid_util::point::Point;
use std::time::{Duration, Instant};

fn main() {
let benchmark_names = get_benchmark_names();
let mut total_time = Duration::ZERO;
for name in benchmark_names {
println!("Benchmark name: {}", name);

let (bool_grid, scenarios) = get_benchmark(name);
// for (allow_diag, pruning) in [(false, false), (true, false), (true, true)] {
for (allow_diag, pruning) in [(true, false)] {
let mut pathing_grid: PathingGrid =
PathingGrid::new(bool_grid.width, bool_grid.height, true);
pathing_grid.grid = bool_grid.clone();
pathing_grid.allow_diagonal_move = allow_diag;
pathing_grid.improved_pruning = pruning;
pathing_grid.update_all_neighbours();
pathing_grid.generate_components();
let number_of_scenarios = scenarios.len() as u32;
let before = Instant::now();
run_scenarios(&pathing_grid, &scenarios);
let elapsed = before.elapsed();
println!(
"\tElapsed time: {:.2?}; per scenario: {:.2?}",
elapsed,
elapsed / number_of_scenarios
);
total_time += elapsed;
}
}
println!("\tTotal benchmark time: {:.2?}", total_time);
}

pub fn run_scenarios(pathing_grid: &PathingGrid, scenarios: &Vec<(Point, Point)>) {
for (start, goal) in scenarios {
let path: Option<Vec<Point>> = pathing_grid.get_waypoints_single_goal(*start, *goal, false);
assert!(path.is_some());
}
}
19 changes: 19 additions & 0 deletions grid_pathfinding_benchmark/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "grid_pathfinding_benchmark"
version = "0.1.0"
authors = ["Thom van der Woude <[email protected]>"]
edition = "2021"
description = "Helper crate for loading Moving AI pathfinding benchmarks"
keywords = ["pathfinding","grid","benchmark"]
categories = ["game-development","simulation","algorithms"]
license = "MIT"
repository = "https://github.com/tbvanderwoude/grid_pathfinding"
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
grid_util = "0.1.1"
criterion = { version = "0.4", features = ["html_reports"] }
csv = "1.3.0"
serde = "1.0.204"
walkdir = "2.5.0"
2 changes: 2 additions & 0 deletions grid_pathfinding_benchmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# grid_pathfinding_benchmark
Helper crate for loading Moving AI pathfinding benchmarks.
Loading