| """Tests for BPE tokenizer, HV attention, and abstraction engine.""" |
|
|
| from __future__ import annotations |
|
|
| import pytest |
| import numpy as np |
|
|
| from palimseste import hv |
| from palimseste.learner import Encoder |
| from palimseste.bpe import BPETokenizer |
| from palimseste.attention import HVAttention, AttentionConfig |
| from palimseste.abstraction import AbstractionEngine, AbstractionConfig |
| from palimseste.memory import Memory |
| from palimseste.lm import PalimpsesteForCausalLM, PalimpsesteConfig |
|
|
|
|
| |
| class TestBPE: |
| def _tok(self, D=2000): |
| enc = Encoder(D=D, rng=np.random.default_rng(0)) |
| return BPETokenizer(encoder=enc, vocab_size=1000) |
|
|
| def test_train_and_encode(self): |
| tok = self._tok() |
| text = "hello world hello world hello world hello world" |
| tok.train(text, target_vocab_size=300) |
| ids = tok.encode("hello world") |
| assert len(ids) > 0 |
| assert len(ids) < len("hello world") |
|
|
| def test_encode_decode_roundtrip(self): |
| tok = self._tok() |
| text = "the quick brown fox jumps over the lazy dog" |
| tok.train(text * 10, target_vocab_size=500) |
| ids = tok.encode(text) |
| decoded = tok.decode(ids) |
| assert decoded == text |
|
|
| def test_bpe_reduces_sequence_length(self): |
| """BPE should produce fewer tokens than char-level.""" |
| tok = self._tok() |
| text = "hello world " * 50 |
| tok.train(text, target_vocab_size=2000) |
| bpe_ids = tok.encode("hello world hello world") |
| char_count = len("hello world hello world") |
| assert len(bpe_ids) < char_count, f"BPE {len(bpe_ids)} should be < chars {char_count}" |
|
|
| def test_bos_eos(self): |
| tok = self._tok() |
| tok.train("abc abc abc", target_vocab_size=300) |
| ids = tok.encode("abc", add_bos=True, add_eos=True) |
| assert ids[0] == 1 |
| assert ids[-1] == 2 |
|
|
| def test_token_hv_stable(self): |
| tok = self._tok() |
| tok.train("hello hello hello", target_vocab_size=300) |
| h1 = tok.token_hv(4) |
| h2 = tok.token_hv(4) |
| assert h1 == h2 |
|
|
| def test_save_load(self, tmp_path): |
| tok = self._tok() |
| tok.train("test test test " * 20, target_vocab_size=300) |
| ids_before = tok.encode("test test") |
| tok.save_vocabulary(tmp_path / "bpe.json") |
| enc = Encoder(D=2000, rng=np.random.default_rng(0)) |
| tok2 = BPETokenizer.load_vocabulary(tmp_path / "bpe.json", encoder=enc) |
| ids_after = tok2.encode("test test") |
| assert ids_before == ids_after |
|
|
| def test_encode_context(self): |
| tok = self._tok(D=2000) |
| tok.train("hello world test " * 20, target_vocab_size=500) |
| ids = tok.encode("hello world test") |
| ctx_hv = tok.encode_context(ids, window=32) |
| assert isinstance(ctx_hv, hv.HV) |
| assert ctx_hv.D == 2000 |
|
|
|
|
| |
| class TestHVAttention: |
| def _enc(self, D=2000): |
| return Encoder(D=D, rng=np.random.default_rng(0)) |
|
|
| def test_attend_returns_hv(self): |
| enc = self._enc() |
| att = HVAttention(config=AttentionConfig(max_context=64, top_k=8), |
| encoder=enc) |
| rng = np.random.default_rng(1) |
| tokens = [hv.random_hv(2000, rng=rng) for _ in range(20)] |
| query = hv.random_hv(2000, rng=rng) |
| result = att.attend(tokens, query) |
| assert isinstance(result, hv.HV) |
| assert result.D == 2000 |
|
|
| def test_attention_is_selective(self): |
| """The attended HV should be more similar to the query-matching |
| tokens than to a random HV.""" |
| enc = self._enc(D=5000) |
| att = HVAttention(config=AttentionConfig(max_context=64, top_k=4, temperature=2.0), |
| encoder=enc) |
| rng = np.random.default_rng(2) |
| |
| query = hv.random_hv(5000, rng=rng) |
| near_token = query |
| far_tokens = [hv.random_hv(5000, rng=rng) for _ in range(20)] |
| tokens = far_tokens[:10] + [near_token] + far_tokens[10:] |
| result = att.attend(tokens, query) |
| |
| random_hv = hv.random_hv(5000, rng=rng) |
| assert hv.similarity(result, query) > hv.similarity(random_hv, query) |
|
|
| def test_top_k_limits_selection(self): |
| enc = self._enc() |
| att = HVAttention(config=AttentionConfig(max_context=32, top_k=4), |
| encoder=enc) |
| rng = np.random.default_rng(3) |
| tokens = [hv.random_hv(2000, rng=rng) for _ in range(32)] |
| query = hv.random_hv(2000, rng=rng) |
| result = att.attend(tokens, query) |
| assert isinstance(result, hv.HV) |
|
|
| def test_empty_context(self): |
| enc = self._enc() |
| att = HVAttention(config=AttentionConfig(), encoder=enc) |
| query = hv.random_hv(2000, rng=np.random.default_rng(0)) |
| result = att.attend([], query) |
| assert result == query |
|
|
| def test_max_context_truncation(self): |
| enc = self._enc() |
| att = HVAttention(config=AttentionConfig(max_context=10, top_k=5), |
| encoder=enc) |
| rng = np.random.default_rng(4) |
| tokens = [hv.random_hv(2000, rng=rng) for _ in range(50)] |
| query = hv.random_hv(2000, rng=rng) |
| |
| result = att.attend(tokens, query) |
| assert isinstance(result, hv.HV) |
|
|
| def test_config_validation(self): |
| with pytest.raises(ValueError): |
| AttentionConfig(max_context=0) |
| with pytest.raises(ValueError): |
| AttentionConfig(top_k=0) |
| with pytest.raises(ValueError): |
| AttentionConfig(top_k=100, max_context=50) |
|
|
|
|
| |
| class TestAbstraction: |
| def _mem(self, D=2000): |
| return Memory(D=D, rng=np.random.default_rng(0)) |
|
|
| def test_extract_concepts_from_similar_traces(self): |
| """Traces with similar addresses should form a cluster.""" |
| mem = self._mem(D=3000) |
| rng = np.random.default_rng(1) |
| base = hv.random_hv(3000, rng=rng) |
|
|
| |
| for i in range(10): |
| signs = hv.bits_to_signs(base) |
| |
| flip = rng.choice(3000, size=20, replace=False) |
| signs[flip] = -signs[flip] |
| addr = hv.signs_to_bits(signs) |
| val = hv.random_hv(3000, rng=rng) |
| mem.write(addr, val, tag=f"item_{i}") |
|
|
| engine = AbstractionEngine( |
| mem=mem, |
| config=AbstractionConfig( |
| n_clusters=10, |
| min_cluster_size=3, |
| similarity_threshold=0.2, |
| sample_size=100, |
| ), |
| rng=rng, |
| ) |
| concepts = engine.extract_concepts() |
| assert len(concepts) > 0 |
| assert concepts[0].n_members >= 3 |
|
|
| def test_no_concepts_from_dissimilar_traces(self): |
| mem = self._mem(D=3000) |
| rng = np.random.default_rng(2) |
| for _ in range(20): |
| mem.write(hv.random_hv(3000, rng=rng), hv.random_hv(3000, rng=rng)) |
|
|
| engine = AbstractionEngine( |
| mem=mem, |
| config=AbstractionConfig( |
| similarity_threshold=0.9, |
| min_cluster_size=3, |
| sample_size=100, |
| ), |
| rng=rng, |
| ) |
| concepts = engine.extract_concepts() |
| assert len(concepts) == 0 |
|
|
| def test_find_concept(self): |
| mem = self._mem(D=3000) |
| rng = np.random.default_rng(3) |
| base = hv.random_hv(3000, rng=rng) |
| for i in range(5): |
| signs = hv.bits_to_signs(base) |
| flip = rng.choice(3000, size=10, replace=False) |
| signs[flip] = -signs[flip] |
| mem.write(hv.signs_to_bits(signs), hv.random_hv(3000, rng=rng)) |
|
|
| engine = AbstractionEngine( |
| mem=mem, |
| config=AbstractionConfig(similarity_threshold=0.3, min_cluster_size=2), |
| rng=rng, |
| ) |
| engine.extract_concepts() |
| |
| concept = engine.find_concept(base) |
| assert concept is not None |
|
|
| def test_find_concept_returns_none_for_dissimilar(self): |
| mem = self._mem(D=2000) |
| rng = np.random.default_rng(4) |
| for _ in range(5): |
| mem.write(hv.random_hv(2000, rng=rng), hv.random_hv(2000, rng=rng)) |
| engine = AbstractionEngine(mem=mem, rng=rng) |
| engine.extract_concepts() |
| result = engine.find_concept(hv.random_hv(2000, rng=rng)) |
| |
| assert result is None or isinstance(result, object) |
|
|
| def test_concepts_stored_in_memory(self): |
| """Extracted concept centroids should be written into M.""" |
| mem = self._mem(D=3000) |
| rng = np.random.default_rng(5) |
| base = hv.random_hv(3000, rng=rng) |
| for i in range(10): |
| signs = hv.bits_to_signs(base) |
| flip = rng.choice(3000, size=15, replace=False) |
| signs[flip] = -signs[flip] |
| mem.write(hv.signs_to_bits(signs), hv.random_hv(3000, rng=rng)) |
|
|
| n_before = len(mem) |
| engine = AbstractionEngine( |
| mem=mem, |
| config=AbstractionConfig(similarity_threshold=0.2, min_cluster_size=3), |
| rng=rng, |
| ) |
| engine.extract_concepts() |
| assert len(mem) > n_before |
|
|
| def test_empty_memory(self): |
| mem = self._mem(D=2000) |
| engine = AbstractionEngine(mem=mem, rng=np.random.default_rng(0)) |
| concepts = engine.extract_concepts() |
| assert concepts == [] |
|
|
| def test_config_validation(self): |
| with pytest.raises(ValueError): |
| AbstractionConfig(n_clusters=0) |
| with pytest.raises(ValueError): |
| AbstractionConfig(min_cluster_size=1) |
| with pytest.raises(ValueError): |
| AbstractionConfig(similarity_threshold=0) |
| with pytest.raises(ValueError): |
| AbstractionConfig(similarity_threshold=1.5) |
|
|