refactor engine: decompose Holme-Kim into builder methods
The generator's working state (graph, attachment pool, rng) moves into a holmeKimBuilder struct so each algorithm step is a small named method: attachNewNode, link, pickMutualFriend, degreeProportionalSample. Max nesting drops from five levels to two. The RNG call order is untouched, and the golden test (99/70/7 reached) plus the fixed-seed determinism test prove behaviour is bit-for-bit identical. Go bits: pointer receivers ((b *holmeKimBuilder)) let methods mutate the builder; (value, ok) multiple returns are the idiom for 'may not exist', as in pickMutualFriend.
This commit is contained in:
parent
45fdf7ffa4
commit
e73db0bdd4
1 changed files with 86 additions and 61 deletions
|
|
@ -26,77 +26,102 @@ func HolmeKim(numNodes, edgesPerNode int, triangleProb float64, rng *rand.Rand)
|
||||||
return nil, fmt.Errorf("holme-kim: need 0 <= triangleProb <= 1, got %v", triangleProb)
|
return nil, fmt.Errorf("holme-kim: need 0 <= triangleProb <= 1, got %v", triangleProb)
|
||||||
}
|
}
|
||||||
|
|
||||||
graph := NewGraph(numNodes)
|
builder := holmeKimBuilder{graph: NewGraph(numNodes), rng: rng}
|
||||||
|
// Seed the pool with the first edgesPerNode nodes so the earliest
|
||||||
// One entry per edge endpoint, so sampling uniformly from this list is
|
// arrivals have someone to connect to.
|
||||||
// sampling nodes proportionally to their degree: that is the whole
|
for node := range edgesPerNode {
|
||||||
// "preferential attachment" trick. Seeded with the first edgesPerNode
|
builder.pool = append(builder.pool, node)
|
||||||
// nodes so the earliest arrivals have someone to connect to.
|
|
||||||
attachmentPool := make([]int, edgesPerNode)
|
|
||||||
for node := range attachmentPool {
|
|
||||||
attachmentPool[node] = node
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for newNode := edgesPerNode; newNode < numNodes; newNode++ {
|
for newNode := edgesPerNode; newNode < numNodes; newNode++ {
|
||||||
// Where this node could attach: edgesPerNode distinct existing
|
builder.attachNewNode(newNode, edgesPerNode, triangleProb)
|
||||||
// nodes, drawn degree-proportionally. Consumed from the end.
|
|
||||||
candidates := degreeProportionalSample(attachmentPool, edgesPerNode, rng)
|
|
||||||
|
|
||||||
target := candidates[len(candidates)-1]
|
|
||||||
candidates = candidates[:len(candidates)-1]
|
|
||||||
graph.AddEdge(newNode, target)
|
|
||||||
attachmentPool = append(attachmentPool, target)
|
|
||||||
|
|
||||||
for edgesAdded := 1; edgesAdded < edgesPerNode; {
|
|
||||||
// Triangle step: with probability triangleProb, also link to a
|
|
||||||
// friend of the node we just attached to.
|
|
||||||
if rng.Float64() < triangleProb {
|
|
||||||
var mutualCandidates []int
|
|
||||||
for _, friendOfTarget := range graph.Neighbors(target) {
|
|
||||||
if friendOfTarget != newNode && !graph.HasEdge(newNode, friendOfTarget) {
|
|
||||||
mutualCandidates = append(mutualCandidates, friendOfTarget)
|
|
||||||
}
|
}
|
||||||
}
|
return builder.graph, nil
|
||||||
if len(mutualCandidates) > 0 {
|
}
|
||||||
mutualFriend := mutualCandidates[rng.IntN(len(mutualCandidates))]
|
|
||||||
graph.AddEdge(newNode, mutualFriend)
|
// holmeKimBuilder carries the generator's working state so each step of the
|
||||||
attachmentPool = append(attachmentPool, mutualFriend)
|
// algorithm reads as a small named method instead of one deeply nested loop.
|
||||||
edgesAdded++
|
type holmeKimBuilder struct {
|
||||||
|
graph *Graph
|
||||||
|
// pool holds one entry per edge endpoint, so sampling uniformly from it
|
||||||
|
// picks nodes proportionally to their degree: that is the whole
|
||||||
|
// "preferential attachment" trick.
|
||||||
|
pool []int
|
||||||
|
rng *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
// attachNewNode links newNode to edgesPerNode existing nodes: preferential
|
||||||
|
// attachment by default, a friend-of-a-friend triangle with probability
|
||||||
|
// triangleProb.
|
||||||
|
func (b *holmeKimBuilder) attachNewNode(newNode, edgesPerNode int, triangleProb float64) {
|
||||||
|
// Where this node could attach: edgesPerNode distinct existing nodes,
|
||||||
|
// drawn degree-proportionally, consumed from the end.
|
||||||
|
candidates := b.degreeProportionalSample(edgesPerNode)
|
||||||
|
|
||||||
|
target := popLast(&candidates)
|
||||||
|
b.link(newNode, target)
|
||||||
|
|
||||||
|
for edgesAdded := 1; edgesAdded < edgesPerNode; edgesAdded++ {
|
||||||
|
if b.rng.Float64() < triangleProb {
|
||||||
|
if mutualFriend, found := b.pickMutualFriend(newNode, target); found {
|
||||||
|
b.link(newNode, mutualFriend)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Otherwise (or if no triangle was possible): plain
|
// Plain preferential attachment. Mirrors networkx, including the
|
||||||
// preferential attachment to the next candidate. Mirrors
|
// quirk that a candidate already linked via a triangle step counts
|
||||||
// networkx, including the quirk that a candidate already linked
|
// as an attempt without adding an edge, so a node can end up with
|
||||||
// via a triangle step counts as an attempt without adding an
|
// slightly fewer than edgesPerNode edges.
|
||||||
// edge, so a node can end up with slightly fewer than
|
target = popLast(&candidates)
|
||||||
// edgesPerNode edges.
|
b.link(newNode, target)
|
||||||
target = candidates[len(candidates)-1]
|
|
||||||
candidates = candidates[:len(candidates)-1]
|
|
||||||
graph.AddEdge(newNode, target)
|
|
||||||
attachmentPool = append(attachmentPool, target)
|
|
||||||
edgesAdded++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The new node enters the pool once per edge slot, like networkx.
|
// The new node enters the pool once per edge slot, like networkx.
|
||||||
for range edgesPerNode {
|
for range edgesPerNode {
|
||||||
attachmentPool = append(attachmentPool, newNode)
|
b.pool = append(b.pool, newNode)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return graph, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// degreeProportionalSample draws sampleSize distinct nodes from the pool.
|
// link adds the edge and records the endpoint in the attachment pool,
|
||||||
// The pool holds one entry per edge endpoint, so nodes with more edges are
|
// raising target's future attachment odds.
|
||||||
// proportionally more likely to be drawn. networkx returns a Python set
|
func (b *holmeKimBuilder) link(newNode, target int) {
|
||||||
// here; we keep a slice in draw order so the result is deterministic.
|
b.graph.AddEdge(newNode, target)
|
||||||
func degreeProportionalSample(pool []int, sampleSize int, rng *rand.Rand) []int {
|
b.pool = append(b.pool, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pickMutualFriend draws a random neighbour of target that newNode is not
|
||||||
|
// already connected to; closing that link forms a triangle. found is false
|
||||||
|
// when no such neighbour exists.
|
||||||
|
func (b *holmeKimBuilder) pickMutualFriend(newNode, target int) (mutualFriend int, found bool) {
|
||||||
|
var candidates []int
|
||||||
|
for _, friendOfTarget := range b.graph.Neighbors(target) {
|
||||||
|
if friendOfTarget != newNode && !b.graph.HasEdge(newNode, friendOfTarget) {
|
||||||
|
candidates = append(candidates, friendOfTarget)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return candidates[b.rng.IntN(len(candidates))], true
|
||||||
|
}
|
||||||
|
|
||||||
|
// degreeProportionalSample draws sampleSize distinct nodes from the pool;
|
||||||
|
// nodes with more edges appear more often there, so they are
|
||||||
|
// proportionally more likely. networkx returns a Python set here; we keep
|
||||||
|
// a slice in draw order so the result is deterministic.
|
||||||
|
func (b *holmeKimBuilder) degreeProportionalSample(sampleSize int) []int {
|
||||||
sample := make([]int, 0, sampleSize)
|
sample := make([]int, 0, sampleSize)
|
||||||
for len(sample) < sampleSize {
|
for len(sample) < sampleSize {
|
||||||
drawn := pool[rng.IntN(len(pool))]
|
drawn := b.pool[b.rng.IntN(len(b.pool))]
|
||||||
if !slices.Contains(sample, drawn) {
|
if !slices.Contains(sample, drawn) {
|
||||||
sample = append(sample, drawn)
|
sample = append(sample, drawn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sample
|
return sample
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// popLast removes and returns the last element of the slice.
|
||||||
|
func popLast(stack *[]int) int {
|
||||||
|
last := (*stack)[len(*stack)-1]
|
||||||
|
*stack = (*stack)[:len(*stack)-1]
|
||||||
|
return last
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue