1 package grammar
2 3 import (
4 "log"
5 "math/rand/v2"
6 "sort"
7 8 "git.mleku.dev/mleku/dendrite/pkg/axiom"
9 "git.mleku.dev/mleku/dendrite/pkg/lattice"
10 )
11 12 // BuildGrammarLattice creates a lattice with grammar-shaped topology.
13 //
14 // Each tag gets counts[tag] nodes. Within each tag group, nodes are
15 // connected in a ring (preserving locality). Between groups, connections
16 // are made according to the grammar's adjacency rules: a node with tag A
17 // is connected to nodes with tags that Grammar.CanNeighbor(A, B) permits.
18 //
19 // The instanceSeed provides per-instance variation: different seeds produce
20 // different selections of which grammar-permitted connections are made.
21 // Same grammar rules, different topological realization. This is what
22 // differentiates colony instances — the grammar defines the rigid backbone,
23 // the seed selects the specific innervation.
24 func BuildGrammarLattice(
25 g *Grammar,
26 counts map[string]int,
27 instanceSeed [32]byte,
28 constraintFactory func(string) axiom.Constraint,
29 ) *lattice.Lattice {
30 l := lattice.New()
31 32 // Sort tags for deterministic node creation order.
33 tags := make([]string, 0, len(counts))
34 for tag := range counts {
35 if counts[tag] > 0 {
36 tags = append(tags, tag)
37 }
38 }
39 sort.Strings(tags)
40 41 // Create nodes grouped by tag.
42 type tagGroup struct {
43 tag string
44 nodes []*lattice.Node
45 }
46 groups := make([]tagGroup, 0, len(tags))
47 groupIndex := make(map[string]int) // tag -> index in groups
48 49 for _, tag := range tags {
50 n := counts[tag]
51 tg := tagGroup{tag: tag, nodes: make([]*lattice.Node, n)}
52 for i := range n {
53 node := l.AddNode([]axiom.Constraint{constraintFactory(tag)})
54 node.SetEnergy(true)
55 tg.nodes[i] = node
56 }
57 groupIndex[tag] = len(groups)
58 groups = append(groups, tg)
59 }
60 61 // Intra-group connectivity: ring within each tag group.
62 for _, tg := range groups {
63 if len(tg.nodes) < 2 {
64 continue
65 }
66 for i := range tg.nodes {
67 l.Connect(tg.nodes[i], tg.nodes[(i+1)%len(tg.nodes)])
68 }
69 }
70 71 // Inter-group connectivity: grammar-shaped cross-connections.
72 // Seed a deterministic PRNG from the instance seed.
73 var seed [32]byte
74 copy(seed[:], instanceSeed[:])
75 rng := rand.New(rand.NewChaCha8(seed))
76 77 for i, tgA := range groups {
78 for j := i + 1; j < len(groups); j++ {
79 tgB := groups[j]
80 81 // Check if grammar permits this pair (either direction).
82 canAB := g.CanNeighbor(tgA.tag, tgB.tag)
83 canBA := g.CanNeighbor(tgB.tag, tgA.tag)
84 if !canAB && !canBA {
85 continue
86 }
87 88 // Number of cross-connections: proportional to the smaller
89 // group, with a minimum of 1. The factor (1/3) creates
90 // sparse but meaningful bridging.
91 smaller := min(len(tgA.nodes), len(tgB.nodes))
92 nBridges := max(1, smaller/3)
93 94 // Select which nodes to bridge using the seeded PRNG.
95 // Different seeds select different bridge nodes —
96 // same grammar shape, different wiring realization.
97 for range nBridges {
98 idxA := rng.IntN(len(tgA.nodes))
99 idxB := rng.IntN(len(tgB.nodes))
100 l.Connect(tgA.nodes[idxA], tgB.nodes[idxB])
101 }
102 }
103 }
104 105 return l
106 }
107 108 // ExpandLattice adds growBy nodes to an existing lattice, preserving the
109 // grammar-shaped topology. New nodes are distributed across tags using the
110 // same proportions as MorphemeDefaultCounts, wired into intra-group rings
111 // and inter-group bridges. Existing nodes and bonds are untouched.
112 //
113 // Returns the number of nodes added.
114 func ExpandLattice(
115 l *lattice.Lattice,
116 g *Grammar,
117 growBy int,
118 seed [32]byte,
119 constraintFactory func(string) axiom.Constraint,
120 defaultCounts func(int) map[string]int,
121 ) int {
122 if growBy <= 0 {
123 return 0
124 }
125 126 // Get proportional counts for the new batch.
127 counts := defaultCounts(growBy)
128 129 // Sort tags for deterministic order.
130 tags := make([]string, 0, len(counts))
131 for tag := range counts {
132 if counts[tag] > 0 {
133 tags = append(tags, tag)
134 }
135 }
136 sort.Strings(tags)
137 138 // Collect existing nodes by tag for bridge-wiring.
139 existingByTag := make(map[string][]lattice.NodeID)
140 for _, n := range l.Nodes() {
141 for _, c := range n.Constraints() {
142 existingByTag[c.Tag()] = append(existingByTag[c.Tag()], n.ID())
143 }
144 }
145 146 // Create new nodes grouped by tag.
147 type tagGroup struct {
148 tag string
149 nodes []*lattice.Node
150 }
151 groups := make([]tagGroup, 0, len(tags))
152 153 oldSize := l.Size()
154 for _, tag := range tags {
155 n := counts[tag]
156 tg := tagGroup{tag: tag, nodes: make([]*lattice.Node, n)}
157 for i := range n {
158 node := l.AddNode([]axiom.Constraint{constraintFactory(tag)})
159 node.SetEnergy(true)
160 tg.nodes[i] = node
161 }
162 groups = append(groups, tg)
163 }
164 165 // Intra-group: ring within each new group.
166 for _, tg := range groups {
167 if len(tg.nodes) < 2 {
168 continue
169 }
170 for i := range tg.nodes {
171 l.Connect(tg.nodes[i], tg.nodes[(i+1)%len(tg.nodes)])
172 }
173 }
174 175 // Bridge new nodes to existing lattice: connect each new group to
176 // existing nodes of every permitted neighbor tag.
177 rng := rand.New(rand.NewChaCha8(seed))
178 179 for _, tgNew := range groups {
180 for _, rule := range g.Rules {
181 if !g.CanNeighbor(tgNew.tag, rule.Tag) {
182 continue
183 }
184 existing := existingByTag[rule.Tag]
185 if len(existing) == 0 {
186 continue
187 }
188 // Connect sqrt(new) bridges to existing nodes of this tag.
189 nBridges := max(1, len(tgNew.nodes)/3)
190 for range nBridges {
191 newNode := tgNew.nodes[rng.IntN(len(tgNew.nodes))]
192 oldID := existing[rng.IntN(len(existing))]
193 oldNode := l.Node(oldID)
194 if oldNode != nil {
195 l.Connect(newNode, oldNode)
196 }
197 }
198 }
199 }
200 201 // Inter-group bridges among the new groups themselves.
202 for i, tgA := range groups {
203 for j := i + 1; j < len(groups); j++ {
204 tgB := groups[j]
205 if !g.CanNeighbor(tgA.tag, tgB.tag) && !g.CanNeighbor(tgB.tag, tgA.tag) {
206 continue
207 }
208 smaller := min(len(tgA.nodes), len(tgB.nodes))
209 nBridges := max(1, smaller/3)
210 for range nBridges {
211 l.Connect(
212 tgA.nodes[rng.IntN(len(tgA.nodes))],
213 tgB.nodes[rng.IntN(len(tgB.nodes))],
214 )
215 }
216 }
217 }
218 219 added := l.Size() - oldSize
220 log.Printf("expand: added %d nodes (%d -> %d)", added, oldSize, l.Size())
221 return added
222 }
223