"""Historic ledger and the PAST → PRESENT → FUTURE → PAST ring. The repair for a low-dimensional count is history. A bare counter knows only "how many"; it cannot say what the count *meant* last lap. So every tick gets a ledger entry, and the entry is addressed by the count itself: the key is ``bind(role("tick"), int_to_hv(count))``, so a lap-ago entry is recalled by content, not by an offset that shifts as the ring wraps. The ledger is a ring. ``capacity`` bounds it, and the oldest entry is consumed — then recycled into the per-phase holographic summary, which survives the eviction as a superposition. That is why the past stays queryable after the detail is gone. The pipeline, one lap per call to :meth:`RingController.step`: PAST recall the entry one period back and its phase summary PRESENT take the observation and measure surprise against the past's FUTURE prediction for this tick FUTURE evolve the state algebraically and ask the transition model what comes next -> PAST the future prediction is written into the ledger, to be graded on the next lap The algebraic evolution is ``permute(bind(state, key), k)``. Binding by a fixed key followed by a rotation is a bijection on bit patterns, so it is invertible: :func:`rewind` recovers the predecessor exactly. That is what makes "FUTURE → PAST" a real closure rather than a new forward pass. **Recycling bus.** Every lap produces two measured scalars: surprise (``1 − sim(predicted, observed)``) and the entropy of the surprise histogram in bits. Energy is the prediction-accuracy budget: energy' = clamp(energy + gain·(1 − surprise) − cost, 0, 1) Energy is *spent* on lookahead (the planning horizon is ``1 + 2·energy`` rounded), so an accurate ring plans further ahead and a surprising one pulls in its horizon. This is bookkeeping over measured error statistics, not a claim about physics: the invariant that matters is that energy and horizon move together and that entropy falls when the ring converges. """ from __future__ import annotations from typing import Callable, Iterable, Sequence from .entropy import shannon from .hypervec import DEFAULT_DIM, bind, bundle, hvc, int_to_hv, permute, sim from .memory_modes import HolographicMemory, PhotographicMemory from .state import PredictiveModel PHASES = ("past", "present", "future") def tick_key(count: int, d: int = DEFAULT_DIM) -> int: """The ledger key for a tick: role-bound count, so lookup is by content.""" return bind(hvc("ring/tick", d), int_to_hv(count, d), d) def evolve(state: int, key: int, k: int = 1, d: int = DEFAULT_DIM) -> int: """One algebraic step of the ring: bind then rotate. Invertible.""" return permute(bind(state, key, d), k, d) def rewind(state: int, key: int, k: int = 1, d: int = DEFAULT_DIM) -> int: """Exact inverse of :func:`evolve` — rotate back, unbind.""" return bind(permute(state, -k, d), key, d) class LedgerEntry: """One tick of history.""" __slots__ = ("count", "phase", "state", "predicted", "surprise", "energy") def __init__(self, count: int, phase: str, state: int, predicted: int | None, surprise: float, energy: float): self.count = count self.phase = phase self.state = state self.predicted = predicted self.surprise = surprise self.energy = energy def as_dict(self) -> dict: return {"count": self.count, "phase": self.phase, "surprise": round(self.surprise, 3), "energy": round(self.energy, 3), "predicted": self.predicted is not None} def __repr__(self) -> str: return "" % ( self.count, self.phase, self.surprise, self.energy) class HistoricLedger: """Ring of entries addressed by tick, with holographic summaries per phase.""" def __init__(self, d: int = DEFAULT_DIM, capacity: int = 16): if capacity < 1: raise ValueError("capacity must be positive") self.d = d self.capacity = capacity self.entries: list[LedgerEntry] = [] self.exact = PhotographicMemory(d=d) self.summaries = {phase: HolographicMemory(d=d) for phase in PHASES} self.recycled = 0 def __len__(self) -> int: return len(self.entries) def append(self, entry: LedgerEntry) -> None: """Add a tick; evicting the oldest recycles it into its phase summary.""" self.entries.append(entry) self.exact.capture("tick/%d" % entry.count, entry.state) if len(self.entries) > self.capacity: old = self.entries.pop(0) self.summaries[old.phase].record("recycled/%d" % old.count, old.state) self.recycled += 1 def at(self, count: int) -> LedgerEntry | None: """The entry for a tick, by count.""" for entry in reversed(self.entries): if entry.count == count: return entry return None def ago(self, period: int = 1) -> LedgerEntry | None: """The entry ``period`` ticks back.""" return self.entries[-1 - period] if len(self.entries) > period else None def recall_state(self, count: int) -> tuple[int | None, float]: """Exact recall of a tick's state; a hit means the ring still holds it.""" return self.exact.recall("tick/%d" % count) def timeline_record(self) -> int: """The whole retained history as one superposed vector. Each tick is bound to its own count-derived key and bundled, so a tick can be probed out of the timeline by content alone — the holographic view of the same history the exact index holds verbatim. """ if not self.entries: return 0 return bundle( [bind(tick_key(entry.count, self.d), entry.state, self.d) for entry in self.entries], self.d, ) def recall_timeline(self, count: int) -> tuple[int | None, float]: """Probe one tick out of the superposed timeline (approximate).""" if not self.entries: return None, 0.0 probe = bind(self.timeline_record(), tick_key(count, self.d), self.d) states = [entry.state for entry in self.entries] best, best_sim = None, -2.0 for candidate in states: score = sim(probe, candidate, self.d) if score > best_sim: best, best_sim = candidate, score return best, best_sim def entropy(self, buckets: int = 8) -> float: """Entropy of the surprise distribution, in bits.""" if not self.entries: return 0.0 counts = [0] * buckets for entry in self.entries: counts[min(buckets - 1, int(entry.surprise * buckets))] += 1 return shannon(counts) def summary_similarity(self, phase: str, state: int) -> float: """How much of a phase's recycled history the summary still carries.""" summary = self.summaries[phase] if not summary.keys: return 0.0 return summary.confidence("recycled/%d" % summary.keys[-1]) def stats(self) -> dict: return {"entries": len(self.entries), "capacity": self.capacity, "recycled": self.recycled, "entropy_bits": round(self.entropy(), 3)} class RingController: """Drive PAST → PRESENT → FUTURE → PAST with an energy/entropy budget.""" def __init__( self, d: int = DEFAULT_DIM, capacity: int = 16, gain: float = 0.5, cost: float = 0.25, alpha: float = 0.6, ): self.d = d self.ledger = HistoricLedger(d=d, capacity=capacity) self.model = PredictiveModel(d=d) self.key = hvc("ring/evolution", d) self.gain = gain self.cost = cost self.alpha = alpha self.energy = 1.0 self.count = 0 self.last: LedgerEntry | None = None self.observations: list[int] = [] # ------------------------------------------------------------- horizon def horizon(self) -> int: """Lookahead the current budget can pay for: 1..3 actions.""" return max(1, min(3, int(round(1 + 2 * self.energy)))) def mean_surprise(self) -> float: """Average surprise over retained history.""" if not self.ledger.entries: return 0.0 return sum(e.surprise for e in self.ledger.entries) / len(self.ledger.entries) def mode(self) -> str: """Ring logic mode from measured surprise and the energy budget. ``explore`` the world keeps surprising us: widen the horizon input, learn ``hold`` partly predicted: keep the current policy, plan one step ``stable`` converged and confident: act on the multi-step plan ``reset`` converged but unconfident: the model is spent, rebuild it """ surprise = self.mean_surprise() if surprise > 0.5: return "explore" if surprise > 0.2: return "hold" return "stable" if self.energy > 0.5 else "reset" # ---------------------------------------------------------------- step def step(self, observation: int, actions: Sequence[str] = ("next", "prev")) -> LedgerEntry: """Advance the ring by one tick and return the entry that was written. Every tick runs all three stages; the entry's ``phase`` label records which slot of the three-phase cycle it occupies, so the ledger reads as a rotating ring rather than a flat log. """ previous = self.ledger.entries[-1] if self.ledger.entries else None # PAST -> PRESENT: grade the FUTURE prediction the last tick left behind expected = previous.predicted if previous is not None else None surprise = 0.5 if expected is None else 1.0 - sim(expected, observation, self.d) # PRESENT: teach the transition model what actually followed if previous is not None: self.model.learn(previous.state, "tick", observation) self.observations.append(observation) # FUTURE: algebraic evolution is the fallback when the model is silent evolved = evolve(observation, self.key, 1, self.d) predicted, _confidence = self.model.predict(observation, "tick") if predicted is None: predicted = evolved # recycling bus: energy is spent on lookahead, refunded by accuracy self.energy = max(0.0, min(1.0, self.energy + self.gain * (1.0 - surprise) - self.cost)) entry = LedgerEntry( self.count, PHASES[self.count % len(PHASES)], observation, predicted, surprise, self.energy, ) self.ledger.append(entry) self.last = entry self.count += 1 return entry def run(self, observations: Iterable[int], actions: Sequence[str] = ("next", "prev")) -> list[LedgerEntry]: """Push a sequence of observations through the ring.""" return [self.step(state, actions) for state in observations] def report(self) -> str: lines = ["tick phase surprise energy mode horizon recycled"] for entry in self.ledger.entries: lines.append("%4d %-7s %8.3f %6.3f %-7s %7d %8d" % ( entry.count, entry.phase, entry.surprise, entry.energy, self.mode(), self.horizon(), self.ledger.recycled)) lines.append("energy %.3f, entropy %.3f bits, mode %s, horizon %d" % (self.energy, self.ledger.entropy(), self.mode(), self.horizon())) return "\n".join(lines) def periodic_world(period: int = 4, steps: int = 12, d: int = DEFAULT_DIM) -> list[int]: """A world whose state repeats with a fixed period: the ring should converge.""" symbols = [hvc("ring/world/%d" % i, d) for i in range(period)] return [symbols[i % period] for i in range(steps)] def drifting_world(period: int = 4, steps: int = 12, d: int = DEFAULT_DIM) -> list[int]: """A world that changes every lap: the ring should stay surprised.""" symbols = [hvc("ring/world/%d" % i, d) for i in range(period * steps)] return symbols[:steps] def report(d: int = DEFAULT_DIM) -> str: """Printable ring tour: a converging world and a drifting one.""" lines = ["ring controller, d=%d" % d] lines.append("periodic world (period 4, 12 ticks):") ring = RingController(d=d, capacity=8) ring.run(periodic_world(4, 12, d)) lines.append(ring.report()) lines.append("") lines.append("drifting world (new state every tick):") chaotic = RingController(d=d, capacity=8) chaotic.run(drifting_world(4, 12, d)) lines.append(chaotic.report()) return "\n".join(lines) if __name__ == "__main__": # pragma: no cover - manual inspection print(report())