Research-Stack/5-Applications/scripts/codon_peptide_rl_simulation_v4.py

403 lines
13 KiB
Python

from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
# =========================================================
# OTOM V4 Python Simulator (clean step-by-step version)
# Step layering:
# 0. Base codon->peptide score
# 1. Cotranslational visible prefix
# 2. Pause field + dwell time
# 3. Exposed tail window
# 4. Contact kinetics
# 5. Transient codon structural bias (optional ablation)
# =========================================================
# -----------------------------
# Shared toy biology definitions
# -----------------------------
CODON_TABLE = {
"GCU": "A", "GCC": "A", "GCA": "A", "GCG": "A",
"UUU": "F", "UUC": "F",
"GGU": "G", "GGC": "G", "GGA": "G", "GGG": "G",
}
AA_TO_IDX = {"A": 0, "F": 1, "G": 2}
AA_TO_CODONS = {}
for c, aa in CODON_TABLE.items():
AA_TO_CODONS.setdefault(aa, []).append(c)
for aa in AA_TO_CODONS:
AA_TO_CODONS[aa] = sorted(AA_TO_CODONS[aa])
DEGENERACY = {c: sum(1 for x in CODON_TABLE.values() if x == aa) for c, aa in CODON_TABLE.items()}
TRIPLET_STABILITY = {
"GCU": 0.90, "GCC": 0.95, "GCA": 0.82, "GCG": 0.88,
"UUU": 0.76, "UUC": 0.81,
"GGU": 0.72, "GGC": 0.86, "GGA": 0.67, "GGG": 0.61,
}
CONSERVATION = {
"GCU": 0.72, "GCC": 0.80, "GCA": 0.58, "GCG": 0.64,
"UUU": 0.75, "UUC": 0.83,
"GGU": 0.60, "GGC": 0.79, "GGA": 0.55, "GGG": 0.48,
}
TRANSLATION_EFF = {
"GCU": 0.78, "GCC": 0.90, "GCA": 0.64, "GCG": 0.70,
"UUU": 0.73, "UUC": 0.85,
"GGU": 0.66, "GGC": 0.88, "GGA": 0.52, "GGG": 0.45,
}
LOCAL_ENTROPY = {
"GCU": 0.36, "GCC": 0.28, "GCA": 0.44, "GCG": 0.39,
"UUU": 0.41, "UUC": 0.30,
"GGU": 0.48, "GGC": 0.31, "GGA": 0.53, "GGG": 0.58,
}
MUTATION_PENALTY = {
"GCU": 0.20, "GCC": 0.12, "GCA": 0.30, "GCG": 0.24,
"UUU": 0.22, "UUC": 0.15,
"GGU": 0.33, "GGC": 0.18, "GGA": 0.37, "GGG": 0.42,
}
TRANSLATION_SPEED = {
"GCU": 0.82, "GCC": 1.05, "GCA": 0.68, "GCG": 0.76,
"UUU": 0.72, "UUC": 0.95,
"GGU": 0.66, "GGC": 0.98, "GGA": 0.58, "GGG": 0.50,
}
CODON_BIAS = {
"GCU": np.array([ 0.03, 0.00, -0.01]),
"GCC": np.array([ 0.07, -0.01, -0.02]),
"GCA": np.array([-0.02, 0.01, 0.03]),
"GCG": np.array([ 0.01, 0.00, 0.01]),
"UUU": np.array([-0.01, 0.05, -0.01]),
"UUC": np.array([ 0.00, 0.07, -0.02]),
"GGU": np.array([-0.02, -0.01, 0.05]),
"GGC": np.array([-0.01, 0.00, 0.06]),
"GGA": np.array([-0.03, -0.01, 0.04]),
"GGG": np.array([-0.03, 0.00, 0.03]),
}
# -----------------------------
# Model parameters
# -----------------------------
w_rho, w_q, w_tau, w_H, w_eps = 1.0, 0.8, 0.9, 0.7, 0.75
lambda_deg, C0_codon = 0.35, 1.5
speed_cost_mu = 0.45
dt = 0.04
noise_scale = 0.04
alpha_rl = 0.55
temperature = 1.0
C0_peptide = 8.0
# expert basins
mu_helix = np.array([-1.0, -0.7])
mu_sheet = np.array([-2.2, 2.2])
mu_loop = np.array([ 0.8, 0.8])
# -----------------------------
# Helper functions
# -----------------------------
def softmax(z):
z = np.asarray(z, dtype=float)
z = z - np.max(z)
e = np.exp(z)
return e / e.sum()
def wrap_angle(x):
return (x + np.pi) % (2 * np.pi) - np.pi
def angular_delta(a, b):
return wrap_angle(a - b)
def expert_potential(theta, mu, alpha):
d = angular_delta(theta, mu)
return 0.5 * alpha * np.dot(d, d)
def expert_gradient(theta, mu, alpha):
return alpha * angular_delta(theta, mu)
def torsion_energy(theta):
return 0.16 * (2 - np.cos(theta[0]) - np.cos(theta[1]))
def conformational_entropy(theta):
return np.log1p(1.8 + 0.9 * np.exp(-0.5 * np.dot(theta - mu_loop, theta - mu_loop)))
def peptide_expert_energies(theta):
return np.array([
expert_potential(theta, mu_helix, 1.35),
expert_potential(theta, mu_sheet, 1.20),
expert_potential(theta, mu_loop, 0.75),
])
def peptide_expert_gradients(theta):
return np.vstack([
expert_gradient(theta, mu_helix, 1.35),
expert_gradient(theta, mu_sheet, 1.20),
expert_gradient(theta, mu_loop, 0.75),
])
def structural_coherence(theta):
d_helix = np.linalg.norm(angular_delta(theta, mu_helix))
d_sheet = np.linalg.norm(angular_delta(theta, mu_sheet))
return 1.0 / (1.0 + min(d_helix, d_sheet))
def phi_codon(codon: str) -> float:
num = (
w_rho * TRIPLET_STABILITY[codon] +
w_q * CONSERVATION[codon] +
w_tau * TRANSLATION_EFF[codon] -
w_H * LOCAL_ENTROPY[codon] -
w_eps * MUTATION_PENALTY[codon]
)
denom = (
np.log(64.0) +
lambda_deg * np.log(float(DEGENERACY[codon])) +
speed_cost_mu / TRANSLATION_SPEED[codon] +
C0_codon
)
return num / denom
def visible_prefix(codons, translated_count):
return codons[:translated_count]
def exposed_tail(codons, translated_count, Lexp=2):
vp = visible_prefix(codons, translated_count)
return vp[-Lexp:]
def pause_intensity(codon):
rarity = 1.0 - TRANSLATION_EFF[codon]
return 0.5 / TRANSLATION_SPEED[codon] + 0.25 * rarity
def transient_bias(recent_codons, age_weights, use_bias):
if not use_bias or len(recent_codons) == 0:
return np.zeros(3)
accum = np.zeros(3)
for c, w in zip(recent_codons, age_weights):
accum += w * CODON_BIAS[c]
return accum
def contact_prob(theta, exposed_codons, pause):
if len(exposed_codons) < 2:
return 0.0
# toy contact proxy: more probable near structured basins and during pause
d1 = np.linalg.norm(angular_delta(theta, mu_helix))
d2 = np.linalg.norm(angular_delta(theta, mu_sheet))
geom = np.exp(-(min(d1, d2) ** 2) / 1.2)
access = (1 - np.exp(-0.8 * pause))
return float(geom * access)
def phi_peptide_v4(theta, codons, translated_count, use_bias=False, Lexp=2):
vp = visible_prefix(codons, translated_count)
et = exposed_tail(codons, translated_count, Lexp=Lexp)
aas = [CODON_TABLE[c] for c in vp] # visible prefix
# weighted by recency
weights = np.linspace(0.35, 1.0, len(aas))
weights = weights / weights.sum()
target = np.zeros(3)
for aa, w in zip(aas, weights):
target[AA_TO_IDX[aa]] += w
# active codon
active = vp[-1]
pause = pause_intensity(active)
# delay bias from pausing
delay_bias = np.array([-0.35 * pause, -0.18 * pause, 0.52 * pause])
# transient codon bias only on exposed recent tail
ages = np.linspace(1.0, 0.45, len(et)) if len(et) > 0 else np.array([])
codon_bias = transient_bias(et, ages, use_bias=use_bias)
E_ex = peptide_expert_energies(theta)
gates = softmax(target + delay_bias + codon_bias - 0.5 * E_ex)
contact = contact_prob(theta, et, pause)
free_energy = (
np.dot(gates, E_ex) +
torsion_energy(theta) -
0.8 * contact +
temperature * conformational_entropy(theta)
)
coh = (
0.6 * structural_coherence(theta) +
0.25 * (1.0 - (-np.sum(gates*np.log(gates+1e-12))/np.log(3))) +
0.15 * contact
)
return coh / (free_energy + C0_peptide), gates, pause, delay_bias, codon_bias, contact, free_energy
def phi_cds_v4(theta, codons, translated_count, use_bias=False, Lexp=2):
vp = visible_prefix(codons, translated_count)
codon_avg = np.mean([phi_codon(c) for c in vp]) if len(vp) else 0.0
pep, gates, pause, delay_bias, codon_bias, contact, free_energy = phi_peptide_v4(
theta, codons, translated_count, use_bias=use_bias, Lexp=Lexp
)
score = 0.50 * codon_avg + 0.40 * pep + 0.10 * contact
return score, gates, pause, delay_bias, codon_bias, contact, free_energy
# -----------------------------
# Stage runner
# -----------------------------
def run_v4(use_bias=False, seed=7, T=360, Lexp=2):
rng = np.random.default_rng(seed)
aa_sequence = ["A", "F", "G"]
codons = ["GCA", "UUU", "GGG"] # initial
logits = {i: np.zeros(len(AA_TO_CODONS[aa_sequence[i]])) for i in range(3)}
theta = np.array([2.25, -2.05], dtype=float)
history = {
"phi": [],
"theta": [],
"translated": [],
"pause": [],
"contact": [],
"free_energy": [],
"delay_bias": [],
"codon_bias": [],
"visible": [],
"policy": {0: [], 1: [], 2: []},
"gates": [],
}
translated_count = 1
phi_prev, *_ = phi_cds_v4(theta, codons, translated_count, use_bias=use_bias, Lexp=Lexp)
cycles_per_codon = T // 3
for t in range(T):
translated_count = min(3, 1 + t // cycles_per_codon)
active_pos = translated_count - 1
aa = aa_sequence[active_pos]
choices = AA_TO_CODONS[aa]
probs = softmax(logits[active_pos])
chosen_idx = rng.choice(len(choices), p=probs)
codons[active_pos] = choices[chosen_idx]
score, gates, pause, delay_bias, codon_bias, contact, free_energy = phi_cds_v4(
theta, codons, translated_count, use_bias=use_bias, Lexp=Lexp
)
grads = peptide_expert_gradients(theta)
drift = np.einsum("k,kj->j", gates, grads)
# active codon controls effective dwell time
dt_eff = dt / TRANSLATION_SPEED[codons[active_pos]]
theta = wrap_angle(
theta - dt_eff * drift + np.sqrt(dt_eff) * noise_scale * rng.normal(size=2)
)
phi_new, *_ = phi_cds_v4(theta, codons, translated_count, use_bias=use_bias, Lexp=Lexp)
reward = phi_new - phi_prev
# synonymous RL update at active position
usefulness = np.array([phi_codon(c) - np.mean([phi_codon(x) for x in choices]) for c in choices])
logits[active_pos] = logits[active_pos] + alpha_rl * reward * usefulness
history["phi"].append(phi_new)
history["theta"].append(theta.copy())
history["translated"].append(translated_count)
history["pause"].append(pause)
history["contact"].append(contact)
history["free_energy"].append(free_energy)
history["delay_bias"].append(delay_bias.copy())
history["codon_bias"].append(codon_bias.copy())
history["visible"].append(tuple(visible_prefix(codons, translated_count)))
history["gates"].append(gates.copy())
for i in range(3):
history["policy"][i].append(softmax(logits[i]))
phi_prev = phi_new
for k, v in history.items():
if k in ("policy", "visible"):
continue
history[k] = np.array(v)
history["final_codons"] = tuple(codons)
history["final_phi"] = float(history["phi"][-1])
history["best_phi"] = float(np.max(history["phi"]))
return history
def main():
base = run_v4(use_bias=False, seed=7)
abl = run_v4(use_bias=True, seed=7)
out_dir = Path(".")
fig1, axes = plt.subplots(3, 1, figsize=(10, 10), sharex=True)
axes[0].plot(base["phi"], label="Base v4")
axes[0].plot(abl["phi"], label="Bias ablation v4")
axes[0].set_ylabel("Phi_CDS")
axes[0].set_title("V4 cotranslational score")
axes[0].legend()
axes[1].plot(base["translated"], label="Translated count")
axes[1].set_ylabel("Visible codons")
axes[1].legend()
axes[2].plot(base["contact"], label="Base contact")
axes[2].plot(abl["contact"], label="Bias contact")
axes[2].set_ylabel("Contact")
axes[2].set_xlabel("Step")
axes[2].legend()
fig1.tight_layout()
fig2, axes = plt.subplots(2, 1, figsize=(10, 7), sharex=True)
axes[0].plot(base["pause"], label="Pause intensity")
axes[0].plot(base["free_energy"], label="Free energy")
axes[0].set_title("Pause and free energy")
axes[0].legend()
axes[1].plot(base["delay_bias"][:,0], label="helix delay bias")
axes[1].plot(base["delay_bias"][:,1], label="sheet delay bias")
axes[1].plot(base["delay_bias"][:,2], label="loop delay bias")
axes[1].plot(abl["codon_bias"][:,0], "--", label="helix codon bias")
axes[1].plot(abl["codon_bias"][:,1], "--", label="sheet codon bias")
axes[1].plot(abl["codon_bias"][:,2], "--", label="loop codon bias")
axes[1].set_title("Delay bias vs transient codon bias")
axes[1].set_xlabel("Step")
axes[1].legend(ncol=2)
fig2.tight_layout()
fig3 = plt.figure(figsize=(8, 6))
ax = fig3.add_subplot(1, 1, 1)
ax.plot(base["theta"][:,0], base["theta"][:,1], label="Base")
ax.plot(abl["theta"][:,0], abl["theta"][:,1], label="Bias ablation")
ax.scatter([mu_helix[0], mu_sheet[0], mu_loop[0]],
[mu_helix[1], mu_sheet[1], mu_loop[1]], marker="x", s=100)
ax.set_title(r"V4 trajectory in $(\phi,\psi)$ space")
ax.set_xlabel(r"$\phi$")
ax.set_ylabel(r"$\psi$")
ax.legend()
fig3.tight_layout()
fig1.savefig(out_dir / "codon_peptide_v4_score.png", dpi=180, bbox_inches="tight")
fig2.savefig(out_dir / "codon_peptide_v4_bias_and_pause.png", dpi=180, bbox_inches="tight")
fig3.savefig(out_dir / "codon_peptide_v4_trajectory.png", dpi=180, bbox_inches="tight")
plt.close(fig1); plt.close(fig2); plt.close(fig3)
summary = f"""V4 cotranslational codon->peptide summary
Base model final codons: {base['final_codons']}
Base final Phi_CDS: {base['final_phi']:.6f}
Base best Phi_CDS: {base['best_phi']:.6f}
Bias ablation final codons: {abl['final_codons']}
Bias final Phi_CDS: {abl['final_phi']:.6f}
Bias best Phi_CDS: {abl['best_phi']:.6f}
Delta final score (bias - base): {abl['final_phi'] - base['final_phi']:.6f}
Delta best score (bias - base): {abl['best_phi'] - base['best_phi']:.6f}
"""
(out_dir / "codon_peptide_v4_summary.txt").write_text(summary)
print(summary)
if __name__ == "__main__":
main()