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