graph/layout: add Isomap layout

This commit is contained in:
Dan Kortschak
2019-07-22 12:37:24 +09:30
parent 07619a1c75
commit 1f8b402c01
8 changed files with 243 additions and 0 deletions

63
graph/layout/isomap.go Normal file
View File

@@ -0,0 +1,63 @@
// 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 layout
import (
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/path"
"gonum.org/v1/gonum/mat"
"gonum.org/v1/gonum/spatial/r2"
"gonum.org/v1/gonum/stat/mds"
)
// IsomapR2 implements a graph layout algorithm based on the Isomap
// non-linear dimensionality reduction method. Coordinates of nodes
// are computed by finding a Torgerson multidimensional scaling of
// the shortest path distances between all pairs of node in the graph.
// The all pair shortest path distances are calculated using the
// Floyd-Warshall algorithm and so IsomapR2 will not scale to large
// graphs. Graphs with more than one connected component cannot be
// layed out by IsomapR2.
type IsomapR2 struct{}
// Update is the IsomapR2 spatial graph update function.
func (IsomapR2) Update(g graph.Graph, layout LayoutR2) bool {
nodes := graph.NodesOf(g.Nodes())
v := isomap(g, nodes, 2)
if v == nil {
return false
}
// FIXME(kortschak): The Layout types do not have the capacity to
// be cleared in the current API. Is this a problem? I don't know
// at this stage. It might be if the layout is reused between graphs.
// Someone may do this.
for i, n := range nodes {
layout.SetCoord2(n.ID(), r2.Vec{X: v.At(i, 0), Y: v.At(i, 1)})
}
return false
}
func isomap(g graph.Graph, nodes []graph.Node, dims int) *mat.Dense {
p, ok := path.FloydWarshall(g)
if !ok {
return nil
}
dist := mat.NewSymDense(len(nodes), nil)
for i, u := range nodes {
for j, v := range nodes {
dist.SetSym(i, j, p.Weight(u.ID(), v.ID()))
}
}
k, v, _ := mds.TorgersonScaling(nil, nil, dist)
if k < dims {
return nil
}
return v
}

180
graph/layout/isomap_test.go Normal file
View File

@@ -0,0 +1,180 @@
// 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 layout
import (
"path/filepath"
"testing"
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/simple"
"gonum.org/v1/gonum/spatial/r2"
"gonum.org/v1/plot"
"gonum.org/v1/plot/vg"
)
var isomapR2Tests = []struct {
name string
g graph.Graph
}{
{
name: "line_isomap",
g: func() graph.Graph {
edges := []simple.Edge{
{simple.Node(0), simple.Node(1)},
}
g := simple.NewUndirectedGraph()
for _, e := range edges {
g.SetEdge(e)
}
return orderedGraph{g}
}(),
},
{
name: "square_isomap",
g: func() graph.Graph {
edges := []simple.Edge{
{simple.Node(0), simple.Node(1)},
{simple.Node(0), simple.Node(2)},
{simple.Node(1), simple.Node(3)},
{simple.Node(2), simple.Node(3)},
}
g := simple.NewUndirectedGraph()
for _, e := range edges {
g.SetEdge(e)
}
return orderedGraph{g}
}(),
},
{
name: "tetrahedron_isomap",
g: func() graph.Graph {
edges := []simple.Edge{
{simple.Node(0), simple.Node(1)},
{simple.Node(0), simple.Node(2)},
{simple.Node(0), simple.Node(3)},
{simple.Node(1), simple.Node(2)},
{simple.Node(1), simple.Node(3)},
{simple.Node(2), simple.Node(3)},
}
g := simple.NewUndirectedGraph()
for _, e := range edges {
g.SetEdge(e)
}
return orderedGraph{g}
}(),
},
{
name: "sheet_isomap",
g: func() graph.Graph {
edges := []simple.Edge{
{simple.Node(0), simple.Node(1)},
{simple.Node(0), simple.Node(3)},
{simple.Node(1), simple.Node(2)},
{simple.Node(1), simple.Node(4)},
{simple.Node(2), simple.Node(5)},
{simple.Node(3), simple.Node(4)},
{simple.Node(3), simple.Node(6)},
{simple.Node(4), simple.Node(5)},
{simple.Node(4), simple.Node(7)},
{simple.Node(5), simple.Node(8)},
{simple.Node(6), simple.Node(7)},
{simple.Node(7), simple.Node(8)},
}
g := simple.NewUndirectedGraph()
for _, e := range edges {
g.SetEdge(e)
}
return orderedGraph{g}
}(),
},
{
name: "tube_isomap",
g: func() graph.Graph {
edges := []simple.Edge{
{simple.Node(0), simple.Node(1)},
{simple.Node(0), simple.Node(2)},
{simple.Node(0), simple.Node(3)},
{simple.Node(1), simple.Node(2)},
{simple.Node(1), simple.Node(4)},
{simple.Node(2), simple.Node(5)},
{simple.Node(3), simple.Node(4)},
{simple.Node(3), simple.Node(5)},
{simple.Node(3), simple.Node(6)},
{simple.Node(4), simple.Node(5)},
{simple.Node(4), simple.Node(7)},
{simple.Node(5), simple.Node(8)},
{simple.Node(6), simple.Node(7)},
{simple.Node(6), simple.Node(8)},
{simple.Node(7), simple.Node(8)},
}
g := simple.NewUndirectedGraph()
for _, e := range edges {
g.SetEdge(e)
}
return orderedGraph{g}
}(),
},
{
name: "wp_page_isomap", // https://en.wikipedia.org/wiki/PageRank#/media/File:PageRanks-Example.jpg
g: func() graph.Graph {
edges := []simple.Edge{
{simple.Node(0), simple.Node(3)},
{simple.Node(1), simple.Node(2)},
{simple.Node(1), simple.Node(3)},
{simple.Node(1), simple.Node(4)},
{simple.Node(1), simple.Node(5)},
{simple.Node(1), simple.Node(6)},
{simple.Node(1), simple.Node(7)},
{simple.Node(1), simple.Node(8)},
{simple.Node(3), simple.Node(4)},
{simple.Node(4), simple.Node(5)},
{simple.Node(4), simple.Node(6)},
{simple.Node(4), simple.Node(7)},
{simple.Node(4), simple.Node(8)},
{simple.Node(4), simple.Node(9)},
{simple.Node(4), simple.Node(10)},
}
g := simple.NewUndirectedGraph()
for _, e := range edges {
g.SetEdge(e)
}
return orderedGraph{g}
}(),
},
}
func TestIsomapR2(t *testing.T) {
for _, test := range isomapR2Tests {
o := NewOptimizerR2(test.g, IsomapR2{}.Update)
var n int
for o.Update() {
n++
}
p, err := plot.New()
if err != nil {
t.Errorf("unexpected error: %v", err)
continue
}
p.Add(render{o})
p.HideAxes()
path := filepath.Join("testdata", test.name+".png")
err = p.Save(10*vg.Centimeter, 10*vg.Centimeter, path)
if err != nil {
t.Errorf("unexpected error: %v", err)
continue
}
ok := checkRenderedLayout(t, path)
if !ok {
got := make(map[int64]r2.Vec)
nodes := test.g.Nodes()
for nodes.Next() {
id := nodes.Node().ID()
got[id] = o.Coord2(id)
}
t.Logf("got node positions: %#v", got)
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB