mirror of
https://github.com/gonum/gonum.git
synced 2025-10-07 08:01:20 +08:00
graph/layout: add Isomap layout
This commit is contained in:
63
graph/layout/isomap.go
Normal file
63
graph/layout/isomap.go
Normal 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
180
graph/layout/isomap_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
BIN
graph/layout/testdata/line_isomap_golden.png
vendored
Normal file
BIN
graph/layout/testdata/line_isomap_golden.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
BIN
graph/layout/testdata/sheet_isomap_golden.png
vendored
Normal file
BIN
graph/layout/testdata/sheet_isomap_golden.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
graph/layout/testdata/square_isomap_golden.png
vendored
Normal file
BIN
graph/layout/testdata/square_isomap_golden.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
BIN
graph/layout/testdata/tetrahedron_isomap_golden.png
vendored
Normal file
BIN
graph/layout/testdata/tetrahedron_isomap_golden.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
graph/layout/testdata/tube_isomap_golden.png
vendored
Normal file
BIN
graph/layout/testdata/tube_isomap_golden.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
graph/layout/testdata/wp_page_isomap_golden.png
vendored
Normal file
BIN
graph/layout/testdata/wp_page_isomap_golden.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Reference in New Issue
Block a user