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