diff --git a/graph/community/bisect_test.go b/graph/community/bisect_test.go index 4c2498ee..ab03814a 100644 --- a/graph/community/bisect_test.go +++ b/graph/community/bisect_test.go @@ -10,8 +10,8 @@ import ( "sort" "testing" + "gonum.org/v1/gonum/graph" "gonum.org/v1/gonum/graph/internal/ordered" - "gonum.org/v1/gonum/graph/simple" ) @@ -24,10 +24,10 @@ func ExampleProfile_simple() { // |/ \| // 1 5 // - g := simple.NewUndirectedGraph(0, 0) + g := simple.NewUndirectedGraph() for u, e := range smallDumbell { for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } @@ -56,27 +56,27 @@ func ExampleProfile_simple() { // Low:3.5 High:10 Score:0 Communities:[[0] [1] [2] [3] [4] [5]] Q=-0.607 } -var friends, enemies *simple.UndirectedGraph +var friends, enemies *simple.WeightedUndirectedGraph func init() { - friends = simple.NewUndirectedGraph(0, 0) + friends = simple.NewWeightedUndirectedGraph(0, 0) for u, e := range middleEast.friends { // Ensure unconnected nodes are included. if !friends.Has(simple.Node(u)) { friends.AddNode(simple.Node(u)) } for v := range e { - friends.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + friends.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) } } - enemies = simple.NewUndirectedGraph(0, 0) + enemies = simple.NewWeightedUndirectedGraph(0, 0) for u, e := range middleEast.enemies { // Ensure unconnected nodes are included. if !enemies.Has(simple.Node(u)) { enemies.AddNode(simple.Node(u)) } for v := range e { - enemies.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: -1}) + enemies.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: -1}) } } } @@ -112,8 +112,8 @@ func ExampleProfile_multiplex() { // Output: // Low:0.1 High:0.72 Score:26 Communities:[[0] [1 7 9 12] [2 8 11] [3 4 5 10] [6]] Q=[24.7 1.97] // Low:0.72 High:1.1 Score:24 Communities:[[0 6] [1 7 9 12] [2 8 11] [3 4 5 10]] Q=[16.9 14.1] - // Low:1.1 High:1.2 Score:18 Communities:[[0 2 6 11] [1 7 9 12] [3 4 5 8 10]] Q=[9.16 25.1] - // Low:1.2 High:1.6 Score:10 Communities:[[0 3 4 5 6 10] [1 7 9 12] [2 8 11]] Q=[11.4 24.1] + // Low:1.1 High:1.1 Score:18 Communities:[[0 2 6 11] [1 7 9 12] [3 4 5 8 10]] Q=[9.16 25.1] + // Low:1.1 High:1.6 Score:10 Communities:[[0 3 4 5 6 10] [1 7 9 12] [2 8 11]] Q=[11.5 23.9] // Low:1.6 High:1.6 Score:8 Communities:[[0 1 6 7 9 12] [2 8 11] [3 4 5 10]] Q=[5.56 39.8] // Low:1.6 High:1.8 Score:2 Communities:[[0 2 3 4 5 6 10] [1 7 8 9 11 12]] Q=[-1.82 48.6] // Low:1.8 High:2.3 Score:-6 Communities:[[0 2 3 4 5 6 8 10 11] [1 7 9 12]] Q=[-5 57.5] @@ -124,77 +124,119 @@ func ExampleProfile_multiplex() { func TestProfileUndirected(t *testing.T) { for _, test := range communityUndirectedQTests { - g := simple.NewUndirectedGraph(0, 0) + g := simple.NewUndirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } - fn := ModularScore(g, Weight, 10, nil) - p, err := Profile(fn, true, 1e-3, 0.1, 10) - if err != nil { - t.Errorf("%s: unexpected error: %v", test.name, err) + testProfileUndirected(t, test, g) + } +} + +func TestProfileWeightedUndirected(t *testing.T) { + for _, test := range communityUndirectedQTests { + g := simple.NewWeightedUndirectedGraph(0, 0) + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) + } + for v := range e { + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + } } - const tries = 1000 - for i, d := range p { - var score float64 - for i := 0; i < tries; i++ { - score, _ = fn(d.Low) - if score >= d.Score { - break - } - } - if score < d.Score { - t.Errorf("%s: failed to recover low end score: got: %v want: %v", test.name, score, d.Score) - } - if i != 0 && d.Score >= p[i-1].Score { - t.Errorf("%s: not monotonically decreasing: %v -> %v", test.name, p[i-1], d) + testProfileUndirected(t, test, g) + } +} + +func testProfileUndirected(t *testing.T, test communityUndirectedQTest, g graph.Undirected) { + fn := ModularScore(g, Weight, 10, nil) + p, err := Profile(fn, true, 1e-3, 0.1, 10) + if err != nil { + t.Errorf("%s: unexpected error: %v", test.name, err) + } + + const tries = 1000 + for i, d := range p { + var score float64 + for i := 0; i < tries; i++ { + score, _ = fn(d.Low) + if score >= d.Score { + break } } + if score < d.Score { + t.Errorf("%s: failed to recover low end score: got: %v want: %v", test.name, score, d.Score) + } + if i != 0 && d.Score >= p[i-1].Score { + t.Errorf("%s: not monotonically decreasing: %v -> %v", test.name, p[i-1], d) + } } } func TestProfileDirected(t *testing.T) { for _, test := range communityDirectedQTests { - g := simple.NewDirectedGraph(0, 0) + g := simple.NewDirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } - fn := ModularScore(g, Weight, 10, nil) - p, err := Profile(fn, true, 1e-3, 0.1, 10) - if err != nil { - t.Errorf("%s: unexpected error: %v", test.name, err) + testProfileDirected(t, test, g) + } +} + +func TestProfileWeightedDirected(t *testing.T) { + for _, test := range communityDirectedQTests { + g := simple.NewWeightedDirectedGraph(0, 0) + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) + } + for v := range e { + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + } } - const tries = 1000 - for i, d := range p { - var score float64 - for i := 0; i < tries; i++ { - score, _ = fn(d.Low) - if score >= d.Score { - break - } - } - if score < d.Score { - t.Errorf("%s: failed to recover low end score: got: %v want: %v", test.name, score, d.Score) - } - if i != 0 && d.Score >= p[i-1].Score { - t.Errorf("%s: not monotonically decreasing: %v -> %v", test.name, p[i-1], d) + testProfileDirected(t, test, g) + } +} + +func testProfileDirected(t *testing.T, test communityDirectedQTest, g graph.Directed) { + fn := ModularScore(g, Weight, 10, nil) + p, err := Profile(fn, true, 1e-3, 0.1, 10) + if err != nil { + t.Errorf("%s: unexpected error: %v", test.name, err) + } + + const tries = 1000 + for i, d := range p { + var score float64 + for i := 0; i < tries; i++ { + score, _ = fn(d.Low) + if score >= d.Score { + break } } + if score < d.Score { + t.Errorf("%s: failed to recover low end score: got: %v want: %v", test.name, score, d.Score) + } + if i != 0 && d.Score >= p[i-1].Score { + t.Errorf("%s: not monotonically decreasing: %v -> %v", test.name, p[i-1], d) + } } } diff --git a/graph/community/louvain_common.go b/graph/community/louvain_common.go index 3258f125..987776c0 100644 --- a/graph/community/louvain_common.go +++ b/graph/community/louvain_common.go @@ -356,7 +356,8 @@ const ( ) // positiveWeightFuncFor returns a constructed weight function for the -// positively weighted g. +// positively weighted g. Unweighted graphs have unit weight for existing +// edges. func positiveWeightFuncFor(g graph.Graph) func(x, y graph.Node) float64 { if wg, ok := g.(graph.Weighted); ok { return func(x, y graph.Node) float64 { @@ -375,16 +376,13 @@ func positiveWeightFuncFor(g graph.Graph) func(x, y graph.Node) float64 { if e == nil { return 0 } - w := e.Weight() - if w < 0 { - panic(negativeWeight) - } - return w + return 1 } } // negativeWeightFuncFor returns a constructed weight function for the -// negatively weighted g. +// negatively weighted g. Unweighted graphs have unit weight for existing +// edges. func negativeWeightFuncFor(g graph.Graph) func(x, y graph.Node) float64 { if wg, ok := g.(graph.Weighted); ok { return func(x, y graph.Node) float64 { @@ -403,11 +401,7 @@ func negativeWeightFuncFor(g graph.Graph) func(x, y graph.Node) float64 { if e == nil { return 0 } - w := e.Weight() - if w > 0 { - panic(positiveWeight) - } - return -w + return 1 } } diff --git a/graph/community/louvain_directed_multiplex_test.go b/graph/community/louvain_directed_multiplex_test.go index 6d0bf530..f3870a64 100644 --- a/graph/community/louvain_directed_multiplex_test.go +++ b/graph/community/louvain_directed_multiplex_test.go @@ -647,12 +647,32 @@ func TestLouvainDirectedMultiplex(t *testing.T) { } func TestNonContiguousDirectedMultiplex(t *testing.T) { - g := simple.NewDirectedGraph(0, 0) + g := simple.NewDirectedGraph() + for _, e := range []simple.Edge{ + {F: simple.Node(0), T: simple.Node(1)}, + {F: simple.Node(4), T: simple.Node(5)}, + } { + g.SetEdge(e) + } + + func() { + defer func() { + r := recover() + if r != nil { + t.Error("unexpected panic with non-contiguous ID range") + } + }() + ModularizeMultiplex(DirectedLayers{g}, nil, nil, true, nil) + }() +} + +func TestNonContiguousWeightedDirectedMultiplex(t *testing.T) { + g := simple.NewWeightedDirectedGraph(0, 0) for _, e := range []simple.Edge{ {F: simple.Node(0), T: simple.Node(1), W: 1}, {F: simple.Node(4), T: simple.Node(5), W: 1}, } { - g.SetEdge(e) + g.SetWeightedEdge(e) } func() { @@ -677,7 +697,7 @@ func directedMultiplexFrom(raw []layer) (DirectedLayers, []float64, error) { var layers []graph.Directed var weights []float64 for _, l := range raw { - g := simple.NewDirectedGraph(0, 0) + g := simple.NewWeightedDirectedGraph(0, 0) for u, e := range l.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { @@ -688,7 +708,7 @@ func directedMultiplexFrom(raw []layer) (DirectedLayers, []float64, error) { if l.edgeWeight != 0 { w = l.edgeWeight } - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: w}) + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: w}) } } layers = append(layers, g) diff --git a/graph/community/louvain_directed_test.go b/graph/community/louvain_directed_test.go index 25373e80..6fc5487d 100644 --- a/graph/community/louvain_directed_test.go +++ b/graph/community/louvain_directed_test.go @@ -17,13 +17,15 @@ import ( "gonum.org/v1/gonum/graph/simple" ) -var communityDirectedQTests = []struct { +type communityDirectedQTest struct { name string g []intset structures []structure wantLevels []level -}{ +} + +var communityDirectedQTests = []communityDirectedQTest{ { name: "simple_directed", g: simpleDirected, @@ -199,194 +201,258 @@ var communityDirectedQTests = []struct { func TestCommunityQDirected(t *testing.T) { for _, test := range communityDirectedQTests { - g := simple.NewDirectedGraph(0, 0) + g := simple.NewDirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } - for _, structure := range test.structures { - communities := make([][]graph.Node, len(structure.memberships)) - for i, c := range structure.memberships { - for n := range c { - communities[i] = append(communities[i], simple.Node(n)) - } + + testCommunityQDirected(t, test, g) + } +} + +func TestCommunityQWeightedDirected(t *testing.T) { + for _, test := range communityDirectedQTests { + g := simple.NewWeightedDirectedGraph(0, 0) + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) } - got := Q(g, communities, structure.resolution) - if !floats.EqualWithinAbsOrRel(got, structure.want, structure.tol, structure.tol) && !math.IsNaN(structure.want) { - for _, c := range communities { - sort.Sort(ordered.ByID(c)) - } - t.Errorf("unexpected Q value for %q %v: got: %v want: %v", - test.name, communities, got, structure.want) + for v := range e { + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) } } + + testCommunityQDirected(t, test, g) + } +} + +func testCommunityQDirected(t *testing.T, test communityDirectedQTest, g graph.Directed) { + for _, structure := range test.structures { + communities := make([][]graph.Node, len(structure.memberships)) + for i, c := range structure.memberships { + for n := range c { + communities[i] = append(communities[i], simple.Node(n)) + } + } + got := Q(g, communities, structure.resolution) + if !floats.EqualWithinAbsOrRel(got, structure.want, structure.tol, structure.tol) && !math.IsNaN(structure.want) { + for _, c := range communities { + sort.Sort(ordered.ByID(c)) + } + t.Errorf("unexpected Q value for %q %v: got: %v want: %v", + test.name, communities, got, structure.want) + } } } func TestCommunityDeltaQDirected(t *testing.T) { -tests: for _, test := range communityDirectedQTests { - g := simple.NewDirectedGraph(0, 0) + g := simple.NewDirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } - rnd := rand.New(rand.NewSource(1)).Intn - for _, structure := range test.structures { - communityOf := make(map[int64]int) - communities := make([][]graph.Node, len(structure.memberships)) + testCommunityDeltaQDirected(t, test, g) + } +} + +func TestCommunityDeltaQWeightedDirected(t *testing.T) { + for _, test := range communityDirectedQTests { + g := simple.NewWeightedDirectedGraph(0, 0) + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) + } + for v := range e { + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + } + } + + testCommunityDeltaQDirected(t, test, g) + } +} + +func testCommunityDeltaQDirected(t *testing.T, test communityDirectedQTest, g graph.Directed) { + rnd := rand.New(rand.NewSource(1)).Intn + for _, structure := range test.structures { + communityOf := make(map[int64]int) + communities := make([][]graph.Node, len(structure.memberships)) + for i, c := range structure.memberships { + for n := range c { + n := int64(n) + communityOf[n] = i + communities[i] = append(communities[i], simple.Node(n)) + } + sort.Sort(ordered.ByID(communities[i])) + } + + before := Q(g, communities, structure.resolution) + + l := newDirectedLocalMover(reduceDirected(g, nil), communities, structure.resolution) + if l == nil { + if !math.IsNaN(before) { + t.Errorf("unexpected nil localMover with non-NaN Q graph: Q=%.4v", before) + } + return + } + + // This is done to avoid run-to-run + // variation due to map iteration order. + sort.Sort(ordered.ByID(l.nodes)) + + l.shuffle(rnd) + + for _, target := range l.nodes { + got, gotDst, gotSrc := l.deltaQ(target) + + want, wantDst := math.Inf(-1), -1 + migrated := make([][]graph.Node, len(structure.memberships)) for i, c := range structure.memberships { for n := range c { n := int64(n) - communityOf[n] = i - communities[i] = append(communities[i], simple.Node(n)) - } - sort.Sort(ordered.ByID(communities[i])) - } - - before := Q(g, communities, structure.resolution) - - l := newDirectedLocalMover(reduceDirected(g, nil), communities, structure.resolution) - if l == nil { - if !math.IsNaN(before) { - t.Errorf("unexpected nil localMover with non-NaN Q graph: Q=%.4v", before) - } - continue tests - } - - // This is done to avoid run-to-run - // variation due to map iteration order. - sort.Sort(ordered.ByID(l.nodes)) - - l.shuffle(rnd) - - for _, target := range l.nodes { - got, gotDst, gotSrc := l.deltaQ(target) - - want, wantDst := math.Inf(-1), -1 - migrated := make([][]graph.Node, len(structure.memberships)) - for i, c := range structure.memberships { - for n := range c { - n := int64(n) - if n == target.ID() { - continue - } - migrated[i] = append(migrated[i], simple.Node(n)) - } - sort.Sort(ordered.ByID(migrated[i])) - } - - for i, c := range structure.memberships { - if i == communityOf[target.ID()] { + if n == target.ID() { continue } - connected := false - for n := range c { - if g.HasEdgeBetween(simple.Node(n), target) { - connected = true - break - } - } - if !connected { - continue - } - migrated[i] = append(migrated[i], target) - after := Q(g, migrated, structure.resolution) - migrated[i] = migrated[i][:len(migrated[i])-1] - if after-before > want { - want = after - before - wantDst = i - } + migrated[i] = append(migrated[i], simple.Node(n)) } + sort.Sort(ordered.ByID(migrated[i])) + } - if !floats.EqualWithinAbsOrRel(got, want, structure.tol, structure.tol) || gotDst != wantDst { - t.Errorf("unexpected result moving n=%d in c=%d of %s/%.4v: got: %.4v,%d want: %.4v,%d"+ - "\n\t%v\n\t%v", - target.ID(), communityOf[target.ID()], test.name, structure.resolution, got, gotDst, want, wantDst, - communities, migrated) + for i, c := range structure.memberships { + if i == communityOf[target.ID()] { + continue } - if gotSrc.community != communityOf[target.ID()] { - t.Errorf("unexpected source community index: got: %d want: %d", gotSrc, communityOf[target.ID()]) - } else if communities[gotSrc.community][gotSrc.node].ID() != target.ID() { - wantNodeIdx := -1 - for i, n := range communities[gotSrc.community] { - if n.ID() == target.ID() { - wantNodeIdx = i - break - } + connected := false + for n := range c { + if g.HasEdgeBetween(simple.Node(n), target) { + connected = true + break } - t.Errorf("unexpected source node index: got: %d want: %d", gotSrc.node, wantNodeIdx) } + if !connected { + continue + } + migrated[i] = append(migrated[i], target) + after := Q(g, migrated, structure.resolution) + migrated[i] = migrated[i][:len(migrated[i])-1] + if after-before > want { + want = after - before + wantDst = i + } + } + + if !floats.EqualWithinAbsOrRel(got, want, structure.tol, structure.tol) || gotDst != wantDst { + t.Errorf("unexpected result moving n=%d in c=%d of %s/%.4v: got: %.4v,%d want: %.4v,%d"+ + "\n\t%v\n\t%v", + target.ID(), communityOf[target.ID()], test.name, structure.resolution, got, gotDst, want, wantDst, + communities, migrated) + } + if gotSrc.community != communityOf[target.ID()] { + t.Errorf("unexpected source community index: got: %d want: %d", gotSrc, communityOf[target.ID()]) + } else if communities[gotSrc.community][gotSrc.node].ID() != target.ID() { + wantNodeIdx := -1 + for i, n := range communities[gotSrc.community] { + if n.ID() == target.ID() { + wantNodeIdx = i + break + } + } + t.Errorf("unexpected source node index: got: %d want: %d", gotSrc.node, wantNodeIdx) } } } } func TestReduceQConsistencyDirected(t *testing.T) { -tests: for _, test := range communityDirectedQTests { - g := simple.NewDirectedGraph(0, 0) + g := simple.NewDirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } - for _, structure := range test.structures { - if math.IsNaN(structure.want) { - continue tests - } + testReduceQConsistencyDirected(t, test, g) + } +} - communities := make([][]graph.Node, len(structure.memberships)) - for i, c := range structure.memberships { - for n := range c { - communities[i] = append(communities[i], simple.Node(n)) - } - sort.Sort(ordered.ByID(communities[i])) +func TestReduceQConsistencyWeightedDirected(t *testing.T) { + for _, test := range communityDirectedQTests { + g := simple.NewWeightedDirectedGraph(0, 0) + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) } + for v := range e { + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + } + } - gQ := Q(g, communities, structure.resolution) - gQnull := Q(g, nil, 1) + testReduceQConsistencyDirected(t, test, g) + } +} - cg0 := reduceDirected(g, nil) - cg0Qnull := Q(cg0, cg0.Structure(), 1) - if !floats.EqualWithinAbsOrRel(gQnull, cg0Qnull, structure.tol, structure.tol) { - t.Errorf("disagreement between null Q from method: %v and function: %v", cg0Qnull, gQnull) - } - cg0Q := Q(cg0, communities, structure.resolution) - if !floats.EqualWithinAbsOrRel(gQ, cg0Q, structure.tol, structure.tol) { - t.Errorf("unexpected Q result after initial reduction: got: %v want :%v", cg0Q, gQ) - } +func testReduceQConsistencyDirected(t *testing.T, test communityDirectedQTest, g graph.Directed) { + for _, structure := range test.structures { + if math.IsNaN(structure.want) { + return + } - cg1 := reduceDirected(cg0, communities) - cg1Q := Q(cg1, cg1.Structure(), structure.resolution) - if !floats.EqualWithinAbsOrRel(gQ, cg1Q, structure.tol, structure.tol) { - t.Errorf("unexpected Q result after second reduction: got: %v want :%v", cg1Q, gQ) + communities := make([][]graph.Node, len(structure.memberships)) + for i, c := range structure.memberships { + for n := range c { + communities[i] = append(communities[i], simple.Node(n)) } + sort.Sort(ordered.ByID(communities[i])) + } + + gQ := Q(g, communities, structure.resolution) + gQnull := Q(g, nil, 1) + + cg0 := reduceDirected(g, nil) + cg0Qnull := Q(cg0, cg0.Structure(), 1) + if !floats.EqualWithinAbsOrRel(gQnull, cg0Qnull, structure.tol, structure.tol) { + t.Errorf("disagreement between null Q from method: %v and function: %v", cg0Qnull, gQnull) + } + cg0Q := Q(cg0, communities, structure.resolution) + if !floats.EqualWithinAbsOrRel(gQ, cg0Q, structure.tol, structure.tol) { + t.Errorf("unexpected Q result after initial reduction: got: %v want :%v", cg0Q, gQ) + } + + cg1 := reduceDirected(cg0, communities) + cg1Q := Q(cg1, cg1.Structure(), structure.resolution) + if !floats.EqualWithinAbsOrRel(gQ, cg1Q, structure.tol, structure.tol) { + t.Errorf("unexpected Q result after second reduction: got: %v want :%v", cg1Q, gQ) } } } -var localDirectedMoveTests = []struct { +type localDirectedMoveTest struct { name string g []intset structures []moveStructures -}{ +} + +var localDirectedMoveTests = []localDirectedMoveTest{ { name: "blondel", g: blondel, @@ -431,39 +497,60 @@ var localDirectedMoveTests = []struct { func TestMoveLocalDirected(t *testing.T) { for _, test := range localDirectedMoveTests { - g := simple.NewDirectedGraph(0, 0) + g := simple.NewDirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } - for _, structure := range test.structures { - communities := make([][]graph.Node, len(structure.memberships)) - for i, c := range structure.memberships { - for n := range c { - communities[i] = append(communities[i], simple.Node(n)) - } - sort.Sort(ordered.ByID(communities[i])) + testMoveLocalDirected(t, test, g) + } +} + +func TestMoveLocalWeightedDirected(t *testing.T) { + for _, test := range localDirectedMoveTests { + g := simple.NewWeightedDirectedGraph(0, 0) + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) } + for v := range e { + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + } + } - r := reduceDirected(reduceDirected(g, nil), communities) + testMoveLocalDirected(t, test, g) + } +} - l := newDirectedLocalMover(r, r.communities, structure.resolution) - for _, n := range structure.targetNodes { - dQ, dst, src := l.deltaQ(n) - if dQ > 0 { - before := Q(r, l.communities, structure.resolution) - l.move(dst, src) - after := Q(r, l.communities, structure.resolution) - want := after - before - if !floats.EqualWithinAbsOrRel(dQ, want, structure.tol, structure.tol) { - t.Errorf("unexpected deltaQ: got: %v want: %v", dQ, want) - } +func testMoveLocalDirected(t *testing.T, test localDirectedMoveTest, g graph.Directed) { + for _, structure := range test.structures { + communities := make([][]graph.Node, len(structure.memberships)) + for i, c := range structure.memberships { + for n := range c { + communities[i] = append(communities[i], simple.Node(n)) + } + sort.Sort(ordered.ByID(communities[i])) + } + + r := reduceDirected(reduceDirected(g, nil), communities) + + l := newDirectedLocalMover(r, r.communities, structure.resolution) + for _, n := range structure.targetNodes { + dQ, dst, src := l.deltaQ(n) + if dQ > 0 { + before := Q(r, l.communities, structure.resolution) + l.move(dst, src) + after := Q(r, l.communities, structure.resolution) + want := after - before + if !floats.EqualWithinAbsOrRel(dQ, want, structure.tol, structure.tol) { + t.Errorf("unexpected deltaQ: got: %v want: %v", dQ, want) } } } @@ -471,105 +558,146 @@ func TestMoveLocalDirected(t *testing.T) { } func TestModularizeDirected(t *testing.T) { - const louvainIterations = 20 - for _, test := range communityDirectedQTests { - g := simple.NewDirectedGraph(0, 0) + g := simple.NewDirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } - if test.structures[0].resolution != 1 { - panic("bad test: expect resolution=1") - } - want := make([][]graph.Node, len(test.structures[0].memberships)) - for i, c := range test.structures[0].memberships { - for n := range c { - want[i] = append(want[i], simple.Node(n)) + testModularizeDirected(t, test, g) + } +} + +func TestModularizeWeightedDirected(t *testing.T) { + for _, test := range communityDirectedQTests { + g := simple.NewWeightedDirectedGraph(0, 0) + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) } - sort.Sort(ordered.ByID(want[i])) - } - sort.Sort(ordered.BySliceIDs(want)) - - var ( - got *ReducedDirected - bestQ = math.Inf(-1) - ) - // Modularize is randomised so we do this to - // ensure the level tests are consistent. - src := rand.New(rand.NewSource(1)) - for i := 0; i < louvainIterations; i++ { - r := Modularize(g, 1, src).(*ReducedDirected) - if q := Q(r, nil, 1); q > bestQ || math.IsNaN(q) { - bestQ = q - got = r - - if math.IsNaN(q) { - // Don't try again for non-connected case. - break - } - } - - var qs []float64 - for p := r; p != nil; p = p.Expanded().(*ReducedDirected) { - qs = append(qs, Q(p, nil, 1)) - } - - // Recovery of Q values is reversed. - if reverse(qs); !sort.Float64sAreSorted(qs) { - t.Errorf("Q values not monotonically increasing: %.5v", qs) + for v := range e { + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) } } - gotCommunities := got.Communities() - for _, c := range gotCommunities { - sort.Sort(ordered.ByID(c)) - } - sort.Sort(ordered.BySliceIDs(gotCommunities)) - if !reflect.DeepEqual(gotCommunities, want) { - t.Errorf("unexpected community membership for %s Q=%.4v:\n\tgot: %v\n\twant:%v", - test.name, bestQ, gotCommunities, want) - continue - } + testModularizeDirected(t, test, g) + } +} + +func testModularizeDirected(t *testing.T, test communityDirectedQTest, g graph.Directed) { + const louvainIterations = 20 + + if test.structures[0].resolution != 1 { + panic("bad test: expect resolution=1") + } + want := make([][]graph.Node, len(test.structures[0].memberships)) + for i, c := range test.structures[0].memberships { + for n := range c { + want[i] = append(want[i], simple.Node(n)) + } + sort.Sort(ordered.ByID(want[i])) + } + sort.Sort(ordered.BySliceIDs(want)) + + var ( + got *ReducedDirected + bestQ = math.Inf(-1) + ) + // Modularize is randomised so we do this to + // ensure the level tests are consistent. + src := rand.New(rand.NewSource(1)) + for i := 0; i < louvainIterations; i++ { + r := Modularize(g, 1, src).(*ReducedDirected) + if q := Q(r, nil, 1); q > bestQ || math.IsNaN(q) { + bestQ = q + got = r - var levels []level - for p := got; p != nil; p = p.Expanded().(*ReducedDirected) { - var communities [][]graph.Node - if p.parent != nil { - communities = p.parent.Communities() - for _, c := range communities { - sort.Sort(ordered.ByID(c)) - } - sort.Sort(ordered.BySliceIDs(communities)) - } else { - communities = reduceDirected(g, nil).Communities() - } - q := Q(p, nil, 1) if math.IsNaN(q) { - // Use an equalable flag value in place of NaN. - q = math.Inf(-1) + // Don't try again for non-connected case. + break } - levels = append(levels, level{q: q, communities: communities}) } - if !reflect.DeepEqual(levels, test.wantLevels) { - t.Errorf("unexpected level structure:\n\tgot: %v\n\twant:%v", levels, test.wantLevels) + + var qs []float64 + for p := r; p != nil; p = p.Expanded().(*ReducedDirected) { + qs = append(qs, Q(p, nil, 1)) } + + // Recovery of Q values is reversed. + if reverse(qs); !sort.Float64sAreSorted(qs) { + t.Errorf("Q values not monotonically increasing: %.5v", qs) + } + } + + gotCommunities := got.Communities() + for _, c := range gotCommunities { + sort.Sort(ordered.ByID(c)) + } + sort.Sort(ordered.BySliceIDs(gotCommunities)) + if !reflect.DeepEqual(gotCommunities, want) { + t.Errorf("unexpected community membership for %s Q=%.4v:\n\tgot: %v\n\twant:%v", + test.name, bestQ, gotCommunities, want) + return + } + + var levels []level + for p := got; p != nil; p = p.Expanded().(*ReducedDirected) { + var communities [][]graph.Node + if p.parent != nil { + communities = p.parent.Communities() + for _, c := range communities { + sort.Sort(ordered.ByID(c)) + } + sort.Sort(ordered.BySliceIDs(communities)) + } else { + communities = reduceDirected(g, nil).Communities() + } + q := Q(p, nil, 1) + if math.IsNaN(q) { + // Use an equalable flag value in place of NaN. + q = math.Inf(-1) + } + levels = append(levels, level{q: q, communities: communities}) + } + if !reflect.DeepEqual(levels, test.wantLevels) { + t.Errorf("unexpected level structure:\n\tgot: %v\n\twant:%v", levels, test.wantLevels) } } func TestNonContiguousDirected(t *testing.T) { - g := simple.NewDirectedGraph(0, 0) + g := simple.NewDirectedGraph() + for _, e := range []simple.Edge{ + {F: simple.Node(0), T: simple.Node(1)}, + {F: simple.Node(4), T: simple.Node(5)}, + } { + g.SetEdge(e) + } + + func() { + defer func() { + r := recover() + if r != nil { + t.Error("unexpected panic with non-contiguous ID range") + } + }() + Modularize(g, 1, nil) + }() +} + +func TestNonContiguousWeightedDirected(t *testing.T) { + g := simple.NewWeightedDirectedGraph(0, 0) for _, e := range []simple.Edge{ {F: simple.Node(0), T: simple.Node(1), W: 1}, {F: simple.Node(4), T: simple.Node(5), W: 1}, } { - g.SetEdge(e) + g.SetWeightedEdge(e) } func() { diff --git a/graph/community/louvain_test.go b/graph/community/louvain_test.go index 7ef6c045..32631404 100644 --- a/graph/community/louvain_test.go +++ b/graph/community/louvain_test.go @@ -229,8 +229,8 @@ func hasNegative(f []float64) bool { } var ( - dupGraph = simple.NewUndirectedGraph(0, 0) - dupGraphDirected = simple.NewDirectedGraph(0, 0) + dupGraph = simple.NewUndirectedGraph() + dupGraphDirected = simple.NewDirectedGraph() ) func init() { diff --git a/graph/community/louvain_undirected_multiplex_test.go b/graph/community/louvain_undirected_multiplex_test.go index 769e776b..bef41c14 100644 --- a/graph/community/louvain_undirected_multiplex_test.go +++ b/graph/community/louvain_undirected_multiplex_test.go @@ -616,12 +616,32 @@ func TestLouvainMultiplex(t *testing.T) { } func TestNonContiguousUndirectedMultiplex(t *testing.T) { - g := simple.NewUndirectedGraph(0, 0) + g := simple.NewUndirectedGraph() + for _, e := range []simple.Edge{ + {F: simple.Node(0), T: simple.Node(1)}, + {F: simple.Node(4), T: simple.Node(5)}, + } { + g.SetEdge(e) + } + + func() { + defer func() { + r := recover() + if r != nil { + t.Error("unexpected panic with non-contiguous ID range") + } + }() + ModularizeMultiplex(UndirectedLayers{g}, nil, nil, true, nil) + }() +} + +func TestNonContiguousWeightedUndirectedMultiplex(t *testing.T) { + g := simple.NewWeightedUndirectedGraph(0, 0) for _, e := range []simple.Edge{ {F: simple.Node(0), T: simple.Node(1), W: 1}, {F: simple.Node(4), T: simple.Node(5), W: 1}, } { - g.SetEdge(e) + g.SetWeightedEdge(e) } func() { @@ -646,7 +666,7 @@ func undirectedMultiplexFrom(raw []layer) (UndirectedLayers, []float64, error) { var layers []graph.Undirected var weights []float64 for _, l := range raw { - g := simple.NewUndirectedGraph(0, 0) + g := simple.NewWeightedUndirectedGraph(0, 0) for u, e := range l.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { @@ -657,7 +677,7 @@ func undirectedMultiplexFrom(raw []layer) (UndirectedLayers, []float64, error) { if l.edgeWeight != 0 { w = l.edgeWeight } - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: w}) + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: w}) } } layers = append(layers, g) diff --git a/graph/community/louvain_undirected_test.go b/graph/community/louvain_undirected_test.go index 1cf46c4e..6aa7f8ac 100644 --- a/graph/community/louvain_undirected_test.go +++ b/graph/community/louvain_undirected_test.go @@ -17,13 +17,15 @@ import ( "gonum.org/v1/gonum/graph/simple" ) -var communityUndirectedQTests = []struct { +type communityUndirectedQTest struct { name string g []intset structures []structure wantLevels []level -}{ +} + +var communityUndirectedQTests = []communityUndirectedQTest{ // The java reference implementation is available from http://www.ludowaltman.nl/slm/. { name: "unconnected", @@ -258,194 +260,258 @@ var communityUndirectedQTests = []struct { func TestCommunityQUndirected(t *testing.T) { for _, test := range communityUndirectedQTests { - g := simple.NewUndirectedGraph(0, 0) + g := simple.NewUndirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } - for _, structure := range test.structures { - communities := make([][]graph.Node, len(structure.memberships)) - for i, c := range structure.memberships { - for n := range c { - communities[i] = append(communities[i], simple.Node(n)) - } + + testCommunityQUndirected(t, test, g) + } +} + +func TestCommunityQWeightedUndirected(t *testing.T) { + for _, test := range communityUndirectedQTests { + g := simple.NewWeightedUndirectedGraph(0, 0) + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) } - got := Q(g, communities, structure.resolution) - if !floats.EqualWithinAbsOrRel(got, structure.want, structure.tol, structure.tol) && !math.IsNaN(structure.want) { - for _, c := range communities { - sort.Sort(ordered.ByID(c)) - } - t.Errorf("unexpected Q value for %q %v: got: %v want: %v", - test.name, communities, got, structure.want) + for v := range e { + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) } } + + testCommunityQUndirected(t, test, g) + } +} + +func testCommunityQUndirected(t *testing.T, test communityUndirectedQTest, g graph.Undirected) { + for _, structure := range test.structures { + communities := make([][]graph.Node, len(structure.memberships)) + for i, c := range structure.memberships { + for n := range c { + communities[i] = append(communities[i], simple.Node(n)) + } + } + got := Q(g, communities, structure.resolution) + if !floats.EqualWithinAbsOrRel(got, structure.want, structure.tol, structure.tol) && !math.IsNaN(structure.want) { + for _, c := range communities { + sort.Sort(ordered.ByID(c)) + } + t.Errorf("unexpected Q value for %q %v: got: %v want: %v", + test.name, communities, got, structure.want) + } } } func TestCommunityDeltaQUndirected(t *testing.T) { -tests: for _, test := range communityUndirectedQTests { - g := simple.NewUndirectedGraph(0, 0) + g := simple.NewUndirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } - rnd := rand.New(rand.NewSource(1)).Intn - for _, structure := range test.structures { - communityOf := make(map[int64]int) - communities := make([][]graph.Node, len(structure.memberships)) + testCommunityDeltaQUndirected(t, test, g) + } +} + +func TestCommunityDeltaQWeightedUndirected(t *testing.T) { + for _, test := range communityUndirectedQTests { + g := simple.NewWeightedUndirectedGraph(0, 0) + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) + } + for v := range e { + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + } + } + + testCommunityDeltaQUndirected(t, test, g) + } +} + +func testCommunityDeltaQUndirected(t *testing.T, test communityUndirectedQTest, g graph.Undirected) { + rnd := rand.New(rand.NewSource(1)).Intn + for _, structure := range test.structures { + communityOf := make(map[int64]int) + communities := make([][]graph.Node, len(structure.memberships)) + for i, c := range structure.memberships { + for n := range c { + n := int64(n) + communityOf[n] = i + communities[i] = append(communities[i], simple.Node(n)) + } + sort.Sort(ordered.ByID(communities[i])) + } + + before := Q(g, communities, structure.resolution) + + l := newUndirectedLocalMover(reduceUndirected(g, nil), communities, structure.resolution) + if l == nil { + if !math.IsNaN(before) { + t.Errorf("unexpected nil localMover with non-NaN Q graph: Q=%.4v", before) + } + return + } + + // This is done to avoid run-to-run + // variation due to map iteration order. + sort.Sort(ordered.ByID(l.nodes)) + + l.shuffle(rnd) + + for _, target := range l.nodes { + got, gotDst, gotSrc := l.deltaQ(target) + + want, wantDst := math.Inf(-1), -1 + migrated := make([][]graph.Node, len(structure.memberships)) for i, c := range structure.memberships { for n := range c { n := int64(n) - communityOf[n] = i - communities[i] = append(communities[i], simple.Node(n)) - } - sort.Sort(ordered.ByID(communities[i])) - } - - before := Q(g, communities, structure.resolution) - - l := newUndirectedLocalMover(reduceUndirected(g, nil), communities, structure.resolution) - if l == nil { - if !math.IsNaN(before) { - t.Errorf("unexpected nil localMover with non-NaN Q graph: Q=%.4v", before) - } - continue tests - } - - // This is done to avoid run-to-run - // variation due to map iteration order. - sort.Sort(ordered.ByID(l.nodes)) - - l.shuffle(rnd) - - for _, target := range l.nodes { - got, gotDst, gotSrc := l.deltaQ(target) - - want, wantDst := math.Inf(-1), -1 - migrated := make([][]graph.Node, len(structure.memberships)) - for i, c := range structure.memberships { - for n := range c { - n := int64(n) - if n == target.ID() { - continue - } - migrated[i] = append(migrated[i], simple.Node(n)) - } - sort.Sort(ordered.ByID(migrated[i])) - } - - for i, c := range structure.memberships { - if i == communityOf[target.ID()] { + if n == target.ID() { continue } - connected := false - for n := range c { - if g.HasEdgeBetween(simple.Node(n), target) { - connected = true - break - } - } - if !connected { - continue - } - migrated[i] = append(migrated[i], target) - after := Q(g, migrated, structure.resolution) - migrated[i] = migrated[i][:len(migrated[i])-1] - if after-before > want { - want = after - before - wantDst = i - } + migrated[i] = append(migrated[i], simple.Node(n)) } + sort.Sort(ordered.ByID(migrated[i])) + } - if !floats.EqualWithinAbsOrRel(got, want, structure.tol, structure.tol) || gotDst != wantDst { - t.Errorf("unexpected result moving n=%d in c=%d of %s/%.4v: got: %.4v,%d want: %.4v,%d"+ - "\n\t%v\n\t%v", - target.ID(), communityOf[target.ID()], test.name, structure.resolution, got, gotDst, want, wantDst, - communities, migrated) + for i, c := range structure.memberships { + if i == communityOf[target.ID()] { + continue } - if gotSrc.community != communityOf[target.ID()] { - t.Errorf("unexpected source community index: got: %d want: %d", gotSrc, communityOf[target.ID()]) - } else if communities[gotSrc.community][gotSrc.node].ID() != target.ID() { - wantNodeIdx := -1 - for i, n := range communities[gotSrc.community] { - if n.ID() == target.ID() { - wantNodeIdx = i - break - } + connected := false + for n := range c { + if g.HasEdgeBetween(simple.Node(n), target) { + connected = true + break } - t.Errorf("unexpected source node index: got: %d want: %d", gotSrc.node, wantNodeIdx) } + if !connected { + continue + } + migrated[i] = append(migrated[i], target) + after := Q(g, migrated, structure.resolution) + migrated[i] = migrated[i][:len(migrated[i])-1] + if after-before > want { + want = after - before + wantDst = i + } + } + + if !floats.EqualWithinAbsOrRel(got, want, structure.tol, structure.tol) || gotDst != wantDst { + t.Errorf("unexpected result moving n=%d in c=%d of %s/%.4v: got: %.4v,%d want: %.4v,%d"+ + "\n\t%v\n\t%v", + target.ID(), communityOf[target.ID()], test.name, structure.resolution, got, gotDst, want, wantDst, + communities, migrated) + } + if gotSrc.community != communityOf[target.ID()] { + t.Errorf("unexpected source community index: got: %d want: %d", gotSrc, communityOf[target.ID()]) + } else if communities[gotSrc.community][gotSrc.node].ID() != target.ID() { + wantNodeIdx := -1 + for i, n := range communities[gotSrc.community] { + if n.ID() == target.ID() { + wantNodeIdx = i + break + } + } + t.Errorf("unexpected source node index: got: %d want: %d", gotSrc.node, wantNodeIdx) } } } } func TestReduceQConsistencyUndirected(t *testing.T) { -tests: for _, test := range communityUndirectedQTests { - g := simple.NewUndirectedGraph(0, 0) + g := simple.NewUndirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } - for _, structure := range test.structures { - if math.IsNaN(structure.want) { - continue tests - } + testReduceQConsistencyUndirected(t, test, g) + } +} - communities := make([][]graph.Node, len(structure.memberships)) - for i, c := range structure.memberships { - for n := range c { - communities[i] = append(communities[i], simple.Node(n)) - } - sort.Sort(ordered.ByID(communities[i])) +func TestReduceQConsistencyWeightedUndirected(t *testing.T) { + for _, test := range communityUndirectedQTests { + g := simple.NewWeightedUndirectedGraph(0, 0) + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) } + for v := range e { + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + } + } - gQ := Q(g, communities, structure.resolution) - gQnull := Q(g, nil, 1) + testReduceQConsistencyUndirected(t, test, g) + } +} - cg0 := reduceUndirected(g, nil) - cg0Qnull := Q(cg0, cg0.Structure(), 1) - if !floats.EqualWithinAbsOrRel(gQnull, cg0Qnull, structure.tol, structure.tol) { - t.Errorf("disagreement between null Q from method: %v and function: %v", cg0Qnull, gQnull) - } - cg0Q := Q(cg0, communities, structure.resolution) - if !floats.EqualWithinAbsOrRel(gQ, cg0Q, structure.tol, structure.tol) { - t.Errorf("unexpected Q result after initial reduction: got: %v want :%v", cg0Q, gQ) - } +func testReduceQConsistencyUndirected(t *testing.T, test communityUndirectedQTest, g graph.Undirected) { + for _, structure := range test.structures { + if math.IsNaN(structure.want) { + return + } - cg1 := reduceUndirected(cg0, communities) - cg1Q := Q(cg1, cg1.Structure(), structure.resolution) - if !floats.EqualWithinAbsOrRel(gQ, cg1Q, structure.tol, structure.tol) { - t.Errorf("unexpected Q result after second reduction: got: %v want :%v", cg1Q, gQ) + communities := make([][]graph.Node, len(structure.memberships)) + for i, c := range structure.memberships { + for n := range c { + communities[i] = append(communities[i], simple.Node(n)) } + sort.Sort(ordered.ByID(communities[i])) + } + + gQ := Q(g, communities, structure.resolution) + gQnull := Q(g, nil, 1) + + cg0 := reduceUndirected(g, nil) + cg0Qnull := Q(cg0, cg0.Structure(), 1) + if !floats.EqualWithinAbsOrRel(gQnull, cg0Qnull, structure.tol, structure.tol) { + t.Errorf("disagreement between null Q from method: %v and function: %v", cg0Qnull, gQnull) + } + cg0Q := Q(cg0, communities, structure.resolution) + if !floats.EqualWithinAbsOrRel(gQ, cg0Q, structure.tol, structure.tol) { + t.Errorf("unexpected Q result after initial reduction: got: %v want :%v", cg0Q, gQ) + } + + cg1 := reduceUndirected(cg0, communities) + cg1Q := Q(cg1, cg1.Structure(), structure.resolution) + if !floats.EqualWithinAbsOrRel(gQ, cg1Q, structure.tol, structure.tol) { + t.Errorf("unexpected Q result after second reduction: got: %v want :%v", cg1Q, gQ) } } } -var localUndirectedMoveTests = []struct { +type localUndirectedMoveTest struct { name string g []intset structures []moveStructures -}{ +} + +var localUndirectedMoveTests = []localUndirectedMoveTest{ { name: "blondel", g: blondel, @@ -490,39 +556,60 @@ var localUndirectedMoveTests = []struct { func TestMoveLocalUndirected(t *testing.T) { for _, test := range localUndirectedMoveTests { - g := simple.NewUndirectedGraph(0, 0) + g := simple.NewUndirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } - for _, structure := range test.structures { - communities := make([][]graph.Node, len(structure.memberships)) - for i, c := range structure.memberships { - for n := range c { - communities[i] = append(communities[i], simple.Node(n)) - } - sort.Sort(ordered.ByID(communities[i])) + testMoveLocalUndirected(t, test, g) + } +} + +func TestMoveLocalWeightedUndirected(t *testing.T) { + for _, test := range localUndirectedMoveTests { + g := simple.NewWeightedUndirectedGraph(0, 0) + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) } + for v := range e { + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + } + } - r := reduceUndirected(reduceUndirected(g, nil), communities) + testMoveLocalUndirected(t, test, g) + } +} - l := newUndirectedLocalMover(r, r.communities, structure.resolution) - for _, n := range structure.targetNodes { - dQ, dst, src := l.deltaQ(n) - if dQ > 0 { - before := Q(r, l.communities, structure.resolution) - l.move(dst, src) - after := Q(r, l.communities, structure.resolution) - want := after - before - if !floats.EqualWithinAbsOrRel(dQ, want, structure.tol, structure.tol) { - t.Errorf("unexpected deltaQ: got: %v want: %v", dQ, want) - } +func testMoveLocalUndirected(t *testing.T, test localUndirectedMoveTest, g graph.Undirected) { + for _, structure := range test.structures { + communities := make([][]graph.Node, len(structure.memberships)) + for i, c := range structure.memberships { + for n := range c { + communities[i] = append(communities[i], simple.Node(n)) + } + sort.Sort(ordered.ByID(communities[i])) + } + + r := reduceUndirected(reduceUndirected(g, nil), communities) + + l := newUndirectedLocalMover(r, r.communities, structure.resolution) + for _, n := range structure.targetNodes { + dQ, dst, src := l.deltaQ(n) + if dQ > 0 { + before := Q(r, l.communities, structure.resolution) + l.move(dst, src) + after := Q(r, l.communities, structure.resolution) + want := after - before + if !floats.EqualWithinAbsOrRel(dQ, want, structure.tol, structure.tol) { + t.Errorf("unexpected deltaQ: got: %v want: %v", dQ, want) } } } @@ -530,105 +617,146 @@ func TestMoveLocalUndirected(t *testing.T) { } func TestModularizeUndirected(t *testing.T) { - const louvainIterations = 20 - for _, test := range communityUndirectedQTests { - g := simple.NewUndirectedGraph(0, 0) + g := simple.NewUndirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } - if test.structures[0].resolution != 1 { - panic("bad test: expect resolution=1") - } - want := make([][]graph.Node, len(test.structures[0].memberships)) - for i, c := range test.structures[0].memberships { - for n := range c { - want[i] = append(want[i], simple.Node(n)) + testModularizeUndirected(t, test, g) + } +} + +func TestModularizeWeightedUndirected(t *testing.T) { + for _, test := range communityUndirectedQTests { + g := simple.NewWeightedUndirectedGraph(0, 0) + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) } - sort.Sort(ordered.ByID(want[i])) - } - sort.Sort(ordered.BySliceIDs(want)) - - var ( - got *ReducedUndirected - bestQ = math.Inf(-1) - ) - // Modularize is randomised so we do this to - // ensure the level tests are consistent. - src := rand.New(rand.NewSource(1)) - for i := 0; i < louvainIterations; i++ { - r := Modularize(g, 1, src).(*ReducedUndirected) - if q := Q(r, nil, 1); q > bestQ || math.IsNaN(q) { - bestQ = q - got = r - - if math.IsNaN(q) { - // Don't try again for non-connected case. - break - } - } - - var qs []float64 - for p := r; p != nil; p = p.Expanded().(*ReducedUndirected) { - qs = append(qs, Q(p, nil, 1)) - } - - // Recovery of Q values is reversed. - if reverse(qs); !sort.Float64sAreSorted(qs) { - t.Errorf("Q values not monotonically increasing: %.5v", qs) + for v := range e { + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) } } - gotCommunities := got.Communities() - for _, c := range gotCommunities { - sort.Sort(ordered.ByID(c)) - } - sort.Sort(ordered.BySliceIDs(gotCommunities)) - if !reflect.DeepEqual(gotCommunities, want) { - t.Errorf("unexpected community membership for %s Q=%.4v:\n\tgot: %v\n\twant:%v", - test.name, bestQ, gotCommunities, want) - continue - } + testModularizeUndirected(t, test, g) + } +} + +func testModularizeUndirected(t *testing.T, test communityUndirectedQTest, g graph.Undirected) { + const louvainIterations = 20 + + if test.structures[0].resolution != 1 { + panic("bad test: expect resolution=1") + } + want := make([][]graph.Node, len(test.structures[0].memberships)) + for i, c := range test.structures[0].memberships { + for n := range c { + want[i] = append(want[i], simple.Node(n)) + } + sort.Sort(ordered.ByID(want[i])) + } + sort.Sort(ordered.BySliceIDs(want)) + + var ( + got *ReducedUndirected + bestQ = math.Inf(-1) + ) + // Modularize is randomised so we do this to + // ensure the level tests are consistent. + src := rand.New(rand.NewSource(1)) + for i := 0; i < louvainIterations; i++ { + r := Modularize(g, 1, src).(*ReducedUndirected) + if q := Q(r, nil, 1); q > bestQ || math.IsNaN(q) { + bestQ = q + got = r - var levels []level - for p := got; p != nil; p = p.Expanded().(*ReducedUndirected) { - var communities [][]graph.Node - if p.parent != nil { - communities = p.parent.Communities() - for _, c := range communities { - sort.Sort(ordered.ByID(c)) - } - sort.Sort(ordered.BySliceIDs(communities)) - } else { - communities = reduceUndirected(g, nil).Communities() - } - q := Q(p, nil, 1) if math.IsNaN(q) { - // Use an equalable flag value in place of NaN. - q = math.Inf(-1) + // Don't try again for non-connected case. + break } - levels = append(levels, level{q: q, communities: communities}) } - if !reflect.DeepEqual(levels, test.wantLevels) { - t.Errorf("unexpected level structure:\n\tgot: %v\n\twant:%v", levels, test.wantLevels) + + var qs []float64 + for p := r; p != nil; p = p.Expanded().(*ReducedUndirected) { + qs = append(qs, Q(p, nil, 1)) } + + // Recovery of Q values is reversed. + if reverse(qs); !sort.Float64sAreSorted(qs) { + t.Errorf("Q values not monotonically increasing: %.5v", qs) + } + } + + gotCommunities := got.Communities() + for _, c := range gotCommunities { + sort.Sort(ordered.ByID(c)) + } + sort.Sort(ordered.BySliceIDs(gotCommunities)) + if !reflect.DeepEqual(gotCommunities, want) { + t.Errorf("unexpected community membership for %s Q=%.4v:\n\tgot: %v\n\twant:%v", + test.name, bestQ, gotCommunities, want) + return + } + + var levels []level + for p := got; p != nil; p = p.Expanded().(*ReducedUndirected) { + var communities [][]graph.Node + if p.parent != nil { + communities = p.parent.Communities() + for _, c := range communities { + sort.Sort(ordered.ByID(c)) + } + sort.Sort(ordered.BySliceIDs(communities)) + } else { + communities = reduceUndirected(g, nil).Communities() + } + q := Q(p, nil, 1) + if math.IsNaN(q) { + // Use an equalable flag value in place of NaN. + q = math.Inf(-1) + } + levels = append(levels, level{q: q, communities: communities}) + } + if !reflect.DeepEqual(levels, test.wantLevels) { + t.Errorf("unexpected level structure:\n\tgot: %v\n\twant:%v", levels, test.wantLevels) } } func TestNonContiguousUndirected(t *testing.T) { - g := simple.NewUndirectedGraph(0, 0) + g := simple.NewUndirectedGraph() + for _, e := range []simple.Edge{ + {F: simple.Node(0), T: simple.Node(1)}, + {F: simple.Node(4), T: simple.Node(5)}, + } { + g.SetEdge(e) + } + + func() { + defer func() { + r := recover() + if r != nil { + t.Error("unexpected panic with non-contiguous ID range") + } + }() + Modularize(g, 1, nil) + }() +} + +func TestNonContiguousWeightedUndirected(t *testing.T) { + g := simple.NewWeightedUndirectedGraph(0, 0) for _, e := range []simple.Edge{ {F: simple.Node(0), T: simple.Node(1), W: 1}, {F: simple.Node(4), T: simple.Node(5), W: 1}, } { - g.SetEdge(e) + g.SetWeightedEdge(e) } func() { diff --git a/graph/encoding/dot/decode_test.go b/graph/encoding/dot/decode_test.go index 0416ecd4..d05b0507 100644 --- a/graph/encoding/dot/decode_test.go +++ b/graph/encoding/dot/decode_test.go @@ -110,7 +110,7 @@ type dotDirectedGraph struct { // newDotDirectedGraph returns a new directed capable of creating user-defined // nodes and edges. func newDotDirectedGraph() *dotDirectedGraph { - return &dotDirectedGraph{DirectedGraph: simple.NewDirectedGraph(0, 0)} + return &dotDirectedGraph{DirectedGraph: simple.NewDirectedGraph()} } // NewNode returns a new node with a unique node ID for the graph. @@ -145,7 +145,7 @@ type dotUndirectedGraph struct { // newDotUndirectedGraph returns a new undirected capable of creating user- // defined nodes and edges. func newDotUndirectedGraph() *dotUndirectedGraph { - return &dotUndirectedGraph{UndirectedGraph: simple.NewUndirectedGraph(0, 0)} + return &dotUndirectedGraph{UndirectedGraph: simple.NewUndirectedGraph()} } // NewNode adds a new node with a unique node ID to the graph. diff --git a/graph/encoding/dot/encode_test.go b/graph/encoding/dot/encode_test.go index 0e94291c..d4dba18c 100644 --- a/graph/encoding/dot/encode_test.go +++ b/graph/encoding/dot/encode_test.go @@ -5,7 +5,6 @@ package dot import ( - "math" "testing" "gonum.org/v1/gonum/graph" @@ -55,7 +54,7 @@ var ( ) func directedGraphFrom(g []intset) graph.Directed { - dg := simple.NewDirectedGraph(0, math.Inf(1)) + dg := simple.NewDirectedGraph() for u, e := range g { for v := range e { dg.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) @@ -65,7 +64,7 @@ func directedGraphFrom(g []intset) graph.Directed { } func undirectedGraphFrom(g []intset) graph.Graph { - dg := simple.NewUndirectedGraph(0, math.Inf(1)) + dg := simple.NewUndirectedGraph() for u, e := range g { for v := range e { dg.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) @@ -85,7 +84,7 @@ func (n namedNode) ID() int64 { return n.id } func (n namedNode) DOTID() string { return n.name } func directedNamedIDGraphFrom(g []intset) graph.Directed { - dg := simple.NewDirectedGraph(0, math.Inf(1)) + dg := simple.NewDirectedGraph() for u, e := range g { u := int64(u) nu := namedNode{id: u, name: alpha[u : u+1]} @@ -98,7 +97,7 @@ func directedNamedIDGraphFrom(g []intset) graph.Directed { } func undirectedNamedIDGraphFrom(g []intset) graph.Graph { - dg := simple.NewUndirectedGraph(0, math.Inf(1)) + dg := simple.NewUndirectedGraph() for u, e := range g { u := int64(u) nu := namedNode{id: u, name: alpha[u : u+1]} @@ -120,7 +119,7 @@ func (n attrNode) ID() int64 { return n.id } func (n attrNode) Attributes() []encoding.Attribute { return n.attr } func directedNodeAttrGraphFrom(g []intset, attr [][]encoding.Attribute) graph.Directed { - dg := simple.NewDirectedGraph(0, math.Inf(1)) + dg := simple.NewDirectedGraph() for u, e := range g { u := int64(u) var at []encoding.Attribute @@ -140,7 +139,7 @@ func directedNodeAttrGraphFrom(g []intset, attr [][]encoding.Attribute) graph.Di } func undirectedNodeAttrGraphFrom(g []intset, attr [][]encoding.Attribute) graph.Graph { - dg := simple.NewUndirectedGraph(0, math.Inf(1)) + dg := simple.NewUndirectedGraph() for u, e := range g { u := int64(u) var at []encoding.Attribute @@ -170,7 +169,7 @@ func (n namedAttrNode) DOTID() string { return n.name } func (n namedAttrNode) Attributes() []encoding.Attribute { return n.attr } func directedNamedIDNodeAttrGraphFrom(g []intset, attr [][]encoding.Attribute) graph.Directed { - dg := simple.NewDirectedGraph(0, math.Inf(1)) + dg := simple.NewDirectedGraph() for u, e := range g { u := int64(u) var at []encoding.Attribute @@ -190,7 +189,7 @@ func directedNamedIDNodeAttrGraphFrom(g []intset, attr [][]encoding.Attribute) g } func undirectedNamedIDNodeAttrGraphFrom(g []intset, attr [][]encoding.Attribute) graph.Graph { - dg := simple.NewUndirectedGraph(0, math.Inf(1)) + dg := simple.NewUndirectedGraph() for u, e := range g { u := int64(u) var at []encoding.Attribute @@ -221,7 +220,7 @@ func (e attrEdge) Weight() float64 { return 0 } func (e attrEdge) Attributes() []encoding.Attribute { return e.attr } func directedEdgeAttrGraphFrom(g []intset, attr map[edge][]encoding.Attribute) graph.Directed { - dg := simple.NewDirectedGraph(0, math.Inf(1)) + dg := simple.NewDirectedGraph() for u, e := range g { u := int64(u) for v := range e { @@ -232,7 +231,7 @@ func directedEdgeAttrGraphFrom(g []intset, attr map[edge][]encoding.Attribute) g } func undirectedEdgeAttrGraphFrom(g []intset, attr map[edge][]encoding.Attribute) graph.Graph { - dg := simple.NewUndirectedGraph(0, math.Inf(1)) + dg := simple.NewUndirectedGraph() for u, e := range g { u := int64(u) for v := range e { @@ -273,7 +272,7 @@ func (e portedEdge) ToPort() (port, compass string) { } func directedPortedAttrGraphFrom(g []intset, attr [][]encoding.Attribute, ports map[edge]portedEdge) graph.Directed { - dg := simple.NewDirectedGraph(0, math.Inf(1)) + dg := simple.NewDirectedGraph() for u, e := range g { u := int64(u) var at []encoding.Attribute @@ -295,7 +294,7 @@ func directedPortedAttrGraphFrom(g []intset, attr [][]encoding.Attribute, ports } func undirectedPortedAttrGraphFrom(g []intset, attr [][]encoding.Attribute, ports map[edge]portedEdge) graph.Graph { - dg := simple.NewUndirectedGraph(0, math.Inf(1)) + dg := simple.NewUndirectedGraph() for u, e := range g { u := int64(u) var at []encoding.Attribute @@ -337,10 +336,10 @@ type structuredGraph struct { } func undirectedStructuredGraphFrom(c []edge, g ...[]intset) graph.Graph { - s := &structuredGraph{UndirectedGraph: simple.NewUndirectedGraph(0, math.Inf(1))} + s := &structuredGraph{UndirectedGraph: simple.NewUndirectedGraph()} var base int64 for i, sg := range g { - sub := simple.NewUndirectedGraph(0, math.Inf(1)) + sub := simple.NewUndirectedGraph() for u, e := range sg { u := int64(u) for v := range e { @@ -382,7 +381,7 @@ func undirectedSubGraphFrom(g []intset, s map[int64][]intset) graph.Graph { var base int64 subs := make(map[int64]subGraph) for i, sg := range s { - sub := simple.NewUndirectedGraph(0, math.Inf(1)) + sub := simple.NewUndirectedGraph() for u, e := range sg { u := int64(u) for v := range e { @@ -394,7 +393,7 @@ func undirectedSubGraphFrom(g []intset, s map[int64][]intset) graph.Graph { base += int64(len(sg)) } - dg := simple.NewUndirectedGraph(0, math.Inf(1)) + dg := simple.NewUndirectedGraph() for u, e := range g { u := int64(u) var nu graph.Node diff --git a/graph/encoding/graphql/decode_test.go b/graph/encoding/graphql/decode_test.go index 84ba00d8..4bcc7925 100644 --- a/graph/encoding/graphql/decode_test.go +++ b/graph/encoding/graphql/decode_test.go @@ -155,7 +155,7 @@ type directedGraph struct { } func newDirectedGraph() *directedGraph { - return &directedGraph{DirectedGraph: simple.NewDirectedGraph(0, 0)} + return &directedGraph{DirectedGraph: simple.NewDirectedGraph()} } func (g *directedGraph) NewNode() graph.Node { diff --git a/graph/graph.go b/graph/graph.go index 50750b6a..0ca587d8 100644 --- a/graph/graph.go +++ b/graph/graph.go @@ -15,13 +15,15 @@ type Node interface { type Edge interface { From() Node To() Node - Weight() float64 } -// WeightedEdge is a graph edge. In directed graphs, the direction +// WeightedEdge is a weighted graph edge. In directed graphs, the direction // of the edge is given from -> to, otherwise the edge is semantically // unordered. -type WeightedEdge Edge +type WeightedEdge interface { + Edge + Weight() float64 +} // Graph is a generalized graph. type Graph interface { @@ -143,6 +145,22 @@ type EdgeAdder interface { SetEdge(e Edge) } +// WeightedEdgeAdder is an interface for adding edges to a graph. +type WeightedEdgeAdder interface { + // NewWeightedEdge returns a new WeightedEdge from + // the source to the destination node. + NewWeightedEdge(from, to Node, weight float64) WeightedEdge + + // SetWeightedEdge adds an edge from one node to + // another. If the graph supports node addition + // the nodes will be added if they do not exist, + // otherwise SetWeightedEdge will panic. + // The behavior of a WeightedEdgeAdder when the IDs + // returned by e.From and e.To are equal is + // implementation-dependent. + SetWeightedEdge(e WeightedEdge) +} + // EdgeRemover is an interface for removing nodes from a graph. type EdgeRemover interface { // RemoveEdge removes the given edge, leaving the @@ -157,29 +175,42 @@ type Builder interface { EdgeAdder } +// WeightedBuilder is a graph that can have nodes and weighted edges added. +type WeightedBuilder interface { + NodeAdder + WeightedEdgeAdder +} + // UndirectedBuilder is an undirected graph builder. type UndirectedBuilder interface { Undirected Builder } +// UndirectedWeightedBuilder is an undirected weighted graph builder. +type UndirectedWeightedBuilder interface { + Undirected + WeightedBuilder +} + // DirectedBuilder is a directed graph builder. type DirectedBuilder interface { Directed Builder } +// DirectedWeightedBuilder is a directed weighted graph builder. +type DirectedWeightedBuilder interface { + Directed + WeightedBuilder +} + // Copy copies nodes and edges as undirected edges from the source to the destination // without first clearing the destination. Copy will panic if a node ID in the source // graph matches a node ID in the destination. // // If the source is undirected and the destination is directed both directions will // be present in the destination after the copy is complete. -// -// If the source is a directed graph, the destination is undirected, and a fundamental -// cycle exists with two nodes where the edge weights differ, the resulting destination -// graph's edge weight between those nodes is undefined. If there is a defined function -// to resolve such conflicts, an Undirect may be used to do this. func Copy(dst Builder, src Graph) { nodes := src.Nodes() for _, n := range nodes { @@ -191,3 +222,26 @@ func Copy(dst Builder, src Graph) { } } } + +// CopyWeighted copies nodes and edges as undirected edges from the source to the destination +// without first clearing the destination. Copy will panic if a node ID in the source +// graph matches a node ID in the destination. +// +// If the source is undirected and the destination is directed both directions will +// be present in the destination after the copy is complete. +// +// If the source is a directed graph, the destination is undirected, and a fundamental +// cycle exists with two nodes where the edge weights differ, the resulting destination +// graph's edge weight between those nodes is undefined. If there is a defined function +// to resolve such conflicts, an UndirectWeighted may be used to do this. +func CopyWeighted(dst WeightedBuilder, src Weighted) { + nodes := src.Nodes() + for _, n := range nodes { + dst.AddNode(n) + } + for _, u := range nodes { + for _, v := range src.From(u) { + dst.SetWeightedEdge(src.WeightedEdge(u, v)) + } + } +} diff --git a/graph/graphs/gen/batagelj_brandes_test.go b/graph/graphs/gen/batagelj_brandes_test.go index b9993934..544e2e96 100644 --- a/graph/graphs/gen/batagelj_brandes_test.go +++ b/graph/graphs/gen/batagelj_brandes_test.go @@ -5,7 +5,6 @@ package gen import ( - "math" "testing" "gonum.org/v1/gonum/graph" @@ -54,7 +53,7 @@ func (g *gnDirected) SetEdge(e graph.Edge) { func TestGnpUndirected(t *testing.T) { for n := 2; n <= 20; n++ { for p := 0.; p <= 1; p += 0.1 { - g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph(0, math.Inf(1))} + g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph()} err := Gnp(g, n, p, nil) if err != nil { t.Fatalf("unexpected error: n=%d, p=%v: %v", n, p, err) @@ -75,7 +74,7 @@ func TestGnpUndirected(t *testing.T) { func TestGnpDirected(t *testing.T) { for n := 2; n <= 20; n++ { for p := 0.; p <= 1; p += 0.1 { - g := &gnDirected{DirectedBuilder: simple.NewDirectedGraph(0, math.Inf(1))} + g := &gnDirected{DirectedBuilder: simple.NewDirectedGraph()} err := Gnp(g, n, p, nil) if err != nil { t.Fatalf("unexpected error: n=%d, p=%v: %v", n, p, err) @@ -94,7 +93,7 @@ func TestGnmUndirected(t *testing.T) { for n := 2; n <= 20; n++ { nChoose2 := (n - 1) * n / 2 for m := 0; m <= nChoose2; m++ { - g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph(0, math.Inf(1))} + g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph()} err := Gnm(g, n, m, nil) if err != nil { t.Fatalf("unexpected error: n=%d, m=%d: %v", n, m, err) @@ -116,7 +115,7 @@ func TestGnmDirected(t *testing.T) { for n := 2; n <= 20; n++ { nChoose2 := (n - 1) * n / 2 for m := 0; m <= nChoose2*2; m++ { - g := &gnDirected{DirectedBuilder: simple.NewDirectedGraph(0, math.Inf(1))} + g := &gnDirected{DirectedBuilder: simple.NewDirectedGraph()} err := Gnm(g, n, m, nil) if err != nil { t.Fatalf("unexpected error: n=%d, m=%d: %v", n, m, err) @@ -135,7 +134,7 @@ func TestSmallWorldsBBUndirected(t *testing.T) { for n := 2; n <= 20; n++ { for d := 1; d <= (n-1)/2; d++ { for p := 0.; p < 1; p += 0.1 { - g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph(0, math.Inf(1))} + g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph()} err := SmallWorldsBB(g, n, d, p, nil) if err != nil { t.Fatalf("unexpected error: n=%d, d=%d, p=%v: %v", n, d, p, err) @@ -158,7 +157,7 @@ func TestSmallWorldsBBDirected(t *testing.T) { for n := 2; n <= 20; n++ { for d := 1; d <= (n-1)/2; d++ { for p := 0.; p < 1; p += 0.1 { - g := &gnDirected{DirectedBuilder: simple.NewDirectedGraph(0, math.Inf(1))} + g := &gnDirected{DirectedBuilder: simple.NewDirectedGraph()} err := SmallWorldsBB(g, n, d, p, nil) if err != nil { t.Fatalf("unexpected error: n=%d, d=%d, p=%v: %v", n, d, p, err) diff --git a/graph/graphs/gen/duplication_test.go b/graph/graphs/gen/duplication_test.go index 56e7d05a..b58eb899 100644 --- a/graph/graphs/gen/duplication_test.go +++ b/graph/graphs/gen/duplication_test.go @@ -5,7 +5,6 @@ package gen import ( - "math" "testing" "gonum.org/v1/gonum/graph" @@ -38,7 +37,7 @@ func TestDuplication(t *testing.T) { for alpha := 0.1; alpha <= 1; alpha += 0.1 { for delta := 0.; delta <= 1; delta += 0.2 { for sigma := 0.; sigma <= 1; sigma += 0.2 { - g := &duplication{UndirectedMutator: simple.NewUndirectedGraph(0, math.Inf(1))} + g := &duplication{UndirectedMutator: simple.NewUndirectedGraph()} err := Duplication(g, n, delta, alpha, sigma, nil) if err != nil { t.Fatalf("unexpected error: n=%d, alpha=%v, delta=%v sigma=%v: %v", n, alpha, delta, sigma, err) diff --git a/graph/graphs/gen/holme_kim_test.go b/graph/graphs/gen/holme_kim_test.go index 370c42c7..82eb62a8 100644 --- a/graph/graphs/gen/holme_kim_test.go +++ b/graph/graphs/gen/holme_kim_test.go @@ -5,7 +5,6 @@ package gen import ( - "math" "testing" "gonum.org/v1/gonum/graph/simple" @@ -15,7 +14,7 @@ func TestTunableClusteringScaleFree(t *testing.T) { for n := 2; n <= 20; n++ { for m := 0; m < n; m++ { for p := 0.; p <= 1; p += 0.1 { - g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph(0, math.Inf(1))} + g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph()} err := TunableClusteringScaleFree(g, n, m, p, nil) if err != nil { t.Fatalf("unexpected error: n=%d, m=%d, p=%v: %v", n, m, p, err) @@ -37,7 +36,7 @@ func TestTunableClusteringScaleFree(t *testing.T) { func TestPreferentialAttachment(t *testing.T) { for n := 2; n <= 20; n++ { for m := 0; m < n; m++ { - g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph(0, math.Inf(1))} + g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph()} err := PreferentialAttachment(g, n, m, nil) if err != nil { t.Fatalf("unexpected error: n=%d, m=%d: %v", n, m, err) diff --git a/graph/graphs/gen/small_world_test.go b/graph/graphs/gen/small_world_test.go index dd413a76..ef667146 100644 --- a/graph/graphs/gen/small_world_test.go +++ b/graph/graphs/gen/small_world_test.go @@ -5,7 +5,6 @@ package gen import ( - "math" "testing" "gonum.org/v1/gonum/graph/simple" @@ -22,7 +21,7 @@ func TestNavigableSmallWorldUndirected(t *testing.T) { for q := 0; q < 10; q++ { for r := 0.5; r < 10; r++ { for _, dims := range smallWorldDimensionParameters { - g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph(0, math.Inf(1))} + g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph()} err := NavigableSmallWorld(g, dims, p, q, r, nil) n := 1 for _, d := range dims { @@ -51,7 +50,7 @@ func TestNavigableSmallWorldDirected(t *testing.T) { for q := 0; q < 10; q++ { for r := 0.5; r < 10; r++ { for _, dims := range smallWorldDimensionParameters { - g := &gnDirected{DirectedBuilder: simple.NewDirectedGraph(0, math.Inf(1))} + g := &gnDirected{DirectedBuilder: simple.NewDirectedGraph()} err := NavigableSmallWorld(g, dims, p, q, r, nil) n := 1 for _, d := range dims { diff --git a/graph/network/betweenness_test.go b/graph/network/betweenness_test.go index 401b71b1..20c9b4ed 100644 --- a/graph/network/betweenness_test.go +++ b/graph/network/betweenness_test.go @@ -176,15 +176,14 @@ var betweennessTests = []struct { func TestBetweenness(t *testing.T) { for i, test := range betweennessTests { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewUndirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - // Weight omitted to show weight-independence. - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 0}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } got := Betweenness(g) @@ -206,15 +205,14 @@ func TestBetweenness(t *testing.T) { func TestEdgeBetweenness(t *testing.T) { for i, test := range betweennessTests { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewUndirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - // Weight omitted to show weight-independence. - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 0}) + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) } } got := EdgeBetweenness(g) @@ -239,14 +237,14 @@ func TestEdgeBetweenness(t *testing.T) { func TestBetweennessWeighted(t *testing.T) { for i, test := range betweennessTests { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewWeightedUndirectedGraph(0, math.Inf(1)) for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) } } @@ -275,14 +273,14 @@ func TestBetweennessWeighted(t *testing.T) { func TestEdgeBetweennessWeighted(t *testing.T) { for i, test := range betweennessTests { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewWeightedUndirectedGraph(0, math.Inf(1)) for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) } } diff --git a/graph/network/distance_test.go b/graph/network/distance_test.go index 94778f0a..7e2b1a32 100644 --- a/graph/network/distance_test.go +++ b/graph/network/distance_test.go @@ -143,14 +143,14 @@ func TestDistanceCentralityUndirected(t *testing.T) { prec := 1 - int(math.Log10(tol)) for i, test := range undirectedCentralityTests { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewWeightedUndirectedGraph(0, math.Inf(1)) for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) } } p, ok := path.FloydWarshall(g) @@ -333,14 +333,14 @@ func TestDistanceCentralityDirected(t *testing.T) { prec := 1 - int(math.Log10(tol)) for i, test := range directedCentralityTests { - g := simple.NewDirectedGraph(0, math.Inf(1)) + g := simple.NewWeightedDirectedGraph(0, math.Inf(1)) for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { g.AddNode(simple.Node(u)) } for v := range e { - g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) + g.SetWeightedEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1}) } } p, ok := path.FloydWarshall(g) diff --git a/graph/network/hits_test.go b/graph/network/hits_test.go index 4b50685e..b711f36e 100644 --- a/graph/network/hits_test.go +++ b/graph/network/hits_test.go @@ -43,7 +43,7 @@ var hitsTests = []struct { func TestHITS(t *testing.T) { for i, test := range hitsTests { - g := simple.NewDirectedGraph(0, math.Inf(1)) + g := simple.NewDirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { diff --git a/graph/network/page_test.go b/graph/network/page_test.go index d889c6c8..0a85dcf2 100644 --- a/graph/network/page_test.go +++ b/graph/network/page_test.go @@ -81,7 +81,7 @@ var pageRankTests = []struct { func TestPageRank(t *testing.T) { for i, test := range pageRankTests { - g := simple.NewDirectedGraph(0, math.Inf(1)) + g := simple.NewDirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { @@ -105,7 +105,7 @@ func TestPageRank(t *testing.T) { func TestPageRankSparse(t *testing.T) { for i, test := range pageRankTests { - g := simple.NewDirectedGraph(0, math.Inf(1)) + g := simple.NewDirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { diff --git a/graph/path/a_star_test.go b/graph/path/a_star_test.go index 508a8937..3284880d 100644 --- a/graph/path/a_star_test.go +++ b/graph/path/a_star_test.go @@ -154,7 +154,7 @@ func TestAStar(t *testing.T) { } func TestExhaustiveAStar(t *testing.T) { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewWeightedUndirectedGraph(0, math.Inf(1)) nodes := []locatedNode{ {id: 1, x: 0, y: 6}, {id: 2, x: 1, y: 0}, @@ -179,7 +179,7 @@ func TestExhaustiveAStar(t *testing.T) { {from: g.Node(5), to: g.Node(6), cost: 9}, } for _, e := range edges { - g.SetEdge(e) + g.SetWeightedEdge(e) } heuristic := func(u, v graph.Node) float64 { @@ -247,7 +247,7 @@ func TestAStarNullHeuristic(t *testing.T) { for _, test := range testgraphs.ShortestPathTests { g := test.Graph() for _, e := range test.Edges { - g.SetEdge(e) + g.SetWeightedEdge(e) } var ( diff --git a/graph/path/bellman_ford_moore_test.go b/graph/path/bellman_ford_moore_test.go index 55ebefd4..b753368f 100644 --- a/graph/path/bellman_ford_moore_test.go +++ b/graph/path/bellman_ford_moore_test.go @@ -17,7 +17,7 @@ func TestBellmanFordFrom(t *testing.T) { for _, test := range testgraphs.ShortestPathTests { g := test.Graph() for _, e := range test.Edges { - g.SetEdge(e) + g.SetWeightedEdge(e) } pt, ok := BellmanFordFrom(test.Query.From(), g.(graph.Graph)) diff --git a/graph/path/bench_test.go b/graph/path/bench_test.go index 36279957..e08525f0 100644 --- a/graph/path/bench_test.go +++ b/graph/path/bench_test.go @@ -5,7 +5,6 @@ package path import ( - "math" "testing" "gonum.org/v1/gonum/graph" @@ -23,7 +22,7 @@ var ( ) func gnpUndirected(n int, p float64) graph.Undirected { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewUndirectedGraph() gen.Gnp(g, n, p, nil) return g } @@ -65,7 +64,7 @@ var ( ) func navigableSmallWorldUndirected(n, p, q int, r float64) graph.Undirected { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewUndirectedGraph() gen.NavigableSmallWorld(g, []int{n, n}, p, q, r, nil) return g } diff --git a/graph/path/dijkstra_test.go b/graph/path/dijkstra_test.go index 0a71b9e7..ccb74644 100644 --- a/graph/path/dijkstra_test.go +++ b/graph/path/dijkstra_test.go @@ -19,7 +19,7 @@ func TestDijkstraFrom(t *testing.T) { for _, test := range testgraphs.ShortestPathTests { g := test.Graph() for _, e := range test.Edges { - g.SetEdge(e) + g.SetWeightedEdge(e) } var ( @@ -85,7 +85,7 @@ func TestDijkstraAllPaths(t *testing.T) { for _, test := range testgraphs.ShortestPathTests { g := test.Graph() for _, e := range test.Edges { - g.SetEdge(e) + g.SetWeightedEdge(e) } var ( diff --git a/graph/path/dynamic/dstarlite.go b/graph/path/dynamic/dstarlite.go index bcbfdd79..0fd7904a 100644 --- a/graph/path/dynamic/dstarlite.go +++ b/graph/path/dynamic/dstarlite.go @@ -33,7 +33,7 @@ type DStarLite struct { // WorldModel is a mutable weighted directed graph that returns nodes identified // by id number. type WorldModel interface { - graph.Builder + graph.WeightedBuilder graph.WeightedDirected Node(id int64) graph.Node } @@ -104,7 +104,7 @@ func NewDStarLite(s, t graph.Node, g graph.Graph, h path.Heuristic, m WorldModel if w < 0 { panic("D* Lite: negative edge weight") } - d.model.SetEdge(simple.Edge{F: u, T: d.model.Node(v.ID()), W: w}) + d.model.SetWeightedEdge(simple.Edge{F: u, T: d.model.Node(v.ID()), W: w}) } } @@ -302,7 +302,7 @@ func (d *DStarLite) UpdateWorld(changes []graph.Edge) { cOld, _ := d.model.Weight(from, to) u := d.worldNodeFor(from) v := d.worldNodeFor(to) - d.model.SetEdge(simple.Edge{F: u, T: v, W: c}) + d.model.SetWeightedEdge(simple.Edge{F: u, T: v, W: c}) if cOld > c { if u.ID() != d.t.ID() { u.rhs = math.Min(u.rhs, c+v.g) diff --git a/graph/path/dynamic/dstarlite_test.go b/graph/path/dynamic/dstarlite_test.go index 2733f5f6..bdfcc1a6 100644 --- a/graph/path/dynamic/dstarlite_test.go +++ b/graph/path/dynamic/dstarlite_test.go @@ -35,7 +35,7 @@ func TestDStarLiteNullHeuristic(t *testing.T) { g := test.Graph() for _, e := range test.Edges { - g.SetEdge(e) + g.SetWeightedEdge(e) } var ( @@ -47,7 +47,7 @@ func TestDStarLiteNullHeuristic(t *testing.T) { defer func() { panicked = recover() != nil }() - d = NewDStarLite(test.Query.From(), test.Query.To(), g.(graph.Graph), path.NullHeuristic, simple.NewDirectedGraph(0, math.Inf(1))) + d = NewDStarLite(test.Query.From(), test.Query.To(), g.(graph.Graph), path.NullHeuristic, simple.NewWeightedDirectedGraph(0, math.Inf(1))) }() if panicked || test.HasNegativeWeight { if !test.HasNegativeWeight { @@ -579,7 +579,7 @@ func TestDStarLiteDynamic(t *testing.T) { return test.heuristic(ax-bx, ay-by) } - world := simple.NewDirectedGraph(0, math.Inf(1)) + world := simple.NewWeightedDirectedGraph(0, math.Inf(1)) d := NewDStarLite(test.s, test.t, l, heuristic, world) var ( dp *dumper diff --git a/graph/path/floydwarshall_test.go b/graph/path/floydwarshall_test.go index 147658af..d23c0341 100644 --- a/graph/path/floydwarshall_test.go +++ b/graph/path/floydwarshall_test.go @@ -19,7 +19,7 @@ func TestFloydWarshall(t *testing.T) { for _, test := range testgraphs.ShortestPathTests { g := test.Graph() for _, e := range test.Edges { - g.SetEdge(e) + g.SetWeightedEdge(e) } pt, ok := FloydWarshall(g.(graph.Graph)) diff --git a/graph/path/internal/testgraphs/shortest.go b/graph/path/internal/testgraphs/shortest.go index 8d79f939..26a7e68c 100644 --- a/graph/path/internal/testgraphs/shortest.go +++ b/graph/path/internal/testgraphs/shortest.go @@ -25,7 +25,7 @@ func init() { // dynamic shortest path routine in path/dynamic: DStarLite. var ShortestPathTests = []struct { Name string - Graph func() graph.EdgeAdder + Graph func() graph.WeightedEdgeAdder Edges []simple.Edge HasNegativeWeight bool HasNegativeCycle bool @@ -40,7 +40,7 @@ var ShortestPathTests = []struct { // Positive weighted graphs. { Name: "empty directed", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Query: simple.Edge{F: simple.Node(0), T: simple.Node(1)}, Weight: math.Inf(1), @@ -49,7 +49,7 @@ var ShortestPathTests = []struct { }, { Name: "empty undirected", - Graph: func() graph.EdgeAdder { return simple.NewUndirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedUndirectedGraph(0, math.Inf(1)) }, Query: simple.Edge{F: simple.Node(0), T: simple.Node(1)}, Weight: math.Inf(1), @@ -58,7 +58,7 @@ var ShortestPathTests = []struct { }, { Name: "one edge directed", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ {F: simple.Node(0), T: simple.Node(1), W: 1}, }, @@ -74,7 +74,7 @@ var ShortestPathTests = []struct { }, { Name: "one edge self directed", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ {F: simple.Node(0), T: simple.Node(1), W: 1}, }, @@ -90,7 +90,7 @@ var ShortestPathTests = []struct { }, { Name: "one edge undirected", - Graph: func() graph.EdgeAdder { return simple.NewUndirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedUndirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ {F: simple.Node(0), T: simple.Node(1), W: 1}, }, @@ -106,7 +106,7 @@ var ShortestPathTests = []struct { }, { Name: "two paths directed", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ {F: simple.Node(0), T: simple.Node(2), W: 2}, {F: simple.Node(0), T: simple.Node(1), W: 1}, @@ -125,7 +125,7 @@ var ShortestPathTests = []struct { }, { Name: "two paths undirected", - Graph: func() graph.EdgeAdder { return simple.NewUndirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedUndirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ {F: simple.Node(0), T: simple.Node(2), W: 2}, {F: simple.Node(0), T: simple.Node(1), W: 1}, @@ -144,7 +144,7 @@ var ShortestPathTests = []struct { }, { Name: "confounding paths directed", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ // Add a path from 0->5 of weight 4 {F: simple.Node(0), T: simple.Node(1), W: 1}, @@ -178,7 +178,7 @@ var ShortestPathTests = []struct { }, { Name: "confounding paths undirected", - Graph: func() graph.EdgeAdder { return simple.NewUndirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedUndirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ // Add a path from 0->5 of weight 4 {F: simple.Node(0), T: simple.Node(1), W: 1}, @@ -212,7 +212,7 @@ var ShortestPathTests = []struct { }, { Name: "confounding paths directed 2-step", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ // Add a path from 0->5 of weight 4 {F: simple.Node(0), T: simple.Node(1), W: 1}, @@ -247,7 +247,7 @@ var ShortestPathTests = []struct { }, { Name: "confounding paths undirected 2-step", - Graph: func() graph.EdgeAdder { return simple.NewUndirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedUndirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ // Add a path from 0->5 of weight 4 {F: simple.Node(0), T: simple.Node(1), W: 1}, @@ -282,7 +282,7 @@ var ShortestPathTests = []struct { }, { Name: "zero-weight cycle directed", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ // Add a path from 0->4 of weight 4 {F: simple.Node(0), T: simple.Node(1), W: 1}, @@ -306,7 +306,7 @@ var ShortestPathTests = []struct { }, { Name: "zero-weight cycle^2 directed", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ // Add a path from 0->4 of weight 4 {F: simple.Node(0), T: simple.Node(1), W: 1}, @@ -333,7 +333,7 @@ var ShortestPathTests = []struct { }, { Name: "zero-weight cycle^2 confounding directed", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ // Add a path from 0->4 of weight 4 {F: simple.Node(0), T: simple.Node(1), W: 1}, @@ -363,7 +363,7 @@ var ShortestPathTests = []struct { }, { Name: "zero-weight cycle^3 directed", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ // Add a path from 0->4 of weight 4 {F: simple.Node(0), T: simple.Node(1), W: 1}, @@ -393,7 +393,7 @@ var ShortestPathTests = []struct { }, { Name: "zero-weight 3·cycle^2 confounding directed", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ // Add a path from 0->4 of weight 4 {F: simple.Node(0), T: simple.Node(1), W: 1}, @@ -429,7 +429,7 @@ var ShortestPathTests = []struct { }, { Name: "zero-weight reversed 3·cycle^2 confounding directed", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ // Add a path from 0->4 of weight 4 {F: simple.Node(0), T: simple.Node(1), W: 1}, @@ -465,7 +465,7 @@ var ShortestPathTests = []struct { }, { Name: "zero-weight |V|·cycle^(n/|V|) directed", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: func() []simple.Edge { e := []simple.Edge{ // Add a path from 0->4 of weight 4 @@ -498,7 +498,7 @@ var ShortestPathTests = []struct { }, { Name: "zero-weight n·cycle directed", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: func() []simple.Edge { e := []simple.Edge{ // Add a path from 0->4 of weight 4 @@ -531,7 +531,7 @@ var ShortestPathTests = []struct { }, { Name: "zero-weight bi-directional tree with single exit directed", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: func() []simple.Edge { e := []simple.Edge{ // Add a path from 0->4 of weight 4 @@ -579,7 +579,7 @@ var ShortestPathTests = []struct { // Negative weighted graphs. { Name: "one edge directed negative", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ {F: simple.Node(0), T: simple.Node(1), W: -1}, }, @@ -596,7 +596,7 @@ var ShortestPathTests = []struct { }, { Name: "one edge undirected negative", - Graph: func() graph.EdgeAdder { return simple.NewUndirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedUndirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ {F: simple.Node(0), T: simple.Node(1), W: -1}, }, @@ -607,7 +607,7 @@ var ShortestPathTests = []struct { }, { Name: "wp graph negative", // http://en.wikipedia.org/w/index.php?title=Johnson%27s_algorithm&oldid=564595231 - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ {F: simple.Node('w'), T: simple.Node('z'), W: 2}, {F: simple.Node('x'), T: simple.Node('w'), W: 6}, @@ -630,7 +630,7 @@ var ShortestPathTests = []struct { }, { Name: "roughgarden negative", - Graph: func() graph.EdgeAdder { return simple.NewDirectedGraph(0, math.Inf(1)) }, + Graph: func() graph.WeightedEdgeAdder { return simple.NewWeightedDirectedGraph(0, math.Inf(1)) }, Edges: []simple.Edge{ {F: simple.Node('a'), T: simple.Node('b'), W: -2}, {F: simple.Node('b'), T: simple.Node('c'), W: -1}, diff --git a/graph/path/johnson_apsp_test.go b/graph/path/johnson_apsp_test.go index 0d9d4500..1635ce92 100644 --- a/graph/path/johnson_apsp_test.go +++ b/graph/path/johnson_apsp_test.go @@ -19,7 +19,7 @@ func TestJohnsonAllPaths(t *testing.T) { for _, test := range testgraphs.ShortestPathTests { g := test.Graph() for _, e := range test.Edges { - g.SetEdge(e) + g.SetWeightedEdge(e) } pt, ok := JohnsonAllPaths(g.(graph.Graph)) diff --git a/graph/path/spanning_tree_test.go b/graph/path/spanning_tree_test.go index 8c7da7e5..f22d663b 100644 --- a/graph/path/spanning_tree_test.go +++ b/graph/path/spanning_tree_test.go @@ -26,7 +26,7 @@ func init() { } type spanningGraph interface { - graph.Builder + graph.WeightedBuilder graph.WeightedUndirected Edges() []graph.Edge } @@ -40,7 +40,7 @@ var spanningTreeTests = []struct { }{ { name: "Empty", - graph: func() spanningGraph { return simple.NewUndirectedGraph(0, math.Inf(1)) }, + graph: func() spanningGraph { return simple.NewWeightedUndirectedGraph(0, math.Inf(1)) }, want: 0, }, { @@ -48,7 +48,7 @@ var spanningTreeTests = []struct { // Modified to make edge weights unique; A--B is increased to 2.5 otherwise // to prevent the alternative solution being found. name: "Prim WP figure 1", - graph: func() spanningGraph { return simple.NewUndirectedGraph(0, math.Inf(1)) }, + graph: func() spanningGraph { return simple.NewWeightedUndirectedGraph(0, math.Inf(1)) }, edges: []simple.Edge{ {F: simple.Node('A'), T: simple.Node('B'), W: 2.5}, {F: simple.Node('A'), T: simple.Node('D'), W: 1}, @@ -66,7 +66,7 @@ var spanningTreeTests = []struct { { // https://upload.wikimedia.org/wikipedia/commons/5/5c/MST_kruskal_en.gif name: "Kruskal WP figure 1", - graph: func() spanningGraph { return simple.NewUndirectedGraph(0, math.Inf(1)) }, + graph: func() spanningGraph { return simple.NewWeightedUndirectedGraph(0, math.Inf(1)) }, edges: []simple.Edge{ {F: simple.Node('a'), T: simple.Node('b'), W: 3}, {F: simple.Node('a'), T: simple.Node('e'), W: 1}, @@ -88,7 +88,7 @@ var spanningTreeTests = []struct { { // https://upload.wikimedia.org/wikipedia/commons/8/87/Kruskal_Algorithm_6.svg name: "Kruskal WP example", - graph: func() spanningGraph { return simple.NewUndirectedGraph(0, math.Inf(1)) }, + graph: func() spanningGraph { return simple.NewWeightedUndirectedGraph(0, math.Inf(1)) }, edges: []simple.Edge{ {F: simple.Node('A'), T: simple.Node('B'), W: 7}, {F: simple.Node('A'), T: simple.Node('D'), W: 5}, @@ -116,7 +116,7 @@ var spanningTreeTests = []struct { { // https://upload.wikimedia.org/wikipedia/commons/2/2e/Boruvka%27s_algorithm_%28Sollin%27s_algorithm%29_Anim.gif name: "Borůvka WP example", - graph: func() spanningGraph { return simple.NewUndirectedGraph(0, math.Inf(1)) }, + graph: func() spanningGraph { return simple.NewWeightedUndirectedGraph(0, math.Inf(1)) }, edges: []simple.Edge{ {F: simple.Node('A'), T: simple.Node('B'), W: 13}, {F: simple.Node('A'), T: simple.Node('C'), W: 6}, @@ -159,7 +159,7 @@ var spanningTreeTests = []struct { // https://upload.wikimedia.org/wikipedia/commons/d/d2/Minimum_spanning_tree.svg // Nodes labelled row major. name: "Minimum Spanning Tree WP figure 1", - graph: func() spanningGraph { return simple.NewUndirectedGraph(0, math.Inf(1)) }, + graph: func() spanningGraph { return simple.NewWeightedUndirectedGraph(0, math.Inf(1)) }, edges: []simple.Edge{ {F: simple.Node(1), T: simple.Node(2), W: 4}, {F: simple.Node(1), T: simple.Node(3), W: 1}, @@ -202,7 +202,7 @@ var spanningTreeTests = []struct { // https://upload.wikimedia.org/wikipedia/commons/2/2e/Boruvka%27s_algorithm_%28Sollin%27s_algorithm%29_Anim.gif // but with C--H and E--J cut. name: "Borůvka WP example cut", - graph: func() spanningGraph { return simple.NewUndirectedGraph(0, math.Inf(1)) }, + graph: func() spanningGraph { return simple.NewWeightedUndirectedGraph(0, math.Inf(1)) }, edges: []simple.Edge{ {F: simple.Node('A'), T: simple.Node('B'), W: 13}, {F: simple.Node('A'), T: simple.Node('C'), W: 6}, @@ -244,17 +244,17 @@ func testMinumumSpanning(mst func(dst graph.UndirectedBuilder, g spanningGraph) for _, test := range spanningTreeTests { g := test.graph() for _, e := range test.edges { - g.SetEdge(e) + g.SetWeightedEdge(e) } - dst := simple.NewUndirectedGraph(0, math.Inf(1)) + dst := edgeAdder{simple.NewWeightedUndirectedGraph(0, math.Inf(1))} w := mst(dst, g) if w != test.want { t.Errorf("unexpected minimum spanning tree weight for %q: got: %f want: %f", test.name, w, test.want) } var got float64 - for _, e := range dst.Edges() { + for _, e := range dst.WeightedEdges() { got += e.Weight() } if got != test.want { @@ -281,6 +281,18 @@ func testMinumumSpanning(mst func(dst graph.UndirectedBuilder, g spanningGraph) } } +type edgeAdder struct { + *simple.WeightedUndirectedGraph +} + +func (g edgeAdder) NewEdge(x, y graph.Node) graph.Edge { + return g.WeightedUndirectedGraph.NewWeightedEdge(x, y, 1) +} + +func (g edgeAdder) SetEdge(e graph.Edge) { + g.WeightedUndirectedGraph.SetWeightedEdge(e.(graph.WeightedEdge)) +} + func TestKruskal(t *testing.T) { testMinumumSpanning(func(dst graph.UndirectedBuilder, g spanningGraph) float64 { return Kruskal(dst, g) diff --git a/graph/simple/dense_directed_matrix.go b/graph/simple/dense_directed_matrix.go index c9e61d6c..846c6f15 100644 --- a/graph/simple/dense_directed_matrix.go +++ b/graph/simple/dense_directed_matrix.go @@ -218,9 +218,19 @@ func (g *DirectedMatrix) Weight(x, y graph.Node) (w float64, ok bool) { return g.absent, false } -// SetEdge sets e, an edge from one node to another. If the ends of the edge are not in g -// or the edge is a self loop, SetEdge panics. +// SetEdge sets e, an edge from one node to another with unit weight. If the ends of the edge +// are not in g or the edge is a self loop, SetEdge panics. func (g *DirectedMatrix) SetEdge(e graph.Edge) { + g.setWeightedEdge(e, 1) +} + +// SetWeightedEdge sets e, an edge from one node to another. If the ends of the edge are not in g +// or the edge is a self loop, SetWeightedEdge panics. +func (g *DirectedMatrix) SetWeightedEdge(e graph.WeightedEdge) { + g.setWeightedEdge(e, e.Weight()) +} + +func (g *DirectedMatrix) setWeightedEdge(e graph.Edge, weight float64) { fid := e.From().ID() tid := e.To().ID() if fid == tid { @@ -233,7 +243,7 @@ func (g *DirectedMatrix) SetEdge(e graph.Edge) { panic("simple: unavailable to node ID for dense graph") } // fid and tid are not greater than maximum int by this point. - g.mat.Set(int(fid), int(tid), e.Weight()) + g.mat.Set(int(fid), int(tid), weight) } // RemoveEdge removes e from the graph, leaving the terminal nodes. If the edge does not exist diff --git a/graph/simple/dense_undirected_matrix.go b/graph/simple/dense_undirected_matrix.go index 117c0427..70672d6e 100644 --- a/graph/simple/dense_undirected_matrix.go +++ b/graph/simple/dense_undirected_matrix.go @@ -190,9 +190,19 @@ func (g *UndirectedMatrix) Weight(x, y graph.Node) (w float64, ok bool) { return g.absent, false } -// SetEdge sets e, an edge from one node to another. If the ends of the edge are not in g -// or the edge is a self loop, SetEdge panics. +// SetEdge sets e, an edge from one node to another with unit weight. If the ends of the edge are +// not in g or the edge is a self loop, SetEdge panics. func (g *UndirectedMatrix) SetEdge(e graph.Edge) { + g.setWeightedEdge(e, 1) +} + +// SetWeightedEdge sets e, an edge from one node to another. If the ends of the edge are not in g +// or the edge is a self loop, SetWeightedEdge panics. +func (g *UndirectedMatrix) SetWeightedEdge(e graph.WeightedEdge) { + g.setWeightedEdge(e, e.Weight()) +} + +func (g *UndirectedMatrix) setWeightedEdge(e graph.Edge, weight float64) { fid := e.From().ID() tid := e.To().ID() if fid == tid { @@ -205,7 +215,7 @@ func (g *UndirectedMatrix) SetEdge(e graph.Edge) { panic("simple: unavailable to node ID for dense graph") } // fid and tid are not greater than maximum int by this point. - g.mat.SetSym(int(fid), int(tid), e.Weight()) + g.mat.SetSym(int(fid), int(tid), weight) } // RemoveEdge removes e from the graph, leaving the terminal nodes. If the edge does not exist diff --git a/graph/simple/directed.go b/graph/simple/directed.go index 18483d2b..0796cb13 100644 --- a/graph/simple/directed.go +++ b/graph/simple/directed.go @@ -16,22 +16,17 @@ type DirectedGraph struct { from map[int64]map[int64]graph.Edge to map[int64]map[int64]graph.Edge - self, absent float64 - nodeIDs idSet } // NewDirectedGraph returns a DirectedGraph with the specified self and absent // edge weight values. -func NewDirectedGraph(self, absent float64) *DirectedGraph { +func NewDirectedGraph() *DirectedGraph { return &DirectedGraph{ nodes: make(map[int64]graph.Node), from: make(map[int64]map[int64]graph.Edge), to: make(map[int64]map[int64]graph.Edge), - self: self, - absent: absent, - nodeIDs: newIDSet(), } } @@ -213,12 +208,6 @@ func (g *DirectedGraph) HasEdgeBetween(x, y graph.Node) bool { // Edge returns the edge from u to v if such an edge exists and nil otherwise. // The node v must be directly reachable from u as defined by the From method. func (g *DirectedGraph) Edge(u, v graph.Node) graph.Edge { - return g.WeightedEdge(u, v) -} - -// WeightedEdge returns the weighted edge from u to v if such an edge exists and nil otherwise. -// The node v must be directly reachable from u as defined by the From method. -func (g *DirectedGraph) WeightedEdge(u, v graph.Node) graph.WeightedEdge { if _, ok := g.nodes[u.ID()]; !ok { return nil } @@ -246,24 +235,6 @@ func (g *DirectedGraph) HasEdgeFromTo(u, v graph.Node) bool { return true } -// Weight returns the weight for the edge between x and y if Edge(x, y) returns a non-nil Edge. -// If x and y are the same node or there is no joining edge between the two nodes the weight -// value returned is either the graph's absent or self value. Weight returns true if an edge -// exists between x and y or if x and y have the same ID, false otherwise. -func (g *DirectedGraph) Weight(x, y graph.Node) (w float64, ok bool) { - xid := x.ID() - yid := y.ID() - if xid == yid { - return g.self, true - } - if to, ok := g.from[xid]; ok { - if e, ok := to[yid]; ok { - return e.Weight(), true - } - } - return g.absent, false -} - // Degree returns the in+out degree of n in g. func (g *DirectedGraph) Degree(n graph.Node) int { if _, ok := g.nodes[n.ID()]; !ok { diff --git a/graph/simple/directed_test.go b/graph/simple/directed_test.go index 488e90fb..b66845bd 100644 --- a/graph/simple/directed_test.go +++ b/graph/simple/directed_test.go @@ -5,7 +5,6 @@ package simple import ( - "math" "testing" "gonum.org/v1/gonum/graph" @@ -14,9 +13,8 @@ import ( var ( directedGraph = (*DirectedGraph)(nil) - _ graph.Graph = directedGraph - _ graph.Directed = directedGraph - _ graph.WeightedDirected = directedGraph + _ graph.Graph = directedGraph + _ graph.Directed = directedGraph ) // Tests Issue #27 @@ -36,10 +34,10 @@ func generateDummyGraph() *DirectedGraph { {0, 2}, } - g := NewDirectedGraph(0, math.Inf(1)) + g := NewDirectedGraph() for _, n := range nodes { - g.SetEdge(Edge{F: Node(n.srcID), T: Node(n.targetID), W: 1}) + g.SetEdge(Edge{F: Node(n.srcID), T: Node(n.targetID)}) } return g @@ -52,7 +50,7 @@ func TestIssue123DirectedGraph(t *testing.T) { t.Errorf("unexpected panic: %v", r) } }() - g := NewDirectedGraph(0, math.Inf(1)) + g := NewDirectedGraph() n0 := g.NewNode() g.AddNode(n0) diff --git a/graph/simple/undirected.go b/graph/simple/undirected.go index e3f1c214..7254e4a0 100644 --- a/graph/simple/undirected.go +++ b/graph/simple/undirected.go @@ -15,21 +15,16 @@ type UndirectedGraph struct { nodes map[int64]graph.Node edges map[int64]map[int64]graph.Edge - self, absent float64 - nodeIDs idSet } // NewUndirectedGraph returns an UndirectedGraph with the specified self and absent // edge weight values. -func NewUndirectedGraph(self, absent float64) *UndirectedGraph { +func NewUndirectedGraph() *UndirectedGraph { return &UndirectedGraph{ nodes: make(map[int64]graph.Node), edges: make(map[int64]map[int64]graph.Edge), - self: self, - absent: absent, - nodeIDs: newIDSet(), } } @@ -186,22 +181,11 @@ func (g *UndirectedGraph) HasEdgeBetween(x, y graph.Node) bool { // Edge returns the edge from u to v if such an edge exists and nil otherwise. // The node v must be directly reachable from u as defined by the From method. func (g *UndirectedGraph) Edge(u, v graph.Node) graph.Edge { - return g.WeightedEdgeBetween(u, v) -} - -// WeightedEdge returns the weighted edge from u to v if such an edge exists and nil otherwise. -// The node v must be directly reachable from u as defined by the From method. -func (g *UndirectedGraph) WeightedEdge(u, v graph.Node) graph.WeightedEdge { - return g.WeightedEdgeBetween(u, v) + return g.EdgeBetween(u, v) } // EdgeBetween returns the edge between nodes x and y. func (g *UndirectedGraph) EdgeBetween(x, y graph.Node) graph.Edge { - return g.WeightedEdgeBetween(x, y) -} - -// WeightedEdgeBetween returns the weighted edge between nodes x and y. -func (g *UndirectedGraph) WeightedEdgeBetween(x, y graph.Node) graph.WeightedEdge { // We don't need to check if neigh exists because // it's implicit in the edges access. if !g.Has(x) { @@ -211,24 +195,6 @@ func (g *UndirectedGraph) WeightedEdgeBetween(x, y graph.Node) graph.WeightedEdg return g.edges[x.ID()][y.ID()] } -// Weight returns the weight for the edge between x and y if Edge(x, y) returns a non-nil Edge. -// If x and y are the same node or there is no joining edge between the two nodes the weight -// value returned is either the graph's absent or self value. Weight returns true if an edge -// exists between x and y or if x and y have the same ID, false otherwise. -func (g *UndirectedGraph) Weight(x, y graph.Node) (w float64, ok bool) { - xid := x.ID() - yid := y.ID() - if xid == yid { - return g.self, true - } - if n, ok := g.edges[xid]; ok { - if e, ok := n[yid]; ok { - return e.Weight(), true - } - } - return g.absent, false -} - // Degree returns the degree of n in g. func (g *UndirectedGraph) Degree(n graph.Node) int { if _, ok := g.nodes[n.ID()]; !ok { diff --git a/graph/simple/undirected_test.go b/graph/simple/undirected_test.go index 73b9f2e6..f5c0dafc 100644 --- a/graph/simple/undirected_test.go +++ b/graph/simple/undirected_test.go @@ -5,7 +5,6 @@ package simple import ( - "math" "testing" "gonum.org/v1/gonum/graph" @@ -14,20 +13,19 @@ import ( var ( undirectedGraph = (*UndirectedGraph)(nil) - _ graph.Graph = undirectedGraph - _ graph.Undirected = undirectedGraph - _ graph.WeightedUndirected = undirectedGraph + _ graph.Graph = undirectedGraph + _ graph.Undirected = undirectedGraph ) func TestAssertMutableNotDirected(t *testing.T) { - var g graph.UndirectedBuilder = NewUndirectedGraph(0, math.Inf(1)) + var g graph.UndirectedBuilder = NewUndirectedGraph() if _, ok := g.(graph.Directed); ok { t.Fatal("Graph is directed, but a MutableGraph cannot safely be directed!") } } func TestMaxID(t *testing.T) { - g := NewUndirectedGraph(0, math.Inf(1)) + g := NewUndirectedGraph() nodes := make(map[graph.Node]struct{}) for i := Node(0); i < 3; i++ { g.AddNode(i) @@ -54,7 +52,7 @@ func TestIssue123UndirectedGraph(t *testing.T) { t.Errorf("unexpected panic: %v", r) } }() - g := NewUndirectedGraph(0, math.Inf(1)) + g := NewUndirectedGraph() n0 := g.NewNode() g.AddNode(n0) diff --git a/graph/simple/weighted_directed.go b/graph/simple/weighted_directed.go new file mode 100644 index 00000000..00f8b475 --- /dev/null +++ b/graph/simple/weighted_directed.go @@ -0,0 +1,285 @@ +// Copyright ©2014 The gonum Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package simple + +import ( + "fmt" + + "gonum.org/v1/gonum/graph" +) + +// WeightedDirectedGraph implements a generalized weighted directed graph. +type WeightedDirectedGraph struct { + nodes map[int64]graph.Node + from map[int64]map[int64]graph.WeightedEdge + to map[int64]map[int64]graph.WeightedEdge + + self, absent float64 + + nodeIDs idSet +} + +// NewWeightedDirectedGraph returns a WeightedDirectedGraph with the specified self and absent +// edge weight values. +func NewWeightedDirectedGraph(self, absent float64) *WeightedDirectedGraph { + return &WeightedDirectedGraph{ + nodes: make(map[int64]graph.Node), + from: make(map[int64]map[int64]graph.WeightedEdge), + to: make(map[int64]map[int64]graph.WeightedEdge), + + self: self, + absent: absent, + + nodeIDs: newIDSet(), + } +} + +// NewNode returns a new unique Node to be added to g. The Node's ID does +// not become valid in g until the Node is added to g. +func (g *WeightedDirectedGraph) NewNode() graph.Node { + if len(g.nodes) == 0 { + return Node(0) + } + if int64(len(g.nodes)) == maxInt { + panic("simple: cannot allocate node: no slot") + } + return Node(g.nodeIDs.newID()) +} + +// AddNode adds n to the graph. It panics if the added node ID matches an existing node ID. +func (g *WeightedDirectedGraph) AddNode(n graph.Node) { + if _, exists := g.nodes[n.ID()]; exists { + panic(fmt.Sprintf("simple: node ID collision: %d", n.ID())) + } + g.nodes[n.ID()] = n + g.from[n.ID()] = make(map[int64]graph.WeightedEdge) + g.to[n.ID()] = make(map[int64]graph.WeightedEdge) + g.nodeIDs.use(n.ID()) +} + +// RemoveNode removes n from the graph, as well as any edges attached to it. If the node +// is not in the graph it is a no-op. +func (g *WeightedDirectedGraph) RemoveNode(n graph.Node) { + if _, ok := g.nodes[n.ID()]; !ok { + return + } + delete(g.nodes, n.ID()) + + for from := range g.from[n.ID()] { + delete(g.to[from], n.ID()) + } + delete(g.from, n.ID()) + + for to := range g.to[n.ID()] { + delete(g.from[to], n.ID()) + } + delete(g.to, n.ID()) + + g.nodeIDs.release(n.ID()) +} + +// NewWeightedEdge returns a new weighted edge from the source to the destination node. +func (g *WeightedDirectedGraph) NewWeightedEdge(from, to graph.Node, weight float64) graph.WeightedEdge { + return &Edge{F: from, T: to, W: weight} +} + +// SetWeightedEdge adds a weighted edge from one node to another. If the nodes do not exist, they are added. +// It will panic if the IDs of the e.From and e.To are equal. +func (g *WeightedDirectedGraph) SetWeightedEdge(e graph.WeightedEdge) { + var ( + from = e.From() + fid = from.ID() + to = e.To() + tid = to.ID() + ) + + if fid == tid { + panic("simple: adding self edge") + } + + if !g.Has(from) { + g.AddNode(from) + } + if !g.Has(to) { + g.AddNode(to) + } + + g.from[fid][tid] = e + g.to[tid][fid] = e +} + +// RemoveEdge removes e from the graph, leaving the terminal nodes. If the edge does not exist +// it is a no-op. +func (g *WeightedDirectedGraph) RemoveEdge(e graph.Edge) { + from, to := e.From(), e.To() + if _, ok := g.nodes[from.ID()]; !ok { + return + } + if _, ok := g.nodes[to.ID()]; !ok { + return + } + + delete(g.from[from.ID()], to.ID()) + delete(g.to[to.ID()], from.ID()) +} + +// Node returns the node in the graph with the given ID. +func (g *WeightedDirectedGraph) Node(id int64) graph.Node { + return g.nodes[id] +} + +// Has returns whether the node exists within the graph. +func (g *WeightedDirectedGraph) Has(n graph.Node) bool { + _, ok := g.nodes[n.ID()] + + return ok +} + +// Nodes returns all the nodes in the graph. +func (g *WeightedDirectedGraph) Nodes() []graph.Node { + nodes := make([]graph.Node, len(g.from)) + i := 0 + for _, n := range g.nodes { + nodes[i] = n + i++ + } + + return nodes +} + +// Edges returns all the edges in the graph. +func (g *WeightedDirectedGraph) Edges() []graph.Edge { + var edges []graph.Edge + for _, u := range g.nodes { + for _, e := range g.from[u.ID()] { + edges = append(edges, e) + } + } + return edges +} + +// WeightedEdges returns all the weighted edges in the graph. +func (g *WeightedDirectedGraph) WeightedEdges() []graph.WeightedEdge { + var edges []graph.WeightedEdge + for _, u := range g.nodes { + for _, e := range g.from[u.ID()] { + edges = append(edges, e) + } + } + return edges +} + +// From returns all nodes in g that can be reached directly from n. +func (g *WeightedDirectedGraph) From(n graph.Node) []graph.Node { + if _, ok := g.from[n.ID()]; !ok { + return nil + } + + from := make([]graph.Node, len(g.from[n.ID()])) + i := 0 + for id := range g.from[n.ID()] { + from[i] = g.nodes[id] + i++ + } + + return from +} + +// To returns all nodes in g that can reach directly to n. +func (g *WeightedDirectedGraph) To(n graph.Node) []graph.Node { + if _, ok := g.from[n.ID()]; !ok { + return nil + } + + to := make([]graph.Node, len(g.to[n.ID()])) + i := 0 + for id := range g.to[n.ID()] { + to[i] = g.nodes[id] + i++ + } + + return to +} + +// HasEdgeBetween returns whether an edge exists between nodes x and y without +// considering direction. +func (g *WeightedDirectedGraph) HasEdgeBetween(x, y graph.Node) bool { + xid := x.ID() + yid := y.ID() + if _, ok := g.nodes[xid]; !ok { + return false + } + if _, ok := g.nodes[yid]; !ok { + return false + } + if _, ok := g.from[xid][yid]; ok { + return true + } + _, ok := g.from[yid][xid] + return ok +} + +// Edge returns the edge from u to v if such an edge exists and nil otherwise. +// The node v must be directly reachable from u as defined by the From method. +func (g *WeightedDirectedGraph) Edge(u, v graph.Node) graph.Edge { + return g.WeightedEdge(u, v) +} + +// WeightedEdge returns the weighted edge from u to v if such an edge exists and nil otherwise. +// The node v must be directly reachable from u as defined by the From method. +func (g *WeightedDirectedGraph) WeightedEdge(u, v graph.Node) graph.WeightedEdge { + if _, ok := g.nodes[u.ID()]; !ok { + return nil + } + if _, ok := g.nodes[v.ID()]; !ok { + return nil + } + edge, ok := g.from[u.ID()][v.ID()] + if !ok { + return nil + } + return edge +} + +// HasEdgeFromTo returns whether an edge exists in the graph from u to v. +func (g *WeightedDirectedGraph) HasEdgeFromTo(u, v graph.Node) bool { + if _, ok := g.nodes[u.ID()]; !ok { + return false + } + if _, ok := g.nodes[v.ID()]; !ok { + return false + } + if _, ok := g.from[u.ID()][v.ID()]; !ok { + return false + } + return true +} + +// Weight returns the weight for the edge between x and y if Edge(x, y) returns a non-nil Edge. +// If x and y are the same node or there is no joining edge between the two nodes the weight +// value returned is either the graph's absent or self value. Weight returns true if an edge +// exists between x and y or if x and y have the same ID, false otherwise. +func (g *WeightedDirectedGraph) Weight(x, y graph.Node) (w float64, ok bool) { + xid := x.ID() + yid := y.ID() + if xid == yid { + return g.self, true + } + if to, ok := g.from[xid]; ok { + if e, ok := to[yid]; ok { + return e.Weight(), true + } + } + return g.absent, false +} + +// Degree returns the in+out degree of n in g. +func (g *WeightedDirectedGraph) Degree(n graph.Node) int { + if _, ok := g.nodes[n.ID()]; !ok { + return 0 + } + + return len(g.from[n.ID()]) + len(g.to[n.ID()]) +} diff --git a/graph/simple/weighted_directed_test.go b/graph/simple/weighted_directed_test.go new file mode 100644 index 00000000..12c929ee --- /dev/null +++ b/graph/simple/weighted_directed_test.go @@ -0,0 +1,67 @@ +// Copyright ©2014 The gonum Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package simple + +import ( + "math" + "testing" + + "gonum.org/v1/gonum/graph" +) + +var ( + weightedDirectedGraph = (*WeightedDirectedGraph)(nil) + + _ graph.Graph = weightedDirectedGraph + _ graph.Directed = weightedDirectedGraph + _ graph.WeightedDirected = weightedDirectedGraph +) + +// Tests Issue #27 +func TestWeightedEdgeOvercounting(t *testing.T) { + g := generateDummyGraph() + + if neigh := g.From(Node(Node(2))); len(neigh) != 2 { + t.Errorf("Node 2 has incorrect number of neighbors got neighbors %v (count %d), expected 2 neighbors {0,1}", neigh, len(neigh)) + } +} + +func generateDummyWeightedGraph() *WeightedDirectedGraph { + nodes := [4]struct{ srcID, targetID int }{ + {2, 1}, + {1, 0}, + {2, 0}, + {0, 2}, + } + + g := NewWeightedDirectedGraph(0, math.Inf(1)) + + for _, n := range nodes { + g.SetWeightedEdge(Edge{F: Node(n.srcID), T: Node(n.targetID), W: 1}) + } + + return g +} + +// Test for issue #123 https://github.com/gonum/graph/issues/123 +func TestIssue123WeightedDirectedGraph(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("unexpected panic: %v", r) + } + }() + g := NewWeightedDirectedGraph(0, math.Inf(1)) + + n0 := g.NewNode() + g.AddNode(n0) + + n1 := g.NewNode() + g.AddNode(n1) + + g.RemoveNode(n0) + + n2 := g.NewNode() + g.AddNode(n2) +} diff --git a/graph/simple/weighted_undirected.go b/graph/simple/weighted_undirected.go new file mode 100644 index 00000000..c8b67551 --- /dev/null +++ b/graph/simple/weighted_undirected.go @@ -0,0 +1,260 @@ +// Copyright ©2014 The gonum Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package simple + +import ( + "fmt" + + "gonum.org/v1/gonum/graph" +) + +// WeightedUndirectedGraph implements a generalized weighted undirected graph. +type WeightedUndirectedGraph struct { + nodes map[int64]graph.Node + edges map[int64]map[int64]graph.WeightedEdge + + self, absent float64 + + nodeIDs idSet +} + +// NewWeightedUndirectedGraph returns an WeightedUndirectedGraph with the specified self and absent +// edge weight values. +func NewWeightedUndirectedGraph(self, absent float64) *WeightedUndirectedGraph { + return &WeightedUndirectedGraph{ + nodes: make(map[int64]graph.Node), + edges: make(map[int64]map[int64]graph.WeightedEdge), + + self: self, + absent: absent, + + nodeIDs: newIDSet(), + } +} + +// NewNode returns a new unique Node to be added to g. The Node's ID does +// not become valid in g until the Node is added to g. +func (g *WeightedUndirectedGraph) NewNode() graph.Node { + if len(g.nodes) == 0 { + return Node(0) + } + if int64(len(g.nodes)) == maxInt { + panic("simple: cannot allocate node: no slot") + } + return Node(g.nodeIDs.newID()) +} + +// AddNode adds n to the graph. It panics if the added node ID matches an existing node ID. +func (g *WeightedUndirectedGraph) AddNode(n graph.Node) { + if _, exists := g.nodes[n.ID()]; exists { + panic(fmt.Sprintf("simple: node ID collision: %d", n.ID())) + } + g.nodes[n.ID()] = n + g.edges[n.ID()] = make(map[int64]graph.WeightedEdge) + g.nodeIDs.use(n.ID()) +} + +// RemoveNode removes n from the graph, as well as any edges attached to it. If the node +// is not in the graph it is a no-op. +func (g *WeightedUndirectedGraph) RemoveNode(n graph.Node) { + if _, ok := g.nodes[n.ID()]; !ok { + return + } + delete(g.nodes, n.ID()) + + for from := range g.edges[n.ID()] { + delete(g.edges[from], n.ID()) + } + delete(g.edges, n.ID()) + + g.nodeIDs.release(n.ID()) +} + +// NewWeightedEdge returns a new weighted edge from the source to the destination node. +func (g *WeightedUndirectedGraph) NewWeightedEdge(from, to graph.Node, weight float64) graph.WeightedEdge { + return &Edge{F: from, T: to, W: weight} +} + +// SetWeightedEdge adds a weighted edge from one node to another. If the nodes do not exist, they are added. +// It will panic if the IDs of the e.From and e.To are equal. +func (g *WeightedUndirectedGraph) SetWeightedEdge(e graph.WeightedEdge) { + var ( + from = e.From() + fid = from.ID() + to = e.To() + tid = to.ID() + ) + + if fid == tid { + panic("simple: adding self edge") + } + + if !g.Has(from) { + g.AddNode(from) + } + if !g.Has(to) { + g.AddNode(to) + } + + g.edges[fid][tid] = e + g.edges[tid][fid] = e +} + +// RemoveEdge removes e from the graph, leaving the terminal nodes. If the edge does not exist +// it is a no-op. +func (g *WeightedUndirectedGraph) RemoveEdge(e graph.Edge) { + from, to := e.From(), e.To() + if _, ok := g.nodes[from.ID()]; !ok { + return + } + if _, ok := g.nodes[to.ID()]; !ok { + return + } + + delete(g.edges[from.ID()], to.ID()) + delete(g.edges[to.ID()], from.ID()) +} + +// Node returns the node in the graph with the given ID. +func (g *WeightedUndirectedGraph) Node(id int64) graph.Node { + return g.nodes[id] +} + +// Has returns whether the node exists within the graph. +func (g *WeightedUndirectedGraph) Has(n graph.Node) bool { + _, ok := g.nodes[n.ID()] + return ok +} + +// Nodes returns all the nodes in the graph. +func (g *WeightedUndirectedGraph) Nodes() []graph.Node { + nodes := make([]graph.Node, len(g.nodes)) + i := 0 + for _, n := range g.nodes { + nodes[i] = n + i++ + } + + return nodes +} + +// Edges returns all the edges in the graph. +func (g *WeightedUndirectedGraph) Edges() []graph.Edge { + var edges []graph.Edge + + seen := make(map[[2]int64]struct{}) + for _, u := range g.edges { + for _, e := range u { + uid := e.From().ID() + vid := e.To().ID() + if _, ok := seen[[2]int64{uid, vid}]; ok { + continue + } + seen[[2]int64{uid, vid}] = struct{}{} + seen[[2]int64{vid, uid}] = struct{}{} + edges = append(edges, e) + } + } + + return edges +} + +// WeightedEdges returns all the weighted edges in the graph. +func (g *WeightedUndirectedGraph) WeightedEdges() []graph.WeightedEdge { + var edges []graph.WeightedEdge + + seen := make(map[[2]int64]struct{}) + for _, u := range g.edges { + for _, e := range u { + uid := e.From().ID() + vid := e.To().ID() + if _, ok := seen[[2]int64{uid, vid}]; ok { + continue + } + seen[[2]int64{uid, vid}] = struct{}{} + seen[[2]int64{vid, uid}] = struct{}{} + edges = append(edges, e) + } + } + + return edges +} + +// From returns all nodes in g that can be reached directly from n. +func (g *WeightedUndirectedGraph) From(n graph.Node) []graph.Node { + if !g.Has(n) { + return nil + } + + nodes := make([]graph.Node, len(g.edges[n.ID()])) + i := 0 + for from := range g.edges[n.ID()] { + nodes[i] = g.nodes[from] + i++ + } + + return nodes +} + +// HasEdgeBetween returns whether an edge exists between nodes x and y. +func (g *WeightedUndirectedGraph) HasEdgeBetween(x, y graph.Node) bool { + _, ok := g.edges[x.ID()][y.ID()] + return ok +} + +// Edge returns the edge from u to v if such an edge exists and nil otherwise. +// The node v must be directly reachable from u as defined by the From method. +func (g *WeightedUndirectedGraph) Edge(u, v graph.Node) graph.Edge { + return g.WeightedEdgeBetween(u, v) +} + +// WeightedEdge returns the weighted edge from u to v if such an edge exists and nil otherwise. +// The node v must be directly reachable from u as defined by the From method. +func (g *WeightedUndirectedGraph) WeightedEdge(u, v graph.Node) graph.WeightedEdge { + return g.WeightedEdgeBetween(u, v) +} + +// EdgeBetween returns the edge between nodes x and y. +func (g *WeightedUndirectedGraph) EdgeBetween(x, y graph.Node) graph.Edge { + return g.WeightedEdgeBetween(x, y) +} + +// WeightedEdgeBetween returns the weighted edge between nodes x and y. +func (g *WeightedUndirectedGraph) WeightedEdgeBetween(x, y graph.Node) graph.WeightedEdge { + // We don't need to check if neigh exists because + // it's implicit in the edges access. + if !g.Has(x) { + return nil + } + + return g.edges[x.ID()][y.ID()] +} + +// Weight returns the weight for the edge between x and y if Edge(x, y) returns a non-nil Edge. +// If x and y are the same node or there is no joining edge between the two nodes the weight +// value returned is either the graph's absent or self value. Weight returns true if an edge +// exists between x and y or if x and y have the same ID, false otherwise. +func (g *WeightedUndirectedGraph) Weight(x, y graph.Node) (w float64, ok bool) { + xid := x.ID() + yid := y.ID() + if xid == yid { + return g.self, true + } + if n, ok := g.edges[xid]; ok { + if e, ok := n[yid]; ok { + return e.Weight(), true + } + } + return g.absent, false +} + +// Degree returns the degree of n in g. +func (g *WeightedUndirectedGraph) Degree(n graph.Node) int { + if _, ok := g.nodes[n.ID()]; !ok { + return 0 + } + + return len(g.edges[n.ID()]) +} diff --git a/graph/simple/weighted_undirected_test.go b/graph/simple/weighted_undirected_test.go new file mode 100644 index 00000000..a3796ee3 --- /dev/null +++ b/graph/simple/weighted_undirected_test.go @@ -0,0 +1,69 @@ +// Copyright ©2014 The gonum Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package simple + +import ( + "math" + "testing" + + "gonum.org/v1/gonum/graph" +) + +var ( + weightedUndirectedGraph = (*WeightedUndirectedGraph)(nil) + + _ graph.Graph = weightedUndirectedGraph + _ graph.Undirected = weightedUndirectedGraph + _ graph.WeightedUndirected = weightedUndirectedGraph +) + +func TestAssertWeightedMutableNotDirected(t *testing.T) { + var g graph.UndirectedWeightedBuilder = NewWeightedUndirectedGraph(0, math.Inf(1)) + if _, ok := g.(graph.Directed); ok { + t.Fatal("Graph is directed, but a MutableGraph cannot safely be directed!") + } +} + +func TestWeightedMaxID(t *testing.T) { + g := NewWeightedUndirectedGraph(0, math.Inf(1)) + nodes := make(map[graph.Node]struct{}) + for i := Node(0); i < 3; i++ { + g.AddNode(i) + nodes[i] = struct{}{} + } + g.RemoveNode(Node(0)) + delete(nodes, Node(0)) + g.RemoveNode(Node(2)) + delete(nodes, Node(2)) + n := g.NewNode() + g.AddNode(n) + if !g.Has(n) { + t.Error("added node does not exist in graph") + } + if _, exists := nodes[n]; exists { + t.Errorf("Created already existing node id: %v", n.ID()) + } +} + +// Test for issue #123 https://github.com/gonum/graph/issues/123 +func TestIssue123WeightedUndirectedGraph(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("unexpected panic: %v", r) + } + }() + g := NewWeightedUndirectedGraph(0, math.Inf(1)) + + n0 := g.NewNode() + g.AddNode(n0) + + n1 := g.NewNode() + g.AddNode(n1) + + g.RemoveNode(n0) + + n2 := g.NewNode() + g.AddNode(n2) +} diff --git a/graph/topo/bench_test.go b/graph/topo/bench_test.go index fb23f46f..7b2bdc3e 100644 --- a/graph/topo/bench_test.go +++ b/graph/topo/bench_test.go @@ -5,7 +5,6 @@ package topo import ( - "math" "testing" "gonum.org/v1/gonum/graph" @@ -23,7 +22,7 @@ var ( ) func gnpDirected(n int, p float64) graph.Directed { - g := simple.NewDirectedGraph(0, math.Inf(1)) + g := simple.NewDirectedGraph() gen.Gnp(g, n, p, nil) return g } diff --git a/graph/topo/bron_kerbosch_test.go b/graph/topo/bron_kerbosch_test.go index 875c96e9..8fdb67c0 100644 --- a/graph/topo/bron_kerbosch_test.go +++ b/graph/topo/bron_kerbosch_test.go @@ -5,7 +5,6 @@ package topo import ( - "math" "reflect" "sort" "testing" @@ -51,7 +50,7 @@ var vOrderTests = []struct { func TestVertexOrdering(t *testing.T) { for i, test := range vOrderTests { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewUndirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { @@ -136,7 +135,7 @@ var bronKerboschTests = []struct { func TestBronKerbosch(t *testing.T) { for i, test := range bronKerboschTests { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewUndirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { diff --git a/graph/topo/johnson_cycles_test.go b/graph/topo/johnson_cycles_test.go index 983a3834..bf53120d 100644 --- a/graph/topo/johnson_cycles_test.go +++ b/graph/topo/johnson_cycles_test.go @@ -5,7 +5,6 @@ package topo import ( - "math" "reflect" "sort" "testing" @@ -85,7 +84,7 @@ var cyclesInTests = []struct { func TestDirectedCyclesIn(t *testing.T) { for i, test := range cyclesInTests { - g := simple.NewDirectedGraph(0, math.Inf(1)) + g := simple.NewDirectedGraph() g.AddNode(simple.Node(-10)) // Make sure we test graphs with sparse IDs. for u, e := range test.g { // Add nodes that are not defined by an edge. diff --git a/graph/topo/paton_cycles_test.go b/graph/topo/paton_cycles_test.go index 556ae108..c36590be 100644 --- a/graph/topo/paton_cycles_test.go +++ b/graph/topo/paton_cycles_test.go @@ -5,7 +5,6 @@ package topo import ( - "math" "reflect" "sort" "testing" @@ -74,7 +73,7 @@ var undirectedCyclesInTests = []struct { func TestUndirectedCyclesIn(t *testing.T) { for i, test := range undirectedCyclesInTests { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewUndirectedGraph() g.AddNode(simple.Node(-10)) // Make sure we test graphs with sparse IDs. for u, e := range test.g { // Add nodes that are not defined by an edge. diff --git a/graph/topo/tarjan_test.go b/graph/topo/tarjan_test.go index 611103e7..8751c507 100644 --- a/graph/topo/tarjan_test.go +++ b/graph/topo/tarjan_test.go @@ -5,7 +5,6 @@ package topo import ( - "math" "reflect" "sort" "testing" @@ -130,7 +129,7 @@ var tarjanTests = []struct { func TestSort(t *testing.T) { for i, test := range tarjanTests { - g := simple.NewDirectedGraph(0, math.Inf(1)) + g := simple.NewDirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { @@ -161,7 +160,7 @@ func TestSort(t *testing.T) { func TestTarjanSCC(t *testing.T) { for i, test := range tarjanTests { - g := simple.NewDirectedGraph(0, math.Inf(1)) + g := simple.NewDirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { @@ -288,7 +287,7 @@ var stabilizedSortTests = []struct { func TestSortStabilized(t *testing.T) { for i, test := range stabilizedSortTests { - g := simple.NewDirectedGraph(0, math.Inf(1)) + g := simple.NewDirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { diff --git a/graph/topo/topo_test.go b/graph/topo/topo_test.go index d0e66b25..bb75796d 100644 --- a/graph/topo/topo_test.go +++ b/graph/topo/topo_test.go @@ -5,7 +5,6 @@ package topo import ( - "math" "reflect" "sort" "testing" @@ -16,7 +15,7 @@ import ( ) func TestIsPath(t *testing.T) { - dg := simple.NewDirectedGraph(0, math.Inf(1)) + dg := simple.NewDirectedGraph() if !IsPathIn(dg, nil) { t.Error("IsPath returns false on nil path") } @@ -33,7 +32,7 @@ func TestIsPath(t *testing.T) { if IsPathIn(dg, p) { t.Error("IsPath returns true on bad path of length 2") } - dg.SetEdge(simple.Edge{F: p[0], T: p[1], W: 1}) + dg.SetEdge(simple.Edge{F: p[0], T: p[1]}) if !IsPathIn(dg, p) { t.Error("IsPath returns false on correct path of length 2") } @@ -42,13 +41,13 @@ func TestIsPath(t *testing.T) { t.Error("IsPath erroneously returns true for a reverse path") } p = []graph.Node{p[1], p[0], simple.Node(2)} - dg.SetEdge(simple.Edge{F: p[1], T: p[2], W: 1}) + dg.SetEdge(simple.Edge{F: p[1], T: p[2]}) if !IsPathIn(dg, p) { t.Error("IsPath does not find a correct path for path > 2 nodes") } - ug := simple.NewUndirectedGraph(0, math.Inf(1)) - ug.SetEdge(simple.Edge{F: p[1], T: p[0], W: 1}) - ug.SetEdge(simple.Edge{F: p[1], T: p[2], W: 1}) + ug := simple.NewUndirectedGraph() + ug.SetEdge(simple.Edge{F: p[1], T: p[0]}) + ug.SetEdge(simple.Edge{F: p[1], T: p[2]}) if !IsPathIn(dg, p) { t.Error("IsPath does not correctly account for undirected behavior") } @@ -69,7 +68,7 @@ var pathExistsInUndirectedTests = []struct { func TestPathExistsInUndirected(t *testing.T) { for i, test := range pathExistsInUndirectedTests { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewUndirectedGraph() for u, e := range test.g { if !g.Has(simple.Node(u)) { @@ -108,7 +107,7 @@ var pathExistsInDirectedTests = []struct { func TestPathExistsInDirected(t *testing.T) { for i, test := range pathExistsInDirectedTests { - g := simple.NewDirectedGraph(0, math.Inf(1)) + g := simple.NewDirectedGraph() for u, e := range test.g { if !g.Has(simple.Node(u)) { @@ -145,7 +144,7 @@ var connectedComponentTests = []struct { func TestConnectedComponents(t *testing.T) { for i, test := range connectedComponentTests { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewUndirectedGraph() for u, e := range test.g { if !g.Has(simple.Node(u)) { diff --git a/graph/traverse/traverse_test.go b/graph/traverse/traverse_test.go index 8aec716d..ad817352 100644 --- a/graph/traverse/traverse_test.go +++ b/graph/traverse/traverse_test.go @@ -6,7 +6,6 @@ package traverse import ( "fmt" - "math" "reflect" "sort" "testing" @@ -134,7 +133,7 @@ var breadthFirstTests = []struct { func TestBreadthFirst(t *testing.T) { for i, test := range breadthFirstTests { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewUndirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { @@ -222,7 +221,7 @@ var depthFirstTests = []struct { func TestDepthFirst(t *testing.T) { for i, test := range depthFirstTests { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewUndirectedGraph() for u, e := range test.g { // Add nodes that are not defined by an edge. if !g.Has(simple.Node(u)) { @@ -283,7 +282,7 @@ var walkAllTests = []struct { func TestWalkAll(t *testing.T) { for i, test := range walkAllTests { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewUndirectedGraph() for u, e := range test.g { if !g.Has(simple.Node(u)) { @@ -366,7 +365,7 @@ var ( ) func gnpUndirected(n int, p float64) graph.Undirected { - g := simple.NewUndirectedGraph(0, math.Inf(1)) + g := simple.NewUndirectedGraph() gen.Gnp(g, n, p, nil) return g } diff --git a/graph/undirect.go b/graph/undirect.go index fdb4b27f..e045dad4 100644 --- a/graph/undirect.go +++ b/graph/undirect.go @@ -4,36 +4,12 @@ package graph -// Undirect converts a directed graph to an undirected graph, resolving -// edge weight conflicts. +// Undirect converts a directed graph to an undirected graph. type Undirect struct { G Directed - - // Absent is the value used to - // represent absent edge weights - // passed to Merge if the reverse - // edge is present. - Absent float64 - - // Merge defines how discordant edge - // weights in G are resolved. A merge - // is performed if at least one edge - // exists between the nodes being - // considered. The edges corresponding - // to the two weights are also passed, - // in the same order. - // The order of weight parameters - // passed to Merge is not defined, so - // the function should be commutative. - // If Merge is nil, the arithmetic - // mean is used to merge weights. - Merge func(x, y float64, xe, ye Edge) float64 } -var ( - _ Undirected = Undirect{} - _ WeightedUndirected = Undirect{} -) +var _ Undirected = Undirect{} // Has returns whether the node exists within the graph. func (g Undirect) Has(n Node) bool { return g.G.Has(n) } @@ -68,51 +44,118 @@ func (g Undirect) HasEdgeBetween(x, y Node) bool { return g.G.HasEdgeBetween(x, // If an edge exists, the Edge returned is an EdgePair. The weight of // the edge is determined by applying the Merge func to the weights of the // edges between u and v. -func (g Undirect) Edge(u, v Node) Edge { return g.WeightedEdgeBetween(u, v) } - -// WeightedEdge returns the weighted edge from u to v if such an edge exists and nil otherwise. -// The node v must be directly reachable from u as defined by the From method. -// If an edge exists, the Edge returned is an EdgePair. The weight of -// the edge is determined by applying the Merge func to the weights of the -// edges between u and v. -func (g Undirect) WeightedEdge(u, v Node) WeightedEdge { return g.WeightedEdgeBetween(u, v) } +func (g Undirect) Edge(u, v Node) Edge { return g.EdgeBetween(u, v) } // EdgeBetween returns the edge between nodes x and y. If an edge exists, the // Edge returned is an EdgePair. The weight of the edge is determined by // applying the Merge func to the weights of edges between x and y. func (g Undirect) EdgeBetween(x, y Node) Edge { - return g.WeightedEdgeBetween(x, y) -} - -// WeightedEdgeBetween returns the weighted edge between nodes x and y. If an edge exists, the -// Edge returned is an EdgePair. The weight of the edge is determined by -// applying the Merge func to the weights of edges between x and y. -func (g Undirect) WeightedEdgeBetween(x, y Node) WeightedEdge { fe := g.G.Edge(x, y) re := g.G.Edge(y, x) if fe == nil && re == nil { return nil } - var f, r float64 - if wg, ok := g.G.(WeightedDirected); ok { - f, ok = wg.Weight(x, y) - if !ok { - f = g.Absent + return EdgePair{fe, re} +} + +// UndirectWeighted converts a directed weighted graph to an undirected weighted graph, +// resolving edge weight conflicts. +type UndirectWeighted struct { + G WeightedDirected + + // Absent is the value used to + // represent absent edge weights + // passed to Merge if the reverse + // edge is present. + Absent float64 + + // Merge defines how discordant edge + // weights in G are resolved. A merge + // is performed if at least one edge + // exists between the nodes being + // considered. The edges corresponding + // to the two weights are also passed, + // in the same order. + // The order of weight parameters + // passed to Merge is not defined, so + // the function should be commutative. + // If Merge is nil, the arithmetic + // mean is used to merge weights. + Merge func(x, y float64, xe, ye Edge) float64 +} + +var ( + _ Undirected = UndirectWeighted{} + _ WeightedUndirected = UndirectWeighted{} +) + +// Has returns whether the node exists within the graph. +func (g UndirectWeighted) Has(n Node) bool { return g.G.Has(n) } + +// Nodes returns all the nodes in the graph. +func (g UndirectWeighted) Nodes() []Node { return g.G.Nodes() } + +// From returns all nodes in g that can be reached directly from u. +func (g UndirectWeighted) From(u Node) []Node { + var nodes []Node + seen := make(map[int64]struct{}) + for _, n := range g.G.From(u) { + seen[n.ID()] = struct{}{} + nodes = append(nodes, n) + } + for _, n := range g.G.To(u) { + id := n.ID() + if _, ok := seen[id]; ok { + continue } - r, ok = wg.Weight(y, x) - if !ok { - r = g.Absent - } - } else { + seen[n.ID()] = struct{}{} + nodes = append(nodes, n) + } + return nodes +} + +// HasEdgeBetween returns whether an edge exists between nodes x and y. +func (g UndirectWeighted) HasEdgeBetween(x, y Node) bool { return g.G.HasEdgeBetween(x, y) } + +// Edge returns the edge from u to v if such an edge exists and nil otherwise. +// The node v must be directly reachable from u as defined by the From method. +// If an edge exists, the Edge returned is an EdgePair. The weight of +// the edge is determined by applying the Merge func to the weights of the +// edges between u and v. +func (g UndirectWeighted) Edge(u, v Node) Edge { return g.WeightedEdgeBetween(u, v) } + +// WeightedEdge returns the weighted edge from u to v if such an edge exists and nil otherwise. +// The node v must be directly reachable from u as defined by the From method. +// If an edge exists, the Edge returned is an EdgePair. The weight of +// the edge is determined by applying the Merge func to the weights of the +// edges between u and v. +func (g UndirectWeighted) WeightedEdge(u, v Node) WeightedEdge { return g.WeightedEdgeBetween(u, v) } + +// EdgeBetween returns the edge between nodes x and y. If an edge exists, the +// Edge returned is an EdgePair. The weight of the edge is determined by +// applying the Merge func to the weights of edges between x and y. +func (g UndirectWeighted) EdgeBetween(x, y Node) Edge { + return g.WeightedEdgeBetween(x, y) +} + +// WeightedEdgeBetween returns the weighted edge between nodes x and y. If an edge exists, the +// Edge returned is an EdgePair. The weight of the edge is determined by +// applying the Merge func to the weights of edges between x and y. +func (g UndirectWeighted) WeightedEdgeBetween(x, y Node) WeightedEdge { + fe := g.G.Edge(x, y) + re := g.G.Edge(y, x) + if fe == nil && re == nil { + return nil + } + + f, ok := g.G.Weight(x, y) + if !ok { f = g.Absent - if fe != nil { - f = fe.Weight() - } + } + r, ok := g.G.Weight(y, x) + if !ok { r = g.Absent - if re != nil { - r = re.Weight() - } } var w float64 @@ -121,41 +164,26 @@ func (g Undirect) WeightedEdgeBetween(x, y Node) WeightedEdge { } else { w = g.Merge(f, r, fe, re) } - return EdgePair{E: [2]Edge{fe, re}, W: w} + return WeightedEdgePair{EdgePair: [2]Edge{fe, re}, W: w} } // Weight returns the weight for the edge between x and y if Edge(x, y) returns a non-nil Edge. // If x and y are the same node the internal node weight is returned. If there is no joining // edge between the two nodes the weight value returned is zero. Weight returns true if an edge // exists between x and y or if x and y have the same ID, false otherwise. -func (g Undirect) Weight(x, y Node) (w float64, ok bool) { +func (g UndirectWeighted) Weight(x, y Node) (w float64, ok bool) { fe := g.G.Edge(x, y) re := g.G.Edge(y, x) - var f, r float64 - if wg, wOk := g.G.(WeightedDirected); wOk { - var fOk, rOK bool - f, fOk = wg.Weight(x, y) - if !fOk { - f = g.Absent - } - r, rOK = wg.Weight(y, x) - if !rOK { - r = g.Absent - } - ok = fOk || rOK - } else { + f, fOk := g.G.Weight(x, y) + if !fOk { f = g.Absent - if fe != nil { - f = fe.Weight() - ok = true - } - r = g.Absent - if re != nil { - r = re.Weight() - ok = true - } } + r, rOK := g.G.Weight(y, x) + if !rOK { + r = g.Absent + } + ok = fOk || rOK if g.Merge == nil { return (f + r) / 2, ok @@ -164,30 +192,33 @@ func (g Undirect) Weight(x, y Node) (w float64, ok bool) { } // EdgePair is an opposed pair of directed edges. -type EdgePair struct { - E [2]Edge - W float64 -} +type EdgePair [2]Edge // From returns the from node of the first non-nil edge, or nil. func (e EdgePair) From() Node { - if e.E[0] != nil { - return e.E[0].From() - } else if e.E[1] != nil { - return e.E[1].From() + if e[0] != nil { + return e[0].From() + } else if e[1] != nil { + return e[1].From() } return nil } // To returns the to node of the first non-nil edge, or nil. func (e EdgePair) To() Node { - if e.E[0] != nil { - return e.E[0].To() - } else if e.E[1] != nil { - return e.E[1].To() + if e[0] != nil { + return e[0].To() + } else if e[1] != nil { + return e[1].To() } return nil } +// WeightedEdgePair is an opposed pair of directed edges. +type WeightedEdgePair struct { + EdgePair + W float64 +} + // Weight returns the merged edge weights of the two edges. -func (e EdgePair) Weight() float64 { return e.W } +func (e WeightedEdgePair) Weight() float64 { return e.W } diff --git a/graph/undirect_test.go b/graph/undirect_test.go index f5530608..d9ed6a05 100644 --- a/graph/undirect_test.go +++ b/graph/undirect_test.go @@ -13,8 +13,15 @@ import ( "gonum.org/v1/gonum/mat" ) -var directedGraphs = []struct { - g func() graph.DirectedBuilder +type weightedDirectedBuilder interface { + graph.WeightedBuilder + graph.WeightedDirected +} + +var weightedDirectedGraphs = []struct { + skipUnweighted bool + + g func() weightedDirectedBuilder edges []simple.Edge absent float64 merge func(x, y float64, xe, ye graph.Edge) float64 @@ -22,7 +29,7 @@ var directedGraphs = []struct { want mat.Matrix }{ { - g: func() graph.DirectedBuilder { return simple.NewDirectedGraph(0, 0) }, + g: func() weightedDirectedBuilder { return simple.NewWeightedDirectedGraph(0, 0) }, edges: []simple.Edge{ {F: simple.Node(0), T: simple.Node(1), W: 2}, {F: simple.Node(1), T: simple.Node(0), W: 1}, @@ -35,7 +42,7 @@ var directedGraphs = []struct { }), }, { - g: func() graph.DirectedBuilder { return simple.NewDirectedGraph(0, 0) }, + g: func() weightedDirectedBuilder { return simple.NewWeightedDirectedGraph(0, 0) }, edges: []simple.Edge{ {F: simple.Node(0), T: simple.Node(1), W: 2}, {F: simple.Node(1), T: simple.Node(0), W: 1}, @@ -50,7 +57,9 @@ var directedGraphs = []struct { }), }, { - g: func() graph.DirectedBuilder { return simple.NewDirectedGraph(0, 0) }, + skipUnweighted: true, // The min merge function cannot be used in the unweighted case. + + g: func() weightedDirectedBuilder { return simple.NewWeightedDirectedGraph(0, 0) }, edges: []simple.Edge{ {F: simple.Node(0), T: simple.Node(1), W: 2}, {F: simple.Node(1), T: simple.Node(0), W: 1}, @@ -64,7 +73,7 @@ var directedGraphs = []struct { }), }, { - g: func() graph.DirectedBuilder { return simple.NewDirectedGraph(0, 0) }, + g: func() weightedDirectedBuilder { return simple.NewWeightedDirectedGraph(0, 0) }, edges: []simple.Edge{ {F: simple.Node(0), T: simple.Node(1), W: 2}, {F: simple.Node(1), T: simple.Node(0), W: 1}, @@ -86,7 +95,7 @@ var directedGraphs = []struct { }), }, { - g: func() graph.DirectedBuilder { return simple.NewDirectedGraph(0, 0) }, + g: func() weightedDirectedBuilder { return simple.NewWeightedDirectedGraph(0, 0) }, edges: []simple.Edge{ {F: simple.Node(0), T: simple.Node(1), W: 2}, {F: simple.Node(1), T: simple.Node(0), W: 1}, @@ -102,13 +111,16 @@ var directedGraphs = []struct { } func TestUndirect(t *testing.T) { - for _, test := range directedGraphs { + for i, test := range weightedDirectedGraphs { + if test.skipUnweighted { + continue + } g := test.g() for _, e := range test.edges { - g.SetEdge(e) + g.SetWeightedEdge(e) } - src := graph.Undirect{G: g, Absent: test.absent, Merge: test.merge} + src := graph.Undirect{G: g} dst := simple.NewUndirectedMatrixFrom(src.Nodes(), 0, 0, 0) for _, u := range src.Nodes() { for _, v := range src.From(u) { @@ -116,11 +128,48 @@ func TestUndirect(t *testing.T) { } } + want := unit{test.want} + if !mat.Equal(dst.Matrix(), want) { + t.Errorf("unexpected result for case %d:\ngot:\n%.4v\nwant:\n%.4v", i, + mat.Formatted(dst.Matrix()), + mat.Formatted(want), + ) + } + } +} + +func TestUndirectWeighted(t *testing.T) { + for i, test := range weightedDirectedGraphs { + g := test.g() + for _, e := range test.edges { + g.SetWeightedEdge(e) + } + + src := graph.UndirectWeighted{G: g, Absent: test.absent, Merge: test.merge} + dst := simple.NewUndirectedMatrixFrom(src.Nodes(), 0, 0, 0) + for _, u := range src.Nodes() { + for _, v := range src.From(u) { + dst.SetWeightedEdge(src.WeightedEdge(u, v)) + } + } + if !mat.Equal(dst.Matrix(), test.want) { - t.Errorf("unexpected result:\ngot:\n%.4v\nwant:\n%.4v", + t.Errorf("unexpected result for case %d:\ngot:\n%.4v\nwant:\n%.4v", i, mat.Formatted(dst.Matrix()), mat.Formatted(test.want), ) } } } + +type unit struct { + mat.Matrix +} + +func (m unit) At(i, j int) float64 { + v := m.Matrix.At(i, j) + if v == 0 { + return 0 + } + return 1 +}