mirror of
				https://github.com/gonum/gonum.git
				synced 2025-10-31 02:26:59 +08:00 
			
		
		
		
	Merge pull request #1024 from gonum/graph/layout-isomap
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 | ||||||
|  | // laid 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
	 Dan Kortschak
					Dan Kortschak