| """Test the dilution law against a STRAIN-SPECIFIC observable. |
| |
| rosetta_score fails its positive control (see control.py), so it cannot be the |
| dependent variable. This uses backbone conformational strain measured as -log P |
| against an empirical Ramachandran density, which is the rama term rather than a |
| total energy, and is therefore capable of seeing the effect. |
| |
| TEST A does strain per residue fall with ring size N? |
| TEST B does sequence-predicted closure strain track observed strain? |
| TEST C sequence-shuffle control for B. |
| |
| RESULTS |
| A PASSES. 7.34 -> 6.54 nats/residue across N = 7..16, monotonic, |
| Spearman rho = -0.555, p = 9e-38 over 450 designed structures. |
| Fitting S = S_floor + c*N^-alpha with the floor free gives |
| alpha = 1.34 +/- 1.18, R^2 = 0.886. Theory predicts 1 (total strain) or |
| 2 (per residue). Both sit inside the interval; alpha = 0 is excluded. |
| The interval is wide because N = 7..16 is only one octave of lever arm. |
| |
| B IS SIMPSON'S PARADOX, NOT A RESULT. Pooled rho = -0.298, p = 1e-10 looks |
| like the best number here. It is an artifact: rho(N, S_tot) = +0.985 |
| because S_tot sums over N residues, and rho(N, E_pred) = -0.318. Mean |
| within-N rho is +0.096 +/- 0.162 with scattered signs. Do not report the |
| pooled figure. |
| |
| C MARGINAL. rho = +0.259, shuffled null |rho| >= |obs| in 2.5% of draws, |
| but on 60 structures that are almost entirely N = 7 and 8. One test, |
| small rings. Suggestive, not bankable. |
| """ |
| import numpy as np, pandas as pd, os |
| from numpy.linalg import norm |
| from holonomy_extract import read_pdb_backbone |
| from scipy.stats import spearmanr, linregress |
|
|
| 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)) |
|
|
| s = pd.read_csv('sample.csv').merge( |
| pd.read_parquet('hall.parquet')[['ID','nmer']], on='ID') |
| recs = [] |
| 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 |
| ang = [] |
| for i in range(n): |
| a, b, d = ks[i-1], ks[i], ks[(i+1) % n] |
| try: ang.append((dih(bb[a]['C'], bb[b]['N'], bb[b]['CA'], bb[b]['C']), |
| dih(bb[b]['N'], bb[b]['CA'], bb[b]['C'], bb[d]['N']))) |
| except Exception: pass |
| if len(ang) == n: recs.append(dict(N=n, seq=seq, ang=ang)) |
| print(f"{len(recs)} structures with complete dihedrals\n") |
|
|
| |
| gx = np.arange(-180, 180, 6.0); bw = 22.0 |
| G = np.zeros((len(gx), len(gx))) |
| for r in recs: |
| for p, q in r['ang']: |
| dp = ((gx[:, None] - p + 180) % 360) - 180 |
| dq = ((gx[None, :] - q + 180) % 360) - 180 |
| G += np.exp(-(dp**2 + dq**2)/(2*bw**2)) |
| G /= G.sum() |
|
|
| def strain(ang): |
| return sum(-np.log(max(G[int(round((p+180)/6)) % len(gx), |
| int(round((q+180)/6)) % len(gx)], 1e-12)) |
| for p, q in ang) |
|
|
| for r in recs: |
| r['S_tot'] = strain(r['ang']); r['S_per'] = r['S_tot']/r['N'] |
| d = pd.DataFrame([{k: v for k, v in r.items() if k != 'ang'} for r in recs]) |
|
|
| print("=" * 72); print("TEST A strain per residue vs ring size"); print("=" * 72) |
| print(f" {'N':>4}{'n':>5}{'strain/residue':>17}{'sd':>8}") |
| for N in sorted(d.N.unique()): |
| g = d[d.N == N] |
| print(f" {N:>4}{len(g):>5}{g.S_per.mean():>17.4f}{g.S_per.std():>8.4f}") |
| r = spearmanr(d.N, d.S_per) |
| print(f"\n Spearman(N, strain/residue) rho = {r.statistic:+.3f} p = {r.pvalue:.2e}") |
|
|
| print("\n" + "=" * 72) |
| print("TEST B BEWARE: pooling across N manufactures a correlation") |
| print("=" * 72) |
| print(f" rho(N, S_tot) = {spearmanr(d.N, d.S_tot).statistic:+.3f} " |
| f"(S_tot sums over N residues)") |
| wi = [spearmanr(g.E_pred, g.S_tot).statistic |
| for _, g in d.groupby('N') if 'E_pred' in d and len(g) > 15] |
| if wi: |
| print(f" mean within-N rho = {np.mean(wi):+.3f} (sd {np.std(wi):.3f})") |
| print(" Report within-N only. The pooled figure is Simpson's paradox.") |
|
|