gonum.org/v1/gonum@v0.14.0/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 "gonum.org/v1/gonum/graph" 13 "gonum.org/v1/gonum/graph/path/internal/testgraphs" 14 "gonum.org/v1/gonum/graph/simple" 15 "gonum.org/v1/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 }