graph: imported graph as a subtree

This commit is contained in:
Brendan Tracey
2017-05-23 00:02:59 -06:00
158 changed files with 36849 additions and 0 deletions

1
graph/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
test.out

24
graph/.travis.yml Normal file
View File

@@ -0,0 +1,24 @@
sudo: false
language: go
# Versions of go that are explicitly supported by gonum.
go:
- 1.5.4
- 1.6.3
- 1.7.3
# Required for coverage.
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
# Get deps, build, test, and ensure the code is gofmt'ed.
# If we are building as gonum, then we have access to the coveralls api key, so we can run coverage as well.
script:
- go get -d -t -v ./...
- go build -v ./...
- go test -v -a ./...
- go test -v -a -tags appengine ./...
- test -z "$(gofmt -d .)"
- if [[ $TRAVIS_SECURE_ENV_VARS = "true" ]]; then bash ./.travis/test-coverage.sh; fi

35
graph/.travis/test-coverage.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
PROFILE_OUT=$PWD/profile.out
ACC_OUT=$PWD/acc.out
testCover() {
# set the return value to 0 (succesful)
retval=0
# get the directory to check from the parameter. Default to '.'
d=${1:-.}
# skip if there are no Go files here
ls $d/*.go &> /dev/null || return $retval
# switch to the directory to check
pushd $d > /dev/null
# create the coverage profile
coverageresult=`go test -v -coverprofile=$PROFILE_OUT`
# output the result so we can check the shell output
echo ${coverageresult}
# append the results to acc.out if coverage didn't fail, else set the retval to 1 (failed)
( [[ ${coverageresult} == *FAIL* ]] && retval=1 ) || ( [ -f $PROFILE_OUT ] && grep -v "mode: set" $PROFILE_OUT >> $ACC_OUT )
# return to our working dir
popd > /dev/null
# return our return value
return $retval
}
# Init acc.out
echo "mode: set" > $ACC_OUT
# Run test coverage on all directories containing go files
find . -maxdepth 10 -type d | while read d; do testCover $d || exit; done
# Upload the coverage profile to coveralls.io
[ -n "$COVERALLS_TOKEN" ] && goveralls -coverprofile=$ACC_OUT -service=travis-ci -repotoken $COVERALLS_TOKEN

15
graph/README.md Normal file
View File

@@ -0,0 +1,15 @@
# Gonum Graph [![Build Status](https://travis-ci.org/gonum/graph.svg?branch=master)](https://travis-ci.org/gonum/graph) [![Coverage Status](https://coveralls.io/repos/gonum/graph/badge.svg?branch=master&service=github)](https://coveralls.io/github/gonum/graph?branch=master) [![GoDoc](https://godoc.org/github.com/gonum/graph?status.svg)](https://godoc.org/github.com/gonum/graph)
This is a generalized graph package for the Go language. It aims to provide a clean, transparent API for common algorithms on arbitrary graphs such as finding the graph's strongly connected components, dominators, or searces.
The package is currently in testing, and the API is "semi-stable". The signatures of any functions like AStar are unlikely to change much, but the Graph, Node, and Edge interfaces may change a bit.
## Issues
If you find any bugs, feel free to file an issue on the github issue tracker. Discussions on API changes, added features, code review, or similar requests are preferred on the Gonum-dev Google Group.
https://groups.google.com/forum/#!forum/gonum-dev
## License
Please see github.com/gonum/license for general license information, contributors, authors, etc on the Gonum suite of packages.

248
graph/community/bisect.go Normal file
View File

@@ -0,0 +1,248 @@
// Copyright ©2016 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 community
import (
"errors"
"fmt"
"math"
"math/rand"
"github.com/gonum/graph"
)
// Interval is an interval of resolutions with a common score.
type Interval struct {
// Low and High delimit the interval
// such that the interval is [low, high).
Low, High float64
// Score is the score of the interval.
Score float64
// Reduced is the best scoring
// community membership found for the
// interval.
Reduced
}
// Reduced is a graph reduction.
type Reduced interface {
// Communities returns the community
// structure of the reduction.
Communities() [][]graph.Node
}
// Size is a score function that is the reciprocal of the number of communities.
func Size(g ReducedGraph) float64 { return 1 / float64(len(g.Structure())) }
// Weight is a score function that is the sum of community weights. The concrete
// type of g must be a pointer to a ReducedUndirected or a ReducedDirected, otherwise
// Weight will panic.
func Weight(g ReducedGraph) float64 {
var w float64
switch g := g.(type) {
case *ReducedUndirected:
for _, n := range g.nodes {
w += n.weight
}
case *ReducedDirected:
for _, n := range g.nodes {
w += n.weight
}
default:
panic(fmt.Sprintf("community: invalid graph type: %T", g))
}
return w
}
// ModularScore returns a modularized scoring function for Profile based on the
// graph g and the given score function. The effort parameter determines how
// many attempts will be made to get an improved score for any given resolution.
func ModularScore(g graph.Graph, score func(ReducedGraph) float64, effort int, src *rand.Rand) func(float64) (float64, Reduced) {
return func(resolution float64) (float64, Reduced) {
max := math.Inf(-1)
var best Reduced
for i := 0; i < effort; i++ {
r := Modularize(g, resolution, src)
s := score(r)
if s > max {
max = s
best = r
}
}
return max, best
}
}
// SizeMultiplex is a score function that is the reciprocal of the number of communities.
func SizeMultiplex(g ReducedMultiplex) float64 { return 1 / float64(len(g.Structure())) }
// WeightMultiplex is a score function that is the sum of community weights. The concrete
// type of g must be pointer to a ReducedUndirectedMultiplex or a ReducedDirectedMultiplex,
// otherwise WeightMultiplex will panic.
func WeightMultiplex(g ReducedMultiplex) float64 {
var w float64
switch g := g.(type) {
case *ReducedUndirectedMultiplex:
for _, n := range g.nodes {
for _, lw := range n.weights {
w += lw
}
}
case *ReducedDirectedMultiplex:
for _, n := range g.nodes {
for _, lw := range n.weights {
w += lw
}
}
default:
panic(fmt.Sprintf("community: invalid graph type: %T", g))
}
return w
}
// ModularMultiplexScore returns a modularized scoring function for Profile based
// on the graph g and the given score function. The effort parameter determines how
// many attempts will be made to get an improved score for any given resolution.
func ModularMultiplexScore(g Multiplex, weights []float64, all bool, score func(ReducedMultiplex) float64, effort int, src *rand.Rand) func(float64) (float64, Reduced) {
return func(resolution float64) (float64, Reduced) {
max := math.Inf(-1)
var best Reduced
for i := 0; i < effort; i++ {
r := ModularizeMultiplex(g, weights, []float64{resolution}, all, src)
s := score(r)
if s > max {
max = s
best = r
}
}
return max, best
}
}
// Profile returns an approximate profile of score values in the resolution domain [low,high)
// at the given granularity. The score is calculated by bisecting calls to fn. If log is true,
// log space bisection is used, otherwise bisection is linear. The function fn should be
// monotonically decreasing in at least 1/grain evaluations. Profile will attempt to detect
// non-monotonicity during the bisection.
//
// Since exact modularity optimization is known to be NP-hard and Profile calls modularization
// routines repeatedly, it is unlikely to return the exact resolution profile.
func Profile(fn func(float64) (float64, Reduced), log bool, grain, low, high float64) (profile []Interval, err error) {
if low >= high {
return nil, errors.New("community: zero or negative width domain")
}
defer func() {
r := recover()
e, ok := r.(nonDecreasing)
if ok {
err = e
return
}
if r != nil {
panic(r)
}
}()
left, comm := fn(low)
right, _ := fn(high)
for i := 1; i < int(1/grain); i++ {
rt, _ := fn(high)
right = math.Max(right, rt)
}
profile = bisect(fn, log, grain, low, left, high, right, comm)
// We may have missed some non-monotonicity,
// so merge low score discordant domains into
// their lower resolution neighbours.
return fixUp(profile), nil
}
type nonDecreasing int
func (n nonDecreasing) Error() string {
return fmt.Sprintf("community: profile does not reliably monotonically decrease: tried %d times", n)
}
func bisect(fn func(float64) (float64, Reduced), log bool, grain, low, scoreLow, high, scoreHigh float64, comm Reduced) []Interval {
if low >= high {
panic("community: zero or negative width domain")
}
if math.IsNaN(scoreLow) || math.IsNaN(scoreHigh) {
return nil
}
// Heuristically determine a reasonable number
// of times to try to get a higher value.
maxIter := int(1 / grain)
lowComm := comm
for n := 0; scoreLow < scoreHigh; n++ {
if n > maxIter {
panic(nonDecreasing(n))
}
scoreLow, lowComm = fn(low)
}
if scoreLow == scoreHigh || tooSmall(low, high, grain, log) {
return []Interval{{Low: low, High: high, Score: scoreLow, Reduced: lowComm}}
}
var mid float64
if log {
mid = math.Sqrt(low * high)
} else {
mid = (low + high) / 2
}
scoreMid := math.Inf(-1)
var midComm Reduced
for n := 0; scoreMid < scoreHigh; n++ {
if n > maxIter {
panic(nonDecreasing(n))
}
scoreMid, midComm = fn(mid)
}
lower := bisect(fn, log, grain, low, scoreLow, mid, scoreMid, lowComm)
higher := bisect(fn, log, grain, mid, scoreMid, high, scoreHigh, midComm)
for n := 0; lower[len(lower)-1].Score < higher[0].Score; n++ {
if n > maxIter {
panic(nonDecreasing(n))
}
lower[len(lower)-1].Score, lower[len(lower)-1].Reduced = fn(low)
}
if lower[len(lower)-1].Score == higher[0].Score {
higher[0].Low = lower[len(lower)-1].Low
lower = lower[:len(lower)-1]
if len(lower) == 0 {
return higher
}
}
return append(lower, higher...)
}
// fixUp non-monotonically decreasing interval scores.
func fixUp(profile []Interval) []Interval {
max := profile[len(profile)-1].Score
for i := len(profile) - 2; i >= 0; i-- {
if profile[i].Score > max {
max = profile[i].Score
continue
}
profile[i+1].Low = profile[i].Low
profile = append(profile[:i], profile[i+1:]...)
}
return profile
}
func tooSmall(low, high, grain float64, log bool) bool {
if log {
return math.Log(high/low) < grain
}
return high-low < grain
}

View File

@@ -0,0 +1,269 @@
// Copyright ©2016 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 community
import (
"fmt"
"log"
"sort"
"testing"
"github.com/gonum/graph/internal/ordered"
"github.com/gonum/graph/simple"
)
func ExampleProfile_simple() {
// Create dumbell graph:
//
// 0 4
// |\ /|
// | 2 - 3 |
// |/ \|
// 1 5
//
g := simple.NewUndirectedGraph(0, 0)
for u, e := range smallDumbell {
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
// Get the profile of internal node weight for resolutions
// between 0.1 and 10 using logarithmic bisection.
p, err := Profile(ModularScore(g, Weight, 10, nil), true, 1e-3, 0.1, 10)
if err != nil {
log.Fatal(err)
}
// Print out each step with communities ordered.
for _, d := range p {
comm := d.Communities()
for _, c := range comm {
sort.Sort(ordered.ByID(c))
}
sort.Sort(ordered.BySliceIDs(comm))
fmt.Printf("Low:%.2v High:%.2v Score:%v Communities:%v Q=%.3v\n",
d.Low, d.High, d.Score, comm, Q(g, comm, d.Low))
}
// Output:
// Low:0.1 High:0.29 Score:14 Communities:[[0 1 2 3 4 5]] Q=0.9
// Low:0.29 High:2.3 Score:12 Communities:[[0 1 2] [3 4 5]] Q=0.714
// Low:2.3 High:3.5 Score:4 Communities:[[0 1] [2] [3] [4 5]] Q=-0.31
// Low:3.5 High:10 Score:0 Communities:[[0] [1] [2] [3] [4] [5]] Q=-0.607
}
var friends, enemies *simple.UndirectedGraph
func init() {
friends = simple.NewUndirectedGraph(0, 0)
for u, e := range middleEast.friends {
// Ensure unconnected nodes are included.
if !friends.Has(simple.Node(u)) {
friends.AddNode(simple.Node(u))
}
for v := range e {
friends.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
enemies = simple.NewUndirectedGraph(0, 0)
for u, e := range middleEast.enemies {
// Ensure unconnected nodes are included.
if !enemies.Has(simple.Node(u)) {
enemies.AddNode(simple.Node(u))
}
for v := range e {
enemies.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: -1})
}
}
}
func ExampleProfile_multiplex() {
// The undirected graphs, friends and enemies, are the political relationships
// in the Middle East as described in the Slate article:
// http://www.slate.com/blogs/the_world_/2014/07/17/the_middle_east_friendship_chart.html
g, err := NewUndirectedLayers(friends, enemies)
if err != nil {
log.Fatal(err)
}
weights := []float64{1, -1}
// Get the profile of internal node weight for resolutions
// between 0.1 and 10 using logarithmic bisection.
p, err := Profile(ModularMultiplexScore(g, weights, true, WeightMultiplex, 10, nil), true, 1e-3, 0.1, 10)
if err != nil {
log.Fatal(err)
}
// Print out each step with communities ordered.
for _, d := range p {
comm := d.Communities()
for _, c := range comm {
sort.Sort(ordered.ByID(c))
}
sort.Sort(ordered.BySliceIDs(comm))
fmt.Printf("Low:%.2v High:%.2v Score:%v Communities:%v Q=%.3v\n",
d.Low, d.High, d.Score, comm, QMultiplex(g, comm, weights, []float64{d.Low}))
}
// Output:
// Low:0.1 High:0.72 Score:26 Communities:[[0] [1 7 9 12] [2 8 11] [3 4 5 10] [6]] Q=[24.7 1.97]
// Low:0.72 High:1.1 Score:24 Communities:[[0 6] [1 7 9 12] [2 8 11] [3 4 5 10]] Q=[16.9 14.1]
// Low:1.1 High:1.2 Score:18 Communities:[[0 2 6 11] [1 7 9 12] [3 4 5 8 10]] Q=[9.16 25.1]
// Low:1.2 High:1.6 Score:10 Communities:[[0 3 4 5 6 10] [1 7 9 12] [2 8 11]] Q=[11.4 24.1]
// Low:1.6 High:1.6 Score:8 Communities:[[0 1 6 7 9 12] [2 8 11] [3 4 5 10]] Q=[5.56 39.8]
// Low:1.6 High:1.8 Score:2 Communities:[[0 2 3 4 5 6 10] [1 7 8 9 11 12]] Q=[-1.82 48.6]
// Low:1.8 High:2.3 Score:-6 Communities:[[0 2 3 4 5 6 8 10 11] [1 7 9 12]] Q=[-5 57.5]
// Low:2.3 High:2.4 Score:-10 Communities:[[0 1 2 6 7 8 9 11 12] [3 4 5 10]] Q=[-11.2 79]
// Low:2.4 High:4.3 Score:-52 Communities:[[0 1 2 3 4 5 6 7 8 9 10 11 12]] Q=[-46.1 117]
// Low:4.3 High:10 Score:-54 Communities:[[0 1 2 3 4 6 7 8 9 10 11 12] [5]] Q=[-82 254]
}
func TestProfileUndirected(t *testing.T) {
for _, test := range communityUndirectedQTests {
g := simple.NewUndirectedGraph(0, 0)
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
fn := ModularScore(g, Weight, 10, nil)
p, err := Profile(fn, true, 1e-3, 0.1, 10)
if err != nil {
t.Errorf("%s: unexpected error: %v", test.name, err)
}
const tries = 1000
for i, d := range p {
var score float64
for i := 0; i < tries; i++ {
score, _ = fn(d.Low)
if score >= d.Score {
break
}
}
if score < d.Score {
t.Errorf("%s: failed to recover low end score: got: %v want: %v", test.name, score, d.Score)
}
if i != 0 && d.Score >= p[i-1].Score {
t.Errorf("%s: not monotonically decreasing: ", test.name, p[i-1], d)
}
}
}
}
func TestProfileDirected(t *testing.T) {
for _, test := range communityDirectedQTests {
g := simple.NewDirectedGraph(0, 0)
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
fn := ModularScore(g, Weight, 10, nil)
p, err := Profile(fn, true, 1e-3, 0.1, 10)
if err != nil {
t.Errorf("%s: unexpected error: %v", test.name, err)
}
const tries = 1000
for i, d := range p {
var score float64
for i := 0; i < tries; i++ {
score, _ = fn(d.Low)
if score >= d.Score {
break
}
}
if score < d.Score {
t.Errorf("%s: failed to recover low end score: got: %v want: %v", test.name, score, d.Score)
}
if i != 0 && d.Score >= p[i-1].Score {
t.Errorf("%s: not monotonically decreasing: ", test.name, p[i-1], d)
}
}
}
}
func TestProfileUndirectedMultiplex(t *testing.T) {
for _, test := range communityUndirectedMultiplexQTests {
g, weights, err := undirectedMultiplexFrom(test.layers)
if err != nil {
t.Errorf("unexpected error creating multiplex: %v", err)
continue
}
const all = true
fn := ModularMultiplexScore(g, weights, all, WeightMultiplex, 10, nil)
p, err := Profile(fn, true, 1e-3, 0.1, 10)
if err != nil {
t.Errorf("%s: unexpected error: %v", test.name, err)
}
const tries = 1000
for i, d := range p {
var score float64
for i := 0; i < tries; i++ {
score, _ = fn(d.Low)
if score >= d.Score {
break
}
}
if score < d.Score {
t.Errorf("%s: failed to recover low end score: got: %v want: %v", test.name, score, d.Score)
}
if i != 0 && d.Score >= p[i-1].Score {
t.Errorf("%s: not monotonically decreasing: ", test.name, p[i-1], d)
}
}
}
}
func TestProfileDirectedMultiplex(t *testing.T) {
for _, test := range communityDirectedMultiplexQTests {
g, weights, err := directedMultiplexFrom(test.layers)
if err != nil {
t.Errorf("unexpected error creating multiplex: %v", err)
continue
}
const all = true
fn := ModularMultiplexScore(g, weights, all, WeightMultiplex, 10, nil)
p, err := Profile(fn, true, 1e-3, 0.1, 10)
if err != nil {
t.Errorf("%s: unexpected error: %v", test.name, err)
}
const tries = 1000
for i, d := range p {
var score float64
for i := 0; i < tries; i++ {
score, _ = fn(d.Low)
if score >= d.Score {
break
}
}
if score < d.Score {
t.Errorf("%s: failed to recover low end score: got: %v want: %v", test.name, score, d.Score)
}
if i != 0 && d.Score >= p[i-1].Score {
t.Errorf("%s: not monotonically decreasing: ", test.name, p[i-1], d)
}
}
}
}

466
graph/community/louvain.tex Normal file
View File

@@ -0,0 +1,466 @@
% 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.
\documentclass{article}
\usepackage{amsmath,amsfonts}
\usepackage[margin=4cm]{geometry}
\title{Louvain algorithm for undirected and directed graphs}
\author{The {\tt gonum} Authors}
\begin{document}
\maketitle
The algorithm attempts to find communities (highly connected sub-graphs),
and it does this by minimising the modularity function
\begin{equation}
Q(c) = \frac{1}{2m}\sum_i\sum_j\left[ A_{ij} - \gamma \frac{k_ik_j}{2m} \right] \delta_{ij}(c),
\end{equation}
where $c$ is a partition of nodes into subsets or communities,
$A_{ij}$ is the edge weight between nodes $i$ and $j$,
$\gamma$ is a tuning parameter,
\begin{equation}
m = \frac{1}{2}\sum_i\sum_jA_{ij},
\end{equation}
\begin{equation}
k_i = \sum_j{A_{ij}},
\end{equation}
and
\begin{equation}
\delta_{ij}(c) = \left \{ \begin{array}{ll}
1 & \text{if} \quad c(i) = c(j) \\
0 & \text{otherwise} \end{array} \right ..
\end{equation}
Here $c(i)$ denotes the community to which node $i$ belongs
in the partitioning $c$.
The algorithm finds a hierarchical community structure by iterating
between two phases:
\begin{enumerate}
\item Find a set of communities that minimise $Q$.
\item Construct a new graph, whose nodes are the communities
found in the preceding phase one step.
\end{enumerate}
Each iteration of these two phases is called a `pass'.
In this way, the algorithm obtains a nested community structure,
where at each level $Q$ is minimised for the relevant graph.
We consider this process in more detail, in particular looking
at phase one first in the first pass, when each node is a single
node, and then how this generalises to later passes when each node
is a community.
\section{Undirected Graphs}
\subsection{Initial Pass}
\label{sec:initialPass}
The initial pass is simple as the initial pass uses the original graph,
and in all following passes graphs constructed in the previous pass's
phase two are used.
Here we will consider this initial simple formulation for phase one, and
in Section~\ref{sec:laterPasses} we consider how this generalises for
passes two and onwards.
Phase one works by initially allocating each node to a separate community,
and then iterating through each node $a$ and checking if moving it into
a different community $\beta$ will reduce $Q$.
If there are possible moves that will reduce $Q$, $a$ is moved into the
the community which will generate the largest reduction in $Q$.
This process is continued until there are no moves left to reduce $Q$
further, meaning a local minimum for $Q$ has been achieved.
Then the algorithm moves to phase two (constructing a new graph where
each node in the new graph is a community in the old graph).
Note that we assume the original graph to be simple and undirected.
First, we introduce some notation that will be useful:
Let $c(i)$ denote the community to which node $i$ belongs,
and let $\alpha$ be the community that the node $a$ mentioned above
belongs to, i.e., $\alpha = c_a$.
Then we define
\newcommand{\Stot}[1]{\Sigma_{\text{tot}}^{#1}}
\begin{equation}
\Stot{\alpha} = \sum_{i \in \alpha}\sum_{j}A_{ij} = \sum_{i \in \alpha}k_i,
\end{equation}
\newcommand{\kin}[2]{k_{#1}^{#2}}
\begin{equation}
\kin{i}{\alpha} = \sum_{j \in \alpha}A_{ij},
\end{equation}
and
\newcommand{\Sin}[1]{\Sigma_{\text{in}}^{#1}}
\begin{equation}
\Sin{\alpha} = \sum_{i \in \alpha}\sum_{j \in \alpha}A_{ij} = \sum_{i \in \alpha}\kin{i}{\alpha}.
\end{equation}
We are interested in how $Q$ will change if we move a node $a$ from its
current community $\alpha$, to a new community $\beta$.
This will have two effects, it will remove the terms from $Q$
related to $a$ in $\alpha$, which we will call $Q^-$ and it will add terms
related to $a$ in $\beta$, which we will call $Q^+$.
The total change in $Q$ caused by the movement of $a$ from $\alpha$ to $\beta$ is
\begin{equation}
\Delta Q = Q^{+} - Q^{-},
\end{equation}
where
\begin{align*}
Q^- &= \frac{1}{2m}\left[ \left( A_{aa} - \gamma \frac{k_a^2}{2m} \right)
+ 2\sum_{i \in \alpha, \, i \neq a} \left( A_{ia} - \gamma \frac{k_ik_a}{2m} \right) \right] \\
&= \frac{1}{2m}\left[ \left( A_{aa} - \gamma \frac{k_a^2}{2m} \right)
+ 2 \left( \kin{a}{\alpha} -A_{aa}\right) - \gamma \frac{2k_a}{2m}\sum_{i \in \alpha, \, i \neq a} k_i \right] \\
&= \frac{1}{2m}\left[ \left( A_{aa} - \gamma \frac{k_a^2}{2m} \right)
+ 2 \left( \kin{a}{\alpha} -A_{aa}\right) - \gamma \frac{2k_a}{2m}\left( \Stot{\alpha} - k_a \right) \right], \\
\end{align*}
and
\begin{align*}
Q^+ &= \frac{1}{2m}\left[ \left( A_{aa} - \gamma \frac{k_a^2}{2m} \right)
+ 2\sum_{i \in \beta} \left( A_{ia} - \gamma \frac{k_ik_a}{2m} \right) \right] \\
&= \frac{1}{2m}\left[ \left( A_{aa} - \gamma \frac{k_a^2}{2m} \right)
+ 2\kin{a}{\beta} - \gamma \frac{2k_a}{2m}\sum_{i \in \beta} k_i \right] \\
&= \frac{1}{2m}\left[ \left( A_{aa} - \gamma \frac{k_a^2}{2m} \right)
+ 2\kin{a}{\beta} - \gamma \frac{2k_a\Stot{\beta}}{2m} \right]. \\
\end{align*}
The first term in both these expressions ($Q^-$ and $Q^+$) is the same, and so cancels:
\begin{equation}
\Delta Q = \frac{1}{2m}\left[ \left( 2\kin{a}{\beta} - \gamma \frac{2k_a\Stot{\beta}}{2m} \right)
- \left( 2 \left( \kin{a}{\alpha} -A_{aa}\right) - \gamma \frac{2k_a}{2m}\left( \Stot{\alpha} - k_a \right) \right) \right].
\end{equation}
\subsection{Later Passes}
\label{sec:laterPasses}
In phase two a `meta-graph' is constructed where nodes correspond to
the communities found in the preceding phase one step, and edge weight
between two such communities (nodes, in the meta-graph)
$\alpha$ and $\beta$ are defined to be
\begin{equation}
A_{\alpha \beta}^* = \sum_{i \in \alpha}\sum_{j \in \beta}A_{ij}.
\label{eqn:Aij*}
\end{equation}
Note that $i$ and $j$ refer to nodes in the original graph, not nodes
in the previous graph, and so holds any meta-graph, not just the first.
Also note that this definition of $A^*_{\alpha \beta}$ allows for
$A^*_{\alpha \alpha}$ to be non-zero as
\begin{equation}
A_{\alpha \alpha}^* = \sum_{i \in \alpha}\sum_{j \in \alpha}A_{ij} = \Sin{\alpha}.
\end{equation}
In this newly constructed graph, $\alpha$ and $\beta$ are nodes, but
also refer to communities (sets of nodes) in the original graph, and I
use these two interpretations interchangeably.
This should be the only ambiguous bit of notation in this document, I hope.
The results of Section~\ref{sec:initialPass} generalise to these meta-graphs,
and the generalised results mirror those of Section~\ref{sec:initialPass} closely
-- I distinguish the new results from those of Section~\ref{sec:initialPass} by a
superscript $*$.
I use $i$ and $j$ to denote nodes of the original graph as in Section~\ref{sec:initialPass},
and use $z$ and $w$ to denote nodes of the meta-graph (communities of the original).
I use analogous notation to Section~\ref{sec:initialPass}, $c^*(z)$, to
denote the community to which node $z$ of the meta-graph belongs,
and let $\mathfrak{a}$ be the community that the node $\alpha$ belongs to
($c^*(\alpha) = \mathfrak{a}$), i.e.
\begin{equation}
\mathfrak{a} = \{z | c^*(z) = c^*(\alpha) \}.
\end{equation}
Given this notation, we can observe that
\begin{equation}
m^* = \frac{1}{2}\sum_{z}\sum_{w}{A_{zw}^*} = \frac{1}{2}\sum_{z}\sum_{w}{\sum_{i \in z}\sum_{j \in w}A_{ij}} = \frac{1}{2}\sum_i\sum_jA_{ij} = m,
\end{equation}
\begin{equation}
k_{z}^* = \sum_{w}{A_{zw}^*} = \sum_{w}{\sum_{i \in z}\sum_{j \in w}A_{ij}} = \sum_{i \in z}\sum_{j}A_{ij} = \Stot{z},
\end{equation}
\begin{equation}
\Stot{\mathfrak{a} *} = \sum_{z \in \mathfrak{a}}\sum_{w}A_{zw}^* = \sum_{z \in \mathfrak{a}}k_z^* = \sum_{z \in \mathfrak{a}}\Stot{z},
\end{equation}
\begin{equation}
\kin{z}{\mathfrak{a} *} = \sum_{w \in \mathfrak{a}}{A_{zw}^*} = \sum_{w \in \mathfrak{a}}{\sum_{i \in z}\sum_{j \in w}A_{ij}},
\end{equation}
and
\begin{equation}
\Sin{\mathfrak{a} *} = \sum_{z \in \mathfrak{a}}\sum_{w \in \mathfrak{a}}A_{zw}^* = \sum_{z \in \mathfrak{a}}\kin{z}{\mathfrak{a} *} = \sum_{z \in \mathfrak{a}}\sum_{w \in \mathfrak{a}}{\sum_{i \in z}\sum_{j \in w}A_{ij}}.
%\label{eqn:Sin}
\end{equation}
If we let $\mathfrak{b}$ denote the community to which we are considering moving $\alpha$,
then the expression for $\Delta Q$ from Section~\ref{sec:initialPass} trivially generalises to
\begin{equation}
\Delta Q = \frac{1}{2m}\left[ \left( 2 \kin{\alpha}{\mathfrak{b} *} - \gamma \frac{2k_{\alpha}^*\Stot{\mathfrak{b} *}}{2m} \right)
- \left( 2\left( \kin{\alpha}{\mathfrak{a} *} - A_{\alpha \alpha}^* \right) - \gamma \frac{2k_{\alpha}^*}{2m} \left( \Stot{\mathfrak{a} *} - k_{\alpha}^* \right ) \right) \right] \\
\end{equation}
\section{Directed Graphs}
\label{sec:directedGraphs}
It is of interest to consider how this generalises to directed graphs.
If we are to treat incoming and outgoing nodes equally, there are several
thoughts on how to extend the algorithm to directed graphs, of which we
will explore three:
\begin{itemize}
\item Construct an undirected graph first, and then use the undirected case.
\item Generalise the expressions from the undirected case to the directed case,
we will consider two different suggestions for such generalisations.
\end{itemize}
We will show that one of the two `generalisation of expressions' approaches is
equivalent to constructing an undirected graph, and the other is not.
\subsection{Construction of an undirected graph}
A simple approach to generalising to directed graphs is to construct
an undirected graph with edge weights
\begin{equation}
A_{ij} = B_{ij} + B_{ji},
\label{eqn:undirectedAB}
\end{equation}
and simply use the undirected algorithm.
Another suggestion is to average the directed edges to make
an undirected graph, i.e. to use a directed graph with edge weights
\begin{equation}
A_{ij} = \frac{B_{ij} + B_{ji}}{2}.
\end{equation}
This raises an important question: does scaling all edge weights across
the entire graph by a constant affect the results of the algorithm?
Hopefully not, but worth checking.
We can follow this through the results for the undirected graph by
substituting $A_{ij}^{(1)} = pA_{ij}$, $p \in \mathbb{R}$, and
distinguishing the new expressions by a superscript ${(1)}$. These
new expressions are:
\begin{equation}
m^{(1)} = \frac{1}{2}\sum_i\sum_jpA_{ij} = p\frac{1}{2}\sum_i\sum_j A_{ij} = pm ,
\end{equation}
\begin{equation}
k_i^{(1)} = \sum_j{pA_{ij}} = p\sum_j{A_{ij}} = pk_i,
\end{equation}
and so
\begin{align*}
Q^{(1)}(c) &= \frac{1}{2pm}\sum_i\sum_j\left[ pA_{ij} - \gamma \frac{pk_ipk_j}{2pm} \right] \delta_{ij}(c) \\
&= \frac{1}{2m}\sum_i\sum_j\left[ A_{ij} - \gamma \frac{k_ik_j}{2m} \right] \delta_{ij}(c) \\
&= Q(c)
\end{align*}
Note that as we have shown $Q^{(1)} = Q$ there is no need to go into the remainder of the terms
involved in the algorithm, as they all derive from $Q$.
\subsection{First generalisation of expressions approach}
One suggested extension to directed graphs is to modify the expressions
involved by adding the `from' case and the `to' case for each term.
If we let $B_{ij}$ be the edge weight between nodes $i$ and $j$ in
the directed graph, and distinguishing these extended expressions by
a superscript $(2)$, the extended expressions become:
\begin{equation}
m^{(2)} = \frac{1}{2}\left ( \sum_i\sum_jB_{ij} + \sum_i\sum_jB_{ji}\right) = \frac{1}{2}\sum_i\sum_j \left( B_{ij} + B_{ji} \right) ,
\end{equation}
\begin{equation}
k_i^{(2)} = \sum_jB_{ij} + \sum_jB_{ji} = \sum_j{\left( B_{ij} + B_{ji} \right)},
\end{equation}
and similarly
\begin{equation}
Q^{(2)}(c) = \frac{1}{2m}\sum_i\sum_j\left[ \left( B_{ij} + B_{ji} \right) - \gamma \frac{k_i^{(2)}k_j^{(2)}}{2m} \right] \delta_{ij}(c).
\end{equation}
Note how this is equivalent to the construction of an undirected graph as
per Equation~(\ref{eqn:undirectedAB}). Similarly to above,
there is no need to go into the remainder of the terms
involved in the algorithm, as they all derive from $Q$.
\subsection{Second generalisation of expressions approach}
Another approach to generalising the expressions to the
directed case, that still treats incoming and outgoing edges
as equally important, is to propose an alternative modularity
expression:
\newcommand{\dkin}[1]{k_{#1}^{\text{in}}}
\newcommand{\dkout}[1]{k_{#1}^{\text{out}}}
\begin{equation}
Q^{(3)}(c) = \frac{1}{2m}\sum_i\sum_j\left[ 2B_{ij} - 2\gamma \frac{\dkin{i}\dkout{j}}{2m} \right] \delta_{ij}(c), \\
\end{equation}
where
\begin{equation}
\dkout{i} = \sum_j{B_{ij}}
\quad \quad \text{and} \quad \quad
\dkin{i} = \sum_j{B_{ji}},
\end{equation}
so $k_i^{(2)} = \dkin{i} + \dkout{i}$.
Note I leave the factor of two in the expression for $Q^{(3)}$ so that it
remains as comparable to that for $Q^{(2)}$ as possible.
There is no need for alternative $m$, as it will still be the same as above.
$Q^{(3)}$ will differ from $Q^{(2)}$ in two ways.
Firstly, as $k_i^{(2)} = \dkin{i} + \dkout{i}$,
\begin{align*}
\sum_i\sum_j k_i^{(2)} k_j^{(2)} \delta_{ij}(c) &= \sum_i\sum_j (\dkin{i} + \dkout{i}) (\dkin{j} + \dkout{j}) \delta_{ij}(c) \\
&= \sum_i\sum_j \left[ (\dkin{i}\dkin{j} + \dkout{i}\dkout{j}) + (\dkin{i}\dkout{j} + \dkin{j}\dkout{i}) \right] \delta_{ij}(c). \\
&= \sum_i\sum_j \left[ (\dkin{i}\dkin{j} + \dkout{i}\dkout{j}) + 2\dkin{i}\dkout{j} \right] \delta_{ij}(c), \\
\end{align*}
and similarly,
\begin{equation}
\sum_i\sum_j \left( B_{ij} + B_{ji} \right) \delta_{ij}(c) = 2\sum_i\sum_j B_{ij} \delta_{ij}(c).
\end{equation}
From these two expressions, we can see that
\begin{equation}
Q^{(3)} - Q^{(2)} = \frac{1}{2m}\sum_i\sum_j \gamma \frac{\dkin{i}\dkin{j} + \dkout{i}\dkout{j}}{2m} \delta_{ij}(c).
\end{equation}
\section{Directed Graphs in more detail}
\label{sec:directedGraphsDetail}
In Section \ref{sec:directedGraphs} we essentially showed three
things:
\begin{itemize}
\item How an undirected graph could be constructed from a directed
graph, thereby allowing the undirected algorithm to be used for
directed graphs.
\item How scaling all edge weights by a non-zero constant would not
affect the modularity function.
\item An alternative approach to extending the algorithm to
directed graphs that is not equivalent to first reducing it
to an undirected graph.
\end{itemize}
It is this third point that we will explore here.
Analogously to Sections \ref{sec:initialPass} and \ref{sec:laterPasses} we will
break this up into the initial pass and the later passes.
\subsection{Initial pass}
\label{sec:initialPassDirected}
Continuing with the notation of Section \ref{sec:initialPass}, in which
$c(i)$ denotes the community to which node $i$ belongs,
and $\alpha = c(a)$, we define
\newcommand{\dinStot}[1]{\Sigma_{\text{tot}}^{\text{in}(#1)}}
\newcommand{\doutStot}[1]{\Sigma_{\text{tot}}^{\text{out}(#1)}}
\begin{equation}
\doutStot{\alpha} = \sum_{i \in \alpha}\sum_{j}B_{ij} = \sum_{i \in \alpha}\dkout{i}
\quad \quad \text{and} \quad \quad
\dinStot{\alpha} = \sum_{i \in \alpha}\sum_{j}B_{ji} = \sum_{i \in \alpha}\dkin{i},
\end{equation}
\newcommand{\dinkin}[2]{k_{#1}^{\text{in}(#2)}}
\newcommand{\doutkin}[2]{k_{#1}^{\text{out}(#2)}}
\begin{equation}
\doutkin{i}{\alpha} = \sum_{j \in \alpha}B_{ij}
\quad \quad \text{and} \quad \quad
\dinkin{i}{\alpha} = \sum_{j \in \alpha}B_{ji},
\end{equation}
and we will entertain one more ambiguous notation choice:
%\newcommand{\Sin}[1]{\Sigma_{\text{in}}^{#1}}
\begin{equation}
\Sin{\alpha} = \sum_{i \in \alpha}\sum_{j \in \alpha}B_{ij} = \sum_{i \in \alpha}\doutkin{i}{\alpha} = \sum_{i \in \alpha}\dinkin{i}{\alpha}.
\end{equation}
Analogously to Section \ref{sec:initialPass}, we are interested in how
$Q^{(3)}$ will change if we move a node $a$ from its
current community $\alpha$, to a new community $\beta$,
and analogously this will have two effects -- it will remove the terms
from $Q^{(3)}$ related to $a$ in $\alpha$, which we will call $Q^{-(3)}$
and it will add terms related to $a$ in $\beta$, which we will call $Q^{+(3)}$.
The total change in $Q^{(3)}$ caused by the movement of $a$ from $\alpha$ to $\beta$ is
\begin{equation}
\Delta Q^{(3)} = Q^{+(3)} - Q^{-(3)},
\end{equation}
where
\begin{align*}
Q^{-(3)} &= \frac{1}{2m}\left[ \left( 2B_{aa} - 2\gamma \frac{\dkin{a}\dkout{a}}{2m} \right)
+ \sum_{i \in \alpha, \, i \neq a} \left( 2B_{ia} + 2B_{ai} - 2\gamma \frac{\dkin{i}\dkout{a}}{2m} - 2\gamma \frac{\dkin{a}\dkout{i}}{2m} \right) \right] \\
&= \frac{1}{2m}\left[ \left( 2B_{aa} - 2\gamma \frac{\dkin{a}\dkout{a}}{2m} \right)
+ 2(\dinkin{a}{\alpha} - B_{aa}) + 2(\doutkin{a}{\alpha} - B_{aa}) \hdots \right . \\
& \quad \quad \quad \quad \quad \quad \left .
- \frac{2\gamma\dkout{a}}{2m} (\dinStot{\alpha} - \dkin{a}) - \frac{2\gamma\dkin{a}}{2m} (\doutStot{\alpha} - \dkout{a}) \right] \\
\end{align*}
and
\begin{align*}
Q^{+(3)} &= \frac{1}{2m}\left[ \left( 2B_{aa} - 2\gamma \frac{\dkin{a}\dkout{a}}{2m} \right)
+ \sum_{i \in \beta} \left( 2B_{ia} + 2B_{ai} - 2\gamma \frac{\dkin{i}\dkout{a}}{2m} - 2\gamma \frac{\dkin{a}\dkout{i}}{2m} \right) \right] \\
&= \frac{1}{2m}\left[ \left( 2B_{aa} - 2\gamma \frac{\dkin{a}\dkout{a}}{2m} \right)
+ 2\dinkin{a}{\beta} + 2\doutkin{a}{\beta} - \frac{2\gamma\dkout{a}}{2m} \dinStot{\beta} - \frac{2\gamma\dkin{a}}{2m} \doutStot{\beta} \right] \\
\end{align*}
Similarly to Section \ref{sec:initialPass}, the first term in both these expressions is the same, and so cancels, leaving:
\begin{align*}
\Delta Q^{(3)} &= \frac{2}{2m}\left[
\left( \dinkin{a}{\beta} + \doutkin{a}{\beta} - \frac{\gamma\dkout{a}}{2m} \dinStot{\beta} - \frac{\gamma\dkin{a}}{2m} \doutStot{\beta} \right) \right. \\
& \hspace{-1cm}
- \left. \left( (\dinkin{a}{\alpha} - B_{aa}) + (\doutkin{a}{\alpha} - B_{aa}) - \frac{\gamma\dkout{a}}{2m} (\dinStot{\alpha} - \dkin{a}) - \frac{\gamma\dkin{a}}{2m} (\doutStot{\alpha} - \dkout{a}) \right) \right] \\
&= \frac{2}{2m}\left[ (\dinkin{a}{\beta}-\dinkin{a}{\alpha}) + (\doutkin{a}{\beta}-\doutkin{a}{\alpha}) + 2B_{aa} \right. \\
& \hspace{-1cm} \left.
- \frac{\gamma\dkout{a}}{2m} (\dinStot{\beta}-\dinStot{\alpha}) - \frac{\gamma\dkin{a}}{2m} (\doutStot{\beta} - \doutStot{\alpha}) - \frac{2\gamma\dkin{a}\dkout{a}}{2m} \right]
\end{align*}
\subsection{Later passes}
\label{sec:laterPassesDirected}
In phase two a `meta-graph' is constructed where nodes correspond to
the communities found in the preceding phase one step, and edge weight
between two such communities (nodes, in the meta-graph)
$\alpha$ and $\beta$ are defined to be
\begin{equation}
B_{\alpha \beta}^* = \sum_{i \in \alpha}\sum_{j \in \beta}B_{ij}.
\label{eqn:Bij*}
\end{equation}
Note that $i$ and $j$ refer to nodes in the original graph, not nodes
in the previous graph, and so holds any meta-graph, not just the first.
Also note that this definition of $B^*_{\alpha \beta}$ allows for
$B^*_{\alpha \alpha}$ to be non-zero, in fact
\begin{equation}
B_{\alpha \alpha}^* = \sum_{i \in \alpha}\sum_{j \in \alpha}B_{ij} = \Sin{\alpha}.
\end{equation}
In this newly constructed graph, $\alpha$ and $\beta$ are nodes, but
also refer to communities (sets of nodes) in the original graph, and I
use these two interpretations interchangeably, completely analogously to
Section \ref{sec:laterPasses}.
The results of Section~\ref{sec:initialPassDirected} generalise to these meta-graphs,
and the generalised results mirror those of Section~\ref{sec:initialPassDirected} closely
-- I distinguish the new results from those of Section~\ref{sec:initialPassDirected} by a
superscript $*$.
I use $i$ and $j$ to denote nodes of the original graph as in Sections~\ref{sec:initialPass}
and \ref{sec:initialPassDirected},
and use $z$ and $w$ to denote nodes of the meta-graph (communities of the original).
I use analogous notation to Section~\ref{sec:initialPass}, $c^*(z)$, to
denote the community to which node $z$ of the meta-graph belongs,
and let $\mathfrak{a}$ be the community that the node $\alpha$ belongs to,
i.e., $\mathfrak{a} = c^*(\alpha) $.
Given this notation, we get all the same results as in \ref{sec:laterPasses}, but
each split into two cases `out' and `in', separating by direction, essentially, so
\newcommand{\dkinStar}[1]{k_{#1}^{\text{in} *}}
\newcommand{\dkoutStar}[1]{k_{#1}^{\text{out} *}}
\begin{equation}
\dkoutStar{z} = \sum_w{B_{zw}^*} = \sum_w\sum_{i \in z}\sum_{j \in w}B_{ij} = \sum_{i \in z}\sum_jB_{ij} = \doutStot{z},
\end{equation}
\begin{equation}
\dkinStar{z} = \sum_w{B_{wz}^*} = \sum_w\sum_{i \in z}\sum_{j \in w}B_{ji} = \sum_{i \in z}\sum_jB_{ji} = \dinStot{z},
\end{equation}
\newcommand{\dinStotStar}[1]{\Sigma_{\text{tot}}^{\text{in}(#1) *}}
\newcommand{\doutStotStar}[1]{\Sigma_{\text{tot}}^{\text{out}(#1) *}}
\begin{equation}
\doutStotStar{\mathfrak{a}} = \sum_{z \in \mathfrak{a}}\sum_{w}B_{zw}^* = \sum_{z \in \mathfrak{a}}\dkoutStar{z} = \sum_{z \in \mathfrak{a}}\doutStot{z},
\end{equation}
\begin{equation}
\dinStotStar{\mathfrak{a}} = \sum_{z \in \mathfrak{a}}\sum_{w}B_{wz}^* = \sum_{z \in \mathfrak{a}}\dkinStar{z} = \sum_{z \in \mathfrak{a}}\dinStot{z},
\end{equation}
\newcommand{\dinkinStar}[2]{k_{#1}^{\text{in}(#2) *}}
\newcommand{\doutkinStar}[2]{k_{#1}^{\text{out}(#2) *}}
\begin{equation}
\doutkinStar{z}{\mathfrak{a}} = \sum_{w \in \mathfrak{a}}{B_{zw}^*} = \sum_{w \in \mathfrak{a}}{\sum_{i \in z}\sum_{j \in w}B_{ij}},
\end{equation}
\begin{equation}
\dinkinStar{z}{\mathfrak{a}} = \sum_{w \in \mathfrak{a}}{B_{wz}^*} = \sum_{w \in \mathfrak{a}}{\sum_{i \in z}\sum_{j \in w}B_{ji}},
\end{equation}
and
\begin{equation}
\Sin{\mathfrak{a} *} = \sum_{z \in \mathfrak{a}}\sum_{w \in \mathfrak{a}}A_{zw}^* = \sum_{z \in \mathfrak{a}}\kin{z}{\mathfrak{a} *} = \sum_{z \in \mathfrak{a}}\sum_{w \in \mathfrak{a}}{\sum_{i \in z}\sum_{j \in w}A_{ij}}.
%\label{eqn:Sin}
\end{equation}
If we let $\mathfrak{b}$ denote the community to which we are considering moving $\alpha$,
then the expression for $\Delta Q$ from Section~\ref{sec:initialPassDirected} simply generalises as
\begin{align*}
\Delta Q^{(3)} &= \frac{2}{2m}\left[ (\dinkinStar{\alpha}{\mathfrak{b}}-\dinkinStar{\alpha}{\mathfrak{a}}) + (\doutkinStar{\alpha}{\mathfrak{b}}-\doutkinStar{\alpha}{\mathfrak{a}}) + 2B_{\alpha\alpha}^* \right. \\
& \hspace{-1cm} \left.
- \frac{\gamma\dkoutStar{\alpha}}{2m} (\dinStotStar{\mathfrak{b}}-\dinStotStar{\mathfrak{a}}) - \frac{\gamma\dkinStar{\alpha}}{2m} (\doutStotStar{\mathfrak{b}} - \doutStotStar{\mathfrak{a}}) - \frac{2\gamma\dkinStar{\alpha}\dkoutStar{\alpha}}{2m} \right]
\end{align*}
\end{document}

View File

@@ -0,0 +1,377 @@
// 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 community provides graph community detection functions.
package community
import (
"fmt"
"math/rand"
"github.com/gonum/graph"
)
// Q returns the modularity Q score of the graph g subdivided into the
// given communities at the given resolution. If communities is nil, the
// unclustered modularity score is returned. The resolution parameter
// is γ as defined in Reichardt and Bornholdt doi:10.1103/PhysRevE.74.016110.
// Q will panic if g has any edge with negative edge weight.
//
// If g is undirected, Q is calculated according to
// Q = 1/2m \sum_{ij} [ A_{ij} - (\gamma k_i k_j)/2m ] \delta(c_i,c_j),
// If g is directed, it is calculated according to
// Q = 1/m \sum_{ij} [ A_{ij} - (\gamma k_i^in k_j^out)/m ] \delta(c_i,c_j).
//
// graph.Undirect may be used as a shim to allow calculation of Q for
// directed graphs with the undirected modularity function.
func Q(g graph.Graph, communities [][]graph.Node, resolution float64) float64 {
switch g := g.(type) {
case graph.Undirected:
return qUndirected(g, communities, resolution)
case graph.Directed:
return qDirected(g, communities, resolution)
default:
panic(fmt.Sprintf("community: invalid graph type: %T", g))
}
}
// ReducedGraph is a modularised graph.
type ReducedGraph interface {
graph.Graph
// Communities returns the community memberships
// of the nodes in the graph used to generate
// the reduced graph.
Communities() [][]graph.Node
// Structure returns the community structure of
// the current level of the module clustering.
// Each slice in the returned value recursively
// describes the membership of a community at
// the current level by indexing via the node
// ID into the structure of the non-nil
// ReducedGraph returned by Expanded, or when the
// ReducedGraph is nil, by containing nodes
// from the original input graph.
//
// The returned value should not be mutated.
Structure() [][]graph.Node
// Expanded returns the next lower level of the
// module clustering or nil if at the lowest level.
//
// The returned ReducedGraph will be the same
// concrete type as the receiver.
Expanded() ReducedGraph
}
// Modularize returns the hierarchical modularization of g at the given resolution
// using the Louvain algorithm. If src is nil, rand.Intn is used as the random
// generator. Modularize will panic if g has any edge with negative edge weight.
//
// If g is undirected it is modularised to minimise
// Q = 1/2m \sum_{ij} [ A_{ij} - (\gamma k_i k_j)/2m ] \delta(c_i,c_j),
// If g is directed it is modularised to minimise
// Q = 1/m \sum_{ij} [ A_{ij} - (\gamma k_i^in k_j^out)/m ] \delta(c_i,c_j).
//
// The concrete type of the ReducedGraph will be a pointer to either a
// ReducedUndirected or a ReducedDirected depending on the type of g.
//
// graph.Undirect may be used as a shim to allow modularization of
// directed graphs with the undirected modularity function.
func Modularize(g graph.Graph, resolution float64, src *rand.Rand) ReducedGraph {
switch g := g.(type) {
case graph.Undirected:
return louvainUndirected(g, resolution, src)
case graph.Directed:
return louvainDirected(g, resolution, src)
default:
panic(fmt.Sprintf("community: invalid graph type: %T", g))
}
}
// Multiplex is a multiplex graph.
type Multiplex interface {
// Nodes returns the slice of nodes
// for the multiplex graph.
// All layers must refer to the same
// set of nodes.
Nodes() []graph.Node
// Depth returns the number of layers
// in the multiplex graph.
Depth() int
}
// QMultiplex returns the modularity Q score of the multiplex graph layers
// subdivided into the given communities at the given resolutions and weights. Q is
// returned as the vector of weighted Q scores for each layer of the multiplex graph.
// If communities is nil, the unclustered modularity score is returned.
// If weights is nil layers are equally weighted, otherwise the length of
// weights must equal the number of layers. If resolutions is nil, a resolution
// of 1.0 is used for all layers, otherwise either a single element slice may be used
// to specify a global resolution, or the length of resolutions must equal the number
// of layers. The resolution parameter is γ as defined in Reichardt and Bornholdt
// doi:10.1103/PhysRevE.74.016110.
// QMultiplex will panic if the graph has any layer weight-scaled edge with
// negative edge weight.
//
// If g is undirected, Q is calculated according to
// Q_{layer} = w_{layer} \sum_{ij} [ A_{layer}*_{ij} - (\gamma_{layer} k_i k_j)/2m_{layer} ] \delta(c_i,c_j),
// If g is directed, it is calculated according to
// Q_{layer} = w_{layer} \sum_{ij} [ A_{layer}*_{ij} - (\gamma_{layer} k_i^in k_j^out)/m_{layer} ] \delta(c_i,c_j).
//
// Note that Q values for multiplex graphs are not scaled by the total layer edge weight.
//
// graph.Undirect may be used as a shim to allow calculation of Q for
// directed graphs.
func QMultiplex(g Multiplex, communities [][]graph.Node, weights, resolutions []float64) []float64 {
if weights != nil && len(weights) != g.Depth() {
panic("community: weights vector length mismatch")
}
if resolutions != nil && len(resolutions) != 1 && len(resolutions) != g.Depth() {
panic("community: resolutions vector length mismatch")
}
switch g := g.(type) {
case UndirectedMultiplex:
return qUndirectedMultiplex(g, communities, weights, resolutions)
case DirectedMultiplex:
return qDirectedMultiplex(g, communities, weights, resolutions)
default:
panic(fmt.Sprintf("community: invalid graph type: %T", g))
}
}
// ReducedMultiplex is a modularised multiplex graph.
type ReducedMultiplex interface {
Multiplex
// Communities returns the community memberships
// of the nodes in the graph used to generate
// the reduced graph.
Communities() [][]graph.Node
// Structure returns the community structure of
// the current level of the module clustering.
// Each slice in the returned value recursively
// describes the membership of a community at
// the current level by indexing via the node
// ID into the structure of the non-nil
// ReducedGraph returned by Expanded, or when the
// ReducedGraph is nil, by containing nodes
// from the original input graph.
//
// The returned value should not be mutated.
Structure() [][]graph.Node
// Expanded returns the next lower level of the
// module clustering or nil if at the lowest level.
//
// The returned ReducedGraph will be the same
// concrete type as the receiver.
Expanded() ReducedMultiplex
}
// ModularizeMultiplex returns the hierarchical modularization of g at the given resolution
// using the Louvain algorithm. If all is true and g have negatively weighted layers, all
// communities will be searched during the modularization. If src is nil, rand.Intn is
// used as the random generator. ModularizeMultiplex will panic if g has any edge with
// edge weight that does not sign-match the layer weight.
//
// If g is undirected it is modularised to minimise
// Q = \sum w_{layer} \sum_{ij} [ A_{layer}*_{ij} - (\gamma_{layer} k_i k_j)/2m ] \delta(c_i,c_j).
// If g is directed it is modularised to minimise
// Q = \sum w_{layer} \sum_{ij} [ A_{layer}*_{ij} - (\gamma_{layer} k_i^in k_j^out)/m_{layer} ] \delta(c_i,c_j).
//
// The concrete type of the ReducedMultiplex will be a pointer to a
// ReducedUndirectedMultiplex.
//
// graph.Undirect may be used as a shim to allow modularization of
// directed graphs with the undirected modularity function.
func ModularizeMultiplex(g Multiplex, weights, resolutions []float64, all bool, src *rand.Rand) ReducedMultiplex {
if weights != nil && len(weights) != g.Depth() {
panic("community: weights vector length mismatch")
}
if resolutions != nil && len(resolutions) != 1 && len(resolutions) != g.Depth() {
panic("community: resolutions vector length mismatch")
}
switch g := g.(type) {
case UndirectedMultiplex:
return louvainUndirectedMultiplex(g, weights, resolutions, all, src)
case DirectedMultiplex:
return louvainDirectedMultiplex(g, weights, resolutions, all, src)
default:
panic(fmt.Sprintf("community: invalid graph type: %T", g))
}
}
// undirectedEdges is the edge structure of a reduced undirected graph.
type undirectedEdges struct {
// edges and weights is the set
// of edges between nodes.
// weights is keyed such that
// the first element of the key
// is less than the second.
edges [][]int
weights map[[2]int]float64
}
// directedEdges is the edge structure of a reduced directed graph.
type directedEdges struct {
// edgesFrom, edgesTo and weights
// is the set of edges between nodes.
edgesFrom [][]int
edgesTo [][]int
weights map[[2]int]float64
}
// community is a reduced graph node describing its membership.
type community struct {
id int
nodes []graph.Node
weight float64
}
func (n community) ID() int { return n.id }
// edge is a reduced graph edge.
type edge struct {
from, to community
weight float64
}
func (e edge) From() graph.Node { return e.from }
func (e edge) To() graph.Node { return e.to }
func (e edge) Weight() float64 { return e.weight }
// multiplexCommunity is a reduced multiplex graph node describing its membership.
type multiplexCommunity struct {
id int
nodes []graph.Node
weights []float64
}
func (n multiplexCommunity) ID() int { return n.id }
// multiplexEdge is a reduced graph edge for a multiplex graph.
type multiplexEdge struct {
from, to multiplexCommunity
weight float64
}
func (e multiplexEdge) From() graph.Node { return e.from }
func (e multiplexEdge) To() graph.Node { return e.to }
func (e multiplexEdge) Weight() float64 { return e.weight }
// commIdx is an index of a node in a community held by a localMover.
type commIdx struct {
community int
node int
}
// node is defined to avoid an import of .../graph/simple.
type node int
func (n node) ID() int { return int(n) }
// minTaker is a set iterator.
type minTaker interface {
TakeMin(p *int) bool
}
// dense is a dense integer set iterator.
type dense struct {
pos int
n int
}
// TakeMin mimics intsets.Sparse TakeMin for dense sets. If the dense
// iterator position is less than the iterator size, TakeMin sets *p
// to the the iterator position and increments the position and returns
// true.
// Otherwise, it returns false and *p is undefined.
func (d *dense) TakeMin(p *int) bool {
if d.pos >= d.n {
return false
}
*p = d.pos
d.pos++
return true
}
const (
negativeWeight = "community: unexpected negative edge weight"
positiveWeight = "community: unexpected positive edge weight"
)
// positiveWeightFuncFor returns a constructed weight function for the
// positively weighted g.
func positiveWeightFuncFor(g graph.Graph) func(x, y graph.Node) float64 {
if wg, ok := g.(graph.Weighter); ok {
return func(x, y graph.Node) float64 {
w, ok := wg.Weight(x, y)
if !ok {
return 0
}
if w < 0 {
panic(negativeWeight)
}
return w
}
}
return func(x, y graph.Node) float64 {
e := g.Edge(x, y)
if e == nil {
return 0
}
w := e.Weight()
if w < 0 {
panic(negativeWeight)
}
return w
}
}
// negativeWeightFuncFor returns a constructed weight function for the
// negatively weighted g.
func negativeWeightFuncFor(g graph.Graph) func(x, y graph.Node) float64 {
if wg, ok := g.(graph.Weighter); ok {
return func(x, y graph.Node) float64 {
w, ok := wg.Weight(x, y)
if !ok {
return 0
}
if w > 0 {
panic(positiveWeight)
}
return -w
}
}
return func(x, y graph.Node) float64 {
e := g.Edge(x, y)
if e == nil {
return 0
}
w := e.Weight()
if w > 0 {
panic(positiveWeight)
}
return -w
}
}
// depth returns max(1, len(weights)). It is used to ensure
// that multiplex community weights are properly initialised.
func depth(weights []float64) int {
if weights == nil {
return 1
}
return len(weights)
}

View File

@@ -0,0 +1,633 @@
// 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 community
import (
"math"
"math/rand"
"sort"
"golang.org/x/tools/container/intsets"
"github.com/gonum/graph"
"github.com/gonum/graph/internal/ordered"
)
// qDirected returns the modularity Q score of the graph g subdivided into the
// given communities at the given resolution. If communities is nil, the
// unclustered modularity score is returned. The resolution parameter
// is γ as defined in Reichardt and Bornholdt doi:10.1103/PhysRevE.74.016110.
// qDirected will panic if g has any edge with negative edge weight.
//
// Q = 1/m \sum_{ij} [ A_{ij} - (\gamma k_i^in k_j^out)/m ] \delta(c_i,c_j)
//
func qDirected(g graph.Directed, communities [][]graph.Node, resolution float64) float64 {
nodes := g.Nodes()
weight := positiveWeightFuncFor(g)
// Calculate the total edge weight of the graph
// and the table of penetrating edge weight sums.
var m float64
k := make(map[int]directedWeights, len(nodes))
for _, n := range nodes {
var wOut float64
u := n
for _, v := range g.From(u) {
wOut += weight(u, v)
}
var wIn float64
v := n
for _, u := range g.To(v) {
wIn += weight(u, v)
}
w := weight(n, n)
m += w + wOut // We only need to count edges once.
k[n.ID()] = directedWeights{out: w + wOut, in: w + wIn}
}
if communities == nil {
var q float64
for _, u := range nodes {
kU := k[u.ID()]
q += weight(u, u) - resolution*kU.out*kU.in/m
}
return q / m
}
var q float64
for _, c := range communities {
for _, u := range c {
kU := k[u.ID()]
for _, v := range c {
kV := k[v.ID()]
q += weight(u, v) - resolution*kU.out*kV.in/m
}
}
}
return q / m
}
// louvainDirected returns the hierarchical modularization of g at the given
// resolution using the Louvain algorithm. If src is nil, rand.Intn is used
// as the random generator. louvainDirected will panic if g has any edge with negative
// edge weight.
func louvainDirected(g graph.Directed, resolution float64, src *rand.Rand) ReducedGraph {
// See louvain.tex for a detailed description
// of the algorithm used here.
c := reduceDirected(g, nil)
rnd := rand.Intn
if src != nil {
rnd = src.Intn
}
for {
l := newDirectedLocalMover(c, c.communities, resolution)
if l == nil {
return c
}
if done := l.localMovingHeuristic(rnd); done {
return c
}
c = reduceDirected(c, l.communities)
}
}
// ReducedDirected is a directed graph of communities derived from a
// parent graph by reduction.
type ReducedDirected struct {
// nodes is the set of nodes held
// by the graph. In a ReducedDirected
// the node ID is the index into
// nodes.
nodes []community
directedEdges
// communities is the community
// structure of the graph.
communities [][]graph.Node
parent *ReducedDirected
}
var (
_ graph.Directed = (*ReducedDirected)(nil)
_ graph.Weighter = (*ReducedDirected)(nil)
_ ReducedGraph = (*ReducedUndirected)(nil)
)
// Communities returns the community memberships of the nodes in the
// graph used to generate the reduced graph.
func (g *ReducedDirected) Communities() [][]graph.Node {
communities := make([][]graph.Node, len(g.communities))
if g.parent == nil {
for i, members := range g.communities {
comm := make([]graph.Node, len(members))
for j, n := range members {
nodes := g.nodes[n.ID()].nodes
if len(nodes) != 1 {
panic("community: unexpected number of nodes in base graph community")
}
comm[j] = nodes[0]
}
communities[i] = comm
}
return communities
}
sub := g.parent.Communities()
for i, members := range g.communities {
var comm []graph.Node
for _, n := range members {
comm = append(comm, sub[n.ID()]...)
}
communities[i] = comm
}
return communities
}
// Structure returns the community structure of the current level of
// the module clustering. The first index of the returned value
// corresponds to the index of the nodes in the next higher level if
// it exists. The returned value should not be mutated.
func (g *ReducedDirected) Structure() [][]graph.Node {
return g.communities
}
// Expanded returns the next lower level of the module clustering or nil
// if at the lowest level.
func (g *ReducedDirected) Expanded() ReducedGraph {
return g.parent
}
// reduceDirected returns a reduced graph constructed from g divided
// into the given communities. The communities value is mutated
// by the call to reduceDirected. If communities is nil and g is a
// ReducedDirected, it is returned unaltered.
func reduceDirected(g graph.Directed, communities [][]graph.Node) *ReducedDirected {
if communities == nil {
if r, ok := g.(*ReducedDirected); ok {
return r
}
nodes := g.Nodes()
// TODO(kortschak) This sort is necessary really only
// for testing. In practice we would not be using the
// community provided by the user for a Q calculation.
// Probably we should use a function to map the
// communities in the test sets to the remapped order.
sort.Sort(ordered.ByID(nodes))
communities = make([][]graph.Node, len(nodes))
for i := range nodes {
communities[i] = []graph.Node{node(i)}
}
weight := positiveWeightFuncFor(g)
r := ReducedDirected{
nodes: make([]community, len(nodes)),
directedEdges: directedEdges{
edgesFrom: make([][]int, len(nodes)),
edgesTo: make([][]int, len(nodes)),
weights: make(map[[2]int]float64),
},
communities: communities,
}
communityOf := make(map[int]int, len(nodes))
for i, n := range nodes {
r.nodes[i] = community{id: i, nodes: []graph.Node{n}}
communityOf[n.ID()] = i
}
for _, n := range nodes {
id := communityOf[n.ID()]
var out []int
u := n
for _, v := range g.From(u) {
vid := communityOf[v.ID()]
if vid != id {
out = append(out, vid)
}
r.weights[[2]int{id, vid}] = weight(u, v)
}
r.edgesFrom[id] = out
var in []int
v := n
for _, u := range g.To(v) {
uid := communityOf[u.ID()]
if uid != id {
in = append(in, uid)
}
r.weights[[2]int{uid, id}] = weight(u, v)
}
r.edgesTo[id] = in
}
return &r
}
// Remove zero length communities destructively.
var commNodes int
for i := 0; i < len(communities); {
comm := communities[i]
if len(comm) == 0 {
communities[i] = communities[len(communities)-1]
communities[len(communities)-1] = nil
communities = communities[:len(communities)-1]
} else {
commNodes += len(comm)
i++
}
}
r := ReducedDirected{
nodes: make([]community, len(communities)),
directedEdges: directedEdges{
edgesFrom: make([][]int, len(communities)),
edgesTo: make([][]int, len(communities)),
weights: make(map[[2]int]float64),
},
}
r.communities = make([][]graph.Node, len(communities))
for i := range r.communities {
r.communities[i] = []graph.Node{node(i)}
}
if g, ok := g.(*ReducedDirected); ok {
// Make sure we retain the truncated
// community structure.
g.communities = communities
r.parent = g
}
weight := positiveWeightFuncFor(g)
communityOf := make(map[int]int, commNodes)
for i, comm := range communities {
r.nodes[i] = community{id: i, nodes: comm}
for _, n := range comm {
communityOf[n.ID()] = i
}
}
for id, comm := range communities {
var out, in []int
for _, n := range comm {
u := n
for _, v := range comm {
r.nodes[id].weight += weight(u, v)
}
for _, v := range g.From(u) {
vid := communityOf[v.ID()]
found := false
for _, e := range out {
if e == vid {
found = true
break
}
}
if !found && vid != id {
out = append(out, vid)
}
// Add half weights because the other
// ends of edges are also counted.
r.weights[[2]int{id, vid}] += weight(u, v) / 2
}
v := n
for _, u := range g.To(v) {
uid := communityOf[u.ID()]
found := false
for _, e := range in {
if e == uid {
found = true
break
}
}
if !found && uid != id {
in = append(in, uid)
}
// Add half weights because the other
// ends of edges are also counted.
r.weights[[2]int{uid, id}] += weight(u, v) / 2
}
}
r.edgesFrom[id] = out
r.edgesTo[id] = in
}
return &r
}
// Has returns whether the node exists within the graph.
func (g *ReducedDirected) Has(n graph.Node) bool {
id := n.ID()
return id >= 0 || id < len(g.nodes)
}
// Nodes returns all the nodes in the graph.
func (g *ReducedDirected) Nodes() []graph.Node {
nodes := make([]graph.Node, len(g.nodes))
for i := range g.nodes {
nodes[i] = node(i)
}
return nodes
}
// From returns all nodes in g that can be reached directly from u.
func (g *ReducedDirected) From(u graph.Node) []graph.Node {
out := g.edgesFrom[u.ID()]
nodes := make([]graph.Node, len(out))
for i, vid := range out {
nodes[i] = g.nodes[vid]
}
return nodes
}
// To returns all nodes in g that can reach directly to v.
func (g *ReducedDirected) To(v graph.Node) []graph.Node {
in := g.edgesTo[v.ID()]
nodes := make([]graph.Node, len(in))
for i, uid := range in {
nodes[i] = g.nodes[uid]
}
return nodes
}
// HasEdgeBetween returns whether an edge exists between nodes x and y.
func (g *ReducedDirected) HasEdgeBetween(x, y graph.Node) bool {
xid := x.ID()
yid := y.ID()
if xid == yid {
return false
}
_, ok := g.weights[[2]int{xid, yid}]
if ok {
return true
}
_, ok = g.weights[[2]int{yid, xid}]
return ok
}
// HasEdgeFromTo returns whether an edge exists from node u to v.
func (g *ReducedDirected) HasEdgeFromTo(u, v graph.Node) bool {
uid := u.ID()
vid := v.ID()
if uid == vid {
return false
}
_, ok := g.weights[[2]int{uid, vid}]
return ok
}
// 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.
func (g *ReducedDirected) Edge(u, v graph.Node) graph.Edge {
uid := u.ID()
vid := v.ID()
w, ok := g.weights[[2]int{uid, vid}]
if !ok {
return nil
}
return edge{from: g.nodes[uid], to: g.nodes[vid], weight: 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 *ReducedDirected) Weight(x, y graph.Node) (w float64, ok bool) {
xid := x.ID()
yid := y.ID()
if xid == yid {
return g.nodes[xid].weight, true
}
w, ok = g.weights[[2]int{xid, yid}]
return w, ok
}
// directedLocalMover is a step in graph modularity optimization.
type directedLocalMover struct {
g *ReducedDirected
// nodes is the set of working nodes.
nodes []graph.Node
// edgeWeightsOf is the weighted degree
// of each node indexed by ID.
edgeWeightsOf []directedWeights
// m is the total sum of edge
// weights in g.
m float64
// weight is the weight function
// provided by g or a function
// that returns the Weight value
// of the non-nil edge between x
// and y.
weight func(x, y graph.Node) float64
// communities is the current
// division of g.
communities [][]graph.Node
// memberships is a mapping between
// node ID and community membership.
memberships []int
// resolution is the Reichardt and
// Bornholdt γ parameter as defined
// in doi:10.1103/PhysRevE.74.016110.
resolution float64
// moved indicates that a call to
// move has been made since the last
// call to shuffle.
moved bool
// changed indicates that a move
// has been made since the creation
// of the local mover.
changed bool
}
type directedWeights struct {
out, in float64
}
// newDirectedLocalMover returns a new directedLocalMover initialized with
// the graph g, a set of communities and a modularity resolution parameter.
// The node IDs of g must be contiguous in [0,n) where n is the number of
// nodes.
// If g has a zero edge weight sum, nil is returned.
func newDirectedLocalMover(g *ReducedDirected, communities [][]graph.Node, resolution float64) *directedLocalMover {
nodes := g.Nodes()
l := directedLocalMover{
g: g,
nodes: nodes,
edgeWeightsOf: make([]directedWeights, len(nodes)),
communities: communities,
memberships: make([]int, len(nodes)),
resolution: resolution,
weight: positiveWeightFuncFor(g),
}
// Calculate the total edge weight of the graph
// and degree weights for each node.
for _, n := range l.nodes {
u := n
var wOut float64
for _, v := range g.From(u) {
wOut += l.weight(u, v)
}
v := n
var wIn float64
for _, u := range g.To(v) {
wIn += l.weight(u, v)
}
w := l.weight(n, n)
l.edgeWeightsOf[n.ID()] = directedWeights{out: w + wOut, in: w + wIn}
l.m += w + wOut
}
// Assign membership mappings.
for i, c := range communities {
for _, n := range c {
l.memberships[n.ID()] = i
}
}
return &l
}
// localMovingHeuristic performs the Louvain local moving heuristic until
// no further moves can be made. It returns a boolean indicating that the
// directedLocalMover has not made any improvement to the community structure and
// so the Louvain algorithm is done.
func (l *directedLocalMover) localMovingHeuristic(rnd func(int) int) (done bool) {
for {
l.shuffle(rnd)
for _, n := range l.nodes {
dQ, dst, src := l.deltaQ(n)
if dQ <= 0 {
continue
}
l.move(dst, src)
}
if !l.moved {
return !l.changed
}
}
}
// shuffle performs a Fisher-Yates shuffle on the nodes held by the
// directedLocalMover using the random source rnd which should return an
// integer in the range [0,n).
func (l *directedLocalMover) shuffle(rnd func(n int) int) {
l.moved = false
for i := range l.nodes[:len(l.nodes)-1] {
j := i + rnd(len(l.nodes)-i)
l.nodes[i], l.nodes[j] = l.nodes[j], l.nodes[i]
}
}
// move moves the node at src to the community at dst.
func (l *directedLocalMover) move(dst int, src commIdx) {
l.moved = true
l.changed = true
srcComm := l.communities[src.community]
n := srcComm[src.node]
l.memberships[n.ID()] = dst
l.communities[dst] = append(l.communities[dst], n)
srcComm[src.node], srcComm[len(srcComm)-1] = srcComm[len(srcComm)-1], nil
l.communities[src.community] = srcComm[:len(srcComm)-1]
}
// deltaQ returns the highest gain in modularity attainable by moving
// n from its current community to another connected community and
// the index of the chosen destination. The index into the directedLocalMover's
// communities field is returned in src if n is in communities.
func (l *directedLocalMover) deltaQ(n graph.Node) (deltaQ float64, dst int, src commIdx) {
id := n.ID()
a_aa := l.weight(n, n)
k_a := l.edgeWeightsOf[id]
m := l.m
gamma := l.resolution
// Find communites connected to n.
var connected intsets.Sparse
// The following for loop is equivalent to:
//
// for _, v := range l.g.From(n) {
// connected.Insert(l.memberships[v.ID()])
// }
// for _, v := range l.g.To(n) {
// connected.Insert(l.memberships[v.ID()])
// }
//
// This is done to avoid two allocations.
for _, vid := range l.g.edgesFrom[id] {
connected.Insert(l.memberships[vid])
}
for _, vid := range l.g.edgesTo[id] {
connected.Insert(l.memberships[vid])
}
// Insert the node's own community.
connected.Insert(l.memberships[id])
// Calculate the highest modularity gain
// from moving into another community and
// keep the index of that community.
var dQremove float64
dQadd, dst, src := math.Inf(-1), -1, commIdx{-1, -1}
var i int
for connected.TakeMin(&i) {
c := l.communities[i]
var k_aC, sigma_totC directedWeights // C is a substitution for ^𝛼 or ^𝛽.
var removal bool
for j, u := range c {
uid := u.ID()
if uid == id {
if src.community != -1 {
panic("community: multiple sources")
}
src = commIdx{i, j}
removal = true
}
k_aC.in += l.weight(u, n)
k_aC.out += l.weight(n, u)
// sigma_totC could be kept for each community
// and updated for moves, changing the calculation
// of sigma_totC here from O(n_c) to O(1), but
// in practice the time savings do not appear
// to be compelling and do not make up for the
// increase in code complexity and space required.
w := l.edgeWeightsOf[uid]
sigma_totC.in += w.in
sigma_totC.out += w.out
}
// See louvain.tex for a derivation of these equations.
switch {
case removal:
// The community c was the current community,
// so calculate the change due to removal.
dQremove = (k_aC.in /*^𝛼*/ - a_aa) + (k_aC.out /*^𝛼*/ - a_aa) -
gamma*(k_a.in*(sigma_totC.out /*^𝛼*/ -k_a.out)+k_a.out*(sigma_totC.in /*^𝛼*/ -k_a.in))/m
default:
// Otherwise calculate the change due to an addition
// to c and retain if it is the current best.
dQ := k_aC.in /*^𝛽*/ + k_aC.out /*^𝛽*/ -
gamma*(k_a.in*sigma_totC.out /*^𝛽*/ +k_a.out*sigma_totC.in /*^𝛽*/)/m
if dQ > dQadd {
dQadd = dQ
dst = i
}
}
}
return (dQadd - dQremove) / m, dst, src
}

View File

@@ -0,0 +1,880 @@
// 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 community
import (
"fmt"
"math"
"math/rand"
"sort"
"golang.org/x/tools/container/intsets"
"github.com/gonum/graph"
"github.com/gonum/graph/internal/ordered"
)
// DirectedMultiplex is a directed multiplex graph.
type DirectedMultiplex interface {
Multiplex
// Layer returns the lth layer of the
// multiplex graph.
Layer(l int) graph.Directed
}
// qDirectedMultiplex returns the modularity Q score of the multiplex graph layers
// subdivided into the given communities at the given resolutions and weights. Q is
// returned as the vector of weighted Q scores for each layer of the multiplex graph.
// If communities is nil, the unclustered modularity score is returned.
// If weights is nil layers are equally weighted, otherwise the length of
// weights must equal the number of layers. If resolutions is nil, a resolution
// of 1.0 is used for all layers, otherwise either a single element slice may be used
// to specify a global resolution, or the length of resolutions must equal the number
// of layers. The resolution parameter is γ as defined in Reichardt and Bornholdt
// doi:10.1103/PhysRevE.74.016110.
// qUndirectedMultiplex will panic if the graph has any layer weight-scaled edge with
// negative edge weight.
//
// Q_{layer} = w_{layer} \sum_{ij} [ A_{layer}*_{ij} - (\gamma_{layer} k_i k_j)/2m ] \delta(c_i,c_j)
//
// Note that Q values for multiplex graphs are not scaled by the total layer edge weight.
func qDirectedMultiplex(g DirectedMultiplex, communities [][]graph.Node, weights, resolutions []float64) []float64 {
q := make([]float64, g.Depth())
nodes := g.Nodes()
layerWeight := 1.0
layerResolution := 1.0
if len(resolutions) == 1 {
layerResolution = resolutions[0]
}
for l := 0; l < g.Depth(); l++ {
layer := g.Layer(l)
if weights != nil {
layerWeight = weights[l]
}
if layerWeight == 0 {
continue
}
if len(resolutions) > 1 {
layerResolution = resolutions[l]
}
var weight func(x, y graph.Node) float64
if layerWeight < 0 {
weight = negativeWeightFuncFor(layer)
} else {
weight = positiveWeightFuncFor(layer)
}
// Calculate the total edge weight of the layer
// and the table of penetrating edge weight sums.
var m float64
k := make(map[int]directedWeights, len(nodes))
for _, n := range nodes {
var wOut float64
u := n
for _, v := range layer.From(u) {
wOut += weight(u, v)
}
var wIn float64
v := n
for _, u := range layer.To(v) {
wIn += weight(u, v)
}
w := weight(n, n)
m += w + wOut // We only need to count edges once.
k[n.ID()] = directedWeights{out: w + wOut, in: w + wIn}
}
if communities == nil {
var qLayer float64
for _, u := range nodes {
kU := k[u.ID()]
qLayer += weight(u, u) - layerResolution*kU.out*kU.in/m
}
q[l] = layerWeight * qLayer
continue
}
var qLayer float64
for _, c := range communities {
for _, u := range c {
kU := k[u.ID()]
for _, v := range c {
kV := k[v.ID()]
qLayer += weight(u, v) - layerResolution*kU.out*kV.in/m
}
}
}
q[l] = layerWeight * qLayer
}
return q
}
// DirectedLayers implements DirectedMultiplex.
type DirectedLayers []graph.Directed
// NewDirectedLayers returns a DirectedLayers using the provided layers
// ensuring there is a match between IDs for each layer.
func NewDirectedLayers(layers ...graph.Directed) (DirectedLayers, error) {
if len(layers) == 0 {
return nil, nil
}
var base, next intsets.Sparse
for _, n := range layers[0].Nodes() {
base.Insert(n.ID())
}
for i, l := range layers[1:] {
next.Clear()
for _, n := range l.Nodes() {
next.Insert(n.ID())
}
if !next.Equals(&base) {
return nil, fmt.Errorf("community: layer ID mismatch between layers: %d", i+1)
}
}
return layers, nil
}
// Nodes returns the nodes of the receiver.
func (g DirectedLayers) Nodes() []graph.Node {
if len(g) == 0 {
return nil
}
return g[0].Nodes()
}
// Depth returns the depth of the multiplex graph.
func (g DirectedLayers) Depth() int { return len(g) }
// Layer returns the lth layer of the multiplex graph.
func (g DirectedLayers) Layer(l int) graph.Directed { return g[l] }
// louvainDirectedMultiplex returns the hierarchical modularization of g at the given resolution
// using the Louvain algorithm. If all is true and g has negatively weighted layers, all
// communities will be searched during the modularization. If src is nil, rand.Intn is
// used as the random generator. louvainDirectedMultiplex will panic if g has any edge with
// edge weight that does not sign-match the layer weight.
//
// graph.Undirect may be used as a shim to allow modularization of directed graphs.
func louvainDirectedMultiplex(g DirectedMultiplex, weights, resolutions []float64, all bool, src *rand.Rand) *ReducedDirectedMultiplex {
if weights != nil && len(weights) != g.Depth() {
panic("community: weights vector length mismatch")
}
if resolutions != nil && len(resolutions) != 1 && len(resolutions) != g.Depth() {
panic("community: resolutions vector length mismatch")
}
// See louvain.tex for a detailed description
// of the algorithm used here.
c := reduceDirectedMultiplex(g, nil, weights)
rnd := rand.Intn
if src != nil {
rnd = src.Intn
}
for {
l := newDirectedMultiplexLocalMover(c, c.communities, weights, resolutions, all)
if l == nil {
return c
}
if done := l.localMovingHeuristic(rnd); done {
return c
}
c = reduceDirectedMultiplex(c, l.communities, weights)
}
}
// ReducedDirectedMultiplex is a directed graph of communities derived from a
// parent graph by reduction.
type ReducedDirectedMultiplex struct {
// nodes is the set of nodes held
// by the graph. In a ReducedDirectedMultiplex
// the node ID is the index into
// nodes.
nodes []multiplexCommunity
layers []directedEdges
// communities is the community
// structure of the graph.
communities [][]graph.Node
parent *ReducedDirectedMultiplex
}
var (
_ DirectedMultiplex = (*ReducedDirectedMultiplex)(nil)
_ graph.Directed = (*directedLayerHandle)(nil)
_ graph.Weighter = (*directedLayerHandle)(nil)
)
// Nodes returns all the nodes in the graph.
func (g *ReducedDirectedMultiplex) Nodes() []graph.Node {
nodes := make([]graph.Node, len(g.nodes))
for i := range g.nodes {
nodes[i] = node(i)
}
return nodes
}
// Depth returns the number of layers in the multiplex graph.
func (g *ReducedDirectedMultiplex) Depth() int { return len(g.layers) }
// Layer returns the lth layer of the multiplex graph.
func (g *ReducedDirectedMultiplex) Layer(l int) graph.Directed {
return directedLayerHandle{multiplex: g, layer: l}
}
// Communities returns the community memberships of the nodes in the
// graph used to generate the reduced graph.
func (g *ReducedDirectedMultiplex) Communities() [][]graph.Node {
communities := make([][]graph.Node, len(g.communities))
if g.parent == nil {
for i, members := range g.communities {
comm := make([]graph.Node, len(members))
for j, n := range members {
nodes := g.nodes[n.ID()].nodes
if len(nodes) != 1 {
panic("community: unexpected number of nodes in base graph community")
}
comm[j] = nodes[0]
}
communities[i] = comm
}
return communities
}
sub := g.parent.Communities()
for i, members := range g.communities {
var comm []graph.Node
for _, n := range members {
comm = append(comm, sub[n.ID()]...)
}
communities[i] = comm
}
return communities
}
// Structure returns the community structure of the current level of
// the module clustering. The first index of the returned value
// corresponds to the index of the nodes in the next higher level if
// it exists. The returned value should not be mutated.
func (g *ReducedDirectedMultiplex) Structure() [][]graph.Node {
return g.communities
}
// Expanded returns the next lower level of the module clustering or nil
// if at the lowest level.
func (g *ReducedDirectedMultiplex) Expanded() ReducedMultiplex {
return g.parent
}
// reduceDirectedMultiplex returns a reduced graph constructed from g divided
// into the given communities. The communities value is mutated
// by the call to reduceDirectedMultiplex. If communities is nil and g is a
// ReducedDirectedMultiplex, it is returned unaltered.
func reduceDirectedMultiplex(g DirectedMultiplex, communities [][]graph.Node, weights []float64) *ReducedDirectedMultiplex {
if communities == nil {
if r, ok := g.(*ReducedDirectedMultiplex); ok {
return r
}
nodes := g.Nodes()
// TODO(kortschak) This sort is necessary really only
// for testing. In practice we would not be using the
// community provided by the user for a Q calculation.
// Probably we should use a function to map the
// communities in the test sets to the remapped order.
sort.Sort(ordered.ByID(nodes))
communities = make([][]graph.Node, len(nodes))
for i := range nodes {
communities[i] = []graph.Node{node(i)}
}
r := ReducedDirectedMultiplex{
nodes: make([]multiplexCommunity, len(nodes)),
layers: make([]directedEdges, g.Depth()),
communities: communities,
}
communityOf := make(map[int]int, len(nodes))
for i, n := range nodes {
r.nodes[i] = multiplexCommunity{id: i, nodes: []graph.Node{n}, weights: make([]float64, depth(weights))}
communityOf[n.ID()] = i
}
for i := range r.layers {
r.layers[i] = directedEdges{
edgesFrom: make([][]int, len(nodes)),
edgesTo: make([][]int, len(nodes)),
weights: make(map[[2]int]float64),
}
}
w := 1.0
for l := 0; l < g.Depth(); l++ {
layer := g.Layer(l)
if weights != nil {
w = weights[l]
}
if w == 0 {
continue
}
var sign float64
var weight func(x, y graph.Node) float64
if w < 0 {
sign, weight = -1, negativeWeightFuncFor(layer)
} else {
sign, weight = 1, positiveWeightFuncFor(layer)
}
for _, n := range nodes {
id := communityOf[n.ID()]
var out []int
u := n
for _, v := range layer.From(u) {
vid := communityOf[v.ID()]
if vid != id {
out = append(out, vid)
}
r.layers[l].weights[[2]int{id, vid}] = sign * weight(u, v)
}
r.layers[l].edgesFrom[id] = out
var in []int
v := n
for _, u := range layer.To(v) {
uid := communityOf[u.ID()]
if uid != id {
in = append(in, uid)
}
r.layers[l].weights[[2]int{uid, id}] = sign * weight(u, v)
}
r.layers[l].edgesTo[id] = in
}
}
return &r
}
// Remove zero length communities destructively.
var commNodes int
for i := 0; i < len(communities); {
comm := communities[i]
if len(comm) == 0 {
communities[i] = communities[len(communities)-1]
communities[len(communities)-1] = nil
communities = communities[:len(communities)-1]
} else {
commNodes += len(comm)
i++
}
}
r := ReducedDirectedMultiplex{
nodes: make([]multiplexCommunity, len(communities)),
layers: make([]directedEdges, g.Depth()),
}
communityOf := make(map[int]int, commNodes)
for i, comm := range communities {
r.nodes[i] = multiplexCommunity{id: i, nodes: comm, weights: make([]float64, depth(weights))}
for _, n := range comm {
communityOf[n.ID()] = i
}
}
for i := range r.layers {
r.layers[i] = directedEdges{
edgesFrom: make([][]int, len(communities)),
edgesTo: make([][]int, len(communities)),
weights: make(map[[2]int]float64),
}
}
r.communities = make([][]graph.Node, len(communities))
for i := range r.communities {
r.communities[i] = []graph.Node{node(i)}
}
if g, ok := g.(*ReducedDirectedMultiplex); ok {
// Make sure we retain the truncated
// community structure.
g.communities = communities
r.parent = g
}
w := 1.0
for l := 0; l < g.Depth(); l++ {
layer := g.Layer(l)
if weights != nil {
w = weights[l]
}
if w == 0 {
continue
}
var sign float64
var weight func(x, y graph.Node) float64
if w < 0 {
sign, weight = -1, negativeWeightFuncFor(layer)
} else {
sign, weight = 1, positiveWeightFuncFor(layer)
}
for id, comm := range communities {
var out, in []int
for _, n := range comm {
u := n
for _, v := range comm {
r.nodes[id].weights[l] += sign * weight(u, v)
}
for _, v := range layer.From(u) {
vid := communityOf[v.ID()]
found := false
for _, e := range out {
if e == vid {
found = true
break
}
}
if !found && vid != id {
out = append(out, vid)
}
// Add half weights because the other
// ends of edges are also counted.
r.layers[l].weights[[2]int{id, vid}] += sign * weight(u, v) / 2
}
v := n
for _, u := range layer.To(v) {
uid := communityOf[u.ID()]
found := false
for _, e := range in {
if e == uid {
found = true
break
}
}
if !found && uid != id {
in = append(in, uid)
}
// Add half weights because the other
// ends of edges are also counted.
r.layers[l].weights[[2]int{uid, id}] += sign * weight(u, v) / 2
}
}
r.layers[l].edgesFrom[id] = out
r.layers[l].edgesTo[id] = in
}
}
return &r
}
// directedLayerHandle is a handle to a multiplex graph layer.
type directedLayerHandle struct {
// multiplex is the complete
// multiplex graph.
multiplex *ReducedDirectedMultiplex
// layer is an index into the
// multiplex for the current
// layer.
layer int
}
// Has returns whether the node exists within the graph.
func (g directedLayerHandle) Has(n graph.Node) bool {
id := n.ID()
return id >= 0 || id < len(g.multiplex.nodes)
}
// Nodes returns all the nodes in the graph.
func (g directedLayerHandle) Nodes() []graph.Node {
nodes := make([]graph.Node, len(g.multiplex.nodes))
for i := range g.multiplex.nodes {
nodes[i] = node(i)
}
return nodes
}
// From returns all nodes in g that can be reached directly from u.
func (g directedLayerHandle) From(u graph.Node) []graph.Node {
out := g.multiplex.layers[g.layer].edgesFrom[u.ID()]
nodes := make([]graph.Node, len(out))
for i, vid := range out {
nodes[i] = g.multiplex.nodes[vid]
}
return nodes
}
// To returns all nodes in g that can reach directly to v.
func (g directedLayerHandle) To(v graph.Node) []graph.Node {
in := g.multiplex.layers[g.layer].edgesTo[v.ID()]
nodes := make([]graph.Node, len(in))
for i, uid := range in {
nodes[i] = g.multiplex.nodes[uid]
}
return nodes
}
// HasEdgeBetween returns whether an edge exists between nodes x and y.
func (g directedLayerHandle) HasEdgeBetween(x, y graph.Node) bool {
xid := x.ID()
yid := y.ID()
if xid == yid {
return false
}
_, ok := g.multiplex.layers[g.layer].weights[[2]int{xid, yid}]
if ok {
return true
}
_, ok = g.multiplex.layers[g.layer].weights[[2]int{yid, xid}]
return ok
}
// HasEdgeFromTo returns whether an edge exists from node u to v.
func (g directedLayerHandle) HasEdgeFromTo(u, v graph.Node) bool {
uid := u.ID()
vid := v.ID()
if uid == vid {
return false
}
_, ok := g.multiplex.layers[g.layer].weights[[2]int{uid, vid}]
return ok
}
// 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.
func (g directedLayerHandle) Edge(u, v graph.Node) graph.Edge {
uid := u.ID()
vid := v.ID()
w, ok := g.multiplex.layers[g.layer].weights[[2]int{uid, vid}]
if !ok {
return nil
}
return multiplexEdge{from: g.multiplex.nodes[u.ID()], to: g.multiplex.nodes[v.ID()], weight: w}
}
// EdgeBetween returns the edge between nodes x and y.
func (g directedLayerHandle) EdgeBetween(x, y graph.Node) graph.Edge {
return g.Edge(x, y)
}
// 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 directedLayerHandle) Weight(x, y graph.Node) (w float64, ok bool) {
xid := x.ID()
yid := y.ID()
if xid == yid {
return g.multiplex.nodes[xid].weights[g.layer], true
}
w, ok = g.multiplex.layers[g.layer].weights[[2]int{xid, yid}]
return w, ok
}
// directedMultiplexLocalMover is a step in graph modularity optimization.
type directedMultiplexLocalMover struct {
g *ReducedDirectedMultiplex
// nodes is the set of working nodes.
nodes []graph.Node
// edgeWeightsOf is the weighted degree
// of each node indexed by ID.
edgeWeightsOf [][]directedWeights
// m is the total sum of
// edge weights in g.
m []float64
// weight is the weight function
// provided by g or a function
// that returns the Weight value
// of the non-nil edge between x
// and y.
weight []func(x, y graph.Node) float64
// communities is the current
// division of g.
communities [][]graph.Node
// memberships is a mapping between
// node ID and community membership.
memberships []int
// resolution is the Reichardt and
// Bornholdt γ parameter as defined
// in doi:10.1103/PhysRevE.74.016110.
resolutions []float64
// weights is the layer weights for
// the modularisation.
weights []float64
// searchAll specifies whether the local
// mover should consider non-connected
// communities during the local moving
// heuristic.
searchAll bool
// moved indicates that a call to
// move has been made since the last
// call to shuffle.
moved bool
// changed indicates that a move
// has been made since the creation
// of the local mover.
changed bool
}
// newDirectedMultiplexLocalMover returns a new directedMultiplexLocalMover initialized with
// the graph g, a set of communities and a modularity resolution parameter. The
// node IDs of g must be contiguous in [0,n) where n is the number of nodes.
// If g has a zero edge weight sum, nil is returned.
func newDirectedMultiplexLocalMover(g *ReducedDirectedMultiplex, communities [][]graph.Node, weights, resolutions []float64, all bool) *directedMultiplexLocalMover {
nodes := g.Nodes()
l := directedMultiplexLocalMover{
g: g,
nodes: nodes,
edgeWeightsOf: make([][]directedWeights, g.Depth()),
m: make([]float64, g.Depth()),
communities: communities,
memberships: make([]int, len(nodes)),
resolutions: resolutions,
weights: weights,
weight: make([]func(x, y graph.Node) float64, g.Depth()),
}
// Calculate the total edge weight of the graph
// and degree weights for each node.
var zero int
for i := 0; i < g.Depth(); i++ {
l.edgeWeightsOf[i] = make([]directedWeights, len(nodes))
var weight func(x, y graph.Node) float64
if weights != nil {
if weights[i] == 0 {
zero++
continue
}
if weights[i] < 0 {
weight = negativeWeightFuncFor(g.Layer(i))
l.searchAll = all
} else {
weight = positiveWeightFuncFor(g.Layer(i))
}
} else {
weight = positiveWeightFuncFor(g.Layer(i))
}
l.weight[i] = weight
layer := g.Layer(i)
for _, n := range l.nodes {
u := n
var wOut float64
for _, v := range layer.From(u) {
wOut += weight(u, v)
}
v := n
var wIn float64
for _, u := range layer.To(v) {
wIn += weight(u, v)
}
w := weight(n, n)
l.edgeWeightsOf[i][u.ID()] = directedWeights{out: w + wOut, in: w + wIn}
l.m[i] += w + wOut
}
if l.m[i] == 0 {
zero++
}
}
if zero == g.Depth() {
return nil
}
// Assign membership mappings.
for i, c := range communities {
for _, n := range c {
l.memberships[n.ID()] = i
}
}
return &l
}
// localMovingHeuristic performs the Louvain local moving heuristic until
// no further moves can be made. It returns a boolean indicating that the
// directedMultiplexLocalMover has not made any improvement to the community
// structure and so the Louvain algorithm is done.
func (l *directedMultiplexLocalMover) localMovingHeuristic(rnd func(int) int) (done bool) {
for {
l.shuffle(rnd)
for _, n := range l.nodes {
dQ, dst, src := l.deltaQ(n)
if dQ <= 0 {
continue
}
l.move(dst, src)
}
if !l.moved {
return !l.changed
}
}
}
// shuffle performs a Fisher-Yates shuffle on the nodes held by the
// directedMultiplexLocalMover using the random source rnd which should return
// an integer in the range [0,n).
func (l *directedMultiplexLocalMover) shuffle(rnd func(n int) int) {
l.moved = false
for i := range l.nodes[:len(l.nodes)-1] {
j := i + rnd(len(l.nodes)-i)
l.nodes[i], l.nodes[j] = l.nodes[j], l.nodes[i]
}
}
// move moves the node at src to the community at dst.
func (l *directedMultiplexLocalMover) move(dst int, src commIdx) {
l.moved = true
l.changed = true
srcComm := l.communities[src.community]
n := srcComm[src.node]
l.memberships[n.ID()] = dst
l.communities[dst] = append(l.communities[dst], n)
srcComm[src.node], srcComm[len(srcComm)-1] = srcComm[len(srcComm)-1], nil
l.communities[src.community] = srcComm[:len(srcComm)-1]
}
// deltaQ returns the highest gain in modularity attainable by moving
// n from its current community to another connected community and
// the index of the chosen destination. The index into the
// directedMultiplexLocalMover's communities field is returned in src if n
// is in communities.
func (l *directedMultiplexLocalMover) deltaQ(n graph.Node) (deltaQ float64, dst int, src commIdx) {
id := n.ID()
var iterator minTaker
if l.searchAll {
iterator = &dense{n: len(l.communities)}
} else {
// Find communities connected to n.
var connected intsets.Sparse
// The following for loop is equivalent to:
//
// for i := 0; i < l.g.Depth(); i++ {
// for _, v := range l.g.Layer(i).From(n) {
// connected.Insert(l.memberships[v.ID()])
// }
// for _, v := range l.g.Layer(i).To(n) {
// connected.Insert(l.memberships[v.ID()])
// }
// }
//
// This is done to avoid an allocation for
// each layer.
for _, layer := range l.g.layers {
for _, vid := range layer.edgesFrom[id] {
connected.Insert(l.memberships[vid])
}
for _, vid := range layer.edgesTo[id] {
connected.Insert(l.memberships[vid])
}
}
// Insert the node's own community.
connected.Insert(l.memberships[id])
iterator = &connected
}
// Calculate the highest modularity gain
// from moving into another community and
// keep the index of that community.
var dQremove float64
dQadd, dst, src := math.Inf(-1), -1, commIdx{-1, -1}
var i int
for iterator.TakeMin(&i) {
c := l.communities[i]
var removal bool
var _dQadd float64
for layer := 0; layer < l.g.Depth(); layer++ {
m := l.m[layer]
if m == 0 {
// Do not consider layers with zero sum edge weight.
continue
}
w := 1.0
if l.weights != nil {
w = l.weights[layer]
}
if w == 0 {
// Do not consider layers with zero weighting.
continue
}
var k_aC, sigma_totC directedWeights // C is a substitution for ^𝛼 or ^𝛽.
removal = false
for j, u := range c {
uid := u.ID()
if uid == id {
// Only mark and check src community on the first layer.
if layer == 0 {
if src.community != -1 {
panic("community: multiple sources")
}
src = commIdx{i, j}
}
removal = true
}
k_aC.in += l.weight[layer](n, u)
k_aC.out += l.weight[layer](u, n)
// sigma_totC could be kept for each community
// and updated for moves, changing the calculation
// of sigma_totC here from O(n_c) to O(1), but
// in practice the time savings do not appear
// to be compelling and do not make up for the
// increase in code complexity and space required.
w := l.edgeWeightsOf[layer][uid]
sigma_totC.in += w.in
sigma_totC.out += w.out
}
a_aa := l.weight[layer](n, n)
k_a := l.edgeWeightsOf[layer][id]
gamma := 1.0
if l.resolutions != nil {
if len(l.resolutions) == 1 {
gamma = l.resolutions[0]
} else {
gamma = l.resolutions[layer]
}
}
// See louvain.tex for a derivation of these equations.
// The weighting term, w, is described in V Traag,
// "Algorithms and dynamical models for communities and
// reputation in social networks", chapter 5.
// http://www.traag.net/wp/wp-content/papercite-data/pdf/traag_algorithms_2013.pdf
switch {
case removal:
// The community c was the current community,
// so calculate the change due to removal.
dQremove += w * ((k_aC.in /*^𝛼*/ - a_aa) + (k_aC.out /*^𝛼*/ - a_aa) -
gamma*(k_a.in*(sigma_totC.out /*^𝛼*/ -k_a.out)+k_a.out*(sigma_totC.in /*^𝛼*/ -k_a.in))/m)
default:
// Otherwise calculate the change due to an addition
// to c.
_dQadd += w * (k_aC.in /*^𝛽*/ + k_aC.out /*^𝛽*/ -
gamma*(k_a.in*sigma_totC.out /*^𝛽*/ +k_a.out*sigma_totC.in /*^𝛽*/)/m)
}
}
if !removal && _dQadd > dQadd {
dQadd = _dQadd
dst = i
}
}
return dQadd - dQremove, dst, src
}

View File

@@ -0,0 +1,700 @@
// 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 community
import (
"math"
"math/rand"
"reflect"
"sort"
"testing"
"github.com/gonum/floats"
"github.com/gonum/graph"
"github.com/gonum/graph/internal/ordered"
"github.com/gonum/graph/simple"
)
var communityDirectedMultiplexQTests = []struct {
name string
layers []layer
structures []structure
wantLevels []level
}{
{
name: "unconnected",
layers: []layer{{g: unconnected, weight: 1}},
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0),
1: linksTo(1),
2: linksTo(2),
3: linksTo(3),
4: linksTo(4),
5: linksTo(5),
},
want: math.NaN(),
},
},
wantLevels: []level{
{
q: math.Inf(-1), // Here math.Inf(-1) is used as a place holder for NaN to allow use of reflect.DeepEqual.
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
},
},
},
},
{
name: "simple_directed",
layers: []layer{{g: simpleDirected, weight: 1}},
// community structure and modularity calculated by C++ implementation: louvain igraph.
// Note that louvain igraph returns Q as an unscaled value.
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1),
1: linksTo(2, 3, 4),
},
want: 0.5714285714285716,
tol: 1e-10,
},
},
wantLevels: []level{
{
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1)},
{simple.Node(2), simple.Node(3), simple.Node(4)},
},
q: 0.5714285714285716,
},
{
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
},
q: -1.2857142857142856,
},
},
},
{
name: "simple_directed_twice",
layers: []layer{
{g: simpleDirected, weight: 0.5},
{g: simpleDirected, weight: 0.5},
},
// community structure and modularity calculated by C++ implementation: louvain igraph.
// Note that louvain igraph returns Q as an unscaled value.
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1),
1: linksTo(2, 3, 4),
},
want: 0.5714285714285716,
tol: 1e-10,
},
},
wantLevels: []level{
{
q: 0.5714285714285716,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1)},
{simple.Node(2), simple.Node(3), simple.Node(4)},
},
},
{
q: -1.2857142857142856,
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
},
},
},
},
{
name: "small_dumbell",
layers: []layer{
{g: smallDumbell, edgeWeight: 1, weight: 1},
{g: dumbellRepulsion, edgeWeight: -1, weight: -1},
},
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1, 2),
1: linksTo(3, 4, 5),
},
want: 2.5714285714285716, tol: 1e-10,
},
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1, 2, 3, 4, 5),
},
want: 0, tol: 1e-14,
},
},
wantLevels: []level{
{
q: 2.5714285714285716,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1), simple.Node(2)},
{simple.Node(3), simple.Node(4), simple.Node(5)},
},
},
{
q: -0.857142857142857,
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
},
},
},
},
{
name: "repulsion",
layers: []layer{{g: repulsion, edgeWeight: -1, weight: -1}},
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1, 2),
1: linksTo(3, 4, 5),
},
want: 9.0, tol: 1e-10,
},
{
resolution: 1,
memberships: []set{
0: linksTo(0),
1: linksTo(1),
2: linksTo(2),
3: linksTo(3),
4: linksTo(4),
5: linksTo(5),
},
want: 3, tol: 1e-14,
},
},
wantLevels: []level{
{
q: 9.0,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1), simple.Node(2)},
{simple.Node(3), simple.Node(4), simple.Node(5)},
},
},
{
q: 3.0,
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
},
},
},
},
{
name: "middle_east",
layers: []layer{
{g: middleEast.friends, edgeWeight: 1, weight: 1},
{g: middleEast.enemies, edgeWeight: -1, weight: -1},
},
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0, 6),
1: linksTo(1, 7, 9, 12),
2: linksTo(2, 8, 11),
3: linksTo(3, 4, 5, 10),
},
want: 33.818057455540355, tol: 1e-9,
},
{
resolution: 1,
memberships: []set{
0: linksTo(0, 2, 3, 4, 5, 10),
1: linksTo(1, 7, 9, 12),
2: linksTo(6),
3: linksTo(8, 11),
},
want: 30.92749658, tol: 1e-7,
},
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12),
},
want: 0, tol: 1e-14,
},
},
wantLevels: []level{
{
q: 33.818057455540355,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(6)},
{simple.Node(1), simple.Node(7), simple.Node(9), simple.Node(12)},
{simple.Node(2), simple.Node(8), simple.Node(11)},
{simple.Node(3), simple.Node(4), simple.Node(5), simple.Node(10)},
},
},
{
q: 3.8071135430916545,
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
{simple.Node(6)},
{simple.Node(7)},
{simple.Node(8)},
{simple.Node(9)},
{simple.Node(10)},
{simple.Node(11)},
{simple.Node(12)},
},
},
},
},
}
func TestCommunityQDirectedMultiplex(t *testing.T) {
for _, test := range communityDirectedMultiplexQTests {
g, weights, err := directedMultiplexFrom(test.layers)
if err != nil {
t.Errorf("unexpected error creating multiplex: %v", err)
continue
}
for _, structure := range test.structures {
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communities[i] = append(communities[i], simple.Node(n))
}
}
q := QMultiplex(g, communities, weights, []float64{structure.resolution})
got := floats.Sum(q)
if !floats.EqualWithinAbsOrRel(got, structure.want, structure.tol, structure.tol) && !math.IsNaN(structure.want) {
for _, c := range communities {
sort.Sort(ordered.ByID(c))
}
t.Errorf("unexpected Q value for %q %v: got: %v %.3v want: %v",
test.name, communities, got, q, structure.want)
}
}
}
}
func TestCommunityDeltaQDirectedMultiplex(t *testing.T) {
tests:
for _, test := range communityDirectedMultiplexQTests {
g, weights, err := directedMultiplexFrom(test.layers)
if err != nil {
t.Errorf("unexpected error creating multiplex: %v", err)
continue
}
rnd := rand.New(rand.NewSource(1)).Intn
for _, structure := range test.structures {
communityOf := make(map[int]int)
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communityOf[n] = i
communities[i] = append(communities[i], simple.Node(n))
}
sort.Sort(ordered.ByID(communities[i]))
}
resolution := []float64{structure.resolution}
before := QMultiplex(g, communities, weights, resolution)
// We test exhaustively.
const all = true
l := newDirectedMultiplexLocalMover(
reduceDirectedMultiplex(g, nil, weights),
communities, weights, resolution, all)
if l == nil {
if !math.IsNaN(floats.Sum(before)) {
t.Errorf("unexpected nil localMover with non-NaN Q graph: Q=%.4v", before)
}
continue tests
}
// This is done to avoid run-to-run
// variation due to map iteration order.
sort.Sort(ordered.ByID(l.nodes))
l.shuffle(rnd)
for _, target := range l.nodes {
got, gotDst, gotSrc := l.deltaQ(target)
want, wantDst := math.Inf(-1), -1
migrated := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
if n == target.ID() {
continue
}
migrated[i] = append(migrated[i], simple.Node(n))
}
sort.Sort(ordered.ByID(migrated[i]))
}
for i, c := range structure.memberships {
if i == communityOf[target.ID()] {
continue
}
if !(all && hasNegative(weights)) {
connected := false
search:
for l := 0; l < g.Depth(); l++ {
if weights[l] < 0 {
connected = true
break search
}
layer := g.Layer(l)
for n := range c {
if layer.HasEdgeBetween(simple.Node(n), target) {
connected = true
break search
}
}
}
if !connected {
continue
}
}
migrated[i] = append(migrated[i], target)
after := QMultiplex(g, migrated, weights, resolution)
migrated[i] = migrated[i][:len(migrated[i])-1]
if delta := floats.Sum(after) - floats.Sum(before); delta > want {
want = delta
wantDst = i
}
}
if !floats.EqualWithinAbsOrRel(got, want, structure.tol, structure.tol) || gotDst != wantDst {
t.Errorf("unexpected result moving n=%d in c=%d of %s/%.4v: got: %.4v,%d want: %.4v,%d"+
"\n\t%v\n\t%v",
target.ID(), communityOf[target.ID()], test.name, structure.resolution, got, gotDst, want, wantDst,
communities, migrated)
}
if gotSrc.community != communityOf[target.ID()] {
t.Errorf("unexpected source community index: got: %d want: %d", gotSrc, communityOf[target.ID()])
} else if communities[gotSrc.community][gotSrc.node].ID() != target.ID() {
wantNodeIdx := -1
for i, n := range communities[gotSrc.community] {
if n.ID() == target.ID() {
wantNodeIdx = i
break
}
}
t.Errorf("unexpected source node index: got: %d want: %d", gotSrc.node, wantNodeIdx)
}
}
}
}
}
func TestReduceQConsistencyDirectedMultiplex(t *testing.T) {
tests:
for _, test := range communityDirectedMultiplexQTests {
g, weights, err := directedMultiplexFrom(test.layers)
if err != nil {
t.Errorf("unexpected error creating multiplex: %v", err)
continue
}
for _, structure := range test.structures {
if math.IsNaN(structure.want) {
continue tests
}
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communities[i] = append(communities[i], simple.Node(n))
}
sort.Sort(ordered.ByID(communities[i]))
}
gQ := QMultiplex(g, communities, weights, []float64{structure.resolution})
gQnull := QMultiplex(g, nil, weights, nil)
cg0 := reduceDirectedMultiplex(g, nil, weights)
cg0Qnull := QMultiplex(cg0, cg0.Structure(), weights, nil)
if !floats.EqualWithinAbsOrRel(floats.Sum(gQnull), floats.Sum(cg0Qnull), structure.tol, structure.tol) {
t.Errorf("disagreement between null Q from method: %v and function: %v", cg0Qnull, gQnull)
}
cg0Q := QMultiplex(cg0, communities, weights, []float64{structure.resolution})
if !floats.EqualWithinAbsOrRel(floats.Sum(gQ), floats.Sum(cg0Q), structure.tol, structure.tol) {
t.Errorf("unexpected Q result after initial reduction: got: %v want :%v", cg0Q, gQ)
}
cg1 := reduceDirectedMultiplex(cg0, communities, weights)
cg1Q := QMultiplex(cg1, cg1.Structure(), weights, []float64{structure.resolution})
if !floats.EqualWithinAbsOrRel(floats.Sum(gQ), floats.Sum(cg1Q), structure.tol, structure.tol) {
t.Errorf("unexpected Q result after second reduction: got: %v want :%v", cg1Q, gQ)
}
}
}
}
var localDirectedMultiplexMoveTests = []struct {
name string
layers []layer
structures []moveStructures
}{
{
name: "blondel",
layers: []layer{{g: blondel, weight: 1}, {g: blondel, weight: 0.5}},
structures: []moveStructures{
{
memberships: []set{
0: linksTo(0, 1, 2, 4, 5),
1: linksTo(3, 6, 7),
2: linksTo(8, 9, 10, 12, 14, 15),
3: linksTo(11, 13),
},
targetNodes: []graph.Node{simple.Node(0)},
resolution: 1,
tol: 1e-14,
},
{
memberships: []set{
0: linksTo(0, 1, 2, 4, 5),
1: linksTo(3, 6, 7),
2: linksTo(8, 9, 10, 12, 14, 15),
3: linksTo(11, 13),
},
targetNodes: []graph.Node{simple.Node(3)},
resolution: 1,
tol: 1e-14,
},
{
memberships: []set{
0: linksTo(0, 1, 2, 4, 5),
1: linksTo(3, 6, 7),
2: linksTo(8, 9, 10, 12, 14, 15),
3: linksTo(11, 13),
},
// Case to demonstrate when A_aa != k_a^𝛼.
targetNodes: []graph.Node{simple.Node(3), simple.Node(2)},
resolution: 1,
tol: 1e-14,
},
},
},
}
func TestMoveLocalDirectedMultiplex(t *testing.T) {
for _, test := range localDirectedMultiplexMoveTests {
g, weights, err := directedMultiplexFrom(test.layers)
if err != nil {
t.Errorf("unexpected error creating multiplex: %v", err)
continue
}
for _, structure := range test.structures {
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communities[i] = append(communities[i], simple.Node(n))
}
sort.Sort(ordered.ByID(communities[i]))
}
r := reduceDirectedMultiplex(reduceDirectedMultiplex(g, nil, weights), communities, weights)
l := newDirectedMultiplexLocalMover(r, r.communities, weights, []float64{structure.resolution}, true)
for _, n := range structure.targetNodes {
dQ, dst, src := l.deltaQ(n)
if dQ > 0 {
before := floats.Sum(QMultiplex(r, l.communities, weights, []float64{structure.resolution}))
l.move(dst, src)
after := floats.Sum(QMultiplex(r, l.communities, weights, []float64{structure.resolution}))
want := after - before
if !floats.EqualWithinAbsOrRel(dQ, want, structure.tol, structure.tol) {
t.Errorf("unexpected deltaQ: got: %v want: %v", dQ, want)
}
}
}
}
}
}
func TestLouvainDirectedMultiplex(t *testing.T) {
const louvainIterations = 20
for _, test := range communityDirectedMultiplexQTests {
g, weights, err := directedMultiplexFrom(test.layers)
if err != nil {
t.Errorf("unexpected error creating multiplex: %v", err)
continue
}
if test.structures[0].resolution != 1 {
panic("bad test: expect resolution=1")
}
want := make([][]graph.Node, len(test.structures[0].memberships))
for i, c := range test.structures[0].memberships {
for n := range c {
want[i] = append(want[i], simple.Node(n))
}
sort.Sort(ordered.ByID(want[i]))
}
sort.Sort(ordered.BySliceIDs(want))
var (
got *ReducedDirectedMultiplex
bestQ = math.Inf(-1)
)
// Modularize is randomised so we do this to
// ensure the level tests are consistent.
src := rand.New(rand.NewSource(1))
for i := 0; i < louvainIterations; i++ {
r := ModularizeMultiplex(g, weights, nil, true, src).(*ReducedDirectedMultiplex)
if q := floats.Sum(QMultiplex(r, nil, weights, nil)); q > bestQ || math.IsNaN(q) {
bestQ = q
got = r
if math.IsNaN(q) {
// Don't try again for non-connected case.
break
}
}
var qs []float64
for p := r; p != nil; p = p.Expanded().(*ReducedDirectedMultiplex) {
qs = append(qs, floats.Sum(QMultiplex(p, nil, weights, nil)))
}
// Recovery of Q values is reversed.
if reverse(qs); !sort.Float64sAreSorted(qs) {
t.Errorf("Q values not monotonically increasing: %.5v", qs)
}
}
gotCommunities := got.Communities()
for _, c := range gotCommunities {
sort.Sort(ordered.ByID(c))
}
sort.Sort(ordered.BySliceIDs(gotCommunities))
if !reflect.DeepEqual(gotCommunities, want) {
t.Errorf("unexpected community membership for %s Q=%.4v:\n\tgot: %v\n\twant:%v",
test.name, bestQ, gotCommunities, want)
continue
}
var levels []level
for p := got; p != nil; p = p.Expanded().(*ReducedDirectedMultiplex) {
var communities [][]graph.Node
if p.parent != nil {
communities = p.parent.Communities()
for _, c := range communities {
sort.Sort(ordered.ByID(c))
}
sort.Sort(ordered.BySliceIDs(communities))
} else {
communities = reduceDirectedMultiplex(g, nil, weights).Communities()
}
q := floats.Sum(QMultiplex(p, nil, weights, nil))
if math.IsNaN(q) {
// Use an equalable flag value in place of NaN.
q = math.Inf(-1)
}
levels = append(levels, level{q: q, communities: communities})
}
if !reflect.DeepEqual(levels, test.wantLevels) {
t.Errorf("unexpected level structure:\n\tgot: %v\n\twant:%v", levels, test.wantLevels)
}
}
}
func TestNonContiguousDirectedMultiplex(t *testing.T) {
g := simple.NewDirectedGraph(0, 0)
for _, e := range []simple.Edge{
{F: simple.Node(0), T: simple.Node(1), W: 1},
{F: simple.Node(4), T: simple.Node(5), W: 1},
} {
g.SetEdge(e)
}
func() {
defer func() {
r := recover()
if r != nil {
t.Error("unexpected panic with non-contiguous ID range")
}
}()
ModularizeMultiplex(DirectedLayers{g}, nil, nil, true, nil)
}()
}
func BenchmarkLouvainDirectedMultiplex(b *testing.B) {
src := rand.New(rand.NewSource(1))
for i := 0; i < b.N; i++ {
ModularizeMultiplex(DirectedLayers{dupGraphDirected}, nil, nil, true, src)
}
}
func directedMultiplexFrom(raw []layer) (DirectedLayers, []float64, error) {
var layers []graph.Directed
var weights []float64
for _, l := range raw {
g := simple.NewDirectedGraph(0, 0)
for u, e := range l.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
w := 1.0
if l.edgeWeight != 0 {
w = l.edgeWeight
}
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: w})
}
}
layers = append(layers, g)
weights = append(weights, l.weight)
}
g, err := NewDirectedLayers(layers...)
if err != nil {
return nil, nil, err
}
return g, weights, nil
}

View File

@@ -0,0 +1,589 @@
// 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 community
import (
"math"
"math/rand"
"reflect"
"sort"
"testing"
"github.com/gonum/floats"
"github.com/gonum/graph"
"github.com/gonum/graph/internal/ordered"
"github.com/gonum/graph/simple"
)
var communityDirectedQTests = []struct {
name string
g []set
structures []structure
wantLevels []level
}{
{
name: "simple_directed",
g: simpleDirected,
// community structure and modularity calculated by C++ implementation: louvain igraph.
// Note that louvain igraph returns Q as an unscaled value.
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1),
1: linksTo(2, 3, 4),
},
want: 0.5714285714285716 / 7,
tol: 1e-10,
},
},
wantLevels: []level{
{
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1)},
{simple.Node(2), simple.Node(3), simple.Node(4)},
},
q: 0.5714285714285716 / 7,
},
{
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
},
q: -1.2857142857142856 / 7,
},
},
},
{
name: "zachary",
g: zachary,
// community structure and modularity calculated by C++ implementation: louvain igraph.
// Note that louvain igraph returns Q as an unscaled value.
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1, 2, 3, 7, 11, 12, 13, 17, 19, 21),
1: linksTo(4, 5, 6, 10, 16),
2: linksTo(8, 9, 14, 15, 18, 20, 22, 26, 29, 30, 32, 33),
3: linksTo(23, 24, 25, 27, 28, 31),
},
want: 34.3417721519 / 79 /* 5->6 and 6->5 because of co-equal rank */, tol: 1e-4,
},
},
wantLevels: []level{
{
q: 0.43470597660631316,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(3), simple.Node(7), simple.Node(11), simple.Node(12), simple.Node(13), simple.Node(17), simple.Node(19), simple.Node(21)},
{simple.Node(4), simple.Node(5), simple.Node(6), simple.Node(10), simple.Node(16)},
{simple.Node(8), simple.Node(9), simple.Node(14), simple.Node(15), simple.Node(18), simple.Node(20), simple.Node(22), simple.Node(26), simple.Node(29), simple.Node(30), simple.Node(32), simple.Node(33)},
{simple.Node(23), simple.Node(24), simple.Node(25), simple.Node(27), simple.Node(28), simple.Node(31)},
},
},
{
q: 0.3911232174331037,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(3), simple.Node(7), simple.Node(11), simple.Node(12), simple.Node(13), simple.Node(17), simple.Node(19), simple.Node(21)},
{simple.Node(4), simple.Node(10)},
{simple.Node(5), simple.Node(6), simple.Node(16)},
{simple.Node(8), simple.Node(30)},
{simple.Node(9), simple.Node(14), simple.Node(15), simple.Node(18), simple.Node(20), simple.Node(22), simple.Node(32), simple.Node(33)},
{simple.Node(23), simple.Node(24), simple.Node(25), simple.Node(27), simple.Node(28), simple.Node(31)},
{simple.Node(26), simple.Node(29)},
},
},
{
q: -0.014580996635154624,
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
{simple.Node(6)},
{simple.Node(7)},
{simple.Node(8)},
{simple.Node(9)},
{simple.Node(10)},
{simple.Node(11)},
{simple.Node(12)},
{simple.Node(13)},
{simple.Node(14)},
{simple.Node(15)},
{simple.Node(16)},
{simple.Node(17)},
{simple.Node(18)},
{simple.Node(19)},
{simple.Node(20)},
{simple.Node(21)},
{simple.Node(22)},
{simple.Node(23)},
{simple.Node(24)},
{simple.Node(25)},
{simple.Node(26)},
{simple.Node(27)},
{simple.Node(28)},
{simple.Node(29)},
{simple.Node(30)},
{simple.Node(31)},
{simple.Node(32)},
{simple.Node(33)},
},
},
},
},
{
name: "blondel",
g: blondel,
// community structure and modularity calculated by C++ implementation: louvain igraph.
// Note that louvain igraph returns Q as an unscaled value.
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1, 2, 3, 4, 5, 6, 7),
1: linksTo(8, 9, 10, 11, 12, 13, 14, 15),
},
want: 11.1428571429 / 28, tol: 1e-4,
},
},
wantLevels: []level{
{
q: 0.3979591836734694,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(3), simple.Node(4), simple.Node(5), simple.Node(6), simple.Node(7)},
{simple.Node(8), simple.Node(9), simple.Node(10), simple.Node(11), simple.Node(12), simple.Node(13), simple.Node(14), simple.Node(15)},
},
},
{
q: 0.32525510204081637,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(3), simple.Node(5), simple.Node(7)},
{simple.Node(1), simple.Node(2), simple.Node(4), simple.Node(6)},
{simple.Node(8), simple.Node(10), simple.Node(11), simple.Node(13), simple.Node(15)},
{simple.Node(9), simple.Node(12), simple.Node(14)},
},
},
{
q: -0.022959183673469385,
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
{simple.Node(6)},
{simple.Node(7)},
{simple.Node(8)},
{simple.Node(9)},
{simple.Node(10)},
{simple.Node(11)},
{simple.Node(12)},
{simple.Node(13)},
{simple.Node(14)},
{simple.Node(15)},
},
},
},
},
}
func TestCommunityQDirected(t *testing.T) {
for _, test := range communityDirectedQTests {
g := simple.NewDirectedGraph(0, 0)
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
for _, structure := range test.structures {
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communities[i] = append(communities[i], simple.Node(n))
}
}
got := Q(g, communities, structure.resolution)
if !floats.EqualWithinAbsOrRel(got, structure.want, structure.tol, structure.tol) && !math.IsNaN(structure.want) {
for _, c := range communities {
sort.Sort(ordered.ByID(c))
}
t.Errorf("unexpected Q value for %q %v: got: %v want: %v",
test.name, communities, got, structure.want)
}
}
}
}
func TestCommunityDeltaQDirected(t *testing.T) {
tests:
for _, test := range communityDirectedQTests {
g := simple.NewDirectedGraph(0, 0)
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
rnd := rand.New(rand.NewSource(1)).Intn
for _, structure := range test.structures {
communityOf := make(map[int]int)
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communityOf[n] = i
communities[i] = append(communities[i], simple.Node(n))
}
sort.Sort(ordered.ByID(communities[i]))
}
before := Q(g, communities, structure.resolution)
l := newDirectedLocalMover(reduceDirected(g, nil), communities, structure.resolution)
if l == nil {
if !math.IsNaN(before) {
t.Errorf("unexpected nil localMover with non-NaN Q graph: Q=%.4v", before)
}
continue tests
}
// This is done to avoid run-to-run
// variation due to map iteration order.
sort.Sort(ordered.ByID(l.nodes))
l.shuffle(rnd)
for _, target := range l.nodes {
got, gotDst, gotSrc := l.deltaQ(target)
want, wantDst := math.Inf(-1), -1
migrated := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
if n == target.ID() {
continue
}
migrated[i] = append(migrated[i], simple.Node(n))
}
sort.Sort(ordered.ByID(migrated[i]))
}
for i, c := range structure.memberships {
if i == communityOf[target.ID()] {
continue
}
connected := false
for n := range c {
if g.HasEdgeBetween(simple.Node(n), target) {
connected = true
break
}
}
if !connected {
continue
}
migrated[i] = append(migrated[i], target)
after := Q(g, migrated, structure.resolution)
migrated[i] = migrated[i][:len(migrated[i])-1]
if after-before > want {
want = after - before
wantDst = i
}
}
if !floats.EqualWithinAbsOrRel(got, want, structure.tol, structure.tol) || gotDst != wantDst {
t.Errorf("unexpected result moving n=%d in c=%d of %s/%.4v: got: %.4v,%d want: %.4v,%d"+
"\n\t%v\n\t%v",
target.ID(), communityOf[target.ID()], test.name, structure.resolution, got, gotDst, want, wantDst,
communities, migrated)
}
if gotSrc.community != communityOf[target.ID()] {
t.Errorf("unexpected source community index: got: %d want: %d", gotSrc, communityOf[target.ID()])
} else if communities[gotSrc.community][gotSrc.node].ID() != target.ID() {
wantNodeIdx := -1
for i, n := range communities[gotSrc.community] {
if n.ID() == target.ID() {
wantNodeIdx = i
break
}
}
t.Errorf("unexpected source node index: got: %d want: %d", gotSrc.node, wantNodeIdx)
}
}
}
}
}
func TestReduceQConsistencyDirected(t *testing.T) {
tests:
for _, test := range communityDirectedQTests {
g := simple.NewDirectedGraph(0, 0)
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
for _, structure := range test.structures {
if math.IsNaN(structure.want) {
continue tests
}
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communities[i] = append(communities[i], simple.Node(n))
}
sort.Sort(ordered.ByID(communities[i]))
}
gQ := Q(g, communities, structure.resolution)
gQnull := Q(g, nil, 1)
cg0 := reduceDirected(g, nil)
cg0Qnull := Q(cg0, cg0.Structure(), 1)
if !floats.EqualWithinAbsOrRel(gQnull, cg0Qnull, structure.tol, structure.tol) {
t.Errorf("disagreement between null Q from method: %v and function: %v", cg0Qnull, gQnull)
}
cg0Q := Q(cg0, communities, structure.resolution)
if !floats.EqualWithinAbsOrRel(gQ, cg0Q, structure.tol, structure.tol) {
t.Errorf("unexpected Q result after initial reduction: got: %v want :%v", cg0Q, gQ)
}
cg1 := reduceDirected(cg0, communities)
cg1Q := Q(cg1, cg1.Structure(), structure.resolution)
if !floats.EqualWithinAbsOrRel(gQ, cg1Q, structure.tol, structure.tol) {
t.Errorf("unexpected Q result after second reduction: got: %v want :%v", cg1Q, gQ)
}
}
}
}
var localDirectedMoveTests = []struct {
name string
g []set
structures []moveStructures
}{
{
name: "blondel",
g: blondel,
structures: []moveStructures{
{
memberships: []set{
0: linksTo(0, 1, 2, 4, 5),
1: linksTo(3, 6, 7),
2: linksTo(8, 9, 10, 12, 14, 15),
3: linksTo(11, 13),
},
targetNodes: []graph.Node{simple.Node(0)},
resolution: 1,
tol: 1e-14,
},
{
memberships: []set{
0: linksTo(0, 1, 2, 4, 5),
1: linksTo(3, 6, 7),
2: linksTo(8, 9, 10, 12, 14, 15),
3: linksTo(11, 13),
},
targetNodes: []graph.Node{simple.Node(3)},
resolution: 1,
tol: 1e-14,
},
{
memberships: []set{
0: linksTo(0, 1, 2, 4, 5),
1: linksTo(3, 6, 7),
2: linksTo(8, 9, 10, 12, 14, 15),
3: linksTo(11, 13),
},
// Case to demonstrate when A_aa != k_a^𝛼.
targetNodes: []graph.Node{simple.Node(3), simple.Node(2)},
resolution: 1,
tol: 1e-14,
},
},
},
}
func TestMoveLocalDirected(t *testing.T) {
for _, test := range localDirectedMoveTests {
g := simple.NewDirectedGraph(0, 0)
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
for _, structure := range test.structures {
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communities[i] = append(communities[i], simple.Node(n))
}
sort.Sort(ordered.ByID(communities[i]))
}
r := reduceDirected(reduceDirected(g, nil), communities)
l := newDirectedLocalMover(r, r.communities, structure.resolution)
for _, n := range structure.targetNodes {
dQ, dst, src := l.deltaQ(n)
if dQ > 0 {
before := Q(r, l.communities, structure.resolution)
l.move(dst, src)
after := Q(r, l.communities, structure.resolution)
want := after - before
if !floats.EqualWithinAbsOrRel(dQ, want, structure.tol, structure.tol) {
t.Errorf("unexpected deltaQ: got: %v want: %v", dQ, want)
}
}
}
}
}
}
func TestModularizeDirected(t *testing.T) {
const louvainIterations = 20
for _, test := range communityDirectedQTests {
g := simple.NewDirectedGraph(0, 0)
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
if test.structures[0].resolution != 1 {
panic("bad test: expect resolution=1")
}
want := make([][]graph.Node, len(test.structures[0].memberships))
for i, c := range test.structures[0].memberships {
for n := range c {
want[i] = append(want[i], simple.Node(n))
}
sort.Sort(ordered.ByID(want[i]))
}
sort.Sort(ordered.BySliceIDs(want))
var (
got *ReducedDirected
bestQ = math.Inf(-1)
)
// Modularize is randomised so we do this to
// ensure the level tests are consistent.
src := rand.New(rand.NewSource(1))
for i := 0; i < louvainIterations; i++ {
r := Modularize(g, 1, src).(*ReducedDirected)
if q := Q(r, nil, 1); q > bestQ || math.IsNaN(q) {
bestQ = q
got = r
if math.IsNaN(q) {
// Don't try again for non-connected case.
break
}
}
var qs []float64
for p := r; p != nil; p = p.Expanded().(*ReducedDirected) {
qs = append(qs, Q(p, nil, 1))
}
// Recovery of Q values is reversed.
if reverse(qs); !sort.Float64sAreSorted(qs) {
t.Errorf("Q values not monotonically increasing: %.5v", qs)
}
}
gotCommunities := got.Communities()
for _, c := range gotCommunities {
sort.Sort(ordered.ByID(c))
}
sort.Sort(ordered.BySliceIDs(gotCommunities))
if !reflect.DeepEqual(gotCommunities, want) {
t.Errorf("unexpected community membership for %s Q=%.4v:\n\tgot: %v\n\twant:%v",
test.name, bestQ, gotCommunities, want)
continue
}
var levels []level
for p := got; p != nil; p = p.Expanded().(*ReducedDirected) {
var communities [][]graph.Node
if p.parent != nil {
communities = p.parent.Communities()
for _, c := range communities {
sort.Sort(ordered.ByID(c))
}
sort.Sort(ordered.BySliceIDs(communities))
} else {
communities = reduceDirected(g, nil).Communities()
}
q := Q(p, nil, 1)
if math.IsNaN(q) {
// Use an equalable flag value in place of NaN.
q = math.Inf(-1)
}
levels = append(levels, level{q: q, communities: communities})
}
if !reflect.DeepEqual(levels, test.wantLevels) {
t.Errorf("unexpected level structure:\n\tgot: %v\n\twant:%v", levels, test.wantLevels)
}
}
}
func TestNonContiguousDirected(t *testing.T) {
g := simple.NewDirectedGraph(0, 0)
for _, e := range []simple.Edge{
{F: simple.Node(0), T: simple.Node(1), W: 1},
{F: simple.Node(4), T: simple.Node(5), W: 1},
} {
g.SetEdge(e)
}
func() {
defer func() {
r := recover()
if r != nil {
t.Error("unexpected panic with non-contiguous ID range")
}
}()
Modularize(g, 1, nil)
}()
}
func BenchmarkLouvainDirected(b *testing.B) {
src := rand.New(rand.NewSource(1))
for i := 0; i < b.N; i++ {
Modularize(dupGraphDirected, 1, src)
}
}

View File

@@ -0,0 +1,277 @@
// 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 community
import (
"fmt"
"math/rand"
"github.com/gonum/graph"
"github.com/gonum/graph/graphs/gen"
"github.com/gonum/graph/simple"
)
// set is an integer set.
type set map[int]struct{}
func linksTo(i ...int) set {
if len(i) == 0 {
return nil
}
s := make(set)
for _, v := range i {
s[v] = struct{}{}
}
return s
}
type layer struct {
g []set
edgeWeight float64 // Zero edge weight is interpreted as 1.0.
weight float64
}
var (
unconnected = []set{ /* Nodes 0-4 are implicit .*/ 5: nil}
smallDumbell = []set{
0: linksTo(1, 2),
1: linksTo(2),
2: linksTo(3),
3: linksTo(4, 5),
4: linksTo(5),
5: nil,
}
dumbellRepulsion = []set{
0: linksTo(4),
1: linksTo(5),
2: nil,
3: nil,
4: nil,
5: nil,
}
repulsion = []set{
0: linksTo(3, 4, 5),
1: linksTo(3, 4, 5),
2: linksTo(3, 4, 5),
3: linksTo(0, 1, 2),
4: linksTo(0, 1, 2),
5: linksTo(0, 1, 2),
}
simpleDirected = []set{
0: linksTo(1),
1: linksTo(0, 4),
2: linksTo(1),
3: linksTo(0, 4),
4: linksTo(2),
}
// http://www.slate.com/blogs/the_world_/2014/07/17/the_middle_east_friendship_chart.html
middleEast = struct{ friends, complicated, enemies []set }{
// green cells
friends: []set{
0: nil,
1: linksTo(5, 7, 9, 12),
2: linksTo(11),
3: linksTo(4, 5, 10),
4: linksTo(3, 5, 10),
5: linksTo(1, 3, 4, 8, 10, 12),
6: nil,
7: linksTo(1, 12),
8: linksTo(5, 9, 11),
9: linksTo(1, 8, 12),
10: linksTo(3, 4, 5),
11: linksTo(2, 8),
12: linksTo(1, 5, 7, 9),
},
// yellow cells
complicated: []set{
0: linksTo(2, 4),
1: linksTo(4, 8),
2: linksTo(0, 3, 4, 5, 8, 9),
3: linksTo(2, 8, 11),
4: linksTo(0, 1, 2, 8),
5: linksTo(2),
6: nil,
7: linksTo(9, 11),
8: linksTo(1, 2, 3, 4, 10, 12),
9: linksTo(2, 7, 11),
10: linksTo(8),
11: linksTo(3, 7, 9, 12),
12: linksTo(8, 11),
},
// red cells
enemies: []set{
0: linksTo(1, 3, 5, 6, 7, 8, 9, 10, 11, 12),
1: linksTo(0, 2, 3, 6, 10, 11),
2: linksTo(1, 6, 7, 10, 12),
3: linksTo(0, 1, 6, 7, 9, 12),
4: linksTo(6, 7, 9, 11, 12),
5: linksTo(0, 6, 7, 9, 11),
6: linksTo(0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12),
7: linksTo(0, 2, 3, 4, 5, 6, 8, 10),
8: linksTo(0, 6, 7),
9: linksTo(0, 3, 4, 5, 6, 10),
10: linksTo(0, 1, 2, 6, 7, 9, 11, 12),
11: linksTo(0, 1, 4, 5, 6, 10),
12: linksTo(0, 2, 3, 4, 6, 10),
},
}
// W. W. Zachary, An information flow model for conflict and fission in small groups,
// Journal of Anthropological Research 33, 452-473 (1977).
//
// The edge list here is constructed such that all link descriptions
// head from a node with lower Page Rank to a node with higher Page
// Rank. This has no impact on undirected tests, but allows a sensible
// view for directed tests.
zachary = []set{
0: nil, // rank=0.097
1: linksTo(0, 2), // rank=0.05288
2: linksTo(0, 32), // rank=0.05708
3: linksTo(0, 1, 2), // rank=0.03586
4: linksTo(0, 6, 10), // rank=0.02198
5: linksTo(0, 6), // rank=0.02911
6: linksTo(0, 5), // rank=0.02911
7: linksTo(0, 1, 2, 3), // rank=0.02449
8: linksTo(0, 2, 32, 33), // rank=0.02977
9: linksTo(2, 33), // rank=0.01431
10: linksTo(0, 5), // rank=0.02198
11: linksTo(0), // rank=0.009565
12: linksTo(0, 3), // rank=0.01464
13: linksTo(0, 1, 2, 3, 33), // rank=0.02954
14: linksTo(32, 33), // rank=0.01454
15: linksTo(32, 33), // rank=0.01454
16: linksTo(5, 6), // rank=0.01678
17: linksTo(0, 1), // rank=0.01456
18: linksTo(32, 33), // rank=0.01454
19: linksTo(0, 1, 33), // rank=0.0196
20: linksTo(32, 33), // rank=0.01454
21: linksTo(0, 1), // rank=0.01456
22: linksTo(32, 33), // rank=0.01454
23: linksTo(32, 33), // rank=0.03152
24: linksTo(27, 31), // rank=0.02108
25: linksTo(23, 24, 31), // rank=0.02101
26: linksTo(29, 33), // rank=0.01504
27: linksTo(2, 23, 33), // rank=0.02564
28: linksTo(2, 31, 33), // rank=0.01957
29: linksTo(23, 32, 33), // rank=0.02629
30: linksTo(1, 8, 32, 33), // rank=0.02459
31: linksTo(0, 32, 33), // rank=0.03716
32: linksTo(33), // rank=0.07169
33: nil, // rank=0.1009
}
// doi:10.1088/1742-5468/2008/10/P10008 figure 1
//
// The edge list here is constructed such that all link descriptions
// head from a node with lower Page Rank to a node with higher Page
// Rank. This has no impact on undirected tests, but allows a sensible
// view for directed tests.
blondel = []set{
0: linksTo(2), // rank=0.06858
1: linksTo(2, 4, 7), // rank=0.05264
2: nil, // rank=0.08249
3: linksTo(0, 7), // rank=0.03884
4: linksTo(0, 2, 10), // rank=0.06754
5: linksTo(0, 2, 7, 11), // rank=0.06738
6: linksTo(2, 7, 11), // rank=0.0528
7: nil, // rank=0.07008
8: linksTo(10), // rank=0.09226
9: linksTo(8), // rank=0.05821
10: nil, // rank=0.1035
11: linksTo(8, 10), // rank=0.08538
12: linksTo(9, 10), // rank=0.04052
13: linksTo(10, 11), // rank=0.03855
14: linksTo(8, 9, 10), // rank=0.05621
15: linksTo(8), // rank=0.02506
}
)
type structure struct {
resolution float64
memberships []set
want, tol float64
}
type level struct {
q float64
communities [][]graph.Node
}
type moveStructures struct {
memberships []set
targetNodes []graph.Node
resolution float64
tol float64
}
func reverse(f []float64) {
for i, j := 0, len(f)-1; i < j; i, j = i+1, j-1 {
f[i], f[j] = f[j], f[i]
}
}
func hasNegative(f []float64) bool {
for _, v := range f {
if v < 0 {
return true
}
}
return false
}
var (
dupGraph = simple.NewUndirectedGraph(0, 0)
dupGraphDirected = simple.NewDirectedGraph(0, 0)
)
func init() {
err := gen.Duplication(dupGraph, 1000, 0.8, 0.1, 0.5, rand.New(rand.NewSource(1)))
if err != nil {
panic(err)
}
// Construct a directed graph from dupGraph
// such that every edge dupGraph is replaced
// with an edge that flows from the low node
// ID to the high node ID.
for _, e := range dupGraph.Edges() {
if e.To().ID() < e.From().ID() {
se := e.(simple.Edge)
se.F, se.T = se.T, se.F
e = se
}
dupGraphDirected.SetEdge(e)
}
}
// This init function checks the Middle East relationship data.
func init() {
world := make([]set, len(middleEast.friends))
for i := range world {
world[i] = make(set)
}
for _, relationships := range [][]set{middleEast.friends, middleEast.complicated, middleEast.enemies} {
for i, rel := range relationships {
for inter := range rel {
if _, ok := world[i][inter]; ok {
panic(fmt.Sprintf("unexpected relationship: %v--%v", i, inter))
}
world[i][inter] = struct{}{}
}
}
}
for i := range world {
if len(world[i]) != len(middleEast.friends)-1 {
panic(fmt.Sprintf("missing relationship in %v: %v", i, world[i]))
}
}
}

View File

@@ -0,0 +1,568 @@
// 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 community
import (
"math"
"math/rand"
"sort"
"golang.org/x/tools/container/intsets"
"github.com/gonum/graph"
"github.com/gonum/graph/internal/ordered"
)
// qUndirected returns the modularity Q score of the graph g subdivided into the
// given communities at the given resolution. If communities is nil, the
// unclustered modularity score is returned. The resolution parameter
// is γ as defined in Reichardt and Bornholdt doi:10.1103/PhysRevE.74.016110.
// qUndirected will panic if g has any edge with negative edge weight.
//
// Q = 1/2m \sum_{ij} [ A_{ij} - (\gamma k_i k_j)/2m ] \delta(c_i,c_j)
//
// graph.Undirect may be used as a shim to allow calculation of Q for
// directed graphs.
func qUndirected(g graph.Undirected, communities [][]graph.Node, resolution float64) float64 {
nodes := g.Nodes()
weight := positiveWeightFuncFor(g)
// Calculate the total edge weight of the graph
// and the table of penetrating edge weight sums.
var m2 float64
k := make(map[int]float64, len(nodes))
for _, u := range nodes {
w := weight(u, u)
for _, v := range g.From(u) {
w += weight(u, v)
}
m2 += w
k[u.ID()] = w
}
if communities == nil {
var q float64
for _, u := range nodes {
kU := k[u.ID()]
q += weight(u, u) - resolution*kU*kU/m2
}
return q / m2
}
// Iterate over the communities, calculating
// the non-self edge weights for the upper
// triangle and adjust the diagonal.
var q float64
for _, c := range communities {
for i, u := range c {
kU := k[u.ID()]
q += weight(u, u) - resolution*kU*kU/m2
for _, v := range c[i+1:] {
q += 2 * (weight(u, v) - resolution*kU*k[v.ID()]/m2)
}
}
}
return q / m2
}
// louvainUndirected returns the hierarchical modularization of g at the given
// resolution using the Louvain algorithm. If src is nil, rand.Intn is used as
// the random generator. louvainUndirected will panic if g has any edge with negative edge
// weight.
//
// graph.Undirect may be used as a shim to allow modularization of directed graphs.
func louvainUndirected(g graph.Undirected, resolution float64, src *rand.Rand) *ReducedUndirected {
// See louvain.tex for a detailed description
// of the algorithm used here.
c := reduceUndirected(g, nil)
rnd := rand.Intn
if src != nil {
rnd = src.Intn
}
for {
l := newUndirectedLocalMover(c, c.communities, resolution)
if l == nil {
return c
}
if done := l.localMovingHeuristic(rnd); done {
return c
}
c = reduceUndirected(c, l.communities)
}
}
// ReducedUndirected is an undirected graph of communities derived from a
// parent graph by reduction.
type ReducedUndirected struct {
// nodes is the set of nodes held
// by the graph. In a ReducedUndirected
// the node ID is the index into
// nodes.
nodes []community
undirectedEdges
// communities is the community
// structure of the graph.
communities [][]graph.Node
parent *ReducedUndirected
}
var (
_ graph.Undirected = (*ReducedUndirected)(nil)
_ graph.Weighter = (*ReducedUndirected)(nil)
_ ReducedGraph = (*ReducedUndirected)(nil)
)
// Communities returns the community memberships of the nodes in the
// graph used to generate the reduced graph.
func (g *ReducedUndirected) Communities() [][]graph.Node {
communities := make([][]graph.Node, len(g.communities))
if g.parent == nil {
for i, members := range g.communities {
comm := make([]graph.Node, len(members))
for j, n := range members {
nodes := g.nodes[n.ID()].nodes
if len(nodes) != 1 {
panic("community: unexpected number of nodes in base graph community")
}
comm[j] = nodes[0]
}
communities[i] = comm
}
return communities
}
sub := g.parent.Communities()
for i, members := range g.communities {
var comm []graph.Node
for _, n := range members {
comm = append(comm, sub[n.ID()]...)
}
communities[i] = comm
}
return communities
}
// Structure returns the community structure of the current level of
// the module clustering. The first index of the returned value
// corresponds to the index of the nodes in the next higher level if
// it exists. The returned value should not be mutated.
func (g *ReducedUndirected) Structure() [][]graph.Node {
return g.communities
}
// Expanded returns the next lower level of the module clustering or nil
// if at the lowest level.
func (g *ReducedUndirected) Expanded() ReducedGraph {
return g.parent
}
// reduceUndirected returns a reduced graph constructed from g divided
// into the given communities. The communities value is mutated
// by the call to reduceUndirected. If communities is nil and g is a
// ReducedUndirected, it is returned unaltered.
func reduceUndirected(g graph.Undirected, communities [][]graph.Node) *ReducedUndirected {
if communities == nil {
if r, ok := g.(*ReducedUndirected); ok {
return r
}
nodes := g.Nodes()
// TODO(kortschak) This sort is necessary really only
// for testing. In practice we would not be using the
// community provided by the user for a Q calculation.
// Probably we should use a function to map the
// communities in the test sets to the remapped order.
sort.Sort(ordered.ByID(nodes))
communities = make([][]graph.Node, len(nodes))
for i := range nodes {
communities[i] = []graph.Node{node(i)}
}
weight := positiveWeightFuncFor(g)
r := ReducedUndirected{
nodes: make([]community, len(nodes)),
undirectedEdges: undirectedEdges{
edges: make([][]int, len(nodes)),
weights: make(map[[2]int]float64),
},
communities: communities,
}
communityOf := make(map[int]int, len(nodes))
for i, n := range nodes {
r.nodes[i] = community{id: i, nodes: []graph.Node{n}}
communityOf[n.ID()] = i
}
for _, u := range nodes {
var out []int
uid := communityOf[u.ID()]
for _, v := range g.From(u) {
vid := communityOf[v.ID()]
if vid != uid {
out = append(out, vid)
}
if uid < vid {
// Only store the weight once.
r.weights[[2]int{uid, vid}] = weight(u, v)
}
}
r.edges[uid] = out
}
return &r
}
// Remove zero length communities destructively.
var commNodes int
for i := 0; i < len(communities); {
comm := communities[i]
if len(comm) == 0 {
communities[i] = communities[len(communities)-1]
communities[len(communities)-1] = nil
communities = communities[:len(communities)-1]
} else {
commNodes += len(comm)
i++
}
}
r := ReducedUndirected{
nodes: make([]community, len(communities)),
undirectedEdges: undirectedEdges{
edges: make([][]int, len(communities)),
weights: make(map[[2]int]float64),
},
}
r.communities = make([][]graph.Node, len(communities))
for i := range r.communities {
r.communities[i] = []graph.Node{node(i)}
}
if g, ok := g.(*ReducedUndirected); ok {
// Make sure we retain the truncated
// community structure.
g.communities = communities
r.parent = g
}
weight := positiveWeightFuncFor(g)
communityOf := make(map[int]int, commNodes)
for i, comm := range communities {
r.nodes[i] = community{id: i, nodes: comm}
for _, n := range comm {
communityOf[n.ID()] = i
}
}
for uid, comm := range communities {
var out []int
for i, u := range comm {
r.nodes[uid].weight += weight(u, u)
for _, v := range comm[i+1:] {
r.nodes[uid].weight += 2 * weight(u, v)
}
for _, v := range g.From(u) {
vid := communityOf[v.ID()]
found := false
for _, e := range out {
if e == vid {
found = true
break
}
}
if !found && vid != uid {
out = append(out, vid)
}
if uid < vid {
// Only store the weight once.
r.weights[[2]int{uid, vid}] += weight(u, v)
}
}
}
r.edges[uid] = out
}
return &r
}
// Has returns whether the node exists within the graph.
func (g *ReducedUndirected) Has(n graph.Node) bool {
id := n.ID()
return id >= 0 || id < len(g.nodes)
}
// Nodes returns all the nodes in the graph.
func (g *ReducedUndirected) Nodes() []graph.Node {
nodes := make([]graph.Node, len(g.nodes))
for i := range g.nodes {
nodes[i] = node(i)
}
return nodes
}
// From returns all nodes in g that can be reached directly from u.
func (g *ReducedUndirected) From(u graph.Node) []graph.Node {
out := g.edges[u.ID()]
nodes := make([]graph.Node, len(out))
for i, vid := range out {
nodes[i] = g.nodes[vid]
}
return nodes
}
// HasEdgeBetween returns whether an edge exists between nodes x and y.
func (g *ReducedUndirected) HasEdgeBetween(x, y graph.Node) bool {
xid := x.ID()
yid := y.ID()
if xid == yid {
return false
}
if xid > yid {
xid, yid = yid, xid
}
_, ok := g.weights[[2]int{xid, yid}]
return ok
}
// 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.
func (g *ReducedUndirected) Edge(u, v graph.Node) graph.Edge {
uid := u.ID()
vid := v.ID()
if vid < uid {
uid, vid = vid, uid
}
w, ok := g.weights[[2]int{uid, vid}]
if !ok {
return nil
}
return edge{from: g.nodes[u.ID()], to: g.nodes[v.ID()], weight: w}
}
// EdgeBetween returns the edge between nodes x and y.
func (g *ReducedUndirected) EdgeBetween(x, y graph.Node) graph.Edge {
return g.Edge(x, y)
}
// 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 *ReducedUndirected) Weight(x, y graph.Node) (w float64, ok bool) {
xid := x.ID()
yid := y.ID()
if xid == yid {
return g.nodes[xid].weight, true
}
if xid > yid {
xid, yid = yid, xid
}
w, ok = g.weights[[2]int{xid, yid}]
return w, ok
}
// undirectedLocalMover is a step in graph modularity optimization.
type undirectedLocalMover struct {
g *ReducedUndirected
// nodes is the set of working nodes.
nodes []graph.Node
// edgeWeightOf is the weighted degree
// of each node indexed by ID.
edgeWeightOf []float64
// m2 is the total sum of
// edge weights in g.
m2 float64
// weight is the weight function
// provided by g or a function
// that returns the Weight value
// of the non-nil edge between x
// and y.
weight func(x, y graph.Node) float64
// communities is the current
// division of g.
communities [][]graph.Node
// memberships is a mapping between
// node ID and community membership.
memberships []int
// resolution is the Reichardt and
// Bornholdt γ parameter as defined
// in doi:10.1103/PhysRevE.74.016110.
resolution float64
// moved indicates that a call to
// move has been made since the last
// call to shuffle.
moved bool
// changed indicates that a move
// has been made since the creation
// of the local mover.
changed bool
}
// newUndirectedLocalMover returns a new undirectedLocalMover initialized with
// the graph g, a set of communities and a modularity resolution parameter. The
// node IDs of g must be contiguous in [0,n) where n is the number of nodes.
// If g has a zero edge weight sum, nil is returned.
func newUndirectedLocalMover(g *ReducedUndirected, communities [][]graph.Node, resolution float64) *undirectedLocalMover {
nodes := g.Nodes()
l := undirectedLocalMover{
g: g,
nodes: nodes,
edgeWeightOf: make([]float64, len(nodes)),
communities: communities,
memberships: make([]int, len(nodes)),
resolution: resolution,
weight: positiveWeightFuncFor(g),
}
// Calculate the total edge weight of the graph
// and degree weights for each node.
for _, u := range l.nodes {
w := l.weight(u, u)
for _, v := range g.From(u) {
w += l.weight(u, v)
}
l.edgeWeightOf[u.ID()] = w
l.m2 += w
}
if l.m2 == 0 {
return nil
}
// Assign membership mappings.
for i, c := range communities {
for _, u := range c {
l.memberships[u.ID()] = i
}
}
return &l
}
// localMovingHeuristic performs the Louvain local moving heuristic until
// no further moves can be made. It returns a boolean indicating that the
// undirectedLocalMover has not made any improvement to the community
// structure and so the Louvain algorithm is done.
func (l *undirectedLocalMover) localMovingHeuristic(rnd func(int) int) (done bool) {
for {
l.shuffle(rnd)
for _, n := range l.nodes {
dQ, dst, src := l.deltaQ(n)
if dQ <= 0 {
continue
}
l.move(dst, src)
}
if !l.moved {
return !l.changed
}
}
}
// shuffle performs a Fisher-Yates shuffle on the nodes held by the
// undirectedLocalMover using the random source rnd which should return
// an integer in the range [0,n).
func (l *undirectedLocalMover) shuffle(rnd func(n int) int) {
l.moved = false
for i := range l.nodes[:len(l.nodes)-1] {
j := i + rnd(len(l.nodes)-i)
l.nodes[i], l.nodes[j] = l.nodes[j], l.nodes[i]
}
}
// move moves the node at src to the community at dst.
func (l *undirectedLocalMover) move(dst int, src commIdx) {
l.moved = true
l.changed = true
srcComm := l.communities[src.community]
n := srcComm[src.node]
l.memberships[n.ID()] = dst
l.communities[dst] = append(l.communities[dst], n)
srcComm[src.node], srcComm[len(srcComm)-1] = srcComm[len(srcComm)-1], nil
l.communities[src.community] = srcComm[:len(srcComm)-1]
}
// deltaQ returns the highest gain in modularity attainable by moving
// n from its current community to another connected community and
// the index of the chosen destination. The index into the
// undirectedLocalMover's communities field is returned in src if n
// is in communities.
func (l *undirectedLocalMover) deltaQ(n graph.Node) (deltaQ float64, dst int, src commIdx) {
id := n.ID()
a_aa := l.weight(n, n)
k_a := l.edgeWeightOf[id]
m2 := l.m2
gamma := l.resolution
// Find communites connected to n.
var connected intsets.Sparse
// The following for loop is equivalent to:
//
// for _, v := range l.g.From(n) {
// connected.Insert(l.memberships[v.ID()])
// }
//
// This is done to avoid an allocation.
for _, vid := range l.g.edges[id] {
connected.Insert(l.memberships[vid])
}
// Insert the node's own community.
connected.Insert(l.memberships[id])
// Calculate the highest modularity gain
// from moving into another community and
// keep the index of that community.
var dQremove float64
dQadd, dst, src := math.Inf(-1), -1, commIdx{-1, -1}
var i int
for connected.TakeMin(&i) {
c := l.communities[i]
var k_aC, sigma_totC float64 // C is a substitution for ^𝛼 or ^𝛽.
var removal bool
for j, u := range c {
uid := u.ID()
if uid == id {
if src.community != -1 {
panic("community: multiple sources")
}
src = commIdx{i, j}
removal = true
}
k_aC += l.weight(n, u)
// sigma_totC could be kept for each community
// and updated for moves, changing the calculation
// of sigma_totC here from O(n_c) to O(1), but
// in practice the time savings do not appear
// to be compelling and do not make up for the
// increase in code complexity and space required.
sigma_totC += l.edgeWeightOf[uid]
}
// See louvain.tex for a derivation of these equations.
switch {
case removal:
// The community c was the current community,
// so calculate the change due to removal.
dQremove = k_aC /*^𝛼*/ - a_aa - gamma*k_a*(sigma_totC /*^𝛼*/ -k_a)/m2
default:
// Otherwise calculate the change due to an addition
// to c and retain if it is the current best.
dQ := k_aC /*^𝛽*/ - gamma*k_a*sigma_totC /*^𝛽*/ /m2
if dQ > dQadd {
dQadd = dQ
dst = i
}
}
}
return 2 * (dQadd - dQremove) / m2, dst, src
}

View File

@@ -0,0 +1,811 @@
// 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 community
import (
"fmt"
"math"
"math/rand"
"sort"
"golang.org/x/tools/container/intsets"
"github.com/gonum/graph"
"github.com/gonum/graph/internal/ordered"
)
// UndirectedMultiplex is an undirected multiplex graph.
type UndirectedMultiplex interface {
Multiplex
// Layer returns the lth layer of the
// multiplex graph.
Layer(l int) graph.Undirected
}
// qUndirectedMultiplex returns the modularity Q score of the multiplex graph layers
// subdivided into the given communities at the given resolutions and weights. Q is
// returned as the vector of weighted Q scores for each layer of the multiplex graph.
// If communities is nil, the unclustered modularity score is returned.
// If weights is nil layers are equally weighted, otherwise the length of
// weights must equal the number of layers. If resolutions is nil, a resolution
// of 1.0 is used for all layers, otherwise either a single element slice may be used
// to specify a global resolution, or the length of resolutions must equal the number
// of layers. The resolution parameter is γ as defined in Reichardt and Bornholdt
// doi:10.1103/PhysRevE.74.016110.
// qUndirectedMultiplex will panic if the graph has any layer weight-scaled edge with
// negative edge weight.
//
// Q_{layer} = w_{layer} \sum_{ij} [ A_{layer}*_{ij} - (\gamma_{layer} k_i k_j)/2m ] \delta(c_i,c_j)
//
// Note that Q values for multiplex graphs are not scaled by the total layer edge weight.
//
// graph.Undirect may be used as a shim to allow calculation of Q for
// directed graphs.
func qUndirectedMultiplex(g UndirectedMultiplex, communities [][]graph.Node, weights, resolutions []float64) []float64 {
q := make([]float64, g.Depth())
nodes := g.Nodes()
layerWeight := 1.0
layerResolution := 1.0
if len(resolutions) == 1 {
layerResolution = resolutions[0]
}
for l := 0; l < g.Depth(); l++ {
layer := g.Layer(l)
if weights != nil {
layerWeight = weights[l]
}
if layerWeight == 0 {
continue
}
if len(resolutions) > 1 {
layerResolution = resolutions[l]
}
var weight func(x, y graph.Node) float64
if layerWeight < 0 {
weight = negativeWeightFuncFor(layer)
} else {
weight = positiveWeightFuncFor(layer)
}
// Calculate the total edge weight of the layer
// and the table of penetrating edge weight sums.
var m2 float64
k := make(map[int]float64, len(nodes))
for _, u := range nodes {
w := weight(u, u)
for _, v := range layer.From(u) {
w += weight(u, v)
}
m2 += w
k[u.ID()] = w
}
if communities == nil {
var qLayer float64
for _, u := range nodes {
kU := k[u.ID()]
qLayer += weight(u, u) - layerResolution*kU*kU/m2
}
q[l] = layerWeight * qLayer
continue
}
// Iterate over the communities, calculating
// the non-self edge weights for the upper
// triangle and adjust the diagonal.
var qLayer float64
for _, c := range communities {
for i, u := range c {
kU := k[u.ID()]
qLayer += weight(u, u) - layerResolution*kU*kU/m2
for _, v := range c[i+1:] {
qLayer += 2 * (weight(u, v) - layerResolution*kU*k[v.ID()]/m2)
}
}
}
q[l] = layerWeight * qLayer
}
return q
}
// UndirectedLayers implements UndirectedMultiplex.
type UndirectedLayers []graph.Undirected
// NewUndirectedLayers returns an UndirectedLayers using the provided layers
// ensuring there is a match between IDs for each layer.
func NewUndirectedLayers(layers ...graph.Undirected) (UndirectedLayers, error) {
if len(layers) == 0 {
return nil, nil
}
var base, next intsets.Sparse
for _, n := range layers[0].Nodes() {
base.Insert(n.ID())
}
for i, l := range layers[1:] {
next.Clear()
for _, n := range l.Nodes() {
next.Insert(n.ID())
}
if !next.Equals(&base) {
return nil, fmt.Errorf("community: layer ID mismatch between layers: %d", i+1)
}
}
return layers, nil
}
// Nodes returns the nodes of the receiver.
func (g UndirectedLayers) Nodes() []graph.Node {
if len(g) == 0 {
return nil
}
return g[0].Nodes()
}
// Depth returns the depth of the multiplex graph.
func (g UndirectedLayers) Depth() int { return len(g) }
// Layer returns the lth layer of the multiplex graph.
func (g UndirectedLayers) Layer(l int) graph.Undirected { return g[l] }
// louvainUndirectedMultiplex returns the hierarchical modularization of g at the given resolution
// using the Louvain algorithm. If all is true and g has negatively weighted layers, all
// communities will be searched during the modularization. If src is nil, rand.Intn is
// used as the random generator. louvainUndirectedMultiplex will panic if g has any edge with
// edge weight that does not sign-match the layer weight.
//
// graph.Undirect may be used as a shim to allow modularization of directed graphs.
func louvainUndirectedMultiplex(g UndirectedMultiplex, weights, resolutions []float64, all bool, src *rand.Rand) *ReducedUndirectedMultiplex {
if weights != nil && len(weights) != g.Depth() {
panic("community: weights vector length mismatch")
}
if resolutions != nil && len(resolutions) != 1 && len(resolutions) != g.Depth() {
panic("community: resolutions vector length mismatch")
}
// See louvain.tex for a detailed description
// of the algorithm used here.
c := reduceUndirectedMultiplex(g, nil, weights)
rnd := rand.Intn
if src != nil {
rnd = src.Intn
}
for {
l := newUndirectedMultiplexLocalMover(c, c.communities, weights, resolutions, all)
if l == nil {
return c
}
if done := l.localMovingHeuristic(rnd); done {
return c
}
c = reduceUndirectedMultiplex(c, l.communities, weights)
}
}
// ReducedUndirectedMultiplex is an undirected graph of communities derived from a
// parent graph by reduction.
type ReducedUndirectedMultiplex struct {
// nodes is the set of nodes held
// by the graph. In a ReducedUndirectedMultiplex
// the node ID is the index into
// nodes.
nodes []multiplexCommunity
layers []undirectedEdges
// communities is the community
// structure of the graph.
communities [][]graph.Node
parent *ReducedUndirectedMultiplex
}
var (
_ UndirectedMultiplex = (*ReducedUndirectedMultiplex)(nil)
_ graph.Undirected = (*undirectedLayerHandle)(nil)
_ graph.Weighter = (*undirectedLayerHandle)(nil)
)
// Nodes returns all the nodes in the graph.
func (g *ReducedUndirectedMultiplex) Nodes() []graph.Node {
nodes := make([]graph.Node, len(g.nodes))
for i := range g.nodes {
nodes[i] = node(i)
}
return nodes
}
// Depth returns the number of layers in the multiplex graph.
func (g *ReducedUndirectedMultiplex) Depth() int { return len(g.layers) }
// Layer returns the lth layer of the multiplex graph.
func (g *ReducedUndirectedMultiplex) Layer(l int) graph.Undirected {
return undirectedLayerHandle{multiplex: g, layer: l}
}
// Communities returns the community memberships of the nodes in the
// graph used to generate the reduced graph.
func (g *ReducedUndirectedMultiplex) Communities() [][]graph.Node {
communities := make([][]graph.Node, len(g.communities))
if g.parent == nil {
for i, members := range g.communities {
comm := make([]graph.Node, len(members))
for j, n := range members {
nodes := g.nodes[n.ID()].nodes
if len(nodes) != 1 {
panic("community: unexpected number of nodes in base graph community")
}
comm[j] = nodes[0]
}
communities[i] = comm
}
return communities
}
sub := g.parent.Communities()
for i, members := range g.communities {
var comm []graph.Node
for _, n := range members {
comm = append(comm, sub[n.ID()]...)
}
communities[i] = comm
}
return communities
}
// Structure returns the community structure of the current level of
// the module clustering. The first index of the returned value
// corresponds to the index of the nodes in the next higher level if
// it exists. The returned value should not be mutated.
func (g *ReducedUndirectedMultiplex) Structure() [][]graph.Node {
return g.communities
}
// Expanded returns the next lower level of the module clustering or nil
// if at the lowest level.
func (g *ReducedUndirectedMultiplex) Expanded() ReducedMultiplex {
return g.parent
}
// reduceUndirectedMultiplex returns a reduced graph constructed from g divided
// into the given communities. The communities value is mutated
// by the call to reduceUndirectedMultiplex. If communities is nil and g is a
// ReducedUndirectedMultiplex, it is returned unaltered.
func reduceUndirectedMultiplex(g UndirectedMultiplex, communities [][]graph.Node, weights []float64) *ReducedUndirectedMultiplex {
if communities == nil {
if r, ok := g.(*ReducedUndirectedMultiplex); ok {
return r
}
nodes := g.Nodes()
// TODO(kortschak) This sort is necessary really only
// for testing. In practice we would not be using the
// community provided by the user for a Q calculation.
// Probably we should use a function to map the
// communities in the test sets to the remapped order.
sort.Sort(ordered.ByID(nodes))
communities = make([][]graph.Node, len(nodes))
for i := range nodes {
communities[i] = []graph.Node{node(i)}
}
r := ReducedUndirectedMultiplex{
nodes: make([]multiplexCommunity, len(nodes)),
layers: make([]undirectedEdges, g.Depth()),
communities: communities,
}
communityOf := make(map[int]int, len(nodes))
for i, n := range nodes {
r.nodes[i] = multiplexCommunity{id: i, nodes: []graph.Node{n}, weights: make([]float64, depth(weights))}
communityOf[n.ID()] = i
}
for i := range r.layers {
r.layers[i] = undirectedEdges{
edges: make([][]int, len(nodes)),
weights: make(map[[2]int]float64),
}
}
w := 1.0
for l := 0; l < g.Depth(); l++ {
layer := g.Layer(l)
if weights != nil {
w = weights[l]
}
if w == 0 {
continue
}
var sign float64
var weight func(x, y graph.Node) float64
if w < 0 {
sign, weight = -1, negativeWeightFuncFor(layer)
} else {
sign, weight = 1, positiveWeightFuncFor(layer)
}
for _, u := range nodes {
var out []int
uid := communityOf[u.ID()]
for _, v := range layer.From(u) {
vid := communityOf[v.ID()]
if vid != uid {
out = append(out, vid)
}
if uid < vid {
// Only store the weight once.
r.layers[l].weights[[2]int{uid, vid}] = sign * weight(u, v)
}
}
r.layers[l].edges[uid] = out
}
}
return &r
}
// Remove zero length communities destructively.
var commNodes int
for i := 0; i < len(communities); {
comm := communities[i]
if len(comm) == 0 {
communities[i] = communities[len(communities)-1]
communities[len(communities)-1] = nil
communities = communities[:len(communities)-1]
} else {
commNodes += len(comm)
i++
}
}
r := ReducedUndirectedMultiplex{
nodes: make([]multiplexCommunity, len(communities)),
layers: make([]undirectedEdges, g.Depth()),
}
communityOf := make(map[int]int, commNodes)
for i, comm := range communities {
r.nodes[i] = multiplexCommunity{id: i, nodes: comm, weights: make([]float64, depth(weights))}
for _, n := range comm {
communityOf[n.ID()] = i
}
}
for i := range r.layers {
r.layers[i] = undirectedEdges{
edges: make([][]int, len(communities)),
weights: make(map[[2]int]float64),
}
}
r.communities = make([][]graph.Node, len(communities))
for i := range r.communities {
r.communities[i] = []graph.Node{node(i)}
}
if g, ok := g.(*ReducedUndirectedMultiplex); ok {
// Make sure we retain the truncated
// community structure.
g.communities = communities
r.parent = g
}
w := 1.0
for l := 0; l < g.Depth(); l++ {
layer := g.Layer(l)
if weights != nil {
w = weights[l]
}
if w == 0 {
continue
}
var sign float64
var weight func(x, y graph.Node) float64
if w < 0 {
sign, weight = -1, negativeWeightFuncFor(layer)
} else {
sign, weight = 1, positiveWeightFuncFor(layer)
}
for uid, comm := range communities {
var out []int
for i, u := range comm {
r.nodes[uid].weights[l] += sign * weight(u, u)
for _, v := range comm[i+1:] {
r.nodes[uid].weights[l] += 2 * sign * weight(u, v)
}
for _, v := range layer.From(u) {
vid := communityOf[v.ID()]
found := false
for _, e := range out {
if e == vid {
found = true
break
}
}
if !found && vid != uid {
out = append(out, vid)
}
if uid < vid {
// Only store the weight once.
r.layers[l].weights[[2]int{uid, vid}] += sign * weight(u, v)
}
}
}
r.layers[l].edges[uid] = out
}
}
return &r
}
// undirectedLayerHandle is a handle to a multiplex graph layer.
type undirectedLayerHandle struct {
// multiplex is the complete
// multiplex graph.
multiplex *ReducedUndirectedMultiplex
// layer is an index into the
// multiplex for the current
// layer.
layer int
}
// Has returns whether the node exists within the graph.
func (g undirectedLayerHandle) Has(n graph.Node) bool {
id := n.ID()
return id >= 0 || id < len(g.multiplex.nodes)
}
// Nodes returns all the nodes in the graph.
func (g undirectedLayerHandle) Nodes() []graph.Node {
nodes := make([]graph.Node, len(g.multiplex.nodes))
for i := range g.multiplex.nodes {
nodes[i] = node(i)
}
return nodes
}
// From returns all nodes in g that can be reached directly from u.
func (g undirectedLayerHandle) From(u graph.Node) []graph.Node {
out := g.multiplex.layers[g.layer].edges[u.ID()]
nodes := make([]graph.Node, len(out))
for i, vid := range out {
nodes[i] = g.multiplex.nodes[vid]
}
return nodes
}
// HasEdgeBetween returns whether an edge exists between nodes x and y.
func (g undirectedLayerHandle) HasEdgeBetween(x, y graph.Node) bool {
xid := x.ID()
yid := y.ID()
if xid == yid {
return false
}
if xid > yid {
xid, yid = yid, xid
}
_, ok := g.multiplex.layers[g.layer].weights[[2]int{xid, yid}]
return ok
}
// 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.
func (g undirectedLayerHandle) Edge(u, v graph.Node) graph.Edge {
uid := u.ID()
vid := v.ID()
if vid < uid {
uid, vid = vid, uid
}
w, ok := g.multiplex.layers[g.layer].weights[[2]int{uid, vid}]
if !ok {
return nil
}
return multiplexEdge{from: g.multiplex.nodes[u.ID()], to: g.multiplex.nodes[v.ID()], weight: w}
}
// EdgeBetween returns the edge between nodes x and y.
func (g undirectedLayerHandle) EdgeBetween(x, y graph.Node) graph.Edge {
return g.Edge(x, y)
}
// 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 undirectedLayerHandle) Weight(x, y graph.Node) (w float64, ok bool) {
xid := x.ID()
yid := y.ID()
if xid == yid {
return g.multiplex.nodes[xid].weights[g.layer], true
}
if xid > yid {
xid, yid = yid, xid
}
w, ok = g.multiplex.layers[g.layer].weights[[2]int{xid, yid}]
return w, ok
}
// undirectedMultiplexLocalMover is a step in graph modularity optimization.
type undirectedMultiplexLocalMover struct {
g *ReducedUndirectedMultiplex
// nodes is the set of working nodes.
nodes []graph.Node
// edgeWeightOf is the weighted degree
// of each node indexed by ID.
edgeWeightOf [][]float64
// m2 is the total sum of
// edge weights in g.
m2 []float64
// weight is the weight function
// provided by g or a function
// that returns the Weight value
// of the non-nil edge between x
// and y.
weight []func(x, y graph.Node) float64
// communities is the current
// division of g.
communities [][]graph.Node
// memberships is a mapping between
// node ID and community membership.
memberships []int
// resolution is the Reichardt and
// Bornholdt γ parameter as defined
// in doi:10.1103/PhysRevE.74.016110.
resolutions []float64
// weights is the layer weights for
// the modularisation.
weights []float64
// searchAll specifies whether the local
// mover should consider non-connected
// communities during the local moving
// heuristic.
searchAll bool
// moved indicates that a call to
// move has been made since the last
// call to shuffle.
moved bool
// changed indicates that a move
// has been made since the creation
// of the local mover.
changed bool
}
// newUndirectedMultiplexLocalMover returns a new undirectedMultiplexLocalMover initialized with
// the graph g, a set of communities and a modularity resolution parameter. The
// node IDs of g must be contiguous in [0,n) where n is the number of nodes.
// If g has a zero edge weight sum, nil is returned.
func newUndirectedMultiplexLocalMover(g *ReducedUndirectedMultiplex, communities [][]graph.Node, weights, resolutions []float64, all bool) *undirectedMultiplexLocalMover {
nodes := g.Nodes()
l := undirectedMultiplexLocalMover{
g: g,
nodes: nodes,
edgeWeightOf: make([][]float64, g.Depth()),
m2: make([]float64, g.Depth()),
communities: communities,
memberships: make([]int, len(nodes)),
resolutions: resolutions,
weights: weights,
weight: make([]func(x, y graph.Node) float64, g.Depth()),
}
// Calculate the total edge weight of the graph
// and degree weights for each node.
var zero int
for i := 0; i < g.Depth(); i++ {
l.edgeWeightOf[i] = make([]float64, len(nodes))
var weight func(x, y graph.Node) float64
if weights != nil {
if weights[i] == 0 {
zero++
continue
}
if weights[i] < 0 {
weight = negativeWeightFuncFor(g.Layer(i))
l.searchAll = all
} else {
weight = positiveWeightFuncFor(g.Layer(i))
}
} else {
weight = positiveWeightFuncFor(g.Layer(i))
}
l.weight[i] = weight
layer := g.Layer(i)
for _, u := range l.nodes {
w := weight(u, u)
for _, v := range layer.From(u) {
w += weight(u, v)
}
l.edgeWeightOf[i][u.ID()] = w
l.m2[i] += w
}
if l.m2[i] == 0 {
zero++
}
}
if zero == g.Depth() {
return nil
}
// Assign membership mappings.
for i, c := range communities {
for _, u := range c {
l.memberships[u.ID()] = i
}
}
return &l
}
// localMovingHeuristic performs the Louvain local moving heuristic until
// no further moves can be made. It returns a boolean indicating that the
// undirectedMultiplexLocalMover has not made any improvement to the community
// structure and so the Louvain algorithm is done.
func (l *undirectedMultiplexLocalMover) localMovingHeuristic(rnd func(int) int) (done bool) {
for {
l.shuffle(rnd)
for _, n := range l.nodes {
dQ, dst, src := l.deltaQ(n)
if dQ <= 0 {
continue
}
l.move(dst, src)
}
if !l.moved {
return !l.changed
}
}
}
// shuffle performs a Fisher-Yates shuffle on the nodes held by the
// undirectedMultiplexLocalMover using the random source rnd which should return
// an integer in the range [0,n).
func (l *undirectedMultiplexLocalMover) shuffle(rnd func(n int) int) {
l.moved = false
for i := range l.nodes[:len(l.nodes)-1] {
j := i + rnd(len(l.nodes)-i)
l.nodes[i], l.nodes[j] = l.nodes[j], l.nodes[i]
}
}
// move moves the node at src to the community at dst.
func (l *undirectedMultiplexLocalMover) move(dst int, src commIdx) {
l.moved = true
l.changed = true
srcComm := l.communities[src.community]
n := srcComm[src.node]
l.memberships[n.ID()] = dst
l.communities[dst] = append(l.communities[dst], n)
srcComm[src.node], srcComm[len(srcComm)-1] = srcComm[len(srcComm)-1], nil
l.communities[src.community] = srcComm[:len(srcComm)-1]
}
// deltaQ returns the highest gain in modularity attainable by moving
// n from its current community to another connected community and
// the index of the chosen destination. The index into the
// undirectedMultiplexLocalMover's communities field is returned in src if n
// is in communities.
func (l *undirectedMultiplexLocalMover) deltaQ(n graph.Node) (deltaQ float64, dst int, src commIdx) {
id := n.ID()
var iterator minTaker
if l.searchAll {
iterator = &dense{n: len(l.communities)}
} else {
// Find communities connected to n.
var connected intsets.Sparse
// The following for loop is equivalent to:
//
// for i := 0; i < l.g.Depth(); i++ {
// for _, v := range l.g.Layer(i).From(n) {
// connected.Insert(l.memberships[v.ID()])
// }
// }
//
// This is done to avoid an allocation for
// each layer.
for _, layer := range l.g.layers {
for _, vid := range layer.edges[id] {
connected.Insert(l.memberships[vid])
}
}
// Insert the node's own community.
connected.Insert(l.memberships[id])
iterator = &connected
}
// Calculate the highest modularity gain
// from moving into another community and
// keep the index of that community.
var dQremove float64
dQadd, dst, src := math.Inf(-1), -1, commIdx{-1, -1}
var i int
for iterator.TakeMin(&i) {
c := l.communities[i]
var removal bool
var _dQadd float64
for layer := 0; layer < l.g.Depth(); layer++ {
m2 := l.m2[layer]
if m2 == 0 {
// Do not consider layers with zero sum edge weight.
continue
}
w := 1.0
if l.weights != nil {
w = l.weights[layer]
}
if w == 0 {
// Do not consider layers with zero weighting.
continue
}
var k_aC, sigma_totC float64 // C is a substitution for ^𝛼 or ^𝛽.
removal = false
for j, u := range c {
uid := u.ID()
if uid == id {
// Only mark and check src community on the first layer.
if layer == 0 {
if src.community != -1 {
panic("community: multiple sources")
}
src = commIdx{i, j}
}
removal = true
}
k_aC += l.weight[layer](n, u)
// sigma_totC could be kept for each community
// and updated for moves, changing the calculation
// of sigma_totC here from O(n_c) to O(1), but
// in practice the time savings do not appear
// to be compelling and do not make up for the
// increase in code complexity and space required.
sigma_totC += l.edgeWeightOf[layer][uid]
}
a_aa := l.weight[layer](n, n)
k_a := l.edgeWeightOf[layer][id]
gamma := 1.0
if l.resolutions != nil {
if len(l.resolutions) == 1 {
gamma = l.resolutions[0]
} else {
gamma = l.resolutions[layer]
}
}
// See louvain.tex for a derivation of these equations.
// The weighting term, w, is described in V Traag,
// "Algorithms and dynamical models for communities and
// reputation in social networks", chapter 5.
// http://www.traag.net/wp/wp-content/papercite-data/pdf/traag_algorithms_2013.pdf
switch {
case removal:
// The community c was the current community,
// so calculate the change due to removal.
dQremove += w * (k_aC /*^𝛼*/ - a_aa - gamma*k_a*(sigma_totC /*^𝛼*/ -k_a)/m2)
default:
// Otherwise calculate the change due to an addition
// to c.
_dQadd += w * (k_aC /*^𝛽*/ - gamma*k_a*sigma_totC /*^𝛽*/ /m2)
}
}
if !removal && _dQadd > dQadd {
dQadd = _dQadd
dst = i
}
}
return 2 * (dQadd - dQremove), dst, src
}

View File

@@ -0,0 +1,669 @@
// 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 community
import (
"math"
"math/rand"
"reflect"
"sort"
"testing"
"github.com/gonum/floats"
"github.com/gonum/graph"
"github.com/gonum/graph/internal/ordered"
"github.com/gonum/graph/simple"
)
var communityUndirectedMultiplexQTests = []struct {
name string
layers []layer
structures []structure
wantLevels []level
}{
{
name: "unconnected",
layers: []layer{{g: unconnected, weight: 1}},
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0),
1: linksTo(1),
2: linksTo(2),
3: linksTo(3),
4: linksTo(4),
5: linksTo(5),
},
want: math.NaN(),
},
},
wantLevels: []level{
{
q: math.Inf(-1), // Here math.Inf(-1) is used as a place holder for NaN to allow use of reflect.DeepEqual.
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
},
},
},
},
{
name: "small_dumbell",
layers: []layer{
{g: smallDumbell, edgeWeight: 1, weight: 1},
{g: dumbellRepulsion, edgeWeight: -1, weight: -1},
},
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1, 2),
1: linksTo(3, 4, 5),
},
want: 7.0, tol: 1e-10,
},
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1, 2, 3, 4, 5),
},
want: 0, tol: 1e-14,
},
},
wantLevels: []level{
{
q: 7.0,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1), simple.Node(2)},
{simple.Node(3), simple.Node(4), simple.Node(5)},
},
},
{
q: -1.4285714285714284,
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
},
},
},
},
{
name: "small_dumbell_twice",
layers: []layer{
{g: smallDumbell, weight: 0.5},
{g: smallDumbell, weight: 0.5},
},
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1, 2),
1: linksTo(3, 4, 5),
},
want: 5, tol: 1e-10,
},
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1, 2, 3, 4, 5),
},
want: 0, tol: 1e-14,
},
},
wantLevels: []level{
{
q: 0.35714285714285715 * 14,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1), simple.Node(2)},
{simple.Node(3), simple.Node(4), simple.Node(5)},
},
},
{
q: -0.17346938775510204 * 14,
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
},
},
},
},
{
name: "repulsion",
layers: []layer{{g: repulsion, edgeWeight: -1, weight: -1}},
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1, 2),
1: linksTo(3, 4, 5),
},
want: 9.0, tol: 1e-10,
},
{
resolution: 1,
memberships: []set{
0: linksTo(0),
1: linksTo(1),
2: linksTo(2),
3: linksTo(3),
4: linksTo(4),
5: linksTo(5),
},
want: 3, tol: 1e-14,
},
},
wantLevels: []level{
{
q: 9.0,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1), simple.Node(2)},
{simple.Node(3), simple.Node(4), simple.Node(5)},
},
},
{
q: 3.0,
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
},
},
},
},
{
name: "middle_east",
layers: []layer{
{g: middleEast.friends, edgeWeight: 1, weight: 1},
{g: middleEast.enemies, edgeWeight: -1, weight: -1},
},
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0, 6),
1: linksTo(1, 7, 9, 12),
2: linksTo(2, 8, 11),
3: linksTo(3, 4, 5, 10),
},
want: 33.8180574555, tol: 1e-9,
},
{
resolution: 1,
memberships: []set{
0: linksTo(0, 2, 3, 4, 5, 10),
1: linksTo(1, 7, 9, 12),
2: linksTo(6),
3: linksTo(8, 11),
},
want: 30.92749658, tol: 1e-7,
},
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12),
},
want: 0, tol: 1e-14,
},
},
wantLevels: []level{
{
q: 33.818057455540355,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(6)},
{simple.Node(1), simple.Node(7), simple.Node(9), simple.Node(12)},
{simple.Node(2), simple.Node(8), simple.Node(11)},
{simple.Node(3), simple.Node(4), simple.Node(5), simple.Node(10)},
},
},
{
q: 3.8071135430916545,
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
{simple.Node(6)},
{simple.Node(7)},
{simple.Node(8)},
{simple.Node(9)},
{simple.Node(10)},
{simple.Node(11)},
{simple.Node(12)},
},
},
},
},
}
func TestCommunityQUndirectedMultiplex(t *testing.T) {
for _, test := range communityUndirectedMultiplexQTests {
g, weights, err := undirectedMultiplexFrom(test.layers)
if err != nil {
t.Errorf("unexpected error creating multiplex: %v", err)
continue
}
for _, structure := range test.structures {
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communities[i] = append(communities[i], simple.Node(n))
}
}
q := QMultiplex(g, communities, weights, []float64{structure.resolution})
got := floats.Sum(q)
if !floats.EqualWithinAbsOrRel(got, structure.want, structure.tol, structure.tol) && !math.IsNaN(structure.want) {
for _, c := range communities {
sort.Sort(ordered.ByID(c))
}
t.Errorf("unexpected Q value for %q %v: got: %v %.3v want: %v",
test.name, communities, got, q, structure.want)
}
}
}
}
func TestCommunityDeltaQUndirectedMultiplex(t *testing.T) {
tests:
for _, test := range communityUndirectedMultiplexQTests {
g, weights, err := undirectedMultiplexFrom(test.layers)
if err != nil {
t.Errorf("unexpected error creating multiplex: %v", err)
continue
}
rnd := rand.New(rand.NewSource(1)).Intn
for _, structure := range test.structures {
communityOf := make(map[int]int)
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communityOf[n] = i
communities[i] = append(communities[i], simple.Node(n))
}
sort.Sort(ordered.ByID(communities[i]))
}
resolution := []float64{structure.resolution}
before := QMultiplex(g, communities, weights, resolution)
// We test exhaustively.
const all = true
l := newUndirectedMultiplexLocalMover(
reduceUndirectedMultiplex(g, nil, weights),
communities, weights, resolution, all)
if l == nil {
if !math.IsNaN(floats.Sum(before)) {
t.Errorf("unexpected nil localMover with non-NaN Q graph: Q=%.4v", before)
}
continue tests
}
// This is done to avoid run-to-run
// variation due to map iteration order.
sort.Sort(ordered.ByID(l.nodes))
l.shuffle(rnd)
for _, target := range l.nodes {
got, gotDst, gotSrc := l.deltaQ(target)
want, wantDst := math.Inf(-1), -1
migrated := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
if n == target.ID() {
continue
}
migrated[i] = append(migrated[i], simple.Node(n))
}
sort.Sort(ordered.ByID(migrated[i]))
}
for i, c := range structure.memberships {
if i == communityOf[target.ID()] {
continue
}
if !(all && hasNegative(weights)) {
connected := false
search:
for l := 0; l < g.Depth(); l++ {
if weights[l] < 0 {
connected = true
break search
}
layer := g.Layer(l)
for n := range c {
if layer.HasEdgeBetween(simple.Node(n), target) {
connected = true
break search
}
}
}
if !connected {
continue
}
}
migrated[i] = append(migrated[i], target)
after := QMultiplex(g, migrated, weights, resolution)
migrated[i] = migrated[i][:len(migrated[i])-1]
if delta := floats.Sum(after) - floats.Sum(before); delta > want {
want = delta
wantDst = i
}
}
if !floats.EqualWithinAbsOrRel(got, want, structure.tol, structure.tol) || gotDst != wantDst {
t.Errorf("unexpected result moving n=%d in c=%d of %s/%.4v: got: %.4v,%d want: %.4v,%d"+
"\n\t%v\n\t%v",
target.ID(), communityOf[target.ID()], test.name, structure.resolution, got, gotDst, want, wantDst,
communities, migrated)
}
if gotSrc.community != communityOf[target.ID()] {
t.Errorf("unexpected source community index: got: %d want: %d", gotSrc, communityOf[target.ID()])
} else if communities[gotSrc.community][gotSrc.node].ID() != target.ID() {
wantNodeIdx := -1
for i, n := range communities[gotSrc.community] {
if n.ID() == target.ID() {
wantNodeIdx = i
break
}
}
t.Errorf("unexpected source node index: got: %d want: %d", gotSrc.node, wantNodeIdx)
}
}
}
}
}
func TestReduceQConsistencyUndirectedMultiplex(t *testing.T) {
tests:
for _, test := range communityUndirectedMultiplexQTests {
g, weights, err := undirectedMultiplexFrom(test.layers)
if err != nil {
t.Errorf("unexpected error creating multiplex: %v", err)
continue
}
for _, structure := range test.structures {
if math.IsNaN(structure.want) {
continue tests
}
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communities[i] = append(communities[i], simple.Node(n))
}
sort.Sort(ordered.ByID(communities[i]))
}
gQ := QMultiplex(g, communities, weights, []float64{structure.resolution})
gQnull := QMultiplex(g, nil, weights, nil)
cg0 := reduceUndirectedMultiplex(g, nil, weights)
cg0Qnull := QMultiplex(cg0, cg0.Structure(), weights, nil)
if !floats.EqualWithinAbsOrRel(floats.Sum(gQnull), floats.Sum(cg0Qnull), structure.tol, structure.tol) {
t.Errorf("disagreement between null Q from method: %v and function: %v", cg0Qnull, gQnull)
}
cg0Q := QMultiplex(cg0, communities, weights, []float64{structure.resolution})
if !floats.EqualWithinAbsOrRel(floats.Sum(gQ), floats.Sum(cg0Q), structure.tol, structure.tol) {
t.Errorf("unexpected Q result after initial reduction: got: %v want :%v", cg0Q, gQ)
}
cg1 := reduceUndirectedMultiplex(cg0, communities, weights)
cg1Q := QMultiplex(cg1, cg1.Structure(), weights, []float64{structure.resolution})
if !floats.EqualWithinAbsOrRel(floats.Sum(gQ), floats.Sum(cg1Q), structure.tol, structure.tol) {
t.Errorf("unexpected Q result after second reduction: got: %v want :%v", cg1Q, gQ)
}
}
}
}
var localUndirectedMultiplexMoveTests = []struct {
name string
layers []layer
structures []moveStructures
}{
{
name: "blondel",
layers: []layer{{g: blondel, weight: 1}, {g: blondel, weight: 0.5}},
structures: []moveStructures{
{
memberships: []set{
0: linksTo(0, 1, 2, 4, 5),
1: linksTo(3, 6, 7),
2: linksTo(8, 9, 10, 12, 14, 15),
3: linksTo(11, 13),
},
targetNodes: []graph.Node{simple.Node(0)},
resolution: 1,
tol: 1e-14,
},
{
memberships: []set{
0: linksTo(0, 1, 2, 4, 5),
1: linksTo(3, 6, 7),
2: linksTo(8, 9, 10, 12, 14, 15),
3: linksTo(11, 13),
},
targetNodes: []graph.Node{simple.Node(3)},
resolution: 1,
tol: 1e-14,
},
{
memberships: []set{
0: linksTo(0, 1, 2, 4, 5),
1: linksTo(3, 6, 7),
2: linksTo(8, 9, 10, 12, 14, 15),
3: linksTo(11, 13),
},
// Case to demonstrate when A_aa != k_a^𝛼.
targetNodes: []graph.Node{simple.Node(3), simple.Node(2)},
resolution: 1,
tol: 1e-14,
},
},
},
}
func TestMoveLocalUndirectedMultiplex(t *testing.T) {
for _, test := range localUndirectedMultiplexMoveTests {
g, weights, err := undirectedMultiplexFrom(test.layers)
if err != nil {
t.Errorf("unexpected error creating multiplex: %v", err)
continue
}
for _, structure := range test.structures {
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communities[i] = append(communities[i], simple.Node(n))
}
sort.Sort(ordered.ByID(communities[i]))
}
r := reduceUndirectedMultiplex(reduceUndirectedMultiplex(g, nil, weights), communities, weights)
l := newUndirectedMultiplexLocalMover(r, r.communities, weights, []float64{structure.resolution}, true)
for _, n := range structure.targetNodes {
dQ, dst, src := l.deltaQ(n)
if dQ > 0 {
before := floats.Sum(QMultiplex(r, l.communities, weights, []float64{structure.resolution}))
l.move(dst, src)
after := floats.Sum(QMultiplex(r, l.communities, weights, []float64{structure.resolution}))
want := after - before
if !floats.EqualWithinAbsOrRel(dQ, want, structure.tol, structure.tol) {
t.Errorf("unexpected deltaQ: got: %v want: %v", dQ, want)
}
}
}
}
}
}
func TestLouvainMultiplex(t *testing.T) {
const louvainIterations = 20
for _, test := range communityUndirectedMultiplexQTests {
g, weights, err := undirectedMultiplexFrom(test.layers)
if err != nil {
t.Errorf("unexpected error creating multiplex: %v", err)
continue
}
if test.structures[0].resolution != 1 {
panic("bad test: expect resolution=1")
}
want := make([][]graph.Node, len(test.structures[0].memberships))
for i, c := range test.structures[0].memberships {
for n := range c {
want[i] = append(want[i], simple.Node(n))
}
sort.Sort(ordered.ByID(want[i]))
}
sort.Sort(ordered.BySliceIDs(want))
var (
got *ReducedUndirectedMultiplex
bestQ = math.Inf(-1)
)
// Modularize is randomised so we do this to
// ensure the level tests are consistent.
src := rand.New(rand.NewSource(1))
for i := 0; i < louvainIterations; i++ {
r := ModularizeMultiplex(g, weights, nil, true, src).(*ReducedUndirectedMultiplex)
if q := floats.Sum(QMultiplex(r, nil, weights, nil)); q > bestQ || math.IsNaN(q) {
bestQ = q
got = r
if math.IsNaN(q) {
// Don't try again for non-connected case.
break
}
}
var qs []float64
for p := r; p != nil; p = p.Expanded().(*ReducedUndirectedMultiplex) {
qs = append(qs, floats.Sum(QMultiplex(p, nil, weights, nil)))
}
// Recovery of Q values is reversed.
if reverse(qs); !sort.Float64sAreSorted(qs) {
t.Errorf("Q values not monotonically increasing: %.5v", qs)
}
}
gotCommunities := got.Communities()
for _, c := range gotCommunities {
sort.Sort(ordered.ByID(c))
}
sort.Sort(ordered.BySliceIDs(gotCommunities))
if !reflect.DeepEqual(gotCommunities, want) {
t.Errorf("unexpected community membership for %s Q=%.4v:\n\tgot: %v\n\twant:%v",
test.name, bestQ, gotCommunities, want)
continue
}
var levels []level
for p := got; p != nil; p = p.Expanded().(*ReducedUndirectedMultiplex) {
var communities [][]graph.Node
if p.parent != nil {
communities = p.parent.Communities()
for _, c := range communities {
sort.Sort(ordered.ByID(c))
}
sort.Sort(ordered.BySliceIDs(communities))
} else {
communities = reduceUndirectedMultiplex(g, nil, weights).Communities()
}
q := floats.Sum(QMultiplex(p, nil, weights, nil))
if math.IsNaN(q) {
// Use an equalable flag value in place of NaN.
q = math.Inf(-1)
}
levels = append(levels, level{q: q, communities: communities})
}
if !reflect.DeepEqual(levels, test.wantLevels) {
t.Errorf("unexpected level structure:\n\tgot: %v\n\twant:%v", levels, test.wantLevels)
}
}
}
func TestNonContiguousUndirectedMultiplex(t *testing.T) {
g := simple.NewUndirectedGraph(0, 0)
for _, e := range []simple.Edge{
{F: simple.Node(0), T: simple.Node(1), W: 1},
{F: simple.Node(4), T: simple.Node(5), W: 1},
} {
g.SetEdge(e)
}
func() {
defer func() {
r := recover()
if r != nil {
t.Error("unexpected panic with non-contiguous ID range")
}
}()
ModularizeMultiplex(UndirectedLayers{g}, nil, nil, true, nil)
}()
}
func BenchmarkLouvainMultiplex(b *testing.B) {
src := rand.New(rand.NewSource(1))
for i := 0; i < b.N; i++ {
ModularizeMultiplex(UndirectedLayers{dupGraph}, nil, nil, true, src)
}
}
func undirectedMultiplexFrom(raw []layer) (UndirectedLayers, []float64, error) {
var layers []graph.Undirected
var weights []float64
for _, l := range raw {
g := simple.NewUndirectedGraph(0, 0)
for u, e := range l.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
w := 1.0
if l.edgeWeight != 0 {
w = l.edgeWeight
}
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: w})
}
}
layers = append(layers, g)
weights = append(weights, l.weight)
}
g, err := NewUndirectedLayers(layers...)
if err != nil {
return nil, nil, err
}
return g, weights, nil
}

View File

@@ -0,0 +1,648 @@
// 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 community
import (
"math"
"math/rand"
"reflect"
"sort"
"testing"
"github.com/gonum/floats"
"github.com/gonum/graph"
"github.com/gonum/graph/internal/ordered"
"github.com/gonum/graph/simple"
)
var communityUndirectedQTests = []struct {
name string
g []set
structures []structure
wantLevels []level
}{
// The java reference implementation is available from http://www.ludowaltman.nl/slm/.
{
name: "unconnected",
g: unconnected,
structures: []structure{
{
resolution: 1,
memberships: []set{
0: linksTo(0),
1: linksTo(1),
2: linksTo(2),
3: linksTo(3),
4: linksTo(4),
5: linksTo(5),
},
want: math.NaN(),
},
},
wantLevels: []level{
{
q: math.Inf(-1), // Here math.Inf(-1) is used as a place holder for NaN to allow use of reflect.DeepEqual.
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
},
},
},
},
{
name: "small_dumbell",
g: smallDumbell,
structures: []structure{
{
resolution: 1,
// community structure and modularity calculated by java reference implementation.
memberships: []set{
0: linksTo(0, 1, 2),
1: linksTo(3, 4, 5),
},
want: 0.357, tol: 1e-3,
},
{
resolution: 1,
memberships: []set{
0: linksTo(0, 1, 2, 3, 4, 5),
},
// theoretical expectation.
want: 0, tol: 1e-14,
},
},
wantLevels: []level{
{
q: 0.35714285714285715,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1), simple.Node(2)},
{simple.Node(3), simple.Node(4), simple.Node(5)},
},
},
{
q: -0.17346938775510204,
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
},
},
},
},
{
name: "zachary",
g: zachary,
structures: []structure{
{
resolution: 1,
// community structure and modularity from doi: 10.1140/epjb/e2013-40829-0
memberships: []set{
0: linksTo(0, 1, 2, 3, 7, 11, 12, 13, 17, 19, 21),
1: linksTo(4, 5, 6, 10, 16),
2: linksTo(8, 9, 14, 15, 18, 20, 22, 26, 29, 30, 32, 33),
3: linksTo(23, 24, 25, 27, 28, 31),
},
// Noted to be the optimal modularisation in the paper above.
want: 0.4198, tol: 1e-4,
},
{
resolution: 0.5,
// community structure and modularity calculated by java reference implementation.
memberships: []set{
0: linksTo(0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 16, 17, 19, 21),
1: linksTo(8, 14, 15, 18, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33),
},
want: 0.6218, tol: 1e-3,
},
{
resolution: 2,
// community structure and modularity calculated by java reference implementation.
memberships: []set{
0: linksTo(14, 18, 20, 22, 32, 33, 15),
1: linksTo(0, 1, 11, 17, 19, 21),
2: linksTo(2, 3, 7, 9, 12, 13),
3: linksTo(4, 5, 6, 10, 16),
4: linksTo(24, 25, 28, 31),
5: linksTo(23, 26, 27, 29),
6: linksTo(8, 30),
},
want: 0.1645, tol: 1e-3,
},
},
wantLevels: []level{
{
q: 0.4197896120973044,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(3), simple.Node(7), simple.Node(11), simple.Node(12), simple.Node(13), simple.Node(17), simple.Node(19), simple.Node(21)},
{simple.Node(4), simple.Node(5), simple.Node(6), simple.Node(10), simple.Node(16)},
{simple.Node(8), simple.Node(9), simple.Node(14), simple.Node(15), simple.Node(18), simple.Node(20), simple.Node(22), simple.Node(26), simple.Node(29), simple.Node(30), simple.Node(32), simple.Node(33)},
{simple.Node(23), simple.Node(24), simple.Node(25), simple.Node(27), simple.Node(28), simple.Node(31)},
},
},
{
q: 0.39907955292570674,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(3), simple.Node(7), simple.Node(11), simple.Node(12), simple.Node(13), simple.Node(17), simple.Node(19), simple.Node(21)},
{simple.Node(4), simple.Node(10)},
{simple.Node(5), simple.Node(6), simple.Node(16)},
{simple.Node(8), simple.Node(9), simple.Node(14), simple.Node(15), simple.Node(18), simple.Node(20), simple.Node(22), simple.Node(26), simple.Node(29), simple.Node(30), simple.Node(32), simple.Node(33)},
{simple.Node(23), simple.Node(24), simple.Node(25), simple.Node(27), simple.Node(28), simple.Node(31)},
},
},
{
q: -0.04980276134122286,
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
{simple.Node(6)},
{simple.Node(7)},
{simple.Node(8)},
{simple.Node(9)},
{simple.Node(10)},
{simple.Node(11)},
{simple.Node(12)},
{simple.Node(13)},
{simple.Node(14)},
{simple.Node(15)},
{simple.Node(16)},
{simple.Node(17)},
{simple.Node(18)},
{simple.Node(19)},
{simple.Node(20)},
{simple.Node(21)},
{simple.Node(22)},
{simple.Node(23)},
{simple.Node(24)},
{simple.Node(25)},
{simple.Node(26)},
{simple.Node(27)},
{simple.Node(28)},
{simple.Node(29)},
{simple.Node(30)},
{simple.Node(31)},
{simple.Node(32)},
{simple.Node(33)},
},
},
},
},
{
name: "blondel",
g: blondel,
structures: []structure{
{
resolution: 1,
// community structure and modularity calculated by java reference implementation.
memberships: []set{
0: linksTo(0, 1, 2, 3, 4, 5, 6, 7),
1: linksTo(8, 9, 10, 11, 12, 13, 14, 15),
},
want: 0.3922, tol: 1e-4,
},
},
wantLevels: []level{
{
q: 0.39221938775510207,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(3), simple.Node(4), simple.Node(5), simple.Node(6), simple.Node(7)},
{simple.Node(8), simple.Node(9), simple.Node(10), simple.Node(11), simple.Node(12), simple.Node(13), simple.Node(14), simple.Node(15)},
},
},
{
q: 0.34630102040816324,
communities: [][]graph.Node{
{simple.Node(0), simple.Node(1), simple.Node(2), simple.Node(4), simple.Node(5)},
{simple.Node(3), simple.Node(6), simple.Node(7)},
{simple.Node(8), simple.Node(9), simple.Node(10), simple.Node(12), simple.Node(14), simple.Node(15)},
{simple.Node(11), simple.Node(13)},
},
},
{
q: -0.07142857142857144,
communities: [][]graph.Node{
{simple.Node(0)},
{simple.Node(1)},
{simple.Node(2)},
{simple.Node(3)},
{simple.Node(4)},
{simple.Node(5)},
{simple.Node(6)},
{simple.Node(7)},
{simple.Node(8)},
{simple.Node(9)},
{simple.Node(10)},
{simple.Node(11)},
{simple.Node(12)},
{simple.Node(13)},
{simple.Node(14)},
{simple.Node(15)},
},
},
},
},
}
func TestCommunityQUndirected(t *testing.T) {
for _, test := range communityUndirectedQTests {
g := simple.NewUndirectedGraph(0, 0)
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
for _, structure := range test.structures {
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communities[i] = append(communities[i], simple.Node(n))
}
}
got := Q(g, communities, structure.resolution)
if !floats.EqualWithinAbsOrRel(got, structure.want, structure.tol, structure.tol) && !math.IsNaN(structure.want) {
for _, c := range communities {
sort.Sort(ordered.ByID(c))
}
t.Errorf("unexpected Q value for %q %v: got: %v want: %v",
test.name, communities, got, structure.want)
}
}
}
}
func TestCommunityDeltaQUndirected(t *testing.T) {
tests:
for _, test := range communityUndirectedQTests {
g := simple.NewUndirectedGraph(0, 0)
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
rnd := rand.New(rand.NewSource(1)).Intn
for _, structure := range test.structures {
communityOf := make(map[int]int)
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communityOf[n] = i
communities[i] = append(communities[i], simple.Node(n))
}
sort.Sort(ordered.ByID(communities[i]))
}
before := Q(g, communities, structure.resolution)
l := newUndirectedLocalMover(reduceUndirected(g, nil), communities, structure.resolution)
if l == nil {
if !math.IsNaN(before) {
t.Errorf("unexpected nil localMover with non-NaN Q graph: Q=%.4v", before)
}
continue tests
}
// This is done to avoid run-to-run
// variation due to map iteration order.
sort.Sort(ordered.ByID(l.nodes))
l.shuffle(rnd)
for _, target := range l.nodes {
got, gotDst, gotSrc := l.deltaQ(target)
want, wantDst := math.Inf(-1), -1
migrated := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
if n == target.ID() {
continue
}
migrated[i] = append(migrated[i], simple.Node(n))
}
sort.Sort(ordered.ByID(migrated[i]))
}
for i, c := range structure.memberships {
if i == communityOf[target.ID()] {
continue
}
connected := false
for n := range c {
if g.HasEdgeBetween(simple.Node(n), target) {
connected = true
break
}
}
if !connected {
continue
}
migrated[i] = append(migrated[i], target)
after := Q(g, migrated, structure.resolution)
migrated[i] = migrated[i][:len(migrated[i])-1]
if after-before > want {
want = after - before
wantDst = i
}
}
if !floats.EqualWithinAbsOrRel(got, want, structure.tol, structure.tol) || gotDst != wantDst {
t.Errorf("unexpected result moving n=%d in c=%d of %s/%.4v: got: %.4v,%d want: %.4v,%d"+
"\n\t%v\n\t%v",
target.ID(), communityOf[target.ID()], test.name, structure.resolution, got, gotDst, want, wantDst,
communities, migrated)
}
if gotSrc.community != communityOf[target.ID()] {
t.Errorf("unexpected source community index: got: %d want: %d", gotSrc, communityOf[target.ID()])
} else if communities[gotSrc.community][gotSrc.node].ID() != target.ID() {
wantNodeIdx := -1
for i, n := range communities[gotSrc.community] {
if n.ID() == target.ID() {
wantNodeIdx = i
break
}
}
t.Errorf("unexpected source node index: got: %d want: %d", gotSrc.node, wantNodeIdx)
}
}
}
}
}
func TestReduceQConsistencyUndirected(t *testing.T) {
tests:
for _, test := range communityUndirectedQTests {
g := simple.NewUndirectedGraph(0, 0)
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
for _, structure := range test.structures {
if math.IsNaN(structure.want) {
continue tests
}
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communities[i] = append(communities[i], simple.Node(n))
}
sort.Sort(ordered.ByID(communities[i]))
}
gQ := Q(g, communities, structure.resolution)
gQnull := Q(g, nil, 1)
cg0 := reduceUndirected(g, nil)
cg0Qnull := Q(cg0, cg0.Structure(), 1)
if !floats.EqualWithinAbsOrRel(gQnull, cg0Qnull, structure.tol, structure.tol) {
t.Errorf("disagreement between null Q from method: %v and function: %v", cg0Qnull, gQnull)
}
cg0Q := Q(cg0, communities, structure.resolution)
if !floats.EqualWithinAbsOrRel(gQ, cg0Q, structure.tol, structure.tol) {
t.Errorf("unexpected Q result after initial reduction: got: %v want :%v", cg0Q, gQ)
}
cg1 := reduceUndirected(cg0, communities)
cg1Q := Q(cg1, cg1.Structure(), structure.resolution)
if !floats.EqualWithinAbsOrRel(gQ, cg1Q, structure.tol, structure.tol) {
t.Errorf("unexpected Q result after second reduction: got: %v want :%v", cg1Q, gQ)
}
}
}
}
var localUndirectedMoveTests = []struct {
name string
g []set
structures []moveStructures
}{
{
name: "blondel",
g: blondel,
structures: []moveStructures{
{
memberships: []set{
0: linksTo(0, 1, 2, 4, 5),
1: linksTo(3, 6, 7),
2: linksTo(8, 9, 10, 12, 14, 15),
3: linksTo(11, 13),
},
targetNodes: []graph.Node{simple.Node(0)},
resolution: 1,
tol: 1e-14,
},
{
memberships: []set{
0: linksTo(0, 1, 2, 4, 5),
1: linksTo(3, 6, 7),
2: linksTo(8, 9, 10, 12, 14, 15),
3: linksTo(11, 13),
},
targetNodes: []graph.Node{simple.Node(3)},
resolution: 1,
tol: 1e-14,
},
{
memberships: []set{
0: linksTo(0, 1, 2, 4, 5),
1: linksTo(3, 6, 7),
2: linksTo(8, 9, 10, 12, 14, 15),
3: linksTo(11, 13),
},
// Case to demonstrate when A_aa != k_a^𝛼.
targetNodes: []graph.Node{simple.Node(3), simple.Node(2)},
resolution: 1,
tol: 1e-14,
},
},
},
}
func TestMoveLocalUndirected(t *testing.T) {
for _, test := range localUndirectedMoveTests {
g := simple.NewUndirectedGraph(0, 0)
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
for _, structure := range test.structures {
communities := make([][]graph.Node, len(structure.memberships))
for i, c := range structure.memberships {
for n := range c {
communities[i] = append(communities[i], simple.Node(n))
}
sort.Sort(ordered.ByID(communities[i]))
}
r := reduceUndirected(reduceUndirected(g, nil), communities)
l := newUndirectedLocalMover(r, r.communities, structure.resolution)
for _, n := range structure.targetNodes {
dQ, dst, src := l.deltaQ(n)
if dQ > 0 {
before := Q(r, l.communities, structure.resolution)
l.move(dst, src)
after := Q(r, l.communities, structure.resolution)
want := after - before
if !floats.EqualWithinAbsOrRel(dQ, want, structure.tol, structure.tol) {
t.Errorf("unexpected deltaQ: got: %v want: %v", dQ, want)
}
}
}
}
}
}
func TestModularizeUndirected(t *testing.T) {
const louvainIterations = 20
for _, test := range communityUndirectedQTests {
g := simple.NewUndirectedGraph(0, 0)
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
if test.structures[0].resolution != 1 {
panic("bad test: expect resolution=1")
}
want := make([][]graph.Node, len(test.structures[0].memberships))
for i, c := range test.structures[0].memberships {
for n := range c {
want[i] = append(want[i], simple.Node(n))
}
sort.Sort(ordered.ByID(want[i]))
}
sort.Sort(ordered.BySliceIDs(want))
var (
got *ReducedUndirected
bestQ = math.Inf(-1)
)
// Modularize is randomised so we do this to
// ensure the level tests are consistent.
src := rand.New(rand.NewSource(1))
for i := 0; i < louvainIterations; i++ {
r := Modularize(g, 1, src).(*ReducedUndirected)
if q := Q(r, nil, 1); q > bestQ || math.IsNaN(q) {
bestQ = q
got = r
if math.IsNaN(q) {
// Don't try again for non-connected case.
break
}
}
var qs []float64
for p := r; p != nil; p = p.Expanded().(*ReducedUndirected) {
qs = append(qs, Q(p, nil, 1))
}
// Recovery of Q values is reversed.
if reverse(qs); !sort.Float64sAreSorted(qs) {
t.Errorf("Q values not monotonically increasing: %.5v", qs)
}
}
gotCommunities := got.Communities()
for _, c := range gotCommunities {
sort.Sort(ordered.ByID(c))
}
sort.Sort(ordered.BySliceIDs(gotCommunities))
if !reflect.DeepEqual(gotCommunities, want) {
t.Errorf("unexpected community membership for %s Q=%.4v:\n\tgot: %v\n\twant:%v",
test.name, bestQ, gotCommunities, want)
continue
}
var levels []level
for p := got; p != nil; p = p.Expanded().(*ReducedUndirected) {
var communities [][]graph.Node
if p.parent != nil {
communities = p.parent.Communities()
for _, c := range communities {
sort.Sort(ordered.ByID(c))
}
sort.Sort(ordered.BySliceIDs(communities))
} else {
communities = reduceUndirected(g, nil).Communities()
}
q := Q(p, nil, 1)
if math.IsNaN(q) {
// Use an equalable flag value in place of NaN.
q = math.Inf(-1)
}
levels = append(levels, level{q: q, communities: communities})
}
if !reflect.DeepEqual(levels, test.wantLevels) {
t.Errorf("unexpected level structure:\n\tgot: %v\n\twant:%v", levels, test.wantLevels)
}
}
}
func TestNonContiguousUndirected(t *testing.T) {
g := simple.NewUndirectedGraph(0, 0)
for _, e := range []simple.Edge{
{F: simple.Node(0), T: simple.Node(1), W: 1},
{F: simple.Node(4), T: simple.Node(5), W: 1},
} {
g.SetEdge(e)
}
func() {
defer func() {
r := recover()
if r != nil {
t.Error("unexpected panic with non-contiguous ID range")
}
}()
Modularize(g, 1, nil)
}()
}
func BenchmarkLouvain(b *testing.B) {
src := rand.New(rand.NewSource(1))
for i := 0; i < b.N; i++ {
Modularize(dupGraph, 1, src)
}
}

View File

@@ -0,0 +1,142 @@
// Copyright ©2016 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.
// +build ignore
// printgraphs allows us to generate a consistent directed view of
// a set of edges that follows a reasonably real-world-meaningful
// graph. The interpretation of the links in the resulting directed
// graphs are either "suggests" in the context of a Page Ranking or
// possibly "looks up to" in the Zachary graph.
package main
import (
"fmt"
"sort"
"github.com/gonum/graph"
"github.com/gonum/graph/internal/ordered"
"github.com/gonum/graph/network"
"github.com/gonum/graph/simple"
)
// set is an integer set.
type set map[int]struct{}
func linksTo(i ...int) set {
if len(i) == 0 {
return nil
}
s := make(set)
for _, v := range i {
s[v] = struct{}{}
}
return s
}
var (
zachary = []set{
0: linksTo(1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 17, 19, 21, 31),
1: linksTo(2, 3, 7, 13, 17, 19, 21, 30),
2: linksTo(3, 7, 8, 9, 13, 27, 28, 32),
3: linksTo(7, 12, 13),
4: linksTo(6, 10),
5: linksTo(6, 10, 16),
6: linksTo(16),
8: linksTo(30, 32, 33),
9: linksTo(33),
13: linksTo(33),
14: linksTo(32, 33),
15: linksTo(32, 33),
18: linksTo(32, 33),
19: linksTo(33),
20: linksTo(32, 33),
22: linksTo(32, 33),
23: linksTo(25, 27, 29, 32, 33),
24: linksTo(25, 27, 31),
25: linksTo(31),
26: linksTo(29, 33),
27: linksTo(33),
28: linksTo(31, 33),
29: linksTo(32, 33),
30: linksTo(32, 33),
31: linksTo(32, 33),
32: linksTo(33),
33: nil,
}
blondel = []set{
0: linksTo(2, 3, 4, 5),
1: linksTo(2, 4, 7),
2: linksTo(4, 5, 6),
3: linksTo(7),
4: linksTo(10),
5: linksTo(7, 11),
6: linksTo(7, 11),
8: linksTo(9, 10, 11, 14, 15),
9: linksTo(12, 14),
10: linksTo(11, 12, 13, 14),
11: linksTo(13),
15: nil,
}
)
func main() {
for _, raw := range []struct {
name string
set []set
}{
{"zachary", zachary},
{"blondel", blondel},
} {
g := simple.NewUndirectedGraph(0, 0)
for u, e := range raw.set {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
nodes := g.Nodes()
sort.Sort(ordered.ByID(nodes))
fmt.Printf("%s = []set{\n", raw.name)
rank := network.PageRank(asDirected{g}, 0.85, 1e-8)
for _, u := range nodes {
to := g.From(nodes[u.ID()])
sort.Sort(ordered.ByID(to))
var links []int
for _, v := range to {
if rank[u.ID()] <= rank[v.ID()] {
links = append(links, v.ID())
}
}
if links == nil {
fmt.Printf("\t%d: nil, // rank=%.4v\n", u.ID(), rank[u.ID()])
continue
}
fmt.Printf("\t%d: linksTo(", u.ID())
for i, v := range links {
if i != 0 {
fmt.Print(", ")
}
fmt.Print(v)
}
fmt.Printf("), // rank=%.4v\n", rank[u.ID()])
}
fmt.Println("}")
}
}
type asDirected struct{ *simple.UndirectedGraph }
func (g asDirected) HasEdgeFromTo(u, v graph.Node) bool {
return g.UndirectedGraph.HasEdgeBetween(u, v)
}
func (g asDirected) To(v graph.Node) []graph.Node { return g.From(v) }

38
graph/doc.go Normal file
View File

@@ -0,0 +1,38 @@
// Copyright ©2014 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 implements functions and interfaces to deal with formal discrete graphs. It aims to
be first and foremost flexible, with speed as a strong second priority.
In this package, graphs are taken to be directed, and undirected graphs are considered to be a
special case of directed graphs that happen to have reciprocal edges. Graphs are, by default,
unweighted, but functions that require weighted edges have several methods of dealing with this.
In order of precedence:
1. These functions have an argument called Cost (and in some cases, HeuristicCost). If this is
present, it will always be used to determine the cost between two nodes.
2. These functions will check if your graph implements the Coster (and/or HeuristicCoster)
interface. If this is present, and the Cost (or HeuristicCost) argument is nil, these functions
will be used.
3. Finally, if no user data is supplied, it will use the functions UniformCost (always returns 1)
and/or NulLHeuristic (always returns 0).
For information on the specification for Cost functions, please see the Coster interface.
Finally, although the functions take in a Graph -- they will always use the correct behavior.
If your graph implements DirectedGraph, it will use Successors and To where applicable,
if undirected, it will use From instead. If it implements neither, it will scan the edge list
for successors and predecessors where applicable. (This is slow, you should always implement either
Directed or Undirected)
This package will never modify a graph that is not Mutable (and the interface does not allow it to
do so). However, return values are free to be modified, so never pass a reference to your own edge
list or node list. It also guarantees that any nodes passed back to the user will be the same
nodes returned to it -- that is, it will never take a Node's ID and then wrap the ID in a new
struct and return that. You'll always get back your original data.
*/
package graph

View File

@@ -0,0 +1,237 @@
// Copyright ©2017 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 dot
import (
"fmt"
"github.com/gonum/graph"
"github.com/gonum/graph/formats/dot"
"github.com/gonum/graph/formats/dot/ast"
"golang.org/x/tools/container/intsets"
)
// Builder is a graph that can have user-defined nodes and edges added.
type Builder interface {
graph.Graph
graph.Builder
// NewNode adds a new node with a unique node ID to the graph.
NewNode() graph.Node
// NewEdge adds a new edge from the source to the destination node to the
// graph, or returns the existing edge if already present.
NewEdge(from, to graph.Node) graph.Edge
}
// UnmarshalerAttr is the interface implemented by objects that can unmarshal a
// DOT attribute description of themselves.
type UnmarshalerAttr interface {
// UnmarshalDOTAttr decodes a single DOT attribute.
UnmarshalDOTAttr(attr Attribute) error
}
// Unmarshal parses the Graphviz DOT-encoded data and stores the result in dst.
func Unmarshal(data []byte, dst Builder) error {
file, err := dot.ParseBytes(data)
if err != nil {
return err
}
if len(file.Graphs) != 1 {
return fmt.Errorf("invalid number of graphs; expected 1, got %d", len(file.Graphs))
}
return copyGraph(dst, file.Graphs[0])
}
// copyGraph copies the nodes and edges from the Graphviz AST source graph to
// the destination graph. Edge direction is maintained if present.
func copyGraph(dst Builder, src *ast.Graph) (err error) {
defer func() {
switch e := recover().(type) {
case nil:
case error:
err = e
default:
panic(e)
}
}()
gen := &generator{
directed: src.Directed,
ids: make(map[string]graph.Node),
}
for _, stmt := range src.Stmts {
gen.addStmt(dst, stmt)
}
return err
}
// A generator keeps track of the information required for generating a gonum
// graph from a dot AST graph.
type generator struct {
// Directed graph.
directed bool
// Map from dot AST node ID to gonum node.
ids map[string]graph.Node
// Nodes processed within the context of a subgraph, that is to be used as a
// vertex of an edge.
subNodes []graph.Node
// Stack of start indices into the subgraph node slice. The top element
// corresponds to the start index of the active (or inner-most) subgraph.
subStart []int
}
// node returns the gonum node corresponding to the given dot AST node ID,
// generating a new such node if none exist.
func (gen *generator) node(dst Builder, id string) graph.Node {
if n, ok := gen.ids[id]; ok {
return n
}
n := dst.NewNode()
gen.ids[id] = n
// Check if within the context of a subgraph, that is to be used as a vertex
// of an edge.
if gen.isInSubgraph() {
// Append node processed within the context of a subgraph, that is to be
// used as a vertex of an edge
gen.appendSubgraphNode(n)
}
return n
}
// addStmt adds the given statement to the graph.
func (gen *generator) addStmt(dst Builder, stmt ast.Stmt) {
switch stmt := stmt.(type) {
case *ast.NodeStmt:
n := gen.node(dst, stmt.Node.ID)
if n, ok := n.(UnmarshalerAttr); ok {
for _, attr := range stmt.Attrs {
a := Attribute{
Key: attr.Key,
Value: attr.Val,
}
if err := n.UnmarshalDOTAttr(a); err != nil {
panic(fmt.Errorf("unable to unmarshal node DOT attribute (%s=%s)", a.Key, a.Value))
}
}
}
case *ast.EdgeStmt:
gen.addEdgeStmt(dst, stmt)
case *ast.AttrStmt:
// ignore.
case *ast.Attr:
// ignore.
case *ast.Subgraph:
for _, stmt := range stmt.Stmts {
gen.addStmt(dst, stmt)
}
default:
panic(fmt.Sprintf("unknown statement type %T", stmt))
}
}
// addEdgeStmt adds the given edge statement to the graph.
func (gen *generator) addEdgeStmt(dst Builder, e *ast.EdgeStmt) {
fs := gen.addVertex(dst, e.From)
ts := gen.addEdge(dst, e.To)
for _, f := range fs {
for _, t := range ts {
edge := dst.NewEdge(f, t)
if edge, ok := edge.(UnmarshalerAttr); ok {
for _, attr := range e.Attrs {
a := Attribute{
Key: attr.Key,
Value: attr.Val,
}
if err := edge.UnmarshalDOTAttr(a); err != nil {
panic(fmt.Errorf("unable to unmarshal edge DOT attribute (%s=%s)", a.Key, a.Value))
}
}
}
}
}
}
// addVertex adds the given vertex to the graph, and returns its set of nodes.
func (gen *generator) addVertex(dst Builder, v ast.Vertex) []graph.Node {
switch v := v.(type) {
case *ast.Node:
n := gen.node(dst, v.ID)
return []graph.Node{n}
case *ast.Subgraph:
gen.pushSubgraph()
for _, stmt := range v.Stmts {
gen.addStmt(dst, stmt)
}
return gen.popSubgraph()
default:
panic(fmt.Sprintf("unknown vertex type %T", v))
}
}
// addEdge adds the given edge to the graph, and returns its set of nodes.
func (gen *generator) addEdge(dst Builder, to *ast.Edge) []graph.Node {
if !gen.directed && to.Directed {
panic(fmt.Errorf("directed edge to %v in undirected graph", to.Vertex))
}
fs := gen.addVertex(dst, to.Vertex)
if to.To != nil {
ts := gen.addEdge(dst, to.To)
for _, f := range fs {
for _, t := range ts {
dst.NewEdge(f, t)
}
}
}
return fs
}
// pushSubgraph pushes the node start index of the active subgraph onto the
// stack.
func (gen *generator) pushSubgraph() {
gen.subStart = append(gen.subStart, len(gen.subNodes))
}
// popSubgraph pops the node start index of the active subgraph from the stack,
// and returns the nodes processed since.
func (gen *generator) popSubgraph() []graph.Node {
// Get nodes processed since the subgraph became active.
start := gen.subStart[len(gen.subStart)-1]
// TODO: Figure out a better way to store subgraph nodes, so that duplicates
// may not occur.
nodes := unique(gen.subNodes[start:])
// Remove subgraph from stack.
gen.subStart = gen.subStart[:len(gen.subStart)-1]
if len(gen.subStart) == 0 {
// Remove subgraph nodes when the bottom-most subgraph has been processed.
gen.subNodes = gen.subNodes[:0]
}
return nodes
}
// unique returns the set of unique nodes contained within ns.
func unique(ns []graph.Node) []graph.Node {
var nodes []graph.Node
var set intsets.Sparse
for _, n := range ns {
id := n.ID()
if set.Has(id) {
// skip duplicate node
continue
}
set.Insert(id)
nodes = append(nodes, n)
}
return nodes
}
// isInSubgraph reports whether the active context is within a subgraph, that is
// to be used as a vertex of an edge.
func (gen *generator) isInSubgraph() bool {
return len(gen.subStart) > 0
}
// appendSubgraphNode appends the given node to the slice of nodes processed
// within the context of a subgraph.
func (gen *generator) appendSubgraphNode(n graph.Node) {
gen.subNodes = append(gen.subNodes, n)
}

View File

@@ -0,0 +1,196 @@
// Copyright ©2017 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 dot
import (
"fmt"
"testing"
"github.com/gonum/graph"
"github.com/gonum/graph/simple"
)
func TestRoundTrip(t *testing.T) {
golden := []struct {
want string
directed bool
}{
{
want: directed,
directed: true,
},
{
want: undirected,
directed: false,
},
}
for i, g := range golden {
var dst Builder
if g.directed {
dst = newDotDirectedGraph()
} else {
dst = newDotUndirectedGraph()
}
data := []byte(g.want)
if err := Unmarshal(data, dst); err != nil {
t.Errorf("i=%d: unable to unmarshal DOT graph; %v", i, err)
continue
}
buf, err := Marshal(dst, "", "", "\t", false)
if err != nil {
t.Errorf("i=%d: unable to marshal graph; %v", i, dst)
continue
}
got := string(buf)
if got != g.want {
t.Errorf("i=%d: graph content mismatch; expected `%s`, got `%s`", i, g.want, got)
continue
}
}
}
const directed = `digraph {
// Node definitions.
0 [label="foo 2"];
1 [label="bar 2"];
// Edge definitions.
0 -> 1 [label="baz 2"];
}`
const undirected = `graph {
// Node definitions.
0 [label="foo 2"];
1 [label="bar 2"];
// Edge definitions.
0 -- 1 [label="baz 2"];
}`
// Below follows a minimal implementation of a graph capable of validating the
// round-trip encoding and decoding of DOT graphs with nodes and edges
// containing DOT attributes.
// dotDirectedGraph extends simple.DirectedGraph to add NewNode and NewEdge
// methods for creating user-defined nodes and edges.
//
// dotDirectedGraph implements the dot.Builder interface.
type dotDirectedGraph struct {
*simple.DirectedGraph
}
// newDotDirectedGraph returns a new directed capable of creating user-defined
// nodes and edges.
func newDotDirectedGraph() *dotDirectedGraph {
return &dotDirectedGraph{DirectedGraph: simple.NewDirectedGraph(0, 0)}
}
// NewNode adds a new node with a unique node ID to the graph.
func (g *dotDirectedGraph) NewNode() graph.Node {
n := &dotNode{Node: simple.Node(g.NewNodeID())}
g.AddNode(n)
return n
}
// NewEdge adds a new edge from the source to the destination node to the graph,
// or returns the existing edge if already present.
func (g *dotDirectedGraph) NewEdge(from, to graph.Node) graph.Edge {
if e := g.Edge(from, to); e != nil {
return e
}
e := &dotEdge{Edge: simple.Edge{F: from, T: to}}
g.SetEdge(e)
return e
}
// dotUndirectedGraph extends simple.UndirectedGraph to add NewNode and NewEdge
// methods for creating user-defined nodes and edges.
//
// dotUndirectedGraph implements the dot.Builder interface.
type dotUndirectedGraph struct {
*simple.UndirectedGraph
}
// newDotUndirectedGraph returns a new undirected capable of creating user-
// defined nodes and edges.
func newDotUndirectedGraph() *dotUndirectedGraph {
return &dotUndirectedGraph{UndirectedGraph: simple.NewUndirectedGraph(0, 0)}
}
// NewNode adds a new node with a unique node ID to the graph.
func (g *dotUndirectedGraph) NewNode() graph.Node {
n := &dotNode{Node: simple.Node(g.NewNodeID())}
g.AddNode(n)
return n
}
// NewEdge adds a new edge from the source to the destination node to the graph,
// or returns the existing edge if already present.
func (g *dotUndirectedGraph) NewEdge(from, to graph.Node) graph.Edge {
if e := g.Edge(from, to); e != nil {
return e
}
e := &dotEdge{Edge: simple.Edge{F: from, T: to}}
g.SetEdge(e)
return e
}
// dotNode extends simple.Node with a label field to test round-trip encoding
// and decoding of node DOT label attributes.
type dotNode struct {
simple.Node
// Node label.
Label string
}
// UnmarshalDOTAttr decodes a single DOT attribute.
func (n *dotNode) UnmarshalDOTAttr(attr Attribute) error {
if attr.Key != "label" {
return fmt.Errorf("unable to unmarshal node DOT attribute with key %q", attr.Key)
}
n.Label = attr.Value
return nil
}
// DOTAttributes returns the DOT attributes of the node.
func (n *dotNode) DOTAttributes() []Attribute {
if len(n.Label) == 0 {
return nil
}
attr := Attribute{
Key: "label",
Value: n.Label,
}
return []Attribute{attr}
}
// dotEdge extends simple.Edge with a label field to test round-trip encoding and
// decoding of edge DOT label attributes.
type dotEdge struct {
simple.Edge
// Edge label.
Label string
}
// UnmarshalDOTAttr decodes a single DOT attribute.
func (e *dotEdge) UnmarshalDOTAttr(attr Attribute) error {
if attr.Key != "label" {
return fmt.Errorf("unable to unmarshal node DOT attribute with key %q", attr.Key)
}
e.Label = attr.Value
return nil
}
// DOTAttributes returns the DOT attributes of the edge.
func (e *dotEdge) DOTAttributes() []Attribute {
if len(e.Label) == 0 {
return nil
}
attr := Attribute{
Key: "label",
Value: e.Label,
}
return []Attribute{attr}
}

378
graph/encoding/dot/dot.go Normal file
View File

@@ -0,0 +1,378 @@
// 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 dot implements GraphViz DOT marshaling of graphs.
//
// See the GraphViz DOT Guide and the DOT grammar for more information
// on using specific aspects of the DOT language:
//
// DOT Guide: http://www.graphviz.org/Documentation/dotguide.pdf
//
// DOT grammar: http://www.graphviz.org/doc/info/lang.html
//
package dot
import (
"bytes"
"errors"
"fmt"
"sort"
"strings"
"github.com/gonum/graph"
"github.com/gonum/graph/internal/ordered"
)
// Node is a DOT graph node.
type Node interface {
// DOTID returns a DOT node ID.
//
// An ID is one of the following:
//
// - a string of alphabetic ([a-zA-Z\x80-\xff]) characters, underscores ('_').
// digits ([0-9]), not beginning with a digit.
// - a numeral [-]?(.[0-9]+ | [0-9]+(.[0-9]*)?).
// - a double-quoted string ("...") possibly containing escaped quotes (\").
// - an HTML string (<...>).
DOTID() string
}
// Attributers are graph.Graph values that specify top-level DOT
// attributes.
type Attributers interface {
DOTAttributers() (graph, node, edge Attributer)
}
// Attributer defines graph.Node or graph.Edge values that can
// specify DOT attributes.
type Attributer interface {
DOTAttributes() []Attribute
}
// Attribute is a DOT language key value attribute pair.
type Attribute struct {
Key, Value string
}
// Porter defines the behavior of graph.Edge values that can specify
// connection ports for their end points. The returned port corresponds
// to the the DOT node port to be used by the edge, compass corresponds
// to DOT compass point to which the edge will be aimed.
type Porter interface {
FromPort() (port, compass string)
ToPort() (port, compass string)
}
// Structurer represents a graph.Graph that can define subgraphs.
type Structurer interface {
Structure() []Graph
}
// Graph wraps named graph.Graph values.
type Graph interface {
graph.Graph
DOTID() string
}
// Subgrapher wraps graph.Node values that represent subgraphs.
type Subgrapher interface {
Subgraph() graph.Graph
}
// Marshal returns the DOT encoding for the graph g, applying the prefix
// and indent to the encoding. Name is used to specify the graph name. If
// name is empty and g implements Graph, the returned string from DOTID
// will be used. If strict is true the output bytes will be prefixed with
// the DOT "strict" keyword.
//
// Graph serialization will work for a graph.Graph without modification,
// however, advanced GraphViz DOT features provided by Marshal depend on
// implementation of the Node, Attributer, Porter, Attributers, Structurer,
// Subgrapher and Graph interfaces.
func Marshal(g graph.Graph, name, prefix, indent string, strict bool) ([]byte, error) {
var p printer
p.indent = indent
p.prefix = prefix
p.visited = make(map[edge]bool)
if strict {
p.buf.WriteString("strict ")
}
err := p.print(g, name, false, false)
if err != nil {
return nil, err
}
return p.buf.Bytes(), nil
}
type printer struct {
buf bytes.Buffer
prefix string
indent string
depth int
visited map[edge]bool
err error
}
type edge struct {
inGraph string
from, to int
}
func (p *printer) print(g graph.Graph, name string, needsIndent, isSubgraph bool) error {
nodes := g.Nodes()
sort.Sort(ordered.ByID(nodes))
p.buf.WriteString(p.prefix)
if needsIndent {
for i := 0; i < p.depth; i++ {
p.buf.WriteString(p.indent)
}
}
_, isDirected := g.(graph.Directed)
if isSubgraph {
p.buf.WriteString("sub")
} else if isDirected {
p.buf.WriteString("di")
}
p.buf.WriteString("graph")
if name == "" {
if g, ok := g.(Graph); ok {
name = g.DOTID()
}
}
if name != "" {
p.buf.WriteByte(' ')
p.buf.WriteString(name)
}
p.openBlock(" {")
if a, ok := g.(Attributers); ok {
p.writeAttributeComplex(a)
}
if s, ok := g.(Structurer); ok {
for _, g := range s.Structure() {
_, subIsDirected := g.(graph.Directed)
if subIsDirected != isDirected {
return errors.New("dot: mismatched graph type")
}
p.buf.WriteByte('\n')
p.print(g, g.DOTID(), true, true)
}
}
havePrintedNodeHeader := false
for _, n := range nodes {
if s, ok := n.(Subgrapher); ok {
// If the node is not linked to any other node
// the graph needs to be written now.
if len(g.From(n)) == 0 {
g := s.Subgraph()
_, subIsDirected := g.(graph.Directed)
if subIsDirected != isDirected {
return errors.New("dot: mismatched graph type")
}
if !havePrintedNodeHeader {
p.newline()
p.buf.WriteString("// Node definitions.")
havePrintedNodeHeader = true
}
p.newline()
p.print(g, graphID(g, n), false, true)
}
continue
}
if !havePrintedNodeHeader {
p.newline()
p.buf.WriteString("// Node definitions.")
havePrintedNodeHeader = true
}
p.newline()
p.writeNode(n)
if a, ok := n.(Attributer); ok {
p.writeAttributeList(a)
}
p.buf.WriteByte(';')
}
havePrintedEdgeHeader := false
for _, n := range nodes {
to := g.From(n)
sort.Sort(ordered.ByID(to))
for _, t := range to {
if isDirected {
if p.visited[edge{inGraph: name, from: n.ID(), to: t.ID()}] {
continue
}
p.visited[edge{inGraph: name, from: n.ID(), to: t.ID()}] = true
} else {
if p.visited[edge{inGraph: name, from: n.ID(), to: t.ID()}] {
continue
}
p.visited[edge{inGraph: name, from: n.ID(), to: t.ID()}] = true
p.visited[edge{inGraph: name, from: t.ID(), to: n.ID()}] = true
}
if !havePrintedEdgeHeader {
p.buf.WriteByte('\n')
p.buf.WriteString(strings.TrimRight(p.prefix, " \t\n")) // Trim whitespace suffix.
p.newline()
p.buf.WriteString("// Edge definitions.")
havePrintedEdgeHeader = true
}
p.newline()
if s, ok := n.(Subgrapher); ok {
g := s.Subgraph()
_, subIsDirected := g.(graph.Directed)
if subIsDirected != isDirected {
return errors.New("dot: mismatched graph type")
}
p.print(g, graphID(g, n), false, true)
} else {
p.writeNode(n)
}
e, edgeIsPorter := g.Edge(n, t).(Porter)
if edgeIsPorter {
p.writePorts(e.FromPort())
}
if isDirected {
p.buf.WriteString(" -> ")
} else {
p.buf.WriteString(" -- ")
}
if s, ok := t.(Subgrapher); ok {
g := s.Subgraph()
_, subIsDirected := g.(graph.Directed)
if subIsDirected != isDirected {
return errors.New("dot: mismatched graph type")
}
p.print(g, graphID(g, t), false, true)
} else {
p.writeNode(t)
}
if edgeIsPorter {
p.writePorts(e.ToPort())
}
if a, ok := g.Edge(n, t).(Attributer); ok {
p.writeAttributeList(a)
}
p.buf.WriteByte(';')
}
}
p.closeBlock("}")
return nil
}
func (p *printer) writeNode(n graph.Node) {
p.buf.WriteString(nodeID(n))
}
func (p *printer) writePorts(port, cp string) {
if port != "" {
p.buf.WriteByte(':')
p.buf.WriteString(port)
}
if cp != "" {
p.buf.WriteByte(':')
p.buf.WriteString(cp)
}
}
func nodeID(n graph.Node) string {
switch n := n.(type) {
case Node:
return n.DOTID()
default:
return fmt.Sprint(n.ID())
}
}
func graphID(g graph.Graph, n graph.Node) string {
switch g := g.(type) {
case Node:
return g.DOTID()
default:
return nodeID(n)
}
}
func (p *printer) writeAttributeList(a Attributer) {
attributes := a.DOTAttributes()
switch len(attributes) {
case 0:
case 1:
p.buf.WriteString(" [")
p.buf.WriteString(attributes[0].Key)
p.buf.WriteByte('=')
p.buf.WriteString(attributes[0].Value)
p.buf.WriteString("]")
default:
p.openBlock(" [")
for _, att := range attributes {
p.newline()
p.buf.WriteString(att.Key)
p.buf.WriteByte('=')
p.buf.WriteString(att.Value)
}
p.closeBlock("]")
}
}
var attType = []string{"graph", "node", "edge"}
func (p *printer) writeAttributeComplex(ca Attributers) {
g, n, e := ca.DOTAttributers()
haveWrittenBlock := false
for i, a := range []Attributer{g, n, e} {
attributes := a.DOTAttributes()
if len(attributes) == 0 {
continue
}
if haveWrittenBlock {
p.buf.WriteByte(';')
}
p.newline()
p.buf.WriteString(attType[i])
p.openBlock(" [")
for _, att := range attributes {
p.newline()
p.buf.WriteString(att.Key)
p.buf.WriteByte('=')
p.buf.WriteString(att.Value)
}
p.closeBlock("]")
haveWrittenBlock = true
}
if haveWrittenBlock {
p.buf.WriteString(";\n")
}
}
func (p *printer) newline() {
p.buf.WriteByte('\n')
p.buf.WriteString(p.prefix)
for i := 0; i < p.depth; i++ {
p.buf.WriteString(p.indent)
}
}
func (p *printer) openBlock(b string) {
p.buf.WriteString(b)
p.depth++
}
func (p *printer) closeBlock(b string) {
p.depth--
p.newline()
p.buf.WriteString(b)
}

File diff suppressed because it is too large Load Diff

278
graph/ex/fdpclust/gn.go Normal file
View File

@@ -0,0 +1,278 @@
package main
import (
"github.com/gonum/graph"
"github.com/gonum/graph/simple"
)
type GraphNode struct {
id int
neighbors []graph.Node
roots []*GraphNode
}
func (g *GraphNode) Has(n graph.Node) bool {
if n.ID() == g.id {
return true
}
visited := map[int]struct{}{g.id: struct{}{}}
for _, root := range g.roots {
if root.ID() == n.ID() {
return true
}
if root.has(n, visited) {
return true
}
}
for _, neigh := range g.neighbors {
if neigh.ID() == n.ID() {
return true
}
if gn, ok := neigh.(*GraphNode); ok {
if gn.has(n, visited) {
return true
}
}
}
return false
}
func (g *GraphNode) has(n graph.Node, visited map[int]struct{}) bool {
for _, root := range g.roots {
if _, ok := visited[root.ID()]; ok {
continue
}
visited[root.ID()] = struct{}{}
if root.ID() == n.ID() {
return true
}
if root.has(n, visited) {
return true
}
}
for _, neigh := range g.neighbors {
if _, ok := visited[neigh.ID()]; ok {
continue
}
visited[neigh.ID()] = struct{}{}
if neigh.ID() == n.ID() {
return true
}
if gn, ok := neigh.(*GraphNode); ok {
if gn.has(n, visited) {
return true
}
}
}
return false
}
func (g *GraphNode) Nodes() []graph.Node {
toReturn := []graph.Node{g}
visited := map[int]struct{}{g.id: struct{}{}}
for _, root := range g.roots {
toReturn = append(toReturn, root)
visited[root.ID()] = struct{}{}
toReturn = root.nodes(toReturn, visited)
}
for _, neigh := range g.neighbors {
toReturn = append(toReturn, neigh)
visited[neigh.ID()] = struct{}{}
if gn, ok := neigh.(*GraphNode); ok {
toReturn = gn.nodes(toReturn, visited)
}
}
return toReturn
}
func (g *GraphNode) nodes(list []graph.Node, visited map[int]struct{}) []graph.Node {
for _, root := range g.roots {
if _, ok := visited[root.ID()]; ok {
continue
}
visited[root.ID()] = struct{}{}
list = append(list, graph.Node(root))
list = root.nodes(list, visited)
}
for _, neigh := range g.neighbors {
if _, ok := visited[neigh.ID()]; ok {
continue
}
list = append(list, neigh)
if gn, ok := neigh.(*GraphNode); ok {
list = gn.nodes(list, visited)
}
}
return list
}
func (g *GraphNode) From(n graph.Node) []graph.Node {
if n.ID() == g.ID() {
return g.neighbors
}
visited := map[int]struct{}{g.id: struct{}{}}
for _, root := range g.roots {
visited[root.ID()] = struct{}{}
if result := root.findNeighbors(n, visited); result != nil {
return result
}
}
for _, neigh := range g.neighbors {
visited[neigh.ID()] = struct{}{}
if gn, ok := neigh.(*GraphNode); ok {
if result := gn.findNeighbors(n, visited); result != nil {
return result
}
}
}
return nil
}
func (g *GraphNode) findNeighbors(n graph.Node, visited map[int]struct{}) []graph.Node {
if n.ID() == g.ID() {
return g.neighbors
}
for _, root := range g.roots {
if _, ok := visited[root.ID()]; ok {
continue
}
visited[root.ID()] = struct{}{}
if result := root.findNeighbors(n, visited); result != nil {
return result
}
}
for _, neigh := range g.neighbors {
if _, ok := visited[neigh.ID()]; ok {
continue
}
visited[neigh.ID()] = struct{}{}
if gn, ok := neigh.(*GraphNode); ok {
if result := gn.findNeighbors(n, visited); result != nil {
return result
}
}
}
return nil
}
func (g *GraphNode) HasEdgeBetween(u, v graph.Node) bool {
return g.EdgeBetween(u, v) != nil
}
func (g *GraphNode) Edge(u, v graph.Node) graph.Edge {
return g.EdgeBetween(u, v)
}
func (g *GraphNode) EdgeBetween(u, v graph.Node) graph.Edge {
if u.ID() == g.id || v.ID() == g.id {
for _, neigh := range g.neighbors {
if neigh.ID() == u.ID() || neigh.ID() == v.ID() {
return simple.Edge{F: g, T: neigh}
}
}
return nil
}
visited := map[int]struct{}{g.id: struct{}{}}
for _, root := range g.roots {
visited[root.ID()] = struct{}{}
if result := root.edgeBetween(u, v, visited); result != nil {
return result
}
}
for _, neigh := range g.neighbors {
visited[neigh.ID()] = struct{}{}
if gn, ok := neigh.(*GraphNode); ok {
if result := gn.edgeBetween(u, v, visited); result != nil {
return result
}
}
}
return nil
}
func (g *GraphNode) edgeBetween(u, v graph.Node, visited map[int]struct{}) graph.Edge {
if u.ID() == g.id || v.ID() == g.id {
for _, neigh := range g.neighbors {
if neigh.ID() == u.ID() || neigh.ID() == v.ID() {
return simple.Edge{F: g, T: neigh}
}
}
return nil
}
for _, root := range g.roots {
if _, ok := visited[root.ID()]; ok {
continue
}
visited[root.ID()] = struct{}{}
if result := root.edgeBetween(u, v, visited); result != nil {
return result
}
}
for _, neigh := range g.neighbors {
if _, ok := visited[neigh.ID()]; ok {
continue
}
visited[neigh.ID()] = struct{}{}
if gn, ok := neigh.(*GraphNode); ok {
if result := gn.edgeBetween(u, v, visited); result != nil {
return result
}
}
}
return nil
}
func (g *GraphNode) ID() int {
return g.id
}
func (g *GraphNode) AddNeighbor(n *GraphNode) {
g.neighbors = append(g.neighbors, graph.Node(n))
}
func (g *GraphNode) AddRoot(n *GraphNode) {
g.roots = append(g.roots, n)
}
func NewGraphNode(id int) *GraphNode {
return &GraphNode{id: id, neighbors: make([]graph.Node, 0), roots: make([]*GraphNode, 0)}
}

75
graph/ex/fdpclust/main.go Normal file
View File

@@ -0,0 +1,75 @@
package main
import (
"fmt"
"github.com/gonum/graph"
"github.com/gonum/graph/topo"
)
func main() {
// graph G {
G := NewGraphNode(0)
// e
e := NewGraphNode(1)
// subgraph clusterA {
clusterA := NewGraphNode(2)
// a -- b
a := NewGraphNode(3)
b := NewGraphNode(4)
a.AddNeighbor(b)
b.AddNeighbor(a)
clusterA.AddRoot(a)
clusterA.AddRoot(b)
// subgraph clusterC {
clusterC := NewGraphNode(5)
// C -- D
C := NewGraphNode(6)
D := NewGraphNode(7)
C.AddNeighbor(D)
D.AddNeighbor(C)
clusterC.AddRoot(C)
clusterC.AddRoot(D)
// }
clusterA.AddRoot(clusterC)
// }
// subgraph clusterB {
clusterB := NewGraphNode(8)
// d -- f
d := NewGraphNode(9)
f := NewGraphNode(10)
d.AddNeighbor(f)
f.AddNeighbor(d)
clusterB.AddRoot(d)
clusterB.AddRoot(f)
// }
// d -- D
d.AddNeighbor(D)
D.AddNeighbor(d)
// e -- clusterB
e.AddNeighbor(clusterB)
clusterB.AddNeighbor(e)
// clusterC -- clusterB
clusterC.AddNeighbor(clusterB)
clusterB.AddNeighbor(clusterC)
G.AddRoot(e)
G.AddRoot(clusterA)
G.AddRoot(clusterB)
// }
if !topo.IsPathIn(G, []graph.Node{C, D, d, f}) {
fmt.Println("Not working!")
} else {
fmt.Println("Working!")
}
}

View File

@@ -0,0 +1,9 @@
# formats/dot
## License
The source code and any original content of the formats/dot directory is released under [Public Domain Dedication](https://creativecommons.org/publicdomain/zero/1.0/).
The source code is also licensed under the gonum license, and users are free to choice the license which suits their needs.
Please see github.com/gonum/license for general license information, contributors, authors, etc on the Gonum suite of packages.

View File

@@ -0,0 +1,408 @@
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
// Package ast declares the types used to represent abstract syntax trees of
// Graphviz DOT graphs.
package ast
import (
"bytes"
"fmt"
)
// === [ File ] ================================================================
// A File represents a DOT file.
//
// Examples.
//
// digraph G {
// A -> B
// }
// graph H {
// C - D
// }
type File struct {
// Graphs.
Graphs []*Graph
}
// String returns the string representation of the file.
func (f *File) String() string {
buf := new(bytes.Buffer)
for i, graph := range f.Graphs {
if i != 0 {
buf.WriteString("\n")
}
buf.WriteString(graph.String())
}
return buf.String()
}
// === [ Graphs ] ==============================================================
// A Graph represents a directed or an undirected graph.
//
// Examples.
//
// digraph G {
// A -> {B C}
// B -> C
// }
type Graph struct {
// Strict graph; multi-edges forbidden.
Strict bool
// Directed graph.
Directed bool
// Graph ID; or empty if anonymous.
ID string
// Graph statements.
Stmts []Stmt
}
// String returns the string representation of the graph.
func (g *Graph) String() string {
buf := new(bytes.Buffer)
if g.Strict {
buf.WriteString("strict ")
}
if g.Directed {
buf.WriteString("digraph ")
} else {
buf.WriteString("graph ")
}
if len(g.ID) > 0 {
fmt.Fprintf(buf, "%s ", g.ID)
}
buf.WriteString("{\n")
for _, stmt := range g.Stmts {
fmt.Fprintf(buf, "\t%s\n", stmt)
}
buf.WriteString("}")
return buf.String()
}
// === [ Statements ] ==========================================================
// A Stmt represents a statement, and has one of the following underlying types.
//
// *NodeStmt
// *EdgeStmt
// *AttrStmt
// *Attr
// *Subgraph
type Stmt interface {
fmt.Stringer
// isStmt ensures that only statements can be assigned to the Stmt interface.
isStmt()
}
// --- [ Node statement ] ------------------------------------------------------
// A NodeStmt represents a node statement.
//
// Examples.
//
// A [color=blue]
type NodeStmt struct {
// Node.
Node *Node
// Node attributes.
Attrs []*Attr
}
// String returns the string representation of the node statement.
func (e *NodeStmt) String() string {
buf := new(bytes.Buffer)
buf.WriteString(e.Node.String())
if len(e.Attrs) > 0 {
buf.WriteString(" [")
for i, attr := range e.Attrs {
if i != 0 {
buf.WriteString(" ")
}
buf.WriteString(attr.String())
}
buf.WriteString("]")
}
return buf.String()
}
// --- [ Edge statement ] ------------------------------------------------------
// An EdgeStmt represents an edge statement.
//
// Examples.
//
// A -> B
// A -> {B C}
// A -> B -> C
type EdgeStmt struct {
// Source vertex.
From Vertex
// Outgoing edge.
To *Edge
// Edge attributes.
Attrs []*Attr
}
// String returns the string representation of the edge statement.
func (e *EdgeStmt) String() string {
buf := new(bytes.Buffer)
fmt.Fprintf(buf, "%s %s", e.From, e.To)
if len(e.Attrs) > 0 {
buf.WriteString(" [")
for i, attr := range e.Attrs {
if i != 0 {
buf.WriteString(" ")
}
buf.WriteString(attr.String())
}
buf.WriteString("]")
}
return buf.String()
}
// An Edge represents an edge between two vertices.
type Edge struct {
// Directed edge.
Directed bool
// Destination vertex.
Vertex Vertex
// Outgoing edge; or nil if none.
To *Edge
}
// String returns the string representation of the edge.
func (e *Edge) String() string {
op := "--"
if e.Directed {
op = "->"
}
if e.To != nil {
return fmt.Sprintf("%s %s %s", op, e.Vertex, e.To)
}
return fmt.Sprintf("%s %s", op, e.Vertex)
}
// --- [ Attribute statement ] -------------------------------------------------
// An AttrStmt represents an attribute statement.
//
// Examples.
//
// graph [rankdir=LR]
// node [color=blue fillcolor=red]
// edge [minlen=1]
type AttrStmt struct {
// Graph component kind to which the attributes are assigned.
Kind Kind
// Attributes.
Attrs []*Attr
}
// String returns the string representation of the attribute statement.
func (a *AttrStmt) String() string {
buf := new(bytes.Buffer)
fmt.Fprintf(buf, "%s [", a.Kind)
for i, attr := range a.Attrs {
if i != 0 {
buf.WriteString(" ")
}
buf.WriteString(attr.String())
}
buf.WriteString("]")
return buf.String()
}
// Kind specifies the set of graph components to which attribute statements may
// be assigned.
type Kind uint
// Graph component kinds.
const (
KindGraph Kind = iota // graph
KindNode // node
KindEdge // edge
)
// String returns the string representation of the graph component kind.
func (k Kind) String() string {
switch k {
case KindGraph:
return "graph"
case KindNode:
return "node"
case KindEdge:
return "edge"
}
panic(fmt.Sprintf("invalid graph component kind (%d)", uint(k)))
}
// --- [ Attribute ] -----------------------------------------------------------
// An Attr represents an attribute.
//
// Examples.
//
// rank=same
type Attr struct {
// Attribute key.
Key string
// Attribute value.
Val string
}
// String returns the string representation of the attribute.
func (a *Attr) String() string {
return fmt.Sprintf("%s=%s", a.Key, a.Val)
}
// --- [ Subgraph ] ------------------------------------------------------------
// A Subgraph represents a subgraph vertex.
//
// Examples.
//
// subgraph S {A B C}
type Subgraph struct {
// Subgraph ID; or empty if none.
ID string
// Subgraph statements.
Stmts []Stmt
}
// String returns the string representation of the subgraph.
func (s *Subgraph) String() string {
buf := new(bytes.Buffer)
if len(s.ID) > 0 {
fmt.Fprintf(buf, "subgraph %s ", s.ID)
}
buf.WriteString("{")
for i, stmt := range s.Stmts {
if i != 0 {
buf.WriteString(" ")
}
buf.WriteString(stmt.String())
}
buf.WriteString("}")
return buf.String()
}
// isStmt ensures that only statements can be assigned to the Stmt interface.
func (*NodeStmt) isStmt() {}
func (*EdgeStmt) isStmt() {}
func (*AttrStmt) isStmt() {}
func (*Attr) isStmt() {}
func (*Subgraph) isStmt() {}
// === [ Vertices ] ============================================================
// A Vertex represents a vertex, and has one of the following underlying types.
//
// *Node
// *Subgraph
type Vertex interface {
fmt.Stringer
// isVertex ensures that only vertices can be assigned to the Vertex
// interface.
isVertex()
}
// --- [ Node identifier ] -----------------------------------------------------
// A Node represents a node vertex.
//
// Examples.
//
// A
// A:nw
type Node struct {
// Node ID.
ID string
// Node port; or nil if none.
Port *Port
}
// String returns the string representation of the node.
func (n *Node) String() string {
if n.Port != nil {
return fmt.Sprintf("%s%s", n.ID, n.Port)
}
return n.ID
}
// A Port specifies where on a node an edge should be aimed.
type Port struct {
// Port ID; or empty if none.
ID string
// Compass point.
CompassPoint CompassPoint
}
// String returns the string representation of the port.
func (p *Port) String() string {
buf := new(bytes.Buffer)
if len(p.ID) > 0 {
fmt.Fprintf(buf, ":%s", p.ID)
}
if p.CompassPoint != CompassPointDefault {
fmt.Fprintf(buf, ":%s", p.CompassPoint)
}
return buf.String()
}
// CompassPoint specifies the set of compass points.
type CompassPoint uint
// Compass points.
const (
CompassPointDefault CompassPoint = iota // _
CompassPointNorth // n
CompassPointNorthEast // ne
CompassPointEast // e
CompassPointSouthEast // se
CompassPointSouth // s
CompassPointSouthWest // sw
CompassPointWest // w
CompassPointNorthWest // nw
CompassPointCenter // c
)
// String returns the string representation of the compass point.
func (c CompassPoint) String() string {
switch c {
case CompassPointDefault:
return "_"
case CompassPointNorth:
return "n"
case CompassPointNorthEast:
return "ne"
case CompassPointEast:
return "e"
case CompassPointSouthEast:
return "se"
case CompassPointSouth:
return "s"
case CompassPointSouthWest:
return "sw"
case CompassPointWest:
return "w"
case CompassPointNorthWest:
return "nw"
case CompassPointCenter:
return "c"
}
panic(fmt.Sprintf("invalid compass point (%d)", uint(c)))
}
// isVertex ensures that only vertices can be assigned to the Vertex interface.
func (*Node) isVertex() {}
func (*Subgraph) isVertex() {}

View File

@@ -0,0 +1,101 @@
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
package ast_test
import (
"bytes"
"io/ioutil"
"testing"
"github.com/gonum/graph/formats/dot"
"github.com/gonum/graph/formats/dot/ast"
)
func TestParseFile(t *testing.T) {
golden := []struct {
in string
out string
}{
{in: "../internal/testdata/empty.dot"},
{in: "../internal/testdata/graph.dot"},
{in: "../internal/testdata/digraph.dot"},
{in: "../internal/testdata/strict.dot"},
{in: "../internal/testdata/multi.dot"},
{in: "../internal/testdata/named_graph.dot"},
{in: "../internal/testdata/node_stmt.dot"},
{in: "../internal/testdata/edge_stmt.dot"},
{in: "../internal/testdata/attr_stmt.dot"},
{in: "../internal/testdata/attr.dot"},
{
in: "../internal/testdata/subgraph.dot",
out: "../internal/testdata/subgraph.golden",
},
{
in: "../internal/testdata/semi.dot",
out: "../internal/testdata/semi.golden",
},
{
in: "../internal/testdata/empty_attr.dot",
out: "../internal/testdata/empty_attr.golden",
},
{
in: "../internal/testdata/attr_lists.dot",
out: "../internal/testdata/attr_lists.golden",
},
{
in: "../internal/testdata/attr_sep.dot",
out: "../internal/testdata/attr_sep.golden",
},
{in: "../internal/testdata/subgraph_vertex.dot"},
{
in: "../internal/testdata/port.dot",
out: "../internal/testdata/port.golden",
},
}
for _, g := range golden {
file, err := dot.ParseFile(g.in)
if err != nil {
t.Errorf("%q: unable to parse file; %v", g.in, err)
continue
}
// If no output path is specified, the input is already golden.
out := g.in
if len(g.out) > 0 {
out = g.out
}
buf, err := ioutil.ReadFile(out)
if err != nil {
t.Errorf("%q: unable to read file; %v", g.in, err)
continue
}
got := file.String()
// Remove trailing newline.
want := string(bytes.TrimSpace(buf))
if got != want {
t.Errorf("%q: graph mismatch; expected %q, got %q", g.in, want, got)
}
}
}
// Verify that all statements implement the Stmt interface.
var (
_ ast.Stmt = (*ast.NodeStmt)(nil)
_ ast.Stmt = (*ast.EdgeStmt)(nil)
_ ast.Stmt = (*ast.AttrStmt)(nil)
_ ast.Stmt = (*ast.Attr)(nil)
_ ast.Stmt = (*ast.Subgraph)(nil)
)
// Verify that all vertices implement the Vertex interface.
var (
_ ast.Vertex = (*ast.Node)(nil)
_ ast.Vertex = (*ast.Subgraph)(nil)
)

63
graph/formats/dot/dot.go Normal file
View File

@@ -0,0 +1,63 @@
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
// Package dot implements a parser for Graphviz DOT files.
package dot
import (
"fmt"
"io"
"io/ioutil"
"github.com/gonum/graph/formats/dot/ast"
"github.com/gonum/graph/formats/dot/internal/lexer"
"github.com/gonum/graph/formats/dot/internal/parser"
)
// ParseFile parses the given Graphviz DOT file into an AST.
func ParseFile(path string) (*ast.File, error) {
buf, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return ParseBytes(buf)
}
// Parse parses the given Graphviz DOT file into an AST, reading from r.
func Parse(r io.Reader) (*ast.File, error) {
buf, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
return ParseBytes(buf)
}
// ParseBytes parses the given Graphviz DOT file into an AST, reading from b.
func ParseBytes(b []byte) (*ast.File, error) {
l := lexer.NewLexer(b)
p := parser.NewParser()
file, err := p.Parse(l)
if err != nil {
return nil, err
}
f, ok := file.(*ast.File)
if !ok {
return nil, fmt.Errorf("invalid file type; expected *ast.File, got %T", file)
}
if err := check(f); err != nil {
return nil, err
}
return f, nil
}
// ParseString parses the given Graphviz DOT file into an AST, reading from s.
func ParseString(s string) (*ast.File, error) {
return ParseBytes([]byte(s))
}

View File

@@ -0,0 +1,37 @@
gen: dot.bnf
gocc $<
# TODO: Remove once https://github.com/goccmack/gocc/issues/36 gets resolved.
./paste_copyright.bash
find . -type f -name '*.go' | xargs goimports -w
debug_lexer: dot.bnf
gocc -debug_lexer -v -a $<
# TODO: Remove once https://github.com/goccmack/gocc/issues/36 gets resolved.
find . -type f -name '*.go' | xargs goimports -w
debug_parser: dot.bnf
gocc -debug_parser -v -a $<
# TODO: Remove once https://github.com/goccmack/gocc/issues/36 gets resolved.
find . -type f -name '*.go' | xargs goimports -w
clean:
rm -f errors/errors.go
rm -f lexer/acttab.go
rm -f lexer/lexer.go
rm -f lexer/transitiontable.go
rm -f parser/action.go
rm -f parser/actiontable.go
rm -f parser/gototable.go
rm -f parser/parser.go
rm -f parser/productionstable.go
rm -f token/token.go
rm -f util/litconv.go
rm -f util/rune.go
-rmdir --ignore-fail-on-non-empty errors
-rmdir --ignore-fail-on-non-empty lexer
-rmdir --ignore-fail-on-non-empty parser
-rmdir --ignore-fail-on-non-empty token
-rmdir --ignore-fail-on-non-empty util
rm -f terminals.txt LR1_conflicts.txt LR1_sets.txt first.txt lexer_sets.txt
.PHONY: gen debug_lexer debug_parser clean

View File

@@ -0,0 +1,328 @@
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
// Package astx implements utility functions for generating abstract syntax
// trees of Graphviz DOT graphs.
package astx
import (
"fmt"
"strings"
"github.com/gonum/graph/formats/dot/ast"
"github.com/gonum/graph/formats/dot/internal/token"
)
// === [ File ] ================================================================
// NewFile returns a new file based on the given graph.
func NewFile(graph interface{}) (*ast.File, error) {
g, ok := graph.(*ast.Graph)
if !ok {
return nil, fmt.Errorf("invalid graph type; expected *ast.Graph, got %T", graph)
}
return &ast.File{Graphs: []*ast.Graph{g}}, nil
}
// AppendGraph appends graph to the given file.
func AppendGraph(file, graph interface{}) (*ast.File, error) {
f, ok := file.(*ast.File)
if !ok {
return nil, fmt.Errorf("invalid file type; expected *ast.File, got %T", file)
}
g, ok := graph.(*ast.Graph)
if !ok {
return nil, fmt.Errorf("invalid graph type; expected *ast.Graph, got %T", graph)
}
f.Graphs = append(f.Graphs, g)
return f, nil
}
// === [ Graphs ] ==============================================================
// NewGraph returns a new graph based on the given graph strictness, direction,
// optional ID and optional statements.
func NewGraph(strict, directed, optID, optStmts interface{}) (*ast.Graph, error) {
s, ok := strict.(bool)
if !ok {
return nil, fmt.Errorf("invalid strictness type; expected bool, got %T", strict)
}
d, ok := directed.(bool)
if !ok {
return nil, fmt.Errorf("invalid direction type; expected bool, got %T", directed)
}
id, ok := optID.(string)
if optID != nil && !ok {
return nil, fmt.Errorf("invalid ID type; expected string or nil, got %T", optID)
}
stmts, ok := optStmts.([]ast.Stmt)
if optStmts != nil && !ok {
return nil, fmt.Errorf("invalid statements type; expected []ast.Stmt or nil, got %T", optStmts)
}
return &ast.Graph{Strict: s, Directed: d, ID: id, Stmts: stmts}, nil
}
// === [ Statements ] ==========================================================
// NewStmtList returns a new statement list based on the given statement.
func NewStmtList(stmt interface{}) ([]ast.Stmt, error) {
s, ok := stmt.(ast.Stmt)
if !ok {
return nil, fmt.Errorf("invalid statement type; expected ast.Stmt, got %T", stmt)
}
return []ast.Stmt{s}, nil
}
// AppendStmt appends stmt to the given statement list.
func AppendStmt(list, stmt interface{}) ([]ast.Stmt, error) {
l, ok := list.([]ast.Stmt)
if !ok {
return nil, fmt.Errorf("invalid statement list type; expected []ast.Stmt, got %T", list)
}
s, ok := stmt.(ast.Stmt)
if !ok {
return nil, fmt.Errorf("invalid statement type; expected ast.Stmt, got %T", stmt)
}
return append(l, s), nil
}
// --- [ Node statement ] ------------------------------------------------------
// NewNodeStmt returns a new node statement based on the given node and optional
// attributes.
func NewNodeStmt(node, optAttrs interface{}) (*ast.NodeStmt, error) {
n, ok := node.(*ast.Node)
if !ok {
return nil, fmt.Errorf("invalid node type; expected *ast.Node, got %T", node)
}
attrs, ok := optAttrs.([]*ast.Attr)
if optAttrs != nil && !ok {
return nil, fmt.Errorf("invalid attributes type; expected []*ast.Attr or nil, got %T", optAttrs)
}
return &ast.NodeStmt{Node: n, Attrs: attrs}, nil
}
// --- [ Edge statement ] ------------------------------------------------------
// NewEdgeStmt returns a new edge statement based on the given source vertex,
// outgoing edge and optional attributes.
func NewEdgeStmt(from, to, optAttrs interface{}) (*ast.EdgeStmt, error) {
f, ok := from.(ast.Vertex)
if !ok {
return nil, fmt.Errorf("invalid source vertex type; expected ast.Vertex, got %T", from)
}
t, ok := to.(*ast.Edge)
if !ok {
return nil, fmt.Errorf("invalid outgoing edge type; expected *ast.Edge, got %T", to)
}
attrs, ok := optAttrs.([]*ast.Attr)
if optAttrs != nil && !ok {
return nil, fmt.Errorf("invalid attributes type; expected []*ast.Attr or nil, got %T", optAttrs)
}
return &ast.EdgeStmt{From: f, To: t, Attrs: attrs}, nil
}
// NewEdge returns a new edge based on the given edge direction, destination
// vertex and optional outgoing edge.
func NewEdge(directed, vertex, optTo interface{}) (*ast.Edge, error) {
d, ok := directed.(bool)
if !ok {
return nil, fmt.Errorf("invalid direction type; expected bool, got %T", directed)
}
v, ok := vertex.(ast.Vertex)
if !ok {
return nil, fmt.Errorf("invalid destination vertex type; expected ast.Vertex, got %T", vertex)
}
to, ok := optTo.(*ast.Edge)
if optTo != nil && !ok {
return nil, fmt.Errorf("invalid outgoing edge type; expected *ast.Edge or nil, got %T", optTo)
}
return &ast.Edge{Directed: d, Vertex: v, To: to}, nil
}
// --- [ Attribute statement ] -------------------------------------------------
// NewAttrStmt returns a new attribute statement based on the given graph
// component kind and attributes.
func NewAttrStmt(kind, optAttrs interface{}) (*ast.AttrStmt, error) {
k, ok := kind.(ast.Kind)
if !ok {
return nil, fmt.Errorf("invalid graph component kind type; expected ast.Kind, got %T", kind)
}
attrs, ok := optAttrs.([]*ast.Attr)
if optAttrs != nil && !ok {
return nil, fmt.Errorf("invalid attributes type; expected []*ast.Attr or nil, got %T", optAttrs)
}
return &ast.AttrStmt{Kind: k, Attrs: attrs}, nil
}
// NewAttrList returns a new attribute list based on the given attribute.
func NewAttrList(attr interface{}) ([]*ast.Attr, error) {
a, ok := attr.(*ast.Attr)
if !ok {
return nil, fmt.Errorf("invalid attribute type; expected *ast.Attr, got %T", attr)
}
return []*ast.Attr{a}, nil
}
// AppendAttr appends attr to the given attribute list.
func AppendAttr(list, attr interface{}) ([]*ast.Attr, error) {
l, ok := list.([]*ast.Attr)
if !ok {
return nil, fmt.Errorf("invalid attribute list type; expected []*ast.Attr, got %T", list)
}
a, ok := attr.(*ast.Attr)
if !ok {
return nil, fmt.Errorf("invalid attribute type; expected *ast.Attr, got %T", attr)
}
return append(l, a), nil
}
// AppendAttrList appends the optional attrs to the given optional attribute
// list.
func AppendAttrList(optList, optAttrs interface{}) ([]*ast.Attr, error) {
list, ok := optList.([]*ast.Attr)
if optList != nil && !ok {
return nil, fmt.Errorf("invalid attribute list type; expected []*ast.Attr or nil, got %T", optList)
}
attrs, ok := optAttrs.([]*ast.Attr)
if optAttrs != nil && !ok {
return nil, fmt.Errorf("invalid attributes type; expected []*ast.Attr or nil, got %T", optAttrs)
}
return append(list, attrs...), nil
}
// --- [ Attribute ] -----------------------------------------------------------
// NewAttr returns a new attribute based on the given key-value pair.
func NewAttr(key, val interface{}) (*ast.Attr, error) {
k, ok := key.(string)
if !ok {
return nil, fmt.Errorf("invalid key type; expected string, got %T", key)
}
v, ok := val.(string)
if !ok {
return nil, fmt.Errorf("invalid value type; expected string, got %T", val)
}
return &ast.Attr{Key: k, Val: v}, nil
}
// --- [ Subgraph ] ------------------------------------------------------------
// NewSubgraph returns a new subgraph based on the given optional subgraph ID
// and optional statements.
func NewSubgraph(optID, optStmts interface{}) (*ast.Subgraph, error) {
id, ok := optID.(string)
if optID != nil && !ok {
return nil, fmt.Errorf("invalid ID type; expected string or nil, got %T", optID)
}
stmts, ok := optStmts.([]ast.Stmt)
if optStmts != nil && !ok {
return nil, fmt.Errorf("invalid statements type; expected []ast.Stmt or nil, got %T", optStmts)
}
return &ast.Subgraph{ID: id, Stmts: stmts}, nil
}
// === [ Vertices ] ============================================================
// --- [ Node identifier ] -----------------------------------------------------
// NewNode returns a new node based on the given node id and optional port.
func NewNode(id, optPort interface{}) (*ast.Node, error) {
i, ok := id.(string)
if !ok {
return nil, fmt.Errorf("invalid ID type; expected string, got %T", id)
}
port, ok := optPort.(*ast.Port)
if optPort != nil && !ok {
return nil, fmt.Errorf("invalid port type; expected *ast.Port or nil, got %T", optPort)
}
return &ast.Node{ID: i, Port: port}, nil
}
// NewPort returns a new port based on the given id and optional compass point.
func NewPort(id, optCompassPoint interface{}) (*ast.Port, error) {
// Note, if optCompassPoint is nil, id may be either an identifier or a
// compass point.
//
// The following strings are valid compass points:
//
// "n", "ne", "e", "se", "s", "sw", "w", "nw", "c" and "_"
i, ok := id.(string)
if !ok {
return nil, fmt.Errorf("invalid ID type; expected string, got %T", id)
}
// Early return if optional compass point is absent and ID is a valid compass
// point.
if optCompassPoint == nil {
if compassPoint, ok := getCompassPoint(i); ok {
return &ast.Port{CompassPoint: compassPoint}, nil
}
}
c, ok := optCompassPoint.(string)
if optCompassPoint != nil && !ok {
return nil, fmt.Errorf("invalid compass point type; expected string or nil, got %T", optCompassPoint)
}
compassPoint, _ := getCompassPoint(c)
return &ast.Port{ID: i, CompassPoint: compassPoint}, nil
}
// getCompassPoint returns the corresponding compass point to the given string,
// and a boolean value indicating if such a compass point exists.
func getCompassPoint(s string) (ast.CompassPoint, bool) {
switch s {
case "_":
return ast.CompassPointDefault, true
case "n":
return ast.CompassPointNorth, true
case "ne":
return ast.CompassPointNorthEast, true
case "e":
return ast.CompassPointEast, true
case "se":
return ast.CompassPointSouthEast, true
case "s":
return ast.CompassPointSouth, true
case "sw":
return ast.CompassPointSouthWest, true
case "w":
return ast.CompassPointWest, true
case "nw":
return ast.CompassPointNorthWest, true
case "c":
return ast.CompassPointCenter, true
}
return ast.CompassPointDefault, false
}
// === [ Identifiers ] =========================================================
// NewID returns a new identifier based on the given ID token.
func NewID(id interface{}) (string, error) {
i, ok := id.(*token.Token)
if !ok {
return "", fmt.Errorf("invalid identifier type; expected *token.Token, got %T", id)
}
s := string(i.Lit)
// As another aid for readability, dot allows double-quoted strings to span
// multiple physical lines using the standard C convention of a backslash
// immediately preceding a newline character.
if strings.HasPrefix(s, `"`) && strings.HasSuffix(s, `"`) {
// Strip "\\\n" sequences.
s = strings.Replace(s, "\\\n", "", -1)
}
// TODO: Add support for concatenated using a '+' operator.
return s, nil
}

View File

@@ -0,0 +1,90 @@
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
package astx_test
import (
"bytes"
"io/ioutil"
"testing"
"github.com/gonum/graph/formats/dot"
)
func TestParseFile(t *testing.T) {
golden := []struct {
in string
out string
}{
{in: "../testdata/empty.dot"},
{in: "../testdata/graph.dot"},
{in: "../testdata/digraph.dot"},
{in: "../testdata/strict.dot"},
{in: "../testdata/multi.dot"},
{in: "../testdata/named_graph.dot"},
{in: "../testdata/node_stmt.dot"},
{in: "../testdata/edge_stmt.dot"},
{in: "../testdata/attr_stmt.dot"},
{in: "../testdata/attr.dot"},
{
in: "../testdata/subgraph.dot",
out: "../testdata/subgraph.golden",
},
{
in: "../testdata/semi.dot",
out: "../testdata/semi.golden",
},
{
in: "../testdata/empty_attr.dot",
out: "../testdata/empty_attr.golden",
},
{
in: "../testdata/attr_lists.dot",
out: "../testdata/attr_lists.golden",
},
{
in: "../testdata/attr_sep.dot",
out: "../testdata/attr_sep.golden",
},
{in: "../testdata/subgraph_vertex.dot"},
{
in: "../testdata/port.dot",
out: "../testdata/port.golden",
},
{in: "../testdata/quoted_id.dot"},
{
in: "../testdata/backslash_newline_id.dot",
out: "../testdata/backslash_newline_id.golden",
},
}
for _, g := range golden {
file, err := dot.ParseFile(g.in)
if err != nil {
t.Errorf("%q: unable to parse file; %v", g.in, err)
continue
}
// If no output path is specified, the input is already golden.
out := g.in
if len(g.out) > 0 {
out = g.out
}
buf, err := ioutil.ReadFile(out)
if err != nil {
t.Errorf("%q: unable to read file; %v", g.in, err)
continue
}
got := file.String()
// Remove trailing newline.
want := string(bytes.TrimSpace(buf))
if got != want {
t.Errorf("%q: graph mismatch; expected `%s`, got `%s`", g.in, want, got)
}
}
}

View File

@@ -0,0 +1,358 @@
// The DOT Language
//
// http://www.graphviz.org/doc/info/lang.html
// ### [ Tokens ] ##############################################################
// The keywords node, edge, graph, digraph, subgraph, and strict are case-
// independent.
node
: 'n' 'o' 'd' 'e'
| 'N' 'o' 'd' 'e'
| 'N' 'O' 'D' 'E'
;
edge
: 'e' 'd' 'g' 'e'
| 'E' 'd' 'g' 'e'
| 'E' 'D' 'G' 'E'
;
// TODO: Rename graphx to graph once gocc#20 is fixed [1].
//
// [1]: https://github.com/goccmack/gocc/issues/20
graphx
: 'g' 'r' 'a' 'p' 'h'
| 'G' 'r' 'a' 'p' 'h'
| 'G' 'R' 'A' 'P' 'H'
;
digraph
: 'd' 'i' 'g' 'r' 'a' 'p' 'h'
| 'D' 'i' 'g' 'r' 'a' 'p' 'h'
| 'd' 'i' 'G' 'r' 'a' 'p' 'h'
| 'D' 'i' 'G' 'r' 'a' 'p' 'h'
| 'D' 'I' 'G' 'R' 'A' 'P' 'H'
;
subgraph
: 's' 'u' 'b' 'g' 'r' 'a' 'p' 'h'
| 'S' 'u' 'b' 'g' 'r' 'a' 'p' 'h'
| 's' 'u' 'b' 'G' 'r' 'a' 'p' 'h'
| 'S' 'u' 'b' 'G' 'r' 'a' 'p' 'h'
| 'S' 'U' 'B' 'G' 'R' 'A' 'P' 'H'
;
strict
: 's' 't' 'r' 'i' 'c' 't'
| 'S' 't' 'r' 'i' 'c' 't'
| 'S' 'T' 'R' 'I' 'C' 'T'
;
// An arbitrary ASCII character except null (0x00), double quote (0x22) and
// backslash (0x5C).
_ascii_char
// skip null (0x00)
: '\x01' - '\x21'
// skip double quote (0x22)
| '\x23' - '\x5B'
// skip backslash (0x5C)
| '\x5D' - '\x7F'
;
_ascii_letter
: 'a' - 'z'
| 'A' - 'Z'
;
_ascii_digit : '0' - '9' ;
_unicode_char
: _ascii_char
| _unicode_byte
;
_unicode_byte
: '\u0080' - '\uFFFC'
// skip invalid code point (\uFFFD)
| '\uFFFE' - '\U0010FFFF'
;
_letter : _ascii_letter | _unicode_byte | '_' ;
_decimal_digit : _ascii_digit ;
_decimals : _decimal_digit { _decimal_digit } ;
// An ID is one of the following:
//
// 1) Any string of alphabetic ([a-zA-Z\200-\377]) characters, underscores
// ('_') or digits ([0-9]), not beginning with a digit;
//
// 2) a numeral [-]?(.[0-9]+ | [0-9]+(.[0-9]*)? );
//
// 3) any double-quoted string ("...") possibly containing escaped quotes
// (\");
//
// 4) an HTML string (<...>).
id
: _letter { _letter | _decimal_digit }
| _int_lit
| _string_lit
| _html_lit
;
_int_lit
: [ '-' ] '.' _decimals
| [ '-' ] _decimals [ '.' { _decimal_digit } ]
;
// In quoted strings in DOT, the only escaped character is double-quote (").
// That is, in quoted strings, the dyad \" is converted to "; all other
// characters are left unchanged. In particular, \\ remains \\.
// As another aid for readability, dot allows double-quoted strings to span
// multiple physical lines using the standard C convention of a backslash
// immediately preceding a newline character.
// In addition, double-quoted strings can be concatenated using a '+' operator.
_escaped_char : '\\' ( _unicode_char | '"' | '\\' ) ;
_char : _unicode_char | _escaped_char ;
_string_lit : '"' { _char } '"' ;
// An arbitrary HTML character except null (0x00), left angle bracket (0x3C) and
// right angle bracket (0x3E).
_html_char
// skip null (0x00)
: '\x01' - '\x3B'
// skip left angle bracket (0x3C)
| '\x3D'
// skip right angle bracket (0x3E)
| '\x3F' - '\xFF'
;
_html_chars : { _html_char } ;
_html_tag : '<' _html_chars '>' ;
_html_lit : '<' { _html_chars | _html_tag } '>' ;
// The language supports C++-style comments: /* */ and //. In addition, a line
// beginning with a '#' character is considered a line output from a C
// preprocessor (e.g., # 34 to indicate line 34 ) and discarded.
_line_comment
: '/' '/' { . } '\n'
| '#' { . } '\n'
;
_block_comment : '/' '*' { . | '*' } '*' '/' ;
!comment : _line_comment | _block_comment ;
!whitespace : ' ' | '\t' | '\r' | '\n' ;
// ### [ Syntax ] ##############################################################
<< import (
"github.com/gonum/graph/formats/dot/ast"
"github.com/gonum/graph/formats/dot/internal/astx"
) >>
// === [ Files ] ===============================================================
File
: Graph << astx.NewFile($0) >>
| File Graph << astx.AppendGraph($0, $1) >>
;
// === [ Graphs ] ==============================================================
// Graph : [ "strict" ] ( "graph" | "digraph" ) [ ID ] "{" [ StmtList ] "}"
Graph
: OptStrict DirectedGraph OptID
"{" OptStmtList "}" << astx.NewGraph($0, $1, $2, $4) >>
;
OptStrict
: empty << false, nil >>
| strict << true, nil >>
;
DirectedGraph
: graphx << false, nil >>
| digraph << true, nil >>
;
// === [ Statements ] ==========================================================
// StmtList
// : Stmt [ ";" ]
// | StmtList Stmt [ ";" ]
StmtList
: Stmt OptSemi << astx.NewStmtList($0) >>
| StmtList Stmt OptSemi << astx.AppendStmt($0, $1) >>
;
OptStmtList
: empty
| StmtList
;
Stmt
: NodeStmt
| EdgeStmt
| AttrStmt
| Attr
| Subgraph
;
OptSemi
: empty
| ";"
;
// --- [ Node statement ] ------------------------------------------------------
// NodeStmt : Node [ AttrList ]
NodeStmt
: Node OptAttrList << astx.NewNodeStmt($0, $1) >>
;
// --- [ Edge statement ] ------------------------------------------------------
// EdgeStmt : ( Node | Subgraph ) Edge [ AttrList ]
EdgeStmt
: Vertex Edge OptAttrList << astx.NewEdgeStmt($0, $1, $2) >>
;
// Edge : ( "--" | "-->" ) ( Node | Subgraph ) [ Edge ]
Edge
: DirectedEdge Vertex OptEdge << astx.NewEdge($0, $1, $2) >>
;
DirectedEdge
: "--" << false, nil >>
| "->" << true, nil >>
;
OptEdge
: empty
| Edge
;
// --- [ Attribute statement ] -------------------------------------------------
// AttrStmt : ( "graph" | "node" | "edge" ) AttrList
AttrStmt
: Component AttrList << astx.NewAttrStmt($0, $1) >>
;
Component
: graphx << ast.KindGraph, nil >>
| node << ast.KindNode, nil >>
| edge << ast.KindEdge, nil >>
;
// AttrList : "[" [ AList ] "]" [ AttrList ]
AttrList
: "[" OptAList "]" << $1, nil >>
| AttrList "[" OptAList "]" << astx.AppendAttrList($0, $2) >>
;
OptAttrList
: empty
| AttrList
;
// AList
// : Attr [ ( ";" | "," ) ]
// | AList Attr [ ( ";" | "," ) ]
AList
: Attr OptSep << astx.NewAttrList($0) >>
| AList Attr OptSep << astx.AppendAttr($0, $1) >>
;
OptAList
: empty
| AList
;
OptSep
: empty
| ";"
| ","
;
// --- [ Attribute ] -----------------------------------------------------------
Attr
: ID "=" ID << astx.NewAttr($0, $2) >>
;
// --- [ Subgraph ] ------------------------------------------------------------
// Subgraph : [ "subgraph" [ ID ] ] "{" [ StmtList ] "}"
Subgraph
: OptSubgraphID "{" OptStmtList "}" << astx.NewSubgraph($0, $2) >>
;
OptSubgraphID
: empty
| subgraph OptID << $1, nil >>
;
// === [ Vertices ] ============================================================
Vertex
: Node
| Subgraph
;
// --- [ Node identifier ] -----------------------------------------------------
// Node : ID [ Port ]
Node
: ID OptPort << astx.NewNode($0, $1) >>
;
// Port
// : ":" ID [ ":" CompassPoint ]
// | ":" CompassPoint
//
// CompassPoint
// : "n" | "ne" | "e" | "se" | "s" | "sw" | "w" | "nw" | "c" | "_"
// Note also that the allowed compass point values are not keywords, so these
// strings can be used elsewhere as ordinary identifiers and, conversely, the
// parser will actually accept any identifier.
Port
: ":" ID << astx.NewPort($1, nil) >>
| ":" ID ":" ID << astx.NewPort($1, $3) >>
;
OptPort
: empty
| Port
;
// === [ Identifiers ] =========================================================
ID
: id << astx.NewID($0) >>
;
OptID
: empty << "", nil >>
| ID
;

View File

@@ -0,0 +1,66 @@
// Code generated by gocc; DO NOT EDIT.
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
package errors
import (
"bytes"
"fmt"
"github.com/gonum/graph/formats/dot/internal/token"
)
type ErrorSymbol interface {
}
type Error struct {
Err error
ErrorToken *token.Token
ErrorSymbols []ErrorSymbol
ExpectedTokens []string
StackTop int
}
func (E *Error) String() string {
w := new(bytes.Buffer)
fmt.Fprintf(w, "Error")
if E.Err != nil {
fmt.Fprintf(w, " %s\n", E.Err)
} else {
fmt.Fprintf(w, "\n")
}
fmt.Fprintf(w, "Token: type=%d, lit=%s\n", E.ErrorToken.Type, E.ErrorToken.Lit)
fmt.Fprintf(w, "Pos: offset=%d, line=%d, column=%d\n", E.ErrorToken.Pos.Offset, E.ErrorToken.Pos.Line, E.ErrorToken.Pos.Column)
fmt.Fprintf(w, "Expected one of: ")
for _, sym := range E.ExpectedTokens {
fmt.Fprintf(w, "%s ", sym)
}
fmt.Fprintf(w, "ErrorSymbol:\n")
for _, sym := range E.ErrorSymbols {
fmt.Fprintf(w, "%v\n", sym)
}
return w.String()
}
func (e *Error) Error() string {
w := new(bytes.Buffer)
fmt.Fprintf(w, "Error in S%d: %s, %s", e.StackTop, token.TokMap.TokenString(e.ErrorToken), e.ErrorToken.Pos.String())
if e.Err != nil {
fmt.Fprintf(w, e.Err.Error())
} else {
fmt.Fprintf(w, ", expected one of: ")
for _, expected := range e.ExpectedTokens {
fmt.Fprintf(w, "%s ", expected)
}
}
return w.String()
}

View File

@@ -0,0 +1,597 @@
// Code generated by gocc; DO NOT EDIT.
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
package lexer
import (
"fmt"
"github.com/gonum/graph/formats/dot/internal/token"
)
type ActionTable [NumStates]ActionRow
type ActionRow struct {
Accept token.Type
Ignore string
}
func (this ActionRow) String() string {
return fmt.Sprintf("Accept=%d, Ignore=%s", this.Accept, this.Ignore)
}
var ActTab = ActionTable{
ActionRow{ // S0
Accept: 0,
Ignore: "",
},
ActionRow{ // S1
Accept: -1,
Ignore: "!whitespace",
},
ActionRow{ // S2
Accept: 0,
Ignore: "",
},
ActionRow{ // S3
Accept: 0,
Ignore: "",
},
ActionRow{ // S4
Accept: 15,
Ignore: "",
},
ActionRow{ // S5
Accept: 0,
Ignore: "",
},
ActionRow{ // S6
Accept: 0,
Ignore: "",
},
ActionRow{ // S7
Accept: 0,
Ignore: "",
},
ActionRow{ // S8
Accept: 19,
Ignore: "",
},
ActionRow{ // S9
Accept: 18,
Ignore: "",
},
ActionRow{ // S10
Accept: 8,
Ignore: "",
},
ActionRow{ // S11
Accept: 0,
Ignore: "",
},
ActionRow{ // S12
Accept: 16,
Ignore: "",
},
ActionRow{ // S13
Accept: 19,
Ignore: "",
},
ActionRow{ // S14
Accept: 19,
Ignore: "",
},
ActionRow{ // S15
Accept: 19,
Ignore: "",
},
ActionRow{ // S16
Accept: 19,
Ignore: "",
},
ActionRow{ // S17
Accept: 19,
Ignore: "",
},
ActionRow{ // S18
Accept: 19,
Ignore: "",
},
ActionRow{ // S19
Accept: 13,
Ignore: "",
},
ActionRow{ // S20
Accept: 14,
Ignore: "",
},
ActionRow{ // S21
Accept: 19,
Ignore: "",
},
ActionRow{ // S22
Accept: 19,
Ignore: "",
},
ActionRow{ // S23
Accept: 19,
Ignore: "",
},
ActionRow{ // S24
Accept: 19,
Ignore: "",
},
ActionRow{ // S25
Accept: 19,
Ignore: "",
},
ActionRow{ // S26
Accept: 19,
Ignore: "",
},
ActionRow{ // S27
Accept: 2,
Ignore: "",
},
ActionRow{ // S28
Accept: 3,
Ignore: "",
},
ActionRow{ // S29
Accept: 19,
Ignore: "",
},
ActionRow{ // S30
Accept: 0,
Ignore: "",
},
ActionRow{ // S31
Accept: 19,
Ignore: "",
},
ActionRow{ // S32
Accept: 0,
Ignore: "",
},
ActionRow{ // S33
Accept: 0,
Ignore: "",
},
ActionRow{ // S34
Accept: -1,
Ignore: "!comment",
},
ActionRow{ // S35
Accept: 9,
Ignore: "",
},
ActionRow{ // S36
Accept: 10,
Ignore: "",
},
ActionRow{ // S37
Accept: 19,
Ignore: "",
},
ActionRow{ // S38
Accept: 0,
Ignore: "",
},
ActionRow{ // S39
Accept: 0,
Ignore: "",
},
ActionRow{ // S40
Accept: 19,
Ignore: "",
},
ActionRow{ // S41
Accept: 0,
Ignore: "",
},
ActionRow{ // S42
Accept: 0,
Ignore: "",
},
ActionRow{ // S43
Accept: 19,
Ignore: "",
},
ActionRow{ // S44
Accept: 19,
Ignore: "",
},
ActionRow{ // S45
Accept: 19,
Ignore: "",
},
ActionRow{ // S46
Accept: 19,
Ignore: "",
},
ActionRow{ // S47
Accept: 19,
Ignore: "",
},
ActionRow{ // S48
Accept: 19,
Ignore: "",
},
ActionRow{ // S49
Accept: 19,
Ignore: "",
},
ActionRow{ // S50
Accept: 19,
Ignore: "",
},
ActionRow{ // S51
Accept: 19,
Ignore: "",
},
ActionRow{ // S52
Accept: 19,
Ignore: "",
},
ActionRow{ // S53
Accept: 19,
Ignore: "",
},
ActionRow{ // S54
Accept: 19,
Ignore: "",
},
ActionRow{ // S55
Accept: 19,
Ignore: "",
},
ActionRow{ // S56
Accept: 19,
Ignore: "",
},
ActionRow{ // S57
Accept: 19,
Ignore: "",
},
ActionRow{ // S58
Accept: 19,
Ignore: "",
},
ActionRow{ // S59
Accept: 19,
Ignore: "",
},
ActionRow{ // S60
Accept: 19,
Ignore: "",
},
ActionRow{ // S61
Accept: 19,
Ignore: "",
},
ActionRow{ // S62
Accept: 19,
Ignore: "",
},
ActionRow{ // S63
Accept: 0,
Ignore: "",
},
ActionRow{ // S64
Accept: 0,
Ignore: "",
},
ActionRow{ // S65
Accept: 0,
Ignore: "",
},
ActionRow{ // S66
Accept: 0,
Ignore: "",
},
ActionRow{ // S67
Accept: 19,
Ignore: "",
},
ActionRow{ // S68
Accept: 0,
Ignore: "",
},
ActionRow{ // S69
Accept: 19,
Ignore: "",
},
ActionRow{ // S70
Accept: 19,
Ignore: "",
},
ActionRow{ // S71
Accept: 19,
Ignore: "",
},
ActionRow{ // S72
Accept: 19,
Ignore: "",
},
ActionRow{ // S73
Accept: 19,
Ignore: "",
},
ActionRow{ // S74
Accept: 19,
Ignore: "",
},
ActionRow{ // S75
Accept: 19,
Ignore: "",
},
ActionRow{ // S76
Accept: 19,
Ignore: "",
},
ActionRow{ // S77
Accept: 19,
Ignore: "",
},
ActionRow{ // S78
Accept: 19,
Ignore: "",
},
ActionRow{ // S79
Accept: 19,
Ignore: "",
},
ActionRow{ // S80
Accept: 19,
Ignore: "",
},
ActionRow{ // S81
Accept: 19,
Ignore: "",
},
ActionRow{ // S82
Accept: 19,
Ignore: "",
},
ActionRow{ // S83
Accept: 19,
Ignore: "",
},
ActionRow{ // S84
Accept: 19,
Ignore: "",
},
ActionRow{ // S85
Accept: 19,
Ignore: "",
},
ActionRow{ // S86
Accept: 19,
Ignore: "",
},
ActionRow{ // S87
Accept: 19,
Ignore: "",
},
ActionRow{ // S88
Accept: 19,
Ignore: "",
},
ActionRow{ // S89
Accept: -1,
Ignore: "!comment",
},
ActionRow{ // S90
Accept: 0,
Ignore: "",
},
ActionRow{ // S91
Accept: 19,
Ignore: "",
},
ActionRow{ // S92
Accept: 19,
Ignore: "",
},
ActionRow{ // S93
Accept: 19,
Ignore: "",
},
ActionRow{ // S94
Accept: 12,
Ignore: "",
},
ActionRow{ // S95
Accept: 19,
Ignore: "",
},
ActionRow{ // S96
Accept: 19,
Ignore: "",
},
ActionRow{ // S97
Accept: 11,
Ignore: "",
},
ActionRow{ // S98
Accept: 19,
Ignore: "",
},
ActionRow{ // S99
Accept: 19,
Ignore: "",
},
ActionRow{ // S100
Accept: 19,
Ignore: "",
},
ActionRow{ // S101
Accept: 19,
Ignore: "",
},
ActionRow{ // S102
Accept: 19,
Ignore: "",
},
ActionRow{ // S103
Accept: 19,
Ignore: "",
},
ActionRow{ // S104
Accept: 19,
Ignore: "",
},
ActionRow{ // S105
Accept: 19,
Ignore: "",
},
ActionRow{ // S106
Accept: 19,
Ignore: "",
},
ActionRow{ // S107
Accept: 19,
Ignore: "",
},
ActionRow{ // S108
Accept: 19,
Ignore: "",
},
ActionRow{ // S109
Accept: 19,
Ignore: "",
},
ActionRow{ // S110
Accept: 19,
Ignore: "",
},
ActionRow{ // S111
Accept: 19,
Ignore: "",
},
ActionRow{ // S112
Accept: 6,
Ignore: "",
},
ActionRow{ // S113
Accept: 19,
Ignore: "",
},
ActionRow{ // S114
Accept: 19,
Ignore: "",
},
ActionRow{ // S115
Accept: 19,
Ignore: "",
},
ActionRow{ // S116
Accept: 19,
Ignore: "",
},
ActionRow{ // S117
Accept: 19,
Ignore: "",
},
ActionRow{ // S118
Accept: 19,
Ignore: "",
},
ActionRow{ // S119
Accept: 19,
Ignore: "",
},
ActionRow{ // S120
Accept: 19,
Ignore: "",
},
ActionRow{ // S121
Accept: 19,
Ignore: "",
},
ActionRow{ // S122
Accept: 19,
Ignore: "",
},
ActionRow{ // S123
Accept: 19,
Ignore: "",
},
ActionRow{ // S124
Accept: 19,
Ignore: "",
},
ActionRow{ // S125
Accept: 19,
Ignore: "",
},
ActionRow{ // S126
Accept: 5,
Ignore: "",
},
ActionRow{ // S127
Accept: 19,
Ignore: "",
},
ActionRow{ // S128
Accept: 19,
Ignore: "",
},
ActionRow{ // S129
Accept: 19,
Ignore: "",
},
ActionRow{ // S130
Accept: 19,
Ignore: "",
},
ActionRow{ // S131
Accept: 19,
Ignore: "",
},
ActionRow{ // S132
Accept: 19,
Ignore: "",
},
ActionRow{ // S133
Accept: 19,
Ignore: "",
},
ActionRow{ // S134
Accept: 7,
Ignore: "",
},
ActionRow{ // S135
Accept: 19,
Ignore: "",
},
ActionRow{ // S136
Accept: 19,
Ignore: "",
},
ActionRow{ // S137
Accept: 19,
Ignore: "",
},
ActionRow{ // S138
Accept: 19,
Ignore: "",
},
ActionRow{ // S139
Accept: 19,
Ignore: "",
},
ActionRow{ // S140
Accept: 17,
Ignore: "",
},
}

View File

@@ -0,0 +1,338 @@
// Code generated by gocc; DO NOT EDIT.
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
package lexer
import (
// "fmt"
"io/ioutil"
"unicode/utf8"
// "github.com/gonum/graph/formats/dot/internal/util"
"github.com/gonum/graph/formats/dot/internal/token"
)
const (
NoState = -1
NumStates = 141
NumSymbols = 184
)
type Lexer struct {
src []byte
pos int
line int
column int
}
func NewLexer(src []byte) *Lexer {
lexer := &Lexer{
src: src,
pos: 0,
line: 1,
column: 1,
}
return lexer
}
func NewLexerFile(fpath string) (*Lexer, error) {
src, err := ioutil.ReadFile(fpath)
if err != nil {
return nil, err
}
return NewLexer(src), nil
}
func (this *Lexer) Scan() (tok *token.Token) {
// fmt.Printf("Lexer.Scan() pos=%d\n", this.pos)
tok = new(token.Token)
if this.pos >= len(this.src) {
tok.Type = token.EOF
tok.Pos.Offset, tok.Pos.Line, tok.Pos.Column = this.pos, this.line, this.column
return
}
start, startLine, startColumn, end := this.pos, this.line, this.column, 0
tok.Type = token.INVALID
state, rune1, size := 0, rune(-1), 0
for state != -1 {
// fmt.Printf("\tpos=%d, line=%d, col=%d, state=%d\n", this.pos, this.line, this.column, state)
if this.pos >= len(this.src) {
rune1 = -1
} else {
rune1, size = utf8.DecodeRune(this.src[this.pos:])
this.pos += size
}
// Production start
if rune1 != -1 {
state = TransTab[state](rune1)
} else {
state = -1
}
// Production end
// Debug start
// nextState := -1
// if rune1 != -1 {
// nextState = TransTab[state](rune1)
// }
// fmt.Printf("\tS%d, : tok=%s, rune == %s(%x), next state == %d\n", state, token.TokMap.Id(tok.Type), util.RuneToString(rune1), rune1, nextState)
// fmt.Printf("\t\tpos=%d, size=%d, start=%d, end=%d\n", this.pos, size, start, end)
// if nextState != -1 {
// fmt.Printf("\t\taction:%s\n", ActTab[nextState].String())
// }
// state = nextState
// Debug end
if state != -1 {
switch rune1 {
case '\n':
this.line++
this.column = 1
case '\r':
this.column = 1
case '\t':
this.column += 4
default:
this.column++
}
switch {
case ActTab[state].Accept != -1:
tok.Type = ActTab[state].Accept
// fmt.Printf("\t Accept(%s), %s(%d)\n", string(act), token.TokMap.Id(tok), tok)
end = this.pos
case ActTab[state].Ignore != "":
// fmt.Printf("\t Ignore(%s)\n", string(act))
start, startLine, startColumn = this.pos, this.line, this.column
state = 0
if start >= len(this.src) {
tok.Type = token.EOF
}
}
} else {
if tok.Type == token.INVALID {
end = this.pos
}
}
}
if end > start {
this.pos = end
tok.Lit = this.src[start:end]
} else {
tok.Lit = []byte{}
}
tok.Pos.Offset, tok.Pos.Line, tok.Pos.Column = start, startLine, startColumn
// fmt.Printf("Token at %s: %s \"%s\"\n", tok.String(), token.TokMap.Id(tok.Type), tok.Lit)
return
}
func (this *Lexer) Reset() {
this.pos = 0
}
/*
Lexer symbols:
0: 'n'
1: 'o'
2: 'd'
3: 'e'
4: 'N'
5: 'o'
6: 'd'
7: 'e'
8: 'N'
9: 'O'
10: 'D'
11: 'E'
12: 'e'
13: 'd'
14: 'g'
15: 'e'
16: 'E'
17: 'd'
18: 'g'
19: 'e'
20: 'E'
21: 'D'
22: 'G'
23: 'E'
24: 'g'
25: 'r'
26: 'a'
27: 'p'
28: 'h'
29: 'G'
30: 'r'
31: 'a'
32: 'p'
33: 'h'
34: 'G'
35: 'R'
36: 'A'
37: 'P'
38: 'H'
39: 'd'
40: 'i'
41: 'g'
42: 'r'
43: 'a'
44: 'p'
45: 'h'
46: 'D'
47: 'i'
48: 'g'
49: 'r'
50: 'a'
51: 'p'
52: 'h'
53: 'd'
54: 'i'
55: 'G'
56: 'r'
57: 'a'
58: 'p'
59: 'h'
60: 'D'
61: 'i'
62: 'G'
63: 'r'
64: 'a'
65: 'p'
66: 'h'
67: 'D'
68: 'I'
69: 'G'
70: 'R'
71: 'A'
72: 'P'
73: 'H'
74: 's'
75: 'u'
76: 'b'
77: 'g'
78: 'r'
79: 'a'
80: 'p'
81: 'h'
82: 'S'
83: 'u'
84: 'b'
85: 'g'
86: 'r'
87: 'a'
88: 'p'
89: 'h'
90: 's'
91: 'u'
92: 'b'
93: 'G'
94: 'r'
95: 'a'
96: 'p'
97: 'h'
98: 'S'
99: 'u'
100: 'b'
101: 'G'
102: 'r'
103: 'a'
104: 'p'
105: 'h'
106: 'S'
107: 'U'
108: 'B'
109: 'G'
110: 'R'
111: 'A'
112: 'P'
113: 'H'
114: 's'
115: 't'
116: 'r'
117: 'i'
118: 'c'
119: 't'
120: 'S'
121: 't'
122: 'r'
123: 'i'
124: 'c'
125: 't'
126: 'S'
127: 'T'
128: 'R'
129: 'I'
130: 'C'
131: 'T'
132: '{'
133: '}'
134: ';'
135: '-'
136: '-'
137: '-'
138: '>'
139: '['
140: ']'
141: ','
142: '='
143: ':'
144: '_'
145: '-'
146: '.'
147: '-'
148: '.'
149: '\'
150: '"'
151: '\'
152: '"'
153: '"'
154: '='
155: '<'
156: '>'
157: '<'
158: '>'
159: '/'
160: '/'
161: '\n'
162: '#'
163: '\n'
164: '/'
165: '*'
166: '*'
167: '*'
168: '/'
169: ' '
170: '\t'
171: '\r'
172: '\n'
173: \u0001-'!'
174: '#'-'['
175: ']'-\u007f
176: 'a'-'z'
177: 'A'-'Z'
178: '0'-'9'
179: \u0080-\ufffc
180: \ufffe-\U0010ffff
181: \u0001-';'
182: '?'-\u00ff
183: .
*/

View File

@@ -0,0 +1,54 @@
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
package lexer_test
import (
"bytes"
"io/ioutil"
"testing"
"github.com/gonum/graph/formats/dot"
)
func TestParseFile(t *testing.T) {
golden := []struct {
in string
out string
}{
{
in: "testdata/tokens.dot",
out: "testdata/tokens.golden",
},
}
for _, g := range golden {
file, err := dot.ParseFile(g.in)
if err != nil {
t.Errorf("%q: unable to parse file; %v", g.in, err)
continue
}
// If no output path is specified, the input is already golden.
out := g.in
if len(g.out) > 0 {
out = g.out
}
buf, err := ioutil.ReadFile(out)
if err != nil {
t.Errorf("%q: unable to read file; %v", g.in, err)
continue
}
got := file.String()
// Remove trailing newline.
want := string(bytes.TrimSpace(buf))
if got != want {
t.Errorf("%q: graph mismatch; expected %q, got %q", g.in, want, got)
}
}
}

View File

@@ -0,0 +1,39 @@
# C preprocessing directives act as comments.
/* block comment */
// keywords are case-insensitive.
graph {
node []
Node []
NODE []
edge []
Edge []
EDGE []
subgraph {}
subGraph {}
Subgraph {}
SubGraph {}
SUBGRAPH S {}
A; B [style=filled, fillcolor=red]
C:nw -- D:se
"foo"
.10
-20
3.14
F [label=<<div>foo</div>>]
_foo
a10
}
Graph {
}
GRAPH {
}
digraph {
}
Digraph {
}
diGraph {
}
DiGraph {
}
DIGRAPH {
}

View File

@@ -0,0 +1,37 @@
graph {
node []
node []
node []
edge []
edge []
edge []
{}
{}
{}
{}
subgraph S {}
A
B [style=filled fillcolor=red]
C:nw -- D:se
"foo"
.10
-20
3.14
F [label=<<div>foo</div>>]
_foo
a10
}
graph {
}
graph {
}
digraph {
}
digraph {
}
digraph {
}
digraph {
}
digraph {
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,61 @@
// Code generated by gocc; DO NOT EDIT.
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
package parser
import (
"fmt"
)
type action interface {
act()
String() string
}
type (
accept bool
shift int // value is next state index
reduce int // value is production index
)
func (this accept) act() {}
func (this shift) act() {}
func (this reduce) act() {}
func (this accept) Equal(that action) bool {
if _, ok := that.(accept); ok {
return true
}
return false
}
func (this reduce) Equal(that action) bool {
that1, ok := that.(reduce)
if !ok {
return false
}
return this == that1
}
func (this shift) Equal(that action) bool {
that1, ok := that.(shift)
if !ok {
return false
}
return this == that1
}
func (this accept) String() string { return "accept(0)" }
func (this shift) String() string { return fmt.Sprintf("shift:%d", this) }
func (this reduce) String() string {
return fmt.Sprintf("reduce:%d(%s)", this, productionsTable[this].String)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,222 @@
// Code generated by gocc; DO NOT EDIT.
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
package parser
import (
"bytes"
"fmt"
parseError "github.com/gonum/graph/formats/dot/internal/errors"
"github.com/gonum/graph/formats/dot/internal/token"
)
const (
numProductions = 55
numStates = 87
numSymbols = 50
)
// Stack
type stack struct {
state []int
attrib []Attrib
}
const iNITIAL_STACK_SIZE = 100
func newStack() *stack {
return &stack{state: make([]int, 0, iNITIAL_STACK_SIZE),
attrib: make([]Attrib, 0, iNITIAL_STACK_SIZE),
}
}
func (this *stack) reset() {
this.state = this.state[0:0]
this.attrib = this.attrib[0:0]
}
func (this *stack) push(s int, a Attrib) {
this.state = append(this.state, s)
this.attrib = append(this.attrib, a)
}
func (this *stack) top() int {
return this.state[len(this.state)-1]
}
func (this *stack) peek(pos int) int {
return this.state[pos]
}
func (this *stack) topIndex() int {
return len(this.state) - 1
}
func (this *stack) popN(items int) []Attrib {
lo, hi := len(this.state)-items, len(this.state)
attrib := this.attrib[lo:hi]
this.state = this.state[:lo]
this.attrib = this.attrib[:lo]
return attrib
}
func (S *stack) String() string {
w := new(bytes.Buffer)
fmt.Fprintf(w, "stack:\n")
for i, st := range S.state {
fmt.Fprintf(w, "\t%d:%d , ", i, st)
if S.attrib[i] == nil {
fmt.Fprintf(w, "nil")
} else {
fmt.Fprintf(w, "%v", S.attrib[i])
}
fmt.Fprintf(w, "\n")
}
return w.String()
}
// Parser
type Parser struct {
stack *stack
nextToken *token.Token
pos int
}
type Scanner interface {
Scan() (tok *token.Token)
}
func NewParser() *Parser {
p := &Parser{stack: newStack()}
p.Reset()
return p
}
func (P *Parser) Reset() {
P.stack.reset()
P.stack.push(0, nil)
}
func (P *Parser) Error(err error, scanner Scanner) (recovered bool, errorAttrib *parseError.Error) {
errorAttrib = &parseError.Error{
Err: err,
ErrorToken: P.nextToken,
ErrorSymbols: P.popNonRecoveryStates(),
ExpectedTokens: make([]string, 0, 8),
}
for t, action := range actionTab[P.stack.top()].actions {
if action != nil {
errorAttrib.ExpectedTokens = append(errorAttrib.ExpectedTokens, token.TokMap.Id(token.Type(t)))
}
}
if action := actionTab[P.stack.top()].actions[token.TokMap.Type("error")]; action != nil {
P.stack.push(int(action.(shift)), errorAttrib) // action can only be shift
} else {
return
}
if action := actionTab[P.stack.top()].actions[P.nextToken.Type]; action != nil {
recovered = true
}
for !recovered && P.nextToken.Type != token.EOF {
P.nextToken = scanner.Scan()
if action := actionTab[P.stack.top()].actions[P.nextToken.Type]; action != nil {
recovered = true
}
}
return
}
func (P *Parser) popNonRecoveryStates() (removedAttribs []parseError.ErrorSymbol) {
if rs, ok := P.firstRecoveryState(); ok {
errorSymbols := P.stack.popN(int(P.stack.topIndex() - rs))
removedAttribs = make([]parseError.ErrorSymbol, len(errorSymbols))
for i, e := range errorSymbols {
removedAttribs[i] = e
}
} else {
removedAttribs = []parseError.ErrorSymbol{}
}
return
}
// recoveryState points to the highest state on the stack, which can recover
func (P *Parser) firstRecoveryState() (recoveryState int, canRecover bool) {
recoveryState, canRecover = P.stack.topIndex(), actionTab[P.stack.top()].canRecover
for recoveryState > 0 && !canRecover {
recoveryState--
canRecover = actionTab[P.stack.peek(recoveryState)].canRecover
}
return
}
func (P *Parser) newError(err error) error {
e := &parseError.Error{
Err: err,
StackTop: P.stack.top(),
ErrorToken: P.nextToken,
}
actRow := actionTab[P.stack.top()]
for i, t := range actRow.actions {
if t != nil {
e.ExpectedTokens = append(e.ExpectedTokens, token.TokMap.Id(token.Type(i)))
}
}
return e
}
func (this *Parser) Parse(scanner Scanner) (res interface{}, err error) {
this.Reset()
this.nextToken = scanner.Scan()
for acc := false; !acc; {
action := actionTab[this.stack.top()].actions[this.nextToken.Type]
if action == nil {
if recovered, errAttrib := this.Error(nil, scanner); !recovered {
this.nextToken = errAttrib.ErrorToken
return nil, this.newError(nil)
}
if action = actionTab[this.stack.top()].actions[this.nextToken.Type]; action == nil {
panic("Error recovery led to invalid action")
}
}
// fmt.Printf("S%d %s %s\n", this.stack.top(), token.TokMap.TokenString(this.nextToken), action.String())
switch act := action.(type) {
case accept:
res = this.stack.popN(1)[0]
acc = true
case shift:
this.stack.push(int(act), this.nextToken)
this.nextToken = scanner.Scan()
case reduce:
prod := productionsTable[int(act)]
attrib, err := prod.ReduceFunc(this.stack.popN(prod.NumSymbols))
if err != nil {
return nil, this.newError(err)
} else {
this.stack.push(gotoTab[this.stack.top()][prod.NTType], attrib)
}
default:
panic("unknown action: " + action.String())
}
}
return res, nil
}

View File

@@ -0,0 +1,114 @@
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
package parser_test
import (
"bytes"
"io/ioutil"
"testing"
"github.com/gonum/graph/formats/dot"
)
func TestParseFile(t *testing.T) {
golden := []struct {
in string
out string
}{
{in: "../testdata/empty.dot"},
{in: "../testdata/graph.dot"},
{in: "../testdata/digraph.dot"},
{in: "../testdata/strict.dot"},
{in: "../testdata/multi.dot"},
{in: "../testdata/named_graph.dot"},
{in: "../testdata/node_stmt.dot"},
{in: "../testdata/edge_stmt.dot"},
{in: "../testdata/attr_stmt.dot"},
{in: "../testdata/attr.dot"},
{
in: "../testdata/subgraph.dot",
out: "../testdata/subgraph.golden",
},
{
in: "../testdata/semi.dot",
out: "../testdata/semi.golden",
},
{
in: "../testdata/empty_attr.dot",
out: "../testdata/empty_attr.golden",
},
{
in: "../testdata/attr_lists.dot",
out: "../testdata/attr_lists.golden",
},
{
in: "../testdata/attr_sep.dot",
out: "../testdata/attr_sep.golden",
},
{in: "../testdata/subgraph_vertex.dot"},
{
in: "../testdata/port.dot",
out: "../testdata/port.golden",
},
{in: "../testdata/quoted_id.dot"},
{
in: "../testdata/backslash_newline_id.dot",
out: "../testdata/backslash_newline_id.golden",
},
}
for _, g := range golden {
file, err := dot.ParseFile(g.in)
if err != nil {
t.Errorf("%q: unable to parse file; %v", g.in, err)
continue
}
// If no output path is specified, the input is already golden.
out := g.in
if len(g.out) > 0 {
out = g.out
}
buf, err := ioutil.ReadFile(out)
if err != nil {
t.Errorf("%q: unable to read file; %v", g.in, err)
continue
}
got := file.String()
// Remove trailing newline.
want := string(bytes.TrimSpace(buf))
if got != want {
t.Errorf("%q: graph mismatch; expected `%s`, got `%s`", g.in, want, got)
}
}
}
func TestParseError(t *testing.T) {
golden := []struct {
path string
want string
}{
{
path: "../testdata/error.dot",
want: `Error in S30: INVALID(0,~), Pos(offset=13, line=2, column=7), expected one of: { } graphx ; -- -> node edge [ = subgraph : id `,
},
}
for _, g := range golden {
_, err := dot.ParseFile(g.path)
if err == nil {
t.Errorf("%q: expected error, got nil", g.path)
continue
}
got := err.Error()
if got != g.want {
t.Errorf("%q: error mismatch; expected `%v`, got `%v`", g.path, g.want, got)
continue
}
}
}

View File

@@ -0,0 +1,586 @@
// Code generated by gocc; DO NOT EDIT.
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
package parser
import (
"github.com/gonum/graph/formats/dot/ast"
"github.com/gonum/graph/formats/dot/internal/astx"
)
type (
//TODO: change type and variable names to be consistent with other tables
ProdTab [numProductions]ProdTabEntry
ProdTabEntry struct {
String string
Id string
NTType int
Index int
NumSymbols int
ReduceFunc func([]Attrib) (Attrib, error)
}
Attrib interface {
}
)
var productionsTable = ProdTab{
ProdTabEntry{
String: `S' : File << >>`,
Id: "S'",
NTType: 0,
Index: 0,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `File : Graph << astx.NewFile(X[0]) >>`,
Id: "File",
NTType: 1,
Index: 1,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.NewFile(X[0])
},
},
ProdTabEntry{
String: `File : File Graph << astx.AppendGraph(X[0], X[1]) >>`,
Id: "File",
NTType: 1,
Index: 2,
NumSymbols: 2,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.AppendGraph(X[0], X[1])
},
},
ProdTabEntry{
String: `Graph : OptStrict DirectedGraph OptID "{" OptStmtList "}" << astx.NewGraph(X[0], X[1], X[2], X[4]) >>`,
Id: "Graph",
NTType: 2,
Index: 3,
NumSymbols: 6,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.NewGraph(X[0], X[1], X[2], X[4])
},
},
ProdTabEntry{
String: `OptStrict : empty << false, nil >>`,
Id: "OptStrict",
NTType: 3,
Index: 4,
NumSymbols: 0,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return false, nil
},
},
ProdTabEntry{
String: `OptStrict : strict << true, nil >>`,
Id: "OptStrict",
NTType: 3,
Index: 5,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return true, nil
},
},
ProdTabEntry{
String: `DirectedGraph : graphx << false, nil >>`,
Id: "DirectedGraph",
NTType: 4,
Index: 6,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return false, nil
},
},
ProdTabEntry{
String: `DirectedGraph : digraph << true, nil >>`,
Id: "DirectedGraph",
NTType: 4,
Index: 7,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return true, nil
},
},
ProdTabEntry{
String: `StmtList : Stmt OptSemi << astx.NewStmtList(X[0]) >>`,
Id: "StmtList",
NTType: 5,
Index: 8,
NumSymbols: 2,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.NewStmtList(X[0])
},
},
ProdTabEntry{
String: `StmtList : StmtList Stmt OptSemi << astx.AppendStmt(X[0], X[1]) >>`,
Id: "StmtList",
NTType: 5,
Index: 9,
NumSymbols: 3,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.AppendStmt(X[0], X[1])
},
},
ProdTabEntry{
String: `OptStmtList : empty << >>`,
Id: "OptStmtList",
NTType: 6,
Index: 10,
NumSymbols: 0,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return nil, nil
},
},
ProdTabEntry{
String: `OptStmtList : StmtList << >>`,
Id: "OptStmtList",
NTType: 6,
Index: 11,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `Stmt : NodeStmt << >>`,
Id: "Stmt",
NTType: 7,
Index: 12,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `Stmt : EdgeStmt << >>`,
Id: "Stmt",
NTType: 7,
Index: 13,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `Stmt : AttrStmt << >>`,
Id: "Stmt",
NTType: 7,
Index: 14,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `Stmt : Attr << >>`,
Id: "Stmt",
NTType: 7,
Index: 15,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `Stmt : Subgraph << >>`,
Id: "Stmt",
NTType: 7,
Index: 16,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `OptSemi : empty << >>`,
Id: "OptSemi",
NTType: 8,
Index: 17,
NumSymbols: 0,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return nil, nil
},
},
ProdTabEntry{
String: `OptSemi : ";" << >>`,
Id: "OptSemi",
NTType: 8,
Index: 18,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `NodeStmt : Node OptAttrList << astx.NewNodeStmt(X[0], X[1]) >>`,
Id: "NodeStmt",
NTType: 9,
Index: 19,
NumSymbols: 2,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.NewNodeStmt(X[0], X[1])
},
},
ProdTabEntry{
String: `EdgeStmt : Vertex Edge OptAttrList << astx.NewEdgeStmt(X[0], X[1], X[2]) >>`,
Id: "EdgeStmt",
NTType: 10,
Index: 20,
NumSymbols: 3,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.NewEdgeStmt(X[0], X[1], X[2])
},
},
ProdTabEntry{
String: `Edge : DirectedEdge Vertex OptEdge << astx.NewEdge(X[0], X[1], X[2]) >>`,
Id: "Edge",
NTType: 11,
Index: 21,
NumSymbols: 3,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.NewEdge(X[0], X[1], X[2])
},
},
ProdTabEntry{
String: `DirectedEdge : "--" << false, nil >>`,
Id: "DirectedEdge",
NTType: 12,
Index: 22,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return false, nil
},
},
ProdTabEntry{
String: `DirectedEdge : "->" << true, nil >>`,
Id: "DirectedEdge",
NTType: 12,
Index: 23,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return true, nil
},
},
ProdTabEntry{
String: `OptEdge : empty << >>`,
Id: "OptEdge",
NTType: 13,
Index: 24,
NumSymbols: 0,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return nil, nil
},
},
ProdTabEntry{
String: `OptEdge : Edge << >>`,
Id: "OptEdge",
NTType: 13,
Index: 25,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `AttrStmt : Component AttrList << astx.NewAttrStmt(X[0], X[1]) >>`,
Id: "AttrStmt",
NTType: 14,
Index: 26,
NumSymbols: 2,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.NewAttrStmt(X[0], X[1])
},
},
ProdTabEntry{
String: `Component : graphx << ast.KindGraph, nil >>`,
Id: "Component",
NTType: 15,
Index: 27,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return ast.KindGraph, nil
},
},
ProdTabEntry{
String: `Component : node << ast.KindNode, nil >>`,
Id: "Component",
NTType: 15,
Index: 28,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return ast.KindNode, nil
},
},
ProdTabEntry{
String: `Component : edge << ast.KindEdge, nil >>`,
Id: "Component",
NTType: 15,
Index: 29,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return ast.KindEdge, nil
},
},
ProdTabEntry{
String: `AttrList : "[" OptAList "]" << X[1], nil >>`,
Id: "AttrList",
NTType: 16,
Index: 30,
NumSymbols: 3,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[1], nil
},
},
ProdTabEntry{
String: `AttrList : AttrList "[" OptAList "]" << astx.AppendAttrList(X[0], X[2]) >>`,
Id: "AttrList",
NTType: 16,
Index: 31,
NumSymbols: 4,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.AppendAttrList(X[0], X[2])
},
},
ProdTabEntry{
String: `OptAttrList : empty << >>`,
Id: "OptAttrList",
NTType: 17,
Index: 32,
NumSymbols: 0,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return nil, nil
},
},
ProdTabEntry{
String: `OptAttrList : AttrList << >>`,
Id: "OptAttrList",
NTType: 17,
Index: 33,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `AList : Attr OptSep << astx.NewAttrList(X[0]) >>`,
Id: "AList",
NTType: 18,
Index: 34,
NumSymbols: 2,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.NewAttrList(X[0])
},
},
ProdTabEntry{
String: `AList : AList Attr OptSep << astx.AppendAttr(X[0], X[1]) >>`,
Id: "AList",
NTType: 18,
Index: 35,
NumSymbols: 3,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.AppendAttr(X[0], X[1])
},
},
ProdTabEntry{
String: `OptAList : empty << >>`,
Id: "OptAList",
NTType: 19,
Index: 36,
NumSymbols: 0,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return nil, nil
},
},
ProdTabEntry{
String: `OptAList : AList << >>`,
Id: "OptAList",
NTType: 19,
Index: 37,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `OptSep : empty << >>`,
Id: "OptSep",
NTType: 20,
Index: 38,
NumSymbols: 0,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return nil, nil
},
},
ProdTabEntry{
String: `OptSep : ";" << >>`,
Id: "OptSep",
NTType: 20,
Index: 39,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `OptSep : "," << >>`,
Id: "OptSep",
NTType: 20,
Index: 40,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `Attr : ID "=" ID << astx.NewAttr(X[0], X[2]) >>`,
Id: "Attr",
NTType: 21,
Index: 41,
NumSymbols: 3,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.NewAttr(X[0], X[2])
},
},
ProdTabEntry{
String: `Subgraph : OptSubgraphID "{" OptStmtList "}" << astx.NewSubgraph(X[0], X[2]) >>`,
Id: "Subgraph",
NTType: 22,
Index: 42,
NumSymbols: 4,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.NewSubgraph(X[0], X[2])
},
},
ProdTabEntry{
String: `OptSubgraphID : empty << >>`,
Id: "OptSubgraphID",
NTType: 23,
Index: 43,
NumSymbols: 0,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return nil, nil
},
},
ProdTabEntry{
String: `OptSubgraphID : subgraph OptID << X[1], nil >>`,
Id: "OptSubgraphID",
NTType: 23,
Index: 44,
NumSymbols: 2,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[1], nil
},
},
ProdTabEntry{
String: `Vertex : Node << >>`,
Id: "Vertex",
NTType: 24,
Index: 45,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `Vertex : Subgraph << >>`,
Id: "Vertex",
NTType: 24,
Index: 46,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `Node : ID OptPort << astx.NewNode(X[0], X[1]) >>`,
Id: "Node",
NTType: 25,
Index: 47,
NumSymbols: 2,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.NewNode(X[0], X[1])
},
},
ProdTabEntry{
String: `Port : ":" ID << astx.NewPort(X[1], nil) >>`,
Id: "Port",
NTType: 26,
Index: 48,
NumSymbols: 2,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.NewPort(X[1], nil)
},
},
ProdTabEntry{
String: `Port : ":" ID ":" ID << astx.NewPort(X[1], X[3]) >>`,
Id: "Port",
NTType: 26,
Index: 49,
NumSymbols: 4,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.NewPort(X[1], X[3])
},
},
ProdTabEntry{
String: `OptPort : empty << >>`,
Id: "OptPort",
NTType: 27,
Index: 50,
NumSymbols: 0,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return nil, nil
},
},
ProdTabEntry{
String: `OptPort : Port << >>`,
Id: "OptPort",
NTType: 27,
Index: 51,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
ProdTabEntry{
String: `ID : id << astx.NewID(X[0]) >>`,
Id: "ID",
NTType: 28,
Index: 52,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return astx.NewID(X[0])
},
},
ProdTabEntry{
String: `OptID : empty << "", nil >>`,
Id: "OptID",
NTType: 29,
Index: 53,
NumSymbols: 0,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return "", nil
},
},
ProdTabEntry{
String: `OptID : ID << >>`,
Id: "OptID",
NTType: 29,
Index: 54,
NumSymbols: 1,
ReduceFunc: func(X []Attrib) (Attrib, error) {
return X[0], nil
},
},
}

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
find . -type f -name '*.go' \
| xargs sed -i -e "s|// Code generated by gocc; DO NOT EDIT.|\
// Code generated by gocc; DO NOT EDIT.\n\
\n\
// This file is dual licensed under CC0 and The gonum license.\n\
//\n\
// Copyright ©2017 The gonum Authors. All rights reserved.\n\
// Use of this source code is governed by a BSD-style\n\
// license that can be found in the LICENSE file.\n\
//\n\
// Copyright ©2017 Robin Eklind.\n\
// This file is made available under a Creative Commons CC0 1.0\n\
// Universal Public Domain Dedication.\n\
|"

View File

@@ -0,0 +1,4 @@
digraph {
bgcolor=transparent
A
}

View File

@@ -0,0 +1,3 @@
digraph {
A [style=filled] [fillcolor=red]
}

View File

@@ -0,0 +1,3 @@
digraph {
A [style=filled fillcolor=red]
}

View File

@@ -0,0 +1,3 @@
digraph {
A [style=filled, fillcolor=red; color=blue]
}

View File

@@ -0,0 +1,3 @@
digraph {
A [style=filled fillcolor=red color=blue]
}

View File

@@ -0,0 +1,6 @@
digraph {
graph [bgcolor=transparent]
node [style=filled fillcolor=white]
edge [minlen=2]
A -> B
}

View File

@@ -0,0 +1,4 @@
digraph {
A [name="hello \
world"]
}

View File

@@ -0,0 +1,3 @@
digraph {
A [name="hello world"]
}

View File

@@ -0,0 +1,3 @@
digraph {
A -> B
}

View File

@@ -0,0 +1,4 @@
digraph {
A -> B -> C
D -> E [color=red minlen=2]
}

View File

@@ -0,0 +1,2 @@
graph {
}

View File

@@ -0,0 +1,3 @@
digraph {
A []
}

View File

@@ -0,0 +1,3 @@
digraph {
A
}

View File

@@ -0,0 +1,3 @@
digraph {
A ~ B
}

View File

@@ -0,0 +1,3 @@
graph {
A -- B
}

View File

@@ -0,0 +1,6 @@
digraph {
A -> B
}
digraph {
C -> D
}

View File

@@ -0,0 +1,3 @@
graph G {
A
}

View File

@@ -0,0 +1,3 @@
digraph {
A
}

View File

@@ -0,0 +1,11 @@
digraph {
A:ne -> B:sw
C:foo -> D:bar:se
E:_ -> F
G:n
H:e
I:s
J:w
K:nw
L:c
}

View File

@@ -0,0 +1,11 @@
digraph {
A:ne -> B:sw
C:foo -> D:bar:se
E -> F
G:n
H:e
I:s
J:w
K:nw
L:c
}

View File

@@ -0,0 +1,3 @@
digraph {
"A" -> "B" ["color"="red"]
}

View File

@@ -0,0 +1,3 @@
digraph {
A -> B; C
}

View File

@@ -0,0 +1,4 @@
digraph {
A -> B
C
}

View File

@@ -0,0 +1,4 @@
strict digraph {
A -> B
A -> B
}

View File

@@ -0,0 +1,5 @@
digraph {
{A}
subgraph {B}
subgraph S {C}
}

View File

@@ -0,0 +1,5 @@
digraph {
{A}
{B}
subgraph S {C}
}

View File

@@ -0,0 +1,3 @@
digraph {
{A B} -> C
}

View File

@@ -0,0 +1,116 @@
// Code generated by gocc; DO NOT EDIT.
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
package token
import (
"fmt"
)
type Token struct {
Type
Lit []byte
Pos
}
type Type int
const (
INVALID Type = iota
EOF
)
type Pos struct {
Offset int
Line int
Column int
}
func (this Pos) String() string {
return fmt.Sprintf("Pos(offset=%d, line=%d, column=%d)", this.Offset, this.Line, this.Column)
}
type TokenMap struct {
typeMap []string
idMap map[string]Type
}
func (this TokenMap) Id(tok Type) string {
if int(tok) < len(this.typeMap) {
return this.typeMap[tok]
}
return "unknown"
}
func (this TokenMap) Type(tok string) Type {
if typ, exist := this.idMap[tok]; exist {
return typ
}
return INVALID
}
func (this TokenMap) TokenString(tok *Token) string {
//TODO: refactor to print pos & token string properly
return fmt.Sprintf("%s(%d,%s)", this.Id(tok.Type), tok.Type, tok.Lit)
}
func (this TokenMap) StringType(typ Type) string {
return fmt.Sprintf("%s(%d)", this.Id(typ), typ)
}
var TokMap = TokenMap{
typeMap: []string{
"INVALID",
"$",
"{",
"}",
"empty",
"strict",
"graphx",
"digraph",
";",
"--",
"->",
"node",
"edge",
"[",
"]",
",",
"=",
"subgraph",
":",
"id",
},
idMap: map[string]Type{
"INVALID": 0,
"$": 1,
"{": 2,
"}": 3,
"empty": 4,
"strict": 5,
"graphx": 6,
"digraph": 7,
";": 8,
"--": 9,
"->": 10,
"node": 11,
"edge": 12,
"[": 13,
"]": 14,
",": 15,
"=": 16,
"subgraph": 17,
":": 18,
"id": 19,
},
}

View File

@@ -0,0 +1,118 @@
// Code generated by gocc; DO NOT EDIT.
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
package util
import (
"fmt"
"strconv"
"unicode"
"unicode/utf8"
)
/* Interface */
/*
Convert the literal value of a scanned token to rune
*/
func RuneValue(lit []byte) rune {
if lit[1] == '\\' {
return escapeCharVal(lit)
}
r, size := utf8.DecodeRune(lit[1:])
if size != len(lit)-2 {
panic(fmt.Sprintf("Error decoding rune. Lit: %s, rune: %d, size%d\n", lit, r, size))
}
return r
}
/*
Convert the literal value of a scanned token to int64
*/
func IntValue(lit []byte) (int64, error) {
return strconv.ParseInt(string(lit), 10, 64)
}
/*
Convert the literal value of a scanned token to uint64
*/
func UintValue(lit []byte) (uint64, error) {
return strconv.ParseUint(string(lit), 10, 64)
}
/* Util */
func escapeCharVal(lit []byte) rune {
var i, base, max uint32
offset := 2
switch lit[offset] {
case 'a':
return '\a'
case 'b':
return '\b'
case 'f':
return '\f'
case 'n':
return '\n'
case 'r':
return '\r'
case 't':
return '\t'
case 'v':
return '\v'
case '\\':
return '\\'
case '\'':
return '\''
case '0', '1', '2', '3', '4', '5', '6', '7':
i, base, max = 3, 8, 255
case 'x':
i, base, max = 2, 16, 255
offset++
case 'u':
i, base, max = 4, 16, unicode.MaxRune
offset++
case 'U':
i, base, max = 8, 16, unicode.MaxRune
offset++
default:
panic(fmt.Sprintf("Error decoding character literal: %s\n", lit))
}
var x uint32
for ; i > 0 && offset < len(lit)-1; i-- {
ch, size := utf8.DecodeRune(lit[offset:])
offset += size
d := uint32(digitVal(ch))
if d >= base {
panic(fmt.Sprintf("charVal(%s): illegal character (%c) in escape sequence. size=%d, offset=%d", lit, ch, size, offset))
}
x = x*base + d
}
if x > max || 0xD800 <= x && x < 0xE000 {
panic(fmt.Sprintf("Error decoding escape char value. Lit:%s, offset:%d, escape sequence is invalid Unicode code point\n", lit, offset))
}
return rune(x)
}
func digitVal(ch rune) int {
switch {
case '0' <= ch && ch <= '9':
return int(ch) - '0'
case 'a' <= ch && ch <= 'f':
return int(ch) - 'a' + 10
case 'A' <= ch && ch <= 'F':
return int(ch) - 'A' + 10
}
return 16 // larger than any legal digit val
}

View File

@@ -0,0 +1,49 @@
// Code generated by gocc; DO NOT EDIT.
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
package util
import (
"fmt"
)
func RuneToString(r rune) string {
if r >= 0x20 && r < 0x7f {
return fmt.Sprintf("'%c'", r)
}
switch r {
case 0x07:
return "'\\a'"
case 0x08:
return "'\\b'"
case 0x0C:
return "'\\f'"
case 0x0A:
return "'\\n'"
case 0x0D:
return "'\\r'"
case 0x09:
return "'\\t'"
case 0x0b:
return "'\\v'"
case 0x5c:
return "'\\\\\\'"
case 0x27:
return "'\\''"
case 0x22:
return "'\\\"'"
}
if r < 0x10000 {
return fmt.Sprintf("\\u%04x", r)
}
return fmt.Sprintf("\\U%08x", r)
}

160
graph/formats/dot/sem.go Normal file
View File

@@ -0,0 +1,160 @@
// This file is dual licensed under CC0 and The gonum license.
//
// Copyright ©2017 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.
//
// Copyright ©2017 Robin Eklind.
// This file is made available under a Creative Commons CC0 1.0
// Universal Public Domain Dedication.
package dot
import (
"fmt"
"github.com/gonum/graph/formats/dot/ast"
)
// check validates the semantics of the given DOT file.
func check(file *ast.File) error {
for _, graph := range file.Graphs {
// TODO: Check graph.ID for duplicates?
if err := checkGraph(graph); err != nil {
return err
}
}
return nil
}
// check validates the semantics of the given graph.
func checkGraph(graph *ast.Graph) error {
for _, stmt := range graph.Stmts {
if err := checkStmt(graph, stmt); err != nil {
return err
}
}
return nil
}
// check validates the semantics of the given statement.
func checkStmt(graph *ast.Graph, stmt ast.Stmt) error {
switch stmt := stmt.(type) {
case *ast.NodeStmt:
return checkNodeStmt(graph, stmt)
case *ast.EdgeStmt:
return checkEdgeStmt(graph, stmt)
case *ast.AttrStmt:
return checkAttrStmt(graph, stmt)
case *ast.Attr:
// TODO: Verify that the attribute is indeed of graph component kind.
return checkAttr(graph, ast.KindGraph, stmt)
case *ast.Subgraph:
return checkSubgraph(graph, stmt)
default:
panic(fmt.Sprintf("support for statement of type %T not yet implemented", stmt))
}
}
// checkNodeStmt validates the semantics of the given node statement.
func checkNodeStmt(graph *ast.Graph, stmt *ast.NodeStmt) error {
if err := checkNode(graph, stmt.Node); err != nil {
return err
}
for _, attr := range stmt.Attrs {
// TODO: Verify that the attribute is indeed of node component kind.
if err := checkAttr(graph, ast.KindNode, attr); err != nil {
return err
}
}
return nil
}
// checkEdgeStmt validates the semantics of the given edge statement.
func checkEdgeStmt(graph *ast.Graph, stmt *ast.EdgeStmt) error {
// TODO: if graph.Strict, check for multi-edges.
if err := checkVertex(graph, stmt.From); err != nil {
return err
}
for _, attr := range stmt.Attrs {
// TODO: Verify that the attribute is indeed of edge component kind.
if err := checkAttr(graph, ast.KindEdge, attr); err != nil {
return err
}
}
return checkEdge(graph, stmt.From, stmt.To)
}
// checkEdge validates the semantics of the given edge.
func checkEdge(graph *ast.Graph, from ast.Vertex, to *ast.Edge) error {
if !graph.Directed && to.Directed {
return fmt.Errorf("undirected graph %q contains directed edge from %q to %q", graph.ID, from, to.Vertex)
}
if err := checkVertex(graph, to.Vertex); err != nil {
return err
}
if to.To != nil {
return checkEdge(graph, to.Vertex, to.To)
}
return nil
}
// checkAttrStmt validates the semantics of the given attribute statement.
func checkAttrStmt(graph *ast.Graph, stmt *ast.AttrStmt) error {
for _, attr := range stmt.Attrs {
if err := checkAttr(graph, stmt.Kind, attr); err != nil {
return err
}
}
return nil
}
// checkAttr validates the semantics of the given attribute for the given
// component kind.
func checkAttr(graph *ast.Graph, kind ast.Kind, attr *ast.Attr) error {
switch kind {
case ast.KindGraph:
// TODO: Validate key-value pairs for graphs.
return nil
case ast.KindNode:
// TODO: Validate key-value pairs for nodes.
return nil
case ast.KindEdge:
// TODO: Validate key-value pairs for edges.
return nil
default:
panic(fmt.Sprintf("support for component kind %v not yet supported", kind))
}
}
// checkSubgraph validates the semantics of the given subgraph.
func checkSubgraph(graph *ast.Graph, subgraph *ast.Subgraph) error {
// TODO: Check subgraph.ID for duplicates?
for _, stmt := range subgraph.Stmts {
// TODO: Refine handling of subgraph statements?
// checkSubgraphStmt(graph, subgraph, stmt)
if err := checkStmt(graph, stmt); err != nil {
return err
}
}
return nil
}
// checkVertex validates the semantics of the given vertex.
func checkVertex(graph *ast.Graph, vertex ast.Vertex) error {
switch vertex := vertex.(type) {
case *ast.Node:
return checkNode(graph, vertex)
case *ast.Subgraph:
return checkSubgraph(graph, vertex)
default:
panic(fmt.Sprintf("support for vertex of type %T not yet supported", vertex))
}
}
// checNode validates the semantics of the given node.
func checkNode(graph *ast.Graph, node *ast.Node) error {
// TODO: Check node.ID for duplicates?
// TODO: Validate node.Port.
return nil
}

5
graph/formats/dot/testdata/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
*.dot
*.png
graphviz
input
output

106
graph/formats/dot/testdata/Makefile vendored Normal file
View File

@@ -0,0 +1,106 @@
# Dependencies:
#
# * imgcmp
# go get github.com/mewkiz/cmd/imgcmp
# * dotfmt
# go get github.com/graphism/dot/cmd/dotfmt
# * dot
# sudo pacman -S graphviz
# * recode
# sudo pacman -S recode
DOT=$(wildcard *.dot)
# Skip DOT files for which the generated PNG images mismatch.
#
# ref: https://github.com/graphism/dot/issues/2
#
# pixel colors differ at x=550, y=1885
DOT:=$(filter-out b51.dot, $(DOT))
# pixel colors differ at x=5395, y=1920
DOT:=$(filter-out b106.dot, $(DOT))
# Skip segfaulting files.
#
# Segmentation fault (core dumped)
DOT:=$(filter-out b15.dot, $(DOT))
# Segmentation fault (core dumped)
DOT:=$(filter-out b81.dot, $(DOT))
# *** stack smashing detected ***: dot terminated
DOT:=$(filter-out sides.dot, $(DOT))
# *** stack smashing detected ***: dot terminated
DOT:=$(filter-out tee.dot, $(DOT))
# Skip DOT files above 100 kB.
DOT:=$(filter-out 4elt.dot, $(DOT))
DOT:=$(filter-out b29.dot, $(DOT))
DOT:=$(filter-out b81.dot, $(DOT))
DOT:=$(filter-out b100.dot, $(DOT))
DOT:=$(filter-out b102.dot, $(DOT))
DOT:=$(filter-out b103.dot, $(DOT))
DOT:=$(filter-out b104.dot, $(DOT))
DOT:=$(filter-out root.dot, $(DOT))
DOT:=$(filter-out root_circo.dot, $(DOT))
DOT:=$(filter-out root_twopi.dot, $(DOT))
# Skip invalid DOT file.
#
# Error: No or improper image file="eqn.png"
# in label of node struct1
DOT:=$(filter-out html4.dot, $(DOT))
# Skip multi-graph DOT file which outputs to standard output.
DOT:=$(filter-out multi.dot, $(DOT))
# *.dot -> *.png
PNG=$(DOT:.dot=.png)
INPUT_PNG=$(addprefix input/,$(PNG))
OUTPUT_PNG=$(addprefix output/,$(PNG))
all:
test: input $(INPUT_PNG) output $(OUTPUT_PNG)
@echo "PASS"
input:
mkdir -p $@
dot -V
input/%.png: %.dot
dot -Tpng -o $@ $<
output:
mkdir -p $@
output/%.png: %.dot
dotfmt -o "output/$<" $<
dot -Tpng -o $@ "output/$<"
imgcmp "input/$(notdir $@)" $@
fetch: graphviz
# Copy *.gv and *.dot files.
find graphviz -type f -name '*.gv' -not -wholename "graphviz/rtest/share/b545.gv" -not -name "base.gv" | xargs -I '{}' cp "{}" .
find graphviz -type f -name '*.dot' | xargs -I '{}' cp "{}" .
# Rename *.gv to *.dot.
#rename .gv .dot *.gv
ls *.gv | xargs -I '{}' basename "{}" .gv | xargs -I '{}' mv "{}.gv" "{}.dot"
# Remove execute permissions.
chmod 0644 *.dot
# Convert Latin1 encoded files to UTF-8.
grep -l "charset=latin1" *.dot | xargs -I '{}' recode ISO-8859-1..UTF8 "{}"
recode ISO-8859-1..UTF8 Latin1.dot
# Clean up.
rm -rf graphviz
graphviz:
git clone https://github.com/ellson/graphviz.git
clean:
rm -rf *.dot input output
.PHONY: all test fetch clean

153
graph/graph.go Normal file
View File

@@ -0,0 +1,153 @@
// Copyright ©2014 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
// Node is a graph node. It returns a graph-unique integer ID.
type Node interface {
ID() int
}
// Edge is a graph edge. In directed graphs, the direction of the
// edge is given from -> to, otherwise the edge is semantically
// unordered.
type Edge interface {
From() Node
To() Node
Weight() float64
}
// Graph is a generalized graph.
type Graph interface {
// Has returns whether the node exists within the graph.
Has(Node) bool
// Nodes returns all the nodes in the graph.
Nodes() []Node
// From returns all nodes that can be reached directly
// from the given node.
From(Node) []Node
// HasEdgeBeteen returns whether an edge exists between
// nodes x and y without considering direction.
HasEdgeBetween(x, y Node) bool
// 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.
Edge(u, v Node) Edge
}
// Undirected is an undirected graph.
type Undirected interface {
Graph
// EdgeBetween returns the edge between nodes x and y.
EdgeBetween(x, y Node) Edge
}
// Directed is a directed graph.
type Directed interface {
Graph
// HasEdgeFromTo returns whether an edge exists
// in the graph from u to v.
HasEdgeFromTo(u, v Node) bool
// To returns all nodes that can reach directly
// to the given node.
To(Node) []Node
}
// Weighter defines graphs that can report edge weights.
type Weighter interface {
// 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 or there is no
// joining edge between the two nodes the weight
// value returned is implementation dependent.
// Weight returns true if an edge exists between
// x and y or if x and y have the same ID, false
// otherwise.
Weight(x, y Node) (w float64, ok bool)
}
// NodeAdder is an interface for adding arbitrary nodes to a graph.
type NodeAdder interface {
// NewNodeID returns a new unique arbitrary ID.
NewNodeID() int
// Adds a node to the graph. AddNode panics if
// the added node ID matches an existing node ID.
AddNode(Node)
}
// NodeRemover is an interface for removing nodes from a graph.
type NodeRemover interface {
// RemoveNode removes a node from the graph, as
// well as any edges attached to it. If the node
// is not in the graph it is a no-op.
RemoveNode(Node)
}
// EdgeSetter is an interface for adding edges to a graph.
type EdgeSetter interface {
// SetEdge adds an edge from one node to another.
// If the graph supports node addition the nodes
// will be added if they do not exist, otherwise
// SetEdge will panic.
// If the IDs returned by e.From and e.To are
// equal, SetEdge will panic.
SetEdge(e Edge)
}
// EdgeRemover is an interface for removing nodes from a graph.
type EdgeRemover interface {
// RemoveEdge removes the given edge, leaving the
// terminal nodes. If the edge does not exist it
// is a no-op.
RemoveEdge(Edge)
}
// Builder is a graph that can have nodes and edges added.
type Builder interface {
NodeAdder
EdgeSetter
}
// UndirectedBuilder is an undirected graph builder.
type UndirectedBuilder interface {
Undirected
Builder
}
// DirectedBuilder is a directed graph builder.
type DirectedBuilder interface {
Directed
Builder
}
// Copy copies nodes and edges as undirected edges from the source to the destination
// without first clearing the destination. Copy will panic if a node ID in the source
// graph matches a node ID in the destination.
//
// If the source is undirected and the destination is directed both directions will
// be present in the destination after the copy is complete.
//
// 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. 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 {
dst.AddNode(n)
}
for _, u := range nodes {
for _, v := range src.From(u) {
dst.SetEdge(src.Edge(u, v))
}
}
}

View File

@@ -0,0 +1,357 @@
// 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.
// The functions in this file are random graph generators from the paper
// by Batagelj and Brandes http://algo.uni-konstanz.de/publications/bb-eglrn-05.pdf
package gen
import (
"fmt"
"math"
"math/rand"
"github.com/gonum/graph"
"github.com/gonum/graph/simple"
)
// Gnp constructs a Gilberts model graph in the destination, dst, of order n. Edges
// between nodes are formed with the probability, p. If src is not nil it is used
// as the random source, otherwise rand.Float64 is used. The graph is constructed
// in O(n+m) time where m is the number of edges added.
func Gnp(dst GraphBuilder, n int, p float64, src *rand.Rand) error {
if p == 0 {
return nil
}
if p < 0 || p > 1 {
return fmt.Errorf("gen: bad probability: p=%v", p)
}
var r func() float64
if src == nil {
r = rand.Float64
} else {
r = src.Float64
}
for i := 0; i < n; i++ {
if !dst.Has(simple.Node(i)) {
dst.AddNode(simple.Node(i))
}
}
lp := math.Log(1 - p)
// Add forward edges for all graphs.
for v, w := 1, -1; v < n; {
w += 1 + int(math.Log(1-r())/lp)
for w >= v && v < n {
w -= v
v++
}
if v < n {
dst.SetEdge(simple.Edge{F: simple.Node(w), T: simple.Node(v), W: 1})
}
}
// Add backward edges for directed graphs.
if _, ok := dst.(graph.Directed); !ok {
return nil
}
for v, w := 1, -1; v < n; {
w += 1 + int(math.Log(1-r())/lp)
for w >= v && v < n {
w -= v
v++
}
if v < n {
dst.SetEdge(simple.Edge{F: simple.Node(v), T: simple.Node(w), W: 1})
}
}
return nil
}
// edgeNodesFor returns the pair of nodes for the ith edge in a simple
// undirected graph. The pair is returned such that w.ID < v.ID.
func edgeNodesFor(i int) (v, w simple.Node) {
// This is an algebraic simplification of the expressions described
// on p3 of http://algo.uni-konstanz.de/publications/bb-eglrn-05.pdf
v = simple.Node(0.5 + math.Sqrt(float64(1+8*i))/2)
w = simple.Node(i) - v*(v-1)/2
return v, w
}
// Gnm constructs a Erdős-Rényi model graph in the destination, dst, of
// order n and size m. If src is not nil it is used as the random source,
// otherwise rand.Intn is used. The graph is constructed in O(m) expected
// time for m ≤ (n choose 2)/2.
func Gnm(dst GraphBuilder, n, m int, src *rand.Rand) error {
if m == 0 {
return nil
}
hasEdge := dst.HasEdgeBetween
d, isDirected := dst.(graph.Directed)
if isDirected {
m /= 2
hasEdge = d.HasEdgeFromTo
}
nChoose2 := (n - 1) * n / 2
if m < 0 || m > nChoose2 {
return fmt.Errorf("gen: bad size: m=%d", m)
}
var rnd func(int) int
if src == nil {
rnd = rand.Intn
} else {
rnd = src.Intn
}
for i := 0; i < n; i++ {
if !dst.Has(simple.Node(i)) {
dst.AddNode(simple.Node(i))
}
}
// Add forward edges for all graphs.
for i := 0; i < m; i++ {
for {
v, w := edgeNodesFor(rnd(nChoose2))
e := simple.Edge{F: w, T: v, W: 1}
if !hasEdge(e.F, e.T) {
dst.SetEdge(e)
break
}
}
}
// Add backward edges for directed graphs.
if !isDirected {
return nil
}
for i := 0; i < m; i++ {
for {
v, w := edgeNodesFor(rnd(nChoose2))
e := simple.Edge{F: v, T: w, W: 1}
if !hasEdge(e.F, e.T) {
dst.SetEdge(e)
break
}
}
}
return nil
}
// SmallWorldsBB constructs a small worlds graph of order n in the destination, dst.
// Node degree is specified by d and edge replacement by the probability, p.
// If src is not nil it is used as the random source, otherwise rand.Float64 is used.
// The graph is constructed in O(nd) time.
//
// The algorithm used is described in http://algo.uni-konstanz.de/publications/bb-eglrn-05.pdf
func SmallWorldsBB(dst GraphBuilder, n, d int, p float64, src *rand.Rand) error {
if d < 1 || d > (n-1)/2 {
return fmt.Errorf("gen: bad degree: d=%d", d)
}
if p == 0 {
return nil
}
if p < 0 || p >= 1 {
return fmt.Errorf("gen: bad replacement: p=%v", p)
}
var (
rnd func() float64
rndN func(int) int
)
if src == nil {
rnd = rand.Float64
rndN = rand.Intn
} else {
rnd = src.Float64
rndN = src.Intn
}
hasEdge := dst.HasEdgeBetween
dg, isDirected := dst.(graph.Directed)
if isDirected {
hasEdge = dg.HasEdgeFromTo
}
for i := 0; i < n; i++ {
if !dst.Has(simple.Node(i)) {
dst.AddNode(simple.Node(i))
}
}
nChoose2 := (n - 1) * n / 2
lp := math.Log(1 - p)
// Add forward edges for all graphs.
k := int(math.Log(1-rnd()) / lp)
m := 0
replace := make(map[int]int)
for v := 0; v < n; v++ {
for i := 1; i <= d; i++ {
if k > 0 {
j := v*(v-1)/2 + (v+i)%n
ej := simple.Edge{W: 1}
ej.T, ej.F = edgeNodesFor(j)
if !hasEdge(ej.From(), ej.To()) {
dst.SetEdge(ej)
}
k--
m++
em := simple.Edge{W: 1}
em.T, em.F = edgeNodesFor(m)
if !hasEdge(em.From(), em.To()) {
replace[j] = m
} else {
replace[j] = replace[m]
}
} else {
k = int(math.Log(1-rnd()) / lp)
}
}
}
for i := m + 1; i <= n*d && i < nChoose2; i++ {
r := rndN(nChoose2-i) + i
er := simple.Edge{W: 1}
er.T, er.F = edgeNodesFor(r)
if !hasEdge(er.From(), er.To()) {
dst.SetEdge(er)
} else {
er.T, er.F = edgeNodesFor(replace[r])
if !hasEdge(er.From(), er.To()) {
dst.SetEdge(er)
}
}
ei := simple.Edge{W: 1}
ei.T, ei.F = edgeNodesFor(i)
if !hasEdge(ei.From(), ei.To()) {
replace[r] = i
} else {
replace[r] = replace[i]
}
}
// Add backward edges for directed graphs.
if !isDirected {
return nil
}
k = int(math.Log(1-rnd()) / lp)
m = 0
replace = make(map[int]int)
for v := 0; v < n; v++ {
for i := 1; i <= d; i++ {
if k > 0 {
j := v*(v-1)/2 + (v+i)%n
ej := simple.Edge{W: 1}
ej.F, ej.T = edgeNodesFor(j)
if !hasEdge(ej.From(), ej.To()) {
dst.SetEdge(ej)
}
k--
m++
if !hasEdge(edgeNodesFor(m)) {
replace[j] = m
} else {
replace[j] = replace[m]
}
} else {
k = int(math.Log(1-rnd()) / lp)
}
}
}
for i := m + 1; i <= n*d && i < nChoose2; i++ {
r := rndN(nChoose2-i) + i
er := simple.Edge{W: 1}
er.F, er.T = edgeNodesFor(r)
if !hasEdge(er.From(), er.To()) {
dst.SetEdge(er)
} else {
er.F, er.T = edgeNodesFor(replace[r])
if !hasEdge(er.From(), er.To()) {
dst.SetEdge(er)
}
}
if !hasEdge(edgeNodesFor(i)) {
replace[r] = i
} else {
replace[r] = replace[i]
}
}
return nil
}
/*
// Multigraph generators.
type EdgeAdder interface {
AddEdge(graph.Edge)
}
func PreferentialAttachment(dst EdgeAdder, n, d int, src *rand.Rand) {
if d < 1 {
panic("gen: bad d")
}
var rnd func(int) int
if src == nil {
rnd = rand.Intn
} else {
rnd = src.Intn
}
m := make([]simple.Node, 2*n*d)
for v := 0; v < n; v++ {
for i := 0; i < d; i++ {
m[2*(v*d+i)] = simple.Node(v)
m[2*(v*d+i)+1] = simple.Node(m[rnd(2*v*d+i+1)])
}
}
for i := 0; i < n*d; i++ {
dst.AddEdge(simple.Edge{F: m[2*i], T: m[2*i+1], W: 1})
}
}
func BipartitePreferentialAttachment(dst EdgeAdder, n, d int, src *rand.Rand) {
if d < 1 {
panic("gen: bad d")
}
var rnd func(int) int
if src == nil {
rnd = rand.Intn
} else {
rnd = src.Intn
}
m1 := make([]simple.Node, 2*n*d)
m2 := make([]simple.Node, 2*n*d)
for v := 0; v < n; v++ {
for i := 0; i < d; i++ {
m1[2*(v*d+i)] = simple.Node(v)
m2[2*(v*d+i)] = simple.Node(n + v)
if r := rnd(2*v*d + i + 1); r&0x1 == 0 {
m1[2*(v*d+i)+1] = m2[r]
} else {
m1[2*(v*d+i)+1] = m1[r]
}
if r := rnd(2*v*d + i + 1); r&0x1 == 0 {
m2[2*(v*d+i)+1] = m1[r]
} else {
m2[2*(v*d+i)+1] = m2[r]
}
}
}
for i := 0; i < n*d; i++ {
dst.AddEdge(simple.Edge{F: m1[2*i], T: m1[2*i+1], W: 1})
dst.AddEdge(simple.Edge{F: m2[2*i], T: m2[2*i+1], W: 1})
}
}
*/

View File

@@ -0,0 +1,175 @@
// 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 gen
import (
"math"
"testing"
"github.com/gonum/graph"
"github.com/gonum/graph/simple"
)
type gnUndirected struct {
graph.UndirectedBuilder
addBackwards bool
addSelfLoop bool
addMultipleEdge bool
}
func (g *gnUndirected) SetEdge(e graph.Edge) {
switch {
case e.From().ID() == e.To().ID():
g.addSelfLoop = true
return
case e.From().ID() > e.To().ID():
g.addBackwards = true
case g.UndirectedBuilder.HasEdgeBetween(e.From(), e.To()):
g.addMultipleEdge = true
}
g.UndirectedBuilder.SetEdge(e)
}
type gnDirected struct {
graph.DirectedBuilder
addSelfLoop bool
addMultipleEdge bool
}
func (g *gnDirected) SetEdge(e graph.Edge) {
switch {
case e.From().ID() == e.To().ID():
g.addSelfLoop = true
return
case g.DirectedBuilder.HasEdgeFromTo(e.From(), e.To()):
g.addMultipleEdge = true
}
g.DirectedBuilder.SetEdge(e)
}
func TestGnpUndirected(t *testing.T) {
for n := 2; n <= 20; n++ {
for p := 0.; p <= 1; p += 0.1 {
g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph(0, math.Inf(1))}
err := Gnp(g, n, p, nil)
if err != nil {
t.Fatalf("unexpected error: n=%d, p=%v: %v", n, p, err)
}
if g.addBackwards {
t.Errorf("edge added with From.ID > To.ID: n=%d, p=%v", n, p)
}
if g.addSelfLoop {
t.Errorf("unexpected self edge: n=%d, p=%v", n, p)
}
if g.addMultipleEdge {
t.Errorf("unexpected multiple edge: n=%d, p=%v", n, p)
}
}
}
}
func TestGnpDirected(t *testing.T) {
for n := 2; n <= 20; n++ {
for p := 0.; p <= 1; p += 0.1 {
g := &gnDirected{DirectedBuilder: simple.NewDirectedGraph(0, math.Inf(1))}
err := Gnp(g, n, p, nil)
if err != nil {
t.Fatalf("unexpected error: n=%d, p=%v: %v", n, p, err)
}
if g.addSelfLoop {
t.Errorf("unexpected self edge: n=%d, p=%v", n, p)
}
if g.addMultipleEdge {
t.Errorf("unexpected multiple edge: n=%d, p=%v", n, p)
}
}
}
}
func TestGnmUndirected(t *testing.T) {
for n := 2; n <= 20; n++ {
nChoose2 := (n - 1) * n / 2
for m := 0; m <= nChoose2; m++ {
g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph(0, math.Inf(1))}
err := Gnm(g, n, m, nil)
if err != nil {
t.Fatalf("unexpected error: n=%d, m=%d: %v", n, m, err)
}
if g.addBackwards {
t.Errorf("edge added with From.ID > To.ID: n=%d, m=%d", n, m)
}
if g.addSelfLoop {
t.Errorf("unexpected self edge: n=%d, m=%d", n, m)
}
if g.addMultipleEdge {
t.Errorf("unexpected multiple edge: n=%d, m=%d", n, m)
}
}
}
}
func TestGnmDirected(t *testing.T) {
for n := 2; n <= 20; n++ {
nChoose2 := (n - 1) * n / 2
for m := 0; m <= nChoose2*2; m++ {
g := &gnDirected{DirectedBuilder: simple.NewDirectedGraph(0, math.Inf(1))}
err := Gnm(g, n, m, nil)
if err != nil {
t.Fatalf("unexpected error: n=%d, m=%d: %v", n, m, err)
}
if g.addSelfLoop {
t.Errorf("unexpected self edge: n=%d, m=%d", n, m)
}
if g.addMultipleEdge {
t.Errorf("unexpected multiple edge: n=%d, m=%d", n, m)
}
}
}
}
func TestSmallWorldsBBUndirected(t *testing.T) {
for n := 2; n <= 20; n++ {
for d := 1; d <= (n-1)/2; d++ {
for p := 0.; p < 1; p += 0.1 {
g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph(0, math.Inf(1))}
err := SmallWorldsBB(g, n, d, p, nil)
if err != nil {
t.Fatalf("unexpected error: n=%d, d=%d, p=%v: %v", n, d, p, err)
}
if g.addBackwards {
t.Errorf("edge added with From.ID > To.ID: n=%d, d=%d, p=%v", n, d, p)
}
if g.addSelfLoop {
t.Errorf("unexpected self edge: n=%d, d=%d, p=%v", n, d, p)
}
if g.addMultipleEdge {
t.Errorf("unexpected multiple edge: n=%d, d=%d, p=%v", n, d, p)
}
}
}
}
}
func TestSmallWorldsBBDirected(t *testing.T) {
for n := 2; n <= 20; n++ {
for d := 1; d <= (n-1)/2; d++ {
for p := 0.; p < 1; p += 0.1 {
g := &gnDirected{DirectedBuilder: simple.NewDirectedGraph(0, math.Inf(1))}
err := SmallWorldsBB(g, n, d, p, nil)
if err != nil {
t.Fatalf("unexpected error: n=%d, d=%d, p=%v: %v", n, d, p, err)
}
if g.addSelfLoop {
t.Errorf("unexpected self edge: n=%d, d=%d, p=%v", n, d, p)
}
if g.addMultipleEdge {
t.Errorf("unexpected multiple edge: n=%d, d=%d, p=%v", n, d, p)
}
}
}
}
}

View File

@@ -0,0 +1,125 @@
// 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 gen
import (
"fmt"
"math"
"math/rand"
"sort"
"github.com/gonum/graph"
"github.com/gonum/graph/internal/ordered"
"github.com/gonum/graph/simple"
)
// UndirectedMutator is an undirected graph builder that can remove edges.
type UndirectedMutator interface {
graph.UndirectedBuilder
graph.EdgeRemover
}
// Duplication constructs a graph in the destination, dst, of order n. New nodes
// are created by duplicating an existing node and all its edges. Each new edge is
// deleted with probability delta. Additional edges are added between the new node
// and existing nodes with probability alpha/|V|. An exception to this addition
// rule is made for the parent node when sigma is not NaN; in this case an edge is
// created with probability sigma. With the exception of the sigma parameter, this
// corresponds to the completely correlated case in doi:10.1016/S0022-5193(03)00028-6.
// If src is not nil it is used as the random source, otherwise rand.Float64 is used.
func Duplication(dst UndirectedMutator, n int, delta, alpha, sigma float64, src *rand.Rand) error {
// As described in doi:10.1016/S0022-5193(03)00028-6 but
// also clarified in doi:10.1186/gb-2007-8-4-r51.
if delta < 0 || delta > 1 {
return fmt.Errorf("gen: bad delta: delta=%v", delta)
}
if alpha <= 0 || alpha > 1 {
return fmt.Errorf("gen: bad alpha: alpha=%v", alpha)
}
if sigma < 0 || sigma > 1 {
return fmt.Errorf("gen: bad sigma: sigma=%v", sigma)
}
var (
rnd func() float64
rndN func(int) int
)
if src == nil {
rnd = rand.Float64
rndN = rand.Intn
} else {
rnd = src.Float64
rndN = src.Intn
}
nodes := dst.Nodes()
sort.Sort(ordered.ByID(nodes))
if len(nodes) == 0 {
n--
dst.AddNode(simple.Node(0))
nodes = append(nodes, simple.Node(0))
}
for i := 0; i < n; i++ {
u := nodes[rndN(len(nodes))]
d := simple.Node(dst.NewNodeID())
// Add the duplicate node.
dst.AddNode(d)
// Loop until we have connectivity
// into the rest of the graph.
for {
// Add edges to parent's neigbours.
to := dst.From(u)
sort.Sort(ordered.ByID(to))
for _, v := range to {
if rnd() < delta || dst.HasEdgeBetween(v, d) {
continue
}
if v.ID() < d.ID() {
dst.SetEdge(simple.Edge{F: v, T: d, W: 1})
} else {
dst.SetEdge(simple.Edge{F: d, T: v, W: 1})
}
}
// Add edges to old nodes.
scaledAlpha := alpha / float64(len(nodes))
for _, v := range nodes {
switch v.ID() {
case u.ID():
if !math.IsNaN(sigma) {
if i == 0 || rnd() < sigma {
if v.ID() < d.ID() {
dst.SetEdge(simple.Edge{F: v, T: d, W: 1})
} else {
dst.SetEdge(simple.Edge{F: d, T: v, W: 1})
}
}
continue
}
fallthrough
default:
if rnd() < scaledAlpha && !dst.HasEdgeBetween(v, d) {
if v.ID() < d.ID() {
dst.SetEdge(simple.Edge{F: v, T: d, W: 1})
} else {
dst.SetEdge(simple.Edge{F: d, T: v, W: 1})
}
}
}
}
if len(dst.From(d)) != 0 {
break
}
}
nodes = append(nodes, d)
}
return nil
}

View File

@@ -0,0 +1,59 @@
// 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 gen
import (
"math"
"testing"
"github.com/gonum/graph"
"github.com/gonum/graph/simple"
)
type duplication struct {
UndirectedMutator
addBackwards bool
addSelfLoop bool
addMultipleEdge bool
}
func (g *duplication) SetEdge(e graph.Edge) {
switch {
case e.From().ID() == e.To().ID():
g.addSelfLoop = true
return
case e.From().ID() > e.To().ID():
g.addBackwards = true
case g.UndirectedMutator.HasEdgeBetween(e.From(), e.To()):
g.addMultipleEdge = true
}
g.UndirectedMutator.SetEdge(e)
}
func TestDuplication(t *testing.T) {
for n := 2; n <= 50; n++ {
for alpha := 0.1; alpha <= 1; alpha += 0.1 {
for delta := 0.; delta <= 1; delta += 0.2 {
for sigma := 0.; sigma <= 1; sigma += 0.2 {
g := &duplication{UndirectedMutator: simple.NewUndirectedGraph(0, math.Inf(1))}
err := Duplication(g, n, delta, alpha, sigma, nil)
if err != nil {
t.Fatalf("unexpected error: n=%d, alpha=%v, delta=%v sigma=%v: %v", n, alpha, delta, sigma, err)
}
if g.addBackwards {
t.Errorf("edge added with From.ID > To.ID: n=%d, alpha=%v, delta=%v sigma=%v", n, alpha, delta, sigma)
}
if g.addSelfLoop {
t.Errorf("unexpected self edge: n=%d, alpha=%v, delta=%v sigma=%v", n, alpha, delta, sigma)
}
if g.addMultipleEdge {
t.Errorf("unexpected multiple edge: n=%d, alpha=%v, delta=%v sigma=%v", n, alpha, delta, sigma)
}
}
}
}
}
}

22
graph/graphs/gen/gen.go Normal file
View File

@@ -0,0 +1,22 @@
// 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 gen provides random graph generation functions.
package gen
import "github.com/gonum/graph"
// GraphBuilder is a graph that can have nodes and edges added.
type GraphBuilder interface {
Has(graph.Node) bool
HasEdgeBetween(x, y graph.Node) bool
graph.Builder
}
func abs(a int) int {
if a < 0 {
return -a
}
return a
}

View File

@@ -0,0 +1,160 @@
// 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 gen
import (
"errors"
"fmt"
"math/rand"
"github.com/gonum/graph"
"github.com/gonum/graph/simple"
"github.com/gonum/stat/sampleuv"
)
// TunableClusteringScaleFree constructs a graph in the destination, dst, of order n.
// The graph is constructed successively starting from an m order graph with one node
// having degree m-1. At each iteration of graph addition, one node is added with m
// additional edges joining existing nodes with probability proportional to the nodes'
// degrees. The edges are formed as a triad with probability, p.
// If src is not nil it is used as the random source, otherwise rand.Float64 and
// rand.Intn are used.
//
// The algorithm is essentially as described in http://arxiv.org/abs/cond-mat/0110452.
func TunableClusteringScaleFree(dst graph.UndirectedBuilder, n, m int, p float64, src *rand.Rand) error {
if p < 0 || p > 1 {
return fmt.Errorf("gen: bad probability: p=%v", p)
}
if n <= m {
return fmt.Errorf("gen: n <= m: n=%v m=%d", n, m)
}
var (
rnd func() float64
rndN func(int) int
)
if src == nil {
rnd = rand.Float64
rndN = rand.Intn
} else {
rnd = src.Float64
rndN = src.Intn
}
// Initial condition.
wt := make([]float64, n)
for u := 0; u < m; u++ {
if !dst.Has(simple.Node(u)) {
dst.AddNode(simple.Node(u))
}
// We need to give equal probability for
// adding the first generation of edges.
wt[u] = 1
}
ws := sampleuv.NewWeighted(wt, src)
for i := range wt {
// These weights will organically grow
// after the first growth iteration.
wt[i] = 0
}
// Growth.
for v := m; v < n; v++ {
var u int
pa:
for i := 0; i < m; i++ {
// Triad formation.
if i != 0 && rnd() < p {
for _, w := range permute(dst.From(simple.Node(u)), rndN) {
wid := w.ID()
if wid == v || dst.HasEdgeBetween(w, simple.Node(v)) {
continue
}
dst.SetEdge(simple.Edge{F: w, T: simple.Node(v), W: 1})
wt[wid]++
wt[v]++
continue pa
}
}
// Preferential attachment.
for {
var ok bool
u, ok = ws.Take()
if !ok {
return errors.New("gen: depleted distribution")
}
if u == v || dst.HasEdgeBetween(simple.Node(u), simple.Node(v)) {
continue
}
dst.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
wt[u]++
wt[v]++
break
}
}
ws.ReweightAll(wt)
}
return nil
}
func permute(n []graph.Node, rnd func(int) int) []graph.Node {
for i := range n[:len(n)-1] {
j := rnd(len(n)-i) + i
n[i], n[j] = n[j], n[i]
}
return n
}
// PreferentialAttachment constructs a graph in the destination, dst, of order n.
// The graph is constructed successively starting from an m order graph with one
// node having degree m-1. At each iteration of graph addition, one node is added
// with m additional edges joining existing nodes with probability proportional
// to the nodes' degrees. If src is not nil it is used as the random source,
// otherwise rand.Float64 is used.
//
// The algorithm is essentially as described in http://arxiv.org/abs/cond-mat/0110452
// after 10.1126/science.286.5439.509.
func PreferentialAttachment(dst graph.UndirectedBuilder, n, m int, src *rand.Rand) error {
if n <= m {
return fmt.Errorf("gen: n <= m: n=%v m=%d", n, m)
}
// Initial condition.
wt := make([]float64, n)
for u := 0; u < m; u++ {
if !dst.Has(simple.Node(u)) {
dst.AddNode(simple.Node(u))
}
// We need to give equal probability for
// adding the first generation of edges.
wt[u] = 1
}
ws := sampleuv.NewWeighted(wt, src)
for i := range wt {
// These weights will organically grow
// after the first growth iteration.
wt[i] = 0
}
// Growth.
for v := m; v < n; v++ {
for i := 0; i < m; i++ {
// Preferential attachment.
u, ok := ws.Take()
if !ok {
return errors.New("gen: depleted distribution")
}
dst.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
wt[u]++
wt[v]++
}
ws.ReweightAll(wt)
}
return nil
}

View File

@@ -0,0 +1,56 @@
// 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 gen
import (
"math"
"testing"
"github.com/gonum/graph/simple"
)
func TestTunableClusteringScaleFree(t *testing.T) {
for n := 2; n <= 20; n++ {
for m := 0; m < n; m++ {
for p := 0.; p <= 1; p += 0.1 {
g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph(0, math.Inf(1))}
err := TunableClusteringScaleFree(g, n, m, p, nil)
if err != nil {
t.Fatalf("unexpected error: n=%d, m=%d, p=%v: %v", n, m, p, err)
}
if g.addBackwards {
t.Errorf("edge added with From.ID > To.ID: n=%d, m=%d, p=%v", n, m, p)
}
if g.addSelfLoop {
t.Errorf("unexpected self edge: n=%d, m=%d, p=%v", n, m, p)
}
if g.addMultipleEdge {
t.Errorf("unexpected multiple edge: n=%d, m=%d, p=%v", n, m, p)
}
}
}
}
}
func TestPreferentialAttachment(t *testing.T) {
for n := 2; n <= 20; n++ {
for m := 0; m < n; m++ {
g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph(0, math.Inf(1))}
err := PreferentialAttachment(g, n, m, nil)
if err != nil {
t.Fatalf("unexpected error: n=%d, m=%d: %v", n, m, err)
}
if g.addBackwards {
t.Errorf("edge added with From.ID > To.ID: n=%d, m=%d", n, m)
}
if g.addSelfLoop {
t.Errorf("unexpected self edge: n=%d, m=%d", n, m)
}
if g.addMultipleEdge {
t.Errorf("unexpected multiple edge: n=%d, m=%d", n, m)
}
}
}
}

View File

@@ -0,0 +1,204 @@
// 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 gen
import (
"errors"
"fmt"
"math"
"math/rand"
"github.com/gonum/graph"
"github.com/gonum/graph/simple"
"github.com/gonum/stat/sampleuv"
)
// NavigableSmallWorld constructs an N-dimensional grid with guaranteed local connectivity
// and random long-range connectivity in the destination, dst. The dims parameters specifies
// the length of each of the N dimensions, p defines the Manhattan distance between local
// nodes, and q defines the number of out-going long-range connections from each node. Long-
// range connections are made with a probability proportional to |d(u,v)|^-r where d is the
// Manhattan distance between non-local nodes.
//
// The algorithm is essentially as described on p4 of http://www.cs.cornell.edu/home/kleinber/swn.pdf.
func NavigableSmallWorld(dst GraphBuilder, dims []int, p, q int, r float64, src *rand.Rand) (err error) {
if p < 1 {
return fmt.Errorf("gen: bad local distance: p=%v", p)
}
if q < 0 {
return fmt.Errorf("gen: bad distant link count: q=%v", q)
}
if r < 0 {
return fmt.Errorf("gen: bad decay constant: r=%v", r)
}
n := 1
for _, d := range dims {
n *= d
}
for i := 0; i < n; i++ {
if !dst.Has(simple.Node(i)) {
dst.AddNode(simple.Node(i))
}
}
hasEdge := dst.HasEdgeBetween
d, isDirected := dst.(graph.Directed)
if isDirected {
hasEdge = d.HasEdgeFromTo
}
locality := make([]int, len(dims))
for i := range locality {
locality[i] = p*2 + 1
}
iterateOver(dims, func(u []int) {
uid := idFrom(u, dims)
iterateOver(locality, func(delta []int) {
d := manhattanDelta(u, delta, dims, -p)
if d == 0 || d > p {
return
}
vid := idFromDelta(u, delta, dims, -p)
e := simple.Edge{F: simple.Node(uid), T: simple.Node(vid), W: 1}
if uid > vid {
e.F, e.T = e.T, e.F
}
if !hasEdge(e.From(), e.To()) {
dst.SetEdge(e)
}
if !isDirected {
return
}
e.F, e.T = e.T, e.F
if !hasEdge(e.From(), e.To()) {
dst.SetEdge(e)
}
})
})
defer func() {
r := recover()
if r != nil {
if r != "depleted distribution" {
panic(r)
}
err = errors.New("depleted distribution")
}
}()
w := make([]float64, n)
ws := sampleuv.NewWeighted(w, src)
iterateOver(dims, func(u []int) {
uid := idFrom(u, dims)
iterateOver(dims, func(v []int) {
d := manhattanBetween(u, v)
if d <= p {
return
}
w[idFrom(v, dims)] = math.Pow(float64(d), -r)
})
ws.ReweightAll(w)
for i := 0; i < q; i++ {
vid, ok := ws.Take()
if !ok {
panic("depleted distribution")
}
e := simple.Edge{F: simple.Node(uid), T: simple.Node(vid), W: 1}
if !isDirected && uid > vid {
e.F, e.T = e.T, e.F
}
if !hasEdge(e.From(), e.To()) {
dst.SetEdge(e)
}
}
for i := range w {
w[i] = 0
}
})
return nil
}
// iterateOver performs an iteration over all dimensions of dims, calling fn
// for each state. The elements of state must not be mutated by fn.
func iterateOver(dims []int, fn func(state []int)) {
iterator(0, dims, make([]int, len(dims)), fn)
}
func iterator(d int, dims, state []int, fn func(state []int)) {
if d >= len(dims) {
fn(state)
return
}
for i := 0; i < dims[d]; i++ {
state[d] = i
iterator(d+1, dims, state, fn)
}
}
// manhattanBetween returns the Manhattan distance between a and b.
func manhattanBetween(a, b []int) int {
if len(a) != len(b) {
panic("gen: unexpected dimension")
}
var d int
for i, v := range a {
d += abs(v - b[i])
}
return d
}
// manhattanDelta returns the Manhattan norm of delta+translate. If a
// translated by delta+translate is out of the range given by dims,
// zero is returned.
func manhattanDelta(a, delta, dims []int, translate int) int {
if len(a) != len(dims) {
panic("gen: unexpected dimension")
}
if len(delta) != len(dims) {
panic("gen: unexpected dimension")
}
var d int
for i, v := range delta {
v += translate
t := a[i] + v
if t < 0 || t >= dims[i] {
return 0
}
d += abs(v)
}
return d
}
// idFrom returns a node id for the slice n over the given dimensions.
func idFrom(n, dims []int) int {
s := 1
var id int
for d, m := range dims {
p := n[d]
if p < 0 || p >= m {
panic("gen: element out of range")
}
id += p * s
s *= m
}
return id
}
// idFromDelta returns a node id for the slice base plus the delta over the given
// dimensions and applying the translation.
func idFromDelta(base, delta, dims []int, translate int) int {
s := 1
var id int
for d, m := range dims {
n := base[d] + delta[d] + translate
if n < 0 || n >= m {
panic("gen: element out of range")
}
id += n * s
s *= m
}
return id
}

View File

@@ -0,0 +1,73 @@
// 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 gen
import (
"math"
"testing"
"github.com/gonum/graph/simple"
)
var smallWorldDimensionParameters = [][]int{
{50},
{10, 10},
{6, 5, 4},
}
func TestNavigableSmallWorldUndirected(t *testing.T) {
for p := 1; p < 5; p++ {
for q := 0; q < 10; q++ {
for r := 0.5; r < 10; r++ {
for _, dims := range smallWorldDimensionParameters {
g := &gnUndirected{UndirectedBuilder: simple.NewUndirectedGraph(0, math.Inf(1))}
err := NavigableSmallWorld(g, dims, p, q, r, nil)
n := 1
for _, d := range dims {
n *= d
}
if err != nil {
t.Fatalf("unexpected error: dims=%v n=%d, p=%d, q=%d, r=%v: %v", dims, n, p, q, r, err)
}
if g.addBackwards {
t.Errorf("edge added with From.ID > To.ID: dims=%v n=%d, p=%d, q=%d, r=%v", dims, n, p, q, r)
}
if g.addSelfLoop {
t.Errorf("unexpected self edge: dims=%v n=%d, p=%d, q=%d, r=%v", dims, n, p, q, r)
}
if g.addMultipleEdge {
t.Errorf("unexpected multiple edge: dims=%v n=%d, p=%d, q=%d, r=%v", dims, n, p, q, r)
}
}
}
}
}
}
func TestNavigableSmallWorldDirected(t *testing.T) {
for p := 1; p < 5; p++ {
for q := 0; q < 10; q++ {
for r := 0.5; r < 10; r++ {
for _, dims := range smallWorldDimensionParameters {
g := &gnDirected{DirectedBuilder: simple.NewDirectedGraph(0, math.Inf(1))}
err := NavigableSmallWorld(g, dims, p, q, r, nil)
n := 1
for _, d := range dims {
n *= d
}
if err != nil {
t.Fatalf("unexpected error: dims=%v n=%d, p=%d, q=%d, r=%v, r=%v: %v", dims, n, p, q, r, err)
}
if g.addSelfLoop {
t.Errorf("unexpected self edge: dims=%v n=%d, p=%d, q=%d, r=%v", dims, n, p, q, r)
}
if g.addMultipleEdge {
t.Errorf("unexpected multiple edge: dims=%v n=%d, p=%d, q=%d, r=%v", dims, n, p, q, r)
}
}
}
}
}
}

View File

@@ -0,0 +1,74 @@
// 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 linear provides common linear data structures.
package linear
import (
"github.com/gonum/graph"
)
// NodeStack implements a LIFO stack of graph.Node.
type NodeStack []graph.Node
// Len returns the number of graph.Nodes on the stack.
func (s *NodeStack) Len() int { return len(*s) }
// Pop returns the last graph.Node on the stack and removes it
// from the stack.
func (s *NodeStack) Pop() graph.Node {
v := *s
v, n := v[:len(v)-1], v[len(v)-1]
*s = v
return n
}
// Push adds the node n to the stack at the last position.
func (s *NodeStack) Push(n graph.Node) { *s = append(*s, n) }
// NodeQueue implements a FIFO queue.
type NodeQueue struct {
head int
data []graph.Node
}
// Len returns the number of graph.Nodes in the queue.
func (q *NodeQueue) Len() int { return len(q.data) - q.head }
// Enqueue adds the node n to the back of the queue.
func (q *NodeQueue) Enqueue(n graph.Node) {
if len(q.data) == cap(q.data) && q.head > 0 {
l := q.Len()
copy(q.data, q.data[q.head:])
q.head = 0
q.data = append(q.data[:l], n)
} else {
q.data = append(q.data, n)
}
}
// Dequeue returns the graph.Node at the front of the queue and
// removes it from the queue.
func (q *NodeQueue) Dequeue() graph.Node {
if q.Len() == 0 {
panic("queue: empty queue")
}
var n graph.Node
n, q.data[q.head] = q.data[q.head], nil
q.head++
if q.Len() == 0 {
q.head = 0
q.data = q.data[:0]
}
return n
}
// Reset clears the queue for reuse.
func (q *NodeQueue) Reset() {
q.head = 0
q.data = q.data[:0]
}

View File

@@ -0,0 +1,62 @@
// 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 ordered provides common sort ordering types.
package ordered
import "github.com/gonum/graph"
// ByID implements the sort.Interface sorting a slice of graph.Node
// by ID.
type ByID []graph.Node
func (n ByID) Len() int { return len(n) }
func (n ByID) Less(i, j int) bool { return n[i].ID() < n[j].ID() }
func (n ByID) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
// BySliceValues implements the sort.Interface sorting a slice of
// []int lexically by the values of the []int.
type BySliceValues [][]int
func (c BySliceValues) Len() int { return len(c) }
func (c BySliceValues) Less(i, j int) bool {
a, b := c[i], c[j]
l := len(a)
if len(b) < l {
l = len(b)
}
for k, v := range a[:l] {
if v < b[k] {
return true
}
if v > b[k] {
return false
}
}
return len(a) < len(b)
}
func (c BySliceValues) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
// BySliceIDs implements the sort.Interface sorting a slice of
// []graph.Node lexically by the IDs of the []graph.Node.
type BySliceIDs [][]graph.Node
func (c BySliceIDs) Len() int { return len(c) }
func (c BySliceIDs) Less(i, j int) bool {
a, b := c[i], c[j]
l := len(a)
if len(b) < l {
l = len(b)
}
for k, v := range a[:l] {
if v.ID() < b[k].ID() {
return true
}
if v.ID() > b[k].ID() {
return false
}
}
return len(a) < len(b)
}
func (c BySliceIDs) Swap(i, j int) { c[i], c[j] = c[j], c[i] }

View File

@@ -0,0 +1,18 @@
// Copyright ©2014 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.
//+build !appengine
package set
import "unsafe"
// same determines whether two sets are backed by the same store. In the
// current implementation using hash maps it makes use of the fact that
// hash maps are passed as a pointer to a runtime Hmap struct. A map is
// not seen by the runtime as a pointer though, so we use unsafe to get
// the maps' pointer values to compare.
func same(a, b Nodes) bool {
return *(*uintptr)(unsafe.Pointer(&a)) == *(*uintptr)(unsafe.Pointer(&b))
}

View File

@@ -0,0 +1,18 @@
// Copyright ©2014 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.
//+build appengine
package set
import "reflect"
// same determines whether two sets are backed by the same store. In the
// current implementation using hash maps it makes use of the fact that
// hash maps are passed as a pointer to a runtime Hmap struct. A map is
// not seen by the runtime as a pointer though, so we use reflect to get
// the maps' pointer values to compare.
func same(a, b Nodes) bool {
return reflect.ValueOf(a).Pointer() == reflect.ValueOf(b).Pointer()
}

190
graph/internal/set/set.go Normal file
View File

@@ -0,0 +1,190 @@
// Copyright ©2014 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 set provides integer and graph.Node sets.
package set
import "github.com/gonum/graph"
// Ints is a set of integer identifiers.
type Ints map[int]struct{}
// The simple accessor methods for Ints are provided to allow ease of
// implementation change should the need arise.
// Add inserts an element into the set.
func (s Ints) Add(e int) {
s[e] = struct{}{}
}
// Has reports the existence of the element in the set.
func (s Ints) Has(e int) bool {
_, ok := s[e]
return ok
}
// Remove deletes the specified element from the set.
func (s Ints) Remove(e int) {
delete(s, e)
}
// Count reports the number of elements stored in the set.
func (s Ints) Count() int {
return len(s)
}
// Nodes is a set of nodes keyed in their integer identifiers.
type Nodes map[int]graph.Node
// The simple accessor methods for Nodes are provided to allow ease of
// implementation change should the need arise.
// Add inserts an element into the set.
func (s Nodes) Add(n graph.Node) {
s[n.ID()] = n
}
// Remove deletes the specified element from the set.
func (s Nodes) Remove(e graph.Node) {
delete(s, e.ID())
}
// Has reports the existence of the element in the set.
func (s Nodes) Has(n graph.Node) bool {
_, ok := s[n.ID()]
return ok
}
// clear clears the set, possibly using the same backing store.
func (s *Nodes) clear() {
if len(*s) != 0 {
*s = make(Nodes)
}
}
// Copy performs a perfect copy from src to dst (meaning the sets will
// be equal).
func (dst Nodes) Copy(src Nodes) Nodes {
if same(src, dst) {
return dst
}
if len(dst) > 0 {
dst = make(Nodes, len(src))
}
for e, n := range src {
dst[e] = n
}
return dst
}
// Equal reports set equality between the parameters. Sets are equal if
// and only if they have the same elements.
func Equal(a, b Nodes) bool {
if same(a, b) {
return true
}
if len(a) != len(b) {
return false
}
for e := range a {
if _, ok := b[e]; !ok {
return false
}
}
return true
}
// Union takes the union of a and b, and stores it in dst.
//
// The union of two sets, a and b, is the set containing all the
// elements of each, for instance:
//
// {a,b,c} UNION {d,e,f} = {a,b,c,d,e,f}
//
// Since sets may not have repetition, unions of two sets that overlap
// do not contain repeat elements, that is:
//
// {a,b,c} UNION {b,c,d} = {a,b,c,d}
//
func (dst Nodes) Union(a, b Nodes) Nodes {
if same(a, b) {
return dst.Copy(a)
}
if !same(a, dst) && !same(b, dst) {
dst.clear()
}
if !same(dst, a) {
for e, n := range a {
dst[e] = n
}
}
if !same(dst, b) {
for e, n := range b {
dst[e] = n
}
}
return dst
}
// Intersect takes the intersection of a and b, and stores it in dst.
//
// The intersection of two sets, a and b, is the set containing all
// the elements shared between the two sets, for instance:
//
// {a,b,c} INTERSECT {b,c,d} = {b,c}
//
// The intersection between a set and itself is itself, and thus
// effectively a copy operation:
//
// {a,b,c} INTERSECT {a,b,c} = {a,b,c}
//
// The intersection between two sets that share no elements is the empty
// set:
//
// {a,b,c} INTERSECT {d,e,f} = {}
//
func (dst Nodes) Intersect(a, b Nodes) Nodes {
var swap Nodes
if same(a, b) {
return dst.Copy(a)
}
if same(a, dst) {
swap = b
} else if same(b, dst) {
swap = a
} else {
dst.clear()
if len(a) > len(b) {
a, b = b, a
}
for e, n := range a {
if _, ok := b[e]; ok {
dst[e] = n
}
}
return dst
}
for e := range dst {
if _, ok := swap[e]; !ok {
delete(dst, e)
}
}
return dst
}

View File

@@ -0,0 +1,413 @@
// Copyright ©2014 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 set
import "testing"
type node int
func (n node) ID() int { return int(n) }
// count reports the number of elements stored in the node set.
func (s Nodes) count() int {
return len(s)
}
// TestSame tests the assumption that pointer equality via unsafe conversion
// of a map[int]struct{} to uintptr is a valid test for perfect identity between
// set values. If any of the tests in TestSame fail, the package is broken and same
// must be reimplemented to conform to the runtime map implementation. The relevant
// code to look at (at least for gc) is in runtime/hashmap.{h,goc}.
func TestSame(t *testing.T) {
var (
a = make(Nodes)
b = make(Nodes)
c = a
)
if same(a, b) {
t.Error("Independently created sets test as same")
}
if !same(a, c) {
t.Error("Set copy and original test as not same.")
}
a.Add(node(1))
if !same(a, c) {
t.Error("Set copy and original test as not same after addition.")
}
if !same(nil, nil) {
t.Error("nil sets test as not same.")
}
if same(b, nil) {
t.Error("nil and empty sets test as same.")
}
}
func TestAdd(t *testing.T) {
s := make(Nodes)
if s == nil {
t.Fatal("Set cannot be created successfully")
}
if s.count() != 0 {
t.Error("Set somehow contains new elements upon creation")
}
s.Add(node(1))
s.Add(node(3))
s.Add(node(5))
if s.count() != 3 {
t.Error("Incorrect number of set elements after adding")
}
if !s.Has(node(1)) || !s.Has(node(3)) || !s.Has(node(5)) {
t.Error("Set doesn't contain element that was added")
}
s.Add(node(1))
if s.count() > 3 {
t.Error("Set double-adds element (element not unique)")
} else if s.count() < 3 {
t.Error("Set double-add lowered len")
}
if !s.Has(node(1)) {
t.Error("Set doesn't contain double-added element")
}
if !s.Has(node(3)) || !s.Has(node(5)) {
t.Error("Set removes element on double-add")
}
for e, n := range s {
if e != n.ID() {
t.Error("Element ID did not match key: %d != %d", e, n.ID())
}
}
}
func TestRemove(t *testing.T) {
s := make(Nodes)
s.Add(node(1))
s.Add(node(3))
s.Add(node(5))
s.Remove(node(1))
if s.count() != 2 {
t.Error("Incorrect number of set elements after removing an element")
}
if s.Has(node(1)) {
t.Error("Element present after removal")
}
if !s.Has(node(3)) || !s.Has(node(5)) {
t.Error("Set remove removed wrong element")
}
s.Remove(node(1))
if s.count() != 2 || s.Has(node(1)) {
t.Error("Double set remove does something strange")
}
s.Add(node(1))
if s.count() != 3 || !s.Has(node(1)) {
t.Error("Cannot add element after removal")
}
}
func TestClear(t *testing.T) {
s := make(Nodes)
s.Add(node(8))
s.Add(node(9))
s.Add(node(10))
s.clear()
if s.count() != 0 {
t.Error("clear did not properly reset set to size 0")
}
}
func TestSelfEqual(t *testing.T) {
s := make(Nodes)
if !Equal(s, s) {
t.Error("Set is not equal to itself")
}
s.Add(node(1))
if !Equal(s, s) {
t.Error("Set ceases self equality after adding element")
}
}
func TestEqual(t *testing.T) {
a := make(Nodes)
b := make(Nodes)
if !Equal(a, b) {
t.Error("Two different empty sets not equal")
}
a.Add(node(1))
if Equal(a, b) {
t.Error("Two different sets with different elements not equal")
}
b.Add(node(1))
if !Equal(a, b) {
t.Error("Two sets with same element not equal")
}
}
func TestCopy(t *testing.T) {
a := make(Nodes)
b := make(Nodes)
a.Add(node(1))
a.Add(node(2))
a.Add(node(3))
b.Copy(a)
if !Equal(a, b) {
t.Fatalf("Two sets not equal after copy")
}
b.Remove(node(1))
if Equal(a, b) {
t.Errorf("Mutating one set mutated another after copy")
}
}
func TestSelfCopy(t *testing.T) {
a := make(Nodes)
a.Add(node(1))
a.Add(node(2))
a.Copy(a)
if a.count() != 2 {
t.Error("Something strange happened when copying into self")
}
}
func TestUnionSame(t *testing.T) {
a := make(Nodes)
b := make(Nodes)
c := make(Nodes)
a.Add(node(1))
a.Add(node(2))
b.Add(node(1))
b.Add(node(2))
c.Union(a, b)
if c.count() != 2 {
t.Error("Union of same sets yields set with wrong len")
}
if !c.Has(node(1)) || !c.Has(node(2)) {
t.Error("Union of same sets yields wrong elements")
}
for i, s := range []Nodes{a, b, c} {
for e, n := range s {
if e != n.ID() {
t.Error("Element ID did not match key in s%d: %d != %d", i+1, e, n.ID())
}
}
}
}
func TestUnionDiff(t *testing.T) {
a := make(Nodes)
b := make(Nodes)
c := make(Nodes)
a.Add(node(1))
a.Add(node(2))
b.Add(node(3))
c.Union(a, b)
if c.count() != 3 {
t.Error("Union of different sets yields set with wrong len")
}
if !c.Has(node(1)) || !c.Has(node(2)) || !c.Has(node(3)) {
t.Error("Union of different sets yields set with wrong elements")
}
if a.Has(node(3)) || !a.Has(node(2)) || !a.Has(node(1)) || a.count() != 2 {
t.Error("Union of sets mutates non-destination set (argument 1)")
}
if !b.Has(node(3)) || b.Has(node(1)) || b.Has(node(2)) || b.count() != 1 {
t.Error("Union of sets mutates non-destination set (argument 2)")
}
for i, s := range []Nodes{a, b, c} {
for e, n := range s {
if e != n.ID() {
t.Error("Element ID did not match key in s%d: %d != %d", i+1, e, n.ID())
}
}
}
}
func TestUnionOverlapping(t *testing.T) {
a := make(Nodes)
b := make(Nodes)
c := make(Nodes)
a.Add(node(1))
a.Add(node(2))
b.Add(node(2))
b.Add(node(3))
c.Union(a, b)
if c.count() != 3 {
t.Error("Union of overlapping sets yields set with wrong len")
}
if !c.Has(node(1)) || !c.Has(node(2)) || !c.Has(node(3)) {
t.Error("Union of overlapping sets yields set with wrong elements")
}
if a.Has(node(3)) || !a.Has(node(2)) || !a.Has(node(1)) || a.count() != 2 {
t.Error("Union of sets mutates non-destination set (argument 1)")
}
if !b.Has(node(3)) || b.Has(node(1)) || !b.Has(node(2)) || b.count() != 2 {
t.Error("Union of sets mutates non-destination set (argument 2)")
}
for i, s := range []Nodes{a, b, c} {
for e, n := range s {
if e != n.ID() {
t.Error("Element ID did not match key in s%d: %d != %d", i+1, e, n.ID())
}
}
}
}
func TestIntersectSame(t *testing.T) {
a := make(Nodes)
b := make(Nodes)
c := make(Nodes)
a.Add(node(2))
a.Add(node(3))
b.Add(node(2))
b.Add(node(3))
c.Intersect(a, b)
if card := c.count(); card != 2 {
t.Errorf("Intersection of identical sets yields set of wrong len %d", card)
}
if !c.Has(node(2)) || !c.Has(node(3)) {
t.Error("Intersection of identical sets yields set of wrong elements")
}
for i, s := range []Nodes{a, b, c} {
for e, n := range s {
if e != n.ID() {
t.Error("Element ID did not match key in s%d: %d != %d", i+1, e, n.ID())
}
}
}
}
func TestIntersectDiff(t *testing.T) {
a := make(Nodes)
b := make(Nodes)
c := make(Nodes)
a.Add(node(2))
a.Add(node(3))
b.Add(node(1))
b.Add(node(4))
c.Intersect(a, b)
if card := c.count(); card != 0 {
t.Errorf("Intersection of different yields non-empty set %d", card)
}
if !a.Has(node(2)) || !a.Has(node(3)) || a.Has(node(1)) || a.Has(node(4)) || a.count() != 2 {
t.Error("Intersection of sets mutates non-destination set (argument 1)")
}
if b.Has(node(2)) || b.Has(node(3)) || !b.Has(node(1)) || !b.Has(node(4)) || b.count() != 2 {
t.Error("Intersection of sets mutates non-destination set (argument 1)")
}
for i, s := range []Nodes{a, b, c} {
for e, n := range s {
if e != n.ID() {
t.Error("Element ID did not match key in s%d: %d != %d", i+1, e, n.ID())
}
}
}
}
func TestIntersectOverlapping(t *testing.T) {
a := make(Nodes)
b := make(Nodes)
c := make(Nodes)
a.Add(node(2))
a.Add(node(3))
b.Add(node(3))
b.Add(node(4))
c.Intersect(a, b)
if card := c.count(); card != 1 {
t.Errorf("Intersection of overlapping sets yields set of incorrect len %d", card)
}
if !c.Has(node(3)) {
t.Errorf("Intersection of overlapping sets yields set with wrong element")
}
if !a.Has(node(2)) || !a.Has(node(3)) || a.Has(node(4)) || a.count() != 2 {
t.Error("Intersection of sets mutates non-destination set (argument 1)")
}
if b.Has(node(2)) || !b.Has(node(3)) || !b.Has(node(4)) || b.count() != 2 {
t.Error("Intersection of sets mutates non-destination set (argument 1)")
}
for i, s := range []Nodes{a, b, c} {
for e, n := range s {
if e != n.ID() {
t.Error("Element ID did not match key in s%d: %d != %d", i+1, e, n.ID())
}
}
}
}

View File

@@ -0,0 +1,256 @@
// 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 network
import (
"math"
"github.com/gonum/graph"
"github.com/gonum/graph/internal/linear"
"github.com/gonum/graph/path"
)
// Betweenness returns the non-zero betweenness centrality for nodes in the unweighted graph g.
//
// C_B(v) = \sum_{s ≠ v ≠ t ∈ V} (\sigma_{st}(v) / \sigma_{st})
//
// where \sigma_{st} and \sigma_{st}(v) are the number of shortest paths from s to t,
// and the subset of those paths containing v respectively.
func Betweenness(g graph.Graph) map[int]float64 {
// Brandes' algorithm for finding betweenness centrality for nodes in
// and unweighted graph:
//
// http://www.inf.uni-konstanz.de/algo/publications/b-fabc-01.pdf
// TODO(kortschak): Consider using the parallel algorithm when
// GOMAXPROCS != 1.
//
// http://htor.inf.ethz.ch/publications/img/edmonds-hoefler-lumsdaine-bc.pdf
// Also note special case for sparse networks:
// http://wwwold.iit.cnr.it/staff/marco.pellegrini/papiri/asonam-final.pdf
cb := make(map[int]float64)
brandes(g, func(s graph.Node, stack linear.NodeStack, p map[int][]graph.Node, delta, sigma map[int]float64) {
for stack.Len() != 0 {
w := stack.Pop()
for _, v := range p[w.ID()] {
delta[v.ID()] += sigma[v.ID()] / sigma[w.ID()] * (1 + delta[w.ID()])
}
if w.ID() != s.ID() {
if d := delta[w.ID()]; d != 0 {
cb[w.ID()] += d
}
}
}
})
return cb
}
// EdgeBetweenness returns the non-zero betweenness centrality for edges in the
// unweighted graph g. For an edge e the centrality C_B is computed as
//
// C_B(e) = \sum_{s ≠ t ∈ V} (\sigma_{st}(e) / \sigma_{st}),
//
// where \sigma_{st} and \sigma_{st}(e) are the number of shortest paths from s
// to t, and the subset of those paths containing e, respectively.
//
// If g is undirected, edges are retained such that u.ID < v.ID where u and v are
// the nodes of e.
func EdgeBetweenness(g graph.Graph) map[[2]int]float64 {
// Modified from Brandes' original algorithm as described in Algorithm 7
// with the exception that node betweenness is not calculated:
//
// http://algo.uni-konstanz.de/publications/b-vspbc-08.pdf
_, isUndirected := g.(graph.Undirected)
cb := make(map[[2]int]float64)
brandes(g, func(s graph.Node, stack linear.NodeStack, p map[int][]graph.Node, delta, sigma map[int]float64) {
for stack.Len() != 0 {
w := stack.Pop()
for _, v := range p[w.ID()] {
c := sigma[v.ID()] / sigma[w.ID()] * (1 + delta[w.ID()])
vid := v.ID()
wid := w.ID()
if isUndirected && wid < vid {
vid, wid = wid, vid
}
cb[[2]int{vid, wid}] += c
delta[v.ID()] += c
}
}
})
return cb
}
// brandes is the common code for Betweenness and EdgeBetweenness. It corresponds
// to algorithm 1 in http://algo.uni-konstanz.de/publications/b-vspbc-08.pdf with
// the accumulation loop provided by the accumulate closure.
func brandes(g graph.Graph, accumulate func(s graph.Node, stack linear.NodeStack, p map[int][]graph.Node, delta, sigma map[int]float64)) {
var (
nodes = g.Nodes()
stack linear.NodeStack
p = make(map[int][]graph.Node, len(nodes))
sigma = make(map[int]float64, len(nodes))
d = make(map[int]int, len(nodes))
delta = make(map[int]float64, len(nodes))
queue linear.NodeQueue
)
for _, s := range nodes {
stack = stack[:0]
for _, w := range nodes {
p[w.ID()] = p[w.ID()][:0]
}
for _, t := range nodes {
sigma[t.ID()] = 0
d[t.ID()] = -1
}
sigma[s.ID()] = 1
d[s.ID()] = 0
queue.Enqueue(s)
for queue.Len() != 0 {
v := queue.Dequeue()
stack.Push(v)
for _, w := range g.From(v) {
// w found for the first time?
if d[w.ID()] < 0 {
queue.Enqueue(w)
d[w.ID()] = d[v.ID()] + 1
}
// shortest path to w via v?
if d[w.ID()] == d[v.ID()]+1 {
sigma[w.ID()] += sigma[v.ID()]
p[w.ID()] = append(p[w.ID()], v)
}
}
}
for _, v := range nodes {
delta[v.ID()] = 0
}
// S returns vertices in order of non-increasing distance from s
accumulate(s, stack, p, delta, sigma)
}
}
// WeightedGraph is a graph with edge weights.
type WeightedGraph interface {
graph.Graph
graph.Weighter
}
// BetweennessWeighted returns the non-zero betweenness centrality for nodes in the weighted
// graph g used to construct the given shortest paths.
//
// C_B(v) = \sum_{s ≠ v ≠ t ∈ V} (\sigma_{st}(v) / \sigma_{st})
//
// where \sigma_{st} and \sigma_{st}(v) are the number of shortest paths from s to t,
// and the subset of those paths containing v respectively.
func BetweennessWeighted(g WeightedGraph, p path.AllShortest) map[int]float64 {
cb := make(map[int]float64)
nodes := g.Nodes()
for i, s := range nodes {
for j, t := range nodes {
if i == j {
continue
}
d := p.Weight(s, t)
if math.IsInf(d, 0) {
continue
}
// If we have a unique path, don't do the
// extra work needed to get all paths.
path, _, unique := p.Between(s, t)
if unique {
for _, v := range path[1 : len(path)-1] {
// For undirected graphs we double count
// passage though nodes. This is consistent
// with Brandes' algorithm's behaviour.
cb[v.ID()]++
}
continue
}
// Otherwise iterate over all paths.
paths, _ := p.AllBetween(s, t)
stFrac := 1 / float64(len(paths))
for _, path := range paths {
for _, v := range path[1 : len(path)-1] {
cb[v.ID()] += stFrac
}
}
}
}
return cb
}
// EdgeBetweennessWeighted returns the non-zero betweenness centrality for edges in
// the weighted graph g. For an edge e the centrality C_B is computed as
//
// C_B(e) = \sum_{s ≠ t ∈ V} (\sigma_{st}(e) / \sigma_{st}),
//
// where \sigma_{st} and \sigma_{st}(e) are the number of shortest paths from s
// to t, and the subset of those paths containing e, respectively.
//
// If g is undirected, edges are retained such that u.ID < v.ID where u and v are
// the nodes of e.
func EdgeBetweennessWeighted(g WeightedGraph, p path.AllShortest) map[[2]int]float64 {
cb := make(map[[2]int]float64)
_, isUndirected := g.(graph.Undirected)
nodes := g.Nodes()
for i, s := range nodes {
for j, t := range nodes {
if i == j {
continue
}
d := p.Weight(s, t)
if math.IsInf(d, 0) {
continue
}
// If we have a unique path, don't do the
// extra work needed to get all paths.
path, _, unique := p.Between(s, t)
if unique {
for k, v := range path[1:] {
// For undirected graphs we double count
// passage though edges. This is consistent
// with Brandes' algorithm's behaviour.
uid := path[k].ID()
vid := v.ID()
if isUndirected && vid < uid {
uid, vid = vid, uid
}
cb[[2]int{uid, vid}]++
}
continue
}
// Otherwise iterate over all paths.
paths, _ := p.AllBetween(s, t)
stFrac := 1 / float64(len(paths))
for _, path := range paths {
for k, v := range path[1:] {
uid := path[k].ID()
vid := v.ID()
if isUndirected && vid < uid {
uid, vid = vid, uid
}
cb[[2]int{uid, vid}] += stFrac
}
}
}
}
return cb
}

View File

@@ -0,0 +1,340 @@
// 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 network
import (
"fmt"
"math"
"sort"
"testing"
"github.com/gonum/floats"
"github.com/gonum/graph/path"
"github.com/gonum/graph/simple"
)
var betweennessTests = []struct {
g []set
wantTol float64
want map[int]float64
wantEdges map[[2]int]float64
}{
{
// Example graph from http://en.wikipedia.org/wiki/File:PageRanks-Example.svg 16:17, 8 July 2009
g: []set{
A: nil,
B: linksTo(C),
C: linksTo(B),
D: linksTo(A, B),
E: linksTo(D, B, F),
F: linksTo(B, E),
G: linksTo(B, E),
H: linksTo(B, E),
I: linksTo(B, E),
J: linksTo(E),
K: linksTo(E),
},
wantTol: 1e-1,
want: map[int]float64{
B: 32,
D: 18,
E: 48,
},
wantEdges: map[[2]int]float64{
[2]int{A, D}: 20,
[2]int{B, C}: 20,
[2]int{B, D}: 16,
[2]int{B, E}: 12,
[2]int{B, F}: 9,
[2]int{B, G}: 9,
[2]int{B, H}: 9,
[2]int{B, I}: 9,
[2]int{D, E}: 20,
[2]int{E, F}: 11,
[2]int{E, G}: 11,
[2]int{E, H}: 11,
[2]int{E, I}: 11,
[2]int{E, J}: 20,
[2]int{E, K}: 20,
},
},
{
// Example graph from http://en.wikipedia.org/w/index.php?title=PageRank&oldid=659286279#Power_Method
g: []set{
A: linksTo(B, C),
B: linksTo(D),
C: linksTo(D, E),
D: linksTo(E),
E: linksTo(A),
},
wantTol: 1e-3,
want: map[int]float64{
A: 2,
B: 0.6667,
C: 0.6667,
D: 2,
E: 0.6667,
},
wantEdges: map[[2]int]float64{
[2]int{A, B}: 2 + 2/3. + 4/2.,
[2]int{A, C}: 2 + 2/3. + 2/2.,
[2]int{A, E}: 2 + 2/3. + 2/2.,
[2]int{B, D}: 2 + 2/3. + 4/2.,
[2]int{C, D}: 2 + 2/3. + 2/2.,
[2]int{C, E}: 2,
[2]int{D, E}: 2 + 2/3. + 2/2.,
},
},
{
g: []set{
A: linksTo(B),
B: linksTo(C),
C: nil,
},
wantTol: 1e-3,
want: map[int]float64{
B: 2,
},
wantEdges: map[[2]int]float64{
[2]int{A, B}: 4,
[2]int{B, C}: 4,
},
},
{
g: []set{
A: linksTo(B),
B: linksTo(C),
C: linksTo(D),
D: linksTo(E),
E: nil,
},
wantTol: 1e-3,
want: map[int]float64{
B: 6,
C: 8,
D: 6,
},
wantEdges: map[[2]int]float64{
[2]int{A, B}: 8,
[2]int{B, C}: 12,
[2]int{C, D}: 12,
[2]int{D, E}: 8,
},
},
{
g: []set{
A: linksTo(C),
B: linksTo(C),
C: nil,
D: linksTo(C),
E: linksTo(C),
},
wantTol: 1e-3,
want: map[int]float64{
C: 12,
},
wantEdges: map[[2]int]float64{
[2]int{A, C}: 8,
[2]int{B, C}: 8,
[2]int{C, D}: 8,
[2]int{C, E}: 8,
},
},
{
g: []set{
A: linksTo(B, C, D, E),
B: linksTo(C, D, E),
C: linksTo(D, E),
D: linksTo(E),
E: nil,
},
wantTol: 1e-3,
want: map[int]float64{},
wantEdges: map[[2]int]float64{
[2]int{A, B}: 2,
[2]int{A, C}: 2,
[2]int{A, D}: 2,
[2]int{A, E}: 2,
[2]int{B, C}: 2,
[2]int{B, D}: 2,
[2]int{B, E}: 2,
[2]int{C, D}: 2,
[2]int{C, E}: 2,
[2]int{D, E}: 2,
},
},
}
func TestBetweenness(t *testing.T) {
for i, test := range betweennessTests {
g := simple.NewUndirectedGraph(0, math.Inf(1))
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
// Weight omitted to show weight-independence.
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 0})
}
}
got := Betweenness(g)
prec := 1 - int(math.Log10(test.wantTol))
for n := range test.g {
wantN, gotOK := got[n]
gotN, wantOK := test.want[n]
if gotOK != wantOK {
t.Errorf("unexpected betweenness result for test %d, node %c", i, n+'A')
}
if !floats.EqualWithinAbsOrRel(gotN, wantN, test.wantTol, test.wantTol) {
t.Errorf("unexpected betweenness result for test %d:\ngot: %v\nwant:%v",
i, orderedFloats(got, prec), orderedFloats(test.want, prec))
break
}
}
}
}
func TestEdgeBetweenness(t *testing.T) {
for i, test := range betweennessTests {
g := simple.NewUndirectedGraph(0, math.Inf(1))
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
// Weight omitted to show weight-independence.
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 0})
}
}
got := EdgeBetweenness(g)
prec := 1 - int(math.Log10(test.wantTol))
outer:
for u := range test.g {
for v := range test.g {
wantQ, gotOK := got[[2]int{u, v}]
gotQ, wantOK := test.wantEdges[[2]int{u, v}]
if gotOK != wantOK {
t.Errorf("unexpected betweenness result for test %d, edge (%c,%c)", i, u+'A', v+'A')
}
if !floats.EqualWithinAbsOrRel(gotQ, wantQ, test.wantTol, test.wantTol) {
t.Errorf("unexpected betweenness result for test %d:\ngot: %v\nwant:%v",
i, orderedPairFloats(got, prec), orderedPairFloats(test.wantEdges, prec))
break outer
}
}
}
}
}
func TestBetweennessWeighted(t *testing.T) {
for i, test := range betweennessTests {
g := simple.NewUndirectedGraph(0, math.Inf(1))
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
p, ok := path.FloydWarshall(g)
if !ok {
t.Errorf("unexpected negative cycle in test %d", i)
continue
}
got := BetweennessWeighted(g, p)
prec := 1 - int(math.Log10(test.wantTol))
for n := range test.g {
gotN, gotOK := got[n]
wantN, wantOK := test.want[n]
if gotOK != wantOK {
t.Errorf("unexpected betweenness existence for test %d, node %c", i, n+'A')
}
if !floats.EqualWithinAbsOrRel(gotN, wantN, test.wantTol, test.wantTol) {
t.Errorf("unexpected betweenness result for test %d:\ngot: %v\nwant:%v",
i, orderedFloats(got, prec), orderedFloats(test.want, prec))
break
}
}
}
}
func TestEdgeBetweennessWeighted(t *testing.T) {
for i, test := range betweennessTests {
g := simple.NewUndirectedGraph(0, math.Inf(1))
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
p, ok := path.FloydWarshall(g)
if !ok {
t.Errorf("unexpected negative cycle in test %d", i)
continue
}
got := EdgeBetweennessWeighted(g, p)
prec := 1 - int(math.Log10(test.wantTol))
outer:
for u := range test.g {
for v := range test.g {
wantQ, gotOK := got[[2]int{u, v}]
gotQ, wantOK := test.wantEdges[[2]int{u, v}]
if gotOK != wantOK {
t.Errorf("unexpected betweenness result for test %d, edge (%c,%c)", i, u+'A', v+'A')
}
if !floats.EqualWithinAbsOrRel(gotQ, wantQ, test.wantTol, test.wantTol) {
t.Errorf("unexpected betweenness result for test %d:\ngot: %v\nwant:%v",
i, orderedPairFloats(got, prec), orderedPairFloats(test.wantEdges, prec))
break outer
}
}
}
}
}
func orderedPairFloats(w map[[2]int]float64, prec int) []pairKeyFloatVal {
o := make(orderedPairFloatsMap, 0, len(w))
for k, v := range w {
o = append(o, pairKeyFloatVal{prec: prec, key: k, val: v})
}
sort.Sort(o)
return o
}
type pairKeyFloatVal struct {
prec int
key [2]int
val float64
}
func (kv pairKeyFloatVal) String() string {
return fmt.Sprintf("(%c,%c):%.*f", kv.key[0]+'A', kv.key[1]+'A', kv.prec, kv.val)
}
type orderedPairFloatsMap []pairKeyFloatVal
func (o orderedPairFloatsMap) Len() int { return len(o) }
func (o orderedPairFloatsMap) Less(i, j int) bool {
return o[i].key[0] < o[j].key[0] || (o[i].key[0] == o[j].key[0] && o[i].key[1] < o[j].key[1])
}
func (o orderedPairFloatsMap) Swap(i, j int) { o[i], o[j] = o[j], o[i] }

124
graph/network/distance.go Normal file
View File

@@ -0,0 +1,124 @@
// 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 network
import (
"math"
"github.com/gonum/graph"
"github.com/gonum/graph/path"
)
// Closeness returns the closeness centrality for nodes in the graph g used to
// construct the given shortest paths.
//
// C(v) = 1 / \sum_u d(u,v)
//
// For directed graphs the incoming paths are used. Infinite distances are
// not considered.
func Closeness(g graph.Graph, p path.AllShortest) map[int]float64 {
nodes := g.Nodes()
c := make(map[int]float64, len(nodes))
for _, u := range nodes {
var sum float64
for _, v := range nodes {
// The ordering here is not relevant for
// undirected graphs, but we make sure we
// are counting incoming paths.
d := p.Weight(v, u)
if math.IsInf(d, 0) {
continue
}
sum += d
}
c[u.ID()] = 1 / sum
}
return c
}
// Farness returns the farness for nodes in the graph g used to construct
// the given shortest paths.
//
// F(v) = \sum_u d(u,v)
//
// For directed graphs the incoming paths are used. Infinite distances are
// not considered.
func Farness(g graph.Graph, p path.AllShortest) map[int]float64 {
nodes := g.Nodes()
f := make(map[int]float64, len(nodes))
for _, u := range nodes {
var sum float64
for _, v := range nodes {
// The ordering here is not relevant for
// undirected graphs, but we make sure we
// are counting incoming paths.
d := p.Weight(v, u)
if math.IsInf(d, 0) {
continue
}
sum += d
}
f[u.ID()] = sum
}
return f
}
// Harmonic returns the harmonic centrality for nodes in the graph g used to
// construct the given shortest paths.
//
// H(v)= \sum_{u ≠ v} 1 / d(u,v)
//
// For directed graphs the incoming paths are used. Infinite distances are
// not considered.
func Harmonic(g graph.Graph, p path.AllShortest) map[int]float64 {
nodes := g.Nodes()
h := make(map[int]float64, len(nodes))
for i, u := range nodes {
var sum float64
for j, v := range nodes {
// The ordering here is not relevant for
// undirected graphs, but we make sure we
// are counting incoming paths.
d := p.Weight(v, u)
if math.IsInf(d, 0) {
continue
}
if i != j {
sum += 1 / d
}
}
h[u.ID()] = sum
}
return h
}
// Residual returns the Dangalchev's residual closeness for nodes in the graph
// g used to construct the given shortest paths.
//
// C(v)= \sum_{u ≠ v} 1 / 2^d(u,v)
//
// For directed graphs the incoming paths are used. Infinite distances are
// not considered.
func Residual(g graph.Graph, p path.AllShortest) map[int]float64 {
nodes := g.Nodes()
r := make(map[int]float64, len(nodes))
for i, u := range nodes {
var sum float64
for j, v := range nodes {
// The ordering here is not relevant for
// undirected graphs, but we make sure we
// are counting incoming paths.
d := p.Weight(v, u)
if math.IsInf(d, 0) {
continue
}
if i != j {
sum += math.Exp2(-d)
}
}
r[u.ID()] = sum
}
return r
}

View File

@@ -0,0 +1,394 @@
// 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 network
import (
"math"
"testing"
"github.com/gonum/floats"
"github.com/gonum/graph/path"
"github.com/gonum/graph/simple"
)
var undirectedCentralityTests = []struct {
g []set
farness map[int]float64
harmonic map[int]float64
residual map[int]float64
}{
{
g: []set{
A: linksTo(B),
B: linksTo(C),
C: nil,
},
farness: map[int]float64{
A: 1 + 2,
B: 1 + 1,
C: 2 + 1,
},
harmonic: map[int]float64{
A: 1 + 1.0/2.0,
B: 1 + 1,
C: 1.0/2.0 + 1,
},
residual: map[int]float64{
A: 1/math.Exp2(1) + 1/math.Exp2(2),
B: 1/math.Exp2(1) + 1/math.Exp2(1),
C: 1/math.Exp2(2) + 1/math.Exp2(1),
},
},
{
g: []set{
A: linksTo(B),
B: linksTo(C),
C: linksTo(D),
D: linksTo(E),
E: nil,
},
farness: map[int]float64{
A: 1 + 2 + 3 + 4,
B: 1 + 1 + 2 + 3,
C: 2 + 1 + 1 + 2,
D: 3 + 2 + 1 + 1,
E: 4 + 3 + 2 + 1,
},
harmonic: map[int]float64{
A: 1 + 1.0/2.0 + 1.0/3.0 + 1.0/4.0,
B: 1 + 1 + 1.0/2.0 + 1.0/3.0,
C: 1.0/2.0 + 1 + 1 + 1.0/2.0,
D: 1.0/3.0 + 1.0/2.0 + 1 + 1,
E: 1.0/4.0 + 1.0/3.0 + 1.0/2.0 + 1,
},
residual: map[int]float64{
A: 1/math.Exp2(1) + 1/math.Exp2(2) + 1/math.Exp2(3) + 1/math.Exp2(4),
B: 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(2) + 1/math.Exp2(3),
C: 1/math.Exp2(2) + 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(2),
D: 1/math.Exp2(3) + 1/math.Exp2(2) + 1/math.Exp2(1) + 1/math.Exp2(1),
E: 1/math.Exp2(4) + 1/math.Exp2(3) + 1/math.Exp2(2) + 1/math.Exp2(1),
},
},
{
g: []set{
A: linksTo(C),
B: linksTo(C),
C: nil,
D: linksTo(C),
E: linksTo(C),
},
farness: map[int]float64{
A: 2 + 2 + 1 + 2,
B: 2 + 1 + 2 + 2,
C: 1 + 1 + 1 + 1,
D: 2 + 1 + 2 + 2,
E: 2 + 2 + 1 + 2,
},
harmonic: map[int]float64{
A: 1.0/2.0 + 1.0/2.0 + 1 + 1.0/2.0,
B: 1.0/2.0 + 1 + 1.0/2.0 + 1.0/2.0,
C: 1 + 1 + 1 + 1,
D: 1.0/2.0 + 1 + 1.0/2.0 + 1.0/2.0,
E: 1.0/2.0 + 1.0/2.0 + 1 + 1.0/2.0,
},
residual: map[int]float64{
A: 1/math.Exp2(2) + 1/math.Exp2(2) + 1/math.Exp2(1) + 1/math.Exp2(2),
B: 1/math.Exp2(2) + 1/math.Exp2(1) + 1/math.Exp2(2) + 1/math.Exp2(2),
C: 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1),
D: 1/math.Exp2(2) + 1/math.Exp2(1) + 1/math.Exp2(2) + 1/math.Exp2(2),
E: 1/math.Exp2(2) + 1/math.Exp2(2) + 1/math.Exp2(1) + 1/math.Exp2(2),
},
},
{
g: []set{
A: linksTo(B, C, D, E),
B: linksTo(C, D, E),
C: linksTo(D, E),
D: linksTo(E),
E: nil,
},
farness: map[int]float64{
A: 1 + 1 + 1 + 1,
B: 1 + 1 + 1 + 1,
C: 1 + 1 + 1 + 1,
D: 1 + 1 + 1 + 1,
E: 1 + 1 + 1 + 1,
},
harmonic: map[int]float64{
A: 1 + 1 + 1 + 1,
B: 1 + 1 + 1 + 1,
C: 1 + 1 + 1 + 1,
D: 1 + 1 + 1 + 1,
E: 1 + 1 + 1 + 1,
},
residual: map[int]float64{
A: 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1),
B: 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1),
C: 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1),
D: 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1),
E: 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1),
},
},
}
func TestDistanceCentralityUndirected(t *testing.T) {
const tol = 1e-12
prec := 1 - int(math.Log10(tol))
for i, test := range undirectedCentralityTests {
g := simple.NewUndirectedGraph(0, math.Inf(1))
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
p, ok := path.FloydWarshall(g)
if !ok {
t.Errorf("unexpected negative cycle in test %d", i)
continue
}
var got map[int]float64
got = Closeness(g, p)
for n := range test.g {
if !floats.EqualWithinAbsOrRel(got[n], 1/test.farness[n], tol, tol) {
want := make(map[int]float64)
for n, v := range test.farness {
want[n] = 1 / v
}
t.Errorf("unexpected closeness centrality for test %d:\ngot: %v\nwant:%v",
i, orderedFloats(got, prec), orderedFloats(want, prec))
break
}
}
got = Farness(g, p)
for n := range test.g {
if !floats.EqualWithinAbsOrRel(got[n], test.farness[n], tol, tol) {
t.Errorf("unexpected farness for test %d:\ngot: %v\nwant:%v",
i, orderedFloats(got, prec), orderedFloats(test.farness, prec))
break
}
}
got = Harmonic(g, p)
for n := range test.g {
if !floats.EqualWithinAbsOrRel(got[n], test.harmonic[n], tol, tol) {
t.Errorf("unexpected harmonic centrality for test %d:\ngot: %v\nwant:%v",
i, orderedFloats(got, prec), orderedFloats(test.harmonic, prec))
break
}
}
got = Residual(g, p)
for n := range test.g {
if !floats.EqualWithinAbsOrRel(got[n], test.residual[n], tol, tol) {
t.Errorf("unexpected residual closeness for test %d:\ngot: %v\nwant:%v",
i, orderedFloats(got, prec), orderedFloats(test.residual, prec))
break
}
}
}
}
var directedCentralityTests = []struct {
g []set
farness map[int]float64
harmonic map[int]float64
residual map[int]float64
}{
{
g: []set{
A: linksTo(B),
B: linksTo(C),
C: nil,
},
farness: map[int]float64{
A: 0,
B: 1,
C: 2 + 1,
},
harmonic: map[int]float64{
A: 0,
B: 1,
C: 1.0/2.0 + 1,
},
residual: map[int]float64{
A: 0,
B: 1 / math.Exp2(1),
C: 1/math.Exp2(2) + 1/math.Exp2(1),
},
},
{
g: []set{
A: linksTo(B),
B: linksTo(C),
C: linksTo(D),
D: linksTo(E),
E: nil,
},
farness: map[int]float64{
A: 0,
B: 1,
C: 2 + 1,
D: 3 + 2 + 1,
E: 4 + 3 + 2 + 1,
},
harmonic: map[int]float64{
A: 0,
B: 1,
C: 1.0/2.0 + 1,
D: 1.0/3.0 + 1.0/2.0 + 1,
E: 1.0/4.0 + 1.0/3.0 + 1.0/2.0 + 1,
},
residual: map[int]float64{
A: 0,
B: 1 / math.Exp2(1),
C: 1/math.Exp2(2) + 1/math.Exp2(1),
D: 1/math.Exp2(3) + 1/math.Exp2(2) + 1/math.Exp2(1),
E: 1/math.Exp2(4) + 1/math.Exp2(3) + 1/math.Exp2(2) + 1/math.Exp2(1),
},
},
{
g: []set{
A: linksTo(C),
B: linksTo(C),
C: nil,
D: linksTo(C),
E: linksTo(C),
},
farness: map[int]float64{
A: 0,
B: 0,
C: 1 + 1 + 1 + 1,
D: 0,
E: 0,
},
harmonic: map[int]float64{
A: 0,
B: 0,
C: 1 + 1 + 1 + 1,
D: 0,
E: 0,
},
residual: map[int]float64{
A: 0,
B: 0,
C: 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1),
D: 0,
E: 0,
},
},
{
g: []set{
A: linksTo(B, C, D, E),
B: linksTo(C, D, E),
C: linksTo(D, E),
D: linksTo(E),
E: nil,
},
farness: map[int]float64{
A: 0,
B: 1,
C: 1 + 1,
D: 1 + 1 + 1,
E: 1 + 1 + 1 + 1,
},
harmonic: map[int]float64{
A: 0,
B: 1,
C: 1 + 1,
D: 1 + 1 + 1,
E: 1 + 1 + 1 + 1,
},
residual: map[int]float64{
A: 0,
B: 1 / math.Exp2(1),
C: 1/math.Exp2(1) + 1/math.Exp2(1),
D: 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1),
E: 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1) + 1/math.Exp2(1),
},
},
}
func TestDistanceCentralityDirected(t *testing.T) {
const tol = 1e-12
prec := 1 - int(math.Log10(tol))
for i, test := range directedCentralityTests {
g := simple.NewDirectedGraph(0, math.Inf(1))
for u, e := range test.g {
// Add nodes that are not defined by an edge.
if !g.Has(simple.Node(u)) {
g.AddNode(simple.Node(u))
}
for v := range e {
g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v), W: 1})
}
}
p, ok := path.FloydWarshall(g)
if !ok {
t.Errorf("unexpected negative cycle in test %d", i)
continue
}
var got map[int]float64
got = Closeness(g, p)
for n := range test.g {
if !floats.EqualWithinAbsOrRel(got[n], 1/test.farness[n], tol, tol) {
want := make(map[int]float64)
for n, v := range test.farness {
want[n] = 1 / v
}
t.Errorf("unexpected closeness centrality for test %d:\ngot: %v\nwant:%v",
i, orderedFloats(got, prec), orderedFloats(want, prec))
break
}
}
got = Farness(g, p)
for n := range test.g {
if !floats.EqualWithinAbsOrRel(got[n], test.farness[n], tol, tol) {
t.Errorf("unexpected farness for test %d:\ngot: %v\nwant:%v",
i, orderedFloats(got, prec), orderedFloats(test.farness, prec))
break
}
}
got = Harmonic(g, p)
for n := range test.g {
if !floats.EqualWithinAbsOrRel(got[n], test.harmonic[n], tol, tol) {
t.Errorf("unexpected harmonic centrality for test %d:\ngot: %v\nwant:%v",
i, orderedFloats(got, prec), orderedFloats(test.harmonic, prec))
break
}
}
got = Residual(g, p)
for n := range test.g {
if !floats.EqualWithinAbsOrRel(got[n], test.residual[n], tol, tol) {
t.Errorf("unexpected residual closeness for test %d:\ngot: %v\nwant:%v",
i, orderedFloats(got, prec), orderedFloats(test.residual, prec))
break
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More