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