Skip to content

Commit ed57a3b

Browse files
committed
feat(graph-modeling): improve graph types and add tests for complete graph
1 parent b5eafc6 commit ed57a3b

9 files changed

+143
-52
lines changed

.github/workflows/go.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Set up Go
1818
uses: actions/setup-go@v4
1919
with:
20-
go-version: '1.20'
20+
go-version: '1.21'
2121

2222
- name: Build
2323
run: go build -v ./...

io/io.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -50,22 +50,22 @@ func FromAdjacencyList(reader io.Reader) {
5050
return
5151
}
5252
slog.Info(fmt.Sprintf("read: %+v", read))
53-
g.AddEdgesFromIntEdgeList(lineCount, lineToList(read))
53+
g.AddEdgesFromIntEdgeList(model.Node(lineCount), lineToList(read))
5454
lineCount++
5555
}
5656

5757
// todo remove this log
5858
slog.Info(fmt.Sprintf("graph: %+v", g))
5959
}
6060

61-
func lineToList(values []string) (integers []int) {
62-
integers = make([]int, len(values))
61+
func lineToList(values []string) (integers []model.Node) {
62+
integers = make([]model.Node, len(values))
6363
for index, value := range values {
6464
valueInt, err := strconv.Atoi(value)
6565
if err != nil {
6666
panic(err)
6767
}
68-
integers[index] = valueInt
68+
integers[index] = model.Node(valueInt)
6969
}
7070
return
7171
}

model/generator_classic.go

+11-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package model
22

3-
func CompleteGraph(numberOfNodes int) (g UndirectedGraph) {
4-
g = UndirectedGraph{}
5-
for i := 0; i <= numberOfNodes; i++ {
6-
for j := i + 1; j <= numberOfNodes; i++ {
3+
func CompleteGraph(numberOfNodes int) *UndirectedGraph {
4+
g := &UndirectedGraph{}
5+
for i := 0; i < numberOfNodes; i++ {
6+
for j := i + 1; j < numberOfNodes; j++ {
77
g.AddEdge(Edge{
8-
Node1: Node{NodeId: i},
9-
Node2: Node{NodeId: j},
8+
Node1: Node(i),
9+
Node2: Node(j),
1010
})
1111
}
1212
}
@@ -22,8 +22,8 @@ func LadderGraph(nodesInSinglePath int) (g UndirectedGraph) {
2222
}
2323
for i := 0; i <= nodesInSinglePath; i++ {
2424
g.AddEdge(Edge{
25-
Node1: Node{NodeId: i},
26-
Node2: Node{NodeId: i + nodesInSinglePath},
25+
Node1: Node(i),
26+
Node2: Node(i + nodesInSinglePath),
2727
})
2828
}
2929
return g
@@ -33,12 +33,11 @@ func LadderGraph(nodesInSinglePath int) (g UndirectedGraph) {
3333
func CircularLadderGraph(nodesInSinglePath int) (g UndirectedGraph) {
3434
g = LadderGraph(nodesInSinglePath)
3535
g.AddEdge(Edge{
36-
Node1: Node{},
37-
Node2: Node{NodeId: nodesInSinglePath - 1},
36+
Node2: Node(nodesInSinglePath - 1),
3837
})
3938
g.AddEdge(Edge{
40-
Node1: Node{NodeId: nodesInSinglePath},
41-
Node2: Node{NodeId: 2*nodesInSinglePath - 1},
39+
Node1: Node(nodesInSinglePath),
40+
Node2: Node(2*nodesInSinglePath - 1),
4241
})
4342
return g
4443
}

model/generator_classic_test.go

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package model
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestCompleteGraph(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
numberOfNodes int
11+
expectedEdges int
12+
expectedDegree int
13+
}{
14+
{
15+
name: "CompleteGraph with 0 nodes",
16+
numberOfNodes: 0,
17+
expectedEdges: 0,
18+
expectedDegree: 0,
19+
},
20+
{
21+
name: "CompleteGraph with 2 nodes",
22+
numberOfNodes: 2,
23+
expectedEdges: 1, // 2C1 (combination)
24+
expectedDegree: 1,
25+
},
26+
{
27+
name: "CompleteGraph with 5 nodes",
28+
numberOfNodes: 5,
29+
expectedEdges: 10, // 5C2 (combination)
30+
expectedDegree: 4,
31+
},
32+
{
33+
name: "CompleteGraph with 10 nodes",
34+
numberOfNodes: 10,
35+
expectedEdges: 45, // 10C2 (combination)
36+
expectedDegree: 9,
37+
},
38+
}
39+
40+
for _, tt := range tests {
41+
t.Run(tt.name, func(t *testing.T) {
42+
g := CompleteGraph(tt.numberOfNodes)
43+
44+
// Check the number of edges
45+
actualEdges := g.NumberOfEdges()
46+
if actualEdges != tt.expectedEdges {
47+
t.Errorf("Expected %d edges, but got %d", tt.expectedEdges, actualEdges)
48+
}
49+
50+
// Check the degree of each node
51+
for i := 0; i < tt.numberOfNodes; i++ {
52+
actualDegree := g.NodeDegree(Node(i))
53+
if actualDegree != tt.expectedDegree {
54+
t.Errorf("Expected degree of node %d to be %d, but got %d", i, tt.expectedDegree, actualDegree)
55+
}
56+
}
57+
})
58+
}
59+
}

model/generator_random.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ import (
3030
// References: [1] Vladimir Batagelj and Ulrik Brandes, "Efficient generation of large random networks", Phys. Rev. E, 71, 036113, 2005.
3131
func FastGNPRandomGraph(numberOfNodes int, probabilityForEdgeCreation float64) (g UndirectedGraph) {
3232
g = UndirectedGraph{}
33-
g.Edges = make(map[int][]int)
34-
g.Nodes = make(map[int]bool)
33+
g.Edges = make(map[Node][]Node)
34+
g.Nodes = make(map[Node]bool, numberOfNodes)
3535
lp := math.Log(1.0 - probabilityForEdgeCreation)
3636
// Nodes in graph are from 0,n-1 (start with v as the second node index).
3737
v := 1
@@ -43,7 +43,7 @@ func FastGNPRandomGraph(numberOfNodes int, probabilityForEdgeCreation float64) (
4343
w = w - v
4444
v = v + 1
4545
if v < numberOfNodes {
46-
g.AddEdge(Edge{Node{v}, Node{w}})
46+
g.AddEdge(Edge{Node(v), Node(w)})
4747
}
4848
}
4949
}
@@ -57,12 +57,12 @@ func FastGNPRandomGraph(numberOfNodes int, probabilityForEdgeCreation float64) (
5757
// in section 3.4.2 of [1]
5858
// References: [1] Donald E. Knuth, The Art of Computer Programming,
5959
// Volume 2/Seminumerical algorithms, Third Edition, Addison-Wesley, 1997.
60-
func DenseGNMRandomGraph(numberOfNodes int, numberOfEdges int) (g UndirectedGraph) {
60+
func DenseGNMRandomGraph(numberOfNodes int, numberOfEdges int) (g *UndirectedGraph) {
6161
edgesMax := numberOfNodes * (numberOfNodes - 1) // 2
6262
if numberOfEdges >= edgesMax {
6363
return CompleteGraph(numberOfNodes)
6464
} else {
65-
g = UndirectedGraph{}
65+
g = &UndirectedGraph{}
6666
}
6767
if numberOfNodes == 1 || numberOfEdges >= edgesMax {
6868
return g
@@ -71,7 +71,7 @@ func DenseGNMRandomGraph(numberOfNodes int, numberOfEdges int) (g UndirectedGrap
7171
u, v, t, k := 0, 0, 0, 0
7272
for {
7373
if (t + rand.Int()*(edgesMax-t)) < (numberOfEdges - k) {
74-
g.AddEdge(Edge{Node{u}, Node{v}})
74+
g.AddEdge(Edge{Node(u), Node(v)})
7575
k = k + 1
7676
if k == numberOfEdges {
7777
return g

model/graph.go

+55-22
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,86 @@
11
package model
22

3-
import "fmt"
3+
import (
4+
"fmt"
5+
)
46

57
type Graph interface {
68
AddEdge(edge Edge)
79
AddNode(node Node)
810
GetEdgeTuples() []Edge
9-
Sample(sampler *SamplingStrategy, samplingRate float32) Graph
11+
Sample(sampler SamplingStrategy, samplingRate float32) UndirectedGraph
12+
NodeDegree(node Node) int
13+
NumberOfEdges() int
1014
}
1115

12-
type UndirectedGraph struct {
13-
Nodes map[int]bool
14-
Edges map[int][]int
15-
}
16-
17-
func (g UndirectedGraph) String() string {
18-
return fmt.Sprintf("graph has %d nodes and %d edges", len(g.Nodes), len(g.Edges))
19-
}
16+
type Node int
2017

2118
type Edge struct {
2219
Node1 Node
2320
Node2 Node
2421
}
2522

26-
type Node struct {
27-
NodeId int
23+
type UndirectedGraph struct {
24+
Nodes map[Node]bool
25+
Edges map[Node][]Node
26+
}
27+
28+
func (g UndirectedGraph) String() string {
29+
return fmt.Sprintf("graph has %d nodes and %d edges. Nodes: %v; Edges: %v", len(g.Nodes), len(g.Edges), g.Nodes, g.Edges)
2830
}
2931

3032
func (g *UndirectedGraph) AddEdge(edge Edge) {
33+
if g.Edges == nil {
34+
g.Edges = make(map[Node][]Node)
35+
}
3136
g.AddNode(edge.Node1)
3237
g.AddNode(edge.Node2)
33-
g.Edges[edge.Node1.NodeId] = append(g.Edges[edge.Node1.NodeId], edge.Node2.NodeId)
38+
g.Edges[edge.Node1] = append(g.Edges[edge.Node1], edge.Node2)
39+
g.Edges[edge.Node2] = append(g.Edges[edge.Node2], edge.Node1)
3440
}
3541

3642
func (g *UndirectedGraph) AddNode(node Node) {
37-
g.Nodes[node.NodeId] = true
43+
if g.Nodes == nil {
44+
g.Nodes = make(map[Node]bool)
45+
}
46+
g.Nodes[node] = true
3847
}
3948

4049
func (g *UndirectedGraph) AddEdgesFromIntTupleList(edges [][2]int) {
4150
for _, nodes := range edges {
42-
g.AddEdge(Edge{Node{nodes[0]}, Node{nodes[1]}})
51+
g.AddEdge(Edge{Node(nodes[0]), Node(nodes[1])})
4352
}
4453
}
4554

46-
func (g *UndirectedGraph) AddEdgesFromIntEdgeList(sourceNode int, edges []int) {
55+
func (g *UndirectedGraph) AddEdgesFromIntEdgeList(sourceNode Node, edges []Node) {
4756
for _, node := range edges {
48-
g.AddEdge(Edge{Node{sourceNode}, Node{node}})
57+
g.AddEdge(Edge{sourceNode, node})
4958
}
5059
}
5160

52-
func (g *UndirectedGraph) AddNodes(nodes map[int]bool) {
53-
for node := range nodes {
54-
g.AddNode(Node{node})
61+
func (g *UndirectedGraph) AddNodes(nodes []Node) {
62+
for _, node := range nodes {
63+
g.AddNode(node)
5564
}
5665
}
5766

67+
// NodeDegree returns the degree (number of incident edges) of the specified node in the graph.
68+
func (g *UndirectedGraph) NodeDegree(node Node) int {
69+
// Check if the node exists in the graph
70+
if _, exists := g.Nodes[node]; !exists {
71+
return 0 // Node not found, degree is 0
72+
}
73+
74+
// Retrieve the neighbors of the node and return the degree
75+
return len(g.Edges[node])
76+
}
77+
5878
// todo suggest rename to GetEdges
5979
func (g *UndirectedGraph) GetEdgeTuples() []Edge {
6080
var edges []Edge
6181
for node1, array := range g.Edges {
62-
for node2 := range array {
63-
edges = append(edges, Edge{Node{node1}, Node{node2}})
82+
for _, node2 := range array {
83+
edges = append(edges, Edge{node1, node2})
6484
}
6585
}
6686
return edges
@@ -69,3 +89,16 @@ func (g *UndirectedGraph) GetEdgeTuples() []Edge {
6989
func (g *UndirectedGraph) Sample(sampler SamplingStrategy, samplingRate float32) UndirectedGraph {
7090
return sampler.Sample(g, samplingRate)
7191
}
92+
93+
// NumberOfEdges returns the total number of edges in the undirected graph.
94+
func (g *UndirectedGraph) NumberOfEdges() int {
95+
totalEdges := 0
96+
97+
// Iterate over each node's neighbors and count the unique edges
98+
for _, neighbors := range g.Edges {
99+
totalEdges += len(neighbors)
100+
}
101+
102+
// Divide by 2 to account for the fact that each edge is counted twice (undirected graph)
103+
return totalEdges / 2
104+
}

model/random_sampling_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func BenchmarkFastGNPRandomGraph(b *testing.B) {
1616
// Run the benchmark
1717
for i := 0; i < b.N; i++ {
1818
// Reset the random seed for each iteration to ensure consistent results
19-
rand.Seed(int64(i))
19+
rand.NewSource(int64(i))
2020

2121
// Run the FastGNPRandomGraph function
2222
g := FastGNPRandomGraph(numberOfNodes, probabilityForEdgeCreation)

model/sampling.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ func (strategy *RandomNodeSampling) Sample(g *UndirectedGraph, samplingRate floa
3232
ng = UndirectedGraph{}
3333
sampleSize := int(float32(len(g.Nodes)) * samplingRate)
3434
for _, node := range rand.Perm(sampleSize) {
35-
ng.AddNode(Node{NodeId: node})
35+
ng.AddNode(Node(node))
3636
}
3737
for node1 := range ng.Nodes {
38-
for node2 := range g.Edges[node1] {
38+
for _, node2 := range g.Edges[node1] {
3939
ng.AddEdge(Edge{
40-
Node1: Node{node1},
41-
Node2: Node{node2},
40+
Node1: node1,
41+
Node2: node2,
4242
})
4343
}
4444
}
@@ -62,7 +62,7 @@ func (strategy *RandomDegreeNodeSampling) Sample(g *UndirectedGraph, samplingRat
6262
}
6363
pick := choice.Pick()
6464
// todo make sampling without replacement
65-
ng.AddNode(Node{pick.(int)})
65+
ng.AddNode(Node(pick.(int)))
6666
}
6767
return ng, nil
6868
}

model/utils.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package model
33
func Pairwise(nodeIds []int) (e []Edge) {
44
e = []Edge{}
55
for i := 0; i < len(nodeIds)-1; i++ {
6-
e = append(e, Edge{Node{nodeIds[i]}, Node{nodeIds[i+1]}})
6+
e = append(e, Edge{Node(nodeIds[i]), Node(nodeIds[i+1])})
77
}
88
return e
99
}

0 commit comments

Comments
 (0)