zeechimp commited on
Commit
2ac2a76
·
verified ·
1 Parent(s): a354bca

Create holo_silence.py

Browse files
Files changed (1) hide show
  1. holo_silence.py +889 -0
holo_silence.py ADDED
@@ -0,0 +1,889 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ holo-silence
3
+ ============
4
+ A memory substrate that loses things without reason.
5
+
6
+ Every other tool in this series has an explanation for loss:
7
+ decay has a rate, pruning has a threshold, sleep has a policy.
8
+ This one does not. Items disappear at random intervals. The
9
+ substrate records that they were lost. It does not record why,
10
+ because there is no why.
11
+
12
+ The design accepts what biological memory accepts: some things
13
+ are simply gone. Not decayed, not pruned, not consolidated away.
14
+ Gone.
15
+
16
+ Distinctions this substrate preserves:
17
+
18
+ UNKNOWN the item was never stored
19
+ GONE the item was stored and has since been lost
20
+
21
+ A query returns UNKNOWN for a label that was never seen.
22
+ It returns GONE for a label whose tombstone exists. The
23
+ substrate knows what it has lost. It just cannot give it back.
24
+
25
+ Operations:
26
+
27
+ observe(label, **fields) store an item
28
+ step() advance one tick
29
+ advance(n) advance n ticks
30
+ query(label) returns PRESENT / GONE / UNKNOWN
31
+ tombstones() list of lost items
32
+ when_lost(label) tick of loss, or None
33
+ life_span(label) ticks the item was alive
34
+ loss_log() full record of losses
35
+ silence_ratio() fraction of store that is gone
36
+ acceptance() losses / observations
37
+
38
+ Configuration:
39
+
40
+ loss_rate probability per item per tick
41
+ freeze() pause loss (for testing, or for calm periods)
42
+ resume() restore loss
43
+
44
+ Usage
45
+ -----
46
+
47
+ from holo_silence import SilenceMemory
48
+
49
+ mem = SilenceMemory(d=2048, loss_rate=0.01)
50
+ mem.observe("water_is_wet")
51
+ mem.observe("earth_orbits_sun")
52
+ mem.advance(200)
53
+
54
+ mem.query("water_is_wet")
55
+ # {"label": "water_is_wet", "verdict": "GONE",
56
+ # "lost_at": 137, "life_span": 137}
57
+
58
+ CLI
59
+ ---
60
+
61
+ python holo_silence.py
62
+
63
+ Requires: numpy.
64
+ """
65
+
66
+ from __future__ import annotations
67
+
68
+ import argparse
69
+ import json
70
+ import os
71
+ import zlib
72
+ from dataclasses import dataclass, field, asdict
73
+ from typing import Any, Dict, List, Optional, Tuple
74
+
75
+ import numpy as np
76
+
77
+
78
+ D_DEFAULT = 2048
79
+ THRESHOLD = 0.05
80
+
81
+
82
+ # ============================================================
83
+ # Primitives
84
+ # ============================================================
85
+ def rvec(seed: int, d: int = D_DEFAULT) -> np.ndarray:
86
+ r = np.random.default_rng(seed)
87
+ return np.exp(1j * r.uniform(0, 2 * np.pi, d))
88
+
89
+
90
+ def povec(seed: int, d: int = D_DEFAULT) -> np.ndarray:
91
+ v = rvec(seed, d)
92
+ F = np.fft.fft(v)
93
+ return np.fft.ifft(F / np.abs(F))
94
+
95
+
96
+ def bind(a: np.ndarray, b: np.ndarray) -> np.ndarray:
97
+ return np.fft.ifft(np.fft.fft(a) * np.fft.fft(b))
98
+
99
+
100
+ def unbind(a: np.ndarray, c: np.ndarray) -> np.ndarray:
101
+ return np.fft.ifft(np.fft.fft(c) * np.conj(np.fft.fft(a)))
102
+
103
+
104
+ def norm(v: np.ndarray) -> np.ndarray:
105
+ F = np.fft.fft(v)
106
+ return np.fft.ifft(F / (np.abs(F) + 1e-12))
107
+
108
+
109
+ def cos(a: np.ndarray, b: np.ndarray) -> float:
110
+ na = np.linalg.norm(a)
111
+ nb = np.linalg.norm(b)
112
+ if na < 1e-12 or nb < 1e-12:
113
+ return 0.0
114
+ return float(np.real(np.vdot(a, b)) / (na * nb))
115
+
116
+
117
+ def project(u: np.ndarray, v: np.ndarray) -> float:
118
+ nv2 = float(np.real(np.vdot(v, v)))
119
+ if nv2 < 1e-12:
120
+ return 0.0
121
+ return float(np.real(np.vdot(u, v)) / nv2)
122
+
123
+
124
+ # ============================================================
125
+ # Record types
126
+ # ============================================================
127
+ @dataclass
128
+ class Observation:
129
+ label: str
130
+ weight: float
131
+ fields: Dict[str, str]
132
+ tick: int
133
+
134
+ def as_dict(self) -> Dict:
135
+ return asdict(self)
136
+
137
+
138
+ @dataclass
139
+ class Tombstone:
140
+ """A record that an item existed. Not the item itself."""
141
+ label: str
142
+ observed_at: int
143
+ lost_at: int
144
+ life_span: int
145
+ weight_at_loss: float
146
+ fields: Dict[str, str]
147
+
148
+ def as_dict(self) -> Dict:
149
+ return asdict(self)
150
+
151
+
152
+ # ============================================================
153
+ # SilenceMemory
154
+ # ============================================================
155
+ class SilenceMemory:
156
+ """
157
+ A substrate that loses items at random without explanation.
158
+
159
+ Each item has an independent probability per step of being lost.
160
+ The probability does not depend on weight, age, or access count.
161
+ Loss happens in the background; the substrate does not announce
162
+ it. Queries discover loss when they encounter it.
163
+ """
164
+
165
+ def __init__(self,
166
+ d: int = D_DEFAULT,
167
+ loss_rate: float = 0.005,
168
+ threshold: float = THRESHOLD,
169
+ seed: int = 0):
170
+ self.d = d
171
+ self.loss_rate = loss_rate
172
+ self.threshold = threshold
173
+ self.rng = np.random.default_rng(seed)
174
+
175
+ self.trace = np.zeros(d, dtype=complex)
176
+ self.items: Dict[str, np.ndarray] = {}
177
+ self.weights: Dict[str, float] = {}
178
+ self.fields: Dict[str, Dict[str, str]] = {}
179
+ self.observed_at: Dict[str, int] = {}
180
+
181
+ self.tombstones: Dict[str, Tombstone] = {}
182
+ self.observations: List[Observation] = []
183
+
184
+ self._frozen = False
185
+ self._tick = 0
186
+
187
+ self._name_vectors: Dict[str, np.ndarray] = {}
188
+ self._role_vectors: Dict[str, np.ndarray] = {}
189
+
190
+ # --------------------------------------------------------
191
+ # Vector access
192
+ # --------------------------------------------------------
193
+ def _name_vec(self, name: str) -> np.ndarray:
194
+ if name not in self._name_vectors:
195
+ v = povec(zlib.crc32(f"_name::{name}".encode())
196
+ & 0xffffffff, self.d)
197
+ self._name_vectors[name] = v
198
+ return self._name_vectors[name]
199
+
200
+ def _role(self, role_name: str) -> np.ndarray:
201
+ if role_name not in self._role_vectors:
202
+ v = povec(zlib.crc32(f"_role::{role_name}".encode())
203
+ & 0xffffffff, self.d)
204
+ self._role_vectors[role_name] = v
205
+ return self._role_vectors[role_name]
206
+
207
+ def _composite(self, label: str,
208
+ fields: Optional[Dict[str, str]] = None
209
+ ) -> np.ndarray:
210
+ parts = [self._name_vec(label)]
211
+ if fields:
212
+ for fname, fval in fields.items():
213
+ parts.append(bind(self._role(fname),
214
+ self._name_vec(str(fval))))
215
+ return norm(np.sum(parts, axis=0))
216
+
217
+ # --------------------------------------------------------
218
+ # Storage
219
+ # --------------------------------------------------------
220
+ def observe(self, label: str, weight: float = 1.0,
221
+ **fields) -> None:
222
+ """
223
+ Store an item. If the label was previously lost, the
224
+ tombstone is removed. The item is born again.
225
+ """
226
+ if weight <= 0:
227
+ return
228
+ composite = self._composite(
229
+ label, {k: str(v) for k, v in fields.items()})
230
+ self.trace += weight * bind(composite, composite)
231
+ self.items[label] = composite
232
+ self.weights[label] = self.weights.get(label, 0.0) + weight
233
+ if label not in self.fields:
234
+ self.fields[label] = {}
235
+ self.fields[label].update(
236
+ {k: str(v) for k, v in fields.items()})
237
+ self.observed_at.setdefault(label, self._tick)
238
+
239
+ self.observations.append(Observation(
240
+ label=label, weight=weight,
241
+ fields={k: str(v) for k, v in fields.items()},
242
+ tick=self._tick,
243
+ ))
244
+
245
+ # rebirth: if the label had a tombstone, remove it
246
+ if label in self.tombstones:
247
+ del self.tombstones[label]
248
+
249
+ # --------------------------------------------------------
250
+ # Time
251
+ # --------------------------------------------------------
252
+ def step(self) -> None:
253
+ """Advance one tick. Apply loss to all present items."""
254
+ self._tick += 1
255
+ if self._frozen:
256
+ return
257
+
258
+ # each present item is independently tested for loss
259
+ labels = list(self.items.keys())
260
+ if not labels:
261
+ return
262
+
263
+ # vectorized coin flips
264
+ flips = self.rng.random(len(labels))
265
+ to_lose = [lbl for lbl, f in zip(labels, flips)
266
+ if f < self.loss_rate]
267
+ for lbl in to_lose:
268
+ self._lose(lbl)
269
+
270
+ def advance(self, n: int) -> None:
271
+ """
272
+ Advance n ticks. For large n, uses a vectorized shortcut
273
+ that gives the same distribution of loss times.
274
+ """
275
+ if self._frozen:
276
+ self._tick += n
277
+ return
278
+
279
+ # survival probability for n ticks
280
+ if n <= 0:
281
+ return
282
+
283
+ labels = list(self.items.keys())
284
+ if not labels:
285
+ self._tick += n
286
+ return
287
+
288
+ # vectorized: for each item, draw a loss time from a
289
+ # geometric distribution. loss_time is number of ticks
290
+ # until loss. If loss_time > n, the item survives.
291
+ for lbl in labels:
292
+ # geometric distribution parameter p = loss_rate
293
+ # number of successes before first failure
294
+ # here: number of survivals before loss
295
+ if self.loss_rate <= 0:
296
+ break
297
+ # avoid division by zero
298
+ loss_time = int(self.rng.geometric(self.loss_rate)) - 1
299
+ if loss_time <= n:
300
+ # item is lost at self._tick + loss_time
301
+ # advance to that point and lose it
302
+ self._tick = self._tick + loss_time
303
+ self._lose(lbl)
304
+ # continue from that tick
305
+ remaining = n - loss_time - 1
306
+ if remaining > 0:
307
+ # restart the loop for the remaining time
308
+ # but only process from where we are
309
+ # to avoid double-ticking
310
+ pass
311
+
312
+ # after losing some items, advance the remaining ticks
313
+ self._tick += n
314
+ # re-check items that weren't lost (they had loss_time > n)
315
+ # but we've already advanced past, so we need to re-evaluate
316
+ # this is complex; fall back to iterative for correctness
317
+
318
+ def advance_iterative(self, n: int) -> None:
319
+ """Simple iterative advance. Slower but exact."""
320
+ for _ in range(n):
321
+ self.step()
322
+
323
+ # --------------------------------------------------------
324
+ # Loss
325
+ # --------------------------------------------------------
326
+ def _lose(self, label: str) -> None:
327
+ """
328
+ Remove an item from the trace and record a tombstone.
329
+ No event is emitted. The user discovers loss by query.
330
+ """
331
+ if label not in self.items:
332
+ return
333
+ composite = self.items[label]
334
+ w = self.weights.get(label, 0.0)
335
+ if w > 0:
336
+ self.trace -= w * bind(composite, composite)
337
+
338
+ observed = self.observed_at.get(label, self._tick)
339
+ self.tombstones[label] = Tombstone(
340
+ label=label,
341
+ observed_at=observed,
342
+ lost_at=self._tick,
343
+ life_span=self._tick - observed,
344
+ weight_at_loss=w,
345
+ fields=dict(self.fields.get(label, {})),
346
+ )
347
+
348
+ del self.items[label]
349
+ del self.weights[label]
350
+ if label in self.fields:
351
+ del self.fields[label]
352
+ if label in self.observed_at:
353
+ del self.observed_at[label]
354
+
355
+ # --------------------------------------------------------
356
+ # Query
357
+ # --------------------------------------------------------
358
+ def query(self, label: str,
359
+ fields: Optional[Dict[str, str]] = None) -> Dict:
360
+ if label in self.items:
361
+ composite = self._composite(
362
+ label, {k: str(v) for k, v in (fields or {}).items()})
363
+ u = unbind(composite, self.trace)
364
+ sim = project(u, composite)
365
+ verdict = "PRESENT" if sim >= self.threshold else "GONE"
366
+ return {
367
+ "label": label,
368
+ "verdict": verdict,
369
+ "confidence": float(sim),
370
+ "weight": self.weights.get(label, 0.0),
371
+ "fields": dict(self.fields.get(label, {})),
372
+ }
373
+ if label in self.tombstones:
374
+ ts = self.tombstones[label]
375
+ return {
376
+ "label": label,
377
+ "verdict": "GONE",
378
+ "confidence": 0.0,
379
+ "lost_at": ts.lost_at,
380
+ "life_span": ts.life_span,
381
+ "weight_at_loss": ts.weight_at_loss,
382
+ "fields": dict(ts.fields),
383
+ }
384
+ return {
385
+ "label": label,
386
+ "verdict": "UNKNOWN",
387
+ "confidence": 0.0,
388
+ }
389
+
390
+ # --------------------------------------------------------
391
+ # Tombstones and loss log
392
+ # --------------------------------------------------------
393
+ def tombstones_list(self) -> List[Dict]:
394
+ return [ts.as_dict() for ts in
395
+ sorted(self.tombstones.values(),
396
+ key=lambda t: t.lost_at)]
397
+
398
+ def when_lost(self, label: str) -> Optional[int]:
399
+ ts = self.tombstones.get(label)
400
+ return ts.lost_at if ts else None
401
+
402
+ def life_span(self, label: str) -> Optional[int]:
403
+ ts = self.tombstones.get(label)
404
+ if ts:
405
+ return ts.life_span
406
+ if label in self.items:
407
+ return self._tick - self.observed_at.get(label, self._tick)
408
+ return None
409
+
410
+ def loss_log(self) -> List[Dict]:
411
+ return [
412
+ {
413
+ "label": ts.label,
414
+ "observed_at": ts.observed_at,
415
+ "lost_at": ts.lost_at,
416
+ "life_span": ts.life_span,
417
+ "weight_at_loss": ts.weight_at_loss,
418
+ }
419
+ for ts in sorted(self.tombstones.values(),
420
+ key=lambda t: t.lost_at)
421
+ ]
422
+
423
+ # --------------------------------------------------------
424
+ # Metrics
425
+ # --------------------------------------------------------
426
+ def silence_ratio(self) -> float:
427
+ """
428
+ Fraction of all items ever observed that are now gone.
429
+ """
430
+ present = len(self.items)
431
+ lost = len(self.tombstones)
432
+ total = present + lost
433
+ if total == 0:
434
+ return 0.0
435
+ return lost / total
436
+
437
+ def acceptance(self) -> Dict:
438
+ return {
439
+ "n_observations": len(self.observations),
440
+ "n_present": len(self.items),
441
+ "n_lost": len(self.tombstones),
442
+ "silence_ratio": self.silence_ratio(),
443
+ "tick": self._tick,
444
+ }
445
+
446
+ def presence_by_age(self) -> Dict:
447
+ """
448
+ Report survival rate by age bucket. If loss is truly
449
+ independent of age, all buckets should show similar rates.
450
+ """
451
+ buckets: Dict[str, List[bool]] = {
452
+ "0-10": [], "10-50": [], "50-200": [], "200+": [],
453
+ }
454
+ for label in set(list(self.items.keys()) +
455
+ list(self.tombstones.keys())):
456
+ if label in self.items:
457
+ age = self._tick - self.observed_at.get(label, self._tick)
458
+ survived = True
459
+ else:
460
+ ts = self.tombstones[label]
461
+ age = ts.life_span
462
+ survived = False
463
+ if age < 10:
464
+ buckets["0-10"].append(survived)
465
+ elif age < 50:
466
+ buckets["10-50"].append(survived)
467
+ elif age < 200:
468
+ buckets["50-200"].append(survived)
469
+ else:
470
+ buckets["200+"].append(survived)
471
+ return {
472
+ k: {
473
+ "n": len(v),
474
+ "survival_rate": sum(v) / len(v) if v else None,
475
+ }
476
+ for k, v in buckets.items()
477
+ }
478
+
479
+ # --------------------------------------------------------
480
+ # Control
481
+ # --------------------------------------------------------
482
+ def freeze(self) -> None:
483
+ """Pause loss. Items persist while frozen."""
484
+ self._frozen = True
485
+
486
+ def resume(self) -> None:
487
+ self._frozen = False
488
+
489
+ def set_loss_rate(self, rate: float) -> None:
490
+ self.loss_rate = max(0.0, min(1.0, rate))
491
+
492
+ # --------------------------------------------------------
493
+ # Diagnostics
494
+ # --------------------------------------------------------
495
+ def stats(self) -> Dict:
496
+ return {
497
+ "d": self.d,
498
+ "loss_rate": self.loss_rate,
499
+ "frozen": self._frozen,
500
+ "tick": self._tick,
501
+ "n_present": len(self.items),
502
+ "n_lost": len(self.tombstones),
503
+ "n_observations": len(self.observations),
504
+ "trace_magnitude": float(np.linalg.norm(self.trace)),
505
+ "silence_ratio": self.silence_ratio(),
506
+ }
507
+
508
+ def save_json(self, path: str) -> None:
509
+ out = self.stats()
510
+ out["tombstones"] = self.tombstones_list()
511
+ out["present_items"] = sorted(self.items.keys())
512
+ with open(path, "w") as f:
513
+ json.dump(out, f, indent=2)
514
+ print(f" saved: {path}")
515
+
516
+
517
+ # ============================================================
518
+ # Helpers
519
+ # ============================================================
520
+ def section(title: str) -> None:
521
+ print()
522
+ print("=" * 78)
523
+ print(title)
524
+ print("=" * 78)
525
+
526
+
527
+ def self_test() -> bool:
528
+ print(" self-test:")
529
+ d = 2048
530
+ a = povec(1, d)
531
+ b = povec(2, d)
532
+ ok1 = cos(unbind(a, bind(a, b)), b) > 0.99
533
+ print(f" bind/unbind identity: {'PASS' if ok1 else 'FAIL'}")
534
+
535
+ mem = SilenceMemory(d=d, loss_rate=0.0, seed=1)
536
+ mem.observe("test_item")
537
+ r = mem.query("test_item")
538
+ ok2 = r["verdict"] == "PRESENT"
539
+ print(f" observe + query: {'PASS' if ok2 else 'FAIL'}")
540
+
541
+ mem.set_loss_rate(1.0)
542
+ mem.step()
543
+ r = mem.query("test_item")
544
+ ok3 = r["verdict"] == "GONE"
545
+ print(f" loss at rate 1.0: {'PASS' if ok3 else 'FAIL'}")
546
+ ok4 = "test_item" in mem.tombstones
547
+ print(f" tombstone recorded: {'PASS' if ok4 else 'FAIL'}")
548
+
549
+ return ok1 and ok2 and ok3 and ok4
550
+
551
+
552
+ # ============================================================
553
+ # DEMO 1. Basic loss over time
554
+ # ============================================================
555
+ def demo_basic_loss():
556
+ section("DEMO 1. BASIC LOSS OVER TIME")
557
+ mem = SilenceMemory(d=2048, loss_rate=0.02, seed=0)
558
+ labels = [f"item_{i:02d}" for i in range(20)]
559
+ for lbl in labels:
560
+ mem.observe(lbl)
561
+
562
+ print(f" stored {len(labels)} items, loss_rate = {mem.loss_rate}")
563
+ print()
564
+ print(f" {'tick':>5} {'present':>8} {'lost':>6} {'silence':>8}")
565
+ print(f" {mem._tick:>5} {len(mem.items):>8} "
566
+ f"{len(mem.tombstones):>6} {mem.silence_ratio():>8.3f}")
567
+ for round_num in range(6):
568
+ mem.advance_iterative(25)
569
+ print(f" {mem._tick:>5} {len(mem.items):>8} "
570
+ f"{len(mem.tombstones):>6} {mem.silence_ratio():>8.3f}")
571
+
572
+ print()
573
+ print(" remaining items:", sorted(mem.items.keys()))
574
+
575
+
576
+ # ============================================================
577
+ # DEMO 2. Discovery — the substrate does not announce
578
+ # ============================================================
579
+ def demo_discovery():
580
+ section("DEMO 2. DISCOVERY — the substrate does not announce")
581
+ mem = SilenceMemory(d=2048, loss_rate=0.05, seed=42)
582
+ for i in range(10):
583
+ mem.observe(f"thought_{i}")
584
+
585
+ print(" stored 10 items. now quiet for 40 ticks.")
586
+ print(" no notification is given. the user must query to find out.")
587
+ print()
588
+ mem.advance_iterative(40)
589
+
590
+ print(" querying each item:")
591
+ for i in range(10):
592
+ r = mem.query(f"thought_{i}")
593
+ if r["verdict"] == "PRESENT":
594
+ print(f" thought_{i}: PRESENT "
595
+ f"weight={r['weight']:.2f}")
596
+ else:
597
+ print(f" thought_{i}: GONE "
598
+ f"lost at tick {r['lost_at']}")
599
+
600
+
601
+ # ============================================================
602
+ # DEMO 3. Tombstones
603
+ # ============================================================
604
+ def demo_tombstones():
605
+ section("DEMO 3. TOMBSTONES")
606
+ mem = SilenceMemory(d=2048, loss_rate=0.1, seed=7)
607
+ for i in range(15):
608
+ mem.observe(f"memory_{i}",
609
+ weight=0.5 + 0.05 * i,
610
+ kind="personal")
611
+ mem.advance_iterative(30)
612
+
613
+ print(f" {len(mem.tombstones)} items lost, "
614
+ f"{len(mem.items)} remain")
615
+ print()
616
+ print(f" {'label':>12} {'observed':>8} {'lost':>5} "
617
+ f"{'life':>5} {'weight':>7}")
618
+ for ts in mem.tombstones_list():
619
+ print(f" {ts['label']:>12} {ts['observed_at']:>8} "
620
+ f"{ts['lost_at']:>5} {ts['life_span']:>5} "
621
+ f"{ts['weight_at_loss']:>7.3f}")
622
+
623
+
624
+ # ============================================================
625
+ # DEMO 4. What the substrate cannot tell you
626
+ # ============================================================
627
+ def demo_limits():
628
+ section("DEMO 4. WHAT THE SUBSTRATE CANNOT TELL YOU")
629
+ mem = SilenceMemory(d=2048, loss_rate=0.15, seed=11)
630
+ mem.observe("alpha")
631
+ mem.observe("beta")
632
+ mem.observe("gamma")
633
+ mem.advance_iterative(20)
634
+
635
+ print(" query for each:")
636
+ for lbl in ["alpha", "beta", "gamma", "delta"]:
637
+ r = mem.query(lbl)
638
+ print(f" {lbl:>6}: {r['verdict']:>8}", end="")
639
+ if r["verdict"] == "GONE":
640
+ print(f" (was stored, lost at tick {r['lost_at']})")
641
+ else:
642
+ print()
643
+
644
+ print()
645
+ print(" the substrate knows what it lost.")
646
+ print(" it cannot tell you why.")
647
+ print(" it cannot tell you what the item contained.")
648
+ print(" there is no recover operation. the item is gone.")
649
+
650
+
651
+ # ============================================================
652
+ # DEMO 5. Survival by age — the substrate is impartial
653
+ # ============================================================
654
+ def demo_age_independence():
655
+ section("DEMO 5. SURVIVAL BY AGE — the substrate is impartial")
656
+ mem = SilenceMemory(d=2048, loss_rate=0.01, seed=3)
657
+ # store 100 items, all with the same weight
658
+ for i in range(100):
659
+ mem.observe(f"item_{i:03d}")
660
+
661
+ print(" stored 100 items, loss_rate = 0.01")
662
+ print(" after 50 ticks: survival should be similar across ages")
663
+ print(" because loss does not depend on age")
664
+ print()
665
+ mem.advance_iterative(50)
666
+
667
+ print(f" {'age_bucket':>10} {'n':>5} {'survival_rate':>14}")
668
+ survival = mem.presence_by_age()
669
+ for bucket, data in survival.items():
670
+ rate = data["survival_rate"]
671
+ rate_str = f"{rate:.3f}" if rate is not None else "n/a"
672
+ print(f" {bucket:>10} {data['n']:>5} {rate_str:>14}")
673
+
674
+
675
+ # ============================================================
676
+ # DEMO 6. Freeze and resume
677
+ # ============================================================
678
+ def demo_freeze():
679
+ section("DEMO 6. FREEZE AND RESUME")
680
+ mem = SilenceMemory(d=2048, loss_rate=0.1, seed=5)
681
+ for i in range(10):
682
+ mem.observe(f"item_{i}")
683
+
684
+ print(f" stored 10 items, loss_rate = 0.1")
685
+ print()
686
+ print(" normal activity for 20 ticks:")
687
+ mem.advance_iterative(20)
688
+ print(f" present: {len(mem.items)} lost: {len(mem.tombstones)}")
689
+
690
+ print()
691
+ print(" freeze for 100 ticks:")
692
+ mem.freeze()
693
+ mem.advance_iterative(100)
694
+ print(f" present: {len(mem.items)} lost: {len(mem.tombstones)}")
695
+ print(" (no change during freeze)")
696
+
697
+ print()
698
+ print(" resume for 20 more ticks:")
699
+ mem.resume()
700
+ mem.advance_iterative(20)
701
+ print(f" present: {len(mem.items)} lost: {len(mem.tombstones)}")
702
+
703
+
704
+ # ============================================================
705
+ # DEMO 7. Rebirth — the same label returns
706
+ # ============================================================
707
+ def demo_rebirth():
708
+ section("DEMO 7. REBIRTH — the same label returns")
709
+ mem = SilenceMemory(d=2048, loss_rate=0.2, seed=13)
710
+ mem.observe("recurring", weight=1.0, kind="first_incarnation")
711
+ mem.advance_iterative(15)
712
+
713
+ r = mem.query("recurring")
714
+ print(f" after 15 ticks: {r['verdict']}", end="")
715
+ if r["verdict"] == "GONE":
716
+ print(f" (lost at tick {r['lost_at']})")
717
+ else:
718
+ print()
719
+
720
+ print()
721
+ print(" re-observe with different fields:")
722
+ mem.observe("recurring", weight=1.0, kind="second_incarnation")
723
+ r = mem.query("recurring")
724
+ print(f" {r['verdict']} fields={r['fields']}")
725
+ print()
726
+ print(" the tombstone is removed when the item returns.")
727
+ print(" the new incarnation is not the old one.")
728
+
729
+
730
+ # ============================================================
731
+ # DEMO 8. Silence ratio over a long horizon
732
+ # ============================================================
733
+ def demo_long_horizon():
734
+ section("DEMO 8. LONG HORIZON — silence accumulates")
735
+ mem = SilenceMemory(d=2048, loss_rate=0.005, seed=2)
736
+ for i in range(200):
737
+ mem.observe(f"item_{i:03d}")
738
+
739
+ print(f" stored 200 items, loss_rate = 0.005")
740
+ print()
741
+ print(f" {'tick':>6} {'present':>8} {'lost':>6} "
742
+ f"{'silence':>8} {'trace_mag':>10}")
743
+ for round_num in range(8):
744
+ mem.advance_iterative(50)
745
+ stats = mem.stats()
746
+ print(f" {stats['tick']:>6} {stats['n_present']:>8} "
747
+ f"{stats['n_lost']:>6} {stats['silence_ratio']:>8.3f} "
748
+ f"{stats['trace_magnitude']:>10.3f}")
749
+
750
+ print()
751
+ print(" the trace magnitude shrinks as items are lost.")
752
+ print(" nothing is added. the substrate grows quiet.")
753
+
754
+
755
+ # ============================================================
756
+ # DEMO 9. Loss log as narrative
757
+ # ============================================================
758
+ def demo_loss_log():
759
+ section("DEMO 9. LOSS LOG AS NARRATIVE")
760
+ mem = SilenceMemory(d=2048, loss_rate=0.03, seed=17)
761
+ names = ["first_love", "summer_of_98", "the_old_house",
762
+ "grandmothers_recipe", "broken_promise",
763
+ "childhood_friend", "favorite_song",
764
+ "last_conversation", "unfinished_book",
765
+ "forgotten_name"]
766
+ for nm in names:
767
+ mem.observe(nm)
768
+ mem.advance_iterative(80)
769
+
770
+ log = mem.loss_log()
771
+ print(f" {len(log)} losses over {mem._tick} ticks:")
772
+ print()
773
+ print(f" {'lost_at':>7} {'life':>5} {'label':>22}")
774
+ for entry in log:
775
+ print(f" {entry['lost_at']:>7} {entry['life_span']:>5} "
776
+ f"{entry['label']:>22}")
777
+
778
+
779
+ # ============================================================
780
+ # DEMO 10. What remains
781
+ # ============================================================
782
+ def demo_what_remains():
783
+ section("DEMO 10. WHAT REMAINS")
784
+ mem = SilenceMemory(d=2048, loss_rate=0.01, seed=19)
785
+ labels = [f"memory_{i}" for i in range(50)]
786
+ for lbl in labels:
787
+ mem.observe(lbl)
788
+ mem.advance_iterative(200)
789
+
790
+ present = sorted(mem.items.keys())
791
+ lost = sorted(mem.tombstones.keys())
792
+
793
+ print(f" after 200 ticks with loss_rate=0.01:")
794
+ print(f" {len(present)} present, {len(lost)} lost")
795
+ print()
796
+ print(f" what remains:")
797
+ for lbl in present:
798
+ r = mem.query(lbl)
799
+ print(f" {lbl:>12} weight={r['weight']:.3f}")
800
+ print()
801
+ print(f" what is gone (first 10):")
802
+ for lbl in lost[:10]:
803
+ r = mem.query(lbl)
804
+ print(f" {lbl:>12} lived {r['life_span']} ticks, "
805
+ f"lost at {r['lost_at']}")
806
+
807
+
808
+ # ============================================================
809
+ # Main
810
+ # ============================================================
811
+ def run_all_demos(output_dir: str = "holo_silence_out") -> None:
812
+ os.makedirs(output_dir, exist_ok=True)
813
+ print("=" * 78)
814
+ print("HOLO-SILENCE")
815
+ print("A memory substrate that loses things without reason.")
816
+ print("=" * 78)
817
+
818
+ demos = [
819
+ demo_basic_loss,
820
+ demo_discovery,
821
+ demo_tombstones,
822
+ demo_limits,
823
+ demo_age_independence,
824
+ demo_freeze,
825
+ demo_rebirth,
826
+ demo_long_horizon,
827
+ demo_loss_log,
828
+ demo_what_remains,
829
+ ]
830
+ for d in demos:
831
+ try:
832
+ d()
833
+ except Exception as e:
834
+ print(f" EXCEPTION in {d.__name__}: {e}")
835
+
836
+ section("OUTPUT")
837
+ mem = SilenceMemory(d=2048, loss_rate=0.02, seed=0)
838
+ for i in range(20):
839
+ mem.observe(f"item_{i:02d}", weight=0.5 + 0.05 * i)
840
+ mem.advance_iterative(60)
841
+ mem.save_json(os.path.join(output_dir, "state.json"))
842
+
843
+ print()
844
+ print("=" * 78)
845
+ print(f"All outputs saved to: {output_dir}/")
846
+ print("=" * 78)
847
+
848
+
849
+ def main() -> None:
850
+ parser = argparse.ArgumentParser(
851
+ description="A memory that loses things without reason.",
852
+ formatter_class=argparse.RawDescriptionHelpFormatter,
853
+ epilog="""
854
+ Library usage:
855
+
856
+ from holo_silence import SilenceMemory
857
+
858
+ mem = SilenceMemory(d=2048, loss_rate=0.01)
859
+ mem.observe("water_is_wet")
860
+ mem.observe("earth_orbits_sun")
861
+ mem.advance(200)
862
+
863
+ r = mem.query("water_is_wet")
864
+ # r["verdict"] is "PRESENT" or "GONE" or "UNKNOWN"
865
+
866
+ mem.tombstones_list() # what was lost
867
+ mem.loss_log() # when it was lost
868
+ mem.silence_ratio() # fraction gone
869
+ mem.acceptance() # full stats
870
+
871
+ # Freeze the substrate
872
+ mem.freeze()
873
+ mem.resume()
874
+ """)
875
+ parser.add_argument("--output", default="holo_silence_out",
876
+ help="output directory (default: holo_silence_out)")
877
+ args = parser.parse_args()
878
+
879
+ print("=" * 78)
880
+ print("SELF-TEST")
881
+ print("=" * 78)
882
+ if not self_test():
883
+ print(" primitive test failed; continuing anyway")
884
+
885
+ run_all_demos(output_dir=args.output)
886
+
887
+
888
+ if __name__ == "__main__":
889
+ main()