pathfinding Rust Library

repository·main·Indexed 22 days ago

https://github.com/evenfurther/pathfinding

A Rust library providing pathfinding, flow, and graph algorithms including Dijkstra, A*, and BFS. It uses a functional successor interface, allowing algorithms to work with any graph representation (such as adjacency lists or implicit graphs) by defining how to navigate from one node to its neighbors. Version 4.15.0.

Tokens
12K
Snippets
35
Records
43
Agent score
77%

What's inside pathfinding

  1. Optimize memory with dynamic successor functions

    main

    The successor function is called on-demand rather than pre-calculating all paths. To optimize memory efficiency for large graphs, you can:

    • Generate successors dynamically during the search.
    • Use lazy evaluation.
    • Avoid storing the entire graph in memory by calculating neighbors on the fly.
  2. How the successor function works in pathfinding

    main

    The pathfinding library uses a functional approach rather than requiring specific graph data structures. Instead, all algorithms rely on a successor function that defines how to navigate from one node to its neighbors. This allows you to use any representation (adjacency lists, matrices, edge lists, or implicit graphs).

    Depending on the algorithm type, the successor function must return different types:

    • For weighted algorithms (e.g., dijkstra, astar): The function must return an iterator or collection of (neighbor_node, cost) pairs.
    • For unweighted algorithms (e.g., bfs, dfs): The function must return an iterator or collection of just the neighbor_nodes.

    This design implicitly defines your graph structure through the logic of the function itself.

    // For weighted graphs (Dijkstra, A*, Fringe, etc.)
    fn successors(node: &Node) -> impl IntoIterator<Item = (Node, Cost)>
    
    // For unweighted graphs (BFS, DFS, etc.)
    fn successors(node: &Node) -> impl IntoIterator<Item = Node>
  3. Best practices for Node and Cost types

    main

    To ensure compatibility with the pathfinding algorithms, follow these type constraints:

    Node Types

    Nodes can be any type that implements Eq, Hash, and Clone. Examples include:

    • Integers (u32, usize)
    • Strings (String, &str)
    • Custom structs
    • Tuples (e.g., (i32, i32) for grid coordinates)

    Cost Types

    Costs must implement Zero, Ord, and Copy. Examples include:

    • Unsigned integers (u32, usize)
    • Signed integers (i32)
    • For floating-point values, use the ordered_float crate to satisfy the Ord requirement.
  4. Implement bidirectional edges

    main

    If your graph is undirected or requires bidirectional movement, you must manually add edges in both directions when constructing your adjacency list:

    // For an edge between a and b
    graph.entry(a).or_default().push((b, cost));
    graph.entry(b).or_default().push((a, cost));
  5. Convert graph data from R (igraph) or Python (NetworkX)

    main

    When migrating from languages like R or Python, you must convert explicit graph structures (like edge lists or NetworkX objects) into an adjacency list format (typically a HashMap<Node, Vec<(Neighbor, Cost)>>) to use with pathfinding's Dijkstra implementation.

    // Example: Converting an edge list to an adjacency list for Dijkstra
    use pathfinding::prelude::dijkstra;
    use std::collections::HashMap;
    
    let mut graph: HashMap<String, Vec<(String, u32)>> = HashMap::new();
    for (from, to, weight) in edges {
        graph.entry(from).or_default().push((to, weight));
    }
    
    let result = dijkstra(
        &start_node,
        |node| graph.get(node).cloned().unwrap_or_default(),
        |node| node == &goal_node,
    );
  6. Common pathfinding patterns

    main

    Check if a path exists

    Use dijkstra and check if the result is is_some().

    let path_exists = dijkstra(&start, successors, |&n| n == goal).is_some();

    Find all reachable nodes and costs

    Use dijkstra_all to get a map of all reachable nodes and their minimum costs from the start.

    use pathfinding::prelude::dijkstra_all;
    let result = dijkstra_all(&start, successors);

    Check against multiple goals

    Pass a closure to the goal parameter that checks if the current node is contained in a list of goals.

    let goals = vec![goal1, goal2, goal3];
    let result = dijkstra(&start, successors, |node| goals.contains(node));

    Filter nodes within a cost budget

    Use dijkstra_all and then filter the resulting iterator by cost.

    use pathfinding::prelude::dijkstra_all;
    let reachable = dijkstra_all(&start, successors);
    let within_budget: Vec<_> = reachable
        .into_iter()
        .filter(|(_, cost)| *cost <= budget)
        .collect();
  7. Use the pathfinding prelude for easy access

    main

    The pathfinding::prelude module exports all major public functions and structures, allowing you to use the library without navigating the internal module hierarchy.

    Included in the prelude:

    • Directed Graph Algorithms: astar, bfs, count_paths, cycle_detection, dfs, dijkstra, edmonds_karp, fringe, idastar, iddfs, strongly_connected_components, topological_sort, yen.
    • Undirected Graph Algorithms: cliques, connected_components, kruskal.
    • Matching: kuhn_munkres.
    • Data Structures: Grid, Matrix.
    • Utilities: utils.
    use pathfinding::prelude::*;
  8. Implement A* for spatial shortest paths

    main

    You can use the astar function to find the shortest path in spatial graphs (like GIS or mapping applications). This requires defining a Location struct for nodes, a distance_to method for the heuristic, and a successor function that returns neighbors and their associated costs. For A* to be effective, the heuristic must be admissible (it must never overestimate the true cost to the goal).

    use pathfinding::prelude::astar;
    use std::collections::HashMap;
    
    #[derive(Debug, Clone, Hash, Eq, PartialEq)]
    struct Location {
        id: u32,
        x: f64,
        y: f64,
    }
    
    impl Location {
        fn distance_to(&self, other: &Location) -> u32 {
            let dx = self.x - other.x;
            let dy = self.y - other.y;
            ((dx * dx + dy * dy).sqrt() * 100.0) as u32 // Scale for integer costs
        }
    }
    
    struct SpatialGraph {
        locations: HashMap<u32, Location>,
        edges: HashMap<u32, Vec<(u32, u32)>>, // node_id -> vec of (neighbor_id, cost)
    }
    
    impl SpatialGraph {
        // ... (new, add_location, add_edge methods) ...
    
        fn find_shortest_path(&self, start_id: u32, goal_id: u32) -> Option<(Vec<u32>, u32)> {
            let goal_location = self.locations.get(&goal_id)?;
    
            astar(
                &start_id,
                |&node_id| {
                    self.edges
                        .get(&node_id)
                        .cloned()
                        .unwrap_or_default()
                },
                |&node_id| {
                    // Heuristic: straight-line distance to goal
                    self.locations
                        .get(&node_id)
                        .map(|loc| loc.distance_to(goal_location))
                        .unwrap_or(u32::MAX)
                },
                |&node_id| node_id == goal_id,
            )
        }
    }
  9. Note on using floating-point types for weights

    main

    Many algorithms in this crate require edge weights to implement the Ord trait. Because Rust's built-in floating-point types (f32, f64) only implement PartialOrd, they cannot be used directly as weights.

    To use floating-point numbers for weights, wrap them in a type that implements Ord, such as those provided by the ordered-float crate.

  10. Use idastar with closures for concise syntax

    main

    If you do not want to define a custom struct for your nodes, you can use idastar with closures and primitive types (like tuples) directly. This is useful for quick implementations or simple coordinate systems.

    Example

    Searching for a knight's path on a chess board using (i32, i32) tuples:

    use pathfinding::prelude::idastar;
    
    static GOAL: (i32, i32) = (4, 6);
    let result = idastar(&(1, 1),
                       |&(x, y)| vec![(x+1,y+2), (x+1,y-2), (x-1,y+2), (x-1,y-2),
                                       (x+2,y+1), (x+2,y-1), (x-2,y+1), (x-2,y-1)]
                                   .into_iter().map(|p| (p, 1)),
                       |&(x, y)| (GOAL.0.abs_diff(x) + GOAL.1.abs_diff(y)) / 3,
                       |&p| p == GOAL);
    assert_eq!(result.expect("no path found").1, 4);
  11. Visualize a Grid using Debug formatting

    main

    The Grid implements Debug for easy visualization in the console:

    • Standard {:#?}: Uses # for vertices and . for empty spaces.
    • Alternate {:#?}: Uses for vertices and for empty spaces.
    • Inverted lines {:-#?}: Reverses the order of rows (useful for coordinate systems where Y increases upwards).
    use pathfinding::prelude::Grid;
    
    let mut g = Grid::new(3, 4);
    g.add_borders();
    
    // Standard debug: uses # and .
    println!("{:?}", g);
    
    // Alternate debug: uses ▓ and ░
    println!("{:#?}", g);
    
    // Inverted rows
    println!("{:-#?}", g);
  12. Compute a shortest path using `iddfs`

    main

    The iddfs (Iterative Deepening Depth-First Search) function computes the shortest path from a start node to a node that satisfies the success condition. It is useful for finding shortest paths in unweighted graphs while maintaining the memory efficiency of depth-first search.

    Parameters

    • start: The starting node of type N.
    • successors: A closure FnMut(&N) -> IN that returns an iterator of successor nodes for a given node.
    • success: A closure FnMut(&N) -> bool that returns true if the given node is the goal. Note that the goal does not have to be a specific node; it can be a dynamic condition.

    Behavior

    • Returns Some(Vec<N>) containing the shortest path (including both the start and end nodes) if a path is found.
    • Returns None if no path can be found.
    • A node will never be included twice in the path (it avoids cycles based on the Eq relationship).
    • The start node's ownership is taken by iddfs as no clones are made.
    use pathfinding::prelude::iddfs;
    
    // Example 1: Using a custom struct
    #[derive(Eq, PartialEq, Clone, Debug)]
    struct Pos(i32, i32);
    
    impl Pos {
      fn successors(&self) -> Vec<Pos> {
        let &Pos(x, y) = self;
        vec![Pos(x+1,y+2), Pos(x+1,y-2), Pos(x-1,y+2), Pos(x-1,y-2),
             Pos(x+2,y+1), Pos(x+2,y-1), Pos(x-2,y+1), Pos(x-2,y-1)]
      }
    }
    
    static GOAL: Pos = Pos(4, 6);
    let result = iddfs(Pos(1, 1), |p| p.successors(), |p| *p == GOAL);
    assert_eq!(result.expect("no path found").len(), 5);
    
    // Example 2: Using closures and tuples for brevity
    static GOAL_TUPLE: (i32, i32) = (4, 6);
    let result = iddfs((1, 1),
                     |&(x, y)| vec![(x+1,y+2), (x+1,y-2), (x-1,y+2), (x-1,y-2),
                                    (x+2,y+1), (x+2,y-1), (x-2,y+1), (x-2,y-1)],
                     |&p| p == GOAL_TUPLE);
    assert_eq!(result.expect("no path found").len(), 5);