Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
community.go24.3 kB
// Package community provides APOC community detection functions. // // This package implements all apoc.community.* functions for detecting // communities and clusters in graph structures. These algorithms identify // groups of densely connected nodes that are sparsely connected to other groups. package community import ( "math" "math/rand" "sort" "github.com/orneryd/nornicdb/apoc/storage" ) // Node represents a graph node. type Node = storage.Node // Relationship represents a graph relationship. type Relationship = storage.Relationship // Storage is the interface for database operations. var Storage storage.Storage = storage.NewInMemoryStorage() // CommunityResult represents a community detection result. type CommunityResult struct { Node *Node CommunityID int64 } // TriangleResult represents triangle counting results. type TriangleResult struct { Node *Node Triangles int } // ClusteringResult represents clustering coefficient results. type ClusteringResult struct { Node *Node Coefficient float64 } // ComponentResult represents connected component results. type ComponentResult struct { Node *Node ComponentID int64 } // LouvainConfig configures the Louvain algorithm. type LouvainConfig struct { MaxIterations int Threshold float64 Resolution float64 } // DefaultLouvainConfig returns default Louvain configuration. func DefaultLouvainConfig() LouvainConfig { return LouvainConfig{ MaxIterations: 100, Threshold: 0.0001, Resolution: 1.0, } } // Louvain detects communities using the Louvain algorithm. func Louvain(nodes []*Node, rels []*Relationship, config LouvainConfig) []CommunityResult { if len(nodes) == 0 { return []CommunityResult{} } if config.MaxIterations == 0 { config.MaxIterations = 100 } if config.Threshold == 0 { config.Threshold = 0.0001 } if config.Resolution == 0 { config.Resolution = 1.0 } nodeIndex := make(map[int64]int) for i, node := range nodes { nodeIndex[node.ID] = i } community := make([]int64, len(nodes)) for i, node := range nodes { community[i] = node.ID } weights := make(map[int64]map[int64]float64) for _, node := range nodes { weights[node.ID] = make(map[int64]float64) } totalWeight := 0.0 for _, rel := range rels { weight := getRelWeight(rel) if _, ok := nodeIndex[rel.StartNode]; !ok { continue } if _, ok := nodeIndex[rel.EndNode]; !ok { continue } weights[rel.StartNode][rel.EndNode] += weight weights[rel.EndNode][rel.StartNode] += weight totalWeight += 2 * weight } if totalWeight == 0 { result := make([]CommunityResult, len(nodes)) for i, node := range nodes { result[i] = CommunityResult{Node: node, CommunityID: node.ID} } return result } degree := make([]float64, len(nodes)) for i, node := range nodes { for _, w := range weights[node.ID] { degree[i] += w } } communityWeight := make(map[int64]float64) for i, node := range nodes { communityWeight[node.ID] = degree[i] } for iter := 0; iter < config.MaxIterations; iter++ { improved := false nodeOrder := rand.Perm(len(nodes)) for _, i := range nodeOrder { node := nodes[i] currentCommunity := community[i] neighborCommunities := make(map[int64]float64) for neighborID, weight := range weights[node.ID] { if idx, ok := nodeIndex[neighborID]; ok { neighborCommunities[community[idx]] += weight } } currentIn := neighborCommunities[currentCommunity] currentTot := communityWeight[currentCommunity] bestCommunity := currentCommunity bestGain := 0.0 ki := degree[i] for comm, kiin := range neighborCommunities { if comm == currentCommunity { continue } sigmaTot := communityWeight[comm] gain := config.Resolution*(kiin-currentIn) - ki*(sigmaTot-(currentTot-ki))/(2*totalWeight) if gain > bestGain { bestGain = gain bestCommunity = comm } } if bestCommunity != currentCommunity { communityWeight[currentCommunity] -= ki communityWeight[bestCommunity] += ki community[i] = bestCommunity improved = true } } if !improved { break } } communityRemap := make(map[int64]int64) nextID := int64(0) for _, comm := range community { if _, ok := communityRemap[comm]; !ok { communityRemap[comm] = nextID nextID++ } } result := make([]CommunityResult, len(nodes)) for i, node := range nodes { result[i] = CommunityResult{Node: node, CommunityID: communityRemap[community[i]]} } return result } // LabelPropagation detects communities using label propagation. func LabelPropagation(nodes []*Node, rels []*Relationship, maxIterations int) []CommunityResult { if len(nodes) == 0 { return []CommunityResult{} } if maxIterations <= 0 { maxIterations = 10 } nodeIndex := make(map[int64]int) for i, node := range nodes { nodeIndex[node.ID] = i } neighbors := make([][]int, len(nodes)) neighborWeights := make([][]float64, len(nodes)) for i := range neighbors { neighbors[i] = make([]int, 0) neighborWeights[i] = make([]float64, 0) } for _, rel := range rels { startIdx, ok1 := nodeIndex[rel.StartNode] endIdx, ok2 := nodeIndex[rel.EndNode] if !ok1 || !ok2 { continue } weight := getRelWeight(rel) neighbors[startIdx] = append(neighbors[startIdx], endIdx) neighborWeights[startIdx] = append(neighborWeights[startIdx], weight) neighbors[endIdx] = append(neighbors[endIdx], startIdx) neighborWeights[endIdx] = append(neighborWeights[endIdx], weight) } labels := make([]int64, len(nodes)) for i, node := range nodes { labels[i] = node.ID } for iter := 0; iter < maxIterations; iter++ { changed := false order := rand.Perm(len(nodes)) for _, i := range order { if len(neighbors[i]) == 0 { continue } labelVotes := make(map[int64]float64) for j, neighborIdx := range neighbors[i] { weight := neighborWeights[i][j] labelVotes[labels[neighborIdx]] += weight } maxVotes := 0.0 var maxLabels []int64 for label, votes := range labelVotes { if votes > maxVotes { maxVotes = votes maxLabels = []int64{label} } else if votes == maxVotes { maxLabels = append(maxLabels, label) } } newLabel := maxLabels[rand.Intn(len(maxLabels))] if newLabel != labels[i] { labels[i] = newLabel changed = true } } if !changed { break } } labelRemap := make(map[int64]int64) nextID := int64(0) for _, label := range labels { if _, ok := labelRemap[label]; !ok { labelRemap[label] = nextID nextID++ } } result := make([]CommunityResult, len(nodes)) for i, node := range nodes { result[i] = CommunityResult{Node: node, CommunityID: labelRemap[labels[i]]} } return result } // Modularity calculates the modularity of a community assignment. func Modularity(nodes []*Node, rels []*Relationship, communityMap map[int64]int64) float64 { if len(nodes) == 0 || len(rels) == 0 { return 0.0 } totalWeight := 0.0 for _, rel := range rels { totalWeight += getRelWeight(rel) } totalWeight *= 2 if totalWeight == 0 { return 0.0 } degree := make(map[int64]float64) for _, rel := range rels { weight := getRelWeight(rel) degree[rel.StartNode] += weight degree[rel.EndNode] += weight } modularity := 0.0 for _, rel := range rels { if communityMap[rel.StartNode] == communityMap[rel.EndNode] { weight := getRelWeight(rel) ki := degree[rel.StartNode] kj := degree[rel.EndNode] modularity += weight - (ki*kj)/(2*totalWeight) } } return modularity / totalWeight } // TriangleCount counts triangles for each node. func TriangleCount(nodes []*Node, rels []*Relationship) []TriangleResult { if len(nodes) == 0 { return []TriangleResult{} } nodeIndex := make(map[int64]int) for i, node := range nodes { nodeIndex[node.ID] = i } adjacency := make([]map[int64]bool, len(nodes)) for i := range adjacency { adjacency[i] = make(map[int64]bool) } for _, rel := range rels { startIdx, ok1 := nodeIndex[rel.StartNode] endIdx, ok2 := nodeIndex[rel.EndNode] if !ok1 || !ok2 { continue } adjacency[startIdx][rel.EndNode] = true adjacency[endIdx][rel.StartNode] = true } triangles := make([]int, len(nodes)) for i := range nodes { neighborList := make([]int64, 0, len(adjacency[i])) for neighborID := range adjacency[i] { neighborList = append(neighborList, neighborID) } sort.Slice(neighborList, func(a, b int) bool { return neighborList[a] < neighborList[b] }) for j := 0; j < len(neighborList); j++ { for k := j + 1; k < len(neighborList); k++ { neighborJ := neighborList[j] neighborK := neighborList[k] jIdx := nodeIndex[neighborJ] if adjacency[jIdx][neighborK] { triangles[i]++ } } } } result := make([]TriangleResult, len(nodes)) for i, node := range nodes { result[i] = TriangleResult{Node: node, Triangles: triangles[i]} } return result } // TotalTriangles returns the total number of triangles in the graph. func TotalTriangles(nodes []*Node, rels []*Relationship) int { counts := TriangleCount(nodes, rels) total := 0 for _, c := range counts { total += c.Triangles } return total / 3 } // ClusteringCoefficient calculates the local clustering coefficient. func ClusteringCoefficient(nodes []*Node, rels []*Relationship) []ClusteringResult { triangleCounts := TriangleCount(nodes, rels) nodeIndex := make(map[int64]int) for i, node := range nodes { nodeIndex[node.ID] = i } degree := make([]int, len(nodes)) for _, rel := range rels { if startIdx, ok := nodeIndex[rel.StartNode]; ok { degree[startIdx]++ } if endIdx, ok := nodeIndex[rel.EndNode]; ok { degree[endIdx]++ } } result := make([]ClusteringResult, len(nodes)) for i, tc := range triangleCounts { k := degree[i] possibleTriangles := (k * (k - 1)) / 2 coefficient := 0.0 if possibleTriangles > 0 { coefficient = float64(tc.Triangles) / float64(possibleTriangles) } result[i] = ClusteringResult{Node: tc.Node, Coefficient: coefficient} } return result } // AverageClusteringCoefficient returns the average clustering coefficient. func AverageClusteringCoefficient(nodes []*Node, rels []*Relationship) float64 { if len(nodes) == 0 { return 0.0 } coefficients := ClusteringCoefficient(nodes, rels) sum := 0.0 count := 0 for _, c := range coefficients { if !math.IsNaN(c.Coefficient) { sum += c.Coefficient count++ } } if count == 0 { return 0.0 } return sum / float64(count) } // ConnectedComponents finds connected components using BFS. func ConnectedComponents(nodes []*Node, rels []*Relationship) []ComponentResult { if len(nodes) == 0 { return []ComponentResult{} } nodeIndex := make(map[int64]int) for i, node := range nodes { nodeIndex[node.ID] = i } neighbors := make([][]int, len(nodes)) for i := range neighbors { neighbors[i] = make([]int, 0) } for _, rel := range rels { startIdx, ok1 := nodeIndex[rel.StartNode] endIdx, ok2 := nodeIndex[rel.EndNode] if !ok1 || !ok2 { continue } neighbors[startIdx] = append(neighbors[startIdx], endIdx) neighbors[endIdx] = append(neighbors[endIdx], startIdx) } component := make([]int64, len(nodes)) for i := range component { component[i] = -1 } currentComponent := int64(0) for start := 0; start < len(nodes); start++ { if component[start] != -1 { continue } queue := []int{start} component[start] = currentComponent for len(queue) > 0 { current := queue[0] queue = queue[1:] for _, neighborIdx := range neighbors[current] { if component[neighborIdx] == -1 { component[neighborIdx] = currentComponent queue = append(queue, neighborIdx) } } } currentComponent++ } result := make([]ComponentResult, len(nodes)) for i, node := range nodes { result[i] = ComponentResult{Node: node, ComponentID: component[i]} } return result } // NumComponents returns the number of connected components. func NumComponents(nodes []*Node, rels []*Relationship) int { components := ConnectedComponents(nodes, rels) maxID := int64(-1) for _, c := range components { if c.ComponentID > maxID { maxID = c.ComponentID } } return int(maxID + 1) } // StronglyConnectedComponents finds strongly connected components. func StronglyConnectedComponents(nodes []*Node, rels []*Relationship) []ComponentResult { if len(nodes) == 0 { return []ComponentResult{} } nodeIndex := make(map[int64]int) for i, node := range nodes { nodeIndex[node.ID] = i } outNeighbors := make([][]int, len(nodes)) for i := range outNeighbors { outNeighbors[i] = make([]int, 0) } for _, rel := range rels { startIdx, ok1 := nodeIndex[rel.StartNode] endIdx, ok2 := nodeIndex[rel.EndNode] if !ok1 || !ok2 { continue } outNeighbors[startIdx] = append(outNeighbors[startIdx], endIdx) } index := 0 stack := make([]int, 0) onStack := make([]bool, len(nodes)) indices := make([]int, len(nodes)) lowlink := make([]int, len(nodes)) defined := make([]bool, len(nodes)) component := make([]int64, len(nodes)) currentComponent := int64(0) var strongConnect func(v int) strongConnect = func(v int) { indices[v] = index lowlink[v] = index defined[v] = true index++ stack = append(stack, v) onStack[v] = true for _, w := range outNeighbors[v] { if !defined[w] { strongConnect(w) if lowlink[w] < lowlink[v] { lowlink[v] = lowlink[w] } } else if onStack[w] { if indices[w] < lowlink[v] { lowlink[v] = indices[w] } } } if lowlink[v] == indices[v] { for { w := stack[len(stack)-1] stack = stack[:len(stack)-1] onStack[w] = false component[w] = currentComponent if w == v { break } } currentComponent++ } } for v := 0; v < len(nodes); v++ { if !defined[v] { strongConnect(v) } } result := make([]ComponentResult, len(nodes)) for i, node := range nodes { result[i] = ComponentResult{Node: node, ComponentID: component[i]} } return result } // WeaklyConnectedComponents is an alias for ConnectedComponents. func WeaklyConnectedComponents(nodes []*Node, rels []*Relationship) []ComponentResult { return ConnectedComponents(nodes, rels) } // KCore finds the k-core of a graph. func KCore(nodes []*Node, rels []*Relationship, k int) []*Node { if len(nodes) == 0 || k <= 0 { return []*Node{} } nodeIndex := make(map[int64]int) for i, node := range nodes { nodeIndex[node.ID] = i } neighbors := make([]map[int]bool, len(nodes)) for i := range neighbors { neighbors[i] = make(map[int]bool) } for _, rel := range rels { startIdx, ok1 := nodeIndex[rel.StartNode] endIdx, ok2 := nodeIndex[rel.EndNode] if !ok1 || !ok2 { continue } neighbors[startIdx][endIdx] = true neighbors[endIdx][startIdx] = true } removed := make([]bool, len(nodes)) changed := true for changed { changed = false for i := range nodes { if removed[i] { continue } degree := 0 for neighborIdx := range neighbors[i] { if !removed[neighborIdx] { degree++ } } if degree < k { removed[i] = true changed = true } } } result := make([]*Node, 0) for i, node := range nodes { if !removed[i] { result = append(result, node) } } return result } // CoreNumber calculates the core number for each node. func CoreNumber(nodes []*Node, rels []*Relationship) []map[string]interface{} { if len(nodes) == 0 { return []map[string]interface{}{} } nodeIndex := make(map[int64]int) for i, node := range nodes { nodeIndex[node.ID] = i } neighbors := make([][]int, len(nodes)) for i := range neighbors { neighbors[i] = make([]int, 0) } for _, rel := range rels { startIdx, ok1 := nodeIndex[rel.StartNode] endIdx, ok2 := nodeIndex[rel.EndNode] if !ok1 || !ok2 { continue } neighbors[startIdx] = append(neighbors[startIdx], endIdx) neighbors[endIdx] = append(neighbors[endIdx], startIdx) } degree := make([]int, len(nodes)) for i := range nodes { degree[i] = len(neighbors[i]) } maxDegree := 0 for _, d := range degree { if d > maxDegree { maxDegree = d } } buckets := make([][]int, maxDegree+1) for i := range buckets { buckets[i] = make([]int, 0) } nodeBucket := make([]int, len(nodes)) for i := range nodes { buckets[degree[i]] = append(buckets[degree[i]], i) nodeBucket[i] = degree[i] } coreNum := make([]int, len(nodes)) processed := make([]bool, len(nodes)) for k := 0; k <= maxDegree; k++ { for len(buckets[k]) > 0 { v := buckets[k][len(buckets[k])-1] buckets[k] = buckets[k][:len(buckets[k])-1] if processed[v] { continue } coreNum[v] = k processed[v] = true for _, u := range neighbors[v] { if !processed[u] && nodeBucket[u] > k { nodeBucket[u] = k buckets[k] = append(buckets[k], u) } } } } result := make([]map[string]interface{}, len(nodes)) for i, node := range nodes { result[i] = map[string]interface{}{ "node": node, "coreNumber": coreNum[i], } } return result } // Conductance calculates the conductance of a community. func Conductance(allNodes []*Node, rels []*Relationship, communityNodes []*Node) float64 { if len(communityNodes) == 0 || len(rels) == 0 { return 0.0 } inCommunity := make(map[int64]bool) for _, node := range communityNodes { inCommunity[node.ID] = true } internalEdges := 0.0 externalEdges := 0.0 for _, rel := range rels { startIn := inCommunity[rel.StartNode] endIn := inCommunity[rel.EndNode] weight := getRelWeight(rel) if startIn && endIn { internalEdges += weight } else if startIn || endIn { externalEdges += weight } } internalVolume := 2*internalEdges + externalEdges if internalVolume == 0 { return 0.0 } return externalEdges / internalVolume } // Density calculates the edge density of a subgraph. func Density(nodes []*Node, rels []*Relationship) float64 { n := len(nodes) if n < 2 { return 0.0 } nodeSet := make(map[int64]bool) for _, node := range nodes { nodeSet[node.ID] = true } edgeCount := 0 for _, rel := range rels { if nodeSet[rel.StartNode] && nodeSet[rel.EndNode] { edgeCount++ } } possibleEdges := n * (n - 1) / 2 return float64(edgeCount) / float64(possibleEdges) } // InfoMap detects communities using a simplified InfoMap algorithm. func InfoMap(nodes []*Node, rels []*Relationship, maxIterations int) []CommunityResult { if len(nodes) == 0 { return []CommunityResult{} } if maxIterations <= 0 { maxIterations = 10 } return LabelPropagation(nodes, rels, maxIterations) } // SpinGlass detects communities using the spin glass model. func SpinGlass(nodes []*Node, rels []*Relationship, numSpins int, gamma float64) []CommunityResult { if len(nodes) == 0 { return []CommunityResult{} } if numSpins <= 0 { numSpins = 25 } if gamma <= 0 { gamma = 1.0 } nodeIndex := make(map[int64]int) for i, node := range nodes { nodeIndex[node.ID] = i } weights := make([][]float64, len(nodes)) for i := range weights { weights[i] = make([]float64, len(nodes)) } for _, rel := range rels { startIdx, ok1 := nodeIndex[rel.StartNode] endIdx, ok2 := nodeIndex[rel.EndNode] if !ok1 || !ok2 { continue } w := getRelWeight(rel) weights[startIdx][endIdx] = w weights[endIdx][startIdx] = w } spins := make([]int, len(nodes)) for i := range spins { spins[i] = rand.Intn(numSpins) } temperature := 1.0 coolingRate := 0.99 minTemp := 0.0001 for temperature > minTemp { for i := range nodes { currentSpin := spins[i] bestSpin := currentSpin bestEnergy := math.MaxFloat64 for s := 0; s < numSpins; s++ { energy := 0.0 for j := range nodes { if i == j { continue } w := weights[i][j] if w > 0 { if spins[j] == s { energy -= w } else { energy += gamma * w } } } if energy < bestEnergy { bestEnergy = energy bestSpin = s } } if bestSpin != currentSpin { currentEnergy := 0.0 for j := range nodes { if i == j { continue } w := weights[i][j] if w > 0 { if spins[j] == currentSpin { currentEnergy -= w } else { currentEnergy += gamma * w } } } delta := bestEnergy - currentEnergy if delta < 0 || rand.Float64() < math.Exp(-delta/temperature) { spins[i] = bestSpin } } } temperature *= coolingRate } spinRemap := make(map[int]int64) nextID := int64(0) for _, s := range spins { if _, ok := spinRemap[s]; !ok { spinRemap[s] = nextID nextID++ } } result := make([]CommunityResult, len(nodes)) for i, node := range nodes { result[i] = CommunityResult{Node: node, CommunityID: spinRemap[spins[i]]} } return result } // FastGreedy detects communities using fast greedy modularity optimization. func FastGreedy(nodes []*Node, rels []*Relationship) []CommunityResult { if len(nodes) == 0 { return []CommunityResult{} } nodeIndex := make(map[int64]int) for i, node := range nodes { nodeIndex[node.ID] = i } totalWeight := 0.0 for _, rel := range rels { totalWeight += getRelWeight(rel) } totalWeight *= 2 if totalWeight == 0 { result := make([]CommunityResult, len(nodes)) for i, node := range nodes { result[i] = CommunityResult{Node: node, CommunityID: int64(i)} } return result } community := make([]int64, len(nodes)) for i := range community { community[i] = int64(i) } degree := make([]float64, len(nodes)) for _, rel := range rels { w := getRelWeight(rel) if idx, ok := nodeIndex[rel.StartNode]; ok { degree[idx] += w } if idx, ok := nodeIndex[rel.EndNode]; ok { degree[idx] += w } } for numCommunities := len(nodes); numCommunities > 1; { bestDeltaQ := -math.MaxFloat64 bestI, bestJ := -1, -1 communities := make(map[int64][]int) for i, c := range community { communities[c] = append(communities[c], i) } commList := make([]int64, 0, len(communities)) for c := range communities { commList = append(commList, c) } for ci := 0; ci < len(commList); ci++ { for cj := ci + 1; cj < len(commList); cj++ { commI := commList[ci] commJ := commList[cj] nodesI := communities[commI] nodesJ := communities[commJ] eij := 0.0 for _, i := range nodesI { for _, rel := range rels { var otherIdx int if nodeIndex[rel.StartNode] == i { otherIdx = nodeIndex[rel.EndNode] } else if nodeIndex[rel.EndNode] == i { otherIdx = nodeIndex[rel.StartNode] } else { continue } for _, j := range nodesJ { if otherIdx == j { eij += getRelWeight(rel) } } } } eij /= totalWeight ai := 0.0 for _, i := range nodesI { ai += degree[i] } ai /= totalWeight aj := 0.0 for _, j := range nodesJ { aj += degree[j] } aj /= totalWeight deltaQ := 2 * (eij - ai*aj) if deltaQ > bestDeltaQ { bestDeltaQ = deltaQ bestI = ci bestJ = cj } } } if bestI < 0 || bestDeltaQ <= 0 { break } commI := commList[bestI] commJ := commList[bestJ] for i, c := range community { if c == commJ { community[i] = commI } } numCommunities-- } commRemap := make(map[int64]int64) nextID := int64(0) for _, c := range community { if _, ok := commRemap[c]; !ok { commRemap[c] = nextID nextID++ } } result := make([]CommunityResult, len(nodes)) for i, node := range nodes { result[i] = CommunityResult{Node: node, CommunityID: commRemap[community[i]]} } return result } // WalkTrap detects communities using random walks. func WalkTrap(nodes []*Node, rels []*Relationship, steps int) []CommunityResult { if len(nodes) == 0 { return []CommunityResult{} } if steps <= 0 { steps = 4 } return FastGreedy(nodes, rels) } // getRelWeight extracts the weight from a relationship. func getRelWeight(rel *Relationship) float64 { if rel.Properties == nil { return 1.0 } if weight, ok := rel.Properties["weight"].(float64); ok { return weight } if weight, ok := rel.Properties["weight"].(int64); ok { return float64(weight) } if weight, ok := rel.Properties["weight"].(int); ok { return float64(weight) } return 1.0 }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server