rOGUEgRINGO's picture
Positive control: rosetta_score fails to track backbone strain (justifies R1)
1a9fdda verified
Raw
History Blame Contribute Delete
3.74 kB
"""POSITIVE CONTROL. Run this before believing any null against rosetta_score.
Three tests came back null against rosetta_score. Before concluding anything
from that, check whether the score responds to backbone strain AT ALL.
It does not. The most direct possible strain measure -- raw dihedral deviation
from the nearest populated Ramachandran basin -- gives:
mean dihedral deviation rho = +0.105 p = 0.03
max dihedral deviation rho = +0.004 p = 0.93
closure-Jacobian sigma_6 rho = -0.025 p = 0.61
with every per-N correlation non-significant and signs flipping from -0.18 to
+0.25. Rosetta total score is dominated by van der Waals packing, solvation,
H-bonding and rotamer terms; the backbone dihedral contribution is a thin slice,
and these structures were relaxed before scoring, which compresses the range
further.
CONSEQUENCE: the nulls were uninformative, not falsifying. This file is the
evidence behind retraction R1 in FINDINGS.md. A strain-specific observable is
required instead: the rama term alone, ring strain as cyclized minus relaxed
linear, or experimental cyclization yields.
"""
import numpy as np, pandas as pd, os
from numpy.linalg import norm, svd
from holonomy_extract import read_pdb_backbone, closure_jacobian
from scipy.stats import spearmanr
def dih(p0, p1, p2, p3):
b0, b1, b2 = p0-p1, p2-p1, p3-p2; b1 = b1/norm(b1)
v = b0 - (b0 @ b1)*b1; w = b2 - (b2 @ b1)*b1
return np.degrees(np.arctan2(np.cross(b1, v) @ w, v @ w))
# populated basins by residue class
BAS = {'X': [(-63,-43), (-120,130), (-75,145)],
'G': [(-82,8), (82,-8), (-63,-43), (63,43)],
'P': [(-65,145), (-65,-30)]}
def dev(res, phi, psi):
c = res if res in 'GP' else 'X'
return min(np.hypot(((phi-a+180) % 360)-180, ((psi-b+180) % 360)-180)
for a, b in BAS[c])
s = pd.read_csv('sample.csv').merge(
pd.read_parquet('hall.parquet')[['ID','nmer']], on='ID')
rows = []
for _, t in s.iterrows():
f = "pdbs/" + os.path.basename(t['hf_path'])
if not os.path.exists(f): continue
bb = read_pdb_backbone(f)
ks = sorted(k for k in bb if {'N','CA','C'} <= set(bb[k]))
n = len(ks); seq = t['sequence']
if n < 6 or len(seq) != n: continue
ds = []
for i in range(n):
a, b, d = ks[i-1], ks[i], ks[(i+1) % n]
try:
ph = dih(bb[a]['C'], bb[b]['N'], bb[b]['CA'], bb[b]['C'])
ps = dih(bb[b]['N'], bb[b]['CA'], bb[b]['C'], bb[d]['N'])
ds.append(dev(seq[i], ph, ps))
except Exception: pass
if len(ds) < n-1: continue
J, _ = closure_jacobian({k: bb[k] for k in ks}); sv = svd(J, compute_uv=False)
rows.append(dict(ID=t['ID'], N=n, score=t['rosetta_score'],
dev_mean=float(np.mean(ds)), dev_max=float(np.max(ds)),
sigma6=float(sv[5]), cond=float(sv[0]/sv[5])))
d = pd.DataFrame(rows); q = d[d.score < d.score.quantile(.95)]
print(f"{len(d)} structures, {len(q)} after trimming the top 5% of scores\n")
print("=" * 72); print("Does rosetta_score see backbone strain?"); print("=" * 72)
print(f" {'predictor':<28}{'Spearman rho':>14}{'p':>12}")
for c, lab in [('dev_mean','mean dihedral deviation'),
('dev_max','max dihedral deviation'),
('sigma6','closure-Jacobian sigma_6'),
('cond','condition number')]:
r = spearmanr(q[c], q.score)
print(f" {lab:<28}{r.statistic:>+14.3f}{r.pvalue:>12.2e}")
print("\n per ring size, mean deviation vs score:")
print(f" {'N':>4}{'n':>5}{'rho':>9}{'p':>10}")
for N in sorted(q.N.unique()):
g = q[q.N == N]
if len(g) < 15: continue
r = spearmanr(g.dev_mean, g.score)
print(f" {N:>4}{len(g):>5}{r.statistic:>+9.3f}{r.pvalue:>10.3f}")