diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 35a86e3..4fcef17 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -25,4 +25,4 @@ jobs: run: go build -v ./... - name: Test - run: go test -v ./... + run: go test ./... diff --git a/graph/algorithm/betweenness_centrality.go b/graph/algorithm/betweenness_centrality.go index b81d480..fc414f7 100644 --- a/graph/algorithm/betweenness_centrality.go +++ b/graph/algorithm/betweenness_centrality.go @@ -5,7 +5,6 @@ import ( "sync" "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" ) // BetweennessCentrality computes betweenness centrality using cached all shortest paths. @@ -13,8 +12,8 @@ import ( // - For each pair (s,t), each interior node on a shortest path gets 1/|SP(s,t)| credit. // - Undirected graphs enqueue only i 2/((n-1)(n-2)), directed => 1/((n-1)(n-2)). -func BetweennessCentrality(g *graph.Graph, cfg *Config) map[node.ID]float64 { - res := make(map[node.ID]float64) +func BetweennessCentrality(g *graph.Graph, cfg *Config) map[graph.NodeID]float64 { + res := make(map[graph.NodeID]float64) if g == nil { return res } @@ -46,16 +45,16 @@ func BetweennessCentrality(g *graph.Graph, cfg *Config) map[node.ID]float64 { } // Use cached all-pairs shortest paths. - // Type: map[node.ID]map[node.ID][]path.Path + // Type: map[graph.NodeID]map[graph.NodeID][]path.Path all := AllShortestPaths(g, cfg) // Build an index for stable iteration and pair generation. - idxOf := make(map[node.ID]int, n) + idxOf := make(map[graph.NodeID]int, n) for i, u := range ids { idxOf[u] = i } - type pair struct{ s, t node.ID } + type pair struct{ s, t graph.NodeID } isUndirected := g.IsBidirectional() // Generate all (s,t) jobs. @@ -63,13 +62,13 @@ func BetweennessCentrality(g *graph.Graph, cfg *Config) map[node.ID]float64 { var wg sync.WaitGroup // Global accumulator with lock; each worker keeps a local map to minimize contention. - global := make(map[node.ID]float64, n) + global := make(map[graph.NodeID]float64, n) var mu sync.Mutex // Worker: consume pairs and accumulate contributions into a local map, then merge. workerFn := func() { defer wg.Done() - local := make(map[node.ID]float64, n) + local := make(map[graph.NodeID]float64, n) for job := range jobs { row, ok := all[job.s] @@ -84,7 +83,7 @@ func BetweennessCentrality(g *graph.Graph, cfg *Config) map[node.ID]float64 { // For each shortest path s->...->t, every interior node gets 1/den. for _, pth := range pathsST { - seq := pth.Nodes() // []node.ID; interior nodes are [1 : len-1) + seq := pth.Nodes() // []graph.NodeID; interior nodes are [1 : len-1) if len(seq) <= 2 { continue // no interior node } diff --git a/graph/algorithm/cache.go b/graph/algorithm/cache.go index ab6ff65..5da6b47 100644 --- a/graph/algorithm/cache.go +++ b/graph/algorithm/cache.go @@ -2,18 +2,30 @@ package algorithm import ( "sync" + "time" - "github.com/elecbug/netkit/graph/path" + "github.com/elecbug/netkit/graph" ) -var cachedAllShortestPaths = make(map[string]path.GraphPaths) -var cachedAllShortestPathLengths = make(map[string]path.PathLength) +var cachedAllShortestPaths = make(map[string]graph.Paths) +var cachedAllShortestPathLengths = make(map[string]graph.PathLength) var cacheMu sync.RWMutex // CacheClear clears the cached shortest paths and their lengths. func CacheClear() { cacheMu.Lock() defer cacheMu.Unlock() - cachedAllShortestPaths = make(map[string]path.GraphPaths) - cachedAllShortestPathLengths = make(map[string]path.PathLength) + cachedAllShortestPaths = make(map[string]graph.Paths) + cachedAllShortestPathLengths = make(map[string]graph.PathLength) +} + +// AutoCacheClear starts a goroutine that clears the cache at regular intervals defined by tick. +func AutoCacheClear(tick time.Duration) { + go func() { + for { + time.Sleep(tick) + + CacheClear() + } + }() } diff --git a/graph/algorithm/closeness_centrality.go b/graph/algorithm/closeness_centrality.go index b73947b..f13ac90 100644 --- a/graph/algorithm/closeness_centrality.go +++ b/graph/algorithm/closeness_centrality.go @@ -2,7 +2,6 @@ package algorithm import ( "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" ) // ClosenessCentrality computes NetworkX-compatible closeness centrality. @@ -17,8 +16,8 @@ import ( // Requirements: // - AllShortestPaths(g, cfg) must respect directedness of g. // - cfg.Closeness.WfImproved follows NetworkX default (true) unless overridden. -func ClosenessCentrality(g *graph.Graph, cfg *Config) map[node.ID]float64 { - out := make(map[node.ID]float64) +func ClosenessCentrality(g *graph.Graph, cfg *Config) map[graph.NodeID]float64 { + out := make(map[graph.NodeID]float64) if g == nil { return out } diff --git a/graph/algorithm/clustering_coefficient.go b/graph/algorithm/clustering_coefficient.go index bc26048..e63334e 100644 --- a/graph/algorithm/clustering_coefficient.go +++ b/graph/algorithm/clustering_coefficient.go @@ -5,15 +5,14 @@ import ( "sync" "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" ) // ClusteringCoefficientAll computes local clustering coefficients for all nodes. // - If g.IsBidirectional()==false (directed): Fagiolo (2007) directed clustering (matches NetworkX). // - If g.IsBidirectional()==true (undirected): standard undirected clustering. -// Returns map[node.ID]float64 with a value for every node in g. -func ClusteringCoefficient(g *graph.Graph, cfg *Config) map[node.ID]float64 { - res := make(map[node.ID]float64) +// Returns map[graph.NodeID]float64 with a value for every node in g. +func ClusteringCoefficient(g *graph.Graph, cfg *Config) map[graph.NodeID]float64 { + res := make(map[graph.NodeID]float64) if g == nil { return res } @@ -35,10 +34,10 @@ func ClusteringCoefficient(g *graph.Graph, cfg *Config) map[node.ID]float64 { // Build helper structures // outNeighbors[v] = slice of out-neighbors of v (exclude self) // inNeighbors[v] = slice of in-neighbors of v (exclude self) - only needed for directed - outNeighbors := make(map[node.ID][]node.ID, n) + outNeighbors := make(map[graph.NodeID][]graph.NodeID, n) for _, v := range nodes { ns := g.Neighbors(v) - buf := make([]node.ID, 0, len(ns)) + buf := make([]graph.NodeID, 0, len(ns)) for _, w := range ns { if w != v { buf = append(buf, w) @@ -48,9 +47,9 @@ func ClusteringCoefficient(g *graph.Graph, cfg *Config) map[node.ID]float64 { } isDirected := !g.IsBidirectional() - var inNeighbors map[node.ID][]node.ID + var inNeighbors map[graph.NodeID][]graph.NodeID if isDirected { - inNeighbors = make(map[node.ID][]node.ID, n) + inNeighbors = make(map[graph.NodeID][]graph.NodeID, n) for _, u := range nodes { for _, w := range outNeighbors[u] { // u -> w, so u is in-neighbor of w @@ -59,13 +58,13 @@ func ClusteringCoefficient(g *graph.Graph, cfg *Config) map[node.ID]float64 { } } - type job struct{ v node.ID } + type job struct{ v graph.NodeID } jobs := make(chan job, workers*2) var wg sync.WaitGroup var mu sync.Mutex // protects res map // Edge multiplicity for Fagiolo: b(u,v) = a_uv + a_vu ∈ {0,1,2} - b := func(u, v node.ID) int { + b := func(u, v graph.NodeID) int { sum := 0 if g.HasEdge(u, v) { @@ -98,7 +97,7 @@ func ClusteringCoefficient(g *graph.Graph, cfg *Config) map[node.ID]float64 { mu.Unlock() continue } - outSet := make(map[node.ID]struct{}, kOut) + outSet := make(map[graph.NodeID]struct{}, kOut) for _, w := range outNeighbors[v] { outSet[w] = struct{}{} } @@ -119,7 +118,7 @@ func ClusteringCoefficient(g *graph.Graph, cfg *Config) map[node.ID]float64 { // T_v = sum_{j != k} b(v,j) * b(j,k) * b(k,v) // with j,k in tot = in(v) ∪ out(v) - totSet := make(map[node.ID]struct{}, kTot) // upper bound + totSet := make(map[graph.NodeID]struct{}, kTot) // upper bound for _, u := range outNeighbors[v] { totSet[u] = struct{}{} } @@ -127,7 +126,7 @@ func ClusteringCoefficient(g *graph.Graph, cfg *Config) map[node.ID]float64 { totSet[u] = struct{}{} } // Make a slice to iterate - tot := make([]node.ID, 0, len(totSet)) + tot := make([]graph.NodeID, 0, len(totSet)) for u := range totSet { if u != v { // guard (shouldn't be in set anyway) tot = append(tot, u) diff --git a/graph/algorithm/degree_assortativity_coefficient.go b/graph/algorithm/degree_assortativity_coefficient.go index 396ff93..9e01811 100644 --- a/graph/algorithm/degree_assortativity_coefficient.go +++ b/graph/algorithm/degree_assortativity_coefficient.go @@ -6,7 +6,6 @@ import ( "math" "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" ) // DegreeAssortativityCoefficient computes Newman's degree assortativity coefficient (Pearson correlation) @@ -54,16 +53,16 @@ func DegreeAssortativityCoefficient(g *graph.Graph, cfg *Config) float64 { } // Build an index for upper-triangle filtering on undirected graphs. - idxOf := make(map[node.ID]int, n) + idxOf := make(map[graph.NodeID]int, n) for i, u := range ids { idxOf[u] = i } // Degree caches // NOTE: Replace the neighbor getters with your graph API if needed. - outDeg := make(map[node.ID]int, n) - inDeg := make(map[node.ID]int, n) - undeg := make(map[node.ID]int, n) + outDeg := make(map[graph.NodeID]int, n) + inDeg := make(map[graph.NodeID]int, n) + undeg := make(map[graph.NodeID]int, n) if isUndirected { for _, u := range ids { diff --git a/graph/algorithm/degree_centrality.go b/graph/algorithm/degree_centrality.go index 8526ab0..f4ef2e1 100644 --- a/graph/algorithm/degree_centrality.go +++ b/graph/algorithm/degree_centrality.go @@ -5,7 +5,6 @@ import ( "sync" "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" ) // DegreeCentralityConfig (suggested to be added inside your config package) @@ -32,8 +31,8 @@ import ( // - Undirected: deg(u)/(n-1). // - Directed (default "total"): (in(u)+out(u))/(n-1). Use "in"/"out" for the specific variants. // Self-loops are ignored for centrality. -func DegreeCentrality(g *graph.Graph, cfg *Config) map[node.ID]float64 { - res := make(map[node.ID]float64) +func DegreeCentrality(g *graph.Graph, cfg *Config) map[graph.NodeID]float64 { + res := make(map[graph.NodeID]float64) if g == nil { return res } @@ -55,7 +54,7 @@ func DegreeCentrality(g *graph.Graph, cfg *Config) map[node.ID]float64 { } // --- indexing --- - idxOf := make(map[node.ID]int, n) + idxOf := make(map[graph.NodeID]int, n) for i, u := range ids { idxOf[u] = i } diff --git a/graph/algorithm/edge_betweenness_centrality.go b/graph/algorithm/edge_betweenness_centrality.go index 1775b03..c56c667 100644 --- a/graph/algorithm/edge_betweenness_centrality.go +++ b/graph/algorithm/edge_betweenness_centrality.go @@ -5,10 +5,9 @@ import ( "sync" "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" ) -func makeEdgeKey(u, v node.ID, undirected bool) (node.ID, node.ID) { +func makeEdgeKey(u, v graph.NodeID, undirected bool) (graph.NodeID, graph.NodeID) { if undirected && v < u { u, v = v, u } @@ -34,11 +33,11 @@ func makeEdgeKey(u, v node.ID, undirected bool) (node.ID, node.ID) { // in Brandes accumulation (same practice as NetworkX). // // Returns: -// - map[node.ID]map[node.ID]float64 where: +// - map[graph.NodeID]map[graph.NodeID]float64 where: // - Undirected: key is canonical [min(u,v), max(u,v)] // - Directed: key is (u,v) ordered -func EdgeBetweennessCentrality(g *graph.Graph, cfg *Config) map[node.ID]map[node.ID]float64 { - out := make(map[node.ID]map[node.ID]float64) +func EdgeBetweennessCentrality(g *graph.Graph, cfg *Config) map[graph.NodeID]map[graph.NodeID]float64 { + out := make(map[graph.NodeID]map[graph.NodeID]float64) if g == nil { return out } @@ -79,7 +78,7 @@ func EdgeBetweennessCentrality(g *graph.Graph, cfg *Config) map[node.ID]map[node u, v := makeEdgeKey(u, v, isUndirected) if out[u] == nil { - out[u] = make(map[node.ID]float64) + out[u] = make(map[graph.NodeID]float64) } out[u][v] = 0.0 @@ -87,7 +86,7 @@ func EdgeBetweennessCentrality(g *graph.Graph, cfg *Config) map[node.ID]map[node } // ----- worker pool over source nodes ----- - type job struct{ s node.ID } + type job struct{ s graph.NodeID } jobs := make(chan job, n) var mu sync.Mutex @@ -97,16 +96,16 @@ func EdgeBetweennessCentrality(g *graph.Graph, cfg *Config) map[node.ID]map[node defer wg.Done() // Local accumulator to reduce lock contention - local := make(map[node.ID]map[node.ID]float64, 64) + local := make(map[graph.NodeID]map[graph.NodeID]float64, 64) for jb := range jobs { s := jb.s // Brandes data structures - stack := make([]node.ID, 0, n) - preds := make(map[node.ID][]node.ID, n) - sigma := make(map[node.ID]float64, n) - dist := make(map[node.ID]int, n) + stack := make([]graph.NodeID, 0, n) + preds := make(map[graph.NodeID][]graph.NodeID, n) + sigma := make(map[graph.NodeID]float64, n) + dist := make(map[graph.NodeID]int, n) for _, v := range ids { dist[v] = -1 @@ -115,7 +114,7 @@ func EdgeBetweennessCentrality(g *graph.Graph, cfg *Config) map[node.ID]map[node dist[s] = 0 // BFS (unweighted) - q := []node.ID{s} + q := []graph.NodeID{s} for len(q) > 0 { v := q[0] q = q[1:] @@ -136,7 +135,7 @@ func EdgeBetweennessCentrality(g *graph.Graph, cfg *Config) map[node.ID]map[node } // Dependency accumulation - delta := make(map[node.ID]float64, n) + delta := make(map[graph.NodeID]float64, n) for len(stack) > 0 { w := stack[len(stack)-1] stack = stack[:len(stack)-1] @@ -150,7 +149,7 @@ func EdgeBetweennessCentrality(g *graph.Graph, cfg *Config) map[node.ID]map[node eu, ev := makeEdgeKey(v, w, isUndirected) if local[eu] == nil { - local[eu] = make(map[node.ID]float64) + local[eu] = make(map[graph.NodeID]float64) } local[eu][ev] += c diff --git a/graph/algorithm/eigenvector_centrality.go b/graph/algorithm/eigenvector_centrality.go index 6639b01..fe44992 100644 --- a/graph/algorithm/eigenvector_centrality.go +++ b/graph/algorithm/eigenvector_centrality.go @@ -6,7 +6,6 @@ import ( "sync" "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" ) // EigenvectorCentrality computes eigenvector centrality using parallel power iteration. @@ -16,8 +15,8 @@ import ( // Set Reverse=true to use successors/out-edges (right eigenvector). // // Unweighted edges are assumed. The result vector is L2-normalized (sum of squares == 1). -func EigenvectorCentrality(g *graph.Graph, cfg *Config) map[node.ID]float64 { - out := make(map[node.ID]float64) +func EigenvectorCentrality(g *graph.Graph, cfg *Config) map[graph.NodeID]float64 { + out := make(map[graph.NodeID]float64) if g == nil { return out } @@ -26,7 +25,7 @@ func EigenvectorCentrality(g *graph.Graph, cfg *Config) map[node.ID]float64 { maxIter := 100 tol := 1e-6 reverse := false - var nstart *map[node.ID]float64 + var nstart *map[graph.NodeID]float64 workers := runtime.NumCPU() if cfg != nil { @@ -54,7 +53,7 @@ func EigenvectorCentrality(g *graph.Graph, cfg *Config) map[node.ID]float64 { if n == 0 { return out } - idxOf := make(map[node.ID]int, n) + idxOf := make(map[graph.NodeID]int, n) for i, u := range ids { idxOf[u] = i } diff --git a/graph/algorithm/modularity.go b/graph/algorithm/modularity.go index 5927c10..fdf1a9f 100644 --- a/graph/algorithm/modularity.go +++ b/graph/algorithm/modularity.go @@ -7,7 +7,6 @@ import ( "math" "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" ) // Modularity computes Newman-Girvan modularity Q. @@ -33,7 +32,7 @@ func Modularity(g *graph.Graph, cfg *Config) float64 { // - m = number_of_undirected_edges // - inv2m = 1/(2m); each undirected edge contributes inv2m twice (symmetrically) // Returns a partition map: nodeID -> compact community label. -func GreedyModularityCommunitiesNX(g *graph.Graph) map[node.ID]int { +func GreedyModularityCommunitiesNX(g *graph.Graph) map[graph.NodeID]int { ids := g.Nodes() n := len(ids) if n == 0 { @@ -41,7 +40,7 @@ func GreedyModularityCommunitiesNX(g *graph.Graph) map[node.ID]int { } // Build stable indices - idxOf := make(map[node.ID]int, n) + idxOf := make(map[graph.NodeID]int, n) for i, u := range ids { idxOf[u] = i } @@ -49,7 +48,7 @@ func GreedyModularityCommunitiesNX(g *graph.Graph) map[node.ID]int { // Build undirected edge set and degrees for the projection edges, deg, m := undirectedEdgesAndDegrees(g, ids, idxOf) if m == 0 { - part := make(map[node.ID]int, n) + part := make(map[graph.NodeID]int, n) for i, u := range ids { part[u] = i } @@ -188,7 +187,7 @@ func GreedyModularityCommunitiesNX(g *graph.Graph) map[node.ID]int { next++ } - part := make(map[node.ID]int, n) + part := make(map[graph.NodeID]int, n) for oldComm, label := range labelOf { for _, idx := range members[oldComm] { part[ids[idx]] = label @@ -202,13 +201,13 @@ func GreedyModularityCommunitiesNX(g *graph.Graph) map[node.ID]int { // m = number_of_undirected_edges // inv2m = 1/(2m) // Q = (1/2m) * sum_{(i,j) in undirected edges, c(i)=c(j)} [ 1 - (k_i k_j)/(2m) ] -func modularityQNX(g *graph.Graph, partition map[node.ID]int) float64 { +func modularityQNX(g *graph.Graph, partition map[graph.NodeID]int) float64 { ids := g.Nodes() n := len(ids) if n == 0 { return 0.0 } - idxOf := make(map[node.ID]int, n) + idxOf := make(map[graph.NodeID]int, n) for i, u := range ids { idxOf[u] = i } @@ -240,7 +239,7 @@ func modularityQNX(g *graph.Graph, partition map[node.ID]int) float64 { // undirectedEdgesAndDegrees builds the undirected projection (unique i 0 { @@ -60,7 +59,7 @@ func PageRank(g *graph.Graph, cfg *Config) map[node.ID]float64 { if n == 0 { return res } - idxOf := make(map[node.ID]int, n) + idxOf := make(map[graph.NodeID]int, n) for i, u := range ids { idxOf[u] = i } @@ -71,7 +70,7 @@ func PageRank(g *graph.Graph, cfg *Config) map[node.ID]float64 { outs := make([][]int, n) outdeg := make([]int, n) - getOuts := func(u node.ID) []int { + getOuts := func(u graph.NodeID) []int { nbrs := g.Neighbors(u) if bidir { // Undirected: treat neighbors as outs directly. diff --git a/graph/algorithm/shortest_path.go b/graph/algorithm/shortest_path.go index c3f61cc..b0091fc 100644 --- a/graph/algorithm/shortest_path.go +++ b/graph/algorithm/shortest_path.go @@ -5,12 +5,10 @@ import ( "sync" "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" - "github.com/elecbug/netkit/graph/path" ) // ShortestPaths finds all shortest paths between two nodes in a graph. -func ShortestPaths(g *graph.Graph, start, end node.ID) []path.Path { +func ShortestPaths(g *graph.Graph, start, end graph.NodeID) []graph.Path { gh := g.Hash() cacheMu.RLock() @@ -31,11 +29,11 @@ func ShortestPaths(g *graph.Graph, start, end node.ID) []path.Path { cacheMu.Lock() if _, ok := cachedAllShortestPaths[gh]; !ok { - cachedAllShortestPaths[gh] = make(map[node.ID]map[node.ID][]path.Path) + cachedAllShortestPaths[gh] = make(map[graph.NodeID]map[graph.NodeID][]graph.Path) } if _, ok := cachedAllShortestPaths[gh][start]; !ok { - cachedAllShortestPaths[gh][start] = make(map[node.ID][]path.Path) + cachedAllShortestPaths[gh][start] = make(map[graph.NodeID][]graph.Path) } if _, exists := cachedAllShortestPaths[gh][start][end]; !exists { @@ -47,7 +45,7 @@ func ShortestPaths(g *graph.Graph, start, end node.ID) []path.Path { return res } -// AllShortestPaths computes all-pairs shortest paths while keeping the same return structure (path.GraphPaths). +// AllShortestPaths computes all-pairs shortest paths while keeping the same return structure (graph.Paths). // Performance improvements over the (s,t)-pair BFS approach: // - Run exactly one BFS per source node (O(n*(m+n)) instead of O(n^2*(m+n)) in the worst case). // - Reconstruct all shortest paths to every target using predecessors (no repeated BFS). @@ -58,9 +56,9 @@ func ShortestPaths(g *graph.Graph, start, end node.ID) []path.Path { // (matching prior behavior and saving work). If you need reversed node order per entry, that can be changed, // but be aware of the extra cost. // - Self paths [u][u] are set to a single self path. -func AllShortestPaths(g *graph.Graph, cfg *Config) path.GraphPaths { +func AllShortestPaths(g *graph.Graph, cfg *Config) graph.Paths { if g == nil { - return path.GraphPaths{} + return graph.Paths{} } gh := g.Hash() @@ -85,11 +83,11 @@ func AllShortestPaths(g *graph.Graph, cfg *Config) path.GraphPaths { ids := g.Nodes() n := len(ids) if n == 0 { - return path.GraphPaths{} + return graph.Paths{} } // Precompute adjacency lists once to avoid per-step allocations in g.Neighbors. - adj := make(map[node.ID][]node.ID, n) + adj := make(map[graph.NodeID][]graph.NodeID, n) for _, u := range ids { adj[u] = g.Neighbors(u) } @@ -97,17 +95,17 @@ func AllShortestPaths(g *graph.Graph, cfg *Config) path.GraphPaths { // Per-source row buckets with independent locks. type row struct { mu sync.Mutex - m map[node.ID][]path.Path + m map[graph.NodeID][]graph.Path } - rows := make(map[node.ID]*row, n) + rows := make(map[graph.NodeID]*row, n) for _, s := range ids { - rows[s] = &row{m: make(map[node.ID][]path.Path, n-1)} + rows[s] = &row{m: make(map[graph.NodeID][]graph.Path, n-1)} } isUndirected := g.IsBidirectional() // Jobs are source nodes (one BFS per source). - srcJobs := make(chan node.ID, workers*2) + srcJobs := make(chan graph.NodeID, workers*2) var wg sync.WaitGroup wg.Add(workers) @@ -118,15 +116,15 @@ func AllShortestPaths(g *graph.Graph, cfg *Config) path.GraphPaths { for s := range srcJobs { // --- BFS from s to get dist and preds on shortest-path DAG --- - dist := make(map[node.ID]int, n) - preds := make(map[node.ID][]node.ID, n) + dist := make(map[graph.NodeID]int, n) + preds := make(map[graph.NodeID][]graph.NodeID, n) for _, u := range ids { dist[u] = -1 } dist[s] = 0 - q := []node.ID{s} + q := []graph.NodeID{s} for len(q) > 0 { v := q[0] q = q[1:] @@ -146,24 +144,24 @@ func AllShortestPaths(g *graph.Graph, cfg *Config) path.GraphPaths { } // --- Memoized enumeration of ALL shortest paths s->u for all u reachable --- - // returns list of sequences (each sequence is []node.ID from s to x). - memo := make(map[node.ID][][]node.ID, n) - var enum func(u node.ID) [][]node.ID - enum = func(u node.ID) [][]node.ID { + // returns list of sequences (each sequence is []graph.NodeID from s to x). + memo := make(map[graph.NodeID][][]graph.NodeID, n) + var enum func(u graph.NodeID) [][]graph.NodeID + enum = func(u graph.NodeID) [][]graph.NodeID { if paths, ok := memo[u]; ok { return paths } if u == s { - out := [][]node.ID{{s}} + out := [][]graph.NodeID{{s}} memo[u] = out return out } - var out [][]node.ID + var out [][]graph.NodeID for _, p := range preds[u] { if dist[p] >= 0 && dist[p] == dist[u]-1 { pfxs := enum(p) for _, pfx := range pfxs { - seq := make([]node.ID, len(pfx)+1) + seq := make([]graph.NodeID, len(pfx)+1) copy(seq, pfx) seq[len(pfx)] = u out = append(out, seq) @@ -176,7 +174,7 @@ func AllShortestPaths(g *graph.Graph, cfg *Config) path.GraphPaths { // Build result slice for this source s and write into rows. rS := rows[s] - loc := make(map[node.ID][]path.Path, n-1) + loc := make(map[graph.NodeID][]graph.Path, n-1) for _, t := range ids { if t == s { @@ -190,9 +188,9 @@ func AllShortestPaths(g *graph.Graph, cfg *Config) path.GraphPaths { if len(seqs) == 0 { continue } - pp := make([]path.Path, 0, len(seqs)) + pp := make([]graph.Path, 0, len(seqs)) for _, seq := range seqs { - pp = append(pp, *path.New(seq...)) + pp = append(pp, *graph.NewPath(seq...)) } loc[t] = pp @@ -225,14 +223,14 @@ func AllShortestPaths(g *graph.Graph, cfg *Config) path.GraphPaths { wg.Wait() // Assemble final output - out := make(path.GraphPaths, n) + out := make(graph.Paths, n) for s, r := range rows { out[s] = r.m } // Ensure self paths exist for _, u := range ids { - out[u][u] = []path.Path{*path.NewSelf(u)} + out[u][u] = []graph.Path{*graph.NewPath(u)} } // Cache the result @@ -244,15 +242,15 @@ func AllShortestPaths(g *graph.Graph, cfg *Config) path.GraphPaths { } // allShortestPathsBFS finds all shortest paths between two nodes in a graph using BFS. -func allShortestPathsBFS(g *graph.Graph, start, end node.ID) []path.Path { +func allShortestPathsBFS(g *graph.Graph, start, end graph.NodeID) []graph.Path { if start == end { - return []path.Path{*path.New(start)} + return []graph.Path{*graph.NewPath(start)} } - queue := []node.ID{start} - dist := make(map[node.ID]int) + queue := []graph.NodeID{start} + dist := make(map[graph.NodeID]int) dist[start] = 0 - preds := make(map[node.ID][]node.ID) + preds := make(map[graph.NodeID][]graph.NodeID) targetDist := -1 for len(queue) > 0 { @@ -285,16 +283,16 @@ func allShortestPathsBFS(g *graph.Graph, start, end node.ID) []path.Path { } if targetDist < 0 { - return []path.Path{} + return []graph.Path{} } - var all [][]node.ID - cur := []node.ID{end} + var all [][]graph.NodeID + cur := []graph.NodeID{end} - var dfs func(u node.ID) - dfs = func(u node.ID) { + var dfs func(u graph.NodeID) + dfs = func(u graph.NodeID) { if u == start { - seq := make([]node.ID, len(cur)) + seq := make([]graph.NodeID, len(cur)) for i := range cur { seq[i] = cur[len(cur)-1-i] @@ -314,10 +312,10 @@ func allShortestPathsBFS(g *graph.Graph, start, end node.ID) []path.Path { dfs(end) - res := make([]path.Path, 0, len(all)) + res := make([]graph.Path, 0, len(all)) for _, seq := range all { - res = append(res, *path.New(seq...)) + res = append(res, *graph.NewPath(seq...)) } return res } @@ -326,8 +324,8 @@ func allShortestPathsBFS(g *graph.Graph, start, end node.ID) []path.Path { // - For each source u, the inner map contains v -> dist(u,v) for all reachable v (including u with 0). // - Unreachable targets are omitted from the inner map. // - Uses a worker pool sized by cfg.Workers (or NumCPU when <=0). -func AllShortestPathLength(g *graph.Graph, cfg *Config) path.PathLength { - out := make(path.PathLength) +func AllShortestPathLength(g *graph.Graph, cfg *Config) graph.PathLength { + out := make(graph.PathLength) if g == nil { return out } @@ -358,7 +356,7 @@ func AllShortestPathLength(g *graph.Graph, cfg *Config) path.PathLength { } // Jobs: each source node runs one BFS - jobs := make(chan node.ID, n) + jobs := make(chan graph.NodeID, n) var ( wg sync.WaitGroup @@ -366,9 +364,9 @@ func AllShortestPathLength(g *graph.Graph, cfg *Config) path.PathLength { ) // Worker: standard unweighted BFS from a single source - bfsFrom := func(s node.ID) map[node.ID]int { - dist := make(map[node.ID]int, n) - q := make([]node.ID, 0, 64) + bfsFrom := func(s graph.NodeID) map[graph.NodeID]int { + dist := make(map[graph.NodeID]int, n) + q := make([]graph.NodeID, 0, 64) dist[s] = 0 q = append(q, s) diff --git a/graph/algorithm/subconfig.go b/graph/algorithm/subconfig.go index 18472ff..1c7a04f 100644 --- a/graph/algorithm/subconfig.go +++ b/graph/algorithm/subconfig.go @@ -1,6 +1,6 @@ package algorithm -import "github.com/elecbug/netkit/graph/node" +import "github.com/elecbug/netkit/graph" // ClosenessCentralityConfig holds the configuration settings for the closeness centrality algorithm. type ClosenessCentralityConfig struct { @@ -10,11 +10,11 @@ type ClosenessCentralityConfig struct { // PageRankConfig holds the configuration settings for the PageRank algorithm. type PageRankConfig struct { - Alpha float64 // damping, default 0.85 - MaxIter int // default 100 - Tol float64 // L1 error, default 1e-6 - Personalization *map[node.ID]float64 // p(u); if nil is uniform - Dangling *map[node.ID]float64 // d(u); if nil p(u) + Alpha float64 // damping, default 0.85 + MaxIter int // default 100 + Tol float64 // L1 error, default 1e-6 + Personalization *map[graph.NodeID]float64 // p(u); if nil is uniform + Dangling *map[graph.NodeID]float64 // d(u); if nil p(u) Reverse bool } @@ -33,7 +33,7 @@ type EigenvectorCentralityConfig struct { MaxIter int Tol float64 Reverse bool - NStart *map[node.ID]float64 // initial vector; if nil, uniform distribution + NStart *map[graph.NodeID]float64 // initial vector; if nil, uniform distribution } // DegreeCentralityConfig holds the configuration settings for the degree centrality algorithm. @@ -74,5 +74,5 @@ type AssortativityCoefficientConfig struct { type ModularityConfig struct { // Partition maps each node to its community ID. // If nil, algorithm will compute greedy modularity communities automatically. - Partition map[node.ID]int + Partition map[graph.NodeID]int } diff --git a/graph/edge.go b/graph/edge.go index 319664c..bacfc1c 100644 --- a/graph/edge.go +++ b/graph/edge.go @@ -2,12 +2,10 @@ package graph import ( "fmt" - - "github.com/elecbug/netkit/graph/node" ) // AddEdge adds an edge from -> to. If bidirectional is true, adds the reverse edge as well. -func (g *Graph) AddEdge(from, to node.ID) error { +func (g *Graph) AddEdge(from, to NodeID) error { if _, ok := g.nodes[from]; !ok { return fmt.Errorf("from node %s does not exist", from) } @@ -16,7 +14,7 @@ func (g *Graph) AddEdge(from, to node.ID) error { } if _, ok := g.edges[from]; !ok { - g.edges[from] = make(map[node.ID]bool) + g.edges[from] = make(map[NodeID]bool) } if g.edges[from][to] { @@ -27,7 +25,7 @@ func (g *Graph) AddEdge(from, to node.ID) error { if g.isUndirected { if _, ok := g.edges[to]; !ok { - g.edges[to] = make(map[node.ID]bool) + g.edges[to] = make(map[NodeID]bool) } if g.edges[to][from] { @@ -41,7 +39,7 @@ func (g *Graph) AddEdge(from, to node.ID) error { } // RemoveEdge removes the edge from -> to. If bidirectional is true, removes the reverse edge as well. -func (g *Graph) RemoveEdge(from, to node.ID) error { +func (g *Graph) RemoveEdge(from, to NodeID) error { if _, ok := g.edges[from]; !ok { return fmt.Errorf("no edges from node %s", from) } @@ -54,7 +52,7 @@ func (g *Graph) RemoveEdge(from, to node.ID) error { if g.isUndirected { if _, ok := g.edges[to]; !ok { - g.edges[to] = make(map[node.ID]bool) + g.edges[to] = make(map[NodeID]bool) } delete(g.edges[to], from) @@ -64,7 +62,7 @@ func (g *Graph) RemoveEdge(from, to node.ID) error { } // HasEdge checks if an edge exists from -> to. -func (g *Graph) HasEdge(from, to node.ID) bool { +func (g *Graph) HasEdge(from, to NodeID) bool { if edges, ok := g.edges[from]; ok { return edges[to] } diff --git a/graph/graph.go b/graph/graph.go index 1e57d89..64526e0 100644 --- a/graph/graph.go +++ b/graph/graph.go @@ -6,22 +6,20 @@ import ( "encoding/json" "fmt" "strings" - - "github.com/elecbug/netkit/graph/node" ) // Graph maintains nodes and adjacency edges. type Graph struct { - nodes map[node.ID]bool - edges map[node.ID]map[node.ID]bool + nodes map[NodeID]bool + edges map[NodeID]map[NodeID]bool isUndirected bool } // New creates and returns an empty Graph. func New(isUndirected bool) *Graph { return &Graph{ - nodes: make(map[node.ID]bool), - edges: make(map[node.ID]map[node.ID]bool), + nodes: make(map[NodeID]bool), + edges: make(map[NodeID]map[NodeID]bool), isUndirected: isUndirected, } } @@ -33,7 +31,7 @@ func FromMatrix(matrix [][]bool, bidirectional bool) *Graph { for i := 0; i < len(matrix); i++ { for j := 0; j < len(matrix[i]); j++ { if matrix[i][j] { - g.AddEdge(node.ID(fmt.Sprintf("%d", i)), node.ID(fmt.Sprintf("%d", j))) + g.AddEdge(NodeID(fmt.Sprintf("%d", i)), NodeID(fmt.Sprintf("%d", j))) } } } @@ -75,8 +73,8 @@ func Load(data string) (*Graph, error) { return nil, fmt.Errorf("invalid graph data") } - nodes := make(map[node.ID]bool) - edges := make(map[node.ID]map[node.ID]bool) + nodes := make(map[NodeID]bool) + edges := make(map[NodeID]map[NodeID]bool) var bidirectional bool if err := json.Unmarshal([]byte(lines[0]), &nodes); err != nil { diff --git a/graph/graph_test.go b/graph/graph_test.go index 0d13e3a..3a607d5 100644 --- a/graph/graph_test.go +++ b/graph/graph_test.go @@ -11,18 +11,16 @@ import ( "github.com/elecbug/netkit/graph" "github.com/elecbug/netkit/graph/algorithm" - "github.com/elecbug/netkit/graph/node" - "github.com/elecbug/netkit/graph/path" ) func TestSimple(t *testing.T) { // Create a sample graph g := graph.New(true) - n1 := node.ID("1") - n2 := node.ID("2") - n3 := node.ID("3") - n4 := node.ID("4") + n1 := graph.NodeID("1") + n2 := graph.NodeID("2") + n3 := graph.NodeID("3") + n4 := graph.NodeID("4") g.AddNode(n1) g.AddNode(n2) @@ -34,15 +32,15 @@ func TestSimple(t *testing.T) { g.AddEdge(n4, n3) tests := []struct { - start node.ID - end node.ID - want []path.Path + start graph.NodeID + end graph.NodeID + want []graph.Path wantErr bool }{ - {start: n1, end: n3, want: []path.Path{*path.New(n1, n2, n3)}, wantErr: false}, - {start: n1, end: n1, want: []path.Path{*path.New(n1)}, wantErr: false}, - {start: n2, end: n1, want: []path.Path{*path.New(n2, n1)}, wantErr: false}, - {start: n3, end: n4, want: []path.Path{*path.New(n3, n4)}, wantErr: false}, + {start: n1, end: n3, want: []graph.Path{*graph.NewPath(n1, n2, n3)}, wantErr: false}, + {start: n1, end: n1, want: []graph.Path{*graph.NewPath(n1)}, wantErr: false}, + {start: n2, end: n1, want: []graph.Path{*graph.NewPath(n2, n1)}, wantErr: false}, + {start: n3, end: n4, want: []graph.Path{*graph.NewPath(n3, n4)}, wantErr: false}, } for _, tt := range tests { @@ -63,16 +61,16 @@ func TestPathLengths(t *testing.T) { edgeCount := 10000 for i := 0; i < nodeCount; i++ { - g.AddNode(node.ID(fmt.Sprintf("%d", i))) + g.AddNode(graph.NodeID(fmt.Sprintf("%d", i))) } for i := 0; i < edgeCount; i++ { - g.AddEdge(node.ID(fmt.Sprintf("%d", i)), node.ID(fmt.Sprintf("%d", rand.Intn(nodeCount)))) + g.AddEdge(graph.NodeID(fmt.Sprintf("%d", i)), graph.NodeID(fmt.Sprintf("%d", rand.Intn(nodeCount)))) } t.Run("CheckEqualShortestPaths", func(t *testing.T) { - var got path.GraphPaths - var want path.PathLength + var got graph.Paths + var want graph.PathLength t.Run("WithPaths", func(t *testing.T) { got = algorithm.AllShortestPaths(g, &algorithm.Config{Workers: 4}) @@ -97,11 +95,11 @@ func TestBidirectionalGraph(t *testing.T) { edgeCount := 5000 for i := 0; i < nodeCount; i++ { - g.AddNode(node.ID(fmt.Sprintf("%d", i))) + g.AddNode(graph.NodeID(fmt.Sprintf("%d", i))) } for i := 0; i < edgeCount; i++ { - g.AddEdge(node.ID(fmt.Sprintf("%d", rand.Intn(nodeCount))), node.ID(fmt.Sprintf("%d", rand.Intn(nodeCount)))) + g.AddEdge(graph.NodeID(fmt.Sprintf("%d", rand.Intn(nodeCount))), graph.NodeID(fmt.Sprintf("%d", rand.Intn(nodeCount)))) } str, err := graph.Save(g) @@ -132,11 +130,11 @@ func TestDirectionalGraph(t *testing.T) { edgeCount := 10000 for i := 0; i < nodeCount; i++ { - g.AddNode(node.ID(fmt.Sprintf("%d", i))) + g.AddNode(graph.NodeID(fmt.Sprintf("%d", i))) } for i := 0; i < edgeCount; i++ { - g.AddEdge(node.ID(fmt.Sprintf("%d", rand.Intn(nodeCount))), node.ID(fmt.Sprintf("%d", rand.Intn(nodeCount)))) + g.AddEdge(graph.NodeID(fmt.Sprintf("%d", rand.Intn(nodeCount))), graph.NodeID(fmt.Sprintf("%d", rand.Intn(nodeCount)))) } str, err := graph.Save(g) diff --git a/graph/node.go b/graph/node.go index 12b20fd..f13f912 100644 --- a/graph/node.go +++ b/graph/node.go @@ -2,12 +2,17 @@ package graph import ( "fmt" - - "github.com/elecbug/netkit/graph/node" ) +// NodeID uniquely identifies a node in a network-graph. +type NodeID string + +func (id NodeID) String() string { + return string(id) +} + // AddNode adds a node to the graph. -func (g *Graph) AddNode(id node.ID) error { +func (g *Graph) AddNode(id NodeID) error { if _, ok := g.nodes[id]; !ok { g.nodes[id] = true @@ -18,7 +23,7 @@ func (g *Graph) AddNode(id node.ID) error { } // RemoveNode removes a node and its incident edges from the graph. -func (g *Graph) RemoveNode(id node.ID) error { +func (g *Graph) RemoveNode(id NodeID) error { if _, ok := g.nodes[id]; !ok { return fmt.Errorf("node %s does not exist", id) } @@ -34,7 +39,7 @@ func (g *Graph) RemoveNode(id node.ID) error { } // HasNode reports whether a node with the given id exists. -func (g *Graph) HasNode(id node.ID) bool { +func (g *Graph) HasNode(id NodeID) bool { if _, ok := g.nodes[id]; ok { return true } else { @@ -43,8 +48,8 @@ func (g *Graph) HasNode(id node.ID) bool { } // Nodes returns a slice of all node IDs in the graph. -func (g *Graph) Nodes() []node.ID { - var nodes []node.ID +func (g *Graph) Nodes() []NodeID { + var nodes []NodeID for id := range g.nodes { nodes = append(nodes, id) @@ -54,9 +59,9 @@ func (g *Graph) Nodes() []node.ID { } // Neighbors returns the list of neighbors reachable from the given node id. -func (g *Graph) Neighbors(id node.ID) []node.ID { +func (g *Graph) Neighbors(id NodeID) []NodeID { if edges, ok := g.edges[id]; ok { - var result []node.ID + var result []NodeID for to, v := range edges { if v { diff --git a/graph/node/node.go b/graph/node/node.go deleted file mode 100644 index a9e60db..0000000 --- a/graph/node/node.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package node defines node identifiers used in network-graph. -package node - -// ID uniquely identifies a node in a network-graph. -type ID string - -func (id ID) String() string { - return string(id) -} diff --git a/graph/path/path.go b/graph/path.go similarity index 54% rename from graph/path/path.go rename to graph/path.go index 88b7ade..e231c63 100644 --- a/graph/path/path.go +++ b/graph/path.go @@ -1,39 +1,40 @@ -// Package path defines path structures used by network-graph algorithms. -package path - -import ( - "github.com/elecbug/netkit/graph/node" -) +package graph // Path represents an ordered sequence of nodes with a hop distance. type Path struct { distance int - nodes []node.ID + nodes []NodeID isInf bool } -// GraphPaths is a mapping of start node IDs to end node IDs and their corresponding paths. -type GraphPaths map[node.ID]map[node.ID][]Path +// Paths is a mapping of start node IDs to end node IDs and their corresponding paths. +type Paths map[NodeID]map[NodeID][]Path // PathLength represents the length of a path between two nodes. -type PathLength map[node.ID]map[node.ID]int +type PathLength map[NodeID]map[NodeID]int // New constructs a Path from the given nodes. Distance is hops (edges). // If no nodes are provided, the path is considered infinite (unreachable). -func New(nodes ...node.ID) *Path { - return &Path{ - distance: len(nodes) - 1, // Assuming distance is the number of edges - isInf: len(nodes) == 0, - nodes: nodes, - } -} +func NewPath(nodes ...NodeID) *Path { + if len(nodes) == 0 { + return &Path{ + distance: 0, + isInf: true, + nodes: []NodeID{}, + } + } else if len(nodes) == 1 { + return &Path{ + distance: 0, + isInf: false, + nodes: []NodeID{nodes[0]}, + } + } else { + return &Path{ + distance: len(nodes) - 1, // Assuming distance is the number of edges + isInf: len(nodes) == 0, + nodes: nodes, + } -// NewSelf constructs a new Path representing a self-loop at the given node. -func NewSelf(id node.ID) *Path { - return &Path{ - distance: 0, - isInf: false, - nodes: []node.ID{id}, } } @@ -48,12 +49,12 @@ func (p *Path) Distance() int { } // Nodes returns the node IDs in the path. -func (p *Path) Nodes() []node.ID { +func (p *Path) Nodes() []NodeID { return p.nodes } // OnlyLength returns a slice of PathLength representing the lengths of all paths in the graph. -func (g GraphPaths) OnlyLength() PathLength { +func (g Paths) OnlyLength() PathLength { results := make(PathLength, 0) for start, endMap := range g { @@ -63,7 +64,7 @@ func (g GraphPaths) OnlyLength() PathLength { } if results[start] == nil { - results[start] = make(map[node.ID]int) + results[start] = make(map[NodeID]int) } results[start][end] = paths[0].Distance() diff --git a/graph/standard_graph/barabasi_albert.go b/graph/standard_graph/barabasi_albert.go index 1b41761..3fc1cf5 100644 --- a/graph/standard_graph/barabasi_albert.go +++ b/graph/standard_graph/barabasi_albert.go @@ -2,7 +2,6 @@ package standard_graph import ( "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" ) // BarabasiAlbertGraph generates a graph based on the Barabási–Albert preferential attachment model. @@ -16,21 +15,21 @@ func BarabasiAlbertGraph(n int, m int, isUndirected bool) *graph.Graph { // --- 1. initialize --- for i := 0; i < m; i++ { - g.AddNode(node.ID(toString(i))) + g.AddNode(graph.NodeID(toString(i))) } for i := 0; i < m; i++ { for j := i + 1; j < m; j++ { - g.AddEdge(node.ID(toString(i)), node.ID(toString(j))) + g.AddEdge(graph.NodeID(toString(i)), graph.NodeID(toString(j))) } } // --- 2. preferential attachment --- for i := m; i < n; i++ { - newNode := node.ID(toString(i)) + newNode := graph.NodeID(toString(i)) g.AddNode(newNode) // calculate current node degrees - degrees := make(map[node.ID]int) + degrees := make(map[graph.NodeID]int) totalDegree := 0 for _, id := range g.Nodes() { d := len(g.Neighbors(id)) // each node degree @@ -39,11 +38,11 @@ func BarabasiAlbertGraph(n int, m int, isUndirected bool) *graph.Graph { } // degree based sampling - chosen := make(map[node.ID]bool) + chosen := make(map[graph.NodeID]bool) for len(chosen) < m { r := ra.Intn(totalDegree) accum := 0 - var target node.ID + var target graph.NodeID for id, d := range degrees { accum += d if r < accum { diff --git a/graph/standard_graph/erdos_reyni.go b/graph/standard_graph/erdos_reyni.go index 555a278..fa9a2f9 100644 --- a/graph/standard_graph/erdos_reyni.go +++ b/graph/standard_graph/erdos_reyni.go @@ -2,7 +2,6 @@ package standard_graph import ( "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" ) // ErdosRenyiGraph generates a random graph based on the Erdős-Rényi model. @@ -12,14 +11,14 @@ func ErdosRenyiGraph(n int, p float64, isUndirected bool) *graph.Graph { g := graph.New(isUndirected) for i := 0; i < n; i++ { - g.AddNode(node.ID(toString(i))) + g.AddNode(graph.NodeID(toString(i))) } if isUndirected { for i := 0; i < n; i++ { for j := i + 1; j < n; j++ { if ra.Float64() < p { - g.AddEdge(node.ID(toString(i)), node.ID(toString(j))) + g.AddEdge(graph.NodeID(toString(i)), graph.NodeID(toString(j))) } } } @@ -27,7 +26,7 @@ func ErdosRenyiGraph(n int, p float64, isUndirected bool) *graph.Graph { for i := 0; i < n; i++ { for j := 0; j < n; j++ { if i != j && ra.Float64() < p { - g.AddEdge(node.ID(toString(i)), node.ID(toString(j))) + g.AddEdge(graph.NodeID(toString(i)), graph.NodeID(toString(j))) } } } diff --git a/graph/standard_graph/random_geometric.go b/graph/standard_graph/random_geometric.go index 54f6532..3701643 100644 --- a/graph/standard_graph/random_geometric.go +++ b/graph/standard_graph/random_geometric.go @@ -4,7 +4,6 @@ import ( "math" "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" ) // RandomGeometricGraph generates a random geometric graph (RGG). @@ -21,10 +20,10 @@ func RandomGeometricGraph(n int, r float64, isUndirected bool) *graph.Graph { // --- 1. Generate Nodes --- type point struct{ x, y float64 } - positions := make(map[node.ID]point) + positions := make(map[graph.NodeID]point) for i := 0; i < n; i++ { - id := node.ID(toString(i)) + id := graph.NodeID(toString(i)) g.AddNode(id) positions[id] = point{ x: ra.Float64(), @@ -35,8 +34,8 @@ func RandomGeometricGraph(n int, r float64, isUndirected bool) *graph.Graph { // --- 2. Generate Edges --- for i := 0; i < n; i++ { for j := i + 1; j < n; j++ { - id1 := node.ID(toString(i)) - id2 := node.ID(toString(j)) + id1 := graph.NodeID(toString(i)) + id2 := graph.NodeID(toString(j)) p1, p2 := positions[id1], positions[id2] dx := p1.x - p2.x diff --git a/graph/standard_graph/random_regular.go b/graph/standard_graph/random_regular.go index fad15c6..e874a4e 100644 --- a/graph/standard_graph/random_regular.go +++ b/graph/standard_graph/random_regular.go @@ -2,7 +2,6 @@ package standard_graph import ( "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" ) // RandomRegularGraph generates a random k-regular graph with n nodes. @@ -21,14 +20,14 @@ func RandomRegularGraph(n, k int, isUndirected bool) *graph.Graph { // add nodes for i := 0; i < n; i++ { - g.AddNode(node.ID(toString(i))) + g.AddNode(graph.NodeID(toString(i))) } // duplicate each node k times as stubs - stubs := make([]node.ID, 0, n*k) + stubs := make([]graph.NodeID, 0, n*k) for i := 0; i < n; i++ { for j := 0; j < k; j++ { - stubs = append(stubs, node.ID(toString(i))) + stubs = append(stubs, graph.NodeID(toString(i))) } } diff --git a/graph/standard_graph/watts_strogatz.go b/graph/standard_graph/watts_strogatz.go index 6fd736d..9a6e2d6 100644 --- a/graph/standard_graph/watts_strogatz.go +++ b/graph/standard_graph/watts_strogatz.go @@ -2,7 +2,6 @@ package standard_graph import ( "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" ) // WattsStrogatzGraph generates a Watts–Strogatz small-world graph. @@ -19,14 +18,14 @@ func WattsStrogatzGraph(n, k int, beta float64, isUndirected bool) *graph.Graph // --- 1. Generate Nodes --- for i := 0; i < n; i++ { - g.AddNode(node.ID(toString(i))) + g.AddNode(graph.NodeID(toString(i))) } // --- 2. Generate Regular Ring Lattice --- for i := 0; i < n; i++ { for j := 1; j <= k/2; j++ { neighbor := (i + j) % n - g.AddEdge(node.ID(toString(i)), node.ID(toString(neighbor))) + g.AddEdge(graph.NodeID(toString(i)), graph.NodeID(toString(neighbor))) } } @@ -36,13 +35,13 @@ func WattsStrogatzGraph(n, k int, beta float64, isUndirected bool) *graph.Graph neighbor := (i + j) % n if ra.Float64() < beta { // Remove existing edge - g.RemoveEdge(node.ID(toString(i)), node.ID(toString(neighbor))) + g.RemoveEdge(graph.NodeID(toString(i)), graph.NodeID(toString(neighbor))) // Select a random other node (self-loop, duplicate prevention) for { - newNeighbor := node.ID(toString(ra.Intn(n))) - if newNeighbor != node.ID(toString(i)) && !g.HasEdge(node.ID(toString(i)), newNeighbor) { - g.AddEdge(node.ID(toString(i)), newNeighbor) + newNeighbor := graph.NodeID(toString(ra.Intn(n))) + if newNeighbor != graph.NodeID(toString(i)) && !g.HasEdge(graph.NodeID(toString(i)), newNeighbor) { + g.AddEdge(graph.NodeID(toString(i)), newNeighbor) break } } diff --git a/graph/standard_graph/waxman.go b/graph/standard_graph/waxman.go index e3194e5..abf8aae 100644 --- a/graph/standard_graph/waxman.go +++ b/graph/standard_graph/waxman.go @@ -4,7 +4,6 @@ import ( "math" "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" ) // WaxmanGraph generates a Waxman random graph. @@ -21,10 +20,10 @@ func WaxmanGraph(n int, alpha, beta float64, isUndirected bool) *graph.Graph { // --- 1. Generate Node Positions --- type point struct{ x, y float64 } - positions := make(map[node.ID]point) + positions := make(map[graph.NodeID]point) for i := 0; i < n; i++ { - id := node.ID(toString(i)) + id := graph.NodeID(toString(i)) g.AddNode(id) positions[id] = point{ x: ra.Float64(), @@ -38,8 +37,8 @@ func WaxmanGraph(n int, alpha, beta float64, isUndirected bool) *graph.Graph { // --- 2. Generate Edges Based on Distance --- for i := 0; i < n; i++ { for j := i + 1; j < n; j++ { - id1 := node.ID(toString(i)) - id2 := node.ID(toString(j)) + id1 := graph.NodeID(toString(i)) + id2 := graph.NodeID(toString(j)) p1, p2 := positions[id1], positions[id2] dx := p1.x - p2.x diff --git a/p2p/broadcast.go b/p2p/broadcast.go new file mode 100644 index 0000000..bf70916 --- /dev/null +++ b/p2p/broadcast.go @@ -0,0 +1,10 @@ +package p2p + +// BroadcastProtocol defines the protocol used for broadcasting messages in the P2P network. +type BroadcastProtocol int + +var ( + Flooding BroadcastProtocol = 0 + Gossiping BroadcastProtocol = 1 + Custom BroadcastProtocol = 2 +) diff --git a/p2p/broadcast/broadcast.go b/p2p/broadcast/broadcast.go deleted file mode 100644 index 4ac3dfb..0000000 --- a/p2p/broadcast/broadcast.go +++ /dev/null @@ -1,11 +0,0 @@ -// Package broadcast defines the protocols used for communication in the P2P network. -package broadcast - -// Protocol defines the protocol used for broadcasting messages in the P2P network. -type Protocol int - -var ( - Flooding Protocol = 0 - Gossiping Protocol = 1 - Custom Protocol = 2 -) diff --git a/p2p/network.go b/p2p/network.go index 54b39ef..bfb4149 100644 --- a/p2p/network.go +++ b/p2p/network.go @@ -8,21 +8,19 @@ import ( "time" "github.com/elecbug/netkit/graph" - "github.com/elecbug/netkit/graph/node" - "github.com/elecbug/netkit/p2p/broadcast" ) // Config holds configuration parameters for the P2P network. type Network struct { - nodes map[ID]*p2pNode + nodes map[PeerID]*p2pNode cfg *Config } // GenerateNetwork creates a P2P network from the given graph. // nodeLatency and edgeLatency are functions that generate latencies for nodes and edges respectively. func GenerateNetwork(g *graph.Graph, nodeLatency, edgeLatency func() float64, cfg *Config) (*Network, error) { - nodes := make(map[ID]*p2pNode) - maps := make(map[node.ID]ID) + nodes := make(map[PeerID]*p2pNode) + maps := make(map[graph.NodeID]PeerID) // create nodes for _, gn := range g.Nodes() { @@ -32,8 +30,8 @@ func GenerateNetwork(g *graph.Graph, nodeLatency, edgeLatency func() float64, cf return nil, err } - n := newNode(ID(num), nodeLatency()) - n.edges = make(map[ID]p2pEdge) + n := newNode(PeerID(num), nodeLatency()) + n.edges = make(map[PeerID]p2pEdge) nodes[n.id] = n maps[gn] = n.id @@ -46,13 +44,13 @@ func GenerateNetwork(g *graph.Graph, nodeLatency, edgeLatency func() float64, cf return nil, err } - n := nodes[ID(num)] + n := nodes[PeerID(num)] for _, neighbor := range g.Neighbors(gn) { j := maps[neighbor] edge := p2pEdge{ - TargetID: ID(j), + TargetID: PeerID(j), Latency: edgeLatency(), } @@ -76,8 +74,8 @@ func (n *Network) RunNetworkSimulation(ctx context.Context) { } // NodeIDs returns a slice of all node IDs in the network. -func (n *Network) NodeIDs() []ID { - ids := make([]ID, 0, len(n.nodes)) +func (n *Network) NodeIDs() []PeerID { + ids := make([]PeerID, 0, len(n.nodes)) for id := range n.nodes { ids = append(ids, id) @@ -87,7 +85,7 @@ func (n *Network) NodeIDs() []ID { } // Publish sends a message to the specified node's message queue. -func (n *Network) Publish(nodeID ID, msg string, protocol broadcast.Protocol) error { +func (n *Network) Publish(nodeID PeerID, msg string, protocol BroadcastProtocol) error { if node, ok := n.nodes[nodeID]; ok { if !node.alive { return fmt.Errorf("node %d is not alive", nodeID) @@ -149,7 +147,7 @@ func (n *Network) NumberOfDuplicateMessages(msg string) int { } // MessageInfo returns a snapshot of the node's message-related information. -func (n *Network) MessageInfo(nodeID ID, content string) (map[string]any, error) { +func (n *Network) MessageInfo(nodeID PeerID, content string) (map[string]any, error) { node := n.nodes[nodeID] if node == nil { @@ -161,14 +159,14 @@ func (n *Network) MessageInfo(nodeID ID, content string) (map[string]any, error) info := make(map[string]any) - info["recv"] = make([]ID, 0) + info["recv"] = make([]PeerID, 0) for k := range node.recvFrom[content] { - info["recv"] = append(info["recv"].([]ID), k) + info["recv"] = append(info["recv"].([]PeerID), k) } - info["sent"] = make([]ID, 0) + info["sent"] = make([]PeerID, 0) for k := range node.sentTo[content] { - info["sent"] = append(info["sent"].([]ID), k) + info["sent"] = append(info["sent"].([]PeerID), k) } info["seen"] = node.seenAt[content].String() diff --git a/p2p/node.go b/p2p/node.go index 9c64049..36fd489 100644 --- a/p2p/node.go +++ b/p2p/node.go @@ -4,19 +4,17 @@ import ( "context" "sync" "time" - - "github.com/elecbug/netkit/p2p/broadcast" ) // p2pNode represents a node in the P2P network. type p2pNode struct { - id ID + id PeerID nodeLatency float64 - edges map[ID]p2pEdge + edges map[PeerID]p2pEdge - recvFrom map[string]map[ID]struct{} // content -> set of senders - sentTo map[string]map[ID]struct{} // content -> set of targets - seenAt map[string]time.Time // content -> first arrival time + recvFrom map[string]map[PeerID]struct{} // content -> set of senders + sentTo map[string]map[PeerID]struct{} // content -> set of targets + seenAt map[string]time.Time // content -> first arrival time msgQueue chan Message mu sync.Mutex @@ -26,19 +24,19 @@ type p2pNode struct { // p2pEdge represents a connection from one node to another in the P2P network. type p2pEdge struct { - TargetID ID + TargetID PeerID Latency float64 // in milliseconds } // newNode creates a new Node with the given ID and node latency. -func newNode(id ID, nodeLatency float64) *p2pNode { +func newNode(id PeerID, nodeLatency float64) *p2pNode { return &p2pNode{ id: id, nodeLatency: nodeLatency, - edges: make(map[ID]p2pEdge), + edges: make(map[PeerID]p2pEdge), - recvFrom: make(map[string]map[ID]struct{}), - sentTo: make(map[string]map[ID]struct{}), + recvFrom: make(map[string]map[PeerID]struct{}), + sentTo: make(map[string]map[PeerID]struct{}), seenAt: make(map[string]time.Time), msgQueue: make(chan Message, 1000), @@ -60,11 +58,11 @@ func (n *p2pNode) eachRun(network *Network, wg *sync.WaitGroup, ctx context.Cont return default: first := false - var excludeSnapshot map[ID]struct{} + var excludeSnapshot map[PeerID]struct{} n.mu.Lock() if _, ok := n.recvFrom[msg.Content]; !ok { - n.recvFrom[msg.Content] = make(map[ID]struct{}) + n.recvFrom[msg.Content] = make(map[PeerID]struct{}) } n.recvFrom[msg.Content][msg.From] = struct{}{} @@ -76,7 +74,7 @@ func (n *p2pNode) eachRun(network *Network, wg *sync.WaitGroup, ctx context.Cont n.mu.Unlock() if first { - go func(msg Message, exclude map[ID]struct{}) { + go func(msg Message, exclude map[PeerID]struct{}) { time.Sleep(time.Duration(n.nodeLatency) * time.Millisecond) n.publish(network, msg, exclude) }(msg, excludeSnapshot) @@ -87,8 +85,8 @@ func (n *p2pNode) eachRun(network *Network, wg *sync.WaitGroup, ctx context.Cont } // copyIDSet creates a shallow copy of a set of IDs. -func copyIDSet(src map[ID]struct{}) map[ID]struct{} { - dst := make(map[ID]struct{}, len(src)) +func copyIDSet(src map[PeerID]struct{}) map[PeerID]struct{} { + dst := make(map[PeerID]struct{}, len(src)) for k := range src { dst[k] = struct{}{} } @@ -96,7 +94,7 @@ func copyIDSet(src map[ID]struct{}) map[ID]struct{} { } // publish sends the message to neighbors, excluding 'exclude' and already-sent targets. -func (n *p2pNode) publish(network *Network, msg Message, exclude map[ID]struct{}) { +func (n *p2pNode) publish(network *Network, msg Message, exclude map[PeerID]struct{}) { content := msg.Content protocol := msg.Protocol @@ -104,7 +102,7 @@ func (n *p2pNode) publish(network *Network, msg Message, exclude map[ID]struct{} defer n.mu.Unlock() if _, ok := n.sentTo[content]; !ok { - n.sentTo[content] = make(map[ID]struct{}) + n.sentTo[content] = make(map[PeerID]struct{}) } willSendEdges := make([]p2pEdge, 0) @@ -124,7 +122,7 @@ func (n *p2pNode) publish(network *Network, msg Message, exclude map[ID]struct{} willSendEdges = append(willSendEdges, edge) } - if protocol == broadcast.Gossiping && len(willSendEdges) > 0 { + if protocol == Gossiping && len(willSendEdges) > 0 { k := int(float64(len(willSendEdges)) * network.cfg.GossipFactor) willSendEdges = willSendEdges[:k] } diff --git a/p2p/p2p_test.go b/p2p/p2p_test.go index 9defc33..81b99ea 100644 --- a/p2p/p2p_test.go +++ b/p2p/p2p_test.go @@ -12,7 +12,6 @@ import ( "github.com/elecbug/netkit/graph/standard_graph" "github.com/elecbug/netkit/p2p" - "github.com/elecbug/netkit/p2p/broadcast" ) func TestGenerateNetwork(t *testing.T) { @@ -39,7 +38,7 @@ func TestGenerateNetwork(t *testing.T) { nw.RunNetworkSimulation(ctx) t.Logf("Publishing message '%s' from node %d\n", msg1, nw.NodeIDs()[0]) - err = nw.Publish(nw.NodeIDs()[0], msg1, broadcast.Flooding) + err = nw.Publish(nw.NodeIDs()[0], msg1, p2p.Flooding) if err != nil { t.Fatalf("Failed to publish message: %v", err) } @@ -47,7 +46,7 @@ func TestGenerateNetwork(t *testing.T) { t.Logf("Reachability of message '%s': %f\n", msg1, nw.Reachability(msg1)) t.Logf("Publishing message '%s' from node %d\n", msg2, nw.NodeIDs()[1]) - err = nw.Publish(nw.NodeIDs()[1], msg2, broadcast.Gossiping) + err = nw.Publish(nw.NodeIDs()[1], msg2, p2p.Gossiping) if err != nil { t.Fatalf("Failed to publish message: %v", err) } @@ -58,7 +57,7 @@ func TestGenerateNetwork(t *testing.T) { nw.RunNetworkSimulation(context.Background()) t.Logf("Publishing message '%s' from node %d\n", msg3, nw.NodeIDs()[2]) - err = nw.Publish(nw.NodeIDs()[2], msg3, broadcast.Gossiping) + err = nw.Publish(nw.NodeIDs()[2], msg3, p2p.Gossiping) if err != nil { t.Fatalf("Failed to publish message: %v", err) } @@ -106,7 +105,7 @@ func TestMetrics(t *testing.T) { nw.RunNetworkSimulation(ctx) t.Logf("Publishing message '%s' from node %d\n", msg1, nw.NodeIDs()[0]) - err = nw.Publish(nw.NodeIDs()[0], msg1, broadcast.Flooding) + err = nw.Publish(nw.NodeIDs()[0], msg1, p2p.Flooding) if err != nil { t.Fatalf("Failed to publish message: %v", err) } diff --git a/p2p/type.go b/p2p/type.go index 4a53daf..2507720 100644 --- a/p2p/type.go +++ b/p2p/type.go @@ -1,12 +1,10 @@ package p2p -import "github.com/elecbug/netkit/p2p/broadcast" - // Message represents a message sent between nodes in the P2P network. type Message struct { - From ID + From PeerID Content string - Protocol broadcast.Protocol + Protocol BroadcastProtocol } // Config holds configuration parameters for the P2P network. @@ -14,5 +12,5 @@ type Config struct { GossipFactor float64 // fraction of neighbors to gossip to } -// ID represents a unique identifier for a node in the P2P network. -type ID uint64 +// PeerID represents a unique identifier for a node in the P2P network. +type PeerID uint64