spectral: new package for spectral graph analysis
Initial commit to this is just a move of the Laplacian type from network.
diff --git a/graph/network/diffusion.go b/graph/network/diffusion.go
index 6aec9c0..b9a99de 100644
--- a/graph/network/diffusion.go
+++ b/graph/network/diffusion.go
@@ -5,9 +5,7 @@
package network
import (
- "math"
-
- "gonum.org/v1/gonum/graph"
+ "gonum.org/v1/gonum/graph/spectral"
"gonum.org/v1/gonum/mat"
)
@@ -23,7 +21,7 @@
// Nodes without corresponding entries in h are given an initial heat of zero,
// and entries in h without a corresponding node in the original graph are
// not altered when written to dst.
-func Diffuse(dst, h map[int64]float64, by Laplacian, t float64) map[int64]float64 {
+func Diffuse(dst, h map[int64]float64, by spectral.Laplacian, t float64) map[int64]float64 {
heat := make([]float64, len(by.Index))
for id, i := range by.Index {
heat[i] = h[id]
@@ -58,7 +56,7 @@
// Nodes without corresponding entries in h are given an initial heat of zero,
// and entries in h without a corresponding node in the original graph are
// not altered when written to dst.
-func DiffuseToEquilibrium(dst, h map[int64]float64, by Laplacian, tol float64, iters int) (eq map[int64]float64, ok bool) {
+func DiffuseToEquilibrium(dst, h map[int64]float64, by spectral.Laplacian, tol float64, iters int) (eq map[int64]float64, ok bool) {
heat := make([]float64, len(by.Index))
for id, i := range by.Index {
heat[i] = h[id]
@@ -94,119 +92,3 @@
}
return dst, ok
}
-
-// Laplacian is a graph Laplacian matrix.
-type Laplacian struct {
- // Matrix holds the Laplacian matrix.
- mat.Matrix
-
- // Nodes holds the input graph nodes.
- Nodes []graph.Node
-
- // Index is a mapping from the graph
- // node IDs to row and column indices.
- Index map[int64]int
-}
-
-// NewLaplacian returns a Laplacian matrix for the simple undirected graph g.
-// The Laplacian is defined as D-A where D is a diagonal matrix holding the
-// degree of each node and A is the graph adjacency matrix of the input graph.
-// If g contains self edges, NewLaplacian will panic.
-func NewLaplacian(g graph.Undirected) Laplacian {
- nodes := graph.NodesOf(g.Nodes())
- indexOf := make(map[int64]int, len(nodes))
- for i, n := range nodes {
- id := n.ID()
- indexOf[id] = i
- }
-
- l := mat.NewSymDense(len(nodes), nil)
- for j, u := range nodes {
- uid := u.ID()
- to := graph.NodesOf(g.From(uid))
- l.SetSym(j, j, float64(len(to)))
- for _, v := range to {
- vid := v.ID()
- if uid == vid {
- panic("network: self edge in graph")
- }
- if uid < vid {
- l.SetSym(indexOf[vid], j, -1)
- }
- }
- }
-
- return Laplacian{Matrix: l, Nodes: nodes, Index: indexOf}
-}
-
-// NewSymNormLaplacian returns a symmetric normalized Laplacian matrix for the
-// simple undirected graph g.
-// The normalized Laplacian is defined as I-D^(-1/2)AD^(-1/2) where D is a
-// diagonal matrix holding the degree of each node and A is the graph adjacency
-// matrix of the input graph.
-// If g contains self edges, NewSymNormLaplacian will panic.
-func NewSymNormLaplacian(g graph.Undirected) Laplacian {
- nodes := graph.NodesOf(g.Nodes())
- indexOf := make(map[int64]int, len(nodes))
- for i, n := range nodes {
- id := n.ID()
- indexOf[id] = i
- }
-
- l := mat.NewSymDense(len(nodes), nil)
- for j, u := range nodes {
- uid := u.ID()
- to := graph.NodesOf(g.From(uid))
- if len(to) == 0 {
- continue
- }
- l.SetSym(j, j, 1)
- squdeg := math.Sqrt(float64(len(to)))
- for _, v := range to {
- vid := v.ID()
- if uid == vid {
- panic("network: self edge in graph")
- }
- if uid < vid {
- l.SetSym(indexOf[vid], j, -1/(squdeg*math.Sqrt(float64(g.From(vid).Len()))))
- }
- }
- }
-
- return Laplacian{Matrix: l, Nodes: nodes, Index: indexOf}
-}
-
-// NewRandomWalkLaplacian returns a damp-scaled random walk Laplacian matrix for
-// the simple graph g.
-// The random walk Laplacian is defined as I-D^(-1)A where D is a diagonal matrix
-// holding the degree of each node and A is the graph adjacency matrix of the input
-// graph.
-// If g contains self edges, NewRandomWalkLaplacian will panic.
-func NewRandomWalkLaplacian(g graph.Graph, damp float64) Laplacian {
- nodes := graph.NodesOf(g.Nodes())
- indexOf := make(map[int64]int, len(nodes))
- for i, n := range nodes {
- id := n.ID()
- indexOf[id] = i
- }
-
- l := mat.NewDense(len(nodes), len(nodes), nil)
- for j, u := range nodes {
- uid := u.ID()
- to := graph.NodesOf(g.From(uid))
- if len(to) == 0 {
- continue
- }
- l.Set(j, j, 1-damp)
- rudeg := (damp - 1) / float64(len(to))
- for _, v := range to {
- vid := v.ID()
- if uid == vid {
- panic("network: self edge in graph")
- }
- l.Set(indexOf[vid], j, rudeg)
- }
- }
-
- return Laplacian{Matrix: l, Nodes: nodes, Index: indexOf}
-}
diff --git a/graph/network/diffusion_test.go b/graph/network/diffusion_test.go
index a3f6b05..1deac06 100644
--- a/graph/network/diffusion_test.go
+++ b/graph/network/diffusion_test.go
@@ -6,15 +6,12 @@
import (
"math"
- "sort"
"testing"
"gonum.org/v1/gonum/floats"
"gonum.org/v1/gonum/graph"
- "gonum.org/v1/gonum/graph/internal/ordered"
- "gonum.org/v1/gonum/graph/iterator"
"gonum.org/v1/gonum/graph/simple"
- "gonum.org/v1/gonum/mat"
+ "gonum.org/v1/gonum/graph/spectral"
)
var diffuseTests = []struct {
@@ -160,7 +157,7 @@
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
}
}
- for j, lfn := range []func(g graph.Undirected) Laplacian{NewLaplacian, NewSymNormLaplacian} {
+ for j, lfn := range []func(g graph.Undirected) spectral.Laplacian{spectral.NewLaplacian, spectral.NewSymNormLaplacian} {
normalize := j == 1
var wantTemp float64
for _, v := range test.h {
@@ -194,123 +191,6 @@
}
}
-var randomWalkLaplacianTests = []struct {
- g []set
- damp float64
-
- want *mat.Dense
-}{
- {
- g: []set{
- A: linksTo(B, C),
- B: linksTo(C),
- C: nil,
- },
-
- want: mat.NewDense(3, 3, []float64{
- 1, 0, 0,
- -0.5, 1, 0,
- -0.5, -1, 0,
- }),
- },
- {
- g: []set{
- A: linksTo(B, C),
- B: linksTo(C),
- C: nil,
- },
- damp: 0.85,
-
- want: mat.NewDense(3, 3, []float64{
- 0.15, 0, 0,
- -0.075, 0.15, 0,
- -0.075, -0.15, 0,
- }),
- },
- {
- g: []set{
- A: linksTo(B),
- B: linksTo(C),
- C: linksTo(A),
- },
- damp: 0.85,
-
- want: mat.NewDense(3, 3, []float64{
- 0.15, 0, -0.15,
- -0.15, 0.15, 0,
- 0, -0.15, 0.15,
- }),
- },
- {
- // Example graph from http://en.wikipedia.org/wiki/File:PageRanks-Example.svg 16:17, 8 July 2009
- g: []set{
- A: nil,
- B: linksTo(C),
- C: linksTo(B),
- D: linksTo(A, B),
- E: linksTo(D, B, F),
- F: linksTo(B, E),
- G: linksTo(B, E),
- H: linksTo(B, E),
- I: linksTo(B, E),
- J: linksTo(E),
- K: linksTo(E),
- },
-
- want: mat.NewDense(11, 11, []float64{
- 0, 0, 0, -0.5, 0, 0, 0, 0, 0, 0, 0,
- 0, 1, -1, -0.5, -1. / 3., -0.5, -0.5, -0.5, -0.5, 0, 0,
- 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 1, -1. / 3., 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 1, -0.5, -0.5, -0.5, -0.5, -1, -1,
- 0, 0, 0, 0, -1. / 3., 1, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
- }),
- },
-}
-
-func TestRandomWalkLaplacian(t *testing.T) {
- const tol = 1e-14
- for i, test := range randomWalkLaplacianTests {
- g := simple.NewDirectedGraph()
- for u, e := range test.g {
- // Add nodes that are not defined by an edge.
- if g.Node(int64(u)) == nil {
- g.AddNode(simple.Node(u))
- }
- for v := range e {
- g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
- }
- }
- l := NewRandomWalkLaplacian(g, test.damp)
- _, c := l.Dims()
- for j := 0; j < c; j++ {
- if got := mat.Sum(l.Matrix.(*mat.Dense).ColView(j)); !floats.EqualWithinAbsOrRel(got, 0, tol, tol) {
- t.Errorf("unexpected column sum for test %d, column %d: got:%v want:0", i, j, got)
- }
- }
- l = NewRandomWalkLaplacian(sortedNodeGraph{g}, test.damp)
- if !mat.EqualApprox(l, test.want, tol) {
- t.Errorf("unexpected result for test %d:\ngot:\n% .2v\nwant:\n% .2v",
- i, mat.Formatted(l), mat.Formatted(test.want))
- }
- }
-}
-
-type sortedNodeGraph struct {
- graph.Graph
-}
-
-func (g sortedNodeGraph) Nodes() graph.Nodes {
- n := graph.NodesOf(g.Graph.Nodes())
- sort.Sort(ordered.ByID(n))
- return iterator.NewOrderedNodes(n)
-}
-
var diffuseToEquilibriumTests = []struct {
g []set
builder builder
@@ -478,7 +358,7 @@
for _, v := range test.h {
wantTemp += v
}
- got, ok := DiffuseToEquilibrium(nil, test.h, NewRandomWalkLaplacian(g, test.damp), test.tol*test.tol, test.iter)
+ got, ok := DiffuseToEquilibrium(nil, test.h, spectral.NewRandomWalkLaplacian(g, test.damp), test.tol*test.tol, test.iter)
if ok != test.wantOK {
t.Errorf("unexpected success value for test %d: got:%t want:%t", i, ok, test.wantOK)
}
diff --git a/graph/spectral/doc.go b/graph/spectral/doc.go
new file mode 100644
index 0000000..b26425f
--- /dev/null
+++ b/graph/spectral/doc.go
@@ -0,0 +1,6 @@
+// Copyright ©2019 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 spectral provides graph spectral analysis functions.
+package spectral // import "gonum.org/v1/gonum/graph/spectral"
diff --git a/graph/spectral/laplacian.go b/graph/spectral/laplacian.go
new file mode 100644
index 0000000..0c3bd41
--- /dev/null
+++ b/graph/spectral/laplacian.go
@@ -0,0 +1,128 @@
+// Copyright ©2017 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 spectral
+
+import (
+ "math"
+
+ "gonum.org/v1/gonum/graph"
+ "gonum.org/v1/gonum/mat"
+)
+
+// Laplacian is a graph Laplacian matrix.
+type Laplacian struct {
+ // Matrix holds the Laplacian matrix.
+ mat.Matrix
+
+ // Nodes holds the input graph nodes.
+ Nodes []graph.Node
+
+ // Index is a mapping from the graph
+ // node IDs to row and column indices.
+ Index map[int64]int
+}
+
+// NewLaplacian returns a Laplacian matrix for the simple undirected graph g.
+// The Laplacian is defined as D-A where D is a diagonal matrix holding the
+// degree of each node and A is the graph adjacency matrix of the input graph.
+// If g contains self edges, NewLaplacian will panic.
+func NewLaplacian(g graph.Undirected) Laplacian {
+ nodes := graph.NodesOf(g.Nodes())
+ indexOf := make(map[int64]int, len(nodes))
+ for i, n := range nodes {
+ id := n.ID()
+ indexOf[id] = i
+ }
+
+ l := mat.NewSymDense(len(nodes), nil)
+ for j, u := range nodes {
+ uid := u.ID()
+ to := graph.NodesOf(g.From(uid))
+ l.SetSym(j, j, float64(len(to)))
+ for _, v := range to {
+ vid := v.ID()
+ if uid == vid {
+ panic("network: self edge in graph")
+ }
+ if uid < vid {
+ l.SetSym(indexOf[vid], j, -1)
+ }
+ }
+ }
+
+ return Laplacian{Matrix: l, Nodes: nodes, Index: indexOf}
+}
+
+// NewSymNormLaplacian returns a symmetric normalized Laplacian matrix for the
+// simple undirected graph g.
+// The normalized Laplacian is defined as I-D^(-1/2)AD^(-1/2) where D is a
+// diagonal matrix holding the degree of each node and A is the graph adjacency
+// matrix of the input graph.
+// If g contains self edges, NewSymNormLaplacian will panic.
+func NewSymNormLaplacian(g graph.Undirected) Laplacian {
+ nodes := graph.NodesOf(g.Nodes())
+ indexOf := make(map[int64]int, len(nodes))
+ for i, n := range nodes {
+ id := n.ID()
+ indexOf[id] = i
+ }
+
+ l := mat.NewSymDense(len(nodes), nil)
+ for j, u := range nodes {
+ uid := u.ID()
+ to := graph.NodesOf(g.From(uid))
+ if len(to) == 0 {
+ continue
+ }
+ l.SetSym(j, j, 1)
+ squdeg := math.Sqrt(float64(len(to)))
+ for _, v := range to {
+ vid := v.ID()
+ if uid == vid {
+ panic("network: self edge in graph")
+ }
+ if uid < vid {
+ l.SetSym(indexOf[vid], j, -1/(squdeg*math.Sqrt(float64(g.From(vid).Len()))))
+ }
+ }
+ }
+
+ return Laplacian{Matrix: l, Nodes: nodes, Index: indexOf}
+}
+
+// NewRandomWalkLaplacian returns a damp-scaled random walk Laplacian matrix for
+// the simple graph g.
+// The random walk Laplacian is defined as I-D^(-1)A where D is a diagonal matrix
+// holding the degree of each node and A is the graph adjacency matrix of the input
+// graph.
+// If g contains self edges, NewRandomWalkLaplacian will panic.
+func NewRandomWalkLaplacian(g graph.Graph, damp float64) Laplacian {
+ nodes := graph.NodesOf(g.Nodes())
+ indexOf := make(map[int64]int, len(nodes))
+ for i, n := range nodes {
+ id := n.ID()
+ indexOf[id] = i
+ }
+
+ l := mat.NewDense(len(nodes), len(nodes), nil)
+ for j, u := range nodes {
+ uid := u.ID()
+ to := graph.NodesOf(g.From(uid))
+ if len(to) == 0 {
+ continue
+ }
+ l.Set(j, j, 1-damp)
+ rudeg := (damp - 1) / float64(len(to))
+ for _, v := range to {
+ vid := v.ID()
+ if uid == vid {
+ panic("network: self edge in graph")
+ }
+ l.Set(indexOf[vid], j, rudeg)
+ }
+ }
+
+ return Laplacian{Matrix: l, Nodes: nodes, Index: indexOf}
+}
diff --git a/graph/spectral/laplacian_test.go b/graph/spectral/laplacian_test.go
new file mode 100644
index 0000000..6cdaea1
--- /dev/null
+++ b/graph/spectral/laplacian_test.go
@@ -0,0 +1,177 @@
+// Copyright ©2017 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 spectral
+
+import (
+ "sort"
+ "testing"
+
+ "gonum.org/v1/gonum/floats"
+ "gonum.org/v1/gonum/graph"
+ "gonum.org/v1/gonum/graph/internal/ordered"
+ "gonum.org/v1/gonum/graph/iterator"
+ "gonum.org/v1/gonum/graph/simple"
+ "gonum.org/v1/gonum/mat"
+)
+
+var randomWalkLaplacianTests = []struct {
+ g []set
+ damp float64
+
+ want *mat.Dense
+}{
+ {
+ g: []set{
+ A: linksTo(B, C),
+ B: linksTo(C),
+ C: nil,
+ },
+
+ want: mat.NewDense(3, 3, []float64{
+ 1, 0, 0,
+ -0.5, 1, 0,
+ -0.5, -1, 0,
+ }),
+ },
+ {
+ g: []set{
+ A: linksTo(B, C),
+ B: linksTo(C),
+ C: nil,
+ },
+ damp: 0.85,
+
+ want: mat.NewDense(3, 3, []float64{
+ 0.15, 0, 0,
+ -0.075, 0.15, 0,
+ -0.075, -0.15, 0,
+ }),
+ },
+ {
+ g: []set{
+ A: linksTo(B),
+ B: linksTo(C),
+ C: linksTo(A),
+ },
+ damp: 0.85,
+
+ want: mat.NewDense(3, 3, []float64{
+ 0.15, 0, -0.15,
+ -0.15, 0.15, 0,
+ 0, -0.15, 0.15,
+ }),
+ },
+ {
+ // Example graph from http://en.wikipedia.org/wiki/File:PageRanks-Example.svg 16:17, 8 July 2009
+ g: []set{
+ A: nil,
+ B: linksTo(C),
+ C: linksTo(B),
+ D: linksTo(A, B),
+ E: linksTo(D, B, F),
+ F: linksTo(B, E),
+ G: linksTo(B, E),
+ H: linksTo(B, E),
+ I: linksTo(B, E),
+ J: linksTo(E),
+ K: linksTo(E),
+ },
+
+ want: mat.NewDense(11, 11, []float64{
+ 0, 0, 0, -0.5, 0, 0, 0, 0, 0, 0, 0,
+ 0, 1, -1, -0.5, -1. / 3., -0.5, -0.5, -0.5, -0.5, 0, 0,
+ 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 1, -1. / 3., 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 1, -0.5, -0.5, -0.5, -0.5, -1, -1,
+ 0, 0, 0, 0, -1. / 3., 1, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+ }),
+ },
+}
+
+func TestRandomWalkLaplacian(t *testing.T) {
+ const tol = 1e-14
+ for i, test := range randomWalkLaplacianTests {
+ g := simple.NewDirectedGraph()
+ for u, e := range test.g {
+ // Add nodes that are not defined by an edge.
+ if g.Node(int64(u)) == nil {
+ g.AddNode(simple.Node(u))
+ }
+ for v := range e {
+ g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)})
+ }
+ }
+ l := NewRandomWalkLaplacian(g, test.damp)
+ _, c := l.Dims()
+ for j := 0; j < c; j++ {
+ if got := mat.Sum(l.Matrix.(*mat.Dense).ColView(j)); !floats.EqualWithinAbsOrRel(got, 0, tol, tol) {
+ t.Errorf("unexpected column sum for test %d, column %d: got:%v want:0", i, j, got)
+ }
+ }
+ l = NewRandomWalkLaplacian(sortedNodeGraph{g}, test.damp)
+ if !mat.EqualApprox(l, test.want, tol) {
+ t.Errorf("unexpected result for test %d:\ngot:\n% .2v\nwant:\n% .2v",
+ i, mat.Formatted(l), mat.Formatted(test.want))
+ }
+ }
+}
+
+type sortedNodeGraph struct {
+ graph.Graph
+}
+
+func (g sortedNodeGraph) Nodes() graph.Nodes {
+ n := graph.NodesOf(g.Graph.Nodes())
+ sort.Sort(ordered.ByID(n))
+ return iterator.NewOrderedNodes(n)
+}
+
+const (
+ A = iota
+ B
+ C
+ D
+ E
+ F
+ G
+ H
+ I
+ J
+ K
+ L
+ M
+ N
+ O
+ P
+ Q
+ R
+ S
+ T
+ U
+ V
+ W
+ X
+ Y
+ Z
+)
+
+// set is an integer set.
+type set map[int64]struct{}
+
+func linksTo(i ...int64) set {
+ if len(i) == 0 {
+ return nil
+ }
+ s := make(set)
+ for _, v := range i {
+ s[v] = struct{}{}
+ }
+ return s
+}