Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 116 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -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`
Expand All @@ -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:

Expand All @@ -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:
Expand All @@ -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
````
```

---

Expand All @@ -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

Expand All @@ -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() {
Expand All @@ -73,21 +89,108 @@ 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
}
```

> API details may vary by version. See package documentation and examples for the latest usage.

---

## 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.
Expand Down Expand Up @@ -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.

---
Expand All @@ -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.
19 changes: 17 additions & 2 deletions v2/graph/analyzer/analyzer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package analyzer

import (
"runtime"
"sync"

"github.com/elecbug/netkit/v2/graph"
Expand All @@ -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: "",
Expand All @@ -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
Expand Down
12 changes: 6 additions & 6 deletions v2/graph/analyzer/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions v2/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions v2/p2p/p2p.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
9 changes: 9 additions & 0 deletions v2/p2p/peer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading