probe.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  // ProbeEvent records what happened when an element was probed against a
  12  // trained lattice. Unlike growth events which bond elements to empty sites,
  13  // probe events measure how well the element fits the existing occupants.
  14  type ProbeEvent struct {
  15  	// Type: EventBonded means a matching occupant was found (type match
  16  	// at a constrained site). EventExpired means no match within MaxSteps.
  17  	Type EventType
  18  
  19  	// NodeID is the node where a match was found (if matched).
  20  	NodeID lattice.NodeID
  21  
  22  	// Element is the probed element.
  23  	Element axiom.Element
  24  
  25  	// Steps is how many walk steps before finding a match (or expiring).
  26  	Steps int
  27  }
  28  
  29  // Probe walks elements through a trained (saturated) lattice without bonding.
  30  // For each element, it walks the lattice and checks if visited nodes have
  31  // occupants matching the element's type. A match means the lattice has
  32  // "seen" this kind of element at this position — the text fits the trained
  33  // topology.
  34  //
  35  // This is the inference counterpart to Run: Run grows the lattice,
  36  // Probe reads it.
  37  func Probe(ctx context.Context, l *lattice.Lattice, solution <-chan axiom.Element, cfg Config, events chan<- ProbeEvent) {
  38  	var wg sync.WaitGroup
  39  
  40  	for range cfg.Workers {
  41  		wg.Add(1)
  42  		go func() {
  43  			defer wg.Done()
  44  			for {
  45  				select {
  46  				case <-ctx.Done():
  47  					return
  48  				case elem, ok := <-solution:
  49  					if !ok {
  50  						return
  51  					}
  52  					ev := probeWalk(ctx, l, elem, cfg.MaxSteps)
  53  					select {
  54  					case events <- ev:
  55  					case <-ctx.Done():
  56  						return
  57  					}
  58  				}
  59  			}
  60  		}()
  61  	}
  62  
  63  	wg.Wait()
  64  }
  65  
  66  // probeWalk walks the lattice checking for type-matching occupants.
  67  // It uses the same directed start and chemotaxis as growth walks,
  68  // but instead of bonding, it checks occupant compatibility.
  69  func probeWalk(ctx context.Context, l *lattice.Lattice, elem axiom.Element, maxSteps int) ProbeEvent {
  70  	// Start at a random node (can't use VacantByTag — lattice is full).
  71  	current := l.RandomNode()
  72  	if current == nil {
  73  		return ProbeEvent{Type: EventExpired, Element: elem}
  74  	}
  75  
  76  	tag := elem.Type()
  77  
  78  	for step := 0; step < maxSteps; step++ {
  79  		select {
  80  		case <-ctx.Done():
  81  			return ProbeEvent{Type: EventExpired, Element: elem, Steps: step}
  82  		default:
  83  		}
  84  
  85  		// Check if this node's occupant matches the element's type.
  86  		occ := current.Occupant()
  87  		if occ != nil && occ.Type() == tag {
  88  			return ProbeEvent{
  89  				Type:    EventBonded, // "matched" — reuse the type
  90  				NodeID:  current.ID(),
  91  				Element: elem,
  92  				Steps:   step,
  93  			}
  94  		}
  95  
  96  		// Walk toward matching occupants via neighbor checking.
  97  		neighbors := current.Neighbors()
  98  		if len(neighbors) == 0 {
  99  			break
 100  		}
 101  
 102  		// Check neighbors for immediate match.
 103  		for _, nb := range neighbors {
 104  			occ := nb.Occupant()
 105  			if occ != nil && occ.Type() == tag {
 106  				return ProbeEvent{
 107  					Type:    EventBonded,
 108  					NodeID:  nb.ID(),
 109  					Element: elem,
 110  					Steps:   step,
 111  				}
 112  			}
 113  		}
 114  
 115  		// No immediate match — step to a random neighbor.
 116  		next := lattice.RandomNeighbor(current)
 117  		if next == nil {
 118  			break
 119  		}
 120  		current = next
 121  	}
 122  
 123  	return ProbeEvent{Type: EventExpired, Element: elem, Steps: maxSteps}
 124  }
 125