engine: edge thresholds and the independent cascade
One random threshold per DIRECTED edge, drawn once per world: scenarios sharing thresholds differ only in who is educated, the prototype's 'same world, different lever' trick. The cascade is plain BFS in rounds; an educated student receives the fake but never forwards it, which is the entire effect of the lever. The result stores the first-reached round per node (exactly what a frontend animation needs). Tests hand-craft a 4-node line graph with exact thresholds, so every expectation is exact: spread, directional blocking, educated cutoff.
This commit is contained in:
parent
7f06ea0d6f
commit
736cd9070d
2 changed files with 187 additions and 0 deletions
85
internal/engine/cascade.go
Normal file
85
internal/engine/cascade.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package engine
|
||||
|
||||
import "math/rand/v2"
|
||||
|
||||
// EdgeThresholds holds one random draw in [0, 1) per directed edge. The
|
||||
// cascade forwards across u -> v when the draw is below the forwarding
|
||||
// probability. Drawing all thresholds once and reusing them recreates the
|
||||
// prototype's "same world, different lever" comparison: scenarios that
|
||||
// share thresholds differ only in who is educated.
|
||||
type EdgeThresholds map[[2]int]float64
|
||||
|
||||
// NewEdgeThresholds draws a threshold for every directed edge. Both
|
||||
// directions get independent draws (u forwarding to v is a different event
|
||||
// than v forwarding to u). Node order makes the draws deterministic.
|
||||
func NewEdgeThresholds(graph *Graph, rng *rand.Rand) EdgeThresholds {
|
||||
thresholds := make(EdgeThresholds, 2*graph.NumEdges())
|
||||
for node := range graph.NumNodes() {
|
||||
for _, neighbor := range graph.Neighbors(node) {
|
||||
if node < neighbor { // visit each undirected edge exactly once
|
||||
thresholds[[2]int{node, neighbor}] = rng.Float64()
|
||||
thresholds[[2]int{neighbor, node}] = rng.Float64()
|
||||
}
|
||||
}
|
||||
}
|
||||
return thresholds
|
||||
}
|
||||
|
||||
// NeverReached marks a node the fake never got to.
|
||||
const NeverReached = -1
|
||||
|
||||
// CascadeResult describes one finished spread.
|
||||
type CascadeResult struct {
|
||||
// ReachedAtRound[node] is the round in which node first received the
|
||||
// fake (round 0 is the origin posting it), or NeverReached. This is
|
||||
// exactly what an animation needs: the activation time per node.
|
||||
ReachedAtRound []int
|
||||
NumReached int
|
||||
NumRounds int
|
||||
}
|
||||
|
||||
// RunCascade spreads the fake from origin: in every round, each newly
|
||||
// reached student forwards to each neighbour whose edge threshold falls
|
||||
// below forwardProb. Educated students receive the fake but never forward
|
||||
// it; that is the entire effect of the education lever.
|
||||
func RunCascade(graph *Graph, origin int, forwardProb float64, educated []int, thresholds EdgeThresholds) CascadeResult {
|
||||
isEducated := make([]bool, graph.NumNodes())
|
||||
for _, student := range educated {
|
||||
isEducated[student] = true
|
||||
}
|
||||
|
||||
reachedAtRound := make([]int, graph.NumNodes())
|
||||
for node := range reachedAtRound {
|
||||
reachedAtRound[node] = NeverReached
|
||||
}
|
||||
reachedAtRound[origin] = 0
|
||||
|
||||
numReached := 1
|
||||
lastRound := 0
|
||||
frontier := []int{origin}
|
||||
for round := 1; len(frontier) > 0; round++ {
|
||||
var nextFrontier []int
|
||||
for _, forwarder := range frontier {
|
||||
if isEducated[forwarder] {
|
||||
continue // received the fake, refuses to pass it on
|
||||
}
|
||||
for _, receiver := range graph.Neighbors(forwarder) {
|
||||
alreadyReached := reachedAtRound[receiver] != NeverReached
|
||||
forwards := thresholds[[2]int{forwarder, receiver}] < forwardProb
|
||||
if !alreadyReached && forwards {
|
||||
reachedAtRound[receiver] = round
|
||||
numReached++
|
||||
lastRound = round
|
||||
nextFrontier = append(nextFrontier, receiver)
|
||||
}
|
||||
}
|
||||
}
|
||||
frontier = nextFrontier
|
||||
}
|
||||
|
||||
return CascadeResult{
|
||||
ReachedAtRound: reachedAtRound,
|
||||
NumReached: numReached,
|
||||
NumRounds: lastRound + 1,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue