engine: undirected simple graph with deterministic neighbour order
Adjacency lives in slices, not maps, on purpose: Go randomises map iteration order between runs, and the engine's determinism guarantee needs every graph walk to visit neighbours in the same order. AddEdge mirrors networkx semantics (duplicates and self loops are no-ops) so the Holme-Kim port can lean on the same behaviour. Receiver names stay short per Go convention (g *Graph); everything else uses descriptive names.
This commit is contained in:
parent
33d40a18ea
commit
33a85cb720
3 changed files with 133 additions and 6 deletions
50
internal/engine/graph.go
Normal file
50
internal/engine/graph.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package engine
|
||||
|
||||
import "slices"
|
||||
|
||||
// Graph is an undirected simple graph on nodes 0..n-1. Neighbours are kept
|
||||
// in insertion order (a slice, not a map) because Go randomises map
|
||||
// iteration order and the engine must be deterministic: every walk over the
|
||||
// graph has to visit nodes in the same order on every run.
|
||||
type Graph struct {
|
||||
adj [][]int
|
||||
edges int
|
||||
}
|
||||
|
||||
// NewGraph returns an empty graph with n nodes and no edges.
|
||||
func NewGraph(n int) *Graph {
|
||||
return &Graph{adj: make([][]int, n)}
|
||||
}
|
||||
|
||||
// NumNodes returns the number of nodes.
|
||||
func (g *Graph) NumNodes() int { return len(g.adj) }
|
||||
|
||||
// NumEdges returns the number of undirected edges.
|
||||
func (g *Graph) NumEdges() int { return g.edges }
|
||||
|
||||
// AddEdge connects u and v and reports whether the edge was added. Self
|
||||
// loops and duplicate edges are ignored (reported as false), mirroring how
|
||||
// networkx's Graph.add_edge treats duplicates as no-ops. Out-of-range nodes
|
||||
// panic: that is a programmer error, not a runtime condition.
|
||||
func (g *Graph) AddEdge(u, v int) bool {
|
||||
if u == v || g.HasEdge(u, v) {
|
||||
return false
|
||||
}
|
||||
g.adj[u] = append(g.adj[u], v)
|
||||
g.adj[v] = append(g.adj[v], u)
|
||||
g.edges++
|
||||
return true
|
||||
}
|
||||
|
||||
// HasEdge reports whether u and v are connected. Degrees in this model are
|
||||
// small, so a linear scan beats the bookkeeping of a set per node.
|
||||
func (g *Graph) HasEdge(u, v int) bool {
|
||||
return slices.Contains(g.adj[u], v)
|
||||
}
|
||||
|
||||
// Degree returns the number of neighbours of u.
|
||||
func (g *Graph) Degree(u int) int { return len(g.adj[u]) }
|
||||
|
||||
// Neighbors returns u's neighbours in insertion order. The slice is the
|
||||
// graph's own storage: callers must not modify it.
|
||||
func (g *Graph) Neighbors(u int) []int { return g.adj[u] }
|
||||
76
internal/engine/graph_test.go
Normal file
76
internal/engine/graph_test.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package engine
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Table-driven tests are the standard Go shape: a slice of cases, one
|
||||
// t.Run per case so failures name the case that broke.
|
||||
|
||||
func TestGraphAddEdge(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
edges [][2]int // applied in order
|
||||
wantAdded []bool // expected AddEdge result per edge
|
||||
wantEdges int // expected NumEdges afterwards
|
||||
}{
|
||||
{
|
||||
name: "simple edges",
|
||||
edges: [][2]int{{0, 1}, {1, 2}},
|
||||
wantAdded: []bool{true, true},
|
||||
wantEdges: 2,
|
||||
},
|
||||
{
|
||||
name: "duplicate ignored both directions",
|
||||
edges: [][2]int{{0, 1}, {0, 1}, {1, 0}},
|
||||
wantAdded: []bool{true, false, false},
|
||||
wantEdges: 1,
|
||||
},
|
||||
{
|
||||
name: "self loop ignored",
|
||||
edges: [][2]int{{2, 2}},
|
||||
wantAdded: []bool{false},
|
||||
wantEdges: 0,
|
||||
},
|
||||
}
|
||||
for _, testCase := range tests {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
graph := NewGraph(4)
|
||||
for edgeIndex, edge := range testCase.edges {
|
||||
added := graph.AddEdge(edge[0], edge[1])
|
||||
if added != testCase.wantAdded[edgeIndex] {
|
||||
t.Errorf("AddEdge(%d, %d) = %v, want %v",
|
||||
edge[0], edge[1], added, testCase.wantAdded[edgeIndex])
|
||||
}
|
||||
}
|
||||
if got := graph.NumEdges(); got != testCase.wantEdges {
|
||||
t.Errorf("NumEdges() = %d, want %d", got, testCase.wantEdges)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGraphNeighborsAndDegree(t *testing.T) {
|
||||
graph := NewGraph(4)
|
||||
graph.AddEdge(0, 1)
|
||||
graph.AddEdge(0, 2)
|
||||
graph.AddEdge(0, 3)
|
||||
|
||||
if got := graph.Degree(0); got != 3 {
|
||||
t.Errorf("Degree(0) = %d, want 3", got)
|
||||
}
|
||||
if got := graph.Degree(3); got != 1 {
|
||||
t.Errorf("Degree(3) = %d, want 1", got)
|
||||
}
|
||||
// Insertion order is part of the contract (determinism).
|
||||
if got, want := graph.Neighbors(0), []int{1, 2, 3}; !slices.Equal(got, want) {
|
||||
t.Errorf("Neighbors(0) = %v, want %v", got, want)
|
||||
}
|
||||
if !graph.HasEdge(2, 0) {
|
||||
t.Error("HasEdge(2, 0) = false, want true (undirected)")
|
||||
}
|
||||
if graph.HasEdge(1, 2) {
|
||||
t.Error("HasEdge(1, 2) = true, want false")
|
||||
}
|
||||
}
|
||||
|
|
@ -7,18 +7,19 @@ import "testing"
|
|||
// these assertions are exact, not probabilistic.
|
||||
|
||||
func TestNewRandSameSeedSameStream(t *testing.T) {
|
||||
a, b := newRand(17), newRand(17)
|
||||
for i := range 100 {
|
||||
if got, want := a.Float64(), b.Float64(); got != want {
|
||||
t.Fatalf("draw %d: streams diverged: %v != %v", i, got, want)
|
||||
firstStream, secondStream := newRand(17), newRand(17)
|
||||
for draw := range 100 {
|
||||
first, second := firstStream.Float64(), secondStream.Float64()
|
||||
if first != second {
|
||||
t.Fatalf("draw %d: streams diverged: %v != %v", draw, first, second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRandDifferentSeedsDiffer(t *testing.T) {
|
||||
a, b := newRand(17), newRand(18)
|
||||
seededWith17, seededWith18 := newRand(17), newRand(18)
|
||||
for range 100 {
|
||||
if a.Float64() != b.Float64() {
|
||||
if seededWith17.Float64() != seededWith18.Float64() {
|
||||
return // diverged, as expected
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue