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

bug fix for map path finding #7

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

lilejin322
Copy link

@lilejin322 lilejin322 commented Jul 27, 2024

Problem Description

In the original pathfinding algorithm, a potential error in the recursive return causes paths at a certain depth (length) to be returned repeatedly. This leads to inconsistent probabilities when sampling paths because some paths are considered multiple times.

Example to Explain the Problem

Let’s consider a simple acyclic directed graph where we want to find all paths of a specific length(depth) starting from the specified node.

graph
(Illustration is in .svg vector image format)

Now we implement the new function find_paths() to compare with the legancy get_reachable_from()

import networkx as nx
from typing import List
from matplotlib import pyplot as plt

class TestPath:

    def __init__(self):
        """
        Test the graph algorithm for finding paths
        """

        self.__lane_nx = nx.DiGraph()

    def add_edge(self, u: str, v: str):
        """
        Add an edge to the graph.

        :param str u: ID of the starting lane
        :param str v: ID of the ending lane
        """

        self.__lane_nx.add_edge(u, v)

    def get_reachable_from(self, lane_id: str, depth=5) -> List[List[str]]:
        """
        Recursive method to compute paths no more than ``depth`` lanes from
        the starting lane.

        :param str lane_id: ID of the starting lane
        :param int depth: maximum number of lanes traveled

        :returns: list of possible paths
        :rtype: List[List[str]]
        """

        if depth == 1:
            return [[lane_id]]
        result = list()
        for u, v in self.__lane_nx.edges(lane_id):
            result.append([u, v])
            for rp in self.get_reachable_from(v, depth-1):
                result.append([u] + rp)
        return result

    def find_paths(self, lane_id: str, depth=5) -> List[List[str]]:
        """
        New implementation of finding paths from a starting lane

        :param str lane_id: ID of the starting lane
        :parm int depth: maximum depth of the path, default to 5

        :returns: list of possible paths, except the single init_lane
        :rtype: List[List[str]]
        """
        
        depth -= 1

        def dfs(current_node: str, current_path: List[str], current_length: int) -> None:
            """
            Depth-first search to find all paths from a starting lane

            :param str current_node: ID of the current lane
            :param List[str] current_path: current path
            :param int current_length: current length of the path
            """

            if current_length > depth:
                return
            
            current_path.append(current_node)
            if len(current_path) > 1:
                paths.append(list(current_path))
            
            for neighbor in self.__lane_nx.successors(current_node):
                if current_length < depth:
                    # Only continue DFS if current length is less than max length
                    dfs(neighbor, current_path, current_length + 1)
            
            # Backtrack
            current_path.pop()

        paths = []
        dfs(lane_id, [], 0)
        return paths
    
    def plot(self):
        """
        Plot the graph
        """

        pos = nx.spring_layout(self.__lane_nx)
        nx.draw(self.__lane_nx, pos, with_labels=True, node_size=2000, node_color='lightblue',
                font_size=15, font_color='black', edge_color='gray', arrowsize=20)
        plt.title('The __lane_nx Graph', fontsize=20)
        plt.savefig('graph.svg')

graph = TestPath()

graph.add_edge('a', 'b')
graph.add_edge('b', 'c')
graph.add_edge('c', 'd')
graph.add_edge('d', 'g')
graph.add_edge('b', 'e')
graph.add_edge('e', 'f')
graph.add_edge('f', 'g')

Then let's have some tests:

  • Find all paths starting from node ‘b’ with a length (depth) of 3.
print("Get reachable from result: ",graph.get_reachable_from('b', 3))
print("Find paths result: " ,graph.find_paths('b', 3))

Result: ⬇️

Get reachable from result:  [['b', 'c'], ['b', 'c', 'd'], ['b', 'c', 'd'], ['b', 'e'], ['b', 'e', 'f'], ['b', 'e', 'f']]
Find paths result:  [['b', 'c'], ['b', 'c', 'd'], ['b', 'e'], ['b', 'e', 'f']]

We can find that the old one tells ['b', 'c', 'd'] and ['b', 'e', 'f'] twice respectively.

  • Find all paths starting from node ‘a’ with a length (depth) of 4.
print("Get reachable from result: ",graph.get_reachable_from('a', 4))
print("Find paths result: " ,graph.find_paths('a', 4))

Result: ⬇️

Get reachable from result:  [['a', 'b'], ['a', 'b', 'c'], ['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'd'], ['a', 'b', 'e'], ['a', 'b', 'e', 'f'], ['a', 'b', 'e', 'f']]
Find paths result:  [['a', 'b'], ['a', 'b', 'c'], ['a', 'b', 'c', 'd'], ['a', 'b', 'e'], ['a', 'b', 'e', 'f']]

It is obvious that the path ['a', 'b', 'c', 'd'], ['a', 'b', 'e', 'f'] are replicated in get_reachable_from().

As a result, we made the corresponding bug fix pull request.

@YuqiHuai
Copy link
Collaborator

YuqiHuai commented Aug 4, 2024

Hi, thank you for your effort on improving the work! I’ll accept the PR once I verify it on my end. I’ll be returning to work soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants