github.com/gopherd/gonum@v0.0.4/graph/path/a_star_test.go (about)

     1  // Copyright ©2014 The Gonum Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package path
     6  
     7  import (
     8  	"math"
     9  	"reflect"
    10  	"testing"
    11  
    12  	"github.com/gopherd/gonum/graph"
    13  	"github.com/gopherd/gonum/graph/path/internal/testgraphs"
    14  	"github.com/gopherd/gonum/graph/simple"
    15  	"github.com/gopherd/gonum/graph/topo"
    16  )
    17  
    18  var aStarTests = []struct {
    19  	name string
    20  	g    graph.Graph
    21  
    22  	s, t      int64
    23  	heuristic Heuristic
    24  	wantPath  []int64
    25  }{
    26  	{
    27  		name: "simple path",
    28  		g: func() graph.Graph {
    29  			return testgraphs.NewGridFrom(
    30  				"*..*",
    31  				"**.*",
    32  				"**.*",
    33  				"**.*",
    34  			)
    35  		}(),
    36  
    37  		s: 1, t: 14,
    38  		wantPath: []int64{1, 2, 6, 10, 14},
    39  	},
    40  	{
    41  		name: "small open graph",
    42  		g:    testgraphs.NewGrid(3, 3, true),
    43  
    44  		s: 0, t: 8,
    45  	},
    46  	{
    47  		name: "large open graph",
    48  		g:    testgraphs.NewGrid(1000, 1000, true),
    49  
    50  		s: 0, t: 999*1000 + 999,
    51  	},
    52  	{
    53  		name: "no path",
    54  		g: func() graph.Graph {
    55  			tg := testgraphs.NewGrid(5, 5, true)
    56  
    57  			// Create a complete "wall" across the middle row.
    58  			tg.Set(2, 0, false)
    59  			tg.Set(2, 1, false)
    60  			tg.Set(2, 2, false)
    61  			tg.Set(2, 3, false)
    62  			tg.Set(2, 4, false)
    63  
    64  			return tg
    65  		}(),
    66  
    67  		s: 2, t: 22,
    68  	},
    69  	{
    70  		name: "partially obstructed",
    71  		g: func() graph.Graph {
    72  			tg := testgraphs.NewGrid(10, 10, true)
    73  
    74  			// Create a partial "wall" across the middle
    75  			// row with a gap at the left-hand end.
    76  			tg.Set(4, 1, false)
    77  			tg.Set(4, 2, false)
    78  			tg.Set(4, 3, false)
    79  			tg.Set(4, 4, false)
    80  			tg.Set(4, 5, false)
    81  			tg.Set(4, 6, false)
    82  			tg.Set(4, 7, false)
    83  			tg.Set(4, 8, false)
    84  			tg.Set(4, 9, false)
    85  
    86  			return tg
    87  		}(),
    88  
    89  		s: 5, t: 9*10 + 9,
    90  	},
    91  	{
    92  		name: "partially obstructed with heuristic",
    93  		g: func() graph.Graph {
    94  			tg := testgraphs.NewGrid(10, 10, true)
    95  
    96  			// Create a partial "wall" across the middle
    97  			// row with a gap at the left-hand end.
    98  			tg.Set(4, 1, false)
    99  			tg.Set(4, 2, false)
   100  			tg.Set(4, 3, false)
   101  			tg.Set(4, 4, false)
   102  			tg.Set(4, 5, false)
   103  			tg.Set(4, 6, false)
   104  			tg.Set(4, 7, false)
   105  			tg.Set(4, 8, false)
   106  			tg.Set(4, 9, false)
   107  
   108  			return tg
   109  		}(),
   110  
   111  		s: 5, t: 9*10 + 9,
   112  		// Manhattan Heuristic
   113  		heuristic: func(u, v graph.Node) float64 {
   114  			uid := u.ID()
   115  			cu := (uid % 10)
   116  			ru := (uid - cu) / 10
   117  
   118  			vid := v.ID()
   119  			cv := (vid % 10)
   120  			rv := (vid - cv) / 10
   121  
   122  			return math.Abs(float64(ru-rv)) + math.Abs(float64(cu-cv))
   123  		},
   124  	},
   125  }
   126  
   127  func TestAStar(t *testing.T) {
   128  	t.Parallel()
   129  	for _, test := range aStarTests {
   130  		pt, _ := AStar(simple.Node(test.s), simple.Node(test.t), test.g, test.heuristic)
   131  
   132  		p, cost := pt.To(test.t)
   133  
   134  		if !topo.IsPathIn(test.g, p) {
   135  			t.Errorf("got path that is not path in input graph for %q", test.name)
   136  		}
   137  
   138  		bfp, ok := BellmanFordFrom(simple.Node(test.s), test.g)
   139  		if !ok {
   140  			t.Fatalf("unexpected negative cycle in %q", test.name)
   141  		}
   142  		if want := bfp.WeightTo(test.t); cost != want {
   143  			t.Errorf("unexpected cost for %q: got:%v want:%v", test.name, cost, want)
   144  		}
   145  
   146  		var got = make([]int64, 0, len(p))
   147  		for _, n := range p {
   148  			got = append(got, n.ID())
   149  		}
   150  		if test.wantPath != nil && !reflect.DeepEqual(got, test.wantPath) {
   151  			t.Errorf("unexpected result for %q:\ngot: %v\nwant:%v", test.name, got, test.wantPath)
   152  		}
   153  	}
   154  }
   155  
   156  func TestExhaustiveAStar(t *testing.T) {
   157  	t.Parallel()
   158  	g := simple.NewWeightedUndirectedGraph(0, math.Inf(1))
   159  	nodes := []locatedNode{
   160  		{id: 1, x: 0, y: 6},
   161  		{id: 2, x: 1, y: 0},
   162  		{id: 3, x: 8, y: 7},
   163  		{id: 4, x: 16, y: 0},
   164  		{id: 5, x: 17, y: 6},
   165  		{id: 6, x: 9, y: 8},
   166  	}
   167  	for _, n := range nodes {
   168  		g.AddNode(n)
   169  	}
   170  
   171  	edges := []weightedEdge{
   172  		{from: g.Node(1), to: g.Node(2), cost: 7},
   173  		{from: g.Node(1), to: g.Node(3), cost: 9},
   174  		{from: g.Node(1), to: g.Node(6), cost: 14},
   175  		{from: g.Node(2), to: g.Node(3), cost: 10},
   176  		{from: g.Node(2), to: g.Node(4), cost: 15},
   177  		{from: g.Node(3), to: g.Node(4), cost: 11},
   178  		{from: g.Node(3), to: g.Node(6), cost: 2},
   179  		{from: g.Node(4), to: g.Node(5), cost: 7},
   180  		{from: g.Node(5), to: g.Node(6), cost: 9},
   181  	}
   182  	for _, e := range edges {
   183  		g.SetWeightedEdge(e)
   184  	}
   185  
   186  	heuristic := func(u, v graph.Node) float64 {
   187  		lu := u.(locatedNode)
   188  		lv := v.(locatedNode)
   189  		return math.Hypot(lu.x-lv.x, lu.y-lv.y)
   190  	}
   191  
   192  	if ok, edge, goal := isMonotonic(g, heuristic); !ok {
   193  		t.Fatalf("non-monotonic heuristic at edge:%v for goal:%v", edge, goal)
   194  	}
   195  
   196  	ps := DijkstraAllPaths(g)
   197  	ends := graph.NodesOf(g.Nodes())
   198  	for _, start := range ends {
   199  		for _, goal := range ends {
   200  			pt, _ := AStar(start, goal, g, heuristic)
   201  			gotPath, gotWeight := pt.To(goal.ID())
   202  			wantPath, wantWeight, _ := ps.Between(start.ID(), goal.ID())
   203  			if gotWeight != wantWeight {
   204  				t.Errorf("unexpected path weight from %v to %v result: got:%f want:%f",
   205  					start, goal, gotWeight, wantWeight)
   206  			}
   207  			if !reflect.DeepEqual(gotPath, wantPath) {
   208  				t.Errorf("unexpected path from %v to %v result:\ngot: %v\nwant:%v",
   209  					start, goal, gotPath, wantPath)
   210  			}
   211  		}
   212  	}
   213  }
   214  
   215  type locatedNode struct {
   216  	id   int64
   217  	x, y float64
   218  }
   219  
   220  func (n locatedNode) ID() int64 { return n.id }
   221  
   222  type weightedEdge struct {
   223  	from, to graph.Node
   224  	cost     float64
   225  }
   226  
   227  func (e weightedEdge) From() graph.Node         { return e.from }
   228  func (e weightedEdge) To() graph.Node           { return e.to }
   229  func (e weightedEdge) ReversedEdge() graph.Edge { e.from, e.to = e.to, e.from; return e }
   230  func (e weightedEdge) Weight() float64          { return e.cost }
   231  
   232  func isMonotonic(g UndirectedWeightLister, h Heuristic) (ok bool, at graph.Edge, goal graph.Node) {
   233  	for _, goal := range graph.NodesOf(g.Nodes()) {
   234  		for _, edge := range graph.WeightedEdgesOf(g.WeightedEdges()) {
   235  			from := edge.From()
   236  			to := edge.To()
   237  			w, ok := g.Weight(from.ID(), to.ID())
   238  			if !ok {
   239  				panic("A*: unexpected invalid weight")
   240  			}
   241  			if h(from, goal) > w+h(to, goal) {
   242  				return false, edge, goal
   243  			}
   244  		}
   245  	}
   246  	return true, nil, nil
   247  }
   248  
   249  func TestAStarNullHeuristic(t *testing.T) {
   250  	t.Parallel()
   251  	for _, test := range testgraphs.ShortestPathTests {
   252  		g := test.Graph()
   253  		for _, e := range test.Edges {
   254  			g.SetWeightedEdge(e)
   255  		}
   256  
   257  		var (
   258  			pt Shortest
   259  
   260  			panicked bool
   261  		)
   262  		func() {
   263  			defer func() {
   264  				panicked = recover() != nil
   265  			}()
   266  			pt, _ = AStar(test.Query.From(), test.Query.To(), g.(graph.Graph), nil)
   267  		}()
   268  		if panicked || test.HasNegativeWeight {
   269  			if !test.HasNegativeWeight {
   270  				t.Errorf("%q: unexpected panic", test.Name)
   271  			}
   272  			if !panicked {
   273  				t.Errorf("%q: expected panic for negative edge weight", test.Name)
   274  			}
   275  			continue
   276  		}
   277  
   278  		if pt.From().ID() != test.Query.From().ID() {
   279  			t.Fatalf("%q: unexpected from node ID: got:%d want:%d", test.Name, pt.From().ID(), test.Query.From().ID())
   280  		}
   281  
   282  		p, weight := pt.To(test.Query.To().ID())
   283  		if weight != test.Weight {
   284  			t.Errorf("%q: unexpected weight from To: got:%f want:%f",
   285  				test.Name, weight, test.Weight)
   286  		}
   287  		if weight := pt.WeightTo(test.Query.To().ID()); weight != test.Weight {
   288  			t.Errorf("%q: unexpected weight from Weight: got:%f want:%f",
   289  				test.Name, weight, test.Weight)
   290  		}
   291  
   292  		var got []int64
   293  		for _, n := range p {
   294  			got = append(got, n.ID())
   295  		}
   296  		ok := len(got) == 0 && len(test.WantPaths) == 0
   297  		for _, sp := range test.WantPaths {
   298  			if reflect.DeepEqual(got, sp) {
   299  				ok = true
   300  				break
   301  			}
   302  		}
   303  		if !ok {
   304  			t.Errorf("%q: unexpected shortest path:\ngot: %v\nwant from:%v",
   305  				test.Name, p, test.WantPaths)
   306  		}
   307  
   308  		np, weight := pt.To(test.NoPathFor.To().ID())
   309  		if pt.From().ID() == test.NoPathFor.From().ID() && (np != nil || !math.IsInf(weight, 1)) {
   310  			t.Errorf("%q: unexpected path:\ngot: path=%v weight=%f\nwant:path=<nil> weight=+Inf",
   311  				test.Name, np, weight)
   312  		}
   313  	}
   314  }