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