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.
- protlabel-4.6.0/.gitignore +53 -0
- protlabel-4.6.0/PKG-INFO +63 -0
- protlabel-4.6.0/README.md +51 -0
- protlabel-4.6.0/benchmarks/bench_knn.py +221 -0
- protlabel-4.6.0/benchmarks/bench_memory.py +102 -0
- protlabel-4.6.0/pyproject.toml +23 -0
- protlabel-4.6.0/src/protlabel/__init__.py +20 -0
- protlabel-4.6.0/src/protlabel/backends.py +183 -0
- protlabel-4.6.0/src/protlabel/lookup.py +75 -0
- protlabel-4.6.0/src/protlabel/reliability.py +45 -0
- protlabel-4.6.0/src/protlabel/transfer.py +97 -0
- protlabel-4.6.0/tests/test_protlabel_backends.py +207 -0
- protlabel-4.6.0/tests/test_protlabel_boundary.py +39 -0
- protlabel-4.6.0/tests/test_protlabel_lookup.py +66 -0
- protlabel-4.6.0/tests/test_protlabel_reliability.py +55 -0
- protlabel-4.6.0/tests/test_protlabel_transfer.py +115 -0
- protlabel-4.6.0/tests/test_protlabel_version.py +11 -0
|
@@ -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
|
protlabel-4.6.0/PKG-INFO
ADDED
|
@@ -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__"]
|