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