diff --git a/README.md b/README.md index 645b83d..48f3d88 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Netkit -**Netkit** is a Go graph algorithm library focused on clarity, extensibility, and practical performance. +**Netkit** is a Go toolkit for graph algorithms, network analysis, and P2P network simulation. -It provides reusable graph data structures and common network analysis algorithms, with selected results validated against [NetworkX](https://networkx.org/). +It provides reusable graph data structures, common network analysis algorithms, and a programmable P2P layer for testing message propagation over graph-based network topologies. + +Selected graph algorithm results are validated against [NetworkX](https://networkx.org/). - Go 1.25+ - Module: `github.com/elecbug/netkit` @@ -12,7 +14,7 @@ It provides reusable graph data structures and common network analysis algorithm ## Features -Netkit provides graph utilities and network analysis algorithms for both directed and undirected graphs. +Netkit provides graph utilities, network analysis algorithms, and P2P simulation tools for both directed and undirected graphs. Current focus areas include: @@ -23,6 +25,8 @@ Current focus areas include: - Graph diameter - PageRank - Modularity analysis +- P2P overlay construction +- Message propagation simulation - NetworkX-compatible validation for selected algorithms Implemented or planned algorithms include: @@ -38,13 +42,22 @@ Implemented or planned algorithms include: - Diameter / weighted diameter - Modularity +The P2P simulation layer supports: + +- Creating peer networks from graph topologies +- Defining message propagation behavior +- Configuring processing latency +- Configuring network latency +- Testing broadcast and relay strategies +- Evaluating propagation behavior over generated or custom graphs + --- ## Installation ```bash go get github.com/elecbug/netkit@latest -```` +``` --- @@ -53,6 +66,8 @@ go get github.com/elecbug/netkit@latest > [!NOTE] > Netkit is under active development. Public APIs may change before the first stable release. +The same graph can be used both for graph analysis and as a P2P overlay topology. + ```go package main @@ -61,6 +76,7 @@ import ( "github.com/elecbug/netkit/v2/analyzer" "github.com/elecbug/netkit/v2/graph" + "github.com/elecbug/netkit/v2/p2p" ) func main() { @@ -73,14 +89,81 @@ func main() { g.AddEdge("0", "1") g.AddEdge("1", "2") - a := analyzer.NewAnalyzer(g, nil) + a := analyzer.NewAnalyzer(g, 4, analyzer.DefaultConfig()) degree, err := a.DegreeCentrality() if err != nil { panic(err) } - fmt.Println(degree) + fmt.Println("degree centrality:", degree) + + cfg := &p2p.Config{ + ProcessingLatencyFunc: func(src p2p.PeerID) float64 { + return 100 + }, + NetworkLatencyFunc: func(src, dst p2p.PeerID) float64 { + return 1 + }, + } + + network, err := p2p.New(g, cfg) + if err != nil { + panic(err) + } + + _ = network + + // The graph is now also available as a P2P overlay. + // Users can define propagation behavior and run message dissemination tests + // over the same topology used for graph analysis. +} +``` + +A propagation function can decide which peers should receive a message next. + +```go +package main + +import "github.com/elecbug/netkit/v2/p2p" + +func ForwardToUnseenPeers( + id p2p.PeerID, + msg p2p.Message, + known []p2p.PeerID, + sent []p2p.PeerID, + received []p2p.PeerID, + params map[string]any, +) *[]p2p.PeerID { + result := make([]p2p.PeerID, 0) + + for _, peerID := range known { + alreadySent := false + for _, s := range sent { + if peerID == s { + alreadySent = true + break + } + } + if alreadySent { + continue + } + + alreadyReceived := false + for _, r := range received { + if peerID == r { + alreadyReceived = true + break + } + } + if alreadyReceived { + continue + } + + result = append(result, peerID) + } + + return &result } ``` @@ -88,6 +171,26 @@ func main() { --- +## P2P Simulation + +Netkit can form a P2P network directly from a graph. + +This allows users to generate or load a topology, analyze its structural properties, and then run message propagation experiments on the same network. + +Typical use cases include: + +* Broadcast protocol testing +* Gossip-style dissemination experiments +* Overlay topology evaluation +* Reachability analysis +* Duplicate message analysis +* Latency-sensitive propagation experiments +* Comparing propagation behavior across graph models + +This makes Netkit useful not only as a graph algorithm library, but also as a lightweight experimental framework for P2P network behavior. + +--- + ## Validation Netkit reimplements common graph and network algorithms in Go. @@ -143,12 +246,15 @@ Netkit is designed with the following goals: Algorithms should be readable and easy to inspect. 2. **Extensibility** - Graph structures and analysis components should be easy to extend. + Graph structures, analysis components, and P2P protocol logic should be easy to extend. 3. **Practical performance** - Implementations should be efficient enough for medium-to-large network analysis workloads. + Implementations should be efficient enough for medium-to-large network analysis and simulation workloads. + +4. **P2P experimentation** + Users should be able to construct graph-based overlays and test message propagation behavior on them. -4. **Validation** +5. **Validation** Results should be checked against established libraries such as NetworkX when possible. --- @@ -163,4 +269,4 @@ MIT © 2025 elecbug. See [`LICENSE`](./LICENSE). This project reimplements common graph and network algorithms in Go with selected results validated against NetworkX. -NetworkX is © the NetworkX Developers and distributed under the BSD 3-Clause License. +NetworkX is © the NetworkX Developers and distributed under the BSD 3-Clause License. \ No newline at end of file diff --git a/v2/graph/analyzer/analyzer.go b/v2/graph/analyzer/analyzer.go index 91c7043..49bcd0a 100644 --- a/v2/graph/analyzer/analyzer.go +++ b/v2/graph/analyzer/analyzer.go @@ -1,6 +1,7 @@ package analyzer import ( + "runtime" "sync" "github.com/elecbug/netkit/v2/graph" @@ -16,8 +17,8 @@ type Analyzer struct { cfg *Config // cfg holds configuration options for the analyzer, such as worker counts and normalization settings. } -// NewAnalyzer creates a new Analyzer instance based on the provided graph. -func NewAnalyzer(g *graph.Graph, parallelCoreCount int, cfg *Config) *Analyzer { +// New creates a new Analyzer instance based on the provided graph. +func New(g *graph.Graph, parallelCoreCount int, cfg *Config) *Analyzer { return &Analyzer{ baseGraph: g, graphHash: "", @@ -27,6 +28,20 @@ func NewAnalyzer(g *graph.Graph, parallelCoreCount int, cfg *Config) *Analyzer { } } +// ClearCache clears the cached shortest paths and resets the graph hash. This should be called when the underlying graph +// changes to ensure that subsequent shortest path computations are accurate and not based on stale data. +func (a *Analyzer) ClearCache() string { + a.mu.Lock() + defer a.mu.Unlock() + + a.allShortestPaths = make(map[graph.NodeID]map[graph.NodeID][]graph.Path) + a.graphHash = "" + + runtime.GC() // Trigger garbage collection to free memory used by the old cache + + return a.graphHash +} + // Graph returns the base graph associated with the analyzer. func (a *Analyzer) Graph() *graph.Graph { return a.baseGraph diff --git a/v2/graph/analyzer/analyzer_test.go b/v2/graph/analyzer/analyzer_test.go index daa8315..47d2f9d 100644 --- a/v2/graph/analyzer/analyzer_test.go +++ b/v2/graph/analyzer/analyzer_test.go @@ -36,7 +36,7 @@ func testComputeShortestPath(t *testing.T) { g.AddEdge("A", "C", graph.NewWeight(2)) g.AddEdge("C", "D", graph.NewWeight(1)) - a := analyzer.NewAnalyzer(g, 1, analyzer.DefaultConfig()) + a := analyzer.New(g, 1, analyzer.DefaultConfig()) paths, err := a.ShortestPaths("A", "D") if err != nil { @@ -106,7 +106,7 @@ func TestPerformance(t *testing.T) { t.Fatalf("failed to create graph: %v", err) } - a := analyzer.NewAnalyzer(g, 1, analyzer.DefaultConfig()) + a := analyzer.New(g, 1, analyzer.DefaultConfig()) startTime := time.Now() paths, err := a.ShortestPaths("0", "999") @@ -121,7 +121,7 @@ func TestPerformance(t *testing.T) { duration := time.Since(startTime) fmt.Printf(" - Time taken to compute shortest paths: %v\n", duration) - a = analyzer.NewAnalyzer(g, 4, analyzer.DefaultConfig()) + a = analyzer.New(g, 4, analyzer.DefaultConfig()) startTime = time.Now() pathsCompared, err := a.ShortestPaths("0", "999") @@ -135,7 +135,7 @@ func TestPerformance(t *testing.T) { t.Errorf("expected total distance %v, got %v", paths[0].TotalDistance(), pathsCompared[0].TotalDistance()) } - a = analyzer.NewAnalyzer(g, 16, analyzer.DefaultConfig()) + a = analyzer.New(g, 16, analyzer.DefaultConfig()) startTime = time.Now() pathsCompared, err = a.ShortestPaths("0", "999") @@ -149,7 +149,7 @@ func TestPerformance(t *testing.T) { t.Errorf("expected total distance %v, got %v", paths[0].TotalDistance(), pathsCompared[0].TotalDistance()) } - a = analyzer.NewAnalyzer(g, 32, analyzer.DefaultConfig()) + a = analyzer.New(g, 32, analyzer.DefaultConfig()) startTime = time.Now() pathsCompared, err = a.ShortestPaths("0", "999") @@ -204,7 +204,7 @@ func TestAnaylzer(t *testing.T) { } cfg := analyzer.DefaultConfig() - a := analyzer.NewAnalyzer(g, 16, cfg) + a := analyzer.New(g, 16, cfg) t.Run("ShortestPaths", func(t *testing.T) { results["shortest_paths"] = make(map[string]any) diff --git a/v2/graph/graph.go b/v2/graph/graph.go index 08ca5a4..19a5bd3 100644 --- a/v2/graph/graph.go +++ b/v2/graph/graph.go @@ -34,6 +34,13 @@ func New(directed bool, weighted bool) *Graph { } } +// Free clears all nodes and edges from the graph, effectively resetting it to an empty state. +func (g *Graph) Free() { + for id := range g.nodes { + delete(g.nodes, id) + } +} + /* Node */ // AddNode adds a node to the graph. diff --git a/v2/p2p/p2p.go b/v2/p2p/p2p.go index 0ae9b00..20618a5 100644 --- a/v2/p2p/p2p.go +++ b/v2/p2p/p2p.go @@ -68,6 +68,14 @@ func New(source *graph.Graph, cfg *Config) (*P2P, error) { return &P2P{peers: nodes, cfg: cfg}, nil } +// Free clears all peers from the P2P network, effectively resetting it to an empty state. +func (p *P2P) Free() { + for id := range p.peers { + p.peers[id].eachStop() + delete(p.peers, id) + } +} + /* Basic Actions */ // Run starts the message handling routines for all peers in the network. @@ -250,6 +258,7 @@ func (p *P2P) MessageInfo(peerID PeerID, content string) (map[string]any, error) } info["seen"] = peer.seenAt[content].String() + info["first_from"] = peer.firstFrom[content] return info, nil } diff --git a/v2/p2p/peer.go b/v2/p2p/peer.go index 0533e60..3dee3bf 100644 --- a/v2/p2p/peer.go +++ b/v2/p2p/peer.go @@ -146,3 +146,12 @@ func (p *peer) eachPublish(network *P2P, msg Message) { }(edgeCopy) } } + +// eachStop marks the peer as inactive and closes its message queue. +func (p *peer) eachStop() { + p.mu.Lock() + defer p.mu.Unlock() + + p.alive = false + close(p.msgQueue) +}