"""Where in Ramachandran space does a uniform backbone close into a ring? A uniform (phi,psi) generates a helix: the residue transform is a screw with rotation Omega about an axis and a rise d along it. The chain closes into a flat N-ring iff rise = 0 and Omega = 2*pi*k/N for some winding number k. So closure is not a free parameter. It is a LOCUS, one curve per (N,k). RESULT: across N = 5..16 and all k, there are 171 solutions, and every one satisfies psi = phi to within +/- 9.6 deg. Ring closure is a LINE, the diagonal, running corner to corner. And nothing populated sits on it: alpha-R 20 deg off beta 110 deg off alpha-L 10 deg off PPII 140 deg off Gly-bridge 90 deg off Pro-II 150 deg off No cyclic peptide can be built from residues all sitting in their preferred geometry. Strain is not a design failure, it is mandatory. CAVEAT: this is derived for a UNIFORM backbone. Real cyclic peptides are not uniform, so the line does not apply to them directly. Testing real dihedrals against it gives 19.6% within +/- 30 deg against a 16.7% uniform null, KS p = 0.075, i.e. nothing. The per-residue generalisation is the 2N-6 dimensional closure manifold, not a line. """ import numpy as np from scipy import optimize B = {'N_CA': 1.458, 'CA_C': 1.525, 'C_N': 1.329} A = {'N_CA_C': np.deg2rad(111.2), 'CA_C_N': np.deg2rad(116.2), 'C_N_CA': np.deg2rad(121.7)} def Rx(a): c, s = np.cos(a), np.sin(a); return np.array([[1,0,0],[0,c,-s],[0,s,c]]) def Rz(a): c, s = np.cos(a), np.sin(a); return np.array([[c,-s,0],[s,c,0],[0,0,1]]) def H(R, t): T = np.eye(4); T[:3,:3] = R; T[:3,3] = t; return T def T_res(phi, psi, omega=np.pi): return (H(Rz(phi), [B['N_CA'],0,0]) @ H(Rz(psi) @ Rx(np.pi - A['N_CA_C']), [B['CA_C'],0,0]) @ H(Rz(omega) @ Rx(np.pi - A['CA_C_N']), [B['C_N'],0,0]) @ H(Rx(np.pi - A['C_N_CA']), [0,0,0])) def so3_log(R): c = np.clip((np.trace(R)-1)/2, -1, 1); th = np.arccos(c) if th < 1e-9: return np.zeros(3) K = (R - R.T)/(2*np.sin(th)) return th*np.array([K[2,1], K[0,2], K[1,0]]) def screw(phi, psi): """(rotation per residue, rise per residue) for a uniform backbone.""" T = T_res(phi, psi); w = so3_log(T[:3,:3]); Om = np.linalg.norm(w) if Om < 1e-9: return 0.0, np.linalg.norm(T[:3,3]) ax = w/Om return Om, float(T[:3,3] @ ax) def resid(x, tgt): Om, ri = screw(x[0], x[1]); return [ri, Om - tgt] if __name__ == "__main__": rng = np.random.default_rng(1); out = [] print(f" {'N':>4}{'k':>3}{'Omega':>9}{'#sol':>6} representative (phi,psi) deg") for N in range(5, 17): for k in range(1, N//2 + 1): tgt = 2*np.pi*k/N if tgt > np.pi: tgt = 2*np.pi - tgt sols = [] for _ in range(250): x0 = rng.uniform(-np.pi, np.pi, 2) x, info, ier, _ = optimize.fsolve(resid, x0, args=(tgt,), full_output=True) if ier == 1 and max(abs(np.array(resid(x, tgt)))) < 1e-9: x = (x + np.pi) % (2*np.pi) - np.pi if not any(np.allclose(x, y, atol=2e-3) for y in sols): sols.append(x) if sols: out += [(N, k, a, b) for a, b in sols] ex = ", ".join(f"({np.degrees(a):.0f},{np.degrees(b):.0f})" for a, b in sols[:2]) print(f" {N:>4}{k:>3}{np.degrees(tgt):>8.1f}d{len(sols):>6} {ex}") d = np.degrees([((b - a + np.pi) % (2*np.pi)) - np.pi for _, _, a, b in out]) print(f"\n {len(out)} solutions. psi - phi: mean {d.mean():+.3f} deg, " f"sd {d.std():.3f}, range {d.min():+.2f} to {d.max():+.2f}") print(" -> the closure locus is the diagonal psi = phi.")