Skip to content

Commit 7d8f9cd

Browse files
authored
Merge pull request #42 from elecbug/elecbug/master
Update v0.12.3
2 parents c649267 + 456af34 commit 7d8f9cd

7 files changed

Lines changed: 415 additions & 6 deletions

File tree

v2/graph/analyzer/analyzer_test.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import (
1111
"github.com/elecbug/netkit/v2/graph/standard"
1212
)
1313

14-
// TestShortestPaths tests the ShortestPaths method of the Analyzer to ensure it correctly finds the shortest path between two nodes in a graph.
14+
// TestShortestPaths tests the functionality of the Analyzer's shortest path computations, including
15+
// cache management and performance with different parallel core counts.
1516
func TestShortestPaths(t *testing.T) {
1617
fmt.Println("Test Shortest Paths")
1718
testComputeShortestPath(t)
@@ -86,6 +87,9 @@ func testComputeShortestPath(t *testing.T) {
8687
}
8788
}
8889

90+
// testPerformance creates a larger random graph and tests the performance of the ShortestPaths method with different
91+
// parallel core counts. It measures the time taken to compute shortest paths and to retrieve cached results, ensuring
92+
// that the method works correctly and efficiently under various conditions.
8993
func testPerformance(t *testing.T) {
9094
fmt.Println("- Test Performance")
9195

@@ -103,45 +107,62 @@ func testPerformance(t *testing.T) {
103107
a := analyzer.NewAnalyzer(g, 1)
104108

105109
startTime := time.Now()
106-
_, err = a.ShortestPaths("0", "999")
110+
paths, err := a.ShortestPaths("0", "999")
111+
// NOTE: paths may be empty if 0 and 999 are not connected (P(no path) ≈ 0.009%),
112+
// but we just want to test the performance of the method,
113+
// so we won't fail the test in that case.
107114
if err != nil {
108115
t.Fatalf("unexpected error: %v", err)
116+
} else if len(paths) > 0 {
117+
fmt.Printf(" - Found shortest paths from 0 to 999: %v\n", paths[0])
109118
}
110119
duration := time.Since(startTime)
111120
fmt.Printf(" - Time taken to compute shortest paths: %v\n", duration)
112121

113122
a = analyzer.NewAnalyzer(g, 4)
114123

115124
startTime = time.Now()
116-
_, err = a.ShortestPaths("0", "999")
125+
pathsCompared, err := a.ShortestPaths("0", "999")
117126
if err != nil {
118127
t.Fatalf("unexpected error: %v", err)
119128
}
120129
duration = time.Since(startTime)
121130
fmt.Printf(" - Time taken to compute shortest paths with 4 cores: %v\n", duration)
122131

132+
if paths[0].TotalDistance() != pathsCompared[0].TotalDistance() {
133+
t.Errorf("expected total distance %v, got %v", paths[0].TotalDistance(), pathsCompared[0].TotalDistance())
134+
}
135+
123136
a = analyzer.NewAnalyzer(g, 16)
124137

125138
startTime = time.Now()
126-
_, err = a.ShortestPaths("0", "999")
139+
pathsCompared, err = a.ShortestPaths("0", "999")
127140
if err != nil {
128141
t.Fatalf("unexpected error: %v", err)
129142
}
130143
duration = time.Since(startTime)
131144
fmt.Printf(" - Time taken to compute shortest paths with 16 cores: %v\n", duration)
132145

146+
if paths[0].TotalDistance() != pathsCompared[0].TotalDistance() {
147+
t.Errorf("expected total distance %v, got %v", paths[0].TotalDistance(), pathsCompared[0].TotalDistance())
148+
}
149+
133150
a = analyzer.NewAnalyzer(g, 32)
134151

135152
startTime = time.Now()
136-
_, err = a.ShortestPaths("0", "999")
153+
pathsCompared, err = a.ShortestPaths("0", "999")
137154
if err != nil {
138155
t.Fatalf("unexpected error: %v", err)
139156
}
140157
duration = time.Since(startTime)
141158
fmt.Printf(" - Time taken to compute shortest paths with 32 cores: %v\n", duration)
142159

160+
if paths[0].TotalDistance() != pathsCompared[0].TotalDistance() {
161+
t.Errorf("expected total distance %v, got %v", paths[0].TotalDistance(), pathsCompared[0].TotalDistance())
162+
}
163+
143164
startTime = time.Now()
144-
_, err = a.ShortestPaths("0", "999")
165+
_, err = a.ShortestPaths("0", "1")
145166
if err != nil {
146167
t.Fatalf("unexpected error: %v", err)
147168
}

v2/graph/node.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ type Weight float64
1212
type Node struct {
1313
ID NodeID // ID is the unique identifier for the node.
1414
edges map[NodeID]Weight // Edges maps the destination NodeID to the weight of the edge.
15+
tags map[string]string // Tags can hold additional metadata about the node.
1516
}
1617

1718
// NewNode creates a new node with the given ID.
1819
func NewNode(id NodeID) *Node {
1920
return &Node{
2021
ID: id,
2122
edges: make(map[NodeID]Weight),
23+
tags: make(map[string]string),
2224
}
2325
}
2426

@@ -60,6 +62,50 @@ func (n *Node) edgeWeight(to NodeID) (Weight, error) {
6062
return weight, nil
6163
}
6264

65+
/* Tagging */
66+
67+
// AddTag adds a key-value pair as a tag to the node.
68+
// It returns an error if a tag with the same key already exists.
69+
func (n *Node) AddTag(key, value string) error {
70+
if n.HasTag(key) {
71+
return fmt.Errorf("tag with key %s already exists", key)
72+
}
73+
74+
n.tags[key] = value
75+
return nil
76+
}
77+
78+
// UpdateTag updates the value of an existing tag on the node.
79+
// If the tag does not exist, it will be added.
80+
func (n *Node) UpdateTag(key, value string) {
81+
n.tags[key] = value
82+
}
83+
84+
// RemoveTag removes a tag from the node by its key. It returns an error if the tag does not exist.
85+
func (n *Node) RemoveTag(key string) error {
86+
if _, exists := n.tags[key]; !exists {
87+
return fmt.Errorf("tag with key %s does not exist", key)
88+
}
89+
90+
delete(n.tags, key)
91+
return nil
92+
}
93+
94+
// HasTag checks if a tag with the specified key exists on the node.
95+
func (n *Node) HasTag(key string) bool {
96+
_, exists := n.tags[key]
97+
return exists
98+
}
99+
100+
// Tag retrieves the value of a tag by its key. It returns an error if the tag does not exist.
101+
func (n *Node) Tag(key string) (string, bool) {
102+
value, exists := n.tags[key]
103+
if !exists {
104+
return "", false
105+
}
106+
return value, true
107+
}
108+
63109
/* Connectivity */
64110

65111
// Neighbors returns a slice of NodeIDs representing the neighbors of this node.

v2/graph/standard/common.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type WeightedFunc func(from, to graph.NodeID) *graph.Weight
1414
type GraphType string
1515

1616
const (
17+
Grid GraphType = "grid"
18+
TriangleHex GraphType = "triangle_hex"
1719
ErdosRenyi GraphType = "erdos_renyi"
1820
BarabasiAlbert GraphType = "barabasi_albert"
1921
WattsStrogatz GraphType = "watts_strogatz"
@@ -52,6 +54,26 @@ func Unweighted() WeightedFunc {
5254
// StandardGraph generates a graph based on the provided configuration. It supports various graph types and parameters.
5355
func StandardGraph(seed int, directed bool, weightFunc WeightedFunc, config GraphConfig) (*graph.Graph, error) {
5456
switch config.Type {
57+
case Grid:
58+
rows, ok := config.Params["rows"].(int)
59+
if !ok {
60+
return nil, fmt.Errorf("invalid parameter 'rows' for grid graph")
61+
}
62+
cols, ok := config.Params["cols"].(int)
63+
if !ok {
64+
return nil, fmt.Errorf("invalid parameter 'cols' for grid graph")
65+
}
66+
torus, ok := config.Params["torus"].(bool)
67+
if !ok {
68+
return nil, fmt.Errorf("invalid parameter 'torus' for grid graph")
69+
}
70+
return GridGraph(seed, directed, weightFunc, rows, cols, torus)
71+
case TriangleHex:
72+
edge, ok := config.Params["edge"].(int)
73+
if !ok {
74+
return nil, fmt.Errorf("invalid parameter 'edge' for triangle hex graph")
75+
}
76+
return TriangleHexGraph(seed, directed, weightFunc, edge)
5577
case ErdosRenyi:
5678
n, ok := config.Params["n"].(int)
5779
if !ok {

v2/graph/standard/grid.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package standard
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/elecbug/netkit/v2/graph"
7+
)
8+
9+
// GridGraph generates a grid graph with the specified number of rows and columns.
10+
// If torus is true, the graph will wrap around at the edges, creating a toroidal structure.
11+
func GridGraph(seed int, directed bool, weightFunc WeightedFunc, rows, cols int, torus bool) (*graph.Graph, error) {
12+
if weightFunc == nil {
13+
weightFunc = Unweighted()
14+
}
15+
if rows < 0 || cols < 0 {
16+
return nil, fmt.Errorf("rows and cols must be non-negative")
17+
}
18+
19+
g := graph.New(directed, true)
20+
21+
nodeID := func(row, col int) graph.NodeID {
22+
return graph.NodeID(fmt.Sprintf("%d", row*cols+col))
23+
}
24+
25+
for i := 0; i < rows; i++ {
26+
for j := 0; j < cols; j++ {
27+
g.AddNode(nodeID(i, j))
28+
if node, err := g.Node(nodeID(i, j)); err != nil {
29+
return nil, fmt.Errorf("failed to retrieve node: %w", err)
30+
} else {
31+
node.AddTag("x", fmt.Sprintf("%d", i))
32+
node.AddTag("y", fmt.Sprintf("%d", j))
33+
}
34+
}
35+
}
36+
37+
for i := 0; i < rows; i++ {
38+
for j := 0; j < cols; j++ {
39+
id := nodeID(i, j)
40+
41+
if torus {
42+
if rows > 2 {
43+
ni := (i + 1) % rows
44+
if err := g.AddEdge(id, nodeID(ni, j), weightFunc(id, nodeID(ni, j))); err != nil {
45+
return nil, fmt.Errorf("failed to add edge: %w", err)
46+
}
47+
}
48+
} else {
49+
if i < rows-1 {
50+
if err := g.AddEdge(id, nodeID(i+1, j), weightFunc(id, nodeID(i+1, j))); err != nil {
51+
return nil, fmt.Errorf("failed to add edge: %w", err)
52+
}
53+
}
54+
}
55+
56+
if torus {
57+
if cols > 2 {
58+
nj := (j + 1) % cols
59+
if err := g.AddEdge(id, nodeID(i, nj), weightFunc(id, nodeID(i, nj))); err != nil {
60+
return nil, fmt.Errorf("failed to add edge: %w", err)
61+
}
62+
}
63+
} else {
64+
if j < cols-1 {
65+
if err := g.AddEdge(id, nodeID(i, j+1), weightFunc(id, nodeID(i, j+1))); err != nil {
66+
return nil, fmt.Errorf("failed to add edge: %w", err)
67+
}
68+
}
69+
}
70+
}
71+
}
72+
73+
return g, nil
74+
}

v2/graph/standard/random_geometric.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ func RandomGeometricGraph(seed int, directed bool, weightFunc WeightedFunc, n in
3333
if err := g.AddNode(id); err != nil {
3434
return nil, fmt.Errorf("failed to add node: %w", err)
3535
}
36+
if node, err := g.Node(id); err != nil {
37+
return nil, fmt.Errorf("failed to retrieve node: %w", err)
38+
} else {
39+
node.AddTag("x", fmt.Sprintf("%f", rr.Float64()))
40+
node.AddTag("y", fmt.Sprintf("%f", rr.Float64()))
41+
}
3642
positions[id] = point{
3743
x: rr.Float64(),
3844
y: rr.Float64(),

0 commit comments

Comments
 (0)