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  }