thefinalboss commited on
Commit
dff20cc
·
verified ·
1 Parent(s): 7156b42

Upload tests/test_lsh.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. tests/test_lsh.py +137 -0
tests/test_lsh.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the Hamming LSH index (``palimseste.lsh``).
2
+
3
+ Key properties to verify:
4
+ - exact neighbors are always retrievable within radius (recall on self)
5
+ - near-duplicates collide far more often than random pairs (selectivity)
6
+ - query_candidates returns a small fraction of |M| (sub-linear scaling)
7
+ - rebuild is consistent with incremental insert
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import numpy as np
13
+ import pytest
14
+
15
+ from palimseste import hv, lsh
16
+
17
+
18
+ def _flip_bits(x: hv.HV, n: int, rng) -> hv.HV:
19
+ """Return a copy of x with n random bit positions flipped."""
20
+ signs = hv.bits_to_signs(x)
21
+ pos = rng.choice(x.D, size=n, replace=False)
22
+ signs[pos] = -signs[pos]
23
+ return hv.signs_to_bits(signs)
24
+
25
+
26
+ def test_insert_and_size():
27
+ cfg = lsh.LSHConfig(D=500, K=8, L=4)
28
+ idx = lsh.LSHIndex(config=cfg)
29
+ rng = np.random.default_rng(0)
30
+ for _ in range(10):
31
+ idx.insert(idx.size, hv.random_hv(D=500, rng=rng))
32
+ assert idx.size == 10
33
+
34
+
35
+ def test_self_query_returns_self():
36
+ cfg = lsh.LSHConfig(D=1000, K=6, L=6)
37
+ idx = lsh.LSHIndex(config=cfg)
38
+ rng = np.random.default_rng(1)
39
+ hvs = [hv.random_hv(D=1000, rng=rng) for _ in range(50)]
40
+ for i, h in enumerate(hvs):
41
+ idx.insert(i, h)
42
+ # query with one of the stored HVs at radius 0 -> should find itself
43
+ for i in (0, 17, 49):
44
+ nbs = idx.query_neighbors(hvs[i], radius=0, items=hvs)
45
+ assert i in nbs
46
+
47
+
48
+ def test_near_neighbor_found():
49
+ cfg = lsh.LSHConfig(D=2000, K=10, L=12)
50
+ idx = lsh.LSHIndex(config=cfg)
51
+ rng = np.random.default_rng(2)
52
+ base = hv.random_hv(D=2000, rng=rng)
53
+ # near neighbor: 5 bits different
54
+ near = _flip_bits(base, 5, rng)
55
+ # fill with unrelated random HVs
56
+ hvs = [base] + [hv.random_hv(D=2000, rng=rng) for _ in range(200)]
57
+ hvs.append(near)
58
+ near_id = len(hvs) - 1
59
+ for i, h in enumerate(hvs):
60
+ idx.insert(i, h)
61
+ found = idx.query_neighbors(near, radius=10, items=hvs)
62
+ # both base and near should be within radius 10 of `near`
63
+ assert 0 in found
64
+ assert near_id in found
65
+
66
+
67
+ def test_candidate_set_is_small():
68
+ # With reasonable (K,L), candidates << |M|
69
+ cfg = lsh.LSHConfig(D=2000, K=14, L=8)
70
+ idx = lsh.LSHIndex(config=cfg)
71
+ rng = np.random.default_rng(3)
72
+ N = 1000
73
+ hvs = [hv.random_hv(D=2000, rng=rng) for _ in range(N)]
74
+ for i, h in enumerate(hvs):
75
+ idx.insert(i, h)
76
+ q = hv.random_hv(D=2000, rng=rng)
77
+ cand = idx.query_candidates(q)
78
+ # should be well under N
79
+ assert len(cand) < N * 0.5
80
+
81
+
82
+ def test_rebuild_matches_insert():
83
+ # Two indices built with identical rng seeds must have identical
84
+ # projections, so rebuild() and incremental insert() produce the
85
+ # same bucket membership.
86
+ cfg = lsh.LSHConfig(D=500, K=6, L=4)
87
+ rng = np.random.default_rng(4)
88
+ hvs = [hv.random_hv(D=500, rng=rng) for _ in range(30)]
89
+
90
+ idx = lsh.LSHIndex(config=cfg, _rng=np.random.default_rng(123))
91
+ for i, h in enumerate(hvs):
92
+ idx.insert(i, h)
93
+ cand_before = idx.query_candidates(hvs[5])
94
+
95
+ idx2 = lsh.LSHIndex(config=cfg, _rng=np.random.default_rng(123))
96
+ idx2.rebuild(hvs)
97
+ cand_after = idx2.query_candidates(hvs[5])
98
+ assert cand_before == cand_after
99
+
100
+
101
+ def test_rebuild_clears_old_entries():
102
+ cfg = lsh.LSHConfig(D=500, K=6, L=4)
103
+ idx = lsh.LSHIndex(config=cfg, _rng=np.random.default_rng(7))
104
+ rng = np.random.default_rng(0)
105
+ # insert some, then rebuild with a smaller set -> size must reflect new set
106
+ for _ in range(50):
107
+ idx.insert(idx.size, hv.random_hv(D=500, rng=rng))
108
+ new_hvs = [hv.random_hv(D=500, rng=rng) for _ in range(5)]
109
+ idx.rebuild(new_hvs)
110
+ assert idx.size == 5
111
+
112
+
113
+ def test_tune_config_valid():
114
+ cfg = lsh.LSHConfig.tune(D=10000, target_radius=0.1, recall=0.9)
115
+ assert 1 <= cfg.K <= cfg.D
116
+ assert cfg.L >= 1
117
+ # larger recall -> at least as many tables
118
+ cfg_hi = lsh.LSHConfig.tune(D=10000, target_radius=0.1, recall=0.99)
119
+ assert cfg_hi.L >= cfg.L
120
+
121
+
122
+ def test_dimension_mismatch_raises():
123
+ cfg = lsh.LSHConfig(D=100)
124
+ idx = lsh.LSHIndex(config=cfg)
125
+ with pytest.raises(ValueError):
126
+ idx.insert(0, hv.random_hv(D=200))
127
+
128
+
129
+ def test_invalid_config():
130
+ with pytest.raises(ValueError):
131
+ lsh.LSHConfig(D=0)
132
+ with pytest.raises(ValueError):
133
+ lsh.LSHConfig(D=100, K=0)
134
+ with pytest.raises(ValueError):
135
+ lsh.LSHConfig(D=100, L=0)
136
+ with pytest.raises(ValueError):
137
+ lsh.LSHConfig(D=100, K=101)