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