mirror of
https://github.com/gonum/gonum.git
synced 2025-10-05 15:16:59 +08:00
315 lines
7.4 KiB
Go
315 lines
7.4 KiB
Go
// Copyright ©2014 The Gonum Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package path
|
|
|
|
import (
|
|
"math"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"gonum.org/v1/gonum/graph"
|
|
"gonum.org/v1/gonum/graph/path/internal/testgraphs"
|
|
"gonum.org/v1/gonum/graph/simple"
|
|
"gonum.org/v1/gonum/graph/topo"
|
|
)
|
|
|
|
var aStarTests = []struct {
|
|
name string
|
|
g graph.Graph
|
|
|
|
s, t int64
|
|
heuristic Heuristic
|
|
wantPath []int64
|
|
}{
|
|
{
|
|
name: "simple path",
|
|
g: func() graph.Graph {
|
|
return testgraphs.NewGridFrom(
|
|
"*..*",
|
|
"**.*",
|
|
"**.*",
|
|
"**.*",
|
|
)
|
|
}(),
|
|
|
|
s: 1, t: 14,
|
|
wantPath: []int64{1, 2, 6, 10, 14},
|
|
},
|
|
{
|
|
name: "small open graph",
|
|
g: testgraphs.NewGrid(3, 3, true),
|
|
|
|
s: 0, t: 8,
|
|
},
|
|
{
|
|
name: "large open graph",
|
|
g: testgraphs.NewGrid(1000, 1000, true),
|
|
|
|
s: 0, t: 999*1000 + 999,
|
|
},
|
|
{
|
|
name: "no path",
|
|
g: func() graph.Graph {
|
|
tg := testgraphs.NewGrid(5, 5, true)
|
|
|
|
// Create a complete "wall" across the middle row.
|
|
tg.Set(2, 0, false)
|
|
tg.Set(2, 1, false)
|
|
tg.Set(2, 2, false)
|
|
tg.Set(2, 3, false)
|
|
tg.Set(2, 4, false)
|
|
|
|
return tg
|
|
}(),
|
|
|
|
s: 2, t: 22,
|
|
},
|
|
{
|
|
name: "partially obstructed",
|
|
g: func() graph.Graph {
|
|
tg := testgraphs.NewGrid(10, 10, true)
|
|
|
|
// Create a partial "wall" across the middle
|
|
// row with a gap at the left-hand end.
|
|
tg.Set(4, 1, false)
|
|
tg.Set(4, 2, false)
|
|
tg.Set(4, 3, false)
|
|
tg.Set(4, 4, false)
|
|
tg.Set(4, 5, false)
|
|
tg.Set(4, 6, false)
|
|
tg.Set(4, 7, false)
|
|
tg.Set(4, 8, false)
|
|
tg.Set(4, 9, false)
|
|
|
|
return tg
|
|
}(),
|
|
|
|
s: 5, t: 9*10 + 9,
|
|
},
|
|
{
|
|
name: "partially obstructed with heuristic",
|
|
g: func() graph.Graph {
|
|
tg := testgraphs.NewGrid(10, 10, true)
|
|
|
|
// Create a partial "wall" across the middle
|
|
// row with a gap at the left-hand end.
|
|
tg.Set(4, 1, false)
|
|
tg.Set(4, 2, false)
|
|
tg.Set(4, 3, false)
|
|
tg.Set(4, 4, false)
|
|
tg.Set(4, 5, false)
|
|
tg.Set(4, 6, false)
|
|
tg.Set(4, 7, false)
|
|
tg.Set(4, 8, false)
|
|
tg.Set(4, 9, false)
|
|
|
|
return tg
|
|
}(),
|
|
|
|
s: 5, t: 9*10 + 9,
|
|
// Manhattan Heuristic
|
|
heuristic: func(u, v graph.Node) float64 {
|
|
uid := u.ID()
|
|
cu := (uid % 10)
|
|
ru := (uid - cu) / 10
|
|
|
|
vid := v.ID()
|
|
cv := (vid % 10)
|
|
rv := (vid - cv) / 10
|
|
|
|
return math.Abs(float64(ru-rv)) + math.Abs(float64(cu-cv))
|
|
},
|
|
},
|
|
}
|
|
|
|
func TestAStar(t *testing.T) {
|
|
t.Parallel()
|
|
for _, test := range aStarTests {
|
|
pt, _ := AStar(simple.Node(test.s), simple.Node(test.t), test.g, test.heuristic)
|
|
|
|
p, cost := pt.To(test.t)
|
|
|
|
if !topo.IsPathIn(test.g, p) {
|
|
t.Errorf("got path that is not path in input graph for %q", test.name)
|
|
}
|
|
|
|
bfp, ok := BellmanFordFrom(simple.Node(test.s), test.g)
|
|
if !ok {
|
|
t.Fatalf("unexpected negative cycle in %q", test.name)
|
|
}
|
|
if want := bfp.WeightTo(test.t); cost != want {
|
|
t.Errorf("unexpected cost for %q: got:%v want:%v", test.name, cost, want)
|
|
}
|
|
|
|
var got = make([]int64, 0, len(p))
|
|
for _, n := range p {
|
|
got = append(got, n.ID())
|
|
}
|
|
if test.wantPath != nil && !reflect.DeepEqual(got, test.wantPath) {
|
|
t.Errorf("unexpected result for %q:\ngot: %v\nwant:%v", test.name, got, test.wantPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExhaustiveAStar(t *testing.T) {
|
|
t.Parallel()
|
|
g := simple.NewWeightedUndirectedGraph(0, math.Inf(1))
|
|
nodes := []locatedNode{
|
|
{id: 1, x: 0, y: 6},
|
|
{id: 2, x: 1, y: 0},
|
|
{id: 3, x: 8, y: 7},
|
|
{id: 4, x: 16, y: 0},
|
|
{id: 5, x: 17, y: 6},
|
|
{id: 6, x: 9, y: 8},
|
|
}
|
|
for _, n := range nodes {
|
|
g.AddNode(n)
|
|
}
|
|
|
|
edges := []weightedEdge{
|
|
{from: g.Node(1), to: g.Node(2), cost: 7},
|
|
{from: g.Node(1), to: g.Node(3), cost: 9},
|
|
{from: g.Node(1), to: g.Node(6), cost: 14},
|
|
{from: g.Node(2), to: g.Node(3), cost: 10},
|
|
{from: g.Node(2), to: g.Node(4), cost: 15},
|
|
{from: g.Node(3), to: g.Node(4), cost: 11},
|
|
{from: g.Node(3), to: g.Node(6), cost: 2},
|
|
{from: g.Node(4), to: g.Node(5), cost: 7},
|
|
{from: g.Node(5), to: g.Node(6), cost: 9},
|
|
}
|
|
for _, e := range edges {
|
|
g.SetWeightedEdge(e)
|
|
}
|
|
|
|
heuristic := func(u, v graph.Node) float64 {
|
|
lu := u.(locatedNode)
|
|
lv := v.(locatedNode)
|
|
return math.Hypot(lu.x-lv.x, lu.y-lv.y)
|
|
}
|
|
|
|
if ok, edge, goal := isMonotonic(g, heuristic); !ok {
|
|
t.Fatalf("non-monotonic heuristic at edge:%v for goal:%v", edge, goal)
|
|
}
|
|
|
|
ps := DijkstraAllPaths(g)
|
|
ends := graph.NodesOf(g.Nodes())
|
|
for _, start := range ends {
|
|
for _, goal := range ends {
|
|
pt, _ := AStar(start, goal, g, heuristic)
|
|
gotPath, gotWeight := pt.To(goal.ID())
|
|
wantPath, wantWeight, _ := ps.Between(start.ID(), goal.ID())
|
|
if gotWeight != wantWeight {
|
|
t.Errorf("unexpected path weight from %v to %v result: got:%f want:%f",
|
|
start, goal, gotWeight, wantWeight)
|
|
}
|
|
if !reflect.DeepEqual(gotPath, wantPath) {
|
|
t.Errorf("unexpected path from %v to %v result:\ngot: %v\nwant:%v",
|
|
start, goal, gotPath, wantPath)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type locatedNode struct {
|
|
id int64
|
|
x, y float64
|
|
}
|
|
|
|
func (n locatedNode) ID() int64 { return n.id }
|
|
|
|
type weightedEdge struct {
|
|
from, to graph.Node
|
|
cost float64
|
|
}
|
|
|
|
func (e weightedEdge) From() graph.Node { return e.from }
|
|
func (e weightedEdge) To() graph.Node { return e.to }
|
|
func (e weightedEdge) ReversedEdge() graph.Edge { e.from, e.to = e.to, e.from; return e }
|
|
func (e weightedEdge) Weight() float64 { return e.cost }
|
|
|
|
func isMonotonic(g UndirectedWeightLister, h Heuristic) (ok bool, at graph.Edge, goal graph.Node) {
|
|
for _, goal := range graph.NodesOf(g.Nodes()) {
|
|
for _, edge := range graph.WeightedEdgesOf(g.WeightedEdges()) {
|
|
from := edge.From()
|
|
to := edge.To()
|
|
w, ok := g.Weight(from.ID(), to.ID())
|
|
if !ok {
|
|
panic("A*: unexpected invalid weight")
|
|
}
|
|
if h(from, goal) > w+h(to, goal) {
|
|
return false, edge, goal
|
|
}
|
|
}
|
|
}
|
|
return true, nil, nil
|
|
}
|
|
|
|
func TestAStarNullHeuristic(t *testing.T) {
|
|
t.Parallel()
|
|
for _, test := range testgraphs.ShortestPathTests {
|
|
g := test.Graph()
|
|
for _, e := range test.Edges {
|
|
g.SetWeightedEdge(e)
|
|
}
|
|
|
|
var (
|
|
pt Shortest
|
|
|
|
panicked bool
|
|
)
|
|
func() {
|
|
defer func() {
|
|
panicked = recover() != nil
|
|
}()
|
|
pt, _ = AStar(test.Query.From(), test.Query.To(), g.(graph.Graph), nil)
|
|
}()
|
|
if panicked || test.HasNegativeWeight {
|
|
if !test.HasNegativeWeight {
|
|
t.Errorf("%q: unexpected panic", test.Name)
|
|
}
|
|
if !panicked {
|
|
t.Errorf("%q: expected panic for negative edge weight", test.Name)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if pt.From().ID() != test.Query.From().ID() {
|
|
t.Fatalf("%q: unexpected from node ID: got:%d want:%d", test.Name, pt.From().ID(), test.Query.From().ID())
|
|
}
|
|
|
|
p, weight := pt.To(test.Query.To().ID())
|
|
if weight != test.Weight {
|
|
t.Errorf("%q: unexpected weight from To: got:%f want:%f",
|
|
test.Name, weight, test.Weight)
|
|
}
|
|
if weight := pt.WeightTo(test.Query.To().ID()); weight != test.Weight {
|
|
t.Errorf("%q: unexpected weight from Weight: got:%f want:%f",
|
|
test.Name, weight, test.Weight)
|
|
}
|
|
|
|
var got []int64
|
|
for _, n := range p {
|
|
got = append(got, n.ID())
|
|
}
|
|
ok := len(got) == 0 && len(test.WantPaths) == 0
|
|
for _, sp := range test.WantPaths {
|
|
if reflect.DeepEqual(got, sp) {
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
if !ok {
|
|
t.Errorf("%q: unexpected shortest path:\ngot: %v\nwant from:%v",
|
|
test.Name, p, test.WantPaths)
|
|
}
|
|
|
|
np, weight := pt.To(test.NoPathFor.To().ID())
|
|
if pt.From().ID() == test.NoPathFor.From().ID() && (np != nil || !math.IsInf(weight, 1)) {
|
|
t.Errorf("%q: unexpected path:\ngot: path=%v weight=%f\nwant:path=<nil> weight=+Inf",
|
|
test.Name, np, weight)
|
|
}
|
|
}
|
|
}
|