enzyme-ec-predictor / predict.py
p-gupta's picture
Upload folder using huggingface_hub
48671b8 verified
Raw History Blame Contribute Delete
16.4 kB
import argparse
import json
import math
import os
import shutil
import stat
import subprocess
import tempfile
import time
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).parent
BETA, GATE, K_HITS = 0.7, 20, 30
EXACT = 'ACDEFGHIKLMNPQRSTVWY'
GROUPS = ['LVIM', 'C', 'A', 'G', 'ST', 'P', 'FYW', 'EDNQ', 'KR', 'H']
K, BASE = 6, 11
MAXDF, MAXPOS, BAND, TOP = 2000, 60000, 32, 200
KEYMUL = 2 * MAXPOS // BAND + 2
LETTERS = np.frombuffer(b'ACDEFGHIKLMNPQRSTVWYX', dtype=np.uint8)
COLS = ['q', 't', 'bits', 'pident', 'qcov', 'tcov']
NF = 29
LUT = np.full(256, 20, dtype=np.int64)
for i, c in enumerate(EXACT):
LUT[ord(c)] = i
MAP = np.full(21, 10, dtype=np.int64)
for g, letters in enumerate(GROUPS):
for c in letters:
MAP[EXACT.index(c)] = g
LUT_NET = np.full(256, 21, dtype=np.int64)
for i, c in enumerate(EXACT):
LUT_NET[ord(c)] = i + 1
def to_codes(seq):
return LUT[np.frombuffer(seq.encode('ascii', 'ignore'), dtype=np.uint8)]
def hits_of(codes):
codes = MAP[codes[:MAXPOS].astype(np.int64)]
n = len(codes) - K + 1
if n <= 0:
return np.empty(0, dtype=np.int32), np.empty(0, dtype=np.int32)
ids = np.zeros(n, dtype=np.int64)
for j in range(K):
ids = ids * BASE + codes[j:j + n]
return ids.astype(np.int32), np.arange(n, dtype=np.int32)
def build_index(codes, lens):
starts = np.cumsum(lens) - lens
parts = [hits_of(codes[s:s + n]) for s, n in zip(starts, lens)]
sizes = np.array([len(p[0]) for p in parts])
ids_all = np.concatenate([p[0] for p in parts])
pos_all = np.concatenate([p[1] for p in parts]).astype(np.uint16)
del parts
order = np.argsort(ids_all, kind='stable')
kmer = ids_all[order]
del ids_all
pos = pos_all[order]
del pos_all
seq = np.repeat(np.arange(len(lens), dtype=np.int32), sizes)[order]
return {'kmer': kmer, 'pos': pos, 'seq': seq}
def candidates(index, codes):
empty = np.empty(0, dtype=np.int64)
ids, qpos = hits_of(codes)
if len(ids) == 0:
return empty, empty
lo = np.searchsorted(index['kmer'], ids, 'left')
cnt = np.searchsorted(index['kmer'], ids, 'right') - lo
keep = cnt <= MAXDF
lo, cnt, qpos = lo[keep], cnt[keep], qpos[keep]
total = cnt.sum()
if total == 0:
return empty, empty
offs = np.cumsum(cnt) - cnt
idx = np.arange(total) - np.repeat(offs, cnt) + np.repeat(lo, cnt)
diag = index['pos'][idx].astype(np.int64) - np.repeat(qpos, cnt).astype(np.int64) + MAXPOS
key = index['seq'][idx].astype(np.int64) * KEYMUL + diag // BAND
uk, counts = np.unique(key, return_counts=True)
nxt = np.minimum(np.searchsorted(uk, uk + 1), len(uk) - 1)
score = counts + np.where(uk[nxt] == uk + 1, counts[nxt], 0)
top = np.argsort(-score)[:TOP]
cand = uk[top] // KEYMUL
_, first = np.unique(cand, return_index=True)
first.sort()
return cand[first], score[top][first]
def vote(cand, score, lab_flat, lab_ptr, n_ec, k=5):
v = np.zeros(n_ec, dtype=np.float32)
for j, s in zip(cand[:k], score[:k]):
labs = lab_flat[lab_ptr[j]:lab_ptr[j + 1]]
v[labs] += s / len(labs)
total = v.sum()
return v / total if total > 0 else v
def red_votes(index, lab_flat, lab_ptr, seqs, n_ec):
out = np.zeros((len(seqs), n_ec), dtype=np.float32)
best = np.zeros(len(seqs))
for r, seq in enumerate(seqs):
cand, s = candidates(index, to_codes(seq))
out[r] = vote(cand, s, lab_flat, lab_ptr, n_ec)
best[r] = s[0] if len(s) else 0
return out, best
def binary_path():
flags = ''
try:
flags = Path('/proc/cpuinfo').read_text()
except OSError:
pass
path = ROOT.resolve() / ('mmseqs_avx2' if ' avx2' in flags else 'mmseqs_sse41')
try:
os.chmod(path, os.stat(path).st_mode | stat.S_IEXEC)
except OSError:
pass
return str(path)
def search(codes, lens, queries, threads=2, sens=4.0, max_seqs=50, timeout=480):
work = tempfile.mkdtemp(dir='/tmp')
try:
exe = binary_path()
starts = np.cumsum(lens) - lens
with open(os.path.join(work, 'target.fa'), 'wb') as f:
for i in range(len(lens)):
seq = LETTERS[codes[starts[i]:starts[i] + lens[i]].astype(np.int64)].tobytes()
f.write(b'>' + str(i).encode() + b'\n' + seq + b'\n')
with open(os.path.join(work, 'query.fa'), 'wb') as f:
for i, s in enumerate(queries):
f.write(b'>' + str(i).encode() + b'\n' + s.encode('ascii', 'ignore') + b'\n')
def run(*args):
subprocess.run([exe, *args, '-v', '1'], check=True, cwd=work, timeout=timeout,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
run('createdb', 'target.fa', 'tDB')
run('createdb', 'query.fa', 'qDB')
run('search', 'qDB', 'tDB', 'res', 'tmp', '-s', str(sens), '--max-seqs', str(max_seqs), '-e', '10',
'--threads', str(threads), '--split-memory-limit', '6G')
run('convertalis', 'qDB', 'tDB', 'res', 'res.m8', '--format-output', 'query,target,bits,pident,qcov,tcov')
out = os.path.join(work, 'res.m8')
if os.path.getsize(out) == 0:
return pd.DataFrame(columns=COLS)
table = pd.read_csv(out, sep='\t', names=COLS)
return table.sort_values(['q', 'bits'], ascending=[True, False], kind='stable')
finally:
shutil.rmtree(work, ignore_errors=True)
def build_vocab_maps(ec_list):
l3 = {}
l2 = {}
l3of = np.zeros(len(ec_list), dtype=np.int32)
l2of = np.zeros(len(ec_list), dtype=np.int32)
for i, e in enumerate(ec_list):
p = e.split('.')
l3of[i] = l3.setdefault('.'.join(p[:3]), len(l3))
l2of[i] = l2.setdefault('.'.join(p[:2]), len(l2))
return l3of, l2of
def query_features(t, bits, pid, qcov, tcov, labs, prior_log, l3of, l2of, qlen, tlens):
prior_cnt = np.exp(prior_log)
K = len(t)
top = float(bits[0])
rel = bits / top
r4 = rel ** 4
r10 = rel ** 10
cand = {}
tot4 = 0.0
tot1 = 0.0
l3s = {}
l2s = {}
for r in range(K):
L = labs[t[r]]
n = len(L)
w4 = r4[r] / n
w1 = rel[r] / n
tot4 += r4[r]
tot1 += rel[r]
for e in L:
d = cand.get(e)
if d is None:
d = [0.0] * 14
d[5] = r
d[6] = bits[r]
d[7] = pid[r]
d[8] = n
d[9] = qcov[r] * tcov[r]
d[10] = tlens[t[r]] / max(qlen, 1)
cand[e] = d
d[0] += w1
d[12] += pid[r]
d[13] += bits[r]
d[1] += w4
d[2] += r10[r] / n
d[3] += 1
if r < 5:
d[4] += 1
if r < 10:
d[11] += 1
for e in set(int(l3of[x]) for x in L):
l3s[e] = l3s.get(e, 0.0) + r4[r] / len(set(int(l3of[x]) for x in L))
for e in set(int(l2of[x]) for x in L):
l2s[e] = l2s.get(e, 0.0) + r4[r] / len(set(int(l2of[x]) for x in L))
ecs = list(cand.keys())
F = np.zeros((len(ecs), NF), dtype=np.float32)
top_pid = float(pid[0])
top_labs = set(labs[t[0]])
for i, e in enumerate(ecs):
d = cand[e]
F[i, 0] = d[0] / tot1
F[i, 1] = d[1] / tot4
F[i, 2] = d[2]
F[i, 3] = d[3]
F[i, 4] = d[4]
F[i, 5] = d[5]
F[i, 6] = d[6] / top
F[i, 7] = d[7]
F[i, 8] = d[8]
F[i, 9] = d[9]
F[i, 10] = d[10]
F[i, 11] = d[11]
F[i, 12] = prior_log[e]
F[i, 13] = l3s[int(l3of[e])] / tot4
F[i, 14] = l2s[int(l2of[e])] / tot4
F[i, 15] = 1.0 if e in top_labs else 0.0
F[i, 16] = np.log(top)
F[i, 17] = top_pid
F[i, 18] = np.log(max(qlen, 1))
F[i, 19] = K
F[i, 20] = d[1]
F[i, 21] = len(top_labs)
F[i, 22] = bits[min(1, K - 1)] / top
F[i, 23] = d[6]
F[i, 24] = d[3] / prior_cnt[e]
F[i, 25] = d[12] / d[3]
F[i, 26] = d[13] / bits.sum()
F[i, 27] = np.log(d[3] / prior_cnt[e] + 1e-4)
F[i, 28] = pid.mean()
return ecs, F
def full_features(F):
v = F[:, 1]
v0 = F[:, 0]
srt = np.sort(v)[::-1]
ex = np.zeros((len(F), 7), dtype=np.float32)
ex[:, 0] = len(F)
ex[:, 1] = v - srt[0]
ex[:, 2] = v - (srt[1] if len(srt) > 1 else 0)
ex[:, 3] = (-v).argsort().argsort()
ex[:, 4] = v0 - v0.max()
pos = v[v > 0]
ex[:, 5] = -(pos * np.log(pos)).sum()
ex[:, 6] = F[:, 12] - F[:, 12].max()
return np.hstack([F, ex])
class Ranker:
def __init__(self, z):
self.feat, self.thr = z['r_feat'], z['r_thr']
self.left, self.right = z['r_left'], z['r_right']
self.val, self.leaf = z['r_val'], z['r_leaf']
self.base = float(z['r_base'][0])
def score(self, X):
rows = np.arange(len(X))
raw = np.full(len(X), self.base)
for i in range(len(self.feat)):
node = np.zeros(len(X), dtype=np.int64)
for _ in range(64):
lf = self.leaf[i][node]
if lf.all():
break
f = self.feat[i][node]
go_left = X[rows, f] <= self.thr[i][node]
nxt = np.where(go_left, self.left[i][node], self.right[i][node])
node = np.where(lf, node, nxt)
raw += self.val[i][node]
return raw
def rank_queries(hits, seqs, labs, prior_log, l3of, l2of, tlens, ranker, ec_list):
out = {}
q = hits.q.values
if len(q) == 0:
return out
cuts = np.concatenate([[0], np.where(q[1:] != q[:-1])[0] + 1, [len(q)]])
t_all, b_all = hits.t.values, hits.bits.values.astype(float)
p_all, qc_all, tc_all = hits.pident.values, hits.qcov.values, hits.tcov.values
feats, owners = [], []
for a, b in zip(cuts[:-1], cuts[1:]):
b = min(b, a + K_HITS)
qi = int(q[a])
ecs, F = query_features(t_all[a:b], b_all[a:b], p_all[a:b], qc_all[a:b], tc_all[a:b], labs, prior_log,
l3of, l2of, len(seqs[qi]), tlens)
feats.append(full_features(F))
owners.append((qi, ecs))
scores = ranker.score(np.vstack(feats))
pos = 0
for qi, ecs in owners:
s = scores[pos:pos + len(ecs)]
pos += len(ecs)
out[qi] = ec_list[ecs[int(np.argmax(s))]]
return out
def network_scores(z, seqs):
import torch
import torch.nn.functional as F
torch.set_grad_enabled(False)
torch.set_num_threads(2)
P = {k[2:]: torch.from_numpy(z[k]) for k in z.files if k.startswith('p_')}
dilations = z['dilations'].tolist()
alpha = float(z['alpha'][0])
bank = F.normalize(torch.from_numpy(z['bank'].astype(np.float32)), dim=1)
bank_flat = z['bank_flat'].astype(np.int64)
bank_ptr = np.concatenate([[0], np.cumsum(z['bank_len'])])
n_ec = len(z['ec_list'])
def softmax(x):
e = torch.exp(x - x.max(1, keepdim=True).values)
return e / e.sum(1, keepdim=True)
def encode(seq):
codes = LUT_NET[np.frombuffer(seq.encode('ascii', 'ignore'), dtype=np.uint8)]
if len(codes) > 800:
start = (len(codes) - 800) // 2
codes = codes[start:start + 800]
return codes
def forward(tokens):
m = (tokens > 0).unsqueeze(-1).to(P['embed.w'].dtype)
x = P['embed.w'][tokens] * m
for i, d in enumerate(dilations):
pre = f'b{i}.'
w = P[pre + 'dw.w']
xc = x.transpose(1, 2).contiguous()
h = F.conv1d(xc, w, P[pre + 'dw.b'], padding=d * (w.shape[-1] // 2), dilation=d, groups=xc.shape[1])
h = h.transpose(1, 2).contiguous() * m
mu = h.mean(-1, keepdim=True)
var = h.var(-1, unbiased=False, keepdim=True)
h = P[pre + 'ln.g'] * ((h - mu) * (1.0 / torch.sqrt(var + 1e-6))) + P[pre + 'ln.b']
h = h @ P[pre + 'pw1.w'] + P[pre + 'pw1.b']
h = h * 0.5 * (1.0 + torch.erf(h / math.sqrt(2.0)))
h = h @ P[pre + 'pw2.w'] + P[pre + 'pw2.b']
x = (x + h) * m
n = m.sum(1).clamp(min=1.0)
mean = x.sum(1) / n
mx, idx = (x - (1.0 - m) * 1e4).max(1)
pooled = torch.cat([mx, mean], 1)
mu = pooled.mean(-1, keepdim=True)
var = pooled.var(-1, unbiased=False, keepdim=True)
z1 = P['head.ln.g'] * ((pooled - mu) * (1.0 / torch.sqrt(var + 1e-6))) + P['head.ln.b']
e = z1 @ P['head.proj.w'] + P['head.proj.b']
a = e * 0.5 * (1.0 + torch.erf(e / math.sqrt(2.0)))
return a @ P['head.out.w'] + P['head.out.b'], e
order = np.argsort([len(s) for s in seqs], kind='stable')
emb = np.zeros((len(seqs), 128), np.float32)
probs = np.zeros((len(seqs), n_ec), np.float32)
for s in range(0, len(seqs), 64):
ids = order[s:s + 64]
enc = [encode(seqs[i]) for i in ids]
x = np.zeros((len(enc), max(len(e) for e in enc)), dtype=np.int64)
for r, e in enumerate(enc):
x[r, :len(e)] = e
logits, e = forward(torch.from_numpy(x))
emb[ids] = e.cpu().numpy()
probs[ids] = softmax(logits).cpu().numpy()
q = F.normalize(torch.from_numpy(emb), dim=1)
votes = np.zeros((len(q), n_ec), np.float32)
for s in range(0, len(q), 256):
v, idx = (q[s:s + 256] @ bank.T).topk(5, dim=1)
w = softmax(v / 0.05).numpy()
idx = idx.numpy()
for r in range(len(idx)):
for j in range(5):
labs = bank_flat[bank_ptr[idx[r, j]]:bank_ptr[idx[r, j] + 1]]
votes[s + r, labs] += w[r, j] / len(labs)
return alpha * votes + (1 - alpha) * probs
def predict_file(input_path, output_path):
t0 = time.time()
frame = pd.read_parquet(input_path)[['id', 'sequence']]
if frame.id.isna().any() or frame.id.duplicated().any():
raise ValueError('Input IDs must be present and unique.')
frame = frame.drop_duplicates('sequence', keep='first').reset_index(drop=True)
seqs = frame.sequence.tolist()
z = np.load(ROOT / 'model.npz')
ec_list = z['ec_list'].tolist()
n_ec = len(ec_list)
codes, lens = z['codes'], z['lens']
lab_flat = z['lab_flat'].astype(np.int64)
lab_ptr = np.concatenate([[0], np.cumsum(z['lab_len'])])
chosen = {}
try:
labs = [lab_flat[lab_ptr[i]:lab_ptr[i + 1]] for i in range(len(lens))]
prior_log = np.log(np.maximum(np.bincount(lab_flat, minlength=n_ec), 1).astype(float))
l3of, l2of = build_vocab_maps(ec_list)
ranker = Ranker(z)
hits = search(codes, lens, seqs)
print('search finished after', round(time.time() - t0), 's, hits:', len(hits), flush=True)
chosen = rank_queries(hits, seqs, labs, prior_log, l3of, l2of, lens, ranker, ec_list)
except Exception as error:
print('search or ranking failed, using the backup method for every input:', error, flush=True)
chosen = {}
rest = np.array([i for i in range(len(seqs)) if i not in chosen], dtype=np.int64)
print('ranked inputs:', len(chosen), ' inputs without a search hit:', len(rest), ' of', len(seqs), flush=True)
preds = [chosen.get(i) for i in range(len(seqs))]
if len(rest):
index = build_index(codes, lens)
red, best = red_votes(index, lab_flat, lab_ptr, [seqs[i] for i in rest], n_ec)
weak = np.where(best < GATE)[0]
if len(weak):
try:
net = network_scores(z, [seqs[rest[i]] for i in weak])
red[weak] = BETA * red[weak] + (1 - BETA) * net
except Exception as error:
print('network step skipped:', error, flush=True)
for r, i in enumerate(rest):
preds[i] = ec_list[int(red[r].argmax())]
with Path(output_path).open('w') as output:
for key, ec in zip(frame.id, preds):
output.write(json.dumps({'id': key, 'prediction': [ec]}) + '\n')
print('total time', round(time.time() - t0), 's', flush=True)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--input', required=True)
parser.add_argument('--output', required=True)
args = parser.parse_args()
predict_file(args.input, args.output)