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