| """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)) |
|
|
| |
| 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}") |
|
|