seqprobe.go raw

   1  package grow
   2  
   3  import (
   4  	"context"
   5  
   6  	"git.mleku.dev/mleku/dendrite/pkg/axiom"
   7  	"git.mleku.dev/mleku/dendrite/pkg/lattice"
   8  )
   9  
  10  // SeqProbe walks a trained (saturated) lattice sequentially, checking whether
  11  // each element from the input stream matches an occupant reachable from the
  12  // previous match position. This tests whether the input text follows the
  13  // structural patterns encoded in the lattice topology.
  14  //
  15  // Unlike Probe (parallel, position-independent), SeqProbe maintains a cursor
  16  // position in the lattice. Each new element searches outward from the cursor
  17  // for a matching occupant. If found, the cursor moves there and a bonded event
  18  // is emitted. If not found within MaxSteps, an expired event is emitted and
  19  // the cursor jumps to a random matching occupant (if any exist) to resync.
  20  //
  21  // The lattice is never modified — this is purely read-only.
  22  func SeqProbe(ctx context.Context, l *lattice.Lattice, solution <-chan axiom.Element, cfg Config, events chan<- Event) {
  23  	var cursor *lattice.Node
  24  
  25  	for {
  26  		select {
  27  		case <-ctx.Done():
  28  			return
  29  		case elem, ok := <-solution:
  30  			if !ok {
  31  				return
  32  			}
  33  			ev := seqProbeStep(l, cursor, elem, cfg.MaxSteps)
  34  			if ev.Type == EventBonded {
  35  				cursor = l.Node(ev.NodeID)
  36  			} else {
  37  				// Resync: find any matching occupant.
  38  				cursor = findMatchingOccupant(l, elem.Type())
  39  			}
  40  			select {
  41  			case events <- ev:
  42  			case <-ctx.Done():
  43  				return
  44  			}
  45  		}
  46  	}
  47  }
  48  
  49  // seqProbeStep searches outward from cursor for a node whose occupant
  50  // matches the element's type. Returns a bonded event if found within
  51  // MaxSteps, or expired if not.
  52  func seqProbeStep(l *lattice.Lattice, cursor *lattice.Node, elem axiom.Element, maxSteps int) Event {
  53  	tag := elem.Type()
  54  
  55  	// If no cursor yet (first element), find any matching occupant.
  56  	if cursor == nil {
  57  		n := findMatchingOccupant(l, tag)
  58  		if n != nil {
  59  			return Event{Type: EventBonded, NodeID: n.ID(), Element: elem, Steps: 0}
  60  		}
  61  		return Event{Type: EventExpired, Element: elem, Steps: 0}
  62  	}
  63  
  64  	// BFS-like expansion from cursor: check cursor itself, then neighbors,
  65  	// then neighbors of neighbors, etc. The walk distance (steps) measures
  66  	// how far we had to go in the lattice topology to find a matching occupant.
  67  	// Short distances = the input follows the lattice structure.
  68  	// Long distances = the input deviates from trained patterns.
  69  
  70  	visited := make(map[lattice.NodeID]bool)
  71  	current := []*lattice.Node{cursor}
  72  
  73  	for step := 0; step < maxSteps && len(current) > 0; step++ {
  74  		var next []*lattice.Node
  75  		for _, n := range current {
  76  			if visited[n.ID()] {
  77  				continue
  78  			}
  79  			visited[n.ID()] = true
  80  
  81  			occ := n.Occupant()
  82  			if occ != nil && occ.Type() == tag {
  83  				return Event{
  84  					Type:    EventBonded,
  85  					NodeID:  n.ID(),
  86  					Element: elem,
  87  					Steps:   step,
  88  				}
  89  			}
  90  
  91  			for _, nb := range n.Neighbors() {
  92  				if !visited[nb.ID()] {
  93  					next = append(next, nb)
  94  				}
  95  			}
  96  		}
  97  		current = next
  98  	}
  99  
 100  	return Event{Type: EventExpired, Element: elem, Steps: maxSteps}
 101  }
 102  
 103  // findMatchingOccupant scans the lattice for any node whose occupant
 104  // matches the given type tag. Used for resyncing after a miss.
 105  func findMatchingOccupant(l *lattice.Lattice, tag string) *lattice.Node {
 106  	// Random start to avoid always landing on the same node.
 107  	start := l.RandomNode()
 108  	if start == nil {
 109  		return nil
 110  	}
 111  	// Walk from random start looking for a match.
 112  	current := start
 113  	for i := 0; i < 100; i++ {
 114  		occ := current.Occupant()
 115  		if occ != nil && occ.Type() == tag {
 116  			return current
 117  		}
 118  		next := lattice.RandomNeighbor(current)
 119  		if next == nil {
 120  			break
 121  		}
 122  		current = next
 123  	}
 124  	return nil
 125  }
 126