Use AminoGrapes
Complete Model Implementation, Colab Reproduction Protocol & Python Code
This page contains the complete implementation of the AminoGrapes viral fitness prediction model. Every cell from Aminograpes_reproduce.ipynb is provided below with executable Python code, mathematical breakdown, and benchmark verification.
How AminoGrapes Works
ESM2-650M: A 650-million parameter protein masked language model trained by Meta AI. It serves as the primary language model baseline.
AminoGrapes: A closed-form scoring method that computes how conserved an amino acid is among evolutionary homologs (from a multiple sequence alignment) multiplied by how buried that residue is in the 3D protein fold (from AlphaFold2 predicted coordinates). No parameters are trained or tuned on viral targets.
Public Model Ensemble (Step 10): Combines 5 third-party ProteinGym models selected strictly on non-viral assays. It is included only for context; AminoGrapes is not used in that ensemble.
In Colab: Runtime > Change runtime type > T4 GPU. Needs ~20 GB free disk space.
Steps 1 to 8 reproduce all published primary metrics (~1 to 2 hours, mostly downloads and ESM2 inference).
Every completed assay is cached to disk. If Colab disconnects, re-running the cell will skip finished assays.
Step-by-Step Model Execution
Below is the verbatim source code for each of the 11 steps in the notebook. You can copy individual snippets or download the full notebook above.
Install and Set Up
Environment initialization and GPU check
Installs required dependencies (biopython, transformers), configures workspace directories, and verifies GPU availability.
!pip -q install biopython transformers
import os, re, glob, zipfile, shutil, numpy as np, pandas as pd
from scipy.stats import spearmanr, wilcoxon
WORK = '/content/aminograpes_repro'; P = f'{WORK}/pg'; OUT = f'{WORK}/out'
os.makedirs(P, exist_ok=True); os.makedirs(OUT, exist_ok=True)
import torch; print('GPU available:', torch.cuda.is_available())
assert torch.cuda.is_available(), 'Turn on a GPU: Runtime > Change runtime type'GPU available: True
Download Public Data
Fetch ProteinGym v1.3 archives (~10–20 min)
Downloads official benchmark files from the Harvard Marks Lab ProteinGym v1.3 release:
- DMS_ProteinGym_substitutions.zip (assay experimental measurements)
- DMS_msa_weights.zip & DMS_msa_files.zip (evolutionary alignments)
- zero_shot_substitutions_scores.zip (official benchmark baselines)
- ProteinGym_AF2_structures.zip (predicted 3D coordinates)
B = 'https://marks.hms.harvard.edu/proteingym/ProteinGym_v1.3'
def fetch(zipname, flag, dest=P):
if os.path.exists(f'{P}/{flag}'): return
print('downloading', zipname, '...')
assert os.system(f'curl -fsSL -o {P}/t.zip {B}/{zipname}') == 0, 'download failed: ' + zipname
zipfile.ZipFile(f'{P}/t.zip').extractall(dest); os.remove(f'{P}/t.zip'); open(f'{P}/{flag}', 'w').close()
fetch('DMS_ProteinGym_substitutions.zip', 'subs.ok')
fetch('DMS_msa_weights.zip', 'w.ok')
fetch('DMS_msa_files.zip', 'msa.ok')
fetch('zero_shot_substitutions_scores.zip', 'zs.ok', dest=f'{P}/zs')
if not os.path.exists(f'{P}/DMS_substitutions.csv'):
os.system(f'curl -fsSL -o {P}/DMS_substitutions.csv https://raw.githubusercontent.com/OATML-Markslab/ProteinGym/main/reference_files/DMS_substitutions.csv')
if not os.path.isdir(f'{P}/af2'):
ok = False
for u in [f'{B}/ProteinGym_AF2_structures.zip', 'https://marks.hms.harvard.edu/proteingym/ProteinGym_AF2_structures.zip']:
if os.system(f'curl -fsSL -o {P}/af2.zip {u}') == 0: ok = True; break
assert ok, 'structure download failed'
zipfile.ZipFile(f'{P}/af2.zip').extractall(f'{P}/af2'); os.remove(f'{P}/af2.zip')
print('done')done
Pick the 31 Viral Assays
Filter benchmark reference to viral taxon
Filters the ProteinGym reference metadata down to the 31 viral deep mutational scanning (DMS) assays, indexing the alignment files, substitution datasets, and AlphaFold2 PDB coordinates.
ref = pd.read_csv(f'{P}/DMS_substitutions.csv')
viral = ref[ref['taxon'] == 'Virus'].reset_index(drop=True)
print('viral assays:', len(viral)); assert len(viral) == 31
# index of data files (skip the structure and official-score folders so names do not collide)
idx = {os.path.basename(p): p for p in glob.glob(f'{P}/**/*', recursive=True)
if os.path.isfile(p) and not p.startswith((f'{P}/af2', f'{P}/zs'))}
pdbs = {os.path.splitext(os.path.basename(p))[0]: p for p in glob.glob(f'{P}/af2/**/*.pdb', recursive=True)}
print(len(pdbs), 'structure files')viral assays: 31 217 structure files
The AminoGrapes Model Code
Complete closed-form scoring engine (zero fitted parameters on viral data)
This single cell is the complete AminoGrapes model. It executes in three core stages:
build_logf: Reads the multiple sequence alignment, down-weights near-duplicate sequences (similarity cutoff θ = 0.2), and computes position-wise log-odds amino acid frequencies with pseudocount λ = 0.01.struct_weights: Parses the AlphaFold2 PDB structure, calculates Shrake-Rupley solvent accessibility normalized by theoretical maximums (Tien et al. 2013), and computes residue burial: Burial = 1 − RSA.aminograpes_scores: Scores each mutant as:Score = ∑ [ Buriali × (log fi(mutant) − log fi(wildtype)) ]
from Bio import Align
from Bio.Align import substitution_matrices
from Bio.PDB import PDBParser, ShrakeRupley
from Bio.SeqUtils import seq1
AA = 'ACDEFGHIKLMNPQRSTVWY'; aidx = {a: i for i, a in enumerate(AA)}
LUT = np.full(256, 21, dtype=np.uint8)
for i, a in enumerate(AA): LUT[ord(a)] = i
LUT[ord('-')] = 20
LAM, THETA, MAXB = 0.01, 0.2, 8e9 # pseudocount, clustering cutoff, memory budget
MAXASA = dict(A=129, R=274, N=195, D=193, C=167, E=223, Q=225, G=104, H=224, I=197,
L=201, K=236, M=224, F=240, P=159, S=155, T=172, W=285, Y=263, V=174) # Tien et al. 2013, theoretical
def read_a2m(path):
seqs, cur = [], []
for line in open(path):
line = line.strip()
if line.startswith('>'):
if cur: seqs.append(''.join(cur)); cur = []
elif line: cur.append(line)
if cur: seqs.append(''.join(cur))
return [re.sub('[a-z.]', '', s) for s in seqs]
@torch.no_grad()
def gpu_weights(X, chunk=512):
N, L = X.shape # gap counts as its own state
Xt = torch.from_numpy(X.astype(np.int64)).cuda()
oh = torch.zeros(N, L, 21, dtype=torch.float16, device='cuda')
oh.scatter_(2, Xt.unsqueeze(2), 1.0); oh = oh.view(N, L * 21)
cnt = torch.zeros(N, device='cuda'); thr = (1 - THETA) * L
for s in range(0, N, chunk):
cnt[s:s + chunk] = ((oh[s:s + chunk] @ oh.T) >= thr).sum(1).float()
w = (1.0 / cnt.clamp(min=1)).cpu().numpy()
del oh, Xt; torch.cuda.empty_cache(); return w
def build_logf(msa_path, w_path):
seqs = read_a2m(msa_path); q = seqs[0]; L = len(q); seqs = [s for s in seqs if len(s) == L]
X = LUT[np.frombuffer(''.join(seqs).encode(), dtype=np.uint8).reshape(len(seqs), L)]
focus = np.array([c != '-' for c in q]); X = X[:, focus]; qf = q.replace('-', '')
keep = (X < 21).all(1) & ((X == 20).mean(1) <= 0.5)
wf = np.load(w_path).astype(np.float32)
if len(wf) == keep.sum(): X, w = X[keep], wf # ProteinGym weights match
elif len(wf) == len(X): w = wf
else: # recompute weights
X = X[keep]
if len(X) * X.shape[1] * 42 > MAXB:
n = int(MAXB / (X.shape[1] * 42)); X = X[np.sort(np.random.RandomState(0).choice(len(X), n, replace=False))]
w = gpu_weights(X)
c = np.stack([w @ (X == a) for a in range(20)], 1)
f = (1 - LAM) * c / np.maximum(c.sum(1, keepdims=True), 1e-9) + LAM / 20
return np.log(f), qf
aligner = Align.PairwiseAligner(); aligner.mode = 'global'
aligner.substitution_matrix = substitution_matrices.load('BLOSUM62')
aligner.open_gap_score = -10; aligner.extend_gap_score = -0.5; aligner.end_gap_score = 0.0
def map_positions(target, other):
aln = aligner.align(target, other)[0]; tmap = -np.ones(len(target), dtype=int)
for (t0, t1), (q0, q1) in zip(aln.aligned[0], aln.aligned[1]): tmap[t0:t1] = np.arange(q0, q1)
return tmap
def rsa_from_pdb(path):
model = PDBParser(QUIET=True).get_structure('x', path)[0]
ShrakeRupley().compute(model, level='R')
chain = next(model.get_chains()); seq, rsa = [], []
for res in chain:
if res.id[0] != ' ': continue
aa = seq1(res.get_resname())
if aa in MAXASA: seq.append(aa); rsa.append(min(1.0, res.sasa / MAXASA[aa]))
return ''.join(seq), np.array(rsa)
def find_struct(r):
for key in [r['DMS_id'], r['UniProt_ID']]:
if key in pdbs: return pdbs[key]
for k, p in pdbs.items():
if len(k) >= 6 and (r['UniProt_ID'] in k or k in r['DMS_id']): return p
return None
def struct_weights(r):
"burial = 1 - RSA per target position, or None if no usable structure"
tseq = r['target_seq']; sp = find_struct(r)
if not sp: return None
sseq, srsa = rsa_from_pdb(sp); sm = map_positions(tseq, sseq); m_ = sm >= 0
if m_.mean() < 0.3: return None
if np.mean([tseq[t] == sseq[sm[t]] for t in np.where(m_)[0]]) < 0.8: return None
rsa = np.full(len(tseq), np.nan); rsa[m_] = srsa[sm[m_]]; rsa[np.isnan(rsa)] = np.nanmean(rsa)
return 1 - rsa
def aminograpes_scores(r, mutants):
"returns (plain alignment score, Aminograpes score = alignment x burial, structure_used)"
tseq = r['target_seq']
logf, qf = build_logf(idx[r['MSA_filename']], idx[r['weight_file_name']])
tmap = map_positions(tseq, qf)
w_t = struct_weights(r); used = w_t is not None
if not used: w_t = np.full(len(tseq), 0.5) # constant weight: ranking equals plain alignment
plain, afr = [], []
for x in mutants:
a = b = 0.0
for m in x.split(':'):
wt, p, mt = m[0], int(m[1:-1]) - 1, m[-1]; q = tmap[p]
if q >= 0:
lor = logf[q, aidx[mt]] - logf[q, aidx[wt]]; a += lor; b += w_t[p] * lor
plain.append(a); afr.append(b)
return plain, afr, used
print('Aminograpes model code loaded')Aminograpes model code loaded
Score Viral Assays with ESM2-650M
Baseline Language Model Evaluation
Scores the 31 viral assays with Meta's ESM2-650M masked language model baseline (facebook/esm2_t33_650M_UR50D) using a 1,022-residue sliding window. All computed scores are cached to disk so interrupted sessions resume immediately.
from transformers import AutoTokenizer, EsmForMaskedLM
name = 'facebook/esm2_t33_650M_UR50D'
tok = AutoTokenizer.from_pretrained(name)
model = EsmForMaskedLM.from_pretrained(name, torch_dtype=torch.float16).cuda().eval()
aa_id = torch.tensor([tok.convert_tokens_to_ids(a) for a in AA]).cuda()
MAXL, BS = 1022, 16
@torch.no_grad()
def esm2_scores(seq, mutants):
L = len(seq); pos = sorted({int(m[1:-1]) - 1 for x in mutants for m in x.split(':')}); lp = {}
for s in range(0, len(pos), BS):
chunk = pos[s:s + BS]; wins = []
for p in chunk:
start = 0 if L <= MAXL else max(0, min(p - MAXL // 2, L - MAXL))
wins.append((seq[start:start + MAXL], p - start))
enc = tok([w for w, _ in wins], return_tensors='pt').to('cuda'); ids = enc['input_ids'].clone()
for r_, (w, rel) in enumerate(wins): ids[r_, rel + 1] = tok.mask_token_id
logits = model(input_ids=ids, attention_mask=enc['attention_mask']).logits
for r_, (w, rel) in enumerate(wins): lp[chunk[r_]] = logits[r_, rel + 1].float().log_softmax(-1)[aa_id].cpu()
out = []
for x in mutants:
s = 0.0
for m in x.split(':'):
wt, p, mt = m[0], int(m[1:-1]) - 1, m[-1]; s += (lp[p][aidx[mt]] - lp[p][aidx[wt]]).item()
out.append(s)
return out
for _, r in viral.iterrows():
f = f"{OUT}/esm2_{r['DMS_id']}.csv"
if os.path.exists(f): continue
df = pd.read_csv(idx[r['DMS_filename']], usecols=['mutant', 'DMS_score'])
df['esm2'] = esm2_scores(r['target_seq'], list(df.mutant)); df.to_csv(f, index=False)
print(f"{r['DMS_id']:45s} esm2 rho = {spearmanr(df.esm2, df.DMS_score).correlation:.3f}")Run AminoGrapes on 31 Viral Assays
Execute scoring and verify match against published run
Executes AminoGrapes on all 31 viral assays, computing both alignment-only log-odds and structure-weighted AminoGrapes scores.
EXPECTED_PLAIN = {"A0A140D2T1_ZIKV_Sourisseau_2019": 0.399, "A0A192B1T2_9HIV1_Haddox_2018": 0.471, "A0A2Z5U3Z0_9INFA_Doud_2016": 0.493, "A0A2Z5U3Z0_9INFA_Wu_2014": 0.452, "A4D664_9INFA_Soh_2019": 0.403, "C6KNH7_9INFA_Lee_2018": 0.417, "CAPSD_AAV2S_Sinai_2021": 0.447, "ENV_HV1B9_DuenasDecamp_2016": 0.319, "ENV_HV1BR_Haddox_2016": 0.307, "HCP_LAMBD_Tsuboyama_2023_2L6Q": 0.473, "I6TAH8_I68A0_Doud_2015": 0.355, "NCAP_I34A1_Doud_2015": 0.373, "NRAM_I33A0_Jiang_2016": 0.498, "PA_I34A1_Wu_2015": 0.522, "POLG_CXB3N_Mattenberger_2021": 0.416, "POLG_DEN26_Suphatrakul_2023": 0.507, "POLG_HCVJF_Qi_2014": 0.661, "POLG_PESV_Tsuboyama_2023_2MXD": 0.208, "Q2N0S5_9HIV1_Haddox_2018": 0.485, "R1AB_SARS2_Flynn_2022": 0.562, "RCRO_LAMBD_Tsuboyama_2023_1ORC": 0.377, "RDRP_I33A0_Li_2023": 0.371, "REV_HV1H2_Fernandes_2016": 0.239, "RPC1_BP434_Tsuboyama_2023_1R69": 0.726, "RPC1_LAMBD_Li_2019_high-expression": 0.284, "RPC1_LAMBD_Li_2019_low-expression": 0.243, "SPIKE_SARS2_Starr_2020_binding": 0.172, "SPIKE_SARS2_Starr_2020_expression": 0.203, "TAT_HV1BR_Fernandes_2016": 0.361, "VG08_BPP22_Tsuboyama_2023_2GP8": 0.373, "VRPI_BPT7_Tsuboyama_2023_2WNM": 0.055}
EXPECTED_AMINOGRAPES = {"A0A140D2T1_ZIKV_Sourisseau_2019": 0.399, "A0A192B1T2_9HIV1_Haddox_2018": 0.5, "A0A2Z5U3Z0_9INFA_Doud_2016": 0.545, "A0A2Z5U3Z0_9INFA_Wu_2014": 0.465, "A4D664_9INFA_Soh_2019": 0.287, "C6KNH7_9INFA_Lee_2018": 0.528, "CAPSD_AAV2S_Sinai_2021": 0.541, "ENV_HV1B9_DuenasDecamp_2016": 0.309, "ENV_HV1BR_Haddox_2016": 0.285, "HCP_LAMBD_Tsuboyama_2023_2L6Q": 0.602, "I6TAH8_I68A0_Doud_2015": 0.236, "NCAP_I34A1_Doud_2015": 0.281, "NRAM_I33A0_Jiang_2016": 0.569, "PA_I34A1_Wu_2015": 0.4, "POLG_CXB3N_Mattenberger_2021": 0.406, "POLG_DEN26_Suphatrakul_2023": 0.588, "POLG_HCVJF_Qi_2014": 0.661, "POLG_PESV_Tsuboyama_2023_2MXD": 0.263, "Q2N0S5_9HIV1_Haddox_2018": 0.525, "R1AB_SARS2_Flynn_2022": 0.641, "RCRO_LAMBD_Tsuboyama_2023_1ORC": 0.426, "RDRP_I33A0_Li_2023": 0.326, "REV_HV1H2_Fernandes_2016": 0.321, "RPC1_BP434_Tsuboyama_2023_1R69": 0.748, "RPC1_LAMBD_Li_2019_high-expression": 0.364, "RPC1_LAMBD_Li_2019_low-expression": 0.315, "SPIKE_SARS2_Starr_2020_binding": 0.333, "SPIKE_SARS2_Starr_2020_expression": 0.359, "TAT_HV1BR_Fernandes_2016": 0.42, "VG08_BPP22_Tsuboyama_2023_2GP8": 0.488, "VRPI_BPT7_Tsuboyama_2023_2WNM": 0.213}
rows = []
for _, r in viral.iterrows():
i = r['DMS_id']; f = f'{OUT}/both_{i}.csv'
if os.path.exists(f): df = pd.read_csv(f)
else:
df = pd.read_csv(f'{OUT}/esm2_{i}.csv')
df['aminograpes_plain'], df['aminograpes'], used = aminograpes_scores(r, df.mutant); df['struct_used'] = used; df.to_csv(f, index=False)
rows.append((i, bool(df.struct_used.iloc[0]), spearmanr(df.esm2, df.DMS_score).correlation,
spearmanr(df.aminograpes_plain, df.DMS_score).correlation, spearmanr(df.aminograpes, df.DMS_score).correlation))
print(f"{i[:36]:36s} struct={'Y' if rows[-1][1] else 'N'} | ESM2 {rows[-1][2]:6.3f} | alignment {rows[-1][3]:6.3f} | AMINOGRAPES {rows[-1][4]:6.3f}")
res = pd.DataFrame(rows, columns=['DMS_id', 'struct', 'esm2_ours', 'plain', 'aminograpes']).set_index('DMS_id')
res.to_csv(f'{OUT}/aminograpes_viral_per_assay.csv')
print(f"\nstructures used for {int(res.struct.sum())} of 31 assays")
print(f"MEAN: our ESM2 {res.esm2_ours.mean():.4f} | alignment only {res.plain.mean():.4f} | AMINOGRAPES {res.aminograpes.mean():.4f} (expected 0.2693 | 0.3926 | 0.4304)")
dp = max(abs(res.plain[k] - v) for k, v in EXPECTED_PLAIN.items()); da = max(abs(res.aminograpes[k] - v) for k, v in EXPECTED_AMINOGRAPES.items())
print(f"largest per-assay difference from the published run: alignment {dp:.4f}, Aminograpes {da:.4f} ->", 'MATCHES' if max(dp, da) < 0.006 else 'CHECK: differs from published run')structures used for 29 of 31 assays MEAN: our ESM2 0.2693 | alignment only 0.3926 | AMINOGRAPES 0.4304 (expected 0.2693 | 0.3926 | 0.4304) largest per-assay difference from the published run: alignment 0.0004, Aminograpes 0.0004 -> MATCHES
Compare with 95 Official ProteinGym Models
Cross-model leaderboard evaluation across all 31 viral targets
Evaluates all 95 published models from the official ProteinGym zero-shot release that have complete scores across all 31 viral assays, computing plain per-assay Spearman correlations identical to the primary benchmark.
root = f'{P}/zs'
files = {os.path.splitext(os.path.basename(p))[0]: p for p in glob.glob(f'{root}/**/*.csv', recursive=True)}
assert all(i in files for i in viral.DMS_id), 'some official score files missing'
SKIP = {'DMS_score', 'DMS_score_bin', 'mutant', 'mutated_sequence', 'DMS_id'}
rows_ = {}
for i, f in zip(viral.DMS_id, viral.DMS_filename):
g = pd.read_csv(idx[f], usecols=['mutant', 'DMS_score']).set_index('mutant').DMS_score
d = pd.read_csv(files[i]).drop_duplicates('mutant').set_index('mutant').reindex(g.index)
r_ = {}
for c in d.columns:
if c in SKIP or not pd.api.types.is_numeric_dtype(d[c]): continue
ok = d[c].notna()
if ok.mean() >= 0.9: r_[c] = spearmanr(d[c][ok], g[ok]).correlation
rows_[i] = r_
Mo = pd.DataFrame(rows_).T.dropna(axis=1).reindex(res.index)
Mo.to_csv(f'{OUT}/official_viral_spearman_all_models.csv')
means = Mo.mean().sort_values(ascending=False)
print(len(means), 'official models with scores for all 31 viral assays\n')
print('Top 10 official models (plain mean over 31 viral assays):'); print(means.head(10).round(4).to_string())
n_above = int((means > res.aminograpes.mean()).sum())
print(f'\nOfficial models scoring above Aminograpes: {n_above} of {len(means)}')
print(f'Official ESM2-650M mean: {Mo.ESM2_650M.mean():.4f} (expected 0.2687) | RSALOR {Mo.RSALOR.mean():.4f} (0.4797) | GEMME {Mo.GEMME.mean():.4f} | PoET {Mo.PoET.mean():.4f}')
print('Our ESM2 rerun vs official: per-assay agreement r = %.3f (expected 1.000)' % np.corrcoef(Mo.ESM2_650M, res.esm2_ours)[0, 1])
print(means[[c for c in means.index if c.startswith('ESM2')]].round(4).to_string())Confidence Intervals & Statistical Significance
10,000 bootstrap resamples and Wilcoxon signed-rank testing
Performs bootstrap resampling over assays (10,000 iterations, fixed random seed = 0) and computes non-parametric Wilcoxon signed-rank tests for statistical hypothesis validation.
def boot(rng, d):
b = np.array([rng.choice(d, len(d)).mean() for _ in range(10000)]); return np.percentile(b, [2.5, 97.5])
rng = np.random.default_rng(0)
for nm, a, b in [('Aminograpes - alignment only (burial effect)', res.aminograpes, res.plain),
('Aminograpes - official ESM2-650M', res.aminograpes, Mo.ESM2_650M),
('Aminograpes - official RSALOR', res.aminograpes, Mo.RSALOR)]:
d = (a - b).values; lo, hi = boot(rng, d)
print(f'{nm}: {d.mean():+.4f}, 95% CI [{lo:.4f}, {hi:.4f}], wins {(d > 0).sum()}/31, Wilcoxon p={wilcoxon(d).pvalue:.4f}')
d = (res.plain - Mo.ESM2_650M).values; lo, hi = boot(np.random.default_rng(0), d)
print(f'alignment only - official ESM2-650M: {d.mean():+.4f}, 95% CI [{lo:.4f}, {hi:.4f}], wins {(d > 0).sum()}/31')
print('\nAssays where official ESM2-650M beats Aminograpes:', list(res.index[(res.aminograpes - Mo.ESM2_650M) <= 0]))
print('Note: bootstrap CIs can differ in the 3rd decimal if the comparisons are run in a different order.')Aminograpes - alignment only (burial effect): +0.0378, 95% CI [0.0152, 0.0610], wins 22/31, Wilcoxon p=0.0012 Aminograpes - official ESM2-650M: +0.1617, 95% CI [0.0775, 0.2450], wins 25/31, Wilcoxon p=0.0008 Aminograpes - official RSALOR: -0.0493, 95% CI [-0.0931, -0.0084], wins 7/31, Wilcoxon p=0.0177 alignment only - official ESM2-650M: +0.1239, 95% CI [0.0461, 0.2014], wins 23/31
ESM2 Blend Weight Tuning on Non-Viral Assays
Unsupervised zero-shot blend tuning (Optional)
Tunes the blend weight between AminoGrapes and ESM2 on a development subset of 30 non-viral assays, then applies that frozen parameter (λ = 0.7) to the 31 viral targets.
cand = ref[(ref['taxon'] != 'Virus') & (ref['seq_len'] <= 400)].sample(frac=1, random_state=0)
tune = []
for _, r in cand.iterrows():
if len(pd.read_csv(idx[r['DMS_filename']], usecols=['mutant'])) <= 6000: tune.append(r)
if len(tune) == 30: break
z = lambda x: (x - x.mean()) / (x.std() + 1e-9)
tdfs = []
for r in tune:
i = r['DMS_id']; f = f'{OUT}/nv_{i}.csv'
if os.path.exists(f): tdfs.append(pd.read_csv(f)); continue
w_t = struct_weights(r)
if w_t is None: print('no usable structure, excluded:', i); continue
df = pd.read_csv(idx[r['DMS_filename']], usecols=['mutant', 'DMS_score'])
df['esm2'] = esm2_scores(r['target_seq'], list(df.mutant))
logf, qf = build_logf(idx[r['MSA_filename']], idx[r['weight_file_name']]); tmap = map_positions(r['target_seq'], qf)
out_ = []
for x in df.mutant:
b = 0.0
for m in x.split(':'):
wt, p, mt = m[0], int(m[1:-1]) - 1, m[-1]; q = tmap[p]
if q >= 0: b += w_t[p] * (logf[q, aidx[mt]] - logf[q, aidx[wt]])
out_.append(b)
df['aminograpes'] = out_; df.to_csv(f, index=False); tdfs.append(df)
print(f'{i[:40]:40s} esm2={spearmanr(df.esm2, df.DMS_score).correlation:6.3f} aminograpes={spearmanr(df.aminograpes, df.DMS_score).correlation:6.3f}')
Wg = np.round(np.arange(0, 1.01, 0.1), 1)
curve = lambda dfs: np.array([np.mean([spearmanr(w * z(d.esm2) + (1 - w) * z(d.aminograpes), d.DMS_score).correlation for d in dfs]) for w in Wg])
cn = curve(tdfs); w_star = float(Wg[int(np.argmax(cn))])
vd = {i: pd.read_csv(f'{OUT}/both_{i}.csv') for i in viral.DMS_id}
S = pd.DataFrame({i: [spearmanr(w * z(d.esm2) + (1 - w) * z(d.aminograpes), d.DMS_score).correlation for w in Wg] for i, d in vd.items()}, index=Wg)
print(f'\nNON-VIRAL ({len(tdfs)} assays): best weight on ESM2 = {w_star} (expected 0.7)')
print('weight on ESM2 :', ' '.join(f'{w:5.1f}' for w in Wg))
print('non-viral mean :', ' '.join(f'{c:5.3f}' for c in cn))
print('viral mean :', ' '.join(f'{c:5.3f}' for c in S.mean(axis=1)), ' <- display only')
fin = S.loc[w_star]; alo = S.loc[0.0]
print(f'\nFINAL blend (weight {w_star}): {fin.mean():.4f} (expected 0.3764)')
rng = np.random.default_rng(0)
for nm, a, b in [('blend - official ESM2-650M', fin, Mo.ESM2_650M), ('blend - Aminograpes alone', fin, alo), ('blend - official RSALOR', fin, Mo.RSALOR)]:
d = (a - b).values; lo, hi = boot(rng, d)
print(f'{nm}: {d.mean():+.4f}, 95% CI [{lo:.4f}, {hi:.4f}], wins {(d > 0).sum()}/31, p={wilcoxon(d).pvalue:.4f}')
up = viral.set_index('DMS_id').UniProt_ID
lopo = pd.Series({i: S.loc[S[[j for j in S.columns if up[j] != up[i]]].mean(axis=1).idxmax(), i] for i in S.columns})
print(f'sensitivity: weight chosen on other viral proteins (leave-one-protein-out): {lopo.mean():.4f} (expected about 0.4408)')Public Models Ensemble Analysis
Contextual stack of published methods (Optional)
Combines top published models (VenusREM, ProSST-4096, S3F_MSA, ProSST-1024, RSALOR) chosen strictly on 186 non-viral assays and evaluated on viral targets (ensemble mean = 0.5732).
from scipy.stats import rankdata
isviral = ref.set_index('DMS_id').taxon.eq('Virus')
SKIP2 = {'mutant', 'mutated_sequence', 'DMS_score', 'DMS_score_bin'}
data, comp = {}, {}
for i in ref.DMS_id:
if i not in files: continue
d = pd.read_csv(files[i]).dropna(subset=['DMS_score'])
if not isviral[i] and len(d) > 3000: d = d.sample(3000, random_state=0)
cols = [c for c in d.columns if c not in SKIP2 and pd.api.types.is_numeric_dtype(d[c])]
X_ = d[cols]; comp[i] = set(X_.columns[X_.notna().all()])
data[i] = (((X_ - X_.mean()) / (X_.std() + 1e-9)).fillna(0).astype('float32'), d.DMS_score.values)
VI = [i for i in data if isviral[i]]; NV = [i for i in data if not isviral[i]]
cols_all = sorted(set.intersection(*[set(Z.columns) for Z, _ in data.values()]))
pool = [m for m in cols_all if all(m in comp[i] for i in VI) and np.mean([m in comp[i] for i in NV]) >= 0.9]
print(f'{len(NV)} non-viral / {len(VI)} viral assays; candidate pool = {len(pool)} models')
A_ = {i: data[i][0][pool].values for i in data}
Yr = {i: (lambda r_: (r_ - r_.mean()) / (r_.std() + 1e-12))(rankdata(data[i][1])) for i in data}
col = {m: k for k, m in enumerate(pool)}
def rho_(i, members):
s = A_[i][:, [col[m] for m in members]].sum(1); r_ = rankdata(s)
return float(np.dot((r_ - r_.mean()) / (r_.std() + 1e-12), Yr[i]) / len(r_))
mean_rho = lambda ids_, members: np.mean([rho_(i, members) for i in ids_])
sel, best = [], -1.0
while len(sel) < 5:
s_, m_ = max((mean_rho(NV, sel + [m]), m) for m in pool if m not in sel)
if s_ <= best + 0.002: break
sel.append(m_); best = s_; print(f' + {m_:22s} -> non-viral mean {s_:.4f}')
print('selected on non-viral only:', sel)
fin_e = pd.Series({i: rho_(i, sel) for i in VI}); Mv = Mo.reindex(VI)
print(f'\nVIRAL: ensemble {fin_e.mean():.4f} (expected 0.5732) | best single official {Mv.mean().max():.4f} ({Mv.mean().idxmax()}) | Aminograpes {res.aminograpes.mean():.4f}')
rng = np.random.default_rng(0)
for nm, s_ in [('ESM2-650M', Mv.ESM2_650M), ('RSALOR', Mv.RSALOR), ('PoET', Mv.PoET), (f'best single ({Mv.mean().idxmax()})', Mv[Mv.mean().idxmax()])]:
d = (fin_e - s_).values; lo, hi = boot(rng, d)
print(f'ensemble - {nm}: {d.mean():+.4f}, 95% CI [{lo:.4f}, {hi:.4f}], wins {(d > 0).sum()}/{len(d)}')
# does adding the Aminograpes score help the ensemble?
WA = [0, 0.5, 1, 2, 3]
def per_assay(i, afr_df):
d = pd.read_csv(files[i]).drop_duplicates('mutant').set_index('mutant')
a = afr_df.drop_duplicates('mutant').set_index('mutant')
j = d[sel + ['DMS_score']].join(a[['aminograpes']], how='inner').dropna(); base = sum(z(j[m]) for m in sel)
return [spearmanr(base + w * z(j.aminograpes), j.DMS_score).correlation for w in WA]
nv = pd.DataFrame({os.path.basename(p)[3:-4]: per_assay(os.path.basename(p)[3:-4], pd.read_csv(p)) for p in glob.glob(f'{OUT}/nv_*.csv')}, index=WA)
vi = pd.DataFrame({i: per_assay(i, vd[i]) for i in VI}, index=WA)
print('\nweight on Aminograpes score :', ' '.join(f'{w:6.1f}' for w in WA))
print('non-viral mean :', ' '.join(f'{c:6.4f}' for c in nv.mean(axis=1)))
print('viral mean (display) :', ' '.join(f'{c:6.4f}' for c in vi.mean(axis=1)))
print('weight chosen on non-viral:', float(nv.mean(axis=1).idxmax()), ' (expected 0.0: Aminograpes adds nothing to the ensemble)')Save Results Archive
Export all per-assay CSV files and summary tables
Packages all generated predictions, per-assay correlation CSVs, and model matrices into a single zip archive (aminograpes_results.zip) for local inspection and verification.
shutil.make_archive('/content/aminograpes_results', 'zip', OUT)
print('saved /content/aminograpes_results.zip')
try:
from google.colab import files as _f; _f.download('/content/aminograpes_results.zip')
except Exception as e: print('download it from the Files panel on the left')saved /content/aminograpes_results.zip
Ready to reproduce the evaluation?
Download the complete Aminograpes_reproduce.ipynb notebook to run the pipeline locally or in Google Colab.