| """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 |
|
|
|
|
| |
| def test_HV_rejects_bad_length(): |
| with pytest.raises(ValueError): |
| hv.HV(bits=np.zeros(5, dtype=np.uint8), D=100) |
|
|
|
|
| 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) |
|
|
|
|
| |
| 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 |
|
|
|
|
| |
| def test_bind_xor_semantics(): |
| |
| 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) |
| |
| 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) |
|
|
|
|
| |
| def test_bundle_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) |
| |
| out = hv.bundle([a, a, b], rng=rng) |
| assert out == 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) |
| |
| 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_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]) |
|
|
|
|
| |
| 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(): |
| |
| |
| 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)) |
| |
| assert abs(mean) < 0.02 |
| assert std < 1 / np.sqrt(5000) * 5 |
|
|
|
|
| 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 |
|
|
|
|
| |
| 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) |
|
|