Skip to content

Commit 456af34

Browse files
committed
feat: add grid and triangle hex graph generation functions with tagging support
1 parent fe0ab3f commit 456af34

6 files changed

Lines changed: 388 additions & 0 deletions

File tree

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(),

v2/graph/standard/standart_graph_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import (
66
"sync"
77
"testing"
88

9+
"github.com/elecbug/netkit/v2/graph"
910
"github.com/elecbug/netkit/v2/graph/standard"
1011
)
1112

1213
func TestStandardGraph(t *testing.T) {
1314
fmt.Println("Test Standard Graph Generation")
15+
testGridGraph(t)
16+
testTriangleHexGraph(t)
1417
testBarabasiAlbertGraph(t)
1518
testErdosRenyiGraph(t)
1619
testRandomGeometricGraph(t)
@@ -553,6 +556,104 @@ func testWattsStrogatzGraph(t *testing.T) {
553556
}
554557
}
555558

559+
// testGridGraph tests the grid graph generation function.
560+
func testGridGraph(t *testing.T) {
561+
fmt.Println("- Test Grid Graph")
562+
563+
rows := 10
564+
cols := 10
565+
torus := false
566+
567+
g, err := standard.GridGraph(0, false, standard.Unweighted(), rows, cols, torus)
568+
if err != nil {
569+
t.Fatalf("failed to generate grid graph: %v", err)
570+
}
571+
572+
expectedNodes := rows * cols
573+
if len(g.Nodes()) != expectedNodes {
574+
t.Errorf("expected %d nodes, got %d", expectedNodes, len(g.Nodes()))
575+
}
576+
577+
for i := 0; i < rows; i++ {
578+
for j := 0; j < cols; j++ {
579+
id := graph.NodeID(fmt.Sprintf("%d", i*cols+j))
580+
node, err := g.Node(id)
581+
if err != nil {
582+
t.Errorf("failed to get node: %v", err)
583+
continue
584+
}
585+
586+
x, okX := node.Tag("x")
587+
y, okY := node.Tag("y")
588+
if !okX || !okY {
589+
t.Errorf("node %s is missing tags", id)
590+
continue
591+
}
592+
593+
if x != fmt.Sprintf("%d", i) || y != fmt.Sprintf("%d", j) {
594+
t.Errorf("node %s has incorrect tags: x=%s, y=%s", id, x, y)
595+
}
596+
597+
expectedDegree := 4
598+
if i == 0 || i == rows-1 {
599+
expectedDegree--
600+
}
601+
if j == 0 || j == cols-1 {
602+
expectedDegree--
603+
}
604+
605+
if node.Degree() != expectedDegree {
606+
t.Errorf("node %s has degree %d, expected %d", id, node.Degree(), expectedDegree)
607+
}
608+
}
609+
}
610+
}
611+
612+
// testTriangleHexGraph tests the triangle hex graph generation function.
613+
func testTriangleHexGraph(t *testing.T) {
614+
fmt.Println("- Test Triangle Hex Graph")
615+
616+
edge := 3
617+
618+
g, err := standard.TriangleHexGraph(0, false, standard.Unweighted(), edge)
619+
if err != nil {
620+
t.Fatalf("failed to generate triangle hex graph: %v", err)
621+
}
622+
623+
expectedNodes := 3*edge*(edge+1)/2 + 1
624+
if len(g.Nodes()) != expectedNodes {
625+
t.Errorf("expected %d nodes, got %d", expectedNodes, len(g.Nodes()))
626+
}
627+
628+
for i := 0; i < expectedNodes; i++ {
629+
nodeID := graph.NodeID(fmt.Sprintf("%d", i))
630+
631+
node, err := g.Node(nodeID)
632+
if err != nil {
633+
t.Errorf("failed to get node: %v", err)
634+
continue
635+
}
636+
637+
q, okQ := node.Tag("q")
638+
r, okR := node.Tag("r")
639+
if !okQ || !okR {
640+
t.Errorf("node %s is missing tags", nodeID)
641+
continue
642+
}
643+
644+
qInt := 0
645+
rInt := 0
646+
fmt.Sscanf(q, "%d", &qInt)
647+
fmt.Sscanf(r, "%d", &rInt)
648+
649+
expectedDegree := degreeOfTriangleHex(qInt, rInt, edge)
650+
651+
if node.Degree() != expectedDegree {
652+
t.Errorf("node %s (q=%d, r=%d) has degree %d, expected %d", nodeID, qInt, rInt, node.Degree(), expectedDegree)
653+
}
654+
}
655+
}
656+
556657
// testGenerateFromConfig tests the StandardGraph function with various configurations.
557658
func testGenerateFromConfig(t *testing.T) {
558659
fmt.Println("- Test StandardGraph with Config")
@@ -673,3 +774,37 @@ func poissonLowerExtreme(n int, lambda float64) int {
673774

674775
return -1
675776
}
777+
778+
// degreeOfTriangleHex calculates the degree of a node in the triangle hex graph based on its q and r coordinates and the edge length.
779+
func degreeOfTriangleHex(q, r, n int) int {
780+
dirs := [][2]int{
781+
{1, 0},
782+
{-1, 0},
783+
{0, 1},
784+
{0, -1},
785+
{1, -1},
786+
{-1, 1},
787+
}
788+
789+
degree := 0
790+
791+
for _, d := range dirs {
792+
nq := q + d[0]
793+
nr := r + d[1]
794+
795+
if exists(nq, nr, n) {
796+
degree++
797+
}
798+
}
799+
800+
return degree
801+
}
802+
803+
// exists checks if the coordinates (q, r) are valid for a node in the triangle hex graph with edge length n.
804+
func exists(q, r, n int) bool {
805+
limit := n - 1
806+
807+
return q >= -limit && q <= limit &&
808+
r >= -limit && r <= limit &&
809+
q+r >= -limit && q+r <= limit
810+
}

0 commit comments

Comments
 (0)