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)