rOGUEgRINGO's picture
Ring closure is a line in Ramachandran space: psi = phi, 171 solutions
9a7f20d verified
Raw
History Blame Contribute Delete
3.78 kB
"""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.")