gonum.org/v1/gonum@v0.14.0/graph/community/louvain_undirected_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/scalar" 16 "gonum.org/v1/gonum/graph" 17 "gonum.org/v1/gonum/graph/internal/ordered" 18 "gonum.org/v1/gonum/graph/simple" 19 ) 20 21 type communityUndirectedQTest struct { 22 name string 23 g []intset 24 structures []structure 25 26 wantLevels []level 27 } 28 29 var 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 265 func 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 282 func 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 299 func 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 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 318 func 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 335 func 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 352 func 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 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 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 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 443 func 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 460 func 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 477 func 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 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 512 type localUndirectedMoveTest struct { 513 name string 514 g []intset 515 structures []moveStructures 516 } 517 518 var 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 561 func 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 578 func 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 595 func 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 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 623 func 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 640 func 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 657 func 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 ordered.ByID(want[i]) 669 } 670 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 ordered.ByID(c) 705 } 706 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 ordered.ByID(c) 720 } 721 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 737 func 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 757 func 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 777 func 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 }