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