a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
156 lines
3.9 KiB
Go
156 lines
3.9 KiB
Go
package analysis
|
|
|
|
import (
|
|
"sort"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// KCoreHit is one row of the k-core decomposition output: a node
|
|
// plus its k-degree (the largest k for which it stays in the
|
|
// k-core after iterative degree-< k pruning). High k-degree
|
|
// signals a node sits inside a densely connected core; a chain of
|
|
// leaves all have k-degree 1, a triangle has k-degree 2, a
|
|
// 4-clique has k-degree 3.
|
|
type KCoreHit struct {
|
|
NodeID string
|
|
KDegree int
|
|
}
|
|
|
|
// KCoreOptions filters the working set. Empty NodeKinds /
|
|
// EdgeKinds means "all kinds". Edges are treated as undirected
|
|
// (k-core is defined on undirected graphs).
|
|
type KCoreOptions struct {
|
|
NodeKinds []graph.NodeKind
|
|
EdgeKinds []graph.EdgeKind
|
|
}
|
|
|
|
// ComputeKCore returns the k-core decomposition of g. Classic
|
|
// algorithm — Batagelj & Zaversnik 2003, O(V + E):
|
|
//
|
|
// 1. compute every node's undirected degree
|
|
// 2. process nodes in degree-ascending order
|
|
// 3. when a node is removed, decrement its still-present
|
|
// neighbours' degrees so they can be picked up at the right
|
|
// level
|
|
//
|
|
// Used as the fallback when the backing graph.Store does not
|
|
// implement graph.KCorer.
|
|
func ComputeKCore(g graph.Store, opts KCoreOptions) []KCoreHit {
|
|
if g == nil {
|
|
return nil
|
|
}
|
|
nodeAllow := makeComponentKindAllow(opts.NodeKinds)
|
|
edgeAllow := makeComponentEdgeAllow(opts.EdgeKinds)
|
|
|
|
// Dense index over allowed nodes.
|
|
nodes := g.AllNodes()
|
|
idx := make(map[string]int, len(nodes))
|
|
dense := make([]string, 0, len(nodes))
|
|
for _, n := range nodes {
|
|
if n == nil || !nodeAllow(n.Kind) {
|
|
continue
|
|
}
|
|
idx[n.ID] = len(dense)
|
|
dense = append(dense, n.ID)
|
|
}
|
|
if len(dense) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Undirected adjacency; dedupe self-loops + parallel edges.
|
|
type edge struct{ a, b int }
|
|
seenEdge := make(map[edge]bool)
|
|
adj := make([][]int, len(dense))
|
|
for _, e := range g.AllEdges() {
|
|
if e == nil || !edgeAllow(e.Kind) {
|
|
continue
|
|
}
|
|
i, ok1 := idx[e.From]
|
|
j, ok2 := idx[e.To]
|
|
if !ok1 || !ok2 || i == j {
|
|
continue
|
|
}
|
|
key := edge{i, j}
|
|
if i > j {
|
|
key = edge{j, i}
|
|
}
|
|
if seenEdge[key] {
|
|
continue
|
|
}
|
|
seenEdge[key] = true
|
|
adj[i] = append(adj[i], j)
|
|
adj[j] = append(adj[j], i)
|
|
}
|
|
|
|
n := len(dense)
|
|
degree := make([]int, n)
|
|
maxDeg := 0
|
|
for i := range dense {
|
|
degree[i] = len(adj[i])
|
|
if degree[i] > maxDeg {
|
|
maxDeg = degree[i]
|
|
}
|
|
}
|
|
|
|
// Bucket sort by degree (Batagelj & Zaversnik). bucket[d]
|
|
// holds dense-indices currently at degree d; pos[v] is v's
|
|
// position in its bucket; vertOrder is the global processing
|
|
// order populated as we drain the buckets.
|
|
bucket := make([][]int, maxDeg+1)
|
|
pos := make([]int, n)
|
|
for v, d := range degree {
|
|
pos[v] = len(bucket[d])
|
|
bucket[d] = append(bucket[d], v)
|
|
}
|
|
|
|
kdeg := make([]int, n)
|
|
processed := make([]bool, n)
|
|
for d := 0; d <= maxDeg; d++ {
|
|
for len(bucket[d]) > 0 {
|
|
// Pop the back of bucket[d] (O(1)).
|
|
v := bucket[d][len(bucket[d])-1]
|
|
bucket[d] = bucket[d][:len(bucket[d])-1]
|
|
if processed[v] {
|
|
continue
|
|
}
|
|
processed[v] = true
|
|
kdeg[v] = d
|
|
for _, w := range adj[v] {
|
|
if processed[w] {
|
|
continue
|
|
}
|
|
if degree[w] > d {
|
|
// Move w one bucket down.
|
|
old := degree[w]
|
|
// O(1) removal: swap with the back element
|
|
// of the old bucket and adjust its pos.
|
|
i := pos[w]
|
|
last := len(bucket[old]) - 1
|
|
if i != last {
|
|
other := bucket[old][last]
|
|
bucket[old][i] = other
|
|
pos[other] = i
|
|
}
|
|
bucket[old] = bucket[old][:last]
|
|
degree[w] = old - 1
|
|
pos[w] = len(bucket[degree[w]])
|
|
bucket[degree[w]] = append(bucket[degree[w]], w)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
out := make([]KCoreHit, 0, n)
|
|
for v, id := range dense {
|
|
out = append(out, KCoreHit{NodeID: id, KDegree: kdeg[v]})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
if out[i].KDegree != out[j].KDegree {
|
|
return out[i].KDegree > out[j].KDegree
|
|
}
|
|
return out[i].NodeID < out[j].NodeID
|
|
})
|
|
return out
|
|
}
|