Package: pygraphsearch
pygraphsearchA python package to search graphs.1. InstallationTo install the package, run the following command.pip install pygraphsearch2. UsageAs an example on how to use this package, we will make a program that will solve sliding puzzles. The complete source code is in the folderexample. We will provide the program with some shuffled arrangement of tiles as the starting node, and we will tell it what the correct tile arrangement is as the target node.We will also tell it how to move from one board to another.2.1 NodeThis package includes a Node class which your nodes should inherit from. In our example, we will make a classBoardthat extendsNode. This class will represent the layout of the tiles on the board at any given time.To overrideNodewe must override the functionneighbours. This function should return edges to all the nodes reachable from the current node.In a sliding puzzle, there are always up to four legal moves. The empty tile can move up, down, left, or right, unless it's on an edge of the board in which case it can't move past the edge. We can make an enum calledMoveto store these four options.To check if a move is legal, we can create a methodcan_move. If we are able to move in a direction, then we call the board'smovemethod which will return a copy of the board but with the given move applied.The important thing here is that we define the functionneighbourswhich returns some edges.Note: Your node objects must also implement__hash__and__eq__.frompygraphsearchimportNode,EdgefromtypingimportIterable# it's always good to specify typesfrom.MoveimportMoveclassBoard(Node):# ...defneighbours(self)->Iterable[Edge]:return[Edge(self,self.move(move),move)formoveinMoveifself.can_move(move)]def__eq__(self,board:object)->bool:returnisinstance(board,Board)andself.__tiles==board.__tilesdef__hash__(self)->int:returnhash(tuple(self.__tiles))2.2 EdgesAs we saw in the previous section, theneighboursmethod has to return anEdge. This class is defined in this package and can be constructed by passing it two nodes and some optional data. The data, if passed, is used to convert an edge into a string for convenience, but otherwise not used by the search package.In our example it will be useful to store the direction that the empty tile is moved in an edge, as it will allow us to reconstruct the solution later.2.3 Basic UsageNow that we have our nodes, we will write a basic program that will request a puzzle from the user and attempt to solve it using thesearchfunction provided by this package.To get the puzzle size and initial board layout, we'll make the following function.defget_board_details()->Tuple[int,List[int]]:# get board sizesize=int(input("Enter the size of the sliding puzzle. (e.g. for a 3x3 puzzle enter 3): "))print(f"Enter{size*size}tile values row by row from top left to bottom right."" (Use 0 for the empty tile)")# get list of size * size integer inputs from usertiles=[int(input())for_inrange(size*size)]returnsize,tilesThen we'll use the following code to perform the search.# Get board details from the usersize,tiles=get_board_details()# Construct the initial nodestart_board=Board(size,tiles)# Construct the target node, eg a board with the tiles in order: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]target=Board(size,range(size*size))# Create a frontier with custom parametersfrontier=IterativeDeepeningFrontier(start_board,initial_depth=2,depth_step=2)# Call the search function with our frontier and with a lambda to tell it that a node is a target if it is equal to `target`state1=search(frontier,lambdanode:node==target)# Or simply call the function with the default options for one of the predefined algorithmsstate2=search(start_board,lambdanode:node==target,Algorithm.BREADTH_FIRST_SEARCH# (DFS would take forever for this problem))As you can see, there are two ways to call the search function. The simplest way is to provide it with:a start node,a function which takes a node and returns true if the node is a target node or false otherwise, andwhich algorithm to use.The other way to call the search function is to provide it with:a frontier (more on that in the next section) anda function which takes a node and returns true if the node is a target node or false otherwise.2.4 FrontiersA frontier is the heart of the search algorithm. It is a data structure which holds the furthest nodes we have explored and it determines the order in which we will explore them further.For BFS, the frontier would be a FIFO queue. For DFS, it would be a LIFO stack.If you want to implement your own search algorithms, all you need is to implement a frontier to pass to thesearchfunction by extending the abstractFrontierclass provided by this package and implementing the abstract methods. These are:insert(self, state: State)to insert states into the stack,extract(self) -> Optional[State]to extract states from the stack, and__len__(self) -> intwhich is the number of states stored in the frontier. The only important thing to be aware of about this function is that it should return 0 only when there are no more states to extract. If for some reason you can't or don't want to conform to this, you'll have to overrideis_emptyto return true only when there are no more states to extract, and the search is done.2.5 StateThe search function returns a state object. This object has two properties.node (Node): The target node. In our example this would be the state of the solved board.path (List[Edge]): A list of the edges that lead from the start to the target node.Using these, we can print the moves to solve the puzzle to the user, like so (continuing from the previous code):ifstate1isnotNone:foredgeinstate1.path:print(edge.data,end=" ")Here we're using edge.data which we set before by passing the moves to theEdgeconstructor in theBoardclass. By accessing the edges' data, we can retrieve the moves that were taken by the search algorithm to reach the solved state and print it back to the user.