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