From b0c84b39a51a47b14d0707214bbe255f2a132fa4 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 3 Aug 2024 19:00:50 +0200 Subject: [PATCH 01/14] Added prototype for running standard Moving AI pathfinding benchmarks --- .gitignore | 2 + Cargo.lock | 23 +++++++ Cargo.toml | 2 + examples/standard_benchmark.rs | 110 +++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+) mode change 100755 => 100644 Cargo.toml create mode 100644 examples/standard_benchmark.rs diff --git a/.gitignore b/.gitignore index 15aedc7..bb060d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ target/ .idea +scenarios/ +maps/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 2cd273f..5a8b3c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,6 +185,27 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + [[package]] name = "either" version = "1.13.0" @@ -228,6 +249,7 @@ name = "grid_pathfinding" version = "0.2.0" dependencies = [ "criterion", + "csv", "fxhash", "grid_util", "indexmap 2.2.6", @@ -236,6 +258,7 @@ dependencies = [ "num-traits", "petgraph", "rand", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml old mode 100755 new mode 100644 index 1de17e5..f9740e3 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ bench = false [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } rand = "0.8.5" +csv = "1.3.0" +serde = "1.0.204" [[bench]] name = "comparison_bench" diff --git a/examples/standard_benchmark.rs b/examples/standard_benchmark.rs new file mode 100644 index 0000000..8bf26c0 --- /dev/null +++ b/examples/standard_benchmark.rs @@ -0,0 +1,110 @@ +use csv::ReaderBuilder; +use grid_pathfinding::PathingGrid; +use grid_util::grid::Grid; +use grid_util::point::Point; +use serde::Deserialize; +use std::error::Error; +use std::fs::{self, File}; +use std::io::{self, BufRead}; +use std::path::Path; +use std::time::Instant; + +#[derive(Debug, Deserialize)] +struct Scenario { + id: u32, + file_name: String, + w: u32, + h: u32, + x1: u32, + y1: u32, + x2: u32, + y2: u32, + distance: f64, +} + +fn main() { + let paths = fs::read_dir("./maps").unwrap(); + + for path in paths { + let filename = path.unwrap().file_name(); + let name = filename.to_str().unwrap(); + println!("Name: {}", name); + let map_str = fs::read_to_string(Path::new(&format!("./maps/{}", name))) + .expect("Could not read scenario file"); + + let file = File::open(Path::new(&format!("./scenarios/{}.scen", name))) + .expect("Could not open scenario file"); + + // Create a buffer reader to read lines + let reader = io::BufReader::new(file); + let mut lines = reader.lines(); + + // Skip the first line + lines.next(); + + // Create a CSV reader with tab delimiter from remaining lines + let remaining_data = lines.collect::, _>>().unwrap().join("\n"); + + let mut csv_reader = ReaderBuilder::new() + .delimiter(b'\t') + .has_headers(false) + // .flexible(true) + .from_reader(remaining_data.as_bytes()); + // Initialize an empty vector to store the parsed data + let mut data_array: Vec<(Point, Point)> = Vec::new(); + + // Iterate over the records in the file + for result in csv_reader.deserialize() { + let record: Scenario = result.expect("Could not parse scenario record"); + let start = Point::new(record.y1 as i32, record.x1 as i32); + let goal = Point::new(record.y2 as i32, record.x2 as i32); + data_array.push((start, goal)); + } + + let lines: Vec<&str> = map_str.lines().collect(); + let parse_line = |line: &str| -> usize { + line.split_once(' ') + .unwrap() + .1 + .parse::() + .expect("Could not parse value") + }; + + let w = parse_line(&lines[1]); + let h = parse_line(&lines[2]); + + let offset = 4; + // 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(w, h, false); + pathing_grid.allow_diagonal_move = allow_diag; + pathing_grid.improved_pruning = pruning; + for y in 0..pathing_grid.height() { + for x in 0..pathing_grid.width() { + // Not sure why x, y have to be swapped here... + let tile_val = lines[offset + x].as_bytes()[y]; + let val = ![b'.', b'G'].contains(&tile_val); + pathing_grid.set(x, y, val); + } + } + + pathing_grid.generate_components(); + let number_of_scenarios = data_array.len() as u32; + let before = Instant::now(); + run_scenarios(&pathing_grid, &data_array); + let elapsed = before.elapsed(); + println!( + "\tElapsed time: {:.2?}; per scenario: {:.2?}", + elapsed, + elapsed / number_of_scenarios + ); + } + } +} + +pub fn run_scenarios(pathing_grid: &PathingGrid, scenarios: &Vec<(Point, Point)>) { + for (start, goal) in scenarios { + let path: Option> = pathing_grid.get_waypoints_single_goal(*start, *goal, false); + assert!(path.is_some()); + } +} From 2b90f08640caae06f36e3eaf687bdee2c8c63cf6 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Mon, 5 Aug 2024 10:46:08 +0200 Subject: [PATCH 02/14] Created load_benchmark function and introduced update_all_neighbours to PathingGrid for easier use existing BoolGrid --- examples/standard_benchmark.rs | 117 ++++++++++++++++++--------------- src/lib.rs | 8 +++ 2 files changed, 71 insertions(+), 54 deletions(-) diff --git a/examples/standard_benchmark.rs b/examples/standard_benchmark.rs index 8bf26c0..c3d36f9 100644 --- a/examples/standard_benchmark.rs +++ b/examples/standard_benchmark.rs @@ -2,6 +2,7 @@ use csv::ReaderBuilder; use grid_pathfinding::PathingGrid; use grid_util::grid::Grid; use grid_util::point::Point; +use grid_util::BoolGrid; use serde::Deserialize; use std::error::Error; use std::fs::{self, File}; @@ -22,76 +23,84 @@ struct Scenario { distance: f64, } -fn main() { - let paths = fs::read_dir("./maps").unwrap(); +fn load_benchmark(name: &str) -> (BoolGrid, Vec<(Point, Point)>) { + let map_str = fs::read_to_string(Path::new(&format!("./maps/{}.map", name))) + .expect("Could not read scenario file"); - for path in paths { - let filename = path.unwrap().file_name(); - let name = filename.to_str().unwrap(); - println!("Name: {}", name); - let map_str = fs::read_to_string(Path::new(&format!("./maps/{}", name))) - .expect("Could not read scenario file"); + let file = File::open(Path::new(&format!("./scenarios/{}.map.scen", name))) + .expect("Could not open scenario file"); - let file = File::open(Path::new(&format!("./scenarios/{}.scen", name))) - .expect("Could not open scenario file"); + // Create a buffer reader to read lines + let reader = io::BufReader::new(file); + let mut lines = reader.lines(); - // Create a buffer reader to read lines - let reader = io::BufReader::new(file); - let mut lines = reader.lines(); + // Skip the first line + lines.next(); - // Skip the first line - lines.next(); + // Create a CSV reader with tab delimiter from remaining lines + let remaining_data = lines.collect::, _>>().unwrap().join("\n"); - // Create a CSV reader with tab delimiter from remaining lines - let remaining_data = lines.collect::, _>>().unwrap().join("\n"); + let mut csv_reader = ReaderBuilder::new() + .delimiter(b'\t') + .has_headers(false) + // .flexible(true) + .from_reader(remaining_data.as_bytes()); + // Initialize an empty vector to store the parsed data + let mut data_array: Vec<(Point, Point)> = Vec::new(); - let mut csv_reader = ReaderBuilder::new() - .delimiter(b'\t') - .has_headers(false) - // .flexible(true) - .from_reader(remaining_data.as_bytes()); - // Initialize an empty vector to store the parsed data - let mut data_array: Vec<(Point, Point)> = Vec::new(); + // Iterate over the records in the file + for result in csv_reader.deserialize() { + let record: Scenario = result.expect("Could not parse scenario record"); + let start = Point::new(record.y1 as i32, record.x1 as i32); + let goal = Point::new(record.y2 as i32, record.x2 as i32); + data_array.push((start, goal)); + } - // Iterate over the records in the file - for result in csv_reader.deserialize() { - let record: Scenario = result.expect("Could not parse scenario record"); - let start = Point::new(record.y1 as i32, record.x1 as i32); - let goal = Point::new(record.y2 as i32, record.x2 as i32); - data_array.push((start, goal)); - } + let lines: Vec<&str> = map_str.lines().collect(); + let parse_line = |line: &str| -> usize { + line.split_once(' ') + .unwrap() + .1 + .parse::() + .expect("Could not parse value") + }; - let lines: Vec<&str> = map_str.lines().collect(); - let parse_line = |line: &str| -> usize { - line.split_once(' ') - .unwrap() - .1 - .parse::() - .expect("Could not parse value") - }; + let w = parse_line(&lines[1]); + let h = parse_line(&lines[2]); - let w = parse_line(&lines[1]); - let h = parse_line(&lines[2]); + let offset = 4; + let mut bool_grid: BoolGrid = BoolGrid::new(w, h, false); + for y in 0..bool_grid.height() { + for x in 0..bool_grid.width() { + // Not sure why x, y have to be swapped here... + let tile_val = lines[offset + x].as_bytes()[y]; + let val = ![b'.', b'G'].contains(&tile_val); + bool_grid.set(x, y, val); + } + } + (bool_grid, data_array) +} +fn main() { + let paths = fs::read_dir("./maps").unwrap(); + + for path in paths { + let filename = path.unwrap().file_name(); + let name = filename.to_str().unwrap().split_once('.').unwrap().0; + println!("Name: {}", name); - let offset = 4; + let (bool_grid, scenarios) = load_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(w, h, 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; - for y in 0..pathing_grid.height() { - for x in 0..pathing_grid.width() { - // Not sure why x, y have to be swapped here... - let tile_val = lines[offset + x].as_bytes()[y]; - let val = ![b'.', b'G'].contains(&tile_val); - pathing_grid.set(x, y, val); - } - } - + pathing_grid.compute_all_neighbours(); pathing_grid.generate_components(); - let number_of_scenarios = data_array.len() as u32; + let number_of_scenarios = scenarios.len() as u32; let before = Instant::now(); - run_scenarios(&pathing_grid, &data_array); + run_scenarios(&pathing_grid, &scenarios); let elapsed = before.elapsed(); println!( "\tElapsed time: {:.2?}; per scenario: {:.2?}", diff --git a/src/lib.rs b/src/lib.rs index 78dd004..1bca388 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -444,6 +444,14 @@ impl PathingGrid { self.generate_components(); } } + + pub fn update_all_neighbours(&mut self) { + for x in 0..self.width() { + for y in 0..self.height() { + self.update_neighbours(x as i32, y as i32, self.get(x, y)); + } + } + } /// Generates a new [UnionFind] structure and links up grid neighbours to the same components. pub fn generate_components(&mut self) { let w = self.grid.width; From 860f7cfc18463b03fbd5e30325018025865e2a1c Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Mon, 5 Aug 2024 11:16:58 +0200 Subject: [PATCH 03/14] Refactored standard_benchmark, supporting maps and scenarios in folders and making loading of benchmark far easier --- Cargo.lock | 1 + Cargo.toml | 1 + examples/standard_benchmark.rs | 42 ++++++++++++++++++++++++++-------- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a8b3c8..577ee22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,6 +259,7 @@ dependencies = [ "petgraph", "rand", "serde", + "walkdir", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f9740e3..514aabf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ criterion = { version = "0.4", features = ["html_reports"] } rand = "0.8.5" csv = "1.3.0" serde = "1.0.204" +walkdir = "2.5.0" [[bench]] name = "comparison_bench" diff --git a/examples/standard_benchmark.rs b/examples/standard_benchmark.rs index c3d36f9..e5b5779 100644 --- a/examples/standard_benchmark.rs +++ b/examples/standard_benchmark.rs @@ -8,7 +8,8 @@ use std::error::Error; use std::fs::{self, File}; use std::io::{self, BufRead}; use std::path::Path; -use std::time::Instant; +use std::time::{Duration, Instant}; +use walkdir::WalkDir; #[derive(Debug, Deserialize)] struct Scenario { @@ -80,15 +81,36 @@ fn load_benchmark(name: &str) -> (BoolGrid, Vec<(Point, Point)>) { } (bool_grid, data_array) } -fn main() { - let paths = fs::read_dir("./maps").unwrap(); - for path in paths { - let filename = path.unwrap().file_name(); - let name = filename.to_str().unwrap().split_once('.').unwrap().0; - println!("Name: {}", name); +fn get_benchmark_names() -> Vec { + let root = Path::new("maps/"); + let root = root + .canonicalize() + .expect("Failed to canonicalize root path"); + let mut names = Vec::new(); + for entry in WalkDir::new(&root).into_iter().skip(2) { + let path_str = entry.expect("Could not get dir entry"); + let name = path_str + .path() + .strip_prefix(&root) + .unwrap() + .to_str() + .unwrap() + .split_once('.') + .unwrap() + .0; + names.push(name.to_owned()); + } + names +} + +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) = load_benchmark(name); + let (bool_grid, scenarios) = load_benchmark(name.as_str()); // for (allow_diag, pruning) in [(false, false), (true, false), (true, true)] { for (allow_diag, pruning) in [(true, false)] { let mut pathing_grid: PathingGrid = @@ -96,7 +118,7 @@ fn main() { pathing_grid.grid = bool_grid.clone(); pathing_grid.allow_diagonal_move = allow_diag; pathing_grid.improved_pruning = pruning; - pathing_grid.compute_all_neighbours(); + pathing_grid.update_all_neighbours(); pathing_grid.generate_components(); let number_of_scenarios = scenarios.len() as u32; let before = Instant::now(); @@ -107,8 +129,10 @@ fn main() { 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)>) { From 62cf2435341b6d614e9fa9fee8d843e5d5acf2e6 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Mon, 5 Aug 2024 12:41:26 +0200 Subject: [PATCH 04/14] Created utility crate grid_pathfinding_benchmark for loading benchmarks and added dao_benchmark to criterion benchmark --- Cargo.lock | 11 ++++- Cargo.toml | 4 +- examples/benchmark_runner.rs | 43 ++++++++++++++++++ grid_pathfinding_benchmark/Cargo.toml | 19 ++++++++ grid_pathfinding_benchmark/README.md | 2 + .../src/lib.rs | 45 +++---------------- 6 files changed, 82 insertions(+), 42 deletions(-) create mode 100644 examples/benchmark_runner.rs create mode 100644 grid_pathfinding_benchmark/Cargo.toml create mode 100644 grid_pathfinding_benchmark/README.md rename examples/standard_benchmark.rs => grid_pathfinding_benchmark/src/lib.rs (65%) diff --git a/Cargo.lock b/Cargo.lock index 577ee22..c438e45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -249,8 +249,8 @@ name = "grid_pathfinding" version = "0.2.0" dependencies = [ "criterion", - "csv", "fxhash", + "grid_pathfinding_benchmark", "grid_util", "indexmap 2.2.6", "itertools 0.12.1", @@ -258,6 +258,15 @@ dependencies = [ "num-traits", "petgraph", "rand", +] + +[[package]] +name = "grid_pathfinding_benchmark" +version = "0.1.0" +dependencies = [ + "criterion", + "csv", + "grid_util", "serde", "walkdir", ] diff --git a/Cargo.toml b/Cargo.toml index 514aabf..bbc136e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,9 +26,7 @@ bench = false [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } rand = "0.8.5" -csv = "1.3.0" -serde = "1.0.204" -walkdir = "2.5.0" +grid_pathfinding_benchmark = { path = "grid_pathfinding_benchmark" } [[bench]] name = "comparison_bench" diff --git a/examples/benchmark_runner.rs b/examples/benchmark_runner.rs new file mode 100644 index 0000000..3bdd0c3 --- /dev/null +++ b/examples/benchmark_runner.rs @@ -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> = pathing_grid.get_waypoints_single_goal(*start, *goal, false); + assert!(path.is_some()); + } +} diff --git a/grid_pathfinding_benchmark/Cargo.toml b/grid_pathfinding_benchmark/Cargo.toml new file mode 100644 index 0000000..de99d32 --- /dev/null +++ b/grid_pathfinding_benchmark/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "grid_pathfinding_benchmark" +version = "0.1.0" +authors = ["Thom van der Woude "] +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" diff --git a/grid_pathfinding_benchmark/README.md b/grid_pathfinding_benchmark/README.md new file mode 100644 index 0000000..8e10f36 --- /dev/null +++ b/grid_pathfinding_benchmark/README.md @@ -0,0 +1,2 @@ +# grid_pathfinding_benchmark +Helper crate for loading Moving AI pathfinding benchmarks. \ No newline at end of file diff --git a/examples/standard_benchmark.rs b/grid_pathfinding_benchmark/src/lib.rs similarity index 65% rename from examples/standard_benchmark.rs rename to grid_pathfinding_benchmark/src/lib.rs index e5b5779..f5ec6d5 100644 --- a/examples/standard_benchmark.rs +++ b/grid_pathfinding_benchmark/src/lib.rs @@ -1,10 +1,8 @@ use csv::ReaderBuilder; -use grid_pathfinding::PathingGrid; use grid_util::grid::Grid; use grid_util::point::Point; use grid_util::BoolGrid; use serde::Deserialize; -use std::error::Error; use std::fs::{self, File}; use std::io::{self, BufRead}; use std::path::Path; @@ -12,7 +10,7 @@ use std::time::{Duration, Instant}; use walkdir::WalkDir; #[derive(Debug, Deserialize)] -struct Scenario { +pub struct Scenario { id: u32, file_name: String, w: u32, @@ -82,7 +80,7 @@ fn load_benchmark(name: &str) -> (BoolGrid, Vec<(Point, Point)>) { (bool_grid, data_array) } -fn get_benchmark_names() -> Vec { +pub fn get_benchmark_names() -> Vec { let root = Path::new("maps/"); let root = root .canonicalize() @@ -104,40 +102,11 @@ fn get_benchmark_names() -> Vec { names } -fn main() { +pub fn get_benchmark(name: String) -> (BoolGrid, Vec<(Point, Point)>) { 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) = load_benchmark(name.as_str()); - // 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> = pathing_grid.get_waypoints_single_goal(*start, *goal, false); - assert!(path.is_some()); + if benchmark_names.contains(&name) { + load_benchmark(name.as_str()) + } else { + panic!("Could not load benchmark!"); } } From 6622dc16de3eb0c455cf37c161ea230ac942d869 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Mon, 5 Aug 2024 12:42:10 +0200 Subject: [PATCH 05/14] Changed dao_bench to run maps dao/arena, dao/den312d, dao/arena2 --- benches/comparison_bench.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index 72666f6..b960f04 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -1,5 +1,6 @@ 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::*; @@ -33,6 +34,31 @@ fn test(pathing_grid: &PathingGrid, start: Point, end: Point) -> Option Date: Mon, 5 Aug 2024 14:20:21 +0200 Subject: [PATCH 06/14] Added GRAPH_PRUNING const flag to toggle JPS pruning of A* neighborhood --- src/lib.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1bca388..bf2bbc8 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ use core::fmt; use std::collections::VecDeque; const EQUAL_EDGE_COST: bool = true; +const GRAPH_PRUNING: bool = true; // Costs for diagonal and cardinal moves. // Values for unequal costs approximating a ratio D/C of sqrt(2) are from @@ -384,7 +385,11 @@ impl PathingGrid { let result = astar_jps( &start, |parent, node| { - self.jps_neighbours(*parent, node, &|node_pos| goals.contains(&node_pos)) + if GRAPH_PRUNING { + self.jps_neighbours(*parent, node, &|node_pos| goals.contains(&node_pos)) + } else { + self.neighborhood_points_and_cost(node) + } }, |point| { (goals @@ -415,9 +420,13 @@ impl PathingGrid { astar_jps( &start, |parent, node| { - self.jps_neighbours(*parent, node, &|node_pos| { - self.heuristic(node_pos, &goal) <= 1 - }) + if GRAPH_PRUNING { + self.jps_neighbours(*parent, node, &|node_pos| { + self.heuristic(node_pos, &goal) <= 1 + }) + } else { + self.neighborhood_points_and_cost(node) + } }, |point| (self.heuristic(point, &goal) as f32 * self.heuristic_factor) as i32, |point| self.heuristic(point, &goal) <= 1, @@ -430,7 +439,13 @@ impl PathingGrid { // The goal is reachable from the start, compute a path astar_jps( &start, - |parent, node| self.jps_neighbours(*parent, node, &|node_pos| *node_pos == goal), + |parent, node| { + if GRAPH_PRUNING { + self.jps_neighbours(*parent, node, &|node_pos| *node_pos == goal) + } else { + self.neighborhood_points_and_cost(node) + } + }, |point| (self.heuristic(point, &goal) as f32 * self.heuristic_factor) as i32, |point| *point == goal, ) From e7da3da54535a5e3ccc02158cd1cac136cdcab22 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Mon, 5 Aug 2024 14:44:12 +0200 Subject: [PATCH 07/14] Fully switched to standard benchmark for each algorithm setting --- Cargo.lock | 1 - Cargo.toml | 1 - benches/comparison_bench.rs | 107 +++++++----------------------------- 3 files changed, 20 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c438e45..685346b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,7 +257,6 @@ dependencies = [ "log", "num-traits", "petgraph", - "rand", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bbc136e..bc5c42f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,6 @@ bench = false [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } -rand = "0.8.5" grid_pathfinding_benchmark = { path = "grid_pathfinding_benchmark" } [[bench]] diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index b960f04..cbaf390 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -3,105 +3,38 @@ 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> { black_box(pathing_grid.get_path_single_goal(start, end, false)) } fn dao_bench(c: &mut Criterion) { - let allow_diag = true; - let pruning = false; - for name in ["dao/arena", "dao/den312d", "dao/arena2"] { - 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 { - test(&pathing_grid, *start, *end); - } - }) - }); - } -} - -fn criterion_benchmark(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 = 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 { + test(&pathing_grid, *start, *end); } }) - }, - ); - 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()); - } - }) - }, - ); } } From 09fea5f701aeeb43127a9bca7480a5a2ff26e241 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Tue, 6 Aug 2024 10:30:16 +0200 Subject: [PATCH 08/14] Extended README.md to include discussion of new benchmarks and relative performance, switched to single_bench --- Cargo.lock | 1 + Cargo.toml | 3 ++- README.md | 16 +++++++++++----- benches/single_bench.rs | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 benches/single_bench.rs diff --git a/Cargo.lock b/Cargo.lock index 685346b..c438e45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,6 +257,7 @@ dependencies = [ "log", "num-traits", "petgraph", + "rand", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bc5c42f..0dc9803 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,8 @@ bench = false [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } grid_pathfinding_benchmark = { path = "grid_pathfinding_benchmark" } +rand = "0.8.5" [[bench]] -name = "comparison_bench" +name = "single_bench" harness = false diff --git a/README.md b/README.md index 7e235f7..7953359 100644 --- a/README.md +++ b/README.md @@ -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; @@ -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 ``` @@ -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. \ No newline at end of file diff --git a/benches/single_bench.rs b/benches/single_bench.rs new file mode 100644 index 0000000..9e36992 --- /dev/null +++ b/benches/single_bench.rs @@ -0,0 +1,38 @@ +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; + +fn test(pathing_grid: &PathingGrid, start: Point, end: Point) -> Option> { + black_box(pathing_grid.get_path_single_goal(start, end, false)) +} + +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 { + test(&pathing_grid, *start, *end); + } + }) + }); + } + } +} + +criterion_group!(benches, dao_bench_single); +criterion_main!(benches); From e831b96ffd59e05024638b9d978425cfe5756da0 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Tue, 6 Aug 2024 10:30:27 +0200 Subject: [PATCH 09/14] Disabled equal edge cost --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index bf2bbc8..ac19982 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ use crate::astar_jps::astar_jps; use core::fmt; use std::collections::VecDeque; -const EQUAL_EDGE_COST: bool = true; +const EQUAL_EDGE_COST: bool = false; const GRAPH_PRUNING: bool = true; // Costs for diagonal and cardinal moves. From a783a4d113cd99e1309e3e0201f9011f4f195917 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Tue, 6 Aug 2024 10:35:41 +0200 Subject: [PATCH 10/14] Removed redundant Duration and Instant imports in grid_pathfinding_benchmark --- grid_pathfinding_benchmark/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/grid_pathfinding_benchmark/src/lib.rs b/grid_pathfinding_benchmark/src/lib.rs index f5ec6d5..9c3d655 100644 --- a/grid_pathfinding_benchmark/src/lib.rs +++ b/grid_pathfinding_benchmark/src/lib.rs @@ -6,7 +6,6 @@ use serde::Deserialize; use std::fs::{self, File}; use std::io::{self, BufRead}; use std::path::Path; -use std::time::{Duration, Instant}; use walkdir::WalkDir; #[derive(Debug, Deserialize)] From b3c304423e3579d7d926c5eeedd8332abb7138c9 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Tue, 6 Aug 2024 10:50:59 +0200 Subject: [PATCH 11/14] Implemented support for multiple benchmark sets in maps and scenarios folder --- grid_pathfinding_benchmark/src/lib.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/grid_pathfinding_benchmark/src/lib.rs b/grid_pathfinding_benchmark/src/lib.rs index 9c3d655..cd427f4 100644 --- a/grid_pathfinding_benchmark/src/lib.rs +++ b/grid_pathfinding_benchmark/src/lib.rs @@ -85,18 +85,21 @@ pub fn get_benchmark_names() -> Vec { .canonicalize() .expect("Failed to canonicalize root path"); let mut names = Vec::new(); - for entry in WalkDir::new(&root).into_iter().skip(2) { + for entry in WalkDir::new(&root).into_iter() { let path_str = entry.expect("Could not get dir entry"); - let name = path_str - .path() - .strip_prefix(&root) - .unwrap() + let rel_path = path_str + .path() + .strip_prefix(&root) + .unwrap(); + if rel_path.components().count() >= 2{ + let name = rel_path .to_str() .unwrap() .split_once('.') .unwrap() .0; - names.push(name.to_owned()); + names.push(name.to_owned()); + } } names } From 730f8552cb9948cf6857ab7e5a0e8e66fd52680e Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Tue, 6 Aug 2024 10:53:13 +0200 Subject: [PATCH 12/14] Refactored benches to make test function redundant --- benches/comparison_bench.rs | 6 +----- benches/single_bench.rs | 7 +------ 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index cbaf390..0f1e40d 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -4,10 +4,6 @@ use grid_pathfinding_benchmark::*; use grid_util::grid::Grid; use grid_util::point::Point; -fn test(pathing_grid: &PathingGrid, start: Point, end: Point) -> Option> { - black_box(pathing_grid.get_path_single_goal(start, end, false)) -} - fn dao_bench(c: &mut Criterion) { for (allow_diag, pruning) in [(false, false), (true, false), (true, true)] { let bench_set = if allow_diag { @@ -30,7 +26,7 @@ fn dao_bench(c: &mut Criterion) { c.bench_function(format!("{name}, {diag_str}{improved_str}").as_str(), |b| { b.iter(|| { for (start, end) in &scenarios { - test(&pathing_grid, *start, *end); + black_box(pathing_grid.get_path_single_goal(*start, *end, false)); } }) }); diff --git a/benches/single_bench.rs b/benches/single_bench.rs index 9e36992..6103ee2 100644 --- a/benches/single_bench.rs +++ b/benches/single_bench.rs @@ -2,11 +2,6 @@ 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; - -fn test(pathing_grid: &PathingGrid, start: Point, end: Point) -> Option> { - black_box(pathing_grid.get_path_single_goal(start, end, false)) -} fn dao_bench_single(c: &mut Criterion) { for (allow_diag, pruning) in [(true, false)] { @@ -26,7 +21,7 @@ fn dao_bench_single(c: &mut Criterion) { c.bench_function(format!("{name}, {diag_str}{improved_str}").as_str(), |b| { b.iter(|| { for (start, end) in &scenarios { - test(&pathing_grid, *start, *end); + black_box(pathing_grid.get_path_single_goal(*start, *end, false)); } }) }); From 3c460315a7f6f84750ee1db6ef49708ffdcaee35 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Tue, 6 Aug 2024 11:02:46 +0200 Subject: [PATCH 13/14] Added #[allow(unused)] above Scenario as unused fields are necessary for the file format --- benches/comparison_bench.rs | 1 - grid_pathfinding_benchmark/src/lib.rs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index 0f1e40d..b9d8899 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -2,7 +2,6 @@ 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; fn dao_bench(c: &mut Criterion) { for (allow_diag, pruning) in [(false, false), (true, false), (true, true)] { diff --git a/grid_pathfinding_benchmark/src/lib.rs b/grid_pathfinding_benchmark/src/lib.rs index cd427f4..942a496 100644 --- a/grid_pathfinding_benchmark/src/lib.rs +++ b/grid_pathfinding_benchmark/src/lib.rs @@ -8,6 +8,7 @@ use std::io::{self, BufRead}; use std::path::Path; use walkdir::WalkDir; +#[allow(unused)] #[derive(Debug, Deserialize)] pub struct Scenario { id: u32, From d2d66fdfb7576ba02a20c3c1c3f14323f9e3e37c Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Tue, 6 Aug 2024 11:04:03 +0200 Subject: [PATCH 14/14] Switched back to default bench (for main) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0dc9803..8d1f043 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,5 +29,5 @@ grid_pathfinding_benchmark = { path = "grid_pathfinding_benchmark" } rand = "0.8.5" [[bench]] -name = "single_bench" +name = "comparison_bench" harness = false