1 package grow
2 3 import (
4 "context"
5 "sync"
6 7 "git.mleku.dev/mleku/dendrite/pkg/axiom"
8 "git.mleku.dev/mleku/dendrite/pkg/lattice"
9 )
10 11 // DryRun is like Run but bonds are immediately reversed after recording.
12 // Each element is walked through the lattice using the same directed start,
13 // chemotaxis, and constraint checking as Run. If a bond forms, the event is
14 // emitted and the node is immediately dissolved so the lattice never saturates.
15 //
16 // This gives a "would this bond?" test for every token without capacity
17 // exhaustion. The trained lattice topology and constraints are the detector;
18 // the occupancy state doesn't accumulate.
19 func DryRun(ctx context.Context, l *lattice.Lattice, solution <-chan axiom.Element, cfg Config, events chan<- Event) {
20 var wg sync.WaitGroup
21 22 for range cfg.Workers {
23 wg.Add(1)
24 go func() {
25 defer wg.Done()
26 for {
27 select {
28 case <-ctx.Done():
29 return
30 case elem, ok := <-solution:
31 if !ok {
32 return
33 }
34 ev := dryWalk(ctx, l, elem, cfg.MaxSteps)
35 select {
36 case events <- ev:
37 case <-ctx.Done():
38 return
39 }
40 }
41 }
42 }()
43 }
44 45 wg.Wait()
46 }
47 48 // dryWalk performs a single Brownian walk that bonds and immediately unbonds.
49 // Uses the same vascularization and chemotaxis as walk() but dissolves the
50 // bonded element after recording the event, keeping the lattice unsaturated.
51 func dryWalk(ctx context.Context, l *lattice.Lattice, elem axiom.Element, maxSteps int) Event {
52 // Directed start: try to begin near a compatible vacant site.
53 current := l.VacantByTag(elem.Type())
54 if current == nil {
55 current = l.RandomNode()
56 }
57 if current == nil {
58 return Event{Type: EventRejected, Element: elem}
59 }
60 61 for step := 0; step < maxSteps; step++ {
62 select {
63 case <-ctx.Done():
64 return Event{Type: EventExpired, Element: elem, Steps: step}
65 default:
66 }
67 68 // Does this site admit the element?
69 if current.Admits(elem) {
70 if current.Bond(elem) {
71 ev := Event{
72 Type: EventBonded,
73 NodeID: current.ID(),
74 Element: elem,
75 Steps: step,
76 }
77 // Immediately unbond so lattice stays unsaturated.
78 current.Dissolve()
79 l.ReindexVacant(current)
80 return ev
81 }
82 }
83 84 // Chemotaxis: check neighbors for immediate bond opportunity.
85 neighbors := current.Neighbors()
86 if len(neighbors) > 0 {
87 bonded := false
88 for _, nb := range neighbors {
89 if nb.Admits(elem) {
90 if nb.Bond(elem) {
91 ev := Event{
92 Type: EventBonded,
93 NodeID: nb.ID(),
94 Element: elem,
95 Steps: step,
96 }
97 nb.Dissolve()
98 l.ReindexVacant(nb)
99 return ev
100 }
101 bonded = true
102 }
103 }
104 105 // Gradient following toward vacant space.
106 if !bonded {
107 bestScore := -1
108 var best *lattice.Node
109 for _, nb := range neighbors {
110 score := 0
111 for _, nnb := range nb.Neighbors() {
112 if !nnb.Occupied() {
113 score++
114 }
115 }
116 if score > bestScore {
117 bestScore = score
118 best = nb
119 }
120 }
121 if best != nil && bestScore > 0 {
122 current = best
123 continue
124 }
125 }
126 }
127 128 // Random walk fallback.
129 next := lattice.RandomNeighbor(current)
130 if next == nil {
131 next = l.RandomNode()
132 if next == nil {
133 return Event{Type: EventRejected, Element: elem}
134 }
135 }
136 current = next
137 }
138 139 return Event{Type: EventExpired, Element: elem, Steps: maxSteps}
140 }
141