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

Commit

Permalink
community: simplify modularization API
Browse files Browse the repository at this point in the history
* Provide a single entry point to Q and modularization for each of
  simplex or multiplex graphs.
* Rename Louvain* => Modularise*.
  • Loading branch information
kortschak committed Aug 12, 2016
1 parent d73488d commit 6dc2c91
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 32 deletions.
116 changes: 115 additions & 1 deletion community/louvain_common.go
Expand Up @@ -7,6 +7,7 @@ package community

import (
"fmt"
"math/rand"

"github.com/gonum/graph"
)
Expand Down Expand Up @@ -35,6 +36,56 @@ func Q(g graph.Graph, communities [][]graph.Node, resolution float64) float64 {
}
}

// 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.
// 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.
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
Expand Down Expand Up @@ -62,7 +113,7 @@ type Multiplex interface {
// negative edge weight.
//
// If g is undirected, Q is calculated according to
// Q = \sum_{layer} w_{layer} \sum_{ij} [ A_{layer}*_{ij} - (\gamma_{layer} k_i k_j)/2m ] \delta(c_i,c_j)
// Q = \sum_{layer} w_{layer} \sum_{ij} [ A_{layer}*_{ij} - (\gamma_{layer} k_i k_j)/2m ] \delta(c_i,c_j).
//
// Explicitly directed graph modularization is not yet implemented.
//
Expand Down Expand Up @@ -92,6 +143,69 @@ func QMultiplex(g Multiplex, communities [][]graph.Node, weights, resolutions []
}
}

// 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.
// 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.
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_{layer} w_{layer} \sum_{ij} [ A_{layer}*_{ij} - (\gamma_{layer} k_i k_j)/2m ] \delta(c_i,c_j).
//
// Explicitly directed graph modularization is not yet implemented.
//
// 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 louvainMultiplex(g, weights, resolutions, all, src)
case interface {
Multiplex
Layer(int) graph.Directed
}:
// TODO(kortschak): Implement DirectedMultiplex handling.
panic("community: multiplex directed graph modularization not implemented")
default:
panic(fmt.Sprintf("community: invalid graph type: %T", g))
}
}

// undirectedEdges is the edge structure of a reduced graph.
type undirectedEdges struct {
// edges and weights is the set
Expand Down
7 changes: 4 additions & 3 deletions community/louvain_directed.go
Expand Up @@ -69,11 +69,11 @@ func qDirected(g graph.Directed, communities [][]graph.Node, resolution float64)
return q / m
}

// LouvainDirected returns the hierarchical modularization of g at the given
// 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. Louvain will panic if g has any edge with negative
// edge weight.
func LouvainDirected(g graph.Directed, resolution float64, src *rand.Rand) *ReducedDirected {
func louvainDirected(g graph.Directed, resolution float64, src *rand.Rand) ReducedGraph {
// See louvain.tex for a detailed description
// of the algorithm used here.

Expand Down Expand Up @@ -119,6 +119,7 @@ type ReducedDirected struct {
var (
_ graph.Directed = (*ReducedDirected)(nil)
_ graph.Weighter = (*ReducedDirected)(nil)
_ ReducedGraph = (*ReducedUndirected)(nil)
)

// Communities returns the community memberships of the nodes in the
Expand Down Expand Up @@ -160,7 +161,7 @@ func (g *ReducedDirected) Structure() [][]graph.Node {

// Expanded returns the next lower level of the module clustering or nil
// if at the lowest level.
func (g *ReducedDirected) Expanded() *ReducedDirected {
func (g *ReducedDirected) Expanded() ReducedGraph {
return g.parent
}

Expand Down
14 changes: 7 additions & 7 deletions community/louvain_directed_test.go
Expand Up @@ -474,7 +474,7 @@ func TestMoveLocalDirected(t *testing.T) {
}
}

func TestLouvainDirected(t *testing.T) {
func TestModularizeDirected(t *testing.T) {
const louvainIterations = 20

for _, test := range communityDirectedQTests {
Expand Down Expand Up @@ -505,11 +505,11 @@ func TestLouvainDirected(t *testing.T) {
got *ReducedDirected
bestQ = math.Inf(-1)
)
// Louvain is randomised so we do this to
// 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 := LouvainDirected(g, 1, src)
r := Modularize(g, 1, src).(*ReducedDirected)
if q := Q(r, nil, 1); q > bestQ || math.IsNaN(q) {
bestQ = q
got = r
Expand All @@ -521,7 +521,7 @@ func TestLouvainDirected(t *testing.T) {
}

var qs []float64
for p := r; p != nil; p = p.Expanded() {
for p := r; p != nil; p = p.Expanded().(*ReducedDirected) {
qs = append(qs, Q(p, nil, 1))
}

Expand All @@ -543,7 +543,7 @@ func TestLouvainDirected(t *testing.T) {
}

var levels []level
for p := got; p != nil; p = p.Expanded() {
for p := got; p != nil; p = p.Expanded().(*ReducedDirected) {
var communities [][]graph.Node
if p.parent != nil {
communities = p.parent.Communities()
Expand Down Expand Up @@ -583,13 +583,13 @@ func TestNonContiguousDirected(t *testing.T) {
t.Error("unexpected panic with non-contiguous ID range")
}
}()
LouvainDirected(g, 1, nil)
Modularize(g, 1, nil)
}()
}

func BenchmarkLouvainDirected(b *testing.B) {
src := rand.New(rand.NewSource(1))
for i := 0; i < b.N; i++ {
LouvainDirected(dupGraphDirected, 1, src)
Modularize(dupGraphDirected, 1, src)
}
}
12 changes: 7 additions & 5 deletions community/louvain_undirected.go
Expand Up @@ -67,12 +67,13 @@ func qUndirected(g graph.Undirected, communities [][]graph.Node, resolution floa
return q / m2
}

// Louvain 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. Louvain will panic if g has any edge with negative edge weight.
// 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. Louvain 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 Louvain(g graph.Undirected, resolution float64, src *rand.Rand) *ReducedUndirected {
func louvainUndirected(g graph.Undirected, resolution float64, src *rand.Rand) *ReducedUndirected {
// See louvain.tex for a detailed description
// of the algorithm used here.

Expand Down Expand Up @@ -113,6 +114,7 @@ type ReducedUndirected struct {
var (
_ graph.Undirected = (*ReducedUndirected)(nil)
_ graph.Weighter = (*ReducedUndirected)(nil)
_ ReducedGraph = (*ReducedUndirected)(nil)
)

// Communities returns the community memberships of the nodes in the
Expand Down Expand Up @@ -154,7 +156,7 @@ func (g *ReducedUndirected) Structure() [][]graph.Node {

// Expanded returns the next lower level of the module clustering or nil
// if at the lowest level.
func (g *ReducedUndirected) Expanded() *ReducedUndirected {
func (g *ReducedUndirected) Expanded() ReducedGraph {
return g.parent
}

Expand Down
6 changes: 3 additions & 3 deletions community/louvain_undirected_multiplex.go
Expand Up @@ -154,14 +154,14 @@ 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] }

// LouvainMultiplex returns the hierarchical modularization of g at the given resolution
// louvainMultiplex 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. LouvainMultiplex 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 LouvainMultiplex(g UndirectedMultiplex, weights, resolutions []float64, all bool, src *rand.Rand) *ReducedUndirectedMultiplex {
func louvainMultiplex(g UndirectedMultiplex, weights, resolutions []float64, all bool, src *rand.Rand) *ReducedUndirectedMultiplex {
if weights != nil && len(weights) != g.Depth() {
panic("community: weights vector length mismatch")
}
Expand Down Expand Up @@ -268,7 +268,7 @@ func (g *ReducedUndirectedMultiplex) Structure() [][]graph.Node {

// Expanded returns the next lower level of the module clustering or nil
// if at the lowest level.
func (g *ReducedUndirectedMultiplex) Expanded() *ReducedUndirectedMultiplex {
func (g *ReducedUndirectedMultiplex) Expanded() ReducedMultiplex {
return g.parent
}

Expand Down
12 changes: 6 additions & 6 deletions community/louvain_undirected_multiplex_test.go
Expand Up @@ -496,11 +496,11 @@ func TestLouvainMultiplex(t *testing.T) {
got *ReducedUndirectedMultiplex
bestQ = math.Inf(-1)
)
// Louvain is randomised so we do this to
// 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 := LouvainMultiplex(g, weights, nil, true, src)
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
Expand All @@ -512,7 +512,7 @@ func TestLouvainMultiplex(t *testing.T) {
}

var qs []float64
for p := r; p != nil; p = p.Expanded() {
for p := r; p != nil; p = p.Expanded().(*ReducedUndirectedMultiplex) {
qs = append(qs, floats.Sum(QMultiplex(p, nil, weights, nil)))
}

Expand All @@ -534,7 +534,7 @@ func TestLouvainMultiplex(t *testing.T) {
}

var levels []level
for p := got; p != nil; p = p.Expanded() {
for p := got; p != nil; p = p.Expanded().(*ReducedUndirectedMultiplex) {
var communities [][]graph.Node
if p.parent != nil {
communities = p.parent.Communities()
Expand Down Expand Up @@ -574,14 +574,14 @@ func TestNonContiguousUndirectedMultiplex(t *testing.T) {
t.Error("unexpected panic with non-contiguous ID range")
}
}()
LouvainMultiplex(UndirectedLayers{g}, nil, nil, true, nil)
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++ {
LouvainMultiplex(UndirectedLayers{dupGraph}, nil, nil, true, src)
ModularizeMultiplex(UndirectedLayers{dupGraph}, nil, nil, true, src)
}
}

Expand Down
14 changes: 7 additions & 7 deletions community/louvain_undirected_test.go
Expand Up @@ -527,7 +527,7 @@ func TestMoveLocalUndirected(t *testing.T) {
}
}

func TestLouvain(t *testing.T) {
func TestModularizeUndirected(t *testing.T) {
const louvainIterations = 20

for _, test := range communityUndirectedQTests {
Expand Down Expand Up @@ -558,11 +558,11 @@ func TestLouvain(t *testing.T) {
got *ReducedUndirected
bestQ = math.Inf(-1)
)
// Louvain is randomised so we do this to
// 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 := Louvain(g, 1, src)
r := Modularize(g, 1, src).(*ReducedUndirected)
if q := Q(r, nil, 1); q > bestQ || math.IsNaN(q) {
bestQ = q
got = r
Expand All @@ -574,7 +574,7 @@ func TestLouvain(t *testing.T) {
}

var qs []float64
for p := r; p != nil; p = p.Expanded() {
for p := r; p != nil; p = p.Expanded().(*ReducedUndirected) {
qs = append(qs, Q(p, nil, 1))
}

Expand All @@ -596,7 +596,7 @@ func TestLouvain(t *testing.T) {
}

var levels []level
for p := got; p != nil; p = p.Expanded() {
for p := got; p != nil; p = p.Expanded().(*ReducedUndirected) {
var communities [][]graph.Node
if p.parent != nil {
communities = p.parent.Communities()
Expand Down Expand Up @@ -636,13 +636,13 @@ func TestNonContiguousUndirected(t *testing.T) {
t.Error("unexpected panic with non-contiguous ID range")
}
}()
Louvain(g, 1, nil)
Modularize(g, 1, nil)
}()
}

func BenchmarkLouvain(b *testing.B) {
src := rand.New(rand.NewSource(1))
for i := 0; i < b.N; i++ {
Louvain(dupGraph, 1, src)
Modularize(dupGraph, 1, src)
}
}

0 comments on commit 6dc2c91

Please sign in to comment.