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