| """Tests for the Learner + Encoder (``palimseste.learner``). |
| |
| Verifies: |
| - learn() is O(1): the millionth example costs the same as the first |
| (we measure write cost stays flat across |M| scales) |
| - predict() recovers learned mappings |
| - context binding: same x in different ctx yields different addresses |
| - Encoder: ints/strs/floats/sequences/sets encode deterministically & |
| with the right similarity structure (floats level-coded, sequences |
| order-sensitive, sets order-insensitive) |
| """ |
|
|
| from __future__ import annotations |
|
|
| import time |
| import numpy as np |
| import pytest |
|
|
| from palimseste import hv |
| from palimseste.memory import Memory |
| from palimseste.phi import Phi, KernelConfig |
| from palimseste.learner import Learner, Encoder, Prediction |
|
|
|
|
| def _learner(D=2000, seed=0, radius=0) -> tuple[Learner, np.random.Generator]: |
| rng = np.random.default_rng(seed) |
| mem = Memory(D=D, rng=np.random.default_rng(seed)) |
| phi = Phi(config=KernelConfig(radius=radius, min_weight=1e-6)) |
| return Learner(mem=mem, phi=phi, rng=rng), rng |
|
|
|
|
| |
| def test_learn_then_predict_exact(): |
| lr, rng = _learner(D=2000, radius=0) |
| x = hv.random_hv(D=2000, rng=rng) |
| y = hv.random_hv(D=2000, rng=rng) |
| lr.learn(x, y) |
| p = lr.predict(x) |
| assert p.value == y |
| assert p.n_matches == 1 |
| assert p.confidence > 0.99 |
|
|
|
|
| def test_learn_context_binding(): |
| |
| lr, rng = _learner(D=2000, radius=0) |
| x = hv.random_hv(D=2000, rng=rng) |
| ctx_a = hv.random_hv(D=2000, rng=rng) |
| ctx_b = hv.random_hv(D=2000, rng=rng) |
| y_a = hv.random_hv(D=2000, rng=rng) |
| y_b = hv.random_hv(D=2000, rng=rng) |
| lr.learn(x, y_a, ctx=ctx_a) |
| lr.learn(x, y_b, ctx=ctx_b) |
| pa = lr.predict(x, ctx=ctx_a) |
| pb = lr.predict(x, ctx=ctx_b) |
| assert pa.value == y_a |
| assert pb.value == y_b |
|
|
|
|
| def test_learn_is_o1_flat_cost(): |
| |
| |
| D = 4000 |
| rng = np.random.default_rng(7) |
| mem = Memory(D=D, rng=np.random.default_rng(7)) |
| phi = Phi(config=KernelConfig(radius=10)) |
| lr = Learner(mem=mem, phi=phi, rng=rng) |
|
|
| xs = [hv.random_hv(D=D, rng=rng) for _ in range(2000)] |
| ys = [hv.random_hv(D=D, rng=rng) for _ in range(2000)] |
|
|
| t0 = time.perf_counter() |
| for i in range(50): |
| lr.learn(xs[i], ys[i]) |
| small = (time.perf_counter() - t0) / 50 |
|
|
| for i in range(50, 2000): |
| lr.learn(xs[i], ys[i]) |
|
|
| t1 = time.perf_counter() |
| for i in range(2000, 2050): |
| lr.learn(xs[i % len(xs)], ys[i % len(ys)]) |
| large = (time.perf_counter() - t1) / 50 |
|
|
| |
| |
| ratio = large / max(small, 1e-9) |
| assert ratio < 5.0, f"write cost grew: small={small:.2e} large={large:.2e} ratio={ratio:.2f}" |
|
|
|
|
| def test_learn_many_appends_all(): |
| lr, rng = _learner(D=1000, radius=5) |
| pairs = [(hv.random_hv(D=1000, rng=rng), hv.random_hv(D=1000, rng=rng)) for _ in range(30)] |
| traces = lr.learn_many(pairs) |
| assert len(traces) == 30 |
| assert len(lr.mem) == 30 |
|
|
|
|
| def test_predict_empty_returns_none(): |
| lr, _ = _learner(D=500) |
| p = lr.predict(hv.random_hv(D=500)) |
| assert p.value is None |
| assert p.confidence == 0.0 |
| assert p.n_matches == 0 |
|
|
|
|
| |
| def _enc(D=2000, seed=3) -> Encoder: |
| return Encoder(D=D, rng=np.random.default_rng(seed)) |
|
|
|
|
| def test_encode_int_deterministic(): |
| e = _enc() |
| assert e.encode_int(5) == e.encode_int(5) |
| assert e.encode_int(5) != e.encode_int(6) |
|
|
|
|
| def test_encode_str_deterministic(): |
| e = _enc() |
| assert e.encode_str("cat") == e.encode_str("cat") |
| assert e.encode_str("cat") != e.encode_str("dog") |
|
|
|
|
| def test_encode_float_level_similarity(): |
| |
| |
| |
| |
| e = _enc(D=5000, seed=1) |
| a = e.encode_float(0.41) |
| b = e.encode_float(0.42) |
| c = e.encode_float(0.90) |
| assert hv.similarity(a, b) > 0.5 |
| |
| assert hv.similarity(a, c) < 0.3 |
|
|
|
|
| def test_encode_float_clamps(): |
| e = _enc() |
| |
| assert isinstance(e.encode_float(-5.0), hv.HV) |
| assert isinstance(e.encode_float(99.0), hv.HV) |
|
|
|
|
| def test_encode_float_bad_range(): |
| e = _enc() |
| with pytest.raises(ValueError): |
| e.encode_float(0.5, lo=1.0, hi=0.0) |
|
|
|
|
| def test_encode_sequence_order_sensitive(): |
| e = _enc(D=5000, seed=2) |
| a = e.encode_int(1) |
| b = e.encode_int(2) |
| c = e.encode_int(3) |
| seq_abc = e.encode_sequence([a, b, c]) |
| seq_cba = e.encode_sequence([c, b, a]) |
| |
| assert hv.similarity(seq_abc, seq_cba) < 0.6 |
|
|
|
|
| def test_encode_set_order_insensitive(): |
| e = _enc(D=5000, seed=3) |
| a = e.encode_int(1) |
| b = e.encode_int(2) |
| c = e.encode_int(3) |
| s1 = e.encode_set([a, b, c]) |
| s2 = e.encode_set([c, b, a]) |
| |
| |
| assert hv.similarity(s1, s2) > 0.7 |
|
|
|
|
| def test_encode_kv(): |
| |
| |
| |
| |
| |
| e = _enc(D=5000, seed=4) |
| pairs = [ |
| (e.encode_str("color"), e.encode_str("red")), |
| (e.encode_str("size"), e.encode_int(42)), |
| (e.encode_str("weight"), e.encode_int(7)), |
| (e.encode_str("shape"), e.encode_str("cube")), |
| (e.encode_str("owner"), e.encode_str("ada")), |
| ] |
| rec = e.encode_kv(pairs) |
| rec2 = e.encode_kv(list(reversed(pairs))) |
| assert hv.similarity(rec, rec2) > 0.7 |
|
|
|
|
| def test_prediction_dataclass(): |
| p = Prediction(query=hv.random_hv(10), value=None, confidence=0.0, n_matches=0) |
| assert p.value is None |
| assert p.confidence == 0.0 |
|
|