protlabel 4.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,53 @@
1
+ # OS
2
+ .DS_Store
3
+
4
+ # IDE / tools
5
+ /.vscode
6
+ /.claude
7
+ /.playwright-mcp
8
+ /.ruff_cache
9
+
10
+ # Project-specific
11
+ /docs/publication
12
+
13
+ # --- data ---
14
+ # Ignore all data directories except 3FTx, Pla2g2, toxins, and jmb_2025
15
+ /data/*
16
+ !/data/3FTx
17
+ !/data/Pla2g2
18
+ !/data/toxins
19
+ !/data/jmb_2025
20
+ # Always ignore pdb subdirectories, tmp subdirectories, and .h5 files everywhere
21
+ **/pdb/
22
+ **/tmp/
23
+ **/*.h5
24
+
25
+ # Python bytecode
26
+ __pycache__/
27
+ *.py[cod]
28
+ *$py.class
29
+ *.so
30
+
31
+ # Distribution / packaging
32
+ build/
33
+ dist/
34
+ *.egg-info/
35
+ *.egg
36
+
37
+ # Testing / coverage
38
+ htmlcov/
39
+ .coverage
40
+ .coverage.*
41
+ coverage.xml
42
+ .pytest_cache/
43
+
44
+ # Environments
45
+ .env
46
+ .venv
47
+ venv/
48
+
49
+ # Jupyter
50
+ .ipynb_checkpoints
51
+
52
+ # Logs
53
+ *.log
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: protlabel
3
+ Version: 4.6.0
4
+ Summary: Embedding Annotation Transfer (EAT): nearest-neighbour label transfer in protein-language-model embedding space
5
+ Project-URL: Homepage, https://github.com/tsenoner/protspace
6
+ Project-URL: Repository, https://github.com/tsenoner/protspace
7
+ Author-email: Tobias Senoner <tobias.senoner@tum.de>
8
+ License-Expression: GPL-3.0
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: numpy>=1.23.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ # protlabel
14
+
15
+ **Embedding Annotation Transfer (EAT) engine** — nearest-neighbour label transfer in
16
+ protein-language-model (pLM) embedding space, with the goPredSim reliability index.
17
+
18
+ `protlabel` is a small, dependency-light library (numpy only). It is
19
+ ProtSpace-agnostic by design — it imports nothing from `protspace` — so it is
20
+ independently testable and reusable from notebooks, `protspace_uniprot`, or any
21
+ other project. The `protspace transfer` CLI is a thin consumer of this engine.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install protlabel
27
+ ```
28
+
29
+ ## Use
30
+
31
+ ```python
32
+ import numpy as np
33
+ from protlabel import eat, Lookup
34
+
35
+ preds = eat(
36
+ query_emb=np.random.rand(3, 1024).astype("float32"),
37
+ query_ids=["Q1", "Q2", "Q3"],
38
+ ref_emb=np.random.rand(100, 1024).astype("float32"),
39
+ ref_ids=[f"R{i}" for i in range(100)],
40
+ ref_labels=["toxin", "enzyme"] * 50,
41
+ k=1,
42
+ metric="cosine", # or "euclidean" (the goPredSim default)
43
+ )
44
+ for p in preds:
45
+ print(p.query_id, p.label, round(p.reliability, 3))
46
+ ```
47
+
48
+ `Lookup` builds and serialises a reusable reference set (`.npz` sidecar) so the
49
+ reference matrix can be rebuilt on demand rather than shipped.
50
+
51
+ ## Method
52
+
53
+ Nearest-neighbour transfer in the *original* pLM embedding space (not a 2-D/3-D
54
+ projection), with the goPredSim reliability index — see Littmann et al.,
55
+ *Sci Rep* 2021 (Eq. 5) and Heinzinger et al., *NAR Genom Bioinform* 2022.
56
+
57
+ Distances are computed with an exact, chunked brute-force search (numpy BLAS GEMM
58
+ + `argpartition`); queries are processed in batches. No approximate-nearest-neighbour
59
+ index is needed at Swiss-Prot scale.
60
+
61
+ ## License
62
+
63
+ GPL-3.0 — part of the [ProtSpace](https://github.com/tsenoner/protspace) project.
@@ -0,0 +1,51 @@
1
+ # protlabel
2
+
3
+ **Embedding Annotation Transfer (EAT) engine** — nearest-neighbour label transfer in
4
+ protein-language-model (pLM) embedding space, with the goPredSim reliability index.
5
+
6
+ `protlabel` is a small, dependency-light library (numpy only). It is
7
+ ProtSpace-agnostic by design — it imports nothing from `protspace` — so it is
8
+ independently testable and reusable from notebooks, `protspace_uniprot`, or any
9
+ other project. The `protspace transfer` CLI is a thin consumer of this engine.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install protlabel
15
+ ```
16
+
17
+ ## Use
18
+
19
+ ```python
20
+ import numpy as np
21
+ from protlabel import eat, Lookup
22
+
23
+ preds = eat(
24
+ query_emb=np.random.rand(3, 1024).astype("float32"),
25
+ query_ids=["Q1", "Q2", "Q3"],
26
+ ref_emb=np.random.rand(100, 1024).astype("float32"),
27
+ ref_ids=[f"R{i}" for i in range(100)],
28
+ ref_labels=["toxin", "enzyme"] * 50,
29
+ k=1,
30
+ metric="cosine", # or "euclidean" (the goPredSim default)
31
+ )
32
+ for p in preds:
33
+ print(p.query_id, p.label, round(p.reliability, 3))
34
+ ```
35
+
36
+ `Lookup` builds and serialises a reusable reference set (`.npz` sidecar) so the
37
+ reference matrix can be rebuilt on demand rather than shipped.
38
+
39
+ ## Method
40
+
41
+ Nearest-neighbour transfer in the *original* pLM embedding space (not a 2-D/3-D
42
+ projection), with the goPredSim reliability index — see Littmann et al.,
43
+ *Sci Rep* 2021 (Eq. 5) and Heinzinger et al., *NAR Genom Bioinform* 2022.
44
+
45
+ Distances are computed with an exact, chunked brute-force search (numpy BLAS GEMM
46
+ + `argpartition`); queries are processed in batches. No approximate-nearest-neighbour
47
+ index is needed at Swiss-Prot scale.
48
+
49
+ ## License
50
+
51
+ GPL-3.0 — part of the [ProtSpace](https://github.com/tsenoner/protspace) project.
@@ -0,0 +1,221 @@
1
+ """Micro-benchmark: exact brute-force chunked-GEMM kNN (protlabel.backends.nearest)
2
+ vs approximate HNSW kNN (usearch) on this machine.
3
+
4
+ Run (usearch + psutil are not project dependencies — pull them just for the run):
5
+ cd <repo> && uv run --with usearch --with psutil python packages/protlabel/benchmarks/bench_knn.py
6
+
7
+ Grid: n_refs in {1000, 10000, 100000}; dim in {960, 1024, 1152, 1280, 2560};
8
+ n_queries=128; cosine; k=1. Dims span the pLMs ProtSpace embeds with:
9
+ 960 = ESMC-300M, 1024 = ProtT5 (the transfer default), 1152 = ESMC-600M,
10
+ 1280 = ESM2-650M, 2560 = ESM2-3B (the large-model / memory-ceiling case).
11
+ Timings use time.perf_counter. BLAS is warmed up once before timing.
12
+ peak_mb is whole-process peak RSS sampled during each method's build+query window
13
+ (best-effort; includes interpreter + shared ref/query arrays baseline).
14
+
15
+ See docs/superpowers/research/2026-06-29-usearch-vs-bruteforce.md for results + analysis.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import gc
21
+ import json
22
+ import platform
23
+ import threading
24
+ import time
25
+
26
+ import numpy as np
27
+ import psutil
28
+
29
+ from protlabel.backends import nearest
30
+
31
+ try:
32
+ from usearch.index import Index, MetricKind
33
+
34
+ HAVE_USEARCH = True
35
+ USEARCH_ERR = None
36
+ except Exception as exc: # pragma: no cover - environment guard
37
+ HAVE_USEARCH = False
38
+ USEARCH_ERR = repr(exc)
39
+
40
+
41
+ # usearch HNSW hyperparameters (library standard settings)
42
+ M = 16
43
+ EF_CONSTRUCTION = 128
44
+ EF_SEARCH = 64
45
+
46
+ N_QUERIES = 128
47
+ K = 1
48
+ SEED = 0
49
+
50
+ GRID = [(n, d) for n in (1_000, 10_000, 100_000) for d in (960, 1024, 1152, 1280, 2560)]
51
+
52
+
53
+ class PeakRSS:
54
+ """Sample whole-process RSS in a background thread; report the peak (MB)."""
55
+
56
+ def __init__(self, interval_s: float = 0.002):
57
+ self._proc = psutil.Process()
58
+ self._interval = interval_s
59
+ self._peak = 0
60
+ self._stop = threading.Event()
61
+ self._thread: threading.Thread | None = None
62
+
63
+ def _run(self):
64
+ while not self._stop.is_set():
65
+ try:
66
+ rss = self._proc.memory_info().rss
67
+ except Exception:
68
+ rss = 0
69
+ if rss > self._peak:
70
+ self._peak = rss
71
+ self._stop.wait(self._interval)
72
+
73
+ def __enter__(self):
74
+ # Seed with the current RSS so a fast method still records something.
75
+ self._peak = self._proc.memory_info().rss
76
+ self._thread = threading.Thread(target=self._run, daemon=True)
77
+ self._thread.start()
78
+ return self
79
+
80
+ def __exit__(self, *exc):
81
+ self._stop.set()
82
+ if self._thread is not None:
83
+ self._thread.join()
84
+ # Final reading in case the peak happened right before exit.
85
+ try:
86
+ rss = self._proc.memory_info().rss
87
+ self._peak = max(self._peak, rss)
88
+ except Exception:
89
+ pass
90
+ return False
91
+
92
+ @property
93
+ def peak_mb(self) -> float:
94
+ return self._peak / (1024 * 1024)
95
+
96
+
97
+ def warmup():
98
+ """Warm up numpy/BLAS and the nearest() code path once (not timed)."""
99
+ rng = np.random.default_rng(123)
100
+ a = rng.standard_normal((128, 512), dtype=np.float32)
101
+ b = rng.standard_normal((2000, 512), dtype=np.float32)
102
+ _ = a @ b.T
103
+ _ = nearest(a, b, k=1, metric="cosine")
104
+ if HAVE_USEARCH:
105
+ idx = Index(
106
+ ndim=512,
107
+ metric=MetricKind.Cos,
108
+ connectivity=M,
109
+ expansion_add=EF_CONSTRUCTION,
110
+ expansion_search=EF_SEARCH,
111
+ )
112
+ idx.add(np.arange(2000), b)
113
+ _ = idx.search(a, count=1)
114
+ del idx
115
+ gc.collect()
116
+
117
+
118
+ def bench_one(n_refs: int, dim: int) -> list[dict]:
119
+ rng = np.random.default_rng(SEED)
120
+ refs = rng.standard_normal((n_refs, dim)).astype(np.float32)
121
+ queries = rng.standard_normal((N_QUERIES, dim)).astype(np.float32)
122
+ rows: list[dict] = []
123
+
124
+ # --- 1) brute-force exact (ground truth) ---
125
+ gc.collect()
126
+ with PeakRSS() as mon:
127
+ t0 = time.perf_counter()
128
+ bf_idx, _bf_dist = nearest(queries, refs, k=K, metric="cosine")
129
+ q_s = time.perf_counter() - t0
130
+ bf_top1 = bf_idx[:, 0]
131
+ rows.append(
132
+ {
133
+ "n_refs": n_refs,
134
+ "dim": dim,
135
+ "n_queries": N_QUERIES,
136
+ "method": "brute_force",
137
+ "build_s": 0.0,
138
+ "query_total_s": q_s,
139
+ "per_query_ms": q_s / N_QUERIES * 1000.0,
140
+ "recall_at_1": 1.0,
141
+ "peak_mb": mon.peak_mb,
142
+ }
143
+ )
144
+
145
+ # --- 2) usearch HNSW (approximate) ---
146
+ if HAVE_USEARCH:
147
+ keys = np.arange(n_refs, dtype=np.int64)
148
+ gc.collect()
149
+ with PeakRSS() as mon:
150
+ idx = Index(
151
+ ndim=dim,
152
+ metric=MetricKind.Cos,
153
+ connectivity=M,
154
+ expansion_add=EF_CONSTRUCTION,
155
+ expansion_search=EF_SEARCH,
156
+ dtype="f32",
157
+ )
158
+ t0 = time.perf_counter()
159
+ idx.add(keys, refs)
160
+ build_s = time.perf_counter() - t0
161
+
162
+ t1 = time.perf_counter()
163
+ matches = idx.search(queries, count=K)
164
+ q_s = time.perf_counter() - t1
165
+ us_keys = np.asarray(matches.keys).reshape(N_QUERIES, -1)
166
+ us_top1 = us_keys[:, 0]
167
+ recall = float(np.mean(us_top1 == bf_top1))
168
+ rows.append(
169
+ {
170
+ "n_refs": n_refs,
171
+ "dim": dim,
172
+ "n_queries": N_QUERIES,
173
+ "method": f"usearch(M={M},ef={EF_SEARCH})",
174
+ "build_s": build_s,
175
+ "query_total_s": q_s,
176
+ "per_query_ms": q_s / N_QUERIES * 1000.0,
177
+ "recall_at_1": recall,
178
+ "peak_mb": mon.peak_mb,
179
+ }
180
+ )
181
+ del idx, keys
182
+
183
+ del refs, queries
184
+ gc.collect()
185
+ return rows
186
+
187
+
188
+ def main():
189
+ print(f"usearch available: {HAVE_USEARCH} ({USEARCH_ERR or 'ok'})")
190
+ print(
191
+ f"python={platform.python_version()} numpy={np.__version__} "
192
+ f"psutil={psutil.__version__} platform={platform.platform()}"
193
+ )
194
+ warmup()
195
+
196
+ all_rows: list[dict] = []
197
+ wall0 = time.perf_counter()
198
+ for n_refs, dim in GRID:
199
+ all_rows.extend(bench_one(n_refs, dim))
200
+ wall = time.perf_counter() - wall0
201
+
202
+ # Pretty table
203
+ hdr = (
204
+ f"{'n_refs':>8} {'dim':>5} {'nq':>4} {'method':>22} "
205
+ f"{'build_s':>9} {'query_s':>9} {'per_q_ms':>9} {'recall@1':>9} {'peak_mb':>9}"
206
+ )
207
+ print("\n" + hdr)
208
+ print("-" * len(hdr))
209
+ for r in all_rows:
210
+ print(
211
+ f"{r['n_refs']:>8} {r['dim']:>5} {r['n_queries']:>4} {r['method']:>22} "
212
+ f"{r['build_s']:>9.4f} {r['query_total_s']:>9.4f} {r['per_query_ms']:>9.4f} "
213
+ f"{r['recall_at_1']:>9.3f} {r['peak_mb']:>9.1f}"
214
+ )
215
+ print(f"\nTotal wall time for grid: {wall:.2f}s")
216
+ print("\n===JSON===")
217
+ print(json.dumps(all_rows))
218
+
219
+
220
+ if __name__ == "__main__":
221
+ main()
@@ -0,0 +1,102 @@
1
+ """Single-config memory + timing probe for protlabel.backends.nearest.
2
+
3
+ Runs ONE (metric, n_refs, dim) config and exits — run it once per config in a
4
+ *fresh* process so peak RSS reflects that config alone (process RSS is a
5
+ monotonic high-water mark, so measuring several configs in one process is
6
+ misleading). Vectors are generated directly as float32 (no float64 temporary).
7
+
8
+ Usage:
9
+ python bench_memory.py <metric> <n_refs> <dim> [n_queries]
10
+
11
+ Loop over a grid inside a resource-limited container (the deployment envelope):
12
+ for m in euclidean cosine; do for n in 100000 300000 570000; do
13
+ python packages/protlabel/benchmarks/bench_memory.py $m $n 1024
14
+ done; done
15
+
16
+ See docs/superpowers/research/2026-06-29-usearch-vs-bruteforce.md.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import platform
24
+ import sys
25
+ import threading
26
+ import time
27
+
28
+ import numpy as np
29
+ import psutil
30
+
31
+ from protlabel.backends import nearest
32
+
33
+
34
+ class PeakRSS:
35
+ def __init__(self, interval_s: float = 0.005):
36
+ self._proc = psutil.Process()
37
+ self._interval = interval_s
38
+ self._peak = 0
39
+ self._stop = threading.Event()
40
+ self._t: threading.Thread | None = None
41
+
42
+ def _run(self):
43
+ while not self._stop.is_set():
44
+ self._peak = max(self._peak, self._proc.memory_info().rss)
45
+ self._stop.wait(self._interval)
46
+
47
+ def __enter__(self):
48
+ self._peak = self._proc.memory_info().rss
49
+ self._t = threading.Thread(target=self._run, daemon=True)
50
+ self._t.start()
51
+ return self
52
+
53
+ def __exit__(self, *exc):
54
+ self._stop.set()
55
+ if self._t is not None:
56
+ self._t.join()
57
+ self._peak = max(self._peak, self._proc.memory_info().rss)
58
+ return False
59
+
60
+ @property
61
+ def peak_mb(self) -> float:
62
+ return self._peak / (1024 * 1024)
63
+
64
+
65
+ def main() -> None:
66
+ metric = sys.argv[1] if len(sys.argv) > 1 else "cosine"
67
+ n_refs = int(sys.argv[2]) if len(sys.argv) > 2 else 100_000
68
+ dim = int(sys.argv[3]) if len(sys.argv) > 3 else 1024
69
+ n_queries = int(sys.argv[4]) if len(sys.argv) > 4 else 128
70
+
71
+ rng = np.random.default_rng(0)
72
+ # Generate float32 directly — no float64 temporary inflating peak RSS.
73
+ refs = rng.standard_normal((n_refs, dim), dtype=np.float32)
74
+ queries = rng.standard_normal((n_queries, dim), dtype=np.float32)
75
+
76
+ # Warm up BLAS once (not timed).
77
+ nearest(queries[:8], refs[:1000], k=1, metric=metric)
78
+
79
+ with PeakRSS() as mon:
80
+ t0 = time.perf_counter()
81
+ nearest(queries, refs, k=1, metric=metric)
82
+ q_s = time.perf_counter() - t0
83
+
84
+ print(
85
+ json.dumps(
86
+ {
87
+ "metric": metric,
88
+ "n_refs": n_refs,
89
+ "dim": dim,
90
+ "n_queries": n_queries,
91
+ "refs_gb_f32": round(refs.nbytes / 1e9, 2),
92
+ "per_query_ms": round(q_s / n_queries * 1000, 4),
93
+ "peak_mb": round(mon.peak_mb, 1),
94
+ "cpus": os.cpu_count(),
95
+ "platform": platform.platform(),
96
+ }
97
+ )
98
+ )
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "protlabel"
3
+ version = "4.6.0"
4
+ description = "Embedding Annotation Transfer (EAT): nearest-neighbour label transfer in protein-language-model embedding space"
5
+ authors = [{ name = "Tobias Senoner", email = "tobias.senoner@tum.de" }]
6
+ readme = "README.md"
7
+ license = "GPL-3.0"
8
+ requires-python = ">=3.10"
9
+ dependencies = [
10
+ "numpy>=1.23.0",
11
+ ]
12
+
13
+ [project.urls]
14
+ Homepage = "https://github.com/tsenoner/protspace"
15
+ Repository = "https://github.com/tsenoner/protspace"
16
+
17
+
18
+ [build-system]
19
+ requires = ["hatchling"]
20
+ build-backend = "hatchling.build"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/protlabel"]
@@ -0,0 +1,20 @@
1
+ """protlabel — Embedding Annotation Transfer (EAT) engine.
2
+
3
+ Nearest-neighbour label transfer in protein-language-model embedding space,
4
+ with the goPredSim reliability index. Pure numpy (plus the standard library);
5
+ no protspace imports.
6
+ """
7
+
8
+ from importlib.metadata import PackageNotFoundError, version
9
+
10
+ from protlabel.lookup import Lookup
11
+ from protlabel.transfer import Prediction, eat
12
+
13
+ try:
14
+ # Report the installed distribution version rather than a hard-coded literal
15
+ # that would silently drift across releases.
16
+ __version__ = version("protlabel")
17
+ except PackageNotFoundError: # pragma: no cover - source/uninstalled fallback
18
+ __version__ = "0.0.0"
19
+
20
+ __all__ = ["Lookup", "Prediction", "eat", "__version__"]