Skip to content
This repository has been archived by the owner on Apr 26, 2019. It is now read-only.

Commit

Permalink
graph: add type to allow defined conversion directed -> undirected
Browse files Browse the repository at this point in the history
  • Loading branch information
kortschak committed Oct 30, 2015
1 parent f32c7f2 commit 872be85
Show file tree
Hide file tree
Showing 3 changed files with 291 additions and 1 deletion.
3 changes: 2 additions & 1 deletion graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ type DirectedBuilder interface {
//
// If the source is a directed graph, the destination is undirected, and a fundamental
// cycle exists with two nodes where the edge weights differ, the resulting destination
// graph's edge weight between those nodes is undefined.
// graph's edge weight between those nodes is undefined. If there is a defined function
// to resolve such conflicts, an Undirect may be used to do this.
func Copy(dst Builder, src Graph) {
nodes := src.Nodes()
for _, n := range nodes {
Expand Down
163 changes: 163 additions & 0 deletions undirect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright ©2015 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 graph

// Undirect converts a directed graph to an undirected graph, resolving
// edge weight conflicts.
type Undirect struct {
G Directed

// Absent is the value used to
// represent absent edge weights
// passed to Merge if the reverse
// edge is present.
Absent float64

// Merge defines how discordant edge
// weights in G are resolved. A merge
// is performed if at least one edge
// exists between the nodes being
// considered. The edges corresponding
// to the two weights are also passed,
// in the same order.
// The order of weight parameters
// passed to Merge is not defined, so
// the function should be commutative.
// If Merge is nil, the arithmetic
// mean is used to merge weights.
Merge func(x, y float64, xe, ye Edge) float64
}

var (
_ Undirected = Undirect{}
_ Weighter = Undirect{}
)

// Has returns whether the node exists within the graph.
func (g Undirect) Has(n Node) bool { return g.G.Has(n) }

// Nodes returns all the nodes in the graph.
func (g Undirect) Nodes() []Node { return g.G.Nodes() }

// From returns all nodes in g that can be reached directly from u.
func (g Undirect) From(u Node) []Node { return g.G.From(u) }

// HasEdgeBetween returns whether an edge exists between nodes x and y.
func (g Undirect) HasEdgeBetween(x, y Node) bool { return g.G.HasEdgeBetween(x, y) }

// Edge returns the edge from u to v if such an edge exists and nil otherwise.
// The node v must be directly reachable from u as defined by the From method.
// If an edge exists, the Edge returned is an EdgePair. The weight of
// the edge is determined by applying the Merge func to the weights of the
// edges between u and v.
func (g Undirect) Edge(u, v Node) Edge { return g.EdgeBetween(u, v) }

// EdgeBetween returns the edge between nodes x and y. If an edge exists, the
// Edge returned is an EdgePair. The weight of the edge is determined by
// applying the Merge func to the weights of edges between x and y.
func (g Undirect) EdgeBetween(x, y Node) Edge {
fe := g.G.Edge(x, y)
re := g.G.Edge(y, x)
if fe == nil && re == nil {
return nil
}

var f, r float64
if wg, ok := g.G.(Weighter); ok {
f, ok = wg.Weight(x, y)
if !ok {
f = g.Absent
}
r, ok = wg.Weight(y, x)
if !ok {
r = g.Absent
}
} else {
f = g.Absent
if fe != nil {
f = fe.Weight()
}
r = g.Absent
if re != nil {
r = re.Weight()
}
}

var w float64
if g.Merge == nil {
w = (f + r) / 2
} else {
w = g.Merge(f, r, fe, re)
}
return EdgePair{E: [2]Edge{fe, re}, W: w}
}

// Weight returns the weight for the edge between x and y if Edge(x, y) returns a non-nil Edge.
// If x and y are the same node the internal node weight is returned. If there is no joining
// edge between the two nodes the weight value returned is zero. Weight returns true if an edge
// exists between x and y or if x and y have the same ID, false otherwise.
func (g Undirect) Weight(x, y Node) (w float64, ok bool) {
fe := g.G.Edge(x, y)
re := g.G.Edge(y, x)

var f, r float64
if wg, ok := g.G.(Weighter); ok {
var fOk, rOK bool
f, fOk = wg.Weight(x, y)
if !fOk {
f = g.Absent
}
r, rOK = wg.Weight(y, x)
if !rOK {
r = g.Absent
}
ok = fOk || rOK
} else {
f = g.Absent
if fe != nil {
f = fe.Weight()
ok = true
}
r = g.Absent
if re != nil {
r = re.Weight()
ok = true
}
}

if g.Merge == nil {
return (f + r) / 2, ok
}
return g.Merge(f, r, fe, re), ok
}

// EdgePair is an opposed pair of directed edges.
type EdgePair struct {
E [2]Edge
W float64
}

// From returns the from node of the first non-nil edge, or nil.
func (e EdgePair) From() Node {
if e.E[0] != nil {
return e.E[0].From()
} else if e.E[1] != nil {
return e.E[1].From()
}
return nil
}

// To returns the to node of the first non-nil edge, or nil.
func (e EdgePair) To() Node {
if e.E[0] != nil {
return e.E[0].To()
} else if e.E[1] != nil {
return e.E[1].To()
}
return nil
}

// Weight returns the merged edge weights of the two edges.
func (e EdgePair) Weight() float64 { return e.W }
126 changes: 126 additions & 0 deletions undirect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright ©2015 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 graph_test

import (
"math"
"testing"

"github.com/gonum/graph"
"github.com/gonum/graph/simple"
"github.com/gonum/matrix/mat64"
)

var directedGraphs = []struct {
g func() graph.DirectedBuilder
edges []simple.Edge
absent float64
merge func(x, y float64, xe, ye graph.Edge) float64

want mat64.Matrix
}{
{
g: func() graph.DirectedBuilder { return simple.NewDirectedGraph(0, 0) },
edges: []simple.Edge{
{F: simple.Node(0), T: simple.Node(1), W: 2},
{F: simple.Node(1), T: simple.Node(0), W: 1},
{F: simple.Node(1), T: simple.Node(2), W: 1},
},
want: mat64.NewSymDense(3, []float64{
0, (1. + 2.) / 2., 0,
(1. + 2.) / 2., 0, 1. / 2.,
0, 1. / 2., 0,
}),
},
{
g: func() graph.DirectedBuilder { return simple.NewDirectedGraph(0, 0) },
edges: []simple.Edge{
{F: simple.Node(0), T: simple.Node(1), W: 2},
{F: simple.Node(1), T: simple.Node(0), W: 1},
{F: simple.Node(1), T: simple.Node(2), W: 1},
},
absent: 1,
merge: func(x, y float64, _, _ graph.Edge) float64 { return math.Sqrt(x * y) },
want: mat64.NewSymDense(3, []float64{
0, math.Sqrt(1 * 2), 0,
math.Sqrt(1 * 2), 0, math.Sqrt(1 * 1),
0, math.Sqrt(1 * 1), 0,
}),
},
{
g: func() graph.DirectedBuilder { return simple.NewDirectedGraph(0, 0) },
edges: []simple.Edge{
{F: simple.Node(0), T: simple.Node(1), W: 2},
{F: simple.Node(1), T: simple.Node(0), W: 1},
{F: simple.Node(1), T: simple.Node(2), W: 1},
},
merge: func(x, y float64, _, _ graph.Edge) float64 { return math.Min(x, y) },
want: mat64.NewSymDense(3, []float64{
0, math.Min(1, 2), 0,
math.Min(1, 2), 0, math.Min(1, 0),
0, math.Min(1, 0), 0,
}),
},
{
g: func() graph.DirectedBuilder { return simple.NewDirectedGraph(0, 0) },
edges: []simple.Edge{
{F: simple.Node(0), T: simple.Node(1), W: 2},
{F: simple.Node(1), T: simple.Node(0), W: 1},
{F: simple.Node(1), T: simple.Node(2), W: 1},
},
merge: func(x, y float64, xe, ye graph.Edge) float64 {
if xe == nil {
return y
}
if ye == nil {
return x
}
return math.Min(x, y)
},
want: mat64.NewSymDense(3, []float64{
0, math.Min(1, 2), 0,
math.Min(1, 2), 0, 1,
0, 1, 0,
}),
},
{
g: func() graph.DirectedBuilder { return simple.NewDirectedGraph(0, 0) },
edges: []simple.Edge{
{F: simple.Node(0), T: simple.Node(1), W: 2},
{F: simple.Node(1), T: simple.Node(0), W: 1},
{F: simple.Node(1), T: simple.Node(2), W: 1},
},
merge: func(x, y float64, _, _ graph.Edge) float64 { return math.Max(x, y) },
want: mat64.NewSymDense(3, []float64{
0, math.Max(1, 2), 0,
math.Max(1, 2), 0, math.Max(1, 0),
0, math.Max(1, 0), 0,
}),
},
}

func TestUndirect(t *testing.T) {
for _, test := range directedGraphs {
g := test.g()
for _, e := range test.edges {
g.SetEdge(e)
}

src := graph.Undirect{G: g, Absent: test.absent, Merge: test.merge}
dst := simple.NewUndirectedMatrixFrom(src.Nodes(), 0, 0, 0)
for _, u := range src.Nodes() {
for _, v := range src.From(u) {
dst.SetEdge(src.Edge(u, v))
}
}

if !mat64.Equal(dst.Matrix(), test.want) {
t.Errorf("unexpected result:\ngot:\n%.4v\nwant:\n%.4v",
mat64.Formatted(dst.Matrix()),
mat64.Formatted(test.want),
)
}
}
}

0 comments on commit 872be85

Please sign in to comment.