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/scalar"
16	"gonum.org/v1/gonum/graph"
17	"gonum.org/v1/gonum/graph/internal/ordered"
18	"gonum.org/v1/gonum/graph/simple"
19)
20
21type communityUndirectedQTest struct {
22	name       string
23	g          []intset
24	structures []structure
25
26	wantLevels []level
27}
28
29var communityUndirectedQTests = []communityUndirectedQTest{
30	// The java reference implementation is available from http://www.ludowaltman.nl/slm/.
31	{
32		name: "unconnected",
33		g:    unconnected,
34		structures: []structure{
35			{
36				resolution: 1,
37				memberships: []intset{
38					0: linksTo(0),
39					1: linksTo(1),
40					2: linksTo(2),
41					3: linksTo(3),
42					4: linksTo(4),
43					5: linksTo(5),
44				},
45				want: math.NaN(),
46			},
47		},
48		wantLevels: []level{
49			{
50				q: math.Inf(-1), // Here math.Inf(-1) is used as a place holder for NaN to allow use of reflect.DeepEqual.
51				communities: [][]graph.Node{
52					{simple.Node(0)},
53					{simple.Node(1)},
54					{simple.Node(2)},
55					{simple.Node(3)},
56					{simple.Node(4)},
57					{simple.Node(5)},
58				},
59			},
60		},
61	},
62	{
63		name: "small_dumbell",
64		g:    smallDumbell,
65		structures: []structure{
66			{
67				resolution: 1,
68				// community structure and modularity calculated by java reference implementation.
69				memberships: []intset{
70					0: linksTo(0, 1, 2),
71					1: linksTo(3, 4, 5),
72				},
73				want: 0.357, tol: 1e-3,
74			},
75			{
76				resolution: 1,
77				memberships: []intset{
78					0: linksTo(0, 1, 2, 3, 4, 5),
79				},
80				// theoretical expectation.
81				want: 0, tol: 1e-14,
82			},
83		},
84		wantLevels: []level{
85			{
86				q: 0.35714285714285715,
87				communities: [][]graph.Node{
88					{simple.Node(0), simple.Node(1), simple.Node(2)},
89					{simple.Node(3), simple.Node(4), simple.Node(5)},
90				},
91			},
92			{
93				q: -0.17346938775510204,
94				communities: [][]graph.Node{
95					{simple.Node(0)},
96					{simple.Node(1)},
97					{simple.Node(2)},
98					{simple.Node(3)},
99					{simple.Node(4)},
100					{simple.Node(5)},
101				},
102			},
103		},
104	},
105	{
106		name: "zachary",
107		g:    zachary,
108		structures: []structure{
109			{
110				resolution: 1,
111				// community structure and modularity from doi: 10.1140/epjb/e2013-40829-0
112				memberships: []intset{
113					0: linksTo(0, 1, 2, 3, 7, 11, 12, 13, 17, 19, 21),
114					1: linksTo(4, 5, 6, 10, 16),
115					2: linksTo(8, 9, 14, 15, 18, 20, 22, 26, 29, 30, 32, 33),
116					3: linksTo(23, 24, 25, 27, 28, 31),
117				},
118				// Noted to be the optimal modularisation in the paper above.
119				want: 0.4198, tol: 1e-4,
120			},
121			{
122				resolution: 0.5,
123				// community structure and modularity calculated by java reference implementation.
124				memberships: []intset{
125					0: linksTo(0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 16, 17, 19, 21),
126					1: linksTo(8, 14, 15, 18, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33),
127				},
128				want: 0.6218, tol: 1e-3,
129			},
130			{
131				resolution: 2,
132				// community structure and modularity calculated by java reference implementation.
133				memberships: []intset{
134					0: linksTo(14, 18, 20, 22, 32, 33, 15),
135					1: linksTo(0, 1, 11, 17, 19, 21),
136					2: linksTo(2, 3, 7, 9, 12, 13),
137					3: linksTo(4, 5, 6, 10, 16),
138					4: linksTo(24, 25, 28, 31),
139					5: linksTo(23, 26, 27, 29),
140					6: linksTo(8, 30),
141				},
142				want: 0.1645, tol: 1e-3,
143			},
144		},
145		wantLevels: []level{
146			{
147				q: 0.4197896120973044,
148				communities: [][]graph.Node{
149					{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)},
150					{simple.Node(4), simple.Node(5), simple.Node(6), simple.Node(10), simple.Node(16)},
151					{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)},
152					{simple.Node(23), simple.Node(24), simple.Node(25), simple.Node(27), simple.Node(28), simple.Node(31)},
153				},
154			},
155			{
156				q: 0.3496877054569362,
157				communities: [][]graph.Node{
158					{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)},
159					{simple.Node(4), simple.Node(10)},
160					{simple.Node(5), simple.Node(6), simple.Node(16)},
161					{simple.Node(8), simple.Node(9), simple.Node(14), simple.Node(15), simple.Node(18), simple.Node(20), simple.Node(22), simple.Node(30), simple.Node(32), simple.Node(33)},
162					{simple.Node(23), simple.Node(25)},
163					{simple.Node(24), simple.Node(27)},
164					{simple.Node(26), simple.Node(29)},
165					{simple.Node(28), simple.Node(31)},
166				},
167			},
168			{
169				q: -0.04980276134122286,
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					{simple.Node(6)},
178					{simple.Node(7)},
179					{simple.Node(8)},
180					{simple.Node(9)},
181					{simple.Node(10)},
182					{simple.Node(11)},
183					{simple.Node(12)},
184					{simple.Node(13)},
185					{simple.Node(14)},
186					{simple.Node(15)},
187					{simple.Node(16)},
188					{simple.Node(17)},
189					{simple.Node(18)},
190					{simple.Node(19)},
191					{simple.Node(20)},
192					{simple.Node(21)},
193					{simple.Node(22)},
194					{simple.Node(23)},
195					{simple.Node(24)},
196					{simple.Node(25)},
197					{simple.Node(26)},
198					{simple.Node(27)},
199					{simple.Node(28)},
200					{simple.Node(29)},
201					{simple.Node(30)},
202					{simple.Node(31)},
203					{simple.Node(32)},
204					{simple.Node(33)},
205				},
206			},
207		},
208	},
209	{
210		name: "blondel",
211		g:    blondel,
212		structures: []structure{
213			{
214				resolution: 1,
215				// community structure and modularity calculated by java reference implementation.
216				memberships: []intset{
217					0: linksTo(0, 1, 2, 3, 4, 5, 6, 7),
218					1: linksTo(8, 9, 10, 11, 12, 13, 14, 15),
219				},
220				want: 0.3922, tol: 1e-4,
221			},
222		},
223		wantLevels: []level{
224			{
225				q: 0.39221938775510207,
226				communities: [][]graph.Node{
227					{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(3), simple.Node(4), simple.Node(5), simple.Node(6), simple.Node(7)},
228					{simple.Node(8), simple.Node(9), simple.Node(10), simple.Node(11), simple.Node(12), simple.Node(13), simple.Node(14), simple.Node(15)},
229				},
230			},
231			{
232				q: 0.3463010204081633,
233				communities: [][]graph.Node{
234					{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(4), simple.Node(5)},
235					{simple.Node(3), simple.Node(6), simple.Node(7)},
236					{simple.Node(8), simple.Node(9), simple.Node(10), simple.Node(12), simple.Node(14), simple.Node(15)},
237					{simple.Node(11), simple.Node(13)},
238				},
239			},
240			{
241				q: -0.07142857142857144,
242				communities: [][]graph.Node{
243					{simple.Node(0)},
244					{simple.Node(1)},
245					{simple.Node(2)},
246					{simple.Node(3)},
247					{simple.Node(4)},
248					{simple.Node(5)},
249					{simple.Node(6)},
250					{simple.Node(7)},
251					{simple.Node(8)},
252					{simple.Node(9)},
253					{simple.Node(10)},
254					{simple.Node(11)},
255					{simple.Node(12)},
256					{simple.Node(13)},
257					{simple.Node(14)},
258					{simple.Node(15)},
259				},
260			},
261		},
262	},
263}
264
265func TestCommunityQUndirected(t *testing.T) {
266	for _, test := range communityUndirectedQTests {
267		g := simple.NewUndirectedGraph()
268		for u, e := range test.g {
269			// Add nodes that are not defined by an edge.
270			if g.Node(int64(u)) == nil {
271				g.AddNode(simple.Node(u))
272			}
273			for v := range e {
274				g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
275			}
276		}
277
278		testCommunityQUndirected(t, test, g)
279	}
280}
281
282func TestCommunityQWeightedUndirected(t *testing.T) {
283	for _, test := range communityUndirectedQTests {
284		g := simple.NewWeightedUndirectedGraph(0, 0)
285		for u, e := range test.g {
286			// Add nodes that are not defined by an edge.
287			if g.Node(int64(u)) == nil {
288				g.AddNode(simple.Node(u))
289			}
290			for v := range e {
291				g.SetWeightedEdge(simple.WeightedEdge{F: simple.Node(u), T: simple.Node(v), W: 1})
292			}
293		}
294
295		testCommunityQUndirected(t, test, g)
296	}
297}
298
299func testCommunityQUndirected(t *testing.T, test communityUndirectedQTest, g graph.Undirected) {
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		got := Q(g, communities, structure.resolution)
308		if !scalar.EqualWithinAbsOrRel(got, structure.want, structure.tol, structure.tol) && !math.IsNaN(structure.want) {
309			for _, c := range communities {
310				sort.Sort(ordered.ByID(c))
311			}
312			t.Errorf("unexpected Q value for %q %v: got: %v want: %v",
313				test.name, communities, got, structure.want)
314		}
315	}
316}
317
318func TestCommunityDeltaQUndirected(t *testing.T) {
319	for _, test := range communityUndirectedQTests {
320		g := simple.NewUndirectedGraph()
321		for u, e := range test.g {
322			// Add nodes that are not defined by an edge.
323			if g.Node(int64(u)) == nil {
324				g.AddNode(simple.Node(u))
325			}
326			for v := range e {
327				g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
328			}
329		}
330
331		testCommunityDeltaQUndirected(t, test, g)
332	}
333}
334
335func TestCommunityDeltaQWeightedUndirected(t *testing.T) {
336	for _, test := range communityUndirectedQTests {
337		g := simple.NewWeightedUndirectedGraph(0, 0)
338		for u, e := range test.g {
339			// Add nodes that are not defined by an edge.
340			if g.Node(int64(u)) == nil {
341				g.AddNode(simple.Node(u))
342			}
343			for v := range e {
344				g.SetWeightedEdge(simple.WeightedEdge{F: simple.Node(u), T: simple.Node(v), W: 1})
345			}
346		}
347
348		testCommunityDeltaQUndirected(t, test, g)
349	}
350}
351
352func testCommunityDeltaQUndirected(t *testing.T, test communityUndirectedQTest, g graph.Undirected) {
353	rnd := rand.New(rand.NewSource(1)).Intn
354	for _, structure := range test.structures {
355		communityOf := make(map[int64]int)
356		communities := make([][]graph.Node, len(structure.memberships))
357		for i, c := range structure.memberships {
358			for n := range c {
359				n := int64(n)
360				communityOf[n] = i
361				communities[i] = append(communities[i], simple.Node(n))
362			}
363			sort.Sort(ordered.ByID(communities[i]))
364		}
365
366		before := Q(g, communities, structure.resolution)
367
368		l := newUndirectedLocalMover(reduceUndirected(g, nil), communities, structure.resolution)
369		if l == nil {
370			if !math.IsNaN(before) {
371				t.Errorf("unexpected nil localMover with non-NaN Q graph: Q=%.4v", before)
372			}
373			return
374		}
375
376		// This is done to avoid run-to-run
377		// variation due to map iteration order.
378		sort.Sort(ordered.ByID(l.nodes))
379
380		l.shuffle(rnd)
381
382		for _, target := range l.nodes {
383			got, gotDst, gotSrc := l.deltaQ(target)
384
385			want, wantDst := math.Inf(-1), -1
386			migrated := make([][]graph.Node, len(structure.memberships))
387			for i, c := range structure.memberships {
388				for n := range c {
389					n := int64(n)
390					if n == target.ID() {
391						continue
392					}
393					migrated[i] = append(migrated[i], simple.Node(n))
394				}
395				sort.Sort(ordered.ByID(migrated[i]))
396			}
397
398			for i, c := range structure.memberships {
399				if i == communityOf[target.ID()] {
400					continue
401				}
402				connected := false
403				for n := range c {
404					if g.HasEdgeBetween(int64(n), target.ID()) {
405						connected = true
406						break
407					}
408				}
409				if !connected {
410					continue
411				}
412				migrated[i] = append(migrated[i], target)
413				after := Q(g, migrated, structure.resolution)
414				migrated[i] = migrated[i][:len(migrated[i])-1]
415				if after-before > want {
416					want = after - before
417					wantDst = i
418				}
419			}
420
421			if !scalar.EqualWithinAbsOrRel(got, want, structure.tol, structure.tol) || gotDst != wantDst {
422				t.Errorf("unexpected result moving n=%d in c=%d of %s/%.4v: got: %.4v,%d want: %.4v,%d"+
423					"\n\t%v\n\t%v",
424					target.ID(), communityOf[target.ID()], test.name, structure.resolution, got, gotDst, want, wantDst,
425					communities, migrated)
426			}
427			if gotSrc.community != communityOf[target.ID()] {
428				t.Errorf("unexpected source community index: got: %d want: %d", gotSrc, communityOf[target.ID()])
429			} else if communities[gotSrc.community][gotSrc.node].ID() != target.ID() {
430				wantNodeIdx := -1
431				for i, n := range communities[gotSrc.community] {
432					if n.ID() == target.ID() {
433						wantNodeIdx = i
434						break
435					}
436				}
437				t.Errorf("unexpected source node index: got: %d want: %d", gotSrc.node, wantNodeIdx)
438			}
439		}
440	}
441}
442
443func TestReduceQConsistencyUndirected(t *testing.T) {
444	for _, test := range communityUndirectedQTests {
445		g := simple.NewUndirectedGraph()
446		for u, e := range test.g {
447			// Add nodes that are not defined by an edge.
448			if g.Node(int64(u)) == nil {
449				g.AddNode(simple.Node(u))
450			}
451			for v := range e {
452				g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
453			}
454		}
455
456		testReduceQConsistencyUndirected(t, test, g)
457	}
458}
459
460func TestReduceQConsistencyWeightedUndirected(t *testing.T) {
461	for _, test := range communityUndirectedQTests {
462		g := simple.NewWeightedUndirectedGraph(0, 0)
463		for u, e := range test.g {
464			// Add nodes that are not defined by an edge.
465			if g.Node(int64(u)) == nil {
466				g.AddNode(simple.Node(u))
467			}
468			for v := range e {
469				g.SetWeightedEdge(simple.WeightedEdge{F: simple.Node(u), T: simple.Node(v), W: 1})
470			}
471		}
472
473		testReduceQConsistencyUndirected(t, test, g)
474	}
475}
476
477func testReduceQConsistencyUndirected(t *testing.T, test communityUndirectedQTest, g graph.Undirected) {
478	for _, structure := range test.structures {
479		if math.IsNaN(structure.want) {
480			return
481		}
482
483		communities := make([][]graph.Node, len(structure.memberships))
484		for i, c := range structure.memberships {
485			for n := range c {
486				communities[i] = append(communities[i], simple.Node(n))
487			}
488			sort.Sort(ordered.ByID(communities[i]))
489		}
490
491		gQ := Q(g, communities, structure.resolution)
492		gQnull := Q(g, nil, 1)
493
494		cg0 := reduceUndirected(g, nil)
495		cg0Qnull := Q(cg0, cg0.Structure(), 1)
496		if !scalar.EqualWithinAbsOrRel(gQnull, cg0Qnull, structure.tol, structure.tol) {
497			t.Errorf("disagreement between null Q from method: %v and function: %v", cg0Qnull, gQnull)
498		}
499		cg0Q := Q(cg0, communities, structure.resolution)
500		if !scalar.EqualWithinAbsOrRel(gQ, cg0Q, structure.tol, structure.tol) {
501			t.Errorf("unexpected Q result after initial reduction: got: %v want :%v", cg0Q, gQ)
502		}
503
504		cg1 := reduceUndirected(cg0, communities)
505		cg1Q := Q(cg1, cg1.Structure(), structure.resolution)
506		if !scalar.EqualWithinAbsOrRel(gQ, cg1Q, structure.tol, structure.tol) {
507			t.Errorf("unexpected Q result after second reduction: got: %v want :%v", cg1Q, gQ)
508		}
509	}
510}
511
512type localUndirectedMoveTest struct {
513	name       string
514	g          []intset
515	structures []moveStructures
516}
517
518var localUndirectedMoveTests = []localUndirectedMoveTest{
519	{
520		name: "blondel",
521		g:    blondel,
522		structures: []moveStructures{
523			{
524				memberships: []intset{
525					0: linksTo(0, 1, 2, 4, 5),
526					1: linksTo(3, 6, 7),
527					2: linksTo(8, 9, 10, 12, 14, 15),
528					3: linksTo(11, 13),
529				},
530				targetNodes: []graph.Node{simple.Node(0)},
531				resolution:  1,
532				tol:         1e-14,
533			},
534			{
535				memberships: []intset{
536					0: linksTo(0, 1, 2, 4, 5),
537					1: linksTo(3, 6, 7),
538					2: linksTo(8, 9, 10, 12, 14, 15),
539					3: linksTo(11, 13),
540				},
541				targetNodes: []graph.Node{simple.Node(3)},
542				resolution:  1,
543				tol:         1e-14,
544			},
545			{
546				memberships: []intset{
547					0: linksTo(0, 1, 2, 4, 5),
548					1: linksTo(3, 6, 7),
549					2: linksTo(8, 9, 10, 12, 14, 15),
550					3: linksTo(11, 13),
551				},
552				// Case to demonstrate when A_aa != k_a^��.
553				targetNodes: []graph.Node{simple.Node(3), simple.Node(2)},
554				resolution:  1,
555				tol:         1e-14,
556			},
557		},
558	},
559}
560
561func TestMoveLocalUndirected(t *testing.T) {
562	for _, test := range localUndirectedMoveTests {
563		g := simple.NewUndirectedGraph()
564		for u, e := range test.g {
565			// Add nodes that are not defined by an edge.
566			if g.Node(int64(u)) == nil {
567				g.AddNode(simple.Node(u))
568			}
569			for v := range e {
570				g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
571			}
572		}
573
574		testMoveLocalUndirected(t, test, g)
575	}
576}
577
578func TestMoveLocalWeightedUndirected(t *testing.T) {
579	for _, test := range localUndirectedMoveTests {
580		g := simple.NewWeightedUndirectedGraph(0, 0)
581		for u, e := range test.g {
582			// Add nodes that are not defined by an edge.
583			if g.Node(int64(u)) == nil {
584				g.AddNode(simple.Node(u))
585			}
586			for v := range e {
587				g.SetWeightedEdge(simple.WeightedEdge{F: simple.Node(u), T: simple.Node(v), W: 1})
588			}
589		}
590
591		testMoveLocalUndirected(t, test, g)
592	}
593}
594
595func testMoveLocalUndirected(t *testing.T, test localUndirectedMoveTest, g graph.Undirected) {
596	for _, structure := range test.structures {
597		communities := make([][]graph.Node, len(structure.memberships))
598		for i, c := range structure.memberships {
599			for n := range c {
600				communities[i] = append(communities[i], simple.Node(n))
601			}
602			sort.Sort(ordered.ByID(communities[i]))
603		}
604
605		r := reduceUndirected(reduceUndirected(g, nil), communities)
606
607		l := newUndirectedLocalMover(r, r.communities, structure.resolution)
608		for _, n := range structure.targetNodes {
609			dQ, dst, src := l.deltaQ(n)
610			if dQ > 0 {
611				before := Q(r, l.communities, structure.resolution)
612				l.move(dst, src)
613				after := Q(r, l.communities, structure.resolution)
614				want := after - before
615				if !scalar.EqualWithinAbsOrRel(dQ, want, structure.tol, structure.tol) {
616					t.Errorf("unexpected deltaQ for %q: got: %v want: %v", test.name, dQ, want)
617				}
618			}
619		}
620	}
621}
622
623func TestModularizeUndirected(t *testing.T) {
624	for _, test := range communityUndirectedQTests {
625		g := simple.NewUndirectedGraph()
626		for u, e := range test.g {
627			// Add nodes that are not defined by an edge.
628			if g.Node(int64(u)) == nil {
629				g.AddNode(simple.Node(u))
630			}
631			for v := range e {
632				g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
633			}
634		}
635
636		testModularizeUndirected(t, test, g)
637	}
638}
639
640func TestModularizeWeightedUndirected(t *testing.T) {
641	for _, test := range communityUndirectedQTests {
642		g := simple.NewWeightedUndirectedGraph(0, 0)
643		for u, e := range test.g {
644			// Add nodes that are not defined by an edge.
645			if g.Node(int64(u)) == nil {
646				g.AddNode(simple.Node(u))
647			}
648			for v := range e {
649				g.SetWeightedEdge(simple.WeightedEdge{F: simple.Node(u), T: simple.Node(v), W: 1})
650			}
651		}
652
653		testModularizeUndirected(t, test, g)
654	}
655}
656
657func testModularizeUndirected(t *testing.T, test communityUndirectedQTest, g graph.Undirected) {
658	const louvainIterations = 20
659
660	if test.structures[0].resolution != 1 {
661		panic("bad test: expect resolution=1")
662	}
663	want := make([][]graph.Node, len(test.structures[0].memberships))
664	for i, c := range test.structures[0].memberships {
665		for n := range c {
666			want[i] = append(want[i], simple.Node(n))
667		}
668		sort.Sort(ordered.ByID(want[i]))
669	}
670	sort.Sort(ordered.BySliceIDs(want))
671
672	var (
673		got   *ReducedUndirected
674		bestQ = math.Inf(-1)
675	)
676	// Modularize is randomised so we do this to
677	// ensure the level tests are consistent.
678	src := rand.New(rand.NewSource(1))
679	for i := 0; i < louvainIterations; i++ {
680		r := Modularize(g, 1, src).(*ReducedUndirected)
681		if q := Q(r, nil, 1); q > bestQ || math.IsNaN(q) {
682			bestQ = q
683			got = r
684
685			if math.IsNaN(q) {
686				// Don't try again for non-connected case.
687				break
688			}
689		}
690
691		var qs []float64
692		for p := r; p != nil; p = p.Expanded().(*ReducedUndirected) {
693			qs = append(qs, Q(p, nil, 1))
694		}
695
696		// Recovery of Q values is reversed.
697		if reverse(qs); !sort.Float64sAreSorted(qs) {
698			t.Errorf("Q values not monotonically increasing: %.5v", qs)
699		}
700	}
701
702	gotCommunities := got.Communities()
703	for _, c := range gotCommunities {
704		sort.Sort(ordered.ByID(c))
705	}
706	sort.Sort(ordered.BySliceIDs(gotCommunities))
707	if !reflect.DeepEqual(gotCommunities, want) {
708		t.Errorf("unexpected community membership for %s Q=%.4v:\n\tgot: %v\n\twant:%v",
709			test.name, bestQ, gotCommunities, want)
710		return
711	}
712
713	var levels []level
714	for p := got; p != nil; p = p.Expanded().(*ReducedUndirected) {
715		var communities [][]graph.Node
716		if p.parent != nil {
717			communities = p.parent.Communities()
718			for _, c := range communities {
719				sort.Sort(ordered.ByID(c))
720			}
721			sort.Sort(ordered.BySliceIDs(communities))
722		} else {
723			communities = reduceUndirected(g, nil).Communities()
724		}
725		q := Q(p, nil, 1)
726		if math.IsNaN(q) {
727			// Use an equalable flag value in place of NaN.
728			q = math.Inf(-1)
729		}
730		levels = append(levels, level{q: q, communities: communities})
731	}
732	if !reflect.DeepEqual(levels, test.wantLevels) {
733		t.Errorf("unexpected level structure:\n\tgot: %v\n\twant:%v", levels, test.wantLevels)
734	}
735}
736
737func TestNonContiguousUndirected(t *testing.T) {
738	g := simple.NewUndirectedGraph()
739	for _, e := range []simple.Edge{
740		{F: simple.Node(0), T: simple.Node(1)},
741		{F: simple.Node(4), T: simple.Node(5)},
742	} {
743		g.SetEdge(e)
744	}
745
746	func() {
747		defer func() {
748			r := recover()
749			if r != nil {
750				t.Error("unexpected panic with non-contiguous ID range")
751			}
752		}()
753		Modularize(g, 1, nil)
754	}()
755}
756
757func TestNonContiguousWeightedUndirected(t *testing.T) {
758	g := simple.NewWeightedUndirectedGraph(0, 0)
759	for _, e := range []simple.WeightedEdge{
760		{F: simple.Node(0), T: simple.Node(1), W: 1},
761		{F: simple.Node(4), T: simple.Node(5), W: 1},
762	} {
763		g.SetWeightedEdge(e)
764	}
765
766	func() {
767		defer func() {
768			r := recover()
769			if r != nil {
770				t.Error("unexpected panic with non-contiguous ID range")
771			}
772		}()
773		Modularize(g, 1, nil)
774	}()
775}
776
777func BenchmarkLouvain(b *testing.B) {
778	src := rand.New(rand.NewSource(1))
779	for i := 0; i < b.N; i++ {
780		Modularize(dupGraph, 1, src)
781	}
782}
783