mirror of
https://github.com/gonum/gonum.git
synced 2025-10-05 15:16:59 +08:00
249 lines
6.9 KiB
Go
249 lines
6.9 KiB
Go
// 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/v2"
|
|
|
|
"gonum.org/v1/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.Source) 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.Source) 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
|
|
}
|