ProfRick/EC-Coupling
0
1import gradio as gr2import matplotlib.pyplot as plt3import numpy as np4from io import BytesIO5from PIL import Image6from matplotlib.patches import Rectangle, Circle, FancyArrowPatch, PathPatch7from matplotlib.path import Path8 9# ================== Content (transcript language) ==================10STEPS = [11 dict(12 title="Step 1: Motor neuron → NMJ",13 where=("A motor neuron goes directly from the spinal cord to the skeletal muscle. "14 "The action potential travels down the axon to the axon terminal, calcium channels open, "15 "and acetylcholine is released into the synapse."),16 question="When the motor neuron releases acetylcholine, what happens next?",17 options=[18 "The muscle relaxes immediately.",19 "Acetylcholine moves across the synapse and binds to receptors on the muscle cell membrane.",20 "Calcium leaves the muscle fiber."21 ],22 correct=1,23 visual="neuron"24 ),25 dict(26 title="Step 2: Motor end plate (ligand-gated channels)",27 where=("Acetylcholine moves to the motor end plate and binds nicotinic acetylcholine receptors. "28 "These are ligand-gated channels that open when acetylcholine binds."),29 question="When acetylcholine binds to its receptor at the motor end plate, which ion moves into the muscle cell?",30 options=[31 "Sodium moves into the cell.",32 "Potassium moves into the cell.",33 "Calcium leaves the sarcoplasmic reticulum."34 ],35 correct=0,36 visual="nmj"37 ),38 dict(39 title="Step 3: Threshold → action potential on sarcolemma",40 where=("Sodium entry changes the voltage until threshold potential is reached. "41 "Voltage-gated channels open and an action potential travels along the sarcolemma."),42 question="What allows the electrical signal to travel quickly across the muscle cell membrane?",43 options=[44 "Voltage-gated sodium channels opening along the sarcolemma.",45 "Continuous acetylcholine release.",46 "ATP from mitochondria."47 ],48 correct=0,49 visual="sarcolemma"50 ),51 dict(52 title="Step 4: T-tubule voltage sensing (DHP)",53 where=("The sarcolemma dives into the cell as the T-tubule. "54 "When the action potential reaches this area, the DHP receptor senses the voltage change."),55 question="When the DHP receptor senses the voltage change, what does it do?",56 options=[57 "It moves the ryanidine receptor so calcium can leave the sarcoplasmic reticulum.",58 "It brings more sodium into the cell.",59 "It breaks down ATP."60 ],61 correct=0,62 visual="t_tubule"63 ),64 dict(65 title="Step 5: Calcium leaves the SR",66 where=("The ryanidine receptor opens. Calcium moves out of the sarcoplasmic reticulum into the cytoplasm "67 "following a concentration gradient (high in SR → lower in cytoplasm)."),68 question="Why does calcium move out of the sarcoplasmic reticulum?",69 options=[70 "There is a high concentration of calcium inside the SR and a lower concentration in the cytoplasm.",71 "Calcium is pushed out by sodium.",72 "It is actively pumped out using ATP."73 ],74 correct=0,75 visual="sr_release"76 ),77 dict(78 title="Step 6: Troponin → tropomyosin moves",79 where=("Once calcium is in the sarcoplasm, it binds to troponin, which causes tropomyosin to move away "80 "from the binding sites on actin."),81 question="What is exposed when tropomyosin moves?",82 options=[83 "The myosin binding sites on actin.",84 "The ATP-binding sites on myosin.",85 "The calcium pumps on the sarcoplasmic reticulum."86 ],87 correct=0,88 visual="thin_filament"89 ),90 dict(91 title="Step 7: Cross-bridge cycling (ATP’s role)",92 where=("Myosin binds to actin. ATP causes detachment; ATP hydrolysis re-cocks the myosin head. "93 "Without ATP, myosin stays attached and the muscle is stiff."),94 question="If there is no ATP available, what happens?",95 options=[96 "The myosin remains attached to actin, causing stiffness.",97 "The muscle continues to contract rapidly.",98 "The SR releases more calcium."99 ],100 correct=0,101 visual="crossbridge"102 ),103 dict(104 title="Step 8: Relaxation",105 where=("When the excitatory signal stops, acetylcholine esterase breaks down acetylcholine. "106 "Calcium is pumped back into the sarcoplasmic reticulum, and tropomyosin moves back over the binding sites."),107 question="What two actions cause relaxation?",108 options=[109 "Acetylcholine breakdown and calcium re-uptake into the sarcoplasmic reticulum.",110 "More acetylcholine release and ATP depletion.",111 "Sodium leaving the muscle fiber."112 ],113 correct=0,114 visual="relax"115 ),116]117 118NODES = [119 "ACh released at NMJ",120 "ACh binds nicotinic receptor",121 "Na⁺ entry → threshold → sarcolemma AP",122 "T-tubule DHP senses voltage",123 "RYR opens; Ca²⁺ leaves SR",124 "Ca²⁺ binds troponin; tropomyosin moves",125 "Cross-bridge cycling (ATP present)",126 "Relaxation: AChE + SERCA"127]128 129EDGES = {130 "ACh released at NMJ": ["ACh binds nicotinic receptor"],131 "ACh binds nicotinic receptor": ["Na⁺ entry → threshold → sarcolemma AP"],132 "Na⁺ entry → threshold → sarcolemma AP": ["T-tubule DHP senses voltage"],133 "T-tubule DHP senses voltage": ["RYR opens; Ca²⁺ leaves SR"],134 "RYR opens; Ca²⁺ leaves SR": ["Ca²⁺ binds troponin; tropomyosin moves"],135 "Ca²⁺ binds troponin; tropomyosin moves": ["Cross-bridge cycling (ATP present)"],136 "Cross-bridge cycling (ATP present)": ["Relaxation: AChE + SERCA"],137 "Relaxation: AChE + SERCA": []138}139 140# ================== Visual style helpers (HD schematics) ==================141PALETTE = {142 "membrane": "#222831",143 "t_tubule": "#3e8ed0",144 "sr": "#f59e0b",145 "channel": "#6b7280",146 "receptor": "#7c3aed",147 "vesicle": "#22c55e",148 "ach": "#16a34a",149 "na": "#2563eb",150 "ca": "#ef4444",151 "text": "#111827",152}153 154def _ion(ax, x, y, label, color, r=0.035):155 ax.add_patch(Circle((x, y), r, facecolor=color, edgecolor="white", linewidth=1.2))156 ax.text(x, y, label, ha="center", va="center", color="white", fontsize=9, fontweight="bold")157 158def _membrane(ax, x0=0.05, x1=0.95, y=0.5, thickness=0.03):159 ax.add_patch(Rectangle((x0, y - thickness/2), x1-x0, thickness,160 facecolor=PALETTE["membrane"], alpha=0.15, edgecolor=PALETTE["membrane"]))161 162def _t_tubule(ax, x=0.5, y0=0.12, y1=0.88, w=0.06):163 ax.add_patch(Rectangle((x-w/2, y0), w, y1-y0, facecolor=PALETTE["t_tubule"], alpha=0.12, edgecolor=PALETTE["t_tubule"]))164 ax.text(x, y1+0.05, "T-tubule", ha="center", va="bottom", fontsize=11, color=PALETTE["t_tubule"])165 166def _sr(ax, x0=0.3, x1=0.7, y=0.18, h=0.06, label=True):167 ax.add_patch(Rectangle((x0, y), x1-x0, h, facecolor=PALETTE["sr"], alpha=0.10, edgecolor=PALETTE["sr"]))168 if label:169 ax.text((x0+x1)/2, y-0.03, "SR", ha="center", va="top", fontsize=11, color=PALETTE["sr"])170 171def _nicotinic_receptor(ax, x=0.8, y=0.5, w=0.06, h=0.04):172 # dimer-like shapes173 ax.add_patch(Rectangle((x-w, y-h/2), w, h, facecolor=PALETTE["receptor"], alpha=0.25, edgecolor=PALETTE["receptor"]))174 ax.add_patch(Rectangle((x, y-h/2), w, h, facecolor=PALETTE["receptor"], alpha=0.25, edgecolor=PALETTE["receptor"]))175 ax.text(x, y-0.07, "nicotinic AChR", ha="center", va="top", fontsize=9, color=PALETTE["receptor"])176 177def _channel(ax, x, y, open_state=True, label=None):178 h = 0.06179 ax.add_patch(Rectangle((x-0.01, y-h/2), 0.02, h,180 facecolor=PALETTE["channel"], alpha=0.20, edgecolor=PALETTE["channel"]))181 if open_state:182 ax.add_line(plt.Line2D([x-0.007, x+0.007], [y-h/2+0.01, y+h/2-0.01], color=PALETTE["channel"], linewidth=2))183 else:184 ax.add_line(plt.Line2D([x-0.01, x+0.01], [y+0.03, y-0.03], color=PALETTE["channel"], linewidth=2))185 if label:186 ax.text(x, y+0.055, label, ha="center", va="bottom", fontsize=9, color=PALETTE["channel"])187 188def _arrow(ax, x0, y0, x1, y1, color="#111", width=2.4, curve=0.0, label=None, label_pos=0.5):189 if curve == 0:190 arrow = FancyArrowPatch((x0, y0), (x1, y1),191 arrowstyle="-|>", mutation_scale=12, linewidth=width, color=color)192 else:193 verts = [(x0, y0), ((x0+x1)/2, (y0+y1)/2 + curve), (x1, y1)]194 codes = [Path.MOVETO, Path.CURVE3, Path.CURVE3]195 path = Path(verts, codes)196 arrow = FancyArrowPatch(path=path, arrowstyle="-|>", mutation_scale=12, linewidth=width, color=color)197 ax.add_patch(arrow)198 if label:199 mx = x0 + (x1-x0)*label_pos200 my = y0 + (y1-y0)*label_pos + (curve if curve else 0)201 ax.text(mx, my, label, fontsize=10, color=color, fontweight="bold",202 ha="center", va="bottom")203 204def _vesicle(ax, x, y, r=0.035, n_ach=3):205 ax.add_patch(Circle((x, y), r, facecolor=PALETTE["vesicle"], edgecolor="#136f45", linewidth=1))206 for k in range(n_ach):207 _ion(ax, x + (k-1)*(r*0.45), y, "ACh", PALETTE["ach"], r=0.017)208 209def _legend(ax, items):210 # items: list of (color, label)211 x, y = 0.05, 0.05212 for i, (c, t) in enumerate(items):213 ax.add_patch(Rectangle((x, y+i*0.04), 0.02, 0.02, facecolor=c, edgecolor=c))214 ax.text(x+0.025, y+i*0.04+0.01, t, va="center", ha="left", fontsize=9, color=PALETTE["text"])215 216def draw_visual(kind: str, detail: str = "HD") -> np.ndarray:217 """218 detail: 'Basic' or 'HD'219 """220 # Larger canvas for HD; use antialiasing and facecolors221 figsize = (7.2, 4.0) if detail == "HD" else (6, 3)222 fig, ax = plt.subplots(figsize=figsize)223 ax.set_xlim(0, 1)224 ax.set_ylim(0, 1)225 ax.axis("off")226 227 # Common elements for several views228 if kind in {"neuron", "nmj", "sarcolemma"}:229 _membrane(ax, y=0.5, thickness=0.035)230 231 if kind == "neuron":232 ax.text(0.06, 0.86, "Motor neuron → axon terminal (NMJ)", color=PALETTE["text"], fontsize=12, fontweight="bold")233 # Axon234 ax.add_line(plt.Line2D([0.06, 0.38], [0.5, 0.5], color=PALETTE["membrane"], linewidth=5))235 # Vesicles at terminal236 for dx in [0.44, 0.50, 0.56]:237 _vesicle(ax, dx, 0.62, r=0.035 if detail=="HD" else 0.03)238 # ACh diffusion to membrane239 for dx in [0.48, 0.54]:240 _arrow(ax, dx, 0.60, dx+0.10, 0.52, color=PALETTE["ach"], width=2, curve=-0.05)241 ax.text(0.72, 0.44, "ACh in synapse", color=PALETTE["ach"], fontsize=10)242 _legend(ax, [(PALETTE["vesicle"], "Vesicle"), (PALETTE["ach"], "Acetylcholine")])243 244 elif kind == "nmj":245 ax.text(0.06, 0.9, "Motor end plate (nicotinic receptors open with ACh)", fontsize=12, fontweight="bold", color=PALETTE["text"])246 _nicotinic_receptor(ax, x=0.78, y=0.5)247 # ACh arrows from cleft to receptor248 for dy in [-0.03, 0.0, 0.03]:249 _arrow(ax, 0.62, 0.55+dy, 0.75, 0.50+dy, color=PALETTE["ach"], width=2, curve=-0.02)250 # Na+ entry (if open)251 _channel(ax, 0.78, 0.5, open_state=True, label="Na⁺ channel")252 for k in range(4):253 _ion(ax, 0.80 + 0.03*k, 0.58 + 0.02*np.sin(k), "Na⁺", PALETTE["na"])254 _arrow(ax, 0.80 + 0.03*k, 0.58 + 0.02*np.sin(k), 0.78, 0.52, color=PALETTE["na"], width=1.8, curve=-0.01, label=None)255 256 elif kind == "sarcolemma":257 ax.text(0.5, 0.9, "Sarcolemma AP via voltage-gated Na⁺", ha="center", fontsize=12, fontweight="bold", color=PALETTE["text"])258 # A wave of open channels (dots) and Na influx259 for xi in np.linspace(0.15, 0.85, 7):260 _channel(ax, xi, 0.5, open_state=True)261 _ion(ax, xi+0.03, 0.62, "Na⁺", PALETTE["na"])262 _arrow(ax, xi+0.03, 0.62, xi, 0.52, color=PALETTE["na"], width=1.6, curve=-0.015)263 264 elif kind == "t_tubule":265 _t_tubule(ax, x=0.5, y0=0.12, y1=0.88)266 _sr(ax, x0=0.30, x1=0.70, y=0.12, h=0.08, label=True)267 ax.text(0.5, 0.94, "DHP senses voltage → moves RYR", ha="center", fontsize=12, fontweight="bold", color=PALETTE["text"])268 # DHP (on T-tubule wall)269 ax.add_patch(Rectangle((0.48, 0.50-0.04), 0.04, 0.08, facecolor=PALETTE["receptor"], alpha=0.25, edgecolor=PALETTE["receptor"]))270 ax.text(0.50, 0.46, "DHP", ha="center", va="top", fontsize=9, color=PALETTE["receptor"])271 # RYR (on SR)272 ax.add_patch(Rectangle((0.42, 0.12), 0.16, 0.06, facecolor=PALETTE["sr"], alpha=0.18, edgecolor=PALETTE["sr"]))273 ax.text(0.50, 0.19, "RYR", ha="center", va="center", fontsize=10, color=PALETTE["sr"])274 # Link arrow275 _arrow(ax, 0.50, 0.50, 0.50, 0.18, color="#444", width=2, label="Coupling", label_pos=0.55)276 277 elif kind == "sr_release":278 _t_tubule(ax, x=0.5, y0=0.60, y1=0.95)279 _sr(ax, x0=0.20, x1=0.80, y=0.18, h=0.10, label=True)280 ax.text(0.5, 0.56, "RYR opens; Ca²⁺ leaves SR (high → lower)", ha="center", fontsize=12, fontweight="bold", color=PALETTE["text"])281 # Ca arrows SR -> cytosol282 for x in np.linspace(0.28, 0.72, 5):283 _ion(ax, x, 0.25, "Ca²⁺", PALETTE["ca"])284 _arrow(ax, x, 0.25, x, 0.45, color=PALETTE["ca"], width=2, curve=0.0)285 286 elif kind == "thin_filament":287 ax.text(0.5, 0.9, "Ca²⁺ binds troponin → Tropomyosin moves", ha="center", fontsize=12, fontweight="bold", color=PALETTE["text"])288 # Actin cable289 ax.add_patch(Rectangle((0.15, 0.45), 0.70, 0.05, facecolor="#9ca3af", edgecolor="#6b7280"))290 # Binding sites reveal (dots)291 for x in np.linspace(0.18, 0.80, 7):292 ax.add_patch(Circle((x, 0.475), 0.01, facecolor="#374151"))293 # Ca icons near troponin294 for x in [0.30, 0.50, 0.70]:295 _ion(ax, x, 0.58, "Ca²⁺", PALETTE["ca"])296 _arrow(ax, x, 0.58, x, 0.48, color=PALETTE["ca"], width=2)297 298 elif kind == "crossbridge":299 ax.text(0.5, 0.90, "Cross-bridge cycling (ATP detaches; hydrolysis re-cocks)", ha="center",300 fontsize=12, fontweight="bold", color=PALETTE["text"])301 # Actin (top) and myosin (bottom)302 ax.add_patch(Rectangle((0.12, 0.62), 0.76, 0.04, facecolor="#9ca3af", edgecolor="#6b7280"))303 ax.add_patch(Rectangle((0.12, 0.36), 0.76, 0.04, facecolor="#6b7280", edgecolor="#374151"))304 # Heads and binding305 for x in np.linspace(0.18, 0.82, 5):306 _arrow(ax, x, 0.40, x, 0.60, color="#374151", width=2)307 ax.text(0.50, 0.50, "ATP binds → detachment\nATP hydrolysis → re-cock", ha="center", va="center",308 fontsize=10, color=PALETTE["text"])309 310 elif kind == "relax":311 ax.text(0.5, 0.90, "Relaxation: ACh broken down; Ca²⁺ pumped back to SR", ha="center", fontsize=12, fontweight="bold", color=PALETTE["text"])312 _sr(ax, x0=0.20, x1=0.80, y=0.70, h=0.08, label=True)313 # Ca back to SR314 for x in np.linspace(0.28, 0.72, 5):315 _ion(ax, x, 0.42, "Ca²⁺", PALETTE["ca"])316 _arrow(ax, x, 0.45, x, 0.74, color=PALETTE["ca"], width=2)317 ax.text(0.20, 0.30, "AChE breaks down ACh", fontsize=10, color=PALETTE["ach"])318 _legend(ax, [(PALETTE["ca"], "Calcium"), (PALETTE["ach"], "Acetylcholine")])319 320 # Render to array321 buf = BytesIO()322 fig.tight_layout()323 fig.savefig(buf, format="png", dpi=(180 if detail == "HD" else 110), bbox_inches="tight")324 plt.close(fig)325 buf.seek(0)326 img = Image.open(buf).convert("RGB")327 return np.array(img)328 329# ================== Step Trainer logic (now passes detail level) ==================330def render_step(i:int, detail:str):331 i = int(i)332 s = STEPS[i]333 img = draw_visual(s["visual"], detail=detail)334 return (f"### {s['title']}",335 s["where"],336 img,337 f"**{s['question']}**",338 gr.update(choices=s["options"], value=s["options"][0]),339 "", # feedback340 i, # state341 i)342 343def submit_step(i:int, picked:str, detail:str):344 i = int(i)345 s = STEPS[i]346 idx = s["options"].index(picked) if picked in s["options"] else -1347 if idx == s["correct"]:348 if i < len(STEPS)-1:349 i += 1350 fb = "✅ **Correct. Advancing…**"351 else:352 fb = "✅ **Done. Relaxation complete.**"353 else:354 i = 0355 fb = "❌ **Incorrect. Returning to Step 1.**"356 title, where, img, q, choices, _, _, _ = render_step(i, detail)357 return title, where, img, q, choices, fb, i, i358 359def restart_step(_i:int, detail:str):360 title, where, img, q, choices, _, i, p = render_step(0, detail)361 return title, where, img, q, choices, "Restarted.", i, p362 363# ================== Failure-Point ==================364def failure_check(fails, guess):365 failed_idx = sorted([NODES.index(f) for f in (fails or [])]) if fails else []366 lines = []367 if failed_idx:368 stop = failed_idx[0]369 for j, lab in enumerate(NODES):370 ok = j < stop371 lines.append(("✅ " if ok else "⛔ ") + lab)372 if not ok:373 break374 fb = "✅ **Correct: first failed step located.**" if (guess and NODES.index(guess)==stop) else "❌ **Not quite—identify the FIRST failed step.**"375 else:376 lines = ["✅ " + l for l in NODES]377 lines.append("(No failures set — full propagation to relaxation.)")378 fb = "No failures toggled."379 return "```\n" + "\n".join(lines) + "\n```", fb380 381# ================== Sandbox ==================382def sandbox_metrics(Na_out, Na_in, Ca_sr, Ca_cyto, ATP):383 na_drive = max(0.0, (Na_out - Na_in) / max(1.0, Na_out))384 ap_prob = min(1.0, na_drive * 1.5)385 dhp_ok = ap_prob386 ca_drive = max(0.0, (Ca_sr - Ca_cyto) / max(1.0, Ca_sr))387 ca_rel = dhp_ok * ca_drive388 xbridges = min(1.0, ca_rel * (0.5 + 0.5*min(1.0, ATP)))389 relax_ok = min(1.0, ATP) * 0.7 + (1.0 - ca_rel) * 0.3390 return dict(ap_prob=ap_prob, ca_release=ca_rel, crossbridge=xbridges, relax_ok=relax_ok)391 392def sandbox_plot(Na_out, Na_in, Ca_sr, Ca_cyto, ATP):393 vals = sandbox_metrics(Na_out, Na_in, Ca_sr, Ca_cyto, ATP)394 fig, ax = plt.subplots(figsize=(6,3))395 keys = ["ap_prob","ca_release","crossbridge","relax_ok"]396 ax.bar(keys, [vals[k] for k in keys], color=[PALETTE["na"], PALETTE["ca"], "#374151", "#10b981"])397 ax.set_ylim(0,1); ax.set_title("Predicted behaviors (0–1)")398 for i, k in enumerate(keys):399 ax.text(i, vals[k]+0.03, f"{vals[k]:.2f}", ha="center", va="bottom", fontsize=9)400 buf = BytesIO(); fig.tight_layout(); fig.savefig(buf, format="png", bbox_inches="tight", dpi=150); plt.close(fig)401 buf.seek(0); img = Image.open(buf).convert("RGB")402 return np.array(img)403 404# ================== Causality Builder ==================405def chain_add(chain, pick):406 import json407 chain = json.loads(chain)408 remaining = [n for n in NODES if n not in chain]409 if not remaining:410 return (gr.update(value=chain, interactive=False),411 "Chain complete.",412 gr.update(choices=[], interactive=False),413 " → ".join(chain))414 if pick is None:415 return (gr.update(value=chain),416 "Pick a next event.",417 gr.update(),418 " → ".join(chain))419 ok = pick in EDGES.get(chain[-1], [])420 if ok:421 chain.append(pick)422 remaining = [n for n in NODES if n not in chain]423 msg = "✅ **Link accepted.**"424 else:425 msg = "❌ **That event doesn’t logically follow. Try a different next step.**"426 return (gr.update(value=chain),427 msg,428 gr.update(choices=remaining, value=(remaining[0] if remaining else None), interactive=bool(remaining)),429 " → ".join(chain))430 431def chain_reset():432 import json433 chain = [NODES[0]]434 remaining = [n for n in NODES if n not in chain]435 return (gr.update(value=chain),436 "Reset.",437 gr.update(choices=remaining, value=remaining[0] if remaining else None, interactive=bool(remaining)),438 " → ".join(chain))439 440# ================== Fatigue Lab ==================441def simulate_fatigue(tmax=60, dt=0.5, atp_init=1.0, aerobic=0.3, anaerobic=0.2, load=0.5, serca_load=0.3):442 n=int(tmax/dt)+1; t=np.linspace(0,tmax,n); atp=np.zeros(n); atp[0]=atp_init; rigor=np.zeros(n,dtype=bool)443 for i in range(1,n):444 cons = load*0.4 + serca_load*0.25445 prod = aerobic*0.2 + anaerobic*0.15446 atp[i] = np.clip(atp[i-1] + dt*(prod - cons), 0, 1.2)447 rigor[i] = atp[i] < 0.1448 fig, ax = plt.subplots(1,2, figsize=(8,3))449 ax[0].plot(t, atp, color="#111827"); ax[0].set_xlabel("time (s)"); ax[0].set_ylabel("ATP (a.u.)"); ax[0].set_title("ATP dynamics")450 ax[1].bar(["rigor fraction"], [rigor.mean()], color="#ef4444"); ax[1].set_ylim(0,1); ax[1].set_title("Rigor")451 buf = BytesIO(); fig.tight_layout(); fig.savefig(buf, format="png", bbox_inches="tight", dpi=140); plt.close(fig)452 buf.seek(0); img = Image.open(buf).convert("RGB")453 msg = "Low ATP periods → stiffness (myosin remains attached)." if rigor.mean()>0 else "No stiffness expected (ATP maintained)."454 return np.array(img), f"Estimated rigor fraction: {rigor.mean():.2f}\n{msg}"455 456# ================== Passport Analyzer ==================457CORE_CONCEPTS = {458 "Flow down gradients": ["gradient","concentration","moves from high to low","diffuse","diffusion"],459 "Cell-to-cell communication": ["neurotransmitter","acetylcholine","receptor","synapse","binds"],460 "Structure–function": ["structure","function","troponin","tropomyosin","binding site","receptor opens"],461 "Energy flow": ["ATP","ADP","Pi","hydrolysis","energy"],462 "Interdependence": ["depends","linked","together","if/then","cascade","pathway"]463}464def passport_analyze(text):465 t = (text or "").lower()466 counts = {k: sum(t.count(w) for w in ws) for k,ws in CORE_CONCEPTS.items()}467 keys = list(counts.keys()); vals = [counts[k] for k in keys]468 fig, ax = plt.subplots(figsize=(6,3)); ax.bar(keys, vals, color="#3e8ed0"); ax.set_title("Core Concept mentions"); ax.tick_params(axis='x', rotation=30)469 for i,k in enumerate(keys):470 ax.text(i, vals[i]+0.05, str(vals[i]), ha="center")471 buf = BytesIO(); fig.tight_layout(); fig.savefig(buf, format="png", bbox_inches="tight", dpi=140); plt.close(fig)472 buf.seek(0); img = Image.open(buf).convert("RGB")473 weak = [k for k in keys if counts[k]==0]474 note = ("Consider adding explicit references to: " + ", ".join(weak)) if weak else "Balanced coverage detected."475 import json476 return np.array(img), json.dumps(counts, indent=2) + "\n\n" + note477 478# ================== UI ==================479with gr.Blocks(title="EC Coupling Suite") as demo:480 gr.Markdown("# EC Coupling Learning Suite (Transcript Language)")481 482 with gr.Row():483 detail_picker = gr.Radio(choices=["Basic", "HD"], value="HD", label="Visual detail")484 485 with gr.Tabs():486 # Step Trainer487 with gr.Tab("Step Trainer"):488 step_state = gr.State(0)489 title = gr.Markdown()490 where = gr.Markdown()491 img = gr.Image(label="Where are we?")492 q = gr.Markdown()493 choice = gr.Radio(choices=[], label="Predict what happens next")494 submit = gr.Button("Submit", variant="primary")495 restart = gr.Button("Restart (Step 1)")496 fb = gr.Markdown()497 prog = gr.Slider(0, len(STEPS)-1, value=0, step=1, interactive=False, label="Progress")498 499 demo.load(lambda d: render_step(0, d), inputs=[detail_picker],500 outputs=[title, where, img, q, choice, fb, step_state, prog])501 502 submit.click(submit_step, [step_state, choice, detail_picker],503 [title, where, img, q, choice, fb, step_state, prog])504 restart.click(restart_step, [step_state, detail_picker],505 [title, where, img, q, choice, fb, step_state, prog])506 507 # Failure-Point508 with gr.Tab("Failure-Point"):509 gr.Markdown("Toggle failures and diagnose the **first** failed step.")510 fails = gr.CheckboxGroup(choices=NODES, label="Failures")511 guess = gr.Dropdown(choices=NODES, label="Your diagnosis")512 check = gr.Button("Test & Check", variant="primary")513 log = gr.Markdown()514 verdict = gr.Markdown()515 check.click(failure_check, [fails, guess], [log, verdict])516 517 # Sandbox518 with gr.Tab("Sandbox"):519 gr.Markdown("Adjust gradients and ATP; observe predicted behaviors (heuristic).")520 Na_out = gr.Slider(10, 160, value=140, step=1, label='[Na⁺] outside')521 Na_in = gr.Slider(0, 50, value=15, step=1, label='[Na⁺] inside')522 Ca_sr = gr.Slider(0.1, 10.0, value=3.0, step=0.1, label='[Ca²⁺] SR')523 Ca_c = gr.Slider(0.0, 1.0, value=0.1, step=0.01, label='[Ca²⁺] cytoplasm')524 ATP = gr.Slider(0.0, 1.0, value=0.8, step=0.01, label='ATP (0–1)')525 sb_img = gr.Image(label="Predicted bars")526 for w in [Na_out, Na_in, Ca_sr, Ca_c, ATP]:527 w.change(sandbox_plot, [Na_out, Na_in, Ca_sr, Ca_c, ATP], [sb_img])528 sb_img.value = sandbox_plot(Na_out.value, Na_in.value, Ca_sr.value, Ca_c.value, ATP.value)529 530 # Causality531 with gr.Tab("Causality"):532 gr.Markdown("Build a valid chain; logic is checked at each link.")533 import json534 chain_state = gr.State(json.dumps([NODES[0]]))535 chain_text = gr.Markdown(" → ".join([NODES[0]]))536 next_pick = gr.Dropdown(choices=[n for n in NODES if n != NODES[0]], label="Next event")537 add = gr.Button("Add link", variant="primary")538 reset = gr.Button("Reset")539 fb2 = gr.Markdown()540 add.click(chain_add, [chain_state, next_pick], [chain_state, fb2, next_pick, chain_text])541 reset.click(chain_reset, [], [chain_state, fb2, next_pick, chain_text])542 543 # Fatigue544 with gr.Tab("Fatigue"):545 gr.Markdown("Adjust ATP supply/demand; see ATP curve and rigor fraction.")546 aerobic = gr.Slider(0,1,value=0.4,step=0.01,label="Aerobic supply")547 anaer = gr.Slider(0,1,value=0.3,step=0.01,label="Anaerobic supply")548 work = gr.Slider(0,1,value=0.6,step=0.01,label="Mechanical load")549 serca = gr.Slider(0,1,value=0.4,step=0.01,label="SERCA load")550 dur = gr.Slider(10,180,value=90,step=1,label="Duration (s)")551 ftg_img = gr.Image(label="ATP & Rigor")552 ftg_txt = gr.Markdown()553 554 def ftg_update(dur_val, aer, anr, load, sl):555 return simulate_fatigue(dur_val, 0.5, 1.0, aer, anr, load, sl)556 557 for w in [aerobic, anaer, work, serca, dur]:558 w.change(ftg_update, [dur, aerobic, anaer, work, serca], [ftg_img, ftg_txt])559 560 img0, txt0 = simulate_fatigue(90, 0.5, 1.0, 0.4, 0.3, 0.6, 0.4)561 ftg_img.value, ftg_txt.value = img0, txt0562 563 # Passport564 with gr.Tab("Passport"):565 gr.Markdown("Paste your notes; see Core Concept emphasis.")566 ta = gr.Textbox(lines=8, label="Notes / reflection")567 pass_img = gr.Image(label="Concept counts")568 pass_txt = gr.Markdown()569 run = gr.Button("Analyze", variant="primary")570 run.click(passport_analyze, [ta], [pass_img, pass_txt])571 572demo.launch()573 