mirror of
https://github.com/gonum/gonum.git
synced 2025-10-30 02:02:09 +08:00
graph/network: add heat diffusion propagation functions
This commit is contained in:
212
graph/network/diffusion.go
Normal file
212
graph/network/diffusion.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
// 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 network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"gonum.org/v1/gonum/graph"
|
||||||
|
"gonum.org/v1/gonum/mat"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Diffuse performs a heat diffusion across nodes of the undirected
|
||||||
|
// graph described by the given Laplacian using the initial heat distribution,
|
||||||
|
// h, according to the Laplacian with a diffusion time of t.
|
||||||
|
// The resulting heat distribution is returned, written into the map dst and
|
||||||
|
// returned,
|
||||||
|
// d = exp(-Lt)×h
|
||||||
|
// where L is the graph Laplacian. Indexing into h and dst is defined by the
|
||||||
|
// Laplacian Index field. If dst is nil, a new map is created.
|
||||||
|
//
|
||||||
|
// 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 {
|
||||||
|
heat := make([]float64, len(by.Index))
|
||||||
|
for id, i := range by.Index {
|
||||||
|
heat[i] = h[id]
|
||||||
|
}
|
||||||
|
v := mat.NewVecDense(len(heat), heat)
|
||||||
|
|
||||||
|
var m, tl mat.Dense
|
||||||
|
tl.Scale(-t, by)
|
||||||
|
m.Exp(&tl)
|
||||||
|
v.MulVec(&m, v)
|
||||||
|
|
||||||
|
if dst == nil {
|
||||||
|
dst = make(map[int64]float64)
|
||||||
|
}
|
||||||
|
for i, n := range heat {
|
||||||
|
dst[by.Nodes[i].ID()] = n
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiffuseToEquilibrium performs a heat diffusion across nodes of the
|
||||||
|
// graph described by the given Laplacian using the initial heat
|
||||||
|
// distribution, h, according to the Laplacian until the update function
|
||||||
|
// h_{n+1} = h_n - L×h_n
|
||||||
|
// results in a 2-norm update difference within tol, or iters updates have
|
||||||
|
// been made.
|
||||||
|
// The resulting heat distribution is returned as eq, written into the map dst,
|
||||||
|
// and a boolean indicating whether the equilibrium converged to within tol.
|
||||||
|
// Indexing into h and dst is defined by the Laplacian Index field. If dst
|
||||||
|
// is nil, a new map is created.
|
||||||
|
//
|
||||||
|
// 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) {
|
||||||
|
heat := make([]float64, len(by.Index))
|
||||||
|
for id, i := range by.Index {
|
||||||
|
heat[i] = h[id]
|
||||||
|
}
|
||||||
|
v := mat.NewVecDense(len(heat), heat)
|
||||||
|
|
||||||
|
last := make([]float64, len(by.Index))
|
||||||
|
for id, i := range by.Index {
|
||||||
|
last[i] = h[id]
|
||||||
|
}
|
||||||
|
lastV := mat.NewVecDense(len(last), last)
|
||||||
|
|
||||||
|
var tmp mat.VecDense
|
||||||
|
for {
|
||||||
|
iters--
|
||||||
|
if iters < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lastV, v = v, lastV
|
||||||
|
tmp.MulVec(by.Matrix, lastV)
|
||||||
|
v.SubVec(lastV, &tmp)
|
||||||
|
if normDiff(heat, last) < tol {
|
||||||
|
ok = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dst == nil {
|
||||||
|
dst = make(map[int64]float64)
|
||||||
|
}
|
||||||
|
for i, n := range v.RawVector().Data {
|
||||||
|
dst[by.Nodes[i].ID()] = n
|
||||||
|
}
|
||||||
|
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 := 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 {
|
||||||
|
to := g.From(u)
|
||||||
|
l.SetSym(j, j, float64(len(to)))
|
||||||
|
uid := u.ID()
|
||||||
|
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 := 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 {
|
||||||
|
to := g.From(u)
|
||||||
|
if len(to) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l.SetSym(j, j, 1)
|
||||||
|
uid := u.ID()
|
||||||
|
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(len(g.From(v))))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := 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 := g.From(u)
|
||||||
|
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}
|
||||||
|
}
|
||||||
526
graph/network/diffusion_test.go
Normal file
526
graph/network/diffusion_test.go
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
// 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 network
|
||||||
|
|
||||||
|
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/simple"
|
||||||
|
"gonum.org/v1/gonum/mat"
|
||||||
|
)
|
||||||
|
|
||||||
|
var diffuseTests = []struct {
|
||||||
|
g []set
|
||||||
|
h map[int64]float64
|
||||||
|
t float64
|
||||||
|
|
||||||
|
wantTol float64
|
||||||
|
want map[bool]map[int64]float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
g: grid(5),
|
||||||
|
h: map[int64]float64{0: 1},
|
||||||
|
t: 0.1,
|
||||||
|
|
||||||
|
wantTol: 1e-9,
|
||||||
|
want: map[bool]map[int64]float64{
|
||||||
|
false: {
|
||||||
|
A: 0.826684055, B: 0.078548060, C: 0.003858840, D: 0.000127487, E: 0.000003233,
|
||||||
|
F: 0.078548060, G: 0.007463308, H: 0.000366651, I: 0.000012113, J: 0.000000307,
|
||||||
|
K: 0.003858840, L: 0.000366651, M: 0.000018012, N: 0.000000595, O: 0.000000015,
|
||||||
|
P: 0.000127487, Q: 0.000012113, R: 0.000000595, S: 0.000000020, T: 0.000000000,
|
||||||
|
U: 0.000003233, V: 0.000000307, W: 0.000000015, X: 0.000000000, Y: 0.000000000,
|
||||||
|
},
|
||||||
|
true: {
|
||||||
|
A: 0.9063462486, B: 0.0369774705, C: 0.0006161414, D: 0.0000068453, E: 0.0000000699,
|
||||||
|
F: 0.0369774705, G: 0.0010670895, H: 0.0000148186, I: 0.0000001420, J: 0.0000000014,
|
||||||
|
K: 0.0006161414, L: 0.0000148186, M: 0.0000001852, N: 0.0000000016, O: 0.0000000000,
|
||||||
|
P: 0.0000068453, Q: 0.0000001420, R: 0.0000000016, S: 0.0000000000, T: 0.0000000000,
|
||||||
|
U: 0.0000000699, V: 0.0000000014, W: 0.0000000000, X: 0.0000000000, Y: 0.0000000000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: grid(5),
|
||||||
|
h: map[int64]float64{0: 1},
|
||||||
|
t: 1,
|
||||||
|
|
||||||
|
wantTol: 1e-9,
|
||||||
|
want: map[bool]map[int64]float64{
|
||||||
|
false: {
|
||||||
|
A: 0.2743435076, B: 0.1615920872, C: 0.0639346641, D: 0.0188054933, E: 0.0051023569,
|
||||||
|
F: 0.1615920872, G: 0.0951799548, H: 0.0376583937, I: 0.0110766934, J: 0.0030053582,
|
||||||
|
K: 0.0639346641, L: 0.0376583937, M: 0.0148997194, N: 0.0043825455, O: 0.0011890840,
|
||||||
|
P: 0.0188054933, Q: 0.0110766934, R: 0.0043825455, S: 0.0012890649, T: 0.0003497525,
|
||||||
|
U: 0.0051023569, V: 0.0030053582, W: 0.0011890840, X: 0.0003497525, Y: 0.0000948958,
|
||||||
|
},
|
||||||
|
true: {
|
||||||
|
A: 0.4323917545, B: 0.1660487336, C: 0.0270298904, D: 0.0029720194, E: 0.0003007247,
|
||||||
|
F: 0.1660487336, G: 0.0463974679, H: 0.0063556078, I: 0.0006056850, J: 0.0000589574,
|
||||||
|
K: 0.0270298904, L: 0.0063556078, M: 0.0007860810, N: 0.0000691647, O: 0.0000065586,
|
||||||
|
P: 0.0029720194, Q: 0.0006056850, R: 0.0000691647, S: 0.0000057466, T: 0.0000005475,
|
||||||
|
U: 0.0003007247, V: 0.0000589574, W: 0.0000065586, X: 0.0000005475, Y: 0.0000000555,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: grid(5),
|
||||||
|
h: map[int64]float64{0: 1},
|
||||||
|
t: 10,
|
||||||
|
|
||||||
|
wantTol: 1e-9,
|
||||||
|
want: map[bool]map[int64]float64{
|
||||||
|
false: {
|
||||||
|
A: 0.0432408511, B: 0.0425986522, C: 0.0415977802, D: 0.0405588482, E: 0.0399403788,
|
||||||
|
F: 0.0425986522, G: 0.0420083007, H: 0.0409532810, I: 0.0399982373, J: 0.0393463013,
|
||||||
|
K: 0.0415977802, L: 0.0409532810, M: 0.0400339958, N: 0.0389913353, O: 0.0384232854,
|
||||||
|
P: 0.0405588482, Q: 0.0399982373, R: 0.0389913353, S: 0.0380844049, T: 0.0374622025,
|
||||||
|
U: 0.0399403788, V: 0.0393463013, W: 0.0384232854, X: 0.0374622025, Y: 0.0368918429,
|
||||||
|
},
|
||||||
|
true: {
|
||||||
|
A: 0.0532814862, B: 0.0594280160, C: 0.0462076361, D: 0.0330529557, E: 0.0211688130,
|
||||||
|
F: 0.0594280160, G: 0.0612529898, H: 0.0462850376, I: 0.0319891593, J: 0.0213123519,
|
||||||
|
K: 0.0462076361, L: 0.0462850376, M: 0.0340410963, N: 0.0229646704, O: 0.0152763556,
|
||||||
|
P: 0.0330529557, Q: 0.0319891593, R: 0.0229646704, S: 0.0153031853, T: 0.0103681461,
|
||||||
|
U: 0.0211688130, V: 0.0213123519, W: 0.0152763556, X: 0.0103681461, Y: 0.0068893147,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: grid(5),
|
||||||
|
h: func() map[int64]float64 {
|
||||||
|
m := make(map[int64]float64, 25)
|
||||||
|
for i := int64(A); i <= Y; i++ {
|
||||||
|
m[i] = 1
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}(),
|
||||||
|
t: 0.01, // FIXME(kortschak): Low t used due to instability in mat.Exp.
|
||||||
|
|
||||||
|
wantTol: 1e-1, // FIXME(kortschak): High tolerance used due to instability in mat.Exp.
|
||||||
|
want: map[bool]map[int64]float64{
|
||||||
|
false: {
|
||||||
|
A: 1, B: 1, C: 1, D: 1, E: 1,
|
||||||
|
F: 1, G: 1, H: 1, I: 1, J: 1,
|
||||||
|
K: 1, L: 1, M: 1, N: 1, O: 1,
|
||||||
|
P: 1, Q: 1, R: 1, S: 1, T: 1,
|
||||||
|
U: 1, V: 1, W: 1, X: 1, Y: 1,
|
||||||
|
},
|
||||||
|
true: {
|
||||||
|
// Output from the python implementation associated with doi:10.1371/journal.pcbi.1005598.
|
||||||
|
A: 0.98264450473308107, B: 1.002568278028513, C: 0.9958911385307706, D: 1.002568278028513, E: 0.98264450473308107,
|
||||||
|
F: 1.002568278028513, G: 1.0075291695232433, H: 1.0038067383118021, I: 1.0075291695232433, J: 1.002568278028513,
|
||||||
|
K: 0.9958911385307706, L: 1.0038067383118021, M: 1.0001850837547184, N: 1.0038067383118021, O: 0.9958911385307706,
|
||||||
|
P: 1.002568278028513, Q: 1.0075291695232433, R: 1.0038067383118021, S: 1.0075291695232433, T: 1.002568278028513,
|
||||||
|
U: 0.98264450473308107, V: 1.002568278028513, W: 0.9958911385307706, X: 1.002568278028513, Y: 0.98264450473308107,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: []set{
|
||||||
|
A: linksTo(B, C),
|
||||||
|
B: linksTo(D),
|
||||||
|
C: nil,
|
||||||
|
D: nil,
|
||||||
|
E: linksTo(F),
|
||||||
|
F: nil,
|
||||||
|
},
|
||||||
|
h: map[int64]float64{A: 1, E: 10},
|
||||||
|
t: 0.1,
|
||||||
|
|
||||||
|
wantTol: 1e-9,
|
||||||
|
want: map[bool]map[int64]float64{
|
||||||
|
false: {
|
||||||
|
A: 0.8270754166, B: 0.0822899600, C: 0.0863904410, D: 0.0042441824, E: 9.0936537654, F: 0.9063462346,
|
||||||
|
},
|
||||||
|
true: {
|
||||||
|
A: 0.9082331512, B: 0.0453361743, C: 0.0640616812, D: 0.0016012085, E: 9.0936537654, F: 0.9063462346,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiffuse(t *testing.T) {
|
||||||
|
for i, test := range diffuseTests {
|
||||||
|
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)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for j, lfn := range []func(g graph.Undirected) Laplacian{NewLaplacian, NewSymNormLaplacian} {
|
||||||
|
normalize := j == 1
|
||||||
|
var wantTemp float64
|
||||||
|
for _, v := range test.h {
|
||||||
|
wantTemp += v
|
||||||
|
}
|
||||||
|
got := Diffuse(nil, test.h, lfn(g), test.t)
|
||||||
|
prec := 1 - int(math.Log10(test.wantTol))
|
||||||
|
for n := range test.g {
|
||||||
|
if !floats.EqualWithinAbsOrRel(got[int64(n)], test.want[normalize][int64(n)], test.wantTol, test.wantTol) {
|
||||||
|
t.Errorf("unexpected Diffuse result for test %d with normalize=%t:\ngot: %v\nwant:%v",
|
||||||
|
i, normalize, orderedFloats(got, prec), orderedFloats(test.want[normalize], prec))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if j == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var gotTemp float64
|
||||||
|
for _, v := range got {
|
||||||
|
gotTemp += v
|
||||||
|
}
|
||||||
|
gotTemp /= float64(len(got))
|
||||||
|
wantTemp /= float64(len(got))
|
||||||
|
if !floats.EqualWithinAbsOrRel(gotTemp, wantTemp, test.wantTol, test.wantTol) {
|
||||||
|
t.Errorf("unexpected total heat for test %d with normalize=%t: got:%v want:%v",
|
||||||
|
i, normalize, gotTemp, wantTemp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.Node {
|
||||||
|
n := g.Graph.Nodes()
|
||||||
|
sort.Sort(ordered.ByID(n))
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
var diffuseToEquilibriumTests = []struct {
|
||||||
|
g []set
|
||||||
|
builder builder
|
||||||
|
h map[int64]float64
|
||||||
|
damp float64
|
||||||
|
tol float64
|
||||||
|
iter int
|
||||||
|
|
||||||
|
want map[int64]float64
|
||||||
|
wantOK bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
g: grid(5),
|
||||||
|
builder: simple.NewUndirectedGraph(),
|
||||||
|
h: map[int64]float64{0: 1},
|
||||||
|
damp: 0.85,
|
||||||
|
tol: 1e-6,
|
||||||
|
iter: 1e4,
|
||||||
|
|
||||||
|
want: map[int64]float64{
|
||||||
|
A: 0.025000, B: 0.037500, C: 0.037500, D: 0.037500, E: 0.025000,
|
||||||
|
F: 0.037500, G: 0.050000, H: 0.050000, I: 0.050000, J: 0.037500,
|
||||||
|
K: 0.037500, L: 0.050000, M: 0.050000, N: 0.050000, O: 0.037500,
|
||||||
|
P: 0.037500, Q: 0.050000, R: 0.050000, S: 0.050000, T: 0.037500,
|
||||||
|
U: 0.025000, V: 0.037500, W: 0.037500, X: 0.037500, Y: 0.025000,
|
||||||
|
},
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 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),
|
||||||
|
},
|
||||||
|
builder: simple.NewDirectedGraph(),
|
||||||
|
h: map[int64]float64{
|
||||||
|
A: 1. / 11.,
|
||||||
|
B: 1. / 11.,
|
||||||
|
C: 1. / 11.,
|
||||||
|
D: 1. / 11.,
|
||||||
|
E: 1. / 11.,
|
||||||
|
F: 1. / 11.,
|
||||||
|
G: 1. / 11.,
|
||||||
|
H: 1. / 11.,
|
||||||
|
I: 1. / 11.,
|
||||||
|
J: 1. / 11.,
|
||||||
|
K: 1. / 11.,
|
||||||
|
},
|
||||||
|
damp: 0.85,
|
||||||
|
tol: 1e-6,
|
||||||
|
iter: 1e4,
|
||||||
|
|
||||||
|
// This does not look like Page Rank because we do not
|
||||||
|
// do the random node hops. An alternative Laplacian
|
||||||
|
// value that does do that would replicate PageRank. This
|
||||||
|
// is left as an excercise for the reader.
|
||||||
|
want: map[int64]float64{
|
||||||
|
A: 0.227273,
|
||||||
|
B: 0.386364,
|
||||||
|
C: 0.386364,
|
||||||
|
D: 0.000000,
|
||||||
|
E: 0.000000,
|
||||||
|
F: 0.000000,
|
||||||
|
G: 0.000000,
|
||||||
|
H: 0.000000,
|
||||||
|
I: 0.000000,
|
||||||
|
J: 0.000000,
|
||||||
|
K: 0.000000,
|
||||||
|
},
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: []set{
|
||||||
|
A: linksTo(B, C),
|
||||||
|
B: linksTo(D, C),
|
||||||
|
C: nil,
|
||||||
|
D: nil,
|
||||||
|
E: linksTo(F),
|
||||||
|
F: nil,
|
||||||
|
},
|
||||||
|
builder: simple.NewDirectedGraph(),
|
||||||
|
h: map[int64]float64{A: 1, E: -10},
|
||||||
|
tol: 1e-6,
|
||||||
|
iter: 3,
|
||||||
|
|
||||||
|
want: map[int64]float64{
|
||||||
|
A: 0, B: 0, C: 0.75, D: 0.25, E: 0, F: -10,
|
||||||
|
},
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: []set{
|
||||||
|
A: linksTo(B, C),
|
||||||
|
B: linksTo(D, C),
|
||||||
|
C: nil,
|
||||||
|
D: nil,
|
||||||
|
E: linksTo(F),
|
||||||
|
F: nil,
|
||||||
|
},
|
||||||
|
builder: simple.NewUndirectedGraph(),
|
||||||
|
h: map[int64]float64{A: 1, E: -10},
|
||||||
|
damp: 0.85,
|
||||||
|
tol: 1e-6,
|
||||||
|
iter: 1e4,
|
||||||
|
|
||||||
|
want: map[int64]float64{
|
||||||
|
A: 0.25, B: 0.375, C: 0.25, D: 0.125, E: -5, F: -5,
|
||||||
|
},
|
||||||
|
wantOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: []set{
|
||||||
|
A: linksTo(B),
|
||||||
|
B: linksTo(C),
|
||||||
|
C: nil,
|
||||||
|
},
|
||||||
|
builder: simple.NewUndirectedGraph(),
|
||||||
|
h: map[int64]float64{B: 1},
|
||||||
|
iter: 1,
|
||||||
|
tol: 1e-6,
|
||||||
|
want: map[int64]float64{
|
||||||
|
A: 0.5, B: 0, C: 0.5,
|
||||||
|
},
|
||||||
|
wantOK: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: []set{
|
||||||
|
A: linksTo(B),
|
||||||
|
B: linksTo(C),
|
||||||
|
C: nil,
|
||||||
|
},
|
||||||
|
builder: simple.NewUndirectedGraph(),
|
||||||
|
h: map[int64]float64{B: 1},
|
||||||
|
iter: 2,
|
||||||
|
tol: 1e-6,
|
||||||
|
want: map[int64]float64{
|
||||||
|
A: 0, B: 1, C: 0,
|
||||||
|
},
|
||||||
|
wantOK: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiffuseToEquilibrium(t *testing.T) {
|
||||||
|
for i, test := range diffuseToEquilibriumTests {
|
||||||
|
g := test.builder
|
||||||
|
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)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var wantTemp float64
|
||||||
|
for _, v := range test.h {
|
||||||
|
wantTemp += v
|
||||||
|
}
|
||||||
|
got, ok := DiffuseToEquilibrium(nil, test.h, 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)
|
||||||
|
}
|
||||||
|
prec := -int(math.Log10(test.tol))
|
||||||
|
for n := range test.g {
|
||||||
|
if !floats.EqualWithinAbsOrRel(got[int64(n)], test.want[int64(n)], test.tol, test.tol) {
|
||||||
|
t.Errorf("unexpected DiffuseToEquilibrium result for test %d:\ngot: %v\nwant:%v",
|
||||||
|
i, orderedFloats(got, prec), orderedFloats(test.want, prec))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var gotTemp float64
|
||||||
|
for _, v := range got {
|
||||||
|
gotTemp += v
|
||||||
|
}
|
||||||
|
gotTemp /= float64(len(got))
|
||||||
|
wantTemp /= float64(len(got))
|
||||||
|
if !floats.EqualWithinAbsOrRel(gotTemp, wantTemp, test.tol, test.tol) {
|
||||||
|
t.Errorf("unexpected total heat for test %d: got:%v want:%v",
|
||||||
|
i, gotTemp, wantTemp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type builder interface {
|
||||||
|
graph.Graph
|
||||||
|
graph.Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func grid(d int) []set {
|
||||||
|
dim := int64(d)
|
||||||
|
s := make([]set, dim*dim)
|
||||||
|
for i := range s {
|
||||||
|
s[i] = make(set)
|
||||||
|
}
|
||||||
|
for i := int64(0); i < dim*dim; i++ {
|
||||||
|
if i%dim != 0 {
|
||||||
|
s[i][i-1] = struct{}{}
|
||||||
|
}
|
||||||
|
if i/dim != 0 {
|
||||||
|
s[i][i-dim] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -16,6 +16,21 @@ const (
|
|||||||
I
|
I
|
||||||
J
|
J
|
||||||
K
|
K
|
||||||
|
L
|
||||||
|
M
|
||||||
|
N
|
||||||
|
O
|
||||||
|
P
|
||||||
|
Q
|
||||||
|
R
|
||||||
|
S
|
||||||
|
T
|
||||||
|
U
|
||||||
|
V
|
||||||
|
W
|
||||||
|
X
|
||||||
|
Y
|
||||||
|
Z
|
||||||
)
|
)
|
||||||
|
|
||||||
// set is an integer set.
|
// set is an integer set.
|
||||||
|
|||||||
Reference in New Issue
Block a user