From 714dbde7be27a78156a05a17188552c6ae4d3f2f Mon Sep 17 00:00:00 2001 From: elecbug Date: Fri, 17 Oct 2025 10:17:04 +0900 Subject: [PATCH 1/4] refactor: degree cent. config --- graph/algorithm/config.go | 2 +- graph/algorithm/degree_centrality.go | 2 +- graph/algorithm/subconfig.go | 10 ++++++- p2p/helper.go | 44 ++++++++++++++++++++++++++++ p2p/network.go | 4 +-- p2p/node.go | 7 ++--- p2p/p2p_test.go | 24 +++++++-------- p2p/type.go | 6 ++-- 8 files changed, 75 insertions(+), 24 deletions(-) diff --git a/graph/algorithm/config.go b/graph/algorithm/config.go index 8713e06..6029957 100644 --- a/graph/algorithm/config.go +++ b/graph/algorithm/config.go @@ -27,7 +27,7 @@ func Default() *Config { Betweenness: &BetweennessCentralityConfig{Normalized: true}, EdgeBetweenness: &EdgeBetweennessCentralityConfig{Normalized: true}, Eigenvector: &EigenvectorCentralityConfig{MaxIter: 100, Tol: 1e-6, Reverse: false, NStart: nil}, - Degree: &DegreeCentralityConfig{Mode: "total"}, + Degree: &DegreeCentralityConfig{Mode: DegreeCentralityTotal}, Assortativity: &AssortativityCoefficientConfig{Mode: AssortativityProjected, IgnoreSelfLoops: true}, Modularity: &ModularityConfig{Partition: nil}, } diff --git a/graph/algorithm/degree_centrality.go b/graph/algorithm/degree_centrality.go index f4ef2e1..3178421 100644 --- a/graph/algorithm/degree_centrality.go +++ b/graph/algorithm/degree_centrality.go @@ -48,7 +48,7 @@ func DegreeCentrality(g *graph.Graph, cfg *Config) map[graph.NodeID]float64 { denom := float64(n - 1) // --- read config --- - mode := "total" + mode := DegreeCentralityTotal if cfg != nil && cfg.Degree != nil && cfg.Degree.Mode != "" { mode = cfg.Degree.Mode } diff --git a/graph/algorithm/subconfig.go b/graph/algorithm/subconfig.go index 1c7a04f..5a883a9 100644 --- a/graph/algorithm/subconfig.go +++ b/graph/algorithm/subconfig.go @@ -36,9 +36,17 @@ type EigenvectorCentralityConfig struct { NStart *map[graph.NodeID]float64 // initial vector; if nil, uniform distribution } +type DegreeCentralityMode string + +const ( + DegreeCentralityTotal DegreeCentralityMode = "total" + DegreeCentralityIn DegreeCentralityMode = "in" + DegreeCentralityOut DegreeCentralityMode = "out" +) + // DegreeCentralityConfig holds the configuration settings for the degree centrality algorithm. type DegreeCentralityConfig struct { - Mode string + Mode DegreeCentralityMode } // AssortativityMode defines how degree pairs (j,k) are taken on each edge/arc. diff --git a/p2p/helper.go b/p2p/helper.go index a3f733f..ec3911b 100644 --- a/p2p/helper.go +++ b/p2p/helper.go @@ -16,3 +16,47 @@ func LogNormalRand(mu, sigma float64, src rand.Source) float64 { return math.Exp(mu + sigma*z) } + +// PoissonRand generates a Poisson-distributed random integer +// with given lambda parameter. +func PoissonRand(lambda float64, src rand.Source) int { + L := math.Exp(-lambda) + k := 0 + p := 1.0 + + r := rand.New(src) + + for p > L { + k++ + p *= r.Float64() + } + + return k - 1 +} + +// ExponentialRand generates an exponentially distributed random number +// with given rate parameter. +func ExponentialRand(rate float64, src rand.Source) float64 { + r := rand.New(src) + u := r.Float64() + return -math.Log(1-u) / rate +} + +// UniformRand generates a uniformly distributed random number +// between min and max. +func UniformRand(min, max float64, src rand.Source) float64 { + r := rand.New(src) + return min + r.Float64()*(max-min) +} + +// NormalRand generates a normally distributed random number +// with given mean and standard deviation. +func NormalRand(mean, stddev float64, src rand.Source) float64 { + r := rand.New(src) + + u1 := r.Float64() + u2 := r.Float64() + z := math.Sqrt(-2.0*math.Log(u1)) * math.Cos(2*math.Pi*u2) + + return mean + stddev*z +} diff --git a/p2p/network.go b/p2p/network.go index bfb4149..de0123a 100644 --- a/p2p/network.go +++ b/p2p/network.go @@ -73,8 +73,8 @@ func (n *Network) RunNetworkSimulation(ctx context.Context) { wg.Wait() } -// NodeIDs returns a slice of all node IDs in the network. -func (n *Network) NodeIDs() []PeerID { +// PeerIDs returns a slice of all node IDs in the network. +func (n *Network) PeerIDs() []PeerID { ids := make([]PeerID, 0, len(n.nodes)) for id := range n.nodes { diff --git a/p2p/node.go b/p2p/node.go index 36fd489..9462aba 100644 --- a/p2p/node.go +++ b/p2p/node.go @@ -46,9 +46,8 @@ func newNode(id PeerID, nodeLatency float64) *p2pNode { // eachRun starts the message handling routine for the node. func (n *p2pNode) eachRun(network *Network, wg *sync.WaitGroup, ctx context.Context) { - defer wg.Done() - - go func(ctx context.Context) { + go func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() n.alive = true for msg := range n.msgQueue { @@ -81,7 +80,7 @@ func (n *p2pNode) eachRun(network *Network, wg *sync.WaitGroup, ctx context.Cont } } } - }(ctx) + }(ctx, wg) } // copyIDSet creates a shallow copy of a set of IDs. diff --git a/p2p/p2p_test.go b/p2p/p2p_test.go index 81b99ea..1c3fd59 100644 --- a/p2p/p2p_test.go +++ b/p2p/p2p_test.go @@ -27,7 +27,7 @@ func TestGenerateNetwork(t *testing.T) { t.Fatalf("Failed to generate network: %v", err) } - t.Logf("Generated network with %d nodes\n", len(nw.NodeIDs())) + t.Logf("Generated network with %d nodes\n", len(nw.PeerIDs())) msg1 := "Hello, P2P World!" msg2 := "Goodbye, P2P World!" @@ -37,16 +37,16 @@ func TestGenerateNetwork(t *testing.T) { defer cancel() nw.RunNetworkSimulation(ctx) - t.Logf("Publishing message '%s' from node %d\n", msg1, nw.NodeIDs()[0]) - err = nw.Publish(nw.NodeIDs()[0], msg1, p2p.Flooding) + t.Logf("Publishing message '%s' from node %d\n", msg1, nw.PeerIDs()[0]) + err = nw.Publish(nw.PeerIDs()[0], msg1, p2p.Flooding) if err != nil { t.Fatalf("Failed to publish message: %v", err) } time.Sleep(1000 * time.Millisecond) 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, p2p.Gossiping) + t.Logf("Publishing message '%s' from node %d\n", msg2, nw.PeerIDs()[1]) + err = nw.Publish(nw.PeerIDs()[1], msg2, p2p.Gossiping) if err != nil { t.Fatalf("Failed to publish message: %v", err) } @@ -56,8 +56,8 @@ func TestGenerateNetwork(t *testing.T) { t.Logf("Reachability of message '%s': %f\n", msg2, nw.Reachability(msg2)) nw.RunNetworkSimulation(context.Background()) - t.Logf("Publishing message '%s' from node %d\n", msg3, nw.NodeIDs()[2]) - err = nw.Publish(nw.NodeIDs()[2], msg3, p2p.Gossiping) + t.Logf("Publishing message '%s' from node %d\n", msg3, nw.PeerIDs()[2]) + err = nw.Publish(nw.PeerIDs()[2], msg3, p2p.Gossiping) if err != nil { t.Fatalf("Failed to publish message: %v", err) } @@ -66,7 +66,7 @@ func TestGenerateNetwork(t *testing.T) { result := make(map[string]map[string]any) - for _, nodeID := range nw.NodeIDs() { + for _, nodeID := range nw.PeerIDs() { if info, err := nw.MessageInfo(nodeID, msg1); err == nil { result[fmt.Sprintf("msg_1-node_%d", nodeID)] = info } @@ -96,7 +96,7 @@ func TestMetrics(t *testing.T) { t.Fatalf("Failed to generate network: %v", err) } - t.Logf("Generated network with %d nodes\n", len(nw.NodeIDs())) + t.Logf("Generated network with %d nodes\n", len(nw.PeerIDs())) msg1 := "Hello, P2P World!" @@ -104,13 +104,13 @@ func TestMetrics(t *testing.T) { defer cancel() nw.RunNetworkSimulation(ctx) - t.Logf("Publishing message '%s' from node %d\n", msg1, nw.NodeIDs()[0]) - err = nw.Publish(nw.NodeIDs()[0], msg1, p2p.Flooding) + t.Logf("Publishing message '%s' from node %d\n", msg1, nw.PeerIDs()[0]) + err = nw.Publish(nw.PeerIDs()[0], msg1, p2p.Flooding) if err != nil { t.Fatalf("Failed to publish message: %v", err) } time.Sleep(1000 * time.Millisecond) - t.Logf("Number of nodes: %d\n", len(nw.NodeIDs())) + t.Logf("Number of nodes: %d\n", len(nw.PeerIDs())) t.Logf("Reachability of message '%s': %f\n", msg1, nw.Reachability(msg1)) t.Logf("First message reception times of message '%s': %v\n", msg1, nw.FirstMessageReceptionTimes(msg1)) t.Logf("Number of duplicate messages of message '%s': %d\n", msg1, nw.NumberOfDuplicateMessages(msg1)) diff --git a/p2p/type.go b/p2p/type.go index 2507720..593256d 100644 --- a/p2p/type.go +++ b/p2p/type.go @@ -1,5 +1,8 @@ package p2p +// PeerID represents a unique identifier for a node in the P2P network. +type PeerID uint64 + // Message represents a message sent between nodes in the P2P network. type Message struct { From PeerID @@ -11,6 +14,3 @@ type Message struct { type Config struct { GossipFactor float64 // fraction of neighbors to gossip to } - -// PeerID represents a unique identifier for a node in the P2P network. -type PeerID uint64 From 6548d244f6ec823047cddd40f22f94ec14158889 Mon Sep 17 00:00:00 2001 From: elecbug Date: Fri, 17 Oct 2025 10:18:35 +0900 Subject: [PATCH 2/4] comment: p2p --- p2p/{type.go => p2p.go} | 1 + 1 file changed, 1 insertion(+) rename p2p/{type.go => p2p.go} (82%) diff --git a/p2p/type.go b/p2p/p2p.go similarity index 82% rename from p2p/type.go rename to p2p/p2p.go index 593256d..dde0bf5 100644 --- a/p2p/type.go +++ b/p2p/p2p.go @@ -1,3 +1,4 @@ +// Package p2p provides types and interfaces for a peer-to-peer networking simulation. package p2p // PeerID represents a unique identifier for a node in the P2P network. From cbc472b56bfabf416da4a00ad91b2e2a8729c38a Mon Sep 17 00:00:00 2001 From: elecbug Date: Fri, 17 Oct 2025 10:27:14 +0900 Subject: [PATCH 3/4] fix: bug publish bug and update gossiping logic --- README.md | 5 ++--- p2p/network.go | 2 +- p2p/node.go | 16 ++++++++++++++-- p2p/p2p.go | 1 + 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a1a7965..e5c0d2c 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,8 @@ go get github.com/elecbug/netkit@latest ### Graph -- [`network-graph`](./network-graph/): Unweighted network analysis library. - - [`graph`](./network-graph/graph/): Library for creating and building graphs. - - [`standard_graph`](./network-graph/graph/standard_graph/): Library for generating standard graphs like Erdos-Reyni graph. +- [`graph`](./network-graph/graph/): Library for creating and building unweighted graphs. + - [`standard_graph`](./network-graph/graph/standard_graph/): Library for generating standard graphs like Erdos-Reyni graph. - [`algorithm`](./network-graph/algorithm/): Library containing various graph algorithms. ### P2P diff --git a/p2p/network.go b/p2p/network.go index de0123a..9a19a91 100644 --- a/p2p/network.go +++ b/p2p/network.go @@ -91,7 +91,7 @@ func (n *Network) Publish(nodeID PeerID, msg string, protocol BroadcastProtocol) return fmt.Errorf("node %d is not alive", nodeID) } - node.msgQueue <- Message{From: nodeID, Content: msg, Protocol: protocol} + node.msgQueue <- Message{From: nodeID, Content: msg, Protocol: protocol, HopCount: 0} return nil } diff --git a/p2p/node.go b/p2p/node.go index 9462aba..9ae851c 100644 --- a/p2p/node.go +++ b/p2p/node.go @@ -2,6 +2,7 @@ package p2p import ( "context" + "math/rand" "sync" "time" ) @@ -47,8 +48,8 @@ func newNode(id PeerID, nodeLatency float64) *p2pNode { // eachRun starts the message handling routine for the node. func (n *p2pNode) eachRun(network *Network, wg *sync.WaitGroup, ctx context.Context) { go func(ctx context.Context, wg *sync.WaitGroup) { - defer wg.Done() n.alive = true + wg.Done() for msg := range n.msgQueue { select { @@ -96,6 +97,7 @@ func copyIDSet(src map[PeerID]struct{}) map[PeerID]struct{} { func (n *p2pNode) publish(network *Network, msg Message, exclude map[PeerID]struct{}) { content := msg.Content protocol := msg.Protocol + hopCount := msg.HopCount n.mu.Lock() defer n.mu.Unlock() @@ -122,6 +124,10 @@ func (n *p2pNode) publish(network *Network, msg Message, exclude map[PeerID]stru } if protocol == Gossiping && len(willSendEdges) > 0 { + rand.Shuffle(len(willSendEdges), func(i, j int) { + willSendEdges[i], willSendEdges[j] = willSendEdges[j], willSendEdges[i] + }) + k := int(float64(len(willSendEdges)) * network.cfg.GossipFactor) willSendEdges = willSendEdges[:k] } @@ -131,7 +137,13 @@ func (n *p2pNode) publish(network *Network, msg Message, exclude map[PeerID]stru go func(e p2pEdge) { time.Sleep(time.Duration(e.Latency) * time.Millisecond) - network.nodes[e.TargetID].msgQueue <- Message{From: n.id, Content: content, Protocol: protocol} + + network.nodes[e.TargetID].msgQueue <- Message{ + From: n.id, + Content: content, + Protocol: protocol, + HopCount: hopCount + 1, + } }(edgeCopy) } } diff --git a/p2p/p2p.go b/p2p/p2p.go index dde0bf5..55db4af 100644 --- a/p2p/p2p.go +++ b/p2p/p2p.go @@ -9,6 +9,7 @@ type Message struct { From PeerID Content string Protocol BroadcastProtocol + HopCount int } // Config holds configuration parameters for the P2P network. From 54dfe271707e8e536d3a5356c0eb20a94b09d08d Mon Sep 17 00:00:00 2001 From: elecbug Date: Tue, 18 Nov 2025 14:41:52 +0900 Subject: [PATCH 4/4] feat: change standard graph format --- graph/standard_graph/barabasi_albert.go | 4 ++-- graph/standard_graph/erdos_reyni.go | 4 ++-- graph/standard_graph/random_geometric.go | 4 ++-- graph/standard_graph/random_regular.go | 4 ++-- graph/standard_graph/seed.go | 24 ++++++++++++++++-------- graph/standard_graph/watts_strogatz.go | 4 ++-- graph/standard_graph/waxman.go | 4 ++-- p2p/p2p_test.go | 6 ++++-- 8 files changed, 32 insertions(+), 22 deletions(-) diff --git a/graph/standard_graph/barabasi_albert.go b/graph/standard_graph/barabasi_albert.go index 3fc1cf5..1395a58 100644 --- a/graph/standard_graph/barabasi_albert.go +++ b/graph/standard_graph/barabasi_albert.go @@ -5,12 +5,12 @@ import ( ) // BarabasiAlbertGraph generates a graph based on the Barabási–Albert preferential attachment model. -func BarabasiAlbertGraph(n int, m int, isUndirected bool) *graph.Graph { +func (sg *StandardGraph) BarabasiAlbertGraph(n int, m int, isUndirected bool) *graph.Graph { if m < 1 || n <= m { return nil } - ra := genRand() + ra := sg.genRand() g := graph.New(isUndirected) // --- 1. initialize --- diff --git a/graph/standard_graph/erdos_reyni.go b/graph/standard_graph/erdos_reyni.go index fa9a2f9..b8ac61a 100644 --- a/graph/standard_graph/erdos_reyni.go +++ b/graph/standard_graph/erdos_reyni.go @@ -5,8 +5,8 @@ import ( ) // ErdosRenyiGraph generates a random graph based on the Erdős-Rényi model. -func ErdosRenyiGraph(n int, p float64, isUndirected bool) *graph.Graph { - ra := genRand() +func (sg *StandardGraph) ErdosRenyiGraph(n int, p float64, isUndirected bool) *graph.Graph { + ra := sg.genRand() g := graph.New(isUndirected) diff --git a/graph/standard_graph/random_geometric.go b/graph/standard_graph/random_geometric.go index 3701643..a61d5f6 100644 --- a/graph/standard_graph/random_geometric.go +++ b/graph/standard_graph/random_geometric.go @@ -10,12 +10,12 @@ import ( // n = number of nodes // r = connection radius (0~1) // isUndirected = undirected or directed graph -func RandomGeometricGraph(n int, r float64, isUndirected bool) *graph.Graph { +func (sg *StandardGraph) RandomGeometricGraph(n int, r float64, isUndirected bool) *graph.Graph { if n < 1 || r <= 0 { return nil } - ra := genRand() + ra := sg.genRand() g := graph.New(isUndirected) // --- 1. Generate Nodes --- diff --git a/graph/standard_graph/random_regular.go b/graph/standard_graph/random_regular.go index e874a4e..3ea6df0 100644 --- a/graph/standard_graph/random_regular.go +++ b/graph/standard_graph/random_regular.go @@ -6,7 +6,7 @@ import ( // RandomRegularGraph generates a random k-regular graph with n nodes. // Each node has exactly degree k. Returns nil if impossible. -func RandomRegularGraph(n, k int, isUndirected bool) *graph.Graph { +func (sg *StandardGraph) RandomRegularGraph(n, k int, isUndirected bool) *graph.Graph { if k < 0 || n < 1 || k >= n { return nil } @@ -15,7 +15,7 @@ func RandomRegularGraph(n, k int, isUndirected bool) *graph.Graph { return nil } - ra := genRand() + ra := sg.genRand() g := graph.New(isUndirected) // add nodes diff --git a/graph/standard_graph/seed.go b/graph/standard_graph/seed.go index 5af109b..0b98926 100644 --- a/graph/standard_graph/seed.go +++ b/graph/standard_graph/seed.go @@ -7,22 +7,30 @@ import ( const randCode = 42 -var seed = int64(randCode) +type StandardGraph struct { + seed int64 +} + +func NewStandardGraph() *StandardGraph { + return &StandardGraph{ + seed: randCode, + } +} // SetSeed sets the seed for random operations in the graph. -func SetSeed(value int64) { - seed = value +func (g *StandardGraph) SetSeed(value int64) { + g.seed = value } // SetSeedRandom sets the seed to a random value for non-deterministic behavior. -func SetSeedRandom() { - seed = randCode +func (g *StandardGraph) SetSeedRandom() { + g.seed = randCode } -func genRand() *rand.Rand { - localSeed := seed +func (g *StandardGraph) genRand() *rand.Rand { + localSeed := g.seed - if seed == randCode { + if g.seed == randCode { localSeed = rand.Int63() } diff --git a/graph/standard_graph/watts_strogatz.go b/graph/standard_graph/watts_strogatz.go index 9a6e2d6..06fa4ca 100644 --- a/graph/standard_graph/watts_strogatz.go +++ b/graph/standard_graph/watts_strogatz.go @@ -8,12 +8,12 @@ import ( // n = number of nodes // k = each node is connected to k nearest neighbors in ring (must be even) // beta = rewiring probability (0 = regular lattice, 1 = random graph) -func WattsStrogatzGraph(n, k int, beta float64, isUndirected bool) *graph.Graph { +func (sg *StandardGraph) WattsStrogatzGraph(n, k int, beta float64, isUndirected bool) *graph.Graph { if n < 1 || k < 2 || k >= n || k%2 != 0 { return nil } - ra := genRand() + ra := sg.genRand() g := graph.New(isUndirected) // --- 1. Generate Nodes --- diff --git a/graph/standard_graph/waxman.go b/graph/standard_graph/waxman.go index abf8aae..c3a2ba5 100644 --- a/graph/standard_graph/waxman.go +++ b/graph/standard_graph/waxman.go @@ -10,12 +10,12 @@ import ( // n = number of nodes // alpha, beta = Waxman parameters (0 1 || beta <= 0 || beta > 1 { return nil } - ra := genRand() + ra := sg.genRand() g := graph.New(isUndirected) // --- 1. Generate Node Positions --- diff --git a/p2p/p2p_test.go b/p2p/p2p_test.go index 1c3fd59..dbe739a 100644 --- a/p2p/p2p_test.go +++ b/p2p/p2p_test.go @@ -15,7 +15,8 @@ import ( ) func TestGenerateNetwork(t *testing.T) { - g := standard_graph.ErdosRenyiGraph(1000, 50.000/1000, true) + sg := standard_graph.NewStandardGraph() + g := sg.ErdosRenyiGraph(1000, 50.000/1000, true) t.Logf("Generated graph with %d nodes and %d edges\n", len(g.Nodes()), g.EdgeCount()) src := rand.NewSource(time.Now().UnixNano()) @@ -84,7 +85,8 @@ func TestGenerateNetwork(t *testing.T) { } func TestMetrics(t *testing.T) { - g := standard_graph.ErdosRenyiGraph(1000, 50.000/1000, true) + sg := standard_graph.NewStandardGraph() + g := sg.ErdosRenyiGraph(1000, 50.000/1000, true) t.Logf("Generated graph with %d nodes and %d edges\n", len(g.Nodes()), g.EdgeCount()) src := rand.NewSource(time.Now().UnixNano())