Download tests/test_learner.py from thefinalboss/palimpseste-max: direct link, hf CLI and curl.
- Browser
- Download file 6.55 kB
-
https://huggingface.co/thefinalboss/palimpseste-max/resolve/main/tests/test_learner.py
- Command line
-
hf download hf://thefinalboss/palimpseste-max/tests/test_learner.py
-
curl -L -o test_learner.py https://huggingface.co/thefinalboss/palimpseste-max/resolve/main/tests/test_learner.py
6.55 kB
| """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 | |
| # ----------------------------------------------------------------- learner | |
| 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(): | |
| # same x, different ctx -> different addresses, different predictions | |
| 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(): | |
| # The write cost must not grow with |M|. We compare per-write time at | |
| # small |M| vs large |M| and require they are within a generous factor. | |
| 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 | |
| # O(1) amortized: large-|M| writes should be within 5x of small-|M| writes | |
| # (generous to absorb LSH dict growth / GC noise). | |
| 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 | |
| # ----------------------------------------------------------------- encoder | |
| 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(): | |
| # adjacent floats are similar, distant are not. | |
| # With 256 levels and ~0.025*D bits/step, the random-walk level code | |
| # saturates to quasi-orthogonal within ~30 levels, so values far apart | |
| # in [0,1] decorrelate as VSA scalar coding requires. | |
| 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 | |
| # 0.41 vs 0.90 are ~126 levels apart -> well past saturation | |
| assert hv.similarity(a, c) < 0.3 | |
| def test_encode_float_clamps(): | |
| e = _enc() | |
| # out-of-range clamps, no error | |
| 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]) | |
| # order matters: different permutations are dissimilar | |
| 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]) | |
| # same set, different order -> similar (not necessarily equal due to tie-breaks, | |
| # but strongly correlated) | |
| assert hv.similarity(s1, s2) > 0.7 | |
| def test_encode_kv(): | |
| # Bundling is commutative ONLY when there are no ties. With an even | |
| # number of independent bound-pairs every coordinate is an exact tie and | |
| # the result degenerates to random tie-breaking (sim ~ 0 between orderings). | |
| # With an odd count (>=3) ties are broken by genuine majority and the | |
| # bundle is order-insensitive. | |
| 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 | |