Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
52de927
Added straightforward implementation of ALT algorithm in ALTSolver
tbvanderwoude Nov 23, 2025
58629bf
Much improved ALTSolver speed by switching from vec of grids to grid …
tbvanderwoude Nov 23, 2025
840239a
Added benchmark of ALT, using primitive monte carlo generation of lan…
tbvanderwoude Nov 23, 2025
d370bf6
Introduced generic dao_bench_solver, reusing much of the benchmarking…
tbvanderwoude Nov 23, 2025
4040436
Changed successors, heuristic and success in astar_jps from FnMut to …
tbvanderwoude Nov 23, 2025
01d1c4d
Implemented greedy landmark selection algorithm from single initial l…
tbvanderwoude Nov 23, 2025
754d6f6
Simplified approximate goal condition using D instead of repeated EQU…
tbvanderwoude Nov 23, 2025
d1422a1
Improved tightness of heuristic by adding missing base heuristic of s…
tbvanderwoude Nov 25, 2025
c4372a7
Removed grid reference parameter from heuristic and cost, simplifying…
tbvanderwoude Nov 26, 2025
b429215
Removed criterion 0.4 from grid_pathfinding_benchmark dependencies an…
tbvanderwoude Nov 28, 2025
ee3b2de
Created solver::landmarks submodule, cherry-picking this change from …
tbvanderwoude Nov 28, 2025
3c1c399
Cleaned up comparison_bench, comparing just arena2 between solvers as…
tbvanderwoude Nov 28, 2025
a0452b0
Removed pathing_grid parameter from get_path_cost and get_path_cost_f…
tbvanderwoude Nov 28, 2025
05108a9
Merge branch 'benchmark-verification' into altsolver
tbvanderwoude Nov 30, 2025
de7ad00
Made reachable_fuzzer generic over GridSolver's
tbvanderwoude Nov 30, 2025
61cfaf0
Added reachability fuzz tests for ALT
tbvanderwoude Nov 30, 2025
4f72d7f
Made new_minmax_greedy handle the case where no new landmark is found
tbvanderwoude Jan 3, 2026
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
154 changes: 89 additions & 65 deletions benches/comparison_bench.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
use criterion::{criterion_group, criterion_main, Criterion};
use grid_pathfinding::{
pathing_grid::PathingGrid,
solver::{astar::AstarSolver, dijkstra::DijkstraSolver, jps::JPSSolver, GridSolver},
solver::{
alt::ALTSolver, astar::AstarSolver, dijkstra::DijkstraSolver, jps::JPSSolver, GridSolver,
},
Pathfinder,
};
use grid_pathfinding_benchmark::*;
use grid_util::grid::ValueGrid;
use grid_util::{grid::ValueGrid, Point};
use rand::{rngs::StdRng, Rng, SeedableRng};
use smallvec::{smallvec, SmallVec};
use std::hint::black_box;

fn dao_bench<const ALLOW_DIAGONAL: bool>(c: &mut Criterion) {
let arr: SmallVec<[bool; 2]> = if ALLOW_DIAGONAL {
smallvec![false, true]
smallvec![false]
} else {
smallvec![false]
};
let bench_set = if ALLOW_DIAGONAL {
["dao/arena", "dao/den312d", "dao/arena2"]
["dao/arena2"]
} else {
["dao/arena", "dao/den009d", "dao/den312d"]
["dao/arena2"]
};
for pruning in arr {
for name in bench_set {
Expand All @@ -43,54 +46,29 @@ fn dao_bench<const ALLOW_DIAGONAL: bool>(c: &mut Criterion) {
}
}

fn dao_bench_jps<const ALLOW_DIAGONAL: bool>(c: &mut Criterion) {
let arr: SmallVec<[bool; 2]> = if ALLOW_DIAGONAL {
smallvec![false, true]
} else {
smallvec![false]
};
fn dao_bench_solver<const ALLOW_DIAGONAL: bool, S, FS>(
c: &mut Criterion,
solver_name: &str,
create_solver: FS,
) where
S: GridSolver,
FS: Fn(&mut PathingGrid<ALLOW_DIAGONAL>) -> S,
{
let bench_set = if ALLOW_DIAGONAL {
["dao/arena", "dao/den312d", "dao/arena2"]
["dao/arena2"]
} else {
["dao/arena", "dao/den009d", "dao/den312d"]
["dao/arena2"]
};
for pruning in arr {
for name in bench_set {
let (bool_grid, scenarios) = get_benchmark(name.to_owned());
let mut pathing_grid: PathingGrid<ALLOW_DIAGONAL> =
PathingGrid::new(bool_grid.width, bool_grid.height, true);
pathing_grid.grid = bool_grid.clone();
pathing_grid.generate_components();
let mut solver = JPSSolver::new(&pathing_grid, pruning);
solver.initialize(&pathing_grid);
let diag_str = if ALLOW_DIAGONAL { "8-grid" } else { "4-grid" };
let improved_str = if pruning { " (improved pruning)" } else { "" };

c.bench_function(
format!("{name}, JPS {diag_str}{improved_str}").as_str(),
|b| {
b.iter(|| {
for (start, end, _) in &scenarios {
black_box(solver.get_path_single_goal(&mut pathing_grid, *start, *end));
}
})
},
);
}
}
}
fn dao_bench_astar<const ALLOW_DIAGONAL: bool>(c: &mut Criterion) {
let bench_set = ["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<ALLOW_DIAGONAL> =
PathingGrid::new(bool_grid.width, bool_grid.height, true);
pathing_grid.grid = bool_grid.clone();
pathing_grid.generate_components();
let solver = AstarSolver::new();
let solver = create_solver(&mut pathing_grid);
let diag_str = if ALLOW_DIAGONAL { "8-grid" } else { "4-grid" };

c.bench_function(format!("{name}, Astar {diag_str}").as_str(), |b| {
c.bench_function(format!("{name}, {solver_name} {diag_str}").as_str(), |b| {
b.iter(|| {
for (start, end, _) in &scenarios {
black_box(solver.get_path_single_goal(&mut pathing_grid, *start, *end));
Expand All @@ -99,36 +77,82 @@ fn dao_bench_astar<const ALLOW_DIAGONAL: bool>(c: &mut Criterion) {
});
}
}
fn dao_bench_dijkstra<const ALLOW_DIAGONAL: bool>(c: &mut Criterion) {
let bench_set = ["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<ALLOW_DIAGONAL> =
PathingGrid::new(bool_grid.width, bool_grid.height, true);
pathing_grid.grid = bool_grid.clone();
pathing_grid.generate_components();
let solver = DijkstraSolver;
let diag_str = if ALLOW_DIAGONAL { "8-grid" } else { "4-grid" };
pub fn generate_landmarks_mc<const ALLOW_DIAGONAL: bool>(
pathing_grid: &PathingGrid<ALLOW_DIAGONAL>,
number: usize,
) -> Vec<Point> {
let mut landmarks = Vec::new();
let mut rng = StdRng::seed_from_u64(0);
while landmarks.len() < number {
let p = Point::new(
rng.random_range(0..pathing_grid.width() as i32),
rng.random_range(0..pathing_grid.height() as i32),
);
if pathing_grid.can_move_to_simple(p) {
landmarks.push(p);
}
}
landmarks
}

c.bench_function(format!("{name}, Dijkstra {diag_str}").as_str(), |b| {
b.iter(|| {
for (start, end, _) in &scenarios {
black_box(solver.get_path_single_goal(&mut pathing_grid, *start, *end));
}
})
});
fn dao_bench_jps<const ALLOW_DIAGONAL: bool>(c: &mut Criterion) {
let arr: SmallVec<[bool; 2]> = if ALLOW_DIAGONAL {
smallvec![false]
} else {
smallvec![false]
};
for pruning in arr {
let improved_str = if pruning { " (improved pruning)" } else { "" };
dao_bench_solver(
c,
format!("JPS{improved_str}").as_str(),
|pathing_grid: &mut PathingGrid<ALLOW_DIAGONAL>| {
let mut solver = JPSSolver::new(&pathing_grid, pruning);
solver.initialize(&pathing_grid);
solver
},
);
}
}

const N_LANDMARKS: usize = 64;
fn dao_bench_alt_mc<const ALLOW_DIAGONAL: bool>(c: &mut Criterion) {
dao_bench_solver(
c,
"ALT (MC)",
|pathing_grid: &mut PathingGrid<ALLOW_DIAGONAL>| {
let landmarks = generate_landmarks_mc(&pathing_grid, N_LANDMARKS);
ALTSolver::new_from_landmarks(landmarks, pathing_grid)
},
);
}
fn dao_bench_alt_greedy<const ALLOW_DIAGONAL: bool>(c: &mut Criterion) {
dao_bench_solver(
c,
"ALT (greedy)",
|pathing_grid: &mut PathingGrid<ALLOW_DIAGONAL>| {
let landmarks = generate_landmarks_mc(&pathing_grid, 1);
ALTSolver::new_greedy(landmarks[0], N_LANDMARKS, pathing_grid)
},
);
}
fn dao_bench_astar<const ALLOW_DIAGONAL: bool>(c: &mut Criterion) {
dao_bench_solver(c, "Astar", |_: &mut PathingGrid<ALLOW_DIAGONAL>| {
AstarSolver::new()
});
}

fn dao_bench_dijkstra<const ALLOW_DIAGONAL: bool>(c: &mut Criterion) {
dao_bench_solver(c, "Dijkstra", |_: &mut PathingGrid<ALLOW_DIAGONAL>| {
DijkstraSolver
});
}

criterion_group!(
benches,
// dao_bench<false>,
dao_bench<true>,
// dao_bench_jps<false>,
dao_bench_jps<true>,
// dao_bench_astar<false>,
// dao_bench_astar<true>,
// dao_bench_dijkstra<false>,
// dao_bench_dijkstra<true>
dao_bench_alt_greedy<true>,
dao_bench_alt_mc<true>,
);
criterion_main!(benches);
22 changes: 11 additions & 11 deletions src/astar_jps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ where
/// [AstarContext] represents the search fringe and node parent map, facilitating reuse of memory allocations.
#[derive(Clone, Debug)]
pub struct SearchContext<N, C, F> {
fringe: F,
parents: FxIndexMap<N, (usize, C)>,
pub(crate) fringe: F,
pub(crate) parents: FxIndexMap<N, (usize, C)>,
}

pub type BinaryHeapSearchContext<N, C> = SearchContext<N, C, BinaryHeap<SearchNode<C>>>;
Expand All @@ -120,15 +120,15 @@ where
pub fn astar_jps<FN, IN, FH, FS>(
&mut self,
start: &N,
mut successors: FN,
mut heuristic: FH,
mut success: FS,
successors: FN,
heuristic: FH,
success: FS,
) -> Option<(Vec<N>, C)>
where
FN: FnMut(&Option<&N>, &N) -> IN,
FN: Fn(&Option<&N>, &N) -> IN,
IN: IntoIterator<Item = (N, C)>,
FH: FnMut(&N) -> C,
FS: FnMut(&N) -> bool,
FH: Fn(&N) -> C,
FS: Fn(&N) -> bool,
{
self.fringe.clear();
self.parents.clear();
Expand Down Expand Up @@ -199,10 +199,10 @@ pub fn astar_jps<N, C, FN, IN, FH, FS>(
where
N: Eq + Hash + Clone,
C: Zero + Ord + Copy + Hash,
FN: FnMut(&Option<&N>, &N) -> IN,
FN: Fn(&Option<&N>, &N) -> IN,
IN: IntoIterator<Item = (N, C)>,
FH: FnMut(&N) -> C,
FS: FnMut(&N) -> bool,
FH: Fn(&N) -> C,
FS: Fn(&N) -> bool,
{
let mut search = DefaultSearchContext::new();
search.astar_jps(start, successors, heuristic, success)
Expand Down
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use std::sync::{Arc, Mutex};

use crate::astar_jps::DefaultSearchContext;

pub const ALLOW_CORNER_CUTTING: bool = false;
pub const ALLOW_CORNER_CUTTING: bool = true;
const EQUAL_EDGE_COST: bool = false;
const GRAPH_PRUNING: bool = true;
const N_SMALLVEC_SIZE: usize = 8;
Expand Down Expand Up @@ -516,14 +516,14 @@ impl<const ALLOW_DIAGONAL: bool> Pathfinder<ALLOW_DIAGONAL> {
|parent, node| {
if GRAPH_PRUNING {
self.jps_neighbours(*parent, node, &|node_pos| {
self.heuristic(node_pos, &goal) <= if EQUAL_EDGE_COST { 1 } else { 99 }
self.heuristic(node_pos, &goal) <= D
})
} else {
self.neighborhood_points_and_cost(node)
}
},
|point| (self.heuristic(point, &goal) as f32 * self.heuristic_factor) as i32,
|point| self.heuristic(point, &goal) <= if EQUAL_EDGE_COST { 1 } else { 99 },
|point| self.heuristic(point, &goal) <= D,
)
.map(|(v, _c)| v)
}
Expand Down
54 changes: 54 additions & 0 deletions src/solver/alt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use crate::solver::landmarks::LandmarkMap;
use crate::{pathing_grid::PathingGrid, solver::GridSolver, N_SMALLVEC_SIZE};
use grid_util::Point;
use smallvec::SmallVec;

#[derive(Clone, Debug)]
pub struct ALTSolver {
landmark_map: LandmarkMap,
}
impl ALTSolver {
pub fn new_from_landmarks<const ALLOW_DIAGONAL: bool>(
landmarks: Vec<Point>,
grid: &mut PathingGrid<ALLOW_DIAGONAL>,
) -> ALTSolver {
ALTSolver {
landmark_map: LandmarkMap::new_from_landmarks(landmarks, grid),
}
}
pub fn new_from_landmark_map(landmark_map: LandmarkMap) -> ALTSolver {
ALTSolver { landmark_map }
}
pub fn new_greedy<const ALLOW_DIAGONAL: bool>(
initial_landmark: Point,
landmark_count: usize,
grid: &mut PathingGrid<ALLOW_DIAGONAL>,
) -> ALTSolver {
ALTSolver {
landmark_map: LandmarkMap::new_minmax_greedy(initial_landmark, landmark_count, grid),
}
}
}

impl GridSolver for ALTSolver {
type Successors = SmallVec<[(Point, i32); N_SMALLVEC_SIZE]>;

fn successors<const ALLOW_DIAGONAL: bool, F>(
&self,
grid: &PathingGrid<ALLOW_DIAGONAL>,
_parent: Option<&Point>,
node: &Point,
_goal: &F,
) -> Self::Successors
where
F: Fn(&Point) -> bool,
{
grid.neighborhood_points_and_cost(node)
}

fn heuristic<const ALLOW_DIAGONAL: bool>(&self, p1: &Point, p2: &Point) -> i32 {
self.landmark_map
.triangle_heuristic(p1, p2)
.max(self.cost::<ALLOW_DIAGONAL>(p1, p2))
}
}
9 changes: 2 additions & 7 deletions src/solver/astar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,8 @@ impl GridSolver for AstarSolver {
}

/// Just the normal cost times a heuristic factor.
fn heuristic<const ALLOW_DIAGONAL: bool>(
&self,
grid: &PathingGrid<ALLOW_DIAGONAL>,
p1: &Point,
p2: &Point,
) -> i32 {
(self.cost(grid, p1, p2) as f32 * self.heuristic_factor) as i32
fn heuristic<const ALLOW_DIAGONAL: bool>(&self, p1: &Point, p2: &Point) -> i32 {
(self.cost::<ALLOW_DIAGONAL>(p1, p2) as f32 * self.heuristic_factor) as i32
}
}

Expand Down
7 changes: 1 addition & 6 deletions src/solver/dijkstra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,7 @@ impl GridSolver for DijkstraSolver {
}

/// Just the cost times a heuristic factor.
fn heuristic<const ALLOW_DIAGONAL: bool>(
&self,
_: &PathingGrid<ALLOW_DIAGONAL>,
_: &Point,
_: &Point,
) -> i32 {
fn heuristic<const ALLOW_DIAGONAL: bool>(&self, _: &Point, _: &Point) -> i32 {
0
}
}
9 changes: 2 additions & 7 deletions src/solver/jps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,8 @@ impl GridSolver for JPSSolver {
}

/// Uses C as cost for cardinal (straight) moves and D for diagonal moves.
fn heuristic<const ALLOW_DIAGONAL: bool>(
&self,
grid: &PathingGrid<ALLOW_DIAGONAL>,
p1: &Point,
p2: &Point,
) -> i32 {
self.cost(grid, p1, p2)
fn heuristic<const ALLOW_DIAGONAL: bool>(&self, p1: &Point, p2: &Point) -> i32 {
self.cost::<ALLOW_DIAGONAL>(p1, p2)
}
}
impl JPSSolver {
Expand Down
Loading