dryrun.go raw

   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