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