| // Copyright ©2023 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 ( |
| "fmt" |
| "math" |
| "reflect" |
| "testing" |
| |
| "golang.org/x/exp/rand" |
| |
| "gonum.org/v1/gonum/graph" |
| "gonum.org/v1/gonum/graph/graphs/gen" |
| "gonum.org/v1/gonum/graph/internal/ordered" |
| "gonum.org/v1/gonum/graph/simple" |
| ) |
| |
| var shortestTests = []struct { |
| n, d int |
| p float64 |
| seed uint64 |
| }{ |
| {n: 100, d: 2, p: 0.5, seed: 1}, |
| {n: 200, d: 2, p: 0.5, seed: 1}, |
| {n: 100, d: 4, p: 0.25, seed: 1}, |
| {n: 200, d: 4, p: 0.25, seed: 1}, |
| {n: 100, d: 16, p: 0.1, seed: 1}, |
| {n: 200, d: 16, p: 0.1, seed: 1}, |
| } |
| |
| func TestShortestAlts(t *testing.T) { |
| for _, test := range shortestTests { |
| t.Run(fmt.Sprintf("AllTo_%d×%d|%v", test.n, test.d, test.p), func(t *testing.T) { |
| g := simple.NewDirectedGraph() |
| gen.SmallWorldsBB(g, test.n, test.d, test.p, rand.New(rand.NewSource(test.seed))) |
| all := allShortest(DijkstraAllPaths(g)) |
| |
| for uid := int64(0); uid < int64(test.n); uid++ { |
| p := DijkstraAllFrom(g.Node(uid), g) |
| for vid := int64(0); vid < int64(test.n); vid++ { |
| got, gotW := p.AllTo(vid) |
| want, wantW := all.AllBetween(uid, vid) |
| if gotW != wantW { |
| t.Errorf("mismatched weight: got:%f want:%f", gotW, wantW) |
| continue |
| } |
| |
| var gotPaths [][]int64 |
| if len(got) != 0 { |
| gotPaths = make([][]int64, len(got)) |
| } |
| for i, p := range got { |
| for _, v := range p { |
| gotPaths[i] = append(gotPaths[i], v.ID()) |
| } |
| } |
| ordered.BySliceValues(gotPaths) |
| var wantPaths [][]int64 |
| if len(want) != 0 { |
| wantPaths = make([][]int64, len(want)) |
| } |
| for i, p := range want { |
| for _, v := range p { |
| wantPaths[i] = append(wantPaths[i], v.ID()) |
| } |
| } |
| ordered.BySliceValues(wantPaths) |
| if !reflect.DeepEqual(gotPaths, wantPaths) { |
| t.Errorf("unexpected shortest paths %d --> %d:\ngot: %v\nwant:%v", |
| uid, vid, gotPaths, wantPaths) |
| } |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestAllShortest(t *testing.T) { |
| for _, test := range shortestTests { |
| t.Run(fmt.Sprintf("AllBetween_%d×%d|%v", test.n, test.d, test.p), func(t *testing.T) { |
| g := simple.NewDirectedGraph() |
| gen.SmallWorldsBB(g, test.n, test.d, test.p, rand.New(rand.NewSource(test.seed))) |
| |
| p := DijkstraAllPaths(g) |
| for uid := int64(0); uid < int64(test.n); uid++ { |
| for vid := int64(0); vid < int64(test.n); vid++ { |
| got, gotW := p.AllBetween(uid, vid) |
| want, wantW := allShortest(p).AllBetween(uid, vid) // Compare to naive. |
| if gotW != wantW { |
| t.Errorf("mismatched weight: got:%f want:%f", gotW, wantW) |
| continue |
| } |
| |
| var gotPaths [][]int64 |
| if len(got) != 0 { |
| gotPaths = make([][]int64, len(got)) |
| } |
| for i, p := range got { |
| for _, v := range p { |
| gotPaths[i] = append(gotPaths[i], v.ID()) |
| } |
| } |
| ordered.BySliceValues(gotPaths) |
| var wantPaths [][]int64 |
| if len(want) != 0 { |
| wantPaths = make([][]int64, len(want)) |
| } |
| for i, p := range want { |
| for _, v := range p { |
| wantPaths[i] = append(wantPaths[i], v.ID()) |
| } |
| } |
| ordered.BySliceValues(wantPaths) |
| if !reflect.DeepEqual(gotPaths, wantPaths) { |
| t.Errorf("unexpected shortest paths %d --> %d:\ngot: %v\nwant:%v", |
| uid, vid, gotPaths, wantPaths) |
| } |
| } |
| } |
| }) |
| } |
| } |
| |
| // allShortest implements an allocation-naive AllBetween. |
| type allShortest AllShortest |
| |
| // at returns a slice of node indexes into p.nodes for nodes that are mid points |
| // between nodes indexed by from and to. |
| func (p allShortest) at(from, to int) (mid []int) { |
| return p.next[from+to*len(p.nodes)] |
| } |
| |
| // AllBetween returns all shortest paths from u to v and the weight of the paths. Paths |
| // containing zero-weight cycles are not returned. If a negative cycle exists between |
| // u and v, paths is returned nil and weight is returned as -Inf. |
| func (p allShortest) AllBetween(uid, vid int64) (paths [][]graph.Node, weight float64) { |
| from, fromOK := p.indexOf[uid] |
| to, toOK := p.indexOf[vid] |
| if !fromOK || !toOK || len(p.at(from, to)) == 0 { |
| if uid == vid { |
| if !fromOK { |
| return [][]graph.Node{{node(uid)}}, 0 |
| } |
| return [][]graph.Node{{p.nodes[from]}}, 0 |
| } |
| return nil, math.Inf(1) |
| } |
| |
| weight = p.dist.At(from, to) |
| if math.Float64bits(weight) == defacedBits { |
| return nil, math.Inf(-1) |
| } |
| |
| var n graph.Node |
| if p.forward { |
| n = p.nodes[from] |
| } else { |
| n = p.nodes[to] |
| } |
| seen := make([]bool, len(p.nodes)) |
| paths = p.allBetween(from, to, seen, []graph.Node{n}, nil) |
| |
| return paths, weight |
| } |
| |
| // allBetween recursively constructs a slice of paths extending from the node |
| // indexed into p.nodes by from to the node indexed by to. len(seen) must match |
| // the number of nodes held by the receiver. The path parameter is the current |
| // working path and the results are written into paths. |
| func (p allShortest) allBetween(from, to int, seen []bool, path []graph.Node, paths [][]graph.Node) [][]graph.Node { |
| if p.forward { |
| seen[from] = true |
| } else { |
| seen[to] = true |
| } |
| if from == to { |
| if path == nil { |
| return paths |
| } |
| if !p.forward { |
| ordered.Reverse(path) |
| } |
| return append(paths, path) |
| } |
| first := true |
| for _, n := range p.at(from, to) { |
| if seen[n] { |
| continue |
| } |
| if first { |
| path = append([]graph.Node(nil), path...) |
| first = false |
| } |
| if p.forward { |
| from = n |
| } else { |
| to = n |
| } |
| path = path[:len(path):len(path)] |
| paths = p.allBetween(from, to, append([]bool(nil), seen...), append(path, p.nodes[n]), paths) |
| } |
| return paths |
| } |
| |
| var shortestBenchmarks = []struct { |
| n, d int |
| p float64 |
| seed uint64 |
| }{ |
| {n: 100, d: 2, p: 0.5, seed: 1}, |
| {n: 1000, d: 2, p: 0.5, seed: 1}, |
| {n: 100, d: 4, p: 0.25, seed: 1}, |
| {n: 1000, d: 4, p: 0.25, seed: 1}, |
| {n: 100, d: 16, p: 0.1, seed: 1}, |
| {n: 1000, d: 16, p: 0.1, seed: 1}, |
| } |
| |
| func BenchmarkShortestAlts(b *testing.B) { |
| for _, bench := range shortestBenchmarks { |
| g := simple.NewDirectedGraph() |
| gen.SmallWorldsBB(g, bench.n, bench.d, bench.p, rand.New(rand.NewSource(bench.seed))) |
| |
| // Find the widest path set. |
| var ( |
| bestP ShortestAlts |
| bestVid int64 |
| n int |
| ) |
| for uid := int64(0); uid < int64(bench.n); uid++ { |
| p := DijkstraAllFrom(g.Node(uid), g) |
| for vid := int64(0); vid < int64(bench.n); vid++ { |
| paths, _ := p.AllTo(vid) |
| if len(paths) > n { |
| n = len(paths) |
| bestP = p |
| bestVid = vid |
| } |
| } |
| } |
| |
| b.Run(fmt.Sprintf("AllTo_%d×%d|%v(%d)", bench.n, bench.d, bench.p, n), func(b *testing.B) { |
| for i := 0; i < b.N; i++ { |
| paths, _ := bestP.AllTo(bestVid) |
| if len(paths) != n { |
| b.Errorf("unexpected number of paths: got:%d want:%d", len(paths), n) |
| } |
| } |
| }) |
| b.Run(fmt.Sprintf("AllToFunc_%d×%d|%v(%d)", bench.n, bench.d, bench.p, n), func(b *testing.B) { |
| for i := 0; i < b.N; i++ { |
| var paths int |
| bestP.AllToFunc(bestVid, func(_ []graph.Node) { paths++ }) |
| if paths != n { |
| b.Errorf("unexpected number of paths: got:%d want:%d", paths, n) |
| } |
| } |
| }) |
| } |
| } |
| |
| func BenchmarkAllShortest(b *testing.B) { |
| for _, bench := range shortestBenchmarks { |
| g := simple.NewDirectedGraph() |
| gen.SmallWorldsBB(g, bench.n, bench.d, bench.p, rand.New(rand.NewSource(bench.seed))) |
| p := DijkstraAllPaths(g) |
| |
| // Find the widest path set. |
| var ( |
| bestUid, bestVid int64 |
| n int |
| ) |
| for uid := int64(0); uid < int64(bench.n); uid++ { |
| for vid := int64(0); vid < int64(bench.n); vid++ { |
| paths, _ := p.AllBetween(uid, vid) |
| if len(paths) > n { |
| n = len(paths) |
| bestUid = uid |
| bestVid = vid |
| } |
| } |
| } |
| |
| b.Run(fmt.Sprintf("AllBetween_%d×%d|%v(%d)", bench.n, bench.d, bench.p, n), func(b *testing.B) { |
| for i := 0; i < b.N; i++ { |
| paths, _ := p.AllBetween(bestUid, bestVid) |
| if len(paths) != n { |
| b.Errorf("unexpected number of paths: got:%d want:%d", len(paths), n) |
| } |
| } |
| }) |
| b.Run(fmt.Sprintf("AllBetweenFunc_%d×%d|%v(%d)", bench.n, bench.d, bench.p, n), func(b *testing.B) { |
| for i := 0; i < b.N; i++ { |
| var paths int |
| p.AllBetweenFunc(bestUid, bestVid, func(_ []graph.Node) { paths++ }) |
| if paths != n { |
| b.Errorf("unexpected number of paths: got:%d want:%d", paths, n) |
| } |
| } |
| }) |
| } |
| } |