One panel's whole world in one call: effective config in, echoed config + cascade result + undirected edge list out. Edges are [from, to] pairs with from < to in deterministic node order; Graph.Edges() walks the adjacency once, GraphEdges(config) rebuilds the seeded world (~25us) so Result stays lean and /api/comparison stays untouched. This closes the topology gap the design brief flagged; the frontend's seeded d3-force layout consumes these pairs. Go bits: [][2]int is a slice of fixed-size arrays; [2]int is a value type, comparable, and JSON-marshals to [a, b], exactly the wire shape the spec asks for. tygo regen includes a fix: engine.Strategy now maps to the generated Strategy type instead of decaying to 'any' in ScenarioRequest. Verified live through the dev stack: 7/120 reached, 351 edges.
65 lines
2.1 KiB
Go
65 lines
2.1 KiB
Go
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] }
|
|
|
|
// Edges returns every undirected edge exactly once as a [from, to] pair
|
|
// with from < to, in deterministic node order. The slice is freshly
|
|
// allocated; callers may keep it.
|
|
func (g *Graph) Edges() [][2]int {
|
|
edges := make([][2]int, 0, g.edges)
|
|
for node := range g.NumNodes() {
|
|
for _, neighbor := range g.adj[node] {
|
|
if node < neighbor {
|
|
edges = append(edges, [2]int{node, neighbor})
|
|
}
|
|
}
|
|
}
|
|
return edges
|
|
}
|