gonum.org/v1/gonum@v0.15.1-0.20240517103525-f853624cb1bb/graph/community/louvain_undirected_multiplex_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  	"fmt"
     9  	"math"
    10  	"reflect"
    11  	"slices"
    12  	"sort"
    13  	"testing"
    14  
    15  	"golang.org/x/exp/rand"
    16  
    17  	"gonum.org/v1/gonum/floats"
    18  	"gonum.org/v1/gonum/floats/scalar"
    19  	"gonum.org/v1/gonum/graph"
    20  	"gonum.org/v1/gonum/graph/simple"
    21  	"gonum.org/v1/gonum/internal/order"
    22  )
    23  
    24  var communityUndirectedMultiplexQTests = []struct {
    25  	name       string
    26  	layers     []layer
    27  	structures []structure
    28  
    29  	wantLevels []level
    30  }{
    31  	{
    32  		name:   "unconnected",
    33  		layers: []layer{{g: unconnected, weight: 1}},
    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  		layers: []layer{
    65  			{g: smallDumbell, edgeWeight: 1, weight: 1},
    66  			{g: dumbellRepulsion, edgeWeight: -1, weight: -1},
    67  		},
    68  		structures: []structure{
    69  			{
    70  				resolution: 1,
    71  				memberships: []intset{
    72  					0: linksTo(0, 1, 2),
    73  					1: linksTo(3, 4, 5),
    74  				},
    75  				want: 7.0, tol: 1e-10,
    76  			},
    77  			{
    78  				resolution: 1,
    79  				memberships: []intset{
    80  					0: linksTo(0, 1, 2, 3, 4, 5),
    81  				},
    82  				want: 0, tol: 1e-14,
    83  			},
    84  		},
    85  		wantLevels: []level{
    86  			{
    87  				q: 7.0,
    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: -1.4285714285714284,
    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: "small_dumbell_twice",
   108  		layers: []layer{
   109  			{g: smallDumbell, weight: 0.5},
   110  			{g: smallDumbell, weight: 0.5},
   111  		},
   112  		structures: []structure{
   113  			{
   114  				resolution: 1,
   115  				memberships: []intset{
   116  					0: linksTo(0, 1, 2),
   117  					1: linksTo(3, 4, 5),
   118  				},
   119  				want: 5, tol: 1e-10,
   120  			},
   121  			{
   122  				resolution: 1,
   123  				memberships: []intset{
   124  					0: linksTo(0, 1, 2, 3, 4, 5),
   125  				},
   126  				want: 0, tol: 1e-14,
   127  			},
   128  		},
   129  		wantLevels: []level{
   130  			{
   131  				q: 0.35714285714285715 * 14,
   132  				communities: [][]graph.Node{
   133  					{simple.Node(0), simple.Node(1), simple.Node(2)},
   134  					{simple.Node(3), simple.Node(4), simple.Node(5)},
   135  				},
   136  			},
   137  			{
   138  				q: -0.17346938775510204 * 14,
   139  				communities: [][]graph.Node{
   140  					{simple.Node(0)},
   141  					{simple.Node(1)},
   142  					{simple.Node(2)},
   143  					{simple.Node(3)},
   144  					{simple.Node(4)},
   145  					{simple.Node(5)},
   146  				},
   147  			},
   148  		},
   149  	},
   150  	{
   151  		name:   "repulsion",
   152  		layers: []layer{{g: repulsion, edgeWeight: -1, weight: -1}},
   153  		structures: []structure{
   154  			{
   155  				resolution: 1,
   156  				memberships: []intset{
   157  					0: linksTo(0, 1, 2),
   158  					1: linksTo(3, 4, 5),
   159  				},
   160  				want: 9.0, tol: 1e-10,
   161  			},
   162  			{
   163  				resolution: 1,
   164  				memberships: []intset{
   165  					0: linksTo(0),
   166  					1: linksTo(1),
   167  					2: linksTo(2),
   168  					3: linksTo(3),
   169  					4: linksTo(4),
   170  					5: linksTo(5),
   171  				},
   172  				want: 3, tol: 1e-14,
   173  			},
   174  		},
   175  		wantLevels: []level{
   176  			{
   177  				q: 9.0,
   178  				communities: [][]graph.Node{
   179  					{simple.Node(0), simple.Node(1), simple.Node(2)},
   180  					{simple.Node(3), simple.Node(4), simple.Node(5)},
   181  				},
   182  			},
   183  			{
   184  				q: 3.0,
   185  				communities: [][]graph.Node{
   186  					{simple.Node(0)},
   187  					{simple.Node(1)},
   188  					{simple.Node(2)},
   189  					{simple.Node(3)},
   190  					{simple.Node(4)},
   191  					{simple.Node(5)},
   192  				},
   193  			},
   194  		},
   195  	},
   196  	{
   197  		name: "middle_east",
   198  		layers: []layer{
   199  			{g: middleEast.friends, edgeWeight: 1, weight: 1},
   200  			{g: middleEast.enemies, edgeWeight: -1, weight: -1},
   201  		},
   202  		structures: []structure{
   203  			{
   204  				resolution: 1,
   205  				memberships: []intset{
   206  					0: linksTo(0, 6),
   207  					1: linksTo(1, 7, 9, 12),
   208  					2: linksTo(2, 8, 11),
   209  					3: linksTo(3, 4, 5, 10),
   210  				},
   211  				want: 33.8180574555, tol: 1e-9,
   212  			},
   213  			{
   214  				resolution: 1,
   215  				memberships: []intset{
   216  					0: linksTo(0, 2, 3, 4, 5, 10),
   217  					1: linksTo(1, 7, 9, 12),
   218  					2: linksTo(6),
   219  					3: linksTo(8, 11),
   220  				},
   221  				want: 30.92749658, tol: 1e-7,
   222  			},
   223  			{
   224  				resolution: 1,
   225  				memberships: []intset{
   226  					0: linksTo(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12),
   227  				},
   228  				want: 0, tol: 1e-14,
   229  			},
   230  		},
   231  		wantLevels: []level{
   232  			{
   233  				q: 33.818057455540355,
   234  				communities: [][]graph.Node{
   235  					{simple.Node(0), simple.Node(6)},
   236  					{simple.Node(1), simple.Node(7), simple.Node(9), simple.Node(12)},
   237  					{simple.Node(2), simple.Node(8), simple.Node(11)},
   238  					{simple.Node(3), simple.Node(4), simple.Node(5), simple.Node(10)},
   239  				},
   240  			},
   241  			{
   242  				q: 3.8071135430916545,
   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  				},
   258  			},
   259  		},
   260  	},
   261  }
   262  
   263  func TestCommunityQUndirectedMultiplex(t *testing.T) {
   264  	for _, test := range communityUndirectedMultiplexQTests {
   265  		g, weights, err := undirectedMultiplexFrom(test.layers)
   266  		if err != nil {
   267  			t.Errorf("unexpected error creating multiplex: %v", err)
   268  			continue
   269  		}
   270  
   271  		for _, structure := range test.structures {
   272  			communities := make([][]graph.Node, len(structure.memberships))
   273  			for i, c := range structure.memberships {
   274  				for n := range c {
   275  					communities[i] = append(communities[i], simple.Node(n))
   276  				}
   277  			}
   278  			q := QMultiplex(g, communities, weights, []float64{structure.resolution})
   279  			got := floats.Sum(q)
   280  			if !scalar.EqualWithinAbsOrRel(got, structure.want, structure.tol, structure.tol) && !math.IsNaN(structure.want) {
   281  				for _, c := range communities {
   282  					order.ByID(c)
   283  				}
   284  				t.Errorf("unexpected Q value for %q %v: got: %v %.3v want: %v",
   285  					test.name, communities, got, q, structure.want)
   286  			}
   287  		}
   288  	}
   289  }
   290  
   291  func TestCommunityDeltaQUndirectedMultiplex(t *testing.T) {
   292  tests:
   293  	for _, test := range communityUndirectedMultiplexQTests {
   294  		g, weights, err := undirectedMultiplexFrom(test.layers)
   295  		if err != nil {
   296  			t.Errorf("unexpected error creating multiplex: %v", err)
   297  			continue
   298  		}
   299  
   300  		rnd := rand.New(rand.NewSource(1)).Intn
   301  		for _, structure := range test.structures {
   302  			communityOf := make(map[int64]int)
   303  			communities := make([][]graph.Node, len(structure.memberships))
   304  			for i, c := range structure.memberships {
   305  				for n := range c {
   306  					n := int64(n)
   307  					communityOf[n] = i
   308  					communities[i] = append(communities[i], simple.Node(n))
   309  				}
   310  				order.ByID(communities[i])
   311  			}
   312  			resolution := []float64{structure.resolution}
   313  
   314  			before := QMultiplex(g, communities, weights, resolution)
   315  
   316  			// We test exhaustively.
   317  			const all = true
   318  
   319  			l := newUndirectedMultiplexLocalMover(
   320  				reduceUndirectedMultiplex(g, nil, weights),
   321  				communities, weights, resolution, all)
   322  			if l == nil {
   323  				if !math.IsNaN(floats.Sum(before)) {
   324  					t.Errorf("unexpected nil localMover with non-NaN Q graph: Q=%.4v", before)
   325  				}
   326  				continue tests
   327  			}
   328  
   329  			// This is done to avoid run-to-run
   330  			// variation due to map iteration order.
   331  			order.ByID(l.nodes)
   332  
   333  			l.shuffle(rnd)
   334  
   335  			for _, target := range l.nodes {
   336  				got, gotDst, gotSrc := l.deltaQ(target)
   337  
   338  				want, wantDst := math.Inf(-1), -1
   339  				migrated := make([][]graph.Node, len(structure.memberships))
   340  				for i, c := range structure.memberships {
   341  					for n := range c {
   342  						n := int64(n)
   343  						if n == target.ID() {
   344  							continue
   345  						}
   346  						migrated[i] = append(migrated[i], simple.Node(n))
   347  					}
   348  					order.ByID(migrated[i])
   349  				}
   350  
   351  				for i, c := range structure.memberships {
   352  					if i == communityOf[target.ID()] {
   353  						continue
   354  					}
   355  					if !(all && hasNegative(weights)) {
   356  						connected := false
   357  					search:
   358  						for l := 0; l < g.Depth(); l++ {
   359  							if weights[l] < 0 {
   360  								connected = true
   361  								break search
   362  							}
   363  							layer := g.Layer(l)
   364  							for n := range c {
   365  								if layer.HasEdgeBetween(int64(n), target.ID()) {
   366  									connected = true
   367  									break search
   368  								}
   369  							}
   370  						}
   371  						if !connected {
   372  							continue
   373  						}
   374  					}
   375  					migrated[i] = append(migrated[i], target)
   376  					after := QMultiplex(g, migrated, weights, resolution)
   377  					migrated[i] = migrated[i][:len(migrated[i])-1]
   378  					if delta := floats.Sum(after) - floats.Sum(before); delta > want {
   379  						want = delta
   380  						wantDst = i
   381  					}
   382  				}
   383  
   384  				if !scalar.EqualWithinAbsOrRel(got, want, structure.tol, structure.tol) || gotDst != wantDst {
   385  					t.Errorf("unexpected result moving n=%d in c=%d of %s/%.4v: got: %.4v,%d want: %.4v,%d"+
   386  						"\n\t%v\n\t%v",
   387  						target.ID(), communityOf[target.ID()], test.name, structure.resolution, got, gotDst, want, wantDst,
   388  						communities, migrated)
   389  				}
   390  				if gotSrc.community != communityOf[target.ID()] {
   391  					t.Errorf("unexpected source community index: got: %d want: %d", gotSrc, communityOf[target.ID()])
   392  				} else if communities[gotSrc.community][gotSrc.node].ID() != target.ID() {
   393  					wantNodeIdx := -1
   394  					for i, n := range communities[gotSrc.community] {
   395  						if n.ID() == target.ID() {
   396  							wantNodeIdx = i
   397  							break
   398  						}
   399  					}
   400  					t.Errorf("unexpected source node index: got: %d want: %d", gotSrc.node, wantNodeIdx)
   401  				}
   402  			}
   403  		}
   404  	}
   405  }
   406  
   407  func TestReduceQConsistencyUndirectedMultiplex(t *testing.T) {
   408  tests:
   409  	for _, test := range communityUndirectedMultiplexQTests {
   410  		g, weights, err := undirectedMultiplexFrom(test.layers)
   411  		if err != nil {
   412  			t.Errorf("unexpected error creating multiplex: %v", err)
   413  			continue
   414  		}
   415  
   416  		for _, structure := range test.structures {
   417  			if math.IsNaN(structure.want) {
   418  				continue tests
   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 := QMultiplex(g, communities, weights, []float64{structure.resolution})
   430  			gQnull := QMultiplex(g, nil, weights, nil)
   431  
   432  			cg0 := reduceUndirectedMultiplex(g, nil, weights)
   433  			cg0Qnull := QMultiplex(cg0, cg0.Structure(), weights, nil)
   434  			if !scalar.EqualWithinAbsOrRel(floats.Sum(gQnull), floats.Sum(cg0Qnull), structure.tol, structure.tol) {
   435  				t.Errorf("disagreement between null Q from method: %v and function: %v", cg0Qnull, gQnull)
   436  			}
   437  			cg0Q := QMultiplex(cg0, communities, weights, []float64{structure.resolution})
   438  			if !scalar.EqualWithinAbsOrRel(floats.Sum(gQ), floats.Sum(cg0Q), structure.tol, structure.tol) {
   439  				t.Errorf("unexpected Q result after initial reduction: got: %v want :%v", cg0Q, gQ)
   440  			}
   441  
   442  			cg1 := reduceUndirectedMultiplex(cg0, communities, weights)
   443  			cg1Q := QMultiplex(cg1, cg1.Structure(), weights, []float64{structure.resolution})
   444  			if !scalar.EqualWithinAbsOrRel(floats.Sum(gQ), floats.Sum(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  
   451  var localUndirectedMultiplexMoveTests = []struct {
   452  	name       string
   453  	layers     []layer
   454  	structures []moveStructures
   455  }{
   456  	{
   457  		name:   "blondel",
   458  		layers: []layer{{g: blondel, weight: 1}, {g: blondel, weight: 0.5}},
   459  		structures: []moveStructures{
   460  			{
   461  				memberships: []intset{
   462  					0: linksTo(0, 1, 2, 4, 5),
   463  					1: linksTo(3, 6, 7),
   464  					2: linksTo(8, 9, 10, 12, 14, 15),
   465  					3: linksTo(11, 13),
   466  				},
   467  				targetNodes: []graph.Node{simple.Node(0)},
   468  				resolution:  1,
   469  				tol:         1e-14,
   470  			},
   471  			{
   472  				memberships: []intset{
   473  					0: linksTo(0, 1, 2, 4, 5),
   474  					1: linksTo(3, 6, 7),
   475  					2: linksTo(8, 9, 10, 12, 14, 15),
   476  					3: linksTo(11, 13),
   477  				},
   478  				targetNodes: []graph.Node{simple.Node(3)},
   479  				resolution:  1,
   480  				tol:         1e-14,
   481  			},
   482  			{
   483  				memberships: []intset{
   484  					0: linksTo(0, 1, 2, 4, 5),
   485  					1: linksTo(3, 6, 7),
   486  					2: linksTo(8, 9, 10, 12, 14, 15),
   487  					3: linksTo(11, 13),
   488  				},
   489  				// Case to demonstrate when A_aa != k_a^𝛼.
   490  				targetNodes: []graph.Node{simple.Node(3), simple.Node(2)},
   491  				resolution:  1,
   492  				tol:         1e-14,
   493  			},
   494  		},
   495  	},
   496  }
   497  
   498  func TestMoveLocalUndirectedMultiplex(t *testing.T) {
   499  	for _, test := range localUndirectedMultiplexMoveTests {
   500  		g, weights, err := undirectedMultiplexFrom(test.layers)
   501  		if err != nil {
   502  			t.Errorf("unexpected error creating multiplex: %v", err)
   503  			continue
   504  		}
   505  
   506  		for _, structure := range test.structures {
   507  			communities := make([][]graph.Node, len(structure.memberships))
   508  			for i, c := range structure.memberships {
   509  				for n := range c {
   510  					communities[i] = append(communities[i], simple.Node(n))
   511  				}
   512  				order.ByID(communities[i])
   513  			}
   514  
   515  			r := reduceUndirectedMultiplex(reduceUndirectedMultiplex(g, nil, weights), communities, weights)
   516  
   517  			l := newUndirectedMultiplexLocalMover(r, r.communities, weights, []float64{structure.resolution}, true)
   518  			for _, n := range structure.targetNodes {
   519  				dQ, dst, src := l.deltaQ(n)
   520  				if dQ > 0 {
   521  					before := floats.Sum(QMultiplex(r, l.communities, weights, []float64{structure.resolution}))
   522  					l.move(dst, src)
   523  					after := floats.Sum(QMultiplex(r, l.communities, weights, []float64{structure.resolution}))
   524  					want := after - before
   525  					if !scalar.EqualWithinAbsOrRel(dQ, want, structure.tol, structure.tol) {
   526  						t.Errorf("unexpected deltaQ: got: %v want: %v", dQ, want)
   527  					}
   528  				}
   529  			}
   530  		}
   531  	}
   532  }
   533  
   534  func TestLouvainMultiplex(t *testing.T) {
   535  	const louvainIterations = 20
   536  
   537  	for _, test := range communityUndirectedMultiplexQTests {
   538  		g, weights, err := undirectedMultiplexFrom(test.layers)
   539  		if err != nil {
   540  			t.Errorf("unexpected error creating multiplex: %v", err)
   541  			continue
   542  		}
   543  
   544  		if test.structures[0].resolution != 1 {
   545  			panic("bad test: expect resolution=1")
   546  		}
   547  		want := make([][]graph.Node, len(test.structures[0].memberships))
   548  		for i, c := range test.structures[0].memberships {
   549  			for n := range c {
   550  				want[i] = append(want[i], simple.Node(n))
   551  			}
   552  			order.ByID(want[i])
   553  		}
   554  		order.BySliceIDs(want)
   555  
   556  		var (
   557  			got   *ReducedUndirectedMultiplex
   558  			bestQ = math.Inf(-1)
   559  		)
   560  		// Modularize is randomised so we do this to
   561  		// ensure the level tests are consistent.
   562  		src := rand.New(rand.NewSource(1))
   563  		for i := 0; i < louvainIterations; i++ {
   564  			r := ModularizeMultiplex(g, weights, nil, true, src).(*ReducedUndirectedMultiplex)
   565  			if q := floats.Sum(QMultiplex(r, nil, weights, nil)); q > bestQ || math.IsNaN(q) {
   566  				bestQ = q
   567  				got = r
   568  
   569  				if math.IsNaN(q) {
   570  					// Don't try again for non-connected case.
   571  					break
   572  				}
   573  			}
   574  
   575  			var qs []float64
   576  			for p := r; p != nil; p = p.Expanded().(*ReducedUndirectedMultiplex) {
   577  				qs = append(qs, floats.Sum(QMultiplex(p, nil, weights, nil)))
   578  			}
   579  
   580  			// Recovery of Q values is reversed.
   581  			if slices.Reverse(qs); !sort.Float64sAreSorted(qs) {
   582  				t.Errorf("Q values not monotonically increasing: %.5v", qs)
   583  			}
   584  		}
   585  
   586  		gotCommunities := got.Communities()
   587  		for _, c := range gotCommunities {
   588  			order.ByID(c)
   589  		}
   590  		order.BySliceIDs(gotCommunities)
   591  		if !reflect.DeepEqual(gotCommunities, want) {
   592  			t.Errorf("unexpected community membership for %s Q=%.4v:\n\tgot: %v\n\twant:%v",
   593  				test.name, bestQ, gotCommunities, want)
   594  			continue
   595  		}
   596  
   597  		var levels []level
   598  		for p := got; p != nil; p = p.Expanded().(*ReducedUndirectedMultiplex) {
   599  			var communities [][]graph.Node
   600  			if p.parent != nil {
   601  				communities = p.parent.Communities()
   602  				for _, c := range communities {
   603  					order.ByID(c)
   604  				}
   605  				order.BySliceIDs(communities)
   606  			} else {
   607  				communities = reduceUndirectedMultiplex(g, nil, weights).Communities()
   608  			}
   609  			q := floats.Sum(QMultiplex(p, nil, weights, nil))
   610  			if math.IsNaN(q) {
   611  				// Use an equalable flag value in place of NaN.
   612  				q = math.Inf(-1)
   613  			}
   614  			levels = append(levels, level{q: q, communities: communities})
   615  		}
   616  		if !reflect.DeepEqual(levels, test.wantLevels) {
   617  			t.Errorf("unexpected level structure:\n\tgot: %v\n\twant:%v", levels, test.wantLevels)
   618  		}
   619  	}
   620  }
   621  
   622  func TestNonContiguousUndirectedMultiplex(t *testing.T) {
   623  	g := simple.NewUndirectedGraph()
   624  	for _, e := range []simple.Edge{
   625  		{F: simple.Node(0), T: simple.Node(1)},
   626  		{F: simple.Node(4), T: simple.Node(5)},
   627  	} {
   628  		g.SetEdge(e)
   629  	}
   630  
   631  	func() {
   632  		defer func() {
   633  			r := recover()
   634  			if r != nil {
   635  				t.Error("unexpected panic with non-contiguous ID range")
   636  			}
   637  		}()
   638  		ModularizeMultiplex(UndirectedLayers{g}, nil, nil, true, nil)
   639  	}()
   640  }
   641  
   642  func TestNonContiguousWeightedUndirectedMultiplex(t *testing.T) {
   643  	g := simple.NewWeightedUndirectedGraph(0, 0)
   644  	for _, e := range []simple.WeightedEdge{
   645  		{F: simple.Node(0), T: simple.Node(1), W: 1},
   646  		{F: simple.Node(4), T: simple.Node(5), W: 1},
   647  	} {
   648  		g.SetWeightedEdge(e)
   649  	}
   650  
   651  	func() {
   652  		defer func() {
   653  			r := recover()
   654  			if r != nil {
   655  				t.Error("unexpected panic with non-contiguous ID range")
   656  			}
   657  		}()
   658  		ModularizeMultiplex(UndirectedLayers{g}, nil, nil, true, nil)
   659  	}()
   660  }
   661  
   662  func BenchmarkLouvainMultiplex(b *testing.B) {
   663  	src := rand.New(rand.NewSource(1))
   664  	for i := 0; i < b.N; i++ {
   665  		ModularizeMultiplex(UndirectedLayers{dupGraph}, nil, nil, true, src)
   666  	}
   667  }
   668  
   669  func undirectedMultiplexFrom(raw []layer) (UndirectedLayers, []float64, error) {
   670  	var layers []graph.Undirected
   671  	var weights []float64
   672  	for _, l := range raw {
   673  		g := simple.NewWeightedUndirectedGraph(0, 0)
   674  		for u, e := range l.g {
   675  			// Add nodes that are not defined by an edge.
   676  			if g.Node(int64(u)) == nil {
   677  				g.AddNode(simple.Node(u))
   678  			}
   679  			for v := range e {
   680  				w := 1.0
   681  				if l.edgeWeight != 0 {
   682  					w = l.edgeWeight
   683  				}
   684  				g.SetWeightedEdge(simple.WeightedEdge{F: simple.Node(u), T: simple.Node(v), W: w})
   685  			}
   686  		}
   687  		layers = append(layers, g)
   688  		weights = append(weights, l.weight)
   689  	}
   690  	g, err := NewUndirectedLayers(layers...)
   691  	if err != nil {
   692  		return nil, nil, err
   693  	}
   694  	return g, weights, nil
   695  }
   696  
   697  func BenchmarkNewUndirectedLayers(b *testing.B) {
   698  	for _, graphSize := range []int{1e0, 1e1, 1e3, 1e5} {
   699  		for _, numGraphs := range []int{1e0, 1e1} {
   700  			b.Run(
   701  				fmt.Sprintf("graphSize=%d,numGraphs=%d", graphSize, numGraphs),
   702  				func(b *testing.B) {
   703  					g := simple.NewUndirectedGraph()
   704  					for i := 0; i < graphSize; i++ {
   705  						g.AddNode(g.NewNode())
   706  					}
   707  					gs := make([]graph.Undirected, numGraphs)
   708  					for i := 0; i < numGraphs; i++ {
   709  						gs[i] = g
   710  					}
   711  
   712  					b.ResetTimer()
   713  					for i := 0; i < b.N; i++ {
   714  						_, err := NewUndirectedLayers(gs...)
   715  						if err != nil {
   716  							b.Fatalf("NewUndirectedLayers failed: %v", err)
   717  						}
   718  					}
   719  				})
   720  		}
   721  	}
   722  }