astar

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 7, 2022 License: GPL-3.0 Imports: 5 Imported by: 0

README

General

Coverage

This is an implementation of the A* path finding algoritm written in plain Golang. Please see wikipedia for a description of the algorithm.

astar logo

Overview

The algorithm has been developed during the amazing Advent of Code event in 2021. This software is in beta state. Development started in Dec 2021.

How to use

The main function most users will want to use is FindPath in astar.go. It requires a graph of nodes as input. Assuming you want to create a regular grid of 100 nodes, you can create one like this and find a path from the top left to the bottom right:

package main

import (
    "fmt"
    "log"

    "github.com/razziel89/astar"
)

func main() {
    // 100 nodes means a 10x10 grid.
    gridSize := [2]int{10, 10}
    // We want to go to the bottom right corner...
    endPos := [2]int{9, 9}
    // ... from the top left corner.
    startPos := [2]int{0, 0}
    // We disallow diagonal movements. Thus, each node has 4
    // neighbours, 2 in x direction and 2 in y direction.
    // Create a data structure that specifies these connections.
    // The algorithm will automatically only connect to existing
    // nodes and ignore displacements that would take it beyond
    // the specified grid.
    connections := [][2]int{
        [2]int{-1, 0},
        [2]int{0, -1},
        [2]int{1, 0},
        [2]int{0, 1},
    }

    // Create the regular 2D grid including all connections as
    // specified by `connections` above. We receive a graph
    // suitable for path finding, a map from positions to nodes,
    // and possibly an error. We created a heaped graph to get
    // the best performance. The alternative would be a "default"
    // graph, which is a simpler data structure but has worse
    // performance. The value `0` here is the default cost for
    // all nodes in the graph.
    graph, posToNode, err := astar.CreateRegular2DGrid(
        gridSize, connections, "heaped", 0,
    )
    // Error handling.
    if err != nil {
        log.Fatal(err.Error())
    }

    // Create a default, constant heuristic that estimates the
    // remaining cost as the line-of-sight distance between each
    // node and the desired destination. The value 0 is simply
    // the default estimate for unknown nodes, which is not
    // important here.
    heuristic, err := astar.CreateConstantHeuristic2D(
        posToNode, endPos, 0,
    )

    // Each node has a default cost of 0 assigned until now. To
    // create a useful example, we need to set some varying costs
    // on some nodes.
    for x := 0; x < gridSize[0]; x++ {
        for y := 0; y < gridSize[1]; y++ {

            // Extract the node.
            node := posToNode[[2]int{x, y}]
            // Note that we decrease the cost quadratically
            // towards x==5 and increase it for y towards higher
            // values. That way, the path through the middle is
            // preferred.
            node.Cost = (x-5)*(x-5) + 2*y

        }
    }

    // Extract start and end nodes.
    start := posToNode[startPos]
    end := posToNode[endPos]

    // Find the path! As you can see, creating the data
    // structures is the biggest headache. But this algorithm
    // permits arbitrary connections, even one-directional ones.
    path, err := astar.FindPath(graph, start, end, heuristic)
    // Error handling.
    if err != nil {
        log.Fatal(err.Error())
    }

    // Output the path we found!
    for _, node := range path {
        fmt.Println(node.ToString())
    }
}

The output will be this:

{id: x:0,y:0, cost: 25, con: ['x:0,y:1', 'x:1,y:0']}
{id: x:1,y:0, cost: 16, con: ['x:2,y:0', 'x:0,y:0', 'x:1,y:1']}
{id: x:2,y:0, cost: 9, con: ['x:1,y:0', 'x:2,y:1', 'x:3,y:0']}
{id: x:3,y:0, cost: 4, con: ['x:2,y:0', 'x:3,y:1', 'x:4,y:0']}
{id: x:4,y:0, cost: 1, con: ['x:3,y:0', 'x:4,y:1', 'x:5,y:0']}
{id: x:5,y:0, cost: 0, con: ['x:4,y:0', 'x:5,y:1', 'x:6,y:0']}
{id: x:6,y:0, cost: 1, con: ['x:5,y:0', 'x:6,y:1', 'x:7,y:0']}
{id: x:6,y:1, cost: 3, con: ['x:5,y:1', 'x:6,y:0', 'x:6,y:2', 'x:7,y:1']}
{id: x:6,y:2, cost: 5, con: ['x:6,y:1', 'x:6,y:3', 'x:7,y:2', 'x:5,y:2']}
{id: x:6,y:3, cost: 7, con: ['x:5,y:3', 'x:6,y:2', 'x:6,y:4', 'x:7,y:3']}
{id: x:6,y:4, cost: 9, con: ['x:5,y:4', 'x:6,y:3', 'x:6,y:5', 'x:7,y:4']}
{id: x:6,y:5, cost: 11, con: ['x:5,y:5', 'x:6,y:4', 'x:6,y:6', 'x:7,y:5']}
{id: x:6,y:6, cost: 13, con: ['x:5,y:6', 'x:6,y:5', 'x:6,y:7', 'x:7,y:6']}
{id: x:6,y:7, cost: 15, con: ['x:7,y:7', 'x:5,y:7', 'x:6,y:6', 'x:6,y:8']}
{id: x:6,y:8, cost: 17, con: ['x:5,y:8', 'x:6,y:7', 'x:6,y:9', 'x:7,y:8']}
{id: x:6,y:9, cost: 19, con: ['x:5,y:9', 'x:6,y:8', 'x:7,y:9']}
{id: x:7,y:9, cost: 22, con: ['x:8,y:9', 'x:6,y:9', 'x:7,y:8']}
{id: x:8,y:9, cost: 27, con: ['x:7,y:9', 'x:8,y:8', 'x:9,y:9']}
{id: x:9,y:9, cost: 34, con: ['x:8,y:9', 'x:9,y:8']}

As you can see, you get a nice string representation of the path. The algorithm has found the best path by first going to the middle in x direction, then upwards (positive y direction), and then to the right again once it cannot avoid it. Basically the path looks like this, with . being empty spaces and # being on the path, S is the start and E is the end:

. . . . . . # # # E
. . . . . . # . . .
. . . . . . # . . .
. . . . . . # . . .
. . . . . . # . . .
. . . . . . # . . .
. . . . . . # . . .
. . . . . . # . . .
. . . . . . # . . .
S # # # # # # . . .

Installation

Simply add github.com/razziel89/astar as a dependency to your project by running:

go get github.com/razziel89/astar@latest

Then, use as in the example above!

How to contribute

If you have found a bug and want to fix it, please simply go ahead and fork the repository, fix the bug, and open a pull request to this repository! Bug fixes are always welcome.

In all other cases, please open an issue on GitHub first to discuss the contribution. The feature you would like to introduce might already be in development.

Licence

GPLv3

If you want to use this piece of software under a different, more permissive open-source licence, please contact me. I am very open to discussing this point.

Documentation

Overview

Package astar implements the A* path finding algorithm. The main function is FindPath. Please find a description here: https://en.wikipedia.org/wiki/A*_search_algorithm

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FindReversePath

func FindReversePath(open, closed GraphOps, end *Node, heuristic Heuristic) error

FindReversePath finds a reverse path from the start node to the end node. Follow the prev member of the end node to traverse the path backwards. To use this function, in the beginning, the open list must contain the start node and the closed list must be empty.

This function may panic. If you want panics to be handled internally, use FindPath instead.

func GraphVal

func GraphVal() int

GraphVal is a convenience wrapper to return the default graph value.

Types

type ConstantHeuristic

type ConstantHeuristic struct {
	// contains filtered or unexported fields
}

ConstantHeuristic can be used to construct a simple heuristic function with constant (as: never changing for any one node, but differing between nodes) costs for reaching the end node. Use AddNode to add a node with estimated cost and use Heuristic to retrieve the heuristic function. That heuristic function then simply remembers the constant costs provided for each node. Omce the heuristic function has been retrieved, its data can no longer be modified. Adding a node would start the creation of a new heuristic function.

func (*ConstantHeuristic) AddNode

func (s *ConstantHeuristic) AddNode(node *Node, estimate int) error

AddNode adds a new node with estimated constant cost data. If the node is already there with a different estimate, this will error out.

func (*ConstantHeuristic) Heuristic

func (s *ConstantHeuristic) Heuristic(defaultValue int) Heuristic

Heuristic obtains a heuristic function for the provided data. If a node cannot be found in the available data, return the default value. This is suitable for use with FindPath. The gathered data is cleared so that adding new nodes won't influence the function's data.

type Error

type Error struct {
	// contains filtered or unexported fields
}

Error is the error type that can be returned by the astar package. It is used to determine which errors occurred inside the package and which ones occurred outside of it.

func (Error) Error

func (e Error) Error() string

type Graph

type Graph map[*Node]int

Graph is a collection of nodes. Note that there are no guarantees for the nodes to be connected. Ensuring that is the user's task. Each nodes is assigned to its estimate. That means a node's estimate will never be able to change once added. Get a graph via NewGraph.

func (*Graph) Add

func (g *Graph) Add(node *Node)

Add adds a node to the graph. If the node already exists, this a no-op.

func (*Graph) Apply

func (g *Graph) Apply(fn func(*Node) error) error

Apply applies a function to all nodes in the graph.

func (*Graph) Has

func (g *Graph) Has(node *Node) bool

Has determines whether a graph contains a specific node.

func (*Graph) Len

func (g *Graph) Len() int

Len determines the number of elements.

func (*Graph) PopCheapest

func (g *Graph) PopCheapest() *Node

PopCheapest retrieves one of the cheapest nodes and removes it. This will return nil if the graph is empty.

func (*Graph) Push

func (g *Graph) Push(node *Node, estimate int)

Push adds a node to the graph, including its estimate. If the node already exists, this a no-op.

func (*Graph) Remove

func (g *Graph) Remove(node *Node)

Remove removes a node from the graph. If the node does not exist, this a no-op.

func (*Graph) ToString

func (g *Graph) ToString(heuristic Heuristic) string

ToString provides a string representation of the graph. The nodes are sorted according to their user-defined names. If you provide a heuristic != nil, the value that heuristic provides for each node is also provided at the end of a line. Providing nil will use the stored estimates.

func (*Graph) UpdateIfBetter

func (g *Graph) UpdateIfBetter(node, prev *Node, newCost int)

UpdateIfBetter updates a node's best connection if that is cheaper than any previously found one. It takes the node to update, the new possible best predecessor and the cost for reaching that predecessor.

type GraphOps

type GraphOps interface {
	// Len specifies how many elements there are in the graph.
	Len() int
	// Has checks whether a node is in the graph.
	Has(node *Node) bool
	// Add adds a node to a graph without an estimate.
	Add(node *Node)
	// Push adds a node to a graph with an estimate.
	Push(node *Node, estimate int)
	// Remove removes a specific node from a graph.
	Remove(node *Node)
	// PopCheapest retrieves and removes the cheapest node from the graph. Cost is equal to the
	// node's cost added to its estimate.
	PopCheapest() *Node
	// Apply applies a function to all nodes in the graph. That function may error out.
	Apply(func(*Node) error) error
	// UpdateIfBetter updates a node's best connection if that is cheaper than any previously found
	// one. It takes the node to update, the new possible best predecessor and the cost for reaching
	// that predecessor.
	UpdateIfBetter(*Node, *Node, int)
}

GraphOps is an interface needed for a graph to be usable with the path finding functions in this module. See the method documentation of the actual Graph type for what the individual methods do in detail.

func CreateRegular2DGrid added in v0.2.0

func CreateRegular2DGrid(
	size [2]int, connections [][2]int, graphType string, defaultCost int,
) (GraphOps, map[[2]int]*Node, error)

CreateRegular2DGrid creates a regular 2D grid of connected nodes in a graph suitable for path finding. All nodes have a default cost of zero assigned.

Provide the number of nodes in each direction via the `size` argument. Provide a list of displacements that connect a node to its neighbours via the `connections` argument. For example, to connect horizontally and vertically only, pass the following as input:

[][2]int{
    [2]int{-1, 0},
    [2]int{0, -1},
    [2]int{1, 0},
    [2]int{0, 1},
}

Also provide the name of the type of graph you want to obtain as input. This can be "default" or "heaped". The heaped graph has a better performance but is a more complex data structure.

CreateRegular2DGrid returns three values:

  1. A graph object suitable for paht finding via FindPath.
  2. A map from node positions to node pointers, this makes modifying the nodes easy, e.g. to set the costs.
  3. An error value in case there were problems.

func NewGraph

func NewGraph(estimatedSize int) GraphOps

NewGraph obtains a new graph. No arguments are required. This function returns a normal graph based on a Go map. That structure has sub-optimal performance but works. Specify the estimated number of nodes as argument to boost performance. See Graph for details.

func NewHeapedGraph

func NewHeapedGraph(estimatedSize int) GraphOps

NewHeapedGraph obtains a new heaped graph. Specify the estimated number of nodes as argument to boost performance. See HeapedGraph for details.

type Heap

type Heap []HeapElement

Heap is a collection of nodes on a minimum heap. It implements Go's heap.Interface. It is used by the heapable graph to store the actual nodes. Don't use this data structre on its own.

func (*Heap) Len

func (h *Heap) Len() int

Len provides the length of the heap. This is needed for Go's heap interface.

func (*Heap) Less

func (h *Heap) Less(i, j int) bool

Less determines whether one value is smaller than another one. This is needed for Go's heap interface.

func (*Heap) Pop

func (h *Heap) Pop() interface{}

Pop removes a value from the heap. This is needed for Go's heap interface.

func (*Heap) Push

func (h *Heap) Push(x interface{})

Push adds a value to the heap. This is needed for Go's heap interface. Don't use it directly, use Add to add nodes. This one will panic if you provide an incorrect type thanks to Go's lack of generics.

func (*Heap) Swap

func (h *Heap) Swap(i, j int)

Swap swaps two values in the heap. This is needed for Go's heap interface.

func (*Heap) ToString

func (h *Heap) ToString(heuristic Heuristic) string

ToString provides a string representation of the heap. The nodes are sorted according to their user-defined names. If you provide a heuristic != nil, the value that heuristic provides for each node is also provided at the end of a line. Provide nil to disable.

type HeapElement

type HeapElement struct {
	Node     *Node
	Estimate int
}

HeapElement is an element of a heap.

type HeapedGraph

type HeapedGraph struct {
	Heap Heap
}

HeapedGraph is a collection of nodes. Note that there are no guarantees for the nodes to be connected. Ensuring that is the user's task. Each nodes is assigned to its estimate. That means a node's estimate will never be able to change once added. Get a heaped graph via NewHeapedGraph. This uses a Heap as storage backend.

func (*HeapedGraph) Add

func (g *HeapedGraph) Add(node *Node)

Add adds a node to the graph. If the node already exists, this a no-op. This panics if the node already has a different graph set.

func (*HeapedGraph) Apply

func (g *HeapedGraph) Apply(fn func(*Node) error) error

Apply apples a function to all nodes in the graph.

func (*HeapedGraph) Has

func (g *HeapedGraph) Has(node *Node) bool

Has determines whether a graph contains a specific node.

func (*HeapedGraph) Len

func (g *HeapedGraph) Len() int

Len determines the number of elements.

func (*HeapedGraph) PopCheapest

func (g *HeapedGraph) PopCheapest() *Node

PopCheapest retrieves one of the cheapest nodes and removes it. This will return nil if the graph is empty.

func (*HeapedGraph) Push

func (g *HeapedGraph) Push(node *Node, estimate int)

Push adds a node to the graph, including its estimate. If the node already exists, this a no-op.

func (*HeapedGraph) Remove

func (g *HeapedGraph) Remove(findNode *Node)

Remove removes a node from the graph. If the node does not exist, this a no-op. This function is inefficient but not needed for the algorithm in general.

func (*HeapedGraph) ToString

func (g *HeapedGraph) ToString(heuristic Heuristic) string

ToString provides a string representation of the graph. The nodes are sorted according to their user-defined names. If you provide a heuristic != nil, the value that heuristic provides for each node is also provided at the end of a line. Providing nil will use the stored estimates.

func (*HeapedGraph) UpdateIfBetter

func (g *HeapedGraph) UpdateIfBetter(node, prev *Node, newCost int)

UpdateIfBetter updates a node's best connection if that is cheaper than any previously found one. It takes the node to update, the new possible best predecessor and the cost for reaching that predecessor.

type Heuristic

type Heuristic = func(*Node) int

Heuristic is a function that estimates the remaining cost to reach the end for a node. It must always return an integer cost value, even for nodes it does not know. For the algorithm to be guaranteed to return the least-cost path, the heuristic must never over-estimate the actual costs. In many cases, the direct, line-of-sight distance is a good heuristic.

func CreateConstantHeuristic2D added in v0.2.0

func CreateConstantHeuristic2D(
	posMap map[[2]int]*Node, dest [2]int, defaultVal int,
) (Heuristic, error)

CreateConstantHeuristic2D creates a constant heuristic for a regular 2D grid. It takes a map from node positions to node pointers and the desired end position and returns a simple, constant heuristic that estimates the remaining cost as the line-of-sight distance to the desired destination. Heuristics must return a value for all nodes, even ones they don't remember. For such nodes, it returns `defaultVal`.

type Node

type Node struct {
	// Public members follow.
	// ID identifies the node. It is just a nice representation for the user and not used by the
	// algorithm.
	ID string
	// Cost specifies the cost of accessing this node from one connected to it.
	Cost int
	// Payload is some arbitrary user-defined payload that can be used with the heuristic, for
	// example. Type checks are the user's obligation.
	Payload interface{}
	// contains filtered or unexported fields
}

Node is a node for a connected graph along which to travel. Use NewNode to create one. It *will* be modified while the algorithm is being executed but FindPath reverts it to its original state at the end.

func ExtractPath

func ExtractPath(end, start *Node, orgOrder bool) ([]*Node, error)

ExtractPath follows the connection from the end to the beginning and returns it. It begins at end and follows the prev member until it reaches start or until there is no prev member. In the latter case, an error is returned. Specify whether you want the original path, which is in reverse order, or the path from the original start to the end.

func FindPath

func FindPath(graph GraphOps, start, end *Node, heuristic Heuristic) (path []*Node, err error)

FindPath finds the path between the start and end node. It is a convenience wrapper around FindReversePath. FindPath takes a graph in the form of a set of nodes, a start node, and an end node. It returns errors in case there are problems with the input or during execution. The path is returned in the correct order. This is achieved by using the normal algorithm and reversing the path at the end.

This function requires you to provide one of the graph types from this package as the `graph` argument. Use NewGraph or NewHeapedGraph to obtain a suitable data structure. If you want to use your own implementation of a GraphOps, you will need to use FindReversePath instead.

This implementation modifies the original nodes during execution! In the end, the nodes are reverted to null states (private members set to the appropriate zero values), which allows you to use the same input graph again with FindPath.

FindPath also takes a heuristic that estimates the cost for moving from a node to the end. In the easiest case, this can be built using ConstantHeuristic. This heuristic is evaluated exactly once for a node when adding that node to an internal graph.

This function is guaranteed to handle panics from this package and not to propagate the panic.

func NewNode

func NewNode(id string, cost int, numExpectedNeighbours int, payload interface{}) (*Node, error)

NewNode creates a new node. Provide an id string that describes this node for the user. Also provide a non-negative cost value. If the cost is negative, an error is returned. For performance reasons, specify the number of expected neighbours. A non-positive value means you are not sure or don't want to optimise this part, which is fine, too.

func (*Node) AddConnection

func (n *Node) AddConnection(neighbour *Node)

AddConnection adds a connection to a node. If the connection already exists, this is a no-op.

func (*Node) AddPairwiseConnection

func (n *Node) AddPairwiseConnection(neighbour *Node)

AddPairwiseConnection adds a connection to a node and from that node back to the receiver.. If the connection already exists, this is a no-op.

func (*Node) RemoveConnection

func (n *Node) RemoveConnection(neighbour *Node)

RemoveConnection removes a connection to a node. If the speicified node does not connect to this node, this is a no-op.

func (Node) String

func (n Node) String() string

String obtains a string representation suitable for use with fmt's Print functions.

func (*Node) ToString

func (n *Node) ToString() string

ToString provides a nice string representation for this node. Not all members are used.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL