// Package lattice implements the growth structure — nodes, sites, constraint // envelopes, and the graph that connects them. // // The lattice is the crystalline state. It grows by accretion, prunes by // dissolution, and maintains dynamic equilibrium. All operations are // single-threaded per lattice instance. Concurrent access requires external // synchronization. package lattice import ( "math/rand/v2" "sync" "git.mleku.dev/mleku/dendrite/pkg/axiom" "git.mleku.dev/mleku/dendrite/pkg/ratio" "git.mleku.dev/mleku/dendrite/pkg/state" ) // NodeID is a unique identifier for a lattice node. // It is the index into the lattice's node slice (0-based). type NodeID uint64 // LockInDepth measures how firmly an element is held at a lattice site. // Higher values mean deeper lock-in — more constraint satisfaction, // harder to dissolve. Stored as an exact rational number. type LockInDepth = ratio.Ratio // Node is a position in the lattice. Each node has a constraint envelope // (the negative space defining what fits), an optional occupant, and // connections to neighbors. No per-node mutex — all access is single-threaded, // serialized by the cache's block-level lock or the caller's goroutine. // // Neighbors are stored as uint32 indices into the lattice's node slice // rather than *Node pointers. This halves neighbor memory (4 vs 8 bytes // per edge) and eliminates ~100M GC pointer roots at 16.5M nodes. type Node struct { lat *Lattice id NodeID constraints []axiom.Constraint occupant axiom.Element // nil if vacant candidates []axiom.Element // ambiguity: multiple valid fillers (Lake state) neighborIdx []uint32 // indices into lat.nodes hex state.Hexagram lockIn LockInDepth bondCount int // number of constraints satisfied by current occupant crossLayer bool // true if occupant is misaligned with constraint layer (post-hoc detection target) perm uint8 // S_3 permutation index (0-5), set by PermutedElement on bond projVertex uint8 // 3-bit cube vertex (0-7), set by ProjectedElement on bond projKey uint8 // 3-bit projection key (0-7), set by ProjectedElement on bond projPath uint16 // rendering path index, set by ProjectedElement on bond age uint8 // 2-bit ADSR envelope (0=Attack, 1=Decay, 2=Sustain, 3=Release) } // ID returns the node's unique identifier. func (n *Node) ID() NodeID { return n.id } // Hexagram returns the node's current dynamical state. func (n *Node) Hexagram() state.Hexagram { return n.hex } // Occupied reports whether this node has an element bonded to it. func (n *Node) Occupied() bool { return n.occupant != nil } // Occupant returns the bonded element, or nil if vacant. func (n *Node) Occupant() axiom.Element { return n.occupant } // CrossLayer reports whether this node's occupant is misaligned with the // node's constraint layer. func (n *Node) CrossLayer() bool { return n.crossLayer } // LockIn returns the current lock-in depth. func (n *Node) LockIn() LockInDepth { return n.lockIn } // Permutation returns the node's active S_3 permutation index (0-5). // Identity (0) is the default for nodes without a PermutedElement. func (n *Node) Permutation() uint8 { return n.perm } // SetPermutation sets the node's active S_3 permutation index. // Values >= 6 are ignored. func (n *Node) SetPermutation(p uint8) { if p < 6 { n.perm = p } } // ProjectionVertex returns the node's 3-bit cube vertex (0-7). func (n *Node) ProjectionVertex() uint8 { return n.projVertex } // ProjectionKey returns the node's 3-bit projection key (0-7). func (n *Node) ProjectionKey() uint8 { return n.projKey } // ProjectionPath returns the node's rendering path index. func (n *Node) ProjectionPath() uint16 { return n.projPath } // SetProjection sets the node's full projection encoding. func (n *Node) SetProjection(vertex, key uint8, path uint16) { if vertex < 8 { n.projVertex = vertex } if key < 8 { n.projKey = key } n.projPath = path } // Projection6Bit returns the packed 6-bit projection: vertex (low 3) | key (high 3). func (n *Node) Projection6Bit() uint8 { return (n.projVertex & 0b111) | (n.projKey&0b111)<<3 } // Age returns the node's current age (0-3), encoding the ADSR envelope: // // 0 = Attack — freshly bonded, high-energy accretion // 1 = Decay — actively growing, settling // 2 = Sustain — locked in, durable (stable attractor) // 3 = Release — dissolving, returning to solution func (n *Node) Age() uint8 { return n.age } // IncrementAge advances Attack→Decay→Sustain automatically. func (n *Node) IncrementAge() { if n.age < 2 { n.age++ } } // Destabilize forces a Sustain node into Release phase. // No-op if the node is not in Sustain (age 2). func (n *Node) Destabilize() { if n.age == 2 { n.age = 3 } } // ProjectionByte returns the full 8-bit encoding: // // bit 7 6 5 4 3 2 1 0 // [age ] [ key ] [vertex] func (n *Node) ProjectionByte() uint8 { return (n.age&0b11)<<6 | (n.projKey&0b111)<<3 | (n.projVertex & 0b111) } // ContextualLockIn computes effective lock-in that accounts for neighborhood. // A bonded element surrounded by occupied neighbors has higher effective lock-in // than an isolated one. // // Formula: 0.3 + 0.7 * neighborOccupancyRate func (n *Node) ContextualLockIn() LockInDepth { if n.lockIn.IsZero() { return ratio.Zero } if len(n.neighborIdx) == 0 { return ratio.New(3, 10) } occupied := 0 for _, idx := range n.neighborIdx { if n.lat.nodes[idx].occupant != nil { occupied++ } } rate := ratio.New(int64(occupied), int64(len(n.neighborIdx))) return ratio.New(3, 10).Add(ratio.New(7, 10).Mul(rate)) } // Constraints returns the node's constraint envelope. func (n *Node) Constraints() []axiom.Constraint { out := make([]axiom.Constraint, len(n.constraints)) copy(out, n.constraints) return out } // Neighbors returns all neighbor nodes as resolved pointers. // The returned slice is freshly allocated; callers must not cache it across // mutations. For index-only access in hot paths, use NeighborCount + NeighborAt. func (n *Node) Neighbors() []*Node { out := make([]*Node, len(n.neighborIdx)) for i, idx := range n.neighborIdx { out[i] = n.lat.nodes[idx] } return out } // NeighborCount returns the number of neighbors without allocating. func (n *Node) NeighborCount() int { return len(n.neighborIdx) } // NeighborAt returns the i-th neighbor node. func (n *Node) NeighborAt(i int) *Node { return n.lat.nodes[n.neighborIdx[i]] } // NeighborIndices returns the raw uint32 index slice. The caller must not // modify the returned slice. Used by serialization to avoid allocating a // temporary []*Node. func (n *Node) NeighborIndices() []uint32 { return n.neighborIdx } // Admits checks whether the given element satisfies this node's constraint // envelope. func (n *Node) Admits(e axiom.Element) bool { if n.occupant != nil { return false } if len(n.constraints) == 0 { return false } for _, c := range n.constraints { if !c.Admits(e) { return false } } return true } // Bond attempts to place an element at this node. Returns true if the // bond formed, false if the site was already claimed or the element // doesn't fit. func (n *Node) Bond(e axiom.Element) bool { if n.occupant != nil { return false } satisfied := 0 misaligned := false for _, c := range n.constraints { if lc, ok := c.(axiom.LayeredConstraint); ok { if le, ok := e.(axiom.LayeredElement); ok { if !lc.Aligns(le) { misaligned = true } } } if c.Admits(e) { satisfied++ } else { return false } } // Contextual constraint check using direct neighbor reads. for _, c := range n.constraints { if cc, ok := c.(axiom.ContextualConstraint); ok { nbOccupants := make([]axiom.Element, len(n.neighborIdx)) for i, idx := range n.neighborIdx { nbOccupants[i] = n.lat.nodes[idx].occupant } if !cc.AdmitsInContext(e, nbOccupants) { return false } break } } n.occupant = e n.bondCount = satisfied n.crossLayer = misaligned if misaligned { n.lockIn = ratio.New(int64(satisfied), 2) } else { n.lockIn = ratio.FromInt(int64(satisfied)) } if pe, ok := e.(axiom.PermutedElement); ok { p := pe.Permutation() if p < 6 { n.perm = p } } if pe, ok := e.(axiom.ProjectedElement); ok { v := pe.ProjectionVertex() if v < 8 { n.projVertex = v } k := pe.ProjectionKey() if k < 8 { n.projKey = k } n.projPath = pe.ProjectionPath() n.perm = keyToPermLookup(k) } n.age = 0 n.updateHex() if wc := n.lat.walkCols; wc != nil { wc.SetOccupied(n.id, true) wc.SetLockIn(n.id, n.lockIn.Num, n.lockIn.Denom) } return true } // Dissolve removes the occupant from this node, returning the element // to the free pool. Returns the dissolved element, or nil if vacant. func (n *Node) Dissolve() axiom.Element { e := n.occupant n.occupant = nil n.candidates = nil n.bondCount = 0 n.crossLayer = false n.lockIn = ratio.Zero n.perm = 0 n.projVertex = 0 n.projKey = 0 n.projPath = 0 n.age = 0 n.updateHex() if wc := n.lat.walkCols; wc != nil { wc.SetOccupied(n.id, false) wc.SetLockIn(n.id, 0, 1) } return e } // Displace checks whether element e would have stronger lock-in than the // current occupant. If so, the occupant is ejected and e bonds in its place. // Returns the displaced element and true on success, or nil and false if // the site is vacant, the element doesn't fit, or the current occupant is // stronger. // // This is competitive displacement: a higher-affinity element replaces a // lower-affinity one at the same site, like a stronger ligand displacing // a weaker one from a receptor. func (n *Node) Displace(e axiom.Element) (axiom.Element, bool) { if n.occupant == nil { // Vacant — use Bond instead. return nil, false } // Check if the newcomer satisfies all constraints. satisfied := 0 misaligned := false for _, c := range n.constraints { if lc, ok := c.(axiom.LayeredConstraint); ok { if le, ok := e.(axiom.LayeredElement); ok { if !lc.Aligns(le) { misaligned = true } } } if c.Admits(e) { satisfied++ } else { return nil, false } } // Contextual constraint check. for _, c := range n.constraints { if cc, ok := c.(axiom.ContextualConstraint); ok { nbOccupants := make([]axiom.Element, len(n.neighborIdx)) for i, idx := range n.neighborIdx { nbOccupants[i] = n.lat.nodes[idx].occupant } if !cc.AdmitsInContext(e, nbOccupants) { return nil, false } break } } // Compute what the newcomer's lock-in would be. var newLockIn ratio.Ratio if misaligned { newLockIn = ratio.New(int64(satisfied), 2) } else { newLockIn = ratio.FromInt(int64(satisfied)) } // Only displace if strictly stronger. if !n.lockIn.Less(newLockIn) { return nil, false } // Eject the current occupant. displaced := n.occupant // Bond the newcomer in place. n.occupant = e n.bondCount = satisfied n.crossLayer = misaligned n.lockIn = newLockIn if pe, ok := e.(axiom.PermutedElement); ok { p := pe.Permutation() if p < 6 { n.perm = p } } if pe, ok := e.(axiom.ProjectedElement); ok { v := pe.ProjectionVertex() if v < 8 { n.projVertex = v } k := pe.ProjectionKey() if k < 8 { n.projKey = k } n.projPath = pe.ProjectionPath() n.perm = keyToPermLookup(k) } n.age = 0 n.updateHex() if wc := n.lat.walkCols; wc != nil { wc.SetOccupied(n.id, true) wc.SetLockIn(n.id, n.lockIn.Num, n.lockIn.Denom) } return displaced, true } // AddCandidate records an element as a valid filler for this site // without committing to it. func (n *Node) AddCandidate(e axiom.Element) bool { for _, c := range n.constraints { if !c.Admits(e) { return false } } for _, existing := range n.candidates { if existing.Type() == e.Type() && existing.Value() == e.Value() { return false } } const maxCandidates = 32 if len(n.candidates) >= maxCandidates { return false } n.candidates = append(n.candidates, e) return true } // Candidates returns the set of valid fillers at this site. func (n *Node) Candidates() []axiom.Element { if len(n.candidates) == 0 { return nil } out := make([]axiom.Element, len(n.candidates)) copy(out, n.candidates) return out } // Ambiguous reports whether this site has multiple valid fillers. func (n *Node) Ambiguous() bool { return len(n.candidates) > 1 } // Collapse resolves ambiguity by selecting one candidate as the occupant // and clearing the rest. Returns true if collapse occurred. func (n *Node) Collapse(selector func([]axiom.Element) axiom.Element) bool { if len(n.candidates) < 2 { return false } chosen := selector(n.candidates) if chosen == nil { return false } satisfied := 0 for _, c := range n.constraints { if c.Admits(chosen) { satisfied++ } } n.occupant = chosen n.bondCount = satisfied n.lockIn = ratio.FromInt(int64(satisfied)) n.candidates = nil n.updateHex() return true } // keyToPermLookup maps a 3-bit projection key to an S_3 permutation index. func keyToPermLookup(k uint8) uint8 { switch k { case 0: return 0 case 1: return 3 case 2: return 2 case 3: return 1 case 4: return 4 case 5: return 5 case 6: return 0 case 7: return 3 default: return 0 } } // updateHex recalculates the node's hexagram based on current state. func (n *Node) updateHex() { currentEnergy := n.hex.Inner().Energy() inner := state.Trigram(0). SetBonding(n.occupant != nil). SetConstraint(n.occupant != nil && n.bondCount > 0). SetEnergy(currentEnergy) n.hex = state.Hex(inner, n.hex.Outer()) } // Lattice is the graph of nodes — the crystalline structure. type Lattice struct { mu sync.RWMutex nodes []*Node // vacantIdx maps constraint tags to slices of node IDs with matching // vacant sites. vacantIdx map[string][]NodeID // cache is the optional block-level cache. When non-nil, walks // check for stripped nodes and trigger block loading via EnsureResident. cache *Cache // walkCols is the optional flat-array representation of walk-hot fields. // When non-nil, Bond/Dissolve/ForceOccupant write-through to keep the // columns consistent with the node graph. walkCols *WalkColumns } // SetCache attaches a block-level cache to the lattice. func (l *Lattice) SetCache(c *Cache) { l.mu.Lock() defer l.mu.Unlock() l.cache = c } // GetCache returns the attached cache, or nil if none. func (l *Lattice) GetCache() *Cache { return l.cache } // BuildAndAttachWalkColumns builds flat-array walk columns from the current // node state and attaches them to the lattice. Subsequent Bond/Dissolve calls // write-through to the columns for cache-friendly walk access. func (l *Lattice) BuildAndAttachWalkColumns() { l.walkCols = BuildWalkColumns(l) } // DetachWalkColumns removes the walk columns. Bond/Dissolve stop writing through. func (l *Lattice) DetachWalkColumns() { l.walkCols = nil } // GetWalkColumns returns the attached walk columns, or nil. func (l *Lattice) GetWalkColumns() *WalkColumns { return l.walkCols } // ensureResident loads the block containing nodeID if there is a cache // and the node is stripped. No-op if cache is nil or node is resident. func (l *Lattice) ensureResident(id NodeID) { if l.cache != nil { l.cache.EnsureResident(id) } } // EnsureResidentPublic is the exported form of ensureResident, for use by // packages that need to trigger cache block loading (e.g. hexagram activation). func (l *Lattice) EnsureResidentPublic(id NodeID) { l.ensureResident(id) } // New creates an empty lattice. func New() *Lattice { return &Lattice{ nodes: make([]*Node, 0, 256), vacantIdx: make(map[string][]NodeID), } } // AddSkeletonNode creates a node with only its ID and neighbor index slice. func (l *Lattice) AddSkeletonNode() *Node { l.mu.Lock() id := NodeID(len(l.nodes)) n := &Node{ lat: l, id: id, neighborIdx: getNeighborSlice(), } l.nodes = append(l.nodes, n) l.mu.Unlock() return n } // AddNode creates a new node with the given constraint envelope and // adds it to the lattice. Returns the new node. func (l *Lattice) AddNode(constraints []axiom.Constraint) *Node { l.mu.Lock() id := NodeID(len(l.nodes)) n := &Node{ lat: l, id: id, constraints: constraints, neighborIdx: getNeighborSlice(), } l.nodes = append(l.nodes, n) for _, c := range constraints { tag := c.Tag() l.vacantIdx[tag] = append(l.vacantIdx[tag], id) } l.mu.Unlock() return n } // Connect creates a bidirectional neighbor relationship between two nodes. func (l *Lattice) Connect(a, b *Node) { if a == b { return } aIdx := uint32(a.id) bIdx := uint32(b.id) if !hasNeighborIdx(a.neighborIdx, bIdx) { a.neighborIdx = append(a.neighborIdx, bIdx) } if !hasNeighborIdx(b.neighborIdx, aIdx) { b.neighborIdx = append(b.neighborIdx, aIdx) } } // Disconnect severs the neighbor relationship between two nodes. func (l *Lattice) Disconnect(a, b *Node) { if a == b { return } a.neighborIdx = removeNeighborIdx(a.neighborIdx, uint32(b.id)) b.neighborIdx = removeNeighborIdx(b.neighborIdx, uint32(a.id)) } // hasNeighborIdx reports whether idx is in the neighbor index slice. func hasNeighborIdx(nbs []uint32, idx uint32) bool { for _, n := range nbs { if n == idx { return true } } return false } // removeNeighborIdx removes idx from the slice using swap-remove. func removeNeighborIdx(nbs []uint32, idx uint32) []uint32 { for i, n := range nbs { if n == idx { last := len(nbs) - 1 nbs[i] = nbs[last] return nbs[:last] } } return nbs } // Node returns a node by ID, or nil if not found. func (l *Lattice) Node(id NodeID) *Node { l.mu.RLock() defer l.mu.RUnlock() idx := int(id) if idx < 0 || idx >= len(l.nodes) { return nil } return l.nodes[idx] } // Size returns the number of nodes in the lattice. func (l *Lattice) Size() int { l.mu.RLock() defer l.mu.RUnlock() return len(l.nodes) } // Nodes returns all nodes. The caller must not modify the slice. func (l *Lattice) Nodes() []*Node { l.mu.RLock() defer l.mu.RUnlock() return l.nodes } // VacantSites returns all nodes that are unoccupied and have constraints. func (l *Lattice) VacantSites() []*Node { l.mu.RLock() defer l.mu.RUnlock() var sites []*Node for _, n := range l.nodes { if n.occupant == nil && len(n.constraints) > 0 { sites = append(sites, n) } } return sites } // Health reports the lattice's structural health metrics. type Health struct { NodeCount int // total nodes Occupied int // nodes with bonded elements Vacant int // nodes available for bonding AvgLockIn ratio.Ratio // average lock-in depth across occupied nodes MaxLockIn ratio.Ratio // deepest lock-in Ambiguous int // nodes with multiple candidate elements OccupancyRate ratio.Ratio // occupied / total [0, 1] AccretionReady int // vacant nodes with energy (ready for growth) DissolveSoft int // occupied nodes with lock-in < 1 (dissolution candidates) } // Health computes the current structural health of the lattice. func (l *Lattice) Health() Health { l.mu.RLock() defer l.mu.RUnlock() h := Health{NodeCount: len(l.nodes)} totalLockIn := ratio.Zero for _, n := range l.nodes { if n.occupant != nil { h.Occupied++ totalLockIn = totalLockIn.Add(n.lockIn) if h.MaxLockIn.Less(n.lockIn) { h.MaxLockIn = n.lockIn } if n.lockIn.Less(ratio.One) { h.DissolveSoft++ } } else if len(n.constraints) > 0 { h.Vacant++ if n.hex.Inner().Energy() { h.AccretionReady++ } } if len(n.candidates) > 1 { h.Ambiguous++ } } if h.Occupied > 0 { h.AvgLockIn = totalLockIn.Div(ratio.FromInt(int64(h.Occupied))) } if h.NodeCount > 0 { h.OccupancyRate = ratio.New(int64(h.Occupied), int64(h.NodeCount)) } return h } // ClearOccupants removes all occupants and resets bond counts across the // lattice while preserving topology (nodes, edges) and constraints. func (l *Lattice) ClearOccupants() { l.mu.Lock() defer l.mu.Unlock() for _, n := range l.nodes { n.occupant = nil n.candidates = nil n.bondCount = 0 n.lockIn = ratio.Zero n.age = 0 } if wc := l.walkCols; wc != nil { clear(wc.Occupied) for i := range wc.LockInNum { wc.LockInNum[i] = 0 wc.LockInDenom[i] = 1 } } l.vacantIdx = make(map[string][]NodeID, len(l.vacantIdx)) for _, n := range l.nodes { for _, c := range n.constraints { l.vacantIdx[c.Tag()] = append(l.vacantIdx[c.Tag()], n.id) } } } // RandomNode returns a random node from the lattice for walk initialization. func (l *Lattice) RandomNode() *Node { l.mu.RLock() defer l.mu.RUnlock() if len(l.nodes) == 0 { return nil } return l.nodes[rand.IntN(len(l.nodes))] } // VacantByTag returns a random vacant node whose constraint envelope // matches the given tag. func (l *Lattice) VacantByTag(tag string) *Node { l.mu.Lock() defer l.mu.Unlock() ids := l.vacantIdx[tag] if len(ids) == 0 { return nil } for attempts := len(ids); attempts > 0 && len(ids) > 0; attempts-- { idx := rand.IntN(len(ids)) nid := ids[idx] if int(nid) >= len(l.nodes) { ids[idx] = ids[len(ids)-1] ids = ids[:len(ids)-1] l.vacantIdx[tag] = ids continue } n := l.nodes[nid] if n.occupant == nil && n.constraints != nil { return n } ids[idx] = ids[len(ids)-1] ids = ids[:len(ids)-1] l.vacantIdx[tag] = ids } return nil } // ReindexVacant re-registers a node in the vascular index after its // occupant was dissolved. func (l *Lattice) ReindexVacant(n *Node) { l.mu.Lock() defer l.mu.Unlock() for _, c := range n.constraints { tag := c.Tag() l.vacantIdx[tag] = append(l.vacantIdx[tag], n.id) } } // RandomNeighbor returns a random neighbor of the given node. func RandomNeighbor(n *Node) *Node { if len(n.neighborIdx) == 0 { return nil } return n.lat.nodes[n.neighborIdx[rand.IntN(len(n.neighborIdx))]] } // SetEnergy updates the energy bit of a node's inner trigram. func (n *Node) SetEnergy(supersaturated bool) { n.hex = state.Hex(n.hex.Inner().SetEnergy(supersaturated), n.hex.Outer()) } // SetOuterTrigram updates the node's outer trigram based on its // environment (computed from neighbor states). func (n *Node) SetOuterTrigram(outer state.Trigram) { n.hex = state.Hex(n.hex.Inner(), outer) } // BondCount returns the number of constraints satisfied by the current occupant. func (n *Node) BondCount() int { return n.bondCount } // IncrementBond increases the bond count by 1 (Long-Term Potentiation). // Updates lockIn to reflect the new bond strength. func (n *Node) IncrementBond() { if n.occupant == nil { return } n.bondCount++ n.lockIn = ratio.FromInt(int64(n.bondCount)) n.updateHex() if wc := n.lat.walkCols; wc != nil { wc.SetLockIn(n.id, n.lockIn.Num, n.lockIn.Denom) } } // DecrementBond decreases the bond count by 1 (Long-Term Depression). // Bond count cannot go below 0. Updates lockIn accordingly. func (n *Node) DecrementBond() { if n.occupant == nil || n.bondCount <= 0 { return } n.bondCount-- n.lockIn = ratio.FromInt(int64(n.bondCount)) n.updateHex() if wc := n.lat.walkCols; wc != nil { wc.SetLockIn(n.id, n.lockIn.Num, n.lockIn.Denom) } } // RestoreAge sets the node's age directly (bypass increment logic). func (n *Node) RestoreAge(age uint8) { if age > 3 { age = 3 } n.age = age } // ForceOccupant places an element without checking constraints. // Used during mindsicle thaw where the state has been pre-validated. func (n *Node) ForceOccupant(e axiom.Element, bondCount int) { n.occupant = e n.bondCount = bondCount n.lockIn = ratio.FromInt(int64(bondCount)) n.age = 0 n.updateHex() if wc := n.lat.walkCols; wc != nil { wc.SetOccupied(n.id, true) wc.SetLockIn(n.id, n.lockIn.Num, n.lockIn.Denom) } } // RestoreLockIn sets the exact lock-in ratio from a frozen snapshot. func (n *Node) RestoreLockIn(li ratio.Ratio) { n.lockIn = li if wc := n.lat.walkCols; wc != nil { wc.SetLockIn(n.id, li.Num, li.Denom) } } // --- Backward-compatible aliases for block-decomposed growth --- // These used to skip mutex acquisition. Now that Node has no mutex, // they are identical to the standard methods. Kept as aliases so // existing callers in pkg/grow compile without changes. // OccupiedUnsafe checks occupancy (alias for Occupied). func (n *Node) OccupiedUnsafe() bool { return n.occupant != nil } // NeighborsUnsafe returns the neighbor slice (alias for Neighbors). func (n *Node) NeighborsUnsafe() []*Node { return n.Neighbors() } // AdmitsUnsafe checks constraint satisfaction (alias for Admits). func (n *Node) AdmitsUnsafe(e axiom.Element) bool { return n.Admits(e) } // BondUnsafe places an element (alias for Bond). func (n *Node) BondUnsafe(e axiom.Element) bool { return n.Bond(e) } // StripForEvictionUnsafe zeros all node state except id and neighbors. func (n *Node) StripForEvictionUnsafe() { n.occupant = nil n.candidates = nil n.constraints = nil n.bondCount = 0 n.crossLayer = false n.lockIn = ratio.Zero n.perm = 0 n.projVertex = 0 n.projKey = 0 n.projPath = 0 n.age = 0 n.hex = 0 } // RestoreConstraintsUnsafe sets the constraint envelope on a stripped node. func (n *Node) RestoreConstraintsUnsafe(constraints []axiom.Constraint) { n.constraints = constraints } // NeighborStates returns a summary trigram of the neighborhood. // Majority vote on each bit across all neighbors. func (n *Node) NeighborStates() state.Trigram { if len(n.neighborIdx) == 0 { return state.Earth } var bonding, constraint, energy int total := len(n.neighborIdx) for _, idx := range n.neighborIdx { nb := n.lat.nodes[idx] inner := nb.hex.Inner() if inner.Bonding() { bonding++ } if inner.Constraint() { constraint++ } if inner.Energy() { energy++ } } return state.Trigram(0). SetBonding(bonding > total/2). SetConstraint(constraint > total/2). SetEnergy(energy > total/2) }