// Neighbor-counting and common-neighbor analytics: the neighbor histogram,// the pairwise common-neighborhood set, and the masked A^2 / link-// prediction primitive (per-source distinct two-hop endpoint counts,// folded in parallel). Split from kernels.go, which holds the// roots-via / functional-relationship kernels.
package chickpeasimport ()// NeighborCounts is the histogram of neighbor nodes reached from sources// via the m-matched rels in dir (how many of the sources point to each// neighbor). Counts into pooled generation-stamped dense scratch.func ( *Snapshot) ( []NodeID, Direction, RelMatch) map[NodeID]int { := int(.CSRIDSpace()) := takeScratch()deferscratchPool.Put()var []NodeIDfor , := range {for := range .NeighborsMatch(, , ) {if .gen[] != .cur { .gen[] = .cur .dist[] = 0 = append(, ) } .dist[]++ } } := make(map[NodeID]int, len())for , := range { [] = int(.dist[]) }return}// CommonNeighbors is N(a) ∩ N(b) over the m-matched rels in dir (Both// gives the undirected common neighborhood) -- the link-prediction// primitive, returned as a set so it composes with And/Or/AndNot.func ( *Snapshot) (, NodeID, Direction, RelMatch) *nodeset.Set { , := roaring.New(), roaring.New()for := range .NeighborsMatch(, , ) { .Add() }for := range .NeighborsMatch(, , ) { .Add() }returnnodeset.FromBitmap(roaring.And(, ))}// CommonNeighborCount is one (source, target, count) triple of// CommonNeighborCounts.typeCommonNeighborCountstruct { Source, Target NodeID Count uint64}// pairScratch is the per-worker scratch behind CommonNeighborCounts: gen-// stamped counts plus two dedup stamps (per-source mids, per-mid targets).type pairScratch struct { val []uint64 gen, seenMid []uint32 tSeen []uint32 cur, tCur uint32 touched []NodeID}var pairScratchPool = sync.Pool{New: func() any { return &pairScratch{} }}// CommonNeighborCounts is the masked A^2 / link-prediction primitive: for// each s in sources, count the DISTINCT mid nodes on two-hop paths// s -[m]- mid -[m]- t whose endpoint t is in targets, emitting one// (s, t, count) per reached pair (count > 0 only). count is |N(s) ∩ N(t)|// over de-duplicated neighborhoods -- exact even when the rel is stored// bidirectionally and traversed Both -- not a walk multiplicity. (s, s)// self-pairs ARE emitted; callers wanting distinct endpoints filter them.// Sources are processed in parallel (chunk results concatenated in source// order, so output order is deterministic); counting scatters from each// source through its mids (a "push"), far cheaper than per-pair set// intersections when few pairs share a neighbor.// cncChunk is the pair accumulator's slab size: fixed chunks replace the// per-worker doubling ladder (each doubling discarded its predecessor,// dominating the primitive's allocation profile), and the final assembly// is one exact-size copy.const cncChunk = 4096// cncAcc is one worker's accumulation state: the worker-held scratch// (one pool round-trip per worker, not per source) and the ordered pair// slabs.type cncAcc struct { sc *pairScratch full [][]CommonNeighborCount cur []CommonNeighborCount}// seal closes the open slab into the ordered list.func ( *cncAcc) () {iflen(.cur) > 0 { .full = append(.full, .cur) .cur = nil }}func ( *Snapshot) ( []NodeID, Direction, RelMatch, *nodeset.Set) []CommonNeighborCount { := int(.CSRIDSpace()) := parallel.Fold(len(),func() *cncAcc { return &cncAcc{sc: pairScratchPool.Get().(*pairScratch)} },func( *cncAcc, int) *cncAcc { := [] := .sciflen(.val) < { .val = make([]uint64, ) .gen = make([]uint32, ) .seenMid = make([]uint32, ) .tSeen = make([]uint32, ) .cur, .tCur = 0, 0 } .cur++if .cur == 0 {clear(.gen)clear(.seenMid) .cur = 1 } := .cur .touched = .touched[:0]for := range .NeighborsMatch(, , ) {// First-hop dedup: process each distinct mid once.if .seenMid[] == {continue } .seenMid[] = .tCur++if .tCur == 0 {clear(.tSeen) .tCur = 1 } := .tCurfor := range .NeighborsMatch(, , ) {if !.Contains() {continue }// Second-hop dedup: bump each distinct endpoint once per mid.if .tSeen[] == {continue } .tSeen[] = if .gen[] != { .gen[] = .val[] = 0 .touched = append(.touched, ) } .val[]++ } }for , := range .touched {iflen(.cur) == cncChunk { .full = append(.full, .cur) .cur = make([]CommonNeighborCount, 0, cncChunk) }if .cur == nil { .cur = make([]CommonNeighborCount, 0, cncChunk) } .cur = append(.cur, CommonNeighborCount{Source: , Target: , Count: .val[]}) }return },// Merge keeps slab order (ascending worker ranges): the left side's // open slab seals before the right side's slabs append, and the // right side's scratch returns to the pool.func(, *cncAcc) *cncAcc { .seal() .seal() .full = append(.full, .full...)if .sc != nil {pairScratchPool.Put(.sc) .sc = nil }return }) .seal()if .sc != nil {pairScratchPool.Put(.sc) .sc = nil } := 0for , := range .full { += len() } := make([]CommonNeighborCount, 0, )for , := range .full { = append(, ...) }return}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.