gonum.org/v1/gonum@v0.15.1-0.20240517103525-f853624cb1bb/graph/path/shortest_test.go (about) 1 // Copyright ©2023 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 "fmt" 9 "math" 10 "reflect" 11 "slices" 12 "testing" 13 14 "golang.org/x/exp/rand" 15 16 "gonum.org/v1/gonum/graph" 17 "gonum.org/v1/gonum/graph/graphs/gen" 18 "gonum.org/v1/gonum/graph/simple" 19 "gonum.org/v1/gonum/internal/order" 20 ) 21 22 var shortestTests = []struct { 23 n, d int 24 p float64 25 seed uint64 26 }{ 27 {n: 100, d: 2, p: 0.5, seed: 1}, 28 {n: 200, d: 2, p: 0.5, seed: 1}, 29 {n: 100, d: 4, p: 0.25, seed: 1}, 30 {n: 200, d: 4, p: 0.25, seed: 1}, 31 {n: 100, d: 16, p: 0.1, seed: 1}, 32 {n: 200, d: 16, p: 0.1, seed: 1}, 33 } 34 35 func TestShortestAlts(t *testing.T) { 36 for _, test := range shortestTests { 37 t.Run(fmt.Sprintf("AllTo_%d×%d|%v", test.n, test.d, test.p), func(t *testing.T) { 38 g := simple.NewDirectedGraph() 39 gen.SmallWorldsBB(g, test.n, test.d, test.p, rand.New(rand.NewSource(test.seed))) 40 all := allShortest(DijkstraAllPaths(g)) 41 42 for uid := int64(0); uid < int64(test.n); uid++ { 43 p := DijkstraAllFrom(g.Node(uid), g) 44 for vid := int64(0); vid < int64(test.n); vid++ { 45 got, gotW := p.AllTo(vid) 46 want, wantW := all.AllBetween(uid, vid) 47 if gotW != wantW { 48 t.Errorf("mismatched weight: got:%f want:%f", gotW, wantW) 49 continue 50 } 51 52 var gotPaths [][]int64 53 if len(got) != 0 { 54 gotPaths = make([][]int64, len(got)) 55 } 56 for i, p := range got { 57 for _, v := range p { 58 gotPaths[i] = append(gotPaths[i], v.ID()) 59 } 60 } 61 order.BySliceValues(gotPaths) 62 var wantPaths [][]int64 63 if len(want) != 0 { 64 wantPaths = make([][]int64, len(want)) 65 } 66 for i, p := range want { 67 for _, v := range p { 68 wantPaths[i] = append(wantPaths[i], v.ID()) 69 } 70 } 71 order.BySliceValues(wantPaths) 72 if !reflect.DeepEqual(gotPaths, wantPaths) { 73 t.Errorf("unexpected shortest paths %d --> %d:\ngot: %v\nwant:%v", 74 uid, vid, gotPaths, wantPaths) 75 } 76 } 77 } 78 }) 79 } 80 } 81 82 func TestAllShortest(t *testing.T) { 83 for _, test := range shortestTests { 84 t.Run(fmt.Sprintf("AllBetween_%d×%d|%v", test.n, test.d, test.p), func(t *testing.T) { 85 g := simple.NewDirectedGraph() 86 gen.SmallWorldsBB(g, test.n, test.d, test.p, rand.New(rand.NewSource(test.seed))) 87 88 p := DijkstraAllPaths(g) 89 for uid := int64(0); uid < int64(test.n); uid++ { 90 for vid := int64(0); vid < int64(test.n); vid++ { 91 got, gotW := p.AllBetween(uid, vid) 92 want, wantW := allShortest(p).AllBetween(uid, vid) // Compare to naive. 93 if gotW != wantW { 94 t.Errorf("mismatched weight: got:%f want:%f", gotW, wantW) 95 continue 96 } 97 98 var gotPaths [][]int64 99 if len(got) != 0 { 100 gotPaths = make([][]int64, len(got)) 101 } 102 for i, p := range got { 103 for _, v := range p { 104 gotPaths[i] = append(gotPaths[i], v.ID()) 105 } 106 } 107 order.BySliceValues(gotPaths) 108 var wantPaths [][]int64 109 if len(want) != 0 { 110 wantPaths = make([][]int64, len(want)) 111 } 112 for i, p := range want { 113 for _, v := range p { 114 wantPaths[i] = append(wantPaths[i], v.ID()) 115 } 116 } 117 order.BySliceValues(wantPaths) 118 if !reflect.DeepEqual(gotPaths, wantPaths) { 119 t.Errorf("unexpected shortest paths %d --> %d:\ngot: %v\nwant:%v", 120 uid, vid, gotPaths, wantPaths) 121 } 122 } 123 } 124 }) 125 } 126 } 127 128 // allShortest implements an allocation-naive AllBetween. 129 type allShortest AllShortest 130 131 // at returns a slice of node indexes into p.nodes for nodes that are mid points 132 // between nodes indexed by from and to. 133 func (p allShortest) at(from, to int) (mid []int) { 134 return p.next[from+to*len(p.nodes)] 135 } 136 137 // AllBetween returns all shortest paths from u to v and the weight of the paths. Paths 138 // containing zero-weight cycles are not returned. If a negative cycle exists between 139 // u and v, paths is returned nil and weight is returned as -Inf. 140 func (p allShortest) AllBetween(uid, vid int64) (paths [][]graph.Node, weight float64) { 141 from, fromOK := p.indexOf[uid] 142 to, toOK := p.indexOf[vid] 143 if !fromOK || !toOK || len(p.at(from, to)) == 0 { 144 if uid == vid { 145 if !fromOK { 146 return [][]graph.Node{{node(uid)}}, 0 147 } 148 return [][]graph.Node{{p.nodes[from]}}, 0 149 } 150 return nil, math.Inf(1) 151 } 152 153 weight = p.dist.At(from, to) 154 if math.Float64bits(weight) == defacedBits { 155 return nil, math.Inf(-1) 156 } 157 158 var n graph.Node 159 if p.forward { 160 n = p.nodes[from] 161 } else { 162 n = p.nodes[to] 163 } 164 seen := make([]bool, len(p.nodes)) 165 paths = p.allBetween(from, to, seen, []graph.Node{n}, nil) 166 167 return paths, weight 168 } 169 170 // allBetween recursively constructs a slice of paths extending from the node 171 // indexed into p.nodes by from to the node indexed by to. len(seen) must match 172 // the number of nodes held by the receiver. The path parameter is the current 173 // working path and the results are written into paths. 174 func (p allShortest) allBetween(from, to int, seen []bool, path []graph.Node, paths [][]graph.Node) [][]graph.Node { 175 if p.forward { 176 seen[from] = true 177 } else { 178 seen[to] = true 179 } 180 if from == to { 181 if path == nil { 182 return paths 183 } 184 if !p.forward { 185 slices.Reverse(path) 186 } 187 return append(paths, path) 188 } 189 first := true 190 for _, n := range p.at(from, to) { 191 if seen[n] { 192 continue 193 } 194 if first { 195 path = append([]graph.Node(nil), path...) 196 first = false 197 } 198 if p.forward { 199 from = n 200 } else { 201 to = n 202 } 203 path = path[:len(path):len(path)] 204 paths = p.allBetween(from, to, append([]bool(nil), seen...), append(path, p.nodes[n]), paths) 205 } 206 return paths 207 } 208 209 var shortestBenchmarks = []struct { 210 n, d int 211 p float64 212 seed uint64 213 }{ 214 {n: 100, d: 2, p: 0.5, seed: 1}, 215 {n: 1000, d: 2, p: 0.5, seed: 1}, 216 {n: 100, d: 4, p: 0.25, seed: 1}, 217 {n: 1000, d: 4, p: 0.25, seed: 1}, 218 {n: 100, d: 16, p: 0.1, seed: 1}, 219 {n: 1000, d: 16, p: 0.1, seed: 1}, 220 } 221 222 func BenchmarkShortestAlts(b *testing.B) { 223 for _, bench := range shortestBenchmarks { 224 g := simple.NewDirectedGraph() 225 gen.SmallWorldsBB(g, bench.n, bench.d, bench.p, rand.New(rand.NewSource(bench.seed))) 226 227 // Find the widest path set. 228 var ( 229 bestP ShortestAlts 230 bestVid int64 231 n int 232 ) 233 for uid := int64(0); uid < int64(bench.n); uid++ { 234 p := DijkstraAllFrom(g.Node(uid), g) 235 for vid := int64(0); vid < int64(bench.n); vid++ { 236 paths, _ := p.AllTo(vid) 237 if len(paths) > n { 238 n = len(paths) 239 bestP = p 240 bestVid = vid 241 } 242 } 243 } 244 245 b.Run(fmt.Sprintf("AllTo_%d×%d|%v(%d)", bench.n, bench.d, bench.p, n), func(b *testing.B) { 246 for i := 0; i < b.N; i++ { 247 paths, _ := bestP.AllTo(bestVid) 248 if len(paths) != n { 249 b.Errorf("unexpected number of paths: got:%d want:%d", len(paths), n) 250 } 251 } 252 }) 253 b.Run(fmt.Sprintf("AllToFunc_%d×%d|%v(%d)", bench.n, bench.d, bench.p, n), func(b *testing.B) { 254 for i := 0; i < b.N; i++ { 255 var paths int 256 bestP.AllToFunc(bestVid, func(_ []graph.Node) { paths++ }) 257 if paths != n { 258 b.Errorf("unexpected number of paths: got:%d want:%d", paths, n) 259 } 260 } 261 }) 262 } 263 } 264 265 func BenchmarkAllShortest(b *testing.B) { 266 shortestPathAlgs := []struct { 267 name string 268 fn func(g graph.Graph) AllShortest 269 }{ 270 { 271 name: "DijkstraAllPaths", 272 fn: DijkstraAllPaths, 273 }, 274 { 275 name: "FloydWarshall", 276 fn: func(g graph.Graph) AllShortest { 277 p, _ := FloydWarshall(g) 278 return p 279 }, 280 }, 281 } 282 283 for _, bench := range shortestBenchmarks { 284 for _, f := range shortestPathAlgs { 285 g := simple.NewDirectedGraph() 286 gen.SmallWorldsBB(g, bench.n, bench.d, bench.p, rand.New(rand.NewSource(bench.seed))) 287 p := f.fn(g) 288 289 // Find the widest path set. 290 var ( 291 bestUid, bestVid int64 292 n int 293 ) 294 for uid := int64(0); uid < int64(bench.n); uid++ { 295 for vid := int64(0); vid < int64(bench.n); vid++ { 296 paths, _ := p.AllBetween(uid, vid) 297 if len(paths) > n { 298 n = len(paths) 299 bestUid = uid 300 bestVid = vid 301 } 302 } 303 } 304 305 b.Run(fmt.Sprintf("%s_Between_%d×%d|%v(%d)", f.name, bench.n, bench.d, bench.p, n), func(b *testing.B) { 306 for i := 0; i < b.N; i++ { 307 path, _, _ := p.Between(bestUid, bestVid) 308 if len(path) == 0 { 309 b.Errorf("unexpected empty path: got:%d want:%d", len(path), 0) 310 } 311 } 312 }) 313 b.Run(fmt.Sprintf("%s_AllBetween_%d×%d|%v(%d)", f.name, bench.n, bench.d, bench.p, n), func(b *testing.B) { 314 for i := 0; i < b.N; i++ { 315 paths, _ := p.AllBetween(bestUid, bestVid) 316 if len(paths) != n { 317 b.Errorf("unexpected number of paths: got:%d want:%d", len(paths), n) 318 } 319 } 320 }) 321 b.Run(fmt.Sprintf("%s_AllBetweenFunc_%d×%d|%v(%d)", f.name, bench.n, bench.d, bench.p, n), func(b *testing.B) { 322 for i := 0; i < b.N; i++ { 323 var paths int 324 p.AllBetweenFunc(bestUid, bestVid, func(_ []graph.Node) { paths++ }) 325 if paths != n { 326 b.Errorf("unexpected number of paths: got:%d want:%d", paths, n) 327 } 328 } 329 }) 330 } 331 } 332 }