File size: 5,878 Bytes
d004f9e | 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 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | """Tests for hypervector primitives (``palimseste.hv``).
These encode the algebraic invariants the whole substrate relies on:
- bind is self-inverse (reversible / addressable)
- similarity of random HVs concentrates near 0 (quasi-orthogonality)
- bundle is majority vote with random tie-breaks
- packed-bit representation round-trips correctly
"""
from __future__ import annotations
import numpy as np
import pytest
from palimseste import hv
# ----------------------------------------------------------------- repr/shape
def test_HV_rejects_bad_length():
with pytest.raises(ValueError):
hv.HV(bits=np.zeros(5, dtype=np.uint8), D=100) # 5 != ceil(100/8)=13
def test_HV_rejects_bad_dtype():
with pytest.raises(TypeError):
hv.HV(bits=np.zeros(13, dtype=np.int32), D=100)
def test_HV_rejects_2d():
with pytest.raises(ValueError):
hv.HV(bits=np.zeros((2, 6), dtype=np.uint8), D=96)
# ------------------------------------------------------------ construction
def test_random_hv_is_bipolar():
rng = np.random.default_rng(0)
x = hv.random_hv(D=1000, rng=rng)
s = hv.bits_to_signs(x)
assert set(np.unique(s).tolist()) <= {1, -1}
assert x.D == 1000
def test_constant_hv():
p = hv.constant_hv(D=64, value=+1)
m = hv.constant_hv(D=64, value=-1)
assert hv.bits_to_signs(p).tolist() == [1] * 64
assert hv.bits_to_signs(m).tolist() == [-1] * 64
with pytest.raises(ValueError):
hv.constant_hv(D=64, value=0)
def test_bits_signs_roundtrip():
rng = np.random.default_rng(1)
x = hv.random_hv(D=500, rng=rng)
signs = hv.bits_to_signs(x)
x2 = hv.signs_to_bits(signs)
assert x == x2
# ------------------------------------------------------------------ binding
def test_bind_xor_semantics():
# bind(a,b) XOR semantics: bind(a, bind(a, b)) == b
rng = np.random.default_rng(2)
a = hv.random_hv(D=256, rng=rng)
b = hv.random_hv(D=256, rng=rng)
assert hv.bind(a, hv.bind(a, b)) == b
def test_unbind_equals_bind():
rng = np.random.default_rng(3)
a, b = hv.random_hv(D=64, rng=rng), hv.random_hv(D=64, rng=rng)
assert hv.unbind(hv.bind(a, b), a) == hv.bind(hv.bind(a, b), a)
def test_bind_self_inverse_recovers_value():
rng = np.random.default_rng(4)
key = hv.random_hv(D=2000, rng=rng)
value = hv.random_hv(D=2000, rng=rng)
pair = hv.bind(key, value)
recovered = hv.unbind(pair, key)
# recovered HV should be *very* close to value (exact, since XOR is lossless)
assert recovered == value
def test_bind_dimension_mismatch():
a = hv.random_hv(D=100)
b = hv.random_hv(D=200)
with pytest.raises(ValueError):
hv.bind(a, b)
# ----------------------------------------------------------------- bundling
def test_bundle_majority():
# Three identical -> same; two vs one -> majority
rng = np.random.default_rng(5)
a = hv.random_hv(D=300, rng=rng)
b = hv.random_hv(D=300, rng=rng)
c = hv.random_hv(D=300, rng=rng)
# a ⊕ a ⊕ b == a (a wins the majority)
out = hv.bundle([a, a, b], rng=rng)
assert out == a
# a ⊕ a ⊕ a == a
assert hv.bundle([a, a, a], rng=rng) == a
def test_bundle_with_weights():
rng = np.random.default_rng(6)
a = hv.random_hv(D=300, rng=rng)
b = hv.random_hv(D=300, rng=rng)
# weight b by 2 -> b wins
out = hv.bundle([a, b], weights=[1.0, 2.0], rng=rng)
assert out == b
def test_bundle_tie_random_break():
rng = np.random.default_rng(7)
a = hv.random_hv(D=400, rng=rng)
# a ⊕ a_negated -> all ties -> output is random, but still a valid HV
a_neg = hv.signs_to_bits(-hv.bits_to_signs(a))
out = hv.bundle([a, a_neg], rng=rng)
s = hv.bits_to_signs(out)
assert set(np.unique(s).tolist()) <= {1, -1}
def test_bundle_empty_raises():
with pytest.raises(ValueError):
hv.bundle([])
def test_bundle_negative_weights_raise():
a = hv.random_hv(D=10)
b = hv.random_hv(D=10)
with pytest.raises(ValueError):
hv.bundle([a, b], weights=[-1.0, 1.0])
def test_bundle_dimension_mismatch():
a = hv.random_hv(D=10)
b = hv.random_hv(D=20)
with pytest.raises(ValueError):
hv.bundle([a, b])
# ----------------------------------------------------------- similarity
def test_similarity_self_is_one():
x = hv.random_hv(D=1000)
assert hv.similarity(x, x) == pytest.approx(1.0)
def test_similarity_opposite_is_minus_one():
x = hv.random_hv(D=1000)
neg = hv.signs_to_bits(-hv.bits_to_signs(x))
assert hv.similarity(x, neg) == pytest.approx(-1.0)
def test_random_hvs_quasi_orthogonal():
# Concentration of measure: with D large, two random HVs have
# similarity concentrated around 0 with small std.
rng = np.random.default_rng(8)
sims = [hv.similarity(hv.random_hv(D=5000, rng=rng),
hv.random_hv(D=5000, rng=rng))
for _ in range(200)]
mean = float(np.mean(sims))
std = float(np.std(sims))
# mean near 0, std ~ 1/sqrt(D)
assert abs(mean) < 0.02
assert std < 1 / np.sqrt(5000) * 5 # generous bound
def test_hamming_basic():
rng = np.random.default_rng(9)
a = hv.random_hv(D=100, rng=rng)
assert hv.hamming(a, a) == 0
b = hv.random_hv(D=100, rng=rng)
h = hv.hamming(a, b)
assert 0 <= h <= 100
assert h == hv.hamming(b, a)
def test_similarity_range():
rng = np.random.default_rng(10)
a = hv.random_hv(D=1000, rng=rng)
b = hv.random_hv(D=1000, rng=rng)
s = hv.similarity(a, b)
assert -1.0 - 1e-9 <= s <= 1.0 + 1e-9
# ----------------------------------------------------------- hash / eq
def test_hash_equality_stable():
rng = np.random.default_rng(11)
a = hv.random_hv(D=100, rng=rng)
a_copy = hv.HV(bits=a.bits.copy(), D=a.D)
assert a == a_copy
assert hash(a) == hash(a_copy)
|