File size: 5,768 Bytes
ad474c9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | import unittest
from hdc.hypervec import hvc, sim
from hdc.memory_modes import (
HolographicMemory,
HybridMemory,
PhotographicMemory,
capacity_report,
compare_modes,
)
D = 2000
SYMBOLS = [hvc("test/symbol/%d" % i, D) for i in range(8)]
class TestPhotographic(unittest.TestCase):
def test_recall_is_bit_exact(self):
mem = PhotographicMemory(d=D)
value = hvc("pixel/42", D)
mem.capture("frame/42", value)
got, score = mem.recall("frame/42")
self.assertEqual(got, value & ((1 << D) - 1))
self.assertEqual(score, 1.0)
def test_miss_is_none(self):
mem = PhotographicMemory(d=D)
self.assertEqual(mem.recall("absent"), (None, 0.0))
def test_capacity_evicts_oldest(self):
mem = PhotographicMemory(d=D, capacity=2)
for i in range(3):
mem.capture("k/%d" % i, SYMBOLS[i])
self.assertEqual(len(mem), 2)
self.assertNotIn("k/0", mem)
self.assertIn("k/2", mem)
self.assertEqual(mem.stats()["evicted"], 1)
def test_overwrite_does_not_evict(self):
mem = PhotographicMemory(d=D, capacity=1)
mem.capture("k", SYMBOLS[0])
mem.capture("k", SYMBOLS[1])
self.assertEqual(mem.recall("k")[0], SYMBOLS[1])
self.assertEqual(mem.stats()["evicted"], 0)
class TestHolographic(unittest.TestCase):
def test_majority_superposition_recalls_exactly_at_small_k(self):
mem = HolographicMemory(d=D, alphabet=SYMBOLS)
for i in range(4):
mem.record("slot/%d" % i, SYMBOLS[i])
for i in range(4):
got, _ = mem.recall("slot/%d" % i)
self.assertEqual(got, SYMBOLS[i], msg="slot %d" % i)
def test_xor_accumulation_is_the_wrong_rule(self):
# Regression: XOR crosstalk is itself random at every bit, so unbinding
# is chance at any k. Majority is what makes recall work at all.
# Measured: XOR recalls 0/2, 0/4, 2/8 (chance); majority 8/8.
from hdc.hypervec import bind
for k in (2, 4, 8):
trace = 0
for i in range(k):
trace ^= bind(hvc("xor/%d" % i, D), SYMBOLS[i], D)
xor_hits = 0
for i in range(k):
probe = bind(trace, hvc("xor/%d" % i, D), D)
best = max(SYMBOLS, key=lambda s: sim(probe, s, D))
xor_hits += best == SYMBOLS[i]
self.assertLessEqual(xor_hits, k // 2, msg="k=%d" % k)
majority = HolographicMemory(d=D, alphabet=SYMBOLS)
for i in range(k):
majority.record("xor/%d" % i, SYMBOLS[i])
self.assertEqual(
sum(1 for i in range(k) if majority.recall("xor/%d" % i)[0] == SYMBOLS[i]),
k,
msg="k=%d" % k,
)
def test_confidence_falls_as_the_trace_fills(self):
mem = HolographicMemory(d=D, alphabet=SYMBOLS)
mem.record("slot/0", SYMBOLS[0])
alone = mem.confidence("slot/0")
for i in range(1, 6):
mem.record("slot/%d" % i, SYMBOLS[i])
crowded = mem.confidence("slot/0")
self.assertLess(crowded, alone)
self.assertGreater(alone, 0.9)
def test_empty_memory_returns_nothing(self):
mem = HolographicMemory(d=D, alphabet=SYMBOLS)
self.assertEqual(mem.recall("anything"), (None, 0.0))
self.assertEqual(mem.confidence("anything"), 0.0)
def test_trace_is_bounded_by_dimension(self):
mem = HolographicMemory(d=D, alphabet=SYMBOLS)
for i in range(8):
mem.record("slot/%d" % i, SYMBOLS[i])
self.assertLess(mem.trace.bit_length(), D + 1)
self.assertEqual(mem.stats()["items"], 8)
def test_capacity_report_is_monotone_in_similarity(self):
rows = capacity_report(D, sizes=(2, 8, 16), symbols=8)
self.assertEqual([r["items"] for r in rows], [2, 8, 16])
for earlier, later in zip(rows, rows[1:]):
self.assertLessEqual(later["mean_similarity"], earlier["mean_similarity"])
class TestHybrid(unittest.TestCase):
def test_exact_path_wins_when_present(self):
mem = HybridMemory(d=D, buckets=4, alphabet=SYMBOLS)
mem.capture("key/1", SYMBOLS[1])
value, score, path = mem.recall("key/1")
self.assertEqual((value, score, path), (SYMBOLS[1], 1.0, "exact"))
def test_trace_path_answers_after_eviction(self):
mem = HybridMemory(d=D, buckets=1, per_bucket=1, alphabet=SYMBOLS)
mem.capture("first", SYMBOLS[0])
mem.capture("second", SYMBOLS[1]) # evicts "first" from the exact index
self.assertNotIn("first", mem.exact[0])
value, score, path = mem.recall("first")
self.assertEqual(path, "trace")
self.assertIsNotNone(value)
def test_buckets_are_disjoint(self):
mem = HybridMemory(d=D, buckets=8, alphabet=SYMBOLS)
mem.capture("key/1", SYMBOLS[1])
filled = [i for i, cell in enumerate(mem.exact) if cell]
self.assertEqual(len(filled), 1)
def test_consolidate_matches_a_fresh_trace(self):
mem = HybridMemory(d=D, buckets=2, alphabet=SYMBOLS)
for i in range(4):
mem.capture("k/%d" % i, SYMBOLS[i])
self.assertEqual(mem.consolidate(), 0)
for i in range(4):
self.assertEqual(mem.recall("k/%d" % i)[0], SYMBOLS[i])
def test_compare_modes_photographic_is_perfect(self):
result = compare_modes(D, k=8, symbols=8)
self.assertEqual(result["photographic"], 1.0)
self.assertEqual(result["items"], 8)
def test_zero_buckets_rejected(self):
with self.assertRaises(ValueError):
HybridMemory(d=D, buckets=0)
if __name__ == "__main__":
unittest.main()
|