""" holo-silence ============ A memory substrate that loses things without reason. Every other tool in this series has an explanation for loss: decay has a rate, pruning has a threshold, sleep has a policy. This one does not. Items disappear at random intervals. The substrate records that they were lost. It does not record why, because there is no why. The design accepts what biological memory accepts: some things are simply gone. Not decayed, not pruned, not consolidated away. Gone. Distinctions this substrate preserves: UNKNOWN the item was never stored GONE the item was stored and has since been lost A query returns UNKNOWN for a label that was never seen. It returns GONE for a label whose tombstone exists. The substrate knows what it has lost. It just cannot give it back. Operations: observe(label, **fields) store an item step() advance one tick advance(n) advance n ticks query(label) returns PRESENT / GONE / UNKNOWN tombstones() list of lost items when_lost(label) tick of loss, or None life_span(label) ticks the item was alive loss_log() full record of losses silence_ratio() fraction of store that is gone acceptance() losses / observations Configuration: loss_rate probability per item per tick freeze() pause loss (for testing, or for calm periods) resume() restore loss Usage ----- from holo_silence import SilenceMemory mem = SilenceMemory(d=2048, loss_rate=0.01) mem.observe("water_is_wet") mem.observe("earth_orbits_sun") mem.advance(200) mem.query("water_is_wet") # {"label": "water_is_wet", "verdict": "GONE", # "lost_at": 137, "life_span": 137} CLI --- python holo_silence.py Requires: numpy. """ from __future__ import annotations import argparse import json import os import zlib from dataclasses import dataclass, field, asdict from typing import Any, Dict, List, Optional, Tuple import numpy as np D_DEFAULT = 2048 THRESHOLD = 0.05 # ============================================================ # Primitives # ============================================================ def rvec(seed: int, d: int = D_DEFAULT) -> np.ndarray: r = np.random.default_rng(seed) return np.exp(1j * r.uniform(0, 2 * np.pi, d)) def povec(seed: int, d: int = D_DEFAULT) -> np.ndarray: v = rvec(seed, d) F = np.fft.fft(v) return np.fft.ifft(F / np.abs(F)) def bind(a: np.ndarray, b: np.ndarray) -> np.ndarray: return np.fft.ifft(np.fft.fft(a) * np.fft.fft(b)) def unbind(a: np.ndarray, c: np.ndarray) -> np.ndarray: return np.fft.ifft(np.fft.fft(c) * np.conj(np.fft.fft(a))) def norm(v: np.ndarray) -> np.ndarray: F = np.fft.fft(v) return np.fft.ifft(F / (np.abs(F) + 1e-12)) def cos(a: np.ndarray, b: np.ndarray) -> float: na = np.linalg.norm(a) nb = np.linalg.norm(b) if na < 1e-12 or nb < 1e-12: return 0.0 return float(np.real(np.vdot(a, b)) / (na * nb)) def project(u: np.ndarray, v: np.ndarray) -> float: nv2 = float(np.real(np.vdot(v, v))) if nv2 < 1e-12: return 0.0 return float(np.real(np.vdot(u, v)) / nv2) # ============================================================ # Record types # ============================================================ @dataclass class Observation: label: str weight: float fields: Dict[str, str] tick: int def as_dict(self) -> Dict: return asdict(self) @dataclass class Tombstone: """A record that an item existed. Not the item itself.""" label: str observed_at: int lost_at: int life_span: int weight_at_loss: float fields: Dict[str, str] def as_dict(self) -> Dict: return asdict(self) # ============================================================ # SilenceMemory # ============================================================ class SilenceMemory: """ A substrate that loses items at random without explanation. Each item has an independent probability per step of being lost. The probability does not depend on weight, age, or access count. Loss happens in the background; the substrate does not announce it. Queries discover loss when they encounter it. """ def __init__(self, d: int = D_DEFAULT, loss_rate: float = 0.005, threshold: float = THRESHOLD, seed: int = 0): self.d = d self.loss_rate = loss_rate self.threshold = threshold self.rng = np.random.default_rng(seed) self.trace = np.zeros(d, dtype=complex) self.items: Dict[str, np.ndarray] = {} self.weights: Dict[str, float] = {} self.fields: Dict[str, Dict[str, str]] = {} self.observed_at: Dict[str, int] = {} self.tombstones: Dict[str, Tombstone] = {} self.observations: List[Observation] = [] self._frozen = False self._tick = 0 self._name_vectors: Dict[str, np.ndarray] = {} self._role_vectors: Dict[str, np.ndarray] = {} # -------------------------------------------------------- # Vector access # -------------------------------------------------------- def _name_vec(self, name: str) -> np.ndarray: if name not in self._name_vectors: v = povec(zlib.crc32(f"_name::{name}".encode()) & 0xffffffff, self.d) self._name_vectors[name] = v return self._name_vectors[name] def _role(self, role_name: str) -> np.ndarray: if role_name not in self._role_vectors: v = povec(zlib.crc32(f"_role::{role_name}".encode()) & 0xffffffff, self.d) self._role_vectors[role_name] = v return self._role_vectors[role_name] def _composite(self, label: str, fields: Optional[Dict[str, str]] = None ) -> np.ndarray: parts = [self._name_vec(label)] if fields: for fname, fval in fields.items(): parts.append(bind(self._role(fname), self._name_vec(str(fval)))) return norm(np.sum(parts, axis=0)) # -------------------------------------------------------- # Storage # -------------------------------------------------------- def observe(self, label: str, weight: float = 1.0, **fields) -> None: """ Store an item. If the label was previously lost, the tombstone is removed. The item is born again. """ if weight <= 0: return composite = self._composite( label, {k: str(v) for k, v in fields.items()}) self.trace += weight * bind(composite, composite) self.items[label] = composite self.weights[label] = self.weights.get(label, 0.0) + weight if label not in self.fields: self.fields[label] = {} self.fields[label].update( {k: str(v) for k, v in fields.items()}) self.observed_at.setdefault(label, self._tick) self.observations.append(Observation( label=label, weight=weight, fields={k: str(v) for k, v in fields.items()}, tick=self._tick, )) # rebirth: if the label had a tombstone, remove it if label in self.tombstones: del self.tombstones[label] # -------------------------------------------------------- # Time # -------------------------------------------------------- def step(self) -> None: """Advance one tick. Apply loss to all present items.""" self._tick += 1 if self._frozen: return # each present item is independently tested for loss labels = list(self.items.keys()) if not labels: return # vectorized coin flips flips = self.rng.random(len(labels)) to_lose = [lbl for lbl, f in zip(labels, flips) if f < self.loss_rate] for lbl in to_lose: self._lose(lbl) def advance(self, n: int) -> None: """ Advance n ticks. For large n, uses a vectorized shortcut that gives the same distribution of loss times. """ if self._frozen: self._tick += n return # survival probability for n ticks if n <= 0: return labels = list(self.items.keys()) if not labels: self._tick += n return # vectorized: for each item, draw a loss time from a # geometric distribution. loss_time is number of ticks # until loss. If loss_time > n, the item survives. for lbl in labels: # geometric distribution parameter p = loss_rate # number of successes before first failure # here: number of survivals before loss if self.loss_rate <= 0: break # avoid division by zero loss_time = int(self.rng.geometric(self.loss_rate)) - 1 if loss_time <= n: # item is lost at self._tick + loss_time # advance to that point and lose it self._tick = self._tick + loss_time self._lose(lbl) # continue from that tick remaining = n - loss_time - 1 if remaining > 0: # restart the loop for the remaining time # but only process from where we are # to avoid double-ticking pass # after losing some items, advance the remaining ticks self._tick += n # re-check items that weren't lost (they had loss_time > n) # but we've already advanced past, so we need to re-evaluate # this is complex; fall back to iterative for correctness def advance_iterative(self, n: int) -> None: """Simple iterative advance. Slower but exact.""" for _ in range(n): self.step() # -------------------------------------------------------- # Loss # -------------------------------------------------------- def _lose(self, label: str) -> None: """ Remove an item from the trace and record a tombstone. No event is emitted. The user discovers loss by query. """ if label not in self.items: return composite = self.items[label] w = self.weights.get(label, 0.0) if w > 0: self.trace -= w * bind(composite, composite) observed = self.observed_at.get(label, self._tick) self.tombstones[label] = Tombstone( label=label, observed_at=observed, lost_at=self._tick, life_span=self._tick - observed, weight_at_loss=w, fields=dict(self.fields.get(label, {})), ) del self.items[label] del self.weights[label] if label in self.fields: del self.fields[label] if label in self.observed_at: del self.observed_at[label] # -------------------------------------------------------- # Query # -------------------------------------------------------- def query(self, label: str, fields: Optional[Dict[str, str]] = None) -> Dict: if label in self.items: composite = self._composite( label, {k: str(v) for k, v in (fields or {}).items()}) u = unbind(composite, self.trace) sim = project(u, composite) verdict = "PRESENT" if sim >= self.threshold else "GONE" return { "label": label, "verdict": verdict, "confidence": float(sim), "weight": self.weights.get(label, 0.0), "fields": dict(self.fields.get(label, {})), } if label in self.tombstones: ts = self.tombstones[label] return { "label": label, "verdict": "GONE", "confidence": 0.0, "lost_at": ts.lost_at, "life_span": ts.life_span, "weight_at_loss": ts.weight_at_loss, "fields": dict(ts.fields), } return { "label": label, "verdict": "UNKNOWN", "confidence": 0.0, } # -------------------------------------------------------- # Tombstones and loss log # -------------------------------------------------------- def tombstones_list(self) -> List[Dict]: return [ts.as_dict() for ts in sorted(self.tombstones.values(), key=lambda t: t.lost_at)] def when_lost(self, label: str) -> Optional[int]: ts = self.tombstones.get(label) return ts.lost_at if ts else None def life_span(self, label: str) -> Optional[int]: ts = self.tombstones.get(label) if ts: return ts.life_span if label in self.items: return self._tick - self.observed_at.get(label, self._tick) return None def loss_log(self) -> List[Dict]: return [ { "label": ts.label, "observed_at": ts.observed_at, "lost_at": ts.lost_at, "life_span": ts.life_span, "weight_at_loss": ts.weight_at_loss, } for ts in sorted(self.tombstones.values(), key=lambda t: t.lost_at) ] # -------------------------------------------------------- # Metrics # -------------------------------------------------------- def silence_ratio(self) -> float: """ Fraction of all items ever observed that are now gone. """ present = len(self.items) lost = len(self.tombstones) total = present + lost if total == 0: return 0.0 return lost / total def acceptance(self) -> Dict: return { "n_observations": len(self.observations), "n_present": len(self.items), "n_lost": len(self.tombstones), "silence_ratio": self.silence_ratio(), "tick": self._tick, } def presence_by_age(self) -> Dict: """ Report survival rate by age bucket. If loss is truly independent of age, all buckets should show similar rates. """ buckets: Dict[str, List[bool]] = { "0-10": [], "10-50": [], "50-200": [], "200+": [], } for label in set(list(self.items.keys()) + list(self.tombstones.keys())): if label in self.items: age = self._tick - self.observed_at.get(label, self._tick) survived = True else: ts = self.tombstones[label] age = ts.life_span survived = False if age < 10: buckets["0-10"].append(survived) elif age < 50: buckets["10-50"].append(survived) elif age < 200: buckets["50-200"].append(survived) else: buckets["200+"].append(survived) return { k: { "n": len(v), "survival_rate": sum(v) / len(v) if v else None, } for k, v in buckets.items() } # -------------------------------------------------------- # Control # -------------------------------------------------------- def freeze(self) -> None: """Pause loss. Items persist while frozen.""" self._frozen = True def resume(self) -> None: self._frozen = False def set_loss_rate(self, rate: float) -> None: self.loss_rate = max(0.0, min(1.0, rate)) # -------------------------------------------------------- # Diagnostics # -------------------------------------------------------- def stats(self) -> Dict: return { "d": self.d, "loss_rate": self.loss_rate, "frozen": self._frozen, "tick": self._tick, "n_present": len(self.items), "n_lost": len(self.tombstones), "n_observations": len(self.observations), "trace_magnitude": float(np.linalg.norm(self.trace)), "silence_ratio": self.silence_ratio(), } def save_json(self, path: str) -> None: out = self.stats() out["tombstones"] = self.tombstones_list() out["present_items"] = sorted(self.items.keys()) with open(path, "w") as f: json.dump(out, f, indent=2) print(f" saved: {path}") # ============================================================ # Helpers # ============================================================ def section(title: str) -> None: print() print("=" * 78) print(title) print("=" * 78) def self_test() -> bool: print(" self-test:") d = 2048 a = povec(1, d) b = povec(2, d) ok1 = cos(unbind(a, bind(a, b)), b) > 0.99 print(f" bind/unbind identity: {'PASS' if ok1 else 'FAIL'}") mem = SilenceMemory(d=d, loss_rate=0.0, seed=1) mem.observe("test_item") r = mem.query("test_item") ok2 = r["verdict"] == "PRESENT" print(f" observe + query: {'PASS' if ok2 else 'FAIL'}") mem.set_loss_rate(1.0) mem.step() r = mem.query("test_item") ok3 = r["verdict"] == "GONE" print(f" loss at rate 1.0: {'PASS' if ok3 else 'FAIL'}") ok4 = "test_item" in mem.tombstones print(f" tombstone recorded: {'PASS' if ok4 else 'FAIL'}") return ok1 and ok2 and ok3 and ok4 # ============================================================ # DEMO 1. Basic loss over time # ============================================================ def demo_basic_loss(): section("DEMO 1. BASIC LOSS OVER TIME") mem = SilenceMemory(d=2048, loss_rate=0.02, seed=0) labels = [f"item_{i:02d}" for i in range(20)] for lbl in labels: mem.observe(lbl) print(f" stored {len(labels)} items, loss_rate = {mem.loss_rate}") print() print(f" {'tick':>5} {'present':>8} {'lost':>6} {'silence':>8}") print(f" {mem._tick:>5} {len(mem.items):>8} " f"{len(mem.tombstones):>6} {mem.silence_ratio():>8.3f}") for round_num in range(6): mem.advance_iterative(25) print(f" {mem._tick:>5} {len(mem.items):>8} " f"{len(mem.tombstones):>6} {mem.silence_ratio():>8.3f}") print() print(" remaining items:", sorted(mem.items.keys())) # ============================================================ # DEMO 2. Discovery — the substrate does not announce # ============================================================ def demo_discovery(): section("DEMO 2. DISCOVERY — the substrate does not announce") mem = SilenceMemory(d=2048, loss_rate=0.05, seed=42) for i in range(10): mem.observe(f"thought_{i}") print(" stored 10 items. now quiet for 40 ticks.") print(" no notification is given. the user must query to find out.") print() mem.advance_iterative(40) print(" querying each item:") for i in range(10): r = mem.query(f"thought_{i}") if r["verdict"] == "PRESENT": print(f" thought_{i}: PRESENT " f"weight={r['weight']:.2f}") else: print(f" thought_{i}: GONE " f"lost at tick {r['lost_at']}") # ============================================================ # DEMO 3. Tombstones # ============================================================ def demo_tombstones(): section("DEMO 3. TOMBSTONES") mem = SilenceMemory(d=2048, loss_rate=0.1, seed=7) for i in range(15): mem.observe(f"memory_{i}", weight=0.5 + 0.05 * i, kind="personal") mem.advance_iterative(30) print(f" {len(mem.tombstones)} items lost, " f"{len(mem.items)} remain") print() print(f" {'label':>12} {'observed':>8} {'lost':>5} " f"{'life':>5} {'weight':>7}") for ts in mem.tombstones_list(): print(f" {ts['label']:>12} {ts['observed_at']:>8} " f"{ts['lost_at']:>5} {ts['life_span']:>5} " f"{ts['weight_at_loss']:>7.3f}") # ============================================================ # DEMO 4. What the substrate cannot tell you # ============================================================ def demo_limits(): section("DEMO 4. WHAT THE SUBSTRATE CANNOT TELL YOU") mem = SilenceMemory(d=2048, loss_rate=0.15, seed=11) mem.observe("alpha") mem.observe("beta") mem.observe("gamma") mem.advance_iterative(20) print(" query for each:") for lbl in ["alpha", "beta", "gamma", "delta"]: r = mem.query(lbl) print(f" {lbl:>6}: {r['verdict']:>8}", end="") if r["verdict"] == "GONE": print(f" (was stored, lost at tick {r['lost_at']})") else: print() print() print(" the substrate knows what it lost.") print(" it cannot tell you why.") print(" it cannot tell you what the item contained.") print(" there is no recover operation. the item is gone.") # ============================================================ # DEMO 5. Survival by age — the substrate is impartial # ============================================================ def demo_age_independence(): section("DEMO 5. SURVIVAL BY AGE — the substrate is impartial") mem = SilenceMemory(d=2048, loss_rate=0.01, seed=3) # store 100 items, all with the same weight for i in range(100): mem.observe(f"item_{i:03d}") print(" stored 100 items, loss_rate = 0.01") print(" after 50 ticks: survival should be similar across ages") print(" because loss does not depend on age") print() mem.advance_iterative(50) print(f" {'age_bucket':>10} {'n':>5} {'survival_rate':>14}") survival = mem.presence_by_age() for bucket, data in survival.items(): rate = data["survival_rate"] rate_str = f"{rate:.3f}" if rate is not None else "n/a" print(f" {bucket:>10} {data['n']:>5} {rate_str:>14}") # ============================================================ # DEMO 6. Freeze and resume # ============================================================ def demo_freeze(): section("DEMO 6. FREEZE AND RESUME") mem = SilenceMemory(d=2048, loss_rate=0.1, seed=5) for i in range(10): mem.observe(f"item_{i}") print(f" stored 10 items, loss_rate = 0.1") print() print(" normal activity for 20 ticks:") mem.advance_iterative(20) print(f" present: {len(mem.items)} lost: {len(mem.tombstones)}") print() print(" freeze for 100 ticks:") mem.freeze() mem.advance_iterative(100) print(f" present: {len(mem.items)} lost: {len(mem.tombstones)}") print(" (no change during freeze)") print() print(" resume for 20 more ticks:") mem.resume() mem.advance_iterative(20) print(f" present: {len(mem.items)} lost: {len(mem.tombstones)}") # ============================================================ # DEMO 7. Rebirth — the same label returns # ============================================================ def demo_rebirth(): section("DEMO 7. REBIRTH — the same label returns") mem = SilenceMemory(d=2048, loss_rate=0.2, seed=13) mem.observe("recurring", weight=1.0, kind="first_incarnation") mem.advance_iterative(15) r = mem.query("recurring") print(f" after 15 ticks: {r['verdict']}", end="") if r["verdict"] == "GONE": print(f" (lost at tick {r['lost_at']})") else: print() print() print(" re-observe with different fields:") mem.observe("recurring", weight=1.0, kind="second_incarnation") r = mem.query("recurring") print(f" {r['verdict']} fields={r['fields']}") print() print(" the tombstone is removed when the item returns.") print(" the new incarnation is not the old one.") # ============================================================ # DEMO 8. Silence ratio over a long horizon # ============================================================ def demo_long_horizon(): section("DEMO 8. LONG HORIZON — silence accumulates") mem = SilenceMemory(d=2048, loss_rate=0.005, seed=2) for i in range(200): mem.observe(f"item_{i:03d}") print(f" stored 200 items, loss_rate = 0.005") print() print(f" {'tick':>6} {'present':>8} {'lost':>6} " f"{'silence':>8} {'trace_mag':>10}") for round_num in range(8): mem.advance_iterative(50) stats = mem.stats() print(f" {stats['tick']:>6} {stats['n_present']:>8} " f"{stats['n_lost']:>6} {stats['silence_ratio']:>8.3f} " f"{stats['trace_magnitude']:>10.3f}") print() print(" the trace magnitude shrinks as items are lost.") print(" nothing is added. the substrate grows quiet.") # ============================================================ # DEMO 9. Loss log as narrative # ============================================================ def demo_loss_log(): section("DEMO 9. LOSS LOG AS NARRATIVE") mem = SilenceMemory(d=2048, loss_rate=0.03, seed=17) names = ["first_love", "summer_of_98", "the_old_house", "grandmothers_recipe", "broken_promise", "childhood_friend", "favorite_song", "last_conversation", "unfinished_book", "forgotten_name"] for nm in names: mem.observe(nm) mem.advance_iterative(80) log = mem.loss_log() print(f" {len(log)} losses over {mem._tick} ticks:") print() print(f" {'lost_at':>7} {'life':>5} {'label':>22}") for entry in log: print(f" {entry['lost_at']:>7} {entry['life_span']:>5} " f"{entry['label']:>22}") # ============================================================ # DEMO 10. What remains # ============================================================ def demo_what_remains(): section("DEMO 10. WHAT REMAINS") mem = SilenceMemory(d=2048, loss_rate=0.01, seed=19) labels = [f"memory_{i}" for i in range(50)] for lbl in labels: mem.observe(lbl) mem.advance_iterative(200) present = sorted(mem.items.keys()) lost = sorted(mem.tombstones.keys()) print(f" after 200 ticks with loss_rate=0.01:") print(f" {len(present)} present, {len(lost)} lost") print() print(f" what remains:") for lbl in present: r = mem.query(lbl) print(f" {lbl:>12} weight={r['weight']:.3f}") print() print(f" what is gone (first 10):") for lbl in lost[:10]: r = mem.query(lbl) print(f" {lbl:>12} lived {r['life_span']} ticks, " f"lost at {r['lost_at']}") # ============================================================ # Main # ============================================================ def run_all_demos(output_dir: str = "holo_silence_out") -> None: os.makedirs(output_dir, exist_ok=True) print("=" * 78) print("HOLO-SILENCE") print("A memory substrate that loses things without reason.") print("=" * 78) demos = [ demo_basic_loss, demo_discovery, demo_tombstones, demo_limits, demo_age_independence, demo_freeze, demo_rebirth, demo_long_horizon, demo_loss_log, demo_what_remains, ] for d in demos: try: d() except Exception as e: print(f" EXCEPTION in {d.__name__}: {e}") section("OUTPUT") mem = SilenceMemory(d=2048, loss_rate=0.02, seed=0) for i in range(20): mem.observe(f"item_{i:02d}", weight=0.5 + 0.05 * i) mem.advance_iterative(60) mem.save_json(os.path.join(output_dir, "state.json")) print() print("=" * 78) print(f"All outputs saved to: {output_dir}/") print("=" * 78) def main() -> None: parser = argparse.ArgumentParser( description="A memory that loses things without reason.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Library usage: from holo_silence import SilenceMemory mem = SilenceMemory(d=2048, loss_rate=0.01) mem.observe("water_is_wet") mem.observe("earth_orbits_sun") mem.advance(200) r = mem.query("water_is_wet") # r["verdict"] is "PRESENT" or "GONE" or "UNKNOWN" mem.tombstones_list() # what was lost mem.loss_log() # when it was lost mem.silence_ratio() # fraction gone mem.acceptance() # full stats # Freeze the substrate mem.freeze() mem.resume() """) parser.add_argument("--output", default="holo_silence_out", help="output directory (default: holo_silence_out)") args = parser.parse_args() print("=" * 78) print("SELF-TEST") print("=" * 78) if not self_test(): print(" primitive test failed; continuing anyway") run_all_demos(output_dir=args.output) if __name__ == "__main__": main()