topology.go raw

   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