Initial release: ENSEMBLE training-free AI — compressed .exp experts + Kuramoto brain
1f71c7d verified | """PALIMPSESTE — Meta-subspace ``H_meta`` & Lyapunov-bounded self-rewrite (Axiome 5). | |
| Axiome 5 — Auto-reference bornee par une fonction de Lyapunov | |
| The read-back kernel ``Phi`` is parameterized (radius, threshold sharpness, | |
| topk, min_weight). These *meta-parameters* are themselves encoded as traces | |
| in a reserved subspace ``H_meta ⊂ M``. The system may write into ``H_meta`` | |
| — i.e. rewrite its own read-back kernel — but only under the constraint: | |
| Δ E[surprise_predite] ≤ 0 | |
| i.e. a self-rewrite is accepted only if it would *retroactively* reduce the | |
| mean predictive surprise over a held-out replay horizon. This is Friston's | |
| active inference, reformulated as an energy-decreasing (Lyapunov) condition. | |
| Bounded recursion by construction | |
| --------------------------------- | |
| The spec flags a risk: calibrating ``r`` needs a meta-meta loop, ad infinitum. | |
| We bound this *by construction*: | |
| - the set of meta-knobs is **fixed and finite** (KernelConfig fields); | |
| - the **acceptance criterion** (the Lyapunov test) is **immutable** — it is | |
| not itself stored in ``H_meta`` and cannot be rewritten. It is the | |
| "constitution" of the system. This is the deliberate asymmetry that makes | |
| self-modification safe: the system can rewrite *what* it reads with, but | |
| never *the rule that says whether a rewrite is allowed*. | |
| Alignment (the third open problem) | |
| ---------------------------------- | |
| We add an invariant term to the Lyapunov energy: | |
| L = E[surprise] + λ · invariant_violations | |
| A self-rewrite is accepted iff ``ΔL ≤ 0``. Invariants are pluggable callables | |
| ``(Memory, KernelConfig) -> float >= 0`` (0 means no violation). By default we | |
| ship a ``max_radius`` invariant (prevents the trivial "accept everything" | |
| rewrite of inflating ``r`` to infinity) and a ``min_capacity`` invariant. | |
| Users can add their own (e.g. alignment constraints). This directly addresses | |
| the spec's honesty about needing a constraint term in ``L``. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| import math | |
| import numpy as np | |
| from .hv import HV, bind, random_hv, similarity, constant_hv | |
| from .memory import Memory, Trace | |
| from .phi import Phi, KernelConfig, Retrieval | |
| __all__ = [ | |
| "Invariant", | |
| "max_radius_invariant", | |
| "LyapunovEnergy", | |
| "MetaController", | |
| "MetaProposal", | |
| "MetaDecision", | |
| ] | |
| # ----------------------------------------------------------------- invariants | |
| class Invariant: | |
| """A pluggable constraint on (Memory, KernelConfig). | |
| ``violation(mem, cfg) -> float >= 0``; 0 means satisfied. The Lyapunov | |
| energy sums ``lambda * violation`` over all invariants. | |
| """ | |
| name: str | |
| violation: "callable[[Memory, KernelConfig], float]" | |
| lam: float = 1.0 | |
| def max_radius_invariant(max_r: int) -> Invariant: | |
| """Penalize configs whose radius exceeds ``max_r`` (in bits).""" | |
| def _viol(_mem: Memory, cfg: KernelConfig) -> float: | |
| return max(0.0, float(cfg.radius - max_r)) | |
| return Invariant(name=f"max_radius({max_r})", violation=_viol, lam=10.0) | |
| # ----------------------------------------------------------------- energy | |
| class LyapunovEnergy: | |
| """``L = E[surprise] + Σ λ_k · invariant_k``. | |
| The surprise term is the mean prediction error of ``Phi`` over a *replay* | |
| set of (query, target) pairs drawn from ``M`` itself (the system's own | |
| history). Lower is better. | |
| """ | |
| invariants: list[Invariant] = field(default_factory=list) | |
| def surprise(self, mem: Memory, phi: Phi, replay: list[tuple[HV, HV]]) -> float: | |
| """Mean per-item surprise over replay = ``1 - sim(Phi(q), target)``.""" | |
| if not replay: | |
| return 0.0 | |
| total = 0.0 | |
| for q, target in replay: | |
| out = phi(mem, q) | |
| if out is None: | |
| total += 1.0 # maximum surprise: no reconstruction | |
| else: | |
| total += 1.0 - max(0.0, similarity(out, target)) | |
| return total / len(replay) | |
| def __call__(self, mem: Memory, phi: Phi, cfg: KernelConfig, | |
| replay: list[tuple[HV, HV]]) -> float: | |
| s = self.surprise(mem, phi, replay) | |
| inv = sum(inv.lam * inv.violation(mem, cfg) for inv in self.invariants) | |
| return s + inv | |
| # ----------------------------------------------------------------- controller | |
| class MetaProposal: | |
| """A candidate new KernelConfig proposed by the controller.""" | |
| config: KernelConfig | |
| rationale: str | |
| class MetaDecision: | |
| """Outcome of evaluating a proposal against the Lyapunov constraint.""" | |
| proposal: MetaProposal | |
| accepted: bool | |
| energy_before: float | |
| energy_after: float | |
| delta: float | |
| reason: str | |
| class MetaController: | |
| """Reads/writes the kernel config in ``H_meta`` under Lyapunov control. | |
| The controller: | |
| 1. holds the *current* :class:`KernelConfig` (also the config of the | |
| :class:`Phi` it governs); | |
| 2. proposes perturbations of that config (small random walks over the | |
| knob space, biased toward reducing surprise); | |
| 3. accepts a proposal iff ``ΔL ≤ 0`` over a replay set; | |
| 4. on acceptance, writes the new config as a meta-trace in ``H_meta`` | |
| (append-only, like everything else) and swaps it into ``Phi``. | |
| The acceptance criterion (the Lyapunov test) is **not** in ``H_meta`` and | |
| **cannot** be rewritten by the controller. That asymmetry bounds recursion. | |
| """ | |
| mem: Memory | |
| phi: Phi | |
| energy: LyapunovEnergy | |
| rng: np.random.Generator = field(default_factory=np.random.default_rng) | |
| history: list[MetaDecision] = field(default_factory=list) | |
| _config_addr: HV | None = None # stable address under which configs are stored | |
| def __post_init__(self) -> None: | |
| # A stable, reserved address for the "current config" record. | |
| # We use a fixed seed-derived HV so reads are deterministic. | |
| rng = np.random.default_rng(0xC0FFEE) | |
| self._config_addr = random_hv(self.mem.D, rng=rng) | |
| self._write_current_config() | |
| # ----------------------------------------------------------- config store | |
| def _write_current_config(self) -> Trace: | |
| """Encode the current KernelConfig as a meta-trace in H_meta. | |
| The encoding is *symbolic* (a tagged dict serialized to a stable HV | |
| via the Encoder) rather than a raw HV, so it is human-readable on | |
| inspection and deterministic to decode. For simplicity here we store | |
| the config dict's repr-derived hash plus the actual values via a | |
| dedicated encode; decode reads the latest meta-trace under the | |
| config address. | |
| """ | |
| # We store the config as a meta-trace whose *value* is a deterministic | |
| # HV encoding of the config fields. Decoding is done by scanning the | |
| # latest meta-trace and reading the field values from a side-table. | |
| # To keep it simple and robust, we keep a parallel Python-side record | |
| # (self.phi.config) as the source of truth, and the meta-trace is the | |
| # append-only audit log of accepted rewrites. | |
| enc = _config_to_hv(self.phi.config, self.mem.D, self.rng) | |
| return self.mem.write( | |
| self._config_addr, # type: ignore[arg-type] | |
| enc, | |
| weight=1.0, | |
| meta=True, | |
| tag=f"kernel_config:{self.phi.config.encode()}", | |
| ) | |
| def config(self) -> KernelConfig: | |
| return self.phi.config | |
| # ----------------------------------------------------------- proposals | |
| def _perturb(self) -> MetaProposal: | |
| """Propose a small random walk over the knob space.""" | |
| cfg = self.phi.config | |
| r = self.rng | |
| # pick one knob to perturb | |
| knob = r.choice(["radius", "min_weight", "sharpness", "topk"]) | |
| rationale = f"perturb {knob}" | |
| if knob == "radius": | |
| new_r = max(0, cfg.radius + int(r.choice([-2, -1, 1, 2]))) | |
| return MetaProposal(KernelConfig(new_r, cfg.min_weight, cfg.sharpness, cfg.topk), rationale) | |
| if knob == "min_weight": | |
| lw = cfg.min_weight * r.choice([0.5, 2.0]) | |
| lw = min(max(lw, 1e-9), 1.0) | |
| return MetaProposal(KernelConfig(cfg.radius, lw, cfg.sharpness, cfg.topk), rationale) | |
| if knob == "sharpness": | |
| ns = max(0.0, cfg.sharpness + r.choice([-1.0, -0.5, 0.5, 1.0])) | |
| return MetaProposal(KernelConfig(cfg.radius, cfg.min_weight, ns, cfg.topk), rationale) | |
| # topk | |
| if cfg.topk is None: | |
| return MetaProposal(KernelConfig(cfg.radius, cfg.min_weight, cfg.sharpness, 16), "add topk") | |
| nk = max(1, cfg.topk + int(r.choice([-4, -2, 2, 4]))) | |
| return MetaProposal(KernelConfig(cfg.radius, cfg.min_weight, cfg.sharpness, nk), rationale) | |
| # ----------------------------------------------------------- evaluation | |
| def evaluate(self, proposal: MetaProposal, | |
| replay: list[tuple[HV, HV]]) -> MetaDecision: | |
| """Test ``ΔL ≤ 0`` for the proposal over ``replay``.""" | |
| before = self.energy(self.mem, self.phi, self.phi.config, replay) | |
| # temporarily swap config | |
| old = self.phi.config | |
| self.phi.config = proposal.config | |
| after = self.energy(self.mem, self.phi, proposal.config, replay) | |
| self.phi.config = old | |
| delta = after - before | |
| accepted = delta <= 0.0 | |
| reason = "ΔL ≤ 0" if accepted else f"ΔL = {delta:.4f} > 0 rejected" | |
| return MetaDecision( | |
| proposal=proposal, | |
| accepted=accepted, | |
| energy_before=before, | |
| energy_after=after, | |
| delta=delta, | |
| reason=reason, | |
| ) | |
| def step(self, replay: list[tuple[HV, HV]], | |
| max_proposals: int = 8) -> MetaDecision | None: | |
| """Propose up to ``max_proposals`` perturbations; accept the first that passes. | |
| Returns the accepted decision, or None if none passed. | |
| """ | |
| last: MetaDecision | None = None | |
| for _ in range(max_proposals): | |
| prop = self._perturb() | |
| dec = self.evaluate(prop, replay) | |
| self.history.append(dec) | |
| last = dec | |
| if dec.accepted: | |
| # commit: swap config + audit-log to H_meta | |
| self.phi.config = dec.proposal.config | |
| self._write_current_config() | |
| return dec | |
| return last | |
| # ----------------------------------------------------------- replay builder | |
| def build_replay(self, n: int = 64) -> list[tuple[HV, HV]]: | |
| """Build a replay set from ``M``'s own traces: ``(address, value)``. | |
| This is the "system's own past sensory stream" used for the Lyapunov | |
| test. Drawing from ``M`` itself makes the energy a measure of how well | |
| the current kernel reconstructs the system's own history. | |
| """ | |
| if not self.mem.traces: | |
| return [] | |
| idx = self.rng.choice( | |
| len(self.mem.traces), | |
| size=min(n, len(self.mem.traces)), | |
| replace=False, | |
| ) | |
| return [(self.mem.traces[i].address, self.mem.traces[i].value) for i in idx] | |
| # ----------------------------------------------------------- config <-> HV | |
| def _config_to_hv(cfg: KernelConfig, D: int, rng: np.random.Generator) -> HV: | |
| """Deterministic HV encoding of a KernelConfig (audit-log value). | |
| We don't need to *decode* the HV back into a config (the Python-side | |
| ``Phi.config`` is the source of truth); we just need a stable, distinct HV | |
| per distinct config so the audit log in ``H_meta`` is queryable. | |
| """ | |
| # Mix field values into a seed and draw a deterministic HV. | |
| seed = ( | |
| cfg.radius * 1_000_003 | |
| ^ int(cfg.min_weight * 1e9) | |
| ^ int(cfg.sharpness * 1e6) | |
| ^ (cfg.topk if cfg.topk is not None else -1) | |
| ) & 0xFFFFFFFF | |
| return random_hv(D, rng=np.random.default_rng(seed)) | |