func TestCommunityQ(t *testing.T) { for _, test := range communityQTests { 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(got) != 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 TestDenseLists(t *testing.T) { dg := NewDirectedMatrix(15, 1, 0, math.Inf(1)) nodes := dg.Nodes() if len(nodes) != 15 { t.Fatalf("Wrong number of nodes") } sort.Sort(ordered.ByID(nodes)) for i, node := range dg.Nodes() { if i != node.ID() { t.Errorf("Node list doesn't return properly id'd nodes") } } edges := dg.Edges() if len(edges) != 15*14 { t.Errorf("Improper number of edges for passable dense graph") } dg.RemoveEdge(Edge{F: Node(12), T: Node(11)}) edges = dg.Edges() if len(edges) != (15*14)-1 { t.Errorf("Removing edge didn't affect edge listing properly") } }
// NewDirectedMatrixFrom creates a directed dense graph with the given nodes. // The IDs of the nodes must be contiguous from 0 to len(nodes)-1, but may // be in any order. If IDs are not contiguous NewDirectedMatrixFrom will panic. // All edges are initialized with the weight given by init. The self parameter // specifies the cost of self connection, and absent specifies the weight // returned for absent edges. func NewDirectedMatrixFrom(nodes []graph.Node, init, self, absent float64) *DirectedMatrix { sort.Sort(ordered.ByID(nodes)) for i, n := range nodes { if i != n.ID() { panic("simple: non-contiguous node IDs") } } g := NewDirectedMatrix(len(nodes), init, self, absent) g.nodes = nodes return g }
func TestReduceQConsistency(t *testing.T) { tests: for _, test := range communityQTests { 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 := reduce(g, nil) cg0Qnull := Q(cg0, cg0.Structure(), 1) if !floats.EqualWithinAbsOrRel(gQnull, cg0Qnull, structure.tol, structure.tol) { t.Errorf("disgagreement 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 conversion: got: %v want :%v", gQ, cg0Q) } cg1 := reduce(cg0, communities) cg1Q := Q(cg1, cg1.Structure(), structure.resolution) if !floats.EqualWithinAbsOrRel(gQ, cg1Q, structure.tol, structure.tol) { t.Errorf("unexpected Q result after initial condensation: got: %v want :%v", gQ, cg1Q) } } } }
func TestMoveLocal(t *testing.T) { for _, test := range localMoveTests { 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 := reduce(reduce(g, nil), communities) l := newLocalMover(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) } } } } } }
// Sort performs a topological sort of the directed graph g returning the 'from' to 'to' // sort order. If a topological ordering is not possible, an Unorderable error is returned // listing cyclic components in g with each cyclic component's members sorted by ID. When // an Unorderable error is returned, each cyclic component's topological position within // the sorted nodes is marked with a nil graph.Node. func Sort(g graph.Directed) (sorted []graph.Node, err error) { sccs := TarjanSCC(g) sorted = make([]graph.Node, 0, len(sccs)) var sc Unorderable for _, s := range sccs { if len(s) != 1 { sort.Sort(ordered.ByID(s)) sc = append(sc, s) sorted = append(sorted, nil) continue } sorted = append(sorted, s[0]) } if sc != nil { for i, j := 0, len(sc)-1; i < j; i, j = i+1, j-1 { sc[i], sc[j] = sc[j], sc[i] } err = sc } reverse(sorted) return sorted, err }
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\xa0")) // 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 }
// 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 }
// reduce returns a reduced graph constructed from g divided // into the given communities. The communities value is mutated // by the call to reduce. If communities is nil and g is a // ReducedUndirected, it is returned unaltered. func reduce(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 := weightFuncFor(g) r := ReducedUndirected{ nodes: make([]community, len(nodes)), 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)), 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 := weightFuncFor(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 }
func TestLouvain(t *testing.T) { const louvainIterations = 20 for _, test := range communityQTests { 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) ) // Louvain 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) 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() { 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() { 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 = reduce(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 TestCommunityDeltaQ(t *testing.T) { tests: for _, test := range communityQTests { 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 := newLocalMover(reduce(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) } } } } }