gonum.org/v1/gonum@v0.15.1-0.20240517103525-f853624cb1bb/graph/community/louvain_directed_test.go (about)

     1  // Copyright ©2015 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 community
     6  
     7  import (
     8  	"math"
     9  	"reflect"
    10  	"slices"
    11  	"sort"
    12  	"testing"
    13  
    14  	"golang.org/x/exp/rand"
    15  
    16  	"gonum.org/v1/gonum/floats/scalar"
    17  	"gonum.org/v1/gonum/graph"
    18  	"gonum.org/v1/gonum/graph/simple"
    19  	"gonum.org/v1/gonum/internal/order"
    20  )
    21  
    22  type communityDirectedQTest struct {
    23  	name       string
    24  	g          []intset
    25  	structures []structure
    26  
    27  	wantLevels []level
    28  }
    29  
    30  var communityDirectedQTests = []communityDirectedQTest{
    31  	{
    32  		name: "simple_directed",
    33  		g:    simpleDirected,
    34  		// community structure and modularity calculated by C++ implementation: louvain igraph.
    35  		// Note that louvain igraph returns Q as an unscaled value.
    36  		structures: []structure{
    37  			{
    38  				resolution: 1,
    39  				memberships: []intset{
    40  					0: linksTo(0, 1),
    41  					1: linksTo(2, 3, 4),
    42  				},
    43  				want: 0.5714285714285716 / 7,
    44  				tol:  1e-10,
    45  			},
    46  		},
    47  		wantLevels: []level{
    48  			{
    49  				communities: [][]graph.Node{
    50  					{simple.Node(0), simple.Node(1)},
    51  					{simple.Node(2), simple.Node(3), simple.Node(4)},
    52  				},
    53  				q: 0.5714285714285716 / 7,
    54  			},
    55  			{
    56  				communities: [][]graph.Node{
    57  					{simple.Node(0)},
    58  					{simple.Node(1)},
    59  					{simple.Node(2)},
    60  					{simple.Node(3)},
    61  					{simple.Node(4)},
    62  				},
    63  				q: -1.2857142857142856 / 7,
    64  			},
    65  		},
    66  	},
    67  	{
    68  		name: "zachary",
    69  		g:    zachary,
    70  		// community structure and modularity calculated by C++ implementation: louvain igraph.
    71  		// Note that louvain igraph returns Q as an unscaled value.
    72  		structures: []structure{
    73  			{
    74  				resolution: 1,
    75  				memberships: []intset{
    76  					0: linksTo(0, 1, 2, 3, 7, 11, 12, 13, 17, 19, 21),
    77  					1: linksTo(4, 5, 6, 10, 16),
    78  					2: linksTo(8, 9, 14, 15, 18, 20, 22, 26, 29, 30, 32, 33),
    79  					3: linksTo(23, 24, 25, 27, 28, 31),
    80  				},
    81  				want: 34.3417721519 / 79 /* 5->6 and 6->5 because of co-equal rank */, tol: 1e-4,
    82  			},
    83  		},
    84  		wantLevels: []level{
    85  			{
    86  				q: 0.43470597660631316,
    87  				communities: [][]graph.Node{
    88  					{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(3), simple.Node(7), simple.Node(11), simple.Node(12), simple.Node(13), simple.Node(17), simple.Node(19), simple.Node(21)},
    89  					{simple.Node(4), simple.Node(5), simple.Node(6), simple.Node(10), simple.Node(16)},
    90  					{simple.Node(8), simple.Node(9), simple.Node(14), simple.Node(15), simple.Node(18), simple.Node(20), simple.Node(22), simple.Node(26), simple.Node(29), simple.Node(30), simple.Node(32), simple.Node(33)},
    91  					{simple.Node(23), simple.Node(24), simple.Node(25), simple.Node(27), simple.Node(28), simple.Node(31)},
    92  				},
    93  			},
    94  			{
    95  				q: 0.3911232174331037,
    96  				communities: [][]graph.Node{
    97  					{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(3), simple.Node(7), simple.Node(11), simple.Node(12), simple.Node(13), simple.Node(17), simple.Node(19), simple.Node(21)},
    98  					{simple.Node(4), simple.Node(10)},
    99  					{simple.Node(5), simple.Node(6), simple.Node(16)},
   100  					{simple.Node(8), simple.Node(30)},
   101  					{simple.Node(9), simple.Node(14), simple.Node(15), simple.Node(18), simple.Node(20), simple.Node(22), simple.Node(32), simple.Node(33)},
   102  					{simple.Node(23), simple.Node(24), simple.Node(25), simple.Node(27), simple.Node(28), simple.Node(31)},
   103  					{simple.Node(26), simple.Node(29)},
   104  				},
   105  			},
   106  			{
   107  				q: -0.014580996635154624,
   108  				communities: [][]graph.Node{
   109  					{simple.Node(0)},
   110  					{simple.Node(1)},
   111  					{simple.Node(2)},
   112  					{simple.Node(3)},
   113  					{simple.Node(4)},
   114  					{simple.Node(5)},
   115  					{simple.Node(6)},
   116  					{simple.Node(7)},
   117  					{simple.Node(8)},
   118  					{simple.Node(9)},
   119  					{simple.Node(10)},
   120  					{simple.Node(11)},
   121  					{simple.Node(12)},
   122  					{simple.Node(13)},
   123  					{simple.Node(14)},
   124  					{simple.Node(15)},
   125  					{simple.Node(16)},
   126  					{simple.Node(17)},
   127  					{simple.Node(18)},
   128  					{simple.Node(19)},
   129  					{simple.Node(20)},
   130  					{simple.Node(21)},
   131  					{simple.Node(22)},
   132  					{simple.Node(23)},
   133  					{simple.Node(24)},
   134  					{simple.Node(25)},
   135  					{simple.Node(26)},
   136  					{simple.Node(27)},
   137  					{simple.Node(28)},
   138  					{simple.Node(29)},
   139  					{simple.Node(30)},
   140  					{simple.Node(31)},
   141  					{simple.Node(32)},
   142  					{simple.Node(33)},
   143  				},
   144  			},
   145  		},
   146  	},
   147  	{
   148  		name: "blondel",
   149  		g:    blondel,
   150  		// community structure and modularity calculated by C++ implementation: louvain igraph.
   151  		// Note that louvain igraph returns Q as an unscaled value.
   152  		structures: []structure{
   153  			{
   154  				resolution: 1,
   155  				memberships: []intset{
   156  					0: linksTo(0, 1, 2, 3, 4, 5, 6, 7),
   157  					1: linksTo(8, 9, 10, 11, 12, 13, 14, 15),
   158  				},
   159  				want: 11.1428571429 / 28, tol: 1e-4,
   160  			},
   161  		},
   162  		wantLevels: []level{
   163  			{
   164  				q: 0.3979591836734694,
   165  				communities: [][]graph.Node{
   166  					{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(3), simple.Node(4), simple.Node(5), simple.Node(6), simple.Node(7)},
   167  					{simple.Node(8), simple.Node(9), simple.Node(10), simple.Node(11), simple.Node(12), simple.Node(13), simple.Node(14), simple.Node(15)},
   168  				},
   169  			},
   170  			{
   171  				q: 0.36862244897959184,
   172  				communities: [][]graph.Node{
   173  					{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(4), simple.Node(5)},
   174  					{simple.Node(3), simple.Node(6), simple.Node(7)},
   175  					{simple.Node(8), simple.Node(9), simple.Node(10), simple.Node(11), simple.Node(12), simple.Node(13), simple.Node(14), simple.Node(15)},
   176  				},
   177  			},
   178  			{
   179  				q: -0.022959183673469385,
   180  				communities: [][]graph.Node{
   181  					{simple.Node(0)},
   182  					{simple.Node(1)},
   183  					{simple.Node(2)},
   184  					{simple.Node(3)},
   185  					{simple.Node(4)},
   186  					{simple.Node(5)},
   187  					{simple.Node(6)},
   188  					{simple.Node(7)},
   189  					{simple.Node(8)},
   190  					{simple.Node(9)},
   191  					{simple.Node(10)},
   192  					{simple.Node(11)},
   193  					{simple.Node(12)},
   194  					{simple.Node(13)},
   195  					{simple.Node(14)},
   196  					{simple.Node(15)},
   197  				},
   198  			},
   199  		},
   200  	},
   201  }
   202  
   203  func TestCommunityQDirected(t *testing.T) {
   204  	for _, test := range communityDirectedQTests {
   205  		g := simple.NewDirectedGraph()
   206  		for u, e := range test.g {
   207  			// Add nodes that are not defined by an edge.
   208  			if g.Node(int64(u)) == nil {
   209  				g.AddNode(simple.Node(u))
   210  			}
   211  			for v := range e {
   212  				g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
   213  			}
   214  		}
   215  
   216  		testCommunityQDirected(t, test, g)
   217  	}
   218  }
   219  
   220  func TestCommunityQWeightedDirected(t *testing.T) {
   221  	for _, test := range communityDirectedQTests {
   222  		g := simple.NewWeightedDirectedGraph(0, 0)
   223  		for u, e := range test.g {
   224  			// Add nodes that are not defined by an edge.
   225  			if g.Node(int64(u)) == nil {
   226  				g.AddNode(simple.Node(u))
   227  			}
   228  			for v := range e {
   229  				g.SetWeightedEdge(simple.WeightedEdge{F: simple.Node(u), T: simple.Node(v), W: 1})
   230  			}
   231  		}
   232  
   233  		testCommunityQDirected(t, test, g)
   234  	}
   235  }
   236  
   237  func testCommunityQDirected(t *testing.T, test communityDirectedQTest, g graph.Directed) {
   238  	for _, structure := range test.structures {
   239  		communities := make([][]graph.Node, len(structure.memberships))
   240  		for i, c := range structure.memberships {
   241  			for n := range c {
   242  				communities[i] = append(communities[i], simple.Node(n))
   243  			}
   244  		}
   245  		got := Q(g, communities, structure.resolution)
   246  		if !scalar.EqualWithinAbsOrRel(got, structure.want, structure.tol, structure.tol) && !math.IsNaN(structure.want) {
   247  			for _, c := range communities {
   248  				order.ByID(c)
   249  			}
   250  			t.Errorf("unexpected Q value for %q %v: got: %v want: %v",
   251  				test.name, communities, got, structure.want)
   252  		}
   253  	}
   254  }
   255  
   256  func TestCommunityDeltaQDirected(t *testing.T) {
   257  	for _, test := range communityDirectedQTests {
   258  		g := simple.NewDirectedGraph()
   259  		for u, e := range test.g {
   260  			// Add nodes that are not defined by an edge.
   261  			if g.Node(int64(u)) == nil {
   262  				g.AddNode(simple.Node(u))
   263  			}
   264  			for v := range e {
   265  				g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
   266  			}
   267  		}
   268  
   269  		testCommunityDeltaQDirected(t, test, g)
   270  	}
   271  }
   272  
   273  func TestCommunityDeltaQWeightedDirected(t *testing.T) {
   274  	for _, test := range communityDirectedQTests {
   275  		g := simple.NewWeightedDirectedGraph(0, 0)
   276  		for u, e := range test.g {
   277  			// Add nodes that are not defined by an edge.
   278  			if g.Node(int64(u)) == nil {
   279  				g.AddNode(simple.Node(u))
   280  			}
   281  			for v := range e {
   282  				g.SetWeightedEdge(simple.WeightedEdge{F: simple.Node(u), T: simple.Node(v), W: 1})
   283  			}
   284  		}
   285  
   286  		testCommunityDeltaQDirected(t, test, g)
   287  	}
   288  }
   289  
   290  func testCommunityDeltaQDirected(t *testing.T, test communityDirectedQTest, g graph.Directed) {
   291  	rnd := rand.New(rand.NewSource(1)).Intn
   292  	for _, structure := range test.structures {
   293  		communityOf := make(map[int64]int)
   294  		communities := make([][]graph.Node, len(structure.memberships))
   295  		for i, c := range structure.memberships {
   296  			for n := range c {
   297  				n := int64(n)
   298  				communityOf[n] = i
   299  				communities[i] = append(communities[i], simple.Node(n))
   300  			}
   301  			order.ByID(communities[i])
   302  		}
   303  
   304  		before := Q(g, communities, structure.resolution)
   305  
   306  		l := newDirectedLocalMover(reduceDirected(g, nil), communities, structure.resolution)
   307  		if l == nil {
   308  			if !math.IsNaN(before) {
   309  				t.Errorf("unexpected nil localMover with non-NaN Q graph: Q=%.4v", before)
   310  			}
   311  			return
   312  		}
   313  
   314  		// This is done to avoid run-to-run
   315  		// variation due to map iteration order.
   316  		order.ByID(l.nodes)
   317  
   318  		l.shuffle(rnd)
   319  
   320  		for _, target := range l.nodes {
   321  			got, gotDst, gotSrc := l.deltaQ(target)
   322  
   323  			want, wantDst := math.Inf(-1), -1
   324  			migrated := make([][]graph.Node, len(structure.memberships))
   325  			for i, c := range structure.memberships {
   326  				for n := range c {
   327  					n := int64(n)
   328  					if n == target.ID() {
   329  						continue
   330  					}
   331  					migrated[i] = append(migrated[i], simple.Node(n))
   332  				}
   333  				order.ByID(migrated[i])
   334  			}
   335  
   336  			for i, c := range structure.memberships {
   337  				if i == communityOf[target.ID()] {
   338  					continue
   339  				}
   340  				connected := false
   341  				for n := range c {
   342  					if g.HasEdgeBetween(int64(n), target.ID()) {
   343  						connected = true
   344  						break
   345  					}
   346  				}
   347  				if !connected {
   348  					continue
   349  				}
   350  				migrated[i] = append(migrated[i], target)
   351  				after := Q(g, migrated, structure.resolution)
   352  				migrated[i] = migrated[i][:len(migrated[i])-1]
   353  				if after-before > want {
   354  					want = after - before
   355  					wantDst = i
   356  				}
   357  			}
   358  
   359  			if !scalar.EqualWithinAbsOrRel(got, want, structure.tol, structure.tol) || gotDst != wantDst {
   360  				t.Errorf("unexpected result moving n=%d in c=%d of %s/%.4v: got: %.4v,%d want: %.4v,%d"+
   361  					"\n\t%v\n\t%v",
   362  					target.ID(), communityOf[target.ID()], test.name, structure.resolution, got, gotDst, want, wantDst,
   363  					communities, migrated)
   364  			}
   365  			if gotSrc.community != communityOf[target.ID()] {
   366  				t.Errorf("unexpected source community index: got: %d want: %d", gotSrc, communityOf[target.ID()])
   367  			} else if communities[gotSrc.community][gotSrc.node].ID() != target.ID() {
   368  				wantNodeIdx := -1
   369  				for i, n := range communities[gotSrc.community] {
   370  					if n.ID() == target.ID() {
   371  						wantNodeIdx = i
   372  						break
   373  					}
   374  				}
   375  				t.Errorf("unexpected source node index: got: %d want: %d", gotSrc.node, wantNodeIdx)
   376  			}
   377  		}
   378  	}
   379  }
   380  
   381  func TestReduceQConsistencyDirected(t *testing.T) {
   382  	for _, test := range communityDirectedQTests {
   383  		g := simple.NewDirectedGraph()
   384  		for u, e := range test.g {
   385  			// Add nodes that are not defined by an edge.
   386  			if g.Node(int64(u)) == nil {
   387  				g.AddNode(simple.Node(u))
   388  			}
   389  			for v := range e {
   390  				g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
   391  			}
   392  		}
   393  
   394  		testReduceQConsistencyDirected(t, test, g)
   395  	}
   396  }
   397  
   398  func TestReduceQConsistencyWeightedDirected(t *testing.T) {
   399  	for _, test := range communityDirectedQTests {
   400  		g := simple.NewWeightedDirectedGraph(0, 0)
   401  		for u, e := range test.g {
   402  			// Add nodes that are not defined by an edge.
   403  			if g.Node(int64(u)) == nil {
   404  				g.AddNode(simple.Node(u))
   405  			}
   406  			for v := range e {
   407  				g.SetWeightedEdge(simple.WeightedEdge{F: simple.Node(u), T: simple.Node(v), W: 1})
   408  			}
   409  		}
   410  
   411  		testReduceQConsistencyDirected(t, test, g)
   412  	}
   413  }
   414  
   415  func testReduceQConsistencyDirected(t *testing.T, test communityDirectedQTest, g graph.Directed) {
   416  	for _, structure := range test.structures {
   417  		if math.IsNaN(structure.want) {
   418  			return
   419  		}
   420  
   421  		communities := make([][]graph.Node, len(structure.memberships))
   422  		for i, c := range structure.memberships {
   423  			for n := range c {
   424  				communities[i] = append(communities[i], simple.Node(n))
   425  			}
   426  			order.ByID(communities[i])
   427  		}
   428  
   429  		gQ := Q(g, communities, structure.resolution)
   430  		gQnull := Q(g, nil, 1)
   431  
   432  		cg0 := reduceDirected(g, nil)
   433  		cg0Qnull := Q(cg0, cg0.Structure(), 1)
   434  		if !scalar.EqualWithinAbsOrRel(gQnull, cg0Qnull, structure.tol, structure.tol) {
   435  			t.Errorf("disagreement between null Q from method: %v and function: %v", cg0Qnull, gQnull)
   436  		}
   437  		cg0Q := Q(cg0, communities, structure.resolution)
   438  		if !scalar.EqualWithinAbsOrRel(gQ, cg0Q, structure.tol, structure.tol) {
   439  			t.Errorf("unexpected Q result after initial reduction: got: %v want :%v", cg0Q, gQ)
   440  		}
   441  
   442  		cg1 := reduceDirected(cg0, communities)
   443  		cg1Q := Q(cg1, cg1.Structure(), structure.resolution)
   444  		if !scalar.EqualWithinAbsOrRel(gQ, cg1Q, structure.tol, structure.tol) {
   445  			t.Errorf("unexpected Q result after second reduction: got: %v want :%v", cg1Q, gQ)
   446  		}
   447  	}
   448  }
   449  
   450  type localDirectedMoveTest struct {
   451  	name       string
   452  	g          []intset
   453  	structures []moveStructures
   454  }
   455  
   456  var localDirectedMoveTests = []localDirectedMoveTest{
   457  	{
   458  		name: "blondel",
   459  		g:    blondel,
   460  		structures: []moveStructures{
   461  			{
   462  				memberships: []intset{
   463  					0: linksTo(0, 1, 2, 4, 5),
   464  					1: linksTo(3, 6, 7),
   465  					2: linksTo(8, 9, 10, 12, 14, 15),
   466  					3: linksTo(11, 13),
   467  				},
   468  				targetNodes: []graph.Node{simple.Node(0)},
   469  				resolution:  1,
   470  				tol:         1e-14,
   471  			},
   472  			{
   473  				memberships: []intset{
   474  					0: linksTo(0, 1, 2, 4, 5),
   475  					1: linksTo(3, 6, 7),
   476  					2: linksTo(8, 9, 10, 12, 14, 15),
   477  					3: linksTo(11, 13),
   478  				},
   479  				targetNodes: []graph.Node{simple.Node(3)},
   480  				resolution:  1,
   481  				tol:         1e-14,
   482  			},
   483  			{
   484  				memberships: []intset{
   485  					0: linksTo(0, 1, 2, 4, 5),
   486  					1: linksTo(3, 6, 7),
   487  					2: linksTo(8, 9, 10, 12, 14, 15),
   488  					3: linksTo(11, 13),
   489  				},
   490  				// Case to demonstrate when A_aa != k_a^𝛼.
   491  				targetNodes: []graph.Node{simple.Node(3), simple.Node(2)},
   492  				resolution:  1,
   493  				tol:         1e-14,
   494  			},
   495  		},
   496  	},
   497  }
   498  
   499  func TestMoveLocalDirected(t *testing.T) {
   500  	for _, test := range localDirectedMoveTests {
   501  		g := simple.NewDirectedGraph()
   502  		for u, e := range test.g {
   503  			// Add nodes that are not defined by an edge.
   504  			if g.Node(int64(u)) == nil {
   505  				g.AddNode(simple.Node(u))
   506  			}
   507  			for v := range e {
   508  				g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
   509  			}
   510  		}
   511  
   512  		testMoveLocalDirected(t, test, g)
   513  	}
   514  }
   515  
   516  func TestMoveLocalWeightedDirected(t *testing.T) {
   517  	for _, test := range localDirectedMoveTests {
   518  		g := simple.NewWeightedDirectedGraph(0, 0)
   519  		for u, e := range test.g {
   520  			// Add nodes that are not defined by an edge.
   521  			if g.Node(int64(u)) == nil {
   522  				g.AddNode(simple.Node(u))
   523  			}
   524  			for v := range e {
   525  				g.SetWeightedEdge(simple.WeightedEdge{F: simple.Node(u), T: simple.Node(v), W: 1})
   526  			}
   527  		}
   528  
   529  		testMoveLocalDirected(t, test, g)
   530  	}
   531  }
   532  
   533  func testMoveLocalDirected(t *testing.T, test localDirectedMoveTest, g graph.Directed) {
   534  	for _, structure := range test.structures {
   535  		communities := make([][]graph.Node, len(structure.memberships))
   536  		for i, c := range structure.memberships {
   537  			for n := range c {
   538  				communities[i] = append(communities[i], simple.Node(n))
   539  			}
   540  			order.ByID(communities[i])
   541  		}
   542  
   543  		r := reduceDirected(reduceDirected(g, nil), communities)
   544  
   545  		l := newDirectedLocalMover(r, r.communities, structure.resolution)
   546  		for _, n := range structure.targetNodes {
   547  			dQ, dst, src := l.deltaQ(n)
   548  			if dQ > 0 {
   549  				before := Q(r, l.communities, structure.resolution)
   550  				l.move(dst, src)
   551  				after := Q(r, l.communities, structure.resolution)
   552  				want := after - before
   553  				if !scalar.EqualWithinAbsOrRel(dQ, want, structure.tol, structure.tol) {
   554  					t.Errorf("unexpected deltaQ for %q: got: %v want: %v", test.name, dQ, want)
   555  				}
   556  			}
   557  		}
   558  	}
   559  }
   560  
   561  func TestModularizeDirected(t *testing.T) {
   562  	for _, test := range communityDirectedQTests {
   563  		g := simple.NewDirectedGraph()
   564  		for u, e := range test.g {
   565  			// Add nodes that are not defined by an edge.
   566  			if g.Node(int64(u)) == nil {
   567  				g.AddNode(simple.Node(u))
   568  			}
   569  			for v := range e {
   570  				g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
   571  			}
   572  		}
   573  
   574  		testModularizeDirected(t, test, g)
   575  	}
   576  }
   577  
   578  func TestModularizeWeightedDirected(t *testing.T) {
   579  	for _, test := range communityDirectedQTests {
   580  		g := simple.NewWeightedDirectedGraph(0, 0)
   581  		for u, e := range test.g {
   582  			// Add nodes that are not defined by an edge.
   583  			if g.Node(int64(u)) == nil {
   584  				g.AddNode(simple.Node(u))
   585  			}
   586  			for v := range e {
   587  				g.SetWeightedEdge(simple.WeightedEdge{F: simple.Node(u), T: simple.Node(v), W: 1})
   588  			}
   589  		}
   590  
   591  		testModularizeDirected(t, test, g)
   592  	}
   593  }
   594  
   595  func testModularizeDirected(t *testing.T, test communityDirectedQTest, g graph.Directed) {
   596  	const louvainIterations = 20
   597  
   598  	if test.structures[0].resolution != 1 {
   599  		panic("bad test: expect resolution=1")
   600  	}
   601  	want := make([][]graph.Node, len(test.structures[0].memberships))
   602  	for i, c := range test.structures[0].memberships {
   603  		for n := range c {
   604  			want[i] = append(want[i], simple.Node(n))
   605  		}
   606  		order.ByID(want[i])
   607  	}
   608  	order.BySliceIDs(want)
   609  
   610  	var (
   611  		got   *ReducedDirected
   612  		bestQ = math.Inf(-1)
   613  	)
   614  	// Modularize is randomised so we do this to
   615  	// ensure the level tests are consistent.
   616  	src := rand.New(rand.NewSource(1))
   617  	for i := 0; i < louvainIterations; i++ {
   618  		r := Modularize(g, 1, src).(*ReducedDirected)
   619  		if q := Q(r, nil, 1); q > bestQ || math.IsNaN(q) {
   620  			bestQ = q
   621  			got = r
   622  
   623  			if math.IsNaN(q) {
   624  				// Don't try again for non-connected case.
   625  				break
   626  			}
   627  		}
   628  
   629  		var qs []float64
   630  		for p := r; p != nil; p = p.Expanded().(*ReducedDirected) {
   631  			qs = append(qs, Q(p, nil, 1))
   632  		}
   633  
   634  		// Recovery of Q values is reversed.
   635  		if slices.Reverse(qs); !sort.Float64sAreSorted(qs) {
   636  			t.Errorf("Q values not monotonically increasing: %.5v", qs)
   637  		}
   638  	}
   639  
   640  	gotCommunities := got.Communities()
   641  	for _, c := range gotCommunities {
   642  		order.ByID(c)
   643  	}
   644  	order.BySliceIDs(gotCommunities)
   645  	if !reflect.DeepEqual(gotCommunities, want) {
   646  		t.Errorf("unexpected community membership for %s Q=%.4v:\n\tgot: %v\n\twant:%v",
   647  			test.name, bestQ, gotCommunities, want)
   648  		return
   649  	}
   650  
   651  	var levels []level
   652  	for p := got; p != nil; p = p.Expanded().(*ReducedDirected) {
   653  		var communities [][]graph.Node
   654  		if p.parent != nil {
   655  			communities = p.parent.Communities()
   656  			for _, c := range communities {
   657  				order.ByID(c)
   658  			}
   659  			order.BySliceIDs(communities)
   660  		} else {
   661  			communities = reduceDirected(g, nil).Communities()
   662  		}
   663  		q := Q(p, nil, 1)
   664  		if math.IsNaN(q) {
   665  			// Use an equalable flag value in place of NaN.
   666  			q = math.Inf(-1)
   667  		}
   668  		levels = append(levels, level{q: q, communities: communities})
   669  	}
   670  	if !reflect.DeepEqual(levels, test.wantLevels) {
   671  		t.Errorf("unexpected level structure:\n\tgot: %v\n\twant:%v", levels, test.wantLevels)
   672  	}
   673  }
   674  
   675  func TestNonContiguousDirected(t *testing.T) {
   676  	g := simple.NewDirectedGraph()
   677  	for _, e := range []simple.Edge{
   678  		{F: simple.Node(0), T: simple.Node(1)},
   679  		{F: simple.Node(4), T: simple.Node(5)},
   680  	} {
   681  		g.SetEdge(e)
   682  	}
   683  
   684  	func() {
   685  		defer func() {
   686  			r := recover()
   687  			if r != nil {
   688  				t.Error("unexpected panic with non-contiguous ID range")
   689  			}
   690  		}()
   691  		Modularize(g, 1, nil)
   692  	}()
   693  }
   694  
   695  func TestNonContiguousWeightedDirected(t *testing.T) {
   696  	g := simple.NewWeightedDirectedGraph(0, 0)
   697  	for _, e := range []simple.WeightedEdge{
   698  		{F: simple.Node(0), T: simple.Node(1), W: 1},
   699  		{F: simple.Node(4), T: simple.Node(5), W: 1},
   700  	} {
   701  		g.SetWeightedEdge(e)
   702  	}
   703  
   704  	func() {
   705  		defer func() {
   706  			r := recover()
   707  			if r != nil {
   708  				t.Error("unexpected panic with non-contiguous ID range")
   709  			}
   710  		}()
   711  		Modularize(g, 1, nil)
   712  	}()
   713  }
   714  
   715  func BenchmarkLouvainDirected(b *testing.B) {
   716  	src := rand.New(rand.NewSource(1))
   717  	for i := 0; i < b.N; i++ {
   718  		Modularize(dupGraphDirected, 1, src)
   719  	}
   720  }