Spaces:
Sleeping
Sleeping
| """ | |
| Morphic simulation engine. | |
| Generates Gray-Scott reaction-diffusion patterns that self-assemble | |
| based on protein properties — each protein produces a unique visual fingerprint. | |
| """ | |
| from __future__ import annotations | |
| import hashlib, io, math | |
| import numpy as np | |
| from PIL import Image, ImageDraw, ImageFont | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| from matplotlib.colors import LinearSegmentedColormap | |
| # ── Protein-property → Gray-Scott parameter mapping ─────────────────────────── | |
| FUNCTION_PALETTES = { | |
| "enzyme": [(0.02, 0.055), "#00f5ff", "#0a0a2e"], | |
| "receptor": [(0.035, 0.065), "#c084fc", "#0d0520"], | |
| "transporter": [(0.025, 0.06), "#10b981", "#031a0e"], | |
| "structural": [(0.04, 0.063), "#fbbf24", "#1a1000"], | |
| "transcription": [(0.03, 0.057), "#f97316", "#1a0a00"], | |
| "signaling": [(0.022, 0.051), "#3b82f6", "#00020f"], | |
| "unknown": [(0.028, 0.058), "#94a3b8", "#0c0c14"], | |
| } | |
| def _protein_to_gs_params( | |
| uniprot_id: str, | |
| plddt: float, | |
| length: int, | |
| function: str, | |
| ) -> dict: | |
| """Map protein properties to Gray-Scott f/k parameters and palette.""" | |
| palette_key = function.lower() if function.lower() in FUNCTION_PALETTES else "unknown" | |
| (f_base, k_base), fg_hex, bg_hex = FUNCTION_PALETTES[palette_key] | |
| # pLDDT (0–100) shifts f: high confidence → more stable patterns | |
| f = f_base + (plddt / 100 - 0.5) * 0.008 | |
| # Protein length shifts k: longer → finer pattern | |
| k = k_base + min(length / 10000, 0.006) | |
| # Unique seed from UniProt ID | |
| seed = int(hashlib.md5(uniprot_id.encode()).hexdigest()[:8], 16) % 10000 | |
| return {"f": f, "k": k, "fg": fg_hex, "bg": bg_hex, "seed": seed} | |
| def _run_gs(f: float, k: float, seed: int, steps: int = 3000, N: int = 128) -> np.ndarray: | |
| """Run Gray-Scott PDE on an N×N grid, return V concentration.""" | |
| rng = np.random.default_rng(seed) | |
| U = np.ones((N, N), dtype=np.float32) | |
| V = np.zeros((N, N), dtype=np.float32) | |
| # Seed perturbation | |
| cx, cy = N // 2, N // 2 | |
| r = max(4, N // 16) | |
| U[cx - r:cx + r, cy - r:cy + r] = 0.5 | |
| V[cx - r:cx + r, cy - r:cy + r] = 0.25 | |
| V += rng.uniform(0, 0.01, (N, N)).astype(np.float32) | |
| Du, Dv, dt = 0.16, 0.08, 1.0 | |
| for _ in range(steps): | |
| Lu = (np.roll(U, 1, 0) + np.roll(U, -1, 0) + | |
| np.roll(U, 1, 1) + np.roll(U, -1, 1) - 4 * U) | |
| Lv = (np.roll(V, 1, 0) + np.roll(V, -1, 0) + | |
| np.roll(V, 1, 1) + np.roll(V, -1, 1) - 4 * V) | |
| uvv = U * V * V | |
| U += dt * (Du * Lu - uvv + f * (1 - U)) | |
| V += dt * (Dv * Lv + uvv - (f + k) * V) | |
| np.clip(U, 0, 1, out=U) | |
| np.clip(V, 0, 1, out=V) | |
| return V | |
| def _hex_to_rgb(h: str) -> tuple: | |
| h = h.lstrip("#") | |
| return tuple(int(h[i:i+2], 16) / 255.0 for i in (0, 2, 4)) | |
| def generate_morphic_image( | |
| uniprot_id: str, | |
| plddt: float = 75.0, | |
| length: int = 300, | |
| function: str = "unknown", | |
| protein_name: str = "", | |
| width: int = 800, | |
| height: int = 800, | |
| ) -> bytes: | |
| """ | |
| Return PNG bytes of a morphic simulation unique to this protein. | |
| The pattern self-assembles based on protein properties. | |
| """ | |
| params = _protein_to_gs_params(uniprot_id, plddt, length, function) | |
| V = _run_gs(params["f"], params["k"], params["seed"]) | |
| # Build custom colormap from protein palette | |
| bg = _hex_to_rgb(params["bg"]) | |
| fg = _hex_to_rgb(params["fg"]) | |
| mid = tuple(min(1.0, c * 1.6) for c in fg) | |
| cmap = LinearSegmentedColormap.from_list( | |
| "protein", [bg, params["bg"], fg, mid, "#ffffff"], N=512 | |
| ) | |
| fig, ax = plt.subplots(figsize=(width / 100, height / 100), dpi=100) | |
| fig.patch.set_facecolor(params["bg"]) | |
| ax.set_facecolor(params["bg"]) | |
| ax.imshow(V, cmap=cmap, interpolation="bilinear", aspect="auto", vmin=0, vmax=V.max()) | |
| ax.axis("off") | |
| # Overlay protein ID and pLDDT badge | |
| label = f"{uniprot_id} · pLDDT {plddt:.1f} · {length} aa" | |
| ax.text(0.5, 0.03, label, transform=ax.transAxes, color=params["fg"], | |
| fontsize=10, ha="center", va="bottom", | |
| fontfamily="monospace", alpha=0.85) | |
| if protein_name: | |
| ax.text(0.5, 0.96, protein_name, transform=ax.transAxes, color="#ffffff", | |
| fontsize=13, ha="center", va="top", fontweight="bold", alpha=0.9) | |
| buf = io.BytesIO() | |
| plt.savefig(buf, format="png", bbox_inches="tight", pad_inches=0, | |
| facecolor=params["bg"]) | |
| plt.close(fig) | |
| buf.seek(0) | |
| return buf.read() | |
| def generate_confidence_heatmap( | |
| residue_scores: list[float], | |
| uniprot_id: str, | |
| protein_name: str = "", | |
| width: int = 900, | |
| height: int = 280, | |
| ) -> bytes: | |
| """Per-residue pLDDT heatmap with adaptive colour coding.""" | |
| arr = np.array(residue_scores, dtype=np.float32).reshape(1, -1) | |
| cmap = LinearSegmentedColormap.from_list( | |
| "plddt", ["#f97316", "#eab308", "#22c55e", "#1d4ed8"], N=256 | |
| ) | |
| fig, ax = plt.subplots(figsize=(width / 100, height / 100), dpi=100) | |
| fig.patch.set_facecolor("#020817") | |
| ax.set_facecolor("#020817") | |
| im = ax.imshow(arr, cmap=cmap, aspect="auto", vmin=0, vmax=100) | |
| ax.set_yticks([]) | |
| ax.tick_params(colors="#64748b", labelsize=8) | |
| ax.set_xlabel("Residue index", color="#64748b", fontsize=9) | |
| title = f"{protein_name} ({uniprot_id}) — per-residue pLDDT" if protein_name else f"{uniprot_id} — per-residue pLDDT" | |
| ax.set_title(title, color="#94a3b8", fontsize=10, pad=8) | |
| plt.colorbar(im, ax=ax, orientation="horizontal", fraction=0.04, pad=0.18, | |
| label="pLDDT confidence").ax.xaxis.label.set_color("#64748b") | |
| buf = io.BytesIO() | |
| plt.savefig(buf, format="png", bbox_inches="tight", facecolor="#020817") | |
| plt.close(fig) | |
| buf.seek(0) | |
| return buf.read() | |