pgraph: Add a GraphCmp method

This could probably be more efficient using a known algorithm, and it
could definitely require more tests, but is good enough for now.
This commit is contained in:
James Shubin
2017-05-30 17:28:22 -04:00
parent f65c5fb147
commit 51369adad1
3 changed files with 137 additions and 18 deletions

View File

@@ -167,20 +167,6 @@ func (g *Graph) DeleteEdge(e Edge) {
} }
} }
// VertexMatchFn searches for a vertex in the graph and returns the vertex if
// one matches. It uses a user defined function to match. That function must
// return true on match, and an error if anything goes wrong.
func (g *Graph) VertexMatchFn(fn func(Vertex) (bool, error)) (Vertex, error) {
for v := range g.adjacency {
if b, err := fn(v); err != nil {
return nil, errwrap.Wrapf(err, "fn in VertexMatchFn() errored")
} else if b {
return v, nil
}
}
return nil, nil // nothing found
}
// HasVertex returns if the input vertex exists in the graph. // HasVertex returns if the input vertex exists in the graph.
func (g *Graph) HasVertex(v Vertex) bool { func (g *Graph) HasVertex(v Vertex) bool {
if _, exists := g.adjacency[v]; exists { if _, exists := g.adjacency[v]; exists {
@@ -239,8 +225,8 @@ func (vs VertexSlice) Len() int { return len(vs) }
func (vs VertexSlice) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } func (vs VertexSlice) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }
func (vs VertexSlice) Less(i, j int) bool { return vs[i].String() < vs[j].String() } func (vs VertexSlice) Less(i, j int) bool { return vs[i].String() < vs[j].String() }
// VerticesSorted returns a sorted slice of all vertices in the graph // VerticesSorted returns a sorted slice of all vertices in the graph.
// The order is sorted by String() to avoid the non-determinism in the map type // The order is sorted by String() to avoid the non-determinism in the map type.
func (g *Graph) VerticesSorted() []Vertex { func (g *Graph) VerticesSorted() []Vertex {
var vertices []Vertex var vertices []Vertex
for k := range g.adjacency { for k := range g.adjacency {
@@ -512,6 +498,102 @@ func (g *Graph) Reachability(a, b Vertex) []Vertex {
return result return result
} }
// VertexMatchFn searches for a vertex in the graph and returns the vertex if
// one matches. It uses a user defined function to match. That function must
// return true on match, and an error if anything goes wrong.
func (g *Graph) VertexMatchFn(fn func(Vertex) (bool, error)) (Vertex, error) {
for v := range g.adjacency {
if b, err := fn(v); err != nil {
return nil, errwrap.Wrapf(err, "fn in VertexMatchFn() errored")
} else if b {
return v, nil
}
}
return nil, nil // nothing found
}
// GraphCmp compares the topology of this graph to another and returns nil if
// they're equal. It uses a user defined function to compare topologically
// equivalent vertices, and edges.
// FIXME: add more test cases
func (g *Graph) GraphCmp(graph *Graph, vertexCmpFn func(Vertex, Vertex) (bool, error), edgeCmpFn func(Edge, Edge) (bool, error)) error {
n1, n2 := g.NumVertices(), graph.NumVertices()
if n1 != n2 {
return fmt.Errorf("base graph has %d vertices, while input graph has %d", n1, n2)
}
if e1, e2 := g.NumEdges(), graph.NumEdges(); e1 != e2 {
return fmt.Errorf("base graph has %d edges, while input graph has %d", e1, e2)
}
var m = make(map[Vertex]Vertex) // g to graph vertex correspondence
Loop:
// check vertices
for v1 := range g.Adjacency() { // for each vertex in g
for v2 := range graph.Adjacency() { // does it match in graph ?
b, err := vertexCmpFn(v1, v2)
if err != nil {
return errwrap.Wrapf(err, "could not run vertexCmpFn() properly")
}
// does it match ?
if b {
m[v1] = v2 // store the mapping
continue Loop
}
}
return fmt.Errorf("base graph, has no match in input graph for: %s", v1)
}
// vertices match :)
// is the mapping the right length?
if n1 := len(m); n1 != n2 {
return fmt.Errorf("mapping only has correspondence of %d, when it should have %d", n1, n2)
}
// check if mapping is unique (are there duplicates?)
m1 := []Vertex{}
m2 := []Vertex{}
for k, v := range m {
if VertexContains(k, m1) {
return fmt.Errorf("mapping from %s is used more than once to: %s", k, m1)
}
if VertexContains(v, m2) {
return fmt.Errorf("mapping to %s is used more than once from: %s", v, m2)
}
m1 = append(m1, k)
m2 = append(m2, v)
}
// check edges
for v1 := range g.Adjacency() { // for each vertex in g
v2 := m[v1] // lookup in map to get correspondance
// g.Adjacency()[v1] corresponds to graph.Adjacency()[v2]
if e1, e2 := len(g.Adjacency()[v1]), len(graph.Adjacency()[v2]); e1 != e2 {
return fmt.Errorf("base graph, vertex(%s) has %d edges, while input graph, vertex(%s) has %d", v1, e1, v2, e2)
}
for vv1, ee1 := range g.Adjacency()[v1] {
vv2 := m[vv1]
ee2 := graph.Adjacency()[v2][vv2]
// these are edges from v1 -> vv1 via ee1 (graph 1)
// to cmp to edges from v2 -> vv2 via ee2 (graph 2)
// check: (1) vv1 == vv2 ? (we've already checked this!)
// check: (2) ee1 == ee2
b, err := edgeCmpFn(ee1, ee2)
if err != nil {
return errwrap.Wrapf(err, "could not run edgeCmpFn() properly")
}
if !b {
return fmt.Errorf("base graph edge(%s) doesn't match input graph edge(%s)", ee1, ee2)
}
}
}
return nil // success!
}
// VertexContains is an "in array" function to test for a vertex in a slice of vertices. // VertexContains is an "in array" function to test for a vertex in a slice of vertices.
func VertexContains(needle Vertex, haystack []Vertex) bool { func VertexContains(needle Vertex, haystack []Vertex) bool {
for _, v := range haystack { for _, v := range haystack {

View File

@@ -18,6 +18,7 @@
package pgraph package pgraph
import ( import (
"fmt"
"reflect" "reflect"
"testing" "testing"
) )
@@ -657,3 +658,38 @@ func TestPgraphDelete1(t *testing.T) {
t.Errorf("should have 0 vertices instead of: %d", i) t.Errorf("should have 0 vertices instead of: %d", i)
} }
} }
func vertexCmpFn(v1, v2 Vertex) (bool, error) {
if v1.String() == "" || v2.String() == "" {
return false, fmt.Errorf("oops, empty vertex")
}
return v1.String() == v2.String(), nil
}
func edgeCmpFn(e1, e2 Edge) (bool, error) {
if e1.String() == "" || e2.String() == "" {
return false, fmt.Errorf("oops, empty edge")
}
return e1.String() == e2.String(), nil
}
func TestPgraphGraphCmp1(t *testing.T) {
g1 := &Graph{}
g2 := &Graph{}
g3 := &Graph{}
g3.AddVertex(NV("v1"))
g4 := &Graph{}
g4.AddVertex(NV("v2"))
if err := g1.GraphCmp(g2, vertexCmpFn, edgeCmpFn); err != nil {
t.Errorf("should have no error during GraphCmp, but got: %v", err)
}
if err := g1.GraphCmp(g3, vertexCmpFn, edgeCmpFn); err == nil {
t.Errorf("should have error during GraphCmp, but got nil")
}
if err := g3.GraphCmp(g4, vertexCmpFn, edgeCmpFn); err == nil {
t.Errorf("should have error during GraphCmp, but got nil")
}
}

View File

@@ -117,8 +117,9 @@ func NewNoopResTest(name string) *NoopResTest {
return obj return obj
} }
// GraphCmp compares the topology of two graphs and returns nil if they're equal // GraphCmp compares the topology of two graphs and returns nil if they're
// It also compares if grouped element groups are identical // equal. It also compares if grouped element groups are identical.
// TODO: port this to use the pgraph.GraphCmp function instead.
func GraphCmp(g1, g2 *pgraph.Graph) error { func GraphCmp(g1, g2 *pgraph.Graph) error {
if n1, n2 := g1.NumVertices(), g2.NumVertices(); n1 != n2 { if n1, n2 := g1.NumVertices(), g2.NumVertices(); n1 != n2 {
return fmt.Errorf("graph g1 has %d vertices, while g2 has %d", n1, n2) return fmt.Errorf("graph g1 has %d vertices, while g2 has %d", n1, n2)